# Webpack 模块系统

本来盘点模块化的基础知识, 其中将之前源码调试的配置记录下来, 最后再实现一个简单的模块打包工具
# 基础理论
在模块化系统中, 会经过包装和加载两部分实现, 最终形成一个自执行函数
- 代码包装
- 模块加载
- 源码调试
# 代码包装
例如原模块的代码如下:
//mA.js
var aa = 1
function inc() {
aa++
}
module.exports = {
aa: aa,
inc: inc
}
//app.js
var mA = require('./mA.js')
console.log('mA.aa =' + mA.aa)
mA.inc()
经过包装过后的模块, module
被替换成 webpack
内部的一个新变量(或者缓存), export
变成该模块中的一个变量{}
var modules = {
'./mA.js': generated_mA,
'./app.js': generated_app
}
function generated_mA(module, exports, webpack_require) {
var aa = 1
function inc() {
aa++
}
module.exports = {
aa: aa,
inc: inc
}
}
function generated_app(module, exports, webpack_require) {
var mA_imported_module = webpack_require('./mA.js')
console.log('mA.aa =' + mA_imported_module['aa'])
mA_imported_module['inc']()
}
# 模块加载
;(function (modules) {
// 加载完毕的所有模块。
var installedModules = {}
function webpack_require(moduleId) {
// webpack的模块引入require实现
// 如果模块已经加载过了,直接从Cache中读取。
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
// 创建新模块并添加到installedModules。
var module = (installedModules[moduleId] = {
id: moduleId,
exports: {}
})
// 加载模块,即运行模块的生成代码,
modules[moduleId].call(
module.exports,
module,
module.exports,
webpack_require
)
return module.exports
}
})(modules)
模块加载示例:
// 从入口entry处改写
webpack_require('./app.js');
# 源码调试
调试Webpack
源码, 建议使用Vscode
, 调试之前做好基础准备:
- 创建基础代码
- 新增 Debug 配置
# 创建基础代码
关于第一部分, 作者有个有个 Demos 库, 里面包含了该调试基础代码 (opens new window)
# 新增 Debug 配置
{
"version": "0.2.0",
"configurations": [
{
"name": "Chrome",
"request": "launch",
"type": "chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}/projects/wds-html/src",
// "preLaunchTask": "debug", // 添加的配置,在执行之前需要启动项目,这个是启动项目用的任务。
"sourceMapPathOverrides": {
"webpack:///./*": "${webRoot}/*",
"webpack:///src/*": "${webRoot}/*",
"webpack:///./src/*": "${webRoot}/*",
"webpack:///*": "*",
"webpack:///./~/*": "${webRoot}/node_modules/*"
} // 添加的配置,为了找到打包文件和源代码之间的关联,使断点生效。
}
]
}
# 手写工具
- 将 ES6 转换为 ES5
- 支持在 JS 文件中
import
CSS 文件
通过这个工具的实现,加深对打包工具的原理的原理的理解
# 依赖安装
因为涉及到 ES6 转 ES5,所以我们首先需要安装一些 Babel 相关的工具
yarn add babylon babel-traverse babel-core babel-preset-env
接下来我们将这些工具引入文件中
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')
# 生成 AST
首先,我们先来实现如何使用 Babel 转换代码
function readCode(filePath) {
// 读取文件内容
const content = fs.readFileSync(filePath, 'utf-8')
// 生成 AST
const ast = babylon.parse(content, {
sourceType: 'module'
})
// 寻找当前文件的依赖关系
const dependencies = []
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value)
}
})
// 通过 AST 将代码转为 ES5
const { code } = transformFromAst(ast, null, {
presets: ['env']
})
return {
filePath,
dependencies,
code
}
}
- 首先我们传入一个文件路径参数,然后通过
fs
将文件中的内容读取出来 - 接下来我们通过
babylon
解析代码获取 AST,目的是为了分析代码中是否还引入了别的文件 - 通过
dependencies
来存储文件中的依赖,然后再将 AST 转换为 ES5 代码 - 最后函数返回了一个对象,对象中包含了当前文件路径、当前文件依赖和当前文件转换后的代码
# 收集依赖
接下来我们需要实现一个函数,这个函数的功能有以下几点
- 调用
readCode
函数,传入入口文件 - 分析入口文件的依赖
- 识别 JS 和 CSS 文件
function getDependencies(entry) {
// 读取入口文件
const entryObject = readCode(entry)
const dependencies = [entryObject]
// 遍历所有文件依赖关系
for (const asset of dependencies) {
// 获得文件目录
const dirname = path.dirname(asset.filePath)
// 遍历当前文件依赖关系
asset.dependencies.forEach((relativePath) => {
// 获得绝对路径
const absolutePath = path.join(dirname, relativePath)
// CSS 文件逻辑就是将代码插入到 `style` 标签中
if (/\.css$/.test(absolutePath)) {
const content = fs.readFileSync(absolutePath, 'utf-8')
const code = `
const style = document.createElement('style')
style.innerText = ${JSON.stringify(content).replace(/\\r\\n/g, '')}
document.head.appendChild(style)
`
dependencies.push({
filePath: absolutePath,
relativePath,
dependencies: [],
code
})
} else {
// JS 代码需要继续查找是否有依赖关系
const child = readCode(absolutePath)
child.relativePath = relativePath
dependencies.push(child)
}
})
}
return dependencies
}
- 首先我们读取入口文件,然后创建一个数组,该数组的目的是存储代码中涉及到的所有文件
- 接下来我们遍历这个数组,一开始这个数组中只有入口文件,在遍历的过程中,如果入口文件有依赖其他的文件,那么就会被
push
到这个数组中 - 在遍历的过程中,我们先获得该文件对应的目录,然后遍历当前文件的依赖关系
- 在遍历当前文件依赖关系的过程中,首先生成依赖文件的绝对路径,然后判断当前文件是 CSS 文件还是 JS 文件
- 如果是 CSS 文件的话,我们就不能用 Babel 去编译了,只需要读取 CSS 文件中的代码,然后创建一个
style
标签,将代码插入进标签并且放入head
中即可 - 如果是 JS 文件的话,我们还需要分析 JS 文件是否还有别的依赖关系
- 最后将读取文件后的对象
push
进数组中
- 如果是 CSS 文件的话,我们就不能用 Babel 去编译了,只需要读取 CSS 文件中的代码,然后创建一个
# 代码生成
现在我们已经获取到了所有的依赖文件,接下来就是实现打包的功能了
function bundle(dependencies, entry) {
let modules = ''
// 构建函数参数,生成的结构为
// { './entry.js': function(module, exports, require) { 代码 } }
dependencies.forEach((dep) => {
const filePath = dep.relativePath || entry
modules += `'${filePath}': (
function (module, exports, require) { ${dep.code} }
),`
})
// 构建 require 函数,目的是为了获取模块暴露出来的内容
const result = `
(function(modules) {
function require(id) {
const module = { exports : {} }
modules[id](module, module.exports, require)
return module.exports
}
require('${entry}')
})({${modules}})
`
// 当生成的内容写入到文件中
fs.writeFileSync('./bundle.js', result)
}
这段代码需要结合着 Babel 转换后的代码来看,这样大家就能理解为什么需要这样写了
// entry.js
var _a = require('./a.js')
var _a2 = _interopRequireDefault(_a)
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_a2.default)
// a.js
Object.defineProperty(exports, '__esModule', {
value: true
})
var a = 1
exports.default = a
Babel 将我们 ES6 的模块化代码转换为了 CommonJS(如果你不熟悉 CommonJS 的话,可以阅读这一章节中关于 模块化的知识点 (opens new window)) 的代码,但是浏览器是不支持 CommonJS 的,所以如果这段代码需要在浏览器环境下运行的话,我们需要自己实现 CommonJS 相关的代码,这就是 bundle
函数做的大部分事情。
接下来我们再来逐行解析 bundle
函数
- 首先遍历所有依赖文件,构建出一个函数参数对象
- 对象的属性就是当前文件的相对路径,属性值是一个函数,函数体是当前文件下的代码,函数接受三个参数
module
、exports
、require
module
参数对应 CommonJS 中的module
exports
参数对应 CommonJS 中的module.export
require
参数对应我们自己创建的require
函数
- 接下来就是构造一个使用参数的函数了,函数做的事情很简单,就是内部创建一个
require
函数,然后调用require(entry)
,也就是require('./entry.js')
,这样就会从函数参数中找到./entry.js
对应的函数并执行,最后将导出的内容通过module.export
的方式让外部获取到 - 最后再将打包出来的内容写入到单独的文件中
如果你对于上面的实现还有疑惑的话,可以阅读下打包后的部分简化代码
;(function (modules) {
function require(id) {
// 构造一个 CommonJS 导出代码
const module = { exports: {} }
// 去参数中获取文件对应的函数并执行
modules[id](module, module.exports, require)
return module.exports
}
require('./entry.js')
})({
'./entry.js': function (module, exports, require) {
// 这里继续通过构造的 require 去找到 a.js 文件对应的函数
var _a = require('./a.js')
console.log(_a2.default)
},
'./a.js': function (module, exports, require) {
var a = 1
// 将 require 函数中的变量 module 变成了这样的结构
// module.exports = 1
// 这样就能在外部取到导出的内容了
exports.default = a
}
// 省略
})
虽然实现这个工具只写了不到 100 行的代码,但是打包工具的核心原理就是这些了
- 找出入口文件所有的依赖关系
- 然后通过构建 CommonJS 代码来获取
exports
导出的内容
← Webpack 异步加载 代码风格工具集成 →