乐趣区

前端技术演进(六):前端项目与技术实践

这个来自之前做的培训,删减了一些业务相关的,参考了很多资料(参考资料列表),谢谢前辈们,么么哒 ????
任何五花八门的技术,最终还是要在实践中落地。现代的软件开发,大部分讲求的不是高难度高精尖,而是效率和质量。
这里主要来说说现代前端技术在项目中的实践。
开发规范
开发规范是开发工程师之间交流的另一种语言,它在一定程度上决定了代码是否具有一致性和易维护性,统一的开发规范常常可以降低代码的出错概率和团队开发的协作成本。
就拿命名规范来说,如果没有规范,你会经常看到这样的代码:
var a1,a2,temp1,temp2,woshimt;

开发规范制定的重要性不言而喻,使用怎样的规范又成为了另一个问题,因为编程规范并不唯一。通俗地讲,规范的差别很多时候只是代码写法的区别,不同的规范都有各自的特点,大部分没有优劣之分。一般在选择时没必要纠结于使用哪一种规范,只要团队成员都认可并达成一致就行。
实际上,我们平时所说的开发规范更多时候指的是狭义上的编码规范,广义上的开发规范包括实际项目开发中可能涉及的所有规范,如项目技术选型规范、组件规范、接口规范、模块化规范等。由于每个团队使用的项目技术实现不一样,规范也可能千差万别,但无论是哪一种规范,在一个团队中尽可能保持统一。
这里是一个规范的例子:https://guide.aotu.io/docs/index.html
如果使用框架,各个框架会有自己的最佳实践,一般来说参考官方的最佳实践,结合自己团队的习惯即可。
比如 Vue:https://cn.vuejs.org/v2/style-guide/
自动化构建
在现代软件开发中,自动化构建已经成为一个不可缺少的部分。
对于编译型语言来说,一般都会通过命令行或者 IDE 先进行编译,然后在不同平台上安装运行。而前端代码不需要软件编译,Javascript 算是解释型语言,浏览器变解析边执行,所以前端的自动化构建和传统语言略有不同。
前端自动化构建目的
前端构建工具的作用主要是对项目源文件或资源进行文件级处理,将文件或资源处理成需要的最佳输出结构和形式。
在处理过程中,我们可以对文件进行模块化引入、依赖分析、资源合并、压缩优化、文件嵌入、路径替换、生成资源包等多种操作,这样就能完成很多原本需要手动完成的事情,极大地提高开发效率。
前端自动化构建工具
在没有自动化构建工具之前,前端在上线前的处理一般是这样的:

HTML 代码语法检查
HTML 去掉注释
CSS 代码去掉注释,添加版权信息
CSS 代码语法检查
CSS 文件添加兼容性属性
CSS 文件压缩合并
JS 文件语法检查
JS 文件去掉注释,添加版权信息
JS 文件压缩
图片压缩、合并
各个文件名称添加唯一 hash
修改 HTML 文件引用路径
区分线上和开发环境

整个过程每个步骤会用到相应的工具,比如:CSSLint、JSLint、Uglyfy、HTMLMin、CssMinify、imagemin 等,繁琐且浪费时间。
而且还有一些附加的构建要求,比如代码一旦修改就要自动校验,自动测试,刷新浏览器等,这种在几年前基本上无法实现。
渐渐地,出现了一些自动化构建的工具。
Grunt
Grunt 是比较早期的工具,它通过安装插件和配置任务,来执行自动化构建。比如:
module.exports = function(grunt) {

grunt.initConfig({
jshint: {
files: [‘Gruntfile.js’, ‘src/**/*.js’, ‘test/**/*.js’],
options: {
globals: {
jQuery: true
}
}
},
watch: {
files: [‘<%= jshint.files %>’],
tasks: [‘jshint’]
}
});

grunt.loadNpmTasks(‘grunt-contrib-jshint’);
grunt.loadNpmTasks(‘grunt-contrib-watch’);

grunt.registerTask(‘default’, [‘jshint’]);

};
这里就是监控 js 文件的变化,一旦改版,就执行 jshint,也就是语法校验。
Grunt 有很强的生态,但是它运用配置的思想来写打包脚本,一切皆配置,所以会出现比较多的配置项,诸如 option,src,dest 等等。而且不同的插件可能会有自己扩展字段,导致认知成本的提高,运用的时候要搞懂各种插件的配置规则。
Grunt 的速度也比较慢,他是一个任务一个任务依次执行,会有很多 IO 操作。现在基本上用的人比较少了。
Gulp
Gulp 用代码方式来写打包脚本,并且代码采用流式的写法,只抽象出了 gulp.src, gulp.pipe, gulp.dest, gulp.watch 接口,运用相当简单,使用 Gulp 的代码量能比 Grunt 少一半左右。
var gulp = require(‘gulp’);
var pug = require(‘gulp-pug’);
var less = require(‘gulp-less’);
var minifyCSS = require(‘gulp-csso’);
var concat = require(‘gulp-concat’);
var sourcemaps = require(‘gulp-sourcemaps’);

