likes
comments
collection
share

Nodejs 第二十二章 脚手架

作者站长头像
站长
· 阅读数 33

Nodejs 第二十二章 脚手架

随着现代网页应用程序的复杂性增加,前端开发的复杂度也相应提升。在这样的背景下,前端脚手架(Scaffolding)工具的出现就成为了开发流程中的一个必要工具

脚手架出现的原因

  1. 项目结构标准化:在没有脚手架的时候,每个开发者或团队可能会有自己的方式去组织项目结构,这导致了协作和维护的困难。脚手架帮助统一项目结构,使得新成员加入项目和理解代码变得更容易。
  2. 自动化流程:前端项目通常需要一系列重复性的设置步骤,包括配置文件的创建、目录结构的设置等。脚手架可以自动化这些流程,提升效率。
  3. 快速启动新项目:脚手架可以快速生成项目基础架构和文件,使开发者可以直接进入开发阶段,减少重复性工作。
  4. 技术栈整合:现代前端开发往往涉及多个技术栈的整合,如React、Vue、Angular、Babel、Webpack等,脚手架可以预先配置好这些工具的整合,减少开发者的配置负担。
  5. 规范开发流程:脚手架可以强制执行代码风格、提交信息格式等规范,保持团队开发风格的一致性。

掌握脚手架的必要性

  1. 提升开发效率:通过减少重复和繁琐的初始化工作,脚手架让开发者能够更快地开始实际的开发工作。
  2. 减少错误:手动创建项目结构和配置可能会引入错误。脚手架通过自动化过程,降低了犯错的机会。
  3. 教育和学习:对于新手开发者,脚手架提供了学习项目结构和配置的机会,也帮助他们了解行业最佳实践。
  4. 适应新技术:随着新技术和工具的不断出现,脚手架帮助开发者适应新的技术生态,维护项目的现代化。
  5. 保持竞争力:在激烈的市场竞争中,能够快速启动和交付项目是至关重要的。脚手架工具提供了这一能力,确保团队能够快速响应市场需求。

而理解脚手架最快的方法就是自己实现一遍,那流程不就通透了,有哪个前端不想拥有自己的一套脚手架,在这一章节你会用到和学到非常多的第三方库

  • 让我们来看一个通过npm init vue初始化后的vue脚手架吧!

Nodejs 第二十二章 脚手架

  • 完成一个详细完整的脚手架是很困难的,但通过一个小突破,实现以点破面却是可以降低难度,那就让我们正式开始吧!

工具介绍

  • 让我们来看下在接下来搭建脚手架中,会用到的第三方库

  • 在使用之前,一定要记得先下载,毕竟这些都是第三方库

    • npm i commander inquirer ora download-git-repo

commander

Commander 是一个用于构建命令行工具的 npm 库。它提供了一种简单而直观的方式来创建命令行接口,并处理命令行参数和选项。使用 Commander,可以轻松定义命令、子命令、选项和帮助信息。它还可以处理命令行的交互,使用户能够与你的命令行工具进行交互

 // 引入 Commander 库
 const { program } = require('commander');
 ​
 // 设置你的CLI工具的版本和描述
 program
   .version('0.0.1')
   .description('An example CLI tool built with Commander');
 ​
 // 定义一个命令和它的参数,以及执行该命令时要运行的动作
 program
   .command('greet <name>')//这里的<xxx>中,xxx是可以随意命名的,最后通过action方法可以拿到xxx的内容
   .description('say hello to <name>')//这是一个给用户的描述
   .action((name) => {
     console.log(`Hello, ${name}!`);
   });
 ​
 // 定义一个带选项的命令
 program
   .command('order <type>')
   .description('order a pizza')
   .option('-s, --size <size>', 'choose the size of the pizza', 'medium')
   .action((type, options) => {
     console.log(`Order received: ${options.size} ${type}`);
   });
 ​
 // 解析命令行参数
 program.parse(process.argv);
 ​
 // 1.引入Commander库,并从中获取program对象。
 // 2.设置CLI工具的版本和描述,这会显示在帮助信息中。
 // 3.用command()方法定义一个名为greet的命令,接受一个参数<name>。当greet命令被调用时,action()中的函数会执行,并打印出问候语。
 // 4.又定义了一个order命令,它带有一个<type>参数和一个--size选项。如果用户不指定--size,它会默认为medium。命令执行时会反馈订单信息。
 // 5.最后,program.parse()解析命令行参数并触发对应的命令。
  • 要使用这个脚本,我们可以将它保存为一个.js文件,并在命令行中通过Node.js运行。例如,如果保存为cli.js,可以这样用:
 node cli.js greet XiaoYu
 ​
 //然后会输出 Hello, XiaoYu!
 //其中greet是命令,而XiaoYu是参数
  • 或者,可以这样使用order命令:
 node cli.js order pizza --size large
 ​
 //会输出Order received: large pizza

