关于前端:webpack三两事浅入深出原理解析构建优化

3次阅读

共计 23553 个字符,预计需要花费 59 分钟才能阅读完成。

基础知识回顾

  • 入口(entry)

    module.exports = {entry: './path/to/my/entry/file.js'};
    // 或者
    module.exports = {
      entry: {main: './path/to/my/entry/file.js'}
    };
  • 输入(output)

    module.exports = {
      output: {filename:'[name][chunkhash:8].js',
        path:path.resolve(__dirname,'dist')
      }
    };
  • loader
    预处理 loader

    • css-loader 解决 css 中门路援用等问题
    • style-loader 动静把款式写入 css
    • sass-loader scss 编译器
    • less-loader less 编译器
    • postcss-loader scss 再解决

解决 js loader

  • babel-loader
  • jsx-loader
  • ts-loader

图片解决 loader

    • url-loader
    • 插件 (plugin)
      plugins 外面放的是插件,插件的作用在于进步开发效率,可能解放双手,让咱们去做更多有意义的事件。一些很 low 的事就通通交给插件去实现。

      const webpackConfig = {
          plugins: [
              // 革除文件
              new CleanWebpackPlugin(),
              //css 独自打包
              new MiniCssExtractPlugin({filename: "[name].css",
                  chunkFilename: "[name].css"
              }),
              // 引入热更新插件
              new webpack.HotModuleReplacementPlugin()]
      }
    • 模式(mode)

      • production 生产环境
    • development 开发环境

      • 晋升了构建速度
      • 默认为开发环境,不须要专门配置
      • 提供压缩性能,不须要借助插件
      • 提供SouceMap,不须要专门配置
    • 浏览器兼容性(browser compatibility)
    • 环境(environment)

    我的项目中具体配置

    构建过程

    Webpack 解决应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中蕴含应用程序须要的每个模块,而后将所有模块打包成一个或多个 bundle

    其实就是:Webpack 是一个 JS 代码打包器。

    至于图片、CSS、Less、TS 等其余文件,就须要 Webpack 配合 loader 或者 plugin 性能来实现。

    构建流程

    1. 依据配置,辨认入口文件;
    2. 逐层辨认模块依赖(包含 Commonjs、AMD、或 ES6 的 import 等,都会被辨认和剖析);
    3. Webpack 次要工作内容就是剖析代码,转换代码,编译代码,最初输入代码;
    4. 输入最初打包后的代码。

    webpack 构建的三个阶段:

    1. 初始化阶段
    2. 编译阶段
    3. 输入阶段

    初始化

    • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。
    • 初始化默认参数配置: new WebpackOptionsDefaulter().process(options)
    • 实例化 Compiler 对象: 用上一步失去的参数初始化 Compiler 实例,Compiler 负责文件监听和启动编译。Compiler 实例中蕴含了残缺的 Webpack 配置,全局只有一个 Compiler 实例。
    • 加载插件: 顺次调用插件的 apply 办法,让插件能够监听后续的所有事件节点。同时给插件传入 compiler 实例的援用,以不便插件通过 compiler 调用 Webpack 提供的 API。
    • 解决入口: 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为前面该 Entry 的递归解析工作做筹备

    编译

    1、生成 chunk

    chunk 是 webpack 外部运行时的概念;一个 chunk 是对依赖图的局部进行封装的后果(`Chunkthe class is the encapsulation for parts of your dependency graph`);能够通过多个 entry-point 来生成一个 chunk
    chunk 能够分为三类;

    • entry chunk

      • 蕴含 webpack runtime code 并且是最先执行的 chunk
    • initial chunk

      • 蕴含同步加载进来的 module 且不蕴含 runtime code 的 chunk
      • 在 entry chunk 执行后再执行的
    • normal chunk

      • 应用 require.ensureSystem.importimport() 异步加载进来的module,会被放到 normal chunk 中

    每个 chunk 都至多有一个属性:

    • name: 默认为 main
    • id: 惟一的编号,开发环境和 name 雷同,生产环境是一个数字,从 0 开始

    2、构建依赖模块

    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 下产生的数组是空,整个文件依赖就解析结束

    3、产生 chunk assets

    在第二步实现当前,chunk 中会产生一个模块列表,列表中蕴含了 模块 id 模块转换后的代码

    接下来,webpack 会依据配置为 chunk 生成一个 资源列表,即chunk assets, 资源列表能够了解为是生成到最终文件的文件名和文件内容

    • 为什么叫资源列表呢?
    • 因为有可能配置 devtool 生成的除了./dist/main.js 还有./dist/main.js.map

    即:文件名:./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(一捆)

    4、合并 chunk assets

    将多个 chunk 的 assets 合并到一起,并产生一个总的 hash

    输入

    webpack 将利用 node 中的 fs 模块(文件解决模块),依据编译产生的总的 assets,生成相应的文件

    波及术语

    1. module: 模块,宰割的代码单元,webpack 中的模块能够是任何内容的文件,不仅限于 JS
    2. chunk: webpack 外部构建模块的块,一个 chunk 中蕴含多个模块,这些模块是从入口模块通过依赖剖析得来的
    3. bundle:chunk 构建好模块后会生成 chunk 的资源清单,清单中的每一项就是一个 bundle,能够认为 bundle 就是最终生成的文件
    4. hash:最终的资源清单所有内容联结生成的 hash 值
    5. chunkhash: chunk 生成的资源清单内容联结生成的 hash 值
    6. chunkname:chunk 的名称,如果没有配置则应用 main
    7. id: 通常指 chunk 的惟一编号,如果在开发环境下构建,和 chunkname 雷同;如果是生产环境下构建,则应用一个从 0 开始的数字进行编号

    HMR 热更新原理

    简介

    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);

    热更新

    热更新流程

    1. 文件系统发生变化
    2. 当监听到文件发生变化时,webpack 应用 HotModuleReplacementPlugin 编译文件,并将代码保留在内存中(webpack-dev-middleware)。
    3. 同时,webpack-dev-server 通过编译器 compiler 取得文件的编译状况。
    4. 在 compiler 的 done 钩子函数(生命周期)里调用_sendStats 发送向 client 发送 hash 值,在 client 保留下来。
    5. client 接管到 ok 或 warning 音讯后调用 reloadApp 公布客户端查看更新事件 webpackHotUpdate。
    6. webpack/hot 监听到 webpackHotUpdate 事件,调用 check 办法进行 hash 值比照以及查看各 modules 是否须要更新。
    7. 调用 JsonpMainTemplate.runtime 的 hotDownloadManifest 办法向 server 端发送 ajax 申请
    8. 服务端返回 hot-update.json(manifest)文件,该文件蕴含所有要更新模块的 hash 值和 chunk 名。
    9. JsonpRuntime 依据返回的 json 值应用 jsonp 申请具体的代码块
    10. jsonp 返回最新的 chunk 代码,并间接执行。
    11. HotModulePlugin 将会对新旧模块进行比照,决定是否更新模块,查看模块之间的依赖关系,更新模块的同时更新模块间的依赖援用。
    12. HMR runtime 自身并不会解决代码批改,它会将不同文件交给对应的 loader runtime 解决
    13. 更新代码
    14. 如果更新失败,则间接刷新

    webpaserver 端源码

    在我的项目初始化时,服务端与客户端曾经开启了长连贯服务,当 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。

    client 端源码

    客户端 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);// 执行代码模块
            }
            ...
        }
        ...
    }

    手写 webpack 构建工具

    手写 webpack 流程

    AST

    AST(Abstract Syntax Tree)

    形象语法树,源代码语法结构的一种形象示意

    • 以树状的模式体现编程语言的语法结构
    • 树上的每个节点都示意源代码中的一种构造

    AST 生成过程

    形象语法树的生成次要依附的是 Javascript Parser(js 解析器)

    • 词法剖析(Lexical Analysis)
    • 语法分析(Parse Analysis)

    在手写 webpack 中应用

    通过 Visitor 实现依赖的收集

    访问者(visitor)是一个用于 AST 遍历的跨语言模式,定义 了用于在一个树状构造中获取具体节点的办法

    树的宽度优先搜寻(BFS)算法思维

    利用于循环剖析依赖

    树的宽度优先搜寻(BFS)算法思维

    循环剖析后果

    打包后果为一个 IIFE

    打包后果剖析

    后果运行剖析

    webpack 构建优化

    背景

    现在前端工程化的概念早曾经深入人心,抉择一款适合的编译和资源管理工具曾经成为了所有前端工程中的标配,而在诸多的构建工具中,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 成,而且在每次开发过程中也会从新读取和编译对应的依赖资源,这其实是很大的的资源开销节约,而且对编译后果影响微不足道,毕竟在理论业务开发中,咱们很少会去被动批改第三方库中的源码,改良计划如下:

    计划一、合理配置 CommonsChunkPlugin

    webpack 的资源入口通常是以 entry 为单元进行编译提取,那么当多 entry 共存的时候,CommonsChunkPlugin 的作用就会施展进去,对所有依赖的 chunk 进行公共局部的提取,然而在这里可能很多人会误认为抽取公共局部指的是能抽取某个代码片段,其实并非如此,它是以 module 为单位进行提取。

    假如咱们的页面中存在 entry1,entry2,entry3 三个入口,这些入口中可能都会援用如 utils,loadash,fetch 等这些通用模块,那么就能够思考对这部分的共用局部机提取。通常提取形式有如下四种实现:

    1、传入字符串参数,由 chunkplugin 主动计算提取

    new webpack.optimize.CommonsChunkPlugin('common.js')

    这种做法默认会把所有入口节点的公共代码提取进去, 生成一个common.js

    2、有抉择的提取公共代码

    new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);

    只提取 entry1 节点和 entry2 中的共用局部模块, 生成一个common.js

    3、将 entry 下所有的模块的公共局部(可指定援用次数)提取到一个通用的 chunk 中

    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 中的最小援用数;

    4、抽取 enry 中的一些 lib 抽取到 vendors 中

    entry = {vendors: ['fetch', 'loadash']
    };
    new webpack.optimize.CommonsChunkPlugin({
        name: "vendors",
        minChunks: Infinity
    });

    增加一个 entry 名叫为 vendors,并把vendors 设置为所须要的资源库,CommonsChunk会主动提取指定库至 vendors 中。

    计划二、通过 externals 配置来提取罕用库

    在理论我的项目开发过程中,咱们并不需要实时调试各种库的源码,这时候就能够思考应用 external 选项了。

    简略来说 external 就是把咱们的依赖资源申明为一个内部依赖,而后通过 script 外链脚本引入。这也是咱们晚期页面开发中资源引入的一种翻版,只是通过配置后能够告知 webapck 遇到此类变量名时就能够不必解析和编译至模块的外部文件中,而改用从内部变量中读取,这样能极大的晋升编译速度,同时也能更好的利用 CDN 来实现缓存。

    external 的配置绝对比较简单,只须要实现如下三步:

    1、在页面中退出须要引入的 lib 地址,如下:

    <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>

    2、在 webapck.config.js 中退出 external 配置项:

    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 中的 requireimport * 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"]);

    }

    3、十分重要的是肯定要在 output 选项中退出如下一句话:

    output: {libraryTarget: 'umd'}

    因为通过 external 提取过的 js 模块是不会被记录到 webapckchunk信息中,通过 libraryTarget 可告知咱们构建进去的业务模块,当读到了 externals 中的 key 时,须要以 umd 的形式去获取资源名,否则会有呈现找不到 module 的状况。

    通过配置后,咱们能够看到对应的资源信息曾经能够在浏览器的 source map 中读到了。

    对应的资源也能够间接由页面外链载入,无效地减小了资源包的体积。

    计划三、利用 DllPlugin 和 DllReferencePlugin 预编译资源模块

    咱们的我的项目依赖中通常会援用大量的 npm 包,而这些包在失常的开发过程中并不会进行批改,然而在每一次构建过程中却须要重复的将其解析,如何来躲避此类损耗呢?这两个插件就是干这个用的。

    简略来说 DllPlugin 的作用是事后编译一些模块,而 DllReferencePlugin 则是把这些事后编译好的模块援用起来。这边须要留神的是 DllPlugin 必须要在 DllReferencePlugin 执行前先执行一次,dll 这个概念应该也是借鉴了 windows 程序开发中的 dll 文件的设计理念。

    绝对于 externals,dllPlugin 有如下几点劣势:

    • dll 预编译进去的模块能够作为动态资源链接库可被重复使用,尤其适宜多个我的项目之间的资源共享,如同一个站点 pc 和手机版等;
    • dll 资源能无效地解决资源循环依赖的问题,局部依赖库如:react-addons-css-transition-group这种原先从 react 外围库中抽取的资源包,整个代码只有一句话:
    module.exports = require('react/lib/ReactCSSTransitionGroup');

    却因为从新指向了 react/lib 中,这也会导致在通过 externals 引入的资源只能辨认 react, 寻址解析 react/lib 则会呈现无奈被正确索引的状况。

    • 因为 externals 的配置项须要对每个依赖库进行一一定制,所以每次减少一个组件都须要手动批改,稍微繁琐,而通过 dllPlugin 则能齐全通过配置读取,缩小保护的老本;

    1、配置 dllPlugin 对应资源表并编译文件

    那么 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 信息,在理论开发过程中就无须要进行反复编译;

    2、dllPlugin 的动态资源引入

    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),
          })
      ]

    计划四、应用 Happypack 减速你的代码构建

    以上介绍均为针对 webpack 中的 chunk 计算和编译内容的优化与改良,对资源的理论体积改良上也较为显著,那么除此之外,咱们是否针对资源的编译过程和速度优化上做些尝试呢?

    家喻户晓,webpack 中为了不便各种资源和类型的加载,设计了以 loader 加载器的模式读取资源,然而受限于 node 的编程模型影响,所有的 loader 尽管以 async 的模式来并发调用,然而还是运行在单个 node 的过程以及在同一个事件循环中,这就间接导致了当咱们须要同时读取多个 loader 文件资源时,比方 babel-loader 须要 transform 各种 jsx,es6 的资源文件。在这种同步计算同时须要大量消耗 cpu 运算的过程中,node 的单过程模型就无劣势了,那么 happypack 就针对解决此类问题而生。

    开启 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 原理解析]()

    计划五、加强 uglifyPlugin

    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})

    计划六、Tree-shaking & Scope Hoisting

    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
    正文完
     0