模块四(一):搭建自己的SSR

前言:同构渲染是将服务器渲染和客户端渲染相结合的一种渲染方式,在服务端生成初始页面,提升首屏加载速度,并且有利于SEO;在客户端接管HTML,并且将静态HTML激活为数据绑定的动态HTML,为用户提供更流畅的交互服务,并且可以更流畅的实现路由跳转,无需刷新整个页面。
同构渲染可以使用NuxtJS实现,它非常的方便,开箱即用,给我们配置好了各种各样的配置,感兴趣的可以参考NuxtJS开发实例。然而,如果想要对于项目有更加灵活的控制,就可以使用Vue提供的 Vue SSR。使用 Vue SSR 的前提是具有 Node.js 和 webpack 的应用经验。
使用 Vue SSR 相比于使用 SPA 有利有弊,好处是
① 更好的 SEO
② 更快的首屏加载速度
坏处是
① 开发条件所限。浏览器端特定的代码只能在某些特定的生命周期钩子函数中使用,并且第三方库的使用可能会受限制。
② 涉及构建设置和部署的要求更多。需要更多的配置部署以及依赖于 Node Server 环境。
③ 更多的服务器端负载。在服务器端构建 html 会占用更多的CPU资源。
在选择 Vue SSR 开发项目之前,要考虑是否真正需要它,因为使用起来开发成本会增加很多。是否需要使用 Vue SSR 主要取决于初始加载的几百毫秒优化是否真正有必要。如果只是为了优化某一些页面,可以使用预渲染技术。使用 Vue SSR 意味着编写代码时要考虑代码的运行环境,有在服务器端运行的代码、服务器端和客户端都运行的通用代码以及只在客户端运行的代码。

(一)简单入门案例

服务器端渲染是在服务器端生成完整的 html 页面,返回给客户端,客户端直接渲染服务器端返回的 html 。首先介绍如何使用Vue 提供的服务端渲染工具生成一个Vue实例。

1、渲染一个Vue实例

① 创建一个项目根目录,进入项目根目录
npm init -y初始化项目
npm install [email protected] vue-server-renderer --save安装服务端渲染的依赖包和 Vue 库;注意指定Vue的版本,否则会下载最新版本,使用起来和Vue2语法不同
④ 在根目录下创建一个server.js,把官网的示例代码复制粘贴进去,稍作修改

// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
    template: `
      

{{ message }}

`
, data: { message: 'Hello World!' } }) // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:将 Vue 实例渲染为 HTML // 参数一:Vue实例 // 参数二:回调函数 renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) })

⑤ 控制台运行node server.js运行代码,查看输出
可以看到在Node环境中,vue-server-renderer 将Vue实例渲染成字符串,并且给根元素增加了data-server-rendered属性
在这里插入图片描述

2、结合到web服务中

接下来介绍如何将 Vue SSR 渲染之后的静态 HTML发送给Web服务器中,然后渲染到页面上。这里会使用到 express 框架。Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。可以灵活处理 http 请求、路由、中间件、静态资源托管等业务场景。
① 安装 NodeJS 框架 npm install express --save
② 在 server.js 当中加载 express

// 加载express
const express = require('express')

③ 给服务配置路由,并且在访问服务的时候,创建 Vue 实例,并使用 vue-server-renderer 渲染成静态HTML。如果发生错误,就返回状态码500,并且返回错误信息;如果正确执行,就把 vue-server-renderer 生成的静态HTML返回给客户端

