从0到1搭建Vue和React基础UI库
vue和react组件经常使用,是时候学会从0到1搭建组件库了。
搭建React基础UI库
typescript+react+vite+jest+storybook+eslint+husky+commitlint
1.typescript和babel配置
- 安装typescript
pnpm add -D typescript tslib
- typescript初始化,生成tsconfig.json
pnpm exec tsc --init
- 修改tsconfig.json
{
"compilerOptions": {
//react jsx
"jsx": "react-jsx",
},
"include": ["src"],
//测试文件test,storybook的配置文件stories
"exclude": ["src/**/*.stories.ts", "src/**/*.test.tsx", "src/examples/"]
}
- 安装babel
pnpm add -D babel @babel/preset-env @babel/preset-typescript @babel/preset-react
- 配置babel
{
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true,
"targets": {
"node": "current"
}
}
],
["@babel/preset-react", { "runtime": "automatic" }],
"@babel/preset-typescript"
]
}
2.React组件
- 添加react依赖
pnpm add -D react @types/react react-dom @types/react-dom
- 添加组件
src\components\Button\Button.tsx
import { FC, ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
styleType?: 'primary' | 'info';
color?: string;
}
const Button: FC<ButtonProps> = ({ styleType, ...props }) => {
return (
<button className={`${styles['xld-button']} ${styleType ? styles[styleType] : ''}`} {...props}>
<i style={{ color: props.color }}>*</i>
{props.children}
<i style={{ color: props.color }}>*</i>
</button>
);
};
export default Button;
- 添加
index.ts
用于导出组件
//src\components\Button\index.ts
import Button from './Button';
export default Button;
//src\components\index.ts
export { default as Button } from './Button';
// src\index.ts
export * from './components';
- 搭建vite环境,用于组件示例展示
pnpm add -D vite @vitejs/plugin-react
- 配置vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default ({ mode }: { mode: string }) => {
//获取不同环境的变量
// const env=loadEnv(mode, process.cwd())
return defineConfig({
//开发环境和生产环境前缀路径配置
base: mode === 'development' ? '/' : '/xld-react-ui',
plugins: [react()]
});
};
- 示例组件
//src\examples\main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.js';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
//src\examples\App.tsx
import type { FC } from 'react';
import * as Comps from '../index';
import React from 'react';
const App: FC = () => {
const list: any = [];
//遍历展示组件
for (const k in Comps) {
const El = Comps[k as keyof typeof Comps];
list.push(
<div key={k}>
{k}:<El>{k}</El>
</div>
);
}
return <>{list}</>;
};
export default App;
3.修改package.json
设置package.json的相关文件
{
"type": "module",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
//要先安装的依赖
"peerDependencies": {
"react": "^18.3.1"
}
}
4.rollup配置
-
安装rolup相关依赖
@rollup/plugin-commonjs
cjs转换为ems@rollup/plugin-node-resolve
路径@rollup/plugin-typescript
ts转换rollup-plugin-postcss
css样式注入rollup-plugin-dts
导出d.ts
pnpm add -D rollup @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript rollup-plugin-postcss rollup-plugin-dts
- 设置rollup
import packageJson from './package.json' assert { type: 'json' };
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
import postcss from 'rollup-plugin-postcss';
import dts from 'rollup-plugin-dts';
import babel from '@rollup/plugin-babel';
const plugins = [
resolve(),
commonjs(),
babel({
exclude: 'node_modules/**'
}),
typescript({ tsconfig: './tsconfig.json' })
];
export default [
{
//外部引入依赖
external: ['react', 'react-dom'],
//入口文件
input: 'src/index.ts',
//导出文件
output: [
{ file: packageJson.main, format: 'cjs', sourcemap: true },
{ file: packageJson.module, format: 'esm', sourcemap: true }
],
plugins: [
...plugins,
//css文件处理
postcss({
extensions: ['.css'],
minimize: true,
sourceMap: true,
modules: true,
inject: { insertAt: 'top' }
})
]
},
{
input: 'src/index.ts',
//导出ds.ts文件
ouput: {
file: packageJson.types,
format: 'esm'
},
plugins: [...plugins, dts()]
}
];
执行build构建组件库
{
"build": "rollup -c",
}
打包可以得到以下文件,index.js是全部的组件库,组件对应生成d.ts
5.添加jest组件测试
- 安装jest库
jest-environment-jsdom``@testing-library/jest-dom
jsdom作为测试模拟环境babel-jest
babelts-jest
typescriptjest-css-modules
css样式@testing-library/react
react测试库
pnpm add -D jest @types/jest babel-jest jest-css-modules @testing-library/react @testing-library/jest-dom jest-environment-jsdom
- 配置package.js的jest环境
"jest": {
"testEnvironment": "jsdom",
"moduleNameMapper": {
"\\.css$": "jest-css-modules"
}
}
- 编写test.ts测试用例
//src\tests\Button.test.tsx
import '@testing-library/jest-dom';
import { screen, fireEvent, render, cleanup } from '@testing-library/react';
import Button from '../components/Button';
import styles from '../components/Button/Button.module.css';
import React from 'react';
describe('Button', () => {
//测试前清空
afterEach(() => cleanup());
//渲染
it('renders', () => {
render(<Button data-testid="btn">Hello</Button>);
const button = screen.getByTestId('btn');
expect(button).toBeInTheDocument();
});
//动作测试
it('click', () => {
const fn = jest.fn();
render(
<Button data-testid="btn1" onClick={fn}>
Hello
</Button>
);
const button = screen.getByTestId('btn1');
fireEvent.click(button);
expect(fn).toHaveBeenCalled();
});
//样式测试
it('style', () => {
render(
<Button data-testid="btn2" styleType="primary">
Hello
</Button>
);
const button = screen.getByTestId('btn2');
console.log(styles.primary);
expect(button).toHaveClass(styles.primary || 'primary');
});
});
执行测试脚本
{
"test": "jest --verbose",
//生成测试覆盖率报告
"test-c": "jest --coverage",
}
- 覆盖率测试报告
coverage\lcov-report\index.html
6.添加storybook
- 安装storybook
pnpm add -D storybook
- 初始化storybook
pnpm exec sb init
- 自动生成
src\stories
示例,可以作为参考 .storybook\main.ts
storybook的vite服务配置.storybook\preview.ts
预览页面配置
- 添加自己的stories配置
//src\components\Button\Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button';
//Doc默认配置
const meta = {
title: 'Normal/Button',
component: Button,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
//输入类型配置
argTypes: {
color: { control: 'color' }
}
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
//覆盖属性值
export const Primary: Story = {
args: {
styleType: 'primary',
children: 'Button'
}
};
//点击
export const OnClickBtn: Story = {
args: {
onClick: () => alert('you click the Button'),
children: 'Button'
}
};
- 执行storybook脚本
{
//启动stoybook
"storybook": "storybook dev -p 6006",
//构建静态界面
"build-storybook": "storybook build"
}
可以修改配置属性值,来查看效果
7.代码规范
- commitlint检查git提交信息是否符合规范
pnpm add -D @commitlint/cli @commitlint/config-conventional
- 配置commitlint.config.js
export default { extends: ['@commitlint/config-conventional'] };
- eslint检查代码规范
pnpm add -D eslint
- 初始化eslint
pnpm exec eslint --init
-
生成 eslint.config.js,用于命令行检查
globals
全局变量,如window,documenteslint-plugin-react
react配置@eslint/compat
es版本兼容性@eslint/js
js检测typescript-eslint
ts检测
import { fixupConfigRules } from '@eslint/compat';
import globals from 'globals';
import pluginJs from '@eslint/js';
import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js';
import tseslint from 'typescript-eslint';
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...fixupConfigRules(pluginReactConfig),
{
files: ['src/**/*.{js,mjs,cjs,ts,jsx,tsx}'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-explicit-any': 'off'
},
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } }, globals: globals.browser }
}
];
- .eslintrc用于vscode的配置
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
},
"extends": [
"prettier",
"eslint:recommend",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
],
"plugins": ["react", "react-hooks", "@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"root": true
}
- package.json添加命令,检测src文件夹内的源码
{
"lint": "eslint src/",
}
- husky,在git hooks不同生命周期,执行代码检查,测试之类的
pnpm add -D husky
- 初始化husky
pnpm exec husky init
- .husky文件夹下新建commit-msg文件,添加commitlint命令
pnpm exec commitlint --config commitlint.config.js --edit "${1}"
注意:"${1}"
一定要有"
包裹,否则执行错误,读取不到对应的提交信息
- 将.husky文件下pre-commit文件命令行改成
pnpm lint
- 在执行
git commit -m "feat:message"
的时候,自动执行eslint和commitlint两个命令
8.Github地址
https://github.com/xiaolidan00/xld-react-ui
搭建Vue基础UI库
大致配置与react相似,但又一些针对vue的不同配置
typescript+vue+vite+vitest+storybook+eslint+husky+commitlint
1.typescript和babel配置
- 安装typescript
pnpm add -D typescript tslib @vue/tsconfig vue-tsc
- tsconfig.json配置
{
"compilerOptions": {
"jsx": "preserve",
"lib": ["ESNext", "DOM"]
},
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["src/**/*.stories.ts", "src/examples/","src/test/*.test.ts"],
}
- 安装babel
pnpm add -D @babel/core @babel/preset-env @vue/babel-plugin-jsx
- .babelrc配置
{
"presets": ["@babel/preset-env", "@vue/babel-plugin-jsx"]
}
2.Vue组件
- 添加vue依赖
pnpm add -D vue
- 添加vue组件
src\components\Button\Button.vue
<template>
<button :class="[type, size]" :style="{ color: color || '' }">{{ text }}</button>
</template>
<script lang="ts" setup>
defineOptions({
name: 'MyButton'
});
export interface Props {
text?: string;
type?: 'primary' | 'info';
size?: 'small' | 'medium' | 'large';
color?: string;
}
const props = withDefaults(defineProps<Props>(), {
text: 'hello',
type: 'primary',
size: 'medium'
});
</script>
<style lang="scss" scoped>
button {
border-radius: 4px;
&.medium {
height: 32px;
}
&.small {
height: 24px;
}
&.large {
height: 40px;
}
&.primary {
background-color: dodgerblue;
color: white;
border: solid dodgerblue;
}
&.info {
border: solid 1px green;
color: green;
background-color: white;
}
}
</style>
- 导出vue组件
//src\components\Button\index.ts
import Button from './Button.vue';
Button.install = function (Vue: any) {
Vue.component(Button.name, Button);
};
export default Button;
//src\index.ts导出全部组件
import * as comps from './components';
export * from './components';
export const allComps = comps;
export default {
install: (Vue: any) => {
for (const k in comps) {
const c = comps[k as keyof typeof comps];
Vue.component(c.name, c);
}
}
};
- 搭建vite的vue环境
pnpm add -D vite @vitejs/plugin-vue
- 添加vue展示页面
//vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()]
});
- vue展示组件
//src\examples\main.ts
import { createApp } from 'vue';
import App from './App.vue';
import Comps from '../index';
const app = createApp(App);
app.use(Comps);
app.mount('#app');
src\examples\App.vue
<template>
<div>
<component :is="item.name" :key="item.name" v-for="item in allComps"></component>
</div>
</template>
<script setup lang="ts">
import { allComps } from '../index';
</script>
<style scoped></style>
3.修改package.json
packages.json相关文件路径配置与react一致
4.rollup配置
其他rollup插件安装与react一致,还需要添加vue的插件,另外特别注意:将@rollup/plugin-typescript
换成rollup-plugin-typescript2
,否则vue模板里面的ts会解析失败
pnpm add -D rollup-plugin-vue rollup-plugin-typescript2
如果有用到sass
pnpm add -D sass rollup-plugin-scss
- 打包vue组件
import packageJson from './package.json' assert { type: 'json' };
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import postcss from 'rollup-plugin-postcss';
import dts from 'rollup-plugin-dts';
import vue from 'rollup-plugin-vue';
import babel from '@rollup/plugin-babel';
import scss from 'rollup-plugin-scss';
const plugins = [
typescript({ tsconfig: './tsconfig.json' }),
//babel转译
babel({
exclude: 'node_modules/**'
}),
commonjs(),
resolve(),
//处理vue组件
vue({
css: true,
compileTemplate: true
})
];
export default [
//打包vue组件
{//外部引入vue
external: ['vue'],
input: 'src/index.ts',
output: [
{ file: packageJson.main, format: 'cjs', sourcemap: true },
{ file: packageJson.module, format: 'esm', sourcemap: true }
],
plugins: [
...plugins,
//样式文件处理
scss(),
postcss({
extensions: ['.css', '.scss'],
minimize: true,
sourceMap: true,
modules: true,
extract: false,
inject: { insertAt: 'top' }
})
]
},
//打包d.ts文件
{
input: 'src/index.ts',
ouput: {
file: packageJson.types,
format: 'esm'
},
plugins: [...plugins, dts()],
//注意vue里面有样式style会作为其中模块,一定要添加external去除
external: [/\.(css|scss)$/]
}
];
打包可以得到以下文件,index.js是全部的组件库,组件对应生成d.ts
5.添加vitest组件测试
- 安装测试库
pnpm add -D @vue/test-utils@next happy-dom vitest
- 添加覆盖率测试报告
pnpm add -D @vitest/ui @vitest/coverage-v8
- 配置vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config';
import viteConfig from './vite.config';
export default mergeConfig(
//融合vite的配置
viteConfig,
defineConfig({
test: {
include: ['src/tests/*.test.ts'],
//模拟测试环境
environment: 'happy-dom',
//测试报告类型
reporters: ['html']
}
})
);
- 写vue测试用例
src\tests\Button.test.ts
/**
* @vitest-environment happy-dom
*/
import { mount } from '@vue/test-utils';
import { describe, expect, test } from 'vitest';
import Button from '../components/Button/Button.vue';
describe('Button.vue text', () => {
const text = 'HAHAHAHA';
test(`text ${text}`, () => {
const wrapper = mount(Button, {
props: { text: text }
});
expect(wrapper.text()).toEqual(text);
});
});
describe('Button.vue type', () => {
const types = ['primary', 'info'];
types.forEach((item) => {
test(`type ${item}`, () => {
const wrapper = mount(Button, {
props: { type: item }
});
expect(wrapper.classes()).toEqual(expect.arrayContaining([item]));
});
});
});
- 执行测试命令
{
"test": "vitest",
"test-c": "vitest run --coverage"
}
html\index.html
覆盖率测试报告地址
6.添加storybook
初始化操作与react一致,示例编写有些不同
- 添加组件stories示例
src\components\Button\Button.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3';
import Button from './Button.vue';
const meta = {
title: 'Example/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
type: { control: 'select', options: ['primary', 'info'] },
size: { control: 'select', options: ['small', 'medium', 'large'] },
color: { control: 'color' }
},
args: {
onClick: ()=>(alert("hello"))
}
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
type: 'primary',
text: 'Button'
}
};
export const Info: Story = {
args: {
type: 'info',
text: 'Button'
}
};
7.代码规范
- eslint初始化与react操作一致
eslint-plugin-vue
vue配置vue-eslint-parser
解析vue文件
pnpm add -D vue-eslint-parser
- .eslintrc用于vscode
{
"root": true,
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended"
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser"
}
}
- eslint.config.js用于命令行检查规范
import { fixupConfigRules } from '@eslint/compat';
import globals from 'globals';
import pluginJs from '@eslint/js';
import pluginVue from 'eslint-plugin-vue';
import tseslint from 'typescript-eslint';
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...fixupConfigRules(pluginVue.configs['flat/essential']),
{
files: ['**/*.{js,mjs,cjs,ts,vue}'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
"@typescript-eslint/no-unused-vars":'off'
},
languageOptions: {
parserOptions: {
parser: '@typescript-eslint/parser'
},
globals:globals.browser
}
}
];
其他代码规范与react一致
8.Github地址
https://github.com/xiaolidan00/xld-vue-ui
参考:
转载自:https://juejin.cn/post/7388399895508762676