【前端组件化】系列第二篇——monorepo方案实战

本文正在参加「金石计划 . 瓜分6万现金大奖」

前端组件化系列文章第二篇

一个使用 pnpm + workspace + changeset 的 monorepo 多包管理组件化项目

第一篇看这里:【前端组件化】系列文章第一篇——方案探究

前言

在上一篇的前端组件化方案探究中,我们研究了什么是组件化以及我们为什么需要组件化。也调研和测试了一些开源项目,并且在使用、学习、研究、对比之后最终确定了以 pnpm + workspace + changeset 为技术方向的 monorepo 多包管理方案

  • 本文主要是沿着该路线进行项目落地,是一篇聚焦于实战的文章。
  • 文章的目标是能够让未接触过monorepo架构的新手也能从0到1的完成一个多包管理的前端组件化项目

细致一点的说,我们主要从这几个方面入手:

  1. monorepo项目架构的搭建
  2. 整个项目的工程化规范搭建
  3. 子项目的配置和构建配置,包括出入口管理、webpack打包
  4. 文档站点的搭建
  5. 版本管理和发布

3.jpg

使用 pnpm 快速搭建一个 monorepo 项目

在上一篇文章中我们描述了什么是monorepo,以及pnpmworkspace工作空间概念,简单理解的话就是两个方面:

  • 如何通过一个工程管理多个项目的依赖
  • 如何通过一个工程管理多个项目间的相互依赖
  • 如何通过一个工程管理多个项目的版本

这里,我们主要围绕这几个核心思想展开工作。

创建主项目

首先,我们先创建一个主项目,在命令行中执行:

  1. mkdir my-monorepo-app && cd my-monorepo-app
  2. pnpm init

接下来我们创建一个packages目录作为我们的子项目目录使用,然后再创建几个子项目,如下:

├── package.json       
├── packages           // 子项目目录
  ├── components       // UI基础组件
  ├── pro-sqltiptree   // 高级业务组件
  ├── utils            // 工具类

在主目录下简单配置下.gitignore

/**/node_modules/

