SSR是什么


SSR:Server Side Rendering
服务端渲染,由服务器进行渲染并返回给客户端渲染完成的html

  • 优点
    超快的响应速度
    易做SEO
  • 缺点
    增加服务器压力
  • 主流框架
    Next.js —— React的SSR方案
    Nuxt.js —— Vue的SSR方案

    SPA是什么 ? 它的优缺点是什么?


SPA:single page application
按照字面意思就是单页面应用,通俗点就是整个网站由一个html页面构成。

SPA仅在Web页面初始化时加载相应的HTML、JavaScript 和CSS。 一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML内容的变化,UI与用户的交互,避免页面的重新加载。

优点:

  • 1、用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染。
  • 2、基于上面一点,SPA相对服务器的压力小;
  • 3、前后端指责分离,架构清晰,前端进行交互逻辑,后端负责数据处理。

缺点:

  • 1、初次加载耗时多:为实现单页Web应用功能及显示效果,需要在加载页面的时候将JavaScript、CSS统一加载,部分页面按需加载;
  • 2、前进后退路由管理:由于单页应用在一个页面中进行跳转,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • 3、SEO难度较大:由于所有的内容都在一个页面中动态替换显示,所以在SEO上其有着天然的弱势。
传统的vue/react项目纯浏览器渲染步骤
  1. 浏览器输入url -> 发送请求到服务器
  2. 服务器接收到请求 -> 发送项目的index.html + app.bundle.js文件给浏览器
  3. 浏览器执行js,生成dom,渲染dom,发送请求,接收请求,解析数据,操作数据,重新渲染
  • SPA缺点
  1. 如果没有进行异步请求,首屏加载过慢。(因为要一次性加载多种依赖和包)
    2.缺少SEO, 难以进行搜索引擎优化(对于爬虫来说,它仅仅获取到了2个标签,而没有页面真实呈现内容的信息)
  2. 性能问题
  • SPA优点
  1. 带来接近原生的体验
  2. 前后端分离
  3. 服务器压力小 响应速度快
进行ssr的vue/react项目浏览器渲染步骤
  1. 浏览器输入url -> 发送请求到服务器
  2. 服务器(node服务)接收到请求 -> 解析对应的js文件,生成对应的html->发送给浏览器
  3. 浏览器接收并渲染html

SSR需要哪些配置

image.png

搭建Vue的SSR服务端渲染


在vue项目过中安装vue-server-renderer

1
2
$ npm i vue-server-renderer
$ npm i server

在vue项目中创建server.js文件
server.js文件的内容为:

1
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
/* server.js */
const Vue = require('vue');
const server = require('express')();
const renderer = require('vue-server-renderer').createRenderer();
const fs = require('fs');

function createApp(url) {
if (url == '/') {
url = '/index'
}
let json = fs.readFileSync(`json${url}.json`,'utf_8');
let template = fs.readFileSync(`template${url}.html`,'utf_8');
return new Vue({
template: template,
data: JSON.parse(json).data
})
}
// 响应路由请求
server.get('*', (req, res) => {
if (req.url !=='/favicon.ico') {
const app = createApp(req.url);
renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('运行时错误') }
res.send(html);
});
}
});

// 服务器监听地址
server.listen(8080, () => {
console.log('服务器已启动!')
});

目录结构
image.png
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* index.js */
<!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>Index</title>
</head>
<body>
{{a}}
</body>
</html>
/* index.json */
{
"data": {"a": 1}
}

执行命令

1
$ node server.js

打开浏览器,地址栏输入:

1
localhost:8080

我们可以看到,页面加载成功
image.png
image.png

对原有的Vue项目改造成SSR


  1. 在src文件下新建server.js + client.js
  2. 在根目录下新建index.ssr.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* index.ssr.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>Document</title>
</head>
<body>
<!-- 注意⚠️!!!⬇️下面注释不能少:如果没有,服务器就不知道将生成好的html代码插在什么位置-->
<!--vue-ssr-outlet-->
<script type="text/javescript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>
</html>
  1. 对router部分进行改造
  • 将路由改造成方法
    1
    2
    3
    4
    5
    6
    7
    8
    // export default router;
    export function createRouter(){
    return new VueRouter({
    mode: "history",
    base: process.env.BASE_URL,
    routes
    });
    };
  1. 对main.js进行改造
  • 引入createRouter
  • 将main.js改造成方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import Vue from "vue";
    import App from "./App.vue";
    // import router from "./router";
    import { createRouter } from "./router";
    const router = createRouter();
    // new Vue({
    // router,
    // store,
    // render: h => h(App)
    // }).$mount("#app");
    export function createApp(){
    const app = new Vue({
    router,
    store,
    render: h => h(App)
    });
    return {app, router}
    };

为什么将main.js和router改造成方法? 回答:方便调用!

  1. server.js
    由于sever.js是在服务端运行,我们将代码形成node格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { createApp } from "./main";
    // context = req (服务端的request)
    export function context=>{
    return new Promise((resolve, reject)=> {
    const {app, router} = createApp();
    // 将当前的请求路径添加到路由表中,
    router.push(context.url);
    router.onReady(() => {
    const matchCcmponents = router.getMatchedComponents(path);
    if (!matchCcmponents.length) {
    return reject({code: 404})
    }
    resolve(app)
    }, reject);
    })
    };
  2. client.js

    1
    2
    3
    4
    5
    6
    import { createApp } from "./main";
    const {app, router} = createApp();
    router.onReady(() => {
    // 手动挂载;
    app.$mount('#app');
    });
  3. 在build新建webpack.buildclinet.js+webpack.buildserver.js

  4. webpack.buildserver.js
    webpack.buildserver.js类似于webpack.prod.conf.js
    对部分内容进行改造:
1
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
var VueSSRServerPlugin = require('vue-server-renderer/server-plugin')


// 指定entry,传统SPA打包配置文件没有entry,打包时是使用webpack.base.conf.js + webpack.prod.conf.js合并之后的entry
entry: {
app: './src/server.js'
},
// 打包之后的结果是在服务端运行的。
target: 'node',
output: {
// 打包之后的文件的模块化规范,遵循node的模块化规范
libraryTarget: "commonjs2"
},
new HtmlWebpackPlugin({
filename: 'index.srr.html',
template: 'index.srr.html',
inject: true,
files: {
js: 'app.js'
},
// 注意⚠️!!!删除掉压缩部分配置代码⬇️:为什么不能压缩,看第2步的解释(你能找到解释么?嘿嘿)!
// minify: {
//removeComments: true,
//collapseWhitespace: true,
//removeAttributeQuotes: true
//},
chunksSortMode: 'dependency'
}),
plugins: [
// 插件作用:对服务端代码进行打包
new VueSSRServerPlugin(),
......
  1. webpack.buildclient.js
    webpack.buildclient.js类似于webpack.prod.conf.js
    对部分内容进行改造:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

    // 指定entry,传统SPA打包配置文件没有entry,打包时是使用webpack.base.conf.js + webpack.prod.conf.js合并之后的entry
    entry: {
    app: './src/client.js'
    },
    // 注意⚠️!!!删掉出口配置output:

    plugins: [
    // 插件作用:对客户端代码进行打包
    new VueSSRClientPlugin(),
    ......
  2. 增加打包命令

1
2
"build:client": "webpack --config build/webpack.buildclient.js"
"build:server": "webpack --config build/webpack.buildserver.js"
  1. 执行打包命令
1
2
$ npm run build:server
$ npm run build:client

打包之后结果
image.png
image.png

12 编写server

  • 在根目录下新建server文件夹 + server.js
    server.js
    1
    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
    const express = require('express');
    const server = express();
    const {createBundleRenderer} = require('vue-server-renderder');
    const path = require('path');
    const fs = require('fs');
    const serverBundle = require(path.resolve(__dirname, '../dist/vue-ssr-server-bundle.json'));
    const clientManifest = require(path.resolve(__dirname, '../dist/vue-ssr-client-manifest.json'));
    const template= fs.readFileSync(path.resolve(__dirname, '..dist/index.ssr.html'), 'utf-8');
    // 打包出来的json的作用:通知服务器如何分割js,部分js用于客户端执行,部分js用于服务端运行。
    const renderer = createBundleRenderer(serverBundle,{
    runInNewContext: false,
    template:template,
    clientManifest:clientManifest
    });
    // 设置静态目录,以dist文件夹为静态目录,dist文件夹在服务开始之后可以访问
    server.use(express.static(path.resolve(__dirname, '../dist')));
    // 设置路由
    server.get('*',(req,res)=>{
    if (req.url !=='/favicon.ico') {
    const context = {url: req.url};
    // 在项目中生成的html文件巨大,通过流的方式处理大的文件
    const ssrstream = renderer.renderToStream(context);
    let buffers = [];
    ssrstream.on('error', (err) => {
    console.log(err);
    });
    ssrstream.on('data', (data) => buffers.push(data));
    ssrstream.on('end', () => {
    res.end(Buffer.concat(buffers));
    });
    }
    });
    server.listen(2000);

13 node 运行server.js

1
$ node server.js

项目运行之后,会发现切换页面,都会重新请求页面刷新。


总结:大功告成✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️