zoukankan      html  css  js  c++  java
  • vue SSR 部署详解

     先用vue cli初始化一个项目

    输入命令行开始创建项目:

    vue create my-vue-ssr

    记得不要选PWA,不知为何加了这个玩意儿就报错。

    后续选router模式记得选 history 模式。

    项目就绪后,cd 进入,开始改造。

    先贴项目结构:

    改造main.js、router.js和store.js

    根据vue ssr官方文档进行路由和代码分割,将 router.js 改造如下:

    // router.js
    import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default function createRouter() { return new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: () => import('./views/Home.vue') }, { path: '/about', name: 'about', component: () => import('./views/About.vue') } ] }) }

    返回一个router构造函数(此处没有什么特别之处)。

    再修改 store.js

    // store.js
    import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default function createStore () { return new Vuex.Store({ state: { }, mutations: { }, actions: { } }) }

    此处还是返回一个构建函数,store 的用法和注意事项参考官方文档数据预取和状态

    接下来是main.js改造,在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。

    sync方法用于连接store和router,这样就可以在组件中直接访问 this.$store.state.route.

    main.js 简单地使用 export 导出一个 createApp 函数:

    // main.js
    import Vue from 'vue'
    import App from './App.vue'
    import createRouter from './router'
    import createStore from './store'
    import { sync } from 'vuex-router-sync'
    
    Vue.config.productionTip = false
    
    export default function createApp () {
      // 创建 router 和 store 实例
      const router = createRouter()
      const store = createStore()
    
      // 同步路由状态(route state)到 store
      sync(store, router)
      
      const app = new Vue({
        router,
        store,
        render: h => h(App)
      })
      return {app, router, store}
    }

    注意:此处new Vue之后没有 $mount('#app') 方法,该步骤放到客户端渲染方法中。

    entry-client.js

    客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:

    // entry-client.js
    import createApp from './main'
    
    const {app, router, store} = createApp()
    
    if (window.__INITIAL_STATE__) {
      store.replaceState(window.__INITIAL_STATE__)
    }
    
    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')
    })

    window.__INITIAL_STATE__ 保存的是服务端返回的 context.state,客户端在挂载之前,将其替换到 store.state 中。

    注意:上面代码是在路由导航之前解析数据。使用此策略,应用程序会等待视图所需数据全部解析之后,再传入数据并处理当前视图。好处在于,可以直接在数据准备就绪时,传入视图渲染完整内容,但是如果数据预取需要很长时间,用户在当前视图会感受到"明显卡顿"。因此,如果使用此策略,建议提供一个数据加载指示器 (data loading indicator)。

    官方还提供了另外一种方法,即把 asyncData 函数放到 beforeMount 或 beforeRouteUpdate 中执行。

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

    当路由组件重用(同一路由,但是 params 或 query 已更改,例如,从 user/1 到 user/2)时,也应该调用 asyncData 函数。

    Vue.mixin({
      beforeRouteUpdate (to, from, next) {
        const { asyncData } = this.$options
        if (asyncData) {
          asyncData({
            store: this.$store,
            route: to
          }).then(next).catch(next)
        } else {
          next()
        }
      }
    })

    entry-server.js

    服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。

    // entry-server.js
    import createApp from './main'
    
    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 })
          }
    
          // 对所有匹配的路由组件调用 `asyncData()`
          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)
      })
    }

    需要注意的是,你仍然需要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子。

    在 entry-server.js 中,我们可以通过路由获得与 router.getMatchedComponents() 相匹配的组件,如果组件暴露出 asyncData,我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文(render context)中。注意,由于此函数会在组件实例化之前调用,所以它无法访问 this

    在路由组件上暴露出一个自定义静态函数 asyncData:

    <!-- 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>

    接下来在根目录创建 server.js 和作为模板的index.html:

    <!DOCTYPE html>
    <html>
        <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>{{title}}</title>
        </head>
        <body>
            <!--vue-ssr-outlet-->
        </body>
    </html>

    注意 <!--vue-ssr-outlet--> 注释 -- 这里将是应用程序 HTML 标记注入的地方。

    模板还支持简单插值。

    <html>
      <head>
        <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
        <title>{{ title }}</title>
    
        <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
        {{{ meta }}}
      </head>
      <body>
        <!--vue-ssr-outlet-->
      </body>
    </html>

    渲染一个 Vue 实例

    // server.js
    const http = require('http')
    const path = require('path')
    const fs = require('fs')
    const express = require('express')
    const {createBundleRenderer} = require('vue-server-renderer')
    const serverBundle = require('./dist/server/vue-ssr-server-bundle.json')
    const clientManifest = require('./dist/client/vue-ssr-client-manifest.json')
    
    const app = express()
    app.set('port', 8001);
    // 静态文件目录指向dist文件夹
    app.use(express.static(path.join(__dirname, './dist/client')))
    
    const renderer = createBundleRenderer(serverBundle, {
      runInNewContext: false,
      template: fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8'),
      clientManifest
    })
    
    app.get('*', (req, res) => {
      const context = {
        title: 'Hello SSR',
        url: req.url
      }
    
      renderer.renderToString(context, (err, html) => {
        if (err) {
          if (err.code === 404) {
            res.status(404).end('404 not found')
          } else {
            res.status(500).end(err.message)
          }
        } else {
          res.end(html)
        }
      })
    })
    
    /*服务启动*/
    http.createServer(app).listen(app.get('port'), function () {
      console.log('service start at ' + app.get('port'));
    });

    此处与express集成,核心方法是通过 renderer.renderToString 返回渲染结果。

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

    webpack在构建过程中会分别生成 vue-ssr-client-manifest.json 和 vue-ssr-server-bundle.json,vue-server-renderer 提供一个名为 createBundleRenderer 的 API,使用 server bundle 和(可选的)选项创建一个 BundleRenderer 实例。具体参数与使用方法参看官方文档。bundle renderer 在调用 renderToString 时,它将自动执行「由 bundle 创建的应用程序实例」所导出的函数(传入上下文作为参数),然后渲染它。

     构建配置

     在根目录新建vue.config.js

    // vue.config.js
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
    const nodeExternals = require('webpack-node-externals')
    const env = process.env
    const isServer = env.RUN_ENV === 'server'
    
    module.exports = {
      publicPath: './',
      outputDir: `dist/${env.RUN_ENV}`,
      configureWebpack: {
        // 将 entry 指向应用程序的 server / client 文件
        entry: `./src/entry-${env.RUN_ENV}.js`,
        devtool: 'eval',
        // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
        // 并且还会在编译 Vue 组件时,
        // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
        target: isServer ? 'node' : 'web',
        // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
        output: {
          libraryTarget: isServer ? 'commonjs2' : undefined
        },
        // https://webpack.js.org/configuration/externals/#function
        // https://github.com/liady/webpack-node-externals
        // 外置化应用程序依赖模块。可以使服务器构建速度更快,
        // 并生成较小的 bundle 文件。
        externals: isServer ? nodeExternals({
          // 不要外置化 webpack 需要处理的依赖模块。
          // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
          // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
          whitelist: /.css$/
        }) : undefined,
        // 这是将服务器的整个输出
        // 构建为单个 JSON 文件的插件。
        // 服务端默认文件名为 `vue-ssr-server-bundle.json`
        // 客户端默认文件名为 `vue-ssr-client-manifest.json`
        plugins: [
          isServer ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
        ]
      }
    }

    此处合并了服务端配置和客户端配置,通过命令参数 RUN_ENV来区分服务端配置和客户端配置。服务器配置,是用于生成传递给 createBundleRenderer 的 server bundle。 官方文档

    最后

    在package.json中添加以下命令:

        "start": "npm run build:server && npm run build:client && npm run service",
        "build:client": "cross-env RUN_ENV=client vue-cli-service build",
        "build:server": "cross-env RUN_ENV=server vue-cli-service build --mode server",
        "service": "node server.js",
    build:server 命令用于构建服务端bundle,
    build:client用于构建客户端 bundle,
    service 启动服务。

  • 相关阅读:
    二分图 洛谷P2055 [ZJOI2009]假期的宿舍
    并查集 洛谷P1640 [SCOI2010]连续攻击游戏
    贪心 洛谷P2870 Best Cow Line, Gold
    贪心 NOIP2013 积木大赛
    快速幂 NOIP2013 转圈游戏
    倍增LCA NOIP2013 货车运输
    树形DP 洛谷P2014 选课
    KMP UVA1328 Period
    动态规划入门 BZOJ 1270 雷涛的小猫
    KMP POJ 2752Seek the Name, Seek the Fame
  • 原文地址:https://www.cnblogs.com/flicat/p/10937933.html
Copyright © 2011-2022 走看看