Skip to content
当前页导航

组件库

有哪些组件的开发模式

  • 利用 options API + TS
  • 利用 composition API + TS,比如 element-ui
  • 利用 TSX 语法, 比如 ant-design-vue、naive-ui
  • 利用 TSX + render 函数,比较老的用法

monorepo 开发模式介绍

Monorepo(单一仓库)是一种源代码管理策略,它将多个项目或组件的代码存储在同一个版本控制系统仓库中。这种模式在大型项目或由多个团队协作开发的项目中特别有用。

优点:

  • 代码共享,在不同项目或组件中,能更容易互相引用代码。
  • 简化依赖管理,不同的项目或组件,依赖都在统一的地方,能减少版本冲突和重复依赖。
  • 便捷的团队协作,所有成员都能看到所有代码及项目变更。

缺点: 项目会变得庞大,管理起来比较困难

组件库为什么要有一个完整的样式体系

保证不同开发者开发出来的组件,样式风格能够保持统一。

为什么要做单元测试

  1. 保证质量:单元测试可以确保每个组件按预期工作。它可以捕捉在开发过程中可能遗漏的错误,提高组件库的整体质量。

  2. 易于维护:当你对组件进行修改或扩展时,单元测试可以帮助你确保当前的更改没有破坏现有功能。这使得维护和升级组件库更加容易和安全。

  3. 文档作用:单元测试不仅是测试代码,同时也是关于如何使用组件的一个很好的文档。它们展示了组件的预期用法和行为。

  4. 快速反馈:在开发过程中,单元测试提供了快速的反馈机制。你可以立即知道你的更改是否有效,而不是在开发过程后期或生产环境中才发现问题。

  5. 简化调试:当出现问题时,良好的单元测试可以帮助你快速定位到出错的部分,简化调试过程。

  6. 促进设计:编写可测试的代码通常会有更好的设计决策和更清晰的代码结构。单元测试可以鼓励我们编写松耦合且高内聚的代码。

  7. 团队协作:在团队协作的环境中,单元测试可以提高信心,确保新加入的代码不会破坏现有功能,促进团队内部的协作和交流。

  8. 持续集成/持续部署(CI/CD):单元测试是 CI/CD 流程的关键组成部分。自动化测试使得持续集成和部署流程更加可靠和高效。

总的来说,对组件库进行单元测试是一个非常重要的步骤,它有助于提高代码质量,减少后期的 bug 修复成本,同时也是实现敏捷开发和持续交付的关键要素。

Vitest 单元测试库

它基于 vite ,与 jest 兼容,而且只会重新运行更改的文件。

插件 @vue/test-utils 可以用来测试 vue 文件。

执行 vitest coverage 命令,可以查看项目测试文件的覆盖率,即有多少文件写了测试用例

tsx
// index.test.tsx
import { mount } from '@vue/test-utils'
import { Button } from 'tov-ui'

describe('button', () => {
  it('should work', () => {
    const wrapper = mount(<Button type="primary">测试</Button>)
    const btnEl = wrapper.find('button')
    const hasPrimay = btnEl.element.classList.contains('tov-button--primary')
    expect(hasPrimay).toBe(true)
    wrapper.unmount()
    // dashed
  })

  it('size', () => {
    const wrapper = mount(<Button size="small">测试</Button>)
    const btnEl = wrapper.find('button')
    expect(btnEl.element.classList.contains('tov-button-size--small')).toBe(
      true
    )
    wrapper.unmount()
  })

  it('click', () => {
    // 测试事件
    let clickState = false
    const handleClick = () => {
      clickState = true
    }
    const wrapper = mount(<Button onClick={handleClick}>测试</Button>)
    wrapper.trigger('click')
    expect(clickState).toBe(true)
    wrapper.unmount()
  })
})
  • describe 定义一个新测试
  • it 创建一个测试内容
  • expect 创建一个断言,相当于判断语句
  • mount 挂载 vue 内容
  • unmount 卸载 vue 内容

组件库如何打包

ESM 和 CJS 包的区别

  • ESM (ECMAScript Modules)
  1. es module 是 js 官方的模块化系统,它的语法是 importexport
  2. 支持异步加载,更适合浏览器环境,并且浏览器就直接支持 es module 语法。
  3. 在编译时进行解析和加载。这意味着 import 引入文件只能在模块最顶层,并且不能动态改变。
  4. 由于是静态结构,更适合 tree shaking ,有助于减少代码体积。
  • CJS (CommonJS)
  1. 主要用于 Node.js,它的语法是 requiremodule.exports
  2. 它是同步加载
  3. 它可以在代码的任何位置动态加载文件
  4. 由于动态结构,它没法进行 tree shaking
  5. 浏览器不支持 commonjs 语法,需要打包工具转换。

配置 ESM 和 CJS 格式打包

