【手把手带你搓组件库】从零开始实现Element Plus

前言

在本文中,将手把手带你从零开始实现一个类似于Element Plus 的组件库。Element Plus 是一个非常流行的Vue UI 组件库,我们将尝试实现一些常见的组件,如基础组件、反馈组件、表单组件等。让我们开始吧!

亮点

  • Vite+Vitest+Vitepress 工具链 (项目构建+测试+项目文档)
  • monorepo 分包管理
  • GitHub actions 实现 CI/CD 自动化部署
  • 大模型辅助:使用大模型辅助完成需求分析,设计思路,快速实现组件,提升开发效率
  • 发布开箱即用的npm包

项目搭建

1、创建项目

初始化

mkdir Wannaer-element
cd Wannaer-element
git init
pnpm init

在这里插入图片描述

monorepo

monorepo ,那就先创建一个 pnpm-workspace.yaml 文件。

mkdir packages
echo -e 'packages:\n  - "packages/*"' > pnpm-workspace.yaml
// 在Windows系统中,echo命令默认不支持像在Linux系统中那样使用"-e"参数来表示换行符
// 创建完成后,手动操作换行
echo 'packages:\n  - "packages/*"' > pnpm-workspace.yaml

// 如果出现 pnpm: null byte is not allowed in input (1:4) 可能是有隐藏字符问题
packages:
  - "packages/*"

在这里插入图片描述

创建 .gitignore

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
coverage
dist
dist-ssr
*.local

/cyperss/videos/
/cypress/srceenshots/

.vitepress/dist
.vitepress/cache

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

目录结构

为了目录扁平,就只创建 packages 这么一个 pnpm 工作区,下面大概说一下这个项目计划的分包结构

- components # 组件目录
- core # npm 包入口
- docs # 文档目录
- hooks # 组合式API hooks 目录
- play # 组件开发实验室
- theme # 主题目录
- utils # 工具函数目录
// 创建这些目录
cd packages
// 分别初始化这些目录 在 packages 目录下创建 init.shell 内容如下
for i in components core docs hooks theme utils; do
  mkdir $i
  cd $i
  pnpm init
  cd ..
done
// 执行后删除 init.shell

在这里插入图片描述
这波 play 目录先留着,我们用 vite 来创建一个 vue 开发项目

pnpm create vite play --template vue-ts

创建完成后分别到 各个分包目录中修改 package.json 中的 name,防止重名

- core # npm 包入口
	"name": "Wannaer-element",
- components # 组件目录
	"name": "@Wannaer-element/components",
- docs # 文档目录
	"name": "@Wannaer-element/docs",
- hooks # 组合式API hooks 目录
	"name": "@Wannaer-element/hooks",
- play # 组件开发实验室
	"name": "@Wannaer-element/play",
- theme # 主题目录
	"name": "@Wannaer-element/theme",
- utils # 工具函数目录
	"name": "@Wannaer-element/utils",
- 根目录
	“name”: "@Wannaer-element/workspace"

安装基础依赖

在根目录 安装
-Dw表示在package.json文件中配置的scripts中运行特定的脚本命令,xxx为脚本命令的名称。
-w表示在指定的工作区目录中运行特定的脚本命令,xxx为脚本命令的名称。

// 开发依赖
pnpm add -Dw typescript@^5.2.2 vite@^5.1.4 vitest@^1.4.0 vue-tsc@^1.8.27 postcss-color-mix@^1.1.0 postcss-each@^1.1.0 postcss-each-variables@^0.3.0 postcss-for@^2.1.1 postcss-nested@^6.0.1 @types/node@^20.11.20 @types/lodash-es@^4.17.12 @vitejs/plugin-vue@^5.0.4 @vitejs/plugin-vue-jsx@^3.1.0 @vue/tsconfig@^0.5.1

// 非开发依赖
pnpm add -w lodash-es@^4.17.21 vue@^3.4.19

在 根目录 package.json 中添加如下内容 添加一下子包的依赖

{
  "dependencies": {
    "Wannaer-element": "workspace:*",
    "@Wannaer-element/hooks": "workspace:*",
    "@Wannaer-element/utils": "workspace:*",
    "@Wannaer-element/theme": "workspace:*"
  }
}
  • components
