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 projectsrc
Create one in the directorysw.js
The 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.html
To 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 is
express
Case: 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 is
webpack-dev-server
Let’s change itwebpack.dev.conf.js
This file, indevServer
Add 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.