初步应用

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-28

SSR概念

将app在服务端渲染成一个完整的 HTML 字符串并发送到浏览器,最后再将这些静态标记“激活”为可交互应用程序的过程称为服务端渲染。更多详情

SSR工作流程

启用一个node中间层服务端,对于页面资源的请求,在node中间层进行SSR渲染,把渲染完的完整HTML发送给浏览器,就可以减少首屏渲染时间。而对于其他的数据接口API就向web服务器去请求,因为返回的HTML页面通过加载客户端相关js,就会“恢复”成SPA应用,之后就只与web服务端做ajax交互。

这样的弊端有:

  • 服务器负载会变大,每一次用户请求都会创建一个 vue 实例 (或者需要 Vuex、VueRouter插件的时候,也需要每次请求创建对应的实例),所以你需要确保有效地缓存页面。

  • 开发变得更加困难

    • 编写代码需要兼容浏览器端和服务端,因为有的API只在浏览器端支持,在服务端不支持。
    • 项目构建需要构建两套环境,需要编写中间层。

代码结构

src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用入口
├── entry-client.js # 客户端入口,仅运行于浏览器 用于将应用激活成SPA
└── entry-server.js # 服务端入口,仅运行于服务器

vue-ssr-flow

基本使用

安装依赖: npm i vue-server-renderer express -D

最简易的SSR代码:

// server.js

const express = require('express');
const Vue = require('vue'); // 在node环境引入Vue

// 创建express实例和Vue实例
const app = express();

// 创建渲染器
const renderer = require('vue-server-renderer').createRenderer();

// 将来用渲染器渲染Page
const page = new Vue({
  data: { title: '我的app' }
  template: `
    <div>
      <h1>{{title}}</h1>
      <p>hello</p>
    </div>`
})
// template 一样要遵循vue的只有一个根标签

// 监听服务
app.get('/', async  (req, res) => {
  try {
    const html = await renderer.renderToString(page);
    res.send(html);
  } catch (error) {
    res.status(500).send('服务器内部错误');
  }
})

app.listen(8080);

编写代码

结合 Vue Router

安装: npm i vue-router -S

// router/index.js

import Vue from 'vue';
import Router from 'vue-router';

import Index from '@/components/Index';
import Detail from '@/components/Detail';

Vue.use(Router);

// 这里为什么不导出一个 router 实例 ?
// 因为避免污染,不然整个应用都只有一个 Vue 实例 和 一个 VueRouter 实例
// 但多个用户请求的时候就都是同一个了 就会弄混淆 导致异常
// 所以每次请求都是返回一个Vue实例和VueRouter实例 (上面的server.js只是简版代码)
export default function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      { path: '/', component: Index },
      { path: '/detial', component: Detail },
    ]
  })
}

通用入口文件(app.js)

使用工厂函数来创建Vue实例,给服务端和客户端引用,相当于 main.js

// app.js
import Vue from "vue";
import App from "./App.vue";
import createRouter from "./router";

export default function createApp() {
  const router = createRouter();
  const app = new Vue({
    router,
    render: h => h(App),
  });
  // 不需要挂载 因为最终直接访问就是 HTML文档字符串了
  // SPA应用的时候 返回空div 所以需要 $mount 来完成客户端渲染
  return { app, router };
}

服务端入口(entry-server.js)

// entry-server.js
import createApp from './app';

export default (context) => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp();
    // 进入首屏 context 是在 server.js 渲染 HTML 模板的时候传入的
    // 当用户访问 localhost:8080/about
    // server.js 中间层应用程序会将 req.url 即 /about 传入到 context
    router.push(context.url);
    router.onReady(() => {
      // 路由可能是异步的 整个函数返回一个Promise
      // 这里在router准备就绪的情况下 resolve 中 vue 实例 app
      const matchedComponents = router.getMatchedComponents();
      // 匹配不到的路由,执行 reject 函数,并返回 404
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      resolve(app);
    }, reject)
  })
}

客户端入口(entry-client.js)

// entry-client.js

// 只有一个作用:挂载、激活app
import createApp from './app';

const {app, router } = createApp();

// 需要注意的是,你仍然需要在挂载 app 之前调用 router.onReady
// 因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子

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

处理Vue SSR node服务端代码(server/index.js)

// server/index.js
// nodejs服务器
const express = require("express");
const Vue = require("vue");
const fs = require('fs')

// 创建express实例和vue实例
const app = express();
// 创建渲染器,这里使用createBundleRenderer
const { createBundleRenderer } = require("vue-server-renderer");
// 通过webpack加对应插件生产的对应的客户端打包代码和服务端打包代码
const serverBundle = require('../dist/server/vue-ssr-server-bundle.json');
const clientManifest = require('../dist/client/vue-ssr-client-manifest.json');

// 创建用于ssr的renderer
// 该renderer能够生成对应的完整html
const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template: fs.readFileSync('../public/index.temp.html', 'utf-8'), // 宿主模板文件
  clientManifest, 
  // 这个完整的html需要用到的一些客户端js,css等文件,会通过script标签defer延迟引用
  // 因为首次返回的是纯html文件,只是首页的内容,而之后的交互需要激活以及对应的静态文件
})

// 中间件处理静态文件请求
// index:false,是因为当我们访问项目根路径的时候,默认都是首页,而此时我们希望程序进入到我们的ssr服务端代码中
// 由于客户端静态文件也是会生产一个 index.html 文件用于客户端渲染
// 如果不关闭,则会访问 ../dist/client/index.html 而不会进入到我们的ssr程序中
app.use(express.static('../dist/client', {index: false}))
// app.use(express.static('../dist/client'))

// 路由处理交给vue
app.get("*", async (req, res) => {
  try {
    const context = {
      url: req.url,
      title: 'ssr test'
    }
    const html = await renderer.renderToString(context);
    console.log(html);
    res.send(html);
  } catch (error) {
    res.status(500).send("服务器内部错误");
  }
});

app.listen(3000, () => {
  console.log("渲染服务器启动成功");
});

示例代码

demo github地址

示例演示:
1、安装依赖 npm i --registry https://registry.npm.taobao.org
2、执行 npm run build
3、执行 node ./server/index2.js