pnpm add -D @vue/test-utils@^2.4.5 @vitest/coverage-v8@^1.4.0 jsdom@^24.0.0 --filter @Wannaer-element/components
pnpm add @popperjs/core@^2.11.8 async-validator@^4.2.5 --filter @Wannaer-element/components
  • core
// 在 core/package.json 中添加如下内容
{
  "dependencies": {
    "@Wannaer-element/components": "workspace:*"
  }
}
  • docs
pnpm add -D vitepress@1.0.0-rc.44 --filter @Wannaer-element/docs
  • play
    将 play/package.json 中冗余部分删除, 并且删除掉tsconfig.jsontsconfig.node.json
{
  "name": "@Wannaer-element/play",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.4"
  }
}

配置文件

在根目录创建一些必要额配置文件,比如刚才删除play中的ts配置,我们在根目录配置

  • tsconfig.json
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "jsxImportSource": "vue",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["packages/**/*.ts", "packages/**/*.tsx", "packages/**/*.vue"]
}
  • tsconfig.node.json
{
  "extends": "@tsconfig/node18/tsconfig.json",
  "include": ["packages/**/**.config.ts"],
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "types": ["node"]
  }
}
  • postcss.config.cjs
/* eslint-env node */
module.exports = {
  plugins: [
    require("postcss-nested"),
    require("postcss-each-variables"),
    require("postcss-each")({
      plugins: {
        beforeEach: [require("postcss-for"), require("postcss-color-mix")],
      },
    }),
  ],
};

配置完成后,重新安装一下依赖 pnpm install 执行之前更新的部分操作

创建各个分包入口

utils

在utils文件夹 新建一个文件 install.ts 用于 vue plugin 安装的一系列操作

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) => {
      app.use(c);
    });

  return install;
}

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

创建一个utils入口 index.ts 文件 用于导出utils所有方法

export * from "./install";

在这里插入图片描述

components

创建 index.ts 以及第一个基础组件 Button 组件目录

// index.ts
export * from './Button'
//  Button 目录 Button.vue
<template>
  <button style="color: red">this is a button</button>
</template>

<script setup lang="ts">
defineOptions({
  name: "WanButton",
});
</script>

<style scoped></style>
// Button 目录 index.ts
import Button from "./Button.vue";
import { withInstall } from "@Wannaer-element/utils";

export const WanButton = withInstall(Button);

在这里插入图片描述

core

创建 index.ts 、components.ts

// components.ts

import { ErButton } from "@toy-element/components";
import type { Plugin } from "vue";

export default [ErButton] as Plugin[];
import { makeInstaller } from "@toy-element/utils";
import components from "./components";

const installer = makeInstaller(components);

export * from "@toy-element/components";
export default installer;

在这里插入图片描述

play

