服务端渲染


服务端渲染 SSR

目标

  • ssr 概念
  • vue ssr 原生实现
  • nuxt.js

理解 ssr

传统服务端渲染 SSR VS 单页面应用 SPA VS 服务端渲染 SSR

传统 web 开发

传统 web 开发,网页内容在服务端渲染完成,一次性传输到浏览器

单页面应用 Single Page App

单页应用优秀的用户体验,使其逐渐成为主流,页面内容由 JS 渲染出来,这种方式称为客户端渲染

Spa 缺点:

  • seo
  • 首屏内容到达时间

服务端渲染 Server Side Render

SSR 解决方案,后端渲染出完整的首屏的 dom 结构返回,前端拿到的内容包括首屏及完整 spa 结构,应用激活后依照 spa 方式运行,这种页面渲染方式被称为服务端渲染(server side render)

服务端渲染

Vue SSR 实战

新建工程

vue-cli 创建工程即可

vue create ssr

安装依赖

npm install vue-server-renderer express -D

确保 vue、vue-server-render、 vue-template-compiler 版本一致

启动脚本

创建一个 express 服务器,将 vue ssr 集成进来

//nodejs代码
//express是我们的web服务器
const express = require("express");
const path = require("path");
const fs = require("fs");
//获取express实例
const server = express();

const vue = require("vue");
//获取绝对路由函数
function resolve(dir) {
  // 把当前执行js文件绝对地址和传入dir做拼接
  return path.resolve(__dirname, dir);
}
//2.获取渲染器实例
const { createRenderer } = require("vue-server-renderer");
const renderer = createRenderer();
//处理favicon
const favicon = require("serve-favicon");
server.use(favicon(path.join(__dirname, "../public", "favicon.ico")));
//静态资源处理
server.use(express.static(resolve("../dist/client"), { index: false }));
//编写路由处理不同url请求
server.get("*", (req, res) => {
  //console.log(req.url)
  //解析模板名称 /user
  const template = req.url.substr(1) || "index";
  //加载模板
  const buffer = fs.readeFileSync(path.join(__dirname, `${template}.html`));

  //res.send('<strong>hello world</strong>')
  //1.创建vue实例
  const app = new Vue({
    template: buffer.toString(), //转换为模板字符串
    data() {
      return { msg: "vue ssr" };
    },
    methods: {
      onClick() {
        console.log("do something");
      },
    },
  });
  //3.用渲染器渲染vue实例
  renderer
    .renderToString(app)
    .then((html) => {
      res.send(html);
    })
    .catch((err) => {
      res.status(500);
      res.send("Internal Server Error, 500!");
    });
});

//监听端口
server.listen(3000, () => {
  console.log("server running!");
});
//解决express前端请求过来的/favicon请求
npm i serve-favicon -s

路由

路由支持仍然使用 vue-router

安装

若未引入 vue-router 则需要安装

npm i vue-router -s

配置

创建@/router/index.js

import Vue from "vue";
import Router from "vue-router";
import Home from "@/views/Home";
import About from "@/views/About";
Vue.use(Router);
//导出工厂函数
//修改1:路由这里是工厂函数
export function createRouter() {
  return new Router({
    routes: [
      { path: "/", component: Home },
      { path: "/about", component: About },
    ],
  });
}

构建

对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包-服务器需要[服务器 bundle]然后用于服务器端渲染(SSR),而[客户端 bundel]会发送给浏览器,用于混合静态标记

构建流程

代码结构

