跳到主要内容

Web 前端架构设计

· 阅读需 72 分钟
不如怀念
Web 前端工程师 (Web Front-end Engineer)

最后更新于 2018-07-01 01:29:00

让 Web 前端开发可持续化、可扩展,关注四个核心代码流程测试文档

代码

HTML

模块化标记

构建模块化标记原则:标签表达结构类名控制外观。这样做的好处就是,对相同类型结构的模块可以复用标签模版,同时又可以通过改变类名来灵活的控制模块的外观。例如:

```html
<section class="theme-container-card">
<nav class="theme-nav-block-items"></nav>
<header class="theme-title-xxx">
<h2></h2>
</header>
<main class="theme-content-xxx">
<p></p>
<div></div>
</main>
<footer class="theme-endnote-fluid"></footer>
</section>
```

在这里,类名其实对应的是不同的主题样式。

构建一个完整的页面,应该将其分解为一些更细小的可复用的单元,也就是组件模块。

编码规范

文档类型

HTML5 的文档类型申明:<!DOCTYPE html>

HTML 验证

规范化的 HTML 是显现技术要求与局限的显著质量基线,它促进了 HTML 被更好地运用。

推荐:

```html
<!DOCTYPE html>
<meta charset="utf-8">
<title>Test</title>
<article>This is only a test.</article>
```

不推荐:

```html
<title>Test</title>
<article>This is only a test.</article>
```
省略可选标签

HTML5 规范中规定了 HTML 等标签是可以省略的。但从可读性来说,在开发的源文件中不要这样做,因为省略标签可能会导致一些问题。

资源加载

CSS 资源(<link>)在 <head> 标签中引入,避免 DOM 加载完后重复渲染;JS 资源(<script>)在文档尾部 </body> 闭合标签前引入,避免过早的加载 JS 阻塞 DOM 渲染。例如:

```html
<head>
...
<link rel="stylesheet" href="base.css">
</head>
<body>
...
<script src="common.js"></script>
</body>
```

慎用 <script> 标签的 asyncdefer 属性。

语义化

使用 HTML 5 新标签,构建语义化标签模块,有利于理解和提高效率。

推荐:

```html
<section>
<nav></nav>
<header></header>
<main></main>
<footer></footer>
</section>
```

不推荐:

```html
<div class="section">
<div class="nav"></div>
<div class="header"></div>
<div class="main"></div>
<div class="footer"></div>
</div>
```
多媒体回溯

对页面上的媒体而言,像图片、视频、canvas 动画等,要确保其有可替代的接入接口。图片文件我们可采用有意义的备选文本(alt),视频和音频文件我们可以为其加上说明文字或字幕。

推荐:

```html
<img src="imgs/banner.png" alt="Prairie and Horse">
```

不推荐:

```html
<img src="imgs/banner.png">

<img src="imgs/banner.png" alt="Banner image one">
```

这些替代文字应该描述媒体资源的内容,而不是这些媒体资源的作用、类型等。

关注点分离

严格地保证结构(HTML)表现(CSS)、**行为(JS)**三者分离,并尽量使三者之间没有太多的交互和联系。遵循:

  • 不要引入太多零散的样式表,合并成大文件。
  • 不要使用内联样式(<style> … </style>)、和行内样式。
  • 不要引入太多零散的脚本文件,合并成大文件。
  • 不要使用内联脚本(<script> … </script>)。
  • 不要使用表象元素(例如 <b><u><font> 等)。
  • 不要使用表象类名(例如 center、red、left)。

这样做的好处是,代码干净整洁,利于维护。

内容至上

不要让非内容信息污染了你的 HTML。遵循:

  • 不要引入一些特定的 HTML 结构来解决一些视觉设计问题。
  • 不要将 <img> 元素当做专门用来做视觉设计的元素。

这些是什么意思呢?HTML 结构应该表达的是文档内容,而非设计要素。例如,列表 元素 <li> 前面的原点、空心圆等修饰性的东西不应该用额外的标签去实现,可以借助伪元素实现。同样地,<img> 引入的图片应该是内容相关的,而非修饰性东西。

Tab Index 在可用性上的运用

依据元素的重要性来重新排列其 tab 切换顺序。你可以设置 tabindex="-1" 在任何元素上来禁用其 tab 切换。

当你在一个默认不可聚焦的元素上增加了功能,你应该总是为其加上 tabindex 属性使其变为可聚焦状态,而且这也会激活其 CSS 的伪类 :focus。选择合适的 tabindex 值,或是直接使用 tabindex="0" 将元素们组织成同一 tab 顺序水平,并强制干预其自然阅读顺序。

ID 和锚点

通常一个比较好的做法是将页面内所有的标题元素(h2h3)都加上 ID。这样做,页面 URL 的 hash 中带上对应的 ID 名称,即形成描点,方便跳转至对应元素所处位置。

格式化

块级元素应独占一行,内联元素放在同一行,子元素缩进使用制表符。

推荐:

```html
<nav>
<ul>
<li><span>Item</span> one</li>
</ul>
</nav>
```

不推荐:

```html
<nav><ul><li><span>Item</span> one</li></ul></nav>
```
引号

HTML 标签属性值应该用双引号,而不是单引号。

推荐:

```html
<div class="container"></div>
```

不推荐:

```html
<div class='container'></div>
```

注释

在 HTML 页面进行必要的注释是应该的,尤其是 SPA 单页面应用,标明不同的模块位置,便于维护和扩展。

```html
<body>
<header>
<h1>Single Page Web Application</h1>
</header>
<!-- container 容器 -->
<main id="content">
<!-- Module-1 -->
<section>
...
</section>
<!-- Module-2 -->
<section>
...
</section>
</main>
<footer>
<p>CopyRight 2018</p>
</footer>
</body>
```

CSS(Sass)

模块化 CSS

构建模块化的 CSS 有多种方法,这里推荐三种:

OOCSS 方法

Object-Oriented CSS,即面向对象的 CSS,主要有两个原则:分离结构和外观,分离容器和内容。

分离结构和外观,意味着将视觉特性定义为可复用的单元,最简单的例子就是以主题形式定义 CSS。

