搭建cli命令行脚手架

文章目录

    • node知识
    • 常用到的几个库
      • commander命令行界面
      • yargs处理命令行参数
      • inquirer
      • shelljs
      • ora加载工具
      • chalk和tracer颜色工具
      • download-git-repo和git-clone下载远程模板
      • Handlebars模板引擎
      • metalsmith读取所有文件,实现模板渲染
      • log-symbols显示出 √ 或 × 等的图标
      • ini 格式转换
    • 简单的cli
    • 实现类似vue-cli脚手架
    • 发布npm包
    • 使用typescript编写发布包代码
    • 扩展知识

CLI(Command-line interface)命令行工具。常用的git 、npm、vim、node脚本、shell脚本等都是cli工具,比如我们可以通过 git clone 等命令简单把远程代码复制到本地。可以用来制作命令行脚手架工具vue-cli、create-reate-app、angular-cli、gulp-cli等在本地快速构建开发项目。
帮你减少做重复性工作的工具,比如创建项目、打包构建上传OSS

解决痛点:

  • 项目配置繁琐、耗时,重复无意义的工作
  • 项目结构不统一、不规范
  • 前端项目类型繁多,不同项目不同配置,管理成本高
  • 脚手架也可以是一套命令集,不只用来创建项目

node知识

process.argv 接收命令行参数

// 输入 s-cli init create-demo
process.argv
/* [
  'D:\\Program Files\\nodejs\\node.exe',
  'C:\\Users\\USER\\AppData\\Roaming\\npm\\node_modules\\s-cli\\bin\\cli',
  'init',
  'create-demo'
] */

process.argv.slice(2) // [ 'init', 'create-name' ] 不带s-cli时

常用到的几个库

commander命令行界面

用来编写指令和处理命令行参数的(参数解析 --help其实就借助了他)

const program = require("commander");
// 定义指令
program
  .version('0.0.1')
  .command('init ')
  .description('Generate a new project from a template')
  .action(name => {  // 回调函数
    console.log('init' + name);
  })
  .command('create ', 'Generate a new project from a template') // 这种写法相当于把文件分离出去,执行 s-cli create demo时,找cli-create.js文件
// 解析命令行参数
program.parse(process.argv);

// 测试
s-cli -V 
s-cli init demo

官方文档 使用参考

yargs处理命令行参数

类似commander,还有minimist库,都是处理命令行参数

var argv = require('yargs').argv;
console.log('hello ', argv.name);

// 测试
s-cli --name=xiaoming    // hello  xiaoming

inquirer

输入交互库,就是你在创建项目的时候,输入项目名,项目描述等等

const inquirer = require('inquirer');
const promptList = [{
  type: "checkbox",
  message: "选择颜色:",
  name: "color",
  choices: [
      {
          name: "red"
      },
      {
          name: "blur",
          checked: true // 默认选中
      },
      {
          name: "green"
      },
      {
          name: "yellow"
      }
  ]
}];
inquirer.prompt(promptList).then(answers => {
  console.log(answers); // 返回的结果
})

// 测试
s-cli

参考

shelljs

-f:强制删除文件;
-i:删除之前先询问用户;
-r:递归处理目录;
-v:显示处理过程;
echo:在控制台输出指定内容
exit(code):以退出码为code退出当前进程

const shell = require('shelljs')
shell.rm('-rf','out/Release');//强制递归删除out/Release目录
shell.cp('-r','stuff/','out/Release');//将`stuff/`中所有内容拷贝至`out/Release`目录
shell.mv(`./ssr-with-${language}`, `./${option.appName}`) // 把移到文件

使用参考

ora加载工具

显示loading动画

const ora = require('ora')('正在构建');
ora.start();
setTimeout(function(){
    ora.succeed('构建成功');
}, 1000)

chalk和tracer颜色工具

用来修改控制台输出内容样式的,比如颜色

  • chalk
const chalk = require('chalk')
console.log(chalk.green('Project initialization finished!')) // 绿色字
console.log(chalk.red('The project already exists')) // 红色字
  • tracer
