手写实现一个webpack打包工具(实现webpack核心思路)

720 阅读14分钟

1. Webpack本质

本质上,webpack是一个现代js应用程序的静态模块打包器。

主旨:当Webpack处理应用程序的时候,它会递归的构建一个依赖关系图,这个依赖关系图里包含应用程序需要的每个模块,然后将所有这些模块打包成一个或者多个bundle输出。

2. Webpack打包的主要流程

  • 初始化参数:shell webpack.config.js配置文件,读取各配置参数
  • 开始编译:初始化一个compiler对象,加载所有配置,开始进行编译
  • 确定入口:根据entry中的配置,找到所有的入口文件
  • 编译模块:从入口文件开始,调用所有的loader,再去递归的寻找依赖
  • 完成模块编译:得到的每个模块被编译后的最终内容以及各模块之间的依赖关系
  • 输出资源:根据得到的依赖关系,组装成一个个包含多个modulechunk
  • 输出完成:根据配置,确定要输出的文件名以及文件路径

3. Webpac简单实现

例如,平常写的Esmodule

引入一个代码,导出一个代码

// 引入代码
import axios from 'axios'
// 导出代码
expert default axios

这种esmodule的语法在低级浏览器,例如ie8,是不支持的,所以我们需要把这种代码打包,最后输出的文件是低级浏览器可以读取执行的。

1. Webpack打包工具简单实现步骤

  1. 找到一个入口文件
  2. 解析这个入口文件,提取他的依赖
  3. 解析入口文件依赖的依赖,即递归的去创建一个文件间的依赖关系图,描述所有文件的依赖关系
  4. 把所有文件打包成一个文件

2. webpack打包工具代码编写

1. 创建一个文件夹simple-webpack

直接建就可以了。

2. 为了管理包方便,我们需要初始化一下项目

执行命令

npm init

生成一个package.json文件

为什么要使用npm init命令初始化项目?

使用npm init命令初始化项目会生成一个package.json文件,这个文件主要
是记录项目的详细信息,例如项目开发需要的所有的依赖包、项目的详细信息都在这个文件中。
方便版本迭代及项目移植。

3. 新建几个js文件

新建一个source目录,在source目录下我们会创建几个js文件

  • name.js
  • message.js
  • entry.js

1). 创建一个js文件name.js,作为一个常量的存储文件,内容如下:

export const name = 'test';

2). 创建一个js文件message.js,引入刚才创建的name.js里面的常量,做一层封装,然后导出去,内容如下:

import { name } from './name';

export default `${name} is important!!!` // 封装成一个字符串

3). 创建一个入口文件entry.js,引入message.js文件内的message这个字符串,并打印,内容如下:

import message from "./message";

console.log(message)

三者导入关系如下:

entry.js --> message.js --> name.js

注意:关于exportexport default有以下几点需要注意:

  1. export与export default均可用于导出常量、函数、文件、模块等\
  2. 你可以在其它文件或模块中通过import+(常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用
  3. 在一个文件或模块中,export、import可以有多个,export default仅有一个
  4. 通过export方式导出,在导入时要加{ },export default则不需要 示例代码如下:
1. export导出方式
// a.js
export const str = 'dfnsadnjk';
export function log(sth) {
 return sth;
}

// 对应的导入方式:
// b.js
import { str, log} from 'a';  // 也可以分开写两次,导入的时候一定加花括号

2. export default导出方式
// a.js
const str = 'budjkjs';
export default str;

// 对应的导入方式:
// b.js
import str from 'a';

5. 开始编写打包工具,工具名叫myWebpack.js

根目录下创建一个myWebpack.js文件,功能如下:

1. 找到一个入口文件
2. 解析这个入口文件,提取他的依赖
3. 解析入口文件依赖的依赖,即递归的去创建一个文件间的依赖关系图,描述所有文件的依赖关系
4. 把所有文件打包成一个文件

首先我们找到的入口文件为source下的entry.js,我们通过一些方式把他引进来,怎么引进?

node有一个fs模块,可以读取文件,先引进fs模块,同时创建一个createAsset方法读取文件内容

const fs = require('fs');

function createAsset(filename) {
    // 同步读取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    console.log(content);
}

// 传参调用(传入我们的入口文件路径作为参数)
createAsset('./source/entry.js'); // 参数为相对路径

我们在终端执行命令:

node myWebpack.js

来打印一下我们传入的文件的内容(即entry.js)内容

1638790941(1).jpg 此时我们把入口文件的内容打印出来了。我们获取到入口文件的内容肯定还远远达不到需求,我们目的是为了获取入口文件的依赖,及它依赖的依赖的。接下来

6. 生成AST语法树,分析AST,思考如何能够解析出entry.js的依赖

AST语法树,我们都知道,这棵树定义了代码的结构,通过操纵这棵树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作。

在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里源代码特指编程语言的源代码。

普通代码转成抽象语法树的样子如下:

1638793696(1).jpg 在这里分析AST语法树,我们用到一个工具astexplorer.net/, 通过该工具可以在线查看转换语法结构。我们就简单的把语法树设想为一个对象。

对于import引入这种语法的AST,我们只需要关注AST语法树的几个节点,几个属性,代码如下:

improt message from './message.js';

如下图:

1638793883(1).jpg 注意:需要注意我箭头标注的那几个节点

File(是一个文件) -> program(指我们的程序) -> body(里面有我们用到的各种语法描述,是一个数组) -> ImportDeclaration(import定义声明的地方,就是指AST分析完之后,分析到我们代码里有个import) -> source -> value(引入文件的地址)

我们最重要的目的是要拿到import message from './message.js'中的./message.js

我们上面的分析都是基于entry.js文件的AST语法树的,所以接下来

7. 生成entry.js的AST语法树

安装babylon工具,一个基于babel的js解析工具,通过该工具,我们可以很方便的拿到js解析出来的语法树。

在根目录下新建一个.npmrc文件,设置npm的淘宝源,代码如下:

registry=https://registry.npmmirror.com

注意:淘宝源镜像地址改成最新的了。

安装babylon

npm i babylon

我们在代码中引入,生成entry.js的ast:

const fs = require('fs');
const babylon = require('babylon');

function createAsset(filename) {
    // 同步读取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    const ast = babylon.parse(content, {
        sourceType: "module"
    })
    console.log(ast);
}

// 传参调用(传入我们的入口文件路径作为参数)
createAsset('./source/entry.js'); // 参数为相对路径

1638917221(1).png 遍历AST节点,使用工具babel-traverse,安装命令如下:

npm i babel-traverse

它可以让我们像遍历对象一样遍历语法树。我们遍历语法树的目的就是为了找到ImportDeclaration下的source下的value。 我们在代码中引入babel-traverse

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;

function createAsset(filename) {
    // 同步读取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    // 获取语法树
    const ast = babylon.parse(content, {
        sourceType: "module"
    });
    // 遍历到语法树目标节点
    traverse(ast, { // 第二个参数是对每一个节点要做的什么事情,咱们选择ImportDeclaration节点
        ImportDeclaration: ({
            node // node就是语法树输出的,节点
        }) => {
            console.log(node)
        }
    })
}

// 传参调用(传入我们的入口文件路径作为参数)
createAsset('./source/entry.js'); // 参数为相对路径

输出结果如下图:

1638926724(1).png

8. 获取entry.js的依赖

怎么读取节点呢? 依赖babel,通过使用babel-traverse可以遍历语法树的节点。

注意:真情情况下entry.js的依赖不可能只有一个,我们只是举例打印了一个,所以我们可以定义一个数组,把entry.js的相关依赖都推进定义的数组中。代码如下:

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;

function createAsset(filename) {
    // 同步读取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    // 获取语法树
    const ast = babylon.parse(content, {
        sourceType: "module"
    });

    // 定义变量存储entry.js相关依赖
    const dependencies = [];
    // 遍历到语法树目标节点
    traverse(ast, { // 第二个参数是对每一个节点要做的什么事情,咱们选择ImportDeclaration节点
        ImportDeclaration: ({
            node // node就是语法树输出的,节点
        }) => {
            dependencies.push(node.source.value);
        }
    });

    console.log(dependencies);
}

// 传参调用(传入我们的入口文件路径作为参数)
createAsset('./source/entry.js'); // 参数为相对路径

打印结果为:

1638969158(1).jpg

9. 优化createAsset,使其能够区分不同文件的不同依赖

即让我们能够一一对应,知道某个文件他的对应依赖是哪些,例如,我们输入时是知道输入的哪个文件,但我们输出的文件就不知道是输入的哪个文件的依赖了,故我们要优化一下我们写的createAsset方法。

因为我们是要获取所有文件的依赖,所以我们需要一个id来标识这些文件。

使用id获取的依赖一一对应。

这里我们用一个自增的number来作为id的值,这样遍历的每一个文件id都是唯一的了。

我们先获取entry.js的id、filename以及dependencies。

代码如下:

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;

// 为了保持自增需要把计算自增的ID放在createAsset外面
let ID = 0;

function createAsset(filename) {
    // 同步读取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    // 获取语法树
    const ast = babylon.parse(content, {
        sourceType: "module"
    });

    // 定义变量存储entry.js相关依赖
    const dependencies = [];
    // 遍历到语法树目标节点
    traverse(ast, { // 第二个参数是对每一个节点要做的什么事情,咱们选择ImportDeclaration节点
        ImportDeclaration: ({
            node // node就是语法树输出的,节点
        }) => {
            dependencies.push(node.source.value);
        }
    });

    // 使传入的文件能够与获得的依赖一一对应,再抛出去
    const id = ID++;
    return {
        id,
        filename,
        dependencies
    }
}

// 传参调用(传入我们的入口文件路径作为参数)
const mainAsset = createAsset('./source/entry.js'); // 参数为相对路径

console.log(mainAsset);

注意:为了保持自增需要把计算自增的ID放在createAsset外面。

10. 我们已获取到单个文件的依赖,现在尝试建立依赖图

  • 新增一个函数createGraph,把createAsset的调用移入createGraph。
  • 特别注意:我们的入口文件不能写死了,entry的路径需要是动态的,所以createGraph接受一个参数entry(对应入口文件) 代码如下:
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;

// 为了保持自增需要把计算自增的ID放在createAsset外面
let ID = 0;

function createAsset(filename) {
    // 同步读取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    // 获取语法树
    const ast = babylon.parse(content, {
        sourceType: "module"
    });

    // 定义变量存储entry.js相关依赖
    const dependencies = [];
    // 遍历到语法树目标节点
    traverse(ast, { // 第二个参数是对每一个节点要做的什么事情,咱们选择ImportDeclaration节点
        ImportDeclaration: ({
            node // node就是语法树输出的,节点
        }) => {
            dependencies.push(node.source.value);
        }
    });

    // 使传入的文件能够与获得的依赖一一对应,再抛出去
    const id = ID++;
    return {
        id,
        filename,
        dependencies
    }
}

function createGraph(entry) {
    // 传参调用(传入我们的入口文件路径作为参数)
    const mainAsset = createAsset(entry); // 参数为相对路径
    return mainAsset;
}

const graph = createGraph('./source/entry.js');
console.log(graph);

11. 上面的过程,传的参数为相对路径,想办法把他们转成绝对路径

相对路径转化为绝对路径

怎么把相对路径转化为绝对路径呢?分为一下几步:

  1. 遍历存储多个文件(文件指entry.js这类文件)及他们的依赖信息的allAsset(例如当前的entry.js文件及它的依赖信息)
  2. 通过node.js自带的path.dirname()方法获取到当前文件例如entry.js文件所在的目录dirname
  3. 遍历当前文件(例如entry.js)的依赖信息(dependencies),通过node.js方法path.join(),把之前获取的entry.js文件的目录dirname,与依赖的相对路径拼接,获取到依赖的绝对路径absolutePath
  4. 把依赖的绝对路径absolutePath作为参数,重新调用createAsset方法,这样就获取到依赖的依赖相关信息(类似entry.js的依赖相关信息一样) 注意:在这里我们也看到createAsset方法已经从第八步的获取entry.js文件的相关依赖信息优化成了一个获取任意传入的文件的依赖信息的方法。 即获取文件的资源详情

有了绝对路径,我们才能获取到各个文件的asset

