前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Webpack 实用技巧高效实战

Webpack 实用技巧高效实战

作者头像
QQ音乐技术团队
发布2018-01-31 17:29:47
1.6K0
发布2018-01-31 17:29:47
举报

在项目中使用了一段时间的 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.js1 //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 见:https://github.com/mishoo/UglifyJS2#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。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 QQ音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、复杂项目配置正确姿势 - Node API:
  • 二、关于 loader 配置:
  • 三、关于全局模块/全局变量/环境变量:
  • 四、关于公共文件打包配置:
  • 五、关于 DllPlugin (manifest):
  • 六、关于分片/按需加载:
    • 例:
    • 七、关于Uglify:
    • 八、关于服务端:
      • 示例:
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档