15分钟学会 pnpm monorepo+vue3+vite 搭建组件库并发布到私有仓库(人人都能学会)

8,311 阅读11分钟

pnpm 是什么

本文正在参加「金石计划」

pnpm 是 performant npm(高性能的 npm),它是一款快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspace 和 monorepo,简化开发者在多包组件开发下的复杂度和开发流程。

pnpm 为 performant npm 的简称,意为高性能的 npm

pnpm 主要有以下优点:

  • 快速: pnpm 比其他包管理工具快两倍;
  • 高效: node_modules 中的文件链接自特定的内容寻址存储库;
  • 支持 monorepo: pnpm 内置了对存储库中的多个包的支持;
  • 严格: pnpm 默认创建一个非平铺的 node_modules,因此代码不能访问任意包;

pnpm monorepo 搭建

安装 pnpm

npm install -g pnpm

新建文件夹作为工作区 ,例如我这里新建文件夹 monorepo-demo

cd 到目录下

初始化环境

  • 初始化
pnpm init 

文件夹下生成了 package.json

pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持,你只需要创建一个 workspace 就可将多个项目合并到一个仓库中,这样的作用是能在我们开发调试多包时,彼此间的依赖引用更加简单。

我们在根目录下新建 packages 文件夹,再新建 pnpm-workspace.yaml 文件,用来声明对应的工作区,写入如下内容:

packages:
  # 存放组件库和其他工具库
  - 'packages/*'
  # 存放组件测试的代码
  - 'example'

这里我们打算把我们的组件库 components 放于 packages 下,这样如果后续有需要我们还可以在packages文件夹下添加工具库 utils等,example 则为我们示例项目,用来测试我们开发的组件效果。

接下来我们创建组件库项目和示例项目

我们先利用vite创建一个vue3项目作为示例项目,在根目录下执行:pnpm create vite example ,为跟我们实际项目接近,我们暂时选择了安装这些

example1.png

接着在 packages 目录下,执行:pnpm create vite components 选择 Vue+JavaScript 两项

? Select a framework: » - Use arrow-keys. Return to submit.
? Select a framework: » - Use arrow-keys. Return to submit.
√ Select a framework: » Vue
√ Select a variant: » JavaScript

这里我们用 vite 创建了一个vue3项目,后续我们的组件库将在此基础上开发

编写一个组件

我们进入 components 目录下,并运行以下 pnpm i 安装项目依赖,然后启动项目

src文件夹下新建 button 文件夹和 index.js 文件(用于集中导出src下的所有组件),并新建以下文件,

components
···
├─ src
  ├─ button
     ├─ src
       └─ index.vue // 我们的组件代码
  └─ index.js // 用于导出button组件
└─ index.js // 集中导出src下的所有组件
···

基本结构如上,src 中编写组件内容,index.js 中插件形式导出组件

index.vue 编写我们的 button 组件代码如下

<template>
  <button class="button" :class="typeClass">
    <slot></slot>
  </button>
</template>

// 两个 script 的形式,这个用于定义 name 属性
<script>
export default {
  name: 'SButton',
}
</script>
<script setup>
import { computed } from 'vue'
const props = defineProps({
  type: {
    type: String,
    default: 'default'
  }
})
const typeClass = computed(() => `button-${props.type}`)
</script>

<style lang="scss" scoped>
.button {
  border-radius: 4px;
  padding: 8px 16px;
  font-size: 16px;
  cursor: pointer;

  &-default {
    background-color: #eee;
    color: #333;
  }

  &-primary {
    background-color: #007bff;
    color: #fff;
  }
}
</style>

上面我们为了定义组件的 name ,采用了两个 script 标签的形式,这样虽然可以,但是写两个 script 标签不够优雅,有时候也让开发人员费解,我们希望可以<script name="SButton" setup> 这样的形式,这里我们可以借助插件来实现

  • 安装 vite-plugin-vue-setup-extend -D

components 目录下

pnpm add vite-plugin-vue-setup-extend -D
  • vite.config.js 配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'

export default defineConfig({
  plugins: [vue(),VueSetupExtend()]
})
  • 使用

我们把 button 组件定义 name 的部分更改如下:

...

<!-- 注释定义name 的 script -->
<!-- <script>
export default {
  name: 'SButton',
}
</script> -->

<!-- 利用安装的插件,直接于script 标签上定义 name 属性 -->
<script name='SButton' setup>
import { computed } from 'vue'
const props = defineProps({
  type: {
    type: String,
    default: 'default'
  }
})
const typeClass = computed(() => `button-${props.type}`)
</script>
...

