1.0.1 • Published 5 years ago

tpld v1.0.1

Weekly downloads
1
License
ISC
Repository
-
Last release
5 years ago

手写一个规范流程化的脚手架

对于一些流程化的,没有任何技术含量的工作,能不能交由计算机来完成呢?我们能不能只输入少量的变量就可以完成相关的工作呢?通常而言,我们第一个想到的就是使用 shell 这样的脚本,但我们可以使用 node 做得更多且更好。本节我们将通过实现一个 前端脚手架 来示例如何使用 node 来处理流程化的工作。

TL;DR

在前端工程中,我们希望通过使用最新的语法(js和css)和各类代码检查工具来提升开发效率。同时,我们也希望把一些积累下来的最佳实践、通用方法、部署配置、性能优化等等方案继承下来。

最先想到的就是在 gitlab 上创建一个最佳实践的示例工程,定期有人维护、更新最新的各类插件。当我们新开一个项目的时候,只需开发人员下载工程,并且修改些许配置就可以应用了。但是这 些许 配置就有可能因为各种原因而浪费无尽的时间,例如 配置文档写得不清楚、文档更新不及时、开发人员没有相关配置经验等等。

npm.io

因此,我们期待一个示例工程以及一个自动生成工程的脚手架,使得我们通过多个下一步正确无误地创建工程。

npm.io

以下我们会从几个方面来讲解如何实现一个前端脚手架。

思路

总体思路:

  • 用户在 terminal 输入必要信息;
  • 脚本自动从 gitlab 下载模板工程;
  • 脚本使用 用户输入的信息 修改 模板工程 中的文件;

npm.io

具体实现思路

  • 注册命令,并能接收命令行传入的参数
  • 询问的方式获得用户的输入信息
  • gitlab 上获得模板工程
  • 改写模板工程中的文件
  • 自动下载工程依赖

接下来我们将分步实现

分步实现

1 注册命令

我们需要使用 npm link 命令将我们的可执行文件(bin)链接到 {prefix}/bin/{name}。换句话说,npm link 命令可以让我们在任意位置使用命令行直接运行我们的脚本。

step-1: 初始化工程

首先初始化一个工程,并在工程中安装 .editorconfigeslint.jsprettier.js 等插件。详细可以参考 一个易维护的 webpack 工程

示例工程: examples/01

step-2: 创建 src/bin.js 文件

在第一行中,#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。#!/usr/bin/env node 这句话让系统动态的去 PATH 目录中查找 node 来执行你的脚本文件。

src/bin.js

#!/usr/bin/env node

// 获得参数
console.info(process.argv)

step-3: 调整 package.json

package.json 中,添加 bin 属性

{
  "bin": {
    "lcli": "src/bin.js"
  }
}

step-4: 运行 npm link

在工程根目录下执行

npm link

执行脚本后,terminal 中显示了链接信息。

...

/usr/local/bin/scli -> /usr/local/lib/node_modules/00/src/bin.js
/usr/local/lib/node_modules/00 -> /Users/CodingNutsZac/Documents/git/write-a-cli/examples/01

测试

terminal 中输入

lcli

获得以下结果:

npm.io

示例工程

示例工程: examples/01


2 解析命令

我们通过 console 可以知道,从 argv数组第三个元素开始就是我们输入的参数信息,我们可以通过 switch 或者 if else 来解析参数,也可以使用更好的库 commander.js

npm.io

commander.js 是一个轻巧的nodejs模块,提供了用户命令行输入和参数解析强大功能。commander.js 是从 Ruby同名项目移植过来的。详细内容可以参考 commander.js 文档

2.1 调整 src/bin.js

src/bin.js

#!/usr/bin/env node

const { program } = require('commander')
const version = require('../package.json').version

program.version(version, '-v, --version')

program
  .usage('<command> [options]') // 用户使用提示
  .command('create') // 如果没有action 会在同目录下找x-init文件执行
  .description('创建一个工程')
  .action(async () => {
    console.info('----done----')
  })

program.parseAsync(process.argv)

2.2 测试

terminal 中输入

fcli

fcli create

查看结果:

npm.io

示例工程

示例工程: examples/02

3 获得用户信息

3.1 创建 prompts.js 文件

