Vue + vue-router + express SSR实践

Vue SSR

SSR概念:

https://ssr.vuejs.org/zh/

server side render服务端渲染

服务端渲染解释:

将一个Vue组件在服务器渲染为HTML字符串并发送到浏览器,最后再将这些静态标记“激活”为可交互应用程序的过程称为服务端渲染。

百度蜘蛛爬虫的机制

百度蜘蛛是百度搜索引擎的一个自动化程序,它会不断的访问收集互联网上的网页、文章、视频等,通过抓取链接来收录网站,计算网站的权重和排名。纯html等静态化网站对百度蜘蛛比较友好,且百度蜘蛛几乎不会爬取js动态的网站,如vue/react构建的且经webpack/gulp等构建工具压缩处理过的网站。百度蜘蛛爬取网站是从主站开始爬,一次根据网站暴露的内链依次往深层次爬取。meta的设置,以及网站TDK的优化,网站结构优化,外链,文章原创等同样对SEO有很大作用,但本文主要是从技术层面入手,则主要是针对网站内链的处理以及基于vue等现在技术流做ssr处理。

传统服务端渲染
asp.net php jsp

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LtpY1HE7-1628232174962)(/Users/moira/Library/Application Support/typora-user-images/image-20210805173415868.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-deqfY4lK-1628232174963)(/Users/moira/Library/Application Support/typora-user-images/image-20210805200808286.png)]

浏览器拿到的是全部的dom结构。

优缺点:

  1. 前后端职责不清
  2. 前后端代码杂揉在一起
  3. 项目难以管理和维护

尽管如此,这种渲染方式还是有一些好处:

  1. 客户端能够快速呈现服务器端渲染好的页面,减少白屏时间,这能够提供很好的用户体验
  2. SEO 友好,服务端渲染从服务器发出的html带有页面内容,可提高搜索排名。
CSR

单页应用优秀的用户体验,使其逐渐成为主流,页面内容由JS渲染出来,这种方式称为客户端渲染(Client Side Rendering)。

客户端发起页面请求,服务端把页面(响应的字符串)发送给客户端,客户端从上到下依次解析,解析过程中,发现网络请求,再次向服务器发送网络请求,客服端拿到响应的结果,模板引擎渲染到HTML页面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dsy4qUy8-1628232174964)(/Users/moira/Library/Application Support/typora-user-images/image-20210805192404021.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J2vrrn0r-1628232174965)(/Users/moira/Library/Application Support/typora-user-images/image-20210805200713565.png)]

优点:灵活,真正的前后端分离,方便于前后台各自更新维护。

缺点: 对SEO不友好,增加了HTTP请求的次数,减缓了页面加载速度。

SSR

在后端看来,页面文件其实就是一个“字符串”,所以服务端完全可以在获取到HTML文件的内容之后经过一些处理再返回给客户端,也就说,服务端可以将数据插入到HTML 字符串中之后再返回给客户端。

优点:对 SEO搜索引擎友好,减少了HTTP请求的次数,加速了页面初次渲染的速度。

缺点:不灵活,前后端耦合太深。

在 SPA 模式下,所有的数据请求和 Dom 渲染都在浏览器端完成,所以当我们第一次访问页面的时候很可能会存在“白屏”等待,而服务端渲染所有数据请求和 html内容已在服务端处理完成,浏览器收到的是完整的 html 内容,可以更快的看到渲染内容,在服务端完成数据请求肯定是要比v在浏览器端效率要高的多。

服务端渲染和客户端渲染结合

最好的方案就是服务端渲染和客户端渲染的结合,第一次访问页面是服务端渲染,基于第一次访问后续的交互就是客户端渲染的效果和体验,同时还不影响SEO效果

在这里插入图片描述

实现Vue SSR具体过程

创建工程

vue-cli创建工程即可

安装依赖
vue-server-renderer  express

要确保vue、vue-server-renderer版本一致

启动脚本

创建一个express服务器,将vue ssr集成进来。

./server/index.js

/* 
* SSR
* 思考:vue在nodejs中如何展示?
*/

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

const app = express()
const page = new vue({
    data: { title: '沐沐沐 --- 服务端渲染' },
    template: `<div>
                <h1>{{title}}</h1>
                <div>hello moira !</div>
               </div>`
})

// 将vue实例渲染成html
// 使用vue官方渲染器 vue-server-renderer
// createRenderer()工厂函数,返回一个渲染器实例
// 渲染器实例作用:把vue实例作为参数传给它,可以将vue当前页面的内容生成html
const renderer = require('vue-server-renderer').createRenderer()
// 页面可能写得有问题,在render的过程中可能会发生异常
// 考虑健壮性,使用try catch 
app.get('/', async (req, res) => {
    try {
        // renderToString 异步生成一个html,参数是一个vue实例。 
        // 异步方法,返回一个Promise
        const html = await renderer.renderToString(page)
        res.send(html)
    }
    catch (error) {
        res.status(500).send('服务器内部错误!')
    }

})

