整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

js提取html富文本字符串某个标签的所有属性

js提取html富文本字符串某个标签的所有属性

HTML中,标签可以包含各种属性,用于定义标签的特性和行为。例如,img标签可以有src属性来指定要显示的图像的URL,a标签可以有href属性来指定链接的目标地址。通过获取标签的属性值,我们可以获取到这些额外的信息,然后根据需要进行处理。

定义方法

/**
 * 提取富文本字符串某个标签的所有属性
 * @param {String} str 要提取的html富文本
 * @param {Object} tagName 要提取的标签名称
 * @param {Object} attrName 要提取的属性名称
 * @returns {Array} list 属性列表
 */
function getStrTagAttribute(str,tagName,attrName){
	let attributeList=[]
	const re=new RegExp(`<${tagName} [^>]*${attrName}=['"]([^'"]+)[^>]*>`,'g')
	str.replace(re, (match, capture)=> {
	  attributeList.push(capture);
	});
	return attributeList;
}

使用方法

域请求:

现代的所有的浏览器都遵守同源策略,所谓的源指的就是一个域名、一个服务器,同源是指两个脚本拥有相同的域名,不同源指的是不同的域名,同源策略指的是,一个源的脚本不能读取或操作其他源的http响应和cookie,也就是出于安全方面的考虑,页面中的JavaScript无法访问其他服务器上的数据,这就是所谓的“同源策略”。
同源是指协议、域名和端口都一致。

不同源限制的内容:

  • Cookie、LocalStorge、IndexedDb等存储性内容;
  • DOM节点;
  • Ajax请求;

跨域请求时,不同域的服务器是返回了数据的,只不过浏览器拦截了响应数据;同时也说明了跨域并不能完全阻止CSRF,因为请求毕竟是发出去了;

CORS(Cross-Origin Response Sharing)跨域资源共享:

通过XHR实现Ajax通信的主要限制,是跨域安全策略;默认情况下,只能访问同一个域中的资源,这种安全策略可以预防某些恶意行为,如:

var xhr=new XMLHttpRequest();
xhr.onload=function(){
console.log(xhr.responseText);
}
xhr.open("GET", "https://www.zeronetwork.cn/study/index.html");
xhr.send(null);

其抛出了CORS policy异常;

XHR2规范了在通过HTTP响应中如何选择合适的CORS(Cross-Origin Response Sharing,跨域资源共享)去跨域访问资源;其定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通;CORS的基本思想是,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从面决定请求或响应是否应该成功;

比如一个简单的使用GET或POST发送的请求,默认情况下它没有自定义的头,但一般会包括一个Origin请求头,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应,如Origin头部示例:

Origin: https://www.zeronetwork.cn

如果服务器认为这个请求可以接受,就在响应的Access-Control-Allow-Origin([??la?])头中回发相同的源信息(如果是公共资源,可以回发”*”),例如:

Access-Control-Allow-Origin: https://www.zeronetwork.cn

如果没有这个响应头,或者有这个响应头但与请求Origin头信息不匹配,浏览器就会驳回请求;反之,浏览器会处理请求;

实现跨域:

IE和标准浏览器已经实现了各自的跨域解决方案;

标准浏览器对CORS的实现:

在标准浏览器中,客户端在使用Ajax跨域请求时,抛出异常,不能访问;如:

var xhr=new XMLHttpRequest();
xhr.onreadystatechange=function(){
if (xhr.readyState==4 && xhr.status==200) {
console.log(xhr.responseText);
}
}
xhr.open("GET","https://www.b.com/normal/example.json");
xhr.send(null);

被请求的服务端需要设置Access-Control-Allow-Origin响应头,以便于浏览器识别它是否为可信源。

例如,在Apache服务器中,在服务器的配置中添加如下设置:

Header set Access-Control-Allow-Origin 'origin-list'

对于Nginx,设置此http头的命令是:

add_header 'Access-Control-Allow-Origin' 'origin-list'

或者使用.htaccess文件配置,如:

<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin "*"
</IfModule>
order allow,deny
allow from all

应用:

xhr.open("GET","https://www.b.com/cors/example.json");

单独为某个后端程序设置响应头,例如b.com/cors.php:

<?php
header("Access-Control-Allow-Origin: *");
echo "跨域访问b.com/cors.php";

无论同源请求还是跨源请求都使用相同的接口,因此对地本地资源,最好使用相对URL,在访问远程资源时再使用绝对URL;这样做能消除歧义,避免出现限制访问头部或本地cookie信息等问题。

IE对CORS的实现:

微软在IE8中引入了XDR(XDomainRequest)对象,其与XHR类似,其可直接用于发起安全的跨域请求,实现安全可靠的跨域通信;

var xdr=new XDomainRequest();
console.log(xdr);

IE11和标准浏览器并不支持;

XDR对象的使用方法与XHR对象非常相似,两者拥有几乎相同的属性和方法,也是调用open()方法,再调用send()方法;但与XHR对象的open()方法不同,XDR对象的open()方法只接收两个参数:请求的类型和URL;如:

var xdr=new XDomainRequest();
console.log(xdr);
// xdr.open("GET", "http://www.c.com/nocors.php");
xdr.open("GET", "example.php");
xdr.onload=function(){
console.log(xdr.responseText);
}
xdr.send();

此时,不管是跨域的还是同源的都不允许访问,抛出“在 Access-Control-Allow-Origin 标头中未找到源”;

XDR对象的安全机制中部分实现了CORS,后端也需要设置 Access-Control-Allow-Origin响应头,如c.com/cors.php:

<?php
header("Access-Control-Allow-Origin: *");
echo "设置了ACAO响应头";

请求端:

xdr.open("GET", "http://www.c.com/cors.php");

XDR对象属性和事件:

请求返回后,会触发onload事件,响应的数据也会保存在responseText属性中,响应的MIME类型保存在contentType属性中,如:

var xdr=new XDomainRequest();
xdr.onload=function(){
console.log(xdr.contentType); // application/json
console.log(xdr.responseText);
}
xdr.open("post","http://www.c.com/cors/example.json");
xdr.send(null);

例如再请求一个同源的contentType.php:

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json");
echo '{"username":"王唯","age":18,"sex":true}';

在接收到响应后,只能访问响应的原始文本,不能确定响应的状态代码(也就是它没有status属性);而且,只要响应有效就会触发onload事件,如果失败就会触发error事件,但除了错误本身之外,没有其他信息可以确定请求是否成功,所以唯一能够确定的就只有请求未成功;

要检测错误,如要指定error事件处理程序,如:

