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

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

snowpack 的代碼很輕量,本文會從實現原理的角度介紹 snowpack 的特色。同時,帶你們一塊兒看看,做爲一個以原生 JavaScript 模塊化爲核心的年輕的構建工具,它是如何實現「老牌」構建工具所提供的那些特性的。css

1. 初識 snowpack

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

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

文章的核心觀點以下:java

在現在(2019年),咱們徹底能夠拋棄打包工具,而直接在瀏覽器中使用瀏覽器原生的 JavaScript 模塊功能。這主要基於三點考慮:node

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

因爲我認爲 webpack 之類的打包工具,「發家」後轉型作構建工具並不是最優解,實是一種陰差陽錯的階段性成果。因此當時對這個項目提到的觀點也很贊同,其中印象最深的當屬它提到的:react

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

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

2. 初窺 snowpack

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

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

因此從它的處理流程上來看,對業務代碼的模塊,基本只須要把 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.

相關文章
相關標籤/搜索