# qi-ui-plus **Repository Path**: yin-javaweb/qi-ui-plus ## Basic Information - **Project Name**: qi-ui-plus - **Description**: 基于Vue3 + Typescript + pnpm + rollup/gulp pnpm workspace实现monorepo架构的UI组件库 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2024-09-30 - **Last Updated**: 2024-09-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 介绍 一个基于`monorepo`模式管理的的一个`vue组件库`。 * 💋 支持按需加载 / 全量加载 * :gift: 支持esm 和 umd 打包模式 * :factory: plop 自动化创建组件 * 🔥 基于pnpm的workspace管理的Monorepo * 💪 完善的组件提示功能 三斜线方式 * :heavy_check_mark: 代码风格统一 * :link: ​ 全量导入`element-plus` 及其 `icon` ## 技术选型 - vue3 - vite - pnpm - gulp + rollup - vitePress - typeScript - plop自动化构建组件文件 - ts 类型声明三斜线,完善的组件提示 - eslint团队代码规范 ## 目录结构 ### 目录含义 ```apl |-- qi-ui-plus |-- package.json |-- plopfile.js |-- pnpm-workspace.yaml |-- build | |-- component.ts |-- docs | |-- index.md | |-- package.json | |-- tsconfig.json | |-- vite.config.ts | |-- .vitepress | | |-- config.js | | |-- theme | | |-- index.js | |-- components | | |-- icon | | |-- index.md | |-- guide | |-- install | | |-- index.md | |-- intro | | |-- index.md | |-- quickstart | |-- index.md |-- packages | |-- components | | |-- index.ts | | |-- package.json | | |-- icon | | |-- index.ts | | |-- src | | |-- icon.ts | | |-- icon.vue | |-- qi-ui-plus | | |-- index.ts | | |-- package.json | |-- theme-chalk | | |-- gulpfile.ts | | |-- package.json | | |-- src | | |-- icon.scss | | |-- index.scss | | |-- fonts | | | |-- iconfont.ttf | | |-- mixins | | |-- mixin.scss | |-- types | | |-- index.ts | |-- utils |-- play |-- plop-template | |-- config.js | |-- component | | |-- index.hbs | | |-- src | | |-- ts.hbs | | |-- vue.hbs | |-- constants | | |-- index.js | |-- docs | |-- md.hbs ``` ### 生成目录结构 ```bash 1)安装mddir (-g是全局安装,可以选择不全局安装,这里因为以后都要使用所以选择的全局安装) npm install mddir -g 2)cd 到你想生成目录的工程结构,直接运行mddir mddir 会有一个叫directoryList.md的文件,项目对应的目录结构就在里面 ``` ## pnpm ### pnpm 介绍 ``` pnpm-workspace.yaml中注册的文件夹为pnpm管理的子项目 , 在根目录(workspace root)中执行pnpm会安装node_modules到所有子项目中包括根目录 ``` https://developer.51cto.com/article/708411.html ### monorepo两种项目的组织方式 - Multirepo(Multiple):每一个包对应一个项目 - Monorepo(Monolithic Repository):一个项目仓库中管理多个模块/包 https://www.kancloud.cn/chandler/web_technology/2625186#lerna_61 ### pnpm 、npm 、yarn 、lerna `npm/yarn` 采用了直接平铺的方式,而 `pnpm` 则是采用 `.pnpm` 隐藏目录隐藏真实的平铺结构,再使用链接(symbollink)的方式将真实安装的目录映射到 node_modules 下 参考链接(非常推荐):平铺的结构不是 node_modules 的唯一实现方式 天生支持 monorepo(workspace 特性,体验也比 lerna 或是 yarn workspace 好太多) https://www.kancloud.cn/chandler/web_technology/2625186 https://blog.csdn.net/weixin_44691608/article/details/122379051 注:`mono-repo`最出名是使用 [Lerna](https://github.com/lerna/lerna/) 管理 workspaces。但是后来 `pnpm` 取代之前的 `lerna`,https://blog.csdn.net/astonishqft/article/details/124823381 #### Lerna 自动化发布 管理发布npm和git `Lerna`是`npm`模块的管理工具,为项目提供了集中管理`package`的目录模式,如统一的` repo` 依赖安装、`package scripts`和`发版` 、`清理工程环境`等特性。 https://blog.csdn.net/Moonoly/article/details/108330361 ### workspace依赖管理 如果不用`workspaces`时,因为各个`package`理论上都是独立的,所以每个`package`都维护着自己的`dependencies`,而很大的可能性,`package`之间有不少相同的依赖,而这就可能使`install`时出现重复安装,使本来就很大的 `node_modules`继续膨胀(这就是「依赖爆炸」...)。 ```bash # 安装到工作区根目录并且是开发依赖 pnpm install 包名 -D -w --filter 安装到子目录 ``` 注:`yarn`也有`workspaces`依赖管理 #### pnpm 清理 在依赖乱掉或者工程混乱的情况下,清理依赖 ```js // 参考element-plus 作者 https://zhuanlan.zhihu.com/p/484016976 "clean": "pnpm run clean:dist && pnpm run clean --filter ./packages/ --stream", "clean:dist": "rimraf dist", ``` #### 外层script 执行内层命令 ```js ... "scripts": { "dev": "pnpm -C play dev" # 执行play下的dev脚本 }, ... ``` #### pnpm init -y yes 和 -f force ## plop plop是一个命令行工具,通过配置模板,生成对应文件,可以理解为脚手架工具(参考tg-ui) ``` https://segmentfault.com/a/1190000040776418?sort=newest https://juejin.cn/post/6873767308607619085 ``` ## 组件类型提示 ### ts的三斜线 类型提示 `qi-ui-plus`提供了所有组件的类型定义,你可以参考下面的代码进行导入类型声明。(参考idux) ```js // env.d.ts /// /// /// declare module '*.vue' { import { DefineComponent } from 'vue' // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types const component: DefineComponent<{}, {}, any> export default component } ``` 以下无用: element-plus的根目录下components.d.ts有类似的文件,但是element是给volar使用的,xbb-plus的组件提示 也是一样 ```js declare module '@vue/runtime-core' { export interface GlobalComponents { "dx-button": typeof import('@tophant-cd-ui/components/button')['default'] DxButton: typeof import('@tophant-cd-ui/components/button')['default'] } } ``` ### 发布到@types ``` https://juejin.cn/post/6844904141840449544?1 ``` ### webstorm编译器的提示 ``` "web-types": "highlight/web-types.json" ``` ## Rollup打包 ### 代码压缩 ``` 已支持rollup打包 压缩代码 https://blog.csdn.net/weixin_39951988/article/details/121857641 ``` ### 清除无用代码 ``` 启用rollup清除无用代码工具rollup-plugin-cleanup ``` ### rollup打包后删除console ``` rollup打包后删除console ``` ### 组件内部如果有css 打包会报错 ```JavaScript https://my.oschina.net/skywingjiang/blog/5277908 // build/components.ts full-components.ts import vue from "rollup-plugin-vue"; import RollupPluginPostcss from 'rollup-plugin-postcss'; // 组件内部如果有css 打包会报错 import Autoprefixer from 'autoprefixer' vue({ preprocessStyles: false }), RollupPluginPostcss({ extract: true, plugins: [Autoprefixer] }), ``` ### gulp 打包utils ``` https://juejin.cn/post/6872616202993139720 https://segmentfault.com/a/1190000040776418 ``` ### 排除第三方库 ```js /* 只要ui组件库中引入了第三方库,就会连同第三方库一起打包,所以需要排除,且需要把第三方库 安装在packages/qi-ui-plus中,(如果其他项目要引用ui组件库,则在npm i qi-ui-plus的同时 去安装第三方库, 因为在qi-ui-plus的package.json中依赖了第三方库) https://www.csdn.net/tags/MtTaEg0sOTY4NDc2LWJsb2cO0O0O.html */ // build/component.ts const config = { ... // external: (id) => /^vue/.test(id) || /^@tophant-cd-ui/.test(id) || /^moment/.test(id) || /^element-plus/.test(id), // 排除掉vue和@qi-ui-plus 和 moment、element-plus的依赖, 只要是组件中引入了(import)第三方库都需要排除 external: (id: string) => /^(vue|@vue|@vueuse|element-plus|@element-plus|@qi-ui-plus|moment|lodash)/.test(id), ... } ``` ## 代码风格统一 ### prettierrc.js ## 文档doc ### vitePress 本UI组件库文档采用vitePress ``` 参考: https://www.jianshu.com/p/0210f603006d https://www.cfanz.cn/resource/detail/rMOjvVVLlnrQL ``` ### vitepress文档站内搜索 ### 可选:可视化组件库storybook ## 开发utils ## 本地测试"包" ### 全量引入(本地测试,推荐) ```js import QiUi from '@qi-ui-plus/components' // 本地测试时,全量导入 也可以按需导入 import '@qi-ui-plus/theme-chalk/src/index.scss'; // 导入css样式 icon图标组件需要 ``` ### 按需引入 ```js import { QiIcon } from '@qi-ui-plus/components' // 本地测试时 推荐此方式按需导入 ``` ### 模拟发布后 ```js import QiUi from 'qi-ui-plus' // 本地测试时,可以把打包后的dist复制到node_modules中并改名为qi-ui-plus ``` ## 发布NPM NPM 本地发布 发布到npm的方法很简单, 首先我们需要先注册去npm官网注册一个账号, 然后控制台登录即可,最后我们执行npm publish即可.具体流程如下: ```javascript // 本地编译好组件库代码,进入编译后的目录 // 登录 npm login // 发布过程 // 确保 registry 是 https://registry.npmjs.org npm config get registry // 如果不是则先修改 registry npm config set registry=https://registry.npmjs.org // 发布 npm publish // 如果发布失败提示权限问题,请执行以下命令 npm publish --access public //删除已发布的组件(不推荐删除已发布的组件),则执行以下命令(加 --force 强制删除) > npm unpublish --force 删除指定版本的包,比如包名为 vue-vant 版本 0.1.0 > npm unpublish vue-vant@0.1.0 如果24小时内有删除过同名的组件包,那么将会发布失败 只能换一个名称发布或者等24小时之后发布,所以不要随便删除已发布的组件(万一有项目已经引用) ``` npm相关的知识这里简单提一下 #### 1. .npmignore 配置文件 `.npmignore`配置文件类似于 `.gitignore` 文件,如果没有 `.npmignore`,会使用`.gitignore`来取代他的功能。 #### 2. npm发包的版本管理 npm的发包遵循语义化版本,一个版本号格式如下:Major.Minor.Patch,每一部分具体介绍如下: - Major 表示主版本号,做了不兼容的API修改时需要更新 - Minor 表示次版本号,做了向下兼容的功能性需求时需要更新 - Patch 表示修订号, 做了向下兼容的问题修正时需要更新 对应的npm也提供了脚本帮我们实现自动更新版本号,如下: ```bash npm version patch npm version minor npm version major ``` 还有更加深入的知识比如版本的tag化这些,大家感兴趣也可以研究一下. 本文的组件库搭建参考element的目录组织方式,大家也可以直接采用element或者其他开源组件库的脚手架来实现. ### NPM私有服搭建(verdaccio) ``` https://www.freesion.com/article/6294468019/ https://blog.csdn.net/tglsaturn/article/details/120831892?1 ``` ## 其他项目使用 ```js import { createApp } from 'vue' import QiUi from 'qi-ui-plus' // 全量导入 // import { QiIcon } from 'qi-ui-plus' // 按需导入 // import QiUi,{ QiIcon } from 'qi-ui-plus' import App from './App.vue' createApp(App) .use(QiUi) .mount('#app') ``` ## 发布Npm后 项目中引入Css ### 全量引入css ```js import 'qi-ui-plus/theme-chalk/index.css'; ``` 注:开发UI组件时请尽量不要组件内部写style(虽然也会提取出来放在`dist/theme-chalk/components-style.css`),而应该写在theme-chalk目录下按照组件名命名,例如`button.scss` ### 抽取vue组件内部的style 开发UI组件时,组件内的style 不要加`scoped`,因为组件库打包不需要作用域,会把所有组件内的style抽离成components-style.css 并复制打包进`dist/theme-chalk/components-style.css` 具体项目使用UI组件库时,可在main.ts中引入css ```js import 'qi-ui-plus/theme-chalk/components-style.css' ``` 一开始考虑把组件内的css也统一打包进`theme-chalk/index.scss`,但是没必要,最终采取不在`vue`组件内写`style`,而在`theme-chalk`目录内创建组件对应的`scss`文件,然后统一在`theme-chalk/index.scss`中引入,所以实际项目中不需要单独引入此样式 ## 全量导入element-plus(难点) 本UI库已全量导入了`element-plus`,故项目中使用`qi-ui-plus`时就不用导入`element-plus`了,直接使用其组件即可; 且开发组件库时`docs`和`play`目录也不需要导入`element-plus`即可使用其组件。 过程如下: ### 1、doc和play中使用 在`packages/components/index.ts`中导入`element-plus`供`doc`和`play`中使用 ```js // packages/components/index.ts import ElementPlus from 'element-plus' // 此处加element-plus 主要是给本地docs、play测试用 import 'element-plus/dist/index.css' // components中需要导入css,打包时会打包这句话,但是不起作用 // 注册所有的组件 const install = function (app: App): void { ... app.use(ElementPlus) } ``` ### 2、最终一起打包进dist中 在`packages/qi-ui-plus/index.ts`中导入`element-plus`供最终一起打包进`dist`中 ```js // packages/qi-ui-plus/index.ts import ElementPlus from 'element-plus' // 此处加element-plus 主要是打包进最终的dist中(tophant-cd-ui) // import 'element-plus/dist/index.css' // 不需要导入element-plus的样式,因为样式已经在packages/theme-chalk/gulpfile.ts中 在执行打包css后,向打包后的index.css中动态导入了 const install = (app: App) => { ... app.use(ElementPlus) }; ``` ### 3、css在哪儿导入呢? 发现上面2个地方都没有导入`element`的`css`,故有如下几种测试: 1、一开始考虑就在上面2个地方 像平时导入css一样,但是打包后在项目中引用`qi-ui-plus`后使用`element-plus`的组件发现css样式根本没有起作用; 2、在`packages/theme-chalk/src/index.scss`中导入`element-plus`的css,但是发现打包的时候会把element-plus的所有css拷贝进`dist/index.css`中; ```scss // @use "../../../node_modules/element-plus/dist/index.css"; // 不需要显示导入,因为打包的时候会把element-plus的所有css拷贝过来,所以利用gulp的插件在打包后的index.css文件的头部插入element的样式引入,就避免了打包所有的element的css @use 'icon.scss'; @use 'date.scss'; ``` 3、如果只是在打包后的index.css中有一句导入element-plus的样式,而不需要拷贝其所有css就解决这个问题了; ```js // packages/theme-chalk/gulpfile.ts import header from "gulp-header"; // import footer from "gulp-footer"; // 向打包后的theme-chalk/dist/index.css的头部添加element-plus的css function addElementToHeader(){ return src(path.resolve(__dirname, "./dist/index.css")) .pipe(header('@import \"element-plus/dist/index.css\";\n')) .pipe(dest('./dist')); } export default series(compile, addElementToHeader, copyfont, copyfullstyle); ``` **gulp 向文件插入代码** 实现element-plus的样式导入 https://qa.1r1g.com/sf/ask/2686245551/ https://www.jianshu.com/p/bbaa0d821c81 ### 4、全量导入element-icon ```js // packages/components/index.ts 和 packages/qi-ui-plus/index.ts import * as ElementPlusIconsVue from '@element-plus/icons-vue' const install = (app: App) => { ... // 统一注册Icon图标 Object.entries(ElementPlusIconsVue).forEach(([iconName, component]) => { app.component(iconName, component) }) }; ``` ## 杂项 ### 开源许可协议