VVbuys Blog

standalone Linux lover

一、顺序结构中的双指针法

顺序结构:这里主要指的是数组或字符串。

(1)普通数组

slow指向 当前所需元素数组的下一个空位。返回值为slow时,slow就代表目标数组的长度。

题目简述 关键点 解题思路 力扣题目 相关链接
删除数组中的某类元素 同向移动,快慢指针同时出发; slow:指向当前所需元素数组的下一个空位。fast:遍历数组。 如果快指针遇到目标值则快指针直接跳过它一步,慢指针不动,如果快指针没有遇到目标值则将自己手中的数交给慢指针,然后各自前进一步。 27 移除元素(简单难度) https://leetcode-cn.com/problems/remove-element/
移动数组中的某类元素到开头或者末尾 同向移动,快慢指针同时出发; 如果快指针遇到目标值则快指针直接跳过它一步,慢指针不动,如果快指针没有遇到目标值则将自己手中的数和慢指针交换,然后各自前进一步。 283 移动零(简单难度) https://leetcode-cn.com/problems/move-zeroes/

(2)有序数组

无序数组可以直接使用sort()函数排序后变成有序数组。sort(nums.begin(),nums.end())

题目简述 关键点 解题思路 力扣题目 相关链接
删除有序数组中的重复元素 同向移动,快指针先行一步,每次不断比较两个指针上的数是否相同。 一个指针(fast)从头到尾遍历,另一个(slow)始终指示当前没有重复的元素数组的下一个空位置;相同则快指针直接跳过去多行一步,不相同则快指针将手中的数交给慢指针,然后快慢指针同时前进一步;(这时慢指针指的是当前没有重复元素的最后一个*:注意返回值是slow+1。) 26 删除排序数组中的重复项(简单难度) https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/
求有序数组中的众数(可能不止一个) 同向移动,快指针先行一步,每次不断比较两个指针上的数是否相同。 相同则频率计时器+1,不相同则频率计时器设为0;每次检查频率计时器是否超过最大计数,超过则记录当前快指针上的数为众数。vector数组排序:sort(nums.begin(),nums.end()) 169 多数元素(中等难度) https://leetcode-cn.com/problems/majority-element/
将有序数组中每个元素平方后再排序 反向移动,左指针和右指针同时出发,每次比较左指针和右指针手上的数的平方; 如果左指针手中数的平方更大则移动起始指针,如果末尾指针手中的数更大则移动末尾指针;当起始指针大于末尾指针时循环结束。(有序数组平方数组平方的最大值就在数组的两端,所以排序需要不断比较数组两端的数) 977 有序数组的平方(简单难度) https://leetcode-cn.com/problems/squares-of-a-sorted-array/

169 多数元素(中等难度)

1、排序法

众数是指在数组中出现次数大于⌊ n/2 ⌋ 的元素。 先将nums排序, ,然后返回中间元素的值即可(众数的个数大于一半,排好序的nums中间元素一定是众数) 。

1
2
3
4
5
6
7
class Solution {
public:
int majorityElement(vector<int>& nums) {
sort(nums.begin(),nums.end());
return nums[nums.size()/2];
}
};

2、摩尔投票法思路

候选人(cand_num)初始化为nums[0],票数count初始化为1。
当遇到与cand_num相同的数,则票数count = count + 1,否则票数count = count - 1。
当票数count为0时,更换候选人,并将票数count重置为1。
遍历完数组后,cand_num即为最终答案。

为何这行得通呢?
投票法是遇到相同的则票数 + 1,遇到不同的则票数 - 1。
且“多数元素”的个数> ⌊ n/2 ⌋,其余元素的个数总和<= ⌊ n/2 ⌋。
因此“多数元素”的个数 - 其余元素的个数总和 的结果 肯定 >= 1。
这就相当于每个“多数元素”和其他元素 两两相互抵消,抵消到最后肯定还剩余至少1个“多数元素”。

无论数组是1 2 1 2 1,亦或是1 2 2 1 1,总能得到正确的候选人。

作者:gfu
链接:https://leetcode-cn.com/problems/majority-element/solution/3chong-fang-fa-by-gfu-2/
来源:力扣(LeetCode)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int majorityElement(vector<int>& nums) {
int cand=nums[0],count=1;
for(int i=1;i<nums.size();i++){
if(cand==nums[i]){
count++;
}
else if(--count==0){
cand=nums[i];
count=1;
}
}
return cand;
}
};

二、非顺序结构中的双指针法

这里的非顺序结构主要指的是:链表或二叉树。

1、链表

链表删除某个元素的模板:

1、创建头结点和当前结点

2、删除结点

3、还原真实头结点,返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ListNode* removeElements(ListNode* head, int val) {
//新建虚拟头结点
ListNode * myHead = new ListNode(0);
myHead->next=head;
//循环检查链表的当前节点的下一节点
ListNode* cur=myHead;//当前节点(一定不为空)
while (cur->next != NULL) {
if(cur->next->val == val) {
cur->next = cur->next->next;//删除下一节点
} else {
cur = cur->next;//更新当前节点为下一节点
}
}
//还原真实头结点
head = myHead->next;
return head;
}

(1)链表的删除操作

有序链表中删除一个节点cur需要其前一个节点pre->next=cur->next; 所以其天生需要节点的先后关系。

所以在执行删除操作时,一般就需要(头结点)

题目简述 关键点 解题思路 力扣题目 相关链接
删除链表中的某类元素(简单难度) 虚拟头结点;链表的循环操作和节点的删除操作 同向移动,快指针从头结点先行一步,每次判断快指针是否符合删除条件,若符合则删除快指针并且重新为快指针赋值;否则则快指针和慢指针都前进一步。 203 移除链表元素(简单难度) https://leetcode-cn.com/problems/remove-linked-list-elements/
删除链表的倒数第N个节点(中等难度) 快指针先走N步之后进行判断 同向出发,快指针从头结点先行N步,如果此时快指针不存在则直接返回,若存在则快指针和慢指针(从头结点)同时出发,当快指针不存在时删除慢指针之后的一个节点。 19 删除链表的倒数第N个节点(中等难度) https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/

19.删除链表的倒数第n个结点

(1)假设链表长度为m+n。

fast、slow开始指向虚拟头结点。

1、fast先走n步

2、fast和slow同时走,直到fast指向链表的最后一个。

​ 则slow走了m步。

(2)链表的删除操作,需要得到目标结点的前一个结点。

所以

1、fast先走n+1步。

2、fast和slow同时走,直到fast指向链表的最后一个的下一个位置:NULL。

​ 则slow走了m-1步,在倒数第n个结点前。

(2)链表的反转操作

题目简述 关键点 解题思路 力扣题目 相关链接
将一个链表全部反转过来 相邻节点之间的反转操作 同向移动,快指针从头结点先行一步;每次移动快节点和慢节点都进行一次反向 206 反转链表(简单难度) https://leetcode-cn.com/problems/reverse-linked-list/
将一个链表中的[left,right]区域反转过来 慢指针从虚拟头结点出发找到第left个节点,然后快指针从慢指针的后一个节点出发,共同前进找到第right个节点 92 反转链表II(中等难度) https://leetcode-cn.com/problems/reverse-linked-list-ii/
根据快慢指针找到中点;反转后半段;比较前后两端节点值 234 回文链表(简单难度) https://leetcode-cn.com/problems/palindrome-linked-list/

206、反转链表

1、保存后面链表

2、断开指针

3、重新规划pre、cur

234、回文链表

1、慢指针走一步,快指针走两步

2、pre记录慢指针的前一个,用来分割链表,如果链表长度为奇数,则后半部分多一个结点。

3、将后半部分反转,得cur2,前半部分为cur1.

4、按照cur1的长度,比较cur1和cur2的节点数值。

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
class Solution {
public:
bool isPalindrome(ListNode* head) {
ListNode* slow=head;
ListNode* fast=head;
ListNode* pre=head;

//慢指针走一步,快指针走两步
while(fast && fast->next){
pre=slow;
slow=slow->next;
fast=fast->next->next;
}

//pre断开链表
pre->next=NULL;
ListNode* cur1=head;
//反转cur2
ListNode* cur2=reverselist(slow);
//按照cur1的长度,比较cur1,cur2
while(cur1){
if(cur1->val!=cur2->val){
return false;
}
cur1=cur1->next;
cur2=cur2->next;
}
return true;

}
ListNode* reverselist(ListNode* head){
ListNode* cur=head;
ListNode* temp=head;
ListNode* pre=NULL;
while(cur){
//保存后面链表
temp=cur->next;
//断开原链表
cur->next=pre;
//重新规划pre,cur
pre=cur;
cur=temp;
}
return pre;

}
};

学习率调度器(Scheduler)负责根据训练回合(epoch)来调整优化器的学习率(learning rate),该策略可以让模型更高效地收敛。

可以看出,学习率调度器Scheduler和参数优化器optimizer的使用紧密结合。

一、优化器optimizer的定义

我们以最常见的Adam优化器为例进行介绍(所有optimizers都继承自torch.optim.Optimizer类):

1
2
3
4
5
6
import torch
net = model()
initial_lr = 0.1
optimizer = torch.optim.Adam(params=net.parameters(),# 需要优化的可迭代的网络参数,可以是多个网络的参数
lr=initial_lr,# 初始学习率
)

二、学习率调度器scheduler的定义

torch.optim.lr_scheduler模块提供了一些根据epoch训练次数来调整学习率(learning rate)的方法。

下面列举常见的学习率调整策略有几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch.optim.lr_scheduler as lr_scheduler
# 将每个参数组的学习率设置为初始lr与给定函数的乘积
scheduler = lr_scheduler.LambdaLR(optimizer,
lr_lambda=lambda epoch:0.95**epoch, # 根据epoch计算衰减因子的函数,也可是函数列表
)
# 每过step_size个epoch,做一次学习率更新:
scheduler = lr_scheduler.StepLR(optimizer,
step_size=30, # 每训练step_size个回合更新一次学习率
gamma=0.1,# 衰减因子,学习率的乘法因子
)
# 该策略能够读取模型的性能指标,当该指标停止改善时,持续关系几个epochs之后,自动减小学习率。
scheduler = lr_scheduler.ReduceLROnPlateau(optimizer,
mode='min', # 指示指标不再减小/增大时降低学习率,可取min/max
factor=0.1,# 衰减因子,默认为0.1
patience= 10,# 默认为10,patience个回合之后降低学习率
min_lr= self.min_lr# 默认为0,最小学习率
)

当然我们也可以继承lr_scheduler或其子类来自定义学习率的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LinearDecay(lr_scheduler._LRScheduler):
"""This class implements LinearDecay"""
def __init__(self, optimizer, num_epochs, start_epoch=0, min_lr=0, last_epoch=-1):
"""implements LinearDecay
Parameters:
----------

"""
super().__init__(optimizer, last_epoch)
self.num_epochs = num_epochs # 训练的总回合
self.start_epoch = start_epoch # 起始回合
self.min_lr = min_lr # 最小学习率


def get_lr(self):
# 如果没有到指定回合则直接返回
if self.last_epoch < self.start_epoch:
return self.base_lrs
lr = [base_lr - ((base_lr - self.min_lr) / self.num_epochs) * (self.last_epoch - self.start_epoch) for base_lr in self.base_lrs]
return lr

三、学习率预热机制-Warmup

1、什么是Warmup?

Warmup是在ResNet论文中提到的一种学习率预热的方法,即先用最初的小学习率训练,然后每个step增大一点点,直到达到最初设置的比较大的学习率时(注:此时预热学习率完成),采用最初设置的学习率进行训练(注:预热学习率完成后的训练过程,学习率是衰减的),有助于使模型收敛速度变快,效果更佳。

2、为什么使用Warmup?

由于刚开始训练时,模型的权重(weights)是随机初始化的,此时若选择一个较大的学习率,可能带来模型的不稳定(振荡),选择Warmup预热学习率的方式,可以使得开始训练的几个epoches或者一些steps内学习率较小,在预热的小学习率下,模型可以慢慢趋于稳定,等模型相对稳定后再选择预先设置的学习率进行训练,使得模型收敛速度变得更快,模型效果更佳。

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
class WarmRestart(lr_scheduler.CosineAnnealingLR):
"""This class implements Stochastic Gradient Descent with Warm Restarts(SGDR): https://arxiv.org/abs/1608.03983.
Set the learning rate of each parameter group using a cosine annealing schedule, When last_epoch=-1, sets initial lr as lr.
This can't support scheduler.step(epoch). please keep epoch=None.
"""

def __init__(self, optimizer, T_max=30, T_mult=1, eta_min=0, last_epoch=-1):
"""implements SGDR
Parameters:
----------
T_max : int
Maximum number of epochs.
T_mult : int
Multiplicative factor of T_max.
eta_min : int
Minimum learning rate. Default: 0.
last_epoch : int
The index of last epoch. Default: -1.
"""
self.T_mult = T_mult
super().__init__(optimizer, T_max, eta_min, last_epoch)

def get_lr(self):
import math
if self.last_epoch == self.T_max:
self.last_epoch = 0
self.T_max *= self.T_mult
return [self.eta_min + (base_lr - self.eta_min) * (1 + math.cos(math.pi * self.last_epoch / self.T_max)) / 2 for
base_lr in self.base_lrs]

四、封装的学习率调整器

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
import torch.optim.lr_scheduler as lr_scheduler

class Scheduler():
def __init__(self,name):
self.name = name
self.min_lr = 0.0000001 # 衰减的最低学习率


def get_scheduler(self, optimizer):
if self.name == 'lambdaLR':
# 将每个参数组的学习率设置为初始lr与给定函数的乘积
scheduler = lr_scheduler.LambdaLR(optimizer,
lr_lambda=lambda epoch:0.95**epoch, # 根据epoch计算衰减因子的函数,也可以是函数列表
)
elif self.name == 'stepLR':
# 每过step_size个epoch,做一次学习率更新:
scheduler = lr_scheduler.StepLR(optimizer,
step_size=30, # 每训练step_size个回合更新一次学习率
gamma=0.1,# 衰减因子,学习率的乘法因子
)
elif self.name == 'plateau':
# 该策略能够读取模型的性能指标,当该指标停止改善时,持续关系几个epochs之后,自动减小学习率。
scheduler = lr_scheduler.ReduceLROnPlateau(optimizer,
mode='min', # 指示指标不再减小/增大时降低学习率,可取min/max
factor=0.1,# 衰减因子,默认为0.1
patience= 10,# 默认为10,patience个回合之后降低学习率
min_lr= self.min_lr)# 默认为0,最小学习率
elif self.name == 'sgdr':
# 学习率的预热机制
scheduler = WarmRestart(optimizer)
elif self.name == 'linear':
# 从start_epoch开始进行学习率的线性衰减:
scheduler = LinearDecay(optimizer,
min_lr=self.min_lr, # 最小学习率
num_epochs=10, # 训练的总回合数
start_epoch=5) # 开始衰减的回合数
else:
raise ValueError("Scheduler [%s] 无法初始化." % self.config['scheduler']['name'])
return scheduler
def get_test_optimizer():
import torch
class model(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv1 = torch.nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)

def forward(self, x):
pass
net = model()
initial_lr = 0.1
optimizer = torch.optim.Adam(params=net.parameters(),# 需要优化的可迭代的网络参数,也可以是多个网络的参数
lr=initial_lr,# 初始学习率
)
return optimizer

if __name__=="__main__":
optimizer = get_test_optimizer()
#scheduler = Scheduler('lambdaLR').get_scheduler(optimizer)
scheduler = Scheduler('linear').get_scheduler(optimizer)
print("初始化的学习率:", optimizer.defaults['lr'])
for epoch in range(1, 11):
optimizer.zero_grad()
optimizer.step()
print("第%d个epoch的学习率:%f" % (epoch, optimizer.param_groups[0]['lr']))
scheduler.step()

测试输出结果:

初始化的学习率: 0.1

第1个epoch的学习率:0.100000

第2个epoch的学习率:0.100000

第3个epoch的学习率:0.100000

第4个epoch的学习率:0.100000

第5个epoch的学习率:0.100000

第6个epoch的学习率:0.100000

第7个epoch的学习率:0.090000

第8个epoch的学习率:0.080000

第9个epoch的学习率:0.070000

第10个epoch的学习率:0.060000

进程已结束,退出代码 0

「博文视点」邀请我给《PWA实战:面向下一代的Progressive Web APP》 写的推荐序。

Progressive Web App 是继 Ajax、响应式设计、HTML5 之后,web 平台的又一次革命性突破。它在开放 Web 标准的基础之上,突破了以往 Web 应用只能「依赖互联网分发」与「依赖浏览器为入口」的两大桎梏,一下子打开了 Web 应用从性能、架构到用户体验上的一系列可能性。

PWA 中最引入注目的核心新特性,Service Worker,实质上是为 Web 应用带来了一种安全而又低功耗的后台处理能力。无论是用于实现离线 Web 应用所需要的缓存读写与网络代理,还是用于提升 Web 应用能力的推送通知、后台同步,其实都得益于这种新的并发能力。随着 Edge 与 Safari 的相继发布,Service Worker 已经历史性的达到了全浏览器的支持。

而这就要归功于 Web 开放性的力量。相比于其他众多私有的“类 Web”技术,PWA 技术完全属于开放 Web 标准。PWA 因此具备了独一无二的跨平台能力,不止于移动端,Chrome 与 Windows 已经让 PWA 在桌面端也晋升为了第一公民。这使得「一套代码,发布可以同时跨移动桌面设备、跨操作系统、跨浏览器的超级应用」真正成为可能。这里有非常大的想象空间,非常值得我们期待。

PWA 作为“下一代 Web 应用模型”从 2015 年第一次发布,到现在的 2018 年中,国际上 Google、Twitter、Facebook、Instagram、FlipKart、Uber、Lyft、Pinterest、Tinder、Flipboard、Spotify……国内诸如 AliExpress、饿了么、微博,都已经在使用 PWA 技术甚至发布了专门的 PWA 产品。可以说 PWA 从生态到工具链都已经逐渐成熟,接下来将会迎来更大的爆发。

在这个时间点上,很高兴能看到本书的翻译团队能在如此短的时间里将最新的技术带回中文社区,非常难能可贵。本人也做过一些 PWA 的分享,但要对社区带来更大的推动,我们更需要这样完整的大部头作品。

本书原著非常详实且不失生动地涵盖了 PWA 的方方面面。作者不但通过一个贯穿全书的案例将 PWA 的各项技术串起,还把它们所要解决的问题与可以带来的产品价值也一一娓娓道来。书中讨论到的策略与模式非常实用,既可以帮助你快速上手 PWA,也能帮助你对 Web 应用的工程化有更好的理解。

在此,我谨作为整个中文 Web 社区的一员,感谢团队的贡献!

今年 9 月份的时候,《程序员》杂志社就邀请我写一篇关于 PWA 的文章。后来花式拖稿,拖过了 10 月的 QCon,11 月的 GDG DevFest,终于在 12 月把这篇长文熬了出来。几次分享的不成熟,这次的结构算是比较满意了。「 可能是目前中文世界里对 PWA 最全面详细的长文了」,希望你能喜欢。


本文首发于 CSDN 与《程序员》2017 年 2 月刊,同步发布于 Hux Blog前端外刊评论 - 知乎专栏,转载请保留链接 ;)

下一代 Web 应用?

近年来,Web 应用在整个软件与互联网行业承载的责任越来越重,软件复杂度和维护成本越来越高,Web 技术,尤其是 Web 客户端技术,迎来了爆发式的发展。

包括但不限于基于 Node.js 的前端工程化方案;诸如 Webpack、Rollup 这样的打包工具;Babel、PostCSS 这样的转译工具;TypeScript、Elm 这样转译至 JavaScript 的编程语言;React、Angular、Vue 这样面向现代 web 应用需求的前端框架及其生态,也涌现出了像同构 JavaScript通用 JavaScript 应用这样将服务器端渲染(Server-side Rendering)与单页面应用模型(Single-page App)结合的 web 应用架构方式,可以说是百花齐放。

但是,Web 应用在移动时代并没有达到其在桌面设备上流行的程度。究其原因,尽管上述的各种方案已经充分利用了现有的 JavaScript 计算能力、CSS 布局能力、HTTP 缓存与浏览器 API 对当代基于 Ajax响应式设计的 web 应用模型的性能与体验带来了工程角度的巨大突破,我们仍然无法在不借助原生程序辅助浏览器的前提下突破 web 平台本身对 web 应用固有的桎梏:客户端软件(即网页)需要下载所带来的网络延迟;与 Web 应用依赖浏览器作为入口所带来的体验问题。


Web 与原生应用在移动平台上的使用时长对比 图片来源: Google

在桌面设备上,由于网络条件稳定,屏幕尺寸充分,交互方式趋向于多任务,这两点造成的负面影响对比 web 应用免于安装、随叫随到、无需更新等优点,瑕不掩瑜。但是在移动时代,脆弱的网络连接与全新的人机交互方式使得这两个问题被无限放大,严重制约了 web 应用在移动平台的发展。在用户眼里,原生应用不会出现「白屏」,清一色都摆在主屏幕上;而 web 应用则是浏览器这个应用中的应用,使用起来并不方便,而且加载也比原生应用要慢。

Progressive Web Apps(以下简称 PWA)以及构成 PWA 的一系列关键技术的出现,终于让我们看到了彻底解决这两个平台级别问题的曙光:能够显著提高应用加载速度、甚至让 web 应用可以在离线环境使用的 Service Worker 与 Cache Storage;用于描述 web 应用元数据(metadata)、让 web 应用能够像原生应用一样被添加到主屏、全屏执行的 Web App Manifest;以及进一步提高 web 应用与操作系统集成能力,让 web 应用能在未被激活时发起推送通知的 Push API 与 Notification API 等等。

将这些技术组合在一起会是怎样的效果呢?「印度阿里巴巴」 —— Flipkart 在 2015 年一度关闭了自己的移动端网站,却在年底发布了现在最为人津津乐道的 PWA 案例 FlipKart Lite,成为世界上第一个支撑大规模业务的 PWA。发布的一周后它就亮相于 Chrome Dev Summit 2015 上,笔者当时就被惊艳到了。为了方便各媒介上的读者观看,笔者做了几幅图方便给大家介绍:


图片来源: Hux & Medium.com

当浏览器发现用户需要 Flipkart Lite 时,它就会提示用户「嘿,你可以把它添加至主屏哦」(用户也可以手动添加)。这样,Flipkart Lite 就会像原生应用一样在主屏上留下一个自定义的 icon 作为入口;与一般的书签不同,当用户点击 icon 时,Flipkat Lite 将直接全屏打开,不再受困于浏览器的 UI 中,而且有自己的启动屏效果。


图片来源: Hux & Medium.com

更强大的是,在无法访问网络时,Flipkart Lite 可以像原生应用一样照常执行,还会很骚气的变成黑白色;不但如此,曾经访问过的商品都会被缓存下来得以在离线时继续访问。在商品降价、促销等时刻,Flipkart Lite 会像原生应用一样发起推送通知,吸引用户回到应用。

无需担心网络延迟;有着独立入口与独立的保活机制。之前两个问题的一并解决,宣告着 web 应用在移动设备上的浴火重生:满足 PWA 模型的 web 应用,将逐渐成为移动操作系统的一等公民,并将向原生应用发起挑战与「复仇」。

更令笔者兴奋的是,就在今年 11 月的 Chrome Dev Summit 2016 上,Chrome 的工程 VP Darin Fisher 介绍了 Chrome 团队正在做的一些实验:把「添加至主屏」重命名为「安装」,被安装的 PWA 不再仅以 widget 的形式显示在桌面上,而是真正做到与所有原生应用平级,一样被收纳进应用抽屉(App Drawer)里,一样出现在系统设置中 🎉🎉🎉。


图片来源: Hux & @adityapunjani

图中从左到右分别为:类似原生应用的安装界面;被收纳在应用抽屉里的 Flipkart Lite 与 Hux Blog;设置界面中并列出现的 Flipkart 原生应用与 Flipkart Lite PWA (可以看到 PWA 巨大的体积优势)

笔者相信,PWA 模型将继约 20 年前横空出世的 Ajax 与约 10 年前风靡移动互联网的响应式设计之后,掀起 web 应用模型的第三次根本性革命,将 web 应用带进一个全新的时代。

PWA 关键技术的前世今生

Web App Manifest

Web App Manifest,即通过一个清单文件向浏览器暴露 web 应用的元数据,包括名字、icon 的 URL 等,以备浏览器使用,比如在添加至主屏或推送通知时暴露给操作系统,从而增强 web 应用与操作系统的集成能力。

让 web 应用在移动设备上的体验更接近原生应用的尝试其实早在 2008 年的 iOS 1.1.3 与 iOS 2.1.0 时就开始了,它们分别为 web 应用增加了对自定义 icon 和全屏打开的支持。


图片来源: appleinsider.com

但是很快,随着越来越多的私有平台通过 <meta>/<link> 标签来为 web 应用添加「私货」,<head> 很快就被塞满了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- Add to homescreen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Lighten">

<!-- Add to homescreen for Chrome on Android -->
<meta name="mobile-web-app-capable" content="yes">
<mate name="theme-color" content="#000000">

<!-- Icons for iOS and Android Chrome M31~M38 -->
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="images/touch/apple-touch-icon-144x144-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="images/touch/apple-touch-icon-114x114-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="images/touch/apple-touch-icon-72x72-precomposed.png">
<link rel="apple-touch-icon-precomposed" href="images/touch/apple-touch-icon-57x57-precomposed.png">

<!-- Icon for Android Chrome, recommended -->
<link rel="shortcut icon" sizes="196x196" href="images/touch/touch-icon-196x196.png">

<!-- Tile icon for Win8 (144x144 + tile color) -->
<meta name="msapplication-TileImage" content="images/touch/ms-touch-icon-144x144-precomposed.png">
<meta name="msapplication-TileColor" content="#3372DF">

<!-- Generic Icon -->
<link rel="shortcut icon" href="images/touch/touch-icon-57x57.png">

显然,这种做法并不优雅:分散又重复的元数据定义多余且难以维持同步,与 html 耦合在一起也加重了浏览器检查元数据未来变动的成本。与此同时,社区里开始出现使用 manifest 文件以中心化地描述元数据的方案,比如 Chrome Extension、 Chrome Hosted Web Apps (2010)Firefox OS App Manifest (2011) 使用 JSON;CordovaWindows Pinned Site 使用 XML;

2013 年,W3C WebApps 工作组开始对基于 JSON 的 Manifest 进行标准化,于同年年底发布第一份公开 Working Draft,并逐渐演化成为今天的 W3C Web App Manifest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"short_name": "Manifest Sample",
"name": "Web Application Manifest Sample",
"icons": [{
"src": "launcher-icon-2x.png",
"sizes": "96x96",
"type": "image/png"
}],
"scope": "/sample/",
"start_url": "/sample/index.html",
"display": "standalone",
"orientation": "landscape"
"theme_color": "#000",
"background_color": "#fff",
}
1
2
<!-- document -->
<link rel="manifest" href="/manifest.json">

诸如 nameiconsdisplay 都是我们比较熟悉的,而大部分新增的成员则为 web 应用带来了一系列以前 web 应用想做却做不到(或在之前只能靠 hack)的新特性:

  • scope:定义了 web 应用的浏览作用域,比如作用域外的 URL 就会打开浏览器而不会在当前 PWA 里继续浏览。
  • start_url:定义了一个 PWA 的入口页面。比如说你添加 Hux Blog 的任何一个文章到主屏,从主屏打开时都会访问 Hux Blog 的主页。
  • orientation:终于,我们可以锁定屏幕旋转了(喜极而泣…)
  • theme_color/background_color:主题色与背景色,用于配置一些可定制的操作系统 UI 以提高用户体验,比如 Android 的状态栏、任务栏等。

这个清单的成员还有很多,比如用于声明「对应原生应用」的 related_applications 等等,本文就不一一列举了。作为 PWA 的「户口本」,承载着 web 应用与操作系统集成能力的重任,Web App Manifest 还将在日后不断扩展,以满足 web 应用高速演化的需要。

Service Worker

我们原有的整个 Web 应用模型,都是构建在「用户能上网」的前提之下的,所以一离线就只能玩小恐龙了。其实,对于「让 web 应用离线执行」这件事,Service Worker 至少是 web 社区的第三次尝试了。

故事可以追溯到 2007 年的 Google Gears:为了让自家的 Gmail、Youtube、Google Reader 等 web 应用可以在本地存储数据与离线执行,Google 开发了一个浏览器拓展来增强 web 应用。Google Gears 支持 IE 6、Safari 3、Firefox 1.5 等浏览器;要知道,那一年 Chrome 都还没出生呢。

在 Gears API 中,我们通过向 LocalServer 模块提交一个缓存文件清单来实现离线支持:

1
2
3
4
// Somewhere in your javascript
var localServer = google.gears.factory.create("bata.localserver");
var store = localServer.createManagedStore(STORE_NAME);
store.manifestUrl = "manifest.json"
1
2
3
4
5
6
7
8
9
// manifest.json
{
"betaManifestVersion": 1,
"version": "1.0",
"entries": [
{ "url": "index.html" },
{ "url": "main.js" }
]
}

是不是感到很熟悉?好像 HTML5 规范中的 Application Cache 也是类似的东西?

1
<html manifest="cache.appcache">
1
2
3
4
5
CACHE MANIFEST

CACHE:
index.html
main.js

是的,Gears 的 LocalServer 就是后来大家所熟知的 App Cache 的前身,大约从 2008 年开始 W3C 就开始尝试将 Gears 进行标准化了;除了 LocalServer,Gears 中用于提供并行计算能力的 WorkerPool 模块与用于提供本地数据库与 SQL 支持的 Database 模块也分别是日后 Web Worker 与 Web SQL Database(后被废弃)的前身。

HTML5 App Cache 作为第二波「让 web 应用离线执行」的尝试,确实也服务了比如 Google Doc、尤雨溪早年作品 HTML5 Clear、以及一直用 web 应用作为自己 iOS 应用的 FT.com(Financial Times)等不少 web 应用。那么,还有 Service Worker 什么事呢?

是啊,如果 App Cache 没有被设计得烂到完全不可编程、无法清理缓存、几乎没有路由机制、出了 Bug 一点救都没有,可能就真没 Service Worker 什么事了。App Cache 已经在前不久定稿的 HTML5.1 中被拿掉了,W3C 为了挽救 web 世界真是不惜把自己的脸都打肿了……

时至今日,我们终于迎来了 Service Worker 的曙光。简单来说,Service Worker 是一个可编程的 Web Worker,它就像一个位于浏览器与网络之间的客户端代理,可以拦截、处理、响应流经的 HTTP 请求;配合随之引入 Cache Storage API,你可以自由管理 HTTP 请求文件粒度的缓存,这使得 Service Worker 可以从缓存中向 web 应用提供资源,即使是在离线的环境下。


Service Worker 就像一个运行在客户端的代理

比如说,我们可以给网页 foo.html 注册这么一个 Service Worker,它将劫持由 foo.html 发起的一切 HTTP 请求,并统统返回未设置 Content-TypeHello World!

1
2
3
4
// sw.js
self.onfetch = (e) => {
e.respondWith(new Response('Hello World!'))
}

Service Worker 第一次发布于 2014 年的 Google IO 上,目前已处于 W3C 工作草案的状态。其设计吸取了 Application Cache 的失败经验,作为 web 应用的开发者的你有着完全的控制能力;同时,它还借鉴了 Chrome 多年来在 Chrome Extension 上的设计经验(Chrome Background Pages 与 Chrome Event Pages),采用了基于「事件驱动」的唤醒机制,以大幅节省后台计算的能耗。比如上面的 fetch 其实就是会唤醒 Service Worker 的事件之一。


Service Worker 的生命周期

除了类似 fetch 这样的功能事件外,Service Worker 还提供了一组生命周期事件,包括安装、激活等等。比如,在 Service Worker 的「安装」事件中,我们可以把 web 应用所需要的资源统统预先下载并缓存到 Cache Storage 中去:

1
2
3
4
5
6
7
8
9
10
11
// sw.js
self.oninstall = (e) => {
e.waitUntil(
caches.open('installation')
.then(cache => cache.addAll([
'./',
'./styles.css',
'./script.js'
]))
)
});

这样,当用户离线,网络无法访问时,我们就可以从缓存中启动我们的 web 应用:

1
2
3
4
5
6
7
8
9
//sw.js
self.onfetch = (e) => {
const fetched = fetch(e.request)
const cached = caches.match(e.request)

e.respondWith(
fetched.catch(_ => cached)
)
}

可以看出,Service Worker 被设计为一个相对底层(low-level)、高度可编程、子概念众多,也因此异常灵活且强大的 API,故本文只能展示它的冰山一角。出于安全考虑,注册 Service Worker 要求你的 web 应用部署于 HTTPS 协议下,以免利用 Service Worker 的中间人攻击。笔者在今年 GDG 北京的 DevFest 上分享了 Service Worker 101,涵盖了 Service Worker 譬如「网络优先」、「缓存优先」、「网络与缓存比赛」这些更复杂的缓存策略、学习资料、以及示例代码,可以供大家参考。


