脚手架搭建
heywc Lv1

为啥会想到自己搭建一个项目脚手架

我举个身边的例子:

  1. 同事甲在搭建新项目的时候,直接 pull 了他在远程仓库预先配置好的项目模板
  2. 同事乙经验老道,直接将之前写的项目拷贝了一份,删除了一些无用的代码,那么也是搭建起了新的项目
  3. 同事丙前端新手玩家,选择了 vue-cli 然后一步步开始下载插件,增写配置

相对而言,甲的方式更加节省时间,但是如果新项目和远程仓库的项目存在一些差异,他依旧需要手动去修改这些差异;乙的方式,虽然也不慢,但是会导致新项目中残留老项目的残留代码(要知道新时代怎么会存在承载旧时代残党的船呢–致敬-白胡子);但是丙的搭建项目方式可想而知,非常费时间。

因此我就在想有没有一种技术方案能同时解决上述问题,既操作简单又节省时间,还能使新搭建的项目更加规范。这不就是所谓的给团队增效的方案嘛。一想到这,我觉得我他娘是个天才(O(∩_∩)O 手动狗头)

为此我命名它为ywc-cli 脚手架方案。没错,就是以我名字缩写加上 cli 组合而成的!设计的思路有以下两种:

最简易版本: 可选地拉取远程仓库预设好的模板,只需要安装完 ywc-cli,运行 cmd 脚本指令 ywc-cli create xxx就可以直接选择需要安装的项目模板了,而我目前是简单提供了 vue2 和 vue3 的初始化模板,后续会对这两个模板进行完善,当然我也希望增加 react 的项目模板。

由于留给我的时间不多了,所以我只实现了这个版本的代码。

吹一波好处:这种应该是最常见且最简单层面的脚手架,只需要在远程仓库中存有不用版本的项目模板,开发人员安装完脚手架之后可根据项目自行选择安装的版本,快速搭建起项目。这种脚手架适用于公司专门定制化项目,例如专门定制商城项目或者数据展示项目以及是内网的后台系统,这类项目的特点就是在确保 UI 组件大致不变的情况下,不同项目之间代码复用率极高。因此公司的定制化项目应该有其对应的脚手架,这样便能大幅度缩短项目的开发周期并且规范代码

自定义版本:可执行性待验证

  1. 脚手架中预设好了项目模板,执行 create 指令时首先进行模板类型选择,将拷贝模板至临时目录,这步通常是选择技术框架;

  2. 根据开发人员的选择去安装对应的插件,这一步需要准备一个映射文件存储技术框架与对应的插件之间的映射关系,同时还得准备插件对应的配置代码。根据选中的插件,通过模板引擎渲染,临时目录中的模板

  3. 生成临时目录中的项目模板

这个最定义版本的核心思想就是通过模板引擎渲染对应插件的配置代码,实现起来也是不难的,不过如果能将这个过程的核心通过一个工具类来实现,那才是技术难点。

技术方案想法简单介绍完了,下面就该介绍技术实现了,相关工具必须先安排上,当然这个脚手架是基于 node.js 实现的。

脚手架相关工具


  1. inquirer.js

为 node.js 提供了易于嵌入且美观的命令行接口。通俗点解释就是询问工具,我们可以通过该工具设置脚手架需要向用户询问的一些问题,比如是否安装 xxx 工具、该项目版本号等等

安装

1
npm install inquirer

用法:
调用 inquirer.prompt 方法,prompt 接收两个参数:一个是 qustionArray(数组),用来设置需要询问的问题;另一个是 answer(对象),默认为{},如果设置了某个问题的答案,该问题将被跳过。执行结果是个 promise 对象。问题的类型有很多种,在此我只是展示了常见的 input。

在 cli.js 文件中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var inquirer = require('inquirer');
inquirer.prompt([
/* 设置问题 */
{
type: 'input',
name: 'author',
message: 'author',
default: '',
},
{
type: 'input',
name: 'version',
message: 'version',
}
],{version: '1.0.1'})
.then((answers) => {
// 完成询问,执行回调,在此可对询问结果做一些保存等的操作
console.log(answers);
})
.catch((error) => {
if (error.isTtyError) {
//在当前环境中无法呈现
console.log('在当前环境中无法呈现');
} else {
// 其他错误
console.log('其他报错',error);
}
});