server.get('/',(req,res)=>{
    const app = new Vue({
        template: `
Hello World
`
}) // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { if (err) { return res.status(500).end('服务器错误') } res.end(html) }) })

④ 启动服务并监听端口号

// 启动服务
server.listen(3000, ()=>{
    console.log('服务启动')
})

⑤ 使用nodemon server.js启动服务
nodemon 是一个 Node.js 应用的实时监视工具。它可以监视你的代码,如果发现有任何更改,它会自动重启你的应用程序,不必手动重新启动它。如果没安装的话,记得npm i nodemon安装一下。
启动服务之后,控制台会打印启动成功的消息:
模块四(一):搭建自己的SSR_第1张图片
访问 http://localhost:3000/,可以看到页面中展示 Vue SSR 生成的页面内容
模块四(一):搭建自己的SSR_第2张图片
还可以从网络的响应中看到,服务端响应的就是静态html
在这里插入图片描述
⑥ 把模板中的文本改成中文之后

const app = new Vue({
    template: `
哈哈哈
`
})

页面中会出现乱码
模块四(一):搭建自己的SSR_第3张图片
这是因为没有设置编码方式,解决方式有两种
1️⃣ 设置响应头的 content-type,告诉浏览器返回的内容是 html 格式的内容,是用utf8字符集编码的

 renderer.renderToString(app, (err, html) => {
     if (err) {
         return res.statue(500).end('服务器错误')
     }
     res.setHeader('Content-Type','text/html; charset=utf8')
     res.end(html)
 })

可以在响应头中看到content-type的设置
模块四(一):搭建自己的SSR_第4张图片
2️⃣ 给模板中加入meta标签
上面只是生成了一个div标签,即html片段,并不是一个完整的页面结构。返回的时候可以返回一个完整的页面结构,其中使用meta标签执行charset编码方式

res.end(`
             
                 
                 
                 
                 
	              
		              ${html}
	              
	          `)
          })
      })

此时返回的数据就具有完整的文档结构
模块四(一):搭建自己的SSR_第5张图片
通常为了确保正确,两种方式都进行配置是最好的。

3、使用页面模板

可以将页面基础结构的模板放在单独的文件中进行维护,vue-server-renderer 会自动将模板和 Vue 实例的template结合成一个完整的html
① 在根目录下创建index.template.html存放页面模板代码,其中,需要用 Vue 实例的template属性填充的地方,使用注释占位,这句是固定的,不能修改格式。

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
    
body>
html>

② 创建 renderer 时传入页面模板的配置选项

const fs = require('fs')
// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./index.template.html', 'utf-8')
})

③ 返回给客户端时,直接返回参数html即可

// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
    if (err) {
        return res.status(500).end('服务器错误')
    }
    res.setHeader('Content-Type', 'text/html; charset=utf8')
    res.end(html)
})
4、在模板中使用外部数据

renderer.renderToString()方法的第二个参数可以定义传递给模板的数据,在模板中使用插值表达式绑定即可。如果想给模板传递html标签,则模板内部要使用{{{}}},这样就不会进行解析,会直接原文输出
server.js

renderer.renderToString(app,{
    title:'Vue SSR',
    meta:``
}, (err, html) => {
    if (err) {
        return res.status(500).end('服务器错误')
    }
    res.setHeader('Content-Type', 'text/html; charset=utf8')
    res.end(html)
})

index.template.html

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    {{{ meta }}}
    <title>{{title}}title>
head>
<body>
    
body>
html>

(二)构建配置

Vue 项目需要处理 ES6 代码,以及处理 css 等资源以保证在旧版浏览器中程序也能正确运行,因此我们需要使用 webpack 进行打包;Node.js 环境是完全支持 ES6 语法的,但是 webpack 提供的很多 loader 在 Node.js 环境中是不生效的,所以服务端和客户端需要两套不同的配置和打包步骤。Vue SSR 官网给我们提供了一套可供参考的代码结构。

1、源码结构

源码结构可以参考官网推荐的源码结构,
① 创建目录src/App.vue,里面存放应用的模板

<template>
  <div id="app">{{ message }}div>
template>
<script>
export default {
  data() {
    return {
      message: '哈哈哈'
    }
  }
}
script>

② 创建src/app.js,里面创建并导出一个创建 Vue 应用的工厂函数,这是为了每次请求都创建一个新的 Vue 实例,避免相互污染。Node.js 一旦进入一个进程,变量的状态会一直保存,所以多次请求会使用同一个 Vue 实例。所以这里要导出一个工厂函数,保证每次请求到的都是新的 Vue 实例。

/**
 * 同构应用通用的启动入口
 * */
import Vue from 'vue'
import App from './App.vue'

// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
    const app = new Vue({
        // 根实例简单的渲染应用程序组件。
        render: h => h(App)
    })
    return { app }
}