Service Worker 的一种缓存策略:让网络请求与读取缓存比赛

你也可以尝试在支持 PWA 的浏览器中访问笔者的博客 Hux Blog,感受 Service Worker 的实际效果:所有访问过的页面都会被缓存并允许在离线环境下继续访问,所有未访问过的页面则会在离线环境下展示一个自定义的离线页面。

在笔者看来,Service Worker 对 PWA 的重要性相当于 XMLHTTPRequest 之于 Ajax,媒体查询(Media Query)之于响应式设计,是支撑 PWA 作为「下一代 web 应用模型」的最核心技术。由于 Service Worker 可以与包括 Indexed DB、Streams 在内的大部分 DOM 无关 API 进行交互,它的潜力简直无可限量。笔者几乎可以断言,Service Worker 将在未来十年里成为 web 客户端技术工程化的兵家必争之地,带来「离线优先(Offline-first)」的架构革命。

Push Notification

PWA 推送通知中的「推送」与「通知」,其实使用的是两个不同但又相得益彰的 API:

Notification API 相信大家并不陌生,它负责所有与通知本身相关的机制,比如通知的权限管理、向操作系统发起通知、通知的类型与音效,以及提供通知被点击或关闭时的回调等等,目前国内外的各大网站(尤其在桌面端)都有一定的使用。Notification API 最早应该是在 2010 年前后由 Chromium 提出草案webkitNotifications 前缀方式实现;随着 2011 年进入标准化;2012 年在 Safari 6(Mac OSX 10.8+)上获得支持;2015 年 Notification API 成为 W3C Recommendation;2016 年 Edge 的支持;Web Notifications 已经在桌面浏览器中获得了全面支持(Chrome、Edge、Firefox、Opera、Safari)的成就。

Push API 的出现则让推送服务具备了向 web 应用推送消息的能力,它定义了 web 应用如何向推送服务发起订阅、如何响应推送消息,以及 web 应用、应用服务器与推送服务之间的鉴权与加密机制;由于 Push API 并不依赖 web 应用与浏览器 UI 存活,所以即使是在 web 应用与浏览器未被用户打开的时候,也可以通过后台进程接受推送消息并调用 Notification API 向用户发出通知。值得一提的是,Mac OSX 10.9 Mavericks 与 Safari 7 在 2013 年就发布了自己的私有推送支持,基于 APNS 的 Safari Push Notifications

在 PWA 中,我们利用 Service Worker 的后台计算能力结合 Push API 对推送事件进行响应,并通过 Notification API 实现通知的发出与处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// sw.js
self.addEventListener('push', event => {
event.waitUntil(
// Process the event and display a notification.
self.registration.showNotification("Hey!")
);
});

self.addEventListener('notificationclick', event => {
// Do something with the event
event.notification.close();
});

self.addEventListener('notificationclose', event => {
// Do something with the event
});

对于 Push Notification,笔者的几次分享中一直都提的稍微少一些,一是因为 Push API 还处于 Editor Draft 的状态,二是目前浏览器与推送服务间的协议支持还不够成熟:Chrome(与其它基于 Blink 的浏览器)在 Chromium 52 之前只支持基于 Google 私有的 GCM/FCM 服务进行通知推送。不过好消息是,继 Firefox 44 之后,Chrome 52 与 Opera 39 也紧追其后实现了正在由 IETF 进行标准化的 Web 推送协议(Web Push Protocol)

如果你已经在使用 Google 的云服务(比如 Firebase),并且主要面向的是海外用户,那么在 web 应用上支持基于 GCM/FCM 的推送通知并不是一件费力的事情,笔者推荐你阅读一下 Google Developers 的系列文章,很多国外公司已经玩起来了。

从 Hybrid 到 PWA,从封闭到开放

2008 年,当移动时代来临,唱衰移动 Web 的声音开始出现,而浏览器的进化并不能跟上时,来自 Nitobi 的 Brian Leroux 等人创造了 Phonegap,希望它能以 Polyfill 的形式、弥补目前浏览器与移动设备间的「鸿沟」,从此开启了混合应用(Hybrid Apps)的时代。

几年间,Adobe AIRWindows Runtime AppsChrome AppsFirefox OSWebOSCordova/PhonegapElectron 以及国内比如微信、淘宝,无数的 Hybrid 方案拔地而起,让 web 开发者可以在继续使用 web 客户端技术的同时,做到一些只有原生应用才能做到的事情,包括访问一些设备与操作系统 API,给用户带来更加 「Appy」 的体验,以及进入 App Store 等等。


众多的 Hybrid 方案

PWA 作为一个涵盖性术语,与过往的这些或多或少通过私有平台 API 增强 web 应用的尝试最大的不同,在于构成 PWA 的每一项基本技术,都已经或正在被 IETF、ECMA、W3C 或 WHATWG 标准化,不出意外的话,它们都将被纳入开放 web 标准,并在不远的将来得到所有浏览器与全平台的支持。我们终于可以逃出 App Store 封闭的秘密花园,重新回到属于 web 的那片开放自由的大地。

有趣的是,从上文中你也可以发现,组成 PWA 的各项技术的草案正是由上述各种私有方案背后的浏览器厂商或开发者直接贡献或间接影响的。可以说,PWA 的背后并不是某一家或两家公司,而是整个 web 社区与整个 web 规范。正是因为这种开放与去中心化的力量,使得万维网(World Wide Web)能够成为当今世界上跨平台能力最强、且几乎是唯一一个具备这种跨平台能力的应用平台。

「我们相信 Web,是因为相信它是解决设备差异化的终极方案;我们相信,当 Web 在今天做不到一件事的时候,是因为它还没来得及去实现,而不是因为他做不到。而 Phonegap,它的终极目的就是消失在 Web 标准的背后。」

在不丢失 web 的开放灵魂,在不需要依靠 Hybrid 把应用放在 App Store 的前提下,让 web 应用能够渐进式地跳脱出浏览器的标签,变成用户眼中的 App。这是 Alex Russell 在 2015 年提出 PWA 概念的原委

而又正因为 web 是一个整体,PWA 可以利用的技术远不止上述的几个而已:Ajax、响应式设计、JavaScript 框架、ECMAScript Next、CSS Next、Houdini、Indexed DB、Device APIs、Web Bluetooth、Web Socket、Web Payment、孵化中的 Background Sync APIStreams、WebVR……开放 Web 世界 27 年来的发展以及未来的一切,都与 PWA 天作之合。

鱼与熊掌的兼得

经过几年来的摸索,整个互联网行业仿佛在「Web 应用 vs. 原生应用」这个问题上达成了共识:

  • web 应用是鱼:迭代快,获取用户成本低;跨平台强体验弱,开发成本低。适合拉新
  • 原生应用是熊掌:迭代慢,获取用户成本高;跨平台弱体验强,开发成本高。适合保活

要知道,虽然用户花在原生应用上的时间要明显多于 web 应用,但其中有 80% 的时间是花在前五个应用中的调查显示,美国有一半的智能手机用户平均每月新 App 安装量为零,而月均网站访问量却有 100 个,更别提 Google Play 上有 60% 的应用从未被人下载过了。于是,整个行业的产品策略清一色地「拿鱼换熊掌」,比如笔者的老东家阿里旅行(飞猪旅行),web 应用布满阿里系各种渠道,提供「优秀的第一手体验」,等你用的开心了,再引诱你去下载安装原生应用。


原生应用、当代 Web 与 PWA 图片来源: Hux & Google

但是,PWA 的出现,让鱼与熊掌兼得变成了可能 —— 它同时具备了 web 应用与原生应用的优点,有着自己独有的先进性:「浏览器 -> 添加至主屏/安装 -> 具备原生应用体验的 PWA -> 推送通知 -> 具备原生应用体验的 PWA」,PWA 自身就包含着从拉新到保活的闭环。

除此之外,PWA 还继承了 web 应用的另外两大优点:无需先付出几十兆的下载安装成本即可开始使用,以及不需要经过应用超市审核就可以发布新版本。所以,PWA 可以称得上是一种「流式应用(Streamable App)」与「常青应用(Evergreen App)」

未来到来了吗

在笔者分享 PWA 的经历中,最不愿意回答的两个问题莫过于「PWA 已经被广泛支持了吗?」以及「PWA 与 ABCDEFG 这些技术方案相比有什么优劣?」,但是这确实是两个逃不开的问题。

PWA 的支持情况?

