自定义vue SSR

项目源码地址 : https://github.com/qifutian/learngit/tree/main/vue-ssr

搭建自己的SSR

  1. mkdir vue-ssr
  2. cd vue-ssr
  3. npm init -y
  4. npm i vue vue-server-renderer
  5. 创建server.js
  6. 使用node运行
  7. 服务端会将vue渲染成字符串

server.js

const Vue = require('vue')
const renderer = require('vue-server-renderer')

const app = new({
     
    template:`
       

{ {message}}

`
, data:{ message: "ssr text" } }) renderer.renderToString(app,(err,heml)=>{ if(err) throw err console.log(html) })

结合到Web服务中

目的:是将渲染结果发送给客户端浏览器

  1. 安装web服务端,之前代码
  2. npm i express
  3. 修改文件,通过nodemon运行
  4. nodemon 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:`
           

{ {message}}

`
, data:{ message: "ssr text" } }) renderer.renderToString(app,(err,html)=>{ if(err){ return res.status(500).end("Server ERROR")} res.setHeader('Content-Type','text/html;charset=utf8') // res.end(html) res.end( ` Document ${ html} ` ) }) }) server.listen(3000,()=>{ console.log('server running at port 3000'); })

使用html模板

页面的模板可以存放到一个单独的文件中,对他进行管理和维护
创建index.template.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
  <!--vue-ssr-outlet-->
</body>

</html>

修改server.js

 const Vue = require('vue')
const fs = require('fs')

const renderer = require('vue-server-renderer').createRenderer({
     
    template: fs.readFileSync('./index.template.html','utf-8')
})
const express = require('express')
const server = express()

server.get('/',(req,res)=>{
     
    const app = new Vue({
     
        template:`
           

{ {message}}

`
, data:{ message: "ssr text" } }) renderer.renderToString(app,(err, html)=>{ if(err){ return res.status(500).end("Server ERROR")} res.setHeader('Content-Type','text/html;charset=utf8') res.end(html) }) }) server.listen(3000,()=>{ console.log('server running at port 3000'); })

在模板中使用外部数据

修改renderToString方法

renderer.renderToString(app,{
     
        title:"ssr",
        meta: `
         
        `
    },(err, html)=>{
     
        if(err){
      return res.status(500).end("Server ERROR")}
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.end(html)
    })

Vue SSR 构建配置

基本思路

服务端渲染只是将其处理为纯静态的字符串,将其发送给客户端,对与vue中需要客户端处理的功能未提供,就是对应的事件或者绑定未工作

自定义vue SSR_第1张图片

构建思路

需求
  1. 将生成的字符串提供浏览器解析
  2. 拥有客户端渲染,客户端的动态交互
  3. 组织对应的代码结构
  4. 由webpack对源代码进行打包构建
  5. 使用打包构建的结构通过node启动起来

建议使用官网推荐的结构
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

  1. 建立src文件夹,创建App.vue
  2. 创建app.js,目的是同构应用的通用启动入口
  3. 创建 entry-client.js,目的是创建应用程序,挂载到dom中
  4. 创建 entry-server.js,目的是服务端的入口, 使用 default export 导出函数,并在每次渲染中重复调用此函数

安装依赖

  1. npm i vue vue-server-renderer express cross-env
  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

cross-env 作用 :通过npm scripts设置扩平台环境变量 webpack-cli: webpack 的命令行工具
webpack-merge: webpack 配置信息合并工具 webpack-node-externals: 排除 webpack 中的
Node 模块 rimraf: 基于 Node 封装的一个跨平台 rm -rf 工具
friendly-errors-webpack-plugin: 友好的 webpack 错误提示 Babel 相关工具 vue-loader
vue-template-compiler 处理
.vue 资源 file-loader 处理字体资源
css-loader 处理 CSS资源
url-loader 处理图片资源

构建配置-webpack配置文件

  1. 初始化webpack 打包配置文件,创建build文件夹及对应的打包文件
  2. webpack.base.config.js是公共配置
  3. webpack.client.config.js 是客户端打包配置
  4. webpack.server.config.js 是服务端打包配置

构建配置-配置构建的命令

运行:
  • npm run build:client
  • npm run build:server

可根据package.json进行配置

{
     
  "name": "vue-ssr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "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",
  },
  "dependencies": {
     
    "axios": "^0.19.2",
    "chokidar": "^3.4.0",
    "cross-env": "^7.0.2",
    "express": "^4.17.1",
    "vue": "^2.6.11",
    "vue-meta": "^2.4.0",
    "vue-router": "^3.3.4",
    "vue-server-renderer": "^2.6.11",
    "vuex": "^3.5.1"
  },
  "devDependencies": {
     
    "@babel/core": "^7.10.4",
    "@babel/plugin-transform-runtime": "^7.10.4",
    "@babel/preset-env": "^7.10.4",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.6.0",
    "file-loader": "^6.0.0",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "rimraf": "^3.0.2",
    "url-loader": "^4.1.0",
    "vue-loader": "^15.9.3",
    "vue-template-compiler": "^2.6.11",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-middleware": "^3.7.2",
    "webpack-hot-middleware": "^2.25.0",
    "webpack-merge": "^5.0.9",
    "webpack-node-externals": "^2.5.0"
  }
}

构建配置-启动应用

先进行打包工作,在修改server.js
const Vue = require('vue')
const fs = require('fs')

const serverBundle = require("./dist/vue-ssr-server-bundle.json")
const clientManifest = require("./dist/vue-ssr-client-manifest.json") // 打包需要的资源清单

const template = fs.readFileSync('./index.template.html','utf-8')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle,{
     
    template,
    clientManifest
})
const express = require('express')
const server = express()

