前端知识体系(5)——javascript 基础(2)

165 阅读30分钟

前言:本前端知识体系供个人复习与巩固用,不一定适合其他人。会逐步的完善该系列文章。

白屏时间

白屏时间是指用户屏幕为空白持续的时间,计算白屏时间的公式如下:

白屏时间 = 首次看到内容的时间点 - 页面开始请求的时间点

首屏时间

首屏时间是指用户首屏内容全部加载完成的时间,计算白屏时间的公式如下:

首屏时间 = 首屏内容全部加载完成的时间 - 页面开始请求的时间点

业务中如何计算白屏和首屏时间

白屏时间和首屏时间是衡量网站加载性能的两个重要指标。在开发中,需要根据业务页面的具体情况,相应地统计页面白屏时间和首屏时间。对于白屏时间和首屏时间,计算的原理是不变的,但计算的的方式可能会根据页面情况有所改变,因此需要具体问题具体分析。

白屏时间

白屏时间指的是浏览器开始显示内容的时间。因此只需要知道是浏览器开始显示内容的时间点,即页面白屏结束时间点即可获取到页面的白屏时间。

firstpaint.png

通过浏览器工作原理,可以知道:

  • 浏览器在加载页面的刚开始,页面是处于空白的状态,只有当页面发生绘制操作时会开始显示内容。
  • 浏览器加载页面是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收页面内容的同时,呈现引擎会将部分内容解析并显示出来。
  • 非可视化的 DOM 元素不会显示到窗口中,例如 <head>,这就意味着浏览器在绘制之前,至少需要先解析完head元素中的内容。

计算白屏时间

因此,通常认为浏览器开始渲染 <body> 标签或者解析完 <head> 标签的时刻就是页面白屏结束的时间点。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>白屏</title>
  <script type="text/javascript">
    // 不兼容performance.timing 的浏览器,如IE8
    window.pageStartTime = Date.now();
  </script>
  <!-- 页面 CSS 资源 -->
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
  <script type="text/javascript">
    // 白屏时间结束点
    window.firstPaint = Date.now();
  </script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

因此白屏时间则可以这样计算出:

可使用 Performance API 时

白屏时间 = firstPaint - performance.timing.navigationStart;

不可使用 Performance API 时

白屏时间 = firstPaint - pageStartTime;

首屏时间

首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来说,首屏时间是用户对一个网站的重要体验因素。通常一个网站,如果首屏时间在5秒以内是比较优秀的,10秒以内是可以接受的,10秒以上就不可容忍了。超过10秒的首屏时间用户会选择刷新页面或立刻离开。

firstscreen.png

计算首屏时间

明白了首屏时间的概念后,计算首屏时间则更加清晰了。现实中,不同业务的首屏内容并不是一样的。因此计算首屏时间,需要根据业务页面情况,相应地选择计算首屏时间的方法。

通常计算首屏的方法有

  • 首屏模块标签标记法
  • 统计首屏内加载最慢的图片的时间
  • 自定义首屏内容计算法

1、首屏模块标签标记法

首屏模块标签标记法,通常适用于首屏内容不需要通过拉取数据才能生存以及页面不考虑图片等资源加载的情况。会在 HTML 文档中对应首屏内容的标签结束位置,使用内联的 JavaScript 代码记录当前时间戳。如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>首屏</title>
  <script type="text/javascript">
    window.pageStartTime = Date.now();
  </script>
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
</head>
<body>
  <!-- 首屏可见模块1 -->
  <div class="module-1"></div>
  <!-- 首屏可见模块2 -->
  <div class="module-2"></div>
  <script type="text/javascript">
    window.firstScreen = Date.now();
  </script>
  <!-- 首屏不可见模块3 -->
  <div class="module-3"></div>
    <!-- 首屏不可见模块4 -->
  <div class="module-4"></div>
</body>
</html>

此时首屏时间等于 firstScreen - performance.timing.navigationStart;

事实上首屏模块标签标记法 在业务中的情况比较少,大多数页面都需要通过接口拉取数据才能完整展示,因此会使用 JavaScript 脚本来判断首屏页面内容加载情况。

2、统计首屏内图片完成加载的时间

通常首屏内容加载最慢的就是图片资源,因此会把首屏内加载最慢的图片的时间当做首屏的时间。

由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。因此在 DOM树 构建完成后将会去遍历首屏内的所有图片标签,并且监听所有图片标签 onload 事件,最终遍历图片标签的加载时间的最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。

此时首屏时间等于 加载最慢的图片的时间点 - performance.timing.navigationStart;

3、自定义模块内容计算法

由于统计首屏内图片完成加载的时间比较复杂。因此在业务中通常会通过自定义模块内容,来简化计算首屏时间。如下面的做法:

  • 忽略图片等资源加载情况,只考虑页面主要 DOM
  • 只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容

总结

对于首屏时间和白屏时间,业界并没有一个统一的计算标准。不能通过死记硬背来实现首屏时间和白屏时间的统计,而应该根据首屏时间和白屏时间的具体原理,切合自身业务情况来制定统计的方法。统计首屏时间和白屏时间是为了能够更明白业务页面的情况,而不是为了统计而去统计。

更多阅读

CDN 相关

为了提高网页应用的加载速度,会使用到浏览器缓存,然而浏览器缓存只能提升二次访问的速度。对于加速首次访问的速度,最常见的手段则是使用 CDN 加速。

CDN 的全称是 Content Delivery Network,即内容分发网络。通过将静态资源缓存到离用户很近的相同网络运营商的 CDN 节点上,使用户可以就近取得所需的内容,解决网络拥塞状况,提高用户访问网站的响应速度。

事实上,在一些较大型的公司来说,通常是由公司的运维开发人员来负责部署 CDN 服务的,前端人员只需要了解使用 CDN 服务的大致流程即可。

1 选择 CDN 供应商

事实上,现在还有许多的 CDN 服务厂家,需要根据需求的情况去选择相应的厂商。现阶段 CDN 云服务商的竞争已经由 技术 逐渐取代价格成为 CDN 厂商在客户谈判时最关键因素,技术更高的 CDN 服务能够降低带宽成本、提升用户体验。

如今 CDN 各大厂商都会提供十分详尽的接入文档,使接入的开发成本变得更低。这里列举国内比较出名的 CDN 厂商:

2 连接 CDN 服务

通常购买了 CDN 服务后,该 CDN 服务会提供相应的接口去上传和部署资源。这部分的工作通常是由运维人员去负责的,对于一些小公司可能则需要前端或者后台人员去负责连接 CDN 服务,使业务能够部署静态资源到相应的服务器上。

3 前端工程静态资源部署

使用 CDN 服务,对前端工程产生了一定的影响,需要对静态资源的部署做两项改变:

3.1 将静态资源部署到不同网络线路的服务器中

由于用户的网络线路有很多种,如网通、电信、移动等,为了让不同地区的用户会访问到离自己最近的相同网络线路上的 CDN 节点。因此需要将我们的资源部署到不同的网络线路的服务器中,然后通过这些服务器同步到不同线路的 CDN 节点上。

3.2 加载静态资源时需使用与页面不同的域名

使用不同的域名来加载 CDN 服务的静态资源,一方面便于接入为 CDN 时设置 DNS 的智能解析。另一方面当静态资源和主页面使用不同的域名,在加载静态资源时 HTTP 请求并不会带上主域页面的 Cookie 等数据,较少了请求时的数据传输量,进一步加快网络访问。

3.3 资源路径的修正

通常情况下,资源都是在本地,比如有个 JS 文件,在 HTML 里面按照相对目录引用它:

<script src="js/script.js"></script>

使用了 CDN 的话,资源都部署到了 CDN 服务器上去咯,这个时候必须用绝对路径来引用:

<script src="http://xxx.xxx.xxx/js/script.js"></script>

所有啊,必须有个修正资源路径的过程,而这个过程当然不能手动,通常都是在工程化里面解决这个问题。

总结

如今 CDN 服务基本上已经成为现代大型网页应用的标配。这项技术是一种十分常用的网络性能优化手段。在未来的前端开发中,会经常与 CDN 打交道。因此如果能够清楚了解了 CDN 服务的相关原理和概念,将会在将来的开发中能够更加得心应手。

立即调用的函数表达式(IIFE)

在学习 JavaScript 的过程中,可能会看到许多下面的或者类似的代码:

(function(){
  //do something here;
})();

对于上面的代码,有两种说法,一部分人称之为 自执行的匿名函数(self-executing anonymous function) ,另外一部分则更为推崇 Ben Alman 的叫法:立即调用的函数表达式(IIFE,Immediately-Invoked Function Expression ) 。事实上,可以按照自己的理解来选择符合的说法。

立即调用函数会报错

在Javascript中,一对圆括号()是一种运算符,可以使用圆括号来调用一个函数,如调用 sayHello 方法则可以这样做。

var sayHello = function() {
  // some code
}
// 使用圆括号表达式调用
sayHello();

有时候,可能会和上面的代码一样,在定义一个函数后,立即且只调用一次该函数。然而当按照下面这样写的时候,发现会报错。

// 定义了一个匿名函数
function () {
  // some code
}();
// SyntaxError: Unexpected token (

之所以产生这个错误的原因是,JavaScript引擎在解析发现 function 关键字出现在行首,认为这一段代码是函数申明语句,因此在 function 关键字后面需要的是一个函数标识符名称,而对于标识符来说是不能以 ( 来命名的。

因此,给该函数加上命名。但是发现还是会报一个不一样的错误:

// 定义了一个匿名函数
function sayHello() {
  // some code
}();
// SyntaxError: Unexpected token )

原因其实前面也提及到了,JavaScript引擎在解析发现 function 关键字出现在行首,认为这一段代码是函数申明语句。而函数申明语句并不能通过圆括号 () 来结尾,因此就报错了。

立即调用的函数表达式

上面的问题的解决方法就是,让函数申明语句转换成函数表达式,即使 function 关键字不出现在行首,让 JavaScript 解析引擎将其理解为一个表达式,避免错误。要使其成为一个表达式,有很多种方法。其中最简单且最容易阅读的方式,便是使用圆括号来实现。

(function(){ /* some code */ }());
// 或者可以这样写
(function(){ /* some code */ })();

因此把上面的表达式叫做 立即调用的函数表达式(Immediately-Invoked Function Expression)  也就是 IIFE

其他的写法

除了上面的写法外,还有许多能够让 JavaScript 解析器解析成表达式的写法,如下面的写法:

true && function(){ /* some code */ }();

// 或者使用操作符
!function(){ /* some code */ }();
~function(){ /* some code */ }();
-function(){ /* some code */ }();
+function(){ /* some code */ }();

IIFE 的好处

大多数情况,使用到 IIFE ,并不会被再次调用,因此通常会使用匿名函数,即忽略立即执行的函数的函数名称。

可以使用 IIFE 来形成了一个私有的作用域(模拟块级作用域),因此可以封装想要的私有变量,以及避免变量出现冲突的情况。

// 可被访问 canRead
var canRead = 2;
var same = 1;

// 私有变量 noRead
(function (){
  var noRead = 3;
  var same = 2;
})();

console.log(canRead); // 2
console.log(noRead); // noRead is not defined
// IIFE 定义变量不会污染外部的变量
console.log(same); // 1

IIFE 创建 Module

可以利用 IIFE 来创建私有的函数作用域,不仅可以避免变量冲突还可以创建私有变量和方法,十分适合用来创建一个模块,如下所示:

// 创建一个立即调用的匿名函数表达式
// return一个变量,其中这个变量里包含你要暴露的东西
// 返回的这个变量将赋值给 module
var module = (function() {
  var num = 1;
  return {
    get: function () {
      return num;
    },
    set: function (value) {
      num = value;
    }
  }
})();

更多阅读

CMD & SeaJS

CMD(Common Module Definition) 通用模块定义。其是国内前端大牛玉伯的杰作,而 SeaJS 则是 CMD 规范的模块加载器即具体实现。然而随着前端的发展,SeaJS 已经逐渐退出了前端的舞台了。因此这里只会稍微讲解下 CMD 和 SeaJS 的一些知识点。了解下就好。

CMD 和 AMD

通常会拿 CMD 规范来和 AMD 规范进行对比。那么 AMD 和 CMD 有什么区别和对比的呢?

申明依赖模块不同

对于依赖的模块,AMD 和 CMD 的处理方式是不一样的。

  • AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。
  • CMD 推崇依赖就近,只有在用到某个模块的时候再去 require 。
// CMD
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // 依赖可以就近书写
  var b = require('./b') 
  b.doSomething()
})

