webpack 拍了拍你,给了你一份图解指南(模块化部分)

在前面一篇文章中 《模块化系列》完全理清 AMD,CommonJS,CMD,UMD,ES6,咱们能够学到了各类模块化的机制。那么接下里咱们就来分析一下 webpack 的模块化机制。(主要讲 JS 部分)javascript

提到 webpack,能够说是与咱们的开发工程很是密切的工具,不论是平常开发、进行面试仍是对于自个人提升,都离不开它,由于它给咱们的开发带了极大的便利以及学习的价值。可是因为webpack是一个很是庞大的工程体系,使得咱们望之却步。本文想以这种图解的形式可以将它慢慢地剥开一层一层复杂的面纱,最终露出它的真面目。如下是我列出的关于 webpack 相关的体系。html

webpack-2

本文讲的是 打包 - CommonJS 模块,主要分为两个部分前端

  • webpack 的做用
  • webpack 的模块化机制与实现

webpack 的做用

在咱们前端多样化的今天,不少工具为了知足咱们日益增加的开发需求,都变得很是的庞大,例如 webpack 。在咱们的印象中,它彷佛集成了全部关于开发的功能,模块打包,代码降级,文件优化,代码校验等等。正是由于面对如此庞大的一个工具,因此才让咱们望而却步,固然了还有一点就是,webpack 的频繁升级,周边的生态插件配套版本混乱,也加重咱们对它的恐惧。java

那么咱们是否是应该思考一下,webpack 的出现究竟给咱们带来了什么?咱们为啥须要用它?而上面全部的一些代码降级(babel转化)、编译SCSS 、代码规范检测都是得益于它的插件系统和loader机制,并非完彻底全属于它。webpack

因此在我看来,它的功能核心是打包,而打包则是可以让模块化的规范得以在浏览器直接执行。所以咱们来看看打包后所带来的功能:git

  • 模块隔离
  • 模块依赖加载

模块隔离

若是咱们不用打包的方式,咱们全部的模块都是直接暴露在全局,也就是挂载在 window/global 这个对象。也许代码量少的时候还能够接受,不会有那么多的问题。特别是在代码增多,多人协做的状况下,给全局空间带来的影响是不可预估的,若是你的每一次开发都得去一遍一遍查找是否有他们使用当前的变量名。github

举个例子(仅仅为例子说明,实际工程会比如下复杂许多),一开始咱们的 user1 写了一下几个模块,跑起来很是的顺畅。web

image-20200626231748187

├── bar.js    function bar(){}
├── baz.js    function baz(){}
└── foo.js	function foo(){}
复制代码

可是呢,随着业务迭代,工程的复杂性增长,来了一个 user2,这个时候 user2,须要开发一个 foo 业务,里面也有一个 baz 模块,代码也很快写好了,变成了下面这个样子。面试

├── bar.js    function bar(){}
├── baz.js    function baz(){}
├── foo
│   └── baz.js	function baz(){}
└── foo.js	function foo(){}
复制代码

可是呢这个时候,老板来找 user2 了,为何增长了新业务后,原来的业务出错了呢?这个时候发现原来是 user2 写的新模块覆盖了 user1 的模块,从而致使了这场事故。typescript

image-20200626220806881

所以,当咱们开发的时候将全部的模块都暴露在全局的时候,想要避免错误,一切都得很是的当心翼翼,咱们很容易在不知情的偷偷覆盖咱们之前定义的函数,从而酿成错误。

所以 webpack 带来的第一个核心做用就是隔离,将每一个模块经过闭包的形式包裹成一个个新的模块,将其放于局部做用域,全部的函数声明都不会直接暴露在全局。

image-20200626220851909

原来咱们调用的 是 foo 函数,可是 webpack 会帮咱们生成独一无二的模块ID,彻底不须要担忧模块的冲突,如今能够愉快地书写代码啦。

baz.js
module.exports = function baz (){}

foo/baz.js
module.exports = function baz (){}

main.js
var baz = require('./baz.js');
var fooBaz = require('./foo/baz.js');

baz();
fooBaz();
复制代码

