手摸手教你撸一个脚手架

作者:前端宇宙 公号 / 刘小夕 (本文来自作者投稿)

脚手架

vue-cli, create-react-appreact-native-cli 等都是非常优秀的脚手架,通过脚手架,我们可以快速初始化一个项目,无需自己从零开始一步步配置,有效提升开发体验。尽管这些脚手架非常优秀,但是未必是符合我们的实际应用的,我们可以定制一个属于自己的脚手架(或公司通用脚手架),来提升自己的开发效率。

脚手架的作用

  • 减少重复性的工作,不需要复制其他项目再删除无关代码,或者从零创建一个项目和文件。

  • 可以根据交互动态生成项目结构和配置文件。

  • 多人协作更为方便,不需要把文件传来传去。

实现的功能

在开始之前,我们需要明确自己的脚手架需要哪些功能。vue init template-name project-namecreate-react-app project-name。我们这次编写的脚手架(eos-cli)具备以下能力(脚手架的名字爱叫啥叫啥,我选用了Eos黎明女神):

  • eos init template-name project-name 根据远程模板,初始化一个项目(远程模板可配置)

  • eos config set 修改配置信息

  • eos config get [] 查看配置信息

  • eos --version 查看当前版本号

  • eos -h

大家可以自行扩展其它的 commander,本篇文章旨在教大家如何实现一个脚手架。

效果展示

初始化一个项目

手摸手教你撸一个脚手架

修改.eosrc文件,从 vuejs-template 下载模板

手摸手教你撸一个脚手架

需要使用的第三方库

  • babel-cli/babel-env: 语法转换

  • commander: 命令行工具

  • download-git-repo: 用来下载远程模板

  • ini: 格式转换

  • inquirer: 交互式命令行工具

  • ora: 显示loading动画

  • chalk: 修改控制台输出内容样式

  • log-symbols: 显示出 √ 或 × 等的图标

关于这些第三方库的说明,可以直接npm上查看相应的说明,此处不一一展开。

初始化项目

创建一个空项目(eos-cli),使用 npm init 进行初始化。

安装依赖

npm install babel-cli babel-env chalk commander download-git-repo ini inquirer log-symbols ora

目录结构

├── bin│   └── www             //可执行文件├── dist    ├── ...             //生成文件└── src    ├── config.js       //管理eos配置文件    ├── index.js        //主流程入口文件    ├── init.js         //init command    ├── main.js         //入口文件    └── utils        ├── constants.js //定义常量        ├── get.js       //获取模板        └── rc.js        //配置文件├── .babelrc             //babel配置文件├── package.json├── README.md

babel 配置

开发使用了ES6语法,使用 babel 进行转义,

.bablerc

{    "presets": [        [            "env",            {                "targets": {                    "node": "current"                }            }        ]    ]}

`eos` 命令

node.js 内置了对命令行操作的支持,package.json 中的 bin 字段可以定义命令名和关联的执行文件。在 package.json 中添加 bin 字段

package.json

{    "name": "eos-cli",    "version": "1.0.0",    "description": "脚手架",    "main": "index.js",    "bin": {        "eos": "./bin/www"    },    "scripts": {        "compile": "babel src -d dist",        "watch": "npm run compile -- --watch"    }}

www 文件

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

#! /usr/bin/env noderequire('../dist/main.js');

链接到全局环境

开发过程中为了方便调试,在当前的 eos-cli 目录下执行 npm link,将 eos 命令链接到全局环境。

启动项目

npm run watch

处理命令行

利用 commander 来处理命令行。

main

