Outline design of front-end scaffolding

I. Introduction

      As the concept of front-end engineering continues to deepen, the emergence of scaffolding is a command-line tool introduced to reduce repetitive work. As we all know,
creating a new project is a very cumbersome task. It is necessary to consider the project directory structure, the configuration of the basic library, and various In this process, how to get rid of ctrl + c, ctrl + v, and build the project from zero to one through scaffolding becomes more necessary. In addition, for many systems, their
page similarity is very high, Therefore, it can be built based on a set of templates. Although it is developed by different people, it is built with scaffolding. The same project structure and code writing specifications are very conducive to the later maintenance of the project; the above is the significance of the existence of scaffolding, so
that The project is faster and more standardized from "building-development-deployment".

      At present, in the front-end scaffolding market, the most familiar ones are create-react-app and vue-cli, which can help us initialize the configuration, generate the project structure, and automatically install dependencies. Finally, we can run the project to start development or carry out the project with one line of instructions. Build.
These scaffolds provide best practices in a general sense, but in actual development, it is found that with the continuous development of business, there will inevitably be adjustments to the actual situation of business development. For example:
● Optimize Webpack packaging performance by adjusting plug-ins and configurations
● Project structure adjustment
● Coding style
● User authority control
● Integrate the company’s infrastructure
● Quickly create modules with public/basic services
● …
      In a word, with business development, we A more "personalized" business plan will often be precipitated.
At this time, our most direct approach is to develop a scaffolding of the program so that these best practices and programs can be reused in the future.

Two, ideas

● Decoupling: separation of scaffolding and templates
● Scaffolding is responsible for the construction process, interacting with users through the command line to obtain project information
● Templates are responsible for unifying project structure, workflow, and dependency management
● Scaffolding needs to detect whether the version of the template has been updated, and supports templates Delete and create
● Refer to vue-cli

3. Architecture design

Engineering Architecture.png

Front-end engineering is a complex concept. Many functions in the above figure can be integrated into the scaffolding, which can be selected according to actual needs.

4. Basic process

Five, the three-party library used

serial number library name describe
1 commander Process console commands
2 chalk Command line output beautification
3 latest-version Get the latest npm package
4 inquirer console query
5 download-git-repo git remote warehouse pull (github)
6 figlet chalk
7 glob Match the specified path file
8 not The loading effect of the command line environment
9 clear Clear console output information
10 and symbols Colorful symbols for various diary levels
11 metalsmith Handle template content
12 axios ajax request processing
13 not template engine
14 ncp recursive file copy
15 consolidate template class
16 rollup JavaScript module bundler
17 download download library
18 extract-zip Archive
19 validate-npm-package-name Check project name



6. Function summary

Function overview
insert image description here

7. Basic Specifications

The version method outputs version information
command register command, the parameter is the command name and the parameter description returned to the action method
to output the description of the command
action subscribes to the callback function when the command is triggered
parse parses the incoming parameters and executes the corresponding callback

8. Development and Debugging

to develop

First imagine the function to be realized: initialize the project according to the template (similar to vue create)

  • The core library commander used is used to parse user commands

Obtain user operation commands, for example: gfe-cli create appDemo, namely to obtain the create command

import {
    
     Command } from 'commander';
import {
    
     actionMap } from './contsant/actions';
import {
    
     pkg } from './contsant/pkg';

const program = new Command(pkg.name);

Reflect.ownKeys(actionMap).forEach((actKey: string) => {
    
    
    const action = actionMap[actKey];
    program
        .command(actKey) // 配置命令的名字
        .alias(action.alias) // 配置命令的别名
        .description(action.description) // 命令对应的描述
        .action(() => {
    
    
            
          // 当输入 gfe create appDemo时, 输出create
          console.log(actKey);
        })
});

When the command entered by the user is wrong, the user needs to be prompted. For example, when the user misspells create as craete, the user needs to be informed that the command is not available.

// command not found
if (actKey == "*") {
    
    

  const errorCmd: string = process.argv.pop() as string;
  console.log(chalk.red(errorCmd) + '  ' + chalk.yellow(action.description))
} else {
    
    

  // TODO:这里实现create功能
}
  • Execute user commands

Usually a scaffold will correspond to many kinds of operations, take vue as an example: create add build ...many common commands
so that different files can be planned to achieve corresponding functions
image.png
Create create.ts to realize the functions of new projects

export const create = async (argv: string[]) => {
    
    
  // TODO:业务实现  
}

Call dynamically in index.ts

import * as ActionFn from './actions';

if (actKey == "*") {
    
    

   const errorCmd: string = process.argv.pop() as string;
   console.log(chalk.red(errorCmd) + '  ' + chalk.yellow(action.description))
 } else {
    
    

   // 当actKey为create时,调用上面create函数
   ActionFn[actKey](process.argv);
 }

Implementation of cretae.ts - pull template project
Let me talk about the overall idea of ​​​​realizing this function:
1. Obtain the user input command gfe create appName, appName is the project name of the project created by the user
2. Pull the list of remote templates for the user to choose
3 .After selecting the template, select the Tag corresponding to the
template 4. Pull the corresponding template item according to the template and Tag
5. Simple templates can be copied directly to the root directory of the project, and complex templates need to be parsed and rendered related variables

In actual scenarios, we usually put the template source code on the company's private npm. Here we take github as an example. The implementation methods are the same, just call the corresponding api.
github api documentation: https://docs.github.com/en/ rest

  • Provide --help help prompt

First, we define the content of the prompt

export const actionMap = {
    
    
    create: {
    
    
        alias: 'c',
        description: 'create a project',
        examples: [
            'gfe-cli create <project-name>'
        ]
    },
    add: {
    
    
        alias: 'a',
        description: 'add a component',
        examples: [
            'gfe-cli add component <component-name>'
        ]
    },
    config: {
    
    
        alias: 'conf',
        description: 'config project',
        examples: [
            'gfe-cli config set <k> <v>',
            'gfe-cli config get <k>'
        ]
    },
    list: {
    
    
        alias: 'ls',
        description: 'list all projects',
        examples: [
            'gfe-cli list',
            'gfe-cli ls'
        ]
    },
    '*': {
    
    
        alias: '',
        description: 'command not found',
        expmples: []
    }
};

The definition prompt command is usually: --help

// listen help command
program.on('--help', () => {
    
    
    Reflect.ownKeys(actionMap).forEach((actKey: string) => {
    
    
        const action = actionMap[actKey];
        (action.examples || []).forEach(example => {
    
    
            console.log(chalk.cyan(`   ${
      
      example}`));
        });
    });
});

image.png

Initialize project configuration

  • parse template

Here we use the metalsmith library for rendering, and metalsmith can resolve <%=name=> into the corresponding name value.
When creating a project, we usually need to resolve the name in package.json into a user-defined project name.
In the template, we can do this bind variable

{
    
    
  "name": "<%=projectName%>"
  ...
  ...
}

After the analysis is completed, the corresponding package.json is as follows

{
    
    
  "name": "app-name"
  ...
  ...
}

When selecting a template, the usual interaction is to ask the user on the command line, and the user makes a selection or input. Take vue as an example:
image.png
here we inquirerimplement the user's query interaction through the library, and usually we put the files that need to be queried into one ask. In the json file,
for example, ask during the process of creating a project: whether the project properties are private, author name, project description

[
	{
    
    
		"type": "confirm",
		"name": "private",
		"message": "ths resgistery is private?"
	},
	{
    
    
		"type": "input",
		"name": "author",
		"message": "author?"
	},
	{
    
    
		"type": "input",
		"name": "description",
		"message": "description?"
	}
]

In this way, we have uniformly obtained the user input information, which can be rendered into the project package.json through metalsmith

const args = require(path.join(dest, 'ask.json'));
const result = await inquirer.prompt(args);
// result -> 用户输入结果  { private:false,author:'zhang01',description:'test desc' }

local test

The bin field is added to package.json, which is a mapping between an executable command and a local file name.
Execute npm link in the project root directory, which will generate a symbolic link under the global node_modules, and then the package can be used globally. The command name of the bin field in json

test in project

Register the private npm source through nrm, release a new version of scaffolding,
put into use the functions of the current cli in the involved projects

Nine, source code

/**
 * 新建项目
 */
import inquirer from 'inquirer';
import Metalsmith from 'metalsmith';
import ncp from 'ncp';
import path from 'path';
import util from 'util';
import {
    
     askFileName, downloadDir, loadingFn } from '../contsant';