我们使用 inquirer 来帮助我们以问询的方式获得用户输入的信息。

我们设置项目名称、版本号、描述信息、模板、包管理工具等问题,获得一个 JSON 格式的结果。

src/prompts.js

const fse = require('fs-extra')
const inquirer = require('inquirer')
const cwd = process.cwd()

const templates = require('../config/default.json').templates
const choices = Object.keys(templates).map((type) => ({
  name: type,
  value: type
}))

// 设置交互问题
const projectNameQuestion = {
  type: 'input',
  message: `请输入项目名称(英文):`,
  name: 'name',
  validate: function (name) {
    if (!/^[a-zA-Z_][\w_\-]+$/g.test(name)) {
      return '请输入正确的项目名称!'
    }
    if (fse.existsSync(`${cwd}/${name}`)) {
      return '该文件夹已存在!'
    }
    return true
  }
}

const versionQuestion = {
  type: 'input',
  message: `Version: `,
  name: 'version',
  default: '0.1.0',
  validate: function (val) {
    if (/^((\d){1,3}\.(\d){1,3}\.(\d){1,3})$/g.test(val)) {
      // 校验位数
      return true
    }
    return '请输入正确的格式[数字.数字.数字],例如:11.22.33'
  }
}

const descriptionQuestion = {
  type: 'input',
  message: `请输入项目描述: `,
  name: 'description'
}

const templateQuestion = {
  type: 'list',
  message: `请选择要下载的模板: `,
  name: 'template',
  choices: [...choices]
}

const cliQuestion = {
  type: 'list',
  message: `请选择包管理工具(如果是公司的项目,必须使用yarn!): `,
  name: 'cli',
  choices: [
    {
      name: 'yarn',
      value: 'yarn'
    },
    {
      name: 'npm',
      value: 'npm'
    },
    {
      name: 'cnpm',
      value: 'cnpm'
    }
  ]
}

async function getUserInputs() {
  const answers = await inquirer.prompt([
    projectNameQuestion,
    versionQuestion,
    descriptionQuestion,
    templateQuestion,
    cliQuestion
  ])
  return {
    ...answers,
    projectPath: `${cwd}/${answers.name}`
  }
}

module.exports = getUserInputs

3.2 测试

为了方便,我们直接在 src/prompts.js 最后一行调用 getUserInputs() 方法。

// ...

const answers = getUserInputs()
console.info(answers)

node 运行 src/prompts.js ,查看结果

npm.io

4 下载模板工程

4.1 准备

我们使用以下插件来提升下载过程中的用户体验:

download-git-repo: 下载git模板

chalk: 给客户端输出的文字添加颜色

npm.io

ora 显示进度条

npm.io

symbols 给log添加图标

npm.io

4.2 创建 src/download-template.js 文件

我们在 src/download-template.js 中,主要实现是根据模板的 git 地址,使用 download-git-repo 库下载模板工程。

为了避免 回调地狱,我们返回一个 Promise 对象。

src/download-template.js

const fse = require('fs-extra')
const downloadTemplate = require('download-git-repo')
const chalk = require('chalk')
const ora = require('ora')
const symbols = require('log-symbols')

// 模板的下载地址
const templateGitUrls = require('../config/default.json').templates

async function downloadTemplates(options) {
  return new Promise((resolve, reject) => {
    // 1. 删除与项目同名的文件夹
    if (fse.existsSync(options.projectPath)) {
      const fse = require('fs-extra')
      fse.removeSync(options.projectPath)
    }
    // 2. 下载模板
    const spinner = ora(`下载 ${options.template} 模板 中...`).start()
    downloadTemplate(
      `direct:${templateGitUrls[options.template].url}`,
      options.name,
      { clone: true },
      (err) => {
        // 失败的话,结束进程
        if (err) {
          spinner.fail()
          console.error(
            symbols.error,
            chalk.red(`${err} \n下载模板失败,可能是网络问题...`)
          )
          reject(err)
          process.exit(1)
        }
        // 成功的话,返回 options
        spinner.succeed('下载成功!')
        resolve(options)
      }
    )
  })
}

module.exports = downloadTemplates