分离容器和内容,指的是不再将元素位置作为样式的限定词,定义可复用的 CSS 类名,无关于标签内容位置。

例如:

```html
<div class="toggle simple">
<div class="toggle-control open">
<h1 class="toggle-title">Title</h1>
</div>
<div class="toggle-details open"></div>
...
</div>
```
SMACSS 方法

Scalable and Modular Architecture for CSS,即模块化架构的可扩展 CSS,它将样式系统划分为五个类别:

  • 基础

    如果不添加 CSS 类名,标记会以什么外观呈现。

  • 布局

    把页面分成一些区域。

  • 模块

    设计中的模块化、可复用的单元。

  • 状态

    描述在特定的状态或情况下,模块或布局的显示方式。

  • 主题

    一个可选的视觉外观层,可以让你更换不同主题。

OOCSS 与 SMACSS 有许多相似之处,它们都把样式的作用域限定到根节点的 CSS 类名上,然后通过皮肤(OOCSS)与子模块(SMACSS)进行修改,后者使用了 is 前缀的状态类名。例如:

```html
<div class="toggle toggle-simple">
<div class="toggle-control is-active">
<h2 class="toggle-title">Title</h2>
</div>
<div class="toggle-details is-active">
...
</div>
...
</div>
```
BEM 方法

Block Element Modifier,即块元素修饰符,只是一个 CSS 类命名的规则,建议每个元素都添加带有如下内容的 CSS 类名:

  • 块名

    所属组件的名称。

  • 元素

    元素在块里面的名称。

  • 修饰符

    任何与块或者元素相关联的修饰符。

例如:

```html
<div class="toggle toggle--simple">
<div class="toggle__control toggle__control--active">
<h2 class="toggle__title">Title</h2>
</div>
<div class="toggle__details toggle__details--active">
...
</div>
...
</div>
```

以上三种方法各有优势,提供给了我们构建模块化 CSS 的方式,也是三种思维方式,在实际开发过程中可以借鉴。

编码规范

ID and class 命名

命名应该遵循语义化原则,表达其具体的用途和含义,这样做的好处是更容易理解,同时发生变化的可能性也很小。同时,单词的分隔符统一使用中划线 “-”

推荐:

```css
.bg-important {
background-color: red;
}
```

不推荐:

```css
.bg-red {
background-color: red;
}
```

命名不要出现表象词,比如颜色等,同时表达的含义应具体而不是通用化。

避免使用 ID

通常,在样式文件中不应该出现 ID,所有的样式均应该由 class 来定义,因为 ID 会导致样式不可重用的后果。

避免使用标签名

在选择器中不应该出现标签名,这样做的好处是可以提高样式的复用性。

推荐:

```css
.container > .content > .title {
font-size: 2em;
}
```

不推荐:

```css
div.container > main.content > h2.title {
font-size: 2em;
}
```

选择器中出现标签名的话,会将外观(CSS)与结构(HTML)绑定在一起,不利于重用。

精确匹配

在使用选择器时应该尽可能的精确匹配到目标元素,这样发生问题时更容易找到问题也有利于性能优化。

推荐:

```css
.content > .title {
font-weight: bold;
}
```

不推荐:

```css
.content .title {
font-weight: bold;
}
```

如果匹配的是直接子代元素,就使用直接子代选择器,这样性能更好,也不容易影响非直接子代元素的后代元素样式。

缩写属性

部分的 CSS 属性值是可以进行缩写的,这样编码效率也会提高,但缩写属性也应该慎用,因为缩写属性牵扯到顺序问题,像 fontbackground 这些顺序难记的属性不应该使用缩写,而像 paddingmargin 这些常用并且顺序好记的属性应该使用缩写。

推荐:

```css
.content {
background: #000;
background-image: url("./imgs/bg.png");
padding: 0 20px 0 10px;
}
```

不推荐:

```css
.content {
background: #000;
background-image: url("./imgs/bg.png");
padding-left: 10px;
padding-right: 20px;
}
```
0 与 单位

如果属性值为 0,不在使用单位。

推荐:

```css
{ padding: 2px 0; }
```

不推荐:

```css
{ padding: 2px 0px; }
```
十六进制表示法

当使用十六进制表示颜色值时,尽可能用更简短的方式,例如使用 3 位。同时,使用小写表示,不要使用大写

推荐:

```css
{ color: #d8a; }
```

不推荐:

```css
{ color: #DD88AA; }
```
声明顺序

采用统一的属性声明顺序,可以提高可读性。通常,应遵循以下顺序(依次从上至下):

  • 结构性属性:
    1. display;
    2. positionlefttopz-index 等;
    3. overflowfloat 等;
    4. widthheight;
    5. marginpadding
  • 表现性属性:
    1. colortext;
    2. font;
    3. backgroundborder 等。
分号 与 空格

CSS 属性值后必须用分号结束,每条属性声明都应该使用新的一行,并且在冒号与属性值中间空出一个空格,提高可读性。

推荐:

```css
.content {
width: 200px;
margin-bottom: 10px;
}
```

不推荐:

```css
.content {
width:200px; margin-bottom:10px
}
```
规格分隔

每个规则之间使用一行进行分割,每个选择器应该使用新的一行。

推荐:

```css
.container {
padding: 10px 20px;
}

.content,
.item:hover {
color: orange;
}
```

不推荐:

```css
.container {
padding: 10px 20px;
}
.content, .item:hover {
color: orange;
}
```
引号

属性值中的引号应该使用双引号,而不是单引号。

推荐:

```css
{ background-image: url("./imgs/bg.png"); }
```

不推荐:

```css
{ background-image: url('./imgs/bg.png'); }
```
使用 Scss 语法

SCSS 是 Sass 3 引入的新语法,其语法完全兼容 CSS 3,并且继承了 Sass 的强大功能。Scss 语法相较于 Sass 语法更接近 CSS 语法,所以统一使用 Scss 语法

选择器嵌套

使用 Sass 预处理器后,使得我们可以进行选择器嵌套,大幅度提高了编码效率,也使 CSS 代码变得更为简洁,结构更为清晰。

推荐:

```css
.container {
padding: 10px 20px;

& > .content {
border: 1px solid black;
}
}
```

不推荐:

```css
.container {
padding: 10px 20px;
}

.container > .content {
border: 1px solid black;
}
```
选择器嵌套顺序

属性声明遵循一定顺序,同样地,选择器的嵌套也应该遵循一定的顺序以提高可读性。 通常,应该遵循以下顺序(依次从上至下):

  1. 当前选择器的样式属性;
  2. 父级选择器的伪类选择器 (:first-letter、:hover、:active 等);
  3. 伪类元素 (:before and :after);
  4. 父级选择器的声明样式 (.selected、.active、.enlarged 等);
  5. 用 Sass 的上下文媒体查询;
  6. 子选择器作为最后的部分。

推荐:

```css
.item {
// 1. 当前选择器样式属性
font-size: .8em;

// 2. 父级选择器的伪类
&:hover {
color: orange;
}

// 3. ::before && ::after
&::before {
content: attr("tip");
display: block;
}

// 4. 父级选择器声明样式
&.selected {
background-color: red;
}

// 5. 上下文媒体查询
@media screen and (min-width: 768px) {
font-size: 1.2em;
}

// 6. 子类选择器
& > .text {
font-weight: 400;
}
}
```

注释

在使用 Sass 写 CSS 时有很大的灵活性来组织代码结构,但注释也是很必要的。

文档注释

通常写在文件的开始部分,涉及文档的概述以及版本号,及其依赖等。

```typescript
/*!
normalize.css v8.0.0 | MIT License |github.com/necolas/normalize.css
*/
```
模块注释

模块的注释使用多行注释,标明该模块、代码块的作用等。

```typescript
/**
* Remove the margin in all browsers.
*/
```
普通注释

对于一些比较关键的代码,要进行注释,写在单行即可。

```typescript
/* menu-1 */
```

JavaScript

前后端分离后,前端需要写更多的业务逻辑代码,不再是单纯写 HTML 与 CSS 了,很多需求的实现都依靠于 JavaScript。

基于 JS 的 Web 应用

创建可扩展且可持续的设计系统,并维护一套高质量的代码。

选择框架

没有哪个 JavaScript 框架是完美的,任何一个框架都是基于 JavaScript 来实现的,框架提供给我们的是一种设计模式和更优的实现方式。

选择哪个框架,首先我们要考虑业务需求是否复杂,大多时候使用框架反而会增加代码量和无畏的复杂逻辑。其实,很多时候我们是用不到框架的,我们更多时候用到的是一些例如 Jquery 库、Bootstrap CSS 库这些工具,当现有的业务手动实现遇到技术瓶颈时,我们才应该去考虑使用一些开源的框架和工具。

永远保证采用最精简的方案做项目,而不是一开始就准备一大套工具和大规模的启动页,这对我们没有任何好处。

维护整洁的代码

通常,开发一个项目,尤其是多人协作的过程中,应该遵循一定的“JavaScript”开发编码规范,这样整体代码具有相同的风格,利于后期维护,即便是再简单的项目也应该如此。

  • 保持代码的整洁性

    JavaScript 是一种脚本语言,而且语法相对松散,编写恰当的 JavaScript 代码非常关键,最好在项目中结合单元测试使用一些格式/错误提示,而且能帮助团队编写符合规范的代码。其实,JS Hint 是这些工具中一个很好的例子。

  • 创造可复用的函数

    编程语言最大的特点就是可复用性,在开发过程中应尽可能的将有类似行为的过程操作抽象出来,形成一个可复用的函数,这样做可以大幅减少整体代码量,也能够更好的组织代码结构。

编码规范

缩进

保持良好的代码缩进习惯,缩进统一使用**“制表符”**。

IIFE

将代码包裹在一个 IIFE(Immediately-Invoked Function Expression,立即执行函数)中,创建独立的作用域,保证每一个人的代码不会污染全局作用域。

推荐:

```typescript
(function(window) {
...
})(window);
```
严格模式

使用严格模式保证 JavaScript 代码的健壮性,严格模式可以作用于整个脚本或者一段代码块中,为了尽可能的不引起冲突,请在代码块中使用严格模式。

推荐:

```typescript
(function(window) {
'use strict';
...
})(window);
```
变量声明

推荐:

```typescript
var a = 1,
b = 2;
```

不推荐:

```typescript
var a = 1;
var b = 2;
```
== 与 ===

比较时如果使用 == 的话,会忽略掉比较对象的类型,应该总是使用 === 进行精确的类型与值比较。

推荐:

```typescript
0 === '' // false
```

不推荐:

```typescript
0 == '' // true
```
声明提升

JavaScript 中有声明提升的机制,因为不存在块作用域,在同一个作用域中不同代码块中声明的变量与函数最终都会被提升到作用域顶层声明。

```typescript
var a;

a = 1;
if (a) {
var b = 1;
}
```

等同于:

```typescript
var a,
b;

a = 1;
if (a) {
b = 1;
}
```

因为存在声明提升的机制,所以在编码时应该将变量与函数声明写到作用域顶层

条件运算符

在逻辑比较简单时,应该使用条件运算符而不是 if…else 语句。

推荐:

```typescript
a === 1 ? console.log('true') : console.log('false');
```

不推荐:

```typescript
if (a === 1) {
console.log('true');
} else {
console.log('false');
}
```
函数声明

函数的声明应该在作用域的顶层,而不是某个语句块中,因为 JavaScript 没有块作用域的概念,并且由于声明提升的原因,最好将函数声明写在作用域的顶层。

推荐:

```typescript
function foo() {}

if(b){
var bar = function() {};
...
}
```

不推荐:

```typescript
if(b){
function foo() {}
function bar() {}
...
}
```
闭包

JavaScript 代码中大量的使用了闭包机制,这是一个很好的机制,但是应该时刻注意闭包所带来的内存泄漏的问题。应该在使用完变量后,尽可能将不再需要使用但存在内存泄漏隐患的变量手动释放。

eval() 函数

eval() 函数可以将字符串编译为 JavaScript 代码然后执行,但不应该使用它,一方面是效率很低,另一方面则是涉及到安全问题。

this 关键字

