Parcel 源码解读
Version parcel-bundler: 1.11.0
类

Parcel中主要包含上述类:
- Bundler,打包逻辑的入口
- Parser,Asset的注册表,根据文件后缀查找并创建对应的Asset类
- Asset,文件资源类,负责自身资源处理、依赖收集等操作,同时记录着原始资源、打包结果等信息;HTMLAsset、JSAsset等资源的Asset继承自此基类
- Bundle,打包输出文件类,它由多个资源(Asset)组成,会根据当前Bundle类的类型查找对应的打包器(从PackagerRegistry中获取),调用打包器的package方法将自身包含的Asset打包进目的文件;bundle可以有子bundle,当动态从该bundle导入文件的时候,或者导入一个其他类型资源的文件的时候会产生childBundles
- PackagerRegistry,Packager注册表,根据资源类型(基本上是Bundle在调用,所以基本上是Bundle的类型,也可以说是对应Asset的类型)注册、获取打包器(Packager)
- Packager,打包组合类,用于将各个Asset产生的结果打包进目标文件,比如JSPackager将类型为JS的Asset产生的内容,打包以Bundle.name为名字的文件中
- HMRServer,热更服务,其中包含启动ws服务,触发update等方法
- FSCache,缓存
- Resolver,资源路径解析类,如何对代码中引入的各种相对路径的资源路径进行解析,从而找到该模块的绝对路径
它们直接的调用及继承关系如下:
- Bundler作为打包的入口,其中包含有Parser、Bundle、HMRServer、FSCache、Resolver等类
- 构建的第一阶段,Bundler类调用Parser类获取文件对应的Asset,然后调用对应Asset的process等方法,取得Asset树
- 构建的第二阶段,Bundler类中实例化根Bundle(初始空bundle),根据第一阶段中Asset的依赖信息,构建Bundle树
- 构建的第三阶段,调用根Bundle类中的package方法,根据Bundle树进行文件写入等操作
- Asset和Packager为基类,对应类型的(HTML、JS等)类继承自此基类
打包流程
打包的整体过程就在Bundler.bundle()方法中,代码如下:
async bundle() {
// If another bundle is already pending, wait for that one to finish and retry.
if (this.pending) {
return new Promise((resolve, reject) => {
this.once('buildEnd', () => {
this.bundle().then(resolve, reject);
});
});
}
......
logger.clear();
logger.progress('Building...');
try {
// Start worker farm, watcher, etc. if needed
await this.start();
// Emit start event, after bundler is initialised
this.emit('buildStart', this.entryFiles);
// If this is the initial bundle, ensure the output directory exists, and resolve the main asset.
if (isInitialBundle) {
await fs.mkdirp(this.options.outDir);
this.entryAssets = new Set();
for (let entry of this.entryFiles) {
try {
let asset = await this.resolveAsset(entry);
this.buildQueue.add(asset);
this.entryAssets.add(asset);
} catch (err) {
throw new Error(
`Cannot resolve entry "${entry}" from "${this.options.rootDir}"`
);
}
}
if (this.entryAssets.size === 0) {
throw new Error('No entries found.');
}
initialised = true;
}
// Build the queued assets.
let loadedAssets = await this.buildQueue.run();
// The changed assets are any that don't have a parent bundle yet
// plus the ones that were in the build queue.
let changedAssets = [...this.findOrphanAssets(), ...loadedAssets];
// Invalidate bundles
for (let asset of this.loadedAssets.values()) {
asset.invalidateBundle();
}
logger.progress(`Producing bundles...`);
// Create a root bundle to hold all of the entry assets, and add them to the tree.
this.mainBundle = new Bundle();
for (let asset of this.entryAssets) {
this.createBundleTree(asset, this.mainBundle);
}
// If there is only one child bundle, replace the root with that bundle.
if (this.mainBundle.childBundles.size === 1) {
this.mainBundle = Array.from(this.mainBundle.childBundles)[0];
}
// Generate the final bundle names, and replace references in the built assets.
this.bundleNameMap = this.mainBundle.getBundleNameMap(
this.options.contentHash
);
for (let asset of changedAssets) {
asset.replaceBundleNames(this.bundleNameMap);
}
// Emit an HMR update if this is not the initial bundle.
if (this.hmr && !isInitialBundle) {
this.hmr.emitUpdate(changedAssets);
}
logger.progress(`Packaging...`);
// Package everything up
this.bundleHashes = await this.mainBundle.package(
this,
this.bundleHashes
);
......
return this.mainBundle;
} catch (err) {
......
} finally {
this.pending = false;
this.emit('buildEnd');
// If not in watch mode, stop the worker farm so we don't keep the process running.
if (!this.watcher && this.options.killWorkers) {
await this.stop();
}
}
}
这里主要做了如下几件事:
- 准备工作,加载插件等
- 根据入口文件及其依赖构建Asset Tree
- 根据Asset Tree构建Bundle Tree
- 根据Bundle Tree进行Package操作
下面我们一步一步的讲解:
准备工作
准备工作主要在Bundler.start()中,代码如下:
async 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;
}
this.options.extensions = Object.assign({}, this.parser.extensions);
this.options.bundleLoaders = this.bundleLoaders;
if (this.options.watch) {
this.watcher = new Watcher();
// Wait for ready event for reliable testing on watcher
if (process.env.NODE_ENV === 'test' && !this.watcher.ready) {
await new Promise(resolve => this.watcher.once('ready', resolve));
}
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')
});
}
这里主要做了如下几件事
- 加载Parcel插件
- 监听文件变化(可选)
- 启动HMR服务(可选)
加载Parcel插件
加载Parcel插件的代码如下:
async loadPlugins() {
let relative = Path.join(this.options.rootDir, 'index');
let pkg = await config.load(relative, ['package.json']);
if (!pkg) {
return;
}
try {
let deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
for (let dep in deps) {
const pattern = /^(@.*\/)?parcel-plugin-.+/;
if (pattern.test(dep)) {
let plugin = await localRequire(dep, relative);
await plugin(this);
}
}
} catch (err) {
logger.warn(err);
}
}
加载插件步骤如下:
- 读取根目录上的package.json
- 循环遍历dependencies和devDependencies
- 查找其中满足
parcel-plugin-格式的依赖 - 调用
localRequire方法进行加载,localRequire获取到文件路径并缓存,然后做require操作(如果没有安装该npm包,则会调用npm / yarm install进行安装)。localRequire可以说是一个代理模式,代理了对文件的访问 - 执行插件
- 查找其中满足
注意,这里的localRequire就是一个代理模式,中间加入了缓存机制,控制了模块的访问。
监听文件变化和HMR后面会进行介绍。
构建Asset Tree
构建Asset Tree的主要逻辑在Bundler.Bundle()方法中,代码如下:
// If this is the initial bundle, ensure the output directory exists, and resolve the main asset.
if (isInitialBundle) {
await fs.mkdirp(this.options.outDir);
this.entryAssets = new Set();
for (let entry of this.entryFiles) {
try {
let asset = await this.resolveAsset(entry);
this.buildQueue.add(asset);
this.entryAssets.add(asset);
} catch (err) {
throw new Error(
`Cannot resolve entry "${entry}" from "${this.options.rootDir}"`
);
}
}
if (this.entryAssets.size === 0) {
throw new Error('No entries found.');
}
initialised = true;
}
// Build the queued assets.
let loadedAssets = await this.buildQueue.run();
这里主要做了如下几件事:
- 遍历入口文件
- 根据文件后缀获取到入口文件对应的Asset实例
- 将Asset实例加入到buildQueue中
- 执行
buildQueue.run()
Asset类
首先说明下Asset,Asset是文件资源类,与文件保持一对一的关系,Asset基类代码如下:
class Asset {
constructor(name, options) {
this.id = null;
this.name = name;
this.basename = path.basename(this.name);
this.relativeName = path
.relative(options.rootDir, this.name)
.replace(/\\/g, '/');
......
this.contents = options.rendition ? options.rendition.value : null;
this.ast = null;
this.generated = null;
......
}
shouldInvalidate() {
return false;
}
async loadIfNeeded() {
if (this.contents == null) {
this.contents = await this.load();
}
}
async parseIfNeeded() {
await this.loadIfNeeded();
if (!this.ast) {
this.ast = await this.parse(this.contents);
}
}
async getDependencies() {
if (
this.options.rendition &&
this.options.rendition.hasDependencies === false
) {
return;
}
await this.loadIfNeeded();
if (this.contents && this.mightHaveDependencies()) {
await this.parseIfNeeded();
await this.collectDependencies();
}
}
addDependency(name, opts) {
this.dependencies.set(name, Object.assign({name}, opts));
}
addURLDependency(url, from = this.name, opts) {
if (!url || isURL(url)) {
return url;
}
if (typeof from === 'object') {
opts = from;
from = this.name;
}
const parsed = URL.parse(url);
let depName;
let resolved;
let dir = path.dirname(from);
const filename = decodeURIComponent(parsed.pathname);
if (filename[0] === '~' || filename[0] === '/') {
if (dir === '.') {
dir = this.options.rootDir;
}
depName = resolved = this.resolver.resolveFilename(filename, dir);
} else {
resolved = path.resolve(dir, filename);
depName = './' + path.relative(path.dirname(this.name), resolved);
}
this.addDependency(depName, Object.assign({dynamic: true, resolved}, opts));
parsed.pathname = this.options.parser
.getAsset(resolved, this.options)
.generateBundleName();
return URL.format(parsed);
}
......
parse() {
// do nothing by default
}
collectDependencies() {
// do nothing by default
}
async pretransform() {
// do nothing by default
}
async transform() {
// do nothing by default
}
async generate() {
return {
[this.type]: this.contents
};
}
async process() {
// Generate the id for this asset, unless it has already been set.
// We do this here rather than in the constructor to avoid unnecessary work in the main process.
// In development, the id is just the relative path to the file, for easy debugging and performance.
// In production, we use a short hash of the relative path.
if (!this.id) {
this.id =
this.options.production || this.options.scopeHoist
? md5(this.relativeName, 'base64').slice(0, 4)
: this.relativeName;
}
if (!this.generated) {
await this.loadIfNeeded();
await this.pretransform();
await this.getDependencies();
await this.transform();
this.generated = await this.generate();
}
return this.generated;
}
......
}
这里主要关注下process方法,也就是文件的文件资源的处理过程:
- loadIfNeeded,加载文件内容
- pretransform,预处理,比如js资源会用babel()进行转换
- getDependencies, 这里主要对资源字符串进行解析,例如html字符串用posthtml-parser, js资源用babylon.parse来解析。然后收集依赖collectDependencies,具体操作稍后分析。
- transform, 资源转换步骤接收 AST并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。
- generate,产出一份处理后的文件内容,基本返回的数据格式是
[this.type]: this.contents - generateHash,根据处理后的文件内容,产出对应hash值
注意,这里不同的子类会继承自此基类,实现基类暴露的接口,这其实就是针对接口编程的设计原则。
收集依赖的过程会在下面进行详细介绍。
Bundler.resolveAsset
根据文件后缀获取到入口文件对应的Asset实例的逻辑Bundler.resolveAsset中,代码如下:
async resolveAsset(name, parent) {
let {path} = await this.resolver.resolve(name, parent);
return this.getLoadedAsset(path);
}
getLoadedAsset(path) {
if (this.loadedAssets.has(path)) {
return this.loadedAssets.get(path);
}
let asset = this.parser.getAsset(path, this.options);
this.loadedAssets.set(path, asset);
this.watch(path, asset);
return asset;
}
主要做了如下两件事:
- 利用Resolver类,获取到文件的绝对路径
- 利用Parser类,根据文件的后缀获取到Asset实例
这里简单说下Parser,Parser可以说是Asset的注册表,根据类型存储对应的Asset实例,parser.getAsset方法根据文件路径获取对应的Asset实例。
buildQueue.run
buildQueue是PromiseQueue的实例,PromiseQueue.run方法将对列中的内容一次通过process函数处理。PromiseQueue有兴趣大家可以去看下代码,这里不在赘述。
buildQueue的初始化代码在Bundler的constructor中,代码如下:
this.buildQueue = new PromiseQueue(this.processAsset.bind(this));
在我们上述的场景中,执行逻辑就是对所有的入口文件对应的Asset,执行Bundler.processAsset(Asset)。
Bundler.processAsset()最终调用的是Bundler.loadAsset()方法,代码如下:
async loadAsset(asset) {
......
if (!processed || asset.shouldInvalidate(processed.cacheData)) {
processed = await this.farm.run(asset.name);
cacheMiss = true;
}
......
// Call the delegate to get implicit dependencies
let dependencies = processed.dependencies;
if (this.delegate.getImplicitDependencies) {
let implicitDeps = await this.delegate.getImplicitDependencies(asset);
if (implicitDeps) {
dependencies = dependencies.concat(implicitDeps);
}
}
// Resolve and load asset dependencies
let assetDeps = await Promise.all(
dependencies.map(async dep => {
if (dep.includedInParent) {
// This dependency is already included in the parent's generated output,
// so no need to load it. We map the name back to the parent asset so
// that changing it triggers a recompile of the parent.
this.watch(dep.name, asset);
} else {
dep.parent = asset.name;
let assetDep = await this.resolveDep(asset, dep);
if (assetDep) {
await this.loadAsset(assetDep);
}
return assetDep;
}
})
);
// Store resolved assets in their original order
dependencies.forEach((dep, i) => {
asset.dependencies.set(dep.name, dep);
let assetDep = assetDeps[i];
if (assetDep) {
asset.depAssets.set(dep, assetDep);
dep.resolved = assetDep.name;
}
});
logger.verbose(`Built ${asset.relativeName}...`);
if (this.cache && cacheMiss) {
this.cache.write(asset.name, processed);
}
}
这里主要做了如下几件事:
-
this.farm.run(asset.name),其实就是调用了/src/pipeline.js中Pipeline类的processAsset方法,执行asset.process()对asset进行处理 - 对该Asset的依赖执行
resolveDep和this.loadAsset(assetDep),获取依赖的asset - 将所有依赖的asset放在asset.depAssets中进行记录
到此为止,Asset的树结构已经构建完成,构建的过程就是一个递归的操作,对本身进行process,然后递归的对其依赖进行process,最终形成asset tree。
注意,有一些细节点后面会进行详细介绍,比如上述this.farm是子进程管理的实例,可以利用多进程加快构建的速度;收集依赖的过程会根据文件类型的不同而不同。
this.farm也是一个代理模式的应用。
构建Bundle Tree
构建Bundle Tree的主要逻辑也在Bundler.Bundle()中,代码如下:
// Create a root bundle to hold all of the entry assets, and add them to the tree.
this.mainBundle = new Bundle();
for (let asset of this.entryAssets) {
this.createBundleTree(asset, this.mainBundle);
}
这里主要做了如下几件事:
- 创建一个根bundle
- 利用
this.createBundleTree方法将所有的入口asset加入到根bundle中
Bundle类
Bundle类是文件束的类,每个Bundle表示一个大包后的文件,其中包含子assets、childBundle等属性,代码如下:
class Bundle {
constructor(type, name, parent, options = {}) {
this.type = type;
this.name = name;
this.parentBundle = parent;
this.entryAsset = null;
this.assets = new Set();
this.childBundles = new Set();
this.siblingBundles = new Set();
this.siblingBundlesMap = new Map();
......
}
static createWithAsset(asset, parentBundle, options) {
let bundle = new Bundle(
asset.type,
Path.join(asset.options.outDir, asset.generateBundleName()),
parentBundle,
options
);
bundle.entryAsset = asset;
bundle.addAsset(asset);
return bundle;
}
addAsset(asset) {
asset.bundles.add(this);
this.assets.add(asset);
}
......
getSiblingBundle(type) {
if (!type || type === this.type) {
return this;
}
if (!this.siblingBundlesMap.has(type)) {
let bundle = new Bundle(
type,
Path.join(
Path.dirname(this.name),
// keep the original extension for source map files, so we have
// .js.map instead of just .map
type === 'map'
? Path.basename(this.name) + '.' + type
: Path.basename(this.name, Path.extname(this.name)) + '.' + type
),
this
);
this.childBundles.add(bundle);
this.siblingBundles.add(bundle);
this.siblingBundlesMap.set(type, bundle);
}
return this.siblingBundlesMap.get(type);
}
createChildBundle(entryAsset, options = {}) {
let bundle = Bundle.createWithAsset(entryAsset, this, options);
this.childBundles.add(bundle);
return bundle;
}
createSiblingBundle(entryAsset, options = {}) {
let bundle = this.createChildBundle(entryAsset, options);
this.siblingBundles.add(bundle);
return bundle;
}
......
async package(bundler, oldHashes, newHashes = new Map()) {
let promises = [];
let mappings = [];
if (!this.isEmpty) {
let hash = this.getHash();
newHashes.set(this.name, hash);
if (!oldHashes || oldHashes.get(this.name) !== hash) {
promises.push(this._package(bundler));
}
}
for (let bundle of this.childBundles.values()) {
if (bundle.type === 'map') {
mappings.push(bundle);
} else {
promises.push(bundle.package(bundler, oldHashes, newHashes));
}
}
await Promise.all(promises);
for (let bundle of mappings) {
await bundle.package(bundler, oldHashes, newHashes);
}
return newHashes;
}
async _package(bundler) {
let Packager = bundler.packagers.get(this.type);
let packager = new Packager(this, bundler);
let startTime = Date.now();
await packager.setup();
await packager.start();
let included = new Set();
for (let asset of this.assets) {
await this._addDeps(asset, packager, included);
}
await packager.end();
this.totalSize = packager.getSize();
let assetArray = Array.from(this.assets);
let assetStartTime =
this.type === 'map'
? 0
: assetArray.sort((a, b) => a.startTime - b.startTime)[0].startTime;
let assetEndTime =
this.type === 'map'
? 0
: assetArray.sort((a, b) => b.endTime - a.endTime)[0].endTime;
let packagingTime = Date.now() - startTime;
this.bundleTime = assetEndTime - assetStartTime + packagingTime;
}
async _addDeps(asset, packager, included) {
if (!this.assets.has(asset) || included.has(asset)) {
return;
}
included.add(asset);
for (let depAsset of asset.depAssets.values()) {
await this._addDeps(depAsset, packager, included);
}
await packager.addAsset(asset);
const assetSize = packager.getSize() - this.totalSize;
if (assetSize > 0) {
this.addAssetSize(asset, assetSize);
}
}
......
}
- Bundle具有assets、childBundle等属性,同时拥有addAsset方法来注册asset,createChildBundle方法用来创建子bundle来构建bundle tree。
- Bundle出了具有构建bundle tree能力外,还有package方法,可以递归的调用bundle tree中各个bundle的package方法,进行打包操作
Bundler.createBundleTree
Bundler.createBundleTree()是创建Bundle tree的主要方法,其目的是将入口的asset加入到根bundle中,代码如下:
createBundleTree(asset, bundle, dep, parentBundles = new Set()) {
if (dep) {
asset.parentDeps.add(dep);
}
if (asset.parentBundle && !bundle.isolated) {
// If the asset is already in a bundle, it is shared. Move it to the lowest common ancestor.
if (asset.parentBundle !== bundle) {
let commonBundle = bundle.findCommonAncestor(asset.parentBundle);
// If the common bundle's type matches the asset's, move the asset to the common bundle.
// Otherwise, proceed with adding the asset to the new bundle below.
if (asset.parentBundle.type === commonBundle.type) {
this.moveAssetToBundle(asset, commonBundle);
return;
}
} else {
return;
}
// Detect circular bundles
if (parentBundles.has(asset.parentBundle)) {
return;
}
}
......
// If the asset generated a representation for the parent bundle type, and this
// is not an async import, add it to the current bundle
if (bundle.type && asset.generated[bundle.type] != null && !dep.dynamic) {
bundle.addAsset(asset);
}
if ((dep && dep.dynamic) || !bundle.type) {
// If the asset is already the entry asset of a bundle, don't create a duplicate.
if (isEntryAsset) {
return;
}
// Create a new bundle for dynamic imports
bundle = bundle.createChildBundle(asset, dep);
} else if (
asset.type &&
!this.packagers.get(asset.type).shouldAddAsset(bundle, asset)
) {
// If the asset is already the entry asset of a bundle, don't create a duplicate.
if (isEntryAsset) {
return;
}
// No packager is available for this asset type, or the packager doesn't support
// combining this asset into the bundle. Create a new bundle with only this asset.
bundle = bundle.createSiblingBundle(asset, dep);
} else {
// Add the asset to the common bundle of the asset's type
bundle.getSiblingBundle(asset.type).addAsset(asset);
}
// Add the asset to sibling bundles for each generated type
if (asset.type && asset.generated[asset.type]) {
for (let t in asset.generated) {
if (asset.generated[t]) {
bundle.getSiblingBundle(t).addAsset(asset);
}
}
}
asset.parentBundle = bundle;
parentBundles.add(bundle);
for (let [dep, assetDep] of asset.depAssets) {
this.createBundleTree(assetDep, bundle, dep, parentBundles);
}
parentBundles.delete(bundle);
return bundle;
}
这里主要做了如下几件事:
- 处理重复打包,如果重复则走另外一块逻辑,下面详细介绍
- 如果bundle的类型在asset.generated中有对应项并且文件不是动态引入的,将asset加入到bundle的assets属性中
- 如果文件是动态引入的或者是初始的根bundle(没有type),创建一个子bundle来容纳该asset,同时将当前bundle赋值为新创建的子bundle
- 将asset.generated其他类型的产出加入到该bundle的兄弟bundle中
- 遍历asset的依赖depAsset,递归的创建bundle tree,同时将当前bundle作为根bundle传入到
Bundler.createBundleTree中
这里需要注意的是如何判断是否重复打包呢?
if (asset.parentBundle) {
// If the asset is already in a bundle, it is shared. Move it to the lowest common ancestor.
if (asset.parentBundle !== bundle) {
let commonBundle = bundle.findCommonAncestor(asset.parentBundle);
if (
asset.parentBundle !== commonBundle &&
asset.parentBundle.type === commonBundle.type
) {
this.moveAssetToBundle(asset, commonBundle);
return;
}
} else return;
}
- 如果一个资源的parentBundle已经存在但是不等于此次正在对它进行打包的bundle,那么将其转移到最近的公共父bundle中,避免一份代码重复的打包到了两份bundle中
- 如果一个资源的parentBundle已经存在并且等于此次正在对它进行打包的bundle,说明他已经被打包过了,则直接跳过接下来的打包程序。
Package
打包(package)的入口逻辑Bundler.Bundle()中,代码如下:
// Package everything up
this.bundleHashes = await this.mainBundle.package(
this,
this.bundleHashes
);
这段代码就是调用了mainBundle.package方法,从根bundle开始进行打包
bundle.package
构建好bundle tree之后,从根bundle开始,递归的调用每个bundle的package方法,进行打包操作,Bundle.package()的代码如下:
async package(bundler, oldHashes, newHashes = new Map()) {
let promises = [];
let mappings = [];
if (!this.isEmpty) {
let hash = this.getHash();
newHashes.set(this.name, hash);
if (!oldHashes || oldHashes.get(this.name) !== hash) {
promises.push(this._package(bundler));
}
}
for (let bundle of this.childBundles.values()) {
if (bundle.type === 'map') {
mappings.push(bundle);
} else {
promises.push(bundle.package(bundler, oldHashes, newHashes));
}
}
await Promise.all(promises);
for (let bundle of mappings) {
await bundle.package(bundler, oldHashes, newHashes);
}
return newHashes;
}
这里主要做了如下几件事:
- 获取bundle的hash值(利用bundle中包含的asset的hash值来获取),只有在旧的hash值不存在或者新的hash值不等于旧的hash值的时候,才进行package操作
- 从根节点开始,递归的调用每个bundle的package方法进行打包操作
- 根据bundle类型(打包文件类型)找到对应的打包资源处理类(Packager),然后调用
Packager.addAsset(asset)方法将asset generate出的内容写入目标文件流 - 每个bundle实例都会生成一个最终的打包文件
- 根据bundle类型(打包文件类型)找到对应的打包资源处理类(Packager),然后调用
Packager
Packager根据bundle类型不同而有不同的Packager子类,使用者通过PackagerRegistry进行注册和获取某个类型的Packager。
基类代码如下:
class Packager {
constructor(bundle, bundler) {
this.bundle = bundle;
this.bundler = bundler;
this.options = bundler.options;
}
static shouldAddAsset() {
return true;
}
async setup() {
// Create sub-directories if needed
if (this.bundle.name.includes(path.sep)) {
await mkdirp(path.dirname(this.bundle.name));
}
this.dest = fs.createWriteStream(this.bundle.name);
this.dest.write = promisify(this.dest.write.bind(this.dest));
this.dest.end = promisify(this.dest.end.bind(this.dest));
}
async write(string) {
await this.dest.write(string);
}
......
}
我们主要关注其setup和write方法即可,两个方法分别是创建文件写流、向文件中写入字符串。
子类的话我们以JSPackager为例,代码如下:
class JSPackager extends Packager {
async start() {
this.first = true;
this.dedupe = new Map();
this.bundleLoaders = new Set();
this.externalModules = new Set();
let preludeCode = this.options.minify ? prelude.minified : prelude.source;
if (this.options.target === 'electron') {
preludeCode =
`process.env.HMR_PORT=${
this.options.hmrPort
};process.env.HMR_HOSTNAME=${JSON.stringify(
this.options.hmrHostname
)};` + preludeCode;
}
await this.write(preludeCode + '({');
this.lineOffset = lineCounter(preludeCode);
}
async addAsset(asset) {
// If this module is referenced by another JS bundle, it needs to be exposed externally.
// In that case, don't dedupe the asset as it would affect the module ids that are referenced by other bundles.
let isExposed = !Array.from(asset.parentDeps).every(dep => {
let depAsset = this.bundler.loadedAssets.get(dep.parent);
return this.bundle.assets.has(depAsset) || depAsset.type !== 'js';
});
if (!isExposed) {
let key = this.dedupeKey(asset);
if (this.dedupe.has(key)) {
return;
}
// Don't dedupe when HMR is turned on since it messes with the asset ids
if (!this.options.hmr) {
this.dedupe.set(key, asset.id);
}
}
......
this.bundle.addOffset(asset, this.lineOffset);
await this.writeModule(
asset.id,
asset.generated.js,
deps,
asset.generated.map
);
}
......
async end() {
let entry = [];
// Add the HMR runtime if needed.
if (this.options.hmr) {
let asset = await this.bundler.getAsset(
require.resolve('../builtins/hmr-runtime')
);
await this.addAssetToBundle(asset);
entry.push(asset.id);
}
if (await this.writeBundleLoaders()) {
entry.push(0);
}
if (this.bundle.entryAsset && this.externalModules.size === 0) {
entry.push(this.bundle.entryAsset.id);
}
await this.write(
'},{},' +
JSON.stringify(entry) +
', ' +
JSON.stringify(this.options.global || null) +
')'
);
if (this.options.sourceMaps) {
// Add source map url if a map bundle exists
let mapBundle = this.bundle.siblingBundlesMap.get('map');
if (mapBundle) {
let mapUrl = urlJoin(
this.options.publicURL,
path.basename(mapBundle.name)
);
await this.write(`\n//# sourceMappingURL=${mapUrl}`);
}
}
await super.end();
}
}
这里主要关注上述几个方法:
-
start,将预设的前端模块加载器(后面会详述)代码写入目标文件 -
addAsset,将asset.generated.js及其依赖模块的id按模块加载器所需格式写入目标文件 -
end,将hmr所需的客户端代码和sourceMaps url写入目标文件,对于动态引入的模块,需要把响应的loader注册代码写入文件。
周边技术点
如何收集依赖
我们在上述的Asset处理时,有一个步骤是收集依赖(collectDependencies),这个步骤根据不同的文件类型处理方式会有不同,我们下面以JSAsset为例讲解一下。
- 首先在pretransform阶段中,JSAsset利用
@babel/core生成ast,代码在/transforms/babel/babel7.js中,
let res;
if (asset.ast) {
res = babel.transformFromAst(asset.ast, asset.contents, config);
} else {
res = babel.transformSync(asset.contents, config);
}
if (res.ast) {
asset.ast = res.ast;
asset.isAstDirty = true;
}
- 遍历AST中的每个节点,收集依赖
遍历AST的过程由babylon-walk进行控制,代码如下:
const walk = require('babylon-walk');
collectDependencies() {
walk.ancestor(this.ast, collectDependencies, this);
}
其中collectDependencies对应的是babel visitors,简单来说,在遇到某类型的节点时,就会触发某类型的visitors,我们可以控制进入节点或退出节点的处理逻辑。
在看用于收集依赖的visitor之前,先了解下ES6 module和nodejs的模块系统的几种导入导出方式以及对应在抽象语法树中代表的declaration类型:
// ImportDeclaration
import { stat, exists, readFile } from 'fs';
// ExportNamedDeclaration with node.source = null;
export var year = 1958;
// ExportNamedDeclaration with node.source = null;
export default function () {
console.log('foo');
}
// ExportNamedDeclaration with node.source.value = 'my_module';
export { foo, bar } from 'my_module';
// CallExpression with node.Callee.name is require;
// CallExpression with node.Callee.arguments[0] is the 'react';
import('react').then(...)
// CallExpression with node.Callee.name is require;
// CallExpression with node.Callee.arguments[0] is the 'react';
var react = require('react');
除了上述这些依赖引入方式之外,还有两种比较特殊的方式:
// web Worker
new Worker('sw.js')
// service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-test/sw.js', { scope: '/sw-test/' }).then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}
下面我们正式来看collectDependencies对应的viditors,代码如下:
module.exports = {
ImportDeclaration(node, asset) {
asset.isES6Module = true;
addDependency(asset, node.source);
},
ExportNamedDeclaration(node, asset) {
asset.isES6Module = true;
if (node.source) {
addDependency(asset, node.source);
}
},
ExportAllDeclaration(node, asset) {
asset.isES6Module = true;
addDependency(asset, node.source);
},
ExportDefaultDeclaration(node, asset) {
asset.isES6Module = true;
},
CallExpression(node, asset) {
let {callee, arguments: args} = node;
let isRequire =
types.isIdentifier(callee) &&
callee.name === 'require' &&
args.length === 1 &&
types.isStringLiteral(args[0]);
if (isRequire) {
addDependency(asset, args[0]);
return;
}
let isDynamicImport =
callee.type === 'Import' &&
args.length === 1 &&
types.isStringLiteral(args[0]);
if (isDynamicImport) {
asset.addDependency('_bundle_loader');
addDependency(asset, args[0], {dynamic: true});
node.callee = requireTemplate().expression;
node.arguments[0] = argTemplate({MODULE: args[0]}).expression;
asset.isAstDirty = true;
return;
}
const isRegisterServiceWorker =
types.isStringLiteral(args[0]) &&
matchesPattern(callee, serviceWorkerPattern);
if (isRegisterServiceWorker) {
addURLDependency(asset, args[0]);
return;
}
},
NewExpression(node, asset) {
const {callee, arguments: args} = node;
const isWebWorker =
callee.type === 'Identifier' &&
callee.name === 'Worker' &&
args.length === 1 &&
types.isStringLiteral(args[0]);
if (isWebWorker) {
addURLDependency(asset, args[0]);
return;
}
}
};
我们可以看到,每次遇到引入模块,就会调用addDependency,这里对动态引入(import())的处理稍微特殊一点,我们下面会详细介绍。
前端模块加载器
我们先来看一下构建好的js bundle的内容:
// modules are defined as an array
// [ module function, map of requires ]
//
// map of requires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the require for previous bundles
// eslint-disable-next-line no-global-assign
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]) {
if (!modules[name]) {
// if we cannot find the module within our internal map or
// cache jump to the current global require ie. the last bundle
// that was added to the page.
var currentRequire = typeof parcelRequire === 'function' && parcelRequire;
if (!jumped && currentRequire) {
return currentRequire(name, true);
}
// If there are other bundles on this page the require from the
// previous one is saved to 'previousRequire'. Repeat this as
// many times as there are bundles until the module is found or
// we exhaust the require chain.
if (previousRequire) {
return previousRequire(name, true);
}
// Try the node require function if it exists.
if (nodeRequire && typeof name === 'string') {
return nodeRequire(name);
}
var err = new Error('Cannot find module \'' + name + '\'');
err.code = 'MODULE_NOT_FOUND';
throw err;
}
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][1][x] || x;
}
}
function Module(moduleName) {
this.id = moduleName;
this.bundle = newRequire;
this.exports = {};
}
newRequire.isParcelRequire = true;
newRequire.Module = Module;
newRequire.modules = modules;
newRequire.cache = cache;
newRequire.parent = previousRequire;
newRequire.register = function (id, exports) {
modules[id] = [function (require, module) {
module.exports = exports;
}, {}];
};
for (var i = 0; i < entry.length; i++) {
newRequire(entry[i]);
}
if (entry.length) {
// Expose entry point to Node, AMD or browser globals
// Based on https://github.com/ForbesLindesay/umd/blob/master/template.js
var mainExports = newRequire(entry[entry.length - 1]);
// CommonJS
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = mainExports;
// RequireJS
} else if (typeof define === "function" && define.amd) {
define(function () {
return mainExports;
});
// <script>
} else if (globalName) {
this[globalName] = mainExports;
}
}
// Override the current require with this new one
return newRequire;
})({"a.js":[function(require,module,exports) {
var name = 'tsy'; // console.log(Buffer);
module.exports = name;
},{}],"index.js":[function(require,module,exports) {
var a = require('./a.js');
console.log(a);
},{"./a.js":"a.js"}]},{},["index.js"], null)
//# sourceMappingURL=/parcel-demo.e31bb0bc.js.map
我们可以看到这是一个立即执行的函数,参数有modules、cache、entry、globalName
-
modules为当前bandle中包含的所有模块,也就是上面提到的Bundle类中的assets,modules的类型为一个对象,key是模块名称,value是一个数组,数组第一项为包装过的模块内容,第二项是依赖的模块信息。比如如下内容
{"a.js":[function(require,module,exports) {
var name = 'tsy'; // console.log(Buffer);
module.exports = name;
},{}],"index.js":[function(require,module,exports) {
var a = require('./a.js');
console.log(a);
},{"./a.js":"a.js"}]}
-
entry为该bundle的入口文件
下面我们来看下该立即执行函数的主要逻辑是便利入口文件,调用newRequire方法:
function newRequire(name, jumped) {
if (!cache[name]) {
if (!modules[name]) {
// if we cannot find the module within our internal map or
// cache jump to the current global require ie. the last bundle
// that was added to the page.
var currentRequire = typeof parcelRequire === 'function' && parcelRequire;
if (!jumped && currentRequire) {
return currentRequire(name, true);
}
// If there are other bundles on this page the require from the
// previous one is saved to 'previousRequire'. Repeat this as
// many times as there are bundles until the module is found or
// we exhaust the require chain.
if (previousRequire) {
return previousRequire(name, true);
}
// Try the node require function if it exists.
if (nodeRequire && typeof name === 'string') {
return nodeRequire(name);
}
var err = new Error('Cannot find module \'' + name + '\'');
err.code = 'MODULE_NOT_FOUND';
throw err;
}
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][1][x] || x;
}
}
function Module(moduleName) {
this.id = moduleName;
this.bundle = newRequire;
this.exports = {};
}
每一个文件就是一个模块,在每个模块中,都会有一个module对象,这个对象就指向当前的模块。Parcel中的module对象具有以下属性:
- id:当前模块的名称
- bundle:newRequire方法
- exports:当前模块暴露给外部的值
newRequire方法的逻辑如下:
- 判断模块对象是否已被缓存
- 如果是,直接
return cache[name].exports - 如果没有,判断modules[name]是否存在
- 如果存在,调用
var module = cache[name] = new newRequire.Module(name); modules[name][0].call(module.exports, localRequire, module, module.exports, this);,缓存模块对象,并执行该模块 - 如果不存在,则一次尝试调用其他bundle的parcelRequire(previousRequire)、node的require
- 如果存在,调用
- 如果是,直接
在执行模块时,会将localRequire, module, module.exports作为形参,我们在模块中可以直接使用的require、module、exports即为执行该模块时传入的对应参数。
总结一下,我们利用函数把一个个模块封装起来,并给其提供 require和exports 的接口和一套模块规范,这样在不支持模块机制的浏览器环境中,我们也能够不去污染全局变量,体验到模块化带来的优势。
动态引入
我们接着来看动态引入,在上面JSAsset的collectDependencies中,已经有所提及。
我们首先看下在js遍历节点的过程中,遇到动态引入的情况如何处理:
if (isDynamicImport) {
asset.addDependency('_bundle_loader');
addDependency(asset, args[0], {dynamic: true});
node.callee = requireTemplate().expression;
node.arguments[0] = argTemplate({MODULE: args[0]}).expression;
asset.isAstDirty = true;
return;
}
这里我们可以看出,如果碰到Import()导入的资源, 直接将_bundle_loader加入其依赖列表,同时对表达式进行处理。根据上面代码,在ast中如果遇到import('./a.js')这段动态引入的代码, 会被直接替换为require('_bundle_loader')(require.resolve('./a.js'))。
这里插一段背景,这种动态资源由于设置了dynamic: true,在后见bundle tree的时候,会单独生成一个bundle作为当前bundle的child bundle,同时在当前bundle中记录动态资源的信息。最后在当前的bundle中得到的打包资源数组,比如[md5(dynamicAsset).js, md5(cssWithDynamicAsset).css, ..., assetId], 由打包之后的文件名和该模块的id所组成.
根据上述前端模块加载器部分的介绍,require.resolve('./a.js')实际上获取的是./a.js模块的id,代码如下:
function resolve(x){
return modules[name][1][x] || x;
}
_bundle_loader是Parcel-bundler的内置模块,位于/src/builtins/bundle-loader.js中,代码如下:
var getBundleURL = require('./bundle-url').getBundleURL;
function loadBundlesLazy(bundles) {
if (!Array.isArray(bundles)) {
bundles = [bundles]
}
var id = bundles[bundles.length - 1];
try {
return Promise.resolve(require(id));
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
return new LazyPromise(function (resolve, reject) {
loadBundles(bundles.slice(0, -1))
.then(function () {
return require(id);
})
.then(resolve, reject);
});
}
throw err;
}
}
function loadBundles(bundles) {
return Promise.all(bundles.map(loadBundle));
}
var bundleLoaders = {};
function registerBundleLoader(type, loader) {
bundleLoaders[type] = loader;
}
module.exports = exports = loadBundlesLazy;
exports.load = loadBundles;
exports.register = registerBundleLoader;
var bundles = {};
function loadBundle(bundle) {
var id;
if (Array.isArray(bundle)) {
id = bundle[1];
bundle = bundle[0];
}
if (bundles[bundle]) {
return bundles[bundle];
}
var type = (bundle.substring(bundle.lastIndexOf('.') + 1, bundle.length) || bundle).toLowerCase();
var bundleLoader = bundleLoaders[type];
if (bundleLoader) {
return bundles[bundle] = bundleLoader(getBundleURL() + bundle)
.then(function (resolved) {
if (resolved) {
module.bundle.register(id, resolved);
}
return resolved;
}).catch(function(e) {
delete bundles[bundle];
throw e;
});
}
}
function LazyPromise(executor) {
this.executor = executor;
this.promise = null;
}
LazyPromise.prototype.then = function (onSuccess, onError) {
if (this.promise === null) this.promise = new Promise(this.executor)
return this.promise.then(onSuccess, onError)
};
LazyPromise.prototype.catch = function (onError) {
if (this.promise === null) this.promise = new Promise(this.executor)
return this.promise.catch(onError)
};
其中loadBundlesLazy方法首先直接去require模块,如果没有的话,调用loadBundles加载后再去require。
loadBundles方法对每个模块调用loadBundle方法,loadBundle根据bundle类型获取相应的loader动态加载对应的bundle(被动态引入的模块会作为一个新的bundle),加载完成后注册到该bundle的modules中,这样后面的require就可以利用modules[name]获取到该模块了。
bundler loader在上述bundle的package.end()中将注册bundler loader的逻辑写入bundle,代码如下(JSPackager为例):
// Generate a module to register the bundle loaders that are needed
let loads = 'var b=require(' + JSON.stringify(bundleLoader.id) + ');';
for (let bundleType of this.bundleLoaders) {
let loader = this.options.bundleLoaders[bundleType];
if (loader) {
let target = this.options.target === 'node' ? 'node' : 'browser';
let asset = await this.bundler.getAsset(loader[target]);
await this.addAssetToBundle(asset);
loads +=
'b.register(' +
JSON.stringify(bundleType) +
',require(' +
JSON.stringify(asset.id) +
'));';
}
}
这段代码最终会在在modules中加入:
0:[function(require,module,exports) {
var b=require("../parcel/packages/core/parcel-bundler/src/builtins/bundle-loader.js");b.register("js",require("../parcel/packages/core/parcel-bundler/src/builtins/loaders/browser/js-loader.js"));
},{}]
同时将0这个模块加入到bundle的入口中(开始就会执行),这样在loadBundle就可以获取到对应的loader用于动态加载模块,以js-loader为例:
module.exports = function loadJSBundle(bundle) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.charset = 'utf-8';
script.src = bundle;
script.onerror = function (e) {
script.onerror = script.onload = null;
reject(e);
};
script.onload = function () {
script.onerror = script.onload = null;
resolve();
};
document.getElementsByTagName('head')[0].appendChild(script);
});
};
在加载完资源后,我们又利用了module.bundle.register(id, resolved);注册到当前bundle的modules中,注册的代码在前端模块加载那里已经提及,代码如下:
newRequire.register = function (id, exports) {
modules[id] = [function (require, module) {
module.exports = exports;
}, {}];
};
这样,我们利用require就可以直接获取到动态加载的资源了。
Worker
Parcel利用子进程来加快构建Asset Tree的速度,特别是编译生成AST的阶段。其最终调用的是node的child_process,但前面还有一些进程管理的工作,我们下面来探究一下。
worker在/src/bundler.js中load asset(this.farm.run())时使用,在start中被定义,我们来看下如何定义:
this.farm = await WorkerFarm.getShared(this.options, {
workerPath: require.resolve('./worker.js')
});
这里传入了一些配置参数和workerPath,workerPath对应的模块中实现了init和run接口,后面在worker中会被使用,这也是面向接口编程的体现。
worker主要的代码在@parcel/workers中,worker中重要有三个类,WorkerFarm、Worker、Child。
-
WorkerFarm是worker的入口,用来管理所有的子进程 -
Worker类用来管理单个子进程,具有fork、回调处理等能力 -
Child为子进程中执行的模块,在其中通过IPC 通信信道来接受父进程发送的命令,执行对应对应模块的方法,我们这里就是执行./worker.js中的对应方法,执行后通过信道将结果传递给父进程Worker。
这里的父进程向子进程发送命令,应用了设计模式中的命令模式。
监听文件变化
监听文件变化同样是根据子进程对文件进行监听,但这里的子进程管理就比较简单了,创建一个子进程,然后发动命令就可以了,子进程中通过chokidar对文件进行监听,如果发现文件变化,发送消息给父进程,父进程出发相应的事件。
handleEmit(event, data) {
if (event === 'watcherError') {
data = errorUtils.jsonToError(data);
}
this.emit(event, data);
}
HMR
HMR通过WebSocket来实现,具有服务端和客户端两部分逻辑。
服务端逻辑(/src/HMRServer.js):
class HMRServer {
async start(options = {}) {
await new Promise(async resolve => {
if (!options.https) {
this.server = http.createServer();
} else if (typeof options.https === 'boolean') {
this.server = https.createServer(generateCertificate(options));
} else {
this.server = https.createServer(await getCertificate(options.https));
}
let websocketOptions = {
server: this.server
};
if (options.hmrHostname) {
websocketOptions.origin = `${options.https ? 'https' : 'http'}://${
options.hmrHostname
}`;
}
this.wss = new WebSocket.Server(websocketOptions);
this.server.listen(options.hmrPort, resolve);
});
this.wss.on('connection', ws => {
ws.onerror = this.handleSocketError;
if (this.unresolvedError) {
ws.send(JSON.stringify(this.unresolvedError));
}
});
this.wss.on('error', this.handleSocketError);
return this.wss._server.address().port;
}
......
emitUpdate(assets) {
if (this.unresolvedError) {
this.unresolvedError = null;
this.broadcast({
type: 'error-resolved'
});
}
const shouldReload = assets.some(asset => asset.hmrPageReload);
if (shouldReload) {
this.broadcast({
type: 'reload'
});
} else {
this.broadcast({
type: 'update',
assets: assets.map(asset => {
let deps = {};
for (let [dep, depAsset] of asset.depAssets) {
deps[dep.name] = depAsset.id;
}
return {
id: asset.id,
generated: asset.generated,
deps: deps
};
})
});
}
}
......
broadcast(msg) {
const json = JSON.stringify(msg);
for (let ws of this.wss.clients) {
ws.send(json);
}
}
}
这里的start方法用来创建WebSocket server,当有asset更新时,触发emitUpdate将asset id、asset 内容发送给客户端。
客户端逻辑:
var OVERLAY_ID = '__parcel__error__overlay__';
var OldModule = module.bundle.Module;
function Module(moduleName) {
OldModule.call(this, moduleName);
this.hot = {
data: module.bundle.hotData,
_acceptCallbacks: [],
_disposeCallbacks: [],
accept: function (fn) {
this._acceptCallbacks.push(fn || function () {});
},
dispose: function (fn) {
this._disposeCallbacks.push(fn);
}
};
module.bundle.hotData = null;
}
module.bundle.Module = Module;
var parent = module.bundle.parent;
if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') {
var hostname = process.env.HMR_HOSTNAME || location.hostname;
var protocol = location.protocol === 'https:' ? 'wss' : 'ws';
var ws = new WebSocket(protocol + '://' + hostname + ':' + process.env.HMR_PORT + '/');
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
if (data.type === 'update') {
console.clear();
data.assets.forEach(function (asset) {
hmrApply(global.parcelRequire, asset);
});
data.assets.forEach(function (asset) {
if (!asset.isNew) {
hmrAccept(global.parcelRequire, asset.id);
}
});
}
if (data.type === 'reload') {
ws.close();
ws.onclose = function () {
location.reload();
}
}
if (data.type === 'error-resolved') {
console.log('[parcel] ✨ Error resolved');
removeErrorOverlay();
}
if (data.type === 'error') {
console.error('[parcel] 🚨 ' + data.error.message + '\n' + data.error.stack);
removeErrorOverlay();
var overlay = createErrorOverlay(data);
document.body.appendChild(overlay);
}
};
}
......
function hmrApply(bundle, asset) {
var modules = bundle.modules;
if (!modules) {
return;
}
if (modules[asset.id] || !bundle.parent) {
var fn = new Function('require', 'module', 'exports', asset.generated.js);
asset.isNew = !modules[asset.id];
modules[asset.id] = [fn, asset.deps];
} else if (bundle.parent) {
hmrApply(bundle.parent, asset);
}
}
function hmrAccept(bundle, id) {
var modules = bundle.modules;
if (!modules) {
return;
}
if (!modules[id] && bundle.parent) {
return hmrAccept(bundle.parent, id);
}
var cached = bundle.cache[id];
bundle.hotData = {};
if (cached) {
cached.hot.data = bundle.hotData;
}
if (cached && cached.hot && cached.hot._disposeCallbacks.length) {
cached.hot._disposeCallbacks.forEach(function (cb) {
cb(bundle.hotData);
});
}
delete bundle.cache[id];
bundle(id);
cached = bundle.cache[id];
if (cached && cached.hot && cached.hot._acceptCallbacks.length) {
cached.hot._acceptCallbacks.forEach(function (cb) {
cb();
});
return true;
}
return getParents(global.parcelRequire, id).some(function (id) {
return hmrAccept(global.parcelRequire, id)
});
}
这里主要创建了Websocket Client,监听update消息,如果有,则替换modules中的对应内容,同时利用global.parcelRequire重新执行模块。