从零搭建pnpm + monorepo + vuepress2.x + vue3的组件库

2,718 阅读8分钟

前言

monorepomultirepo 的区别、workspace是干嘛的,pnpm优缺点,pnpm比 lerna + yarn的方式好在哪,这些咱一概不讲哈,有需要的自行查阅。

如果我们已经达成共识,要使用pnpm搭建一个前端组件库,并且使用monorepo的方式结构化工程,最终要将这个组件库发布到NPM,那么,你就可以继续看下去了~

为什么选择pnpm?

  1. 节省磁盘空间并提升安装速度,是同类工具速度的将近2倍
  2. 支持monorepo,对于组件库来说这一点很重要,现在意义上的组件库不仅仅只实现了组件共享,也包含了工具包、hooks、自定义指令、主题等,他们可以独自发包也可以统一发包
  3. 使用 pnpm创建的node_modules是非扁平的,解决了幽灵依赖的问题。(当使用 npm 或 yarn 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下。其结果是,源码可以访问 本不属于当前项目所设定的依赖包,这些就是幽灵依赖)
  4. 另外一个就是好奇心,拥抱新技术(不卷不行了~)

搭建工程

一、安装pnpm

npm i pnpm -g

正常安装就行,如果提示需要升级node版本,那就按照要求升级版本就好了,如果不知道怎么升级可以使用下面的方法。

升级node版本教程

n是一个 Node.js的版本管理器,操作简单,支持多种操作系统。

// 安装n
npm i -g n
// 查看已经安装的node版本
n list
// 安装node的稳定版本
n stable
// 安装指定的node版本:example==> 14.1.0,(安装node指定版本14.1.0)
n 11.10.0 

二、搭建组件库

安装VuePress2.x

  • 步骤 1: 创建并进入一个新目录
mkdir again-ui
cd again-ui
  • 步骤 2: 初始化项目
git init
pnpm init
  • 步骤 3: 将 VuePress 安装为本地依赖
pnpm add -D vuepress@next @vuepress/client@next vue
{
  "scripts": {
    "docs:dev": "vuepress dev docs",
    "docs:build": "vuepress build docs"
  }
}
  • 步骤 5: 将默认的临时目录和缓存目录添加到 .gitignore 文件中
echo 'node_modules' >> .gitignore
echo '.temp' >> .gitignore
echo '.cache' >> .gitignore
  • 步骤 6: 创建你的第一篇文档
mkdir docs
echo '# Hello VuePress' > docs/README.md
  • 步骤 7: 在本地启动服务器来开发你的文档网站
pnpm docs:dev

点击参考文档

根据自身需要对文档进行配置,下面贴出我的配置文件

// .vuepress/config.ts
import { defaultTheme } from 'vuepress'
import navbar from './configs/navbar'
import sidebar from './configs/sidebar

export default {
  lang: 'zh-CN',
  title: '组件库',
  description: '基于Vue3 + VuepressUI组件库',
  theme: defaultTheme({
    navbar,
    sidebar,
    editLinkText: 'GitHub 上编辑此页',
    lastUpdatedText: '上次更新',
    contributorsText: '贡献者',
  }),
}

// navbar.ts
export default [
  { text: '指南', link: '/guide/intro.html' },
  { text: '组件', link: '/components/title.html' },
  // { text: 'API 参考', link: '/api/' },
  {
    text: '更新日志',
    link: 'https://gitee.com/NewCoderMCL/vuepress-ui',
  },
]

// sidebar.ts
export default {
  '/api/': getAPISidebar(),
  '/components/': getComponentsSidebar(),
  '/guide/': getGuideSidebar(),
}

function getComponentsSidebar() {
  return [
    {
      isGroup: true,
      text: '组件',
      children: [
        {
          text: 'title 标题',
          link: '/components/title.md',
        },
        {
          text: 'Table',
          link: '/components/table.md',
        },
        {
          text: 'Pagination',
          link: '/components/Pagination.md',
        },
      ],
    },
  ]
}

function getGuideSidebar() {
  return [
    {
      isGroup: true,
      text: '指南',
      children: [
        { text: '介绍', link: '/guide/intro.md' },
        { text: '快速开始', link: '/guide/installation.md' },
      ],
    },
    {
      isGroup: true,
      text: '教程',
      children: [
        { text: '教程1', link: '/guide/button.md' },
        { text: '教程2', link: '/guide/modal.md' },
      ],
    },
  ]
}

function getAPISidebar() {
  return [{ text: 'API参考', link: '/api/index.md' }]
}

编写组件

在项目根目录下新建packages/components目录,在该目录下编写组件title.vue,内容如下

<template>
  <h1 :style="{ fontSize }">
    <span>
      <slot name="icon" />
    </span>
    <slot> {{ msg }}</slot>
    <!-- {{ msg }} -->
  </h1>
</template>
<script lang="ts">
export default {
  name: 'BaseTitle',
}
</script>
<script setup lang="ts">
defineProps<{ msg?: string; fontSize?: string }>()
</script>
<style scoped lang="scss">
h1 {
  display: flex;
  align-items: center;
  font-size: 20px;
  font-weight: normal;
  margin-bottom: 20px;
  > span {
    display: flex;
    margin-right: 5px;
    align-items: center;
  }
}
</style>

注册组件:

v2.vuepress.vuejs.org/zh/guide/mi…,文档给出两种方式注册vue组件,本文使用在 .vuepress/client.{js,ts} 中手动注册组件。

import { defineClientConfig } from '@vuepress/client';
import BaseTitle from '../../packages/components/title.vue';

export default defineClientConfig({
  enhance({ app }) {
    // 注册组件
    app.component('BaseTitle', BaseTitle);
  }
});

实现组件文档的示例代码块

使用vuepress官方文档编写组件示例(点击跳转),扩展阅读

官方示例的缺点

  1. 代码和效果展示,需要分别写两次代码
  2. Vuepress 无法渲染 Markdown 中的 scriptstyle 代码块。

在此推荐使用vuepress-plugin-demoblock-plus插件,解决以上问题,并且效果与element-ui官方文档类似。

1、安装vuepress-plugin-demoblock-plus

由于项目是基于vuepress2.x 的,当前插件仅限于在当前环境下可用。

pnpm add -D vuepress-plugin-demoblock-plus

2、配置插件

docs/.vuepress/config.ts中引入插件并配置,如下:

import { demoblockPlugin } from 'vuepress-plugin-demoblock-plus'

export default {
  lang: 'zh-CN',
  title: '组件库',
  ...
  plugins: [
    demoblockPlugin({
      // locales,
      customClass: 'demoblock-custom',
      theme: 'github-light',
      cssPreprocessor: 'scss',
      // customStyleTagName: 'style lang="scss"', // style标签会解析为<style lang="scss"><style>
      scriptReplaces: [
        {
          searchValue: /const ({ defineComponent as _defineComponent }) = Vue/g,
          replaceValue: 'const { defineComponent: _defineComponent } = Vue',
        },
      ],
    }),
  ],
}

Q&A

  1. 浏览器报错:chunk-O5XLV5GU.js?v=e7120082:208 Uncaught ReferenceError: Cannot access 'clientConfigs' before initialization

解决方案:因为vuepress-plugin-demoblock-plus需要锁定版本:vue为3.2.44。vuepress: 2.0.0-beta.51,因此调整package.json为:

"@vuepress/client": "2.0.0-beta.51",
"vue": "3.2.44",
"vuepress": "2.0.0-beta.51",
"vuepress-plugin-demoblock-plus": "^2.0.4"

重新install之后,示例代码块就已经实现啦~,👏👏👏

代码块效果如下

三、Monorepo工程化项目

pnpm自带monorepo,使用monorepo的目的是多个项目可以共用一套代码风格、一套代码提交规则以及CI相关流程,当然,只要是搭建项目用到的一切都可以共用。比如,你手头上项目有十个八个的,时间长了,总会积累出一些常用的工具函数(日期格式化、正则表达式等)这些想要单独管理的话,又得单独开一个项目。但是使用monorepo可以很大程度改善这些问题。

设计实现

