preface

Scaffolding has become an essential development tool in the daily work of the front-end, through which it can not only reduce mechanical duplication, but also effectively organize and manage enterprise project templates.

There are different types of front-end projects within the enterprise, such as PC, H5 and small programs for different platforms. Different frameworks include the React and Vue technology stacks. The business type can be divided into the project area into background management system, portal website and data screen.

With so many different types of projects, the combination of technology stacks is tailored to the scenario. Background management system usually adopts VUE +Element UI + VUex + TS to build projects. In addition to basic Settings, the React Native project also needs to configure adaptation for different screens. At the beginning of the React Native project, you need to configure universal methods, such as route skipping, loading, and request methods.

It is not in the programmer’s style to have to repeat the process of building a project every time you start a new one.

Scaffolding can help front-end developers manage various types of project templates within the enterprise, for example by typing the following command on the command line:

cli create my-app  
Copy the code

Cli is the name of the scaffolding tool we developed,create is the command to create a new project, and my-app is the name given to the new project.

The command line accepts the above command and immediately lists all project templates in the enterprise (as follows):

* vue2
* vue3
* react-mobile
* CRM
Copy the code

All you need to do is press up and down on the keyboard to select the project template, and then press Enter to select it. The scaffold then automates the following tasks:

  • Based on the template name chosen by the developer, find the correspondinggithupWarehouse address, and download it locally
  • If the download fails due to network fluctuation, disconnect the connection and try again5time
  • After the project is downloaded successfully, open it in the root directorypackage.jsonFile, will project namenameChange the property tomy-app
  • package.jsonAfter modifying the file, install the dependencies, and finally run the start command to start the project

The whole process developers only need to simply input a few commands, the new project from download, installation to start all automatically completed.

Scaffolding not only helps us effectively manage and create new projects, but also adds additional features, such as one-click commands, and scaffolding automatically helps us create new pages or components.

The final implementation looks like this (source code at the end of the article):

implementation

Hello world

Create a new project folder mycli, open the folder and run NPM init to create a new project.

Add field “bin”: “./bin/index” in package.json (code as follows).

/ / package. The json file {" name ":" mycli ", "version" : "1.0.0", "description" : ""," main ":" index. Js ", "bin" : "./bin/index", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }Copy the code

Create a file bin/index in the project root directory and enter the following code.

#! /usr/bin/env node specifies the operating environment as Node.

#! /usr/bin/env node

console.log("hello world");

Copy the code

At this point you can test the code directly. Go to the project root directory and run the NPM link to link the scaffolding globally, then run the mycli command to print “Hello World “(as shown below).

commander

As you can see from the “Hello world” example above, the logic written in bin/index is executed as soon as mycli is entered on the command line.

To implement the functionality of scaffolding, we will first introduce a third party library, Commander, which is used to write various command-line tools.

Commander defines a command (the code is as follows). Program.com mand defines the create

command, where create is the defined keyword and

is the parameter passed in by the user.

#! /usr/bin/env node const { program } = require('commander'); The program version (" 1.0.0 "); Program.com command("create <app-name>").description(" create a new project ").action((appName)=>{console.log(appName); }) program.parse(process.argv);Copy the code

The following figure shows the invocation method: mycli create my-app The defined command is successfully invoked, and the appName is printed as my-app.

Program. version Sets the version number. You can run mycli -v to query the version number.

Mycli -h displays the help information. The description defined by the preceding command is displayed in the help information.

A command line tool can append parameters in addition to directly entering commands. For example, use option to define -t or –template to accept arguments passed in by the user.

#! /usr/bin/env node const { program } = require('commander'); The program version (" 1.0.0 "); Command ("create <app-name>").description(" create a new project ").option('-t, --template <template-name>',' select a template to download '). Action ((appName,options)=>{console.log(appName,options.template); }) program.parse(process.argv);Copy the code

The call method is shown in the following figure. Options to get the parameters passed by the user.

inquirer

Inquirer is a third-party tool library for command-line interaction with users, providing an Api that makes it easy to output lists and questions from the command line.

The first question asks the user if he likes durian and the type is confirm. The user only needs to return yes and no.

The second question is to ask the user what fruit they like to eat, and the user needs to input the answer.

#! /usr/bin/env node const inquirer = require('inquirer'); Inquirer. Prompt ([{type:"confirm", name:"firut", message:" do you like durian?", {type:"input", name:"food", }]). Then ((answers) => {console.log(answers); })Copy the code

Answers outputs the answers entered by the user based on the name attribute.

The scenario inquirer uses most frequently in scaffolding development is the output list (code and runtime renderings below).

When type is list, the command line window outputs a list for the user to select. Users can press up and down keys on the keyboard to select different answers, and then press Enter to confirm the answers.

#! /usr/bin/env node const inquirer = require('inquirer'); Inquirer. Prompt ([{type:"list", message:" which of the following characters has the highest skill?", name:"master", Choices: [" Monkey King ", "dapeng golden wings", "ox demon king", "yellow robe", "Huang Mei king"]}]), then ((answers) = > {the console. The log (answers); // {master:" master "}})Copy the code

In practical scenarios,inquirer is usually used with Commander.

