HeRunBin's blog


  • 首页

  • 标签

  • 归档

  • Search

使用 prerender(预渲染)加快网页访问速度

发表于 2019-05-17 | 更新于: 2019-05-20
字数统计: 1,448 | 阅读时长 ≈ 6

一、前言

如何加速网页访问速度?除了常见的gzip压缩、文件bundle、各级缓存等等,还有没有其他的方式呢?
今天了解了一下 Resource Hints,一种浏览器预操作机制,草案定义了 dns-prefetch、preconnect、prefetch、prerender 等多种预操作,在一个页面被访问之前,预先做一些工作,如DNS预解析、HTTP预连接、资源预加载、页面预渲染等,当页面真正加载时,可以更快的呈现出来。

二、核心

1. prerender 基本介绍

上述多种预操作之中,prerender(预渲染)是其中提升访问速度最明显的,假定我们已经知道用户下一个将访问的页面地址 http://www.xxx.com.cn,我们可以在当前页通过以下方式告知浏览器

1
<link rel="prerender" href="http://www.xxx.com.cn">

浏览器加载并渲染页面,设置页面状态为’prerender’(此时页面不可见),当用户真正访问时,浏览器变更页面的状态为’visible’,将其迅速呈现出来,页面秒速响应。

注意: 渲染页面对浏览器而言是比较昂贵的操作,因此并不是所有情况下都会去做预渲染,当出现以下情况时,预处理将被中止。

  • 当资源有限时,防止启动预渲染。
  • 由于高成本或资源需求而放弃预渲染 - 例如高CPU或内存使用,昂贵的数据访问等等。
  • 由于所获取内容的类型或属性而放弃预渲染:
  • 如果目标表现出非幂等行为:共享本地存储的突变,带有除GET,HEAD或OPTION之外的动词的XMLHttpRequest,依此类推。
  • 如果目标触发需要用户输入的条件:确认对话框,身份验证提示,警报等。

2. prerender 兼容性

因为Resource Hints还处于工作草案状态,浏览器并没有完备支持,prerender 目前只有Chrome支持的比较好,具体见下图:

3. prerender 实例

因为 prerender 涉及到 页面可见状态 的变化,当页面处于预加载状态时,document.visibilityState(全局只读属性) 取值是 ‘prerender’,当页面真正呈现时,值变更为 ‘visible’,当页面被预加载时,因为用户还没有真正进入,我们希望浏览器加载静态资源并渲染原始页面结构,但不希望在此时执行真正的业务代码,此外浏览器在预加载状态页会对一些操作进行限制(如变更存储状态localStorage等..),因此,最好的方式是将业务代码延迟到页面真正呈现时再执行。

以我现在正在做的一个Vue项目为例:

首先,新建一个 test.html 页面,一个单独的测试网页,用于发起prerender

