Yeoman - 搭建自己的脚手架

引言

随着前端工程化的理念不断深入,越来越多的人选择使用脚手架来从零到一搭建自己的项目。其中大家最熟悉的就是create-react-appvue-cli,它们可以帮助我们初始化配置、生成项目结构、自动安装依赖,最后我们一行指令即可运行项目开始开发,或者进行项目构建(build)。

这些脚手架提供的都是普遍意义上的最佳实践,但是我在开发中发现,随着业务的不断发展,必然会出现需要针对业务开发的实际情况来进行调整。例如:

  • 通过调整插件与配置实现 Webpack 打包性能优化后

  • 删除脚手架构建出来的部分功能

  • 项目架构调整

  • 融合公司开发工具

  • ……

总而言之,随着业务发展,我们往往会沉淀出一套更“个性化”的业务方案。这时候我们最直接的做法就是开发出一个该方案的脚手架来,以便今后能复用这些最佳实践与方案。

脚手架怎么工作?

功能丰富程度不同的脚手架,复杂程度自然也不太一样。但是总体来说,脚手架的工作大体都会包含几个步骤:

  • 初始化,一般在这个时候会进行环境的初始化,做一些前置的检查

  • 用户输入,例如用 vue-cli 的时候,它会“问”你很多配置选项

  • 生成配置文件

  • 生成项目结构,这是候可能会使用一个项目模版

  • 安装依赖

  • 清理、校验等收尾工作

生命周期

Yeoman 内置的“生命周期”方法执行顺序如下:

  1. initializing - 初始化一些状态之类的,通常是和用户输入的 options 或者 arguments 打交道,这个后面说。

  2. prompting - 和用户交互的时候(命令行问答之类的)调用。

  3. configuring - 保存配置文件(如 .babelrc 等)。

  4. default - 其他方法都会在这里按顺序统一调用。

  5. writing - 在这里写一些模板文件。

  6. conflicts - 处理文件冲突,比如当前目录下已经有了同名文件。

  7. install - 开始安装依赖。

  8. end - 擦屁股的部分... Say Goodbye maybe...

其中 default 阶段会执行你自定义地各种方法。

开发一个自己的脚手架

准备一个项目模版

脚手架是帮助我们快速生成一套既定的项目架构、文件、配置,而最常见的做法的就是先写好一套项目框架模版,等到脚手架要生成项目时,则将这套模版拷贝到目标目录下。这里其实会有两个小点需要关注。

第一个是模版内变量的填充。

在模版中的某些文件内容可能会需要生成时动态替换,例如根据用户在终端中输入的内容,动态填充package.json中的name值。而 Yeoman 内置了 ejs 作为模版引擎,可以直接使用。

第二个就是模版的放置位置。

一种是直接放在本地,也就是直接放到 generator 中,跟随 generator 一起下载,每次安装都是本地拷贝,速度很快,但是项目模版自身的更新升级比较困难,需要提示用户升级 generator。

另一种则是将模版文件放到某个服务器上,每次使用脚手架初始化时通过某个地址动态下载,想要更新升级模版会很方便,通常会选择托管在 github 上。

关于第二个模版放置究竟是选择在本地好,还是远端好,其实还是依据你个人的业务场景而定,在不同的场景的限制的需求不同,我之前既写过模版放在本地的脚手架(即和脚手架一起通过 npm 安装),也写过托管在 git 仓库上的这种方式。

创建 generator

创建 Yeoman 的 generator 需要遵循它的规则。

首先是 generator 命名规则。需要以generator打头,横线连接。

执行命令

1 npm i -g yo
2 npm i generator-generator -g
3 yo generator
# 若3一直处于卡顿状态可以使用4命令,执行generator
# 4 yo

例如你想创建一个名为 webpack-kickoff 的 generator,包名需要取成 generator-webpack-kickoff,需要在执行3时输入包名

执行3命令后,主要生成如下文件:

├── .yo-rc.json
├── package.json
├── generators
│   ├── app
│       ├── templates
│           ├── dummyfile.txt
│       ├── index.js
  • .yo-rc.json 用于存储项目配置,一般不会用到,无需关注
  • package.json npm 项目信息文件,主要关注 author、version 域即可
  • generators 目录即项目模板代码
  • generators/templates 用于存放项目模板文件
  • generators/app/index.js 定义项目手脚架的代码

index.js中写入你的脚手架工作流程。