因为是多包项目,存在主和子关系的嵌套结构,过滤规则需要考虑子目录下的情况(**/node_modules

准备工作差不多了,接下来我们配置下工作空间(workspace),为安装npm包和依赖管理做准备。

其实对于使用pnpm的项目来说,还是比较简单的,我们只需要在主目录下创建一个pnpm-workspace.yaml 文件即可开启pnpm的工作空间功能。

pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中。

Workspace 协议更多的是对一个项目下多子项目的管理以及依赖管理。

我们先把packages目录添加到工作空间中,pnpm-workspace.yaml配置如下:

packages:
  - packages/*

这样的话,所有在 packages 目录下的项目都会被工作空间统一接管,该目录下的子项目可以相互引用依赖,而不必重复安装,尤其是针对本地开发包的管理使用,提供了极大的方便,具体细节我们继续往下看~

安装依赖

monorepo最主要的功能就是对于依赖的管理👀

1. 全局共用的依赖

对于使用比较频繁的依赖,在多个子项目中都有用到的情况,比如我们当前技术栈为vue2,那么核心库 vuevue-router基本都会用到,以及一些通用工具,如 lodashdayjs等,此时,就可以采用全局安装的方式,这样做的好处是所有的子项目都可以直接引入使用(此时的依赖是安装在主目录下的 node_modules 中的)。

如果全局中没有依赖,而子项目中有相同的依赖,你也不用担心,pnpm也会自动优化处理,通过软连接的方式安装一次就可以,可以实现最小化安装产物,非常的好用😎~

pnpm i vue@^2.6.11 dayjs -Dw
  • pnpm ipnpm add 功能是一样的
  • -D:作为开发依赖使用
  • -w:表示把包安装在 root 下,该包会放置在 <root>/node_modules

2. 单一子项目的依赖

作为一个多包项目,肯定会存在某个项目独有的一些依赖,这个时候我们就可以针对性的安装到指定子目录。

  • 这里我们只需要关注使用层面即可,对于多个子项目是否存在相同的依赖,是否会重复安装,你完全不用担心,pnpm会优雅的帮你处理好。

  • 你可以这样理解——pnpm会优先保证只下载一个依赖,当你在另一个子项目中也安装了同样的依赖时,pnpm不会再傻傻的去服务器重新下载一份。

  • 对pnpm的设计实现原理感兴趣的可以参考这篇文章pnpm 是凭什么对 npm 和 yarn 降维打击的,这里不再赘述~

# 语法
pnpm add <package-name> --filter <target-package-name>

# 比如要将lodash装到components下
# --filter 后面可以为目录名称也可以为 package.josn 的 name 名称
# 比较推荐的做法是根据 package.josn 的 name 名称进行区别
pnpm add lodash --filter @ah-ailpha/components
  • --filter:过滤,过滤允许您将命令限制于包的特定子集,一般用于 packages/* 下面的子项目。

如下,我们需要安装 iviewcomponents 子项目中,那么,标准的做法是:

  1. 先查看 packages/components/package.json 中的 name 字段,把它作为 filter 的作用域条件

    // components 子项目的目录结构
    ├── packages           // 子项目目录
      ├── components       // UI基础组件
        ├── package.json   // name: @ah-ailpha/components
    
  2. 然后,在根目录下执行安装命令,并指定 filter

    # 安装 iview 到 components子项目中
    pnpm add iview --filter @ah-ailpha/components
    
  3. 查看项目依赖变化

    这个时候你应该可以发现 components 子项目的 package.json 文件的 dependencies 属性是变化了的,而且应当只有这个子项目是受影响的。

    删除等操作命令保持一致,限定作用域即可,如:pnpm rm iview –filter @ah-ailpha/components

    {
      "dependencies": {
        "iview": "^3.5.4"
      }
    }
    

3. 子项目互相作为依赖

作为 monorepo 管理的项目,这一步的功能才是最为核心的(🙋‍♂️敲黑板啦!!!)。

以标准的组件库项目为例:

  1. 我们一般会把组件类代码单独做为一个子项目进行管理
  2. 文档网站也会作为一个子项目单独管理
  3. 工具类也会作为一个子项目单独管理
  4. 以及可能根据个人业务需求还有其他功能包(cli等等)

但是这些子项目又不是完全独立的,他们之间存在着一些相互依赖关系,如:

  • 组件代码会依赖于工具类的一些方法
  • 文档网站会依赖于组件和工具类(我的UI文档网站肯定用的是我自己的UI组件😄)

最终他们都会作为一个独立的模块提供给用户,因此,从使用者的角度考虑,我们最好也保持这种使用方式——npm安装包的形式import utils from '@an-ailpha/utils')。

那么,当我把其中一个子项目作为另一个项目的依赖时,如何实现我在本地调试开发的时候,能够做到类似安装npm远程包一样使用呢(而不是通过路径引用去使用,这样做既不优雅也不方便,当我们发布之后,路径引用就会失效)?以及如何保证我所引用的依赖都是最新的、实时的呢?因为我不可能没调整一点代码就重新发个npm包,然后子项目再重新下载一遍,以这种方式验证吧。

2.jpg

令人高兴的是,pnpmworkspace 工作空间的功能可以完全满足我们的需求,它可以实现这种效果。

如:子项目packages/components需要使用packages/utils子项目作为项目依赖时,我们直接按照 npm 安装依赖的逻辑使用即可✅

目录结构:

├── packages           // 子项目目录
  ├── components       // UI基础组件
    ├── package.json   // name: @ah-ailpha/components
  ├── utils            // 工具类
    ├── package.json   // name: @ah-ailpha/utils
  1. 查看 packages/utils/package.jsonpackages/components/package.json 中的 name 字段

  2. 然后,在根目录下执行安装命令,并指定 filter

    # 把子模块 packages/utils 当成依赖安装到 packages/components
    pnpm add @ah-ailpha/utils --filter @ah-ailpha/components
    
  3. 查看依赖变化

    这个时候你应该可以发现 components 子项目的 package.json 文件的 dependencies 属性是变化了的:

    {
      "dependencies": {
        "@ah-ailpha/utils": "workspace:^1.0.0",
        "iview": "^3.5.4"
      }
    }
    
    • "@ah-ailpha/utils": "workspace:^1.0.0",这是pnpmworkspace工作空间协议写法

    它实现的效果是:当utils子项目中的代码更新后,你在 components 中引用的代码也一定是最新的,你可以理解为他俩已经形成了绑定关系,你变我就变~(版本更新时也不用担心,具体可以看下面的版本管理部分的详细说明)

    • 需要指出的是,当你在执行发布操作之后,npm包中的版本会被自动更新为指定目录下package.jsonversion

    如果,此时utils/package.json中的version值为1.1.0,那么上面的"@ah-ailpha/utils": "workspace:^1.0.0"会自动更新为"@ah-ailpha/utils": "^1.1.0"

    不过,在实际开发中,我们一般会使用另一种写法,请继续往下看~🧐

简单说说npm版本规范

从上面的安装依赖中,我们可以发现npm中的一些版本规则:^,这也是我们比较常见的,npm的默认按照逻辑是按照这个规则执行的,其他前缀还有:~*,那么它们是什么意思呢?作用又是什么呢?

  • 我们先了解下版本格式:

    主版本号.次版本号.修订号

  • 版本号递增规则如下:

    1. 主版本号:当你做了不兼容的 API 修改,
    2. 次版本号:当你做了向下兼容的功能性新增,
    3. 修订号:当你做了向下兼容的问题修正。

    先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

    比如,我们经常在开源项目中见到的:alpha、beta 、rc

这里不再做过多赘述,感兴趣的可以看这里 语义化版本号标准

  • npm 的版本匹配策略

    • ^1.0.1:只要主版本一致都匹配(1.x),如:11.x
    • ~1.0.1:只要主版本和次版本一致都匹配(1.1.x),如:1.11.1.x
    • * :全匹配,不受版本号影响,可以命中一切新发布的版本号

不过,就针对我们这个项目来说,它的workspace协议也是符合npm版本规范的,我们子项目内的引用也可以采用对应的匹配策略。

但是,不同于用户使用,我们各子项目为了方便不受版本影响,从而做到匹配一致,一般采用*全匹配策略,从开发调试的角度考虑,各子项目间的依赖引用应该都是最新的代码,这一点是需要清楚的。

你可以这样安装使用:

pnpm add @ah-ailpha/utils@* --filter @ah-ailpha/components

然后,依赖会变成这样:

{
  "dependencies": {
    "@ah-ailpha/utils": "workspace:*",
  }
}

目前,这种写法也是子项目间依赖引用比较推荐的,依赖关系不受版本影响,可以做到相对统一。

小结

  1. 所有的命令操作都是在主目录下进行的,用户视角应始终在主目录下才对
  2. 各子项目的package.jsonname应该符合该规范——@ah-ailpha/components
    • 这样做的好处之一是可以帮我们统一作用域前缀
    • 其次,当我们发布npm包时,@ah-ailpha会作为npm的一个组织域使用
  3. 子项目内部引用使用npm *全匹配策略

monorepo项目的工程化搭建

一个基本标准就是:所有的规范配置都应以主目录为起点开始。

目前工程规范有:eslintprettiermarkdownlintcommitlinttypescript

为什么在vue2的项目中引入了typescript

我们都知道vue2版本对ts的支持很一般,相关生态库大多也都是js开发的,包括iview也是使用js开发的;那么,我这里为什么依旧采用了对ts的支持呢?

  1. 最核心的一点,当下前端生态TS正大步向前,我之前也一直在使用TS开发,现在不能因为vue2而就放弃了,还是应该以积极拥抱的心态
  2. 本项目架构支持子项目采用不同的规范策略,以vue2为主的components子项目完全可以在本目录下配置一套自己的项目规则,进而覆盖主项目配置,那么,现在可以做到的是,
  3. 本项目不是单一的vue2组件项目,他还包括 utils等其他子项目,是一个完善的前端组件化项目,这些都需要考虑在内

我在开发utils子项目时采用了typescript + rollup的开发和构建模式,TS完善的类型支持系统对开发这一类工具函数库,提供了极大的方便。目前这个库也完全不受技术栈影响,你可以在任何需要的地方使用,就类似lodash那样,而且当你在TS项目使用时又能获得完善的类型提示,岂不美哉~

对于vue2 + iview我就采用官方的js生态即可,直接复用现成的项目配置,在这里你也可以把它当成一个独立的项目去开发,也是完全OK的。

更进一步的说,以后都是使用vue3生态了,全面拥抱TS不可避免,学起来~

工程化规范配置

须要指出的是一定要注意版本一定要注意版本一定要注意版本

前端的工程化配置我在另一篇vue3的相关文章中也介绍过,都是大同小异,感兴趣的可以看这篇文章——vite + vue3 + setup + pinia + ts 项目实战

唯一不同的是,本项目增加了对Markdown类型文件的格式化处理(.md),这里主要使用的是 markdownlint-cli

为什么增加了这一项规则,这和我们的项目需求有很大的关联,前面我们说过我们会搭建一个完善的文档网站系统,组件库离不开详细的文档说明,Markdown是我们的不二之选,之后项目中会存在大量的.md文件,此时,保持一个统一规范的格式化规则是十分必要的。

这方面的配置我推荐参考下element-plus的相关实现,值得学习。

前人栽树我乘凉😬

在配置 eslintprettier经常踩的坑就是因为版本问题,这里并不推荐从零开始,我们完全可以找一些和自己风格倾向比较一致的开源项目,借鉴别人项目中配置好的依赖,使用到我们自己的项目中😏。

4.jpg

只有在我们想定义自己的特殊规范时,这时才需要结合官方文档了解规则配置的各种参数,以及相关规则包的一些插件库,按需安装配置即可,这样可以节省大量时间成本,毕竟很多开源项目都是极其成熟的了(这里也需要测试版本,默认安装一般都是基于vue3环境的最新的,vue2相关的可以参考一些vue2的老项目)。

在引入TypeScript支持时,需要安装@typescript-eslint/parser依赖,这时eslint的配置是有所调整的:

- "parser": "@typescript-eslint/parser",
+ "parser": "vue-eslint-parser",
  "parserOptions": {
+     "parser": "@typescript-eslint/parser",
      "sourceType": "module"
  }

自定义解析器 @typescript-eslint/parser 需要 vue-eslint-parser插件先解析 .vue 文件,这是需要注意的一点。

项目规范的命令配置

"scripts": {
  // 约束包管理工具为 pnpm
  "preinstall": "npx only-allow pnpm", 
  // 配合git commit的提交规范 commitlint
  "prepare": "husky install", 
  // eslint lint告警信息查看不合规范的错误
  "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx,.json --max-warnings 0", 
  // eslint fix 直接修复不合规范的代码
  "lint:fix": "eslint --fix . --ext .vue,.js,.ts,.jsx,.tsx,.json", 
  // 工具类的单独lint
  "lint:utils": "eslint --fix \"./packages/utils/**\" --ext .js,.ts,.json", 
  // Markdown 文件 lint(packages目录下)
  "markdownlint": "markdownlint \"./packages/**/*.md\"",
  // Markdown 文件 lint fix(整个项目)
  "markdownlint:fix": "markdownlint --fix \"**/*.md\"",
}

