Hello各位靓仔大家好呀!在我一次一次迁延症之后这篇文章终于问世了;首先祝大家新年好吧(搪塞)~

明天来讲讲webpack的打包原理,以及简略实现吧~

置信大家对webpack多多少少都有了解,说到webpack是什么基本上都能说出这是一个模块化打包工具,明天我就从模块化打包动手来简略解说一下

模块化与webpack

说到Webpack总绕不开模块化,咱们来看看模块化的倒退过程与Webpack在其中起到的作用;晚期因为没有标准的束缚,模块化计划也是层出不穷,最后的模块化计划是最原始简略的文件划分计划;每个文件代表一个独自模块,而后在页面中通过script标签援用,因为代码是在同一个作用域中执行的短期内的确没有什么影响,然而随着我的项目体量的增大随之而来的是各种命名抵触,作用域净化,变量晋升等一系列辣手的问题;过后最显著的莫过于命名抵触问题;为了解决命名抵触问题,优良的前端切图仔先辈们在文件划分的根底之上整出了一个命名空间模块化划分计划,顾名思义就是为每一个模块提供独立的命名空间并挂载到Window上,通过Window.moduleXXX的形式去调用模块内的属性,这的确解决了命名抵触的问题;然而作用域净化的问题仍旧存在,那怎么办?当然是给模块提供独自的作用域咯!过后最广泛的是IIFE模块化划分计划,在命名空间划分计划的根底之上为模块提供一个公有作用域;这个公有作用域是通过调用一个立刻执行函数通过闭包的模式将模块挂载到window之上至此也就解决了作用域净化问题;人么吃饱了饭没事干总喜爱打老婆(bushi),没事总喜爱瞎折腾,尽管IIFE曾经完满解决了作用域净化与变量命名抵触的问题然而切图仔们并不满足,他们感觉IIFE这种形式并不够快,当我的项目体量变得非常微小的时候很容易造成卡顿(页面引入的模块并不会打消,每次加载都会将所有模块加载);这时官网出手了,官网并没有惩治这群不知足的切图仔,相同还助他们一臂之力,随之而来的推出了一套AMD标准(异步模块加载标准) 因为cjs并不能很好的实用于浏览器,然而前端代码却是要在浏览器中执行的,因而诞生了赫赫有名的require.js,原理即是在须要引入模块时创立一个script标签挂载到页面中来做到异步加载;随着前端的倒退,切图仔的欲望又得不到满足;在应用require.js倒退的期间为了能兼容cjs也诞生过例如CMD标准然而究竟是过眼云烟;再往后随着模块化规范的增多,与依赖的不对立切图仔们发现急需一套新规范来标准模块化,为了兼容AMDcjs全局变量于是便诞生了UMD,随着ES规范的不断完善ES Modules诞生了,这便是咱们当初罕用的import导入的模块化形式;

讲了这么多模块化常识那么跟webpack有个鸡毛关系?随着前端模块的与我的项目资源的一直增多,对模块的治理熬掉了不少切图仔乌黑亮丽的秀发;秃头的切图仔们急需一个模块化管理工具来治理高效我的项目中的每一个文件与资源;于是诞生了webpack rollup gulp等等一系列工具

webpack解决了什么问题?

关上webpack官网映入眼帘的就是这张图片

由这张图中咱们能够看到webpack将一堆零散的文件打包编译成为js css 以及图片;这么做有什么用?又解决了什么问题呢?

我总结出了以下三点:

模块整合能力

随着我的项目体量的增大,模块文件增多使切图仔们解决模块文件变得异样艰苦,而模块资源又在浏览器当中运行,频繁的申请零散文件势必会对服务器造成负荷与利用的工作效率;而webpack能够将多个模块文件打包成一个文件,在缩小资源内存的同时也缩小了对服务器的申请

代码编译能力

