Skip to main content

MPA性能优化初探

控制翻转,控制权从应用程序转移到框架架(如IOC容器),是一种设计模式。

const apiRouter = require('./routers/api')
app.use('/api',apiRouter)

可以使用依赖注入的方式松散的添加路由,如前面自动注册路由

解耦

如controllers层需要用到services的服务如下:

import apiService from '../services/apiService';
router.get('/', async(ctx)=>{
const data = apiService.getData();
...
})

如果有很多service需要引入,有没有一个方法像如下这样自动注入到对象中:

Public Class A(){
Public setB(B b){
This.b = b;
}
}

awilix实现依赖注入

yarn add awilix awilix-koa

AwilixAPI使用简单,只需要做如下三步:

  • 创建一个容器
  • 注册模块
  • 使用注入内容
app.js
import {createContainer, lifetime} from 'awilix';
import {scopePerRequest, loadControllers} from 'awilix-koa';

//创建容器
const container = createContainer();
//注册services
container.loadModules([`${__dirname}/services/*.js`], {
formatName: 'camelCase',
resolverOptions: {
lifetime: Lifetime.SCOPED,
},
});
// 注入
app.use(scopePerRequest(container));

新建 controllers/IndexController.js 编写一个路由

import { route, GET } from 'awilix-koa'

@route('/')
class IndexController {
constructor() { }
@route('/')
@GET()
async getData(ctx) {
ctx.body = "首页"
}
}

export default IndexController

终端启动运行出现如下异常:

Locale Dropdown

缺少 @babel/plugin-proposal-decorators

yarn add @babel/plugin-proposal-decorators -D

配置 gulpfile 中的 babel

babel({
babelrc: false,
plugins: [
['@babel/plugin-proposal-decorators', { 'legacy': true }],
'@babel/plugin-transform-modules-commonjs'],
})

再次启动可以正常运行

pjax的使用

修改layout.html在里面加入jquery.min.js,启动服务访问路由结果如下:

Locale Dropdown

  • 点开 Preserve log 保留请求日志,跳转页面的时候勾选上,可以看到跳转前的请求

在切换页面的时候 公共页面的资源每次都会被加载,这我们肯定不能忍受的,如何做到像 vue-router 和 react-router 那样切换路由

pjax 的工作原理是通过 ajax 从服务器端获取 HTML,在页面中用获取到的 HTML 替换指定容器元素中的内容。然后使用 pushState 技术更新浏览器地址栏中的当前地址。

  • 按需请求,每次只需加载页面的部分内容,而不用重复加载一些公共的资源文件和不变的页面结构,大大减小了数据请求量,以减轻对服务器的带宽和性能压力,还大大提升了页面的加载速度。
  • 只刷新部分页面,切换效果更加流畅,而且可以定制过度动画,优化页面跳转体验

缺点:要做到普通请求返回完整页面,而pjax请求只返回部分页面,服务端就需要做一些特殊处理,使服务端处理变得复杂.

修改 公共页面 layout.html,加入如下代码:

 <div id="app">
{% block content %}{% endblock %}
</div>
<script src="https://cdn.staticfile.org/jquery/3.5.1/jquery.js"></script>
<script src="https://cdn.staticfile.org/jquery.pjax/2.0.1/jquery.pjax.min.js"></script>
<script>
$(document).pjax('a', '#app');
</script>
{% block scripts %}{% endblock %}

现在再点击切换页面可以看到下面 在请求头新增了 X-PJAX 字段

Locale Dropdown

服务端处理

首先要判断是直接刷新的页面还是通过别的页面切换过来的,怎么判断呢,这个时候就要用到了 X-PJAX 字段了

if (ctx.request.header['x-pjax']) {
console.log('站内切');
} else {
console.log('落地页');
}

在做 MAP 页面最忌讳的就是首页一下子返回一个大页面,这对体验很不好,因此我们要使用 buffer 让页面缓慢的流回来

所以需要做如下修改

import { Readable } from "stream"

const data = await this.bookService.getData()
const html = await ctx.render('book/pages/list', {
data
})
if (ctx.request.header['x-pjax']) {
console.log('站内切');
} else {
const htmlStream = new Readable();
htmlStream.push(html)
htmlStream.push(null)
ctx.status = 200
ctx.type = "html"
htmlStream.on('error', err => { }).pipe(ctx.res)
}

刷新页面,发现页面出现 ok 两个字,经常会出现异步的问题所以修改如下代码:

function createSSRStream() {
return new Promise((resolve, reject) => {
const htmlStream = new Readable();
htmlStream.push(html)
htmlStream.push(null)
ctx.status = 200
ctx.type = "html"
htmlStream.on('error', err => reject).pipe(ctx.res)
})
}
await createSSRStream()

启动服务查看

Locale Dropdown

发现请求头里面添加了 Transfer-Encoding:chunked 表示现在以及是以流的形式在进行传输

分块编码 Transfer-Encoding:chunked

当返回的数据比较大时,如果等待生成完数据再传输,这样效率比较低下。相比而言,服务器更希望边生成数据边传输。可以在响应头加上以下字段标识分块传输

Transfer-Encoding: chunked
  • Transfer-Encoding,是一个 HTTP 头部字段(响应头域),字面意思是「传输编码」。最新的 HTTP 规范里,只定义了一种编码传输:分块编码(chunked)
  • 分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许HTTP由网页服务器发送给客户端的数据可以分成多个部分。分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供。
  • 数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。

解决站内切重复渲染

如上完成了落地页直刷采用stream流的传输,但是站内切还未解决

  • 判断是否是站内切还是落地页
  • 只吐出部分资源

cheerio 是专为服务器设计的核心jQuery的快速,灵活和精益实现。他可以像jquery一样操作字符串

yarn add cheerio

读取html返回节点的部分资源

if (ctx.request.header['x-pjax']) {
const $ = cheerio.load(html)
ctx.status = 200;
ctx.type = 'html';
$('.pjaxcontent').each(function () {
ctx.res.write($(this).html());
});
}

打开浏览器调试目录查看:

Locale Dropdown

服务器只返回了部分需要更换的 HTML

对于页面返回的JS 加载也可以做标示返回,如下:

$('.lazyload-js').each(function () {
ctx.res.write(
`<script class="lazyload-js" src="${$(this).attr('src')}"></script>`
);
});

但是这个 class lazyload-js 怎么加上呢,这个文件是我们前段编写 webpack 插入到指定的位置,修改代码如下:

htmlAfterPlugin
...
for (const jsitem of jsList) {
js.push(`<script class="lazyload-js" src="${jsitem}"></script>`)
}

再次访问JS也对应返回了

问题

客户端开发环境下无问题,但是使用production打包会发现js不能正常加载,查看html-webpack-plugin发现,在production模式下,会对html进行压缩、清除空格、注释等信息所以需要修改webpack配置:

new HtmlWebpackPlugin({
...
minify: {
removeComments: false
}
})

总结

  • awilix实现依赖注入
  • pajx 实现无刷新修改页面和路由切换
  • 使用 chunked 分段传输
  • 服务端判断处理并吐出对于的资源