配合vscode保存时自动检查fix

{
  "editor.formatOnSave": true, //每次保存的时候自动格式化
  "editor.codeActionsOnSave": {
    // 保存时使用 ESLint 修复可修复错误
    "source.fixAll.eslint": true
  },
  // ESLint要验证的语言
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "vue",
    "typescript",
    "typescriptreact",
    "html",
    "css",
    "scss",
    "less",
    "mpx",
    "json",
    "markdown"
  ],
    // eslint保存时只修复有问题的代码
  "eslint.codeActionsOnSave.mode": "problems",
  "eslint.format.enable": true,
  "eslint.quiet": true, // 忽略 warning 的错误
}

为了不使 eslintprettier的规则发生冲突,你应该在eslint中这样配置,以保证可以合并配置,并使用 prettier格式化

{
  extends: [
    // 'plugin:vue/recommended',
    'plugin:prettier/recommended',
    'prettier',
    // 'plugin:@typescript-eslint/recommended',
  ],
  rules: {
    'prettier/prettier': 'error',
  }
}

配合git(参见 .husky目录)

  • commit-msg:提交信息规范验证
  • pre-commit:提交前执行代码格式化验证等其他工作

配合pre-commit工作的lint-staged相关配置:

{
  "*.{vue,js,ts,jsx,tsx,json}": [
    "eslint --fix",
    "prettier --write"
  ],
  "*.md": [
    "markdownlint --fix",
    "prettier --write"
  ]
}