此外,你创建的 generator 类需要继承 yeoman-generator。所以我们会在generators/app/index.js中写如下代码:

const Generator = require('yeoman-generator');
class WebpackKickoffGenerator extends Generator {
    constructor(params, opts) {
        super(params, opts);
    }
}
module.exports = WebpackKickoffGenerator;

还记得之前提到的“生命周期”方法么?包括 initializing、prompting、default、writing、conflicts、install 和 end。除了default,其他都代表了 Generator 中的一个同名方法,你需要的就是在子类中重写后所需的对应方法。default阶段则会执行用户定义的类方法。

例如,你想在初始化时打印下版本信息,可以这么做:

const Generator = require('yeoman-generator');
class WebpackKickoffGenerator extends Generator {
    constructor(params, opts) {
        super(params, opts);
    }

    initializing() {
        const version = require('../../package.json').version;
        this.log(version);
    }
}
module.exports = WebpackKickoffGenerator;

可见,剩下的工作就是在 WebpackKickoffGenerator 类中填充各种方法的实现细节了。

处理用户交互

脚手架工作中一般都会有一些用户自定义的内容,例如创建的项目目录名,或者是否启用某个配置等。这些交互一般都是通过交互式的终端来实现的,例如下面这个功能。

可以使用 Inquirer.js 来实现。而 Yeoman 已经帮我们集成好了,直接在 generator 里调用this.prompt 即可。

在用户交互部分的需求也比较简单,只需要询问用户所需创建的项目目录名即可,随后也会作为项目名。按照 Yeoman 的流程规范,我们将该部分代码写在 prompting 方法中:


    prompting() {
        const done = this.async();

        const opts = [{
            type: 'input',
            name: 'dirName',
            message: 'Please enter the directory name for your project:',
            default: 'webpack-app',
            validate: dirName => {
                if (dirName.length < 1) {
                    return '⚠️  directory name must not be null!';
                }
                return true;
            }
        }];

        return this.prompt(opts).then(({dirName}) => {
            this.dirName = dirName;
            done();
        });
    }

注意,由于用户交互是一个“异步”的行为,为了让后续生命周期方法在“异步”完成后再继续执行,需要调用this.async()方法来通知方法为异步方法,避免顺序执行完同步代码后直接调用下一阶段的生命周期方法。调用后会返回一个函数,执行函数表明该阶段完成。

下载模版

我们选择将模版托管在 github 上,因此在生成具体项目代码前,需要将相应的文件下载下来。可以使用 download-git-repo 来快速实现。

class WebpackKickoffGenerator extends Generator {
    // ……
    _downloadTemplate() {
        return new Promise((resolve, reject) => {
            const dirPath = this.destinationPath(this.dirName, '.tmp');
            download('alienzhou/webpack-kickoff-template', dirPath, err => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve();
            });
        });
    }
    // ……
}

这里我们使用了this.destinationPath()方法,该方法主要用于获取路径。不传参时返回当前命令行运行的目录;如果收到多个参数,则会进行路径的拼接。

此外,如果你细心的话,会发现_downloadTemplate()方法带了一个下划线前缀。这是 Yeoman 中的一个约定:Yeoman 执行顺序中有个default阶段,该阶段包含了所有用户自定义的类方法。但是,如果某些方法你不希望被 Yeoman 的脚手架流程直接调用,而是作为工具方法提供给其他类方法,则可以添加一个下划线前缀。对于这种命名的方法,则会在default阶段被忽略。

模版文件拷贝

项目模版下载完毕后,下面就可以将相关的目录、文件拷贝到目标文件夹中。这些都可以在writing阶段操作。此时需要遍历模版中的所有目录,将所有文件进行模版填充与拷贝。遍历方式如下:

class WebpackKickoffGenerator extends Generator {
    // ……
    _walk(filePath, templateRoot) {
        if (fs.statSync(filePath).isDirectory()) {
            fs.readdirSync(filePath).forEach(name => {
                this._walk(path.resolve(filePath, name), templateRoot);
            });
            return;
        }

        const relativePath = path.relative(templateRoot, filePath);
        const destination = this.destinationPath(this.dirName, relativePath);
        this.fs.copyTpl(filePath, destination, {
            dirName: this.dirName
        });
    }
    // ……
}

这里使用了this.fs.copyTpl()方法,它支持文件拷贝,同时还可以指定相应的模版参数,此外,如果出现重名覆盖情况会在控制台自动输出相应信息。

