文章目录
- 一、工程化概述
- 1.1 工程化的定义和主要解决的问题
- 1.2 一个项目过程中工程化的表现
- 1.3 工程化 ≠ 某个工具
- 1.4 工程化与 Node.js
- 二、脚手架工具
- 2.1 脚手架工具概要
- 2.2 常用脚手架工具
- 三、Yeoman
- 3.1 Yeoman 简介
- 3.2 Yeoman 基础使用
- 3.3 Sub Generator
- 3.4 Yeoman 使用步骤总结
- 3.5 自定义 Generator(基于 Yeoman 搭建自己的脚手架)
- 3.5.1 根据模板创建文件
- 3.5.2 接收用户输入数据
- 3.6 发布 Generator
- 四、Plop 脚手架工具
- 4.1 Plop 介绍
- 4.2 Plop 的具体使用(以 React 项目为例)
- 五、脚手架工作原理
一、工程化概述
1.1 工程化的定义和主要解决的问题
- 前端工程化是指遵循一定的标准和规范,通过工具提高效率降低成本的一种手段。近些年之所以被广泛的关注和探讨是因为前端应用的功能要求不断提高,业务逻辑日益复杂
- 从传统的网站到现在的 H5、移动APP、桌面应用,以及小程序,前端技术几乎是无所不能的全面覆盖。在这些表象的背后实际上是前端行业对我们开发人员的要求发生了天翻地覆的变化
- 以往这种写 Demo 套模板调页面的刀耕火种的方式已经完全不符合当下对开发效率的要求了。前端工程化也是在这样一个背景下被提上台面,成为前端工程师必备的手段之一
日常开发会面临的一些问题(技术是为了解决问题而存在的)
- 想要使用 ES6+ 新特性,但是兼容有问题
- 想要使用 Less/Sass/PostCss 增强 CSS 的编程性,但是运行环境不能直接支持
- 想要使用 模块化/组件化 的方式提高项目的可维护性,但是运行环境也不能直接支持
- 部署上线前需要手动压缩代码及资源文件,部署过程需要手动上传代码到服务器
- 多人协作开发,无法硬性统一大家的代码风格,从仓库中 pull 回来的代码质量无法保证
- 部分功能开发时需要等待后端服务接口提前完成才可以做具体编码
主要解决的问题
1.2 一个项目过程中工程化的表现
一切提高效率、降低成本、质量保证为目的的手段都属于工程化
实现前端工程化可以从以下几方面出发:
- 模块化
- 组件化
- 规范化
- 自动化
一切重复的工作都应该被工程化
- 创建项目:使用脚手架工具自动地完成基础项目结构搭建并创建特定类型文件
- 编码:借助工程化的工具自动做代码格式化、校验代码风格、编译/构建/打包
- 预览/测试:借助一些现代化的 Web Server 提供热更新、编译后通过 Source Map 定位源代码错误的位置,使用 Mock 解决在后端服务未完成的情况下如何提前开发具体的业务功能
- 提交:使用 Git Hooks 自动化地检查代码(项目质量/项目风格),确保不会把有问题的代码提交到仓库,也可以对提交日志做严格的格式限制
- 部署:包装命令代替传统的 ftp 上传、自动部署服务器
1.3 工程化 ≠ 某个工具
- 现阶段,部分工具过于强大,例如像 Webpack,导致很多新手误认为工程化就是指Webpack。其实不然,工具并非工程化的核心,工程化的核心应该是对项目的一种整体规划或者架构,工具只是来帮我们落地实现这种规划架构的一种手段
- 我们落实工程化地第一件事应该是去规划一个项目整体的工作流架构,有了这些整体规划过后在具体去考虑我们应该搭配那些工具做哪些具体的配置选项来去实现工程化整体的规划
一些成熟的工程化集成
- 许多人认为这些工具就是官方给出的脚手架,其实,不同于之前所提到的工具,这里所列出的更应该属于特定类型的项目,官方给出的集成式工程化方案(不仅仅是帮我们创建了项目,更多的是约定项目结构,并且提供许多工具)
1.4 工程化与 Node.js
Node 对于前端而言,除了让 JavaScript 有了一个新的舞台,更多的是让整个前端行业进行了一次工业革命
工程化是一个非常庞大的概念,而且在这个过程中也不断地发展和成长,值得强调的是,不管怎么发展,始终它都是为了解决问题而存在的,切莫为了技术而技术,接下来,我会从这五个方面具体落实前端工程化。
- 脚手架工具开发
- 自动化构建系统
- 模块化打包
- 项目代码规范化
- 自动化部署
二、脚手架工具
2.1 脚手架工具概要
脚手架工具的本质作用:创建项目基础结构、提供项目规范和约定
通常我们在去开发相同类型的项目时都会有一些相同的约定,其中包括:
- 相同的组织结构
- 相同的开发范式
- 相同的模块依赖
- 相同的工具配置
- 相同的基础代码
我们可以通过脚手架工具去快速搭建特定类型的项目骨架,基于这个骨架完成后续的开发工作
2.2 常用脚手架工具
目前市面上有许多成熟的前端脚手架工具,但大都是为了特定项目类型服务
- React 项目 -> creat-react-app
- Vue.js 项目 -> vue-cli
- Angular 项目 -> angular-cli
还有一类以 Yeoman 这样的工具为代表的通用型项目脚手架工具。它们可以根据一套模板生成一个对应的项目结构,这种类型的脚手架一般都很灵活,很容易扩展
还有一类以 Plop 为代表的脚手架,它们用于在项目开发过程中创建一些特定类型的文件,例如创建组件/模板所需要的文件
三、Yeoman
3.1 Yeoman 简介
一款用于创造现代化 Web 应用的脚手架工具(The web’s scaffolding tool for medern webapps)
- Yeoman 更像是一个脚手架的运行平台,我们可以通过 Yeoman 搭配不同的 generator 创建任何类型的项目
3.2 Yeoman 基础使用
Yeoman 是一款基于 node.js 开发的工具模块,我们首先要确定 node 环境没有问题
- 在全局范围安装 yo
$ npm install yo --global # or yarn global add yo
- 安装对应的 generator
$ npm install generator-node --global # or yarn global add generator-node
- 通过 yo 运行 generator(在自己新建的文件根目录运行)
$ yo node
- 填写相关信息
3.3 Sub Generator
有时候我们并不需要去创建完整的项目结构,可能只是需要在已有的项目之上去创建一些特定类型的文件。我们可以使用 Yeoman 提供的 Sub Generator 这样一个特性来实现
- 在项目目录下运行特定的 Sub Generator 命令生成对应的文件,例如在项目中使用 generator-node 中的子集的 cli 生成器,来帮我们生成一个 cli 应用所需要的文件
- 这里会提示我们是否要重写 package.json 这样一个文件,原因是在去添加 cli 支持的时候,会添加一些新的模块和配置,我们选择 yes
$ yo node:cli
- lib 目录下的 cli.js 提供了一些 cli 应用基础的代码结构,有了这些我们就可以将这个模块作为一个全局的命令行模块去使用
- 本地的模块我们通过 yarn link 到全局范围
$ yarn link
- 运行模块
$ yarn
$ my-module --help
注意,并不是每一个 generator 都提供自己生成器,所以我们在使用之前需要通过你所使用的 generator 的官方文档明确这个 generator 下面有没有子集生成器
3.4 Yeoman 使用步骤总结
- 明确你的需求
- 找到合适的 Generator
- 全局范围安装找到的 Generator
- 通过 Yo 运行对应的 Generator
- 通过命令行交互填写选项
- 生成你所需要的项目结构
3.5 自定义 Generator(基于 Yeoman 搭建自己的脚手架)
即便是市面上已经有了很多的 Generator,我们还是有创造自己的 Generator 的必要,因为市面上的 Generator 都是通用的,而我们在实际开发过程中会出现一部分基础代码甚至业务代码在相同类型项目时还是重复的,这时我们就可以把公共部分都放到脚手架工具当中生成,让脚手架工具发挥更大的价值
- 创建一个 Generator 本质上就是一个 NPM 模块,我们来看一下 Generator 基本结构
- 如果你需要提供多个 Sub Generator,可以在 app 下添加一个新生成器的目录作为其子生成器
注意:Yeoman 的 Generator 的模块名称必须是 generator - < name >
的格式,如果不遵守,Yeoman 后续工作时就无法找到你所提供的生成器模块
- 创建工作目录
- 创建 package.json
$ yarn init
- 安装 yeoman-generator 的模块,它提供了生成器的一个基类,这个基类中提供了一些工具函数,让我们可以在创建生成器的时候更加便捷
$ yarn add yeoman-generator
- 安装完依赖过后,打开这个目录,在这个目录下按照项目结构要求创建一个 generators 目录,在其之下创建一个 app 目录,在下面创建 index.js 文件
- 此文件会作为 Generator 的核心入口
- 需要导出一个继承自 Yeoman Generator 的类型
- Yeoman Generator 在工作时会自动调用我们在此类型中定义的一些生命周期方法
- 我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能,例如:文件写入
// index.js
const Generator = require("yeoman-generator");
module.exports = class extends Generator {
writing() {
// Yeoman 自动在生成文件阶段调用此方法
// 尝试往项目目录写入文件
// 这里的fs模块与Node中的fs不一样,这是一个高度封装的模块,功能更加强大
// write方法有两个参数:写入文件的绝对路径、写入文件的内容
// 借助destinationPath方法自动获取生成项目目录下对应的文件路径
this.fs.write(this.destinationPath("temp.txt"), Math.random().toString())
}
};
- 将此模块链接到全局范围,使之成为一个全局模块包
yarn link
- 创建一个新的工作目录(my-proj)
- 在新的工作目录下输入命令 yo sample 自动创建了名叫 temp.txt 的文件,里面内容为一个随机数
yo sample
3.5.1 根据模板创建文件
很多时候我们需要去自动创建的文件有很多,而且文件的内容也相对复杂,在这种情况下,我们就可以使用模板去创建文件,因为这样可以更加便捷一些
- 在生成器目录下添加一个目录 templates,将要生成的文件都放入目录作为模板
foo.txt
// 这是一个模版文件
// 内部可以使用 EJS 模版标记输出数据
例如 <%= title %>
// 其他的 EJS 语法也支持
<% if (success) { %>
哈哈哈
<% }%>
在生成文件时,就不用再使用 fs.write 方法了
const Generator = require("yeoman-generator");
module.exports = class extends Generator {
writing() {
// Yeoman 自动在生成文件阶段调用此方法
// 尝试往项目目录写入文件
// 这里的fs模块与Node中的fs不一样,这是一个高度封装的模块,功能更加强大
// write方法有两个参数:写入文件的绝对路径、写入文件的内容
// 借助destinationPath方法自动获取生成项目目录下对应的文件路径
/* this.fs.write(this.destinationPath("temp.txt"),
Math.random().toString())*/;
// 通过模板方式写入文件到目标目录
// 有三个参数:模板文件的路径、输出文件的路径、模板数据的上下文
// 模版文件路径
const tmpl = this.templatePath("foo.txt");
// 输出目录路径
const output = this.destinationPath("foo.txt");
// 模板数据上下文
const context = { title: "Hello", success: false };
// 自动将模版文件映射到生成的输出文件上
this.fs.copyTpl(tmpl, output, context);
}
};
- 运行 generator
yo sample
- 相对于手动创建每一个文件,模板的方式大大提高了效率,特别是在文件比较多比较复杂的情况下
3.5.2 接收用户输入数据
对于模板中的一些动态数据,例如项目的标题,项目的名称,这样的数据,我们一般通过命令行交互的方式去询问使用者从而得到
- 我们可以通过实现 generator 中的 prompting 方法发起命令行交互的询问
<!-- bar.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title><%= name %></title>
</head>
<body>
<h1><%= name %></h1>
</body>
</html>
// index.js
const Generator = require("yeoman-generator");
module.exports = class extends Generator {
prompting() {
// Yeoman 在询问用户环节会自动调用此方法
// 在此方法中可以调用父类的 prompt() 方法发出对用户的命令行询问
// prompt 接收一个数组参数,数组每一项都是一个问题对象
return this.prompt([
{
type: "input",
name: "name",
message: "Your project name", // 给用户的提示
// 父类中自动帮我们拿到的当前目录的文件夹名字,作为默认值
default: this.appname,
},
]).then((answers) => {
// answers => { name: 'user input value' }
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);
}
};
- 运行 generator
yo sample
<!-- my-proj - bar.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>my-project</title>
</head>
<body>
<h1>my-project</h1>
</body>
</html>
3.6 发布 Generator
因为 Generator 就是一个 npm 模块,所以我们发布 Generator 实际就是去发布一个 npm 模块。
- 创建 .gitignore 去忽略项目中的 node_modules
echo node_modules > .gitignore
- 初始化本地空仓库
git init
- 查看本地仓库状态并提交
git status
git add .
git commit -m "feat: initial commit"
- 在 github 创建一个新的仓库并推送
- 发布模块(使用 yarn 的官方镜像)
yarn publish --registry=https://registry.yarnpkg.com
- 如果需要你的 Generator 在官方的仓库列表出现,可以为项目添加一个 yeoman-generator 的关键词,官方会发现到此项目
四、Plop 脚手架工具
除了像 Yeoman 这样大型的脚手架工具,还有一些小型的脚手架工具也非常出色,如 Plop
4.1 Plop 介绍
Plop 是一款主要去为了创建项目中特定类型文件的小工具,有点类似 Yeoman 中的 Sub Generator,不过它不会独立去使用,一般我们会把 Plop 集成到项目当中用来去自动化的创建同类型的项目文件
- 在我们开发中经常需要创建相同类型的文件,整个过程非常繁琐,而且很难统一项目中基础的代码,这时候就可以用到 Plop
4.2 Plop 的具体使用(以 React 项目为例)
- 将 Plop 作为一个 npm 的模块安装到开发依赖当中
yarn add plop --dev
- 在项目的根目录下创建 plopfile.js 文件,并创建模版文件(都在根目录下创建)
plop-templates / component.hbs(模版文件)
import React from "react";
export default () => {
<div className="{{name}}">
<h1>{{ name }} component</h1>
</div>;
};
plop-templates / component.css.hbs(模版文件)
.{{name}} {
}
plop-templates / component.test.hbs(模版文件)
import React from 'react';
import ReactDOM from 'react-dom';
import {{name}} from './{{name}}';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<{{name}} />, div);
ReactDOM.unmountComponentAtNode(div);
});
plopfile.js
// plopfile.js
// Plop 入口文件,需要导出一个函数
// 此函数接收一个 plop 对象,用于创建生成器任务
module.exports = plop => {
// 接收两个参数:生成器的名字、生成器的配置选项
plop.setGenerator('component', {
description: 'create a component', // 生成器描述
prompts: [ // 命令行交互问题
{
type: 'input', // 指定问题的输入方式
name: 'name', // 指定问题返回值的键
message: 'component name', // 提示
default: 'MyComponent' // 问题的默认答案
}
],
actions: [ // 完成命令行交互过后的动作
{
type: 'add', // 代表添加文件
// name 表示之前在命令行交互时候的 name
// components 是某个项目里面的文件名称,可修改
path: 'src/components/{{name}}/{{name}}.js', // 使用插值表达式插入数据
templateFile: 'plop-templates/component.hbs' // 模板文件
},
{
type: 'add', // 代表添加文件
path: 'src/components/{{name}}/{{name}}.css',
templateFile: 'plop-templates/component.css.hbs'
},
{
type: 'add', // 代表添加文件
path: 'src/components/{{name}}/{{name}}.test.js',
templateFile: 'plop-templates/component.test.hbs'
}
]
})
}
- Plop 提供了 CLI 程序,我们可以启动这个 Plop 程序运行脚手架任务
yarn plop 生成器的名字
例:yarn plop component
总结
- 将 plop 模块作为项目开发依赖安装
- 在项目根目录下创建一个 plopfile.js 文件
- 在 plopfile.js 文件中定义脚手架任务
- 编写用于生成特定类型文件的模板
- 通过 Plop 提供的 CLI 运行脚手架任务
五、脚手架工作原理
基于 node.js 开发一个小型脚手架工具,脚手架工具实际上就是一个 nodecli 应用,创建脚手架工具就相当于创建一个 nodecli 应用
- 创建目录(新建一个叫 sample-scaffolding 的文件夹)
- 创建 package.json
yarn init
- 在 package.json 中添加 bin 字段,用于指定 cli 应用的入口文件
4. 创建 cli.js 文件
在 node 中发起命令行交互询问,需要使用 inquirer
yarn add inquirer
在项目根目录下新建 templates 文件夹
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= name %></title>
</head>
<body>
</body>
</html>
style.css
body {
margin: 0;
background-color: #f8f9fb;
}
安装模版引擎,用来渲染文件
yarn add ejs
cli.js
#!/usr/bin/env node
// Node CLI 应用入口文件必须要有这样的文件头
// 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改
// 脚手架的工作过程:
// 1. 通过命令行交互询问用户问题
// 2. 根据用户回答的结果生成文件
const fs = require("fs");
const path = require("path");
const inquirer = require("inquirer");
const ejs = require("ejs");
inquirer // 发起命令行交互询问,数组中每个成员都是命令行问题
.prompt([
{
type: "input",
name: "name",
message: "Project name?",
},
])
.then((anwsers) => {
// console.log(anwsers)
// 根据用户回答的结果生成文件
// 模板目录
const tmplDir = path.join(__dirname, "templates");
// 目标目录
const destDir = process.cwd(); // 当前Node.js进程执行时的文件夹地址
// 将模板下的文件全部转换到目标目录
fs.readdir(tmplDir, (err, files) => {
if (err) throw err;
files.forEach((file) => {
// file 是相对于 templates 下的相对路径
// 通过模板引擎渲染文件
// 参数:文件的绝对路径、数据上下文、回调函数
ejs.renderFile(path.join(tmplDir, file), anwsers, (err, result) => {
if (err) throw err;
// 将结果写入目标文件路径
fs.writeFileSync(path.join(destDir, file), result);
});
});
});
});
- 新建一个文件夹,输入命令,测试一下这个脚手架
sample-scaffolding