function createGraph(entry) {
    // 传参调用(传入我们的入口文件路径作为参数)
    const mainAsset = createAsset(entry); // 参数为相对路径
    // 我们需要一个数组去存储所有文件的依赖信息例如mainAsset的这类信息
    // 因为我们会有多个文件,所以需要数组去存储
    // 现在我们就一个文件mainAsset
    const allAsset = [mainAsset];
     // 现在我们遍历allAssets,我们在遍历的过程中会一直往allAssets中推东西,一直遍历到结束,所以用了一个数组
    for (let asset of allAsset) {
        // 拿到当前这个文件asset.filename所在的目录名
        // 拿到目录名才能拼出他的结对路径
        const dirname = path.dirname(asset.filename);
        console.log(dirname)
        // 遍历当前文件的依赖
        asset.dependencies.forEach(relativePath => {
            // 获取当前文件(entry.js)依赖(message.js)的绝对路径
            const absolutePath = path.join(dirname, relativePath);
            console.log(absolutePath)
            // 之前的这些我们都是拿的当前文件(例如entry.js)的依赖,获取到的是当前文件的依赖的绝对路径
            // 那当前文件的依赖的相关信息(即entry.js的依赖的依赖信息)
            // 即A依赖B,通过上面的一系列方法可以获取到B是谁,B的绝对路径是什么了,那B的依赖文件C我们怎么获取,怎么知道C的绝对路径是什么呢?
            // 很简单,我们像之前entry.js一样,直接把B的绝对路径当做参数,传给createGraph
            // 即类似遍历递归!!!!!!
            const childAsset = createAsset(absolutePath); // 获取到依赖文件的相关依赖信息了
            console.log(childAsset);
        })
    }
}

12. 当我们把相对路径转化为绝对路径后,我们需要一个map属性,记录dependencies中的相对路径 和 childAsset的对应关系。

因为我们后面要做依赖的引入,需要这样的一个对应关系。

在这个对应关系中,我们用dependencies中储存的相对路径作为map的key,这个key对应存储的是childAsset的id。

大家还记得childAsset,也就是createAsset方法生成的一个数据格式吗?再给大家看一下吧,这样的:

{
  id: 0,
  filename: './source/entry.js',
  dependencies: [ './message.js' ]
}

或者这样的

{
  id: 1,
  filename: 'source\\message.js',
  dependencies: [ './name.js' ]
}

或者下面的

{ 
    id: 2, 
    filename: 'source\\name.js', 
    dependencies: []
}

设置asset.mapping = {},且用相对路径relativePath作为map的key。

function createGraph(entry) {
    // 传参调用(传入我们的入口文件路径作为参数)
    const mainAsset = createAsset(entry); // 参数为相对路径
    // 我们需要一个数组去存储所有文件的依赖信息例如mainAsset的这类信息
    // 因为我们会有多个文件,所以需要数组去存储
    // 现在我们就一个文件mainAsset
    const allAsset = [mainAsset];
     // 现在我们遍历allAssets,我们在遍历的过程中会一直往allAssets中推东西,一直遍历到结束,所以用了一个数组
    for (let asset of allAsset) {
        // 拿到当前这个文件asset.filename所在的目录名
        // 拿到目录名才能拼出他的结对路径
        const dirname = path.dirname(asset.filename);

        // 当我们把相对路径转化为绝对路径后,我们需要一个map,记录dependencies中的相对路径 和 childAsset的对应关系。
        // 因为我们后面要做依赖的引入,需要这样的一个对应关系。
        asset.mapping = {}

        // 遍历当前文件的依赖
        asset.dependencies.forEach(relativePath => {
            // 获取当前文件(entry.js)依赖(message.js)的绝对路径
            const absolutePath = path.join(dirname, relativePath);
            // 之前的这些我们都是拿的当前文件(例如entry.js)的依赖,获取到的是当前文件的依赖的绝对路径
            // 那当前文件的依赖的相关信息(即entry.js的依赖的依赖信息)
            // 即A依赖B,通过上面的一系列方法可以获取到B是谁,B的绝对路径是什么了,那B的依赖文件C我们怎么获取,怎么知道C的绝对路径是什么呢?
            // 很简单,我们像之前entry.js一样,直接把B的绝对路径当做参数,传给createGraph
            // 即类似遍历递归!!!!!!
            const childAsset = createAsset(absolutePath); // 获取到依赖文件的相关依赖信息了

            // 用相对路径作为key
            asset.mapping[relativePath] = childAsset.id
        })
    }
}

现在设置完map属性后的值如下:

[
  {
    id: 0,
    filename: './source/entry.js',
    dependencies: [ './message.js' ],
    mapping: { './message.js': 1 }
  },
  {
    id: 1,
    filename: 'source\\message.js',
    dependencies: [ './name.js' ],
    mapping: { './name.js': 2 }
  },
  { id: 2, filename: 'source\\name.js', dependencies: [], mapping: {} }
]

即map的是当前文件资源详情里dependencies属性内的信息,即当前文件的依赖文件的相对路径是依赖文件的资源详情的id。这样,当前文件的依赖文件就跟依赖文件的资源详情做了一一对应关系。

13. 接下来开始遍历所有的文件了,上面所有的内容都只是铺垫。

怎么遍历所有文件呢,即把当前文件的依赖相对路径转化后的信息详情(通过map一一对应过)推到数组allAsset,这样allAsset就有新的信息继续遍历了。

allAsset.push(childAsset) 这样做相当于是递归了,把所有文件及依赖文件,依赖文件的依赖文件...都放在数组allAsset中了。

代码如下:

function createGraph(entry) {
    // 传参调用(传入我们的入口文件路径作为参数)
    const mainAsset = createAsset(entry); // 参数为相对路径
    // 我们需要一个数组allAsset去存储所有文件的依赖信息例如mainAsset的这类信息
    // 因为我们会有多个文件,所以需要数组去存储
    // 现在我们就一个文件mainAsset
    const allAsset = [mainAsset];
     // 现在我们遍历allAssets,我们在遍历的过程中会一直往allAssets中推东西,一直遍历到结束,所以用了一个数组
    for (let asset of allAsset) {
        // 拿到当前这个文件asset.filename所在的目录名
        // 拿到目录名才能拼出他的结对路径
        const dirname = path.dirname(asset.filename);

        // 当我们把相对路径转化为绝对路径后,我们需要一个map,记录dependencies中的相对路径 和 childAsset的对应关系。
        // 因为我们后面要做依赖的引入,需要这样的一个对应关系。
        asset.mapping = {}

        // 遍历当前文件的依赖
        asset.dependencies.forEach(relativePath => {
            // 获取当前文件(entry.js)依赖(message.js)的绝对路径
            const absolutePath = path.join(dirname, relativePath);
            // 之前的这些我们都是拿的当前文件(例如entry.js)的依赖,获取到的是当前文件的依赖的绝对路径
            // 那当前文件的依赖的相关信息(即entry.js的依赖的依赖信息)
            // 即A依赖B,通过上面的一系列方法可以获取到B是谁,B的绝对路径是什么了,那B的依赖文件C我们怎么获取,怎么知道C的绝对路径是什么呢?
            // 很简单,我们像之前entry.js一样,直接把B的绝对路径当做参数,传给createGraph
            // 即类似遍历递归!!!!!!
            const childAsset = createAsset(absolutePath); // 获取到依赖文件的相关依赖信息了

            // 用相对路径作为key
            asset.mapping[relativePath] = childAsset.id

            // 把当前文件的依赖相对路径转化后的信息推到数组allAsset,这样allAsset就有新的信息继续遍历了
            allAsset.push(childAsset) // 这样做相当于是递归了,一遍遍的遍历 
        })
    }

    return allAsset;
}

