webpack入门到优化

一、为什么使用webpack,webpack的优点是什么?

  • 专注于处理模块化项目,能做到 开箱即用、一步到位
  • 可通过Plugin扩展,完整好用又不失灵活;
  • 使用场景不局限与web开发;
  • 社区庞大活跃,经常引入紧跟时代发展的新特性,能够为大数场景找到开源扩展;
  • 良好的开发体验;

二、webpack核心概念

  • Entry: 入口,webpack执行构建的第一步将从Entry开始,可抽象成输入。
  • Module: 模块,在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。
  • Chunk:代码块,一个Chunk有多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于将模块的原内容按照需求转换成新内容
  • Plugin:扩展插件,在Webpack构建流程中的特定时机注入扩展逻辑,来改变构建结果或做成我们想要的事情。

三、流程概况

webpack的运行流程是一个串行的过程,从启动到结束会依次执行一下流程。

  • 初始化参数: 从配置文件和Shell语句中读取与合并参数,得出最终的参数。
  • 开始编译: 用上一步得到参数初始化comilper对象,加载所有配置的插件,通过执行对象的run方法开始执行编译。
  • 确定入口:根据配置中的entry找出所有入口文件。
  • 编译模块:从入口文件出发,调用所有配置的Loader对模块进行编译,在找出该模块依赖的模块,在递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  • 完成模块编译:在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容及他们之间的依赖关系。
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再将每个chunk转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会。
  • 输出完成:在确定好输出内容后,在根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。

四、基本配置

1)entry 入口
 // entry 表示 入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  // 类型可以是 string | object | array   
  entry: './app/entry', // 只有1个入口,入口只有1个文件
  entry: ['./app/entry1', './app/entry2'], // 只有1个入口,入口有2个文件
  entry: { // 有2个入口
    a: './app/entry-a',
    b: ['./app/entry-b1', './app/entry-b2']
  },
   // 项目实战
   entry: {client: resolve('../src/client.js'),},
   ?输出一个什么样的js文件
   entry: resolve('../src/client.js'),
   ?输出一个什么样的js文件
2)output

filename & chunkFilename

变量名 含义
id Chunk的唯一标识,从0开始
name Chunk的名称
hash Chunk的唯一标识的Hash值
chunkhash Chunk内容的Hash值
output: {
path: resolve('../dist'),
filename: dev? '[name].bundle.js' : '[name].[chunkhash:4].js',
chunkFilename: dev ? 'chunks/[name].js' : 'chunks/[name].[chunkhash:4].js',
    },

publicPath

  // 如何输出结果:在 Webpack 经过一系列处理后,如何输出最终想要的代码。
  output: {
    // 输出文件存放的目录,必须是 string 类型的绝对路径。
    path: path.resolve(__dirname, 'dist'),
    // 发布到线上的所有资源的 URL 前缀,string 类型
    publicPath: '/assets/', // 放到指定目录下
    publicPath: '', // 放到根目录下
    publicPath: 'https://cdn.example.com/', // 放到 CDN 上去
  },
3)Module
Loader配置

rules配置模块的读取和解析规则,通常用来配置Loader。其类型是一个数组,数组里的每一项都描述了如何处理部分文件。

  • 条件匹配:通过test、include、exclude三个配置来选中Loader要应用规则的文件
  • 应用规则:对选中的文件通过user配置项来应用Loader,可以只应用一个Loader或者按照从后往前的顺序应用一组Loader,同时可以分别向Loader传入参数。
  • 重置顺序:一组Loader的执行顺序默认是从右向左执行,通过enforce选项可以将其中一个Loader的执行顺序放到最前或者最后
  module: { // 配置模块相关
    rules: [ // 配置 Loader
      {  
        test: /\.jsx?$/, // 正则匹配命中要使用 Loader 的文件
        include: [ // 只会命中这里面的文件
          path.resolve(__dirname, 'app')
        ],
        exclude: [ // 忽略这里面的文件
          path.resolve(__dirname, 'app/demo-files')
        ],
        use: ['style-loader', 'css-loader','sass-loader' ]
      // 使用那些 Loader,有先后次序,从后往前执行
      enforce:'post',
     // enforce:'post'的含义是将该Loader的执行顺序放到最后
     // enforce:'pre',代表将Loader的执行顺序放到最前面
      },  ], }
