从0到1开发一个小程序cli脚手架(一)--建立页面/组件模版篇

github地址:github.com/jinxuanzhen…,有兴趣的同窗能够体验一下javascript

原文地址:www.yuque.com/docs/share/…vue

cli工具是什么?

在正文以前先大体描述下什么是cli工具,
cli工具英文名command-line interface,也就是命令行交互接口,比较典型的几个case例如,create-react-app,vue-cli,具体能够去百度一下,下面gif是小打卡目前用的一套自动化发布工具🔧java

QQ20190716-183106-HD (1).gif

能够看到整个发布流程大体是以选择或默认项的形式实现,大体分析下面几步node

  • 选择打包形式    开发模式/debug模式/发布模式
  • 设置版本号
  • 填写发布信息
  • 选择环境
  • 是否提交版本commit

是否是很是无脑?是否是不再用担忧线上发错环境了?有了它就算不一样项目间,就算一天发n次版本还须要担忧什么呢?react

固然除了简单的发布功能还,还能够作不少的事情,好比建立page/component模版等一些更多有趣的事情webpack

为了节约版面就不贴图了,具体能够看下仓库  github.com/jinxuanzhen…(目前该工具是从小打卡现有的cli库中抽离的部分功能)git

明确痛点

也就是我为何要作这么一个工具,其实最开始我只是为了解决一个问题,就是在整个发布流程中须要人工去改动/确认发布环境和版本信息,大体能够想象下把线下环境发布到线上的尴尬处境github

后续发现从cli角度触发,不少东西都变得简单了,大体列了下:web

  • 环境变量切换(线上环境,线下环境)
  • 建立启动模版,包括页面,组件
  • 自动化发布
  • ...

准备工做

本文会以快速建立页面模版文件为例教你怎么快速撸一个属于本身的cli工具,
若是以为本身作比较麻烦,能够clone下个人仓库本身改装下vue-cli

须要了解的三方库

中间会用到一些第三方库

  • commander, 一个解析命令行命令和参数工具
  • inquirer,经常使用交互式命令行用户界面的集合
  • chalk,美化你的终端输出样式
  • fuzzy,字符串模糊匹配的插件,根据输入关键词进行模糊匹配
  • json-format,json美化/格式化工具

其余的一些小知识:好比path模块,fs模块,你们能够去node官网自行查看:nodejs.org/api/

搭建开发环境

建立一个空文件夹,而且npm初始化, 而且建立一个index.js页面,这个index.js将做为你整个包的入口文件

npm init -y
复制代码

安装上述的三方包,固然也能够后续按需安装,这样更能清楚每一个包是作什么的

npm install @moyuyc/inquirer-autocomplete-prompt commander chalk commander fuzzy inquirer json-format --save
复制代码

在package.json里添加bin字段, 将自定义的命令软连到全局环境,同时执行npm link建立连接,这里若是报错{code EACCES,errno:13,...},是由于权限不足,能够尝试sudo npm link

"bin": {
    "cli-demo": "./index.js"
  }
复制代码

在入口文件,index.js 行首加入一行#!/usr/bin/env node指定当前脚本由node.js进行解析

#!/usr/bin/env node // 指定运行环境

// 输出文本
console.log('Hello World!!!');
复制代码

这时能够在命令行中执行cli-demo验收一下成果了

image.png

ok,能够看到当在全局状态下输入自定义命令时,正确运行了入口文件,也就意味着的开发玩具已经搭建完成

Let‘ Go

整理逻辑

以快速建立页面模版文件为例,就须要考虑须要哪些逻辑:

  • 设置页面名称
  • 找到已有模版文件
  • copy到项目中
  • 修改app.json

识别命令行

在刚才的Hello World!!!环节,已经能够正确识别cli-demo,可是须要在一个cli工具中集成更多功能,可能须要有不一样的执行策略,以git为例:git clone, git status,git push,因此须要识别不一样的命令和参数,

是时候就须要用到commander这个第三方包帮助解析命令行参数了,固然你也能够本身撸一个lib,本质上仍是方便解析process.argv

index.js (本质上这个js就是一个路由)

#!/usr/bin/env node

const version                       = require('./package').version;                 // 版本号

/* = package import -------------------------------------------------------------- */

const program                       = require('commander');                         // 命令行解析

/* = task events -------------------------------------------------------------- */
const createProgramFs               = require('./lib/create-program-fs');           // 建立项目文件