在main.ts 中 引入了我们刚刚写好的"Wannaer-element"的自定义元素库,并在App.vue中使用。
通过createApp(App).use(WElement).mount(“#app”)这行代码,将"Wannaer-element"库应用到了Vue实例中,并挂载到了id为"app"的DOM元素上。
在这里插入图片描述
在根目录的package.json中配置

  "scripts": {
    "dev": "pnpm --filter @Wannaer-element/play dev",
    "test": "echo \"Error: no test specified\" && exit 1"
  }

它定义了一个名为"dev"的脚本命令。在这个命令中,使用了pnpm工具,并通过"–filter @Wannaer-element/play"参数指定了要过滤的包,然后执行"dev"命令。这段代码的作用是在开发过程中使用pnpm工具来过滤特定的包并执行相应的开发命令。

配置完成后运行 pnpm dev 可以查看到我们刚刚封装好的 Button 虽然很简陋 接下来我们进行样式的修改,让他变得更加美观
在这里插入图片描述

theme

创建 index.css 、reset.css 在 theme/index.css 中导入 reset.css

/** index.css */
@import "./reset.css";
/** reset.css */
body {
  font-family: var(--wan-font-family);
  font-weight: 400;
  font-size: var(--wan-font-size-base);
  line-height: calc(var(--wan-font-size-base) * 1.2);
  color: var(--wan-text-color-primary);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
}

a {
  color: var(--wan-color-primary);
  text-decoration: none;

  &:hovwan,
  &:focus {
    color: var(--wan-color-primary-light-3);
  }

  &:active {
    color: var(--wan-color-primary-dark-2);
  }
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: var(--wan-text-color-regular);
  font-weight: inhwanit;

  &:first-child {
    margin-top: 0;
  }

  &:last-child {
    margin-bottom: 0;
  }
}

h1 {
  font-size: calc(var(--wan-font-size-base) + 6px);
}

h2 {
  font-size: calc(var(--wan-font-size-base) + 4px);
}

h3 {
  font-size: calc(var(--wan-font-size-base) + 2px);
}

h4,
h5,
h6,
p {
  font-size: inhwanit;
}

p {
  line-height: 1.8;

  &:first-child {
    margin-top: 0;
  }

  &:last-child {
    margin-bottom: 0;
  }
}

sup,
sub {
  font-size: calc(var(--wan-font-size-base) - 1px);
}

small {
  font-size: calc(var(--wan-font-size-base) - 2px);
}

hr {
  margin-top: 20px;
  margin-bottom: 20px;
  bordwan: 0;
  bordwan-top: 1px solid var(--wan-bordwan-color-lightwan);
}

最后改 package.json 中 入口为 index.css 在 core/index.ts 中导出我们的 theme
在这里插入图片描述

2、创建VitePress文档

可以直接参考官方文档

npx vitepress init

在这里插入图片描述

// 运行查看效果
pnpm docs:dev

在这里插入图片描述
我们改一下package.json指令 配置后统一可以从根目录运行

// docs目录 package.json
  "scripts": {
    "dev": "vitepress dev",
    "build": "vitepress build",
    "preview": "vitepress preview"
  },
// 根目录 package.js
  "scripts": {
    "dev": "pnpm --filter @Wannaer-element/play dev",
    "docs:dev": "pnpm --filter @Wannaer-element/docs dev",
    "docs:build": "pnpm --filter @Wannaer-element/docs build",
    "docs:preview": "pnpm --filter @Wannaer-element/docs preview",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

接下来我们需要将 VitePress文档部署到 GitHub Actions
所以需要配置一下 docs目录下vitepress => config.mts 添加一个 base: “/wan-element”,解决部署后样式丢失问题

import { defineConfig } from "vitepress";

// https://vitepress.dev/reference/site-config
export default defineConfig({
  title: "Wan-Element",
  description: "高仿 ElementPlus 组件库",
  base: "/wan-element",
  themeConfig: {
    // https://vitepress.dev/reference/default-theme-config
    nav: [
      { text: "Home", link: "/" },
      { text: "Examples", link: "/markdown-examples" },
    ],

    sidebar: [
      {
        text: "Examples",
        items: [
          { text: "Markdown Examples", link: "/markdown-examples" },
          { text: "Runtime API Examples", link: "/api-examples" },
        ],
      },
    ],

    socialLinks: [
      { icon: "github", link: "https://github.com/vuejs/vitepress" },
    ],
  },
});

3、部署到Github Actions

创建一个 .github/workflows/deploy.yml 文件,内容如下

name: deploy

on:
  push:
    branches:
      - master

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: ./packages/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: .

secrets.GH_TOKEN 需要到Github 上面去生成

接下来去 github 创建一个仓库
在这里插入图片描述
复制仓库地址

https://github.com/Manba0/wan-element.git
git remote add origin https://github.com/Manba0/wan-element.git

git add .

git commit -m ":data: first commit"
生成 GH_TOKEN

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最后将刚刚提交的代码 push到Github仓库

git push origin master

如果 push 出现一下报错
fatal: unable to access ‘https://github.com/XXXX/XXXX.git/’: Failed to connect to github.com port 443 after 21067 ms: Couldn’t connect to server

有可能你的gitbub之前设置过代理,只需分别执行如下代码即可:

git config --global --unset http.proxy
git config --global --unset https.proxy

提交成功后 发现 Settings 中Page 没有找到访问的链接,我们查看 Actions 发现 Run tests 没有通过, 因为我们根目录下 package.json 中的 test 指令 "test": "echo \"Error: no test specified\" && exit 1",修改成 "test": "echo 'todo'"重新提交
在这里插入图片描述
在这里插入图片描述

这样就是成功了 我们直接去看Settings中的page https://manba0.github.io/wan-element/
在这里插入图片描述
在这里插入图片描述

GitHub Page 演示

在这里插入图片描述

4、总结

到此我们就已经全流程跑通了 接下来就是完善组件内容了。

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值