平时在工作中我们都会用一些脚手架工具来快速构建项目的原型,比如Vue的vue-cli,React的create-react-app等;但是因为这些脚手架是通用型的,少不了还要对其进行二次封装,比如配置CSS、配置网络请求axios、配置UI库等等;既然这样的话那就有必要根据自己的需求来定制属于自己的脚手架。
项目效果
项目核心依赖
- commander 命令行工具
- download-git-repo git仓库代码下载
- chalk 命令行输出样式美化
- Inquirer.js 命令行交互
- ora 命令行加载中效果
- cross-spawn 执行依赖安装命令
相关依赖的用法,请到对应的github仓库查看,我们只会用到最基本的用法,稍微看一下就知道怎么用了。
项目结构
├── bin
│ └── www //可执行文件
├── dist
├── ... //生成文件
└── src
├── index.ts //主流程入口文件
├── init.ts //脚手架初始化
└── utils
├── constants.ts //定义常量
├── install.ts //安装依赖
├── downloadGitRepo.ts //拉取远程模版
└── downloadGitRepo.d.ts // 类型声明
├── .babelrc //babel配置文件
├── tsconfig.json //typescript配置文件
└── package.json //包管理
初始化项目
创建一个空项目(gong-cli)(名字随意,如果准备发布到npm,就不能和别人的重名),使用 npm init -y进行初始化。
gong-cli 命令
node.js 内置了对命令行操作的支持,package.json 中的 bin 字段可以定义命令名和关联的执行文件。在 package.json 中添加 bin 字段
package.json
{
"name": "gong-cli",
"version": "1.0.0",
"description": "a simple cli",
"main": "dist/index.js",
"files": [ // 上传到npm的文件
"bin",
"dist/**/*.js"
],
"bin": {
"gong-cli": "./bin/www"
},
"scripts": {
"compile": "babel src -d dist --extensions '.ts' ",
"watch": "npm run compile -- --watch"
},
}
www
#!/usr/bin/env node
require('../dist/index.js'); // 通过babel把ts编译成js再运行
在当前 gong-cli 的根目录下执行 npm link,将 gong-cli 命令链接到全局环境;这样在开发环境中,就可以运行gong-cli相关命令。
入口文件
./src/index.ts
import commander from 'commander'
import { VERSION } from './utils/constants'
const program = new commander.Command()
program
.command('init <projectName>')
.description('gong-cli init')
.action(() => {
require('./init')(...process.argv.slice(3))
})
program.version(VERSION, '-v --version')
program.parse(process.argv)
在入口文件中我们通过command定义了一个init <projectName>命令,其中projectName由用户自己输入,不能为空;比如:
gong-cli init demo
当用户输入该命令后,即开始加载初始化函数init,传入的参数为字符串demo。
模版初始化
./src/init.ts
import { download } from './utils/downloadGitRepo'
import { install } from './utils/install'
import { promisify } from 'util'
import ora from 'ora'
import inquirer from 'inquirer'
import fs from 'fs'
import chalk from 'chalk'
import path from 'path'
const exist = promisify(fs.stat)
const init = async (projectName: string) => {
const projectExist = await exist(projectName).catch(err => {
// 处理除文件已存在之外的其他错误
if (err.code !== 'ENOENT') {
console.log(chalk.redBright.bold(err))
}
})
// 文件已存在
if (projectExist) {
console.log(chalk.redBright.bold('The file has exited!'))
return
}
// 接收用户命令
inquirer
.prompt([
{
name: 'description',
message: 'Please enter the project description',
},
{
name: 'author',
message: 'Please enter the project author',
},
{
type: 'list',
name: 'language',
message: 'select the develop language',
choices: ['javaScript', 'typeScript'],
},
{
type: 'list',
name: 'package',
message: 'select the package management',
choices: ['npm', 'yarn'],
},
])
.then(async answer => {
// 下载模板 配置相关信息
let loading = ora('downloading template...')
loading.start()
loading.color = 'yellow'
download(projectName, answer.language).then(
async () => {
loading.succeed()
const fileName = `${projectName}/package.json`
if (await exist(fileName)) {
const data = fs.readFileSync(fileName).toString()
let json = JSON.parse(data)
json.name = projectName
json.author = answer.author
json.description = answer.description
fs.writeFileSync(
fileName,
JSON.stringify(json, null, '\t'),
'utf-8',
)
console.log(chalk.green('Project initialization finished!'))
console.log()
console.log(chalk.yellowBright('start install dependencies...'))
// 安装依赖
await install({
cwd: path.join(process.cwd(), projectName),
package: answer.package,
}).then(() => {
console.log()
console.log('We suggest that you begin by typing:')
console.log()
console.log(chalk.cyan(' cd'), projectName)
console.log(` ${chalk.cyan(`${answer.package} start`)}`)
})
}
},
() => {
loading.fail()
},
)
})
}
module.exports = init
进入init初始化函数后,首先判断是否存在和demo同名的文件夹,如果有抛出错误让用户重新输入;然后通过inquirer获取用户在终端输入的信息,这里我们只先获取用户定义的项目描述、项目作者、项目开发语言、项目包管理四个字段。然后通过download去下载对应的模版,下载完模版之后再通过install函数安装依赖。整个过程到这就结束了,还是比较简单的。
拉取远程模版
./src/utils/downloadGitRepo.ts
import downloadGit from 'download-git-repo'
export const download = async (projectName: string, language: string) => {
// 这里先用了巨硬家的react模版
let api = 'microsoft/'
language === 'javaScript'
? (api = api + 'vscode-react-sample')
: (api = api + 'TypeScript-React-Starter')
return new Promise((resolve, reject) => {
downloadGit(api, projectName, (err: any) => {
if (err) {
reject(err)
}
resolve()
})
})
}
安装依赖
./src/utils/install.ts
const spawn = require('cross-spawn')
interface installProps {
cwd: string // 项目路径
package: string //包管理器 yarn 或者 npm
}
export const install = async (options: installProps) => {
const cwd = options.cwd
return new Promise((resolve, reject) => {
const command = options.package
const args = ['install', '--save', '--save-exact', '--loglevel', 'error']
const child = spawn(command, args, {
cwd,
stdio: ['pipe', process.stdout, process.stderr],
})
child.once('close', (code: number) => {
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`,
})
return
}
resolve()
})
child.once('error', reject)
})
}
启动项目
yarn watch
打包发布到npm
运行打包命令yarn compile,生成的文件在dist文件夹下;然后运行npm adduser登录自己的npm账户,最后运行npm publish 将bin和dist两个文件夹的内容发布到npm上。其它用户通过 npm install gong-cli -g 全局安装后, 即可使用 gong-cli 命令安装脚手架。