当前位置: 首页 > Web前端 > HTML5

如何自己实现一个简单的webpack构建工具【精读】

时间:2023-04-05 19:31:07 HTML5

如果你对React技术栈感兴趣,可以看我之前的两篇文章:从零开始实现一个mini-React框架,从零开始搭建一个React优化版本脚手架GitHub有对应的源码~欢迎Star这个项目在Github上的源码地址:mini-webpackwebpack可以说是目前最流行的打包工具了。如果用不好,真不敢说自己是合格的前端工程师本文将首先介绍webpack的打包过程和运行原理,然后实现一个简单的webpack。本质上,webpack是现代JavaScript应用程序的静态模块打包器。当webpack处理应用程序时,它会递归地构建一个包含应用程序所需的每个模块的依赖关系图,然后将所有这些模块打包到一个或多个包中。webpack打包过程1.识别入口文件2.逐层识别模块依赖。(commonjs、amd或es6imports会被webpack分析获取代码依赖)3.webpack做的是分析代码。转换代码,编译代码,输出代码4.最终形成打包后的代码webpack打包原理1.首先递归一步步识别依赖,构建依赖图2.将代码转换成AST抽象语法树。下图是一个抽象语法树:]3、处理AST阶段的代码4、将AST抽象语法树变成浏览器可以识别的代码,然后准备输出。在编写自己的构建工具之前,需要下载四个包。1.@babel/parser:通过fs.readFileSync分析我们读取的文件内容,返回AST(抽象语法树)2.@babel/traverse:可以遍历AST,得到必要的数据3.@babel/core:babel的核心模块,有一个transformFromAst方法,可以将AST转换成浏览器可以运行的代码4.@babel/preset-env:将代码转换成ES5代码,使用yarn下载:$yarninit-y$yarnadd@babel/parser@babel/traverse@babel/core@babel/preset-env先看如何将最简单的文件转换成AST目录结构:代码实现:constfs=require('fs');constpath=require('路径');constparser=require('@babel/parser');consttraverse=require('@babel/traverse').default;//遍历导出的ESModule,如果我们通过requier导入,添加.defaultconstbabel=require('@babel/core');constread=fileName=>{constbuffer=fs.readFileSync(fileName,'utf-8');constAST=parser.parse(buffer,{sourceType:'module'});console.log(AST);};read('./test1.js');上面的代码:1.首先使用同步NodeAPI读取文件流2.然后将对应的buffer转换成下面的ASTNode{type:'File',start:0,end:32,loc:SourceLocation{start:Position{行:1,列:0},结束:位置{行:1,列:32}},亲gram:Node{type:'Program',start:0,end:32,loc:SourceLocation{start:[Position],end:[Position]},sourceType:'module',解释器:null,body:[[节点]],directives:[]},comments:[]}我们已经将代码转换成了AST语法树,所以我们需要遍历AST,然后将其转换成浏览器可以理解的代码。在读取函数中添加如下代码://依赖集合constdependencies={};//使用traverse遍历ASTtraverse(AST,{ImportDeclaration({node}){//函数名是AST中包含的内容,参数是一些节点,node代表这些节点下的childrenContentconstdirname=path.dirname(filename);//我们从抽象语法树中得到的路径是相对路径,然后我们要对其进行处理,这样我们才能在bundler中正确使用constnewDirname='./'+path。js.join(dirname,node.source.value).replace('\\','/');//结合dirname和获取的依赖生成一个绝对路径dependencies[node.source.value]=newDirname;//以key-value形式存储源路径和新路径}})//将抽象语法树转换为浏览器可以运行的代码const{code}=babel.transformFromAst(AST,null,{presets:['@babel/preset-env']})return{文件名,dependencies,code}当我们调用read函数读取test1.js的内容时:constresult=read('./test1.js');控制台日志(结果);得到打印输出结果:{fileName:'./test1.js',dependencies:{},code:'"usestrict";\n\nconsole.log(\'thisistest1.js\');'}test1.js原来的内容是:正式启动,下面添加ES6模块化,重新定义文件目录启动文件index.js...//一些逻辑在这个文件里,我们只需要传入一个entry入口app即可。jsimporttest1from'./test1.js'console.log(test1)test1.jsimporttest2from'./test2.js';console.log('thisistest1.js',test2);test2.jsfunctiontest2(){console.log('thisistest2');}exportdefaulttest2;依赖关系很明确:入口是index.js->dependsontest1.jsdependson->test2.js只做了上面的一些处理,如果遇到依赖文件和依赖就不行了所以我们需要创建一个可以处理依赖关系的函数:getdependencygraph//创建依赖关系图函数,递归遍历所有依赖模块//首先将我们分析的入口文件的结果放入图形数组中for(leti=0;i{gragh[item.filename]={dependencies:item.dependencies,code:item.code}})console.log(gragh)returngragh;}打印gragh获得的对象:{'./app.js':{dependencies:{'./test1.js':'./test1.js'},代码:'"usestrict";\n\nvar_test=_interopRequireDefault(require("./test1.js"));\n\nfunction_interopRequireDefault(obj){返回obj&&obj.__esModule?对象:{“默认”:对象};}\n\nconsole.log(test1);'},'./test1.js':{依赖项:{'./test2.js':'./test2.js'},代码:'"usestrict";\n\nvar_test=_interopRequireDefault(require("./test2.js"));\n\nfunction_interopRequireDefault(obj){返回obj&&obj.__esModule?对象:{“默认”:对象};}\n\nconsole.log(\'这是test1.js\',_test["default"]);'},'./test2.js':{dependencies:{},code:'"usestrict";\n\nObject.defineProperty(exports,"__esModule",{\nvalue:true\n});\nexports["default"]=void0;\n\nfunctiontest2(){\nconsole.log(\'这是test2\');\n}\n\nvar_default=test2;\nexports["default"]=_default;'}}至此我们已经获取了所有的依赖以及依赖的代码内容,我们只需要处理输出就可以最终处理代码输出constgenerateCode=(entry)=>{//注意:我们的gragh是一个对象,关键是我们所有模块的绝对路径都需要通过JSON.stringifyconstgragh=JSON.stringify(makeDependenciesGraph(entry));进行转换//我们知道webpack把我们所有的模块都放在了闭包中它在里面执行,所以我们写一个自执行函数//注意:在我们生成的代码中,我们使用require和exports来导入导出模块,但是我们的浏览器并不知道,所以我们需要构建这样的函数return`(function(gragh){functionrequire(module){//将相对路径转换为绝对路径的方法functionlocalRequire(relativePath){returnrequire(gragh[module].dependencies[relativePath])}constexports={};(function(require,exports,code){eval(code)})(localRequire,exports,gragh[module].code)returnexports;}require('${entry}')})(${gragh})`;}constcode=generateCode('./app.js');console.log(code)获取编译输出代码代码如下:(function(gragh){functionrequire(module){//将相对路径转换为绝对路径方法functionlocalRequire(relativePath){returnrequire(gragh[模块].dependencies[relativePath])}constexports={};(function(require,exports,code){eval(code)})(localRequire,exports,gragh[module].code)returnexports;}require('./app.js')})({"./app.js":{"dependencies":{"./test1.js":"./test1.js"},"code":"\"usestrict\";\n\nvar_test=_interopRequireDefault(require(\"./test1.js\"));\n\nfunction_interopRequireDefault(obj){returnobj&&obj.__esModule?obj:{\"default\":obj};}\n\nconsole.log(_test[\"default\"]);"},"./test1.js":{"dependencies":{"./test2.js":"./test2.js"},"code":"\"usestrict\";\n\nvar_test=_interopRequireDefault(require(\"./test2.js\"));\n\nfunction_interopRequireDefault(obj){返回obj&&obj.__es模块?obj:{\"默认\":obj};}\n\nconsole.log('这是test1.js',_test[\"default\"]);"},"./test2.js":{"dependencies":{},"code":"\"usestrict\";\n\nObject.defineProperty(exports,\"__esModule\",{\nvalue:true\n});\nexports[\"default\"]=void0;\n\nfunctiontest2(){\nconsole.log('thisistest2');\n}\n\nvar_default=test2;\nexports[\"default\"]=_default;"}})复制这段代码在浏览器中运行:代码可以运行,ES6模块化已经可以被浏览器识别模仿webpack实现loader和plugin:开头有介绍文章,webpack的loader和plugin的本质:loader的本质是字符串的正则匹配操作plugin,依赖于webpackruntime广播的生命周期事件,然后调用Node.js的API来使用webpack的全局实例对象来操作,无论是硬盘文件的操作还是内存中的数据操作,webpack的核心依赖库:Tapabletapable是webpack的核心依赖库如果想了解webpack的源码,你必须首先熟悉tapableconst{SyncHook,SyncBailHook,SyncWaterfallHook,SyncLoopHook,AsyncParallelHook,AsyncParallelBailHook,AsyncSeriesHook,AsyncSeriesBailHook,AsyncSeriesWaterfallHook}=require("tapable");这些钩子可以分为同步钩子和异步钩子。Sync开头的都是同步钩子,Async开头的都是异步钩子。异步钩子可以分为并行和串行。其实同步钩子也可以理解为串行钩子。我的理解:这是webpack运行时广播事件的一种发布-订阅模式,让订阅这些事件的订阅者(其实是插件)触发相应的事件,得到全局的webpack实例对象,然后再做一系列的处理可以完成非常复杂的功能。同步钩子是串行的,异步钩子分为并行钩子和串行钩子。并行指的是等待所有并发的异步事件执行完毕,再执行最后的异步回调。串行是值。第一步执行完之后,再执行第二步,以此类推,直到所有回调都执行完,才执行最后一个异步回调。拿最简单的同步钩子,SyncHook,const{SyncHook}=require('tapable');classHook{constructor(){/**1生成SyncHook实例*/this.hooks=newSyncHook(['name']);}tap(){/**2注册监听函数*/this.hooks.tap('node',function(name){console.log('node',name);});this.hooks.tap('react',function(name){console.log('react',name);});}start(){/**3开始监听函数*/this.hooks.call('callend.');}}leth=newHook();h.tap();/**类似于订阅*/h.start();/**类似于发布*//*打印顺序:节点调用结束。反应呼叫结束。*/再看一个异步钩子AsyncParallelHookconst{AsyncParallelHook}=require('tapable');classHook{constructor(){this.hooks=newAsyncParallelHook(['name']);}tap(){/**异步注册方式为tapAsync()*并有回调函数cb。*/this.hooks.tapAsync('node',function(name,cb){setTimeout(()=>{console.log('node',name);cb();},1000);});this.hooks.tapAsync('react',function(name,cb){setTimeout(()=>{console.log('react',name);cb();},1000);});}start(){/**异步触发方法为callAsync()*多一个最终回调函数fn.*/this.hooks.callAsync('调用结束',function(){console.log('最终回调');});}}leth=newHook();h.tap();/**类似于订阅*/h.start();/**类似于发布*//*打印顺序:节点调用结束。反应呼叫结束。finalcallback*/当然,作者的能力还没有达到完全解析webpack的水平。有兴趣的可以研究一下Tapable库的源码。有兴趣深入研究的可以看阅读这两篇文章深入了解Webpack核心模块Tapablehook---深入了解Webpack核心模块Tapablehook同步版---这里先写异步版本。觉得写的不错,别忘了点个赞,欢迎加入segmentFault前端交流群我的个人微信:CALASF小谭拉你入群,小姐姐们等着你呢~