替代 webpack?帶你了解 snowpack 原理,你還學得動么

作者:AlienZHOU
來源:https://zhuanlan.zhihu.com/p/149351900
近期,隨著 vue3 的各種曝光,vite 的熱度上升,與 vite 類似的 snowpack 的關注度也逐漸增加了。目前(2020.06.18)snowpack 在 Github 上已經(jīng)有了將近 1w stars。
snowpack 的代碼很輕量,本文會從實現(xiàn)原理的角度介紹 snowpack 的特點。同時,帶大家一起看看,作為一個以原生 JavaScript 模塊化為核心的年輕的構建工具,它是如何實現(xiàn)“老牌”構建工具所提供的那些特性的。
1. 初識 snowpack
近期,隨著 vue3 的各種曝光,vite 的熱度上升,與 vite 類似的 snowpack 的關注度也逐漸增加了。目前(2020.06.18)snowpack 在 Github 上已經(jīng)有了將近 1w stars。
時間撥回到 2019 年上半年,一天中午我百無聊賴地讀到了 A Future Without Webpack 這篇文章。通過它了解到了 pika/snowpack 這個項目(當時還叫 pika/web)。
文章的核心觀點如下:
在如今(2019年),我們完全可以拋棄打包工具,而直接在瀏覽器中使用瀏覽器原生的 JavaScript 模塊功能。這主要基于三點考慮:
- 兼容性可接受:基本主流的瀏覽器版本都支持直接使用 JavaScript Module 了(當然,IE 一如既往除外)。
- 性能問題的改善:之前打包的一個重要原因是 HTTP/1.1 的特性導致,我們合并請求來優(yōu)化性能;而如今 HTTP/2 普及之后,這個性能問題不像以前那么突出了。
- 打包的必要性:打包工具的存在主要就是為了處理模塊化與合并請求,而以上兩點基本解決這兩個問題;再加之打包工具越來越復雜,此消彼長,其存在的必要性自然被作者所質疑。
由于我認為 webpack 之類的打包工具,“發(fā)家”后轉型做構建工具并非最優(yōu)解,實是一種陰差陽錯的階段性成果。所以當時對這個項目提到的觀點也很贊同,其中印象最深的當屬它提到的:
In 2019, you should use a bundler because you want to, not because you need to.
2. 初窺 snowpack
看到這片文章后(大概是19年6、7月?),抱著好奇立刻去 Github 上讀了這個項目。當時看這個項目的時候大概是 0.4.x 版本,其源碼和功能都非常簡單。
snowpack 的最初版核心目標就是不再打包業(yè)務代碼,而是直接使用瀏覽器原生的 JavaScript Module 能力。

所以從它的處理流程上來看,對業(yè)務代碼的模塊,基本只需要把 ESM 發(fā)布(拷貝)到發(fā)布目錄,再將模塊導入路徑從源碼路徑換為發(fā)布路徑即可。
而對 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 版本的源碼可以看出,其初期功能確實非常簡單,甚至有些簡陋,以至于缺乏很多現(xiàn)代前端開發(fā)所需的特性,明顯是不能用于生產(chǎn)環(huán)境的。
直觀感受來說,它當時就欠缺以下能力:
- import CSS / image / …:由于 webpack 一切皆模塊的理念 + 組件化開發(fā)的深入人心,import anything 的書寫模式已經(jīng)深入開發(fā)者的觀念中。對 CSS 等內容依賴與加載能力的缺失,將成為它的阿克琉斯之踵。
- 語法轉換能力:作為目標成為構建工具的 snowpack(當時叫 web),并沒有能夠編譯 Typescript、JSX 等語法文件的能力,你當然可以再弄一個和它毫無關系的工具來處理語法,但是,這不就是構建工具應該集成的么?
- HMR:這可能不那么要命,但俗話說「由儉入奢易,由奢入儉難」,被“慣壞”開發(fā)者們自然會有人抵觸這一特性的缺失。
- 性能:雖說它指出,上了 HTTP2 后,使用 JavaScript modules 性能并不會差,但畢竟沒有實踐過,對此還是抱有懷疑。
- 環(huán)境變量:這雖然是一個小特性,但在我接觸過的大多數(shù)項目中都會用到它,它可以幫助開發(fā)者自動測卸載線上代碼中的調試工具,可以根據(jù)環(huán)境判斷,自動將埋點上報到不同的服務上。確實需要一個這樣好用的特性。
3. snowpack 的進化
時間回到 2020 年上半年,隨著 vue3 的不斷曝光,與其有一定關聯(lián)的另一個項目 vite 也逐漸吸引了人們的目光。而其介紹中提到的 snowpack 也突然吸引到了更多的熱度與討論。當時我只是對 pika 感到熟悉,好奇的點開 snowpack 項目主頁的時候,才發(fā)現(xiàn)這個一年前初識的項目(pika/web)已經(jīng)升級到了 pika/snowpack v2。而項目源碼也不再是之前那唯一而簡單的 index.ts,在核心代碼外,還包含了諸多官方插件。
看著已經(jīng)完全變樣的 Readme,我的第一直覺是,之前我想到的那些問題,應該已經(jīng)有了解決方案。

