【写个工作用的脚手架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 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变量恢复原样。

参考链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值