本文源码已经收录到github,欢迎各位小哥哥小姐姐们star,持续更新中。
前言
在前两篇文章10分钟带你升级webpack5、【建议细读】从0到1手把手带你捋一套webpack+vue项目模板中,带小伙伴们一起如何手动的从0到1搭建一个前端项目模板,但在我们实际的项目开发过程中,单纯的靠拷贝搭建好的项目模板生成新的项目,显然是比较低效的;如何规避这种低效的拷贝操作
呢?答案是脚手架/CLI
。那接下来我们就将上篇文章中我们手动搭建好的项目模板的基础上,通过脚手架/CLI
的形式一键快捷生成
我们想要的项目。
为什么需要脚手架/CLI
CLI,全称是 command-line interface,也就是命令行界面。
脚手架的好处有哪些呢?
- 规避通过人工低效的拷贝项目模板的操作
- 规范项目开发目录结构
- 统一团队统一开发风格,便于跨团队合作,以及后期维护,降低新人上手成本
- 提供一键前端项目的创建、配置、本地开发、插件扩展等功能,让开发者更多时间专注于业务
脚手架/CLI的工作流程
- 一个完整的脚手架通常包含有这些:
项目的创建
、项目模块的新增删除
、项目打包
、项目统一测试
、项目发布
等; - 下边通过这种图简单的阐述了脚手架的工作流程:
各自不同的端
->执行不同的脚手架命令
->从远端拉取不同的项目模板
->根据拉取的不同模板进行项目开发
->开发完自测后提交代码到远端仓库
;
脚手架/CLI需要用到的依赖分析
包名 | 功能描述 |
---|---|
commander | 处理控制台命令 |
chalk | 美化我们在命令行中输出内容的样式 |
fs-extra | 文件操作 |
inquirer | 控制台询问 |
handlebars | 模板引擎渲染 |
log-symbols | 打印日志提醒 |
ora | 命令行loading动效 |
download-git-repo | 远程下载模板 |
如何编写一个极简的脚手架/CLI
我们先来给脚手架取一个名字吧,caoyp-cli
完成步骤拆解:
- 创建项目(
create
) - 自定义脚手架启动命令(
commander
) - 询问用户获取创建的信息(
inquirer
) - 从github下载用户需要的模板(
download-git-repo
) - 发布项目(
npm
)
1. 创建项目
创建项目之前,我们先简单的定义一下我们的项目结构
caoyp-cli
├─ bin
│ └─ cli.js # 启动文件
├─ lib
│ └─ create.js # 创建项目文件
│ └─ generator.js # 下载项目文件
├─ .gitignore
├─ README.md
└─ package.json
github上创建项目
git clone https://github.com/Paulinho89/caoyp-cli
cd caoyp-cli
在项目更目录下执行:
npm init -y
在项目的根目录会自动帮助我们生成一个package.json
文件
{
"name": "caoyp-cli",
"version": "1.0.0",
"description": "前端单页脚手架",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"caoyp-cli",
"caoyp",
"脚手架"
],
"author": "caoyp",
"license": "ISC"
}
敲黑板了,这里是关键!!!
接下来我们要在package.json
中添加一个bin
字段,在安装时npm
会将文件符号链接到 prefix/bin
以进行全局安装或./node_modules/.bin/
本地安装。这样,就可以全局使用了。例如,下面的将caoyp-cli
作为命令名称,执行文件是根目录的index.js
;
{
"name": "caoyp-cli",
"version": "1.0.0",
"description": "前端单页脚手架",
"main": "index.js",
"bin": {
"caoyp": "./bin/cli.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"caoyp-cli",
"caoyp",
"脚手架"
],
"author": "caoyp",
"license": "ISC"
}
简单的编辑一下./bin/cli.js
#! /usr/bin/env node
console.log('caoyp-cli working ~~')
#! /usr/bin/env node
这段说明一下,不是注释,这段代码是告诉你的脚本工具(bash/zsh
), 下面的内容是要在node环境
下运行的代码,不能省略!!!
为了方便调试,使用npm link
链接到全局
➜ caoyp-cli git:(main) npm link
npm WARN my-node-cli@1.0.0 No description
npm WARN my-node-cli@1.0.0 No repository field.
audited 51 packages in 1.7s
found 0 vulnerabilities
/usr/local/bin/caoyp-cli -> /usr/local/lib/node_modules/caoyp-cli/cli.js
/usr/local/lib/node_modules/caoyp-cli -> /Users/caoyp/study/caoyp-cli
在命令行中输入caoyp
,测试一下
➜ caoyp-cli git:(main) caoyp
caoyp-cli working ~~
完成测试,打印出我们想要的结果。
2. 自定义脚手架启动命令
先来分析一下我们要怎么做?
- 我们要借助
commander
来实现 - 参考
vue-cli
,初始化一些常用的命令create
、help
等等 - 如果项目已经存在,需要给用户提示是否需要
强制覆盖
2.1 安装依赖
npm i commander --save
2.2 创建命令
在./bin/cli.js
中编辑
#! /usr/bin/env node
const program = require('commander')
program
// 定义命令和参数
.command('create <app-name>')
.description('create a new project')
// -f or --force 为强制创建,如果创建的目录存在则直接覆盖
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
// 打印执行结果
console.log('name:', name, 'options:', options)
})
program
// 配置版本号信息
.version(`v${require('../package.json').version}`)
.usage('<command> [option]')
// 解析用户执行命令传入参数
program.parse(process.argv);
在命令行中输入caoyp
,测试一下
➜ caoyp-cli git:(main) caoyp
Usage: caoyp <command> [option]
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
create [options] <app-name> create a new project
help [command] display help for command
此时,我们发现Commands中已经有了create [options] <app-name>
命令,那接下来我们再输入caoyp create
试试,看看会打印什么
➜ caoyp-cli git:(main) caoyp create
error: missing required argument 'app-name'
➜ caoyp-cli git:(main) caoyp create my-project
执行结果 >>> name: my-project options: {}
➜ caoyp-cli git:(main) caoyp create my-project -f
执行结果 >>> name: my-project options: { force: true }
➜ caoyp-cli git:(main) caoyp create my-project --force
执行结果 >>> name: my-project options: { force: true }
此时,我们可以在命令行中拿到我们的输入信息了。
2.3 执行命令
创建一个lib
文件夹,并且创建一个create.js
module.exports = async function(name, options) {
console.log('-----create.js', name, options);
}
./bin/cli.js
中引入create.js
const program = require('commander');
program.command('create <app-name>')
.description('create a new project')
// -f or --force 为强制创建,如果创建的目录存在则直接覆盖
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
// 在 create.js 中执行创建任务
require('../lib/create.js')(name, options)
})
再次执行caoyp create my-project
,结果打印显示
➜ caoyp-cli git:(main) caoyp create my-project
-----create.js
my-project {}
按照开头描述的,我们这个时候需要思考一下,我们在执行create
的时候,项目目录到底是否存在;
- 如果存在,友情提示创建项目已经存在;如果存在相同的项目目录,且加上-f,代表强制执行,此时删除老的目录,直接创建新的目录
- 如果不存在,直接创建新的目录
我们需要借助另外一个插件fs-extra来辅助我们判断文件是否存在,同时安装一下log-symbols,方便我们在日志打印的时候,更加友好的给用户提示
安装一下fs-extra
、log-symbols
npm i fs-extra log-symbols --save
我们按照上边的思路再继续完善一下create.js
const path = require('path');
const fs = require('fs-extra');
const symbols = require('log-symbols');
module.exports = async function(name, options) {
const cwd = process.cwd();
// 需要创建的目录地址
const targetAir = path.join(cwd, name);
// 判断目录是否已经存在
if (!fs.existsSync(targetAir)) {
// TODO:创建新的目录
} else {
console.log(symbols.error, chalk.red('项目已存在'));
// 是否要强制创建
if (options.force) {
await fs.remove(targetAir);
// TODO:询问用户是否确定要覆盖
}
}
}
标注TODO
的逻辑,我们会在下边再详细讲解。
2.4 完善帮助命令
细心的小伙伴们在我们使用vue-cli
的时候,--help
会发现有很多帮助命令可以引导我们如何✅的姿势去使用脚手架。
➜ ~ vue --help
Usage: vue <command> [options]
Options:
-V, --version output the version number
-h, --help output usage information
Commands:
init generate a new project from a template
list list available official templates
build prototype a new project
create (for v3 warning only)
help [cmd] display help for [cmd]
所以,这里我们也模仿一下vue-cli
,完善一下我们的帮助命令,先再安装一下chalk,用于美化一下命令行;figlet帮助我们打印一个属于自己的logo。
const program = require('commander');
const figlet = require('figlet');
const chalk = require('chalk');
program
.on('--help', () => {
console.log('\r\n' + figlet.textSync('caoyp', {
font: 'Ghost',
horizontalLayout: 'default',
verticalLayout: 'default',
width: 80,
whitespaceBreak: true
}));
console.log(`\r\nRun ${chalk.cyan(`caoyp <command> --help`)} for detailed usage of given command\r\n`)
})
执行caoyp --help
,看看会打印出什么
➜ caoyp-cli git:(main) caoyp --help
Usage: caoyp <command> [option]
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
create [options] <app-name> create a new project
help [command] display help for command
('-. _ (`-.
( OO ).-. ( (OO )
.-----. / . --. / .-'),-----. ,--. ,--._.` \
' .--./ | \-. \ ( OO' .-. ' \ `.' /(__...--''
| |('-..-'-' | |/ | | | | .-') / | / | |
/_) |OO )\| |_.' |\_) | |\| |(OO \ / | |_.' |
|| |`-'| | .-. | \ | | | | | / /\_ | .___.'
(_' '--'\ | | | | `' '-' ' `-./ /.__) | |
`-----' `--' `--' `-----' `--' `--'
Run caoyp <command> --help for detailed usage of given command
3. 询问用户获取创建的信息
还是老规矩,动手之前我们先来分析一下询问用户信息该如何去做?
- 完善上一步TODO:询问用户已经存在的目录是否要覆盖下载最新的
const path = require('path');
const fs = require('fs-extra');
const symbols = require('log-symbols');
module.exports = async function(name, options) {
const cwd = process.cwd();
// 需要创建的目录地址
const targetAir = path.join(cwd, name);
// 判断目录是否已经存在
if (!fs.existsSync(targetAir)) {
// 创建新的目录
fn(name, targetAir);
} else {
console.log(symbols.error, chalk.red('项目已存在'));
// 是否要强制创建
if (options.force) {
await fs.remove(targetAir);
// 创建新的目录
fn(name, targetAir);
}
}
}
- 自定义模板信息
const fn = (name, targetAir) => {
// 自定义模板项目信息
inquirer.prompt([
{
name: 'version',
message: '请输入项目版本',
default: '1.0.0'
},
{
name: 'description',
message: '请输入项目描述信息',
default: '这是一个自定义脚手架生成的项目'
},
{
name: 'author',
message: '请输入作者名称',
default: ''
}
]).then(res => {
// TODO 下载github模板
})
}
此时,我们再次输入caoyp create my-project
,会发现有对应的模板信息需要我们填写,创建一个空的my-project
目录;如果输入caoyp create my-project -f
,回车后会发现上次创建的my-project
目录被移除掉,重新让我们输入对应的模板信息,重新创建新的模板项目;
4. 从github下载用户需要的模板
还是老规矩,动手之前我们先来分析一下该如何从github
下载用户需要的模板?
github
上我们提前已经上传好默认的下载 模板
- 借助
download-git-repo
辅助下载模板
先安装一下依赖download-git-repo用于下载github中的模板;handlebars用户动态的渲染模板提示信息;ora下载的时候给一个loading
效果。
npm i download-git-repo handlebars ora --save
./lib
目录下创建generator.js
文件
const ora = require('ora');
const symbols = require('log-symbols');
const inquirer = require('inquirer');
const fs = require('fs-extra');
const downloadGitRepo = require('download-git-repo');
const chalk = require('chalk');
const handlebar = require('handlebars');
class Generator {
constructor(name, targetDir, res) {
this.name = name;
this.targetDir = targetDir;
this.res = res;
}
async download() {
// 1)拼接下载地址
const requestUrl = `github.com:Paulinho89/webpack5-single-template#master`;
const spinner = ora(`正在下载模板,源地址:${requestUrl}`);
spinner.start();
// 2)调用下载方法
await downloadGitRepo(
// 直连下载,默认下载master
requestUrl,
this.targetDir,
(error) => {
if (error) {
spinner.fail();
// 下载失败给出友情提示 console.log(symbols.error, chalk.red(error));
} else {
spinner.succeed();
const fileName = `${this.name}/package.json`;
// 下载成功后获取命令行输入的提示信息
const meta = {
name: this.name,
version: this.res.version,
description: this.res.description,
author: this.res.author
}
// {{name}}/package.json路径存在时
if (fs.existsSync(fileName)) {
// 读取提示信息
const content = fs.readFileSync(fileName).toString();
const resultContent = handlebar.compile(content)(meta); //写入提示信息到模板项目的package.json文中
fs.writeFileSync(fileName, resultContent);
}
console.log(symbols.success, chalk.green('项目初始化成功'))
console.log(symbols.info, `\r\n cd ${chalk.cyan(this.name)}`)
console.log(symbols.info, `\r\n npm install`)
console.log(symbols.info, 'npm run start\r\n')
}
}
)
}
}
module.exports = Generator;
关于下载downloadGitRepo()
方法使用时会经常报出路径错误导致下载模板失败--git clone status 128
解决方案:
错误的写法:
- 直接去github模板链接当做第一个参数直接传递
- 使用{clone: true}
downloadGitRepo('https://github.com/Paulinho89/webpack5-single-template', name, {clone: true}, (err) => {
console.log(err ? 'Fail' : 'Success')
})
正确的写法:
- 使用github.com:用户名/模板名称#分支名称
- 去掉{clone: true}
downloadGitRepo('github.com:Paulinho89/webpack5-single-template#master', name, (err) => {
console.log(err ? 'Fail' : 'Success')
})
- 完善上一步TODO:下载成功之后,将每次输入自定义的模板信息填充到模板项目的
package.json
中去
模板项目的package.json
中的version
、 description
、author
这几个字段我们可以写成动态渲染的模式。
{
"name": "webpack-single-template",
"version": "{{version}}",
"description": "{{description}}",
"main": "index.js",
"scripts": {
"start": "webpack serve --config build/webpack.config.dev.js",
"build:test": "rimraf dist && webpack --config build/webpack.config.test.js",
"build:prod": "rimraf dist && webpack --config build/webpack.config.prod.js",
"lint": "eslint --fix --ext .vue,.js src build",
"lint-staged": "lint-staged",
"build:stats": "webpack --config build/webpack.config.prod.js --json > stats.json",
"analyz": "cross-env NODE_ENV=production ANALYZE=true npm_config_report=true npm run prod",
"dll": "webpack --config build/webpack.config.dll"
},
"pre-commit": [
"lint-staged"
],
"lint-staged": {
"*.{js, vue}": [
"eslint --fix",
"git add"
]
},
"author": "{{author}}",
"license": "ISC"
}
const Generator = require('./generator');
const fn = (name, targetAir) => {
// 自定义模板项目信息
inquirer.prompt([
{
name: 'version',
message: '请输入项目版本',
default: '1.0.0'
},
{
name: 'description',
message: '请输入项目描述信息',
default: '这是一个自定义脚手架生成的项目'
},
{
name: 'author',
message: '请输入作者名称',
default: ''
}
]).then(res => {
// 创建项目
const generator = new Generator(name, targetAir, res);
generator.download();
})
}
当下载模板完成之后会将用户输入的答案渲染到 package.json
中。
5. 发布项目
经过上边的逻辑梳理后,我们搭建的简易脚手架已经完成了,我们在实际使用的时候都是需要发布到npm仓库里的。
发布流程:
- 注册一个
npm账号
如果是新注册的账号,需要先登录填写的邮箱进行验证,否则直接发布的话发布失败。
- 完善
package.json
中的配置,确定发布的版本号
{
"name": "caoyp-cli",
"version": "1.0.0",
"description": "前端单页脚手架",
"main": "index.js",
"bin": {
"caoyp": "./bin/cli.js"
},
"directories": {
"lib": "lib"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Paulinho89/caoyp-cli.git"
},
"author": "caoyp",
"keywords": [
"caoyp-cli",
"caoyp",
"脚手架"
],
"license": "ISC",
"bugs": {
"url": "https://github.com/Paulinho89/caoyp-cli/issues"
},
"homepage": "https://github.com/Paulinho89/caoyp-cli#readme",
"dependencies": {
"axios": "^0.21.1",
"chalk": "^4.1.1",
"commander": "^7.2.0",
"download-git-repo": "^3.0.2",
"figlet": "^1.5.0",
"fs-extra": "^10.0.0",
"handlebars": "^4.7.7",
"inquirer": "^8.1.1",
"log-symbols": "^3.0.0",
"ora": "^5.4.1"
}
}
- 使用
npm publish
发布
npm publish
发布成功之后,可以进入到npm个人仓库里,看到自己发布的包。
按照我们README.md
中定义好的安装命令把脚手架install
到我们的本地,然后创建,重启,一切正常;OK,这时我们的脚手架从项目搭建到发布整个流程就算结束了。
写到最后
- 目前,这个脚手架还是偏于简单,有些步骤还需要进一步完善,像我们在向远端仓库拉取模板的时候,通常我们在实际的开发过程中会有
多个版本的模板
,且每个模板都会有不同的tag
;每次拉取可以通过选择不同的版本模板、不同的tag、添加更多的模板、删除模板等。接下来,我也会将这些功能持续完善到脚手架中。
如果您觉得这篇文章对您有一点点帮助,欢迎您看完后给我点赞
、评论
、关注
支持一下,您的支持是我写作的动力,谢谢~~😁