Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

45. 工程化新秀 #45

Open
funfish opened this issue Jul 15, 2020 · 2 comments
Open

45. 工程化新秀 #45

funfish opened this issue Jul 15, 2020 · 2 comments

Comments

@funfish
Copy link
Owner

funfish commented Jul 15, 2020

炎热的七月,透着一点雨水,就这么来临。半年就如此过去了,看了不少内容,但是想写成博客的却越来越少,可能是人懒了。
最近不少工程化的新秀如后浪般出现,虽然不至于动摇 webpack 这个巨浪,只是对行业也有很深刻的影响,觉得蛮有意思,这里介绍一下:

esbuild

esbuild 做的事情很简单,打包压缩,没有其他的复杂功能,目前也没有其他的插件系统,倒是 esbuild 本身更像一个插件,有点像 webpack 刚出来那会的情况。

esbuild 最大特点就是快,飞快,其本身采用 Go 语言实现,加上高并发的特色,在打包压缩的路上,一骑绝尘。官方数据,和正常的 webpack 相比,在打包方面提高了 100+ 倍以上,这对于需要代码更新后立刻发版到线上的项目而言,超级有意义,这不就是大家一直追求的快速构建嘛。

在构建项目的时候,基本都可以看到这一幕,打包到最后,本以为要结束了,结果进度条一直在 90% 左右的位置,一动不动,尤其是项目大了之后。其实这个最后的过程,是代码丑化、压缩以及 tree-shaking 的过程。代码压缩这部分,在以前的 webpack,是 UglifyjsWebpackPlugin 来处理的,后来内置到 webpack 里面,再后来,由于 uglify-js 不支持 es6,改用 terser 作为 webpack 内置的默认打包压缩工具。即便如此,业务小的时候还好,上来后,打包的时间会非常长。

本地尝试

按照文档思索着建一个最小的 demo,来看看速度如何。按照首页的提示,采用如下内容,分别用 webpack 和 esbuild 来打包:

// 业务内容
import * as React from "react";
import * as ReactDOM from "react-dom";

ReactDOM.render(<h1>Hello, world!</h1>, document.getElementById("root"));

// webpack 配置,只是对jsx采用babel打包,同时还要配置babel的基本配置
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },
};

// esbuild 指令内容,由于采用首页的方式一直报错,最后根据错误,改为如下指令
esbuild --bundle main.jsx --outdir=dist --minify --sourcemap

采用 esbuild 的时候,可以明显感觉到速度飞快,基本上 半秒不到就打包好了,而 webpack 嗯。。。三四秒的样子,速度还是很明显的,可能是因为项目小,没有 100 倍的感觉,但是 esbuild 基本上不用等。只是看看打包的体积,发现 esbuild 的体积比 webpack 的大三倍。这难道是时间换体积?经过排查是 process.env.NODE_ENV 的问题,esbuild 的版本里面包含了 development 和 production 两个模式的内容。官方文档有提示到:

Note that the double quotes around "production" are important because the replacement should be a string, not an identifier. The outer single quotes are for escaping the double quotes in Bash but may not be necessary in other shells.

process.env.NODE_ENV 变量需要配置,并且不能省略 "production" 的引号,只是在 json 里面,添加引号一直无法正常使用,去掉引号会导致无法识别变量,最后采用 api 的方式构建,如下

const { build } = require("esbuild");

build({
  entryPoints: ["./main.jsx"],
  outdir: "dist",
  minify: true,
  bundle: true,
  sourcemap: true,
  define: {
    "process.env.NODE_ENV": '"production"',
  },
}).catch(() => process.exit(1));

需要注意的是,define 里面的 key-value 结构的 value 不能是对象,不支持嵌套的 key。最后会打包有如下效果:

// 原本的 process.env.NODE_ENV 会被替换,development的内容会被设置为null
if (true) {
  checkDCE();
  module.exports = require_react_dom_production_min();
} else {
  module.exports = null;
}

