AI 摘要

文章系统梳理 Vue SSR 实现路径:先对比 CSR 与 SSR 优劣,再用 vue-server-renderer 手写 Node/Express/Koa 最小 demo;继而用 webpack 分 entry-server、entry-client 双 Bundle,配合 vue-router history 模式完成同构;最后介绍 Nuxt.js 脚手架一键创建 SSR 项目,自动路由与页面机制,实现零配置服务端渲染。

初识服务器端渲染

客户端渲染概述

客户端渲染,即传统的单页面应用(SPA)模式,Vue.js 构建的应用程序默认情况下是一个 HTML 模板页面,只有一个 id 为 app 的 div 根容器,然后通过 webpack 打包生成 css、js 等资源文件,浏览器加载、解析来渲染 HTML。

在客户端渲染时,一般使用的是 webpack-dev-server 插件,它可以帮助用户自动开启一个服务器端,主要作用是监控代码并打包,也可以配合 webpack-hot-middleware 来进行热更替(HMR),这样能提高开发效率。

服务器端渲染概述

服务器端渲染,就是将页面或者组件通过服务器生成 HTML 字符串,将它们直接发送到浏览器,最后将静态标记“混合”为客户端上完全交互的应用程序。

Vue 进行服务器端渲染时,需要利用 Node.js 搭建一个服务器,并添加服务器端渲染的代码逻辑。

服务器端渲染的优点:

  • 利于 SEO。
  • 首屏渲染速度快。
  • ......

服务器端渲染的缺点:

  • 服务器端压力增加。
  • 涉及构建设置和部署的要求比较严格。
  • ......

服务器端渲染的注意事项:

  • Vue 2.3.0+ 版本的服务器端渲染(SSR),要求 vue-server-renderer(服务端渲染插件)的版本要与 Vue 版本相匹配。
  • 在使用 Vue-Router 路由插件时,路由模式只能是 history 模式。

服务器端渲染的简单实现

创建 vue-ssr 项目

在项目存储目录下,使用命令行工具创建一个 vue-ssr 项目。


示例:创建 vue-ssr 项目

终端执行:

# 创建文件夹
mkdir vue-ssr

# 切换目录
cd vue-ssr

# 初始化项目
npm init -y

# 安装Vue和服务器端渲染的插件vue-server-renderer
npm install vue@2.7.16 vue-server-renderer@2.7.16

示例效果:


渲染 Vue 实例

vue-server-renderer 安装完成后,创建服务器脚本文件 test.js,实现将 Vue 实例的渲染结果输出到控制台中。


示例:渲染 Vue 实例

服务器脚本文件(test.js):

// 1、require导入Vue
const Vue = require('vue')

// 2、创建一个Vue实例
const vm = new Vue({
    template: '<div>SSR的简单使用</div>'
})

// 3、导入vue-server-renderer,并创建一个renderer实例
const renderer = require('vue-server-renderer').createRenderer()

// 4、将Vue实例渲染为HTML
// 参数1:要渲染的Vue实例对象
// 参数2:回调箭头函数
// err:渲染过程中的错误信息
// html:渲染后的HTML代码
renderer.renderToString(vm, (err, html) => {
    if (err) {
        throw err
    }
  
    console.log(html)
})

终端执行:

node test.js

示例效果:


Express 搭建 SSR

Express 是一个基于 Node.js 平台的 Web 应用开发框架,用来快速开发 Web 应用。


示例:Express 搭建 SSR

终端执行:

npm install express

页面模板(template.html):

<!DOCTYPE html>
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--
    下面的注释不能删除也不能修改
    这个注释是HTML占位符
-->
<!--vue-ssr-outlet-->
</body>
</html>

Express 配置文件(express.js):

// 1、require导入Vue和Express
const Vue = require('vue')
const express = require('express')

// 2、创建Express服务器
const server=express()

// 3、借助fs文件读取工具读取页面模板
const renderer = require('vue-server-renderer').createRenderer({
    template: require('fs').readFileSync('./template.html', 'utf-8')
})

