前言
首先我们要了解什么是menorepo及它是如何搭建的吧
就是指在一个大的项目仓库中,管理多个模块/包(package),这种类型的项目大都在项目根目录下有一个packages文件夹,分多个项目管理。大概结构如下:
-- packages
-- pkg1
--package.json
-- pkg2
--package.json
--package.json
简单来说就是单仓库 多项目
目前很多我们熟知的项目都是采用这种模式,如Vant,ElementUI,Vue3等。打造一个menorepo环境的工具有很多,如:lerna、pnpm、yarn等,这里我们将使用pnpm来开发我们的UI组件库。
为什么要使用pnpm?
因为它简单高效,它没有太多杂乱的配置,它相比于lerna操作起来方便太多
好了,下面我们就开始用pnpm来进行我们的组件库搭建吧
创建项目
- 新建文件夹 xy-modules
- 进入项目后,使用pnpm
npm install pnpm -g
初始化项目
pnpm init
在xy-modules下新建packages、examples文件夹,packages放我们的自定义组件,examples下是我们的测试项目; packages下新建utils、components文件夹,分别在utils、components下执行pnpm init, 文件结构如下:
xy-modules
.
├── README.md
├── packages
│ ├── utils
│ │ ├── package.json
│ └── components
│ ├── package.json
├── examples
└── package.json
初始化utils、modules两个工程,package.json 中的 name 字段分别叫做 @xy/utils 和 @xy/components(PS:@xy是提前在npm上创建好的组织,没有的话需要提前创建)。
// packages/utils/packages.json
{
"name": "@xy/utils",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
// packages/components/packages.json
{
"name": "@xy/components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
安装对应依赖
我们开发环境中的依赖一般全部安装在整个项目根目录下,方便下面我们每个包都可以引用,所以在安装的时候需要加个 -w
在packages/utils 执行
pnpm i vue@next typescript less -D -w
会发现在项目根目录package.json中,增加了全局依赖
{
"name": "xy-modules",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"less": "^4.1.3",
"typescript": "^5.0.2",
"vue": "^3.2.36"
}
}
monorepo的实现
接下就是pnpm如何实现monorepo的了。
新建配置文件.npmrc
shamefully-hoist = true
这里简单说下为什么要配置shamefully-hoist。
如果某些工具仅在根目录的node_modules时才有效,可以将其设置为true来提升那些不在根目录的node_modules,就是将你安装的依赖包的依赖包的依赖包的...都放到同一级别(扁平化)。说白了就是不设置为true有些包就有可能会出问题。
新建配置文件 pnpm-workspace.yaml
为了我们各个项目之间能够互相引用我们要新建一个pnpm-workspace.yaml文件将我们的包关联起来
packages:
- 'packages/**'
- 'examples'
这样就能将我们项目下的packages目录和examples目录关联起来了,当然如果你想关联更多目录你只需要往里面添加即可。根据上面的目录结构很显然你在根目录下新packages和examples文件夹,packages文件夹存放我们开发的包,examples用来调试我们的组件
接下来我们开始创建我们的自定义组件库啦***~~~~~~~
packages:新建自定义组件库
utils包
因为我们使用ts写的,所以需要将入口文件index.js改为index.ts,并新建index.ts文件:(先导出一个简单的加法函数)
// utils/index.ts
export const testfun = (a:number,b:number):number=>{
return a + b
}
组件库包
components是我们用来存放各种UI组件的包
新建index.ts入口文件并引入utils包
// components/index.ts
import {testfun} from '@xy/utils'
const result = testfun (1,1)
console.log(result)
包之间本地调试
由于组件库是基于ts的,所以需要安装esno来执行ts文件便于测试组件之间的引入情况 控制台输入esno xxx.ts即可执行ts文件
npm i esno -g
进入components文件夹执行
pnpm install @xy/utils
你会发现pnpm会自动创建个软链接直接指向我们的utils包;此时components下的packages:
{
"name": "@xy/components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@xy/utils": "workspace:^1.0.0"
}
}
你会发现它的依赖@xy/utils对应的版本为:workspace:^1.0.0;因为pnpm是由workspace管理的,所以有一个前缀workspace可以指向utils下的工作空间从而方便本地调试各个包直接的关联引用。
在components下执行
esno index.ts
控制台输出结果: 2
到这里基本开发方法我们已经知道啦;接下来就要进入正题了,开发一个button组件
试着开发一个button组件
在components文件夹下新建src,同时在src下新建button组件目录和icon组件目录(新建icon为了便于调试);此时components文件目录如下
-- components
-- src
-- button
-- icon
-- index.ts
-- package.json
首先在button下新建一个简单的button.vue
<template>
<button>测试按钮</button>
</template>
然后新建button/index.ts, 在button/index.ts中将其导出
import Button from './button.vue'
export default Button
因为我们开发组件库的时候不可能只有button,src/index.ts将我们开发的组件一个个的集中导出
import Button from './button'
export {
Button
}
此时components文件结果为:
-- components
-- src
-- button
-- button.vue
-- index.ts
-- icon
-- index.ts
-- package.json
好了,一个组件的大体目录差不多就是这样了,接下来请进入我们的examples来看看能否引入我们的button组件
examples:搭建基于vite的vue3项目
其实搭建一个vite+vue3项目是非常容易的,因为vite已经帮我们做了大部分事情
初始化仓库
进入examples文件夹,执行
pnpm init
安装vite和@vitejs/plugin-vue
@vitejs/plugin-vue用来支持.vue文件的转译
pnpm install vite @vitejs/plugin-vue -D -w
这里安装的插件都放在根目录下
配置vite.config.ts
新建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
新建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="main.ts" type="module"></script>
</body>
</html>
注意:
vite 是基于esmodule的 所以type="module"
新建app.vue模板
<template>
<div>
启动测试
</div>
</template>
新建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文件)文件夹来专门放这些声明文件。
typings/vue-shim.d.ts
TypeScriptTS默认只认ES 模块。如果你要导入.vue文件就要declare module把他们声明出来。
declare module '*.vue' {
import type { DefineComponent } from "vue";
const component:DefineComponent<{},{},any>
}
配置脚本启动项目
最后在package.json文件中配置scripts脚本
...
"scripts": {
"dev": "vite"
},
...
然后终端输入我们熟悉的命令:pnpm run dev
vite启动默认端口为3000;在浏览器中打开localhost:3000 就会看我们的“启动测试”页面。
vue3项目使用button
上面已经说过执行在workspace执行 pnpm i xxx的时候pnpm会自动创建个软链接直接指向我们的xxx包。
所以这里我们直接在examples执行:pnpm i @xy/components
此时你就会发现packages.json的依赖多了个
"dependencies": {
"@xy/components": "workspace:^1.0.0"
}
这时候我们就能直接在我们的测试项目下引入我们本地的components组件库了,启动我们的测试项目,来到我们的 examples/app.vue 直接引入Button
<template>
<div>
<Button />
</div>
</template>
<script lang="ts" setup>
import { Button } from "@xy/components";
</script>
不出意外的话你的页面就会展示我们刚刚写的button组件了
接下来的工作就是专注于组件的开发了;让我们回到我们的button组件目录下(测试页面不用关,此时我们已经可以边开发边调试边看效果了)
因为我们的button组件是需要接收很多属性的,color、size等等,我们对button组件进行修改,增加props
<template>
<button :style="'color: ' + color + ';'">xy测试按钮</button>
</template>
<script setup>
const props = defineProps({
color: {
type: String,
default() {
return 'blue';
},
},
});
</script>
在examples/src/App.vue中,button增加color属性,就能看到按钮有颜色区分了
<XyButton type="success" color="red"/>
TIPS
import type 表示只导入类型;ExtractPropTypes是vue3中内置的类型声明,它的作用是接收一个类型,然后把对应的vue3所接收的props类型提供出来,后面有需要可以直接使用
很多时候我们在vue中使用一个组件会用的app.use 将组件挂载到全局。要使用app.use函数的话我们需要让我们的每个组件都提供一个install方法,app.use()的时候就会调用这个方法;
我们将button/index.ts调整为
import button from './button.vue'
import type {App,Plugin} from "vue"
type SFCWithInstall<T> = T&Plugin
const withInstall = <T>(comp:T) => {
(comp as SFCWithInstall<T>).install = (app:App)=>{
//注册组件
app.component((comp as any).name,comp)
}
return comp as SFCWithInstall<T>
}
const Button = withInstall(button)
export default Button
此时我们就可以使用app.use来挂载我们的组件啦
其实withInstall方法可以做个公共方法放到工具库里,因为后续每个组件都会用到,这里等后面开发组件的时候再调整
到这里组件开发的基本配置已经完成,最后我们对我们的组件库以及工具库进行打包,打包之前如果要发公共包的话记得将我们的各个包的协议改为MIT开源协议
"license": "MIT",
这个 license 就是开源许可说明,各大组织设立了为代码开源许可的规范文档,当作者声明此文档类型时,他人必须遵守该文档类型的规范。(当然啦这是君子条款)
打包
配置文件
打包们这里选择vite,它有一个库模式专门为我们来打包这种库组件的。
前面已经安装过vite了,所以这里直接在components下直接新建vite.config.ts(配置参数文件中已经注释):
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"
export default defineConfig(
{
build: {
target: 'modules',
//打包文件目录
outDir: "es",
//压缩
minify: false,
//css分离
//cssCodeSplit: true,
rollupOptions: {
//忽略打包vue文件
external: ['vue'],
input: ['src/index.ts'],
output: [
{
format: 'es',
//不用打包成.es.js,这里我们想把它打包成.js
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'es',
preserveModulesRoot: 'src'
},
{
format: 'cjs',
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'lib',
preserveModulesRoot: 'src'
}
]
},
lib: {
entry: './index.ts',
formats: ['es', 'cjs']
}
},
plugins: [
vue()
]
}
)
这里我们选择打包cjs(CommonJS)和esm(ESModule)两种形式,cjs模式主要用于服务端引用(ssr),而esm就是我们现在经常使用的方式,它本身自带treeShaking而不需要额外配置按需引入(前提是你将模块分别导出),非常好用~
components/packages.json添加build脚本
"scripts": {
"build": "vite build"
},
其实到这里就已经可以直接打包了;components下执行: pnpm run build你就会发现打包了es和lib两个目录
到这里其实打包的组件库只能给js项目使用,在ts项目下运行会出现一些错误,而且使用的时候还会失去代码提示功能,这样的话我们就失去了用ts开发组件库的意义了。所以我们需要在打包的库里加入声明文件(.d.ts)。
那么如何向打包后的库里加入声明文件呢? 其实很简单,只需要引入vite-plugin-dts
pnpm i vite-plugin-dts -D -w
然后修改一下我们的vite.config.ts引入这个插件
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"
import dts from 'vite-plugin-dts'
export default defineConfig(
{
build: {...},
plugins: [
vue(),
dts({
//指定使用的tsconfig.json为我们整个项目根目录下掉,如果不配置,你也可以在components下新建tsconfig.json
tsConfigFilePath: '../../tsconfig.json'
}),
//因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个
dts({
outputDir:'lib',
tsConfigFilePath: '../../tsconfig.json'
})
]
}
)
因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个dts插件,暂时没有想到其它更好的处理方法~
然后执行打包命令你就会发现你的es和lib下就有了声明文件
其实后面就可以进行发布了,发布之前更改一下我们components下的package.json如下:
{
"name": "xy",
"version": "1.0.0",
"description": "",
"main": "lib/index.js",
"module": "es/index.js",
"scripts": {
"build": "vite build"
},
"keywords": [
"xy",
"vue3组件库"
],
"author": "",
"license": "MIT",
"dependencies": {
"@xy/utils": "workspace:^1.0.0"
},
"typings": "lib/index.d.ts"
}
解释一下里面部分字段
pkg.module
我们组件库默认入口文件是传统的CommonJS模块,但是如果你的环境支持ESModule的话,构建工具会优先使用我们的module入口
pkg.files
files是指我们1需要发布到npm上的目录,因为不可能components下的所有目录都被发布上去
开始发布
做了那么多终于到发布的阶段了;其实npm发包是很容易的,就拿我们的组件库xy举例吧
发布之前记得到npm官网注册个账户,如果你要发布@xx/xx这种包的话需要在npm新建个组织组织名就是@后面的,比如我建的组织就是xy,注册完之后你就可以发布了
首先要将我们代码提交到git仓库,不然pnpm发布无法通过,后面每次发版记得在对应包下执行 pnpm version patch你就会发现这个包的版本号patch(版本号第三个数) +1 了,同样的 pnpm version major major和 pnpm version minor 分别对应版本号的第一和第二位增加。
登录npm账号
npm adduser
如果你发布的是公共包的话,在对应包下执行
pnpm publish --access public
发布成功!!
之后就可以在项目中使用 npm i xy-modules(@xy 重名啦,改为xy-modules了) 来使用自己的库啦!
注意
发布的时候要将npm的源切换到npm的官方地址(registry.npmjs.org/); 如果你使用了其它镜像源的话