本篇文章旨在说明一个简单脚手架工具的开发流程和简要的依赖工具,对于功能更人性化,代码健壮性更强的脚手架工具,详细见后续
现在的开发过程中,第一步都是搭建项目目录,像vue-cli, create-react-app, express-generator等脚手架都提供了这样一个功能来帮助开发者创建基础项目目录。而一个稳定的开发团队,技术选型及技术栈相当稳定,每次开发都从新搭建项目目录太过麻烦,则可以通过自己开发一套脚手架的方式来引入自己团队提炼出的项目模板。
通过上述对cli的工作流程分析,接下来我们实现脚手架的步骤大致如下:
rm -rf
来删除文件=================
|__ bin
|__ my-cli # 命令脚本
|__ src
|__ create.js # 初始化命令
|__ list.js # 列出模板
|__ node_modules
|__ package.json
|__ templates.json # 模板列表
通过如上目录结构先创建项目目录
npm init --yes
1. 在bin文件夹下创建my-cli.js 如下,
#! usr/bin/env node
console.log(process.argv)
#!/usr/bin/env node
: 这一行是必须加的,这是告诉系统当前脚本由node.js来解析执行。在这里,我们使用node来作为脚本的解释程序。而我们#! /usr/bin/env node这样写,目的是使用env来找到node,并使用node来作为程序的解释程序。env
:在env中规定很多系统的环境变量,包括我们安装的一些环境的路径等。在不同的操作系统中,我们安装node的路径可能会有所不同,但是其环境变量会存在于env里面,所以,这里我们使用env来找到node,并用node作为解释程序。所以,env的主要目的就是让我们的脚本在不同的操作系统上都能够正常的被解释,启动。
2. 在package.json新增bin字段
"bin":{
"my-cli": "bin/my-cli.js"
}
3. 执行npm link用于调试
npm link
,创建软链接至全局,这样我们就可以全局使用my-cli
命令了,在开发 npm 包的前期都会使用link方式在其他项目中测试来开发,后期再发布到npm上npm link --force
再试一次# 命令行执行
my-cli 123
# 输出如下:
[
‘xxxx\node.exe',
'xxxx\bin\my-cli.js'
‘123’
]
上面步骤可能遇到的问题:
npm link --force
强制覆盖通过上述1.1中步骤已经可以基本实现对简单命令的解析了,为了简化很杂cli参数操作,这里引入
commander
工具(提前npm install安装),关于commander的具体使用,请参考commander文档
#! /usr/bin/env node
const program = require('commander')
const version = require('../package.json').version;
// 定义当前版本
program.version(version, "-v, --version")
// 定义使用方法
program
.command("create " )
.description("使用my-cli创建一个新的模板项目")
.action(()=>{
require('../src/init')
})
program
.command("list")
.description("可用模板列表")
.action(()=>{
require('../src/list')
})
// 必须,用于解析用户命令输入内容
program.parse(process.argv)
if(!program.args.length){
program.help()
}
git clone 128错误
,可能是本地已经存在同名文件,删除后可重试。在template.json中预设了一个项目模板地址
{"admin":"https://gitee.com/cy-edu/cli-template-admin.git"}
const templateUrl = require('../template.json').admin
const download = require('download-git-repo')
// tmp-临时存放下载文件的地址
download(`direct:${templateUrl}`, "tmp", {clone: true},(err) => {
console.log("下载完毕回调");
});
通过上述操作后,执行
my-cli create my-app
可以执行git clone
admin项目模板到本地tmp文件夹中
上述步骤大致实现了从解析命令到下载模板的过程,为了实现根据用户输入项来实现模板内容自定义,这里需要引入
inquirer.js
来实现交互问询功能。使用文档
const templateUrl = require('../template.json').admin
const download = require('download-git-repo')
const inquirer = require("inquirer");
download(`direct:${templateUrl}`, "tmp", {clone: true},(err) => {
console.log("下载完毕回调");
inquirer
.prompt([
{ name: "decription", message: "请输入项目描述" },
{ name: "author", message: "请输入作者" },
])
.then((params) => {
console.log(params);
});
});
如上代码,再次执行
my-cli create my-app
会显示出相应问询内容,并通过回调中的params
拿到用户输入内容
通过上述步骤我们已经可以拿到用户输入的项目描述信息及作者信息了,下一步即可通过
handlebars
模板替换工具来将package.json中对应内容替换为用户输入的内容。关于handlebars的使用文档 请参考这里
// handlebars 基础使用
var source = "Hello, my name is {{name}}. I am from {{hometown}}. I have "
+
"{{kids.length}} kids:" +
"{{#kids}}- {{name}} is {{age}}
{{/kids}}
";
var template = Handlebars.compile(source);
var data = { "name": "Alan", "hometown": "Somewhere, TX",
"kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
var result = template(data);
上述是handlebars的基本使用方法,其通过Mustache模板语法识别到
{{ }}
中的内容后,动态的替换。因此,我们首先要改造一下模板中的package.json
文件,使其符合mustache规范
package.json
。【PS: 这是模板项目中的package.json并不是当前脚手架项目中的哦~】{
"name": "{{ name }}",
"version": "1.0.0",
"description": "{{ description }}",
"author": "{{ author }}",
}
const templateUrl = require('../template.json').admin
const download = require('download-git-repo')
const inquirer = require("inquirer");
// 新增项
const fs = require('fs')
const path = require('path')
const Handlebars = require('handlebars')
const packagePath = path.join(__dirname,'../','/tmp/package.json')
// 注意这里JSON.stringify(data, '','\t') 是为了让最终生成的JSON保留制表符等格式
const content = JSON.stringify(require(packagePath),'','\t')
const template = Handlebars.compile(content)
download(`direct:${templateUrl}`, "tmp", {clone: true},(err) => {
console.log("下载完毕回调");
inquirer
.prompt([
{ name: "name", message: "请输入项目名称" },
{ name: "description", message: "请输入项目描述" },
{ name: "author", message: "请输入作者" },
])
.then((params) => {
const result = template(params);
fs.writeFileSync(packagePath, result);
console.log(params);
});
});
经过上述操作我们已经可以基本的实现一个模板从
命令解析--》模板拉取--》交互问询--》模板信息替换--》生成最终项目
的过程,但也可以发现,上述只是简单的把模板下载到一个固定名为"tmp"的文件夹下,以上有代码也有许多不严谨的地方,因此,接下来做一些简单的优化:
- 美化:加颜色【chalk】,加loading【ora】,加icon【log-symbols】
- 代码健壮性:提示用户输入模板名称,项目名称,用于替换
- 自动命名:下载的模板项目自动以输入的项目名称命名
- 发布到npm
/* 生成最终项目 generator.js*/
// npm i handlebars metalsmith -D
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
module.exports = function(metadata = {}, src, dest = '.'){
if(!src){
return Promise.reject(new Error(`我效的source: ${src}`))
}
return new Promise((resolve, reject)=>{
Metalsmith(process.cwd()) // 在当前目录下执行
.metadata(metadata) // 设置全局元信息-可被应用于所有文件
.clean(false) // 设置是否在写入文件前移除目标路径
.source(src) // 设置资源目录(相对路径)
.destination(dest) // 目标路径
.use((files, metalsmith,done) =>{ // 使用函数插件
const meta = metalsmith.metadata()
// 修改package.json中的内容
Object.keys(files).filter(x=>x.includes('package.json')).forEach(fileName=>{
const t= files[fileName].contents.toString()
files[fileName].contents = Buffer.from(Handlebars.compile(t)(meta), 'utf-8')
})
done()
}).build(err=>{
err ? reject(err):resolve()
})
})
}
#!/usr/bin/env node
const download = require('download-git-repo')
const template = require('../template.json')
const ora = require('ora') // loading效果
const chalk = require('chalk') // 添加颜色
const inquirer = require('inquirer') // 交互命令
const handlebars = require('handlebars') // 元信息替换
const getUser = require('./libs/git-user')
const fs = require('fs')
const generator = require('./libs/generator')
const question = [
{
name: 'templateName',
type: 'input',
message: '请输入模板名称',
validate(val){
if(val===''){
console.log(chalk.yellow('模板名称不能为空'))
console.log(chalk.grey('请通过my-cli list命令查看可用模板'))
return
}else if(!template[val]){
console.log(chalk.red('无此模板,请确认模板名称后重试'))
return
}else{
return true
}
}
},
{
name: 'projectName',
type: 'input',
message: '请输入项目名称',
validate(val){
if(val===''){
console.log(chalk.yellow('项目名称不能为空'))
return
}
return true
}
}
]
inquirer
.prompt(question)
.then(answers=>{
let {templateName, projectName} = answers
const spinner = ora('项目模板下载中...')
const tempUrl = template[templateName]
spinner.start()
download(`direct:${tempUrl}`,projectName,{clone:true}, err=>{
if(err) {
spinner.fail()
console.log(chalk.red(`项目生成失败:${err}`))
return
}
spinner.succeed()
let currentPath = process.cwd()
let packageJSON = require(`${currentPath}/${projectName}/package.json`)
let template = handlebars.compile(JSON.stringify(packageJSON))
let gitUser = getUser()
let metaData = {
name: projectName,
author: gitUser.name,
description: '这是默认项目描述呀'
}
generator(metaData, `${currentPath}/${projectName}`,`${currentPath}/${projectName}`)
})
})
# 1. 登录npm
npm login
# 2. 输入用户名&密码后,发布
npm publish
本篇blog简单的介绍了一个脚手架的基础创建流程,这其中还有很多需要补充优化的地方,参考vue-cli2的源码 可以学习理解更多。后续也将根据vue-cli2的开发方式来出一波更详细的优化篇教程,包括接入github开放式API来自动获取模板仓库中的内容。