浅谈前端工程化之前端脚手架工具--Yeoman、Plop

在日常开发中,我们经常使用脚手架工具来帮助我们创建项目,如vue-cli,create-react-app等,本文主要概述下什么是工程化,工程化主要解决的问题是什么,探讨如何使用Yeoman和Plop工具来搭建脚手架,最后来探讨下脚手架的基本原理。

前端工程化

前端工程化是指遵循一定的标准和规范,通过工具去提高效率,降低成本的一种手段,近些年被广泛的关注和探讨,究其原因主要是因为前端应用功能要求不断提高,业务逻辑日益复杂,从传统的网站,到现在的H5、移动web、桌面应用、以及小程序,前端技术几乎是无所不能全面覆盖,在这些表象的背后,实际上是前段行业我对我们开发人员的要求越来越高。

之前的前端

以往这种写Demo,套模板,再去调页面的这种“刀耕火种”方式,已经完全不符合对当下开发效率的要求,前段工程化就是在这样背景下被提上台面的,成为前端工程师必备的手段之一,技术是为了解决问题而存在的,前端工程化也不例外。

1. 工程化之前我们面临的一些问题:

  • 想要使用ES6 +新特性,提高编码效率但是兼容有问题

  • 想要使用 Less / Sass / PostCSS 提高 CSS 的编程性,但是这种工具在运行环境不能直接支持

  • 想要使用模块化或者组件化的方式提高项目的可维护性,但实际运行环境不能直接支持

  • 开发中经常会手动做的一些重复的工作,如部署上线前需要手动压缩代码及资源文件,部署过程需要手动上传代码到服务器

  • 多人协作开发,无法硬性统一大家的代码风格,从仓库中 pull 回来的代码质量无法保证

  • 部分功能开发时,需要等待后端服务接口提前完成,才可以做具体的编码

2. 工程化主要解决的问题

  • 传统语言或语法的弊端

  • 无法使用模块化/组件化

  • 重复的机械式工作

  • 代码风格统一、质量保证

  • 依赖后端服务接口支持

  • 整体依赖后端项目

3. 工程化的表现

以项目开发过程为例,来看下工程化的表现:

工程化的表现

  • 创建项目阶段:使用脚手架自动完成项目基础结构的搭建;

  • 编码阶段:借助工程化的工具,自动化的帮我们做一些代码的格式化和风格校验,确保在项目中每个开发人员写出的代码风格相似,借助一些编译工具,提高代码编码效率

  • 预览/测试阶段:传统的预览需要借助Apache服务器提供基础的web服务,让应用可以在上面运行起来,但是没有热更新体验,除此之外在开发阶段会用到编译,编译就涉及到实际编写的代码和最终运行的代码有个转换,如果运行中存在问题需要借助source Map工具去定位到源代码所在的位置,使用mock方式可以解决,后端服务未完成的情况下提前开发业务功能,即写假接口方式,跟后端接口是以相同的规格存在的

  • 提交阶段:使用Git Hooks 方式自动化的在代码提交之前对项目做整体的检查,包括项目质量检查和项目风格检查

  • 部署阶段:使用命令代替传统的FTP上传,还可以实现代码提交后自动化的通过持续集成或者持续部署的方式自动将我们代码部署到服务器,避免了手动操作产生的不稳定的因素

一切重复的工作都应该被自动化。实现前端工程化可以从 模块化、组件化、规范化、自动化等方面出发。

4. 工程化不等于工具

工程化是对项目整体的一种规划或者架构,工具只是实现规划或者结构的手段

工程化流程

如上图 一个工程化应该有的过程是规划项目整体的工作流架构,包括文件的组织结构,源代码的开发范式,以什么样的方式做前后端分离(基于ajax还是中间层),明确规划后再根据这些规划来搭配工具做具体的配置选项,从而实现工程化整体的规划等

一些成熟的工程化集成:create-react-app、vue-cli、angular-cli、gatsby-cli工具,需要注意的是这里工具不是脚手架,不同于之前的工具,这里的工具属于特定类型的项目官方给出的集成式工程化方案,以vue-cli为例,它不仅仅帮我们创建了项目,更多的是约定了vue项目应该是什么样的结构,提供了热更新开发服务的工具,自动编译单文件组件以及其他的模块文件,代码风格校验等,这些都是集成在vue-cli内部的server中。

5. 工程化与Node.js

工程化的一切都应该归功于Node。

如果说Ajax给前端带来了新的生命力,那么Node对于前端而言,它除了让JavaScript有了一个新的舞台,更多的是让我们整个前端行业进行了一次工业革命,可以毫无夸张的说,没有Node.js,就没有今天的前端。

前端工程化是由Node.js强烈驱动的。

脚手架工具

概要