xdr.onerror=function(){
console.log("出现错误");
}

由于导致XDR请求失败的因素很多,因此,最好通过error事件处理程序来捕获该事件,否则,即使请求失败也不会有任何提示。

在请求返回前调用abort()方法可以终止请求,如:

xdr.abort();

与XHR对象一样,XDR也支持timout属性和ontimeout事件,如:

xdr.timeout=1000;
xdr.ontimeout=function(){
console.log("请求超过1秒");
};

onprogress事件:

应该始终定义 xdr.onprogress 事件,即使它是一个空函数,否则 XDomainRequest 对于重复请求,可能不会触发 onload 事件;

xdr.onprogress=function(event){
console.log(event);
};

XDR与XHR的不同之外:

1、必须使用 HTTP 或 HTTPS 协议访问目标 URL:
因为XDR对象依赖于一个HTTP响应头来实现访问控制,所以它要求目标URL符合HTTP或HTTPS 协议,以便于XDR对象检验响应头;
2、只支持GET和POST请求;
3、不能设置自定义请求头信息,也不能访问响应头部信息;
4、只支持text/plain作为请求头Content-Type的取值:在XDR中,不管是GET还是POST请求,Content-Type被限制成“text/plain”,所以服务端不会把请求主体数据解析成键值对,即不能从参数中获取到POST的数据,只能读取流数据,需要其自行解析,如:

var xdr=new XDomainRequest();
xdr.open("POST", "xdrpost.php");
xdr.onload=function(){
console.log(xdr.responseText);
};
var param="username=wangwei&age=18";
xdr.send(param);

xdrpost.php:

<?php
header("Access-Control-Allow-Origin: *");
$content=file_get_contents("php://input");
// echo $content; // username=王 唯&age=18
$arr=explode("&", $content);
foreach ($arr as $value) {
$v=explode("=", $value);
echo "key:$v[0], value:$v[1] \r\n";
}

5、身份验证和cookie不能和请求一起发送,也不会随响应返回;
6、请求的URL必须和被请求的URL采用相同的协议:两者的协议必须统一,要么是HTTP,要么是HTTPS;
7、所有XDR请求都是异步执行的,不能用它来创建同步请求;

Preflighted Requests:

CORS通过一种叫做Prelighted Requests的透明服务器验证机制支持开发人员使用自定义的头部、GET或POST之外的请求方式,以及不同类型的主体内容;
在使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求,这种请求使用OPTIONS方式,发送下列头部:

  • Origin:与简单的请求相同;
  • Access-Control-Request-Method:请求自身使用的方法;
  • Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔;

以下是一个带有自定义头部customHeader,并使用POST方法发送的数据,如:

  • Origin: http://www.zeronetwork.cn
  • Access-Control-Request-Method: POST
  • Access-Control-Request-Headers: customHeader

发送这个请求后,服务器端可以决定是否允许这种类型的请求,其通过在响应中发送如下头部与浏览器进行沟通:

  • Access-Control-Allow-Origin:与简单的请求相同;
  • Access-Control-Allow-Methods:允许的方法,多个方法以逗号分隔;
  • Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔;
  • Access-Control-Max-Age:应该将这个Preflight请求缓存多长时间(以秒表示)

例如,允许任何源、POST请求方式、自定义头customHeader以及请求的缓存时间:

  • Access-Control-Allow-Origin: https://www.zeronetwork.cn
  • Access-Control-Allow-Methods: POST
  • Access-Control-Allow-Headers: customHeader
  • Access-Control-Max-Age: 1728000

如:

var xhr=new XMLHttpRequest();
xhr.onload=function(){
console.log(xhr.responseText);
}
xhr.open("OPTIONS","https://www.b.com/flighted.php",true);
xhr.setRequestHeader("customHeader", "customValue");
xhr.send(null);

后端flighted.php:

<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: *");
header("Access-Control-Allow-Methods: *");
header("Access-Control-Max-Age: 1728000");
echo "有Headers、Methods头信息";

Preflight请求结束后,结果将按照响应中指定的时间缓存起来;

带凭据的请求:

默认情况下,跨域请求不提供凭据(cookie、HTTP认证及客户端SSL证明等);
通过将XHR对象的withCredentials属性设置为true,可以指定某个跨域请求应该发送凭据(授权信息);如:

xhr.withCredentials=true;
xhr.send(null);

当使用带有凭据的请求时,不能把Access-Control-Allow-Origin设为*,并且Access-Control-Allow-Origin只能设置一个域,不能是多个,否则会抛出异常;

后端c.com/credentials.php:

header("Access-Control-Allow-Origin: http://www.a.com");
echo "c.com/example.php,已经设置了ACAO";

如果服务端接受带凭据的请求,必须设置Access-Control-Allow-Credentials: true响应头;

如后端c.com/ credentials.php:

header("Access-Control-Allow-Origin: http://www.a.com");
header("Access-Control-Allow-Credentials: true");
echo "设置了Origin,也设置了Credentials";
echo json_encode($_COOKIE);

如果在同源下配置withCredentials,无论配置true还是false,效果都会相同,且会一直提供凭据信息;另外,同时还可以发送自定义请求头,如后端credentials.php:

<?php
header("Access-Control-Allow-Origin: http://www.a.com");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Allow-Headers: customHeader");
echo "设置了Origin,也设置了Credentials";
echo json_encode($_COOKIE);

服务端还可以在Preflight响应中发送这个HTTP头部,但不能把Access-Control-Allow-Headers设为*;

跨浏览器的CORS:

即使浏览器对CORS的支持程度并不一致,但所有浏览器都支持简单的请求(非Preflight和不带凭据的请求),因此有必要实现一个跨浏览器的方案:检测XHR是否支持CORS的最简单方式,就是检查是否存在withCredentials属性,再结合检测XDomainRequest对象是否存在,就可以兼顾所有浏览器了,如:

function createCORSRequest(method, url, withCredentials){
var xhr=new XMLHttpRequest();
if ("withCredentials" in xhr) {
xhr.open(method, url);
xhr.withCredentials=withCredentials;
}else if(typeof XDomainRequest !="undefined"){
xhr=new XDomainRequest();
xhr.open(method, url);
}else{
xhr=null;
}
return xhr;
}
var request=createCORSRequest("GET", "https://www.b.com/credentials.php", true);
if(request){
request.onload=function(){
console.log(request.responseText);
};
request.send(null);
}

示例:使用HEAD和CORS请求链接的详细信息,如:

var supportsCORS=(new XMLHttpRequest).withCredentials !=undefined;
var links=document.getElementsByTagName("a");
for(var i=0; i<links.length; i++){
var link=links[i];
if(!link.href) continue;
if(link.title) continue;
if(link.host !==location.host || link.protocol !=location.protocol){
link.title="站外链接";
if(!supportsCORS) continue;
}
if(link.addEventListener)
link.addEventListener("mouseover", mouseoverHandler, false);
else
link.attachEvent("onmouseover", mouseoverHandler);
}
function mouseoverHandler(e){
var link=e.target || e.srcElement;
var url=link.href;
var xhr=new XMLHttpRequest();
xhr.open("HEAD", url);
xhr.onreadystatechange=function(){
if(xhr.readyState !==4) return;
if(xhr.status==200){
var type=xhr.getResponseHeader("Content-Type");
var size=xhr.getResponseHeader("Content-Length");
var date=xhr.getResponseHeader("Last-Modified");
link.title="类型:" + type + "\n" +
"大小:" + size + "\n" +
"时间:" + date;
}else{
if(!link.title)
link.title="获取不到详细信息:\n" +
xhr.status + " " + xhr.statusText;
}
};
xhr.send(null);
if(link.removeEventListener)
link.removeEventListener("mouseover", mouseoverHandler, false);
else
link.detachEvent("onmouseover", mouseoverHandler);
}

HTML:

<a href="https://www.zeronetwork.cn/edu/">edu</a>
<a href="https://www.zeronetwork.cn/study/">study</a>
<a href="http://www.a.com/ demo.html">a.com</a>
<a>no href</a>
<a href="https://www.apple.com" title="baidu">apple.com</a>
<a href="https://cn.bing.com">bing</a>

其它跨域技术:

虽然CORS技术已经无处不在,但在CORS出现之前,就已经存在一些跨域的技术了,虽然这些技术应用起来有些麻烦,但它们绝大部分不需要修改服务器端代码,所以直到现在这些技术仍然被广泛使用;

后端代理方式:

这种方式可以解决所有跨域问题,也就是将本域的后端程序作为代理,每次对其它域的请求都转交给该代理程序,其通过模拟http请求去访问其它域,再将返回的结果返回给前端,这样做的好处是,无论访问的是文档、还是JS文件都可以实现跨域;
例如,b.com/data.php响应JSON字符串:

<?php
$json_str='{"name":"wagwei","sex":true,"age":18}';
echo $json_str;

a.com/getdata.php服务端获取b.com/data.php响应:

<?php
// 创建cURL资源
$ch=curl_init();
// 设置URL和相应的选项
curl_setopt($ch, CURLOPT_URL, "http://www.b.com/data.php");
curl_setopt($ch, CURLOPT_HEADER, 0);
// 抓取URL并把它传递给浏览器
curl_exec($ch);
// 关闭cURL资源,并释放系统资源
curl_close($ch);

a.com/data.html使用Ajax请求同源的getdata.php:

var xhr=new XMLHttpRequest();
xhr.open("GET", "getdata.php");
xhr.onreadystatechange=function(){
if(xhr.readyState==4 && xhr.status==200){
console.log(xhr.responseText);
}
};
xhr.send(null);

基于iframe实现跨域:

基于iframe实现的跨域要求两个域属于同一个根域,如:www.a.com和b.a.com其使用同一协议(例如都是 http)和同一端口(例如都是80),此时在两个页面中同时设置document.domain为同一个主域,就实现了同域,从而可以实现通信;如b.a.com中的iframe.html:

<h1>iframe</h1>
<img src="images/hu.png" />
<script>
document.domain="a.com";
function show(msg){
alert("收到的:" + msg);
}
if(parent.parentFun){
parent.parentFun();
}
</script>

www.a.com主页面为:

<script>
function parentFun(){
alert("parentFun");
}
</script>
<iframe src="http://b.a.com/iframe.html" id="myframe"></iframe>
<script>
var myframe=document.getElementById("myframe");
document.domain="a.com";
myframe.onload=function(){
var win=myframe.contentWindow;
console.log(win);
win.show("零点程序员");
var doc=myframe.contentDocument;
console.log(doc);
}
</script>

使用window.name和iframe进行跨域:

window的name属性返回的是该window的名称,它的值有个特点:在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的name值(2MB),即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name,每个页面对window.name都有读写的权限;

正因为window的name属性的这个特征,所以可以使用window.name来进行跨域;例如a.html:

<h1>a.html</h1>
<script>
window.name="页面a设置的name值";
setTimeout(function(){
window.location="b.html";
},3000); // 3秒后在当前window中载入新的页面
</script>

b.html:

<h1>b.html</h1>
<script>
alert(window.name); // 页面a设置的name值
</script>

跨域:例如,有一个a.com/a.html页面,需要通过js来获取位于另一个不同域上的页面,如:b.com/b.html里的数据:

<script>
window.name="b.com/b.html中的数据";
</script>

如果b.html不跳转,其他页也可以获取数据,可以采用iframe;
如a.com/a.html:

<h1>a.html</h1>
<iframe id="iframe" src="http://www.b.com/b.html" style="display:none"></iframe>
<script>
var iframe=document.getElementById("iframe");
// iframe在一开始载入b.com/b.html会执行此函数
iframe.onload=function(){
// 当iframe.src为b.html时触发,此时iframe和当前页面已经同源,可以访问
iframe.onload=function(){
var data=iframe.contentWindow.name;
alert(data);
};
// 这里的b.html为随便一个页面,只要与当前页面同源就可以,
// 目录是让iframe与当前页面同源
iframe.src="b.html";
}
</script>

使用location.hash+iframe跨域:

假设a.com/a.html要向b.com/b.html传递信息;如a.com/a.html:

<h1>a.html</h1>
<script>
function checkHash(){
try{
var data=location.hash ? location.hash.substring(1) : '';
console.log("收到的数据是:" + data);
}catch(e){}
}
setInterval(checkHash, 5000);
window.onload=function(){
var iframe=document.createElement("iframe");
// iframe.style.display="none";
iframe.src="http://www.b.com/b.html#param"; // 传递的location.hash
document.body.appendChild(iframe);
};
</script>

b.com/b.html:

<h1>b.html</h1>
<script>
function checkHash(){
var data="";
// 模拟一个简单的参数处理操作
switch(location.hash){
case "#param":
data="somedata";
break;
case "#other":
// ...
break;
default:
break;
}
data && callBack("#" + data);
}
function callBack(hash){
// ie、chrome的安全机制无法修改parent.location.hash
//所以要利用一个中间的www.csdnblogs.com域下的代理iframe
var proxy=document.createElement("iframe");
proxy.style.display="none";
proxy.src="http://www.a.com/c.html" + hash;
// 注意该文件在a.com中
document.body.appendChild(proxy);
}
window.onload=checkHash;
</script>