gulp.task(‘html’, function(){
return gulp.src(‘client/templates/*.pug’)
.pipe(pug())
.pipe(gulp.dest(‘build/html’))
});

gulp.task(‘css’, function(){
return gulp.src(‘client/templates/*.less’)
.pipe(less())
.pipe(minifyCSS())
.pipe(gulp.dest(‘build/css’))
});

gulp.task(‘js’, function(){
return gulp.src(‘client/javascript/*.js’)
.pipe(sourcemaps.init())
.pipe(concat(‘app.min.js’))
.pipe(sourcemaps.write())
.pipe(gulp.dest(‘build/js’))
});

gulp.task(‘default’, [ ‘html’, ‘css’, ‘js’]);
Gulp 基于并行执行任务的思想,通过一个 pipe 方法,以数据流的方式处理打包任务,中间文件只生成于内存,不会产生多余的 IO 操作,所以 Gulp 比 Grunt 要快很多。
Webpack

Grunt 和 Gulp 可以算是第一代的自动化构建工具。现在前端主要使用的是 Webpack。
其实对比 Gulp 来说,Webpack 并不是一个完全的替代平,Gulp 是任务运行工具,它只是一个自动执行可重复活动的应用程序,它的用途更加的广泛,因为自动任务的范围更广。
相对 Gulp 来说,Webpack 是一个静态模块打包器 (static module bundler),主要目的是帮助程序模块及其依赖构建静态资源。但是因为前端自动化构建的主要任务其实就是静态资源的构建,所以 Webpack 基本都可以完成。因此 Gulp 现在的使用比较少了。

其实 Webpack 之所以流行,是因为之前的工具对模块化的支持不足,以前的工具大部分是以文件为单位的,而现代 JS 开发,都是基于模块的,模块依赖的识别是需要语法语义分析的,像 Gulp 之类的工具,只是一个自动执行的工具,没法很好的识别所有的模块依赖,所以继续使用会限制书写的方式和项目结构,配置起来也更加繁琐。
Webpack 把所有的代码或图片都当做资源,它会从一个或多个入口文件开始找起,找到所有的资源依赖,然后做语法分析,去除掉不用的或重复的,最终按照配置要求生成处理过的文件。

一个典型的 Webpack 配置文件:
var webpack = require(‘webpack’);
var path = require(‘path’);
var HtmlWebpackPlugin = require(‘html-webpack-plugin’)
var CleanWebpackPlugin = require(‘clean-webpack-plugin’)
var ExtractTextPlugin = require(‘extract-text-webpack-plugin’)
var OptimizeCSSPlugin = require(‘optimize-css-assets-webpack-plugin’)

const VENOR = [
“lodash”,
“react”,
“redux”,
]

module.exports = {
entry: {
bundle: ‘./src/index.js’,
vendor: VENOR
},
// 如果想修改 webpack-dev-server 配置,在这个对象里面修改
devServer: {
port: 8081
},
output: {
path: path.join(__dirname, ‘dist’),
filename: ‘[name].[chunkhash].js’
},
module: {
rules: [{
test: /\.js$/,
use: ‘babel-loader’
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [{
loader: ‘url-loader’,
options: {
limit: 10000,
name: ‘images/[name].[hash:7].[ext]’
}
}]
},
{
test: /\.css$/,
loader: ExtractTextPlugin.extract({
fallback: ‘style-loader’,
use: [{
// 这边其实还可以使用 postcss 先处理下 CSS 代码
loader: ‘css-loader’
}]
})
},
]
},
plugins: [
// 抽取共同代码
new webpack.optimize.CommonsChunkPlugin({
name: [‘vendor’, ‘manifest’],
minChunks: Infinity
}),
// 删除不需要的 hash 文件
new CleanWebpackPlugin([‘dist/*.js’], {
verbose: true,
dry: false
}),
new HtmlWebpackPlugin({
template: ‘index.html’
}),
// 生成全局变量
new webpack.DefinePlugin({
“process.env.NODE_ENV”: JSON.stringify(“process.env.NODE_ENV”)
}),
// 分离 CSS 代码
new ExtractTextPlugin(“css/[name].[contenthash].css”),
// 压缩提取出的 CSS,并解决 ExtractTextPlugin 分离出的 JS 重复问题
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// 压缩 JS 代码
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
};
打包后生成:

Rollup

最近,React,Vue、Ember、Preact、D3、Three.js、Moment 等众多知名项目都使用了 Rollup 这个构建工具。
Rollup 可以使用 ES2015 的语法来写配置文件,而 Webpack 不行:
// rollup.config.js
import babel from ‘rollup-plugin-babel’;

export default {
input: ‘./src/index.js’,
output: {
file: ‘./dist/bundle.rollup.js’,
format: ‘cjs’
},
plugins: [
babel({
presets: [
[
‘es2015’, {
modules: false
}
]
]
})
]
}
// webpack.config.js
const path = require(‘path’);
const webpack = require(‘webpack’);

module.exports = {
entry: {
‘index.webpack’: path.resolve(‘./src/index.js’)
},
output: {
libraryTarget: “umd”,
filename: “bundle.webpack.js”,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: ‘babel-loader’,
query: {
presets: [‘es2015’]
}
}
]
}
};
举个简单的例子,两个文件:
//some-file.js
export default 10;

// index.js
import multiplier from ‘./some-file.js’;

export function someMaths() {
console.log(multiplier);
console.log(5 * multiplier);
console.log(10 * multiplier);
}
通过 Rollup 和 Webpack 打包之后,分别长成下面这样:
// bundle.rollup.js — ~245 bytes

‘use strict’;

Object.defineProperty(exports, ‘__esModule’, { value: true});

var multiplier = 10;

function someMaths() {
console.log(multiplier);
console.log(5 * multiplier);
console.log(10 * multiplier);
}

exports.someMaths = someMaths;
// bundle.webpack.js — ~4108 bytes

module.exports =
/******/ (function(modules) {// webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
………
可以看到 Webpack 打包后的代码基本上不具备可读性,尺寸也有些大。
所以对于主要是给其他人使用的纯 JS 库或框架来说,Rollup 比 Webpack 更适合。
性能优化
前端性能优化是一个很宽泛的概念,不过最终目的都是提升用户体验,改善页面性能。
性能优化是个很有意思的事情,很多人常常竭尽全力进行前端页面优化,但却忽略了这样做的效果和意义。
通常前端性能可以认为是用户获取所需要页面数据或执行某个页面动作的一个实时性指标,一般以用户希望获取数据的操作到用户实际获得数据的时间间隔来衡量。例如用户希望获取数据的操作是打开某个页面,那么这个操作的前端性能就可以用该用户操作开始到屏幕展示页面内容给用户的这段时间间隔来评判。
用户的等待延时可以分成两部分:可控等待延时和不可控等待延时。可控等待延时可以理解为能通过技术手段和优化来改进缩短的部分,例如减小图片大小让请求加载更快、减少 HTTP 请求数等。不可控等待延时则是不能或很难通过前后端技术手段来改进优化的,例如鼠标点击延时、CPU 计算时间延时、ISP (Internet Service Provider,互联网服务提供商)网络传输延时等。前端中的所有优化都是针对可控等待延时这部分来进行的。
前端性能测试
Performance Timing API
Performance Timing API 是一个支持 Internet Explorer9 以上版本及 WebKit 内核浏览器中用于记录页面加载和解析过程中关键时间点的机制,它可以详细记录每个页面资源从开始加载到解析完成这一过程中具体操作发生的时间点,这样根据开始和结束时间戳就可以计算出这个过程所花的时间了。
之前我们介绍 Chrome 网络面板的时候说过一个请求的生命周期:

可以通过 Performance Timing API 捕获到各个阶段的时间,通过计算各个属性的差值来评测性能,比如:
var timinhObj = performance.timing;

DNS 查询耗时:domainLookupEnd – domainLookupStart
TCP 链接耗时:connectEnd – connectStart
request 请求耗时:responseEnd – responseStart
解析 dom 树耗时:domComplete – domInteractive
白屏时间:responseStart – navigationStart
domready 时间:domContentLoadedEventEnd – navigationStart
onload 时间:loadEventEnd – navigationStart
Profile 工具
之前有说过,使用 Chrome 开发者工具的 Audit 面板或者 Performance 面板,可以评估性能。
埋点计时
在关键逻辑之间手动埋点计时,比如:
let timeList = []

timeList.push({tag: ‘xxxBegin’, time: +new Date})

timeList.push({tag: ‘xxxEnd’, time: +new Date})
这种方式常常在移动端页面中使用,因为移动端浏览器 HTML 解析和 JavaScript 执行相对较慢,通常为了进行性能优化,需要找到页面中执行 JavaScript 耗时的操作,如果将关键 JavaScript 的执行过程进行埋点计时并上报,就可以轻松找出 JavaScript 执行慢的地方,并有针对性地进行优化。
资源时序图
可以通过 Chrome 的网络面板,或者 Fiddler 之类的工具查看时序图,来分析页面阻塞:

前端优化策略
前端优化的策略非常多,主要的策略大概可以归为几大类:
网络加载类
减少 HTTP 资源请求次数
在前端页面中,通常建议尽可能合并静态资源图片、JavaScript 或 CSS 代码,减少页面请求数和资源请求消耗,这样可以缩短页面首次访问的用户等待时间。
减小 HTTP 请求大小
应尽量减小每个 HTTP 请求的大小。如减少没必要的图片、JavaScript、CSS 及 HTML 代码,对文件进行压缩优化,或者使用 gzip 压缩传输内容等都可以用来减小文件大小,缩短网络传输等待时延。
将 CSS 或 JavaScript 放到外部文件中,避免使用 script 标签直接引入
在 HTML 文件中引用外部资源可以有效利用浏览器的静态资源缓存。
避免使用空的 href 和 src
当 link 标签的 href 属性为空,或 script、img、iframe 标签的 src 属性为空时,浏览器在渲染的过程中仍会将 href 属性或 src 属性中的空内容进行加载,直至加载失败,这样就阻塞了页面中其他资源的下载进程,而且最终加载到的内容是无效的,因此要尽量避免。
为 HTML 指定 Cache-Control 或 Expires
为 HTML 内容设置 Cache-Control 或 Expires 可以将 HTML 内容缓存起来,避免频繁向服务器端发送请求。前面讲到,在页面 Cache-Control 或 Expires 头部有效时,浏览器将直接从缓存中读取内容,不向服务器端发送请求。比如:
<meta http-equiv=”Cache -Control” content=”max-age=7200″ />
合理设置 Etag 和 Last-Modified
合理设置 Etag 和 Last-Modified 使用浏览器缓存,对于未修改的文件,静态资源服务器会向浏览器端返回 304,让浏览器从缓存中读取文件,减少 Web 资源下载的带宽消耗并降低服务器负载。
减少页面重定向
页面每次重定向都会延长页面内容返回的等待延时,一次重定向大约需要 600 毫秒的时间开销,为了保证用户尽快看到页面内容,要尽量避免页面重定向。
使用静态资源分域存放来增加下载并行数
浏览器在同一时刻向同一个域名请求文件的并行下载数是有限的,因此可以利用多个域名的主机来存放不同的静态资源,增大页面加载时资源的并行下载数,缩短页面资源加载的时间。通常根据多个域名来分别存储 JavaScript、CSS 和图片文件。比如京东:

使用静态资源 CDN 来存储文件
如果条件允许,可以利用 CDN 网络加快同一个地理区域内重复静态资源文件的响应下载速度,缩短资源请求时间。
使用 CDN Combo 下载传输内容
CDN Combo 是在 CDN 服务器端将多个文件请求打包成一个文件的形式来返回的技术,这样可以实现 HTTP 连接传输的一次性复用,减少浏览器的 HTTP 请求数,加快资源下载速度。比如:
//g.alicdn.com/??kissy/k/6.2.4/seed-min.js,tbc/global/0.0.8/index-min.js,tms/tb-init/6.1.0/index-min.js,sea/sitenav-global/0.5.2/global-min.js
使用可缓存的 AJAX
对于返回内容相同的请求,没必要每次都直接从服务端拉取,合理使用 AJAX 缓存能加快 AJAX 响应速度并减轻服务器压力。比如:
const cachedFetch = (url, options) => {
let cacheKey = url

let cached = sessionStorage.getItem(cacheKey)
if (cached !== null) {
let response = new Response(new Blob([cached]))
return Promise.resolve(response)
}

return fetch(url, options).then(response => {
if (response.status === 200) {
let ct = response.headers.get(‘Content-Type’)
if (ct && (ct.match(/application\/json/i) || ct.match(/text\//i))) {
response.clone().text().then(content => {
sessionStorage.setItem(cacheKey, content)
})
}
}
return response
})
}
使用 GET 来完成 AJAX 请求
使用 XMLHttpRequest 时, 浏览器中的 POST 方法发送请求首先发送文件头,然后发送 HTTP 正文数据。而使用 GET 时只发送头部,所以在拉取服务端数据时使用 GET 请求效率更高。
减少 Cookie 的大小并进行 Cookie 隔离
HTTP 请求通常默认带上浏览器端的 Cookie 一起发送给服务器,所以在非必要的情况下,要尽量减少 Cookie 来减小 HTTP 请求的大小。对于静态资源,尽量使用不同的域名来存放,因为 Cookie 默认是不能跨域的,这样就做到了不同域名下静态资源请求的 Cookie 隔离。
缩小 favicon.ico 并缓存
这样有利于 favicon.ico 的重复加载,因为一般一个 Web 应用的 favicon.ico 是很少改变的。
推荐使用异步 JavaScript 资源
异步的 JavaScript 资源不会阻塞文档解析,所以允许在浏览器中优先渲染页面,延后加载脚本执行。比如:
<script src=”main.js” defer></script>
<script src=”main.js” async></script>
使用 async 时,加载和渲染后续文档元素的过程和 main.js 的加载与执行是并行的。使用 defer 时,加载后续文档元素的过程和 main.js 的加载也是并行的,但是 main.js 的执行要在页面所有元素解析完成之后才开始执行。
使用异步 Javascript,加载的先后顺序被打乱,要注意依赖问题。
消除阻塞渲染的 CSS 及 JavaScript
对于页面中加载时间过长的 CSS 或 JavaScript 文件,需要进行合理拆分或延后加载,保证关键路径的资源能快速加载完成。
避免使用 CSS import 引用加载 CSS
CSS 中的 @import 可以从另一个样式文件中引入样式,但应该避免这种用法,因为这样会增加 CSS 资源加载的关键路径长度,带有 @import 的 CSS 样式需要在 CSS 文件串行解析到 @import 时才会加载另外的 CSS 文件,大大延后 CSS 渲染完成的时间。
首屏数据请求提前,避免 JavaScript 文件加载后才请求数据
针对移动端,为了进一步提升页面加载速度,可以考虑将页面的数据请求尽可能提前,避免在 JavaScript 加载完成后才去请求数据。通常数据请求是页面内容渲染中关键路径最长的部分,而且不能并行,所以如果能将数据请求提前,可以极大程度. 上缩短页面内容的渲染完成时间。
首屏加载和按需加载,非首屏内容滚屏加载,保证首屏内容最小化
由于移动端网络速度相对较慢,网络资源有限,因此为了尽快完成页面内容的加载,需要保证首屏加载资源最小化,非首屏内容使用滚动的方式异步加载。一般推荐移动端页面首屏数据展示延时最长不超过 3 秒。目前中国联通 3G 的网络速度为 338KB/s (2.71Mb/s),不能保证客户都是流畅的 4G 网络,所以推荐首屏所有资源大小不超过 1014KB,即大约不超过 1MB。
模块化资源并行下载
在移动端资源加载中,尽量保证 JavaScript 资源并行加载,主要指的是模块化 JavaScript 资源的异步加载,使用并行的加载方式能够缩短多个文件资源的加载时间。
inline 首屏必备的 CSS 和 JavaScript
通常为了在 HTML 加载完成时能使浏览器中有基本的样式,需要将页面渲染时必备的 CSS 和 JavaScript 通过 style 内联到页面中,避免页面 HTML 载入完成到页面内容展示这段过程中页面出现空白。比如百度:
<!Doctype html><html xmlns=http://www.w3.org/1999/xhtml><head>
<meta http-equiv=Content-Type content=”text/html;charset=utf-8″>
<meta http-equiv=X-UA-Compatible content=”IE=edge,chrome=1″>
<meta content=always name=referrer>
<link rel=”shortcut icon” href=/favicon.ico type=image/x-icon>
<link rel=icon sizes=any mask href=//www.baidu.com/img/baidu_85beaf5496f291521eb75ba38eacbd87.svg>
<title> 百度一下,你就知道 </title>
<style id=”style_super_inline”>
body,h1,h2,h3,h4,h5,h6,hr,p,blockquote,dl,dt,dd,ul,ol,li,pre,form,fieldset,legend,button,input,textarea,th,td{margin:0;padding:0}html{color:#000;overflow-y:scroll;overflow:-moz-scrollbars}body,button,input,select,textarea{font:12px arial}

meta dns prefetch 设置 DNS 预解析
设置文件资源的 DNS 预解析,让浏览器提前解析获取静态资源的主机 IP,避免等到请求时才发起 DNS 解析请求。通常在移动端 HTML 中可以采用如下方式完成。
<!– cdn 域名预解析 –>
<meta http-equiv=”x-dns-prefetch-control” content=”on”>
<link rel=”dns-prefetch” href=”//cdn.domain.com”>
资源预加载
对于移动端首屏加载后可能会被使用的资源,需要在首屏完成加载后尽快进行加载,保证在用户需要浏览时已经加载完成,这时候如果再去异步请求就显得很慢。
合理利用 MTU 策略
通常情况下,我们认为 TCP 网络传输的最大传输单元(Maximum Transmission Unit, MTU)为 1500B,即一个 RTT (Round-Trip Time,网络请求往返时间)内可以传输的数据量最大为 1500 字节。因此,在前后端分离的开发模式中,尽量保证页面的 HTML 内容在 1KB 以内,这样整个 HTML 的内容请求就可以在一个 RTT 内请求完成,最大限度地提高 HTML 载入速度。
页面渲染类
把 CSS 资源引用放到 HTML 文件顶部
一般推荐将所有 CSS 资源尽早指定在 HTML 文档中,这样浏览器可以优先下载 CSS 并尽早完成页面渲染。
JavaScript 资源引用放到 HTML 文件底部
JavaScript 资源放到 HTML 文档底部可以防止 JavaScript 的加载和解析执行对页面渲染造成阻塞。由于 JavaScript 资源默认是解析阻塞的,除非被标记为异步或者通过其他的异步方式加载,否则会阻塞 HTML DOM 解析和 CSS 渲染的过程。
不要在 HTML 中直接缩放图片
在 HTML 中直接缩放图片会导致页面内容的重排重绘,此时可能会使页面中的其他操作产生卡顿,因此要尽量减少在页面中直接进行图片缩放。
减少 DOM 元素数量和深度
HTML 中标签元素越多,标签的层级越深,浏览器解析 DOM 并绘制到浏览器中所花的时间就越长,所以应尽可能保持 DOM 元素简洁和层级较少。
尽量避免使用 table、iframe 等慢元素
table 内容的渲染是将 table 的 DOM 渲染树全部生成完并一次性绘制到页面上的,所以在长表格渲染时很耗性能,应该尽量避免使用它,可以考虑使用列表元素 ul 代替。尽量使用异步的方式动态添加 iframe,因为 iframe 内资源的下载进程会阻塞父页面静态资源的下载与 CSS 及 HTML DOM 的解析。
避免运行耗时的 JavaScript
长时间运行的 JavaScript 会阻塞浏览器构建 DOM 树、DOM 渲染树、渲染页面。所以,任何与页面初次渲染无关的逻辑功能都应该延迟加载执行,这和 JavaScript 资源的异步加载思路是一致的。
避免使用 CSS 表达式或 CSS 滤镜
CSS 表达式或 CSS 滤镜的解析渲染速度是比较慢的,在有其他解决方案的情况下应该尽量避免使用。
缓存类
合理利用浏览器缓存
除了上面说到的使用 Cache-Control、Expires、Etag 和 Last-Modified 来设置 HTTP 缓存外,在移动端还可以使用 localStorage 等来保存 AJAX 返回的数据,或者使用 localStorage 保存 CSS 或 JavaScript 静态资源内容,实现移动端的离线应用,尽可能减少网络请求,保证静态资源内容的快速加载。
静态资源离线方案
对于移动端或 Hybrid 应用,可以设置离线文件或离线包机制让静态资源请求从本地读取,加快资源载入速度,并实现离线更新。
图片类
图片压缩处理
在移动端,通常要保证页面中一切用到的图片都是经过压缩优化处理的,而不是以原图的形式直接使用的,因为那样很消耗流量,而且加载时间更长。
使用较小的图片,合理使用 base64 内嵌图片
在页面使用的背景图片不多且较小的情况下,可以将图片转化成 base64 编码嵌入到 HTML 页面或 CSS 文件中,这样可以减少页面的 HTTP 请求数。需要注意的是,要保证图片较小,一般图片大小超过 2KB 就不推荐使用 base64 嵌入显示了。
使用更高压缩比格式的图片
使用具有较高压缩比格式的图片,如 webp 等。在同等图片画质的情况下,高压缩比格式的图片体积更小,能够更快完成文件传输,节省网络流量。不过注意 webp 的兼容性,除了 Chrome 其他浏览器支持不好。
图片懒加载
为了保证页面内容的最小化,加速页面的渲染,尽可能节省移动端网络流量,页面中的图片资源推荐使用懶加载实现,在页面滚动时动态载入图片。比如京东首页滚动。
使用 Media Query 或 srcset 根据不同屏幕加载不同大小图片
介绍响应式时说过,针对不同的移动端屏幕尺寸和分辨率,输出不同大小的图片或背景图能保证在用户体验不降低的前提下节省网络流量,加快部分机型的图片加载速度,这在移动端非常值得推荐。
使用 iconfont 代替图片图标
在页面中尽可能使用 iconfont 来代替图片图标,这样做的好处有以下几个:使用 iconfont 体积较小,而且是矢量图,因此缩放时不会失真;可以方便地修改图片大小尺寸和呈现颜色。
但是需要注意的是,iconfont 引用不同 webfont 格式时的兼容性写法,根据经验推荐尽量按照以下顺序书写,否则不容易兼容到所有的浏览器上。
@font-face {
 font-family: iconfont;
 src: url(“./iconfont.eot”) ;
 src: url(“./iconfont.eot?#iefix”) format(“eot”),
      url(“./iconfont.woff”) format(“woff”),
      url(“./iconfont.ttf”) format(“truetype”);
}
定义图片大小限制
加载的单张图片一般建议不超过 30KB,避免大图片加载时间长而阻塞页面其他资源的下载,因此推荐在 10KB 以内。如果用户,上传的图片过大,建议设置告警系统。
脚本类
脚本类涉及到代码的优化,这里只简单列一些:

尽量使用 id 选择器
合理缓存 DOM 对象
页面元素尽量使用事件代理,避免直接事件绑定
使用 touchstart 代替 click
避免 touchmove、scroll 连续事件处理,设置事件节流
推荐使用 ECMAScript 6 的字符串模板连接字符串
尽量使用新特性

渲染类
使用 Viewport 固定屏幕渲染,可以加速页面渲染内容
在移动端设置 Viewport 可以加速页面的渲染,同时可以避免缩放导致页面重排重绘。比如:
<meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0,
user-scalable=no”>
避免各种形式重排重绘
页面的重排重绘很耗性能,所以一定要尽可能减少页面的重排重绘。
使用 CSS3 动画,开启 GPU 加速
使用 CSS3 动画时可以设置 transform: translateZ(0) 来开启移动设备浏览器的 GPU 图形处理,加速,让动画过程更加流畅。
合理使用 Canvas 和 requestAnimationFrame
选择 Canvas 或 requestAnimationFrame 等更高效的动画实现方式,尽量避免使用 setTimeout、setInterval 等方式来直接处理连续动画。
SVG 代替图片
部分情况下可以考虑使用 SVG 代替图片实现动画,因为使用 SVG 格式内容更小,而且 SVG DOM 结构方便调整。
不滥用 float
在 DOM 渲染树生成后的布局渲染阶段,使用 float 的元素布局计算比较耗性能,推荐使用固定布局或 flex-box 弹性布局的方式来实现页面元素布局。
架构协议类
尝试使用 SPDY 和 HTTP 2
在条件允许的情况下可以考虑使用 SPDY 协议来进行文件资源传输,利用连接复用加快传输过程,缩短资源加载时间。HTTP2 在未来也是可以考虑尝试的。
使用后端数据渲染
使用后端数据渲染的方式可以加快页面内容的渲染展示,避免空白页面的出现,同时可以解决移动端页面 SEO 的问题。如果条件允许,后端数据渲染是一个很不错的实践思路。
使用 NativeView 代替 DOM 的性能劣势
可以尝试使用 Native View 等来避免 HTML DOM 性能慢的问题,目前使用 React Native、Weex 等已经可以将页面内容渲染体验做到接近客户端 Native 应用的体验了。
这里列举了一部分优化的策略,世界上没有十全十美的事情,在做到了极致优化的同时也会付出很大的代价,这也是前端优化的一个问题。理论上这些优化都是可以实现的,但是作为工程师,要懂得权衡。优化提升了用户体验,使数据加载更快,但是项目代码却可能打乱,异步内容要拆分出来,首屏的一个雪碧图可能要分成两个,页面项目代码的数量和维护成本可能成倍增加,项目结构也可能变得不够清晰。
任何一部分优化都可以做得很深入,但不一定都值得,在优化的同时也要尽量考虑性价比,这才是处理前端优化时应该具有的正确思维。
用户数据分析
在现代互联网产品的开发迭代中,对前端用户数据的统计分析严重影响着最终产品的成败。谈到前端数据,涉及的方面就比较广了。网站用户数据统计分析通常可以反映出网站的用户规模、用户使用习惯、用户的内容偏好等,了解了这些就能帮助我们调整产品策略、改进产品需求、提高产品质量,除此之外用户数据的统计甚至也会直接和广告收入相关联。
用户访问统计
通常页面上用户访问统计主要包括 PV(Page View)、UV(Unique Visitor)、VV(Visit View)、IP(访问站点的不同 IP 数)等。
PV
PV 一般指在一天时间之内页面被所有用户访问的总次数,即每一次页面刷新都会增加一次 PV。PV 作为单个页面的统计量参数,通常用来统计获取关键入口页面或临时推广性页面的访问量或推广效果,由于 PV 的统计一般是不做任何条件限制的,可以人为地刷新来提升统计量,所以单纯靠 PV 是无法反应页面被用户访问的具体情况的。
UV
UV 是指在一天时间之访问内页的不同用户个数,和 PV 不同的是,如果一个页面在同一天内被某个相同用户多次访问,只计算一次 UV。
UV 可以认为是前端页面统计中一个最有价值的统计指标,因为其直接反应页面的访问用户数。目前有较多站点的 UV 是按照一天之内访问目标页面的 IP 数来计算的,因此也可以根据 UV 来统计站点的周活跃用户量和月活跃用户量。
严格来讲,根据一天时间内访问目标页面的 IP 数来计算 UV 是不严谨的,因为在办公区或校园局域网的情况下,多个用户访问互联网网站的 IP 可能是同一个,但实际上的访问用户却有很多。所以为了得到更加准确的结果,除了根据 IP,还需要结合其他的辅助信息来识别统计不同用户的 UV,比如有两种常用的方式:

根据浏览器 Cookie 和 IP 统计:在目标页面每次打开时向浏览器中写入唯一的某个 Cookie 信息,再结合 IP 一起上报统计,就可以精确统计出一天时间内访问页面的用户数。存在的问题是如果用户手动清除了 Cookie 再进入访问,页面被重新访问时就只能算第二次。
结合用户浏览器标识 userAgent 和 IP 统计:由于使用 Cookie 统计存在可能被手动清除的问题,所以推荐结合浏览器标识 userAgent 来统计。这样可以在一定程度上区分同 IP 下的不同用户,但也不完全准确,IP 和浏览器标识 userAgent 相同的情况也很常见,但仍却只能计算一次。

由此可见,虽然 UV 是网站统计的一个很重要的统计量,但一般情况下是无法用于精确统计的,所以通常需要结合 PV、UV 来一起分析网站被用户访问的情况。此外,我们还可以对站点一天的新访客数、新访客比率等进行统计,计算第一次访问网站的新用户数和比例,这对判断网站用户增长也是很有意义的。
VV
PV 和 UV 更多是针对单页面进行的统计,而 VV 则是用户访问整个网站的统计指标。例如用户打开站点,并在内部做了多次跳转操作,最后关闭该网站所有的页面,即为一次 VV。
IP
IP 是一天时间内访问网页或网站的独立 IP 数,一般服务器端可以直接获取用户访问网站时的独立 IP,统计也比较容易处理。需要注意 IP 统计与 UV 统计的区别和联系。
用户行为分析
对于较小的项目团队来说,或许得到页面或网站的 PV、UV、VV、IP 这些基本的统计数据就可以了。其实相对于访问量的统计,用户行为分析才是更加直接反映网页内容是否受用户喜欢或满足用户需求的一个重要标准,用户在页面上操作的行为有很多种,每种操作都可能对应页面上不同的展示内容。如果我们能知道用户浏览目标页面时所有的行为操作,一定程度上就可以知道用户对页面的哪些内容感兴趣,对哪些内容不感兴趣,这对产品内容的调整和改进是很有意义的。一般用于分析用户行为的参数指标主要包括:页面点击量、用户点击流、用户访问路径、用户点击热力图、用户转换率、用户访问时长分析和用户访问内容分析等。
页面点击量
页面点击量用来统计用户对于页面某个可点击或可操作区域的点击或操作次数。以点击的情况为例,统计页面上某个按钮被点击的次数就可以通过该方法来计算,这样通过统计的结果可以分析出页面上哪些按钮对应的内容是用户可能感兴趣的。
用户点击流分析
点击流用来统计用户在页面中发生点击或操作动作的顺序,可以反映用户在页面上的操作行为。所以统计上报时需要在浏览器上先保存记录用户的操作顺序,例如在关键的按钮中埋点,点击时向 localStorage 中记录点击或操作行为的唯一 id,在用户一次 VV 结束或在下一次 VV 开始时进行点击流上报,然后通过后台归并统计分析。
用户访问路径分析
用户访问路径和用户点击流有点类似,不过用户访问路径不针对用户的可点击或操作区域埋点,而是针对每个页面埋点记录用户访问不同页面的路径。上报信息的方法和用户点击流上报相同,常常也是在一次 VV 结束或下一次 VV 开始时,上报用户的访问路径。

用户点击热力图
用户点击热力图是为了统计用户的点击或操作发生在整个页面哪些区域位置的一种分析方法,一般是统计用户操作习惯和页面某些区域内容是否受用户关注的一种方式。

这种统计方法获取上报点的方式主要是捕获鼠标事件在屏幕中的坐标位置进行上报,然后在服务端进行计算归类分析并绘图。
用户转化率与导流转化率
对用户转化率的分析一 般在一些临时推广页面或拉取新用户宜传页面上比较常用,这里统计也很简单,例如要统计某个新产品推广页面的用户转化率,通过计算经过该页面注册的用户数相对于页面的 PV 比例就可以得出。
用户转化率 = 通过该页面注册的用户数 / 页面 PV
相对来说,用户转化率分析的应用场景比较单一。还有另一种导流的页面统计分析和该页面的功能类似,不过其作用是将某个页面的用户访问流量引导到另一个页面中,导流转化率可以用通过源页面导入的页面访问 PV 相对于源页面的总 PV 比例来表示。
导流转化率 = 通过源页面导入的页面访问 PV / 源页面 PV
本质上,关键的统计分析仍是对现有页面访问量进行对比和计算而得出的,并不是统计出来的。
用户访问时长、内容分析
用户访问时长和内容分析则是统计分析用户在某些关键内容页面的停留时间,来判断用户对该页面的内容是否感兴趣,从而分析出用户对网站可能感兴趣的内容,方便以后精确地向该用户推荐他们感兴趣的内容。
前端日志上报
后端开发一般在程序运行出现异常时可以通过写服务器日志的方式来记录错误的信息,然后下载服务器日志打开查看是哪里的问题并进行修复。但是如果是前端页面运行出现了问题,我们却不能打开用户浏览器的控制台记录来查看代码中到底出现了什么错误。
一般情况下,在前端开发中,前端工程师按照需求完成页面开发,通过产品体验确认和测试,页面就可以上线了。但不幸的是,产品很快就收到了用户的投诉。用户反映页面点击按钮没反应,我们自己试了一下却一切正常,于是追问用户所用的环境,最后结论是用户使用了一个非常小众的浏览器打开页面,因为该浏览器不支持某个特性,因此页面报错,整个页面停止响应。在这种情况下,用户反馈的投诉花掉了我们很多时间去定位问题,然而这并不是最可怕的,更让我们担忧的是更多的用户遇到这种场景后便会直接抛弃这个有问题的“垃圾产品”。
这个问题唯一的解决办法就是在尽量少的用户遇到这样的场景时就把问题即时修复掉,保证尽量多的用户可以正常使用。首先需要在少数用户使用产品出错时知道有用户出错,而且尽量定位到是什么错误。由于用户的运行环境是在浏览器端的,因此可以在前端页面脚本执行出错时将错误信息上传到服务器,然后打开服务器收集的错误信息进行分析来改进产品的质量。要实现这个过程,我们必须考虑下面几个问题。
怎样获取错误日志
浏览器提供了 try.. .catch 和 window. onerror 的两种机制来帮助我们获取用户页面的脚本错误信息。
window.onerror = function (msg, url, lineNo, columnNo, error) {
// … handle error …

return false;
}
怎样将错误信息上传到服务器
如果捕获到了具体的错误或栈信息,就可以将错误信息进行上报了,如出错信息、错误行号、列号、用户浏览器信息等,通过创建 HTTP 请求的方式即可将它们发送到日志收集服务器。当然错误信息上报设计时需要注意一点:页面的访问量可能很大,如果到达百万级、千万级,那么就需要按照一定的条件上报,例如根据一定的概率进行上报,否则大量的错误信息上报请求会占用日志收集服务器的很多资源和流量。
怎样通过高效的方式来找到问题
为了方便查看收集到的这些信息,我们通常可以建立一个简单的内容管理系统(Content Management System,CMS)来管理查看错误日志,对同一类型的错误做归并统计,也可以建立错误量实时统计来查看错误量的即时变化情况。当某个版本发布后,如果收到的错误量明显增加,就需要格外注意。另外一点要注意的是,上报错误信息机制是用来辅助产品质量改进的,不能因为在页面中添加了错误信息收集和上报而影响了原有的业务模块功能。
文件加载失败监控
如果要进一步完善地检测页面的异常信息,可以尝试对静态资源文件加载失败的情况进行监控。例如在 CDN 网络中,可能因为部分机器故障,导致用户加载不到 <img>、<script> 等静态资源,但是开发者不一定能复现,而且无法第一时间知道静态资源加载失败了。这种情况下这就需要在页面上自动捕获文件加载失败的异常来进行处理,可以对 <img> 或 <script> 标签元素的 readyChange 进行是否加载成功的判断。不幸的是,只有部分 IE 浏览器支持 <img> 或 <script> 的 readyState,因此一般还需要结合其他方式,如 onload,针对不同浏览器分开处理。
前端性能分析上报
开发者怎样知道用户端打开页面时的性能如何呢,一个可行的方法就是将页面性能数据进行上报统计,例如将 PerformanceTiming 数据、开发者自己埋点的性能统计数据通过页面 JavaScript 统一上报到远程服务器,在服务器端统计计算性能数据的平均值来评判前端具体页面的性能情况。
以上介绍的是前端页面数据统计和分析的主要内容,在实际项目中可以根据产品或开发需要来进行调整。需要注意的是,不要过度设计,例如对于访问量很少的网站进行大量的用户行为分析可能就得不偿失了。
搜索引擎优化
搜索引擎优化简称 SEO。对于很多网站来说,搜索引擎是最重要的入口,提升自然排名相当于提升网站的曝光度,作为前端工程师,了解搜索引擎优化方面的相关知识是很重要的。
title、keywords、description 的优化
title. keywords、description 是可以在 HTML 的 <meta> 标签内定义的,有助于搜索引擎抓取到网页的内容。要注意的是,一般 title 的权重是最高的,也是最重要的。keywords 相对权重较低,可以作为页面的辅助关键词搜索。description 的描述一般会直接显示在搜索结果的介绍中,可以使用户快速了解页面内容的描述文字,所以要尽量让这段文字能够描述整个页面的内容,增加用户进入页面的概率。
title 的优化
一般 title 的设置要尽量能够概括页面的内容,可以使用多个 title 关键字组合的形式,并用分隔符连接起来。分隔符一般有“_”、“-”、“”、“,”等,其中“_”分隔符比较容易被百度搜索引擎检索到,“-”分隔符则容易被谷歌搜索引擎检索到,“,”则在英文站点中使用比较多,可以使用空格。
title 的长度在桌面浏览器端一般建议控制在 30 个字以内,在移动端控制在 20 个字以内,若长度超出时浏览器会默认截断并显示省略号。
关于 title 格式的优化设置可以遵循以下规则:

每个网页都应该有独一无二的标题,切忌所有的页面都使用同样的默认标题。
标题主题明确,应该包含网页中最重要的信息。
简明精练,不应该罗列与网页内容不相关的信息。
用户浏览通常从左到右的,建议将重要的内容放到 title 靠前的位置。
使用用户所熟知的语言描述,如果有中、英文两种网站名称,尽量使用用户熟知的语言作为标题描述。

对于网站不同页面 title 的定义可以设置如下:

首页:网站名称提供服务介绍或产品介绍
列表页:列表名称_网站名称
文章页:文章标题_文章分类_网站名称
如果文章标题不是很长,还可以增加部分关键词来提高网页的检索量,如文章 title_ 关键词_网站名称

例如某个博客的名称为极限前端,那么其首页的 title 就可以如下编写:
<!– 不好的 title 设置 –>
<title> 极限前端 </title>
<title> 极限前端_ front end</title>

<!– 良好的 title 设置 –>
<title> 极限前端_首页_ 前端技术知识_某某某的博客 </title>
keywords 的优化
keywords 是目前用于页面内容检索的辅助关键字信息,容易被搜索引擎检索到,所以恰当的设置页面 keywords 内容对于页面的 SEO 也是很重要的,而且 keywords 本身的使用也比较简单。
description 优化
在搜索引擎检索结果中,description 更重要的作用是作为搜索结果的描述,而不是作为权值计算的重要参考因素。description 的长度在桌面浏览器页面中一般为 78 个中文字符,移动端为 50 个,超过则会自动截断并显示省略号。如下定义 title、keywords、description 比较合适:
<!– 不好的 title. keywords、description 优化设置 –>
<title> 极限前端 </title>
<meta name=”keywords” content=” 极限前端 ”>
<meta name=”description” content=” 极限前端 ”>

<!– 良好的 title. keywords、description 优化设置 –>
<title> 前端搜索引擎优化基础_极限前端_前端技术知识_某某某的博客 </title>
<meta name=”keywords” content=” 现代前端技术, 前端页面 SEO 优化, 极限前端, 某某某的博客 ”>
<meta name=”description” content=” 本章讲述了前端搜索引擎优化基础实践技术。”>
语义化标签的优化
title、keywords. description 的设置对页面 SEO 具有重要意义,但除了页面 title、keywords、description 外,还有页面结构语义化设计,因为搜索引擎分析页面内容时可以解析语义化的标签来获取内容,并赋予相关的权重,因此语义化结构的页面就比全部为 <div> 标签元素布局的页面更容易被检索到。
使用具有语义化的 HTML5 标签结构
如果页面兼容性条件允许,尽量使用 HTML5 语义化结构标签。使用 <header>、<nav>、<aside>、<article>、<footer> 等标签增加页面的语义化内容,可以让搜索引擎更容易获取页面的结构内容。

唯一的 H1 标题
建议每个页面都有一个唯一的 <h1> 标题,但一般 <h1> 内容并不是网站的标题。<h1> 作为页面最高层级的标题能够更容易被搜索引擎收录,并赋予页面相对较高权重的内容描述。一般设置首页的 <h1> 标题为站点名称,其他内页的 <h1> 标题则可以为各个内页的标题,如分类页用分类的名字、详情页用详情页标题等。
因为 SEO 的需要,应该尽量保证搜索引擎抓取到的页面是有内容的,但是以 AJAX 技术实现的 SPA 应用在 SEO 上不具有优势,因此要尽量避免这样的页面实现方式。
<img> 添加 alt 属性
一般要求 <img> 标签必须设置 alt 属性,这样更有利于搜索引擎检索出图片的描述信息。
URL 规范化
统一网站的地址链接:
http://www.domain.comhttp://domain.comhttp://www.domain.com/index.htmlhttp://domain. com/index.html
以上四个地址都可以表示跳转到同一个站点的首页,虽然不会对用户访问造成什么麻烦,但对于搜索引擎来说是四条网址并且内容相同。这种情况有可能会被搜索引擎误认为是作弊手段,另外当搜索引擎要规范化网址时,需要从这些选择中挑一个作为代表,但是挑的这个不一定是最好的,因此我们最好统一搜索引擎访问页面的地址,否则可能影响网站入口搜索结果的权重。
301 跳转
如果 URL 发生改变,一定要使旧的地址 301 指向新的页面,否则搜索引擎会把原有的这个 URL 当作死链处理,之前完成的页面内容收录权重的工作就都失效了。
canonical
当该页面有不同参数传递的时候,标签属性也可以起到标识页面唯一性的作用,例如以下三个地址。
domain.com/index.htmldomain.com/index.html?from=123domain.com/index.html?from=456
在搜索引擎中,以上三个地址分别表示三个页面,但其实后面两个一般表示页面跳转的来源,所以为了确保这三个地址为同一个页面,往往在 <head> 上加上 canonical 声明,告诉搜索引擎在收录页面时可以按照这个 href 提供的页面地址去处理,而不是将每个地址都独立处理。
<link rel=”cononical” href=”//:domain. com/index.html” />
robots
robots.txt 是网站站点用来配置搜索引擎抓取站点内容路径的一种控制方式,放置于站点根目录下。搜索引擎爬虫访问网站时会访问 robots.txt 文件,robots.txt 可以指导搜索引擎爬虫禁止抓取网站某些内容或只允许抓取哪些内容,这就保证了搜索引擎不抓取站点中临时或不重要的内容,保证网站的主要内容被搜索引擎收录。
sitemap
sitemap 格式一般分为 HTML 和 XML 两种,命名可以为 sitemap.html 或 sitemap.xml,作用是列出网站所有的 URL 地址,方便搜索引擎去逐个抓取网站的页面,增加网站页面在搜索引擎中的的曝光量。
关于 SEO 的内容有很多,这里只是简单提了一些实际开发中可能涉及的部分。关于内外链、权重、内容结构、内容建设等和编码基本没啥关系的,就没有说了。
前端协作
前端技术涉及 UI 界面、数据展示、用户交互等实现,因此不可避免地要和团队其他成员进行协作沟通,如产品经理、UI 设计师、交互设计师、后台工程师、运维工程师等。前端主要协作的内容有:
和产品经理:主要关注需求是否明确,技术方案是否可行,需求性价比是否高,是否有简单的可接受的替代方案,需求变更的影响等。
和后端工程师:主要关注数据接口定义,线上问题定位,接口调试等。
和 UI 设计师:主要关注设计图是否容易实现,使用什么样的组件,操作过程中的交互和动画效果等。
和运维工程师:主要关注如何上线,环境配置等。
注意在协作过程中不要殴打同事。

退出移动版