noParse

noParse配置项可以让Webpack忽略对部分没有采用模块化的文件的递归解析和处理,这样做能提高构建性能。

// 使用正则表达式
  noParse: /jquery|chartjs/,
// 使用函数,从webpack3.0.0开始
noParse: ()=>{
   // content 代表一个模块的文件路径
  // 返回true或fasle
retrun /jquery|chartjs/test(content);
}
4)Resolve
 // 配置寻找模块的规则
  resolve: { 
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    // 其中,_dirname表示当前工作目录,也就是项目根目录
    modules: [ // 寻找模块的根目录,array 类型,默认以 node_modules 为根目录
      'node_modules',
      path.resolve(__dirname, 'app')
    ],
    extensions: ['.js', '.json', '.jsx', '.css'], // 模块的后缀名
alias: { // 模块别名配置,用于映射模块
 // 把 'module' 映射 'new-module',同样的 'module/path/file' 也会被映射成 'new-module/path/file'
      'module': 'new-module',
      // 使用结尾符号 $ 后,把 'only-module' 映射成 'new-module',
      // 但是不像上面的,'module/path/file' 不会被映射成 'new-module/path/file'
      'only-module$': 'new-module', 
    },
    ],
    mainFields: ['jsnext:main','browser','main'], 
// 第三方模块会针对不同的环境提供几分代码。例如分别提供采用了ES5和ES6的两份代码,
例如
{
    "jsnext:main":"es/index.js", //采用ES6语法的代码入口
    "main":"lib/index.js", //采用ES6语法的代码入口
}
    enforceExtension: false, // 是否强制导入语句必须要写明文件后缀
  },
5)DevServer
  devServer: { // DevServer 相关的配置
    proxy: { // 代理到后端服务接口
      '/api': 'http://localhost:3000'
    },
    contentBase: path.join(__dirname, 'public'), 
// 配置 DevServer HTTP 服务器的文件根目录
    compress: true, // 是否开启 gzip 压缩
    historyApiFallback: true, // 是否开发 HTML5 History API 网页
    hot: true, // 是否开启模块热替换功能
    https: false, // 是否开启 HTTPS 模式
    },
6)Watch和WatchOptions
    watch: true, // 是否开始
    watchOptions: { // 监听模式选项
    // 不监听的文件或文件夹,支持正则匹配。默认为空
    ignored: /node_modules/,
    // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    // 默认为300ms 
    aggregateTimeout: 300,
    // 判断文件是否发生变化是不停的去询问系统指定文件有没有变化,默认每隔1000毫秒询问一次
    poll: 1000

五、常用插件介绍

1)HtmlWebpackPlugin

inject属性参数

参数 含义
ture 默认值,scirpt标签位于html文件的body底部
body script标签位于html文件的body底部
head script标签位于html文件的head中
false 不插入生成的js文件,这个几乎用不到

chunksSortModet属性参数

参数 含义
manual 按照手动的顺序载入
dependency 按照不同文件的依赖关系来排序
auto 默认值,插件的内置的排序方式,具体顺序....
none 无序
const HtmlWebpackPlugin = require('html-webpack-plugin')
new HtmlWebpackPlugin({
    template: resolve('../src/index.html'),
    inject: true,
    // chunks: htmlWebpackConfig.chunks,
    // chunksSortMode: 'manual'
}),
// webpack3 举例
// 需要chunks的包列表,支持正则
let chunksPackage = {
    'antd': ['antd'],
    'antdesign': ['^_@ant-design_icons@'],
    'react': ['react', 'react-router', 'react-redux', 'redux'],
    'immutable': ['immutable', 'moment', 'core-js','lodash','axios'],
    'bcsd': ['^_rc-'],
}
exports.chunksWebpackConfig = {
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            children: true,
            async: 'async-common',
            minChunks: 3,
          }),
        // split vendor js into its own file
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: function (module, count) {
                // any required modules inside node_modules are extracted to vendor
                return (
                    module.resource &&
                    /\.js$/.test(module.resource) &&
                    module.resource.indexOf(
                        path.join(__dirname, '../node_modules')
                    ) === 0
                )
            }
        }),
    ...Object.keys(chunksPackage).map(packageName => {
            return new webpack.optimize.CommonsChunkPlugin({
                name: packageName,
                chunks: ['vendor'],
                minChunks: function (module, count) {
                    return module.resource && chunksPackage[packageName].filter(item => new RegExp(item).test(getModuleName(module)))[0] && count >= 0
                }
            })
        }),
        // extract webpack runtime and module manifest to its own file in order to
        // prevent vendor hash from being updated whenever app bundle is updated
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest',
            chunks: Infinity,
        }),
    ],
}
exports.htmlWebpackConfig = {
    chunks: ['manifest', 'react', 'bcsd', 'immutable', 'antdesign', 'antd', 'vendor', 'client'],
}
参考文章:[https://segmentfault.com/a/1190000013883242](https://segmentfault.com/a/1190000013883242)

2)BundleAnalyzerPlugin
const BundleAnalyzerPlugin = 
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
   // 分析代码
