前端开发小细节

Ajax页面跳转并传入数据

在暑假进行的项目 医疗文本处理平台 中,我需要将队友的Ajax写好的功能整合到我的界面上,因为他之前是一个界面,而我想做成像搜索引擎那种,在一个页面上输入搜索词,跳转到另一个页面显示结果(后跳转界面还可以继续通过Ajax获得新的搜索结果)

在我们把后台服务化后,前端跨平台化之前,我们还需要了解前台和后台之间怎么通讯。从现有的一些技术上来看,Ajax是比较受欢迎的。

Ajax

AJAX 即 “Asynchronous JavaScript And XML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。这个功能在之前的很多年来一直被 Web 开发者所忽视,直到最近 Gmail、Google Suggest 和 Google Maps 的出现,才使人们开始意识到其重要性。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页如果需要更新内容,必须重载整个网页页面。

Ajax 请求

说起 Ajax,我们就需要用 JavaScript 向服务器发送一个 HTTP 请求。这个过程要从 XMLHttpRequest 开始说起,它是一个 JavaScript 对象。它最初由微软设计,随后被 Mozilla、Apple 和 Google 采纳。如今,该对象已经被 W3C 组织标准化。

如下的所示的是一个 Ajax 请求的示例代码:

1
2
3
4
5
6
7
8
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
alert(xhr.responseText);
}
}
xhr.open('GET', 'http://example.com', true);
xhr.send(null);

我们只需要简单的创建一个请求对象实例,打开一个 URL,然后发送这个请求。当传输完毕后,结果的 HTTP 状态以及返回的响应内容也可以从请求对象中获取。

而这个返回的内容可以是多种格式,如 XML 和 JSON,但是从近年的趋势来看,XML 基本上已经很少看到了。这里我们以 JSON 为主,来简单地介绍一下返回数据的解析。

