前言
在上一篇的前端组件化方案探究中,我们研究了什么是组件化以及我们为什么需要组件化。也调研和测试了一些开源项目,并且在使用、学习、研究、对比之后最终确定了以 pnpm
+ workspace
+ changeset
为技术方向的 monorepo 多包管理方案。
- 本文主要是沿着该路线进行项目落地,是一篇聚焦于实战的文章。
- 文章的目标是能够让未接触过
monorepo
架构的新手也能从0到1的完成一个多包管理的前端组件化项目
细致一点的说,我们主要从这几个方面入手:
monorepo
项目架构的搭建- 整个项目的工程化规范搭建
- 子项目的配置和构建配置,包括出入口管理、
webpack
打包 - 文档站点的搭建
- 版本管理和发布
使用 pnpm 快速搭建一个 monorepo 项目
在上一篇文章中我们描述了什么是monorepo
,以及pnpm
的workspace
工作空间概念,简单理解的话就是两个方面:
- 如何通过一个工程管理多个项目的依赖
- 如何通过一个工程管理多个项目间的相互依赖
- 如何通过一个工程管理多个项目的版本
这里,我们主要围绕这几个核心思想展开工作。
创建主项目
首先,我们先创建一个主项目,在命令行中执行:
mkdir my-monorepo-app && cd my-monorepo-app
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
,那么核心库 vue
、vue-router
基本都会用到,以及一些通用工具,如 lodash
、dayjs
等,此时,就可以采用全局安装的方式,这样做的好处是所有的子项目都可以直接引入使用(此时的依赖是安装在主目录下的 node_modules
中的)。
如果全局中没有依赖,而子项目中有相同的依赖,你也不用担心,pnpm也会自动优化处理,通过软连接的方式安装一次就可以,可以实现最小化安装产物,非常的好用😎~
pnpm i vue@^2.6.11 dayjs -Dw
复制代码
pnpm i
与pnpm 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/*
下面的子项目。
如下,我们需要安装 iview
到 components
子项目中,那么,标准的做法是:
-
先查看
packages/components/package.json
中的name
字段,把它作为filter
的作用域条件// components 子项目的目录结构 ├── packages // 子项目目录 ├── components // UI基础组件 ├── package.json // name: @ah-ailpha/components 复制代码
-
然后,在根目录下执行安装命令,并指定
filter
# 安装 iview 到 components子项目中 pnpm add iview --filter @ah-ailpha/components 复制代码
-
查看项目依赖变化
这个时候你应该可以发现
components
子项目的package.json
文件的dependencies
属性是变化了的,而且应当只有这个子项目是受影响的。删除等操作命令保持一致,限定作用域即可,如:pnpm rm iview --filter @ah-ailpha/components
{ "dependencies": { "iview": "^3.5.4" } } 复制代码
3. 子项目互相作为依赖
作为 monorepo 管理的项目,这一步的功能才是最为核心的(🙋♂️敲黑板啦!!!)。
以标准的组件库项目为例:
- 我们一般会把组件类代码单独做为一个子项目进行管理
- 文档网站也会作为一个子项目单独管理
- 工具类也会作为一个子项目单独管理
- 以及可能根据个人业务需求还有其他功能包(cli等等)
但是这些子项目又不是完全独立的,他们之间存在着一些相互依赖关系,如:
- 组件代码会依赖于工具类的一些方法
- 文档网站会依赖于组件和工具类(我的UI文档网站肯定用的是我自己的UI组件😄)
最终他们都会作为一个独立的模块提供给用户,因此,从使用者的角度考虑,我们最好也保持这种使用方式——npm安装包的形式(import utils from '@an-ailpha/utils'
)。
那么,当我把其中一个子项目作为另一个项目的依赖时,如何实现我在本地调试开发的时候,能够做到类似安装npm远程包
一样使用呢(而不是通过路径引用去使用,这样做既不优雅也不方便,当我们发布之后,路径引用就会失效)?以及如何保证我所引用的依赖都是最新的、实时的呢?因为我不可能没调整一点代码就重新发个npm
包,然后子项目再重新下载一遍,以这种方式验证吧。
令人高兴的是,pnpm
的 workspace 工作空间的功能可以完全满足我们的需求,它可以实现这种效果。
如:子项目packages/components
需要使用packages/utils
子项目作为项目依赖时,我们直接按照 npm
安装依赖的逻辑使用即可✅
目录结构:
├── packages // 子项目目录
├── components // UI基础组件
├── package.json // name: @ah-ailpha/components
├── utils // 工具类
├── package.json // name: @ah-ailpha/utils
复制代码
-
查看
packages/utils/package.json
、packages/components/package.json
中的name
字段 -
然后,在根目录下执行安装命令,并指定
filter
# 把子模块 packages/utils 当成依赖安装到 packages/components pnpm add @ah-ailpha/utils --filter @ah-ailpha/components 复制代码
-
查看依赖变化
这个时候你应该可以发现
components
子项目的package.json
文件的dependencies
属性是变化了的:{ "dependencies": { "@ah-ailpha/utils": "workspace:^1.0.0", "iview": "^3.5.4" } } 复制代码
-
"@ah-ailpha/utils": "workspace:^1.0.0"
,这是pnpm
的workspace
工作空间协议写法
它实现的效果是:当utils子项目中的代码更新后,你在 components 中引用的代码也一定是最新的,你可以理解为他俩已经形成了绑定关系,你变我就变~(版本更新时也不用担心,具体可以看下面的版本管理部分的详细说明)
-
需要指出的是,当你在执行发布操作之后,
npm
包中的版本会被自动更新为指定目录下package.json
的version
值
如果,此时
utils/package.json
中的version
值为1.1.0
,那么上面的"@ah-ailpha/utils": "workspace:^1.0.0"
会自动更新为"@ah-ailpha/utils": "^1.1.0"
不过,在实际开发中,我们一般会使用另一种写法,请继续往下看~🧐
-
简单说说npm版本规范
从上面的安装依赖中,我们可以发现npm
中的一些版本规则:^
,这也是我们比较常见的,npm
的默认按照逻辑是按照这个规则执行的,其他前缀还有:~
、*
,那么它们是什么意思呢?作用又是什么呢?
-
我们先了解下版本格式:
主版本号.次版本号.修订号
-
版本号递增规则如下:
-
主版本号:当你做了不兼容的 API 修改,
-
次版本号:当你做了向下兼容的功能性新增,
-
修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。
比如,我们经常在开源项目中见到的:alpha、beta 、rc
-
这里不再做过多赘述,感兴趣的可以看这里 语义化版本号标准
-
npm 的版本匹配策略
^1.0.1
:只要主版本一致都匹配(1.x),如:1
、1.x
~1.0.1
:只要主版本和次版本一致都匹配(1.1.x),如:1.1
、1.1.x
*
:全匹配,不受版本号影响,可以命中一切新发布的版本号
不过,就针对我们这个项目来说,它的workspace
协议也是符合npm
版本规范的,我们子项目内的引用也可以采用对应的匹配策略。
但是,不同于用户使用,我们各子项目为了方便不受版本影响,从而做到匹配一致,一般采用*
全匹配策略,从开发调试的角度考虑,各子项目间的依赖引用应该都是最新的代码,这一点是需要清楚的。
你可以这样安装使用:
pnpm add @ah-ailpha/utils@* --filter @ah-ailpha/components
复制代码
然后,依赖会变成这样:
{
"dependencies": {
"@ah-ailpha/utils": "workspace:*",
}
}
复制代码
目前,这种写法也是子项目间依赖引用比较推荐的,依赖关系不受版本影响,可以做到相对统一。
小结
- 所有的命令操作都是在主目录下进行的,用户视角应始终在主目录下才对
- 各子项目的
package.json
的name
应该符合该规范——@ah-ailpha/components
- 这样做的好处之一是可以帮我们统一作用域前缀
- 其次,当我们发布npm包时,
@ah-ailpha
会作为npm的一个组织域使用
- 子项目内部引用使用npm
*
全匹配策略
monorepo项目的工程化搭建
一个基本标准就是:所有的规范配置都应以主目录为起点开始。
目前工程规范有:eslint
、prettier
、markdownlint
、commitlint
、typescript
为什么在vue2的项目中引入了typescript
我们都知道vue2
版本对ts
的支持很一般,相关生态库大多也都是js
开发的,包括iview
也是使用js
开发的;那么,我这里为什么依旧采用了对ts
的支持呢?
- 最核心的一点,当下前端生态
TS
正大步向前,我之前也一直在使用TS
开发,现在不能因为vue2
而就放弃了,还是应该以积极拥抱的心态 - 本项目架构支持子项目采用不同的规范策略,以
vue2
为主的components
子项目完全可以在本目录下配置一套自己的项目规则,进而覆盖主项目配置,那么,现在可以做到的是, - 本项目不是单一的
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的相关实现,值得学习。
前人栽树我乘凉😬
在配置 eslint
、prettier
经常踩的坑就是因为版本问题,这里并不推荐从零开始,我们完全可以找一些和自己风格倾向比较一致的开源项目,借鉴别人项目中配置好的依赖,使用到我们自己的项目中😏。
只有在我们想定义自己的特殊规范时,这时才需要结合官方文档了解规则配置的各种参数,以及相关规则包的一些插件库,按需安装配置即可,这样可以节省大量时间成本,毕竟很多开源项目都是极其成熟的了(这里也需要测试版本,默认安装一般都是基于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 的错误
}
复制代码
为了不使 eslint
和prettier
的规则发生冲突,你应该在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"
]
}
复制代码
eslint
、prettier
、commitlint
这些都是很常规的配置,这里就不在过多叙述了,仓库地址放在下面了~
包构建
包构建的基本思路依然是按照当前成熟方案进行选择,vue2
生态的组件类代码优先使用 Webpack
进行构建打包,并且要支持ES Module
这种引入方式,方便为之后的按需引入做铺垫,当然,为了兜底的兼容,也要支持 umd
这种方式(同时以 AMD
、CommonJS2
和全局属性
形式输出,支持直接使用 <script>
在 HTML
中引入)。
我在搭建本项目的vue2
+ iview
的组件库时,碍于版本的影响,子项目相关全部采用 iview
项目中的构建配置进行打包,这样可以避免大量Webpack
配置时的版本问题。
而对于 utils
这种不受vue2
影响的独立库,则优先采用TypeScript
+ Rollup
这种更方便库模式的方式进行构建,使得其能够对上下做到很好的支持(js
、ts
)。
统一出入口
每个组件都应该以 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
可以做到更全面的自动化操作😋
版本管理和包发布
Changesets 是一个用于 Monorepo 项目下版本以及 Changelog 文件管理的工具。
本项目目前使用的是 changeset进行多包版本管理,它拥有交互式 CLI,可以非常友好的帮我们确定包版本 => 生成相应 CHANGELOG.md
日志,并更新 package.json
的 version
信息,然后一键发布包至 npm
Changesets的实战使用细节可以参考这篇文章:Changesets: 流行的 monorepo 场景发包工具
-
changeset init
- 新项目执行该命令,完成对项目的初始化
- 会在根目录下生成
.changeset
目录,config.json
配置文件(注意分支"baseBranch": "master"
)
-
changeset
- 执行该命令,进行版本管理,会交互式选择不同项目,以及确定发布的版本
- 会生成一些
.md
文件在目录下,会在version
的时候消耗
-
changeset version
- 消耗上一步生成的相关的一些版本信息及记录内容的
.md
文件,并生成或更新CHANGELOG.md
文件,之后.md
文件会被自动删除 - 相应的
package.json
中的version
信息也会同步更新
- 消耗上一步生成的相关的一些版本信息及记录内容的
-
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
命令,通过交互式命令确定版本(空格——选择,回车——确定)
此时,我们选择为 1.5.0版本,执行结束,.changeset
目录下回生成一个临时文件,这是给第二步命令使用的,我们不用管它~
第二步:
执行 pnpm run version
命令后,临时文件会被消耗掉,并且会更新 pro-sqltiptree
目录下的CHANGELOG.md
文件、package.json
文件,以及相关项目间的依赖引用也会更新(pnpm会针对变化执行一次install操作,更新引用,使各项目中的版本保持一致),就很酷😎~
第三步:
执行 changeset publish
命令,发布npm包,这里我为了测试,直接使用 pnpm run release:nobuild
(跳过构建打包步骤)
在执行发布命令之前,你应该已经通过
npm login
进行了登录操作
第四步:
在npm查看更新
文档站点
说了那么多,很多都是代码层面的东西。那么对于用户来说,如何能够快速的了解本项目呢?以及如何快速上手和使用相关组件和工具呢?
那么,一个成熟的开发文档就显得相当重要了,他不仅要起到介绍、引导入门的作用,还应该有其作为一个技术文档网站的核心功能——详细的API介绍和使用说明、真实使用场景下的渲染demo等。
文档网站——其功能性和重要性不言而喻,主要作为第一入口提供给用户,作为了解本项目的第一途径。
- 第一要素:基本内容应包含,使用指南,组件 API、属性、方法等相关介绍说明等。
- 第二点:使用方式和用法展示。真实渲染环境展示效果,相关代码使用展示(demo / code)。
- 在项目代码层面可能会涉及到与约定目录的自动化脚本生成相关,进而快速自动实现如:路由、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中以真实效果渲染组件,我选择使用一个开源的插件来实现这个能力:
-
先安装插件
pnpm add vuepress-plugin-demo-container --filter docs -D 复制代码
-
在
docs/.vuepress/config.js
中注册插件module.exports = { plugins: ['demo-container'], } 复制代码
-
在Markdown文件中使用
```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",
}
复制代码
文档站点基本效果
文档站点搭建小结
如果想要快速、简单的搭建一个文档站点,其实还是相当简单的,市面上有很多针对不同技术方案的框架。但是,在使用的过程中,也发现了不少问题
- 技术框架的选择和核心库的版本关系很大,需要考虑版本区别
- 一键快速搭建的文档站点很难满足我们的复杂需求,需要自己提供插件
vuepress
不如vitepress
那样提供有很多钩子方便拓展能力边界- 如果完全以自身开发的组件搭建一个文档网站需要一定的时间成本
总的来说,目前的文档站点还是属于一种快速完工的初级阶段水平,对于后面的自动化文档生成还有一定的路要走。按照设想,只要约定好规范,所有的组件文档接口及使用说明,也同步维护在源码目录下的 demo
目录中,再结合一些自定义插件,比如配合Webpack
可以通过编写loader
在编译的时候做一些处理,vitepress
的话可以使用钩子函数做一些处理,目前市面上也有不少成熟方案都值得学习,期待在接下来的开发过程中进一步完善。
总结
其实,对于本篇文章来说,算是鸽了挺长一段时间的了,惭愧😅~
在第一篇文章中我主要介绍了项目的一些前期设计和技术预研,以及如何进行最终技术方案确定的,属于是一种由浅入深的心路历程了,对于新人了解整个项目的脉络还是十分有益的。同样是站在项目任务目标的角度看,为了新人能够快速上手,这里选择了从0到1对整个项目生命周期进行说明,并通过这两篇文章进行阐述,也是尽力通过结合自身的实际经历做到全面而细致的描述,以达到一个内期待心的理想状态~
在整个开发过程中,就相当于一个学习的过程,期间遇到不少问题,但这也是提升自己的好时机,解决问题、记录下来、再接再厉,保持这样一个曲线应当是挺不错的。
通过这两篇文章,你应该能够掌握以下能力:
- 了解和知晓组件化,并能够明白其作用
- 了解工程化规范、组件化规范,并能够把标准实际落地
- 了解工作空间(
workspace
)概念、了解什么是monorepo,并能够使用pnpm
管理多包项目 - 能够从0到1搭建一个属于自己的monorepo多包项目
- 会使用 vuepress 搭建博客文档站点(你甚至可以用它搭建自己的博客,比如我的个人博客)
- 会使用 changesets 管理 monorepo 多包项目
- 了解
npm
的发包流程,并会使用npm
发布自己的包