前端项目开发规范

4,088

规范目的

为提高团队协作效率,便于前端后期优化维护,输出高质量的文档。

基本原则

结构、样式、行为分离

尽量确保文档和模板只包含 HTML 结构,样式都放到样式表里,行为都放到脚本里。

缩进

统一两个空格缩进(总之缩进统一即可),不要使用 Tab 或者 Tab、空格混搭。

文件编码(编译器一般自动生成)

使用不带 BOM 的 UTF-8 编码。

在 HTML中指定编码 <meta charset="utf-8">

无需使用 @charset 指定样式表的编码,它默认为 UTF-8 (参考 @charset);

一律使用小写字母

<!-- 推荐 -->
<img src="google.png" alt="Google">

<!-- 不推荐 -->
<A HREF="/">Home</A>
/* 推荐 */
color: #e5e5e5;

/* 不推荐 */
color: #E5E5E5;

省略外链资源 URL 协议部分

省略外链资源(图片及其它媒体资源)URL 中的 http / https 协议,使 URL 成为相对地址,避免Mixed Content 问题,减小文件字节数。

其它协议(ftp 等)的 URL 不省略。

<!-- 推荐 -->
<script src="//www.xxx.cn/statics/js/autotrack.js"></script>

<!-- 不推荐 -->
<script src="http://www.xxx.cn/statics/js/autotrack.js"></script>
/* 推荐 */
.example {
  background: url(//www.google.com/images/example);
}

/* 不推荐 */
.example {
  background: url(http://www.google.com/images/example);
}

统一注释

通过配置编辑器,可以提供快捷键来输出一致认可的注释模式(ESlint 规范里介绍)。

HTML 注释

  • 模块注释
<!-- 文章列表列表模块 -->
<div class="article-list">
...
</div>
  • 区块注释
<!--
@name: Drop Down Menu
@description: Style of top bar drop down menu.
@author: Ashu(Aaaaaashu@gmail.com)
-->

CSS 注释

组件块和子组件块以及声明块之间使用一空行分隔,子组件块之间三空行分隔;

/* ==========================================================================
   组件块
 ============================================================================ */

/* 子组件块
 ============================================================================ */
.selector {
  padding: 15px;
  margin-bottom: 15px;
}

/* 子组件块
 ============================================================================ */
.selector-secondary {
  display: block; /* 注释*/
}

.selector-three {
  display: span;
}

JavaScript 注释

  • 单行注释

    必须独占一行。// 后跟一个空格,缩进与下一行被注释说明的代码一致。

  • 多行注释

    避免使用 /.../ 这样的多行注释。有多行注释内容时,使用多个单行注释。

  • 函数/方法注释

    函数/方法注释必须包含函数说明,有参数和返回值时必须使用注释标识;

    参数和返回值注释必须包含类型信息和说明;

    当函数是内部函数,外部不可访问时,可以使用 @inner 标识。

/**
 * 函数描述
 *
 * @param {string} p1 参数1的说明
 * @param {string} p2 参数2的说明,比较长
 *     那就换行了.
 * @param {number=} p3 参数3的说明(可选)
 * @return {Object} 返回值描述
 */
function foo(p1, p2, p3) {
    var p3 = p3 || 10;
    return {
        p1: p1,
        p2: p2,
        p3: p3
    };
}

文件注释

文件注释用于告诉不熟悉这段代码的读者这个文件中包含哪些东西。 应该提供文件的大体内容, 它的作者, 依赖关系和兼容性信息。如下:

/**
 * @fileoverview Description of file, its uses and information
 * about its dependencies.
 * @author user@meizu.com (Firstname Lastname)
 * Copyright 2015 Meizu Inc. All Rights Reserved.
 */

代码验证(基本用不到)

代码验证不是最终目的,真的目的在于让开发者在经过多次的这种验证过程后,能够深刻理解到怎样的语法或写法是非标准和不推荐的,即使在某些场景下被迫要使用非标准写法,也可以做到心中有数。

HTML

尽量遵循 HTML 标准和语义,但是不要以牺牲实用性为代价。任何时候都要尽量使用最少的标签并保持最小的复杂度。

通用约定

标签

  • 自闭合(self-closing)标签,无需闭合 ( 例如: img input br hr 等 );
  • 可选的闭合标签(closing tag),需闭合 ( 例如:
  • 或 );
  • 尽量减少标签数量。
<img src="https://atts.w3cschool.cn/attachments/image/cimg/google.png" alt="Google">
<input type="text" name="title">

<ul>
  <li>Style</li>
  <li>Guide</li>
</ul>

<!-- 不推荐 -->
<span class="avatar">
  <img src="...">
</span>

<!-- 推荐 -->
<img class="avatar" src="...">

Class 与 ID

  • class 应以功能或内容命名,不以表现形式命名;
  • class 与 id 单词字母小写,多个单词组成时,采用中划线-分隔;
  • 使用唯一的 id 作为 Javascript hook, 同时避免创建无样式信息的 class;
<!-- 不推荐 -->
<div class="j-hook left contentWrapper"></div>

<!-- 推荐 -->
<div id="j-hook" class="sidebar content-wrapper"></div>

属性顺序

HTML 属性应该按照特定的顺序出现以保证易读性。

  • id
  • class
  • name
  • data-xxx
  • src, for, type, href
  • title, alt
  • aria-xxx, role
<a id="..." class="..." data-modal="toggle" href="###"></a>

<input class="form-control" type="text">

<img src="..." alt="...">

引号

属性的定义,统一使用双引号。

<!-- 不推荐 -->
<span id='j-hook' class=text>Google</span>

<!-- 推荐 -->
<span id="j-hook" class="text">Google</span>

嵌套

a 不允许嵌套 div 这种约束属于语义嵌套约束,与之区别的约束还有严格嵌套约束,比如a 不允许嵌套 a。

严格嵌套约束在所有的浏览器下都不被允许;而语义嵌套约束,浏览器大多会容错处理,生成的文档树可能相互不太一样。

语义嵌套约束

  • <li> 用于 <ul><ol> 下;
  • <dd>, <dt> 用于 <dl> 下;
  • <thead>, <tbody>, <tfoot>, <tr>, <td> 用于 <table> 下。

严格嵌套约束

  • inline-Level 元素,仅可以包含文本或其它 inline-Level 元素;
  • <a>里不可以嵌套交互式元素<a><button><select>等;
  • <p>里不可以嵌套块级元素<div><h1>~<h6><p><ul>/<ol>/<li><dl>/<dt>/<dd><form>等。

布尔值属性

HTML5 规范中 disabled、checked、selected 等属性不用设置值。

<input type="text" disabled>

<input type="checkbox" value="1" checked>

<select>
  <option value="1" selected>1</option>
</select>

语义化

没有 CSS 的 HTML 是一个语义系统而不是 UI 系统。

此外语义化的 HTML 结构,有助于机器(搜索引擎)理解,另一方面多人协作时,能迅速了解开发者意图。

常见标签语义(H5 的未罗列)

标签 语义
<p> 段落
<h1> <h2> <h3> ... 标题
<ul> 无序列
<ol> 有序列表
<blockquote> 大段引用
<cite> 一般引用
<b> 为样式加粗而加粗
<strong> 为强调内容而加粗
<i> 为样式倾斜而倾斜
<em> 为强调内容而倾斜
code 代码标识
abbr 缩写
... ...

示例

将你构建的页面当作一本书,将标签的语义对应的其功能和含义;

  • 书的名称:<h1>
  • 书的每个章节标题: <h2>
  • 章节内的文章标题: <h3>
  • 小标题/副标题: <h4> <h5> <h6>
  • 章节的段落: <p>

HEAD

文档类型

为每个 HTML 页面的第一行添加标准模式(standard mode)的声明, 这样能够确保在每个浏览器中拥有一致的表现。

<!DOCTYPE html>

语言属性

为什么使用 lang="zh-cmn-Hans" 而不是我们通常写的 lang="zh-CN" 呢? 请参考知乎上的讨论: 网页头部的声明应该是用 lang="zh" 还是 lang="zh-cn"

<!-- 中文 -->
<html lang="zh-Hans">

<!-- 简体中文 -->
<html lang="zh-cmn-Hans">

<!-- 繁体中文 -->
<html lang="zh-cmn-Hant">

<!-- English -->
<html lang="en">

字符编码

  • 以无 BOM 的 utf-8 编码作为文件格式;
  • 指定字符编码的 meta 必须是 head 的第一个直接子元素;
<html>
  <head>
    <meta charset="utf-8">
    ......
  </head>
  <body>
    ......
  </body>
</html>

IE 兼容模式

优先使用最新版本的IE 和 Chrome 内核

<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

SEO 优化

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <!-- SEO -->
    <title>Style Guide</title>
    <meta name="keywords" content="your keywords">
    <meta name="description" content="your description">
    <meta name="author" content="author,email address">
</head>

viewport

  • viewport: 一般指的是浏览器窗口内容区的大小,不包含工具条、选项卡等内容;
  • width: 浏览器宽度,输出设备中的页面可见区域宽度;
  • device-width: 设备分辨率宽度,输出设备的屏幕可见宽度;
  • initial-scale: 初始缩放比例;
  • maximum-scale: 最大缩放比例;

为移动端设备优化,设置可见区域的宽度和初始缩放比例。

<meta name="viewport" content="width=device-width, initial-scale=1.0">

iOS 图标

  • apple-touch-icon 图片自动处理成圆角和高光等效果;
  • apple-touch-icon-precomposed 禁止系统自动添加效果,直接显示设计原图;
<!-- iPhone 和 iTouch,默认 57x57 像素,必须有 -->
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-57x57-precomposed.png">

<!-- iPad,72x72 像素,可以没有,但推荐有 -->
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-72x72-precomposed.png" sizes="72x72">

<!-- Retina iPhone 和 Retina iTouch,114x114 像素,可以没有,但推荐有 -->
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-114x114-precomposed.png" sizes="114x114">

<!-- Retina iPad,144x144 像素,可以没有,但推荐有 -->
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-144x144-precomposed.png" sizes="144x144">

favicon

在未指定 favicon 时,大多数浏览器会请求 Web Server 根目录下的 favicon.ico 。为了保证 favicon 可访问,避免404,必须遵循以下两种方法之一:

在 Web Server 根目录放置 favicon.ico 文件; 使用 link 指定 favicon;

<link rel="shortcut icon" href="path/to/favicon.ico">

HEAD 模板

<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title>Style Guide</title>
    <meta name="description" content="不超过150个字符">
    <meta name="keywords" content="">
    <meta name="author" content="name, email@gmail.com">

    <!-- 为移动设备添加 viewport -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- iOS 图标 -->
    <link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-57x57-precomposed.png">

    <link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
    <link rel="shortcut icon" href="path/to/favicon.ico">
</head>

CSS

通用约定

代码组织

  • 以组件为单位组织代码段;
  • 制定一致的注释规范;
  • 组件块和子组件块以及声明块之间使用一空行分隔,子组件块之间三空行分隔;
  • 如果使用了多个 CSS 文件,将其按照组件而非页面的形式分拆,因为页面会被重组,而组件只会被移动。

良好的注释是非常重要的。请留出时间来描述组件(component)的工作方式、局限性和构建它们的方法。不要让你的团队其它成员 来猜测一段不通用或不明显的代码的目的。

提示:通过配置编辑器,可以提供快捷键来输出一致认可的注释模式。

/* ==========================================================================
   组件块
 ============================================================================ */

/* 子组件块
 ============================================================================ */
.selector {
  padding: 15px;
  margin-bottom: 15px;
}

/* 子组件块
 ============================================================================ */
.selector-secondary {
  display: block; /* 注释*/
}

.selector-three {
  display: span;
}

Class 和 ID

  • 使用语义化、通用的命名方式;
  • 使用连字符 - 作为 ID、Class 名称界定符,不要驼峰命名法和下划线;
  • 避免选择器嵌套层级过多,尽量少于 3 级;
  • 避免选择器和 Class、ID 叠加使用。

出于性能考量,在没有必要的情况下避免元素选择器叠加 Class、ID 使用。

元素选择器和 ID、Class 混合使用也违反关注分离原则。如果HTML标签修改了,就要再去修改 CSS 代码,不利于后期维护。

/* 不推荐 */
.red {}
.box_green {}
.page .header .login #username input {}
ul#example {}

/* 推荐 */
#nav {}
.box-video {}
#username input {}
#example {}

声明块格式

  • 选择器分组时,保持独立的选择器占用一行;
  • 声明块的左括号 { 前添加一个空格;
  • 声明块的右括号 } 应单独成行;
  • 声明语句中的 : 后应添加一个空格;
  • 声明语句应以分号 ; 结尾;
  • 一般以逗号分隔的属性值,每个逗号后应添加一个空格;
  • rgb()、rgba()、hsl()、hsla() 或 rect() 括号内的值,逗号分隔,但逗号后不添加一个空格;
  • 对于属性值或颜色参数,省略小于 1 的小数前面的 0 (例如,.5 代替 0.5;-.5px 代替-0.5px);
  • 十六进制值应该全部小写和尽量简写,例如,#fff 代替 #ffffff;
  • 避免为 0 值指定单位,例如,用 margin: 0; 代替 margin: 0px;。
/* 不推荐  */
.selector, .selector-secondary, .selector[type=text] {
  padding:15px;
  margin:0px 0px 15px;
  background-color:rgba(0, 0, 0, 0.5);
  box-shadow:0px 1px 2px #CCC,inset 0 1px 0 #FFFFFF
}

/* 推荐 */
.selector,
.selector-secondary,
.selector[type="text"] {
  padding: 15px;
  margin-bottom: 15px;
  background-color: rgba(0,0,0,.5);
  box-shadow: 0 1px 2px #ccc, inset 0 1px 0 #fff;
}

声明顺序

相关属性应为一组,推荐的样式编写顺序

  1. Positioning
  2. Box model
  3. Typographic
  4. Visual

由于定位(positioning)可以从正常的文档流中移除元素,并且还能覆盖盒模型(box model)相关的样式,因此排在首位。盒模型决定了组件的尺寸和位置,因此排在第二位。

其他属性只是影响组件的内部(inside)或者是不影响前两组属性,因此排在后面。

.declaration-order {
  /* Positioning */
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;

  /* Box model */
  display: block;
  box-sizing: border-box;
  width: 100px;
  height: 100px;
  padding: 10px;
  border: 1px solid #e5e5e5;
  border-radius: 3px;
  margin: 10px;
  float: right;
  overflow: hidden;

  /* Typographic */
  font: normal 13px "Helvetica Neue", sans-serif;
  line-height: 1.5;
  text-align: center;

  /* Visual */
  background-color: #f5f5f5;
  color: #fff;
  opacity: .8;

  /* Other */
  cursor: pointer;
}

引号使用

url() 、属性选择符、属性值使用双引号。 参考 Is quoting the value of url() really necessary?

/* 不推荐 */
@import url(//www.google.com/css/maia.css);

html {
  font-family: 'open sans', arial, sans-serif;
}

/* 推荐 */
@import url("//www.google.com/css/maia.css");

html {
  font-family: "open sans", arial, sans-serif;
}

.selector[type="text"] {

}

媒体查询(Media query)的位置

将媒体查询放在尽可能相关规则的附近。不要将他们打包放在一个单一样式文件中或者放在文档底部。如果你把他们分开了,将来只会被大家遗忘。

.element { ... }
.element-avatar { ... }
.element-selected { ... }

@media (max-width: 768px) {
  .element { ...}
  .element-avatar { ... }
  .element-selected { ... }
}

不要使用 @import

<link> 相比,@import 要慢很多,不光增加额外的请求数,还会导致不可预料的问题。

替代办法:

  • 使用多个 元素;
  • 通过 Sass 或 Less 类似的 CSS 预处理器将多个 CSS 文件编译为一个文件;
  • 其他 CSS 文件合并工具;

参考 don’t use @import

链接的样式顺序:

a:link -> a:visited -> a:hover -> a:active(LoVeHAte)

无需添加浏览器厂商前缀

使用 Autoprefixer 自动添加浏览器厂商前缀,编写 CSS 时不需要添加浏览器前缀,直接使用标准的 CSS 编写。

Autoprefixer 通过 Can I use,按兼容的要求,对相应的 CSS 代码添加浏览器厂商前缀。

模块组织

任何超过 1000 行的 CSS 代码,你都曾经历过这样的体验:

  1. 这个 class 到底是什么意思呢?
  2. 这个 class 在哪里被使用呢?
  3. 如果我创建一个 xxoo class,会造成冲突吗?

Reasonable System for CSS Stylesheet Structure 的目标就是解决以上问题,它不是一个框架,而是通过规范,让你构建更健壮和可维护的 CSS 代码。

Components(组件)

从 Components 的角度思考,将网站的模块都作为一个独立的 Components。

Naming components (组件命名)

Components 最少以两个单词命名,通过 - 分离,例如:

  • 点赞按钮 (.like-button)
  • 搜索框 (.search-form)
  • 文章卡片 (.article-card)

Elements (元素)

Elements 是 Components 中的元素

Naming elements (元素命名)

Elements 的类名应尽可能仅有一个单词。

 .search-form {
    > .field { /* ... */ }
    > .action { /* ... */ }
  }

On multiple words (多个单词)

对于倘若需要两个或以上单词表达的 Elements 类名,不应使用中划线和下划线连接,应直接连接。

  .profile-box {
    > .firstname { /* ... */ }
    > .lastname { /* ... */ }
    > .avatar { /* ... */ }
  }

Avoid tag selectors (避免标签选择器)

任何时候尽可能使用 classnames。标签选择器在使用上没有问题,但是其性能上稍弱,并且表意不明确。

  .article-card {
    > h3    { /* ✗ avoid */ }
    > .name { /* ✓ better */ }
  }

Variants (变体)

Components 和 Elements 可能都会拥有 Variants。

Naming variants (变体命名)

Variants 的 classname 应带有前缀中划线 -

.like-button {
    &.-wide { /* ... */ }
    &.-short { /* ... */ }
    &.-disabled { /* ... */ }
  }

Element variants (元素变体)

  .shopping-card {
    > .title { /* ... */ }
    > .title.-small { /* ... */ }
  }

Dash prefixes (中划线前缀)

为什么使用中划线作为变体的前缀?

  • 它可以避免歧义与 Elements
  • CSS class 仅能以单词和 _ 或 - 开头
  • 中划线比下划线更容易输出

Layout (布局)

Avoid positioning properties (避免定位属性)

Components 应该在不同的上下文中都可以复用,所以应避免设置以下属性:

  • Positioning (position, top, left, right, bottom)
  • Floats (float, clear)
  • Margins (margin)
  • Dimensions (width, height) *

Fixed dimensions (固定尺寸)

头像和 logos 这些元素应该设置固定尺寸(宽度,高度...)。

Define positioning in parents (在父元素中设置定位)

倘若你需要为组件设置定位,应将在组件的上下文(父元素)中进行处理,比如以下例子中,将 widths和 floats 应用在 list component(.article-list) 当中,而不是 component(.article-card) 自身。

  .article-list {
    & {
      @include clearfix;
    }

    > .article-card {
      width: 33.3%;
      float: left;
    }
  }

  .article-card {
    & { /* ... */ }
    > .image { /* ... */ }
    > .title { /* ... */ }
    > .category { /* ... */ }
  }

Avoid over-nesting (避免过分嵌套)

当出现多个嵌套的时候容易失去控制,应保持不超过一个嵌套。

  /* ✗ Avoid: 3 levels of nesting */
  .image-frame {
    > .description {
      /* ... */

      > .icon {
        /* ... */
      }
    }
  }

  /* ✓ Better: 2 levels */
  .image-frame {
    > .description { /* ... */ }
    > .description > .icon { /* ... */ }
  }

Apprehensions (顾虑)

  • 中划线-是一坨糟糕的玩意:其实你可以选择性的使用,只要将 Components, Elements, Variants记在心上即可。
  • 我有时候想不出两个单词唉:有些组件的确使用一个单词就能表意,比如 alert 。但其实你可以使用后缀,使其意识更加明确。

比如块级元素:

  • .alert-box
  • .alert-card
  • .alert-block

或行内级元素

  • .link-button
  • .link-span

Summary (总结)

  • 以 Components 的角度思考,以两个单词命名(.screenshot-image)
  • Components 中的 Elements,以一个单词命名(.blog-post .title)
  • Variants,以中划线-作为前缀(.shop-banner.-with-icon)
  • Components 可以互相嵌套
  • 记住,你可以通过继承让事情变得更简单

Less 规范

代码组织

代码按一下顺序组织:

  1. @import
  2. 变量声明
  3. 样式声明
@import "mixins/size.less";

@default-text-color: #333;

.page {
  width: 960px;
  margin: 0 auto;
}

@import 语句

@import 语句引用的文需要写在一对引号内,.less 后缀不得省略。引号使用 ' 和 " 均可,但在同一项目内需统一。

/* 不推荐 */
@import "mixins/size";
@import 'mixins/grid.less';

/* 推荐 */
@import "mixins/size.less";
@import "mixins/grid.less";

混入(Mixin)

  1. 在定义 mixin 时,如果 mixin 名称不是一个需要使用的 className,必须加上括号,否则即使不被调用也会输出到 CSS 中。

  2. 如果混入的是本身不输出内容的 mixin,需要在 mixin 后添加括号(即使不传参数),以区分这是否是一个 className。

/* 不推荐 */
.big-text {
  font-size: 2em;
}

h3 {
  .big-text;
  .clearfix;
}

/* 推荐 */
.big-text() {
  font-size: 2em;
}

h3 {
  .big-text(); /* 1 */
  .clearfix(); /* 2 */
}

避免嵌套层级过多

  • 将嵌套深度限制在2级。对于超过3级的嵌套,给予重新评估。这可以避免出现过于详实的CSS选择器;
  • 避免大量的嵌套规则。当可读性受到影响时,将之打断。推荐避免出现多于20行的嵌套规则出现。

字符串插值

变量可以用类似ruby和php的方式嵌入到字符串中,像@{name}这样的结构: @base-url: "assets.fnord.com"; background-image: url("@{base-url}/images/bg.png");

性能优化

慎重选择高消耗的样式

浏览器渲染过程、重绘和重排等原理不清楚的,推荐文章:你真的了解回流和重绘吗?

高消耗属性在绘制前需要浏览器进行大量计算:

  • box-shadows

  • border-radius

  • transparency

  • transforms

  • CSS filters(性能杀手)

    【注】:使用 filter 的问题是它会阻塞渲染并且当浏览在加载图片时,会冻结浏览器。它也会提升内存消耗,它是作用于一个元素而不是一个图片,所以问题会更严重。最好的解决方案是完全避免使用 AlphaImageLoader,用 PNG8 来代替。

避免过分重排

当发生重排的时候,浏览器需要重新计算布局位置与大小。

常见的重排元素:

  • width
  • height
  • padding
  • margin
  • display
  • border-width
  • position
  • top
  • left
  • right
  • bottom
  • font-size
  • float
  • text-align
  • overflow-y
  • font-weight
  • overflow
  • font-family
  • line-height
  • vertical-align
  • clear
  • white-space
  • min-height

正确使用 Display 的属性

Display 属性会影响页面的渲染,请合理使用。

  • display: inline后不应该再使用 width、height、margin、padding 以及 float;
  • display: inline-block 后不应该再使用 float;
  • display: block 后不应该再使用 vertical-align;
  • display: table-* 后不应该再使用 margin 或者 float;

不滥用 Float

Float在渲染时计算量比较大,尽量减少使用。

动画性能优化

动画的实现原理,是利用了人眼的“视觉暂留”现象,在短时间内连续播放数幅静止的画面,使肉眼因视觉残象产生错觉,而误以为画面在“动”。

动画的基本概念:

  • 帧:在动画过程中,每一幅静止画面即为一“帧”;
  • 帧率:即每秒钟播放的静止画面的数量,单位是fps(Frame per second);
  • 帧时长:即每一幅静止画面的停留时间,单位一般是ms(毫秒);
  • 跳帧(掉帧/丢帧):在帧率固定的动画中,某一帧的时长远高于平均帧时长,导致其后续数帧被挤压而丢失的现象。

一般浏览器的渲染刷新频率是 60 fps,所以在网页当中,帧率如果达到 50-60 fps 的动画将会相当流畅,让人感到舒适。

  • 如果使用基于 javaScript 的动画,尽量使用 requestAnimationFrame. 避免使用 setTimeout, setInterval;
  • 避免通过类似 jQuery animate()-style 改变每帧的样式,使用 CSS 声明动画会得到更好的浏览器优化; 使用 translate 取代 absolute 定位就会得到更好的 fps,动画会更顺滑。

多利用硬件能力,如通过 3D 变形开启 GPU 加速

一般在 Chrome 中,3D或透视变换(perspective transform)CSS属性和对 opacity 进行 CSS 动画会创建新的图层,在硬件加速渲染通道的优化下,GPU 完成 3D 变形等操作后,将图层进行复合操作(Compesite Layers),从而避免触发浏览器大面积重绘和重排。

注:3D 变形会消耗更多的内存和功耗。

使用 translate3d 右移 500px 的动画流畅度要明显优于直接使用 left:

.ball-1 {
  transition: -webkit-transform .5s ease;
  -webkit-transform: translate3d(0, 0, 0);
}
.ball-1.slidein{
  -webkit-transform: translate3d(500px, 0, 0);
}
.ball-2 {
  transition: left .5s ease; left:0;
}
.ball-2.slidein {
  left:500px;
}

提升 CSS 选择器性能

CSS 选择器对性能的影响源于浏览器匹配选择器和文档元素时所消耗的时间,所以优化选择器的原则是应尽量避免使用消耗更多匹配时间的选择器。而在这之前我们需要了解 CSS 选择器匹配的机制, 如子选择器规则:

#header > a {font-weight:blod;}

我们中的大多数人都是从左到右的阅读习惯,会习惯性的设定浏览器也是从左到右的方式进行匹配规则,推测这条规则的开销并不高。

我们会假设浏览器以这样的方式工作:寻找 id 为 header 的元素,然后将样式规则应用到直系子元素中的 a 元素上。我们知道文档中只有一个 id 为 header 的元素,并且它只有几个 a 元素的子节点,所以这个 CSS 选择器应该相当高效。

事实上,却恰恰相反,CSS 选择器是从右到左进行规则匹配。了解这个机制后,例子中看似高效的选择器在实际中的匹配开销是很高的,浏览器必须遍历页面中所有的 a 元素并且确定其父元素的 id 是否为 header 。

如果把例子的子选择器改为后代选择器则会开销更多,在遍历页面中所有 a 元素后还需向其上级遍历直到根节点。

#header  a {font-weight:blod;}

理解了CSS选择器从右到左匹配的机制后,明白只要当前选择符的左边还有其他选择符,样式系统就会继续向左移动,直到找到和规则匹配的选择符,或者因为不匹配而退出。我们把最右边选择符称之为关键选择器。

  1. 避免使用通用选择器
/* 不推荐 */
.content * {color: red;}

浏览器匹配文档中所有的元素后分别向上逐级匹配 class 为 content 的元素,直到文档的根节点。因此其匹配开销是非常大的,所以应避免使用关键选择器是通配选择器的情况。

  1. 避免使用标签或 class 选择器限制 id 选择器
/* 不推荐 */
button#backButton {…}
/* 推荐 */
#newMenuIcon {…}
  1. 避免使用标签限制 class 选择器
/* 不推荐 */
treecell.indented {…}
/* 推荐 */
.treecell-indented {…}
/* 更推荐 */
.hierarchy-deep {…}
  1. 避免使用多层标签选择器。使用 class 选择器替换,减少css查找
/* 不推荐 */
treeitem[mailfolder="true"] > treerow > treecell {…}
/* 推荐 */
.treecell-mailfolder {…}
  1. 避免使用子选择器
/* 不推荐 */
treehead treerow treecell {…}
/* 推荐 */
treehead > treerow > treecell {…}
/* 更推荐 */
.treecell-header {…}
  1. 使用继承
/* 不推荐 */
#bookmarkMenuItem > .menu-left { list-style-image: url(blah) }
/* 推荐 */
#bookmarkMenuItem { list-style-image: url(blah) }

JavaScript

通用约定

注释

原则

  • As short as possible(如无必要,勿增注释):尽量提高代码本身的清晰性、可读性;
  • As long as necessary(如有必要,尽量详尽):合理的注释、空行排版等,可以让代码更易阅读、更具美感。

单行注释

必须独占一行。// 后跟一个空格,缩进与下一行被注释说明的代码一致。

多行注释

避免使用 /.../ 这样的多行注释。有多行注释内容时,使用多个单行注释。

函数/方法注释

  1. 函数/方法注释必须包含函数说明,有参数和返回值时必须使用注释标识;
  2. 参数和返回值注释必须包含类型信息和说明;
  3. 当函数是内部函数,外部不可访问时,可以使用 @inner 标识。
/**
 * 函数描述
 *
 * @param {string} p1 参数1的说明
 * @param {string} p2 参数2的说明,比较长
 *     那就换行了.
 * @param {number=} p3 参数3的说明(可选)
 * @return {Object} 返回值描述
 */
function foo(p1, p2, p3) {
    var p3 = p3 || 10;
    return {
        p1: p1,
        p2: p2,
        p3: p3
    };
}

文件注释

文件注释用于告诉不熟悉这段代码的读者这个文件中包含哪些东西。 应该提供文件的大体内容, 它的作者, 依赖关系和兼容性信息。如下:

/**
 * @fileoverview Description of file, its uses and information
 * about its dependencies.
 * @author user@meizu.com (Firstname Lastname)
 * Copyright 2009 Meizu Inc. All Rights Reserved.
 */

命名

变量, 使用 Camel (驼峰式)命名法。

var loadingModules = {};

私有属性、变量和方法以下划线 _ 开头。

var _privateMethod = {};

常量, 使用全部字母大写,单词间下划线分隔的命名方式。

var HTML_ENTITY = {};

函数, 使用 Camel 命名法。

函数的参数, 使用 Camel 命名法。

function stringFormat(source) {}

function hear(theBells) {}

类, 使用 Pascal(帕斯卡) 命名法。

类的 方法 / 属性, 使用 Camel 命名法。

function TextNode(value, engine) {
    this.value = value;
    this.engine = engine;
}

TextNode.prototype.clone = function () {
    return this;
};

枚举变量 使用 Pascal 命名法。

枚举的属性, 使用全部字母大写,单词间下划线分隔的命名方式。

var TargetState = {
    READING: 1,
    READED: 2,
    APPLIED: 3,
    READY: 4
};

由多个单词组成的缩写词,在命名中,根据当前命名法和出现的位置,所有字母的大小写与首字母的大小写保持一致。

function XMLParser() {}

function insertHTML(element, html) {}

var httpRequest = new HTTPRequest();

命名语法

类名,使用名词。

function Engine(options) {}

函数名,使用动宾短语。

function getStyle(element) {}

boolean 类型的变量使用 is 或 has 开头。

var isReady = false;
var hasMoreCommands = false;

Promise 对象用动宾短语的进行时表达。

var loadingData = ajax.get('url');
loadingData.then(callback);

接口命名规范

  1. 可读性强,见名晓义;
  2. 尽量不与 Vue、React、jQuery 等社区已有的习惯冲突;
  3. 尽量写全。不用缩写,除非是下面列表中约定的(变量以表达清楚为目标,uglify 会完成压缩体积工作)。
常用词 说明
options 表示选项,与 jQuery 社区保持一致,不要用 config, opts 等
active 表示当前,不要用 current 等
index 表示索引,不要用 idx 等
trigger 触点元素
triggerType 触发类型、方式
context 表示传入的 this 对象
object 推荐写全,不推荐简写为 o, obj 等
element 推荐写全,不推荐简写为 el, elem 等
length 不要写成 len, l
prev previous 的缩写
next next 下一个
constructor 不能写成 ctor
easing 示动画平滑函数
min minimize 的缩写
max maximize 的缩写
DOM 不要写成 dom, Dom
.hbs 使用 hbs 后缀表示模版
btn button 的缩写
link 超链接
title 主要文本
img 图片路径(img标签src属性)
dataset html5 data-xxx 数据接口
theme 主题
className 类名
classNameSpace class 命名空间
... ...

True 和 False 布尔表达式

类型检测优先使用 typeof。对象类型检测使用 instanceof。null 或 undefined 的检测使用 == null。

下面的布尔表达式都返回 false:

  • null
  • undefined
  • '' 空字符串
  • 0 数字0

但小心下面的, 可都返回 true:

  • '0' 字符串0
  • [] 空数组
  • {} 空对象

不要在 Array 上使用 for-in 循环

for-in 循环只用于 object/map/hash 的遍历, 对 Array 用 for-in 循环有时会出错. 因为它并不是从 0 到 length - 1 进行遍历, 而是所有出现在对象及其原型链的键值。

// Not recommended
function printArray(arr) {
  for (var key in arr) {
    print(arr[key]);
  }
}

printArray([0,1,2,3]);  // This works.

var a = new Array(10);
printArray(a);  // This is wrong.

a = document.getElementsByTagName('*');
printArray(a);  // This is wrong.

a = [0,1,2,3];
a.buhu = 'wine';
printArray(a);  // This is wrong again.

a = new Array;
a[3] = 3;
printArray(a);  // This is wrong again.

// Recommended
function printArray(arr) {
  var l = arr.length;
  for (var i = 0; i < l; i++) {
    print(arr[i]);
  }
}

二元和三元操作符

操作符始终写在前一行, 以免分号的隐式插入产生预想不到的问题。

// 不推荐 
var x = a ? b : c;

// 推荐 
var y = a ?
    longButSimpleOperandB : longButSimpleOperandC;

var z = a ?
        moreComplicatedB :
        moreComplicatedC;

. 操作符也是如此:

var x = foo.bar().
    doSomething().
    doSomethingElse();

条件(三元)操作符 (?:)

三元操作符用于替代 if 条件判断语句。

// 不推荐 
if (val != 0) {
  return foo();
} else {
  return bar();
}

// 推荐
return val ? foo() : bar();

&& 和 ||

二元布尔操作符是可短路的, 只有在必要时才会计算到最后一项。

// 不推荐
function foo(opt_win) {
  var win;
  if (opt_win) {
    win = opt_win;
  } else {
    win = window;
  }
  // ...
}

if (node) {
  if (node.kids) {
    if (node.kids[index]) {
      foo(node.kids[index]);
    }
  }
}

// 推荐
function foo(opt_win) {
  var win = opt_win || window;
  // ...
}

var kid = node && node.kids && node.kids[index];
if (kid) {
  foo(kid);
}

性能优化

鲜为人知的DOM

dom性能缓慢可以归结为一下3个原因:

  • 大规模的 DOM 操作
  • 脚本触发太多的重构和重绘
  • 定位节点在 DOM 中的路径慢

解决方案

  1. 尽可能减小 DOM 的操作次数

浏览器遍历 DOM 元素的代价是昂贵的。最简单优化 DOM 树查询的方案是,当一个元素出现多次时,将它保存在一个变量中,就避免多次查询 DOM 树了。

// 推荐
var myList = "";
var myListHTML = document.getElementById("myList").innerHTML;

for (var i = 0; i < 100; i++) {
  myList += "<span>" + i + "</span>";
}

myListHTML = myList;

// 不推荐
for (var i = 0; i < 100; i++) {
  document.getElementById("myList").innerHTML += "<span>" + i + "</span>";
}

Vue 和 React 等框架中采用的是 Diff 算法。

  1. 避免不必要的回流重绘(css 性能优化有提到)

eval && try-catch-finally

eval

无论什么时候,避免使用 eval 方法,因为执行这一方法会造成很大的开销。

使用 eval 或者 function constructor 会加大开销因为每一次脚本引擎调用他们是必须将源码转换成可执行代码;

另外,使用 eval 时,字符串会在执行时被打断。

try-catch-finally

不要在对性能影响很大的地方使用 try-catch-finally。

当你不确定这个方法是否能执行成功时,建议使用 try-catch,避免阻断当前代码的执行。

缓存数组长度

循环无疑是和 JavaScript 性能非常相关的一部分。通过存储数组的长度,可以有效避免每次循环重新计算。

注: 虽然现代浏览器引擎会自动优化这个过程,但是不要忘记还有旧的浏览器。

var arr = new Array(1000),
    len, i;
// 推荐 - 长度只计算一次并存储在 len 
for (i = 0, len = arr.length; i < len; i++) {

}

// 不推荐 - 长度需要每次都计算
for (i = 0; i < arr.length; i++) {

}

异步加载第三方内容

当你无法保证嵌入第三方内容比如 Youtube 视频或者一个 like/tweet 按钮可以正常工作的时候,你需要考虑用异步加载这些代码,避免阻塞整个页面加载。

(function() {

    var script,
        scripts = document.getElementsByTagName('script')[0];

    function load(url) {
      script = document.createElement('script');
      script.async = true;
      script.src = url;
      scripts.parentNode.insertBefore(script, scripts);
    }

    load('//apis.google.com/js/plusone.js');
    load('//platform.twitter.com/widgets.js');
    load('//s.widgetsite.com/widget.js');

}());

避免使用 jQuery 实现动画

禁止使用 slideUp/Down() fadeIn/fadeOut() 等方法; 尽量不使用 animate() 方法。

HTTP优化

  1. 减少 HTTP 请求
  • 合并文件,例如 css 代码和 js 代码分别合并到一个 css 文件和 js 文件中
  • 使用 css sprite,详细请参考css优化中的css sprite部分(HTTP2.0 的话精灵图的优势已经不存在了)
  • 使用 base64 技术
  1. 重定向优化
  • 消除没必要的跳转
  • 利用服务器重写用户键入的链接
  • 使用 HTTP 而不是 js 或者 meta 来重定向
  1. 避免死链/空链/404/410错误
  • 避免出现404 Not Found 错误
  • 避免出现图片请求空链接(img.src=””)
  1. 尽早 flush buffer
  • 尽早的使用 flush buffer 可以让一部分内容先加载出来,提高用户体验
  1. http 中的 character 设置
  • 指定 content-type 和正确的 character 编码

缓存优化

  1. 浏览器缓存

添加 Expires 或 Cache-Control 头

  • 对于静态资源:通过设置一个很远的过期时间来实现“从不过期”

  • 对于动态资源:用一个适当的 Cache-Control 头来帮助浏览器控制请求

    浏览器利用缓存来减少 http 请求的数目和大小,让页面加载的更快。web 服务器利用 http 响应中的 Expires 头来告诉客户端一个资源能够被缓存多久。需要注意的是,如果你利用了一个“不过期”的 Expires 头,你必须在资源改变的同时改变资源的名字。这一技术提高页面性能是基于用户已经访问过你的网站的基础之上的。

  • 设置Last-Modified日期为最近资源需要改变的时间

对动态可用缓存使用“身份识别”

通过链接到资源的唯一url(每次改变资源时同时改变其文件名)来改变缓存

为 IE 设置 Vary 的 header

避免 firefox 中 URL 造成的缓存冲突

firefox 通过 hash 表存储 url 的缓存,但 hash 值仅仅有8个字符,可能会造成 hash 冲突,所以你需要确保你的资源 urldiff 多于8字符边界。

  1. 代理服务器(静态资源服务器)缓存

利用 Cache-control:public 的头可以让资源缓存在一个 web 代理服务器上面来让其他用户使用。

  • 不要在静态资源的 url 上面添加查询字符串
  • 不用在代理服务器上缓存设置了 cookie 的资源
  • 要有使用代理缓存 js 和 css 文件的意识

DNS

  1. 减小DNS解析
  • 固定URL提供资源
  • 尽可能的使用URL路径来代替主机域名,例如 developer.example.com 可以被 www.example.com/developer 代替。除非有技术上的原因需要不同的主机域名。
  • 将从同一主机域名下的需要先加载的 js 文件作为主要文件送达
  • 考虑使用DNS预解析
  1. 增加静态资源域名
  • 实现多个资源的并行下载

服务器负载优化

  1. 使用 CND

CDN 可以帮助用户更快的获取到所需要的资源。

  1. Cookie 优化
  • 使用服务器端的存储为大多数的 cookie 来做有效载荷:在 cookie 里存 key,在服务器端存 value。
  • 移除没有用的或者重复的 cookie
  • 静态资源请求中不要带上 cookie
  • 不要将需要提前加载的 js 放到没有 cookie 的域中加载
  1. 使用 Gzip

Gzip是当前最流行,最有效的压缩方式。

  • 在 http/1.1 中,web 客户端明确支持在 http 请求中 Accept-Encoding 头的 Accept-Encoding: gzip, deflate 压缩方式
  • Gzip 一般能减少服务器响应文件70%的大小,90%的浏览器都支持 gzip
  • 服务器基于文件类型来选择 gzip 压缩,很多网站 gzip 他们的 html 文件,同样也可以 gzip 脚本和样式表
  • 事实上,任何的响应文本,包括 xml 和 json 都有压缩的价值,图片和 pdf 文件不应该被 gizp,因为他们已经被压缩过了
  1. 压缩文件

压缩包括:Javascript、CSS、HTML

让你写的页面能够更有效的压缩:

  • 按照字母排序指定 css 键对值
  • 按照字母排序指定 html 属性
  • 对 html 属性使用一致的引号
  • 使用一致的字母(小写字母)
  • 移除没有用到的css

图片相关优化

  1. 图片压缩:在条件允许的情况下尽量使用 PNG8 格式
  2. 图片缩放:服务器端进行图片缩放

Vue 代码规范

必要的(规避错误)

组件名为多个单词

组件名应该始终是多个单词的,根组件 App 以及 <transition><component> 之类的 Vue 内置组件除外。

这样做可以避免跟现有的以及未来的 HTML 元素相冲突,因为所有的 HTML 元素名称都是单个单词的。

// 反例
Vue.component('todo', {
  // ...
})
export default {
  name: 'Todo',
  // ...
}
// 好例子
Vue.component('todo-item', {
  // ...
})
export default {
  name: 'TodoItem',
  // ...
}

组件数据

组件的 data 必须是一个函数。

当在组件中使用 data property 的时候 (除了 new Vue 外的任何地方),它的值必须是返回一个对象的函数。

// 反例
Vue.component('some-comp', {
  data: {
    foo: 'bar'
  }
})
export default {
  data: {
    foo: 'bar'
  }
}
// 好例子
Vue.component('some-comp', {
  data: function () {
    return {
      foo: 'bar'
    }
  }
})
// In a .vue file
export default {
  data () {
    return {
      foo: 'bar'
    }
  }
}
// 在一个 Vue 的根实例上直接使用对象是可以的,
// 因为只存在一个这样的实例。
new Vue({
  data: {
    foo: 'bar'
  }
})

Prop 定义

Prop 定义应该尽量详细。

在你提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。

// 反例
// 这样做只有开发原型系统时可以接受
props: ['status']
// 好例子
props: {
  status: String
}
// 更好的做法!
props: {
  status: {
    type: String,
    required: true,
    validator: function (value) {
      return [
        'syncing',
        'synced',
        'version-conflict',
        'error'
      ].indexOf(value) !== -1
    }
  }
}

为 v-for 设置键值

总是用 key 配合 v-for。

在组件上总是必须用 key 配合 v-for,以便维护内部组件及其子树的状态。甚至在元素上维护可预测的行为,比如动画中的对象固化 (object constancy),也是一种好的做法。

// 反例
<ul>
  <li v-for="todo in todos">
    {{ todo.text }}
  </li>
</ul>
// 好例子
<ul>
  <li
    v-for="todo in todos"
    :key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>

避免 v-if 和 v-for 用在一起

永远不要把 v-if 和 v-for 同时用在同一个元素上。

一般我们在两种常见的情况下会倾向于这样做:

为了过滤一个列表中的项目 (比如 v-for="user in users" v-if="user.isActive")。在这种情形下,请将 users 替换为一个计算属性 (比如 activeUsers),让其返回过滤后的列表。

为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers")。这种情形下,请将 v-if 移动至容器元素上 (比如 ul、ol)。

// 反例
<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
<ul>
  <li
    v-for="user in users"
    v-if="shouldShowUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
// 好例子
<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
<ul v-if="shouldShowUsers">
  <li
    v-for="user in users"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

为组件样式设置作用域

对于应用来说,顶级 App 组件和布局组件中的样式可以是全局的,但是其它所有组件都应该是有作用域的。

这条规则只和单文件组件有关。你不一定要使用 scoped attribute。设置作用域也可以通过 CSS Modules,那是一个基于 class 的类似 BEM 的策略,当然你也可以使用其它的库或约定。

不管怎样,对于组件库,我们应该更倾向于选用基于 class 的策略而不是 scoped attribute。

这让覆写内部样式更容易:使用了常人可理解的 class 名称且没有太高的选择器优先级,而且不太会导致冲突。

// 反例
<template>
  <button class="btn btn-close">X</button>
</template>

<style>
.btn-close {
  background-color: red;
}
</style>
// 好例子
<template>
  <button class="button button-close">X</button>
</template>

<!-- 使用 `scoped` attribute -->
<style scoped>
.button {
  border: none;
  border-radius: 2px;
}

.button-close {
  background-color: red;
}
</style>
<template>
  <button :class="[$style.button, $style.buttonClose]">X</button>
</template>

<!-- 使用 CSS Modules -->
<style module>
.button {
  border: none;
  border-radius: 2px;
}

.buttonClose {
  background-color: red;
}
</style>
<template>
  <button class="c-Button c-Button--close">X</button>
</template>

<!-- 使用 BEM 约定 -->
<style>
.c-Button {
  border: none;
  border-radius: 2px;
}

.c-Button--close {
  background-color: red;
}
</style>

私有 property 名

使用模块作用域保持不允许外部访问的函数的私有性。如果无法做到这一点,就始终为插件、混入等不考虑作为对外公共 API 的自定义私有 property 使用 _ 前缀。并附带一个命名空间以回避和其它作者的冲突 (比如yourPluginName)。

// 反例
var myGreatMixin = {
  // ...
  methods: {
    update: function () {
      // ...
    }
  }
}
var myGreatMixin = {
  // ...
  methods: {
    _update: function () {
      // ...
    }
  }
}
var myGreatMixin = {
  // ...
  methods: {
    $update: function () {
      // ...
    }
  }
}
var myGreatMixin = {
  // ...
  methods: {
    $_update: function () {
      // ...
    }
  }
}
// 好例子
var myGreatMixin = {
  // ...
  methods: {
    $_myGreatMixin_update: function () {
      // ...
    }
  }
}
// 甚至更好!
var myGreatMixin = {
  // ...
  methods: {
    publicMethod() {
      // ...
      myPrivateFunction()
    }
  }
}

function myPrivateFunction() {
  // ...
}

export default myGreatMixin

强烈推荐 (增强可读性)

组件文件

只要有能够拼接文件的构建系统,就把每个组件单独分成文件。

当你需要编辑一个组件或查阅一个组件的用法时,可以更快速的找到它。

// 反例
Vue.component('TodoList', {
  // ...
})

Vue.component('TodoItem', {
  // ...
})
// 好例子
components/
|- TodoList.js
|- TodoItem.js
components/
|- TodoList.vue
|- TodoItem.vue

单文件组件文件的大小写

单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。

单词大写开头对于代码编辑器的自动补全最为友好,因为这使得我们在 JS(X) 和模板中引用组件的方式尽可能的一致。然而,混用文件命名方式有的时候会导致大小写不敏感的文件系统的问题,这也是横线连接命名同样完全可取的原因。

// 反例
components/
|- mycomponent.vue
components/
|- myComponent.vue
// 好例子
components/
|- MyComponent.vue
components/
|- my-component.vue

基础组件名

应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如 Base、App 或 V。

// 反例
components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue
// 好例子
components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue
components/
|- AppButton.vue
|- AppTable.vue
|- AppIcon.vue
components/
|- VButton.vue
|- VTable.vue
|- VIcon.vue

单例组件名

只应该拥有单个活跃实例的组件应该以 The 前缀命名,以示其唯一性。

这不意味着组件只可用于一个单页面,而是每个页面只使用一次。这些组件永远不接受任何 prop,因为它们是为你的应用定制的,而不是它们在你的应用中的上下文。如果你发现有必要添加 prop,那就表明这实际上是一个可复用的组件,只是目前在每个页面里只使用一次。

// 反例
components/
|- Heading.vue
|- MySidebar.vue
// 好例子
components/
|- TheHeading.vue
|- TheSidebar.vue

紧密耦合的组件名

和父组件紧密耦合的子组件应该以父组件名作为前缀命名。

如果一个组件只在某个父组件的场景下有意义,这层关系应该体现在其名字上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。

// 反例
components/
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue
components/
|- SearchSidebar.vue
|- NavigationForSearchSidebar.vue
// 好例子
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
components/
|- SearchSidebar.vue
|- SearchSidebarNavigation.vue

组件名中的单词顺序

组件名应该以高级别的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。

// 反例
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
// 好例子
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputQuery.vue
|- SearchInputExcludeGlob.vue
|- SettingsCheckboxTerms.vue
|- SettingsCheckboxLaunchOnStartup.vue

自闭合组件

在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。

自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。

不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。

// 反例
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>
<!-- 在 DOM 模板中 -->
<my-component/>
// 好例子
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>

模板中的组件名大小写

对于绝大多数项目来说,在单文件组件和字符串模板中组件名应该总是 PascalCase 的——但是在 DOM 模板中总是 kebab-case 的。

// 反例
<!-- 在单文件组件和字符串模板中 -->
<mycomponent/>
<!-- 在单文件组件和字符串模板中 -->
<myComponent/>
<!-- 在 DOM 模板中 -->
<MyComponent></MyComponent>
// 好例子
<!-- 在单文件组件和字符串模板中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
// 或者

<!-- 在所有地方 -->
<my-component></my-component>

JS/JSX 中的组件名大小写

JS/JSX 中的组件名应该始终是 PascalCase 的。

// 反例
Vue.component('myComponent', {
  // ...
})
import myComponent from './MyComponent.vue'
export default {
  name: 'myComponent',
  // ...
}
export default {
  name: 'my-component',
  // ...
}
// 好例子
Vue.component('MyComponent', {
  // ...
})
Vue.component('my-component', {
  // ...
})
import MyComponent from './MyComponent.vue'
export default {
  name: 'MyComponent',
  // ...
}

完整单词的组件名

组件名应该倾向于完整单词而不是缩写。

编辑器中的自动补全已经让书写长命名的代价非常之低了,而其带来的明确性却是非常宝贵的。不常用的缩写尤其应该避免。

// 反例
components/
|- SdSettings.vue
|- UProfOpts.vue
// 好例子
components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue

Prop 名大小写

在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板和 JSX 中应该始终使用 kebab-case。

// 反例
props: {
  'greeting-text': String
}
<WelcomeMessage greetingText="hi"/>
// 好例子
props: {
  greetingText: String
}
<WelcomeMessage greeting-text="hi"/>

多个 attribute 的元素

多个 attribute 的元素应该分多行撰写,每个 attribute 一行。

在 JavaScript 中,用多行分隔对象的多个 property 是很常见的最佳实践,因为这样更易读。模板和 JSX 值得我们做相同的考虑。

// 反例
<img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/5/171e32206dd8edfa~tplv-t2oaga2asx-image.image" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>
// 好例子
<img
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/5/171e32206dd8edfa~tplv-t2oaga2asx-image.image"
  alt="Vue Logo"
>
<MyComponent
  foo="a"
  bar="b"
  baz="c"
/>

模板中简单的表达式

组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。

复杂表达式会让你的模板变得不那么声明式。我们应该尽量描述应该出现的是什么,而非如何计算那个值。而且计算属性和方法使得代码可以重用。

// 反例
{{
  fullName.split(' ').map(function (word) {
    return word[0].toUpperCase() + word.slice(1)
  }).join(' ')
}}
// 好例子
<!-- 在模板中 -->
{{ normalizedFullName }}
// 复杂表达式已经移入一个计算属性
computed: {
  normalizedFullName: function () {
    return this.fullName.split(' ').map(function (word) {
      return word[0].toUpperCase() + word.slice(1)
    }).join(' ')
  }
}

简单的计算属性

应该把复杂计算属性分割为尽可能多的更简单的 property。

// 反例
computed: {
  price: function () {
    var basePrice = this.manufactureCost / (1 - this.profitMargin)
    return (
      basePrice -
      basePrice * (this.discountPercent || 0)
    )
  }
}
// 好例子
computed: {
  basePrice: function () {
    return this.manufactureCost / (1 - this.profitMargin)
  },
  discount: function () {
    return this.basePrice * (this.discountPercent || 0)
  },
  finalPrice: function () {
    return this.basePrice - this.discount
  }
}

带引号的 attribute 值

非空 HTML attribute 值应该始终带引号 (单引号或双引号,选你 JS 里不用的那个)。

// 反例
<input type=text>
<AppSidebar :style={width:sidebarWidth+'px'}>
// 好例子
<input type="text">
<AppSidebar :style="{ width: sidebarWidth + 'px' }">

指令缩写

指令缩写 (用 : 表示 v-bind:、用 @ 表示 v-on: 和用 # 表示 v-slot:) 。

// 反例
<input
  v-bind:value="newTodoText"
  :placeholder="newTodoInstructions"
>
<input
  v-on:input="onInput"
  @focus="onFocus"
>
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>
// 好例子
<input
  :value="newTodoText"
  :placeholder="newTodoInstructions"
>
<input
  v-bind:value="newTodoText"
  v-bind:placeholder="newTodoInstructions"
>
<input
  @input="onInput"
  @focus="onFocus"
>
<input
  v-on:input="onInput"
  v-on:focus="onFocus"
>
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template v-slot:footer>
  <p>Here's some contact info</p>
</template>
<template #header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>

推荐 (将选择和认知成本最小化)

组件/实例的选项的顺序

组件/实例的选项应该有统一的顺序。

  1. 副作用 (触发组件外的影响)
  • el
  1. 全局感知 (要求组件以外的知识)
  • name
  • parent
  1. 组件类型 (更改组件的类型)
  • functional
  1. 模板修改器 (改变模板的编译方式)
  • delimiters
  • comments
  1. 模板依赖 (模板内使用的资源)
  • components
  • directives
  • filters
  1. 组合 (向选项里合并 property)
  • extends
  • mixins
  1. 接口 (组件的接口)
  • inheritAttrs
  • model
  • props/propsData
  1. 本地状态 (本地的响应式 property)
  • data
  • computed
  1. 事件 (通过响应式事件触发的回调)
  • watch

  • 生命周期钩子 (按照它们被调用的顺序)

    beforeCreate

    created

    beforeMount

    mounted

    beforeUpdate

    updated

    activated

    deactivated

    beforeDestroy

    destroyed

  1. 非响应式的 property (不依赖响应系统的实例 property)
  • methods
  1. 渲染 (组件输出的声明式描述)
  • template/render
  • renderError

元素 attribute 的顺序

元素 (包括组件) 的 attribute 应该有统一的顺序。

  1. 定义 (提供组件的选项)
  • is
  1. 列表渲染 (创建多个变化的相同元素)
  • v-for
  1. 条件渲染 (元素是否渲染/显示)
  • v-if
  • v-else-if
  • v-else
  • v-show
  • v-cloak
  1. 渲染方式 (改变元素的渲染方式)
  • v-pre
  • v-once
  1. 全局感知 (需要超越组件的知识)
  • id
  1. 唯一的 attribute (需要唯一值的 attribute)
  • ref
  • key
  1. 双向绑定 (把绑定和事件结合起来)
  • v-model
  1. 其它 attribute (所有普通的绑定或未绑定的 attribute)

  2. 事件 (组件事件监听器)

  • v-on
  1. 内容 (覆写元素的内容)
  • v-html
  • v-text

组件/实例选项中的空行

// 好例子
props: {
  value: {
    type: String,
    required: true
  },

  focused: {
    type: Boolean,
    default: false
  },

  label: String,
  icon: String
},

computed: {
  formattedValue: function () {
    // ...
  },

  inputClasses: function () {
    // ...
  }
}
// 没有空行在组件易于阅读和导航时也没问题。
props: {
  value: {
    type: String,
    required: true
  },
  focused: {
    type: Boolean,
    default: false
  },
  label: String,
  icon: String
},
computed: {
  formattedValue: function () {
    // ...
  },
  inputClasses: function () {
    // ...
  }
}

单文件组件的顶级元素的顺序

单文件组件应该总是让 <script><template><style> 标签的顺序保持一致。且 <style> 要放在最后,因为另外两个标签至少要有一个。

// 反例
<style>/* ... */</style>
<script>/* ... */</script>
<template>...</template>
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
// 好例子
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>
<!-- ComponentA.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

谨慎使用 (有潜在危险的模式)

没有在 v-if/v-else-if/v-else 中使用 key

// 反例
<div v-if="error">
  错误:{{ error }}
</div>
<div v-else>
  {{ results }}
</div>
// 好例子
<div
  v-if="error"
  key="search-status"
>
  错误:{{ error }}
</div>
<div
  v-else
  key="search-results"
>
  {{ results }}
</div>

scoped 中的元素选择器

元素选择器应该避免在 scoped 中出现。

在 scoped 样式中,类选择器比元素选择器更好,因为大量使用元素选择器是很慢的。

// 反例
<template>
  <button>X</button>
</template>

<style scoped>
button {
  background-color: red;
}
</style>
// 好例子
<template>
  <button class="btn btn-close">X</button>
</template>

<style scoped>
.btn-close {
  background-color: red;
}
</style>

隐性的父子组件通信

应该优先通过 prop 和事件进行父子组件之间的通信,而不是 this.$parent 或变更 prop。

一个理想的 Vue 应用是 prop 向下传递,事件向上传递的。遵循这一约定会让你的组件更易于理解。然而,在一些边界情况下 prop 的变更或 this.$parent 能够简化两个深度耦合的组件。

// 反例
Vue.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  template: '<input v-model="todo.text">'
})
Vue.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  methods: {
    removeTodo () {
      var vm = this
      vm.$parent.todos = vm.$parent.todos.filter(function (todo) {
        return todo.id !== vm.todo.id
      })
    }
  },
  template: `
    <span>
      {{ todo.text }}
      <button @click="removeTodo">
        X
      </button>
    </span>
  `
})
// 好例子
Vue.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  template: `
    <input
      :value="todo.text"
      @input="$emit('input', $event.target.value)"
    >
  `
})
Vue.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  template: `
    <span>
      {{ todo.text }}
      <button @click="$emit('delete')">
        X
      </button>
    </span>
  `
})