Creating a new project

To get back to business, our initial goal is to create a configuration file, repo.js, to store the details of the various templates in order for the scaffold to automatically download the new project and install dependencies and start the application.

The URL is the githup address of the project,bootstrap is the startup command, and install is the installation command.

When you have a new project template, just add an item to the end of the configuration file.

Exports. config = {"vue3":{url:"kaygod/vue3-demo", bootstrap:" NPM run serve"}, "vue":{ url:"kaygod/vue_demo", bootstrap:"yarn serve", install:"yarn install" } }Copy the code

The scaffold entry file /bin/index is coded as follows. This file defines a command to create a project, but the logic for handling this command is wrapped in /bin/actions/create.js.

In this way, it is easier to maintain the structure of the code. If the entry file defines a new command, you can create a NEW JS file in /bin/actions to handle the logic of the command.

#! /usr/bin/env node const { program } = require('commander'); The program version (" 1.0.0 "); Program.com mand("create <app-name>").description(" create a new project ").action((appName)=>{ require("./actions/create")(appName); }) program.parse(process.argv);Copy the code

The create.js file is shown below, and the entire process of creating the project can be seen in the createProject function.

  • createProjectAccept project nameappName. Callinquirer.promptFor the first time, an input box is displayed asking the user to fill in the project description. A template list is displayed for the second time, asking the user to select a template to download.
  • process.cwd()isnodejsTo provide theapiTo return to the path where the user runs the COMMAND line window. Associate this path withappNameConcatenate to get the local path of the new project created.
  • downloadFunction to download the project locally
  • After the project is downloaded successfully,updatePackageFunction changespackage.jsoninformation
  • startThe function installs the dependencies for the new project before executing the start command
// /bin/actions/create.js file const inquirer = require('inquirer'); const { download } = require(".. /download"); const { updatePackage } = require(".. /updatePackage"); const { start } = require(".. /startProject"); const path = require("path"); const { config } = require(".. /repo"); async function createProject(appName){ const prompList = [ { type: 'input', name: 'description', message: 'Please enter project description :',}, {type:"list", message:" Please select a template to download :", name:"template_name", Choices: object.keys (config) // Get the names of all templates dynamically from the configuration file rebo.js}]; const { template_name ,description } = await inquirer.prompt(prompList); const project_dir = path.join(process.cwd(),appName); // The path to the new project try {await download(template_name,project_dir); / / download project to local await updatePackage (project_dir, {name: appName, the description, the template: template_name}); // Change package.json start(project_dir,template_name); // Start the project} catch (error) {console.log(error); } } module.exports = createProject;Copy the code

The operation effect picture is as follows:

download

In addition to downloading the project locally, the download function also needs to handle retry downloads if the download fails (code below).

The download-Git-repo third-party library provides an API to easily pull the repository source code from Githup.

Download-git-repo calls such as dl(‘ ${url} ‘,project_dir,async function(err)) {}), whose first parameter is the address of the remote repository (the configuration file repo has already been configured), the second parameter is the path to the local download, and the third parameter is the downloaded callback function.

If the download fails, the err of the callback function is not empty and you need to start the download and retry.

Download-git-repo also provides many other ways to download git, such as using Git Clone, private repositories, etc. See the official documentation for details.