当我们说到 PWA 是否被支持时,其实我们在说的是 PWA 背后的几个关键技术都得到支持了没有。以浏览器内核来划分的话,Blink(Chrome、Oprea、Samsung Internet 等)与 Gecko(Firefox)都已经实现了 PWA 所需的所有关键技术(👏👏👏),并已经开始探寻更多的可能性。EdgeHTML(Edge)简直积极得不能更积极了,所有的特性都已经处于「正在开发中」的状态。最大的绊脚石仍然来自于 Webkit(Safari),尤其是在 iOS 上,上述的四个 API 都未得到支持,而且由于平台限制,第三方浏览器也无法在 iOS 上支持。(什么你说 IE?

不过,也不要气馁,Webkit 不但在它 2015 年发布的五年计划里提到了 Service Worker,更是已经在最近实现了 Service Worker 所依赖的 Request、Response 与 Fetch API,还把 Service Worker 与 Web App Manifest 纷纷列入了「正在考虑」的 API 中;要知道,Webkit 可是把 Web Components 中的 HTML Imports 直接列到「不考虑」里去了……(其实 Firefox 也是)

更何况,由于 web 社区一直以来所追求的「渐进增强、优雅降级」,一个 PWA 当然可以在 iOS 环境正常执行。事实上,华盛顿邮报将网站迁移到 PWA 之后发现,不止是 Android,在 iOS 上也获得了 5 倍的活跃度增长,(无论是不是它们之前的网站写得太烂吧),就算 iOS 现在还不支持 PWA 也不会怎么样,我们更是有理由相信 PWA 会很快在 iOS 上到来。

PWA vs. Others

贺老(贺师俊)曾说过:「从纯 Web 到纯 Native,之间有许多可能的点」。当考虑移动应用的技术选型时,除了 Web 与原生应用,我们还有各种不同程度的 Hybrid,还有今年爆发的诸多 JS-to-Native 方案。

虽然我在上文中用了「复仇」这样的字眼,不过无论从技术还是商业的角度,我们都没必要把 web 或是 PWA 放到 Native 的对立面去看。它们当然存在竞争关系,但是更多的时候,web-only 与 app-only 的策略都是不完美的,当公司资源足够的时候,我们通常会选择同时开发两者。当然,无论与不与原生应用对比,PWA 让 web 应用变得体验更好这件事本身是毋庸置疑的。「不谈场景聊技术都是扯淡」,我们仍然还是需要根据自己产品与团队的情况来决定对应的技术选型与平台策略,只是 PWA 让 web 应用在面对选型考验时更加强势了而已。


众多的技术选型,以及笔者的一种猜测

笔者不负责任得做一些猜测:虽然重量级的 Hybrid 架构与基础设施仍是目前不少场景下最优的解决方案;但是随着移动设备本身的硬件性能提升与新技术的成熟与普及,JS-to-Native 与以 PWA 为首的纯 web 应用,将分别从两个方向挤压 Hybrid 的生存空间,消化当前 Hybrid 架构主要解决的问题;前者将逐渐演化为类似 Xarmarin 这样针对跨平台原生应用开发的解决方案;后者将显著降低当前 Hybrid 架构的容器开发与部署成本,将 Hybrid 返璞归真为简单的 webview 调用。

这种猜测当然不是没有依据的瞎猜,比如前者可以参考阿里巴巴集团级别迁移 Weex 的战略与微信小程序的 roadmap;后者则可以参考当前 Cordova 与 Ionic 两大 Hybrid 社区对 PWA 的热烈反响。

PWA in China

看看 Google 官方宣传较多的 PWA 案例就会发现,FlipKart、Housing.com 来自印度;Lyft、华盛顿邮报来自北美;唯一来自中国的 AliExpress 主要开展的则是海外业务。

由于中国的特殊性,笔者在第一次聊到 PWA 时难免表现出了一定程度的悲观:

  • 国内较重视 iOS,而 iOS 目前还不支持 PWA。
  • 国内的 Android 实为「安卓」,不自带 Chrome 是一,可能还会有其他兼容问题。
  • 国内厂商可能并不会像三星那样对推动自家浏览器支持 PWA 那么感兴趣。
  • 依赖 GCM 推送的通知不可用,Web Push Protocol 还没有国内的推送服务实现。
  • 国内 webview 环境较为复杂(比如微信),黑科技比较多。

反观印度,由于 Google 服务健全、标配 Chrome 的 Android 手机市占率非常高,PWA 的用户达到率简直直逼 100%,也难免获得无数好评与支持了。笔者奢望着本文能对推动 PWA 的国内环境有一定的贡献。不过无论如何,PWA 在国内的春天可能的确会来得稍微晚一点了。

结语

我们信仰 Web,不仅仅在于软件、软件平台与单纯的技术,还在于『任何人,在任何时间任何地点,都可以在万维网上发布任何信息,并被世界上的任何一个人所访问到。』而这才是 web 的最为革命之处,堪称我们人类,作为一个物种的一次进化。

请不要让 web 再继续离我们远去,浏览器厂商们已经重新走到了一起,而下一棒将是交到我们 web 应用开发者的手上。乔布斯曾相信 web 应用才移动应用的未来,那就让我们用代码证明给这个世界看吧。

让我们的用户,也像我们这般热爱 web 吧。

黄玄,于 12 月的北京。


注:在笔者撰文期间,Google 在 Google China Developers Days 上宣布了 developers.google.cn 域名的启用,方便国内开发者访问。对于文中所有链向 developers.google.com 的参考文献,应该都可以在 cn 站点中找到。

下滑这里查看更多内容

TLDR; It covers lots of cool stuff about Service Worker!

Watching Fullscreen →

Scanning on mobile

Demo Code →

  • Hello World of Service Worker
  • Make your own Offline Dinosaurs
  • Stale/Fastest while revalidate

Notes

This slides is powered by Yanshuo.io (演说.io), a online software helping you create, store and share web slides.

There are 2 ways that you can fork or contribute this project:

  1. index.html is the HTML source code exported from Yanshuo.io, and many of its dependencis (js, css, fonts) are still linked to CDN of Yanshuo.io. You can do any secondary development and host it by yourself.
  2. Download the project file under shuo/, drag it into Yanshuo.io, and you are ready to go. You can edit whatever you want, upload it to your account, and even share your distributions.

下滑这里查看更多内容

Watching Fullscreen →

Watching Video →

Scanning on mobile

Catalog

  • The State Of Web
  • Rethinking Hybridzation
  • PWA 101
    • Definition
    • Add To HomeScreen
      • Web Manifest
    • Reliable Experience (Network as PE)
      • Service Worker
        • Register SW
        • On Install & Cache API
          • ExtendableEvent & SkipWaiting
        • On Fetch
        • Stale-While-Revalidate & Fallback
        • Updating SW
        • SW LifeCycle
        • On Activate
        • SW Brings Architectural Revolution
    • Re-engageable
  • PWA In Production
    • User Expectation & Guiding
    • Low Deliver Friction
  • PWA vs. Others
  • The Belief In Web
    • One Web
    • Fulfill WWDC 2007

Notes

This slides is powered by Yanshuo.io (演说.io), a online software helping you create, store and share web slides.

index.html is the HTML source code exported from Yanshuo.io, and many of its dependencis (js, css, fonts) are still linked to CDN of Yanshuo.io. You can do any secondary development and host it by yourself.

这篇文章转载自我在知乎专栏「前端外刊评论」上发表的文章

Angular 2 已经发布 Beta 版,而且似乎很有信心在 2016 年成为热门框架。是时候进行一场巅峰对决了,我们来看看它如何与 React 这个 2015 年的新宠抗衡。

免责声明:我之前很喜欢使用 Angular 1,不过在 2015 年转到了 React。最近我也在 Pluralsight 上发布了一门关于 React 和 Flux 的课程免费试学)。所以,是的,我本人是有偏见的,但我不会偏袒任何一方。

好了,我们开始吧,这场对决将会非常血腥。

图片来源:@jwcarrol

两者根本不具有可比性!

是的是的,Angular 是框架,React 是类库。所以有人觉得比较这两者没有逻辑性可言。大错特错!

选择 Angular 还是 React 就像选择直接购买成品电脑还是买零件自己组装一样。

两者的优缺点本文都会提及,我会拿 React 语法和组件模型跟 Angular 的语法和组件模型做对比。这就像是拿成品电脑的 CPU 跟零售的 CPU 做对比,没有任何不妥。

Angular 2 的优点

我们先看 Angular 相对 React 有哪些优势。

无选择性疲劳

Angular 是一个完整的框架,本身就提供了比 React 多得多的建议和功能。而要用 React,开发者通常还需要借助别的类库来打造一个真正的应用。比如你可能需要额外的库来处理路由、强制单向数据流、进行 API 调用、做测试以及管理依赖等等。要做的选择和决定太多了,让人很有压力。这也是为什么 React 有那么多的入门套件的原因(我自己就写了两个:12)。

Angular 自带了不少主张,所以能够帮助你更快开始,不至于因为要做很多决定而无所适从。这种强制的一致性也能帮助新人更快适应其开发模式,并使得开发者在不同团队间切换更具可行性。

Angular 核心团队让我非常欣赏的一点是,他们拥抱了 TypeScript,这就造成了另一个优势。

TypeScript = 阳关大道

没错,并非所有人都喜欢 TypeScript,但是 Angular 2 毅然决然地选择了它确实是个巨大的优势。反观 React,网上的各种示例应用令人沮丧地不一致——ES5 和 ES6 的项目基本上各占一半,而且目前存在三种不同的组件声明方式。这无疑给初学者造成了困惑。(Angular 还拥抱了装饰器(decorator)而不是继承(extends)——很多人认为这也是个加分项)。

尽管 Angular 2 并不强制使用 TypeScript,但显然的是,Angular 的核心团队默认在文档中使用 TypeScript。这意味着相关的示例应用和开源项目更有可能保持一致性。Angular 已经提供了非常清晰的关于如何使用 TypeScript 编译器的例子。(诚然,目前并非所有人都在拥抱 TypeScript,但我有理由相信等到正式发布之后,TypeScript 会成为事实上的标准)。这种一致性应该会帮助初学者避免在学习 React 时遇到的疑惑和选择困难。

极少的代码变动

2015 年是 JavaScript 疲劳元年,React 可以说是罪魁祸首。而且 React 尚未发布 1.0,所以未来还可能有很多变数。React 生态圈依旧在快速地变动着,尤其是各种 Flux 变种路由。也就是说,你今天用 React 写的所有东西,都有可能在 React 1.0 正式发布后过时,或者必须进行大量的改动。

相反,Angular 2 是一个对已经成熟完整框架(Angular 1)的重新发明,而且经过仔细、系统的设计。所以 Angular 不大可能在正式发布后要求已有项目进行痛苦的代码变动。Angular 作为一个完整的框架,你在选择它的时候,也会信任其开发团队,相信他们会认真决定框架的未来。而使用 React,一切都需要你自己负责,你要自己整合一大堆开源类库来打造一个完整的应用,类库之间互不相干且变动频繁。这是一个令人沮丧的耗时工作,而且永远没有尽头。

广泛的工具支持

后面我会说,我认为 React 的 JSX 是非常耀眼的亮点。然而要使用 JSX,你需要选择支持它的工具。尽管 React 已经足够流行,工具支持不再是什么问题,但诸如 IDE 和 lint 工具等新工具还不大可能很快得到支持。Angular 2 的模版是保存在一个字符串或独立的 HTML 文件中的,所以不要求特殊的工具支持(不过似乎 Angular 字符串模版的智能解析工具已经呼之欲出了)。

Web Components 友好

Angular 2 还拥抱了 Web Component 标准。唉,真尴尬我居然一开始忘记提到这点了——最近我还发布了一门关于Web Components 课程呢!简单来说,把 Angular 2 组件转换成原生 Web Components 应该会比 React 组件容易得多。固然 Web Components 的浏览器支持度依然很弱,但长期来看,对 Web Components 友好是很大的优势。

Angular 的实现有其自身的局限和陷阱,这正好让我过渡到对 React 优势的讨论。

React 的优点

现在,让我们看看是什么让 React 如此与众不同。

JSX

JSX 是一种类似 HTML 的语法,但它实际上会被编译成 JavaScript。将标签与代码混写在同一个文件中意味着输入一个组件的函数或者变量时你将享受到自动补全的福利。而 Angular 基于字符串的模版就相形见绌了:很多编辑器都不会高亮它们(只会显示单色)、只有有限的代码补全支持,并且一直到运行时才会报错。并且,通常你也只能得到很有限的错误提示。不过,Angular 的团队造了一个自己的 HTML 解析器来解决这个问题。(叼叼叼!)

如果你不喜欢 Angular 的字符串模版,你可以把模版移到一个单独的文件里去。不过这样你就回到了我认为的“老样子”:你需要在自己脑袋里记住这两个文件的关联,不但没有代码自动补全,也没有任何编译时检查来协助你。这听起来可能并不算什么……除非你已经爱上了与 React 相伴的日子。在同一个文件中组合组件还能享受编译时的检查,大概是 JSX 最与众不同的地方之一了。

对比 Angular 2 与 React 在标签忘记闭合时是如何表现的。

关于为什么 JSX 是一个巨大的优势,可以看看 JSX:硬币的另一面(JSX: The Other Side of the Coin). (P.S. 这是作者写的另一篇文章,如果大家希望我们可以把这篇也翻了,欢迎在评论区举手)

React 报错清晰快速

当你在 React 的 JSX 中不小心手抖打错时,它并不会被编译。这是一件非常美妙的事情:无论你是忘记闭合了标签还是引用了一个不存在的属性(property),你都可以立刻知道到底是哪一行出错了。JSX 编译器会指出你手抖的具体行号,彻彻底底加速你的开发。