非 Flux 的全局状态管理

应该优先通过 Vuex 管理全局状态,而不是通过 this.$root 或一个全局事件总线。

通过 this.$root 和/或全局事件总线管理状态在很多简单的情况下都是很方便的,但是并不适用于绝大多数的应用。

Vuex 是 Vue 的官方类 flux 实现,其提供的不仅是一个管理状态的中心区域,还是组织、追踪和调试状态变更的好工具。它很好地集成在了 Vue 生态系统之中 (包括完整的 Vue DevTools 支持)。

// 反例
// main.js
new Vue({
  data: {
    todos: []
  },
  created: function () {
    this.$on('remove-todo', this.removeTodo)
  },
  methods: {
    removeTodo: function (todo) {
      var todoIdToRemove = todo.id
      this.todos = this.todos.filter(function (todo) {
        return todo.id !== todoIdToRemove
      })
    }
  }
})
// 好例子
// store/modules/todos.js
export default {
  state: {
    list: []
  },
  mutations: {
    REMOVE_TODO (state, todoId) {
      state.list = state.list.filter(todo => todo.id !== todoId)
    }
  },
  actions: {
    removeTodo ({ commit, state }, todo) {
      commit('REMOVE_TODO', todo.id)
    }
  }
}
<!-- TodoItem.vue -->
<template>
  <span>
    {{ todo.text }}
    <button @click="removeTodo(todo)">
      X
    </button>
  </span>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  methods: mapActions(['removeTodo'])
}
</script>

