Stories

Detail Return Return

替代 webpack?一文帶你瞭解 snowpack 原理,你還學得動麼 - Stories Detail

近期,隨着 vue3 的各種曝光,vite 的熱度上升,與 vite 類似的 snowpack 的關注度也逐漸增加了。目前(2020.06.18)snowpack 在 Github 上已經有了將近 1w stars。

snowpack 的代碼很輕量,本文會從實現原理的角度介紹 snowpack 的特點。同時,帶大家一起看看,作為一個以原生 JavaScript 模塊化為核心的年輕的構建工具,它是如何實現“老牌”構建工具所提供的那些特性的。

1. 初識 snowpack

近期,隨着 vue3 的各種曝光,vite 的熱度上升,與 vite 類似的 snowpack 的關注度也逐漸增加了。目前(2020.06.18)snowpack 在 Github 上已經有了將近 1w stars。

時間撥回到 2019 年上半年,一天中午我百無聊賴地讀到了 A Future Without Webpack 這篇文章。通過它瞭解到了 pika/snowpack 這個項目(當時還叫 pika/web)。

文章的核心觀點如下:

在如今(2019年),我們完全可以拋棄打包工具,而直接在瀏覽器中使用瀏覽器原生的 JavaScript 模塊功能。這主要基於三點考慮:

  1. 兼容性可接受:基本主流的瀏覽器版本都支持直接使用 JavaScript Module 了(當然,IE 一如既往除外)。
  2. 性能問題的改善:之前打包的一個重要原因是 HTTP/1.1 的特性導致,我們合併請求來優化性能;而如今 HTTP/2 普及之後,這個性能問題不像以前那麼突出了。
  3. 打包的必要性:打包工具的存在主要就是為了處理模塊化與合併請求,而以上兩點基本解決這兩個問題;再加之打包工具越來越複雜,此消彼長,其存在的必要性自然被作者所質疑。

由於我認為 webpack 之類的打包工具,“發家”後轉型做構建工具並非最優解,實是一種陰差陽錯的階段性成果。所以當時對這個項目提到的觀點也很贊同,其中印象最深的當屬它提到的:

In 2019, you should use a bundler because you want to, not because you need to.

同時,我也認為,打包工具(Bundler) ≠ 構建工具(Build Tools) ≠ 工程化。

2. 初窺 snowpack

看到這片文章後(大概是19年6、7月?),抱着好奇立刻去 Github 上讀了這個項目。當時看這個項目的時候大概是 0.4.x 版本,其源碼和功能都非常簡單。

snowpack 的最初版核心目標就是不再打包業務代碼,而是直接使用瀏覽器原生的 JavaScript Module 能力。

所以從它的處理流程上來看,對業務代碼的模塊,基本只需要把 ESM 發佈(拷貝)到發佈目錄,再將模塊導入路徑從源碼路徑換為發佈路徑即可。

而對 node_modules 則通過遍歷 package.json 中的依賴,按該依賴列表為粒度將 node_modules 中的依賴打包。以 node_modules 中每個包的入口作為打包 entry,使用 rollup 生成對應的 ESM 模塊文件,放到 web_modules 目錄中,最後替換源碼的 import 路徑,是得可以通過原生 JavaScript Module 來加載 node_modules 中的包。

- import { createElement, Component } from "preact";
- import htm from "htm";
+ import { createElement, Component } from "/web_modules/preact.js";
+ import htm from "/web_modules/htm.js";

從 v0.4.0 版本的源碼 可以看出,其初期功能確實非常簡單,甚至有些簡陋,以至於缺乏很多現代前端開發所需的特性,明顯是不能用於生產環境的。

