1. 首页 > 快讯

Nodejs 第二十二章 脚手架


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

脚手架出现的原因

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

掌握脚手架的必要性

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

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

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

工具介绍

  • 让我们来看下在接下来搭建脚手架中,会用到的第三方库
  • 在使用之前,一定要记得先下载,毕竟这些都是第三方库
    • 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

Inquirer

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

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

  • input:普通的文本输入。
  • number:数字输入。
  • confirm:确认框,用户输入yes或no。
  • list:允许用户从列表中选择一个选项。
  • checkbox:允许用户选择多个选项。
  • password:隐藏输入内容。
// 引入Inquirer库 constinquirer =require('inquirer'); // 定义一组问题 constquestions = [
{ type:'input',// 文本输入类型 name:'name',// 接收输入值的变量名 message:'What is your name?',// 显示给用户的提示信息 validate:function(value){// 输入验证函数 varpass = value.match( /^[a-zA-Z\s]+$/i ); if(pass) { returntrue;
} 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){// 过滤处理输入值 returnval.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 库 constora =require('ora'); // 创建一个 spinner 对象并开始旋转 constspinner = 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 库 constdownload =require('download-git-repo'); // 定义仓库来源,格式为:'直接仓库地址或平台:用户名/仓库名#分支' // 例如,从GitHub下载Node.js仓库的master分支 constrepository ='github:nodejs/node#master'; // 定义目标目录,即将仓库代码下载到哪里 constdestination ='./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函数,传入上面定义的repository和destination。
    • 第三个参数{ 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)**映射在一起了
  • 那此时的话,其实还不能使用。因为我们还没将这个命令挂载到全局,所以说执行的时候是找不到这个命令的

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

    • 这样就可以使用了

  • 然后我们能看到,欸?为什么前面连着我们的路径都打印出来了,而且console.log()本身也显示出来了?
    • 这是因为我们忘记在最上面加上\#!/usr/bin/env node了
    • 像下图这样就完全没问题了

创建自己附带参数

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

    • 我们已经实现了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拿到了终端输入的参数
  • 通过结果,我们可以看到确实是输出了版本号了。但这个版本号是写死的,正常情况下哪里可以写死呢?我们应该要从package.json中去获取才行
    • 由于我们在package.json中进行配置了type为modules了,所以我们只能使用ESM的写法
    • 使用fs模块获取package.json文件的内容,且由于获取到的内容是不是JSON格式的,我们没办法直接拿到里面的version版本,所以需要使用JSON.parse进行转化一下
#!/usr/bin/env node import{ program }from"commander" importfsfrom"node:fs" //获取到package.json文件的内容 letjson = fs.readFileSync('./package.json','utf-8') //进行格式转化(JSON格式) json =JSON.parse(json); //获取文件内JSON内容中的version版本 program.version(json.version)


program.parse(process.argv)
  • 通过这步骤进行操作,很显然是成功了
  • 那我们接下来就按部就班的,当用户进行创建项目的时候,我们让他起一个项目名称和选择是否选用TS的模板
#!/usr/bin/env node import{ program }from"commander" importfsfrom"node:fs" letjson = 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)
  • 通过这样,我们简单的自定义了create参数以及参数之后的内容进行打印输出
    • 以这个为输入口,我们创建项目之后,就可以开始给这个即将要创建的模板起目录名字(或者叫做文件夹名字)和是否选用TS模板
    • 对其进行一个样式的优化,就完成一大半了
    • 而样式的优化和互动,来自Inquirer这个第三方库,我们要进行使用了
#!/usr/bin/env node import{ program }from"commander" importinquirerfrom'inquirer' importfsfrom"node:fs" letjson = 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)
  • 而第二个prompt的type类型是一个可选项confirm,Y为Yes,n为No
  • 当我们输入结束之后,结果会以Promise的方式,以then的方式进行返还结果,我们接收到的内容则是一个对象形式的数据

验证路径

  • 当然,我们已经实现了一大半的功能了,此时我们需要进行一点边界条件的判断
    • 我们创建的这个模板,是否会与当前文件夹内的文件名发送冲突?
    • 对于这个验证路径的函数方法,我们就写在utils.js文件中,然后导入index.js入口文件即可,避免入口文件的臃肿
importfsfrom'node:fs' //验证路径 exportconstcheckPath =(path) =>{ returnfs.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路径后面加上#分支,就能切到想要下载的分支上了 exportconstdownloadTemp =(branch,project) =>{ returnnewPromise((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模板,选择不同的模板分支进行下载到本地

为什么需要direct:前缀

  1. 绕过预设的解析器
    • download-git-repo默认支持GitHub,GitLab, 和Bitbucket的简写形式,例如github:user/repo或gitlab: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('下载中...'),然后放在下载函数中
constspinner = ora('下载中...') exportconstdownloadTemp =(branch,project) =>{
spinner.start()//通过这个方法使用加载动画 returnnewPromise( //省略... spinner.succeed('下载完成')//在下载完了之后,给出下载成功的提示 )
}
  • 然后在index入口文件中进行配置
import{ checkPath , downloadTemp }from"./utils.js" //无关内容已经省略 .then(answers=>{ // 判断是否需要TS模板 answers.isTs ? downloadTemp('ts', answers.projectName) : downloadTemp('js', answers.projectName)
})
  • 这样就有下载的动画了
  • 进行完整步骤的流程,能够看到我们已经成功了
  • 同时,我们需要验证一下当目录下已经存在该文件名的时候,我们前面设置的验证路径边界判断是否生效
    • 通过下图,也是输出了文件夹已经存在,没有继续拉取代码进行覆盖本地文件

完整代码

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

index.js

#!/usr/bin/env node import{ program }from"commander" importinquirerfrom'inquirer' importfsfrom"node:fs" import{ checkPath , downloadTemp }from"./utils.js" letjson = 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文件

importfsfrom'node:fs' importdownloadfrom'download-git-repo' importorafrom'ora' constspinner = ora('下载中...') //验证路径 exportconstcheckPath =(path) =>{ returnfs.existsSync(path) ?true:false } //下载 exportconstdownloadTemp =(branch,project) =>{
spinner.start() returnnewPromise((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://www.iotsj.com//kuaixun/3551.html

联系我们

在线咨询:点击这里给我发消息

微信号:666666