Build PWA for the original NextJS

It took me all morning to finish pwa at last. Let's talk about pwa first.

Progressive Web applications will provide installable and application like experience on desktop and mobile devices, which can be built and delivered directly through the Web. They are fast and reliable Web applications. Most importantly, they are Web applications for any browser. If you're building a Web application, you're already building a progressive Web application.

Simply put, the mobile terminal or desktop terminal of a website supporting pwa can be simulated as an app in the device and exist on the home screen.

Before starting

Each pwa application needs a manifest JSON, which may be regarded as a configuration file. Can go https://app-manifest.firebaseapp.com/ Generate.

The structure is similar to this

json

1{
2  "name": "quietかなSen",
3  "short_name": "quietかなSen",
4  "theme_color": "#27ae60",
5  "description": "To the empty pole, keep quiet.",
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

But when I tested here, the icon generation of this website blew up, and then I found a cli tool that can be used to generate icons. You can go to GitHub and search PWA asset generator.

After the preparation, you can have the following documents.

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

Integration into NextJs project

First, you need to copy the above files to the public directory of the project root directory. If they do not exist, you can create an empty directory.

Go to the src directory and create a new one_ document. The TSX file is used to control how NexJs renders the root node.

Here we need to add some meta tags.

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="quietかなSen" />
5<meta name="apple-mobile-web-app-title" content="quietかなSen" />
6<meta name="msapplication-tooltip" content="quietかなSen" />
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

You can also add some other meta here, such as the icons displayed on Apple devices and so on.

Then you have completed one step, and the next is the most important step.

First of all, you need to know that PWA applications must use workservice. In other words, only workservice can be accessed offline, which can be regarded as an application.

Deploy WorkService

The traditional scheme is cumbersome. Here we use next offline to implement it.

First install next offline

sh

1yarn add next-offline

COPY

Next config. JS is configured as follows

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

If you have multiple configurations, you can use nested writing, such as

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

Start the application again after installation. The workservice has been completed. It's that simple.

Open Chrome devtools, select the audits option and generate a report. You will see that the icon of the processed web app is on.

Production environment deployment

This step is the most difficult, because we usually use nginx or other high-performance servers. Considering the difference between cache and Headers, the probability will cause different problems.

I have the following problems:

  • 504 error. Check the cache time of nginx. It is recommended to close the cache because the workservice has its own cache.
  • 500 error. If pm2 is used to host nodejs application, the memory size is exceeded when viewing pm2 and the application is restarted
  • network error, check the number of nginx concurrency. Since workservice is adopted, the number of concurrency of a single ip is relatively large. It is recommended to set it to twice the general number
  • js,css load 404, and check whether nginx has enabled the anti-theft chain. When the workservice requests, it does not bring a reference

The reference of nginx reverse configuration is attached

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/

Added by phpORcaffine on Fri, 31 Dec 2021 22:32:44 +0200