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 环境):

Webpack 实用技巧高效实战

这时已经可以通过静态分析得到不可达的部分(console.log('prod')),再过 Uglify 压缩无用的代码就会被清除掉:

Webpack 实用技巧高效实战


四、关于公共文件打包配置:

如果是多入口页面的项目,多个 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  }), }

生成文件:

Webpack 实用技巧高效实战
这时再在 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 文件。

Webpack 实用技巧高效实战
vendor.dll.js:

Webpack 实用技巧高效实战
vendor-manifest.json:

Webpack 实用技巧高效实战

第二步,使用 DllReferencrPlugin 在项目中引用 Dll 库文件:

plugins:[
 new webpack.DllReferencePlugin({    context:__dirname,    manifest: require( './output/dll/vendor-manifest.json' )  }) ]

这样只要遇到在 manifest.json 文件中存在的模块,都不会再打包进入项目中,而是运行时到指明的 Dll 库中寻找(页面中 <script> 提前加载好 Dll 库):

Webpack 实用技巧高效实战
Webpack 实用技巧高效实战

注:这里 Dll 库除了暴露到 window 全局下使用,也可以生成服务端使用的 commonJS,需要在配置中指定 libraryTarget(详见):Webpack 实用技巧高效实战

Webpack 实用技巧高效实战

六、关于分片/按需加载:

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

Webpack 实用技巧高效实战
c.js

Webpack 实用技巧高效实战

打包后生成文件:

Webpack 实用技巧高效实战
1.chunk-one.js:

(可以看到同一个 ensure 块内的引用被打到了这个独立的 chunk 里)

Webpack 实用技巧高效实战

注:默认开头的1.为这个chunk文件的id(非moduleId),命名规则可以在 output 中配置 chunkFilename 更改,例如:

output: {
 
path: './output/',  filename: "[name].js",  chunkFilename: "[id].[name].js?ver=[chunkhash]", //queryString 也可以加!
}

a.js

Webpack 实用技巧高效实战
最后这段代码运行究竟会输出什么?如上所说,只放在 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 

Webpack 实用技巧高效实战
b.js

Webpack 实用技巧高效实战
打包后:

Webpack 实用技巧高效实战
要注意的是:如果没有指定 target 为 node,而代码里有 require Node 的原生模块(例如http、url等)但又没有设置 Alias,也就是找不到这些模块时,Webpack 会尝试一个兼容逻辑:引入 Browserify 的兼容模块代替之(当作你是在前端运行,等于是在帮你做同构了)。


编译后(未指定target:"node"):

具体支持的模块见: https://github.com/substack/node-browserify#compatibility 。

所以如果是后端代码,不要忘了给配置指定 target。


以上是关于Webpack 实用技巧高效实战的主要内容,如果未能解决你的问题,请参考以下文章

(23/24) webpack实战技巧:如何在webpack环境中使用Json

高效Web开发的10个jQuery代码片段

(22/24) webpack实战技巧:静态资源集中输出

你可能不知道的JavaScript代码片段和技巧(下)

你可能不知道的JavaScript代码片段和技巧(上)

为开发提速,项目 Webpack 转 Vite 实战!(实用)