undefined

undefined 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 example.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

undefined 发布了文章 · 2018-03-20

Node启动https服务器

首先你需要生成https证书,可以去付费的网站购买或者找一些免费的网站,可能会是key或者crt或者pem结尾的。不同格式之间可以通过OpenSSL转换,如:

openssl x509 -in mycert.crt -out mycert.pem -outform PEM

Node原生版本:

const https = require('https')
const path = require('path')
const fs = require('fs')

// 根据项目的路径导入生成的证书文件
const privateKey = fs.readFileSync(path.join(__dirname, './certificate/private.key'), 'utf8')
const certificate = fs.readFileSync(path.join(__dirname, './certificate/certificate.crt'), 'utf8')
const credentials = {
  key: privateKey,
  cert: certificate,
}

// 创建https服务器实例
const httpsServer = https.createServer(credentials, async (req, res) => {
  res.writeHead(200)
  res.end('Hello World!')
})

// 设置https的访问端口号
const SSLPORT = 443

// 启动服务器,监听对应的端口
httpsServer.listen(SSLPORT, () => {
  console.log(`HTTPS Server is running on: https://localhost:${SSLPORT}`)
})

express版本

const express = require('express')
const path = require('path')
const fs = require('fs')
const https = require('https')

// 根据项目的路径导入生成的证书文件
const privateKey = fs.readFileSync(path.join(__dirname, './certificate/private.key'), 'utf8')
const certificate = fs.readFileSync(path.join(__dirname, './certificate/certificate.crt'), 'utf8')
const credentials = {
  key: privateKey,
  cert: certificate,
}

// 创建express实例
const app = express()

// 处理请求
app.get('/', async (req, res) => {
  res.status(200).send('Hello World!')
})

// 创建https服务器实例
const httpsServer = https.createServer(credentials, app)

// 设置https的访问端口号
const SSLPORT = 443

// 启动服务器,监听对应的端口
httpsServer.listen(SSLPORT, () => {
  console.log(`HTTPS Server is running on: https://localhost:${SSLPORT}`)
})

koa版本

const koa = require('koa')
const path = require('path')
const fs = require('fs')
const https = require('https')

// 根据项目的路径导入生成的证书文件
const privateKey = fs.readFileSync(path.join(__dirname, './certificate/private.key'), 'utf8')
const certificate = fs.readFileSync(path.join(__dirname, './certificate/certificate.crt'), 'utf8')
const credentials = {
  key: privateKey,
  cert: certificate,
}

// 创建koa实例
const app = koa()

// 处理请求
app.use(async ctx => {
  ctx.body = 'Hello World!'
})

// 创建https服务器实例
const httpsServer = https.createServer(credentials, app.callback())

// 设置https的访问端口号
const SSLPORT = 443

// 启动服务器,监听对应的端口
httpsServer.listen(SSLPORT, () => {
  console.log(`HTTPS Server is running on: https://localhost:${SSLPORT}`)
})
查看原文

赞 0 收藏 0 评论 0

undefined 发布了文章 · 2018-03-18

Node启动https服务器

首先你需要生成https证书,可以去付费的网站购买或者找一些免费的网站,可能会是key或者crt或者pem结尾的。不同格式之间可以通过OpenSSL转换,如:

openssl x509 -in mycert.crt -out mycert.pem -outform PEM

Node原生版本:

const https = require('https')
const path = require('path')
const fs = require('fs')

// 根据项目的路径导入生成的证书文件
const privateKey = fs.readFileSync(path.join(__dirname, './certificate/private.key'), 'utf8')
const certificate = fs.readFileSync(path.join(__dirname, './certificate/certificate.crt'), 'utf8')
const credentials = {
  key: privateKey,
  cert: certificate,
}

// 创建https服务器实例
const httpsServer = https.createServer(credentials, async (req, res) => {
  res.writeHead(200)
  res.end('Hello World!')
})

// 设置https的访问端口号
const SSLPORT = 443

// 启动服务器,监听对应的端口
httpsServer.listen(SSLPORT, () => {
  console.log(`HTTPS Server is running on: https://localhost:${SSLPORT}`)
})

express版本

const express = require('express')
const path = require('path')
const fs = require('fs')
const https = require('https')

// 根据项目的路径导入生成的证书文件
const privateKey = fs.readFileSync(path.join(__dirname, './certificate/private.key'), 'utf8')
const certificate = fs.readFileSync(path.join(__dirname, './certificate/certificate.crt'), 'utf8')
const credentials = {
  key: privateKey,
  cert: certificate,
}

// 创建express实例
const app = express()

// 处理请求
app.get('/', async (req, res) => {
  res.status(200).send('Hello World!')
})

// 创建https服务器实例
const httpsServer = https.createServer(credentials, app)

// 设置https的访问端口号
const SSLPORT = 443

// 启动服务器,监听对应的端口
httpsServer.listen(SSLPORT, () => {
  console.log(`HTTPS Server is running on: https://localhost:${SSLPORT}`)
})

koa版本

const koa = require('koa')
const path = require('path')
const fs = require('fs')
const https = require('https')

// 根据项目的路径导入生成的证书文件
const privateKey = fs.readFileSync(path.join(__dirname, './certificate/private.key'), 'utf8')
const certificate = fs.readFileSync(path.join(__dirname, './certificate/certificate.crt'), 'utf8')
const credentials = {
  key: privateKey,
  cert: certificate,
}

// 创建koa实例
const app = koa()

// 处理请求
app.use(async ctx => {
  ctx.body = 'Hello World!'
})

// 创建https服务器实例
const httpsServer = https.createServer(credentials, app.callback())

// 设置https的访问端口号
const SSLPORT = 443

// 启动服务器,监听对应的端口
httpsServer.listen(SSLPORT, () => {
  console.log(`HTTPS Server is running on: https://localhost:${SSLPORT}`)
})
查看原文

赞 0 收藏 0 评论 0

undefined 发布了文章 · 2018-02-10

Lighthouse的使用与Google的移动端最佳实践

Lighthouse是一个Google开源的自动化工具,主要用于改进网络应用(移动端)的质量。目前测试项包括页面性能PWA可访问性(无障碍)最佳实践SEO。Lighthouse会对各个测试项的结果打分,并给出优化建议,这些打分标准和优化建议可以视为Google的网页最佳实践。
options.png

使用入门


运行Lighthouse的方式有三种:在开发者工具(Devtools)的Audits,作为Chrome拓展程序使用,或者作为命令行工具使用。Chrome开发者工具不需要额外安装,和扩展程序一样提供了一个用户友好的界面,方便读取报告;扩展程序相对于开发者工具的优势是更及时,不用等待Chrome发版就能体验到最新的功能;命令行工具可以将Lighthouse集成到持续集成系统。

开发者工具

仅能在Chrome60及以上使用,因为之前版本的Chrome的开发者工具的audits面板还不是Lighthouse。extension.png

audits.png

通过右上角的菜单或者快捷键(command+option+i)打开开发者工具,然后选择audits面板,点击Perform an audits会弹出一个options面板勾选测试项然后点击Run audits即可。

Chrome拓展程序

安装地址(需要梯子)
extension.png
generate.png

在右上角或者菜单里点击图中图标,Options可以配置测试项目,点击Generate report即可测试。

命令行工具

安装:

npm install -g lighthouse
# or use yarn:
# yarn global add lighthouse

使用:

lighthouse https://example.com

配置项:

$ lighthouse --help

lighthouse <url>

Logging:
  --verbose  Displays verbose logging                                                                                                      [boolean]
  --quiet    Displays no progress, debug logs or errors                                                                                    [boolean]

Configuration:
  --save-assets                  Save the trace contents & screenshots to disk                                                             [boolean]
  --list-all-audits              Prints a list of all available audits and exits                                                           [boolean]
  --list-trace-categories        Prints a list of all required trace categories and exits                                                  [boolean]
  --additional-trace-categories  Additional categories to capture with the trace (comma-delimited).
  --config-path                  The path to the config JSON.
  --chrome-flags                 Custom flags to pass to Chrome (space-delimited). For a full list of flags, see
                                 http://peter.sh/experiments/chromium-command-line-switches/.

                                 Environment variables:
                                 CHROME_PATH: Explicit path of intended Chrome binary. If set must point to an executable of a build of
                                 Chromium version 54.0 or later. By default, any detected Chrome Canary or Chrome (stable) will be launched.
                                                                                                                                       [default: ""]
  --perf                         Use a performance-test-only configuration                                                                 [boolean]
  --port                         The port to use for the debugging protocol. Use 0 for a random port                                    [default: 0]
  --hostname                     The hostname to use for the debugging protocol.                                              [default: "localhost"]
  --max-wait-for-load            The timeout (in milliseconds) to wait before the page is considered done loading and the run should continue.
                                 WARNING: Very high values can lead to large traces and instability                                 [default: 45000]
  --enable-error-reporting       Enables error reporting, overriding any saved preference. --no-enable-error-reporting will do the opposite. More:
                                 https://git.io/vFFTO
  --gather-mode, -G              Collect artifacts from a connected browser and save to disk. If audit-mode is not also enabled, the run will quit
                                 early.                                                                                                    [boolean]
  --audit-mode, -A               Process saved artifacts from disk                                                                         [boolean]

Output:
  --output       Reporter for the results, supports multiple values                        [choices: "json", "html", "domhtml"] [default: "domhtml"]
  --output-path  The file path to output the results. Use 'stdout' to write to stdout.
                 If using JSON output, default is stdout.
                 If using HTML output, default is a file in the working directory with a name based on the test URL and date.
                 If using multiple outputs, --output-path is ignored.
                 Example: --output-path=./lighthouse-results.html
  --view         Open HTML report in your browser                                                                                          [boolean]

Options:
  --help                        Show help                                                                                                  [boolean]
  --version                     Show version number                                                                                        [boolean]
  --blocked-url-patterns        Block any network requests to the specified URL patterns                                                     [array]
  --disable-storage-reset       Disable clearing the browser cache and other storage APIs before a run                                     [boolean]
  --disable-device-emulation    Disable Nexus 5X emulation                                                                                 [boolean]
  --disable-cpu-throttling      Disable CPU throttling                                                                    [boolean] [default: false]
  --disable-network-throttling  Disable network throttling                                                                                 [boolean]
  --extra-headers               Set extra HTTP Headers to pass with request                                                                 [string]

Examples:
  lighthouse <url> --view                                                   Opens the HTML report in a browser after the run completes
  lighthouse <url> --config-path=./myconfig.js                              Runs Lighthouse with your own configuration: custom audits, report
                                                                            generation, etc.
  lighthouse <url> --output=json --output-path=./report.json --save-assets  Save trace, screenshots, and named JSON report.
  lighthouse <url> --disable-device-emulation --disable-network-throttling  Disable device emulation
  lighthouse <url> --chrome-flags="--window-size=412,732"                   Launch Chrome with a specific window size
  lighthouse <url> --quiet --chrome-flags="--headless"                      Launch Headless Chrome, turn off logging
  lighthouse <url> --extra-headers "{\"Cookie\":\"monster=blue\"}"          Stringify\'d JSON HTTP Header key/value pairs to send in requests
  lighthouse <url> --extra-headers=./path/to/file.json                      Path to JSON file of HTTP Header key/value pairs to send in requests

For more information on Lighthouse, see https://developers.google.com/web/tools/lighthouse/.

测试结果示例


result.png

最佳实践


这些最佳实践主要针对移动端或者Web应用。某些技术对浏览器版本要求较高,用之前最好在Can I useMDN上查一下浏览器支持情况

打开外部链接使用rel="noopener"

当页面使用 target="_blank" 跳转至另一个页面时,新页面将与您的页面在同一个进程上运行。 如果新页面正在执行开销极大的 JavaScript,您的页面性能可能会受影响。最重要的是target="_blank”也是一个安全漏洞。新页面可以通过window.opener访问旧页面的window对象,并且它可以使用window.opener.location=newURL将旧页面导航至不同的网址。所以当在新窗口或标签中打开一个外部链接时,应该始终加上rel="noopener",例如:

<a href="https://examplepetstore.com" target="_blank" rel="noopener">...</a>
地址栏颜色应该和品牌颜色、网页主题匹配

address.png

就是地址栏的背景颜色应该和品牌颜色一致
通过meta标签实现的:

<meta name="theme-color" content="#ff6633">

不过仅在认可这个meta的浏览器上有效,比如Chrome for Android,实测pc、ios的Chrome、Safari无效。
bilibili.png

如果场景能用上还是能提高一些用户体验的,避免了地址栏突兀。

避免使用AppCache

AppCache已被废弃
考虑使用service worker的Cache API,另外现在ios 11.3也支持了service worker,未来一两年应该有很大发展。

避免使用console.time()

如果使用console.time()测试页面性能,请考虑使用User Timing API。其优势包括:

  • 高分辨率时间戳
  • 可导出的计时数据
  • 与Chrome Devtools TImeline相集成。在 Timeline 录制期间调用 User Timing 函数 performance.measure() 时,DevTools 自动将此测量结果添加到 Timeline 的结果中。

将console.time()替换为performance.mark()。如果需要测量两个label之间经过的时间,则使用performance.measure()。User Timing API

// 获得命名时间戳
window.performance.mark('mark_fully_loaded');
// 获得命名时间戳之间的时间间隔或者与PerformanceTiming的时间间隔
window.performance.measure('measure_load_from_dom', 'domComplete', 'mark_fully_loaded');
避免使用Date.now()

考虑改用performance.now()代替Date.now()。performance.now()可提供较高的时间戳分辨率,并始终以恒定的速率增加,它不受系统时钟(可以调整)的影响。performance.now()

// 获取相对于navigationStart属性中的时间戳为起点开始计时的精确到千分之一毫秒的时间戳
 window.performance.now()
避免弃用的API

已弃用的API计划从Chrome中移除,使用这些API后,被删除后将导致网页出错。查看Chrome平台状态

避免使用document.write()

对于网速较慢(2G、3G或较慢的WLAN)的用户,外部脚本通过document.write()动态注入会使页面内容的显示延迟数十秒。

避免巨大的网络负载

延迟请求直到需要它们
启用文本压缩
压缩HTML、JS和CSS
使用Webp而不是JPEG或PNG
将JPEG图像的压缩级别设置为85
缓存请求

避免使用mutation events

以下mutation events会损害性能,在DOM事件规范中已经弃用:

  • DOMAttrModified
  • DOMAttributeNameChanged
  • DOMCharacterDataModified
  • DOMElementNameChanged
  • DOMNodeInserted
  • DOMNodeInsertedIntoDocument
  • DOMNodeRemoved
  • DOMNodeRemovedFromDocument
  • DOMSubtreeModified

建议将每个mutation events替换成MutationObserver

避免使用旧版CSS Flexbox

2009年的旧Flexbox规范已弃用,其速度比最新的规范慢2.3倍。将页面中的display:box及以box开头的每个属性替换成标准的Flexbox属性。

避免在页面加载时自动请求地理位置

页面在加载时自动请求用户位置会使用户不信任页面或感到困惑。应将此请求与用户的手势进行关联,而不是在页面加载时自动请求用户的位置。

避免在页面加载时自动请求通知权限

好的通知需要做到及时、相关且准确。如果页面在加载时要求权限以发送通知,则这些通知可能与您的用户无关或者不是他们的精准需求。为提高用户体验,最好是向用户发送特定类型的通知,并在他们选择加入后显示权限请求。

避免使用Web SQL

Web SQL已弃用,建议替换为IndexedDB

背景和前景应该有足够的对比度

低对比度文本对于许多用户来说很难或不可能读取
使用Chrome扩展程序aXe可以分析出所有的可访问性问题

按钮有一个可访问的名称

没有名字的按钮对依赖屏幕阅读器的用户不可用。当一个按钮没有名字时,屏幕阅读器会宣布“按钮”。
对<button>元素和role="button"的元素:

  • 设置元素的内部文本
  • 设置aria-label属性
  • 将该aria-labelledby属性设置为屏幕阅读器可见的文本元素。

对于<input type = "button">的元素:

  • 设置value属性
  • 设置aria-label属性
  • 设置aria-labelledby属性

对于<input type="submit">和<input type="rest">:

  • 设置value属性,或省略它。浏览器在value省略时赋予"submit"或"reset"的默认值
  • 设置aria-label属性
  • 设置aria-labelledby属性
页面在其脚本不可用时包含一些内容

基本内容和页面功能不应该依赖于CSS或JS。对于必需依赖JavaScript的页面,一种方法是使用一个<noscript>元素,以提醒用户此页面需要JavaScript。

