实现 Vue 服务端渲染(Vue SSR)

什么是服务端渲染(SSR)?

Vue.js 是构建客户端应用程序的框架,但是也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记“激活”为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以叫做“同构”或“通用”,程序上的大部分代码都可以在服务器和客户端上运行。

是否需要服务器渲染?

与传统 SPA 相比,SSR 的主要优势在于:

  • 更好的 SEO
  • 更快的内容到达时间(time-to-content)

需要注意的是:

  • 开发条件有限,由于没有动态更新,服务端渲染过程中,只有 beforeCreatecreated钩子函数被调用,在这两个生命周期函数中应该避免产生全局副作用的代码,例如在其中使用setInterval设置 timer。
  • 涉及构建设置和部署的更多要求。服务器渲染应用程序,需要处于 Node.js server 运行环境。
  • 更多的服务器端负载。如果预料到在高流量环境下使用,需要准备相应的服务器负载,并明智地采用缓存策略。

如果确实需要服务端渲染,那么可以继续看下面的用法。

基本用法


安装
npm install vue vue-server-renderer --save
复制代码

注意:

  • Node.js 版本 6+
  • vue-servier-renderervue必须匹配版本

构建步骤


1. 建立入口文件

对于客户端应用程序和服务器应用程序,都需要使用 webpack 打包两个 Bundle,服务器需要 Server Bundle 用于服务器渲染,Client Bundle 会发送给浏览器,用于混合静态标记。

一个基本项目像这样:

src
├── components
│   ├── Foo.vue
│   └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── index.template.html
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
复制代码

因单线程机制,在服务端渲染中有类似于单例的操作,所有的请求都会共享这个单例的操作,所以应该使用工厂函数来确保每个请求之间的独立性。app.js主要是 export 一个createApp函数。类似地 storerouter都需要导出这样的工厂函数:

# app.js
import Vue from 'vue';
import App from './App.vue';
import { createStore } from './store';
import { createRouter } from './router';

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

复制代码

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

# entry-client.js
import { createApp } from './app';
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) => {
    ...
  });

  app.$mount('#app');
});

复制代码

服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。在这里可以执行服务端路由匹配 (server-side route matching) 和数据预取逻辑(data-pre-fetching logic)。

# entry-server.js
import { createApp } from './app';

export default context => {
  # 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
  # 以便服务器能够等待所有的内容在渲染前就已经准备就绪。
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp();
    router.push(context.url);
    
    router.onReady(() => {
      # 服务器端数据预取
      ...
    }, reject);
  });
};

复制代码
2. webpack 构建配置

配置文件结构像这样:

build
├── dev-server.js
├── setup-dev-server.js
├── webpack.base.conf.js 
├── webpack.client.conf.js 
├── webpack.dev.conf.js 
├── webpack.prod.conf.js 
└── webpack.server.conf.js
复制代码

package.json打包命令:

"scripts": {
    "dev": "NODE_ENV=dev node server/index.js",
    "build:client": "webpack --config build/webpack.client.conf.js --progress --hide-modules --progress",
    "build:server": "webpack --config build/webpack.server.conf.js --progress --hide-modules --progress",
    "build:prod": "NODE_ENV=prod npm run build:client && NODE_ENV=prod npm run build:server",
  },
复制代码
3.开发服务器集成

开发服务使用的是 Koa,配置参考:

import Koa from 'koa';
import koaRouter from 'koa-router';
import { createBundleRenderer } from 'vue-server-renderer';

const app = new Koa();
const router = koaRouter();
const createRenderer = (bundle, options) => {
  return createBundleRenderer(
    bundle,
    {...options, { runInNewContext: false }
  );
};

const renderData = (ctx, renderer) => {
  const context = {
    url: ctx.url
  };
  return new Promise((resolve, reject) => {
    renderer.renderToString(context, (err, html) => {
      if (err) {
        reject(err);
      }
      resolve(html);
    });
  });
};

let renderer;
require('../build/setup-dev-server.js')(app, (bundle, options) => {
  renderer = createRenderer(bundle, options);
});

# proxy api request
const proxy = require('koa-server-http-proxy');
# 代理配置...

router.get('*', async (ctx, next) => {
    if (!renderer) {
      ctx.type = 'html';
      return (ctx.body = 'waiting for compilation...');
    }
    
    let html;
    try {
      html = await renderData(ctx, renderer);
    } catch (e) {
      # 处理特殊情况
      ...
    }
    ctx.body = html;
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(80, '0.0.0.0', () => {
  console.log(`server is running...`);
});

复制代码
4.线上服务器集成

线上服务使用的是 Egg.js,参考配置如下:

# app/controller/home.js
const Controller = require('egg').Controller;
const path = require('path');
const { createBundleRenderer } = require('vue-server-renderer');

const serverBundle = require('../public/vue-ssr-server-bundle.json');
const clientManifest = require('../public/vue-ssr-client-manifest.json');
const template = require('fs').readFileSync(
  path.resolve(__dirname, '../public/index.html'),
  'utf-8'
);
const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template,
  clientManifest
});

class HomeController extends Controller {
  async index() {
    const ctx = this.ctx;
    const context = { url: ctx.url };

    try {
      # 传入context渲染上下文对象
      renderer.renderToString(context, (err, html) => {
        if (err) {
          throw err;
        }
        ctx.status = 200;
        # 传入了template, html结构会插入到<!--vue-ssr-outlet-->
        ctx.body = html;
      });
    } catch (error) {
      ctx.status = 500;
      ctx.body = 'Internal Server Error';
    }
  }
}

module.exports = HomeController;

复制代码

路由匹配:

router.get(/^(?!\/api\/)/, controller.home.index);
复制代码

如此依照开发和生产环境配置,能够实现基本的服务端渲染。篇幅有限,大段代码暂时没有贴出,后续会开放源代码示例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值