开发一个简单的脚手架cli

背景

随着开发的项目越来越多、越来越频繁,我意识到了一个问题,那就是每次新项目的搭建可能或多或少地需要搬运旧项目的配置和构建文件,这就带给我一种真实的代码搬运工的感觉。

所以随着这种感触越来越深,我开始寻求一种一键生成项目模板(框架)从而减少开发成本的方法。那便是脚手架cli,相信用过vue-cli的同学都会觉得很好用很好使,那么为什么我们不能定制一个这样的cli呢?答案是可以的。

开始正题

基本思路

参考vue-cli的实现方式以及查阅资料看了很多大佬的实现方式后,首先我们要知道诸如vue-cli这些脚手架是怎么工作的:

image

大致流程引用大佬的文章内容

  1. vue-cli会先判断你的模板在远程github仓库上还是在你的本地某个文件里面,若是本地文件夹则会立即跳到第3步,反之则走第2步。
  2. 第2步会判断是否为官方模板,官方模板则会从官方github仓库中下载模板到本地的默认仓库下,即根目录下.vue-templates文件夹下。
  3. 第3步则读取模板目录下meta.js或者meta.json文件,根据里面的内容会询问开发者,根据开发者的回答,确定一些修改。
  4. 根据模板内容以及开发者的回答,渲染出项目结构并生成到指定目录。

简单说就是项目模板存储在远程仓库(可以是github、gitlab或者其他),然后我们通过cli工具将其下载到本地的同时,设置一些诸如项目名称、作者、是否使用vue-router。。。等等的配置,然后生成一个新的项目模板。对的听起来就是这么简单,实际该怎么做呢?

代码分析

这里直接贴代码来进行解释,毕竟水平不够,还没办法做到像大佬那样一步一步解析。

本文源码地址:https://github.com/HeadmasterTan/xtc-platform-cli

目录结构
|-- bin
    |-- xtc-platform.js
    |-- xtc-platform-init.js
|-- lib
    |-- download.js
    |-- generator.js
|-- package.json
xtc-platform.js
#!/usr/bin/env node
// 这个是被称为 Shebang 的东西,它指明了执行这个脚本文件的解释程序

// commander.js 用来处理命令行的工具
const program = require('commander')

program.version('1.0.0') // 当你执行 xtc-platform -V 命令的时候,就会显示:1.0.0
    .usage('<command> [项目名称]')
    .command('init', '创建新项目') // 设置子命令,当你执行 xtc-platform init 命令的时候会执行xtc-platform-init文件的内容
    .parse(process.argv) // 解析获取输入的参数 例如:xtc-platform init project-name 中 project-name 就是参数,然后就可以通过program.args拿到这个参数

参考资料:

xtc-platform-init.js
#!/usr/bin/env node

const fs = require('fs') // node自带的fs模块下的existsSync方法,用于检测路径是否存在。(会阻塞)
const glob = require('glob') // node的glob模块允许你使用 *等符号, 来写一个glob规则,像在shell里一样,获取匹配对应规则的文件
const path = require('path') // node自带的path模块,用于拼接路径
const chalk = require('chalk') // 用于高亮终端打印出的信息
const program = require('commander') // 命令行处理工具
const logSymbols = require('log-symbols') // 在终端上显示 × 和 √

const download = require('../lib/download') // 自定义工具-用于下载模板
const generator = require('../lib/generator') // 自定义工具-用于渲染模板

program.usage('<project-name>') // project-name必填
program.parse(process.argv) // 解析

// 根据输入,获取项目名称
let projectName = program.args[0]

if (!projectName) {
    // 相当于执行命令的--help选项,显示help信息,这是commander内置的一个命令选项
    program.help()
    return
}

const inquirer = require('inquirer') // 用于命令行与开发者交互
const list = glob.sync('*') // 遍历当前目录

let next = undefined
if (list.length) {
    if (list.filter(name => {
            const fileName = path.resolve(process.cwd(), path.join('.', name))
            const isDir = fs.statSync(fileName).isDirectory() // 使用同步函数 statSync,不使用异步函数stat
            return name.indexOf(projectName) !== -1 && isDir
        }).length !== 0) {
        console.log(`项目${projectName}已经存在`)
        return
    }
    next = Promise.resolve(projectName)
} else {
    next = Promise.resolve(projectName)
}