server.get('/',(req,res)=>{
     
    // 会找到entry-server.js 找到对应的渲染实例
    renderer.renderToString({
     
        title:"ssr",
        meta: `
         
        `
    },(err, html)=>{
     
        if(err){
      return res.status(500).end("Server ERROR")}
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.end(html)
    })
})

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

使用 node server 运行,浏览器会报错
可以通过node.static处理静态资源,
浏览器的事件正常处理

构建配置-解析渲染流程

问题:
  1. 服务端如何输出内容的流程
  2. 客户端如何加载的流程

服务端流程

  1. 服务端从server.js开始
  2. 从server.get的路由开始
  3. 调用renderer.renderToString方法,将一个vue实例渲染成字符串返回客户端
  4. 渲染的vue实例通过serverBundle加装

客户端加载

  1. 服务端返回打包之后的字符串,会包含对应的引入资源
  2. 通过vue-ser-renderer的createBundleRenderer
  3. vuessrclient-mainfest.json中存放的是对应文件,all是加载的资源,initinitial是将需要资源注入到页面中,async是处理异步资源,modules是原始模块的资源信息

构建开发模式

解决打包问题,通过server启动web服务

实现自动构建,自动重启,自动刷新浏览器等

解决server.js中的renderer是关键内容
开发模式下需要更新,需要根据源代码改变修改打包之后的资源文件
在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",
    "start": "cross-env NODE_ENV=production node server.js",
    "dev": "node server.js"
  },

在server.js中拿到环境变量执行server.js,判断是生产模式还是开发模式

const Vue = require('vue')
const fs = require('fs')
const express = require('express')

let renderer

// 根据环境变量执行对应代码
const isProd = process.env.NODE_ENV === "production"

if(isProd){
     
    const serverBundle = require("./dist/vue-ssr-server-bundle.json")
    const clientManifest = require("./dist/vue-ssr-client-manifest.json") // 打包需要的资源清单
    
    const template = fs.readFileSync('./index.template.html','utf-8')
    renderer = require('vue-server-renderer').createBundleRenderer(serverBundle,{
     
        template,
        clientManifest
    })
} else {
     
    // 开发模式 -> 监视源代码改变,进行打包构建 -> 重新生成 Renderer渲染器
    
}

const server = express()

server.use('/dist',express.static('./dist'))

const render = (req,res)=>{
     
    // 会找到entry-server.js 找到对应的渲染实例
    renderer.renderToString({
     
        title:"ssr",
        meta: `
         
        `
    },(err, html)=>{
     
        if(err){
      return res.status(500).end("Server ERROR")}
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.end(html)
    })
} 
server.get('/', isProd ? render : (req, res) => {
     
    // 等待有了 Renderer 渲染器以后,调用render 进行渲染
    render()
})

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

提取处理模块

server.js中引入 增加setup-setup-dev-server.js
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')

const resolve = file => path.resolve(__dirname, file)

module.exports = (server, callback) => {
     
  let ready
  const onReady = new Promise(r => ready = r)

  // 监视构建 -> 更新 Renderer

  let template
  let serverBundle
  let clientManifest

  const update = () => {
     
    if (template && serverBundle && clientManifest) {
     
      ready()
      callback(serverBundle, template, clientManifest)
    }
  }

  // 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
  const templatePath = path.resolve(__dirname, '../index.template.html')
  template = fs.readFileSync(templatePath, 'utf-8')
  update()
  // fs.watch、fs.watchFile
  chokidar.watch(templatePath).on('change', () => {
     
    template = fs.readFileSync(templatePath, 'utf-8')
    update()
  })

  // 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
  const serverConfig = require('./webpack.server.config')
  const serverCompiler = webpack(serverConfig)
  const serverDevMiddleware = devMiddleware(serverCompiler, {
     
    logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
  })
  serverCompiler.hooks.done.tap('server', () => {
     
    serverBundle = JSON.parse(
      serverDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
    )
    update()
  })

  // 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
  const clientConfig = require('./webpack.client.config')
  clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
  clientConfig.entry.app = [
    'webpack-hot-middleware/client?quiet=true&reload=true', // 和服务端交互处理热更新一个客户端脚本
    clientConfig.entry.app
  ]
  clientConfig.output.filename = '[name].js' // 热更新模式下确保一致的 hash
  const clientCompiler = webpack(clientConfig)
  const clientDevMiddleware = devMiddleware(clientCompiler, {
     
    publicPath: clientConfig.output.publicPath,
    logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
  })
  clientCompiler.hooks.done.tap('client', () => {
     
    clientManifest = JSON.parse(
      clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')
    )
    update()
  })
  server.use(hotMiddleware(clientCompiler, {
     
    log: false // 关闭它本身的日志输出
  }))

  // 重要!!!将 clientDevMiddleware 挂载到 Express 服务中,提供对其内部内存中数据的访问
  server.use(clientDevMiddleware)

  return onReady
}

