通俗易懂地谈谈,前端工程化之自定义脚手架的理解,并附上一个实践案例发布到npm上

前言

  • 如果要开发一个新项目,传统方式要敲不少命令
  • 如下:使用最新版的vite,创建一个项目,选择对应的框架语言等

然后就是安装各种依赖,安装antd、安装路由、安装zustand等,如npm install axios react-router-dom antd ......

  • 若每次新开一个常规项目,都执行这样的搭建操作,整体来说,效率略低,不优雅------(毕竟是手动配置)

  • 于是,在此基础上,社区有作者提供了进一步的现成模板,比如针对于后台管理系统这类业务场景,有React-Admin、Ant-Design-Pro、或者ruoyi这样后台模板框架,开发者根据自己情况适当修改增删功能------(需根据自己公司业务适配修改)

如此这般,后续若再新开常规项目(假设需要新开发一个茶叶管理系统 ),直接复制一份先前已经沉淀好了的框架模板(假设原先沉淀好的就叫做基础管理系统)修修改改,在先前的基础上三次开发即可

自定义脚手架简述

新项目手动复制粘贴的痛点

但是,这里有一个麻烦的地方:

  • 首先,我们需要新建一个文件夹,然后把原本沉淀好的一套代码复制过来(假设叫做base-admin)
  • 然后,执行npm i安装依赖
  • 紧接着需要手动修改package.json里面的name的值为新项目名、也要修改index.html文件里面的title标签里面的名字等(当然还可能有其他要修改的),如下:
json 复制代码
{
  "name": "base-admin", // 修改成:"name": "tea-admin",
  "version": "1.1.1",
  "type": "module",
  "scripts": { ... },
  "dependencies": { ... },
  "devDependencies": { ... }
}

然后

html 复制代码
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>基础管理系统</title>
  <!-- 修改成:茶叶管理系统 -->
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>

</html>
  • 所以,我们思考,能不能写一个脚本,通过命令行交互的方式,交互执行一下

  • 就自动能够把原本沉淀好的那套base-admin代码拷贝过来

    • 并且也能自动够修改package.json里面的name的值为新项目名
    • 也能自动修改index.html文件里面的title标签里面的名字
    • 包括自动执行npm i下载依赖
    • 等其他个性化操作

简约来说,这件事,就是自定义脚手架,所做的事情

自定义脚手架------可定制内容

  • 实际上,自定义脚手架,不仅仅只是做 复制 base-admin 代码→改 name→改 title这样的 基础功能
  • 还可以进阶操作,比如base-admin有8个模块、但是tea-admin只需要3个模块,我们也可以通过命令行,使用自定义脚手架创建项目的时候,选择保留那些模块,或者丢弃那些模块
  • 甚至,自定义脚手架,还可以帮我执行git仓库初始化命令等

所以,自定义脚手架的收益就是:

减轻项目初始化的工作量、做到开发的规范和统一,当然也可以灵活的定制一些内容

自定义脚手架的大致步骤

  1. 把以往的沉淀好的base-admin发布到github/gitlab上(这是前提,要有基础项目框架模板代码,便于后续开发项目的复用)

  2. 编辑自己的自定义脚手架(就是一个npm项目,带有package.json和一堆js脚本文件)

  3. 把写好的自定义脚手架(假设叫做self-cli)发布到npm上(或者使用Verdaccio搭建自己的私服npm),然后所有同事可在自己电脑上全局安装npm i self-cli -g

    1. 使用Verdaccio搭建自己的私服npm,可以参考笔者的这篇文章:《20张图的保姆级教程,记录使用Verdaccio在Ubuntu服务器上搭建Npm私服
  4. 然后,就可以在命令行执行自定义脚手架提供的命令,比如self-cli -V(查看自定义版本号)、或self-cli create tea-admin(使用自定义脚手架self-cli创建新项目tea-admin)

  5. 这样的话,就会自动拉取git仓库上的base-admin代码

  6. 紧接着,命令行会提供一些问询交互,以便于创建新项目的时候,可以自定义一些东西(相当于执行npm create vite@latest xxx的效果)

  7. 最后命令行回车,自动帮我们执行修改base-admin的一些基础信息和其他自定义操作,最后自动执行npm i安装依赖,并跑起来项目

自定义脚手架常用的包

常用包

强大命令包shelljs可以便捷地运行命令:www.npmjs.com/package/she...

基本的包

  • 自定义脚手架要允许在命令行执行命令,可使用 commander
  • 执行完命令以后,要允许用户输入选择等交互操作,可使用 inquirer
  • 要能够拉取git仓库代码,可使用 download-git-repo
  • 在拉取代码的过程中,需要有加载loading效果,可使用 ora
  • 在拉取代码的过程中,需要有进度条百分比加载的效果,可使用progress

