Hello, I'm A bowl week , a front end that doesn't want to be drunk. If you are lucky enough to get your favor, I am very lucky~
Write in front
Now there are so many mature scaffolds on the market, do we still need to develop a scaffold? If we are in the perspective of application, such scaffolds as Vue CIL and create react app are enough; However, in the process of development, we need to re open many project templates, but often such re opening is not once; As a mature program ape, if you do a lot of repetitive work, you must refuse. At this time, you need to develop a scaffold for your own use, or you can upload Github open source for everyone to use.
Also, if we create a project from the perspective of learning, if we just use scaffolding, we will never know how to build a project.
This article will teach you how to develop a scaffold.
preparation
First create a project, use the npm init -y command to initialize a Node project, and then create the project directory structure, as shown below:
├── node_modules # Project dependent resources ├── bin # Scaffold entrance. │ └── ywz.js # Import file. ├── lib # Main logic code of the project │ └── index.js # js file for logical processing ├── .gitignore # Git push ignore list profile ├── .prettierrc # Prettier format configuration file └── package.json # Various modules required by the project, as well as the configuration information of the project
Now let's introduce some plug-ins used in scaffold:
-
commander : complete node.js Command line solution, Chinese documents
-
axios: pull data
-
ora: achieve loading effect
-
inquirer : a collection of generic interactive command line user interfaces
-
chalk: realize color terminal font
-
Download Git repo: Download and extract Git repository based on Node
-
metalsmith: a very simple static site generator that can be inserted
-
ncp: for copy files
-
consolidate: a collection of template engines
-
handlebars: template engine
The installation commands are as follows:
npm i commander axios ora inquirer chalk download-git-repo metalsmith ncp consolidate handlebars -D
Now let's modify our package JSON file, as follows:
{ "name": "ywz", "version": "1.0.0", "description": "", "main": "lib/index.js", "directories": { "lib": "lib" }, "bin": { "ywz": "bin/ywz.js" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "ywanzhou", "license": "ISC", "devDependencies": { "axios": "^0.24.0", "chalk": "^5.0.0", "commander": "^8.3.0", "consolidate": "^0.16.0", "download-git-repo": "^3.0.2", "handlebars": "^4.7.7", "inquirer": "^8.2.0", "metalsmith": "^2.3.0", "ncp": "^2.0.0", "ora": "^5.4.1" } }
The following contents are mainly modified here:
-
main: the entry file of the project
-
bin: executable command file
Then in bin / YWZ JS file, write the following code:
#! /usr/bin/env node console.log('Scaffolding')
#! / usr/bin/env node tells the system to find it in the PATH directory, and then run it using Node
Then link ywz to the global through the link command to facilitate our testing. The example code is as follows:
npm link
This link is equivalent to the soft link in Linux. If the test is completed, you can cancel the link through npm unlink.
It is worth noting that when using the npm link command, it must be in the root directory of the project.
Now we can type ywz on the command line to see the results of the command line prompt.
So far, our preparatory work has been completed. Now let's get to the point.
Configuration command
process.argv attribute
The first step of handwritten scaffold is to configure our commands. Only after configuring the commands can it be developed.
First, introduce process Argv property, which returns an array containing when the node is started Command line parameters passed in when the JS process. The first element is process Execpath, i.e. node JS installation path. The second element will be the path to the JavaScript file being executed. The remaining elements will be any other command line parameters.
Suppose our bin / YWZ JS
#! /usr/bin/env node console.log(process.argv)
Then enter the following command on the command line
ywz create node
You can see the following paragraph
[ 'C:\\Program Files\\nodejs\\node.exe', 'E:\\nodejs\\npm-global\\node_modules\\ywz\\bin\\ywz.js', 'create', 'node' ]
It must be very troublesome to operate the parameters in the command line through the attribute provided by the native Node, so we use the package provided by the third party, that is commander.
Use of commander
First, we write the following code:
#! /usr/bin/env node const { program } = require('commander') const { version } = require('../package.json') // . The version() method is used to set the version number. When -- version or - V is executed on the command line, the version displayed // . parse() is used to parse named line parameters. The default value is process Argv * important program.version(version).parse()
Then enter ywz -V on the command line to see the version number.
Now we can pass command() method to define the command. The first parameter of this method is the command name, which can be followed by command parameters. Command parameters can also be used The argument() method is specified separately.
The method accepts three parameters as follows:
-
Required parameter: indicated by angle brackets
-
Optional parameters: indicated by square brackets
-
Variable parameter: add..., after the parameter name, For example, < dirs... >, If there are variable parameters, they must be at the end.
It can also be passed alias() method is used to set alias The action() method processes the command.
The example code is as follows:
#! /usr/bin/env node const { program } = require('commander') const { version } = require('../package.json') program // Define command .command('create') // Define alias .alias('crt') // Define parameters .argument('<projectName>') // Define command processing methods .action(projectName => { // This method accepts a callback function, and the parameter name of the callback function is the parameter we defined earlier console.log(projectName) }) program.version(version).parse()
Moreover, using the commander will automatically help us generate the help option. The test is as follows:
ywz --help
The results are as follows:
Usage: ywz [options] [command] Options: -V, --version output the version number -h, --help display help for command Commands: create|crt <projectName> help [command] display help for command
Optimization command
Now, for our project, it is enough to use commander at this level, and then modify the code
#! /usr/bin/env node const { program } = require('commander') const { version } = require('../package.json') const creatProject = require('..') program .command('create') .alias('crt') .argument('<projectName>') .action(projectName => { // Processing function, defined externally creatProject(projectName) }) program.version(version).parse()
require(..) Index. In the parent directory of JS, due to our package The entry in the main field of JSON is in lib / index JS, so require (..) Lib / index. Is introduced in JS file, write the following code in this file
module.exports = function (name) { console.log(name) }
Then enter ywz create node on the command line to see the node output in the command, that is, the name of the project.
Get remote template
summary
Here, we store the remote warehouse in Github and introduce the two API s provided by Github, as follows:
- Gets the warehouse list of the specified user
`https://api.github.com/users/${username}/repos`
- Gets the branch list of the specified warehouse
`https://api.github.com/repos/${username}/${repositoriesName}/branches`
The reference address of the template we use for testing is: Pacpc / node template: node warehouse template (github.com)
Use of basic library
First, let's introduce the use of ora and inquirer libraries.
The inquirer library is used to interact on the command line. Its syntax structure is as follows:
const inquirer = require('inquirer') module.exports = async name => { let { projectName } = await inquirer.prompt({ /* Pass your questions in here */ }) console.log(projectName) }
inquirer. The return value of prompt () method is a Promise. Here we use async/await syntax.
Method accepts two parameters, both of which are objects. Usually, the first one is enough. For specific syntax content, please refer to here . The following code shows the basic usage of the inquirer library
const inquirer = require('inquirer') module.exports = async name => { let { projectName } = await inquirer.prompt({ // The type of problem. Input indicates input type: 'input', // key of the answer name: 'projectName', // What's the problem message: 'The project name is it?', // Default value default: name, }) let { license } = await inquirer.prompt({ // The type of question. list indicates that you can choose type: 'list', // key of the answer name: 'license', // What's the problem message: 'Choose a license', // The selected options are supported choices: ['LGPL', 'Mozilla', 'GPL', 'BSD', 'MIT', 'Apache'], // Default value default: 'MIT', }) console.log(projectName, license) }
Now enter YWZ create node test on the command line, and the running results are as follows:
The ora library is used to realize the loading effect. The use of the library is relatively simple. Directly call the ora() method, and you can pass in a string as the display content. The method returns an instance object. You can call the start() method to start rotation, stop() to stop rotation, succeed() to succeed and stop rotation, and fail() to stop rotation. There are also many example methods. For details, please refer to the documentation, click here
The example code is as follows:
const inquirer = require('inquirer') const ora = require('ora') module.exports = async name => { let { projectName } = await inquirer.prompt({ type: 'input', name: 'projectName', message: 'The project name is it?', default: name, }) const spinner = ora('Start loading...').start() setTimeout(() => { console.log('\n The project name is:' + projectName) spinner.succeed('Loading complete') }, 3000) }
The test results are as follows:
Get remote template
Now you can get our specific warehouse name through the axios library, and then get the corresponding warehouse branch according to the warehouse name. Select the branch and download it directly.
First, we encapsulate a loading method, which can add a loading effect to axios requests. The specific implementation code is as follows:
/** * @description: Add a loading effect to a Promise function * @param {Function} callback Function that returns Promise and needs to be modified by loading * @returns {Function} Modified method */ const loading = callback => { return async (...args) => { // start let spinner = ora('start...').start() try { // Success without exception let res = await callback(...args) spinner.succeed('success') return res } catch (error) { spinner.fail('fail') return error } } }
However, the two API s mentioned earlier are encapsulated into methods, and the code is as follows:
/** * @description: Get warehouse list * @param {string} username Obtained user name * @returns {Array} Warehouse list */ const fetchRepoList = async username => { let { data } = await axios.get( `https://api.github.com/users/${username}/repos`, ) return data.map(item => item.name) } /** * @description: Get branches list * @param {string} username User name to get * @param {string} repoName Warehouse name to be obtained * @returns {Array} branches list */ const fetchTagList = async (username, repoName) => { let { data } = await axios.get( `https://api.github.com/repos/${username}/${repoName}/branches`, ) return data.map(item => item.name) }
The code for obtaining the remote template is as follows:
module.exports = async name => { let { projectName } = await inquirer.prompt({ // The type of problem. Input indicates input type: 'input', // key of the answer name: 'projectName', // What's the problem message: 'The project name is it?', // Default value default: name, }) // Get warehouse list let repos = await loading(fetchRepoList)('pacpc') // Select warehouse list let { repoName } = await inquirer.prompt({ type: 'list', name: 'repoName', message: 'Choose a template', choices: repos, }) // Get all branches let branches = await loading(fetchTagList)('pacpc', repoName) // If there are multiple branches, the user can select multiple branches, and no multiple branches can be downloaded directly if (branches.length > 1) { // existence let { checkout } = await inquirer.prompt({ type: 'list', name: 'checkout', message: 'Choose the target version', choices: branches, }) repoName += `#${checkout}` } else { repoName += `#${branches[0]}` } }
Now we can test the feasibility of this code through the command line.
Download template
Download git repo Library
The download git repo library can download the repository in Github, and the use method is relatively simple. You can directly pass the user name / warehouse name as a parameter; Here we use node JS provides the Promise () method to convert the method provided by the download git repo library into Promise. The example code is as follows:
const { promisify } = require('util') const download = promisify(require('download-git-repo'))
Cache processing
If we download the template every time we create a project, it is actually unnecessary. We can cache it when we download it for the first time. In the future, if necessary, we can use it directly without downloading.
Generally, we store the cache in the user directory Under tmp directory, in node JS to get the user directory through process env. Userprofile to obtain the user directory under windows through process env. Home to get the user directory under macOS. You can also use process Platform property to get whether the current is a Windows system. The example code is as follows:
// win32 means Windows system console.log(process.platform) // win32 const user = process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'] console.log(user) // C:\Users\Administrator
Define download function
Now that we know how to download a template on Github and get the directory where the template is stored, let's define a download function. The implementation code is as follows:
/** * @description: Download content from specific warehouse * @param {string} username Name of warehouse owner * @param {string} repoName Warehouse name + branch name, # No * @returns {string} Temporary directory for download */ const downloadGithub = async (username, repoName) => { const cacheDir = `${ process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'] }/.tmp` // Splice a downloaded directory let dest = path.join(cacheDir, repoName) // The existsSync method provided by the fs module is used to determine whether the directory exists. If it exists, it means that it does not need to be downloaded let flag = existsSync(dest) let url = `${username}/${repoName}` if (!flag) { // If you need to download, perform the download await loading(download)(url, dest) } return dest }
The use of this function is as follows:
let dest = await downloadGithub('pacpc', repoName)
Render template data
Now that we have downloaded the template locally, we will start processing the data in the template.
Determine template data
One of our scaffolds may be used to use many templates, but each template may have some personalized content. We add a question in each template JS, which is used to store the questions of each template to generate the corresponding content.
The question tested here JS is as follows:
module.exports = [ { type: 'input', name: 'version', message: 'version?', default: '0.1.0', }, { type: 'input', name: 'description', message: 'description', }, { type: 'input', name: 'author', message: 'author?', }, { type: 'input', name: 'email', message: 'email?', }, { type: 'input', name: 'github', message: 'github?', }, { type: 'list', name: 'license', message: 'Choose a license', choices: ['LGPL', 'Mozilla', 'GPL', 'BSD', 'MIT', 'Apache'], default: 'MIT', }, ]
template engine
The template engine we use here is Handlebars (handlebarsjs.com) , we passed consolidate To uniformly manage the template engine, and the use method is relatively simple. The example code is as follows:
const { render } = require('consolidate').handlebars content = await render(content, data)
The content in the above code represents the original data, and the return value is to replace the template syntax in the original data with the data content in data.
Application of metalsmith Library
Here is an introduction to the application of metalsmith library, which is a static site generator with simple usage, as shown in the following code:
Metalsmith(__dirname) // Source directory default src .source() // Target directory default build .destination() // Intermediate treatment method .use(async (files, metal, done) => { // Files refers to all types of files in the template directory to be rendered // metal.metadata() can save all the data and hand it over to the next use r // Call done() after execution done() }) // There can be multiple processing methods .use((files, metal, done) => { // Obtain the data filled in by the user in the previous use done() }) // Processing completed .build((err) => { if (err) { // failed } else { // succeed } })
The above is a basic application of metalsmith library.
Render data
We have known the application of some tool libraries before rendering data and the questions in each template, and began to write our main code. The code is as follows:
// Download template to temporary directory let dest = await downloadGithub('pacpc', repoName) // Judge whether the downloaded template contains question If JS is included, replace the template, otherwise copy it directly to the target warehouse if (existsSync(path.join(dest, 'question.js'))) { await new Promise((resolve, reject) => { Metalsmith(__dirname) .source(dest) .destination(path.resolve(projectName)) .use(async (files, metal, done) => { // Files refers to all types of files in the template directory to be rendered // Load question file const quesList = require(path.join(dest, 'question.js')) // Define interaction problems based on problem data let answers = await inquirer.prompt(quesList) // Currently, answers stores the data passed by the user. We use metal Metadata () saves it for use in the next use let meta = metal.metadata() Object.assign(meta, answers, { projectName }) // Delete question JS file to avoid copying user templates // The reason why you can delete with the delete keyword is that all existing in files are buffer s. We can delete this key directly and the corresponding value will be deleted delete files['question.js'] done() }) .use((files, metal, done) => { // Get the data stored in the previous use let data = metal.metadata() // Make all self owned attributes in files as one data let arr = Reflect.ownKeys(files) // By traversing the array, all buffers are converted into strings, then replaced by the template engine, and finally converted into buffer storage arr.forEach(async file => { // Only js or json files are replaced if (file.includes('js') || file.includes('json')) { let content = files[file].contents.toString() // Replace if template engine syntax is included if (content.includes('{{')) { content = await render(content, data) files[file].contents = Buffer.from(content) } } }) done() }) // If there is an exception, Promise calls reject .build(err => { if (err) { reject(err) } else { resolve() } }) }) console.log('\nsuccess~') } else { // If the template is not required for processing, copy it directly to the project directory ncp(dest, projectName) }
At this point, our basic code is complete. Now we can test with a code. On the command line, enter YWZ create node test. The test results are as follows:
Write at the end
This article is over. It took about an afternoon to write this article. I hope it can be helpful to you.
This is How do wheels run The first article of the column, which continuously outputs some articles on the principle of wheels and how to make wheels. If it is consistent with your appetite, you can support it for three times.
Previous recommendation