最后回头一看发现和 webpack 打包的体积居然是一模一样的,esbuild 大了 0.5k 不到。另外有个有趣的现象,如果把 bundle 配置去掉,包的内容,真的只有上面的 react 的业务代码。

想看看 esbuild 的源码,专门学了一下 go 语言,发现还是蛮简单(可能是学比较基础)。只是三脚猫功夫直接看源码,还是云里雾里的,也就放弃了。

esbuild-webpack-plugin

看到 umi 里面支持 esbuild,具体可以看看 esbuild-webpack-plugin 的代码。结构是一个典型的 webpack 的插件,通过 esbuild 的 transform 这个 api 来实现打包,可以看看下面的配置:

const transform = async () =>
  await ESBuildPlugin.service.transform(source, {
    ...this.options,
    minify: true,
    sourcemap: !!devtool,
    sourcefile: file,
  });

官方介绍到,如果需要重复调用 esbuild 的 api,最好是实例化 esbuild,达到复用的方式,也就是采用 transform 这个 api。

可以看到上面的代码,采用的配置只是 minify 而已,没有对 bundle 处理,按照作者的介绍

esbuild 有两个功能,bundler 和 minifier。bundler 的功能和 babel 以及 webpack 相比肯定差很多,直接上风险太大;而 minifier 倒是可以试试,在 webpack 和 babel 产物的基础上做一次压缩,坑应该相对较少。

这样确实不错,让 esbuild 做最专业的事情,同时可以继续使用生态丰富的 webpack,而压缩则是 esbuild,作者说到: 试验性功能,可能有坑,但效果拔群,具体的时间效果也不对比了,送上传送门。效果只是减少 1/3,估计是 webpack 本身其他操作占用了时间。

这个插件有配合 umi 的部分,但也可以用到其他 webapck 项目里面。具体是要配置 optimization.minimizer 如下:

optimization: {
  minimizer: [new (require("esbuild-webpack-plugin").default)()];
}

正常的 webpack 会采用 terser 作为内置的默认压缩工具,这里面改为 Esbuild 就可以了。

ES Module

上面的 esbuild,可以说很好的解决了生产模式的压缩疼点,提高打包速度,但是开发环境呢?能用上 esbuild 吗?当然也是可以的,只是最优解并非如此。

有一次,看到一个线上地址 https://iconsvg.xyz/ 的页面,打开控制台一看发现居然是采用 ES Module 的形式,如下图。

现在的浏览器基本已经支持 ES 模块化了,直接模块化有什么不可以?直接用在生产环境会有很多问题,比如请求加载数量,比如兼容性,那对于开发环境呢?如 vite 和 snowpack 这样的工具已经就是 bundleless 的工具,在开发环境上采用 ES Module 的方式实现快速热更新。

对于 ES Module,目前文件扩展名为 .js 结构,有推荐采用 .mjs 后缀,可以更清晰的表明是个模块,由于兼容问题,现在采用 .js 后缀就可以了。

应用的时候要采用下面的格式,来声明这个脚本是一个模块:

<script type="module" src="main.mjs"></script>

如果没有声明 type="module" 浏览器会提示 Uncaught SyntaxError: Cannot use import statement outside a module 错误。

vite

vite 在开发环境通过解析文件返回到浏览器,不会有打包过程。这样当修改项目某个文件的时候,只会向浏览器发送更新该文件的请求,而不会去处理别的文件,最终打包的速度项目大小没有关系,可以很大提高开发环境热更新效率。需要注意的是 vite 在生产环境采用 rollup 打包。

开发服务器劫持

vite 在开发环境的定位和 webpack-dev-server 是有点像的,都是作为一个开发服务器,响应客户端的请求。先看看 demo 上具体的效果,官方直接提供一个 create-vite-app 项目作为起步脚手架模板,上面提供 vue 到 react 的模板,采用 template-vue 模板,启动的时间非常快,基本上按下回车差不多就跑起来了。可以看看下图:

几秒钟的时间,项目就启动完毕了,对比一下 vue-cli 3,差不多要 10s 的样子,当然也是由于业务体积的问题,少量的业务,webpack 自然是非常快的(复杂的例子,就没有了,因为 vite 支持的是 vue-next,老项目用的是 vue 2 可能支持力度不好,无法迁移)。

