解析 Webpack中import、require、按需加载的执行过程

最近由于一篇分享手淘过年项目中采用到的前端技术的影响,重新研究了一下项目中CSS的架构.本来打算写一篇文章,但是写到一半突然发现自己像在写文档介绍一样,所以后来就放弃了。但是觉得过程中研究的 Webpack 倒是可以单独拿出来讲一讲

在这里非常感谢印记中文 团队翻译的 Webpack 文档.

搭建一个简单环境

  1. npm init
  2. npm install css-loader html-webpack-plugin style-loader webpack webpack-cli
// Webpack 4.0
const htmlPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "[name].js",
    path: __dirname + "/dist"
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: "style-loader"
          },
          {
            loader: "css-loader",
          },
        ]
      }
    ]
  },
  plugins: [
    new htmlPlugin({
      title: "Test Webpack",
      filename: "index.html"
    })
  ]
};

一个基本的配置就搭建好了,详细的配置内容我就不介绍了, 然后我们在 src/index.js 上面写我们的测试代码, 在 dist/main.js 看一下 webpack 实现的原理,那么目前我们的项目结构是这样子的

|-- project
    |-- dist
    |-- src
        |-- index.js
    |-- node_modules
    |-- webpack.config.js

webpack 中 require 和 import 的执行过程

在进入按需加载的讲解之前,我们需要看一个问题 requireimportwebpack 的执行过程是怎样的呢 ?现在我们在 src建立两个文件 index.jsmodule-es6.jsmodule-commonjs.js。我们通过这三个文件解析 requireimport 的执行过程

首先我们要区分的是 CommonJSES6 模块导出之间的区别,在 CommonJS 中你导出模块方式是改变 module.exports,但是对于 ES6 来说并不存在 module 这个变量,他的导出方式是通过一个关键词 export来实现的。在我们书写 JS文件的时候,我们发现无论是以 CommomJS 还是 ES6 的形式导出都可以实现,这是因为 Webpack做了一个兼容处理

我们建立一个小 DEMO 来查看一下,我们现在上面建立的三个文件的代码如下

// index.js
// import moduleDefault, { moduleValue } from "./module-es6.js";
// import moduleDefault, { moduleValue1, moduleValue2 } from "./module-commanjs.js";
// module-es6.js
export let moduleValue = "moduleValue" //ES6模块导出
export default "ModuleDefaultValue"
// module-commonjs.js
exports.moduleValue1 = "moduleValue1"
exports.moduleValue2 = "moduleValue2"

现在我们打开 index.js 中加载 module-commonjs.js 的代码,首先会先给当前模块打上 ES6模块的标识符,在 index 则会产生两个变量 AB. A 保存 module-commonjs 的导出的结果,B 则是兼容 CommonJs中没有 ES6通过 export default导出的结果,其值跟 A一样. 用B来兼容 export default 的结果

然后我们重新注释代码,再打开 index.js 中加载 module-es6.js 的代码

这次和上面一样会先给当前模块打上 ES6模块的标识符,然后去加载 module-es6,获取他的导出值。但是浏览器是不识别 export 这个关键词的所以 Webpack 会对的代码进行解释,首先给 module.exports 设定导出的值,如果是 export default 会直接赋值给 module.exports,如果是其他形式,则给module.exports的导出的key设定一个 getter,该 getter 的返回值就是导出的结果

而对于require来说整个执行过程其实过程和import是一样的。

对于 webpack 来说只要你使用了 import 或者 export等关键字, 他就会给 module.exports添加一个__esModule : true 来识别这是一个 ES6的模块,通过这个值来做一些特殊处理

如果觉得我上面讲的不太明白 那可以看看下面这些代码

let commonjs = {
  "./src/index.js": function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    //给当前模块打上 `ES6`模块的标识符
    __webpack_require__.r(__webpack_exports__); //给当前模块打上 `ES6`模块的标识符

    // 执行 ./src/module-commonjs.js 的代码 获取导出值
    var A = __webpack_require__("./src/module-commonjs.js");

    // 根据 ./src/module-commonjs.js 是否为ES6模块 给返回值增加不同的 getter函数
    var B = __webpack_require__.n(A);
  },
  "./src/module-commonjs.js": function(module, exports) {
    exports.moduleValue1 = "moduleValue1";
    exports.moduleValue2 = "moduleValue2";
  }
};

