替代 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.

相关文章
相关标签/搜索