import program from 'commander';import { VERSION } from './utils/constants';import apply from './index';import chalk from 'chalk';/** * eos commands *    - config *    - init  */let actionMap = {    init: {        description: 'generate a new project from a template',        usages: [            'eos init templateName projectName'        ]    },    config: {        alias: 'cfg',        description: 'config .eosrc',        usages: [            'eos config set <k> <v>',            'eos config get <k>',            'eos config remove <k>'        ]    },    //other commands}// 添加 init / config 命令Object.keys(actionMap).forEach((action) => {    program.command(action)    .description(actionMap[action].description)    .alias(actionMap[action].alias) //别名    .action(() => {        switch (action) {            case 'config':                 //配置                apply(action, ...process.argv.slice(3));                break;            case 'init':                apply(action, ...process.argv.slice(3));                break;            default:                break;        }    });});function help() {    console.log('rnUsage:');    Object.keys(actionMap).forEach((action) => {        actionMap[action].usages.forEach(usage => {            console.log('  - ' + usage);        });    });    console.log('r');}program.usage('<command> [options]');// eos -h program.on('-h', help);program.on('--help', help);// eos -V   VERSION 为 package.json 中的版本号program.version(VERSION, '-V --version').parse(process.argv);// eos 不带参数时if (!process.argv.slice(2).length) {    program.outputHelp(make_green);}function make_green(txt) {    return chalk.green(txt); }

下载模板

download-git-repo 支持从 Github、Gitlab 下载远程仓库到本地。

get.js

import { getAll } from './rc';import downloadGit from 'download-git-repo';export const downloadLocal = async (templateName, projectName) => {    let config = await getAll();    let api = `${config.registry}/${templateName}`;    return new Promise((resolve, reject) => {        //projectName 为下载到的本地目录        downloadGit(api, projectName, (err) => {            if (err) {                reject(err);            }            resolve();        });    });}

`init` 命令

命令行交互

在用户执行 init 命令后,向用户提出问题,接收用户的输入并作出相应的处理。命令行交互利用 inquirer 来实现:

inquirer.prompt([    {        name: 'description',        message: 'Please enter the project description: '    },    {        name: 'author',        message: 'Please enter the author name: '    }]).then((answer) => {    //...});

手摸手教你撸一个脚手架

视觉美化

在用户输入之后,开始下载模板,这时候使用 ora 来提示用户正在下载模板,下载结束之后,也给出提示。

import ora from 'ora';let loading = ora('downloading template ...');loading.start();//downloadloading.succeed(); //或 loading.fail();

手摸手教你撸一个脚手架

index.js

import { downloadLocal } from './utils/get';import ora from 'ora';import inquirer from 'inquirer';import fs from 'fs';import chalk from 'chalk';import symbol from 'log-symbols';let init = async (templateName, projectName) => {    //项目不存在    if (!fs.existsSync(projectName)) {        //命令行交互        inquirer.prompt([            {                name: 'description',                message: 'Please enter the project description: '            },            {                name: 'author',                message: 'Please enter the author name: '            }        ]).then(async (answer) => {            //下载模板 选择模板            //通过配置文件,获取模板信息            let loading = ora('downloading template ...');            loading.start();            downloadLocal(templateName, projectName).then(() => {                loading.succeed();                const fileName = `${projectName}/package.json`;                if(fs.existsSync(fileName)){                    const data = fs.readFileSync(fileName).toString();                    let json = JSON.parse(data);                    json.name = projectName;                    json.author = answer.author;                    json.description = answer.description;                    //修改项目文件夹中 package.json 文件                    fs.writeFileSync(fileName, JSON.stringify(json, null, 't'), 'utf-8');                    console.log(symbol.success, chalk.green('Project initialization finished!'));                }            }, () => {                loading.fail();            });        });    }else {        //项目已经存在        console.log(symbol.error, chalk.red('The project already exists'));    }}module.exports = init;

`config` 配置

eos config set registry vuejs-templates

config 配置,支持我们使用其它仓库的模板,例如,我们可以使用 vuejs-templates 中的仓库作为模板。这样有一个好处:更新模板无需重新发布脚手架,使用者无需重新安装,并且可以自由选择下载目标。

config.js

// 管理 .eosrc 文件 (当前用户目录下)import { get, set, getAll, remove } from './utils/rc';let config = async (action, key, value) => {    switch (action) {        case 'get':            if (key) {                let result = await get(key);                console.log(result);            } else {                let obj = await getAll();                Object.keys(obj).forEach(key => {                    console.log(`${key}=${obj[key]}`);                })            }            break;        case 'set':            set(key, value);            break;        case 'remove':            remove(key);            break;        default:            break;    }}module.exports = config;

rc.js

.eosrc 文件的增删改查

import { RC, DEFAULTS } from './constants';import { decode, encode } from 'ini';import { promisify } from 'util';import chalk from 'chalk';import fs from 'fs';const exits = promisify(fs.exists);const readFile = promisify(fs.readFile);const writeFile = promisify(fs.writeFile);//RC 是配置文件//DEFAULTS 是默认的配置export const get = async (key) => {    const exit = await exits(RC);    let opts;    if (exit) {        opts = await readFile(RC, 'utf8');        opts = decode(opts);        return opts[key];    }    return '';}export const getAll = async () => {    const exit = await exits(RC);    let opts;    if (exit) {        opts = await readFile(RC, 'utf8');        opts = decode(opts);        return opts;    }    return {};}export const set = async (key, value) => {    const exit = await exits(RC);    let opts;    if (exit) {        opts = await readFile(RC, 'utf8');        opts = decode(opts);        if(!key) {            console.log(chalk.red(chalk.bold('Error:')), chalk.red('key is required'));            return;        }        if(!value) {            console.log(chalk.red(chalk.bold('Error:')), chalk.red('value is required'));            return;        }        Object.assign(opts, { [key]: value });    } else {        opts = Object.assign(DEFAULTS, { [key]: value });    }    await writeFile(RC, encode(opts), 'utf8');}export const remove = async (key) => {    const exit = await exits(RC);    let opts;    if (exit) {        opts = await readFile(RC, 'utf8');        opts = decode(opts);        delete opts[key];        await writeFile(RC, encode(opts), 'utf8');    }}

发布

npm publish 将本脚手架发布至npm上。其它用户可以通过 npm install eos-cli -g 全局安装。
即可使用 eos 命令。

项目地址

本项目完整代码请戳: https://github.com/YvetteLau/Blog/tree/master/eos-cli


评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×