前言
其实在实际开发中,可能也会遇到,就是创建一个新的项目时。可能一些初始化,以及技术栈,和现有的项目的基本是一样的。所有这些频繁重复性的工作,或许可以做成一个脚手架类似于vue-cli。
制作流程
一项目初始化
1.新建一个项目 pro-init-cli,初始化项目npm init --y生成package.json,安装一些依赖commander,git-clone, open,shelljs,如:
{
"name": "pro-init-cli",
"version": "1.0.0",
"description": "制作一个类似于vue-cli的脚手架",
"repository": true,
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^6.0.0",
"git-clone": "^0.1.0",
"open": "^8.0.6",
"shelljs": "^0.8.4"
},
"devDependencies": {
"chalk": "4.1.2"
}
}
- 新建
index.js文件编写测试代码,并且修改pack.json文件,全局link指令,如。
- package.json,新增指令参数
"bin": {
"pro-init-cli": "./index.js"
},
- index.js
#! /usr/bin/env node
console.log('测试')
第一行是必须添加的,是指定这里用node解析这个脚本。默认找/usr/bin目录下,如果找不到去系统环境变量查找。
这时我们cmd输入pro-init-cli命令,你会发现报错了:
为什么找不到test命令呢?让我们先来理解下为啥我们安装了vue-cli之后,输入vue就不报错呢?带着疑问打开我们npm全局安装的文件目录:
link到全局:项目根目录下执行如下命令
npm link,执行成功之后我们再查看我们全局npm的目录,这个时候我们会发现,多了几个项,细心地你肯定会发现,多出来的文件命名就是我们在package.json中配置的bin键
- 全局link
npm link bin配合npm link,生成命令文件- 指令测试
实现pro-init-cli业务代码
#! /usr/bin/env node
+ const program = require('commander')
+ const shell = require('shelljs')
+ const download = require('git-clone')
+ const open = require('open')
+ // 定义版本
+ program.version('1.0.0')
+ // 定义命令
+ program
+ .command('new <name>')
+ .description('创建项目')
+ .action(name => {
+ const gitUrl = 'https://github.com/vuejs/vue-next-webpack-preview.git'
+ download(gitUrl, `./${name}`, () => {
+ // 删除 .git 隐藏文件
+ shell.rm('-rf', `${name}/.git`)
+ shell.cd(name)
+ shell.exec('npm install')
+ // 成功后的提示
+ console.log(`
+ 创建项目:${name} 成功
+ pro-init-cli run 启动项目
+ `)
+ })
+ })
+ program
+ .command('run')
+ .description('运行项目')
+ .action(() => {
+ // 使用 shell 完成
+ shell.exec('npm run dev')
+ console.log(`
+ 项目启动成功
+ `)
+ })
+ // 解析命令行参数
+ program.parse(process.argv)
扩展
commander
-
cb build
上边配置的bin, key就是命令名称,而value就是对应的路径。
所以新建index.js:
javascript
复制代码
#!/usr/bin/env node
const program = require('commander')
program.version('1.0.0')
program.command('build').description('打包cli成功')
program.parse(process.argv)
此时第一个cli已经写好,本地安装一下: npm link
即可执行:cb 与 cb build
-
命令传参: cb param
假设需要接收外部的参数,供脚手架使用,可以借助action的options获取,代码如下:
#!/usr/bin/env node
const program = require('commander')
program.version('1.0.0')
const getParams = options => {
options || (options = [])
return options.reduce((prev, cur) => {
if (cur.includes('=')) {
const array = cur.split('=')
return { ...prev, [array[0]]: array[1] }
} else {
return prev
}
}, {})
}
program
.command('build')
.description('打包cli命令')
.action((appName, options) => {
// console.log(`打包cli成功, options`, options.template)
const params = getParams(options)
console.log(`打包cli成功, 对应的参数是`, params)
})
program.parse(process.argv)
-
读取配置文件: cb config
如果参数角度的情况下,命令传参已经无法满足。此时需要借助配置文件,如常见的vue.config.js等。教程如下:
新建cb.config.js
module.exports = {
name: '成功咯',
}
再新建脚本:
program
.command('config')
.description('读取配置文件')
.action((appName, options) => {
let config = {
path: 'svg',
}
const configPath = CWD + '\cb.config.js'
// 如果使用了配置文件,则以配置文件为准
if (existsSync(configPath)) {
const userConfig = require(resolve(CWD, 'cb.config.js'))
config = { ...config, ...userConfig }
console.log(`存在配置文件cb.config.js,获取到的名字为`, config.name)
} else {
console.log(`不存在配置文件`)
}
})
-
条件/判断: cb condition
cli少不了,让用户快速选择,快速输入的情况。 如vue-cli,需要让你输入项目名称,选择是否需要eslint等。
这一般都是借助inquirer实现。直接查看案例:
#!/usr/bin/env node
const CWD = process.cwd()
import commander from 'commander'
import fs from 'fs'
import inquirer from 'inquirer'
import path from 'path'
const { resolve } = path
const { program } = commander
program
.command('condition')
.description('选择属性')
.action((appName, options) => {
inquirer
.prompt([
{
type: 'confirm',
name: 'language',
message: '新建项目是否引入typescript?',
},
{
type: 'input',
name: 'desc',
message: '请输入项目备注',
},
])
.then(result => {
console.log('请输入', result)
})
})
program.parse(process.argv)
-
新建/拷贝页面: cb create
实际脚手架中,有很多使用到的页面。如vue-cli,他自带了许多页面。那如何新建一个页面呢?
直接看代码:
program
.command('create')
.description('创建index文件')
.action(async (appName, options) => {
const copyPath = `/template/demo.vue`
const pageName = 'new'
console.log(path.join(CWD, copyPath))
let template_content = await fs.readFile(path.join(CWD, copyPath))
template_content = template_content.toString()
const result = Mustache.render(template_content, {
pageName,
})
//开始创建文件
await fs.writeFile(path.join('./dist/', `${pageName}.vue`), result)
console.log('\n页面创建成功!\n')
})
program.parse(process.argv)
此时,执行cb create后,将产生新文件:
-
注入依赖: cb rely vue
我们还会经常遇到一个脚手架的方法,通常会帮你配置好package.json, 然后帮你npm install。这就是自动注入依赖。
我们需要借助execa来实现。
// 注入依赖
program
.version('0.1.0')
.command('rely <name>')
.description('新建一个项目注入依赖')
.action(name => {
create(name)
})
// create.js
const dir = process.cwd()
const execa = require('execa')
const fs = require('fs-extra')
const path = require('path')
async function create(projectPath) {
// package.json 文件内容
const packageObject = {
name: projectPath,
version: '0.1.0',
dependencies: {
vue: '^2.6.12',
},
devDependencies: {},
}
const packagePath = projectPath + '/package.json'
const filePath = path.join(dir, packagePath)
fs.ensureDirSync(path.dirname(filePath))
fs.writeFileSync(filePath, JSON.stringify(packageObject))
console.log('\n正在下载依赖...\n', filePath)
// // 下载依赖
execa('npm install', [], {
cwd: path.join(dir, projectPath),
stdio: ['inherit', 'pipe', 'inherit'],
})
console.log(`下载成功 ${projectPath}`)
console.log(`cd ${projectPath}`)
console.log(`npm run dev`)
}
module.exports = create
更多使用参考:commander
chalk
chalk包的作用是修改控制台中字符串的样式:例如字体样式,字体颜色,背景颜色等。
更多使用参考:chalk
log-symbols
为各种日志级别提供着色的符号(类似下载成功的√)
更多使用参考:log-symbols
ora
显示下载中,可以设置样式等;有start,fail,succeed方法等。
inquirer命令交互
这个库是我们可以和用户交互的工具;我们定义了两个问题:项目名称和版本号,create.js中写入以下代码:
const inquirer = require('inquirer')
getInquirer().then((res) => {
console.log(res)
})
function getInquirer() {
return inquirer.prompt([
{
name: 'projectName',
message: 'project name',
default: 'project',
},
{
name: 'projectVersion',
message: '项目版本号',
default: '1.0.0',
},
])
}
终端执行test create,上面的代码打印了执行后的拿到的参数如下图所示:
download-git-repo
该模块用于下载git上的模板项目,类似于vue-cli初始化一样,也是在git上下载一份代码回来本地完成开发环境搭建。下面代码封装了下载的代码,作用是根据传入的git地址克隆目标地址的代码到本地:
const ora = require('ora');
const chalk = require("chalk");
const logSymbols = require('log-symbols');
const download = require('download-git-repo');
const {
spawnSync
} = require("child_process");
module.exports = function (target, downLoadURL) {
let {
error
} = spawnSync("git", ["--version"]);
if (error) {
let downurl = downLoadURL.replace("direct:", "");
console.log(logSymbols.warning, chalk.yellow("未添加Git环境变量引起,添加Git与git管理库的环境变量即可;"))
console.log(logSymbols.info, chalk.green('或直接到模板地址下载:', downurl));
return Promise.reject(error);
}
return new Promise((resolve, reject) => {
const spinner = ora(`正在下载模板`)
spinner.start();
console.log(downLoadURL)
download(downLoadURL, target, {
clone: true
}, (err) => {
if (err) {
let errStr = err.toString()
spinner.fail();
reject(err);
if (errStr.includes("status 128")) {
console.log('\n', logSymbols.warning, chalk.yellow("Git默认开启了SSL验证,执行下面命令关闭后再重试即可;"))
console.log(logSymbols.info, chalk.green("git config --global http.sslVerify false"))
}
} else {
spinner.succeed();
resolve(target);
}
})
})
}
下载模板时校验输入的文件目录名:
function checkProjectName(projectName) {
let next = null;
if (list.length) {
if (
list.filter((name) => {
const fileName = path.resolve(process.cwd(), path.join(".", name));
const isDir = fs.statSync(fileName).isDirectory();
return name.indexOf(projectName) !== -1 && isDir;
}).length !== 0
) {
console.error(logSymbols.error, chalk.red(`项目${projectName}已经存在`));
return Promise.resolve(false);
}
next = Promise.resolve(projectName);
} else if (rootName === projectName) {
next = inquirer
.prompt([{
name: "buildInCurrent",
message: "当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目?",
type: "confirm",
default: true,
}, ])
.then((answer) => {
return Promise.resolve(answer.buildInCurrent ? "." : projectName);
});
} else {
next = Promise.resolve(projectName);
}
return next;
}
child_process
这是nodejs自带的一个包,用于开启子线程,非常强大,详情可以参考这里;这里利用spawnSync方法校验本地是否有git环境:
const { spawnSync } = require("child_process");
let { error } = spawnSync("git", ["--version"]);