The background,

With the increasing number of active components in Vivo Wukon, active Partners are increasingly aware that we lack a component library that can precipitation general ability and improve code reuse. On the basis of this goal, Acitivity components was born. However, as the number of components removed increased, when communicating with upstream and downstream, I found that common components were black boxes for operation, product and testing students. Only the developer himself knew which capabilities had been deposited and which modules had been extracted in business. At the same time, in the process of external empowerment, there was also a lack of a platform to present the components we removed. Based on this goal, the development partners began to formulate the development plan of plug-in management platform.

Second, platform architecture

2.1 Technology selection

At the beginning of the platform development, we determined Midway+Vue+MySQL technology stack through communication with our friends and completed the corresponding architecture sorting. When choosing the Node layer framework, we chose Midway as the Node layer development framework for the following reasons:

  • Based on Egg.js — Midway is well compatible with the egg.js plugin ecosystem.

  • Dependency Injection (loC) — Known as Inversion of Control (loC), is an object-oriented design pattern that can be used to decouple code to achieve a cohesive, weakly coupled architecture.

  • Better Typescript support — Thanks to TS development for Midway, we were able to use TS directly during project development, taking advantage of TS’s static type checking, decorators, and other capabilities to improve our code robustness and productivity.

2.2 Architecture Disassembling

First let’s look at the architecture diagram of the plug-in management platform:

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

  • Component Extraction – Extract more basic component content one layer down from the site-built component and centrally host it to activity-Components.

  • Md generation – All components need to be exported, a layer of compilation needs to be done within the activity-Components, and the corresponding MD document needs to be generated automatically for each component.

  • Gitlab hooks — How to ensure that the server responds to activity-Components changes in a timely manner and that components are up to date, using push Events in the GitLab integration to monitor component push actions.

  • NPM Remote loading — The platform needs to be able to pull the NPM package remotely and extract the corresponding package to get the activity-Components source file.

  • Vue family bucket usage – The Platform introduces Vue family bucket into the Web end, and uses dynamic routing to match each component.

  • Single component preview — The extracted component base layer is dependent on the site-building capability, so it is necessary to disassemble the site-building editor page and integrate the site-building base layer capability to complete the component preview of activity-Components.

  • File service — upload public component planning documents to facilitate operation and product access to public components.

Detailed explanation of key technologies

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

3.1 Components Removed

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

{
  "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"}}Copy the code

As can be seen from the instructions configured in scripts, when the component publishes, we use NPM’s Pre event hook to complete the first layer compilation of the component itself. Map-components is mainly used to iterate the file directory of the component and 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
Copy the code

Map-components mainly implements traversal of files and directories.

// deeply traverse the directory
const deepMapDir = (rootPath, name, cb) => {
  const list = fse.readdirSync(rootPath)
  list.forEach((targetPath) => {
    const fullPath = path.join(rootPath, targetPath)
    // Parse the folder
    const stat = fse.lstatSync(fullPath)
    if (stat.isDirectory()) {
      // If it is a folder, continue traversing down
      deepMapDir(fullPath, targetPath, cb)
    } else if (targetPath === 'index.vue') {
      // If it is a file
      if (typeof cb === 'function') {
        cb(rootPath, path.relative('./src', fullPath), name)
      }
    }
  })
}
***
***
***
// Concatenate the contents of the file
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)
}
Copy the code

Traversed doing file, we have adopted a recursive function, ensure that our file directory of the current accomplish complete traverse, find out all the components, by this code, you can see, the definition of the component needs to have a index. The vue components as entry documents retrieval, to find the component, we will stop looking for down, And parse out the current component directory, as shown in the following figure.

The exported file contains the following contents:

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'* * *Copy the code

After the preceding operations are performed, external directory files can be generated uniformly. You only need to add components to the component library.

3.2 Automatic Markdown file generation

After creating the component directory, how to generate the corresponding component description document? Here, we use vuE-Doc (Open New Window) developed by Feng Di, another colleague in the same center, to complete automatic generation of the CORRESPONDING VUE component MD document. First, let’s look at the doc directive 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
  })
