webpack4.0之简易webpack实现

完整代码

一个简单的webpack结果模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(function(modules) {
var installedModules = {};

function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
});
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);

module.l = true;

return module.exports;
}
__webpack_require__.d = function(exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
__webpack_require__.r = function(exports) {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true });
};
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
return __webpack_require__((__webpack_require__.s = "<%- entryId%>"));
})({
<% for (let moduleName in modules) {%>
"<%- moduleName%>": function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval(
`<%-modules[moduleName]%>`
);
},
<% }%>

});

简单来说, webpack主流程上就作了这么几件事情

  1. 编译一个入口文件(代码文件)
  2. 解析并改造代码如将 importrequire转换成 __webpack_require__
  3. 收集依赖的模块并重复2
  4. 生成文件并导出上面的模版。

构建代码骨架

先搭建一个基本的类骨架,先不考虑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Compiler{
constructor(config) {
// 读取webpack.config.js并初始化
}
buildModule(modulePath, isEntry) {
// 构建模块
}
emitFile() {
// 渲染模版并生成文件。
}
getSource(path) {
//读取本地文件内容
}
parse(source, parentPath) {
// 将源代码解析成 ast语法树并处理
}
run() {
// 执行并创建模块的依赖关系
}
}

module.exports = Compiler

初始化

1
2
3
4
5
6
7
8
9
10
constructor(config) {
this.config = config
// 保存入口文件
this.entryId ;
// 保存所有的模块依赖
this.modules = {}
// 工作路径
this.root = process.cwd()
this.entry = config.entry
}

构建模块

构建模块的基本思路

  1. 读取代码文件
  2. 构建模块id:./path/[name].[ext]
  3. 解析代码、修改代码、收集依赖。

    1.首先借助工具babylon将代码解析成ast语法树,方便处理

    1. 使用工具@babel/traverse遍历ast节点
    2. 检测require,取出require函数的第一个参数,让它做为当前模块的依赖
    3. require 函数名修改成__webpack_require__,将参数修改成webpack支持的./path/[name].[js]
    4. 将修改后的ast还原成代码
    5. 将代码和依赖列表返回

4.递归依赖列表,重复以上步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
let babylon = require('babylon')
let traverse = require('@babel/traverse').default
let babelTyeps = require('@babel/types')
let generator = require('@babel/generator').default

getSource(path) {
let content = fs.readFileSync(path, 'utf8')
return content
}
parse(source, parentPath) {// Ast 语法解析
// 将源代码解析成 ast语法树
let deps = []
let ast = babylon.parse(source)
// 遍历ast节点
traverse(ast, {
CallExpression(p) {
let node = p.node
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
// 构建新的模块路径(模块名)

let moduleName = node.arguments[0].value
// 这里作了简化处理,可能引用的还有其他模块 。
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js
moduleName = './' + path.join(parentPath, moduleName) // ./src/a.js
deps.push(moduleName)
// 替换node arguments 的值
node.arguments = [babelTyeps.stringLiteral(moduleName)]

}
}
})
let sourceCode = generator(ast).code
return { sourceCode, deps}
}

buildModule(modulePath, isEntry) {
// 读取模块内容
let source = this.getSource(modulePath)
// 构建模块Id
let moduleName = './' + path.relative(this.root, modulePath)

if (isEntry) {
this.entryId = moduleName
}
// 对源代码进行改造,并返回依赖列表。
let {sourceCode, deps} = this.parse(source, path.dirname(moduleName))

this.modules[moduleName] = sourceCode
// 递归加载依赖模块。
deps.forEach((dep) => {
this.buildModule(path.join(this.root, dep), false)
})

}

导出文件

基本思路,使用 ejs模版,将模块信息填充进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
emitFile() {
// 渲染模版并生成文件。
let { output } = this.config
let main = path.join(output.path, output.filename)
let tpl = this.getSource('./src/tpl.ejs')


let code = ejs.render(tpl, {
entryId: this.entryId,
modules: this.modules,
})
this.assets = {}
this.assets[main] = code;
fs.writeFileSync(main, code)
}

爆露一个执行方法

1
2
3
4
5
6
run() {
// 执行并创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true)
// 发射 打包后的文件。
this.emitFile()
}

扩展loader

以上就完成了主流程的代码,下面来扩张webpackloader
首先,loader所作的事情就是在加载文件时匹配hash,如果正则匹配则调用loader去处理它并返回处理后的结果。

那么,先改造一下 getSource

1
2
3
4
5
getSource(path) {
let content = fs.readFileSync(path, 'utf8')
// return content
return this.progressLoaders(path, content)
}

再来实现 this.progressLoaders方法。
loader有几个注意事项:

  1. loader 是一个数组并从下往上执行
  2. 每一个loader的执行结果都会返回给下一个待执行的loader
  3. loader是一个可执行函数。

那么思路就很清晰了,只要在正则匹配之后依次执行数组里的loader即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
progressLoaders(modulePath, content){
let rules = this.config.module.rules

rules.forEach((rule) => {
const { test:match, use} = rule
let len = use.length - 1

function normalLoader() {
let p = use[len]
let moduleName = p.loader ? p.loader : p
let loader = require(moduleName)
content = loader(content)

}

if (match.test(modulePath)) {

do {
normalLoader()
len--

} while (len >= 0)

}
})

return content
}

扩展plugins

插件即钩子,插件的实现就很简单了。只要在constructor的时候添初始化钩子,然后在合适的地方调用即可。

  1. 改造 constructor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
constructor(config) {
this.config = config
// 保存入口文件
this.entryId ;
// 保存所有的模块依赖
this.modules = {}
// 工作路径
this.root = process.cwd()
this.entry = config.entry
this.initHooks()
this.progressPlugins()
}
initHooks() {
// 配置钩子
this.hooks = {
entryOption: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook()
}
}
progressPlugins() {
// 初始化用户配置插件
// 挂载钩子函数
let { plugins } = this.config
if (Array.isArray(plugins)) {

plugins.forEach((plugin) => {
plugin.apply(this)
})
this.hooks.afterPlugins.call(this)
}
}
  1. 在合适的地方调用钩子
1
2
3
4
5
6
7
8
9
10
11
run() {
// 执行并创建模块的依赖关系
this.hooks.run.call(this)
this.hooks.compile.call(this)
this.buildModule(path.resolve(this.root, this.entry), true)
this.hooks.afterCompile.call(this)
// 发射 打包后的文件。
this.emitFile()
this.hooks.emit.call(this)
this.hooks.done.call(this)
}