# UI
**Repository Path**: dulily/UI
## Basic Information
- **Project Name**: UI
- **Description**: 个人组件库
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-10-08
- **Last Updated**: 2024-11-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 个人项目组件库
**使用vite + typescript 打造专属的 vue3 组件库**
## 前言
随着前端技术的发展,业界涌现出了许多的UI组件库。例如我们熟知的ElementUI,Vant,AntDesign等等。但是作为一个前端开发者,你知道一个UI组件库是如何被打造出来的吗?
读完这篇文章你将学会:
- 如何使用pnpm搭建出一个Monorepo环境
- 如何使用vite搭建一个基本的Vue3脚手架项目
- 如何开发调试一个自己的UI组件库
- 如何使用vite打包并发布自己的UI组件库
作为一个前端拥有一个属于自己的UI组件库是一件非常酷的事情。它不仅能让我们对组件的原理有更深的理解,还能在找工作的时候为自己增色不少。试问有哪个前端不想拥有一套属于自己的UI组件库呢?
本文将使用Vue3和TypeScript来编写一个组件库,使用Vite+Vue3来对这个组件库中的组件进行调试,最后使用vite来对组件库进行打包并且发布到npm上。最终的产物是一个名为duli-ui的组件库。 话不多说~ 接下来让我们开始搭建属于我们自己的UI组件库吧!
## Monorepo环境
首先我们要了解什么是Monorepo及它是如何搭建的吧 就是指在一个大的项目仓库中,管理多个模块/包(package),这种类型的项目大都在项目根目录下有一个`packages`文件夹,分多个项目管理。大概结构如下:
```json
-- packages
-- pkg1
--package.json
-- pkg2
--package.json
--package.json
```
简单来说就是**单仓库、多项目**
目前很多我们熟知的项目都是采用这种模式,如`Vant`,`ElementUI`,`Vue3`等。打造一个Monorepo环境的工具有很多,如:`lerna`、`pnpm`、`yarn`等,这里我们将使用`pnpm`来开发我们的UI组件库。
为什么要使用pnpm?
> 因为它简单高效,它没有太多杂乱的配置,它相比于lerna操作起来方便太多
好了,下面我们就开始用pnpm来进行我们的组件库搭建吧!
## 使用pnpm
### 安装
```sh
npm install pnpm -g
```
### 在电脑本地磁盘创建目录
```sh
mkdir duli-ui
```
### 初始化package.json
```sh
pnpm init
```
### 新建配置文件 .npmrc
```sh
shamefully-hoist = true
```
这里简单说下为什么要配置`shamefully-hoist`。
>如果某些工具仅在根目录的`node_modules`时才有效,可以将其设置为`true`来提升那些不在根目录的node_modules,就是将你安装的依赖包的依赖包的依赖包的...都放到同一级别(扁平化)。说白了就是不设置为`true`有些包就有可能会出问题。
## monorepo的实现
接下就是`pnpm`如何实现`monorepo`的了。 为了我们各个项目之间能够互相引用我们要新建一个`pnpm-workspace.yaml`文件将我们的包关联起来
```
packages:
- 'packages/**'
- 'examples'
```
记住:需要在项目根目录下创建`packages`和`examples`目录 这样就能将我们项目下的`packages`目录和`examples`目录关联起来了,当然如果你想关联更多目录你只需要往里面添加即可。根据上面的目录结构很显然你在根目录下新`packages`和`examples`文件夹。
- `packages`文件夹存放我们开发的包;
- `examples`用来调试我们的组件,`examples`文件夹就是接下来我们要使用vite搭建一个基本的Vue3脚手架项目的地方;
## 安装对应依赖
我们开发环境中的依赖一般全部安装在整个项目根目录下,方便下面我们每个包都可以引用,所以在安装的时候需要加个`-w`
```
# 因为我们使用了-w参数,所以在其他目录下安装也会将依赖安装到根目录下
pnpm i vue@next typescript less -D -w
```
因为我们开发的是vue3组件, 所以需要安装vue3,当然ts肯定是必不可少的(当然如果你想要js开发也是可以的,甚至可以省略到很多配置和写法。但是ts可以为我们组件加上类型,并且使我们的组件有代码提示功能,未来ts也将成为主流);less为了我们写样式方便,以及使用它的命名空间(这个暂时这里没用到,后面有时间再补)
## 配置tsconfig.json
这里的配置就不细说了,可以自行搜索都是代表什么意思。或者你可以先直接复制
```
npx tsc --init # 或者tsc --init
```
`tsconfig.json`文件内容如下:
```
{
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve",
"strict": true,
"target": "ES2015",
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"moduleResolution": "Node",
"lib": ["esnext", "dom"]
}
}
```
## 手动搭建一个基于vite的vue3项目
其实搭建一个vite+vue3项目是非常容易的,因为vite已经帮我们做了大部分事情
### 初始化仓库
进入`examples`文件夹,执行:
```
cd examples
pnpm init
```
该操作会默认再`examples`下创建`package.json`文件。
### 安装vite和@vitejs/plugin-vue
`@vitejs/plugin-vue`用来支持`.vue`文件的转译
```
pnpm install vite @vitejs/plugin-vue -D -w
```
使用上面的命令,会将依赖都放在根目录下。
### 配置vite.config.ts
在`examples`目录下新建`vite.config.ts`
```
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins:[vue()]
})
```
### 新建html文件
`@vitejs/plugin-vue`会默认加载`examples`下的`index.html`,在`examples`目录下新建`index.html`文件:
```
Document
```
**注意**:`vite`是基于`esmodule`的,所以`type="module"`
### 新建app.vue模板
在`examples`目录下新建`app.vue`文件:
```
启动测试
```
### 新建main.ts
在`examples`目录下新建`main.ts`文件:
```
import {createApp} from 'vue'
import App from './app.vue'
const app = createApp(App)
app.mount('#app')
```
此时会发现编译器会提示个错误:找不到模块“./app.vue”或其相应的类型声明 
因为直接引入`.vue`文件,TS会找不到对应的类型声明;所以需要新建`typings`(命名没有明确规定,TS会自动寻找`.d.ts`文件)文件夹来专门放这些声明文件。 在`examples`目录下新建`typings/vue-shim.d.ts`文件: TypeScript默认只认ES模块。如果你要导入`.vue`文件就要`declare module`把他们声明出来。
```
declare module '*.vue' {
import type { DefineComponent } from "vue";
const component:DefineComponent<{},{},any>
}
```
### 配置脚本启动项目
最后在`examples/package.json`文件中配置`scripts`脚本:
```
...
"scripts": {
"dev": "vite"
},
...
```
然后终端输入我们熟悉的命令:`pnpm run dev`(需要进入`examples`目录下再执行)。 vite启动默认端口为`5173`;在浏览器中打开`localhost:5173`就会看我们的“启动测试”页面。 
## 本地调试
### 新建包文件
本节可能和目前组件的开发关联不大,但是未来组件需要引入一些工具方法的时候会用到 接下来就是要往我们的`packages`文件夹冲填充内容了。
### utils包
一般`packages`要有`utils`包来存放我们公共方法,工具函数等。 既然它是一个包,所以我们新建`utils`目录后就需要初始化它,让它变成一个包;终端进入`utils`文件夹执行:`pnpm init`然后会生成一个`package.json`文件;这里需要改一下包名,我这里将`name`改成`@itchenliang-ui/utils`表示这个`utils`包是属于`itchenliang-ui`这个组织下的。所以记住发布之前要登录`npm`新建一个组织;例如`itchenliang-ui`。
```
// packages/utils/package.json
{
"name": "@itchenliang-ui/utils", // 修改name名称
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
```
因为我们使用`ts`写的,所以需要将入口文件`index.js`改为`index.ts`:
```
// packages/utils/package.json
{
...
"main": "index.ts", // 修改
...
}
```
在`packages/utils`下新建`index.ts`文件(先导出一个简单的加法函数):
```
export const testfun = (a:number,b:number):number=>{
return a + b
}
```
### components包
`components`是我们用来存放各种UI组件的包。 在`packages`目录下新建`components`目录并执行`pnpm init`生成`package.json`文件:
```
// packages/components/package.json
{
"name": "components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
```
将我们的组件库包命名为`itchenliang-ui`,所以修改`packages/components/package.json`,并且由于我们的组件都是使用`ts`开发,将入口文件`index.js`改为`index.ts`:
```
{
"name": "duli-ui",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
```
### 新建index.ts
在`packages/components`目录下新建`index.ts`入口文件并引入`utils`包:
```
import {testfun} from '@duli-ui/utils'
const result = testfun (1,1)
console.log(result)
```
可以看到上面的文件会报错:找不到模块“@itchenliang-ui/utils”或其相应的类型声明 在`packages/components`目录下安装`@duli-ui/utils`:
```
pnpm install @duli-ui/utils
```
你会发现`pnpm`会自动创建个软链接直接指向我们的`utils`包;此时`components`下的`packages`:
```
{
"name": "duli-ui",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@duli-ui/utils": "workspace:^1.0.0"
}
}
```
你会发现它的依赖`@duli-ui/utils`对应的版本为:`workspace:^1.0.0`;因为`pnpm`是由`workspace`管理的,所以有一个前缀`workspace`可以指向`utils`下的工作空间从而方便本地调试各个包直接的关联引用。 到这里基本开发方法我们已经知道啦;接下来就要进入正题了,开发一个`button`组件。
## 试着开发一个button组件
在`packages/components`文件夹下新建`src`目录,同时在`src`下新建`button`组件目录和`icon`组件目录;此时`components`文件目录如下
```
-- components
-- src
-- button
-- icon
-- index.ts
-- package.json
```
让我们先测试一下我们的`button`组件能否在我们搭建的`examples`下的vue3项目本引用~
### 新建button.vue
在`components/src/button`目录下新建`button.vue`文件:
```
```
由于我们`compoennts`的入口文件是`index.ts`,之后所有的`import * from 'duli-ui'`操作都是从`index.ts`中查找,所以需要在`components/index.ts`**(注意:是components/index.ts)**将其导出
```
...
import Button from './src/button/button.vue'
export default Button
```
因为我们开发组件库的时候不可能只有button,所以我们需要一个`components/index.ts`将我们开发的组件一个个的集中导出
```
import Button from './src/button/button.vue'
// export default Button
export {
Button
}
```
好了,一个组件的大体目录差不多就是这样了,接下来请进入我们的`examples`来看看能否引入我们的`button`组件。
## vue3项目中使用button
上面已经说过执行在`workspace`执行`pnpm i xxx`的时候`pnpm`会自动创建个软链接直接指向我们的`xxx`包。 所以这里我们直接在`examples`执行:`pnpm i itchenliang-ui` 此时你就会发现`exmaples/packages.json`的依赖多了个:
```
"duli-ui": "workspace:^1.0.0"
```
这时候我们就能直接在我们的测试项目下引入我们本地的`components`组件库了,启动我们的测试项目,来到我们的`examples/app.vue`直接引入`Button`:
```
```
再次在`examples`目录下执行:
```
pnpm run dev
```
不出意外的话你的页面就会展示我们刚刚写的`button`组件了。 好了万事具...(其实还差个打包,这个后面再说~);接下来的工作就是专注于组件的开发了;让我们回到我们的button组件目录下(测试页面不用关,此时我们已经可以边开发边调试边看效果了)
### 深入开发button组件
因为我们的`button`组件是需要接收很多属性的,如`type`、`size`等等,所以我们要新建个`types.ts`文件来规范这些属性 在`components/src/button`目录下新建`types.ts`文件:
```
import type { ExtractPropTypes } from 'vue'
export const ButtonType = ['default', 'primary', 'success', 'warning', 'danger']
export const ButtonSize = ['large', 'normal', 'medium', 'small', 'mini'];
export const buttonProps = {
type: {
type: String,
values: ButtonType
},
size: {
type: String,
values: ButtonSize
}
}
export type ButtonProps = ExtractPropTypes
```
**注意**: `import type`表示只导入类型;`ExtractPropTypes`是vue3中内置的类型声明,它的作用是接收一个类型,然后把对应的vue3所接收的`props`类型提供出来,后面有需要可以直接使用; 很多时候我们在vue中使用一个组件会用的`app.use`将组件挂载到全局。要使用`app.use`函数的话我们需要让我们的每个组件都提供一个`install`方法,`app.use()`的时候就会调用这个方法;
### 新建index.ts
在`components/src/button`目录下新建`index.ts`文件:
```
import button from './button.vue'
import type {App,Plugin} from "vue"
type SFCWithInstall = T&Plugin
const withInstall = (comp:T) => {
(comp as SFCWithInstall).install = (app:App)=>{
//注册组件
app.component((comp as any).name,comp)
}
return comp as SFCWithInstall
}
const Button = withInstall(button)
export default Button
```
然后修改`compoennts/index.ts`文件:
```
...
import Button from './src/button'
export {
Button as DlButton
}
```
### 按钮样式
在`button`目录下新建`style/index.less`文件:
```less
.dl-button {
cursor: pointer;
display: inline-block;
white-space: nowrap;
background: #fff;
text-align: center;
box-sizing: border-box;
outline: 0;
font-size: 14px;
user-select: none;
color: #606266;
margin: 0;
border-radius: 4px;
border: 1px solid #dcdfe6;
padding: 8px 15px;
&:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
}
.dl-button--primary {
color: #fff;
background-color: #409eff;
border-color: #409eff;
&:hover {
background: #66b1ff;
border-color: #66b1ff;
color: #fff;
}
}
.dl-button--success {
color: #fff;
background-color: #67c23a;
border-color: #67c23a;
&:hover {
background: #85ce61;
border-color: #85ce61;
color: #fff;
}
}
.dl-button--info {
color: #fff;
background-color: #909399;
border-color: #909399;
&:hover {
background: #a6a9ad;
border-color: #a6a9ad;
color: #fff;
}
}
.dl-button--warning {
color: #fff;
background-color: #e6a23c;
border-color: #e6a23c;
&:hover {
background: #ebb563;
border-color: #ebb563;
color: #fff;
}
}
.dl-button--danger {
color: #fff;
background-color: #f56c6c;
border-color: #f56c6c;
&:hover {
background: #f78989;
border-color: #f78989;
color: #fff;
}
}
// size
.dl-button--large {
padding: 13px 24px;
font-size: 14px;
border-radius: 4px;
}
.dl-button--medium {
padding: 10px 20px;
font-size: 14px;
border-radius: 4px;
}
.dl-button--small {
padding: 9px 15px;
font-size: 12px;
border-radius: 3px;
}
.dl-button--mini {
padding: 7px 15px;
font-size: 12px;
border-radius: 3px;
}
```
修改`button.vue`文件:
```
```
修改`exmaple/app.vue`文件:
```vue
基础用法
默认按钮
主要按钮
成功按钮
信息按钮
警告按钮
危险按钮
不同尺寸
默认按钮
超大按钮
中等按钮
小型按钮
超小按钮
```
## app.use使用button组件
此时我们就可以使用`app.use`来挂载我们的组件啦,其实`withInstall`方法可以做个公共方法放到工具库里,因为后续每个组件都会用到,这里等后面开发组件的时候再调。 修改`examples/main.ts`文件:
```
import {createApp} from 'vue'
import App from './app.vue'
import { DlButton } from 'duli-ui'
const app = createApp(App)
app.use(DlButton)
app.mount('#app')
```
并且将`app.vue`中的`import { DlButton } from 'duli-ui'`删除。 **缺点:此处并不可以,后续查询解决方案。**
到这里组件开发的基本配置已经完成,最后我们对我们的组件库以及工具库进行打包,打包之前如果要发公共包的话记得将我们的各个包的协议改为MIT开源协议
```
// components/package.json
...
"license": "MIT",
...
```
[https://juejin.cn/post/7117886038126624805#heading-21](https://gitee.com/link?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7117886038126624805%23heading-21)