// 4、处理 GET 请求
// *:所有请求都会进入该方法
// req:请求对象
// res:响应对象
server.get('*', (req, res) => {
    res.set({'Content-Type': 'text/html; charset=utf-8'})

    // 5、创建一个Vue实例
    const vm = new Vue({
        data: {
            title: '当前位置',
            url: req.url
        },
        template: '<div>{{title}}:{{url}}</div>',
    })

    // 6、将Vue实例渲染为HTML后输出
    renderer.renderToString(vm, (err, html) => {
        // end():结束响应并返回结果

        if (err) {
            res.status(500).end('err: ' + err)
            return
        }

        res.end(html)
    })
})

// 7、为Express监听8080端口
server.listen(8080, function () {
    console.log('server started at localhost:8080')
})

终端执行:

node express.js

示例效果:


Koa 搭建 SSR

Koa 是一个基于 Node.js 平台的 Web 开发框架,致力于成为 Web 应用和 API 开发领域更富有表现力的技术框架。

Koa 能帮助开发者快速地编写服务器端应用程序,通过 async 函数很好地处理异步的逻辑,有力地增强错误处理。


示例:Koa 搭建 SSR

终端执行:

npm install koa

Koa 配置文件(koa.js):

// 1、require导入Vue和Koa
const Vue = require('vue')
const Koa = require('koa')

// 2、创建Koa实例
const server = new Koa()

// 3、借助fs文件读取工具读取页面模板
const renderer = require('vue-server-renderer').createRenderer({
    template: require('fs').readFileSync('./template.html', 'utf-8')
})

// 4、添加一个中间件来处理所有请求
// async:异步函数
// ctx:上下文对象
// next:将要处理的下一个函数
server.use(async (ctx, next) => {

    // 5、创建一个 Vue 实例
    const vm = new Vue({
        data: {
            title: '当前位置',
            url: ctx.url
        },
        template: '<div>{{title}}:{{url}}</div>'
    })

    // 6、将Vue实例渲染为HTML后输出
    renderer.renderToString(vm, (err, html) => {
        if (err) {
            ctx.res.status(500).end('err: ' + err)
            return
        }
        ctx.body = html
    })
})

// 7、为Koa监听8081端口
server.listen(8081, function () {
    console.log('server started at localhost:8081')
})

终端执行:

node koa.js

示例效果:


webpack 搭建服务器端渲染

基本流程

webpack 服务器端渲染需要使用 entry-server.js 和 entry-client.js 两个入口文件,两者通过打包生成两份 bundle 文件。其中,通过 entry-server.js 打包的代码是运行在服务器端,而通过 entry-client.js 打包的代码运行在客户端。

webpack 服务器端渲染的流程:

项目搭建

使用 webpack 可以实现服务器端渲染。


使用 webpack 可以实现服务器端渲染

进入 Vue-CLI GUI,创建 Vue-CLI 项目,并添加 vue-router、koa、koa-static、vue-server-renderer、cross-env、lodash.merge、webpack-node-externals 依赖:

路由配置文件(src/router/index.js):

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

Vue.use(VueRouter)

// 将路由改造成工厂
// 确保每个请求时路由对象都是单例
export function createRouter() {
    const routes = [
        {
            path: '/',
            name: 'home',
            component: () => import('../views/HomeView.vue')
        },
        {
            path: '/about',
            name: 'about',
            component: () => import('../views/AboutView.vue')
        }
    ]

    const router = new VueRouter({
        // 只能使用history模式
        mode: 'history',
        routes
    })

    return router
}

项目入口配置文件(src/main.js):

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

Vue.config.productionTip = false

export function createApp() {
    const router = createRouter()

      const app = new Vue({
        router,
        render: h => h(App)
      })

      return {app, router}
}

服务端入口文件(src/entry-server.js):

import { createApp } from './main'

export default context => {
    return new Promise((resolve, reject) => {
        // 创建Vue实例和VueRouter路由对象
        const { app, router } = createApp()

        // 路由跳转
        router.push(context.url)

        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents()
            if (!matchedComponents.length) {
                return reject(new Error('no components matched'))
            }
            resolve(app)
        }, reject) })
}

客户端入口文件(src/entry-client.js):

import {createApp} from "./main.js";
let {app,router} = createApp();

router.onReady(() => {
    app.$mount("#app");
})

