在快速迭代、持续创新的前端开发领域,项目脚手架作为项目启动的基石,其重要性愈发凸显。然而传统的做法,如借助vue-cli
、create-react-app
(cra)等工具快速生成基础模板后,仍然需要根据公司的开发手册进行繁琐的配置调整、API封装以及依赖安装等等,这种流程不仅耗时耗力,而且容易在初始化项目时引入不必要的复杂性。
如果将这些操作步骤统一整合到自定义的脚手架中,我们便能大幅减少项目初始化的工作量,并在项目初始阶段就实现规范与统一,为公司项目管理带来显著优势。因此,一个高效、灵活且充分符合团队特色的自定义脚手架成为了我们提升开发效率、优化工作流程的不可或缺的工具。本文将带领大家探索如何搭建一个自定义的前端项目脚手架,从需求分析到技术选型,再到具体实现,我们将一步步搭建起属于自己的开发利器。
一,创建脚手架
首先创建脚手架工程文件夹,并初始化package.json
文件:
sh
makir my-cl && cd my-cli && npm init -y
默认使用ESM
模块化开发,所以需要在package.json
文件中设置:"type": "module"
。
安装搭建脚手架需要的插件列表:
chalk
:可以给终端字体样式。commander
:解析命令行。download-git-repo
:下载并提取git仓库,用于下载模版。figlet
:创建脚手架Logo。fs-extra
:文件处理。inquirer
:命令行界面与用户交互的插件。log-symbols
:命令行界面状态图标。ora
:显示loading动画效果。shelljs
:基于nodejs的shell命令工具。table
:命令行界面表格内容显示。
然后创建入口执行文件index.js
,此时工程目录结构为:
sh
my-cli
├─ index.js
├─ package-lock.json
└─ package.json
编辑index.js
文件:
js
#!/usr/bin/env node
console.log('hello my-cli')
然后我们还需要在 package.json
中增加 bin
属性:
bin
属性用来将可执行文件加载到全局环境中,指定了bin
字段的npm包,一旦在全局安装,就会被加载到全局环境中,可以通过别名来执行该文件,如果非全局安装,那么会自动连接到项目的node_module/.bin
目录中。
json
"bin": {
"my-cli": "./index.js"
},
在脚手架开发阶段: 我们可以使用npm link
命令把这个文件映射到全局后,就可以在任意目录下的命令行中输入 my-cli
执行我们的 index.js
脚本文件【开发调试】。
输入 npm list -g
可以查看已安装的全局模块。
npm link
可以帮助我们模拟包安装后的状态,它会在系统中做一个快捷方式映射,让本地的包就好像全局安装过一样,可以直接使用。
下面我们开始设置脚手架具体的内容。
二,命令行交互
1,项目模板列表
关于项目模板:主要就是将《公司的开发手册规范内容》都编写到模板文件中,比如常见的Eslint/Prettier
统一配置,API
封装,axios
请求封装,公共依赖添加等等,一般根据技术栈可以准备React/Vue
两套模板。
在脚手架工程目录下建立一个scripts
脚本目录,后面会将其它的脚本文件都放在这个目录下:
sh
my-cli
├─ scripts
├─ index.js
└─ package.json
然后在该目录下创建一个constants.js
文件:
js
// scripts/constants.js
/**
* 项目模板列表
*/
export const templates = [
{
name: "vue-template",
value: "direct:https://gitee.com/账号/vue-template.git",
desc: "基于vite的自定义vue项目模板"
},
{
name: "react-template",
value: "direct:https://gitee.com/账号/react-template.git",
desc: "基于umi的自定义react项目模板"
}
]
/**
* 项目信息
*/
export const messages = [
{
name: "name",
message: "请输入项目名称:",
},
{
name: "description",
message: "请输入项目描述:",
}
]
在constants
脚本文件中,我们定义两个常量:
templates
存储需要使用的模板列表,后续我们将会使用download-git-repo
插件来克隆远程仓库,这里要注意的是download-git-repo
默认的地址是github
,要想使用gitee
或者自定义的gitlab
仓库需要书写完整的地址,并且要在url
前面添加direct:
标记。另外要注意的是默认克隆的是项目master
分支,如果想要克隆其他分支可以在url
尾部添加具体的分支名称#dev
。messages
存储两个常见的package
信息,后面可以通过命令行交互让用户输入这两个字段信息,拿到输入结果后就可以重写到克隆的项目模板的package.json
文件中,当然你也可以根据需求添加更多的交互信息,比如填写作者信息,关键词信息等等。
这里还有一点要说明的是:模板文件可以放在脚手架工程目录下,也可以独立存储到远程仓库中。不过这里我推荐把项目模板放到独立的仓库中。因为脚手架搭建完成之后,一般改动较少,但是项目模板会经常更新,这样可以保证即使没有更新脚手架也可以一直拉取到最新的模板文件。
2,克隆封装
在scripts
目录下创建一个clone.js
文件:
js
import download from "download-git-repo";
import ora from "ora";
import chalk from "chalk";
/**
* 克隆模板方法
* @param {*} repository 远程仓库地址
* @param {*} appName 项目名称
* @returns
*/
export default function clone(repository, appName) {
const spinner = ora("正在创建项目......").start();
return new Promise((resolve, reject) => {
// 第四个参数为一个callback,如果err存在则代表拉取项目失败
download(repository, appName, {clone: true}, err => {
if (err) {
spinner.fail(chalk.red(err));
reject(err);
return;
}
spinner.succeed(chalk.greenBright("项目创建成功"));
resolve();
})
})
}
基于download-git-repo
插件封装一个clone
方法,这里可以使用ora
插件在命令行界面显示一个加载动画,并且在项目克隆完成之后显示一个成功的标记,而chalk
插件的作用就是美化终端的文字显示。
3,文件操作
在scripts
目录下创建一个utils.js
文件:
js
import fs from "fs-extra";
import chalk from "chalk";
import path from "path";
import logSymbols from 'log-symbols'
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
// 删除文件夹
export async function removeDir(dir) {
try {
await fs.remove(resolveApp(dir));
console.log(logSymbols.warning, `已覆盖同名文件夹${dir}`);
} catch (err) {
console.log(err);
return;
}
}
// 修改package.json配置
export async function changePackageJson(name, info) {
try {
const pkg = await fs.readJson(resolveApp(`${name}/package.json`));
Object.keys(info).forEach((item) => {
if (info[item] && info[item].trim()) {
pkg[item] = info[item];
}
});
await fs.writeJson(resolveApp(`${name}/package.json`), pkg, { spaces: 2 });
} catch (err) {
console.log(err);
console.log(logSymbols.warning, chalk.yellow("更新项目信息失败,请手动修改package.json"));
}
}
在utils
脚本文件中封装两个操作文件的方法:
removeDir
主要是在项目创建的过程中,如果存在同名文件夹可以调用此方法删除目录,然后再创建项目。changePackageJson
就是将用户输入的交互信息(项目名称,描述)更新到项目的package.json
文件中。
4,终端交互
在scripts
目录下创建一个interactive.js
文件:
js
import inquirer from 'inquirer';
// 封装命令行交互的相关方法
/**
* @param {string} message 询问提示语句
* @returns {Object} 根据name属性获取用户输入的值{confirm: y/n}
*/
export const inquirerConfirm = async (message) => {
const answer = await inquirer.prompt({
type: "confirm",
name: "confirm",
message,
});
return answer;
}
/**
*
* @param {string} message 询问提示语句
* @param {Array} choices 选择模板列表,默认读取对象的name属性
* @returns {Object} 根据name属性获取用户输入的值{请选择项目模板: xxxxxx}
*/
export const inquirerChoose = async (message, choices) => {
const answer = await inquirer.prompt({
type: 'list',
name: "choose",
message,
choices,
});
return answer;
}
/**
* @param {Array} messages 询问提示语句数组
* @returns {Object} 结果对象
*/
export const inquirerInputs = async (messages) => {
const questions = messages.map(msg => {
return {
name: msg.name,
type: "input",
message: msg.message,
}
})
const answers = await inquirer.prompt(questions);
return answers
}
基于inquirer
插件在interactive
脚本文件中封装三个常用的命令行交互方法。
inquirerChoose
用于显示模板列表,用户可以通过上下键选择克隆的项目模板。inquirerConfirm
用于询问用户,比如存在同名文件夹时是否覆盖(y/n)
,用户可以输入y/n
返回一个布尔值结果。inquirerInputs
用于接收用户输入的一些项目信息,比如项目名称,项目描述等等。
5,创建项目
在scripts
目录下创建一个create.js
文件:
js
import fs from "fs-extra";
import shell from "shelljs"
import chalk from "chalk";
import logSymbols from 'log-symbols'
import clone from './clone.js'
import { removeDir, changePackageJson } from './utils.js'
import { templates, messages } from './constants.js'
import { inquirerConfirm, inquirerChoose, inquirerInputs } from './interactive.js'
/**
* 创建项目方法
* @param {*} appName 项目名称
* @param {*} option 配置项
*/
export default async function create(appName, option) {
if (!shell.which("git")) {
console.log(logSymbols.error, chalk.redBright("Error:运行脚手架必须先安装git!"));
shell.exit(1);
}
// 验证appName输入是否符合规范
if (appName.match(/[\u4E00-\u9FFF`~!@#$%&^*[\]()\\;:<.>/?]/g)) {
console.log(logSymbols.error, chalk.redBright("Error:<app-name>存在非法字符!"));
return;
}
let repository = '';
// 验证是否使用了--template配置项
if (option.template) {
// 从模板列表中找到目标templaet,如果不存在则抛出异常
const template = templates.find(template => template.name === option.template);
if (!template) {
console.log(logSymbols.warning, `不存在模板${chalk.yellowBright(option.template)}`);
console.log(`\r\n运行 ${chalk.cyanBright("my-cli ls")} 查看所有可用模板\r\n`);
return;
}
repository = template.value;
} else {
// 从模板列表中选择
const answer = await inquirerChoose("请选择项目模板:", templates);
repository = answer.choose;
}
// 验证是否存在appName同名文件夹
if (fs.existsSync(appName)) {
if (option.force) {
// 存在force配置项,直接覆盖
await removeDir(appName);
} else {
// 不存在force配置项,询问是否覆盖
const answer = await inquirerConfirm(`已存在同名文件夹${appName}, 是否覆盖:`);
if (answer.confirm) {
await removeDir(appName);
} else {
console.log(logSymbols.error, chalk.redBright(`Error:项目创建失败!存在同名文件夹${appName}`));
return;
}
}
}
let answers = {};
// 验证是否使用了--ignore配置项
if (!option.ignore) {
// 没有使用则需要输入项目信息
answers = await inquirerInputs(messages);
}
// 拉取模板
try {
await clone(repository, appName);
}
catch (err) {
console.log(logSymbols.error, chalk.redBright("项目创建失败"));
console.log(err);
shell.exit(1);
}
// 最后更新package.json
if (answers.name || answers.description) {
await changePackageJson(appName, answers);
}
}
封装一个create
方法,这就是我们执行my-cli create app
命令执行的完整逻辑,之前封装的命令行交互,模板拉取,文件操作都会在这里按顺序调用,这就是一个创建项目的基本逻辑,当然如果你还有其他需求,可以在这里添加更多的处理逻辑。
6,命令封装
最后我们回到index.js
入口文件:
js
#! /usr/bin/env node
import fs from "fs-extra";
import figlet from "figlet";
import chalk from "chalk";
import { table } from 'table';
import { program } from "commander";
import create from './scripts/create.js'
import { templates } from './scripts/constants.js'
// 读取package.json配置信息
const pkg = fs.readJsonSync(new URL("./package.json", import.meta.url));
// 查看版本号
program.version(pkg.version, "-v, --version");
// 创建项目命令
program
.command("create <app-name>")
.description("创建一个新的项目")
.option("-t --template [template]", "输入模板名称快速创建项目")
.option("-f --force", "强制覆盖本地同名项目")
.option("-i --ignore", "忽略项目相关描述,快速创建项目")
.action(create);
// 查看模板列表
program
.command("ls")
.description("查看所有可用的模板")
.action(() => {
const data = templates.map(item => [chalk.greenBright(item.name), chalk.white(item.value), chalk.white(item.desc)]);
data.unshift([chalk.white("模板名称"), chalk.white("模板地址"), chalk.white("模板描述")]);
console.log(table(data));
})
// 配置脚手架基本信息
program
.name("my-cli")
.description("一个简单的自定义脚手架")
.usage("<command> [options]")
// 用在内置的帮助信息之后输出自定义的额外信息
.on("--help", () => {
console.log("\r\n" + chalk.greenBright.bold(figlet.textSync("my-cli", {
font: "Standard",
horizontalLayout: "default",
verticalLayout: "default",
width: 100,
whitespaceBreak: true,
})))
console.log(`\r\n Run ${chalk.cyanBright(`my-cli <command> --help`)} for detailed usage of given command.`)
});
program.parse(process.argv);
我们在index
入口文件中封装几个常用的脚手架命令:
-v
:查看脚手架版本号。ls
:查看所有可用的模板。create <app-name>
:创建项目,脚手架最核心的命令【触发封装的create
方法】,然后我们还添加了几个配置选项:-t --template
:输入模板名称快速创建项目。-f --force
:强制覆盖本地同名文件。-i --ignore
:忽略项目信息输入,快速创建项目。
最后在命令行输入my-cli
即可查看脚手架的基本信息:
通过create <app-name>
命令一步一步创建项目:
也可以通过添加配置项,快速创建项目:
到此为止,我们的脚手架基本搭建完成。
bash
my-cli
├── scripts
│ └── clone.js
│ └── constants.js
│ └── create.js
│ └── interactive.js
│ └── utils.js
├── index.js
├── package-lock.json
├── package.json
├── readme.md
三,发布npm包
最后,将搭建完成的脚手架my-cli
发布到公司的npm
私服。
npm私服搭建可以参考《Linux系统使用Verdaccio搭建Npm私服》
其他成员就可以全局安装脚手架:
sh
npm install my-cli -g
然后使用脚手架快速的创建项目后进行开发了。