最后,把下载与拷贝整合起来即可完成writing阶段。

根据用户的选择,判断哪些文件需要copy

class WebpackKickoffGenerator extends Generator {
    // ……
    writing() {
        const done = this.async();
        this._downloadTemplate()
            .then(() => {
                const templateRoot = this.destinationPath(this.dirName, '.tmp');
                this._walk(templateRoot, templateRoot);
                fs.removeSync(templateRoot);
                done();
            })
            .catch(err => {
                this.env.error(err);
            });
    }
    // ……
}

yo 提供mem-fs-editor实例接口,包含一系列fs工具:

  • this.fs.read - 读取文件
  • this.fs.readJSON - 以JSON方式读取文件
  • this.fs.write - 写文件
  • this.fs.writeJson - 以JSON 方式写文件
  • this.fs.append - 将内容已追加方式写入文件
  • this.fs.extendJSON - 扩展JSON文件内容
  • this.fs.delete - 删除文件

此外,还有一系列路径及模板接口:

  1. this.fs.copyTpl - 复制模板文件,并按参数解析模板内容,写入目标文件中
  2. this.templatePath - 返回模板文件路径,即上述 generator/app/templates 中的文件路径
  3. this.destinationPath - 返回目标文件路径,即执行 yo 生成模板文件的路径
  4. this.registerTransformStream - 生命钩子接口,用于转化文件内容,兼容gulp插件

依赖安装

到目前,脚手架已经可以帮我们把项目开发所需的配置、目录结构、依赖清单都准备好了。这时候可以进一步帮开发人员将依赖安装完毕,这样脚手架创建项目完成后,开发人员就可以直接开发了。

Yeoman 也提供了this.npmInstall()来方法来实现 npm 包的安装:

class WebpackKickoffGenerator extends Generator {
    // ……
    install() {
        this.npmInstall('', {}, {
            cwd: this.destinationPath(this.dirName)
        });
    }
    // ……
}

到这里,脚手架的核心功能就完成了。已经可以使用咱们的这个 generator 来快速创建项目了。很简单吧~

完整的代码可以参考 generator-webpack-kickoff。

Sub-Generator

generator目录下添加其他目录,并添加templates目录和index.js文件

命令 yo xxx 默认执行 app/index.js

命令 yo xxx:component  默认执行 component/index.js

可以实现在项目中添加component模块

使用脚手架 🚀

使用该脚手架会同时需要 Yeoman 与上述咱们刚创建的 yeoman-generator。当然,有一个前提,Yeoman 与这个 generator 都需要全局安装。全局安装 Yeoman 没啥有问题(npm install -g yo),处理 generator-webpack-kickoff 的话可能有几种方式:

  1. 直接发布到 npm,然后正常全局安装

  2. 直接手动拷贝到全局 node_modules

  3. 使用npm link将某个目录链接到全局

由于包已经发到 npm 上了,所以要使用该脚手架可以运行如下指令:

# 安装一次即可
npm i -g yo
npm i -g generator-webpack-kickoff

# 启动脚手架
yo webpack-kickoff

4. 优化

从上文这个例子可以看出,实现一个脚手架非常简单。例子虽小,但也包含了脚手架开发的主要部分。当然,为了简化省略了一些“优化”功能。例如

  • 项目目录的重名检测,生成项目时,检查是否目录已存在,并提示警告

  • 项目模版的缓存。虽然我们使用 github 托管方式,但也可以考虑不必每次都重新下载,可以放一份本地缓存,然后每天或每周更新;

  • CLI 的优化。完整版里还会包含一些更丰富的 CLI 使用,例如我们在动图中看到的 loading 效果、头尾显示的信息面板等。这些工具包括

    • ora,用于创建 spinner,也就是上面所说的 loading 效果

    • chalk,用于打印彩色的信息

    • update-notifier,用于检查包的线上版本与本地版本

    • beeper,可以“哔”一下你,例如出错的时候

    • boxen,创建头尾的那个小“面板”

  • 版本检查。上面提到可以用 update-notifier 来检查版本。所以可以在 initializing 阶段进行版本检查,提示用户更新脚手架。


通过以下Blog,摸索并实现了一个简单的脚手架(仅供学习):Yxd-Cli

如果有不正确的地方,欢迎请指出😃

使用Yeoman搭建自己的脚手架

如何快速开发一个自己的项目脚手架?

 前端工程化系列[07] Yeoman-generator的创建

手把手入门 Yeoman 模板开发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值