🛠如何快速开发一个本身的项目脚手架?

喜欢的朋友欢迎关注个人博客RSS 订阅html

引言

下面是一个使用脚手架来初始化项目的典型例子。前端

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

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

  • 经过调整插件与配置实现 Webpack 打包性能优化后
  • 删除脚手架构建出来的部分功能
  • 项目架构调整
  • 融合公司开发工具
  • ……

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

1. 脚手架怎么工做?

功能丰富程度不一样的脚手架,复杂程度天然也不太同样。可是整体来讲,脚手架的工做大致都会包含几个步骤:webpack

  • 初始化,通常在这个时候会进行环境的初始化,作一些前置的检查
  • 用户输入,例如用 vue-cli 的时候,它会“问”你不少配置选项
  • 生成配置文件
  • 生成项目结构,这是候可能会使用一个项目模版
  • 安装依赖
  • 清理、校验等收尾工做

此外,你还须要处理命令行行为等。每每咱们只是想轻量级、快速得建立一个特定场景的脚手架(不用想vue-cli那么完备)。而对于想要快速建立一个脚手架,其实咱们不用彻底从零开始。Yeoman 就是一个能够帮咱们快速建立脚手架的工具。git

可能不少同窗都不太了解,那么先简单介绍一下 Yeoman 是什么,又是如何帮咱们来简化脚手架搭建的。github

首先,Yeoman 能够简单理解为是一个脚手架的运行框架,它定义了一个脚手架在运行过程当中所要经历的各个阶段(例如咱们上面说的,可能会先读取用户输入,而后生成项目文件,最后安装依赖),咱们所须要的就是在生命周期的对应阶段,填充对应的操做代码便可。而咱们填充代码的地方,在 Yeoman 中叫作 generator,物如其名,Yeoman 经过调用某个 generator 便可生成(generate)对应的项目。web

若是你还不是特别清楚它们之间的关系,那么能够举个小例子:vue-cli

将脚手架开发类比为前端组件开发,Yeoman 的角色就像是 React,是一个框架,尤为是定义了组件的生命周期函数;而 generator 相似于你写的一个 React 业务组件,根据 React 的规则在各个生命周期中填代码便可。

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

  1. initializing
  2. prompting
  3. default
  4. writing
  5. conflicts
  6. install
  7. end

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

同时,Yeoman 还集成了脚手架开发中经常使用的各种工具,像是文件操做、模版填充、终端上的用户交互功能,命令行等,而且封装成了简单易用的方法。

经过这两点,Yeoman 能够帮咱们大大规范与简化脚手架的开发。

2. 开发一个本身的脚手架

了解了一些脚手架的工做方式与 Yeoman 的基本概念,我们就能够来建立一个属于本身的脚手架。做为例子,这个脚手架的功能很简单,它会为咱们建立一个最简版的基于 Webpack 的前端项目。最终脚手架使用效果以下:

2.1. 准备一个项目模版

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

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

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

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

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

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

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

回到咱们「建立一个最简版的基于 Webpack 的前端项目」的目标,我准备了一个项目模版,以后就会用它来做为脚手架生成的项目内容。

2.2. 建立 generator(yeoman-generator)

建立 Yeoman 的 generator 须要遵循它的规则。

首先是 generator 命名规则。须要以generator打头,横线链接。例如你想建立一个名为 webpack-kickoff 的 generator,包名须要取成 generator-webpack-kickoff

这样,当你经过

npm i -g yo
复制代码

安装完 Yeoman 的 CLI 后,就能够经过yo命令来使用 generator 来启动脚手架:

yo webpack-kickoff
复制代码

这里的 webpack-kickoff 就是包名里generator-后面的内容,Yeoman 会按这个规则去全局找相匹配的包。

其次,依据 Yeoman 的规范,默认状况下你须要在项目(即 generator)的generators/app/目录下建立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 类中填充各类方法的实现细节了。

2.3. 处理用户交互

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

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

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

class WebpackKickoffGenerator extends Generator {
    // ……
    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()方法来通知方法为异步方法,避免顺序执行完同步代码后直接调用下一阶段的生命周期方法。调用后会返回一个函数,执行函数代表该阶段完成。

2.4. 下载模版

正如2.1.中所述,咱们选择将模版托管在 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阶段被忽略。

2.5. 模版文件拷贝

项目模版下载完毕后,下面就能够将相关的目录、文件拷贝到目标文件夹中。这些均可以在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阶段。

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);
            });
    }
    // ……
}
复制代码

2.6. 依赖安装

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

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

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

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

完整的代码能够参考 generator-webpack-kickoff

3. 使用脚手架 🚀

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

  1. 直接发布到 npm,而后正常全局安装
  2. 直接手动拷贝到全局 node_modules
  3. 使用npm link将某个目录连接到全局

依据2.2.节的内容,我们的 generator 名称为 generator-webpack-kickoff。因为个人包已经发到 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 阶段进行版本检查,提示用户更新脚手架。

最后

本文经过一个简单的例子来告诉你们如何使用 Yeoman 快速建立脚手架。要了解更多 yeoman-generator 的开发与使用,能够参考社区里你们写的各种 generator。目前在 npm 上有超过 8000 个 yeoman-generator,也许就会有你的菜。

文中完成的代码请查看 generator-webpack-kickoff

相关文章
相关标签/搜索