由于 JavaScript 的词法作用域机制,以及代码中大多时候都会存在多层嵌套的函数,this 关键字的指向很容易被搞错,如果要在内层函数使用外层函数的 this 引用对象,应该使用一个变量在外层函数作用域内将其缓存起来,然后使用该变量在内层函数中进行引用。

数组初始化

数组的初始化应该使用字面量而不是构造函数,构造函数的参数容易引起误会。

推荐:

```typescript
var a = [3]; // [3]
```

不推荐:

```typescript
var a = new Array(3); // [undefined, undefined, undefined]
```
引号

JavaScript 中引号统一使用**“单引号”**,因为这在书写 HTML 字符串模版属性时将非常有用。

toString()

可以自定义 toString() 函数来控制对象的字符串化,但要保证该方法始终能够正确执行。

注释

JavaScript 是一个灵活性很大的语言,那么就会带来代码维护和阅读上的困难,良好的注释会帮助我们减轻这些负担。

文档注释

通常写在文件的开始部分,涉及文档的概述以及版本号,及其依赖等。

```typescript
/*!
jQuery v1.11.3 |
(c) 2005, 2015 jQuery Foundation, Inc. |
jquery.org/license
*/
```
方法注释

对于模块以及方法尤其要写明注释,这对于如何理解你的方法是至关重要的,应该遵循:为什么要写这个方法或者模块,解决了什么问题,而不是这个方法是用来干什么。

```typescript
/**
* ...
*
* @description "..."
* @author mrwang
* @param {Jquery Object} $all_a
* @returns
*/
```
普通注释

普通注释可分为多行注释和单行注释,在必要的地方进行注释即可。

```typescript
单行:
// somethings

多行:
/*
somethings
*/
```

流程

流程的核心是工作流。工作流指的就是把想法需求变成现实的过程,从产品的角度来看,就是修复 bug、迭代升级的一系列流程和方法。

过去的工作流

在过去,Web 前端开发还是基于 PSD 文档编写标签和一堆页面的时代,但这个时代从几年前就已经结束了。前端开发不再只是单纯的为了做出好看漂亮的页面,而是更关注高效率开发、构建高性能应用以及快速迭代。

过去的工作流:
需求 -> 线框图 -> 开发(设计)并行 -> 前端

现代的工作流

过去的工作流根据角色逐级交付,前端开发通常是在项目最后阶段才参与的,这样的流程效率很低,而且前端的参与度太低,产品最终质量无法保证。现代的工作流则是相反地,前端将参与整个项目阶段,更大程度地保证产品质量和后期迭代速度。

现代的工作流:
需求 -> 原型 -> 开发

需求

工作流一般从收集需求开始,现代的工作流在这个阶段,将会改变需求所面向的人群,会让交互设计、视觉设计、后端开发以及前端开发人员共同参与。这样一个来自交叉领域的团队,意味着我们将注重创建一个完整的解决方案,而不是一个大概的线框图了。来自不同领域的人员共同参与需求收集的过程,能尽早的发现需求中存在的问题和不足。

原型设计

以往的工作流偏向于在每个环节交付一个成品,而现代的工作流更注重在用户交互模型、视觉设计和前端解决方案中的持续迭代

原型设计则给我们提供了讨论和反馈的公共空间,在把我们丰满的想法通过在桌面和移动端浏览器中实现之后,我们可以基于原型进行讨论、修改、增删,直到开发人员和产品负责人对原型满意,就可以进入下一步开发环节了。

相对来说,原型设计阶段实现起来成本要低得多而且更为灵活,要确保在这个阶段将产品原型确定,然后在开发环节将会节省不少时间成本。

程序开发

实际上,在通过原型设计阶段之后,开发环节只需要关注数据处理和业务逻辑的实现即可。优秀的原型设计基本上可以直接拿来在开发环节中使用,不需要太多额外的修改,而且这对于测试人员来说也更为方便。

前端工作流

前端开发是一个零散化的过程,没有专业的 IDE 工具为我们处理繁多复杂的任务,一个流畅、高效率的前端工作流显得格外重要。

开发工具

我们要安装很多必要的工具来搭建一个适合我们的开发环节和软件运行环境,包括代码编辑器、常用浏览器、版本控制工具等等。这个过程要尽可能的流畅,这样开发人员才能更快的进入实际编码工作中。

本地部署

进入到实际编码工作过程中,首先就是要将项目源码使用版本控制工具从版本库中下载下来,然后部署在本地,成功运行后才可以开始编码。这个过程实际上不复杂,例如,如果采用了前后端完全分离的开发/部署方案,我们可能会在前端使用一个 nginx 服务器作为代理服务器,这反而是增加了本地部署的复杂度。所以,在这个过程中,涉及到的流程中的细节必须完整的写在README.md文件中,帮助任何一个开发人员都能流畅、快速地搭建好本地环境,成功部署应用。

开发

在开发过程中,如果需求发生了微小的变动,我们应该尽可能对系统做最小的改动来实现这个需求。通常好的做法是,加入新的实现方案,去覆盖掉原有的实现方案;而比较糟糕的做法是,在原有的实现方案上进行更改。

发布

项目源码通常使用版本控制工具来进行管理,而编译之后的生产环境代码如何发布也是一个值得关注的问题。

提交编译之后的代码

在使用的大多数 github 开源项目中,我们会发现通常会有一个dist目录,而这个目录中其实就是编译之后的代码。这样做的好处是,其它人可以很方便的将这些编译之后的代码复制到本地成功运行,而不需要经过漫长的搭建编译工具的过程。

然而,这么做也有不好的地方,其中比较重要的就是合并代码时的冲突问题,当然最简单的解决方法就是将整个项目重新编译一遍进行提交,但这也意味着不同分支将不会有合并请求。

持续集成的服务器

使用类似 Jenkins 或 Travis CI 的服务可以避免出现以上问题,它们可以在我们将代码发布到服务器之前,先对代码做一些处理。这意味着我们可以在版本库中忽略编译后的资源文件,CI 服务器会自动执行我们的编译任务,然后将代码发布到服务器。

这样做的好处不仅可以保持代码库的整洁,也不会出现提交编译后代码合并冲突的情况。

标签分支

Git 有个强大的功能就是创建标签分支,我们可以在任何分支上创建便签,便于我们进行选择性的发布。