相反,当你在 Angular 2 中不小心敲错了一个变量时,鸦雀无声。Angular 2 并不会在编译时做什么,它会等到运行时才静默报错。它报错得如此之慢,我加载完整个应用然后奇怪为什么我的数据没有显示出来呢?这太不爽了。

React 以 JavaScript 为中心

终于来了。这才是 React 和 Angular 的根本区别。很不幸,Angular 2 仍然是以 HTML 而非 JavaScript 为中心的。Angular 2 并没有解决它设计上的根本问题:

Angular 2 继续把 “JS” 放到 HTML 里。React 则把 “HTML” 放到 JS 里。

这种分歧带来的影响真是再怎么强调也不为过。它们从根本上影响着开发体验。Angular 以 HTML 为中心的设计留下了巨大的缺陷。正如我在 JSX:硬币的另一面 中所说的,JavaScript 远比 HTML 要强大。因此,增强 JavaScript 让其支持标签要比增强 HTML 让其支持逻辑要合理得多。无论如何,HTML 与 JavaScript 都需要某种方式以粘合在一起。React 以 JavaScript 为中心的思路从根本上优于 Angular、Ember、Knockout 这些以 HTML 为中心的思路。

让我们来看看为什么。

React 以 JavaScript 为中心的设计 = 简约

Angular 2 延续了 Angular 1 试图让 HTML 更加强大的老路子。所以即使是像循环或者条件判断这样的简单任务你也不得不使用 Angular 2 的独特语法来完成。例如,Angular 2 通过两种语法同时提供了单向数据绑定与双向数据绑定,可不幸的是它们实在差得有点多:

1
2
{{myVar}}        //单向数据绑定
ngModel="myVar" //双向数据绑定

在 React 中,数据绑定语法不取决于数据流的单双向(数据绑定的单双向是在其他地方处理的,不得不说我觉得理应如此)。不管是单向还是双向数据流,绑定语法都是这样的:

1
{myVar}

Angular 2 的内联母版(inline master templates)使用了这样的语法:

1
2
3
4
5
<ul>
<li *ngFor="#hero of heroes">
{{hero.name}}
</li>
</ul>

上面这个代码片段遍历了一组 hero,而我比较关心的几点是:

  • 通过星号来声明一个“母版”实在是太晦涩了
  • hero 前的英镑符号(#)用于声明一个局部模版变量。这个概念感觉非常鸡肋(如果你偏好不使用 #,你也可以使用 var- 前缀写法)
  • 为 HTML 加入了循环语义的HTML 特性(attribute)ngFor 是 Angular 特有的东西

相比上面 Angular 2 的语法,React 的语法可是纯净的 JavaScript (不过我得承认下面的属性 key 是个 React 的私货)

1
2
3
4
5
<ul>
{ heroes.map(hero =>
<li key={hero.id}>{hero.name}</li>
)}
</ul>

鉴于 JS 原生支持循环,React JSX 利用 JS 的力量来做到这类事情简直易如反掌,配合 mapfilter 能做的还远不止此。

去看看 Angular 2 速查表?那不是 HTML,也不是 JavaScript……这叫 Angular

读懂 Angular: 学一大堆 Angular 特有的语法

读懂 React: 学 JavaScript

React 因为语法和概念的简约而与众不同。我们不妨品味下当今流行的 JS 框架/库都是如何实现遍历的:

1
2
3
4
5
Ember     : {{# each}}
Angular 1 : ng-repeat
Angular 2 : ngFor
Knockout : data-bind="foreach"
React : 直接用 JS 就好啦 :)

除了 React,所有其它框架都用自己的专有语法重新发明了一个我们在 JavaScript 常见得不能再常见的东西:循环。这大概就是 React 的美妙之处,利用 JavaScript 的力量来处理标签,而不是什么奇怪的新语法。

Angular 2 中的奇怪语法还有点击事件的绑定:

1
(click)="onSelect(hero)"

相反,React 再一次使用了普通的 JavaScript:

1
onClick={this.onSelect.bind(this, hero)}

并且,鉴于 React 内建了一个模拟的事件机制(Angular 2 也有),你并不需要去担心使用内联语法声明事件处理器所暗含的性能问题。

为什么要强迫自己满脑子都是一个框架的特殊语法呢?为什么不直接拥抱 JS 的力量?

奢华的开发体验

JSX 具备的代码自动补全、编译时检查与丰富的错误提示已经创造了非常棒的开发体验,既为我们减少了输入,与节约了时间。而配合上热替换(hot reloading)与时间旅行(time travel),你将获得前所未有的开发体验,效率高到飞起。

原文这里链了个 Youtube 上的视频:Dan Abramov - Live React: Hot Reloading with Time Travel at react-europe 2015,大家自备梯子。

担心框架的大小?

这里是一些常见框架/库压缩后的大小(来源):

  • Angular 2: 566k (766k with RxJS)
  • Ember: 435k
  • Angular 1: 143k
  • React + Redux: 139k

列出的都是框架级的、用于浏览器且压缩后的大小(但并未 gzip)。需要补充的是,Angular 2 的尺寸在最终版本发布时应该会有所减小。

为了做一个更真实的对比,我将 Angular 2 官方教程中的 Tour of Heroes 应用用 Angular 2 和 React(还用上了新的 React Slingshot 入门套件)都实现了一遍,结果如何呢?

可以看到,做一个差不多的东西,Angular 2 目前的尺寸是 React + Redux 的五倍还多。重要的事情再说一遍,Angular 2 的最终版本应该会减重。

不过,我承认关于框架大小的担忧可能被夸大了:

大型应用往往至少有几百 KB 的代码,经常还更多,不管它们是不是使用了框架。开发者需要做很多的抽象来构建一个复杂的软件。无论这些抽象是来自框架的还是自己手写的,它都会对应用的加载性能造成负面影响。

就算你完全杜绝框架的使用,许多应用仍然是几百 KB 的 JavaScript 在那。 — Tom Dale JavaScript Frameworks and Mobile Performance

Tom 的观点是对的。像 Angular、Ember 这样的框架之所以更大是因为它们自带了更多的功能。

但是,我关心的点在于:很多应用其实用不到这种大型框架提供的所有功能。在这个越来越拥抱微服务、微应用、单一职责模块(single-responsibility packages)的时代,React 通过让你自己挑选必要模块,让你的应用大小真正做到量身定做。在这个有着 200,000 个 npm 模块的世界里,这点非常强大。

React 信奉Unix 哲学.

React 是一个类库。它的哲学与 Angular、Ember 这些大而全的框架恰恰相反。你可以根据场景挑选各种时髦的类库,搭配出你的最佳组合。JavaScript 世界在飞速发展,React 允许你不断用更好的类库去迭代你应用中的每个小部分,而不是傻等着你选择的框架自己升级。

Unix 久经沙场屹立不倒,原因就是:

小而美、可组合、目的单一,这种哲学永远不会过时。

React 作为一个专注、可组合并且目的单一的工具,已经被全世界的各大网站们使用,预示着它的前途光明(当然,Angular 也被用于许多大牌网站)。

谢幕之战

Angular 2 相比第一代有着长足的进步。新的组件模型比第一代的指令(directives)易学许多;新增了对于同构/服务器端渲染的支持;使用虚拟 DOM 提供了 3-10 倍的性能提升。这些改进使得 Angular 2 与 React 旗鼓相当。不可否认,它功能齐全、观点鲜明,能够显著减少 “JavaScript 疲劳” 。

不过,Angular 2 的大小和语法都让我望而却步。Angular 致力的 HTML 中心设计比 React 的 JavaScript 中心模型要复杂太多。在 React 中,你并不需要学习 ng-什么什么 这种框架特有的 HTML 补丁(shim),你只要写 JavaScript 就好了。这才是我相信的未来。

著作权声明

本文译自 Angular 2 versus React: There Will Be Blood,其实之前有人翻译过,但是翻得水平有一点不忍直视,我们不希望浪费这篇好文章。
本文由 @李凌豪 @黄玄 联合翻译,首次发布于前端外刊评论 · 知乎专栏,转载请保留原文链接 ;)

下滑这里查看更多内容

Watching Fullscreen →

你也可以通过扫描二维码在手机上观看

这个 Web Slides 开源在我的 Github 上,欢迎你帮助我完善这个展示文稿,你可以给我提 issue,可以 fork & pull request。如果它能帮助到你了,希望你还能不吝啬 star 一下这个项目

Catalog

  • Document Times
    • Frameworks
    • Style Guide
      • OOCSS
      • SMACSS
    • Pre-processer
    • PostCSS
  • Application Times
    • Shadow DOM
    • CSS “4”
    • Naming Convention
      • BEM
      • SUIT
    • Atomic CSS
    • CSS in JS
    • CSS Modules
      • Interoperable CSS
    • PostCSS, again
  • My Opinionated Proposal
    • POCss

POCss: Page Override Components CSS

1. Scoping Components
CSS Blocks should only be used inside a component of the same name.

1
2
3
4
5
6
7
8
// Component/index.scss
.ComponentName {
&--mofierName {}
&__decendentName {
&--modifierName {}
}
.isStateOfComponent {}
}
1
2
// Component/index.js
require('./index.scss');

CSS is always bundled with components
(from loading, mount to unmount)

2. Components can be Overrode by Pages
There is always requirements to rewrite styles of components in pages

1
2
3
4
5
6
7
// Pages/PageA.scss
#PageA {
.pagelet-name {
.pagelet-descendent-name {}
}
.ComponentName{ /* override */ }
}
1
2
// Pages/index.js
require('./PageA.scss');
  • #Page for absolutely scoping between pages
  • .pagelet-name should be lowercase to prevent conflicting with components

Why POC?

  • It’s technology-agnostic

    *One css framework can be played with whatever technology stacks*
    *You can combined Scss, PostCSS and whatever you want*
  • Solving problems, and easy

    *Makes reading and teamwork much easier*
    *Get all benefit from BEM, SUITCSS and others*
  • Leverage the power of cascading properly

    *Scoping components but allow reasonable overriding*
    *It's pragmatic, flexible and hitting the sweet spot*

Thanks

Reveal.js

2015 年 9 月,Apple 重磅发布了全新的 iPhone 6s/6s Plus、iPad Pro 与全新的操作系统 watchOS 2 与 tvOS 9(是的,这货居然是第 9 版),加上已经发布的 iOS 9,它们都为前端世界带来了哪些变化呢?作为一个 web 开发者,是时候站在我们的角度来说一说了!

注! 该译文存在大量英文术语,笔者将默认读者知晓 ES6、viewport、native app、webview 等常用前端术语,并不对这些已知术语进行汉语翻译
对于新发布或较新的产品名称与技术术语,诸如 Apple Pen、Split View 等专有名词,笔者将在文中使用其英文名,但会尝试对部分名词进行汉语标注
另外,出于对 wiki 式阅读的偏爱,笔者为您添加了很多额外的链接,方便您查阅文档或出处

简而言之

如果你不想阅读整篇文章,这里为你准备了一个总结:

新的设备特性

  • iPhone 6s 与 6s Plus 拥有 3D Touch,这是一个全新的硬件特性,它可以侦测压力,是一个可以让你拿到手指压力数据的 API
  • iPad Pro 的 viewport 为 1024px,与以往的 iPad 全都不同
  • 想在 iPad Pro 上支持新的 Apple Pen?不好意思,目前似乎并没有适用于网站的 API