const log = require("tracer").colorConsole()
log.info("模板已建立");
log.error("请填写正确的项目名称!");

download-git-repo和git-clone下载远程模板

  • download-git-repo用来下载远程模板的,支持GitHub、 GitLab 和 Bitbucket等
const { promisify } = require('util')
const downloadWithPromise = promisify(require('download-git-repo'))
downloadWithPromise('github:sayid760/koa-react-ssr#master/example/react-ssr-js', path.resolve(__dirname, '../demo'));
  • git-clone拉取git文件
const clone = require("git-clone")
var url = "https://github.com/sayid760/vue3-vite-template.git"
clone(url, process.cwd()+'/demo', null, function(){
  console.log('拉取完成')
})

扩展:

Github API 接口

// RC 配置下载模板的地方,给 github 的 api 使用
https://api.github.com/users/xxx/repos // https://api.github.com/${type}/${registry}/repos
curl -u name:secret https://api.github.com/user/repos
https://api.github.com/users/xxx // xxx的用户信息,包含粉丝数
https://api.github.com/users/xxx/keys  // xxx的 ssh key
https://api.github.com/users/xxx/gpg_keys // xxx的gpg
https://api.github.com/users/xxx/followers?page=2 // xxx的粉丝
https://api.github.com/users/xxx/following?page=2 // xxx关注谁
https://api.github.com/orgs/bitcoin bitcoin项目的信息
https://api.github.com/orgs/bitcoin/repos bitcoin项目的repos
https://api.github.com/repos/bitcoin/bitcoin
https://api.github.com/repos/bitcoin/bitcoin/contributors bitcoin的贡献者

官方文档
Github API 调用
如何将Github API与Node.js服务器连接

Handlebars模板引擎

const Handlebars = require('handlebars')
var template = Handlebars.compile("Handlebars {{doesWhat}}");
console.log(template({ doesWhat: "rocks!" }));  // 进行编译并输出

metalsmith读取所有文件,实现模板渲染

/*
思路:
- 将文件内容提取出来,转换为字符串
- 使用 handlebar 将模板内容字符串替换成用户输入的值
- Metalsmith build 成最终文件
- 删除下载下来的模板文件
*/
// down/index.js
'Handlebars {{doesWhat}}'

// main.js
const MetalSmith = require('metalsmith'); 
const Handlebars = require('handlebars')
const rm = require('rimraf').sync
const path = require('path')

MetalSmith(path.join(__dirname, './down/index.js')) // 遍历文件夹
  .metadata({doesWhat: "rocks!"}) //metadata 为用户输入的内容
  .source(path.join(__dirname, './down')) // 模板文件 path
  .destination(path.join(__dirname, './new')) // 最终编译好的文件存放位置
  .use((files, metalsmith, done) => {
    Object.keys(files).forEach(fileName => {
      const fileContentsString = files[fileName].contents.toString() //Handlebar compile 前需要转换为字符创
      files[fileName].contents = Handlebars.compile(fileContentsString)(metalsmith.metadata())
    })
    done()
  })
.build(function(err) { // 打包
  rm(path.join(__dirname, './down'))  // 删除模板文件
  if (err) throw err;
  console.log('Build finished!');
})

log-symbols显示出 √ 或 × 等的图标

const symbol = require('log-symbols')
console.log(symbol.success)  // √
console.log(symbol.error)  // ×

ini 格式转换

// index.js
const fs = require('fs'), { decode, encode } = require('ini')
const config = decode.parse(fs.readFileSync('./config.ini', 'utf-8'))
/*
{
  '': true,
  '',
  '': true,
  '',
  '',
  'Document': true,
  '': true,
  '': true,
  '

ddddddddddd

': true, '': true, '': true } */
// 测试 node index.js

简单的cli

// package.json
"bin": {
    "m-cli": "./main.js"
}
  
// main.js
#!/usr/bin/env node

const clone = require("git-clone")
const program = require("commander")
const shell = require("shelljs")
const log = require("tracer").colorConsole()

program
  .version("0.0.1")
  .description("vue项目基础模板")
  .program.command("create  [template]")
  .option("-tsx, --typescript", "typescript tsx template")
  .action(function (project, template, cmd) {
    var ts_tpl = !!cmd.typescript;
    log.info(`create ${project} ${ts_tpl ? "--tsx" : ""}`);
    if (project) {
      var pwd = shell.pwd();
      var url = "https://github.com/xxx/width-js-template.git";
      if (ts_tpl)
        url = "https://github.com/xxx/width-ts-template.git";
      log.info("正在拉取远端模板,请稍等..");
      clone(url, pwd + `/${project}`, null, function () {
        shell.rm("-rf", pwd + `/${project}/.git`);
        log.info("模板已建立");
      });
    } else {
      log.error("请填写正确的项目名称!");
    }
  });

program.parse(process.argv);

// 输入
npm link
m-cli create vuedemo -tsx

扩展:npm link把s-cli注册到全局,然后链接到工程目录,可以直接去执行(把当前目录放到当前环境变量)

C:\Users\USER\AppData\Roaming\npm\s-cli -> C:\Users\USER\AppData\Roaming\npm\node_modules\s-cli\bin\www
C:\Users\USER\AppData\Roaming\npm\node_modules\s-cli -> C:\cli-template\s-cli

实现类似vue-cli脚手架

思路:

  • init:拉取模板 (saycli init)
  • dev:启动webpack及热更新 (saycli dev)
  • build:打包(saycli build)
// 目录
├── bin
│   └── cli              // 可执行文件
└── src
 	├── cache.js       	 // 拉取模板
 	├── check.js       	 // 创建文件名时判断是否已存在
    ├── clientRender.js  // 处理dev和build的应用
    ├── config.js        // 应用配置环节
    ├── init.js          // init command
    ├── main.js        	 // 入口文件
    └── utils
        ├── index.js      
        └── webpack.js        
├── package.json
├── README.md
// 1. 初始化
say-cli npm init -y

// 2. 创建bin/cli,输入
#! /usr/bin/env  node  // 告诉当前代码需要使用node环境来执行
require('../src/main.js'); // 一个可执行的文件

// src/main.js
console.log('hello')   

// 3. package.json添加
"bin": {
	"saycli": "./bin/cli"  // 执行s-cli这个命令就会帮我执行这个文件
}
// 4. 输入npm link,把命令链接到全局下(解除 npm unlink)
npm link 

// 5. 测试,会打印hello
saycli
// src/main.js
const yargs = require('yargs')
const {init} = require('./init')
const {processError} = require('./util')

/* 配置交互命令:输入不同的命令实现不同的内容
	saycli init:输入名字,选择vue2或vue3,js或ts,根据不同的选择拉取相应模板
	saycli dev:读取webpack.config.js的内容当作参数传给webpack,启动一个webpack,
				使用webpack-dev-server把webpack编译后文件会输出到内存中
	saycli build:使用webpack来打包 
*/
yargs
  .command('init [appName]', 'init the program', {}, async (argv) => {
    const option = {
      appName: argv.appName || 'app',
      language: 'javascript',
      vueVersion: 'vue2'
    }
    try {
      await init(option)
    } catch (error) {
      processError(error)
    }
  })
  .command('dev', 'start clientRender', {}, async () => {
    process.env.NODE_ENV = 'development'
    const { dev } = require('./clientRender')
    await dev(yargs.argv)
  })
  .command('build', 'start clientBuild', {}, async () => {
    process.env.NODE_ENV = 'production'
    const { build } = require('./clientRender')
    await build()
  })
  .demandCommand(1, 'You need at least one command before moving on')
  .option('version', {
    alias: 'v',
    default: false
  })
  .parse()
