Skip to content

组件实战

那么你会学到什么?

  1. 如何实现一个 Tree 组件
  2. 递归组件的用法
  3. 组件库封装
  4. 前端打包格式 UMD ESM CJS
  5. 组件库发布 npm

目录创建

  • dist 打包文件
  • docs 文档
  • packages 组件
    • Tree
      • index.ts 入口
      • tree.tsx 组件
      • styles.css 样式
      • type.ts 类型
    • Button
      • index.ts 入口
      • button.tsx 组件
      • styles.css 样式
      • type.ts 类型
    • index.ts 组件汇总
    • vite.d.ts 类型
  • example
    • index.html 示例
    • App.tsx 示例
    • main.tsx 示例
  • package.json 包管理
  • vite.config.ts vite 配置
  • tsconfig.json ts 配置
  • README.md README

TIP

package.json 可以通过 npm init -y 生成

tsconfig.json 可以通过 tsc --init 生成

所需要的依赖

bash
npm install vite -D # vite 构建工具
npm install @vitejs/plugin-react-swc -D # 插件编译React
npm install vite-plugin-dts -D #生成d.ts文件 声明文件
npm install react #react依赖
npm install react-dom #react依赖
npm install @types/react -D # 类型
npm install @types/react-dom -D # 类型
npm install @types/node -D # 类型

初始化文件

  • example/App.tsx
tsx
export default function App() {
  return <div>Hello World</div>;
}
  • example/main.tsx
tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <App />
);
  • example/index.html
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>example</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./main.tsx"></script>
  </body>
</html>

因为vite默认是从根目录找index.html文件,但是我们的项目结构是example/index.html,所以需要配置vite.config.ts

  • vite.config.ts
ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'node:path';
export default defineConfig({
  plugins: [react()],
  root: path.resolve(__dirname, 'example'),
  server: {
    port: 3000,
    open: true,
  },
});
  • tsconfig.json
json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "NodeNext",
    "jsx": "preserve",
    "jsxImportSource": "react",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["packages/**/*", "vite.config.ts"]
}
  • package.json
json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}

核心逻辑编写

  1. 首先需要确定声明文件,TreeNode定义每一个叶子结点的类型,TreeProps定义组件的属性
  • packages/Tree/type.ts
ts
export interface TreeNode {
  id: string | number; //id用于绑定key
  name: string; //name用于显示
  children?: TreeNode[]; //children用于存储子节点
  selected: boolean; //selected用于存储节点是否选中
}

export interface TreeProps {
  data: TreeNode[]; //数据源
  onChecked: (node: TreeNode) => void; //选中回调
}
  1. 组件的实现,其核心思想就是递归,如果出现了 children,就继续递归调用自身。
  • packages/Tree/tree.tsx
tsx
import React, { useState } from 'react';
import './styles.css';
import type { TreeProps, TreeNode } from './type';
const Tree: React.FC<TreeProps> = ({ data, onChecked }) => {
  const [treeData, setTreeData] = useState(data);
  const changeSelected = (
    e: React.ChangeEvent<HTMLInputElement>,
    item: TreeNode
  ) => {
    setTreeData(
      treeData.map(node =>
        node.id === item.id ? { ...node, selected: e.target.checked } : node
      )
    ); // 更新选中状态
    onChecked && onChecked({ ...item, selected: e.target.checked }); // 触发回调
  };
  return (
    <div>
      {treeData.map(item => {
        return (
          <div key={item.id}>
            <input
              onChange={e => changeSelected(e, item)}
              type="checkbox"
              checked={item.selected}
            />
            <span>{item.name}</span>
            <div className="tree-node">
              {item.children && (
                <Tree data={item.children} onChecked={onChecked}></Tree>
              )}
            </div>
          </div>
        );
      })}
    </div>
  );
};

export default Tree;
  1. 样式文件的编写(主要是为了有点边距)
  • packages/Tree/styles.css
css
.tree-node {
  margin-left: 10px;
  padding: 10px;
}
  1. 组件的入口文件(为了方便使用,需要导出组件和类型)
  • packages/Tree/index.ts
ts
import Tree from './tree';
export * from './type';
export { Tree };
  1. 组件的汇总文件(为了方便使用,需要导出组件和类型)
  • packages/index.ts
ts
import { Tree } from './Tree';
import { Button } from './Button'; //例子
import { Input } from './Input'; //例子
export * from './Tree';
export * from './Button'; //例子
export * from './Input'; //例子
export { Tree, Button, Input };
  1. App.tsx 使用
tsx
import React from 'react';
import { Tree, type TreeNode } from '../packages';
export default function App() {
  const data: TreeNode[] = [
    {
      id: '1',
      name: 'parent-1',
      selected: false,
      children: [
        {
          id: '1-1',
          name: 'child-1-1',
          selected: false,
          children: [{ id: '1-1-1', name: 'child-1-1-1', selected: false }],
        },
      ],
    },
    {
      id: '2',
      name: 'parent-2',
      selected: true,
      children: [
        {
          id: '2-1',
          name: 'child-2-1',
          selected: true,
          children: [{ id: '2-1-1', name: 'child-2-1-1', selected: true }],
        },
      ],
    },
  ];
  return (
    <div>
      <Tree
        data={data}
        onChecked={(data: TreeNode) => {
          console.log(data);
        }}
      ></Tree>
    </div>
  );
}

打包发布

vite 里面集成了 rollup,所以打包发布很简单。dts 插件用于生成类型文件(声明文件)。 配置说明:

  • outDir:打包后的文件夹
  • lib:打包配置
  • formats:打包格式(es/umd/cjs)
  • fileName:打包后的文件名
  • root:根据环境判断,开发环境是 example,打包时是根目录
  • rollupOptions:rollup 配置
    • external:忽略的依赖(用户项目已安装)
    • globals:全局变量配置

es 就是 esModule,umd 就是全局变量包含了 amd 和 cjs,cjs 就是 commonjs,iife 就是立即执行函数,

  • vite.config.ts
ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'node:path';
import dts from 'vite-plugin-dts';
export default defineConfig(({ command }) => {
  const isDev = command === 'serve';
  return {
    plugins: [
      react(),
      dts({
        outDir: path.resolve(__dirname, 'dist'),
        insertTypesEntry: true, //这是dts插件的配置,用于生成类型文件
        include: ['packages/**/*.ts', 'packages/**/*.tsx'],
        rollupTypes: true,
      }),
    ],
    root: isDev ? path.resolve(__dirname, 'example') : undefined,
    server: {
      port: 3000,
      open: true,
    },
    build: {
      outDir: path.resolve(__dirname, 'dist'),
      lib: {
        entry: path.resolve(__dirname, 'packages/index.ts'),
        name: 'ui',
        formats: ['es', 'umd', 'cjs', 'iife'],
        fileName: format => `ui.${format}.js`,
      },
      emptyOutDir: false,
      rollupOptions: {
        external: ['react', 'react-dom'],
        output: {
          globals: {
            react: 'React',
            'react-dom': 'ReactDOM',
          },
        },
      },
    },
  };
});

配置 package.json

  • name:包名(npm 上的名字)
  • version:版本
  • files:要上传到 npm 的文件(这里边 dist 上传到 npm)
  • main:cjs 入口
  • module:esm 入口
  • types:类型文件

发布 Npm

  1. 没有账号可以注册一个 npm adduser npm 官网
  2. 登录 npm login 输入账号密码
  3. 发布 npm publish 发布成功后,就可以在 npm 官网搜索到你的包了

安装测试

bash
npm install xm-ui-react-1

随便一个文件

tsx
import React from 'react';
import { Tree } from 'xm-ui-react-1';
import 'xm-ui-react-1/dist/xm-ui-react.css';
const App: React.FC = () => {
  const data = [
    {
      id: '1',
      name: 'parent-1',
      selected: true,
      children: [
        {
          id: '2',
          name: 'child-2',
          selected: true,
          children: [
            {
              id: '3',
              name: 'child-3',
              selected: true,
            },
          ],
        },
      ],
    },
    {
      id: '4',
      name: 'parent-4',
      selected: true,
      children: [
        {
          id: '5',
          name: 'child-5',
          selected: true,
          children: [
            {
              id: '6',
              name: 'child-6',
              selected: true,
            },
          ],
        },
      ],
    },
  ];
  return (
    <>
      <Tree onChecked={console.log} data={data}></Tree>
    </>
  );
};

export default App;

Keep Reading, Keep Writing, Keep Coding