Vue.js 是目前比较流行的前端框架之一,那么开发一个基于 Vue.js 的组件是每个前端的心愿。在每次开发新的 Vue.js 组件的时候,都会做的事情有下面几项:
- 创建项目目录
- git 初始化
- npm 初始化
- 搭建开发环境
- 语法检测
- 代码编译
- 代码打包
- 本地预览及热重载
- 搭建生产环境
- 语法检测
- 代码编译
- 代码打包及压缩
- 持续集成的配置
那么为什么不把这些工作有效的提炼出来,让他一键生成呢?目前一键生成的工具有很多,如 yeoman 等。yeoman 搭建项目需要提供 yeoman 的脚手架包 。 yeoman 的脚手架包本质上就是一个具备完整文件结构的项目样板,用户需要手动地把这些脚手架包下载到本地,然后 yeoman 就会根据这些脚手架包自动生成各种不同的项目。
yeoman 固然好用,但总是多了一步很是麻烦,还得下载脚手架包。市面上还流行 cli 技术,就是针对远程仓库的模板根据一些配置拉取到本地。显然 cli 的模式很好,不用下载就可以。我们依据这个原理自己搭建了一款叫做 fecli 的脚手架。
技术栈
- 开发环境: OS X El Capitan 10.11.6
- 开发工具: Atom
- Node.js:整个脚手架的运行环境。本脚手架的 Node.js 版本: 9.11.1 。
- es6: JavaScript 的新语法。
- commander:TJ大神开发的工具,能够更好地组织和处理命令行的输入。本脚手架的 commander 版本: 2.15.1 。
- co:异步流程控制工具。本脚手架的 co 版本: 4.6.0 。
- co-prompt:分步接收用户的输入。本脚手架的 co-prompt 版本: 1.0.0 。
- chalk:色彩丰富的终端工具。本脚手架的 chalk 版本: 2.3.2 。
- ora:典雅的终端微调器,可以控制终端输出。本脚手架的 ora 版本: 2.0.0 。
项目核心
一张图说明整体的架构。 ⤵️
关于模板
模板 就是未来拉取下来的东西。这个模板里往往会有一些环境的配置,语法检测的配置,单元测试的配置,持续集成的配置等。
模板的相关信息会存放在 templates.json 文件中。用户也可以通过一些命令操作 templates.json 中的内容。
脚手架的文件结构
. ├── bind/ # 运行命令的入口文件 │ └── ... ├── lib/ # 核心代码 │ ├── table.js # 模板列表表格形式的封装 │ ├── tip.js # 终端提示信息的封装 │ ├── cli/ # 命令管理 │ │ └── ... │ └── cmd/ # 命令操作 │ │ └── ... ├── public/ # 命令预览 │ └── ... └── templates.json # 模板管理
配置全局使用
新建一个目录 mkdir fecli
,并进入 cd fecli
。然后 npm 初始化一下 npm init
。 为了可以全局使用,我们需要在 package.json 里面设置一下:
"bin": {
"fe": "./bin/fe.js"
},
本地调试的时候,在项目根目录下执行: npm link
。
即可把 fecli 命令绑定到全局,以后就可以直接以 fe 作为命令开头。
入口文件的设置
在 package.json 里面写入依赖并执行 npm install
或者 yarn install
:
"dependencies": {
"chalk": "^2.3.2",
"co": "^4.6.0",
"co-prompt": "^1.0.0",
"commander": "^2.15.1",
"ora": "^2.0.0"
}
在根目录下建立 \bin 文件夹,在里面建立一个 fe.js
文件。这个 bin/fe.js 文件是整个脚手架的入口文件,所以我们首先对它进行编写。
首先是一些初始化的代码,很简单就是引用了一下命令管理的文件(lib/cli/index.js):
require('../lib/cli/');
命令管理 (lib/cli/index.js)
首先是一些初始化的事情:
const program = require('commander');
const packageInfo = require('../../package.json');
program
.version(packageInfo.version)
我们通过 commander 来设置不同的命令。 command 方法是设置命令的名字。 description 方法是设置描述。 alias 方法是设置简写。 action 方法是设置回调。
program
.command('init') // fe init
.description('生成一个项目')
.alias('i') // 简写
.action(() => {
require('../cmd/init')();
});
program
.command('add') // fe add
.description('添加新模板')
.alias('a') // 简写
.action(() => {
require('../cmd/add')();
});
program
.command('list') // fe list
.description('查看模板列表')
.alias('l') // 简写
.action(() => {
require('../cmd/list')();
});
program
.command('delete') // fe delete
.description('查看模板列表')
.alias('d') // 简写
.action(() => {
require('../cmd/delete')();
});
如果没有参数,运行帮助方法。接下来是解析 program.args 中的参数:
program.parse(process.argv);
if(!program.args.length){
program.help()
}
运行 fe
之后的结果:
commander 的具体使用方法在这里就不展开了,可以直接到官网去看详细的文档。
处理用户输入
在项目根目录下建立 /lib/cmd 文件夹,专门用来存放命令处理文件。
在根目录下建立 templates.json 文件并写入如下内容,用来存放模版信息:
{"tpl":{}}
添加模板 (fe add
)
进入 /lib/cmd 并新建 add.js 文件。
'use strict'
const co = require('co');
const prompt = require('co-prompt');
const fs = require('fs');
const table = require('../table');
const tip = require('../tip');
const tpls = require('../../templates');
const writeFile = (err) => {
// 处理错误
if (err) {
console.log(err);
tip.fail('请重新运行!');
process.exit();
}
table(tpls);
tip.suc('新模板添加成功!');
process.exit();
};
const resolve = (result) => {
const { tplName, gitUrl, branch, description, } = result;
// 避免重复添加
if (!tpls[tplName]) {
tpls[tplName] = {};
tpls[tplName]['url'] = gitUrl.replace(/[\u0000-\u0019]/g, ''); // 过滤unicode字符
tpls[tplName]['branch'] = branch;
tpls[tplName]['description'] = description;
} else {
tip.fail('模板已经存在!');
process.exit();
};
// 把模板信息写入templates.json
fs.writeFile(__dirname + '/../../templates.json', JSON.stringify(tpls), 'utf-8', writeFile);
};
module.exports = () => {
co(function *() {
// 分步接收用户输入的参数
const tplName = yield prompt('模板名字: ');
const gitUrl = yield prompt('Git https 链接: ');
const branch = yield prompt('Git 分支: ');
const description = yield prompt('模板描述: ');
return new Promise((resolve, reject) => {
resolve({
tplName,
gitUrl,
branch,
description,
});
});
}).then(resolve);
};
删除模板 (fe delete
)
进入 /lib/cmd 并新建 delete.js 文件。
'use strict'
const co = require('co');
const prompt = require('co-prompt');
const fs = require('fs');
const table = require('../table');
const tip = require('../tip');
const tpls = require('../../templates');
const writeFile = (err) => {
if (err) {
console.log(err);
tip.fail('请重新运行!');
process.exit();
}
tip.suc('新模板删除成功!');
if (JSON.stringify(tpls) !== '{}') {
table(tpls);
} else {
tip.info('还未添加模板!');
}
process.exit();
};
const resolve = (tplName) => {
// 删除对应的模板
if (tpls[tplName]) {
delete tpls[tplName];
} else {
tip.fail('模板不经存在!');
process.exit();
}
// 写入template.json
fs.writeFile(__dirname + '/../../templates.json', JSON.stringify(tpls), 'utf-8', writeFile);
};
module.exports = () => {
co(function *() {
// 分步接收用户输入的参数
const tplName = yield prompt('模板名字: ');
return new Promise((resolve, reject) => {
resolve(tplName);
});
}).then(resolve);
};
初始化项目 (fe init
)
进入 /lib/cmd 并新建 init.js 文件。
'use strict'
// 操作命令行
const exec = require('child_process').exec;
const co = require('co');
const ora = require('ora');
const prompt = require('co-prompt');
const tip = require('../tip');
const tpls = require('../../templates');
const spinner = ora('正在生成...');
const execRm = (err, projectName) => {
spinner.stop();
if (err) {
console.log(err);
tip.fail('请重新运行!');
process.exit();
}
tip.suc('初始化完成!');
tip.info(`cd ${projectName} && npm install`);
process.exit();
};
const download = (err, projectName) => {
if (err) {
console.log(err);
tip.fail('请重新运行!');
process.exit();
}
// 删除 git 文件
exec('cd ' + projectName + ' && rm -rf .git', (err, out) => {
execRm(err, projectName);
});
}
const resolve = (result) => {
const { tplName, url, branch, projectName, } = result;
// git命令,远程拉取项目并自定义项目名
const cmdStr = `git clone ${url} ${projectName} && cd ${projectName} && git checkout ${branch}`;
spinner.start();
exec(cmdStr, (err) => {
download(err, projectName);
});
};
module.exports = () => {
co(function *() {
// 处理用户输入
const tplName = yield prompt('模板名字: ');
const projectName = yield prompt('项目名字: ');
if (!tpls[tplName]) {
tip.fail('模板不存在!');
process.exit();
}
return new Promise((resolve, reject) => {
resolve({
tplName,
projectName,
...tpls[tplName],
});
});
}).then(resolve);
}
显示模板列表 (fe list
)
进入 /lib/cmd 并新建 list.js 文件。
'use strict'
const table = require('../table');
module.exports = () => {
table(require('../../templates'));
process.exit();
};
现在我们的 fecli 脚手架工具已经搭建好了,一起来尝试一下吧!
使用测试
fe add
添加模板
fe delete
添加模板
fe list
添加模板
fe init
添加模板
项目源码
更多源码信息请移步 fecli 。