新的操作系统特性(与 web 相关的)

  • iPad 上的 Safari 现在可以通过 Split View(分屏视图)与其他应用一起使用,这意味着新的 viewport 尺寸将会越来越常见
  • 新的 Safari View Controller(SFSafariViewController)可以让你在 native app 内提供与 Safari 界面、行为连贯一致的应用内网页浏览体验
  • 注意啦!Safari 新加入了 Content Blocker(内容拦截器)。以后,并不是所有的访问都一定会出现在你的 Google Analytics 了
  • Universal Links 可以让应用的拥有者在 iOS 内部“占有”自己的域名。因此,访问 yourdomain.com 将会打开你的应用(类似 Android 的 Intents 机制)
  • App Search(应用搜索):现在,Apple 将会抓取你的网页内容(与 native app 内容)用于 Spotlight 与 Siri 的搜索结果,想知道你的标签都兼容吗?
  • 你的网站现在可以通过 JavaScript API 访问 iCloud 的用户数据

新的 API 支持

  • Performance Timing API 在 iOS 9 得到回归
  • 关于 HTML5 Video,你现在可以在支持 Picture in Picture(画中画)的 iPad 设备上提供这项新功能;你的视频甚至可以在 Safari 关闭后继续播放
  • 更好的 ES6 支持:classes(类), computed properties(可计算属性), template literals(模版字符串)等
  • Backdrop CSS filters(背景滤镜)
  • CSS @supports 与 CSS Supports JavaScript API
  • CSS Level4 伪选择器
  • 用于支持分页内容的 CSS Scroll Snapping
  • WKWebView 现在可以访问本地文件了
  • 我们仍然需要等待 Push Notification,camera access,Service Workers 这些现代 web API 的到来

新的操作系统

  • 新一代 Apple TV 的 tvOS: 没有浏览器,也没有 webview。但是 JavaScript、XHR 和 DOM 可以通过一个叫做 TVML 的标记语言来使用
  • Apple Watch 的 watchOS:完全没有任何浏览器和 webview

再注! 由于原文写于 Apple 发布会之前,为了不让读者感到奇怪,笔者将会对文章进行适当改写与补充,以保证本文的连贯性

新的 iOS 设备特性

iPhones 6s 与 3D Touch

从 web 设计与开发的角度来说,新的 iPhone 6s 与 6s Plus 与之前的版本并没有太多差别。不过,有一个特性注定会吸引我们的目光:3D Touch

我们无法确定 Apple 是不是只是重命名了一下 “Force Touch”(用于 Apple Watch、TrackPad 2 与最新的 MacBook 上)或者 3D Touch 的确是一个为 iPhone 定制的似曾相识却不同的东西。3D Touch 允许操作系统和应用侦测每一个手指与屏幕接触时的压力。从用户体验的角度来说,最大的变化莫过于当你用点力去触碰或者拖拽屏幕时,操作系统将会触发诸如 peek,pop 这些新机制。那么问题来了:我们是否能够在网站中使用这个新玩意呢?让我们一点点来看:

iOS 9 搭载的 Safari 包含了一些用于 “Force Touch” 的新 API,但它们其实并不是那个用于 iPhone 6s 3D Touch 的 API。你可以理解为这些 API 就是 MacBook 版 Safari 里为 Force Touch 准备的那些 API ,因为共享一套 codebase,所以它理所当然得存在了 iOS 版里而已。

Force Touch API 为我们添加了两个新东西:

  1. 你的 click 事件处理函数将会从 MouseEvent 中收到一个新的属性:webkitForce
  2. DOM 也新增了四个事件:(webkit)mouseforcewillbeginmouseforcedownmouseforceupmouseforcechange。下边的示意图将告诉你这些事件是在何时被触发的:

Force Events

相信你已经从它们的名字中意识到了,这些事件都是基于鼠标而非触摸的,毕竟它们是为 MacBook 设计的。并且,TouchEvent 也并没有包含 webkitForce 这个属性,它仅仅存在于 MouseEvent 里。在 iOS Safari 里,你确实可以找到 onwebkitmouseforce 这一系列事件处理器,但是很可惜它们并不会被触发,click 返回的 MouseEvent 也永远只能得到一个 webkitForce: 0

可喜可贺的是,故事还没有结束。Touch Events v2 draft spec(触摸事件第二版草案) 中正式添加了 force 属性。3D Touch 也得以在 iPhone 6s 与 6s+ 中通过 TouchEvent 访问到。不过,笔者也要在这里提醒大家,由于没有 webkitmouseforcechange 这样给力的事件,在手机上我们只能通过 轮询 TouchEvent 的做法 来不断检测压力值的改变……非常坑爹

@Marcel Freinbichler 第一个在 Twitter 上晒出了自己的 Demo。在 6s 或 new Macbook 的 Safari(目前仅 Safari 支持)上访问就可以看到圆圈会随着压力放大。墙内的小伙伴可以直接试试下面这个圆圈,体验下 3D/Force Touch 带来的的奇妙体验。

如果你不巧在用不支持 3D/Force Touch 的设备,发现尼玛用力按下去之后居然圆圈也有反映!?

放心,这真的不是你的设备突然习得了“感应压力”这项技能,而是因为 Forcify 是一个用于在所有设备上 polyfill 3D/Force Touch API 的 JS 库……它不但封装了 OSX/iOS 两个平台之间 API 的差异,还使用”长按”来模拟了 force 值的变化……

iPad Pro

全新的 iPad Pro(12.9 寸)打破了以往 iPad 渲染网站的方式。在此之前,市面上所有的 iPad(从初代 iPad,到 iPad Air 4,到 iPad Mini)都是以 768px 的宽度提供 viewport。

而屏幕更大的 iPad Pro 选择了宽 1024px 的 viewport,这使得它天生就能容纳更多的内容。不少人说iPad Pro 就是抄 Microsft Surface Pro 的嘛……嗯哼,IE/Edge 在 Surface Pro 上就是以 1024px 作为视口宽度的……

从交互的角度上来说,iPad Pro 虽然不支持 3D Touch,但是可以搭配 Smart Keyboard 与/或 Apple Pen(带有压力侦测)使用。对于键盘其实并没有什么好说的,如果一个网站在搭配键盘的桌面电脑上好用,它在 iPad Pro 上应该也不赖。而对于 Apple Pen,很可惜,目前似乎并没有 API 能让你在网站上获得这根笔的压力与角度。

新的 iOS 操作系统特性

iPad 上的多任务处理

自 iOS 9 起,iPad 允许两个应用在同一时刻并肩执行,有三种方式:Slide OverSplit ViewPicture-in-Picture。不过,每一种方式都有其硬件需求,比如说 Slide Over 需要 iPad Air, iPad Mini 2 以上的设备,而 Split View 由于对内存的要求目前只支持 iPad Air 2 与 iPad Pro。

Slide Over(滑过来!)

Slide Over 支持的 App 并不多,不过 Safari 名列其中,这意味着我们的网站将可能在这个模式下被渲染。当网站处于 Slide Over 模式下时,它将在屏幕的右 1/4 位置渲染,并且置于其他 native app 之上。

