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

3,398 阅读45分钟
这个来自之前做的培训,删减了一些业务相关的,参考了很多资料(参考资料列表),谢谢前辈们,么么哒 😘

任何五花八门的技术,最终还是要在实践中落地。现代的软件开发,大部分讲求的不是高难度高精尖,而是效率和质量。

这里主要来说说现代前端技术在项目中的实践。

开发规范

开发规范是开发工程师之间交流的另一种语言,它在一定程度上决定了代码是否具有一致性和易维护性,统一的开发规范常常可以降低代码的出错概率和团队开发的协作成本。

就拿命名规范来说,如果没有规范,你会经常看到这样的代码:

var a1,a2,temp1,temp2,woshimt;

image.png | center | 640x356

开发规范制定的重要性不言而喻,使用怎样的规范又成为了另一个问题,因为编程规范并不唯一。通俗地讲,规范的差别很多时候只是代码写法的区别,不同的规范都有各自的特点,大部分没有优劣之分。一般在选择时没必要纠结于使用哪一种规范, 只要团队成员都认可并达成一致就行。

实际上,我们平时所说的开发规范更多时候指的是狭义上的编码规范,广义上的开发规范包括实际项目开发中可能涉及的所有规范,如项目技术选型规范、组件规范、接口规范、模块化规范等。由于每个团队使用的项目技术实现不一样,规范也可能千差万别,但无论是哪一种规范, 在一个团队中尽可能保持统一。

这里是一个规范的例子:guide.aotu.io/docs/index.…

如果使用框架,各个框架会有自己的最佳实践,一般来说参考官方的最佳实践,结合自己团队的习惯即可。

比如Vue:cn.vuejs.org/v2/style-gu…

自动化构建

在现代软件开发中,自动化构建已经成为一个不可缺少的部分。

对于编译型语言来说,一般都会通过命令行或者IDE先进行编译,然后在不同平台上安装运行。而前端代码不需要软件编译,Javascript算是解释型语言,浏览器变解析边执行,所以前端的自动化构建和传统语言略有不同。

前端自动化构建目的

前端构建工具的作用主要是对项目源文件或资源进行文件级处理,将文件或资源处理成需要的最佳输出结构和形式。

在处理过程中,我们可以对文件进行模块化引入、依赖分析、资源合并、压缩优化、文件嵌入、路径替换、生成资源包等多种操作,这样就能完成很多原本需要手动完成的事情,极大地提高开发效率。

前端自动化构建工具

在没有自动化构建工具之前,前端在上线前的处理一般是这样的:

  1. HTML代码语法检查
  2. HTML去掉注释
  3. CSS代码去掉注释,添加版权信息
  4. CSS代码语法检查
  5. CSS文件添加兼容性属性
  6. CSS文件压缩合并
  7. JS文件语法检查
  8. JS文件去掉注释,添加版权信息
  9. JS文件压缩
  10. 图片压缩、合并
  11. 各个文件名称添加唯一hash
  12. 修改HTML文件引用路径
  13. 区分线上和开发环境

整个过程每个步骤会用到相应的工具,比如: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

image.png | center | 827x444

Grunt 和 Gulp 可以算是第一代的自动化构建工具。现在前端主要使用的是 Webpack。

其实对比 Gulp 来说,Webpack 并不是一个完全的替代平,Gulp 是任务运行工具,它只是一个自动执行可重复活动的应用程序,它的用途更加的广泛,因为自动任务的范围更广。

相对Gulp来说, Webpack是一个静态模块打包器(static module bundler),主要目的是帮助程序模块及其依赖构建静态资源。但是因为前端自动化构建的主要任务其实就是静态资源的构建,所以Webpack基本都可以完成。因此 Gulp 现在的使用比较少了。

image.png | center | 827x414

其实 Webpack 之所以流行,是因为之前的工具对模块化的支持不足,以前的工具大部分是以文件为单位的,而现代JS开发,都是基于模块的,模块依赖的识别是需要语法语义分析的,像 Gulp 之类的工具,只是一个自动执行的工具,没法很好的识别所有的模块依赖,所以继续使用会限制书写的方式和项目结构,配置起来也更加繁琐。

Webpack 把所有的代码或图片都当做资源,它会从一个或多个入口文件开始找起,找到所有的资源依赖,然后做语法分析,去除掉不用的或重复的,最终按照配置要求生成处理过的文件。

image.png | center | 576x566

一个典型的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
      }
    })
  ]
};

打包后生成:

image.png | center | 720x186

Rollup

image.png | center | 827x414

最近,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 网络面板的时候说过一个请求的生命周期:

image.png | center | 827x494

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

var timinhObj = performance.timing;

image.png | left | 386x371

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 之类的工具查看时序图,来分析页面阻塞:

image.png | center | 585x366

前端优化策略

前端优化的策略非常多,主要的策略大概可以归为几大类:

网络加载类

减少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和图片文件。比如京东:

image.png | center | 827x513

使用静态资源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开始时,上报用户的访问路径。

image.png | center | 827x601

用户点击热力图

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

image.png | center | 395x435

这种统计方法获取上报点的方式主要是捕获鼠标事件在屏幕中的坐标位置进行上报,然后在服务端进行计算归类分析并绘图。

用户转化率与导流转化率

对用户转化率的分析一 般在一些临时推广页面或拉取新用户宜传页面上比较常用,这里统计也很简单,例如要统计某个新产品推广页面的用户转化率,通过计算经过该页面注册的用户数相对于页面的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>等标签增加页面的语义化内容,可以让搜索引擎更容易获取页面的结构内容。

image.png | center | 640x480

唯一的H1标题

建议每个页面都有一个唯一的<h1>标题, 但一般<h1>内容并不是网站的标题。<h1>作为页面最高层级的标题能够更容易被搜索引擎收录,并赋予页面相对较高权重的内容描述。一般设置首页的<h1>标题为站点名称,其他内页的<h1>标题则可以为各个内页的标题,如分类页用分类的名字、详情页用详情页标题等。

因为SEO的需要,应该尽量保证搜索引擎抓取到的页面是有内容的,但是以AJAX技术实现的SPA应用在SEO上不具有优势,因此要尽量避免这样的页面实现方式。

添加alt属性

一般要求<img>标签必须设置 alt属性,这样更有利于搜索引擎检索出图片的描述信息。

URL规范化

统一网站的地址链接:

http://www.domain.com
http://domain.com
http://www.domain.com/index.html
http://domain. com/index.html

以上四个地址都可以表示跳转到同一个站点的首页,虽然不会对用户访问造成什么麻烦,但对于搜索引擎来说是四条网址并且内容相同。这种情况有可能会被搜索引擎误认为是作弊手段,另外当搜索引擎要规范化网址时,需要从这些选择中挑一个作为代表,但是挑的这个不一定是最好的,因此我们最好统一搜索引擎访问页面的地址,否则可能影响网站入口搜索结果的权重。

301跳转

如果URL发生改变,一定要使旧的地址301指向新的页面,否则搜索引擎会把原有的这个URL当作死链处理,之前完成的页面内容收录权重的工作就都失效了。

canonical

当该页面有不同参数传递的时候,标签属性也可以起到标识页面唯一性的作用,例如以下
三个地址。

domain.com/index.html
domain.com/index.html?from=123
domain.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设计师:主要关注设计图是否容易实现,使用什么样的组件,操作过程中的交互和动画效果等。

和运维工程师:主要关注如何上线,环境配置等。

注意在协作过程中不要殴打同事。

image.png | center | 496x269