Nodejs 第二十二章 脚手架

Inquirer

Inquirer 是一个强大的命令行交互工具,用于与用户进行交互和收集信息。它提供了各种丰富的交互式提示(如输入框、选择列表、确认框等),可以帮助你构建灵活的命令行界面。通过 Inquirer,你可以向用户提出问题,获取用户的输入,并根据用户的回答采取相应的操作。

Inquirer支持多种类型的问题,例如:

  • input:普通的文本输入。
  • number:数字输入。
  • confirm:确认框,用户输入yes或no。
  • list:允许用户从列表中选择一个选项。
  • checkbox:允许用户选择多个选项。
  • password:隐藏输入内容。
 // 引入Inquirer库
 const inquirer = require('inquirer');
 ​
 // 定义一组问题
 const questions = [
   {
     type: 'input', // 文本输入类型
     name: 'name', // 接收输入值的变量名
     message: 'What is your name?', // 显示给用户的提示信息
     validate: function(value) { // 输入验证函数
       var pass = value.match(
         /^[a-zA-Z\s]+$/i
       );
       if (pass) {
         return true;
       }
 ​
       return 'Please enter a valid name (alphabetical characters only)';
     }
   },
   {
     type: 'confirm', // 确认类型
     name: 'likeNode', // 接收输入值的变量名
     message: 'Do you like Node.js?', // 显示给用户的提示信息
     default: true // 默认值
   },
   {
     type: 'list', // 列表选择类型
     name: 'favoriteFramework', // 接收输入值的变量名
     message: 'Which Node.js framework do you prefer?', // 显示给用户的提示信息
     choices: ['Express', 'Koa', 'Hapi', 'Sails'], // 可选择的列表项
     filter: function(val) { // 过滤处理输入值
       return val.toLowerCase();
     }
   }
 ];
 ​
 // 使用prompt方法显示问题并接收用户的输入
 inquirer.prompt(questions).then(answers => {
   // 输出用户的回答
   console.log(`Hello ${answers.name}, you said ${answers.likeNode ? 'yes' : 'no'} to liking Node.js.`);
   console.log(`Your favorite Node.js framework is ${answers.favoriteFramework}.`);
 });

ora

Ora 是一个用于在命令行界面显示加载动画的 npm 库。它可以帮助你在执行耗时的任务时提供一个友好的加载状态提示。Ora 提供了一系列自定义的加载动画,如旋转器、进度条等,你可以根据需要选择合适的加载动画效果,并在任务执行期间显示对应的加载状态。

  • 创建一个基本的加载指示器(spinner)

    • 我们使用start()方式进行简单的旋转(也可以实现更多我们想要的加载状态),并且调用成功或者失败的方法来进一步实现后续提示
 // 引入 ora 库
 const ora = require('ora');
 ​
 // 创建一个 spinner 对象并开始旋转
 const spinner = ora('Loading unicorns').start();
 ​
 // 模拟一项长时间运行的任务,比如数据加载或者复杂计算
 setTimeout(() => {
     // 任务完成后,可以调用 succeed 方法标记成功完成
     spinner.succeed('Unicorns loaded successfully');
 ​
     // 或者,如果任务失败,可以调用 fail 方法
     // spinner.fail('Failed to load unicorns');
 }, 2000); // 模拟任务运行了 2000 毫秒(2秒)

download-git-repo

Download-git-repo 是一个用于下载 Git 仓库的 npm 库。它提供了一个简单的接口,可以方便地从远程 Git 仓库中下载项目代码。你可以指定要下载的仓库和目标目录,并可选择指定分支或标签。Download-git-repo 支持从各种 Git 托管平台(如 GitHub、GitLab、Bitbucket 等)下载代码。

 // 引入 download-git-repo 库
 const download = require('download-git-repo');
 ​
 // 定义仓库来源,格式为:'直接仓库地址或平台:用户名/仓库名#分支'
 // 例如,从GitHub下载Node.js仓库的master分支
 const repository = 'github:nodejs/node#master';
 ​
 // 定义目标目录,即将仓库代码下载到哪里
 const destination = './node-repo';
 ​
 // 调用 download 函数下载仓库
 download(repository, destination, { clone: true }, function (err) {
     if (err) {
         console.error('Failed to download repo ' + repository + ': ' + err.message);
     } else {
         console.log('Successfully downloaded ' + repository + ' into ' + destination);
     }
 });

