基于 Vite + Vue3 + TS 开发组件库
技术栈
HTML5 + CSS3 + Less + ES6+ + Vue3.x + Composition-API + Vite + Gulp + Rollup + Jest
初始化项目
- 可以使用 vite 的官方 template,也可以自己搭建。
官方命令
sh
npm init vite@latest ui --template vue
从零搭建
生成 package.json
sh
npm init -y
json
{
"name": "ui",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
安装依赖
- dependencies
sh
# 简化版本
npm i -S clipboard mockjs vue vue-router
# or 指定版本
npm i -S clipboard@2.0.8 mockjs@1.1.0 vue@3.0.11 vue-router@4.0.6
clipboard:
可选
复制到剪切板mockjs:
可选
生成随机数据vue:渐进式框架
vue-router:路由管理器
devDependencies
sh
# 简化版本
npm i -D @rollup/plugin-node-resolve @types/jest @types/mockjs @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser @vitejs/plugin-vue @vue/compiler-sfc @vue/test-utils autoprefixer del eslint eslint-config-airbnb-base eslint-config-prettier eslint-plugin-import eslint-plugin-jest eslint-plugin-prettier eslint-plugin-vue gulp gulp-autoprefixer gulp-cssmin gulp-less gulp-postcss jest less markdown-it-container postcss-pxtorem prettier rollup rollup-plugin-terser rollup-plugin-typescript2 rollup-plugin-vue ts-jest typescript vite vite-plugin-vuedoc vue-jest vue-tsc
# or 指定版本
npm i -D @rollup/plugin-node-resolve@13.0.0 @types/jest@26.0.23 @types/mockjs@1.0.4 @types/node@15.0.2 @typescript-eslint/eslint-plugin@4.25.0 @typescript-eslint/parser@4.25.0 @vitejs/plugin-vue@1.2.2 @vue/compiler-sfc@3.0.5 @vue/test-utils@2.0.0-rc.6 autoprefixer@10.2.6 del@6.0.0 eslint@7.27.0 eslint-config-airbnb-base@14.2.1 eslint-config-prettier@8.3.0 eslint-plugin-import@2.23.4 eslint-plugin-jest@24.3.6 eslint-plugin-prettier@3.4.0 eslint-plugin-vue@7.10.0 gulp@4.0.2 gulp-autoprefixer@7.0.1 gulp-cssmin@0.2.0 gulp-less@4.0.1 gulp-postcss@9.0.0 jest@26.6.3 less@3.13.1 markdown-it-container@3.0.0 postcss-pxtorem@6.0.0 prettier@2.3.0 rollup@2.50.5 rollup-plugin-terser@7.0.2 rollup-plugin-typescript2@0.30.0 rollup-plugin-vue@6.0.0 ts-jest@26.5.6 typescript@4.1.3 vite@2.2.3 vite-plugin-vuedoc@3.1.3 vue-jest@5.0.0-alpha.10 vue-tsc@0.0.24
- @rollup/plugin-node-resolve:rollup 路径解析插件,告诉 Rollup 如何查找外部模块
- @types/jest:jest 的 TS 模块
- @types/mockjs:mockjs 的 TS 模块
- @types/node:关于 nodejs 的类型定义,用于 nodejs 中使用 TS
- @typescript-eslint/eslint-plugin:eslint 插件,包含了各类定义好的检测 TS 代码的规范
- @typescript-eslint/parser:eslint 的解析器,用于解析 TS,从而检查和规范 TS
- @vitejs/plugin-vue:vite 解析 Vue 的插件
- @vue/compiler-sfc:解析 SFC(Single File Components) 组件
- @vue/test-utils:Vue 单元测试
- autoprefixer:浏览器前缀工具
- del:用于删除文件夹和文件
- eslint:JS 代码检测工具
- eslint-config-airbnb-base:eslint 的 airbnb 编码规则
- eslint-config-prettier:处理 eslint 中的样式规范和 prettier 中样式规范的冲突
- eslint-plugin-import:验证正确的导入的 eslint 插件
- eslint-plugin-jest:解析 jest 的 eslint 插件
- eslint-plugin-prettier:将 prettier 作为 eslint 规范来使用
- eslint-plugin-vue:解析 Vue 的 eslint 插件
- gulp:自动化构建工具
- gulp-autoprefixer:自动获取浏览器厂商前缀,如 -webkit-
- gulp-cssmin:css 压缩
- gulp-less:解析 CSS 预编译器 LESS
- gulp-postcss:转换前缀工具,和 gulp-autoprefixer 搭配使用
- jest:单元测试
- less:CSS 预编译器
- markdown-it-container:Markdown 解析器
- postcss-pxtorem:
可选
转换 rem 单位 - prettier:格式化规范
- rollup:自动化打包工具
- rollup-plugin-terser:rollup 压缩
- rollup-plugin-typescript2:rollup 解析 TS
- rollup-plugin-vue:rollup 解析 Vue
- ts-jest:单元测试解析 TS
- typescript:JS 类型的超集,强类型
- vite:自动化构建工具
- vite-plugin-vuedoc:Vite 解析 Markdown
- vue-jest:单元测试解析 Vue
- vue-tsc:Vue 文件生成
.d.ts
类型文件
创建项目目录
创建演示项目目录
- 生成
tsconfig.json
sh
# 全局安装
npm i -g typescript
# 初始化
tsc --init
- 根目录创建 index.html
html
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UI组件库</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/examples/main.ts"></script>
</body>
</html>
- 创建 examples 目录,此目录为演示文档主目录,相当于 src
- 创建
examples/assets
:资源目录 - 创建
examples/components
:演示项目的公共组件 - 创建
examples/router
:路由 - 创建
examples/App.vue
:页面入口 - 创建
examples/main.ts
:主入口
- 创建
ts
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
const app = createApp(App)
app.use(router)
app.mount('#app')
- 配置 Vite
- 创建
vite.config.ts
- 创建
ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()]
})
- 配置 Eslint
- 创建
.eslintrc.js
- 创建
js
// 配置信息
const config = {
env: {
browser: true,
es2021: true,
node: true
},
extends: ['plugin:vue/essential', 'airbnb-base', 'plugin:prettier/recommended', 'plugin:jest/recommended'],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
plugins: ['vue', '@typescript-eslint'],
settings: {},
rules: {}
}
module.exports = config
- TS 识别 Vue 文件
- 创建
typings/shims-vue.d.ts
- 创建
ts
// vue
declare module '*.vue' {
import { App, defineComponent } from 'vue'
const component: ReturnType<typeof defineComponent> & {
install(app: App): void
}
export default component
}
创建单元测试主目录
- 配置 Jest
- 创建
jest.config.js
- 创建
js
// 配置
const config = {
moduleFileExtensions: ['vue', 'json', 'js', 'ts'],
preset: 'ts-jest',
testEnvironment: 'jsdom',
transform: {
'^.+\\.vue$': 'vue-jest', // vue 文件用 vue-jest 转换
'^.+\\.ts$': 'ts-jest' // ts 文件用 ts-jest 转换
},
testMatch: ['**/tests/unit/*.spec.ts'], // 匹配 tests/unit 目录下的 .ts 文件
verbose: true, // 显示冗余代码,true:显示测试用例,false:显示 console.log
bail: true // 经历几次失败后停止运行测试
}
module.exports = config
- 创建测试用例
tests/unit/img.spec.ts
ts
import { mount } from '@vue/test-utils'
import MeImg from '~/MeImg/index.vue' // 引入主要测试的组件
describe('MeImg', () => {
const src = 'http://dummyimage.com/100x100/0079cb/fff' // 图片地址
// 测试传参 src
test('props src', () => {
// 向组件里传参,获取组件实例
const wrapper = mount(MeImg, {
props: { src }
})
const viewer = wrapper.find('.me-img') // 获取 DOM
expect(viewer.exists()).toBeTruthy() // 是否存在
const imgEl = viewer.find('img')
expect(viewer.exists()).toBeTruthy()
expect(imgEl.attributes('src')).toBe(src) // 传入的 src 地址是否在组件里正确
})
})
创建组件库主目录
- 创建
packages/index.ts
ts
import type { App } from 'vue'
/* 基础组件 start */
import MeImg from './MeImg' // 图片
/* 基础组件 end */
// 所有组件
const components: any[] = [MeImg]
/**
* 组件注册
* @param {App} app Vue 对象
* @returns {Void}
*/
const install = (app: App) => {
// 注册组件
components.forEach(component => app.component(component.name, component))
}
export { MeImg }
// 全部导出
export default {
install,
...components
}
开发组件
- 创建组件文件
packages/MeImg/index.vue
vue
<template>
<!-- 图片 -->
<div class="me-img" @click="onClick">
<img
:src="src"
width="40px"
height="40px"
:alt="alt"
v-if="!fill"
:style="`width:${width};height:${height};border-radius:${radius};`"
@load="onLoad"
@error="onError"
/>
<span
:style="`width:${width};height:${height};border-radius:${radius};background:url(${src}) no-repeat center;background-size:${fill};`"
v-else
></span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'MeImg',
props: {
// 图片地址
src: {
type: String,
required: true
},
// 宽度
width: {
type: String,
default: ''
},
// 高度
height: {
type: String,
default: ''
},
// 填充方式
fill: {
type: String,
default: ''
},
// 倒角
radius: {
type: String,
default: '0'
},
// 错误显示alt
alt: {
type: String,
default: ''
}
},
setup(props, { emit }) {
// 点击按钮
const onClick = (e: MouseEvent) => {
emit('on-click', e)
}
// 加载完成
const onLoad = (e: Event) => {
emit('on-load', e)
}
// 加载失败
const onError = (e: Event) => {
emit('on-error', e)
}
return { onClick, onLoad, onError }
}
})
</script>
- 创建导出文件
packages/MeImg/index.ts
ts
import type { App } from 'vue'
import MeImg from './index.vue'
type SFCWithInstall<T> = T & { install(app: App): void } // vue 安装
// 安装
MeImg.install = (app: App) => {
app.component(MeImg.name, MeImg)
}
const InMeImg: SFCWithInstall<typeof MeImg> = MeImg // 增加类型
export default InMeImg
- 样式开发
theme-default/MeImg.less
less
/**
* @file 图片
*/
.me-img {
.inline-block;
// 相同的样式
.same-style {
display: block;
width: @img-size;
overflow: hidden;
}
img {
.same-style;
height: auto;
}
span {
.same-style;
height: 40px;
}
}
完成
开发完成
打包演示项目
package.json
设置命令
json
"scripts": {
"start": "npm run dev",
"dev": "vite -m development",
"build": "npm run build:theme && npm run build:package && npm run build:package:dts",
"build:docs": "vite build",
"build:theme": "gulp build -f build/gulpfile.prod.js",
"build:package": "rollup -c build/rollup.config.js",
"build:package:dts": "rollup -c build/rollup.config.dts.js",
"test:unit": "jest -c=jest.config.js --detectOpenHandles"
}
配置组件库打包
组件 JS 打包
- 创建 Rollup 配置文件
build/rollup.config.js
和build/rollup.config.dts.js
- rollup.config.js
js
import nodeResolve from '@rollup/plugin-node-resolve' // 告诉 Rollup 如何查找外部模块
import typescript from 'rollup-plugin-typescript2'
import vue from 'rollup-plugin-vue' // 处理vue文件
import { readdirSync } from 'fs' // 写文件
import { resolve } from 'path'
const input = resolve(__dirname, '../packages') // 入口文件
const output = resolve(__dirname, '../lib') // 输出文件
const config = readdirSync(input)
.filter(name => !['theme-default', 'index.ts', 'types.ts'].includes(name))
.map(name => ({
input: `${input}/${name}/index.ts`,
external: ['vue'],
plugins: [
nodeResolve(),
vue(),
typescript({
tsconfigOverride: {
compilerOptions: {
declaration: false
},
exclude: ['node_modules', 'examples', 'tests']
},
abortOnError: false,
clean: true
})
],
output: {
name: 'index',
file: `${output}/${name}/index.js`,
format: 'es'
}
}))
config.push({
input: `${input}/index.ts`,
external: ['vue'],
plugins: [
nodeResolve(),
vue(),
typescript({
tsconfigOverride: {
compilerOptions: {
declaration: false
},
exclude: ['node_modules', 'examples', 'tests']
},
abortOnError: false,
clean: true
})
],
output: {
name: 'index',
file: `${output}/index.js`,
format: 'es'
}
})
export default config
- rollup.config.dts.js
js
import nodeResolve from '@rollup/plugin-node-resolve' // 告诉 Rollup 如何查找外部模块
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript2'
import vue from 'rollup-plugin-vue' // 处理vue文件
import { resolve } from 'path'
const input = resolve(__dirname, '../packages') // 入口文件
const output = resolve(__dirname, '../lib') // 输出文件
const config = [
{
input: `${input}/index.ts`,
output: {
format: 'es',
file: `${output}/index.esm.js`
},
plugins: [
terser(),
nodeResolve(),
vue({
target: 'browser',
css: false,
exposeFilename: false
}),
typescript({
useTsconfigDeclarationDir: false,
tsconfigOverride: {
include: ['packages/**/*'],
exclude: ['node_modules', 'examples', 'tests']
},
abortOnError: false
})
],
external: ['vue']
}
]
export default config
组件样式打包
- 创建 Gulp 配置文件
build/gulpfile.base.js
- rollup.config.dts.js
js
const { src, dest, series, parallel } = require('gulp')
const less = require('gulp-less')
const autoprefixer = require('gulp-autoprefixer')
const cssmin = require('gulp-cssmin')
const del = require('del')
// 打包配置
const config = {
input: '../packages/theme-default/',
output: '../lib/theme-default'
}
// 导出配置项
exports.config = config
// 复制字体
exports.copyfont = () =>
src([`${config.input}fonts/*`, `!${config.input}fonts/*.css`]).pipe(dest(`${config.output}/fonts`))
// 压缩font 里的 CSS
exports.minifontCss = () =>
src(`${config.input}fonts/*.css`)
.pipe(cssmin())
.pipe(dest(`${config.output}/fonts`))
// 删除之前css打包文件
exports.clean = done => {
del(
['*.css', 'fonts'].map(name => `${config.output}/${name}`),
{ force: true }
)
done()
}
// 编译 LESS
const compile = () =>
src([`${input}*.less`, ...['base', 'variable'].map(name => `!${input}${name}.less`)])
.pipe(less())
.pipe(
autoprefixer({
overrideBrowserslist: ['last 2 versions']
})
)
.pipe(cssmin())
.pipe(dest(output))
exports.build = series(clean, parallel(compile, copyfont, minifontCss))
上传 npm 官网
- 登录
sh
npm login
- 发布
sh
npm publish