③ 创建客户端模块
src/entry-client.js

/**
 * 客户端入口
 * */
import { createApp } from './app'

// 客户端特定引导逻辑……

const { app } = createApp()

// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')

④ 服务端入口
src/entry-server.js

/**
 * 服务端入口
 * */
import { createApp } from './app'

export default context => {
    const { app } = createApp()
    // 服务端路由处理、数据预取 ...
    return app
}

创建好源码结构后,需要使用 webpack 进行打包构建,服务器端代码需要打包成「服务器 bundle」,然后用于服务器端渲染(SSR),而客户端代码要打包成「客户端 bundle」,处理客户端渲染。

2、安装依赖

(1)安装生产依赖(这里有坑哦,其实应该安装最新版本的 vue ,否则的话就会遇到和本仙女一样的坑,真实记录一下,大家可以避坑)
npm i [email protected] vue-server-renderer express cross-env

说明
vue Vue.js 核心库
vue-server-renderer Vue 服务端渲染工具
express 基于 Node 的 Web 服务框架
cross-env 通过 npm scripts 设置跨平台环境变量,用于区分不同模式的打包环境:生产模式、开发模式

(2)安装开发依赖
npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler friendly-errors-webpack-plugin

说明
webpack webpack 核心包
webpack-cli webpack 的命令行工具
webpack-merge webpack 配置信息合并工具;把服务端打包的配置文件和客户端打包的配置文件中的共同部分抽取出来进行合并
webpack-node-externals 排除 webpack 中的 Node 模块,例如fs、http、path等不需要进行打包
rimraf 基于 Node 封装的一个跨平台 rm -rf 工具;可以在命令行执行删除操作,主要用于删除之前的打包出来的dist
friendly-errors-webpack-plugin 友好的 webpack 错误提示
@babel/core、@babel/plugin-transform-runtime、@babel/preset-env、babel-loader Babel 相关工具,把项目中的es6转换成es5
vue-loader、vue-template-compiler 处理 .vue 资源
file-loader 处理字体资源
css-loader 处理 CSS 资源
url-loader 处理图片资源

运行上述命令安装生产环境的依赖和开发环境的依赖。

3、webpack配置文件

初始化 webpack 打包配置文件
在根目录下新建 build 文件夹存放 webpack 配置文件
build
├── webpack.base.config.js # 公共配置
├── webpack.client.config.js # 客户端打包配置文件
└── webpack.server.config.js # 服务端打包配置文件
可以参考一下配置
webpack.base.config.js

/**
 * 公共配置
 */
// 处理.vue资源的插件
const VueLoaderPlugin = require('vue-loader/lib/plugin')
// 处理文件路径
const path = require('path')
// 友好的错误日志输出
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
// 拼接文件名和路径
const resolve = file => path.resolve(__dirname, file)
// 环境变量中的NODE_ENV
const isProd = process.env.NODE_ENV === 'production'

module.exports = {
    mode: isProd ? 'production' : 'development',
    output: {
        path: resolve('../dist/'), // 打包结果输出到dist文件夹
        publicPath: '/dist/',  // 所有的打包结果在请求的时候都以 /dist 开头,避免和路由匹配规则冲突
        filename: '[name].[chunkhash].js'  // 文件名 + hash 生成文件名;一旦文件内容发生改变,生成的文件名也会发生变化,强制浏览器请求新的资源
    },
    resolve: {
        alias: {
            // 路径别名,@ 指向 src
            '@': resolve('../src/')
        },
        // 可以省略的扩展名
        // 当省略扩展名的时候,按照从前往后的顺序依次解析
        extensions: ['.js', '.vue', '.json']
    },
    devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map', // 方便定位源代码的位置
    module: {
        rules: [
            // 处理图片资源
            {
                test: /\.(png|jpg|gif)$/i,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192,
                        },
                    },
                ],
            },
            // 处理字体资源
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: [
                    'file-loader',
                ],
            },
            // 处理 .vue 资源
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            // 处理 CSS 资源
            // 它会应用到普通的 `.css` 文件
            // 以及 `.vue` 文件中的 `