html&js组合相关汇总

# 编码

# Unicode、UTF-8、UTF-16、UTF-32有什么区别?

# Unicode

在说Unicode之前需要先了解一下ASCII码: ASCII 码(American Standard Code for Information Interchange)称为美国标准信息交换码。

  • 它是基于拉丁字母的一套电脑编码系统。
  • 它定义了一个用于代表常见字符的字典。
  • 它包含了"A-Z"(包含大小写),数据"0-9" 以及一些常见的符号。
  • 它是专门为英语而设计的,有128个编码,对其他语言无能为力

ASCII码可以表示的编码有限,要想表示其他语言的编码,还是要使用Unicode来表示,可以说UnicodeASCII 的超集。

Unicode全称 Unicode Translation Format,又叫做统一码、万国码、单一码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。 Unicode的实现的方式: Unicode的实现方式(也就是编码方式)有很多种,常见的是UTF-8UTF-16UTF-32USC-2

# UTF-8

UTF-8是使用最广泛的Unicode编码方式,它是一种可变长的编码方式,可以是1—4个字节不等,它可以完全兼容ASCII码的128个字符。 注意: UTF-8 是一种编码方式,Unicode是一个字符集合。 UTF-8的编码规则:

  • 对于单字节的符号,字节的第一位为0,后面的7位为这个字符的Unicode编码,因此对于英文字母,它的Unicode编码和ACSII编码一样。
  • 对于n字节的符号,第一个字节的前n位都是1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的Unicode码 。

来看一下具体的Unicode编号范围与对应的UTF-8二进制格式 :

编码范围(编号对应的十进制数) 二进制格式
0x00—0x7F (0-127) 0xxxxxxx
0x80—0x7FF (128-2047) 110xxxxx 10xxxxxx
0x800—0xFFFF (2048-65535) 1110xxxx 10xxxxxx 10xxxxxx
0x10000—0x10FFFF (65536以上) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

那该如何通过具体的Unicode编码,进行具体的UTF-8编码呢? 步骤:

  • 找到该Unicode编码的所在的编号范围,进而找到与之对应的二进制格式
  • Unicode编码转换为二进制数(去掉最高位的0)
  • 将二进制数从右往左一次填入二进制格式的X中,如果有X未填,就设为0

来看一个实际的例子: “” 字的Unicode编码是:0x9A6C,整数编号是39532 (1)首选确定了该字符在第三个范围内,它的格式是 1110xxxx 10xxxxxx 10xxxxxx (2)39532对应的二进制数为1001 1010 0110 1100 (3)将二进制数填入X中,结果是:11101001 10101001 10101100

# UTF-16

1. 平面的概念 在了解UTF-16之前,先看一下平面的概念: Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区存放65536(2)个字符,这称为一个平面,目前总共有17 个平面。

最前面的一个平面称为基本平面,它的码点从0 — 2-1,写成16进制就是U+0000 — U+FFFF,那剩下的16个平面就是辅助平面,码点范围是 U+10000—U+10FFFF

2. UTF-16 概念: UTF-16也是Unicode编码集的一种编码形式,把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位需要1个或者2个16位长的码元来表示,因此UTF-16也是用变长字节表示的。

3. UTF-16 编码规则:

  • 编号在 U+0000—U+FFFF 的字符(常用字符集),直接用两个字节表示。
  • 编号在 U+10000—U+10FFFF 之间的字符,需要用四个字节表示。

4. 编码识别 那么问题来了,当遇到两个字节时,怎么知道是把它当做一个字符还是和后面的两个字节一起当做一个字符呢?

UTF-16 编码肯定也考虑到了这个问题,在基本平面内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因此这些空段就可以用来映射辅助平面的字符。

辅助平面共有 2 个字符位,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为高位(H),后 10 位映射在 U+DC00 — U+DFFF,称为低位(L)。这就相当于,将一个辅助平面的字符拆成了两个基本平面的字符来表示。

因此,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就可以知道,它后面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。

5. 举例说明 以 "𡠀" 字为例,它的 Unicode 码点为 0x21800,该码点超出了基本平面的范围,因此需要用四个字节来表示,步骤如下:

  • 首先计算超出部分的结果:0x21800 - 0x10000
  • 将上面的计算结果转为20位的二进制数,不足20位就在前面补0,结果为:0001000110 0000000000
  • 将得到的两个10位二进制数分别对应到两个区间中
  • U+D800 对应的二进制数为 1101100000000000, 将0001000110填充在它的后10 个二进制位,得到 1101100001000110,转成 16 进制数为 0xD846。同理,低位为 0xDC00,所以这个字的UTF-16 编码为 0xD846 0xDC00

# UTF-32

UTF-32 就是字符所对应编号的整数二进制形式,每个字符占四个字节,这个是直接进行转换的。该编码方式占用的储存空间较多,所以使用较少。

比如“” 字的Unicode编号是:U+9A6C,整数编号是39532,直接转化为二进制:1001 1010 0110 1100,这就是它的UTF-32编码。

# 总结

简单的了解完这几个概念,那就来简单回答一下最开始的那个问题: Unicode、UTF-8、UTF-16、UTF-32有什么区别?

  • Unicode 是编码字符集(字符集),而UTF-8UTF-16UTF-32是字符集编码(编码规则)
  • UTF-16 使用变长码元序列的编码方式,相较于定长码元序列的UTF-32算法更复杂,甚至比同样是变长码元序列的UTF-8也更为复杂,因为其引入了独特的代理对这样的代理机制
  • UTF-8需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力教强
  • 如果字符内容全部英文或英文与其他文字混合,但英文占绝大部分,那么用UTF-8就比UTF-16节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16就占优势了,可以节省很多空间

# escape,encodeURI,encodeURIComponent 的区别?

encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义

encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。 escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。

# 位置尺寸

# js的各种位置,比如clientHeight,scrollHeight,offsetHeight ,以及scrollTop, offsetTop,clientTop的区别?

  • clientHeight:表示的是可视区域的高度,不包含border和滚动条
  • offsetHeight:表示可视区域的高度,包含了border和滚动条
  • scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。
  • clientTop:表示边框border的厚度,在未指定的情况下一般为0
  • scrollTop:滚动后被隐藏的高度,获取对象相对于由offsetParent属性指定的父坐标(css定位的元素或body元素)距离顶端的高度。

# JSON

# 对JSON的理解

JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。

在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。

因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。

在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,

  • JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
  • JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

# 加载

# JavaScript脚本延迟加载的方式有哪些?

相关知识点: js 延迟加载,也就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。

一般有以下几种方式:

  • defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
  • async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
  • 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  • 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载js脚本文件
  • 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

# script标签中defer和async的区别

如果没有defer或async属性,浏览器会立即加载并执行相应的脚本。也就是说在渲染script标签之后的文档之前,不等待后续加载的文档元素,读到就开始加载和执行,这样就会阻塞后续文档的加载。

下图可以直观的看出三者之间的区别: image.png

其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。

defer 和 async属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析,其区别如下:

  • 多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行
  • async属性,表示后续文档的加载和渲染与js脚本的加载和执行是并行进行的,即异步执行;
  • 有了defer属性,加载后续文档的过程和js脚本的加载(此时仅加载不执行)是并行进行的(异步),js脚本的执行需要等到文档所有元素解析完成之后,DOMContentLoaded事件触发执行之前

# DOM

# 什么是 DOM 和 BOM?

  • 文档对象类型(DOM):把整个页面规划成由节点层级构成的文档
  • 浏览器对象模型(BOM):处理浏览器宽口和框架
  • DOM 指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
  • BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

# DOMContentLoaded、window.onload、body.onload

  • DOMCOntentLoaded:指文档加载完成触发的事件,即dom加载完成,不用考虑其他资源,例如图片
    • 常见的库中都提供了此事件的兼容各个浏览器的封装,如果你是个jQuery使用者,你可能会经常使用**$(document).ready(); 或者$(function(){}) 这都是使用了DOMContentLoaded事件**; DOMContentLoaded事件就相当于jquery中的ready方法,也就是DOM结构加载完成的事件。
  • onload:当页面载入完毕后执行Javascript代码; 页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。
    • window.onload和body.onload谁在下面就是谁覆盖谁,只会执行后面的那个。
<script language="javascript">
    if(document.addEventListener){
        function DOMContentLoaded(){
            console.log("window.onload");
            $("#status").text("DOM is ready now!");
        }
        document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );//1
    }
    window.οnlοad=function(){
        onsole.log("DOMContentLoaded");
        $("#status").text("DOM is ready AND wondow.onload is excute!");//2
    }
</script>
<body onload="console.log('bodyonload');">
    <div id="div1">a</div>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

执行顺序: 在chrome、IE10和FireFox亲测,执行结果是:DOMContentLoaded然后才是onload的输出所以说一般情况下,DOMContentLoaded事件要在window.onload之前执行,当DOM树构建完成的时候就会执行DOMContentLoaded事件。当window.onload事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。

**$(document).ready()**下面三个写法是等价的:

  • $(document).ready(handler)
  • $(handler)
  • $().ready(handler) 不推荐;
//window.onload不能同时编写多个。//以下代码无法正确执行,结果只输出第二个。
window.onload = function(){
  alert("test1");
};
window.onload = function(){
  alert("test2");
};

//$(document).ready()能同时编写多个//结果两次都输出
$(document).ready(function(){ 
   alert("Hello World"); 
}); 
$(document).ready(function(){ 
   alert("Hello again"); 
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 几个很实用的BOM属性对象方法?

什么是Bom? Bom是浏览器对象。有哪些常用的Bom属性呢?

# (1)location对象

location.href-- 返回或设置当前文档的URL location.search -- 返回URL中的查询字符串部分。例如 www.dreamdu.com/dreamdu.php… (opens new window) 返回包括(?)后面的内容?id=5&name=dreamdu location.hash -- 返回URL#后面的内容,如果没有#,返回空 location.host -- 返回URL中的域名部分,例如www.dreamdu.com location.hostname -- 返回URL中的主域名部分,例如dreamdu.com location.pathname -- 返回URL的域名后的部分。例如 www.dreamdu.com/xhtml/ (opens new window) 返回/xhtml/ location.port -- 返回URL中的端口部分。例如 www.dreamdu.com:8080/xhtml/ (opens new window) 返回8080 location.protocol -- 返回URL中的协议部分。例如 www.dreamdu.com:8080/xhtml/ (opens new window) 返回(//)前面的内容http: location.assign -- 设置当前文档的URL location.replace() -- 设置当前文档的URL,并且在history对象的地址列表中移除这个URL location.replace(url); location.reload() -- 重载当前页面

# (2)history对象

history.go() -- 前进或后退指定的页面数 history.go(num); history.back() -- 后退一页 history.forward() -- 前进一页

# (3)Navigator对象

navigator.userAgent -- 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串) navigator.cookieEnabled -- 返回浏览器是否支持(启用)cookie

# AJAX

# 对AJAX的理解,实现一个AJAX请求

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

# 创建AJAX请求的步骤

  • 创建一个 XMLHttpRequest 对象。
  • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 使用Promise封装AJAX

// promise 封装实现:
function getJSON(url) {
  // 创建一个 promise 对象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    // 新建一个 http 请求
    xhr.open("GET", url, true);
    // 设置状态的监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;
      // 当请求成功或失败时,改变 promise 的状态
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    // 设置错误监听函数
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };
    // 设置响应的数据类型
    xhr.responseType = "json";
    // 设置请求头信息
    xhr.setRequestHeader("Accept", "application/json");
    // 发送 http 请求
    xhr.send(null);
  });
  return promise;
}
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

# 跨域

# 为什么会有跨域

同源策略的限制:同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略的限制内容有:

  • cookie、LocalStorage、IndexedDB等
  • DOM 节点
  • http请求的结果被浏览器拦截

但是有三个属性是允许跨域的:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

# 跨域的解决方案

# jsonp

利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

  • 缺陷 不安全 只能get方法

后端逻辑:

// jsonp/server.js
const url = require('url');
	
require('http').createServer((req, res) => {
	const data = {
		x: 10
	};
	// 拿到回调函数名
	const callback = url.parse(req.url, true).query.callback;
	console.log(callback);
	res.writeHead(200);
	res.end(`${callback}(${JSON.stringify(data)})`);

}).listen(3000, '127.0.0.1');

console.log('启动服务,监听 127.0.0.1:3000');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

前端逻辑:

// jsonp/index.html
<script>
    function jsonpCallback(data) {
        alert('获得 X 数据:' + data.x);
    }
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>
1
2
3
4
5
6
7

# cors

CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

// cors/server.js
require('http').createServer((req, res) => {
  res.writeHead(200, {
    'Access-Control-Allow-Origin': 'http://localhost:8080',
    'Content-Type': 'text/html;charset=utf-8',
  });
  res.end('这是你要的数据:1111');
}).listen(3000, '127.0.0.1');

console.log('启动服务,监听 127.0.0.1:3000');
1
2
3
4
5
6
7
8
9
10

# postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

前端逻辑:

// postMessage/client/index.html 对应 localhost:8080/index.html
<iframe src="http://localhost:8081/data.html" style='display: none;'></iframe>
<script>
	window.onload = function() {
		let targetOrigin = 'http://localhost:8081';
		window.frames[0].postMessage('index.html 的 data!', targetOrigin);
	}
	window.addEventListener('message', function(e) {
		console.log('index.html 接收到的消息:', e.data);
	});
</script>
1
2
3
4
5
6
7
8
9
10
11

创建一个 iframe,使用 iframe 的一个方法 postMessage 可以想 http://localhost:8081/data.html 发送消息,然后监听 message,可以获得其文档发来的消息。

数据端逻辑:

// postMessage/server/data.html 对应 localhost:8081/data.html
<script>
	window.addEventListener('message', function(e) {
		if(e.source != window.parent) {
			return;
		}
		let data = e.data;
		console.log('data.html 接收到的消息:', data);
		parent.postMessage('data.html 的 data!', e.origin);
	});
</script>
1
2
3
4
5
6
7
8
9
10
11

启动服务:yarn postMessage 并打开浏览器访问:

image-20211019191931624

# Websocket

# Node中间件代理(两次跨域)

服务器代理,顾名思义,当你需要有跨域的请求操作时发送请求给后端,让后端帮你代为请求,然后最后将获取的结果发送给你。

假设有这样的一个场景,你的页面需要获取 CNode:Node.js专业中文社区 (opens new window) 论坛上一些数据,如通过 https://cnodejs.org/api/v1/topics,当时因为不同域,所以你可以将请求后端,让其对该请求代为转发。

代码如下:

// serverProxy/server.js
const url = require('url');
const http = require('http');
const https = require('https');

const server = http.createServer((req, res) => {
	const path = url.parse(req.url).path.slice(1);
	if(path === 'topics') {
		https.get('https://cnodejs.org/api/v1/topics', (resp) => {
			let data = "";
			resp.on('data', chunk => {
				data += chunk;
			});
			resp.on('end', () => {
				res.writeHead(200, {
					'Content-Type': 'application/json; charset=utf-8'
				});
				res.end(data);
			});
		})		
	}
}).listen(3000, '127.0.0.1');
console.log('启动服务,监听 127.0.0.1:3000');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

通过代码你可以看出,当你访问 http://127.0.0.1:3000/topics 的时候,服务器收到请求,会代你发送请求 https://cnodejs.org/api/v1/topics 最后将获取到的数据发送给浏览器。

# nginx反向代理

# window.name

window.name(一般在 js 代码里出现)的值不是一个普通的全局变量,而是当前窗口的名字,这里要注意的是每个 iframe 都有包裹它的 window,而这个 window 是 top window 的子窗口,而它自然也有 window.name 的属性,window.name 属性的神奇之处在于 name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。

举个简单的例子:

你在某个页面的控制台输入:

window.name = "Hello World"
window.location = "http://www.baidu.com"
1
2

页面跳转到了百度首页,但是 window.name 却被保存了下来,还是 Hello World,跨域解决方案似乎可以呼之欲出了:

前端逻辑:

// name/client/index.html 对应 localhost:8080/index.html 
<script>
	let data = '';
	const ifr = document.createElement('iframe');
	ifr.src = "http://localhost:8081/data.html";
	ifr.style.display = 'none';
	document.body.appendChild(ifr);
	ifr.onload = function() {
		ifr.onload = function() {
			data = ifr.contentWindow.name;
			console.log('收到数据:', data);
		}
		ifr.src = "http://localhost:8080/proxy.html";
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

数据页面:

// name/server/data.html 对应 localhost:8081/data.html
<script>
	window.name = "data.html 的数据!";
</script>
1
2
3
4

localhost:8080index.html 在请求数据端 localhost:8081/data.html 时,我们可以在该页面新建一个 iframe,该 iframe 的 src 指向数据端地址(利用 iframe 标签的跨域能力),数据端文件设置好 window.name 的值。

但是由于 index.html 页面与该页面 iframe 的 src 如果不同源的话,则无法操作 iframe 里的任何东西,所以就取不到 iframe 的 name 值,所以我们需要在 data.html 加载完后重新换个 src 去指向一个同源的 html 文件,或者设置成 'about:blank;' 都行,这时候我只要在 index.html 相同目录下新建一个 proxy.html 的空页面即可。如果不重新指向 src 的话直接获取的 window.name 的话会报错:

image-20211019191803449

# localtion.hash

在 url 中,http://www.baidu.com#helloworld 的 "#helloworld" 就是 location.hash,改变 hash 值不会导致页面刷新,所以可以利用 hash 值来进行数据的传递,当然数据量是有限的。

假设 localhost:8080 下有文件 index.html 要和 localhost:8081 下的 data.html 传递消息,index.html 首先创建一个隐藏的 iframe,iframe 的 src 指向 localhost:8081/data.html,这时的 hash 值就可以做参数传递。

// hash/client/index.html 对应 localhost:8080/index.html
<script>
	let ifr = document.createElement('iframe');
	ifr.style.display = 'none';
	ifr.src = "http://localhost:8081/data.html#data";
	document.body.appendChild(ifr);
	
	function checkHash() {
		try {
			let data = location.hash ? location.hash.substring(1) : '';
			console.log('获得到的数据是:', data);
		}catch(e) {

		}
	}
	window.addEventListener('hashchange', function(e) {
		console.log('获得的数据是:', location.hash.substring(1));
	});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

data.html 收到消息后通过 parent.location.hash 值来修改 index.html 的 hash 值,从而达到数据传递。

// hash/server/data.html 对应 localhost:8081/data.html
<script>
	switch(location.hash) {
		case "#data":
			callback();
			break;
	}
	function callback() {
		const data = "data.html 的数据"
		try {
			parent.location.hash = data;
		}catch(e) {
			// ie, chrome 下的安全机制无法修改 parent.location.hash
			// 所以要利用一个中间的代理 iframe 
			var ifrproxy = document.createElement('iframe');
			ifrproxy.style.display = 'none';
			ifrproxy.src = 'http://localhost:8080/proxy.html#' + data;     // 该文件在 client 域名的域下
			document.body.appendChild(ifrproxy);
		}
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

由于两个页面不在同一个域下 IE、Chrome 不允许修改 parent.location.hash 的值,所以要借助于 localhost:8080 域名下的一个代理 iframe 的 proxy.html 页面

// hash/client/proxy.html 对应 localhost:8080/proxy.html
<script>
    parent.parent.location.hash = self.location.hash.substring(1);
</script>
1
2
3
4

之后启动服务 yarn hash,即可在 localhost:8080 下观察到:

image-20211019191638289

当然这种方法存在着诸多的缺点:

  1. 数据直接暴露在了 url 中
  2. 数据容量和类型都有限等等

# document.domain

对于主域相同而子域不同的情况下,可以通过设置 document.domain 的办法来解决,具体做法是可以在 http://www.example.com/index.htmlhttp://sub.example.com/data.html 两个文件分别加上 document.domain = "example.com" 然后通过 index.html 文件创建一个 iframe,去控制 iframe 的 window,从而进行交互,当然这种方法只能解决主域相同而二级域名不同的情况,如果你异想天开的把 script.example.com 的 domain 设为 qq.com 显然是没用的,那么如何测试呢?

测试的方式稍微复杂点,需要安装 nginx 做域名映射,如果你电脑没有安装 nginx,请先去安装一下: nginx (opens new window)

前端逻辑:

// domain/client/index.html 对应 sub1.example.com/index.html
<script>
	document.domain = 'example.com';
	let ifr = document.createElement('iframe');
	ifr.src = 'http://sub2.example.com/data.html';
	ifr.style.display = 'none';
	document.body.append(ifr);
	ifr.onload = function() {
		let win = ifr.contentWindow;
		alert(win.data);
	}
</script>
1
2
3
4
5
6
7
8
9
10
11
12

数据端逻辑:

// domain/server/data 对应 sub2.example.com/data.html
<script>
	document.domain = 'example.com';
	window.data = 'data.html 的数据!';
</script>
1
2
3
4
5

打开操作系统下的 hosts 文件:mac 是位于 /etc/hosts 文件,并添加:

127.0.0.1 sub1.example.com
127.0.0.1 sub2.example.com
1
2

之后打开 nginx 的配置文件:/usr/local/etc/nginx/nginx.conf,并在 http 模块里添加,记得输入 nginx 启动 nginx 服务:

/usr/local/etc/nginx/nginx.conf
http {
    // ...
    server {
        listen 80;
        server_name sub1.example.com;
        location / {
            proxy_pass http://127.0.0.1:8080/;
        }
    }
    server {
        listen 80;
        server_name sub2.example.com;
        location / {
            proxy_pass http://127.0.0.1:8081/;
        }
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

相当于是讲 sub1.example.comsub2.example.com 这些域名地址指向本地 127.0.0.1:80,然后用 nginx 做反向代理分别映射到 8080 和 8081 端口。

这样访问 sub1(2).example.com 等于访问 127.0.0.1:8080(1)

启动服务 yarn domain 访问浏览器即可看到效果:

image-20211019192033409

上次更新: 2022/04/15, 05:41:33
×