import {
    
     downloadFile, fetchaTags, fetchRepos } from '../service';
const promisify = util.promisify;
const ncpPromise = promisify(ncp);
const {
    
     ejs } = require('consolidate');
const render = promisify(ejs.render);
const fs = require('fs-extra');
import chalk from 'chalk';
import extract from 'extract-zip';
const validateProjectName = require('validate-npm-package-name');

export const create = async (argv: string[]) => {
    
    

    // 用户创建的项目名
    const projectName: string = argv.slice(3)[0];

    const result = validateProjectName(projectName)
    if (!result.validForNewPackages) {
    
    
        console.error(chalk.red(`Invalid project name: "${
      
      projectName}"`))
        result.errors && result.errors.forEach((err: any) => {
    
    
            console.error(chalk.red.dim('Error: ' + err))
        })
        result.warnings && result.warnings.forEach((warn: any) => {
    
    
            console.error(chalk.red.dim('Warning: ' + warn))
        });

        return false;
    }

    // 选择模板
    const repos = await loadingFn(fetchRepos, 'fetching...')(true);
    // console.log(repos);
    const names = repos.map((t: any) => {
    
    

        return {
    
     name: t.name }
    });

    // 获取选择结果
    const {
    
     repo } = await inquirer.prompt({
    
    
        name: 'repo',
        type: 'list',
        message: chalk.cyan('please choose a template to create project'),
        choices: names
    });
    const targetRepo = repos.find((r: any) => r.name == repo);
    const projectId: string = targetRepo.id;

    // 选择tag
    const tags = await loadingFn(fetchaTags, 'fetching tags...')(projectId);
    const tagNames = tags.map((t: any) => t.name)
    const {
    
     tag } = await inquirer.prompt({
    
    
        name: 'tag',
        type: 'list',
        message:  chalk.cyan('please choose a tag'),
        choices: tagNames
    });

    // 下载模板文件
    const repositoryName: string = targetRepo.name;// 用户选择选择模板名称
    const source = `${
      
      downloadDir}/${
      
      repositoryName}.zip`;
    if (fs.existsSync(source)) fs.removeSync(source);
    const data = await downloadFile(projectId, tag);
    await fs.promises.writeFile(source, data);

    // 解压当前版本库- 解压到同级同名文件夹内
    const dest = `${
      
      downloadDir}/${
      
      repositoryName}`;
    if (fs.existsSync(dest)) fs.removeSync(dest);
    if (!fs.existsSync(dest)) fs.mkdirSync(dest);
    await extract(source, {
    
     dir: dest });


    // 是否需要渲染
    const isNeedRender: boolean = fs.existsSync(path.join(dest, askFileName));
    if (!isNeedRender) {
    
    

        // 简单模板 直接复制模板文件到当前目录
        await ncpPromise(dest, path.resolve(projectName));
    } else {
    
    
        // 渲染模板
        await new Promise((resolve, reject) => {
    
    
            Metalsmith(__dirname)
                .source(dest)
                .destination(path.resolve(projectName))
                .use(async (files, metal, done) => {
    
    
                    const args = require(path.join(dest, askFileName));
                    const result = await inquirer.prompt(args);
                    const meta = metal.metadata();
                    Object.assign(meta, result);
                    delete files[askFileName];
                    done();
                })
                .use((files, metal, done) => {
    
    
                    const meta = metal.metadata();
                    const mergeMeta = Object.assign(meta, {
    
    
                        projectName: projectName
                    });
                    Reflect.ownKeys(files).forEach(async (fileKey: any) => {
    
    
                        // 需要处理内容的文件类型: ts,js,json
                        if (fileKey.includes('.js') || fileKey.includes('.ts') || fileKey.includes('.tsx') || fileKey.includes('.json') || fileKey.includes('.yml')) {
    
    
                            // 处理文件内容
                            let content = files[fileKey].contents.toString();
                            if (content.includes('<%')) {
    
    
                                content = await render(content, mergeMeta);
                                // 更新文件内容
                                files[fileKey].contents = Buffer.from(content);
                            }
                        }
                    });
                    done();
                })
                .build(err => {
    
    
                    err ? reject() : resolve();
                })
        });
    }

}

10. Deployment

gfe-cli is directly published locally to npm on the intranet

Guess you like

Origin blog.csdn.net/gaojinbo0531/article/details/129294688