简单来说,脚手架就是用来自动的去帮我们创建项目基础文件的一个工具。脚手架工具,可以认为是前端工程化的发起者。

脚手架工具的作用

创建项目基础结构、提供项目规范和约定。

一般来说,一个项目中包含一些相同的约定,即:

  • 相同的组织结构

  • 相同的开发范式

  • 相同的模块依赖

  • 相同的工具配置

  • 相同的基础代码

我们可以通过脚手架工具去快速搭建特定类型项目的骨架,然后去基于这个骨架进行后续开发工作。因为前端技术选项比较多样,也没有一个统一的标准,所以前端方向的脚手架不会集成在某一个IDE当中,都是以一个独立的工具存在,相对会复杂些。

脚手架目标都是一样的,都是为了解决我们在创建项目过程当中那些复杂的工作。

常用的脚手架工具

  • 适用于特例项目类型服务的脚手架工具

    目前一些成熟的脚手架工具(但大都是为了一些特例项目类型服务的):

    • React项目 => create-react-app

    • Vue.js项目 => vue-cli

    • Angular项目 => angular-cli

​ 这些工具的实现方式都大同小异,无非都是根据信息创建对应的项目基础结构。

  • 通用性脚手架工具: Yeoman

Yeoman

概述

Yeoman是基于Nodejs开发的模块,是一款用于创造现代化web应用的脚手架工具,不同于cli工具,Yeoman更像是一个脚手架运行平台,我们可以通过它,搭配不同的Generator,去创建任何类型的项目。

基于Yeoman搭建一个项目类型的脚手架

  • 全局范围安装Yeoman
1
$ yarn global add yo #or npm install yo --global
  • 安装项目模板:Generator

    Yeoman 需要搭配特定的 Generator 使用,在这里使用的node_module,因此还需要全局安装 generator-node

1
$ yarn global add generator-node # or npm install generator-node --global
  • 通过yo运行对应的Generator

    使用 Yeoman 提供的yo命令,去运行generator-node生成器。运行特定的生成器,就是将generator-前缀去掉,直接使用yo运行后面的部分即可。

1
$ yo node

运行结果如下:

项目结构,如下图所示:

Sub Generator

有时候我们不需要创建完整的项目结构,可能是在已有的项目基础上创建一些特定类型的文件,比如在原有项目中添加一些配置,如eslit,babel文件,都会有一些基础的文件,可以通过生成器自动生成,可以使用Yeoman提供的Sub Generator特性进行实现,具体实现是在项目中运行特定的Sub Generator命令,生成对应的文件

  • 使用 generator-node中的子集的生成器,即cli生成器,它可以生成cli应用所需要的一些文件。
1
$ yo node:cli # or yo + generator的名字 + : + Sub Generator的名字

  • 通过以下命令,将这个模块连接到全局范围,使其可以作为全局的命令行模块进行使用。
1
$ npm link # or yarn link

可以看到,此时会将新的模块生成的依赖包,存放到 npm 应用程序的本地数据文件夹中。

  • 安装项目依赖。
1
$ yarn
  • 全局访问生成的模块
1
$ my-module --help

注意:并不是每一个 generator 都存在子集的生成器,需要以官方文档为准。

基于Yeoman自定义一个脚手架工具

基于Yeoman自定义一个脚手架工具,这个脚手架工具通常用来搭建项目类型的脚手架。实现上其实也就是自定义Yeoman的Generator,在这个过程中我们需要准备自己的脚手架模板项目并编写自己的generator模块。与Plop一样,相对于完全自定义实现脚手架脚本 / 工具而言,使用Yeoman虽然不能实现完全的自定义,但在它的规范下编写脚本可以调用它封装好的Api,可以简化脚本的编写。这让我们能够更加关注任务本身,而无需关注过多任务的实现细节,进而提高开发效率。

命名规范

如果命名不规范,那么后期 Yeoman 就无法找到所定义的生成器模块。

1
generator-name

以自定义Vue Generator为例:

  • 创建generator-eline-vue文件夹,并创建及初始化package.json包管理文件,以及安装yeoman-generator模块
1
2
3
4
5
6
7
$ mkdir generator-eline-vue

$ cd generator-eline-vue

$ yarn init # or npm init

$ yarn add yeoman-generator # or npm install yeoman-generator -D
  • 将 vue中生成的目录结构,拷贝到Generator中,使其作为模板文件存在,如下图所示:

  • 根据自定义 Generator 步骤,在 index.js 入口文件中,解析项目模板创建项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// 此文件作为Generator的核心入口

// 需要导出一个继承自Yeoman Generator 的类型

// Yeoman Generator 在工作时会自动调动我们在此类型中定义的一些生命周期方法

// 我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能,例如文件写入

const Generator = require('yeoman-generator')

// 发起命令行交互的询问,使用generator中的prompting方法

