搭建自定义脚手架【前端工程化】

在快速迭代、持续创新的前端开发领域,项目脚手架作为项目启动的基石,其重要性愈发凸显。然而传统的做法,如借助vue-clicreate-react-app(cra)等工具快速生成基础模板后,仍然需要根据公司的开发手册进行繁琐的配置调整、API封装以及依赖安装等等,这种流程不仅耗时耗力,而且容易在初始化项目时引入不必要的复杂性。

如果将这些操作步骤统一整合到自定义的脚手架中,我们便能大幅减少项目初始化的工作量,并在项目初始阶段就实现规范与统一,为公司项目管理带来显著优势。因此,一个高效、灵活且充分符合团队特色的自定义脚手架成为了我们提升开发效率、优化工作流程的不可或缺的工具。本文将带领大家探索如何搭建一个自定义的前端项目脚手架,从需求分析到技术选型,再到具体实现,我们将一步步搭建起属于自己的开发利器。

一,创建脚手架

首先创建脚手架工程文件夹,并初始化package.json文件:

sh 复制代码
makir my-cl && cd my-cli && npm init -y

默认使用ESM模块化开发,所以需要在package.json文件中设置:"type": "module"

安装搭建脚手架需要的插件列表:

  • chalk:可以给终端字体样式。
  • commander:解析命令行。
  • download-git-repo:下载并提取git仓库,用于下载模版。
  • figlet:创建脚手架Logo。
  • fs-extra:文件处理。
  • inquirer:命令行界面与用户交互的插件。
  • log-symbols:命令行界面状态图标。
  • ora:显示loading动画效果。
  • shelljs:基于nodejs的shell命令工具。
  • table:命令行界面表格内容显示。

然后创建入口执行文件index.js,此时工程目录结构为:

sh 复制代码
my-cli
├─ index.js
├─ package-lock.json
└─ package.json

编辑index.js文件:

js 复制代码
#!/usr/bin/env node

console.log('hello my-cli')

然后我们还需要在 package.json 中增加 bin 属性:

bin属性用来将可执行文件加载到全局环境中,指定了bin字段的npm包,一旦在全局安装,就会被加载到全局环境中,可以通过别名来执行该文件,如果非全局安装,那么会自动连接到项目的node_module/.bin目录中。

json 复制代码
"bin": {
	"my-cli": "./index.js"
},

在脚手架开发阶段: 我们可以使用npm link 命令把这个文件映射到全局后,就可以在任意目录下的命令行中输入 my-cli 执行我们的 index.js 脚本文件【开发调试】。

输入 npm list -g 可以查看已安装的全局模块。

npm link可以帮助我们模拟包安装后的状态,它会在系统中做一个快捷方式映射,让本地的包就好像全局安装过一样,可以直接使用。

下面我们开始设置脚手架具体的内容。

二,命令行交互

1,项目模板列表

关于项目模板:主要就是将《公司的开发手册规范内容》都编写到模板文件中,比如常见的Eslint/Prettier统一配置,API封装,axios请求封装,公共依赖添加等等,一般根据技术栈可以准备React/Vue两套模板。

在脚手架工程目录下建立一个scripts脚本目录,后面会将其它的脚本文件都放在这个目录下:

sh 复制代码
my-cli
├─ scripts
├─ index.js
└─ package.json

然后在该目录下创建一个constants.js文件:

js 复制代码
// scripts/constants.js

/**
 * 项目模板列表
 */
export const templates = [
  {
    name: "vue-template",
    value: "direct:https://gitee.com/账号/vue-template.git",
    desc: "基于vite的自定义vue项目模板"
  },
  {
    name: "react-template",
    value: "direct:https://gitee.com/账号/react-template.git",
    desc: "基于umi的自定义react项目模板"
  }
]

/**
 * 项目信息
 */
export const messages = [
  {
    name: "name",
    message: "请输入项目名称:",
  },
  {
    name: "description",
    message: "请输入项目描述:",
  }
]

