前端脚手架(简易版)搭建攻略

KongZhiZhen
• 阅读 686

简介

脚手架CLI(command-line-interface)是一类快速形成工程化目录的工具。

开发过程中,如果需要新建前端项目,我们经常都会用到脚手架来创建工程,通过命令行式的交互,可快速选择选项并完成初始项目的搭建。而CV大法往往会带来很多重复的删减工作,且会导致项目分散、架构不统一等等弊端。

常见的主流框架都有自己的脚手架:

通过本文内容实践,可以开发出一套较基础的脚手架工具,满足日常工作中搭建项目的同时,持续对脚手架和模版内容做优化。

搭建

按照惯例,先附上代码 thjjames/th-cli 并整理下知识点:

  • 如何声明全局命令
  • 如何实现命令行交互
  • 如何实现用户选项交互
  • 如何创建项目(预设和远程)

依赖包

在此之前,还需要先访问一下 package.json 文件,看下需要安装的依赖并简单了解这些依赖对应的功能:

  • 核心工具库

    • commander TJ大神写的 Nodejs 命令行交互工具
    • inquirer 选项命令行交互工具,也可以用 prompts
    • fs-extra fs 的扩展包,添加了部分方法和 promise 支持
    • mem-fs / mem-fs-editor 基于 ejs 的文件编辑助手
    • download-git-repo 远程模板下载工具
  • 辅助工具库

    • minimist 轻量级的命令行参数解析引擎
    • semver 语义化版本号规则
    • validate-npm-package-name 验证项目名称
    • chalk 美化终端输出
    • ora 终端加载动画
    • figlet 输出终端艺术字

声明全局命令

为何安装完脚手架包后,就能在任意地方执行脚手架命令呢?

// 安装
npm i -g thjjames/th-cli
// 使用
th-cli create <project-name>

先看 package.json 文件:

// 当只有一个可执行文件且命令名就是包名称,例如这里可以简写为
// "bin": "bin/th-cli.js"
{
  "name": "th-cli",
  "version": "1.0.0",
  "description": "Scaffold for Creating Projects.",
  "author": "田豪峻 James<thjjames@163.com>",
  "bin": {
    "th-cli": "bin/th-cli.js"
  },
  ...
}

bin 字段指定了命令名到本地文件名的映射,安装包时会在 node_modules 文件夹下面的 .bin 目录中复制可执行文件,这样就可以在安装的目录下执行自定义脚手架命令。当全局安装时,会映射到全局存储的位置(LinuxmacOS 系统默认目录 /usr/local/lib/)。

前端脚手架(简易版)搭建攻略

如果想直接执行 th-cli 命令,会直接从全局目录查找,必须全局安装。

npm run[-script] 执行命令时的查找顺序为: 先从当前项目目录的 node_modules 文件夹下查找,然后是全局安装目录,最后再是 Node 根目录。

如何实现命令行交互

理解完上述的命令查找关系链,我们看下命令名 th-cli 对应的 bin/th-cli.js 文件,是开始执行脚手架内容的关键,先看第一行:

#! /usr/bin/env node

#! 是一种在 Linux 系统中使用的特殊注释,通常用于指定脚本文件的解释器。/usr/bin/env 代表解释器目录, 用 node 执行。更多相关内容可以通过 Shebang 了解。

执行前可以做些校验内容,用 semver 判断当前环境的 node 版本是否满足脚手架需要的最低版本、validate-npm-package-name 判断新建的包名是否符合命名规范,这些都是锦上添花的功能,不花篇幅去讲了,请自行撸代码。

接下去就是核心内容 commander 部分,这里看下我们使用到的一些功能:

  • option 定义选项
  • command 创建 create 和 list 等自定义命令

    • alias 给命令添加别名
    • description 给命令添加描述
    • argument 给命令声明参数
    • option 给命令定义选项
    • action 给命令添加执行函数
  • version 提供版本号

具体用法文档上已经写的很详细了,照着文档创建一个自定义的create命令:

program
  .command('create <project-name>')
  .description('create a new project by th-cli')
  .option('-d, --default', 'skip prompts and use default preset')
  .option('-f, --force', 'overwrite target directory if it exists')
  .option('-c, --clone', 'use git clone when fetching remote preset')
  .action((name, options) => {
    if (minimist(process.argv.slice(3))._.length > 1) {
      console.warn(chalk.yellow('You provided more than one argument.'))
      console.warn(chalk.yellow('The first one will be used as the project name, the rest are ignored.'))
    }
    require('../lib/create')(name, options);
  });

