avatar


99.iPhone设备的一个BUG

现象

最初是我发现《基于Java的后端开发入门:2.面向对象》这篇文章通过iPhone设备无法访问。
其状况如下:
打不开

而且不仅仅是我的iPhone,我找很多人帮我试了,都无法访问。

是JS吗

最初我判断是有某个异常的JS脚本导致的。
但是其他的页面都是OK的,唯独这个页面打不开,我也没有为这个页面专门写过JS。
这个页面相比其他大多数的页面有一个特点,我通过html的iframe标签引入了哔哩哔哩。但是我这个博客通过iframe标签引入哔哩哔哩的页面不止这一个,其他都是正常的。
但无论如何,首先判断是不是JS。

我采用了一个最直接的方法,直接禁用浏览器的JS。

结果非常的出乎我的意料,还是打不开。

是CSS吗

然后我开始通过谷歌搜索,终于让我搜索到了一点内容,是有一个CSS,iOS的Safari浏览器解析不了。但是我发现,那个解析不了的CSS,我似乎并没有用。
但无论如何,不是JS,就只可能是CSS了。

我怀疑是我的文章中有某段内容导致的,这段内容在经过Hexo的渲染之后,生成了iOS的Safari解析不了的CSS。
为了定位到那段内容,我先只用一小块进行渲染,测试一下,是OK的,然后我再加一小块,在也是OK,再加一小块,还是OK。终于在添加最后一小块的时候,复现了。
问题就出在最后一小块?
但是又一件非常奇怪的事情发生了,只用最后一小块进行渲染的话,不用其他部分的话,是OK的。
难道是各个部分相互作用导致的?

如果是各个部分相互作用导致的?
如果是这样的话,那么排查的难度就特别大了。

妥协,拆文章

但是呢,不能背离我写博客的目的,不要在这种事情上浪费时间。于是我选择了妥协,我开始拆文章,我把"面向对象"拆成了"封装"、"继承"和"多态"三部分。把"java.time"从"最常用的Java自带的类"中拆出来。

是代码高亮引擎吗?

但一直妥协不舒服。
我发现一个特点,这些文章普遍代码量比之前的多。此外,我通过技术交流群,发现也有其他人遇到了这个现象,而且他的文章的特点也是代码比较多。
而且我还是非常怀疑是某个CSS导致的,毕竟禁用浏览器的JS脚本能复现。

有一个东西能生成CSS,代码高亮引擎。
我的代码高亮引擎是Highlight,于是我想办法,引入了prismjs这个代码高亮引擎,但是居然没有解决问题!总不可能两个引擎都有问题把。
然后我索性disable掉所有的代码高亮引擎,不进行代码高亮,这时候只有pre标签和code标签,没有CSS内容。
还是没有解决问题!

反馈给苹果公司

技术交流群里的同学和我说,他的那篇文章在刚写好的时候,是OK的。
莫非和最近几次的iOS更新有关?
无奈之下,我选择反馈给苹果公司。
很快,苹果公司承认了,这是他们的BUG。

苹果公司承认了

跳转

所以,这件事情就提交给苹果公司了。而在苹果公司解决问题之前,我只能继续选择拆文章。
我把"集合"拆成"Collection"和"Map"两部分。
然后接下来,又要拆"IO",终于我不乐意了,我认为这个本来就一整块,没法拆,不好拆。
而且,我不喜欢写个文章,还时刻注意代码量,时刻担心又触发了iOS的BUG。
不符合技术人的风格。
所以我拒绝拆文章。

我选择正面指出这是iOS的BUG。

对于无法访问的文章,添加这段JS。

1
2
3
4
5
6
7
8
9
<script type="text/javascript">
// 如果是iPhone
if(navigator.platform == "iPhone"){
var url = window.location.href;
var id = url.substr(url.length - 2,2);
var replace = "/10899?id=" + id;
location.replace(replace);
}
</script>

检测是否是iPhone设备,对于iPhone设备,直接进行跳转,并同时把pdf的id传入。

在另一个页面接收id参数。
同时为了让document.getElementById("iphone-safari-bug").href=href;这段代码生效,让其在onload事件之后再执行。

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
window.onload=function(){
var query = window.location.search.substring(1);
var vars = query.split("&");
var pair = vars[0].split("=");
var id = pair[1];
var href = "/-/1/08/" + id + "/" + id + ".pdf";
console.log(href);
document.getElementById("iphone-safari-bug").href=href;
}
</script>
1
2
3
{% raw %}
<a id="iphone-safari-bug" style="font-weight: bold;">点击访问PDF版本</a>
{% endraw %}

