巨集
巨集是會在建置時執行的 JavaScript 函式。巨集傳回的值會內嵌到套件中,取代原本的函式呼叫。這讓你可以產生常數、程式碼,甚至額外的資源,而不需要任何自訂的外掛。
巨集會使用 import 屬性 來匯入,用以表示它們應該在建置時執行,而不是被套件到輸出的檔案中。你可以匯入任何 JavaScript 或 TypeScript 模組作為巨集,包括內建的 Node 模組和 npm 套件。
注意:基於安全性考量,巨集無法在 node_modules
內部呼叫。
這個範例使用 regexgen 函式庫在建置時從一組字串產生最佳化的正規表示式。
import regexgen from 'regexgen' with {type: 'macro'};
const regex = regexgen(['foobar', 'foobaz', 'foozap', 'fooza']);
console.log(regex);
這會編譯成下列套件
console.log(/foo(?:zap?|ba[rz])/);
如你所見,regexgen
函式庫已被完全編譯掉,而我們得到一個靜態的正規表示式!
參數
#巨集參數會在靜態時評估,這表示它們的值必須在建置時已知。你可以傳遞任何 JavaScript 文字值,包括字串、數字、布林值、物件等。字串串接、算術和比較運算子等簡單的表達式也受支援。
import {myMacro} from './macro.ts' with {type: 'macro'};
const result = myMacro({
name: 'Devon'
});
然而,不支援參照非常數變數、呼叫巨集以外的函式等值。
import {myMacro} from './macro.ts' with {type: 'macro'};
const result = myMacro({
name: getName() // Error: Cannot statically evaluate macro argument
});
常數
#Parcel 也會評估透過 const
關鍵字宣告的常數。這些常數可以在巨集引數中參照。
import {myMacro} from './macro.ts' with {type: 'macro'};
const name = 'Devon';
const result = myMacro({name});
一個巨集的結果也可以傳遞給另一個巨集。
import {myMacro} from './macro.ts' with {type: 'macro'};
import {getName} from './name.ts' with {type: 'macro'};
const name = getName();
const result = myMacro({name});
然而,如果你嘗試變異常數的值,這將導致錯誤。
import {myMacro} from './macro.ts' with {type: 'macro'};
const arg = {name: 'Devon'};
arg.name = 'Peter'; // Error: Cannot statically evaluate macro argument
const result = myMacro({name});
傳回值
#巨集可以傳回任何 JavaScript 值,包括物件、字串、布林值、數字,甚至函式。這些值會轉換成 AST,並取代程式碼中原本的函式呼叫。
import {getRandomNumber} from './macro.ts' with {type: 'macro'};
console.log(getRandomNumber());
export function getRandomNumber() {
return Math.random();
}
這個範例的捆綁輸出看起來像這樣
console.log(0.006024956627355804);
非同步巨集
#巨集也可以傳回解決為任何支援值的承諾。例如,你可以在建置時對 URL 內容發出 HTTP 要求,並將結果內嵌到捆綁中,作為字串。
import {fetchText} from './macro.ts' with {type: 'macro'};
console.log(fetchText('http://example.com'));
export async function fetchText(url: string) {
let res = await fetch(url);
return res.text();
}
產生函式
#巨集可以傳回函式,這允許你在建置時產生程式碼。使用 new Function
建構函式,從字串動態產生函式。
這個範例使用 micromatch 函式庫,在建置時編譯 glob 匹配函式。
import {compileGlob} from './glob.ts' with {type: 'macro'};
const isMatch = compileGlob('foo/**/bar.js');
import micromatch from 'micromatch';
export function compileGlob(glob) {
let regex = micromatch.makeRe(glob);
return new Function('string', `return ${regex}.test(string)`);
}
這個範例的捆綁輸出看起來像這樣
const isMatch = function(string) {
return /^(?:foo(?:\/(?!\.)(?:(?:(?!(?:^|\/)\.).)*?)\/|\/|$)bar\.js)$/.test(string);
};
產生資源
#巨集可以產生額外的資源,這些資源會成為呼叫它的 JavaScript 模組的相依性。例如,巨集可以產生 CSS,這些 CSS 會靜態萃取到 CSS 捆綁中,就像從 JS 檔案匯入一樣。
在巨集函式中,this
是包含 Parcel 提供方法的物件。若要建立資源,請呼叫 this.addAsset
並提供類型和內容。
這個範例接受 CSS 字串並傳回產生的類別名稱。CSS 會新增為資源並捆綁到 CSS 檔案中,而 JavaScript 捆綁只包含產生的類別名稱,作為靜態字串。
import {css} from './css.ts' with {type: 'macro'};
<div className={css('color: red; &:hover { color: green }')}>
Hello!
</div>
import type {MacroContext} from '@parcel/macros';
export async function css(this: MacroContext | void, code: string) {
let className = hash(code);
code = `.${className} { ${code} }`;
this?.addAsset({
type: 'css',
content: code
});
return className;
}
上述範例的捆綁輸出看起來像這樣
<div className="ax63jk4">
Hello!
</div>
.ax63jk4 {
color: red;
&:hover {
color: green;
}
}
快取
#預設情況下,Parcel 會快取巨集的結果,直到呼叫它的檔案變更為止。不過,有時巨集可能有其他應使快取失效的輸入。例如,它可能會讀取檔案、存取環境變數等。巨集函式內的 this
語境包含用於控制快取行為的方法。
interface MacroContext {
/** Invalidate the macro call whenever the given file changes. */
invalidateOnFileChange(filePath: string): void,
/** Invalidate the macro call when a file matching the given pattern is created. */
invalidateOnFileCreate(options: FileCreateInvalidation): void,
/** Invalidate the macro whenever the given environment variable changes. */
invalidateOnEnvChange(env: string): void,
/** Invalidate the macro whenever Parcel restarts. */
invalidateOnStartup(): void,
/** Invalidate the macro on every build. */
invalidateOnBuild(): void,
}
type FileCreateInvalidation = FileInvalidation | GlobInvalidation | FileAboveInvalidation;
/** Invalidate when a file matching a glob is created. */
interface GlobInvalidation {
glob: string
}
/** Invalidate when a specific file is created. */
interface FileInvalidation {
filePath: string
}
/** Invalidate when a file of a specific name is created above a certain directory in the hierarchy. */
interface FileAboveInvalidation {
fileName: string,
aboveFilePath: string
}
例如,在巨集中讀取檔案時,將檔案路徑新增為失效,以便在該檔案變更時重新編譯呼叫程式碼。在此範例中,每當編輯 message.txt
時,index.ts
都會重新編譯,且會再次呼叫 readFile
巨集。
import {readFile} from './macro.ts' with {type: 'macro'};
console.log(readFile('message.txt'))
import type {MacroContext} from '@parcel/macros';
import fs from 'fs';
export async function readFile(this: MacroContext | void, filePath: string) {
this?.invalidateOnFileChange(filePath);
return fs.readFileSync(filePath, 'utf8');
}
hello world!
與其他工具一起使用
#巨集只是正常的 JavaScript 函式,因此它們可以輕鬆地與其他工具整合。
TypeScript
#從 5.3 版開始,TypeScript 支援匯入屬性,且巨集的自動完成和類型就像一般函式一樣運作。
Babel
#@babel/plugin-syntax-import-attributes
外掛程式讓 Babel 可以剖析匯入屬性。如果您使用 @babel/preset-env
,啟用 shippedProposals
選項也會啟用剖析匯入屬性。
{
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true
}
]
]
}
ESLint
#ESLint 在使用支援匯入屬性的剖析器(例如 Babel 或 TypeScript)時支援匯入屬性。
module.exports = {
parser: '@typescript-eslint/parser'
};
單元測試
#單元測試巨集就像測試任何其他 JavaScript 函式一樣。一個注意事項是,如果您的巨集使用上述章節中所述的 this
語境。如果您要測試巨集本身,您可以模擬 this
參數,以驗證它是否如預期般被呼叫。
import {css} from '../src/css.ts';
it('should generate css', () => {
let addAsset = jest.fn();
let className = css.call({
addAsset,
// ...
}, 'color: red');
expect(addAsset).toHaveBeenCalledWith({
type: 'css',
content: '.ax63jk4 { color: red }'
});
expect(className).toBe('ax63jk4');
});
當測試間接使用巨集的程式碼時,巨集函式會在執行階段作為一個正常函式被呼叫,而不是在編譯階段由 Parcel 呼叫。在這種情況下,通常由 Parcel 提供的巨集內容將不可用。這就是為什麼在上述範例中,this
參數被設定為 MacroContext | void
,而且我們會執行執行階段檢查,以查看 this
是否存在。當內容不可用時,使用該內容的程式碼(例如 this?.addAsset
)不會執行,但函式應該如常傳回一個值。
與 Bun 的差異
#透過匯入屬性進行巨集處理最初是在 Bun 中實作的。Parcel 的實作大部分與 Bun 的巨集 API 相容,但有一些差異
- Parcel 支援從巨集中傳回函式。
- Parcel 在巨集中支援
this
內容,以啟用產生資源和控制快取行為。 - Parcel 目前不支援 Bun 的特殊案例傳回值,例如型別化陣列、擷取
Response
物件或Blob
物件。您需要在從巨集中傳回這些值之前,自行將它們轉換為字串。 - Parcel 目前不支援
"macro"
package.jsonexports
條件。