直觀感受來説,它當時就欠缺以下能力:

  1. import CSS / image / …:由於 webpack 一切皆模塊的理念 + 組件化開發的深入人心,import anything 的書寫模式已經深入開發者的觀念中。對 CSS 等內容依賴與加載能力的缺失,將成為它的阿克琉斯之踵。
  2. 語法轉換能力:作為目標成為構建工具的 snowpack(當時叫 web),並沒有能夠編譯 Typescript、JSX 等語法文件的能力,你當然可以再弄一個和它毫無關係的工具來處理語法,但是,這不就是構建工具應該集成的麼?
  3. HMR:這可能不那麼要命,但俗話説「由儉入奢易,由奢入儉難」,被“慣壞”開發者們自然會有人牴觸這一特性的缺失。
  4. 性能:雖説它指出,上了 HTTP2 後,使用 JavaScript modules 性能並不會差,但畢竟沒有實踐過,對此還是抱有懷疑。
  5. 環境變量:這雖然是一個小特性,但在我接觸過的大多數項目中都會用到它,它可以幫助開發者自動測卸載線上代碼中的調試工具,可以根據環境判斷,自動將埋點上報到不同的服務上。確實需要一個這樣好用的特性。

3. snowpack 的進化

時間回到 2020 年上半年,隨着 vue3 的不斷曝光,與其有一定關聯的另一個項目 vite 也逐漸吸引了人們的目光。而其介紹中提到的 snowpack 也突然吸引到了更多的熱度與討論。當時我只是對 pika 感到熟悉,好奇的點開 snowpack 項目主頁的時候,才發現這個一年前初識的項目(pika/web)已經升級到了 pika/snowpack v2。而項目源碼也不再是之前那唯一而簡單的 index.ts,在核心代碼外,還包含了諸多官方插件。

看着已經完全變樣的 Readme,我的第一直覺是,之前我想到的那些問題,應該已經有了解決方案。

抱着學習的態度,對它進行重新瞭解之後,發現果然如此。好奇心趨勢我對它的解決方案去一探究竟。

本文寫於 2020.06.18,源碼基於 snowpack@2.5.1

3.1. import CSS

import CSS 的問題還有一個更大的範圍,就是非 JavaScript 資源的加載,包括圖片、JSON 文件、文本等。

先説説 CSS。

import './index.css';

上面這種語法目前瀏覽是不支持的。所以 snowpack 用了一個和之前 webpack 很類似的方式,將 CSS 文件變為用於注入樣式的 JS 模塊。如果你熟悉 webpack,肯定知道如果你只是在 loader 中處理 CSS,那麼並不會生成單獨的 CSS 文件(這就是為什麼會有 mini-css-extract-plugin),而是加載一個 JS 模塊,然後在 JS 模塊中通過 DOM API 將 CSS 文本作為 style 標籤的內容插入到頁面中。

為此,snowpack 自己寫了一個簡單的模板方法,生成將 CSS 樣式注入頁面的 JS 模塊。下面這段代碼可以實現樣式注入的功能:

const code = '.test { height: 100px }';
const styleEl = document.createElement("style");
const codeEl = document.createTextNode(code);
styleEl.type = 'text/css';
styleEl.appendChild(codeEl);
document.head.appendChild(styleEl);

可以看到,除了第一行式子的右值,其他都是不變的,因此可以很容易生成一個符合需求的 JS 模塊:

const jsContent = `
  const code = ${JSON.stringify(code)};
  const styleEl = document.createElement("style");
  const codeEl = document.createTextNode(code);
  styleEl.type = 'text/css';
  styleEl.appendChild(codeEl);
  document.head.appendChild(styleEl);
`;

fs.writeFileSync(filename, jsContent);

snowpack 中的實現代碼比我們上面多了一些東西,不過與樣式注入無關,這個放到後面再説。

通過將 CSS 文件的內容保存到 JS 變量,然後再使用 JS 調用 DOM API 在頁面注入 CSS 內容即可使用 JavaScript Modules 的能力加載 CSS。而源碼中的 index.css 也會被替換為 index.css.proxy.js

- import './index.css';
+ import './index.css.proxy.js';

proxy 這個名詞之後會多次出現,因為為了能夠以模塊化方式導入非 JS 資源,snowpack 把生成的中間 JavaScript 模塊都叫做 proxy。這種實現方式也幾乎和 webpack 一脈相承。