JSON

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它基于 ECMAScript 的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于 C 语言家族的习惯(包括 C、C++、C#、Java、JavaScript、Perl、Python等)。这些特性使 JSON 成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成(一般用于提升网络传输速率)。

XML VS JSON

JSON 格式的数据具有以下的一些特点:

  • 容易阅读
  • 解析速度更快
  • 占用空间更少

如下所示的是一个简单的对比过程:

1
myJSON = {"age" : 12, "name" : "Danielle"}

如果我们要取出上面数值中的age,那么我们只需要这样做:

1
2
anObject = JSON.parse(myJSON);
anObject.age === 12 // True

同样的,对于 XML 来说,我们有下面的格式:

1
2
3
4
<person>
<age>12</age>
<name>Danielle</name>
</person>

而如果我们要取出上面数据中的age的值,他将是这样的:

1
2
3
4
myObject = parseThatXMLPlease();
thePeople = myObject.getChildren("person");
thePerson = thePeople[0];
thePerson.getChildren("age")[0].value() == "12" // True

对比一下,我们可以发现XML的数据不仅仅解析上比较麻烦,而且还繁琐。而且还有个问题,XML解析的时候,一般xml的起始语句前不能有空行。

JSON WEB Tokens

JSON Web Token (JWT) 是一种基于 token 的认证方案。

在人们大规模地开始 Web 应用的时候,我们在授权的时候遇到了一些问题,而这些问题不是 Cookie 所能解决的。Cookie 存在一些明显的问题:不能支持跨域、并且不是无状态的、不能使用CDN、与系统耦合等等。除了解决上面的问题,它还可以提高性能等等。基于 Session 的授权机制需要服务端来保存这个状态,而使用 JWT 则可以跳过这个问题,并且使我们设计出来的 API 满足 RESTful 规范。即,我们 API 的状态应该是没有状态的。因此人们提出了 JWT 来解决这一系列的问题。

通过 JWT 我们可以更方便地写出适用于前端应用的认证方案,如登陆、注册这些功能。当我们使用 JWT 来实现我们的注册、登陆功能时,我们在登陆的时候将向我们的服务器发送我们的用户名和密码,服务器验证后将生成对应的 Token。在下次我们进行页面操作的时候,如访问 /Dashboard 时,发出的 HTTP 请求的 Header 中会包含这个 Token。服务器在接收到请求后,将对这个 Token 进行验证并判断这个 Token 是否已经过期了。

实例

在这里将前一个页面命名为A,后一个为B(什么文件格式不重要,只要是静态页面就成)。

A中的JavaScript代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="text/javascript">
function jumpOnClick(flag) {
url = "section3_2.jsp?text=" + encodeURIComponent(document.getElementById('search').value) + "&flag=" + flag;
if(document.getElementById('search').value.match("\\s+") || document.getElementById('search').value == null || document.getElementById('search').value == ""){
alert("请输入症状或问题后点击相应查询按钮!");
return;
}
//网页跳转
location.href = url;

window.event.returnValue=false;
}
</script>

在A中,我将调用放到了按钮的onclick中。

B中的JavaScript代码

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
<script type="text/javascript">
  function GetUrlParam() {
    var url = document.location.toString();
    var arrObj = url.split("?");
var text,flag;
    if (arrObj.length > 1) {
      var arrPara = arrObj[1].split("&");
      var arr;
      for (var i = 0; i < arrPara.length; i++) {
        arr = arrPara[i].split("=");
        if (arr != null && arr[0] == "text") {
          text = decodeURIComponent(arr[1]);
flag = 0;
if(text == "" || text == null){
return;
}
var psel = document.getElementById("kw");
psel.value = text; //设置
        }else if(arr != null && arr[0] == "flag"){
flag = arr[1];
}
      }
if(flag == 1){
searchOnClick(text);
}else if(flag == 2){
search2OnClick(text);
}
    }
  }
</script>

在B中,我将调用放到了body的onload中。

至此完成上述功能,并且保证了在后跳入页面上刷新时,不会因为保留了跳入内容而无法刷新的情况。

H5网页失去焦点Title改变的方法

这里要分享的其实是一个API:visibilitychange

这个 API 本身非常简单,由以下三部分组成。

document.hidden:表示页面是否隐藏的布尔值。页面隐藏包括 页面在后台标签页中 或者 浏览器最小化 (注意,页面被其他软件遮盖并不算隐藏,比如打开的 sublime 遮住了浏览器)。

document.visibilityState:表示下面 4 个可能状态的值

  • hidden:页面在后台标签页中或者浏览器最小化
  • visible:页面在前台标签页中
  • prerender:页面在屏幕外执行预渲染处理 document.hidden 的值为 true
  • unloaded:页面正在从内存中卸载

Visibilitychange事件:当文档从可见变为不可见或者从不可见变为可见时,会触发该事件。

这样,我们可以监听 Visibilitychange 事件,当该事件触发时,获取 document.hidden 的值,根据该值进行页面一些事件的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>这是原来的title</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<script>
var tmptitle = document.title;
document.addEventListener('visibilitychange', function() {
var isHidden = document.hidden;
if (isHidden) {
document.title = '当焦点不在当前窗口时的网页标题';
} else {
document.title = tmptitle;
}
});
</script>
</head>
<body>
</body>
</html>

JavaScript操作DOM

创建节点

除了可以使用createElement创建元素,也可以使用createTextNode创建文本节点。document.body指向的是<body>元素,document.documentElement则指向<html>元素。

1
2
3
4
5
6
//创建节点
var createNode = document.createElement("div");
var createTextNode = document.createTextNode("hello world");
createNode.appendChild(createTextNode);
document.body.appendChild(createNode);
document.documentElement.appendChild(createNode);

插入节点

可以使用appendChild,insertBefore,insertBefore接收两个参数,第一个是插入的节点,第二个是参照节点,如insertBefore(a,b),则a会插入在b的前面

1
2
3
4
5
6
//插入节点
var createNode = document.createElement("div");
var createTextNode = document.createTextNode("hello world");
createNode.appendChild(createTextNode);
var div1 = document.getElementById("div1");
document.body.insertBefore(createNode,div1);

替换和删除元素

从replaceChild和removeChild的字面意思看,就是删除子节点,因此调用者,需要包含子节点div1,不然调用会报错。返回的节点是替换的或删除的元素,被替换/删除的元素仍然存在,但document中已经没有他们的位置了。

1
2
3
4
//替换元素
var replaceChild = document.body.replaceChild(createNode,div1);
//删除元素
var removeChild = document.body.removeChild(div1);

节点的属性

  • firstChild:第一个子节点
  • lastChild:最后一个子节点
  • childNodes:子节点集合,获取其中子节点可
  • someNode.childNodes[index]或
  • someNode.childNodes.item(index)
  • nextSibling:下一个兄弟节点
  • previousSibling:上一个兄弟节点
  • parentNode:父节点
1
2
3
4
5
6
<ul id="ul">
<li>sdsssssss</li>
<li>qqqq</li>
<li>wwww</li>
<li>eeee</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//节点属性
var ul = document.getElementById("ul");
var firstChild = ul.firstChild;
console.log(firstChild.innerHTML);
var lastChild = ul.lastChild;
console.log(lastChild.innerHTML);
var length = ul.childNodes.length;
console.log(length);
var secondChild = ul.childNodes.item(1);
console.log(secondChild.innerHTML);
var forthChild = ul.childNodes.item(2).nextSibling;
console.log(forthChild.innerHTML);
var thridChild = forthChild.previousSibling;
console.log(thridChild.innerHTML);
var parentNode = forthChild.parentNode;
console.log(parentNode.innerHTML);

文档片段

好处在于减少dom的渲染次数,可以优化性能。

1
2
3
4
5
6
7
8
9
10
//文本片段
var fragment = document.createDocumentFragment();
var ul = document.getElementById("ul");
var li = null;
for (var i = 4; i >= 0; i--) {
li = document.createElement("li");
li.appendChild(document.createTextNode("item "+i));
fragment.appendChild(li);
}
ul.appendChild(fragment);

克隆元素

  • someNode.cloneNode(true):深度克隆,会复制节点及整个子节点
  • someNode.cloneNode(false):浅克隆,会复制节点,但不复制子节点
1
2
3
//克隆
var clone = ul.cloneNode(true);
document.body.appendChild(clone);

需要注意的问题

childNodes.length存在跨浏览器的问题

可以看到有关列表的html片段没有用

1
2
3
4
5
6
<ul id="ul">
<li>sdsssssss</li>
<li>qqqq</li>
<li>wwww</li>
<li>eeee</li>
</ul>

这种书写格式而是使用没有换行的格式书写,是因为在不同的浏览器中,获取ul.childNodes.length的结果有差异:

  • 在ie中,ul.childNodes.length不会计算li之间的换行空格,从而得到数值为4
  • 在ff、chrome,safari中,会有包含li之间的空白符的5个文本节点,因此ul.childNodes.length为9
    若要解决跨浏览器问题,可以将li之间的换行去掉,改成一行书写格式。

cloneNode存在跨浏览器的问题

  • 在IE中,通过cloneNode方法复制的元素,会复制事件处理程序,比如,var b = a.cloneNode(true).若a存在click,mouseover等事件监听,则b也会拥有这些事件监听。
  • 在ff,chrome,safari中,通过cloneNode方法复制的元素,只会复制特性,其他一切都不会复制
    因此,若要解决跨浏览器问题,在复制前,最好先移除事件处理程序。

JavaScript将页面导出为图片

2016-12-12那天心血来潮,突然想将我们组开发的网站上的“导出Excel”功能做一点拓展,于是就想能不能直接将网页表格导出为图片!

在我的不懈搜索后(搜索过程中绝大部分博客上的博文要么相互抄袭要么没什么用),终于得到了“canvas2image.js”这个神奇的JavaScript脚本,具体使用办法见如下代码:

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
<!doctype html>
<html>
<script src="canvas2image.js"></script>
<body>
<canvas id="cvs"></canvas>
<button id="save">save</button>
<script>
var canvas, ctx, bMouseIsDown = false, iLastX, iLastY,
$save, $imgs,
$convert, $imgW, $imgH,
$sel;
function init (){
canvas = document.getElementById('cvs');
ctx = canvas.getContext('2d');
$save = document.getElementById('save');
$convert = document.getElementById('convert');
$sel = "png";
$imgs = document.getElementById('imgs');
$imgW = 1980;
$imgH = 2000;
bind();
draw();
}
function bind () {
canvas.onmousedown = function(e) {
bMouseIsDown = true;
iLastX = e.clientX - canvas.offsetLeft + (window.pageXOffset||document.body.scrollLeft||document.documentElement.scrollLeft);
iLastY = e.clientY - canvas.offsetTop + (window.pageYOffset||document.body.scrollTop||document.documentElement.scrollTop);
}
canvas.onmouseup = function() {
bMouseIsDown = false;
iLastX = -1;
iLastY = -1;
}
canvas.onmousemove = function(e) {
if (bMouseIsDown) {
var iX = e.clientX - canvas.offsetLeft + (window.pageXOffset||document.body.scrollLeft||document.documentElement.scrollLeft);
var iY = e.clientY - canvas.offsetTop + (window.pageYOffset||document.body.scrollTop||document.documentElement.scrollTop);
ctx.moveTo(iLastX, iLastY);
ctx.lineTo(iX, iY);
ctx.stroke();
iLastX = iX;
iLastY = iY;
}
};
$save.onclick = function (e) {
var type = $sel.value,
w = $imgW.value,
h = $imgH.value;
Canvas2Image.saveAsImage(canvas, w, h, type);
}
}
onload = init;
</script>
</body>
</html>

Web表格搜索

虽然表格的排列相当困难,但表格的搜索却非常容易。增加一个搜索输入,如果那里的值匹配到了任意一行的文本,则显示该行,并隐藏其他所有的行。使用jQuery来实现就像下面这么简单:

1
2
3
4
5
var allRows = $("tr");
$("input#search").on("keydown keyup", function() {
allRows.hide();
$("tr:contains('" + $(this).val() + "')").show();
});

没有看错,就是这么简单,如果是在实际应用中,可以这样来写:
先声明一个按钮:

1
<input type="search" id="search" placeholder="请输入内容……">

在input框之后加入以下JavaScript代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
// Quick Table Search
$('#search').keyup(function() {
var regex = new RegExp($('#search').val(), "i");
var rows = $('table tr:gt(0)');
rows.each(function (index) {
title = $(this).children("#title").html()
if (title.search(regex) != -1) {
$(this).show();
} else {
$(this).hide();
}
});
});
</script>

浮出层使用

进行《软件工程》大项目时,遇到想在主页上仿Google首页上的那种“一个输入框+两个按钮”,同时一个按钮搜索、一个按钮上传文件,于是到网上查了好久,都没有关于“一个按钮实现文件上传”的功能介绍,最后只能使用“通过一个浮出层弹出上传文件”这种方式,下面贴出JavaScript代码。

在按钮中使用时只需要这样使用就行:

1
<a href="javascript:_iframe() %>')" class="button">点击进入我的博客</a>

JavaScript代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
function _iframe() {
zeroModal.show({
title: '我的博客',
iframe: true,
url: '',
width: '60%',
height: '60%',
cancel: true
});
}
</script>

多个input合并提交到后台

需求

我在做我们的《软件工程》作业时,遇到了这样一个问题:我们需要打开一个表,这个表的列数不确定,但要增加增加行的操作。

实现

于是,需求产生了,我需要将前端的多个input标签内容合并成一个字符串来进行提交,我看了几个比较牛的方法(json、ognl……)但是好像与我们的需求偏的有点远(如果可以实现,欢迎留言),最后,没办法只能自己想,由于我还是会一点JavaScript的,所以我就想用JavaScript实现,在尝试了很多次之后,终于成功了,在此先贴上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script type="text/javascript">
function n(n){
var num="";
for(var i=0;i<n;i++){
num += document.getElementById("num"+i).value;
}
document.getElementById("result").value = num;
}
</script>
<%
int num=3;
%>
<form action="addAction">
<input id="num" name="num" value=<%=num %> type="text">
<%
for(int i=0;i<num;i++){
%>
<input id="num<%=i %>" type="text" onblur="n(<%=num%>)">
<%
}
%>
<input name="str" id="result" type="hidden" >
<button >提交</button>
</form>

分析

最后来分析,到底是怎么实现的,其实道理特别简单,就是JavaScript获取input的个数,然后一个循环,将所有input合并,并且给到一个“hidden”的input里,在后台接收这个input就可以了。

Hexo搭建博客中的小技巧

Hexo + GitHub 可能是一组比较简单的搭建博客的方式了。在使用Hexo的这么长的时间里,我发现很多不舒适的问题,通过搜索与探索,基本解决,现在分享出来。

添加404页面

有人可能也有这个疑问,通过GitHub发布的静态网页,没有路由,怎么识别404错误呢?GitHub已经替我们想到了这些,我们只需要在我们的网站主目录添加404.html,就可以实现出错自动跳转到404界面了,关于404界面的书写,大家可以直接下载我的404页面:/404.html

排除编译某文件

排除HTML等格式

在添加了404页面后,最简单的方法就是将404.html放到本地博客的public目录下。但是有个问题,也是最重要的,就是每次hexo clean之后,public/文件夹就会被删除,这样的话每次都需要粘贴进去,自然费时费力。

一个简单方法就是,将404.html等html文件,直接放到博客source或者主题source目录下,再hexo g生成,就自动生成了,但是又有个问题,虽然我们的html已经是完整的网页了,但是还是被hexo嵌入到系统的主题下。那怎么办呢?方法如下:

1
2
3
layout: false
title: "404"
---

将上述三行代码添加到不想被嵌入的HTML等文件的最前面。

排除.md格式

在不跟踪了HTML文件后,我们是不是又想用同样的方法,让hexo不要编译readme.md文件呢?

从网上的信息看,大致有三种方法:

  • Readme.md 改名Readme;(在GitHub显示上有些问题)
  • Readme.md改名Read.mdown;(完美)
  • _config.yml文件中添加skip_render:\n - README.md。(这种方法也适用于其他文件)

JavaScript加载图片

<img src="xx.jpg" />是每个前端开发都会的技能。然而,如果你想做到极致,事情还没有这么简单。

滚屏加载

这是最容易想到的点,也是一开始就准备做的。

随web体验的进步,滚屏加载代替分页加载的情形越来越多。也就是先预留图片位置,而不去加载图片,直到这个预留区域滚动到屏幕中,用户能看见了,才去加载图片。如此一来,有“按需加载”的意味,由于图片加载延后,不抢占带宽,在打开页面的首屏会快很多。我们称之为“懒加载(lazyload)”。

其实现也很简单,在html里面写<img lazy-src="xx.jpg" />,然后用js去判断这个节点是否出现在屏幕中,如果是,则取出lazy-src属性,赋值成<img lazy-src="xx.jpg" src="xx.jpg"/>,触发此节点onload,这就实现最简单的滚屏加载了。

特殊状态处理

特殊状态有两种:加载中与加载失败。这两种情况的处理逻辑相类似,拿加载中的逻辑做例子。

图片触发加载,到图片加载完成(或失败)之间,肯定会有一段时间。不做处理的话,用户在等待的过程中,就只能看到空白的区域,非常的奇怪。在低网速,以及用户非常快的拉滚动条的情形下,这种现象将更加明显。

那么在触发onload之前,就需要补一些逻辑,展示对应的loading图。
将需要处理的img节点作为参数,调用tempImg函数,克隆一个节点强行插在img之前,用于loading中的展示。

1
2
3
4
5
6
7
8
9
10
11
12
var tempImg = function(target){
var w = target.width();
var h = target.height();
var tempDom = target.clone().addClass("lazy-loding").insertBefore(target);

if(w/h == 1){
tempDom[0].src = "http://9.url.cn/edu/img/img-loading.png";
}else{
tempDom[0].src = "http://9.url.cn/edu/img/img-loading2.png";
}
target.hide();
}

上报监控

这一步在大型前端项目中非常重要,也是经常被忽略的地方。尽管需要简易的后台配合,但不算麻烦的上报监控,能让产品更加稳定和健壮。

我在两个地方用到了上报。其一是图片加载失败,触发onerror时,这样一来我们能知道每天图片拉取失败的量;其二是图片加载的时间,能够帮助我们分析cdn服务是否异常,分析全国慢速用户比例等等。

而所谓的上报其实就是一个http请求,我会大概把这些信息带上:

1
2
3
4
5
6
   log({
'type': 'error',
'msg': 'lazyload拉取图片失败上报 ',
'url': window.location.href,
'pid': 414342 //产品对应的id
});

居中截取

这是前端无可避免的一个问题,先来说下此问题的背景。

由于我们是先用一个空白的img标签占位,再去加载图片,如果图片的高度特别长(比如新浪长微博),加载完成时就会撑开节点,引起滚动条的跳动。由于移动端屏幕较PC窄,一个跳动就可能让你找不到前一秒正在浏览的内容,这种体验尤其严重。在移动端的web设计中,可以看到许多知名互联网公司的产品,也经常忽略这一点。

因此我们可以限定占位区域的size,以此区域来做居中截取。当占位区域与图片最终展示同宽同高时,就不会引起跳动,而且也保持了视觉的一致性。

其原理如下,先判断是竖向长型图,还是横向长型图,根据不同的情况,优先让宽或高填充满占位区域,然后通过不同的负margin去实现居中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var calSize = function($img) {
var w = $img.width(), h = $img.height(), width = size[0], height = size[1];
if(w+h == 0) return;

//如果是长型图,优先适配宽度,高度居中截取
if(w/h > width/height){
var newWidth = height * w / h;
var margin = (width - newWidth)/2;
$img.height(height).css({"margin-left": margin});
}else{
var newHeight = width * h / w;
var margin = (height - newHeight)/2;
$img.width(width).css({"margin-top": margin});
}
}

支持.webp

.webp格式图片是Google开发的一种旨在加快图片加载速度的图片格式,压缩提交大概只有jpg的2/3。随chrome的比例越来越多,其实让更多用户体验到.webp也是一件好事。

那么问题来了,怎么去判断用户的浏览器是否支持webp呢?
根据ua去判断是个好方法,但不太靠谱,因为chrome中其实也有设置,让它不能去支持webp,而且webkit本身就开源,会衍生出很多你不知道名字的浏览器。

最终我使用的是特性检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(!supportedWebPIsLoading) {
supportedWebPIsLoading = true;
var images = {
basic: "data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACyAgCdASoCAAEALmk0mk0iIiIiIgBoSygABc6zbAAA/v56QAAAAA=="
}, $img = new Image();
$img.onload = function () {
supportedWebPIsLoading = false;
$.cookie.set("iswebp" , +supportedWebP);
};
$img.onerror = function () {
supportedWebP = false;
supportedWebPIsLoading = false;
$.cookie.set("iswebp" , +supportedWebP);
};
$img.src = images.basic;
}

我们会让浏览器试着加载一张非常小的base64格式的webp图片,如果能够正常加载,说明是支持webp的。

并且,会把测试记录在cookie里,所以第二次直接从cookie里读结果,基本不会影响性能。完成了最重要的检查,我们就可以放心让服务器返回不同格式的图片了。



The link of this page is https://blog.nooa.tech/articles/2b207973/ . Welcome to reproduce it!

© 2018.02.08 - 2024.05.25 Mengmeng Kuang  保留所有权利!

:D 获取中...

Creative Commons License