eslintprettiercommitlint这些都是很常规的配置,这里就不在过多叙述了,仓库地址放在下面了~

包构建

包构建的基本思路依然是按照当前成熟方案进行选择,vue2生态的组件类代码优先使用 Webpack进行构建打包,并且要支持ES Module这种引入方式,方便为之后的按需引入做铺垫,当然,为了兜底的兼容,也要支持 umd 这种方式(同时以 AMDCommonJS2全局属性形式输出,支持直接使用 <script>HTML 中引入)。

我在搭建本项目的vue2 + iview的组件库时,碍于版本的影响,子项目相关全部采用 iview项目中的构建配置进行打包,这样可以避免大量Webpack配置时的版本问题。

而对于 utils这种不受vue2影响的独立库,则优先采用TypeScript + Rollup这种更方便库模式的方式进行构建,使得其能够对上下做到很好的支持(jsts)。

统一出入口

每个组件都应该以 index.js 作为出入口,而整个组件库下的index.js应该是所有入口的集合。

这样做的好处,其一,目录结构清晰方便维护,其二方便引用注册统一管理。

├── packages                        // packages工作目录
      ├── components                // components组件目录
        ├── index.js                // components组件统一出入口
        ├── src                     // components组件源码目录
          ├── select                // select组件的开发目录
              ├── src               // 组件源码
                  ├── style         // 样式相关
                  ├── index.vue     // 组件文件
                  ├── props.js      // props相关定义的声明
                  ├── const.js      // 常量
              ├── index.js          // 组件唯一出入口