/* = config -------------------------------------------------------------- */

// 设置版本号
program.version(version, '-v, --version');

/* = deal receive command -------------------------------------------------------------- */

program
    .command('create')		
    .description('建立页面或组件')
    .action((cmd, options) => createProgramFs(cmd));

/* 后续能够根据不一样的命令进行不一样的处理,能够简单的理解为路由 */
// program
// .command('build [cli]')
// .description('执行打包构建')
// .action((cmd, env) => callback);

/* = main entrance -------------------------------------------------------------- */
program.parse(process.argv)
复制代码

这时候当键入cli-demo create时会自动执行createProgramFs

createProgramFs.js

module.exports = function () {
    console.log('Hi, create-program-fs.js');
};
复制代码

命令行输入 cli-demo create

image.png

能够看到已经成功的开辟出了一块独立的业务模块,后续就只须要依据需求填补相应的内容便可

建立交互命令

收到执行命令,这个时候按第一张图,是须要开始一系列QA(固然你也能够不作交互式,直接配置命令行参数),
引入三方包 inquirer,来指定问题队列

const question = [
  
    // 选择模式使用 page -> 建立页面 | component -> 建立组件
    {
        type: 'list',
        name: 'mode',
        message: '选择想要建立的模版',
        choices: [
            'page',
            'component',
        ]
    },
    
    // 设置名称
    {
        type: 'input',
        name: 'name',
        message: answer => `设置 ${answer.mode} 名称 (e.g: index):`,
    },
];

module.exports = function() {
	
    // 问题执行
    inquirer.prompt(question).then(answers => {
		console.log(answers);
    });
};
复制代码

demo1 (1).gif

能够看到经过一系列QA交互,实际输出拿到的是一个json对象,第一步已完成

建立模版文件

建立一个存放模版文件的文件夹template,并准备好你但愿的模版

image.png

项目中使用模版文件

为了方便阅读,下面的代码,须要明确下面变量的定义, Config.dir_root  = 命令行执行目录 Config.root  = cli项目根目录 Config.appRoot = 小程序项目路径 Config.template = 模版目录

这里有两个点,一个是执行路径的问题,另外一个是分包的问题,具体以下:

执行路径

这里必定要弄明白**__dirname, process.cwd()**的区别,同时还有一些小程序是本身搭的gulp/webpack,可能小程序项目是在src目录下,必定要分清楚

  • __dirname: 被执行js文件的绝对路径,通常在index.js执行时缓存起来做为项目的全局路径,好比找到template文件夹就会使用 ${__dirname}/template

  • process.cwd():当前命令行运行时的工做目录,好比在/Users/xuan/Documents/cli-demo

  • 若是当前项目在src,或其余文件夹里怎么办?能够提供一个给用户项目中的配置文件,相似于gulpfile.js或是webpack.config.js的形式,内容例如(具体能够看git仓库

module.exports = {

    // 小程序路径
    app: './src',

    // 模版文件夹
    template: './template'
};
复制代码

能够看到对象中app属性,能够指定你当前小程序项目的路径

分包

由于小程序的分包机制会致使页面实际路径与在主包的路径不相符,例如:

  • 主包:pages/index/index
  • 分包:pages/main_module/pages/habit_enlist/habit_enlist

解决这个问题一方面是要有页面建立要有必定的规范,统一格式,另外一方面须要根据规则解析app.json,
上面的主包,分包路径差很少是我目前使用的规范

解析app.json

// 获取app.json
function getAppJson() {
    let appJsonRoot = path.join(Config.appRoot, '/app.json');
    try {
        return require(appJsonRoot);
    }catch (e) {
        Log.error(`未找到app.json, 请检查当前文件目录是否正确,path: ${appJsonRoot}`);
        process.exit(1);			// 异常退出
    }
}

// 解析app.json
let parseAppJson = () => {

    // app Json 原文件
    let appJson = __Data__.appJson = getAppJson();

    // 获取主包页面
    appJson.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = '');

    // 获取分包,页面列表
    appJson.subPackages.forEach(item => {
        __Data__.appModuleList[getPathSubSting(item.root)] = item.root;
        item.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = item.root);
    });
};

// __Data__.appPagesList = 小程序所有页面
// __Data__.appModuleList = 小程序所有分包页面
// item结构 {util_module: 'pages/util_module/'},这么定义结构是为了方便后续取数
复制代码

