深入 Parcel--架构与流程篇
[图片]
本篇文章是对 [代码]Parce[代码] 的源码解析,代码基本架构与执行流程,带你了解打包工具的内部原理,在这之前你如果对 [代码]parcel[代码] 不熟悉可以先到 Parcel官网 了解
介绍
下面是偷懒从官网抄下来的介绍:
极速零配置Web应用打包工具
极速打包
[代码]Parcel[代码] 使用 [代码]worker[代码] 进程去启用多核编译。同时有文件系统缓存,即使在重启构建后也能快速再编译。
将你所有的资源打包
Parcel 具备开箱即用的对 [代码]JS[代码], [代码]CSS[代码], [代码]HTML[代码], 文件 及更多的支持,而且不需要插件。
自动转换
如若有需要,[代码]Babel[代码], [代码]PostCSS[代码], 和 [代码]PostHTML[代码] 甚至 [代码]node_modules[代码] 包会被用于自动转换代码.
零配置代码分拆
使用动态 [代码]import()[代码] 语法, [代码]Parcel[代码] 将你的输出文件束([代码]bundles[代码])分拆,因此你只需要在初次加载时加载你所需要的代码。
热模块替换
[代码]Parcel[代码] 无需配置,在开发环境的时候会自动在浏览器内随着你的代码更改而去更新模块。
友好的错误日志
当遇到错误时,Parcel 会输出 语法高亮的代码片段,帮助你定位问题。
打包工具
时间
browserify
22.98s
webpack
20.71s
parcel
9.98s
parcel - with cache
2.64s
打包工具
我们常用的打包工具大致功能:
模块化(代码的拆分, 合并, [代码]Tree-Shaking[代码] 等)
编译([代码]es6,7,8 sass typescript[代码] 等)
压缩 ([代码]js, css, html[代码]包括图片的压缩)
HMR (热替换)
version
[代码]parcel-bundler[代码] 版本:
“version”: “1.11.0”
文件架构
[代码]|-- assets 资源目录 继承自 Asset.js
|-- builtins 用于最终构建
|-- packagers 打包
|-- scope-hoisting 作用域提升 Tree-Shake
|-- transforms 转换代码为 AST
|-- utils 工具
|-- visitors 遍历 js AST树 收集依赖等
|-- Asset.js 资源
|-- Bundle.js 用于构建 bundle 树
|-- Bundler.js 主目录
|-- FSCache.js 缓存
|-- HMRServer.js HMR服务器提供 WebSocket
|-- Parser.js 根据文件扩展名获取对应 Asset
|-- Pipeline.js 多线程执行方法
|-- Resolver.js 解析模块路径
|-- Server.js 静态资源服务器
|-- SourceMap.js SourceMap
|-- cli.js cli入口 解析命令行参数
|-- worker.js 多线程入口
[代码]
流程
说明
[代码]Parcel[代码]是面向资源的,[代码]JavaScript,CSS,HTML[代码] 这些都是资源,并不是 [代码]webpack[代码] 中 [代码]js[代码] 是一等公民,[代码]Parcel[代码] 会自动的从入口文件开始分析这些文件 和 模块中的依赖,然后构建一个 [代码]bundle[代码] 树,并对其进行打包输出到指定目录
一个简单的例子
我们从一个简单的例子开始了解 [代码]parcel[代码] 内部源码与流程
[代码]index.html
|-- index.js
|-- module1.js
|-- module2.js
[代码]
上面是我们例子的结构,入口为 [代码]index.html[代码], 在 [代码]index.html[代码] 中我们用 [代码]script[代码] 标签引用了 [代码]src/index.js[代码],在 [代码]index.js[代码] 中我们引入了2个子模块
执行
[代码]npx parcel index.html[代码] 或者 [代码]./node_modules/.bin/parcel index.html[代码],或者使用 [代码]npm script[代码]
cli
[代码]"bin": {
"parcel": "bin/cli.js"
}
[代码]
查看 [代码]parcel-bundler[代码]的 [代码]package.json[代码] 找到 [代码]bin/cli.js[代码],在[代码]cli.js[代码]里又指向 [代码]../src/cli[代码]
[代码]const program = require('commander');
program
.command('serve [input...]') // watch build
...
.action(bundle);
program.parse(process.argv);
async function bundle(main, command) {
const Bundler = require('./Bundler');
const bundler = new Bundler(main, command);
if (command.name() === 'serve' && command.target === 'browser') {
const server = await bundler.serve();
if (server && command.open) {...启动自动打开浏览器}
} else {
bundler.bundle();
}
}
[代码]
在 [代码]cli.js[代码] 中利用 [代码]commander[代码] 解析命令行并调用 [代码]bundle[代码] 方法
有 [代码]serve, watch, build[代码] 3个命令来调用 [代码]bundle[代码] 函数,执行 [代码]pracel index.html[代码] 默认为 [代码]serve[代码],所以调用的是 [代码]bundler.serve[代码] 方法
进入 [代码]Bundler.js[代码]
bundler.serve
[代码]async serve(port = 1234, https = false, host) {
this.server = await Server.serve(this, port, host, https);
try {
await this.bundle();
} catch (e) {}
return this.server;
}
[代码]
[代码]bundler.serve[代码] 方法 调用 [代码]serveStatic[代码] 起了一个静态服务指向 最终打包的文件夹
下面就是重要的 [代码]bundle[代码] 方法
bundler.bundle
[代码]async bundle() {
// 加载插件 设置env 启动多线程 watcher hmr
await this.start();
if (isInitialBundle) {
// 创建 输出目录
await fs.mkdirp(this.options.outDir);
this.entryAssets = new Set();
for (let entry of this.entryFiles) {
let asset = await this.resolveAsset(entry);
this.buildQueue.add(asset);
this.entryAssets.add(asset);
}
}
// 打包队列中的资源
let loadedAssets = await this.buildQueue.run();
// findOrphanAssets 获取所有资源中独立的没有父Bundle的资源
let changedAssets = [...this.findOrphanAssets(), ...loadedAssets];
// 因为接下来要构建 Bundle 树,先对上一次的 Bundle树 进行 clear 操作
for (let asset of this.loadedAssets.values()) {
asset.invalidateBundle();
}
// 构建 Bundle 树
this.mainBundle = new Bundle();
for (let asset of this.entryAssets) {
this.createBundleTree(asset, this.mainBundle);
}
// 获取新的最终打包文件的url
this.bundleNameMap = this.mainBundle.getBundleNameMap(
this.options.contentHash
);
// 将代码中的旧文件url替换为新的
for (let asset of changedAssets) {
asset.replaceBundleNames(this.bundleNameMap);
}
// 将改变的资源通过websocket发送到浏览器
if (this.hmr && !isInitialBundle) {
this.hmr.emitUpdate(changedAssets);
}
// 对资源打包
this.bundleHashes = await this.mainBundle.package(
this,
this.bundleHashes
);
// 将独立的资源删除
this.unloadOrphanedAssets();
return this.mainBundle;
}
[代码]
我们一步步先从 [代码]this.start[代码] 看
start
[代码]if (this.farm) {
return;
}
await this.loadPlugins();
if (!this.options.env) {
await loadEnv(Path.join(this.options.rootDir, 'index'));
this.options.env = process.env;
}
if (this.options.watch) {
this.watcher = new Watcher();
this.watcher.on('change', this.onChange.bind(this));
}
if (this.options.hmr) {
this.hmr = new HMRServer();
this.options.hmrPort = await this.hmr.start(this.options);
}
this.farm = await WorkerFarm.getShared(this.options, {
workerPath: require.resolve('./worker.js')
});
[代码]
[代码]start[代码]:
[代码]开头的判断[代码] 防止多次执行,也就是说 [代码]this.start[代码] 只会执行一次
[代码]loadPlugins[代码] 加载插件,找到 [代码]package.json[代码] 文件 [代码]dependencies, devDependencies[代码] 中 [代码]parcel-plugin-[代码]开头的插件进行调用
[代码]loadEnv[代码] 加载环境变量,利用 [代码]dotenv, dotenv-expand[代码] 包将 [代码]env.development.local, .env.development, .env.local, .env[代码] 扩展至 [代码]process.env[代码]
[代码]watch[代码] 初始化监听文件并绑定 [代码]change[代码] 回调函数,内部 [代码]child_process.fork[代码] 起一个子进程,使用 [代码]chokidar[代码] 包来监听文件改变
[代码]hmr[代码] 起一个服务,[代码]WebSocket[代码] 向浏览器发送更改的资源
[代码]farm[代码] 初始化多进程并指定 [代码]werker[代码] 工作文件,开启多个 [代码]child_process[代码] 去解析编译资源
接下来回到 [代码]bundle[代码],[代码]isInitialBundle[代码] 是一个判断是否是第一次构建
[代码]fs.mkdirp[代码] 创建输出文件夹
遍历入口文件,通过 [代码]resolveAsset[代码],内部调用 [代码]resolver[代码] 解析路径,并 [代码]getAsset[代码] 获取到对应的 [代码]asset[代码](这里我们入口是 [代码]index.html[代码],根据扩展名获取到的是 [代码]HTMLAsset[代码])
将 [代码]asset[代码] 添加进队列
然后启动 [代码]this.buildQueue.run()[代码] 对资源从入口递归开始打包
PromiseQueue
这里 [代码]buildQueue[代码] 是一个 [代码]PromiseQueue[代码] 异步队列
[代码]PromiseQueue[代码] 在初始化的时候传入一个回调函数 [代码]callback[代码],内部维护一个参数队列 [代码]queue[代码],[代码]add[代码] 往队列里 [代码]push[代码] 一个参数,[代码]run[代码] 的时候[代码]while[代码]遍历队列 [代码]callback(...queue.shift())[代码],队列全部执行完毕 [代码]Promise[代码] 置为完成([代码]resolved[代码])(可以将其理解为 [代码]Promise.all[代码])
这里定义的回调函数是 [代码]processAsset[代码],参数就是入口文件 [代码]index.html[代码] 的 [代码]HTMLAsset[代码]
[代码]async processAsset(asset, isRebuild) {
if (isRebuild) {
asset.invalidate();
if (this.cache) {
this.cache.invalidate(asset.name);
}
}
await this.loadAsset(asset);
}
[代码]
[代码]processAsset[代码] 函数内先判断是否是 [代码]Rebuild[代码] ,是第一次构建,还是 [代码]watch[代码] 监听文件改变进行的重建,如果是重建则对资源的属性[代码]重置[代码],并使其缓存失效
之后调用 [代码]loadAsset[代码] 加载资源编译资源
loadAsset
[代码]async loadAsset(asset) {
if (asset.processed) {
return;
}
// Mark the asset processed so we don't load it twice
asset.processed = true;
// 先尝试读缓存,缓存没有在后台加载和编译
asset.startTime = Date.now();
let processed = this.cache && (await this.cache.read(asset.name));
let cacheMiss = false;
if (!processed || asset.shouldInvalidate(processed.cacheData)) {
processed = await this.farm.run(asset.name);
cacheMiss = true;
}
asset.endTime = Date.now();
asset.buildTime = asset.endTime - asset.startTime;
asset.id = processed.id;
asset.generated = processed.generated;
asset.hash = processed.hash;
asset.cacheData = processed.cacheData;
// 解析和加载当前资源的依赖项
let assetDeps = await Promise.all(
dependencies.map(async dep => {
dep.parent = asset.name;
let assetDep = await this.resolveDep(asset, dep);
if (assetDep) {
await this.loadAsset(assetDep);
}
return assetDep;
})
);
if (this.cache && cacheMiss) {
this.cache.write(asset.name, processed);
}
}
[代码]
[代码]loadAsset[代码] 在开始有个判断防止重复编译
之后去读缓存,读取失败就调用 [代码]this.farm.run[代码] 在多进程里编译资源
编译完就去加载并编译依赖的文件
最后如果是新的资源没有用到缓存,就重新设置一下缓存
下面说一下这里吗涉及的两个东西:缓存 [代码]FSCache[代码] 和 多进程 [代码]WorkerFarm[代码]
FSCache
[代码]read[代码] 读取缓存,并判断最后修改时间和缓存的修改时间
[代码]write[代码] 写入缓存
[图片]
缓存目录为了加速读取,避免将所有的缓存文件放在一个文件夹里,[代码]parcel[代码] 将 [代码]16进制[代码] 两位数的 [代码]256[代码] 种可能创建为文件夹,这样存取缓存文件的时候,将目标文件路径 [代码]md5[代码] 加密转换为 [代码]16进制[代码],然后截取前两位是目录,后面几位是文件名
WorkerFarm
在上面 [代码]start[代码] 里初始化 [代码]farm[代码] 的时候,[代码]workerPath[代码] 指向了 [代码]worker.js[代码] 文件,[代码]worker.js[代码] 里有两个函数,[代码]init[代码] 和 [代码]run[代码]
[代码]WorkerFarm.getShared[代码] 初始化的时候会创建一个 [代码]new WorkerFarm[代码] ,调用 [代码]worker.js[代码] 的 [代码]init[代码] 方法,根据 [代码]cpu[代码] 获取最大的 [代码]Worker[代码] 数,并启动一半的子进程
[代码]farm.run[代码] 会通知子进程执行 [代码]worker.js[代码] 的 [代码]run[代码] 方法,如果进程数没有达到最大会再次开启一个新的子进程,子进程执行完毕后将 [代码]Promise[代码]状态更改为完成
[代码]worker.run -> pipeline.process -> pipeline.processAsset -> asset.process[代码]
[代码]Asset.process[代码] 处理资源:
[代码]async process() {
if (!this.generated) {
await this.loadIfNeeded();
await this.pretransform();
await this.getDependencies();
await this.transform();
this.generated = await this.generate();
}
return this.generated;
}
[代码]
将上面的代码内部扩展一下:
[代码]async process() {
// 已经有就不需要编译
if (!this.generated) {
// 加载代码
if (this.contents == null) {
this.contents = await this.load();
}
// 可选。在收集依赖之前转换。
await this.pretransform();
// 将代码解析为 AST 树
if (!this.ast) {
this.ast = await this.parse(this.contents);
}
// 收集依赖
await this.collectDependencies();
// 可选。在收集依赖之后转换。
await this.transform();
// 生成代码
this.generated = await this.generate();
}
return this.generated;
}
// 最后处理代码
async postProcess(generated) {
return generated
}
[代码]
[代码]processAsset[代码] 中调用 [代码]asset.process[代码] 生成 [代码]generated[代码] 这个[代码]generated[代码] 不一定是最终代码 ,像 [代码]html[代码]里内联的 [代码]script[代码] ,[代码]vue[代码] 的 [代码]html, js, css[代码],都会进行二次或多次递归处理,最终调用 [代码]asset.postProcess[代码] 生成代码
Asset
下面说几个实现
[代码]HTMLAsset[代码]:
pretransform 调用 [代码]posthtml[代码] 将 [代码]html[代码] 解析为 [代码]PostHTMLTree[代码](如果没有设置[代码]posthtmlrc[代码]之类的不会走)
parse 调用 [代码]posthtml-parser[代码] 将 [代码]html[代码] 解析为 [代码]PostHTMLTree[代码]
collectDependencies 用 [代码]walk[代码] 遍历 [代码]ast[代码],找到 [代码]script, img[代码] 的 [代码]src[代码],[代码]link[代码] 的 [代码]href[代码] 等的地址,将其加入到依赖
transform [代码]htmlnano[代码] 压缩代码
generate 处理内联的 [代码]script[代码] 和 [代码]css[代码]
postProcess [代码]posthtml-render[代码] 生成 [代码]html[代码] 代码
[代码]JSAsset[代码]:
pretransform 调用 [代码]@babel/core[代码] 将 [代码]js[代码] 解析为 [代码]AST[代码],处理 [代码]process.env[代码]
parse 调用 [代码]@babel/parser[代码] 将 [代码]js[代码] 解析为 [代码]AST[代码]
collectDependencies 用 [代码]babylon-walk[代码] 遍历 [代码]ast[代码], 如 [代码]ImportDeclaration[代码],[代码]import xx from 'xx'[代码] 语法,[代码]CallExpression[代码] 找到 [代码]require[代码]调用,[代码]import[代码] 被标记为 [代码]dynamic[代码] 动态导入,将这些模块加入到依赖
transform 处理 [代码]readFileSync[代码],[代码]__dirname, __filename, global[代码]等,如果没有设置[代码]scopeHoist[代码] 并存在 [代码]es6 module[代码] 就将代码转换为 [代码]commonjs[代码],[代码]terser[代码] 压缩代码
generate [代码]@babel/generator[代码] 获取 [代码]js[代码] 与 [代码]sourceMap[代码] 代码
[代码]VueAsset[代码]:
parse [代码]@vue/component-compiler-utils[代码] 与 [代码]vue-template-compiler[代码] 对 [代码].vue[代码] 文件进行解析
generate 对 [代码]html, js, css[代码] 处理,就像上面说到会对其分别调用 [代码]processAsset[代码] 进行二次解析
postProcess [代码]component-compiler-utils[代码] 的 [代码]compileTemplate, compileStyle[代码]处理 [代码]html,css[代码],[代码]vue-hot-reload-api[代码] HMR处理,压缩代码
回到 [代码]bundle[代码] 方法:
[代码]let loadedAssets = await this.buildQueue.run()[代码] 就是上面说到的[代码]PromiseQueue[代码] 和 [代码]WorkerFarm[代码] 结合起来:[代码]buildQueue.run —> processAsset -> loadAsset -> farm.run -> worker.run -> pipeline.process -> pipeline.processAsset -> asset.process[代码],执行之后所有资源编译完毕,并返回入口资源[代码]loadedAssets[代码]就是 [代码]index.html[代码] 对应的 [代码]HTMLAsset[代码] 资源
之后是 [代码]let changedAssets = [...this.findOrphanAssets(), ...loadedAssets][代码] 获取到改变的资源
[代码]findOrphanAssets[代码] 是从所有资源中查找没有 [代码]parentBundle[代码] 的资源,也就是独立的资源,这个 [代码]parentBundle[代码] 会在等会的构建 [代码]Bundle[代码] 树中被赋值,第一次构建都没有 [代码]parentBundle[代码],所以这里会重复入口文件,这里的 [代码]findOrphanAssets[代码] 的作用是在第一次构建之后,文件[代码]change[代码]的时候,在这个文件 [代码]import[代码]了新的一个文件,因为新文件没有被构建过 [代码]Bundle[代码] 树,所以没有 [代码]parentBundle[代码],这个新文件也被标记物 [代码]change[代码]
[代码]invalidateBundle[代码] 因为接下来要构建新的树所以调用重置所有资源上一次树的属性
[代码]createBundleTree[代码] 构建 [代码]Bundle[代码] 树:
首先一个入口资源会被创建成一个 bundle,然后动态的 import() 会被创建成子 bundle ,这引发了代码的拆分。
当不同类型的文件资源被引入,兄弟 bundle 就会被创建。例如你在 JavaScript 中引入了 CSS 文件,那它会被放置在一个与 JavaScript 文件对应的兄弟 bundle 中。
如果资源被多于一个 bundle 引用,它会被提升到 bundle 树中最近的公共祖先中,这样该资源就不会被多次打包。
[代码]Bundle[代码]:
[代码]type[代码]:它包含的资源类型 (例如:js, css, map, …)
[代码]name[代码]:bundle 的名称 (使用 entryAsset 的 Asset.generateBundleName() 生成)
[代码]parentBundle[代码]:父 bundle ,入口 bundle 的父 bundle 是 null
[代码]entryAsset[代码]:bundle 的入口,用于生成名称(name)和聚拢资源(assets)
[代码]assets[代码]:bundle 中所有资源的集合(Set)
[代码]childBundles[代码]:所有子 bundle 的集合(Set)
[代码]siblingBundles[代码]:所有兄弟 bundle 的集合(Set)
[代码]siblingBundlesMap[代码]:所有兄弟 bundle 的映射 Map<String(Type: js, css, map, …), Bundle>
[代码]offsets[代码]:所有 bundle 中资源位置的映射 Map<Asset, number(line number inside the bundle)> ,用于生成准确的 sourcemap 。
我们的例子会被构建成:
[代码]html ( index.html )
|-- js ( index.js, module1.js, module2.js )
|-- map ( index.js, module1.js, module2.js )
[代码]
[代码]module1.js[代码] 和 [代码]module2.js[代码] 被提到了与 [代码]index.js[代码] 同级,[代码]map[代码] 因为类型不同被放到了 子[代码]bundle[代码]
一个复杂点的树:
[代码]// 资源树
index.html
|-- index.css
|-- bg.png
|-- index.js
|-- module.js
[代码]
[代码]// mainBundle
html ( index.html )
|-- js ( index.js, module.js )
|-- map ( index.map, module.map )
|-- css ( index.css )
|-- js ( index.css, css-loader.js bundle-url.js )
|-- map ( css-loader.js, bundle-url.js )
|-- png ( bg.png )
[代码]
因为要对 css 热更新,所以新增了 [代码]css-loader.js, bundle-url.js[代码] 两个 js
[代码]replaceBundleNames[代码]替换引用:生成树之后将代码中的文件引用替换为最终打包的文件名,如果是生产环境会替换为 [代码]contentHash[代码] 根据内容生成 [代码]hash[代码]
[代码]hmr[代码]更新: 判断启用 [代码]hmr[代码] 并且不是第一次构建的情况,调用 [代码]hmr.emitUpdate[代码] 将改变的资源发送给浏览器
[代码]Bundle.package[代码] 打包
[代码]unloadOrphanedAssets[代码] 将独立的资源删除
package
[代码]package[代码] 将[代码]generated[代码] 写入到文件
有6种打包:
[代码]CSSPackager[代码],[代码]HTMLPackager[代码],[代码]SourceMapPackager[代码],[代码]JSPackager[代码],[代码]JSConcatPackager[代码],[代码]RawPackager[代码]
当开启 [代码]scopeHoist[代码] 时用 [代码]JSConcatPackager[代码] 否则 [代码]JSPackager[代码]
图片等资源用 [代码]RawPackager[代码]
最终我们的例子被打包成 [代码]index.html, src.[hash].js, src.[hash].map[代码] 3个文件
[代码]index.html[代码] 里的 [代码]js[代码] 路径被替换成立最终打包的地址
我们看一下打包的 js:
[代码]parcelRequire = (function (modules, cache, entry, globalName) {
// Save the require from previous bundle to this closure if any
var previousRequire = typeof parcelRequire === 'function' && parcelRequire;
var nodeRequire = typeof require === 'function' && require;
function newRequire(name, jumped) {
if (!cache[name]) {
localRequire.resolve = resolve;
localRequire.cache = {};
var module = cache[name] = new newRequire.Module(name);
modules[name][0].call(module.exports, localRequire, module, module.exports, this);
}
return cache[name].exports;
function localRequire(x){
return newRequire(localRequire.resolve(x));
}
function resolve(x){
return modules[name][4][x] || x;
}
}
for (var i = 0; i < entry.length; i++) {
newRequire(entry[i]);
}
// Override the current require with this new one
return newRequire;
})({"src/module1.js":[function(require,module,exports) {
"use strict";
},{}],"src/module2.js":[function(require,module,exports) {
"use strict";
},{}],"src/index.js":[function(require,module,exports) {
"use strict";
var _module = require("./module");
var _module2 = require("./module1");
var _module3 = require("./module2");
console.log(_module.m);
},{"./module":"src/module.js","./module1":"src/module1.js","./module2":"src/module2.js","fs":"node_modules/parcel-bundler/src/builtins/_empty.js"}]
,{}]},{},["node_modules/parcel-bundler/src/builtins/hmr-runtime.js","src/index.js"], null)
//# sourceMappingURL=/src.a2b27638.map
[代码]
可以看到代码被拼接成了对象的形式,接收参数 [代码]module, require[代码] 用来模块导入导出,实现了 [代码]commonjs[代码] 的模块加载机制,一个更加简化版:
[代码]parcelRequire = (function (modules, cache, entry, globalName) {
function newRequire(id){
if(!cache[id]){
let module = cache[id] = { exports: {} }
modules[id][0].call(module.exports, newRequire, module, module.exports, this);
}
return cache[id]
}
for (var i = 0; i < entry.length; i++) {
newRequire(entry[i]);
}
return newRequire;
})()
[代码]
代码被拼接起来:
[代码]`(function(modules){
//...newRequire
})({` +
asset.id +
':[function(require,module,exports) {\n' +
asset.generated.js +
'\n},' +
'})'
[代码]
[代码](function(modules){
//...newRequire
})({
"src/index.js":[function(require,module,exports){
// code
}]
})
[代码]
hmr-runtime
上面打包的 [代码]js[代码] 中还有个 [代码]hmr-runtime.js[代码] 太长被我省略了
[代码]hmr-runtime.js[代码] 创建一个 [代码]WebSocket[代码] 监听服务端消息
修改文件触发 [代码]onChange[代码] 方法,[代码]onChange[代码] 将改变的资源 [代码]buildQueue.add[代码] 加入构建队列,重新调用 [代码]bundle[代码] 方法,打包资源,并调用 [代码]emitUpdate[代码] 通知浏览器更新
当浏览器接收到服务端有新资源更新消息时
新的资源就会设置或覆盖之前的模块
[代码]modules[asset.id] = new Function('require', 'module', 'exports', asset.generated.js)[代码]
对模块进行更新:
[代码]function hmrAccept(id){
// dispose 回调
cached.hot._disposeCallbacks.forEach(function (cb) {
cb(bundle.hotData);
});
delete bundle.cache[id]; // 删除之前缓存
newRequire(id); // 重新此加载
// accept 回调
cached.hot._acceptCallbacks.forEach(function (cb) {
cb();
});
// 递归父模块 进行更新
getParents(global.parcelRequire, id).some(function (id) {
return hmrAccept(global.parcelRequire, id);
});
}
[代码]
至此整个打包流程结束
总结
[代码]parcle index.html[代码]
进入 [代码]cli[代码],启动[代码]Server[代码]调用 [代码]bundle[代码],初始化配置([代码]Plugins[代码], [代码]env[代码], [代码]HMRServer, Watcher, WorkerFarm[代码]),从入口资源开始,递归编译([代码]babel, posthtml, postcss, vue-template-compiler[代码]等),编译完设置缓存,构建 [代码]Bundle[代码] 树,进行打包
如果没有 [代码]watch[代码] 监听,结束关闭 [代码]Watcher, Worker, HMR[代码]
有 [代码]watch[代码] 监听:
文件修改,触发 [代码]onChange[代码],将修改的资源加入构建队列,递归编译,查找缓存(这一步缓存的作用就提醒出来了),编译完设置新缓存,构建 [代码]Bundle[代码] 树,进行打包,将 [代码]change[代码] 的资源发送给浏览器,浏览器接收 [代码]hmr[代码] 更新资源
最后
通过此文章希望你对 [代码]parcel[代码] 的大致流程,打包工具原理有更深的了解
了解更多请关注专栏,后续 深入Parcel 同系列文章,对 [代码]Asset[代码],[代码]Packager[代码],[代码]Worker[代码],[代码]HMR[代码],[代码]scopeHoist[代码],[代码]FSCache[代码],[代码]SourceMap[代码],[代码]import[代码] 更加 详细讲解与代码实现