preface
Under article is simply that the front-end scaffolding code, and the implementation mentality, has recently increased the function of the part, this is the last article, plus the are described in a unified, recently saw the nuggets articles of others, found that only vernacular is the best, can let a person quickly understand the language, so this time, Just write a front end scaffold in plain English.
start
What is front-end scaffolding
1. What is
- Scaffolding is to ensure the smooth construction process and set up the work platform. (Baidu’s definition of scaffolding)
- A directory template that helps me quickly generate new projects
- Can improve my development efficiency and development comfort
2. What are the benefits
- It is to unify the technology stack of each line of business and formulate specifications, so that problems can be solved uniformly and upgrades and iterations can be carried out synchronously
- To improve efficiency, on the basis of a unified basis, provide more tools to improve efficiency. This efficiency is not only the development efficiency, but also the efficiency of the whole process from development to online, such as the encapsulation of some business components, the release system to improve online efficiency, various utils and so on
Start coding
In this paper, the test branch of code at https://github.com/amazingliyuzhao/cli-demo
1. Project Catalog
Look at the macaw – hellow. Js
console.log('hello, cli')
Copy the code
And then I’m going to run it from the command line
node ./bin/macaw-hellow.js
Copy the code
You can see the print statement
So far we have written the execution order. And then we’re going to add a little bit of functionality, first of all we’re going to look at other scaffolding like vue-cli when you create a file, vue create projectName, which is basically a custom command and then + projectName
We noticed that they didn’t have the +node command in front of them, but they still executed the statement. This is because the github official documentation for commander. Js can be accessed directly using the commander. Js tool written by TJ.
2. commander.js
Get project name
Step one, introduce
const program = require('commander')
Copy the code
Let’s change our Hellow file
program.usage('<project-name>')
.parse(process.argv) // Add this to get the project name
// Get the project name based on the input
let projectName = program.rawArgs[2] // Get the project name
console.log(projectName)
Copy the code
Execute the hellow file
node ./bin/macaw-hello.js testDemo
Copy the code
You can see the print
What to do next with the name, of course, is to create our project based on the name.
Project name tolerance
But for fault tolerance, what if there’s no name
Help () is equivalent to executing the –help option of the command, displaying the help message, which is a command option built into COMMANDER
if(! projectName) {// project-name Mandatory If no name is entered, run helphelp
program.help()// This is equivalent to executing the --help option of the command to display the help message, which is a built-in commander command option
return
}
Copy the code
Creating a folder
There is a lot of logic for creating folders, so we can sort it out first
Logic for creating files
- If the project name is the same as the root directory
Generate files directly in the root directory
- If create project name and root directory are inconsistent
Create the project directory directly under the root directory and generate the widgets
- If a file with the same project name already exists in the root directory
Ask the user whether to overwrite
Take a look at the processing code
#! /usr/bin/env node
const program = require('commander')
const path = require('path')
const fs = require('fs')
const glob = require('glob') // npm i glob -D
const download = require('.. /lib/download') // Download the configuration
const inquirer = require('inquirer') // Import on demand
const logSymbols = require("log-symbols");
const chalk = require('chalk')
const remove = require('.. /lib/remove') // Delete file js
const generator = require('.. /lib/generator')// Template insertion
const CFonts = require('cfonts');
program.usage('<project-name>')
.parse(process.argv) // Add this to get the project name
// Get the project name based on the input
// console.log(program)
let projectName = program.rawArgs[2] // Get the project name
if(! projectName) {// project-name Mandatory If no name is entered, run helphelp
program.help()// This is equivalent to executing the --help option of the command to display the help message, which is a built-in commander command option
return
}
// The current directory is empty. If the name of the current directory is the same as project-name, the project is directly created in the current directory. Otherwise, the project root directory is created in the current directory with project-name as the name
// The current directory is not empty. If there is no directory with the same name as project-name, create a directory with project-name as the root directory of the project. Otherwise, a message is displayed indicating that the project already exists.
// process.cwd() is the directory where the node command is currently executed
//__dirname is the address of the js file being executed -- the directory in which the file resides
const list = glob.sync(The '*') // Traverses the current directory, array type
let next = undefined;
let rootName = path.basename(process.cwd());
if (list.length) { // If the current directory is not empty
if (list.some(n= > {
const fileName = path.resolve(process.cwd(), n);
const isDir = fs.statSync(fileName).isDirectory();
return projectName === n && isDir // Find a file with the same name as the current directory file{}))// If the file already exists
next = inquirer.prompt([
{
name:"isRemovePro".message:` project${projectName}Already exists, whether to overwrite file '.type: 'confirm'.default: true
}
]).then(answer= >{
if(answer.isRemovePro){
remove(path.resolve(process.cwd(), projectName))
rootName = projectName;
return Promise.resolve(projectName);
}else{
console.log("Stop creating")
next = undefined
// return;}}}})else if (rootName === projectName) { // If the file name is the same as the root directory name
rootName = '. ';
next = inquirer.prompt([
{
name: 'buildInCurrent'.message: 'The current directory is empty and the directory name is the same as the project name. Do you want to create a new project directly under the current directory? '.type: 'confirm'.default: true
}
]).then(answer= > {
console.log(answer.buildInCurrent)
return Promise.resolve(answer.buildInCurrent ? '. ' : projectName)
})
} else {
rootName = projectName;
next = Promise.resolve(projectName) // Return the resole function, passing projectName
}
next && go()
function go () {
// reserved, processing subcommands
}
Copy the code
We talked about asking the user, so how do we interact with the user at the command line
3. Inquirer. Js — a tool for users to interact with the command line
This library can be used to interact with the user at the command line. Let’s look at the scenario
- Type: Indicates the type of the query. The options are input, confirm, list, rawlist, expand, checkbox, password, and editor.
- Message: a description of the problem;
- Default: the default value.
- Choices: list options, available under certain types, and containing a separator;
- Validate: verifies the user’s answer;
- Filter: The user answers are filtered and the processed value is returned.
- Transformer: Process the display of the user’s answers (e.g. change the font or background color) without affecting the content of the final answer;
- When: Judge whether the current question needs to be answered based on the answers to the previous questions.
- PageSize: Changes the number of rendered lines for certain types;
- Prefix: Modify the default message prefix.
- Suffix: Change the default message suffix.
The general framework is
const inquirer = require('inquirer');/ / introduction
const promptList = [];// User interaction
inquirer.prompt(promptList).then(answers= > {
console.log("Last output")
console.log(answers); // The result returned
})
Copy the code
Let’s look at them one by one
- The first is input in type
Copy the code
const promptList = [{
type: 'input'.message: 'Set a user name :'.name: 'name'.default: "test_user" / / the default value}, {type: 'input'.message: 'Please enter mobile phone number :'.name: 'phone'.validate: function(val) {
if(! val.match(/\d{11}/g)) { // Check digit
return "Please enter 11 digits.";
}
return true}}];Copy the code
Effect of
- Type – > confirm
const promptList = [{
type: "confirm".message: "Is the first condition true?".name: "firstChange".prefix: "The prefix of the first condition"}, {type: "confirm".message: "Is the second condition true (dependent on the first condition)?".name: "secondChange".suffix: "Suffixes for the second condition.".when: function(answers) { // The current question will only be asked if firstChange is true
return answers.firstChange
}
}];
Copy the code
Effect of
const promptList = [{
type: 'list'.message: 'Please choose a fruit :'.name: 'fruit'.choices: [
"🍎 Apple"."🍐 Pear"."🍌 Banana"].filter: function (val) { // Use filter to change the answer to lowercase
returnval.toLowerCase(); }}];Copy the code
Effect of
- type–>list
const promptList = [{
type: "expand".message: "Please choose a fruit:".name: "fruit".choices: [{key: "a".name: "Apple".value: "apple"
},
{
key: "O".name: "Orange".value: "orange"
},
{
key: "p".name: "Pear".value: "pear"}}]];Copy the code
Key is a prompt, and key can only be a simple letter. H defaults to help, and selecting H shows all the options
Effect of
const promptList = [{
type: "checkbox".message: "Type.".name: "color".choices: [
new inquirer.Separator("Brand - - -"), // Add a delimiter
{
name: "Audi"}, {name: "Mercedes"}, {name: "Red flag",},new inquirer.Separator("- color -"), // Add a delimiter
{
name: "blur".checked: true // It is selected by default
},
{
name: "red"}, {name: "green"}}]];Copy the code
- Enter the ciphertext value in type –>password
const promptList = [{
type: "password".// Enter the password in ciphertext
message: "Please enter your password:".name: "pwd"
}];
Copy the code
- Enter ciphertext in type –> Editor
const promptList = [{
type: "editor".message: "Please enter remarks:".name: "editor"
}];
Copy the code
Press Enter to write the remarks, esc+:wq to save the configuration and exit
That leaves us with the types of inquire commonly used, and let’s move on to our project
So going back to the previous flow, we’ve gone to create a file and after we’ve done the logic of creating a file we execute next and go,
next = Promise.resolve(projectName) // Return the resole function, passing projectName
Copy the code
Create our directory in the go function
function go () {
next.then(projectRoot= > { //
if(projectRoot ! = ='. ') {
fs.mkdirSync(projectRoot)// Create a directory file}}}Copy the code
Let’s take a look at the details. I’m executing amaz-test + project name, where amaz is my custom command. Let’s look at the configuration of our package.json file
{
"name": "liyuzhaocli-2"."version": "1.0.0"."description": "Amz Scaffolding 1.0"."bin": {
"macaw": "./bin/macaw.js"."amaz": "./bin/macaw-init.js"."amaz-test": "./bin/macaw-test.js"
},
"main": "./bin/macaw-hellow.js"."scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"cli"]."author": "amaizngli"."license": "ISC"."dependencies": {
"cfonts": "^ 2.4.5"."commander": "^ 4.0.0"."download-git-repo": "^ 3.0.2." "."glob": "^ 7.1.5." "."handlebars": "^ 4.5.1." "."inquirer": "^ 7.0.0." "."metalsmith": "^ 2.3.0." "."ora": "^ 4.0.2." "}}Copy the code
You can see below bin is our custom command
If you want to simulate the effect of publishing NPM, you can link to global directly from the local NPM link so that you can use our commands on your computer.
But now that we’ve just created an empty directory and need to fill it with our templates, we need another tool
4. Download-git-repo — a tool to download remote Git files
First we will create a download.js file in the lib directory
www.npmjs.com/package/dow…
Let’s take a look at the code
const download = require('download-git-repo')
const path = require("path")
const ora = require('ora')
module.exports = function (target) {
target = path.join(target || '. '.'.download-temp');
return new Promise(function (res, rej) {
// You can set the url for downloading according to the template address. Note that if git is used, the branch behind the URL cannot be ignored
// The format is the name/address without.git but with the # branch
// let url = 'amazingliyuzhao/CliTestGit#test'
let url = 'amazingliyuzhao/cli-template#test'
const spinner = ora('Downloading project template, source address:${url}`)
spinner.start();
download(url, target, { clone: false }, function (err) { // clone false to false
if (err) {
spinner.fail()
rej(err)
}
else {
// The downloaded template is stored in a temporary path. After the download is complete, you can notify this temporary path down for subsequent processing
spinner.succeed()
res(target)
}
})
})
}
Copy the code
Ora is a code beautification library
Note that a big hole is the url format, our normal git address is github.com/amaz/cli-de…
Use amaz/cli-demo#test when using this tool. # is followed by the corresponding branch name. Master must also write without default
If you want to update the template, you just need to update the remote Git repository. Each time you pull the template, it will be the latest code.
Finally, it is introduced in our macaw-init file
const download = require('.. /lib/download') // Download the configuration
Copy the code
And then I rewrite the go function
function go () {
next.then(projectRoot= > {
if(projectRoot ! = ='. ') {
fs.mkdirSync(projectRoot) // Create a file
}
return download(projectRoot).then(target= > {// Download the template
return {
projectRoot,
downloadTemp: target
}
})
})
}
Copy the code
Download the remote template
Well, the problem came again. After downloading the template, all the information was written in the template. The version information, project name and version number need to be modified by ourselves in the project, which seems not as customizable as we expected
So our expectation is to customize our information through user input. When it comes to user interaction we first think of inquire.js tools mentioned above, but how do we populate our templates with what we get from users?
Now we’re going to use a new tool
Metalsmith – a tool for downloading remote Git files
Metalsmith. IO/but to be honest, it’s a little hard to read.
Recommend a site www.kutu66.com//GitHub/art…
Create a new generator file under lib,
The installation
npm i handlebars metalsmith -D
Copy the code
// // npm i handlebars metalsmith -D
const rm = require('rimraf').sync // Package the rm -rf command as a package to delete files and folders, regardless of whether the folder is empty
const Metalsmith = require('metalsmith') / / the interpolation
const Handlebars = require('handlebars') / / template
const remove = require(".. /lib/remove") / / delete
const fs = require("fs")
const path = require("path")
module.exports = function (context) {
let metadata = context.metadata; // User-defined information
let src = context.downloadTemp; // Temporarily store the file directory
let dest = '/' + context.projectRoot; // The root of the project
if(! src) {return Promise.reject(new Error('invalid source:${src}`))}return new Promise((resolve, reject) = > {
const metalsmith = Metalsmith(process.cwd())
.metadata(metadata) // Put the user input information into
.clean(false)
.source(src)
.destination(dest);
metalsmith.use((files, metalsmith, done) = > {
const meta = metalsmith.metadata()
Object.keys(files).forEach(fileName= > {
if(fileName.split(".").pop() ! ="png") {const t = files[fileName].contents.toString()
files[fileName].contents = new Buffer.from(Handlebars.compile(t)(meta),'UTF-8')
}
})
done()
}).build(err= >{ remove(src); err ? reject(err) : resolve(context); })})}Copy the code
For specific usage methods, you can see the website (too long did not see, I also forgot ~, later to add details).
Basically, this is a file to achieve interpolation, use this tool at the same time also need a template library, here I choose Handlebars, specific syntax can be searched, and EJS and other template JS files, but the syntax may be different.
The idea is to go through all the files in the template, and then convert the contents of the file to a string, and then find the content of the template string and process it, but there is a catch, the tool doesn’t seem to recognize the image, so I put the code above to filter the image.
Then add our code to macaw-init.js
const generator = require('.. /lib/generator')// Template insertion
Copy the code
So let’s rewrite our go function
function go () {
next.then(projectRoot= > { //
if(projectRoot ! = ='. ') {
fs.mkdirSync(projectRoot)
}
CFonts.say('amazing', {
font: 'block'.// define the font face
align: 'left'.// define text alignment
colors: ['#f80'].// define all colors
background: 'transparent'.// define the background color, you can also use `backgroundColor` here as key
letterSpacing: 1.// define letter spacing
lineHeight: 1.// define the line height
space: true.// define if the output text should have empty lines on top and on the bottom
maxLength: '0'.// define how many character can be on one line
});
return download(projectRoot).then(target= > {
return {
projectRoot,
downloadTemp: target
}
})
})
.then(context= > {
// console.log(context)
return inquirer.prompt([
{
name: 'projectName'.message: 'Project name'.default: context.name
}, {
name: 'projectVersion'.message: 'Project Version number'.default: '1.0.0'
}, {
name: 'projectDescription'.message: 'Introduction to the Project'.default: `A project named ${context.projectRoot}`}, {name: 'isElement'.message: 'Use element or not'.default: "No"}, {name: 'isEslint'.message: 'Whether to use isEslint'.default: "No",
}
]).then(answers= > { // Optional callback function
let v = answers.isElement.toUpperCase();
answers.isElement = v === "YES" || v === "Y";
let iseslint = answers.isEslint.toUpperCase();
answers.isEslint = iseslint === "YES" || iseslint === "Y";
return {
...context,
metadata: {
...answers
}
}
})
}).then(context= > {
console.log("Generate file")
console.log(context)
// Delete the temporary folder and move the file to the target directory
return generator(context); // Interpolate
}).then(context= > {
// Success is shown in green to give positive feedback
console.log(logSymbols.success, chalk.green('created successfully :)'))
console.log(chalk.green('cd ' + context.projectRoot + '\nnpm install\nnpm start'))
}).catch(err= > {
console.error(err)
// Failed with red, enhanced hint
console.log(err);
console.error(logSymbols.error, chalk.red('failed to create:${err.message}`))})}Copy the code
You can see our user interaction with inquire.js and then interpolate the user responses after processing them
Website www.npmjs.com/package/cfo…
There are all kinds of fancy effects to explore
CFonts.say('amazing', {
font: 'block'.// define the font face
align: 'left'.// define text alignment
colors: ['#f80'].// define all colors
background: 'transparent'.// define the background color, you can also use `backgroundColor` here as key
letterSpacing: 1.// define letter spacing
lineHeight: 1.// define the line height
space: true.// define if the output text should have empty lines on top and on the bottom
maxLength: '0'.// define how many character can be on one line
});
Copy the code
Now that we have pulled the template from the remote end and implemented the interpolation, what do we do with the template?
6. Interpolate in the template
Let’s take a look at the processing code
{ "name": "{{projectName}}", "version": "{{projectVersion}}", "description": "{{projectDescription}}", "author": "lyz", "private": true, "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "lint": "eslint --ext .js,.vue src", "build": "node build/build.js" }, "dependencies": { {{#if isElement}} "element-ui": "^ 2.13.0", {{/ if}} "vue" : "^ 2.5.2", "vue - the router" : "^ 3.0.1", "node - sass" : "^ 4.13.0"}, "devDependencies" : {" autoprefixer ":" ^ 7.1.2 ", "Babel - core" : "^ 6.22.1", "Babel - eslint" : "^ 8.2.1", "Babel - helper - vue - JSX - merge - props" : "^ 2.0.3", "Babel - loader" : "^ 7.1.1", "Babel - plugin - syntax - JSX" : "^ 6.18.0", "Babel - plugin - transform - runtime" : "^ 6.22.0", "Babel - plugin - transform - vue - JSX" : "^ 3.5.0", "Babel - preset - env" : "^ 1.3.2", "Babel - preset - stage - 2" : "^ 6.22.0", "chalk" : "^ 2.0.1", "copy - webpack - plugin" : "^ 4.0.1", "CSS - loader" : "^ 0.28.0", "eslint" : "^ 4.15.0 eslint - config -", "standard" : "^ 10.2.1", "eslint - friendly - the formatter" : "^ 3.0.0", "eslint - loader" : "^ 1.7.1 eslint", "- the plugin - import" : "^ 2.7.0", "eslint - plugin - node" : "^ 5.2.0", "eslint - plugin - promise" : "^ 3.4.0 eslint - plugin -", "standard" : "^ 3.0.1", "eslint - plugin - vue" : "^ 4.0.0", "extract - text - webpack - plugin" : "^ 3.0.0 file -", "loader" : "^ 1.1.4", "friendly - errors - webpack - plugin" : "^ 1.6.1", "HTML - webpack - plugin" : "^ 2.30.1 node", "- the notifier" : "^ 5.1.2", "optimize - CSS - assets - webpack - plugin" : "^ 3.2.0", "ora" : "^ 1.2.0", "portfinder" : "^ 1.0.13 postcss -", "import" : "^ 11.0.0", "postcss - loader" : "^ mid-atlantic moved", "postcss - url" : "^ 7.2.1", "rimraf" : "^" server, "sass - loader" : "^ 7.3.1," "semver" : "^ 5.3.0", "shelljs" : "^ 0.7.6", "uglifyjs - webpack - plugin" : "^ 1.1.1", "url - loader" : "^ 0.5.8", "vue - loader" : "^ 13.3.0", "vue - style - loader" : "^ 3.0.1", "vue - the template - the compiler" : "^ 2.5.2 webpack", ""," ^ 3.6.0 ", "webpack - bundle - analyzer" : "^ 2.9.0", "webpack - dev - server" : "^ 2.9.1", "webpack - merge" : "^ 4.1.0"}, "engines" : {" node ":" > = 6.0.0 ", "NPM" : "> = 3.0.0"}, "browserslist" : [ "> 1%", "last 2 versions", "not ie <= 8" ] }Copy the code
As you can see, our template syntax {{}} and the following {{#if isElement}}{{/if}} are handlebus syntax that will be automatically parsed when encountered in the traversal file by the Metalsmith tool. Insert user input processing into our template. Here, I choose whether to configure Element and whether to configure ESLint by selecting whether to load the corresponding dependency packages in Packagejson
7. More extensions
In fact, so far, we can write a micro-customization template, but our interpolation is done in the code, but if we design to the file level of the configuration of how to add and delete, the current idea is to use the.gitignore file, add or remove files that are not needed from the file. Again, using our interpolation tool. This module will be supplemented later,
When it comes to changing the entire framework, such as a Vue and a React, it is recommended to add user queries when downloading templates, and then maintain multiple Git repositories on our remote site, downloading templates for different repositories according to the user’s choice.
conclusion
It may take more than a week to finish this article. Every time I open my mind again, THERE may be some omissions. I hope to correct any mistakes I find in the comments section.