通过控制台可以看一下,发起的请求:

前面是 vite 加载过程,后面是 vue-cli 3 的项目,可以明显看到 vite 是直接请求了 .vue 文件以及 vue.js 文件,而 vue-cli 3 则是请求打包好后的开发文件,只是前图的 vite 里面明明是一个 App.vue 文件为什么会请求三次呢?这里就要说一下 vite 作为开发服务器对网络的劫持作用。

vite 里面会启动一个 koa 服务器,采用中间件的方式对请求的文件劫持,结构如下

// 省略部分代码
const resolvedPlugins = [
  moduleRewritePlugin,
  htmlRewritePlugin,
  moduleResolvePlugin,
  proxyPlugin,
  serviceWorkerPlugin,
  hmrPlugin,
  vuePlugin,
  cssPlugin,
  esbuildPlugin,
  jsonPlugin,
  assetPathPlugin,
  serveStaticPlugin,
];

插件的配置从查找模块、模块路径重写到 vue、css 等资源的处理,客户端请求什么内容,就由专门的中间件处理。比如入口,请求 http://localhost:3000/ 返回的是 index.html,但是结果如下:

中间的 script 部分是和原 index.html 不一样的。额外加载 hmr 文件,正是上面 vite 请求网络图里面的 hmr 请求,同时还注入了全局的环境变量 process.env.NODE_ENV,可以看一下是如何实现的:

// htmlRewritePlugin 的内容,下面是注入的代码
const devInjectionCode =
  `\n<script type="module">\n` +
  `import "${hmrClientPublicPath}"\n` +
  `window.__DEV__ = true\n` +
  `window.process = { env: ${JSON.stringify({
    ...env,
    NODE_ENV: mode,
    BASE_URL: "/",
  })}}\n` +
  `</script>\n`;
async function rewriteHtml(importer: string, html: string) {
  // 省略缓存以及script标签替换内容
  return injectScriptToHtml(html, devInjectionCode);
}
app.use(async (ctx, next) => {
  await next();
  if (ctx.status === 304) {
    return;
  }
  if (ctx.response.is("html") && ctx.body) {
    // 省略部分代码
    ctx.body = await rewriteHtml(importer, html);
    return;
  }
});

除了 html 的特殊处理外,vite 还会对 import { createApp } from "vue" 这样的 import 语句重写路径为 import { createApp } from "/@modules/vue.js",前者的路径客户端是无法正常找到的,通过重写 @modules vite 可以明白这是一个第三方模块包的请求,对于这些 node_modules 的包可以做一系列的优化,后面会介绍到。

vue 文件处理

对于 vue 单文件的处理,首个文件的访问路径还是源于 main.js 的正常 import,但是到了 vite,.vue 文件则会被 vuePlugin 处理,毕竟浏览器无法识别 .vue 文件,需要解析再返回给浏览器。先看看原始代码 App.vue:

// 原始文件
<template>
  <div class="hehe">522215{{ a }}</div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      a: 123
    };
  }
};
</script>

<style scoped>
.hehe {
  background: red;
}
</style>

拦截后输出的文件

截图是返回的 App.vue 文件,可以看到原始的 .vue 文件变成一个 js 文件,也就是上图的代码。上图仅保留了原 App.vue 里面的 script 部分,渲染模板 template 以及样式 style 在 script 部分里通过 import 的方式引入,一个 .vue 文件拆分成三个。于是就有了左边 network 里面请求的 App.vue?type=style&index=0App.vue?type=template。拆分成三个请求,每个请求各司其责,比如更新 template 的时候,就发送新的 template 文件到客户端,避免一次修改三个文件:script、template 和 style 混在一起发送,可以说很巧妙。

vuePlugin 里面的实现,更多的是对请求路径的参数判断,如果参数 type 为 undefined(就是 script)、template 以及 style,都分别处理,同时在 script 的时候,如果文件是 typescript,还会采用 esbuild 的 transform API 来编译代码。

