【服务端渲染】之 Vue SSR

前言

笔记来源:拉勾教育 大前端高薪训练营

阅读建议:内容较多,建议通过左侧导航栏进行阅读

Vue SSR

基本介绍

Vue SSR 是什么

  • 官方文档:https://ssr.vuejs.org/
  • Vue SSR(Vue.js Server-Side Rendering)是 Vue.js 官方提供的一个服务端渲染(同构应用)解决方案
  • 使用它可以构建同构应用
  • 还是基于原有的 Vue.js 技术栈

官方文档的解释:

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

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

使用场景

在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。

技术层面:

  • 更快的首屏渲染速度
  • 更好的 SEO

业务层面:

  • 不适合管理系统
  • 适合门户资讯类网站,例如企业官网、知乎、简书等
  • 适合移动网站

如何实现 Vue SSR

  1. 基于 Vue SSR 官方文档提供的解决方案

    官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中, 也会对Vue SSR有更加深入的了解。

    该方式需要你熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。

  2. Nuxt.js 开发框架

    NUXT提供了平滑的开箱即用的体验,它建立在同等的Vue技术栈之上,但抽象出很多模板,并提供了 一些额外的功能,例如静态站点生成。通过 Nuxt.js 可以快速的使用 Vue SSR 构建同构应用。

基本使用

接下来我们以 Vue SSR 的官方文档为参考,来学习一下它的基本用法。

渲染一个 Vue 实例

目标:

  • 了解如何使用 VueSSR 将一个 Vue 实例渲染为 HTML 字符串

首先,我们来学习一下服务端渲染中最基础的工作:模板渲染。 说白了就是如何在服务端使用 Vue 的方式解析替换字符串。

