我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式資料中臺產品。我們始終保持工匠精神,探索前端道路,為社群積累並傳播經驗價值。
本文作者:修能
朝聞道,夕死可矣
何為 Molecule?
輕量級的 Web IDE UI 框架——Molecule
我們開源了一個輕量的 Web IDE UI 框架
Molecule實現數棧至簡前端開發新體驗
前言
構建通常指的是把原始碼轉換成釋出到線上的可執行 JavaScrip、CSS、HTML 程式碼。在前端發展的過程中,原始碼的模組體系在不斷的更新,最終產物也在不斷的更新。而隨之也使得構建工具也在不斷更新換代。
而目前來看,基於前端的細化領域下,針對不同領域下的構建工具也日新月異。來看看 Molecule 該如何選擇構建工具呢?
Molecule 的需求
首先,我們需要分析 Molecule 對構建工具的需求有什麼?
老版本的問題
- 本地開發和 build 的構建工具不同,不得不增加 web 命令來執行一個預覽的任務,確保 build 後的產物沒問題。
- 慢,由於使用 tsc 作為編譯,所以編譯較慢。
- 部分變數無法複用,導致重複定義。
程式碼編譯
由於 Molecule 的程式碼是用 ESM 的模組書寫,且 Molecule 面向的是 Web 應用。通常來說面向 Web 應用的依賴庫是需要提供 ESM 的程式碼實現 tree shaking 的作用的。
所以我們這裡需要把 ESM 書寫的 Molecule 程式碼透過構建工具編譯成 ESM。
思考:為什麼要把 ESM 程式碼編譯成 ESM?
- 將 TypeScript 編譯成 JavaScript
- 將高階語法編譯成低階語法
除此之外,由於我們考慮到 Node.js 後續發展以 Pure ESM 為主,且 Molecule 針對 CommonJS 的場景較少,故我們不考慮輸出 CommonJS 的產物。
型別
需要支援輸出型別。
樣式
Molecule 中使用 BEM 作為類名規範,通常情況下使得需要在 Sass 中和 JavaScript 中都定義相同變數名。而類 Sass-in-JS 使得我們可以從 Sass 中匯出變數名,在 JS 檔案中使用。
這就使得構建工具不僅要支援 Sass 的編譯,同時還需要支援外掛,允許我們做 Sass-in-JS 的需求。
其他
其他相關檔案,例如 JSON,PNG 等檔案需要支援複製至相關指定目錄。
調研構建工具
Webpack
Webpack 是目前構建工具中的老大哥了,作為頂級老牌構建工具,幾乎所有場景都能適用。
缺點也僅僅是冗餘程式碼較多,配置項太多,體積較大等。
Rollup
作為面向 JS 類庫而出現的構建工具。其和 Webpack 相比,在打包後產生的冗餘程式碼少,體積較小,功能專注。缺點僅僅是不支援 HMR。
Vite
直接排除
Parcel
Parcel 目前看作是面向 Web 應用的零配置,高速度的 Webpack。其有一個致命的弱點是,自定義外掛支援不如 Webpack。這會讓我們無法實現 Sass-in-JS。
2.0 可能有所改善,我不清楚。不予評價
swc
swc 在某種程度上,是 babel 和 tsc 的競品,屬於比較底層的構建工具。和 esbuild 同型別,只是 esbuild 基於 Go,swc 基於 Rust。
esbuild
extremely fast JavaScript Compiler
babel
很好,就是慢
tsc
很好,就是更慢。有一個優點,只有 tsc 能支援輸出型別。
方案實施
由於大多數的構建工具都是 bundler,並不符合 Molecule 的定位。故採取的方案是 esbuild + Sass + tsc 的方案。
esbuild 取其作為 Compiler 的部分,Sass 取其編譯 SCSS 檔案的部分,tsc 負責編譯出型別檔案。
tsx 相關檔案輸出
transformCtx = await esbuild.context({
entryPoints,
bundle: false,
format: 'esm',
outdir: dist,
jsx: 'automatic',
plugins: [
{
name: 'alias',
setup(build) {
build.onLoad({ filter: /.*/ }, async (args) => {
const source = await fs.promises.readFile(args.path, 'utf8');
const contents = sassLoader(alias(source, args.path));
return {
contents,
loader: args.path.endsWith('.tsx') ? 'tsx' : 'ts',
};
});
},
},
],
});
await transformCtx.watch();
做兩件事
- 別名重定位
- 將檔案中的樣式檔案改為 css
樣式檔案輸出
/**
*
* @param {string} entry
*/
async function _transform(entry) {
const res = await sass.compileAsync(entry);
const regex = /^:export {(\n|.)+}$/m;
const target = entry.replace(/src\//, 'esm/').replace(/.scss/, '.css');
const dirname = path.dirname(target);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
const css = res.css.replace(regex, '');
fs.writeFileSync(target, css);
if (regex.test(res.css)) {
const exportModules = res.css.match(regex)[0];
fs.writeFileSync(
path.join(dirname, styleVariablesFileName),
exportModules
.replace(':export', 'export default')
.replace(/: .*;/gm, (substring) => {
const stringLiteral = /(?<="|')\S+(?="|')/g;
if (!stringLiteral.test(substring)) {
const startIdx = substring.indexOf(':');
const endIdx = substring.indexOf(';');
return `:"${substring.substring(startIdx + 1, endIdx).trim()}",`;
} else {
return substring.replace(';', ',');
}
})
);
}
}
做兩件事
- 把
:export
幹掉 - 把
:export
的內容放到當前目錄下的style__variables.js
的目錄中
型別檔案輸出
型別檔案非同步輸出,防止阻塞
async function transformTyping() {
typingCtx = spawn('tsc && (concurrently "tsc -w" "tsc-alias -w")', {
stdio: 'inherit',
shell: true,
});
}
其他檔案輸出
/**
*
* @param {string} filePath
*/
function _copyFile(filePath) {
const dest = filePath.replace(/src\//, 'esm/');
const dirname = path.dirname(dest);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
fs.createReadStream(filePath, 'utf-8').pipe(fs.createWriteStream(dest));
}
遺留問題
- 增量編譯的問題
- 程式碼壓縮
歡迎大家就以上問題留言討論!
最後
歡迎關注【袋鼠雲數棧UED團隊】~
袋鼠雲數棧UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star