如果美化一下命令行,可使用如下包

  • 如果想让终端的输出文字五颜六色,可以使用 chalk
  • 如果想让终端输出的文字有字符画效果,可以使用 figlet
  • 如果想让终端输出的文字呈现表格形式,可以使用 table
  • 如果想让终端输出带有emoji,可以使用node-emoji

常用包做的有意思的效果

上述效果对应package.json包如下

json 复制代码
{
  "name": "some-npm-pkg",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "chalk": "^5.6.2",
    "commander": "^14.0.2",
    "figlet": "^1.9.4",
    "inquirer": "^12.11.1",
    "node-emoji": "^2.2.0",
    "ora": "^9.0.0",
    "progress": "^2.0.3",
    "table": "^6.9.0"
  }
}

其他的包

  • fs-extra,增强版的fs文件操作,更好用
  • ejs模板引擎,可用来替换模板中的变量
  • semver,语义化版本号解析 / 对比(判断版本高低、是否符合规则)
  • which,查找系统中可执行文件的路径(如找到 node/npm 等命令的安装位置)
  • chokidar,高性能文件监听(监控文件 / 目录变化,如文件修改后自动触发操作)------nodemon核心依赖
  • portfinder,自动查找可用端口(避免端口占用,如本地服务自动选端口)
  • opener,跨平台打开文件 / 浏览器(如自动打开本地服务页面)
  • mime,MIME 类型解析(判断文件 / 请求的内容类型,如 json、html、jpg 等)
  • giturl,解析 / 转换 Git 仓库 URL(如把 HTTPS 转 SSH,或提取仓库信息)
  • npm-request,简化 HTTP 请求的工具(聚焦 npm 相关接口请求,如查询包、下载包)
  • clipanion,Node.js 命令行参数解析(更优雅的 CLI 构建)------对标Conmand
  • diff,文本差异对比(测试时验证输出 / 文件变化)
  • is-windows,判断是否 Windows 系统

自己写一个简单的self-cli

首先要有一个基础模板项目 base-admin

  • 笔者已经上传到github上了,地址: github.com/shuirongshu...
  • base-admin是一个演示的项目,没有太多东西,实际开发中,这里基础模板会有很多东西,比如eslint、prettier等
  • 同时,也可能会有多个模板,比如react技术栈基础模板、vue技术栈基础模板、后台基础模板、前台基础模板等

如下图:

需求:

  • 当新项目开启的时候,我们使用自定义脚手架在命令行执行self-ci create,
  • 会从git仓库拉取这个base-admin
  • 然后,在命令行中我们可以输入新项目名
  • 输入的新项目名,会自动替换模板引擎和修改package.json文件里面的name
  • 同时,也会自动帮我们执行npm install命令
  • 最后,可以让我们选择,是否启动这个项目(是否npm run dev)

就是把原先需要手动复制粘贴项目,修改项目里面的内容的步骤,换成了命令行脚本自动化执行了...

自定义脚手架完成效果图

我们先看一下,完成后的效果图

对应拉取的创建并修改的新项目

  • 在动态图中,我们可以看到,左上角拉取了项目,名字叫做 pro-new ,这个明显也就是我们输入的新项目的名字
  • 这个是简单案例,实际项目中,控制台的交互会多一些

接下来,我们来快速过一下这个脚手架做了那些事情

commander包定制命令行输入命令

index.js

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

import { program } from 'commander';
import fs from 'fs-extra';
import app from './app.js';

const pkg = fs.readJsonSync(new URL('./package.json', import.meta.url));

program
  .version(pkg.version, '-v, --version')
  .name('self-cli')
  .description('自定义脚手架工具');

program
  .command('create [app-name]')
  .description('创建一个新的项目')
  .action(app);

program.parse(process.argv);

shelljs包去判断是否安装了git、执行git clone命令等

**shelljs 很强大,强的可怕 **

js 复制代码
import shell from 'shelljs';

// 检查 git
if (!shell.which('git')) {
    console.log(chalk.red('❌ 请先安装 git'));
    shell.exit(1);
}

// 拉取 Git 仓库 - 使用 git clone
const TEMPLATE_REPO = 'shuirongshuifu/base-admin';
const spinner = ora('正在拉取项目...').start();

// 使用 SSH URL
const repoUrl = `git@github.com:${TEMPLATE_REPO}.git`;
const cloneResult = shell.exec(`git clone ${repoUrl} ${projectName}`, { silent: false });

if (cloneResult.code !== 0) {
    spinner.fail(chalk.red('拉取失败'));
    console.log(chalk.red('错误信息:' + cloneResult.stderr));
    console.log(chalk.yellow('\n提示:请确保已配置 SSH key,或检查网络连接'));
    shell.exit(1);
}

