webpack拍了拍你,给了你一份图解

北京治疗荨麻疹好医院 http://baidianfeng.39.net/bdfby/yqyy/

在前面一篇文章中《模块化系列》彻底理清AMD,CommonJS,CDM,UMD,ES6我们可以学到了各种模块化的机制。那么接下里我们就来分析一下webpack的模块化机制。(主要讲JS部分)

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

webpack-2

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

webpack的作用webpack的模块化机制与实现webpack的作用

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

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

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

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

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

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

image-

├──bar.jsfunctionbar(){}├──baz.jsfunctionbaz(){}└──foo.jsfunctionfoo(){}

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

├──bar.jsfunctionbar(){}├──baz.jsfunctionbaz(){}├──foo│└──baz.jsfunctionbaz(){}└──foo.jsfunctionfoo(){}

但是呢这个时候,老板来找user2了,为什么增加了新业务后,原来的业务出错了呢?这个时候发现原来是user2写的新模块覆盖了user1的模块,从而导致了这场事故。

image-

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

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

image-

原来我们调用的是foo函数,但是webpack会帮我们生成独一无二的模块ID,完全不需要担心模块的冲突,现在可以愉快地书写代码啦。

baz.jsmodule.exports=functionbaz(){}foo/baz.jsmodule.exports=functionbaz(){}main.jsvarbaz=require(./baz.js);varfooBaz=require(./foo/baz.js);baz();fooBaz();

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

image-模块依赖加载

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

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

image-

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

scriptsrc="./bar.js"/scriptscriptsrc="./baz.js"/scriptscriptsrc="./foo.js"/script

可是过了不久user2又接手了这个业务。user2发现,他开发的abc模块,通过依赖bar模块,可以进行快速地开发。可是粗心的user2不太明白依赖关系。竟然将abc的位置随意写了一下,这就导致运行abc的时候,无法找到bar模块。

image-

scriptsrc="./abc.js"/scriptscriptsrc="./bar.js"/scriptscriptsrc="./baz.js"/scriptscriptsrc="./foo.js"/script

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

baz.jsconstbar=require(./bar.js);module.exports=functionbaz(){...bar();...}abc.jsconstbar=require(./bar.js);module.exports=functionbaz(){...bar();...}

scriptsrc="./bundle.js"/scriptimage-webpack的模块化机制与实现

基于以上两项特性,模块的隔离以及模块的依赖聚合。我们现在可以非常清晰的知道了webpack所起的核心作用。

为了尽可能降低编写的难度和理解成本,我没有使用AST的解析,(当然AST也不是什么很难的东西,以后的文章中我会讲解AST是什么以及AST解析器的实现过程。仅实现了CommonJS的支持bundle工作原理

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

「源文件」

//index.jsconstb=require(./b);b();//b.jsmodule.exports=function(){console.log(11);}

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

(function(modules){varinstalledModules={};function__webpack_require__(moduleId){if(installedModules[moduleId]){returninstalledModules[moduleId].exports;}varmodule=(installedModules[moduleId]={i:moduleId,l:false,exports:{},});modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;returnmodule.exports;}return__webpack_require__((__webpack_require__.s=0));})([/*0*/function(module,exports,__webpack_require__){varb=__webpack_require__(1);b();},/*1*/function(module,exports){module.exports=function(){console.log(11);};},]);image-

以上就是bundle的运作原理。通过上述的流程图我们可以看到,有四个关键点

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

通过bundle的分析,我们只需要做的就是4件事

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

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

interfaceGraphStruct{context:string;moduleId:string;}

{"context":`function(module,exports,require){constbar=require(./bar.js);constfoo=require(./foo.js);console.log(bar());foo();}`,"moduleId":"./example/index.js"}

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

用来匹配全局的requireconstREQUIRE_REG_GLOBAL=/require\(("

)(.+)("

)\)/g;用来匹配require中的内容constREQUIRE_REG_SINGLE=/require\(("

)(.+)("

)\)/;

constcontext=`constbar=require(./bar.js);constfoo=require(./foo.js);console.log(bar());foo();`;console.log(context.match(REQUIRE_REG_GLOBAL));//["require(./bar.js)","require(./foo.js)"]image-

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

image-

以下是代码实现

...privateentryPath:stringprivategraph:GraphStruct[]...createGraph(rootPath:string,relativePath:string){//通过获取文件内容constcontext=fs.readFileSync(rootPath,utf-8);//匹配出依赖关系constchildrens=context.match(REQUIRE_REG_GLOBAL);//将当前的模块存储下来this.graph.push({context,moduleId:relativePath,})constdirname=path.dirname(rootPath);if(childrens){//如有有依赖,就进行递归childrens.forEach(child={constchildPath=child.match(REQUIRE_REG_SINGLE)[2];this.createGraph(path.join(dirname,childPath),childPath);});}}模块包装

为了能够使得模块隔离,我们在外部封装一层函数,然后传入对应的模拟require和module使得模块能进行正常的注册以及导入。

function(module,exports,require){...},提供注册模块、模块列表变量和导入函数

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

/*modules={"./example/index.js":function(module,exports,require){consta=require("./a.js");constb=require("./b.js");console.log(a());b();},...};*/bundle(graph:GraphStruct[]){letmodules=;graph.forEach(module={modules+=`"{module.moduleId}":function(module,exports,require){{module.context}},`;});constbundleOutput=`(function(modules){varinstalledModules={};//导入函数functionrequire(moduleId){//检查是否已经注册该模块if(installedModules[moduleId]){returninstalledModules[moduleId].exports;}//没有注册则从模块列表获取模块进行注册varmodule=(installedModules[moduleId]={i:moduleId,l:false,exports:{},});//执行包装函数,执行后更新模块的内容modules[moduleId].call(module.exports,module,module.exports,require);//设置标记已经注册module.l=true;//返回实际模块returnmodule.exports;}require("{graph[0].moduleId}");})({{modules}})`;returnbundleOutput;}持久化导出

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

fs.writeFileSync(bundle.js,this.bundle(this.graph))

完整代码行代码不到,详情可以查看以下完整示例。

github


转载请注明:http://www.guyukameng.com/html/html1/12802.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了