Home

Awesome

1.webpack打包策略分析

1.1 单入口文件

通过下面的打包结果,并结合下面说的多入口文件打包问题,应该是不难理解的。

/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
      module.exports = __webpack_require__(1);
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
       var util1 = __webpack_require__(2)
       var util2 = __webpack_require__(3)
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
      var util2 = __webpack_require__(3)
/***/ },
/* 3 */
/***/ function(module, exports) {
    module.exports = {"name": "util2.js"}

/***/ }
/******/ ]);

这里只是需要注意一点,就是虽然有entry.js和util1.js同时引用了util2模块,但是我们最终也只会为util2生成一个id!webpack打包的原理为,在入口文件中,对每个require资源文件配置一个id, 也 就是说,对于同一个资源,就算是require多次的话,它的id也是一样的,所以无论在多少个文件中 require,它都只会打包一份。

1.2 多入口文件打包分析

1.2.1 多入口文件打包ID分配

首先webpack.config.js配置如下:

 var path = require('path')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var webpack = require('webpack')
module.exports = {
    entry: {
        index: ['./src/js1/entry.js'],   //配置两个入口
        index2: ['./src/js1/entry2.js']
    },
    output: {
        path: path.resolve(__dirname, './dist/static'),
        publicPath: 'static/',
        filename: '[name].[chunkhash].js'
    },
    resolve: {
         extensions: ['', '.js', '.less', '.swig', '.html']
    },
    module: {
        loaders: [
        ]
    },
    plugins: [
    ]
}

此时会在./dist/static目录中生成两个文件,分别对应于index.[chunkhash].js和index2.[chunkhash].js。

其中index.[chunkhash].js文件内容如下:

/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
        module.exports = __webpack_require__(1);
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
    var util1 = __webpack_require__(2)
    var util2 = __webpack_require__(3)
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
       var util2 = __webpack_require__(3)
/***/ },
/* 3 */
/***/ function(module, exports) {
        module.exports = {"name": "util2.js"}
/***/ }
/******/ ]);

很显然,入口文件的id是1,而入口文件引用的资源的id依次增加。同时注意一点:

/* 0 */
/***/ function(module, exports, __webpack_require__) {
        module.exports = __webpack_require__(1);
/***/ },

也就是id为0的打包模块其实是对入口文件调用的模块,其中通过module.exports对外导出!

其中index2.[chunkhash].js文件内容如下:

/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
         module.exports = __webpack_require__(4);
/***/ },
/* 1 */,
/* 2 */,
/* 3 */
/***/ function(module, exports) {
        module.exports = {"name": "util2.js"}
/***/ },
/* 4 */
/***/ function(module, exports, __webpack_require__) {
      var util2 = __webpack_require__(3)
     setTimeout(function() {console.log(util2.name), 2000})
/***/ }
/******/ ]);

首先,和上面分析的一样,我们对入口文件调用的模块id是0,通过module.exports向外导出。

/* 4 */
/***/ function(module, exports) {
  // removed by extract-text-webpack-plugin
/***/ },
/* 5 */,
/* 6 */,
/* 7 */,
/* 8 */
/***/ function(module, exports) {
  // removed by extract-text-webpack-plugin
/***/ },
/* 9 */,
/* 10 */
/***/ function(module, exports) {

  // removed by extract-text-webpack-plugin

/***/ }

很清楚的知道id为5,6,7,8的情况没有被分配,因为没有被打包进来,这在单入口文件中也是存在的,而和extract-text-webpack-plugin没有关系!而之所以是这样,我猜测(需要证实)是webpack在打包的过程中自己生成了很多文件,而这些文件也分配了ID值,但是在我们的文件中并不需要这些模块,所以就没有引入为空!这可能和webpack自身的功能有关系。

1.2.2 多入口文件打包结果

下面给出了控制台打包信息:

其中asset部分就是生产的文件的文件名,是name+chunkhash的格式;chunks就是对生成的文件的数字标注(chunkid);chunkName就是我们在webpack配置中指定;

结合上面的分析很容易知道,对于入口文件调用的模块Asset(打包资源)部分都是0,而其余资源的部分都是webpac打包后生成的id值!

1.2.3 CommonChunkPlugin插件作用

之前提到过,每个入口文件,都会独立打包自己依赖的模块,那就会造成很多重复打包的模块,有没有一种方法 能把多个入口文件中,共同依赖的部分给独立出来呢? 肯定是有的 CommonsChunkPlugin

这个插件使用非常简单,它原理就是把多个入口共同的依赖都给定义成一个新入口。为何我这里说是定义成新入口呢,因为这个名字不仅仅对应着js 而且对于着和它相关的css等,比如 HtmlWebpackPlugin 中 就能体现出来,可以它的 chunks中 加入 common 新入口,它会自动把common 的css也导入html

什么是chunkid?

可以参见控制台的部分:

从图中可以看到,我们最终输出的common.js也是会有自己独立的chunkid的(也就是chunks列),虽然我们在webpack的entry配置中并没有指明(每个chunkid对应的是一个js文件):

entry: {
    index: ['./src/js1/entry.js'],   
    index2: ['./src/js1/entry2.js']
  },

这是CommonChunkPlugin插件完成的。为何要出来一个chunkid呢? 这个chunkid的作用就是,标记这个js文件是否已经加载过了。我们首先分析下这个plugin抽取出来的多个入口公共的代码部分:

/******/ (function(modules) { // webpackBootstrap
/******/  // install a JSONP callback for chunk loading
/******/  var parentJsonpFunction = window["webpackJsonp"];
/******/  window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
/******/    // add "moreModules" to the modules object,
/******/    // then flag all "chunkIds" as loaded and fire callback
/******/    var moduleId, chunkId, i = 0, callbacks = [];
/******/    for(;i < chunkIds.length; i++) {
/******/      chunkId = chunkIds[i];
/******/      if(installedChunks[chunkId])//非0表示没有加载完成,否则是数组,数组元素是函数
/******/        callbacks.push.apply(callbacks, installedChunks[chunkId]);
/******/      installedChunks[chunkId] = 0;//加载完成
/******/    }
/******/    for(moduleId in moreModules) {
/******/      modules[moduleId] = moreModules[moduleId];
/******/    }//modules中得到的都是函数,in遍历数组的时候key是下标,此处modules存储的是键值是每一个函数
/******/    if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
                 //首先调用父级jsonpFunction
/******/    while(callbacks.length)
/******/      callbacks.shift().call(null, __webpack_require__);
                     //得到callback数组中的函数并调用,每一个回调函数传入一个__webpack_require__就可以了
/******/    if(moreModules[0]) {
/******/      installedModules[0] = 0;
/******/      return __webpack_require__(0);
/******/    }
                //加载第一个模块0,这个模块是我们的入口文件的exports对象,相当于执行模块了!其他的模块只是打包进去,这个0表示直接执行了
/******/  };

/******/  // The module cache
/******/  var installedModules = {};

/******/  // object to store loaded and loading chunks
/******/  // "0" means "already loaded"
/******/  // Array means "loading", array contains callbacks
            //如果是正在加载那么这里就是一个数组,而且数组中包含的是回调函数
/******/  var installedChunks = {
/******/    2:0 //common.js的chunkID是2表示一开始的时候就加载完成,因为他是通用模块必须先加载,然后加载其他的
/******/  };

/******/  // The require function
/******/  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
                //入口执行模块是函数签名是function(module, exports, __webpack_require__) 
/******/    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 {
/******/      // 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 + "" + chunkId + "." + ({"0":"index","1":"index2"}[chunkId]||chunkId) + "." + {"0":"2a3a9ad51d7007de1a89","1":"e7a73c1617697d3e2912"}[chunkId] + ".js";
/******/      head.appendChild(script);
/******/    }
/******/  };

/******/  // expose the modules object (__webpack_modules__)
            //向外导出的modules包含了common.js这个文件,同时也包含我们的moreModules数组中的modules
/******/  __webpack_require__.m = modules;

/******/  // expose the module cache
            //webpack缓存模块
/******/  __webpack_require__.c = installedModules;

/******/  // __webpack_public_path__
            //webpack的__webpack_public_path__
/******/  __webpack_require__.p = "static/";
/******/ })
/************************************************************************/
/******/ ({

/***/ 3:
/***/ function(module, exports) {

  module.exports = {"name": "util2.js"}


/***/ }

/******/ });

(1)webpackJsonp第一个参数是chunkId,但是我们的CommonChunkPlugin抽取出来的部分是作为modules参数传入的(在webpack中每一个文件都是一个chunk,所以这个common.js也有自己的chunkid),而且是一个引用,所以每次加载一个chunk的时候,他们引用的都是同一个(防止一个页面重复加载一个模块的代码,如多次加载jQuery)。

(2)这里要看清楚什么是chunk,什么是module,在webpackJsonp这个函数中第一个参数就是chunkId对应于chunk,而而第二个参数就是chunk中被打包进去的多个module,这也是为什么下面的webpackJsonpCallback的函数签名是如下:

webpackJsonpCallback(chunkIds, moreModules)

但是仔细想想,我们的common.js其实更像是一个module而不是chunk(因为他会被modules形参接受)。但是从控制台中的输出你可以知道其实webpack已经把它当做chunk了(webpack中每一个文件都是一个chunk)。我们common.js虽然没有在entry中配置,但是我们实际上在common.js中被当做自执行函数的参数传入最后放在modules中,以后再每一个chunk中require公有的这个文件的时候就直接require这个参数指定的内容就可以了

(3)modules集合最终得到的是一个chunk中所有的moreModules(该chunk独有的module)以及共有的commmon.js这个module(执行的时候只要运行moreModules[0]就可以了)。每次加载一个chunk,其modules最终都会包含我们的CommonChunkPlugin抽取出来的部分。例如我们的例子,直接执行下面代码:

webpack --profile --json > stats.json

然后在visualizer中打开你就会看到如下的图形:

我们知道chunkId为0,1的模块父级chunk是'main',而'main'的父级chunk是'vendor',其实我这里关心的是打包后的代码,其如下:

0.bundle.js(只是含有非公有代码部分)

webpackJsonp([0,3],[
/* 0 */,
/* 1 */
/***/ (function(module, exports, __webpack_require__) {})])

"use strict";

其表示我们的0.bundle.js也会加载common.js

1.bundle.js(只是含有非公有代码部分)

webpackJsonp([1,3],[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";})])

其表示我们的1.bundle.js也会加载common.js

bundle.js(业务逻辑代码)

webpackJsonp([2,3],{
/***/ 2:
/***/ (function(module, exports, __webpack_require__) {

"use strict";
//code splitting
if (document.querySelectorAll('a').length) {
    __webpack_require__.e/* require.ensure */(1).then((function () {
        var Button = __webpack_require__(0).default;
        var button = new Button('google.com');
        button.render('a');
    }).bind(null, __webpack_require__)).catch(__webpack_require__.oe);
}

if (document.querySelectorAll('h1').length) {
    __webpack_require__.e/* require.ensure */(0).then((function () {
        var Header = __webpack_require__(1).default;
        new Header().render('h1');
    }).bind(null, __webpack_require__)).catch(__webpack_require__.oe);
}
/***/ })

},[2]);

其表示我们的bundle.js也会加载common.js,而这里webpackJsonp有三个参数,和上面的webpackJsonp作用具有本质差别。因此,当你加载这个main.js的时候,其并不是马上加载common.js,但是只有满足了这个条件后,加载了其他的chunk都会同时把common.js加载下来,这就达到了按需加载的目的!所以webpackJsonp函数第一个参数数组中的第二个元素后都是表示该chunk依赖的其他的chunk集合!

(4)__webpack_require__方法最后返回的是modules[moduleId].call(module.exports, module, module.exports, webpack_require);也就是执行了id为0的入口文件的exports后得到的对象,这就是这个模块的最后返回值

(5)在html中必须先加载common.js文件

     <script src="{{ root }}common.js"><\/script>
    <script src="{{ root }}index.js"><\/script>

下面是每一个打包后的文件对common.js部分的依赖:

webpackJsonp([1],[
/* 0 */
/***/ function(module, exports, __webpack_require__) {
  module.exports = __webpack_require__(4);
/***/ },
/* 1 */,
/* 2 */,
/* 3 */,
/* 4 */
/***/ function(module, exports, __webpack_require__) {

  var util2 = __webpack_require__(3)
  setTimeout(function() {console.log(util2.name), 2000})

/***/ }
]);

通过这里你因该会知道,在执行commonChunkPlugin之前是不存在我们的common.js的,而加上了common.js后,所有的chunk之间的关系都是要维护的!

什么是moduleid?

结合上面的chunkid应该不难理解

2.参考资料

webpack打包原理解析