背景
你是否在工作中找个模板,要反复去代码仓库去拉取代码,但是现在只要简单几分钟,就可以快速实现像vue cli一样的功能,在终端选择对应的模板,生成对应的模板文件,同时支持任意拓展不同的模板。对于公司的前端基建的建设还是很有必要的
github源码地址 github.com/ccj-007/web… 觉得不错可以点个star ⭐
开始
首先我们要知道脚手架需要对应的哪些包?
npm | description |
---|---|
chalk | 在终端的文本样式修改 |
commander | 命令行交互界面 |
cross-spawn | 用于执行node的命令 |
ejs | 模板渲染可以更加细化的配合commander控制代码逻辑生成 |
figlet | 终端显示的logo |
shelljs | 执行shell命令 |
ora | 转圈圈 |
inquirer | 跟终端交互,选择对应的模板类型 |
fs-extra | 封装node的文件api,更强大 |
了解了我们的工具包,下面我们的思路是输入我们的包名,能想vue一样生成对应的交互模板 ,命令行输入类似 vue create my-app生成对应的项目名的文件,然后生成多个模板选择,同时考虑文件夹是否已经存在,如果存在那么就覆盖,同时也像vue-cli一样可以获取版本信息等。当然也需要考虑加一些logo、chalk文本样式效果,还有进度条。然后最后按需要安装依赖,最后给个cd <文件夹> npm run start 的提示。
开启一个交互界面
#! /usr/bin/env node //意思用node来执行这个脚本文件, 意味着你可以通过create <project> 来创建文件
const program = require('commander')
const log = require('../lib/log')
//commander配置node命令交互界面
program
.on('--help', () => {
log.outLOGO()
log.outCyanLog(`Run roc <command> --help show details`)
})
.version('0.1.0')
.command('create <name>')
.description('create a new project')
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
log.outLOGO()
//在这个../lib/create中获取对应的输入的信息,项目名和选项,后面我们根据这个选项生成
require('../lib/create.js')(name, options)
})
program
// 配置版本号信息
.version(`v${require('../package.json').version}`)
.usage('<command> [option]')
program.parse(process.argv) // 解析命令
对终端的文本样式封装下吧
//.lib/log.js 样式的封装,logo,chalk
const figlet = require('figlet');
const chalk = require('chalk')
const outLOGO = () => {
console.log('\r\n' + figlet.textSync('WEB-CORE-CLI', {
font: 'Standard',
horizontalLayout: 'fitted',
width: 120,
whitespaceBreak: true
}));
}
const outCyanLog = (info) => {
console.log(`\n ${chalk.cyan.bold(info)} \n`);
}
const outRedLog = (info) => {
console.log(`\n ${chalk.red.bold(info)} \n`);
}
module.exports = {
outLOGO,
outCyanLog,
outRedLog
}
根据用户交互生成模板
/**
* @description cli
*/
const path = require('path')
const fs = require('fs-extra')
const inquirer = require('inquirer')
const ejs = require('ejs')
const spawn = require('cross-spawn');
const ora = require('ora')
const process = require('process')
const shell = require('shelljs');
const log = require('./log');
let tplList = ['h5', 'node-mock', 'vue3-ts', 'h5-vue2'] //对应的模板文件名
let excludeFileList = ['node_modules', 'dist'] //需要排除的文件
let noNpmList = ['h5', 'node-mock'] //不需要安装的依赖
module.exports = async function (name, options) {
const cwd = process.cwd() //node 命令执行路径
const projUrl = path.join(cwd, name)
//目录下存在此项目文件夹
if (fs.existsSync(projUrl)) {
//是否带有-f强制创建指令
if (options.force) {
await fs.remove(projUrl)
} else {
//todo: 询问用户是否确定要覆盖
inquirer.prompt([
{
name: 'action', //boolean
type: 'list',
message: 'Target directory already exists Pick an action:',
choices: [
{
name: 'Overwrite',
value: 'overwrite'
}, {
name: 'Cancel',
value: false
}
]
}
]).then(async answer => {
let { action } = answer
if (!action) {
return;
} else if (action === 'overwrite') {
// 移除已存在的目录
await fs.remove(projUrl)
createProject(name, projUrl)
}
})
}
} else {
createProject(name, projUrl)
}
}
/**
* 创建项目文件
*
* @param {string} name
* @param {string} projUrl
*/
const createProject = (name, projUrl) => {
let choices = []
tplList.forEach(item => {
choices.push({ name: item })
})
//询问需要的模板
inquirer.prompt([
{
name: 'tplname',
type: 'list',
message: '请选择一个模板使用: ',
choices: choices
}
]).then(async answer => {
let { tplname } = answer
if (tplname) {
const destUrl = path.join(__dirname, '../', 'templates/', tplname);
deepCopyFiles(destUrl, projUrl, name) //复制文件
log.outCyanLog(`目录: ${projUrl} 项目名:${name} 创建成功!`)
startShell(name, tplname) //安装依赖
}
})
}
/**
*拷贝文件夹下所有文件
*
* @param {string} destUrl
* @param {string} projUrl
* @param {string} name
*/
const deepCopyFiles = (destUrl, projUrl, name) => {
fs.mkdir(projUrl, { recursive: true }, (err) => {
if (err) return
fs.readdir(destUrl, (err, files) => {
if (err) throw err;
files.forEach((file) => {
//判断是否是文件夹,排除不需要的文件
if (!(excludeFileList.findIndex((f) => f === file) > -1)) {
fs.stat(path.join(destUrl, file), (err, stats) => {
if (err) return
if (stats.isDirectory()) {
deepCopyFiles(path.join(destUrl, file), path.join(projUrl, file), name)
} else {
// 使用 ejs 渲染对应的模版文件
ejs.renderFile(path.join(destUrl, file), name).then(data => {
// 生成 ejs 处理后的模版文件
fs.writeFileSync(path.join(projUrl, file), data)
})
}
})
}
})
})
})
}
/**
* shell命令
*/
const startShell = async (name, tplname) => {
//如果是不需要安裝依賴的
if (noNpmList.findIndex((item) => item === tplname) > -1) {
log.outCyanLog(`创建${tplname}成功!`)
return
}
const spinner = ora('安装依赖中......').start();
shell.cd(name);
// 安装依赖
const pnpmCommend = spawn('pnpm install', [], {
stdio: 'inherit'
});
// 监听执行结果
pnpmCommend.on('close', function (code) {
// 执行失败
if (code !== 0) {
log.outRedLog('安装依赖失败')
process.exit(1);
}
// 执行成功
else {
log.outCyanLog(`安装依赖成功!!\n cd ${name} \n npm run dev`)
}
spinner.stop()
})
}
写入模板
在tempaltes文件中定义不同的模板,主要文件名要对应上面的tplList字段,根据需求定义
调试看下!发个包
调试首先要修改package内的bin(全局包命令的入口)、file(需要打包的文件)、mian字段 (项目执行主入口), 在调试的时候其他我们可以直接用npm link直接软连接到node的npm文件夹中。通过 npm config get prefix 快速获取文件夹路径。同时也可以看下file是否成功过滤了文件。如果想在其他包中测试使用,建议执行npx link <你的路径>,当然你也可以取消链接npm unlink <你的路径>。
如果调试没问题,我们要确保name字段是唯一,版本递增,然后nrm切换npm官方源,登录后npm publish就大功告成 !
//package.json
{
"name": "web-core-cli",
"version": "1.0.3",
"description": "All-in-one scaffolding, cli integrated with Vue3 ecological chain, cli of H5 page, mock template cli of Node",
"main": "./bin/cli.js",
"bin": {
"web-core-cli": "bin/cli.js"
},
"keywords": [
"cli",
"allin",
"vue",
"template",
"h5",
"node"
],
"scripts": {
"cli": "node ./bin/cli.js"
},
"author": "chen",
"license": "ISC",
"dependencies": {
"chalk": "^4.1.2",
"commander": "^9.3.0",
"cross-spawn": "^7.0.3",
"ejs": "^3.1.8",
"figlet": "^1.5.2",
"fs-extra": "^10.1.0",
"inquirer": "^7.3.3",
"ora": "^5.4.1",
"shelljs": "^0.8.5"
},
}
如何部署到私人仓库 ?
😅 我们做个给公司的前端基建用的可不能直接发布到npm官网,那么其实我们可以使用这个包 Verdaccio, 配合docker安装下,然后注意在config.yaml
写入配置,注意下路径,然后创建对应的用户名和账号,他也可以统一管理那你的账号数据在某个文件中。那么后面无非nrm add 你的ip地址,然后npm publish, 那么你的私有仓库就搭建上去了,不明白的小伙伴可以网上看看教程。
还有哪些优化的点?
1.其实可以跟vue一样有个默认配置和自定义配置的,当然目前这个还是个雏形,通过ejs模板的变量可以控制很多东西,这里可以多挖一些东西。