当设置问题类型为 input 时,遇到个很有意思的现象,就是不管我们输入的是什么类型的数据,node 环境中接收到的始终是字符串类型。因此要对数字进行校验时,不可直接使用 typeof 来判断数字类型,应该用 parseFloat、parseInt 或者正则。

测试执行结果

  1. commander

指令工具,我们可以配置create等等操作指令

安装

1
npm install commander

用法

1
2
3
4
5
6
program
.command('clone <source> [destination]')
.description('clone a repository into a newly created directory')
.action((source, destination) => {
console.log('clone command called');
});
  1. handlebars、ejs

模板替换工具(预编译工具)

模板工具有很多种根据自己的想法选择合适的即可。

  1. axios

异步请求工具

这个不必过多解释,vue 中常用。我们要想下载远程仓库中模板文件,必然是需要先请求地址的,去获取远程的模板信息(模板文件名/模板版本信息等),之后我们进行选择之后,方可下载文件

  1. download-git-repo

下载远程文件工具

利用该工具,我们便可以根据 git 地址下载我们所需要的的初始化模板文件。

  1. ora

下载提示工具

安装

1
npm install ora

使用

1
2
3
4
5
6
import ora from "ora";
const spinner = ora("fetch template......");

spinner.start(); // 开始加载loading

spinner.succeed(); // 结束加载loading

在使用的时候,遇到了这么个问题如下:

其实就是 node.js 在引入 es6 模块时与其 common.js 规范冲突。所以只需解决这个问题,就自然而然解决上了上面的问题了。

我们可以检测下当前的 node 环境对 es6 的支持情况:

安装 es-checker

1
npm -g install es-checker

在执行命令

1
es-checker

可以看到以下结果,虽然对 ES6 的支持度很高,但是对 ES6 的模块显示是不支持的。

解法一:

增设 package.json 中的信息:
type:“module”。我试了下,并没有解决问题。

解法二:

降低 ora 的版本,安装ora@4.0.4版本。这个办法应该是最简单粗暴的了。既然最新的版本是以 ES6 的模块的形式导出,那么我们将版本降至符合 common.js 规范的版本即可。

1
npm install ora@4.0.4

与此同时,我们将修改 ora 的引入方式。

1
2
3
- import ora from 'ora';
+ const ora = require('ora')

解法三:

配置babel,众所周知 babel 是是一个 JavaScript 编译器,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

配置不恰当 不生效 待完善!

这块我真的挺苦恼的,尝试了很久还是没有很好的处理,同时也没有大牛指导

脚手架搭建流程

可将脚手架搭建分成 3 种流程。

第一种:

1. 初始化脚手架

2. 设置操作指令

安装项目指令等等

3. 拉取远程仓库中的项目模板

便于及时更新,同时可以使得项目更加工程化、标准化。
针对不同类型的项目设置不同的模板,例如移动端 H5 项目及 PC 端项目;又或者是针对不同前端框架的模板,例如针对 react 或者 vue

4. 发布至npm

安装发布了的脚手架可以快速搭建项目,大大减少项目搭建的时间。

第二种:

1. 初始化脚手架

2. 设置操作指令

3. 根据用户的输入,配置初始化信息

设置项目的个性化信息。比如项目名称,创建者等

4. 根据用户的选择,配置功能插件

根据项目的大小以及类型。搭配对应的功能插件

5. * 拓展脚手架的功能

实现前端自动化测试等功能

6. 发布至npm

第三种

1. 初始化脚手架

2. 设置操作指令

3. 拉取远程仓库中的项目模板

4. 根据用户的输入,配置初始化信息

5. 根据用户的选择,配置功能插件

6. * 拓展脚手架的功能

7. 发布至npm

具体实现及代码展示

1.初始化脚手架

  1. 创建工程文件夹 yu-cli。执行 npm init,创建 package.json 文件。
  2. 创建入口文件 cli.js。
1
type nul>cli.js
  1. 修改 package.json。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "ywc-cli", // 自定义
"version": "1.0.0",
"description": "",
"main": "cli.js",
+ "bin": "cli.js",// 入口文件
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "ywc",// 自定义
"license": "ISC"
}

  1. 测试初始化脚手架能否运行。

给 cli.js 添加测试代码