//main.js
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;
//返回工厂函数,每次请求创建一个Vue实例
export function createApp(context) {
  //1.创建路由器实例
  const router = createRouter();
  //2.创建Vue实例
  const app = new Vue({
    router,
    context, //上下文用于给渲染器传递参数
    render: (h) => h(App),
  });

  return { app, router };
}
//entry-server.js
//调用刚才main里面的工厂函数创建实例
import { createApp } from "./main";
//该函数会被express路由处理函数调用,用于创建vue实例
export default (context) => {
  // 返回promise,确保异步的操作都结束
  return new Promise((resolve, reject) => {
    // 获取vue/router实例
    const { app, router, store } = createApp(context);

    //显示首屏处理/跳转至首屏
    //路由模式: hash/history/abstract
    router.push(context.url);
    //检测路由就绪事件
    router.onReady(() => {
      //获取匹配的路由组件数组
      const matchedComponent = router.getMatchedComponents();
      //若无匹配则抛出异常
      if(!matchedComponents.length){
        return reject({code:404})
      }
      //对所有匹配的路由组件调用可能存在的'asyncData()'
      Promise.all(
        //遍历匹配组件
        mathedComponents.map(component => {
          if(Component.asyncData){
            return Component.asyncData({
              store,
              route: router.currentRoute,
            });
          }
        })
      ).then(() => {
        //所有预取钩子resolve后,
        //store已经填充入渲染应用所需状态
        //将状态附加到上下稳,且'template' 选项用于renderer时,
        //状态将自动序列化为`window.__INITIAL_STATE__`,并注入HTML
        context.state = store.state;
        resolve(app);
      })
      .catch(reject); 
    }, reject);
  });
};
//客户端也需要创建vue实例
//entry-client.js
import { createApp } from "./main";
const { app, router,store } = createApp();
//当使用template时,context.state将作为window.__INITIAL_STATE__状态自动嵌入到最终
 
if(window._INITIAL_STATE__){
  //还原store状态
  store.replaceState(window.__INITIAL_STATE__);
}
//路由就绪,执行挂载(激活过程)
router.onReady(() => {
  //挂载激活
  app.$mount("#app");
});

webpack 配置

安装依赖

npm install webpack-node-externals lodash.merge -D

具体配置,vue.config.js


脚本配置

安装依赖

npm i cross-env -D

定义创建脚本,package.json

"scripts": {
  "build:client": "vue-cli-service build",
   "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build", "build": "npm run build:server && npm run build:client"
}

执行打包:npm run build

宿主文件

最后需要定义宿主文件,修改./public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

服务器启动文件

修改服务器启动文件,现在需要处理所有路由,

//nodejs代码
//express是我们的web服务器
const express = require('express')
const path = require('path')
const fs = require('fs')
//获取express实例
const server = express()

const vue = require('vue')
//获取绝对路由函数
function resolve(dir) {
  // 把当前执行js文件绝对地址和传入dir做拼接
  return path.resolve(__dirname,dir )
}
//2.获取渲染器实例 第 2 步:获得一个createBundleRenderer
const { createBundleRenderer } = require('vue-server-renderer')
//参数1服务端brundle,第3步:导入服务端打包文件
const bundle = require(ewsolve('../dist/server/vue-ssr-server-bundle.json'))
// 第 4 步:创建渲染器
 const template = fs.readFileSync(resolve("../public/index.html"), "utf-8");
const clientManifest = require(resolve("../dist/client/vue-ssr-client- manifest.json"));

const renderer = createBundleRenderer(bundle, {
    runInNewContext: false, // https://ssr.vuejs.org/zh/api/#
    runinnewcontext template, // 宿主文件
    clientManifest // 客户端清单
})
//处理favicon
const favicon = require('serve-favicon')
server.use(favicon(path.join(__dirname,'../public','favicon.ico')))
//静态资源处理 第1步:开放dist/client目录,关闭默认下在index页的选项,不然到不了后面路由
// /index.html
server.use(express.static(resolve('../dist/client'),{index: false}))
//编写路由处理不同url请求
server.get('*', async (req,res) => {
  //构造上下文
  const context = {
    title: 'ssr test',
    url: req.url //首屏地址
  }
  try{
    //渲染输出
 const html =  await renderer.renderToString(context)
 //响应给前端
 res.send(html)

  } catch(error) {
    res.status(500).send(''服务器渲染出错)

  }

})

//监听端口
server.listen(3000, () => {
  console.log('server running!')
})

整合VueX

安装vuex

npm install -s vuex

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
VUe.use(Vuex)

//工厂函数
export function createStore () {
  return new Vuex.Store({
    state: {
      count:108
    }
    mutations: {
      add(state){
        state.count += 1;
      }
      //加一个初始化
      init(state, count){
        state.count = count;
      }
    },
    actions: {
      //加一个异步请求count的action
      getCount({commit}){
        //加一个异步请求count的action
        return new Promise(resolve => {
          
        })

      }
    }
  })
}

挂载store,main.js

import {createStore} from './store'
export function createApp (context) {
  //创建实例
  const store = createStore()
  const app = new Vue({
    store,//挂载
    render: h => h(App)
  })
  return { app, router, store}
}

使用,.src/components/index.vue

<h2 @click="$store.commit('add')">{{$store.state.count}}</h2>
//main.js
import Vue from "vue";
import App from "./App.vue";
import { createRouter } from './router';
import { createStore } from './store';

//确保客户端每个组件如果有asyncData,要执行之

Vue.mixin({
  beforeMount() {
    const { asyncData } = this.$options;
    if(asyncData) {
      //将获取数据操作分配给promise
      //以便在组件中,我们可以在数据准备就绪后
      //通过运行`this.dataPromisr.then(...) ` 来执行 其他任务
      this.dataPromise = asyncData({
        store:this.$store,
        route:this.$route,  
      })
    }
  }
})

Vue.config.productionTip = false;
//返回工厂函数,每次请求创建一个Vue实例
export function createApp(context) {
  //1.创建路由器实例
  const router = createRouter(); 
  const store = createStore();
  //2.创建Vue实例
  const app = new Vue({
    router,
    context, //上下文用于给渲染器传递参数
    store
    render: (h) => h(App),
  });

  return { app, router,store };
}

数据预取

服务器端渲染的时应用程序的”快照”,如果应用依赖于一些异步数据,那么在开始渲染之前,需要先预取和解析好这些数据

异步数据获取,store/index.js

export function createStore() {
  return new Vuex.Store({
    mutations: {
      //加一个初始化
      init(state, count){
        state.count = count;
      }
    },
    actions:{
      //加一个异步请求count的action
      getCount({commit}){
        return new Promise(resolve => {
          setTimeput(() => {
            commit("init", Math.random() * 100);
            resolve();
          }, 1000)
        })
      }
    }

  })
}

总结

SSR优缺点都很明显

优点:

  • seo

  • 首屏显示时间

    缺点:

  • 开发逻辑复杂

  • 开发条件限制:比如一些生命周期不能用,一些第三方库会不能用

  • 服务器负载大

    已经存在spa

  • 需要seo页面是否只是少数几个营销页面预渲染是否可以考虑

  • 确实需要做ssr改造,利用服务器端爬虫技术puppeteer

  • 最后选择重构

    全新项目建议nuxt.js

    Nuxt.js实战

    Nuxt.js是一个基于vue。js的通用应用框架。

    通过对客户端/服务端基础架构的抽象组织,Nuxt.js主要关注的是应用的UI渲染。

nuxt.js特性

  • 代码分层
  • 服务端渲染
  • 强大的路由功能
  • 静态文件服务
  • 。。。

nuxt渲染流程

一个完整的服务器请求到渲染的流程

nuxt安装

运行create-nuxt-app

npx create-nuxt-app <项目名>

选项

运行项目: npm run dev

案例

实现如下功能点

  • 服务端渲染
  • 权限控制
  • 全局状态管理
  • 数据接口调用

nuxt路由配置

配置嵌套路由

![](nuxt-children .png)

路由传参


路由跳转

路由配置

视图

下图展示了Nuxt.js如何为指定的路由配置数据和视图

默认布局

查看layouts/default.vue

<template>
    <nuxt/>
</template>

自定义布局

创建空白布局页面layouts/blank.vue,用于login.vue

<template>
  <div>
    <nuxt/>
  </div>
</template>

页面 pages/login.vue 使用自定义布局:

export default { layout: 'blank' }

自定义错误页面

创建layouts/error.vue

<template>
   <div class="container">
    <h1 v-if="error.statusCode === 404">页面不存在</h1>
     <h1 v-else>应用发生错误异常</h1>
      <p>{{error}}</p>
       <nuxt-link to="/">首 页</nuxt-link> 
   </div>
 </template>
 <script>
  export default {
     props: ['error'], layout:'blank' 
     }
 </script>

文章作者: zhengzheng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zhengzheng !
  目录