你有无遇到过在没有vue-cli、create-react-app这模样的脚手架的时刻一个文件一个文件的去拷贝老项目的设置文件。近来,笔者就在为组里的框架去做一套基础的cli东西。经由过程这边文章,笔者愿望人人都能简朴的去完成一个属于本身的脚手架东西。
做好准备事情
起首,我们须要去新建一个项目并初始化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东西。