return allAsset,打印graph,如下:

1639394788(1).jpg 这个输出就是依赖图!!!

14. 有了依赖图,我们就可以尝试把所有文件打包成一个文件了

新增一个方法bundle,为什么要新增一个bundle?因为我们最终输出的代码是打包后的,如果我们不经过各种打包,其实我们最终输出的还是esmodule的各种写法,并不能在低版本浏览器上运行。

所以,我们必须要经过一次转译的过程。这个转译的方法,我们称为bundle。代码如下:

function bundle(graph) {

}

const graph = createGraph('./source/entry.js');
const result = bundle(graph);
console.log(result);

15. 创建整体的结果代码

整体的结果代码需要一个包裹的,因为咱们最后打包出来的东西需要接受一个参数,且需要立即执行,因为咱们最后打包出来的是没有一个主函数去执行所有逻辑的,但他其实是一个整个代码块,他又需要立即执行,所以用一个自执行函数来包裹(即用一个自执行函数包裹整体的结果代码)。

即包裹的这个函数是IIFE(立即执行函数)。

立即执行函数(IIFE)接受的参数是什么?

module,module是什么?

是每一个模块,即每一个模块webpack都会用立即执行函数(IIFE,也就是自执行函数)包裹。

在bundle方法里:

  • 首先我们确定我们最后输出的是一个字符串形式
  • 所以最后的代码块一定是一个字符串,我们首先用一个空字符串声明这个变量
  • 然后咱们遍历graph,去获取所有的module,然后都拼接在一起,成为一个字符串(graph里面其实每一个item就是一个module) graph示例如下:
[
  {
      id: 0,
      filename: './source/entry.js',
      dependencies: [ './message.js' ],
      mapping: { './message.js': 1 }
  },
    {
        id: 1,
        filename: 'source\message.js',
        dependencies: [ './name.js' ],
        mapping: { './name.js': 2 }
    },
    { 
        id: 2,
        filename: 'source\name.js', 
        dependencies: [], 
        mapping: {} 
    }
  ]
  • 然后循环graph把module拼接,拼接怎么拼?
  • 就是需要一个id各种参数对应的,加逗号是因为一直要往modules上拼接东西,所以要加逗号分割每次拼接的内容

代码如下:

function bundle(graph) {
    // 首先我们确定我们最后输出的是一个字符串的形式
    // 所以最后的代码块一定是一个字符串,我们首先用一个空字符串声明这个变量
    let modules = ''

    // 然后咱们遍历graph,去获取所有的module,然后都拼接在一起,成为一个字符串
    // graph里面其实每一个item就是一个module
      /*
     * graph示例
        [
          {
              id: 0,
              filename: './source/entry.js',
              dependencies: [ './message.js' ],
              mapping: { './message.js': 1 }
          },
            {
                id: 1,
                filename: 'source\message.js',
                dependencies: [ './name.js' ],
                mapping: { './name.js': 2 }
            },
            { id: 2, filename: 'source\name.js', dependencies: [], mapping: {} }
          ]
     */
    // 然后循环graph把module拼接,拼接怎么拼?
    // 就是需要一个id与各种参数对应的,加逗号是因为一直要往modules上拼接东西,所以要加逗号分割每次拼接的东西
    graph.forEach(module => {

        modules += `${module.id}:[
        ],`
    })
    const reslut = `
        (function(){
        })(${modules})
    `
}

我们拼接的modules字符串到底是个什么样子的东西呢?

我们使用babel工具 这个工具左边可以写你输入的esmodule的格式,右边会实时生成你打包好的格式。例如:

1639398367(1).png 我们可以在右边看到他编译出的代码是什么样子的,以及他需要什么样的东西。

注意:我们看到他里面有一个exports,有一个require,但在右侧编译的代码里我们没有看到exprots对象的声明,也没有任何require函数的声明。

所以这两个东西我们在webpack模块引入以及导出,我们需要手动给他们创建的两个东西。

16. 编译源代码

在这里我们需要用到babel工具

  • babel-core:我们是通过babel把他们从源代码转译成刚才在babel工具看到的右侧的奇奇怪怪的代码的。
  • 我们还需要装一个babel预设的东西,babel-preset-env:就是来告诉babel要把源代码转成什么格式的,即在特定的平台上执行特定的转码规则(即按需转码) 安装代码如下:
npm i babel-core babel-preset-env

记住,安装上面两个插件的目的是为了编译代码

安装完插件后,引入我们安装的插件。

在此之前,我们获取到的每一个文件资源如下:

[
  {
      id: 0,
      filename: './source/entry.js',
      dependencies: [ './message.js' ],
      mapping: { './message.js': 1 }
  },
    {
        id: 1,
        filename: 'source\message.js',
        dependencies: [ './name.js' ],
        mapping: { './name.js': 2 }
    },
    { 
        id: 2,
        filename: 'source\name.js', 
        dependencies: [], 
        mapping: {} 
    }
  ]

每一个文件的资源仅仅只有它的filenam及依赖,没有源代码啊。

所以接下来,我们创建它的源代码。

创建源代码在createAsset方法里,此时createAsset方法代码如下:

function createAsset(filename) {
    // 同步读取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    // 获取语法树
    const ast = babylon.parse(content, {
        sourceType: "module"
    });

    // 定义变量存储entry.js相关依赖
    const dependencies = [];
    // 遍历到语法树目标节点
    traverse(ast, { // 第二个参数是对每一个节点要做的什么事情,咱们选择ImportDeclaration节点
        ImportDeclaration: ({
            node // node就是语法树输出的,节点
        }) => {
            dependencies.push(node.source.value);
        }
    });

    // 使传入的文件能够与获得的依赖一一对应,再抛出去
    const id = ID++;
    return {
        id,
        filename,
        dependencies
    }
}

接下来这里我们需要用到我们刚才安装的babel工具,使用babel.transformFromAst对我们之前拿到的ast语法树进行转码,代码如下:

const {node} = babel.transformFromAst(ast, null, {
    presets: ['env']
})

// 使传入的文件能够与获得的依赖一一对应,再抛出去
const id = ID++;
return {
    id,
    filename,
    dependencies,
    node
}

其中transformFromAst的第二个参数code,因为我们用了ast了,所以不需要原生的code了,就传null,第三个参数是告诉babel你希望产生什么样的代码,直接默认的就可以了,传{ presets: ['env'] },此时获取的code编译好的code

打印我们整体代码的graph,查看编译的结果code,结果如下:

1639658295(1).png 首先我们看每一个文件的code编译跟之前我们使用babel工具编译的结果是一样的,都是带有requireexports

注意:entry.js的code编译没有exports是因为entry.js没有导出)

接下来咱们要做的就是往code源代码传入它需要的东西:requireexports,还有modulemodule就是所有的东西都需要的模块)

17. 把编译后的代码加入到bundle方法result

首先稍微科普一下,在commonjs规范要求中:

  • module变量代表当前模块

这个变量(也就是module)是一个对象,它的exports属性是对外的接口,比如常见的module.exports,对外的接口是暴露给其他地方的,比如加载(导入)某个模块,其实就是加载该模块的module.exports属性。

  • require方法用于加载模块

接下来我们就需要去实现module、exports、require,我们需要在每一个模块中都去实现function、变量等,所以我们在graph的遍历里去做。

function bundle(graph) {
    // 首先我们确定我们最后输出的是一个字符串的形式
    // 所以最后的代码块一定是一个字符串,我们首先用一个空字符串声明这个变量
    let modules = ''

    // 然后咱们遍历graph,去获取所有的module,然后都拼接在一起,成为一个字符串
    // graph里面其实每一个item就是一个module
      /*
     * graph示例
        [
          {
              id: 0,
              filename: './source/entry.js',
              dependencies: [ './message.js' ],
              mapping: { './message.js': 1 }
          },
            {
                id: 1,
                filename: 'source\message.js',
                dependencies: [ './name.js' ],
                mapping: { './name.js': 2 }
            },
            { id: 2, filename: 'source\name.js', dependencies: [], mapping: {} }
          ]
     */
    // 然后循环graph把module拼接,拼接怎么拼?
    // 就是需要一个id与各种参数对应的,加逗号是因为一直要往modules上拼接东西,所以要加逗号分割每次拼接的东西
    graph.forEach(module => {

        modules += `${module.id}:[
            
        ],`
    })
    const reslut = `
        (function(){
        })(${modules})
    `
}
  1. 我们现在拼接的modules中新建一个function,参数为requiremoduleexports
  2. function里的代码体是什么呢?是我们刚才在上一步已经获取到的code。
  3. 其实我们要遍历的就是code这个东西,我们因为需要require, module, exports,所以我们以一个函数的形式把code包裹。
  4. 我们可以在需要的时候调用创建的这个函数。 代码如下:
graph.forEach(module => {

    modules += `${module.id}:[
        function(require, module, exports) {
            // 代码体就是我们刚才生成的code
            ${module.code}
            // 其实我们要遍历的就是code这个东西,我们因为需要require, module, exports,所以我们以一个函数的形式把code包裹
            // 然后在需要的时候可以调用这个函数
        }
    ],`
})
  1. 其实此时除了requiremoduleexports都可以取到了。
  2. module就是我们传进来的graph的itemmodule
  3. exports就是module的属性,可以暂时理解为就是一个空对象
  4. 因为我们的依赖都是放在mapping里的,所以我们把mapping也传进去。 此时bundle方法的代码体如下:
function bundle(graph) {
    // 首先我们确定我们最后输出的是一个字符串的形式
    // 所以最后的代码块一定是一个字符串,我们首先用一个空字符串声明这个变量
    let modules = ''

    // 然后咱们遍历graph,去获取所有的module,然后都拼接在一起,成为一个字符串
    // graph里面其实每一个item就是一个module
    // 然后循环graph把module拼接,拼接怎么拼?
    // 就是需要一个id与各种参数对应的,加逗号是因为一直要往modules上拼接东西,所以要加逗号分割每次拼接的东西
    
    graph.forEach(module => {
        modules += `${module.id}:[
            function(require, module, exports) {
                // 代码体就是我们刚才生成的code
                ${module.code}
                // 其实我们要遍历的就是code这个东西,我们因为需要require, module, exports,所以我们以一个函数的形式把code包裹
                // 然后在需要的时候可以调用这个函数
                
                // 其实此时除了require,module跟exports都可以取到了
                // module就是我们传进来的graph的item即module
                // exports就是module的属性,可以暂时理解为就是一个空对象
            },
            ${JSON.stringify(module.mapping)},
        ],`
    });
    const reslut = `
        (function(){
        })(${modules})
    `;

    return reslut;
}

输出一下结果: 执行命令

node myWebpack.js

此时的控制台结果比较乱,我们可以使用一个插件js-beautify(格式化代码)使得输出结果变得规整。 npm安装js-beautify

npm i js-beautify

此时执行命令

node myWebpack.js | js-beautify

再次输出结果:

1639664359(1).png

  • 我们看到输出结果里有一个自执行函数,目前还没有接受参数
  • 传参只写了每一个item里面做了什么东西
  • 首先item里面是一个数组,每一个数组里第一个元素是声明的函数,这个函数保函requiremoduleexports真正编译的代码
  • 数组里还一个元素mapping,存每一个文件需要的依赖的,用这个依赖导入各种需要的东西,故把mapping放在了这里

18. 接下来在bundle方法里的result变量里实现require方法

此时bundle方法的代码体如下:

1639666893(1).jpg 其中的result的代码部分如下:

// 实现 require方法
const reslut = `
    (function(modules){
    
    })(${modules})
`;
return reslut;

现在我们要做的是完善这个result。步骤如下:

  1. result是一个自执行函数,由于这个函数传的参数modules,所以这个函数是可以拿到modules的(也就是我们前面步骤获取的modules)。
  2. 在自执行函数内我们定义require,require接受的参数为id(因为咱们所有的映射关系都是通过id来存的)
  3. 我们可以通过传入的id,再根据自执行函数接受到的modules,可以获取一些东西
  4. 取到什么东西呢?请看上面的graph遍历,使用modules[id],我们可以获取到一个数组
  5. 这个数组元素①是一个function元素②mapping
  6. 然后声明变量接受这两个元素