可能你说会以前的方式也能够经过改变函数命名的方式,可是原来的做用范围是整个工程,你得保证,当前命名在整个工程中不冲突,如今,你只须要保证的是单个文件中命名不冲突。(对于顶层依赖也是很是容易发现冲突)

image-20200627140818771

模块依赖加载

还有一种重要的功能就是模块依赖加载。这种方式带来的好处是什么?咱们一样先来看例子,看原来的方式会产生什么问题?

User1 如今写了3个模块,其中 baz 是依赖于 bar 的。

image-20200627000240836

写完后 user1 进行了上线,利用了顺序来指出了依赖关系。

<script src="./bar.js"></script>
<script src="./baz.js"></script>
<script src="./foo.js"></script>
复制代码

但是过了不久 user2 又接手了这个业务。user 2 发现,他开发的 abc 模块,经过依赖 bar 模块,能够进行快速地开发。但是 粗心的 user2 不太明白依赖关系。居然将 abc 的位置随意写了一下,这就致使 运行 abc 的时候,没法找到 bar 模块。

image-20200627000713100

<script src="./abc.js"></script>
<script src="./bar.js"></script>
<script src="./baz.js"></script>
<script src="./foo.js"></script>
复制代码

所以这里 webpack 利用 CommonJS/ ES Modules 规范进行了处理。使得各个模块之间相互引用无需考虑最终实际呈现的顺序。最终会被打包为一个 bunlde 模块,无需按照顺序手动引入。

baz.js
const bar = require('./bar.js');
module.exports = function baz (){
	...
	bar();
	...
}

abc.js
const bar = require('./bar.js');
module.exports = function baz (){
	...
	bar();
	...
}
复制代码
<script src="./bundle.js"></script>
复制代码

image-20200627003815071

webpack 的模块化机制与实现

基于以上两项特性,模块的隔离以及模块的依赖聚合。咱们如今能够很是清晰的知道了webpack所起的核心做用。

  • 为了尽量下降编写的难度和理解成本,我没有使用 AST 的解析,(固然 AST 也不是什么很难的东西,之后的文章中我会讲解 AST是什么以及 AST 解析器的实现过程。
  • 仅实现了 CommonJS 的支持

bundle工做原理

为了可以实现 webpack, 咱们能够经过反推的方法,先看webpack 打包后 bundle 是如何工做的。

源文件

// index.js
const b = require('./b');
b();
// b.js
module.exports = function () {
    console.log(11);
}
复制代码

build 后(去除了一些干扰代码)

(function(modules) {
  var installedModules = {};
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    });
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    module.l = true;
    return module.exports;
  }
  return __webpack_require__((__webpack_require__.s = 0));
})([
  /* 0 */
  function(module, exports, __webpack_require__) {
    var b = __webpack_require__(1);
    b();
  },
  /* 1 */
  function(module, exports) {
    module.exports = function() {
      console.log(11);
    };
  },
]);

复制代码

image-20200627135324956

以上就是 bundle 的运做原理。经过上述的流程图咱们能够看到,有四个关键点

  • 已注册模块(存放已经注册的模块)
  • 模块列表(用来存放全部的包装模块)
  • 模块查找(从原来的树形的模块依赖,变成了扁平查找)
  • 模块的包装(原有的模块都进行了一次包装)

webpack实现

经过 bundle 的分析,咱们只须要作的就是 4 件事

  • 遍历出全部的模块
  • 模块包装
  • 提供注册模块、模块列表变量和导入函数
  • 持久化导出

模块的遍历

首先来介绍一下模块的结构,能使咱们快速有所了解, 结构比较简单,由内容和模块id组成。

interface GraphStruct {
    context: string;
    moduleId: string;
}
复制代码
{
	"context": `function(module, exports, require) {
    const bar = require('./bar.js');
		const foo = require('./foo.js');
		console.log(bar());
		foo();
  }`,
  "moduleId": "./example/index.js"
}
复制代码