constants脚本文件中,我们定义两个常量:

  • templates存储需要使用的模板列表,后续我们将会使用download-git-repo插件来克隆远程仓库,这里要注意的是download-git-repo默认的地址是github,要想使用gitee或者自定义的gitlab仓库需要书写完整的地址,并且要在url前面添加direct:标记。另外要注意的是默认克隆的是项目master分支,如果想要克隆其他分支可以在url尾部添加具体的分支名称#dev
  • messages存储两个常见的package信息,后面可以通过命令行交互让用户输入这两个字段信息,拿到输入结果后就可以重写到克隆的项目模板的package.json文件中,当然你也可以根据需求添加更多的交互信息,比如填写作者信息,关键词信息等等。

这里还有一点要说明的是:模板文件可以放在脚手架工程目录下,也可以独立存储到远程仓库中。不过这里我推荐把项目模板放到独立的仓库中。因为脚手架搭建完成之后,一般改动较少,但是项目模板会经常更新,这样可以保证即使没有更新脚手架也可以一直拉取到最新的模板文件。

2,克隆封装

scripts目录下创建一个clone.js文件:

js 复制代码
import download from "download-git-repo";
import ora from "ora";
import chalk from "chalk";
/**
 * 克隆模板方法
 * @param {*} repository 远程仓库地址
 * @param {*} appName 项目名称
 * @returns 
 */
export default function clone(repository, appName) {
  const spinner = ora("正在创建项目......").start();
  return new Promise((resolve, reject) => {
    // 第四个参数为一个callback,如果err存在则代表拉取项目失败
    download(repository, appName, {clone: true}, err => {
      if (err) {
        spinner.fail(chalk.red(err));
        reject(err);
        return;
      }
      spinner.succeed(chalk.greenBright("项目创建成功"));
      resolve();
    })
  })
}

基于download-git-repo插件封装一个clone方法,这里可以使用ora插件在命令行界面显示一个加载动画,并且在项目克隆完成之后显示一个成功的标记,而chalk插件的作用就是美化终端的文字显示。

3,文件操作

scripts目录下创建一个utils.js文件:

js 复制代码
import fs from "fs-extra";
import chalk from "chalk";
import path from "path";
import logSymbols from 'log-symbols'

const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);

// 删除文件夹
export async function removeDir(dir) {
  try {
    await fs.remove(resolveApp(dir));
    console.log(logSymbols.warning, `已覆盖同名文件夹${dir}`);
  } catch (err) {
    console.log(err);
    return;
  }
}

// 修改package.json配置
export async function changePackageJson(name, info) {
  try {
    const pkg = await fs.readJson(resolveApp(`${name}/package.json`));
    Object.keys(info).forEach((item) => {
      if (info[item] && info[item].trim()) {
        pkg[item] = info[item];
      }
    });
    await fs.writeJson(resolveApp(`${name}/package.json`), pkg, { spaces: 2 });
  } catch (err) {
    console.log(err);
    console.log(logSymbols.warning, chalk.yellow("更新项目信息失败,请手动修改package.json"));
  }
}

utils脚本文件中封装两个操作文件的方法:

  • removeDir主要是在项目创建的过程中,如果存在同名文件夹可以调用此方法删除目录,然后再创建项目。
  • changePackageJson就是将用户输入的交互信息(项目名称,描述)更新到项目的package.json文件中。

4,终端交互

scripts目录下创建一个interactive.js文件:

js 复制代码
import inquirer from 'inquirer';
// 封装命令行交互的相关方法

/**
 * @param {string} message 询问提示语句
 * @returns {Object} 根据name属性获取用户输入的值{confirm: y/n}
 */
export const inquirerConfirm = async (message) => {
  const answer = await inquirer.prompt({
    type: "confirm",
    name: "confirm",
    message,
  });
  return answer;
}

/**
 * 
 * @param {string} message 询问提示语句
 * @param {Array} choices 选择模板列表,默认读取对象的name属性
 * @returns {Object} 根据name属性获取用户输入的值{请选择项目模板: xxxxxx}
 */
