美文网首页Vue
Vue服务器端渲染(Vue SSR )

Vue服务器端渲染(Vue SSR )

作者: CodeMT | 来源:发表于2022-09-15 11:34 被阅读0次

    简介


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

    如何看一个网页是否是服务器端渲染?

    简单的方式是在 Chrome 浏览器打开控制台/开发者工具,查看 Network 中加载的资源,如下图所示segmentfault 网站,可以看到第一个文件总是 document 类型,这是服务器发送过来的完整的 HTML文档,浏览器只需要加载 css/js 进行视图渲染即可。

    看 Vue SSR 官网也是服务器端渲染:

    这里说的渲染,就是指生成 HTML 文档的过程,和之前浏览器的 CSS+HTML 渲染没有关系。简单来说,浏览器端渲染,指的是用 JS 去生成 HTML,例如 React, Vue 等前端框架做的路由。服务器端渲染,指的是用后台语言通过一些模版引擎生成 HTML,例如 Java 配合 VM 模版引擎、NodeJS配合Jade 等,将数据与视图整理输出为完整的 HTML 文档发送给浏览器。

    一、入门配置

    1. 起步

    新建项目,安装 VueSSR 依赖包 vue-server-renderer

    $ mkdir testSSR // 新建空文件夹 vueSSR
    $ cd testSSR // 进入 vueSSR 目录
    $ npm init // 初始化,生成 package.json
    $ npm install vue vue-server-renderer --save-dev // 安装
    

    先使用 vue-server-renderer 渲染一个简单的 Vue 组件

    $ touch test.js // 新建 test.js
    
    // test.js
    const Vue = require('vue')
    const app = new Vue({ // 创建一个 Vue 实例
      template: `<div>Hello World</div>`
    })
    const vueRenderer = require('vue-server-renderer')
    const renderer = vueRenderer.createRenderer() // 创建一个 renderer
    // 通过 renderToString 将 Vue 实例渲染为 HTML
    // 函数签名: renderer.renderToString(vm, context?, callback?): ?Promise<string>
    renderer.renderToString(app, (err, doc) => {
      if (err) throw err
      console.log(doc)
    })
    

    运行 test.js,输出渲染后的 HTML

    注意到应用程序的根元素上添加了一个特殊的属性 data-server-rendered,这是让客户端 Vue 知道这
    部分 HTML 是由 Vue 在服务端渲染的。

    2. 引入模板

    上例只是渲染一个 vue 组件,通常应用程序都会抽象出一个或多个模板来嵌入不同的组件。
    Render 的 template 选项为整个页面的 HTML 提供一个模板。此模板应包含注释 <\!--vue-ssr-outlet-->作为渲染应用程序内容的占位符。

    首先创建一个 HTML 模板 index.template.html

    <!--index.template.html -->
    <!doctype html>
    <html lang="en">
    <head>
      <title></title>
    </head>
    <body>
    <!--vue-ssr-outlet-->
    </body>
    </html>
    

    这里的 <\!--vue-ssr-outlet--> 注释就是应用程序 HTML 标记注入的地方。

    将此模板通过 fs 读取, 然后在 createRenderer( ) 时注入,修改 test.js 如下:

    // test.js
    //const renderer = vueRenderer.createRenderer()
    const fs = require('fs')
    const renderer = vueRenderer.createRenderer({
      template: fs.readFileSync('./index.template.html', 'utf-8') // 同步读取文件
    })
    

    运行 test.js 可以看到之前定义的 hello world 组件已嵌入模板中。

    二、服务器端整合

    选取基于 node.js 的 express 作为服务器,示例 vue ssr 在服务器端的工作。

    1. 启动 express server

    $ cd testSSR // 进入项目
    $ npm install express --save-dev // 安装 express
    $ touch server.js // 新建 server.js
    

    引入 express 并设置一个测试路由

    // server.js
    const express = require('express')
    const server = express()
    server.get('/mytest', (request, response) => {
      response.send("hello world "+request.url)
    })
    server.listen(8000)
    

    运行$ node server.js 后打开浏览器访问 http://localhost:8000/mytest

    服务器启动成功。

    2. Renderer 渲染

    首先创建一个可以重复执行的工厂函数,为每个请求创建新的 Vue 实例,如果创建一个单例对象,它将在每个传入的请求之间共享,很容易导致交叉请求状态污染。

    $ cd testSSR // 进入项目
    $ touch app.js // 新建 app.js
    
    // app.js
    const Vue = require('vue')
    module.exports = function createApp (context) {
      return new Vue({
        data: {
          url: context.url
        },
        template: `<div>Vue SSR URL: {{ url }}</div>`
      })
    }
    

    然后在 server.js 中引入 app.js 创建实例,并配置路由与请求渲染。

    // server.js
    const createApp = require('./app')
    const vueRenderer = require('vue-server-renderer')
    const renderer = vueRenderer.createRenderer()
    
    server.get('/ssr', (request, response) => {
      const context = { url: request.url }
      const app = createApp(context)
      renderer.renderToString(app, (err, doc) => {
        if (err) throw err
        response.send(doc)
      })
    })
    

    运行$ node server.js 后打开浏览器访问 http://localhost:8000/ssr?sadas=2222

    3. 插入模板

    增加页面模板,使用之前定义的 index.template.html 作为模板,注入到一个新的 renderer

    // server.js
    const fs = require('fs')
    const rendererTmp = vueRenderer.createRenderer({
      template: fs.readFileSync('./index.template.html', 'utf-8') // 同步读取文件
    })
    server.get('/template', (request, response) => {
      const context = { url: request.url }
      const app = createApp(context)
      rendererTmp.renderToString(app, (err, doc) => {
        if (err) throw err
        response.send(doc)
      })
    })
    

    运行$ node server.js 后打开浏览器访问 http://localhost:8000/template

    可以看到一个简单的服务器端渲染已经完成。

    三、项目工程化

    1. SSR 项目结构

    通常 Vue 应用程序是由 webpack 和 vue-loader 构建,并且许多 webpack 特定功能不能直接在Node.js 中运行(例如通过 file-loader 导入文件,通过 css-loader 导入 CSS)。

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

    所以一个基本的项目目录可能如下:

    src
    ├── config
    │ ├── webpack.base.config.js
    │ ├── webpack.client.config.js
    │ └── webpack.server.config.js
    ├── components
    │ ├── Foo.vue
    │ └── xxx.vue
    ├── build
    │ ├── index.js
    │ └── xxx.js
    ├── template
    │ ├── index.template.html
    │ └── xxx.html
    ├── route.js # vue-router 路由
    ├── App.vue # 根实例
    ├── app.js # 通用 entry
    ├── entry-client.js # 配置 仅运行于浏览器
    ├── entry-server.js # 配置 仅运行于服务器
    ├── server.js # 服务器
    ├── webpack.config.js
    └── package.json
    

    2. 配置路由

    使用 vue-router

    $ npm intall vue-router --save-dev
    $ touch route.js
    

    在新建的 router.js 中创建 router,类似于 createApp,我们也需要给每个请求一个新的 router 实例,
    所以文件导出一个 createRouter 函数

    // router.js
    import Vue from 'vue'
    import Router from 'vue-router'
    Vue.use(Router)
    export function createRouter () {
      return new Router({
        mode: 'history',
        routes: [
          // ...
        ]
      })
    }
    

    修改 app.js,添加路由

    // app.js
    import Vue from 'vue'
    import App from './App.vue'
    import { createRouter } from './router'
    export function createApp () {
      // 创建 router 实例
      const router = createRouter()
      const app = new Vue({
        // 注入 router 到根 Vue 实例
        router,
        render: h => h(App)
      })
      // 返回 app 和 router
      return { app, router }
    }
    

    3. 配置 webpack

    新建 entry-server.js,实现服务器端路由逻辑:

    // entry-server.js
    import { createApp } from './app'
    export default context => {
      // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
      // 以便服务器能够等待所有的内容在渲染前,
      // 就已经准备就绪。
      return new Promise((resolve, reject) => {
        const { app, router } = createApp()
        // 设置服务器端 router 的位置
        router.push(context.url)
        // 等到 router 将可能的异步组件和钩子函数解析完
        router.onReady(() => {
          const matchedComponents = router.getMatchedComponents()
          // 匹配不到的路由,执行 reject 函数,并返回 404
          if (!matchedComponents.length) {
            return reject({ code: 404 })
          }
          // Promise 应该 resolve 应用程序实例,以便它可以渲染
          resolve(app)
        }, reject)
      })
    }
    

    在生成 vue-ssr-server-bundle.json 之后,只需将文件路径传递给 createBundleRenderer:

    // webpack.server.config.js
    const merge = require('webpack-merge')
    const nodeExternals = require('webpack-node-externals')
    const baseConfig = require('./webpack.base.config.js')
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    module.exports = merge(baseConfig, {
      // 将 entry 指向应用程序的 server entry 文件
      entry: '/path/to/entry-server.js',
      // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
      // 并且还会在编译 Vue 组件时,
      // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
      target: 'node',
      // 对 bundle renderer 提供 source map 支持
      devtool: 'source-map',
      // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
      output: {
        libraryTarget: 'commonjs2'
      },
      // https://webpack.js.org/configuration/externals/#function
      // https://github.com/liady/webpack-node-externals
     // 外置化应用程序依赖模块。可以使服务器构建速度更快,
      // 并生成较小的 bundle 文件。
      externals: nodeExternals({
        // 不要外置化 webpack 需要处理的依赖模块。
        // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
        // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
        whitelist: /\.css$/
      }),
      // 这是将服务器的整个输出
      // 构建为单个 JSON 文件的插件。
      // 默认文件名为 `vue-ssr-server-bundle.json`
      plugins: [
        new VueSSRServerPlugin()
      ]
    })
    

    在生成 vue-ssr-server-bundle.json 之后,只需将文件路径传递给 createBundleRenderer:

    // server.js
    const { createBundleRenderer } = require('vue-server-renderer')
    const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
      // ……renderer 的其他选项
    })
    

    除了 server bundle 之外,我们还可以生成客户端构建清单(client build manifest)。使用客户端清单(client manifest)和服务器bundle(server bundle),renderer 现在具有了服务器和客户端的构建信息,因此它可以自动推断和注入资源预加载 / 数据预取指令(preload / prefetch directive),以及 css 链接 / script 标签到所渲染的 HTML。

    // webpack.client.config.js
    const webpack = require('webpack')
    const merge = require('webpack-merge')
    const baseConfig = require('./webpack.base.config.js')
    const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
    module.exports = merge(baseConfig, {
      entry: '/path/to/entry-client.js',
      plugins: [
        // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
        // 以便可以在之后正确注入异步 chunk。
        // 这也为你的 应用程序/vendor 代码提供了更好的缓存。
        new webpack.optimize.CommonsChunkPlugin({
          name: "manifest",
          minChunks: Infinity
        }),
        // 此插件在输出目录中
        // 生成 `vue-ssr-client-manifest.json`。
        new VueSSRClientPlugin()
      ]
    })
    

    这样就可以生成客户端构建清单(client build manifest)。

    4. Bundle Renderer

    到目前为止,我们假设打包的服务器端代码,将由服务器通过 require 直接使用:

    const createApp = require('/path/to/built-server-bundle.js')
    

    然而在每次编辑过应用程序源代码之后,都必须停止并重启服务。这在开发过程中会影响开发效率。
    此外,Node.js 本身不支持 source map。

    vue-server-renderer 提供一个名为 createBundleRenderer 的 API,用于处理此问题,通过使用
    webpack 的自定义插件,server bundle 将生成为可传递到 bundle renderer 的特殊 JSON 文件。

    // server.js
    const { createBundleRenderer } = require('vue-server-renderer')
    const template = require('fs').readFileSync('/path/to/template.html', 'utf-8')
    const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
    const clientManifest = require('/path/to/vue-ssr-client-manifest.json')
    const renderer = createBundleRenderer(serverBundle, {
      runInNewContext: false, // 推荐
      template, // (可选)页面模板
      clientManifest // (可选)客户端构建 manifest
    })
    // 在服务器处理函数中……
    server.get('/', (req, res) => {
      const context = { url: req.url }
      // 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。
      // 现在我们的服务器与应用程序已经解耦!
      renderer.renderToString(context, (err, html) => {
        // 处理异常……
        res.end(html)
      })
    })
    

    相关文章

      网友评论

        本文标题:Vue服务器端渲染(Vue SSR )

        本文链接:https://www.haomeiwen.com/subject/yemccktx.html