new BundleAnalyzerPlugin({
      analyzerPort: 3011
}),
new BundleAnalyzerPlugin({
    analyzerMode: 'static'
}),
1559037468077.jpg

详细配置

new BundleAnalyzerPlugin({
//  可以是`server`,`static`或`disabled`。
//  在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。
//  在“ static”模式下,会生成带有报告的单个HTML文件。
//  在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。
            analyzerMode: 'server',
//  将在“服务器”模式下使用的主机启动HTTP服务器。
            analyzerHost: '127.0.0.1',
//  将在“服务器”模式下使用的端口启动HTTP服务器。
            analyzerPort: 8888, 
//  路径捆绑,将在`static`模式下生成的报告文件。
//  相对于捆绑输出目录。
       reportFilename: 'report.html',
//  模块大小默认显示在报告中。
//  应该是`stat`,`parsed`或者`gzip`中的一个。
//  有关更多信息,请参见“定义”一节。
            defaultSizes: 'parsed',
//  在默认浏览器中自动打开报告
            openAnalyzer: true,
//  如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
            generateStatsFile: false, 
//  如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
//  相对于捆绑输出目录。
            statsFilename: 'stats.json',
//  stats.toJson()方法的选项。
//  例如,您可以使用`source:false`选项排除统计文件中模块的来源。
//  在这里查看更多选项:https:  //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
            statsOptions: null,
            logLevel: 'info' //日志级别。可以是'信息','警告','错误'或'沉默'。
}),

六、优化

1、拆包
  • webpack3
1)CommonsChunkPlugin

具体参考HtmlWebpackPlugin插件

  • webpack4
2)splitChunks
function getModulePackageName(module) {
    // 获取模块依赖名
    const context = module.context;
    if (!context) return null;
    const nodeModulesPath = path.join(__dirname, '../node_modules/');
    if (context.substring(0, nodeModulesPath.length) !== nodeModulesPath) {
        return null;
    }
    const moduleRelativePath = context.substring(nodeModulesPath.length);
    const [moduleDirName] = moduleRelativePath.split(path.sep);
    let packageName = moduleDirName;
    if (packageName.match('^_')) {
        packageName = packageName.match(/^_(@?[^@]+)/)[1];
    }
    return packageName;
}


optimization: {
        concatenateModules: true, // 使用scope hoisting
        usedExports: true, // 开启tree shaking 只打包使用到的模块
        noEmitOnErrors: true, // 跳过编译异常阶段
        removeAvailableModules: true, // 当这些模块已包含在所有父项中时,检测并从块中删除模块
        removeEmptyChunks: true, // 检测并删除空的块
        mergeDuplicateChunks: true, // 合并包含相同模块的块
        // runtimeChunk: false,
        namedChunks: true, // 入口文件发生改变依然产生稳定hash
        runtimeChunk: {
            name: 'manifest'
        },
        splitChunks: {
            chunks: 'async',
// 必须三选一: "initial" | "all" | "async" (默认就是async)
            minSize: 30000,// 最小30k以上会进行拆分
            maxSize: 0,
            minChunks: 1,
            automaticNameDelimiter: '~',// 命名分隔符
            name: true, // 强制恢复启用文件名称
            // maxAsyncRequests: 5,
            maxInitialRequests: Infinity,
            cacheGroups: {// 这里开始设置缓存的 chunks
                vendors: {
                    chunks: 'initial',
                    priority: 40,
                    name: 'vendors',
                    test: module => {
                        const packageName = getModulePackageName(module);
                        return packageName && ['core-js', 'draft-js', 'moment'].includes(packageName);
                    },
                },
                antdicons: {
                    chunks: 'all',
                    priority: 30,
                    name: 'antdicons',
                    reuseExistingChunk: true,
                    test: module => {
                        const packageName = getModulePackageName(module);
                        return packageName && (packageName.includes('@ant-design_icons') || packageName.includes('@ant-design'));
                    },
                },
                antdlib: {
                    // antd/lib
                    chunks: 'all',
                    priority: 20,
                    reuseExistingChunk: true,
                    // minSize: 400000,
                    // maxSize: 700000,
                    test: module => {
                        const packageName = getModulePackageName(module);
                        return packageName && ['antd'].includes(packageName);
                    },
                    name(module) {
                        const packageName = getModulePackageName(module);
                        if (packageName && ['antd'].includes(packageName) && ++index < 140)  {
                            // 前140个文件打包到lib1中
                            return 'antdlib1'
                        }
                        return 'antdlib2';
                    },
                },
                rclibs: {
                    chunks: 'all',
                    priority: 10,
                    reuseExistingChunk: true,
                    test: module => {
                        const packageName = getModulePackageName(module);
                        return packageName && packageName.includes('rc-')
                    },
                    name(module) {
                        const packageName = getModulePackageName(module);
                        if (['rc-calendar', 'rc-tree-select', 'rc-select', 'rc-table', 
                            'rc-menu', 'rc-tree'].includes(packageName)) {
                            return 'rc_libs'
                        }
                        return 'rc_others';
                    },
                },
                others: {
                    chunks: 'initial',
                    test: /[\\/]node_modules[\\/]/,
                    priority: 0,
                    name: 'others',
                    // // minSize: 400000,
                    // // maxSize: 700000,
                    // test: module => {
                    //  const packageName = getModulePackageName(module);
                    //  return packageName && !packageName.includes('@ant-design');
                    // },
                    reuseExistingChunk: true,
                },
                // default: {
                //  minChunks: 2,
                //  priority: -20,
                //  name: 'default',
                //  reuseExistingChunk: true
                // }
            }
        },
    },
},
// 推荐文章 [https://juejin.im/post/5af1677c6fb9a07ab508dabb](https://juejin.im/post/5af1677c6fb9a07ab508dabb)
2、DLLPlugin

要给web项目构建接入动态链接库的思想

  • 将网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中。在一个动态链接库中可以包含多个模块。
  • 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次打包,而是去动态链接库中获取。
  • 页面依赖的所有动态链接库都需要被加载
为什么会提高打包速度:

包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库中的代码。

实例一
var path = require("path");
var webpack = require("webpack");
module.exports = {
  // 要打包的模块的数组
  mode: 'production',
  entry: {
    react: ['react', 'react-dom', 'react-router'],
    redux: ['redux', 'react-redux', 'redux-actions', 'redux-thunk', 'react-router-redux'],
    vendors: ['dragula', 'axios', 'habo', 'immutable']
  },
  output: {
    path: path.join(__dirname, '../dll'), // 打包后文件输出的位置
    filename: '[name].dll.js',// vendor.dll.js中暴露出的全局变量名。
    library: '[name]', // 打包出的是一个库,暴漏到全局,名叫vendors
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, '../dll/[name].manifest.json'), 
      // context: __dirname
    }),
  ]
};



function getPlugins() {
    const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
    files.forEach(file => {
        if (/.*.dll.js/.test(file)) {
            plugins.push(new AddAssetHtmlWebpackPlugin({
                filepath: path.resolve(__dirname, '../dll', file) 
// 将打包后的dll文件注入html中
            }));
        } else if (/.*.manifest.json/.test(file)) {
            plugins.push(new webpack.DllReferencePlugin({
                manifest: path.resolve(__dirname, '../dll', file)
            })); 
// 分析json文件,里面有的模块就不会引用node_modules里的文件,因为dll.js中已存在
        }
    });
    return plugins;
}

