使用 Vite 建立灵活的外部仓库

0. Vite 与 ESM

与 Webpack 不同,Vite 以 ESM 为其唯一的模块管理规范。首先,在开发环境,它会把每个文件编译成独立的 ESM 模块,实现非常快速的热加载。其次,编译打包时,它默认的目标环境是 ES2019,支持 ESM,所以模块打包后,也会使用 ESM 加载。

这给我们带来一个好处:如果用 Vite 开发项目,并且对其进行分包,构建之后放到线上(比如发布到 NPM);接下来我们就可以在其它项目中,使用 ESM 方式加载这个项目的代码。

举个简单的例子。lodash 有一个同步发布的 lodash-es 包,功能完全一致,只是使用 ESM 构建,我们可以直接在代码中 import forEach from 'https://unpkg.com/lodash-es@4.17.21/forEach.js' 引用。一方面可以节省我们自己的带宽;另一方面如果用户在其它应用里使用过同一个库,就可以提高速度。

1. Vite 分包

Vite 把构建过程委托给 Rollup,所以构建时分包需要传参给 rollupOptions

在某个项目中,我需要整合一批 codepen 上的绘图效果,这些效果都放在 /src/effects/ 目录下,所以我就要检查这个目录,并且生成对应的分包配置。

export default defineConfig(async () => {
  // 从 v10.10 开始,node.js 的 `fs.readdir()` 函数支持 `withFileTypes` 参数,使用这个参数可以直接返回 `fs.Dirent` 对象,类似使用 `fs.stat()` 得到的 `fs.Stats` 对象。方便我们判断对象类型
  const files = await readdir(effectsDirectory, {withFileTypes: true});
  // 把目录下的内容分为两类,一个是基础类库,一个是不同特效
  const [effects, baseFiles] = files.reduce(([effects, base], file) => {
    const {name} = file;
    if (file.isDirectory()) {
      effects.push(name);
    } else if (file.isFile()) {
      base.push(name);
    }
    return [effects, base];
  }, [[], []]);

  return {
    build: {
      rollupOptions: {
        manualChunks(id) {
          // effects/some-effect 下的文件按目录分别打包
          const effect = effects.find(effect => id.includes(`/${effect}/`));
          if (effect) {
            return effect;
          }
          // 效果基类打包成一个文件,因为效果只需要基类,所以从主体剥离
          const baseFile = baseFiles.find(base => id.endsWith(base) && !/p5/i.test(id));
          if (baseFile) {
            return 'BaseEffect';
          }
          // p5 是个很大的效果库,官方不提供 esm 包,只能单独打一个
          if (/p5/i.test(id)) {
            return 'p5';
          }
          // 其它依赖正常打包,只在本项目中使用,不会被引用
          return id.includes('node_modules') ? 'vendor' : 'chuck';
        },
      },
      // 这个第3节会解释
      target: 'es2020',
    },
  },
}

2. 去掉文件名中的 hash

Vite 很贴心的帮我们给生成的文件都加上了 hash。在独立项目中,给文件名加 hash 可以有效避免缓存问题;但是作为外部仓库的话,无法确定的 hash 会增加业务项目的开发难度,所以我希望构建时输出到特定版本号的目录里,然后去掉文件名中的 hash。

这个操作同样需要修改 rollupOptions。rollup 有三个不同的选项分别处理不同的命名,这里我们可以忽略入口文件(entryFileNames),只改剩下两个。

export default defineConfig(() => {
  return {
    build: {
      rollupOptions: {
        output: {
          // 资源文件,包括 css
          assetFileNames: 'assets/[name].[ext]',
          // 分包文件
          chunkFileNames: '[name].js',
        },
      },
    },
  };
});

3. 动态加载 CSS

使用 Vite 开发时,我们同样可以在代码里 import 样式等非 JS 素材。构建时,Vite 会把它们处理后放在合适的地方。

可惜的是,Vite 并不会帮我们自动加载分包后的素材。需要我们手动处理。这时就要利用 import.meta.url,它会返回当前模块的 URL,配合前面的的文件名策略,我们就可以完成动态加载,而不需要业务项目的开发者手动处理。

但是 Vite 默认的版本基线是 ES2019,并不支持 import.meta.url,所以我们需要把 build.target 设置成 ES2020 或以上。

let isCssLoaded = false;

// 只有未加载且处于生产环境才加载 css。
if (!isCssLoaded && __IS_PROD__) {
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  // 这一步非常重要,因为 vite/rollup 有 bug,会把 `import.meta.url` 翻译成 `self.location`,导致出错
  const baseUrl = import.meta.url;
  link.href = new URL('./assets/particle-orb.css', baseUrl).toString();
  document.head.appendChild(link);
  link.onload = () => {
    isCssLoaded = true;
  }
}

4. 总结

新的技术选型总能给我们带来新的可能,ESM 之后,我们在项目之间复用代码也有了新的选择,赶紧用起来吧。

如果你在使用 Vite 或者 ESM 时遇到什么问题,欢迎提问。如果有什么经验,也欢迎分享。

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

标签:

评论

欢迎吐槽,共同进步

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据