module.exports = class extends Generator {

prompting () {

// yeoman在询问用户环节会自动调用此方法

// 在此方法中可以调用父类的prompt()方法发出对用户的命令询问

// 接受一个数字参数,数组的每一项都是一个问题对象

return this.prompt ([ //返回一个promise对象(是一个promise方法)

{

//还用用户输入的方式,让用户提交信息

type: 'input',

//最终得到结果的一个键

name: 'name',

//在界面中给用户提示,即问题

message: 'Your project name',

//appname为项目生成目录名称

default: this.appname

}

// 用户输入之后的结果

]).then(answers => {

// 以对象的形式出现

// answers = >{ name:'user input value'}

this.answers = answers

})

}

writing (){

// Yeoman 自动在生成文件阶段调用writing方法

const templates = [

'.browserslistrc',

'.editorconfig',

'.env.development',

'.env.production',

'.eslintrc.js',

'.gitignore',

'babel.config.js',

'package.json',

'postcss.config.js',

'README.md',

'public/favicon.ico',

'public/index.html',

'src/App.vue',

'src/main.js',

'src/router.js',

'src/assets/logo.png',

'src/components/HelloWorld.vue',

'src/store/actions.js',

'src/store/getters.js',

'src/store/index.js',

'src/store/mutations.js',

'src/store/state.js',

'src/utils/request.js',

'src/views/About.vue',

'src/views/Home.vue'

]

templates.forEach(item => {

// item每个文件路径

// 利用fs的copyTpl方法:解析ejs模板文件后放入目标路径下

// copyTpl(模板文件路径,输出目标路径,模板数据上下文)

this.fs.copyTpl(

this.templatePath(item),

this.destinationPath(item),

this.answers

)

})

}

}
  • 通过以下命令,将这个模块连接到全局范围,使其可以作为全局的命令行模块进行使用。
1
$ npm link # or yarn link

可以看到,此时会将新的模块生成的依赖包,存放到npm应用程序的本地数据文件夹中。

*执行 yeoman 操作,创建一个跟模板相同的新项目

1
$ yo eline-vue
  • 将generator模块发布到GitHub和npm中
  1. 在发布之前,首先将项目的源代码托管到公开的代码仓库里面,这里我们使用GitHub。

    这里就不在介绍,具体可参考GIT关联本地仓库与远端仓库

  2. 在命令行界面输入发布命令,需要确保在npm中有注册账号。

1
$ yarn publish # or npm publish

发布成功后可以去npm官网,查看刚刚发布的模块,如下图所示:

需要注意:若使用淘宝的镜像资源,则会报错下面的错误,这是由于淘宝镜像是只读的,不能将模块进行发布

  • 可以改变镜像资源
1
2
3
$ npm config set registry https://registry.yarnpkg.org  # yarn 镜像源

$ npm config set registry https://registry.npmjs.org # node 默认镜像源
  • 在命令后面,直接跟上相关的镜像资源
1
2
3
$ yarn publish --registry-https://registry.yarnpkg.com # yarn 镜像源

$ yarn publish --registry-https://registry.npmjs.org # node 默认镜像源

发布报错时候可以参考yarn publish 报错npm发布包

基于Plop搭建单 / 多文件类型的脚手架

基本介绍

plop是一个在日常开发当中非常值得且频繁使用的小工具,在我们碰到需要搭建一个单/多文件类型的脚手架需求,比如搭建一个react组件脚手架(需要创建一个.js模板文件、一个.css模板文件以及一个.test.js模板文件),Plop就会是一个不错的助手。相对于完全自定义实现脚手架脚本/工具而言,使用Plop虽然不能实现完全的自定义,但在它的规范下编写脚本可以调用它封装好的Api,实现配置化的脚手架工具。这让我们能够更加关注任务本身,而无需关注过多任务的实现细节,进而提高开发效率。

接下来主要关注下Plop自动化搭建脚手架的工作流程,具体的使用细节查看官方文档Plop

基本使用

这里以react为例,集成Plop。

  1. 创建react项目
1
$ npx create-react-app my-react-app
  1. 将 plop 模块作为项目开发依赖安装
1
$ yarn add plop --dev # or npm install plop -D
  1. 在项目根目录下,新建一个 plopfile.js 文件,定义 Plop 脚手架任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Plop 入口文件,需要导出一个函数

// 此函数接收一个 plop对象,并且提供了一系列的工具函数,用于创建生成器任务