使用步骤

  1. 引入库

    • 使用require方法引入download-git-repo模块。
  2. 设置仓库来源

    • repository变量设置为从GitHub下载Node.js项目的master分支。
    • 格式遵循download-git-repo的规定,通常是platform:user/repo#branch
  3. 设置目标目录

    • destination变量定义了下载文件的存放路径,这里是当前目录下的node-repo文件夹。
  4. 下载仓库

    • 调用download函数,传入上面定义的repositorydestination
    • 第三个参数{ clone: true }指定使用git clone进行下载,这通常更快,并能保留.git目录。
    • 该函数异步执行,提供了一个回调函数来处理完成后的成功或失败情况。

编写脚手架

脚手架需求

  • 编写的脚手架需要满足以下几点要求:

    1. 不使用node开头的命令,而是自定义的命令去执行我们的脚本
    2. 在命令后面能附带参数例如-V --help 等功能去和命令行交互
    3. 根据搭配好的配置,能做到去下载模板到本地
  • 根据前面第三方库的介绍,我想我们是可以实现的了

自定义脚本

  • 想要实现第一点需求,我们需要在文件最上面加上一行代码
 #!/usr/bin/env node
  • 这行代码是很重要的,代表着node执行文件从显式变成了隐式

    • 相当于告诉操作系统,我执行自定义命令的时候,你帮我用node去执行这个文件
    • 比如说 node xiaoyu-cli xxx就可以变成xiaoyu-cli xxx
  • 加上之后,我们就需要去package.json文件中配置一下这个命令

    • 就算是类似启动命令npm run dev都需要在package.json中进行配置,而我们的自定义脚本也是一样的
    • 首先我们确认一下type类型为module,然后进行bin配置
    • 通过bin配置,我们就将我们的自定义命令(xiaoyu-cli)入口文件(index.js) 映射在一起了

Nodejs 第二十二章 脚手架

  • 那此时的话,其实还不能使用。因为我们还没将这个命令挂载到全局,所以说执行的时候是找不到这个命令的

    • 这个时候就可以通过npm link创建一个软链接,把我们的文件挂载到全局上了。想要挂载记得package.json文件是要有name这个属性才可以的
    • 这样就可以使用了

Nodejs 第二十二章 脚手架

  • 然后我们能看到,欸?为什么前面连着我们的路径都打印出来了,而且console.log()本身也显示出来了?

    • 这是因为我们忘记在最上面加上#!/usr/bin/env node
    • 像下图这样就完全没问题了

Nodejs 第二十二章 脚手架

创建自己附带参数

  • 这个就是我们的第二步操作了

    • 我们已经实现了xiaoyu-cli开头的自定义命令,那如何更近一步,变成xiaoyu-cli xxxx?其中xxxx是参数
    • 这时候我们就需要用到第一个库commander了,用这个库来解析我们的参数
  • 那我们要如何获取到我们输入的这些参数?

    • 在前面中,我们process的那章节,就通过process.argv获取到了后面输入的参数,返回一个数组
    • 将参数赋值给这个库,是放在最后面的
    • 因为在前面我们需要先设置好自定义的参数配置,最后再把拿到的参数和我们已经准备好的参数配置进行配合。由于代码是由上向下执行的,如果赋值参数放在前面,参数配置信息在后面。则前面匹配了个寂寞
  • 那我们就先实现一个版本号的获取
 #!/usr/bin/env node
 import { program } from "commander"
 ​
 program.version('1.0.0')
 ​
 program.parse(process.argv)//通过process.argv拿到了终端输入的参数

Nodejs 第二十二章 脚手架

  • 通过结果,我们可以看到确实是输出了版本号了。但这个版本号是写死的,正常情况下哪里可以写死呢?我们应该要从package.json中去获取才行

    • 由于我们在package.json中进行配置了type为modules了,所以我们只能使用ESM的写法
    • 使用fs模块获取package.json文件的内容,且由于获取到的内容是不是JSON格式的,我们没办法直接拿到里面的version版本,所以需要使用JSON.parse进行转化一下
 #!/usr/bin/env node
 import { program } from "commander"
 import fs from "node:fs"
 ​
 //获取到package.json文件的内容
 let json = fs.readFileSync('./package.json', 'utf-8')
 //进行格式转化(JSON格式)
 json = JSON.parse(json);
 //获取文件内JSON内容中的version版本
 program.version(json.version)
 ​
 ​
 program.parse(process.argv)

