Webpack 实用技巧高效实战
Posted QQ音乐技术团队
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Webpack 实用技巧高效实战相关的知识,希望对你有一定的参考价值。
在项目中使用了一段时间的 Webpack ,得益于其多元的功能支持和配置定制,得到了很多本地编译和依赖管理的帮助。在搭建好配置和架构之后,开发过程中可以不再关注模块的组织、载入、转义、合并、精简、兼容等各种方面的工程问题,全部交给 Webpack 来处理。效率和体验都得到了不小的提升。本篇文章就是在对使用 Webpack 过程中的关键配置和方法做一些总结和沉淀。
本文是一些零散的功能记录、关键点配置和 Tips,大部分从使用过程中总结而来,并不是手册翻译也不是入门讲解,正在入手 Webpack 或在使用中遇到问题的同学可以看看是否刚好解决到你的问题,如果有老司机也欢迎指出错误。
一、复杂项目配置正确姿势 - Node API:
Webpack 的配置方式,简单的项目通过一份 webpack.config.js 配置文件可以 hold 住了。并且 webpack.config.js 中可以以数组形式返回多份配置,执行打包命令时会遍历每个配置执行多次打包。
但在复杂项目中(例如同构项目)需要根据不同环境定制配置,写配置文件的方法可能捉襟见肘。这时可以直接用 Node API 来跑,从使用配置文件转为使用一个配置 Function 或者 Class 来灵活生成了。例如一个 build 脚本可以这样写 (文中部分代码为方便读者 Copy 未转图片,浏览折行请见谅):
./build.js:
var webpack = require('webpack');
var configGen = require("./config.generator");
//通过参数生成定制配置,例如通过 process.argv 接收参数
var config = configGen(options);
var compiler = webpack(config); compiler.run(function(err, stats) {
if(err){
console.err(err) }else{
console.log(stats.toString({
//终端显示带上颜色
colors:true
})) } });
然后使用 npm scripts 直接跑就很方便:
./package.json:
{
"scripts": {
"build": "node ./build.js" } }
执行:
npm run build
或者开发时使用 webpack-dev-server 来做本地 server 动态更新, 非常灵活:
var webpack = require('webpack');
var webpackDevServer = require('webpack-dev-server');
var configGen = require("./config.generator");
var config = configGen(options);
var compiler = webpack(config);
var server = new webpackDevServer(compiler,{ contentBase: __dirname, stats: { colors: true } }); server.listen(8081);
二、关于 loader 配置:
loader 可以写在代码里,也可以在配置里设置。建议通用的 loader 都放到配置里,减少代码中的特殊性。否则万一以后要迁移还麻烦。
例如配置 .jsx 文件使用 Babel-loader 支持 React 和 ES6,以及传递一些参数开启更多 Babel 插件:
module:{
loaders:[
{ test: /\.jsx$/, include: path.resolve(__dirname, "lib"), loader: "babel-loader",
//query用于向loader传递参数,不同loader接收参数不一样 query: { presets: ['react', 'es2015'], plugins: [
"syntax-object-rest-spread",
"transform-object-rest-spread"
], cacheDirectory: true } },
] }
如果你有一些 loader 需要提前执行(例如CMD转AMD的兼容处理,不提前处理依赖解析就会有问题),可以使用 module.preLoaders ,配置和 module.loaders 相同。
如果你有用到一些自己写的 loader,想设置别名而不用直接写相对路径,和模块的别名(在resolve.alias 里设置)不同,需要在 resolveLoader.alias 里设置 loader 的别名:
resolveLoader: {
alias: {
"seajs-loader": path.resolve( __dirname, "./web_modules/seajs-loader.js" ) } }
如果你的项目有引用根路径上级的模块(依赖路径在根路径之上),可能会出现找不到 loader 的情况,需要在 resolveLoader.root 中手动指定 loader 的默认位置:
resolveLoader: {
//指定默认的loader路径,否则依赖走到上游会找不到loader
root: path.resolve( __dirname, './node_modules' ) }
三、关于全局模块/全局变量/环境变量:
如果习惯了使用全局模块,例如 jQuery 的 $,而不想每次都写 $ = require('jquery'), 可以使用 ProvidePlugin 插件:
plugins: [
new webpack.ProvidePlugin({ $: "jquery",
jQuery: "jquery" }) ]
如果代码中有需要插入静态的全局变量,或者需要根据环境变量来区分的分支,可以使用 DefinePlugin 插件来插入静态环境变量,插入的变量在编译时将被处理:
plugins: [
new webpack.DefinePlugin({ "process.env": { NODE_ENV: JSON.stringify( options.dev ? 'development' : 'production' ) },
"__SERVER__": isServer ? true : false
})
]
编译前:
编译后 (假设为 development 环境):
这时已经可以通过静态分析得到不可达的部分(console.log('prod')),再过 Uglify 压缩无用的代码就会被清除掉:
四、关于公共文件打包配置:
如果是多入口页面的项目,多个 Entry 之间可能会有一些公共的lib(基础库等),这时候就要用到公共文件提取打包,提高缓存的使用率。手册中写的很明白使用 CommonsChunkPlugin 插件来处理。这个插件支持很多种传参和设置,我比较喜欢下面这种对象传递,这样可以指定生成多个包:
entry: { a:"./a.js", b:"./b.js", common1:[
//以下库文件及其下游依赖都会被打到 common1 中 "./lib/common.js",
"react", "react-dom", "redux", "react-redux", "redux-thunk",
"react-router", "react-router-redux" ], }, plugins:[
new webpack.optimize.CommonsChunkPlugin({
//可以指定多个 entryName,打出多个 common 包 names: ['common1'], minChunks: Infinity }), }
生成文件:
这时再在 a.js 或 b.js 及其依赖中引用 common1 包中包含的库时,将不会再被重复打包到各自的 bundle 中。(注意:bundle 在页面中的载入顺序为: common1 => a/b )
五、关于 DllPlugin (manifest):
DllPlugin 相比 commonsChunkPlugin 是纯粹分离的一种更独立的打包方式。名副其实,相当于独立把文件打成第三方库来使用。这种方式适合用来处理一些不常修改的第三方库(尤其大型的框架源码等),将其独立打包,只通过生成的 manifest 文件对其中的模块进行引用,不用在每次项目编译时都把这些内容一起再编译打包一遍。因为这些通常都是不会被修改的。
使用 DllPlugin 打包分两步,一步是使用 DllPlugin 对需要独立出来的库文件进行独立打包。这里是一个独立的 webpack 打包过程和配置:
例:
./config.dll.js
var webpack = require('webpack');
var path = require('path');
module.exports = { entry:{ vendor: [ "react", "react-dom", "redux", "react-redux", "redux-thunk", "react-router", "react-router-redux" ] }, output:{ filename:'[name].dll.js', path:path.resolve( __dirname, './output/dll' ), library:"[name]" }, plugins:[
new webpack.DllPlugin({ path:path.resolve( __dirname, './output/dll/[name]-manifest.json'), name:"[name]" }),
new webpack.optimize.UglifyJsPlugin({ minimize: true, output: {comments: false} }) ] }
单独打包
webpack --config=config.dll.js
打包后除了生成所谓的 Dll 库文件,还生成一个指出 Dll 文件中包含的模块列表的 manifest.json 文件。
vendor.dll.js:
vendor-manifest.json:
第二步,使用 DllReferencrPlugin 在项目中引用 Dll 库文件:
plugins:[
new webpack.DllReferencePlugin({ context:__dirname, manifest: require( './output/dll/vendor-manifest.json' ) }) ]
这样只要遇到在 manifest.json 文件中存在的模块,都不会再打包进入项目中,而是运行时到指明的 Dll 库中寻找(页面中 <script> 提前加载好 Dll 库):
注:这里 Dll 库除了暴露到 window 全局下使用,也可以生成服务端使用的 commonJS,需要在配置中指定 libraryTarget(详见):
六、关于分片/按需加载:
require.ensure(dependencies, callback) 是 Webpack 的按需加载方法,在一个 ensure 块中产生引用的文件都将被单独打包成分片文件,在运行时动态(运行到ensure语句时)加载。直接写到 dependencies 参数中的模块不同点在于只会被加载,没有被 require 的时候不会被运行。
例:
a.js
//./c 模块只会被加载,没运行
require.ensure(["./c"], function(require){ //./b模块被require了,加载后会运行
var bbb = require("./b"); console.log(bbb.foo) },'chunk-one')
//可以指定生成的 chunkname(不指定默认用 hash)
//注:如果给多个 chunk 指定相同的 name 则会打包到一起
b.js
c.js
打包后生成文件:
1.chunk-one.js:
(可以看到同一个 ensure 块内的引用被打到了这个独立的 chunk 里)
注:默认开头的1.为这个chunk文件的id(非moduleId),命名规则可以在 output 中配置 chunkFilename 更改,例如:
output: {
path: './output/', filename: "[name].js", chunkFilename: "[id].[name].js?ver=[chunkhash]", //queryString 也可以加!
}
a.js
最后这段代码运行究竟会输出什么?如上所说,只放在 dependencies 的模块,没有被 require 是不会运行的。所以最后输出是:
b //console.log('b') from ./b.js
1 //console.log(bbb.foo) from ./a.js
除了 require.ensure 中的dependencies,还有一个 require.include 可以达到同样的效果(先加载不运行)。例如下面这段代码:
require.include('./b');
require.ensure([],function(require){
var b = require("./b");
var c = require("./c");
console.log(b.foo) },'chunk-one');
require.ensure([],function(require){
var b = require("./b");
var d = require("./d");
console.log(d.foobar) },'chunk-two');
这种情况下,如果去掉前面的 require.include('./b'),chunk-one 和 chunk-two 里都会重复打入模块b。这里就是起到了一个依赖前置的作用(提前到了当前的依赖树,子依赖树继承)。而且模块b实际在被 require 的时候才会被运行。
七、关于Uglify:
Uglify 同样是作为 Plugin 内置。示例如下:
plugins: [
new webpack.optimize.UglifyJsPlugin({
//可以加入Uglify的compressor options
compress: {
//去掉压缩过程中的提示
warnings:false
},
//可以指定哪些变量name不混淆,
//如 except: ['require','jQuery']
except: [],
output:{
//是否保留注释,默认为false
comments:true
}
}) ]
压缩过程中会对不可达/未使用代码进行去除,去除时会有大批 warning 提示刷屏。如果不关心可以使用 warnings:false 去掉提示。
Uglify 的 compressor options 见:
八、关于服务端:
关于服务端的环境,关键的配置主要有3个。
首先是 target:"node" :指定是在 Node 环境下,这样在使用到原生模块时会保留为用 require 直接加载,而不尝试去打包。
另一个是在 output 里指定 libraryTarget: "commonjs2" ,告诉 Webpack 使用 module.exports 导出模块。
还有第三个就是 externals 配置,可以指定一些不打包的文件,比如 node_modules/。被指定的文件会直接保留 require,不打包进 bundle。
//用 require 加载原生模块
target:"node",
//指定不打包的文件 (指定路径名,可以用正则)
externals:[
/node_modules/
],
output:{
//指定使用 module.exports 导出模块
libraryTarget: "commonjs2",
//bundle 里打上文件路径方便识别
pathinfo: true
}
示例:
打包前 :
a.js
b.js
打包后:
要注意的是:如果没有指定 target 为 node,而代码里有 require Node 的原生模块(例如http、url等)但又没有设置 Alias,也就是找不到这些模块时,Webpack 会尝试一个兼容逻辑:引入 Browserify 的兼容模块代替之(当作你是在前端运行,等于是在帮你做同构了)。
编译后(未指定target:"node"):
具体支持的模块见: https://github.com/substack/node-browserify#compatibility 。
所以如果是后端代码,不要忘了给配置指定 target。
以上是关于Webpack 实用技巧高效实战的主要内容,如果未能解决你的问题,请参考以下文章