webpack分包及异步加载套路

JenLangham 8年前
   <p>最近一个小项目是用 webpack 来进行构建的。其中用到了 webpack 分包异步加载的功能。今天抽时间看了下 webpack 打包后的文件,大致弄明白了 webpack 分包及异步加载的套路。</p>    <p>由于这个小项目是用自己写的一个路由,路由定义好了不同路径对应下的模板及逻辑代码:</p>    <p>webpack 配置文件:</p>    <pre>  var path = require('path'),      DashboardPlugin = require('webpack-dashboard/plugin'),      HtmlWebpackPlugin = require('html-webpack-plugin'),      webpack = require('webpack'),      ExtractTextPlugin = require('extract-text-webpack-plugin');    var PATHS = {      app: path.join(__dirname, 'src'),      dist: path.join(__dirname, 'dist')  }    var PKG = require('./package.json');  var TARGET = process.env.npm_lifecycle_event;   //获取当前正在运行的脚本名称    var isProduction = function() {      return process.env.NODE_ENV === 'production';  }        module.exports ={      entry: {          'index': path.join(__dirname, 'src/index.js'),          'lib': ['./src/lib/js/index.js'],      },      //filename是主入口文件的名称,即对应的entry      //chunkFilename对应的是非主入口文件的名称,chunk      output: {          path: PATHS.dist,          publicPath: '/static/taxi-driver/',    //publicPath 的话是打包的时候生成的文件链接,如果是在生产环境当然是用服务器地址,如果是开发环境就是用本地静态服务器的地址          filename: 'js/register/[name].js',          chunkFilename: 'js/register/[name].js',          //TODO: build文件中加入hash值      },      //生成source-map文件      devtool: isProduction ? null : 'source-map',      devServer: {          proxy: {              '/api/*': {                  target: 'http://localhost:3000',                  secure: false              }          }      },      module: {          loaders: [              {                  test: /\.js$/,                  exclude: /node_modules|picker.min.js/,                  loader: 'babel'              },              {                  test: /\.less$/,                  loader: ExtractTextPlugin.extract('style', 'css!less')              },              {                  test: /\.html$/,                  loader: 'raw'              },              {                  test: /\.css$/,                  loader: ExtractTextPlugin.extract('style', 'css')              },              {                  test: /\.json$/,                  loader: 'json'              }          ]      },      resolve: {          alias: {              src: path.join(__dirname, 'src'),              modules: path.join(__dirname, 'src/modules'),              lessLib: path.join(__dirname, 'src/lib/less'),                jsLib: path.join(__dirname, 'src/lib/js'),              components: path.join(__dirname, 'src/components')          },          extensions: ['', '.js', '.less', '.html', '.json'],      },      plugins: [          new HtmlWebpackPlugin({              title: '认证资料',              template: './dist/assets/info.html',              inject: 'body',              filename: 'pages/register/index.html'   //输出html文件的位置          }),          new DashboardPlugin(),          new ExtractTextPlugin('css/register/style.css'),     //将引入的样式文件单独抽成style.css文件并插入到head标签当中,带有路径时,最后打包          new webpack.optimize.CommonsChunkPlugin({              name: 'common',              filename: 'js/register/common.js',              minChunks: 3          })      ]  }</pre>    <p>接下来是定义好的路由文件:</p>    <pre>  const Router = new Route();            Route          .addRoute({              path: 'path1',              viewBox: '.public-container',              template: require('modules/path1/index.html'),              pageInit() {              //webpack提供的分包的API. require.ensure                  require.ensure([], () => {                      let controller = require('modules/path1/controller');                      Router.registerCtrl('path1', new controller('.public-container'));                  }, 'path1');              }          })          .addRoute({              path: 'path2',              viewBox: '.public-container',              template: require('modules/path2/index.html'),              pageInit() {                  require.ensure([], () => {                      let controller = require('modules/path2/controller');                      Router.registerCtrl('path2', new controller('.public-container'));                  }, 'path2');              }          });</pre>    <p>最后 webpack 会将这2个需要异步加载的模块,分别打包成 path1.js 和 path2.js .</p>    <p>当页面的路径为:</p>    <p>http://localhost:8080/pages/register/#/path1 时,会加载 path1.js 文件</p>    <p>http://localhost:8080/pages/register/#/path2 时,会加载 path2.js 文件.</p>    <p>再来看看 webpack 打包后的文件:</p>    <p>其中在 common.js 中, webpack 定义了一个全局函数 webpackJsonp .这个全局函数在项目一启动后就定义好。</p>    <p>局部函数 __webpack_require__ 用以在某一个模块中初始化或者调用其他的模块方法。同时这个函数还有一个静态方法 __webpack_require__.e 这个方法就是用来异步加载 js 文件的。</p>    <p>接下来一步一步的看:</p>    <pre>  //common.js      (function(modules) {          //modules用来保存所有的分包,它是一个数组,数组每个元素对应的都是callback,每个分包都是通过数字来进行标识的                    //定义好的全局函数webpackJsonp          //大家可以看看其他打包好的文件,例如index.js, path1.js和path2.js文件.都是webpackJsonp()这种的形式,大家用过JSONP应该会很好理解。首先在前端定义好函数,然后后端下发组装好的函数js文件,前端获取到这个文件后就可以立即进行执行了          var parentJsonpFunction = window["webpackJsonp"];          window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {              var moduleId, chunkId, i = 0, callbacks = [];  /******/         for(;i < chunkIds.length; i++) {  /******/             chunkId = chunkIds[i];  /******/             if(installedChunks[chunkId])  /******/                 callbacks.push.apply(callbacks, installedChunks[chunkId]);  /******/             installedChunks[chunkId] = 0;  /******/         }                  //这个全局函数会将各个分包缓存到modules  /******/         for(moduleId in moreModules) {  /******/             modules[moduleId] = moreModules[moduleId];  /******/         }  /******/         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);  /******/         while(callbacks.length)  /******/             callbacks.shift().call(null, __webpack_require__);                  //用以启动整个应用  /******/         if(moreModules[0]) {  /******/             installedModules[0] = 0;  /******/             return __webpack_require__(0);  /******/         }          };      })([]);</pre>    <pre>  // The require function              //通过数字标识的moduleId  /******/     function __webpack_require__(moduleId) {    /******/         // Check if module is in cache  /******/         if(installedModules[moduleId])  /******/             return installedModules[moduleId].exports;    /******/         // Create a new module (and put it into the cache)  /******/         var module = installedModules[moduleId] = {  /******/             exports: {},  /******/             id: moduleId,  /******/             loaded: false  /******/         };    /******/         // Execute the module function  /******/         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);    /******/         // Flag the module as loaded  /******/         module.loaded = true;    /******/         // Return the exports of the module  /******/         return module.exports;  /******/     }    /******/     // This file contains only the entry chunk.  /******/     // The chunk loading function for additional chunks              //异步加载函数  /******/     __webpack_require__.e = function requireEnsure(chunkId, callback) {  /******/         // "0" is the signal for "already loaded"  /******/         if(installedChunks[chunkId] === 0)  /******/             return callback.call(null, __webpack_require__);    /******/         // an array means "currently loading".  /******/         if(installedChunks[chunkId] !== undefined) {  /******/             installedChunks[chunkId].push(callback);  /******/         } else {                      //创建script表情,请求js文件  /******/             // start chunk loading  /******/             installedChunks[chunkId] = [callback];  /******/             var head = document.getElementsByTagName('head')[0];  /******/             var script = document.createElement('script');  /******/             script.type = 'text/javascript';  /******/             script.charset = 'utf-8';  /******/             script.async = true;    /******/             script.src = __webpack_require__.p + "js/register/" + ({"0":"index","1":"path1","2":"path2"}[chunkId]||chunkId) + ".js";  /******/             head.appendChild(script);  /******/         }  /******/     };    /******/     // expose the modules object (__webpack_modules__)  /******/     __webpack_require__.m = modules;    /******/     // expose the module cache  /******/     __webpack_require__.c = installedModules;    /******/     // __webpack_public_path__              //配置文件中定义的publicPath,build完后加载文件的路径  /******/     __webpack_require__.p = "/static/taxi-driver/";  /******/ })</pre>    <p>在最后输出的 index.html 文件中首先加载的是这个 common.js 文件,然后是入口文件 index.js 。因为这个实例代码里面没有很多共用文件,因此 webpack 自己提供的 commonChunkPlugin 这个插件并没有起到作用,本来作为共用文件的 xRoute.js 因此也被打包进入了 index.js .</p>    <pre>  webpackJsonp([0, 3], [          /* 0 */  /***/ function(module, exports, __webpack_require__) {        'use strict';        __webpack_require__(1);        __webpack_require__(8);    /***/ },  /* 1 */  /* 2 */  /* 3 */  //....  /* 8 */      ])</pre>    <p>index.js 文件在 common.js 后加载,加载完后即开始执行.大家还记得 webpackJsonp 这个全局函数里面的倒数3行代码吧。就是用以调用这里:</p>    <pre>  /* 0 */      function(module, exports, __webpack_require__) {        'use strict';        __webpack_require__(1);        __webpack_require__(8);    }</pre>    <p>其中模块 Id 为 1 和 8 的内容请查看相应文件, 其中 模块1 为我定义的路由文件,在执行 模块1 的代码前,会加载 模块2 的内容, 模块2 的内容为我定义的路由库。</p>    <p>接下来就看下 模块1 中路由定义的具体内容:</p>    <pre>  /* 1 */  /***/ function(module, exports, __webpack_require__) {        'use strict';        Object.defineProperty(exports, "__esModule", {          value: true      });            //加载路由库      var _index = __webpack_require__(2);      //实例化一个路由      var Router = new _index.Route();      //定义好的路由规则      Router.home('path1').addRoute({          path: 'path1',          viewBox: '.public-container',          //模板文件,为模块4          template: __webpack_require__(4),          pageInit: function pageInit() {          //这个方法是在common.js中__webpack_require__的静态方法,用来异步加载js。          //异步加载js的文件(即chunk)用来数字来标识,chunk的顺序从0开始.          //这里path1.js的chunk num为1,大家可以回过头到common.js的__webpack_require__.e方法里面看看,里面已经做好了chunk num和模块文件的映射, chunk 1对应的模块文件为path1.js,chunk 2对用的模块文件为path2.js          //__webpack_require__.e()接收的第二个参数为异步加载模块后的回调. 当path1.js被加载完后,在modules里面进行了缓存.这时就可以通过模块id去获取这个模块。然后进行初始化等后续的操作              __webpack_require__.e/* nsure */(1, function () {                  var controller = __webpack_require__(6);                  Router.registerCtrl('path1', new controller('.public-container'));              });          }      }).addRoute({          path: 'path2',          viewBox: '.public-container',          //模板文件,为模块5          template: __webpack_require__(5),          pageInit: function pageInit() {              __webpack_require__.e/* nsure */(2, function () {                  var controller = __webpack_require__(7);                  Router.registerCtrl('path2', new controller('.public-container'));              });          }      });        Router.bootstrap();        exports.default = Router;    /***/ },</pre>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007962830</p>    <p> </p>