select组件的index.vue如下:

<script>
export default {
  name: 'AhSelect',
}
</script>

select组件的index.js(作为组件Select的出入口)如下:

import Select from './src/index.vue'

Select.install = function (Vue) {
  Vue.component(Select.name, Select)
}

export default Select

components/index.js(作为组件库components的出入口)如下:

import AhButton from './src/button/index'
import AhSelect from './src/select/index'

const components = {
  AhButton,
  AhSelect,
}

const install = function (Vue) {
  if (install.installed) return

  Object.keys(components).forEach(key => {
    Vue.component(key, components[key])
  })
}

// auto install
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

const API = {
  ...components,
  install,
}

export default API

utils的目录结构:

├── packages              // packages工作目录
  ├── utils               // utils的开发目录
      ├── src             // utils源码目录
          ├── dom         // dom相关工具目录
          ├── object      // object相关工具目录
          ├── index.ts    // 统一出入口

src/dom/getScrollTop.ts代码如下:

/**
 * @desc 设置滚动条距顶部的距离
 * @param {Number} value
 */
export function setScrollTop(value: number): number {
  window.scrollTo(0, value)
  return value
}

src/index.ts(作为工具类库utils的出入口)代码如下:

// dom
export * from './dom/setScrollTop'
export * from './dom/getOffset'
// object
export * from './object/deepClone'
export * from './object/isEmptyObject'

// ...

构建配置

应该遵循相对的统一,即,我们应该在主项目下维护一份统一的基础配置文件,每个子项目再根据其package.json做部分参数的细微调整,如:包名称name版本version等。

在根目录下创建 build目录,用于放置一些工程配置相关的文件,,如:webpack.base.config.js

子项目packages/components/webpack.config.js中通过引入该配置,再结合 webpack-merge完成对配置的合并:

// webpack.config.js
const merge = require('webpack-merge')
const webpackBaseConfig = require('../../build/webpack.base.config')
const pkg = require('./package.json')

module.exports = merge(webpackBaseConfig, {
  mode: 'production',
  output: {
    library: pkg.name.slice(11) // @ah-ailpha/pro-sqltiptree
  },
  plugins: [
    new webpack.BannerPlugin({
      banner: 
      `/*!
        * ${pkg.name} v${pkg.version} 🖖
        */`,
      raw: true,
      entryOnly: true,
    })
  ]
  // ...
})