1
2
<!-- src 需要是完整的URL地址(包括http://) -->
<link rel="prerender" href="http://10.18.200.239:7100">

以下是原始业务代码入口,现在我们要对它进行修改

1
2
3
4
5
6
// main.js Vue 项目js入口
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');

监听 visibilitychange 事件,当页面可见状态由 prerender 变更为 visible 时,再对进行业务代码初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// main.js Vue 项目js入口

function init() {
// 初始化vue实例
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
}

function visibleChange() {
// 页面状态从 prerender 变更为 visible ,再执行初始化
if (document.visibilityState === 'visible') {
init();
document.removeEventListener('visibilitychange', visibleChange);
}
}

// 监听页面状态变更
if (document.visibilityState === 'prerender') {
document.addEventListener('visibilitychange', visibleChange);
} else {
// 不经过prerender ,普通方式加载
init();
}

因为启动的是本地nginx服务,服务器文件就在本机,响应速度很快,不管是否进行预处理,页面响应速度变化不明显,所以我将Chrome网络限制为 Fast 3G,以下是对照实验:

方案1:清除浏览器缓存,打开浏览器Tab页, 调整网速, 直接在浏览器访问本地项目 http://10.18.200.239:7100 , 页面加载耗时约10s

方案2:
清除浏览器缓存,请求测试页 http://localhost:8080/test.html , 发起prerender请求, 打开新Tab页, 调整网速,输入项目地址 http://10.18.200.239:7100 ,页面加载耗时约2s, 从下图中可以看到,js、css等资源都是从缓存中加载(from disk cache), 速度从10s 到 2s , 也就是说,我们给用户初次访问速度带来 400% 提升(注意:因为这里只是模拟,真实业务环境可能达不到,以实际为准)。

4. prerender 新变化

如果想要知道 prerender 请求是否成功,可以在 chrome://net-internals/#prerender 查看

上图可以看到 Prerender History 表格,记录了发起的prerender请求列表,下面稍微解释一下常用的几个字段:

  1. Origin: Link Rel Prerender (same domain) 标识这个请求是我们发起的,而 Omnibox 标识是由Chrome浏览器自己发起的(Omnibox:Chrome 地址栏,Chrome浏览器为了提升页面访问速度,当用户在地址栏输入地址时,也会提前发起prerender)

  2. URL: 目标url

  3. Final Status: 最终状态,常用值有 Timed Out超时、Duplicate 重复、Cancelled 取消、 NoStatePrefetch Finished(成功)

自Chrome 63开始,prerender底层的机制发生了变化,表现上也出现了一些差异,但使用方式保持一致,底层使用 NoStatePrefetch 取代原有的prerender底层机制。

上面的示例同时在 chrome41 和 chrome67 上进行了测试,都给访问速度带来了额外的提升,以上的示例也是完全兼容的。

参考

Introducing NoState Prefetch
Resource Hints
详解HTML5中rel属性的prefetch预加载功能使用
Document​.visibility​State

数字产品设计-[笔记-1]

发表于 2019-04-13 | 更新于: 2019-05-13
字数统计: 706 | 阅读时长 ≈ 2

一. 数字产品设计

  • 理解用户的期望、需求、动机和使用情境。
  • 理解商业、技术以及行业的机会、需求和制约。
  • 以上述知识为规划基础来创造产品,让产品的形式、内容、行为可用、 易用,令人满意,无论经济还是技术上均切实可行。

二. 数字产品难用的表现与原因

表现
  1. 不尊重用户,频繁的错误消息,责怪用户操作失败,提醒用户操作失败
  2. 要求用户像计算机一样思考,预设用户很了解技术、了解很多概念和技术名词
  3. 产品未经深刻思考,用户容易误触发,整个产品主线不明确
  4. 要求用户做大量的工作
原因
  1. 设计流程缺失:没有对客户需求进行收集、分析和利用,产品的终端体验不佳。
  2. 无视产品的真实用户:不了解哪些用户的基本需求能推动产品成功。
  3. 利益冲突:开发团队既要设计技术体系又要打造用户体验时存在利益冲突。

三. 模型用户界面应该基于用户心理模型,而不是实现模型。

实现模型、心理模型与呈现模型的对比。
  1. 工程师往往必须按照既定的方式开发软件,受制于技术和业务上的限制。软件如何工作的模型称作“实现模型”。
  2. 用户认为必须用什么方式完成工作以及应用程序如何帮助用户完成工作的 方式被称作用户与软件交互的心理模型。这种模型基于用户自己对如何完成工作和计算机工作原理的理解。
  3. 设计师将软件运行机制呈现给用户的方式称为“呈现模型”。
  4. 不同于其他两个模型,设计师对呈现模型有更大的控制权。设计者的一个重要目标应当是努力让呈现模型尽可能地匹配用户的心理模型。因此,设计师详细理解目标用户对软件使用方法的看法非常关键。

呈现模型越趋近于用户的心理模型,用户就会感觉程序越容易使用和理解。通常,呈现模 型越趋近实现模型,用户对应用软件的学习和使用能力就越低。这是因为用户的心理模型往往 与软件的实现模型存在差异。
我们倾向于采用比实际更简单的心理模型。因此,如果创造的呈现模型比实现模型更为 简单,就能帮助用户更好地理解。

浏览器内部工作原理【摘抄概述】

发表于 2019-03-12 | 更新于: 2019-05-20
字数统计: 2,581 | 阅读时长 ≈ 9

从输入一个网址 google.com 直到您在浏览器屏幕上看到 Google 首页的整个过程中都发生了些什么

一、前置信息

浏览器主要组件

  1. 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
  2. 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
  3. 渲染引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  4. 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  5. 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  6. JavaScript 解释器。用于解析和执行 JavaScript 代码。
  7. 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。

和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个渲染引擎实例。每个标签页都是一个独立的进程

渲染引擎

Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的呈现引擎。而 Safari 和 Chrome 浏览器使用的都是 WebKit。(新版Chrome使用Blink)

二、浏览器渲染

HTML 解析

HTML Parse 同时包含 词法分析器和语法分析器。

  1. 词法分析是将输入内容分割成大量标记的过程。标记是语言中的词汇,即构成内容的单位。在人类语言中,它相当于语言字典中的单词.
  2. 语法分析是应用语言的语法规则的过程

  3. 输出,解析器的输出“解析树”是由 DOM 元素和属性节点构成的树结构。DOM 是文档对象模型 (Document Object Model) 的缩写。
    它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。
    解析树的根节点是“Document”对象。

CSS解析器

解析器都会将 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。

处理脚本和样式表的顺序
  1. 脚本
    网络的模型是同步的。网页作者希望解析器遇到<script>标记时立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。作者也可以将脚本标注为“defer”,这样它就不会停止文档解析,而是等到解析结束才执行。HTML5 增加了一个选项,可将脚本标记为异步,以便由其他线程解析和执行。

  2. 预解析
    WebKit 和 Firefox 都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。

  3. 样式表
    另一方面,样式表有着不同的模型。理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。

渲染树
  1. 渲染树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。
  2. Firefox 将渲染树中的元素称为 “Frame”。WebKit 使用的术语是渲染器或渲染对象。 渲染对象知道如何布局并将自身及其子元素绘制出来
  3. 每一个渲染对象都代表了一个矩形的区域,通常对应于相关节点的 CSS 框,这一点在 CSS2 规范中有所描述。它包含诸如宽度、高度和位置等几何信息。 框的类型会受到与节点相关的“display”样式属性的影响
渲染树和 DOM 树的关系
  • 渲染对象是和 DOM 元素相对应的,但并非一一对应。非可视化的 DOM 元素不会插入渲染树中,例如“head”元素。如果元素的 display 属性值为“none”,那么也不会显示在渲染树中(但是 visibility 属性值为“hidden”的元素仍会显示)。
  • 有一些 DOM 元素对应多个可视化对象。它们往往是具有复杂结构的元素,无法用单一的矩形来描述。例如,“select”元素有 3 个渲染对象:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。如果由于宽度不够,文本无法在一行中显示而分为多行,那么新的行也会作为新的渲染对象而添加
样式计算

存在的难点

  1. 样式数据是一个超大的结构,存储了无数的样式属性,这可能造成内存问题。
  2. 如果不进行优化,为每一个元素查找匹配的规则会造成性能问题。要为每一个元素遍历整个规则列表来寻找匹配规则,这是一项浩大的工程。选择器会具有很复杂的结构,这就会导致某个匹配过程一开始看起来很可能是正确的,但最终发现其实是徒劳的,必须尝试其他匹配路径。
  3. 应用规则涉及到相当复杂的层叠规则(用于定义这些规则的层次)

处理的一些方案

  1. WebKit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享。这些节点是同级关系。并且满足一些其他条件

    • 这些元素必须处于相同的鼠标状态(例如,不允许其中一个是“:hover”状态,而另一个不是)
    • 任何元素都没有 ID
    • 类属性应匹配
    • 元素中不能有任何 inline 样式属性
    • 等等…
    • 不能使用任何同级选择器。WebCore 在遇到任何同级选择器时,只会引发一个全局开关,并停用整个文档的样式共享(如果存在)。这包括 + 选择器以及 :first-child 和 :last-child 等选择器。(注意这点,有待实践)
  2. Firefox 规则树 …

布局
  1. 渲染对象在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。
  2. HTML 采用基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。但是也有例外情况,比如 HTML 表格的计算就需要不止一次的遍历 (3.5)。
  3. 坐标系是相对于根框架而建立的,使用的是上坐标和左坐标。
  4. 布局是一个递归的过程。它从根渲染对象(对应于 HTML 文档的 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的渲染对象计算几何信息。
  5. 根渲染对象的位置左边是 0,0,其尺寸为视口(也就是浏览器窗口的可见区域)。
  6. 所有的渲染对象都有一个“layout”或者“reflow”方法,每一个渲染对象都会调用其需要进行布局的子代的 layout 方法。
渲染
  1. 在绘制阶段,系统会遍历渲染树,并调用渲染对象的“paint”方法,将渲染对象的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。

  2. 全局绘制和增量绘制
    和布局一样,绘制也分为全局(绘制整个渲染树)和增量两种。在增量绘制中,部分渲染对象发生了更改,但是不会影响整个树。更改后的渲染对象将其在屏幕上对应的矩形区域设为无效,这导致 OS 将其视为一块“dirty 区域”,并生成“paint”事件。OS 会很巧妙地将多个区域合并成一个。在 Chrome 浏览器中,情况要更复杂一些,因为 Chrome 浏览器的渲染对象不在主进程上。Chrome 浏览器会在某种程度上模拟 OS 的行为。展示层会侦听这些事件,并将消息委托给呈现根节点。然后遍历渲染树,直到找到相关的渲染对象,该渲染器会重新绘制自己(通常也包括其子代)。

原文链接

Node log4j模块记录日志

发表于 2018-12-21 | 更新于: 2018-12-24
字数统计: 232 | 阅读时长 ≈ 1

一.配置

常规配置, 封装logger方法,logger()调用后打印到控制台,并输出日志到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

const log4js = require('log4js');

log4js.configure({
// 定义 appender
appenders: {
// 打印到控制台
console: { type: 'console' },
// 输出日志到文件
record: { type: 'file', filename: process.cwd() + '/logs/out.log', maxLogSize: '2M' },
},
categories: {
console: { appenders: ['console'], level: 'debug' },
record: { appenders: ['record'], level: 'debug' },
// default 必须有
default: { appenders: ['console'], level: 'debug' }
},
// 将普通 console 输出替换成 log4j info 格式
replaceConsole: true
});

const recordLog = log4js.getLogger('record');
const consoleLog = log4js.getLogger('console');

module.exports = function logger(funcName, msg) {
recordLog[funcName](msg)
consoleLog[funcName](msg)
}

二.使用示例

1
2
3
4
5
6
7
8
9
10
11

const logger = require('./utils/logger')

// 全局异常记录
process.prependListener('uncaughtException', (err) => {
logger('error', `捕获到异常:${err.stack}\n`)
});

process.prependListener('unhandledRejection', (reason, p) => {
logger('error', `未处理的 rejection:${err.stack}\n`)
});

三. 其他

[参考1](https://angular.cn/guide/ajs-quick-reference) 

[库github链接](https://github.com/log4js-node/log4js-node) 

Vue.js集成WebWorker

发表于 2018-12-21 | 更新于: 2018-12-28
字数统计: 694 | 阅读时长 ≈ 3

一. WebWorker简单介绍

  • Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以发送XMLHttpRequest请求,可以执行任务而不干扰用户界面。可以通过指定的方式与创建它的JavaScript代码进行交互
  • 在worker内,不能直接操作DOM节点,也不能使用window对象的默认方法和属性。但可以通过self全局属性去使用WebSocket、IndexedDB等功能
  • workers和主线程间的数据传递都通过postMessage()实现,使用onmessage事件处理函数来响应消息(消息被包含在Message事件的data属性中)。这个过程中数据并不是被共享而是被复制。

二. 配置

vue.config.js (vue 配置文件)

1
2
3
4
5
6
7
8
9
10
11
12

// 配置worker-loader, 处理webworker资源
chainWebpack: config => {
config.module
.rule('webworker')
.test(/\.worker\.js$/)
.use('worker-loader')
.loader('worker-loader')
.end()

}
parallel: false

注意1: parallel需要设置为false,不然 build 的时候 loader 会报错,大概原因是多线程之间实例共享出现了问题,详情见 https://github.com/vuejs/vue-cli/issues/2785

注意2: 通过配置文件的方式,生产环境存在找不到worker文件的问题

更简单的方式是直接通过 inline 方式,不需要配置webpack,也可以避免上面的问题

1
2

import PreviewWorker from "worker-loader!../../workers/preview.worker.js";

preview.worker.js (worker)

1
2
3
4
5
6
// 事件处理
self.addEventListener('message', function (e) {
const { data } = e;
console.log('接收到主线程信息', e)
postMessage('worker反馈信息到主线程')
}, false);

main.js (主线程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import PreviewWorker from "../../workers/preview.worker.js";

const worker = new PreviewWorker();
worker.postMessage({ a: 1 });
worker.addEventListener("message", function(event) {
console.log("接收到worker信息", event);
});

// 错误处理
worker.addEventListener("error", event => {
console.log("worker 错误", {
message: event.message,
filename: event.stack,
lineno: event.lineno,
colno: event.colno
});
});

三.问题解决

  1. 不断发送 websocket 请求, Frame内容是 webpackClose , 禁用热更新,问题仍存在, 详情见 https://github.com/webpack/webpack-dev-server/issues/1604

解决方案: 禁止host检测

1
2
3
4

devServer: {
disableHostCheck: true
}
  1. 加载worker文件,返回的是主页index.html, 定位问题原因是无法检测到worker文件修改,没有生成 .worker.js 文件

解决方案: 直接通过 inline 方式加载

1
2

import PreviewWorker from "worker-loader!../../workers/preview.worker.js";

四.其他相关

  1. 记得及时销毁worker,避免内存泄漏
1
2
3
4
5
6
7
8

beforeDestroy() {
// worker 回收
if (this.worker) {
this.worker.terminate();
console.warn("WORKRT DESTROYED!");
}
}
  1. workerize-loader 也是一个处理webwoker资源的加载器,提供另一种加载方式
  1. worker 与 主线程之间传输大文件时,可以采用 Transferable Objects 提升传输性能,但存在一些限制, 详情见Transferable Objects: Lightning Fast!

nginx缓存302请求导致重定向循环

发表于 2018-10-06 | 更新于: 2018-11-07
字数统计: 1,072 | 阅读时长 ≈ 4

一.概述

前端遗留项目,将其嵌入新开发的门户网站中,作为子项目,嵌入之后发现访问速度很慢,平均加载耗时超过10s,性能分析之后,发现存在以下的问题:

  1. 需要加载的静态资源多,差不多120多个,且大部分没有经过压缩
  2. 原系统部署在内网,门户网站部署在阿里云,嵌入之后访问速度受到内外网带宽的影响,且访问相当于多了一次跳转
  3. 每次ajax请求需要去sso(统一登录平台)校验是否登录,对性能也有一定的影响

二.详细

1. 通过nginx配置加速网页访问

系统访问流程图如下:

因为遗留项目为第三方系统,只提供了压缩包,无法修改项目源码,那就只能在nginx服务器和浏览器端进行优化

采取措施如下:

开启gzip压缩文件大小,浏览器静态资源缓存,开启nginx代理缓存

nginx部分配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

http {

# gzip压缩
gzip on;
gzip_min_length 5k;
gzip_buffers 4 16k;
gzip_comp_level 3;
gzip_types text/plain application/javascript text/css application/xml text/javascript image/jpeg image/gif image/png;
gzip_vary on;

# proxy 缓存空间 声明
proxy_cache_path proxy_cache_static levels=1:2 keys_zone=cache_static:20m;

# 请求头 判断缓存是否命中
add_header Nginx-Cache $upstream_cache_status;

server {

listen 8080;
server_name 127.0.0.1;

# 遗留项目 proxy 缓存
location /url {
# 浏览器静态资源缓存
expires 7d;
proxy_cache cache_static;
# 缓存7天
proxy_cache_valid 200 206 304 301 302 7d;
# 缓存唯一KEY值
proxy_cache_key $uri;
proxy_pass http://10.18.194.161:7001;
}
}
}

经过如上配置,页面访问速度得到极大提升,访问速度控制在1s以内

2. 重定向循环问题出现

解决了访问速度的问题,不久业务部门又反馈了一个问题,登陆之后,隔段时间再操作,页面有时会一直闪烁,无法正常操作,经过chrome调试,发现是因为发生了302重定向循环,开始分析问题

正常访问,命中nginx缓存(HIT),返回200:

重定向循环异常,返回302:

查看异常详情,发现nginx缓存仍然命中(HIT),且返回302重定向到当前资源:

3. 问题发现及解决

我们从结果倒推,首先,查看请求头,请求头中的 Nginx-Cache 返回 HIT ,由此可知,我们的错误响应是由nignx返回的,而没有到达遗留项目,既然错误响应是由nginx缓存返回的,查看nginx缓存配置,发现我们的配置确实存在缓存302请求的可能

1
proxy_cache_valid  200 206 304 301 302 7d;

然后,就是要找到它是什么时候缓存了这个302重定向了,与后台同学一起,从前到后分析了请求的整个流程,最终定位问题出现在sso登录校验阶段,用户一段时间未操作,登录状态过期,导致静态资源请求触发302,并复现了问题

首先,清空浏览器登陆信息,不访问登陆页面,而是先直接访问静态资源common.js,后台拦截请求,判断当前未登陆,跳转到sso单点登陆页面,登陆成功,sso重定向到之前访问的路径,也就是如下图

而我们之前的问题也就很清楚了,浏览器请求静态资源common.js,请求经过nginx,到达遗留项目,遗留项目判断当前未登陆,跳转到sso登陆,登陆成功后返回302重定向到登陆之前的路径也就是common.js资源路径,返回又经nginx,nginx缓存了这个302返回,并将该响应返回到浏览器,浏览器根据302状态码,再次发起对common.js的请求,nginx直接返回302,重定向循环就这么理所当然的发生了

问题的分析,溯源花了很长的时候,解决问题只需稍微修改nginx配置即可,如下,去掉对302请求的缓存即可

1
2
3
4
5
#before
proxy_cache_valid 200 206 304 301 302 7d;

#after
proxy_cache_valid 200 206 304 301 7d;

前端性能监控

发表于 2018-09-19 | 更新于: 2018-09-19
字数统计: 848 | 阅读时长 ≈ 4

一.概述

门户入口,通过iframe集成其他内部项目,希望当用户打开项目(不管是门户或者iframe页)时,上报指标如
当前时间、访问Url、项目、页面加载耗时、超时资源等

主要目的是:

  1. 希望实现网络故障或繁忙时,自动预警
  2. 有针对性的优化用户访问速度,找到用户访问慢的根源
  3. 根据用户上报信息,对访问时段、访问频次、项目打开频次等数据做进一步的探索

二.大致步骤

1. 通过PerFormance对象获取性能信息

通过perFormance全局对象可以获取页面性能相关信息,目前兼容到IE10,已经可用

获取页面加载时间

1
2
3
performance.getEntriesByType('navigation')

通过name获取加载当前url , 根据domComplete获取页面加载耗(ms)

获取超时资源列表

1
2
3
4
5
performance.getEntriesByType('resource')
.filter(item => item.duration >= 5000) // 找出加载时间超过5s 的资源
// initiatorType 类型
// name 名称
// duration 耗时

2. 页面加载完自动上报

1
2
3
4
5
6
7
8
9
10
11
12
13
14

reportNavigate() {
// 后台上报
}

// 简略实现
window.onload = function (e) {
// 尽量不影响页面主线程
if (this.$window.requestIdleCallback) {
this.$window.requestIdleCallback(this.reportNavigate)
} else {
setTimeout(this.reportNavigate)
}
}

3. 监控iframe加载

通过传入iframe的window全局对象

三.完整代码

1
2
3
4
5
6
7
// 监控主项目,自动上报
new PerformanceMonitor('main')

// 监控iframe子项目, 手动上报
this.monitor = new PerformanceMonitor('iframe', window.frames[0].window, null, false)
// jsx
<iframe onLoad={() => {this.monitor.reportNavigate()}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

// 定义监控类
import { report } from '../services/monitor'
import moment from 'moment'

export default class PerformanceMonitor {

SECOND = 1000

/**
* origin 标示来源,当有多个日志源时
* $window 默认为window对象,iframe传入iframe的全局window对象
* timeout 超时时间判断
* auto 默认自动上报,若为false,需手动调用上报方法
*/
constructor(origin, $window, timeout, auto = true) {
this.origin = origin
this.timeout = timeout || this.SECOND * 5
this.$window = $window || window
this.reportNavigate = this.reportNavigate.bind(this)
// 自动上报
if (auto) {
this.bindNavigate()
}
}

// 获取页面加载时间
getLoadTime() {
const navigation = this.$window.performance.getEntriesByType('navigation')
if (navigation && navigation.length !== 0) {
return navigation[0].domComplete
} else {
// 兼容低版本Chrome
return this.$window.performance.timing.domComplete - this.$window.performance.timing.navigationStart
}
}

// 获取超时资源
getTimeoutRes() {
// initiatorType 类型
// name 名称
// duration 耗时
const resourceTimes = this.$window.performance.getEntriesByType('resource')
return resourceTimes
.filter(item => item.duration >= this.timeout)
.map(res => ({
TIMEOUTRES_TYPE: res.initiatorType || 'null',
TIMEOUTRES_URL: res.name,
TIMEOUTRES_DATE: res.duration
}))
}

// 获取当前URl并解码
getUrl() {
let url
try {
url = decodeURIComponent(this.$window.location.href)
} catch (error) {
console.log('url decode异常')
url = this.$window.location.href
}
return url
}

// 页面加载完上报
reportNavigate() {
const domComplete = this.getLoadTime()
const timeoutRes = this.getTimeoutRes()
const url = this.getUrl()
const logData = {
MONITOR_ORIGIN: this.origin,
MONITOR_TYPE: 'navigate',
MONITOR_URL: url,
MONITOR_TIMEOUT: this.timeout,
MONITOR_TIMEOUTRES: timeoutRes,
MONITOR_LOADTIME: domComplete,
MONITOR_REPORTTIME: moment().format('YYYY-MM-DD HH:mm:ss')
}
this.report(logData)
}

// 可以自定义上报信息
report(data) {
try {
report(data)
} catch (error) {
console.log('日志上报异常', error)
}
}

// 绑定事件、自动上报
bindNavigate() {
const oldOnload = this.$window.onload
this.$window.onload = function (e) {
if (oldOnload && typeof oldOnload === 'function') {
oldOnload(e)
}
// 尽量不影响页面主线程
if (this.$window.requestIdleCallback) {
this.$window.requestIdleCallback(this.reportNavigate)
} else {
setTimeout(this.reportNavigate)
}
}.bind(this)
}

}

DvaJS model复用的方案

发表于 2018-08-30 | 更新于: 2018-08-31
字数统计: 427 | 阅读时长 ≈ 2

Dva 下 model 复用 (一个model同时对应多个业务模块,业务模块间数据互不影响)

前置知识: dva  =>  一个基于 redux 和 redux-saga 的数据流方案
         react => 高阶组件的定义

1. 示例描述:

多Tab结构, 存在A、B、C 业务模块,数据不同,业务逻辑一致,希望实现代码复用

2. 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60



// router.js
// 1. 动态加载 model 和 路由组件
{
path: 'product',
getComponent(nextState, cb) {
require.ensure([], require => {
// 从location 中获取业务类型 type
const type = nextState.location.query.type
// 加载model并挂载到dva实例上 注意:require后接了(),因为返回的是一个函数,函数执行返回真正将被挂载的model对象
app.model(require('./models/product')(type))
// 加载路由组件 require 返回的也是一个函数,函数执行返回一个React组件(高阶组件,在真正的路由组件上包了一层)
cb(null, require('./routes/product')(type))
}, 'product')
}
}


// models/product.js
// 2. 根据type 动态生成 model
export default function modelGenerate(type) {
return {
namespace: `products${type}`,
state: {
list: []
},
reducers: {
}
effects: {

}
}
};

// routes/product.jsx
// 3. 根据type 动态返回 新的React组件(对业务组件进行包装)
export default function routeGenarate(type) {
// 根据type找到命名空间
const namespace = `products${type}`
function ProductWrapper() {
return class extends React.Component {
render() {
return <Products {...this.props} />
}
}
}
// 根据 namespace(命名空间) 从redux store 获取对应的model实例数据 ,并命名为 productModel
return connect(store => ({ productModel: store[namespace] }))(ProductWrapper())
}

// 业务组件 从 props.productModel 获取数据
const Products = ({ dispatch, productModel }) => {
return (
<div>
<ProductList list={productModel.list} />
</div>
);
}

Generator与async、await[借鉴自MDN]

发表于 2018-08-30 | 更新于: 2018-09-19
字数统计: 1,127 | 阅读时长 ≈ 5

一. ES6 Generator(生成器) 语法

function* 用于定义一个生成器函数(generator function),它返回一个Generator对象, Generator对象被创建后,通过调用next()方法获取值

yield 关键字用来暂停和恢复一个生成器函数 

语法:

  1. yield关键字使生成器函数执行暂停,yield关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本的return关键字

  2. yield关键字实际返回一个IteratorResult对象,它有两个属性,value和done。value属性是对yield表达式求值的结果,而done是false,表示生成器函数尚未完全完成。

  3. 一旦遇到 yield 表达式,生成器的代码将被暂停运行,直到生成器的 next() 方法被调用。每次调用生成器的next()方法时,生成器都会恢复执行,直到达到以下某个值:

    • yield,导致生成器再次暂停并返回生成器的新值。 下一次调用next()时,在yield之后紧接着的语句继续执行。

    • throw用于从生成器中抛出异常。这让生成器完全停止执行,并在调用者中继续执行,正如通常情况下抛出异常一样。

    • 到达生成器函数的结尾;在这种情况下,生成器的执行结束,并且IteratorResult给调用者返回undefined并且done为true。

    • 到达return 语句。在这种情况下,生成器的执行结束,并将IteratorResult返回给调用者,其值是由return语句指定的,并且done 为true。

  4. 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* countAppleSales () {
    var saleList = [3, 7, 5];
    for (var i = 0; i < saleList.length; i++) {
    yield saleList[i];
    }
    }
    var appleStore = countAppleSales(); // Generator { }
    console.log(appleStore.next()); // { value: 3, done: false }
    console.log(appleStore.next()); // { value: 7, done: false }
    console.log(appleStore.next()); // { value: 5, done: false }
    console.log(appleStore.next()); // { value: undefined, done: true }

注意:

  1. 不能在forEach等方法中直接使用yield,因为forEach等接受一个函数作为参数,yield外层执行环境不是生成器函数,所以会报语法错误,错误示例如下

    1
    2
    3
    4
    5
    function* nameGenerator() {
    ['Hellon','Nancy'].forEach(item => {
    yield item // => Uncaught SyntaxError: Unexpected identifier
    })
    }
  2. 也可作为对象属性存在,通过如下方式定义

    1
    *countAppleSales() {}
  3. yield* 表达式用于委托给另一个generator 或可迭代对象。

二. Async function:

  1. async function 声明将定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。但是如果你的代码使用了异步函数,它的语法和结构会更像是标准的同步函数

    1). 当调用一个 async 函数时,会返回一个 Promise 对象。当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。
    2). async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果

  2. await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用
    1). await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。
    2). 若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。
    3). 另外,如果 await 操作符后的表达式的值不是一个 Promise,那么该值将被转换为一个已正常处理的 Promise。

三. Generator 结合 async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 模拟发起请求,获取数据
function getData() {
return new Promise((resolve) => {
data = `MOCK DATA ${new Date()}`
setTimeout(_ => {
resolve(data)
}, 2000)
})
}

// 迭代器,调用next()请求最新数据
async function* timerGenerator () {
while (true) {
yield getData()
}
}

// 业务代码,获取最新数据
async function execute() {
const timer = timerGenerator()
console.log(await timer.next()) // {value: "MOCK DATA Thu Aug 30 2018 10:57:51 GMT+0800 (中国标准时间)", done: false}
console.log(await timer.next()) // {value: "MOCK DATA Thu Aug 30 2018 10:57:53 GMT+0800 (中国标准时间)", done: false}
console.log(await timer.next()) // {value: "MOCK DATA Thu Aug 30 2018 10:57:55 GMT+0800 (中国标准时间)", done: false}
console.log(await timer.next()) // {value: "MOCK DATA Thu Aug 30 2018 10:57:57 GMT+0800 (中国标准时间)", done: false}
}
execute()

Angular-React比较

发表于 2018-05-26 | 更新于: 2018-09-19
字数统计: 1,538 | 阅读时长 ≈ 6

概述

我对前端框架的看法是,同一个问题的多种解决方案,每个方案都有各有适应的场景和人群,但既然解决的是同一个问题,殊途同归,本文就是根据自己的使用经验,列举出Angular、React二者在不同中的相通之处

示例代码皆为伪代码,仅供理解

脉络

前端现代框架的一些基本逻辑:

  1. 组件化, 从原来代码级的复用提升到组件层,组件抽象出可复用的功能与UI,就像一个个零件
  1. 数据驱动,隐藏DOM操作的细节,页面工程化,整个页面就是一个大的机器,组件就是组成机器一个个的齿轮,受内外部数据驱动,影响周围的齿轮随之发生变化

  2. 工程化,集成脚手架等插件,使用路由统一入口

细节

一. Angular

关键词: 模块化、组件化、依赖注入、脏检查

  1. Angular应用是模块化的,一个NgModule(模块)就是一个容器,存放一些内聚代码块,如组件、指令、服务、管道等,提供导入、导出功能,每个Angular应用都会有一个根模块,引导根模块启动项目

  2. 组件可以通过路由器加载,也可以通过模板创建,组件是指令的一种,是带模版的指令,除了组件外,指令还有结构型指令和功能型指令二种,指令拥有生命周期钩子函数。在生命周期不同阶段被调用

  3. 依赖注入机制,注入器是树形结构,注入的服务是单例,子模块中定义的provider将被注册到根注入器,除非子模块为懒加载,懒加载模块单独生成一个注入器,除此之外,组件也拥有组件级注入器

  4. Angular脏检查由ngZone触发,zone重写了一系列异步方法,如事件、定时器、封装了ajax请求,添加了回调钩子函数,在回调中进行数据变更检查,为组件设置onPush策略,组件将只在Input数据发生变化时触发检查,组件可注入changeDetection手动设置脏检查机制

  5. 其他,路由、HttpCLient + 拦截器机制, 视图封装模式(Native、Emulated、None),动态组件、dom操作(Renderer)、Rxjs管理异步事件流

二. React

 关键词: 组件化、函数式编程、虚拟dom

  1. React 维护一颗组件树, 数据通过props从上往下单向流动,组件内部通过state保存状态,通过setState改变state的值,默认当props或state发生变化,组件重新渲染,PureReactComponent组件默认判断新旧props,state(浅引用判断)值,如果没有变化,不重新渲染,但过多diff操作同样消耗性能, 普通Component 可通过使用 shouldComponentUpdate生命周期钩子,返回true|false 来决定是否重新渲染组件,结合 immutable 对象可以更好的对性能进行优化

  2. React 组件有3种创建方式(函数式、ES6、ES5),函数式无状态组件拥有更好的性能,每次重新渲染相当于重新执行一次函数,但如果需要用到state 、组件生命周期,则需要使用ES6方式, 通过ES5语法创建现已不推荐使用,

  3. jsx-函数式编程,可以用js来构建视图,可以使用临时变量、自带的流程控制、js当前作用域等,相比较于Angular的模板(Template)更为灵活

  4. react利用key来识别组件,它是一种身份标识标识,当key发生变化,销毁原有组件,创建新组件,key没有变化,数据变化,重新渲染原组件

二者比较

  1. 数据流向: Angular 数据从上往下,单向流动,通过Output自定义事件与父组件通信 , React 通过props从上往下,数据单向流动,通过state保存当前组件内部的状态,通过setState改变组件内部状态,与父组件通信通常通过状态提升来完成,常结合redux/mobx使用

  2. 不可变性:因为数据单向流动,所有子组件从父级接收的数据是不可变的(@Input 、props),Angular如果要对@Input传递的数据进行处理,可以用set去实现

  3. Angular使用模板语法,通过结构型指令如ngIf、ngFor 实现条件渲染、列表展示, React通过jsx语法实现同样的功能,如下

    1
    var && <div>、datas.map(i,v)
  4. Angular 表单提供二种方式,模板驱动与响应式表单,前者通过模板语法与指令实现,后者由模型驱动,预先定义字段、校验项、初始值,提供校验方法,React 表单一般通过受控组件的方式实现

  5. React 组件的props.children 类似于 Angular 的内容映射(通过ng-content指令实现)

  6. 避免重复渲染,性能优化,React 通常使用 shouldComponentUpdate + Immutable 对象减少重复渲染, Angular通常设置 changedetectionstrategy.onpush + Immutable 对象

  7. 集成第三方库时,常需要获取Dom对象,React使用 refs传递,Angular 通过ViewChild实现对dom节点的引用

1
2
3
4
5
6
7
8
React : 
<div ref={domRef}> // 通过this.domRef获取dom节点

Angular :
.ts:
ViewChild(selector) domRef
.html:
<div seletor>
  1. Angular 相对React ,概念更多,给出了一个更复杂的解决方案,但同时,Angular提供了React没有的模块化、提供成体系的前端技术解决方案、引入typescript进行类型定义,对于大型协作项目的团队式开发和长期维护,是一个不错的选择,但正因为想要做的事情太多,不得不引入大量抽象概念,前期需要花费更多的时间

  2. React 虽只提供了简单的API,但结合JSX语法,非常灵活且强大,但是灵活意味着容易犯错,相比较Angular,更容易把代码写的难以维护,对开发者个人的能力要求更高

12
何润斌

何润斌

12 日志
9 标签
GitHub E-Mail
© 2019 何润斌 | Site words total count: 11.8k