In the previous article, we introduced the implementation of the low code development platform iVX. Today, we continue to explore a community open source product called Sparrow. The source of the project can be found at github.com/sparrow-js/… .

At present, there are four types of front-end development: pure code development, low code development, no code development and self-driven development.

Figure from: 2020 China Low Code Platform Index Evaluation report

Sparrow is a low-code scenario-building workbench based on vue.js and Element-UI. Sparrow outputs redevelopable source code in real time (i.e., readable source code, non-compiled code) and supports atom-based, block-granularity building. The reason why we emphasize scenarioization is that the efficiency improvement effect of the platform built by general and general components is relatively general, and the purpose of efficiency improvement can be further achieved through the coarse-grained encapsulation of scenarioization. The author task, the author will take such a feature of the scene seriously, is very wise. In fact, such as appropriate, simple cloud, movable type grid and other low code products on the market, are also in the cut scene. And products like iVX, the threshold is still too high, it is difficult for non-professionals to use a large area. Scenification seems to be the way to go if it is to be widely used by non-specialists.

Here’s the demo:

In general, Sparrow’s implementation is shown below:

The code directory structure of the project is as follows:

It looks like monorepo is used to manage the project, but there is no configuration of monorepo management tools like Lerna in the project.

This paper mainly analyzes plugin-demo, plugins, Sparrow-cli and Sparrow-utils folders.

A, the plugin – demo

Two examples of plug-ins (essentially components) are shown in this directory: Sparrow-test-box and Sparrow-test-component.

Sparrow-test-box directory structure is as follows:

The sparrow-test-component directory structure is as follows:

Json, tsconfig.json, sparrow.json, and a SRC directory and a dist directory. The dist directory is the result of packing the contents of the SRC directory; Tsconfig. json is a normal TypeScript packaging configuration that needs no explanation.

1, sparrow. Json

{
  "name": "sparrow-test-box"."description": "Test sparrow box"."thumb": "https://unpkg.com/@sparrow-vue/[email protected]/assets/box.png"
}
Copy the code

You place basic declaration information like the plug-in’s name, description, thumbnail, and so on.

2, package. Json

In package.json, the following two libraries are used:

  • @lukeed/uuid

This is a tiny (about 230B), fast UUID (V4) generator for Node.js and browsers.

  • cheerio

Cheerio is a fast, flexible, and concise implementation of jQuery’s core functionality, intended for server-side DOM manipulation scenarios.

They all rely on the @Sparrow-vue/Sparrow-utils library. Packages/Sparrow-utils More on that below.

3, the SRC/config. Ts

