Nodejs 第二十二章 脚手架
Nodejs 第二十二章 脚手架
随着现代网页应用程序的复杂性增加,前端开发的复杂度也相应提升。在这样的背景下,前端脚手架(Scaffolding)工具的出现就成为了开发流程中的一个必要工具
脚手架出现的原因
- 项目结构标准化:在没有脚手架的时候,每个开发者或团队可能会有自己的方式去组织项目结构,这导致了协作和维护的困难。脚手架帮助统一项目结构,使得新成员加入项目和理解代码变得更容易。
- 自动化流程:前端项目通常需要一系列重复性的设置步骤,包括配置文件的创建、目录结构的设置等。脚手架可以自动化这些流程,提升效率。
- 快速启动新项目:脚手架可以快速生成项目基础架构和文件,使开发者可以直接进入开发阶段,减少重复性工作。
- 技术栈整合:现代前端开发往往涉及多个技术栈的整合,如React、Vue、Angular、Babel、Webpack等,脚手架可以预先配置好这些工具的整合,减少开发者的配置负担。
- 规范开发流程:脚手架可以强制执行代码风格、提交信息格式等规范,保持团队开发风格的一致性。
掌握脚手架的必要性
- 提升开发效率:通过减少重复和繁琐的初始化工作,脚手架让开发者能够更快地开始实际的开发工作。
- 减少错误:手动创建项目结构和配置可能会引入错误。脚手架通过自动化过程,降低了犯错的机会。
- 教育和学习:对于新手开发者,脚手架提供了学习项目结构和配置的机会,也帮助他们了解行业最佳实践。
- 适应新技术:随着新技术和工具的不断出现,脚手架帮助开发者适应新的技术生态,维护项目的现代化。
- 保持竞争力:在激烈的市场竞争中,能够快速启动和交付项目是至关重要的。脚手架工具提供了这一能力,确保团队能够快速响应市场需求。
而理解脚手架最快的方法就是自己实现一遍,那流程不就通透了,有哪个前端不想拥有自己的一套脚手架,在这一章节你会用到和学到非常多的第三方库
- 让我们来看一个通过
npm init vue
初始化后的vue脚手架吧!
- 完成一个详细完整的脚手架是很困难的,但通过一个小突破,实现以点破面却是可以降低难度,那就让我们正式开始吧!
工具介绍
-
让我们来看下在接下来搭建脚手架中,会用到的第三方库
-
在使用之前,一定要记得先下载,毕竟这些都是第三方库
- 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库
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);
}
});
使用步骤
-
引入库:
- 使用
require
方法引入download-git-repo
模块。
- 使用
-
设置仓库来源:
repository
变量设置为从GitHub下载Node.js项目的master分支。- 格式遵循
download-git-repo
的规定,通常是platform:user/repo#branch
。
-
设置目标目录:
destination
变量定义了下载文件的存放路径,这里是当前目录下的node-repo
文件夹。
-
下载仓库:
- 调用
download
函数,传入上面定义的repository
和destination
。 - 第三个参数
{ clone: true }
指定使用git clone
进行下载,这通常更快,并能保留.git
目录。 - 该函数异步执行,提供了一个回调函数来处理完成后的成功或失败情况。
- 调用
编写脚手架
脚手架需求
-
编写的脚手架需要满足以下几点要求:
- 不使用node开头的命令,而是自定义的命令去执行我们的脚本
- 在命令后面能附带参数例如-V --help 等功能去和命令行交互
- 根据搭配好的配置,能做到去下载模板到本地
- 根据前面第三方库的介绍,我想我们是可以实现的了
自定义脚本
- 想要实现第一点需求,我们需要在文件最上面加上一行代码
#!/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进行转化一下
- 由于我们在package.json中进行配置了
#!/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)
- 通过这步骤进行操作,很显然是成功了
- 那我们接下来就按部就班的,当用户进行创建项目的时候,我们让他起一个项目名称和选择是否选用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)
-
通过这样,我们简单的自定义了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)
- 而第二个prompt的type类型是一个可选项confirm,Y为Yes,n为No
- 当我们输入结束之后,结果会以Promise的方式,以then的方式进行返还结果,我们接收到的内容则是一个对象形式的数据
验证路径
-
当然,我们已经实现了一大半的功能了,此时我们需要进行一点边界条件的判断
- 我们创建的这个模板,是否会与当前文件夹内的文件名发送冲突?
- 对于这个验证路径的函数方法,我们就写在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模板,选择不同的模板分支进行下载到本地
为什么需要direct:
前缀
-
绕过预设的解析器:
download-git-repo
默认支持GitHub
,GitLab
, 和Bitbucket
的简写形式,例如github:user/repo
或gitlab:user/repo
。- 使用
direct:
前缀允许我们绕过这种自动解析,直接指向一个具体的Git仓库URL。
-
明确指定仓库位置:
- 当从一个不属于上述三大托管平台的Git仓库下载时,例如自托管的Git仓库或小众平台,需要明确指出完整的Git URL。
- 这样
download-git-repo
就不会尝试去解析URL,而是直接使用我们提供的链接。
-
使用完整的Git URL:
- 通过使用完整的Git URL,可以更精确地控制下载的仓库和分支。
- 这种方式同样支持访问私有仓库,只要URL中包含了足够的权限信息(如在URL中嵌入访问令牌)。
ora的使用
-
为了在下载模板内容到本地的时候有一个加载动画,以免等待太过枯燥.我们可以使用ora进行配置
- 在utils工具文件中,导入ora:
const spinner = ora('下载中...')
,然后放在下载函数中
- 在utils工具文件中,导入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)
})
- 这样就有下载的动画了
- 进行完整步骤的流程,能够看到我们已经成功了
-
同时,我们需要验证一下当目录下已经存在该文件名的时候,我们前面设置的验证路径边界判断是否生效
- 通过下图,也是输出了文件夹已经存在,没有继续拉取代码进行覆盖本地文件
完整代码
- 那通过上面的步骤流程,我们也是完整的实现了一次如何自己配置脚手架,以下就是我配置脚手架用到的完整代码
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