前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从0开始搭建优雅的前端脚手架工具

从0开始搭建优雅的前端脚手架工具

原创
作者头像
用户8738532
发布2022-12-05 15:42:15
5310
发布2022-12-05 15:42:15
举报
文章被收录于专栏:Derry Blog

前言

在日常开发中,我们经常会使用到各种脚手架工具(cli): vue-create-app,ng 包括 npm。它们极大简化了开发人员对于项目结构和文件创建的工作,让我们可以把精力专心在业务实现上。 对于某些项目而言 cli还可以封装一些脚本,用来处理项目中的一些特殊场景。

开发cli的好处:

  • 简化项目初始化流程,对于部门内新立项的项目能快速的生成项目骨架。
  • 集成项目通用指令,例如:本地化发布上传,subtree 拉取配置等,部门内所有项目统一升级等。
  • 减少重复工作,跟据用户输入还可以进行差异化配置

原理

cli 不论有多少功能基本原理就是利用 nodejs 来进行脚本和文件的各种操作,比如 init 等初始化的指令就是在指定的模板代码仓库中拉取相应的代码,再跟据用户输入进行模板替换。 再比如 npm run serve这类的指令就是利用node 的环境运行一些 shell脚本达到相应的功能。

准备工作

编写cli能使用到的工具库有很多,并且各有各的优点,最为常见的配套按 使用顺序 如下:

commander.js,解析用户命令(init, create, -v, help 等)

inquirer,执行命令后的用户交互集合有多种可供支持的交互类型(input, checkbox, list 等)

download-git-repo,下载指定仓库指定分支下的代码,作为项目模板

handlebars,模板引擎,用户根据用户输入替换项目模板中的内容实现自定义与差异化

除开以上功能性的模块,还有许多可以用来提高用户体验的包:

ora, 执行命令时的动画效果,用来标识cli的运行进度。

chalk,给通过终端console命令输出的文本添加字体颜色。

log-symbols,给通过终端console命令输出的文本添加符号标识(info, success,warning,error四种)

步骤

想直接查看源码的请拖到页面底部。

初始化项目

创建一个空目录,根据你的喜好命名(示例项目为d-cli),然后进入项目执行 npm init 生成 package.json文件。 接着安装上面提到的依赖包

npm install commander inquirer download-git-repo handlebars ora chalk log-symbols

接着在package.json 中加上 bin的内容:

代码语言:txt
复制
{
   "name": "d-cli",
   "version": "1.0.0",
   "bin": {
      "d-cli": "index.js"
   },
   ...
}

然后在根目录创建 index.js 文件,加载所有需要用到的依赖

代码语言:JavaScript
复制
const fs = require("fs");
const program = require("commander");
const download = require("download-git-repo");
const handlebars = require("handlebars");
const inquirer = require("inquirer");
const ora = require("ora");
const chalk = require("chalk");
const symbols = require("log-symbols");
const exec = require("child_process").exec;
const path = require("path");

开始编写我们的第一个方法

代码语言:JavaScript
复制
function startCommand() {
  return new Promise((resolve) => {
    program
      .version(
        require(path.resolve(__dirname, "./package")).version,
        "-v --version"
      )
      .command("init <name>")
      .action((name) => {
        if (!fs.existsSync(name)) {
          inquirer
            .prompt([
              {
                type: 'input',
                name: 'name',
                message: 'Please input project name'
              },
              {
                type: 'input',
                name: 'author',
                message: 'Please input project author'
              },
              {
                type: 'list',
                name: 'description',
                choices: ['This project is for learn cli',
                          'How about do a amazing job?'],
                default: 0,
                message: 'Please input project description'
              }
            ])
            .then((answers) => {
              resolve({
                name,
                answers,
              });
            });
      } else {
        console.log(symbols.error, chalk.red("Project already exists!"));
      }
    });
    program.parse(process.argv);
    if (!program.args.length) {
      program.help();
    }
  });
}

startCommand 为开启cli 命令的函数,在内部我们通过commander 包定义cli 的 version 信息(版本号与package.json 保持同步)之后就可以通过 command方法 定义我们需要的功能,<name> 字段为用户在命令关键字后输入的内容,可以在action回调中获取

代码语言:JavaScript
复制
program
      .version(
        require(path.resolve(__dirname, "./package")).version,
        "-v --version"
      )
      .command("init <name>")
      .action((name) => {})

