服务端渲染SSR

php .net
传统web
客户端发送一次请求 服务端返回html模板 su友好


image.png

SPA:单页应用 vue react CSR:客户端渲染
客户端发送第一次请求 服务端返回html结构
再发送请求要数据 su不友好


image.png

SSR:服务端渲染
后端渲染出完整的首屏dom结构 前端拿到内容包括首屏及完整spa结构 应用激活后依然按照spa方式运行


image.png

新建工程
(https://github.com/iosKey/VueSSR)
vue create ssr
安装依赖
npm install vue-server-renderer express -D

这里是静态替换 只是一个例子

创建server/index.js
创建一个渲染器 把一个vue的实例转成html返回

const express = require('express')
const Vue = require('vue')

const app = express();
const page = new Vue({
    data: {name:'开课吧'},
    template: '<div>{{name}}</div>'
})

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

app.get('/', async function(req, res){
    // 2.执行渲染
    const html = await renderer.renderToString(page)
    res.send(html);
})

//监听端口
app.listen(3000, () => {
    console.log('渲染服务器就绪');
    
})

启动服务器
cd server
node .\index.js
浏览器打开localhost:3000

这里开始是动态替换

接下来用Vue router来管理页面
安装Vue router:
npm i vue-router -S
创建src/router/index.js

import Vue from "vue";
import Router from "vue-router";
// 分别创建Index.vue和Detail.vue
import Index from "@/views/Index";
import Detail from "@/views/Detail";

Vue.use(Router);

//导出工厂函数

export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      { path: "/", component: Index },
      { path: "/detail", component: Detail }
    ]
  });
}

创建src/views/Index.vue

<template>
  <div>
    index page
    <h2>num:{{$store.state.count}}</h2>
    <button @click="$store.commit('add')">add</button>
  </div>
</template>

<script>
//数据预取
export default {
  asyncData({ store, route }) {
    // 约定预取逻辑编写在预取钩子asyncData中
    // 触发 action 后,返回 Promise 以便确定请求结果
    return store.dispatch("getCount");
  }
};
</script>

<style lang="scss" scoped>
</style>

创建src/views/Detail.vue

<template>
    <div>
        detail page
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style lang="scss" scoped>

</style>

src/App.vue

<template>
  <div id="app">
    <nav>
      <router-link to="/">首页</router-link>
      <router-link to="/detail">详情页</router-link>
    </nav>
    <router-view></router-view>
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  components: {
    HelloWorld
  },
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

服务端渲染的构建过程
通过webpack打包 生成server bundle和client bundle
server bundle:处理首屏渲染,生成html,包括spa的构成部分,用于服务器端渲染
client bundle:描述spa构成结构和激活,发动给浏览器

整合Vuex
安装

npm install -S vuex
创建src/store/index.js

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

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

src创建app.js (定义创建vue实例方法)传入路由、store实例

// 创建Vue实例
import Vue from 'vue'
import App from './App.vue'
import {createRouter} from './router'
import { createStore } from './store'

// 客户端挂载之前,检查组件是否存在异步数据获取
Vue.mixin({
    beforeMount() {
      const { asyncData } = this.$options;
      if (asyncData) {
        // 将获取数据操作分配给 promise
        // 以便在组件中,我们可以在数据准备就绪后
        // 通过运行 `this.dataPromise.then(...)` 来执行其他任务
        this.dataPromise = asyncData({
          store: this.$store,
          route: this.$route,
        });
      }
    },
  });

export function createApp(context) {
    // 1.获取路由实例
    const router = createRouter();
    // 2.获取store实例
    const store = createStore()
    // 2.创建vue实例
    const app = new Vue({
        router,
        store,
        context,
        render: h => h(App)
    })
    return {app, router, store}
}

根目录创建entry-server.js(创建vue实例 路由跳转 返回vue实例)

// 创建vue实例并且做首屏渲染
import {createApp} from './app'

export default context => {
    return new Promise((resolve, reject) => {
        const {app,router,store} = createApp(context)
        // 跳转首屏
        router.push(context.url)
        router.onReady(() => {
            // 获取匹配的路由组件数组
            const matchedComponents = router.getMatchedComponents();
            if (!matchedComponents.length) {
              return reject({ code: 404 });
            }
            
            // 对所有匹配的路由组件调用 `asyncData()`
            Promise.all(
              matchedComponents.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);
    })
}

根目录创建entry-client.js(挂载到#app上)

// 客户端激活
import { createApp } from "./app";

const { app, router, store } = createApp();

// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态自动嵌入到最终的 HTML // 在客户端挂载到应用程序之前,store 就应该获取到状态:
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

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

做webpack配置
根目录创建vue.config.js

const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");

const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
  css: {
    extract: false
  },
  outputDir: './dist/'+target,
  configureWebpack: () => ({
    // 将 entry 指向应用程序的 server / client 文件
    entry: `./src/entry-${target}.js`,
    // 对 bundle renderer 提供 source map 支持
    devtool: 'source-map',
    // 这允许 webpack 以 Node 适用方式处理动态导入(dynamic import),
    // 并且还会在编译 Vue 组件时告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
    target: TARGET_NODE ? "node" : "web",
    node: TARGET_NODE ? undefined : false,
    output: {
      // 此处使用 Node 风格导出模块
      libraryTarget: TARGET_NODE ? "commonjs2" : undefined
    },
    // 这是将服务器的整个输出构建为单个 JSON 文件的插件。
    // 服务端默认文件名为 `vue-ssr-server-bundle.json`
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  })
};

安装依赖

npm i cross-env

在package.json配置指令

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

执行 在dist打出client和server两个包

npm run build

根目录创建宿主文件 public/index.tmpl.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>

根目录创建server/index2.js(创建渲染器 配置dist里面client和server包的路径和宿主文件 执行渲染)首屏渲染才会走服务器

const express = require('express')
const fs = require('fs')

const app = express();

// 1.渲染器
const {createBundleRenderer} = require('vue-server-renderer');
const bundle = require('../dist/server/vue-ssr-server-bundle.json')
//解决报错和页面刷新 指定为静态目录
app.use(express.static('../dist/client', {index: false}))

// bundle是服务端包
const renderer = createBundleRenderer(bundle, {
    runInNewContext: false,
    template: fs.readFileSync('../public/index.tmpl.html', "utf-8"),
    clientManifest: require('../dist/client/vue-ssr-client-manifest.json')
})

app.get('*', async function(req, res) {
    console.log(req.url);
    
    const context = {
        title: 'SSR Test',
        url: req.url
    }

    // 2.执行渲染
    const html = await renderer.renderToString(context)
    res.send(html);
})

app.listen(3000, () => {
    console.log('渲染服务器就绪');
    
})

渲染服务器 cd进server目录

node .\index2.js

修改代码需要重新npm run build
然后重启服务器 node .\index2.js

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容