VSCode + ESLint 实践前端编码规范

安装 VSCode ESlint 扩展

首先,打开 VSCode 扩展面板并搜索 ESLint 扩展,然后点击安装

安装完毕之后点击重新加载或退出重新打开以激活扩展,但想要让扩展进行工作,我们还需要先进行 ESLint 的配置。

项目中安装 ESLint

当你使用 Vue 或 React 的脚手架进行构建项目时,会咨询你是否安装 ESLint,如果项目中已经安装了的话,只需更改 .eslintrc.js 文件和 VSCode ESlint 配置即可。

这是我们项目中的 .eslintrc.js 配置:

// http://eslint.org/docs/user-guide/configuring

module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module'
  },
  env: {
    browser: true,
  },
  // https://github.com/standard/standard/blob/master/docs/RULES-en.md
  extends: 'standard',
  // required to lint *.vue files
  plugins: [
    'html'
  ],
  // add your custom rules here
  'rules': {
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0,
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  },
  'globals': {
    'moa': true
  }
}

如果你仅仅想让 ESLint 成为你项目构建系统的一部分,我们可以在项目根目录进行本地安装:

npm install eslint --save-dev

如果想使 ESLint 适用于你所有的项目,我们建议使用全局安装,使用全局安装 ESLint 后,你使用的任何 ESLint 插件或可分享的配置也都必须在全局安装。

