明确 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)