const [fn, mapping] = modules[id]; //通过id,就是获取对应的一个数组,获取到里面的元素function、mapping
  1. 此时此步骤获取的function也就是定义的fn是谁?还记得上面modules拼接代码体内定义的function了吗,就是它
  2. 它有三个参数,分别是require,module,exports

在定义的require内:

  1. 现在咱们定义一个localRequire函数,也就是当前要传给后面代码体的一个require函数,咱们定义为localRequire
  2. 这个require是为了传给后面去引入各种自己的依赖的
  3. localRequire接受的参数为一个相对路径
  4. 然后可以在localRequire内直接调用定义的require函数,是把现有的各种资源都能拿到
  5. require接受的是一个id,这个id我们怎么获取呢?还记得mapping吗?createGraph方法遍历asset.dependencies 其中有段代码asset.mapping[relativePath] = childAsset.id,通过mapping[relativePath]
  6. 而此时我们通过modules已经获取到了mapping,故通过相对路径mapping[relativePath]获取通过createGraph方法存到mapping属性中的id 声明的localRequire代码如下:
function localRequire(relativePath) {
    // 这个require是为了传给后面去引入各种自己的依赖的
    // localRequire接受的参数为一个相对路径
    
    // 然后可以直接调用定义的require函数,是把现有的各种资源都能拿到
    // require接受的是一个id,这个id我们怎么获取呢?还记得mapping吗?asset.mapping[relativePath] = childAsset.id
    // 而此时我们通过modules已经获取到了mapping,故通过相对路径mapping[relativePath]获取通过createGraph方法存到mapping属性中的id
    return require(mapping[relativePath]);
}

此时声明的localRequire函数可以继续调用require(即获取到mapping里的id,就可以继续调用require了)

接下来声明module,module是有一个exports属性的,所以可以直接定义为下面的代码体:

const module = { exports: {}};

我们已经有了三个需要的参数require(也就是上面定义的localRequire)、module、exports

那就开始执行上面获取的function吧,也就是声明的fn。步骤如下:

  1. 执行fn
  2. fn还记得是什么样子吗?还记得上面modules拼接代码体内定义的function吗?它接受三个参数require, module, exports
  3. 其中require就是localRequire(什么是依赖,所谓的依赖就是咱们前面定义的用id跟文件存储依赖文件的属性作一一对应的,所以必然是一个id)
  4. 第二个参数module,上面声明过的
  5. 第三个参数是exports,其实就是module.exports
  6. 其实执行该fn也就是执行咱们在createAsset方法中通过babel工具获取的code代码体

执行fn的代码如下:

fn(localRequire, module, module.exports);

接下来,在commonjs规范要求中,加载(导入)某个模块,其实就是加载(导入)该模块的module.exports属性。

所以,require返回的就是module.exports,代码如下:

return module.exports;

整个bundle方法已经完成,代码如下:

function bundle(graph) {
    // 首先我们确定我们最后输出的是一个字符串的形式
    // 所以最后的代码块一定是一个字符串,我们首先用一个空字符串声明这个变量
    let modules = '';

    // 然后咱们遍历graph,去获取所有的module,然后都拼接在一起,成为一个字符串
    // graph里面其实每一个item即module
    // 然后循环graph把module拼接,拼接怎么拼?
    // 就是需要一个id与各种参数对应的,加逗号是因为一直要往modules上拼接东西,所以要加逗号分割每次拼接的东西
    // modules其实就是一个对象

    graph.forEach(module => {
        modules += `${module.id}:[  // modules其实就是一个对象
            function(require, module, exports) {
                // 代码体就是我们刚才生成的code
                ${module.code}
                // 其实我们要遍历的就是code这个东西,我们因为需要require, module, exports,所以我们以一个函数的形式把code包裹
                // 然后在需要的时候可以调用这个函数
                
                // 其实此时除了require,module跟exports都可以取到了
                // module就是我们传进来的graph的item即module
                // exports就是module的属性,可以暂时理解为就是一个空对象
            },
            ${JSON.stringify(module.mapping)},
        ],`
    });
    // 实现 require方法
    const reslut = `
        (function(modules){
            function require(id) {
                // 我们可以通过传入的id,再根据自执行函数接受到的modules,可以获取一些东西
                // 取到什么东西呢?请看上面的graph遍历,使用modules[id],我们可以获取到一个数组
                // 这个数组元素1是一个function,元素2是一个mapping
                
                const [fn, mapping] = modules[id]; //通过id,就是获取对应的一个数组,获取到里面的元素function、mapping
                
                // 此时此步骤获取的function也就是定义的fn是谁?还记得上面modules拼接代码体内定义的function了吗,就是它
                // 它有三个参数,分别是require,module,exports
                
                // 现在咱们定义一个localRequire函数,也就是当前要传给后面代码体的一个require函数,咱们定义为localRequire
                
                function localRequire(relativePath) {
                    // 这个require是为了传给后面去引入各种自己的依赖的
                    // localRequire接受的参数为一个相对路径
                    
                    // 然后可以直接调用定义的require函数,是把现有的各种资源都能拿到
                    // require接受的是一个id,这个id我们怎么获取呢?还记得mapping吗?asset.mapping[relativePath] = childAsset.id
                    // 而此时我们通过modules已经获取到了mapping,故通过相对路径mapping[relativePath]获取通过createGraph方法存到mapping属性中的id
                    return require(mapping[relativePath]);
                }
                // 此时声明的localRequire函数可以继续调用require(即获取到mapping里的id,就可以继续调用require了)
                
                //接下来声明module,module是有一个exports属性的,所以可以直接定义为下面的代码体
                const module = { exports: {}};
                
                // 然后执行fn
                // fn还记得是什么样子吗?还记得上面modules拼接代码体内定义的function吗?它接受三个参数require, module, exports
                // 其中require就是localRequire(什么是依赖,所谓的依赖就是咱们前面定义的用id跟文件存储依赖文件的属性作一一对应的,所以必然是一个id)
                // 第二个参数module,上面声明过的
                // 第三个参数是exports,其实就是module.exports
                // 其实执行该fn也就是执行咱们在createAsset方法中通过babel工具获取的code
                
                fn(localRequire, module, module.exports);
                
                // 在commonjs规范要求中,加载(导入)某个模块,其实就是加载(导入)该模块的module.exports属性
                // 故require返回的就是module.exports
                
                return module.exports;
            }
            require(0); // 先require(0),因为咱们初始的,最开始的id是0,通过require(0),实现对入口文件的调用
        })({${modules}})
    `;
    return reslut;
}
  1. 整个代码已经完成,主体代码体如下:
