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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
|
---
title: PWA 结构
slug: Web/Progressive_web_apps/App_structure
translation_of: Web/Progressive_web_apps/App_structure
---
<div>{{PreviousMenuNext("Web/Apps/Progressive/Introduction", "Web/Progressive_web_apps/Offline_Service_workers", "Web/Progressive_web_apps")}}</div>
<p class="summary">现在,我们已经知道了 PWA 背后的原理, 让我们来看一个推荐的 PWA 结构,这个案例来自一个真实的应用。我们从分析 <a href="https://mdn.github.io/pwa-examples/js13kpwa/">js13kPWA</a> 这个应用开始:为什么它要这样构建?这样做又有什么好处?</p>
<h2 id="应用架构">应用架构</h2>
<p>渲染网站主要有两种方法 - 在服务器上或在客户端上。它们都有其优点和缺点,你可以适当地混合使用这两种方法</p>
<ul>
<li>服务器端渲染(SSR)的意思是在服务器上渲染网页,因此首次加载会更快,但是在不同页面之间导航都需要下载新的HTML内容。它的跨浏览器兼容性良好,但代价是页间加载时间延长,也就是总体感知上的性能降低:每加载一个页面,都需要一个服务器请求往返的时间。</li>
<li>客户端渲染(CSR)允许在导航到不同页面时几乎立即在浏览器中更新网站,但在开始时需要更多的初始下载和客户端上的额外渲染。 首次访问时网站速度较慢,但后续访问速度要快得多。</li>
</ul>
<p>将 SSR 与 CSR 混用可以获得最佳效果:您可以在服务器上渲染网站,缓存其内容,然后在客户端需要时更新渲染。因为使用了 SSR,第一页加载很快;因为客户端可以仅使用已更改的部分重新渲染页面,所以页面之间的导航也是平滑的。</p>
<p>您可以按自己喜欢的方式构建 PWA,但有些方式更合适。最流行的是“App Shell”概念,它完全按照上述方式混用 SSR 和 CSR;此外还遵循“离线优先”方法,这个我们将在后续文章中详细解释,也会在示例应用程序中使用。我们还会简要提及另一种涉及<a href="/en-US/docs/Web/API/Streams_API">Streams API</a>的新方法</p>
<h2 id="App_shell_概念">App shell 概念</h2>
<p>App shell 概念试图尽快加载最小用户界面,然后缓存它,以便在后续访问时可以离线使用,然后再加载应用程序的所有内容。这样,下次有人从设备访问应用程序时,UI 立即从缓存加载;如果缓存数据不可用的话,就从服务器请求新内容。</p>
<p>这种结构的页面很快,给用户的感觉也很快:用户会立即看到内容而不是加载动画或空白页。如果网络连接不可用,它还允许离线访问网站。</p>
<p>我们可以通过 <a href="/en-US/docs/Web/API/Service_Worker_API">service worker</a> 控制从服务器请求的内容以及从缓存中检索的内容,这将在下一篇文章中详细解释。现在让我们关注这个结构本身。</p>
<h3 id="我为什么要用它?">我为什么要用它?</h3>
<p><span class="tlid-translation translation" lang="zh-CN"><span class="alt-edited">这种架构允许网站从 PWA 功能中获益最多:它可以缓存</span><span title=""> app shell</span><span class="alt-edited"> 并以提升大量性能的方式管理动态内容。除了基本外壳之外,您还可以添加其他功能,例如</span></span><a href="/en-US/docs/Web/Apps/Progressive/Add_to_home_screen">添加到主屏幕</a><span class="tlid-translation translation" lang="zh-CN"><span class="alt-edited">或</span></span><a href="/en-US/docs/Web/API/Push_API">推送通知</a><span class="tlid-translation translation" lang="zh-CN"><span class="alt-edited">。即使用户的浏览器不支持这些功能,你也可以放心应用可以正常运行,这就是渐进增强的美妙之处。</span></span></p>
<p><span class="tlid-translation translation" lang="zh-CN"><span title="">在不妥协 web 优势的前提下,网站感觉就像一个原生应用,交互及时、性能可靠。</span></span></p>
<h3 id="可链接、渐进式和响应式">可链接、渐进式和响应式</h3>
<p><span class="tlid-translation translation" lang="zh-CN"><span title="">记住 PWA 的优点并在设计应用程序时牢记这一点非常重要。</span> <span title="">app shell 方案允许网站:</span></span></p>
<ul>
<li>可链接(Linkable):即使行为类似于原生应用,它仍然是一个网站:您可以点击页面内的链接,也可以通过发送 URL 的方式分享网站给别人。</li>
<li>渐进式(Progressive):从“美好的旧式基础网站”开始,逐步添加新功能,在过程中检测其在浏览器上的可用性,并且优雅地处理不支持案例下发生的报错。举个例子,service workers 辅助下的离线模式只是提升网站体验的额外特性,但没有它网站也仍然完全可用。</li>
<li>响应式(Responsive):响应式网页设计也适用于渐进式网络应用程序,因为它们都主要用于移动设备。拥有浏览器的设备太多太杂,所以确保网站在不同屏幕宽度、视口和像素密度上都可以访问就变得尤为重要。<a href="/en-US/docs/Mozilla/Mobile/Viewport_meta_tag">viewport meta tag</a>、<a href="/en-US/docs/Web/CSS/Media_Queries/Using_media_queries">CSS media queries</a>、<a href="/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout">Flexbox</a> 和 <a href="/en-US/docs/Web/CSS/CSS_Grid_Layout">CSS Grid</a> 等技术都可以助您实现这个目标。</li>
</ul>
<h2 id="另一种概念:流">另一种概念:流</h2>
<p>使用 <a href="/zh-CN/docs/Web/API/Streams_API">Streams API</a> 可以实现完全不同的服务器端或客户端渲染方法。在 service worker 的帮助下,它可以极大改进内容解析的方式。</p>
<p>App shell 概念要求在网站开始呈现之前所有资源就已可用。在 HTML 的下载过程中,用户可以从网站的加载和渲染过程看出资源的下载进度,但 JavaScript 必须完全下载完成才能运行。</p>
<p>Streams API 则允许开发人员直接访问来自服务器的数据流。如果您想对数据执行操作(例如给视频添加过滤器),不再需要等待所有数据流下载并转换为 blob(或者别的),而是可以立即开始。它提供精细的粒度控制,将数据流启动、与另一个流链接、取消、查错等等。</p>
<p>从理论上讲,数据流是更好的模型,但也更复杂。在撰写本文时(2018年3月),Streams API 的制订仍在进行,并且在任何主流浏览器都不完全可用。当它可用时,它将是提供内容的最快方式,在性能上会有巨大的好处。</p>
<p>有关可用实例和更多信息,请参阅 <a href="/zh-CN/docs/Web/API/Streams_API">Streams API 文档</a>。</p>
<h2 id="示例应用的结构">示例应用的结构</h2>
<p><a href="https://mdn.github.io/pwa-examples/js13kpwa/">js13kPWA</a> 这个网站的结构比较简单:它包含一个HTML页面(index.heml)、一个CSS样式表(style.css)、一些图片、JS 脚本和字体。它的文件结构如下所示:</p>
<p><img alt="Folder structure of js13kPWA." src="https://mdn.mozillademos.org/files/15925/js13kpwa-directory.png" style="border-style: solid; border-width: 1px; display: block; height: 356px; margin: 0px auto; width: 320px;"></p>
<h3 id="HTML_页面">HTML 页面</h3>
<p>从 HTML 的角度,App Shell 就是 content 节之外的一切:</p>
<pre class="brush: html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>js13kGames A-Frame entries</title>
<meta name="description" content="A list of A-Frame entries submitted to the js13kGames 2017 competition, used as an example for the MDN articles about Progressive Web Apps.">
<meta name="author" content="end3r">
<meta name="theme-color" content="#B12A34">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:image" content="icons/icon-512.png">
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" href="style.css">
<link rel="manifest" href="js13kpwa.webmanifest">
<script src="data/games.js" defer></script>
<script src="app.js" defer></script>
</head>
<body>
<header>
<p><a class="logo" href="http://js13kgames.com"><img src="img/js13kgames.png" alt="js13kGames"></a></p>
</header>
<main>
<h1>js13kGames A-Frame entries</h1>
<p class="description">List of games submitted to the <a href="http://js13kgames.com/aframe">A-Frame category</a> in the <a href="http://2017.js13kgames.com">js13kGames 2017</a> competition. You can <a href="https://github.com/mdn/pwa-examples/blob/master/js13kpwa">fork js13kPWA on GitHub</a> to check its source code.</p>
<button id="notifications">Request dummy notifications</button>
<section id="content">
// Content inserted in here
</section>
</main>
<footer>
<p>© js13kGames 2012-2018, created and maintained by <a href="http://end3r.com">Andrzej Mazur</a> from <a href="http://enclavegames.com">Enclave Games</a>.</p>
</footer>
</body>
</html></pre>
<p>{{htmlelement("head")}} 一节包含一些基本信息,例如标题、描述、CSS 链接、描述文件、游戏内容的 JS 文件,以及 app.js,也就是我们这个 JavaScript 应用程序的入口点。{{htmlelement("body")}} 标签分为 {{htmlelement("header")}}(包含一张带链接的图片)、{{htmlelement("main")}} 页面主体(有标题、描述和放置内容的区域),以及 {{htmlelement("footer")}}(包含版权信息和链接)。</p>
</p>
<p>这个应用的唯一功能就是列出 js13kGames 2017 年比赛中的A-Frame(一个用来构建虚拟现实(VR)应用的网页开发框架,译者注)项目列表。如你所见,这是一个很普通的单页应用,目的是用一个简单的东西来展示 PWA 的真实功能。</p>
<h3 id="CSS_部分">CSS 部分</h3>
<p>CSS 部分也是尽可能的简单:运用 {{cssxref("@font-face")}} 来加载和使用自定义字体,以及给 HTML 元素提供简单的样式,总体的目标是通过使用响应式布局,让页面在移动端和桌面设备上都漂亮。</p>
<h3 id="主角_app.js">主角 app.js</h3>
<p>我们会在下一篇文章中详细分析 app.js 所做的这些工作。首先它用下面的模板生成了 content 中的内容:</p>
<pre class="brush: js">var template = "<article>\n\
<img src='data/img/SLUG.jpg' alt='NAME'>\n\
<h3>#POS. NAME</h3>\n\
<ul>\n\
<li><span>Author:</span> <strong>AUTHOR</strong></li>\n\
<li><span>Twitter:</span> <a href='https://twitter.com/TWITTER'>@TWITTER</a></li>\n\
<li><span>Website:</span> <a href='http://WEBSITE/'>WEBSITE</a></li>\n\
<li><span>GitHub:</span> <a href='https://GITHUB'>GITHUB</a></li>\n\
<li><span>More:</span> <a href='http://js13kgames.com/entries/SLUG'>js13kgames.com/entries/SLUG</a></li>\n\
</ul>\n\
</article>";
var content = '';
for(var i=0; i<games.length; i++) {
var entry = template.replace(/POS/g,(i+1))
.replace(/SLUG/g,games[i].slug)
.replace(/NAME/g,games[i].name)
.replace(/AUTHOR/g,games[i].author)
.replace(/TWITTER/g,games[i].twitter)
.replace(/WEBSITE/g,games[i].website)
.replace(/GITHUB/g,games[i].github);
entry = entry.replace('<a href=\'http:///\'></a>','-');
content += entry;
};
document.getElementById('content').innerHTML = content;</pre>
<p>接着,它注册了一个 service worker:</p>
<pre class="brush: js">if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa-examples/js13kpwa/sw.js');
};</pre>
<p>下面这部分代码实现了一个功能:点击按钮时请求用户授权,用来向用户推送通知。</p>
<pre class="brush: js">var button = document.getElementById("notifications");
button.addEventListener('click', function(e) {
Notification.requestPermission().then(function(result) {
if(result === 'granted') {
randomNotification();
}
});
});</pre>
<p>最后这部分是创建通知的代码,它会随机展示游戏列表中的一个项目:</p>
<pre class="brush: js">function randomNotification() {
var randomItem = Math.floor(Math.random()*games.length);
var notifTitle = games[randomItem].name;
var notifBody = 'Created by '+games[randomItem].author+'.';
var notifImg = 'data/img/'+games[randomItem].slug+'.jpg';
var options = {
body: notifBody,
icon: notifImg
}
var notif = new Notification(notifTitle, options);
setTimeout(randomNotification, 30000);
}</pre>
<h3 id="service_worker">service worker</h3>
<p>最后我们来快速浏览一下 service worker 相关的文件 sw.js。它首先引入 game.js 这个文件:</p>
<pre class="brush: js">self.importScripts('data/games.js');</pre>
<p>接着,程序会对 app shell 和主体内容里面的数据创建一个缓存列表:</p>
<pre class="brush: js">var cacheName = 'js13kPWA-v1';
var appShellFiles = [
'/pwa-examples/js13kpwa/',
'/pwa-examples/js13kpwa/index.html',
'/pwa-examples/js13kpwa/app.js',
'/pwa-examples/js13kpwa/style.css',
'/pwa-examples/js13kpwa/fonts/graduate.eot',
'/pwa-examples/js13kpwa/fonts/graduate.ttf',
'/pwa-examples/js13kpwa/fonts/graduate.woff',
'/pwa-examples/js13kpwa/favicon.ico',
'/pwa-examples/js13kpwa/img/js13kgames.png',
'/pwa-examples/js13kpwa/img/bg.png',
'/pwa-examples/js13kpwa/icons/icon-32.png',
'/pwa-examples/js13kpwa/icons/icon-64.png',
'/pwa-examples/js13kpwa/icons/icon-96.png',
'/pwa-examples/js13kpwa/icons/icon-128.png',
'/pwa-examples/js13kpwa/icons/icon-168.png',
'/pwa-examples/js13kpwa/icons/icon-192.png',
'/pwa-examples/js13kpwa/icons/icon-256.png',
'/pwa-examples/js13kpwa/icons/icon-512.png'
];
var gamesImages = [];
for(var i=0; i<games.length; i++) {
gamesImages.push('data/img/'+games[i].slug+'.jpg');
}
var contentToCache = appShellFiles.concat(gamesImages);</pre>
<p>下面的代码用来配置 service worker,缓存上述列表的工作就发生在这里:</p>
<pre class="brush: js">self.addEventListener('install', function(e) {
console.log('[Service Worker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[Service Worker] Caching all: app shell and content');
return cache.addAll(contentToCache);
})
);
});</pre>
<p>最后,如果条件允许,service worker 将从缓存中请求 content 中所需的数据,从而提供离线应用功能:</p>
<pre class="brush: js">self.addEventListener('fetch', function(e) {
e.respondWith(
caches.match(e.request).then(function(r) {
console.log('[Service Worker] Fetching resource: '+e.request.url);
return r || fetch(e.request).then(function(response) {
return caches.open(cacheName).then(function(cache) {
console.log('[Service Worker] Caching new resource: '+e.request.url);
cache.put(e.request, response.clone());
return response;
});
});
})
);
});</pre>
<h3 id="JavaScript_数据">JavaScript 数据</h3>
<p>项目中所用的游戏数据放置在 data 文件夹下面,以JavaScript对象的形式提供(<a href="https://github.com/mdn/pwa-examples/blob/master/js13kpwa/data/games.js">games.js</a>):</p>
<pre class="brush: js">var games = [
{
slug: 'lost-in-cyberspace',
name: 'Lost in Cyberspace',
author: 'Zosia and Bartek',
twitter: 'bartaz',
website: '',
github: 'github.com/bartaz/lost-in-cyberspace'
},
{
slug: 'vernissage',
name: 'Vernissage',
author: 'Platane',
twitter: 'platane_',
website: 'github.com/Platane',
github: 'github.com/Platane/js13k-2017'
},
// ...
{
slug: 'emma-3d',
name: 'Emma-3D',
author: 'Prateek Roushan',
twitter: '',
website: '',
github: 'github.com/coderprateek/Emma-3D'
}
];</pre>
<p>每一个入口在 data/img 文件夹下面都有属于它自己的图片。这些就是我们的内容数据,我们通过 JS 将这些数据加载到主体内容中。</p>
<h2 id="下一步">下一步</h2>
<p>下一篇文章中,我们会探讨更多的细节:service worker 如何帮助我们缓存 app shell 和内容,从而让我们实现离线功能。</p>
<p>{{PreviousMenuNext("Web/Apps/Progressive/Introduction", "Web/Progressive_web_apps/Offline_Service_workers", "Web/Progressive_web_apps")}}</p>
|