Copy the code

Using the singleVueDocSync method exposed by vue-doc, a new doc folder will be created in the server root directory, which will generate the corresponding component MD document according to the component directory structure. The doc directory structure is:

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

As you can see from this file directory, md files with the same directory structure are generated in the doc folder, based on the directory structure of the component library. By this point, the MD documents for each component have been generated, but this 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 the JSON file and return it to the Web to complete the front-end page rendering. 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 the corresponding component data file
fse.writeJsonSync(path.resolve('./doc/data.json'), info, {
  spaces: 2
})
Copy the code

There are two important libraries introduced here: cheerio and marked. Cheerio is a fast, flexible and concise implementation of the core functions of jquery, mainly intended to be used in the server side where DOM operations are needed, marked mainly to convert MD documents into HTML document format. After completing the above code writing, 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 = \" component is introduced \ "> component introduction < / h3 > < p > < blockquote > \ n \ n common base bounced < / p > < / blockquote > \ n \ n < h2 Id = \ "properties - the attributes \" > attributes - the attributes the < table > < / h2 > \ n \ n < thead > < tr > \ n \ n < th the align = \ \ "center \" > < / th > parameters \ n < th The align = \ \ "center \" > description < / th > \ n < th the align = \ \ "center \" > < / th > type \ n < th the align = \ \ "center \" > default < / th > \ n < th the align = \ \ "center \" > must < / th > \ n < th  align=\"center\">sync\n\n\n\nmaskZIndex\n bounced z - index hierarchy < / td > \ n < td align = \ \ "center \" > Number < / td > \ n < td align = \ \ "center \" > 1000 < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > bgStyle < / td > \ n < td The align = \ \ "center \" > background style < / td > \ n < td align = \ \ "center \" > Object < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > closeBtnPos < / td > \ n < td The align = \ \ "center \" > the location of the close button < / td > \ n < td align = \ \ "center \" > String < / td > \ n < td align = \ \ "center \" > < / td > top - right \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > showCloseBtn < / td > \ n < td Whether the align = \ \ "center \" > show close button < / td > \ n < td align = \ \ "center \" > Boolean < / td > \ n < td align = \ \ "center \" > true < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > v - model < / td > \ n < td If the align = \ \ "center \" > show bounced < / td > \ n < td align = \ \ "center \" > Boolean < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / tr > < / td > \ n \ n < / tbody > < / table > \ n < h2 Id = \ "events - events \" > events - events < table > < / h2 > \ n \ n < thead > < tr > \ n \ n < th the align = \ \ "center \" > event name < / th > \ n < th Align = \ \ "center \" > description < / th > \ n < th the align = \ \ "center \" > parameter < / tr > < / th > \ n \ n < thead > \ n < tbody > < tr > \ n < td align=\"center\">input\n-\n\n\n\n close < / td > \ n < td align = \ \ "center \" > < / td > bounced off events \ n < td align = \ \ "center \" > < / tr > < / td > \ n \ n < / tbody > < / table > \ n < h2 Id = \ "slot - slots \" > slot - slots < table > < / h2 > \ n \ n < thead > < tr > \ n \ n < th the align = \ \ "center \" > name < / th > \ n < th the align = \ \ "center \" > description < / th > \ n < th align=\"center\">scope\ncontent\n\n\n\n default < / td > \ n < td align = \ \ "center \" > bounced content < / td > \ n < td align = \ \ "center \" > < / td > \ n < td align=\"center\">-\n\n\n"."fullPath": "My project/public/F/components/activity - components/doc/base - components/CommonDialog/index. The md." "."path": "doc/base-components/CommonDialog/index.md"."cname": "Universal Basic Frame"
    }, {
      "name": "ConfirmDialog"."md": "< h1 id = \ \" confirmdialog \ "> confirmdialog < / h1 > \ n < h3 id = \" component is introduced \ "> component introduction < / h3 > < blockquote > \ n \ n < / p > < p > confirm play box \ n < / blockquote > \ n < h2 Id = \ "properties - the attributes \" > attributes - the attributes the < table > < / h2 > \ n \ n < thead > < tr > \ n \ n < th the align = \ \ "center \" > < / th > parameters \ n < th The align = \ \ "center \" > description < / th > \ n < th the align = \ \ "center \" > < / th > type \ n < th the align = \ \ "center \" > default < / th > \ n < th the align = \ \ "center \" > must < / th > \ n < th  align=\"center\">sync\n\n\n\nbgStyle\n background style < / td > \ n < td align = \ \ "center \" > Object < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > maskZIndex < / td > \ n < td The align = \ \ "center \" > bounced hierarchy < / td > \ n < td align = \ \ "center \" > Number < / td > \ n < td align = \ \ "center \" > 1000 < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > v - model < / td > \ n < td The align = \ \ "center \" > play box shows the state < / td > \ n < td align = \ \ "center \" > Boolean < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > title < / td > \ n < td The align = \ \ "center \" > bounced headline copy < / td > \ n < td align = \ \ "center \" > String < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > titleColor < / td > \ n < td Color the align = \ "center \" > title < / td > \ n < td align = \ \ "center \" > String < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > leftTitle < / td > \ n < td The align = \ "center \" > left button to copy < / td > \ n < td align = \ \ "center \" > String < / td > \ n < td align = \ \ "center \" > cancel < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > rightTitle < / td > \ n < td The align right button = \ "center \" > copy < / td > \ n < td align = \ \ "center \" > String < / td > \ n < td align = \ \ "center \" > confirm < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > leftBtnStyle < / td > \ n < td The align = \ "center \" > left button style < / td > \ n < td align = \ \ "center \" > Object < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > rightBtnStyle < / td > \ n < td The align = \ \ "center \" > right button style < / td > \ n < td align = \ \ "center \" > Object < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > no < / tr > < / td > \ n \ n < / tbody > < / table > \ n < h2 Id = \ "events - events \" > events - events < table > < / h2 > \ n \ n < thead > < tr > \ n \ n < th the align = \ \ "center \" > event name < / th > \ n < th Align = \ \ "center \" > description < / th > \ n < th the align = \ \ "center \" > parameter < / tr > < / th > \ n \ n < thead > \ n < tbody > < tr > \ n < td Align = \ \ "center \" > cancel < / td > \ n < td align = \ \ "center \" > left click on the button to trigger < / td > \ n < td align = \ \ "center \" > < / td > \ n < / tr > < tr > \ n \ n < td Align = \ \ "center \" > confirm < / td > \ n < td align = \ \ "center \" > right click on the button to trigger < / td > \ n < td align = \ \ "center \" > < / td > \ n < / tr > < tr > \ n \ n < td The align = \ \ "center \" > close < / td > \ n < td align = \ \ "center \" > < / td > bounced off events \ n < td align = \ \ "center \" > < / tr > < / td > \ n \ n < tr > \ n < td align=\"center\">input\n-\n\n\n\n"."fullPath": "My project/public/F/components/activity - components/doc/wap - components/ConfirmDialog/index. Md." "."path": "doc/wap-components/ConfirmDialog/index.md"."cname": "Confirm frame"
    }, {
      "name": "WinnerList"."md": "< h1 id = \ \" winnerlist \ "> winnerlist < / h1 > \ n < h3 id = \" component is introduced \ "> component introduction < / h3 > < p > < blockquote > \ n \ n the winning list < / p > < / blockquote > \ n \ n < h2 Id = \ "properties - the attributes \" > attributes - the attributes the < table > < / h2 > \ n \ n < thead > < tr > \ n \ n < th the align = \ \ "center \" > < / th > parameters \ n < th The align = \ \ "center \" > description < / th > \ n < th the align = \ \ "center \" > < / th > type \ n < th the align = \ \ "center \" > default < / th > \ n < th the align = \ \ "center \" > must < / th > \ n < th  align=\"center\">sync\n\n\n\nitem\n - < / td > \ n < td align = \ \ "center \" > < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td align = \ \ "center \" > is < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > prodHost < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td The align = \ \ "center \" > String < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td align = \ \ "center \" > is < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > prizeTypeOptions < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td The align = \ \ "center \" > Array < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td align = \ \ "center \" > no < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > isOrder < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td The align = \ \ "center \" > Boolean < / td > \ n < td align = \ \ "center \" > true < / td > \ n < td align = \ \ "center \" > no < / td > \ n < td Whether the align = \ \ "center \" > < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > listUrl < / td > \ n < td align = \ \ "center \" > - < / td > \ n < td align=\"center\">String\n/wukongcfg/config/activity/reward/got/list\n < / td > \ n < td align = \ \ "center \" > no < / td > \ n < / tr > < tr > \ n \ n < td align = \ \ "center \" > exportUrl < / td > \ n < td align=\"center\">-\nString\n / wukongcfg/config/activity/reward/export < / td > \ n < td align = \ \ "center \" > no < / td > \ n < td Whether the align = \ \ "center \" > < / td > < / tr > \ n \ n < / tbody > < / table > \ n"."fullPath": "My project/public/F/components/activity - components/doc/web - components/WinnerList/index. The md." "."path": "doc/web-components/WinnerList/index.md"."cname": "List of winners"}}]Copy the code

At this point we have completed the automatic generation of the component’s MD document on the activity-Components side.

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

3.3 gitlab hooks

In the process of platform development, the platform could not perceive the changes in the code of the component library every time a component made a GitLab submission. Therefore, we began to investigate and search the API of GitLab, and found that GitLab had already provided an integrated solution.

After completing the corresponding URL and Secret Token configuration, clicking Save Changes will generate something like the following:

At this point, the basic configuration of Push Events has been completed, and 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(a): Promise<void> {
    try {
      const event = this.ctx.headers['x-gitlab-event'];
      const token = this.ctx.headers['x-gitlab-token'];
      // Check 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; }}}Copy the code

You can see from this code:

  • First we use @Controller to declare this class as a controller class, and we use @POST to define the request;

  • Inject the corresponding instance from the container by @inject() into the current attribute;

  • @provide() defines that the current object needs to be bound to the corresponding container.

When we want to use an instance, the container will automatically instantiate the object to the user, making our code very decoupled.

Request parsing is also done in the above code. When gitlab-Token is defined as the activity-Components in the request header, the subsequent logic is continued. Headers = context.request. Headers = context.request. Headers = context.request. Headers = context.request. Headers = context.request. Headers = context.request.

3.4 Remote Pulling and Decompressing the NPM Package

NPM view [<@scope>/][@] [[.]…] 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 got 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 in vivo. Xyz / @ wukong was % 2 factivity - components / - / activity - components - 1.0.0..tgz"* * *}},Copy the code

Dist. Tarball :[http://****.xyz/ @wukongactivity-component-1.0.0.tgz], From this address, you can download the corresponding TGZ source file directly to your local computer.

However, this address does not solve the problem completely, because the version of the NPM package is constantly changing as the common component library is iterated over time. How do I get the version of the NPM package? With this problem in mind, we went to the NPM private server network and caught an interface :[http://****.xyz/-/verdaccio/sidebar/@wukong/activity-components]; By querying the return from this interface, we get the following information:

Latest. version returns the latest version information. Using these two interfaces, you can download the latest component library directly from the Node layer.

service/activity.ts
***
// The root directory where all plug-ins are loaded
const rootPath = path.resolve('temp');
  /** * Get the latest version of a plug-in *@param{string} fullName the fullName of the plug-in (prefixed:@wukong* / / wk - a API)
  async getNpmInfo(fullName) {
    const { registry } = this.ctx.service.activity;
    // Get the latest version of @wukong/ activity-Components
    const npmInfo = await this.ctx.curl(`${registry}/-/verdaccio/sidebar/${fullName}`, {
      dataType: 'json'});if(npmInfo.status ! = =200) {
      throw newError(' [Error]: failed to obtain ${fullName} version information '); }return npmInfo;
  }
    /** * Remote download NPM package *@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 newError(' download ${tgzName} failed to load '); }// Check whether the folder exists
    fse.existsSync(pathname);
    // Clear the folder
    fse.emptyDirSync(pathname);
    await new Promise((resolve, reject) = > {const stream = fse.createWriteStream(tgzPath);
      stream.write(response.data, (err) => {
        err ? reject(err) : resolve();
      });
    });
}
Copy the code

The getNpmInfo method mainly obtains the version information of the component, the Download method mainly downloads the component, and finally completes the corresponding stream file injection. After these two methods are executed, the following directory structure will be generated:

|-server
|--src
|---app
|----controller
|----***
|--temp
|---activity-components
|----activity-compoponents-1.06..tgz
Copy the code

In the temp file to obtain the component library compression package, but to this step is not enough, we need to decompress the compression package, and to obtain the corresponding source. With this problem in mind, find a targz NPM package and 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!"); }});Copy the code

The decomporess method can decompress the targz package and obtain the corresponding component library source code. For the compressed package, we can use the fs remove method to remove:

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

At this step, we have completed the overall NPM package pull and decompress 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 the ast translation

Background: When introducing wK-BASE-UI, the basic component library of the site building platform, the index.js of the component library is not automatically generated, there will be redundant codes and annotations in it, which will lead to the plug-in management platform can not accurately obtain all the component addresses according to the entry file. In order to solve this problem, We used @babel/ Parser and @babel/traverse to parse the entry files of the WK-BASE-UI component library.

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

General organization of component libraries:

Form 1 :(activity-components as an example)

Package. json has the main specified entry:

// @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. }Copy the code

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'.Copy the code

Form 2: (for example, wk-base-UI) Package. json has no main specified entry, 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'*/.Copy the code

Export {default as XXX} from ‘./ XXX /.. /xxx.vue’ file. To find the export component name and file path accurately from the entry js file, use @babel/parser and @babel/traverse as follows:

// documnet.ts

// exportData is parsed by @babel/parser to obtain the abstract syntax tree AST
const ast = parse(exportData, {
  sourceType: 'module'});const pathList: any[] = [];

// Traverse the AST with @babel/traverse to get the component name and vUE file path for 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
      });
    },
    exit(path, state) {
      console.log('end processing ExportSpecifier! ');
      // do something,}}});Copy the code

The resulting pathList looks like this:

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

Subsequently, the pathList array was iterated again, and the MD document of each component was parsed out using @Vivo/VUE-doc’s singleVueDocSync to complete the document parsing of the component library. A code example is as follows:

pathList.forEach((item) => {
  const vuePath = path.join(jsDirname, item.path); // Input path
  const mdPath = path.join(outputDir, item.path).replace(/\.vue$/, '.md');// Output path
  try {
    singleVueDocSync(vuePath, {
      outputType: 'md'.// Enter type md
      outputPath: mdPath, // Output path
    });
    / /... omit
  } catch (error) {
    // If todo encounters a component that @vivo/ VUe-doc can't handle, skip it for now. (or generate an empty MD file)}});Copy the code

The final result is the doc folder in the project directory:

At this point, the entire process of parsing the component library and generating the corresponding MD document is complete.

Finally, we can take a look at what the platform does:

Four, summary

4.1 Thinking Process

In the process of the establishment of the do component development, first conceived public component library, and solve the problems of the development components between precipitation, increase as components, found the products and operation for the precipitation of common components also have appeal, for this began the plugin architecture design of the platform, to solve the problem of the black box the common components of the product, but also China’s ecological activities can be good assigned to the wu is empty, For other business parties, they can also quickly access the platform components of Vivo Wukong activities to improve their own development efficiency.

4.2 Current situation and future plans

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

Author: Fang Liangliang, Vivo Internet Front-end team