在项目根目录下新建packages目录,作为组件库的组件、hook、工具函数的核心代码包,大概的模块包设计就是你需要共享几个模块就在packages目录下建几个目录,目前我创建的几个目录就是componentsutils,分别在这两个目录下执行npm init命令,修改相应的packages.json中的name"@again-ui/components","@agagin-ui/utils",使用这种方式命名的原因是,这些作用域包之后可以在npm新建一个organization为agagin-ui 的组织,单独发布和管理这些作用域包,这些作用域包的专业名称叫scope packages

作用域包的专业解释:docs.npmjs.com/cli/v8/usin…

❓考虑组件库项目本身和发出去的包的关系

项目本身只是我们开发组件库的环境,我们共享出去的包只需要包含要共享的内容就行了,项目本身不需要发包,按我们目前的设计,其实只需要发布packages目录下的内容即可。

基于以上的思考,发包并不应该把项目根目录下的东西全部发出去,因此,为了防止根目录被发布出去,需要将根目录下的package.json 文件添加 "private": true

四、发包组件库到NPM

  • 第一步:在npm官网创建organization

登陆npm创建organization

  • 第二步:在packages/components目录下执行pnpm publish进行发包,提示如下:
npm ERR! code E402
npm ERR! 402 Payment Required - PUT https://registry.npmjs.org/@again-ui%2fcomponents - You must sign up for private packages

如果发布带组织名的包,默认为私有的包,而私有的包是要收费的。因此需要在发布的时候增加 --access public

pnpm publish --access public

不过更推荐直接配置在package.json(当前目录下的package.json,不是根目录下):

{
  ...
  "publishConfig": {
    "access": "public"
  },
  ...
}

如果本地有修改,并且还未提交到git,那么会提示:

If you want to disable Git checks on publish, set the "git-checks" setting to "false", or run again with "--no-git-checks".

那么发布时需要加上--no-git-checks,执行如下:

pnpm publish --access public  --no-git-checks

控制台显示如下内容,则表示发布成功:

image.png

到目前为止只是实现了发包,但是如果想在其他项目中使用组件库,还需要再做一些调整。

五、在其他项目中使用组件库

现在市面上的UI组件库都会支持按需引入,比如element-ui

import { Button, Select } from 'element-ui'

调整项目目录

如果需要实现上面这样的功能,则需要调整下组件目录,将packgaes/components/title.vue调整为:

packages/components/title/index.ts作为当前组件的入口文件并将组件本身导出,内容如下:

// packages/components/title/index.ts
import Title from './src/title.vue';
import type { App } from 'vue';

Title.install = (app: App): void => {
  app.component(Title.name, Title);
};
export const baseTitle = Title;
export default Title;

packgaes/components/ 目录下新建组件库(components)的入口文件 index.ts

// index.ts
// 将components目录下的所有组件导出,这样才能支持按需引入
export * from './title';

我们组件库的入口文件已经加好了,还需要在相应的package.json中指出入口文件,同时如果你的组件依赖了其他依赖包,也需要更新在当前文件中:

{
  "name": "@again-ui/components",
  "version": "0.0.5",
  "description": "",
  "main": "index.ts", // 入口文件
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "peerDependencies": {
    "vue": "^3.2.0"
  },
  // 其他依赖包
  "dependencies": {
    "sass": "^1.56.1"
  }
}

然后再进行发包,发包前记得更新组件库的 version

发包成功之后,在其他项目中怎么使用呢?

第一步

yarn add @again-ui/components

第二步

在项目中直接使用:

// 某个组件
<script setup lang="ts">
import { baseTitle } from "@again-ui/components";
</script>

<template>
  <base-title font-size="30px">22</base-title>
</template>

总结

使用pnpm来搭建Monorepo工程要比Lerna+yarn的方式快捷简便的多,而pnpm又自带workspace属性,在效率上又更胜一筹。如果你还没使用过pnpm,那你完全可以跟着我的这个项目浅试一下~

大家在实现的过程中有什么问题可以随时在评论区沟通学习~,欢迎点赞收藏评论👏👏

「回顾2022,展望2023,我正在参与2022年终总结征文大赛活动