1. Webpack本质
本质上,webpack是一个现代js应用程序的静态模块打包器。
主旨:当Webpack处理应用程序的时候,它会递归的构建一个依赖关系图,这个依赖关系图里包含应用程序需要的每个模块,然后将所有这些模块打包成一个或者多个bundle输出。
2. Webpack打包的主要流程
- 初始化参数:shell webpack.config.js配置文件,读取各
配置参数
- 开始编译:初始化一个
compiler对象
,加载所有配置,开始进行编译 - 确定入口:根据
entry中的配置
,找到所有的入口文件
- 编译模块:从入口文件开始,
调用所有的loader
,再去递归的寻找依赖
- 完成模块编译:得到的每个模块被
编译后的最终内容
以及各模块之间的依赖关系
- 输出资源:根据得到的依赖关系,组装成一个个
包含多个module
的chunk
- 输出完成:根据配置,确定要
输出的文件名
以及文件路径
3. Webpac简单实现
例如,平常写的Esmodule
引入一个代码,导出一个代码
// 引入代码
import axios from 'axios'
// 导出代码
expert default axios
这种esmodule的语法在低级浏览器,例如ie8,是不支持的,所以我们需要把这种代码打包,最后输出的文件是低级浏览器可以读取执行的。
1. Webpack打包工具简单实现步骤
- 找到一个入口文件
- 解析这个入口文件,提取他的依赖
- 解析入口文件依赖的依赖,即递归的去创建一个文件间的依赖关系图,描述所有文件的依赖关系
- 把所有文件打包成一个文件
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
注意:关于export
与 export default
有以下几点需要注意:
- export与export default均可用于导出常量、函数、文件、模块等\
- 你可以在其它文件或模块中通过import+(常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用
- 在一个文件或模块中,export、import可以有多个,
export default仅有一个
- 通过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)内容
此时我们把入口文件的内容打印出来了。我们获取到入口文件的内容肯定还远远达不到需求,我们目的是为了获取入口文件的依赖,及它依赖的依赖的。接下来
6. 生成AST语法树,分析AST,思考如何能够解析出entry.js的依赖
AST语法树,我们都知道,这棵树定义了代码的结构,通过操纵这棵树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作。
在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里源代码特指编程语言的源代码。
普通代码转成抽象语法树的样子如下:
在这里分析AST语法树,我们用到一个工具astexplorer.net/, 通过该工具可以在线查看转换语法结构。我们就简单的把语法树设想为一个对象。
对于import引入这种语法的AST,我们只需要关注AST语法树的几个节点,几个属性,代码如下:
improt message from './message.js';
如下图:
注意:需要注意我箭头标注的那几个节点
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'); // 参数为相对路径
遍历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'); // 参数为相对路径
输出结果如下图:
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'); // 参数为相对路径
打印结果为:
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. 上面的过程,传的参数为相对路径,想办法把他们转成绝对路径
即相对路径
转化为绝对路径
。
怎么把相对路径转化为绝对路径呢?分为一下几步:
- 遍历存储多个文件(
文件指entry.js
这类文件)及他们的依赖信息的allAsset
(例如当前的entry.js文件及它的依赖信息) - 通过
node.js
自带的path.dirname()
方法获取到当前文件例如entry.js文件
所在的目录
dirname - 遍历当前文件(
例如entry.js
)的依赖信息(dependencies
),通过node.js方法path.join()
,把之前获取的entry.js文件的目录
dirname,与依赖的相对路径拼接,获取到依赖的绝对路径
absolutePath - 把依赖的绝对路径
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
,如下:
这个输出就是依赖图!!!
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的格式,右边会实时生成你打包好的格式。例如:
我们可以在右边看到他编译出的代码是什么样子的,以及他需要什么样的东西。
注意:我们看到他里面有一个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,结果如下:
首先我们看每一个文件的code编译跟之前我们使用babel工具编译的结果是一样的,都是带有
require
跟exports
。
(注意:entry.js的code编译没有exports
是因为entry.js没有导出)
接下来咱们要做的就是往code源代码传入它需要的东西:require
、exports
,还有module
(module
就是所有的东西都需要的模块)
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})
`
}
- 我们现在拼接的modules中新建一个
function
,参数为require
、module
、exports
function里
的代码体是什么呢?是我们刚才在上一步已经获取到的code。- 其实我们要遍历的就是code这个东西,我们因为需要
require
,module
,exports
,所以我们以一个函数的形式
把code包裹。 - 我们可以在需要的时候调用创建的这个函数。 代码如下:
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的属性
,可以暂时理解为就是一个空对象
。- 因为我们的
依赖
都是放在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
再次输出结果:
- 我们看到输出结果里有一个自执行函数,目前还没有接受参数
- 传参只写了每一个item里面做了什么东西
- 首先item里面是一个数组,每一个数组里第一个元素是声明的函数,这个函数保函
require
、module
、exports
及真正编译的代码
- 数组里还一个元素mapping,存每一个文件需要的依赖的,用这个依赖导入各种需要的东西,故把mapping放在了这里
18. 接下来在bundle方法
里的result变量
里实现require方法
此时bundle方法的代码体如下:
其中的result的代码部分如下:
// 实现 require方法
const reslut = `
(function(modules){
})(${modules})
`;
return reslut;
现在我们要做的是完善这个result
。步骤如下:
result
是一个自执行函数
,由于这个函数传的参数
为modules
,所以这个函数是可以拿到modules
的(也就是我们前面步骤获取的modules)。- 在自执行函数内我们
定义require
,require接受的参数为id
(因为咱们所有的映射关系都是通过id来存的) - 我们可以通过
传入的id
,再根据自执行函数接受到的modules
,可以获取一些东西 - 取到什么东西呢?请看上面的
graph遍历
,使用modules[id]
,我们可以获取到一个数组 - 这个数组
元素①
是一个function
,元素②
是mapping
- 然后声明变量接受这两个元素
const [fn, mapping] = modules[id]; //通过id,就是获取对应的一个数组,获取到里面的元素function、mapping
- 此时此步骤获取的function也就是定义的fn是谁?还记得上面modules拼接代码体内定义的function了吗,就是它
- 它有三个参数,分别是require,module,exports
在定义的require内:
- 现在咱们定义一个localRequire函数,也就是当前要传给后面代码体的一个require函数,咱们定义为localRequire
- 这个require是为了
传给后面
去引入各种自己的依赖的 - localRequire接受的参数为一个
相对路径
- 然后可以在localRequire内
直接调用定义的require函数
,是把现有的各种资源都能拿到 - require接受的是一个id,这个id我们怎么获取呢?还记得
mapping
吗?createGraph方法遍历asset.dependencies
其中有段代码asset.mapping[relativePath] = childAsset.id
,通过mapping[relativePath]
- 而此时我们
通过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。步骤如下:
- 执行fn
- fn还记得是什么样子吗?还记得上面modules拼接代码体内定义的function吗?它接受三个参数require, module, exports
- 其中require就是localRequire(什么是依赖,所谓的依赖就是咱们前面定义的用id跟文件存储依赖文件的属性作一一对应的,所以必然是一个id)
- 第二个参数module,上面声明过的
- 第三个参数是exports,其实就是module.exports
- 其实执行该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;
}
- 整个代码已经完成,主体代码体如下:
/*
* @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都实现了。在浏览器执行的打印结果如下:
整个webpack的核心思路在这里就完成了。
3. 附源码
已上传到github