三个请求的由来,其实可以追朔到 vue 对 sfc 文件的解析,在 sfc 单文件处理的模块里面,会通过 ast 的方式将文件拆分成,script、template 和 style 三个模式,自然 vite 里面应该按照这三个模式更新 vue 是最合理的。

热更新机制

上面截图以及代码部分可以看到 hmr 的字样,hmr 则是代表热更新的部分。热更新分为两部分,一部分在客户端,一部分在开发服务器。客户端的主要热更新的代码,在 html 访问的时候,已经通过 import "${hmrClientPublicPath}" 这样的方式加载,而 vite 也会通过 chokidar 来监听访问过的文件,当文件变化的时候,会通过 websocket 来通知客户端,再由客户端请求具体的更新代码。

// 客户端主要代码
socket.addEventListener("message", async ({ data }) => {
  switch (type) {
    case "connected":
      console.log(`[vite] connected.`);
      break;
    case "vue-reload":
    // 重新加载vue
    case "vue-rerender":
    // vue 组件重新渲染
    case "style-update":
    // 样式更新
    case "style-remove":
    // 移除样式
    case "js-update":
    // js更新,react项目更新依赖这个
    case "custom":
    // 自定义的,目前没有用到好像
    case "full-reload":
    // 整个页面重新加载,
  }
});

客户端对 vue-rerender 的指令,在加载文件后,会直接调用 vue-next 里面的热更新的函数:

// @vue/runtime-core > hmr > rerender 代码
function rerender(id: string, newRender?: Function) {
  const record = map.get(id)
  if (!record) return
  // Array.from creates a snapshot which avoids the set being mutated during
  // updates
  Array.from(record).forEach(instance => {
    if (newRender) {
      instance.render = newRender as InternalRenderFunction
    }
    instance.renderCache = []
    // this flag forces child components with slot content to update
    isHmrUpdating = true
    instance.update()
    isHmrUpdating = false
  })
}

可以看到这里将新的 render 注入,也就是 template 解析后生成的渲染函数,再调用实例的 update 方法,而这个 update 方法是,vue-next 里面渲染组件的主要入口,采用 effect 的方式。

服务端监听本地文件的变化。在 vue 的中间件里面,会对更新后的文件发送对应的指令,这里提一下重新加载 vue 文件和重新渲染 vue 组件的处理的方式不同。

// descriptor 是 vue-sfc 里面通过 ast 分析出来的描述器
if (!isEqualBlock(descriptor.script, prevDescriptor.script)) {
  return sendReload();
}

if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
  needRerender = true;
}

可以看到如果前后脚本不一致会重新加载,而如果只是模板不一样,则只会重新渲染组件。这里可以看到是对 vue 的处理,那如果是 react 项目呢?

react 项目处理

在上面的代码里面,我们经常可以看到 vue 的影子,比如 vue 的中间件,vue 的客户端的热更新代码,而对于 react 是需要特殊的配置的,这里我们看看 react 项目的配置时候需要的插件:

// @ts-check
const reactPlugin = require("vite-plugin-react");

/**
 * @type { import('vite').UserConfig }
 */
const config = {
  jsx: "react",
  plugins: [reactPlugin],
};

module.exports = config;

在 vite.config.js 里面需要按照如上配置,而之前的 vue-next 则是什么都不用写。可以明显感觉到 vite 里面 vue-next 是一等生,毕竟连客户端的热更新代码,都用到 vue 的热更新部分。。。。

通过 vite-plugin-react 可以向 vite 项目提供更多的中间件,这个也是类似于 vue 的中间件,只是一个是内置,一个第三方包来配置。通过劫持 html 返回自己的运行时更新代码 react-refresh 部分以及 vite 的 hmr 客户端代码。

//  vite-plugin-react 里面代码
module.exports = {
  resolvers: [resolver],
  configureServer: reactRefreshServerPlugin,
  transforms: [reactRefreshTransform],
};