Export default {model: {attr: {direction: "", 'content-position': ',}, custom: {label: 'Input text ',},}, schema: { fields: [ { type: 'object', label: '', model: 'attr', schema: { fields: [ { type: "select", label: "direction", model: "direction", multi: true, values: ["horizontal", "vertical", ""] }, { type: "select", label: "content-position", model: "content-position", multi: true, values: ["left", "right", "center", ""] }, ] } }, { type: 'object', label: '', model: 'custom', schema: { fields: [ { type: "input", inputType: "text", label: "label", model: "label" } ] } } ] }, }Copy the code

This should be the plug-in’s property configuration declaration file, used to declare which configuration items should be rendered in the visual editor’s property configuration panel.

4, sparrow – test – box/SRC/index. The ts

This plugin inherits the Box class from @Sparrow-vue/Sparrow-utils.

CustomAttrHandler (customAttrHandler, customAttrHandler, customAttrHandler, customAttrHandler)

public customAttrHandler () {
  const custom = _.get(this.config, 'model.custom');
  const styleKeys = [
    'display'.'flex-direction'.'justify-content'.'align-items'.'flex-wrap'.'style',];const styleArr = [];

  styleKeys.forEach(key= > {
    if (key === 'style') {
      styleArr.push(custom[key]);
      return;
    }
    if (custom[key]) {
      styleArr.push(`${key}: ${custom[key]}`); }});if (styleArr.length > 0) {
    this.styleStr = `style="${styleArr.join('; ')}"`}}Copy the code

5, sparrow – test – component/SRC/index. The ts

It inherits the Component class thrown by @Sparrow-vue/Sparrow-utils.

Second, the plugins

At present, there is no substantial content in this directory, so it should only be placeholder.

Three, the sparrow – utils

By looking at its index.ts, we can see that the package contains four classes: Events, Box, Component and VuePress:

export { default as events } from './events; // 因为Events实际上是个类,所以应该首字母为大写更合理些
export { default as Component } from './Component';
export { default as Box } from './Box';
export { default as VueParse} from './VueParse';
Copy the code
  • Events class

Is an event management class that contains four public methods: on (register listening), off (cancel listening), emit (trigger), destroy (clear all listening).

  • Box class

Should be the base class of the container class. There are getFragment, addComponent, renderComp and other important methods, among others.

  • Component classes

Should be the base class of the component class. There are getFragment, renderFragment and other important methods, as well as a few others.

  • VueParse class

This is a very important class that defines how Vue files are parsed and processed. There are getData, setData, getFormatData, getMethods, getComponents, getImport, getCreated public methods, and an init private method.

In init private method, the following three regular expressions are used to extract the contents of template, script and style of vue. js SFC file respectively:

/<template>([\s\S])*<\/template>/g

/ (? <=<style[\s\S]*>)[\s\S]*(? =<\/style>)/g

/ (? <=<script>)[\s\S]*(? =<\/script>)/g

Next, parse the script part into an abstract syntax tree using @babel/parser (note that this is all running on Node.js, not the browser) :

this.scriptAst = parser.parse(this.vueScript, {
  sourceType: 'module'.plugins: [
    "jsx"]});Copy the code

Then iterate through and update nodes in the abstract syntax tree with @babel/traverse:

public getMethods () {
  let methods:any = [];
  traverse(this.scriptAst, {
    ObjectProperty: (path: any) = > {
      const {node} = path;
      if (node.key.name === 'methods') { methods = node.value.properties; }}});return methods;
}
Copy the code

PS: @babel/traverse allows us to locate specific node types in the syntax tree, such as the second argument passed to the traverse method in the code snippet above.

Fourth, sparrow, cli

This is a command line package, which is the sparrow code package referred to in the official documentation NPM install -g Sparrow -code. A visual editor’s front and back projects can be launched locally by executing Sparrow after NPM install -g Sparrow -code.

The main function of sparrow-plugins, Sparrow-view and Sparrow-server is to load the source files of sparrow-plugins, Sparrow-view and Sparrow-server locally, unpack them, and install the dependencies. Then start the front and back services corresponding to the code in Sparrow-view and Sparrow-server. The effect is shown below:

Among them:

  • When loading locally, we need to obtain the source file compression package path first. The implementation uses the Request-Promise-native library, but this library is not recommended anymore, because it is an extension of the request package that is not recommended now. The concrete implementation is as follows:
const request = require('request-promise-native');
const semver = require('semver');

module.exports = async function getNpmTarball(npm, version, registry) {
  const url = `${registry}/${npm}`;
  const body = await request({
    url,
    json: true});if(! semver.valid(version)) { version = body['dist-tags'].latest;
  }

  if (
    semver.valid(version) &&
    body.versions &&
    body.versions[version] &&
    body.versions[version].dist
  ) {
    const tarball = body.versions[version].dist.tarball;
    return tarball;
  }

  throw new Error(`${name}@${version}Not yet released ');
};
Copy the code
  • Local loading uses a combination of Request and request-progress. Zlib is used for local decompression.
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const request = require('request');
const progress = require('request-progress'); // You can get the percentage, download speed, and remaining time
const zlib = require('zlib');
const tar = require('tar');

/**
 * Download tarbar content to the specified directory
 *
 * @param {string} tarballURL tarball url
 * @param {string} destDir target directory
 */
module.exports = function extractTarball({ tarballURL, destDir, progressFunc = () => {}, formatFilename, }) {
  return new Promise((resolve, reject) = > {
    const allFiles = [];
    const allWriteStream = [];
    const directoryCollector = [];
    progress(
      request({                     / / load
        url: tarballURL,
        timeout: 100000,
      })
    )
      .on('progress'.(state) = > {
        progressFunc(state);
      })
      .on('error'.(error = {}) = > {
        error.name = 'download-tarball-error';
        error.data = {
          url: tarballURL,
        };
        reject(error);
      })
      .pipe(zlib.Unzip())          / /
      .on('error'.(error) = > {
        reject(error);
      })
      .pipe(tar.Parse())
      .on('entry'.(entry) = > {
        const realPath = entry.path.replace(/^package\//.' ');

        let filename = path.basename(realPath);
        filename = formatFilename ? formatFilename(filename) : filename;

        const destPath = path.join(destDir, path.dirname(realPath), filename);

        const needCreateDir = path.dirname(destPath);
        if(! directoryCollector.includes(needCreateDir)) { directoryCollector.push(needCreateDir); mkdirp.sync(path.dirname(destPath)); } allFiles.push(destPath);const writeStream = new Promise((streamResolve) = > {
          entry
            .pipe(fs.createWriteStream(destPath))
            .on('finish'.() = > streamResolve());
        });
        allWriteStream.push(writeStream);
      })
      .on('end'.() = > {
        progressFunc({
          percent: 1});Promise.all(allWriteStream)
          .then(() = > resolve(allFiles))
          .catch((error) = > {
            reject(error);
          });
      });
  });
};
Copy the code
  • The install dependency is based on the cross-spawn implementation, which is a cross-platform solution to spawn and spawnSync in Node. This is more common. The concrete implementation is as follows:
const spawn = require('cross-spawn');

module.exports = (cwd, registry) = > {
  return new Promise((resolve, reject) = > {
    const child = spawn('npm'['install'.'--loglevel'.'silly'.'--registry', registry], {
      stdio: ['pipe'],
      cwd,
    });

    child.stdout.on('data'.data= > {
      console.log(data.toString());
    });

    child.stderr.on('data'.data= > {
      console.log(data.toString());
    });

    child.on('error'.error= > {
      reject(error);
    });

    child.on('close'.(code) = > {
      if (code === 0) {
        console.log('>>> install completed');
        resolve();
      } else {
        reject(new Error('install deps error')); }}); }); }Copy the code
  • For the target directory to download to, user-home is used to retrieve the system’s user home directory under the.sparrow directory.

You can do this by executing the Sparrow command under the.Sparrow folder in your user directory: