您好,登錄后才能下訂單哦!
小編給大家分享一下Parcel源碼的案例分析,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!
本篇文章是對 Parce 的源碼解析,代碼基本架構(gòu)與執(zhí)行流程,在這之前你如果對 parcel 不熟悉可以先到 Parcel官網(wǎng) 了解
介紹
下面是偷懶從官網(wǎng)抄下來的介紹:
極速零配置Web應(yīng)用打包工具極速打包
Parcel 使用 worker 進(jìn)程去啟用多核編譯。同時(shí)有文件系統(tǒng)緩存,即使在重啟構(gòu)建后也能快速再編譯。
將你所有的資源打包
Parcel 具備開箱即用的對 JS, CSS, HTML, 文件 及更多的支持,而且不需要插件。
自動轉(zhuǎn)換
如若有需要,Babel, PostCSS, 和 PostHTML 甚至 node_modules 包會被用于自動轉(zhuǎn)換代碼.
零配置代碼分拆
使用動態(tài) import() 語法, Parcel 將你的輸出文件束(bundles)分拆,因此你只需要在初次加載時(shí)加載你所需要的代碼。
熱模塊替換
Parcel 無需配置,在開發(fā)環(huán)境的時(shí)候會自動在瀏覽器內(nèi)隨著你的代碼更改而去更新模塊。
友好的錯誤日志
當(dāng)遇到錯誤時(shí),Parcel 會輸出 語法高亮的代碼片段,幫助你定位問題。
打包工具 | 時(shí)間 |
---|---|
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"
文件架構(gòu)
|-- assets 資源目錄 繼承自 Asset.js |-- builtins 用于最終構(gòu)建 |-- packagers 打包 |-- scope-hoisting 作用域提升 Tree-Shake |-- transforms 轉(zhuǎn)換代碼為 AST |-- utils 工具 |-- visitors 遍歷 js AST樹 收集依賴等 |-- Asset.js 資源 |-- Bundle.js 用于構(gòu)建 bundle 樹 |-- Bundler.js 主目錄 |-- FSCache.js 緩存 |-- HMRServer.js HMR服務(wù)器提供 WebSocket |-- Parser.js 根據(jù)文件擴(kuò)展名獲取對應(yīng) Asset |-- Pipeline.js 多線程執(zhí)行方法 |-- Resolver.js 解析模塊路徑 |-- Server.js 靜態(tài)資源服務(wù)器 |-- SourceMap.js SourceMap |-- cli.js cli入口 解析命令行參數(shù) |-- worker.js 多線程入口
流程
說明
Parcel是面向資源的,JavaScript,CSS,HTML 這些都是資源,并不是 webpack 中 js 是一等公民,Parcel 會自動的從入口文件開始分析這些文件 和 模塊中的依賴,然后構(gòu)建一個 bundle 樹,并對其進(jìn)行打包輸出到指定目錄
一個簡單的例子
我們從一個簡單的例子開始了解 parcel 內(nèi)部源碼與流程
index.html |-- index.js |-- module1.js |-- module2.js
上面是我們例子的結(jié)構(gòu),入口為 index.html, 在 index.html 中我們用 script 標(biāo)簽引用了 src/index.js,在 index.js 中我們引入了2個子模塊
執(zhí)行
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 解析命令行并調(diào)用 bundle 方法
有 serve, watch, build 3個命令來調(diào)用 bundle 函數(shù),執(zhí)行 pracel index.html 默認(rèn)為 serve,所以調(diào)用的是 bundler.serve 方法
進(jìn)入 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 方法 調(diào)用 serveStatic 起了一個靜態(tài)服務(wù)指向 最終打包的文件夾
下面就是重要的 bundle 方法
bundler.bundle
async bundle() { // 加載插件 設(shè)置env 啟動多線程 watcher hmr await this.start(); if (isInitialBundle) { // 創(chuàng)建 輸出目錄 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); } } // 打包隊(duì)列中的資源 let loadedAssets = await this.buildQueue.run(); // findOrphanAssets 獲取所有資源中獨(dú)立的沒有父Bundle的資源 let changedAssets = [...this.findOrphanAssets(), ...loadedAssets]; // 因?yàn)榻酉聛硪獦?gòu)建 Bundle 樹,先對上一次的 Bundle樹 進(jìn)行 clear 操作 for (let asset of this.loadedAssets.values()) { asset.invalidateBundle(); } // 構(gòu)建 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發(fā)送到瀏覽器 if (this.hmr && !isInitialBundle) { this.hmr.emitUpdate(changedAssets); } // 對資源打包 this.bundleHashes = await this.mainBundle.package( this, this.bundleHashes ); // 將獨(dú)立的資源刪除 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:
開頭的判斷 防止多次執(zhí)行,也就是說 this.start 只會執(zhí)行一次loadPlugins 加載插件,找到 package.json 文件 dependencies, devDependencies 中 parcel-plugin-開頭的插件進(jìn)行調(diào)用loadEnv 加載環(huán)境變量,利用 dotenv, dotenv-expand 包將 env.development.local, .env.development, .env.local, .env 擴(kuò)展至 process.envwatch 初始化監(jiān)聽文件并綁定 change 回調(diào)函數(shù),內(nèi)部 child_process.fork 起一個子進(jìn)程,使用 chokidar 包來監(jiān)聽文件改變hmr 起一個服務(wù),WebSocket 向?yàn)g覽器發(fā)送更改的資源farm 初始化多進(jìn)程并指定 werker 工作文件,開啟多個 child_process 去解析編譯資源
接下來回到 bundle,isInitialBundle 是一個判斷是否是第一次構(gòu)建
fs.mkdirp 創(chuàng)建輸出文件夾
遍歷入口文件,通過 resolveAsset,內(nèi)部調(diào)用 resolver 解析路徑,并 getAsset 獲取到對應(yīng)的 asset(這里我們?nèi)肟谑?index.html,根據(jù)擴(kuò)展名獲取到的是 HTMLAsset)
將 asset 添加進(jìn)隊(duì)列
然后啟動 this.buildQueue.run() 對資源從入口遞歸開始打包
PromiseQueue
這里 buildQueue 是一個 PromiseQueue 異步隊(duì)列
PromiseQueue 在初始化的時(shí)候傳入一個回調(diào)函數(shù) callback,內(nèi)部維護(hù)一個參數(shù)隊(duì)列 queue,add 往隊(duì)列里 push 一個參數(shù),run 的時(shí)候while遍歷隊(duì)列 callback(...queue.shift()),隊(duì)列全部執(zhí)行完畢 Promise 置為完成(resolved)(可以將其理解為 Promise.all)
這里定義的回調(diào)函數(shù)是 processAsset,參數(shù)就是入口文件 index.html 的 HTMLAsset
async processAsset(asset, isRebuild) { if (isRebuild) { asset.invalidate(); if (this.cache) { this.cache.invalidate(asset.name); } } await this.loadAsset(asset); }
processAsset 函數(shù)內(nèi)先判斷是否是 Rebuild ,是第一次構(gòu)建,還是 watch 監(jiān)聽文件改變進(jìn)行的重建,如果是重建則對資源的屬性重置,并使其緩存失效
之后調(diào)用 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; // 解析和加載當(dāng)前資源的依賴項(xiàng) 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 在開始有個判斷防止重復(fù)編譯
之后去讀緩存,讀取失敗就調(diào)用 this.farm.run 在多進(jìn)程里編譯資源
編譯完就去加載并編譯依賴的文件
最后如果是新的資源沒有用到緩存,就重新設(shè)置一下緩存
下面說一下這里嗎涉及的兩個東西:緩存 FSCache 和 多進(jìn)程 WorkerFarm
FSCache
read 讀取緩存,并判斷最后修改時(shí)間和緩存的修改時(shí)間
write 寫入緩存
緩存目錄為了加速讀取,避免將所有的緩存文件放在一個文件夾里,parcel 將 16進(jìn)制 兩位數(shù)的 256 種可能創(chuàng)建為文件夾,這樣存取緩存文件的時(shí)候,將目標(biāo)文件路徑 md5 加密轉(zhuǎn)換為 16進(jìn)制,然后截取前兩位是目錄,后面幾位是文件名
WorkerFarm
在上面 start 里初始化 farm 的時(shí)候,workerPath 指向了 worker.js 文件,worker.js 里有兩個函數(shù),init 和 run
WorkerFarm.getShared 初始化的時(shí)候會創(chuàng)建一個 new WorkerFarm ,調(diào)用 worker.js 的 init 方法,根據(jù) cpu 獲取最大的 Worker 數(shù),并啟動一半的子進(jìn)程
farm.run 會通知子進(jìn)程執(zhí)行 worker.js 的 run 方法,如果進(jìn)程數(shù)沒有達(dá)到最大會再次開啟一個新的子進(jìn)程,子進(jìn)程執(zhí)行完畢后將 Promise狀態(tài)更改為完成
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; }
將上面的代碼內(nèi)部擴(kuò)展一下:
async process() { // 已經(jīng)有就不需要編譯 if (!this.generated) { // 加載代碼 if (this.contents == null) { this.contents = await this.load(); } // 可選。在收集依賴之前轉(zhuǎn)換。 await this.pretransform(); // 將代碼解析為 AST 樹 if (!this.ast) { this.ast = await this.parse(this.contents); } // 收集依賴 await this.collectDependencies(); // 可選。在收集依賴之后轉(zhuǎn)換。 await this.transform(); // 生成代碼 this.generated = await this.generate(); } return this.generated; } // 最后處理代碼 async postProcess(generated) { return generated }
processAsset 中調(diào)用 asset.process 生成 generated 這個generated 不一定是最終代碼 ,像 html里內(nèi)聯(lián)的 script ,vue 的 html, js, css,都會進(jìn)行二次或多次遞歸處理,最終調(diào)用 asset.postProcess 生成代碼
Asset
下面說幾個實(shí)現(xiàn)
HTMLAsset:
pretransform 調(diào)用 posthtml 將 html 解析為 PostHTMLTree(如果沒有設(shè)置posthtmlrc之類的不會走)
parse 調(diào)用 posthtml-parser 將 html 解析為 PostHTMLTree
collectDependencies 用 walk 遍歷 ast,找到 script, img 的 src,link 的 href 等的地址,將其加入到依賴
transform htmlnano 壓縮代碼
generate 處理內(nèi)聯(lián)的 script 和 css
postProcess posthtml-render 生成 html 代碼
JSAsset:
pretransform 調(diào)用 @babel/core 將 js 解析為 AST,處理 process.env
parse 調(diào)用 @babel/parser 將 js 解析為 AST
collectDependencies 用 babylon-walk 遍歷 ast, 如 ImportDeclaration,import xx from 'xx' 語法,CallExpression 找到 require調(diào)用,import 被標(biāo)記為 dynamic 動態(tài)導(dǎo)入,將這些模塊加入到依賴
transform 處理 readFileSync,__dirname, __filename, global等,如果沒有設(shè)置scopeHoist 并存在 es6 module 就將代碼轉(zhuǎn)換為 commonjs,terser 壓縮代碼
generate @babel/generator 獲取 js 與 sourceMap 代碼
VueAsset:
parse @vue/component-compiler-utils 與 vue-template-compiler 對 .vue 文件進(jìn)行解析
generate 對 html, js, css 處理,就像上面說到會對其分別調(diào)用 processAsset 進(jìn)行二次解析
postProcess component-compiler-utils 的 compileTemplate, compileStyle處理 html,css,vue-hot-reload-api HMR處理,壓縮代碼
回到 bundle 方法:
let loadedAssets = await this.buildQueue.run() 就是上面說到的PromiseQueue 和 WorkerFarm 結(jié)合起來:buildQueue.run —> processAsset -> loadAsset -> farm.run -> worker.run -> pipeline.process -> pipeline.processAsset -> asset.process,執(zhí)行之后所有資源編譯完畢,并返回入口資源loadedAssets就是 index.html 對應(yīng)的 HTMLAsset 資源
之后是 let changedAssets = [...this.findOrphanAssets(), ...loadedAssets] 獲取到改變的資源
findOrphanAssets 是從所有資源中查找沒有 parentBundle 的資源,也就是獨(dú)立的資源,這個 parentBundle 會在等會的構(gòu)建 Bundle 樹中被賦值,第一次構(gòu)建都沒有 parentBundle,所以這里會重復(fù)入口文件,這里的 findOrphanAssets 的作用是在第一次構(gòu)建之后,文件change的時(shí)候,在這個文件 import了新的一個文件,因?yàn)樾挛募]有被構(gòu)建過 Bundle 樹,所以沒有 parentBundle,這個新文件也被標(biāo)記物 change
invalidateBundle 因?yàn)榻酉聛硪獦?gòu)建新的樹所以調(diào)用重置所有資源上一次樹的屬性
createBundleTree 構(gòu)建 Bundle 樹:
首先一個入口資源會被創(chuàng)建成一個 bundle,然后動態(tài)的 import() 會被創(chuàng)建成子 bundle ,這引發(fā)了代碼的拆分。
當(dāng)不同類型的文件資源被引入,兄弟 bundle 就會被創(chuàng)建。例如你在 JavaScript 中引入了 CSS 文件,那它會被放置在一個與 JavaScript 文件對應(yīng)的兄弟 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)> ,用于生成準(zhǔn)確的 sourcemap 。
我們的例子會被構(gòu)建成:
html ( index.html ) |-- js ( index.js, module1.js, module2.js ) |-- map ( index.js, module1.js, module2.js )
module1.js 和 module2.js 被提到了與 index.js 同級,map 因?yàn)轭愋筒煌环诺搅?子bundle
一個復(fù)雜點(diǎn)的樹:
// 資源樹 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 )
因?yàn)橐獙?css 熱更新,所以新增了 css-loader.js, bundle-url.js 兩個 js
replaceBundleNames替換引用:生成樹之后將代碼中的文件引用替換為最終打包的文件名,如果是生產(chǎn)環(huán)境會替換為 contentHash 根據(jù)內(nèi)容生成 hash
hmr更新: 判斷啟用 hmr 并且不是第一次構(gòu)建的情況,調(diào)用 hmr.emitUpdate 將改變的資源發(fā)送給瀏覽器
Bundle.package 打包
unloadOrphanedAssets 將獨(dú)立的資源刪除
package
package 將generated 寫入到文件
有6種打包:
CSSPackager,HTMLPackager,SourceMapPackager,JSPackager,JSConcatPackager,RawPackager
當(dāng)開啟 scopeHoist 時(shí)用 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
可以看到代碼被拼接成了對象的形式,接收參數(shù) module, require 用來模塊導(dǎo)入導(dǎo)出,實(shí)現(xiàn)了 commonjs 的模塊加載機(jī)制,一個更加簡化版:
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 創(chuàng)建一個 WebSocket 監(jiān)聽服務(wù)端消息
修改文件觸發(fā) onChange 方法,onChange 將改變的資源 buildQueue.add 加入構(gòu)建隊(duì)列,重新調(diào)用 bundle 方法,打包資源,并調(diào)用 emitUpdate 通知瀏覽器更新
當(dāng)瀏覽器接收到服務(wù)端有新資源更新消息時(shí)
新的資源就會設(shè)置或覆蓋之前的模塊
modules[asset.id] = new Function('require', 'module', 'exports', asset.generated.js)
對模塊進(jìn)行更新:
function hmrAccept(id){ // dispose 回調(diào) cached.hot._disposeCallbacks.forEach(function (cb) { cb(bundle.hotData); }); delete bundle.cache[id]; // 刪除之前緩存 newRequire(id); // 重新此加載 // accept 回調(diào) cached.hot._acceptCallbacks.forEach(function (cb) { cb(); }); // 遞歸父模塊 進(jìn)行更新 getParents(global.parcelRequire, id).some(function (id) { return hmrAccept(global.parcelRequire, id); }); }
至此整個打包流程結(jié)束
總結(jié)
parcle index.html
進(jìn)入 cli,啟動Server調(diào)用 bundle,初始化配置(Plugins, env, HMRServer, Watcher, WorkerFarm),從入口資源開始,遞歸編譯(babel, posthtml, postcss, vue-template-compiler等),編譯完設(shè)置緩存,構(gòu)建 Bundle 樹,進(jìn)行打包
如果沒有 watch 監(jiān)聽,結(jié)束關(guān)閉 Watcher, Worker, HMR
有 watch 監(jiān)聽:
文件修改,觸發(fā) onChange,將修改的資源加入構(gòu)建隊(duì)列,遞歸編譯,查找緩存(這一步緩存的作用就提醒出來了),編譯完設(shè)置新緩存,構(gòu)建 Bundle 樹,進(jìn)行打包,將 change 的資源發(fā)送給瀏覽器,瀏覽器接收 hmr 更新資源
看完了這篇文章,相信你對Parcel源碼的案例分析有了一定的了解,想了解更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。