进入对应目录,并安装项目依赖
// 安装依赖
console.log(chalk.cyan('正在安装依赖...'));
const installResult = shell.exec(`cd ${projectName} && npm install`);
if (installResult.code !== 0) {
    console.log(chalk.red('❌ 依赖安装失败'));
    console.log(chalk.red('错误信息:' + installResult.stderr));
    console.log(chalk.yellow('请手动进入项目目录执行:npm install'));
    shell.exit(1);
}

console.log(chalk.green('✅ 项目创建完成!'));

ejs包处理模板文件

js 复制代码
  // 处理 ejs 模板文件
  console.log(chalk.cyan('正在处理模板文件...'));
  const processEjsFiles = (dir) => {
    const files = fs.readdirSync(dir);
    files.forEach(file => {
      const filePath = path.join(dir, file);
      const stat = fs.statSync(filePath);

      if (stat.isDirectory() && file !== 'node_modules' && file !== '.git') {
        processEjsFiles(filePath);
      } else if (file.endsWith('.ejs')) {
        const template = fs.readFileSync(filePath, 'utf-8');
        const rendered = ejs.render(template, { projectName });
        const destPath = filePath.replace(/\.ejs$/, '');
        fs.writeFileSync(destPath, rendered, 'utf-8');
        fs.unlinkSync(filePath);
      }
    });
  };
  processEjsFiles(projectPath);
  console.log(chalk.green('✅ 模板文件处理完成'));

fs模块直接修改package.json文件

js 复制代码
// 修改 package.json
console.log(chalk.cyan('正在修改 package.json...'));
const pkgPath = path.join(projectPath, 'package.json');
const pkg = await fs.readJson(pkgPath);
pkg.name = projectName;
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
console.log(chalk.green('✅ package.json 修改完成'));

等,不赘述...

重点:self-cli为何能够被命令行识别?

  • 上述案例的自定义脚手架,代码并不难,我们思考,为何在命令行执行self-cli命令能够被识别呢
  • 毕竟self-cli并不是操作系统自带的命令

命令行识别逻辑顺序

  • 当我们在命令行中,输入xxx的时候,操作系统会进行如下的查询执行逻辑
  • 比如,先看看这个xxx是不是自带的内部命令,如 ls dir cd 等(是自带的就按照自带的逻辑执行)
  • 不是自带的,就会去环境变量Path里面遍历查找,比如执行了git -v
  • 那么,发现,环境变量真有,找到对应的Path里面对应的路径的值对应的文件夹,再看看文件夹里面是否有对应的exe或cmd或bat,再交给其执行
  • 先遍历环境变量里面有那些文件夹,再到对应文件夹里面,再次遍历找对应可执行文件批处理命令(系统先按Path的目录顺序 "逛文件夹",在每个文件夹里只看直接文件,找 "命令名 + 可执行后缀" 的文件,找到就用,找不到就换下一个文件夹,全逛完都没有就报错)

报错命令:

bash 复制代码
C:\Users\lss13>hello
'hello' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

C:\Users\lss13>

对应路径的确有exe可执行文件

比如,查看java、python、git、node版本也是上述同样类似的道理

bash 复制代码
C:\Users\lss13>java -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

C:\Users\lss13>python --version
Python 3.12.8

C:\Users\lss13>git -v
git version 2.45.2.windows.1

C:\Users\lss13>node -v
v22.12.0

self-cli识别命令,则是当找到node的环境变量path后,在对应文件夹找到了self-cli,如下图

注意,这里有三个,分别是

bash 复制代码
self-cli       # Linux/Mac风格脚本
self-cli.cmd   # Windows CMD批处理脚本(核心)
self-cli.ps1   # PowerShell脚本

所以,就是如下图的箭头所示

  • 所以,这里的本质就是通过 npm link给某个包

npm link介绍

npm link是 npm 专为本地开发 npm 包(比如笔者的self-cli 脚手架)设计的调试工具,核心是通过软链接(类似 Windows 快捷方式) 关联本地代码和全局 npm 环境,避免反复安装的麻烦

  • 比如这个self-cli 这类自定义 CLI 工具,开发时需要频繁修改代码并测试命令效果,npm link 能让全局执行的 self-cli 命令直接指向本地开发目录的代码
  • 改完代码无需重新全局安装,直接执行命令就能看到最新效果,大幅提升调试效率

也就是说,npm link在node的环境变量文件夹里面创建了一个链接,让我们在命令行执行对应的命令的时候,系统能够识别,能够找到对应的脚本js文件,做对应的执行处理

在对应的自定义脚手架里面执行npm link会自动生成可执行文件和软连接,这样就能达到全局挂载可使用的效果了

不过,我们还需要在package.json文件里面,加上bin规则,告知self-cli要执行那个js文件,同时,对应js文件,要加格式固定:#!/usr/bin/env node

