组件库搭建

100 阅读3分钟

本地环境

  • node:v20.10.0
  • pnpm:v8.15.5

搭建 monorepo 环境

使⽤ pnpm 安装包速度快,磁盘空间利⽤率⾼效,使⽤ pnpm 可以快速建⽴ monorepo,so ~ 这⾥我们使⽤ pnpm workspace 来实现 monorepo

npm install pnpm -g # 全局安装pnpm
pnpm init # 初始化package.json配置⽂件 私有库
pnpm install vue typescript -D # 全局下添加依赖

提升 pnpm 下载的包

下载了 vue 和 ts 包,和这两个包相关的依赖包都在 .pnpm 中。要把依赖包提升。
.npmrc

shamefully-hoist = true

再执行 npm install,使用的依赖包就暴露在外部。

tsconfig.json

ts 配置文件初始化:pnpm tsc --init

{
    "compilerOptions": {
      "module": "ESNext", // 打包模块类型ESNext
      "declaration": false, // 默认不要声明文件 
      "noImplicitAny": true, // 支持类型不标注可以默认any
      "removeComments": true, // 删除注释
      "moduleResolution": "node", // 按照node模块来解析
      "esModuleInterop": true, // 支持es6,commonjs模块
      "jsx": "preserve", // jsx 不转
      "noLib": false, // 不处理类库
      "target": "es6", // 遵循es6版本
      "sourceMap": true,
      "lib": [ // 编译时用的库
        "ESNext",
        "DOM"
      ],
      "allowSyntheticDefaultImports": true, // 允许没有导出的模块中导入
      "experimentalDecorators": true, // 装饰器语法
      "forceConsistentCasingInFileNames": true, // 强制区分大小写
      "resolveJsonModule": true, // 解析json模块
      "strict": true, // 是否启动严格模式
      "skipLibCheck": true // 跳过类库检测
    },
    "exclude": [ // 排除掉哪些类库
      "node_modules",
      "**/__tests__", // 单元测试
      "dist/**"
    ]
  }

创建 pnpm 工作空间

pnpm-workspace.yaml

packages: 
    - 'packages/**' # 组件相关 
    - docs # ⽂档 
    - play # 运行、测试组件

组件库目录

QQ截图20240505153633.png

packages 目录下的文件都是一个单独的包,且每一个包都有自己的包名:

  • components
  • core
  • hooks
  • theme
  • utils

在 packages 中的各个包要互相调用,所以要在最外层项目将 packages 中的包下载成依赖, 在 根目录 package.json 中添加如下内容

  "dependencies": {
    "@yujun-element/components": "workspace:^",
    "@yujun-element/hooks": "workspace:^",
    "@yujun-element/theme": "workspace:^",
    "@yujun-element/utils": "workspace:^",
    "typescript": "^5.2.2",
    "vue": "^3.4.21",
    "yujun-element": "workspace:^"
  }

docs 文档

组件库文档利用 vitepress 创建

pnpm install vitepress -D # 在doc⽬录下安装

doc 目录下的 script 指令

"scripts": {
    "dev": "vitepress dev",
    "build": "vitepress build"
}

在根项⽬中增添启动命令

  "scripts": {
    "docs:dev": "pnpm -C docs dev",
    "docs:build": "pnpm -C docs build"
  }

文档发布在 github.io

.github/workflows/deploy.yml

name: Deploy VitePress site to Pages

on:
  push:
    branches:
      - main

jobs:
  test:
    name: Run Lint and Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3

      - name: Install pnpm 
        run: npm install -g pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run tests
        run: npm run test

  build:
    name: Build docs
    runs-on: ubuntu-latest
    needs: test

    steps:
      - name: Checkout repo
        uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3

      - name: Install pnpm
        run: npm install -g pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build docs
        run: npm run docs:build

      - name: Upload docs
        uses: actions/upload-artifact@v3
        with:
          name: docs
          path: ./docs/.vitepress/dist

  deploy:
    name: Deploy to GitHub Pages
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Download docs
        uses: actions/download-artifact@v3
        with:
          name: docs

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GH_TOKEN }}
          publish_dir: .

组件库文档发布到 github

  • 申请 github token

QQ截图20240512150042.png

  • 创建 github action

QQ截图20240512150335.png

  • 查看 workflows

QQ截图20240512150541.png

play 环境

components 中编写的组件在 play 中直接运行。

pnpm create vite play --template vue-ts
cd play
pnpm i

vite-env.d.ts 相当于给 .vue 文件的一个类型提示。我使用 vue 导出的方法时会给出提示。

/// <reference types="vite/client" />
// 为 vue 文件的类型说明
declare module '*.vue' {
    import type { DefineComponent } from 'vue'
    const component: DefineComponent<{}, {}, any>
    export default component
}

