Vite+Typescript打造一个vue3组件库
文章目录
前言
以前也搭建过组件库用的技术比较老,现重新搭建一个vue3组件库用最新的技术栈pnpm+vite+typescript+vue3
nodejs版本如下截图:
读完这篇文章你将学会:
如何使用pnpm搭建出一个Monorepo环境
如何使用vite搭建一个基本的Vue3脚手架项目
如何开发调试一个自己的UI组件库
如何使用vite打包并发布自己的UI组件库
一、搭建 Monorepo 环境
什么是 Monorepo 环境
就是指在一个大的项目仓库中,管理多个模块/包(package),这种类型的项目大都在项目根目录下有一个packages文件夹,分多个项目管理。大概结构如下:
-- packages
-- pkg1
--package.json
-- pkg2
--package.json
--package.json
💡 简单来说就是单仓库 多项目,目前 Vant,ElementUI,Vue3 等项目都是采用这种模式。打造一个Monorepo环境的工具有很多,如:lerna、pnpm、yarn等,这里我们将使用 pnpm来开发我们的UI组件库
使用pnpm
安装
npm install pnpm -g
初始化 package.json
pnpm init
新建配置文件 .npmrc
shamefully-hoist = true
💡 这里简单说下为什么要配置shamefully-hoist。如果某些工具仅在根目录的node_modules时才有效,可以将其设置为true来提升那些不在根目录的node_modules,就是将你安装的依赖包的依赖包的依赖包的…都放到同一级别(扁平化)。说白了就是不设置为true有些包就有可能会出问题。
monorepo 的实现
在 「根目录」 下新建 pnpm-workspace.yaml
文件、packages
文件夹、examples
文件夹,pnpm-workspace.yaml
文件内容如下:
packages:
- 'packages/**'
- 'examples'
如果想关联更多目录你只需要往里面添加即可,packages
文件夹存放开发的包,examples
用来调试组件
安装对应依赖
开发环境中的依赖一般全部安装在整个项目根目录下,方便每个包都可以引用,所以在安装的时候需要加个 -w
pnpm i vue@next typescript less -D -w
如果出现以下报错信息:
Progress: resolved 1, reused 0, downloaded 0, added 0
ERR_PNPM_NO_MATCHING_VERSION No matching version found for vue@next
This error happened while installing a direct dependency of D:\project\demopnpm
The latest release of vue is "3.4.14".
Other releases are:
* alpha: 3.4.0-alpha.4
* beta: 3.4.0-beta.4
* csp: 1.0.28-csp
* legacy: 2.7.16
* rc: 3.4.0-rc.3
* v2-latest: 2.7.16
If you need the full list of all 484 published versions run "$ pnpm view vue versions".
✅ 解决方法是指定vue3版本,即使用
pnpm i vue@3.4.14 typescript less -D -w
如果安装了 TypeScript 那么需要在 「根目录」 下新建一个 tsconfig.json
文件,内容如下:
{
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve", // jsx 不转
"strict": true,
"target": "ES2015", // 遵循es5版本
"module": "ESNext", // 打包模块类型ESNext
"skipLibCheck": true, // 跳过类库检测
"esModuleInterop": true, // 支持es6,commonjs模块
"moduleResolution": "Node", // 按照node模块来解析
"lib": ["esnext", "dom"] // 编译时用的库
}
}
搭建一个基于 vite 的 vue3 项目
💡其实搭建一个vite+vue3项目是非常容易的,因为vite已经帮我们做了大部分事情😄
初始化仓库
进入 examples
文件夹:
cd examples
初始化配置:
pnpm init
安装 vite 和 @vitejs/plugin-vue
pnpm install vite @vitejs/plugin-vue -D -w
@vitejs/plugin-vue 用来支持
.vue
文件的转译,这里安装的插件都放在 「根目录」 下
也就是根目录下的 package.json
文件是这样的:
{
......
"devDependencies": {
......
"@vitejs/plugin-vue": "^5.0.3",
"vite": "^5.0.11",
}
}
配置 vite.config.ts
在 examples
文件夹下新建 vite.config.ts
文件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins:[vue()]
})
新建 html 文件
在 examples
文件夹下新建 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>
⚠️ @vitejs/plugin-vue 会默认加载
examples
下的index.html
,vite 是基于esmodule的,所以要在 script 标签中加上type="module"
新建 app.vue 模板
在 examples
文件夹下新建 app.vue
<template>
<div>启动测试</div>
</template>
新建 main.ts
在 examples
文件夹下新建 main.ts
import {createApp} from 'vue'
import App from './app.vue' // 找不到模块“./app.vue”或其相应的类型声明
const app = createApp(App)
app.mount('#app')
如下截图:
⚠️ 因为直接引入
.vue
文件 TS 会找不到对应的类型声明,所以需要新建 typings(命名没有明确规定,TS 会自动寻找.d.ts
文件)文件夹来专门放这些声明文件
解决办法:
在 「根目录」 下新建 typings/vue-shim.d.ts
文件
💡首先新建
typings
文件夹,然后再新建vue-shim.d.ts
文件
TypeScript 默认只认 ES 模块,如果要导入 .vue
文件就要 declare module 把他们声明出来,在 vue-shim.d.ts
文件中写入如下内容:
declare module '*.vue' {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>
}
完成之后 import App from './app.vue' // 找不到模块“./app.vue”或其相应的类型声明
就不会报错了
配置脚本启动项目
最后在examples
下的 package.json
文件中配置 scripts 脚本
{
......
"scripts": {
"dev": "vite"
},
}
然后进入到examples
文件夹在终端输入熟悉的命令:
pnpm run dev
运行结果如下:
VITE v5.0.11 ready in 249 ms
The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
完成后在浏览器中打开链接🔗 http://localhost:xxx/ 就会看到启动测试页面
如果想解决这个提示
The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
解决方法是在examples
目录下package.json文件添加"type": “module”,具体可以参考vite官网
本地调试
新建包文件
‼️ 下面新建的文件都在
packages
文件夹下面
utils 包
一般 packages
要有 utils
包来存放我们公共方法,工具函数等
既然它是一个包,所以我们新建 utils
目录后就需要初始化它:
- 终端进入
utils
文件夹执行pnpm init
然后会生成一个package.json
文件; - 这里需要改一下包名,这里将 name 改成 @wzw/utils 表示这个
utils
包是属于 wzw这个组织下的,所以记住发布之前要登录 npm 新建一个组织,例如 wzw
{
"name": "@wzw/utils",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
因为使用 ts 写的,所以需要将入口文件 index.js
改为 index.ts
,并新建 index.ts
文件:
export const testfun = (a: number, b: number): number => {
return a + b
}
wzw-ui 组件库包
components
文件夹是用来存放各种 UI 组件的包新建 components
文件夹并执行 pnpm init
生成 package.json
{
"name": "wzw-ui",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
新建 index.ts 入口文件并引入 utils 包
import {testfun} from '@wzw/utils'
const result = testfun (1,1)
console.log(result)
⚠️注意:这里的文件还不能运行
esno
由于组件库是基于 ts的,所以需要安装 esno 来执行 ts 文件便于测试组件之间的引入情况
控制台输入 esno xxx.ts
即可执行 ts 文件
npm i esno -g
包之间本地调试
进入 components 文件夹执行
pnpm install @wzw/utils
会发现 pnpm 会自动创建个软链接直接指向我们的 utils 包;此时 components 下的 packages.json 为:
{
"name": "wzw-ui",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@wzw/utils": "workspace:^"
}
}
💡 会发现它的依赖
@wzw/utils
对应的版本为:workspace:^
;因为 pnpm 是由 workspace 管理的,所以有一个前缀 workspace 可以指向utils
下的工作空间从而方便本地调试各个包直接的关联引用
试着开发一个 button 组件
在 components
文件夹下新建 src
,同时在 src
下新建 button
组件目录和 icon
组件目录;此时components文件目录如下
-- components
-- src
-- button
-- button.vue
-- index.less
-- index.ts
-- icon
-- icon.vue
-- index.less
-- index.ts
-- index.ts
-- package.json
在 button
目录下新建一个简单的 button.vue
,然后写入:
<template>
<button>测试按钮</button>
</template>
然后在 button/index.ts 将其导出:
import Button from './button.vue'
export default Button
因为开发组件库的时候不可能只有 button,所以需要一个 components/src/index.ts 将开发的组件集中导出
import Button from './button'
export {
Button
}
因为组件都在src文件夹下,所以package.json
需要改下"main": "src/index.ts"
,要不然引入组件时找不到组件。
修改后:
{
...
"main": "src/index.ts",
...
}
vue3 项目使用 button 组件
直接在 examples
执行 pnpm i wzw-ui
,此时就会发现 packages.json
中的依赖多了个 "wzw-ui": "workspace:^"
这时候就能直接在测试项目 examples
下引入本地的 components
组件库了,在 examples/app.vue
直接引入 Button
<template>
<div>
<Button />
</div>
</template>
<script lang="ts" setup>
import { Button } from 'wzw-ui'
</script>
然后运行 npm run dev
即可
vite打包
配置文件
打包这里选择 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'
}
},
plugins: [
vue()
]
}
)
这里我们选择打包cjs(CommonJS)和esm(ESModule)两种形式,cjs模式主要用于服务端引用(ssr),而esm就是我们现在经常使用的方式,它本身自带treeShaking而不需要额外配置按需引入(前提是你将模块分别导出),非常好用~
其实到这里就已经可以直接打包了,components 下执行 pnpm run build 就会发现打包了 es 和 lib 两个目录
⚠️记得在
components/package.json
中加入如下指令:
{
......
"scripts": {
"build": "vite build"
},
}
到这里其实打包的组件库只能给 js 项目使用,在 ts 项目下运行会出现一些错误,而且使用的时候还会失去代码提示功能,这样的话就失去了用 ts 开发组件库的意义了,所以需要在打包的库里加入声明文件(.d.ts),只需要引入vite-plugin-dts,然后修改一下的 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({
tsconfigPath: '../../tsconfig.json'
}),
dts({
// 指定使用的 tsconfig.json,如果不配置也可以在 components 下新建 tsconfig.json
tsconfigPath: '../../tsconfig.json',
// 因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个
outDir: 'lib',
}),
]
}
)
安装:vite-plugin-dts
pnpm i vite-plugin-dts -D -w
然后执行打包命令 npm run build
就会发现 es
和 lib
下就有了 *.t.ts
声明文件
其实后面就可以进行发布了,发布之前更改一下 components
下的 package.json
如下:
{
"name": "wzw-ui",
"version": "1.0.0",
"description": "",
"main": "lib/index.js",
"module": "es/index.js",
"files": [
"es",
"lib"
],
"scripts": {
"build": "vite build"
},
"keywords": [
"wzw-ui",
"vue3组件库"
],
"author": "wzw",
"license": "MIT",
"typings": "es/packages/components/src/index.d.ts"
}
- pkg.module:组件库默认入口文件是传统的 CommonJS 模块,但是如果环境支持 ESModule 的话构建工具会优先使用module 入口
- pkg.files:files 是指需要发布到 npm 上的目录,因为不可能 components 下的所有目录都被发布上去
样式问题
引入打包后的组件会发现没有样式,所以需要在全局引入 style.css
才行,那么需要的组件库是每个 css 样式放在每个组件其对应目录下,这样就不需要每次都全量导入 css 样式,下面就来看下如何把样式拆分打包
处理less文件
首先需要做的是将 less 打包成 css 然后放到打包后对应的文件目录下,在components
下新建 build
文件夹来存放一些打包工具,然后新建 buildLess.ts
,首先需要先安装一些工具 cpy 和 fast-glob
pnpm i cpy fast-glob -D -w
cpy
cpy 可以直接复制规定的文件并将文件复制到指定目录,比如 buildLess.ts:
import cpy from 'cpy'
import { resolve } from 'path'
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
// 获取 __dirname 的 ESM 写法
const __dirname = dirname(fileURLToPath(import.meta.url))
const sourceDir = resolve(__dirname, '../src')
//lib文件
const targetLib = resolve(__dirname, '../lib')
//es文件
const targetEs = resolve(__dirname, '../es')
const buildLess = async () => {
await cpy(`${sourceDir}/**/*.less`, targetLib)
await cpy(`${sourceDir}/**/*.less`, targetEs)
}
buildLess()
这里buildLess.ts
文件报找不到对应的类型声明:
Cannot find module 'path' or its corresponding type declarations
解决nodejs类型声明找不到:
pnpm i -D @types/node -w
根目录下tsconfig.json配置
{
"compilerOptions": {
...
"types": [
"node"
]
},
}
然后在 components/package.json 中新增命令
{
......
"scripts": {
"build": "vite build",
"build:less": "esno build/buildLess"
},
}
终端执行 pnpm run build:less
就会发现 lib
和 es
文件对应目录下就出现了 less
文件
但是最终要的并不是 less 文件而是 css 文件,所以要将 less 打包成 css,所以需要用的 less 模块,在 ts 中引入less 因为它本身没有声明文件所以会出现类型错误,所以要先安装它的 @types/less
pnpm i --save-dev @types/less -D -w
buildLess.ts
如下(详细注释都在代码中)
import cpy from 'cpy'
import { resolve } from 'path'
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
import { promises as fs } from "fs"
import less from "less"
import glob from "fast-glob"
// 获取 __dirname 的 ESM 写法
const __dirname = dirname(fileURLToPath(import.meta.url))
const srcDir = resolve(__dirname, '../src')
//lib文件
const targetLib = resolve(__dirname, '../lib')
//es文件
const targetEs = resolve(__dirname, '../es')
const buildLess = async () => {
//直接将less文件复制到打包后目录
await cpy(`${sourceDir}/**/*.less`, targetLib)
await cpy(`${sourceDir}/**/*.less`, targetEs)
//获取打包后.less文件目录(lib和es一样)
const lessFils = await glob("**/*.less", { cwd: srcDir, onlyFiles: true })
//遍历含有less的目录
for (let path in lessFils) {
const filePath = `${srcDir}/${lessFils[path]}`
//获取less文件字符串
const lessCode = await fs.readFile(filePath, 'utf-8')
//将less解析成css
const code = await less.render(lessCode, {
//指定src下对应less文件的文件夹为目录
paths: [srcDir, dirname(filePath)]
})
//拿到.css后缀path
const cssPath = lessFils[path].replace('.less', '.css')
//将css写入对应目录
await fs.writeFile(resolve(targetLib, cssPath), code.css)
await fs.writeFile(resolve(targetEs, cssPath), code.css)
}
}
buildLess()
执行打包命令之后会发现对应文件夹下多了 .css
文件,这个命令是单独打包处理 less 文件的,所以在 pnpm run build
后还要运行 pnpm run build:less
才行,那么可以更改 components/package.json
为如下:
{
......
"scripts": {
"build": "vite build && npm run build:less",
"build:less": "esno build/buildLess"
},
}
这样 pnpm run build 后就可以完成 less 文件的处理了😄
引入 css 文件
现在已经将 css 文件放入对应的目录下了,但是相关组件并没有引入这个 css 文件,所以需要的是每个打包后组件的 index.js
中出现如:
import "xxx/xxx.css"
之类的代码,那么 css 才会生效,所以需要对 vite.config.ts
进行相关配置
首先先将 .less
文件忽略
external: ['vue', /\.less/]
这时候打包后的文件中如 button/index.js
就会出现:
import "./index.less";
然后再将打包后代码的 .less
换成 .css
就大功告成了🎉
......
plugins: [
......
{
name: 'style',
generateBundle(config, bundle) {
//这里可以获取打包后的文件目录以及代码code
const keys = Object.keys(bundle)
for (const key of keys) {
const bundler: any = bundle[key as any]
//rollup内置方法,将所有输出文件code中的.less换成.css,因为我们当时没有打包less文件
this.emitFile({
type: 'asset',
fileName: key,//文件名名不变
source: bundler.code.replace(/\.less/g, '.css')
})
}
}
}
]
⚠️ 我们要在
/components/src/button/index.ts
中引入 less 文件,即import './index.less'
总结
以上内容参考CSDN “Karl _”, 如果大家喜欢可以点赞➕收藏 🌟
附上源码地址:
https://gitee.com/wuzhongwei.com/vue3-vite-typescript.git