"How wheels run" teaches you to develop a scaffold from 0 to 1

Posted by shaunno2007 on Thu, 16 Dec 2021 01:47:55 +0100

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

Topics: Javascript Front-end React Vue.js