// init:判断是否已存在相同文件名,选择应用版本,到github拉取模板
const answers = await inquirer.prompt([{
    type: 'input',
    message: '应用名称:',
    name: 'appName',
    default: option.appName
  }, {
    type: 'list',
    message: 'vue版本',
    name: 'vueVersion',
    default: 'vue2',
    choices: [
      'vue2',
      'vue3'
    ]
  },
  {
    type: 'list',
    message: '开发语言',
    name: 'language',
    default: 'javascript',
    choices: [
      'javascript',
      'typescript'
    ],
    when: function(answers) { // 当vueVersion为vue2的时候才会提问当前问题
      return answers.vueVersion == 'vue2'
    }
  }])
// dev:使用webpack-dev-server把打包出来的内容输出到内存中,轮询刷新内容
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const clientConfig = require(resolve(cwd, baseDir, './build/webpack.config'))
const compiler = webpack(clientConfig)
let server = new WebpackDevServer(compiler)
 server.listen(8080)
// build:启动webpack打包
const { promisify } = require('util') 
const webpack = require('webpack') 
const webpackWithPromise = promisify(webpack)
const clientConfig = require(resolve(cwd, baseDir, './build/webpack.config'))
const stats = await webpackWithPromise(clientConfig)

github代码参考

发布npm包

npm addUser   // 没有注册、有登录
npm publish    // 发包、更新
npm unpublish --force  // 卸包
/*	发布一个简单的包,初始化项目
	mkdir npm-util
	cd mkdir
	npm inti -y
	// index.js
	module.exports = (message)=> message.toUpperCase()
*/

// 发布npm
1. 在npm官网注册一个账号,https://www.npmjs.com/signup
2. 进入需要发布的包下
3. npm login  // 命令行中登录账号,需要输入用户名、密码、邮箱
4. npm publish


// 使用npm包
npm i npm-util -D

const uppercaser = require('npm-util');
console.log(uppercaser('hello world!'));

注意:发包前先去官网查看是否已有重复名的包,npm文档、包和模块

使用typescript编写发布包代码

npm i -g typescript
tsc --init // 生成tsconfig.json

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES6", 
    "module": "commonjs",                     
    "declaration": true  // 表示将要生成d.ts类型声明文件
  }
}

// 编译ts文件生成成js、d.ts的类型声明文件
tsc  index.ts

在package.json中声明类型文件位置:

// package.json,加入types字段
{
  "name": "@penggy/mysql",
  "version": "1.0.6",
  "description": "Promise wrapper of mysql",
  "main": "./index.js", 
  "types": "./index.d.ts", // 声明类型文件位置
  "scripts": {
    "test": "node test.js"
  },
  "files": [ // files 字段指定工程目录下面哪些文件需要发布到npm
    "index.js",
    "index.d.ts",
    "README.md"
  ],

努力填坑中…

扩展知识

  1. npm发布什么版本
npm 包的版本号一般都是 x.y.z 的形式(开发包一般用 x.y.z-a)。

其中 x 表示主版本号,通常有重大改变或者达到里程碑才改变;
y 表示次要版本号,或二级版本号,在保证主体功能基本不变的情况下,如果适当增加了新功能可以更新此版本号;
z 表示尾版本号或者补丁号,一些小范围的修修补补就可以更新补丁号。
第一版本通常是 0.0.1 或者 1.0.0,当修改了代码,需要更新版本号重新发布到 npm,不知道的小伙伴肯定会手动修改 package.json 的 version 字段,而高级的玩法是直接使用 npm version <update_type> 命令自动搞定。
版本格式:主版本号.次版本号.修订号(先行版本号及版本编译元数据可以加到它的后面,作为延伸)
 - 主版本号:当你做了不兼容的 API 修改
 - 次版本号:当你做了向下兼容的功能性新增
 - 修订号:当你做了向下兼容的问题修正

先行版本:
alpha: 内部版本
beta: 公测版本
rc: 即Release candiate,正式版本的候选版本

2. 
// 当前版本 1.0.0
"version": "1.0.0"
// 升级大版本好
npm version major
// 执行后当前结果
"version": "2.0.0"

你可能感兴趣的:(js)