Molecule 在構建工具中的選擇

2023-10-31 15:00:39

我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式資料中臺產品。我們始終保持工匠精神,探索前端道路,為社群積累並傳播經驗價值。

本文作者:修能

朝聞道,夕死可矣

何為 Molecule?
輕量級的 Web IDE UI 框架——Molecule
我們開源了一個輕量的 Web IDE UI 框架
Molecule實現數棧至簡前端開發新體驗

前言

構建通常指的是把原始碼轉換成釋出到線上的可執行 JavaScrip、CSS、HTML 程式碼。在前端發展的過程中,原始碼的模組體系在不斷的更新,最終產物也在不斷的更新。而隨之也使得構建工具也在不斷更新換代。

而目前來看,基於前端的細化領域下,針對不同領域下的構建工具也日新月異。來看看 Molecule 該如何選擇構建工具呢?

Molecule 的需求

首先,我們需要分析 Molecule 對構建工具的需求有什麼?

老版本的問題

  1. 本地開發和 build 的構建工具不同,不得不增加 web 命令來執行一個預覽的任務,確保 build 後的產物沒問題。
  2. 慢,由於使用 tsc 作為編譯,所以編譯較慢。
  3. 部分變數無法複用,導致重複定義。

程式碼編譯

由於 Molecule 的程式碼是用 ESM 的模組書寫,且 Molecule 面向的是 Web 應用。通常來說面向 Web 應用的依賴庫是需要提供 ESM 的程式碼實現 tree shaking 的作用的。
所以我們這裡需要把 ESM 書寫的 Molecule 程式碼通過構建工具編譯成 ESM。

思考:為什麼要把 ESM 程式碼編譯成 ESM?

  1. 將 TypeScript 編譯成 JavaScript
  2. 將高階語法編譯成低階語法

除此之外,由於我們考慮到 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();

做兩件事

  1. 別名重定位
  2. 將檔案中的樣式檔案改為 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(';', ',');
                    }
                })
        );
    }
}

做兩件事

  1. :export幹掉
  2. :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