app.listen(5200, () => {
    console.log('服务器启动成功!')
})


路由

路由支持仍然使用vue-router

创建路由实例

每次请求的url委托给vue-router处理

./server/example_simple_ssr.js

// 创建一个express实例
const express = require('express')

const app = express()

// 导入vue
const Vue = require('vue')

// 创建渲染器
const { createRenderer } = require('vue-server-renderer')

const renderer = createRenderer()

// 导入路由
const Router = require('vue-router')
Vue.use(Router)


app.get('*', async (req, res) => {
  // 创建一个路由器实例
  const router = new Router({
    routes: [
      { path: '/', component: { template: '<div>Index</div>' } },
      { path: '/detail', component: { template: '<div>detail</div>' } },
    ]
  })


  // 构建渲染页面内容
  const vm = new Vue({
    router,
    data() {
      return {
        name: '沐沐'
      }
    },
    template: `
      <div>
        <router-link to="/">index</router-link>
        <router-link to="/detail">detail</router-link>
        <div>{{name}}</div>
        <router-view></router-view>
      </div>
    `
  })

  try {
    // 路由跳转
    router.push(req.url)

    // 渲染: 得到html字符串
    const html = await renderer.renderToString(vm)
    // 发送回前端
    res.send(html)
  } catch (error) {
    res.status(500).send('服务器内部错误')
  }

})

// 监听端口
app.listen(5200, () => {
    console.log('服务器启动成功!')
})
问题
  • 同构开发
同构开发SSR应用

对于同构开发,我们依然使用webpack打包,我们要解决两个问题:

服务端首屏渲染和客户端激活。

目标是生成一个「服务器 bundle」用于服务端首屏渲染,和一个「客户端bundle」用于客户端激活。

server bundle:
处理前端请求渲染哪个页面,把首屏渲染成html

client bundle:
打包页面相关代码,作用:将静态html激活成前端spa

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FWVSfk5I-1628232174966)(/Users/moira/Library/Application Support/typora-user-images/image-20210805232023652.png)]

代码结构

除了两个不同入口之外,其他结构和之前vue应用完全相同。

app.js 通用入口,用于创建vue实例

entry-client.js # 客户端入口,用于静态内容“激活”

entry-server.js # 服务端入口,用于首屏内容渲染

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5vDng344-1628232174967)(/Users/moira/Library/Application Support/typora-user-images/image-20210805232449158.png)]

app.js

// 服务端入口 
// 核心作用:创建vue实例
import Vue from "vue";
// 根页面
import App from "./App.vue"
// router工厂函数
import createRouter from "./router";

// 每一次用户请求都要全新Vue实例
export default function createApp() {
    const router = createRouter()
    const app = new Vue({
        router,
        // 渲染函数
        // 问题: 是否需要 .$mount()挂载 
        render: h => h(App)
    });
    // 要返回app和router实例 router实例之后有用
    return { app, router }
}

entry-client.js

// 客户端入口只需创建vue实例并执行挂载,这一步称为激活。
import createApp from "./app";

const { app, router } = createApp()
// 准备就绪后挂载
router.onReady(() => {
    // 挂载
    app.$mount('#app')
})

entry-server.js

// 服务端入口
// 核心作用:渲染首屏
// 处理前端请求渲染哪个页面,把首屏渲染成html

// 得到具体vue实例
import createApp from "./app";

// 导出
// 还是返回一个工厂函数 将来会被服务器调用 因为只有服务器知道用户请求的是什么 在这里处理首屏并且返回对应实例
export default context => {
    // 这里返回一个Promise,确保路由或组件准备就绪
    return new Promise((resolve, reject) => {
        const { app, router } = createApp();
        // 跳转到首屏的地址
        router.push(context.url)
        // 路由就绪,返回结果
        router.onReady(()=>{
            // 返回当前实例 app
            resolve(app);
        }, reject)
    })
}
webpack 打包

安装依赖

npm install webpack-node-externals lodash.merge -D

具体配置 vue.config.js

// webpack 两个插件分别负责打包客户端和服务端
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
// 两个依赖
const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");

// *** 根据传入环境变量决定入口文件和相应配置项
// 环境变量 WEBPACK_TARGET :决定入口是客户端还是服务端
// node ? server : client        
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
    css: {
        extract: false
    },
    outputDir: './dist/' + target,
    configureWebpack: () => ({
        // *** 将 entry 指向应用程序的 server / client 文件
        entry: `./src/entry-${target}.js`,
        // 对 bundle renderer 提供 source map 支持
        devtool: 'source-map',
        // *** target设置为node使webpack以Node适用的方式处理动态导入,
        // 并且还会在编译Vue组件时告知`vue-loader`输出面向服务器代码。
        target: TARGET_NODE ? "node" : "web",
        // 是否模拟node全局变量
        node: TARGET_NODE ? undefined : false,
        output: {
            // 此处使用Node风格导出模块
            libraryTarget: TARGET_NODE ? "commonjs2" : undefined
        },
        // https://webpack.js.org/configuration/externals/#function
        // https://github.com/liady/webpack-node-externals
        // 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的打包文件。
        // 优化代码
        // externals: TARGET_NODE
        //     ? nodeExternals({
        //         // 不要外置化webpack需要处理的依赖模块。
        //         // 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
        //         // 还应该将修改`global`(例如polyfill)的依赖模块列入白名单
        //         whitelist: [/\.css$/]
        //     })
        //     : undefined,
        // optimization: {
        //     splitChunks: undefined
        // },

        // *** 这是将服务器的整个输出构建为单个 JSON 文件的插件。
        // 服务端默认文件名为 `vue-ssr-server-bundle.json`
        // 客户端默认文件名为 `vue-ssr-client-manifest.json`。
        plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
    }),
    // 优化代码 不太重要
    chainWebpack: config => {
        // cli4项目添加
        if (TARGET_NODE) {
            config.optimization.delete('splitChunks')
        }

        config.module
            .rule("vue")
            .use("vue-loader")
            .tap(options => {
                merge(options, {
                    optimizeSSR: false
                });
            });
    }
};

打包脚本

"build": "npm run build:server & npm run build:client",
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"

宿主文件

./public/index.template.html

<!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>vue ssr moira</title>  </head>  <body>    <!--vue-ssr-outlet-->  </body></html>
服务器启动文件

修改服务器启动文件,现在需要处理所有路由,./server/index_new.js

const express = require('express')const Vue = require('vue')const fs = require('fs') //文件系统const path = require('path')// 创建express实例和vue实例const app = express()// const renderer = require('vue-server-renderer').createRenderer()const { createBundleRenderer } = require('vue-server-renderer')// 服务端的包const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')// 客户端清单const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')// 参数1:服务端的包// 将来生成的页面要把清单解析出来附加到页面中// 最后生成的html页面会有一些js需要附加,客户端清单告诉了js附加到哪里const renderer = createBundleRenderer(serverBundle, {    // 上下文    runInNewContext: false,    // 这里需要用path转换为绝对地址    template: fs.readFileSync(path.join(__dirname, '../public/index.template.html'), 'utf-8'), //宿主模板文件,使用文件系统的方式导入,使用fs     clientManifest})// 中间件处理静态文件请求// 需要把dist client js暴露给客户端,前端是可以访问的// express.static()  静态文件私服app.use(express.static('../dist/client', { index: false }))// 路由请求// * 将来前端请求什么我都不关心,所有路由都让renderer接管// 路由的权限由vue接管// 路由的处理交给vueapp.get('*', async (req, res) => {    try {        // 创建上下文        const context = {            url: req.url,            title: 'ssr moira title'        }        // renderer此时是BundleRenderer,接收上下文,而不是vue实例 约定        const html = await renderer.renderToString(context);        res.send(html)    } catch (error) {        // 返回一个状态码,500,服务器内部错误        res.status(500).send('服务器内部错误')    }});app.listen(5200, () => {    console.log('渲染服务器启动成功!')})

打包后的server端:

json文件,能够告诉渲染器必要的渲染信息。打包信息。

打包后的client端:

完整。index.html 宿主文件。js文件 动态从前端加载。

client目录需要成为服务器的静态目录。

vue-ssr-client-manifest.json 前端挂载

defer 不能阻塞程序 延迟加载

SSR优缺点
优点

与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。

    请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。在这里,同步是关键。如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。

  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间(time-to-content) 与转化率直接相关」的应用程序而言,服务器端渲染 (SSR) 至关重要。

缺点

使用服务器端渲染 (SSR) 时还需要有一些权衡之处:

  • 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
  • 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
  • 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。这主要取决于内容到达时间 (time-to-content) 对应用程序的重要程度。例如,如果你正在构建一个内部仪表盘,初始加载时的额外几百毫秒并不重要,这种情况下去使用服务器端渲染 (SSR) 将是一个小题大作之举。然而,内容到达时间 (time-to-content) 要求是绝对关键的指标,在这种情况下,服务器端渲染 (SSR) 可以帮助你实现最佳的初始加载性能。

整合Vuex

数据预取{ 组件中的数据预取. 服务端数据预取. 客户端数据预取处理}

优化:

  1. 页面缓存
  2. pm2守护node进程配置,用它来做自动重启、性能监控以及负载均衡。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值