这里展示我们使用全局安装:

npm install -g eslint

安装完毕后,我们使用 eslint --init 命令在用户目录中生成一个配置文件(也可以在任何你喜欢的位置进行生成)

我们在第一个选项中选择自定义代码风格,之后根据项目需要自行选择。

设置完成后我们会得到一份文件名为 .eslintrc.js 的配置文件:

// 项目配置不同,初步生成的文件不同,不必纠结
// 采用的是图片中 standard 代码规范
module.exports = {
  env: {
    browser: true,
    es6: true
  },
  extends: [
    'plugin:vue/essential',
    'standard'
  ],
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly'
  },
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module'
  },
  plugins: [
    'vue'
  ],
  rules: {
  }
}

配置 ESLint(了解)

配置文件生成之后,我们接着可以进行自定义修改,这里我们只粗略讲解常用的配置项,完整的可配置项可访问官方文档

配置环境

在上文生成的配置文件中可以使用 env 属性来指定要启用的环境,将其设置为 true,以保证在进行代码检测时不会把这些环境预定义的全局变量识别成未定义的变量而报错:

"env": {
    "browser": true,
    "commonjs": true,
    "es6": true,
    "jquery": true
}

设置语言选项

默认情况下,ESLint 支持 ECMAScript 5 语法,如果你想启用对 ECMAScript 其它版本和 JSX 等的支持,ESLint 允许你使用 parserOptions 属性进行指定想要支持的 JavaScript 语言选项,不过你可能需要自行安装 eslint-plugin-react 等插件。

"parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": {
        "jsx": true
    }
}

配置规则

在上文的配置文件中, "extends": "xxxxxx" 选项表示启用推荐规则,在推荐规则的基础上我们还可以根据需要使用 rules 新增自定义规则,每个规则的第一个值都是代表该规则检测后显示的错误级别:

  • "off" 或 0 - 关闭规则
  • "warn" 或 1 - 将规则视为一个警告
  • "error" 或 2 - 将规则视为一个错误
"rules": {
    "indent": [
        "error",
        4
    ],
    "linebreak-style": [
        "error",
        "windows"
    ],
    "quotes": [
        "error",
        "single"
    ],
    "semi": [
        "error",
        "never"
    ]
}

完整的可配置规则列表可访问:eslint.cn/docs/rules/

其中带 √ 标记的表示该规则为推荐规则。

VSCode 设置 ESLint 扩展

安装并配置完成 ESLint 后,我们继续回到 VSCode 进行扩展设置,依次点击 文件 > 首选项 > 设置 打开 VSCode 配置文件。

从图中可以看到,ESLint 扩展默认已经启用,我们现在只需在 settings.json 中添加配置来指定我们创建的 .eslintrc.js 配置文件路径即可启用自定义规则检测,ESLint 会查找并自动读取它们:

"eslint.options": {
    "configFile": "./.eslintrc.js"
},