/*
 * @Author: JUEDIZHE
 * @Date: 2021-12-05 16:11:04
 * @LastEditors: OBKoro1
 * @LastEditTime: 2021-12-06 19:38:10
 * @Description:
 */

// 1. 找到一个入口文件
// 2. 解析这个入口文件,提取他的依赖
// 3. 解析入口文件依赖的依赖,即递归的去创建一个文件间的依赖关系图,描述所有文件的依赖关系
// 4. 把所有文件打包成一个文件

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const path = require('path');
const babel = require('babel-core');


// 为了保持自增需要把计算自增的ID放在createAsset外面
let ID = 0;

function createAsset(filename) {
    // 同步读取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    // 获取语法树
    const ast = babylon.parse(content, {
        sourceType: "module"
    });

    // 定义变量存储entry.js相关依赖
    const dependencies = [];
    // 遍历到语法树目标节点
    traverse(ast, { // 第二个参数是对每一个节点要做的什么事情,咱们选择ImportDeclaration节点
        ImportDeclaration: ({
            node // node就是语法树输出的,节点
        }) => {
            dependencies.push(node.source.value);
        }
    });

    const {code} = babel.transformFromAst(ast, null, {
        presets: ['env']
    })

    // 使传入的文件能够与获得的依赖一一对应,再抛出去
    const id = ID++;
    return {
        id,
        filename,
        dependencies,
        code
    }
}

function createGraph(entry) {
    // 传参调用(传入我们的入口文件路径作为参数)
    const mainAsset = createAsset(entry); // 参数为相对路径
    // 我们需要一个数组allAsset去存储所有文件的依赖信息例如mainAsset的这类信息
    // 因为我们会有多个文件,所以需要数组去存储
    // 现在我们就一个文件mainAsset
    const allAsset = [mainAsset];
     // 现在我们遍历allAssets,我们在遍历的过程中会一直往allAssets中推东西,一直遍历到结束,所以用了一个数组
    for (let asset of allAsset) {
        // 拿到当前这个文件asset.filename所在的目录名
        // 拿到目录名才能拼出他的结对路径
        const dirname = path.dirname(asset.filename);

        // 当我们把相对路径转化为绝对路径后,我们需要一个map,记录dependencies中的相对路径 和 childAsset的对应关系。
        // 因为我们后面要做依赖的引入,需要这样的一个对应关系。
        asset.mapping = {}

        // 遍历当前文件的依赖
        asset.dependencies.forEach(relativePath => {
            // 获取当前文件(entry.js)依赖(message.js)的绝对路径
            const absolutePath = path.join(dirname, relativePath);
            // 之前的这些我们都是拿的当前文件(例如entry.js)的依赖,获取到的是当前文件的依赖的绝对路径
            // 那当前文件的依赖的相关信息(即entry.js的依赖的依赖信息)
            // 即A依赖B,通过上面的一系列方法可以获取到B是谁,B的绝对路径是什么了,那B的依赖文件C我们怎么获取,怎么知道C的绝对路径是什么呢?
            // 很简单,我们像之前entry.js一样,直接把B的绝对路径当做参数,传给createGraph
            // 即类似遍历递归!!!!!!
            const childAsset = createAsset(absolutePath); // 获取到依赖文件的相关依赖信息了

            // 用相对路径作为key
            asset.mapping[relativePath] = childAsset.id;
            // 把当前文件的依赖相对路径转化后的信息推到数组allAsset,这样allAsset就有新的信息继续遍历了
            allAsset.push(childAsset); // 这样做相当于是递归了,一遍遍的遍历
        })
    }
    return allAsset;
}

function bundle(graph) {
    // 首先我们确定我们最后输出的是一个字符串的形式
    // 所以最后的代码块一定是一个字符串,我们首先用一个空字符串声明这个变量
    let modules = '';

    // 然后咱们遍历graph,去获取所有的module,然后都拼接在一起,成为一个字符串
    // graph里面其实每一个item即module
    // 然后循环graph把module拼接,拼接怎么拼?
    // 就是需要一个id与各种参数对应的,加逗号是因为一直要往modules上拼接东西,所以要加逗号分割每次拼接的东西
    // modules其实就是一个对象

    graph.forEach(module => {
        modules += `${module.id}:[  // modules其实就是一个对象
            function(require, module, exports) {
                // 代码体就是我们刚才生成的code
                ${module.code}
                // 其实我们要遍历的就是code这个东西,我们因为需要require, module, exports,所以我们以一个函数的形式把code包裹
                // 然后在需要的时候可以调用这个函数
                
                // 其实此时除了require,module跟exports都可以取到了
                // module就是我们传进来的graph的item即module
                // exports就是module的属性,可以暂时理解为就是一个空对象
            },
            ${JSON.stringify(module.mapping)},
        ],`
    });
    // 实现 require方法
    const reslut = `
        (function(modules){
            function require(id) {
                // 我们可以通过传入的id,再根据自执行函数接受到的modules,可以获取一些东西
                // 取到什么东西呢?请看上面的graph遍历,使用modules[id],我们可以获取到一个数组
                // 这个数组元素1是一个function,元素2是一个mapping
                
                const [fn, mapping] = modules[id]; //通过id,就是获取对应的一个数组,获取到里面的元素function、mapping
                
                // 此时此步骤获取的function也就是定义的fn是谁?还记得上面modules拼接代码体内定义的function了吗,就是它
                // 它有三个参数,分别是require,module,exports
                
                // 现在咱们定义一个localRequire函数,也就是当前要传给后面代码体的一个require函数,咱们定义为localRequire
                
                function localRequire(relativePath) {
                    // 这个require是为了传给后面去引入各种自己的依赖的
                    // localRequire接受的参数为一个相对路径
                    
                    // 然后可以直接调用定义的require函数,是把现有的各种资源都能拿到
                    // require接受的是一个id,这个id我们怎么获取呢?还记得mapping吗?asset.mapping[relativePath] = childAsset.id
                    // 而此时我们通过modules已经获取到了mapping,故通过相对路径mapping[relativePath]获取通过createGraph方法存到mapping属性中的id
                    return require(mapping[relativePath]);
                }
                // 此时声明的localRequire函数可以继续调用require(即获取到mapping里的id,就可以继续调用require了)
                
                //接下来声明module,module是有一个exports属性的,所以可以直接定义为下面的代码体
                const module = { exports: {}};
                
                // 然后执行fn
                // fn还记得是什么样子吗?还记得上面modules拼接代码体内定义的function吗?它接受三个参数require, module, exports
                // 其中require就是localRequire(什么是依赖,所谓的依赖就是咱们前面定义的用id跟文件存储依赖文件的属性作一一对应的,所以必然是一个id)
                // 第二个参数module,上面声明过的
                // 第三个参数是exports,其实就是module.exports
                // 其实执行该fn也就是执行咱们在createAsset方法中通过babel工具获取的code
                
                fn(localRequire, module, module.exports);
                
                // 在commonjs规范要求中,加载(导入)某个模块,其实就是加载(导入)该模块的module.exports属性
                // 故require返回的就是module.exports
                
                return module.exports;
            }
            require(0); // 先require(0),因为咱们初始的,最开始的id是0,通过require(0),实现对入口文件的调用
        })({${modules}})
    `;
    return reslut;
}