3.2. 圖片的 import

在目前的前端開發場景中,還有一類非常典型的資源就是圖片。

import avatar from './avatar.png';

function render() {
    return (
        <div class="user">
            <img src={avatar} />
        </div>
    );
}

上面代碼的書寫方式已經普遍應用在很多項目代碼中了。那麼 snowpack 是怎麼處理的呢?

太陽底下沒有新鮮事,snowpack 和 webpack 一樣,對於代碼中導入的 avatar 變量,最後其實都是該靜態資源的 URI。

我們以 snowpack 提供的官方 React 模版為例來看看圖片資源的引入處理。

npx create-snowpack-app snowpack-test --template @snowpack/app-template-react

初始化模版運行後,可以看到源碼與構建後的代碼差異如下:

- import React, { useState } from 'react';
- import logo from './logo.svg';
- import './App.css';

+ import React, { useState } from '/web_modules/react.js';
+ import logo from './logo.svg.proxy.js';
+ import './App.css.proxy.js';

與 CSS 類似,也為圖片(svg)生成了一個 JS 模塊 logo.svg.proxy.js,其模塊內容為:

// logo.svg.proxy.js
export default "/_dist_/logo.svg";

套路與 webpack 如出一轍。以 build 命令為例,我們來看一下 snowpack 的處理方式。

首先是將源碼中的靜態文件(logo.svg)拷貝到發佈目錄:

allFiles = glob.sync(`**/*`, {
    ...
});
const allBuildNeededFiles: string[] = [];
await Promise.all(
    allFiles.map(async (f) => {
        f = path.resolve(f); // this is necessary since glob.sync() returns paths with / on windows.  path.resolve() will switch them to the native path separator.
        ...
        return fs.copyFile(f, outPath);
    }),
);

然後,我們可以看到 snowpack 中的一個叫 transformEsmImports 的關鍵方法調用。這個方法可以將源碼 JS 中 import 的模塊路徑進行轉換。例如對 node_modules 中的導入都替換為 web_modules。在這裏對 svg 文件的導入名也會被加上 .proxy.js

code = await transformEsmImports(code, (spec) => {
    ……
    if (spec.startsWith('/') || spec.startsWith('./') || spec.startsWith('../')) {
        const ext = path.extname(spec).substr(1);
        if (!ext) {
            ……
        }
        const extToReplace = srcFileExtensionMapping[ext];
        if (extToReplace) {
            ……
        }
        if (spec.endsWith('.module.css')) {
            ……
        } else if (!isBundled && (extToReplace || ext) !== 'js') {
            const resolvedUrl = path.resolve(path.dirname(outPath), spec);
            allProxiedFiles.add(resolvedUrl);
            spec = spec + '.proxy.js';
        }
        return spec;
    }
    ……
});

此時,我們的 svg 文件和源碼的導入語法(import logo from './logo.svg.proxy.js')均已就緒,最後剩下的就是生成 proxy 文件了。也非常簡單:

for (const proxiedFileLoc of allProxiedFiles) {
    const proxiedCode = await fs.readFile(proxiedFileLoc, {encoding: 'utf8'});
    const proxiedExt = path.extname(proxiedFileLoc);
    const proxiedUrl = proxiedFileLoc.substr(buildDirectoryLoc.length);
    const proxyCode = wrapEsmProxyResponse({
      url: proxiedUrl,
      code: proxiedCode,
      ext: proxiedExt,
      config,
    });
    const proxyFileLoc = proxiedFileLoc + '.proxy.js';
    await fs.writeFile(proxyFileLoc, proxyCode, {encoding: 'utf8'});
 }

wrapEsmProxyResponse 是一個生成 proxy 模塊的方法,目前只處理包括 JSON、image 和其他類型的文件,對於其他類型(包括了圖片),就是非常簡單的導出 url:

return `export default ${JSON.stringify(url)};`;

所以,對於 CSS 與圖片,由於瀏覽器模塊規範均不支持該類型,所以都會轉換為 JS 模塊,這塊 snowpack 和 webpack 實現很類似。