配置 vite.config.ts 中的 build.rollupOptions.output

js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      external: [
        // 外部依赖
        '@floating-ui/vue',
        'vue',
        '@v-c/utils',
        'lodash-es',
        '@tov-ui-study/utils',
        '@tov-ui-study/icons',
      ],
      output: [
        {
          preserveModules: true, // 是否使用原始模块名作为文件名
          preserveModulesRoot: 'src', // 从哪个文件夹获取文件
          entryFileNames: '[name].js', // 打包后的文件名
          format: 'esm', // 转成 esm 格式
          dir: 'es', // 打包后输出目录
        },
        {
          preserveModules: true,
          preserveModulesRoot: 'src',
          entryFileNames: '[name].js',
          exports: 'named', // 导出模式,只有导出 commonjs 格式需要配置
          format: 'cjs',
          dir: 'lib',
        },
      ],
    },
    lib: {
      entry: 'src/index.ts',
    },
  },
})

增加 ESM 和 CJS 打包后使用组件库时的类型提示

主要是通过 .d.ts 文件实现类型提示的,安装 vite-plugin-dts ,实现打包的时候自动生成 dts 文件

ts
// sky-ui/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    vue(),
    dts({
      entryRoot: 'src', // 输入文件夹
      outDir: ['es', 'lib'], // 输出文件夹
      exclude: ['**/test/**'], // 忽略单元测试的文件
    }),
  ],
})

打包 ESM 和 CJS 格式的 css 文件

写一个 node 命令,遍历组件库中的所有 less 文件,并且把它们打包成 css 文件,放到打包后对应的组件库文件夹中。

利用 fast-globfs-extra 插件

ts
// css-build.ts
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import fg from 'fast-glob'
import fs from 'fs-extra'
import less from 'less'

const skyDir = fileURLToPath(new URL('../packages/sky-ui', import.meta.url))

const lessFiles = fg.sync(['src/**/style/index.less', '!src/style'], {
  cwd: skyDir,
})

async function complie() {
  for (const file of lessFiles) {
    const filePath = path.resolve(skyDir, file)
    const lessCode = fs.readFileSync(filePath, 'utf-8')

    const cssCode = await less.render(lessCode, {
      paths: [path.dirname(filePath)],
    })

    // 配置输出文件路径
    const esDir = path.resolve(skyDir, `es${file.slice(3, file.length - 4)}css`)
    const libDir = path.resolve(
      skyDir,
      `lib${file.slice(3, file.length - 4)}css`
    )

    fs.outputFileSync(esDir, cssCode.css)
    fs.outputFileSync(libDir, cssCode.css)
  }
}
complie()

打包成 umd 格式

umd 格式就是通过 script 引入

ts
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

const baseUrl = fileURLToPath(new URL('.', import.meta.url))

export default defineConfig({
  resolve: {
    alias: [
      {
        find: /^@sky-ui\/utils/,
        replacement: path.resolve(baseUrl, '../utils/src'),
      },
    ],
  },
  plugins: [vue()],
  build: {
    rollupOptions: {
      external: ['vue'], // 这里配置 vue ,是因为组件库依赖 vue 的时候,从外部拿,组件库本身不把 vue 打包进去。
      output: {
        exports: 'named',
        globals: {
          vue: 'vue', // 外部第三方包的名字,在 window 上的
        },
      },
    },
    lib: {
      entry: 'src/index.ts',
      formats: ['umd'], // 打包成 umd 格式
      fileName: () => 'sky-ui.js', // 输出文件名
      name: 'skyUI', // 挂载到 window 上的名字, window.skyUI
    },
  },
})

打包 umd 格式的 css 文件

ts
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { defineConfig } from 'vite'
import fs from 'fs-extra'

export default defineConfig({
  build: {
    emptyOutDir: false, // 打包的时候是否清空打包输出的文件夹
    rollupOptions: {
      output: {
        assetFileNames: () => 'sky-ui.css', // css 输出文件名
      },
    },
    lib: {
      entry: 'src/style/index.ts',
      formats: ['es'],
      fileName: () => 'sky-ui-style.js', // 输出文件名
    },
  },
  plugins: [
    {
      // 删除打包后的 sky-ui-style.js ,这个文件没啥用,因为当前配置是用来生成 css 文件的
      name: 'remove:sky-ui-style.js',
      closeBundle() {
        const distPath = fileURLToPath(new URL('dist', import.meta.url))
        const file = path.resolve(distPath, 'sky-ui-style.js')
        fs.removeSync(file)
      },
    },
  ],
})

发布到 npm 上

  1. 切换成 npm 源
  2. 执行 npm login 命令,登录 npm 账号
  3. 在包的根目录下执行 npm publish 命令,发布到 npm 上
  4. 如果要更新版本,修改 package.json 中的 version 字段,再执行 npm publish 命令