Appearance
组件库
有哪些组件的开发模式
- 利用 options API + TS
- 利用 composition API + TS,比如 element-ui
- 利用 TSX 语法, 比如 ant-design-vue、naive-ui
- 利用 TSX + render 函数,比较老的用法
monorepo 开发模式介绍
Monorepo(单一仓库)是一种源代码管理策略,它将多个项目或组件的代码存储在同一个版本控制系统仓库中。这种模式在大型项目或由多个团队协作开发的项目中特别有用。
优点:
- 代码共享,在不同项目或组件中,能更容易互相引用代码。
- 简化依赖管理,不同的项目或组件,依赖都在统一的地方,能减少版本冲突和重复依赖。
- 便捷的团队协作,所有成员都能看到所有代码及项目变更。
缺点: 项目会变得庞大,管理起来比较困难
组件库为什么要有一个完整的样式体系
保证不同开发者开发出来的组件,样式风格能够保持统一。
为什么要做单元测试
保证质量:单元测试可以确保每个组件按预期工作。它可以捕捉在开发过程中可能遗漏的错误,提高组件库的整体质量。
易于维护:当你对组件进行修改或扩展时,单元测试可以帮助你确保当前的更改没有破坏现有功能。这使得维护和升级组件库更加容易和安全。
文档作用:单元测试不仅是测试代码,同时也是关于如何使用组件的一个很好的文档。它们展示了组件的预期用法和行为。
快速反馈:在开发过程中,单元测试提供了快速的反馈机制。你可以立即知道你的更改是否有效,而不是在开发过程后期或生产环境中才发现问题。
简化调试:当出现问题时,良好的单元测试可以帮助你快速定位到出错的部分,简化调试过程。
促进设计:编写可测试的代码通常会有更好的设计决策和更清晰的代码结构。单元测试可以鼓励我们编写松耦合且高内聚的代码。
团队协作:在团队协作的环境中,单元测试可以提高信心,确保新加入的代码不会破坏现有功能,促进团队内部的协作和交流。
持续集成/持续部署(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)
- es module 是 js 官方的模块化系统,它的语法是
import
和export
。 - 支持异步加载,更适合浏览器环境,并且浏览器就直接支持 es module 语法。
- 在编译时进行解析和加载。这意味着
import
引入文件只能在模块最顶层,并且不能动态改变。 - 由于是静态结构,更适合 tree shaking ,有助于减少代码体积。
- CJS (CommonJS)
- 主要用于 Node.js,它的语法是
require
和module.exports
。 - 它是同步加载
- 它可以在代码的任何位置动态加载文件
- 由于动态结构,它没法进行 tree shaking
- 浏览器不支持 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-glob
和 fs-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 上
- 切换成 npm 源
- 执行
npm login
命令,登录 npm 账号 - 在包的根目录下执行
npm publish
命令,发布到 npm 上 - 如果要更新版本,修改
package.json
中的version
字段,再执行npm publish
命令