在使用过Webpack后,它强大的的灵活性给我留下了深入印象,经过Plugin和Loader几乎能够随意扩展功能,因此决定探究Webpack的实现原理,学习做者的编程思想。node
可是在学习源码过程当中仍是遇到了挺大困难,一是它的插件系统设计的错综复杂,刚开始看容易被绕晕,另外是它功能实现覆盖的场景广,有不少内置功能不太熟悉。所以在这记录下学习的过程,将Webpack实现的精华内容提取出来,供后续学习参考。webpack
由于Webpack的代码量也不算少,并且比较绕,若是光看代码会比较枯燥。因此决定以本身实现一个简易Webpack为目标,分步探索实现细节,从构建运行到实现一个能打包代码的工具。为了简化逻辑,不会彻底像Webpack同样实现,如下部分是差别较大的地方:web
如下是完成计划,但愿能坚持 😄typescript
首先咱们须要了解一些基础的Webpack概念,Webpack的构建流程基本是围绕如下概念进行:shell
WebpackOptionsDefaulter
合并默认配置,在webpack里已经默认了部分配置,如context设置为当前目录等。Compiler
WebpackOptionsApply
将选项设置给compiler,并加载各类默认插件,如用于引导入口的EntryOptionPlugin
插件,加载js文件解析的JavascriptModulesPlugin
等NormalModuleFactory
和ContextModuleFactory
,模块工厂主要用于在后续建立和初始化模块Compilation
,在这里会经过钩子调用各类插件来初始化编译工具,如为入口模块添加解析器,为js类型文件添加解析器,添加模版处理方法等ChunkGroup
和Chunk
,根据模块依赖解析出ChunkGraphTemplate
根据Chunk建立输出内容本次咱们实现的效果是将两个简单文件打包成一个js,而且能够在浏览器运行,采用Commonjs模块化,咱们再实现一个简单的loader,将代码中的log转换为warn:编程
// example/index.js
const inc = require('./increment')
const dec = require('./decrement')
console.log(inc(8))
console.log(dec(8))
// example/increment.js
exports.default = function(val) {
return val + 1;
};
// example/decrement.js
exports.default = function(val) {
return val - 1;
};
// example/loader.js
module.exports = function loader(source) {
return source.replace(/console.log/g, 'console.warn')
}
复制代码
代码使用typescript编写,因此先安装typescript相关依赖json
# typescript
"typescript": "^3.7.4"
# 帮助识别node相关的类型定义
"@types/node": "^13.1.4",
# 快速编译运行ts项目
"ts-node": "^8.5.4",
复制代码
在package.json添加运行脚本浏览器
"start": "npx ts-node index.js",
复制代码
入口文件就是咱们运行Webpack的地方,这里咱们定义一些简单的配置,包括编译入口文件entry
,输出文件bundle.js
,还有自定义loader。引入咱们的核心编译器Compiler
,传入配置运行。bash
// index.js
const path = require('path')
const Compiler = require('./lib/Compiler').default
const options = {
entry: path.resolve(__dirname, './example/index.js'),
output: path.resolve(__dirname, './dist/bundle.js'),
loader: path.resolve(__dirname, './example/loader.js')
}
const compiler = new Compiler(options)
compiler.run()
复制代码
编译器负责封装打包过程,输入是用户配置,输出是打包结果,对外提供一个run
函数启动编译。
入口模块是编译器解析的起点,从入口文件开始递归加载模块文件,这里咱们没有递归解析只简单地解析了入口文件的依赖,收集到全部依赖后渲染出合并后的代码,最后写出到文件。模块化
// lib/Compiler.ts
import * as fs from 'fs'
import * as path from 'path'
import Module from './Module'
export default class Compiler {
options: any
constructor(options: any) {
this.options = options
}
run() {
// 建立入口模块
const name = path.basename(this.options.entry)
const entryModule = this.createModule(name, this.options.entry)
// 解析依赖模块
const dependencies = this.parse(entryModule.source)
this.addModuleDependencies(entryModule, dependencies)
// 渲染出结果
const source = this.renderTemplate(entryModule)
// 写入文件
this.write(source, this.options.output)
}
// ...
}
复制代码
Webpack中将一切资源都当作模块,因此咱们要解析的一个个js文件也是用模块表示,首先先定义一个Module
类来表示模块:
// lib/Module.ts
export default class Module {
id: string // 模块惟一标志,这里咱们用文件名表示
source: string // 文件源码
absPath: string // 文件绝对路径
dependencies: Module[] // 文件全部依赖
}
复制代码
有了模块类咱们就能够封装建立模块功能了,除了初始化数据外,咱们还在这里将文件读取出来,而后使用loader对源码进行处理。
// Compiler.createModule
createModule(id: string, absPath: string) {
const module = new Module()
module.id = id
module.absPath = absPath
module.source = fs.readFileSync(absPath).toString()
module.dependencies = []
const loader = require(this.options.loader)
module.source = loader(module.source)
return module
}
复制代码
webpack的基本功能就是将模块化代码打包成浏览器可运行代码。因为浏览器不能直接识别模块化代码,就须要咱们将多个文件按依赖顺序合并成一个文件,因此识别出模块依赖是咱们要解决的第一个问题。
咱们使用CommonJS来组织代码,就要在代码中识别出require
这样的关键字,因此这里咱们简单地使用正则匹配,通过循环匹配后,就能取出包含require('xxx')
中的依赖项了。
用正则匹配还须要考虑注释换行等麻烦的校验。Webpack则是将代码解析成AST树来分析依赖,AST里包含了更丰富的信息且不容易出错。
// Compiler.parse
parse(source: string) {
const dependencies: any[] = []
let result = []
let reg = /require[('"].([^']*)[)'"]./g
while((result = reg.exec(source))) {
dependencies.push({
id: result[1]
})
}
return dependencies
}
复制代码
在这里咱们已经获取到了父模块和他的全部依赖项,此时咱们就要将依赖也转成一个个模块,由于一个依赖也是一个文件,一个文件在webpack中就是一个模块。
// Compiler.addModuleDependencies
addModuleDependencies(module: Module, dependencies: any[]) {
const dir = path.dirname(module.absPath)
for (const dependent of dependencies) {
const depModule = this.createModule(dependent.id, path.resolve(dir, dependent.id) + '.js')
module.dependencies.push(depModule)
}
return
}
复制代码
上面说了,要想将模块化代码转换成在浏览器环境下执行的代码,咱们应该将全部将要执行的代码合并在一块儿,用一个js文件给浏览器执行,并且浏览器不识别的CommonJS语法也须要咱们给打上补丁,让浏览器能正确识别require
和exports
,因此咱们的目标代码应该长这样:
(function (modules) {
function require(moduleId) {
var module = {
id: moduleId,
exports: {}
}
modules[moduleId](module, require)
return module.exports;
}
require("index.js");
})({
'index.js': (function (module, require) {
const inc = require('./increment')
const dec = require('./decrement')
console.warn(inc(8))
console.warn(dec(8))
}),
'./increment': (function (module, require) {
module.exports = function (val) {
return val + 1;
};
}),
'./decrement': (function (module, require) {
module.exports = function (val) {
return val - 1;
};
}),
})
复制代码
当即执行函数传入合并后的全部代码,并建立了require
函数来加载合并后的对象,在咱们的代码中遇到了require
就会带入相应的函数,只要初始化后调用一次入口模块代码就能执行了。能够看到除了传参的代码,其余都是固定的模版代码,参数代码咱们能够用前面解析的依赖来建立。
// Compiler.renderTemplate
renderTemplate(module: Module) {
const buffer = []
buffer.push(`(function(modules) { function require(moduleId) { var module = { id: moduleId, exports: {} } modules[moduleId](module, require) return module.exports; } require("${module.id}"); })({`)
buffer.push(`'${module.id}': (function(module, require) { \n ${module.source} \n }),`)
for (const dependent of module.dependencies) {
const src = `(function(module, require) { \n ${dependent.source.replace('exports.default', 'module.exports')} \n })`
buffer.push(`'${dependent.id}':${src},`)
}
buffer.push(`})`)
return buffer.reduce((pre, cur) => pre + cur, '')
}
复制代码
输出了模版代码后,只要调用系统方法将其输出到硬盘就能够了,很是简单
// Compiler.write
write(source: string, output: string) {
fs.writeFileSync(output, source)
}
复制代码