<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 15726字,需瀏覽 32分鐘

           ·

          2020-07-28 15:42


          作者:AlienZHOU

          來源:https://zhuanlan.zhihu.com/p/149351900

          近期,隨著 vue3 的各種曝光,vite 的熱度上升,與 vite 類似的 snowpack 的關(guān)注度也逐漸增加了。目前snowpack 在 Github 上已經(jīng)超過 1w stars。

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

          1. 初識 snowpack

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

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

          文章的核心觀點如下:

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

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

          由于我認為 webpack 之類的打包工具,“發(fā)家”后轉(zhuǎn)型做構(gòu)建工具并非最優(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è)務(wù)代碼,而是直接使用瀏覽器原生的 JavaScript Module 能力。

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

          而對 node_modules 則通過遍歷 package.json 中的依賴,按該依賴列表為粒度將 node_modules 中的依賴打包。以 node_modules 中每個包的入口作為打包 entry,使用 rollup 生成對應(yīng)的 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)境的。

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

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

          3. snowpack 的進化

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

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

          抱著學(xué)習的態(tài)度,對它進行重新了解之后,發(fā)現(xiàn)果然如此。好奇心驅(qū)使我對它的解決方案去一探究竟。

          本文寫于 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 標簽的內(nèi)容插入到頁面中。

          為此,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)代碼比我們上面多了一些東西,不過與樣式注入無關(guān),這個放到后面再說。

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

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

          proxy 這個名詞之后會多次出現(xiàn),因為為了能夠以模塊化方式導(dǎo)入非 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)普遍應(yīng)用在很多項目代碼中了。那么 snowpack 是怎么處理的呢?

          太陽底下沒有新鮮事,snowpack 和 webpack 一樣,對于代碼中導(dǎo)入的 avatar 變量,最后其實都是該靜態(tài)資源的 URI。

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

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

          初始化模版運行后,可以看到源碼與構(gòu)建后的代碼差異如下:

          -?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,其模塊內(nèi)容為:

          //?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 的關(guān)鍵方法調(diào)用。這個方法可以將源碼 JS 中 import 的模塊路徑進行轉(zhuǎn)換。例如對 node_modules 中的導(dǎo)入都替換為 web_modules。在這里對 svg 文件的導(dǎo)入名也會被加上 .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 文件和源碼的導(dǎo)入語法(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 和其他類型的文件,對于其他類型(包括了圖片),就是非常簡單的導(dǎo)出 url:

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

          所以,對于 CSS 與圖片,由于瀏覽器模塊規(guī)范均不支持該類型,所以都會轉(zhuǎn)換為 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)。它使得當一個模塊更新時,應(yīng)用會在前端自動替換該模塊,而不需要 reload 整個頁面。這對于依賴狀態(tài)構(gòu)建的單頁應(yīng)用開發(fā)非常友好。

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

          我們再來仔細看看上面這段 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)上關(guān)于 webpack HMR 的講解文檔很多,這里就不細說了,基本的實現(xiàn)原理就是:

          • snowpack 進行構(gòu)建,并 watch 源碼;
          • 在 snowpack 服務(wù)端與前端應(yīng)用間建立 websocket 連接;
          • 當源碼變動時,重新構(gòu)建,完成后通過 websocket 將模塊信息(id/url)推送給前端應(yīng)用;
          • 前端應(yīng)用監(jiān)聽到這個消息后,根據(jù)模塊信息加載模塊
          • 同時,觸發(fā)該模塊之前注冊的回調(diào)事件,這個在以上代碼中就是傳入 acceptdispose 中的方法

          因此,wrapEsmProxyResponse 里構(gòu)造出的這段代碼

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

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

          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 能力的理念。


          小憩一下??赐晟厦娴膬?nèi)容,你是不是發(fā)現(xiàn),這些技術(shù)方案都和 webpack 的實現(xiàn)非常類似。snowpack 正是借鑒了這些前端開發(fā)的優(yōu)秀實踐,而其一開始的理念也很明確:為前端開發(fā)提供一個不需要打包器(Bundler)的構(gòu)建工具。

          webpack 的一大知識點就是優(yōu)化,既包括構(gòu)建速度的優(yōu)化,也包括構(gòu)建產(chǎn)物的優(yōu)化。其中一個點就是如何拆包。webpack v3 之前有 CommonChunkPlugin,v4 之后通過 SplitChunk 進行配置。使用聲明式的配置,比我們?nèi)斯ず习鸢印爸悄堋薄:喜⑴c拆分是為了減少重復(fù)代碼,同時增加緩存利用率。但如果本身就不打包,自然這兩個問題就不再存在。而如果都是直接加載 ESM,那么 Tree-Shaking 的所解決的問題也在一定程度上也被緩解了(當然并未根治)。

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

          好了,讓我們回來繼續(xù)聊聊 snowpack 里其他特性的實現(xiàn)。


          3.4. 環(huán)境變量

          通過環(huán)境來判斷是否關(guān)閉調(diào)試功能是一個非常常見的需求。

          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 方法的調(diào)用](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 調(diào)用的代碼,snowpack 都會在里面注入對 env.js 模塊的導(dǎo)入,并將導(dǎo)入值賦在 import.meta.env 上。因此構(gòu)建后的代碼會變?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 的內(nèi)容也很簡單,就是直接將 env 中的鍵值作為對象的鍵值,通過 export default 導(dǎo)出。

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

          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 結(jié)尾,只有這樣才會將文件特殊處理:

          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 注入到文件內(nèi),并作為 default 導(dǎo)出:

          _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;

          對于導(dǎo)出的默認對象,鍵為 CSS 源碼中的 classname,而值則是構(gòu)建后實際的 classname。

          3.6. 性能問題

          還記得雅虎性能優(yōu)化 35 條軍規(guī)么?其中就提到了通過合并文件來減少請求數(shù)。這既是因為 TCP 的慢啟動特點,也是因為瀏覽器的并發(fā)限制。而伴隨著前端富應(yīng)用需求的增多,前端頁面再也不是手工引入幾個 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è)務(wù)技術(shù)形態(tài)的原因,我所在的業(yè)務(wù)線經(jīng)歷了一次構(gòu)建工具遷移,對于模塊的處理上也用了類似的策略:業(yè)務(wù)代碼模塊不合并,只打包 node_modules 中的模塊,都走 HTTP/2。但是沒有使用原生模塊功能,只是模塊的分布狀態(tài)與 snowpack 和該文中提到的類似。從上線后的性能數(shù)據(jù)來看,性能并未下降。當然,由于并非使用原生模塊功能來加載依賴,所以并不完全相同。但也算有些參考價值。

          3.7. JSX / Typescript / Vue / Less …

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

          而新版本下,snowpack 已經(jīng)內(nèi)置了 JSX 和 Typescript 文件的處理。對于 typescript,snowpack 其實用了 typescript 官方提供的 tsc 來編譯。

          ? ? ? ? ?

          對于 JSX 則是通過 @snowpack/plugin-babel 進行編譯,其實際上只是對 @babel/core 的一層簡單包裝,加上 babel 相關(guān)配置即可完成 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
          ??????};
          ????},
          ??};
          };

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

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

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

          此外,對于 Sass(Less 也類似),snowpack 則推薦使用者添加相應(yīng)的 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ā)的構(gòu)建工具,你可以不去實現(xiàn)已有的這些子構(gòu)建過程,但需要將其融入到自己的體系里。

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

          4. 最后聊聊

          snowpack 的一大特點是快 —— 全量構(gòu)建快,增量構(gòu)建也快。因為不需要打包,所以它不需要像 webpack 那樣構(gòu)筑一個巨大的依賴圖譜,并根據(jù)依賴關(guān)系進行各種合并、拆分計算。snowpack 的增量構(gòu)建基本就是改動一個文件就處理這個文件即可,模塊之間算是“松散”的耦合。

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

          • 模塊 A 運行時對 B 是有依賴關(guān)系
          • 但是不希望在 A 構(gòu)建階段把 B 也拿來一起構(gòu)建

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

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

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

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

          snowpack 會成為下一代構(gòu)建工具么?

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


          • 關(guān)注公眾號【前端宇宙】,每日獲取好文推薦
          • 添加微信,入群交流

          瀏覽 98
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲在线无码 | 丁香五月天婷婷婷 | 亚洲AV日韩AV高潮喷人人爽 | 欧美一区二区丁香五月天激情 | 成人黄片AV256 |