共计 18021 个字符,预计需要花费 46 分钟才能阅读完成。
一、背景
随着 vivo 悟空流动中台流动组件越来越多,流动中台开发的小伙伴们愈发的感知到咱们短少一个能够积淀通用能力,晋升代码复用性的组件库。在这个指标根底之上诞生了 acitivity-components,然而随着组件的抽离增多,在和上下游沟通时,发现公共组件对于经营、产品、测试同学来说都是黑盒,只有开发本人晓得积淀了哪些能力,业务上哪些模块进行了抽取。同时,在对外赋能时,也短少了一个平台出现咱们抽离的组件,基于此指标,开发小伙伴们开始构思插件治理平台的开发计划。
二、平台架构
2.1 技术选型
在平台开发之初通过小伙伴们一起沟通确定了 Midway+Vue+MySQL 技术栈,并实现对应的架构梳理,在做 Node 层框架选型时,咱们抉择了 Midway 做为 Node 层的开发框架,起因次要有以下几点:
- 基于 egg.js — Midway 能够很好的兼容 egg.js 的插件生态。
- 依赖注入(loC)– loC 全名叫做管制反转(Inversion of Control,缩写为 loC),是面向对象的一种设计模式,能够用来升高代码之间的耦合度,实现了高内聚,弱耦合的架构指标。
- 更好的 Typescript 反对 — 因为 Midway 应用的 ts 开发,所以在我的项目开发过程中,咱们能够间接应用 ts,利用 ts 的动态类型查看、装璜器等能力,晋升咱们的代码健壮性和开发效率。
2.2 架构拆解
首先咱们来看一下插件治理平台的架构图:
通过对平台整体架构图的梳理,构建了整个平台开发的基本思路:
- 组件抽离 — 从建站的组件在往下一层,抽取更根底的组件内容,并集中托管至 activity-components。
- md 生成 — 所有的组件都须要对外输入,activity-components 内须要做一层编译操作,每个组件须要自动化生成对应的 md 文档。
- gitlab hooks — 如何保障 server 端对 activity-components 的变更都能及时响应,保障组件都是最新的,此处应用了 gitlab 集成中的 push events 监听组件的 push 操作。
- npm 近程加载 — 平台须要具备近程拉取 npm 包能力,并解压缩对应的包,将 activity-components 源文件获取到。
- Vue 全家桶应用 — 平台 web 端引入 Vue 全家桶,利用动静路由对各个组件进行匹配。
- 单组件预览 — 抽离的组件底层存在对建站能力的依赖,此处须要将建站的编辑页进行拆解,集成建站底层能力,实现对 activity-components 的组件预览。
- 文件服务 — 具备公共组件策动文档的上传能力,不便经营和产品对公共组件的接入。
三、重点技术详解
在平台的整体搭建开发过程中,梳理了以下技术点内容,进行重点介绍。
3.1 组件抽离
首先能够看一下 activity-components 组件库 package.json 内容:
{
"name": "@wukong/activity-components",
"version": "1.0.6",
"description": "流动公共组件库",
"scripts": {
"map": "node ./tool/map-components.js",
"doc": "node ./tool/create-doc.js",
"prepublish": "npm run map && npm run doc"
}
}
通过 scripts 外面配置的指令,能够看到,在组件做 publish 操作时,咱们利用了 npm 的 pre 事件钩子,实现组件本身的第一层编译操作,map-components 次要用于实现对组件的文件目录进行遍历,导出所有的组件内容。
文件目录构造如下:
|-src
|--base-components
|---CommonDialog
|---***
|--wap-components
|---ConfirmDialog
|---***
|--web-components
|---WinnerList
|---***
|-tool
|--create-doc.js
|--map-components.js
map-components 次要实现对文件目录的遍历操作;
// 深度遍历目录
const deepMapDir = (rootPath, name, cb) => {const list = fse.readdirSync(rootPath)
list.forEach((targetPath) => {const fullPath = path.join(rootPath, targetPath)
// 解析文件夹
const stat = fse.lstatSync(fullPath)
if (stat.isDirectory()) {
// 如果是文件夹,则持续向下遍历
deepMapDir(fullPath, targetPath, cb)
} else if (targetPath === 'index.vue') {
// 如果是文件
if (typeof cb === 'function') {cb(rootPath, path.relative('./src', fullPath), name)
}
}
})
}
***
***
***
// 拼接文件内容
const file = `
${components.map(c => `export { default as ${c.name} } from './${c.path}'`).join('\n')}
`
// 文件输入
try {fse.outputFile(path.join(__dirname, '..', pkgJson.main), file)
} catch (e) {console.log(e)
}
在做文件遍历时,咱们采纳了递归函数,保障咱们对以后的文件目录做到彻底遍历,将所有的组件全副找出,通过这段代码,能够看到,定义的组件须要有一个 index.vue 组件作为检索的入口文件,找寻到这个组件之后,咱们就会进行向下寻找,并将以后的组件目录解析进去,具体流程如下图。
导出文件内容如下:
export {default as CommonDialog} from './base-components/CommonDialog/index.vue'
export {default as Login} from './base-components/Login/index.vue'
export {default as ScrollReach} from './base-components/ScrollReach/index.vue'
export {default as Test} from './base-components/Test/index.vue'
***
通过上述一系列操作,对立对外的目录文件生成,组件抽离只须要失常往组件库增加即可。
3.2 Markdown 文件自动化生成
生成了组件目录之后,对应的组件阐明文档该如何生成呢,此处咱们援用同核心另一位共事冯镝同学开发的 vue-doc (opens new window),实现对应 Vue 组件 md 文档自动化生成,首先来看一下定义的 doc 指令。
"doc": "node ./tool/create-doc.js"
***
create-doc.js
const {singleVueDocSync} = require('@vivo/vue-doc')
const outputPath = path.join(__dirname, '..', 'doc', mdPath)
singleVueDocSync(path.join(fullPath, 'index.vue'), {
outputType: 'md',
outputPath
})
通过 Vue-doc 裸露的 singleVueDocSync 办法,在 server 端根目录下会新建一个 doc 文件夹,这外面会依据组件的目录构造生成对应的组件 md 文档,此时 doc 的目录构造为:
|-doc
|--base-components
|---CommonDialog
|----index.md
|---***
|--wap-components
|---ConfirmDialog
|----index.md
|---***
|--web-components
|---WinnerList
|----index.md
|---***
|-src
|--**
通过这个文件目录能够看到,依据组件库的目录构造,在 doc 文件夹下生成同样目录构造的 md 文件,至此每个组件的 md 文档都曾经生成了,然而只到这一步是不够的。
咱们还须要将以后的 md 文档进行整合,通过一个 json 文件表述进去,因为插件治理平台是须要解析到这个 json 文件并将其做为返回内容至 web 端,实现前端页面的渲染,基于此指标咱们写了以下代码:
const cheerio = require('cheerio')
const marked = require('marked')
const info = {timestamp: Date.now(),
list: []}
***
let cname = cheerio.load(marked(fse.readFileSync(outputPath).toString()))
info.list.push({
name,
md: marked(fse.readFileSync(outputPath).toString()),
fullPath: convertPath(outputPath),
path: convertPath(path.join('doc', mdPath)),
cname: cname('p').text()})
***
// 生成对应的组件数据文件
fse.writeJsonSync(path.resolve('./doc/data.json'), info, {spaces: 2})
这里引入两个比拟重要的库一个是 cheerio,一个是 marked。cheerio 是 jquery 外围性能的一个疾速灵便而又简洁的实现,次要是为了用在服务器端须要对 DOM 进行操作的中央,marked 次要是将 md 文档转换为 html 的文档格局,实现上述代码编写之后,咱们在 doc 目录下生成一个 data.json 文件,具体内容如下:
{
"timestamp": 1628846618611,
"list": [
{
"name": "CommonDialog",
"md": "<h1 id=\"commondialog\">CommonDialog</h1>\n<h3 id=\" 组件介绍 \"> 组件介绍 </h3>\n<blockquote>\n<p> 通用根底弹框 </p>\n</blockquote>\n<h2 id=\" 属性 -attributes\"> 属性 -Attributes</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\"> 参数 </th>\n<th align=\"center\"> 阐明 </th>\n<th align=\"center\"> 类型 </th>\n<th align=\"center\"> 默认值 </th>\n<th align=\"center\"> 必须 </th>\n<th align=\"center\">sync</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">maskZIndex</td>\n<td align=\"center\"> 弹框的 z -index 层级 </td>\n<td align=\"center\">Number</td>\n<td align=\"center\">1000</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">bgStyle</td>\n<td align=\"center\"> 背景款式 </td>\n<td align=\"center\">Object</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">closeBtnPos</td>\n<td align=\"center\"> 敞开按钮的地位 </td>\n<td align=\"center\">String</td>\n<td align=\"center\">top-right</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">showCloseBtn</td>\n<td align=\"center\"> 是否展现敞开按钮 </td>\n<td align=\"center\">Boolean</td>\n<td align=\"center\">true</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">v-model</td>\n<td align=\"center\"> 是否展现弹框 </td>\n<td align=\"center\">Boolean</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n</tbody></table>\n<h2 id=\" 事件 -events\"> 事件 -Events</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\"> 事件名 </th>\n<th align=\"center\"> 阐明 </th>\n<th align=\"center\"> 参数 </th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">input</td>\n<td align=\"center\">-</td>\n<td align=\"center\"></td>\n</tr>\n<tr>\n<td align=\"center\">close</td>\n<td align=\"center\"> 弹框敞开事件 </td>\n<td align=\"center\"></td>\n</tr>\n</tbody></table>\n<h2 id=\" 插槽 -slots\"> 插槽 -Slots</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\"> 名称 </th>\n<th align=\"center\"> 阐明 </th>\n<th align=\"center\">scope</th>\n<th align=\"center\">content</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">default</td>\n<td align=\"center\"> 弹框内容 </td>\n<td align=\"center\"></td>\n<td align=\"center\">-</td>\n</tr>\n</tbody></table>\n",
"fullPath": "/F/ 我的我的项目 / 公共组件 /activity-components/doc/base-components/CommonDialog/index.md",
"path": "doc/base-components/CommonDialog/index.md",
"cname": "通用根底弹框"
}, {
"name": "ConfirmDialog",
"md": "<h1 id=\"confirmdialog\">ConfirmDialog</h1>\n<h3 id=\" 组件介绍 \"> 组件介绍 </h3>\n<blockquote>\n<p> 确认弹框 </p>\n</blockquote>\n<h2 id=\" 属性 -attributes\"> 属性 -Attributes</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\"> 参数 </th>\n<th align=\"center\"> 阐明 </th>\n<th align=\"center\"> 类型 </th>\n<th align=\"center\"> 默认值 </th>\n<th align=\"center\"> 必须 </th>\n<th align=\"center\">sync</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">bgStyle</td>\n<td align=\"center\"> 背景款式 </td>\n<td align=\"center\">Object</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">maskZIndex</td>\n<td align=\"center\"> 弹框层级 </td>\n<td align=\"center\">Number</td>\n<td align=\"center\">1000</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">v-model</td>\n<td align=\"center\"> 弹框展现状态 </td>\n<td align=\"center\">Boolean</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">title</td>\n<td align=\"center\"> 弹框题目文案 </td>\n<td align=\"center\">String</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">titleColor</td>\n<td align=\"center\"> 题目色彩 </td>\n<td align=\"center\">String</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">leftTitle</td>\n<td align=\"center\"> 左按钮文案 </td>\n<td align=\"center\">String</td>\n<td align=\"center\"> 勾销 </td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">rightTitle</td>\n<td align=\"center\"> 右按钮文案 </td>\n<td align=\"center\">String</td>\n<td align=\"center\"> 确定 </td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">leftBtnStyle</td>\n<td align=\"center\"> 左按钮款式 </td>\n<td align=\"center\">Object</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">rightBtnStyle</td>\n<td align=\"center\"> 右按钮款式 </td>\n<td align=\"center\">Object</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n</tbody></table>\n<h2 id=\" 事件 -events\"> 事件 -Events</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\"> 事件名 </th>\n<th align=\"center\"> 阐明 </th>\n<th align=\"center\"> 参数 </th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">cancel</td>\n<td align=\"center\"> 左按钮点击触发 </td>\n<td align=\"center\"></td>\n</tr>\n<tr>\n<td align=\"center\">confirm</td>\n<td align=\"center\"> 右按钮点击触发 </td>\n<td align=\"center\"></td>\n</tr>\n<tr>\n<td align=\"center\">close</td>\n<td align=\"center\"> 弹框敞开事件 </td>\n<td align=\"center\"></td>\n</tr>\n<tr>\n<td align=\"center\">input</td>\n<td align=\"center\">-</td>\n<td align=\"center\"></td>\n</tr>\n</tbody></table>\n",
"fullPath": "/F/ 我的我的项目 / 公共组件 /activity-components/doc/wap-components/ConfirmDialog/index.md",
"path": "doc/wap-components/ConfirmDialog/index.md",
"cname": "确认弹框"
}, {
"name": "WinnerList",
"md": "<h1 id=\"winnerlist\">WinnerList</h1>\n<h3 id=\" 组件介绍 \"> 组件介绍 </h3>\n<blockquote>\n<p> 中奖列表 </p>\n</blockquote>\n<h2 id=\" 属性 -attributes\"> 属性 -Attributes</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\"> 参数 </th>\n<th align=\"center\"> 阐明 </th>\n<th align=\"center\"> 类型 </th>\n<th align=\"center\"> 默认值 </th>\n<th align=\"center\"> 必须 </th>\n<th align=\"center\">sync</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">item</td>\n<td align=\"center\">-</td>\n<td align=\"center\"></td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 是 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">prodHost</td>\n<td align=\"center\">-</td>\n<td align=\"center\">String</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 是 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">prizeTypeOptions</td>\n<td align=\"center\">-</td>\n<td align=\"center\">Array</td>\n<td align=\"center\">-</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">isOrder</td>\n<td align=\"center\">-</td>\n<td align=\"center\">Boolean</td>\n<td align=\"center\">true</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">listUrl</td>\n<td align=\"center\">-</td>\n<td align=\"center\">String</td>\n<td align=\"center\">/wukongcfg/config/activity/reward/got/list</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n<tr>\n<td align=\"center\">exportUrl</td>\n<td align=\"center\">-</td>\n<td align=\"center\">String</td>\n<td align=\"center\">/wukongcfg/config/activity/reward/export</td>\n<td align=\"center\"> 否 </td>\n<td align=\"center\"> 否 </td>\n</tr>\n</tbody></table>\n",
"fullPath": "/F/ 我的我的项目 / 公共组件 /activity-components/doc/web-components/WinnerList/index.md",
"path": "doc/web-components/WinnerList/index.md",
"cname": "中奖列表"
}]
}
至此咱们就在 activity-components 侧实现了对组件的 md 文档自动化生成。
通过这张图咱们能够清晰的抓取到底层组件中的要害信息,例如:组件反对的属性和事件。
3.3 gitlab hooks
在平台开发的过程中,组件每次做 gitlab 提交时,平台是无奈感知组件库的代码产生了变动,于是咱们开始调研,在对 gitlab 的 api 进行搜寻时,发现 gitlab 曾经提供了集成的解决方案。
在实现对应 url 和 secret Token 配置之后,点击 save changes 会生成如下图所示内容:
此时曾经实现根本的 push events 配置,接下来须要在插件治理平台 server 端实现对应的接口开发。
@provide()
@controller('/api/gitlab')
export class GitlabController {@inject()
ctx: Context;
@post('/push')
async push(): Promise<void> {
try {const event = this.ctx.headers['x-gitlab-event'];
const token = this.ctx.headers['x-gitlab-token'];
// 判断 token 是否正确
if (token === this.ctx.app.config.gitlab.token) {switch (event) {
case 'Push Hook':
// do something
const name = 'activity-components';
const npmInfo = await this.ctx.service.activity.getNpmInfo(`@wukong/${name}`);
await this.ctx.service.activity.getPkg(name, npmInfo.data.latest.version);
break;
}
}
this.ctx.body = {
code: ErrorCode.success,
success: true,
msg: Message.success
} as IRes;
} catch (e) {
this.ctx.body = {
code: ErrorCode.fail,
success: false,
msg: e.toString()} as IRes;
}
}
}
通过这段代码能够发现:
- 首先咱们应用了 @controller 申明这个类为控制器类,同时应用了 @post 定义了申请形式;
- 通过 @inject() 去容器中取出对应的实例注入到以后属性中;
- 通过 @provide() 定义以后的对象须要被绑定到对应容器中。
这段代码能够显著感触 loC 机制给开发带来的便利性,当咱们想要应用某个实例时,容器会主动将对象实例化交给用户,使得咱们的代码具备很好的解耦性。
上述代码中还做了 request 解析,当申请头里的 gitlab-token 为定义的 activity-components,就会持续往后执行后续逻辑,这里 ctx.headers 写法其实就是援用了 koa 的 context.request.headers 的简写,通过 token 验证,保障了只有组件库代码提交时才会触发。
3.4 npm 包近程拉取 + 解压缩
当实现 gitlab hooks 监听后,如何实现从 npm 私服拉取对应的组件库,并将外面的内容解析进去呢,此处通过查阅 npm 私服的指令,发现能够通过 npm view [<@scope>/][@] [[.]…] 来查问以后的私服托管的 npm 包具体信息,基于此,咱们在本地的终端检索了下 @wukong/activity-components 包的信息,失去如下信息:
npm view @wukong/activity-components
***
{
host: 'wk-site-npm-test.vivo.xyz',
pathname: '/@wukong%2factivity-components',
path: '/@wukong%2factivity-components',
***
dist:{
"integrity": "sha512-aaJssqDQfSmwQ1Gonp5FnNvD6TBXZWqsSns3zAncmN97+G9i0QId28KnGWtGe9JugXxhC54AwoT88O2HYCYuHg==",
"shasum": "ff09a0554d66e837697f896c37688662327e4105",
"tarball": "http://wk-****-npm-test.vivo.xyz/@wukong%2factivity-components/-/activity-components-1.0.0.tgz"
},***
}
剖析 npm view 的返回信息,抓到 npm 包的源地址:dist.tarball:[http://**.xyz/@wukongactivity-components-1.0.0.tgz],通过这个地址能够间接将对应的 tgz 源文件下载到本地。
然而这个地址并不能彻底解决掉问题,因为随着公共组件库的一直迭代,npm 包的版本是在一直变动的,如何能力获取到 npm 包的版本呢? 带着这个问题,咱们去了 npm 私服 network 抓到一个接口:[http://**.xyz/-/verdaccio/sidebar/@wukong/activity-components];通过查问这个接口的返回,失去了以下信息:
接口返回能够看到 latest.version 返回了最新的版本信息,通过这两个接口,就能够在 Node 层间接下载到最新的组件库,接下来看下插件治理平台侧的代码:
service/activity.ts
***
// 包寄存的根目录,所有的插件加载后对立放在这里
const rootPath = path.resolve('temp');
/**
* 获取某个插件的最新版本
* @param {string} fullName 插件全名(带前缀:@wukong/wk-api)*/
async getNpmInfo(fullName) {const { registry} = this.ctx.service.activity;
// 近程获取 @wukong/activity-components 的最新版本信息
const npmInfo = await this.ctx.curl(`${registry}/-/verdaccio/sidebar/${fullName}`, {dataType: 'json',});
if (npmInfo.status !== 200) {throw new Error(`[error]: 获取 ${fullName} 版本信息失败 `);
}
return npmInfo;
}
/**
* 近程下载 npm 包
* @param {string} name 插件名(不带前缀:activity-components)* @param {string} tgzName `${name}-${version}.tgz`;
* @param {string} tgzPath path.join(rootPath, name, tgzName);
*/
async download(name,tgzName,tgzPath){const pkgName = `@wukong/${name}`;
const pathname = path.join(rootPath, name);
// 近程下载文件
const response = await this.ctx.curl(`${this.registry}/${pkgName}/-/${tgzName}`);
if (response.status !== 200) {throw new Error(`download ${tgzName} 加载失败 `);
}
// 确定文件夹是否存在
fse.existsSync(pathname);
// 清空文件夹
fse.emptyDirSync(pathname);
await new Promise((resolve, reject) => {const stream = fse.createWriteStream(tgzPath);
stream.write(response.data, (err) => {err ? reject(err) : resolve();});
});
}
getNpmInfo 办法次要是获取组件的版本信息,download 次要是组件的下载操作,最初实现对应的流文件注入,这两个办法执行结束之后,咱们会生成以下的目录构造:
|-server
|--src
|---app
|----controller
|----***
|--temp
|---activity-components
|----activity-compoponents-1.0.6.tgz
在 temp 文件下获取到了组件库的压缩包,然而到这一步是不够的,咱们须要解压缩这个压缩包,并且要获取到对应的源码。带着这个问题,找到一个 targz 的 npm 包,首先看下官网给的 demo:
var targz = require('targz');
// decompress files from tar.gz archive
targz.decompress({
src: 'path_to_compressed file',
dest: 'path_to_extract'
}, function(err){if(err) {console.log(err);
} else {console.log("Done!");
}
});
官网裸露的 decomporess 办法即可实现 targz 包的解压缩,失去对应的组件库源代码,对于压缩包,咱们应用 fs 的 remove 办法移除即可:
|-server
|--src
|---app
|----controller
|----***
|--temp
|---activity-components
|----doc
|----src
|----tool
|----****
到这一步咱们就实现了整体的 npm 包拉取和解压缩操作,获取到了组件库的源代码,此时咱们须要读取到源代码中 doc 通过 3.2 步骤生成的 json 文件,并将 json 内容返回给 web 侧。
3.5 ast 转译
背景 :在对建站平台的根底组件库 wk-base-ui 引入时,因为组件库的 index.js 不是主动生成的,外面会呈现冗余代码以及正文的状况,这样会导致插件治理平台依据入口文件无奈精准的获取到所有的组件地址,为了解决这个问题,咱们应用 @babel/parser、@babel/traverse 解析 wk-base-ui 组件库的入口文件。
思路 :找到组件库 npm 包入口文件,依据入口文件中的 export 语句,找到组件库中每个 Vue 组件的门路,并置换成绝对 npm 包根目录的地址。
组件库的个别组织模式:
模式 1:(activity-components 为例)
package.json 中有 main 指定入口:
// @wukong/activity-components/package.json
{
"name": "@wukong/activity-components",
"description": "流动公共组件库",
"version": "1.0.6",
"main": "src/main.js", // main 指定 npm 包入口
...
}
入口文件:
// src/main.js
export {default as CommonDialog} from './base-components/CommonDialog/index.vue'
export {default as Login} from './base-components/Login/index.vue'
export {default as ScrollReach} from './base-components/ScrollReach/index.vue'
...
模式 2:(wk-base-ui 为例)package.json 中无 main 指定入口,根目录下入口文件为 index.js:
// @wukong/wk-base-ui/index.js
export {default as inputSlider} from './base/InputSlider.vue'
export {default as inputNumber} from './base/InputNumber.vue'
export {default as inputText} from './base/InputText.vue'
/*export {default as colorGroup} from './base/colorGroup.vue'*/
...
以上两种模式最终都指向形如 export {default as xxx} from ‘./xxx/../xxx.vue’ 的文件。为了从入口 js 文件中精确找到 export 组件名和文件门路,这里应用利用 @babel/parser 和 @babel/traverse 来解析,如下:
// documnet.ts
// 通过 @babel/parser 解析入口 js 文件内容 exportData 失去形象语法树 ast
const ast = parse(exportData, {sourceType: 'module',});
const pathList: any[] = [];
// 通过 @babel/traverse 遍历 ast,失去每条 export 语句中的组件名 name 和对应的 vue 文件门路
traverse(ast, {
ExportSpecifier: {enter(path, state) {console.log('start processing ExportSpecifier!');
// do something
pathList.push({
path: path.parent.source.value, // 组件导出门路 eg: from './xxx/../xxx.vue' 这里的./xxx/../xxx.vue
name: path.node.exported.name, // 组件导出名 eg: export {default as xxx} 这里的 xxx
});
},
exit(path, state) {console.log('end processing ExportSpecifier!');
// do something
},
},
});
这里最终失去的 pathList 如下:
[{ name: "inputSlider", path: "./base/InputSlider.vue"},
{name: "inputNumber", path: "./base/InputNumber.vue"},
{name: "inputText", path: "./base/InputText.vue"},
...
]
后续再遍历 pathList 数组,利用 @vivo/vue-doc 的 singleVueDocSync 解析出每个组件的 md 文档,实现对组件库的文档解析工作。代码示例如下:
pathList.forEach((item) => {const vuePath = path.join(jsDirname, item.path); // 输出门路
const mdPath = path.join(outputDir, item.path).replace(/\.vue$/, '.md');// 输入门路
try {
singleVueDocSync(vuePath, {
outputType: 'md', // 输出类型 md
outputPath: mdPath, // 输入门路
});
// ... 省略
} catch (error) {// todo 如果遇到 @vivo/vue-doc 解决不了的组件,临时跳过。(或者生成一个空的 md 文件)}
});
最终成果如下图,在我的项目目录下生成 doc 文件夹:
至此,实现了解析组件库并生成对应 md 文档的全副流程。
最初咱们能够看一下平台实现的成果:
四、小结
4.1 思考过程
在做建站的组件开发过程中,首先构思公共组件库,解决开发之间组件积淀的问题,随着组件增多,发现产品和经营对于积淀的公共组件也有诉求,对于此开始了插件治理平台的架构设计,解决公共组件对产品的黑盒问题,同时也能够很好的赋能悟空流动中台生态,对于别的业务方也能够疾速的接入 vivo 悟空流动中台组件,晋升本身的开发效率。
4.2 现状和将来打算
目前一共抽离了公共组件 26 个,累计笼罩建站组件超过 12 个,接入的业务方 2 个,累计晋升人效大于 20 人天。然而还是不够的,后续咱们须要实现组件的自动化测试,持续丰盛组件库,减少动效区域,更好的赋能上下游。
作者:vivo 互联网前端团队 -Fang Liangliang