你有没有遇到过在没有 vue-cli、create-react-app 这样子的脚手架的时候一个文件一个文件的去拷贝老项目的配置文件。最近,笔者就在为组里的框架去做一套基本的 cli 工具。通过这边文章,笔者希望大家都能简单的去实现一个属于自己的脚手架工具。
原文链接: https://juejin.im/user/57ac15…
做好准备工作
首先,我们需要去新建一个项目并初始化 package.json
mkdir my-cli && cd my-cli
npm init
然后我们需要在项目中新建 bin 文件夹,并将 package.json 中提供一个 bin 字段并指向我们的 bin 文件夹下,这样通过 npm 我们就可以实现指令的软链了。
“bin”: {
“mycli”: “bin/mycli”
},
在 mycli 中,我们要在头部增加这样一句注释,作用是 ” 指定由哪个解释器来执行脚本 ”。
#!/usr/bin/env node
console.log(‘hello world’);
接下来,全局安装我们这个包,这样我们就可以直接在本地使用 mycli 这个指令了。
sudo npm install -g
提供基本模版
既然我们要去做一个初始化项目的 cli,那么项目模版就必不可少了,笔者在这里提前准备了一个 demo 的项目目录模版,这里就不展开赘述了。
编写逻辑
其实核心逻辑很简单,就是通过控制台获取到用户的一些自定义选项,然后根据选项去从本地或者远程仓库拿到我们提前准备好的模版,将配置写入模版并最后拷贝模版到本地就行了。
我们在 src 下新增 creator.js 文件,这个文件导出一个 Creator 的类。在这个类中现在仅需要三个简单的方法:init 用于初始化、ask 用于和命令行交互获取用户选择输入的数据、write 用于调用模版的构建方法去执行拷贝文件写数据的任务。
class Creator {
constructor() {
// 存储命令行获取的数据,作为 demo 这里只要这两个;
this.options = {
name: ”,
description: ”,
};
}
// 初始化;
init() {}
// 和命令行交互;
ask() {}
// 拷贝 & 写数据;
write() {}
}
module.exports = Creator;
先去完善 init 方法,这个方法里我们仅需要调用 ask 方法和命令行交互并做一些提示即可(可以通过 chalk 这个库去丰富我们的命令行交互色彩)
// …
init() {
console.log(chalk.green(‘my cli 开始 ’));
console.log();
this.ask();
}
// …
接下来是 ask 方法,在这个方法中,我们需要根据提示引导用户输入问题并获取用户的输入,这里用到 inquirer 这个库来和命令行交互。
// …
ask() {
// 问题
const prompt = [];
prompt.push({
type: ‘input’,
name: ‘name’,
message: ‘ 请输入项目名称 ’,
validate(input) {
if (!input) {
return ‘ 请输入项目名称!’;
}
if (fs.existsSync(input)) {
return ‘ 项目名已重复!’
}
return true;
}
});
prompt.push({
type: ‘input’,
name: ‘description’,
message: ‘ 请输入项目描述 ’,
});
// 返回 promise
return inquirer.prompt(prompt);
}
// …
修改刚才的 init 方法,将 ask 方法改为 Promise 调用。
init() {
console.log(chalk.green(‘my cli 开始 ’));
console.log();
this.ask().then((answers) => {
this.options = Object.assign({}, this.options, answers);
console.log(this.options);
});
}
现在我们去命令行试一下,修改 bin/mycli 文件,然后去运行 mycli 命令。
#!/usr/bin/env node
const Creator = require(‘../src/creator.js’);
const project = new Creator();
project.init();
在和用户交互完毕并获取到数据后,我们要做的就是去调用 write 方法执行拷贝构建了。考虑到日后可能增加很多的模版目录,不妨我们将每一类的模版拷贝构建工作放到模版中的脚本去做,从而增大可扩展性,新增 template/index.js 文件。
接下来首先根据项目目录结构创建文件夹 (注意区分项目的执行目录和项目目录的关系)。
module.exports = function(creator, options, callback) {
const {name, description} = options;
// 获取当前命令的执行目录,注意和项目目录区分
const cwd = process.cwd();
// 项目目录
const projectPath = path.join(cwd, name);
const buildPath = path.join(projectPath, ‘build’);
const pagePath = path.join(projectPath, ‘page’);
const srcPath = path.join(projectPath, ‘src’);
// 新建项目目录
// 同步创建目录,以免文件目录不对齐
fs.mkdirSync(projectPath);
fs.mkdirSync(buildPath);
fs.mkdirSync(pagePath);
fs.mkdirSync(srcPath);
callback();
}
然后回到 creator.js 文件,在 Creator 中的 write 调用这个方法。
// …
init() {
console.log(chalk.green(‘my cli 开始 ’));
console.log();
this.ask().then((answers) => {
this.options = Object.assign({}, this.options, answers);
this.write();
});
}
// …
write() {
console.log(chalk.green(‘my cli 构建开始 ’));
const tplBuilder = require(‘../template/index.js’);
tplBuilder(this, this.options, () => {
console.log(chalk.green(‘my cli 构建完成 ’));
console.log();
console.log(chalk.grey(` 开始项目: cd ${this.options.name} && npm install`));
});
}
// …
在开启文件拷贝写数据之前,我们需要用到两个库 mem-fs 和 mem-fs-editor,前者可以帮助我们在内存中创建一个临时的文件 store,后者可以以 ejs 的形式去编辑我们的文件。
现在 constructor 中初始化 store。
constructor() {
// 创建内存 store
const store = memFs.create();
this.fs = memFsEditor.create(store);
this.options = {
name: ”,
description: ”,
};
this.rootPath = path.resolve(__dirname, ‘../’);
this.tplDirPath = path.join(this.rootPath, ‘template’);
}
接下来在 Creator 中增加两个方法 copy 和 copyTpl 分别用于直接拷贝文件和拷贝文件并注入数据。
getTplPath(file) {
return path.join(this.tplDirPath, file);
}
copyTpl(file, to, data = {}) {
const tplPath = this.getTplPath(file);
this.fs.copyTpl(tplPath, to, data);
}
copy(file, to) {
const tplPath = this.getTplPath(file);
this.fs.copy(tplPath, to);
}
然后我们根据 ejs 的语法修改模版中的 package.json 文件以实现数据注入的功能
{
“name”: “<%= name %>”,
“version”: “1.0.0”,
“description”: “<%= description %>”,
“main”: “index.js”,
“scripts”: {},
“author”: “”,
“license”: “ISC”
}
回到 template/index.js 中,对模版中的文件进行相应的拷贝和数据注入操作,最后打印一些可视化的信息。
module.exports = function(creator, options, callback) {
const {name, description} = options;
// 获取当前命令的执行目录,注意和项目目录区分
const cwd = process.cwd();
// 项目目录
const projectPath = path.join(cwd, name);
const buildPath = path.join(projectPath, ‘build’);
const pagePath = path.join(projectPath, ‘page’);
const srcPath = path.join(projectPath, ‘src’);
// 新建项目目录
// 同步创建目录,以免文件目录不对齐
fs.mkdirSync(projectPath);
fs.mkdirSync(buildPath);
fs.mkdirSync(pagePath);
fs.mkdirSync(srcPath);
creator.copyTpl(‘packagejson’, path.join(projectPath, ‘package.json’), {
name,
description,
});
creator.copy(‘build/build.js’, path.join(buildPath, ‘build.js’));
creator.copy(‘page/index.html’, path.join(pagePath, ‘index.html’));
creator.copy(‘src/index.js’, path.join(srcPath, ‘index.js’));
creator.fs.commit(() => {
console.log();
console.log(`${chalk.grey(` 创建项目: ${name}`)} ${chalk.green(‘✔ ‘)}`);
console.log(`${chalk.grey(` 创建目录: ${name}/build`)} ${chalk.green(‘✔ ‘)}`);
console.log(`${chalk.grey(` 创建目录: ${name}/page`)} ${chalk.green(‘✔ ‘)}`);
console.log(`${chalk.grey(` 创建目录: ${name}/src`)} ${chalk.green(‘✔ ‘)}`);
console.log(`${chalk.grey(` 创建文件: ${name}/build/build.js`)} ${chalk.green(‘✔ ‘)}`);
console.log(`${chalk.grey(` 创建文件: ${name}/page/index.html`)} ${chalk.green(‘✔ ‘)}`);
console.log(`${chalk.grey(` 创建文件: ${name}/src/index.js`)} ${chalk.green(‘✔ ‘)}`);
callback();
});
}
执行 mycli 指令创建项目,一个简单的 cli 就完成了。
结语
到此,一个简单的 cli 就制作完成了,大家可以参考 vue-cli、create-react-app 等优秀的 cli 适当的扩展自己的 cli 工具。