[camp] at the beginning, improve the power of [leopard] - vivo activity plug-in management platform

Posted by qaokpl on Tue, 22 Feb 2022 04:12:46 +0100

1, Background

With more and more activity components in vivo Wukong activity, the partners in the development of activity platform increasingly perceive that we lack a component library that can precipitate the general ability and improve the reusability of code. On the basis of this goal, acitivity components were born. However, with the increasing separation of components, when communicating with upstream and downstream, it was found that public components are black boxes for operation, products and test students. Only the developer knows what capabilities have been precipitated and which modules have been extracted in the business. At the same time, in the process of external empowerment, there is also a lack of a platform to present the components we 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 platform development, through communication with our partners, we determined the Midway+Vue+MySQL technology stack and completed the corresponding architecture sorting. When selecting the Node layer framework, we chose Midway as the development framework of the Node layer. The main reasons are as follows:

  • Based on egg JS -- midway is well compatible with egg JS plug-in ecology.
  • Dependency injection (loC) -- the full name of loC is called Inversion of Control (abbreviated as loC). It is an object-oriented design pattern, which can be used to reduce the coupling between codes and achieve the architectural goal of high cohesion and weak coupling.
  • Better Typescript support - because Midway uses TS development, we can directly use ts in the process of project development and make use of its static type checking, decorator and other capabilities to improve our code robustness and development efficiency.

2.2 structure disassembly

First, let's take a look at the architecture of the plug-in management platform:

By combing the overall architecture diagram of the platform, the basic idea of the whole platform development is constructed:

  • Component separation - extract more basic component content from the components of the site, and centrally host it to activity components.
  • md generation -- all components need to be output 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 side can respond in time to the changes of activity components and ensure that the components are up-to-date. Here, push events in gitlab integration is used to monitor the push operation of components.
  • 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 bucket usage - the web side of the platform introduces Vue bucket and uses dynamic routing to match various components.
  • Single component Preview - the extracted component bottom layer depends on the site building ability. Here, you need to disassemble the site building editing page, integrate the site building bottom ability, 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 combed and mainly introduced.

3.1 component extraction

First, take a look at the activity components component library package JSON content:

{
  "name": "@wukong/activity-components",
  "version": "1.0.6",
  "description": "Active common component library",
  "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 scripts, we can see that when the component publish es, we use the pre event hook of npm to complete the first level compilation operation of the component itself. Map components is mainly used to traverse the file directory of the component and export all component contents.

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 realizes the traversal of files and directories;

// Depth traversal directory
const deepMapDir = (rootPath, name, cb) => {
  const list = fse.readdirSync(rootPath)
  list.forEach((targetPath) => {
    const fullPath = path.join(rootPath, targetPath)
    // Resolve folder
    const stat = fse.lstatSync(fullPath)
    if (stat.isDirectory()) {
      // If it is a folder, continue to traverse down
      deepMapDir(fullPath, targetPath, cb)
    } else if (targetPath === 'index.vue') {
      // If it's a file
      if (typeof cb === 'function') {
        cb(rootPath, path.relative('./src', fullPath), name)
      }
    }
  })
}
***
***
***
// Splicing file content
const file = `
${components.map(c => `export { default as ${c.name} } from './${c.path}'`).join('\n')}
`
// File output
try {
  fse.outputFile(path.join(__dirname, '..', pkgJson.main), file)
} catch (e) {
  console.log(e)
}

When doing file traversal, we use recursive functions to ensure that we completely traverse the current file directory and find all components. Through this code, we can see that the defined components need an index Vue component is used as the retrieval entry file. After finding this component, we will stop looking down and parse the current component directory. The specific process is shown in the following figure.

The contents of the exported 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 directory files are generated uniformly, and the components are pulled away. You only need to add them 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 Feng Dy, another colleague of the same center vue-doc (opens new window) , complete the automatic generation of md documents of corresponding Vue components. First, take a look at the doc instructions defined.

 "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 doc folder will be created under the root directory of the server side, where 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, md files with the same directory structure are generated in the doc folder. So far, md documents of each component have 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 plug-in management platform needs to parse this json file and return it to the web as the content 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()
 })
 ***
 // Generate corresponding component data file
fse.writeJsonSync(path.resolve('./doc/data.json'), info, {
  spaces: 2
})

Two important libraries are introduced here, one is cherio and the other is marked. Cherio 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. Marked is mainly used to convert md documents into html document format. After completing the above code writing, we generate a data in the doc directory JSON file, as follows:

{
  "timestamp": 1628846618611,
  "list": [
    {
      "name": "CommonDialog",
      "md": "<h1 id=\"commondialog\">CommonDialog</h1>\n<h3 id=\"Component introduction\">Component introduction</h3>\n<blockquote>\n<p>General basic spring frame</p>\n</blockquote>\n<h2 id=\"attribute-attributes\">attribute-Attributes</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\">parameter</th>\n<th align=\"center\">explain</th>\n<th align=\"center\">type</th>\n<th align=\"center\">Default value</th>\n<th align=\"center\">must</th>\n<th align=\"center\">sync</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">maskZIndex</td>\n<td align=\"center\">Bullet frame z-index Hierarchy</td>\n<td align=\"center\">Number</td>\n<td align=\"center\">1000</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">bgStyle</td>\n<td align=\"center\">Background style</td>\n<td align=\"center\">Object</td>\n<td align=\"center\">-</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">closeBtnPos</td>\n<td align=\"center\">Position of the close button</td>\n<td align=\"center\">String</td>\n<td align=\"center\">top-right</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">showCloseBtn</td>\n<td align=\"center\">Show close button</td>\n<td align=\"center\">Boolean</td>\n<td align=\"center\">true</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">v-model</td>\n<td align=\"center\">Show bullet frame</td>\n<td align=\"center\">Boolean</td>\n<td align=\"center\">-</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n</tbody></table>\n<h2 id=\"event-events\">event-Events</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\">Event name</th>\n<th align=\"center\">explain</th>\n<th align=\"center\">parameter</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\">Pop up frame closing event</td>\n<td align=\"center\"></td>\n</tr>\n</tbody></table>\n<h2 id=\"slot -slots\">slot -Slots</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\">name</th>\n<th align=\"center\">explain</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\">Pop up content</td>\n<td align=\"center\"></td>\n<td align=\"center\">-</td>\n</tr>\n</tbody></table>\n",
      "fullPath": "/F/My project/Common component/activity-components/doc/base-components/CommonDialog/index.md",
      "path": "doc/base-components/CommonDialog/index.md",
      "cname": "General basic spring frame"
    }, {
      "name": "ConfirmDialog",
      "md": "<h1 id=\"confirmdialog\">ConfirmDialog</h1>\n<h3 id=\"Component introduction\">Component introduction</h3>\n<blockquote>\n<p>Confirmation pop-up</p>\n</blockquote>\n<h2 id=\"attribute-attributes\">attribute-Attributes</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\">parameter</th>\n<th align=\"center\">explain</th>\n<th align=\"center\">type</th>\n<th align=\"center\">Default value</th>\n<th align=\"center\">must</th>\n<th align=\"center\">sync</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">bgStyle</td>\n<td align=\"center\">Background style</td>\n<td align=\"center\">Object</td>\n<td align=\"center\">-</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">maskZIndex</td>\n<td align=\"center\">Bullet frame level</td>\n<td align=\"center\">Number</td>\n<td align=\"center\">1000</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">v-model</td>\n<td align=\"center\">Pop up display status</td>\n<td align=\"center\">Boolean</td>\n<td align=\"center\">-</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">title</td>\n<td align=\"center\">Bullet box title copy</td>\n<td align=\"center\">String</td>\n<td align=\"center\">-</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">titleColor</td>\n<td align=\"center\">Title Color</td>\n<td align=\"center\">String</td>\n<td align=\"center\">-</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">leftTitle</td>\n<td align=\"center\">Left button copy</td>\n<td align=\"center\">String</td>\n<td align=\"center\">cancel</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">rightTitle</td>\n<td align=\"center\">Right button copy</td>\n<td align=\"center\">String</td>\n<td align=\"center\">determine</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">leftBtnStyle</td>\n<td align=\"center\">Left button style</td>\n<td align=\"center\">Object</td>\n<td align=\"center\">-</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n<tr>\n<td align=\"center\">rightBtnStyle</td>\n<td align=\"center\">Right button style</td>\n<td align=\"center\">Object</td>\n<td align=\"center\">-</td>\n<td align=\"center\">no</td>\n<td align=\"center\">no</td>\n</tr>\n</tbody></table>\n<h2 id=\"event-events\">event-Events</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\">Event name</th>\n<th align=\"center\">explain</th>\n<th align=\"center\">parameter</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">cancel</td>\n<td align=\"center\">Left button click trigger</td>\n<td align=\"center\"></td>\n</tr>\n<tr>\n<td align=\"center\">confirm</td>\n<td align=\"center\">Right click trigger</td>\n<td align=\"center\"></td>\n</tr>\n<tr>\n<td align=\"center\">close</td>\n<td align=\"center\">Pop up frame closing event</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/My project/Common component/activity-components/doc/wap-components/ConfirmDialog/index.md",
      "path": "doc/wap-components/ConfirmDialog/index.md",
      "cname": "Confirmation pop-up"
    }, {
      "name": "WinnerList",
      "md": "<h1 id=\"winnerlist\">WinnerList</h1>\n<h3 id=\"Component introduction\">Component introduction</h3>\n<blockquote>\n<p>Winning list</p>\n</blockquote>\n<h2 id=\"attribute-attributes\">attribute-Attributes</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\">parameter</th>\n<th align=\"center\">explain</th>\n<th align=\"center\">type</th>\n<th align=\"center\">Default value</th>\n<th align=\"center\">must</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\">yes</td>\n<td align=\"center\">no</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\">yes</td>\n<td align=\"center\">no</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\">no</td>\n<td align=\"center\">no</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\">no</td>\n<td align=\"center\">no</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\">no</td>\n<td align=\"center\">no</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\">no</td>\n<td align=\"center\">no</td>\n</tr>\n</tbody></table>\n",
      "fullPath": "/F/My project/Common component/activity-components/doc/web-components/WinnerList/index.md",
      "path": "doc/web-components/WinnerList/index.md",
      "cname": "Winning list"
    }]
 }

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

Through this diagram, 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 submits to gitlab, the platform cannot perceive that the code of the component library has changed, so we began to investigate. When searching the api of gitlab, we found that gitlab has provided an integrated solution.

After configuring the corresponding url and secret Token, click save changes to generate the following content:

At this time, 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'];
      // Judge whether the token is correct
      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;
    }
  }
}

Through this code, you can find:

  • Firstly, we use @ controller to declare this class as the controller class, and @ post to define the request mode;
  • Remove the corresponding @ inject() attribute from the current instance;
  • The current object defined by @ provide() needs to be bound to the corresponding container.

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

The above code also does request parsing. When the gitlab token in the request header is the defined activity components, the subsequent logic will continue to be executed later, here CTX In fact, the writing of headers refers to koa's context request. The abbreviation of headers, which is verified by token, ensures that it will be triggered only when the component library code is submitted.

3.4 npm package remote pull + decompression

After gitlab hooks monitoring, how to pull the corresponding component library from the npm private server and analyze the contents? Here, by consulting the instructions of the npm private server, it is found that you can use npm view [< @ scope > /] [@] [.]...] To query the specific information of the npm package hosted by the current private server. Based on this, we searched the information of the next @ Wukong / activity components package at 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 returned information of npm view and find the source address of npm package: dist.tarball:[http://**.xyz/@wukongactivity-components-1.0.0.tgz]. Through this address, you can directly download the corresponding tgz source file to the local.

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

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

service/activity.ts
***
// The root directory where the package is stored. All plug-ins are placed here after loading
const rootPath = path.resolve('temp');
  /**
   * Get the latest version of a plug-in
   * @param {string} fullName Full plug-in name (prefixed with: @ Wukong / wk API)
   */
  async getNpmInfo(fullName) {
    const { registry } = this.ctx.service.activity;
    // Get the latest version information of @ Wukong / activity components remotely
    const npmInfo = await this.ctx.curl(`${registry}/-/verdaccio/sidebar/${fullName}`, {
      dataType: 'json',
    });

    if (npmInfo.status !== 200) {
      throw new Error(`[error]: obtain ${fullName}Version information failed`);
    }
    return npmInfo;
  }
    /**
   * Download npm package remotely
   * @param {string} name Plug in name (without prefix: 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);
    // Download files remotely
    const response = await this.ctx.curl(`${this.registry}/${pkgName}/-/${tgzName}`);
    if (response.status !== 200) {
      throw new Error(`download ${tgzName}Loading failed`);
    }
    // Determine whether the folder exists
    fse.existsSync(pathname);
    // Empty Folder 
    fse.emptyDirSync(pathname);
    await new Promise((resolve, reject) => {
      const stream = fse.createWriteStream(tgzPath);
      stream.write(response.data, (err) => {
        err ? reject(err) : resolve();
      });
    });
}

getNpmInfo method is mainly used to obtain the version information of components. Download is mainly used to download components 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 problem, find a targz npm package. First, take a 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 decompress method officially exposed can decompress 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 step, we have completed the overall npm package pulling and decompression operation, and obtained the source code of the component library. At this time, we need to read the json file generated by doc in the source code through step 3.2, and return the json content to the web side.

3.5 ast translation

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

Idea: find the npm package entry file 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 of the relative npm package root directory.

General organization of component library:

Form 1: (activity components as an example)

package. main specifies the entry in JSON:

// @wukong/activity-components/package.json
{
    "name": "@wukong/activity-components",
    "description": "Active common component library",
    "version": "1.0.6",
    "main": "src/main.js", // main specifies the npm package entry
    ...
}

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) package There is no main specified entry in 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'*/
...

The above two forms finally point to the form, such as 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, use @ babel/parser and @ babel/traverse to parse, as follows:

// documnet.ts

// Parse the contents of the entry js file exportData through @ babel/parser to get the abstract syntax tree ast
const ast = parse(exportData, {
  sourceType: 'module',
});

const pathList: any[] = [];

// Traverse the ast through @ babel/traverse to get the component name and the corresponding vue file path in each export statement
traverse(ast, {
  ExportSpecifier: {
    enter(path, state) {
      console.log('start processing ExportSpecifier!');
      // do something
      pathList.push({
        path: path.parent.source.value, // Component export path eg: from '/ xxx/../xxx.vue 'here/ xxx/../xxx.vue
        name: path.node.exported.name, // Component export name eg: export {default as xxx} xxx here
      });
    },
    exit(path, state) {
      console.log('end processing ExportSpecifier!');
      // do something
    },
  },
});

The resulting pathList is as follows:

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

Then traverse the pathList array, use @ vivo / Vue Doc's singleVueDocSync to parse the md document of each component, and complete the document parsing of the component library. Code examples are as follows:

pathList.forEach((item) => {
  const vuePath = path.join(jsDirname, item.path); // enter path
  const mdPath = path.join(outputDir, item.path).replace(/\.vue$/, '.md');// Output path
  try {
    singleVueDocSync(vuePath, {
      outputType: 'md', // Input type md
      outputPath: mdPath, // Output path
    });
    // ... ellipsis
  } catch (error) {
    // If todo encounters a component that @ vivo / Vue doc cannot handle, skip it temporarily. (or generate an empty md file)
  }
});

The final effect is shown in the figure below. doc folder is generated under 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 see the effect of the platform:

4, Summary

4.1 thinking process

In the process of component development for building the station, we first conceived the public component library to solve the problem of component precipitation between development. With the increase of components, we found that products and operations also have demands for precipitated public components. For this, we started the architecture design of plug-in management platform to solve the problem of black box of public components to products, and can also well empower the middle stage ecology of Wukong activities, For other business parties, they can also quickly access the console components of vivo Wukong activities to improve their development efficiency.

4.2 current situation and future plan

At present, a total of 26 public components have been removed, covering more than 12 station building components and 2 connected business parties, and the cumulative human efficiency has been improved by more than 20 person days. However, it is not enough. In the future, we need to complete the automatic testing of components, continue to enrich the component library, increase the dynamic effect area, and better enable the upstream and downstream.

Author: vivo Internet front end team - Fang Liangliang

Topics: Front-end architecture