// vite 里面处理插件的 transforms 的方法
app.use(async (ctx, next) => {
  await next();

  const { path, query } = ctx;
  let code: string | null = null;

  for (const t of transforms) {
    if (t.test(path, query)) {
      ctx.type = "js";
      if (ctx.body) {
        code = code || (await readBody(ctx.body));
        if (code) {
          ctx.body = await t.transform(
            code,
            isImportRequest(ctx),
            false,
            path,
            query
          );
          code = ctx.body;
        }
      }
    }
  }
});

reactRefreshServerPlugin 会先服务器添加中间件,当访问 html 代码的时候,则会注入基本的全局代码;transforms 则会在 vite 开发服务器搭建的时候,通过 transforms 方式添加中间件,对 jsx/tsx 文件处理,注入以下关键代码。

const header = `
  import RefreshRuntime from "${runtimePublicPath}";
  let prevRefreshReg;
  let prevRefreshSig;
  if (!window.__vite_plugin_react_preamble_installed__) {
    throw new Error(
      "vite-plugin-react can't detect preamble. Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201"
    );
  }
  if (import.meta.hot) {
    prevRefreshReg = window.$RefreshReg$;
    prevRefreshSig = window.$RefreshSig$;
    window.$RefreshReg$ = (type, id) => {
      RefreshRuntime.register(type, ${JSON.stringify(path)} + " " + id)
    };
    window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
  }`.replace(/[\n]+/gm, "");

const footer = `
  if (import.meta.hot) {
    window.$RefreshReg$ = prevRefreshReg;
    window.$RefreshSig$ = prevRefreshSig;
    import.meta.hot.accept();
    RefreshRuntime.performReactRefresh();
  }`;

上面是注入的代码,header 和 footer。可以看出来来,主要注入的部分是热更新相关的。其中有个很特别的地方 import.meta.hot,这个是 vite 特有的标记;

For manual HMR, an API is provided via import.meta.hot.
For a module to self-accept, use import.meta.hot.accept:

export const count = 1;
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    console.log("updated: count is now ", newModule.count);
  });
}

这是 import.meta.hot 的用法,对于正常的需要热更新的代码文件,可以通过 import.meta.hot 这个条件语句判断。当内容更新的时候,加载内容,并执行下面 accept 的回调,至于回调里面如何处理,则需要自己控制了。react 采用的则是 import.meta.hot 的方式,更新的方式,当然是通过 react-refresh 来。

上面客户端热更新方式里面,有一种是 js-update,当 jsx 文件更新的时候,会通知到客户端执行 js-upload,并最终加载新的 jsx 文件,当然同时也包含上面的添加的代码。

在 js-update 里面,会分析服务端下发文件的 id 路径,而加载哪些文件,则是根据这个 id 路径来判断的,通过分析 id 的所有依赖,依次加载。这些依赖的来源,并不是 webpack 打包时候分析的 import 的包,而是需要用户调用 accept 或者 acceptDeps 显示添加的,以及依赖更新后的回调函数。比如下面的方式:

import { foo } from "./foo.js";

foo();

if (import.meta.hot) {
  import.meta.hot.acceptDeps("./foo.js", (newFoo) => {
    // the callback receives the updated './foo.js' module
    newFoo.foo();
  });

  // Can also accept an array of dep modules:
  import.meta.hot.acceptDeps(
    ["./foo.js", "./bar.js"],
    ([newFooModule, newBarModule]) => {
      // the callback receives the updated modules in an Array
    }
  );
}

官网介绍的这种方式,通过 acceptDeps 指明依赖的路径,当文件变化的时候(指的是自身或者 import 进来的文件),会加载 acceptDeps 中的文件,执行对应的回调。如果不需要指出具体的依赖,比如像 react 的方式,采用 import.meta.hot.accept(),表明是自身的更新,或者是自身 import 的文件的更新,重新加载本身,也就是 jsx 文件就好了。

回到 react 身上,采用 import.meta.hot.accept() 的方式加 react-fresh 的热更新,好像不是最稳妥的,毕竟每次修改,都要重新加载一次文件,再去更新,没有 vue 来得优雅。当然还有就是不像 sfc 那样需要拆分成三个文件。