至此,我们已经可以使用 ESLint 扩展来检测我们的 js 文件了。

让 ESLint 支持 Vue 单文件组件

由于 ESLint 默认只支持 js 文件的脚本检测,如果我们需要支持类 html 文件(如 vue)的内联脚本检测,还需要安装 eslint-plugin-html 插件。

因为我们使用全局安装了 ESLint,所以 eslint-plugin-html 插件也必须进行全局安装:

 npm install -g eslint-plugin-html

安装完成后,我们再次打开 文件 > 首选项 > 设置,在设置中修改 settings.json 的相关配置并保存:

"eslint.options": {
    "configFile": "./.eslintrc.js",
    "plugins": ["html"]
},
"eslint.validate": [
    "javascript",
    "javascriptreact",
    "html",
    "vue"
]

Git 提交规范

在多人协作的项目中,如果 Git 的提交说明精准,在后期协作以及 Bug 处理时会变得有据可查,项目的开发可以根据规范的提交说明快速生成开发日志,从而方便开发者或用户追踪项目的开发信息和功能特性。下图是 Vue 项目提交说明:

Vue 提交说明

手写符合规范的提交说明很难避免错误,可以借助工具来实现规范的提交说明。

规范的Git提交说明

  • 提供更多的历史信息,方便快速浏览
  • 可以过滤某些commit,便于筛选代码review
  • 可以追踪commit生成更新日志
  • 可以关联issues

