前言
在前端开发中,我们所熟知的脚手架工具 vue 有 vue-cli,react 有 create-react-app,根据选择项来生成新项目模板。其模板满足于大多数小型的项目或个人项目,但对企业来说,官方提供的脚手架不够定制化,生成项目之后仍需要添加很多配置或需导入很多已成熟的模块,离生成即用还有一定的距离。
开发者可以通过定制自己的cli工具来进行处理,做一个简单的cli工具,集成自己预设的模块,根据模板来生成,后续只需维护脚手架的更新。
Demo目录
└───demo-mycli/.....................Demo目录
├───lib/............................lib目录
| └───core/...........................指令目录
│ └───create.js.......................create指令
│ └───utils/..........................工具目录
│ └───action.js.......................action代码
│ └───inquirer.js.....................inquirer代码
│ └───ora.js..........................ora代码
│ └───templateCreate.js...............templateCreate代码
│ └───terminal.js.....................terminal代码
├───template/.......................模板文件
│ └───src/
│ └───baseConfigComponents/
│ └───loginComponents/
│ └───queryComponent/
│ └───vite.config.js
├───components/.....................ejs文件目录
│ └───package.ejs.....................需写入的package文件
├───universe/.......................备份文件夹
├───index.js........................index文件(梦开始的地方)
└───package.json....................模块包配置文件
index.js
创建空文件夹demo-mycli,创建index.js文件,在首行写上
#!/usr/bin/env node
。
若是有使用过Linux或者Unix的前端开发者,对于Shebang应该不陌生,它是一个符号的名称,#!
。这个符号通常在Unix系统的基本中第一行开头中出现,用于指明这个脚本文件的解释程序。了解了Shebang之后就可以理解,增加这一行是为了指定用node执行脚本文件。
package.json
通过命令行npm init
或者yarn init
创建一个package.json
文件。
在package.json
文件中再加一项bin
属性,指向index.js
:
"bin": {
"mycli": "index.js"
},
cli各种工具库
为了实现脚手架的功能,我们需要借助各种库:
commander
npm install commander
编写代码来描述你的命令行界面。 Commander 负责将参数解析为选项和命令参数,为问题显示使用错误,并实现一个有帮助的系统。
inquirer
npm install inquirer
inquirer 的主要作用是询问,有input 输入框输入,number 数字输入,confirm yes/no确认,list/rawlist 列表选择(单选),expand展开,checkbox多选,password密码输入,editor在临时文件上启动用户首选编辑器的实例。
ora
npm install ora
相当于命令行终端中的进度条,用它来表示动作的开始和结束,加载中等状态。
ejs
npm install ejs
ejs 是一套简单的模板语言,帮你利用普通的 JavaScript 代码生成 HTML 页面。
至此,该demo的package.json
文件代码为
{
"name": "demo-mycli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": {
"mycli": "index.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^9.4.0",
"ejs": "^3.1.6",
"inquirer": "^8.2.0",
"ora": "^5.4.1"
}
}
在demo-mycli终端使用npm install
拉取依赖
在demo-mycli终端使用npm link
链接mycli
成全局命令
自定义create指令
现在我们开始编写自定义create指令
index.js
调用指令
需在尾行写program.parse(process.argv)
来执行command语句
index.js
页面代码:
#!/usr/bin/env node
const program = require('commander')
// 调用自定义指令
const createCommandsPage = require('./lib/core/create')
createCommandsPage.createCommands()
program.parse(process.argv)
create.js
编写指令
commander的基础用法:
const program = require('commander')
program
.command('name') // 自定义指令名称
.description('description') // 该指令的文字描述
.action(function) // 指令的function执行代码
create.js
页面代码:
const program = require('commander')
const { createProjectAction } = require('../utils/actions')
const createCommands = () => {
program
.command('create')
.description('以模板为基础创建新的项目')
.action(createProjectAction)
}
module.exports = {
createCommands
}
actions.js
编写指令执行代码
inquirer的基础用法:
const inquirer = require('inquirer');
inquirer.prompt([
{
type:"confirm",
name:"firut",
message:"你喜欢吃榴莲吗?"
},
{
type:"input",
name:"food",
message:"告诉我你喜欢吃什么?"
}
]).then((answers) => {
console.log(answers);
})
ora的基础用法:
const ora = require('ora')
const loadingCreate = ora('Loading unicorns')
loadingCreate.color = 'green'
loadingCreate.text = '正在创建中'
loadingCreate.start()
ejs的基础用法:
let template = ejs.compile(str, options);
template(data);
// => 输出渲染后的 HTML 字符串
ejs.render(str, data, options);
// => 输出渲染后的 HTML 字符串
ejs.renderFile(filename, data, options, function(err, str){
// str => 输出渲染后的 HTML 字符串
});
执行终端命令相关const {spawn} = require('child_process')
拉取模板至目标文件主流有两种方式:
1. 从远程仓库拉取,网络可查资料大多数使用的是download-git-repo;
2. 从模板文件夹中拉取
由于download-git-repo并不支持按模块拉取,功能较为有限,该demo采用第二种方式。
inquirer.js
页面代码:
const createInformation = [
{
type: 'input',
name: 'createName',
message: '请输入项目名称',
},{
type: "checkbox",
name: "compList",
message: "请选择你所需要的模块",
choices: [
{ name: 'baseConfigComponents', value: 'baseConfigComponents' },
{ name: 'loginComponents', value: 'loginComponents' },
{ name: 'queryComponent', value: 'queryComponent' },
],
},
]
module.exports = {
createInformation,
}
ora.js
页面代码:
const ora = require('ora')
const loadingCreate = ora('Loading unicorns')
loadingCreate.color = 'green'
module.exports = {
loadingCreate,
}
terminal.js
页面代码:
const {spawn} = require('child_process')
const commandSpawn = (...args) => {
return new Promise((resolve,reject) => {
const childProcess = spawn(...args)
// 显示子进程的控制台打印信息
childProcess.stdout.pipe(process.stdout)
childProcess.stderr.pipe(process.stderr)
childProcess.on("close",() => {
resolve()
})
})
}
module.exports = {
commandSpawn
}
templateCreate.js
页面代码:
const ejs = require('ejs')
const fs = require("fs");
const path = require("path");
const packageCreate = (name) => {
let templateCode = fs.readFileSync(path.join(__dirname,'../../components/package.ejs'))
let template = ejs.render(templateCode.toString(),{
name:name,
})
return template;
}
module.exports = {
packageCreate,
}
package.ejs
页面代码:
{
"name":"<%= name %>",
"version": "3.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --mode development",
"dev": "vue-cli-service build --mode developmentBuild",
"build": "node build.js"
},
"dependencies": {
"vue-router": "^3.2.0",
"vuex": "^3.4.0",
},
"devDependencies": {
"babel-eslint": "^10.1.0",
},
"lint-staged": {
"*.{js,jsx,vue}": [
"vue-cli-service lint",
"git add"
]
}
}
自此,我们在actions.js
文件中引用所需代码
actions.js
页面代码:
const { commandSpawn } = require('./terminal')
const { createInformation } = require('./inquirer')
var inquirer = require('inquirer')
const { loadingCreate } = require('./ora')
const fs = require("fs");
const stat = fs.stat;
const path = require("path");
const { packageCreate } = require('./templateCreate')
// 工具当前目录
const allDir = path.join(__dirname, "../../template");
const componentsDir = path.join(__dirname, "../../template/src");
const createProjectAction = () => {
inquirer.prompt(createInformation).then(async(res) => {
// console.log(res.createName,res.compList)
loadingCreate.start('项目正在努力创建中')
// 预创建总文件夹
mkdirfs(res.createName)
// 复制不可选的所有文件
fn(allDir,res.createName)
// 拉取所选择模块
if(res.compList.length > 0) {
res.compList.forEach(item => {
// console.log(`${componentsDir}/${item}`)
exists(`${componentsDir}/${item}`, `${res.createName}/src/${item}`, copy);
})
}
// 创建并写入需按配置更改内容的文件
fs.writeFileSync(`${res.createName}/package.json`, packageCreate(res.createName));
loadingCreate.succeed('项目完成创建')
// 判断系统平台
loadingCreate.start('正在努力拉取依赖')
const command = process.platform === 'win32'?'npm.cmd':'npm'
// 执行npm install
await commandSpawn(command,['install'],{cwd:`./${res.createName}/`})
loadingCreate.succeed('项目已完成创建并成功拉取依赖')
})
}
function mkdirfs(name){
fs.mkdir(`./${name}`,(err)=>{
if (err) {
console.log("name文件夹创建失败");
return;
}
})
fs.mkdir(`./${name}/src`,(err)=>{
if (err) {
console.log("src文件夹创建失败");
return;
}
})
}
function fn(src,dst){
fs.readdir(src, function (err, paths) {
if (err) {
throw err;
}
paths.forEach(function (path) {
if(path == `src`){
return
}else{
var _src = src + "/" + path,
_dst = dst + "/" + path,
readable,
writable;
stat(_src, function (err, st) {
if (err) {
throw err;
}
if (st.isFile()) {
readable = fs.createReadStream(_src);
writable = fs.createWriteStream(_dst);
readable.pipe(writable);
}
else if (st.isDirectory()) {
exists(_src, _dst, copy);
}
});
}
});
});
}
var exists = function (src, dst, callback) {
fs.exists(dst, function (exists) {
if (exists) {
callback(src, dst);
}
else {
fs.mkdir(dst, function () {
callback(src, dst);
});
}
});
};
var copy = function (src, dst) {
fs.readdir(src, function (err, paths) {
if (err) {
throw err;
}
paths.forEach(function (path) {
var _src = src + "/" + path,
_dst = dst + "/" + path,
readable,
writable;
stat(_src, function (err, st) {
if (err) {
throw err;
}
if (st.isFile()) {
readable = fs.createReadStream(_src);
writable = fs.createWriteStream(_dst);
readable.pipe(writable);
}
else if (st.isDirectory()) {
exists(_src, _dst, copy);
}
});
});
});
};
module.exports = {
createProjectAction,
}
自此,demo编写完成。
实现
重申:
在demo-mycli终端使用npm install
拉取依赖
在demo-mycli终端使用npm link
链接mycli
成全局命令
在终端中输入
mycli create
回车
回车
即成功创建了名为demo01的包含baseConfigComponents、queryComponent模块的项目文件并执行npm install
命令拉取了依赖。
尾声
故乡遥,何日去?家住吴门,久作长安旅。