乐趣区

关于javascript:SSR-服务器端渲染

前端的 SSR 计划最近几年比拟热门,React 的 next 框架 , Vue 的 nuxt 框架等等

简介:

应用 Vue.js 构建客户端应用程序时,默认状况下是在浏览器中输入 Vue 组件,进行生成 DOM 和操作 DOM。而应用 SSR 能够将同一个组件渲染为服务器端的 HTML 字符串,而后将它们间接发送到浏览器,最初将动态标记 ” 混合 ” 为客户端上齐全交互的应用程序。

浏览器端渲染:指的是用 JS 去生成 HTML,例如 React, Vue 等前端框架做的路由

服务器端渲染:指的是用后盾语言通过一些模版引擎生成 HTML,例如 Java 配合 VM 模版引擎、NodeJS 配合 Jade 等,将数据与视图整顿输入为残缺的 HTML 文档发送给浏览器。

一、入门配置

1. 起步

新建我的项目,装置 Vue 与 SSR 依赖包 vue-server-renderer

新建空文件夹
$ mkdir testSSR 
进入 testSSR 目录
$ cd testSSR
初始化,生成 package.json
$ npm init
装置依赖
npm install vue vue-server-renderer --save-dev
先应用 vue-server-renderer 渲染一个简略的 Vue 组件
// 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 能够看到之前定义的 hello world 组件已嵌入模板中

二、服务器端整合

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

1. 启动 express server
进入我的项目
$ cd testSSR
装置 express
$ npm install express --save-dev
新建 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
新建 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
新建 router.js

相似于 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,增加路由
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)
    })
}
服务器端配置 webpack.server.config.js,是用于生成传递给 createBundleRenderer 的 server bundle
// 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 SSR 提供 css 治理、缓存治理、流式渲染等,期待当前持续整顿。
Vue SSR 指南:https://ssr.vuejs.org/zh/
API 参考:https://ssr.vuejs.org/zh/api/

退出移动版