这个模式也为 Responsive Web Design(响应式网站设计)提出了新的挑战:一个只为 iPad 优化的网站,也需要能在该设备上以无需手动刷新的形式支持小屏幕的渲染。因此,如果你正在使用服务器端探测(RESS),那么你的 iPad 版本需要以某种方式包含手机版本的网站,或者在进入该模式后重新加载一次。(如果你不了解 RESS,你可以观看我的另一篇博文

Slide Over

在这个模式下,无论横屏还是竖屏,所有的 iPad(包括 Pro)都会把你的网站以 320px 的 viewport 宽度进行渲染,就好像在一个大 iPhone 5 上一样。你可以在 CSS 中通过 media query(媒体查询)探测到这个模式:

1
2
3
4
/* iPad Air or iPad Mini */
(device-width: 768px) and (width: 320px)
/* iPad Pro */
(device-width: 1024px) and (width: 320px)

Split View(分屏视图)

在较新版本的 iPad 上,你可以将 Slide Over 的 Side View(侧视图)升级为 Split View。此时,两个应用将以相同比例在你的屏幕上同时工作。

在这个模式下,我们的网站将可能……

  • 以屏幕 1/3 比例渲染时,viewport 在 iPad Air/mini 犹如 iPhone 5,宽 320px。而在 iPad Pro 上则像是 iPhone 6:宽 375px
  • 以屏幕 1/2 比例渲染时,viewport 在 iPad Air/mini 上呈现为 507px 宽,而在 iPad Pro(横屏)下呈现为 678px 宽
  • 以屏幕 2/3 比例渲染时,viewport 在 iPad Air/mini 上呈现为 694px 宽,而在 iPad Pro(横屏)下呈现为 981px 宽

Split View

Picture in Picture(画中画)

在一些较新版本的 iPad 上,使用 HTML5 video 标签的网站可以将其暴露到 Picture in Picture 机制中。通过 API(本文稍后会讲)或用户的触发,视频可以独立于网站在其他应用的上方继续播放。

Picture in Picture

iOS 9 下的响应式网页设计

下图向你展示了 iOS 9 所有可能的 viewport 尺寸,检查检查你的响应式断点都包含它们了吗?

iOS 9 RWD

Safari View Controller

如果你用过 Twitter 或者 Facebook(或者微信,微博……),那么你一定知道很多 native app 在打开一个网页链接时并不会默认使用 Safari。它们试图让你留在它们的应用里,所以通过提供 webview 让你在应用内进行网页浏览。可是问题在于,这类 webview 并不会与浏览器共享 cookies,sessions,autofill(自动填充)与 bookmark(书签),为了解决这些问题,就有了 Safari View Controller。

现在,native app 可以使用 Safari View Controller 来打开网站,它提供与 Safari 完全一致的隐私政策、local storage,cookies、sessions 同时让用户留在你的 app 中,它通过一个 “Done”(完成)按钮使用户可以回到 native app 的上一个 controller。这个全新的 controller 还可以让我们在 Share(分享)按钮上添加自定义的操作,这些操作在用户使用 Safari 应用时并不会出现。同时,native app 对这个自定义 Safari 实例具有完全的内容控制,你可以屏蔽不想被渲染的内容。

当你需要基于 web 的鉴权,比如 OAuth 时,使用 Safari View Controller 同样是一个好主意,这样就不再需要打开浏览器再重定向回你的应用。不过注意了,Safari View Controller 只适用于在线、公开的 web 内容。如果你的 web 内容假设在本地或者私服,那么 WKWebView 仍然是最推荐的选择。

笔者八卦一下,Safari View Controller 实际上也算是半个社区推进的产物。早在 2014 年 12 月,Tumblr 的 iOS 工程师 Bryan 就发表了一篇著名的 We need a “Safari view controller” 叙述现有 webview 在第三方登录鉴权时的窘境。
2015 年 6 月,Apple Safari 工程师 Ricky Mondello 的 Twitter 宣告了这个设想的落地:You all asked for it. Come see me introduce it. Introducing Safari View Controller 1:30 PM, Tuesday. Nob Hill.

Safari Content Blockers

现在,iOS 9 上的 Safari 支持一种全新的 App Extensions(应用拓展):Content Blocker(内容拦截器)。这类拓展以 native app 的形式存在,你可以在 App Store 上下载到,它们可以拦截 Safari 内的任何内容,包括:跟踪器、广告、自定义字体、大图片、JavaScript 文件等等。

作为 web 开发者,尽管我们不能禁用 Content Blocker,我们仍然应该注意到它们的存在。诸如 Crystal 的一些拦截器宣称他们可以提高网页的打开速度。Crystal 声称可以加快网页的加载速度 3.9 倍并且少用 53% 的带宽。不过问题是:到底哪些东西被拦截器拦截了?这篇文章提到了一些我们未来可能会遇到的问题。

crystal

在 iOS 9 发布后,Peace,一个 Content Blocker,曾在 App Store 排名跻身前十。从用户的角度来说,如果一个网站由于被 Content Blocker 拦截了某些重要资源而不能正常工作,你可以长按重新加载按钮并且以不启用 Content Blocker 的方式重新加载这个网站(见下图,来自 MacWorld.com)

disable content blocker

Content Blocker 能隐藏元素,也有能力通过 CSS 选择器、域名、类型、或者 URL 来过滤并拦截某个文件的加载,Purify Blocker 给用户提供了拦截某一种内容类型的进阶选项,比如 Web Fonts。

purify

WKWebView 的增强

UIWebView 已经被官方弃用,虽然它还在在那,不过它再也不会得到什么升级。与此相反,WKWebView 正在取代它的位置。一个最受期待的特性现在终于推出:加载本地文件到 WKWebView。因此,现在 Apache Cordova 应用与其他 web 内容都可以直接从 iOS 包中使用本地文件了,不再需要各种诡异的 hack 了。

此外,还有一些新特性也一并推出。比如说,通过 WKWebsiteDataStore,Objective-C 或 Swift 有能力查询与管理 webview 的本地存储(比如 localStorage 或 IndexedDB)。这就允许我们将原有的数据存储替换成新的某些东西,比如说替换成一个不永久的(Chrome for iOS 的隐身模式就需要这种东西)

Universal Links(通用链接)

如果你既有一个网站,又有一个 native app,你现在可以通过 Universal Links 来增强用户体验。它允许你在操作系统内“占有”自己的域名,这样,一切指向你网站的链接都会被重定向到你的 app。

目前,所有的 app 都是通过自定义 URI 来达到这个效果的,比如 comgooglemaps:// 就可以用来从网站或者其他原生 iOS 应用中打开 Google Maps。

想要提供这个特性的话,你首先需要在 native app 中实现 Deep Linking(深度链接),让应用中的内容与 Safari 的 URL 吻合。然后,你需要在 Apple 的网站上关联你的域名,取得这个域名的 SSL 认证并且把签名后的 JSON 部署到该域名上。这是为了防止第三方的应用“占据”了属于你而不属于他们的域名,比如说 twitter.com 被非 Twitter 的其他应用占据掉。

目前唯一的缺点是用户好像并不能决定到底以哪种方式来打开内容(使用 web 还是 app),不过我们可以观望一段时间看看它会如何发展。在不远的这段时间里,你可能会发现在网站或 Google 搜索里点击一个链接时会没有任何预警的就跳进了 native app 里。

App Search(应用搜索)

Apple 带着自己的 web 蜘蛛杀进了搜索的市场,而我们需要支持它得以在 Siri 与 Spotlight 中提升自己的曝光率。这在我们同时拥有网站与 app 时尤为重要,因为现在 Apple 会索引你网站的内容,但打开时却可能将用户带到了 app 里去。

尽管这会开启 SEO 的新篇章,不过却相当容易。你需要使用一些标签标准,诸如 Web SchemaAppLinksOpenGraph 或者 Twitter Cards,配合上 App Banner 与 app-argument,如果你有你自己的 native app 的话。

关于“让你的网页支持 Apple 搜索”的更多详情,请查阅 Apple 官方文档 Mark Up Web Content

Apple 刚刚发布了一个 App Search Validation Tool(应用搜索验证工具)来帮助你搞清楚,需要向你的网站添加什么才能支持 App Search

App Search

CloudKit JS

如果你拥有一个 native app,你很可能会将用户数据保存在 iCloud 上。在过去,只有 iOS 与 Mac 应用被允许使用它。现在,通过 CloudKit JS,你的网站也可以连接上 iCloud 数据了。

Back Button

现在,当你链接到一个 native app 时(通过自定义 URI 或者 Universal Link),Safari 会询问用户是否想要使用 native app 打开这个链接(见下图)。如果用户同意了,这个应用将被打开,并且在左上角会有一个返回按钮可以返回 Safari ,返回到你的网站。

backbutton

新的 API 支持

Navigation Timing API 在 iOS 9 迎来了回归。让我们回忆一下,这货添加于 8.0 却在一周后的 8.1 中去掉了。这对于 Web 性能是个好消息。通过这个 API,我们可以更精确的测量时间,还可以获得一系列有关加载过程的时间戳,它们对于追踪与在真实场景中做决策来改进用户体验都非常有用。

Picture in Picture

PiP API(被称为 Presentation Mode API)目前只支持 iOS,它允许我们手动让一个 <video> 元素进入或离开 PiP 模式如果 video.webkitSupportsPresentationMode 是支持的。

举个例子,我们可以在内嵌模式与 PiP 模式中切换:

1
2
3
4
5
video.webkitSetPresentationMode(
video.webkitPresentationMode === "picture-in-picture" ?
"inline" :
"picture-in-picture"
);

我们还可以通过新的 onwebkitpresentationmodechanged 事件来检测 Presentation Mode(展示模式)的变化。

Backdrop CSS

iOS 7 与最近的 Mac OS 使用 Backdrop filter(背景滤镜)来模糊背景(指 native 开发),而在网站上实现这个却并不容易。

iOS 9 上的 Safari 现在支持了来自 Filter Effect v2 spec(滤镜特效第二版规范)的 backdrop-filter。比如说,我们可以使用一个半透明的背景并且对其背后的背景使用滤镜:

1
2
3
4
5
header {
background-color: rgba(255, 255, 255, 0.4);
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
}

backdrop

CSS Scroll Snapping

在 web 上实现分页内容(比如相册跑马灯)总是非常麻烦,无论是使用 JavaScript 框架、touch 事件还是 hacking 滚动条等等。Apple 新添加了一个很赞的 CSS 特性叫做 CSS Scroll Snapping。这个特性新增了一系列的 CSS 属性让你定义规则或者不规则的 snap zone(停留区域),这样滚动的位置就会“啪”地一下停在这个区域,而非像以前一样可以停在任何地方。

来看个例子:

1
2
3
4
5
6
#photo-gallery{
width: 100%;
overflow-x: scroll;
-webkit-scroll-snap-points-x: repeat(100%);
-webkit-scroll-snap-type: mandatory;
}

想要看个跑起来后的例子?笔者为大家准备了 webkit 的官方 demo,不过这个属性目前只支持 iOS 9 Safari 哦,并不支持 webview

CSS Supports

CSS Supports,包括 CSS @supports 与来自 CSS Conditional Rules Module Level 3 spec 的 JavaScript CSS Supports API 都在 iOS 上迎来降临。现在,我们可以针对某个 CSS 属性的特定值的支持情况来编写代码:

1
2
3
@supports(-webkit-scroll-snap-type: mandatory) {
/* we use it */
}

同样,使用 JavaScript:

1
if (CSS.supports("-webkit-scroll-snap-type", "mandatory")) {}

一些细微的改进

  • ECMAScript 6 的更完善支持:classed、computed properties、template literial 与 week sets
  • 新的 CSS Level4 伪类/元素选择器::not:matches:any-link:placeholder-shown:read-write:read-only
  • Native app 现在可以通过 extension 来向 Safari 的 Shared Links(分享链接)窗口上注入信息
  • 大量无前缀 CSS 属性的支持(终于),比如 transition、animation、@keyframes、flex 与 columns
  • Mac OS El Capitán 上的 Safari 9 提供了一个全新设计的 Web Inspector(Web 检查器)。幸运的是,iOS 9 的远程调试完全兼容 Mac OS 上的 Safari 8,所以你倒是不用急着升级你的 Mac OS
  • iOS 9 通过 -apple-font 加入了一些 Dynamic Fonts(动态字体),并且它们现在应用的是 Apple 的新字体:San Francisco,笔者的博客就已经用上它啦
  • scrollingElement 现在可用了
  • <input type=file> 现在允许你从 iCloud Drive 与已安装的第三方应用,比如 Google Drive 中选择文件
input file
  • 当你加载一个 HTTPS 协议的页面时,你不能混用 HTTP 与 HTTPS 的资源

Bugs

Bug 通常都要在几周之后才会显露出来,我也会持续跟进并更新这篇文章。目前为止,我的发现如下:

  • 对于 Home Screen webapps(添加至主屏的 web 应用),apple-mobile-web-app-status-bar-style 这个 meta 标签不起作用了!所以你现在不能再像过去一样使用 black-translucent 让你的 webapp 渲染在状态栏的后面了。(iOS 9.2 fixed 了这个 bug)
  • Speech Synthesis API (语音综合 API)不再工作了

仍在等待……

当 Mac 上的 Safari、桌面电脑与 Android 上的 Chrome 都已经为网站支持 Push Notification (通知推送)时,iOS 上的 Safari 仍然不支持这个特性。就 API 而言,我们仍然没有:WebRTC、getUserMedia、Service Worker、FileSystem API、Network Information API、Battery Status API、Vibration API 等等……你又在 iOS 上等待哪些特性呢?

watchOS 与 tvOS

新发布的 watchOS 2.0 与 tvOS 9.0 都是基于 iOS 的操作系统,它们针对特定的设备发行(Apple Watch 与新的 Apple TV)。从用户的角度来说,那里并没有浏览器了。从开发者的角度,那里也没有 Webview 了。

尽管有不少人抱怨(大部分都是针对 webview 的缺失),我并不能确定这是不是个坏主意。我猜测 Apple 会尝试通过 Siri 来将 “web” 带给 TV、手表、甚至 CarPlay 的用户。所以,如果你遵循了上述的 “App Search” 的步骤,你的内容将可能通过 Siri 在这些设备上以 widget(小部件)或者快捷回复的形式变得可以访问。

对于 Apple TV ,它支持使用 JavaScript、DOM API 与 XMLHttpRequest 来让我们构建某种类似 Client-Server webapp 的东西。没有 HTML 和 CSS,这是什么把戏?其实它支持的叫 TVML,是一种基于 XML、为那些可以被渲染在 TV 屏幕上的特定内容而优化后的标签。这些标签只可以在来自应用商店的 native app 中渲染,但是这些 TVML 是由服务器端来生成的。

著作权声明

本文译自 iOS 9, Safari and the Web: 3D Touch, new Responsive Web Design, Native integration and HTML5 APIs — Breaking the Mobile Web
译者 黄玄,首次发布于 Hux Blog,转载请保留以上链接

0%