Python渲染

但是呢,这一系列的Java文章,相比其他文章的最大特点是代码量特别多。所以采用跳转方案的话,几乎每一篇文章都会跳转到一个指出这是iOS的bug的的页面,然后用户从这里下载PDF文档。
用户体验终究是不好。

我在不断的实验中,我如果pre标签或code标签太多的话,是的,或,只要其中一种的标签太多,就会触发bug。
那么,就想办法不要pre标签和code标签。
所以新方案考虑用Python来自己渲染代码,并用raw标记hexo不进行渲染。

仍然检测是否是iPhone设备,如果发现是iPhone,跳转到108XX0页面,后面加一个0。

1
2
3
4
5
6
7
8
<script type="text/javascript">
// 如果是iPhone
if(navigator.platform == "iPhone"){
var url = window.location.href;
var replace = url.substr(-6,6) + '0';
location.replace(replace);
}
</script>

同时在108XX0页面进行检查,如果不是iPhone设备访问,就跳转到108XX

1
2
3
4
5
6
7
8
<script type="text/javascript">
// 如果不是iPhone
if(navigator.platform != "iPhone"){
var url = window.location.href;
var replace = url.substr(-7,6);
location.replace(replace);
}
</script>

接下来是最重要的代码了,基于Python实渲染代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from html import escape

file = 'md.md'
three = 0
front = 0
script = 0
code = False
with open(file, 'r', encoding='utf-8') as fr, open(file + '.md', 'w', encoding='utf-8') as fw:
while True:
line = fr.readline()
# 在文件中,如果遇到一个空白行,readline()并不会返回一个空串
# 因为每一行的末尾还有一个或多个分隔符,所以"空白行"至少会有一个换行符或者系统使用的其他符号。
# 只有当真的读到文件末尾时,才会读到空串""。
if not line:
break
# 在博客主页隐藏
if line.startswith('hide:'):
fw.writelines('hide: true' + '\n')
continue
# 不提交给搜索引擎
if line.startswith('sitemap:'):
fw.writelines('sitemap: false' + '\n')
continue
# 在原本的url后面加'0'
if line.startswith('url:'):
url = line.strip('\n') + '0' + '\n'
fw.writelines(url)
continue
# 如果front结束,添加这段JS脚本。
if line.startswith('---') and front < 2:
front = front + 1
if front == 2:
fw.writelines('---' + '\n')
s = '<script type="text/javascript">' + '\n' + \
' // 如果不是iPhone' + '\n' + \
' if(navigator.platform != "iPhone"){' + '\n' + \
' var url = window.location.href;' + '\n' + \
' var replace = url.substr(-7,6);' + '\n' + \
' location.replace(replace);' + '\n' + \
' }' + '\n' + \
' var dom = document.querySelector(".post-meta-wordcount");' + '\n' + \
' dom.removeAttribute("class");' + '\n' + \
' dom.setAttribute("style","display:none");' + '\n' + \
'</script>' + '\n'
fw.writelines(s)
continue
# 原博客的script脚本不要
# script == 0,是为了防止误杀后面真正的脚本
if script == 0 and line.startswith('<script type="text/javascript">'):
script = script + 1
if script == 1 and line.startswith('</script>'):
script = script + 1
continue
if 0 < script < 2:
continue
# 代码块
if line.startswith('```'):
three = three + 1
# 说明是代码起始位置
if three % 2 == 1:
code = True
# 说明是代码结束位置
if three % 2 == 0:
code = False
# 是代码
if code:
# 如果起始位置
if line.startswith('```'):
# 利用white-space这个属性防止父容器内容换行
content = '{% raw %}' + \
'\n' + \
'<div style="' \
'background-color:#F6F6F6;' \
'width: 100%;' \
'padding:10px;' \
'white-space: nowrap;' \
'overflow-x: auto;' \
'-webkit-overflow-scrolling:touch;' \
'">' + \
'\n'
# 否则就是中间内容
else:
# 去除换行
content = line.strip('\n')
# 对 < > & " 等进行编码
content = escape(content)
# 处理空格
content = content.replace(' ', '&nbsp;')
# 处理  
content = content.replace(' ', '&ensp;')
# 处理  
content = content.replace(' ', '&emsp;')
content = '<span>' + \
content + \
'</span><br/>' + \
'\n'
# 打印一下
print(content)
# 不是代码
else:
# 如果是代码结束位置
if line.startswith('```'):
content = '</div>' + \
'\n' + \
'<br/>' + '\n' \
'{% endraw %}' + \
'\n'
# 打印内容
print(content)
# 不做处理
else:
content = line
# 写文件
fw.writelines(content)

