关于javascript:100行代码实现一个乞丐版webpack

1次阅读

共计 17107 个字符,预计需要花费 43 分钟才能阅读完成。

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').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

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

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()

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

创立输入文件

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 文件夹

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

正文完
 0