从零开始实现Element Plus
- 前言
- 亮点
- 项目搭建
- 1、创建项目
- 初始化
- monorepo
- 创建 .gitignore
- 目录结构
- 安装基础依赖
- 配置文件
- 创建各个分包入口
- utils
- components
- core
- play
- theme
- 2、创建VitePress文档
- 3、部署到Github Actions
- 生成 GH_TOKEN
- GitHub Page 演示
- 4、总结
前言
在本文中,将手把手带你从零开始实现一个类似于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; domkdir $icd $ipnpm initcd ..
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.json
和tsconfig.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: "WButton",
});
</script><style scoped></style>
// Button 目录 index.ts
import Button from "./Button.vue";
import { withInstall } from "@Wannaer-element/utils";export const WButton = withInstall(Button);
core
创建 index.ts 、components.ts
// components.tsimport { 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-confignav: [{ 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: deployon:push:branches:- masterjobs:test:name: Run Lint and Testruns-on: ubuntu-lateststeps:- name: Checkout repouses: actions/checkout@v3- name: Setup Nodeuses: actions/setup-node@v3- name: Install pnpm run: npm install -g pnpm- name: Install dependenciesrun: pnpm install --frozen-lockfile- name: Run testsrun: npm run testbuild:name: Build docsruns-on: ubuntu-latestneeds: teststeps:- name: Checkout repouses: actions/checkout@v3- name: Setup Nodeuses: actions/setup-node@v3- name: Install pnpmrun: npm install -g pnpm- name: Install dependenciesrun: pnpm install --frozen-lockfile- name: Build docsrun: npm run docs:build- name: Upload docsuses: actions/upload-artifact@v3with:name: docspath: ./packages/docs/.vitepress/distdeploy:name: Deploy to GitHub Pagesruns-on: ubuntu-latestneeds: buildsteps:- name: Download docsuses: actions/download-artifact@v3with:name: docs- name: Deploy to GitHub Pagesuses: peaceiris/actions-gh-pages@v3with: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.gitgit 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、总结
到此我们就已经全流程跑通了 接下来就是完善组件内容了。