入口(entry)css
module.exports = { entry: './path/to/my/entry/file.js' }; //或者 module.exports = { entry: { main: './path/to/my/entry/file.js' } };
输出(output)html
module.exports = { output: { filename:'[name][chunkhash:8].js', path:path.resolve(__dirname,'dist') } };
loader
预处理loader前端
处理js loadernode
图片处理loaderreact
插件(plugin)plugins
里面放的是插件,插件的做用在于提升开发效率,可以解放双手,让咱们去作更多有意义的事情。一些很low的事就通通交给插件去完成。jquery
const webpackConfig = { plugins: [ //清除文件 new CleanWebpackPlugin(), //css单独打包 new MiniCssExtractPlugin({ filename: "[name].css", chunkFilename: "[name].css" }), // 引入热更新插件 new webpack.HotModuleReplacementPlugin() ] }
模式(mode)webpack
development 开发环境es6
SouceMap
,不须要专门配置项目中详细配置web
Webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph
),其中包含应用程序须要的每一个模块,而后将全部模块打包成一个或多个 bundle
ajax
其实就是:Webpack 是一个 JS 代码打包器。
至于图片、CSS、Less、TS等其余文件,就须要 Webpack 配合 loader 或者 plugin 功能来实现。
webpack构建的三个阶段:
chunk是webpack内部运行时的概念;一个chunk是对依赖图的部分进行封装的结果(`Chunk
the class is the encapsulation for parts of your dependency graph`);能够经过多个entry-point来生成一个chunk
chunk能够分为三类;
entry chunk
initial chunk
normal chunk
require.ensure
、System.import
、import()
异步加载进来的module
,会被放到normal chunk中每一个chunk都至少有一个属性:
var compiler = webpack(options);
从入口文件index.js开始分析,检查右侧表格中的记录,若是有记录就结束。没有记录就继续读取文件内容,读取完文件内容后,开始进行抽象树语法分析,将代码字符串转换成一个对象的描述文件。并将其中的依赖保存在dependencies数组中
dependencies:["./src/a.js"]
保存完之后,替换依赖函数
console.log("index.js"); _webpack_reuqire("./src/a.js");
将转换后的代码字符串保存在右侧的表格中
模块id | 转换后的代码 |
---|---|
./src/index.js | console.log("index.js");_webpack_reuqire("./src/a.js"); |
由于dependencies中有数据,开始递归解析dependencies中的数据。取出.src/a.js
// .src/a.js console.log("a.js"); require("b")
查看右侧表格,发现没有a.js,开始读取文件内容,生成ast抽象语法树,将依赖记录在数组中
dependencies: ["./src/b.js"]
而后替换函数依赖
console.log("a.js"); _webpack_require("./src/b.js"); module.exports = "a"
将转换后的代码记录在右侧的表格中
模块id | 转换后的代码 |
---|---|
./src/index.js | console.log("index.js");_webpack_reuqire("./src/a.js"); |
./src/a.js | console.log("a.js");_webpack_require("./src/b.js");module.exports = "a" |
而后继续取出来dependencies的内容./src/b.js
console.log("b.js"); module.exports = "b";
发现右侧表格中没有b.js这个文件,就继续读取文件内容,进行ast抽象语法树分析,发现没有依赖项,就不须要往数组中放东西,也不须要替换依赖项,将代码字符串存在表格中
模块id | 转换后的代码 |
---|---|
./src/index.js | console.log("index.js");_webpack_reuqire("./src/a.js"); |
./src/a.js | console.log("a.js");_webpack_require("./src/b.js");module.exports = "a" |
./src/b.js | console.log("b.js");module.exports = "b"; |
而后递归回去,发现index下产生的数组是空,整个文件依赖就解析完毕
在第二步完成之后,chunk中会产生一个模块列表,列表中包含了模块id和模块转换后的代码
接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets
,资源列表能够理解为是生成到最终文件的文件名和文件内容
即:文件名:./dist/main.js
文件内容:
(function(){ })({ "./src/index.js": function(){ //是不是eval能够根据devtool来设置,有不少种方式 eval("console.log(\"index module\");\nvar a = __webpack_require__(/*! ./a */ \"./src/a.js\"); \na.abc();\nconsole.log(a);\n\n\n//# sourceURL=webpack:///./src/index.js?") } })
chunk hash: 是根据全部的chunk assets的内容生成的一个hash字符串
hash: 一种算法,具备不少分类。特色是将一个任意长度的字符串转换成一个固定长度的字符串,并且能够保证原始内容不变
就是将咱们上面生成的文件内容,所有联合起来,而后生成一个固定长度的哈希值连接
简图:
多个chunk assets就是一个bundle(一捆)
将多个chunk的assets合并到一块儿,并产生一个总的hash
webpack将利用node中的fs模块(文件处理模块),根据编译产生的总的assets,生成相应的文件
Hot Module Replacement(如下简称:HMR 模块热替换)是 Webpack 提供的一个很是有用的功能,它容许在 JavaScript 运行时更新各类模块,而无需彻底刷新。
当咱们修改代码并保存后,Webpack 将对代码从新打包,HMR 会在应用程序运行过程当中替换、添加或删除模块,而无需从新加载整个页面。
HMR 主要经过如下几种方式,来显著加快开发速度:
webpack-dev-server:不是一个插件,而是一个web服务器
服务启动流程
webpack-dev-server源码解析
//启动服务的具体方法 function startDevServer(config, options) { const log = createLogger(options); //声明全局webpack实例 let compiler; try { compiler = webpack(config); } catch (err) { if (err instanceof webpack.WebpackOptionsValidationError) { log.error(colors.error(options.stats.colors, err.message)); // eslint-disable-next-line no-process-exit process.exit(1); } throw err; } try { //建立server服务 server = new Server(compiler, options, log); serverData.server = server; } catch (err) { if (err.name === 'ValidationError') { log.error(colors.error(options.stats.colors, err.message)); // eslint-disable-next-line no-process-exit process.exit(1); } throw err; } if (options.socket) { //设置服务监听 server.listeningApp.on('error', (e) => { if (e.code === 'EADDRINUSE') { //使用socket创建长链接 //初始化socket const clientSocket = new net.Socket(); clientSocket.on('error', (err) => { if (err.code === 'ECONNREFUSED') { // No other server listening on this socket so it can be safely removed fs.unlinkSync(options.socket); server.listen(options.socket, options.host, (error) => { if (error) { throw error; } }); } }); clientSocket.connect({ path: options.socket }, () => { throw new Error('This socket is already used'); }); } }); server.listen(options.socket, options.host, (err) => { if (err) { throw err; } // chmod 666 (rw rw rw) const READ_WRITE = 438; fs.chmod(options.socket, READ_WRITE, (err) => { if (err) { throw err; } }); }); } else { server.listen(options.port, options.host, (err) => { if (err) { throw err; } }); } } //启动webpack-dev-server服务器 processOptions(config, argv, (config, options) => { startDevServer(config, options); });
server.js源码解析
class Server { constructor(compiler, options = {}, _log) { ...... //构造函数初始化服务 } //建立初始化express应用 setupApp() { this.app = new express(); } // 绑定监听事件 setupHooks() { //当监听到一次webpack编译结束,就会调用_sendStats方法经过websocket给浏览器发送通知, //ok和hash事件,这样浏览器就能够拿到最新的hash值了,作检查更新逻辑 const addHooks = (compiler) => { const { compile, invalid, done } = compiler.hooks; compile.tap('webpack-dev-server', invalidPlugin); invalid.tap('webpack-dev-server', invalidPlugin); // 监听webpack的done钩子,tapable提供的监听方法 done.tap('webpack-dev-server', (stats) => { this._sendStats(this.sockets, this.getStats(stats)); this._stats = stats; }); }; ...... } //使用webpack-dev-middleware中间件,返回生成的bundle文件 setupDevMiddleware() { // middleware for serving webpack bundle this.middleware = webpackDevMiddleware( this.compiler, Object.assign({}, this.options, { logLevel: this.log.options.level }) ); } ...... //建立http服务,并启动服务 createServer() { ... } //建立socket服务器创建长链接 createSocketServer() { ...... } //使用socket在服务器和浏览器直接创建一个websocket长链接 listen(port, hostname, fn){ ... } // 经过websoket给客户端发消息 _sendStats(sockets, stats, force) { ...... this.sockWrite(sockets, 'hash', stats.hash); if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); } }
client/index.js源码解析
var onSocketMessage = { hash: function hash(_hash) { // 更新currentHash值 status.currentHash = _hash; }, ok: function ok() { sendMessage('Ok'); // 进行更新检查等操做 reloadApp(options, status); }, } // 链接服务地址socketUrl,?http://localhost:8080,本地服务地址 socket(socketUrl, onSocketMessage);
done
钩子函数(生命周期)里调用_sendStats发送向client发送hash值,在client保存下来。在项目初始化时,服务端与客户端已经开启了长链接服务,当webpack对文件编译产生变化时,服务端会及时通知客户端。
class Server { ... setupHooks() { //添加webpack的done事件回调 const addHooks = (compiler) => { const { compile, invalid, done } = compiler.hooks; //通知正在客户端编译 compile.tap(\'webpack-dev-server\', invalidPlugin); done.tap(\'webpack-dev-server\', (stats) => { //编译完成向客户端发送消息 this._sendStats(this.sockets, this.getStats(stats)); this._stats = stats; }); }; addHooks(this.compiler); } _sendStats(sockets, stats, force) { if (...) { //无变化则return return this.sockWrite(sockets, \'still-ok\'); } //若是有变化,则发送hash值 this.sockWrite(sockets, \'hash\', stats.hash); if (stats.errors.length > 0) { this.sockWrite(sockets, \'errors\', stats.errors); } else {//没有报错发送ok this.sockWrite(sockets, \'ok\'); } } ... //使用sockjs在浏览器端和服务端之间创建一个 websocket 长链接 listen(port, hostname, fn) {...} }
这里仍然是Server.js中的代码,我详细的写展现了setupHooks中的代码,setupHooks 调用 webpack api 监听 compile的 done 事件,当编译完成,执行done钩子,调用_sendStats,在_sendStats方法中若是文件变化则发送hash。最后发送ok,客户端在接受到OK后会执行reload。
客户端socket接受到hash后保存起来,随后接受到ok执行reload命令。
//Client/index.js var onSocketMessage = { ... hash: function hash(_hash) { //将hash保存到全局currentHash中 status.currentHash = _hash; }, ok: function ok() { ... //执行更新的reloadApp函数 reloadApp(options, status); }, ... }; socket(socketUrl, onSocketMessage); //Client/utils/reloadApp.js function reloadApp(_ref, _ref2) { if (hot) { //hotEmitter是events类,webpack-dev-server发布webpackHotUpdate给webapck var hotEmitter = require(\'webpack/hot/emitter\'); hotEmitter.emit(\'webpackHotUpdate\', currentHash); if (typeof self !== \'undefined\' && self.window) { // broadcast update to window self.postMessage("webpackHotUpdate".concat(currentHash), \'*\'); } } }
客户端接收到ok指令后,执行reloadApp函数。reloadApp函数中,hotEmitter实际上是events模块的实例,即在全局实现发布订阅模式,hotEmitter发布了webpackHotUpdate事件,同时webpack订阅这个指令。
在这里之后,浏览器端进入webpack的代码,webpack-dev-server在客户端的部分完成。
订阅webpackHotUpdate事件的代码在webpack/hot/dev-server.js中:
if (module.hot) { var lastHash; var check = function check() { module.hot .check(true) .then(function (updatedModules) { //检查全部要更新的模块,若是没有模块要更新那么回调函数就是null if (!updatedModules) { window.location.reload(); return; } if (!upToDate()) {//若是还有更新 check(); } }) }; var hotEmitter = require("./emitter"); hotEmitter.on("webpackHotUpdate", function (currentHash) { lastHash = currentHash; check(); //调用check方法 }); }
module为全局对象,module.hot的代码在HMR runtime中,module.hot.check对应hotCheck方法:
hotCheck = () => { //module.hot.check方法 return hotDownloadManifest.then((update) => { //保存全局的热更新信息 hotAvailableFilesMap = update.c; hotUpdateNewHash = update.h; /*globals chunkId */ hotEnsureUpdateChunk(chunkId) }) } hotDownloadManifest(){ //ajax请求模块manifest return new Promise(...); } hotEnsureUpdateChunk(){ //检测模块 if (!hotAvailableFilesMap[chunkId]) {...} else { hotRequestedFilesMap[chunkId] = true; hotDownloadUpdateChunk(); } } hotDownloadUpdateChunk(){} //jsonp格式请求代码模块chunk //chunk是js代码块,格式是webpackHotUpdate("main", {...}),收到后直接执行,window全局中有对应方法 window["webpackHotUpdate"]=function webpackHotUpdateCallback(){ hotAddUpdateChunk() } hotAddUpdateChunk(){//动态的更新代码模块 for (var moduleId in moreModules) { //记录全局的热更新模块 hotUpdate[moduleId] = moreModules[moduleId]; } hotUpdateDownloaded() } hotUpdateDownloaded(){ //执行hotApply模块 hotApply() } hotApply(){ //将代码更新到modules中 }
主要包含了两个请求,在hotDownloadManifest中客户端请求了ajax的manifest,他的格式为 {"h":"bbff25e45ca71af784d0","c":{"main":true}}
包含了要更新模块的hash值和chunk名;另外一个hotDownloadUpdateChunk经过jsonp方法请求更新的代码块,
获取到的代码块能够直接执行,webpack已经在window中注册了webpackHotUpdate方法,执行后调用hotApply热模块替换方法。
function hotApply(options) { function getAffectedStuff(updateModuleId) { ... return { //返回过时的模块和依赖 type: "accepted", moduleId: updateModuleId, outdatedModules: outdatedModules, outdatedDependencies: outdatedDependencies }; } ... result = getAffectedStuff(moduleId); ... { switch (result.type) { case "self-declined": ... break; case "accepted"://对结果进行标记及处理 if (options.onAccepted) options.onAccepted(result); doApply = true; break; case "disposed": ... break; default: ... } ... while (queue.length > 0) { moduleId = queue.pop(); ... delete installedModules[moduleId];//删除过时的模块和依赖 delete outdatedDependencies[moduleId]; } ... for (moduleId in appliedUpdate) { if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) { //新的模块添加到modules中 modules[moduleId] = appliedUpdate[moduleId]; } } ... }
模块热替换主要分三个部分,首先是找出 outdatedModules 和 outdatedDependencies;而后从缓存中删除这些;最后,将新的模块添加到 modules 中,当下次调用 webpack_require (webpack 重写的 require 方法)方法的时候,就是获取到了新的模块代码了。
若是在热更新过程当中出现错误,热更新将回退到刷新浏览器。
当用新的模块代码替换老的模块后,可是咱们的业务代码并不能知道代码已经发生变化,也就是说,当入口文件修改后,咱们须要在入口文件中调用 HMR 的 accept 方法
// index.js if(module.hot) { module.hot.accept(\'./main.js\', function() { render() }) }
更新的代码每次在下面这个循环中执行, cb(moduleOutdatedDependencies)
就是module.hot.accept
的内容,从而实现对代码的渲染
function hotApply(options) { ... for (moduleId in outdatedDependencies) { ... moduleOutdatedDependencies = outdatedDependencies[moduleId]; var callbacks = []; for (i = 0; i < moduleOutdatedDependencies.length; i++) { dependency = moduleOutdatedDependencies[i]; cb = module.hot._acceptedDependencies[dependency]; callbacks.push(cb); //获取全部的模块 } for (i = 0; i < callbacks.length; i++) { cb = callbacks[i]; cb(moduleOutdatedDependencies);//执行代码模块 } ... } ... }
抽象语法树,源代码语法结构的一种抽象表示
抽象语法树的生成主要依靠的是Javascript Parser(js解析器)
访问者(visitor)是一个用于 AST 遍历的跨语言模式,定义 了用于在一个树状结构中获取具体节点的方法
应用于循环分析依赖
循环分析结果
现在前端工程化的概念早已经深刻人心,选择一款合适的编译和资源管理工具已经成为了全部前端工程中的标配,而在诸多的构建工具中,webpack以其丰富的功能和灵活的配置而深受业内吹捧,逐步取代了grunt和gulp成为大多数前端工程实践中的首选,React,Vue,Angular等诸多知名项目也都相继选用其做为官方构建工具,极受业内追捧。可是,随者工程开发的复杂程度和代码规模不断地增长,webpack暴露出来的各类性能问题也愈发明显,极大的影响着开发过程当中的体验。
历经了多个web项目的实战检验,咱们对webapck在构建中逐步暴露出来的性能问题概括主要有以下几个方面:
代码全量构建速度过慢,即便是很小的改动,也要等待长时间才能查看到更新与编译后的结果(引入HMR热更新后有明显改进);
随着项目业务的复杂度增长,工程模块的体积也会急剧增大,构建后的模块一般要以M为单位计算;
多个项目之间共用基础资源存在重复打包,基础库代码复用率不高;
node的单进程实如今耗cpu计算型loader中表现不佳;
针对以上的问题,咱们来看看怎样利用webpack现有的一些机制和第三方扩展插件来逐个击破。
做为工程师,咱们一直鼓励要理性思考,用数据和事实说话,“我以为很慢”,“太卡了”,“太大了”之类的表述不免显得太笼统和太抽象,那么咱们不妨从以下几个方面来着手进行分析:
从项目结构着手,代码组织是否合理,依赖使用是否合理;
从webpack自身提供的优化手段着手,看看哪些api未作优化配置;
从webpack自身的不足着手,作有针对性的扩展优化,进一步提高效率;
在这里咱们推荐使用一个wepback的可视化资源分析工具:webpack-bundle-analyzer,在webpack构建的时候会自动帮你计算出各个模块在你的项目工程中的依赖与分布状况,方便作更精确的资源依赖和引用的分析。
从上图中咱们不难发现大多数的工程项目中,依赖库的体积永远是大头,一般体积能够占据整个工程项目的7-9成,并且在每次开发过程当中也会从新读取和编译对应的依赖资源,这实际上是很大的的资源开销浪费,并且对编译结果影响微乎其微,毕竟在实际业务开发中,咱们不多会去主动修改第三方库中的源码,改进方案以下:
webpack的资源入口一般是以entry为单元进行编译提取,那么当多entry共存的时候,CommonsChunkPlugin的做用就会发挥出来,对全部依赖的chunk进行公共部分的提取,可是在这里可能不少人会误认为抽取公共部分指的是能抽取某个代码片断,其实并不是如此,它是以module为单位进行提取。
假设咱们的页面中存在entry1,entry2,entry3三个入口,这些入口中可能都会引用如utils,loadash,fetch等这些通用模块,那么就能够考虑对这部分的共用部分机提取。一般提取方式有以下四种实现:
new webpack.optimize.CommonsChunkPlugin('common.js')
这种作法默认会把全部入口节点的公共代码提取出来, 生成一个common.js
new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);
只提取entry1
节点和entry2
中的共用部分模块, 生成一个common.js
new webpack.optimize.CommonsChunkPlugin({ name: 'vendors', minChunks: function (module, count) { return ( module.resource && /\.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ) } });
提取全部node_modules
中的模块至vendors
中,也能够指定minChunks
中的最小引用数;
entry = { vendors: ['fetch', 'loadash'] }; new webpack.optimize.CommonsChunkPlugin({ name: "vendors", minChunks: Infinity });
添加一个entry
名叫为vendors
,并把vendors
设置为所须要的资源库,CommonsChunk
会自动提取指定库至vendors
中。
在实际项目开发过程当中,咱们并不须要实时调试各类库的源码,这时候就能够考虑使用external选项了。
简单来讲external就是把咱们的依赖资源声明为一个外部依赖,而后经过script外链脚本引入。这也是咱们早期页面开发中资源引入的一种翻版,只是经过配置后能够告知webapck遇到此类变量名时就能够不用解析和编译至模块的内部文件中,而改用从外部变量中读取,这样能极大的提高编译速度,同时也能更好的利用CDN来实现缓存。
external的配置相对比较简单,只须要完成以下三步:
<head> <script src="//cdn.bootcss.com/jquery.min.js"></script> <script src="//cdn.bootcss.com/underscore.min.js"></script> <script src="/static/common/react.min.js"></script> <script src="/static/common/react-dom.js"></script> <script src="/static/common/react-router.js"></script> <script src="/static/common/immutable.js"></script> </head>
module.export = { externals: { 'react-router': { amd: 'react-router', root: 'ReactRouter', commonjs: 'react-router', commonjs2: 'react-router' }, react: { amd: 'react', root: 'React', commonjs: 'react', commonjs2: 'react' }, 'react-dom': { amd: 'react-dom', root: 'ReactDOM', commonjs: 'react-dom', commonjs2: 'react-dom' } } }
这里要提到的一个细节是:此类文件在配置前,构建这些资源包时须要采用amd/commonjs/cmd
相关的模块化进行兼容封装,即打包好的库已是umd模式包装过的,如在node_modules/react-router
中咱们能够看到umd/ReactRouter.js之类的文件,只有这样webpack
中的require
和import * from 'xxxx'
才能正确读到该类包的引用,在这类js的头部通常也能看到以下字样:
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = factory(require("react"));
} else if (typeof define === 'function' && define.amd) {
define(["react"], factory);
} else if (typeof exports === 'object') {
exports["ReactRouter"] = factory(require("react"));
} else {
root["ReactRouter"] = factory(root["React"]);
}
output: { libraryTarget: 'umd' }
因为经过external
提取过的js模块是不会被记录到webapck
的chunk
信息中,经过libraryTarget可告知咱们构建出来的业务模块,当读到了externals中的key时,须要以umd
的方式去获取资源名,不然会有出现找不到module的状况。
经过配置后,咱们能够看到对应的资源信息已经能够在浏览器的source map
中读到了。
对应的资源也能够直接由页面外链载入,有效地减少了资源包的体积。
咱们的项目依赖中一般会引用大量的npm包,而这些包在正常的开发过程当中并不会进行修改,可是在每一次构建过程当中却须要反复的将其解析,如何来规避此类损耗呢?这两个插件就是干这个用的。
简单来讲DllPlugin的做用是预先编译一些模块,而DllReferencePlugin则是把这些预先编译好的模块引用起来。这边须要注意的是DllPlugin必需要在DllReferencePlugin执行前先执行一次,dll这个概念应该也是借鉴了windows程序开发中的dll文件的设计理念。
相对于externals,dllPlugin
有以下几点优点:
react-addons-css-transition-group
这种原先从react核心库中抽取的资源包,整个代码只有一句话:module.exports = require('react/lib/ReactCSSTransitionGroup');
却由于从新指向了react/lib中,这也会致使在经过externals引入的资源只能识别react,寻址解析react/lib则会出现没法被正确索引的状况。
那么externals该如何使用呢,其实只须要增长一个配置文件:webpack.dll.config.js
:
const webpack = require('webpack'); const path = require('path'); const isDebug = process.env.NODE_ENV === 'development'; const outputPath = isDebug ? path.join(__dirname, '../common/debug') : path.join(__dirname, '../common/dist'); const fileName = '[name].js'; // 资源依赖包,提早编译 const lib = [ 'react', 'react-dom', 'react-router', 'history', 'react-addons-pure-render-mixin', 'react-addons-css-transition-group', 'redux', 'react-redux', 'react-router-redux', 'redux-actions', 'redux-thunk', 'immutable', 'whatwg-fetch', 'byted-people-react-select', 'byted-people-reqwest' ]; const plugin = [ new webpack.DllPlugin({ /** * path * 定义 manifest 文件生成的位置 * [name]的部分由entry的名字替换 */ path: path.join(outputPath, 'manifest.json'), /** * name * dll bundle 输出到那个全局变量上 * 和 output.library 同样便可。 */ name: '[name]', context: __dirname }), new webpack.optimize.OccurenceOrderPlugin() ]; if (!isDebug) { plugin.push( new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), new webpack.optimize.UglifyJsPlugin({ mangle: { except: ['$', 'exports', 'require'] }, compress: { warnings: false }, output: { comments: false } }) ) } module.exports = { devtool: '#source-map', entry: { lib: lib }, output: { path: outputPath, filename: fileName, /** * output.library * 将会定义为 window.${output.library} * 在此次的例子中,将会定义为`window.vendor_library` */ library: '[name]', libraryTarget: 'umd', umdNamedDefine: true }, plugins: plugin };
而后执行命令:
$ NODE_ENV=development webpack --config webpack.dll.lib.js --progress $ NODE_ENV=production webpack --config webpack.dll.lib.js --progress
便可分别编译出支持调试版和生产环境中lib静态资源库,在构建出来的文件中咱们也能够看到会自动生成以下资源:
common ├── debug │ ├── lib.js │ ├── lib.js.map │ └── manifest.json └── dist ├── lib.js ├── lib.js.map └── manifest.json
文件说明:
lib.js能够做为编译好的静态资源文件直接在页面中经过src连接引入,与externals的资源引入方式同样,生产与开发环境能够经过相似charles之类的代理转发工具来作路由替换;
manifest.json中保存了webpack中的预编译信息,这样等于提早拿到了依赖库中的chunk信息,在实际开发过程当中就无须要进行重复编译;
lib.js和manifest.json存在一一对应的关系,因此咱们在调用的过程也许遵循这个原则,如当前处于开发阶段,对应咱们能够引入common/debug文件夹下的lib.js和manifest.json,切换到生产环境的时候则须要引入common/dist下的资源进行对应操做,这里考虑到手动切换和维护的成本,咱们推荐使用add-asset-html-webpack-plugin进行依赖资源的注入,可获得以下结果:
<head> <script src="/static/common/lib.js"></script> </head> 在webpack.config.js文件中增长以下代码: const isDebug = (process.env.NODE_ENV === 'development'); const libPath = isDebug ? '../dll/lib/manifest.json' : '../dll/dist/lib/manifest.json'; // 将mainfest.json添加到webpack的构建中 module.export = { plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require(libPath), }) ] }
配置完成后咱们能发现对应的资源包已经完成了纯业务模块的提取
多个工程之间若是须要使用共同的lib资源,也只须要引入对应的lib.js和manifest.js便可,plugin配置中也支持多个webpack.DllReferencePlugin
同时引入使用,以下:
module.export = { plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require(libPath), }), new webpack.DllReferencePlugin({ context: __dirname, manifest: require(ChartsPath), }) ]
以上介绍均为针对webpack中的chunk计算和编译内容的优化与改进,对资源的实际体积改进上也较为明显,那么除此以外,咱们可否针对资源的编译过程和速度优化上作些尝试呢?
众所周知,webpack中为了方便各类资源和类型的加载,设计了以loader加载器的形式读取资源,可是受限于node的编程模型影响,全部的loader虽然以async的形式来并发调用,可是仍是运行在单个 node的进程以及在同一个事件循环中,这就直接致使了当咱们须要同时读取多个loader文件资源时,好比babel-loader须要transform各类jsx,es6的资源文件。在这种同步计算同时须要大量耗费cpu运算的过程当中,node的单进程模型就无优点了,那么happypack就针对解决此类问题而生。
happypack的处理思路是将原有的webpack对loader的执行过程从单一进程的形式扩展多进程模式,本来的流程保持不变,这样能够在不修改原有配置的基础上来完成对编译过程的优化,具体配置以下:
const HappyPack = require('happypack'); const os = require('os') const HappyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length}); // 启动线程池}); module:{ rules: [ { test: /\.(js|jsx)$/, // use: ['babel-loader?cacheDirectory'], use: 'happypack/loader?id=jsx', exclude: /^node_modules$/ } ] }, plugins:[ new HappyPack({ id: 'jsx', cache: true, threadPool: HappyThreadPool, loaders: ['babel-loader'] }) ]
咱们能够看到经过在loader中配置直接指向happypack提供的loader,对于文件实际匹配的处理 loader,则是经过配置在plugin属性来传递说明,这里happypack提供的loader与plugin的衔接匹配,则是经过id=happybabel
来完成。配置完成后,laoder的工做模式就转变成了以下所示:
happypack在编译过程当中除了利用多进程的模式加速编译,还同时开启了cache计算,能充分利用缓存读取构建文件,对构建的速度提高也是很是明显的,通过测试,最终的构建速度提高以下:
优化前:
优化后:
关于happyoack的更多介绍能够查看:
[happypack]()
|
[happypack 原理解析]()
uglifyJS凭借基于node开发,压缩比例高,使用方便等诸多优势已经成为了js压缩工具中的首选,可是咱们在webpack的构建中观察发现,当webpack build进度走到80%先后时,会发生很长一段时间的停滞,经测试对比发现这一过程正是uglfiyJS在对咱们的output中的bunlde部分进行压缩耗时过长致使,针对这块咱们可使用webpack-uglify-parallel来提高压缩速度。
从插件源码中能够看到,webpack-uglify-parallel的是实现原理是采用了多核并行压缩的方式来提高咱们的压缩速度。
plugin.nextWorker().send({ input: input, inputSourceMap: inputSourceMap, file: file, options: options }); plugin._queue_len++; if (!plugin._queue_len) { callback(); } if (this.workers.length < this.maxWorkers) { var worker = fork(__dirname + '/lib/worker'); worker.on('message', this.onWorkerMessage.bind(this)); worker.on('error', this.onWorkerError.bind(this)); this.workers.push(worker); } this._next_worker++; return this.workers[this._next_worker % this.maxWorkers];
使用配置也很是简单,只须要将咱们原来webpack中自带的uglifyPlugin配置:
new webpack.optimize.UglifyJsPlugin({ exclude:/\.min\.js$/ mangle:true, compress: { warnings: false }, output: { comments: false } }) 修改为以下代码便可: const os = require('os'); const UglifyJsParallelPlugin = require('webpack-uglify-parallel'); new UglifyJsParallelPlugin({ workers: os.cpus().length, mangle: true, compressor: { warnings: false, drop_console: true, drop_debugger: true } })
目前webpack官方也维护了一个支持多核压缩的UglifyJs插件:uglifyjs-webpack-plugin,使用方式相似,优点在于彻底兼容webpack.optimize.UglifyJsPlugin中的配置,能够经过uglifyOptions写入,所以也作为推荐使用,参考配置以下:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); new UglifyJsPlugin({ uglifyOptions: { ie8: false, ecma: 8, mangle: true, output: { comments: false }, compress: { warnings: false } }, sourceMap: false, cache: true, parallel: os.cpus().length * 2 })
wepback在2.X和3.X中从rolluo中借鉴了tree-shaking和Scope Hoisting,利用es6的module特性,利用AST对全部引用的模块和方法作了静态分析,从而能有效地剔除项目中的没有引用到的方法,并将相关方法调用概括到了独立的webpack_module中,对打包构建的体积优化也较为明显,可是前提是全部的模块写法必须使用ES6 Module进行实现,具体配置参考以下:
// .babelrc: 经过配置减小没有引用到的方法 { "presets": [ ["env", { "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }], // https://www.zhihu.com/question/41922432 ["es2015", {"modules": false}] // tree-shaking ] } // webpack.config: Scope Hoisting { plugins:[ // https://zhuanlan.zhihu.com/p/27980441 new webpack.optimize.ModuleConcatenationPlugin() ] }
在实际的开发过程当中,可灵活地选择适合自身业务场景的优化手段。
优化手段 | 开发环境 | 生产环境 |
---|---|---|
CommonsChunk | √ | √ |
externals | √ | |
DllPlugin | √ | √ |
Happypack | √ | |
uglify-parallel | √ |