乐趣区

关于前端:服务器端vue-ssr渲染

从零开始搭建一个 vue-ssr

背景

What?SSR 是什么?

SSR 全拼是 Server-Side Rendering,服务端渲染。
所谓服务端渲染,指的是把 vue 组件在服务器端渲染为组装好的 HTML 字符串,而后将它们间接发送到浏览器,最初须要将这些动态标记混合在客户端上齐全可交互的应用程序。

Why? 为什么抉择 SSR?

①满足 seo 需要,传统的 spa 数据都是异步加载的,爬虫引擎无奈加载,须要利用 ssr 将数据直出渲染在页面源代码中。
②更宽的内容达到工夫(首屏加载更快),当申请页面的时候,服务端渲染完数据之后,把渲染好的页面间接发送给浏览器,并进行渲染。浏览器只须要解析 html 不须要去解析 js。

How?SSR 的原理

借用上面的一张图,咱们来简略论述一下 vue-ssr 的原理。

咱们能够看到,左侧 Source 局部就是咱们所编写的源代码,所有代码有一个公共入口,就是 app.js,紧接着就是服务端的入口
(entry-server.js)和客户端的入口(entry-client.js)。当实现所有源代码的编写之后,咱们通过 webpack 的构建,打包出两个 bundle,别离是 server bundle 和 client bundle;当用户进行页面拜访的时候,先是通过服务端的入口,将 vue 组建组装为 html 字符串,并混入客户端所拜访的 html 模板中,最终就实现了整个 ssr 渲染的过程。

开始搭建

创立一个空白目录并初始化

在终端输出以下命令

mkdir ssr-demo
cd ssr-demo
npm init

因为咱们这个只是一个 demo 我的项目,能够间接一路按回车键,间接疏忽配置。
实现之后咱们能够看到文件夹外面有一个 package.json 的文件,这就是配置表。

装置依赖

该我的项目须要四个依赖,顺次装置

npm install express
npm install vue
npm install vue-router
npm install vue-server-renderer

其中 express 使咱们 node 端的框架,vue 用于创立 vue 实例,vue-router 则用于实现路由管制,最初 vue-server-renderer 尤为要害,咱们实现的 vue-ssr 依附于这个库提供的 API。
在装置依赖结束之后,咱们看到 package.json 中曾经把四个依赖都写上了。

"express": "^4.17.1",
"vue": "^2.6.10",
"vue-router": "^3.0.6",
"vue-server-renderer": "^2.6.10"

创立一个 node 服务

在根目录下咱们新建一个 server.js,用户搭建 node 服务

const express = require("express");
const app = express();

app.get('*', (request, response) => {response.end('hello, ssr');
})

app.listen(3001, () => {console.log('服务已开启')
})

接着为了后续开发的便当,咱们在 package.json 中增加一个启动命令:

"scripts": {
    "test": "echo"Error: no test specified"&& exit 1",
    "server": "node index.js"
 },

接着咱们在终端输出 npm run server,而后再浏览器输出 localhost:3001,便能够看到页面中的文字被胜利渲染。

渲染 html 页面

在上一步咱们曾经能胜利渲染出一个文字,然而 ssr 并不是次要为了渲染文字,而是渲染一个 html 模板。
那么,接下来,咱们得告知浏览器,咱们须要渲染的是 html, 而不只是 text,因而咱们须要批改响应头。
同时,引入 vue-server-renderer 中的 createRenderer 对象,有一个 renderToString 的办法,能够将 vue 实例转成 html 的模式。(renderToString 这个办法承受的第一个参数是 vue 的实例,第二个参数是一个回调函数,如果不想应用回调函数的话,这个办法也返回了一个 Promise 对象,当办法执行胜利之后,会在 then 函数外面返回 html 构造。)
批改 server.js 如下:

const express = require("express");
const app = express();
const Vue = require("vue");
const vueServerRender = require("vue-server-renderer").createRenderer();

app.get('*', (request, response) => {
    const vueApp = new Vue({
        data:{message: "hello, ssr"},
        template: `<h1>{{message}}</h1>`
    });

    response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");
    vueServerRender.renderToString(vueApp).then((html) => {response.end(html);
    }).catch(err => console.log(err))
})

app.listen(3001, () => {console.log('服务已开启')
})

保留代码,重启服务,而后从新刷新页面。咱们发现,页面如同没什么不同,就是字体变粗了而已。其实并不是,你能够尝试查看页面源代码,咱们发现在源代码中,曾经存在一个标签对 h1,这就是 html 模板的雏形。同时,仔细的同学还会发现,h1 下面有一个属性:
data-server-rendered=”true”, 那这个属性是干什么的呢?这个是一个标记,表明这个页面是由 vue-ssr 渲染而来的。大家无妨能够关上一些 seo 页面或者一些公司的网站,查看源代码,你会发现,也是有这个标记。
尽管 h1 标签对被胜利渲染,然而咱们发现这个 html 页面并不残缺,他短少了文档申明,html 标签,body 标签,title 标签等。

将 Vue 实例挂载进 html 模板中

创立一个 index.html,用于挂载 Vue 实例。

<!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>Hello, SSR</title>
</head>
<body>
    <!--vue-ssr-outlet-->
</body>
</html>

留神,body 中的正文不能去掉,这是 Vue 挂载的占位符。
而后批改 server.js,将 html 模板引进去。这里咱们在 createRenderer 函数能够接管一个对象作为配置参数。配置参数中有一项为 template, 这项配置的就是咱们行将应用的 Html 模板。这个接管的不是一个单纯的门路,咱们须要应用 fs 模块将 html 模板读取进去。

let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

保留代码,重启服务,而后从新刷新页面。咱们查看源代码,发现,曾经能胜利渲染出一个残缺的页面了。

创立一个 Vue 我的项目的开发目录

下面的开发模式,很显然只是一个 demo 而已,接下来咱们模仿一下失常的 vue 开发的目录构造。
创立一个 src 文件夹,外面有一个 router 文件夹,再有一个 index,js 用作路由,并创立一个 app.js,用作 vue 的入口,如下图:

批改 router/index.js

const vueRouter = require("vue-router");
const Vue = require("vue");

Vue.use(vueRouter);

module.exports = () => {
    return new vueRouter({
        mode:"history",
        routes:[
            {
                path:"/",
                component:{template:`<h1>this is home page</h1>`},
                name:"home"
            },
            {
                path:"/about",
                component:{template:`<h1>this is about page</h1>`},
                name:"about"
            }
        ]
    })
}

批改 app.js

const Vue = require("vue");
const createRouter = require("./router")

module.exports = (context) => {const router = createRouter();
    return new Vue({
        router,
        data:{message:"Hello,Vue SSR!",},
        template:` <div>
                <h1>{{message}}</h1>
                <ul>
                    <li>
                        <router-link to="/">home</router-link>
                    </li>
                    <li>
                        <router-link to="/about">about</router-link>
                    </li>
                </ul>
                <router-view></router-view>
            </div> ` 
    });
}

而后在 server.js 中,将 app.js 引入

const express = require("express");
const app = express();
const vueApp = require('./src/app.js');

let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

app.get('*', (request, response) => {let vm = vueApp({});

    response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");

    vueServerRender.renderToString(vm).then((html) => {response.end(html);
    }).catch(err => console.log(err))
})

app.listen(3001, () => {console.log('服务已开启')
})

保留代码,重启服务,而后从新刷新页面。而后咱们能够看到浏览器的路由曾经被胜利渲染了,然而无论怎么点击都没反馈,浏览器的 url 有更改,然而页面内容不变。
这是因为咱们只是将页面渲染的工作交给服务端,而页面路由切换,还是在前端执行,服务端并未能接管到该指令,因而无论怎么切换路由,服务端渲染进去的页面基本没变动。

实现服务端管制页面路由

在 src 中创立一个 entry-server.js 文件,该文件为服务端入口文件,接管 app 和 router 实例:

const createApp = require("./app.js");

module.exports = (context) => {return new Promise(async (reslove,reject) => {let {url} = context;

        let {app,router} = createApp(context);
        router.push(url);
        //  router 回调函数
        //  当所有异步申请实现之后就会触发
        router.onReady(() => {let matchedComponents = router.getMatchedComponents();
            if(!matchedComponents.length){return reject();
            }
            reslove(app);
        },reject)
    })
}

在 src 中创立一个 entry-client.js 文件,该文件为客户端入口,负责将路由挂载到 app 外面。

const createApp = require("./app.js");
let {app,router} = createApp({});

router.onReady(() => {app.$mount("#app")
});

批改 app.js,将 router 和 vue 实例裸露进来

const Vue = require("vue");
const createRouter = require("./router")

module.exports = (context) => {const router = createRouter();
    const app =  new Vue({
        router,
        data:{message:"Hello,Vue SSR!",},
        template:` <div>
                <h1>{{message}}</h1>
                <ul>
                    <li>
                        <router-link to="/">home</router-link>
                    </li>
                    <li>
                        <router-link to="/about">about</router-link>
                    </li>
                </ul>
                <router-view></router-view>
            </div> ` 
    });
    return {
        app,
        router
    }
}

最终批改 server.js

const express = require("express");
const app = express();

const App = require('./src/entry-server.js');

let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

app.get('*', async(request, response) => {response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");

    let {url} = request;
    let vm;
    vm = await App({url})
    vueServerRender.renderToString(vm).then((html) => {response.end(html);
    }).catch(err => console.log(err))
})
退出移动版