🔥前端脚手架辣么多,那我也要写一个玩玩

2022-04-12  本文已影响0人  恪晨

前言

  2022年已经过了四分之一还多了,之前说好的每个月一片文章好像也没有让自己兑现。最近公司在做一些前端工程化相关的东西,虽然准备做组件库的事情被领导给毙了,不过在这之前写了一个脚手架的工具,毕竟现在这个环境下,脚手架工具泛滥,所以当然也要写一写玩玩。

最终效果

cliInit.jpeg

支持功能

开发

初始化项目

  那么接下来就开始开发了,首先我们来新建一个项目文件夹就叫new-cli吧,在项目文件夹中新建package.json文件,设置常用的字段,设置完成后如下:

{
  "name": "new-cli",
  "version": "1.0.0",
  "description": "a react project cli, help you create a react project quickly",
  "bin": {
    "new-cli": "bin/www.js"
  },
  "dependencies": {
    "boxen": "^5.1.2",
    "chalk": "^4.1.2",
    "commander": "^9.1.0",
    "consolidate": "^0.16.0",
    "cross-spawn": "^7.0.3",
    "download-git-repo": "^3.0.2",
    "ejs": "^3.1.6",
    "fs-extra": "^10.0.1",
    "inquirer": "^8.2.1",
    "metalsmith": "^2.4.2",
    "ora": "^5.4.1",
    "figlet": "^1.5.2",
    "semver": "^7.3.5",
    "shelljs": "^0.8.5"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/BoWang816/new-cli.git"
  },
  "keywords": [
    "cli",
    "react"
  ],
  "author": "恪晨",
  "publishConfig": {
    "registry": "私有仓库地址"
  },
  "engines": {
    "node":"^12.20.0 || >=14"
   }
}

  通过以上设置以后,我们的脚手架名字就叫new-cli,也就是说到时候安装的时候就是通过npm install -g new-cli进行安装。bin下面设置的名称就是为了设置脚手架执行的命令,并且是从bin/www.js文件作为了入口文件;dependencies中为我们需要的项目依赖,值得注意的是像boxen、chalk、figlet这一类的依赖包在最新版本中已经不支持requier方式引入了所以这里我们需要安装低版本的包;publishConfig中可以设置到时候需要发布的npm地址,如果你搭建了npm私服则通过设置registry就可以发布到你的私服了。

设置项目入口

  建好package.json以后我们就开始建入口文件,也就是bin下面的www.js,事实上你的入口文件放置在根目录也是可以的,可以根据自己的喜好,当然如果放置在了根目录,则bin下面就要改为new-cli: './www.js'www.js中主要是引入commander、inquirer等工具包,进行脚手架工具的初始化。因为www.js将要作为一个node脚本来运行,因此需要在最上方声明环境:#! /usr/bin/env node,我写的这个脚手架中涉及到了init、update、help这三个命令,并且help是commander本身就支持的,这里只是做了一点定制化。

  通过设置以上内容,其实我们就可以使用基本的命令了。本地调试的方式有两种,一种是通过npm link命令将我们写的脚手架工具直接链接到本地的全局npm中,一种则是直接通过node bin/www.js直接执行这个js文件,这里我们使用后者就可以了。

initCommand.jpeg
const chalk = require("chalk");
const path = require("path");
const fs = require('fs-extra');
const figlet = require('figlet');
const create = require('../utils/create');
    
program
    .command("init <project-name>")
    .description("create a new project name is <project-name>")
    .option("-f, --force", "overwrite target directory if it exists")
    .action(async (projectName, options) => {
        const cwd = process.cwd();
        // 拼接到目标文件夹
        const targetDirectory = path.join(cwd, projectName);
        // 如果目标文件夹已存在
        if (fs.existsSync(targetDirectory)) {
            if (!options.force) {
            // 如果没有设置-f则提示,并退出
                console.error(chalk.red(`Project already exist! Please change your project name or use ${chalk.greenBright(`new-cli create ${projectName} -f`)} to create`))
                return;
            }
            // 如果设置了-f则二次询问是否覆盖原文件夹
            const {isOverWrite} = await inquirer.prompt([{
                name: "isOverWrite",
                type: "confirm",
                message: "Target directory already exists, Would you like to overwrite it?",
                choices: [
                    {name: "Yes", value: true},
                    {name: "No", value: false}
                ]
            }]);
            // 如需覆盖则开始执行删除原文件夹的操作
            if (isOverWrite) {
                const spinner = ora(chalk.blackBright('The project is Deleting, wait a moment...'));
                spinner.start();
                await fs.removeSync(targetDirectory);
                spinner.succeed();
                console.info(chalk.green("✨ Deleted Successfully, start init project..."));
                console.log();
                // 删除成功后,开始初始化项目
                // await create(projectName);
                console.log('init project overwrite');
                return;
            }
            console.error(chalk.green("You cancel to create project"));
            return;
        }
        // 如果当前路径中不存在同名文件夹,则直接初始化项目
        // await create(projectName);
        console.log('init project');
    });

