When companies use microfrontend development, they always create child applications. Before CV part of the code structure, and then manually change the configuration, which is not only a waste of time, and it is very likely to not run offline/online because of a small place not modified. So how to fix this pain point? Scaffolding comes to mind.

As a front-end developer, you’ve probably all used vuE-CLI or some other scaffolding, so recall our process for creating a VUE project:

NPM -g install vue-cli // 2. Vue create test-project // 3. For example, whether vuE-Router is installed and whether TS is used in VUEX, Please pick a preset:... CD test-project NPM run serve // 4Copy the code

What if we built our own project from scratch? Create a folder, initialize NPM, initialize Git, handload Webpack, configure various loaders and plugins, study how to practice tsconfig, etc… Without a doubt, creating projects through scaffolding frees up our hands and minds, and without having to worry about how to configure WebPack and which dependencies to install, a working project comes out with a few clicks of the keyboard. It follows that:

  1. Scaffolding helps us initiate projects quickly and efficiently;
  2. Helped us develop a set of relatively standard specifications;
  3. Provides solutions for various scenarios (testing, packaging) in development.

1. Analyze the scaffolding execution process

Without further ado, let’s get right to it. First look at the results:

So we have to analyze, what is the execution process of scaffolding:

Visible, as long as the function of the four modules to achieve, then our scaffolding is done. Here I take EV-CLI as an example, step by step to build a scaffolding that meets the requirements. Our final project structure is as follows:

├ ─ ─ the README. Md ├ ─ ─ bin │ └ ─ ─ evcli. Js ├ ─ ─ package. The json ├ ─ ─ the SRC │ ├ ─ ─ the create. Ts │ ├ ─ ─ main. Ts │ └ ─ ─ util │ ├ ─ ─ commands. The ts │ │ ├ ─ ─ the ts ├ ─ ─ files. The ts │ ├ ─ ─ git. Ts │ └ ─ ─ prompt. Ts ├ ─ ─ tsconfig. Json └ ─ ─ yarn. The lockCopy the code

2. Create an executable environment

2.1 Creating a Global execution File

In the project folder bin/evcli.js write:

The first sentence is used to declare the Node environment where the script will be executed./usr/bin/env node
require('.. /lib/main.js');
Copy the code

Also add the following to package.json:

  "bin": {
    "ev-cli": "./bin/evcli.js"
  },
Copy the code

2.2 npm link

Now our directive is not a global directive. We need to execute the NPM link to add the EV-cli NPM package to the global directive. If we wrote console.log(‘hello’) in main.ts, then ev-cli will now print Hello in CMD.

3. Processing instructions

How are common directives such as -v, -h, or –version parsed? Here we use a library — Commander. You can specify commands, aliases, descriptions, actions, and so on via chains:

program.command(XXX)
    .alias(XXX)
    .description(XXX)
    .action(() = > {
        XXX();
    });
Copy the code

We can use an array to list all commands: util/commands.ts:

const mapActions = {
    create: {
        alias: 'c'.description: 'Create a child app'.examples: [
            'ev-cli create <submodule-name>']},The '*': {
        alias: ' '.description: 'command not found'.examples: []}};export default mapActions;
Copy the code

Importing version information from package.json:

const { version } = require('.. /.. /package.json');

export default version;
Copy the code

Handling logic in main.ts:

import program from 'commander';
import version from './util/constants';
import mapActions from './util/commands';
import create from './create';

// Iterate over the register directive
Reflect.ownKeys(mapActions).forEach((action: string | number | symbol) = > {
    program.command(action as string)
        .alias(mapActions[action].alias)
        .description(mapActions[action].description)
        .action(() = > {
            if(action === The '*') console.log(mapActions[action].description);
            else if(action === 'create') create(process.argv[3]);
        });
});

// The help directive in program does not print Examples
// So we add the print example manually
program.on('--help'.() = > {
    console.log('\nExamples:');
    Reflect.ownKeys(mapActions).forEach((action) = > {
        mapActions[action].examples.forEach((example: string) = > {
            console.log(`${example}`); })})})// Run --version to view the current version
program.version(version).parse(process.argv);
Copy the code

4. Configure

4.1 introduced inquirer

There are often scenes of dialogue in scaffolding:

  • Enter a name
  • Select whether to use XX
  • Choose an appropriate version

Inquirer is a library for doing just that. The basic usage method is also to configure an array:

const inquirer = require('inquirer') 
inquirer.prompt([ 
    { 
        type: 'confirm'.name: 'isLatest'.message: 'Are you using the latest version? '.default: true 
    }
]).then((answers) = > { console.log(answers) })    // { isLatest: true }
Copy the code

Create logic can be implemented in create.ts:

import inquirer from 'inquirer';
import { promptList } from './util/prompt';

const create = async (projectName: string) = > {console.log(The name of the subapplication folder is${projectName}`);
    const answers = await inquirer.prompt(promptList);
    console.log(The result is:);
    const { namespace, port, framework } = answers;
};

export default create;
Copy the code

4.2 configuration promptList

But the promptList quoted is taken out separately, in utill /prompts. Ts:

interface PromptListItem {
    type: string;
    name: string; message? :string; validate? :(v: string) = > Promise<string | boolean>; choices? :string[]; 
    default? :string;
};

export const promptList: PromptListItem[] = [
    {
        type: 'list'.name: 'framework'.message: 'Please select a framework for this child application'.choices: ['react'.'vue'].default: 'react'
    },
    {
        type: 'input'.name: 'namespace'.message: 'Please enter the subapplication route id :'.validate: (input) = > {
            return new Promise((done) = > {
                setTimeout(() = > {
                    if(! input) { done('Invalid input.');
                        return; 
                    }
                    done(true);
                }, 0); })}}, {type: 'input'.name: 'port'.message: 'Please enter port number :'.validate: (input) = > {
            return new Promise((done) = > {
                setTimeout(() = > {
                    if(! input ||typeof Number(input) ! = ='number') {
                        done('Invalid input.');
                        return; 
                    }
                    done(true);
                }, 0); }}})];Copy the code

5. Pull code

5.1 Encapsulating Git Clone

Git Clone: Git clone: git Clone: git Clone Different templates like Vue or React I choose to put in different branches of the remote repository so that the framework type framework I just interacted with can be passed into the cloneGitRepo method as a branch name.

import { execSync } from 'child_process';
import fs from 'fs';

const cloneGitRepo = (repo: string, name: string, branch: string) = > {
    const path = `./packages/${name}`;
    if(fs.existsSync(path)) {
        console.log('Module with the same name already exists');
        return false;
    }
    try {
        execSync(
            `git clone ${repo} ${path} --branch ${branch}`
        );
        return true;
    } catch (error) {
        console.log(`clone ${repo}Failure \n Error cause:${error}`); }};export default cloneGitRepo;
Copy the code

5.2 Using ora to Display ICONS

For example, when we pull the code, we usually need to wait for a while, and there will be a loading state. It is better to show the user the mark of loading and the mark of success after the pull code succeeds, so that the interaction will be more friendly. This can be done very simply with ora, adding the following code to util/prompts. Ts:

export const fnLoadingByOra = async (fn: () = > boolean|void|undefined.message: string) = > {// The logic is easy to understand
    // When an object is created, the corresponding icon and copy are displayed according to the result of the callback
    const spinner = ora(message);
    spinner.start();
    const res = await fn();
    if(res ! = =false) spinner.succeed(message + 'Success');
    else spinner.fail(message + 'Fail');
    return res;
};
Copy the code

Now that we’ve wrapped the fnLoadingByOra function, we can wrap another layer. Under create.ts add:

const fetchLoading = async (name: string.branch: string) = > {const res = await fnLoadingByOra(() = > { 
        const res = cloneGitRepo('XXXX:XXX/ev-submodule-template.git', name, branch); 
        return res;
    }, 'Pull template');
    return res;
};
Copy the code

With this method in place, we continue to add the logic inside create:

const create = async (projectName: string) = > {console.log(The name of the subapplication folder is${projectName}`);
    const answers = await inquirer.prompt(promptList);
    console.log(The result is:);
    const { namespace, port, framework } = answers; const res = await fetchLoading(projectName, framework); if(! res) return; new FileService(projectName,namespace, port);
    fnLoadingByOra(() => {}, 'Template Configuration');
};
Copy the code

What is FileService? Yes, this is our final step – template replacement.

6. Template replacement

For templates, those of you who have studied VUE will be familiar with:

<div>{{name}}<div>
Copy the code

Template substitutions have been common since the early DAYS of SSR server rendering. What we need to do is to render the previously configured information to the appropriate location. In this project, THE template I used was <%XXX%>. For example, publicPath in the webpack template could be written as:

publicPath: '/<%namespace%>'
Copy the code

After substitution, we get:

publicPath: '/test'
Copy the code

Matching with regex is also simple:

const reg = / \ < % (. +?) %\>/g;
Copy the code

The replacement process is also easy to understand: read the file -> re match -> replace -> write the replaced copy to the original file. The concrete implementation is to encapsulate the file processing into a class:

import fs from 'fs';

const reg = / \ < % (. +?) %\>/g;
const DIR = ['build'.'src'.'templates'];

class FileService {
    constructor(projectName: string.namespace: string, port: string) {
        const path =  `./packages/${projectName}`;
        this.writeFile(`${path}/package.json`, projectName);
        DIR.forEach((dir) = > {
            this.readDocuments(`${path}/${dir}`, projectName, namespace, port); }); } // replaceVar(text: string, projectName= ",namespace='', port='') {
        const newText = text.replaceAll(reg, (match, value) = > {
            if(value === 'projectName') return projectName;
            else if(value === 'namespace') return namespace; else if (value === 'port') return port; return ''; }); return newText; } // writeFile writeFile(path: string,... args: string[]) {const res = fs.readFileSync(path, 'utf8');
        const newText = this.replaceVar(res, ... args); fs.writeFileSync(`${path}`, newText);
    }

    // Read the file name under the folder
    readDocuments(path: string. args:string[]) {
        const res = fs.readdirSync(path, 'utf8');
        res.forEach((file) = > {
            this.writeFile(`${path}/${file}`. args); }); }; }export default FileService;
Copy the code

Now that we have implemented the scaffolding, TSC compiles ts and executes it on the command line to see the effect. Of course, if you want to put scaffolding into production, you can publish it as a package that you can download and use later.