前言: 前几天在摸鱼的时候看一个老师的webpack高级课程,最后面提及到怎么开发一个脚手架,并且老师也鼓励俺们自己手动实现一个,于是俺就迫不及待的想自己尝试一下啦,还真别说,虽说功能简单,但整个脚手架完工之后还是有点小收获的,于是乎写此文章记录下来(哗哗哗哗)
求生欲强的菜鸟一枚:俺这个其实连轮子都算不上,只用作自己的学习记录,但后续我会再完善一些功能,在开发公司项目的时候会尝试去用,自给自足何尝不是一种乐
什么是脚手架
在我们想要去创造某个轮子之前,首先要先去了解这个轮子的用途
脚手架就相当于是一个工具箱,里面包含许多常用的库和工具,开发人员可以通过命令行的方式快速创建项目、生成组件模板、配置开发环境等,使得开发更加高效和便捷,省去重复和繁琐的配置过程。
这里我拿用于创建vue2项目的vue-cli举例,vue-cli创建的项目基于webpack配置,我们都知道webpack的配置是十分繁琐和复杂的,各种loader和plugin眼花缭乱,vue-cli很友好的帮我们配置了基本的开发环境使得我们的项目得以运行
怎么开发脚手架
在这里我将整个开发过程浅分为三步
- 初始化项目
- 安装相关依赖
- 创建脚手架
Tips: 示例中的脚手架用于快速生成一个vue3或vue2项目,并提供创建vue组件和vue页面(自动配置路由)的功能
1、初始化项目
- 首先,cd到你的文件夹目录,在终端输入
pnpm init
进行项目的初始化,生成package.json
文件,并在根目录下新建一个文件夹lib
,用于存放我们脚手架的核心代码,并添加一个index.js
文件,作为我们的入口文件 - 在
package.json
中,添加一个bin
属性,属性值是一个对象,包含我们运行脚手架的命令mycli
和刚刚添加的index
文件的相对路径,同时,我们使用ES6语法来编写我们的代码,直接在package.json
添加上"type": "module"
,使node环境可以识别我们的ESM模块
{
"name": "myCli",
"version": "1.0.0",
"type": "module",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin":{
"mycli":"./lib/index.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}
- 在终端运行
npm link
,可以将当前正在开发的脚手架链接到全局 npm 包中进行测试和调试,我们可以在终端用命令行来测试我们的脚手架是否正常工作 - 在我们的入口文件,也就是index.js的最上方添加一行代码
#!/usr/bin/env node
,这行注释代码告诉操作系统在执行这个文件时应该使用哪个解释器来运行这个文件,当我们在终端中输入mycli
命令时,系统会自动使用node
解释器来运行该文件 - 在index.js中随便写点代码,在终端输入
mycli
,测试下是否出现想要的结果
- 至此,我们的脚手架环境算是搭建起来了,下面我们安装项目所需要的依赖
2、安装相关依赖
在项目开发前,先介绍下我们开发过程中需要用到的一些第三方库(具体使用可以跳转到对应链接查看哦),灵活的使用这些库可以提高我们的开发效率
- commander.js :是一个 Node.js 命令行程序开发框架,可以帮助我们轻松地定义和解析命令行选项和参数
- inquirer :是一个基于 Node.js 的命令行交互式用户界面(CLI UI)和交互式提示库。它允许开发者以简单、强大的方式与终端用户进行交互,包括输入文本、选择项等
- fs-extra : 是一个基于 Node.js 的第三方文件系统模块,它提供了比 Node.js 内置的 fs 模块更多的文件系统操作功能,我们可以很方便的使用他对文件进行操作
- ejs :是一种嵌入式 JavaScript 模板引擎,它可以让开发人员使用 JavaScript 代码动态生成 HTML 标记,我们如果想要使用命令行的方式生成模板文件,ejs功不可没
- picocolors : 是一个可以在终端修改输出字符样式的
npm
包,说直白点就是给字符添加颜色
可以在命令行输入pnpm install commander inquirer fs-extra ejs picocolors
安装以上相关依赖,安装完可以在package.json文件中查看是否安装成功
补充:由于commander这个库贯穿了我们整个脚手架的开发阶段,所以我还是简单介绍下这个库的一些基本使用吧
- 首先我们创建本地commander对象
import { Command } from "commander";
const program = new Command();
-
commander提供了很多子命令供我们描述更加完整的程序,这里简单介绍一些最常用的
-
version
:用来查看我们当前脚手架的版本,一般是从package.json
文件中获取版本号,传给.version()
,当我们在命令行输入mycli -v
或者mycli --version
的时候,会输出对应的版本号 -
option
:方法来定义选项,同时可以附加选项的简介,每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(–后面接一个或多个单词),使用逗号、空格或|
分隔,<>
中传入一个必须参数// '-c --color':自己定义的选项名称,'output':与这个选项对应的描述,'blue':默认值 program.option('-c --color <colorName>','output your color','blue') .option('-a, --add', 'add Something') //解析我们定义的命令并处理参数 program.parse(process.argv)
输入
mycli --help
可以输出我们定义的option选项 -
command
:用来配置命令,第一个参数是命令名称,可选参数用<>
表示,可选参数用[]
表示,第二个参数是对这个命令的描述或简介
program.option('-c --color <colorName>', 'output your color', 'blue') .command('start <service>', 'start named service')
输入
mycli --help
查看command
是否配置成功
-
action
: 一般.command
后面会跟上一个.action
,用来指定在终端输入该命令后应该执行的操作program.command("create <name>") .description("create a new vue project~") .action(async (option) => { console.log(option) });
输入
mycli --help
查看action
是否配置成功
-
3、创建脚手架
下面我们开始进行脚手架功能的开发,首先介绍下我们脚手架的功能
1、使用create命令可以创建项目,并且可以自行选择创建vue2或者是vue3项目
2、生成的项目包含了请求接口的方法,路由的基本配置
3、可以使用createCpn命令生成一个新的组件
4、可以使用createPage命令生成一个新的页面,并在router文件中自动进行配置,无需手动配置
-
我给自己的脚手架命名为
newvue-cli
,在package.json
中的bin
属性中配置的名字是newvue
,后续在终端都是通过newvue
这个命令来执行脚手架的操作 -
由于我们需要可以通过命令行创建vue2或者vue3模板,所以我们在lib文件夹下新建一个文件夹
template
用来存放我们的模板文件,在template
下分别创建一个vue2app和vue3app,分别cd到这两个文件夹下,通过vue init webpack vue2app
和npm init vue@latest vue3app
生成一个vue2项目和vue3项目(对于模板项目的网络配置和路由配置等另起一篇文章讲解,这篇文章主要让大家对脚手架开发有个基本的认知),这样我们就有了一个vue2和vue3的项目模板,后续我们通过命令行创建的项目是基于这两个模板的。- 使用
npm init vue\@latest vue3app
创建vue3项目
- 使用
vue init webpack vue2app
创建vue2项目
- 使用
-
接下来我们来实现 【通过
newvue create -n myFirstProject -v2(或者-v3)
创建一个vue2或vue3项目】的功能因为篇幅原因呢,我这边只贴出create的代码,代码里附上了详细的注释,完整代码我已上传至个人gitee账号管理gitee.com/huangwanlin…
index.js
#!/usr/bin/env node
import { Command } from "commander";
const program = new Command();
program.version("1.0.0");
program.option("-d --dest <dest>", "a destination path");
program
.command("create") //命令名称
.option("-n --name <projectName>", "your project name~") //创建的项目名称
.option("-v2 --vue2", "create a vue2 project~") //指定项目为vue2
.option("-v3 --vue3", "create a vue3 project~") //指定项目为vue3
.description("create a new vue project~") //该命令的相关描述
.action(async (projectOpt) => { //在终端输入`newvue create -n [name] -v2`该命令后会执行的操作
//在这里我将相关action代码封装在core文件夹下,这里通过懒加载的方式引入
const { createProject } = await import("./core/createProject.js");
return createProject(projectOpt);
});
core/createProject.js
import fse from "fs-extra";
import { resolve } from "path";
const { copy, pathExistsSync } = fse;
import { CWD, VUE_TEMPLATE } from "../config/index.js"; //CWD当前工作路径,VUE_TEMPLATE是vue模版路径
import { promptAction } from "../utils/actioncommon.js"; //存放action中的公共代码
import logger from "../utils/logger.js"; //指定控制台输出样式的相关代码
export async function createProject(option) {
//第一个参数用来判断创建的是'Project/Component/Page',第二个参数用来接收你输入的命令里面的相关参数,第三个参数用来指定创建的'Project/Component/Page'的默认名
const { name, vueVersion } = await promptAction("Project", option, "vue_app");
const dir = resolve(CWD, name);
if (pathExistsSync(dir)) { //判断需要创建的项目是否已经存在
console.log(`${name} already exists~~`);
return;
}
//根据创建时选择的版本去拼接模板路径
const template = resolve(
VUE_TEMPLATE,
vueVersion == "vue2" ? "vue2app" : "vue3app"
);
await copy(template, dir); //将项目模板复制到新建的项目目录下
logger.success("✨ Application created successfully!");
logger.info(`\
cd ${name}
npm install
npm run ${vueVersion === "vue3" ? "dev" : "serve"}
`);
}
../config/index.js
//这个文件存放一些公共的配置信息
import { resolve } from "path";
export const CWD = process.cwd();
export const VUE_TEMPLATE = resolve(CWD, "lib", "template");
../utils/actioncommon.js
import inquirer from "inquirer";
const { prompt } = inquirer;
import { resolve } from "path";
import fse from "fs-extra";
import { CWD } from "../config/index.js";
const { pathExistsSync, createFileSync, writeFileSync } = fse;
import ejs from "ejs";
// 由于后续添加的功能都会考虑到使用prompt来与用户进行互动,所以将此方法抽取成一个公共的函数,使用时直接引入即可
export const promptAction = async (createType, option, defaultName) => {
//命令行输入的-n表示创建的项目名,如若未指定,通过prompt引导用户输入项目名称
const { name } = option.name
? option
: await prompt([
{
type: "input",
name: "name",
message: `Your Vue ${createType} Name: `,
default: `${defaultName}`,
},
]);
//命令行输入的-v2/-v3代表vue2/vue3项目,如若已指定直接将模板复制到dir目录下,否则通过prompt引导用户选择所需模板
const isSelectVersion = option.vue2 ? "vue2" : option.vue3 ? "vue3" : false;
const { vueVersion } = isSelectVersion
? { vueVersion: isSelectVersion }
: await prompt([
{
type: "list",
name: "vueVersion",
message: "Select the version of your project: ",
choices: ["vue2", "vue3"],
},
]);
return { name, vueVersion };
};
// 下面两个方法是作者在开发后续功能时需要用到,解析对应的组件或页面模板引擎文件到指定的文件夹下,与create这个命令无关
export const handleEsjToFile = async (name, dest, templatePath, filename) => {
const result = await esjComplier(name, templatePath);
const componentPath = resolve(CWD, `${dest}/${filename}`);
// 判断创建的文件是否已经存在
if (pathExistsSync(componentPath)) {
console.log(`${name} already exists~~`);
return;
}
createFileSync(componentPath);
writeFileSync(componentPath, result);
};
// 解析模板,根据传入的data将对应的属性值渲染到ejs模板引擎中
export const esjComplier = async (name, templatePath) => {
const result = await ejs.renderFile(templatePath, {
data: {
name,
lowerName: `${name.toLowerCase()}`,
},
});
return result;
};
logger.js
import pico from "picocolors";
export default {
info(text) {
console.log(text);
},
success(text) {
console.log(pico.green(text));
},
warning(text) {
console.log(pico.yellow(text));
},
error(text) {
console.log(pico.red(text));
},
title(text) {
console.log(pico.cyan(text));
},
};
vue2_cpn.vue.js
// 这是创建vue2组件的esj模板引擎,大家可以借这个代码感受下esj的语法
<template>
<div class="<%= data.lowerName %>">
<h2>
<%= data.lowerName %>
</h2>
</div>
</template>
<script>
export default {
name: '<%= data.name %>',
data() {
return {}
},
mounted() { },
methods: {},
}
</script>
<style scoped>
.<%=data.lowerName %> {}
</style>
这是最终的项目目录,供大家参考
|-- package.json
|-- pnpm-lock.yaml
|-- preset.cjs
|-- tsconfig.json
|-- lib
|-- index.js
|-- config
| |-- index.js
|-- core
| |-- createComponent.js
| |-- createPage.js
| |-- createProject.js
|-- template
| |-- vue2_cpn.vue.ejs
| |-- vue2_router.js.ejs
| |-- vue3_cpn.vue.ejs
| |-- vue2app
| |-- vue3app
|-- utils
|-- actioncommon.js
|-- logger.js
后面其他功能也是可以按照同样的思路进行开发,整个过程下来难点主要在对全局常量等配置信息的管理,以及对公共方法的抽取,后续有时间会补充更多功能。