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);
}
}
// 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;
}
......
}
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);
}
}
// 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);
}
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;
}
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;
}
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();
}
}
// 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);
});
}
// 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
{"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 = {};
}
// 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"));
},{}]
类
Parcel中主要包含上述类:
它们直接的调用及继承关系如下:
打包流程
打包的整体过程就在
Bundler.bundle()
方法中,代码如下:这里主要做了如下几件事:
下面我们一步一步的讲解:
准备工作
准备工作主要在
Bundler.start()
中,代码如下:这里主要做了如下几件事
加载Parcel插件
加载Parcel插件的代码如下:
加载插件步骤如下:
parcel-plugin-
格式的依赖localRequire
方法进行加载,localRequire
获取到文件路径并缓存,然后做require
操作(如果没有安装该npm包,则会调用npm / yarm install
进行安装)。localRequire
可以说是一个代理模式,代理了对文件的访问注意,这里的
localRequire
就是一个代理模式
,中间加入了缓存机制,控制了模块的访问。监听文件变化和HMR后面会进行介绍。
构建Asset Tree
构建Asset Tree的主要逻辑在
Bundler.Bundle()
方法中,代码如下:这里主要做了如下几件事:
buildQueue.run()
Asset类
首先说明下Asset,Asset是文件资源类,与文件保持一对一的关系,Asset基类代码如下:
这里主要关注下process方法,也就是文件的文件资源的处理过程:
[this.type]: this.contents
注意,这里不同的子类会继承自此基类,实现基类暴露的接口,这其实就是
针对接口编程
的设计原则。收集依赖的过程会在下面进行详细介绍。
Bundler.resolveAsset
根据文件后缀获取到入口文件对应的Asset实例的逻辑
Bundler.resolveAsset
中,代码如下:主要做了如下两件事:
这里简单说下Parser,Parser可以说是Asset的注册表,根据类型存储对应的Asset实例,
parser.getAsset
方法根据文件路径获取对应的Asset实例。buildQueue.run
buildQueue是PromiseQueue的实例,
PromiseQueue.run
方法将对列中的内容一次通过process函数处理。PromiseQueue有兴趣大家可以去看下代码,这里不在赘述。buildQueue的初始化代码在Bundler的constructor中,代码如下:
在我们上述的场景中,执行逻辑就是对所有的入口文件对应的Asset,执行
Bundler.processAsset(Asset)
。Bundler.processAsset()
最终调用的是Bundler.loadAsset()
方法,代码如下:这里主要做了如下几件事:
this.farm.run(asset.name)
,其实就是调用了/src/pipeline.js
中Pipeline类的processAsset
方法,执行asset.process()
对asset进行处理resolveDep
和this.loadAsset(assetDep)
,获取依赖的asset到此为止,Asset的树结构已经构建完成,构建的过程就是一个递归的操作,对本身进行process,然后递归的对其依赖进行process,最终形成asset tree。
注意,有一些细节点后面会进行详细介绍,比如上述
this.farm
是子进程管理的实例,可以利用多进程加快构建的速度;收集依赖的过程会根据文件类型的不同而不同。this.farm
也是一个代理模式
的应用。构建Bundle Tree
构建Bundle Tree的主要逻辑也在
Bundler.Bundle()
中,代码如下:这里主要做了如下几件事:
this.createBundleTree
方法将所有的入口asset加入到根bundle中Bundle类
Bundle类是文件束的类,每个Bundle表示一个大包后的文件,其中包含子assets、childBundle等属性,代码如下:
Bundler.createBundleTree
Bundler.createBundleTree()
是创建Bundle tree的主要方法,其目的是将入口的asset加入到根bundle中,代码如下:这里主要做了如下几件事:
Bundler.createBundleTree
中这里需要注意的是如何判断是否重复打包呢?
Package
打包(package)的入口逻辑
Bundler.Bundle
()中,代码如下:这段代码就是调用了
mainBundle.package
方法,从根bundle开始进行打包bundle.package
构建好bundle tree之后,从根bundle开始,递归的调用每个bundle的package方法,进行打包操作,
Bundle.package()
的代码如下:这里主要做了如下几件事:
Packager.addAsset(asset)
方法将asset generate出的内容写入目标文件流Packager
Packager根据bundle类型不同而有不同的Packager子类,使用者通过PackagerRegistry进行注册和获取某个类型的Packager。
基类代码如下:
我们主要关注其
setup
和write
方法即可,两个方法分别是创建文件写流、向文件中写入字符串。子类的话我们以JSPackager为例,代码如下:
这里主要关注上述几个方法:
start
,将预设的前端模块加载器(后面会详述)代码写入目标文件addAsset
,将asset.generated.js
及其依赖模块的id按模块加载器所需格式写入目标文件end
,将hmr所需的客户端代码和sourceMaps url写入目标文件,对于动态引入的模块,需要把响应的loader注册代码写入文件。周边技术点
如何收集依赖
我们在上述的Asset处理时,有一个步骤是收集依赖(collectDependencies),这个步骤根据不同的文件类型处理方式会有不同,我们下面以JSAsset为例讲解一下。
@babel/core
生成ast,代码在/transforms/babel/babel7.js
中,遍历AST的过程由
babylon-walk
进行控制,代码如下:其中
collectDependencies
对应的是babel visitors,简单来说,在遇到某类型的节点时,就会触发某类型的visitors,我们可以控制进入节点或退出节点的处理逻辑。在看用于收集依赖的visitor之前,先了解下ES6 module和nodejs的模块系统的几种导入导出方式以及对应在抽象语法树中代表的declaration类型:
除了上述这些依赖引入方式之外,还有两种比较特殊的方式:
下面我们正式来看
collectDependencies
对应的viditors,代码如下:我们可以看到,每次遇到引入模块,就会调用
addDependency
,这里对动态引入(import()
)的处理稍微特殊一点,我们下面会详细介绍。前端模块加载器
我们先来看一下构建好的js bundle的内容:
我们可以看到这是一个立即执行的函数,参数有
modules
、cache
、entry
、globalName
modules
为当前bandle中包含的所有模块,也就是上面提到的Bundle类中的assets,modules
的类型为一个对象,key是模块名称,value是一个数组,数组第一项为包装过的模块内容,第二项是依赖的模块信息。比如如下内容entry
为该bundle的入口文件下面我们来看下该立即执行函数的主要逻辑是便利入口文件,调用
newRequire
方法:每一个文件就是一个模块,在每个模块中,都会有一个module对象,这个对象就指向当前的模块。Parcel中的module对象具有以下属性:
newRequire
方法的逻辑如下:return cache[name].exports
var module = cache[name] = new newRequire.Module(name); modules[name][0].call(module.exports, localRequire, module, module.exports, this);
,缓存模块对象,并执行该模块在执行模块时,会将
localRequire, module, module.exports
作为形参,我们在模块中可以直接使用的require
、module
、exports
即为执行该模块时传入的对应参数。总结一下,我们利用函数把一个个模块封装起来,并给其提供 require和exports 的接口和一套模块规范,这样在不支持模块机制的浏览器环境中,我们也能够不去污染全局变量,体验到模块化带来的优势。
动态引入
我们接着来看动态引入,在上面JSAsset的
collectDependencies
中,已经有所提及。我们首先看下在js遍历节点的过程中,遇到动态引入的情况如何处理:
这里我们可以看出,如果碰到
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,代码如下:_bundle_loader
是Parcel-bundler的内置模块,位于/src/builtins/bundle-loader.js
中,代码如下:其中
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为例):这段代码最终会在在modules中加入:
同时将
0
这个模块加入到bundle的入口中(开始就会执行),这样在loadBundle
就可以获取到对应的loader用于动态加载模块,以js-loader
为例:在加载完资源后,我们又利用了
module.bundle.register(id, resolved);
注册到当前bundle的modules中,注册的代码在前端模块加载
那里已经提及,代码如下:这样,我们利用require就可以直接获取到动态加载的资源了。
Worker
Parcel利用子进程来加快构建Asset Tree的速度,特别是编译生成AST的阶段。其最终调用的是node的
child_process
,但前面还有一些进程管理的工作,我们下面来探究一下。worker在
/src/bundler.js
中load asset(this.farm.run()
)时使用,在start中被定义,我们来看下如何定义:这里传入了一些配置参数和workerPath,workerPath对应的模块中实现了
init
和run
接口,后面在worker中会被使用,这也是面向接口编程的体现。worker主要的代码在
@parcel/workers
中,worker中重要有三个类,WorkerFarm
、Worker
、Child
。WorkerFarm
是worker的入口,用来管理所有的子进程Worker
类用来管理单个子进程,具有fork
、回调处理等能力Child
为子进程中执行的模块,在其中通过IPC 通信信道来接受父进程发送的命令,执行对应对应模块的方法,我们这里就是执行./worker.js
中的对应方法,执行后通过信道将结果传递给父进程Worker
。这里的父进程向子进程发送命令,应用了设计模式中的
命令模式
。监听文件变化
监听文件变化同样是根据子进程对文件进行监听,但这里的子进程管理就比较简单了,创建一个子进程,然后发动命令就可以了,子进程中通过
chokidar
对文件进行监听,如果发现文件变化,发送消息给父进程,父进程出发相应的事件。HMR
HMR通过WebSocket来实现,具有服务端和客户端两部分逻辑。
服务端逻辑(
/src/HMRServer.js
):这里的
start
方法用来创建WebSocket server,当有asset更新时,触发emitUpdate
将asset id、asset 内容发送给客户端。客户端逻辑:
这里主要创建了Websocket Client,监听
update
消息,如果有,则替换modules中的对应内容,同时利用global.parcelRequire
重新执行模块。