2

1. Background

As there are more and more active components in vivo Wukong activities, the small partners in the development of the event center are increasingly aware that we lack a component library that can accumulate general capabilities and improve code reuse. Based on this goal, acitivity-components was born, but with the increase in the separation of components, when communicating with upstream and downstream, it was found that public components are black boxes for operations, products, and testing students, and only the developers themselves know the precipitation. Which capabilities have been developed and which modules have been extracted from the business. At the same time, when empowering externally, there is also a lack of a platform to present the components we have extracted. Based on this goal, the development partners began to conceive the development plan of the plug-in management platform.

2. Platform Architecture

2.1 Technical selection

At the beginning of the platform development, the Midway+Vue+MySQL technology stack was determined through communication with the small partners, and the corresponding architecture was sorted out. When selecting the Node layer framework, we chose Midway as the Node layer development framework. The reason The main points are as follows:

  • Based on egg.js -- Midway can be well compatible with egg.js plugin ecosystem.
  • Dependency Injection (loC) - The full name of loC is Inversion of Control (Inversion of Control, abbreviated as loC), which is an object-oriented design pattern that can be used to reduce the coupling between codes and achieve high cohesion. Weakly coupled architectural goals.
  • Better Typescript support -- Because Midway uses ts development, we can directly use ts in the project development process, and use ts' static type checking, decorators and other capabilities to improve our code robustness and development efficiency.

2.2 Architecture dismantling

First, let's take a look at the architecture diagram of the plugin management platform:

By sorting out the overall architecture diagram of the platform, the basic ideas for the development of the entire platform are constructed:

  • components are extracted from -- from the components of the website to the next layer, extract more basic component content, and centrally host them to activity-components.
  • md generates -- all components need to be exported externally, a layer of compilation operation needs to be done in activity-components, and each component needs to automatically generate the corresponding md document.
  • gitlab hooks -- How to ensure that the server can respond to changes in activity-components in a timely manner and ensure that the components are up to date. Here, the push events in the gitlab integration are used to monitor the push operation of the component.
  • npm remote loading -- The platform needs to have the ability to remotely pull npm packages, decompress the corresponding packages, and obtain the activity-components source file.
  • Vue family bucket uses -- the platform web side introduces Vue family bucket, and uses dynamic routing to match each component.
  • Single-component preview -- The bottom layer of the extracted component has a dependency on the ability to build a website. Here, it is necessary to disassemble the editing page of the website, integrate the underlying capabilities of the website, and complete the component preview of activity-components.
  • file service -- It has the ability to upload public component planning documents, which is convenient for operation and product access to public components.

3. Detailed explanation of key technologies

In the overall construction and development process of the platform, the following technical points are sorted out and introduced in key points.

3.1 Component extraction

First, take a look at the package.json content of the activity-components component library:

{
  "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"
    }
  }

Through the instructions configured in the scripts, we can see that when the component does the publish operation, we use the npm pre event hook to complete the first-level compilation operation of the component itself. map-components is mainly used to implement the file directory of the component. Traverse, export all component content.

The file directory structure is as follows:

|-src
|--base-components
|---CommonDialog
|---***
|--wap-components
|---ConfirmDialog
|---***
|--web-components
|---WinnerList
|---***
|-tool
|--create-doc.js
|--map-components.js

map-components mainly implements traversal operations on file directories;

// 深度遍历目录
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)
}

When doing file traversal, we use a recursive function to ensure that we thoroughly traverse the current file directory and find all components. Through this code, we can see that the defined component needs to have an index.vue The component is used as the entry file for retrieval. After finding this component, we will stop looking down and parse the current component directory. The specific process is as follows.

The contents of the export file are as follows:

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'
***

Through the above series of operations, the external catalog files are generated uniformly, and the component extraction only needs to be added to the component library normally.

3.2 Automatic generation of Markdown files

After the component directory is generated, how to generate the corresponding component description document, here we refer to the vue-doc (opens new window) developed by another colleague of the same center, Feng Di, to complete the automatic generation of the corresponding Vue component md document , first look at the defined doc directive.

 "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
  })

Through the singleVueDocSync method exposed by Vue-doc, a new doc folder will be created in the root directory of the server side, and the corresponding component md document will be generated according to the directory structure of the component. At this time, the directory structure of the doc is:

|-doc
|--base-components
|---CommonDialog
|----index.md
|---***
|--wap-components
|---ConfirmDialog
|----index.md
|---***
|--web-components
|---WinnerList
|----index.md
|---***
|-src
|--**

From this file directory, you can see that according to the directory structure of the component library, the md file with the same directory structure is generated in the doc folder. So far, the md document of each component has been generated, but this step is not enough.

We also need to integrate the current md document and express it through a json file, because the plugin management platform needs to parse this json file and use it as the return content to the web side to complete the rendering of the front-end page. Based on this goal, we Wrote the following code:

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
})

Two more important libraries are introduced here, one is cheerio and the other is marked. cheerio is a fast, flexible and concise implementation of the core functions of jquery. It is mainly used where the DOM needs to be operated on the server side. Marked is mainly to convert the md document into the html document format. After the above code is written, we Generate a data.json file in the doc directory with the following contents:

{
  "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": "中奖列表"
    }]
 }

So far, we have completed the automatic generation of md documents for components on the activity-components side.

Through this picture, we can clearly capture the key information in the underlying components, such as the properties and events supported by the components.

3.3 gitlab hooks

In the process of platform development, every time a component is submitted to gitlab, the platform cannot perceive that the code of the component library has changed, so we began to investigate, and when we searched gitlab's api, we found that gitlab has provided an integrated solution. .

After completing the configuration of the corresponding url and secret Token, clicking save changes will generate the content shown in the following figure:

At this point, the basic push events configuration has been completed. Next, the corresponding interface development needs to be completed on the server side of the plug-in management platform.

@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;
    }
  }
}

It can be found by this code:

  • First, we use @controller to declare this class as a controller class, and use @post to define the request method;
  • Use @inject() to take out the corresponding instance from the container and inject it into the current property;
  • The current object defined by @provide() needs to be bound to the corresponding container.

This code can clearly feel the convenience brought by the loC mechanism to development. When we want to use an instance, the container will automatically instantiate the object to the user, making our code well decoupled.

The request parsing is also done in the above code. When the gitlab-token in the request header is the defined activity-components, it will continue to execute the subsequent logic. The writing of ctx.headers here actually refers to the context.request.headers of koa Shorthand, through token verification, it is guaranteed that it will only be triggered when the component library code is submitted.

3.4 NPM package remote pull + decompression

When the monitoring of gitlab hooks is completed, how to pull the corresponding component library from the npm private server and parse the contents? @] [[.]...] to query the specific information of the npm package hosted by the current private server. Based on this, we retrieved the information of the @wukong/activity-components package on the local terminal and obtained the following information:

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"
   },
   ***
}

Analyze the return information of npm view and capture the source address of the npm package: dist.tarball:[http:// * *.xyz/@wukongactivity-components-1.0.0.tgz], through this address, you can directly connect the corresponding The tgz source file is downloaded locally.

But this address does not completely solve the problem, because with the continuous iteration of the public component library, the version of the npm package is constantly changing. How can we get the version of the npm package? With this problem, we went to the npm private server The network caught an interface: [http:// * *.xyz/-/verdaccio/sidebar/@wukong/activity-components]; by querying the return of this interface, the following information was obtained:

When the interface returns, you can see that latest.version returns the latest version information. Through these two interfaces, you can directly download the latest component library at the Node layer. Next, look at the code on the plug-in management platform side:

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();
      });
    });
}

The getNpmInfo method is mainly to obtain the version information of the component, download is mainly to download the component, and finally complete the corresponding stream file injection. After the execution of these two methods, we will generate the following directory structure:

|-server
|--src
|---app
|----controller
|----***
|--temp
|---activity-components
|----activity-compoponents-1.0.6.tgz

The compressed package of the component library is obtained under the temp file, but this step is not enough. We need to decompress the compressed package and obtain the corresponding source code. With this question, find a targz npm package, first look at the official 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!");
    }
});

The officially exposed decomporess method can complete the decompression of the targz package and obtain the corresponding component library source code. For the compressed package, we can use the remove method of fs to remove it:

|-server
|--src
|---app
|----controller
|----***
|--temp
|---activity-components
|----doc
|----src
|----tool
|----****

At this point, we have completed the overall npm package pulling and decompression operations, and obtained the source code of the component library. At this time, we need to read the json file generated by the doc in the source code through step 3.2, and return the json content to the web side.

3.5 ast translation

Background : When the basic component library wk-base-ui of the website building platform is introduced, since the index.js of the component library is not automatically generated, there will be redundant code and comments in it, which will cause the plug-in management platform to The entry file cannot accurately obtain all the component addresses. In order to solve this problem, we use @babel/parser and @babel/traverse to parse the entry file of the wk-base-ui component library.

Ideas : Find the entry file of the npm package of the component library, find the path of each Vue component in the component library according to the export statement in the entry file, and replace it with the address relative to the root directory of the npm package.

The general organization of the component library:

Form 1: (activity-components as an example)

There is main specified entry in package.json:

// @wukong/activity-components/package.json
{
    "name": "@wukong/activity-components",
    "description": "活动公共组件库",
    "version": "1.0.6",
    "main": "src/main.js", // main 指定npm包入口
    ...
}

Entry file:

// 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'
...

Form 2: (wk-base-ui as an example) There is no main specified entry in package.json, and the entry file in the root directory is 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'*/
...

Both of the above forms eventually point to a file of the form export {default as xxx } from './xxx/../xxx.vue'. In order to accurately find the export component name and file path from the entry js file, we use @babel/parser and @babel/traverse to parse, as follows:

// 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
    },
  },
});

The final pathList obtained here is as follows:

[
  { name: "inputSlider", path: "./base/InputSlider.vue" },
  { name: "inputNumber", path: "./base/InputNumber.vue" },
  { name: "inputText", path: "./base/InputText.vue" },
  ...
]

Afterwards, traverse the pathList array, and use the singleVueDocSync of @vivo/vue-doc to parse the md document of each component to complete the document parsing of the component library. The code example is as follows:

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文件)
  }
});

The final effect is as shown below, and the doc folder is generated in the project directory:

So far, the whole process of parsing the component library and generating the corresponding md document has been completed.

Finally, we can look at the effect achieved by the platform:

4. Summary

4.1 Thought process

In the component development process of building a website, we first conceived of a public component library to solve the problem of component precipitation between development. As the number of components increased, we found that products and operations also have demands for the precipitation of public components. For this, the architecture of the plug-in management platform began. Design, solve the black box problem of public components for products, and at the same time, it can also well empower the Wukong activity middle platform ecology, and other business parties can also quickly access vivo Wukong activity middle platform components to improve their own development efficiency.

4.2 Current status and future plans

At present, a total of 26 public components have been removed, more than 12 station building components have been covered, and 2 business parties have been connected, and the cumulative improvement in human efficiency is greater than 20 person-days. But it is not enough. In the future, we need to complete the automated testing of components, continue to enrich the component library, increase the dynamic effect area, and better empower the upstream and downstream.

Author: Fang Liangliang, vivo Internet front-end team

vivo互联网技术
3.3k 声望10.2k 粉丝