vite 启动

前面介绍了 vite 的拦截,vue 和 react 的处理,但是在一开始的时候会解析 package.json 中的文件,对 dependencies 中的包缓存到 node_modules/.vite_opt_cache 里面,不管项目中有没有遇到。多次访问的时候,缓存可以提高访问速度,比如对 vue-next 访问速度的提高。

snowpack

snowpack 和 vite 都是优秀 ES Module 加载方案,发力的领域也是开发环境。vite 文档介绍到,项目依赖关系的处理是受到 snowpack 的启发,在开发环境上都是会启动一个开发服务器,并且解析返回速度也是类似的。可以看出来 vite 有不少方面是借鉴了 snowpack 。

当然 vite 有自己特色的部分,比如 热更新,可以做到深入到 vue 的热更新机制,以及调用 react 的热更新,当然 vite 里面 vue 是第一公民。snowpack 不同于 vite 的地方在于,其构建的时候,支持 webpack 和 Parcel 等,这样无疑对开发者更加友好。

这里很好的介绍了 snowpack 构建的 O(1) 的过程,基本上每次文件更新都小于 50ms。well,现在 webpack 5 也做了很多优化,本地开发没有这么不堪了。上图也适用于 vite,两者都是 ES Module 级别的构建。

snowpack 的劫持

snowpack 和 vite 很不一样,vite 使用 koa 中间件的方式,对不同的文件处理,snowpack 没有中间件的概念,没有 koa 甚至是 express,采用 http-proxy、http 和 http2 来搭建开服服务器。

先看看网络加载情况

可以看出在 vite 里面 App.vue 被拆分成三个文件加载,而这里,只是分成两个文件,app.js 包含 script 和 template, app.css.proxy.js 则是 style 部分。

snowpack 采用外部插件来解析 vue 的方式,比如 vue 项目里面的配置:

// snowpack.config.json 里面的配置
"extends": "@snowpack/app-scripts-vue"

// @snowpack/app-scripts-vue 里面的配置
const scripts = {
  "mount:public": "mount public --to /",
  "mount:src": "mount src --to /_dist_",
};

module.exports = {
  scripts,
  plugins: ["@snowpack/plugin-vue", "@snowpack/plugin-dotenv"],
  installOptions: {},
  devOptions: {},
};

// @snowpack/plugin-vue 里面的配置
module.exports = function plugin(config, pluginOptions) {
  return {
    defaultBuildScript: "build:vue",
    async build({ contents, filePath }) {
      // 采用 @vue/compiler-sfc 里面的parse来编译 sfc 文件,和 vite 的编译是一样的。
    }
  }
}

如上面的结构,通过加载插件里面的 build 方法,实现对 sfc 文件的解析,中间过程比 vite 要复杂一些,vite 的中间件体系很直观,而 snowpack 则是通过不断的分析 config.scripts 里面的配置(通过不断的调用 fs.stat 判断),来得到正确的文件路径以及对应的解析方式,比如 _dist_/App.js 最后会转换为 src/App.vue,并采用上述的 @snowpack/plugin-vue 的 build 方法加载 src/App.vue,得到打包后的 script/template 组成的部分,以及 css 内容。发送到客户端并作缓存处理后。

build 方法里面会通过 parse 编译 sfc 文件,得到的 descriptor 和 vite 的差不多,包含 script、template 和 style 三个部分,其中 script 部分的代码会和 tempalte 的代码合并也就是后面 App.js 的主体。 style 作为单独的部分不会立刻发送到客户端,而是先做本地缓存里面。

css 部分会有如下处理方式

// snowpack dev.js wrapResponse
if (responseFileExt === ".js") {
  code = wrapImportMeta({ code, env: true, hmr: isHmr, config });
}
if (responseFileExt === ".js" && cssResource) {
  code =
    `import './${path.basename(reqPath).replace(/.js$/, ".css.proxy.js")}';\n` +
    code;
}