使用 chokidar 第三方库加载监视的变化
是封装了 fs.watch 和fs.watchFile,更加方便使用

chokidar.watch(templatePath).on('change', () => {
     
     在此处科员监听对应的文件变化
  })

服务端监听打包
通过webpack 创建对应的编译器
重新构建,需要先删除之前生成的,创建新的,放到内存中

热更新
使用 webpack-hot-middleware
在打包之后自动刷新浏览器
使用: npm i --save-dev webpack-hot-middleware
在plugins中引入

 const clientConfig = require('./webpack.client.config')
  clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
  clientConfig.entry.app = [
    'webpack-hot-middleware/client?quiet=true&reload=true', // 和服务端交互处理热更新一个客户端脚本
    clientConfig.entry.app
  ]
  clientConfig.output.filename = '[name].js' // 热更新模式下确保一致的 hash
  const clientCompiler = webpack(clientConfig)
  const clientDevMiddleware = devMiddleware(clientCompiler, {
     
    publicPath: clientConfig.output.publicPath,
    logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
  })
  clientCompiler.hooks.done.tap('client', () => {
     
    clientManifest = JSON.parse(
      clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')
    )
    update()
  })
  server.use(hotMiddleware(clientCompiler, {
     
    log: false // 关闭它本身的日志输出
  }))

编写通用应用的注意项

  1. 服务器上的数据响应
  2. 组件生命周期钩子函数
  3. 访问特定平台的api,例如window和document,服务端可能没有,客户端同样没有对应的node中的fs等
  4. 自定义组件,推荐使用抽象机制,运行在虚拟DOM级别

配置 VueRouter

使用vue-router使用方式和 客户端使用方法基本相同
用法
新建router文件夹,新建index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/pages/Home'

Vue.use(VueRouter)

export const createRouter = () => {
     
  const router = new VueRouter({
     
    mode: 'history', // 兼容前后端
    routes: [
      {
     
        path: '/',
        name: 'home',
        component: Home
      },
      {
     
        path: '/about',
        name: 'about',
        component: () => import('@/pages/About')
      },
      {
     
        path: '/posts',
        name: 'post-list',
        component: () => import('@/pages/Posts')
      },
      {
     
        path: '*',
        name: 'error404',
        component: () => import('@/pages/404')
      }
    ]
  })

  return router
}

适配服务端入口

需要在entry-server.js中实现服务端逻辑 官网实现对应代码
// entry-server.js
import {
      createApp } from './app'

export default async context => {
     
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
  const {
      app, router, store } = createApp()

  const meta = app.$meta()

  // 设置服务器端 router 的位置
  router.push(context.url)

  context.meta = meta

  // 等到 router 将可能的异步组件和钩子函数解析完
  await new Promise(router.onReady.bind(router))

  context.rendered = () => {
     
    // Renderer 会把 context.state 数据对象内联到页面模板中
    // 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
    // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中
    context.state = store.state
  }

  return app
}

服务端适配
修改server.js中的get监听路由

// 服务端路由设置为 *,意味着所有的路由都会进入这里
server.get('*', isProd
  ? render
  : async (req, res) => {
     
    // 等待有了 Renderer 渲染器以后,调用 render 进行渲染
    await onReady
    render(req, res)
  }
)

管理页面的head内容

不同页面定义自己的head部分
Vue ssr指南上有专门的head管理
也可以使用vue-meta插件,使用对应的metaInfo

npm i vue-meta
在对应入口文件注册
Vue.use(VueMeta)

数据预取和状态管理

服务端渲染接口数据
基于vuex创建容器

npm i vuex
创建对应的store文件夹下index.js
use方式引入vuex
vuex中请求成功在将数据保存到客户端,解决数据刷新丢失问题
在entry-server.js中,使用context对象设置rendered将数据填充到客户端
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

export const createStore = () => {
     
  return new Vuex.Store({
     
    state: () => ({
     
      posts: []
    }),

    mutations: {
     
      setPosts (state, data) {
     
        state.posts = data
      }
    },

    actions: {
     
      // 在服务端渲染期间务必让 action 返回一个 Promise
      async getPosts ({
      commit }) {
     
        // return new Promise()
        const {
      data } = await axios.get('https://cnodejs.org/api/v1/topics')
        commit('setPosts', data.data)
      }
    }
  })
}

context.rendered = () => {
     
    // Renderer 会把 context.state 数据对象内联到页面模板中
    // 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
    // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中
    context.state = store.state
  }

你可能感兴趣的:(自动化构建,#,webpack,服务端渲染,vue.js,webpack,node.js)