3.3. HMR(熱更新)

如果你剛才仔細去看了 wrapEsmProxyResponse 方法,會發現對於 CSS “模塊”,它除了有注入 CSS 的功能代碼外,還多着這麼幾行:

import * as __SNOWPACK_HMR_API__ from '/${buildOptions.metaDir}/hmr.js';
import.meta.hot = __SNOWPACK_HMR_API__.createHotContext(import.meta.url);
import.meta.hot.accept();
import.meta.hot.dispose(() => {
  document.head.removeChild(styleEl);
});

這些代碼就是用來實現熱更新的,也就是 HMR(Hot Module Replacement)。它使得當一個模塊更新時,應用會在前端自動替換該模塊,而不需要 reload 整個頁面。這對於依賴狀態構建的單頁應用開發非常友好。

import.meta 是一個包含模塊元信息的對象,例如模塊自身的 url 就可以在這裏面取到。而 HMR 其實和 import.meta 沒太大關係,snowpack 只是借用這塊地方存儲了 HMR 相關功能對象。所以不必過分糾結於它。

我們再來仔細看看上面這段 HMR 的功能代碼,API 是不是很熟悉?可下面這段對比一下

import _ from 'lodash';
import printMe from './print.js';

function component() {
  const element = document.createElement('div');
  const btn = document.createElement('button');

  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  btn.innerHTML = 'Click me and check the console!';
  btn.onclick = printMe;

  element.appendChild(btn);

  return element;
}

document.body.appendChild(component());
+
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }

上面的代碼取自 webpack 官網上 HMR 功能的使用説明,可見,snowpack 站在“巨人”的肩膀上,沿襲了 webpack 的 API,其原理也及其相似。網上關於 webpack HMR 的講解文檔很多,這裏就不細説了,基本的實現原理就是:

  • snowpack 進行構建,並 watch 源碼;
  • 在 snowpack 服務端與前端應用間建立 websocket 連接;
  • 當源碼變動時,重新構建,完成後通過 websocket 將模塊信息(id/url)推送給前端應用;
  • 前端應用監聽到這個消息後,根據模塊信息加載模塊
  • 同時,觸發該模塊之前註冊的回調事件,這個在以上代碼中就是傳入 acceptdispose 中的方法

因此,wrapEsmProxyResponse 裏構造出的這段代碼

import.meta.hot.dispose(() => {
  document.head.removeChild(styleEl);
});

其實就是表示,當該 CSS 更新並要被替換時,需要移除之前注入的樣式。而執行順序是:遠程模塊 --> 加載完畢 --> 執行舊模塊的 accept 回調 --> 執行舊模塊的 dispose 回調。

snowpack 中 HMR 前端核心代碼放在了 assets/hmr.js。代碼也非常簡短,其中值得一提的是,不像 webpack 使用向頁面添加 script 標籤來加載新模塊,snowpack 直接使用了原生的 dynamic import 來加載新模塊:

const [module, ...depModules] = await Promise.all([
  import(id + `?mtime=${updateID}`),
  ...deps.map((d) => import(d + `?mtime=${updateID}`)),
]);

也是秉承了使用瀏覽器原生 JavaScript Modules 能力的理念。


小憩一下。看完上面的內容,你是不是發現,這些技術方案都和 webpack 的實現非常類似。snowpack 正是借鑑了這些前端開發的優秀實踐,而其一開始的理念也很明確:為前端開發提供一個不需要打包器(Bundler)的構建工具。

webpack 的一大知識點就是優化,既包括構建速度的優化,也包括構建產物的優化。其中一個點就是如何拆包。webpack v3 之前有 CommonChunkPlugin,v4 之後通過 SplitChunk 進行配置。使用聲明式的配置,比我們人工合包拆包更加“智能”。合併與拆分是為了減少重複代碼,同時增加緩存利用率。但如果本身就不打包,自然這兩個問題就不再存在。而如果都是直接加載 ESM,那麼 Tree-Shaking 的所解決的問題也在一定程度上也被緩解了(當然並未根治)。

