preface

The thing is, since the interface given in the background was to fetch the source data, it was originally just used to make a simple diagram presentation. But as the requirements became more complex, the logic became deeply nested, and the dependencies needed to cascade between the diagrams became too painful to write at the front because there was too much code in the business code that had nothing to do with the business logic. In this case, node can be used as a middle tier to solve this problem.

However, using Node as the middle tier will increase maintenance costs because the company’s operations do not support Node, and it is difficult for Node to handle concurrent and computational-heavy requests as a single thread. After all, the Service Worker can block requests and act like a middle tier, plus the system is internal and can limit users to only Using Chrome, and compatibility issues can be ignored. So I finally decided to try simulating a middle tier with the Service Worker. Finally through their own poor kung fu, successful practice of this demand, and make it engineering. I met a lot of problems and solved them one by one. I feel I have learned a lot.

The first few chapters are about engineering things, the core of which is in Chapter 7

starts

1. In your projectsrcCreate one in the directorysw.jsThe file:

2. Write a little something inside first to show respect:

I used sw-toolbox.js packaged by Google. I downloaded it and put it in SRC /service-worker/lib. The specific usage can be seen on the official website, I will not repeat, after all, the usage is similar to Express. Deeper will have to see the source code, otherwise puzzled, don’t ask me why I know.

self.importScripts('/service-worker/lib/sw-toolbox.js')
const cacheVersion = '20180705v1'
const staticCacheName = 'static' + cacheVersion
const staticAssetsCacheName = '/' + cacheVersion
const vendorCacheName = 'verdor' + cacheVersion
const contentCacheName = 'content' + cacheVersion
const maxEntries = 100

// This sw.js does not use caching and is requested over the network every time
self.toolbox.router.get(
  '/sw.js',
  self.toolbox.networkFirst
)
// Cache static resources under static
self.toolbox.router.get('/static/(.*)', self.toolbox.cacheFirst, {
  cache: {
    name: staticCacheName,
    maxEntries: maxEntries
  }
})

// Cache the js files in the root directory
self.toolbox.router.get("/(.js)", self.toolbox.cacheFirst, {
  cache: {
    name: staticAssetsCacheName,
    maxEntries: maxEntries
  }
})

self.addEventListener("install".function (event) {
  return event.waitUntil(self.skipWaiting())
})

self.addEventListener("activate".function (event) {
  return event.waitUntil(self.clients.claim())
})

Copy the code

3. Let’s modify the WebPack configuration so that it can be used at development time:

There is usually a webpack.base.conf.js file in the project, so let’s start there. Add a plugin to it so that it can be copied into memory at development time and into the appropriate directory at compile time.

new CopyWebpackPlugin([
  {
    from: path.resolve(__dirname, '.. /src/sw.js'),
    to: path.resolve(__dirname, config.build.assetsRoot)
  }
])

Copy the code

Let’s make some changesindex.htmlTo quote thissw.js

// index.html
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
        .then(function(registration) {
          // Successful registration
          console.log('Hooray. Registration successful, scope is:', registration.scope);
        }).catch(function(err) {
          // Failed registration, service worker won’t be installed
          console.log('Whoops. Service worker registration failed, error:', err);
        });
  }
Copy the code

5. We prepare some development environments

Because the Service Worker is so powerful, browsers put some restrictions on it:

  • This can only be used if localhost or HTTPS is available and the certificate is valid
  • In the process of development, we will inevitably encounter cross-domain problems. Some use proxy to forward back-end requests (localhost can be used), and some use localhost (HTTP/HTTPS can be used).

Let’s solve this problem by case:

5.1 The first is the localhost case

Sw.js: localhost:8080 localhost:8080

Because my background interface uses cookies to verify user login status, the request cannot bring the cookie under the domain name of the background interface to the server, but the cookie set by the default local server. At this time, cross-domain problems will occur:

I’m not logged in…

Once again, one of the simpler solutions is to add ModHeader to Chrome:

Then activate, click the corresponding icon in the upper right corner, and fill in Cookie (other header information, such as Referer, if necessary) in the popover to use:

After enabling the plugin, we can find that the previous request will have the Cookie and the Referer(my project actually does not need to add the Referer).

Then the request is successful, and the server uses this Cookie to determine that we are logged in