例如,有时候我们创建了一个分支版本,并添加了一些功能或者修改了一些 bug,但我们并不希望将这些改动合并到主干上去,这个时候则可以在这个分支上创建标签,并进行发布即可。

发布渠道

如果我们的项目被其他人的项目广泛地引用,发布渠道则是比较重要的。这些渠道有很多,下面列举一些常见的包管理器:

  • NPM(Node Package Manager)
  • Bower
  • Ruby Gems
  • RPM
  • Sublime Text Package Control

使用这些包管理器的好处如下。

  • 发布不同的版本

    用户可以选择性的使用某一版本,而不是跟随开发者升级。

  • 版本更新通知

    良好的通知机制和内部升级系统,可以让用户很方便的获知新版本的发布信息。

  • 从私有库中发布代码

    更多的时候我们的项目源码是维护在私有库中的,包管理器允许我们将代码发布到公共空间,让更多的普通用户来使用。

任务处理器

对于前端开发者来说,每次修改文件都要手动刷新浏览器,验证改动效果;在生产环境中部署时,要手动使用工具压缩代码和图片,减小文件体积;没办法很好的利用语言的新特性(ES6、ES7 等)来提高编码效率等等,这些场景下的任务实际上占用了开发者大量的编码时间,去做一些与编码无关的事情,但这些事情又能很好的优化我们的应用性能或者开发工作流。

于是出现了一些任务流管理工具,例如 gulp、grunt、webpack 等等,这些任务处理器工具 实现了一些功能:

  • 清理文件夹
  • 编译 Sass
  • 编译 ES6、ES7 代码
  • 合并文件
  • 文件压缩
  • 自动生成浏览器厂商的 CSS 属性前缀
  • 监听文件改动自动刷新浏览器
  • 启动静态的 Node 服务器

这些任务处理器提供的功能远远不止这些,但这些都是比较常用的功能,能很好的优化我们的开发工作流。将与编码无关的事情交给任务处理器自动化处理,然后开发者专注于编码,实现业务即可。

无论选择哪一种工具,gulp 还是 grunt,实际上它们每个都能替代对方,实现所有功能,只不过配置的代码风格不同,以及优势不同。我个人比较推荐的是 gulp + webpack 相互配合来构建一个自动化的任务流,gulp 负责编译 Sass、压缩图片等任务,而 webpack 负责打包 JavaScript 模块代码,编译 JS 文件等任务。

测试

在一个大型项目,尤其是多人参与的前端项目中,每一次提交、合并都很有可能会影响到原有的系统功能,团队开发者并不会太多地去关注这些问题,而作为一个架构师或者负责人,对于这种问题应该重视,而解决方案就是:测试。一个人并没有太多的精力和时间去评估每一段代码对原有系统所造成的影响,但可以借助自动化的测试工具来验证我们的应用程序是否能够正常的运行。

在规划测试过程中,有以下几点应该尽可能的去遵守:

  • 测试用例应该在建站的同时,甚至是在建站之前就开始编写。
  • 测试代码是可运行的真实代码,应该一起提交到系统代码库中。
  • 必须在所有的测试用例都通过之后,才能把代码合并到主干中。
  • 在主干上运行测试工具,结果应该都为通过。

因此,与其将时间花费在评估每一段代码上,不如去关注如何构建高质量的系统和完整的测试。

单元测试

单元测试是最普遍、最常见的软件测试方法,是将应用程序分解为尽可能小的函数,并创建可重复的、自动化的测试用例的过程。在条件不变的情况下,单元测试应该总是产生相同的结果,它为今后所有应用程序的代码提供构建的基础。

如果没有单元测试,不常用的函数可能长达数月都不会被发现有 bug;相反,通过使用单元测试,我们可以在任何代码合并到主干之前就验证每个系统函数的功能,不会等到代码实际应用的产品中时还会出现问题。

无论是前端还是后端语言,都有一套成熟完整的单元测试框架,例如 Java 的 JUnit,PHP 的 PHPUnit,Node 的 NodeUnit 以及 JavaScript 的 QUnit。

单元

**“一次只做一件事,并把它做好”是构建基于单元测试的应用程序的原则。**开发者在写函数时,应该尽可能的抽象、分解成更小的函数单元,如果在一个函数中融入太多的业务逻辑以实现更多的功能,这样不仅开发效率降低,而且增加了测试和维护的难度,因为这样的函数无法复用。

这有一个例子:通过客户地址,计算出将产品从最近的分拨中心运输给客户的运费。

如何编写函数来实现这个功能,并将其分解为更小的单元,一般可以将其分解为以下三步,也就是三个函数单元:

  1. 根据地址找到最近的分拨中心;
  2. 计算两个地址之间的距离;
  3. 根据距离计算运费。

这样的话,编写三个函数要比编写一个函数来实现这个功能好得多,因为 2、3 步的函数复用的概率是相当大的,这样也符合“一次只做一件事,并把它做好”的理念。

更好的测试

**在测试过程中,我们可以测试每个独立且可重用的函数,而不是测试应用程序所能计算的每一条运输线路。**编写单元测试在开发的前期可能显得工作量变大了,但这为避免以后产品上线出现大量 bug 来说是值得的;而且越到后期,开发新功能所需要的新函数会越来越少,有大量可复用的函数提供给我们来实现新的、高复杂度的功能。

测试驱动的开发(TDD)

通常来说,我们的思路应该是先编写业务代码,再去编写测试代码。但测试驱动的开发(test-driven development,TDD)则颠倒了这一思路,它将单元测试放在第一位,之后才是编写业务代码。

但如果为还没有创建的函数编写测试用例,岂不是肯定无法通过测试?实际上,**测试驱动的开发的目标是,通过测试用例来描述一个正确编写的系统应如何工作,并为实现这个系统来铺平道路。**所以,这样反而会提高编写业务代码的效率。

如何进行单元测试

我们可以使用 QUnit 来为 JavaScript 进行单元测试。单元测试的核心理念非常简单,它的基本思路就是调用要测试的函数,传递一些预设的参数,并描述结果应该是什么。

根据单元测试工具反馈的结果,我们可以及时修复应用程序中出现的 bug,并能以比较细小的粒度获取到出现 bug 的精确位置。

测试覆盖率

一个产品的开发过程中,实际上很难做到 100% 的测试覆盖率(大多数的产品都不是基于 TDD 的开发模式),在这种情况下做到多少测试覆盖率才合适也是很难把握的一件事。如果要测试所有的代码,很可能将会导致开发进度停滞不前;但同样地,测试覆盖率不够,将会遗漏很多关键性问题。

解决分歧点

为已有的项目设计单元测试,大部分情况下,你没有充足的时间为先有的功能编写 100% 覆盖率的测试集。但测试覆盖率的好处是,即使一个单一的测试也能够为系统建设贡献价值。因此,在决定从哪开始编写单元测试时,可以从能够获得最大收益的地方开始。有时候,最大的收益就是为系统最简单的部分编写单元测试。

一旦有了能提供基本覆盖率的测试集,就可以寻找系统中最关键的部分,或者过去频繁出问题的部分,在需求列表中为它们分别创建需求,并确保尽快推动这些需求。

从测试覆盖率开始

**如果能在新项目的启动阶段就开始规划单元测试工作,除了设置好测试框架之外,更重要的是要确保开发流程本身为单元测试做好了准备。**就像写文档和代码审核一样,写单元测试也要花费不少时间,你需要确保任何需要测试的需求都有额外的时间来编写单元测试,并且确认所需的测试覆盖率。

在开发每个系统功能的过程中,应该至少留出 1/3 到 1/4 的时间来编写测试用例,剩下的时间则用来实现业务代码。所以作为一个前端架构师应该争取更多的时间,虽然会花费多一点的时间,但是这其实会节省很多后续回头追查 bug 的时间。

并不是所有的功能都需要同样的测试覆盖率,但所有的需求都是以测试覆盖率的相关任务开始的,只有当所有人都认为给这些任务写测试用例没有必要时,才考虑去掉它。这样我们才能确信,对于任何需要测试的功能,都已经安排了足够的时间去完成它们。

性能测试

任何测试都是为了避免不流畅的用户体验,而网站是严重依赖于网络的,鉴于网络情况浮动较大,糟糕的网站性能正是导致用户体验不流畅的主要原因之一。性能测试虽然不是针对系统或视觉问题的测试,却也是测试库的重要组成部分。

性能测试衡量的是影响用户使用网站的流程程度的关键指标,包括页面大小、请求数量、首字节时间(time of first bite,TTFB)、加载时间和滚动性能等等。

性能测试的关键是制定合适的性能预算并坚持下去。

制定性能预算

制定性能预算是指为每个关键指标设定目标值,然后在所有代码合并或部署之前持续测试这些指标。若有任何一个指标没通过测试,则需要调整新增的功能,或删除一些其它功能。

作为一个前端开发者,很多人习惯了使用例如 JQuery、BootStarp、Angular 等库和框架,一旦离开这些工具,就觉得开发工作无法进行下去。但这些工具通常非常耗费流量资源,会显著的增加应用程序大小,实际上我们用到这些工具提供的功能可能连三分之一都不到。所以,用最简洁的方案实现我们的需求是首要选择。

竞争基线

制定性能预算的一种方法是参考竞争对手。虽然“至少我比某某更好”不能作为网站性能不佳的借口,但是这种方法可以保证你有一定的竞争优势。

通过对竞争对手的网站性能进行分析,你的目标不是要达到竞争对手的水平,而是要确保领先竞争对手至少 20% 甚至更多。这 20% 的优势,是用户将你和竞争对手区分开来所需要的。

优化关键指标不能一劳永逸,它需要的是持续监控。可以确定的是,在你进行不断的优化过程中,你的竞争对手也不会坐以待毙,他们也在寻找更优的方法来改善他们自己的网站。

平均基准

不管你的竞争对手是谁,把你的网站性能基线与行业平均水准和通用的最佳实例相比较总是必不可少的。我们没有理由因为竞争对手的落后而保持平庸。

HTTPArchive 是个不错的服务,它测试并记录了几十万个网站的各种性能指标。

原始指标

网站性能最基本的测试是看渲染页面所需要的资源,包括这些资源的大小和总数。

页面大小

随着网络的发展,用户需求的提升,网站页面正在变得越来越大。虽然网站大小并非影响网站加载速度的唯一因素,但它确实对此影响重大。而且,在现在移动优先的时代,越来越多的用户通过移动设备来访问我们的网站,而他们要为数据流量付费,页面越大则意味着用户要花更多的钱。

我们一些显而易见的地方缩减页面的大小:

  • 图片

    • 优化 PNG 图片,降低 JPEG 图片的质量。
    • 利用新的响应式的 <picture> 标签和 srcset 属性来下载大小合适的图片。
    • 制定一个预算,如果没有移除任何图片,就不增加图片的大小。
  • 自定义字体

    • 制定一个字体预算,不考虑增加第二种或第三种字体。
    • 考虑必要的字体粗细,因为每增加一种粗细变化,都会使字体文件增加几千个字节。
    • 虽然字体图标不错,但要注意文件大小,尽可能只引入需要的字体文件,不要将全部的字体文件引入。
  • JavaScript 库和框架

    • 针对现代浏览器,尽可能的不要使用 JQuery,因为它的文件非常的大。
    • 能用 CSS 实现的一些效果,不要引入其它的 JS 插件来实现。
    • 像 Angular 这样大型的框架,要慎重考虑,如果可以使用更轻的框架例如 React 来实现你的需求,则要进行替换。
    • CSS 框架很多,但实际上框架提供的样式我们能使用到的并不多,而且基于已有的 CSS 样式表来编写我们自己的样式很可能会陷入困境。
  • 使用压缩

    • 对文件和图片进行压缩,在服务器上开启 gzip 压缩,这些都是缩减页面大小的关键步骤。
HTTP 请求次数

浏览器对页面渲染的所需的每个文件都要进行 HTTP 请求。**因为每个浏览器对 HTTP 请求的次数有但域名限制,所以大量单独的文件意味着浏览器必须进行多轮并发请求。**在速度较慢的网络环境中,这么多并发请求会造成很复杂的影响。因此,减少获取所需文件的并发请求次数,效果会更显著。

可以通过以下方法减少并发请求次数:

  • 减少 HTTP 请求次数

    • 将多个单独的 CSS、JavaScript 文件合并成一个文件。
    • 把多个单独的图片文件合并成一个图片。
    • 延迟加载最初不需要加载的资源文件。
  • 增加浏览器每次并发请求的资源个数

    • 分拆静态资源到不同的服务器(CDN),可以使得浏览器单次并发下载更多的资源,因为浏览器的并发请求数量限制是针对单个服务器的。

计时度量

除了站点的资源数量和大小,还有其他的计时度量会影响用户对网站性能的体验。

  • 首字节时间

    首字节时间是指从浏览器请求网站页面开始,到浏览器接收到第一个字节之间的毫秒数。这个数值用来测量浏览器和服务器之间的连通路径,包括 DNS 查询、初始连接和数据接收。它并不是判断站点性能的最佳标准,却是一个值得关注的指标。

  • 开始渲染时间

    更有价值的计时度量是“开始渲染时间”。这个度量是指用户开始在页面上看到内容的时间。这意味着所有阻塞渲染的文件都已经加载完成,浏览器已经开始渲染文档模型了。可以通过以下方式优化开始渲染时间:延迟加载阻塞渲染的 JavaScript 和 CSS 文件、将关键的 CSS 代码内联到页面头部、用数据 URI 代替图片资源,以及延迟加载所有在文档模型渲染完成后才下载的资源。

  • 文档完成时间

    只要最初请求的资源已经加载成功,就可以认为文档“完成”了。文档完成时间不包括 JavaScript 中拉取资源消耗的时间,因此延迟加载的资源不会影响到这个指标。

混合度量标准

混合度量标准不是度量离散的值,而是根据多个性能指标综合打分得出。

PageSpeed 分数

PageSpeed 是 Google 开发的网站工具和 Chrome 浏览器的扩展程序,用来分析站点的性能和网站的可用性,它给出一个用百分比表示的分数,并解释了提高分数的方法。测试包括:

  • 是都存在阻塞渲染的 JavaScript 或者 CSS
  • 重定向至登录页
  • 图片优化
  • 文件压缩
  • 服务器响应时间
  • 服务器端压缩
  • 服务器端缓存
  • 点击目标的大小
  • 窗口可见区域的配置
  • 清晰的字体大小
Speed Index 指标

根据 Speed Index 项目主页上的描述,Speed Index 指的是页面可见部分展示完成的平均时间,该指标通过用毫秒表示,并取决于视图端口的大小。

混合度量标准的分数考虑了上述多个单一的度量标准,并将这些标准和页面加载时用户可以实际看到的标准结合起来。Speed Index 是度量终端用户实际体验的最好标准之一。

设置性能测试

性能测试涉及的指标繁多,我们不可能也不愿意去手动进行测试,我们可以借助一些自动化工具插件来完成这些工作,例如 Grunt PageSpeed 插件,Grunt PerfBudget 插件。

视觉还原测试

对于前端开发者来说,尽可能的高度还原 Photoshop 设计稿是我们的责任,也是评价我们工作的一项重要因素。在一个多人合作的团队中,很有可能会出现这种情况:一段时候后你发现原来已经做好的界面却出现了问题,于是你开始调整。但是,这样的情况会频繁的反复发生,其中很大一部分原因在于项目过大,团队开发者过多,很难让普通开发者去关注整个系统的设计,往往就会出现一个人写的样式影响了另一个人写的样式,从而导致界面发生变化。

因此,确保每一次的提交、合并都不会影响已经完成的界面效果是至关重要的,除了提高开发者的专业素养之外,我们需要进行视觉还原测试来帮助我们解决这个问题。

常见的质疑

为何已经完成的页面界面会在后来发生变化,通常有这么几种原因:

  • 不了解情况的开发者

    即使你的代码完美无缺,但很难确保和你同步进行开发的其他人,或者说后期维护的开发者,他们写的 CSS 类名、样式不会影响到你写的代码。

  • 不一致的设计

    通常一个比较大型的项目中,Photoshop 设计稿文件也非常得多,如果后期发生了一些全局性的细微变动,很难确保设计师会将变动更新到每一个文件,这就会出现不一致的设计稿。

  • 举棋不定的决策者

    通常来说,改动是不可避免的,但是如果决策者仔细研究足够多的功能,进而频繁的进行改动,一次又一次的进行原型开发会大大降低开发效率,原型开发应该是基于设计快速迭代之后的最终设计稿做出。

一个经过测试的解决方案

以上的场景中都突出了更深刻的组织层面的问题,它们可以通过适当的测试覆盖率来缓解。我们不去测试 JavaScript 函数的有效返回结果,而是抓取已授权的设计系统的视觉外观,从而验证我们没有偏离该系统。在提交之前抓取这些视觉还原是保证设计系统一致性的关键。

视觉还原测试让我们可以将正在开发的版本或者即将部署的版本(新版本)与正确的版本(基线版本)进行视觉对比。这个过程只不过是抓取基线版本的截图,与最新版本进行对比,并找出像素层面的差异。

通过把这些基线图片提交到仓库,或者在测试库里将其标记为通过,我们就对任何特定的功能在像素级别的视觉表现有了签名确认并一致认同的核对记录。在任何代码提交到主分支之前,视觉还原测试提供了一种测试网站所有功能的方法,以确保没有出乎意料的视觉改变。

这样,我们通常也会分辨出到底是设计师更新设计稿的疏忽还是正常的需求变更。

视觉还原测试的多面性