在它的官方文档中其实已经给出了示例代码,下面我们来把这个案例的实现过程以及其中含义演示一 下。

  • 准备工作

    # 创建文件夹
    mkdir vue-ssr
    # 进入创建的文件夹内
    cd vue-ssr
    # 初始化包管理文件 package.json
    npm init -y # -y 表示快速初始,不经过一些问题的回答,直接走默认回答
    # 安装 vue 及 vue-server-renderer 依赖包
    npm i vue vue-serve-renderer
    
  • 创建 server.js 文件,将 Vue 实例渲染成 HTML 字符串

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

    { { message }}

    `
    , data: { message: '拉勾教育' } }) // 第 2 步:创建一个 renderer 渲染器 const renderer = require('vue-server-renderer').createRenderer() /** * @param {Vue} app * @param {Function} */ // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { if (err) throw err // html 就是渲染出来的结果字符串 console.log(html); //

    拉勾教育

    }) // 在 2.5.0+,如果没有传入回调函数,则会返回 Promise: renderer .renderToString(app) .then((html) => { console.log(html); }) .catch((err) => { console.error(err); });
  • 使用 node 运行 server.js

    node server.js
    

    执行效果,如图所示:

    【服务端渲染】之 Vue SSR_第1张图片

结合到 Web 服务中

在 Node.js 服务器中使用时相当简单直接,例如 Express

  • 首先,安装 Express 到项目中:

    npm i express
    
  • 然后,使用 Express 创建一个基本的 Web 服务:

    const express = require("express"); 
    const app = express();
    app.get("/", (req, res) => {
             
        res.send("Hello World!"); 
    });
    app.listen(3000, () => console.log("app listening at http://localhost:port"));
    
  • 启动 Web 服务,nodemon 启动服务,可以实时监听,热更新:

    nodemon server.js
    
  • 在 Web 服务中渲染 Vue 实例:

    const Vue = require('vue')
    const express = require('express')
    
    // 创建一个渲染器
    const renderer = require('vue-server-renderer').createRenderer()
    
    // 创建一个 experss 的 server 实例
    const server = express()
    
    // 添加路由
    server.get('/', (req, res) => {
           
        const app = new Vue({
           
            template: `
                

    { { message }}

    `
    , data: { message: '拉勾教育' } }) renderer.renderToString(app, (err, html) => { if (err) { return res.status(500).end('Internal Server Error') } // html 就是渲染出来的结果字符串 res.end(html) }) }) server.listen(3000, () => { console.log('server running at port 3000'); })

    浏览器访问结果,如图所示:

    【服务端渲染】之 Vue SSR_第2张图片

    请求响应数据,如图所示:

    【服务端渲染】之 Vue SSR_第3张图片

  • 解决上面出现的乱码问题,添加 html响应头,或 使用 meta 设置编码格式

    ......
    server.get('/', (req, res) => {
           
    	......
        renderer.renderToString(app, (err, html) => {
           
            if (err) {
           
                return res.status(500).end('Internal Server Error')
            }
            // html 就是渲染出来的结果字符串
            // 添加响应头,解决编码问题
            res.setHeader('Content-Type', 'text/html; charset=utf8')
            res.end(`
                
                
                
                    
                    
                    Document
                
                
                    ${
             html}
                
                
            `)
        })
    })
    ......
    

    建议:

    ​ 将上述的两种解决编码的方案,都保留

使用一个页面模板

  • 创建一个页面模板 index.template.html,优化上述代码

    
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Documenttitle>
        head>
        <body>
            
        body>
    html>
    

    注意:

    • 上面的 注释语句,是固定语法。会作为 Vue实例 转换的 html 字符串存放的位置,类似于占位。
    • vue-ssr-outlet 前后不可以有空格。
  • server.js 中,创建 renderer 渲染器时,添加一个参数,指定模板

    const Vue = require('vue')
    const express = require('express')
    const fs = require('fs') // 读取文件
    
    // 创建一个渲染器
    const renderer = require('vue-server-renderer').createRenderer({
           
        // 设置模板
        template: fs.readFileSync('./index.template.html', 'utf-8')
    })
    
    // 创建一个 experss 的 server 实例
    const server = express()
    
    // 添加路由
    server.get('/', (req, res) => {
           
        ......
        renderer.renderToString(app, (err, html) => {
           
            ......
            // 结合了模板的完整内容
            res.end(html)
        })
    })
    
    server.listen(3000, () => {
           
        console.log('server running at port 3000');
    })
    
  • 访问浏览器,查看响应数据,如图所示:

    【服务端渲染】之 Vue SSR_第4张图片

在模板中使用外部数据

  • server.js,在使用 renderer 渲染器进行渲染时,传入第二个参数(可选的),配置传入到模板中的数据

    renderer.renderToString(app, {
           
            // 配置传到模板中的数据
            title: '拉勾教育',
            meta: ``
        }, (err, html) => {
           
        ......
    })
    
  • index.template.html,使用 vue 中的模板引擎的语法,进行渲染

    {
          {
          { meta }}}
    <title>{
          { title }}title>
    

    { { { }}} 表示原文输出

构建同构渲染

  • 官网地址:[https://ssr.vuejs.org/zh/guide/structure.html#%E4%BD%BF%E7%94%A8-webpack-%E7%9A%84%E6%BA%90%E7%A0%81%E7%BB%93%E6%9E%84](https://ssr.vuejs.org/zh/guide/structure.html#%E4%BD%BF%E7%94%A8-webpack-%E7%9A%84%E6%BA%90%E7%A0%81%E7%BB%93%E6%9E%84)

构建流程

【服务端渲染】之 Vue SSR_第5张图片

源码结构

我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack 打包 Vue 应用程序,因为:

  • 通常 Vue 应用程序是由 webpackvue-loader 构建,并且许多 webpack 特定功能 不能直接在 Node.js 中运行(例如通过 file-loader 导入文件,通过 css-loader 导入 CSS)。
  • 尽管 Node.js 最新版本能够完全支持 ES2015 特性,我们还是需要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。

所以基本看法是,对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要 「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。

现在我们正在使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用 webpack 支持的所有功能。同时,在编写通用代码时,有一些事项要牢记在心。

一个基本项目可能像是这样:

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

App.vue

  • src/App.vue

    <template>
        <div id="app">
            <h1>{
          { message }}h1>
            <h2>客户端动态交互h2>
            <div>
                <input v-model="message" />
            div>
            <div>
                <button @click="onClick">点击测试button>
            div>
        div>
    template>
    
    <script>
        export default {
            
            name: "App",
            data () {
            
                return {
             
                    message: "拉勾教育",
                }
            },
            methods: {
            
                onClick() {
            
                    console.log("Hello World");
                },
            },
        };
    script>
    

app.js

app.js 是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js 简单地使用 export 导出一个 createApp 函数:

  • src/app.js

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

    避免状态单例

    当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。

    如基本示例所示,我们为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)

    因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序、router 和 store实例。

entry-client.js

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

  • src/entry-client.js

    /**
     * 客户端入口
     */
    import {
            createApp } from './app'
    
    // 客户端特定引导逻辑……
    
    const {
            app } = createApp()
    
    // 这里假定 App.vue 模板中根元素具有 `id="app"`
    app.$mount('#app')
    

entry-server.js

服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回 应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。

  • src/entry-server.js

    /**
     * 服务端启动入口
     */
    
    import {
            createApp } from './app'
    
    export default context => {
           
        const {
            app } = createApp()
    
        // 服务端路由处理、数据预取...
        return app
    }
    

构建配置

安装依赖

  • 1,安装生产依赖

    npm i vue 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 模块
    rimraf 基于 Node 封装的一个跨平台 rm -rf 工具
    friendly-errors- webpack-plugin 友好的 webpack 错误提示
    @babel/core
    @babel/plugin-transform-runtime
    @babel/preset-env
    babel-loader


    Babel 相关工具
    vue-loader
    vue-template-compiler
    处理 .vue 资源
    file-loader 处理字体资源
    css-loader 处理 CSS 资源
    url-loader 处理图片资源

配置文件

  1. 初始化 webpack 打包配置文件

    build
    ├── webpack.base.config.js # 公共配置
    ├── webpack.client.config.js # 客户端打包配置文件 
    └── webpack.server.config.js # 服务端打包配置文件
    
  • build/webpack.base.config.js

    /**
    * 公共配置 
    */
    // 处理 .vue 资源的插件
    const VueLoaderPlugin = require('vue-loader/lib/plugin')
    // 处理路径
    const path = require('path')
    // 提供 webpack 打包过程中,友好的日志输出
    const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
    
    // 将路径进行组合,从而得到绝对安全的路径
    const resolve = file => path.resolve(__dirname, file)
    
    const isProd = process.env.NODE_ENV === 'production'
    
    module.exports = {
           
        // 打包模式
        mode: isProd ? 'production' : 'development', 
        // 打包结果输出
        output: {
           
            path: resolve('../dist/'), 
            publicPath: '/dist/', // 打包结果的请求路径
            filename: '[name].[chunkhash].js'
        },
        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` 文件中的 `