前端脚手架(简易版)搭建攻略

通过上述代码完成了需要通过命令行交互来搭建项目的主体,里面的参数例如 -f 作用是在我们创建的项目目录有冲突时提供的强制覆盖选项:

if (fse.existsSync(targetDir)) {
  if (options.force) {
    fse.removeSync(targetDir);
  } else {
    console.error(chalk.red(`Folder ${targetDir} is already in use, please rename or overwrite it by using option -f.`));
    process.exit(1);
  }
}

前端脚手架(简易版)搭建攻略

执行自定义命令并通过验证后可以看到直接进入用户选项交互:

th-cli create <project-name>

前端脚手架(简易版)搭建攻略

如何实现用户选项交互

前端脚手架(简易版)搭建攻略

inquirer 是交互式命令行美化工具,提供了一系列常用组件(如input、list、checkbox等),解析输入并收集、验证答案。经过一系列的交互操作后,在命令的执行目录生成了新的项目。

const inquirer = require('inquirer');
const prompts = [
  {
    name: 'type',
    type: 'list',
    message: '请选择获取模板方式',
    default: 'preset',
    choices: [
      { value: 'preset', name: '预设模板' },
      { value: 'remote', name: '远程模板' }
    ]
  },
  ...
];
const answers = await inquirer.prompt(prompts);

前端脚手架(简易版)搭建攻略

这里交互是帮助我们可以通过一系列选项来确认最终想要的项目,是本地预设还是远程模板、是vue还是react项目、是babel还是esbuild打包、是否需要ts等等,这些都可以通过交互得到答案并体现在最终的项目上。

如何创建项目

如果选择远程模板,需要借助 download-git-repo 从远程 git 仓库上下载,上述的 -c 参数对应此包的clone参数用来区分 git clonehttp下载,下载后脚手架需要帮助执行 git init 以初始化现有仓库;而选择预设模板的话,将会从脚手架代码中的template文件夹复制到本地,当然了,此处并不是简单的 fs.copy 复制,会将模板中的文件基于 ejs 模板引擎和用户交互选项的答案anwsers生成新的模板文件。

const templateDir = answers.type === 'preset' ? path.resolve(TEMPLATE_DIR, answers.preset) : path.resolve(os.tmpdir(), `cli-tmp-${name}`);
const spinner = ora(`Creating a new project in ${targetDir}, please wait...\n`).start();

if (answers.type === 'preset') {
  await copy(name, answers, targetDir, templateDir);
} else {
  await downloadGitRepoAsync(answers.repository, templateDir, {
    clone: options.clone
  });
  await copy(name, answers, targetDir, templateDir);
  fse.removeSync(templateDir);
}
execSync('git init', {
  cwd: targetDir
});
spinner.succeed('Create successfully');

由于上述过程需要消耗点时间,在过程中我们通过 ora 来展现加载效果,完善整个交互过程。

看到这里思考下一个问题,远程模板的下载速度要比本地预设慢很多,那为何还需要这个选项呢?原因有以下,一是当自己开发的脚手架需要给其他部门使用时,不太适合将别人的定制模板放到本地预设中,这种情况下通过 git clone 的方式是最合适的,二是当本地预设模板还不够完善需要频繁改动时,会频繁的更新版本号导致开发者频繁的更新版本,这种情况下也可以考虑使用远程模板来规避。

最后想下,整个用户交互的过程还是略微有一些些成本的,如果我们团队的项目风格比较固定即大部分情况下的选项答案都是相同的,有没有更快速便捷的创建方式呢?往上看选项 -d 就是起这个作用,带上这个参数时,所有的交互选项都会选择默认答案并跳过用户交互环节,直接生成默认项目模板!

结语

到这里,我们实现了一个简易的脚手架,可以选择模板,也有自定义的快捷命令,但回看简介里主流的脚手架代码,可以看出还有很多功能点可以完善,例如:

  • 模板中如何根据选项(例如是否移动端、是否需要pinia、是否需要ts)动态定制成多套?
  • 脚手架中的模板更新后如何同步更新到以前已经生成的项目中?

就卖个关子🤪留给大家思考吧,前端脚手架并没有一成不变的最佳实践,一切还得根据团队实际情况定制。但不管怎么说,完成了从0到1的MVP版本后,再到1.1版本就简单多了。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
4年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
4年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
KongZhiZhen
KongZhiZhen
Lv1
谁没喝过酒尝过孤独,但却忘不了你
文章
3
粉丝
0
获赞
0