Yeoman实战指南:从零打造自定义项目生成器
优秀的开发者从不重复造轮子而是用工具把轮子造得更好,Yeoman就是这样一个工具,它让项目初始化变得如此简单,以至于你可能会忘记曾经为此烦恼过。
目录
初识Yeoman
基础使用
自定义Generator
发布Generator
初识Yeoman
Yeoman:是一个用于自动化项目脚手架创建的工具链,由Google开发并开源,它通过生成器(Generators)快速搭建项目结构,集成了主流框架(如React、Vue、Angular)和工具(如Webpack、Babel)的最佳实践,帮助开发者避免重复劳动,以下是Yeoman脚手架与其他常用脚手架的区别:
工具 | 定位 | 适用场景 |
---|---|---|
Yeoman | 通用脚手架工具 | 复杂项目结构、多技术栈 |
Create React App | React 专用工具 | 快速启动 React 项目 |
Vite | 构建工具 + 脚手架 | 现代前端项目(Vue/React) |
Angular CLI | Angular 官方工具 | Angular 项目全生命周期 |
生成器:在Yeoman生态中生成器(Generator)是核心组件,用于自动化创建项目结构和文件,它可以理解为一个可编程的项目模板,通过代码逻辑而非静态文件来生成定制化的项目,常见的全局生成器如下所示:
生成器名称 | 技术栈支持 | 项目类型 | 核心特点 |
---|---|---|---|
generator-webapp | HTML/CSS/JS, Webpack | 通用Web应用 | 基础Web项目支持PWA、Sass等,可扩展框架 |
generator-vue | Vue, Vuex, Vue Router | Vue 应用 | 多种 Vue 模板(Webpack/Vite) |
generator-react | React, Redux, TypeScript | React 应用 | 专注 React 生态,集成 CRA 替代方案 |
generator-angular | Angular, RxJS | Angular 应用 | 官方 Angular CLI 的 Yeoman 版本 |
generator-node | Node.js, npm, Express | Node 模块 / 服务 | 创建 npm 包、CLI 工具,集成测试框架 |
generator-express | Express.js, MongoDB | Web 后端服务 | 生成 Express 项目骨架,支持 MVC 架构 |
generator-webpack | Webpack | 打包工具配置 | 定制化 Webpack 配置,支持多入口、代码分割 |
generator-gulp | Gulp | 构建工具配置 | 生成Gulp任务配置,支持文件处理、压缩、编译 |
generator-eslint | ESLint | 代码规范 | 生成ESLint配置文件,支持Airbnb等风格 |
在Yeoman生态中,yo和生成器是两个核心组件,它们的关系可以类比为 "引擎" 和 "模板",如下:
它们两者的区别如下所示:
特性 | yo(命令行工具) | 生成器(Generator) |
---|---|---|
安装方式 | 全局安装:npm install -g yo | 全局 / 局部安装:npm install -g generator-webapp |
功能定位 | 执行引擎,驱动整个流程 | 定义具体项目模板和逻辑 |
可扩展性 | 固定功能,通过插件扩展 | 可自定义开发,支持无限扩展 |
使用频率 | 每次创建项目时调用 | 按需安装不同生成器 |
典型命令 | yo | yo webapp, yo node |
具体详情可以参考官方文档:地址 ,这里不再赘述,如下所示:
基础使用
接下来我们以generator-node为例,终端执行如下命令全局安装,这里建议node版本不易太高,18版本即可,否则会出现一些依赖的冲突:
npm i -g yo@4.3.1 generator-node
这里之所以指定yo版本,主要是考虑兼容性问题,因为很多生成器模板是基于旧版本的4来进行开发的,yo新版本5来安装就版本的生成器容易出现generator.run is not a function报错问题,所以这里我们指定安装下载量最多的4版本进行下载:
安装完成之后,我们cmd终端执行如下命令来创建yo脚手架,创建的命令就是将生成器前面的generator删掉保留后面的名称即可,也就是终端执行如下命令:yo node如下所示:
上面的选项也就是要对生成的模块文件中的package.json文件中的字段内容进行填充,当然一些生成器还包含一些子集的生成器,如上面安装的node生成器中就可以执行安装一下子集的生成器:
当然如果我们想安装网站应用的话,可以终端执行如下命令安装generator:
npm i generator-webapp -g
运行项目之后得到的界面如下所示:
自定义Generator
创建项目:市面上的generator不一定都适配自己具有业务逻辑的项目,所以这里可以自定义属于自己的生成器模板进行使用,自定义的generator固定格式都是 generator-<name> 的格式,接下来我们开始创建我们自定义的generator,终端执行如下命令初始化项目:
npm init -y
初始化完成之后,我们需要安装下面这个 generator 基类的插件用于自定义开发generator:
npm i yeoman-generator -s
安装完插件之后,我们在项目设置如下基本结构:
如果想添加子生成器,可以参加下面的结构:
最终得到的文件结构如下所示,这里我们在app文件下的index.js文件中编写代码,该文件是作为Generator的核心入口,需要导出一个继承自Yeoman Generator的类型,Yeoman Generator在工作时会自动调用我们在此类型中定义的一些生命周期方法,我们可以在这些方法中通过调用父类提供的一些工具方法实现一些功能,例如如下示例,演示文件写入操作:
编写完成之后,我们通过npm link的方式将这个模块链接到全局范围,使其成为一个全局模块包,这里我们就可以通过yo test的方式,执行我们的程序,这里注意一下要把package中的name属性和文件夹名称保持一致:"name": "generator-test",如下所示:
生成模板:如果想生成模板文件的话,可以在通过如下方式进行:
生成的文件就会把模板文件中的数据给替换掉,相对于手动创建每一个文件,模板的方式大大提高了效率
接收用户输入:如果想通过命令行的方式来接受用户输入的内容,然后再动态的渲染模板,我们可以通过如下的方式进行,根据用户输入的内容来动态渲染模板的上下文内容:
// 核心入口
import Generator from 'yeoman-generator';export default class extends Generator {// 通过prompt方法询问用户问题prompting() {return this.prompt([{type: 'input',name: 'title',message: 'Your project name',default: this.appname, // 默认值},{type: 'confirm',name: 'success',message: 'Do you want to continue?',default: true}]).then(answers => {this.answers = answers;})}writing() {// 模板文件路径const tmpl = this.templatePath('foo.txt');// 输出目标路径const output = this.destinationPath('foo.txt');// 模板数据上下文const context = this.answers;// 渲染模板文件并输出到目标路径this.fs.copyTpl(tmpl, output, context);}
}
效果如下所示:
后期如果想定义一个完整的项目模板的话,可以通过遍历模板中的每一个文件内容,然后通过模板渲染来给模板中有<%%>等标签的内容动态渲染,如下所示:
const templetes = ['src/index.html','src/index.js','src/index.css','src/index.vue'
]templetes.forEach(item => {this.fs.copyTpl(this.templatePath(item),this.destinationPath(item),this.answers,)
})
当然我们创建完项目之后,还可以自动下载依赖操作并提示用户项目模板下载完成:
install() {this.env.options.nodePackageManager = 'npm';
}
end() {this.log('项目生成成功');
}
当然父类Generator中也已经成长好了一些其他关于目录文件的操作方法,子类生成器中可以直接使用,如下所示:
方法名 | 方法描述 |
---|---|
this.sourceRoot() | 设置/获取模板路径 |
this.destinationRoot() | 获取/设置项目目录。一般不设置 |
this.fs.copyTpl(模板,目标,数据) | 用数据替换模板中的占位,最终拷贝到指定位置 |
this.destinationPath(index.js') | 根据项目目录,拼接路径文件名 |
this.templatePath(index.js') | 根据模板目录,拼接路径文件名 |
this.contextRoot() | 获取从哪里运行的yo |
this.fs.write(文件,内容) | 写入文件,若不存,则创建 |
this.prompt([( ), .( ]) | 发起询问,并返回Promise对象形式的结果 |
当然我们在创建Generator的时候,通过使用特定的生命周期函数可以精准的定位到代码的执行时机,以下是具体的生命周期函数的调用及其作用:
方法名 | 方法描述 |
---|---|
initializing() | 初始化,一般用于检查当前项目状态、获取配置等 |
prompting() | 提示用户做出选择。一般里面会调用this.prompt() |
configuring() | 保存配置和配置项目。如创建.editorconfig文件和其它元数据文件 |
default() | 如果方法名与优先级不匹配,则将其推送到该组 |
writing() | 创建项目时,写入文件或创建目录的地方 |
conflicts() | 处理冲突的地方。一般为内部使用 |
install() | 运行安装的位置。如npm、bower |
end() | 最后调用。通常做一些清理工作。 |
发布Generator
generator实际上就是一个npm的模块,如果我们想发布generator的话就相当于发布npm模块,这里我们可以直接通过通过npm publish的方式进行发布即可:
具体的发布流程及其注意事项,可以参考我之前的文章:地址 ,这里不再赘述,最终发布到npm平台之后,就可以通过yo 你发布的模块 命令来下载你自定义的模板了,发布成功之后就可以看到你的包名,后面全局下载到本地依赖即可: