Node.js Journey (18) - a half-baked visual construction system

The management background maintained by our group will receive a lot of development requirements. Every time a new page is opened, the relevant code will be copied and pasted everywhere.

And also frequently flip through the document, first enter the WIKI address in the bookmark or address bar, then find the documentation, and then locate the component you want to see.

Although the time lost by a single person is not very much, it still interrupts the thinking and affects the fluency of development. When the time of everyone is added up, the wasted time is also considerable.

In order to improve the development efficiency of team members, we began to conceive a visual construction system. Ideally, components are dragged, interactions and styles are configured, and the page is generated, ready to use.

However, to complete this set of functions, the development cost is relatively high. Now I want to solve the current pain points first, reduce the frequency of code duplication and quickly read the component documentation.

For this reason, after thinking about it for many days, I plan to develop a half-baked visual construction system.

The so-called half-baked means that after the construction is completed, click Generate, and two script files, view and data, will be created in the background, permissions will be automatically added, and a menu bar will be added. However, we have to continue to develop and improve the page functions in the future.

1. Interface

The interface is divided into left and right parts, the left is the configuration area, and the right blank is the component preview area.


1) Component area

The first drop-down box in the component area can select Ant Design and some template components. After selecting, the link of the component address will be replaced. Click to jump to the component's documentation.

The second drop-down box can select the required components on the page, such as the prompt component in the figure. After clicking Add, it will be displayed on the right, and a delete icon will also be provided. Currently, the drag effect is not supported.


2) Configuration area

In the configuration area, you can enter information such as menu name, route, file directory and permissions.

In the past, you had to manually add configuration items in the routing and permissions files, but now they can be automated.

The principle is to use Node to read the two files separately to get an array, then stuff the configuration content into this array, and then serialize the array into the file.

Note that it is required to delete the module cache before importing the module (calling the require() function), otherwise the content of the previous file will be read.

//Absolute path to permissions file
const absAuthorityPath = pathObj.resolve(__dirname, 'src/utils/authority.ts');  
delete require.cache[absAuthorityPath];              //delete module cache
const authorities = require(absAuthorityPath);
const obj = {
  id: authority,
  pid: parent,
  name: menu,
  desc: '',
  routers: currentPath,
authorities.push(obj);       //Add permission
//write to file
fs.writeFileSync(absAuthorityPath, `module.exports = ${JSON.stringify(authorities, null, 2)}`);

fs.writeFileSync() is used to write files synchronously. module.exports is Node's module syntax, and export default is ES6 syntax, which Node does not natively support. Fortunately, webpack supports these modular syntaxes.

Once you click the Generate File button, after the project is rebuilt, the menu name just configured (for example, the name is called menu test) will appear in the menu list on the left, and you can jump to it, and the permissions are also added.


View and data files are also created with Node. Write a template string in the Node project (below is the function that generates the view template), and pass the variable part as a parameter.

export function setPageTemplate({name, antd, namespace, code='', props, component}) {
  return `import { connect, Dispatch, ${namespace}ModelState } from "umi";
import { setColumn } from '@/utils/tools';
import { TEMPLATE_MODEL } from '@/utils/constants';
// page parameter type
interface ${name}Props {
  dispatch: Dispatch;
  state: ${namespace}ModelState;
// global declaration
const ${name} = ({ dispatch, state }: ${name}Props) => {
  // dispatch({ type: "xx/xx", payload: {} });
  // state
  // const { } = state;
  // Common component configuration
  return <>
export default connect((data: {${namespace}: ${namespace}ModelState}) => ({ state: data.${namespace} }))(${name});`;

2. Configuration

Configuration is the core of this system. It has been conceived for a long time. Originally, considering the flexibility of the system, I wanted to directly provide a script editing box and customize the logic.

However, there is a problem, that is, I am currently developing in the TypeScript language, so when I customize the script logic, I also need to use the TypeScript syntax.

provided by the browser eval() The function does not support TypeScript syntax and needs to be translated first. After searching online, I got a solution and downloaded the TypeScript library.

But it keeps reporting an error, and I found some solutions on the Internet ( Option OneOption II ), but not for my current project environment.

Critical dependency: the request of a dependency is an expression

Critical dependency: the request of a dependency is an expression

Module not found: Can't resolve 'perf_hooks' in 'C:\Users\User\node_modules\typescript\lib'

Finally decided to temporarily abandon the custom script logic, solve the current pain points first, and bring the system online as soon as possible.

During the period, we also encountered a relatively hidden bug. As shown below, the array will first call toString() to convert it into a string, and finally become eval("(1, 2)"), so the value obtained is 2.

eval(`(${[1,2]})`);  //2

I also encountered a problem, that is, using JSON.stringify() When serializing an object, if the parameter is a function, it will be filtered out.

JSON.stringify({func:() => {}});  //"{}"

1) Material library

The components in the material library are divided into two types, one is custom Backend template component , the other is a third-party Ant Design 3.X components.

In order to quickly build the page, the selected component is the former. This time, I used TypeScript to improve the type declaration of the component code again.


The latter is only used for document queries and splicing import statements in template strings, as shown below.

`import { ${antds.join(',')} } from 'antd';`

2) Custom Components

The declaration of the custom component is in JSON format, and the type of the TypeScript declaration is as follows.

interface OptionsType {
  value: string;
  label: string;
  children: Array<{
    value: string;
    label: string;
    link: string;   //link address
    readonlyProps?: ObjectType;  //Properties that affect the rendering of components and cannot be configured
    readonlyStrProps?: string;   //String properties to be concatenated
    handleProps?: (values:ObjectType) => ObjectType;    //Process specific component properties after formatting the form data
    handleStrProps?: (values:ObjectType) => string; //Concatenate properties that cannot be converted to strings
    props: Array<{
      label: string;
      name: string;
      params?: ObjectType;
      control: JSX.Element | ((index: number) => JSX.Element);
      type?: string;
      initControl?: (props:any) => JSX.Element;

The link address is the address of the description document. Among the properties of the component, part of it is the callback function, and the custom callback logic has been abandoned at present.

So this part of the property needs special treatment (declared in readonlyProps) and cannot be entered in the interface.

        readonlyProps: {
          initPanes: (record: ObjectType): TabPaneType[] => [
              name: "Example",
              key: "demo",
              controls: [
                { label: 'test component', control: <>content</> }

readonlyStrProps is the string format corresponding to readonlyProps. This property will also add some other properties. With comments, it is also equivalent to a component document.

        readonlyStrProps: `,
        // Label bar content callback function, the parameter is record,When the tab bar has only one item, the menu will not be displayed
        "initPanes": (record: ObjectType): TabPaneType[] => [
            name: "Example",
            key: "demo",
            controls: [
              { label: 'test component', control: <>content</> }
        // useEffect The callback function in the hook, the parameters are record
        "effectCallback": (record: ObjectType) => {}`,

handleProps() is a callback function. After the form receives data, some components need to do special processing again.

For example, adding some specific attributes, merging array elements into strings, etc., can be smoothly presented in the preview interface.

        handleProps: (values:ObjectType) => { //Process the values ​​in the form
          // Special handling of interface arrays,from['api', 'get']convert to api.get
          values.url && (values.url = values.url.join('.'));// Components needed to initialize the form
          if(values.controls.length === 0) {
            values.controls = [
                label: "Example",
                name: "demo",
                control: <>test component</>
          }else {
            values.originControls = values.controls;    //array of backup component names
            values.controls = => getControls(item));
          delete values.controlskeys; //Remove redundant properties
          return values;

handleStrProps() is used when outputting templates to write those special properties as strings.

        handleStrProps: (values:ObjectType):string => {
          if(values.controls.length === 0) {
            delete values.originControls; //delete backup array
            delete values.controls;   //delete original attribute
            return `,"controls": [
                label: "Example",
                name: "demo",
                control: <>test component</>
          // Component name array handling
          const newControls = => getStrControls(item));
          delete values.originControls;
          delete values.controls;
          return `,"controls": [${newControls.join(',')}]`;

After a series of processing, some string codes are passed to the interface, and the interface is finally spliced ​​into two files and output to the specified directory.

However, the typesetting of the generated code is a bit confusing, and it needs to be manually formatted every time.


Posted by davey_b_ on Thu, 02 Jun 2022 03:31:31 +0530