let es6 = {
  "./src/index.js": function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    //给当前模块打上 `ES6`模块的标识符
    __webpack_require__.r(__webpack_exports__);

    // 执行 ./src/module-commonjs.js 的代码 获取导出值
    var A = __webpack_require__("./src/module-es6.js");
  },

  "./src/module-es6.js": function(module, __webpack_exports__, __webpack_require__) {
    //给当前模块打上 `ES6`模块的标识符
    __webpack_require__.r(__webpack_exports__);

    // 设置 __webpack_exports__.moduleValue 的 getter
    __webpack_require__.d(__webpack_exports__, "moduleValue", function() {
      return moduleValue;z
    });

    __webpack_exports__["default"] = "ModuleDefaultValue";

    let moduleValue = "moduleValue";
  }
};

按需加载的执行过程

看完上面的 requireimport,我们回到 按需加载 这个执行过程. webpack 的按需加载是通过 import() 或者 require.ensure()来实现的,有些读者可能对于 require.ensure 比较熟悉,所以我们先看看 require.ensure 的执行过程,
现在我们修改建立一个 module-dynamic.js文件,然后修改 index.js文件

这里吐槽一个问题,require.ensure 第一个参数是一个尴尬的存在,写和不写根本没差,如果你填了的这个参数,webpack 会帮你把文件加载近来,但是不执行。一堆不执行的代码是没有意义的,你想让他执行就必须 require() 一遍,但是执行 require 也会帮你加载文件。所以根本没差,但是里面可能涉及到模块加载顺序的问题,这块我没深入研究了,因为 require.ensure() 使用的场景越来越小了
// index.js
setTimeout(function() {
  require.ensure([], function() {
    let d = require("./module2")
  });
}, 1000);

// module2.js
module.exports = {
  name : "Jason"
}

执行 require.ensure(dependencies,callback,errorCallback,chunkName) 实际上会返回一个 promise , 里面的实现逻辑是 先判断 dependencies 是否已经被加载过,如果加载过则取缓存值的 promise, 如果没有被加载过 则生成一个 promise 并将 promise 里面的 resolve,rejectpromise本身 存入一个数组,然后缓存起来.接着生成一个 script 标签,填充完信息之后添加到HTML文件上,其中的 scriptsrc属性 就是我们按需加载的文件(module2),webpack 会对这个 script 标签监听 errorload时间,从而做相应的处理。

webpack打包过程中会给 module2 添加一些代码,主要就是主动触发 window["webpackJsonp"].push这个函数,这个函数会传递
两个参数 文件ID文件内容对象,其中 文件标示如果没有配置的话,会按载入序号自动增长,文件内容对象实际上就是上文说的 require.ensure第一个参数dependencies的文件内容,或者是 callback,errorCallback里面需要加载的文件,以 key(文件路径) --- value(文件内容)的形式出现.里面执行的事情其实就是执行上面创建的promiseresolve函数,让require.ensure里面的callback执行,之后的执行情况就跟我上面将 requirimport 一样了

当然其实讲了那么长的 require.ensure并没有什么用,因为这个函数已经被 import() 取代了,但是考虑到之前的版本应该有很多人都是用 require.ensure 方法去加载的,所以还是讲一下,而且其实 import 的执行过程跟 require.ensure 是一样的,只不过用了更友好的语法而已,所以关于 import 的执行流程我也没啥好讲的了,感兴趣的人看一下两者的 API介绍就好了。

到这里就正式讲完了,如果有对此深入的同学路过看到有不对的地方,希望能帮我指出来.非常谢谢!!!

然后再次感谢印记中文 团队翻译的 Webpack 文档

阅读 12.6k

推荐阅读
路影黑亭
用户专栏

很久没写技术文章了,之前曾经写过一段时间,但是感觉自己写的东西其实在网上随便都能找到,没什么特别的...

3 人关注
6 篇文章
专栏主页