【敲黑板】手把手带你写一个简易版webpack!内附超详细分解
明确webpack实现的功能
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。 当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
我们的代码主要实现以下四步
graph TD
A(1.找到入口文件) --> B(2.解析入口文件并提取出依赖)
B --> C{3.递归创造出依赖图}
C --> D(4.把所有文件打包成一个文件)
开始开发
1.目录下新建三个js源文件
- entry.js
import message from './message.js'
console.log(message)
- message.js
import name from "./name.js"
export default `${name} is a girl`
- name.js
export const name = 'Yolanda'
梳理下依赖关系,我们的入口文件是entry.js,entry依赖message,message依赖name。
2.新建一个mywebpack.js文件,首先读取一下entry入口文件中的内容。读取文件需要用到node的一个基础api-fs(文件系统),fs.readFileSync可以同步拿到js文件中的代码内容。
- mywebpack.js
const fs = require('fs')
function creatAsset (filename){
const content = fs.readFileSync(filename,'utf-8')
console.log(content)
}
creatAsset('./source/entry.js')
在当前目录下运行一下命令, 看一下输出node mywebpack.js
输出内容为entry.js中的代码:
import message from './message.js';
console.log(message);
3. 分析ast,思考如何解析出entry.js文件中的依赖可以使用ast工具 https://astexplorer.net/看一下entry.js文件的ast是什么?
3.1 可以看到最上级是一个File, File中包含换一个program, 就是我们的程序3.2 在program的body属性里, 就是我们各种语法的描述3.3 可以看到第一个就是 ImportDeclaration, 也就是引入的声明. 3.4 ImportDeclaration里有一个source属性, 它的value就是引入的文件地址 './message.js'
4.生成entry.js的ast首先安装一个Babylon(基于Babel的js解析工具)npm i babylon
- mywebpack.js
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');
可以看到输出了一个Object, 这就是咱们entry.js的AST.
5.基于这个ast,找到entry.js的ImportDeclaration中的source.value首先,需要遍历出ImportDeclaration节点,那就需要一个工具:babel-traverse(可以像遍历对象一样遍历ast中的节点)npm i babel-traverse
然后利用它遍历并获取到对应节点,提供一个函数来操作此节点。
- mywebpack.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'
});
traverse(ast, {
ImportDeclaration: ({
node
}) => {
console.log(node)
}
})
}
createAsset('./source/entry.js');
6.获取entry.js的依赖可能会出现多个依赖,这里需要建一个数组存储。
- mywebpack.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'
});
const dependencies = [];
traverse(ast, {
ImportDeclaration: ({
node
}) => {
dependencies.push(node.source.value);
}
})
console.log(dependencies);
}
createAsset('./source/entry.js');
可以自行打印一下dependencies数组看看.这里输出的是一个包括ImportDeclaration.source.value的数组。
7.优化creatAsset函数,增加id用于区分文件
- mywebpack.js
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
let ID = 0;
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const ast = babylon.parse(content, {
sourceType: 'module'
});
const dependencies = [];
traverse(ast, {
ImportDeclaration: ({
node
}) => {
dependencies.push(node.source.value);
}
})
const id = ID++;
return {
id,
filename,
dependencies
}
}
const mainAsset = createAsset('./source/entry.js');
console.log(mainAsset);
运行一下看看, 是不是返回了正确的结果.
8.现在我们有了单个文件的依赖了,接下来尝试建立模块间的依赖关系新建一个createGraph函数,在这个函数里引用createAsset。同时entry这个参数是动态的,所以createGraph接收entry这个参数。
- mywebpack.js - createGraph
function createGraph(entry) {
const mainAsset = createAsset(entry);
}
const graph = createGraph('./source/entry.js');
console.log(graph);
声明一个数组:allAsset用于存储全部的asset
- mywebpack.js - createGraph
function createGraph(entry) {
const mainAsset = createAsset(entry);
}
const graph = createGraph('./source/entry.js');
console.log(graph);
9.把相对路径转换为绝对路径我们在dependencies中存储的都是相对路径,但是我们需要绝对路径才能拿到模块的Asset,这个时候要想办法拿到每个模块的绝对路径。
这里用到了node的另一个基础API:path,其中用到了它的两个方法:1.path.dirname(获取当前文件的路径名)2.path.join(拼接路径)
- mywebpack.js - createGraph
function createGraph(entry) {
const mainAsset = createAsset(entry);
const allAsset = [mainAsset];
for (let asset of allAsset) {
const dirname = path.dirname(asset.filename);
asset.dependencies.forEach(relativePath => {
const absoultePath = path.join(dirname, relativePath);
const childAsset = createAsset(absoultePath);
});
}
}
10.我们还需要一个map,用于记录dependencies和childAsset之间的关系map可以存储模块间的依赖关系,方便后续的查找。
- mywebpack.js - createGraph
function createGraph(entry) {
const mainAsset = createAsset(entry);
const allAsset = [mainAsset];
for (let asset of allAsset) {
const dirname = path.dirname(asset.filename);
asset.mapping = {};
asset.dependencies.forEach(relativePath => {
const absoultePath = path.join(dirname, relativePath);
const childAsset = createAsset(absoultePath);
asset.mapping[relativePath] = childAsset.id;
});
}
}
11.遍历所有文件,形成最终的依赖图🌟🌟🌟🌟🌟核心的一步,就一行代码,完成所有模块的遍历。
- mywebpack.js - createGraph
function createGraph(entry) {
const mainAsset = createAsset(entry);
const allAsset = [mainAsset];
for (let asset of allAsset) {
const dirname = path.dirname(asset.filename);
asset.mapping = {};
asset.dependencies.forEach(relativePath => {
const absoultePath = path.join(dirname, relativePath);
const childAsset = createAsset(absoultePath);
asset.mapping[relativePath] = childAsset.id;
//⬇️关键一步,向数组中推进子依赖
allAsset.push(childAsset);
});
}
return allAsset;
}
获取到了文件依赖图之后,我们需要把所有文件打包成一个文件然后输出。
12.新增一个方法bundle最后一个函数了!胜利在望~bundle实现的是通过graph来创造一个立即执行函数,立即执行函数的参数有全部模块的代码体。
- mywebpack.js - bundle
function bundle(graph) {
//自执行函数的参数
let modules = '';
//遍历所有模块,拼接modules字符串:每个id对应一个value,value里要包括该module的可执行代码
graph.forEach(module => {
modules += `${module.id}:[
],`;
})
//返回的结果
const result = `
(function() {
})({${modules}})
`;
}
到现在我们会发现,graph里只记录了每个模块对应的依赖关系,并没有包括可执行的代码体。所以下一步,我们需要获取每个模块的代码体。
修改createAsset函数,获取到代码体且编译。
首先咱们会用到babel-core,方便各个插件分析语法进行相应的处理。
安装他!npm i babel-core
还会用到 babel-preset-env,它可以根据开发者的配置,按需加载插件,可以作为预设来编译代码。
安装他! npm i babel-preset-env
- mywebpack.js - createAsset
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const ast = babylon.parse(content, {
sourceType: 'module'
});
const dependencies = [];
traverse(ast, {
ImportDeclaration: ({
node
}) => {
dependencies.push(node.source.value);
}
})
const id = ID++;
const {
code
} = babel.transformFromAst(ast, null, {
// 第三个参数, 告诉babel以什么方式编译我们的代码. 这里我们就用官方提供的preset-env, 编译es2015+的js代码.
// 当然还有其他的各种预设, 可以编译ts, react等等代码.
presets: ['env']
})
return {
id,
filename,
dependencies,
code
}
}
13.把获取到的code放到result中根据commonJs的规范,每个模块的代码函数需要接收三个参数:require,module,exports。
- module变量代表当前的模块,它是一个变量,其中exports属性就是对外的模块。加载某个模块,其实就是加载该模块的module.exports属性。
- mywebpack.js - bundle
function bundle(graph) {
let modules = '';
graph.forEach(module => {
modules += `${module.id}:[
function(require, module, exports) {
${module.code}
},
],`;
})
const result = `
(function() {
})({${modules}})
`;
return result;
}
接下来开始实现require函数,首先需要把mapping传到对应modules的value里面。
function bundle(graph) {
let modules = '';
graph.forEach(module => {
modules += `${module.id}:[
function(require, module, exports) {
${module.code}
},
${JSON.stringify(module.mapping)},
],`;
})
const result = `
(function() {
})({${modules}})
`;
return result;
}
requier要接收一个参数,来表示引入了哪些代码,这个时候就可以用mapping的映射关系,使用id找到引入的代码。
function bundle(graph) {
let modules = '';
graph.forEach(module => {
modules += `${module.id}:[
function(require, module, exports) {
${module.code}
},
${JSON.stringify(module.mapping)},
],`;
})
// 取出的fn是上面定义的modules里的function,mapping是${JSON.stringify(module.mapping)}
const result = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
const module = { exports: {}};
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;
return result;
}
当当当!到这里就基本完成了我们的简易版webpack,这个时候可以node运行下mywebpack.js,把输出的结果代码复制到浏览器里看看能不能正常运行。浏览器打印出:Yolanda is a girl 就意味着你的webpack已经正常运转起来了!给自己鼓个掌吧~
最后可以优化一下流程,每次都复制到浏览器看输出结果有点费劲,可以配置下package.json用命令行操作。npm init
- package.json
"scripts": {
"build": "rm -rf dist.js && node mywebpack.js > dist.js"
},
最后运行
npm run build
.
大功告成!
贴上mywebpack.js完整代码:
// 步骤:
// 1.找到一个入口文件;
// 2.解析这个入口文件,提起入口文件的依赖;
// 3.解析入口文件中依赖的依赖,递归的创造一个依赖图,可以描述文件间的依赖关系;
// 4.将所有的文件打包成一个文件。
//node里面的fs模块可以读取文件
const fs = require('fs')
//node的基础api 可以帮助拿到绝对路径
const path = require('path')
//babel(语法转换)的解析工具
const babylon = require('babylon')
//babel-traverse :可以像遍历对象一样遍历ast中的节点
const tranverse = require('babel-traverse').default;
//babel-core:把js文件解析为ast
const babel = require('babel-core')
//记录ID
let ID = 0;
//拿到该file的id,filename 以及 依赖的数组
function createAsset(filename){
//同步的读取文件
const content =fs.readFileSync(filename,'utf-8')
//转化为ast找到相应节点
const ast = babylon.parse(content,{
sourceType:'module'
})
//定义一个数组用于储存依赖
const dependencies = []
//定义一个id 每调用一次creatAsset函数id++
let id = ID++
//遍历拿到ImportDeclaration中的source中的value
tranverse(ast,{
ImportDeclaration:({node})=>{
//拿到node中的source的value push到储存依赖的数组里
dependencies.push(node.source.value)
}
})
const{ code } = babel.transformFromAst(ast , null,{
presets:['env']
})
//输出一个对象,其中包括id,filename以及它所依赖的dependencies
return {
id,
filename,
dependencies,
code
}
}
//创建依赖图 核心!!
function createGraph(entry){
const mainAsset = createAsset(entry)
const allAsset = [mainAsset]
for( let asset of allAsset){
//拿到当前的dirname
const dirname= path.dirname(asset.filename)
//给asset新增一个mapping对象,方便后续查找
asset.mapping = {}
//将dependencies中的相对路径转化为绝对路径
asset.dependencies.forEach(relativePath=>{
const absolutePath = path.join(dirname,relativePath)
//拿到绝对路径下的子元素的asset内容
const childAsset = createAsset(absolutePath)
//存储对应关系
asset.mapping[relativePath]=childAsset.id
//递归的核心一行代码!!🌟🌟🌟🌟🌟
allAsset.push(childAsset)
})
}
return allAsset
}
//创建整体结果代码块(需要立即执行)接收module作为参数
function bundle (graph){
let modules = ''
graph.forEach(module=>{
modules+=`
${module.id}:[
function(require,module,exports){
${module.code}
},
${JSON.stringify(module.mapping)}
],
`
})
//输出的结果为字符串
const result = `(
function(modules){
function require(id){
const [fn,mapping] = modules[id];
function localRequire(relativePath){
return require(mapping[relativePath])
}
const module = { exports:{}}
fn(localRequire,module,module.exports)
return module.exports
}
require(0)
}
)({${modules}})`
return result
}
//传入相对路径
const graph = createGraph('./source/entry.js')
const res = bundle(graph)
console.log(res)
转载自:https://segmentfault.com/a/1190000040235554