美文网首页
【转】前端脚手架开发入门

【转】前端脚手架开发入门

作者: 涅槃快乐是金 | 来源:发表于2022-09-14 09:35 被阅读0次

    前言

    我们大部分时间都专注于业务的开发,初始化项目的时候,拿起模板项目直接npm init X就开干。但是,有时候现成的脚手架未必就能满足我们的业务需求,这时就需要我们自己开发一个脚手架,那么我们使用的脚手架里面到底做了什么,如何自己搭建脚手架呢?
    本次就给大家简单介绍下脚手架的基本开发。

    思考:我们平时用的脚手架模板Taro、create-react-app、rax 的cli都有什么样的特征呢?需要实现什么能力?
    先看下Rax 官方提供的npm init rax命令




    体验了rax脚手架之后,不难回答:

      1. 脚手架需要实现什么?初始化项目模版能力。
      1. 脚手架需要什么功能?
        1. 问询功能
        2. 下载模版
        3. 写入模版
        4. 优化(git初始化,安装依赖等)

    工具介绍

    要想实现以上的功能,我们需要一些工具辅助:

      1. commander.js 命令行工具
      1. chalk 命令行输出样式美化
      1. Inquirer.js 命令行交互
      1. ora 加载提示
      1. download-git-repo
      1. ...

    下面就一一先介绍下以上工具,初始化空项目,安装依赖

    npm install chalk commander download-git-repo inquirer ora --save
    
    commander

    commander github地址:https://github.com/tj/commander.js

    Commander是完整的node 命令行解决方案, 编写代码来描述命令行界面。Commander 负责将参数解析为选项和命令参数,为问题显示使用错误,并实现一个有帮助的系统。

    Commander通过链式调用,有option、argument、action等常用方法,其中:

    1. option方法用来定义选项,同时可以附加选项的简介,每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。

    2. aciton方法添加命令, 第一个参数为命令名称,命令参数可以跟在名称后面,也可以用.argument()单独指定。参数可为必选的(尖括号表示)、可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数

       program.command('clone <source> [destination]')
      
    3. argument方法 在Command对象上使用.argument()来按次序指定命令参数。该方法接受参数名称和参数描述。参数可为必选的(尖括号表示,例如<required>)或可选的(方括号表示,例如[optional])

    4. action方法 处理函数的参数:该命令声明的所有参数、解析出的选项、该命令对象自身。

    import { Command } from 'commander';
    
    const program = new Command();
    program
      .name('test-string-utils')
      .description('CLI to some JavaScript string utilities by 禾汐')
      .version('0.8.0');
    
    program.command('split')
      .description('Split a string into substrings and display as an array')
      .argument('<string>', 'string to split')
      .option('--first', 'display just the first substring')
      .option('-s, --separator <char>', 'separator character', ',')
      .action((str, options) => {
        console.log('===>',str,options);
        const limit = options.first ? 1 : undefined;
        console.log(str.split(options.separator, limit));
      });
    
    program.parse();
    
    // node ./demo/commander.js
    // node ./demo/commander.js split --separator=/ a/b/c
    // node ./demo/commander.js split -s / a/b/c
    
    chalk

    chalk github地址:https://github.com/chalk/chalk

    美化命令行输出的工具

    语法很简单:

    chalk.<style>[.<style>...](string, [string...])
    Example: chalk.red.bold.underline('Hello', 'world');【链式调用】
    
    import chalk from 'chalk';
    const log = console.log;
    
    log(chalk.blue('Hello world!'));
    log(chalk.blue('Hello') + ' World' + chalk.red('!'));
    log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
    log(chalk.green(
      'I am a green line ' +
      chalk.blue.underline.bold('with a blue substring') +
      ' that becomes green again!'
    ));
    
    // Use RGB colors in terminal emulators that support it.
    log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
    log(chalk.hex('#DEADED').bold('Bold gray!'));
    

    [图片上传失败...(image-9b039e-1663078081022)]

    Inquirer

    inquirer github:https://github.com/SBoudrias/Inquirer.js

    用户与命令行交互的工具

    基本语法:

    
    import inquirer from '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为每个问题提供很多参数:
    
    type:表示提问的类型,包括:input, confirm, list, rawlist, expand, checkbox, password, editor;
    name: 存储当前问题回答的变量;
    message:问题的描述;
    default:默认值;
    choices:列表选项,在某些type下可用,并且包含一个分隔符(separator);
    validate:对用户的回答进行校验;
    filter:对用户的回答进行过滤处理,返回处理后的值;
    transformer:对用户回答的显示效果进行处理(如:修改回答的字体或背景颜色),但不会影响最终的答案的内容;
    when:根据前面问题的回答,判断当前问题是否需要被回答;
    pageSize:修改某些type类型下的渲染行数;
    prefix:修改message默认前缀;
    suffix:修改message默认后缀。
    
    • input
    import inquirer from 'inquirer';
    const promptList = [{
        type: 'input',
        message: '设置一个用户名:',
        name: 'name',
        default: "test_user" // 默认值
    },{
        type: 'input',
        message: '请输入手机号:',
        name: 'phone',
        validate: function(val) {
            if(val.match(/\d{11}/g)) { // 校验位数
                return val;
            }
            return "请输入11位数字";
        }
    }];
    
    inquirer.prompt(promptList).then(answers => {
        console.log(answers); // 返回的结果
    })
    
    • confirm
    
    const promptList = [{
        type: "confirm",
        message: "是否使用def创建项目",
        name: "watch",
        prefix: "前缀"
    },{
        type: "confirm",
        message: "是否初始化项目?",
        name: "filter",
        suffix: "后缀",
        when: function(answers) { // 当watch为true的时候才会提问当前问题
            return answers.watch
        }
    }];
    
    
    inquirer.prompt(promptList).then(answers => {
        console.log(answers); // 返回的结果
    })
    
    • list
    • rawlist
    const promptList = [{
        type: 'rawlist',
        message: '请选择一种模板:',
        name: 'template',
        choices: [
            "TNPM",
            "WEB",
            "小程序"
        ]
    }];
    
    inquirer.prompt(promptList).then(answers => {
        console.log(answers); // 返回的结果
    })
    
    • expand
    const promptList = [{
        type: "expand",
        message: "请选择一种初始化模板:",
        name: "template",
        choices: [
            {
                key: "t",
                name: "TNPM",
                value: "tnpm"
            },
            {
                key: "w",
                name: "WEB",
                value: "web"
            },
            {
                key: "m",
                name: "小程序",
                value: "miniapp"
            }
        ]
    }];
    
    • checkbox

    const promptList = [{
        type: "checkbox",
        message: "选择模板:",
        name: "template",
        choices: [
            {
                name: "TNPM"
            },
            new inquirer.Separator(), // 添加分隔符
            {
                name: "WEB",
                checked: true // 默认选中
            },
            new inquirer.Separator("%%%%%%% 分隔符 %%%%%%"), // 自定义分隔符
            {
                name: "小程序"
            },
            new inquirer.Separator("--- 分隔符 ---"), // 自定义分隔符
            {
                name: "Assets"
            }
        ]
    }];
    
    • password

    
    const promptList = [{
        type: "password", // 密码为密文输入
        message: "请输入密码:",
        name: "pwd"
    }];
    
    
    • editor

    const promptList = [{
        type: "editor",
        message: "请输入备注:",
        name: "editor"
    }];
    
    ora

    ora github:https://github.com/sindresorhus/ora

    ora包用于显示加载中的效果,类似于前端页面的loading效果

    
    import ora from 'ora';
    import chalk from 'chalk';
    
    const spinner = ora(`Loading ${chalk.red('模板')}`).start();
    
    setTimeout(() => {
      spinner.color = 'red';
      spinner.text = 'Loading ...';
        spinner.prefixText ='前缀'
        spinner.spinner = {
            "interval": 70, // 转轮动画每帧之间的时间间隔 
            "frames": [
                    "✹",
            ],
        }
    }, 1000);
    setTimeout(() => {
      spinner.warn('模板需要您更新npm包版本!')
    }, 2000);
    setTimeout(() => {
      spinner.succeed ('模板下载成功!')
    }, 3000);
    setTimeout(() => {
      spinner.fail ('模板下载失败!')
    }, 3000);
    
    download-git-repo

    gitlab地址:https://gitlab.com/flippidippi/download-git-repo

    import  download from 'download-git-repo';
    import  rimraf from 'rimraf';
    import  path from 'path';
    
    const dir = path.join(process.cwd(), "template"); 
    
    rimraf.sync(dir, {});  //在下载前需要保证路径下没有同名文件
    
    download(
      "git@gitlab.XX:dinamic/miniapp-tracker.git#master", //仓库地址
      dir,
      { clone: true },
      function (err) {
        console.log(err ? "Error" : "Success", err);
      }
    );
    
    const dir = path.join(process.cwd(), "template"); 
    
    rimraf.sync(dir, {});  //在下载前需要保证路径下没有同名文件
    
    const spinner = ora(`Loading ${chalk.red('miniapp-tracker')} 仓库到template`).start();
    download(
      "git@gitlab.XX:dinamic/miniapp-tracker.git#master", //仓库地址
      dir,
      { clone: true },
      function (err) {
        console.log(err ? "Error" : "Success", err);
        err?spinner.fail ('模板下载失败!'):spinner.succeed ('模板下载成功!')
      }
    );
    

    还有其他很多常用的node交互工具,不一一介绍了,如下:

    作用
    chalk-pipe 使用更简单的样式字符串创建粉笔样式方案
    slash 系统路径符处理
    minimist 解析参数选项
    dotenv 将环境变量从 .env文件加载到process.env中
    dotenv-expand 扩展计算机上已经存在的环境变量
    hash-sum 非常快的唯一哈希生成器
    deepmerge 深度合并两个或多个对象的可枚举属性。
    yaml-front-matter 解析yaml或json
    resolve 实现node的 require.resolve()算法,这样就可以异步和同步地使用require.resolve()代表文件
    semver npm的语义版本器
    leven 测量两字符串之间的差异
    最快的JS实现之一
    lru cache 删除最近最少使用的项的缓存对象
    portfinder 自动寻找 8000至65535内可用端口号
    envinfo 生成故障排除软件问题(如操作系统、二进制版本、浏览器、已安装语言等)时所需的通用详细信息的报告
    memfs 内存文件系统与Node's fs API相同实现
    execa 针对人类的流程执行
    webpack-merge 用于连接数组和合并对象,从而创建一个新对象
    webpack-chain 使用链式API去生成简化webpack版本配置的修改
    strip-ansi 从字符串中去掉ANSI转义码
    address 获取当前机器的IP, MAC和DNS服务器。
    default-gateway 通过对OS路由接口的exec调用获得机器的默认网关
    joi JavaScript最强大的模式描述语言和数据验证器。
    fs-extra 添加了未包含在原生fs模块中的文件系统方法,并向fs方法添加了promise支持
    Acorn 一个小而快速的JavaScript解析器,完全用JavaScript编写。
    zlib.js ZLIB.js是ZLIB(RFC1950), DEFLATE(RFC1951), GZIP(RFC1952)和PKZIP在JavaScript实现。

    重新认识package.json

    {
      "name": "ls",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "type": "module",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC",
      "dependencies": {
        "chalk": "^5.0.1",
        "commander": "^9.4.0",
        "download-git-repo": "^3.0.2",
        "inquirer": "^9.1.0",
        "ora": "^6.1.2",
        "rimraf": "^3.0.2"
      },
      "browserslist": [
        "> 1%",
        "last 2 versions",
        "not ie <= 8"
      ],
      "keywords": [
        "share-cli",
        "cli"
      ],
      "engines": {
        "node": ">= 6.0.0",
        "npm": ">= 3.0.0",
        "yarn": "^0.13.0"
      }
    }
    

    如上是一个中规中矩的package.json文件,我们对于name、version、type、scripts等字段含义都很熟悉了,介绍几个跟发包相关的字段:private、engines、browserlist、bin

      1. private 如果设置为 true,则可以防止应用程序/软件包被意外地发布到 npm。
      1. engines设置了此软件包/应用程序在哪个版本的 Node.js 上运行。
      1. browserslist 用于告知要支持哪些浏览器
    这里重点介绍下bin字段:

    如果我们想在项目中执行一个node文件,直接 node +文件路径 就好了,但是我们要用脚手架,肯定不能这样。我们需要把项目发布到npm(或者tnpm),用户进行全局安装,然后直接使用我们自定义的命令,类似npm init rax这样。
    这个功能就是由 bin 实现的:将bin的字段命令映射到本地文件名, npm将软链接这个文件到prefix/bin里面或者在./node_modules/.bin/用一个demo来说明下:我们在package.json中添加:

    "bin": {
        "test-cli": "./bin.js"
      },
    

    然后项目根目录新建bin.js, 注意这个文件头部一定要 #!/usr/bin/env node, 使用 node 进行脚本的解释

    #!/usr/bin/env node
    function run (argv) {
        if (argv[0] === '-v' || argv[0] === '--version') {
            console.log('  version is 0.0.1');
        } else if (argv[0] === '-h' || argv[0] === '--help') {
            console.log('  usage:\n');
            console.log('  -v --version [show version]');
        }
    }
    run(process.argv.slice(2));
    

    必须要打成全局包才可以使用该命令,打成全局包的命令
    npm install . -g或者npm link
    执行 test-cli -v或者 test-cli -h

    在安装第三方带有bin字段的npm,那可执行文件会被链接到当前项目的./node_modules/.bin中
    举一反三:
    我们通过npm init rax 来初始化项目,那么rax也一定暴露了对应的bin可执行文件,
    首先 在控制台输入rax -v 或者rax -h

    找一下rax暴露的bin可执行文件吧where rax

    https://github.com/raxjs/rax-app/blob/master/packages/rax-cli/bin/rax.js

    实战

    我们以初始化项目模板为例,示例代码:

    
    #!/usr/bin/env node
    import fs from 'fs';
    import path from 'path';
    import chalk from 'chalk';
    import download from 'download-git-repo';
    import ora from 'ora';
    import inquirer from 'inquirer';
    import { exec } from 'child_process';
    import { Command } from 'commander';
    import checkDir from './checkDir.js';
    
    const commander = new Command();
    
    // const packageJsonDir = path.join(process.cwd(), 'package.json');
    // const packageJson = JSON.parse(fs.readFileSync(packageJsonDir, 'utf8'))
    // const { version } = packageJson;
    
    const promptTypeList = [{
        type: 'list',
        message: '请选择拉取的模版类型:',
        name: 'type',
        choices: [
            {
                name: '天猫优品的业务脚手架',
                value: {
                    url: "git@gitlab.XX:dinamic/miniapp-tracker.git#master", //仓库地址
                    gitName: 'web',
                    val: '天猫优品的业务脚手架'
                }
            },
            {
                name: '小程序',
                value: {
                    url: "git@gitlab.XX:dinamic/miniapp-tracker.git#master", //仓库地址
                    gitName: 'miniapp',
                    val: 'miniapp'
                }
            },
    
        ]
    }]
    
    commander.version('1.1.1', '-v, --version')
        .command('init <projectName>')
        .alias("i")
        .description("输入项目名称,初始化项目模版")
        .action(async (projectName, cmd) => {
            const dir = path.join(process.cwd(), projectName);
            await checkDir(path.join(process.cwd(), projectName), projectName);   // 检测创建项目文件夹是否存在
            inquirer.prompt(promptTypeList).then(result => {
                const { url, gitName, val } = result.type;
                console.log("您选择的模版类型信息如下:" + val);
                console.log('项目初始化拷贝获取中...');
                if (!url) {
                    console.log(chalk.red(`${val} 该类型暂不支持...`));
                    process.exit(1);
                }
                const spinner = ora(`Loading ${chalk.red(val)} 仓库到项目中`).start();
                download(
                    url,
                    dir,
                    { clone: true },
                    function (err) {
                        err ? spinner.fail('模板下载失败!') : spinner.succeed('模板下载成功!')
                    }
                );
                fs.rename(url, projectName, (err) => {
                    if (err) {
                        exec('rm -rf ' + gitName, function (err, out) { });
                        console.log(chalk.red(`The ${projectName} project template already exist`));
                    } else {
                        console.log(chalk.green(`✔ The ${projectName} project template successfully create(项目模版创建成功)`));
                    }
                });
            })
        });
    
     commander.parse(process.argv);
    
    tnpm logintnpm 
    publishnpm install @ ali/youpin-cli
    youpin-cli init demo-cli
    

    总结

    本篇文章是一篇入门级别的脚手架开发文章,介绍了脚手架需要的一些工具 commander、chalk、inquirer、ora等,以及package.json中的一些重要字段,最后通过实例demo来展示如何开发脚手架,希望可以为大家带来帮助!


    这里罗列了一些好用的node交互工具库,有兴趣可以研究下:

    相关文章

      网友评论

          本文标题:【转】前端脚手架开发入门

          本文链接:https://www.haomeiwen.com/subject/cxzhortx.html