撰寫外掛程式
撰寫外掛時應注意的事項
外掛 API
#有幾種不同類型的外掛。它們看起來都很相似,但保持分離,因此我們可以對每個外掛允許執行的動作有嚴格的合約。
有一些規則應遵循於每種類型的外掛
- 無狀態 — 避免任何類型的狀態,這很可能會成為使用者的錯誤來源。例如,相同的轉換可能存在於多個不同的工作執行緒中,這些工作執行緒不允許彼此通訊,狀態將無法按預期工作。
- 純粹 — 給定相同的輸入,外掛必須產生相同的輸出,而且你不得有任何可觀察的副作用或隱含的依賴關係。否則,Parcel 的快取將中斷,你的使用者會感到難過。你永遠不應該告訴使用者刪除他們的快取。
外掛 API 都遵循一個常見的形狀
import { NameOfPluginType } from "@parcel/plugin";
export default new NameOfPluginType({
async methodName(opts: JSONObject): Promise<JSONObject> {
return result;
},
});
外掛的每個方法都是一個非同步函式,它
- 接受經過嚴格驗證的
opts
物件。 - 傳回經過嚴格驗證的
result
物件。
如果你需要某些東西未透過 opts
傳遞,請與 Parcel 團隊討論。避免嘗試從其他來源(特別是檔案系統)自行取得資訊。
模組格式
#Parcel 支援以 CommonJS 或 ES 模組編寫的外掛。外掛的模組格式由解析器模組的檔案副檔名或其 package.json
決定。對於 ES 模組外掛,請使用 .mjs
副檔名,對於 CommonJS,請使用 .cjs
或 .js
。如果在外掛的 package.json
中宣告了 "type": "module"
,則 .js
副檔名會被視為 ESM,而不是 CommonJS。此行為與 Node.js 載入模組的方式相符。
ES 模組外掛目前為實驗性質。請將您遇到的任何問題回報 Github。
載入設定
#許多外掛需要從使用者的專案載入某種設定。在某些情況下,外掛包裝的編譯器或工具會內建設定載入機制。在其他情況下,您需要為您的外掛建立設定檔格式。
注意:使用 Parcel 的設定載入機制非常重要,而不是直接從檔案系統讀取。所有外掛的結果都會暫存在 Parcel 中,如果您不使用 Parcel 的設定載入系統,它將不會知道您自己讀取的檔案,也無法正確地使快取失效。
設定載入是在 loadConfig
方法中完成,大多數外掛類型都支援此方法。它會收到一個 Config
物件,其中包含用於載入設定檔的工具方法,以及用於告知 Parcel 設定檔依賴的檔案和依賴項的方法,這些檔案和依賴項應使結果失效。從 loadConfig
函式傳回的結果會傳遞到其他外掛函式中。
import {Transformer} from '@parcel/plugin';
export default new Transformer({
async loadConfig({config}) {
let {contents, filePath} = await config.getConfig([
'tool.config.json'
]);
return contents;
},
async transform({asset, config}) {
// ...
return [asset];
}
});
上述範例使用 Config
物件的 getConfig
方法來載入設定檔。這會從資產的檔案路徑向上搜尋目錄樹,以尋找與給定檔名相符的設定檔。它可以使用 JSON5(預設)、JavaScript 或 TOML 載入檔案,而檔案路徑會自動新增為設定的無效化項。
新增無效化項
#如果您使用的是編譯器或工具中內建的設定載入機制,您需要透過呼叫 invalidateOnFileChange
告訴 Parcel 過程中載入的任何檔案。這樣,Parcel 就能在設定變更時,使使用此設定編譯的檔案失效。各種工具通常會在載入設定時,同時回傳載入檔案的清單。
如果未載入設定,或更接近資產的新設定檔會變更結果,您應使用 invalidateOnFileCreate
方法來監控設定檔的建立。這樣,當 Parcel 偵測到新的設定檔時,外掛程式將會重新執行,且新的設定將會載入。
import {Transformer} from '@parcel/plugin';
export default new Transformer({
async loadConfig({config}) {
let {result, files} = await loadToolConfigSomehow(config.searchPath);
if (result) {
// Invalidate whenever one of the loaded files changes.
for (let file of files) {
config.invalidateOnFileChange(file);
}
} else {
// Invalidate when a new config is created.
config.invalidateOnFileCreate({
fileName: 'tool.config.json',
aboveFilePath: config.searchPath
});
}
return result;
}
});
開發相依性
#Parcel 會自動追蹤 Parcel 外掛程式本身及其所有相依性的原始檔,以進行變更。如果外掛程式的程式碼變更,則必須使快取失效,且必須重建外掛程式產生的任何資產。
有些外掛程式可能會動態載入其他相依性。例如,轉換器可能有其自己的外掛程式,且這些外掛程式會在使用者的專案中設定(例如 Babel)。這些無法自動追蹤,且必須新增為 Config 物件的開發相依性。這是透過使用 addDevDependency
方法來完成。
import {Transformer} from '@parcel/plugin';
export default new Transformer({
async loadConfig({config}) {
let {result, filePath} = await loadToolConfigSomehow(config.searchPath);
for (let plugin of result.plugins) {
config.addDevDependency({
specifier: plugin,
resolveFrom: filePath
});
}
return result;
}
});
JavaScript 設定
#有些工具會使用以 JavaScript 編寫的設定檔,而不是像 JSON、YAML 或 TOML 等靜態設定語言。很遺憾的是,這些程式化設定檔可能會導致 Parcel 中的快取產生問題,因為它們可能會回傳非確定性的結果。
處理此問題的 Parcel 慣例是,在 Parcel 程序重新啟動時,總是使組態無效。這樣一來,JavaScript 組態不會在每次建置時都失效(這會太慢),但如果組態是不確定的,重新啟動 Parcel 可確保組態是最新的。
這可以使用 Config
物件的 invalidateOnStartup
方法來完成。
import {Transformer} from '@parcel/plugin';
export default new Transformer({
async loadConfig({config}) {
let {contents, filePath} = await config.getConfig([
'tool.config.json',
'tool.config.js'
]);
if (filePath.endsWith('.js')) {
config.invalidateOnStartup();
}
return contents;
}
});
請參閱 Config
物件 API 文件,以取得所有可用方法和屬性的更多詳細資訊。
命名
#所有外掛程式都必須遵循命名系統
官方套件 | 社群套件 | 私人公司/範圍團隊套件 | |
---|---|---|---|
組態 | @parcel/config-{name} | parcel-config-{name} | @scope/parcel-config[-{name}] |
解析器 | @parcel/resolver-{name} | parcel-resolver-{name} | @scope/parcel-resolver[-{name}] |
轉換器 | @parcel/transformer-{name} | parcel-transformer-{name} | @scope/parcel-transformer[-{name}] |
打包器 | @parcel/bundler-{name} | parcel-bundler-{name} | @scope/parcel-bundler[-{name}] |
命名器 | @parcel/namer-{name} | parcel-namer-{name} | @scope/parcel-namer[-{name}] |
執行時期 | @parcel/runtime-{name} | parcel-runtime-{name} | @scope/parcel-runtime[-{name}] |
封裝器 | @parcel/packager-{name} | parcel-packager-{name} | @scope/parcel-packager[-{name}] |
最佳化器 | @parcel/optimizer-{name} | parcel-optimizer-{name} | @scope/parcel-optimizer[-{name}] |
報告器 | @parcel/reporter-{name} | parcel-reporter-{name} | @scope/parcel-reporter[-{name}] |
驗證器 | @parcel/validator-{name} | parcel-validator-{name} | @scope/parcel-validator[-{name}] |
{name}
必須具有描述性,並與套件的目的直接相關。只要在 .parcelrc
或 package.json#devDependencies
中讀取名稱,任何人都應該可以了解套件的功能。
parcel-transformer-posthtml
parcel-packager-wasm
parcel-reporter-graph-visualizer
如果您的外掛程式新增特定工具的支援,請使用該工具的名稱。
parcel-transformer-es6 (bad)
parcel-transformer-babel (good)
如果您的外掛程式是對現有功能的重新實作,請嘗試命名為可以說明為什麼它是獨立功能的名稱。
parcel-transformer-better-typescript (bad)
parcel-transformer-typescript-server (good)
我們要求社群成員合作,並在發生分歧時嘗試解決問題。如果有人製作了您外掛程式的較佳版本,請考慮將較佳的套件名稱讓給他們,請他們進行重大版本升級,並將使用者重新導向到新的工具。
請參閱Local plugins,以取得在專案中使用外掛程式而不發佈外掛程式的建議。
版本控制
#您必須遵循語意版本控制(盡您所能)。沒錯,這並不是完美的系統,但它是我們擁有的最佳系統,而且人們確實依賴它。
如果外掛程式作者故意不遵循語意版本控制,Parcel 可能會開始警告使用者,他們應該鎖定外掛程式的版本號碼。
引擎
#您必須指定 package.json#engines.parcel
欄位,並包含外掛程式支援的 Parcel 版本範圍
{
"name": "parcel-transformer-imagemin",
"engines": {
"parcel": "2.x"
}
}
如果您未指定此欄位,Parcel 會輸出警告
Warning: The plugin "parcel-transformer-typescript" needs to specify a
`package.json#engines.parcel` field with the supported Parcel version range.
如果您指定了 parcel 引擎欄位,而使用者使用的是不相容的 Parcel 版本,他們會看到錯誤
Error: The plugin "parcel-transformer-typescript" is not compatible with the
current version of Parcel. Requires "2.x" but the current version is "3.1.4"
Parcel 使用 node-semver
來比對版本範圍。