next && go()

function go() {
    next.then(projectRoot => {
        if (projectRoot !== '.') {
            fs.mkdirSync(projectRoot)
        }
        return download(projectRoot).then(target => { // 下载项目模板
            return {
                name: projectRoot,
                root: projectRoot,
                downloadTemp: target.downloadTemp
            }
        })
    }).then(context => { // 交互问答,配置项目信息
        return inquirer.prompt([{
            name: 'projectName',
            message: '项目名称',
            default: context.name
        }, {
            name: 'projectVersion',
            message: '项目版本号',
            default: '1.0.0'
        }, {
            name: 'projectDescription',
            message: '项目简介',
            default: `A project named ${context.name}`
        }]).then(answers => {
            return {
                ...context,
                metadata: {
                    ...answers
                }
            }
        })
    }).then(context => {
        return generator(context.metadata, context.downloadTemp) // 渲染项目模板
    }).then(context => {
        // 成功用绿色显示,给出积极的反馈
        console.log(logSymbols.success, chalk.green('项目模板构建完成。'))
        console.log()
    }).catch(err => {
        // 失败了用红色,增强提示
        console.error(logSymbols.error, chalk.red(`构建失败:${err.message}`))
    })
}

参考资料:

download.js
const download = require('download-git-repo') // 用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket
const ora = require('ora') // 用于命令行上的加载效果
const path = require('path')

const TEMP_NAME = '.temp'; // 模板的临时存储目录

module.exports = function (target) {
    target = path.join(TEMP_NAME, target || '')
    return new Promise((resolve, reject) => {
        const url = 'direct:https://github.com/HeadmasterTan/xtc-platform-template.git' // 远程仓库模板项目地址
        const spinner = ora('正在下载项目模板...')
        spinner.start()
        download(url, target, {
            clone: true
        }, (err) => {
            if (err) {
                spinner.fail() // wrong :(
                reject(err)
            } else {
                spinner.succeed() // ok :)
                resolve({
                    downloadTemp: TEMP_NAME,
                    target
                })
            }
        })
    })
}

关于模板地址可能会出错的问题,可以根据这个issue来判断问题原因和解决:

https://github.com/flipxfx/download-git-repo/issues/19

参考资料:

generator.js
const Metalsmith = require('metalsmith') // 静态网站生成器
const Handlebars = require('handlebars') // 知名的模板引擎
const rm = require('rimraf').sync // 相当于UNIX的“rm -rf”命令

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()
                Object.keys(files).forEach(fileName => {
                    const t = files[fileName].contents.toString()
                    files[fileName].contents = new Buffer(Handlebars.compile(t)(meta))
                })
                done()
            }).build(err => {
                rm(src) // 删除临时目录
                err ? reject(err) : resolve()
            })
    })
}

metalsmith也可以忽略(不渲染)项目中的某些文件/目录,可以根据个人需求定义问答交互来实现。

参考资料:

package.json
{
    "name": "xtc-platform-cli",
    "version": "1.0.0",
    "description": "自用平台脚手架",
    "keywords": ["cli", "xtc-platform"],
    "bin": {
        "xtc-platform": "./bin/xtc-platform.js"
    },
    "dependencies": {
        "chalk": "^2.4.2",
        "commander": "^2.19.0",
        "download-git-repo": "^1.1.0",
        "glob": "^7.1.3",
        "handlebars": "^4.1.0",
        "inquirer": "^6.2.2",
        "log-symbols": "^2.2.0",
        "metalsmith": "^2.3.0",
        "ora": "^3.2.0"
    }
}

这里不会教你怎么发布npm,但是要是你需要发布npm,有几点是要提醒的

注意点:

  • dependencies 而不是 devDependencies,如果你只是写在开发依赖上,那你 npm install xtc-platform-cli下来是不能用的,npm不会帮你下载开发依赖。你只是本地用的话当然没关系
  • 关于版本version,一旦撤销npm unpublish那该撤销的版本号下次无法使用。比如我发布了1.0.1版本,然后我撤销了,我下载发布就要跳过1.0.1了。

End

参考文章:

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值