# 性能优化实战
没分段,后面补
基于之前的项目,引入三个cdn bootstrap,jquery和pjax(用于将真路由进行代理) 同时引用pjax
src/web/views/layouts/layout.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}My Site{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
{% block head %}
{% endblock %}
</head>
<body>
<div id="app">
{% block content %}{% endblock %}
</div>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<script src="https://cdn.bootcss.com/jquery.pjax/2.0.1/jquery.pjax.js"></script>
<script>
$(document).pjax('a','#app');
</script>
{% block scripts %}{% endblock %}
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
完善list页面 src/web/components/list/index.html
<div class="list">
<h1>图书列表页</h1>
<table class="table table-bordered table-striped">
<tr>
<th>编号</th>
<th>书名</th>
<th>作者</th>
</tr>
{% for val in result.data %}
<tr>
<td>{{ val.id }}</td>
<td>{{ val.name }}</td>
<td>{{ val.author }}</td>
</tr>
{% endfor %}
</table>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
此时启动我们的项目 npm run server:start npm run client:dev
打开页面,打开控制台,看newwork 勾选 Preserve log(可以看到之前的日志) 当我们刷新整个页面的时候,文件正常全部请求 但是当我们点击tag切换页时,会发现多了一个 list?_pjax=%23app的文件,文件会带有请求头 X-PJAX: true X-PJAX-Container: #app X-Requested-With: XMLHttpRequest
因此我们可以通过这个请求头来区别是刷新的页面还是在页面内的操作 接下来就要通过这个请求头来进行操作
回到controllers文件 src/server/controllers/BooksController.js
import Books from '../models/Books';
class BookController {
async actionIndex(ctx, next) {
const $model = new Books();
const result = await $model.getList();
if(ctx.request.header["x-pjax"]){
console.log('站内切');
} else {
console.log('直接刷新');
}
// console.log("返回的值", result);
// ctx.body = result;
ctx.body = await ctx.render('books/pages/list', {
result
});
}
async actionCreate(ctx, next) {
ctx.body = await ctx.render('books/pages/create');
}
}
export default BookController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
通过添加头的判断,可以看到,每次切换到列表页的时候都会刷出'站内切' 后,再刷 '直接刷新' 进行了两次请求,会先拿头的请求进行探测,发现没有配合它之后再正常请求一次。
在切换页面的时候,其实我们只需要替换掉标题就可以 那么我们再来试一试
import Books from '../models/Books';
class BookController {
async actionIndex(ctx, next) {
const $model = new Books();
const result = await $model.getList();
// 把返回的html单独拿出来
const html = await ctx.render('books/pages/list', {
result
});
// 通过判断请求头来返回
if(ctx.request.header["x-pjax"]){
console.log('站内切');
ctx.body = '站内刷的页面';
} else {
console.log('直接刷新');
ctx.body = html;
}
// console.log("返回的值", result);
// ctx.body = result;
}
async actionCreate(ctx, next) {
ctx.body = await ctx.render('books/pages/create');
}
}
export default BookController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
当直接刷新的时候,返回的直接就是原来的页面,当站内切换的时候,页面则变成了'站内刷的页面' 那么接下来我们就可以把变换的地方换掉就可以了,也就是局部加载
安装 cheerio
yarn add cheerio
使用cheerio来分析html结构,服务端的jq 因为在切换的时候需要将整个list部分的html替换掉,所以在 components/list/index.html 的div上加上类名 pjaxcontent,方便查找
<div class="list pjaxcontent">
<h1 id="js-h1">图书列表页</h1>
<table class="table table-bordered table-striped">
<tr>
<th>编号</th>
<th>书名</th>
<th>作者</th>
</tr>
{% for val in result.data %}
<tr>
<td>{{ val.id }}</td>
<td>{{ val.name }}</td>
<td>{{ val.author }}</td>
</tr>
{% endfor %}
</table>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在 BooksController.js中,使用cheerio来解析html,并把需要替换的部分换掉
import Books from '../models/Books';
import cheerio from 'cheerio';
class BookController {
async actionIndex(ctx, next) {
const $model = new Books();
const result = await $model.getList();
const html = await ctx.render('books/pages/list', {
result
});
if(ctx.request.header["x-pjax"]){
console.log('站内切');
const $ = cheerio.load(html);
let _result = '';
$('.pjaxcontent').each(function() {
_result += $(this).html();
})
ctx.body = _result;
} else {
console.log('直接刷新');
ctx.body = html;
}
// console.log("返回的值", result);
// ctx.body = html;
}
async actionCreate(ctx, next) {
ctx.body = await ctx.render('books/pages/create');
}
}
export default BookController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
此时我们在来测试一下,直接刷新页面,network中会正常加载所需的全部文件,当通过站内进行切换到列表页时,network只发了一个请求 list?_pjax=%23app 但是在站内切换到列表的时候,banner没有了,这不是我想要的,继续改进
把banner组件从list和create组件里拿出来,放在layout中
src/web/views/books/pages/list.html
{% extends '@layouts/layout.html' %}
{% block title %}图书新增页面{% endblock %}
{% block content %}
{% include "@components/add/index.html" %}
{% endblock %}
{% block scripts %}
<!-- injectjs -->
{% endblock %}
2
3
4
5
6
7
8
9
10
11
把 banner 从app中拿出来,放在外面,只让app里的东西变 src/web/views/books/layouts/layout.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}My Site{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
{% block head %}
{% endblock %}
</head>
<body>
{% include "../../components/banner/index.html" %}
<div id="app">
{% block content %}{% endblock %}
</div>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<script src="https://cdn.bootcss.com/jquery.pjax/2.0.1/jquery.pjax.js"></script>
<script>
$(document).pjax('a','#app');
</script>
{% block scripts %}{% endblock %}
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这样就可以只替换掉变换的部分了。 但是同时又出现新的问题,站内切换的时候,js没有带过来。 验证一下 在 src/web/components/list/index.js 中添加一个点击事件
const list = {
init() {
console.log('list js 入口');
$('#js-h1').click(function() {
alert('欢迎选择图书');
})
}
}
export default list;
2
3
4
5
6
7
8
9
正常刷新页面,点击标题,能够弹出弹窗 但是在其他页面刷新完之后,站内切换到列表,再点击则不出弹窗。
我们可以通过webpack来分辨哪些是加载时需要的js 将单独加载的文件加上 lazyload-js 类名 在 config/htmlAfterPlugin.js
const pluginName = 'htmlAfterPlugin';
const assetsHelp = (data) => {
let js = [];
const dir = {
js: item => `<script src="${item}"></script>`,
// -------------------------------------------------
lazyjs: item => `<script class="lazyload-js" src="${item}"></script>`,
// -------------------------------------------------
}
// const whiteList = new Map();
// whiteList.set('whiteList', true);
// -------------------------------------------------
for(let jsitem of data.js) {
if (/runtime/g.test(jsitem)) {
js.push(dir.js(jsitem));
} else {
js.push(dir.lazyjs(jsitem));
}
}
// -------------------------------------------------
return {
js
}
}
class HtmlAfterPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(pluginName, compilation => {
// HtmlWebpackPlugin留下的钩子,在插入html之前等一下,必须要泡在HtmlWebpackPlugin插件之后
compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tap(pluginName, htmlPluginData => {
// htmlPluginData能拿到相应的html页面,替换掉写好的占位符
// 获取对应的html
let _html = htmlPluginData.html;
_html = _html.replace(/@layouts/g, '../../layouts');
_html = _html.replace(/@components/g, '../../../components');
// 拿到要插入的js
const result = assetsHelp(htmlPluginData.assets);
// 将js文件替换占位符
_html = _html.replace('<!-- injectjs -->', result.js.join(''));
htmlPluginData.html = _html;
})
})
}
}
module.exports = HtmlAfterPlugin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
再改一下controller 找到 lazyload-js 类的标签拿出来,加到相应的页面。
import Books from '../models/Books';
import cheerio from 'cheerio';
class BookController {
async actionIndex(ctx, next) {
const $model = new Books();
const result = await $model.getList();
const html = await ctx.render('books/pages/list', {
result
});
if(ctx.request.header["x-pjax"]){
console.log('站内切');
const $ = cheerio.load(html);
let _result = '';
$('.pjaxcontent').each(function() {
_result += $(this).html();
})
// ---------------------------------------
$('.lazyload-js').each(function() {
_result += `<script src="${$(this).attr("src")}"></script>`;
})
// ---------------------------------------
ctx.body = _result;
} else {
console.log('直接刷新');
ctx.body = html;
}
// console.log("返回的值", result);
// ctx.body = html;
}
async actionCreate(ctx, next) {
ctx.body = await ctx.render('books/pages/create');
}
}
export default BookController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
重新打包运行,站内切换,network中看到同时还加载了相应的js文件
现在切页是搞定了,但是SSR有个缺点,一次请求所有的页面,如果页面很大那就会一直等待卡着,就很难受了,所以使用bigpipe
在 src/server/controllers下写一个测试的bigpipe
在controllers 的index.js中添加测试路由
import router from 'koa-simple-router';
import IndexController from './IndexController';
const indexController = new IndexController();
import BooksController from './BooksController';
const booksController = new BooksController();
const controllersInit = (app) => {
app.use(router(_ => {
_.get('/', indexController.actionIndex)
_.get('/test', indexController.testIndex)
_.get('/books/list', booksController.actionIndex)
_.get('/books/create', booksController.actionCreate)
}))
}
export default controllersInit;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
再在indexController.js 中添加路由
import Controller from './BaseController';
const fs = require('fs');
const {resolve} = require('path');
class IndexController extends Controller{
constructor() {
super()
}
async actionIndex(ctx, next) {
// ctx.body = await ctx.render('Index/index')
ctx.body = {
home: '首页数据'
}
}
// -----------------------------------
async testIndex(ctx, next) {
const filename = resolve(__dirname, 'index.html');
const file1 = fs.readFileSync(filename ,'utf-8');
ctx.body = file1;
}
//------------------------------------
}
export default IndexController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在dist的controllers中临时创建一个html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>bigpipe测试页</title>
</head>
<body>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
在页面中输入test路由,可以看到通过ssr加载到页面,但是可以看到是直接请求的整个页面,下面通过一点小手段进行chunk 修改indexController.js
import Controller from './BaseController';
const fs = require('fs');
const {resolve} = require('path');
class IndexController extends Controller{
constructor() {
super()
}
async actionIndex(ctx, next) {
// ctx.body = await ctx.render('Index/index')
ctx.body = {
home: '首页数据'
}
}
async testIndex(ctx, next) {
const filename = resolve(__dirname, 'index.html');
const file1 = fs.readFileSync(filename ,'utf-8');
// ctx.body = file1;
// ---------------------------
ctx.res.write(file1);
ctx.res.end();
// ---------------------------
}
}
export default IndexController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
此时再刷新页面,network中,先不用管404,可以看到请求头中多了一个 Transfer-Encoding: chunked 浏览器会自动进行bigpipe 现在浏览器是先把文件都读完之后,一点一点往外写 接下来我们要配置一下读一点写一点
import Controller from './BaseController';
const fs = require('fs');
const {resolve} = require('path');
class IndexController extends Controller{
constructor() {
super()
}
async actionIndex(ctx, next) {
// ctx.body = await ctx.render('Index/index')
ctx.body = {
home: '首页数据'
}
}
async testIndex(ctx, next) {
ctx.status = 200;
ctx.type = 'html';
const filename = resolve(__dirname, 'index.html');
// const file1 = fs.readFileSync(filename ,'utf-8');
// ctx.body = file1;
// ctx.res.write(file1);
// ctx.res.end();
function createSSRStream() {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(filename);
stream.on('error',err => {reject(err)}).pipe(ctx.res);
})
}
await createSSRStream();
}
}
export default IndexController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
还可以有另外的写法
import Controller from './BaseController';
const fs = require('fs');
const {resolve} = require('path');
class IndexController extends Controller{
constructor() {
super()
}
async actionIndex(ctx, next) {
// ctx.body = await ctx.render('Index/index')
ctx.body = {
home: '首页数据'
}
}
async testIndex(ctx, next) {
ctx.status = 200;
ctx.type = 'html';
const filename = resolve(__dirname, 'index.html');
// const file1 = fs.readFileSync(filename ,'utf-8');
// ctx.body = file1;
// ctx.res.write(file1);
// ctx.res.end();
function createSSRStream() {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(filename);
stream.on('data', chunk => {ctx.res.write(chunk)});
stream.on('end', () => {
ctx.res.end();
})
stream.on('error',err => {reject(err)});
})
}
await createSSRStream();
}
}
export default IndexController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
接下来再模拟一下用户延迟的情况
import Controller from './BaseController';
const fs = require('fs');
const {resolve} = require('path');
class IndexController extends Controller{
constructor() {
super()
}
async actionIndex(ctx, next) {
// ctx.body = await ctx.render('Index/index')
ctx.body = {
home: '首页数据'
}
}
async testIndex(ctx, next) {
ctx.status = 200;
ctx.type = 'html';
const task1 = () => {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve('第一次输出<br/>')
}, 1000);
})
}
const task2 = () => {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve('第二次输出<br/>')
}, 2000);
})
}
const filename = resolve(__dirname, 'index.html');
const file1 = fs.readFileSync(filename ,'utf-8');
// ctx.body = file1;
// ctx.res.write(file1);
// ctx.res.end();
/* function createSSRStream() {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(filename);
stream.on('data', chunk => {ctx.res.write(chunk)});
stream.on('end', () => {
ctx.res.end();
})
stream.on('error',err => {reject(err)});
})
}
await createSSRStream(); */
ctx.res.write(file1);
const result1 = await task1();
ctx.res.write(result1);
const result2 = await task2();
ctx.res.write(result2);
ctx.res.end();
}
}
export default IndexController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
可以看到网络延迟下,加载多少吐多少 那么我们再加上loading....效果,让它在固定的地方出现 先修改一下html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>bigpipe测试页</title>
</head>
<body>
bigpipe测试页
<div id='part1'>loading...</div>
<div id='part2'>loading...</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
再修改修改js
import Controller from './BaseController';
const fs = require('fs');
const {resolve} = require('path');
class IndexController extends Controller{
constructor() {
super()
}
async actionIndex(ctx, next) {
// ctx.body = await ctx.render('Index/index')
ctx.body = {
home: '首页数据'
}
}
async testIndex(ctx, next) {
ctx.status = 200;
ctx.type = 'html';
const task1 = () => {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve(`<script>addHTML("part1", "第一次输出<br/>")</script>`)
}, 1000);
})
}
const task2 = () => {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve(`<script>addHTML("part2", "第二次输出<br/>")</script>`)
}, 2000);
})
}
const filename = resolve(__dirname, 'index.html');
const file1 = fs.readFileSync(filename ,'utf-8');
// ctx.body = file1;
// ctx.res.write(file1);
// ctx.res.end();
/* function createSSRStream() {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(filename);
stream.on('data', chunk => {ctx.res.write(chunk)});
stream.on('end', () => {
ctx.res.end();
})
stream.on('error',err => {reject(err)});
})
}
await createSSRStream(); */
ctx.res.write(file1);
const result1 = await task1();
ctx.res.write(result1);
const result2 = await task2();
ctx.res.write(result2);
ctx.res.end();
}
}
export default IndexController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
可以看到最后输出就是两个loading在加载完后变成我们想要的内容
接下来改改books里的东西用一用
booksController.js
import Books from '../models/Books';
import cheerio from 'cheerio';
import {Readable} from 'stream';
class BookController {
async actionIndex(ctx, next) {
ctx.status = 200;
ctx.type = 'html';
const $model = new Books();
const result = await $model.getList();
const html = await ctx.render('books/pages/list', {
result
});
if(ctx.request.header["x-pjax"]){
console.log('站内切');
const $ = cheerio.load(html);
// let _result = '';
$('.pjaxcontent').each(function() {
ctx.res.write($(this).html());
})
$('.lazyload-js').each(function() {
ctx.res.write(`<script src="${$(this).attr("src")}"></script>`);
})
// ctx.body = _result;
ctx.res.end();
} else {
// console.log('直接刷新');
// ctx.body = html;
function createSSRStream() {
return new Promise((resolve, reject) => {
const htmlStream = new Readable();
htmlStream.push(html);
htmlStream.push(null);
htmlStream.on('error',err => {reject(err)}).pipe(ctx.res);
})
}
await createSSRStream();
}
// console.log("返回的值", result);
// ctx.body = html;
}
async actionCreate(ctx, next) {
ctx.body = await ctx.render('books/pages/create');
}
}
export default BookController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
刷新页面可以看到进行了chunk
再配置一下缓存,在开发环境和上线环境上配置 在 src/server/config/index.js
import {extend} from 'lodash';
import {join} from 'path';
let config = {
viewDir: join(__dirname,'..','views'),
staticDir: join(__dirname,'..','assets')
};
if (process.env.NODE_ENV == 'development') {
const localConfig = {
port: 3002,
cache:false,
baseUrl: 'http://192.168.68.138/basic/web/index.php?r='
}
config = extend(config, localConfig);
}
if (process.env.NODE_ENV == 'production') {
const prodConfig = {
port: 80,
cache:'memory',
}
config = extend(config, prodConfig);
}
export default config;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
再在app.js中加入
import 'module-alias/register';
import Koa from 'koa';
const app = new Koa();
import config from '@config';
import controllersInit from './controllers/index';
import render from 'koa-swig';
import { wrap } from 'co';
import serve from 'koa-static';
import errorHandler from "./middlewares/errorHandler";
const { error } = errorHandler;
import { configure, getLogger } from 'log4js';
const { viewDir, staticDir, port, cache } = config;
configure({
appenders: { cheese: { type: 'file', filename: __dirname + '/logs/yd.log' } },
categories: { default: { appenders: ['cheese'], level: 'error' } }
});
const logger = getLogger('cheese');
app.context.logger = logger;
app.context.render = wrap(render({
root: viewDir,
autoescape: true,
cache,
ext: 'html',
writeBody: false
}))
app.use(serve(staticDir));
error(app);
// 路由的注册中心
controllersInit(app);
console.log(port)
app.listen(port, () => {
console.log('服务启动成功!!');
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
再加一个quicklink在空闲时对a标签里的东西进行预加载
src/web/views/layouts/layout.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}My Site{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
{% block head %}
{% endblock %}
</head>
<body>
{% include "../../components/banner/index.html" %}
<div id="app">
{% block content %}{% endblock %}
</div>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<script src="https://cdn.bootcss.com/jquery.pjax/2.0.1/jquery.pjax.js"></script>
<script src="https://cdn.bootcss.com/quicklink/1.0.1/quicklink.umd.js"></script>
<script>
$(document).pjax('a','#app');
quicklink();
</script>
{% block scripts %}{% endblock %}
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
再刷新页面,可以看到network会在空闲时把a标签里的内容也请求出来,大大提高了速度