前言
事情是这样的,由于后台给的接口是获取源数据的,一开始只是拿来做一些简单图表的展示。但是后来需求越来越复杂,逻辑嵌套深,需要在各个图表之间串联依赖关系,把这一层放在前端来写太蛋疼了,因为业务代码里太多跟业务逻辑没有关系的代码了。这种情况其实就挺适合用node来做一个中间层来解决这一问题。
但是用node来做中间层会增加维护成本,因为公司的运维并不支持node,而且node作为单线程处理一些并发量计算量大的请求会比较吃力。思前想后,Service Worker可以拦截请求,似乎可以做个中间层的样子,加上系统是对内的,可以限制用户只用chrome浏览器,兼容性问题可以忽略。所以最终决定尝试用Service Worker来模拟一个中间层试试。最终通过自己蹩脚的功夫,成功实践了这一需求,并使其工程化。其中遇到了很多问题,也一一解决了,感觉自己学到不少东西。
前面几个章节都是讲的一些工程化的东西,核心部分在第7章里
开工
1.在你的项目的src
目录下创建一个sw.js
的文件:
2.先往里面稍微写点东西以示尊敬:
这里用到了google封装的sw-toolbox.js,我把它下载下来放到了src/service-worker/lib
里。具体用法可以到官网看,我就不再赘述了,毕竟用法跟express
类似。深入点的就得看源码了,不然百思不得其解,别问我为什么知道。
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
// 本身的这个sw.js不使用缓存,每次都通过网络请求
self.toolbox.router.get(
'/sw.js',
self.toolbox.networkFirst
)
// 缓存static下的静态资源
self.toolbox.router.get('/static/(.*)', self.toolbox.cacheFirst, {
cache: {
name: staticCacheName,
maxEntries: maxEntries
}
})
// 缓存根目录下的js文件
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())
})
复制代码
3.我们来修改一下webpack配置,使得其可以在开发时使用:
一般现在项目里都有个webpack.base.conf.js
的文件,那我们先从这里开刀。给它加个plugin
,这样既能在开发的时候拷贝到内存里,又能在编译时拷到对应的目录里。
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../src/sw.js'),
to: path.resolve(__dirname, config.build.assetsRoot)
}
])
复制代码
4.我们来改一些index.html
来引用这个sw.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);
});
}
复制代码
5.我们准备些开发环境
因为Service Worker的功能太过强大,所以浏览器对其做了些限制:
- 只有在localhost或者https并且证书有效的情况下才能使用
- 我们在开发的过程中难免会遇到跨域的问题,有的通过proxy转发后端请求(可以用localhost),有的通改本地host(可以用http/https)
那我们分情况来解决这个问题:
5.1 首先是localhost的情况
这个情况是可以直接用的,浏览器打开localhost:8080
就可以注册sw.js
了:
因为我的后台接口都是通过cookie
来验证用户登录态的,请求就带不上后台接口域名下的cookie
给服务器,而是默认本地服务器设置的cookie
,这个时候就会遇到遇到跨域的问题:
可以看到后台判断我没有登录...
我再次就介绍其中一种简单点的解决方法: 给chrome添加ModHeader
插件(应该要翻墙吧,微笑脸):
然后激活,在右上角可以点击对应图标,在弹窗里填入Cookie
(如有需要可以其他头信息,如Referer
)就可以使用了:
启用插件后,我们就可以发现之前的请求就会带上这个Cookie
和Referer
(我这个项目其实是不需要加Referer
的)
然后就请求成功了,服务器根据这个Cookie
判断我们是登录态
Cookie
失效了只能重新生成,然后更新插件里面的值
5.1 首先是https的情况
因为我们本地开发一般来说启动的都是http服务,那么我们这个时候就要启动https服务器了,首先我们需要准备一下证书。这里我们用到了mkcert
这个工具
用命令行生成证书
mkcert '*.example.com'
复制代码
然后可以得到一个秘钥和一个公钥,并把它们弄到项目的config
文件下
左边的为秘钥,右边的为公钥
我们打开公钥,然后会提示导入失败
这个时候需要我们在钥匙串里双击这个证书并信任他我用的mac系统,所以其他系统并没有实践过,小伙伴可以捣鼓捣鼓,应该没什么坑吧!(大雾...)
然后我们来改下配置启动一下https
服务器
- 本地开发服务器为
express
的情况: 修改dev-server.js
const https = require('https')
const SSLPORT = 8081 // 写个合理的值就好
// 引入秘钥
const privateKey = fs.readFileSync(path.resolve(__dirname, '../config/key/_wildcard.xxx.com-key.pem'), 'utf8')
// 引入公钥
const certificate = fs.readFileSync(path.resolve(__dirname, '../config/key/_wildcard.xxx.com.pem'), 'utf8')
const credentials = {
key: privateKey,
cert: certificate
}
...
// 然后再app.listen(port)之后添加
httpsServer.listen(SSLPORT, function () {
console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
})
复制代码
我们来重启服务一波,就会发现开启了https
服务了:
- 本地开发服务器为
webpack-dev-server
的情况: 我们修改一下webpack.dev.conf.js
这个文件,在devServer
这个字段里加点东西
// webpack.dev.conf.js
devServer: {
...,
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')
}
}
复制代码
最后我们在浏览器输入地址https://xxx.xxx.com
,就可以发现证书生效了
最后我们来对比下这两者的优缺点
方式 | 优点 | 缺点 |
---|---|---|
localhost | 方便快捷启动 | 生成Cookie替换过程麻烦 |
https | 启动过程麻烦,需要生成并引用证书 | 可以自动生成Cookie |
6.我们给Service Worker加点环境参数
首先修改一下config/index.js
,往里面添加点东西:
然后我们给webpack.dev.conf.js
和webpack.prod.conf.js
里的HtmlWebpackPlugin
这个插件加入刚刚修改的config
:
这样我们就可以在index.html
里面利用ejs
语法引入config里面新设置的参数了,我们在head标签里面加段脚本:
<script>
__GLOBAL_CONFIG__ = JSON.parse('<%= JSON.stringify(htmlWebpackPlugin.options.config) %>')
__NODE_ENV__ = __GLOBAL_CONFIG__.env
</script>
复制代码
然后我们就可以对Service Worker
插入一些环境参数,修改原来的在index.html
下的有关Service Worker
代码:
// index.html
<script>
if ('serviceWorker' in navigator) {
const ServiceWorker = __GLOBAL_CONFIG__.ServiceWorker
// 根据配置是否开启Service Worker
if (ServiceWorker.enable) {
// 开启则引入sw.js
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
// Successful registration
const messageChannel = new MessageChannel()
// 通过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 {
// 不开启则注销掉以前的缓存
navigator.serviceWorker.getRegistrations().then(function (regs) {
for (var reg of regs) {
reg.unregister()
}
})
}
}
</script>
复制代码
7.我们封装下中间层
7.1封装中间层代码
我们在src/service-worker
目录下创建一个model
的文件夹,用于开发Service Worker
的中间层模块,最终被webpack
打包生成src/service-worker/model.js
,从而被sw.js
引用
然后来对sw.js
动下刀,来使得Service Worker
的开发可以工程化:
// 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__ = ''
// 从index.html的postMessage里接受消息
self.addEventListener('message', function (event) {
const data = event.data
const { type } = data
if (type === 'environment') {
// 这样就成功在Service Worker环境里设置好了环境参数
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
)
// 这个model.js我们需要通过编译打包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())
})
复制代码
下面我们来具体讲解一下src/service-worker/model
这个文件夹里要怎么封装代码,暂时来说还比较简单,所以就直接用单例模式来封装代码了
首先我们来创建index.js
来拦截一下请求,然后分发请求给不同的model
代码里的self.MODEL_BASE_URL = __NODE_ENV__ === 'development' ? '/api' : ''
是重点,为什么我们要煞费苦心把环境变量弄进sw.js
里来,就是为了适应开发和生产环境对api
地址的变动
// index.js
// 具体的model
import Check from './check'
// 为了适应开发和生产环境对api地址的变动
self.MODEL_BASE_URL = __NODE_ENV__ === 'development' ? '/api' : ''
// 只要请求是以/api/v1开头的,都会在这里被拦截到
self.toolbox.router.post('/api/v1/(.*)', async function (request, values, options) {
const body = await request.text()
const { url } = request
// 通过正则来提取出model和api
const [ model, api ] = url.match(/(?<=api\/v1\/).*/)[0].split('/')
// 分发model
if (model === 'check') {
return await Check.startCheckQuque(body)
}
})
复制代码
然后在里面创建http.js
来封装一下fetch
// http.js
class Http {
fetch (url, body, method) {
return fetch(url, {
method,
body,
// 加上这个,fetch请求才会带上Cookie
credentials: 'include',
// 这里要看你后台具体是怎么接受信息的了
headers: {
'Content-Type': 'application/json'
}
})
.then((res) => {
return res.json()
})
}
// get请求
get (url, params) {
return this.fetch(url, params, 'GET')
}
// post请求
post (url, body) {
return this.fetch(url, body, 'POST')
}
...
// 最终要返回一个Response给sw-toolbox.js的路由里
response (result) {
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
})
}
}
export default Http
复制代码
然后我们来撸一撸具体的某个model
这个model的作用就是拦截所有的/api/v1/check
请求,然后做了个队列,往里面push请求信息,并把多个check
请求合并成一个listCheck
请求,这样可以减少http
请求次数,这个具体要看场景,我这里是因为后台支持这种场景才这么做的,具体逻辑我就不说了,不过可以看下下面代码的一些注释,是一些细节点。不同的model
可以干不同的事情,具体怎么搞就看大家发挥了。
// check.js
import Http from './http'
// 继承一下Http
class Check extends Http {
constructor () {
super()
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(() => {
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(() => {
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(
// 实际接口的地址,self.MODEL_BASE_URL在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]
// 我们把之前的数据处理完,通过http类里的response方法把运算结果返回去
return this.response({
code,
msg,
data: {
series: this.result[group].data.series.filter((res) => {
return res.requestId === id
})
}
})
}
}
export default new Check()
复制代码
7.2打包编译model
到此,我们还要编写一个webpack
配置来打包编译这个/src/service-worker/model
,我这里就省点功夫,把开发和生产模式都写到一起了。由于能Service Worker
肯定就能用es6
,所以就不要用任何loader
了,只需要合并压缩一下代码即可
// 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(() => {
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()
}
复制代码
然后还需要修改一下webpack.base.conf.js
首先来把编译后的model.js
导进去,也就是往之前在webpack.base.conf.js
加的CopyWebpckPlugin
里再加一项:
{
from: resolve('src/service-worker/model.js'),
to: path.resolve(__dirname, config.build.assetsRoot, 'service-worker')
}
复制代码
接着在相关的loader
里exclude
掉src/service-worker/model
,因为这些文件的改动并不需要被编译到项目里,是用的另外一个webpack
我们在package.json
里加点script
来启动它
package.json
{
"scripts": {
// 开发环境
"dev:sw": "cross-env NODE_ENV=development node build/webpack.sw.conf.js",
// 生产环境
"build:sw": "node build/webpack.sw.conf.js"
}
}
复制代码
最后我们来看看运行情况怎么样
好像还可以,可以看到我们所有的/api/v1/check
请求都是从
service worker
里返回来的
8.最后谈谈
篇幅意想不到变得太长了,其中一半的内容都在讲实际项目中遇到的大坑小坑,些许无聊,可能看完的人不多,也是通过自己分享实际工作中遇到的问题,希望能帮助到大家。有什么问题大家可以留言交流交流。
最后打波广告,近期部门缺人,以下是jd:
公司:虎牙直播
职位要求:
1.本科以上学历
2.1~3年工作经验
3.js基础扎实
4.熟练vue,react等主流框架之一
5.熟悉http协议
6.对性能优化有自己的见解
7.会一些node
复制代码
大佬们感兴趣的话,可以发一波简历到我邮箱里:[email protected]
,备注掘金