webpack 的运行时分析
基础实现
webpack 的 runtime,也就是 webpack 最后生成的代码,做了以下三件事:
__webpack_modules__: 维护一个所有模块的数组。将入口模块解析为AST,根据AST深度优先搜索所有的模块,并构建出这个模块数组。每个模块都由一个包裹函数 (module,module.exports,__webpack_require__) 对模块进行包裹构成。__webpack_require__(moduleId): 手动实现加载一个模块。对已加载过的模块进行缓存,对未加载过的模块,执行 id 定位到__webpack_modules__中的包裹函数,执行并返回module.exports,并缓存__webpack_require__(0): 运行第一个模块,即运行入口模块
假如有这样 2 个文件
import name from './name'
console.log(name)import name from './name'
console.log(name)const name = 'jack'
export { name }const name = 'jack'
export { name }使用 webpack 打包后,精简代码如下:
const __webpack_modules__ = [
(module, require) => {
const name = require(1)
console.log(name)
},
(modole, require) => {
const name = 'jack'
module.exports = name
}
]
const __webpack_require__ = id => {
const module = { exports: {} }
const m = __webpack_modules__[id](module, __webpack_require__)
return module.exports
}
__webpack_require(0)const __webpack_modules__ = [
(module, require) => {
const name = require(1)
console.log(name)
},
(modole, require) => {
const name = 'jack'
module.exports = name
}
]
const __webpack_require__ = id => {
const module = { exports: {} }
const m = __webpack_modules__[id](module, __webpack_require__)
return module.exports
}
__webpack_require(0)相比与 rollup 的方案
rollup 仅仅将所有模块平铺开,对于变量冲突,直接重新命名
const name = 'jack'
console.log(name)const name = 'jack'
console.log(name)代码分割
通过import()可进行代码分割
import('./sum').then(m => {
m.default(3, 4)
})
// 以下为 sum.js 内容
const sum = (x, y) => x + y
export default sumimport('./sum').then(m => {
m.default(3, 4)
})
// 以下为 sum.js 内容
const sum = (x, y) => x + y
export default sum将被编译成以下代码
__webpack_require__
.e(/* import() | sum */ 644)
.then(__webpack_require__.bind(__webpack_require__, 709))
.then(m => {
m.default(3, 4)
})__webpack_require__
.e(/* import() | sum */ 644)
.then(__webpack_require__.bind(__webpack_require__, 709))
.then(m => {
m.default(3, 4)
})__webpack_require__.e: 加载 chunk。该函数将使用document.createElement('script')异步加载chunk并封装为Promise。self["webpackChunk"].push:JSONP cllaback,收集modules至__webpack_modules__,并将__webpack_require__.e的Promise进行resolve。
加载非 js 资源
通过 loader 处理,loader 根据资源的特性按需处理
以下为常见的几种情况
JSON
被视为普通的 Javascript
// 实际上的 user.json 被编译为以下内容
export default {
id: 10086,
name: 'shanyue',
github: 'https://github.com/shfshanyue'
}// 实际上的 user.json 被编译为以下内容
export default {
id: 10086,
name: 'shanyue',
github: 'https://github.com/shfshanyue'
}json-loader 的最小实现原理如下
module.exports = function (source) {
const json = typeof source === 'string' ? source : JSON.stringify(source)
return `module.exports = ${json}`
}module.exports = function (source) {
const json = typeof source === 'string' ? source : JSON.stringify(source)
return `module.exports = ${json}`
}图片
替换为它自身的路径
Style
css-loader: 将 CSS 中的url与@import解析为模块style-loader: 将样式注入到 DOM 中
module.exports = function (source) {
return `
function injectCss(css) {
const style = document.createElement('style')
style.appendChild(document.createTextNode(css))
document.head.appendChild(style)
}
injectCss(\`${source}\`)
`
}module.exports = function (source) {
return `
function injectCss(css) {
const style = document.createElement('style')
style.appendChild(document.createTextNode(css))
document.head.appendChild(style)
}
injectCss(\`${source}\`)
`
}mini-css-extract-plugin: 将样式打包成单独的文件,提升渲染速度
脚本注入 html
这样做的原因:
main.js即我们最终生成的文件带有hash值,如main.8a9b3c.js。由于长期缓存优化的需要,入口文件不仅只有一个,还包括由第三方模块打包而成的
verdor.js,同样带有hash。脚本地址同时需要注入
publicPath,而在生产环境与测试环境的publicPath并不一致。
可以借助html-webpak-plugin实现
热模块替换
简称 HMR,Hot Module Replacement,热模块替换。
无需刷新在内存环境中即可替换掉过旧模块。
这种做法相对于 live reload 是不一样的,Live Reload 是指当代码进行更新后,在浏览器自动刷新以获取最新前端代码。
其原理是通过 chunk 的方式加载最新的 modules,找到 __webpack_modules__中对应的模块逐一替换,并删除其上下缓存。
代码如下:
const __webpack_modules = [
(module, exports, __webpack_require__) => {
__webpack_require__(0)
},
() => {
console.log('这是一号模块')
}
]
// HMR chunk代码
self['webpackHotUpdate'](0, {
1: () => {
console.log('这是最新的一号模块')
}
})const __webpack_modules = [
(module, exports, __webpack_require__) => {
__webpack_require__(0)
},
() => {
console.log('这是一号模块')
}
]
// HMR chunk代码
self['webpackHotUpdate'](0, {
1: () => {
console.log('这是最新的一号模块')
}
})具体实现流程如下:
webpack-dev-server将打包输出bundle使用内存型文件系统控制,而非真实的文件系统。此时使用的是memfs模拟node.js fs API每当文件发生变更时,
webpack将会重新编译,webpack-dev-server将会监控到此时文件变更事件,并找到其对应的module。此时使用的是chokidar监控文件变更webpack-dev-server将会把变更模块通知到浏览器端,此时使用websocket与浏览器进行交流。此时使用的是ws浏览器根据
websocket接收到hash,并通过hash以JSONP的方式请求更新模块的chunk浏览器加载
chunk,并使用新的模块对旧模块进行热替换,并删除其缓存