It has been 10 months since the last article. In fact, I have summarized a lot of technical points at ordinary times, but I just want to publish articles about front-end engineering series on nuggets, and because there has been no landing engineering projects during this period of time (just lazy 🤦!). “, so it’s not good to blog without doing your own experiments.

OK, the opportunity to write this article is because the author is about to do a super, super, super big project. At the early stage, the author hopes to build some things of front-end infrastructure, so he wants to make a scaffold tool and integrate the things of infrastructure into the template to achieve the purpose of standardization and efficiency improvement. The point of this article is not to teach you how to write a scaffold (there are too many tutorials on this topic), but to show you what scaffolding has that can help you.

As usual, I’ll start with a brief account of how I came to write this scaffolding.

The scaffold

The overall idea is based on vuE-CLI2’s building model (why not vue-CLI3’s? Too complicated!) “And then made some modifications myself. The overall directory structure is as follows:

| - pandly - cli | | - bin # command file | | | - pandly # main command | | | - pandly - create # create command | | - lib # tool module | | | - ask. Js # interaction ask | | | - check - version. Js version # check the scaffold | | | - complete. Js # command execution after the completion of the operation | | | - generate. Js # template rendering | | - package. JsonCopy the code

Vue-cli2 is a big simplification compared to vue-Cli2. The biggest difference is that vue-Cli2 first downloads the template through the scaffolding tool and then renders the template based on the interactive input. I changed it to collect interactive input and then download the template to render.

Use commander to parse commands

First, we initialize an NPM project using NPM init and define the command name and corresponding executable file in the bin field of the project’s package.json:

