搭建自己的SSR

一、基本使用

渲染一个Vue实例

在服务端把 Vue实例渲染为纯文本字符串

mkdir vue-ssr
cd vue-ssr
npm init -y 
npm install vue vue-server-renderer

// server.js
const vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()

const app = new Vue({
	template: `<div id="app">
		<h1>{{msg}}</h1>
	</div>`,
	data: {
		msg: 'xxx'
	}
})

renderer.renderToString(app, (err, html)=> {
	if(err) throw err
	console.log(html)
})


// 运行
node server.js
结合到 Web 服务中

将渲染结果发送到客户端

// 安装 web服务
npm install express nodemon --save

// 加载并启动服务  server.js
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const express = require('express')

const server = express()

server.get('/', (req, res) => {
    const app = new Vue({
        template: `<div>
            <h1>{{ msg }}</h1>
        </div>`,
        data: {
            msg: 'xxx'
        }
    })
    
    renderer.renderToString(app, (err, html) => {
        if(err) {
            return res.status(500).end('Internal Server Error.')
        }
        // 为html 设置 UTF-8编码
        res.setHeader('Content-Type', 'text/html; charset=utf-8')
        // res.end(html)
        res.end(`
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta http-equiv="X-UA-Compatible" content="IE=edge">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Document</title>
            </head>
            <body>
                ${ html }
            </body>
            </html>
        `)
    })
})

server.listen('3000', () => {			// 一个端口对应一个程序,总共有 65535个端口
    console.log('server running at port 3000.')
})

// 使用nodemon 运行
npx nodemon server.js
使用 HTML 模板
//index.tempalte.html
<body>
    <!--vue-ssr-outlet-->					// html会插入到这个位置
</body>

// server.js
const fs = require('fs')
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./index.template.html', 'utf-8')
})
   // ...
res.end(html)						// 此时发送的html, 是与模板结合后的
在模板中使用外部数据
// server.js     多加一个参数
 renderer.renderToString(app, {
        title: '使用外部数据',
        meta: `<meta name="description" content="vue ssr 插入外部数据">`
    },(err, html) => { // ... })

// index.tempalte.html
<meta name="viewport" content="width=device-width, initial-scale=1.0">
    {{{ meta }}}							// html 使用  {{{ }}}
<title>{{ title }}</title>					// 字符使用 {{  }}

二、构建配置

基本思路

服务端渲染只是将实例渲染成字符串,并没有实现客户端交互的功能。

服务端入口——> 打包——>渲染——>生成HTML

客户端入口——> 打包——>接管已生成的HTML ——> 激活交互功能

源码结构
// 1.  src/App.vue
    <template>
      <div id="app">
          <h1> {{ msg }}</h1>
          <p>
              <input type="text" v-model="msg"/>
          </p>
          <button @click="onClick">点击</button>
      </div>
    </template>

    <script>
    export default {
        name: 'App',
        data(){
            return {
                msg: 'xxx'
            }
        },
        methods: {
            onClick(){
                console.log('lagou ~')
            }
        }
    }
    </script>

// 2.  src/app.js
/**
 * 通用入口文件
 * 纯客户端时,负责创建实例,并挂载到DOM。SSR,责任转移到纯客户端的entry 文件
 * */ 
    import Vue from 'vue'
    import App from './App.vue'

    // 导出一个工厂函数,用于创建新的应用程序、router、store 实例
    export default createApp(){
        const app = new Vue({
            render: h => h(app)
        })
        return { app }
    }

// 3.  entry-client.js
/**
 * 客户端入口文件
 * 负责创建应用程序,并挂载到DOM
 * 
 * */ 
    import { createApp } from './app'

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

    const { app } = createApp()

    // 挂载到 App.vue 中的 id="app"
    app.$mount('#app')


// 4. entry-server.js
/**
 * 服务端入口文件
 * 
 * */ 

    import { createApp } from './app'

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

        // 服务端路由处理、数据预取... 

        return app
    }
安装依赖
// 1. 生产依赖
	vue   Vue.js核心库
	vue-server-renderer   Vue服务端渲染工具
	express   基于Node的Web服务框架
	cross-en   通过npm sripts 设置的跨平台环境变量
	
// 2. 安装开发依赖
	webpack
	webpack-cli
	webpack-merge
	webpack-node-externals		// 排除webpack中的node模块
	@babel/core
	@babel/plugin-transform-runtime
	@babel/preset-env
	babel-loader
	css-loader
	url-loader
	file-loader
	rimraf			// 基于Node封装的跨平台 rm -rf 工具,清除之前的dist
	vue-loader
	vue-template-compiler
	friendly-errors-webpack-plugin  // webpack错误提示
webpack 配置文件
  1. build 文件夹
    1. webpack.base.config.js 公共配置
    2. webpack.client.config.js 客户端打包配置文件
    3. webpack.server.config.js 服务端打包配置文件
配置构建命令
// package.json 
  "scripts": {
    "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
    "build": "rimraf dist && npm run build:client && npm run build:server"
  },
启动应用
// server.js 
// 在服务端将vue实例渲染为字符串
const Vue = require('vue')
const fs = require('fs')
const express = require('express')

// 加载打包资源
const template = fs.readFileSync('./index.template.html', 'utf-8')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')

const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {        // 打包后的启动 createRenderer ——> createBundleRenderer, 并加载打包后的文件
    template,
    clientManifest
})