// AMD 默认推荐的是 依赖必须一开始就写好
define(['./a', './b'], function(a, b) { 
  a.doSomething()
  b.doSomething()
}) 

执行依赖模块时机

  • AMD 提前执行依赖(异步加载:依赖先执行)+延迟执行
  • CMD 延迟执行依赖(运行到需加载,根据顺序执行)

加载器

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。

使用SeaJS

SeaJS 是 CMD 规范的具体实现。使用 SeaJS 和使用 requireJS 十分类似,只是写法上稍微有所不同,具体如下:

  1. 引入 SeaJS 的库
  2. 定义模块(define)
  3. 暴露模块接口(exports)
  4. 加载依赖模块(require)

如下面的代码:

<script src="./sea.js"></script>
<script>
  define(function (require,exports,module) {
      // exports : 对外的接口
      // requires : 依赖的模块
      require('./a.js');//如果地址是一个模块的话,那么require的返回值就是模块中的exports
  });
</script> 

SeaJS(CMD)为什么被淘汰

事实上,在过去 SeaJS(CMD) 曾经颇具影响力,为什么要逐渐被淘汰了呢?

感兴趣的同学可以带着这样的问题,阅读下面的帖子。
Sea.js作者发布微博: 应该给 Sea.js 和 KISSY 也树一块墓碑了。 为啥啊?过时了吗?

更多阅读

面向未来的 ES6 模块标准

既然模块化已经越来越重要,那么从语言层面上直接去解决这个问题就显得很有必要(况且其他语言早就有了)。于是 ES6 直接在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

设计思想

简单来说,ES6 模块的设计思想就是:一个 JS 文件就代表一个 JS 模块。在模块中可以使用 import 和 export 关键字来导入或导出模块中的东西。

ES6 模块主要具备以下几个基本特点:

  • 自动开启严格模式,即使没有写 use strict
  • 每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域
  • 模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等
  • 每一个模块只加载一次,每一个 JS 只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。 一个模块就是一个单例,或者说就是一个对象

export

export 命令用于规定模块的对外接口。如果希望外部能够读取模块内部的变量,函数或类等,就必须使用 export 关键字输出该内容。

下面以个人所得税计算为一个模块,来具体使用下 export:

// 个人所得税计算模块
// 在线参考站点:[个人所得税](http://www.gerensuodeshui.cn/)
// personal-income-tax.js

// 个税起征点
export const taxBasicNum = 3500;

// 税率等级
export const taxRatioLevel = [
    {
        num: 1500, // 小于1500
        ratio: '3%',
        subtract: 0, // 速算扣除数
    }, 
    {
        num: 4500, // 大于1500,小于4500
        ratio: '10%',
        subtract: 105,
    }, 
    {
        num: 9000, // 大于4500,小于9000
        ratio: '20%',
        subtract: 555,
    }, 
    {
        num: 35000, // 大于9000,小于35000
        ratio: '25%',
        subtract: 1005,
    }, 
    {
        num: 55000,  // 大于35000,小于55000
        ratio: '30%',
        subtract: 2755,
    }, 
    {
        num: 80000,  // 大于55000,小于80000
        ratio: '35%',
        subtract: 5505,
    }, 
    {
        num: 80000,  // 大于80000
        ratio: '45%',
        subtract: 13505,
    }];

