likes
comments
collection
share

从0到1搭建Vue和React基础UI库

作者站长头像
站长
· 阅读数 16

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-postcsscss样式注入
    • 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 从0到1搭建Vue和React基础UI库

5.添加jest组件测试

  • 安装jest库
    • jest-environment-jsdom``@testing-library/jest-domjsdom作为测试模拟环境
    • babel-jestbabel
    • ts-jesttypescript
    • jest-css-modulescss样式
    • @testing-library/reactreact测试库
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",
}

从0到1搭建Vue和React基础UI库

从0到1搭建Vue和React基础UI库

  • 覆盖率测试报告coverage\lcov-report\index.html

从0到1搭建Vue和React基础UI库

6.添加storybook

  • 安装storybook
pnpm add -D storybook
  • 初始化storybook
pnpm exec sb init
  • 自动生成src\stories示例,可以作为参考
  • .storybook\main.tsstorybook的vite服务配置
  • .storybook\preview.ts预览页面配置

从0到1搭建Vue和React基础UI库

  • 添加自己的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"
}

从0到1搭建Vue和React基础UI库

可以修改配置属性值,来查看效果

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,document
    • eslint-plugin-react react配置
    • @eslint/compat es版本兼容性
    • @eslint/jsjs检测
    • 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

从0到1搭建Vue和React基础UI库

  • .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

从0到1搭建Vue和React基础UI库

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"
}

从0到1搭建Vue和React基础UI库

html\index.html覆盖率测试报告地址

从0到1搭建Vue和React基础UI库

从0到1搭建Vue和React基础UI库

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'
  }
};

从0到1搭建Vue和React基础UI库

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
评论
请登录