{
  "name": "pandly-cli"."version": "1.0.0"."description": "An awesome CLI for scaffolding Vue.js projects"."preferGlobal": true."bin": {
    "pandly": "bin/pandly"."pandly-create": "bin/pandly-create"},...Copy the code

Handle user input commands in the bin/pandly file:

#! /usr/bin/env node

const program = require('commander')

program
  .version(require('.. /package').version)
  .usage('<command> [options]')
  .command('create'.'Create a new project with a template')
  .parse(process.argv)
Copy the code

Two things to note here:

  1. Be sure to add a line to the head of the file#! /usr/bin/env nodeCode to make it an executable;
  2. incommanderIf a subcommand is defined in theaction()When,commanderAn attempt will be made to search for the file name in the directory of the entry script[command]-[subcommand]Executable file execution. Such as.command('create')Command will find those in the same folderpandly-create, so the file is calledbin/pandly-createAnd executed.

Pandly – the cli has only one command pandly create XXX.

Use inquirer for command line interaction

Because of the large number of items being queried, the logic for command line interaction is placed in a separate lib/ask.js file:

const { prompt } = require('inquirer')

const questions = [
  ...,
  {
    name: 'UI'.type: 'list'.message: 'Pick a UI library to install'.choices: [{
      name: 'Element UI'.value: 'element-ui'.short: 'Element'
    }, {
      name: 'View Design'.value: 'view-design'.short: 'View'
    }, {
      name: 'Ant Design'.value: 'ant-design-vue'.short: 'Ant'}}],... ]module.exports = function ask () {
  return prompt(questions).then(answers= > {
    return answers
  })
}
Copy the code

Lib /ask.js finally throws a Promise object that returns the collected user input.

Use the download-git-repo command to download the template

After collecting user input, start downloading the template, in bin/pandly-create:

#! /usr/bin/env node

const program = require('commander')
const chalk = require('chalk')
const path = require('path')
const home = require('user-home')
const exists = require('fs').existsSync
const inquirer = require('inquirer')
const ora = require('ora')
const rm = require('rimraf').sync
const download = require('download-git-repo')

const checkVersion = require('.. /lib/check-version')
const generate = require('.. /lib/generate')
const ask = require('.. /lib/ask')

program
  .usage('[project-name]')
  .parse(process.argv)

// Create a directory name for the project
const rawName = program.args[0]
// true indicates no write or '.', that is, build in current directory
constinPlace = ! rawName || rawName ==='. '
// If you are building under the current directory, create the project name of the current directory; If not, create the project name rawName
const projectName = inPlace ? path.relative('.. / ', process.cwd()) : rawName
// Create the absolute path to the project directory
const projectPath = path.resolve(projectName || '. ')
// Remote template download path to local
const downloadPath = path.join(home, '.vue-pro-template')

const spinner = ora()

process.on('exit'.() = > {
  console.log()
})

// If the directory name created in the current directory already exists, the query will be made, otherwise run will be executed directly
if (inPlace || exists(projectPath)) {
  inquirer.prompt([{
    type: 'confirm'.message: inPlace ? 'Generate project in current directory? ' : 'Target directory exists. Do you want to replace it? '.name: 'ok'
  }]).then(answers= > {
    if (answers.ok) {
      console.log(chalk.yellow('Deleting old project ... '))
      if (exists(projectPath)) rm(projectPath)
      run()
    }
  }).catch(err= > console.log(chalk.red(err.message.trim())))
} else {
  run()
}

function run() {
  // Collect user input before downloading the template
  ask().then(answers= > {
    if (exists(downloadPath)) rm(downloadPath)
    checkVersion(() = > {
      / / template making address to https://github.com/pandly/vue-pro-template
      const officalTemplate = 'pandly/vue-pro-template'
      downloadAndGenerate(officalTemplate, answers)
    })
  })
}

function downloadAndGenerate (officialTemplate, answers) {
  spinner.start('Downloading template ... ')
  download(officialTemplate, downloadPath, { clone: false }, err= > {
    if (err) {
      spinner.fail('Failed to download repo ' + officialTemplate + ':' + err.message.trim())
    } else {
      spinner.succeed('Successful download template! ')
      generate(projectName, downloadPath, projectPath, answers)
    }
  })
}
Copy the code

Since templates are updated from time to time, to ensure the use of the latest template, every time I use scaffolding to create a project, I first delete the old local template, and then download the latest template from Github.

Use Metalsmith + Handlebars to handle the template

The template provided by Pandly-CLI is not just a pure file, but can be compiled with parameters entered by the user to produce different object files. So get each file in the template through Metalsmith, and then compile each file using Handlebars. In the lib/generate. In js:

const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const exists = require('fs').existsSync
const path = require('path')
const rm = require('rimraf').sync
const ora = require('ora')

const complete = require('./complete')

const spinner = ora()

// register handlebars helper
Handlebars.registerHelper('if_eq'.function (a, b, opts) {
  return a === b
    ? opts.fn(this)
    : opts.inverse(this)
})

Handlebars.registerHelper('unless_eq'.function (a, b, opts) {
  return a === b
    ? opts.inverse(this)
    : opts.fn(this)})module.exports = function generate (name, src, dest, answers) {
  spinner.start('Generating template ... ')
  if (exists(dest)) rm(dest)
  Metalsmith(path.join(src, 'template'))
    .metadata(answers)
    .clean(false)
    .source('. ')
    .destination(dest)
    .use((files, metalsmith, done) = > {
      const metadata = metalsmith.metadata()
      const keys = Object.keys(files)
      keys.forEach(fileName= > {
        const str = files[fileName].contents.toString()
        if (!/{{([^{}]+)}}/g.test(str)) {
          return
        }
        files[fileName].contents = Buffer.from(Handlebars.compile(str)(metadata))
      })
      done()
    })
    .build((err, files) = > {
      if (err) {
        spinner.fail(`Faild to generate template: ${err.message.trim()}`)}else {
        new Promise((resolve, reject) = > {
          setTimeout(() = > {
            spinner.succeed('Successful generated template! ')
            resolve()
          }, 3000)
        }).then(() = > {
          constdata = {... answers, ... {destDirName: name,
            inPlace: dest === process.cwd()
          }}
          complete(data)
        })
      }
    })
}
Copy the code

{{}} placeholders are used for parameter substitution and conditional compilation in templates, such as package.json:

{
  "name": "{{ name }}"."version": "1.0.0"."description": "{{ description }}"."author": "{{ author }}"."dependencies": {... {{#if_eq UI"element-ui"}}
    "element-ui": "Tokens ^ 2.13.1"
    {{/if_eq}}
    {{#if_eq UI "view-design"}}
    "view-design": "^ 4.2.0"
    {{/if_eq}}
    {{#if_eq UI "ant-design-vue"}}
    "ant-design-vue": "^ 1.5.3." "
    {{/if_eq}}
  },
  ...
}
Copy the code

Create process execution commands using child_process

After the template is successfully compiled, if you choose to initialize your Git local repository or install your project using NPM, you will need to start a separate process to execute these commands. In the lib/complete. In js:

const spawn = require('child_process').spawn
const chalk = require('chalk')
const path = require('path')

module.exports = function complete(data) {
  const cwd = path.join(process.cwd(), data.inPlace ? ' ' : data.destDirName)
  if (data.git) {
    initGit(cwd).then(() = > {
      if (data.autoInstall) {
        installDependencies(cwd, data.autoInstall).then(() = > {
          printMessage(data)
        }).catch(e= > {
          console.log(chalk.red('Error:'), e)
        })
      } else {
        printMessage(data)
      }
    }).catch(e= > {
      console.log(chalk.red('Error:'), e)
    })
  } else if (data.autoInstall) {
    installDependencies(cwd, data.autoInstall).then(() = > {
      printMessage(data)
    }).catch(e= > {
      console.log(chalk.red('Error:'), e)
    })
  } else {
    printMessage(data)
  }
}

function initGit (cwd, executable = 'git') {
  return runCommand(executable, ['init'], {
    cwd,
  })
}

function installDependencies(cwd, executable = 'npm') {
  console.log(`\n# ${chalk.green('Installing project dependencies ... ')}`)
  console.log('# ========================\n')
  return runCommand(executable, ['install'], {
    cwd,
  })
}

function runCommand(cmd, args, options) {
  return new Promise((resolve, reject) = > {
    const spwan = spawn(
      cmd,
      args,
      Object.assign(
        {
          cwd: process.cwd(),
          stdio: 'inherit'.shell: true,
        },
        options
      )
    )

    spwan.on('exit'.() = > {
      resolve()
    })
  })
}

function printMessage(data) {
  const message = `
# ${chalk.green('Project initialization finished! ')}
# ========================

To get started:

  ${chalk.yellow(
    `${data.inPlace ? ' ' : `cd ${data.destDirName}\n `}${installMsg(data)}npm run serve`
  )}
`
  console.log(message)
}

function installMsg(data) {
  return! data.autoInstall ?'npm install\n ' : ' '
}
Copy the code

The whole process of scaffolding construction is generally the above steps, not difficult, as long as you master the core of several libraries can easily build their own scaffolding. Ok, now I’m going to introduce my template.

The template

The template is a secondary integration based on VUE-CLI3 and contains the following:

Vue buckets

vue + vue-router + vuex

Initialize the Git local repository

Three major UI libraries (ElementUI, ViewDesign, AntDesign) can be selected for installation, and support global import and on-demand import

Directly generate the UI library corresponding to the head and navigation bar layout of the background management template, only need to focus on the preparation of page content

Router module decoupling scheme, annexed navigation bar rendering

|-src
| |-router
| | |-modules
| | | |-index.js
| | | |-navigation1.router.js
| | |-index.js
Copy the code
  • modules/navigation1.js: Routing modules are divided by function, for examplenavigation1.router.jsAboutnavigation1Module routing;
  • modules/index.jsUse:require.contextTo achieve themodulesAutomatic merging of routing modules without manual merging;
  • index.js:vue-routerRelated configuration of.

Finally, the exported routing table renders the navigation bar without the need to write additional navigation bar data.

Elegant Axios request scheme

See: Front-end Engineering (3) : Elegantly designing axios-based request solutions in projects

The front-end caching mechanism is not added to the template. Students can add it themselves if they need it

Vue + JS code style verification

The ESLint mode defaults to Standard and can optionally validate vUE code styles:

Only rely onwebpack-dev-serverThe localmock-serverPlan;

Old solution: Express + Nodemon

New scheme: webpack-dev-server + mocker-API

The mock-Server of the new solution is implemented entirely based on Webpack-dev-server, without the need to install express as a separate service in the project. So mock- Server starts automatically at the same time the front-end service starts. At the same time, with the help of mocker-API, the API proxy is simulated and hot Mocker file replacement is supported.

Multi-environment modulation scheme based on HTTP-proxy-Middleware

See: Front-end Engineering (4) : HTTP-proxy-Middleware proxies in multiple environments

Based on the Angular team’s contracted Git commit specification

See Front-end Engineering (2) for details: Quickly build workflows based on the Angular team’s code submission specifications

Packaging analysis

Execute NPM run build –report for packaging analysis

To be perfect

  1. Unit testing
  2. supportES7New syntax
  3. Optimization of construction speed
  4. Optimized first screen rendering

The last

This article is not a technical article, just provides a scaffolding based project solution, hope to help students!

Usage:

  • npm install pandly-cli -g

  • pandly create xxx

Github address: github.com/pandly/pand…