自用 脚手架 cxn-cli use vue-template & publish https://www.npmjs.com/package/cxn-cli - 续更


填坑之路:

[1] .cxnrc 文件: 在 C:\Users\Administrator.PC-20170224PLKZ 用户目录下

registry=xn213
type=users

[2] Error: Couldn't find preset "env" relative to directory "D:\\Acx\\xn-cli"

debugger: 安装依赖 babel-preset-env

[3] 发布: 需要登录npm: npm login, 完了使用 npm publish 发布到npm上

[4] 关于包名: 在 package.json"name": "cxn-cli", 发布的包名, 不能和npm上的重复, 重复会报错没有权限上传这个包, 因为是别人发布的,,,

版本: "version": "1.0.1", 升级一版根据相关规则修改,然后发布;

[5] 命令行创建项目命令: cxn init vue-template myvue 其中 vue-template 是要克隆的github上模板, myvue 是创建在本地的 自己的项目名字

撸一个自己的脚手架

脚手架

vue-cli, create-react-appreact-native-cli 等都是非常优秀的脚手架,通过脚手架,我们可以快速初始化一个项目,无需自己从零开始一步步配置,有效提升开发体验。尽管这些脚手架非常优秀,但是未必是符合我们的实际应用的,我们可以定制一个属于自己的脚手架(或公司通用脚手架),来提升自己的开发效率。

脚手架的作用

  • 减少重复性的工作,不需要复制其他项目再删除无关代码,或者从零创建一个项目和文件。
  • 可以根据交互动态生成项目结构和配置文件。
  • 多人协作更为方便,不需要把文件传来传去。

本项目完整代码请戳: github.com/xn213/

实现的功能

在开始之前,我们需要明确自己的脚手架需要哪些功能。vue init template-name project-namecreate-react-app project-name。我们这次编写的脚手架(cxn-cli)具备以下能力(脚手架的名字爱叫啥叫啥,我选用了cxn黎明女神):

  • cxn init template-name project-name 根据远程模板,初始化一个项目(远程模板可配置)
  • cxn config set <key> <value> 修改配置信息
  • cxn config get [<key>] 查看配置信息
  • cxn --version 查看当前版本号
  • cxn -h

大家可以自行扩展其它的 commander,本篇文章旨在教大家如何实现一个脚手架。

本项目完整代码请戳(建议先clone代码): github.com/xn213/

> 效果展示

初始化一个项目

修改.cxnrc文件,从 vue-template 下载模板

需要使用的第三方库

  • babel-cli/babel-env: 语法转换
  • commander: 命令行工具
  • download-git-repo: 用来下载远程模板
  • ini: 格式转换
  • inquirer: 交互式命令行工具
  • ora: 显示loading动画
  • chalk: 修改控制台输出内容样式
  • log-symbols: 显示出 √ 或 × 等的图标

关于这些第三方库的说明,可以直接npm上查看相应的说明,此处不一一展开。

初始化项目

创建一个空项目(cxn-cli),使用 npm init 进行初始化。

安装依赖

npm install babel-cli babel-env chalk commander download-git-repo ini inquirer log-symbols ora

目录结构

├── bin
│   └── www             // 可执行文件
├── dist
    ├── ...             // 生成文件
└── src
    ├── config.js       // 管理cxn配置文件
    ├── index.js        // 主流程入口文件
    ├── init.js         // init command
    ├── main.js         // 入口文件
    └── utils
        ├── constants.js // 定义常量
        ├── get.js       // 获取模板
        └── rc.js        // 配置文件
├── .babelrc             // babel配置文件
├── package.json
├── README.md

babel 配置

开发使用了ES6语法,使用 babel 进行转义,

.bablerc