Git提交说明结构

Git提交说明可分为三个部分:Header、Body和Footer。

<Header> <Body> <Footer>

Header

Header部分包括三个字段type(必需)、scope(可选)和subject(必需)。

<type>(<scope>): <subject>

type

type用于说明 commit 的提交性质。

描述
feat 新增一个功能
fix 修复一个Bug
docs 文档变更
style 代码格式(不影响功能,例如空格、分号等格式修正)
refactor 代码重构
perf 改善性能
test 测试
build 变更项目构建或外部依赖(例如scopes: webpack、gulp、npm等)
ci 更改持续集成软件的配置文件和package中的scripts命令,例如scopes: Travis, Circle等
chore 变更构建流程或辅助工具
revert 代码回退

scope

scope说明commit影响的范围。scope依据项目而定,例如在业务项目中可以依据菜单或者功能模块划分,如果是组件库开发,则可以依据组件划分。

提示:scope可以省略。

subject

subject是commit的简短描述。

Body

commit的详细描述,说明代码提交的详细说明。

Footer

如果代码的提交是不兼容变更或关闭缺陷,则Footer必需,否则可以省略。

不兼容变更

当前代码与上一个版本不兼容,则Footer以BREAKING CHANGE开头,后面是对变动的描述、以及变动的理由和迁移方法。

关闭缺陷

如果当前提交是针对特定的issue,那么可以在Footer部分填写需要关闭的单个 issue 或一系列issues。

Commitizen

commitizen/cz-cli是一个可以实现规范的提交说明的工具:

提供选择的提交信息类别,快速生成提交说明。

安装cz工具:

npm install -g commitizen