我们再来查看现在的效果:


initProject.jpeg
&emsp;&emsp;我们将这些询问的list抽离为常量,同时也将模板的地址抽离为常量,因此需要在utils文件夹下建立一个`constants.js`的文件,里面的内容如下: 
  ```js
    /**
     * constants.js
     * @author kechen
     * @since 2022/3/25
     */
    
    const { version } = require('../package.json');
    
    const baseUrl = 'https://github.com/BoWangBlog';
    const promptList = [
        {
            name: 'type',
            message: 'Which build tool to use for the project?',
            type: 'list',
            default: 'webpack',
            choices: ['webpack', 'vite'],
        },
        {
            name: 'frame',
            message: 'Which framework to use for the project?',
            type: 'list',
            default: 'react',
            choices: ['react', 'vue'],
        },
        {
            name: 'setRegistry',
            message: "Would you like to help you set registry remote?",
            type: 'confirm',
            default: false,
            choices: [
                {name: "Yes", value: true},
                {name: "No", value: false}
            ]
        },
        {
            name: 'gitRemote',
            message: 'Input git registry for the project: ',
            type: 'input',
            when: (answers) => {
                return answers.setRegistry;
            },
            validate: function (input) {
                const done = this.async();
                setTimeout(function () {
                    // 校验是否为空,是否是字符串
                    if (!input.trim()) {
                        done('You should provide a git remote url');
                        return;
                    }
                    const pattern = /^(http(s)?:\/\/([^\/]+?\/){2}|git@[^:]+:[^\/]+?\/).*?.git$/;
                    if (!pattern.test(input.trim())) {
                        done(
                            'The git remote url is validate',
                        );
                        return;
                    }
                    done(null, true);
                }, 500);
            },
        }
    ];
    
    module.exports = {
        version,
        baseUrl,
        promptList
    }
  ```
 &emsp;&emsp;其中*version*为我们的脚手架版本号,*baseUrl*为项目模板下载的基础地址,*promptList*为询问用户的问题列表,promptList的具体写法是根据`inquirer.prompt()`方法来写的,具体的怎么写后面我都会将官方文档地址附上,大家可以自己发挥。

  该文件中,我们使用了download-git-repo这个第三方的工具库,用于下载项目模板,因为download-git-repo的返回结果是下载成功或者失败,我们在使用异步的方式的时候如果直接使用会存在问题,因此这里封装为promise,当err的时候给用户抛出异常提示,成功则将目标文件夹路径返回用于后续使用。在create.js中我们使用了go函数,在go函数执行成功后会返回一个data,里面拿到了项目要下载到具体的文件夹的路径,其实主要是为了获取在download中的promise的resolve结果,拿到目标文件夹的路径后,其实项目模板已经下载到了该文件夹中,就可以开始renderTemplate了。

其他花里胡哨的东东

  主要功能基本就是上面这些啦,另外我们需要加一个项目创建成功之后的提示,在上文的create.js中最后面有一个downloadSuccessfully的方法,其实就是创建成功后的提示,主要内容如下:

const downloadSuccessfully = (projectName) => {
    const END_MSG = `${chalk.blue("🎉 created project " + chalk.greenBright(projectName) + " Successfully")}\n\n 🙏 Thanks for using wb-cli !`;
    const BOXEN_CONFIG = {
        padding: 1,
        margin: {top: 1, bottom: 1},
        borderColor: 'cyan',
        align: 'center',
        borderStyle: 'double',
        title: '🚀 Congratulations',
        titleAlignment: 'center'
    }

    const showEndMessage = () => process.stdout.write(boxen(END_MSG, BOXEN_CONFIG))
    showEndMessage();

    console.log('👉 Get started with the following commands:');
    console.log(`\n\r\r cd ${chalk.cyan(projectName)}`);
    console.log("\r\r npm install");
    console.log("\r\r npm run start \r\n");
}