plugins: getPlugins()
实例二
const path = require("path");
const webpack = require("webpack");
const { dependencies } = require("./package.json");
module.exports = {
  mode: "development",
  entry: {
    vendor: Object.keys(dependencies)
  },
  output: {
    path: path.join(__dirname, "dll"), // 生成的dll.js路径,我是存在/build/dev中
    filename: "[name].dll.js", // 生成的文件名字
    library: "[name]_library" // 生成文件的一些映射关系,与下面DllPlugin中配置对应
  },
  plugins: [
    // 使用DllPlugin插件编译上面配置的NPM包
    new webpack.DllPlugin({
      // 会生成一个json文件,里面是关于dll.js的一些配置信息
      path: path.join(__dirname, "dll", "[name]-manifest.json"),
      name: "[name]_library", // 与上面output中配置对应
      context: __dirname
    })
  ]
};
3、HappyPack
实例一
// 使用happypack进行多进程打包
const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
new HappyPack({
        //用id来标识 happypack处理那里类文件
        id: 'happyBabel',
        //如何处理  用法和loader 的配置一样
        loaders: [{
            loader: 'babel-loader?cacheDirectory',
        }],
        //共享进程池
        threadPool: happyThreadPool,
        //允许 HappyPack 输出日志
        verbose: true,
    }), // happypack 实例
    new HappyPack({
        id: 'happyStyl',
        loaders: [{
            loader: 'style-loader!css-loader!stylus-loader',
        }],
        threadPool: happyThreadPool,
        verbose: true,
    })
module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: 'happypack/loader?id=happyBabel', 
// 使用happypack loader
                exclude: /node_modules/
            },
            {
                test: /\.styl$/,
                // loader: 'style-loader!css-loader!stylus-loader',
                loader: 'happypack/loader?id=happyStyl', 
// 使用另一个happypack处理
                //exclude: /node_modules/
                include: path.resolve(__dirname, '../src')
            },
}
4、压缩
var UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
  optimization: {
    minimizer: [
      // 自定义js优化配置,将会覆盖默认配置
      new UglifyJsPlugin({
        exclude: /\.min\.js$/, // 过滤掉以".min.js"结尾的文件,我们认为这个后缀本身就是已经压缩好的代码,没必要进行二次压缩
        cache: true,
        parallel: true, // 开启并行压缩,充分利用cpu
        sourceMap: false,
        extractComments: false, // 移除注释
        uglifyOptions: {
          compress: {
            unused: true,
            warnings: false,// 在uglifyjs删除没有用到代码时不输出警告
            drop_debugger: true, // 删除debugger
          },
          output: {
            beautify: false, // 最紧凑的输出
            comments: false,// 删除所有注释
          }
        }
      }),
    ]
  }
}
5、Tree Shaking 和 Scope Hoisting
optimization: {
        concatenateModules: true, // 使用scope hoisting
        usedExports: true, // 开启tree shaking 只打包使用到的模块
        noEmitOnErrors: true, // 跳过编译异常阶段
        removeAvailableModules: true, 
// 当这些模块已包含在所有父项中时,检测并从块中删除模块
        removeEmptyChunks: true, // 检测并删除空的块
        mergeDuplicateChunks: true, // 合并包含相同模块的块
        // runtimeChunk: false,
        namedChunks: true, // 入口文件发生改变依然产生稳定hash
        runtimeChunk: {
            name: 'manifest'
        },
        splitChunks: {}
        },
    },
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 写在前面 第一次接触webpack,是在一个react项目参与中,刚开始使用的时候,甚至不知道是做什么用的,只看到...
    默默先生Alec阅读 640评论 0 3
  • 2017年12月7日更新,添加了clean-webpack-plugin,babel-env-preset,添加本...
    ZombieBrandg阅读 1,159评论 0 19
  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,663评论 7 110
  • webpack 是什么? 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(mo...
    IT老马阅读 3,299评论 2 27
  • Webpack 第一章 Webpack 简介 Instagram团队在进行前端开发的过程中,发现当项目组成员越来越...
    whitsats阅读 622评论 0 1