美文网首页
服务端渲染SSR

服务端渲染SSR

作者: key君 | 来源:发表于2019-10-21 10:52 被阅读0次

    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

    相关文章

      网友评论

          本文标题:服务端渲染SSR

          本文链接:https://www.haomeiwen.com/subject/dcjzmctx.html