借助于多种技术和流程,视觉还原测试可以有多种风格。虽然新的工具不断地被发布到开源社区,但他们通常是一小部分功能的组合。大多数工具可以归属为以下几类。

  • 基于页面的比较

    Wraith 是一个基于页面的比较的例子。它使用 YAML 作为设置文件,因此可以很轻松地比较来自两个不同来源的一大串页面列表。当你不期望两个不同来源的页面有任何差异时,比如需要比较线上页面和在工作中即将部署的页面时,这个方法会很合适。

  • 基于组件的比较

    BackstopJS 在基于组件或者基于选择器的比较方面,是一个绝佳的选择。基于组件的比较工具使你可以抓取独立的页面片段进行对比,这样可以写出更有针对性的测试,并防止误报。

  • CSS 单位测试

    Quixote 是一类比较独特的比较工具,用于比较 CSS 单位的差异,而不是视觉上的差异。Quixote 可以设置 TDD 模式的测试用例,这些用例会设置好预期的 CSS 数值(比如字体大小为 1em,侧边栏的内边距是 2.5%),然后检测页面是否满足这些条件。它还可以诊断页面是否遵守品牌的视觉规范,比如 logo 的尺寸是否正确,以及 logo 与其它内容是否保持恰当的距离。

  • 基于无头浏览器的测试

    Gemini 是一款可以使用无头浏览器 PhantomJS 的比较工具,它可以在抓取截图之前加载 Web 页面。PhantomJS 是 JavaScript 实现的 WebKit 内核的浏览器,这意味着它速度非常快,并且具有跨平台的一致性。

  • 基于桌面浏览器的测试

    Geimin 非常独特,它支持在在传统的桌面浏览器上运行测试用例。为了达到这个目的,Gemini 使用 Selenium 服务器打开并操作系统中安装的浏览器。这种方式没有基于无头浏览器的方式快,而且也受到系统安装的浏览器版本的影响。但是它更接近真实情况,并且可以发现某个特定浏览器引入的 bug。

  • 包含脚本库文件

    CasperJS 是一个导航脚本库,可以和 PhantomJS 等无头浏览器协同工作。该工具可以和在浏览器中打开的页面进行交互。使用它,你可以点击按钮,等待模态窗口,填充并提交表单,最终对结果进行截图。CasperJS 还可以在 PhantomJS 打开的页面中执行 JavaScript,你可以隐藏元素、关掉动画,甚至还可以使用静态模拟内容替换掉动态真实内容。

  • 基于图像用户界面的比较工具,支持更改确认

    Diffux 项目存储了测试历史数据,并可以在基于 Web 的用户界面中提供测试结果的反馈。基准图像存储在数据库中,任何对它的改动都必须在该应用界面中标记为接收或者拒绝。

  • 基于命令行的比较工具,支持更改确认

    PhantomCSS 是一款基于组件的比较工具,借助于 PhantomJS 和 CasperJS,它可以仅通过命令行来运行。测试是通过命令行终端运行的,无论测试是否通过,其结果都会输出到命令行终端里。这种类型的工具尤其适合通过 Grunt 或者 Gulp 运行,而其输出也很适合 Jenkins 或者 Travis CI 等自动化工具。

文档

前端项目日益变得复杂,但这并不是一件坏事,只是说前端在快速的发展过程中也出现了许多问题。

前端开发不像服务器端,桌面端开发一样,后者无论是使用 Java、PHP 还是 C++ 等语言开发,其语言本身就提供了很清晰的类式结构特性,而且框架发展成熟,因此将一个复杂的功能需求代码拆分、抽象从而实现可重用看起来都是很平常的事情;然而,前端开发却不一样,长期夹杂于 JSP 与 PHP 页面的前端代码要实现这些其实要困难的多。那么,随之而来的问题就是,庞大的前端项目没有清晰的代码结构,没有清晰的开发文档,导致后期维护的工作量可想而知。

不过,随着前端的发展,现今普遍采用了前后端完全分离的开发模式,页面由 JSP 与 PHP 这些夹杂着后端逻辑的页面转变为纯粹的 HTML 页面,为前端的代码拆分、抽象从而实现可复用提供了更多的可能性。与此同时,前端的框架也层出不穷,参考服务器端的开发模式,为前端如何规划项目结构和撰写文档提供了基石。

何为文档

**文档是系统化设计的蓝图。没有文档,我们将难免重复解决已经解决过的问题,而且花大量时间查看代码来寻找最简单的答案。**没有文档,对新员工也很不友好,没有办法让其快速融入项目组。

写文档是开发工作的一部分,而不是等重要工作完成之后才开始的事情。

文档不只是简单地写下代码如何工作,但其主要作用记录我们的开发过程以及开发的代码是如何工作的,帮助其他开发者更好地理解我们所开发的代码。

文档有多种形式,其中有很多只有在架构支持时才能成型。虽然有些文档只是用于描述每个函数的普通文本,但这种文档的背后往往有一套基于搜索、导航和视觉呈现的构建系统。其他的文档用于展示系统的资源,由我们所写的样式、脚本、模版和模式来驱动。

静态文档

Hologram 是基于 Ruby 的通用文档工具,支持 CSS、Sass、JavaScript 文件中内联注释、块注释,而且其注释可以使用 Markdown 格式,从而生成静态的 HTML 页面文档,功能比较全面和强大。

SassDoc 是基于 Node 的系统文档工具,它宣称 “SassDoc 对于 Sass 的意义,就像 JSDoc 对于 JavaScript 的意义一样”,而且它的确如此!如果你正在构建一个大型的 Sass 框架,或者复杂的栅格或者颜色系统,SassDoc 正是你想要的工具。

代码驱动的文档

Pattern Lab 是多平台模式库工具,它可以使你模块化地开发设计系统,并将模板和 CSS 转换成可浏览的模式库。在模块化的系统中,你可以先开发每个单独的模式片段,然后通过组合这些片段产生更复杂的模式。可预览的组件库是开发者、设计师、用户体验师、质量工程师和产品所有者聚在一起时可以使用的完美工具。它为设计系统中每个部分创建了一门通用的语言和稳定的参照系。

JSON 模式是用于描述数据格式的语言,同时也可以说明数据的验证方式。在前端架构的领域中,可以用 JSON 模式来描述模板和模式所需要的数据。JSON 超模式甚至可以描述能够通过 HTTP 协议与设计系统交互的方法,包括验证、渲染和测试。JSON 超模式是一种代码驱动的文档工具,因为它提供了验证和驱动编辑工具的功能。JSON 模式还提供了可读性很强的系统手册,取代了开发者实现一个功能所需的一大堆手写说明。

参考

  • 《前端架构设计》- Micah Godbolt 著