Skip to content

组件的构建

为什么要构建(编译)

在经过一系列迭代之后, 我们满足了大部分需求,且在项目中运行良好。

接下来为了给更多的项目使用,我们需要把这个组件提出来,变成一个单独的 npm 包。

为什么要提炼成单独的 npm 包?

不然你为了共享就需要 cv 整个组件库到不同的项目里,这样每次更新都要重新 cv,而且容易出错

那么,我们可以直接把这些 vue / vue jsx 组件不经过任何编译,就直接发布 .vue 文件到 npm 上吗?

答案是既可以,也不可以,而且极其不推荐这么做!

为什么?

首先我们来回答一下,为什么可以

为什么“可以”这么做?

从技术角度看,npm 并不限制你发布什么类型的文件。你完全可以上传原始的 .vue 文件或包含 JSX 的 .tsx/.jsx 文件。

那么消费者在安装你了这个包之后,只要项目配置了相应的编译支持,例如通过 vite@vitejs/plugin-vue, @vitejs/plugin-vue-jsx 或者 webpackvue-loader 去直接加载这些 .vue 文件,那么它们是可以被正确解析的。

但是你有没有发现,这样无形中添加了额外的编译步骤,同时也非常依赖项目中的编译器版本?

比如你 .vue 文件里面使用了 vue@3.3+ 版本才加入的 defineOptions 编译宏,但是项目中使用的 vue 编译器版本是 3.2,那么就会直接报错用不了了。

但是预先把 .vue 文件编译成 js 文件,这个就可以兼容老版本的 vue

比如你在 .vue 文件中使用了 defineOptions

js
defineOptions({
  inheritAttrs: false,
  customOptions: {
    /* ... */
  },
  data() {
    return {
      version: '3.3+'
    }
  }
})

编译结果为:

js
const __sfc__ = /*@__PURE__*/Object.assign({
  inheritAttrs: false,
  customOptions: {
    /* ... */
  },
  data() {
    return {
      version: '3.3+'
    }
  }
}, {
  __name: 'App',
  setup(__props, { expose: __expose }) {
  __expose();



const __returned__ = {  }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}

});

显然,这个编译结果,在低版本中 vue3 中也能兼容。

但为什么“也不可以”这么做(强烈不推荐)?

1. 依赖环境不一致

不同项目使用的打包工具、Babel/ Vite 配置、vue 版本、tsconfig 等配置可能不一样。这会导致组件在某些项目中无法正确加载、解析或渲染。

2. 构建性能差

每个使用你这个 npm 包的项目都需要对 .vue 文件进行编译,增加了构建时间,浪费计算资源。发布前预编译, 直接引入产物跳过编译,能大大提高下游使用者的效率。

3. 无法 tree-shaking

源码形式往往不具备良好的 tree-shaking 能力,可能导致打包出来的文件变大,最终影响页面加载性能。

4. 类型定义丢失或不准确

如果你用 TypeScript 开发组件但不编译,使用者可能得不到准确的类型提示。尤其是 .vue 文件本身包含 script/template/style,类型推导更加复杂,依赖 vscode vue 插件感应。但是预编译出对应的 d.ts 类型定义文件,那么编辑器原生就支持这个智能感应,无需依赖其他插件。

5. 发布 npm 包会暴露内部结构

发布源码等于把你内部的组织结构完全暴露出来。使用者很容易误用“私有”接口或非 API 级别的代码,增加维护负担。

6. 无法在 Node 中直接执行

有些服务端渲染或测试环境(如 jest)并不会处理 .vue 文件,这会导致包无法运行或测试失败。

开始构建

在打包 Vue 组件库时,请使用 Vite的库模式 来进行构建,因为这是官方维护的推荐方式。

快速示例见本项目的 packages/ice-ui 目录

Vite 构建

通用的库文件配置

js
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'

const __dirname = dirname(fileURLToPath(import.meta.url))

export default defineConfig({
  build: {
    // 启用库模式
    lib: {
      // 打包入口,可以单个入口,也可以传入一个对象/数组作为多个入口
      entry: resolve(__dirname, 'lib/index.ts'),
      // 公开的全局变量 umd/iife 用
      name: 'ui',
      // 包文件输出的名称
      fileName: 'index',
    },
    rollupOptions: {
      // 把 vue 作为 外部依赖,不加这个,vite/rollup 会把 vue 所有引用到的源代码,
      // 全部打入产物中去,这没有必要因为组件本来就需要在已经有 vue 的环境运行
      external: ['vue'],
      output: {
        // umd 用,假设全局已经有 vue 对象
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
})

但是这个这个配置,只能打包 ts/js 文件,不能打包 vue 组件

因为目前 vite 目前还不认识 .vue 文件是什么,想让它认识需要注册插件

这里就要分为 vue3vue2 两种情况

Vue3

vite-plugin-vue

想要通过编写..vue 文件来构建 vue3 组件,需要使用 @vitejs/plugin-vue,加入你还想使用 jsx 来构建组件库,那还需要安装 @vitejs/plugin-vue-jsx

Vue2

同上

构建产物

vite 库模式构建产物中,默认会生成 esmumd 格式的 js 文件,假如写了样式的话还会生成 css

其中 umd 格式是给 cdn/传统 ssr 场景下准备的产物,esm 格式是给正常引入 esm 包/和现代 ssr 场景下准备的产物

生成 .d.ts

默认是不会生成 .d.ts 类型定义文件的,需要安装 vite-plugin-dts 来生成 .d.ts

一般场景下的配置如下

ts
  plugins: [
    dts(
      {
        // 生效的 tsconfig.json 路径, 不设置默认 tsconfig.json
        tsconfigPath: './tsconfig.app.json',
        // 组件的目录
        entryRoot: './lib',
      },
    ),
  ],

生成结果

通过这些配置,你再运行 vite build 之后得到的产物结构就如下所示

bash
index.css
index.d.ts
index.js
index.umd.cjs
....

接下来就需要去进行测试验证了

Released under the CC BY-NC-SA 4.0 License.