1
2
3
#!/usr/bin/env node  // 这行必须得加,否则以下执行会报错。作用:当系统运行到这一行的时候,去 env 中查找 node 配置,并且调用对应的解释器来运行之后的 node 程序

console.log('my cli is working!!!');

执行 npm link。正确操作的情况下,我们在控制台执行 yu-cli,可看到打印信息

1
my cli is working!!!

如果没加这行代码 #!/usr/bin/env node 而导致报错,可在添加后 执行 npm link –force 强制覆盖。

2.设置指令

配置指令信息

1
2
3
4
5
6
7
8
9
10
11
12
const mapActions = {
create: {
alias: "c",
description: "create a project",
examples: ["ywc-cli create <project-name>"],
},
"*": {
alias: "",
description: "command not found",
examples: [],
},
};

如果执行的指令就提示 command not found,反之执行对应的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Reflect.ownKeys(mapActions).forEach((action) => {
program
.command(action) // 配置指令名字
.alias(mapActions[action].alias) // 指令别名
.description(mapActions[action].description) // 指令描述
.action(() => {
if (action === "*") {
console.log(mapActions[action].description);
} else {
// ywc-cli create xxx [node, ywc-cli, create, xxx]
require(path.resolve(__dirname, "lib", action))(
...process.argv.slice(3)
);
}
});
});

3.设置可选模板

这里我是知道自己在远程配置了两种模板vue2template, vue3template,所以可以静态地配置,只不过这样每次新增模板需要更改源码。当然也可以通过 github 的 api 通过异步请求的方式获取模板类型,动态获取。

1
2
3
4
5
6
7
8
9
10
// 预设 模板名
let tags = ["vue2template", "vue3template"];
const { tag } = await inquirer.prompt([
/* 设置问题 */ {
type: "list",
name: "tag",
message: "please choose a template to create project",
choices: tags,
},
]);

4.下载模板

通过上一步选择的模板名,我们便可以通过download-git-repo来下载远程仓库中的项目模板。在此之前先封装下 loading

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ora = require("ora");

// 封装loading
const waitFnLoading = async (fn, message, ...args) => {
const spinner = ora(message);
spinner.start(); // 开始加载loading
let repos = await fn(...args);
spinner.succeed("succeed !"); // 结束加载loading
return repos;
};

module.exports = {
waitFnLoading: waitFnLoading,
};

下载并生成文件

1
2
3
4
5
6
7
8
9
// 从远程仓库下载模板
// tag: 模板名; proName:项目名; sign:是否作为临时文件
const downloadTemplate = async (tag, proName, sign) => {
let api = `heywc/${tag}`;
let pro = sign ? `${downloadDirectory}/${tag}` : proName;
let repo = await downLoadGitRepo(api, pro);
// 后续版本会使用
return repo;
};

关于下个版本

新增功能就是根据用户选择安装对应插件

安装配置vuex为例:

在脚手架中配置 vuex 相关的的初始化模板文件,待用户确定安装 vuex 后,将其写入待生成的项目中。

  1. 配置 vuex.js 模板
1
2
3
4
5
6
7
8
9
10
11
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {},
});
  1. 配置问题项及生成 vuex 关键代码:

这里默认 true 即同意使用 vuex,通过 fs.readFileSync 读取 temp/vuex.js 模板文件,使用 fs.writeFile 写入项目中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
inquirer
.prompt([
{
type: "confirm",
name: "vuex",
message: "是否使用vuex",
default: true,
},
])
.then((answer) => {
if (answer.vuex) {
// 读取模板
const data = fs.readFileSync(path.resolve(__dirname, "temp/vuex.js"));
// 创建store文件夹
fs.mkdirSync(path.resolve(__dirname, "../test/src/store"));
// 根据模板文件创建index.js
fs.writeFile(
path.resolve(__dirname, "../test/src/store/index.js"),
data,
(err) => {
if (err) {
console.log("失败");
} else {
console.log("成功");
}
}
);
}
});

这里我只是简单展示通过 fs 的读写进行配置插件,当然也可以使用模板引擎进行渲染。


写到这,代表文章要收尾了。目前只能说最简易版的一代ywc-cli告一段落,等新东家找到后,我会继续更新版本…

安装包已经发布到 npm 上了,可以直接安装使用,同时源码也分享到了 github 源码很简单,感兴趣的小伙伴可以拉下来看看,,愿共勉!^_^