Vite+TypeScript 打造一个 Vue3 组件库

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 0ERR_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 就会发现 eslib 下就有了 *.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 就会发现 libes 文件对应目录下就出现了 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

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值