<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 原理,你還學得動么

          共 15680字,需瀏覽 32分鐘

           ·

          2020-07-14 09:12


          6d129e1339eae33d983ceac69bf7d9e3.webp

          作者: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 模塊功能。這主要基于三點考慮:

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

          由于我認為 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 能力。

          8a448638b2b168c6cb2eb50bbbba14c0.webp

          所以從它的處理流程上來看,對業(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)境的。

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

          1. import CSS / image / …:由于 webpack 一切皆模塊的理念 + 組件化開發(fā)的深入人心,import anything 的書寫模式已經(jīng)深入開發(fā)者的觀念中。對 CSS 等內容依賴與加載能力的缺失,將成為它的阿克琉斯之踵。
          2. 語法轉換能力:作為目標成為構建工具的 snowpack(當時叫 web),并沒有能夠編譯 Typescript、JSX 等語法文件的能力,你當然可以再弄一個和它毫無關系的工具來處理語法,但是,這不就是構建工具應該集成的么?
          3. HMR:這可能不那么要命,但俗話說「由儉入奢易,由奢入儉難」,被“慣壞”開發(fā)者們自然會有人抵觸這一特性的缺失。
          4. 性能:雖說它指出,上了 HTTP2 后,使用 JavaScript modules 性能并不會差,但畢竟沒有實踐過,對此還是抱有懷疑。
          5. 環(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)有了解決方案。

          c9668fee9483ff4f98ec71a053c967f4.webp

          抱著學習的態(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(熱更新)

          507f65f0a9d9b4faa16bd10705595c65.webp

          如果你剛才仔細去看了 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ā)該模塊之前注冊的回調事件,這個在以上代碼中就是傳入 acceptdispose 中的方法

          因此,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)的構建工具。

          651500c6bf691c61199b5c615d55d102.webp

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

          3a5571b654ebd0602d3ae9a8d9cb9b7d.webp ? ? ? ? ?

          而在 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)物。

          6a1b051a65e72eb4bdf45bd78cb940b8.webp

          隨著近年來 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 來編譯。

          6abd9a96ddf4bf905c67ba00cde88fc4.webp ? ? ? ? ?

          對于 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 技術,它將會有更強的抵御兼容性問題的能力,這也算是一種漸進式營銷手段
          10da950955382c5f1efc55957028220c.webp

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

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



          推薦閱讀




          我的公眾號能帶來什么價值?(文末有送書規(guī)則,一定要看)

          每個前端工程師都應該了解的圖片知識(長文建議收藏)

          為什么現(xiàn)在面試總是面試造火箭?

          瀏覽 55
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  国产欧美日韩免费看 | 波多野结衣一区二区精品系列11 | 人人操91 | 亚洲三级视频手机在线观看 | 夜夜操网站 |