const graph = createGraph('./source/entry.js');
const result = bundle(graph);
console.log(result);

执行代码,打印结果如下:

(function(modules) {
    function require(id) {
        // 我们可以通过传入的id,再根据自执行函数接受到的modules,可以获取一些东西
        // 取到什么东西呢?请看上面的graph遍历,使用modules[id],我们可以获取到一个数组
        // 这个数组元素1是一个function,元素2是一个mapping

        const [fn, mapping] = modules[id]; //通过id,就是获取对应的一个数组,获取到里面的元素function、mapping

        // 此时此步骤获取的function也就是定义的fn是谁?还记得上面modules拼接代码体内定义的function了吗,就是它
        // 它有三个参数,分别是require,module,exports

        // 现在咱们定义一个localRequire函数,也就是当前要传给后面代码体的一个require函数,咱们定义为localRequire

        function localRequire(relativePath) {
            // 这个require是为了传给后面去引入各种自己的依赖的
            // localRequire接受的参数为一个相对路径

            // 然后可以直接调用定义的require函数,是把现有的各种资源都能拿到
            // require接受的是一个id,这个id我们怎么获取呢?还记得mapping吗?asset.mapping[relativePath] = childAsset.id
            // 而此时我们通过modules已经获取到了mapping,故通过相对路径mapping[relativePath]获取通过createGraph方法存到mapping属性中的id
            return require(mapping[relativePath]);
        }
        // 此时声明的localRequire函数可以继续调用require(即获取到mapping里的id,就可以继续调用require了)

        //接下来声明module,module是有一个exports属性的,所以可以直接定义为下面的代码体
        const module = {
            exports: {}
        };

        // 然后执行fn
        // fn还记得是什么样子吗?还记得上面modules拼接代码体内定义的function吗?它接受三个参数require, module, exports
        // 其中require就是localRequire(什么是依赖,所谓的依赖就是咱们前面定义的用id跟文件存储依赖文件的属性作一一对应的,所以必然是一个id)
        // 第二个参数module,上面声明过的
        // 第三个参数是exports,其实就是module.exports
        // 其实执行该fn也就是执行咱们在createAsset方法中通过babel工具获取的code

        fn(localRequire, module, module.exports);

        // 在commonjs规范要求中,加载(导入)某个模块,其实就是加载(导入)该模块的module.exports属性
        // 故require返回的就是module.exports

        return module.exports;
    }
    require(0); // 先require(0),因为咱们初始的,最开始的id是0,通过require(0),实现对入口文件的调用
})({
    0: [ // modules其实就是一个对象
        function(require, module, exports) {
            // 代码体就是我们刚才生成的code
            "use strict";

            var _message = require("./message.js");

            var _message2 = _interopRequireDefault(_message);

            function _interopRequireDefault(obj) {
                return obj && obj.__esModule ? obj : {
                    default: obj
                };
            }

            console.log(_message2.default);
            /*
             * @Author: JUEDIZHE
             * @Date: 2021-12-05 16:01:36
             * @LastEditors: OBKoro1
             * @LastEditTime: 2021-12-06 19:37:36
             * @Description:
             */
            // 其实我们要遍历的就是code这个东西,我们因为需要require, module, exports,所以我们以一个函数的形式把code包裹
            // 然后在需要的时候可以调用这个函数

            // 其实此时除了require,module跟exports都可以取到了
            // module就是我们传进来的graph的item即module
            // exports就是module的属性,可以暂时理解为就是一个空对象
        },
        {
            "./message.js": 1
        },
    ],
    1: [ // modules其实就是一个对象
        function(require, module, exports) {
            // 代码体就是我们刚才生成的code
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });

            var _name = require("./name.js");

            var _name2 = _interopRequireDefault(_name);

            function _interopRequireDefault(obj) {
                return obj && obj.__esModule ? obj : {
                    default: obj
                };
            }

            exports.default = _name2.default+" is important!!!";
            /*
             * @Author: JUEDIZHE
             * @Date: 2021-12-05 15:58:36
             * @LastEditors: OBKoro1
             * @LastEditTime: 2021-12-05 15:58:36
             * @Description:
             */
            // 其实我们要遍历的就是code这个东西,我们因为需要require, module, exports,所以我们以一个函数的形式把code包裹
            // 然后在需要的时候可以调用这个函数

            // 其实此时除了require,module跟exports都可以取到了
            // module就是我们传进来的graph的item即module
            // exports就是module的属性,可以暂时理解为就是一个空对象
        },
        {
            "./name.js": 2
        },
    ],
    2: [ // modules其实就是一个对象
        function(require, module, exports) {
            // 代码体就是我们刚才生成的code
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            /*
             * @Author: JUEDIZHE
             * @Date: 2021-12-05 15:52:33
             * @LastEditors: OBKoro1
             * @LastEditTime: 2021-12-05 15:52:38
             * @Description:
             */
            var name = 'test';
            exports.default = name;
            // 其实我们要遍历的就是code这个东西,我们因为需要require, module, exports,所以我们以一个函数的形式把code包裹
            // 然后在需要的时候可以调用这个函数

            // 其实此时除了require,module跟exports都可以取到了
            // module就是我们传进来的graph的item即module
            // exports就是module的属性,可以暂时理解为就是一个空对象
        },
        {},
    ],
})

这个结果就是编译后的结果了,我们拿到浏览器执行,可以直接获取到我们在入口文件entry.js打印的内容。这个打印的内容是从name.js获取的,证明我们的require、module、exports都实现了。在浏览器执行的打印结果如下:

1639994660(1).jpg 整个webpack的核心思路在这里就完成了。

3. 附源码

已上传到github