a.com/c.html:

<script>
//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash=self.location.hash.substring(1);
</script>

图像Ping:

使用<img>标签,也可以动态创建图像,使用它们的onload和onerror事件处理程序来确定是否接收到了响应;例如:

var img=new Image();
img.onload=img.onerror=function(){
console.log("Done");
};
img.src="https://www.zeronetwork.cn/study/pingimg.php?name=wangwei";
pingimg.php:
if($_GET['name']){
echo $_GET['name'];
}

图像Ping有两个主要的缺点,一是只能发送GET请求,二是无法访问服务器的响应文本,因此,图像Ping只能用于浏览器与服务器间的单向通信;提交的数据是通过查询字符串形式发送的,但响应可以是任意内容,但通常是像素图或204响应;
通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,它能知道响应是什么时候接收到的,此时可以实现一些自身的逻辑;
示例:图像Ping最常用于跟踪用户点击页面或动态广告曝光次数,如:

<input type="button" id="btn" value="图像Ping请求" />
<div id="result"></div>
<script>
var increment=(function(){
var counter=0;
return function(){
return ++counter;
};
})();
var btn=document.getElementById("btn");
btn.addEventListener("click", function(event){
var sum=increment();
var result=document.getElementById("result");
var img=result.getElementsByTagName("img")[0];
if(!img)
img=new Image();
img.onload=img.onerror=function(){
result.appendChild(img);
var oSpan=document.getElementById("sum");
if(!oSpan){
oSpan=document.createElement("span");
oSpan.id="sum";
}
oSpan.innerHTML="发送请求的次数:" + sum;
result.appendChild(oSpan);
};
if(sum % 2)
img.src="https://www.zeronetwork.cn/study/images/ad1.jpg?sum=" + sum;
else
img.src="https://www.zeronetwork.cn/study/images/ad2.jpg?sum="+sum;
</script>

基于<script>标签实现跨域:

在某些HTML元素中,可以通过它的src属性跨域请求内容,例如img、iframe等,也就是没有跨域的限制;同样,script也可以,也就是利用script来执行跨域的javascript代码,从而实现前端跨域请求数据的目的;例如:

<script>
var script=document.createElement('script');
script.src="http://www.b.com/scripts/demo.js";
document.body.appendChild(script);
script.onload=function(){
show("从a.com传过去的数据");
}
</script>

b.com/scripts/demo.js:

function show(msg){
alert("收到的数据:" + msg);
}
alert("www.b.com/script/demo.js");

JSONP:
JSONP是JSON with padding(填充式JSON或参数式JSON)的简写,是应用JSON的新方法,其利用<script>标签没有跨域限制的特点,可以得到从其他源动态产生的JSON数据,但JSONP请求一定需要对方服务器的支持才可以;
JSONP看起来与JSON差不多,是被包含在函数调用中的JSON,形如:callback({“name”: “wangwei”});
JSONP由两部分组成:回调函数callback和json数据;回调函数是当响应到来时应该在页面中调用的函数,其名字一般是在请求中指定的,需要在本地实现;而数据就是传入回调函数中的JSON数据;整个JSONP就是一个标准的JavaScript语句;
JSONP后端服务:被请求服务端程序必须提供JSONP的服务,一般情况下,其会返回标准的JSONP;
例如,被请求服务端b.com/jsonptest.php返回JSONP的简单形式:

<?php
echo 'alert({"name":"王唯","age":18})';

在请求端中使用<script>引入该文件,如:

<script src="http://www.b.com/jsonptest.php"></script>

请求端和服务端共同约定使用自定义函数,而不是内置函数;例如b.com/jsonptest.php:

echo 'handlerJSONP({"name":"王唯","age":18})';

请求端实现handlerJSONP()函数,并使用<script>引入b.com/jsonptest.php文件,如:

<script>
function handlerJSONP(response){
alert("姓名:" + response.name + "\n" + "年龄:" + response.age);
}
</script>
<script src="http://www.b.com/jsonptest.php"></script>

一般来说,后端程序通过查询字符串允许客户端指定一个函数名,然后用这个函数名去填充响应,例如:

<script>
function handlerJSONP(response){
alert(response);
}
</script>
<script src="http://www.b.com/jsonptest.php?callback=handlerJSONP"></script>

b.com/jsonptest.php:

header('Content-type: application/json');
//获取回调函数名
$callback=htmlspecialchars($_REQUEST['callback']);
//json数据
$json_data='["王唯","静静","娟子","大国"]';
//输出jsonp格式的数据
echo $callback . "(" . $json_data . ")";

JSONP是通过动态<script>元素来使用的,例如:

// ...
var script=document.createElement("script");
script.src="http://www.b.com/jsonptest.php?callback=handlerJSONP";
document.body.insertBefore(script, document.body.firstChild);

请求端与后端就该函数名有个约定,使用随机名,如:hander1234();而查询字符串的名字一般约定使用”json”或”callback”,当然也可以是其他任意的名称;

JSONP之所以在开发人员中极为流行,主要原因是它非常简单易用;与图像Ping相比,它的优点在于能够直接获取响应文本,支持在浏览器与服务器之间双向通信;

在某些时候,处理完JSONP数据后,动态创建的<script>就可以删除了;

function clickHandler(e){
var script=document.createElement("script");
script.id="jsonp_script";
script.src="http://www.a.com/jsonp.php?callback=handlerResponse";
document.body.insertBefore(script, document.body.firstChild);
}
function handlerResponse(response){
console.log(response);
var script=document.getElementById("jsonp_script");
script.parentNode.removeChild(script);
}
var btn=document.getElementById("btn");
btn.addEventListener("click", clickHandler);

定义将被脚本执行的回调函数

getJSONP[cbnum]=function(response){
try{
callback(response);
}finally{
delete getJSONP[cbnum];
script.parentNode.removeChild(script);
}
};
script.src=url;
document.body.appendChild(script);
}
getJSONP.counter=0; // 用于创建唯一回调函数名称的计数器
function handlerResponse(response){
console.log(response);
}
getJSONP("http://www.a.com/jsonp.php", handlerResponse);

示例:某接口的应用:

<input type="button" id="btn" value="jsonp请求">
<script>
var btn=document.getElementById("btn");
btn.addEventListener("click", function(){
var script=document.createElement("script");
script.id="jsonscript";
script.src="https://suggest.taobao.com/sug?code=utf-8&q="+ encodeURIComponent("衣服") +"&callback=jsonpCallback";
document.body.appendChild(script);
});
function jsonpCallback(response){
console.log(response);
var script=document.getElementById("jsonscript");
script.parentNode.removeChild(script);
}
</script>

JSONP的不足:

  • 首先,JSONP是从其他域中加载代码执行,如果其他域不安全,很可能会在响应中夹带一些恶意代码;因此在使用不是你自己运维的Web服务时,一定得保证它安全可靠;
  • 其次,只能进行GET请求;
  • 最后,要确定JSONP请求是否失败比较麻烦,虽然HTML5给<script>元素新增了一个onerror事件处理程序,但目前还没有得到任何浏览器支持;

JSONP和AJAX对比:
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式;但AJAX属于同源策略,采用CORS方案跨域,而JSONP属于非同源策略进行跨域请求;

window.postMessage()方法:

window.postMessage()方法可以安全地实现跨域通信;其是HTML5规范提供的一种受控机制来规避跨域安全限制的方法,采用异步的方式进行有限的通信,既可以用于同域传递消息,也可以用于跨域传递消息;

其应用的场景是:

  • 页面和其打开的新窗口的数据传递;
  • 多窗口之间消息传递;
  • 页面与嵌套的iframe消息传递;

一个窗口可以获得对另一个窗口的引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames,然后在窗口上调用targetWindow.postMessage()方法分发一个MessageEvent消息;接收消息的窗口触发onmessage事件,并接收分发过来的消息;

例如a.com/post.html,获取iframe:

<h1>a.com/post.html</h1>
<iframe id="iframe" src="http://www.b.com/message.html"></iframe>
<script>
var iframe=document.getElementById("iframe");
var win=iframe.contentWindow;
console.log(win);
</script>

b.com/ message.html:

<h1>b.com/message.html</h1>

语法:otherWindow.postMessage(message, targetOrigin, [transfer]);

  • otherWindow为其他窗口的一个引用;
  • message参数:将要发送到其他window的数据;它将会被结构化克隆算法序列化,即可以不受限制的将数据对象安全的传送给目标窗口而无需自己序列化;
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI;
  • transfer参数:可选,是一串和message同时传递的Transferable对象,这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权;

例如:a.com/post.html:

// ...
win.postMessage("来自a.com/post.html的消息","*");
b.com/message.html:
window.addEventListener("message", function(event){
console.log(event); // MessageEvent
});

需要延迟执行postMessage()方法,延迟的方式有多种,使用setTimeout()、iframe的onload事件、发送者的onload事件或者使用按钮的click事件处理程序,如a.com/post.html:

setTimeout(function(){
win.postMessage("来自a.com/post.html的消息","*");
},500);
// 或:
var iframe=document.getElementsByTagName("iframe")[0];
iframe.onload=function() {
var iframe=document.getElementById("iframe");
var win=iframe.contentWindow;
win.postMessage("来自a.com/post.html的消息","*");
}
// 或:
var btnSend=document.getElementById("btnSend");
btnSend.addEventListener("click", function(){
var iframe=document.getElementsByTagName("iframe")[0];
var win=iframe.contentWindow;
win.postMessage("来自a.com/post.html的消息","*");
});

使用postMessage()将数据发送到其他窗口时,最好指定明确的targetOrigin,而不是*,原因是恶意网站可以在用户不知情的情况下更改窗口的位置,甚至可以拦截所发送的数据;

win.postMessage("来自a.com/post.html的消息","http://www.b.com"); // 或
win.postMessage("来自a.com/post.html的消息","http://www.b.com/"); // 或
win.postMessage("来自a.com/post.html的消息","http://www.b.com/error.html");

在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送;这个机制用来控制消息可以发送到哪些窗口。

MessageEvent接口:
代表一段被目标对象接收的消息;
属性:

  • data属性:其保存着由发送者发送的字符串数据;
  • lastEventId属性:表示事件的唯一ID;
  • origin属性:返回一个表示消息发送者来源;
  • ports属性:MessagePort对象数组,表示消息正通过特定通道(数据通道)发送的相关端口;
  • source属性:是一个MessageEventSource对象,代表消息发送者;
window.addEventListener("message", function(event){
console.log(event);
console.log(event.data); // 来自a.com/post.html的消息
console.log(event.lastEventId); // 空
console.log(event.origin); // http://www.a.com
console.log(event.ports); // []
console.log(event.source); // window http://www.a.com/post.html
});

如果不希望接收message,就不要实现message事件;如果希望从其他网站接收message,最好使用origin或source属性验证消息发送者的身份;如:

window.addEventListener("message", function(event){
if(event.origin !="http://www.a.com")
return;
console.log(event.data);
});

虽然postMessage()是单向的通信,但可以使用source属性在具有不同origin的两个窗口之间建立双向通信;
例如a.com/message.html:

<iframe id="iframe" src="http://www.b.com/message.html"></iframe>
<input type="button" id="btn" value="send" />
<script>
var btn=document.getElementById("btn");
btn.addEventListener("click", function(){
var iframe=document.getElementById("iframe");
var win=iframe.contentWindow;
win.postMessage("来自a.com/post.html的消息","http://www.b.com/");
});
window.addEventListener("message", function(event){
console.log("a.com收到:" + event.data);
});
</script>

b.com/message.html:

window.addEventListener("message", function(event){
if(event.origin !="http://www.a.com")
return;
console.log("b.com收到:" + event.data);
event.source.postMessage("b.com/message回发的消息", event.origin);
});

与使用open()方法打开的窗口通信:
在向使用open()方法打开的窗口发送消息时,需要延迟执行,如:

btnOpen.addEventListener("click", function(){
var win=window.open("http://www.b.com/message.html","_blank","width:600px,height:400px");
setTimeout(function(){
win.postMessage("从a.com中打开","http://www.b.com");
},1000);
});
window.addEventListener("message", function(event){
console.log("a.com收到:" + event.data);
});

或者反向发送消息,例如a.com/post.html:

var btnOpen=document.getElementById("btnOpen");
btnOpen.addEventListener("click", function(){
var win=window.open("http://www.b.com/message.html","_blank","width:600px,height:400px");
});
window.addEventListener("message", function(event){
console.log("a.com收到:" + event.data);
});

b.com/message.html:

<script>
window.onload=function(){
var targetWindow=window.opener;
console.log(targetWindow);
targetWindow.postMessage("message.html is ready","http://www.a.com");
}
window.addEventListener("message", function(event){
console.log("b.com收到:");
console.log(event.data);
event.source.postMessage("b.com/message.html回发的消息", event.origin);
});
</script>

postMessage()还可以发送结构化数据,该数据将会自动被(结构化克隆算法)序列化,如:

var btn=document.getElementById("btn");
btn.addEventListener("click", function(){
var iframe=document.getElementById("iframe");
var win=iframe.contentWindow;
var person={
name:"wanwei",
sex: true,
age: 18,
friends: ["jingjing","daguo"],
// smoking: function(){console.log(this.name)}, // 异常 could not be cloned.
other1: undefined,
other2: null
}
win.postMessage(person,"http://www.b.com/");

b.com/message.html:

window.addEventListener("message", function(event){
console.log("b.com收到:");
console.log(event.data);
});

因为是跨域,所以即使取得外源窗口的window对象,也无法操作对方的DOM,但是通过接收到的消息,自行构建DOM;

例如a.com/post.html:

var btn=document.getElementById("btn");
btn.addEventListener("click", function(){
var iframe=document.getElementById("iframe");
var win=iframe.contentWindow;
var person={
name:"wanwei",
sex: true,
age: 18,
friends: ["jingjing","daguo"],
}
win.postMessage(person,"http://www.b.com/");
});
window.addEventListener("message", function(event){
if(event.origin=="http://www.b.com"){
if(event.data.state){
document.getElementById("btn").setAttribute("disabled",true);
// ...
}
}
});
b.com/message.html:
window.addEventListener("message", function(event){
if(event.origin=="http://www.a.com"){
var person=event.data;
var h1=document.createElement("h1");
h1.innerText=person.name + "信息";
document.body.appendChild(h1);
var p=document.createElement("p");
p.innerHTML="性别:" + (person.sex ? "男" : "女");
p.innerHTML +="<br/>年龄:" + person.age;
p.innerHTML +="<br/>朋友:" + person.friends.join(",");
document.body.appendChild(p);
event.source.postMessage({state:1}, event.origin);
}
});

示例,后台管理的应用,main.html:

<style>
*{margin:0; padding:0;}
ul,li{list-style: none;}
.top{width:100%; height:100px; background-color:yellowgreen;}
.left{width:20%; height: 100%; float: left; background-color:yellow;}
.left ul li{padding:10px;}
.left ul li a{color:#000; text-decoration: none;}
iframe{width:80%; float: right; border:none;}
.bg{
position: fixed; left:0; top: 0; display: none;
width:100%; height:100%; background-color: rgba(0, 0, 0, .5);
}
.showBg{display: block !important}
.confirm{
position:fixed; width:400px; height:200px; z-index: 2;
left: 50%; top: 50%; transform: translate(-50%, -50%);
padding: 20px; text-align: center; background-color: #FFF;
}
</style>
<div class="top"></div>
<div class="left">
<ul>
<li><a href="console.html" target="iframe">控制台</a></li>
<li><a href="content.html" onclick="sendMessage()" target="iframe">系统设置</a></li>
</ul>
</div>
<iframe name="iframe" src="console.html"></iframe>
<div class="bg">
<div class="confirm">
<p>是否确认保存?</p>
<p><input type="button" id="btnCancel" value="取消" />
<input type="button" id="btnSave" value="保存" /></p>
</div>
</div>
<script>
var iframe=null;
window.onload=function(){
var leftDiv=document.getElementsByClassName("left")[0];
iframe=document.getElementsByTagName("iframe")[0];
iframe.style.height=leftDiv.style.height=(document.documentElement.scrollHeight - 100) + "px";
}
function sendMessage(){
iframe.onload=function(){
iframe.contentWindow.postMessage({
method: 'dataId',
data: {dataId: 1}
}, "*");
}
}
window.addEventListener("message", function(event){
if(event.data.method=="showBg"){
document.getElementsByClassName("bg")[0].classList.add("showBg");
}
});
var btnCancel=document.getElementById("btnCancel");
btnCancel.addEventListener("click", function(){
if(iframe){
iframe.contentWindow.postMessage({
method: "cancel"
}, "*");
document.getElementsByClassName("bg")[0].classList.remove("showBg");
}
});
var btnSave=document.getElementById("btnSave");
btnSave.addEventListener("click", function(){
if(iframe){
iframe.contentWindow.postMessage({
method: "save"
}, "*");
document.getElementsByClassName("bg")[0].classList.remove("showBg");
}
});
</script>

console.html:

<h1>控制台</h1>

content.html:

<style>
.container{width:60%; margin: 0 auto;}
</style>
<div class="container">
<p>设置1:<input type="text" /></p>
<p>设置2:<input type="text" /></p>
<p>设置3:<input type="text" /></p>
<p>设置4:<input type="text" /></p>
<p><input type="button" id="btnConfirm" value="保存" /></p>
</div>
<script>
window.addEventListener("message", function(event){
if(event.data.method=="dataId"){
console.log(event.data.data.dataId);
}else if(event.data.method=="cancel"){
console.log("取消操作");
}else if(event.data.method=="save"){
console.log("保存成功");
}
});
var btnConfirm=document.getElementById("btnConfirm");
btnConfirm.addEventListener("click", function(){
window.parent.postMessage({
method: "showBg"
}, "*");
});
</script>

示例,修改信息:

content.html:
<div class="container">
<h1>用户信息</h1>
<p>ID:001</p>
<p>姓名:<span id="usernameSpan">王唯</span>
<input type="button" id="editInfo" value="修改个人信息" /> </p>
<p>单位:<span id="jobSpan">零点网络</span>
<input type="button" id="editJob" value="修改工作信息" /></p>
<p>地址:<input type="text" id="address" value="北京市东城区" /></p>
<p>电话:<input type="text" id="tel" value="13888888888" /></p>
<p><input type="button" id="btnConfirm" value="保存" /></p>
</div>
<script>
window.addEventListener("message", function(event){
if(event.data.method=="dataId"){
console.log(event.data.data.dataId);
}else if(event.data.method=="cancel"){
console.log("取消操作");
}else if(event.data.method=="save"){
document.getElementById("usernameSpan").innerText=event.data.data.username;
console.log("保存成功");
}
});
var btnConfirm=document.getElementById("btnConfirm");
btnConfirm.addEventListener("click", function(){
window.parent.postMessage({
method: "showBg"
}, "*");
});
var editInfo=document.getElementById("editInfo");
editInfo.addEventListener("click", function(){
var win=window.open("editInfo.html", "_blank","width:200px,height:500px");
setTimeout(function(){
win.postMessage({
method: "info",
dataId: 2
});
},500);
});
</script>

editInfo.html:

<form>
<input type="hidden" id="ID" name="ID" />
<p>姓名:<input type="text" id="username" name="username" /></p>
<p>性别:<input type="radio" id="male" name="sex" value="1" />男
<input type="radio" id="female" name="sex" value="0" />女</p>
<p>年龄:<input type="text" id="age" name="age" /></p>
<p><input type="button" id="btnCancel" value="取消" />
<input type="button" id="btnSave" value="保存" /></p>
</form>
<script>
window.addEventListener("message", function(event){
if(event.data.method=="info"){
// Ajax请求,从数据库中取出这条记录
var id=event.data.dataId;
var xhr=new XMLHttpRequest();
xhr.open("GET", "getInfo.php?action=showInfo&ID=" + id);
xhr.onreadystatechange=function(){
if(xhr.readyState==4 && xhr.status==200){
// console.log(xhr.response);
var person=xhr.response;
document.forms[0].elements['ID'].value=person.ID;
document.forms[0].elements['username'].value=person.username;
document.forms[0].elements['sex'].value=person.sex;
document.forms[0].elements['age'].value=person.age;
xhr=null;
}
};
xhr.responseType="json";
xhr.send(null);
}
});
var btnCancel=document.getElementById("btnCancel");
btnCancel.addEventListener("click", function(){
window.opener.postMessage({method: "cancel"}, "*");
window.close();
});
var btnSave=document.getElementById("btnSave");
btnSave.addEventListener("click", function(){
// 保存到数据库
var data=new FormData(document.forms[0]);
data.append("action", "update");
var xhr=new XMLHttpRequest();
xhr.open("POST", "getInfo.php");
xhr.onreadystatechange=function(){
if(xhr.readyState==4 && xhr.status==200){
var data=JSON.parse(xhr.responseText);
if(data.status){
var person={
ID: document.forms[0].elements['ID'].value,
username: document.forms[0].elements['username'].value,
sex: document.forms[0].elements['sex'].value,
age: document.forms[0].elements['age'].value
};
window.opener.postMessage({
method: "save",
data: person
});
}
xhr=null;
}
};
xhr.send(data);
window.close();
})
</script>

editInfo.php:

<?php
require_once "conn.php";
if(isset($_REQUEST['action'])){
if($_REQUEST['action']=='showInfo'){
$ID=intval($_GET['ID']);
$sql="select ID,username, sex, age from users where ID=$ID";
$result=$conn->query($sql);
$row=mysqli_fetch_array($result);
echo json_encode($row);
}elseif($_REQUEST['action']=='update'){
$ID=intval($_POST['ID']);
$username=$_POST['username'];
$sex=$_POST['sex'];
$age=$_POST['age'];
$sql="update users set username='$username', sex='$sex', age='$age' where ID=$ID";
$result=$conn->query($sql);
if($result){
echo '{"status": 1}';
}else{
echo '{"status": 0}';
}
}

超链接打开的窗口也可使用postMessage()方法进行通信,但需要设置a标签的target属性为自定义值,如:

<p><a href="http://www.b.com/content.html" target="mywin">零点程序员</a></p>
b.com/content.html:
console.log(window.opener); // Window or global
console.log(window.name); // "mywin"

发送消息,b.com/content.html:

if(window.opener){
console.log(window.opener);
window.opener.postMessage("我已经打开了","*");
}

主页面:

何保持页面样式基本不变的前提下将HTML页面导出为PDF,下面提供一些示例代码,纯属个人原创,如对你有帮助请记得加关注、加收藏、点赞、转发、分享~谢谢~~

  • 基本思路:保持页面样式基本不变,使用 `html2canvas` 将页面转换为图片,然后再通过 `jspdf` 将图片分页导出为PDF文件(中间会遇到图片或文字等内容在分页处被切割开的问题,如何解决了?详见末尾干货)
  • 上基础代码:下面为项目中实际代码截取
<div>
    <!-- 要打印的内容区 -->
    <div ref="contentRef">
        <div class="print-item print-out-flow">这是脱离文档流的内容区域</div>
        <div class="print-item">这是一行内容,也是最小叶子元素内容</div>
    </div>
    <!-- 打印内容容器 -->
    <div ref="printContainerRef" class="print-container"></div>
</div>
/**
  * 1.使用一个隐藏div装载有滚动条的div.innerHTML
  * 2.隐藏div使用position: absolute, z-index: -999, left: -9999px, width: 900px 控制让用户无感知
  * 3.根据需要覆写隐藏div内html样式(例如textarea多行显示有问题, 可以新增一个隐藏的div
  *    包裹textarea的绑定值, 然后在打印样式中覆写样式, 隐藏textarea并显示对应div)
  */
handleExport() {
   // 下面是VUE组件内获取DOM元素代码,将内容放置到打印区(定义的隐藏DIV)中
    const contentRef=this.$refs.contentRef as HTMLElement;
    const printContainerRef=this.$refs.printContainerRef as HTMLElement;
    // 打印区的需额外处理绝对定位值, 调整使得第一个元素的.top值为0, 以便于页面计算
    printContainerRef.innerHTML=contentRef.innerHTML;	
    
    // 所有叶子div元素加上 print-item 样式名, 脱离文档流的额外添加 print-out-flow
    handlePrintItem(printContainerRef);  // 解决多页内容可能被切割问题
    
    html2canvas(printContainerRef, {allowTaint: false, useCORS: true}).then((canvas: any)=> {
      const contentHeight=canvas.height;
      const contentWidth=canvas.width;
      // pdf每页显示的内容高度
      const pageHeight=contentWidth / 595.28 * 841.89;
      // 未生成pdf的页面高度
      let offsetHeight=contentHeight;
      // 页面偏移值
      let position=0;
      // a4纸的尺寸[595.28, 841.89], canvas图片按a4纸大小缩放后的宽高
      const imgWidth=595.28;
      const imgHeight=595.28 / contentWidth * contentHeight;

      const dataURL=canvas.toDataURL('image/jpeg', 1.0);
      const doc=new jsPDF('p', 'pt', 'a4');

      if (offsetHeight < pageHeight) {
        doc.addImage(dataURL, 'JPEG', 0, 0, imgWidth, imgHeight);
      } else {
        while (offsetHeight > 0) {
          doc.addImage(dataURL, 'JPEG', 0, position, imgWidth, imgHeight);
          offsetHeight -=pageHeight;
          position -=841.89;

          if (offsetHeight > 0) {
            doc.addPage();
          }
        }
      }

      doc.save(this.generateReportFileName());
      printContainerRef.innerHTML='';
    });
}

上干货代码:上面分页导出PDF可能网上能看到类型代码,但绝对找不到下面的代码,纯手搓解决分页元素被切开问题(思路:获取自身定位,如自己刚好在被分页处,则加上一定的margin-top值将内容向下移)

/** 
 * 处理打印元素项, 修复分页后被切割的元素
 * @param printContainerRef 打印内容div容器
 * @param itemClassName 打印最小元素标识类名
 * @param outFlowClassName 脱离文档流的元素标识类名
 */
export function handlePrintItem(
  printContainerRef: HTMLElement,
  itemClassName: string='print-item',
  outFlowClassName: string='print-out-flow'
): void {
  const rootClientRect=printContainerRef.getBoundingClientRect();
  // 初始化页面相关数据
  const totalHeight=rootClientRect.height;  // 内容总高度
  const a4PageHeight=(printContainerRef.clientWidth / 595.28) * 841.89; // a4纸高度
  let pageNum=Math.ceil(totalHeight / a4PageHeight);  // 总页数
  let addPageHeight=0;  // 修正被分割元素而增加的页面高度总和
  let currentPage=1;  // 当前正在处理切割的页面
  const splitItemObj: { [key: number]: HTMLElement[] }={};  // 内容中各页被切割元素存储对象

  const printItemNodes: NodeListOf<HTMLElement>=printContainerRef.querySelectorAll(`.${itemClassName}`);
  for (let item of printItemNodes) {
    // 如果当前页已经是最后一页, 则中断判断
    if (currentPage >=pageNum) {
      break;
    }

    // 获取元素绝对定位数据
    const clientRect=item.getBoundingClientRect();
    let top=clientRect.top;
    const selfHeight=clientRect.height;
    // 如果当前元素距离顶部高度大于当前页面页脚高度, 则开始判断下一页页脚被切割元素
    if (top > currentPage * a4PageHeight) {
      // 换页前修正上一页被切割元素
      addPageHeight +=fixSplitItems(currentPage, a4PageHeight, splitItemObj[currentPage], outFlowClassName);
      pageNum=Math.ceil((totalHeight + addPageHeight) / a4PageHeight);
      top=item.getBoundingClientRect().top;
      currentPage++;
    }
    // 如果元素刚好处于两页之间, 则记录该元素
    if (top > (currentPage - 1) * a4PageHeight && top < currentPage * a4PageHeight && top + selfHeight > currentPage * a4PageHeight) {
      if (!splitItemObj[currentPage]) {
        splitItemObj[currentPage]=[];
      }
      splitItemObj[currentPage].unshift(item);
      // 如果当前元素是最后一个元素, 则直接处理切割元素, 否则交由处理下一页元素时再处理切割
      if (item===printItemNodes[printItemNodes.length - 1]) {
        fixSplitItems(currentPage, a4PageHeight, splitItemObj[currentPage], outFlowClassName);
      }
    }
  }
}

/**
  * 修复当前页所有被切割元素
  * @param currentPage 当前页
  * @param pageHeight 每页高度
  * @param splitElementItems 当前被切割元素数组
  * @param outFlowClassName 脱离文档流的样式类名
  */
function fixSplitItems(
  currentPage: number,
  pageHeight: number,
  splitElementItems: HTMLElement[],
  outFlowClassName: string
): number {
  if (!splitElementItems || !splitElementItems.length) {
    return 0;
  }

  const yMargin=5;  // y方向距离页眉的距离
  const splitItemsMinTop=getSplitItemsMinTop(splitElementItems);
  if (!splitItemsMinTop) {
    return 0;
  }

  let fixHeight=currentPage * pageHeight - splitItemsMinTop + yMargin;
  const outFlowElement=splitElementItems.find((item)=> item.classList.contains(outFlowClassName));
  if (outFlowElement && outFlowElement.parentElement) {
    const parentPreviousElement=outFlowElement.parentElement.previousElementSibling as HTMLElement;
    fixHeight +=getMarinTopNum(parentPreviousElement, outFlowElement.parentElement);
    outFlowElement.parentElement.style.marginTop=`${fixHeight}px`;
    return fixHeight;
  }

  splitElementItems.forEach((splitElement)=> {
    splitElement.style.marginTop=`${fixHeight}px`;
  });
  return fixHeight;
}

/**
  * 获取被切割元素数组中最小高度值(如一行有多个元素被切割,则选出距离顶部最小的高度值)
  * @param splitElementItems 当前被切割元素数组
  */
function getSplitItemsMinTop(
  splitElementItems: HTMLElement[]
): number | undefined {
  // 获取元素中最小top值作为基准进行修正
  let minTop: number | undefined;
  let minElement: HTMLElement | undefined;
  splitElementItems.forEach((splitElement)=> {
    let top=splitElement.getBoundingClientRect().top;
    if (minTop) {
      minTop=top < minTop ? top : minTop;
      minElement=top < minTop ? splitElement : minElement;
    } else {
      minTop=top;
      minElement=splitElement;
    }
  });

  // 修正当前节点及其前面同层级节点的margin值
  if (minTop && minElement) {
    const previousElement=splitElementItems[splitElementItems.length - 1].previousElementSibling as HTMLElement;
    minTop -=getMarinTopNum(previousElement, minElement);
  }
  return minTop;
}

/**
  * 通过前一个兄弟元素和元素自身的位置确认一个距离顶部高度修正值
  * @param previousElement 前一个兄弟元素
  * @param curElement 当前元素
  */
function getMarinTopNum(previousElement: HTMLElement, curElement: HTMLElement): number {
  let preMarginNum=0;
  let curMarginNum=0;
  if (previousElement) {
    // 获取外联样式需要getComputedStyle(), 直接.style时对象的值都为空
    const previousMarginBottom=window.getComputedStyle(previousElement).marginBottom;
    preMarginNum=previousMarginBottom ? Number(previousMarginBottom.replace('px', '')) : 0;
  }
  const marginTop=window.getComputedStyle(curElement).marginTop;
  curMarginNum=marginTop ? Number(marginTop.replace('px', '')) : 0;
  return preMarginNum > curMarginNum ? preMarginNum : curMarginNum;
}

以上纯原创!欢迎加关注、加收藏、点赞、转发、分享(代码闲聊站)~