Nodejs 第二十二章 脚手架

  • 通过这步骤进行操作,很显然是成功了
  • 那我们接下来就按部就班的,当用户进行创建项目的时候,我们让他起一个项目名称和选择是否选用TS的模板
 #!/usr/bin/env node
 import { program } from "commander"
 import fs from "node:fs"
 ​
 ​
 let json = fs.readFileSync('./package.json', 'utf-8')
 json = JSON.parse(json);
 program.version(json.version)
 ​
 program.command('create <projectName>').description('创建新项目').action(res => {
   console.log(res);
 })
 ​
 program.parse(process.argv)

Nodejs 第二十二章 脚手架

  • 通过这样,我们简单的自定义了create参数以及参数之后的内容进行打印输出

    • 以这个为输入口,我们创建项目之后,就可以开始给这个即将要创建的模板起目录名字(或者叫做文件夹名字)和是否选用TS模板
    • 对其进行一个样式的优化,就完成一大半了
    • 而样式的优化和互动,来自Inquirer这个第三方库,我们要进行使用了
 #!/usr/bin/env node
 import { program } from "commander"
 import inquirer from 'inquirer'
 import fs from "node:fs"
 ​
 ​
 let json = fs.readFileSync('./package.json', 'utf-8')
 json = JSON.parse(json);
 program.version(json.version)
 ​
 program.command('create <projectName>').description('创建新项目').action(projectName => {
   inquirer.prompt([
     // 设置项目名称
     {
       type: 'input',
       name: 'projectName',
       message: '请输入项目名称',
       default: projectName
     },
     // 设置是否选用TS模板
     {
       type: "confirm",
       name: "isTS",
       message: "项目是否支持TypeScript"
     }
   ]).then(answers => {
     console.log(answers)
   })
 })
 ​
 program.parse(process.argv)

Nodejs 第二十二章 脚手架

  • 而第二个prompt的type类型是一个可选项confirm,Y为Yes,n为No

Nodejs 第二十二章 脚手架

  • 当我们输入结束之后,结果会以Promise的方式,以then的方式进行返还结果,我们接收到的内容则是一个对象形式的数据

Nodejs 第二十二章 脚手架

验证路径

  • 当然,我们已经实现了一大半的功能了,此时我们需要进行一点边界条件的判断

    • 我们创建的这个模板,是否会与当前文件夹内的文件名发送冲突?
    • 对于这个验证路径的函数方法,我们就写在utils.js文件中,然后导入index.js入口文件即可,避免入口文件的臃肿
 import fs from 'node:fs'
 //验证路径
 export const checkPath = (path) => {
     return fs.existsSync(path) ? true : false
 }
  • 然后导入到index.js中,且在then后进行判断
 import { checkPath } from "./utils.js"
 ​
 //前面的不涉及内容进行省略
 .then(answers => {
     // 判断当前创建的文件夹是否与已有文件夹重名
     if(checkPath(answers.projectName)){
       console.log("文件夹已经存在");
       return
     }
     // 判断是否需要TS模板
     if(answers.isTS){
       console.log("TypeScript模板")
       }
   })

下载模板到本地

  • 此时根据已经设置好的内容,我们要使用另一个第三方库**download-git-repo**,将放在网上的模板下载到本地了

    • 而这个下载逻辑,我们也放到utils工具文件内
    • 而第三个参数{ clone: true }指定使用git clone进行下载,这通常更快,并能保留.git目录
 //下载,我们就通过branch来确定分支,然后在git路径后面加上#分支,就能切到想要下载的分支上了
 export const downloadTemp = (branch,project) => {
     return new Promise((resolve,reject)=>{
       //需要加上direct:这个前缀,而project则是我们的项目目录名称
         download(`direct:https://gitee.com/chinafaker/vue-template.git#${branch}`, project , { clone: true, }, function (err) {
             if (err) {
                 reject(err)
                 console.log(err)
             }
             resolve()
         })
     })
 }
  • 通过这个gitee仓库,提前建立好了分支,这样就可以根据是否选择TS模板,选择不同的模板分支进行下载到本地

Nodejs 第二十二章 脚手架

