美文网首页
vue-服务端渲(ssr)

vue-服务端渲(ssr)

作者: 一点金光 | 来源:发表于2019-08-22 13:04 被阅读0次

    几个概念

    服务端渲染(SSR):在服务端生成HTML 字符串,发送到浏览器。
    客户端渲染(CSR):在浏览器中输出 Vue 组件,生成静态html内容和注入 DOM。
    浏览器渲染(BSR):浏览器对html字符串进行渲染。
    服务端预渲(SPR):在服务端针对特定路由生成静态 HTML字符串。
    
    

    基本用法

    # 安装类库
    ## 推荐使用 Node.js 版本 6+。
    ## vue-server-renderer 和 vue 必须匹配版本
    vue & vue-server-renderer 2.3.0+
    vue-router 2.5.0+
    vue-loader 12.0.0+ & vue-style-loader 3.0.0+
    
    # 渲染实例
    ## 创建实例
    
    ## 创渲染器
    
    ## 生成静页
    
    # 集服务器
    ## 安装类库
    
    ## 类实例化
    
    ## 获取请求
    
    ## 发送响应
    
    # 使用模板
    ## 标记注入
    <!--vue-ssr-outlet-->
    
    ## 模板插值
    
    ## 样式注入
    在使用 *.vue 组件时,自动注入
    
    。。。
    
    

    通用代码

    # 服务器上的数据响应
    
    # 组件生命周期钩子函
    ## 服务器渲:beforeCreate 、created
    ## 客户端渲:
    ## 避副作用:将副作用代码移动到 beforeMount 或 mounted 生命周期中。
    
    # 访问特定平台接口
    ## 浏览器用:
    ## 服务器用:
    ## 通用接口:
    
    # 自定义指令的实现
    ## 方式1:使用组件作为抽象机制
    ## 方式2:建服务器渲染器时注册
    

    源码结构

    # 避免状态单例
    ## 情景:每个用户应该有自己的状态,应避免交叉请求,以避免状态污染。
    ## 解决:
    ### 方式1:通过工厂函数
    #4 steps-01:创建应用
    // app.js
    const Vue = require('vue')
    
    module.exports = function createApp (context) {
      return new Vue({
        data: {
          url: context.url
        },
        template: `<div>访问的 URL 是: {{ url }}</div>`
      })
    }
    
    #4 steps-02:应用实例
    // server.js
    const createApp = require('./app')
    
    server.get('*', (req, res) => {
      const context = { url: req.url }
      const app = createApp(context)
    
      renderer.renderToString(app, (err, html) => {
        // 处理错误……
        res.end(html)
      })
    })
    
    ## 介绍构建步骤
    
    

    路由和代码分割

    # 使用路由
    ## 定义路由
    // router.js
    import Vue from 'vue'
    import Router from 'vue-router'
    
    Vue.use(Router)
    
    export function createRouter () {
      return new Router({
        mode: 'history',
        routes: [
          // ...
        ]
      })
    }
    
    ## 应用实例
    // app.js
    import Vue from 'vue'
    import App from './App.vue'
    import { createRouter } from './router'
    
    export function createApp () {
      // 创建 router 实例
      const router = createRouter()
    
      const app = new Vue({
        // 注入 router 到根 Vue 实例
        router,
        render: h => h(App)
      })
    
      // 返回 app 和 router
      return { app, router }
    }
    
    ## 组件匹配
    // entry-server.js
    import { createApp } from './app'
    
    ## 数据注入
    // server.js
    const createApp = require('/path/to/built-server-bundle.js')
    
    server.get('*', (req, res) => {
      //获取请求路径
      const context = { url: req.url }
      
      createApp(context).then(app => {
        //生成静态网页
        renderer.renderToString(app, (err, html) => {
          if (err) {
            if (err.code === 404) {
              res.status(404).end('Page not found')
            } else {
              res.status(500).end('Internal Server Error')
            }
          } else {
            res.end(html)
          }
        })
      })
    })
    
    export default context => {
      // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
        // 以便服务器能够等待所有的内容在渲染前,
        // 就已经准备就绪。
      return new Promise((resolve, reject) => {
        const { app, router } = createApp()
    
        // 设置服务器端 router 的位置
        router.push(context.url)
    
        // 等到 router 将可能的异步组件和钩子函数解析完
        router.onReady(() => {
          // 路由匹配对应组件
          const matchedComponents = router.getMatchedComponents()
          // 路由没找不到组件,执行 reject 函数,并返回 404
          if (!matchedComponents.length) {
            return reject({ code: 404 })
          }
    
          // Promise 应该 resolve 应用程序实例,以便它可以渲染
          resolve(app)
        }, reject)
      })
    }
    
    # 代码分割
    ## 情景:有助于减少浏览器在初始渲染中下载的资源体积,可以极大地改善大体积 bundle 的可交互时间(TTI - time-to-interactive)。对初始首屏而言,"只加载所需"。
    ## 动态引入(异步组件)
    const Foo = () => import('./Foo.vue')
    
    ## 应用挂载
    // entry-client.js
    
    import { createApp } from './app'
    
    const { app, router } = createApp()
    
    router.onReady(() => {
      app.$mount('#app')
    })
    ## 路由定义
    // router.js
    import Vue from 'vue'
    import Router from 'vue-router'
    
    Vue.use(Router)
    
    export function createRouter () {
      return new Router({
        mode: 'history',
        routes: [
          { path: '/', component: () => import('./components/Home.vue') },
          { path: '/item/:id', component: () => import('./components/Item.vue') }
        ]
      })
    }
    

    数据预取和状态

    # steps-01:在渲染之前预取数据
    
    # steps-02:将数据填充到容器中
    
    
    ## 数据寄存
    // store.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    // 假定我们有一个可以返回 Promise 的
    // 通用 API(请忽略此 API 具体实现细节)
    import { fetchItem } from './api'
    
    export function createStore () {
      return new Vuex.Store({
        state: {
          items: {}
        },
        actions: {
          fetchItem ({ commit }, id) {
            // `store.dispatch()` 会返回 Promise,
            // 以便我们能够知道数据在何时更新
            return fetchItem(id).then(item => {
              commit('setItem', { id, item })
            })
          }
        },
        mutations: {
          setItem (state, { id, item }) {
            Vue.set(state.items, id, item)
          }
        }
      })
    }
    
    ## 预取数据
    <!-- Item.vue -->
    <template>
      <div>{{ item.title }}</div>
    </template>
    <script>
    export default {
      asyncData ({ store, route }) {
        // 触发 action 后,会返回 Promise
        return store.dispatch('fetchItem', route.params.id)
      },
      computed: {
        // 从 store 的 state 对象中的获取 item。
        item () {
          return this.$store.state.items[this.$route.params.id]
        }
      }
    }
    </script>
    
    ## 应用实例
    // app.js
    import Vue from 'vue'
    import App from './App.vue'
    import { createRouter } from './router'
    import { createStore } from './store'
    import { sync } from 'vuex-router-sync'
    
    export function createApp () {
      // 创建 router 和 store 实例
      const router = createRouter()
      const store = createStore()
    
      // 同步路由状态(route state)到 store
      sync(store, router)
    
      // 创建应用程序实例,将 router 和 store 注入
      const app = new Vue({
        router,
        store,
        render: h => h(App)
      })
    
      // 暴露 app, router 和 store。
      return { app, router, store }
    }
    
    
    
    # 服务器数据预取
    // entry-server.js
    import { createApp } from './app'
    
    export default context => {
      return new Promise((resolve, reject) => {
        const { app, router, store } = createApp()
    
        router.push(context.url)
    
        router.onReady(() => {
          // 路由匹配到相应组件
          const matchedComponents = router.getMatchedComponents()
          if (!matchedComponents.length) {
            return reject({ code: 404 })
          }
    
          // 所得组件预取数据
          Promise.all(matchedComponents.map(Component => {
            if (Component.asyncData) {
              return Component.asyncData({
                store,
                route: router.currentRoute
              })
            }
          })).then(() => {
            // 在所有预取钩子(preFetch hook) resolve 后,
            // 我们的 store 现在已经填充入渲染应用程序所需的状态。
            // 当我们将状态附加到上下文,
            // 并且 `template` 选项用于 renderer 时,
            // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
            context.state = store.state
    
            resolve(app)
          }).catch(reject)
        }, reject)
      })
    }
    
    // entry-client.js
    
    const { app, router, store } = createApp()
    
    if (window.__INITIAL_STATE__) {
      store.replaceState(window.__INITIAL_STATE__)
    }
    
    
    # 客户端数据预取
    ## 方式1:在路由导航之前解析数据
    // entry-client.js
    
    // ...忽略无关代码
    router.onReady(() => {
      // 添加路由钩子函数,用于处理 asyncData.
      // 在初始路由 resolve 后执行,
      // 以便我们不会二次预取(double-fetch)已有的数据。
      // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
      router.beforeResolve((to, from, next) => {
        const matched = router.getMatchedComponents(to)
        const prevMatched = router.getMatchedComponents(from)
    
        // 我们只关心非预渲染的组件
        // 所以我们对比它们,找出两个匹配列表的差异组件
        let diffed = false
        const activated = matched.filter((c, i) => {
          return diffed || (diffed = (prevMatched[i] !== c))
        })
    
        if (!activated.length) {
          return next()
        }
    
        // 这里如果有加载指示器(loading indicator),就触发
    
        Promise.all(activated.map(c => {
          if (c.asyncData) {
            return c.asyncData({ store, route: to })
          }
        })).then(() => {
    
          // 停止加载指示器(loading indicator)
    
          next()
        }).catch(next)
      })
    
      app.$mount('#app')
    })
    ## 方式2:匹配要渲染的视图后获取
    //mixins/client.js
    Vue.mixin({
      beforeMount () {
        const { asyncData } = this.$options
        if (asyncData) {
          // 将获取数据操作分配给 promise
          // 以便在组件中,我们可以在数据准备就绪后
          // 通过运行 `this.dataPromise.then(...)` 来执行其他任务
          this.dataPromise = asyncData({
            store: this.$store,
            route: this.$route
          })
        }
      }
    })
    ### 路由组件重用时也应获取数据
    Vue.mixin({
      beforeRouteUpdate (to, from, next) {
        const { asyncData } = this.$options
        if (asyncData) {
          asyncData({
            store: this.$store,
            route: to
          }).then(next).catch(next)
        } else {
          next()
        }
      }
    })
    

    数据仓库代码拆分

    #方式1:分为多个模块
    
    # 方式2:分割到相应的路由组件 chunk 中
    ## steps-01:
    // store/modules/foo.js
    export default {
      namespaced: true,
      // 重要信息:state 必须是一个函数,
      // 因此可以创建多个实例化该模块
      state: () => ({
        count: 0
      }),
      actions: {
        inc: ({ commit }) => commit('inc')
      },
      mutations: {
        inc: state => state.count++
      }
    }
    ## steps-02:
    // 在路由组件内
    <template>
      <div>{{ fooCount }}</div>
    </template>
    
    <script>
    // 在这里导入模块,而不是在 `store/index.js` 中
    import fooStoreModule from '../store/modules/foo'
    
    export default {
      asyncData ({ store }) {
        store.registerModule('foo', fooStoreModule)
        return store.dispatch('foo/inc')
      },
    
      // 重要信息:当多次访问路由时,
      // 避免在客户端重复注册模块。
      destroyed () {
        this.$store.unregisterModule('foo')
      },
    
      computed: {
        fooCount () {
          return this.$store.state.foo.count
        }
      }
    }
    </script>
    
    

    激客户端

    Vue 在 浏览器端客户端 接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程

    //cilent-entry.js
    app.$mount('#app')
    
    //xx.html
    <div id="app" data-server-rendered="true">
    

    构建配置

    # 服务器配置
    ## steps-01:生成服务器代码
    const merge = require('webpack-merge')
    const nodeExternals = require('webpack-node-externals')
    const baseConfig = require('./webpack.base.config.js')
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    
    module.exports = merge(baseConfig, {
      // 模块入口
      entry: '/path/to/entry-server.js',
    
      // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
     // 运行环境 =>以 Node方式处理顶塔导入+在编译 Vue 组件时,告知 `vue-loader` 输送面向服务器代码
      target: 'node',
    
      //  资源映射=>对 bundle renderer 提供 source map 支持
      devtool: 'source-map',
    
      // 模块输出=>告知 server bundle 使用 Node 风格导出模块
      output: {
        libraryTarget: 'commonjs2'
      },
    
      // https://webpack.js.org/configuration/externals/#function
      // https://github.com/liady/webpack-node-externals
      // 外部扩展=>可使构建速度更快+生成较小的 bundle 文件。
      externals: nodeExternals({
        // 排除:
        // 不要外置化 webpack 需要处理的依赖模块。
        // 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
        // 应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
        whitelist: /\.css$/
      }),
    
      // 所需插件:
      // 将构建生成的服务器代码,构建为单个 JSON 文件
      // 默认文件名为 `vue-ssr-server-bundle.json`
      plugins: [
        new VueSSRServerPlugin()
      ]
    })
    ## steps-02:创建服务器渲染器
    const { createBundleRenderer } = require('vue-server-renderer')
    const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
      // ……renderer 的其他选项
    })
    
    # 客户端配置
    ## steps-01:生成客户端代码
    const webpack = require('webpack')
    const merge = require('webpack-merge')
    const baseConfig = require('./webpack.base.config.js')
    const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
    
    module.exports = merge(baseConfig, {
      //模块入口
      entry: '/path/to/entry-client.js',
     //所需插件
      plugins: [
        // 文件清单=>
        // 将 webpack 运行时分离到一个引导 chunk 中,
        // 以便可以在之后正确注入异步 chunk。
        // 也为应用程序/vendor 代码提供了更好的缓存。
        new webpack.optimize.CommonsChunkPlugin({
          name: "manifest",
          minChunks: Infinity
        }),
        // 页面模板=>
        // 将构建生成的客户端代码,保存在输出目录的 `vue-ssr-client-manifest.json`中。
        new VueSSRClientPlugin()
      ]
    })
    ## steps-02:创建客户端渲染器
    ### 方式1:需要模板+自动注入
    const { createBundleRenderer } = require('vue-server-renderer')
    
    const template = require('fs').readFileSync('/path/to/template.html', 'utf-8')
    const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
    const clientManifest = require('/path/to/vue-ssr-client-manifest.json')
    
    const renderer = createBundleRenderer(serverBundle, {
      template,
      clientManifest
    })
    ### 方式2:不需模板+或模板细粒度控(手动注入)
    context.renderState({
      contextKey: 'myCustomState',
      windowKey: '__MY_STATE__'
    })
    
    ### 方式3:不需模板+自己拼接
    
    

    样式管理

    # 注入
    
    # 提取
    ## 情景1:样式写在组件中
    ## 插件: vue-loader+extract-text-webpack-plugin
    
    
    ## 情景2:样式写在组件外
    ### 情景01:<style src="./foo.css">方式导入组件中
    ## 插件: vue-loader+extract-text-webpack-plugin
    
    ### 情景02:import 'foo.css'方式导入javascript中
    #4 插件:vue-style-loader + css-loader + extract-text-webpack-plugin
    
    ## 情景3:从依赖模块导入
    module.exports = {
      // ...
      plugins: [
        // 将依赖模块提取到 vendor chunk 以获得更好的缓存,是很常见的做法。
        new webpack.optimize.CommonsChunkPlugin({
          name: 'vendor',
          minChunks: function (module) {
            // 一个模块被提取到 vendor chunk 时……
            return (
              // 如果它在 node_modules 中
              /node_modules/.test(module.context) &&
              // 如果 request 是一个 CSS 文件,则无需外置化提取
              !/\.css$/.test(module.request)
            )
          }
        }),
        // 提取 webpack 运行时和 manifest
        new webpack.optimize.CommonsChunkPlugin({
          name: 'manifest'
        }),
        // ...
      ]
    }
    

    头部管理

    # https://ssr.vuejs.org/zh/guide/head.html
    // title-mixin.js
    // 获取数据
    function getTitle (vm) {
      // 组件可以提供一个 `title` 选项
      // 此选项可以是一个字符串或函数
      const { title } = vm.$options
      if (title) {
        return typeof title === 'function'
          ? title.call(vm)
          : title
      }
    }
    
    //服务端渲
    const serverTitleMixin = {
      created () {
        const title = getTitle(this)
        if (title) {
          //2.3.2+:访问组件中的服务器端渲染上下文
          this.$ssrContext.title = title
        }
      }
    }
    
    //客户端渲
    const clientTitleMixin = {
      mounted () {
        const title = getTitle(this)
        if (title) {
          document.title = title
        }
      }
    }
    
    //环境判断
    // 可以通过 `webpack.DefinePlugin` 注入 `VUE_ENV`
    export default process.env.VUE_ENV === 'server'
      ? serverTitleMixin
      : clientTitleMixin
    

    缓存管理

    虽然 Vue 的服务器端渲染(SSR)相当快速,但是由于创建组件实例和虚拟 DOM 节点的开销,无法与纯基于字符串拼接(pure string-based)的模板的性能相当(发现问题)。在 SSR 性能至关重要的情况下,明智地利用缓存策略,可以极大改善响应时间并减少服务器负载(解决思路)。

    # 页面级别缓存
    ## 情景:如果内容不是用户特定(user-specific)的。
    ## 方式1:nginx
    
    ## 方式2:nodejs;利用名为 micro-caching的缓存策略。
    //类实例化
    const microCache = LRU({
      max: 100,
      maxAge: 1000 // 重要提示:条目在 1 秒后过期。
    })
    
    //检查请求:是否是用户特定(user-specific)
    const isCacheable = req => {
      // 只有非用户特定(non-user-specific)页面才会缓存
    }
    
    server.get('*', (req, res) => {
      const cacheable = isCacheable(req)
      if (cacheable) {
        //获取缓存
        const hit = microCache.get(req.url)
        if (hit) {
          return res.end(hit)
        }
      }
    
      renderer.renderToString((err, html) => {
        res.end(html)
        //添加缓存
        if (cacheable) {
          microCache.set(req.url, html)
        }
      })
    })
    
    # 组件级别缓存
    ## 情景:局部状态+无副作用+重复列表
    //app.js
    const LRU = require('lru-cache')
    
    // 集渲染器=>将缓存器与渲染器组合起来
    const renderer = createRenderer({
      cache: LRU({
        max: 10000,
        maxAge: ...
      })
    })
    
    //componet.js
    export default {
      //组件名字=>通过使用唯一的名称,使每个缓存键(cache key)对应一个组件
      name: 'item', // 必填选项
      props: ['item'],
     //缓存标识=>告诉内核这个组件是要缓存的以及其缓存键。
      serverCacheKey: props => props.item.id + '::' + props.item.last_updated
      render (h) {
        return h('div', this.item.id)
      }
    }
    

    流式渲染

    # 情景:不由组件生命周期钩子函数填充上下文数据时可用
    const stream = renderer.renderToStream(context)
    
    let html = ''
    
    // 数据添加
    stream.on('data', data => {
      html += data.toString()
    })
    
    // 渲染完成
    stream.on('end', () => {
      console.log(html) 
    })
    
    // 渲染出错
    stream.on('error', err => {
      // handle error...
    })
    
    

    相关文章

      网友评论

          本文标题:vue-服务端渲(ssr)

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