Vue 项目配置文件(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");

// 根据传入环境变量决定入口文件和相应配置项
// 分别生成server/client的文件
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
    configureWebpack: () => ({
        entry: `./src/entry-${target}.js`,  // 将entry指向应用程序的server/client文件
        devtool: 'source-map',
        target: TARGET_NODE ? "node" : "web",
        node: TARGET_NODE ? undefined : false,
        output: {
            libraryTarget: TARGET_NODE ? "commonjs2" : undefined
        },
        externals: TARGET_NODE
            ? nodeExternals({
                allowlist: [/.css$/]
            })
            : undefined,
        optimization: {
            splitChunks: undefined
        },
        // 这是将服务器的整个输出构建为单个JSON文件的插件
        // 服务端默认文件名为vue-ssr-server-bundle.json
        // 客户端默认文件名为vue-ssr-client-manifest.json
        plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
    }),
    chainWebpack: config => {
        config.module
            .rule("vue")
            .use("vue-loader")
            .tap(options => {
                merge(options, {
                    optimizeSSR: false
                });
            });

        if (TARGET_NODE) {
            config.plugins.delete("hmr");
        }
    }
};

npm 配置文件(package.json):

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "build:client": "vue-cli-service build",
    "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
    "build:ssr": "npm run build:server & npm run build:client"
},

运行 build:ssr:

页面模板(template.html):

<!DOCTYPE html>
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>

Koa 配置文件(koa.js):

const Koa = require("koa")
const path = require("path")
const koaStatic = require('koa-static')
const server = new Koa()

const resolve = file => path.resolve(__dirname, file)

// 开放dist文件夹
server.use(koaStatic(resolve('./dist')))

const { createBundleRenderer } = require("vue-server-renderer")
const bundle = require("./dist/vue-ssr-server-bundle.json")
const clientManifest = require("./dist/vue-ssr-client-manifest.json")

// 渲染页面
const renderer = createBundleRenderer(bundle, {
    runInNewContext: false,
    template: require('fs').readFileSync(resolve("./template.html"), "utf-8"),
    clientManifest: clientManifest
});

function renderToString(context) {
    return new Promise((resolve, reject) => {
        renderer.renderToString(context, (err, html) => {
            err ? reject(err) : resolve(html)
        })
    })
}

// 添加一个中间件来处理所有请求
server.use(async (ctx, next) => {
    const context = {
        title: "ssr test",
        url: ctx.url
    }

    ctx.body = await renderToString(context)
})

server.listen(8081, function () {
    console.log('server started at localhost:8081')
})

终端执行:

node koa.js

示例效果:


Nuxt.js 服务器端渲染框架

创建 Nuxt.js 项目

Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染应用,也可充当静态站点引擎生成静态站点应用,具有优雅的代码结构分层和热加载等特性。

Nuxt.js 提供了利用 vue.js 开发服务端渲染的应用所需要的各种配置,为了快速入门,Nuxt.js 团队创建了脚手架工具 create-nuxt-app,可以使用脚手架工具快速创建 Nuxt.js 项目。


示例:创建 Nuxt.js 项目

终端执行:

# 全局安装create-nuxt-app脚手架
npm install create-nuxt-app -g

# 创建Nuxt.js项目
npx create-nuxt-app 项目名称

示例效果:

终端执行:

# 切换目录
cd nuxtapp

# 终端执行
npm run dev

示例效果:


页面和路由

在项目中,pages 目录用来存放应用的路由及视图。

  • 当直接访问根路径“/”的时候,默认打开的是 index.vue 文件。
  • Nuxt.js 会根据目录结构自动生成对应的路由配置,将请求路径和 pages 目录下的文件名映射。比如访问“/test”就表示访问 test.vue 文件,如果文件不存在,就会提示 This page could not be found 错误。
  • pages 目录下的 vue 文件也可以放在子目录中,在访问的时候也要加上子目录的路径。

页面跳转

Nuxt.js 中使用 nuxt-link 组件来完成页面中路由的跳转,类似于 Vue 中的路由组件 router-link,它们具有相同的属性,并且使用方式也相同。

Nuxt.js 中,还可以使用编程式路由来完成页面中路由的跳转,与 VueRouter 的使用方式一致。