一、 webpack编译流程
- Webpack CLI启动打包流程
- 载入Webpack核心模块,创建Compiler对象
- 使用Compiler对象开始编译整个项目
- 从入口文件开始,解析模块依赖,形成依赖关系树
- 递归依赖树,将每个模块交给对应的Loader处理
- 合并Loader处理完的结果,将打包结果输出到dist目录
二、 webpack代码实现
1. Babel的编译原理
1. 解析
- 接收代码并输出 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)。
- 词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。
- 语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。
Babel 使用 @babel/parser 解析代码,输入的 js 代码字符串根据 ESTree 规范生成 AST(抽象语法树)
code(字符串形式代码) -> tokens(令牌流) -> AST(抽象语法树)
2. 转换
- 接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。
- Babel提供了@babel/traverse(遍历)方法维护这AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。
3. 生成
- 把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。
- Babel使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。
2. bundle.js简易实现
- 依赖收集
- ES6转ES5
- 替换require与exports
(function(list) {
function require(file) {
var exports = {}
(function(exports, code) {
eval(code)
})(exports, list[file])
return exports
}
require("index.js")
})({
"index.js": `
var add = require('add.js'),default
console.log(add(1, 2))
`,
"add.js": `
exports.default = function(a, b) { return a + b }
`
})
3. 基于babel手写webpack
1. 安装依赖
yarn add @babel/core @babel/parser @babel/traverse @babel/preset-env
2. 代码实现
// webpack.ts
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse')
const babel = require('@babel/core')
/*
* 分析单独模块
*/
function getModuleInfo(file) {
// 读取文件
const body = fs.readFileSync(file, 'utf-8')
// 转换AST
// 代码字符串 => 对象 => 对象遍历解析
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
//visitor
ImportDeclaration({ node }) {
// 遇到import节点回调
const dirname = path.dirname(file)
const abspath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = abspath
}
})
// ES6转ES5
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return {
file,
deps,
code
}
}
/*
* 解析模块
*/
function parseModules(file) {
const entry = getModuleInfo(file)
const temp = [entry]
const depsGraph = {}
getDeps(temp, entry)
// 构建依赖树
temp.forEach(info => {
depsGraph[info.file] = {
deps: info.deps,
code: info.code
}
})
return depsGraph
}
/*
* 获取依赖
*/
function getDeps(temp, { deps }) {
Object.keys(deps).forEach(key => {
const child = getModuleInfo(deps[key])
temp.push(child)
getDeps(temp, child)
})
}
/*
* 生成bundle
*/
function bundle(file) {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {}
(function(require, exports, code) {
eval(code)
})(absRequire.exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
const content = parseModules('./src/index.js')
// console.log('content:', content)
!fs.existsSync("./dist") && fs.mkdirSync("./dist")
fs.writeFileSync("./dist/bundle.js", content)
4. webpack的devServer/proxy
webpack的proxy是基于node插件http-proxy-middleware,http-proxy-middleware为何能解决跨域问题:
- 首先,浏览器报跨域,并不是服务器没有返回,而是,服务器返回了,但是被浏览器拦截了。
- 其次,http-proxy-middleware解决跨域的方案,本地起了一个node代理服务器(
var httpProxy = require('http-proxy')),通过代理服务器去请求目标服务器,然后返回请求结果。由于浏览器请求的是本地路径,所以不会有跨域问题。