接下来我们使用 inquirer 包的 prompt 方法来与用户进行交互,数组中的内容会按顺序来提示用户输入,对象中的 name 字段会汇总起来构成回调函数中 answers 的 key。 最后通过 Promise 的 resolve 返回我们收集到的信息。

代码语言:JavaScript
复制
resolve({
  name,
  answers,
});

此外我们还通过 fs.existsSync 判断了用户输入的 name 是否在当前目录下存在重名。 最后通过 program.parse(process.argv) 启动命令配置。

代码语言:JavaScript
复制
if (!program.args.length) {
  program.help();
}

最后判断,当用户未输入参数时给予 help 提示帮助

下载模板

代码语言:JavaScript
复制
function downloadProject(name) {
  return new Promise((resolve) => {
    const spinner = ora("Downloading template...");
    spinner.start();
    download(
      "https://github.com:Derrys/testing-case#cli-template",
      name,
      { clone: true },
      (err) => {
        if (err) {
          spinner.fail();
          console.log(symbols.error, chalk.red(err));
        } else {
          spinner.succeed();
          resolve();
        }
      }
    );
  });
}

downloadProject 方法接收一个 name 参数(上一步返回的 name)指定下载到的文件夹名,通过 download-git-repo库的 download 方法下载对应仓库地址的代码。需要注意的是 download 的第一个参数 https://github.com:Derrys/testing-case#cli-template 是原本正常仓库地址 (https://github.com/Derrys/testing-case.git) 的变形写法在域名后的 url 开始位置用 : 替代 /; 地址最后的 .git 后缀名需要删除; #cli-template 后面为仓库对应的 branch 名称。

Error: 'git clone' failed with status 128 这个报错,大多是地址配置有误

渲染模板

在被clone 的仓库中我们可以使用 handlebars 的语法定义需要被替换的值

代码语言:JSON
复制
// https://github.com/Derrys/testing-case.git/package.json
{
  "name": "{{ name }}",
  "version": "0.1.0",
  "description": "{{ description }}",
  "author": "{{ author }}",
  "private": true,
}

然后定义所有需要被替换的文件路径集合

代码语言:Javascript
复制
const TEMPLATEFILES = [
  // Here you can list all the file directories that need to be replaced
  'package.json'
]

接着定义模板替换方法

代码语言:Javascript
复制
function setTemplate(fileName, meta) {
  return new Promise((resolve, reject) => {
    if (fs.existsSync(fileName)) {
      const content = fs.readFileSync(fileName).toString();
      const result = handlebars.compile(content)(meta);
      fs.writeFileSync(fileName, result);
      resolve();
    } else {
      reject();
    }
  });
}

最后我们就可以使用 Promise.all 等方法进行批量替换了

代码语言:javascript
复制
const spinner = ora("Set template files...");
spinner.start();
await Promise.all(TEMPLATEFILES.map((i) => setTemplate(path.join(__dirname, name, i), answers))).then(() => {
  spinner.succeed();
}).catch(e => {
  console.log(symbols.error, chalk.red(e));
  spinner.fail();
})

执行Shell

在模板替换完毕之后,我们还可以进行很多 shell 脚本操作来简化项目的运行步骤。

代码语言:javascript
复制
function doShellJob(shell, tips) {
  return new Promise((resolve, reject) => {
    const spinner = ora(tips);
    spinner.start();
    exec(shell, (error, stdout, stderr) => {
      if (error) {
        spinner.fail();
        console.log(symbols.error, chalk.red(err));
        reject();
        process.exit();
      }
      spinner.succeed();
      resolve();
    });
  });
}

通过

代码语言:javascript
复制
await doShellJob(`cd ${name} && git init`, "Git initializing...")
await doShellJob(`cd ${name} && npm install`, "Downloading node modules...")

就可以让脚本按顺序依次执行。

最后记得当所有步骤都完成之后给用户一个友好而又显著的提示,并结束当前的 process。

代码语言:javascript
复制
console.log(
  symbols.success,
  chalk.green("Project initialization completed, enjoy you coding now!")
);
process.exit();

现在我们的脚手架工具已经搭建好了,大家可以一起来尝试下了!

全部源码

d-cli

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 原理
  • 准备工作
  • 步骤
    • 初始化项目
      • 下载模板
        • 渲染模板
          • 执行Shell
          • 全部源码
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档