优化关键渲染路径

将关键资源数降至最低:消除关键资源、延迟关键资源的下载并将它们标记为不同步等。
优化关键字节数以缩短下载时间。
优化其余关键资源的加载顺序:尽早下载所有关键资产,以缩短关键路径长度。

避免长宽比不正确的图像

如果渲染的图像与其源文件中的长宽比不同,则呈现的图像可能看起来失真,产生不愉悦的用户体验。

  • 避免将元素的宽度或高度设置为可变大小的容器的百分比。
  • 避免设置不同于源图像尺寸的显式宽度或高度值。
  • 考虑使用css-aspect-ratio或 Aspect Ratio Boxes来帮助保留宽高比。
  • 如果可能的话,在HTML中指定图片的宽度和高度是一个很好的做法,这样浏览器就可以为图片分配空间,这样可以防止页面在加载时跳过。在HTML中而不是CSS中指定宽度和高度是更理想的,因为浏览器在解析CSS之前分配空间。实际上,如果您使用响应式图像,则此方法可能很困难,因为在知道视口尺寸之前无法指定宽度和高度。
启用文本压缩

如果浏览器支持,则配置服务器以使用Brotli压缩响应。Brotli比GZIP可以节省更多的流量。如果不支持Brotli则使用GZIP。在Chrome DevTools检查响应是否被压缩:

  • 打开DevTools的Network面板
  • 点击指定的回复的请求。
  • 点击Headers选项卡
  • 检查Response Headers中content-heading字段

content.png

预计输入延迟时间

输入响应能力对用户如何看待应用的性能起着关键作用。应用有100毫秒的时间响应用户输入。如果超过此时间,用户就会认为应用反应迟缓。

优化代码在浏览器中的运行方式:
  1. 对于动画效果的实现,避免使用setTimeout或setInterval,请使用requestAnimationFrame
  2. 将长时间运行的JavaScript从主线程移动到Web Worker
  3. 使用micro-tasks来执行对多个帧的DOM更改
  4. 使用Chrome DevTools的Timeline和Javascript分析器来评估JavaScript的影响。

降低选择器的复杂性(例如:nth-of-type、:nth-child);使用以类为中心的方法,例如BEM,这有一篇BEM的教程

  1. 尽可能避免布局操作,对“几何属性”(如宽度、高度左侧或顶部)的更改都需要布局计算。布局几乎总是作用到整个文档,如果有大量元素,会消耗很长时间来计算出所有元素的位置和尺寸。
  2. 避免强制同步布局

render.png
首先JavaScript运行,然后计算样式,然后布局。但是,可以使用JavaScript强制浏览器提前执行布局。这被称为强制同步布局。在JavaScript运行时,来自上一帧的所有旧布局值是已知的,并且可供查询。因此,如果在帧的开头写出一个元素的高度是没有问题的,但是在查询高度之前,已经更改其样式,如下列代码。,就会强制页面计算返回正确的高度。这是不必要的,并且开销很大。始终应先批量读取样式并执行,然后执行任何写操作。

function logBoxHeight() {

  box.classList.add('super-big');

  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);
}
  1. 除 transform 或 opacity 属性之外,更改任何属性始终都会触发绘制。可以使用Chrome DevTools来快速确定正在绘制的区域。打开DevTools,按下键盘上的 Esc 键。在出现的面板中,转到“rendering”标签,然后选中“Show paint rectangles”。rendering.png
  2. 每一个表单元素都应该有一个label

label阐明了表单元素的用途。虽然每个元素的目的对于有视觉的用户来说可能是显而易见的,但对于依靠屏幕阅读器的用户来说并非如此。有四种方式可以实现:

  • 隐含标签
<label>First Name <input type="text"/></label>
  • 显式标签
<label for="first">First Name <input type="text" id="first"/></label>
  • aria-label
<button class="hamburger-menu" aria-label="menu">...</button>
  • aria-labelledby
<span id="foo">Select seat:</span>
<custom-dropdown aria-labelledby="foo">...</custom-dropdown>
  1. 每个图像都有一个alt属性

信息性图像应该具有alt包含该图像内容的文本描述的属性。屏幕阅读器使视觉障碍的用户能够通过将文本内容转换为可以使用的表格(如合成语音或盲文)来使用您的网站。屏幕阅读器无法转换图像。因此,如果您的图片包含重要信息,那么视觉障碍用户无法获取该信息。
可以在DevTools的Console选项卡中使用以下命令来查找没有alt属性的图片

$$('img:not([alt])');

在Console中$$()相当于document.querySelectorAll()

  1. 配置HTML的Viewport meta标签

如果没有Viewport meta标签,移动设备将以典型的桌面设备屏幕宽度渲染页面,然后对页面进行缩放以适合移动设备屏幕。通过Viewport meta标签可以控制宽度和缩放比例。
配置视口设置视口
width=device-width键值对将视口宽度设置为设备宽度。在访问页面时,initial-scale=1键值对设置初始缩放级别。

<head>
  ...
  <meta name="viewport" content="width=device-width, initial-scale=1">
  ...
</head>
  1. 压缩图片(仅针对JPEG)

将每个图像的压缩级别设置为85或更低,像TinyJPG这样的Web服务可以帮助自动化图像优化的过程

避免页面存在不成功的HTTP状态码

搜索引擎可能无法正确索引返回不成功的HTTP状态码的页面。

允许用户粘贴到密码字段中

密码粘贴提高了安全性,因为它使用户能够使用密码管理器。密码管理员通常为用户生成强密码,安全地存储密码,然后在用户需要登录时自动将其粘贴到密码字段中。
删除阻止用户粘贴到密码字段的代码。使用事件断点中的Clipboard paste来打断点,可以快速找到阻止粘贴密码的代码。比如下列这种阻止粘贴密码的代码:

let input = document.querySelector('input');
input.addEventListener('paste', (e) => {
  e.preventDefault(); // This is what prevents pasting.
});

dom-breakpoint.png

避免DOM过大

大型的DOM树会以多种方式降低页面性能:

  • 网络效率和负载性能,如果你的服务器发送一个大的DOM树,你可能会运送大量不必要的字节。这也可能会减慢页面加载时间,因为浏览器可能会解析许多没有显示在屏幕上的节点。
  • 运行时性能。当用户和脚本与页面交互时,浏览器必须不断重新计算节点的位置和样式。一个大的DOM树与复杂的样式规则相结合可能会严重减慢渲染速度。

内存性能。如果使用通用查询选择器(例如,document.querySelectorAll('li') 您可能会无意中将引用存储到大量的节点),这可能会压倒用户设备的内存功能。

一个最佳的DOM树:

  • 总共少于1500个节点。
  • 最大深度为32个节点。
  • 没有超过60个子节点的父节点。
  • 一般来说,只需要在需要时寻找创建DOM节点的方法,并在不再需要时将其销毁。

如果你不能避免一个大型的DOM树,改善渲染性能的另一种方法是简化你的CSS选择器。请参阅减少风格计算的范围和复杂性

使用被动事件监听器以提升滚动性能

被动事件是新兴的Web标准,可以显著提高滚动性能,尤其在移动设备上。当使用touch事件监听器(scroll事件不存在这个问题)进行滚动时,因为浏览器不知道你是否会取消滚动,它们总是等待监听器执行完毕后才开始滚动,这样就造成了明显的延迟。事件监听器options中使用passive:true表明监听器永远不会取消滚动,这样浏览器就可以立即滚动。
在支持被动事件侦听器的浏览器中,将侦听器标记为passive即可:

document.addEventListener('touchstart', onTouchStart, {passive: true});

求赞,欢迎访问我的博客

查看原文

赞 0 收藏 0 评论 0

undefined 发布了文章 · 2018-01-28

《高性能javascript》阅读摘要

最近在阅读这本Nicholas C.Zakas(javascript高级程序设计作者)写的最佳实践、性能优化类的书。记录下主要知识。


加载和执行

脚本位置

放在<head>中的javascript文件会阻塞页面渲染:一般来说浏览器中有多种线程:UI渲染线程、javascript引擎线程、浏览器事件触发线程、HTTP请求线程等。多线程之间会共享运行资源,浏览器的js会操作dom,影响渲染,所以js引擎线程和UI渲染线程是互斥的,导致执行js时会阻塞页面的渲染。
最佳实践:所有的script标签应尽可能的放在body标签的底部,以尽量减少对整个页面下载的影响。

组织脚本

每个<script>标签初始下载时都会阻塞页面渲染,所以应减少页面包含的<script>标签数量。内嵌脚本放在引用外链样式表的<link>标签之后会导致页面阻塞去等待样式表的下载,建议不要把内嵌脚本紧跟在<link>标签之后。外链javascript的HTTP请求还会带来额外的性能开销,减少脚本文件的数量将会改善性能。

无阻塞的脚本

无阻塞脚本的意义在于在页面加载完成后才加载javascript代码。(window对象的load事件触发后)

延迟的脚本

带有defer属性的<script>标签可以放置在文档的任何位置。对应的javascript文件将在页面解析到<script>标签时开始下载,但并不会执行,直到DOM加载完成(onload事件被触发前)。当一个带有defer属性的javascript文件下载时,它不会阻塞浏览器的其他进程,可以与其他资源并行下载。执行的顺序是script、defer、load。

动态脚本元素

使用javascript动态创建HTML中script元素,例如一些懒加载库。
优点:动态脚本加载凭借着它在跨浏览器兼容性和易用的有时,成为最通用的无阻塞加载解决方式。

XHR脚本注入

创建XHR对线个,用它下载javascript文件,通过动态创建script元素将代码注入页面中

var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function() {
  if(xht.readyState === 4) {
    if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
      var script = document.createElement("script");
      script.type = "text/javascript";
      script.text = xhr.responseText;
      document.body.appendChild(script); 
    }
  }
};
xhr.send(null);

优点:可以下载javascript但不立即执行,在所有主流浏览器中都可以正常工作。
缺点:javascript文件必须与所请求的页面处于相同的域,意味着不能文件不能从CDN下载。


数据存取

存储的位置

数据存储的位置会很大程度上影响读取速度。

  • 字面量:字面量只代表自身,不存储在特定的位置。包括:字符串、数字、布尔值、对象、数组、函数、正则表达式、null、undefined。(个人理解:对象的指针本身是字面量)
  • 本地变量:var定义的数据存储单元。
  • 数组元素:存储在javascript数组内部,以数字为引。
  • 对象成员:存储在javascript对象内部,以字符串作为索引。

大多数情况下从一个字面量和一个局部变量中存取数据的差距是微不足道的。访问数据元素和对象成员的代价则高一点。如果在乎运行速度,尽量使用字面量和局部变量,减少数组和对象成员的使用。

管理作用域

作用域链

每个javascript函数都表示为一个对象,更确切的说是Function对象的一个实例。它也有仅供javascript引擎存储的内部属性,其中一个内部属性是[[Scope]],包含了一个被创建的作用域中对象的集合即作用域链。作用域链决定哪些数据能被函数访问。作用域中的每个对象被称为一个可变对象。
当一个函数被创建后,作用域链会被创建函数的作用域中可访问的数据对象所填充。执行函数时会创建一个称为执行上下文的内部对象。执行上下文定义了函数执行时的环境。每次函数执行时对应的执行环境都是独一无二的,多次调用同一个函数也会创建多个执行上下文,当函数执行完毕,执行上下文就会被销毁。每个执行上下文都有自己的作用域链,用于解析标识符。当执行上下文被创建时,它的作用域链初始化为当前运行函数的[[Scope]]属性中的对象。这些值按照它们出现在函数中的顺序,被复制到执行环境的作用域链中。这个过程一旦完成,一个被称为活动对象的新对象就为执行上下文创建好了。
活动对象作为函数运行时的变量对象,包含了所有局部对象,命名函数,参数集合以及this。然后此对象被推入作用域链的最前端。当执行环境被销毁时,活动对象也随之销毁。执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存储数据。该过程搜索执行环境的作用域链,查找同名的标识符。搜索过程从作用域链头部开始,也就是当前运行函数的活动对象。如果找到,就使用这个标识符对应的变量,如果没找到,继续搜索作用域链的下一个对象知道找到,若无法搜索到匹配的对象,则标识符被当作未定义的。这个搜索过程影响了性能。

标识符解析的性能

一个标识符所在的位置越深,读写速度就越慢,全局变量总是存在于执行环境作用域的最末端,因此它是最深的。
最佳实践:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量中。

改变作用域链

一般来说一个执行上下文的作用域链是不会改变的。但是,with语句和try-catch语句的catch子语句可以改变作用域链。
with语句用来给对象的所有属性创建一个变量,可以避免多次书写。但是存在性能问题:代码执行到with语句时,执行环境的作用域链临时被改变了,创建了一个新的(包含了with对象所有属性)对象被创建了,之前所有的局部变量现在处于第二个作用域链对象中,提高了访问的代价。建议放弃使用with语句。
try-catch语句中的catch子句也可以改变作用域链,当try代码块中发生错误,执行过程会自动跳转到catch子句,把异常对象推入一个变量对象并置于作用域的首位,局部变量处于第二个作用域链对象中。简化代码可以使catch子句对性能的影响降低。
最佳实践:将错误委托给一个函数来处理。

动态作用域

无论with语句还是try-catch语句的子句catch子句、eval()语句,都被认为是动态作用域。经过优化的javascript引擎,尝试通过分析代码来确定哪些变量是可以在特定的时候被访问,避开了传统的作用域链,取代以标识符索引的方式快速查找。当涉及动态作用域时,这种优化方式就失效了。
最佳实践:只在确实有必要时使用动态作用域。

闭包、作用域和内存

由于闭包的[[Scope]]属性包含了与执行上下文作用域链相同的对象的引用,因此会产生副作用。通常来说,函数的活动对象会随着执行环境一同销毁。但引入闭包时,由于引用仍然存在闭包的[[Scope]]属性中,因此激活对象无法被销毁,导致更多的内存开销。

最需要关注的性能点:闭包频繁访问跨作用域的标识符,每次访问都会带来性能损失。

最佳实践:将常用的跨作用域变量存储在局部变量中,然后直接访问局部变量。

对象成员

无论是通过创建自定义对象还是使用内置对象都会导致频繁的访问对象成员。

原型

javascript中的对象是基于原型的。解析对象成员的过程与解析变量十分相似,会从对象的实例开始,如果实例中没有,会一直沿着原型链向上搜索,直到找到或者到原型链的尽头。对象在原型链中位置越深,找到它也就越慢。搜索实例成员比从字面量或局部变量中读取数据代价更高,再加上遍历原型链带来的开销,这让性能问题更为严重。

嵌套成员

对象成员可能包含其他成员,每次遇到点操作符"."会导致javascript引擎搜索所有对象成员。

缓存对象成员值

由于所有类似的性能问题都与对象成员有关,因此应该尽可能避免使用他们,只在必要时使用对象成员,例如,在同一个函数中没有必要多次读取同一个对象属性(保存到局部变量中),除非它的值变了。这种方法不推荐用于对象的方法,因为将对象方法保存在局部变量中会导致this绑定到window,导致javascript引擎无法正确的解析它的对象成员,进而导致程序出错。


DOM编程

浏览器中的DOM

文档对象模型(DOM)是一个独立于语言的,用于操作XML和HTML文档的程序接口API。DOM是个与语言无关的API,在浏览器中的接口是用javascript实现的。客户端脚本编程大多数时候是在和底层文档打交道,DOM就成为现在javascript编码中的重要组成部分。浏览器把DOM和javascript单独实现,使用不同的引擎。

天生就慢

DOM和javascript就像两个岛屿通过收费桥梁连接,每次通过都要缴纳“过桥费”。
推荐的做法是尽可能减少过桥的次数,努力待在ECMAScript岛上。

DOM访问与修改

访问DOM元素是有代价的——前面的提到的“过桥费”。修改元素则更为昂贵,因为它会导致浏览器重新计算页面的几何变化(重排)。最坏的情况是在循环中访问或修改元素,尤其是对HTML元素集合循环操作。
在循环访问页面元素的内容时,最佳实践是用局部变量存储修改中的内容,在循环结束后一次性写入。
通用的经验法则是:减少访问DOM的次数,把运算尽量留在ECMAScript中处理。

节点克隆

大多数浏览器中使用节点克隆都比创建新元素要更有效率。

选择API

使用css选择器也是一种定位节点的便利途径,浏览器提供了一个名为querySelectorAll()的原生DOM方法。这种方法比使用javascript和DOM来遍历查找元素快很多。使用另一个便利方法——querySelector()来获取第一个匹配的节点。

重绘与重排

浏览器下载完页面中的所有组件——HTML标记、javascript、CSS、图片——之后会解析并生成两个内部的数据结构:DOM树(表示页面结构)、渲染树(表示DOM节点如何显示)。当DOM的变化影响了元素的几何属性,浏览器会使渲染树中受到影响的部分失效,并重构,这个过程成为重排,完成后,会重新绘制受影响的部分到屏幕,该过程叫重绘。并不是所有的DOM变化都会影响几何属性,这时只发生重绘。重绘和重排会导致web应用程序的UI反应迟钝,应该尽量避免。

重排何时发生

当页面布局的几何属性改变时就需要重排:

  1. 添加或删除可见的DOM元素
  2. 元素位置改变
  3. 元素尺寸改变(包括:外边据、内边距、边框厚度、宽度、高度等属性改变)
  4. 内容改变,例如:文本改变或图片被另一个不同尺寸的图片代替
  5. 页面渲染器初始化
  6. 浏览器窗口尺寸改变
渲染树变化的排队与刷新

由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。但是有些操作会导致强制刷新队列并要求任务立刻执行:

  1. offsetTop,offsetLeft,offsetWidth,offsetHeight
  2. scrollTop,scrollLeft,scrollWidth,scrollHeight
  3. clientTop,clientLeft,clientWidth,clientHeight
  4. getComputedStyle()

以上属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的修改变化并触发重排以返回正确的值。
最佳实践:尽量将修改语句放在一起,查询语句放在一起。

最小化重绘和重排

为了减少发生次数,应该合并多次DOM的样式的修改,然后一次处理掉。

批量修改DOM

当你需要对DOM元素进行一系列操作时,可以通过以下步骤来减少重绘和重排的次数:

  1. 使元素脱离文档
  2. 对其应用多重改变
  3. 把元素带回文档流

该过程会触发两次重排——第一步和第三步,如果忽略这两步,在第二步所产生的任何修改都会触发一次重排。

      有三种基本的方法可以使DOM脱离文档:

  1. 隐藏元素,应用修改,重新显示
  2. 使用文档片段,在当前DOM之外构建一个子树,再把它拷贝回文档
  3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
      

推荐使用文档片段,因为它们所产生的DOM遍历和重排次数最少。

缓存缓存布局信息

当你查询布局信息时,浏览器为了返回最新值,会刷新队列并应用所有变更。
最佳实践:尽量减少布局信息的获取次数,获取后把它赋值给局部变量,然后操作局部变量。

让元素脱离动画流

用展开、折叠的方式来显示和隐藏部分页面是一种常见的交互模式。通常包括展开区域的几何动画,并将页面其他部分推向下方。一般来说,重排只影响渲染树中的一小部分,但也可能影响很大的部分,甚至整个渲染树。浏览器所需要重排的次数越少,应用程序的响应速度就越快。当一个动画改变整个页面的余下部分时,会导致大规模重排。节点越多情况越差。避免大规模的重排:

  1. 使用绝对定位页面上的动画元素,将其脱离文档流。
  2. 应用动画
  3. 当动画结束时回恢复定位,从而只会下移一次文档的其他元素。

这样只造成了页面的一个小区域的重绘,不会产生重排并重绘页面的大部分内容。

:hover

如果有大量元素使用了:hover,那么会降低响应速度。此问题在IE8中更为明显。

事件委托

当页面中存在大量元素,并且每一个都要一次或多次绑定事件处理器时,这种情况可能会影响性能,每绑定一个事件处理器都是有代价的,它要么加重了页面负担(更多的代码、标签),要么增加了运行期的执行时间。需要访问和修改的DOM元素越多,应用程序就越慢,特别是事件绑定通常发生在onload时,此时对每一个富交互应用的网页来说都是一个拥堵的时刻。事件绑定占用了处理事件,而且浏览器要跟踪每个事件处理器,这也会占用更多的内存。这些事件处理器中的绝大部分都可能不会被触发。
事件委托原理:事件逐层冒泡并能被父级元素捕获。使用事件代理,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。
根据DOM标准,每个事件都要经历三个阶段:

  1. 捕获
  2. 到达目标
  3. 冒泡

IE不支持捕获,但是对于委托而言,冒泡已经足够。

<body>
     <div>     
          <ul id="menu">
               <li>
                    <a href="menu1.html">menu #1</a>
               </li>
               <li>
                    <a href="menu1.html">menu #2</a>
               </li>
          </ul>
     </div>
</body>

在以上的代码中,当用户点击链接“menu #1”,点击事件首先从a标签元素收到,然后向DOM树上层冒泡,被li标签接收然后是ul标签然后是div标签,一直到达document的顶层甚至window。
委托实例:阻止默认行为(打开链接),只需要给所有链接的外层UL"menu"元素添加一个点击监听器,它会捕获并分析点击是否来自链接。

document.getElementById('menu').onclick = function(e) {
          //浏览器target
          e=e||window.event;
          var target = e.target||e.srcElement;

          var pageid,hrefparts;
          
          //只关心hrefs,非链接点击则退出,注意此处是大写
          if (target.nodeName !== 'A') {
         return;
          }

          //从链接中找出页面ID
          hrefparts = target.href.split('/');
          pageid = hrefparts[hrefparts.length-1];
          pageid = pageid.replace('.html','');

          //更新页面
          ajaxRequest('xhr.php?page='+id,updatePageContents);

          //浏览器阻止默认行为并取消冒泡
          if (type of e.preventDefault === 'function') {
               e.preventDefault();
               e.stopPropagation();
          } else {
               e.returnValue=false;
               e.cancelBubble=true;
          }
};

跨浏览器兼容部分:

  1. 访问事件对象,并判断事件源
  2. 取消文档树中的冒泡(可选)
  3. 阻止默认动作(可选)

算法和流程控制

循环

循环的类型

ECMA-262标准第三版定义了javascript的基本语法和行为,其中共有四种循环。

  1. 第一种是标准的for循环。它由四部分组成:初始化、前测条件、后执行体、循环体。
           for (var i=0;i<10;i++){
                //do something
           }

for循环是javascript最常用的循环结构,直观的代码封装风格被开发者喜爱。

  2. while循环。while循环是最简单的前测循环,由一个前测条件和一个循环体构成。

  3. do-while循环是javascript唯一一种后测循环,由一个循环体和一个后测条件组成,至少会执行一次。
  4. for-in循环。可以枚举任何对象的属性名。
循环的性能

javascript提供的四种循环类型中,只有for-in循环比其他几种明显要慢。因为每次迭代操作会同时搜索实例或原型属性,for-in循环的每次迭代都会产生更多开销。速度只有其他类型循环的七分之一。除非你明确需要迭代一个属性数量未知的对象,否则应该避免使用for-in循环。如果你需要遍历一个数量有限的已知属性列表,使用其他循环类型会更快,比如数组。
除for-in外,其他循环类型的性能都差不多,类型的选择应该基于需求而不是性能。

提高循环的性能
  1. 减少每次迭代处理的事务
  2. 减少迭代的次数
减少迭代的工作量

减少对象成员及数组项的查找次数。

      在不影响的结果的情况下,可以使用倒序来略微提升性能。因为控制条件只要简单的与零比较。控制条件与true比较时,任何非零数会自动转换为true,而零值等同于false,实际上从两次比较(迭代数少于总数么?是否为true?)减少到一次比较(它是true么)。当循环复杂度为O(n)时,减少每次迭代的工作量是最有效的方法。当复杂度大于O(n)时,建议着重减少迭代次数。
减少迭代次数

Duff's Device是一个循环体展开技术,使得一次迭代中实际上执行了多次迭代的操作。一个典型的实现如下:

//credit:Jeff Greenberg
var iterations = Math.floor(items.length / 8),
      startAt = items.length/8,
      i = 0;
do{
      switch(startAt){
                case 0: process(items[i++]);
                case 7: process(items[i++]);
                case 6: process(items[i++]);
                case 5: process(items[i++]);
                case 4: process(items[i++]);
                case 3: process(items[i++]);
                case 2: process(items[i++]);
                case 1: process(items[i++]);
      }
      startAt = 0;
} while (--iterations);

Duff's Device背后的基本理念是:每次循环中最多可以调用8此process()。循环的迭代次数除以8。由于不是所有数字都能被8整除,变量startAt用来存放余数,表示第一次循环中应该调用多少次process()。
此算法稍快的版本取消了switch语句,并将余数处理和主循环分开

//credit:Jeff Greenberg
var i = items.length % 8;
while(i){
        process(item[i--]);
}
i = Math.floor(items.length / 8);
while(i){
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
}

尽管这种实现方法用两次循环代替之前的一次循环,但它移除了循环体中的switch语句,速度比原始循环更快。
如果循环迭代的次数小于1000,可能它与常规循环结构相比只有微不足道的性能提升。如果迭代数超过1000,那么执行效率将明显提升。例如在500000此迭代中,其运行时间比常规循环减少70%

基于函数的迭代

ECMA-262第四版加入的数组方法:forEach()方法。此方法遍历一个数组的所有成员,并在每个成员上执行一个函数。要运行的函数作为参数传给forEach(),并在调用时接受三个参数,分别是当前的值、索引以及数组本身。尽管基于函数的迭代提供了一个更为便利的迭代方法,但它仍比基于循环的迭代要慢一些。对每个数组项调用外部方法所带来的开销是速度慢的主要原因。

条件语句

if-else对比switch

条件数数量越大,越倾向于使用switch,主要是因为易读性。事实证明,大多数情况下switch比if-else运行得要快,但只有条件数量很大时才快得明显。

优化if-else

最小化到达正确分支前所需要判断的条件数量。最简单的优化方法是确保最可能出线的条件放在首位。if-else中的条件语句应该总是按照从最大概率到最小概率的顺序排列,以确保运行速度最快。假设均匀分部,可使用二分法的思想,重写为一系列嵌套的if-else语句。

查找表

有些时候优化条件语句的最佳方案是避免使用if-else和switch。可以使用数组和普通对象来构建查找表,通过查找表访问数据比用if-else或switch快很多。当单个键值存在逻辑映射时,构建查找表的优势就能体现出来。(比如把按照顺序的键值映射放到数组里)

递归

使用递归可以把复杂的算法变的简单。潜在问题是终止条件不明确或缺少终止条件会导致函数长时间运行,并使得用户界面处于假死状态和浏览器的调用栈大小限制。

调用栈限制

javascript引擎支持的递归数量与javascript调用栈大小直接相关。

递归模式

当你遇到调用栈大小限制时,第一步应该检查代码中的递归实例。有两种递归模式,第一种是调用自身,很容易定位错误。第二种是互相调用,很难定位。

迭代

任何递归能实现的算法同样可以使用迭代来实现。使用优化后的循环代替长时间运行的递归函数可以提升性能,因为运行一个循环比反复调用一个函数的开销要少的多。
归并排序算法是最常见的用递归实现的算法:

function merge(left, right) {
    var result = [];

    while (left.length > 0 && right.length > 0){
        if (left[0] < right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());    
        }
    }

    return result.concat(left).concat(right);
}

function mergeSort(items){
    if (items.length == 1){
        return items;
    }

    var middle = Math.floor(items.length / 2),
        left = items.slice(0, middle),
        right = items.slice(middle);
        return merge(mergeSort(left),mergeSort(right));
}

使用迭代实现归并算法:

//使用和上面相同的merge函数

function mergeSort(items){
    if (items.length == 1){
        return items;
    }

    var work = [];
    for (var i=0, len=items.length;i < len; i++){
        work.push([items[i]]);
    }
    work.push([]);

    for (var lim=len; lim>1; lim = (lim+1)/2){
        for (var j=0,k=0; k < lim; j++, k+=2){
            work[j] = merge(work[k],work[k+1]);
        }
        work[j] = [];
    }

    return work[0];
}

尽管迭代版本的归并排序算法比递归实现得要慢一些,但它不会像递归版本那样受到调用栈限制的影响。把递归算法改用迭代实现是避免栈溢出错误的方法之一

Memoization

Memoization是一种避免重复工作的方法,它缓存前一个计算结果供后续计算使用,避免了重复工作。
使用Memoization技术来重写阶乘函数:

function memfactorial(n){
    if(!memfactorial.cache){
        memfactorial.cache={
            "0":1,
            "1":1
        };
    }

    if(!memfactorial.cache.hasOwnProperty(n)){
        memfactorial.cache[n] = n * memfactorial (n-1);
    }

    return memfactorial.cache[n];
}

字符串和正则表达式

字符串链接

+和+=

不应在等号右边进行和被赋值的量无关的字符串拼接运算,这样会创造临时字符串。
例如:

str += "one" + "two";

会经历四个步骤:

  1. 在内存中创建一个临时字符串
  2. 连接后的字符串“onetwo”被赋值给该临时字符串
  3. 临时字符串与str当前的值连接
  4. 结果赋值给str

使用这种方式来代替:

str = str + "one" + "two";
//等价于 str = ((str + "one") + "two")

赋值表达式由str开始作为基础,每次给它附加一个字符串,由做到右一次连接,因此避免了使用临时字符串。

数组项合并

Array.prototype.join方法将数组的所有元素合并成一个字符串,它接受一个字符串参数作为分隔符插入每个元素的中间。大多数浏览器中,数组项合并比其他字符串连接的方法更慢。

String.prototype.concat

字符串的原生方法concat能接收任意数量的参数,并将每一个参数附加到所调用的字符串上。这是最灵活的字符串合并方法。多数情况下,使用concat比使用简单的+和+=稍慢。

正则表达式优化

部分匹配比完全不匹配所用的时间要长。

正则表达式工作原理
  1.  第一步编译

浏览器会验证正则表达式,然后把它转换为一个原生代码程序,用于执行匹配工 作。如果把正则对象赋值给一个变量,可以避免重复这一步。

  2.  第二步设置起始位置
  3.  第三步匹配每个正则表达式字元
  4.  第四步匹配成功或失败
回溯

当正则比到达时匹配目标字符串时,从左到右逐个测试表达式的组成部分,看是否能找到匹配项。在遇到量词和分支时,需要决策下一步如何处理。如果遇到量词,正则表达式需决定何时尝试匹配更多字符;如果遇到分支,那么必须从可选项中选择一个尝试匹配。每当正则表达式做类似的决定时,如果有必要的话,都会记录其他选择,以备返回时使用。如果当前选项匹配成功,正则表达式继续扫描表达式,如果其他部分也匹配成功,尼玛匹配结束。但是如果当前选项找不到匹配值,或后面的部分匹配失败,那么正则表达式会回溯到最后一个决策点,然后在剩余的选项中选择一个。这个过程会一直进行,知道找到匹配项,或者正则表达式中量词和分支选项的所有排列组合都尝试失败,那么它将放弃匹配从而移动到字符串的下一个字符,再重复此过程。

重复和回溯

贪婪匹配是段尾一个个回溯接下来的匹配内容,惰性正好相反;

回调失控

最佳实践:如果你的正则表达式包含了多个捕获组,那么你需要使用适当的反向引用次数。

嵌套量词与回溯失控

所谓的嵌套量词需要格外的关注且小心使用,以确保不会引发潜在的回溯失控。嵌套两次是指两次出线在一个自身被重复量词修饰的组中。确保正则表达式的两个部分不能对字符串的相同部分进行匹配

更多提高正则表达式效率的方法
1.  关于如何让正则匹配更快失败

正则表达式慢的原因通常是匹配失败的过程慢。

2.  正则表达式以简单、必需的字元开始

一个正则表达式的起始标记应当尽可能快速的测试并排除明显不匹配的位置。尽量以一个锚、特定字符串、字符类和单词边界开始,尽量避免以分组或选择字元开头,避免顶层分支。

3.  使用量词模式,使它们后面的字元互斥

当字符与字元相邻或子表达式能够重叠匹配时,正则表达式尝试拆解文本的路径数量将增加。

4.  减少分支数量,缩小分支范围

分支使用竖线|可能要求在字符串的每一个位置上测试所有的分支选项。你通常可以通过使用字符集和选项组件来减少对分支的需求,或将分支在正则表达式上的位置推后。

5.  使用非捕获组

捕获组消耗时间和内存来记录反向引用,并使它保持最新。如果你不需要一个反向引用,可以使用非捕获组来避免这些开销。

6.  只捕获感兴趣的文本以减少后处理

如果需要引用匹配的一部分,应该才去一切手段捕获那些片段,再使用反向引用来处理。

7.  暴露必需的字元

尝试让正则表达式引擎更容易判断哪些字元是必需的。

8.  使用合适的量词
9.  把正则表达式赋值给变量并重用它们

避免在循环体中重复编译正则表达式。

10.  将复杂的正则表达式拆分为简单的片段

何时不使用正则表达式

当只是搜索字面字符串,尤其是事先知道字符串的哪一部分将要被查找时。正则表达式无法直接跳到字符串末尾而不考虑沿途的字符。


快速响应的用户界面

浏览器UI线程

用于执行Javascript和更新用户界面的进程通常被称为“浏览器UI线程”。UI线程的工作基于一个见到那的队列系统,任务会被保存到队列中直到线程空闲。

浏览器限制

浏览器限制了javascript的运行时间。此类限制分为两种:调用栈的大小限制和长时间运行脚本限制。

多久算太久

单个Javascript操作话费的总时间不应该超过100毫秒。
最佳实践:限制所有的Javascript任务在100毫秒或更短的时间内完成。

使用定时器让出时间片段

当Javascript不能在100毫秒或更短的时间内完成。最理想的方法是让出UI线程的控制权,使得UI可以更新。

定时器基础

在Javascript中可以使用setTimeout()和setInterval()创建定时器,它们接收相同的参数:要执行的函数和执行前的等待时间。定时器与UI线程的交互:定时器会告诉Javascript引擎先等待一定时间,然后添加一个Javascript任务到UI队列。定时器代码只有在创建它的函数执行完之后,才有可能执行。无论发生何种情况,创建一个定时器会造成UI线程暂停,如同它从一个任务切换到下一个任务。因此,定时器代码会重置所有相关的浏览器限制,包括 长时间运行脚本定时器。此外,调用栈也会在定时器中重置为0。setTimeout()和setInterval()几近相同,如果 UI队列中已经存在由同一个setInterval()创建的任务,那么后续任务不会被添加到UI队列中。如果setTimeout()中的函数需要消耗比定时器延时更长的运行时间,那么定时器代码中的延时几乎是不可见的。

定时器的精度

Javascript定时器延迟通常不太准确,相差大约为几毫秒,无法用来精确计算时间。而且还存在最小值的限制。

使用定时器处理数组

是否可以用定时器取代循环的两个决定性因素:处理过程是否必须同步;数据是否必须按照顺序处理;如果两个答案都是否,那么代码适用于定时器分解任务。

var todo = items.concat();
// 克隆原数组

setTimeout(function(){

    // 取得数组的下一个元素并进行处理
    process(todo.shift());

    // 如果还有需要处理的元素,创建另一个定时器
    if(todo.length > 0){
        setTimeout(arguments.callee, 25);
    } else {
        callback(items);
    }

}, 25);

每个定时器的真实延时在很程度上取决于具体情况。普遍来讲,最好使用至少25毫秒,因为再小的延时,对大多数UI更新来说不够用。

记录代码运行使劲啊

通过定时器创建Date对象并比较它们的值来记录代码运行事件。加号可以将Date对象转换成数字,那么在后续的运算中就无须转换了。避免把任务分解成过于零碎的碎片,因为定时器之间有最小间隔,会导致出线空闲事件。

定时器与性能

当多个重复的定时器同时创建往往会出线性能问题。因为只有一个UI线程,而所有的定时器都在争夺运行时间。那些间隔在1秒或1秒以上的低频率的重复定时器几乎不会影响Web应用的响应速度。这种情况下定时器延迟远远超过UI线程产生瓶颈的值,可以安全的重复使用。当过个定时器使用较高的频率(100到200毫秒之间)时,会明显影响性能。在web应用中限制高频率重复定时器的数量,作为代替方案,使用一个独立的重复定时器每次执行多个操作。

Web Worker

引入了一个接口,能使代码运行并且不占用浏览器UI线程的时间。

Worker

没有绑定UI线程,每个Web Worker都有自己的全局环境,其功能只是Javascript特性的一个子集。运行环境由如下部分组成:一个navigator对象,值包括四个属性:appName、appVersion、userAgent和platform。
一个location对象(与window.location相同,不过所有属性都是只读的。)。
一个self对象,指向全局worker对象。
一个importScipt()方法,用来加载Worker所用到的外部javascript文件。
所有的ECMAScript对象
XMLHttpRequest构造器
setTimeout()方法和setInterval()方法
一个close()方法,它能立刻停止Worker运行
由于Web Worker有着不同的全局运行环境,因此你无法从javascript代码中创建它。需要创建一个完全独立的javascript文件,其中包含了需要在Worker中运行的代码。要创建网页人工线程,你必须传入这个javascript文件的URL;

与Worker通信

通过事件接口进行通信。网页代码可以通过postMessage()方法给Worker传递数据,它接受一个参数,即需要传递给Worker的数据。此外,Worker还有一个用来接收信息的onmessage事件处理器。Worker可通过它自己的postMessage()方法把信息回传给页面。消息系统是网页和Worker通信的唯一途径。只有特定类型的数据可以使用postMessage()传递。你可以传递原始值(字符串、数字、布尔值、null和undefined),也可以传递Object和Array的实例,其他类型就不允许了。有效数据会被序列化,传入或传出Worker,然后反序列化。虽然看上去对象可以直接传入,但对象实例完全是相同数据的独立表述。

加载外部文件

Worker 通过importScript()方法加载外部javascript文件,该方法接收一个或多个javascript文件URL作为参数。importScript()的调用过程是阻塞式的,知道所有所有文件加载并执行完成之后,脚本才会继续运行。由于Worker在UI线程之外运行,所以这种阻塞并不会影响UI响应。
Web Worker适合用于那些处理纯数据,或者与浏览器UI无关的长时间运行脚本。尽管它看上去用处不大,但Web应用中通常有一些数据处理功能将收益于Worker而不是定时器。
可能的用处:

  1. 编码/解码大字符串
  2. 复杂数学运算
  3. 大数组排序
  4. 任何超过100毫秒的处理过程,都应当考虑Worker方案是不是比基于定时器的方案更为合适。

Ajax

Ajax是高性能javascript的基础。它可以通过延迟下载体积较大的资源文件来使得页面加载速度更快。它通过异步的方式在客户端和服务端之间传输数据,避免同时传输大量数据。

数据传输

请求数据

有五种常用技术用于想服务器请求数据:

  1. XMLHttpRequest
  2. Dynamic script tag insertion(脚本动态注入)
  3. iframes
  4. Comet
  5. Multipart XHR

现代高性能Javascript中使用的三种技术是:XHR、动态脚本注入和Multipart XHR

XMLHttpRequest

XMLHttpRequest是目前最常用的技术,它允许异步发送和接收数据。由于XHR提供了高级的控制,所以浏览器对其增加了一些限制。你不能使用XHR从外域请求数据。对于那些不会改变服务器状态,只会获取数据(幂等行为)的请求,应该使用GET。经GET请求的数据会被缓存起来,如果需要多次请求统一数据的话,它会有助于提升性能。只有当请求的URL加上参数的长度接近或超过2048个字符时,才应该用POST获取数据。因为IE限制URL长度,过长将导致请求的URL被截断。

动态脚本注入

这种技术客服了XHR的最大限制:它能跨域请求数据。这是一个Hack,你不需要实例化一个专用对象,而可以使用javascript创建一个新的脚本标签,并设置它的src属性为不同域的URL。与XHR相比,动态脚本注入提供的控制是有限的。只能使用GET方法而不是POST方法。不能设置请求的超时处理或重试;不能访问请求的头部信息,不能把整个响应信息作为字符串来处理。因为响应消息作为脚本标签的源码,它必须是可执行的javascript代码。你不能使用纯XML、纯JSOn或其他任何格式的数据,无论哪种格式,都必须封装在一个回调函数中。这项技术的速度却非常快。响应消息是作为javascript执行,而不是作为字符串需要进一步处理。正因如此,它有潜力成为客户端获取并解析数据最快的方法。

Multipart XHR

允许客户端只用一个HTTP请求就可以从服务端向客户端传送多个字元。它通过在服务端将字元打包成一个由双方约定的字符串分割的长字符串并发送到客户端。然后用javascript代码处理这个长字符串,并根据它的mime-type类型和传入的其他“头信息”解析出每个资源。缺点:资源不能被浏览器缓存。
能显著提高性能的场景:
页面包含了大量其他地方用不到的资源,尤其是图片;
网站已经在每个页面中使用了一个独立打包的Javascript或CSS文件以减少http请求;

发送数据

XMLHttpRequest
当使用XHR发送数据到服务器时,GET方式会更快。这是因为,对少量数据而言一个GET请求只发送一个数据包。而一个POST请求至少要发两个数据包,一个装载头信息,另一个装载POST正文。POST更适合发送大量数据到服务器,因为它不关心额外数据包的数量,另一个原因是URL长度有限制,它不可能使用过长的GET请求。

Beacons

类似动态脚本注入。使用Javascript创建一个新的Image对象,并把src属性设置为服务器上脚本的URL。该URL包含了我们要通过GET传回的键值对数据。服务器会接受数据并保存下来,无须向客服端发送任何回馈信息,因此没有图片会实际显示出来。这是回传信息最有效的方式。性能消耗更小,而且服务器端的错误不影响客户端。缺点:无法发送POST数据,而URL的长度有最大值,所以可以发送的数据的长度被限制的相当小。

数据格式

考虑数据格式时唯一需要比较的标准就是速度

XML

当Ajax最先开始流行时,它选择了XML作为数据格式。优势:极佳的通用性、格式严格,且易于验证。缺点:冗长,依赖大量结构、有效数据的比例很低、语法模糊,如果有其他格式可选不要使用它。

JSON

是一种使用Javascript对象和数组直接量编写的轻量级且易于解析的数据格式。

JSON-P

事实上,JSON可以被本地执行会导致几个重要的性能影响。当使用XHR时,JSON数据被当成字符串返回。在使用动态脚本注入时,JSON数据要被当成另一个Javascript文件并作为原生代码执行,为实现这一点必须封装在一个回调函数中。JSON-P因为回调包装的原因略微增大了文件尺寸,但性能提升巨大。由于数据是当作原生的Javascript,因此解析速度跟原生Javascript一样快。最快的JSON格式是使用数组形式的JSON-P。不要把敏感数据编码在JSON-P中,因为无法确认它是否保持着私有调用状态。

HTML

通常你请求的数据需要被转换成HTML以显示到页面上。Javascript可以较快地把一个较大的数据结构转换成简单的HTML,但在服务器处理会快很多。一种可考虑的技术是在服务器上构建好整个HTML再传回客户端,Javascript可以很方便地通过innerHTML属性把它插入页面相应的位置。取点:臃肿的数据格式、比XML更繁杂。在数据本身的最外层,可以嵌套HTML标签,每个都带有id、class和其他属性。HTML格式可能比实际数据占用更多空间。应当在客户端的瓶颈是CPU而不是带宽时才使用此技术。

自定义格式

理想的数据格式应该只包含必要的结构,以便你可以分解出每个独立的字段。最重要的决定就是采用哪种分隔符,它应当是一个单字符,而且不应该存在你的数据中。

Ajax性能指南

缓存数据

在服务端,设置HTTP头信息以确保你的响应会被浏览器缓存。
在客户端,把获取到的信息存储到本地,从而避免再次请求。

设置HTTP头信息

如果希望ajax能被浏览器缓存,那么你必须使用GET方式发送请求并且需要在响应中发送正确的HTTP头信息。Expires头信息会告诉浏览器应该缓存多久。它的值是一个日期。

本地数据存储

直接把从服务器接收到的数据储存起来。可以把响应文本保存到一个对象中,以URL为键值作为索引。

Ajax类库的局限性

所有的Javascript类库都允许你访问一个Ajax对象,它屏蔽了浏览器之间的差异,给你一个统一的接口。为了统一接口的功能,类库简化接口,使得你不能访问XMLHttpRequest的完整功能。


编程实践

避免双重求值

Javascript允许你在程序中提取一个包含代码的字符串,然后动态执行它。有四种标准方法可以实现:eval()、Function()构造函数、setTimeout()和setInterval()。首先会以正常的方式求值,然后在执行的过程中对包含于字符串的代码发起另一个求值运算。每次使用这些方法都要创建一个新的解释器/编译器实例,导致消耗时间大大增加。
大多数时候没有必要使用eval()和Function(),因此最好避免使用它们。定时器则建议传入函数而不是字符串作为第一个参数。

使用Object/Array直接量

Javascript中创建对象和数组的方法有多种,但使用对象和数组直接量是最快的方式。

避免重复工作

别做无关紧要的工作,别重复做已经完成的工作。

延迟加载

第一次被调用时,会先检查并决定使用哪种方法去绑定或取消绑定事件处理器。然后原始函数被包含正确操作的新函数覆盖。最后一步调用新的函数,并传入原始参数。随后每次调用都不会再做检测,因为检测代码已经被新的函数覆盖。调用延迟加载函数时,第一次总会消耗较长的费时间,因为它必须运行检测接着再调用另一个函数完成任务。但随后调用相同的函数会更快,因为不需要再执行检测逻辑。当一个函数在页面中不会立刻调用时,延迟加载是最好的选择。

条件预加载

它会在脚本加载期间提前检测,而不会等到函数被调用。检测的操作依然只有一次,知识它在过程中来的更早。条件预加载确保所有函数调用消耗的时间相同。其代价是需要在脚本加载时就检测,而不是加载后。预加载适用于一个函数马上就要被用到,并且在整个页面的生命周期中频繁出现的场合。

使用快的部分

运行速度慢的部分实际上是代码,引擎通常是处理过程中最快的部分。

位操作

使用位运算代替纯数学操作:对2的取模运算可以被&1代替,速度提高很多。位掩码:处理同时存在多个布尔选项时的情形,思路是使用单个数字的每一位来判定是否选项成立,从而有效得把数字转换为布尔值标记组成的数组。

原生方法

原生方法更快,因为写代码前就存在浏览器中了,并且都是用底层语言比如c++编写的。经验不足的Javascript开发者经常犯的错误就是在代码中进行复杂的数学运算,而没有使用内置的Math对象中那些性能更好的版本。另一个例子是选择器API,它允许使用CSS选择器来查找DOM节点。原生的querySelector()和querySelectorAll()方法完成任务平均所需时间是基于Javascript的CSS查询的10%。


构建并部署高性能Javascript应用

合并多个Javascript文件,网站提速指南中第一条也是最重要的一条规则,就是减少http请求数。

预处理Javascript文件

预处理你的Javascript源文件并不会让应用变的更快,但它允许你做些其他的事情,例如有条件地插入测试代码,来衡量你的应用程序的性能。

Javascript压缩

指的是把Javascript文件中所有与运行无关的部分进行剥离的过程。剥离的内容包括注释和不必要的空白字符。该过程通常可以将文件大小减半,促使文件更快被下载,并鼓励程序员编写更好的更详细的注释。

构建时处理对比运行时处理

普遍规则是只要能在构建时完成的工作,就不要留到运行时去做。

Javascript的http压缩

当Web浏览器请求一个资源时,它通常会发送一个Accept-Encoding HTTP头来告诉Web服务器它支持哪种编码转换类型。这个信息主要用来压缩文档以更快的下载,从而改善用户体验。Accept-Encoding可用的值包括:gzip、compress、deflate和identity。gzip是目前最流行的编码方式。它通常能减少70%的下载量,成为提升Web应用性能的首选武器。记住Gzip压缩主要适用于文本,包括Javascript文件。

缓存Javascript文件

缓存HTTP组件能极大提高网站回访用户的体验。Web服务器通过Expires HTTP响应头来告诉客户端一个字元应当缓存多长事件。它的值是一个遵循RFC1123标准的绝对时间戳。

处理缓存问题

适当的缓存控制能提升用户体验,但它有一个缺点:当应用升级时,你需要确保用户下载到最新的静态内容。这个问题可以通过把改动过的静态资源重命名来解决。

使用内容分发网络(CDN)

内容分发网络是在互联网上按地理位置设置分部计算机网络,它负责传递内容给终端用户。使用CDN的主要原因是增强Web应用的可靠性、可扩展性,更重要的是提升性能。事实上,通过向地理位置最近的用户输入内容,CDN能极大减少网络延时。

查看原文

赞 1 收藏 11 评论 0

undefined 发布了文章 · 2018-01-26

《高性能javascript》阅读摘要

最近在阅读这本Nicholas C.Zakas(javascript高级程序设计作者)写的最佳实践、性能优化类的书。记录下主要知识。