上面的组件样式部分用到了 scss ,所以我们需要进行安装,我们可以在 components 目录下执行 pnpm add sass -D 当然,除了进入子包目录 pnpm add pkgname 直接安装之外,还可以通过过滤参数 --filter-F 指定命令作用范围

例如,我们为 example 示例项目也安装 sass

# --filter 或者 -F <package_name> 可以在指定目录 package 执行任务
pnpm -F example add sass   # 在根目录中向 example 目录安装 sass

更多的过滤配置可参考:filtering

二次封装 el-input 组件

上面我们写了一个简单的 button 组件,但是实际开发中,我们更多的其实是基于现有组件库做二次封装,这里我们选择基于 element-ui 做二次封装。

如此前这篇文章当我们对组件二次封装时我们在封装什么提到的封装思路,我们想基于 el-input 实现这样一个需求:希望 el-input 默认可清空,即 clearable 默认为 ture

首先,我们给 components 项目安装 element-ui,这里安装不再赘述,大家可以直接按 官网

安装完毕后,我们在src下新建input文件夹,里面文件结构和 button 一致,基于 el-input 的 input 组件封装如下:

// input/index.vue
<template>
  <el-input v-bind="$attrs" :placeholder="placeholder" :clearable="clearable">
     <template #[slotName] v-for="(slot, slotName) in $slots" >
      <slot :name="slotName" />
    </template>
  </el-input>
</template>

<script name="SInput" setup>
defineProps({
  clearable: {
    type: Boolean,
    default: true
  },
  placeholder: {
    type: String,
    default: '请输入'
  }
})
</script>

<style lang="scss" scoped></style>

导出组件

组件写完之后,我们需要将其导出,因为我们的组件想要在打包后支持全量引入按需引入 考虑到后面我们的组件库肯定还有很多组件,所以我们写一个导出方法 components/src 下新建 utils/withInstall.js

withInstall.js 写入以下:

export default comp => {
  comp.install = app => {
    // 当组件是 script setup 的形式时,会自动以为文件名注册,会挂载到组件的__name 属性上
    // 所以要加上这个条件
    const name = comp.name || comp.__name
    //注册组件
    app.component(name, comp)
  }
  return comp
}

使用刚刚封装的函数导出我们的组件: src/button/index.js 文件导出刚刚的 button 组件,

// src/button/index.js
import { withInstall } from '../utils/withInstall';
import button from './src/index.vue';

// 导出 install
const Button = withInstall(button);
// 导出button组件
export default Button;

input 组件也类似步骤导出

然后再在 src 下的 index.js 的文件下管理我们所有的组件

// components/src/index.js
import SButton from './button'
import SInput from './input'


export { SButton, SInput }

export default [SButton, SInput]

最后 components 组件库目录下新建 index.js 集中导出所有

// components/index.js
import components from './src/index';

export * from './src/index';

export default {
  install: app => components.forEach(c => app.use(c)),
};

配置打包

然后我们需要给组件库配置打包,更改后components项目的 vite.config.js 如下:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(),VueSetupExtend()],
  base: './',
  build: {
    target: 'modules',
    //打包文件目录
    outDir: 'es',
    //压缩
    minify: true,
    //css分离
    //cssCodeSplit: true,
    rollupOptions: {
      //忽略打包vue、element-plus
      external: ['vue', 'element-plus'],
      input: ['index.js'],
      output: [
        {
          format: 'es',
          //不用打包成.es.js,这里我们想把它打包成.js
          entryFileNames: '[name].js',
          //让打包目录和我们目录对应
          preserveModules: true,
          exports: 'named',
          //配置打包根目录
          dir: resolve(__dirname, './ui/es'),
        },
        {
          format: 'cjs',
          entryFileNames: '[name].js',
          //让打包目录和我们目录对应
          preserveModules: true,
          exports: 'named',
          //配置打包根目录
          dir: resolve(__dirname, './ui/lib'),
        },
      ],
    },
    lib: {
      entry: './index.js',
      name: 'shuge',
      formats: ['es', 'cjs'],
    },
  },
})

引用组件库

好,我们的组件已经开发完成,那么我们想要看到效果呢,当然,我们可以启动 components项目,然后 app.vue 里引入编写的组件查看,那么我们该如何在示例项目 example 中使用刚刚开发的组件呢

  • 首先修改 package.json

