本篇文章是对 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
越发 细致解说与代码完成