接下来咱们以拿到一个入口文件来进行讲解,当拿到一个入口文件时,咱们须要对其依赖进行分析。说简单点就是拿到 require 中的值,以便咱们去寻找下一个模块。因为在这一部分不想引入额外的知识,开头也说了,通常采用的是 AST 解析的方式,来获取 require 的模块,在这里咱们使用正则。

用来匹配全局的 require 
const REQUIRE_REG_GLOBAL = /require\(("|')(.+)("|')\)/g;
用来匹配 require 中的内容
const REQUIRE_REG_SINGLE = /require\(("|')(.+)("|')\)/;
复制代码
const context = ` const bar = require('./bar.js'); const foo = require('./foo.js'); console.log(bar()); foo(); `;
console.log(context.match(REQUIRE_REG_GLOBAL));
// ["require('./bar.js')", "require('./foo.js')"]
复制代码

image-20200627202427794

因为模块的遍历并非只有单纯的一层结构,通常为树形结构,所以在这里我采用了深度遍历。主要经过正则去匹配出require 中的依赖项,而后不断递归去获取模块,最后将经过深度遍历到的模块以数组形式存储。(不理解深度遍历,能够理解为递归获取模块)

image-20200627142130902

如下是代码实现

...
private entryPath: string
private graph: GraphStruct[]
...
createGraph(rootPath: string, relativePath: string) {
    // 经过获取文件内容
    const context = fs.readFileSync(rootPath, 'utf-8');
    // 匹配出依赖关系
    const childrens = context.match(REQUIRE_REG_GLOBAL);
  	// 将当前的模块存储下来
    this.graph.push({
        context,
        moduleId: relativePath,
    })
    const dirname = path.dirname(rootPath);
    if (childrens) {
       // 若有有依赖,就进行递归
        childrens.forEach(child => {
            const childPath = child.match(REQUIRE_REG_SINGLE)[2];
            this.createGraph(path.join(dirname, childPath), childPath);
        });
    }
}
复制代码

模块包装

为了可以使得模块隔离,咱们在外部封装一层函数, 而后传入对应的模拟 requiremodule使得模块能进行正常的注册以及导入 。

function (module, exports, require){
    ...
},
复制代码

提供注册模块、模块列表变量和导入函数

这一步比较简单,只要按照咱们分析的流程图提供已注册模块变量、模块列表变量、导入函数。

/* modules = { "./example/index.js": function (module, exports, require) { const a = require("./a.js"); const b = require("./b.js"); console.log(a()); b(); }, ... };*/
bundle(graph: GraphStruct[]) {
    let modules = '';
    graph.forEach(module => {
        modules += `"${module.moduleId}":function (module, exports, require){ ${module.context} },`;
    });
    const bundleOutput = ` (function(modules) { var installedModules = {}; // 导入函数 function require(moduleId) { // 检查是否已经注册该模块 if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 没有注册则从模块列表获取模块进行注册 var module = (installedModules[moduleId] = { i: moduleId, l: false, exports: {}, }); // 执行包装函数,执行后更新模块的内容 modules[moduleId].call( module.exports, module, module.exports, require ); // 设置标记已经注册 module.l = true; // 返回实际模块 return module.exports; } require("${graph[0].moduleId}"); })({${modules}}) `;
    return bundleOutput;
}
复制代码

持久化导出

最后将生成的 bundle 持久写入到磁盘就大功告成。

fs.writeFileSync('bundle.js', this.bundle(this.graph))
复制代码

完整代码100行 代码不到,详情能够查看如下完整示例。

github地址: github.com/hua1995116/…

结尾

以上仅表明我的的理解,但愿让你对webpack的理解有所帮助, 若有讲的很差的请多指出。

欢迎关注公众号 「秋风的笔记」,主要记录平常中以为有意思的工具以及分享开发实践,保持深度和专一度。回复 webpack 获取概览图 xmind 原图

weixin-gongzhonghao

FAQ

Q: 为何打算写这篇文章?

R: 其实主要是为了画图,纯粹比较新奇。

Q: 还会有下一篇吗?

R: 有的,下一篇暂定为 ES module 和 code splitting 相关。

相关文章
相关标签/搜索