# 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)
})
};
```
## 杂项
### 开源许可协议