# mini-element **Repository Path**: cpranmo/mini-element ## Basic Information - **Project Name**: mini-element - **Description**: 使用 menorepo 搭建简易版 element-plus 组件库,手写实现部分组件 - **Primary Language**: JavaScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 2 - **Created**: 2022-09-25 - **Last Updated**: 2025-01-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 搭建 monorepe 环境 本过程全程使用 pnpm 进行安装包 使用 pnpm 安装包速度快,磁盘空间利用高效,使用 pnpm 可以快速建立 monorepe, 所以这里使用 pnpm workspace 来实现 monorepe ```shell pnpm install pnpm -g #全局安装 pnpm init # 初始化 package.json 配置文件, 定为私有库 pnpm install vue typescript -D -w #添加依赖包 ``` > 使用 pnpm 必须要建立 .npmrc 文件 shamefully-hoist = true, 否则安装的模块无法放置在 node_modules 目录下 ```shell pnpm tsc --init # 初始化 tsconfig.json 文件 ``` ```json // tsconfig.json 配置文件 { "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-workspace.yaml 文件映射查看管理包结构 ```yaml # pnpm-workspace.yaml packages: - play # 存放我们组件测试的时候的代码 - docs # 存放我们组件文档的 - "packages/**" ``` 在项目下对应着 pnpm-workspace.yaml 下结构创建对应的文件 在 packages (作用存放自己写的各种组件) 下创建 components (存放组件) theme-chalk(存放组件样式) utils(存放组件工具代码) 分别在对应的包下进行 pnpm init 进行初始化, 并把报名取好 component/package.json ```json { "name": "@cp-ranmo/components", "version": "1.0.0", "description": "存放各种组件的代码", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } ``` theme-chalk/package.json ```json { "name": "@cp-ranmo/theme-chalk", "version": "1.0.0", "description": "用于组件库存放主题样式", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } ``` utils/package.json ```json { "name": "@cp-ranmo/utils", "version": "1.0.0", "description": "用于存放组件中工具函数", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } ``` 由于整个项目中要快捷使用 package 下的文件,所以在项目下载对应的包名(安装本地包)从此可以直接导入 ```shell # 将自己写包安装到 node_modules 下 pnpm install @cp-ranmo/components @cp-ranmo/theme-chalk @cp-ranmo/utils -w ``` ## 创建组件测试环境 ```shell pnpm create vite play --template vue-ts # 创建一个 vite 项目名 play 模板为 vue-ts 解析 ts ``` _vite-env.d.ts 文件为解析 .vue 文件作用(名称可以随意,但是一定后缀为 d.ts 文件)_ 新建一个 typings 目录,然后再目录中新建一个 .d.ts 文件将测试环境 play 中 .d.ts 文件内容复制到该文件中,作用是解析整个项目中的 .vue 文件 ```js // 例如 vue-shim.d.ts declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } ``` > 在项目根 package.json 添加脚本启动命令测试项目 ```json { "scripts": { "dev": "pnpm -C play dev" // 执行 play 包下的 dev 命令 } } ``` ## 编写测试组件 ### 实现 BEM 规范 > packages/utils 工具包下 创建一个 create 文件 ```ts // 前缀名字 z-button-box__element--modifier function _bem( prefixName: string, blockSuffix: string, element: string, modifier: string ) { if (blockSuffix) { prefixName += `-${blockSuffix}`; } if (element) { prefixName += `__${element}`; } if (modifier) { prefixName += `--${modifier}`; } return prefixName; } function createBEM(prefixName: string) { const b = (blockSuffix = "") => _bem(prefixName, blockSuffix, "", ""); const e = (element = "") => element ? _bem(prefixName, "", element, "") : ""; const m = (modifier = "") => modifier ? _bem(prefixName, "", "", modifier) : ""; const be = (blockSuffix = "", element = "") => blockSuffix && element ? _bem(prefixName, blockSuffix, element, "") : ""; const bm = (blockSuffix = "", modifier = "") => blockSuffix && modifier ? _bem(prefixName, blockSuffix, "", modifier) : ""; const em = (element = "", modifier = "") => element && modifier ? _bem(prefixName, "", element, modifier) : ""; const bem = (blockSuffix = "", element = "", modifier = "") => blockSuffix && element && modifier ? _bem(prefixName, blockSuffix, element, modifier) : ""; const is = (name: string, state: string | boolean) => state ? `is-${name}` : ""; return { b, e, m, be, bm, em, bem, is }; } export function createNamespace(name: string) { const prefixName = `z-${name}`; return createBEM(prefixName); } const bem = createNamespace("icon"); console.log(bem.b("box")); console.log(bem.e("element")); console.log(bem.m("modifier")); console.log(bem.bem("box", "element", "modifier")); console.log(bem.is("checked", true)); console.log(bem.be("box", "element")); console.log(bem.bm("box", "modifier")); console.log(bem.em("element", "modifier")); ``` ### 实现 Icon 组件 在 packages/components 新建 icon 目录存放图标组件相关代码 > icon/src/icon.ts 定义组件中相关类型 ```ts // 存放组件相关的属性和 ts 类型 import { ExtractPropTypes, PropType } from "vue"; export const iconProps = { color: String, size: [Number, String] as PropType } as const; export type IconProps = ExtractPropTypes; ``` > icon/src/icon.vue 组件模块 ```shell # 用于给 setup 语法糖组件添加 name pnpm install unplugin-vue-define-options -D -w ``` ```vue ``` ### 导出 Icon 组件 > icon/index.ts 组件入口, 用来整合组件的 最终实现导出组件 ```ts import _Icon from "./src/icon.vue"; import { withInstall } from "@cp-ranmo/utils/with-install"; const Icon = withInstall(_Icon); export default Icon; // 默认导出, 可以通过 app.use 全局使用, 也可以通过单独使用 export * from "./src/icon"; // 导出类型 // 添加类型 可以再模版中被解析 declare module "vue" { // 我们的接口可以自动合并 export interface GlobalComponents { CIcon: typeof Icon; } } ``` ### 展示组件 > play/src/main.ts ```ts import { createApp } from "vue"; import App from "./App.vue"; // 导入全局样式 // import "@cp-ranmo/theme-chalk/src/index.scss"; // 导入对应组件 import Icon from "@cp-ranmo/components/icon"; const plugins = [Icon]; const app = createApp(App); plugins.map(plugin => app.use(plugin)); // 全局引入插件 app.mount("#app"); ``` > play/src/App.vue ```vue ``` ### svg 图标 安装图标库 ```shell pnpm install @vicons/ionicons5 # 安装 icon 图库 ``` > play/src/App.vue ```vue ``` ## scss 编写 ### 结构目录 ```shell theme-chalk | |_src | |_mixins | |_config.scss # BEM 规范命名 ``` ### scss 配置文件 > mixins/config.scss ```scss // 配置基本命名 $namespace: "c"; $element-separator: "__"; $modifier-separator: "--"; $state-prefix: "is-"; ``` > mixins/mixins.scss ```scss // 样式混合 @use "config" as *; @forward "config"; // 导出 // .c-button{} @mixin b($block) { $B: $namespace + "-" + $block; .#{$B} { @content; } } // .c-button.is-desiabled{} @mixin when($state) { @at-root { &.#{$state-prefix + $state} { @content; } } } // .c-button--primary{} @mixin m($modifier) { @at-root { #{& + $modifier-separator + $modifier} { @content; } } } // .c-button__header{} @mixin e($element) { @at-root { #{& + $element-separator + $element} { @content; } } } ``` > src/icon.scss Icon 组件样式 ```scss @use "mixins/mixins.scss" as *; // 全部导入 @include b(icon) { display: inline-flex; height: 1.5em; width: 1.5em; vertical-align: middle; } ``` > src/index.scss 入口文件 ```scss @use "./icon.scss"; // 使用 use 不会多次导入 import 导入则可能多次 ``` ### 测试中使用 scss 全局安装 scss 进行解析 ```shell pnpm install sass -D -w ``` > play/src/main.ts ```ts import { createApp } from "vue"; import App from "./App.vue"; // 导入全局样式 import "@cp-ranmo/theme-chalk/src/index.scss"; // 导入对应组件 import Icon from "@cp-ranmo/components/icon"; const plugins = [Icon]; const app = createApp(App); plugins.map(plugin => app.use(plugin)); // 全局引入插件 app.mount("#app"); ``` ## eslint 配置 ```shell npx eslint --init # 初始化代码校验文件 # 选择如下 √ How would you like to use ESLint? · problems √ What type of modules does your project use? · esm √ Which framework does your project use? · vue √ Does your project use TypeScript? · No / Yes √ Where does your code run? · browser, node √ What format do you want your config file to be in? · JavaScript Local ESLint installation not found. Local ESLint installation not found. The config that youve selected requires the following dependencies: eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest √ Would you like to install them now? · No / Yes A config file was generated, but the config file itself may not follow your linting rules. ``` 手动安装插件 ```shell # 上面不选择直接安装是因为使用 npm 安装而这里我们的目的是要使用 pnpm 安装 pnpm install eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest -D -w ``` > 支持 vue 中 ts `eslint` 配置 ```shell pnpm i @vue/eslint-config-typescript -D -w ``` > .eslintrc 配置 ```js module.exports = { env: { browser: true, es2021: true, node: true }, extends: [ "eslint:recommended", "plugin:vue/vue3-essential", "plugin:@typescript-eslint/recommended", // 代码检测 "@vue/typescript/recommended" ], overrides: [], parserOptions: { ecmaVersion: "latest", parser: "@typescript-eslint/parser", sourceType: "module" }, plugins: ["vue", "@typescript-eslint"], rules: { "vue/html-self-closing": "off", "vue/singleline-html-element-content-newline": "off", "vue/multi-word-component-names": "off", "vue/prefer-import-from-vue": "off", "vue/max-attributes-per-line": "off" }, globals: { defineProps: "readonly", defineOptions: "readonly" } }; ``` > .eslintignore 忽略校验配置 ```shell node_modules dist *.css *.jpg *.jpeg *.png *.gif *.d.ts # 忽略校验文件 ``` > 不管是代码校验还是风格都需要配合编辑器 vscode 安装 eslint 插件完成, eslint 只检测代码规范 ## prettier 配置 安装相关依赖插件 ```shell pnpm install @vue/eslint-config-prettier eslint-plugin-prettier prettier -D -w # eslint-plugin-prettier 为 eslint 与 prettier 中间桥梁 ``` > .prettierrc.js ```js // 代码风格配置 module.exports = { singleQuote: false, // 使用单引号 semi: true, // 使用分号 trailingComma: "none", // 末尾逗号 arrowParens: "avoid", // 箭头函数括号 useTabs: false, endOfLine: "auto" // 结尾换行自动 ``` > .prettierignore 代码风格忽略 ```shell node_modules dist # 忽略文件 ``` > 最终安装 vscode 中 prettier 插件才能生效,prettier 只是用来格式化代码 ## 编辑器配置 > .editorconfig ```shell root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf # 编辑器配置 ``` > 用于保证编辑开发统一规则 ## 代码提交校验 ```shell git init pnpm install husky -D -w npm set-script prepare "husky install" # 在 package 文件脚本 script 添加一条命令,次命令只会在使用 pnpm install 执行一次 npx husky add .husky/pre-commit 'pnpm lint' #代码提交前校验(注意先创建husky文件) ``` > 代码提交校验 ```shell pnpm install @commitlint/cli @commitlint/configconventional -D npx husky add .husky/commit-msg "npx --noinstall commitlint --edit $1" ``` > commitlint.config.js 配置 ```js module.exports = { extends: ["@commitlint/config-conventional"] }; ``` 例如 > git commit -m 'feat: init' 注意 feat 冒号后有空格 ### commitlint | 类型 | 描述 | | -------- | ------------------------------------------------------------------------------------ | | build | 主要⽬的是修改项⽬构建系统(例如 glup, webpack,rollup 的配置等)的提交 | | chore | 不属于以上类型的其他类型 | | ci | 主要⽬的是修改项⽬继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle 等)的提交 | | docs | ⽂档更新 | | feat | 新功能、新特性; | | fix | 修改 bug; | | perf | 更改代码,以提⾼性能; | | refactor | 代码重构(重构,在不影响代码内部⾏为、功能下的代码修改); | | revert | 不影响程序逻辑的代码修改(修改空⽩字符,格式缩进,补全缺失的分号等,没有改变代码逻辑) | | test | 测试⽤例新增、修改; | ## vitepress 组件文档 ```shell pnpm install vitepress -D -w #在 doc 目录下安装(注意先进行 pnpm init 初始化) ``` > docs/package.json ```json { //.... "scripts": { "dev": "vitepress dev ." } //... } ``` > 在根目录中增加启动命令 package.json 文件中 ```shell "docs:dev": "pnpm -C docs dev" ``` ### 首页配置 > docs/index.md ```md --- # 入口文件 layout: home hero: name: c-ui 组件库 text: 基于 Vue 3 的组件库. tagline: 掌握 vue3 组件编写 actions: - theme: brand text: 快速开始 link: /guide/quieStart # 单元块 features: - icon: 🛠️ title: 组件库构建流程 details: Vue3 组件库构建... - icon: ⚙️ title: 组件库单元测试 details: Vue3 组件库测试... --- ``` ### 文档配置 > docs/.vitepress/config.js ```js // 文档的配置 module.exports = { title: "C-UI", description: "cpranmo ui", themeConfig: { lastUpdated: "最后更新时间", docsDir: "docs", editLinks: true, editLinkText: "编辑此网站", repo: "https://gitee.com/cpranmo", // 底部内容 footer: { message: "Released under the MIT License.", copyright: "Copyright © 2022-present cp ranmo" }, // 顶部导航 nav: [ { text: "指南", link: "/guide/installation", activeMatch: "/guide/" }, // link 对应的文件路径名 { text: "组件", link: "/component/icon", activeMatch: "/component/" } ], // 侧边导航(匹配顶部导航) sidebar: { "/guide/": [ { text: "指南", items: [ { text: "安装", link: "/guide/installation" }, { text: "快速开始", link: "/guide/quieStart" } ] } ], "/component/": [ { text: "基础组件", items: [{ text: "Icon", link: "/component/icon" }] } ] } } }; ``` ### 主题配置 > docs/.vitepress/theme/index.js ```js import DefaultTheme from "vitepress/theme"; // 导入 vitepress 主题样式 import Icon from "@cp-ranmo/components/icon"; import "@cp-ranmo/theme-chalk/src/index.scss"; console.log(Icon); export default { ...DefaultTheme, // 类似进行插件安装 enhanceApp({ app }) { app.use(Icon); // 在 vitepress 中注册全局组件 } }; ``` ### Icon 组件文档 组件文档编写创建目录需要根据 config.js 文件对应,更多详情可以查看 vitepress 文档使用 > docs/component/icon.md 内容见 Icon 组件详细文档 ## 虚拟树组件开发 ### 定义树属性 > packages/components/tree/src/tree.ts 定义树相关类型 ```ts // 定义一些树的相关类型 import { ExtractPropTypes, PropType } from "vue"; export type Key = string | number; // 树属性 export interface TreeOption { label?: Key; key?: Key; children?: TreeOption[]; isLeaf?: boolean; // 是否叶子节点 [key: string]: unknown; // 其他类型属性 } // Required 基础 TreeOption 且属性为必填 export interface TreeNode extends Required { level: number; // 层级 rawNode: TreeOption; // 原有数据 children: TreeNode[]; isLeaf: boolean; } // 定义树的属性 export const treeProps = { data: { type: Array as PropType, default: () => [] // 默认空数组 }, defaultExpandeKeys: { type: Array as PropType, // 默认展开数据中 key 值 default: () => [] }, keyField: { type: String, default: "key" }, labelField: { type: String, default: "label" }, childrenField: { type: String, default: "children" } } as const; // 每棵树节点类型 export const treeNodeProps = { node: { type: Object as PropType, required: true }, expanded: { type: Boolean, required: true } } as const; // Partial 可以将属性变为可选 export type TreeNodeProps = Partial>; export type TreeProps = Partial>; export const treeNodeEmitts = { toggle: (node: TreeNode) => node }; ``` > packages/components/tree/src/tree.vue 树的模板 ```vue ``` > packages/components/tree/index.ts 树的入口文件 ```ts import { withInstall } from "@cp-ranmo/utils/with-install"; import _Tree from "./src/tree.vue"; const Tree = withInstall(_Tree); export default Tree; // 默认导出 export * from "./src/tree"; declare module "vue" { export interface GlobalComponents { CTree: typeof Tree; } } ``` > play/src/main.ts ```ts // ..... import Tree from "@cp-ranmo/components/tree"; const plugins = [Icon, Tree]; // ...... ``` ### 数据格式化 #### 创建渲染数据 > play/src/App.vue ```vue ``` #### 封装获取属性方法 > components/tree/src/tree.vue ```vue ``` #### 数据格式化 > components/tree/src/tree.vue ```js function createTree(data: TreeOption[]) { // 将数据进行格式化转化 function traversal( data: TreeOption[], parent: TreeNode | null = null ): TreeNode[] { return data.map(node => { const children = treeOptions.getChildren(node) || []; const treeNode: TreeNode = { key: treeOptions.getKey(node), label: treeOptions.getLabel(node), children: [], // 默认为空 rawNode: node, level: parent ? parent.level + 1 : 0, // 判断节点是否自带 isLeaf 如果自带以自带的为准,如果没有自带的则看一下有没有children属性 isLeaf: node.isLeaf ?? children.length == 0 // ?. ?? 对 || 增强操作 }; if (children.length > 0) { treeNode.children = traversal(children, treeNode); // 存在孩子则进行递归孩子,将其格式化成 treeNode 类型 } return treeNode; }); } return traversal(data); } ``` #### 拍平树数据 > components/tree/src/tree.vue ```vue ``` #### 渲染树组件 > components/tree/src/tree.vue ```vue ``` > play/src/App.vue ```vue ``` ### 抽离 TreeNode 组件