{
  "presets": [
    [
      "env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

cxn 命令

node.js 内置了对命令行操作的支持,package.json 中的 bin 字段可以定义命令名和关联的执行文件。在 package.json 中添加 bin 字段

package.json

{
  "name": "cxn-cli",
  "version": "1.0.0",
  "description": "脚手架",
  "main": "index.js",
  "bin": {
    "cxn": "./bin/www"
  },
  "scripts": {
    "compile": "babel src -d dist",
    "watch": "npm run compile -- --watch"
  }
}

www 文件

行首加入一行 #! /usr/bin/env node 指定当前脚本由 node.js 进行解析

#! /usr/bin/env node
require('../dist/main.js');

链接到全局环境

开发过程中为了方便调试,在当前的 cxn-cli 目录下执行 npm link,将 cxn 命令链接到全局环境。

启动项目

npm run watch

处理命令行

利用 commander 来处理命令行。

main

import program from 'commander';
import { VERSION } from './utils/constants';
import apply from './index';
import chalk from 'chalk';

/**
 * cxn commands
 *    - config
 *    - init 
 */

let actionMap = {
  init: {
    description: 'generate a new project from a template',
    usages: [
      'cxn init templateName projectName'
    ]
  },
  config: {
    alias: 'cfg',
    description: 'config .cxnrc',
    usages: [
        'cxn config set <k> <v>',
        'cxn config get <k>',
        'cxn config remove <k>'
    ]
  },
  //other commands
}

// 添加 init / config 命令
Object.keys(actionMap).forEach((action) => {
  program.command(action)
  .description(actionMap[action].description)
  .alias(actionMap[action].alias) //别名
  .action(() => {
    switch (action) {
      case 'config': 
        //配置
        apply(action, ...process.argv.slice(3));
        break;
      case 'init':
        apply(action, ...process.argv.slice(3));
        break;
      default:
        break;
    }
  });
});

function help() {
  console.log('\r\nUsage:');
  Object.keys(actionMap).forEach((action) => {
    actionMap[action].usages.forEach(usage => {
      console.log('  - ' + usage);
    });
  });
  console.log('\r');
}
program.usage('<command> [options]');
// cxn -h 
program.on('-h', help);
program.on('--help', help);
// cxn -V   VERSION 为 package.json 中的版本号
program.version(VERSION, '-V --version').parse(process.argv);

// cxn 不带参数时
if (!process.argv.slice(2).length) {
  program.outputHelp(make_green);
}
function make_green(txt) {
  return chalk.green(txt); 
}

下载模板

download-git-repo 支持从 Github、Gitlab 下载远程仓库到本地。

get.js

import { getAll } from './rc';
import downloadGit from 'download-git-repo';

export const downloadLocal = async (templateName, projectName) => {
  let config = await getAll();
  let api = `${config.registry}/${templateName}`;
  return new Promise((resolve, reject) => {
    //projectName 为下载到的本地目录
    downloadGit(api, projectName, (err) => {
      if (err) {
        reject(err);
      }
      resolve();
    });
  });
}

init 命令

命令行交互

在用户执行 init 命令后,向用户提出问题,接收用户的输入并作出相应的处理。命令行交互利用 inquirer 来实现:

inquirer.prompt([
  {
    name: 'description',
    message: 'Please enter the project description: '
  },
  {
    name: 'author',
    message: 'Please enter the author name: '
  }
]).then((answer) => {
    //...
});

视觉美化

在用户输入之后,开始下载模板,这时候使用 ora 来提示用户正在下载模板,下载结束之后,也给出提示。

import ora from 'ora';
let loading = ora('downloading template ...');
loading.start();
//download
loading.succeed(); //或 loading.fail();

init.js

import { downloadLocal } from './utils/get';
import ora from 'ora';
import inquirer from 'inquirer';
import fs from 'fs';
import chalk from 'chalk';
import symbol from 'log-symbols';

let init = async (templateName, projectName) => {
  //项目不存在
  if (!fs.existsSync(projectName)) {
    //命令行交互
    inquirer.prompt([
      {
        name: 'description',
        message: 'Please enter the project description: '
      },
      {
        name: 'author',
        message: 'Please enter the author name: '
      }
    ]).then(async (answer) => {
      //下载模板 选择模板
      //通过配置文件,获取模板信息
      let loading = ora('downloading template ...');
      loading.start();
      downloadLocal(templateName, projectName).then(() => {
        loading.succeed();
        const fileName = `${projectName}/package.json`;
        if(fs.existsSync(fileName)){
          const data = fs.readFileSync(fileName).toString();
          let json = JSON.parse(data);
          json.name = projectName;
          json.author = answer.author;
          json.description = answer.description;
          //修改项目文件夹中 package.json 文件
          fs.writeFileSync(fileName, JSON.stringify(json, null, '\t'), 'utf-8');
          console.log(symbol.success, chalk.green('Project initialization finished!'));
        }
      }, () => {
        loading.fail();
      });
    });
  }else {
    //项目已经存在
    console.log(symbol.error, chalk.red('The project already exists'));
  }
}
module.exports = init;

config 配置

cxn config set registry vuejs-templates

config 配置,支持我们使用其它仓库的模板,例如,我们可以使用 vuejs-templates 中的仓库作为模板。这样有一个好处:更新模板无需重新发布脚手架,使用者无需重新安装,并且可以自由选择下载目标。

config.js

// 管理 .cxnrc 文件 (当前用户目录下)
import { get, set, getAll, remove } from './utils/rc';

let config = async (action, key, value) => {
  switch (action) {
    case 'get':
      if (key) {
        let result = await get(key);
        console.log(result);
      } else {
        let obj = await getAll();
        Object.keys(obj).forEach(key => {
          console.log(`${key}=${obj[key]}`);
        })
      }
      break;
    case 'set':
      set(key, value);
      break;
    case 'remove':
      remove(key);
      break;
    default:
      break;
  }
}

module.exports = config;

rc.js

.cxnrc 文件的增删改查

import { RC, DEFAULTS } from './constants';
import { decode, encode } from 'ini';
import { promisify } from 'util';
import chalk from 'chalk';
import fs from 'fs';

const exits = promisify(fs.exists);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

//RC 是配置文件
//DEFAULTS 是默认的配置
export const get = async (key) => {
  const exit = await exits(RC);
  let opts;
  if (exit) {
    opts = await readFile(RC, 'utf8');
    opts = decode(opts);
    return opts[key];
  }
  return '';
}

export const getAll = async () => {
  const exit = await exits(RC);
  let opts;
  if (exit) {
    opts = await readFile(RC, 'utf8');
    opts = decode(opts);
    return opts;
  }
  return {};
}

export const set = async (key, value) => {
  const exit = await exits(RC);
  let opts;
  if (exit) {
    opts = await readFile(RC, 'utf8');
    opts = decode(opts);
    if(!key) {
      console.log(chalk.red(chalk.bold('Error:')), chalk.red('key is required'));
      return;
    }
    if(!value) {
      console.log(chalk.red(chalk.bold('Error:')), chalk.red('value is required'));
      return;
    }
    Object.assign(opts, { [key]: value });
  } else {
      opts = Object.assign(DEFAULTS, { [key]: value });
  }
  await writeFile(RC, encode(opts), 'utf8');
}

export const remove = async (key) => {
  const exit = await exits(RC);
  let opts;
  if (exit) {
    opts = await readFile(RC, 'utf8');
    opts = decode(opts);
    delete opts[key];
    await writeFile(RC, encode(opts), 'utf8');
  }
}

发布

npm publish 将本脚手架发布至npm上。其它用户可以通过 npm install cxn-cli -g 全局安装。 即可使用 cxn 命令。

项目地址

本项目完整代码请戳: github.com/xn213/

编写本文,虽然花费了一定时间,但是在这个过程中,我也学习到了很多知识,谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的肯定是我前进的最大动力。 github.com/xn213/

参考文章:

[1] 珠峰架构课(墙裂推荐)

[2] npm依赖文档 (npmjs.com/package/download-git-repo)

[3] 原作者: 刘小夕的文章