根目录启动 play 项目

  • play/package.json
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  }
  • 最外层 package.json
  "scripts": {
    "dev": "pnpm -C play dev"
  }

pnpm -Cpnpm 命令的一种用法,结合了 -C 选项。这个用法表示在执行 pnpm 命令时,将会先切换到指定的工作目录,然后在该目录下执行后续的 pnpm 命令。 例如,如果你运行 pnpm -C path/to/project install,那么 pnpm 将会先切换到 path/to/project 目录,然后在这个目录下执行 install 命令,即安装项目的依赖。

BEM函数

//  BEM 函数
const _bem = (prefixedName:string, blockSuffix:string,
    element:string, modifier:string) => {
    if (blockSuffix) {
        prefixedName += `-${blockSuffix}`
    }
    if (element) {
        prefixedName += `__${element}`
    }
    if (modifier) {
        prefixedName += `--${modifier}`
    }
    return prefixedName
}
function createBEM(prefixedName: string) {
    const b = (blockSuffix = '') => _bem(prefixedName, blockSuffix, '', '')
    const e = (element = '') =>
        element ? _bem(prefixedName, '', element,
            '') : ''
    const m = (modifier = '') =>
        modifier ? _bem(prefixedName, '', '',
            modifier) : ''
    const be = (blockSuffix = '', element = '') =>
        blockSuffix && element ? _bem(prefixedName,
            blockSuffix, element, '') : ''
    const em = (element:string, modifier:string) =>
        element && modifier ? _bem(prefixedName, '',
            element, modifier) : ''
    const bm = (blockSuffix:string, modifier:string) =>
        blockSuffix && modifier ? _bem(prefixedName,
            blockSuffix, '', modifier) : ''
    const bem = (blockSuffix:string, element:string, modifier:string) =>
        blockSuffix && element && modifier
            ? _bem(prefixedName, blockSuffix, element,
                modifier)
            : ''
    const is = (name:string, state:string) => (state ? `is-
${name}` : '')
    return {
        b,
        e,
        m,
        be,
        em,
        bm,
        bem,
        is
    }
}
export function createNamespace(name: string) {
    const prefixedName = `z-${name}`
    return createBEM(prefixedName)
}

BEM样式

  • mixins/config.scss
$namespace: 'z';
$element-separator: '__';
$modifier-separator:'--';
$state-prefix:'is-';
  • mixins/mixins.scss
@use 'config' as *;
@forward 'config';


// .z-button{}
@mixin b($block) {
    $B: $namespace+'-'+$block;
    .#{$B}{
        @content;
    }
}
// .z-button.is-desiabled
@mixin when($state) {
    @at-root {
        &.#{$state-prefix + $state} {
            @content;
        }
    }
}
// &--primary => .z-button--primary
@mixin m($modifier) {
    @at-root {
        #{&+$modifier-separator+$modifier} {
            @content;
        }
    }
}
// &__header  => .z-button__header
@mixin e($element) {
    @at-root {
        #{&+$element-separator+$element} {
            @content;
        }
    }
}

demo 组件创建

  • 组件编写 components/Button/Button.vue
<script setup lang="ts">
defineOptions({
    name:'Button'
})
</script>
<template>
    <button>this is a button</button>
</template>
  • 组件导出 components/Button/index.ts
import Button from './Button.vue'
import { withInstall } from '@yujun-element/utils' // 每一个组件都绑定一个注册方法 app.use(ElButton)
export const ElButton = withInstall(Button)
  • withInstall 文件
import type { App, Plugin } from "vue";

type SFCWithInstall<T> = T & Plugin;

export const withInstall = <T>(component: T) => {
    (component as SFCWithInstall<T>).install = (app: App) => {
        const name = (component as any)?.name || "UnnamedComponent";
        // 全局注册组件
        app.component(name, component as SFCWithInstall<T>);
    };
    return component as SFCWithInstall<T>;
};
  • 组件库核心包:packages/core/index.ts
import { makeInstaller } from '@yujun-element/utils'
import components from './components'
import '@yujun-element/theme/index.css'
const installer = makeInstaller(components)
export * from '@yujun-element/components'
export default installer
  • makeInstaller 方法:app.use()所有组件
import type { App, Plugin } from "vue";
import { each } from "lodash-es";

type SFCWithInstall<T> = T & Plugin;
export function makeInstaller(components: Plugin[]) {
    const install = (app: App) =>
        each(components, (c) => {
            // use 方法会调用每一个组件的 install 方法
            app.use(c);
        });
    return install;
}
  • 在 paly 中使用 packages/core 包
pnpm i yujun-element

play/src/main.ts

import YuJunElement from 'yujun-element'
const app = createApp(App)
app.use(YuJunElement)
app.mount('#app')

play/src/App.vue

    <Button />