// 所缴税收
// 应纳税所得额 = 应发工资 - 五险一金 - 个税起征点
// 所缴税收 = 应纳税所得额 * 税率 - 速算扣除数
export function calTax (num, insurance) {
    let taxShouldNum = num - insurance - taxBasicNum;
    let tax;
    switch (true) {
        case taxShouldNum < taxRatioLevel[0].num:
            tax = taxShouldNum * taxRatioLevel[0].ratio - taxRatioLevel[0].subtract;
            break;
        case taxShouldNum < taxRatioLevel[1].num:
            tax = taxShouldNum * taxRatioLevel[1].ratio - taxRatioLevel[1].subtract;
            break;
        case taxShouldNum < taxRatioLevel[2].num:
            tax = taxShouldNum * taxRatioLevel[2].ratio - taxRatioLevel[2].subtract;
            break;
        case taxShouldNum < taxRatioLevel[3].num:
            tax = taxShouldNum * taxRatioLevel[3].ratio - taxRatioLevel[3].subtract;
            break;
        case taxShouldNum < taxRatioLevel[4].num:
            tax = taxShouldNum * taxRatioLevel[4].ratio - taxRatioLevel[4].subtract;
            break;
        case taxShouldNum < taxRatioLevel[5].num:
            tax = taxShouldNum * taxRatioLevel[5].ratio - taxRatioLevel[5].subtract;
            break;
        case taxShouldNum > taxRatioLevel[6].num:
            tax = taxShouldNum * taxRatioLevel[6].ratio - taxRatioLevel[6].subtract;
            break;
        default:
            tax = 0;
    }

    return tax;
}

// 实发工资
export function calWages(num, insurance) {
    let tax = calTax(num, insurance);
    let wages = num - insurance - tax;

    return wages;
}

import

使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。现在导入前面设计的个人所得税计算模块。

// main.js
import {taxBasicNum, taxRatioLevel, calTax, calWages} from './personal-income-tax';

// 可以使用 taxBasicNum 输出一段话,说明个税的起征点是多少
console.log(`个税起征点为:taxBasicNum`);
// 还可以使用 taxRatioLevel 数据输出一个表格,对应各个等级的税率,这里就不演示了

// 计算20000元缴纳了五险一金3000后,应该缴纳多少税收及实际税后工资为多少
let tax = calTax(20000, 3000);
let wages = calWages(20000, 3000);

更多 ES6 模块知识

通过上面的 export 和 import 的使用,是不是觉得 ES6 的模块很方便,也很简单。当然上面都是些基础的运用,而关于 ES6 模块网上也已经有了很多详细的文档,在这就不一一介绍了,这里推荐两篇详细文档以供参考学习:

注:目前现代浏览器对 ES6 模块支持程度不同,所以一般都是使用 babel 把 ES6 代码转化为兼容的 ES5 版本的代码。

JSON

前端新手对 JSON 的理解都是错误的。

官网的第一句话:

JSON (JavaScript Object Notation) is a lightweight data-interchange format.

JSON 是一种轻量级的数据交换格式。JSON 是一种格式,而不是对象,所以别再说“这个 JSON 对象有问题”这种话。

var a = { // 这是一个对象字面量,和 JSON 无关
    "hello": "JSON"
};

var b = [ // 这是一个数组字面量,和 JSON 无关    {        "hello": "JSON"    }];

var c = '{"hello":"JSON"}'; // 这是一个字符串变量,和 JSON 有一点关系
{"hello":"JSON"}

这才是 JSON ,JSON 就是文本,和什么语言没有关系,JS 可以解析,自然 JAVA、Python 都可以解析,所以{"hello":function () {}}不是一个 JSON 是不是就很容易理解了,这个 function放在 JAVA 里面算个什么回事?!

当然,它是有格式的文本,至于什么格式,看官网 json.org/ 就好了。

jqXHR

jqXHR 是一个对象,是 jQuery 中的 XHR 的对象,影响了一代人。

辛辛苦苦的写了下面这个代码:

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
        // todo
    } else {
        console.log(xhr.status);
    }
}

xhr.open('get', 'data.json');
xhr.send(null);

使用 jQuery 只用这样:

$.getJSON('data.json', function() {
    // todo
});

没人不喜欢这样写吧,具体可以参考 api.jquery.com/jquery.getj… 。所以很多人认为 Fetch 的出现就是受到 jQuery 的影响。

$.getJSON 是一个简写,jQuery 的 ajax 有更底层的接口:

$.ajax({
    dataType: 'json',
   url: 'data.json',
   success: function() {
        // todo
    }
});

更详细的介绍可以参考这里:api.jquery.com/jQuery.ajax… 。

给上面的代码赋个值:

const jqXHR = $.ajax({
    dataType: 'json',
   url: 'data.json',
   success: function() {
        // todo
    }
});

没错,所有的 jQuery ajax 返回值,不管是 ajax() 底层还是 getJSON() 高阶,都是返回 jqXHR 对象。

jqXHR 是原生 XHR 的封装,原生有的它都有,原生没有的,它也有。

The jqXHR objects returned by $.ajax() as of jQuery 1.5 implement the Promise interface, giving them all the properties, methods, and behavior of a Promise.

注意 Promise 字眼,Promise 是必须要掌握的东西,因为它是所有异步编程技术的基础,可以看看下面的参考资料。

而 Promise 带给 jqXHR 的变化,就是像下面这样写代码:

$.getJSON()
 .done((data) => { // todo })
   .done((data) => { // todo again })
    .fail((err) => { // if error })

这就是为什么要聊聊 jqXHR 。

向 jQuery 致敬。

参考

【HTTP】GET传参最大长度的理解误区

www.jianshu.com/p/512389822…

四种常见的 POST 提交数据方式

imququ.com/post/four-w…

编写一个函数来自动 JSONP

/**
 * 自动发送 jsonp
 * @param {String} url
 * @param {Obj} data
 * @param {Function} callback
 */
function jsonp (url, data, callback) {
  var funcName = getFunctionName()
  data = data || {}
  data.callback = funcName
  url = parseUrl(url, serialize(data))

  window[funcName] = function (response) {
    // 这里可以看情况处理,比如如果是 jsonp 我们可以 parse 一下
    // data = JSON.parse(response)
    callback(response)
  }

  createScript(url)
}

/**
 * 序列化参数
 * jsonp 中参数只能是 GET 方式传递
 * @param {Obj} data
 */
function serialize (data) {
  var ret = []

  Object.keys(data).forEach(item => {
    ret.push(encodeURIComponent(item) + '=' + encodeURIComponent(data[item]))
  })

  return ret.join('&')
}

/**
 * 处理 URL ,把参数拼上来
 * @param {String} url
 * @param {String} param
 */
function parseUrl (url, param) {
  return url + (url.indexOf('?') === -1 ? '?' : '&') + param
}

/**
 * 这里就是对 jsonp 的理解了,必须要有一个全局函数,而且不能重名
 */
function getFunctionName () {
  return ('jsonp_' + Math.random()).replace('.', '')
}

/**
 * 创建 script 标签并插到 body 中
 * @param {String} url
 */
function createScript (url) {
  var doc = document
  var script = doc.createElement('script')
  script.src = url

  doc.body.appendChild(script)
}

编写一个函数来自动创建 iframe 跨域

写一个 cross 函数,将和 iframe 跨域相关的操作都封装起来。

<!-- a 页面的代码 -->
<body>
  <div>正在获取数据……</div>
  <script src="asset/cross.js"></script>
  <script>
    // --->调用 cross 函数,我们就是要实现这个函数!<-----------
    cross({
      targetUrl: 'http://127.0.0.1:3002/b.html', // 请求数据的页面
      skipUrl: 'http://127.0.0.1:3002/c.html', // 当区域跳转的页面
      data: {} // 参数
    }, (data) => {
        data = JSON.parse(data);

        var html = ['<ul>'];
        data.forEach((item) => {
          html.push(`<li>${item.name} love ${item.love}</li>`);
        });
        html.push('</ul>');

        document.querySelector('div').innerHTML = html.join('');
    });
  </script>
</body>

相比于 jsonp ,iframe 要在多个页面协调,以下是 b 和 c 页面相关的代码。

<!-- b 页面 -->
<body>
  <script>
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        window.name = xhr.responseText;
        // 注意这里
        // “跳转页面” 也应该从 `location.search` 拿的,这里没处理
        location.href = `http://127.0.0.1:3001/c.html${location.search}`;
      } else {
        console.log(xhr.status, xhr.statusText);
      }
    }

    xhr.open('get', 'data.json');
    xhr.send(null);

  </script>
</body>
<!-- c 页面 -->
<body>
  <script>
    // 从 location.search 拿数据
    const getParams = query => {
      if (!query) {
        return { };
      }

      return (/^[?#]/.test(query) ? query.slice(1) : query)
        .split('&')
        .reduce((params, param) => {
          let [ key, value ] = param.split('=');
          params[key] = value ? decodeURIComponent(value.replace(/+/g, ' ')) : '';
          return params;
        }, { });
    };

    const query = getParams(location.search);

    // 获取 a 页面的回调并执行
    parent[query.parentFuc](window.name);
  </script>
</body>

ES7 的 Decorator

在许多面向对象的语言中都有装饰器(Decorator) ,用来增加类的功能和职责。

令人喜悦的是,在未来的 JavaScript 中也引入了这个概念,目前 babel 对这个概念也有很好的支持。一个喜欢尝试新技术的开发者,可以借助 babel 来大胆使用它。下面来了解一下更多内容。

如何使用

Decorator 在目前还只是一个提议。如果现在就想要体验使用的话,可以通过下面两种方法:

通过使用 Babel 来转换使用

  1. 首先全局安装babel组件模块

    npm install babel -g
    
  2. 命令行开启 Decorator

    babel --optional es7.decorators test.js > test.es5.js
    

通过 在线的 REPL 提前体验

Babel 的官方网站提供一个在线转码器,其支持 Decorator 的在线转码。

JavaScript 的 Decorator

装饰器(Decorator)  在现代编程语言中是相当普遍的。事实上,JavaScript 装饰器的语法与 Python 装饰器的语法非常相似。有以下两种用法:

  • 装饰类
  • 装饰类的方法

装饰类

@superManPower
class Man {
  // some code
}

// 装饰器函数
function superManPower(target) {
  target.super = true;
}

// 等同于
class Man {}
// 装饰器函数 superFunc
function superManPower(target) {
  this.super = true;
}
Man = superManPower(Man) || Man;

上面是简单的一个 装饰器(Decorator)  的例子,其中

  • @superManPower 则为使用装饰器 superManPower

  • 装饰器 superManPower 修改了类 Man 的行为,给其加上静态属性 super

  • 此时装饰器函数是一个对类进行处理的函数。其第一个参数为需要被装饰的目标类,如下:

    // 装饰器函数
    function superManPower(target) {
      // target 即为被装饰的目标
      target.super = true;
    }
    

装饰类的方法

装饰器除了可以装饰类之外,还可以修饰类的属性方法。如下所示:

function superInitPower(attack, defense) {
  return function(target, key, descriptor) {
    // 保存方法
    const method = descriptor.value;
    // 修改方法
    descriptor.value = (...args)=>{
      // 增加攻击力和防御力
      args[0] += 100;
      args[1] += 100;
      // 调用原本的方法
      return method.apply(target, args);
    }
  return descriptor;
  }
}
class Man {
  @superInitPower
  init(attack, defense) {
    this.attack = attack; // 初始攻击力
    this.defense = defense; // 初始攻击力
  }
}

此时,装饰器函数可以接受三个参数,和 Object.defineProperty() 的参数基本保持一致,分别表示如下:

  • 第一个参数 target 表示修饰的目标对象 即类的实例

    这不同于之前装饰类,之前装饰类 target 表示的是类本身

  • 第二个参数 key 表示需要装饰的属性名

  • 第三个参数 descriptor 是该属性的描述对象

    // descriptor 对象的值
    {
      value: specifiedFunction, // 指定的方法
      enumerable: false, // 是否可遍历迭代
      configurable: true, 
      writable: true
    };
    

多层装饰器

可以同时使用多个装饰器,如下所示:

@healthy
class Person {
  @runFase
  @eatHuge
  init() { 
  }
}

例如上面的,我们使用了多个装饰器,增加类和类的方法的职责。

更多参数

上面看到装饰器参数都是固定的,对于一些场景可能会不够用。此时通过包装一层函数的方式来增加参数,如下所示:

// 装饰器函数 - 增加参数
function superFunc(num, canUse) {
  return function(target) {
    target.canUse = canUse;
    target.num = num;
  }
}

// 因此下面就可以这样调用
@superFunc(1, true)
class Base1() {}
Base1.canUse; // true
Base1.num; // 1

@superFunc(2, false)
class Base2() {}
Base2.canUse; // false
Base2.num; // 2

上面中,看到装饰器 superFunc 是可以接受参数,通过各种参数来增强装饰器的能力。

注: 装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器本质就是编译时执行的函数。

不能装饰普通函数

事实上 装饰器(Decorator)  只使用与装饰类和类的方法,不能使用于普通函数。这是因为变量提升会产生系列的问题。这里可以查看更多阅读来了解相关的问题详情。

使用场景

常常使用 装饰器(Decorator)  来实现 AOP(面向切面编程)

更多阅读

想要了解更多知识,可以阅读以下的资料文献。

数据双向绑定的常见实现原理

前面讲到的 MVVM 框架,其主要亮点就是实现了视图层和数据层的 双向绑定(two-way-binding) 。那么双向绑定在前端中是如何实现的呢?在本文中,将讲解数据双向绑定的常见实现方式。

观察者模式实现

前面学习了什么是观察者模式。观察者模式可以来实现数据的双向绑定。只不过这种方式需要手动地去调用获取和设置数据的指定方法(如 updateData 等)。在数据发生变化的时候,发布一个叫数据变化的的事件。同理,当视图发生变化的时候,发布一个叫视图变化的事件。在这些事件发生时只要让相关的观察者去订阅这些事件即可。

如下面的代码:

// 更新数据 
var data = {};
function updateData(attr, value){
   data[attr] = value;
   Event.trigger('数据变化', attr, value);
}
// 订阅视图变化事件 
Event.on('视图变化', function(attr, value){
  // 更新数据
  updateData(attr, value);
});
// 订阅数据变化事件 
Event.on('数据变化', function(attr, value){
   // 更新绑定的视图
});

//用户操作,发布视图变化事件   
document.body.addEventListener('keyup', function( e ){
   Event.trigger('视图变化');
})

这种方式,是最早期的方式,问题是需要手动的方式来绑定关系,对于那种多交互和数据修改的页面就比较繁琐。

脏检测 (Dirty Checking)实现

典型的 angularJS 的数据双向绑定就是基于数据的脏检测机制来实现的。angularJS 通过检查脏数据来进行 UI 层的操作更新,以下是相关点:大体原理是通过记录所有变量的当前值,然后当发生特定的操作之后,angularJS 会调用 $digest 方法,这个方法内部做的逻辑就是遍历所有的 watcher(观察者),对被监控的属性做对比,对比其在方法调用前后属性值有没有发生变化,如果发生变化,则调用对应的 handler (处理函数)。

触发脏检测的操作

  • DOM事件,譬如用户输入文本,点击按钮(ng-click)等
  • XHR响应事件 ($http)
  • 浏览器Location变更事件 ($location)
  • Timer事件(timeout,timeout, interval)
  • 执行 digest()digest() 或 apply()

如果感兴趣 angularJS 双向数据绑定的具体实现,可以阅读下面的一些文章:

脏检测方式的缺点十分明显,就是遍历 watcher 是很消耗性能的,特别是当监控数量达到一个数量级的时候。

数据劫持(Hijacking)实现

事实上,目前许多 MVVM 框架(vue.js、avalon.js 等)都是使用这种技术实现,数据劫持的核心是通过 Object.defineProperty() 来实现。可以使用 Object.defineProperty() 来定义一个对象的属性情况,如下:

Object.defineProperty(obj, key,{
    value: someValue ,
    writable: true, // 可写
    enumerable: true, // 可被赋值
    configurable: true // 可改变和删除
})

除了上面之外,还可以设置属性的 gettersetter。当存在 gettersetter 函数时,会出现下面的情况:

  • 当获取目标属性时触发 getter 函数的执行
  • 当对目标属性进行赋值操作时会触发 setter函数的执行

根据行业的术语,这种方式称为 数据劫持。通过使用 数据劫持,可以这样实现双向绑定:

// 定义一个设置属性 setter 和 getter 的方法
function defineProperty(obj, attr, value){
  // 私有变量 value
  var value;

  Object.defineProperty(obj, attr, {
    get:function (){
      console.log('获取属性值' + value);
      return value;
    },
    set:function (val){
      console.log('修改属性值为 ' + value); 
      value = val;
      console.log('修改属性值'); 
    }
  })

  // 设置属性值
  obj[attr] = value;
}

// 通过使用 defineProperty 方法,设置 man 的 name 属性的数据变化监听
var man = {};
defineProperty(man, 'name', "cover"); // 修改属性值为 cover
man.name; // 获取属性值 cover
man.name = 'kevin'; // 修改属性值为 kevin

通过 Object.defineProperty() ,可以轻松地得知数据(对象)发生变化,然后在相应的 setter 函数中设置数据更新相应的操作(如发布更新事件)。通过这种方式可以使用赋值操作 = 来实现数据绑定,相对前面通过手动调用绑定指定更新数据接口 updateData 来说更加优雅。

注:比较主流的方法,针对部分还不支持 Object.defineProperty 低级浏览器,可采用 VBScript 作了完美兼容。当然大部分 mvvm 框架已经逐渐放弃对低端浏览器的支持。

总结

上面简单的讲解了前端是如何实现 双向绑定(two-way-binding)  的,可以根据上面的内容尝试去观察上面四种方式的异同点和不同的理念。从中领悟到前端的技术是如何不断更新发展的,以及一个复杂的框架是如何通过最初简单的思路逐渐形成的。

更多阅读

React 体系

React 库

下面提供一些学习的资源,供参考。

首先是 React 的官网,React 官方文档还是比较容易看懂的,还有附带教程,也可以跟着官方教程去练习。推荐最原始的英文官网,因为那里是更新的最快最及时的。

React 的衍生

经常说的 React ,可以称为 React Core ,2011 年由 Facebook 工程师 Jordan Walke 创建,React 现在是一个巨无霸。

React Core 有很大的变化,它进一步的分化出了 React Core、React DOM、React Server 。Core 不多说,React DOM 是用于浏览器,React Server 用于 Nodejs ,它的魔爪已经伸向了服务端,就是这个服务端渲染的特性被很多框架所借鉴,算是开启了一个时代。

围绕这个 React Core ,大批量的辅助库涌现了出来,典型的如 React RouterRedux ,生态越来庞大,于是又有了 ant-design 这些大而全的框架。

前端和服务端都被 React 占领,它还没有停歇,触角又伸向了 iOS 和 Android ,React-Native ,从此可以用 HTML CSS JS 写出手机端应用。

实例解析防抖动(Debouncing)和节流阀(Throttling)

jinlong.github.io/2016/04/24/…

localStorage

localStorage

cookie 库

js-cookie

Web 相关编码和转义

提到 Web 前端安全,第一个想到的就是 XSS。
而提到 XSS,不得不说的就是编码了。事实上,编码涉及的知识十分的复杂,很难短时间梳理清楚和弄明白。在这里只要求对编码有一个概念就好,日后再进行深究。

常用编码

本文从 XSS 中常用的编码讲起,分为

  • URL 编码
  • HTML 编码
  • JS 编码

URL编码

一般来说,URL只能使用英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有(;,/?:@&=+$#)保留字符。

意味着如果使用了一些其他文字和特殊字符,则需要通过编码的方式来进行表示,如:

// 使用了汉字
var url1 = 'http://www.帅.com';

另外在 URL 中传参是通过键值对形式进行的,格式上是以& 和 = 为特征标识进行解析,如果在键或者值的内容中包含一些特殊符号,就会造成解析错误,如下所示:

// 键为汉字
var url2 = 'http://www.a.com?我=1';
// 值的内容为特殊符号
var url3 = 'http://a.com?key=?&';

对于上面的情况如果要正常解析,则需要进行编码,需要用不会造成歧义的符号代替有歧义的符号。

可以通过使用系统原生实现的 API 来对字符进行 URL 编码:

encodeURI

encodeURI 是用来编码 URI 的,最常见的就是编码一个 URL。encodeURI 会将需要编码的字符转换为 UTF-8 的格式。对于保留字符(;,/?:@&=+$#),以及非转义字符(字母数字以及 -_.!~*'())不会进行转义。

例如之前 URL 中包含中文,我们可以使用 encodeURI:

encodeURI('http://www.帅.com'); // http://www.%E5%B8%85.com
encodeURI('http://www.a.com?我=1');// "http://www.a.com?%E6%88%91=1"

在这里,%E5%B8%85 就是  的 URL 编码,%E6%88%91 即为  的 URL 编码。然后由于 encodeURI 不转义&?=。所以对于 URL 参数的值是无法转义的,如下面的例子:

// 值的内容为特殊符号
encodeURI('http://a.com?key=?&'); // "http://a.com?key=?&"

此时就需要使用 encodeURIComponent 来解决。

encodeURIComponent

顾名思义,encodeURIComponent 是用来编码 URI 参数的。它会跳过非转义字符(字母数字以及-_.!~*'())。但会转义 URL的 保留字符(;,/?:@&=+$#)。通常来说会 encodeURI 结合 encodeURIComponent 来使用,如下所示:

// "http://a.com?a=%3F%26"
encodeURI('http://a.com') + '?a=' + encodeURIComponent('?&');  

其中 %3F 和 %26 分别为 ? 和 & 的 URL 编码。需注意的是由于 encodeURIComponent 会编码所有的 URL 保留字,所以不适合编码 URL,例如:

// "http%3A%2F%2Fa.com%3Fkey%3D%3F%26"
encodeURIComponent('http://a.com?key=?&');

上面的 http%3A%2F%2Fa.com%3Fkey%3D%3F%26 在地址栏会被解析为一个普通的字符串而不是 URL。

URL 解码

有了 URL 编码,相应的会有解码机制。比如上面对应的 3个 encode 的API对应的解码 API 如下:

HTML 编码

在 HTML 中,某些字符是预留的,比如不能使用小于号(<)和大于号(>),这是因为浏览器会误认为它们是标签。如果希望正确地显示预留字符,我们必须在 HTML 源代码中使用字符实体(character entities)。当然还另一个重要原因,有些字符在 ASCII 字符集中没有定义,因此需要使用字符实体来表示,比如中文。

HTML 编码分为:

  • HTML 十六进制编码 &#xH;
  • HTML 十进制编码 &#D;
  • HTML 实体编码 &lt; 等

在 HTML 进制编码中其中的数字则是对应字符的 unicode 字符编码。
比如单引号的 unicode 字符编码是27,则单引号可以被编码为&#x27;

HTML 实体编码

通常来说,在业务中用到更多的是 HTML 的实体编码。常用的 HTML 实体编码函数如下所示:

/**
 * 转义 HTML 特殊字符
 * @param {String} str
 */
function htmlEncode(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

更完整的转义列表见这里

Javascript 转义

JavaScript 中有些字符有特殊用途,如果字符串中想使用这些字符原来的含义,需要使用反斜杠对这些特殊符号进行转义。称之为 Javascript编码。一般有以下几类:

  • 三个八进制数字,如果不够个数,前面补0,例如 “e” 编码为“\145”
  • 两个十六进制数字,如果不够个数,前面补0,例如 “e” 编码为“\x65”
  • 四个十六进制数字,如果不够个数,前面补0,例如 “e” 编码为“\u0065”
  • 对于一些控制字符,使用特殊的C类型的转义风格(例如\n和\r)

如下面所示,双引号用于标注字符串,然而在字符串中带了双引号,就会发生歧义:

var str = "Hello"";

于是需要对字符串内的双引号进行转义,也就是加上反斜杠,告诉脚本引擎要区分对待:

var str = "Hello"";

更多阅读

对 Web 相关编码和转义十分感兴趣的同学可以阅读下面的文章和内容来加深理解。

XSS 防范代码库

DDOS防范

在许多黑客题材的电视剧电影中,可以看到 DDOS (分布式拒绝服务) 攻击经常出现。DDOS 出镜率之所以高是因为其易攻难守。十多年过去了,DDOS 攻击仍是业界安全问题中一个难题,当攻击流量超过了服务的网络设备的极限或带宽的最大负荷时,网站服务将瘫痪导致无法正常运作。

通常来说,对于 DDOS 攻击可以通过下面的方式来进行防范。

验证码

验证码是在互联网十分常见的技术之一。不得不说验证码是能够有效地防止多次重复请求的行为。目前来说,随着验证码方式的丰富化,导致黑客们没有一个比较有效的方式来获取和识别验证码。但由于验证码会影响到用户体验,因此验证码只有在比较重要的入口才会使用,如转账和登陆的场景。

限制请求频率(ratelimit)

限制请求频率是最常见的针对 DDOS 攻击的防御措施。其原理为设置每个客户端的请求频率的限制。简单来说,就是通过一些标识(如 IP 与 Cookie)来定位一个客户端,如果该客户端的请求在一定时间内过于频繁。则对该客户端之后的请求都重定向到一个出错的页面或者封杀相关 IP。

当然许多框架都会有相应地限制请求频率的工具包,如 koa 框架便可以使用下面这些:

扩容加带宽

简单来说就是,增加机器增加服务带宽。只要超过了攻击流量便可以避免服务瘫痪。在双十一活动或者12306 抢车票等场景时,网站服务都会对自己的机器进行扩容和提高带宽。然而为了避免由于使用过多机器导致成本太高且浪费的情况。通常来说都是根据网站活动和请求情况来实施相应扩容操作。

其他的方法

事实上还有许多其他的方法,根据不同情况都有相应的效果

  • 设置自己的业务为分布式服务,防止单点失效
  • 使用主流云系统和 CDN(云和 CDN 其自身有 DDOS 的防范作用)
  • 优化资源使用提高 web server 的负载能力

更多阅读

网上有许多 DDOS 的帖子,感兴趣可以阅读下:

HTTP 明文特性

超文本传输协议 HTTP协议 用于在 Web 浏览器和网站服务器之间传递信息。然而由于 HTTP 协议其特性是在传输过程中是明文方式的,即不提供任何方式的数据加密。这也导致了当攻击者截取了这之间的传输报文,就可以直接读懂其中的信息和窥视和篡改。这也是 HTTPS 出现的原因。

安全的 HTTPS

为了解决 HTTP 协议的这一缺陷,需要使用另一种协议:HTTPS。为了数据传输的安全,HTTPS 在 HTTP 的基础上加入了 SSL(安全套接字层)  协议,SSL 依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。SSL 标准化改名为 TLS, SSL/TLS 在传输层对网络连接进行加密。

点击劫持

点击劫持实际上就是在正常界面上覆盖一层透明的内容,引导用户在不知情的情况下进行了某些操作,这些操作可能是广告点击,或者是开启摄像头等操作,甚至可能会导致一些数据别窃取,也有可能结合 XSS 和 CSRF 进行更负责的攻击。

服务端安全

除了前端注意安全,后端也需要对安全进行防范,常见比如防止 SQL 注入,登录会话管理等。

更多阅读

身为一名前端工程师,要学会如何去保护好自己的网站。