花了一上午的时间,总算是把 pwa 整上了。先来说说什么是 pwa。
渐进式 Web 应用会在桌面和移动设备上提供可安装的、仿应用的体验,可直接通过 Web 进行构建和交付。它们是快速、可靠的 Web 应用。最重要的是,它们是适用于任何浏览器的 Web 应用。如果你在构建一个 Web 应用,其实已经开始构建渐进式 Web 应用了。
简单来说,支持 pwa 的网站再移动端或者桌面端都可以模拟成设备中的一个 app,存在于主屏幕上。
每个 pwa 应用都需要一个 manifest.json
, 可能看成是一个配置文件。可以去 https://app-manifest.firebaseapp.com/
生成。
结构类似是这样的
json
1{
2 "name": "静かな森",
3 "short_name": "静かな森",
4 "theme_color": "#27ae60",
5 "description": "致虚极,守静笃。",
6 "background_color": "#2ecc71",
7 "display": "standalone",
8 "scope": "/",
9 "start_url": "/",
10 "lang": "zh-cmn-Hans",
11 "prefer_related_applications": true,
12 "icons": [
13 {
14 "src": "manifest-icon-192.png",
15 "sizes": "192x192",
16 "type": "image/png",
17 "purpose": "maskable any"
18 },
19 {
20 "src": "manifest-icon-512.png",
21 "sizes": "512x512",
22 "type": "image/png",
23 "purpose": "maskable any"
24 }
25 ]
26}
COPY
但是我这边测试的时候,这个网站的图标生成炸掉了,之后我又找了一个 cli 工具,可以用来生成 icon。可以去 GitHub 搜一下 pwa-asset-generator
。
准备工作完成后,你可以有如下文件。
1├── apple-icon-120.png
2├── apple-icon-152.png
3├── apple-icon-167.png
4├── apple-icon-180.png
5├── manifest-icon-192.png
6├── manifest-icon-512.png
7└── manifest.json
COPY
首先你需要把以上文件复制到项目根目录的 public
目录中,如果不存在可以新建一个空的目录。
来到 src
目录,新建一个 _document.tsx
改文件用来控制 NexJs 该如何渲染根节点。
这里我们需要添加亿些 meta
标签。
html
1<link rel="manifest" href="/manifest.json" />
2<meta name="mobile-web-app-capable" content="yes" />
3<meta name="apple-mobile-web-app-capable" content="yes" />
4<meta name="application-name" content="静かな森" />
5<meta name="apple-mobile-web-app-title" content="静かな森" />
6<meta name="msapplication-tooltip" content="静かな森" />
7<meta name="theme-color" content="#27ae60" />
8<meta name="msapplication-navbutton-color" content="#27ae60" />
9<meta name="msapplication-starturl" content="/" />
10<meta
11 name="viewport"
12 content="width=device-width, initial-scale=1, shrink-to-fit=no"
13/>
COPY
你也可以在这里添加一些其他的 meta
比如苹果设备上显示的图标等等。
那么完成了一步,接下来才是最重要的一步。
首先你需要知道 PWA 应用必须使用 workservice, 换句话说只有使用 workservice 才可以离线访问,这才算得上应用。
传统的方案较为繁琐,在这里我们采用 next-offline
来实现。
首先安装 next-offline
sh
1yarn add next-offline
COPY
接着在 next.config.js
中配置如下
js
1const withOffline = require('next-offline')
2module.exports = withOffline({
3 workboxOpts: {
4 swDest: process.env.NEXT_EXPORT
5 ? 'service-worker.js'
6 : 'static/service-worker.js',
7 runtimeCaching: [
8 {
9 urlPattern: /^https?.*/,
10 handler: 'NetworkFirst',
11 options: { cacheName: 'offlineCache', expiration: { maxEntries: 200 } },
12 },
13 ],
14 },
15})
COPY
如果你有多个配置则可以采用嵌套写法,如
js
1const withSourceMaps = require('@zeit/next-source-maps')()
2
3const SentryWebpackPlugin = require('@sentry/webpack-plugin')
4const {
5 NEXT_PUBLIC_SENTRY_DSN: SENTRY_DSN,
6 SENTRY_ORG,
7 SENTRY_PROJECT,
8 SENTRY_AUTH_TOKEN,
9 NODE_ENV,
10} = process.env
11
12process.env.SENTRY_DSN = SENTRY_DSN
13
14const isProd = process.env.NODE_ENV === 'production'
15const withBundleAnalyzer = require('@next/bundle-analyzer')({
16 enabled: process.env.ANALYZE === 'true',
17})
18const env = require('dotenv').config().parsed || {}
19const withImages = require('next-images')
20const withOffline = require('next-offline')
21const configs = withSourceMaps(
22 withImages(
23 withBundleAnalyzer({
24 webpack: (config, options) => {
25 if (!options.isServer) {
26 config.resolve.alias['@sentry/node'] = '@sentry/browser'
27 }
28
29 if (
30 SENTRY_DSN &&
31 SENTRY_ORG &&
32 SENTRY_PROJECT &&
33 SENTRY_AUTH_TOKEN &&
34 NODE_ENV === 'production'
35 ) {
36 config.plugins.push(
37 new SentryWebpackPlugin({
38 include: '.next',
39 ignore: ['node_modules'],
40 urlPrefix: '~/_next',
41 release: options.buildId,
42 }),
43 )
44 }
45
46 return config
47 },
48 env: {
49 PORT: 2323,
50 ...env,
51 },
52 assetPrefix: isProd ? env.ASSETPREFIX || '' : '',
53 async rewrites() {
54 return [
55 { source: '/sitemap.xml', destination: '/api/sitemap' },
56 { source: '/feed', destination: '/api/feed' },
57 { source: '/rss', destination: '/api/feed' },
58 { source: '/atom.xml', destination: '/api/feed' },
59 {
60 source: '/service-worker.js',
61 destination: '/_next/static/service-worker.js',
62 },
63 ]
64 },
65 experimental: {
66 granularChunks: true,
67 modern: true,
68 },
69 }),
70 ),
71)
72module.exports = isProd
73 ? withOffline({
74 workboxOpts: {
75 swDest: process.env.NEXT_EXPORT
76 ? 'service-worker.js'
77 : 'static/service-worker.js',
78 runtimeCaching: [
79 {
80 urlPattern: /^https?.*/,
81 handler: 'NetworkFirst',
82 options: {
83 cacheName: 'offlineCache',
84 expiration: {
85 maxEntries: 200,
86 },
87 },
88 },
89 ],
90 },
91 ...configs,
92 })
93 : configs
COPY
安装之后再次启动应用。workservice 已经搞定。就是这么简单。
打开 Chrome devtools,选择 audits 选项,生成报告,你会看到 processive Web app
图标亮了。
这一步反而是最难的,因为一般我们会使用 nginx 或者其他高性能服务器反代。考虑到缓存和 Headers 不同,大概率会产生不同的问题。
我出现了如下问题:
附 nginx 反代配置参考
1#PROXY-START/
2 location ~ (sw.js)$ {
3 proxy_pass http://127.0.0.1:2323;
4 proxy_set_header Host $host;
5 proxy_set_header X-Real-IP $remote_addr;
6 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
7 proxy_set_header REMOTE-HOST $remote_addr;
8 add_header Last-Modified $date_gmt;
9 add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
10 if_modified_since off;
11 expires off;
12 etag off;
13 }
14 location /
15 {
16 proxy_pass http://127.0.0.1:2323;
17 proxy_set_header Host $host;
18 proxy_set_header X-Real-IP $remote_addr;
19 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
20 proxy_set_header REMOTE-HOST $remote_addr;
21
22 add_header X-Cache $upstream_cache_status;
23 proxy_ignore_headers Set-Cookie Cache-Control expires;
24 add_header Cache-Control no-cache;
25 expires off;
26 }
27
28
29 #PROXY-END/