question队列里,增长删选分包的选项

// 设置page所属module
    {
        type: 'autocomplete',
        name: 'modulePath',
        message: 'Set page ownership module',
        choices: [],
        suggestOnly: false,
        source(answers, input) {
            // none 表明放在主包
            return Promise.resolve(fuzzy.filter(input, ['none', ...Object.keys(__Data__.appModuleList)]).map(el => el.original));
        },
        filter(input) {
            if (input === 'none') {
                return '';
            }
            return __Data__.appModuleList[input];
        },
        when(answer) {
            return answer.mode === 'page';
        }
    }
复制代码

autocomplete类型本质上是个列表,可是能够进行模糊查询,很是方便,像小打卡有接近30个分包的状况下效果尤其明显

QQ20190717-162222 (1).gif

有了文件名,有了分包路径,有了可供copy的模版,接下来就很简单了,把模版文件塞进项目就能够了,下面是一串从仓库里copy的代码,利用async/await很方便的写出一维代码,基本上的流程:

获取路径 -> 校验 -> 获取文件信息 -> 复制文件 -> 修改app.json -> 输出结果信息

async function createPage(name, modulePath = '') {

    // 获取模版文件路径
    let templateRoot = path.join(Config.template, '/page');
    if (!Util.checkFileIsExists(templateRoot)) {
        Log.error(`未找到模版文件, 请检查当前文件目录是否正确,path: ${templateRoot}`);
        return;
    }
    
    // 获取业务文件夹路径
    let page_root = path.join(Config.appRoot, modulePath, '/pages', name);

    // 查看文件夹是否存在
    let isExists = await Util.checkFileIsExists(page_root);
    if (isExists) {
        Log.error(`当前页面已存在,请从新确认, path: ` + page_root);
        return;
    }

    // 建立文件夹
    await Util.createDir(page_root);

    // 获取文件列表
    let files = await Util.readDir(templateRoot);

    // 复制文件
    await Util.copyFilesArr(templateRoot, `${page_root}/${name}`, files);

    // 填充app.json
    await writePageAppJson(name, modulePath);

    // 成功提示
    Log.success(`createPage success, path: ` + page_root);
}
复制代码

扩展

一个基本的快速建立页面模版的cli工具就这样完成,可是有可能须要更多的一些功能

自定义模版

好比说每一个项目的模版都有可能不太同样,很大程度上须要根据项目进行定制,这时候可能就须要前文提到的给用户开放config文件的插槽了

项目中的config:

// xdk.config.js
module.exports = {

    // 小程序路径
    app: './',

    // 模版文件夹
    template: './template'
};

// create-program-fs.js
module.exports = function() {
	
     // 校验:当前是否存在配置文件
    let customConfPath = `${Config.dir_root}/xdk.config.js`;
    if (!Util.checkFileIsExists(customConfPath)) {
        Log.error('当前项目还没有建立xdk.config.js文件');
        return;
    }

    // 获取用户配置项
    let {app, template = ''} = require(customConfPath);

    // 小程序目录
    Config.appRoot = path.resolve(path.join(Config.dir_root, app));

    // 模版文件目录(默认使用cli提供的默认模版,当config文件有设置template路径时,使用自定义路径)
    !!template && (Config.template = path.resolve(path.join(Config.dir_root, template))));
    
    // 问题执行
    inquirer.prompt(question).then(answers => {
		console.log(answers);
    });
};
复制代码

发布的npm仓库

目前从开发到调试本质上是在本地提供服务,利用npm link提供软链接到全局PATH,
其实也能够直接发到npm上,让其余使用的该cli的成员一建安装,好比npm install -g xxxxxxx, 具体教程的话百度,google有不少,做者表示很懒,遇到问题下面留言吧。。

最后

能够看到整个功能逻辑相对于平时写的复杂的业务逻辑来讲相对简单,主要是工具库的一些使用方面的东西,中间的难点可能就是node中概念性的一些东西,然而这些多看一下文档基本就能够解决,但愿你们能够从本文中了解到如何快速搭建一个属于本身的cli工具

顺便预告下后续的话可能会更新一些如何利用cli工具作到自动化发布,版本号控制,环境变量切换,自动生成文档等一系列有趣的功能

下文地址: 从0到1开发一个小程序cli脚手架(二) --版本发布/管理篇