介绍

什么是脚手架?

建筑工程领域的定义:脚手架是为了保证各施工过程顺利进行而搭设的工作平台。

前端中的脚手架也是同样是思想,是一种辅助工具,它有不仅限于以下几点的诸多特点:

  1. 减少重复机械的工作(栗子:一键生成项目结构,再也不需要手动逐个创建了)
  2. 提供项目规范和约定(栗子:帮助你生成 eslint 配置)

常用的脚手架工具

  • 专用型:vue-cli、create-react-app、angular-cli
  • 通用型:Yeoman
  • 其它:Plop

脚手架工作原理

  1. 通过命令行交互询问用户问题
  2. 根据用户回答的结果生成文件

实现步骤

创建 Node CLI 应用

前端脚手架工具其实是一个 Node CLI 应用,CLI 即 Command Line Interface 命令行界面

  1. 指定 CLI 应用的入口文件

创建一个文件夹(我这里叫做 simple-scaffold) ,用 npm 初始化(npm init)为 npm 模块,然后在 package.json 中添加 bin 字段:

{
    "name": "simple-scaffold",
    "version": "0.0.1",
    "description": "a simple scaffold demo",
    "main": "index.js",
    "bin": "cli.js",
}
  1. 创建入口文件 cli.js 并添加头注释

注意要添加在第一行:

#!/usr/bin/env node
// 以上注释告诉操作系统用 node 来运行这个文件

如果是在 Linux 或 macOS 下运行,还需要修改此文件的读写权限为 755:chmod 755 cli.js(相关知识点自行搜索)

  1. 将该 npm 模块 link 到全局

cd 进入 simple-scaffold 目录后执行 npm link

> cd ./simple-scaffold
> npm link

这样模块就链接完成了,可以在 cli.js 中用 console.log 打印一些内容测试下效果:

// cli.js
console.log("success")

然后执行命令:

> simple-scaffold
> success

实现简单的命令行交互

这里借助第三方 npm 模块 inquirer 来实现,官方对于 inquirer 的描述是“常用的交互式命令行用户界面集合”,可以用来发起命令行交互:

// cli.js
const path = require("path")
const inquirer = require("inquirer")

// 获取 Node.js 进程当前工作目录文件夹的名字(作为项目默认名称)
const getCwdName = () => {
    // 当前工作目录去掉上一级目录, 留下的部分就是文件夹的名字了
    const cwd = process.cwd()
    const parentDir = path.resolve(cwd, "../")
    return cwd.replace(parentDir, "").substring(1)
}

inquirer
    .prompt([
        {
            name: "projectName",
            message: "Project name?",
            default: getCwdName, // getCwdName 的返回结果作为默认值
            filter(res) {
                // 过滤空字符串
                if (!res.trim()) return getCwdName()
                return res
            },
        },
    ])
    .then((answers) => {
        console.log(answers)
    })
    .catch((err) => {
        console.log("inquirer error", err)
    })

效果如下:

> cd ./demo
> simple-scaffold
? Project name? demo
{ projectName: 'demo' }

使用模板生成文件

在 simple-scaffold 目录下创建 templates 文件夹,然后随便创建一些文件或文件夹:

├─templates
  ├─code
  ├─notes
  ├─.gitignore
  └─README.md

README.md 中的内容:

# <%= readmeHeadline %>

解析 README.md 中的模板语法,需要安装一下 ejs 模块,然后修改 cli.js,完整的代码如下:

#!/usr/bin/env node

const fs = require("fs")
const path = require("path")
const inquirer = require("inquirer")
const ejs = require("ejs")

// 获取 Node.js 进程当前工作目录文件夹的名字(作为项目默认名称)
const getCwdName = () => {
    const cwd = process.cwd()
    const parentDir = path.resolve(cwd, "../")
    return cwd.replace(parentDir, "").substring(1)
}

const isFile = (path) => {
    const stats = fs.statSync(path)
    return stats.isFile()
}

inquirer
    .prompt([
        {
            name: "projectName",
            message: "Project name?",
            default: getCwdName,
            filter(res) {
                // 过滤空字符串
                if (!res.trim()) return getCwdName()
                return res
            },
        },
        {
            name: "readmeHeadline",
            message: "readme headline:",
            default: "Headline",
        },
    ])
    .then((answers) => {
        console.log(answers)

        // 模板目录
        const tmpDir = path.resolve(__dirname, "templates")
        // 目标目录(Node.js 进程当前工作目录)
        const destDir = process.cwd()

        fs.readdir(tmpDir, (err, files) => {
            if (err) throw err
            files.forEach((file) => {
                const tmpFilePath = path.resolve(tmpDir, file)
                const destFilePath = path.resolve(destDir, file)
                // 如果是文件, 则通过 ejs 渲染后写入目标文件夹
                // answers { projectName: xxx, readmeHeadline: xxx }
                if (isFile(tmpFilePath)) {
                    ejs.renderFile(tmpFilePath, answers, (err, result) => {
                        if (err) throw err
                        fs.writeFileSync(destFilePath, result)
                    })
                } else {
                    // 如果是文件夹, 则创建文件夹
                    if (!fs.existsSync(destFilePath)) fs.mkdirSync(destFilePath)
                }
            })
        })
    })
    .catch((err) => {
        console.log("inquirer error", err)
    })

效果如下:

> cd ./demo
> simple-scaffold
? Project name? test
? readme headline: 测试测试
{ projectName: 'test', readmeHeadline: '测试测试' }

按照模板生成了 demo 的目录结构

├─demo
  ├─code
  ├─notes
  ├─.gitignore
  └─README.md

README.md 中的内容也被正常替换

# 测试测试

结语

至此,一个简易的前端脚手架就完成啦!


千里之行始于足下