Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue-ssr服务端渲染 #15

Open
Wscats opened this issue Mar 11, 2017 · 0 comments
Open

vue-ssr服务端渲染 #15

Wscats opened this issue Mar 11, 2017 · 0 comments

Comments

@Wscats
Copy link
Owner

Wscats commented Mar 11, 2017

安装

  • 推荐使用 Node.js 版本 6+。
  • vue-server-renderervue 必须匹配版本。
  • vue-server-renderer 依赖一些 Node.js 原生模块,因此只能在 Node.js 中使用。我们可能会提供一个更简单的构建,可以在将来在其他「JavaScript 运行时(runtime)」运行。
npm install vue vue-server-renderer express --save

渲染实例

新建一份test.js写入以下代码,并运行node test,将会运行出三段结果<div data-server-rendered="true">Hello World</div>,说明已经成功实现服务端渲染。

// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
    data: {
        title: 'Hello World'
    },
    template: `<div @click='testClick'>{{title}}</div>`,
    methods: {
        testClick() {
            console.log('Hello World')
        }
    }
})

// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()

// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
    if (err) throw err
    console.log(html)
    // => <div data-server-rendered="true">Hello World</div>
})

// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer.renderToString(app).then(html => {
    console.log(html)
    // => <div data-server-rendered="true">Hello World</div>
}).catch(err => {
    console.error(err)
})

// await和async
;(async (renderer) => {
    let html = await renderer.renderToString(app)
    console.log(html)
})(renderer)

与服务器集成

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

const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
    const app = new Vue({
        data: {
            url: req.url
        },
        template: `<div>访问的 URL 是: {{ url }}</div>`
    })

    renderer.renderToString(app, (err, html) => {
        if (err) {
            res.status(500).end('Internal Server Error')
            return
        }
        res.end(`
            <!DOCTYPE html>
            <html lang="en">
                <head><title>Hello</title></head>
                <body>${html}</body>
            </html>
        `)
    })
})

server.listen(8080)

使用一个页面模板

当你在渲染 Vue 应用程序时,renderer 只从应用程序生成 HTML 标记 (markup)。在这个示例中,我们必须用一个额外的 HTML 页面包裹容器,来包裹生成的 HTML 标记。

const Vue = require('vue')
const fs = require('fs')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./index.template.html', 'utf-8')
})
server.get('*', (req, res) => {
    const app = new Vue({
        data: {
            url: req.url
        },
        template: `<div>访问的 URL 是: {{ url }}</div>`
    })
    // 我们可以通过传入一个"渲染上下文对象",作为 renderToString 函数的第二个参数,来提供插值数据:
    renderer.renderToString(app, {
        // 页面 title 将会是 "Hello"
        title: 'hello',
        // meta 标签也会注入
        meta: `
          <meta charset="utf-8" />
        `
    }, (err, html) => {
        if (err) {
            res.status(500).end('Internal Server Error')
            return
        }
        res.end(html)
    })
})
server.listen(8080)

为了简化这些,你可以直接在创建 renderer 时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中,例如 index.template.html

<!DOCTYPE html>
<html>
  <head>
    <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
    <title>{{ title }}</title>

    <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
    {{{ meta }}}
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

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

然后,我们可以读取和传输文件到 Vue renderer 中:

生命周期

由于没有动态更新,所有的生命周期钩子函数中,只有 beforeCreate 和 created 会在服务器端渲染 (SSR) 过程中被调用。这就是说任何其他生命周期钩子函数中的代码(例如 beforeMount 或 mounted),只会在客户端执行。

DOM 和 BOM

通用代码不可接受特定平台的 API,因此如果你的代码中,直接使用了像 windowdocument,这种仅浏览器可用的全局变量,则会在 Node.js 中执行时抛出错误,反之也是如此。

避免状态单例

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

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

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

const Vue = require('vue')
module.exports = function createApp(context) {
    // 为每个请求创建一个新的根 Vue 实例
    return new Vue({
        data: {
            url: context.url
        },
        template: `<div>访问的 URL 是: {{ url }} </div>`
    })
}

并且我们的服务器代码现在变为:

const fs = require('fs')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./index.template.html', 'utf-8')
})
// 导入封装好server.js
const createApp = require('./app')
server.get('*', (req, res) => {
    const context = { url: req.url }
    // 为每个请求创建新的应用程序实例,避免导致交叉请求状态污染
    const app = createApp(context)
    // 我们可以通过传入一个"渲染上下文对象",作为 renderToString 函数的第二个参数,来提供插值数据:
    renderer.renderToString(app, {
        // 页面 title 将会是 "Hello"
        title: 'hello',
        // meta 标签也会注入
        meta: `
          <meta charset="utf-8" />
        `
    }, (err, html) => {
        if (err) {
            res.status(500).end('Internal Server Error')
            return
        }
        res.end(html)
    })
})
server.listen(8080)

同样的规则也适用于 router、store 和 event bus 实例。你不应该直接从模块导出并将其导入到应用程序中,而是需要在 createApp 中创建一个新的实例,并从根 Vue 实例注入。

使用 webpack 的源码结构

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

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

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

app.js

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

import Vue from 'vue'
import App from './App.vue'

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

entry-client.js:

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

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)。

import { createApp } from './app'

export default context => {
  const { app } = createApp()
  return app
}

参考文档

@Wscats Wscats changed the title vue-awesome-swiper vue-ssr Jul 27, 2019
@Wscats Wscats changed the title vue-ssr vue-ssr服务端渲染 Jul 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant