一、背景

随着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 archivetargz.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.jsexport { 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.jsexport { 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失去形象语法树astconst 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