“This article is participating in the technical topic essay node.js advanced road, click to see details”
preface
Before reading this article, you must be wondering why you wrote your own scaffold.
Scaffolding is familiar to us, for example, we often use VUE-CLI, it can help us quickly initialize a project, without zero configuration, greatly convenient for our development. But the power is limited, the public scaffolding sometimes does not meet our actual development.
In the history of the most intimate front-end scaffolding development guidance of the author big guy talked about several reasons, xiao Bao thinks that is very good:
The company has accumulated some project logic, such as skin change, interface request, project architecture, internationalization, etc. If the company starts a new project at this time, we need to CTRL + C, CTRL + V for the common logic of the original project.
But there are a number of downsides to this copy-and-paste approach:
- Repetitive work, tedious and time-consuming
- It is easy to ignore configuration Settings in projects
copy
Incoming templates will have duplicate code
And so on, if we develop our own scaffolding and customize our own templates, the manual process of copying and pasting will be converted to the automated process of the CLI. How’s it going? Is it moving?
But for Xiao Bao, xiao Bao’s work experience is not so much, but also take into account all kinds of tedious work, xiao Bao has his own ideas, mainly have the following points:
vue-cli
Bashing: Small packs used recentlyvue-cli
There are always inexplicable errors when creating projects, solutions are never found, and you have to uninstall and reinstall each time before they can be used properly- multiple
cli
Tedious: recently xiaobao also began to learnreact
Bao always imagined that it would be nice if they both used a scaffold - Architecture growth: Architecture is an attractive word, and scaffolding has always been an essential skill for architecture in small packages.
nodejs
To grow: to make use ofnodejs
Implementing a scaffold is also on yourselfnodejs
A great workout for level.
By studying this article, you will learn:
- 🌟 Master the whole process of developing scaffolding
- 🌟 Command line development of a variety of common third-party modules
- 🌟 has a scaffolding of its own
At the beginning of this article, we can take a look at the zC-CLI function demonstration.
Scaffolding implementation analysis
Using vue-CLI as an example, let’s analyze some of the functions required for a simple scaffold:
Vue – CLI uses vUE as a global command, providing many instructions at the same time.
vue --version
You can view the VUE versionvue --help
Check out the help documentationvue create xxx
You can create a project- .
Vue creates the project
Using the vUE creation project as an example, let’s analyze what scaffolding should do
Step1: run the create command
vue create demo
Copy the code
Step2: Interactive user selection
You can select a version or configuration on the CLI.
Step3: After the user selects, generate the project file required by the user according to the user’s selection
From the scaffolding process above, we can roughly sum up the functions of scaffolding:
- Interact with the user through the command line
- Generate a file based on the user’s selection
Process analysis
Based on vuE-CLI experience, let’s analyze the basic implementation process of scaffolding:
- First we need to initialize a project
- Create a project
zc-cli
To configure the information required for the project npm link
Project to global so that instructions can be temporarily invoked locally
- Create a project
- Project development
- Basic directive configuration: for example
--help --version
等 - Complex instruction configuration:
create
instruction - Realize command line interaction function: based on
inquirer
Implement command line interaction - Pull the project template
- Dynamically generate projects based on user selection
- Basic directive configuration: for example
Tripartite library used
We will use a lot of third-party modules when developing the CLI. Let’s introduce the third-party modules we use. To make the introduction clearer, let’s create a demo project for the demonstration.
Initialize the Demo project
- create
demo
Folder, executenpm init -y
Initialize the warehouse, generatepackage.json
file
{
"name": "demo"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": []."author": ""."license": "ISC"
}
Copy the code
- in
demo
createbin
Folder and create it insidenode
Entrance to the fileenter
- The editor
enter
File and configure it topackage.json
In thebin
field
// enter
#! /usr/bin/env node
// For testing purposes
console.log("hello demo");
Copy the code
// package.json
The // bin field also supports object mode configuration
"bin": "bin/enter".Copy the code
Why do YOU need to add #! To the file header? The/usr/bin/env node?
#!
The symbol name is calledShebang
The interpreter used to specify the script- The development of
npm
Package, you need to specify this directive in the entry file, otherwise it will be thrownNo such file or directory
error
npm link
To the global
Running NPM Link in the demo file directory to link the project to the local environment can temporarily implement global invocation of the Demo directive. (the –force parameter can override the original command.)
- run
demo
Command. The command line is printed successfullyhello demo
.demo
The project is configured successfully.
Commander — Command line command configuration
The third party library commander to implement the scaffold command configuration. For more details, refer to the Commander Chinese documentation
Step1: installationcommander
Dependency, and importdemo
In the project
// Install dependencies
npm install commmander
Copy the code
// enter
const program = require("commander");
// Parses the parameters entered by the user during execution
// process.argv is an attribute provided by nodejs
// npm run server --port 3000
// Port 3000 is the parameter entered by the user
program.parse(process.argv);
Copy the code
Commander comes with the –help command. After importing the command successfully, run demo –help on the command line to print the basic help information.
Step2: version you can configure the version prompt
Step3: Name and usage methods configure the CLI name and –help prompt for the first line respectively
program
.name("demo")
.usage(`<command> [option]`)
.version(` 1.0.0 `);
Copy the code
Once again, demo –help completes the command line message prompt.
More complex methods will be introduced as we work with them.
Chalk – Command line beautification tool
Chalk beautifies the way we output content on the command line, such as multiple colors and fancy command line prompts.
Step1: first install the chalk dependency and introduce it
Step2: you can start to output all sorts of fancy command line prompts
//enter
const chalk = require("chalk");
console.log(`hello ${chalk.blue("world")}`);
console.log(chalk.blue.bgRed.bold("Hello world!"));
console.log(
chalk.green(
"I am a green line " +
chalk.blue.underline.bold("with a blue substring") +
" that becomes green again!"));Copy the code
How about that? That’s a lot of bells and whistles. However, when installing Chalk, be sure to install the 4.x version (4.0.0 is used for small packages); otherwise, errors may occur due to the high version.
Inquirer — Command line interaction tool
When we used the vue create command above, one of the steps was interactive user selection, which was implemented by Inquirer.
Inquirer supports Confirm, List, and Checkbox.
Here we simulate the implementation of vUE’s multi-choice feature:
new Inquirer.prompt([
{
name: "vue".// Multi-choice interaction
// Change this to list
type: "checkbox".message: "Check the features needed for your project:".choices: [{name: "Babel".checked: true}, {name: "TypeScript"}, {name: "Progressive Web App (PWA) Support"}, {name: "Router",
},
],
},
]).then((data) = > {
console.log(data);
});
Copy the code
Ora — command line loading effect
Ora is very simple to use, you can directly see the following case. More use: ORA documentation
Ora is used to implement a simple command-line loading effect.
const ora = require("ora");
// Define a loading
const spinner = ora("Loading unicorns");
/ / start loading
spinner.start();
setTimeout(() = > {
spinner.color = "yellow";
spinner.text = "Loading rainbows";
}, 1000);
/ / loading
spinner.succeed();
/ / loading failure
spinner.fail();
Copy the code
Fs-extra — friendlier file manipulation
Fs-extra module is an extension of fs module, which provides more convenient API and inherits THE API of FS module. Much friendlier to use than FS.
Download-git-repo — command line download tool
Download-git-repo You can download and extract a Git repository from Git.
The download function provided by the download-git-repo repository takes four arguments (the following code is extracted from the download-git-repo source code):
/** ** callback 'fn(err)'. /** ** callback 'fn(err)'@param {String} Repo warehouse address *@param {String} Dest warehouse after downloading the storage path *@param {Object} Opts configuration parameter *@param {Function} Fn callback function */
function download(repo, dest, opts, fn) {}
Copy the code
Note: Download-git-repo does not support Promises
Figlet — Generates an ASCII based word of art
The Figlet module converts text into ASCII – based art-words. Specific effect is not easy to explain, directly look at the effect.
// Enter the entry file
console.log(
"\r\n" +
figlet.textSync("demo", {
font: "Ghost".horizontalLayout: "default".verticalLayout: "default".width: 80.whitespaceBreak: true,}));Copy the code
Figlet offers a variety of fonts, you can go to the official website to choose your favorite font.
The command configuration
The initialization project section is similar to the above section, so I will not go into the text.
Configuration plate number
The commander method provides the version method. The.version() method can be used to set the version. The default options are -v and –version.
// Package. json contains the version number of the project
// Use this property directly
program.version(`zc-cli The ${require(".. /package.json").version}`);
Copy the code
On the CLI, run zc-cli –version
zc-cli 1.0. 0
Copy the code
Configure the first line prompt
Commander also provides the.usage and.name methods, which can modify the first line of the help prompt. Use these two methods to modify the first line prompt for –help.
// Name is the configuration scaffold name
// usage is the configuration command format
program.name("zc-cli").usage(`<command> [option]`);
Copy the code
Zc -cli –help, now –help printing is much more complete.
Usage: zcxiaobao <command> [option]
Options:
-V, --version output the version number
-h, --help display help for command
Copy the code
Configuring the create command
Commander provides the command method. The command method takes the command name as its first parameter, followed by the command name (mandatory parameters are represented by <>, optional parameters are represented by []).
Let’s configure the create command, which creates the project. Also here we add the –force parameter, which overrides the current project by default. (Details on the existence of directories with the same name will be dealt with later.)
The option method defines options and appends a brief description of the options. Each option can define a short name of the option (- followed by a single character) and a long option name (-) followed by one or more words, use commas, Spaces, or | off.
program
.command("create <project-name>") // Add create instruction
.description("create a new project") // Add the description
.option("-f, --force"."overwrite target directory if it exists") // Force overwrite
.action((projectName, cmd) = > {
// Handle the arguments attached to the create directive when the user enters it
console.log(projectName, cmd);
});
Copy the code
Let’s test whether the create command was added successfully.
Create [options]
$ zc-cli create
error: missing required argument 'project-name'
$ zc-cli create xxx
xxx {}
$ zc-cli create xxx --force
xxx { force: true }
Copy the code
The parameters entered on the cli are successfully obtained. Yes!!!
Configure the config command
The config command is also commonly used in scaffolding, so let’s add another config command and get familiar with the use of COMMANDER.
program
.command("config [value]") / / config command
.description("inspect and modify the config")
.option("-g, --get <key>"."get value by key")
.option("-s, --set <key> <value>"."set option[key] is value")
.option("-d, --delete <key>"."delete option by key")
.action((value, keys) = > {
// value fetches the value of [value], and keys fetches the command argument
console.log(value, keys);
});
Copy the code
Optimization –help prompt
Vue
Add this function to zC-CLI:
Commander can automatically listen for command execution using the ON method.
// Listen for the --help directive
program.on("--help".function () {
// Two blank lines before and after the formatting, more comfortable
console.log();
console.log(
" Run zc-cli <command> --help for detailed usage of given command."
);
console.log();
});
Copy the code
Zc -cli –help
Color the –help prompt
Zc -cli
// Use the cyan color
program.on("--help".function () {
// Two blank lines before and after the formatting, more comfortable
console.log();
console.log(
`Run ${chalk.cyan(
"zc-cli <command> --help"
)} for detailed usage of given command.`
);
console.log();
});
Copy the code
Zc -cli –help is highlighted by zc-cli
The command configuration section can be finished for now, rest, and enter the core. 🎉 🎉 🎉
Create a project
The create module
We created a separate module for the creation function, which was stored in lib/create.js and introduced in the ZC entry file where the create directive was configured
// zC entry file
program
.command("create <project-name>") // Add create instruction
.description("create a new project") // Add the description
.option("-f, --force"."overwrite target directory if it exists") // Force overwrite
.action((projectName, cmd) = > {
// Import the create module and pass in the parameters
require(".. /lib/create")(projectName, cmd);
});
// create.js
// There may be many asynchronous operations in the current function, so we'll wrap it async
module.exports = async function (projectName, options) {
console.log(projectName, options);
};
Copy the code
Let’s test whether the CREATE module can receive parameters.
$ zc-cli create xxx --force
xxx { force: true }
Copy the code
A directory with the same name exists
When creating the create command, we configured the –force parameter, which means forced overwrite. When we create a project directory, three things will happen:
- Used when creating a project
--force
Parameter, regardless of whether there is a directory with the same name - Don’t use
--force
Parameter and does not exist in the current working directory - Don’t use
--force
And the project with the same name exists in the current working directory. You need to provide the user with the option to cancel or overwrite the project
Let’s tease out the implementation logic for this part:
- through
process.cwd
Gets the current working directory, then concatenates the project name to get the project directory - Check whether a directory with the same name exists
- A directory with the same name exists
- Used when the user creates the project
--force
Parameter to delete a directory with the same name - Don’t use
--force
Parameter, which provides the user with an interactive selection box, determined by the user
- Used when the user creates the project
- No directory of the same name exists, continue to create the project
const path = require("path");
const fs = require("fs-extra");
const Inquirer = require("inquirer");
module.exports = async function (projectName, options) {
// Get the current working directory
const cwd = process.cwd();
// splice to get the project directory
const targetDirectory = path.join(cwd, projectName);
// Check whether the directory exists
if (fs.existsSync(targetDirectory)) {
// Determine whether to use the --force parameter
if (options.force) {
// Remove directories with similar names (remove is an asynchronous method)
await fs.remove(targetDirectory);
} else {
let { isOverwrite } = await new Inquirer.prompt([
// Return the value promise
{
name: "isOverwrite".// Corresponds to the return value
type: "list"./ / the list type
message: "Target directory exists, Please choose an action".choices: [{name: "Overwrite".value: true },
{ name: "Cancel".value: false},],},]);/ / choose Cancel
if(! isOverrite) {console.log("Cancel");
return;
} else {
// Select Overwirte and delete the original directory with the same name
console.log("\r\nRemoving");
awaitfs.remove(targetDirectory); }}}};Copy the code
Let’s create a aaa folder in the current directory to test whether we can handle the same directory:
The project creates the Creator class
To make the project easier to manage, we will separate the creation project section into the Creator class.
// Creator.js
class Creator {
// Project name and project path
constructor(name, target) {
this.name = name;
this.target = target;
}
// Create the project section
create() {
console.log(this.name, this.target); }}module.exports = Creator;
// create.js
const creator = new Creator(projectName, targetDirectory);
creator.create();
Copy the code
Run the zc-cli create aaa command to print the project name and project path
aaa D:\workspace\forward\notes\cli\aaa
Copy the code
The template of the project is stored in Github. The project uses zhurong-cli template repository. Zhurong-cli provides vue2 and VUe3 repositories respectively, and each repository provides multiple versions.
Github provides an official API. You can call the official API to get the repository and version information.
- Warehouse information
- Version information
Therefore, you can divide the creation project into the following steps:
- By acquiring the warehouse
API
Get template information:Vue2 or Vue 3
- The template information is rendered as an interactive box, and the user selects the template he/she needs
- Obtain version information based on the template selected by the user
- The version information is rendered into an interactive box, and the user selects the desired version
- You can download a template and its version to a specified directory
- Render the template as a project
Obtain the template and version
API request module
Since multiple requests are sent in scaffolding, we set up api.js separately to handle fetching template and version information.
const axios = require("axios");
// Intercepts the global request response
axios.interceptors.response.use((res) = > {
return res.data;
});
/** * get template *@returns Promise warehouse information */
async function getZhuRongRepo() {
return axios.get("https://api.github.com/orgs/zhurong-cli/repos");
}
/** * Get the repository version *@param {string} Repo template name *@returns Promise Version information */
async function getTagsByRepo(repo) {
return axios.get(`https://api.github.com/repos/zhurong-cli/${repo}/tags`);
}
module.exports = {
getZhuRongRepo,
getTagsByRepo,
};
Copy the code
Obtaining Template Information
The method for getting template information is encapsulated in the API section, which is called directly to get the template information, and then rendered into a command line interactive selection box using Inquirer.
// Get template information and the template selected by the user
async function getRepoInfo() {
// Get the warehouse information under the organization
let repoList = await getZhuRongRepo();
// Retrieve the warehouse name
const repos = repoList.map((item) = > item.name);
// Select template information
let { repo } = await new inquirer.prompt([
{
name: "repo".type: "list".message: "Please choose a template".choices: repos,
},
]);
return repo;
}
Copy the code
Test whether the template selection was successful.
Obtaining version Information
Once we get the template information, we can call the second API based on the template name to get the tag (version information)
// Get the version information and the version selected by the user
async getTagInfo(repo) {
let tagList = await getTagsByRepo(repo);
const tags = tagList.map((item) = > item.name);
// Select template information
let { tag } = await new inquirer.prompt([
{
name: "repo".type: "list".message: "Please choose a version".choices: tags,
},
]);
return tag;
}
Copy the code
Test whether the version is successfully pulled.
Adding loading effects
It takes a certain amount of time to pull a template, so adding a loading effect makes the user experience better.
The loading method can be implemented using ora, a third-party library.
/** * loading effect *@param {String} Message loads the message@param {Function} Fn loading function *@param {List} Args fn Function execution argument *@returns Asynchronous calls return the value */
async function loading(message, fn, ... args) {
const spinner = ora(message);
spinner.start(); // Start loading
let executeRes = awaitfn(... args); spinner.succeed();return executeRes;
}
Copy the code
We added the loading method to the template fetch and version fetch sections.
Failure to re-pull
Remote loading can sometimes suffer from poor network or loss of remote resources, so we added a failure repull feature to improve the user experience.
However, we need to limit the frequency of failed repulls, because pulling too often creates a bad experience for users.
/** ** sleep function *@param {Number} N Sleep time */
function sleep(n) {
return new Promise((resolve, reject) = > {
setTimeout(() = > {
resolve();
}, n);
});
}
async loading(message, fn, ... args) {
const spinner = ora(message);
spinner.start(); // Start loading
try {
let executeRes = awaitfn(... args);// The load succeeded
spinner.succeed();
return executeRes;
} catch (error) {
// Load failed
spinner.fail("request fail, refetching");
await sleep(1000);
// pull again
return loading(message, fn, ...args);
}
}
Copy the code
Download the template
Above we have successfully obtained the template and version information, now we can go to download the template.
The Download git-repo module does not support promises, so we first use the promisify method provided by node’s util module to convert it to Promise support.
// Mount the method to the constructor
constructor(name, target) {
this.name = name;
this.target = target;
// Convert to the Promise method
this.downloadGitRepo = util.promisify(downloadGitRepo);
}
Copy the code
Define the Download download method and call it from the Create entry:
async download(repo, tag) {
// Template download address
const templateUrl = `zhurong-cli/${repo}${tag ? "#" + tag : ""}`;
// Call the downloadGitRepo method to download the template to the specified directory
await loading(
"downloading template, please wait".this.downloadGitRepo,
templateUrl,
path.join(process.cwd(), this.target) // Project creation location
);
}
Copy the code
The XXX project was also successfully created in the current directory with the following directory structure:
Beautification project
To add a logo
When the –help command was called, we added the logo display at the end, and after a careful selection, we chose 3D-ASCII font for the package.
Let’s take a look at the effect, cool, can not find a way to color.
Template Usage Tips
When the template is downloaded successfully, we add the template usage tips like VUe-CLI to ensure that users can start the project normally. However, it should be noted that zC-CLI currently downloads the template, so we need to run NPM install to download the dependency package after entering the template.
// Core create logic -- create the project section
async create() {
// Warehouse information -- template information
let repo = await this.getRepoInfo();
// Label information -- version information
let tag = await this.getTagInfo(repo);
// Download the template to the template directory
await this.download(repo, tag);
// Template usage prompts
console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`);
console.log(`\r\n cd ${chalk.cyan(this.name)}`);
console.log(" npm install\r\n");
console.log(" npm run dev\r\n");
}
Copy the code
Finally, we follow the template usage steps and successfully create a Vue project.
Publish the project
Next modify package.json to add personal information, and you’re ready to publish.
The process for publishing an NPM package is very simple.
Step1: first of allnpmRegister a user
Step2: Then log in to NPM locally
Step3: Run the NPM publish command to publish the NPM package.
Step4: Then we can find the published NPM package on the NPM official website. (Zcxiaobao – CLI)
Summary and Prospect
Above we succeeded in creating the project, but there is still a long way to go before the scaffolding is complete
- Render a template as a project:
zc
Scaffolding is essentially the creation of a template, which we will need to render into a real project based on the template and the user’s choices in future development - Caching of templates: the same template does not need to be downloaded multiple times, so we should add caching of templates
- Item name and other parts of the verification
config
Implementation of commands- integration
react cli
function
Zc-cli is far from mature and useful scaffolding, but has explained the general process of scaffolding development, the function of the following package will learn step by step, step by step to improve, and finally zC-CLI to achieve a relatively complete scaffolding.
Packet next decided to read vue-CLI source code, absorb its essence, and then come back to improve zC-CLI.
Source warehouse
Github address: Zcxiaobao – CLI
NPM warehouse address: Zcxiaobao – CLI
If you feel helpful, don’t forget to give the small bag a ⭐
After the language
I am battlefield small bag, a fast growing small front end, I hope to progress together with you.
If you like xiaobao, you can pay attention to me in nuggets, and you can also pay attention to my small public number – Xiaobao learning front end.
All the way to the future!!