技术栈
HTML5 + CSS3 + Less + ES6+ + Vue3.x + Composition-API + Vite + Gulp + Rollup + Jest
初始化项目
- 可以使用 vite 的官方 template,也可以自己搭建。
官方命令
npm init vite@latest ui --template vue
- 1
从零搭建
生成 package.json
npm init -y
- 1
{
"name": "ui",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
安装依赖
- dependencies
# 简化版本
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
- 1
- 2
- 3
- 4
- clipboard:
可选
复制到剪切板 - mockjs:
可选
生成随机数据 - vue:渐进式框架
- vue-router:路由管理器
- devDependencies
# 简化版本
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
- 1
- 2
- 3
- 4
- @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
# 全局安装
npm i -g typescript
# 初始化
tsc --init
- 1
- 2
- 3
- 4
- 根目录创建 index.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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 创建 examples 目录,此目录为演示文档主目录,相当于 src
- 创建
examples/assets
:资源目录 - 创建
examples/components
:演示项目的公共组件 - 创建
examples/router
:路由 - 创建
examples/App.vue
:页面入口 - 创建
examples/main.ts
:主入口
- 创建
import { createApp } from "vue";
import router from "./router";
import App from "./App.vue";
const app = createApp(App);
app.use(router);
app.mount("#app");
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 配置 Vite
- 创建
vite.config.ts
- 创建
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()]
});
- 1
- 2
- 3
- 4
- 5
- 6
- 配置 Eslint
- 创建
.eslintrc.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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- TS 识别 Vue 文件
- 创建
typings/shims-vue.d.ts
- 创建
// vue
declare module "*.vue" {
import { App, defineComponent } from "vue";
const component: ReturnType<typeof defineComponent> & {
install(app: App): void;
};
export default component;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
创建单元测试主目录
- 配置 Jest
- 创建
jest.config.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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 创建测试用例
tests/unit/img.spec.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 地址是否在组件里正确
});
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
创建组件库主目录
- 创建
packages/index.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
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
开发组件
- 创建组件文件
packages/MeImg/index.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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 创建导出文件
packages/MeImg/index.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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 样式开发
theme-default/MeImg.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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
::: tip 完成
开发完成
:::
打包演示项目
package.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"
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
配置组件库打包
组件 JS 打包
- 创建 Rollup 配置文件
build/rollup.config.js
和build/rollup.config.dts.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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
组件样式打包
- 创建 Gulp 配置文件
build/gulpfile.base.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));
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
上传 npm 官网
- 登录
npm login
- 1
- 发布
npm publish
- 1