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使用简单,只需要做如下三步:
- 创建一个容器
- 注册模块
- 使用注入内容
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
终端启动运行出现如下异常:

缺少 @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,启动服务访问路由结果如下:

- 点开 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 字段

服务端处理
首先要判断是直接刷新的页面还是通过别的页面切换过来的,怎么判断呢,这个时候就要用到了 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()
启动服务查看

发现请求头里面添加了 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());
});
}
打开浏览器调试目录查看:

服务器只返回了部分需要更换的 HTML
对于页面返回的JS 加载也可以做标示返回,如下:
$('.lazyload-js').each(function () {
ctx.res.write(
`<script class="lazyload-js" src="${$(this).attr('src')}"></script>`
);
});
但是这个 class lazyload-js 怎么加上呢,这个文件是我们前段编写 webpack 插入到指定的位置,修改代码如下:
...
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 分段传输
- 服务端判断处理并吐出对于的资源