export const inquirerChoose = async (message, choices) => {
  const answer = await inquirer.prompt({
    type: 'list',
    name: "choose",
    message,
    choices,
  });
  return answer;
}

/**
 * @param {Array} messages  询问提示语句数组
 * @returns {Object} 结果对象
 */
export const inquirerInputs = async (messages) => {
  const questions = messages.map(msg => {
    return {
      name: msg.name,
      type: "input",
      message: msg.message,
    }
  })
  const answers = await inquirer.prompt(questions);
  return answers
}

基于inquirer插件在interactive脚本文件中封装三个常用的命令行交互方法。

  • inquirerChoose用于显示模板列表,用户可以通过上下键选择克隆的项目模板。
  • inquirerConfirm用于询问用户,比如存在同名文件夹时是否覆盖(y/n),用户可以输入y/n返回一个布尔值结果。
  • inquirerInputs用于接收用户输入的一些项目信息,比如项目名称,项目描述等等。

5,创建项目

scripts目录下创建一个create.js文件:

js 复制代码
import fs from "fs-extra";
import shell from "shelljs"
import chalk from "chalk";
import logSymbols from 'log-symbols'
import clone from './clone.js'
import { removeDir, changePackageJson } from './utils.js'
import { templates, messages } from './constants.js'
import { inquirerConfirm, inquirerChoose, inquirerInputs } from './interactive.js'

/**
 * 创建项目方法
 * @param {*} appName 项目名称
 * @param {*} option 配置项
 */
