vue服务端渲染ssr的初步实现

附上Demo的仓库地址:https://gitee.com/jfengz003/vue-ssr-demo.git
希望能与大家一起进步~

一、SSR的概念

传统web开发

在这里插入图片描述

单页应用SPA

请添加图片描述

服务端渲染SSR

请添加图片描述

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

官方文档:Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。

我们从上门解释得到以下结论:
1、Vue SSR是一个在SPA上进行改良的服务端渲染;
2、通过Vue SSR渲染的页面,需要在客户端激活才能实现交互;
3、Vue SSR将包含两部分:服务端渲染的首屏,包含交互的SPA;

为什么使用服务器端渲染 (SSR)?

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。

使用服务器端渲染 (SSR) 时还需要有一些权衡之处:

  • 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
  • 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
  • 更多的服务器端负载。每个请求都是n个实例的创建,不然会污染,消耗会变得很大

如果你调研服务器端渲染 (SSR) 只是用来改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。

二、如何实现SSR

对于同构开发,我们依然使用webpack打包,我们要解决两个问题:服务端首屏渲染和客户端激活;

这里需要生成一个服务器bundle文件用于服务端首屏渲染和一个客户端bundle文件用于客户端激活
请添加图片描述

新增:app.js、entry-client.js、entry-server.js、server.js

src 
├── router 
├────── index.js # 路由声明 
├── store 
├────── index.js # 全局状态 
├── server
├────── server.js # node服务
├── app.js # 通用入口,⽤于创建vue实例 
├── entry-client.js # 客户端⼊⼝,⽤于静态内容“激活” 
└── entry-server.js # 服务端⼊⼝,⽤于⾸屏内容渲染

修改路由配置

// src/router/index.js

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

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Login',
    component: () => import('@/views/login.vue')
  },
  {
    path: '/layout',
    name: 'Layout',
    component: () => import('@/views/layout.vue'),
    redirect: '/layout/pageOne',
    children: [
      {
        path: 'pageOne',
        name: 'pageOne',
        component: () => import('@/views/page1.vue')
      },
      {
        path: 'pageTwo',
        name: 'pageTwo',
        component: () => import('@/views/page2.vue')
      }
    ]
  }
]

// 使用工厂函数创建路由
function createRouter() {
  return new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
  });
}

export default createRouter

通用入口

// src/app.js

import Vue from 'vue';
import App from './App.vue';
import createRouter from './router';

// 使用工厂函数,导出Vue实例和Router实例
export default function createApp() {
  const router = createRouter();
  const app = new Vue({
    router,
    render: h => h(App)
  });
  return { app, router };
}

服务端入口

// src/entry-server.js

import createApp from "./app";

// 返回⼀个函数,接收请求上下⽂,返回创建的vue实例
export default context => {
  // 这⾥返回⼀个Promise,确保路由或组件准备就绪
  return new Promise((resolve, reject) => {
    const { app, router } = createApp();
    // 进入首屏
    router.push(context.url);
    // 路由就绪
    router.onReady(() => {
      resolve(app);
    }, reject);
  });
}

客户端入口

// entry-client.js

import createApp from "./app";

// 创建vue、router实例
const { app, router } = createApp();
// 路由就绪 执行挂载
router.onReady(() => {
  app.$mount('#app');
});

webpack配置(vue.config.js)

安装依赖:

npm install webpack-node-externals lodash.merge -D
// vue.config.js

// webpack插件
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 path = require('path');

// 环境变量:决定入口是客户端还是服务端
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
  css: {
    extract: false
  },
  outputDir: path.resolve(__dirname, `./dist/${target}`) ,
  configureWebpack: () => ({
    // 将 entry 指向应用程序的 server / client 文件
    entry: path.resolve(__dirname, `./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: {
      // 此处告知 server bundle 使用 Node 风格导出模块
      libraryTarget: TARGET_NODE ? "commonjs2" : undefined
    },
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的 bundle 文件。
    externals: TARGET_NODE
      ? nodeExternals({
          // 不要外置化 webpack 需要处理的依赖模块。
          // 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
          // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
          allowlist: [/\.css$/]
        })
      : undefined,
    optimization: {
      splitChunks: TARGET_NODE ? false : undefined
    },
    // 这是将服务器的整个输出构建为单个 JSON 文件的插件。
    // 服务端默认文件名为 `vue-ssr-server-bundle.json`
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config => {
    config.module
      .rule("vue")
      .use("vue-loader")
      .tap(options => {
        merge(options, {
          optimizeSSR: false
        });
      });
  }
};

对脚本进行配置,安装依赖

npm install cross-env -D
"scripts": {
    "serve": "vue-cli-service serve",
    "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",
    "lint": "vue-cli-service lint"
  },

编写服务端启动脚本

安装依赖:

npm install vue-server-renderer express -S
// server/server.js

const express = require('express');
const fs = require('fs');
const path = require('path');

// 创建express实例和Vue实例
const app = express();

// 创建渲染器
const { createBundleRenderer } = require('vue-server-renderer');
// 服务端bundle文件
const serverBundle = require(path.resolve(__dirname, '..', 'dist/server/vue-ssr-server-bundle.json'));
// 客户端清单
const clientManifest = require(path.resolve(__dirname, '..', 'dist/client/vue-ssr-client-manifest.json'));

const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template: fs.readFileSync(path.resolve(__dirname, '..', 'public/index.temp.html'), 'utf-8'), // 宿主模板文件
  clientManifest
});


// 中间件处理静态文件请求
app.use(express.static(path.resolve(__dirname, '..', 'dist/client'), { index: false }));

app.get('*', async (req, res) => {
  try {
    // 上下文
    const content = {
      url: req.url,
      title: 'ssr test'
    }
    const html = await renderer.renderToString(content);
    console.log('object', html);
    res.send(html);
  } catch(err) {
    res.status(500).send('服务器内部错误');
  }
});

app.listen(3002, () => {
  console.log('渲染服务器启动成功');
});

新增宿主文件:/public/index.temp.html

<!DOCTYPE html>
<html lang="">
  <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>ssr</title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

注意:<!--vue-ssr-outlet-->是服务端渲染出口位置,不能有空格

执行打包:

npm run build

cd进入server文件夹,在终端执行:

node server.js

打开http://localhost:3002,查看页面源代码如下图的话,恭喜你,初步实现vue服务端渲染ssr。

请添加图片描述

参考视频:手把手教你打造Vue SSR
参考文章:如何在vue中实现SSR服务端渲染?

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值