If the Cookie fails, you can only regenerate it and update the values inside the plug-in

5.1 The first is the CASE of HTTPS

Since our local development usually starts HTTP service, so we will start HTTPS server at this time, first we need to prepare the certificate. So here we’re using mkcert

Generate a certificate using the command line

mkcert '*.example.com'
Copy the code

You can then get a secret key and a public key and drop them into the project config file

The one on the left is the secret key and the one on the right is the public key

We open the public key, and the import fails

I use the MAC system, so I have not practiced other systems, friends can tinker with, should not be a problem! (Fog…)

Then let’s change the configuration and start the HTTPS server

  • The local development server isexpressCase: modifieddev-server.js
const https = require('https')
const SSLPORT = 8081 // Write a reasonable value
// Import the secret key
const privateKey  = fs.readFileSync(path.resolve(__dirname, '.. /config/key/_wildcard.xxx.com-key.pem'), 'utf8')
// Import the public key
const certificate = fs.readFileSync(path.resolve(__dirname, '.. /config/key/_wildcard.xxx.com.pem'), 'utf8')
const credentials = {
  key: privateKey,
  cert: certificate
}
...
// Then app.listen(port)
httpsServer.listen(SSLPORT, function () {
  console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
})
Copy the code

If you want to restart the service, you will find that HTTPS is enabled:

  • The local development server iswebpack-dev-serverLet’s change itwebpack.dev.conf.jsThis file, indevServerAdd something to this field
// webpack.dev.conf.jsdevServer: { ... .https: {
      key: fs.readFileSync(path.resolve(__dirname, '.. /config/key/_wildcard.xxx.com-key.pem'.'utf8'),
      cert: fs.readFileSync(path.resolve(__dirname, '.. /config/key/_wildcard.xxx.com.pem'.'utf8')}}Copy the code

Finally, we enter the address https://xxx.xxx.com in the browser to find that the certificate is valid

Finally, let’s compare the advantages and disadvantages of the two

way advantages disadvantages
localhost Easy and quick startup The Cookie replacement process is cumbersome
https The startup process is cumbersome, and certificates need to be generated and referenced Cookies can be generated automatically

6. We add an environment parameter to the Service Worker

First modify config/index.js and add something to it:

Then we add the config to the HtmlWebpackPlugin in webpack.dev.conf.js and webpack.prod.conf.js:

In index.html we can use ejS syntax to import the new parameters set in config. We add a script to the head tag:

<script>
  __GLOBAL_CONFIG__  = JSON.parse('<%= JSON.stringify(htmlWebpackPlugin.options.config) %>')
  __NODE_ENV__ = __GLOBAL_CONFIG__.env
</script>
Copy the code

Then we can insert some environment parameters to the Service Worker and modify the original Service Worker code in index.html:

// index.html
<script>
  if ('serviceWorker' in navigator) {
    const ServiceWorker = __GLOBAL_CONFIG__.ServiceWorker
    // Whether to enable the Service Worker according to the configuration
    if (ServiceWorker.enable) {
      // Open to introduce sw.js
      navigator.serviceWorker.register('/sw.js')
        .then(function(registration) {
          // Successful registration
          const messageChannel = new MessageChannel()
          // Inject environment parameters via postMessage
          navigator.serviceWorker.controller.postMessage({
            type: 'environment',
            __NODE_ENV__
          }, [messageChannel.port2]);
          console.log('Hooray. Registration successful, scope is:', registration.scope);
        }).catch(function(err) {
          // Failed registration, service worker won’t be installed
          console.log('Whoops. Service worker registration failed, error:', err);
        });
    } else {
      // If this function is not enabled, the previous cache is logged out
      navigator.serviceWorker.getRegistrations().then(function (regs) {
        for (var reg of regs) {
          reg.unregister()
        }
      })
    }
  }
</script>
Copy the code

7. We encapsulate the middle layer

7.1 Encapsulate middle-layer code

We create a model folder under the SRC /service-worker directory, which is used to develop the middle-layer module of service worker. Finally, it is packaged by Webpack to generate SRC /service-worker/model.js, which is referenced by sw.js

Sw.js to make Service Worker development engineering:

// sw.js
self.importScripts('/service-worker/lib/sw-toolbox.js')
const cacheVersion = '20180705v1'
const staticCacheName = 'static' + cacheVersion
const staticAssetsCacheName = '/' + cacheVersion
const vendorCacheName = 'verdor' + cacheVersion
const contentCacheName = 'content' + cacheVersion
const maxEntries = 100
self.__NODE_ENV__ = ' '
// Accept messages from postMessage in index. HTML
self.addEventListener('message'.function (event) {
  const data = event.data
  const { type } = data
  if (type === 'environment') {
    // This successfully sets the environment parameters in the Service Worker environment
    self.__NODE_ENV__ = data.__NODE_ENV__
    self.toolbox.options.debug = false
    self.toolbox.options.networkTimeoutSeconds = 3

    self.toolbox.router.get(
      '/sw.js',
      self.toolbox.networkFirst
    )
    // The model.js file is generated by compiling the package SRC /service-worker/model
    self.toolbox.router.get(
      '/service-worker/model.js',
      self.toolbox.networkFirst
    )
  
    self.toolbox.router.get('/static/(.*)', self.toolbox.cacheFirst, {
      cache: {
        name: staticCacheName,
        maxEntries: maxEntries
      }
    })
  
    self.toolbox.router.get("/(.js)", self.toolbox.cacheFirst, {
      cache: {
        name: staticAssetsCacheName,
        maxEntries: maxEntries
      }
    })

    self.importScripts('/service-worker/model.js')
  }
})


self.addEventListener("install".function (event) {
  return event.waitUntil(self.skipWaiting())
})

self.addEventListener("activate".function (event) {
  return event.waitUntil(self.clients.claim())
})

Copy the code

SRC /service-worker/model SRC /service-worker/model SRC /service-worker/model SRC /service-worker/model

First we create index.js to intercept the request and then distribute the request to the different model code self.model_base_url = __NODE_ENV__ === ‘development’? ‘/ API ‘: “is important, why do we bother to put environment variables into sw.js to accommodate API address changes in development and production environments

// index.js
// Specify the model
import Check from './check'
// To accommodate changes in API addresses in development and production environments
self.MODEL_BASE_URL = __NODE_ENV__ === 'development' ? '/api' : ' '

// Any request that starts with/API /v1 will be intercepted here
self.toolbox.router.post('/api/v1/(.*)'.async function (request, values, options) {
  const body = await request.text()
  const { url } = request
  // Use the re to extract the model and API
  const [ model, api ] = url.match(/ (? <=api\/v1\/).*/) [0].split('/')
  / / distribution model
  if (model === 'check') {
    return await Check.startCheckQuque(body)
  }
})
Copy the code

Then create http.js inside to wrap the fetch

// http.js
class Http {
  fetch (url, body, method) {
    return fetch(url, {
      method,
      body,
      // Add this to the fetch request to take the Cookie
      credentials: 'include'.// It depends on how your background receives information
      headers: {
        'Content-Type': 'application/json'
      }
    })
    .then((res) = > {
      return res.json()
    })
  }

  / / get request
  get (url, params) {
    return this.fetch(url, params, 'GET')}/ / post request
  post (url, body) {
    return this.fetch(url, body, 'POST')}...// Finally return a Response to sw-toolbox.js route
  response (result) {
    return new Response(JSON.stringify(result), {
      headers: { 'Content-Type': 'application/json'}}}})export default Http
Copy the code

And then we’re going to wank off a specific model

This model blocks all/API /v1/check requests, then creates a queue to push the request information, and merges multiple check requests into a listCheck request. This can reduce the number of HTTP requests. I’m doing this because the background supports this scenario, and I won’t go into the logic, but I’ll look at some of the comments in the code below, some of the details. Different models can do different things, and it’s up to you to figure out how.

// check.js
import Http from './http'
// Inherit Http
class Check extends Http {
  constructor () {
    super(a)this.CheckQuqueIndex = 0
    this.CheckQuqueStore = []
    this.OK = []
    this.timer = []
    this.result = {}
  }

  async startCheckQuque (body) {
    let index
    this.CheckQuqueStore.push(JSON.parse(body))
    index = this.CheckQuqueIndex++
    return await this.listCheck(index)
  }

  sleep (group) {
    return new Promise((resolve, reject) = > {
      const timer = setInterval((a)= > {
        if (this.OK[group] === true) {
          resolve()
          clearInterval(timer)
        }
      }, 30)
    })
  }

  forceBoot (index, group) {
    return new Promise((resolve, reject) = > {
      if ((index + 1) % 5= = =0) {
        resolve(true)}else {
        setTimeout((a)= > {
          resolve(true)},50)}}}async listCheck (index) {
    const group = Math.floor(index / 5)
    await new Promise(async (resolve, reject) => {
      const forcable = await this.forceBoot(index, group)
      if (forcable && ((index + 1) % 5= = =0 || (index + 1) = = =this.CheckQuqueIndex)) {
        this.OK[group] = false
        this.result[group] = await this.post(
          // The address of the actual interface, self.model_base_url is defined in SRC /service-worker/mode/index.js
          `${self.MODEL_BASE_URL}/listCheck`.JSON.stringify(this.CheckQuqueStore.slice(index - 4, index + 1)))this.OK[group] = true
      } else {
        await this.sleep(group)
      }
      resolve()
    })
    const id = this.CheckQuqueStore[index].requestId
    const { code, msg } = this.result[group]
    // We process the previous data and return the result via the HTTP class response method
    return this.response({
      code,
      msg,
      data: {
        series: this.result[group].data.series.filter((res) = > {
          return res.requestId === id
        })
      }
    })
  }
}

export default new Check()
Copy the code

7.2 Package and compile the Model

At this point, we also need to write a Webpack configuration to package and compile the/SRC /service-worker/model. I will save some effort here and write the development and production modes together. Since the Service Worker can definitely use ES6, don’t use any loader, just merge and compress the code

// webpack.sw.conf.js
const path = require('path')
const rm = require('rimraf')
const ora = require('ora')
const chalk = require('chalk')
const util = require('util')
const webpack = require('webpack')
const watch = require('watch')
// uglify2
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const env = process.env.NODE_ENV
const rmPromise = util.promisify(rm)

const resolve = function (dir) {
  return path.resolve(__dirname, '.. ', dir)
}


const webpackConfig = {
  entry: resolve('src/service-worker/model'),

  watchOptions: {
    aggregateTimeout: 300.poll: 1000
  },

  output: {
    path: resolve('src/service-worker'),
    filename: 'model.js'
  },

  resolve: {
    extensions: ['.js']},plugins: []}function boot () {
  const spinner = ora('building for production... ')
  spinner.start()
  rmPromise(resolve('src/service-worker/model.js'))
  .then((a)= > {
    webpack(webpackConfig, function (err, stats) {
      spinner.stop()
      if (err) {
        throw err
      }
      process.stdout.write(stats.toString({
        colors: true.modules: false.children: false.chunks: false.chunkModules: false
      }) + '\n\n')
    
      if (stats.hasErrors()) {
        console.log(chalk.red(' Build failed with errors.\n'))
        process.exit(1)}console.log(chalk.cyan(' Build complete.\n'))
      console.log(chalk.yellow(
        ' Tip: built files are meant to be served over an HTTP server.\n' +
        ' Opening index.html over file:// won\'t work.\n'
      ))
    })
  })
  .catch((err) = > {
    throw err
  })
}

if (env === 'development') {
  watch.watchTree(resolve('src/service-worker/model'), function (f, curr, prev) {
    boot()
  })
} else {
  webpackConfig.plugins.unshift(new UglifyJsPlugin())
  boot()
}

Copy the code

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

{
  from: resolve('src/service-worker/model.js'),
  to: path.resolve(__dirname, config.build.assetsRoot, 'service-worker')}Copy the code

Exclude SRC /service-worker/model from the relevant loader, as these changes do not need to be compiled into the project and use a different Webpack

Let’s start it by adding script to package.json

package.json
{
    "scripts": {// Development environment"dev:sw": "cross-env NODE_ENV=development node build/webpack.sw.conf.js"// Production environment"build:sw": "node build/webpack.sw.conf.js"}}Copy the code

And finally let’s see what’s going on

/api/v1/check
service worker

8. One last word

The length is unexpectedly too long. Half of the content is about the big and small pits encountered in the actual project, which is a little boring. Maybe not many people will finish reading it, but also by sharing the problems encountered in the actual work, HOPING to help everyone. If you have any questions, you can leave a message.