随着ES标准的疾速倒退,很多浏览器并不能及时兼容新的语法与API;这导致API的兼容变成一个较大的问题,而webpack则能够将这些浏览器看不懂的API与语法转换成浏览器看得懂的ES5语法;

多品种模块资源管理

随着前端的倒退模块品种增多(css,html,js,图片),而前端须要通过代码或者工具对模块资源进行治理整合;

创立你的第一个Webpack我的项目

置信在日常工作中大家对React与Vue脚手架的应用曾经十分的纯熟,然而不晓得大家在日常工作中是否用webpack从0到1创立过一个残缺的我的项目,接下来我将用一个简略的webpack我的项目动手为大家剖析解说webpack的目录构造与打包原理;

初始化我的项目

#初始化我的项目npm init #为我的项目增加依赖yarn add webpack  

我的项目目录构造

├── node_modules├── package-lock.json└── package.json

在目录下为我的项目增加webpack文件,webpack配置文件以及源码目录(src)

webpack.js

const { webpack } = require("webpack");const webpackOptions = require("./webpack.config.js");const compiler = webpack(webpackOptions);//开始编译compiler.run();

webpack.config.js

const path = require("path");module.exports = {    mode: "development",     entry: "./src/index.js", //入口文件    output: {        path: path.resolve(__dirname, "dist"),//输入门路        filename: "[name].js",    },    devtool: "source-map", };

为源码我的项目(src)增加文件

name.js

export  default  '龙骑士尹道长'

info.js

import name from './name.js'export default `${name} 666`

index.js(入口文件)

import info from "./info.js"console.log(info)

至此一个简略的webpack我的项目就构建实现啦,当初咱们只须要运行webpack.js(运行nodejs形式)便可对我的项目进行打包

打包后,你的我的项目目录中就会呈现一个dist文件夹文件夹中有一个main.js(以下内容较长,经删减整顿以及增加备注,源码未动)

(() => {    "use strict";    var __webpack_modules__ = ({        //info.js        "./src/info.js":            ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {                __webpack_require__.r (__webpack_exports__);                __webpack_require__.d (__webpack_exports__, {                    "default": () => (__WEBPACK_DEFAULT_EXPORT__)                });                var _name_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__ (/*! ./name.js */ "./src/name.js");                const __WEBPACK_DEFAULT_EXPORT__ = (`${_name_js__WEBPACK_IMPORTED_MODULE_0__["default"]} 666`);            }),        //name.js        "./src/name.js":            ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {                __webpack_require__.r (__webpack_exports__);                __webpack_require__.d (__webpack_exports__, {                    "default": () => (__WEBPACK_DEFAULT_EXPORT__)                });                const __WEBPACK_DEFAULT_EXPORT__ = ('龙骑士尹道长');            })    });    var __webpack_module_cache__ = {};    function __webpack_require__ (moduleId) {        var cachedModule = __webpack_module_cache__[moduleId];        if (cachedModule !== undefined) {            return cachedModule.exports;        }        var module = __webpack_module_cache__[moduleId] = {            exports: {}        };        __webpack_modules__[moduleId] (module, module.exports, __webpack_require__);        return module.exports;    }    (() => {        __webpack_require__.d = (exports, definition) => {            for (var key in definition) {                if (__webpack_require__.o (definition, key) && !__webpack_require__.o (exports, key)) {                    Object.defineProperty (exports, key, {enumerable: true, get: definition[key]});                }            }        };    }) ();    (() => {        __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call (obj, prop))    }) ();    (() => {        __webpack_require__.r = (exports) => {            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {                Object.defineProperty (exports, Symbol.toStringTag, {value: 'Module'});            }            Object.defineProperty (exports, '__esModule', {value: true});        };    }) ();    var __webpack_exports__ = {};    //入口文件    (() => {        /*!**********************!*\          !*** ./src/index.js ***!          **********************/        __webpack_require__.r (__webpack_exports__);        var _info_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__ (/*! ./info.js */ "./src/info.js");        console.log (_info_js__WEBPACK_IMPORTED_MODULE_0__["default"])    }) ();}) ();

能够看到打包后的文件是一整个IIFE(立刻执行函数),并将info.jsname.js以路径名为key,value为箭头函数的模式保留在webpack_modules 当中,并在入口文件(对应index.js)的立刻执行函数中进行调用;可能当初咱们对这段进行过打包编译的代码有很多疑难,比方这打包后的代码浏览起来好麻烦,到底做了个啥?为何要采取这种模式对源码进行打包?我书写的明明是ES6代码,为何会转译成ES5代码?ES6代码是如何转译成ES5代码的?等等问题;俗话说工欲善其事必先利其器,咱们只有对这些问题有所深刻能力理解webpack的打包原理,当然也是为了对咱们接下来实现简易版webpack做铺垫;

编译后源码解读

置信很多靓仔到这里还是一脸懵,这打包进去的文件到底是个啥,外面不拘一格的办法到底何用?接下来我会一一解读;

对于IIFE的解读

在整个打包后的源码当中咱们能够看到很多

(x,...)=>{    ...省略}();

相似的办法,这类书写形式在咱们平时工作中并不常见,其实这类办法统称为IIFE 立刻执行函数表达式,全称为Immediately-invoked function expression, 因为在ES5当中没有显著的的块级作用域标准,为了避免模块与模块之间呈现变量净化,所以采取这种形式来标准作用域;从代码自身只需将它了解为定义一个办法,并立刻执行即可;

编译文件的entry入口

webpack.config.js中咱们规定的入口文件是index.js 那么在编译后的代码当中入口又是什么呢?答案是

(() => {    /*!**********************!*\      !*** ./src/index.js ***!      **********************/    __webpack_require__.r (__webpack_exports__);//定义以后模块为ES模块    //获取以后文件中的依赖模块数据,在index.js中的依赖模块为info.js    var _info_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__ (/*! ./info.js */ "./src/info.js");    //输入    console.log (_info_js__WEBPACK_IMPORTED_MODULE_0__["default"])}) ();

对应index.js

import info from "./info.js" console.log(info)

模块获取办法: webpack_require

function __webpack_require__ (moduleId) {    //moduleId为以后模块文件对应的路径名    //获取以后模块缓存    var cachedModule = __webpack_module_cache__[moduleId];    //判断以后模块是否有缓存,若有则返回缓存    if (cachedModule !== undefined) {        return cachedModule.exports;    }    //若以后模块没有缓存,则在缓存变量中存入空export对象,不便向下获取调用    var module = __webpack_module_cache__[moduleId] = {        exports: {}    };    //因为模块文件是以办法的模式保留在__webpack_modules__当中,    //在调用模块文件的同时会将模块文件数据在__webpack_module_cache__    //中进行缓存,并以闭包的形式赋值给module.export最终在__webpack_require__中返回    __webpack_modules__[moduleId] (module, module.exports, __webpack_require__);    return module.exports;}

因为CommonJS在浏览器中无奈运行,因而在编译之后会以__webpack_require__这种模式进行模块获取,如果你是一个足够仔细的靓仔,你就会发现__webpack_require__并没有像其余办法一样有一个独自的立刻执行函数包裹,
因为须要其拜访__webpack_module_cache____webpack_modules__

模块缓存__webpack_module_cache__与模块__webpack_modules__

首先咱们来看看模块缓存__webpack_module_cache__

var __webpack_module_cache__ = {};

为了进步执行效率咱们在调用模块的同时会对以后调用的模块进行判断是否进行过调用,若没有调用过以后模块则会在 __webpack_module_cache__当中进行保留,并以模块路径名作为key进行保留,若调用过则返回缓存内容,来达到优化;

而后咱们来看看咱们的模块__webpack_modules__

var __webpack_modules__ = ({    "./src/info.js":        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {            //定义以后模块为es模块            __webpack_require__.r (__webpack_exports__);            //提供以后模块的getter            __webpack_require__.d (__webpack_exports__, {                "default": () => (__WEBPACK_DEFAULT_EXPORT__)            });            //获取name模块数据            var _name_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__ (/*! ./name.js */ "./src/name.js");            //定义以后模块数据            const __WEBPACK_DEFAULT_EXPORT__ = (`${_name_js__WEBPACK_IMPORTED_MODULE_0__["default"]} 666`);        }),    "./src/name.js":        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {            __webpack_require__.r (__webpack_exports__);            __webpack_require__.d (__webpack_exports__, {                "default": () => (__WEBPACK_DEFAULT_EXPORT__)            });            const __WEBPACK_DEFAULT_EXPORT__ = ('龙骑士尹道长');        })});

这里其实很好了解,每个模块都以路径名为key,办法为value的模式保留在__webpack_modules__当中,调用的同时会将以后模块保留在__webpack_module_cache__ 当中不便下次调用模块

对于__webpack_require__.xxx

在编译后的代码中咱们还能够看到__webpack_require__.r__webpack_require__.d__webpack_require__.o这三个办法,那这三个办法是做什么用的呢

__webpack_require__.r

(() => {    __webpack_require__.r = (exports) => {        if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {            Object.defineProperty (exports, Symbol.toStringTag, {value: 'Module'});        }        Object.defineProperty (exports, '__esModule', {value: true});    };}) ();

该办法其实次要是为了给以后模块打上一个es模块标签

__webpack_require__.d

(() => {    __webpack_require__.d = (exports, definition) => {        for (var key in definition) {            if (__webpack_require__.o (definition, key) && !__webpack_require__.o (exports, key)) {                Object.defineProperty (exports, key, {enumerable: true, get: definition[key]});            }        }    };}) ();

__webpack_require__.d则是为了提供以后模块的getter

__webpack_require__.o

(() => {    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call (obj, prop))}) ();

判断该属性是否继承自该对象

源码中的ES6代码是如何转译成ES5的与打包文件的构建过程

在大抵理解打包后的代码运行逻辑之后,你是否发现咱们在源码当中应用的es6代码也一并转换成了es5,因为并不是所有的浏览器都能辨认es6代码,或者说浏览器不是运行所有的es6代码,因而在这里会做转换;那么到底在webpack当中咱们的es6代码是如何转换成es5代码的呢?

webpack会将咱们模块的es6语法转换成AST对象,而后剖析出以后模块所需的依赖,并将AST对象转换成es5代码,顺次循环剖析,最终会构建成graph(文件依赖图),在最初会生成bundle.js也就是咱们的打包文件,这便是webpack打包构建的全过程;

那么在这里可能就会有靓仔要问了,啥是AST?咱们接着往下看

webpack构建基石:AST

AST又称为形象语法树(Abstract Syntax Tree),是咱们构建的代码的一种形象示意,它以树状模式体现编程语言的语法结构;

在这里咱们须要用到一个AST转换工具
https://astexplorer.net/

以以下代码为例

class Dog{  constructor(){    this.name = '旺财'    this.sex = 'man'  }  lick(){    console.log('我是旺财,我是条老舔狗')  }}

在通过工具转换成AST语法树之后,能够看到源代码在AST语法树中都能够找到对应关系

AST对象如何转换成代码

取得AST对象之后要怎么转换成代码呢?

因为不同语言对应的AST对象构造不尽相同,举个例子,如果咱们当初手头上有一段JS代码要将它转换成Java代码,咱们就须要将这段JS代码先转换成JS对应的AST对象,而后对这段JS对应的AST对象进行转换,转换成Java代码对应的AST对象,最初再依据新的Java对应的AST对象转换成JAVA代码;整个过程次要经验了解析,转换,代码生成三个步骤;

在理解AST转换流程与webpack构建流程之后咱们就能够开始尝试构建一个简易版的webpack打包流程了;

手写webpack

置信当初大家曾经对webpack打包流程曾经有了肯定理解,那么咱们就用最简略的思路来实现一个乞丐版webpack,在此我再放一遍流程图不便大家书写代码

为了不便大家了解,我感觉有必要先带大家理解几个babel的作用;

筹备工作:依赖babel

@babel/parser

把源代码字符串转成形象语法树(AST),在解析过程中次要是两个阶段:词法剖析*语法分析

@babel/traverse

遍历形象语法树(AST),并调用Babel配置文件中的插件,对形象语法树(AST)进行增删改,在文中次要用于文件依赖剖析

@babel/core

是 babel的外围,次要作用就是依据咱们的配置文件转换代码,配置文件个别是.babelrc(动态文件)或 babel.config.js(可编程),次要作用如下:

  • 加载和解决配置(config)
  • 加载插件
  • 调用 Parser 进行语法解析,生成 AST
  • 调用 Traverser 遍历AST,并应用访问者模式利用'插件'对 AST 进行转换
  • 生成代码,包含SourceMap转换和源代码生成

文中次要用于将AST转换为代码字符串

读取文件

咱们能够在以后webpack下随便搭建一个js,实现以下操作

const fs = require ('fs')const {entry, output} = require ("./webpack.config")//webpack配置项function createAssets(filename){    //读取文件内容    const content = fs.readFileSync (filename, "utf-8")    console.log(content)//相熟nodejs的靓仔应该都能了解,这一步会许可文件内容}createAssets(entry)

文件内容转换成AST形象语法树

const fs = require ('fs')+const parser = require ('@babel/parser')const {entry, output} = require ("./webpack.config")//webpack配置项function createAssets(filename){    //读取文件内容    const content = fs.readFileSync (filename, "utf-8")    //转化成AST对象+   const ast = parser.parse (content, {+       sourceType: 'module'//输入内容为模块,还能够抉择为script,然而因为咱们是应用模块化操作所以不采纳script+   })+   console.log(ast)}createAssets(entry)

剖析以后文件依赖

const fs = require ('fs')const parser = require ('@babel/parser')+ const traverse = require ('@babel/traverse').defaultconst {entry, output} = require ("./webpack.config")//webpack配置项+ const path = require ("path")+ let relativeFilenames = {}function createAssets(filename){    //读取文件内容    const content = fs.readFileSync (filename, "utf-8")    //转化成AST对象    const ast = parser.parse (content, {        sourceType: 'module'//输入内容为模块,还能够抉择为script,然而因为咱们是应用模块化操作所以不采纳script    })    let dependencies = []    //剖析以后文件依赖+    traverse (ast, {+        ImportDeclaration ({node}) {+            //将以后依赖推入数组,不便前面构建graph+            dependencies.push (node.source.value)+            if (!relativeFilenames[node.source.value]) {+                const dirname = path.dirname (filename)+                relativeFilenames[node.source.value] = path.join (dirname, node.source.value).replace(/\/g,'/')+            }+        }+    })    console.log(relativeFilenames)}createAssets(entry)

将AST转换为ES5代码

const fs = require ('fs')const parser = require ('@babel/parser')const traverse = require ('@babel/traverse').default+ const babel = require ('@babel/core')const {entry, output} = require ("./webpack.config")//webpack配置项let relativeFilenames = {}function createAssets(filename){    //读取文件内容    const content = fs.readFileSync (filename, "utf-8")    //转化成AST对象    const ast = parser.parse (content, {        sourceType: 'module'//输入内容为模块,还能够抉择为script,然而因为咱们是应用模块化操作所以不采纳script    })    let dependencies = []    //剖析以后文件依赖    traverse (ast, {        ImportDeclaration ({node}) {            //将以后依赖推入数组,不便前面构建graph            dependencies.push (node.source.value)            if (!relativeFilenames[node.source.value]) {                const dirname = path.dirname (filename)                relativeFilenames[node.source.value] = path.join (dirname, node.source.value).replace(/\/g,'/')            }        }    })    //转换成ES5代码+    const {code} = babel.transformFromAstSync (ast, null, {+        presets: ['@babel/preset-env'],+    })    console.log(code)}createAssets(entry)

此时咱们运行代码曾经能够看到打包之后的入口文件 index.js

循环剖析,构建模块对照对象

const fs = require ('fs')const parser = require ('@babel/parser')const traverse = require ('@babel/traverse').defaultconst babel = require ('@babel/core')const path = require ("path")const {entry, output} = require ("./webpack.config")//webpack配置项let relativeFilenames = {}+ let webpackModules = {}//用于存储模块信息function createAssets(filename){    //读取文件内容    const content = fs.readFileSync (filename, "utf-8")    //转化成AST对象    const ast = parser.parse (content, {        sourceType: 'module'//输入内容为模块,还能够抉择为script,然而因为咱们是应用模块化操作所以不采纳script    })    let dependencies = []    //剖析以后文件依赖    traverse (ast, {        ImportDeclaration ({node}) {            //将以后依赖推入数组,不便前面构建graph            dependencies.push (node.source.value)            if (!relativeFilenames[node.source.value]) {                const dirname = path.dirname (filename)                relativeFilenames[node.source.value] = path.join (dirname, node.source.value).replace(/\/g,'/')            }        }    })    //转换成ES5代码    const {code} = babel.transformFromAstSync (ast, null, {        presets: ['@babel/preset-env'],    })    //判断模块是否被转换,若模块不存在则代表还未被转换    if (!webpackModules[filename]) {        const dirname = path.dirname (filename)        dependencies.forEach (item => {            //这里须要留神,因为咱们的模块key为绝对路径,而用babel取得的是相对路径,因而须要将相对路径转换为绝对路径            const absolutePath = path.join (dirname, item)            createAssets (absolutePath)        })        //对模块信息进行赋值        webpackModules[filename] = {            dependencies,            code,        }    }}createAssets(entry)

构建输入代码

此时咱们曾经获取了所有模块对象与模块对应的依赖,以及模块相对路径与绝对路径的对照,当初咱们须要构建输入代码

const fs = require ('fs')const parser = require ('@babel/parser')const traverse = require ('@babel/traverse').defaultconst babel = require ('@babel/core')const path = require ("path")const {entry, output} = require ("./webpack.config")//webpack配置项let relativeFilenames = {}let webpackModules = {}function createAssets(filename){    //读取文件内容    const content = fs.readFileSync (filename, "utf-8")    //转化成AST对象    const ast = parser.parse (content, {        sourceType: 'module'//输入内容为模块,还能够抉择为script,然而因为咱们是应用模块化操作所以不采纳script    })    let dependencies = []    //剖析以后文件依赖    traverse (ast, {        ImportDeclaration ({node}) {            //将以后依赖推入数组,不便前面构建graph            dependencies.push (node.source.value)            if (!relativeFilenames[node.source.value]) {                const dirname = path.dirname (filename)                relativeFilenames[node.source.value] = path.join (dirname, node.source.value).replace(/\/g,'/')            }        }    })    //转换成ES5代码    const {code} = babel.transformFromAstSync (ast, null, {        presets: ['@babel/preset-env'],    })    if (!webpackModules[filename]) {        const dirname = path.dirname (filename)        dependencies.forEach (item => {            const absolutePath = path.join (dirname, item)            createAssets (absolutePath)        })        webpackModules[filename] = {            dependencies,            code,        }    }}createAssets(entry)+function createOutputCode () {+    let codeStr = ''+    //循环模块对象构建输出模块代码对象字符串+    for (let key in webpackModules) {+        codeStr += `"${key.replace(/\/g,'/')}":(module, exports, require)=>{+            ${webpackModules[key].code}+        },`+    }+    const outputCode = `+        (()=>{+    const pathReference = ${JSON.stringify(relativeFilenames)}+    const modules = {${codeStr}}+    const __webpack_module_cache__ = {}+    function require(moduleId){+        var cachedModule = __webpack_module_cache__[moduleId];+        if (cachedModule !== undefined) {+            return cachedModule.exports;+        }+        var module = __webpack_module_cache__[moduleId] = {+            exports: {}+        };+        //这不十分重点,之前提过因为模块外部用的是相对路径,所以在这里咱们须要应用相对路径对应的绝对路径+        function requireFn(path){+            return require(pathReference[path])+        }+        modules[moduleId] (module, module.exports, requireFn);+        return module.exports;+    }+    require('${entry}')+})();    `+    console.log(outputCode)+}+ createOutputCode()

此时咱们再运行代码就能够获取咱们要输入的代码了

创立输入文件

const fs = require ('fs')const parser = require ('@babel/parser')const traverse = require ('@babel/traverse').defaultconst babel = require ('@babel/core')const path = require ("path")const {entry, output} = require ("./webpack.config")//webpack配置项let relativeFilenames = {}let webpackModules = {}function createAssets(filename){    //读取文件内容    const content = fs.readFileSync (filename, "utf-8")    //转化成AST对象    const ast = parser.parse (content, {        sourceType: 'module'//输入内容为模块,还能够抉择为script,然而因为咱们是应用模块化操作所以不采纳script    })    let dependencies = []    //剖析以后文件依赖    traverse (ast, {        ImportDeclaration ({node}) {            //将以后依赖推入数组,不便前面构建graph            dependencies.push (node.source.value)            if (!relativeFilenames[node.source.value]) {                const dirname = path.dirname (filename)                relativeFilenames[node.source.value] = path.join (dirname, node.source.value).replace(/\/g,'/')            }        }    })    //转换成ES5代码    const {code} = babel.transformFromAstSync (ast, null, {        presets: ['@babel/preset-env'],    })    if (!webpackModules[filename]) {        const dirname = path.dirname (filename)        dependencies.forEach (item => {            const absolutePath = path.join (dirname, item)            createAssets (absolutePath)        })        webpackModules[filename] = {            dependencies,            code,        }    }}createAssets(entry)function createOutputCode () {    let codeStr = ''    //循环模块对象构建输出模块代码对象字符串    for (let key in webpackModules) {        codeStr += `"${key.replace(/\/g,'/')}":(module, exports, require)=>{            ${webpackModules[key].code}        },`    }    const outputCode = `        (()=>{    const pathReference = ${JSON.stringify(relativeFilenames)}    const modules = {${codeStr}}    const __webpack_module_cache__ = {}    function require(moduleId){        var cachedModule = __webpack_module_cache__[moduleId];        if (cachedModule !== undefined) {            return cachedModule.exports;        }        var module = __webpack_module_cache__[moduleId] = {            exports: {}        };        function requireFn(path){            return require(pathReference[path])        }        modules[moduleId] (module, module.exports, requireFn);        return module.exports;    }    require('${entry}')})();    `+    createOutputFile(outputCode)}createOutputCode()+ function createOutputFile(code){+    //创立输入js+    function createAction(){+        fs.writeFile(`${output.path}/main.js`,code,()=>{})+    }+    //判断文件夹是否存在+    if (!fs.existsSync(output.path)) {+        //创立输入文件夹+        fs.mkdir(output.path, (err) => {+            createAction()+        })+    }else{+        createAction()+    }}

再运行js你会发现你的我的项目目录中就有了打包后的dist文件夹

至此咱们的乞丐版webpack曾经手写实现,运行dist文件夹下的main.js测试一下成绩吧