再結合最開始提到的性能與兼容性,如果這兩個坎確實邁了過去,那我們何必要用一個內部流程複雜、上萬行代碼的工具來解決一個不再存在的問題呢?

好了,讓我們回來繼續聊聊 snowpack 裏其他特性的實現。


3.4. 環境變量

通過環境來判斷是否關閉調試功能是一個非常常見的需求。

if (process.env.NODE_ENV === 'production') {
  disableDebug();
}

snowpack 中也實現了環境變量的功能。從使用文檔上來看,你可以在模塊中的 import.meta.env 上取到變量。像下面這麼使用:

if (import.meta.env.NODE_ENV === 'production') {
  disableDebug();
}

那麼環境變量是如何被注入進去的呢?

還是以 build 的源碼為例,在代碼生成的階段上,通過 wrapImportMeta 方法的調用生成了新的代碼段,

code = wrapImportMeta({code, env: true, hmr: false, config});

那麼經過 wrapImportMeta 處理後的代碼和之前有什麼區別呢?答案從源碼裏就能知曉:

export function wrapImportMeta({
  code,
  hmr,
  env,
  config: {buildOptions},
}: {
  code: string;
  hmr: boolean;
  env: boolean;
  config: SnowpackConfig;
}) {
  if (!code.includes('import.meta')) {
    return code;
  }
  return (
    (hmr
      ? `import * as  __SNOWPACK_HMR__ from '/${buildOptions.metaDir}/hmr.js';\nimport.meta.hot = __SNOWPACK_HMR__.createHotContext(import.meta.url);\n`
      : ``) +
    (env
      ? `import __SNOWPACK_ENV__ from '/${buildOptions.metaDir}/env.js';\nimport.meta.env = __SNOWPACK_ENV__;\n`
      : ``) +
    '\n' +
    code
  );
}

對於包含 import.meta 調用的代碼,snowpack 都會在裏面注入對 env.js 模塊的導入,並將導入值賦在 import.meta.env 上。因此構建後的代碼會變為:

+ import __SNOWPACK_ENV__ from '/__snowpack__/env.js';
+ import.meta.env = __SNOWPACK_ENV__;

if (import.meta.env.NODE_ENV === 'production') {
    disableDebug();
}

如果是在開發環境下,還會加上 env.js 的 HMR。而 env.js 的內容也很簡單,就是直接將 env 中的鍵值作為對象的鍵值,通過 export default 導出。

默認情況下 env.js 只包含 MODE 和 NODE_ENV 兩個值,你可以通過 @snowpack/plugin-dotenv 插件來直接讀取 .env 相關文件。

3.5. CSS Modules 的支持

CSS 的模塊化一直是一個難題,其一個重要的目的就是做 CSS 樣式的隔離。常用的解決方案包括:

  • 使用 BEM 這樣的命名方式
  • 使用 webpack 提供的 CSS Module 功能
  • 使用 styled components 這樣的 CSS in JS 方案
  • shadow dom 的方案

我之前的文章詳細介紹了這幾類方案。snowpack 也提供了類似 webpack 中的 CSS Modules 功能。

import styles from './index.module.css' 

function render() {
    return <div className={styles.main}>Hello world!</div>;
}

而在 snowpack 中啓用 CSS Module 必須要以 .module.css 結尾,只有這樣才會將文件特殊處理:

if (spec.endsWith('.module.css')) {
    const resolvedUrl = path.resolve(path.dirname(outPath), spec);
    allCssModules.add(resolvedUrl);
    spec = spec.replace('.module.css', '.css.module.js');
}

而所有 CSS Module 都會經過 wrapCssModuleResponse 方法的包裝,其主要作用就是將生成的唯一 class 名的 token 注入到文件內,並作為 default 導出:

_cssModuleLoader = _cssModuleLoader || new (require('css-modules-loader-core'))();
const {injectableSource, exportTokens} = await _cssModuleLoader.load(code, url, undefined, () => {
    throw new Error('Imports in CSS Modules are not yet supported.');
});
return `
    ……
    export let code = ${JSON.stringify(injectableSource)};
    let json = ${JSON.stringify(exportTokens)};
    export default json;
    ……
`;

這裏我將 HMR 和樣式注入的代碼省去了,只保留了 CSS Module 功能的部分。可以看到,它其實是借力了 css-modules-loader-core 來實現的 CSS Module 中 token 生成這一核心能力。

以創建的 React 模版為例,將 App.css 改為 App.module.css 使用後,代碼中會多處如下部分:

+ let json = {"App":"_dist_App_module__App","App-logo":"_dist_App_module__App-logo","App-logo-spin":"_dist_App_module__App-logo-spin","App-header":"_dist_App_module__App-header","App-link":"_dist_App_module__App-link"};
+ export default json;

對於導出的默認對象,鍵為 CSS 源碼中的 classname,而值則是構建後實際的 classname。

3.6. 性能問題

還記得雅虎性能優化 35 條軍規麼?其中就提到了通過合併文件來減少請求數。這既是因為 TCP 的慢啓動特點,也是因為瀏覽器的併發限制。而伴隨這前端富應用需求的增多,前端頁面再也不是手工引入幾個 script 腳本就可以了。同時,瀏覽器中 JS 原生的模塊化能力缺失也讓算是火上澆油,到後來再加上 npm 的加持,打包工具呼之欲出。webpack 也是那個時代走過來的產物。

隨着近年來 HTTP/2 的普及,5G 的發展落地,瀏覽器中 JS 模塊化的不斷髮展,這個合併請求的“真理”也許值得我們再重新審視一下。去年 PHILIP WALTON 在博客上發的「Using Native JavaScript Modules in Production Today」就推薦大家可以在生產環境中嘗試使用瀏覽器原生的 JS 模塊功能。

「Using Native JavaScript Modules in Production Today」 這片文章提到,根據之前的測試,非打包代碼的性能較打包代碼要差很多。但該實驗有偏差,同時隨着近期的優化,非打包的性能也有了很大提升。其中推薦的實踐方式和 snowpack 對 node_modules 的處理基本如出一轍。保證了加載不會超過 100 個模塊和 5 層的深度。

同時,由於業務技術形態的原因,我所在的業務線經歷了一次構建工具遷移,對於模塊的處理上也用了類似的策略:業務代碼模塊不合並,只打包 node_modules 中的模塊,都走 HTTP/2。但是沒有使用原生模塊功能,只是模塊的分佈狀態與 snowpack 和該文中提到的類似。從上線後的性能數據來看,性能並未下降。當然,由於並非使用原生模塊功能來加載依賴,所以並不全完相同。但也算有些參考價值。

3.7. JSX / Typescript / Vue / Less …

對於非標準的 JavaScript 和 CSS 代碼,在 webpack 中我們一般會用 babel、less 等工具加上對應的 loader 來處理。最初版的 snowpack 並沒有對這些語法的處理能力,而是推薦將相關的功能外接到 snowpack 前,先把代碼轉換完,再交給 snowpack 構建。

而新版本下,snowpack 已經內置了 JSX 和 Typescript 文件的處理。對於 typescript,snowpack 其實用了 typescript 官方提供的 tsc 來編譯。

對於 JSX 則是通過 @snowpack/plugin-babel 進行編譯,其實際上只是對 @babel/core 的一層簡單包裝,機上 babel 相關配置即可完成 JSX 的編譯。

const babel = require("@babel/core");

module.exports = function plugin(config, options) {
  return {
    defaultBuildScript: "build:js,jsx,ts,tsx",
    async build({ contents, filePath, fileContents }) {
      const result = await babel.transformAsync(contents || fileContents, {
        filename: filePath,
        cwd: process.cwd(),
        ast: false,
      });

      return { result: result.code };
    },
  };
};