加载和执行

脚本位置

放在<head>中的javascript文件会阻塞页面渲染:一般来说浏览器中有多种线程:UI渲染线程、javascript引擎线程、浏览器事件触发线程、HTTP请求线程等。多线程之间会共享运行资源,浏览器的js会操作dom,影响渲染,所以js引擎线程和UI渲染线程是互斥的,导致执行js时会阻塞页面的渲染。
最佳实践:所有的script标签应尽可能的放在body标签的底部,以尽量减少对整个页面下载的影响。

组织脚本

每个<script>标签初始下载时都会阻塞页面渲染,所以应减少页面包含的<script>标签数量。内嵌脚本放在引用外链样式表的<link>标签之后会导致页面阻塞去等待样式表的下载,建议不要把内嵌脚本紧跟在<link>标签之后。外链javascript的HTTP请求还会带来额外的性能开销,减少脚本文件的数量将会改善性能。

无阻塞的脚本

无阻塞脚本的意义在于在页面加载完成后才加载javascript代码。(window对象的load事件触发后)

延迟的脚本

带有defer属性的<script>标签可以放置在文档的任何位置。对应的javascript文件将在页面解析到<script>标签时开始下载,但并不会执行,直到DOM加载完成(onload事件被触发前)。当一个带有defer属性的javascript文件下载时,它不会阻塞浏览器的其他进程,可以与其他资源并行下载。执行的顺序是script、defer、load。

动态脚本元素

使用javascript动态创建HTML中script元素,例如一些懒加载库。
优点:动态脚本加载凭借着它在跨浏览器兼容性和易用的有时,成为最通用的无阻塞加载解决方式。

XHR脚本注入

创建XHR对线个,用它下载javascript文件,通过动态创建script元素将代码注入页面中

var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function() {
  if(xht.readyState === 4) {
    if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
      var script = document.createElement("script");
      script.type = "text/javascript";
      script.text = xhr.responseText;
      document.body.appendChild(script); 
    }
  }
};
xhr.send(null);

优点:可以下载javascript但不立即执行,在所有主流浏览器中都可以正常工作。
缺点:javascript文件必须与所请求的页面处于相同的域,意味着不能文件不能从CDN下载。


数据存取

存储的位置

数据存储的位置会很大程度上影响读取速度。

  • 字面量:字面量只代表自身,不存储在特定的位置。包括:字符串、数字、布尔值、对象、数组、函数、正则表达式、null、undefined。(个人理解:对象的指针本身是字面量)
  • 本地变量:var定义的数据存储单元。
  • 数组元素:存储在javascript数组内部,以数字为引。
  • 对象成员:存储在javascript对象内部,以字符串作为索引。

大多数情况下从一个字面量和一个局部变量中存取数据的差距是微不足道的。访问数据元素和对象成员的代价则高一点。如果在乎运行速度,尽量使用字面量和局部变量,减少数组和对象成员的使用。

管理作用域

作用域链

每个javascript函数都表示为一个对象,更确切的说是Function对象的一个实例。它也有仅供javascript引擎存储的内部属性,其中一个内部属性是[[Scope]],包含了一个被创建的作用域中对象的集合即作用域链。作用域链决定哪些数据能被函数访问。作用域中的每个对象被称为一个可变对象。
当一个函数被创建后,作用域链会被创建函数的作用域中可访问的数据对象所填充。执行函数时会创建一个称为执行上下文的内部对象。执行上下文定义了函数执行时的环境。每次函数执行时对应的执行环境都是独一无二的,多次调用同一个函数也会创建多个执行上下文,当函数执行完毕,执行上下文就会被销毁。每个执行上下文都有自己的作用域链,用于解析标识符。当执行上下文被创建时,它的作用域链初始化为当前运行函数的[[Scope]]属性中的对象。这些值按照它们出现在函数中的顺序,被复制到执行环境的作用域链中。这个过程一旦完成,一个被称为活动对象的新对象就为执行上下文创建好了。
活动对象作为函数运行时的变量对象,包含了所有局部对象,命名函数,参数集合以及this。然后此对象被推入作用域链的最前端。当执行环境被销毁时,活动对象也随之销毁。执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存储数据。该过程搜索执行环境的作用域链,查找同名的标识符。搜索过程从作用域链头部开始,也就是当前运行函数的活动对象。如果找到,就使用这个标识符对应的变量,如果没找到,继续搜索作用域链的下一个对象知道找到,若无法搜索到匹配的对象,则标识符被当作未定义的。这个搜索过程影响了性能。

标识符解析的性能

一个标识符所在的位置越深,读写速度就越慢,全局变量总是存在于执行环境作用域的最末端,因此它是最深的。
最佳实践:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量中。

改变作用域链

一般来说一个执行上下文的作用域链是不会改变的。但是,with语句和try-catch语句的catch子语句可以改变作用域链。
with语句用来给对象的所有属性创建一个变量,可以避免多次书写。但是存在性能问题:代码执行到with语句时,执行环境的作用域链临时被改变了,创建了一个新的(包含了with对象所有属性)对象被创建了,之前所有的局部变量现在处于第二个作用域链对象中,提高了访问的代价。建议放弃使用with语句。
try-catch语句中的catch子句也可以改变作用域链,当try代码块中发生错误,执行过程会自动跳转到catch子句,把异常对象推入一个变量对象并置于作用域的首位,局部变量处于第二个作用域链对象中。简化代码可以使catch子句对性能的影响降低。
最佳实践:将错误委托给一个函数来处理。

动态作用域

无论with语句还是try-catch语句的子句catch子句、eval()语句,都被认为是动态作用域。经过优化的javascript引擎,尝试通过分析代码来确定哪些变量是可以在特定的时候被访问,避开了传统的作用域链,取代以标识符索引的方式快速查找。当涉及动态作用域时,这种优化方式就失效了。
最佳实践:只在确实有必要时使用动态作用域。

闭包、作用域和内存

由于闭包的[[Scope]]属性包含了与执行上下文作用域链相同的对象的引用,因此会产生副作用。通常来说,函数的活动对象会随着执行环境一同销毁。但引入闭包时,由于引用仍然存在闭包的[[Scope]]属性中,因此激活对象无法被销毁,导致更多的内存开销。

最需要关注的性能点:闭包频繁访问跨作用域的标识符,每次访问都会带来性能损失。

最佳实践:将常用的跨作用域变量存储在局部变量中,然后直接访问局部变量。

对象成员

无论是通过创建自定义对象还是使用内置对象都会导致频繁的访问对象成员。

原型

javascript中的对象是基于原型的。解析对象成员的过程与解析变量十分相似,会从对象的实例开始,如果实例中没有,会一直沿着原型链向上搜索,直到找到或者到原型链的尽头。对象在原型链中位置越深,找到它也就越慢。搜索实例成员比从字面量或局部变量中读取数据代价更高,再加上遍历原型链带来的开销,这让性能问题更为严重。

嵌套成员

对象成员可能包含其他成员,每次遇到点操作符"."会导致javascript引擎搜索所有对象成员。

缓存对象成员值

由于所有类似的性能问题都与对象成员有关,因此应该尽可能避免使用他们,只在必要时使用对象成员,例如,在同一个函数中没有必要多次读取同一个对象属性(保存到局部变量中),除非它的值变了。这种方法不推荐用于对象的方法,因为将对象方法保存在局部变量中会导致this绑定到window,导致javascript引擎无法正确的解析它的对象成员,进而导致程序出错。


DOM编程

浏览器中的DOM

文档对象模型(DOM)是一个独立于语言的,用于操作XML和HTML文档的程序接口API。DOM是个与语言无关的API,在浏览器中的接口是用javascript实现的。客户端脚本编程大多数时候是在和底层文档打交道,DOM就成为现在javascript编码中的重要组成部分。浏览器把DOM和javascript单独实现,使用不同的引擎。

天生就慢

DOM和javascript就像两个岛屿通过收费桥梁连接,每次通过都要缴纳“过桥费”。
推荐的做法是尽可能减少过桥的次数,努力待在ECMAScript岛上。

DOM访问与修改

访问DOM元素是有代价的——前面的提到的“过桥费”。修改元素则更为昂贵,因为它会导致浏览器重新计算页面的几何变化(重排)。最坏的情况是在循环中访问或修改元素,尤其是对HTML元素集合循环操作。
在循环访问页面元素的内容时,最佳实践是用局部变量存储修改中的内容,在循环结束后一次性写入。
通用的经验法则是:减少访问DOM的次数,把运算尽量留在ECMAScript中处理。

节点克隆

大多数浏览器中使用节点克隆都比创建新元素要更有效率。

选择API

使用css选择器也是一种定位节点的便利途径,浏览器提供了一个名为querySelectorAll()的原生DOM方法。这种方法比使用javascript和DOM来遍历查找元素快很多。使用另一个便利方法——querySelector()来获取第一个匹配的节点。

重绘与重排

浏览器下载完页面中的所有组件——HTML标记、javascript、CSS、图片——之后会解析并生成两个内部的数据结构:DOM树(表示页面结构)、渲染树(表示DOM节点如何显示)。当DOM的变化影响了元素的几何属性,浏览器会使渲染树中受到影响的部分失效,并重构,这个过程成为重排,完成后,会重新绘制受影响的部分到屏幕,该过程叫重绘。并不是所有的DOM变化都会影响几何属性,这时只发生重绘。重绘和重排会导致web应用程序的UI反应迟钝,应该尽量避免。

重排何时发生

当页面布局的几何属性改变时就需要重排:

  1. 添加或删除可见的DOM元素
  2. 元素位置改变
  3. 元素尺寸改变(包括:外边据、内边距、边框厚度、宽度、高度等属性改变)
  4. 内容改变,例如:文本改变或图片被另一个不同尺寸的图片代替
  5. 页面渲染器初始化
  6. 浏览器窗口尺寸改变
渲染树变化的排队与刷新

由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。但是有些操作会导致强制刷新队列并要求任务立刻执行:

  1. offsetTop,offsetLeft,offsetWidth,offsetHeight
  2. scrollTop,scrollLeft,scrollWidth,scrollHeight
  3. clientTop,clientLeft,clientWidth,clientHeight
  4. getComputedStyle()

以上属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的修改变化并触发重排以返回正确的值。
最佳实践:尽量将修改语句放在一起,查询语句放在一起。

最小化重绘和重排

为了减少发生次数,应该合并多次DOM的样式的修改,然后一次处理掉。

批量修改DOM

当你需要对DOM元素进行一系列操作时,可以通过以下步骤来减少重绘和重排的次数:

  1. 使元素脱离文档
  2. 对其应用多重改变
  3. 把元素带回文档流

该过程会触发两次重排——第一步和第三步,如果忽略这两步,在第二步所产生的任何修改都会触发一次重排。

      有三种基本的方法可以使DOM脱离文档:

  1. 隐藏元素,应用修改,重新显示
  2. 使用文档片段,在当前DOM之外构建一个子树,再把它拷贝回文档
  3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
      

推荐使用文档片段,因为它们所产生的DOM遍历和重排次数最少。

缓存缓存布局信息

当你查询布局信息时,浏览器为了返回最新值,会刷新队列并应用所有变更。
最佳实践:尽量减少布局信息的获取次数,获取后把它赋值给局部变量,然后操作局部变量。

让元素脱离动画流

用展开、折叠的方式来显示和隐藏部分页面是一种常见的交互模式。通常包括展开区域的几何动画,并将页面其他部分推向下方。一般来说,重排只影响渲染树中的一小部分,但也可能影响很大的部分,甚至整个渲染树。浏览器所需要重排的次数越少,应用程序的响应速度就越快。当一个动画改变整个页面的余下部分时,会导致大规模重排。节点越多情况越差。避免大规模的重排:

  1. 使用绝对定位页面上的动画元素,将其脱离文档流。
  2. 应用动画
  3. 当动画结束时回恢复定位,从而只会下移一次文档的其他元素。

这样只造成了页面的一个小区域的重绘,不会产生重排并重绘页面的大部分内容。

:hover

如果有大量元素使用了:hover,那么会降低响应速度。此问题在IE8中更为明显。

事件委托

当页面中存在大量元素,并且每一个都要一次或多次绑定事件处理器时,这种情况可能会影响性能,每绑定一个事件处理器都是有代价的,它要么加重了页面负担(更多的代码、标签),要么增加了运行期的执行时间。需要访问和修改的DOM元素越多,应用程序就越慢,特别是事件绑定通常发生在onload时,此时对每一个富交互应用的网页来说都是一个拥堵的时刻。事件绑定占用了处理事件,而且浏览器要跟踪每个事件处理器,这也会占用更多的内存。这些事件处理器中的绝大部分都可能不会被触发。
事件委托原理:事件逐层冒泡并能被父级元素捕获。使用事件代理,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。
根据DOM标准,每个事件都要经历三个阶段:

  1. 捕获
  2. 到达目标
  3. 冒泡

IE不支持捕获,但是对于委托而言,冒泡已经足够。

<body>
     <div>     
          <ul id="menu">
               <li>
                    <a href="menu1.html">menu #1</a>
               </li>
               <li>
                    <a href="menu1.html">menu #2</a>
               </li>
          </ul>
     </div>
</body>

在以上的代码中,当用户点击链接“menu #1”,点击事件首先从a标签元素收到,然后向DOM树上层冒泡,被li标签接收然后是ul标签然后是div标签,一直到达document的顶层甚至window。
委托实例:阻止默认行为(打开链接),只需要给所有链接的外层UL"menu"元素添加一个点击监听器,它会捕获并分析点击是否来自链接。

document.getElementById('menu').onclick = function(e) {
          //浏览器target
          e=e||window.event;
          var target = e.target||e.srcElement;

          var pageid,hrefparts;
          
          //只关心hrefs,非链接点击则退出,注意此处是大写
          if (target.nodeName !== 'A') {
         return;
          }

          //从链接中找出页面ID
          hrefparts = target.href.split('/');
          pageid = hrefparts[hrefparts.length-1];
          pageid = pageid.replace('.html','');

          //更新页面
          ajaxRequest('xhr.php?page='+id,updatePageContents);

          //浏览器阻止默认行为并取消冒泡
          if (type of e.preventDefault === 'function') {
               e.preventDefault();
               e.stopPropagation();
          } else {
               e.returnValue=false;
               e.cancelBubble=true;
          }
};

跨浏览器兼容部分:

  1. 访问事件对象,并判断事件源
  2. 取消文档树中的冒泡(可选)
  3. 阻止默认动作(可选)

算法和流程控制

循环

循环的类型

ECMA-262标准第三版定义了javascript的基本语法和行为,其中共有四种循环。

  1. 第一种是标准的for循环。它由四部分组成:初始化、前测条件、后执行体、循环体。
           for (var i=0;i<10;i++){
                //do something
           }

for循环是javascript最常用的循环结构,直观的代码封装风格被开发者喜爱。

  2. while循环。while循环是最简单的前测循环,由一个前测条件和一个循环体构成。

  3. do-while循环是javascript唯一一种后测循环,由一个循环体和一个后测条件组成,至少会执行一次。
  4. for-in循环。可以枚举任何对象的属性名。
循环的性能

javascript提供的四种循环类型中,只有for-in循环比其他几种明显要慢。因为每次迭代操作会同时搜索实例或原型属性,for-in循环的每次迭代都会产生更多开销。速度只有其他类型循环的七分之一。除非你明确需要迭代一个属性数量未知的对象,否则应该避免使用for-in循环。如果你需要遍历一个数量有限的已知属性列表,使用其他循环类型会更快,比如数组。
除for-in外,其他循环类型的性能都差不多,类型的选择应该基于需求而不是性能。

提高循环的性能
  1. 减少每次迭代处理的事务
  2. 减少迭代的次数
减少迭代的工作量

减少对象成员及数组项的查找次数。

      在不影响的结果的情况下,可以使用倒序来略微提升性能。因为控制条件只要简单的与零比较。控制条件与true比较时,任何非零数会自动转换为true,而零值等同于false,实际上从两次比较(迭代数少于总数么?是否为true?)减少到一次比较(它是true么)。当循环复杂度为O(n)时,减少每次迭代的工作量是最有效的方法。当复杂度大于O(n)时,建议着重减少迭代次数。
减少迭代次数

Duff's Device是一个循环体展开技术,使得一次迭代中实际上执行了多次迭代的操作。一个典型的实现如下:

//credit:Jeff Greenberg
var iterations = Math.floor(items.length / 8),
      startAt = items.length/8,
      i = 0;