该脚本还有一个Java版本的,在《基于Java的后端开发入门:5.IO流》,讨论字符缓冲流特有功能的时候,作为了一个例子。

Java图片

上述的Python渲染方式,居然还有BUG。

Java图片

最终采取用Java直接渲染得到图片,然后把代码替换为图片的方式。
其核心代码是渲染代码,是我fork来的。
已开源:https://github.com/KakaWanYifan/code2image

1.0

后来发现,并不是所有的iPhone都会有这个现象。
只有当系统版本较高且缩放因子大于3的情况下,才会复现。所以进行再精细化的处理。
(找了系统为iOS12且缩放因子大于3的设备,没有复现,所以的确和系统版本有关。)

而且,如果收起代码的话,不会复现。
因为收起代码的话,会新增一个属性,display: none

1
2
3
4
5
6
7
figure.highlight
@extend $code-block
position: relative
border-radius: 1px

if hexo-config('highlight_shrink') == true
display: none

所以,新的思路:

  • 默认display: none
  • 对于,是Java文章 且 不是"10899"这一篇 且 是iPhone 且 缩放因子为3,添加提示类信息(点击展开)
  • 否则,移除display: none

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const $highlightTools = $('.highlight-tools')

// location.pathname.startsWith('/108') Java文章
// location.pathname != '/10899' 不是99
// navigator.platform == "iPhone" 是iPhone
// parseInt(window.devicePixelRatio) == 3 缩放因子为3
if (location.pathname.startsWith('/108') && location.pathname != '/10899' && navigator.platform == "iPhone" && parseInt(window.devicePixelRatio) == 3){
// 收起来
$highlightTools.append('<i class="fa fa-angle-down code-expand code-closed" aria-hidden="true"></i>')
$('<div class="highlight-tools" style="padding:0px 10px">点击&nbsp;<i class="fas fa-angle-right"></i>&nbsp;展开</div>').insertBefore('.code-area-wrap')
}else{
// 展开 可见
$highlightTools.append('<i class="fa fa-angle-down code-expand" aria-hidden="true"></i>')
$('.highlight').css('display','block');
}
  • 后来改为了针对所有文章生效。

附iPhone的分辨率和缩放因子:

手机机型
(iPhone)
屏幕尺寸
(inch)
逻辑分辨率
(pt)
设备分辨率
(px)
缩放因子
(Scale Factor)
3G(s)3.5320x480320x480@1x
4(s)3.5320x480640x960@2x
5(s/se)4320x568640x1136@2x
6(s)/7/84.7375x667750x1334@2x
6(s)/7/8 Plus5.5414x7361242x2208@3x
X/Xs /11 Pro5.8375x8121125x2436@3x
Xr /11| 6.16.1414x896828×1792@2x
Xs Max /11 Pro Max6.5414x8961242×2688@3x
12 mini5.4360x7801080x2340@3x
12/12 Pro6.1390x8441170x2532@3x
12 Pro Max6.7428x9261284x2778@3x
13 mini5.4360x7801080x2340@3x
13/13 Pro6.1390x8441170x2532@3x
13 Pro Max6.7428x9261284x2778@3x

2.0

1.0的方案,有一个非常不好的用户体验,在某些iPhone设备上,默认代码都是不展开的,需要用户手动展开。
如图所示:

默认不展开

2.0的方案为,通过代码,依次展开每一个,注意,是依次,每10毫秒展开一个(这么做,主要还是出于性能的考虑,担心一次性展开太多,又有问题)。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$highlightTools.append('<i class="fa fa-angle-down code-expand" aria-hidden="true"></i>')
let highlightSize = $('.highlight').length
let highlightIndex = 0;
let highlightInterval;

function highlightDisplay(){
$('.highlight')[highlightIndex].style.display = 'block';
highlightIndex = highlightIndex + 1;
if(highlightIndex >= highlightSize){
clearInterval(highlightInterval)
}
}

if(highlightSize > 0){
highlightInterval = self.setInterval(highlightDisplay,10)
}
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10899
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板