webpack 实现
这是一个如何实现一个简单的webpack生成bundle.js的demo, 主要的实现流程:
入口entry -> 递归解析AST获取依赖 -> 生成依赖图
-> 为每个模块包裹factory function -> 以入口脚本为起点,递归执行模块 -> 拼接IIFE(factory, require实现) -> 产出bundle
另外需要用到以下插件:
- @babel/parser 用于分析源代码,产出 AST;
- @babel/traverse 用于遍历 AST,找到 import 声明;
- @babel/core 用于编译,将源代码编译为 ES5;
- @babel/preset-env 搭配@babel/core使用;
- resolve 用于获取依赖的绝对路径。
全局配置
const fs = require("fs");
const path = require("path");
// 用于分析源代码,产出 AST
const parser = require("@babel/parser");
// 用于遍历 AST,找到 import 声明
const traverse = require("@babel/traverse").default;
// 用于编译,将源代码编译为 ES5
const babel = require("@babel/core");
// 用于获取依赖的绝对路径
const resolve = require("resolve").sync;
/**
* 维护一个全局 ID,并通过遍历 AST
* 访问ImportDeclaration节点,收集依赖到deps数组中,
* 同时完成 Babel 降级编译
*/
let ID = 0
const log = console.log.bind(console)
复制代码
生成依赖图
/**
* 实现了查找一个文件的所有依赖
* 返回文件的路径,文件的所有引用依赖,文件的esm代码, 模块对应 ID
*/
function createModuleInfo(filePath) {
const content = fs.readFileSync(filePath, 'utf8')
const ast = parser.parse(content, { sourceType: "module" })
const deps = []
traverse(ast, {
ImportDeclaration: ({ node }) => {
deps.push(node.source.value)
}
})
const id = ID++
// 编译成esm5
const { code } = babel.transformFromAstSync(ast, null, {
presets: ["@babel/preset-env"]
})
return {
id,
filePath,
deps,
code
}
}
// log(createModuleInfo(path.resolve(__dirname, './test/app.js')))
/**
* 对entry入口依赖进行遍历,对每个deps进行依赖信息生成
* 注意: 这里不支持循环引用,否则死递归
* 最后生成依赖图
*/
function createDependencyGraph(entry) {
const entryInfo = createModuleInfo(entry)
const graphArr = []
graphArr.push(entryInfo)
// 以入口模块为起点,遍历整个项目依赖的模块,并将每个模块信息维护到 graphArr 中
for (const module of graphArr) {
module.map = {}
module.deps.forEach(depPath => {
const baseDir = path.dirname(module.filePath)
const moduleDepPath = path.resolve(baseDir, depPath)
const moduleInfo = createModuleInfo(moduleDepPath)
graphArr.push(moduleInfo)
module.map[depPath] = moduleInfo.id
})
}
return graphArr
}
复制代码
对依赖图谱进行处理生成IIFE
/**
* 使用 IIFE 的方式,来保证模块变量不会影响到全局作用域
* 构造好的项目依赖树(Dependency Graph)数组,将会作为名为modules的行参,传递给 IIFE
* 通过require(map[requireDeclarationName])方式,按顺序递归调用各个依赖模块
* 通过调用factory(module.exports, localRequire)执行模块相关代码
* 该方法最终返回module.exports对象,module.exports 最初值为空对象({exports: {}})
* 但在一次次调用factory()函数后,module.exports对象内容已经包含了模块对外暴露的内容了
*/
function pack(graph) {
const moduleArgArr = graph.map(module => {
return `${module.id}: {
factory: (exports, require) => {
${module.code}
},
map: ${JSON.stringify(module.map)}
}`
})
const iifeBundler = `(function(modules){
const require = id => {
const {factory, map} = modules[id];
const localRequire = requireDeclarationName => require(map[requireDeclarationName]);
const module = {exports: {}};
factory(module.exports, localRequire);
return module.exports;
}
require(0)
})({${moduleArgArr.join()}})
`
return iifeBundler;
}
复制代码
webpack loader 和 plugin
下面再写一下简单的实现一个loader 和 plugin
代码地址如下
webpack hmr
基本实现原理大致这样的,构建 bundle 的时候,加入一段 HMR runtime 的 js 和一段和服务沟通的 js 。文件修改会触发 webpack 重新构建,服务器通过向浏览器发送更新消息,浏览器通过 jsonp 拉取更新的模块文件,jsonp 回调触发模块热替换逻辑。
服务端主要使用了: webpack, express, websocket
使用express启动本地服务,当浏览器访问资源时对此做响应
服务端和客户端使用websocket实现长连接webpack监听源文件的变化,即当开发者保存文件时触发webpack的重新编译
每次编译都会生成hash值、已改动模块的json文件、已改动模块代码的js文件
编译完成后通过socket向客户端推送当前编译的hash戳客户端的websocket监听到有文件改动推送过来的hash戳,会和上一次对比
一致则走缓存,不一致则通过ajax和jsonp向服务端获取最新资源使用内存文件系统去替换有修改的内容实现局部刷新
具体步骤如下:
启动webpack-dev-server服务器
创建webpack实例
创建Server服务器
添加webpack的done事件回调
编译完成向客户端发送消息
创建express应用app
设置文件系统为内存文件系统
添加webpack-dev-middleware中间件
中间件负责返回生成的文件(生成hash值、已改动模块的json文件、已改动模块代码的js文件)
启动webpack编译
创建http服务器并启动服务
使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
创建socket服务器
客户端具体步骤(客户端bundles.js内被webpack处理后加入了一段websocket客户端代码)
webpack-dev-server/client端会监听到此hash消息
客户端收到ok的消息后会执行reloadApp方法进行更新在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器
在webpack/hot/dev-server.js会监听webpackHotUpdate事件
在check方法里会调用module.hot.check方法
HotModuleReplacement.runtime请求Manifest
它通过调用 JsonpMainTemplate.runtime的hotDownloadManifest方法
调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码补丁JS取回来后会调用JsonpMainTemplate.runtime.js的webpackHotUpdate方法
然后会调用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法动态更新模块代码
然后调用hotApply方法进行热更新