【写个工做用的脚手架cli】用脚手架整合模板和配置

学习总结篇,以可否造轮子来衡量学习效果。本篇主要介绍近期使用的一个cli工具

脚手架cli的解决的问题

随着公司各端的业务进行,前端方面会沉淀出一些通用的解决方案和模板。此时,统一维护和管理就很是有必要了。allen-cli就是基于这样的场景而诞生的。javascript

这个项目脚手架,最终实现:整合各个模板,一键生成模板html

使用示例

目前实现的功能为:前端

  1. 输入allen init命令选择一个脚手架模版进行下载,而后建立对应的app。
  2. 动态选择构建环境,适配移动端等不一样状况。

allen-cli的具体流程

项目的总体结构:
vue

1. 建立项目

npm init建立package.json, 主要加上bin命令java

{
  "bin": {
    "allen": "bin/allen",
    "allen-init": "bin/allen-init"
  },
}

2. 解析参数

一个CLI须要经过命令行输入各类参数,能够直接用nodejs的process相关api进行解析,可是更推荐使用commander这个npm包能够大大简化解析的过程。node

#!/usr/bin/env node
const program = require('commander')

console.log('version', require('../package').version)

program
  .version(require('../package').version)
    .usage('<command> [项目名称]')
    .command('init', '建立新项目')
    .parse(process.argv)

3. main主体流程

allen-initgit

// NODE moudle
//  node.js 命令行解决方案
const program = require("commander");

// node.js path模块
const path = require("path");

// node.js fs模块
const fs = require("fs");

// 常见的交互式命令行用户接口的集合
const inquirer = require("inquirer");

// 使用shell模式匹配文件
const glob = require("glob");

// 活动最新的npm包
const latestVersion = require("latest-version");

// node.js 子进程
const spawn = require("child_process").spawn;

// node.js 命令行环境的 loading效果, 和显示各类状态的图标
const ora = require("ora");

// The UNIX command rm -rf for node.
const rm = require("rimraf").sync;

async function main() {
  let projectRoot, templateName
  try {
    // 检测版本
    let isUpate = await checkVersion();
    // 更新版本
    if (isUpate) await updateCli();
    // 检测路径
    projectRoot = await checkDir();
    // 建立路径
    makeDir(projectRoot)
    // 选择模板
    let { git } = await selectTemplate();
    // 下载模板
    templateName = await dowload(rootName, git);
    // 本地配置
    let customizePrompt = await getCustomizePrompt(templateName, CONST.CUSTOMIZE_PROMPT)
    // 渲染本地配置
    await render(projectRoot, templateName, customizePrompt);
    // 删除无用文件
    deleteCusomizePrompt(projectRoot)
    // 构建结束
    afterBuild();
  } catch (err) {
    log.error(`建立失败:${err.message}`)
    afterError(projectRoot, templateName)
  }
}

3.1 建立文件下载模板

建立文件和选择模板github

// 建立路径
function makeDir (projectRoot) {
  if (projectRoot !== ".") {
    fs.mkdirSync(projectName);
  }
}
/**
 * 模板选择
 */
function selectTemplate() {
  return new Promise((resolve, reject) => {
    let choices = Object.values(templateConfig).map(item => {
      return {
        name: item.name,
        value: item.value
      };
    });
    let config = {
      // type: 'checkbox',
      type: "list",
      message: "请选择建立项目类型",
      name: "select",
      choices: [new inquirer.Separator("模板类型"), ...choices]
    };
    inquirer.prompt(config).then(data => {
      let { select } = data;
      let { value, git } = templateConfig[select];
      resolve({
        git,
        // templateValue: value
      });
    });
  });
}

下载模板, 用的是download-git-repovue-cli

const download = require('download-git-repo')
const path = require('path')
const ora = require('ora')
const logSymbols = require("log-symbols");
const chalk = require("chalk");
const CONST = require('../conf/const')
module.exports = function (target, url) {
  const spinner = ora(`正在下载项目模板,源地址:${url}`)
  target = path.join(CONST.TEMPLATE_NAME)
  spinner.start()
  return new Promise((resolve,reject) => {
    download(`direct:${url}`,
    target, { clone: true }, (err) => {
      if (err) {
        spinner.fail()
        console.log(logSymbols.fail, chalk.red("模板下载失败:("));
        reject(err)
      } else {
        spinner.succeed()
        console.log(logSymbols.success, chalk.green("模板下载完毕:)"));
        resolve(target)
      }
    })
  })
}

