背景
- 新建一个项目的时候要从零开始,重新配置一大堆东西,过于重复冗杂;
- 实际开发中有很多业务功能,文件是固定要建的,比如页面的layout布局,路由权限校验等......
- 当前已经把公共部分抽离生成了一个git项目,每次新建项目都需要创建两个远程仓库去拉取模板代码,再推送到origin远程仓库,过程过于繁琐。
基于解放生产力,减少重复工作,提高效率的原则。如果我们把模板做成一个脚手架,只要运行一下如vue-cli这样的语句就能帮我们下载模板 安装需要的依赖,这样就会省去很多繁琐的工作。
准备工作
- 将公共部分抽离出来,建一个git项目单独维护
- 知识储备
- commander 命令编写
- chalk 让输出带颜色
- download-git-repo 下载git模板
- inquirer 命令行交互,获取用户输入
- ora 进度条
- fs-extra 文件相关操作工具库
- mem-fs 操作模板文件工具库
- mem-fs-editor 操作模板文件工具库
效果
- 执行init命令
- 输入项目名称与描述
- 选择模板
- 下载模板
- 初始化git仓库
- 安装项目依赖
实现
创建项目
npm init 创建 package.json,添加 bin 命令
"bin": {
"project": "bin/project",
"project-init": "bin/project-init"
}
主体流程
创建一个bin目录,在目录里面创建project
#! /usr/bin/env node
const program = require('commander');
program
.usage('<command> [options]') // 用户使用提示
.command('init [name]', 'init a project') // 如果没有action 会在同目录下找x-init文件执行
.parse(process.argv)
处理init命令
在bin目录中创建project-init
#! /usr/bin/env node
const program = require('commander');
const version = require('../package.json').version
program
.version(version, "-v, --version")
.option('--name [name]', 'project-name')
.parse(process.argv)
const { name } = program
const args = program.args // project init [name] 带过来的name参数
const projectName = args[0] || name
主体流程
1. 简单问询
const inquirer = require('inquirer')
const fse = require('fs-extra')
let prompts = []
if (typeof projectName !== 'string') {
prompts.push({
type: 'input',
name: 'projectName',
message: '请输入项目名称:',
validate(input) {
if (!input) {
return '项目名称不能为空'
}
if (fse.existsSync(input)) {
return '已经存在同名项目,请重新输入项目名'
}
return true
}
})
} else if (fse.existsSync(projectName)) {
prompts.push({
type: 'input',
name: 'projectName',
message: '请输入项目名称:',
validate(input) {
if (!input) {
return '项目名称不能为空'
}
if (fse.existsSync(input)) {
return '已经存在同名项目,请重新输入项目名'
}
return true
}
})
}
// 项目描述
prompts.push({
type: 'input',
name: 'description',
message: '请输入项目描述'
})
// 选择模板
prompts.push({
type: 'list',
message: '请选择要下载的模板',
name: 'template',
choices: [
{
name: "PC",
value: "PC-master"
}, {
name: "mobile",
value: "mobile-master"
}
]
})
inquirer.prompt(prompts).then(answer => {
// answer 为用户输入的内容
// 问询之后的操作,一般为下载模板
})
2. 下载模板
const path = require('path');
const ora = require('ora')
const projectPath = path.join(process.cwd(), projectName)
const downloadPath = path.join(projectPath, '__download__')
const spinner = ora('正在从gitlab下载template')
spinner.start()
const sourcePath = `${模板的git地址}#${ answer.template}`
download(sourcePath, downloadPath, { clone: true }, err => {
if (err) {
// 可以输出一些项目失败的信息
spinner.color = 'red';
spinner.fail(err);
console.log(err)
return
}
spinner.color = 'green';
spinner.succeed('下载成功');
})
此时下载的文件是在内存中的临时文件,还不能对其进行操作。我们还需要把临时文件复制下来,提交到磁盘中,真正的保存下来。
3. 保存文件
const memFs = require('mem-fs');
const editor = require('mem-fs-editor');
const INJECT_FILES = ['package.json']
//复制文件
function getDirFileName (dir) {
try {
const files = fse.readdirSync(dir)
const fileToCopy = []
files. forEach(file => {
if (file.indexOf(INJECT_FILES) > -1) return
fileToCopy.push(file)
})
return fileToCopy
} catch (e) {
return []
}
}
const store = memFs.create()
let memFsEditor = editor.create(store)
function injectTemplate(source, dest, data) {
memFsEditor.copyTpl( source, dest, data )
}
const copyFiles = getDirFileName(downloadPath) // 要复制的文件
copyFiles.forEach((file) => {
fse.copySync(path.join(downloadPath, file), path.join(projectPath, file))
console.log(`${chalk.green('✔ ')}${chalk.green(`创建${projectName}/${file}`)}`)
})
INJECT_FILES.forEach(file => {
injectTemplate(path.join(downloadPath, file), path.join(projectName, file), {
projectName,
description
})
})
// 将内存中的文件操作,全部提交到磁盘
memFsEditor.commit(() => {
INJECT_FILES.forEach(file => {
console.log(`${chalk.green('✔ ')}${chalk.green(`创建${projectName}/${file}`)}`)
})
// 删除临时文件
fse.remove(downloadPath)
process.chdir(projectPath)
4. git init && 安装依赖
const { exec } = require('child_process')
const path = require('path');
// git 初始化
const gitInitSpinner = ora(`cd ${chalk.green.bold(projectName)} 目录, 执行 ${chalk.green.bold('git init')}`)
gitInitSpinner.start()
const gitInit = exec('git init')
gitInit.on('close', code => {
if (code === 0) {
gitInitSpinner.color = 'red'
gitInitSpinner.succeed(gitInit.stdout.read())
} else {
gitInitSpinner.color = 'red'
gitInitSpinner.fail(gitInit.stderr.read())
}
// 安装依赖
const installSpinner = ora(`安装项目依赖 ${chalk.green.bold('npm install')}, 请稍后...`)
installSpinner.start()
exec('npm install', (error, stdout, stderr) => {
if (error) {
installSpinner.color = 'red'
installSpinner.fail(chalk.red('安装项目依赖失败,请自行重新安装!'))
console.log(error)
} else {
installSpinner.color = 'green'
installSpinner.succeed('安装成功')
console.log(`${stderr}${stdout}`)
console.log(chalk.green('创建项目成功!'))
console.log(chalk.green('输入npm run serve启动项目'))
}
})
})
本地调试
添加了bin命令之后,需要执行npm link将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试