day04: 手写简单的webpack
开发思路:
- 创建入口文件
- 提取该入口文件的所有依赖
- 解析入口文件依赖的依赖,递归解析文件的依赖形成关系图,描述所有文件的依赖关系
- 把所有文件打包成一个文件
废话不多说,从零开始!
开发准备
- 创建文件夹
mini-webpack
- 在
mini-webpack
下运行yarn init -y
- 新建README.MD
- 创建几个js文件
index.js
,作为入口文件
import add from '../util/add.js'
import minus from './minus.js'
import common from '../util/common.js'
add(1, 4)
minus(6, 8)
common('测试')
minus.js
import common from '../util/common.js'
export default (a, b) => {
common(a - b)
}
add.js
import common from './common.js'
export default (a, b) => {
common(a + b)
}
common.js
export default (res) => {
console.log(`the result is ${res}`)
}
index.js
和 minus.js
放在src
下, add.js
和common.js
放在util
文件夹下。
创建webpack.js
作为编写打包核心代码的文件
完成结果:
开始开发
做好以上准备,我们就可以开始开发了。
按照思路,要先获取入口文件的依赖
1. 先获取文件内容,看是否正确
// webpack.js
const fs = require('fs')
const content = fs.readFileSync('./src/index.js', 'utf-8') // 获取文件内容
console.log(content)
文件内容
拿到入口文件的内容了,那么怎么去得到它的依赖呢?
打开网站https://astexplorer.net/#/2uBU1BLuJ1,将index.js
的内容粘贴到上面,将入口文件的内容转为AST树
,通过观察可以发现:
ImportDeclaration
类型的节点的source
的value
属性包含文件的依赖,所以,下一步要将内容转为AST树
,这一步需要借助工具@babel/parser
2. 将内容转为AST树
首先安装@babel/parser
yarn add -D @babel/parser
const fs = require('fs')
const parser = require('@babel/parser')
const content = fs.readFileSync('./src/index.js', 'utf-8') // 获取文件内容
const ast = parser.parse(content, {
sourceType: 'module', // 识别ES Module
})
console.log(ast)
运行结果:
image.png
3. 遍历AST树获取依赖
成功转换,下一步当然是要获取ImportDeclaration
类型的节点,即需要遍历AST树
,需要借助@babel/traverse
安装:
yarn add @babel/traverse -D
获取依赖
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const content = fs.readFileSync('./src/index.js', 'utf-8') // 获取文件内容
const ast = parser.parse(content, {
sourceType: 'module', // 识别ES Module
})
// 存储依赖
const dependencies = []
// 为了获取文件的依赖, 需要能够遍历ast,拿到ImportDeclaration节点, 于是引入@babel/traverse
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value)
},
})
console.log(dependecies )
运行结果:
image.png
成功获取入口文件的依赖
4. 获取依赖图
上面我们已经能够获取一个文件的依赖的,现在我们要获取依赖的依赖,递归解析形成依赖图
将上面获取一个文件依赖的内容封装成一个方法,且为了区分不同的依赖,添加全局变量ID,如下
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
let ID = 0
// 获取一个文件的依赖
function creatAsset(filePath) {
// 读取文件的内容
const content = fs.readFileSync(filePath, 'utf-8')
// 为了文件的依赖,借助babylon将内容转为AST, 参考:https://astexplorer.net/#/2uBU1BLuJ1
const ast = parser.parse(content, {
sourceType: 'module', // 识别ES Module
})
// 存储依赖
const dependencies = []
// 为了获取文件的依赖, 需要能够遍历ast,拿到ImportDeclaration节点, 于是引入@babel/traverse
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value)
},
})
return {
id: ID++,
filePath,
dependencies
}
}
于是我们创建一个方法createGraph
,用来创建依赖图
function createGraph(entry) {
const mainAsset = creatAsset(entry)
let graph = [mainAsset]
for (let asset of graph) {
const dir = path.dirname(asset.filePath)
asset.mapping = {}
for (let relativePath of asset.dependencies) {
// 这里做路径转化,让被依赖的文件相对与当前文件filePath,而不是webpack.js
let childAsset = creatAsset(path.join(dir, relativePath))
// 由于数组是动态的,这一步可以让数组遍历新推进的元素childAsset
graph.push(childAsset)
asset.mapping[relativePath] = childAsset.id
}
}
return graph
}
const graph = createGraph('./src/index.js')
console.log(graph)
对于以上方法,首先传入入口文件entry, 获得mainAsset ,包含入口文件的id, filepath,和dependencies, 将mainAsset作为graph的第一个节点,遍历这个图,获得节点的依赖(即文件路径数组),通过creatAsset递归地获取依赖的依赖,由于存在路径可能是相对路径,所以需要将路径转化成绝对路径,从而正确加载模块。mapping记录相对路径和资源id的映射关系,最后执行打印一下结果
image.png已经获得所有的依赖关系,但是仅仅是文件路径,所以我们还要加入编译内容
5. 编译文件内容
这一步需要借助@babel/core
和@babel/preset-env
安装
yarn add -D @babel/core @babel/preset-env
在creatAsset
里加入代码编译
// 获取一个文件的依赖
function creatAsset(filePath) {
// 读取文件的内容
const content = fs.readFileSync(filePath, 'utf-8')
// 为了文件的依赖,借助babylon将内容转为AST, 参考:https://astexplorer.net/#/2uBU1BLuJ1
const ast = parser.parse(content, {
sourceType: 'module', // 识别ES Module
})
// 存储依赖
const dependencies = []
// 为了获取文件的依赖, 需要能够遍历ast,拿到ImportDeclaration节点, 于是引入@babel/traverse
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value)
},
})
// 编译@babel/core,参考https://babeljs.io/repl#?browsers=&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=JYWwDg9gTgLgBAQwB7AgZzgMyhEcDkyqa-A3AFDkCmSkscAJlZggK4A28R6pQA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env&prettier=true&targets=&version=7.14.4&externalPlugins=
// @babel/preset-env 编译成什么格式
// 注意babel应该配套使用,不然会因为版本不同而报错
// 将ast编译成预设格式的js代码
const { code } = babel.transformFromAstSync(ast, null, {
presets:["@babel/preset-env"]
})
return {
id: ID++,
filePath,
dependencies,
code
}
}
再次打印
image.png
拿到编译后的代码,开始准备打包
把所有文件打包成一个文件
编译后的代码里包含require,module, exports, 所以要将编译后的模块代码分别用function(require, module, exports) {}
包裹起来,并且自行实现这3个属性,传入函数
// 打包
function bundle (graph) {
let modules = ''
graph.forEach(module => {
modules += `${module.id}: [function (require, module, exports) {
${module.code}
}, ${JSON.stringify(module.mapping)}],`
})
return `(function(modules) {
function require(id) {
const [fn, mapping] = modules[id]
const module = {exports: {} }
function localRequie(relativePath) { // 由于在文件内,使用import通过文件名称引入,但是我们自定义的require使用的是id,所以使用模块的mapping做一个转换
return require(mapping[relativePath])
}
fn(localRequie, module, module.exports)
return module.exports
}
// 调用入口
require(0)
})({${modules}})`
}
在package.json里添加命令:
"scripts": {
"build": "node webpack.js > dist.js"
}
测试: yarn build
将dist.js里面的内容粘贴到浏览器执行,
至此,一个简单的打包工具就完成了。
源码地址: https://github.com/fanqingyun/mini-webpack