In our business, we can quickly generate Vue projects through vue-CLI scaffolding, and we can also develop a CLI scaffold to quickly generate the business base model/architecture that we extract daily. This article will explain in detail how scaffolding is developed, the technical details and pits involved, as well as various third-party packages to ensure that even small white students can follow their own CLI.

Act like a bully! Promotion force!! A promotion and a raise!! Win marry Bai Fumei !!!! What are you waiting for? Let’s get started

It feels like a dream

So let’s first think about what our scaffolding is going to help us do. For example, here we implement a VTA-CLI, which initializations the basic project architecture of a Vue+Ts+ElementUi RBAC system by running a VTA Create My-app at the terminal. With this goal in mind, we can break down the steps to achieve it:

  • Support terminal Commandsvta
  • vta create my-appAfter the command is executed, check whether the current file name exists
  • Pull your template project from Git to local
  • Copy the currently downloaded resource to our target address
  • Update package.json and other file contents (e.g. name, author, version field changes, etc.)
  • Initialize Git in your project to manage your project
  • Automatically installs the required dependencies for the current project
  • Run the app

πŸ‘‡ below we explain the concrete implementation process step by step, keep up with the team don’t lag behind ha ~ ~ ~

✨✨ Initializes the project infrastructure

  • First create a folder, manually click create or run the command on the terminal:
The terminal creates the project root folder and goes to the root folder
mkdir vta-cli && cd vta-cli

# Create bin and SRC folders
mkdir bin src
 # bin creates init.js as the entry file for the script cd bin && touch init.js  # and type the following in init.js: #! /usr/bin/env node console.log('Hello, my bin! ')  # Initialize NPM's package management file and execute in the root directory This command will ask you for many configuration parameters. If you don't want to ask, just add -y to the end npm init Copy the code

The bin folder is used to store our command entry file, init.js is our entry file (name as you like), and SRC is where we actually implement the script command logic:

Vta – CLI project directory folder
  • Next we need to configurepackage.jsonFile to add our script commands to. Open up ourpackage.jsonFile, in which to addbinFields:
{
    "bin": {
        "vta": "bin/init.js"
    },
}
Copy the code

This is where we define the vta command to be run at the terminal. When vta is run, the program will run the bin/init.js script that we configured. When installing a package, NPM automatically queries the bin command and adds it to the node_modules/.bin file as a shell command. So when your package is installed in a local project, its bin commands are locally runnable, and when installed globally they become globally runnable commands.

Note that it does not have to be a JS file, in fact, in Linux everything is a file, there is no suffix provisions, at least for “people” to identify it.

Just to highlight: the first line of the init.js file, which must be the first line, we added#! /usr/bin/env nodeThis code specifies the environment in which our script will run, and customizes the node command to be prefixed when we run the vta commandnode vta.

  • Next, to facilitate our testing, we need to send the package to a global environment that is not local. We can run the following command:
# Terminal run command (need in the current project root directory)
npm link
Copy the code

Note that NPM link is when we link the current package to the local global environment, like when we install dependencies using the -g parameter to wrap some of the packages into the global environment, it is used to facilitate our local development testing, it will let us develop automatic hot update. If you are not familiar with NPM Link, you can go to the NPM website to check the use of NPM Link and continue to learn.

But I want to say this, and a lot of people get potholes here:

  • First, it’s best to change your NPM mirror source to NPM’s own mirror source (if you specify taobao, etc.). Especially if you need to release an NPM repository.
  • Second of all, it has to bepackage.jsonThe configuration ofThe node_modulesAnd so on irrelevant folder to remove (or specify what we need), can also pass.gitignoreYou can ignore the configuration file, or.npmrcAnd so on. You can set it anywhere, because the NPM configuration value is a set of sequential rules, you can refer to the NPM documentation if you are interested. Here is a demonstration of how topackage.jsonFile configuration:
{
    "files": [
        "./bin".        "./src"
].} Copy the code

Json. We tell NPM which files we should include by specifying the files folder in the package.json file. It will also be included by default. This is also what we need to configure when publishing the package to NPM, i.e., which files to publish to the NPM repository.

Note that you can also pass the excluded field, exclude. However, many times it is more convenient to specify which files we need. Again, node_modules must be excluded, otherwise NPM link will be too slow and will fail more often

That seems to make a lot of sense
  • The test command
# Terminal run
vta

When the script is executed, you will see the terminal output
# indicates that the script is successfully executed
Copy the code

Again, make sure to add a gaza rod to the first line of init.js, as follows:

