乐趣区

关于前端:写个工作用的脚手架cli用脚手架整合模板和配置

学习总结篇,以是否造轮子来掂量学习效果。本篇次要介绍近期应用的一个 cli 工具

脚手架 cli 的解决的问题

随着公司各端的业务进行,前端方面会积淀出一些通用的解决方案和模板。此时,对立保护和治理就十分有必要了。allen-cli就是基于这样的场景而诞生的。

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

应用示例

目前实现的性能为:

  1. 输出 allen init 命令抉择一个脚手架模版进行下载,而后创立对应的 app。
  2. 动静抉择构建环境,适配挪动端等不同状况。

allen-cli 的具体流程

我的项目的整体构造:

1. 创立我的项目

npm init创立 package.json, 次要加上bin 命令

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

2. 解析参数

一个 CLI 须要通过命令行输出各种参数,能够间接用 nodejs 的 process 相干 api 进行解析,然而更举荐应用 commander 这个 npm 包能够大大简化解析的过程。

#!/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-init

// 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 创立文件下载模板

创立文件和抉择模板

// 创立门路
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-repo

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 的这个库

// 常见的交互式命令行用户接口的汇合
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 包传送门:https://www.npmjs.com/package/allen-cli
  • github 传送门: 我的项目脚手架 allen-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 变量复原原样。

参考链接

  • 阮一峰的网络日志 -npm scripts 使用指南
  • vue-cli
  • http://nodejs.cn/api/
退出移动版