为什么需要direct:前缀

  1. 绕过预设的解析器

    • download-git-repo默认支持GitHub, GitLab, 和Bitbucket的简写形式,例如github:user/repogitlab:user/repo
    • 使用direct:前缀允许我们绕过这种自动解析,直接指向一个具体的Git仓库URL。
  2. 明确指定仓库位置

    • 当从一个不属于上述三大托管平台的Git仓库下载时,例如自托管的Git仓库或小众平台,需要明确指出完整的Git URL。
    • 这样download-git-repo就不会尝试去解析URL,而是直接使用我们提供的链接。
  3. 使用完整的Git URL

    • 通过使用完整的Git URL,可以更精确地控制下载的仓库和分支。
    • 这种方式同样支持访问私有仓库,只要URL中包含了足够的权限信息(如在URL中嵌入访问令牌)。

ora的使用

  • 为了在下载模板内容到本地的时候有一个加载动画,以免等待太过枯燥.我们可以使用ora进行配置

    • 在utils工具文件中,导入ora:const spinner = ora('下载中...'),然后放在下载函数中
 const spinner = ora('下载中...')
 export const downloadTemp = (branch,project) => {
     spinner.start()//通过这个方法使用加载动画
     return new Promise(
       //省略...
        spinner.succeed('下载完成')//在下载完了之后,给出下载成功的提示
       )
 }
  • 然后在index入口文件中进行配置
 import { checkPath , downloadTemp } from "./utils.js"
 ​
 //无关内容已经省略
 .then(answers => {
   // 判断是否需要TS模板
     answers.isTs ? downloadTemp('ts', answers.projectName) : downloadTemp('js', answers.projectName)
 })
  • 这样就有下载的动画了

Nodejs 第二十二章 脚手架

  • 进行完整步骤的流程,能够看到我们已经成功了

Nodejs 第二十二章 脚手架

  • 同时,我们需要验证一下当目录下已经存在该文件名的时候,我们前面设置的验证路径边界判断是否生效

    • 通过下图,也是输出了文件夹已经存在,没有继续拉取代码进行覆盖本地文件

Nodejs 第二十二章 脚手架

完整代码

  • 那通过上面的步骤流程,我们也是完整的实现了一次如何自己配置脚手架,以下就是我配置脚手架用到的完整代码

index.js

 #!/usr/bin/env node
 import { program } from "commander"
 import inquirer from 'inquirer'
 import fs from "node:fs"
 ​
 import { checkPath , downloadTemp } from "./utils.js"
 ​
 ​
 let json = fs.readFileSync('./package.json', 'utf-8')
 json = JSON.parse(json);
 program.version(json.version)
 ​
 program.command('create <projectName>').description('创建新项目').action(projectName => {
   inquirer.prompt([
     // 设置项目名称
     {
       type: 'input',
       name: 'projectName',
       message: '请输入项目名称',
       default: projectName
     },
     // 设置是否选用TS模板
     {
       type: "confirm",
       name: "isTS",
       message: "项目是否支持TypeScript"
     }
   ]).then(answers => {
     // 判断当前创建的文件夹是否与已有文件夹重名
     if(checkPath(answers.projectName)){
       console.log("文件夹已经存在");
       return
     }
     // 判断是否需要TS模板
     answers.isTs ? downloadTemp('ts', answers.projectName) : downloadTemp('js', answers.projectName)
   })
 })
 ​
 program.parse(process.argv)

utils文件

 import fs from 'node:fs'
 import download from 'download-git-repo'
 import ora from 'ora'
 const spinner = ora('下载中...')
 //验证路径
 export const checkPath = (path) => {
   return fs.existsSync(path) ? true : false
 }
 ​
 //下载
 export const downloadTemp = (branch,project) => {
     spinner.start()
     return new Promise((resolve,reject)=>{
         download(`direct:https://gitee.com/chinafaker/vue-template.git#${branch}`, project , { clone: true, }, function (err) {
             if (err) {
                 reject(err)
                 console.log(err)
             }
             resolve()
             spinner.succeed('下载完成')
         })
     })
 }

package.json文件

 {
   "name": "xiaoyu",
   "main": "index.js",
   "version": "1.0.1",
   "scripts": {
     "dev": "node index.js"
   },
   "type": "module",
   "bin": {
     "xiaoyu-cli":"src/index.js"
   },
   "description": "",
   "dependencies": {
     "commander": "^12.0.0",
     "download-git-repo": "^3.0.2",
     "inquirer": "^9.2.19",
     "ora": "^8.0.1"
   }
 }
转载自:https://juejin.cn/post/7359464203357765658
评论
请登录