export default async function create(appName, option) {
  if (!shell.which("git")) {
    console.log(logSymbols.error, chalk.redBright("Error:运行脚手架必须先安装git!"));
    shell.exit(1);
  }
  // 验证appName输入是否符合规范
  if (appName.match(/[\u4E00-\u9FFF`~!@#$%&^*[\]()\\;:<.>/?]/g)) {
    console.log(logSymbols.error, chalk.redBright("Error:<app-name>存在非法字符!"));
    return;
  }

  let repository = '';

  // 验证是否使用了--template配置项
  if (option.template) {
    // 从模板列表中找到目标templaet,如果不存在则抛出异常
    const template = templates.find(template => template.name === option.template);
    if (!template) {
      console.log(logSymbols.warning, `不存在模板${chalk.yellowBright(option.template)}`);
      console.log(`\r\n运行 ${chalk.cyanBright("my-cli ls")} 查看所有可用模板\r\n`);
      return;
    }
    repository = template.value;
  } else {
    // 从模板列表中选择
    const answer = await inquirerChoose("请选择项目模板:", templates);
    repository = answer.choose;
  }

  // 验证是否存在appName同名文件夹
  if (fs.existsSync(appName)) {
    if (option.force) {
      // 存在force配置项,直接覆盖
      await removeDir(appName);
    } else {
      // 不存在force配置项,询问是否覆盖
      const answer = await inquirerConfirm(`已存在同名文件夹${appName}, 是否覆盖:`);
      if (answer.confirm) {
        await removeDir(appName);
      } else {
        console.log(logSymbols.error, chalk.redBright(`Error:项目创建失败!存在同名文件夹${appName}`));
        return;
      }
    }
  }

  let answers = {};

  // 验证是否使用了--ignore配置项
  if (!option.ignore) {
    // 没有使用则需要输入项目信息
    answers = await inquirerInputs(messages);
  }

  // 拉取模板
  try {
    await clone(repository, appName);
  }
  catch (err) {
    console.log(logSymbols.error, chalk.redBright("项目创建失败"));
    console.log(err);
    shell.exit(1);
  }

  // 最后更新package.json
  if (answers.name || answers.description) {
    await changePackageJson(appName, answers);
  }
}

封装一个create方法,这就是我们执行my-cli create app命令执行的完整逻辑,之前封装的命令行交互,模板拉取,文件操作都会在这里按顺序调用,这就是一个创建项目的基本逻辑,当然如果你还有其他需求,可以在这里添加更多的处理逻辑。

6,命令封装

最后我们回到index.js入口文件:

js 复制代码
#! /usr/bin/env node
import fs from "fs-extra";
import figlet from "figlet";
import chalk from "chalk";
import { table } from 'table';
import { program } from "commander";
import create from './scripts/create.js'
import { templates } from './scripts/constants.js'

// 读取package.json配置信息
const pkg = fs.readJsonSync(new URL("./package.json", import.meta.url));

// 查看版本号
program.version(pkg.version, "-v, --version");

// 创建项目命令
program
  .command("create <app-name>")
  .description("创建一个新的项目")
  .option("-t --template [template]", "输入模板名称快速创建项目")
  .option("-f --force", "强制覆盖本地同名项目")
  .option("-i --ignore", "忽略项目相关描述,快速创建项目")
  .action(create);

// 查看模板列表
program
  .command("ls")
  .description("查看所有可用的模板")
  .action(() => {
    const data = templates.map(item => [chalk.greenBright(item.name), chalk.white(item.value), chalk.white(item.desc)]);
    data.unshift([chalk.white("模板名称"), chalk.white("模板地址"), chalk.white("模板描述")]);
    console.log(table(data));
  })

// 配置脚手架基本信息
program
  .name("my-cli")
  .description("一个简单的自定义脚手架")
  .usage("<command> [options]")
  // 用在内置的帮助信息之后输出自定义的额外信息
  .on("--help", () => {
    console.log("\r\n" + chalk.greenBright.bold(figlet.textSync("my-cli", {
      font: "Standard",
      horizontalLayout: "default",
      verticalLayout: "default",
      width: 100,
      whitespaceBreak: true,
    })))
    console.log(`\r\n Run ${chalk.cyanBright(`my-cli <command> --help`)} for detailed usage of given command.`)
  });

program.parse(process.argv);

我们在index入口文件中封装几个常用的脚手架命令:

  • -v:查看脚手架版本号。
  • ls:查看所有可用的模板。
  • create <app-name>:创建项目,脚手架最核心的命令【触发封装的create方法】,然后我们还添加了几个配置选项:
    • -t --template:输入模板名称快速创建项目。
    • -f --force:强制覆盖本地同名文件。
    • -i --ignore:忽略项目信息输入,快速创建项目。

最后在命令行输入my-cli即可查看脚手架的基本信息:

通过create <app-name>命令一步一步创建项目:

也可以通过添加配置项,快速创建项目:

到此为止,我们的脚手架基本搭建完成。

bash 复制代码
my-cli
├── scripts  
│   └── clone.js
│   └── constants.js
│   └── create.js
│   └── interactive.js
│   └── utils.js
├── index.js  
├── package-lock.json
├── package.json
├── readme.md

三,发布npm包

最后,将搭建完成的脚手架my-cli发布到公司的npm私服。

npm私服搭建可以参考《Linux系统使用Verdaccio搭建Npm私服》

其他成员就可以全局安装脚手架:

sh 复制代码
npm install my-cli -g

然后使用脚手架快速的创建项目后进行开发了。

相关推荐
金灰2 分钟前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
茶卡盐佑星_5 分钟前
说说你对es6中promise的理解?
前端·ecmascript·es6
Манго нектар33 分钟前
JavaScript for循环语句
开发语言·前端·javascript
蒲公英100140 分钟前
vue3学习:axios输入城市名称查询该城市天气
前端·vue.js·学习
天涯学馆1 小时前
Deno与Secure TypeScript:安全的后端开发
前端·typescript·deno
以对_1 小时前
uview表单校验不生效问题
前端·uni-app
程序猿小D2 小时前
第二百六十七节 JPA教程 - JPA查询AND条件示例
java·开发语言·前端·数据库·windows·python·jpa
奔跑吧邓邓子2 小时前
npm包管理深度探索:从基础到进阶全面教程!
前端·npm·node.js
前端李易安3 小时前
ajax的原理,使用场景以及如何实现
前端·ajax·okhttp
汪子熙3 小时前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js