likes
comments
collection
share

100行代码实现一个乞丐版webpack

作者站长头像
站长
· 阅读数 14

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官网映入眼帘的就是这张图片

100行代码实现一个乞丐版webpack

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

我总结出了以下三点:

模块整合能力

随着项目体量的增大,模块文件增多使切图仔们处理模块文件变得异常艰辛,而模块资源又在浏览器当中运行,频繁的请求零散文件势必会对服务器造成负荷与应用的工作效率;而webpack可以将多个模块文件打包成一个文件,在减少资源内存的同时也减少了对服务器的请求

100行代码实现一个乞丐版webpack

代码编译能力

随着ES规范的快速发展,很多浏览器并不能及时兼容新的语法与API;这导致API的兼容变成一个较大的问题,而webpack则可以将这些浏览器看不懂的API与语法转换成浏览器看得懂的ES5语法;

100行代码实现一个乞丐版webpack

多种类模块资源管理

随着前端的发展模块种类增多(css,html,js,图片),而前端需要通过代码或者工具对模块资源进行管理整合;

100行代码实现一个乞丐版webpack

创建你的第一个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打包构建的全过程;

100行代码实现一个乞丐版webpack

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

webpack构建基石:AST

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

在这里我们需要用到一个AST转换工具 astexplorer.net/

以以下代码为例

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

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

100行代码实现一个乞丐版webpack

AST对象如何转换成代码

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

由于不同语言对应的AST对象结构不尽相同,举个例子,如果我们现在手头上有一段JS代码要将它转换成Java代码,我们就需要将这段JS代码先转换成JS对应的AST对象,然后对这段JS对应的AST对象进行转换,转换成Java代码对应的AST对象,最后再根据新的Java对应的AST对象转换成JAVA代码;整个过程主要经历了解析,转换,代码生成三个步骤;

100行代码实现一个乞丐版webpack

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

手写webpack

相信现在大家已经对webpack打包流程已经有了一定了解,那么我们就用最简单的思路来实现一个乞丐版webpack,在此我再放一遍流程图方便大家书写代码

100行代码实现一个乞丐版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').default
const {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

100行代码实现一个乞丐版webpack

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

const fs = require ('fs')
const parser = require ('@babel/parser')
const traverse = require ('@babel/traverse').default
const 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').default
const 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()

此时我们再运行代码就可以获取我们要输出的代码了

100行代码实现一个乞丐版webpack

创建输出文件

const fs = require ('fs')
const parser = require ('@babel/parser')
const traverse = require ('@babel/traverse').default
const 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文件夹

100行代码实现一个乞丐版webpack

至此我们的乞丐版webpack已经手写完成,运行dist文件夹下的main.js测试一下成果吧

100行代码实现一个乞丐版webpack