前端自定义cli
Windows PowerShell
版权所有 (C) Microsoft Corporation。保留所有权利。
尝试新的跨平台 PowerShell https://aka.ms/pscore6
PS C:\Users\admin> hello
hello world在使用vue-cli的过程中,常用的webpack模板只为我们提供最基础的内容,但每次需要新建一个项目的时候就需要把之前项目的一些配置都搬过来,这样就造成挺大的不方便,如果是作为一个团队,那么维护一个通用的模板,我认为是挺有必要的。
前置知识-可执行模块
npm上边不仅仅存在一些用来打包、引用的第三方模块,还有很多优秀的工具(包括部分打包工具),他们与上边提到的模块的区别在于,使用npm install XXX以后,然后就可以通过命令行直接运行了,如各种脚手架工具、webpack命令等等。
举个例子
hello/packge.json
...
"bin": {
"hello": "./bin/index.js"
}
...
hello/bin/index.js配置
#!/usr/bin/env node
,用于指明该脚本文件要使用node来执行。同时也解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件。
还可以指定为 #!/usr/bin/env bash
#!/usr/bin/env node
console.log("hello world");
发包之后,npm install demo -g。
打开cmd,输入hello按下回车
Windows PowerShell
版权所有 (C) Microsoft Corporation。保留所有权利。
尝试新的跨平台 PowerShell https://aka.ms/pscore6
PS C:\Users\admin> hello
hello world
npm link的使用
在开发阶段,为了验证效果,不可能没改一次都要上传发版。
我们可以npm lin命令 创建全局模块一个连接,指向当前目录
这样每次改动 就会同步到全局
npm link用来在本地项目和本地npm模块之间建立连接,可以在本地进行模块测试
具体用法:
-
项目和模块在同一个目录下,可以使用相对路径
npm link ../module -
项目和模块不在同一个目录下
cd到模块目录,npm link,进行全局link
cd到项目目录,npm link 模块名(package.json中的name) -
解除link
解除项目和模块link,项目目录下,npm unlink 模块名
解除模块全局link,模块目录下,npm unlink 模块名
前置知识-inquirerJs
它是一个一个NodeJs交互式命令行工具。
力争成为Node.js的易于嵌入且美观的命令行界面。
提供用户界面和查询会话流。
#!/usr/bin/env node
const inquirer = require('inquirer');
inquirer
.prompt([{
type: "input",
name: "name",
message: "项目名称:",
default: "hello-word"
}])
.then(answers => {
console.log(answers);
})
PS C:\Users\admin> hello
? 项目名称: 测试
前置知识-shelljs
点此进入官方文档
它是Node下的脚本语言解析器,具有丰富且强大的底层操作(Windows/Linux/OS X)权限。
用js代码中编写shell命令实现功能。
它的底层实现是利用‘Nodejs下引入模块child_process实现调用shell’
child_process模块是nodejs的一个子进程模块,可以用来创建一个子进程,并执行一些任务,比如shell命令
原生执行shell
const childProcess = require('child_process');
childProcess.exec('dir', (err, sto)=> {
console.log(err);
console.log(sto);
})
PS C:\Users\admin\Desktop\hello> node .\bin\index.js
C:\Users\admin\Desktop\hello
2021/01/25 14:48.
2021/01/25 14:48..
2021/01/25 14:29bin
2021/01/25 14:48node_modules
2021/01/25 14:29 68 package-lock.json
2021/01/25 14:48 315 package.json
2021/01/25 14:48 10,172 yarn.lock
shelljs执行shell
const shell = require('shelljs');
shell.exec('dir');
效果和原生一样
不过为了抹除平台差异,shell自己也封装了一些方法,比如ls
const shell = require('shelljs');
shell.ls('-l', 'C://Users/admin/Desktop/hello').forEach((file)=>{
console.log(file.name);
})
前置知识-chalkJs
一般情况下我们给log加颜色,写起来有点费劲。比如说
console.log("%c%s", "color: deeppink; font-size: 18px;", "hello World")
const chalk = require('chalk');
console.log(chalk.blue('Hello world!'));
思路
我们先创建一个通用模板
模板里某个文件夹放一些备用资源
通过inquirerJs来收集用户的输入,
然后通过shelljs来下载模板
通过手机用户的信息来修改模板内容。
修改的内容大致有以下信息:
全局替换用户输入的项目名字
根据用户选择的三方包,来动态修改packge.json和src相关信息
代码
首先创建一个项目,结构如下
配置入口文件
packge.json
"bin": {
"aegis": "./bin/index.js"
}
主程序
index.js如下
#!/usr/bin/env node
const inquirer = require('inquirer');
const shell = require('shelljs');
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
//当前cli的名称
console.log(chalk.yellowBright.bold('🌟---------------------------------------🌟\n 👏 欢迎使用aegis-cli 👏 \n🌟---------------------------------------🌟'));
const qs = [
{
type: "input",
name: "name",
message: "项目名称:",
default: "hello-word"
},
{
type: 'list',
name: 'type',
message: '创建项目类型:',
choices: [
"pc",
"mobile"
]
}, {
type: "checkbox",
message: "选择依赖库:",
name: "lib",
choices: [
{
name: "vuex"
},
{
name: "vue-router"
}
]
}, {
type: "confirm",
message: "是否需要我帮你安装依赖(可能会比较慢)?",
name: "autoInstall",
default: false
}];
inquirer
.prompt(qs)
.then(answers => {
console.log(answers);
// shell.exec(`git clone ${tempUrl}`,
console.log(chalk.greenBright(
'-----项目信息-----'+'\n'+
'名称:' + answers.name + '\n'+
'类型:' + answers.type + '\n'+
'三方依赖库:' + answers.lib + '\n'+
'自动安装依赖:' + answers.autoInstall
));
inquirer
.prompt([
{
type: "confirm",
message: "您的项目配置如上,是否开始生成项目?",
name: "start",
default: true
}
])
.then(answers2 => {
if(!answers2.start){
return false;
}
const tempName = answers.type === 'pc' ? 'aegis-pc-template' : 'aegis-pc-template';
const tempUrl = `http://git2.aegis-info.com/fe-resources/${tempName}.git`;
const tempPath = path.resolve(`./${tempName}`);
const packgePath = `${tempPath}/package.json`;
const readMePath = `${tempPath}/README.md`;
const rootPath = path.resolve('./');
const finalProjectPath = path.resolve('./' + answers.name);
shell.rm('-rf', tempPath);
shell.rm('-rf', finalProjectPath);
shell.exec(`git clone ${tempUrl}`, null, () => {
// 处理pc或移动端的库
const handleUi = () => {
const data = fs.readFileSync(path.resolve(tempPath + '/src/main.ts'), 'utf8').split('\n')
if (answers.type === 'pc') {
data.splice(2, 0, `import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);`)
// 添ui依赖
shell.sed('-i', `"dependencies": {`, `"dependencies": {
"element-ui": "^2.15.0",`, packgePath);
} else {
data.splice(2, 0, `import Vant from 'vant';
import 'vant/lib/index.css';
Vue.use(Vant);`)
// 添ui依赖
shell.sed('-i', `"dependencies": {`, `"dependencies": {
"vant": "^2.9.3",`, packgePath);
}
fs.writeFileSync(path.resolve(tempPath + '/src/main.ts'), data.join('\n'), 'utf8');
}
// 如果选择了vuex
const handleVuex = () => {
shell.mv(path.resolve(tempPath + '/multiple-version/store'), path.resolve(tempPath + '/src'));
// shell.sed('-i', 'render: h => h(App)', 'render: h => h(App) store', path.resolve(tempPath+'/src/main.ts'));
const data = fs.readFileSync(path.resolve(tempPath + '/src/main.ts'), 'utf8').split('\n')
data.splice(2, 0, `import store from './store'`)
data.splice(8, 0, ` store`)
fs.writeFileSync(path.resolve(tempPath + '/src/main.ts'), data.join('\n'), 'utf8');
}
// 如果选择了router
const handleRouter = () => {
shell.mv(path.resolve(tempPath + '/multiple-version/router'), path.resolve(tempPath + '/src'));
shell.mv(path.resolve(tempPath + '/multiple-version/views'), path.resolve(tempPath + '/src'));
shell.mv(path.resolve(tempPath + '/multiple-version/App.vue'), path.resolve(tempPath + '/src'));
const data = fs.readFileSync(path.resolve(tempPath + '/src/main.ts'), 'utf8').split('\n')
data.splice(2, 0, `import router from './router'`)
data.splice(8, 0, ` router`)
fs.writeFileSync(path.resolve(tempPath + '/src/main.ts'), data.join('\n'), 'utf8');
}
handleUi();
if (answers.lib.indexOf('vuex') > -1) {
handleVuex()
}
if (answers.lib.indexOf('vue-router') > -1) {
handleRouter()
}
// 修改项目名字
shell.sed('-i', tempName, answers.name, packgePath);
shell.sed('-i', tempName, answers.name, readMePath);
shell.mkdir('-p', answers.name);
shell.mv(path.resolve(tempPath + '/*'), path.resolve(answers.name));
shell.mv(path.resolve(tempPath + '/.[^.]*'), path.resolve(answers.name));
shell.rm('-rf', tempPath);
// 删除模板备选
shell.rm('-rf', path.resolve(`${finalProjectPath}/multiple-version`));
shell.rm('-rf', path.resolve(`${finalProjectPath}/.git`));
shell.rm('-rf', path.resolve(`${finalProjectPath}/.git`));
// 帮助安装项目依赖
if (answers.autoInstall) {
console.log(8888);
shell.exec(`cd ${finalProjectPath} && yarn`);
}
})
})
});
helper.js
var fs = require('fs');
var path = require('path');
//往固定的行写入数据
const writeFileToLine = (value)=>{
let basePath = path.resolve('./');
let data = fs.readFileSync(basePath+'/template.appcache', 'utf8').split(/\r\n|\n|\r/gm); //readFileSync的第一个参数是文件名
data.splice(data.length - 5, 0, ...value);
fs.writeFileSync('./manifest.appcache', data.join('\r\n'))
}
module.exports = {
writeFileToLine
}
总结
实现本身并不难,
重点在于要理解所需,然后逐步实现
对于用户而言操作
对于cli而言
使用 CLI 命令,可以让开发人员更编写项目更简单的创建工具,简单来说可以为公司做一个自定义代码生成器。
在次之前,在使用vue-cli的过程中,cli只为我们提供最基础的内容,但每次需要新建一个项目的时候就需要把之前项目的一些配置都搬过来,这样就造成挺大的不方便,如果是作为一个团队,那么维护一个通用的模板,是挺有必要的。
其它
可以使用commanderJs来实现 直接命令行参数交互,这样就可以实现
vue create xxx
但是这个不是必须的