module.exports = plop => {

/**

* setGenerator(生成器的名称,生成器的配置选项)

*/

plop.setGenerator('component', {

description: 'create a component',

// 指定Generator在执行时,发出的命令行问题

prompts: [

{

type: 'input',

name: 'name',

message: 'component name',

default: 'MyComponent'

}

],

// 完成命令行交互过后,需要执行的动作,数组中的每一个对象,都表示一个动作对象, 可以创建多个模板

{

// type指定动作的类型,add代表添加一个全新的文件

type: 'add',

// 文件输出位置

// 可以通过{{}}插值表达式的形式,去动态插入刚才用户输入的数据

path: 'src/components/{{name}}/{{name}}.js',

// 模板文件位置

templateFile: 'plop-templates/components.hbs'

},

{

type: 'add',

path: 'src/components/{{name}}/{{name}}.css',

templateFile: 'plop-templates/components.css.hbs'

},

{

type: 'add',

path: 'src/components/{{name}}/{{name}}.test.js',

templateFile: 'plop-templates/components.test.hbs'

}

]

})

}
  1. 创建对应的模板文件,模板文件采用Handlebars模板引擎进行书写,一般放置在根目录下的plop-templates下

components.hbs

1
2
3
4
5
6
import React from 'React'
export default () => {
<div className="{{name}}">
<h1>{{name}} Component</h1>
</div>
}

components.css.hbs

1
2
3
.{{name}} {

}

components.test.hbs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { render, screen } from '@testing-library/react';

import {{name}} from './{{name}}';



test('renders learn react link', () => {

render(<<name>> />);

const linkElement = screen.getByText(/learn react/i);

expect(linkElement).toBeInTheDocument();

});
  1. 在安装 plop 时,Plop 提供了一个 CLI 的应用程序,由于yarn会自动找到node_modules下的bin目录下的命令行工具,所以可以通过 yarn 去启动这个程序
1
$ yarn plop component # yarn plop 生成器的名称

执行结果,如下图所示:

执行成功后,我们可以去对应的文件夹下,找到生成的文件,如下图所示:

脚手架工作原理

通过前面对脚手架工具的介绍,我们不难发现,大部分脚手架的工作原理都很简单,无外乎就是启动之后,它会自动的询问一些预设的问题,然后将回答的结果结合一些模板文件生成一个项目的结构,我们都知道脚手架工具就是一个 node cli 的应用,去创建脚手架工具,就是创建cli的应用,接下来通过nodejs开发一个小型的脚手架工具,在深入体会下脚手架工作的过程

  1. 通过 在package.json 中添加属性bin,设置入口文件,用于指定CLI应用的入口文件

  2. 编写 cli.js 文件,与以往的入口文件不同的,Node CLI 应用入口文件必须要有特定的文件头#!/usr/bin/env node,如果是Linux或者macOS系统下还需要修改此文件的读写权限为755

1
2
3
4
5
6
7
//  使用inquirer模块实现用户交互

$ yarn add inquirer # or npm install inquirer

// 使用ejs模块进行文件渲染

$ yarn add ejs # or npm install ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/env node

// Node CLI 应用入口文件必须要有这样的文件头

// 如果是Linux或者macOS系统下还需要修改此文件的读写权限为 755

// 具体就是通过chmod 755 cli.js 实现参数

// 脚手架的工作过程

// 1.通过命令行交互询问用户问题

// 2.根据用户回答生成文件

// 发起命令行交互询问

const path=require('path')

const fs =require('fs')

const inquirer= require('inquirer')

const ejs =require('ejs')

inquirer.prompt([

{

type:'input',

name:'name',

message:'project name'

}

]).then(answers=>{

// 根据用户回答生成文件

// 模板目录

const tmplDir=path.join(__dirname,'templates')

// 目标目录 命令行在哪个目录去执行就是哪个路径

//process.cwd() 方法会返回 Node.js 进程的当前工作目录。

const destDir=process.cwd()

// 如果实在当前项目运行npm init,生成的项目结构的路径

// const destDir=${process.cwd()}/newProject

// 将模板下的文件全部转换到目标目录 files相对路径

fs.readdir(tmplDir,(err,files)=>{

if(err) throw err

files.forEach(file=>{

// 通过模板引擎渲染文件 renderFile(文件绝对路径,文件数据上下文,渲染成功后的函数)

ejs.renderFile(path.join(tmplDir,file),answers,(err,result)=>{

if(err) throw err

// 将结果写入目标文件路径

fs.writeFileSync(path.join(destDir,file),result)

})

})

})

})
  1. 创建模板,这里主要是关注脚手架的工作过程,不需要太过关系模板里面有什么,创建一个简单的html和css文件

  2. 执行脚本,生成项目结构

  • 在新文件中
1
2
3
4
5
$ sample-scaffolding

? project name new-project # 项目名称

? project name new-project

  • 在当前项目中,生成的文件会在newProject文件中
1
2
3
4
5
$  npm run init

? project name new-project # 项目名称

? project name new-project

参考

浅谈前端工程化

前端工程化之脚手架

plop

yeoman/generator-node