從上面可以看到,核心就是調用了 babel.transformAsync 方法。而使用 @snowpack/app-template-react-typescript 模板生成的項目,依賴了一個叫 @snowpack/app-scripts-react 的包,它裏面就使用了 @snowpack/plugin-babel,且相關的 babel.config.json 如下:

{
  "presets": [["@babel/preset-react"], "@babel/preset-typescript"],
  "plugins": ["@babel/plugin-syntax-import-meta"]
}

對於 Vue 項目 snowpack 也提供了一個對應的插件 @snowpack/plugin-vue 來打通構建流程,如果去看下該插件,核心是使用的 @vue/compiler-sfc 來進行 vue 組件的編譯。

此外,對於 Sass(Less 也類似),snowpack 則推薦使用者添加相應的 script 命令:

"scripts": {
  "run:sass": "sass src/css:public/css --no-source-map",
  "run:sass::watch": "$1 --watch"
}

所以實際上對於 Sass 的編譯直接使用了 sass 命令,snowpack 只是按其約定語法對後面的指令進行執行。這有點類似 gulp / grunt,你在 scripts 中定義的是一個簡單的“工作流”。

綜合 ts、jsx、vue、sass 這些語法處理的方式可以發現,snowpack 在這塊自己實現的不多,主要依靠“橋接”已有的各種工具,用一種方式將其融入到自己的系統中。與此類似的,webpack 的 loader 也是這一思想,例如 babel-loader 就是 webpack 和 babel 的橋。説到底,還是指責邊界的問題。如果目標是成為前端開發的構建工具,你可以不去實現已有的這些子構建過程,但需要將其融入到自己的體系裏。

也正是因為近年來前端構建工具的繁榮,讓 snowpack 可以找到各類借力的工具,輕量級地實現了構建流程。

4. 最後聊聊

snowpack 的一大特點是快 —— 全量構建快,增量構建也快。因為不需要打包,所以它不需要像 webpack 那樣構築一個巨大的依賴圖譜,並根據依賴關係進行各種合併、拆分計算。snowpack 的增量構建基本就是改動一個文件就處理這個文件即可,模塊之間算是“鬆散”的耦合。

而 webpack 還有一大痛點就是“外部“依賴的處理,“外部”依賴是指:

  • 模塊 A 運行時對 B 是有依賴關係
  • 但是不希望在 A 構建階段把 B 也拿來一起構建

這時候 B 就像是“外部”依賴。在之前典型的一個解決方式就是 external,當然還可以通過使用前端加載器加載 UMD、AMD 包。或者更進一步,在 webpack 5 中使用 Module Federation 來實現。這一需求的可能場景就是微前端。各個前端微服務如果要統一一起構建,必然會隨着項目的膨脹構建越來越慢,所以獨立構建,動態加載運行的需求也就出現了。

對於打包器來説,import 'B.js' 默認其實就是需要將 B 模塊打包進來,所以我們才需要那麼多“反向”的配置將這種默認行為禁止掉,同時提供一個預期的運行時方案。而如果站在原生 JavaScript Module 的工作方式上來説,import '/dist/B.js' 並不需要在構建的時候獲取 B 模塊,而只是在運行時才有耦合關係。其天生就是構建時非依賴,運行時依賴的。當然,目前 snowpack 在構建時如果缺少的依賴模塊仍然會拋出錯誤,但上面所説的本質上是可實現,難度較打包器會低很多,而且會更符合使用者的直覺。

那麼 snowpack 是 bundleless 的麼?我們可以從這幾個方面來看:

  • 它對業務代碼的處理是 bundleless 的
  • 目前對 node_modules 的處理是做了 bundle 的
  • 它仍然提供了 @snowpack/plugin-webpack / @snowpack/plugin-parcel 這樣的插件來讓你能為生產環境做打包。所以,配合 module/nomodule 技術,它將會有更強的抵禦兼容性問題的能力,這也算是一種漸進式營銷手段

snowpack 會成為下一代構建工具麼?

In 2019, you should use a bundler because you want to, not because you need to.

Add a new Comments

Some HTML is okay.