工作流程图
主要介绍工作原理,webpack基础不再赘述
流程大致为,转载自
初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数;
开始编译: 用上一步得到的参数初始化Complier对象,加载所有配置的插件,执行对象的run方法开始执行编译;
确定入口: 根据配置中的entry找出所有入口文件;
编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤知道所有入口依赖的文件都经过了本步骤的处理;
完成模块编译: 在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系;
输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
手动实现
分析入口文件
const fs = require('fs')
module.exports = class Webpack{
constructor(options) {
console.log(options);
const {entry, output} = options;
this.entry = entry
this.output = output
}
run() {
console.log('webpack run');
this.parse(this.entry)
}
parse(entryFile) {
// 分析入口文件
const content = fs.readFileSync(entryFile, 'utf-8')
console.log(content);
}
}
解析依赖关系
不推荐使用字符串截取,使用@babel/parser来解析内部语法,需要安装npm包
使用parser解析读取到的文件内容
parse(entryFile) {
// 分析入口文件
const content = fs.readFileSync(entryFile, 'utf-8')
console.log(content);
// 分析依赖及以来的路径
// 使用parser把内容抽象成语法树
const ast = parser.parse(content, {
sourceType: 'module'
})
console.log(ast);
}
运行结果如下
可以发现里面折叠了3个Node结点,打开来看一下
什么?看懂了!!!大神收下膝盖,不过相信很多人跟我一样第一眼看到时蒙逼的吧,不慌,再好好看看
打开index.js的时候你可能会发现点什么
3行代码,3个结点,别太早下结论,来验证一下
再加点东西
import {add} from './expo.js'
import {minus} from './expo.js'
add(1,2)
minux(3,2)
console.log('hello webpack');
然后再看解析结果(终端放不开,找了个工具盛一下)
这下稳了
ImportDeclaration
表示引入依赖,ExpressionStatement
表示表达式
然后再来根据分析结果,遍历引入的依赖,使用@babel/traverse,需要安装npm包
const dependencies = []
traverse(ast, {
ImportDeclaration({node}) { // 可以分析表达式
const finalName = path.join(path.dirname(entryFile), node.source.value) // 拼接路径
// console.log(finalName);
dependencies.push(finalName)
}
})
console.log(dependencies);
打印一下,可以看到已经获取到了引入的依赖
然后就是将ES6等语法转换为浏览器可以执行的代码,使用@babel/core和@babel/preset-env
工具
const {code} = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
console.log(code);
可以看到已经将es6的import转换成了require。到这里依赖的分析已经完成了,将解析到的结果作为一个对象返回出去
return {
entryFile,
dependencies,
code
}
然后我们拿到了这么一个东西
然后还要解析依赖中是否存在依赖。。。
run() {
const info = this.parse(this.entry)
// console.log(info);
// 处理其他模块,然后汇总
this.modules.push(info)
for(let i = 0; i < this.modules.length; i++) {
const item = this.modules[i]
const {dependencies} = item
if(dependencies) {
for(let j in dependencies) {
this.modules.push(this.parse(dependencies[j]))
}
}
}
console.log(this.modules);
}
如果你看过webpack打包的代码,你会发现,打包出来的代码都是以对象形式存在的,我们还要继续处理
生成文件
解析完成了就要把解析好的代码输出到文件中
file(code) {
// 生成文件
const filePath = path.join(this.output.path, this.output.filename)
const newCode = JSON.parse(code)
const bundle = `(function() {
})(${newCode})`
fs.writeFileSync(filePath, bundle, 'utf-8')
}
新建一个dist目录,然后运行,会看到已经自动生成了一个main.js
这时其实是有问题的,路径还是原来的引用路径,但是文件到了dist目录下,需要对路径进行处理
因为在对象中有绝对地址,所以直接读取出来
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}
然后就来到了webpack的核心require
,他模仿了Node中的require操作,是一个立即执行函数,所以文件能直接在浏览器运行
file(code) {
// 生成文件
const filePath = path.join(this.output.path, this.output.filename)
const newCode = JSON.stringify(code)
const bundle = `(function(graph) {
function require(module){
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function(require, exports, code) {
eval(code)
})(localRequire, exports, graph[module].code)
return exports
}
require('${this.entry}')
})(${newCode})`
fs.writeFileSync(filePath, bundle, 'utf-8')
}
代码
代码已上传码云