vue-cli3.0搭建服务端渲染SSR

关于SSR

什么是SSR

  • 可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序
  • 简单点说:就是将页面在服务端渲染完成后在客户端直接显示。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。

为什么要用SSR

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。

SSR原理

在这里插入图片描述

  • 1)所有的文件都有一个公共的入口文件app.js
  • 2)进入ServerEntry(服务端入口)与clientEntry(客户端入口)
  • 3)经过webpack打包生成ServerBundle(供服务端SSR使用,一个json文件)与ClientBundle(给浏览器用,和纯Vue前端项目Bundle类似)
  • 4)当请求页面的时候,nodeServerBundle会生成html界面,通过ClientBundle混合到html页面中

通用代码约束:

  • 实际的渲染过程需要确定性,所以我们也将在服务器上“预取”数据 ("pre-fetching" data) - 这意味着在我们开始渲染时,我们的应用程序就已经解析完成其状态。
  • 避免在 beforeCreatecreated 生命周期时产生全局副作用的代码,例如在其中使用 setInterval 设置 timer
  • 通用代码不可接受特定平台的 API,因此如果你的代码中,直接使用了像 windowdocument,这种仅浏览器可用的全局变量,则会在 Node.js 中执行时抛出错误,反之也是如此

构建SSR应用程序

创建vue项目

vue create vue-ssr-demo
  • 根据提示启动项目
    在这里插入图片描述

修改router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

export function createRouter(){
  return new VueRouter({
    mode: 'history',
    routes: [
      {
        path: '/',
        name: 'Home',
        component: () => import(/* webpackChunkName: "about" */ '../views/Home.vue')
      },
      {
        path: '/about',
        name: 'About',
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
      }
    ]
  })
}

修改store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export function createStore(){
  return new Vuex.Store({
    state: {
    },
    mutations: {
    },
    actions: {
    },
    modules: {
    }
  })
}

修改main.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'

Vue.config.productionTip = false

export function createApp(){
  const store = createStore();
  const router = createRouter();
  
  const app = new Vue({
    store, 
    router,
    render: h => h(App)
  })
  
  return {app, store, router}
}

服务端数据预取

  • src目录下创建entry-server.js
// 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)
  })
}

客户端数据预取

  • src创建entry-client.js
import { createApp } from './main';

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

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')
})

构建配置

  • 在根目录下创建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 merge = require('lodash.merge')
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
const target = TARGET_NODE ? 'server' : 'client'
module.exports = {
  css: {
    extract: false
  },
  configureWebpack: () => ({
    // 将 entry 指向应用程序的 server / client 文件
    entry: `./src/entry-${target}.js`,
    // 对 bundle renderer 提供 source map 支持
    devtool: 'source-map',
    target: TARGET_NODE ? 'node' : 'web',
    node: TARGET_NODE ? undefined : false,
    output: {
      libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,
    // 并生成较小的 bundle 文件。
    externals: TARGET_NODE
      ? nodeExternals({
        // 不要外置化 webpack 需要处理的依赖模块。
        // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
        // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
        allowlist: [/\.css$/]
      })
      : undefined,
    optimization: {
      splitChunks: TARGET_NODE ? false : undefined
    },
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        return merge(options, {
          optimizeSSR: false
        })
      })
  }
}

webpack进行打包操作

  • 修改package.json
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build",
"build:win": "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json"
  • 执行build:win, 生成的dist目录如下:
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200731171212338.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI2NDQzNTM1,size_16,color_FFFFFF,t_70

创建服务

  • 在根目录下创建server.js
const express = require('express');
const fs = require('fs');
const path = require('path');
const { createBundleRenderer } = require('vue-server-renderer');

const app = express();

const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
const template = fs.readFileSync(path.resolve('./src/index.template.html'), 'utf-8');

const render = createBundleRenderer(serverBundle, {
  runInNewContext: false, // 推荐
  template, // (可选)页面模板
  clientManifest
});

app.use(express.static('./dist',{index:false}))

app.get('*', (req, res) => {
  const context = { url: req.url }
  // 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。
  // 现在我们的服务器与应用程序已经解耦!
  render.renderToString(context, (err, html) => {
    console.log(html)
    // 处理异常……
    res.end(html)
  })
})

const port = 3003;
app.listen(port, function() {
 console.log(`server started at localhost:${port}`);
});
  • index.template.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></title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>
  • 运行node server.js
    在这里插入图片描述
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值