#! /usr/bin/env node
console.log('Run tests')
Copy the code

❀️❀️ Solution to the CLI

Commander.js is a complete solution to the NodeJS command-line interface. Can help us define various command line commands/arguments, etc. For example, we want to define the create command, or -v as the version query parameter. Let’s see how to use it:

  • The installation
cnpm install commander -S
Copy the code
  • Introduction to use
// Introduce it in init.jsconst { Command } = require('commander');
// Import the package.json file in the current root directory,// To obtain the corresponding field value, such as version versionconst package = require('.. /package');
/ / initializationconst program = new Command(); Copy the code
  • Description of the version command and the help command
// 
/ / so,
program
  .version(package.version, '-v, --version'.'display version for vta-cli')
  .usage('<command> [options]');
//  Copy the code

By calling the version method to define the function of the command line command version, we can enter vta -v on the command line to get the current version information.

Calling the usage method defines the copywriting title of our help command, similar to the feeling of defining the table header, as shown in the figure below, when we type vTA-h, it is the part of the definition shown in the blue box:

Version information demo code

Note that the third parameter to the version method here is the description we defined, as shown in red in the figure above. Help defaults to the same value

  • Define command line arguments
/ * ** Define vTA parameters* / 
program
  .option('-y, --yes'.'run default action')
 .option('-f, --force'.'force all the question');  / * ** You can judge that when the user enters the corresponding parameters,* We can do some operations:* / if (program.force) {  // do something.. } Copy the code

Using the option method, we define our command-line arguments, such as vta -f, as equivalent to vta –force. Note that the first argument is the definition command line argument, which contains a short name (1 character) and a long name, no more. The second parameter is the description of the definition. Note that only long names, such as program.f, can be used to determine parts of the code.

  • Create a subcommand

Creating subcommands is an important part. For example, when we use vue create my-app to create a project, create is a subcommand of the vue command and my-app is a command parameter. Here we also define a subcommand:

/ * ** Call the command method to create a create command,* Also, the create command must be followed by a command argument* If you run vTA Create on a terminal without a name, an error message will be displayed to the user* /
program.command('create <name>')  // Define the description of the command  .description('create a vta template project')  // Specify some parameters for the command  // Finally, we can parse these parameters, and then implement the corresponding logic according to the parameters  .option('-f, --force'.'Ignore folder checks, overwrite folders if they already exist')  / * ** Finally define our implementation logic* source indicates the name parameter currently definedDestination is the terminal CMD object from which we can resolve what we need* /  .action((source, destination) = > {  / * ** For example, we put the implementation logic in another file to implement,* Facilitates code decoupling,* Since destination is a messy parameter, parse it here and then pass it in* You can define a parsing utility function* /  new CreateCommand(source, destination)  }); Copy the code

What is the destination object? It’s still a lot of content. What we need to focus on is this part of the red box, which is the list of all the parameters that we define for this command, we variable this list, take the values of the blue part of the graph, resolve the next part, and then use it as a key to get a match across the CMD object, whose values are the values of the parameters that the user entered.

CMD object display

For example, you might define a parsing utility function:

/ * * * parseCmdParams
* @description Parses the parameters entered by the user* @param {} CMD cammander.action* @returns {Object} Returns a key value Object for a user argument* / exports.parseCmdParams = (cmd) = > {  if(! cmd)return {}  const resOps = {}  cmd.options.forEach(option= > {  const key = option.long.replace(/ ^ - /.' ');  if(cmd[key] && ! isFunction(cmd[key])) { resOps[key] = cmd[key]  }  })  return resOps } Copy the code

The above parsing method is implemented in a similar way to our vue-CLI.

  • Complete resolution
/ * ** Remember to call the parse method in program.parse(),* instead of calling xxx.parse() directly after the chained call above,* Otherwise it will be processed as the current Command parse, so help commands, etc., are not what you expect* /
try {  program.parse(process.argv); } catch (error) {  console.log('err: ', error) } Copy the code

Parse (process.argv) is not used to parse the program. Parse (process.argv) is not used to parse the program. Remember that! Remember that! Remember!! For more detailed commands, see the Commander documentation.

🌞 Check the target path

As you can see from the above steps, we have defined the vta create

command, which initializes our CreateCommand class when we run the vta create my-app command. Below we look into how to implement this logic: we first create the SRC/command/CreateCommand js this file to achieve our logic:

/ * ** class project creation command *
 * @description
* @param {} source Specifies the folder name provided by the user* @param {} destination Parameter of the create command* / class Creator {  constructor(source, destination, ops = {}) {  this.source = source  this.cmdParams = parseCmdParams(destination)  this.RepoMaps = Object.assign({  repo: RepoPath, // The remote address constant placed in the configuration file  temp: path.join(__dirname, '.. /.. /__temp__'),  target: this.genTargetPath(this.source)  }, ops);  this.gitUser = {};  this.spinner = ora();  this.init();  }   // Other instance methods  // ... }  // Finally export the class module.exports = Creator; Copy the code

Let’s take a look at what we’re doing with this constructor. The first thing we’re doing is assigning the arguments passed in during instantiation to this object for use in other instance methods. Then we define the RepoMaps property to set some of our basic parameters, such as the address repo of the project template, the address temp of the temporary project template stored inside our local CLI project, and finally the target address taregt to which we need to install the project. Because the project will eventually be installed under the address where the terminal is running, and your scaffold pack will be installed at another address.

Then we defined gitUser to hold the git information of the user, and then we got the information from the automatic command, and finally we stuffed the information into the package.json file.

this.spinner = ora(); To instantiate a Daisy graph, we can call this.spinner to do the “Daisy spin” when executing the command.

Let’s implement this init method:

// Initialize the function
async init() {
    try {
      // Check whether the destination path file is correct
      await this.checkFolderExist();
 // Pull the vue+ts+ele project template from Git  // In a temporary folder  await this.downloadRepo();  // Copy the downloaded resource files to the target folder  await this.copyRepoFiles();  // Modify the package.json information in the project template according to the user git information  await this.updatePkgFile();  // Initialize git for our project  await this.initGit();  // Finally install dependencies, start projects, etc!  await this.runApp();  } catch (error) {  console.log(' ')  log.error(error);  exit(1)  } finally {  this.spinner.stop();  } } Copy the code

As you can see from the code comment above, our init method simply executes a series of operations in a single call. Finally, let’s look at the configuration file:

exports.InquirerConfig = {
  // The query parameter for the existing name of the folder
  folderExist: [{
    type: 'list'.    name: 'recover'. message: 'The current folder already exists, please select action:'. choices: [  { name: 'Create a new folder'.value: 'newFolder' },  { name: 'cover'.value: 'cover' },  { name: 'exit'.value: 'exit' },  ] }]. // Rename the query parameter  rename: [{  name: 'inputNewName'. type: 'input'. message: 'Please enter a new project name:'  }] }  // Remote Repo address // If you do not have your own project, you can first call my address exercise // You can practice at any address exports.RepoPath = 'github:chinaBerg/vue-typescript-admin' Copy the code

We’ll see later how to implement this set of methods.

🌞 Chrysanthemum graph tool of the terminal

First of all, introduce our little chrysanthemum! When we perform various operations, such as pull template data, etc., are waiting for the actual, can be used for the waiting process, we can turn around in the end there is a small chrysanthemum, this will give the user experience better, let the user know the current script in the execution load, as shown in figure (the left there is a small chrysanthemum in turn around ~ ~ ~) :

Rotating ORA chrysanthemum diagram

Ora is a tool for this terminal. Let’s see how to use it.

  • The installation
cnpm install ora -S
Copy the code
  • use
const ora = require('ora');

// The ora parameter creates the spinner text content
// You can also pass an object to set the spinner's period, color, and so on
// Call the start method to start, and finally return an instance
const spinner = ora('Loading start')  // Turn the chrysanthemums spinner.start();  / / stop spinner.stop()  // Set the copy, the latter chrysanthemum color spinner.text = 'Installing project dependencies, please wait... '; spinner.color = 'green';  // The transfer status is displayed spinner.succeed('Package. json update complete'); Copy the code

Note that the color of the copy is still assisted by Chalk. Chalk will be introduced later. The last picture shows this in action:

Please refer to the ORA documentation for more detailed user information.

πŸŒ› multicolored console

Chalk is a tool that allows our console to print out various colors/backgrounds, so that we can clearly distinguish the contents of various prompts, as shown in the figure below. :

Chalk rendering
  • The installation
# Terminal run
cnpm i chalk -S
Copy the code
  • use
const chalk = require('chalk');

// For example, here defines onelogobjectexports.log = {
  warning(msg = ' ') {
 console.warning(chalk.yellow(`${msg}`));  },  error(msg = ' ') {  console.error(chalk.red(`${msg}`));  },  success(msg = ' ') {  console.log(chalk.green(`${msg}`));  } } Copy the code

For example, above we encapsulate the simplest log method for displaying colored content when printing various types of information. One more thing, let’s talk about how the above can be used with ORA:

const chalk = require('chalk');
const ora = require('ora');

const spinner = ora('Loading start')

// Turn the chrysanthemumsspinner.start(chalk.yellow('Print a yellow text')); Copy the code

Usage is relatively simple, not much to say, more usage or consult the document!

✨fs-extra File operation

Before detailing how each step is implemented, let’s look at the library for file manipulation used in the CLI. Node itself has fs operations, so why introduce fs-extra? Because it can be used to replace the FS library, eliminating mkdirp ‘ ‘rimraf’ ‘NCP and other library installation introduction. For copying, reading, deleting, and so on, but also provides more functionality, and so on.

  • The installation
cnpm install fs-extra
Copy the code

For detailed API methods, please refer to the document Fs-extra, and the detailed implementation of each step will be mentioned later.

✨ Check whether the folder is valid

When we run vta Create my-app, we need to consider that if there is already a folder with the same name in the current location, we cannot directly overwrite the folder. Instead, we need to give the user options, such as overwrite, create a new folder, and exit, as shown in the figure below:

Then according to the user’s different choices to make the operation. Let’s look at the implementation of this folder check:

checkFolderExist() {
    return new Promise(async (resolve, reject) => {
      const { target } = this.RepoMaps
      // If create has --force or -f attached, override is performed directly
      if (this.cmdParams.force) {
 await fs.removeSync(target)  return resolve()  }  try {  // Otherwise, check the folder  const isTarget = await fs.pathExistsSync(target)  if(! isTarget)return resolve()   const { recover } = await inquirer.prompt(InquirerConfig.folderExist);  if (recover === 'cover') {  await fs.removeSync(target);  return resolve();  } else if (recover === 'newFolder') {  const { inputNewName } = await inquirer.prompt(InquirerConfig.rename);  this.source = inputNewName;  this.RepoMaps.target = this.genTargetPath(`. /${inputNewName}`);  return resolve();  } else {  exit(1);  }  } catch (error) {  log.error(`[vta]Error:${error}`)  exit(1);  }  })  } Copy the code

Specific explanation:

  1. We defined this method to return a Promise object.
  2. We judge that the user is typingvta create my-appDid you add it at the end of the-fIf the parameter is added, it tells us to ignore the check and go ahead, which is the default overridden operation. By calling thefs.removeSync(target);Method to remove files that need to be overwritten.
  3. Otherwise, we need to do the implementation logic for folder checking. throughawait fs.pathExistsSync(target)The logic determines whether the current folder name already exists. If it does not, resolve tells the program to execute the program after the folder check is successful.
  4. If it has the same name, the user is prompted to select the operation. The following explains how to interact on the command line.

❀️ Command line interaction

Speaking of command line interaction, the inquirer is a comparison library for command line interaction in the Node environment. It supports radio, multiple selections, user input, confirm queries, and more.

  • The installation
cnpm i inquirer -S
Copy the code
  • use
const inquirer = require('inquirer');

// Define the parameters of the query
// Type indicates the type of the query, such as single, multiple, confirm, etc
// Name can be understood as the identifier of the current interaction, and its value is the result of the interaction
const InquirerConfig = {  // The query parameter for the existing name of the folder  folderExist: [{  type: 'list'. name: 'recover'. message: 'The current folder already exists, please select action:'. choices: [  { name: 'cover'.value: 'cover' },  { name: 'Create a new folder'.value: 'newFolder' },  { name: 'exit'.value: 'exit' },  ] }]. // Rename the query parameter  rename: [{  name: 'inputNewName'. type: 'input'. message: 'Please enter a new project name:'  }] }  / / use // Get the result of the interaction through the current identifier // For example, here is a single demonstration const { recover } = await inquirer.prompt(InquirerConfig.folderExist);  // If the user selects the Override option if (recover === 'cover') {  await fs.removeSync(target);  return resolve(); // If the user selects "Create new folder" selected } else if (recover === 'newFolder') {  // Create a user input interaction again  // Let the user enter a new folder name  const { inputNewName } = await inquirer.prompt(InquirerConfig.rename);  this.RepoMaps.target = this.genTargetPath(`. /${inputNewName}`);  return resolve(); // If the user selects the "exit" option } else {  exit(1); } Copy the code
  1. If the user selects overwrite, we remove the folder and reolve
  2. If the user chooses to create a new folder, we give the user another terminal to enter and ask the user to enter the new folder name. After the user finishes typing, we update the target address.
  3. If the user chooses to exit, we call the process.exit method to exit the current node program.

❀️ pull remote repository code such as git

Once folder monitoring is complete, it’s time to download our project resources on Git. We do this through the download-git-repo library.

  • The installation
cnpm install download-git-repo -S
Copy the code
  • use
const path = require('path');
const downloadRepo = require('download-git-repo');

  // Download the Repo resource
  downloadRepo() {
 // Turn the chrysanthemum ~  this.spinner.start('Pulling project template... ');  const { repo, temp } = this.RepoMaps  return new Promise(async (resolve, reject) => {  // If the local temporary folder exists, delete it first  await fs.removeSync(temp);  / * ** The first argument is the address of the remote repository. Note the type: author/library* The second parameter is the local address of the download,* This can be followed by a configuration parameter object, and the last one is a callback function,* /  download(repo, temp, async err => {  if (err) return reject(err);  // The chrysanthemums become checkmarks  this.spinner.succeed('Template downloaded successfully');  return resolve()  })  })  } Copy the code

The main logic is to download resources to our current temporary folder location, and delete the temporary folder if it already exists.

πŸ‘ Copies the resource to the target address and removes irrelevant files

Above, we downloaded the resources from git to a temporary file in the CLI directory, so we need to move the resources to the location we specified and delete unnecessary resources. So we’ll wrap a public function in utLIS for copying resources:

  • Copy function encapsulation
/ * ** copyFiles Copies downloaded Repo resources* @param {string} tempPath Path of the resource to be copied (absolute path)* @param {string} targetPath (absolute path)* @param {Array<string>} excludes resource names (all subfiles will be removed automatically)* / exports.copyFiles = async (tempPath, targetPath, excludes = []) => {  const removeFiles = ['./git'.'./changelogs']  // Copy the resource  await fs.copySync(tempPath, targetPath)   // Delete additional resource files  if (excludes && excludes.length) {  await Promise.all(excludes.map(file= > async() = > await fs.removeSync(path.resolve(targetPath, file))  ));  } } Copy the code
  • call
// Copy repo resourcesasync copyRepoFiles() {
  const { temp, target } = this.RepoMaps
  await copyFiles(temp, target, ['./git'.'./changelogs']);
}
Copy the code

Here, we remove the./git,./changelogs files from the project itself, because they are required by the Git project and we don’t need them.

πŸ‘ automatically updates the package.json file

By doing so, we have copied the resource to our target address. We also want to automatically update the name, version, author and other fields in package.json to what we need. What should we do?

/ * * * updatePkgFile
* @description Updates the package.json file* /
async updatePkgFile() {
 // Turn the chrysanthemum!  this.spinner.start('Updating package.json... ');  // Get the data pair path of the package.json file in the current project  const pkgPath = path.resolve(this.RepoMaps.target, 'package.json');  // Define the fields to be removed  // These fields themselves are only the content of git project configuration and are not needed for our business project  const unnecessaryKey = ['keywords'.'license'.'files']  // Call a method to get git information from the user  const { name = ' ', email = ' ' } = await getGitUser();   // Read the contents of the package.json file  const jsonData = fs.readJsonSync(pkgPath);  // Remove unwanted fields  unnecessaryKey.forEach(key= > delete jsonData[key]);  // Merge the information we need  Object.assign(jsonData, {  // Use the initial project name as the name  name: this.source,  // Update the author field to our Git name  author: name && email ? `${name} ${email}` : ' '. // Set non-private  provide: true. // The default version number is 1.0.0  version: "1.0.0"  });  // Write the updated package.json data to the package.json file  await fs.writeJsonSync(pkgPath, jsonData, { spaces: '\t' });  // Stop chrysanthemums  this.spinner.succeed('Package. json update complete! '); } Copy the code

This piece, the above code comment has been written very clear, look once should know the process logic!! The logic for getting git information from users will be explained later!

🌟 Obtain Git information

Now let’s look at how to get git information. We define a public method called getGitUser:

/ * * * getGitUser
* @description Gets git user information* /
exports.getGitUser = (a)= > {
 return new Promise(async (resolve) => {  const user = {}  try {  const [name] = await runCmd('git config user.name')  const [email] = await runCmd('git config user.email')  // Remove the ending newline  if (name) user.name = name.replace(/\n/g.' ');  if (email) user.email = ` <${email || ' '}> `.replace(/\n/g.' ')  } catch (error) {  log.error('Failed to get user Git information')  reject(error)  } finally {  resolve(user)  }  }); } Copy the code

As we all know, if you want to check the git information of the user on the terminal, you only need to type git config user.name and git config user.email to get the user’s mailbox. So we can also execute such a command in the script to get it?

So what’s left is how to execute shell commands at the terminal?

✨✨node Run the specified shell command in the script

Node executes script commands by starting a child process. Child_process is a method provided by Node to start a child process. So we can encapsulate a method to execute the child process:

// Node's child_process can start a process to execute a task
const childProcess = require('child_process');


/ * * * runCmd * @description Run the CMD command* @param {string} CMD command to run* / const runCmd = (cmd) = > {  return new Promise((resolve, reject) = > { childProcess.exec(cmd, (err, ... arg) => { if (err) return reject(err)  returnresolve(... arg) })  }) } Copy the code

To get git details, we call this method, ask Node to start a child process to run our git command, and then return the result.

❀️ Initialize the git file

// Initialize git file
  async initGit() {
    // Turn the chrysanthemum
    this.spinner.start('Initializing Git admin project... ');
    // Call the child process and run the CD XXX command to go to our target file directory
 await runCmd(`cd The ${this.RepoMaps.target}`);   // Call the process.chdir method to change the node process execution location to the target directory  // This step is important, otherwise the execution will fail (because the execution is in the wrong place).  process.chdir(this.RepoMaps.target);   // Call the child process to execute git init command to assist us in git initialization  await runCmd(`git init`);  // Chrysanthemums stop  this.spinner.succeed('Git initialization complete! '); } Copy the code

This one is also calling our encapsulated methods to execute git commands. But be sure to note that process.chdir(this.repomaps.target); Change the execution location of the process and throw an exception if the directory change fails (for example, if the specified directory does not exist). This step is very important, remember!! Remember!! For details, see the process.chdir description

🌟 Installation Dependencies

Finally, we need to automatically turn the project dependencies. The essence is to call the child process to execute the NPM command can be. Here we directly specified the use of Taobao mirror source, small partners can also expand, according to the user’s choice to specify NPM, YARN and other mirror source and so on, enjoy play!!

// Install dependencies
  async runApp() {
    try {
      this.spinner.start('Installing project dependencies, please wait... ');
      await runCmd(`npm install --registry=https://registry.npm.taobao.org`);
 await runCmd('git add. && git commit -m"init: Initialize the project base framework "');  this.spinner.succeed('Dependent installation complete! ');   console.log('Please run the following command to start the project: \n');  log.success(` cd The ${this.source}`);  log.success(` npm run serve`);  } catch (error) {  console.log('Project installation failed, please run the following command to manually install: \n');  log.success(` cd The ${this.source}`);  log.success(` npm run install`);  }  } Copy the code

The last πŸ‘ πŸ‘ πŸ‘

Vta – CLI scaffolding Git source address, interested partners can refer to the code implementation. You can also use VTA – CLI to quickly initialize Vue+Ts+ElementUi’s RBAC back end management system infrastructure. How to install vTA-CLI:

# installation cli
npm i vta-cli -g

Initialize the project
vta create my-app
Copy the code

The Vue-typescript-admin project template will be perfected soon!! You are also welcome to contribute your code

On the CLI development of the explanation, to this is basically over!! The above covers the common technical implementation solutions and attention to detail, the project can painlessly get started ~ ~ ~ interested partners can follow the package of their own CLI, the business of the common scenario solution out of the processing, improve their development efficiency! Finally, I am your old friend Lange. Welcome to πŸ‘πŸ‘. Click πŸ‘πŸ‘

Like πŸ‘, collect πŸ‘‹, share anti-lost oh!! When needed, you can take it out and develop it

❀️❀️❀️ Update white space

This will be reserved as the address of more excellent libraries related to scaffolding development in the future, and will be updated in the future

πŸ‘πŸ‘πŸ‘ Other articles recommended by the author

  • Vue + TS +class+ annotated style development row pit full guide
  • Quickly develop SEO-friendly projects with egg.js +Nunjucks template engine in 5 minutes
  • Koa2 Full-stack Core content (with pictures and pictures)
  • Vue plug-in development, documentation, Github release, NPM package release flow
  • MMM, it smells good! Simplified ES functional programming core concepts
  • Vue encapsulates Axios and manages API interfaces
  • The problem and solution that hit you in the Vue project

This article was typeset using MDNICE