(删掉了第一小段,因为和内容关系不大。。)
本来这该是个技术分享会的内容,参加完 Google Developer Day(GDD) 后想做个 AI Demo 来分享,奈何技术实力不够,害怕把大家带进沟里,想想还是讲比较擅长的前端领域的比较靠谱。等哪天经历过人工智能相关项目后再分享也不迟。(分享会已经开过了,这是PPT)
图:在 GDD 上海站围观 TensorFlow 与艺术分享会
Progressive Web Apps
Progressive Web Apps(PWA)指代那些使用最新的 Web 技术构建的类似原生 App 体验的 webapp,这也是今年前端最热词之一,虽然感觉说来说去就是为了一堆新技术取个名字,就像之前的 HTML5 一样。不过,这些技术确确实实提高 Web 的用户体验。
为此各大浏览器厂商都卯足了劲(我是说除了Safari,连 IE 都在努力了啊喂!),这篇文章将从三个方面来讲解这些技术:
可靠,即使网络不稳定也需要提供服务;
更快;
功能更丰富!
(恰好 PWA 也这么认为)
这些都是为了让 WebApp 能像 App 一样提供沉浸式的用户体验。
技术是为内容服务的,单纯的技术没法让我们爬到馬斯洛需求理論的更高层,但是技术一直在为实现它而努力。接下来将要介绍的就是2016年各大浏览器厂商为前端技术做出的努力。(本文不包括类似 async 函数、Promise、以及 Websocket 之类的一些“老技术”)
图:只有Safari还在考虑是否实现 Service worker,连 IE 都在努力你 Safari 凭啥不努力。。。
可靠
技术关键词:Service Worker,Background Sync
相对于 App , Web 最痛的点在于如果网络不靠谱,一切无从谈起。打开任何一个页面,WebApp 都需要经过下载、解析并初始化代码、执行路由逻辑跳转到相应页面、请求网络数据、渲染页面...而性能瓶颈最容易出现在网络部分。设想如果可以预先将网页的外壳安装到设备上,启动时从本地加载这层壳,并请求网络获取渲染需要的数据,甚至更极端一点,数据全存在本地磁盘,启动时优先使用的本地已经保存同步好的数据,保证即使在 Lie-Fi* 环境下也能提供正常服务。
注:Lie-Fi是比断网更要命的移动网络。症状是手机告诉你你已经联网,但是网页却打不开,甚至连“Chrome小恐龙”都不让玩。
图: 这就是那只“Chrome小恐龙”
在讲 Service worker 之前,浏览器中已经实现了一个类似的功能,叫 AppCache ,AppCache 的运行原理不复杂,就是在网页加载完成后,浏览器闲下来的时下载一个清单文件,这个文件列明了哪些资源可以离线使用,哪些资源需要请求网络,如果请求不到网络该怎么办等等。类似一个配置清单。
图:AppCache在首次启动页面时缓存数据到磁盘,此后打开的页面如果有用到已缓存的资源则直接由缓存提供。
不过,HTTP 缓存在上古时期就实现了这个功能,看起来只是换了个马甲。
的确,理论上 HTTP 缓存完全能实现这个功能,但实际却不是这样,由于浏览器厂商很多、版本也多,相互之间兼容性或多或少有些差异,如果某个浏览器对程序员的失误容忍程度过低,很容易导致渲染出现问题。类似问题也出现在 HTTP 请求上,如果浏览器严格按照请求头的缓存控制规则来控制网络请求,很容易导致页面无法更新或者部分更新、新旧代码混合的问题。所以大部分浏览器倾向不完全相信 HTTP 请求头。这也从另一个方面印证了“单一职责原则“的重要性(传输数据的就好好传输数据,别抢人家缓存的饭碗,你也抢不来。。)。
嗯,看起来 AppCache 比 HTTP 缓存棒多了,功能强大而且单一职责,只要一份清单文件就能把资源都缓存起来离线访问。
但AppCache犯了比HTTP严重的多的错误!
我们知道,AppCache的确是单一职责,意味着浏览器必须信任他,如果不相信那它存在的意义是什么?问题在于,假设程序员犯了一个小错,一个匹配规则不小心匹配到了这个清单文件,你的代码将永远都更新不了!!
代码不小心犯个错简直太正常不过了,就好像程序员需要吃饭睡觉上厕所一样,程序员也需要会写bug。。
延伸阅读:AppCache Issue is a Douchebag
好的,冷静一下。。其实有个不完美的解决方案,通过脚本自动生成缓存规则,具体可以参考 offline-plugin AppCache 部分的做法,但这样 AppCache 的其他“好处”就完全体现无法体现出来!
这时候 Service worker 该登场了!简单来说,Service worker 是一段运行在页面之外的一段脚本,这段脚本在启动时会注册一些事件到浏览器中,当事件触发时可以通过预先定义好的 Javascript handler 控制如何响应这个事件。
但为什么说 Service worker 比 AppCache 好呢?
首先 Service worker 应该说彻底解决了 AppCache 无法更新的问题,在使用 Service worker 时,只要网络畅通,浏览器都会尝试在启动后检查 Service worker 是否有更新,而且 Service worker 并不能截获请求更新 Service worker 的事件,换句话说就是无法缓存自身,也就是永远都有机会更新自己。
其次,Service Worker 通过事件机制配合 js 脚本来控制浏览器,并且脱离于页面生存,这让web终于有了“常驻”系统的机会!
这是技术上的一小步,但是却给 web 的能力提供了无限的想象空间!
为了更好的理解什么是 Service Worker,我们可以把它想象成是生存于浏览器页面中、页面之外的动态代理,当类似网络请求之类的事件触发时,由这个代理决定如何处置这个事件。下面这个例子就是演示如何截获并处理网络请求的。
图:浏览器向服务器发起请求前先被 Service worker 截获,处理完成后再向远端服务器发起请求
这里有个小视频,演示了如何通过 Service worker “凭空捏造” 离线数据?。代码在https://github.com/MofeLee/sw...
既然服务器没有的数据都能造出来,离线访问数据岂不是轻而易举?并且这个例子实际上只有几行关键代码????
navigator.serviceWorker.register('/sw.js')
二、 注册响应规则
self.addEventListener('fetch', function(event) {
if (/offline-data$/.test(event.request.url)) {
event.respondWith(
new Response('这是一条来自service worker的响应信息')
);
}
});
三、没有第三步。。。
实际上现在已经有很多网页可以离线访问了,比如说大名鼎鼎的lodash,如果想知道你平常访问的哪些网站是离线可访问的,可以在浏览器地址栏输入 chrome://serviceworker-internals/ 查看~
图:可以看到 https://lodash.com/ 是注册了service worker 到chrome中的,试着打开网址,稍微等下再断网打开 https://lodash.com/ ~ Boom!
到目前为止,我们知道使用 Service Worker 可以解决在没有网络的情况加载应用的场景,现在,我们设想另一种场景:在线聊天。
图:离线情况下发送微信消息,等网络正常后微信会继续处理我们的消息。
无论是在微信中还是手机短信,在没有信号时都不影响我们编辑发送短信,等网络恢复时 App 会自动帮我们把之前编辑好的信息顺利递送出去。而移动设备中的 webapp 体验却糟糕透了,我们必须开着网页直到网络恢复,以便发出我们的消息,假设这时候我们切换到其他App中,不但消息发不出去,很有可能连浏览器占用的资源都会被操作系统回收!!
问题究竟出在哪?
仔细思考后会发现问题出在网络请求生命周期和页面的生命周期耦合了,我们必须要找到一种方法能让浏览器在关掉页面后也能继续为我们继续工作!
但是,Webapp 不像普通的 App, 有权限在退出应用之后继续在后台运行,如何才能既安全又高效的在后台运行程序成了一个比较关键的问题,如果放出权限让 js 持续在后台运行势必会影响整个操作系统的效率(没有人能写出没有bug的程序,只是访问了一个网页,却强奸了我的系统。。当然不能忍。。)。所以 Service worker 把后台应用的权限局限在"事件"级别,这里的事件就是前文提到的 Service worker 在浏览器启动时注册的事件。后台脚本是否执行完全取决于浏览器是否唤醒它,这也意味着如果这段脚本很烦人,我们可以通过设置关掉这段脚本执行的入口,并且,统一管理所有事件的触发也有利于提高执行效率,毕竟让专业的“人”做专业的事更靠谱~
同样的,Background Sync 也是Service worker 事件之一,它让我们有机会在退出浏览器后也能继续执行一些功能。
是不是急不可耐的想看一段演示了呢?
带梯子可以访问这个链接 图片打不开点这
图:断网后发送的消息,联网后自动发送到服务器
图:演示流程
断网
注册Background sync
联网触发sync事件
截获sync事件,弹出弹窗
实际上,除了这些,Service worker 还可以完成很多有趣的东西,比如制作一个网页,网页的header和footer由缓存提供,中间的content从网络上获取。如果对Service worker的其他功能感兴趣,可以看看这个视频 Instant-loading Offline-first (Progressive Web App Summit 2016)(需自带梯子)
更快
技术关键词:WebAssembly,passive event listeners,requestIdleCallback
实际上,web的痛点不止在网络层面上,相对于 native,js的执行效率也是硬伤之一,虽然js在操作 DOM 上得心应手,但在处理大量数据或者图形计算上,依旧比较吃力,以至于web平台上很难有比较华丽动作游戏。直到WebAssembly的出现,让 web 平台实现炫酷的动画效果成为可能。WebAssembly可以让我们在浏览器中执行C或者C++已经编译出来的字节码。但有一点需要注意的是,WebAssembly的出现并不是为了替代 js,更多的是用来编写高性能的模块,替换js中容易出现性能瓶颈的地方。
图: 试玩WebAssembly 试玩链接
鉴于 WebAssembly 目前还处于试验阶段,这里不细讲相关的代码如何实现了。有兴趣可以点击后面的链接了解更多内容 How Webassembly Will Change the Way You Write Javascript | Seth Samuel(需自带梯子) demo源代码
除了 js 的执行效率外,流畅的屏幕滚动体验对web来说非常重要,这点在基于触摸的移动设备体现的尤为明显,但事实上,web浏览器中很容易在屏幕滚动上出现问题,如果细心的话,会发现即使在设置一个空函数为event handler也可能会造成滚动不流畅。
问题出现在滚动时我们可以调用preventDefault来取消滚动,即使完全没有调用preventDefault,浏览器页需要在每次滚动前先等待handler调用结束,以确定我们的确没有调用preventDefault来阻止滚动。
但实际情况却是大多数handler都只是需要获取滚动的位置信息,监听与否并不会影响滚动这个动作的发生,我们完全让屏幕一边滚着一边调用回调函数。所以,只要明确告诉浏览器这个监听事件是不会影响浏览器滚动的,对滚动事件的执行效率有非常大的提升。
为了解释清楚这其中的区别,我再举个例子,游戏中的高楼,即使再高你也会毫不犹豫的往下跳,现实中的蹦极,虽然前人无数次的验证过安全也会犹豫担心。
明确知道安全:无所畏惧
潜在可能危险:畏首畏尾
这就是“明确知道”和“潜在可能”的差别,也是passive event能提升浏览器效率的原因。
当然,Passive Event Listeners的接口非常简单,只需要将第三个参数设置为{passive: true}
即可。
var elem = document.getElementById('elem');
elem.addEventListener('touchmove', function listener() {
/* do something */
}, { passive: true });
图:对比执行效率的差别,右边的listener设置了passive属性
了解更多相关passive event listeners的信息
上面都是一些主动提高web执行效率的方法,下面要讲的是利用好浏览器的空闲时间。
在web开发中,更快的显示出有内容的首屏非常重要,尤其对于webapp来说。相对于传统web页面,webapp在初始化时需要占用大量的时间运行脚本,并且大多数 webapp 在第一次打开时执行的脚本远远多于后续的页面,如何分配好脚本的执行时间也是非常重要的课题。
图:react webapp 在启动前500ms需要进行非常密集的计算。
setTimeout(()=>{
const head = document.getElementsByTagName("head")[0];
const gajs = document.createElement("script");
gajs.async = 1;
gajs.src = "https://www.google-analytics.com/analytics.js";
head.insertBefore(gajs, head.lastChild);
}, 2000);
这是一段等浏览器空闲下来再去加载google analytics的脚本。
相对于动画以及加载首屏页面,加载第三方统计代码的优先级低得多,所以设置2000ms对于大多数场景够用了,但事实上我们完全无法保证timer触发时浏览器处于空闲状态。
如果对于 JavaScript 足够了解的话,应该知道浏览器中的 Timer 是在浏览器的调用栈清空时才会从回调队列中取setTimeout推入的回调函数。但调用栈清空是浏览器空闲的假象,很有可能下一刻就要执行类似requestAnimationFrame注册的回调函数,但是我们却无法查询到已经注册的回调函数会在何时触发。
图:年初做的视频教程截图,下方为回调队列,事件轮询在调用栈清空时会从回调队列取函数出来运行
实际上,真正空闲的时候是在调用栈清空并且Web Api中注册的没有需要立刻推入回调队列的回调函数时。结合下面的图片可能更容易理解上面这句话
图:图中的idel period才是浏览器真正空闲的时候。
requestIdleCallback 提供的接口也非常简单
var handle = window.requestIdleCallback(callback[, options])
如果想了解更多关于requestIdleCallback的内容,可以查看Using requestIdleCallback和Cooperative Scheduling of Background Tasks
功能更丰富
讲完了前面相对来说比较复杂的api,终于可以讲些轻松的内容了,如果是比较简单的内容我就一笔带过啦~(点击右侧导航栏快速跳转到相应内容
Fullscreen Api
全屏Api,可以将类似视频图片或者Canvas之类的元素占满全屏demo
code
var i = document.getElementById("myimage");
// click event handler
i.onclick = function() {
// in full-screen?
if (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
) {
// exit full-screen
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
// go full-screen
if (i.requestFullscreen) {
this.requestFullscreen();
} else if (i.webkitRequestFullscreen) {
i.webkitRequestFullscreen();
} else if (i.mozRequestFullScreen) {
i.mozRequestFullScreen();
} else if (i.msRequestFullscreen) {
i.msRequestFullscreen();
}
}
}
Media Recorder
Media Recorder API 让我们的网页实现录制流媒体的能力。
给个相对简单的demo以及源代码地址:source code demo
延伸阅读:Record Audio and Video with MediaRecorder
(写到这发现可以直接嵌入视频。吐血中。让我静静。。。。???)
Media Source Extensions
另一个比较有用的接口就是 Media source extensions,它让我们可以在 web 上有机会播放任意格式的视频格式,著名的 flv.js 就是靠这个接口实现的。另外,由于视频的内容可以完全用脚本控制,我们可以使用这个接口完成类似播放加密的视频流、限制视频流的加载速度、根据缓冲的快慢自动切换视频流的源、web页面视频聊天等等等等~
说点题外话,直播现在在国内这么火爆,但是直播流的格式兼容性却还有些小问题,要想一个格式支持全平台就会涉及到相关 media source extensions 的接口。flv支持桌面平台,移动设备支持m3u8,一个方案是桌面用flv.js直播,但在实际使用中发现iOS不支持media source extensions,无法使用flv.js直播;另一个方案是在桌面环境使用 media source extensions 将 m3u8 转换成浏览器支持的直播流格式,实际使用中发现后面这个方案还是更靠谱,而且轮子都已经造好了,有兴趣可以看看 videojs-contrib-hls
图:使用 media source extensions 分片加载音频内容。source code demo
Offscreen Canvas
可以在 web worker 上渲染canvas的接口,没有 demo 因为还没有支持的浏览器。。(就算有那应该怎么演示??)
example:
<canvas id="one"></canvas>
<canvas id="two"></canvas>
var one = document.getElementById("one").getContext("bitmaprenderer");
var two = document.getElementById("two").getContext("bitmaprenderer");
var offscreen = new OffscreenCanvas(256, 256);
var gl = offscreen.getContext('webgl');
// ... some drawing for the first canvas using the gl context ...
// Commit rendering to the first canvas
var bitmapOne = offscreen.transferToImageBitmap();
one.transferImageBitmap(bitmapOne);
// ... some more drawing for the second canvas using the gl context ...
// Commit rendering to the second canvas
var bitmapTwo = offscreen.transferToImageBitmap();
two.transferImageBitmap(bitmapTwo);
延伸阅读:OffscreenCanvas
Pointer Events
pointer lock
web bluetooth api
webrtc
canvas media capture
延伸阅读
原创博文,转载写明出处就好(是的,出处就是这个页面)。