do{
      switch(startAt){
                case 0: process(items[i++]);
                case 7: process(items[i++]);
                case 6: process(items[i++]);
                case 5: process(items[i++]);
                case 4: process(items[i++]);
                case 3: process(items[i++]);
                case 2: process(items[i++]);
                case 1: process(items[i++]);
      }
      startAt = 0;
} while (--iterations);

Duff's Device背后的基本理念是:每次循环中最多可以调用8此process()。循环的迭代次数除以8。由于不是所有数字都能被8整除,变量startAt用来存放余数,表示第一次循环中应该调用多少次process()。
此算法稍快的版本取消了switch语句,并将余数处理和主循环分开

//credit:Jeff Greenberg
var i = items.length % 8;
while(i){
        process(item[i--]);
}
i = Math.floor(items.length / 8);
while(i){
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
}

尽管这种实现方法用两次循环代替之前的一次循环,但它移除了循环体中的switch语句,速度比原始循环更快。
如果循环迭代的次数小于1000,可能它与常规循环结构相比只有微不足道的性能提升。如果迭代数超过1000,那么执行效率将明显提升。例如在500000此迭代中,其运行时间比常规循环减少70%

基于函数的迭代

ECMA-262第四版加入的数组方法:forEach()方法。此方法遍历一个数组的所有成员,并在每个成员上执行一个函数。要运行的函数作为参数传给forEach(),并在调用时接受三个参数,分别是当前的值、索引以及数组本身。尽管基于函数的迭代提供了一个更为便利的迭代方法,但它仍比基于循环的迭代要慢一些。对每个数组项调用外部方法所带来的开销是速度慢的主要原因。

条件语句

if-else对比switch

条件数数量越大,越倾向于使用switch,主要是因为易读性。事实证明,大多数情况下switch比if-else运行得要快,但只有条件数量很大时才快得明显。

优化if-else

最小化到达正确分支前所需要判断的条件数量。最简单的优化方法是确保最可能出线的条件放在首位。if-else中的条件语句应该总是按照从最大概率到最小概率的顺序排列,以确保运行速度最快。假设均匀分部,可使用二分法的思想,重写为一系列嵌套的if-else语句。

查找表

有些时候优化条件语句的最佳方案是避免使用if-else和switch。可以使用数组和普通对象来构建查找表,通过查找表访问数据比用if-else或switch快很多。当单个键值存在逻辑映射时,构建查找表的优势就能体现出来。(比如把按照顺序的键值映射放到数组里)

递归

使用递归可以把复杂的算法变的简单。潜在问题是终止条件不明确或缺少终止条件会导致函数长时间运行,并使得用户界面处于假死状态和浏览器的调用栈大小限制。

调用栈限制

javascript引擎支持的递归数量与javascript调用栈大小直接相关。

递归模式

当你遇到调用栈大小限制时,第一步应该检查代码中的递归实例。有两种递归模式,第一种是调用自身,很容易定位错误。第二种是互相调用,很难定位。

迭代

任何递归能实现的算法同样可以使用迭代来实现。使用优化后的循环代替长时间运行的递归函数可以提升性能,因为运行一个循环比反复调用一个函数的开销要少的多。
归并排序算法是最常见的用递归实现的算法:

function merge(left, right) {
    var result = [];

    while (left.length > 0 && right.length > 0){
        if (left[0] < right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());    
        }
    }

    return result.concat(left).concat(right);
}

function mergeSort(items){
    if (items.length == 1){
        return items;
    }

    var middle = Math.floor(items.length / 2),
        left = items.slice(0, middle),
        right = items.slice(middle);
        return merge(mergeSort(left),mergeSort(right));
}

使用迭代实现归并算法:

//使用和上面相同的merge函数

function mergeSort(items){
    if (items.length == 1){
        return items;
    }

    var work = [];
    for (var i=0, len=items.length;i < len; i++){
        work.push([items[i]]);
    }
    work.push([]);

    for (var lim=len; lim>1; lim = (lim+1)/2){
        for (var j=0,k=0; k < lim; j++, k+=2){
            work[j] = merge(work[k],work[k+1]);
        }
        work[j] = [];
    }

    return work[0];
}

尽管迭代版本的归并排序算法比递归实现得要慢一些,但它不会像递归版本那样受到调用栈限制的影响。把递归算法改用迭代实现是避免栈溢出错误的方法之一

Memoization

Memoization是一种避免重复工作的方法,它缓存前一个计算结果供后续计算使用,避免了重复工作。
使用Memoization技术来重写阶乘函数:

function memfactorial(n){
    if(!memfactorial.cache){
        memfactorial.cache={
            "0":1,
            "1":1
        };
    }

    if(!memfactorial.cache.hasOwnProperty(n)){
        memfactorial.cache[n] = n * memfactorial (n-1);
    }

    return memfactorial.cache[n];
}

字符串和正则表达式

字符串链接

+和+=

不应在等号右边进行和被赋值的量无关的字符串拼接运算,这样会创造临时字符串。
例如:

str += "one" + "two";

会经历四个步骤:

  1. 在内存中创建一个临时字符串
  2. 连接后的字符串“onetwo”被赋值给该临时字符串
  3. 临时字符串与str当前的值连接
  4. 结果赋值给str

使用这种方式来代替:

str = str + "one" + "two";
//等价于 str = ((str + "one") + "two")

赋值表达式由str开始作为基础,每次给它附加一个字符串,由做到右一次连接,因此避免了使用临时字符串。

数组项合并

Array.prototype.join方法将数组的所有元素合并成一个字符串,它接受一个字符串参数作为分隔符插入每个元素的中间。大多数浏览器中,数组项合并比其他字符串连接的方法更慢。

String.prototype.concat

字符串的原生方法concat能接收任意数量的参数,并将每一个参数附加到所调用的字符串上。这是最灵活的字符串合并方法。多数情况下,使用concat比使用简单的+和+=稍慢。

正则表达式优化

部分匹配比完全不匹配所用的时间要长。

正则表达式工作原理
  1.  第一步编译

浏览器会验证正则表达式,然后把它转换为一个原生代码程序,用于执行匹配工 作。如果把正则对象赋值给一个变量,可以避免重复这一步。

  2.  第二步设置起始位置
  3.  第三步匹配每个正则表达式字元
  4.  第四步匹配成功或失败
回溯

当正则比到达时匹配目标字符串时,从左到右逐个测试表达式的组成部分,看是否能找到匹配项。在遇到量词和分支时,需要决策下一步如何处理。如果遇到量词,正则表达式需决定何时尝试匹配更多字符;如果遇到分支,那么必须从可选项中选择一个尝试匹配。每当正则表达式做类似的决定时,如果有必要的话,都会记录其他选择,以备返回时使用。如果当前选项匹配成功,正则表达式继续扫描表达式,如果其他部分也匹配成功,尼玛匹配结束。但是如果当前选项找不到匹配值,或后面的部分匹配失败,那么正则表达式会回溯到最后一个决策点,然后在剩余的选项中选择一个。这个过程会一直进行,知道找到匹配项,或者正则表达式中量词和分支选项的所有排列组合都尝试失败,那么它将放弃匹配从而移动到字符串的下一个字符,再重复此过程。

重复和回溯

贪婪匹配是段尾一个个回溯接下来的匹配内容,惰性正好相反;

回调失控

最佳实践:如果你的正则表达式包含了多个捕获组,那么你需要使用适当的反向引用次数。

嵌套量词与回溯失控

所谓的嵌套量词需要格外的关注且小心使用,以确保不会引发潜在的回溯失控。嵌套两次是指两次出线在一个自身被重复量词修饰的组中。确保正则表达式的两个部分不能对字符串的相同部分进行匹配

更多提高正则表达式效率的方法
1.  关于如何让正则匹配更快失败

正则表达式慢的原因通常是匹配失败的过程慢。

2.  正则表达式以简单、必需的字元开始

一个正则表达式的起始标记应当尽可能快速的测试并排除明显不匹配的位置。尽量以一个锚、特定字符串、字符类和单词边界开始,尽量避免以分组或选择字元开头,避免顶层分支。

3.  使用量词模式,使它们后面的字元互斥

当字符与字元相邻或子表达式能够重叠匹配时,正则表达式尝试拆解文本的路径数量将增加。

4.  减少分支数量,缩小分支范围

分支使用竖线|可能要求在字符串的每一个位置上测试所有的分支选项。你通常可以通过使用字符集和选项组件来减少对分支的需求,或将分支在正则表达式上的位置推后。

5.  使用非捕获组

捕获组消耗时间和内存来记录反向引用,并使它保持最新。如果你不需要一个反向引用,可以使用非捕获组来避免这些开销。

6.  只捕获感兴趣的文本以减少后处理

如果需要引用匹配的一部分,应该才去一切手段捕获那些片段,再使用反向引用来处理。

7.  暴露必需的字元

尝试让正则表达式引擎更容易判断哪些字元是必需的。

8.  使用合适的量词
9.  把正则表达式赋值给变量并重用它们

避免在循环体中重复编译正则表达式。

10.  将复杂的正则表达式拆分为简单的片段

何时不使用正则表达式

当只是搜索字面字符串,尤其是事先知道字符串的哪一部分将要被查找时。正则表达式无法直接跳到字符串末尾而不考虑沿途的字符。


快速响应的用户界面

浏览器UI线程

用于执行Javascript和更新用户界面的进程通常被称为“浏览器UI线程”。UI线程的工作基于一个见到那的队列系统,任务会被保存到队列中直到线程空闲。

浏览器限制

浏览器限制了javascript的运行时间。此类限制分为两种:调用栈的大小限制和长时间运行脚本限制。

多久算太久

单个Javascript操作话费的总时间不应该超过100毫秒。
最佳实践:限制所有的Javascript任务在100毫秒或更短的时间内完成。

使用定时器让出时间片段

当Javascript不能在100毫秒或更短的时间内完成。最理想的方法是让出UI线程的控制权,使得UI可以更新。

定时器基础

在Javascript中可以使用setTimeout()和setInterval()创建定时器,它们接收相同的参数:要执行的函数和执行前的等待时间。定时器与UI线程的交互:定时器会告诉Javascript引擎先等待一定时间,然后添加一个Javascript任务到UI队列。定时器代码只有在创建它的函数执行完之后,才有可能执行。无论发生何种情况,创建一个定时器会造成UI线程暂停,如同它从一个任务切换到下一个任务。因此,定时器代码会重置所有相关的浏览器限制,包括 长时间运行脚本定时器。此外,调用栈也会在定时器中重置为0。setTimeout()和setInterval()几近相同,如果 UI队列中已经存在由同一个setInterval()创建的任务,那么后续任务不会被添加到UI队列中。如果setTimeout()中的函数需要消耗比定时器延时更长的运行时间,那么定时器代码中的延时几乎是不可见的。

定时器的精度

Javascript定时器延迟通常不太准确,相差大约为几毫秒,无法用来精确计算时间。而且还存在最小值的限制。

使用定时器处理数组

是否可以用定时器取代循环的两个决定性因素:处理过程是否必须同步;数据是否必须按照顺序处理;如果两个答案都是否,那么代码适用于定时器分解任务。

var todo = items.concat();
// 克隆原数组

setTimeout(function(){

    // 取得数组的下一个元素并进行处理
    process(todo.shift());

    // 如果还有需要处理的元素,创建另一个定时器
    if(todo.length > 0){
        setTimeout(arguments.callee, 25);
    } else {
        callback(items);
    }

}, 25);

每个定时器的真实延时在很程度上取决于具体情况。普遍来讲,最好使用至少25毫秒,因为再小的延时,对大多数UI更新来说不够用。

记录代码运行使劲啊

通过定时器创建Date对象并比较它们的值来记录代码运行事件。加号可以将Date对象转换成数字,那么在后续的运算中就无须转换了。避免把任务分解成过于零碎的碎片,因为定时器之间有最小间隔,会导致出线空闲事件。

定时器与性能

当多个重复的定时器同时创建往往会出线性能问题。因为只有一个UI线程,而所有的定时器都在争夺运行时间。那些间隔在1秒或1秒以上的低频率的重复定时器几乎不会影响Web应用的响应速度。这种情况下定时器延迟远远超过UI线程产生瓶颈的值,可以安全的重复使用。当过个定时器使用较高的频率(100到200毫秒之间)时,会明显影响性能。在web应用中限制高频率重复定时器的数量,作为代替方案,使用一个独立的重复定时器每次执行多个操作。

Web Worker

引入了一个接口,能使代码运行并且不占用浏览器UI线程的时间。

Worker

没有绑定UI线程,每个Web Worker都有自己的全局环境,其功能只是Javascript特性的一个子集。运行环境由如下部分组成:一个navigator对象,值包括四个属性:appName、appVersion、userAgent和platform。
一个location对象(与window.location相同,不过所有属性都是只读的。)。
一个self对象,指向全局worker对象。
一个importScipt()方法,用来加载Worker所用到的外部javascript文件。
所有的ECMAScript对象
XMLHttpRequest构造器
setTimeout()方法和setInterval()方法
一个close()方法,它能立刻停止Worker运行
由于Web Worker有着不同的全局运行环境,因此你无法从javascript代码中创建它。需要创建一个完全独立的javascript文件,其中包含了需要在Worker中运行的代码。要创建网页人工线程,你必须传入这个javascript文件的URL;

与Worker通信

通过事件接口进行通信。网页代码可以通过postMessage()方法给Worker传递数据,它接受一个参数,即需要传递给Worker的数据。此外,Worker还有一个用来接收信息的onmessage事件处理器。Worker可通过它自己的postMessage()方法把信息回传给页面。消息系统是网页和Worker通信的唯一途径。只有特定类型的数据可以使用postMessage()传递。你可以传递原始值(字符串、数字、布尔值、null和undefined),也可以传递Object和Array的实例,其他类型就不允许了。有效数据会被序列化,传入或传出Worker,然后反序列化。虽然看上去对象可以直接传入,但对象实例完全是相同数据的独立表述。

加载外部文件

Worker 通过importScript()方法加载外部javascript文件,该方法接收一个或多个javascript文件URL作为参数。importScript()的调用过程是阻塞式的,知道所有所有文件加载并执行完成之后,脚本才会继续运行。由于Worker在UI线程之外运行,所以这种阻塞并不会影响UI响应。
Web Worker适合用于那些处理纯数据,或者与浏览器UI无关的长时间运行脚本。尽管它看上去用处不大,但Web应用中通常有一些数据处理功能将收益于Worker而不是定时器。
可能的用处:

  1. 编码/解码大字符串
  2. 复杂数学运算
  3. 大数组排序
  4. 任何超过100毫秒的处理过程,都应当考虑Worker方案是不是比基于定时器的方案更为合适。

Ajax

Ajax是高性能javascript的基础。它可以通过延迟下载体积较大的资源文件来使得页面加载速度更快。它通过异步的方式在客户端和服务端之间传输数据,避免同时传输大量数据。

数据传输

请求数据

有五种常用技术用于想服务器请求数据:

  1. XMLHttpRequest
  2. Dynamic script tag insertion(脚本动态注入)
  3. iframes
  4. Comet
  5. Multipart XHR

现代高性能Javascript中使用的三种技术是:XHR、动态脚本注入和Multipart XHR

XMLHttpRequest

XMLHttpRequest是目前最常用的技术,它允许异步发送和接收数据。由于XHR提供了高级的控制,所以浏览器对其增加了一些限制。你不能使用XHR从外域请求数据。对于那些不会改变服务器状态,只会获取数据(幂等行为)的请求,应该使用GET。经GET请求的数据会被缓存起来,如果需要多次请求统一数据的话,它会有助于提升性能。只有当请求的URL加上参数的长度接近或超过2048个字符时,才应该用POST获取数据。因为IE限制URL长度,过长将导致请求的URL被截断。

动态脚本注入

这种技术客服了XHR的最大限制:它能跨域请求数据。这是一个Hack,你不需要实例化一个专用对象,而可以使用javascript创建一个新的脚本标签,并设置它的src属性为不同域的URL。与XHR相比,动态脚本注入提供的控制是有限的。只能使用GET方法而不是POST方法。不能设置请求的超时处理或重试;不能访问请求的头部信息,不能把整个响应信息作为字符串来处理。因为响应消息作为脚本标签的源码,它必须是可执行的javascript代码。你不能使用纯XML、纯JSOn或其他任何格式的数据,无论哪种格式,都必须封装在一个回调函数中。这项技术的速度却非常快。响应消息是作为javascript执行,而不是作为字符串需要进一步处理。正因如此,它有潜力成为客户端获取并解析数据最快的方法。