// const name = 'hello-world'
// downloadTemplates({
//   projectPath: process.cwd() + '/' + name,
//   name,
//   template: 'web'
// })

测试

我们在最下面一行添加

// ...

const name = 'hello-world'
downloadTemplates({
  projectPath: process.cwd() + '/' + name,
  name,
  template: 'web'
})

node 运行 src/download-template.js ,查看结果

npm.io

5. 修改文件

创建 update-files.js 文件

我们借助于 fs-extraJSON 的格式读取 package.json,修改完后,再以 JSON 的形式保存。

src/update-files.js

const fse = require('fs-extra')

function updateFiles(options) {
  return new Promise((resolve, reject) => {
    // 更新 package.json
    updatePackageJson(options)
    resolve()
  })
}

function updatePackageJson(options) {
  const packageJson = `${options.projectPath}/package.json`
  const json = fse.readJsonSync(packageJson)
  json.name = options.name
  json.description = options.description
  json.version = options.version
  fse.outputJsonSync(packageJson, json, { spaces: 2 })
}

module.exports = updateFiles

测试

我们在最下面一行添加

// ...

updateFiles({
  projectPath: process.cwd() + '/' + 'hello-world',
  name: 'hello-world',
  description: 'description',
  version: '1.1.1'
})

node 运行 src/update-files.js ,查看结果

npm.io

6. 安装工程中的依赖

6.1 创建 src/install-dependencies.js

这一步比较简单,我们借助 cross-spawn 在下载的工程中执行yarn || npm脚本。

src/install-dependencies.js

const spawn = require('cross-spawn')
const chalk = require('chalk')
const symbols = require('log-symbols')

function install(options) {
  const cwd = options.projectPath || process.cwd()
  console.info(symbols.info, '工程目录:' + cwd)
  return new Promise((resolve, reject) => {
    const command = options.cli || 'npm'
    const args = ['install', '--save', '--save-exact', '--loglevel', 'error']
    // 在下载的目录下执行脚本 yarn || npm 脚本
    const child = spawn(command, args, {
      cwd,
      stdio: ['pipe', process.stdout, process.stderr]
    })

    // 脚本执行完进行回调,返回一个promise
    child.once('close', (code) => {
      if (code !== 0) {
        console.error(symbols.error, chalk.red(`安装依赖失败... 请耗子尾汁...`))
        reject({
          command: `${command} ${args.join(' ')}`
        })
        return
      }
      resolve()
    })
    child.once('error', reject)
  })
}

module.exports = install

6.2 测试

我们在最下面一行添加

// ...

install({
  projectPath: process.cwd() + '/' + 'hello-world',
  cli: 'yarn'
})

node 运行 src/update-files.js ,查看结果

npm.io

7. 调整 src/bin.js 文件

最后,我们在 src/bin.js 中引用 2、3、4、5、6 步中的文件完成整个流程。

#!/usr/bin/env node

const { program } = require('commander')
const version = require('../package.json').version
const symbols = require('log-symbols')

// 获得用户的输入内容
const getUserInputs = require('./prompts.js')
const downloadTemplate = require('./download-template')
const updateFilesWithUserInputs = require('./update-files')
const installDependencies = require('./install-dependencies')

program.version(version, '-v, --version')

program
  .usage('<command> [options]') // 用户使用提示
  .command('create') // 如果没有action 会在同目录下找x-init文件执行
  .description('创建一个工程')
  .action(async () => {
    try {
      // 1. 获得用户在 terminal 上输入的数据 options: { ... }
      const options = await getUserInputs()
      // 2. 从 gitlab 上下载代码并保存为一个文件夹
      await downloadTemplate(options)
      // 3. 把 options 中的内容复写到下载的文件中
      await updateFilesWithUserInputs(options)
      // 4. 执行 (yarn||npm) install 命令下载依赖
      await installDependencies(options)
    } catch (e) {
      console.error(symbols.error, e)
    }
  })

program.parseAsync(process.argv)

8. 完整演示

terminal 中运行 fcli 命令

npm.io

最终生成的项目文件

npm.io

9. 示例工程

示例工程: examples/000

10. 发布到 npm

为了方便使用,我们可以把写好的脚本发布到 npm 上,推荐几篇文章大家可以参考:

推荐文档