可以看到在 App.js 里面添加 css 的 import 部分,这个和 vite 类似,只是 css 文件后缀采用 css.proxy.js 标识,而 vite 采用 type=style 的方式来区分。

另外 snowpack 对 html 的处理,会有一个 isRoute 变量来判断,并注入热更新等代码;

热更新机制

分为两套代码,客户端代码和开发服务器的代码,其中客户端的代码没有 vite 种类复杂

socket.addEventListener("message", ({ data: _data }) => {
  if (!_data) {
    return;
  }
  const data = JSON.parse(_data);
  debug("message", data);
  if (data.type === "reload") {
    reload();
    return;
  }
  if (data.type !== "update") {
    return;
  }
  runModuleAccept(data.url)
    .then((ok) => {
      if (!ok) {
        reload();
      }
    })
    .catch((err) => {
      console.error(err);
      reload();
    });
});

可以看出 snowpack 只有 reload 和 update 模式,没有 vite 那样复杂,但是其 js 部分更新逻辑是基本一致的,并且有很相同的 import.meta.hot 方式以及 import.meta.hot.accept 功能。基本和 vite 差不多,这里就不介绍了,当然 snowpack 不用判断 import.meta.hot 是不是在 if 条件语句里面。

snowpack 没有像 vite 那样在客户端采用 vue 的热更新。

snowpack 启动的时候,也会对依赖进行分析,不同的是它会将依赖放在 node_modules/.cache/snowpack/dev 下面。node_modules 包的请求路径也会被改写为 web_modules/vue.js 这样的特殊标记。

webpack

在 snowpack 还可以使用 webpack,官方专门维护了 @snowpack/plugin-webpack 插件,和上面的 @snowpack/plugin-vue 一样都归属于插件范畴,在解析文件的时候会用到,提供一个 build 方法,并且最后通过 webpack 打包文件。snowpack 提供了一些默认配置,比如 babel、MiniCssExtractPlugin 这些。如果要扩展的话采用以下的方式配置,和 vue.config.js 的方式蛮像的。

// snowpack.config.js
module.exports = {
  plugins: [
    [
      "@snowpack/plugin-webpack",
      {
        extendConfig: (config) => {
          config.plugins.push(/* ... */);
          return config;
        },
      },
    ],
  ],
};

这里 webpack 的处理方式蛮奇怪的,会将打包好之后的文件,手动注入到 html 里面,而不是采用默认的方式,可能是没有 index.html?可能也是受限于 snowpack 和 webpack 的结合?具体的也就没有深入研究了,感兴趣的可以看看。

总结

上面介绍了三款最近流行的打包工具,esbuild 用于生产环境,vite 和 snowpack 主要用于开发环境。esbuild 打包压缩速度远超同行,也被用于 vite 和 snowpack 里面,作为 JavaScript 文件和 Typescript 文件降级和编译的工具,esbuild 如果要用于生产的话,可以考虑使用 esbuild-webpack-plugin,仅仅作为压缩工具,效率也能提高不少。

vite 里面有不少借鉴 snowpack 的部分,当然也有自己特别的方式,比如中间件的结构,比如客户端更精准的热更新,当然和 snowpack 一样支持 webpack 更好了,只是目前看来难度不大?两者都可以用在生产,目前看来 vite 采用 rollup 打包,离主流 webpack 有点远,而 snowpack 支持 webpack 所以友好度更高。当然 vite 有尤大佬参与,自然不太一样。

本文还有不少源码没有深入介绍到,只是做一个稍微浅的解读,感兴趣的可以继续深入研究,如果能理解 esbuild 的 go 语言的源码就更好了。

@BuptStEve
Copy link

https://iconsvg.xyz/
这个是传了 sourcemap 吧...

@funfish
Copy link
Owner Author

funfish commented Jul 16, 2020

https://iconsvg.xyz/
这个是传了 sourcemap 吧...

应该是的,不知道有没有线上的 ES Module 例子,总不能自己写一个部署上去吧

@funfish funfish closed this as completed Jul 16, 2020
@funfish funfish reopened this Jul 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants