Fork me on GitHub

服务端渲染原理及实现入门

一、 CSR vs SSR

不同于传统拉取JS进行解析渲染的CSR(JS负责进行页面渲染),SSR实现了服务器端直接返回Html代码让浏览器进行渲染。
由此,我们就很容易理解以下代码实现了一个页面SSR:
// server.js

var express = require('express')
var app = express()
app.get('/', (req, res) => {
 res.send(
 `
   <html>
     <head>
       <title>hello</title>
     </head>
     <body>
       <h1>hello, this is SSR content.</h1>
       <p>now, let's begin to learn SSR.</p>
     </body>
   </html>
 `
 )
})
app.listen(3001, () => {
 console.log('listen:3001')
})

SSR优点:缩短首屏加载时间,便于SEO。

二、Vue项目的SSR实现

刚刚我们仅仅做到了让服务器返回一段html字符串,那么,要如何实现Vue项目的服务端渲染呢?
由于整个项目的复杂性,我们可能一时无从下手,但没关系,我们可以先从实现一个Vue组件的服务端渲染开始,在此之前,我们先从Vue实例的SSR开始吧!

1. 实现一个Vue实例的SSR

首先实现一个Vue实例,这个倒是简单,那么问题来了:“我们要如何将它转换成html代码返回给浏览器呢?
大家都学过vue,当然知道Vue的标签是基于虚拟DOM(JS对象)的,在客户端渲染中也是采用一定方法将虚拟DOM渲染为真实DOM的,那么服务端的渲染流程也是通过虚拟DOM的编译来完成的,编译虚拟DOM的方法是renderToString。在Vue中,vue-server-renderer 提供一个名为 createBundleRenderer 的 API,这个API用于创建一个 render,并且自带renderToString方法。
官网实现代码如下:
// server.js

const Vue = require('vue')
const server = require('express')()
// 创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
  // 第 1 步:创建一个 Vue 实例
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>访问的 URL 是: {{ url }}</div>`
  })
  // 第 2 步:将 Vue 实例渲染为 HTML
  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head>
          // <meta charset="utf-8"> // 加上这一行就不会出现乱码了
          <title>Hello</title>
        </head>
        <body>${html}</body>
      </html>
    `)
  })
})
server.listen(8080)

启动express服务,再浏览器上打开对应端口,页面就能显示出你在Home组件中编写的内容了。但不出意外的话,大家看到页面渲染出来的是一段乱码,这是因为官网提供的示例代码返回的html字符串里没有带 ,加上它就OK了,在上面代码中直接取消这一行注释, 至此,我们初步实现了一个Vue实例的服务端渲染。
TODO:(modify)
当你在渲染 Vue 应用程序时,renderer 只从应用程序生成 HTML 标记 (markup)。在这个示例中,我们必须用一个额外的 HTML 页面包裹容器,来包裹生成的 HTML 标记。
为了简化这些,你可以直接在创建 renderer 时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中,例如 index.template.html:

<!-- index.template.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
    <title>{{ title }}</title>
    <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
    {{{ metas }}}
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

注意 注释 -- 这里将是应用程序 HTML 标记注入的地方。
然后,我们可以读取和传输文件到 Vue renderer 中:
// server.js

const renderer = require('vue-server-renderer').createRenderer({
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
  console.log(html) // html 将是注入应用程序内容的完整页面
})

我们可以通过传入一个"渲染上下文对象",作为 renderToString 函数的第二个参数,来提供模板插值数据,也可以与 Vue 应用程序实例共享 context 对象,允许模板插值中的组件动态地注册数据。
完整代码示例:
// server.js

const Vue = require('vue');
const server = require('express')();
const template = require('fs').readFileSync('./index.template.html', 'utf-8');
const renderer = require('vue-server-renderer').createRenderer({
  template,
});
const context = {
    title: 'vue ssr',
    metas: `
        <meta name="keyword" content="vue,ssr">
        <meta name="description" content="vue srr demo">
    `,
};
server.get('*', (req, res) => {
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>访问的 URL 是: {{ url }}</div>`,
  });
  renderer.renderToString(app, context, (err, html) => {
    console.log(html);
    if (err) {
      res.status(500).end('Internal Server Error')
      return;
    }
    res.end(html);
  });
})
server.listen(8080);

TODO:(个人理解)
以下是个人对SSR的理解:服务端渲染实际是一套代码的两次应用,所谓的一套代码就是拿出server.js外面去的vm实例,上面之所以简单是因为我们在server内部创建的vm实例,一旦将vm拿出去,在server.js外部引入,那么涉及的就麻烦了。
这里分两条线说,一个是在server.js外面创建一个app.js;结果是无法引入到server中,而这个也不是关注的重点;
另一条线是使用vue-loader创建一个vm实例,然后引入到server中,整个vue渲染就在解决这个问题,解决引入的问题,解决引入之后与前端混合的问题。下面贴上简单案例的实现代码。
因为不能直接应用.vue文件以及外部的js文件,所以需要借助webpack,借助webpack将vue实例,转译为node可用代码,以及对前端代码进行转译。
以vue init webpack-simple vuessr0 为基础的vue-ssr案例
官网中有提到:每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染。
意思就是:每次服务端渲染都要渲染一个新的app,不能再用上一次渲染过的app对象去进行下一次渲染,这是由于app已经包含上一次渲染过的状态会影响我们渲染内容,所以每次都要创建新的app,即为每个请求创建一个新的根Vue实例。因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例:
// app.js

const Vue = require('vue')
module.exports= function createApp (context) {
  return new Vue({
    data: {
      title: context.title, 
      url: context.url
    },
    template: `
      <div>
        <h2>{{ title }}</h2>
        <div>访问的 URL 是:{{ url }}</div>
      </div>
    `
  })
}

服务器代码只需要更改下vue实例的生成方式就可以了:
// server.js

const createApp = require('./app')
const server = require('express')()
// 创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer({
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
server.get('*', (req, res) => {
  // 创建一个"渲染上下文对象"
  const context = {
    title: 'vue ssr',
    metas: `
      <meta name="keyword" content="vue,ssr">
      <meta name="description" content="vue srr demo">
    `,
    url: req.url
  }
  const app = createApp(context)
  // 第 2 步:将 Vue 实例渲染为 HTML
  renderer.renderToString(app, context, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    console.log(html)
    res.end(html)
  })
})
server.listen(8080)

2. 实现一个Vue组件的SSR

既然涉及到组件,那我们就必须加入路由了,官方建议使用 vue-router。同时,需要webpack来构建项目。
首先创建router.js,实现给每个请求创建一个新的 router 实例,我们在router.js中导出一个 createRouter 函数,然后更新app.js。
// router.js

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter () {
  return new Router({
    mode: 'history',
    routes: [
      // ...
    ]
  })
}

// app.js

const Vue = require('vue')
module.exports= function createApp (context) {
  return new Vue({
    data: {
      title: context.title, 
      url: context.url
    },
    template: `
      <div>
        <h2>{{ title }}</h2>
        <div>访问的 URL 是:{{ url }}</div>
      </div>
    `
  })
}
import App from './App.vue'
import { createRouter } from './router'
export function createApp () {
  // 创建 router 实例
  const router = createRouter()
  const app = new Vue({
    // 注入 router 到根 Vue 实例
    router,
    render: h => h(App)
  })
  // 返回 app 和 router
  return { app, router }
}

首先实现一个Vue组件 src/views/home/index.vue,那么接下来我们仍旧是通过renderToString方法编译Vue组件来实现服务端渲染。

const Vue = require('vue');
const server = require('express')();
// 引入Vue组件
import Home from './containers/Home';
const template = require('fs').readFileSync('./index.template.html', 'utf-8');
const renderer = require('vue-server-renderer').createRenderer({
  template,
});
const context = {
    title: 'vue ssr',
    metas: `
        <meta name="keyword" content="vue,ssr">
        <meta name="description" content="vue srr demo">
    `,
};
server.get('*', (req, res) => {
  // 替换为Vue组件
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>访问的 URL 是: {{ url }}</div>`,
  });
  renderer.renderToString(app, context, (err, html) => {
    console.log(html);
    if (err) {
      res.status(500).end('Internal Server Error')
      return;
    }
    res.end(html);
  });
})
server.listen(8080);

3. 初识同构

首先我们来了解下什么是重构?--->
前提:VUE框架用于构建客户端应用,在浏览器中输出Vue组件,生成并操作DOM。将同一个组件渲染为服务器端的HTML字符串发送到浏览器,然后将这些静态标记“激活”为客户端上可交互的应用程序。
服务器渲染的Vue.js应用程序为"同构"或"通用",应为应用程序的大部分代码都可以在服务端和客户端运行。
通俗的讲,就是一套Vue代码在服务器上运行一遍,到达浏览器又运行一遍。服务端渲染完成页面结构,浏览器端渲染完成事件绑定,这个完成事件绑定的过程就是静态标记“激活”为客户端上可交互应用程序的过程。
那么如何进行浏览器端的事件绑定呢?
首先我们要明白:
只要开启express的静态文件服务,前端的script就能拿到控制浏览器的JS代码啦!
4. node作中间层及请求代码优化
作用:解决前后端协作问题
场景:
在不用中间层的前后端分离开发模式下,前端直接请求后端接口获得返回的数据,但这个返回数据的数据格式也许并非是前端需要的,但出于性能原因或其他因素无法更改接口,就需要前端来做一些数据处理操作,这无疑会产生前端性能损耗,尤其当前端处理数据量很大的时候,甚至会影响用户体验。于是引入node中间层,用于替代前端做数据处理操作,中间层的工作流:前端发送请求--->请求node层的接口--->node对于相应的前端请求做转发,用node去请求真正的后端接口获取数据--->获取后再由node层做对应的数据计算等处理操作--->返回给前端。

posted @ 2021-02-21 21:12  Lynn_z  阅读(937)  评论(0编辑  收藏  举报