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

模仿webpack实现一个简单的打包工具

时间:2023-04-05 11:07:37 HTML5

模仿webpack实现一个简单的打包工具当你开始使用react和vue时,你会使用默认的单页创建指令来创建一个工程项目。其实这些工程项目都是基于webpack构建的;当我们熟悉使用这些工程文件后,我们就会开始思考,为什么我们写的代码不能直接在浏览器中运行,但是经过webpack打包后却可以在浏览器中运行,打包过程中发生了什么?其实webpack是基于node实现的。打包过程包括读取文件流进行处理和导入、解析、导出模块依赖。下面是这样一个过程的简单实现。GitHub源码地址:https://github.com/wzd-front-...项目初始化首先我们新建一个文件夹,可以命名为bundler,在命令行工具(黑色窗口)中使用npminit来initialize,在初始化过程中,会要求我们输入一些项目相关的信息,如下随时按^C退出。包名:(bundler)版本:(1.0.0)描述:入口点:(index.js)testcommand:gitrepository:(https://github.com/wzd-front-end/bundler.git)keywords:author:license:(ISC)如果我们想跳过这个链接,我们可以使用npminit-y,加-y后,默认配置自动生成,不再询问;接下来,在创建测试用例之前,让我们先构建我们的项目。下面是我们的目录结构。src文件夹下的文件是我们的测试例子:--bundler--srcindex.jsmessage.jsword.js--node_modules--bundler.js--package.json--README.mdword.jscodeexportconstword='你好';message.js代码import{word}from'./word.js';constmessage=`say${word}`;exportdefaultmessage;index.js代码importmessagefrom'./message.js';console.log(message);通过观察上面三个简单文件的代码,我们会发现对这些代码主要功能模块的导入导出分析也是打包工具的主要功能。这些代码是如何转换成浏览器可识别的代码的?下面我们通过代码演示来实现这个过程;模块分析首先我们在bundler文件下创建一个bundler.js文件作为我们打包过程的执行文件,然后我们执行nodebundler.js来执行打包过程;我们首先创建一个名为moduleAnalyser的函数来解析模块,该函数接收一个文件名地址字符串,获取对应地址的文件,通过@babel/parser模块的parser方法将对应的文件字符串转化为抽象节点树。不了解抽象节点树的朋友可以通过在控制台打印出以下代码中的ast,观察其结构;生成节点树后,我们需要获取其中的导入节点。很多人可以想一下,不是可以通过字符串截取导入字符吗??当只有一个import的时候确实可以,但是当有多个import的时候,我们通过拦截来实现就比较复杂了。这时候我们可以使用@babel/traverse来帮助我们实现。具体实现可以查看babel官网,引入这个模块后,我们可以从解析器中获取ast,作为参数传入;通过前面输出的节点树,我们可以发现导入节点的类型是ImportDeclaration,我们可以在traverse()的第二个参数中传入一个对象,使用节点的类型type作为名字可以帮助我们得到相应的节点,最后我们将处理后的AST转换为代码字符串并返回。具体实现如下:constfs=require('fs')constpath=require('path')constparser=require('@babel/parser')consttraverse=require('@babel/traverse')。default;constbabel=require('@babel/core');constmoduleAnalyser=(filename)=>{//通过fs模块的异步读取文件api获取传入路径中的文件,编码格式为'utf-8'constcontent=fs.readFileSync(filename,'utf-8');//通过parser.parse方法将读取到的代码转化为抽象节点树,其中sourceType类型为指定导入文件的方式constast=parser.parse(content,{sourceType:"module"});constdependencies={}//通过遍历得到节点树中的类型IimportDeclaration节点,并将其映射关系保存到依赖对象中traverse(ast,{ImportDeclaration({node}){//获取拼接文件中传入路径的根路径constdirname=path.dirname(filename)//导入文件的实际路径constnewFile=dirname+node.source.value//将映射关系存储在dependencies对象中dependencies[node.source.value]=newFile}})//使用presets将ast转换成对应的es5代码,第一个参数为抽象节点树,第二个参数为源代码,第三个参数为配置const{code}=babel.transformFromAst(ast,null,{presets:["@babel/preset-env"]})return{filename,dependencies,code}}console.log(moduleAnalyser('./src/index.js'))通过上面的代码,我们可以得到一个模块入口文件的解析,包括模块名称,依赖和代码,但是我们只得到一个入口文件的解析。入口模块有自己的依赖,依赖也有自己的依赖。因此,我们需要对每个模块进行深入分析;....//用于循环调用A模块constmakeDependenciesGraph=(entry)=>{//首先获取入口模块的分析对象constentryModule=moduleAnalyser(entry)//保存所有模块的分析对象constgraphArray=[entryModule]//对graphArray中的每一项进行分析,分析每一项中的依赖关系,如果存在,我们分析新的依赖模块,直到找到所有依赖for(leti=0;i{//因为我们需要返回对应的可执行字符串,所以我们需要先将对象转成字符串,否则会有'[object,object]'constgraph=JSON.stringify(makeDependenciesGraph(条目));//returnstring使用模板字符串并使用闭包来防止全局污染exports={};(function(require,exports,code){eval(code)})(localRequire,exports,graph[module].code);返回出口;};require('${entry}')})(${图表});`;}constcode=generateCode('./src/index.js')console.log(code)最后我们在控制台输出的代码复制到浏览器的控件中执行。根据预定的结果运行并打印出结果。运行代码如下:,exports,code){eval(code)})(localRequire,exports,graph[module].code);退货出口;};require('./src/index.js')})({"./src/index.js":{"dependencies":{"./message.js":"./src\\message.js"},"code":"\"usestrict\";\n\nvar_message=_interopRequireDefault(require(\"./message.js\"));\n\nfunction_interopRequireDefault(obj){returnobj&&obj.__esModule?obj:{\"default\":obj};}\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"usestrict\";\n\nObject.defineProperty(exports,\"__esModule\",{\n值:true\n});\nexports[\"default\"]=void0;\n\nvar_word=require(\"./word.js\");\n\nvarmessage=\"说\".concat(_word.word);\nvar_default=message;\nexports[\"default\"]=_default;"},"./src\\word.js":{"dependencies":{}"code":"\"usestrict\";\n\nObject.defineProperty(exports,\"__esModule\",{\nvalue:true\n});\nexports.word=void0;\nvarword='你好';\nexports.word=word;"}});上面的代码是我们打包后的代码,我们会发现打包后需要使用其他模块时,会调用requiree方法和require方法会通过传入的地址路径参数查询我们生成的以filename为key值的对象,找到对应的代码,并使用eval()方法执行。这是打包工具的一个基本原理