即:

package.json

json 复制代码
{
  "name": "self-cli",
  "version": "1.2.3",
  "description": "自定义脚手架工具",
  "main": "index.js",
  "type": "module",
  "bin": {
    "self-cli": "./index.js"  // 这个
  },
  "scripts": { ... },
  "license": "ISC",
  "dependencies": { ... }
}

index.js

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

import { program } from 'commander';
import fs from 'fs-extra';
import app from './app.js';

......
方式 本质 代码修改后效果 适用场景
npm link 创建软链接(快捷方式) 实时生效,无需额外操作 本地开发、频繁改代码
npm install xyz -g 复制文件到全局目录 需重新安装才生效 开发完成后安装最终版本
  • 当然,我们本地开发脚手架,使用npm link去全局链接上,方便调试
  • 当这个脚手架self-cli开发完毕后,就可以发到npm上
  • 或者发到公司里面自己搭建的私服npm
  • 搭建私服npm可以参见笔者的这篇文章:20张图的保姆级教程,记录使用Verdaccio在Ubuntu服务器上搭建Npm私服
  • 这样的话,团队成员就可以全局下载self-cli
  • 就可以在命令行使用对应命令,拉取gitlab仓库代码,自定义创建新项目了

一句话总结前端自定义脚手架

前端自定义脚手架(比如self-cli)是基于nodejs语法的全局CLI工具,核心价值是基于模板一键生成标准化且可自定义配置的前端项目,从而达到提效的目的

CLI = 命令行(Command Line)+ 界面(Interface),核心指的是:基于命令行操作的工具或交互方式

笔者的这个演示脚手架,也发布到npm上面了,不过因为包名不能类似原因,笔者做了修改,现在叫做:s-cli-srsf

地址:www.npmjs.com/package/s-c...

大家可以尝试着

js 复制代码
npm install -g s-cli-srsf

然后,就会发现,这次生成的就不是npm link那种链接了,是直接安装的文件夹内容

全局安装以后,就可以愉快地创建项目了

回顾package.json常用的配置键值对

回顾一下知识点

键名 类型 描述 示例
name String 包的名称,必须唯一 "my-package"
version String 版本号,遵循语义化版本控制 "1.0.0"
description String 包的简短描述 "A sample package"
keywords Array 关键词数组,用于搜索 ["web", "framework"]
homepage String 项目主页URL "example.com"
bugs Object Bug报告地址 {"url": "github.com/.../issues"}
license String 许可证类型 "MIT"
author String/Object 作者信息 "Name email@domain.com"
contributors Array 贡献者列表 [{"name": "John", "email": "..."}]
files Array 发布时包含的文件 ["dist/", "lib/"]
main String 主入口文件 "index.js"
browser String 浏览器端入口文件 "./browser.js"
bin Object 命令行工具配置 {"cli": "./bin/cli.js"}
repository Object 仓库信息 {"type": "git", "url": "..."}
scripts Object 脚本命令集合 {"start": "node index.js"}
dependencies Object 生产环境依赖 {"express": "^4.17.1"}
devDependencies Object 开发环境依赖 {"jest": "^27.0.0"}
peerDependencies Object 同级依赖 {"react": "^17.0.0"}
optionalDependencies Object 可选依赖 {"fsevents": "^2.3.2"}
engines Object 支持的引擎版本 {"node": ">=14.0.0"}
os Array 支持的操作系统 ["darwin", "linux"]
cpu Array 支持的CPU架构 ["x64", "arm64"]
private Boolean 是否私有包 true
workspaces Array 工作区配置 ["packages/*"]
相关推荐
页面魔术3 小时前
⭐看完vite纪录片才知道尤大有多屌(上)
前端·javascript·vue.js
UpgradeLink3 小时前
Electron 项目使用官方组件 electron-builder 进行跨架构打包
前端·javascript·electron
蚂蚁不吃土&3 小时前
cmd powershell svm nodejs npm
前端·npm·node.js
Moment3 小时前
别再让 JavaScript 抢 CSS 的活儿了,css原生虚拟化来了
前端·javascript·css
晓得迷路了3 小时前
栗子前端技术周刊第 110 期 - shadcn/create、Github 更新 npm 令牌政策、Deno 2.6...
前端·javascript·css
前端小端长3 小时前
项目里满是if-else?用这5招优化if-else让你的代码清爽到飞起
开发语言·前端·javascript
笨小孩7873 小时前
Flutter跨平台开发全解析:从原理到实战的深度指南
javascript·react native·react.js
毕设源码-郭学长3 小时前
【开题答辩全过程】以 基于Node.js的医院预约挂号系统为例,包含答辩的问题和答案
node.js
AI_56783 小时前
Vue3组件通信的实战指南
前端·javascript·vue.js