package.json中配置如下脚本:

"scripts": {
  "clean": "rimraf dist",
  "build": "pnpm run clean && webpack --config webpack.config.js"
}

需要指出的是上面的脚本一般会在主目录下,由版本控制工具统一执行(当然你也可以单独执行)

在主目录下你可以这样执行以上的命令:

# 使用 --filter
pnpm run --filter ./packages/components build # 路径
pnpm run --filter @ah-ailpha/components build # 包名称
# 使用 -C 指定路径
pnpm run -C ./packages/components build

在测试时你可以选择这样使用,但在实际开发当中我们都是通过脚本一键打包、发包,统一处理的,完整的写法如下:

"scripts": {
  "clean": "pnpm run --filter \"./packages/**\" -r --parallel clean",
  "build:packages": "pnpm run --filter \"./packages/**\" -r --parallel build",
}

这里新增了两个命令行参数,简单介绍下:

  • -r:对所有子项目执行命令(如执行每个子项目的 clean 命令 pnpm run -r clean
  • --parallel:忽略并发,立即在所有匹配的软件包中运行一个给定的脚本,用于在许多 packages 上长时间运行的进程,例如冗长的构建进程。

utils工具类的配置也比较简单,不过多描述了,可以直接看仓库代码~

"scripts": {
  "build": "pnpm run clean && rollup -c",
  "clean": "rimraf lib dist es"
}

构建的相关步骤结束,在之后的版本控制中,配合 changeset可以做到更全面的自动化操作😋

5.jpg

版本管理和包发布

Changesets 是一个用于 Monorepo 项目下版本以及 Changelog 文件管理的工具。

本项目目前使用的是 changeset进行多包版本管理,它拥有交互式 CLI,可以非常友好的帮我们确定包版本 => 生成相应 CHANGELOG.md日志,并更新 package.jsonversion信息,然后一键发布包至 npm

Changesets的实战使用细节可以参考这篇文章:Changesets: 流行的 monorepo 场景发包工具

  1. changeset init

    • 新项目执行该命令,完成对项目的初始化
    • 会在根目录下生成 .changeset 目录,config.json配置文件(注意分支 "baseBranch": "master"
  2. changeset

    • 执行该命令,进行版本管理,会交互式选择不同项目,以及确定发布的版本
    • 会生成一些 .md 文件在目录下,会在 version 的时候消耗
  3. changeset version

    • 消耗上一步生成的相关的一些版本信息及记录内容的 .md 文件,并生成或更新 CHANGELOG.md 文件,之后 .md 文件会被自动删除
    • 相应的 package.json 中的 version 信息也会同步更新
  4. changeset publish

    • 发布包到远程 npm
    • 前置条件是你已经进行了 npm 账户登录,如果包名称为 @ah-ailpha/components该类型,还需要在 npm 账户中设置组织

npm 发包注意事项

为了发布使用,我们还需要在npm创建一个@ah-ailpha组织,私有的需要收费,选择免费~

在执行 npm publish 发布命令之前,你应该已经进行了 npm login 登录操作~

在开始使用changeset publish时,可能会报npm ERR! 402 Payment Required错误

原因:无法发布到私有包,当包名以@your-name开头时,npm publish会默认发布为私有包,但是 npm 的私有包需要付费的~

402 错误

npm ERR! code E402
npm ERR! 402 Payment Required - PUT https://registry.npmjs.org/.... - You must sign up for private packages

解决办法:在项目的package.json 中添加如下配置:

{
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/"
  }
}

发包到npm

为了方便使用,我们在脚本中添加如下命令:

"scripts": {
  "preversion": "changeset",
  "version": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format:code",
  "release": "pnpm run clean && pnpm run build:packages && changeset publish",
  "release:nobuild": "changeset publish",
}

第一步

执行 pnpm run preversion 命令,通过交互式命令确定版本(空格——选择,回车——确定)

monorepo-1.gif

此时,我们选择为 1.5.0版本,执行结束,.changeset目录下回生成一个临时文件,这是给第二步命令使用的,我们不用管它~

第二步

执行 pnpm run version命令后,临时文件会被消耗掉,并且会更新 pro-sqltiptree 目录下的CHANGELOG.md文件、package.json文件,以及相关项目间的依赖引用也会更新(pnpm会针对变化执行一次install操作,更新引用,使各项目中的版本保持一致),就很酷😎~

monorepo-2.png

第三步

执行 changeset publish命令,发布npm包,这里我为了测试,直接使用 pnpm run release:nobuild(跳过构建打包步骤)

在执行发布命令之前,你应该已经通过 npm login 进行了登录操作

monorepo-3.gif

第四步

在npm查看更新

monorepo-4.png

文档站点

说了那么多,很多都是代码层面的东西。那么对于用户来说,如何能够快速的了解本项目呢?以及如何快速上手和使用相关组件和工具呢?

那么,一个成熟的开发文档就显得相当重要了,他不仅要起到介绍、引导入门的作用,还应该有其作为一个技术文档网站的核心功能——详细的API介绍和使用说明、真实使用场景下的渲染demo等。

文档网站——其功能性和重要性不言而喻,主要作为第一入口提供给用户,作为了解本项目的第一途径。

  1. 第一要素:基本内容应包含,使用指南,组件 API、属性、方法等相关介绍说明等。
  2. 第二点:使用方式和用法展示。真实渲染环境展示效果,相关代码使用展示(demo / code)。
  3. 在项目代码层面可能会涉及到与约定目录的自动化脚本生成相关,进而快速自动实现如:路由、API 等相关功能。

在最开始搭建文档网站时,参考了element ui的官网设计,觉得很不错,不过,在看了源码后发现其网站是是有自己的element组件从零开始搭建的,而且,针对Markdown文件的渲染也定制开发了一些特殊逻辑,整体思考下来觉得上手成本有点高,我这一时半会也没那么多时间搞。

想了想既然以文档为主,还是选择一个文档类框架进行搭建吧。为了追求快速方便,首先,想到的就是vue2版本的vuepress(vue2相关生态的许多网站都是使用的这个框架),Markdown类型文件可以直接进行渲染,框架的约定式路由等都使得上手极其方便,也有比较成熟的插件社区,因此,我也是最终选择了这个框架。

多说两句,期间我也对比研究过 vitepress,顾名思义,该框架是以 vite为核心,且内部集成基于 vue3,可定制化极强,性能表现非常优异,现在很多开源项目都采用了这个框架进行文档网站的搭建,element-plus就是很好的例子。

vuepress2我也研究了一下,它主要是基于vue3的,对vue2支持并不友好,因此,非vue3的项目可能要退而求其次使用vuepress了~

vuepress文档站点的搭建直接按照官方文档来即可,非常简单,这里不再啰嗦了。

完事之后,脚本应该是这样的:

"scripts": {
  "dev": "vuepress dev",
  "build": "vuepress build"
}

结合项目,指出一些需要注意的点

  • 文档目录不作为项目包模块管理,因此不放在packages目录下,而是同级创建一个单独的目录 docs进行管理
    • Markdown文件的渲染,最好能够支持API的自动填写
    • 组件能够在文档网站中正常渲染(可以结合官方提供的这个功能在 Markdown 中使用 Vue
  • 同样提供一个 examples目录用于模拟调试真实使用场景下的组件功能,也作为一个同级目录创建

examples在这里的作用,主要作为实际vue项目的使用场景,从而用于调试代码

├── examples                        // 演示目录,用于调试代码
├── docs                            // 文档目录,用于编写对应组件的相关文档,以及文档网站的建设配置
├── packages                        // packages工作目录

vuepress文档站点的配置

在初始化完文档站点之后,我们根据需要把子项目作为依赖安装到项目中:

"dependencies": {
  "@ah-ailpha/components": "workspace:*",
  "@ah-ailpha/pro-sqltiptree": "workspace:*",
}

因为版本较低,如果出现Webpack的版本报错,请在 package.json 中添加如下配置:

"resolutions": {
  "webpack-hot-middleware": "2.25.0"
}

docs/.vuepress/enhanceApp.js中导入我们的组件:

由于 VuePress 是一个标准的 Vue 应用,你可以通过创建一个 .vuepress/enhanceApp.js 文件来做一些应用级别的配置,当该文件存在的时候,会被导入到应用内部。enhanceApp.js 应该 export default 一个钩子函数,并接受一个包含了一些应用级别属性的对象作为参数。你可以使用这个钩子来安装一些附加的 Vue 插件、注册全局组件,或者增加额外的路由钩子等:

import vue from 'vue'
import components from '@ah-ailpha/components'
import ProSqlTipTree from '@ah-ailpha/pro-sqltiptree'

export default ({
  Vue,
  // options,
  // router,
  // siteData,
}) => {
  Vue.use(components)
  Vue.use(ProSqlTipTree)
}

为了能够在Markdown中以真实效果渲染组件,我选择使用一个开源的插件来实现这个能力:

  1. 先安装插件

    pnpm add vuepress-plugin-demo-container --filter docs -D
    
  2. docs/.vuepress/config.js中注册插件

    module.exports = {
      plugins: ['demo-container'],
    }
    
  3. 在Markdown文件中使用

monorepo-5.png

```md
::: demo

这里的代码会被渲染

:::
```

配置文件与脚本调整

工作空间 pnpm-workspace.yaml 做出如下调整:

packages:
  - packages/*
  - docs
  - examples

主目录 package.json 文件增加以下脚本:

"scripts": {
  "dev": "pnpm run -C examples serve",
  "docs:dev": "pnpm run -C docs dev",
  "docs:build": "pnpm run -C docs build",
}

文档站点基本效果

monorepo-6.png


monorepo-7.png

文档站点搭建小结

如果想要快速、简单的搭建一个文档站点,其实还是相当简单的,市面上有很多针对不同技术方案的框架。但是,在使用的过程中,也发现了不少问题

  • 技术框架的选择和核心库的版本关系很大,需要考虑版本区别
  • 一键快速搭建的文档站点很难满足我们的复杂需求,需要自己提供插件
  • vuepress不如vitepress那样提供有很多钩子方便拓展能力边界
  • 如果完全以自身开发的组件搭建一个文档网站需要一定的时间成本

总的来说,目前的文档站点还是属于一种快速完工的初级阶段水平,对于后面的自动化文档生成还有一定的路要走。按照设想,只要约定好规范,所有的组件文档接口及使用说明,也同步维护在源码目录下的 demo 目录中,再结合一些自定义插件,比如配合Webpack可以通过编写loader在编译的时候做一些处理,vitepress的话可以使用钩子函数做一些处理,目前市面上也有不少成熟方案都值得学习,期待在接下来的开发过程中进一步完善。

总结

其实,对于本篇文章来说,算是鸽了挺长一段时间的了,惭愧😅~

在第一篇文章中我主要介绍了项目的一些前期设计和技术预研,以及如何进行最终技术方案确定的,属于是一种由浅入深的心路历程了,对于新人了解整个项目的脉络还是十分有益的。同样是站在项目任务目标的角度看,为了新人能够快速上手,这里选择了从0到1对整个项目生命周期进行说明,并通过这两篇文章进行阐述,也是尽力通过结合自身的实际经历做到全面而细致的描述,以达到一个内期待心的理想状态~

在整个开发过程中,就相当于一个学习的过程,期间遇到不少问题,但这也是提升自己的好时机,解决问题、记录下来、再接再厉,保持这样一个曲线应当是挺不错的。

通过这两篇文章,你应该能够掌握以下能力:

  1. 了解和知晓组件化,并能够明白其作用
  2. 了解工程化规范、组件化规范,并能够把标准实际落地
  3. 了解工作空间(workspace)概念、了解什么是monorepo,并能够使用pnpm管理多包项目
  4. 能够从0到1搭建一个属于自己的monorepo多包项目
  5. 会使用 vuepress 搭建博客文档站点(你甚至可以用它搭建自己的博客,比如我的个人博客
  6. 会使用 changesets 管理 monorepo 多包项目
  7. 了解npm的发包流程,并会使用npm发布自己的包

6.jpg

仓库地址

https://github.com/JS-banana/jack

资料

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
相关推荐
  • 暂无相关文章
  • 评论 抢沙发
    头像
    欢迎您留下宝贵的见解!
    提交
    头像

    昵称

    取消
    昵称表情代码图片

      暂无评论内容