Commitizen适配器

cz-conventional-changelog

如果需要在项目中使用 commitizen 生成符合 AngularJS 规范的提交说明,初始化 cz-conventional-changelog 适配器:

commitizen init cz-conventional-changelog --save --save-exact

如果当前已经有其他适配器被使用,则会报以下错误,此时可以加上 --force 选项进行再次初始化

Error: A previous adapter is already configured. Use --force to override

初始化命令主要进行了3件事情:

  1. 在项目中安装 cz-conventional-changelog 适配器依赖
  2. 将适配器依赖保存到 package.json 的 devDependencies 字段信息
  3. 在 package.json 中新增 config.commitizen 字段信息,主要用于配置cz工具的适配器路径
"devDependencies": {
 "cz-conventional-changelog": "^2.1.0"
},
"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog"
  }
}

接下来可以使用 cz 的命令 git cz 代替 git commit 进行提交说明:

代码提交到远程后,可以在相应的项目中进行查看:

cz-customizable

如果想定制项目的提交说明,可以使用 cz-customizable 适配器:

安装适配器

npm install cz-customizable --save-dev

将之前符合 Angular 规范的 cz-conventional-changelog 适配器路径改成 cz-customizable 适配器路径:

"devDependencies": {
  "cz-customizable": "^5.3.0"
},
"config": {
  "commitizen": {
    "path": "node_modules/cz-customizable"
  }
}

官方提供了一个 .cz-config.js 示例文件 cz-config-EXAMPLE.js,如下所示:

'use strict';

module.exports = {

  types: [
    {value: 'feat',     name: 'feat:     A new feature'},
    {value: 'fix',      name: 'fix:      A bug fix'},
    {value: 'docs',     name: 'docs:     Documentation only changes'},
    {value: 'style',    name: 'style:    Changes that do not affect the meaning of the code\n            (white-space, formatting, missing semi-colons, etc)'},
    {value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
    {value: 'perf',     name: 'perf:     A code change that improves performance'},
    {value: 'test',     name: 'test:     Adding missing tests'},
    {value: 'chore',    name: 'chore:    Changes to the build process or auxiliary tools\n            and libraries such as documentation generation'},
    {value: 'revert',   name: 'revert:   Revert to a commit'},
    {value: 'WIP',      name: 'WIP:      Work in progress'}
  ],

  scopes: [
    {name: 'accounts'},
    {name: 'admin'},
    {name: 'exampleScope'},
    {name: 'changeMe'}
  ],

  // it needs to match the value for field type. Eg.: 'fix'
  /*
  scopeOverrides: {
    fix: [
      {name: 'merge'},
      {name: 'style'},
      {name: 'e2eTest'},
      {name: 'unitTest'}
    ]
  },
  */
  // override the messages, defaults are as follows
  messages: {
    type: 'Select the type of change that you\'re committing:',
    scope: '\nDenote the SCOPE of this change (optional):',
    // used if allowCustomScopes is true
    customScope: 'Denote the SCOPE of this change:',
    subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n',
    body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
    breaking: 'List any BREAKING CHANGES (optional):\n',
    footer: 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n',
    confirmCommit: 'Are you sure you want to proceed with the commit above?'
  },

  allowCustomScopes: true,
  allowBreakingChanges: ['feat', 'fix'],

  // limit subject length
  subjectLimit: 100
  
};

这里对其进行汉化处理(只是为了说明定制说明的一个示例):

'use strict';

module.exports = {

  types: [
    {value: '特性',     name: '特性:    一个新的特性'},
    {value: '修复',      name: '修复:    修复一个Bug'},
    {value: '文档',     name: '文档:    变更的只有文档'},
    {value: '格式',    name: '格式:    空格, 分号等格式修复'},
    {value: '重构', name: '重构:    代码重构,注意和特性、修复区分开'},
    {value: '性能',     name: '性能:    提升性能'},
    {value: '测试',     name: '测试:    添加一个测试'},
    {value: '工具',    name: '工具:    开发工具变动(构建、脚手架工具等)'},
    {value: '回滚',   name: '回滚:    代码回退'}
  ],

  scopes: [
    {name: '模块1'},
    {name: '模块2'},
    {name: '模块3'},
    {name: '模块4'}
  ],

  // it needs to match the value for field type. Eg.: 'fix'
  /*
  scopeOverrides: {
    fix: [
      {name: 'merge'},
      {name: 'style'},
      {name: 'e2eTest'},
      {name: 'unitTest'}
    ]
  },
  */
  // override the messages, defaults are as follows
  messages: {
    type: '选择一种你的提交类型:',
    scope: '选择一个scope (可选):',
    // used if allowCustomScopes is true
    customScope: 'Denote the SCOPE of this change:',
    subject: '短说明:\n',
    body: '长说明,使用"|"换行(可选):\n',
    breaking: '非兼容性说明 (可选):\n',
    footer: '关联关闭的issue,例如:#31, #34(可选):\n',
    confirmCommit: '确定提交说明?'
  },

  allowCustomScopes: true,
  allowBreakingChanges: ['特性', '修复'],

  // limit subject length
  subjectLimit: 100

};

再次使用 git cz 命令进行提交说明:

从上图可以看出此时的提交说明选项已经汉化,继续填写提交说明:

把代码提交到远程:

Commitizen 校验(了解)

commitlint

校验提交说明是否符合规范,安装校验工具commitlint:

npm install --save-dev @commitlint/cli

@commitlint/config-conventional

  • 安装符合Angular风格的校验规则
npm install --save-dev @commitlint/config-conventional 
  • 在项目中新建 commitlint.config.js 文件并设置校验规则:
module.exports = {
  extends: ['@commitlint/config-conventional']
};
  • 安装 huksy(git 钩子工具)
npm install husky --save-dev
  • 在 package.json 中配置 git commit 提交时的校验钩子:
"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }  
}

需要注意,使用该校验规则不能对 .cz-config.js 进行不符合 Angular 规范的定制处理,例如之前的汉化,此时需要将 .cz-config.js 的文件按照官方示例文件 cz-config-EXAMPLE.js 进行符合 Angular 风格的改动。

commitlint 需要配置一份校验规则,@commitlint/config-conventional 就是符合 Angular 规范的一份校验规则。

commitlint-config-cz

如果是使用 cz-customizable 适配器做了破坏 Angular 风格的提交说明配置,那么不能使用 @commitlint/config-conventional 规则进行提交说明校验,可以使用 commitlint-config-cz 对定制化提交说明进行校验。

  • 安装校验规则
npm install commitlint-config-cz --save-dev
  • commitlint 校验规则配置:
module.exports = {
  extends: [
    'cz'
  ]
};

这里推荐使用 @commitlint/config-conventional 校验规则,如果想使用 cz-customizable 适配器,那么定制化的配置不要破坏 Angular 规范即可。

validate-commit-msg

除了使用 commitlint 校验工具,也可以使用 validate-commit-msg 校验工具对cz提交说明是否符合Angular规范进行校验。

Commitizen日志(了解)

如果使用了 cz 工具集,配套 conventional-changelog 可以快速生成开发日志:

npm install conventional-changelog -D

在 package.json 中加入生成日志命令:

"version": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md"

执行npm run version后可查看生产的日志CHANGELOG.md。

注意要使用正确的Header的type,否则生成的日志会不准确。

Vue 项目 Code Review 标准

v-if 和 v-show 区分使用场景

v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

computed 和 watch 区分使用场景

computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

长列表性能优化

Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。

export default {
  data: () => ({
    users: {}
  }),
  async created() {
    const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};

v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  1. v-for 遍历必须为 item 添加 key

在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。

  1. v-for 遍历避免同时使用 v-if

v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。

推荐:

<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
	 return user.isActive
    })
  }
}

不推荐:

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>

事件的销毁

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListener 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:

created() {
  addEventListener('click', this.click, false)
},
beforeDestroy() {
  removeEventListener('click', this.click, false)
}

定时器的清除

如果不是必要的全局定时器的使用,在离开当前组件时一定要将定时器进行清除,避免造成内存泄漏。

图片资源懒加载

对于图片过多的页面,为了加速页面加载速度,我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:

  1. 安装插件
npm install vue-lazyload --save-dev
  1. 在入口文件 man.js 中引入并使用
import VueLazyload from 'vue-lazyload'
  1. Vue 中使用
Vue.use(VueLazyload)
  1. 或添加自定义选项
Vue.use(VueLazyload, {
    preLoad: 1.3,
    error: 'dist/error.png',
    loading: 'dist/loading.gif',
    attempt: 1
})
  1. 在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示:
<img v-lazy="/static/img/1.png">

以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址

路由懒加载

Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。

路由懒加载:

const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
})

第三方插件的按需引入

根据相应组件库/插件文档按需引入即可。

优化无限列表性能

如果应用存在非常长或者无限滚动的列表,那么需要采用窗口化的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 可参考以下开源项目 vue-virtual-scroll-listvue-virtual-scroller 来优化这种无限列表的场景的。

参考文档

前端开发规范:www.w3cschool.cn/webdevelopm…

RSCSS:rscss.io/index.html

前端 JavaScript 规范文档:xuanfengge.com/fedoc/index…

前端性能优化相关编码规范:xuanfengge.com/fedoc/code.…

Vue 风格指南:cn.vuejs.org/v2/style-gu…

凹凸实验室:guide.aotu.io/docs/index.…

使用 VSCode + ESLint 实践前端编码规范:segmentfault.com/a/119000000…

Cz工具集使用介绍 - 规范Git提交说明:juejin.cn/post/684490…

cz-cli:github.com/commitizen/…

Vue 项目性能优化 — 实践指南:juejin.cn/post/684490…

使用 Eslint & standard 规范前端代码:www.cnblogs.com/zhoumingjie…