您好,登錄后才能下訂單哦!
小編給大家分享一下Parcel源碼的案例分析,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!
本篇文章是對 Parce 的源碼解析,代碼基本架構與執行流程,在這之前你如果對 parcel 不熟悉可以先到 Parcel官網 了解
介紹
下面是偷懶從官網抄下來的介紹:
極速零配置Web應用打包工具極速打包
Parcel 使用 worker 進程去啟用多核編譯。同時有文件系統緩存,即使在重啟構建后也能快速再編譯。
將你所有的資源打包
Parcel 具備開箱即用的對 JS, CSS, HTML, 文件 及更多的支持,而且不需要插件。
自動轉換
如若有需要,Babel, PostCSS, 和 PostHTML 甚至 node_modules 包會被用于自動轉換代碼.
零配置代碼分拆
使用動態 import() 語法, Parcel 將你的輸出文件束(bundles)分拆,因此你只需要在初次加載時加載你所需要的代碼。
熱模塊替換
Parcel 無需配置,在開發環境的時候會自動在瀏覽器內隨著你的代碼更改而去更新模塊。
友好的錯誤日志
當遇到錯誤時,Parcel 會輸出 語法高亮的代碼片段,幫助你定位問題。
打包工具 | 時間 |
---|---|
browserify | 22.98s |
webpack | 20.71s |
parcel | 9.98s |
parcel - with cache | 2.64s |
打包工具
我們常用的打包工具大致功能:
模塊化(代碼的拆分, 合并, Tree-Shaking 等)編譯(es6,7,8 sass typescript 等)壓縮 (js, css, html包括圖片的壓縮)HMR (熱替換)
version
parcel-bundler 版本:
"version": "1.11.0"
文件架構
|-- assets 資源目錄 繼承自 Asset.js |-- builtins 用于最終構建 |-- packagers 打包 |-- scope-hoisting 作用域提升 Tree-Shake |-- transforms 轉換代碼為 AST |-- utils 工具 |-- visitors 遍歷 js AST樹 收集依賴等 |-- Asset.js 資源 |-- Bundle.js 用于構建 bundle 樹 |-- Bundler.js 主目錄 |-- FSCache.js 緩存 |-- HMRServer.js HMR服務器提供 WebSocket |-- Parser.js 根據文件擴展名獲取對應 Asset |-- Pipeline.js 多線程執行方法 |-- Resolver.js 解析模塊路徑 |-- Server.js 靜態資源服務器 |-- SourceMap.js SourceMap |-- cli.js cli入口 解析命令行參數 |-- worker.js 多線程入口
流程
說明
Parcel是面向資源的,JavaScript,CSS,HTML 這些都是資源,并不是 webpack 中 js 是一等公民,Parcel 會自動的從入口文件開始分析這些文件 和 模塊中的依賴,然后構建一個 bundle 樹,并對其進行打包輸出到指定目錄
一個簡單的例子
我們從一個簡單的例子開始了解 parcel 內部源碼與流程
index.html |-- index.js |-- module1.js |-- module2.js
上面是我們例子的結構,入口為 index.html, 在 index.html 中我們用 script 標簽引用了 src/index.js,在 index.js 中我們引入了2個子模塊
執行
npx parcel index.html 或者 ./node_modules/.bin/parcel index.html,或者使用 npm script
cli
"bin": { "parcel": "bin/cli.js" }
查看 parcel-bundler的 package.json 找到 bin/cli.js,在cli.js里又指向 ../src/cli
const program = require('commander'); program .command('serve [input...]') // watch build ... .action(bundle); program.parse(process.argv); async function bundle(main, command) { const Bundler = require('./Bundler'); const bundler = new Bundler(main, command); if (command.name() === 'serve' && command.target === 'browser') { const server = await bundler.serve(); if (server && command.open) {...啟動自動打開瀏覽器} } else { bundler.bundle(); } }
在 cli.js 中利用 commander 解析命令行并調用 bundle 方法
有 serve, watch, build 3個命令來調用 bundle 函數,執行 pracel index.html 默認為 serve,所以調用的是 bundler.serve 方法
進入 Bundler.js
bundler.serve
async serve(port = 1234, https = false, host) { this.server = await Server.serve(this, port, host, https); try { await this.bundle(); } catch (e) {} return this.server; }
bundler.serve 方法 調用 serveStatic 起了一個靜態服務指向 最終打包的文件夾
下面就是重要的 bundle 方法
bundler.bundle
async bundle() { // 加載插件 設置env 啟動多線程 watcher hmr await this.start(); if (isInitialBundle) { // 創建 輸出目錄 await fs.mkdirp(this.options.outDir); this.entryAssets = new Set(); for (let entry of this.entryFiles) { let asset = await this.resolveAsset(entry); this.buildQueue.add(asset); this.entryAssets.add(asset); } } // 打包隊列中的資源 let loadedAssets = await this.buildQueue.run(); // findOrphanAssets 獲取所有資源中獨立的沒有父Bundle的資源 let changedAssets = [...this.findOrphanAssets(), ...loadedAssets]; // 因為接下來要構建 Bundle 樹,先對上一次的 Bundle樹 進行 clear 操作 for (let asset of this.loadedAssets.values()) { asset.invalidateBundle(); } // 構建 Bundle 樹 this.mainBundle = new Bundle(); for (let asset of this.entryAssets) { this.createBundleTree(asset, this.mainBundle); } // 獲取新的最終打包文件的url this.bundleNameMap = this.mainBundle.getBundleNameMap( this.options.contentHash ); // 將代碼中的舊文件url替換為新的 for (let asset of changedAssets) { asset.replaceBundleNames(this.bundleNameMap); } // 將改變的資源通過websocket發送到瀏覽器 if (this.hmr && !isInitialBundle) { this.hmr.emitUpdate(changedAssets); } // 對資源打包 this.bundleHashes = await this.mainBundle.package( this, this.bundleHashes ); // 將獨立的資源刪除 this.unloadOrphanedAssets(); return this.mainBundle; }
我們一步步先從 this.start 看
start
if (this.farm) { return; } await this.loadPlugins(); if (!this.options.env) { await loadEnv(Path.join(this.options.rootDir, 'index')); this.options.env = process.env; } if (this.options.watch) { this.watcher = new Watcher(); this.watcher.on('change', this.onChange.bind(this)); } if (this.options.hmr) { this.hmr = new HMRServer(); this.options.hmrPort = await this.hmr.start(this.options); } this.farm = await WorkerFarm.getShared(this.options, { workerPath: require.resolve('./worker.js') });
start:
開頭的判斷 防止多次執行,也就是說 this.start 只會執行一次loadPlugins 加載插件,找到 package.json 文件 dependencies, devDependencies 中 parcel-plugin-開頭的插件進行調用loadEnv 加載環境變量,利用 dotenv, dotenv-expand 包將 env.development.local, .env.development, .env.local, .env 擴展至 process.envwatch 初始化監聽文件并綁定 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源碼的案例分析有了一定的了解,想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。