具体的实现效果就是这样的,这里我是截了之前做好的图。


项目模板

  我们需要创建一个项目模板,里面需要在根目录下包含一个ask.ts文件,其他的就和正常项目一样就好了,aks.ts的文件内容示例如下,

/**
 * demo
 * aks.ts
 * @author kechen
 * @since 2022/3/24
 */

module.exports = [
  {
    name: 'description',
    message: 'Please enter project description:',
  },
  {
    name: 'author',
    message: 'Please enter project author:',
  },
  {
    name: 'apiPrefix',
    message: 'Please enter project apiPrefix:',
    default: 'api/1.0',
    // @ts-ignore
    validate: function (input) {
      const done = this.async();
      setTimeout(function () {
        // 校验是否为空,是否是字符串
        if (!input.trim()) {
          done(
            'You can provide a apiPrefix, or not it will be default【api/1.0】',
          );
          return;
        }
        const pattern = /[a-zA-Z0-9]$/;
        if (!pattern.test(input.trim())) {
          done(
            'The apiPrefix is must end with letter or number, like default 【api/1.0】',
          );
          return;
        }
        done(null, true);
      }, 300);
    },
  },
  {
    name: 'proxy',
    message: 'Please enter project proxy:',
    default: 'https://www.test.com',
    // @ts-ignore
    validate: function (input) {
      const done = this.async();
      setTimeout(function () {
        // 校验是否为空,是否是字符串
        if (!input.trim()) {
          done(
            'You can provide a proxy, or not it will be default【https://www.test.com】',
          );
          return;
        }
        const pattern =
          /(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/;
        if (!pattern.test(input.trim())) {
          done(
            'The proxy is must end with letter or number, like default 【https://www.test.com】',
          );
          return;
        }
        done(null, true);
      }, 300);
    },
  },
];

  这里我设置了四个变量分别是description、author、apiPrefix、proxy,在使用时只需要通过<%= var %>这种方式就可以了,var可以是你在ask.ts中设置的任何变量,具体使用demo如下,当然要替换的文件类型必须是在上面我们提到的renderTemplate函数中设置了后缀名的文件才可以。**使用这种方式,你就可以在项目模板中自由添加变量,且不需要更新脚手架工具。

{
  "name": "xasrd-fe-mobile",
  "description": "<%= description %>",
  "private": true,
  "author": "<%= author %>"
}

  至此,我们的脚手架就全部开发完成啦,接下来就是怎么发布到npm或者npm私服了。

发布

  在上面我们讲过,如果需要发布的npm私服,则需要在package.json中配置publishConfig并指向npm私服的地址,发布的时候则需要通过以下命令进行发布:

当然需要注意的是,发布的时候,package.json中的version版本号不能重复哈!!!

总结

  到这里,我们就完整的开发了一个比较简单前端脚手架工具,并可以发布使用了。其实具体的做法并不是很难,有很多第三方的工具包可以用,当然因为这个工具的交互相对来说比较简单,各位也可以自己奇思妙想,做一些更加花里胡哨的功能进行扩展。示例的demo就不放啦,基本所有的内容都在上面提到了,大家可以自由发挥。当然基于这套我自己也写了一个地址是https://www.npmjs.com/package/wb-fe-cli,不过因为最近实在没时间,所以项目模板还没有,暂时还不能完整的跑起来,后续会慢慢更新的。

参考

结语

  最后希望看完本篇文章后,对你有所帮助,勤快的话可以自己动手手写一写啦。另外希望大家能够关注一下我的Github,哈哈哈哈,带你们看贪吃蛇!
  也可以关注一下GridManager这个好用的表格插件,支持React、Vue,非常好用哦!
  下期,我将会给大家带来一些我常用的Mac软件的介绍,能够帮助你在日常开发与工作中大大提升工作效率!!!可以先预览一下 恪晨的Mac软件推荐

感谢大家的阅读!!

上一篇下一篇

猜你喜欢

热点阅读