Vite多入口组件库构建方案,自带样式导入和treeshaking功能

背景

Vite 在 Web 项目开发中大放异彩,但其实也是可以用于库项目开发的,但是在组件库开发时,通常涉及多入口

虽然 Vite 3.2+ 以后支持了多入口配置,但是并没有提供相关选项让我们将 CSS 和组件关联起来,这样即便我们打包成功,也无法判断组件和样式的关系,从而给库的导出和使用带来不便。

基于这个背景,我们需要编写一个插件尝试寻找二者关系,并顺势注入 CSS 引用,这样就不用再关心样式和入口的关系了。

样式自动导入

作为一个库(主要是组件库),我们希望在引用组件时自动导入样式:

/** component-lib/dist/button.js */
import './assets/button.css'; // 这是我们插件要做的事情,将这行代码注入进来
...
export default Button;

/** component-lib/dist/index.js */ 
import Button from './button.js';
import xxx from './xxx.js';
export { Button, xxx };
...

/** In our project's main file */
// 引用 Button 的同时将一起引入对应的样式文件
import { Button } from 'component-lib';

如上所示,其实要做的很简单,添加一行 import './style.css';; 到生成文件的顶部即可,多个就是多行。作为库的提供者,应该尽可能的提供灵活性,将如何处理这些样式文件的任务,交给用户侧的构建工具

市面上大部分声称自动注入 CSS 的 Vite 插件,都是采用 document.createElement('style') 这样的方式进行的,这并不优雅,并且他假设了当前是浏览器的 DOM 环境。

所以我们的问题就变成了,如何将样式文件和入口文件关联起来?

其实,Vite 在插件的生命周期中,为每一个 chunk 对象都注入了一个属性 viteMetadata,我们可以通过这个属性获取到当前 chunk 关联了哪些资源文件,其中就包括 CSS。

核心代码

基于上面的分析,我们在插件钩子 renderChunk 中进行注入即可,这是最简单和行之有效的方法。

export function plugin(){
  return {
    name: 'vite:inject-css',
    apply: 'build',
    enforce: 'post',
    config() {
      const { rollupOptions, ...lib } = libOptions || {} as LibOptions;
      return {
        build: {
          /**
           * 需要打开这一项,否则多入口下也只会有一个 style.css, 单入口则不受影响。
           */
          cssCodeSplit: true,
        },
      };
    },
    renderChunk(code, chunk) {
      if (!chunk.viteMetadata) return;
      const { importedCss } = chunk.viteMetadata;
      if (!importedCss.size) return;

      let result = code;
      for (const cssFileName of importedCss) {
        let cssFilePath = path.relative(path.dirname(chunk.fileName), cssFileName);
        cssFilePath = cssFilePath.startsWith('.') ? cssFilePath : `./${cssFilePath}`;
        result = `import '${cssFilePath}';\n${result}`
      }
      return result;
    },
  }
}

需要注意的是,需要打开 build.cssCodeSplit,主要是因为在内部实现中,CSS 代码分割开启时才会在 viteMetadata 中记录 chunk 对应那些资源文件,只有文件关系被记录下来,我们才能正常注入。

上方代码是一个最简版本,主要展示核心逻辑,作为生产使用还缺少 sourcemap 的支持,更完善的版本已经发布为 vite-plugin-lib-inject-css,有兴趣的小伙伴可以进行尝试。

多入口构建

如何使用 Vite 创建一个开箱即用,具备 Tree-shaking 功能,还能自动导入样式的组件库呢?

大多数组件库都提供了两种使用方式,一种是全量引入

import Vue from 'vue';
import XxxUI from 'component-lib';
Vue.use(XxxUI);

另一种是按需引入。这种方式通常需要搭配一个第三方插件进行转换,例如 babel-plugin-import

import { Button } from 'component-lib';
// ↓ ↓ ↓ transformed by plugin ↓ ↓ ↓
import Button from 'component-lib/dist/button/index.js'
import 'component-lib/dist/button/style.css'

但最好的使用方式应该是,在正常使用具名导入时,就能自动引入样式,并进行 Tree-shaking。

幸运的是,ES Module 天然具备静态分析能力,主流工具基本都实现了基于 ESM 的 Tree-shaking 功能,比如 webpack/rollup/vite

那么我们只需要以下两步,就能大功告成

  • 将产物格式输出为 ES Module → 开箱即用的 Tree-shaking 功能
  • 使用上面的插件进行样式注入 → 自动导入样式

需要注意的是,CSS 文件的导入是具有副作用的,我们还需要在库的 package.json 文件中声明 sideEffects 字段,防止用户侧构建时 CSS 文件被意外移除。

{
  "name": "component-lib",
  "version": "1.0.0",
  "main": "dist/index.mjs",
  "sideEffects": [
    "**/*.css"
  ]
}

配置示例

上文提到的插件中,第一个参数为可选参数,照搬了 build.lib 的相关配置项。除此之外还提供了一些工具函数,目的是简化配置。

以下是一份多入口组件库的配置示例:

// vite.config.ts
import { libInjectCss, scanEntries } from 'vite-plugin-lib-inject-css';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    libInjectCss(), // 快速使用,其他配置自己写在 build.lib 里即可
    // 参数是 build.lib 的 alias,目的是为了集中配置
    libInjectCss({
      format: ['es'],
      entry: {
        index: 'src/index.ts', // Don't forget the main entry!
        button: 'src/components/button/index.ts',
        select: 'src/components/select/index.ts',
        // Uses with a similar directory structure.
        ...scanEntries([
          'src/components',
          'src/hooks',
        ])
      },
      rollupOptions: {
        output: {
          // Put chunk files at <output>/chunks
          chunkFileNames: 'chunks/[name].[hash].js',
          // Put chunk styles at <output>/styles
          assetFileNames: 'assets/[name][extname]',
        },
      },
    }),
  ],
})

我们的产物结构如下:

--dist
----chunks
----assets
--index.mjs
--button.mjs
--select.mjs
...

随便点开一个组件,比如 dist/button.mjs

import './assets/index2.css'
import { xxx } from 'vue';
const v = ...;

export {
  v as Button
}

可以看到,产物中正确的注入了相关的样式文件,使命达成。

原文链接:https://juejin.cn/post/7214374960192782373 作者:情绪羊

(0)
上一篇 2023年3月25日 下午7:05
下一篇 2023年3月25日 下午7:15

相关推荐

发表回复

登录后才能评论