将组件库 components package.json name 修改为 @vmkt/shuge-ui(以便我们后续包的引入),version修改为 0.0.1,private 修改为 false 代表我们这个组件库需要对外发布,最后再添加打包后的入口

// 使用 require('xxx') 方式引入时, 引入的是这个文件
"main": "./ui/lib/index.js",
// 使用 import x from 'xxx' 方式引入组件时,引入的是这个文件
"module": "./ui/es/index.js",

最终components修改后的 package.json 如下:

{
  "name": "@vmkt/shuge-ui",
  // 代表我们这个组件库需要对外发布
  "private": false,
  "version": "0.0.1",
  // 使用 require('xxx') 方式引入时
  "main": "./ui/lib/index.js",
  // 使用 import x from 'xxx' 方式引入组件时
  "module": "./ui/es/index.js",
  "type": "module",
  // 配置打包上传文件到npm的文件夹内容
  "files": [
    "ui"
  ],
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "element-plus": "^2.3.0",
    "vue": "^3.2.47"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.1.0",
    "sass": "^1.59.3",
    "vite": "^4.2.0",
    "vite-plugin-vue-setup-extend": "^0.4.0"
  }
}
  • 打包组件库

上面配置都完成后,我们于 components 目录下执行 pnpm run build将组件库进行打包

pnpm-build.png

同时components根目录下可以看到多出了我们打包后的组件

pnpm-ui.png

  • example 安装组件库

example 目录下执行pnpm add @vmkt/shuge-ui 引用我们的组件库

然后可以看到 example 下的 package.json 添加上了依赖,因为pnpm是由workspace管理的,所以有一个前缀workspace可以指向components下的工作空间从而方便本地调试各个包直接的关联引用。

example.png

接着我们在 example 里引入我们的组件测试一下,

  • 全局引入
// example/src/main.js
...
// 我们的组件 input 依赖于 element-ui,example 项目同样先安装再引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

import shuge from '@vmkt/shuge-ui'
import  '@vmkt/shuge-ui/ui/es/style.css'

...
app.use(shuge)
...

app.vue 原有内容全部删除,然后写入:

<template>
  <div>
    <s-button @click="onClick" type="primary">button</s-button>
    <s-input v-model="value">
      <template #prepend>Http://</template>
    </s-input>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { SButton, SInput } from '@vmkt/shuge-ui'
const value = ref('')
const onClick = () => {
  console.log('click')
}
</script>

启动 example 项目,可以看到按钮已经正常显示,说明我们的全局引入是成功的

example-button.png

  • 按需引入

先注释掉刚刚 main.js 里的引入代码

改在具体页面引入,这里我们在 app.vue 进行引入import { SButton, SInput } from '@vmkt/shuge-ui',app.vue 修改后如下:

<template>
  <div>
    <s-button @click="onClick" type="primary">button</s-button>
    <s-input v-model="value">
      <template #prepend>Http://</template>
    </s-input>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { SButton, SInput } from '@vmkt/shuge-ui'
const value = ref('')
const onClick = () => {
  console.log('click')
}
</script>

刷新页面,可以看到页面也是正常显示的

至此,我们利用 vue3+pnpm monorepo 开发组件库已经完成,下面我们将打包后的组件库发布到私有仓库

verdaccio 搭建npm私有仓库

verdaccio 是一个轻量级的 npm 缓存终端,按需缓存所有依赖项,并加速本地或私有网络中的安装,是搭建 npm 私服较为流行的方案之一

  • 全局安装 verdaccio
npm i -g verdaccio
  • 然后,在终端中输入 verdaccio 命令启动 verdaccio:
verdaccio

启动成功,终端输出如下

verdaccio-start.png

里面是它的配置文件位置、启动的服务地址等信息

默认 verdaccio 启动的服务都会在 4873 这个端口,在浏览器中输入 http://localhost:4873/ 出现如上页面就说明服务启动成功了:

4873.png

本地发布 npm 包到私有仓库

在此之前,你需要先注册 npm 的账号

1、 登录

npm adduser --registry  http://localhost:4873

输入npm账号用户名、密码和邮箱,登录成功后如下:

Username: yourUsername
Password: 
Email: (this IS public) 1xxxx@qq.com
Logged in as yourUsername on http://localhost:4873/.

2、发布 npm 包到私有仓库

进入到我们的组件库 components 目录下,执行

npm publish --registry http://localhost:4873/

发布成功以后如下:

npm notice 
npm notice package: @vmkt/shuge-ui@0.0.4
npm notice === Tarball Contents ===
npm notice 285B ui/es/style.css
npm notice 134B ui/es/_virtual/_plugin-vue_export-helper.js
npm notice 202B ui/lib/_virtual/_plugin-vue_export-helper.js
npm notice 257B ui/es/index.js
npm notice 126B ui/es/src/button/index.js
npm notice 145B ui/es/src/index.js
npm notice 126B ui/es/src/input/index.js
npm notice 153B ui/es/src/utils/withinstall/index.js
npm notice 327B ui/lib/index.js
npm notice 231B ui/lib/src/button/index.js
npm notice 269B ui/lib/src/index.js
npm notice 231B ui/lib/src/input/index.js
npm notice 223B ui/lib/src/utils/withinstall/index.js
npm notice 694B ui/es/src/button/src/index.vue.js
npm notice 989B ui/es/src/input/src/index.vue.js
npm notice 611B ui/lib/src/button/src/index.vue.js
npm notice 770B ui/lib/src/input/src/index.vue.js
npm notice 41B  ui/es/src/button/src/index.vue2.js
npm notice 41B  ui/es/src/input/src/index.vue2.js
npm notice 138B ui/lib/src/button/src/index.vue2.js
npm notice 138B ui/lib/src/input/src/index.vue2.js
npm notice 509B package.json
npm notice 535B README.md
npm notice === Tarball Details ===
npm notice name:          @vmkt/shuge-ui
npm notice version:       0.0.4
npm notice package size:  2.9 kB
npm notice unpacked size: 7.2 kB
npm notice shasum:        16a8e623842e7028a3bb8445af177efd9ec99c75
npm notice integrity:     sha512-79a9TMF41gv55[...]cQ5ISub13FUvQ==
npm notice total files:   23
npm notice
+ @vmkt/shuge-ui@0.0.4

在浏览器中刷新 http://localhost:4873 页面

vmkt.png

可以看到,我们的组件库 shuge-ui 已经发布成功,接下来我们在其他项目中对其安装使用一下

使用私有仓库npm包

我们首先启一个项目,找一个空白文件夹,cmd 输入:

pnpm create vite demo

选择创建一个 vue 项目,安装依赖并启动

下载我们发布到私有仓库的npm包时,需要修改仓库地址,具体操作如下

npm set registry http://localhost:4873

在执行这条命令以后,再使用pnpm add @vmkt/shuge-ui命令就会优先去我们自己的私有仓库下载npm包,如何没有找到,则会从npm中央仓库下载

ackages: +22
++++++++++++++++++++++
Progress: resolved 80, reused 55, downloaded 3, added 22, done

dependencies:
+ @vmkt/shuge-ui 0.0.4

The integrity of 4629 files was checked. This might have caused installation to take longer.
Done in 33.7s

安装成功后会如上显示输出

因为我们的组件库还依赖于 element-plus 所以我们同样进行安装一下

pnpm add element-ui

最后我们和 example 里操作一样,全局引入和按需引入测试一下我们的组件库,以全局引入示例:

// main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import shuge from '@vmkt/shuge-ui'
import  '@vmkt/shuge-ui/ui/es/style.css'

const app = createApp(App)

app.use(ElementPlus)
app.use(shuge)

app.mount('#app')
// app.vue
<template>
  <div>
    <s-button @click="onClick" type="primary">button</s-button>
    <s-input v-model="value">
      <template #prepend>Http://</template>
    </s-input>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const value = ref('')
const onClick = () => {
  console.log('click')
}
</script>

http://localhost:5173/ 刷新页面,可以看到,我们的组件库使用正常 example-button.png

结尾

至此,我们使用 vue3+ pnpm monorepo 搭建组件库发布到私有仓库,并在项目中使用的教程就到这里结束了。

文章也是带大家简单入门,实际情况中还有很多未考虑,比如我们没有用ts进行开发,只做了组件的按需引入,但是样式按需引入的却没有,还有自动化发布流和生成发布记录,eslint与prettier,代码提交规范,单元测试等这些都是一个完备的组件库所可以或者说需要去做的。

参考

pnpm官网
pnpm+vite+vue3搭建业务组件库踩坑之旅

往期回顾

用微前端 qiankun 接入十几个子应用后,我遇到了这些问题
vue3 正式发布两年后,我才开始学 — vue3+setup+ts 🔥
2022年了,我才开始学 typescript ,晚吗?(7.5k字总结)
当我们对组件二次封装时我们在封装什么
vue 项目开发,我遇到了这些问题
关于首屏优化,我做了哪些