抱著學習的態(tài)度,對它進行重新了解之后,發(fā)現(xiàn)果然如此。好奇心趨勢我對它的解決方案去一探究竟。
本文寫于 2020.06.18,源碼基于 [email protected]
3.1. import CSS
import CSS 的問題還有一個更大的范圍,就是非 JavaScript 資源的加載,包括圖片、JSON 文件、文本等。
先說說 CSS。
import?'./index.css';
上面這種語法目前瀏覽是不支持的。所以 snowpack 用了一個和之前 webpack 很類似的方式,將 CSS 文件變?yōu)橛糜谧⑷霕邮降?JS 模塊。如果你熟悉 webpack,肯定知道如果你只是在 loader 中處理 CSS,那么并不會生成單獨的 CSS 文件(這就是為什么會有 [mini-css-extract-plugin](https://link.zhihu.com/?target=https%3A//webpack.js.org/plugins/mini-css-extract-plugin/)),而是加載一個 JS 模塊,然后在 JS 模塊中通過 DOM API 將 CSS 文本作為 style 標簽的內容插入到頁面中。
為此,snowpack 自己寫了一個簡單的模板方法,生成將 CSS 樣式注入頁面的 JS 模塊。下面這段代碼可以實現(xiàn)樣式注入的功能:
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 中的實現(xiàn)代碼比我們上面多了一些東西,不過與樣式注入無關,這個放到后面再說。
通過將 CSS 文件的內容保存到 JS 變量,然后再使用 JS 調用 DOM API 在頁面注入 CSS 內容即可使用 JavaScript Modules 的能力加載 CSS。而源碼中的 index.css 也會被替換為 index.css.proxy.js:
-?import?'./index.css';
+?import?'./index.css.proxy.js';
proxy 這個名詞之后會多次出現(xiàn),因為為了能夠以模塊化方式導入非 JS 資源,snowpack 把生成的中間 JavaScript 模塊都叫做 proxy。這種實現(xiàn)方式也幾乎和 webpack 一脈相承。
3.2. 圖片的 import
在目前的前端開發(fā)場景中,還有一類非常典型的資源就是圖片。
import?avatar?from?'./avatar.png';
function?render()?{
??return?(
????<div?class="user">
??????<img?src={avatar}?/>
????div>
??);
}
上面代碼的書寫方式已經(jīng)普遍應用在很多項目代碼中了。那么 snowpack 是怎么處理的呢?
太陽底下沒有新鮮事,snowpack 和 webpack 一樣,對于代碼中導入的 avatar 變量,最后其實都是該靜態(tài)資源的 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 的處理方式。
首先是將源碼中的靜態(tài)文件(logo.svg)拷貝到發(fā)布目錄:
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 與圖片,由于瀏覽器模塊規(guī)范均不支持該類型,所以都會轉換為 JS 模塊,這塊 snowpack 和 webpack 實現(xiàn)很類似。
3.3. HMR(熱更新)

如果你剛才仔細去看了 wrapEsmProxyResponse 方法,會發(fā)現(xiàn)對于 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);
});
這些代碼就是用來實現(xiàn)熱更新的,也就是 HMR(Hot Module Replacement)。它使得當一個模塊更新時,應用會在前端自動替換該模塊,而不需要 reload 整個頁面。這對于依賴狀態(tài)構建的單頁應用開發(fā)非常友好。
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 官網(wǎng)上 HMR 功能的使用說明,可見,snowpack 站在“巨人”的肩膀上,沿襲了 webpack 的 API,其原理也及其相似。網(wǎng)上關于 webpack HMR 的講解文檔很多,這里就不細說了,基本的實現(xiàn)原理就是:
- snowpack 進行構建,并 watch 源碼;
- 在 snowpack 服務端與前端應用間建立 websocket 連接;
- 當源碼變動時,重新構建,完成后通過 websocket 將模塊信息(id/url)推送給前端應用;
- 前端應用監(jiān)聽到這個消息后,根據(jù)模塊信息加載模塊
- 同時,觸發(fā)該模塊之前注冊的回調事件,這個在以上代碼中就是傳入
accept和dispose中的方法
因此,wrapEsmProxyResponse 里構造出的這段代碼
import.meta.hot.dispose(()?=>?{
??document.head.removeChild(styleEl);
});
其實就是表示,當該 CSS 更新并要被替換時,需要移除之前注入的樣式。而執(zhí)行順序是:遠程模塊 --> 加載完畢 --> 執(zhí)行舊模塊的 accept 回調 --> 執(zhí)行舊模塊的 dispose 回調。
snowpack 中 HMR 前端核心代碼放在了 [assets/hmr.js](https://link.zhihu.com/?target=https%3A//github.com/pikapkg/snowpack/blob/v2.5.1/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 能力的理念。
小憩一下??赐晟厦娴膬热荩闶遣皇前l(fā)現(xiàn),這些技術方案都和 webpack 的實現(xiàn)非常類似。snowpack 正是借鑒了這些前端開發(fā)的優(yōu)秀實踐,而其一開始的理念也很明確:為前端開發(fā)提供一個不需要打包器(Bundler)的構建工具。

webpack 的一大知識點就是優(yōu)化,既包括構建速度的優(yōu)化,也包括構建產(chǎn)物的優(yōu)化。其中一個點就是如何拆包。webpack v3 之前有 CommonChunkPlugin,v4 之后通過 SplitChunk 進行配置。使用聲明式的配置,比我們人工合包拆包更加“智能”。合并與拆分是為了減少重復代碼,同時增加緩存利用率。但如果本身就不打包,自然這兩個問題就不再存在。而如果都是直接加載 ESM,那么 Tree-Shaking 的所解決的問題也在一定程度上也被緩解了(當然并未根治)。
再結合最開始提到的性能與兼容性,如果這兩個坎確實邁了過去,那我們何必要用一個內部流程復雜、上萬行代碼的工具來解決一個不再存在的問題呢?
好了,讓我們回來繼續(xù)聊聊 snowpack 里其他特性的實現(xiàn)。
3.4. 環(huán)境變量
通過環(huán)境來判斷是否關閉調試功能是一個非常常見的需求。
if?(process.env.NODE_ENV?===?'production')?{
??disableDebug();
}
snowpack 中也實現(xiàn)了環(huán)境變量的功能。從使用文檔上來看,你可以在模塊中的 import.meta.env 上取到變量。像下面這么使用:
if?(import.meta.env.NODE_ENV?===?'production')?{
??disableDebug();
}
那么環(huán)境變量是如何被注入進去的呢?
還是以 build 的源碼為例,在代碼生成的階段上,通過 [wrapImportMeta 方法的調用](https://zhuanlan.zhihu.com/p/149351900/https%3C/code%3E://github.com/pikapkg/snowpack/blob/v2.5.1/src/commands/build.ts#L346)生成了新的代碼段,
code?=?wrapImportMeta({code,?env:?true,?hmr:?false,?config});
那么經(jīng)過 wrapImportMeta 處理后的代碼和之前有什么區(qū)別呢?答案從源碼里就能知曉:
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 上。因此構建后的代碼會變?yōu)椋?/p>
+?import?__SNOWPACK_ENV__?from?'/__snowpack__/env.js';
+?import.meta.env?=?__SNOWPACK_ENV__;
if?(import.meta.env.NODE_ENV?===?'production')?{
??disableDebug();
}
如果是在開發(fā)環(huán)境下,還會加上 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 都會經(jīng)過 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 來實現(xiàn)的 CSS Module 中 token 生成這一核心能力。
以創(chuàng)建的 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. 性能問題
還記得雅虎性能優(yōu)化 35 條軍規(guī)么?其中就提到了通過合并文件來減少請求數(shù)。這既是因為 TCP 的慢啟動特點,也是因為瀏覽器的并發(fā)限制。而伴隨這前端富應用需求的增多,前端頁面再也不是手工引入幾個 script 腳本就可以了。同時,瀏覽器中 JS 原生的模塊化能力缺失也讓算是火上澆油,到后來再加上 npm 的加持,打包工具呼之欲出。webpack 也是那個時代走過來的產(chǎn)物。

隨著近年來 HTTP/2 的普及,5G 的發(fā)展落地,瀏覽器中 JS 模塊化的不斷發(fā)展,這個合并請求的“真理”也許值得我們再重新審視一下。去年 PHILIP WALTON 在博客上發(fā)的「Using Native JavaScript Modules in Production Today」就推薦大家可以在生產(chǎn)環(huán)境中嘗試使用瀏覽器原生的 JS 模塊功能。
「Using Native JavaScript Modules in Production Today」 這片文章提到,根據(jù)之前的測試,非打包代碼的性能較打包代碼要差很多。但該實驗有偏差,同時隨著近期的優(yōu)化,非打包的性能也有了很大提升。其中推薦的實踐方式和 snowpack 對 node_modules 的處理基本如出一轍。保證了加載不會超過 100 個模塊和 5 層的深度。
同時,由于業(yè)務技術形態(tài)的原因,我所在的業(yè)務線經(jīng)歷了一次構建工具遷移,對于模塊的處理上也用了類似的策略:業(yè)務代碼模塊不合并,只打包 node_modules 中的模塊,都走 HTTP/2。但是沒有使用原生模塊功能,只是模塊的分布狀態(tài)與 snowpack 和該文中提到的類似。從上線后的性能數(shù)據(jù)來看,性能并未下降。當然,由于并非使用原生模塊功能來加載依賴,所以并不全完相同。但也算有些參考價值。
3.7. JSX / Typescript / Vue / Less …
對于非標準的 JavaScript 和 CSS 代碼,在 webpack 中我們一般會用 babel、less 等工具加上對應的 loader 來處理。最初版的 snowpack 并沒有對這些語法的處理能力,而是推薦將相關的功能外接到 snowpack 前,先把代碼轉換完,再交給 snowpack 構建。
而新版本下,snowpack 已經(jīng)內置了 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 只是按其約定語法對后面的指令進行執(zhí)行。這有點類似 gulp / grunt,你在 scripts 中定義的是一個簡單的“工作流”。
綜合 ts、jsx、vue、sass 這些語法處理的方式可以發(fā)現(xiàn),snowpack 在這塊自己實現(xiàn)的不多,主要依靠“橋接”已有的各種工具,用一種方式將其融入到自己的系統(tǒng)中。與此類似的,webpack 的 loader 也是這一思想,例如 babel-loader 就是 webpack 和 babel 的橋。說到底,還是指責邊界的問題。如果目標是成為前端開發(fā)的構建工具,你可以不去實現(xiàn)已有的這些子構建過程,但需要將其融入到自己的體系里。
也正是因為近年來前端構建工具的繁榮,讓 snowpack 可以找到各類借力的工具,輕量級地實現(xiàn)了構建流程。
4. 最后聊聊
snowpack 的一大特點是快 —— 全量構建快,增量構建也快。因為不需要打包,所以它不需要像 webpack 那樣構筑一個巨大的依賴圖譜,并根據(jù)依賴關系進行各種合并、拆分計算。snowpack 的增量構建基本就是改動一個文件就處理這個文件即可,模塊之間算是“松散”的耦合。
而 webpack 還有一大痛點就是“外部“依賴的處理,“外部”依賴是指:
- 模塊 A 運行時對 B 是有依賴關系
- 但是不希望在 A 構建階段把 B 也拿來一起構建
這時候 B 就像是“外部”依賴。在之前典型的一個解決方式就是 external,當然還可以通過使用前端加載器加載 UMD、AMD 包。或者更進一步,在 webpack 5 中使用 Module Federation 來實現(xiàn)。這一需求的可能場景就是微前端。各個前端微服務如果要統(tǒng)一一起構建,必然會隨著項目的膨脹構建越來越慢,所以獨立構建,動態(tài)加載運行的需求也就出現(xiàn)了。
對于打包器來說,import 'B.js' 默認其實就是需要將 B 模塊打包進來,所以我們才需要那么多“反向”的配置將這種默認行為禁止掉,同時提供一個預期的運行時方案。而如果站在原生 JavaScript Module 的工作方式上來說,import '/dist/B.js' 并不需要在構建的時候獲取 B 模塊,而只是在運行時才有耦合關系。其天生就是構建時非依賴,運行時依賴的。當然,目前 snowpack 在構建時如果缺少的依賴模塊仍然會拋出錯誤,但上面所說的本質上是可實現(xiàn),難度較打包器會低很多,而且會更符合使用者的直覺。
那么 snowpack 是 bundleless 的么?我們可以從這幾個方面來看:
- 它對業(yè)務代碼的處理是 bundleless 的
- 目前對 node_modules 的處理是做了 bundle 的
- 它仍然提供了 @snowpack/plugin-webpack / @snowpack/plugin-parcel 這樣的插件來讓你能為生產(chǎn)環(huán)境做打包。所以,配合 module/nomodule 技術,它將會有更強的抵御兼容性問題的能力,這也算是一種漸進式營銷手段

snowpack 會成為下一代構建工具么?
In 2019, you should use a bundler because you want to, not because you need to.
推薦閱讀
我的公眾號能帶來什么價值?(文末有送書規(guī)則,一定要看)
每個前端工程師都應該了解的圖片知識(長文建議收藏)
為什么現(xiàn)在面試總是面試造火箭?