Multipart XHR

允许客户端只用一个HTTP请求就可以从服务端向客户端传送多个字元。它通过在服务端将字元打包成一个由双方约定的字符串分割的长字符串并发送到客户端。然后用javascript代码处理这个长字符串,并根据它的mime-type类型和传入的其他“头信息”解析出每个资源。缺点:资源不能被浏览器缓存。
能显著提高性能的场景:
页面包含了大量其他地方用不到的资源,尤其是图片;
网站已经在每个页面中使用了一个独立打包的Javascript或CSS文件以减少http请求;

发送数据

XMLHttpRequest
当使用XHR发送数据到服务器时,GET方式会更快。这是因为,对少量数据而言一个GET请求只发送一个数据包。而一个POST请求至少要发两个数据包,一个装载头信息,另一个装载POST正文。POST更适合发送大量数据到服务器,因为它不关心额外数据包的数量,另一个原因是URL长度有限制,它不可能使用过长的GET请求。

Beacons

类似动态脚本注入。使用Javascript创建一个新的Image对象,并把src属性设置为服务器上脚本的URL。该URL包含了我们要通过GET传回的键值对数据。服务器会接受数据并保存下来,无须向客服端发送任何回馈信息,因此没有图片会实际显示出来。这是回传信息最有效的方式。性能消耗更小,而且服务器端的错误不影响客户端。缺点:无法发送POST数据,而URL的长度有最大值,所以可以发送的数据的长度被限制的相当小。

数据格式

考虑数据格式时唯一需要比较的标准就是速度

XML

当Ajax最先开始流行时,它选择了XML作为数据格式。优势:极佳的通用性、格式严格,且易于验证。缺点:冗长,依赖大量结构、有效数据的比例很低、语法模糊,如果有其他格式可选不要使用它。

JSON

是一种使用Javascript对象和数组直接量编写的轻量级且易于解析的数据格式。

JSON-P

事实上,JSON可以被本地执行会导致几个重要的性能影响。当使用XHR时,JSON数据被当成字符串返回。在使用动态脚本注入时,JSON数据要被当成另一个Javascript文件并作为原生代码执行,为实现这一点必须封装在一个回调函数中。JSON-P因为回调包装的原因略微增大了文件尺寸,但性能提升巨大。由于数据是当作原生的Javascript,因此解析速度跟原生Javascript一样快。最快的JSON格式是使用数组形式的JSON-P。不要把敏感数据编码在JSON-P中,因为无法确认它是否保持着私有调用状态。

HTML

通常你请求的数据需要被转换成HTML以显示到页面上。Javascript可以较快地把一个较大的数据结构转换成简单的HTML,但在服务器处理会快很多。一种可考虑的技术是在服务器上构建好整个HTML再传回客户端,Javascript可以很方便地通过innerHTML属性把它插入页面相应的位置。取点:臃肿的数据格式、比XML更繁杂。在数据本身的最外层,可以嵌套HTML标签,每个都带有id、class和其他属性。HTML格式可能比实际数据占用更多空间。应当在客户端的瓶颈是CPU而不是带宽时才使用此技术。

自定义格式

理想的数据格式应该只包含必要的结构,以便你可以分解出每个独立的字段。最重要的决定就是采用哪种分隔符,它应当是一个单字符,而且不应该存在你的数据中。

Ajax性能指南

缓存数据

在服务端,设置HTTP头信息以确保你的响应会被浏览器缓存。
在客户端,把获取到的信息存储到本地,从而避免再次请求。

设置HTTP头信息

如果希望ajax能被浏览器缓存,那么你必须使用GET方式发送请求并且需要在响应中发送正确的HTTP头信息。Expires头信息会告诉浏览器应该缓存多久。它的值是一个日期。

本地数据存储

直接把从服务器接收到的数据储存起来。可以把响应文本保存到一个对象中,以URL为键值作为索引。

Ajax类库的局限性

所有的Javascript类库都允许你访问一个Ajax对象,它屏蔽了浏览器之间的差异,给你一个统一的接口。为了统一接口的功能,类库简化接口,使得你不能访问XMLHttpRequest的完整功能。


编程实践

避免双重求值

Javascript允许你在程序中提取一个包含代码的字符串,然后动态执行它。有四种标准方法可以实现:eval()、Function()构造函数、setTimeout()和setInterval()。首先会以正常的方式求值,然后在执行的过程中对包含于字符串的代码发起另一个求值运算。每次使用这些方法都要创建一个新的解释器/编译器实例,导致消耗时间大大增加。
大多数时候没有必要使用eval()和Function(),因此最好避免使用它们。定时器则建议传入函数而不是字符串作为第一个参数。

使用Object/Array直接量

Javascript中创建对象和数组的方法有多种,但使用对象和数组直接量是最快的方式。

避免重复工作

别做无关紧要的工作,别重复做已经完成的工作。

延迟加载

第一次被调用时,会先检查并决定使用哪种方法去绑定或取消绑定事件处理器。然后原始函数被包含正确操作的新函数覆盖。最后一步调用新的函数,并传入原始参数。随后每次调用都不会再做检测,因为检测代码已经被新的函数覆盖。调用延迟加载函数时,第一次总会消耗较长的费时间,因为它必须运行检测接着再调用另一个函数完成任务。但随后调用相同的函数会更快,因为不需要再执行检测逻辑。当一个函数在页面中不会立刻调用时,延迟加载是最好的选择。

条件预加载

它会在脚本加载期间提前检测,而不会等到函数被调用。检测的操作依然只有一次,知识它在过程中来的更早。条件预加载确保所有函数调用消耗的时间相同。其代价是需要在脚本加载时就检测,而不是加载后。预加载适用于一个函数马上就要被用到,并且在整个页面的生命周期中频繁出现的场合。

使用快的部分

运行速度慢的部分实际上是代码,引擎通常是处理过程中最快的部分。

位操作

使用位运算代替纯数学操作:对2的取模运算可以被&1代替,速度提高很多。位掩码:处理同时存在多个布尔选项时的情形,思路是使用单个数字的每一位来判定是否选项成立,从而有效得把数字转换为布尔值标记组成的数组。

原生方法

原生方法更快,因为写代码前就存在浏览器中了,并且都是用底层语言比如c++编写的。经验不足的Javascript开发者经常犯的错误就是在代码中进行复杂的数学运算,而没有使用内置的Math对象中那些性能更好的版本。另一个例子是选择器API,它允许使用CSS选择器来查找DOM节点。原生的querySelector()和querySelectorAll()方法完成任务平均所需时间是基于Javascript的CSS查询的10%。


构建并部署高性能Javascript应用

合并多个Javascript文件,网站提速指南中第一条也是最重要的一条规则,就是减少http请求数。

预处理Javascript文件

预处理你的Javascript源文件并不会让应用变的更快,但它允许你做些其他的事情,例如有条件地插入测试代码,来衡量你的应用程序的性能。

Javascript压缩

指的是把Javascript文件中所有与运行无关的部分进行剥离的过程。剥离的内容包括注释和不必要的空白字符。该过程通常可以将文件大小减半,促使文件更快被下载,并鼓励程序员编写更好的更详细的注释。

构建时处理对比运行时处理

普遍规则是只要能在构建时完成的工作,就不要留到运行时去做。

Javascript的http压缩

当Web浏览器请求一个资源时,它通常会发送一个Accept-Encoding HTTP头来告诉Web服务器它支持哪种编码转换类型。这个信息主要用来压缩文档以更快的下载,从而改善用户体验。Accept-Encoding可用的值包括:gzip、compress、deflate和identity。gzip是目前最流行的编码方式。它通常能减少70%的下载量,成为提升Web应用性能的首选武器。记住Gzip压缩主要适用于文本,包括Javascript文件。

缓存Javascript文件

缓存HTTP组件能极大提高网站回访用户的体验。Web服务器通过Expires HTTP响应头来告诉客户端一个字元应当缓存多长事件。它的值是一个遵循RFC1123标准的绝对时间戳。

处理缓存问题

适当的缓存控制能提升用户体验,但它有一个缺点:当应用升级时,你需要确保用户下载到最新的静态内容。这个问题可以通过把改动过的静态资源重命名来解决。

使用内容分发网络(CDN)

内容分发网络是在互联网上按地理位置设置分部计算机网络,它负责传递内容给终端用户。使用CDN的主要原因是增强Web应用的可靠性、可扩展性,更重要的是提升性能。事实上,通过向地理位置最近的用户输入内容,CDN能极大减少网络延时。

查看原文

赞 1 收藏 11 评论 0

undefined 发布了文章 · 2018-01-23

浏览器HTTP缓存机制

介绍HTTP缓存之前先简单说一下Web缓存

Web缓存是一种保存Web资源副本并在下次请求时直接使用该副本的技术。

       Web缓存可以分为这几种:浏览器缓存、CDN缓存、服务器缓存、数据库数据缓存 。因为可能会直接使用副本免于重新发送请求或者仅仅确认资源没变无需重新传输资源实体,Web缓存可以减少延迟加快网页打开速度、重复利用资源减少网络带宽消耗、降低请求次数或者减少传输内容从而减轻服务器压力。

       这篇文章主要讨论和前端密切相关的浏览器HTTP缓存机制。浏览器HTTP缓存可以分为强缓存协商缓存强缓存协商缓存最大也是最根本的区别是:强缓存命中的话不会发请求到服务器(比如chrome中的200 from memory cache),协商缓存一定会发请求到服务器,通过资源的请求首部字段验证资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的实体,而是通知客户端可以从缓存中加载这个资源(304 not modified)。
强缓存与协商缓存

浏览器HTTP缓存由HTTP报文的首部字段决定

报文结构

控制强缓存的字段按优先级介绍

1.Pragma
       Pragma是HTTP/1.1之前版本遗留的通用首部字段,仅作为于HTTP/1.0的向后兼容而使用。虽然它是一个通用首部,但是它在响应报文中时的行为没有规范,依赖于浏览器的实现。RFC中该字段只有no-cache一个可选值,会通知浏览器不直接使用缓存,要求向服务器发请求校验新鲜度。因为它优先级最高,当存在时一定不会命中强缓存。

2.Cache-Control
       Cache-Control是一个通用首部字段,也是HTTP/1.1控制浏览器缓存的主流字段。和浏览器缓存相关的是如下几个响应指令:

指令参数说明
private表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)
public可省略表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存
no-cache可省略缓存前必需确认其有效性
no-store不缓存请求或响应的任何内容
max-age=[s]必需响应的最大值
  • max-age(单位为s)设置缓存的存在时间,相对于发送请求的时间。只有响应报文首部设置Cache-Control为非0的max-age或者设置了大于请求日期的Expires(下文会讲)才有可能命中强缓存。当满足这个条件,同时响应报文首部中Cache-Control不存在no-cacheno-store且请求报文首部不存在Pragma字段,才会真正命中强缓存。以下所有图片均为刷新(command+R)的截图。

响应报文首部no-cache和max-age同时存在
响应报文首部no-store和max-age同时存在
请求报文首部Pragma,响应报文首部max-age

  • no-cache 表示请求必须先与服务器确认缓存的有效性,如果有效才能使用缓存(协商缓存),无论是响应报文首部还是请求报文首部出现这个字段均一定不会命中强缓存。Chrome硬性重新加载(Command+shift+R)会在请求的首部加上Pragma:no-cacheCache-Control:no-cache
  • no-store 表示禁止浏览器以及所有中间缓存存储任何版本的返回响应,一定不会出现强缓存和协商缓存,适合个人隐私数据或者经济类数据。
  • public 表明响应可以被浏览器、CDN等等缓存。
  • private 响应只作为私有的缓存,不能被CDN等缓存。如果要求HTTP认证,响应会自动设置为private

3.Expires
       Expires是一个响应首部字段,它指定了一个日期/时间,在这个时间/日期之前,HTTP缓存被认为是有效的。无效的日期比如0,表示这个资源已经过期了。如果同时设置了Cache-Control响应首部字段的max-age,则Expires会被忽略。它也是HTTP/1.1之前版本遗留的通用首部字段,仅作为于HTTP/1.0的向后兼容而使用。

控制协商缓存的字段

1.Last-Modified/If-Modified-Since
       If-Modified-Since是一个请求首部字段,并且只能用在GET或者HEAD请求中。Last-Modified是一个响应首部字段,包含服务器认定的资源作出修改的日期及时间。当带着If-Modified-Since头访问服务器请求资源时,服务器会检查Last-Modified,如果Last-Modified的时间早于或等于If-Modified-Since则会返回一个不带主体的304响应,否则将重新返回资源。

If-Modified-Since: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

2.ETag/If-None-Match
       ETag是一个响应首部字段,它是根据实体内容生成的一段hash字符串,标识资源的状态,由服务端产生。If-None-Match是一个条件式的请求首部。如果请求资源时在请求首部加上这个字段,值为之前服务器端返回的资源上的ETag,则当且仅当服务器上没有任何资源的ETag属性值与这个首部中列出的时候,服务器才会返回带有所请求资源实体的200响应,否则服务器会返回不带实体的304响应。ETag优先级比Last-Modified高,同时存在时会以ETag为准。

If-None-Match: <etag_value>
If-None-Match: <etag_value>, <etag_value>, …
If-None-Match: *

ETag与If-None-Match

ETag属性之间的比较采用的是弱比较算法,即两个文件除了每个比特都相同外,内容一致也可以认为是相同的。例如,如果两个页面仅仅在页脚的生成时间有所不同,就可以认为二者是相同的。

因为ETag的特性,所以相较于Last-Modified有一些优势:

    1.  某些情况下服务器无法获取资源的最后修改时间
    2.  资源的最后修改时间变了但是内容没变,使用ETag可以正确缓存
    3.  如果资源修改非常频繁,在秒以下的时间进行修改,Last-Modified只能精确到秒

整体流程

整体流程

求赞,欢迎访问我的博客

查看原文

赞 2 收藏 5 评论 0

undefined 发布了文章 · 2018-01-21

使用Nightwatch进行E2E测试中文教程

E2E测试

E2E(end to end)测试是指端到端测试又叫功能测试,站在用户视角,使用各种功能、各种交互,是用户的真实使用场景的仿真。在产品高速迭代的现在,有个自动化测试,是重构、迭代的重要保障。对web前端来说,主要的测试就是,表单、动画、页面跳转、dom渲染、Ajax等是否按照期望。

E2E测试驱动重构

重构代码的目的是什么?是为了使代码质量更高、性能更好、可读性和拓展性更强。在重构时如何保证修改后正常功能不受影响?E2E测试正是保证功能的最高层测试,不关注代码实现细节,专注于代码能否实现对应的功能,相比于单元测试、集成测试更灵活,你可以彻底改变编码的语法、架构甚至编程范式而不用重新写测试用例。

Nightwatch

知道nightwatch是因为vue-cli工具安装的时候会询问是否需要安装nightwatch。本身vue项目也是使用nightwatch来e2e测试的。nightwatch是一个使用selenium或者webdriver或者phantomjs的nodejs编写的e2e自动测试框架,可以很方便的写出测试用例来模仿用户的操作来自动验证功能的实现。selenium是一个强大浏览器测试平台,支持firefox、chrome、edge等浏览器的模拟测试,其原理是打开浏览器时,把自己的JavaScript文件嵌入网页中。然后selenium的网页通过frame嵌入目标网页。这样,就可以使用selenium的JavaScript对象来控制目标网页。

Nightwatch安装

通过npm安装nightwatch。

$ npm install [-g] nightwatch

根据需要安装Selenium-server或者其他Webdriver,比手动去下载jar文件要方便很多。安装哪些Webdriver取决于你想要测试哪些浏览器,如果只测试Chrome甚至可以不装Selenium-server

$ npm install selenium-server
$ npm install chromedriver

Nightwatch的配置

nightwatch的使用很简单,一个nightwatch.json或者nightwatch.config.js(后者优先级高)配置文件,使用runner会自动找同级的这两个文件来获取配置信息。也可以手动使用--config来制定配置文件的相对路径。