3.2 界面交互配置

采用的是inquirer的这个库shell

// 常见的交互式命令行用户接口的集合
const inquirer = require("inquirer");

3.3 本地配置

若是须要将一些配置放在本地文件,则能够建立一些本地配置

/**
 * 
 * @param target 模板路径
 * @param fileName 读取文件名
 */
function getCustomizePrompt (target, fileName) {
  return new Promise ((resolve) => {
    const filePath = path.join(process.cwd(), target, fileName)
    if(fs.existsSync(filePath)) {
      console.log('读取模板配置文件')
      let file = require(filePath)
      resolve(file)
    } else {
      console.log('该文件没有配置文件')
      resolve([])
    }
  })
}

template.json

{
    type: "confirm",
    name: "mobile",
    message: "是否用于移动端?"
  },
  {
    type: "confirm",
    name: "flexible",
    message: "是否使用移动端适配?",
    when: function (answers) {
      return answers.mobile
    }
  },

4. 涉及到的node.js操做

// NODE moudle
//  node.js 命令行解决方案
const program = require("commander");

// node.js path模块
const path = require("path");

// node.js fs模块
const fs = require("fs");

// 常见的交互式命令行用户接口的集合
const inquirer = require("inquirer");

// 使用shell模式匹配文件
const glob = require("glob");

// 活动最新的npm包
const latestVersion = require("latest-version");

// node.js 子进程
const spawn = require("child_process").spawn;

// node.js 命令行环境的 loading效果, 和显示各类状态的图标
const ora = require("ora");

// The UNIX command rm -rf for node.
const rm = require("rimraf").sync;

5. 本地安装使用

在项目目录下运行npm i -g,注册全局命令allen-cli便可使用

C:\Users\XX\AppData\Roaming\npm目录下会生成相应的可执行文件:

6. npm包allen-cli

一个基本的脚手架CLI就完成了。

欢迎试用:npm i -g allen-cli

相关解析

!/usr/bin/env node

使用过Linux或者Unix的开发者,对于Shebang应该不陌生,它是一个符号的名称,#!。这个符号一般在Unix系统的基本中第一行开头中出现,用于指明这个脚本文件的解释程序。了解了Shebang以后就能够理解,增长这一行是为了指定用node执行脚本文件

当你输入一个命令的时候,npm是如何识别并执行对应的文件的呢?

具体的原理阮一峰大神已经在npm scripts 使用指南中介绍过。简单的理解:

就是输入命令后,会有在一个新建的shell中执行指定的脚本,在执行这个脚本的时候,咱们须要来指定这个脚本的解释程序是node。
在一些状况下,即便你增长了这一行,但仍是可能会碰到一下错误,这是为何呢?

No such file or directory

为了解决这个问题,首先须要了解一下/usr/bin/env。咱们已经知道,Shebang是为了指定脚本的解释程序,但是不一样用户或者不一样的脚本解释器有可能安装在不一样的目录下,系统如何知道要去哪里找你的解释程序呢?

/usr/bin/env就是告诉系统能够在PATH目录中查找。

因此配置#!/usr/bin/env node, 就是解决了不一样的用户node路径不一样的问题,可让系统动态的去查找node来执行你的脚本文件。
看到这里你应该理解,为何会出现No such file or directory的错误?由于你的node安装路径没有添加到系统的PATH中。因此去进行node环境变量配置就能够了。

NPM 执行脚本的原理

npm 脚本的原理很是简单。每当执行npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。所以,只要是 Shell(通常是 Bash)能够运行的命令,就能够写在 npm 脚本里面。

比较特别的是,npm run新建的这个 Shell,会将当前目录的node_modules/.bin子目录加入PATH变量,执行结束后,再将PATH变量恢复原样。

参考连接

相关文章
相关标签/搜索