// 启动服务
const server = express()

// 查找静态资源,处理返回
server.use('/dist/', express.static('./dist'))

server.get('/', (req, res) => {     
    renderer.renderToString({           // 打包中自动创建的实例 
        title: '打包运行',
        meta: `<meta name="description" content="vue ssr">`
    },(err, html) => {
        if (err) {
            console.dir(err, 'err')
            return res.status(500).end('Internal Server Error.')
        }
        // 为html 设置 UTF-8编码
        res.setHeader('Content-Type', 'text/html; charset=utf-8')
        res.end(html)
    })
})

server.listen('8080', () => {
    console.log('server running at port 8080.')
})

注意: 使用 【SSR + 客户端混合】时,浏览器可能会更改一些特殊的HTML结构。如 <table>会自动注入<tbody>。 为能够正确匹配,请确保在模板中写入有效的HTML。

三、构建配置开发模式

基本思路

问题:路由、数据预取、每次构建都要重启服务

开发模式下,需要不断的重新生成打包文件,重新生成 renderer 成为核心操作。

 // package.json
 "scripts": {
    "start": "cross-env NODE_ENV=production node server.js",
    "dev": "node server.js"
  },
  1. 开发模式 ——> 监视打包构建 ——> 重新生成 Renderer 渲染器
  2. 开发模式等待有 renderer渲染器后,调用 render 进行渲染
提取处理模块
  1. 创建 ./build/setup-dev-server.js 定义 setupDevServer()
    1. 监视构建serverBundle——> update() ——> 创建 renderer
    2. 监视构建template——> update() ——> 创建 renderer
      1. 使用 chokidar 插件进行监视,替代 fs.watch()、fs.watchFile()
    3. 监视构建 clientManifest ——> update() ——> 创建 renderer

/* 此时 代码 vue-ssr 调试 没有顺利进行 */

服务端监视打包

webpack ——> watch()

把数据写入内存中

使用 webpack-dev-middleware 插件

热更新

使用 webpack-hot-middleware 插件

四、路由处理

编写通用应用注意事项
  1. 每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染。
    1. 使用工厂函数 createApp() 返回 实例
    2. 服务器上“预取”数据,不支持响应式数据。
  2. 只有 beforeCreate 和 created 会在服务器端渲染(SSR)过程张松被调用。
    1. 避免使用 产生全局副作用的代码,如使用 setInterval 设置 timer.
  3. 如果使用了像 window 或 document 这样的特定平台 API,在Node.js中执行会报错。
    1. axios 可以向服务器和客户端都暴露相同的API。
  4. 自定义指令会在SSR过程中导致错误。
    1. 推荐使用组件
    2. 在创建服务器 renderer时,使用 directives 选项所提供的“服务器端版本”
配置 Vue-Router

使用Vue-Router处理路由

将路由注册到根实例
// app.js 
import Vue from 'vue'
import App from './App.vue'
import { createRouter  } from './router'

// 导出一个工厂函数,用于创建新的应用程序、router、store 实例
export function createApp() {
    const router = createRouter()
    const app = new Vue({
        router,
        render: h => h(App)
    })
    return {
        app,
        router
    }
}
适配服务端入口
服务端server适配
适配客户端入口
管理页面Head页面

使用 vue-meta 插件

五、数据预取

服务端不支持异步获取数据,需要 preload 或 prefetch, 放到 vuex 中。

Logo

前往低代码交流专区

更多推荐