const dl = require('download-git-repo'); const { startLoading,endLoading } = require("./loading"); const { config } = require("./repo"); const fse = require("fs-extra"); let count = 0; Exports.download = (template_name,project_dir) => {return new Promise(async (resolve,reject) => {const {url } = config[template_name]; If the directory is not empty, delete the contents of the directory. If the directory does not exist, create an await fse.emptydir (project_dir); (function execuate(){ count++; if(count >= 5){ count = 0; reject(); return; } startLoading(); // Loading dl(' ${url} ',project_dir,async function(err) {endLoading(); If (err) {console.log(err); Console. log("\n Download failed, download 3s later and try again... \n"); await sleep(); execuate(); }else{ resolve(null); count = 0; }})}) (); }); }; /** ** sleep */ const sleep = (time = 3000) =>{return new Promise((resolve)=>{setTimeout(()=>{resolve(null); },time) }) }Copy the code

The downloading process often becomes boring and lengthy due to network reasons. We need to display loading patterns on the interface (as shown in the following figure).

The LOADING pattern can be easily implemented with ora library, packaged as a function and exported to external calls.

// // bin/ load. js file const ora = require('ora'); const loading = ora('Loading'); Exports. startLoading = (exports.startLoading = (exports.startLoading =)) ') => { loading.text = text; loading.color = 'green'; loading.start(); }; exports.endLoading = () => { loading.stop(); };Copy the code

updatePackage

After the project is downloaded locally, we need to modify the package.json of the new project to the following form.

{"name": "my-app", "version": "0.1.0",... // omit "description": "vue3 project ", "template": "vue3"}Copy the code

Name and description are replaced with values entered by the user in scaffolding, while template holds the name of the project template corresponding to the current project (you can use this parameter later when scaffolding is used to create pages).

The updatePackage function relies on the Fs-extra API to read the contents of the file into memory for modification before writing to the original file.

const fse = require('fs-extra'); const path = require("path"); // Change package.json file exports.updatePackage = async (dirpath, data) => { const filename = path.join(dirpath,'package.json'); try { await fse.ensureFile(filename); let packageJson = await fse.readFile(filename); packageJson = JSON.parse(packageJson.toString()); packageJson = { ... packageJson, ... data }; packageJson = JSON.stringify(packageJson, null, '\t'); await fse.writeFile(filename, packageJson); } catch (err) {console.error("\npackage.json file operation failed! \n"); throw err; }};Copy the code

start

After pacakge.json is modified, scaffolding needs to install dependencies for the project and start the application (code below).

The start function does two main things: install dependencies and start projects. To install dependencies, run the NPM I command, and to start projects, run the NPM run serve command. For vue projects, run the NPM run start command.

Both dependencies and projects require NPM commands to be executed. Nodejs’ core module child_process enables scaffolding to run commands directly.

The start commands for different projects may be different. Some use NPM run serve and some use YARN start. These commands can be written in the configuration file repo.js and exported to the start function call.

const exec = require('child_process').exec; const { config } = require("./repo"); / / exports.start = async (path,template_name) => {await installLib(path,template_name); Console. log(' Project dependencies installed... '); await startProject(path,template_name); Console. log(' Project started successfully... '); }; const installLib = (path,template_name) => { const install_command = config[template_name].install || "npm i"; Return new Promise((resolve, reject) => {const workerProcess = exec(// install_command, {CWD: path, }, (err) => { if (err) { console.log(err); reject(err); } else { resolve(null); }}); workerProcess.stdout.on('data', function (data) { console.log(data); }); workerProcess.stderr.on('data', function (data) { console.log(data); }); }); }; const startProject = (path,template_name) => { const bootstrap_command = config[template_name].bootstrap || "npm run serve"; Return new Promise((resolve, reject) => {const workerProcess = exec(// start project bootstrap_command, {CWD: path, }, (err) => { if (err) { console.log(err); reject(err); } else { resolve(null); }}); workerProcess.stdout.on('data', function (data) { console.log(data); }); workerProcess.stderr.on('data', function (data) { console.log(data); }); }); };Copy the code

Create the page

Scaffolding not only does the basic work of creating new projects, but also can be expanded according to actual needs.

Entry file /bin/index defines a new command newpage(code below) to create a newpage for the project.

#! /usr/bin/env node const { program } = require('commander'); The program version (" 1.0.0 "); Command ("create <app-name>").description(" create a new project ").option('-t, --template <template-name>',' select a template to download '). Action ((appName,options)=>{require("./actions/create")(appName,options); Command ("newpage <page-name>").description(" create a newpage ").action((pageName)=>{ require("./actions/newpage")(pageName); }) program.parse(process.argv);Copy the code

The renderings are as follows:

The newpage function code is as follows. Using the vue3 project template as an example, run the newPage command to add a page in the project/SRC /views folder.

The newPage function first reads the template field in the project’s package.json file, which is the template name that scaffolding added to package.json after the project was created earlier.

The Template field lets you know which template type the current project belongs to. You then use the policy pattern to write the logic to create new pages for different templates.

For example, the following code defines a vue3Handler function that creates a new page for a project downloaded using the VUe3 template.

const Mustache = require('mustache'); // template engine const path = require("path"); const fse = require("fs-extra"); // async function newPage(page_name){// Create page name try {const packageJson = await fse.readFile("./package.json"); const { template } = JSON.parse(packageJson.toString()); Const fn = eval(' ${template}Handler '); fn && fn(page_name,template); } catch (error) {console.log("\n Please execute this command in the project root path! \n"); throw error; }} /** * create a new page for the vue3 template */ const vue3Handler = async (page_name,template)=>{let template_content = await fse.readFile(path.join(__dirname,`.. /template/${template}/index`)); template_content = template_content.toString(); const result = Mustache.render(template_content,{ page_name }); // start creating file await fse.writefile (path.join("./ SRC /views", '${page_name}.vue'), result); Console. log("\n Page created successfully! \n"); } module.exports = newPage;Copy the code

The vue3Handler function first reads the vue3 template file placed under /template/vue3/index (code below).

Convert the template code to a string and assign a value to the template_content variable, and use the template engine Mustache to change the page name corresponding to the name attribute in the template to the value the user enters when typing the newPage command.

After the modification is complete, output the new file content in the memory to the/SRC /views file.

<template>
  <div class="container"></div>
</template>

<script lang='ts'>
import {
  reactive,
  toRefs,
  onBeforeMount,
  onMounted,
  defineComponent,
} from 'vue'

interface DataProps {}
export default defineComponent({
  name: '{{page_name}}',
  setup() {
    return {
    }
  },
})
</script>
<style scoped lang="less">
.container{}
</style>
Copy the code

Inspired by this, scaffolding tools can also define more logic to fulfill more requirements. For example, the newpage command does not just create a newpage in the views folder. It can also automatically insert the newpage configuration into routing and vuex, so that the result can be seen in the browser with one click of command.

Finally, publish the tool to NPM and share it with other members.

The source code

The source code