写在前面
你可能用到过很多前端脚手架工具,有没有试想过到底如何写一个属于你的脚手架呢?本人之前写了一个简单版的脚手架,如果你刚好也是想了解这块,本文可能对你有帮助,以下代码逻辑的梳理,你也可以去体验一下
npm install duffy-cli -g
流程图
技术栈
- 开发环境: win7
- 开发工具: VScode
- 所需依赖包:
Node.js:整个脚手架的运行环境。本脚手架的 Node.js 版本: v8.11.1 。
es6: JavaScript 的新语法。
commander:TJ大神开发的工具,能够更好地组织和处理命令行的输入。
chalk:色彩丰富的终端工具。
ora:优雅的终端微调器,可以控制终端输出,这里主要用于命令行上的加载效果。
download-git-repo:用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket
handlebars: 知名的模板引擎
ini: 节点的ini格式解析器和序列化器
inquirer: 用于命令行与开发者交互
metalsmith: 静态网站生成器
request: 发送http请求的工具。
rimraf: 相当于UNIX的“rm -rf”命令
semver: 版本号处理工具
user-home: 用于获取用户的根目录
babel-preset-env: 会根据目标环境选择不支持的新特性来转译
项目搭建
初始化项目
创建项目目录后执行npm init按照提示完成初始化项目。
配置全局使用
为了可以全局使用,我们需要在 package.json 里面设置一下:
"bin": {
"dfcli": "./bin/www"
},
本地调试的时候,在项目根目录下执行: npm link 。 即可把 duffy-cli 命令绑定到全局,以后就可以直接以 dfcli 作为命令开头
安装依赖
依赖为上面的依赖包,
npm install semver rimraf .....
入口文件的设置
当前项目文件夹新建bin文件夹创建www.js文件
#! /usr/bin/env node
require('../dist/main.js')
命令管理 (src/main.js)
通过 commander 来设置不同的命令。
- command 方法是设置命令的名字。
- description 方法是设置描述。
- alias 方法是设置简写。
- action 方法是设置回调。
Object.keys(configMap).forEach((action) => {
program
.command(action)
.alias(configMap[action].alias)
.description(configMap[action].description)
.action(function () {
if (action == 'new') {
handleType()
}
//检测版本并执行main函数
checkVersion(()=>{
main(action, ...process.argv.slice(3))
})
});// 要分段
})
每次执行主文件,需检测版本是否最新并提示
处理用户输入(\src\config.js)
在根目录下建立 .bgrc 文件并写入如下内容,用来存放用户托管平台名字、仓库地址、模版信息等,模版信息尚在开发中,后期会把模板信息提出来专门处理。
处理用户输入命令(\src\utils\rc.js)
新增/修改
dfcli config set <key> <value>
set 功能有两个,第一实现用户输入的新增和修改,第二实现初始化rc文件。
实现代码:
export let set = async (k,v) => {
let has = await exist(rcUrl)
if (k && v) {
let opts
if (has) {
opts = await readFile(rcUrl, 'utf-8')
opts = decode(opts)
opts = Object.assign(opts,{[k]: v})
} else {
opts = Object.assign(DEFAULT,{[k]: v})
}
await writeFile(rcUrl, encode(opts), 'utf-8')
} else { // rc初始化
if (has) return
await writeFile(rcUrl, encode(DEFAULT), 'utf-8')
}
}
查看/获取
dfcli config get <key>
当没有get 后面没有key时,默认走getAll()表示获取全部bgrc文件内容
eg: dfcli config get 实现代码:
export let get = async (k) => {
let has = await exist(rcUrl)
let opts
if (has) {
opts = await readFile(rcUrl, 'utf-8')
opts = decode(opts)
console.log(opts[k])
} else {
return ''
}
}
export let getAll = async () => {
let has = await exist(rcUrl)
let opts
if (has) {
opts = await readFile(rcUrl, 'utf-8')
return decode(opts)
} else {
return ''
}
}
删除
dfcli config remove <key>
代码实现:
export let remove = async (k) => {
let has = await exist(rcUrl)
let opts
if (has) {
opts = await readFile(rcUrl, 'utf-8')
opts = decode(opts)
if (opts.hasOwnProperty(k)) {
delete opts[k]
}
await writeFile(rcUrl, encode(opts), 'utf-8')
} else {
return ''
}
}
更换模板地址
dfcli config set repertroy github:owner
dfcli config set username yourname
查看模板列表(\src\list.js)
dfcli list
初始化项目 (\src\install.js)
命令:
dfcli init
根据.bgrc文件的配置,获取托管在github上面的模板名字:
// 模板名字列表
let getTplNameList = async () => {
let loading = ora('Loading template list .......')
loading.start()
let list = await getTplList()
loading.succeed('template list complete.')
let name = list.map((list) => list.name)
return name
}
获取到模板名字列表后,让用户选择模板
const promptList = [{
type: 'list',
message: 'Please select a template: ',
name: 'tpl_name',
choices: tplnameList
}]
let ans = await inquirer.prompt(promptList)
console.log(ans.tpl_name)
1.先判断当前模板本地是否以缓存,有缓存的话询问用户是否覆盖,没有缓存直接下载模板
2.选择模板后,让用户输入项目名字和项目目标文件夹
3.生成渲染模板到指定的位置
// 判断是本地是否有模板
let localCheckTpl = (tmpName) => {
//远程模板地址
const tmpRepo=path.resolve(userHome,'.tpl')
//本地模板存放仓库
const tmpDest=path.join(tmpRepo,tmpName)
return {
isExist: exists(tmpDest),
tmpDest
}
}
// 下载模板
let downloadTplAndGenrate = async (proName) => {
//远程模板地址
const tmpRepo=path.resolve(userHome,'.tpl')
//本地模板存放仓库
const tmpDest=path.join(tmpRepo,proName)
let all = await getAll()
let loading = ora(`download template start...`)
loading.start()
await download( `${all.repertroy}/${proName}`, home + '/.tpl/' + proName)
loading.succeed(`template download complete.`)
await generate(tmpDest)
}
// generate.js
export default async(tmpPath)=>{
//初始化Metalsmith对象
const metalsmith=Metalsmith(tmpPath)
// 用户输入项目名字和目标文件夹
let answer = await inquirer.prompt([{
type:'input',
name:'name',
message:'Please enter your project name:',
default:'dfcli-project'
},{
type:'input',
name:'destination',
message:'Please enter the path where your project will be stored:',
default:process.cwd()
}])
//项目生成路径
const destination=path.join(absolutePathFormat(answer.destination),answer.name)
const loading = ora('generating...')
//加入新的全局变量
Object.assign(metalsmith.metadata(),answer)
// console.log(metalsmith.metadata())
loading.start()
metalsmith
.source('.')
.destination(destination)
.clean(false)
.build(function(err) {
loading.stop()
if (err) throw err
console.log()
console.log(chalk.green('Build Successfully'))
console.log()
console.log((`${chalk.green('Please cd')} ${destination} ${chalk.green('to start your coding')}`))
console.log()
})
}
项目结构说明
|-- bin
| `-- www // 主文件入口,启动跑main.js
|-- package.json
`-- src
|-- config.js 配置rc文件的增删改查
|-- create.js new 创建单个页面或者模板
|-- generate.js 构建生成项目内容
|-- index.js 根据用户输入动作命令,执行不同命令(dfcli init、dfcli config、dfcli list、dfcli new),具体任务从这里开始分开
|-- install.js 模板初始化(dfcli init)
|-- list.js 本地模板列表(dfcli list)
|-- main.js 通过commander初始化并设置不同的命令
|-- new.js 创建页面或者模板的具体实现
`-- utils
|-- checkVersion.js 包版本检查
|-- constant.js 一些需要的常量
|-- gitHandle.js git相关操作
|-- localPath.js 路径校验
`-- rc.js rc文件的增删改查具体操作方法