QQ 音乐技术团队分享之 Webpack 实用技巧高效实战

4,441 阅读5分钟
原文链接: mp.weixin.qq.com

在项目中使用了一段时间的 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 库中寻找(页面中