服务端渲染SSR

2019-10-21  本文已影响0人  key君

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

上一篇下一篇

猜你喜欢

热点阅读