{
  "src_folders" : ["tests"],
  "output_folder" : "reports",
  "custom_commands_path" : "",
  "custom_assertions_path" : "",
  "page_objects_path" : "",
  "globals_path" : "",

  "selenium" : {
    "start_process" : false,
    "server_path" : "",
    "log_path" : "",
    "port" : 4444,
    "cli_args" : {
      "webdriver.chrome.driver" : "",
      "webdriver.gecko.driver" : "",
      "webdriver.edge.driver" : ""
    }
  },

  "test_settings" : {
    "default" : {
      "launch_url" : "http://localhost",
      "selenium_port"  : 4444,
      "selenium_host"  : "localhost",
      "silent": true,
      "screenshots" : {
        "enabled" : false,
        "path" : ""
      },
      "desiredCapabilities": {
        "browserName": "firefox",
        "marionette": true
      }
    },

    "chrome" : {
      "desiredCapabilities": {
        "browserName": "chrome"
      }
    },

    "edge" : {
      "desiredCapabilities": {
        "browserName": "MicrosoftEdge"
      }
    }
  }
}

json配置文件大概就是上面这样,分为基本配置、selenium配置和测试配置三个部分。基本配置依次为测试用例源文件路径、输出路径、基础指令路径、全局配置路径等。selenium设置包括是否开启、路径、端口等,cli_args指定将要运行的webdriver。test_settings制定测试时各个环境的设置,默认是default,通过--env加环境名可以指定配置的任意环境。只要把测试用例放在对应的文件夹使用module.exports暴露一个对象,其中key是测试名,value是一个接受browser实例的函数,在函数中进行断言,nightwatch会自动依次调用文件夹中的测试用例。一个简易的Chrome headless模式的nightwatch.conf.js配置如下:

{
    'src_folders': ['test/e2e/specs'],
    'output_folder': 'test/e2e/reports',
    'globals_path': 'test/e2e/global.js',
    'selenium': {
        'start_process': true,
        'server_path': require('selenium-server').path,
        'port': port,
        'cli_args': {
            'webdriver.chrome.driver': require('chromedriver').path
        }
    },

    'test_settings': {
        'default': {
            'selenium_port': port,
            'selenium_host': 'localhost',
            'silent': true,
            'globals': {
                'productListUrl': 'http://localhost:' + 9003 + '/productlist.html',
            }
        },

        'chrome': {
            'desiredCapabilities': {
                'browserName': 'chrome',
                'javascriptEnabled': true,
                'acceptSslCerts': true,
                'chromeOptions': {
                    'args': [
                       '--headless',
                     '--disable-gpu'
                    ],
                    'binary': '/opt/google/chrome/google-chrome'
                }
            }
        },

        'globals': {
            'productListUrl': 'http://localhost:' + 9003 + '/productlist.html',
        }
    }
}

API

Nightwatch的API分为四个部分

1.Expect

在browser实例上以.expect.element开头的BDD(行为驱动测试)风格的接口,0.7及以上版本nightwatch可用。通过.element方法传入一个selector(参考querySelector或者jq的语法)获取到dom实例,通过.text、.value、.attribute等方法获取到实例属性。还有一些语意明确的修饰:

  • to
  • be
  • been
  • is
  • that
  • which
  • and
  • has
  • with
  • at
  • does
  • of

再加上比较判断:

.equal(value)/.contain(value)/.match(regex)

.selected

.present

还有时间修饰.before(ms)(表示一段时间之内)、.after(ms)(表示一段时间之后)。就像造句一样:某某元素的某某属性(在某某时间)(不)等于什么值,这就是BDD风格的测试代码。例如:

this.demoTest = function (browser) {
      browser.expect.element('body').to.have.attribute('data-attr');
      browser.expect.element('body').to.not.have.attribute('data-attr');
      browser.expect.element('body').to.not.have.attribute('data-attr', 'Testing if body does not have data-attr');
      browser.expect.element('body').to.have.attribute('data-attr').before(100);
      browser.expect.element('body').to.have.attribute('data-attr')
    .equals('some attribute');
      browser.expect.element('body').to.have.attribute('data-attr')
    .not.equals('other attribute');
      browser.expect.element('body').to.have.attribute('data-attr')
    .which.contains('something');
      browser.expect.element('body').to.have.attribute('data-attr')
    .which.matches(/^something\ else/);
};

2.Assert

以.assert/.verify开头的两套相同的方法库,区别是assert如果断言失败则退出整个测试用例所有步,verify则打印后继续进行。

this.demoTest = function (browser) {
      browser.verify.title("Nightwatch.js");
      browser.assert.title("Nightwatch.js");
};

有如下判断方法:

.attributeContains(selector, attribute, expected[, message])
检查指定元素(selector)的指定属性(attribute)是否包含有期待的值(expected)打印出指定信息(可选填的message)其他方法讲解类似,不一一赘述

.attributeEquals(selector, attribute, expected[, message])
检查元素指定属性是否等于预期

.containText(selector, expectedText[, message])
包含有指定的文本

.cssClassPresent(selector, className[, message])
检查元素指定class是否存在

.cssClassNotPresent(selector, className[, message])
检查元素指定class是否不存在

.cssProperty(selector, cssProperty, expected[, message])
检查元素指定css属性的值是否等于预期

.elementPresent(selector[, message)
检查指定元素是否存在于DOM中

.elementNotPresent(selector[, message)
检查指定元素是否不存在于DOM中

.hidden(selector[, message)
检查指定元素是否不可见

.title(expected[, message])
检查页面标题是否等于预期

.urlContains(expectedText[, message])
检查当前URL是否包含预期的值

.urlEquals(expected[, message])
检查当前URL是否等于预期的值

.value(selector, expectedText[, message])
检查指定元素的value是否等于预期

.valueContains(selector, expectedText[, message])
检查指定元素的value是否包含预期的值

.visible(selector[, message)
检查指定元素是否可见

3.Commands

很多命令的读写,可以操作BOM、DOM对象:

.clearValue(selector[, message])
清空input、textarea的值

.click(selector[, callback])
callback为执行完命令后需要执行的回调

.closeWindow([callback])

.deleteCookie(cookieName[, callback])

.deleteCookies([callback])

.end([callback])
结束会话(关闭窗口)

.getAttribute(selector, attribute, callback)

.getCookie(cookieName, callback)

.getCookies(callback)

.getCssProperty(selector, cssProperty, callback)

.getElementSize(selector, callback)

.getLocation(selector, callback)

.getLocationInView(selector, callback)

.getLog(typeString, callback)
获取selenium的log,其中type为string或者function

.getLogTypes(callback)

.getTagName(selector, callback)

.getText(selector, callback)

.getTitle(callback)

.getValue(selector, callback)

.init([url])
url方法的别名,如果不传url则跳转到配置中的launch_url

.injectScript(scriptUrl[, id, callback])
注入script

.isLogAvailable(typeString, callback)
typeString为string或者function,用来测试log的type是否可用

.isVisible(selector, callback)

.maximizeWindow([callback])
最大化当前窗口

.moveToElement(selector, xoffset, yoffset[, callback])
移动鼠标到相对于指定元素的指定位置

.pause(ms[, callback])
暂停指定的时间,如果没有时间,则无限暂停

.perform(callback)
一个简单的命令,允许在回调中访问api

.resizeWindow(width, height[, callback])
调整窗口的尺寸

.saveScreenshot(fileName, callback)

.setCookie(cookie[, callback])

.setValue(selector, inputValue[, callback])

.setWindowPosition(offsetX, offsetY[, callback])

.submitForm(selector[, callback])

.switchWindow(handleOrName[, callback])

.urlHash(hash)

.useCss()
设置当前选择器模式为CSS

.useXpath()
设置当前选择器模式为Xpath

.waitForElementNotPresent(selector, time[, abortOnFailure, callback, message])
指定元素指定时间内是否不存在

.waitForElementNotVisible(selector, time[, abortOnFailure, callback, message])
指定元素指定时间内是否不可见

.waitForElementPresent(selector, time[, abortOnFailure, callback, message])

.waitForElementVisible(selector, time[, abortOnFailure, callback, message])

简单的例子:

this.demoTest = function (browser) {
    browser.click("#main ul li a.first", function(response) {
    this.assert.ok(browser === this, "Check if the context is right.");
    this.assert.ok(typeof response == "object", "We got a response object.");
    });
};

4.webdriver protocol

可以操作一些更底层的东西,比如:

  • Sessions
  • Navigation
  • Command Contexts
  • Elements
  • Element State
  • Element Interaction
  • Element Location
  • Document Handling
  • Cookies
  • User Actions
  • User Prompts
  • Screen Capture
  • Mobile Related

简单的例子:

module.exports = {
 'demo Test' : function(browser) {
    browser.element('css selector', 'body', function(res) {
      console.log(res)
    });
  }
};

拓展

也可以单独使用chromedriver等进行单一平台测试,效率更高,测试更快。只需要npm安装chromedriver或者其他webdriver,不需要selenium,在selenium设置中把selenium进程设置为false,测试环境配置中做出相应的改变。在golobal_path设置的配置文件中,利用nightwatch测试的全局before和after钩子中开、关服务器就好:

var chromedriver = require('chromedriver');

function startChromeDriver() {
  chromedriver.start();
}

function stopChromeDriver() {
  chromedriver.stop();
}

module.exports = {
  before : function(done) {
    startChromeDriver.call(this);
    done();
  },

  after : function(done) {
    stopChromeDriver.call(this);
    done();
  }
};

配置尤雨溪大神的nightwatch-helpers食用更佳,补了一些api。Assertions:

  • count(selector, count)
  • attributePresent(selector, attr)
  • evaluate(fn, [args], [message])
  • checked(selector, expected)
  • focused(selector, expected)
  • hasHTML(selector, html)
  • notVisible(selector)

Commands:

  • dblClick(selector)
  • waitFor(duration)
  • trigger(selector, event[, keyCode])
  • enterValue(selector, value)

只需要在图中位置配置一下即可image.png

其他

推荐使用Headless测试即不打开浏览器可视界面以便能跑在服务器上。比如Phantomjs可以模拟webkit内核浏览器的行为,在Nightwatch中配置一下Phantomjs环境即可,启动nightwatch时使用--env加上配置里的环境名激活对应的环境。如今(59版本以上)Phantomjs已经停止维护,使用Chrome自带的headless模式是更好的选择。也可以使用Puppeteer来做E2E测试,好处是只依赖一个Puppeteer,并且API相对简单。

欢迎来我博客

查看原文

赞 3 收藏 2 评论 0

undefined 发布了文章 · 2018-01-21

Puppeteer的入门教程和实践

出现的背景

Chrome59(linux、macos)、 Chrome60(windows)之后,Chrome自带headless(无界面)模式很方便做自动化测试或者爬虫。但是如何和headless模式的Chrome交互则是一个问题。通过启动Chrome时的命令行参数仅能实现简易的启动时初始化操作。Selenium、Webdriver等是一种解决方案,但是往往依赖众多,不够扁平。

Puppeteer.png

Puppeteer是谷歌官方出品的一个通过DevTools协议控制headless Chrome的Node库。可以通过Puppeteer的提供的api直接控制Chrome模拟大部分用户操作来进行UI Test或者作为爬虫访问页面来收集数据。

环境和安装

Puppeteer本身依赖6.4以上的Node,但是为了异步超级好用的async/await,推荐使用7.6版本以上的Node。另外headless Chrome本身对服务器依赖的库的版本要求比较高,centos服务器依赖偏稳定,v6很难使用headless Chrome,提升依赖版本可能出现各种服务器问题(包括且不限于无法使用ssh),最好使用高版本服务器。

Puppeteer因为是一个npm的包,所以安装很简单:

npm i puppeteer

或者

yarn add puppeteer

Puppeteer安装时自带一个最新版本的Chromium,可以通过设置环境变量或者npm config中的PUPPETEER_SKIP_CHROMIUM_DOWNLOAD跳过下载。如果不下载的话,启动时可以通过puppeteer.launch([options])配置项中的executablePath指定Chromium的位置。

使用和例子

Puppeteer类似其他框架,通过操作Browser实例来操作浏览器作出相应的反应。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://rennaiqian.com');
  await page.screenshot({path: 'example.png'});
  await page.pdf({path: 'example.pdf', format: 'A4'});
  await browser.close();
})();

上述代码通过puppeteer的launch方法生成了一个browser的实例,对应于浏览器,launch方法可以传入配置项,比较有用的是在本地调试时传入{headless:false}可以关闭headless模式。

const browser = await puppeteer.launch({headless:false})

browser.newPage方法可以打开一个新选项卡并返回选项卡的实例page,通过page上的各种方法可以对页面进行常用操作。上述代码就进行了截屏和打印pdf的操作。

一个很强大的方法是page.evaluate(pageFunction, ...args),可以向页面注入我们的函数,这样就有了无限可能

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://rennaiqian.com');

  // Get the "viewport" of the page, as reported by the page.
  const dimensions = await page.evaluate(() => {
    return {
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
      deviceScaleFactor: window.devicePixelRatio
    };
  });

  console.log('Dimensions:', dimensions);
  await browser.close();
})();

需要注意的是evaluate方法中是无法直接使用外部的变量的,需要作为参数传入,想要获得执行的结果也需要return出来。因为是一个开源一个多月的项目,现在项目很活跃,所以使用时自行查找api才能保证参数、使用方法不会错。

调试技巧

  1. 关掉无界面模式,有时查看浏览器显示的内容是很有用的。使用以下命令可以启动完整版浏览器:
const browser = await puppeteer.launch({headless: false})
  1. 减慢速度,slowMo选项以指定的毫秒减慢Puppeteer的操作。这是另一个看到发生了什么的方法:
const browser = await puppeteer.launch({
  headless:false,
  slowMo:250
});

3.捕获console的输出,通过监听console事件。在page.evaluate里调试代码时这也很方便:

page.on('console', msg => console.log('PAGE LOG:', ...msg.args));
await page.evaluate(() => console.log(`url is ${location.href}`));

4.启动详细日志记录,所有公共API调用和内部协议流量都将通过puppeteer命名空间下的debug模块进行记录

 # Basic verbose logging
 env DEBUG="puppeteer:*" node script.js

 # Debug output can be enabled/disabled by namespace
 env DEBUG="puppeteer:*,-puppeteer:protocol" node script.js # everything BUT protocol messages
 env DEBUG="puppeteer:session" node script.js # protocol session messages (protocol messages to targets)
 env DEBUG="puppeteer:mouse,puppeteer:keyboard" node script.js # only Mouse and Keyboard API calls

 # Protocol traffic can be rather noisy. This example filters out all Network domain messages
 env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'

爬虫实践

很多网页通过user-agent来判断设备,可以通过page.emulate(options)来进行模拟。options有两个配置项,一个为userAgent,另一个为viewport可以设置宽度(width)、高度(height)、屏幕缩放(deviceScaleFactor)、是否是移动端(isMobile)、有无touch事件(hasTouch)。

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.emulate(iPhone);
  await page.goto('https://www.example.com');
  // other actions...
  await browser.close();
});

上述代码则模拟了iPhone6访问某网站,其中devices是puppeteer内置的一些常见设备的模拟参数。

很多网页需要登录,有两种解决方案:

  1. 让puppeteer去输入账号密码

常用方法:点击可以使用page.click(selector[, options])方法,也可以选择聚焦page.focus(selector)。
输入可以使用page.type(selector, text[, options])输入指定的字符串,还可以在options中设置delay缓慢输入更像真人一些。也可以使用keyboard.down(key[, options])来一个字符一个字符的输入。

  1. 如果是通过cookie判断登录状态的可以通过page.setCookie(...cookies),想要维持cookie可以定时访问。
Tip:有些网站需要扫码,但是相同域名的其他网页却有登录,就可以尝试去可以登录的网页登录完利用cookie访问跳过扫码。

简单例子

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: false});
  const page = await browser.newPage();
  await page.goto('https://baidu.com');
  await page.type('#kw', 'puppeteer', {delay: 100});
  page.click('#su')
  await page.waitFor(1000);
  const targetLink = await page.evaluate(() => {
    return [...document.querySelectorAll('.result a')].filter(item => {
      return item.innerText && item.innerText.includes('Puppeteer的入门和实践')
    }).toString()
  });
  await page.goto(targetLink);
  await page.waitFor(1000);
  browser.close();
})()

records.gif

求赞,另外欢迎访问我的博客

查看原文

赞 7 收藏 6 评论 2

undefined 回答了问题 · 2017-09-21

解决windows chrome的命令行工具在哪

原来不像mac有个专门的命令行工具,windows上的chrome.exe就可以直接接受参数打开

关注 2 回答 2

undefined 提出了问题 · 2017-09-20

解决windows chrome的命令行工具在哪

想打开chrome headless模式 但是找不到路径

关注 2 回答 2

认证与成就

  • 获得 16 次点赞
  • 获得 50 枚徽章 获得 2 枚金徽章, 获得 12 枚银徽章, 获得 36 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-01-05
个人主页被 584 人浏览