公司的脚手架靠你了

前言

这不放假了,趁着有时间,准备搞个脚手架玩玩,由于node社区已经很完善了,就只需要踩到巨人的肩上“搭积木”,我们从三个部分着手,工具--工欲善其事必先利其器;实践--实践是检验真理的唯一标准;Coding--唯手熟尔,哈哈,拉下来玩玩。相信我从0开始,看完你上你也行。

效果

先上图:

image.png [图1]

image.png [图2] image.png [图3]

看起来是不是很熟悉?没错,就是vue-cli的仿品。来来来,一步步来,一学就会。

工具篇

学习讲究一个过程,既然是工具,那我们首先就要知道这个工具是什么,能做什么,以及怎么用。接下来我们就围绕是什么,能做什么,以及怎么用来介绍四个工具----inquirer、commander、chalk、shelljs。当然工具有很多,选自己熟悉的就好,如果不熟悉,不妨学习学习。直接看代码跳到实践部分。

inquirer

如果你看过vue-cli代码,会发现它也在用这个库。那么inquirer是什么呢?inquirer是一个控制台交互式js库。能做什么呢?如上截图,能用来和用户更友好的交互。怎么用呢?来一块看看。
`

安装
npm install inquirer
复制代码
使用
var inquirer = require('inquirer');
inquirer
  .prompt([
    /* Pass your questions in here */
  ])
  .then((answers) => {
    // Use user feedback for... whatever!!
  })
  .catch((error) => {
    if (error.isTtyError) {
      // Prompt couldn't be rendered in the current environment
    } else {
      // Something else went wrong
    }
  });
复制代码
inquirer.prompt(questions: [Question]):Promise
复制代码
interface Ichoices {
    name: string,
    value: string | number | boolean,
    short?: string
}
复制代码
interface Question {
    // 交互类型
    type: 'input' | 'number' | 'confirm' | 'list' | 'rawlist' | 'expand' | 'checkbox' | 'password' | 'editor';
    
    // 当前输入的名字,可用来获取输入数据
    name: string;
    
    // 提示信息
    message: string | Function;
    
    // 给与的默认值
    default: string | number | boolean | [any] | Function;
    
    // 提示给用户选择的数据
    choices: number | string | Ichoices | Function;
    
    // 对用户输入数据做验证
    validate: Function;
    
    // 自定义处理用户输入数据
    filter: Function;
    
    // 对用户输入数据做自定义显示转换,不影响最终数据
    transformer: Function;
    
    // 当什么的时候触发,返回true或false
    when: Function | boolean;
    
    // 对type值是checkbox、list、rawList、expand展示给用户看到的行数
    pageSize: number;
    
    // 更改默认的前缀显示默认是?
    prefix: string;
    
    // 更改默认的后缀显示 默认是空字符串
    suffix: string;
    
    // 如果答案已经存在,则强制提出问题
    askAnswered: boolean;
    
    // 列表选择时是否循环操作默认true
    loop: boolean;
}
复制代码

prompt接收一个question数组,数组的每一项如上面的类型定义。举个例子,看看图1对应的代码:

image.png [图4]

type是confirm,需要用户输入y/N,默认值是true。后续的执行根据前面的不同结果再进行执行。

npm
github

commander

commander是什么?它是一个完整的nodejs命令行解决方案。能做什么呢?执行自定义命令,比如本文使用的mcli create <filename> 创建一个工程。下面再看看怎么用:

安装
npm install commander
复制代码
使用
const { program } = require('commander');

program
  .option('-d, --debug', 'output extra debugging')
  .option('-s, --small [type]', 'small pizza size', 'large')
  .option('-p, --pizza-type <type>', 'flavour of pizza', 'pizza');

program.parse(process.argv);

const options = program.opts();
if (options.debug) console.log(options);

console.log('pizza details:');

if (options.small) console.log('- small pizza size');
if (options.pizzaType) console.log(`- ${options.pizzaType}`);
复制代码

上面定义了三个选项,现在命令行执行
node -r ts-node/register src/index.ts

image.png

[图5]

node -r ts-node/register src/index.ts -d

image.png

[图6]

看完结果,我们再说上面是什么意思:执行第一个命令时,由于-s和-p选项都有相应的默认值,所以会输出默认值。[type]<type>分别表示可选和必传。如果执行-p选项,必须传递type值,否则就会报错。执行-s不传递就使用默认值。执行完响应的选项,相应的属性也会改变,不需要传值的属性就是boolean类型,需要传值的就是传递的数据。然后再根据用户执行的选项程序再做下一步处理。

再介绍一个command命令:

.command()可以执行一个命令,比如 .command('create <name>')看到create是不是又感觉到了熟悉,没错就是生成项目时那个create,后面的<name>表示执行命令必须传递name,即我们的工程。

npm
github

chalk

chalk是什么?相信大家都已经用过了。但是不免有没用过的,我们这里也简单介绍一下。它是一个着色库,对展现在用户面前的数据进行着色处理。使用如下:
注:5版本是esm,如果在ts中或者构建工具中使用,目前建议使用4版本

安装
yarn add chalk@4.1.2
复制代码
使用
const chalk = require('chalk');
console.log(chalk.blue('Hello world!'));
复制代码

api之类的官网描述的很详细了,一看就会,有种写css的感觉呦。

npm

shelljs

shell,不言而喻,shelljs是基于 Node.js API 的 Unix shell 命令的可移植 (Windows/Linux/macOS) 实现。这使得我们可以很方便的执行shell脚本。
怎么用呢?

安装
npm install shelljs
复制代码
使用
var shell = require('shelljs');

if (!shell.which('git')) {
  shell.echo('Sorry, this script requires git');
  shell.exit(1);
}

// Copy files to release dir
shell.rm('-rf', 'out/Release');
shell.cp('-R', 'stuff/', 'out/Release');

// Replace macros in each .js file
shell.cd('lib');
shell.ls('*.js').forEach(function (file) {
  shell.sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
  shell.sed('-i', /^.*REMOVE_THIS_LINE.*$/, '', file);
  shell.sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, shell.cat('macro.js'), file);
});
shell.cd('..');

// Run external tool synchronously
if (shell.exec('git commit -am "Auto-commit"').code !== 0) {
  shell.echo('Error: Git commit failed');
  shell.exit(1);
}
复制代码

有了shelljs的加持,脚本执行的问题就解决了。至此,一个cli需要用到的工具我们都了解了,一个想象中的cli的骨架也就成型了。

github

实践

代码整个贴出来篇幅有些长了,具体...还是贴一下

#!/usr/bin/env node
const inquirer = require('inquirer');
const chalk = require('chalk');
const program = require('commander');
const readline = require('readline');
const shell = require('shelljs');


// type choices = number | string | {name: string, value: string | number | boolean, short?: string} | Function;

// 即用户新输入的参数值和当前已有的参数值
const optionFunc = (newVal, preValue) => {
    console.log(newVal, preValue);
}

program
    .configureOutput({
        // 此处使输出变得容易区分
        writeOut: (str) => process.stdout.write(`[OUT] ${str}`),
        writeErr: (str) => process.stdout.write(`[ERR] ${str}`),
        // 将错误高亮显示
        outputError: (str, write) => write(chalk.hex('#f40')(str))
    })
    .command('create <name>')
    .description('创建应用')
    .option('-f,--force <path>', '是否强制创建', optionFunc)
    .action((name, options, command) => {
        console.log(name, 'xx')
        if (!name) {
            console.log(chalk.hex('#f40')('请输入项目名'))
            return;
        } else {
            inquirer.prompt([
                {
                    type: 'confirm',
                    name: 'update',
                    message: `Your connection to the default yarn registry seems to be slow.\n Use https://registry.npm.taobao.org for faster installation?`,
                    default: true,
                },
                {
                    when: (answer) => {
                        if (!answer.update) {
                            console.log(chalk.hex('#f40')('开始更新...'));
                        }
                    }
                },
                {
                    type: 'list',
                    name: 'preset',
                    message: 'Please pick a preset:',
                    choices: [
                        {
                            name: 'Default ([Vue 2] babel, eslint)',
                            value: '1',
                        },
                        {
                            name: 'Default (Vue 3) ([Vue 3] babel, eslint)',
                            value: '2',
                        },
                        {
                            name: 'Manually select features',
                            value: '3',
                        },
                    ],
                    when: (answer) => {
                        if (answer.update) {
                        console.log(chalk.red(`\n\nVue CLI v4.5.13`));
                        console.log(`
┌───────────────────────────────────────────┐
│                                           │
│   New version available ${chalk.hex('#f40')('4.5.13')}${chalk.hex('#f40')('4.5.15')}   │
│     Run ${chalk.hex('#f40')('npm i -g @vue/cli')} to update!      │
│                                           │
└───────────────────────────────────────────┘
                        `);
                        }
                        return answer.update;
                    }
                },
                {
                    type: 'list',
                    name: 'type',
                    message: 'Pick the package manager to use when installing dependencies:',
                    choices: [
                        {
                            name: 'Use Yarn',
                            value: 'yarn',
                        },
                        {
                            name: 'Use NPM',
                            value: 'npm',
                        },
                    ],
                    when: (answer) => {
                        return answer.update
                    }
                },
                {
                    when: (answer) => {
                        if (answer.type) {
                            console.log(chalk.hex('#f40')('Vue CLI v4.5.13'));
                            console.log(chalk.hex('#f40')('✨  Creating project in /Users/zhiepngwan/Desktop/demo/1111.'));
                            console.log(chalk.hex('#f40')('🗃  Initializing git repository...'));
                            console.log(chalk.hex('#f40')('⚙️  Installing CLI plugins. This might take a while...'));
                        }
                    }
                },
                {
                    type: 'confirm',
                    name: 'join',
                    message: '加入我',
                    default: true,
                    when: (answer) => {
                        return answer.update
                    }
                },
                {
                    when: (answer) => {
                        if (!answer.join) {
                            const outStream = process.stdout;
                
                            // 创建interface
                            const rl = readline.createInterface({
                                input: process.stdin,
                                output: outStream
                            });
            
                            const textArr = ['2021', '辞旧', '2022', '迎新', '新的一年', '加油写作', 'Fight', 'Together!'];
            
                            // 随机坐标
                            const randomPos = () => {
                                const x = Math.floor(30 * Math.random());
                                const y = Math.floor(10 * Math.random());
                                return [x, y];
                            }
                            // chalk 上色
                            const randomTextStyle = (text) => {
                                const styles = ['redBright', 'yellowBright', 'blueBright', 'cyanBright', 'greenBright', 'magentaBright', 'whiteBright'];
                                const color = styles[Math.floor(Math.random() * styles.length)];
                                return chalk[color](text);
                            }
                            // 延时
                            const delay = (time) => {
                                return new Promise((resolve) => setTimeout(resolve, time));
                            }
                            setTimeout( async () => {
                                for(let i = 0; i < textArr.length; i++) {
                                    readline.cursorTo(outStream, ...randomPos());
                                    rl.write(randomTextStyle(textArr[i])); // 展示到控制台
            
                                    await delay(1000);
                                    readline.cursorTo(outStream, 0, 0);
                                    readline.clearScreenDown(outStream);
                                }
                            }, 1000);
                        }
                    }
                }
            ])
            .then((answer) => {
                if (!answer.join) {
                    readline.cursorTo(process.stdout, 0, 0); // 移动光标 你就理解成改变控制台位置到 0,0
                    readline.clearScreenDown(process.stdout); // 清屏操作 就像使用vue-cli看到的一样
                    return;
                }
                console.log(answer);
                console.log('etx... 接着再执行一系列命令');

                // 执行命令
                // if (shell.exec('yarn add jQuery').code !== 0) {
                //     shell.echo('Error: install failed');
                //     shell.exit(1);
                // }
                // if (shell.exec('git clone https://github.com/wanzhip/mcli.git').code !== 0) {
                //     shell.echo('Error: install failed');
                //     shell.exit(1);
                // }
            });
        };
    });

program.parse(process.argv);

复制代码

Coding

唯手熟尔,敲一下加深记忆。完整的代码后续发到github。最后祝大家新年快乐!!!新年新气象!财源滚滚!步步高升!

附录

zhuanlan.zhihu.com/p/452195702

www.wmm20.com/posts/8e3b1…

juejin.cn/post/684490…

juejin.cn/post/702032…

juejin.cn/post/686643…

分类:
前端
标签:
  • 小p
    14小时前