VUE SEO方案二 - SSR服务端渲染

在上一章中,我们分享了预渲染的方案来解决SEO问题,个人还是很中意此方案的,既简单又能解决大部分问题。但是也有着一定的缺陷,所以我们继续来看下一个方案--服务端渲染。

1.概述

官方文档

服务端渲染的配置相比预渲染就复杂多了,要做到同构,还要保证服务端和客服端的组件状态一致,我们需要对整个项目进行改造。大部分的内容官方文档中都说明的比较清楚,这里就不重复讲述了,需要各位花费一些时间照着文档一步步改造项目。

本人一开始也是这样照着文档做的,但是改造到最后,发现文档一点不走心啊,本人用官方的@vue/cli创建的项目,一步步走到最后根本跑不起来,要么服务端各种报错,要么客服端各种渲染失败。

所以这边主要讲述在按照文档改造之后还要做的一些配置和其他问题。若有不实之处请大家指正。

2.简单原理

原理很简单,同一套代码,根据服务端和客户端的需求不同,分为两个入口,分别打包出两套逻辑一样的代码包。分别在服务器与客户端运行。

本例中,服务端使用node的express框架搭建。

3.开始配置

首先,需要保证服务端同样安装和开发环境同样的node_modules,因为服务端打包,并没有将所有的第三方包与组件打包到一起,也没有那个必要。在服务器端安装好就行了,服务端会自行调用,否则服务端会报找不到第三方插件的错误,如:

1Error: Cannot find module 'vuex'

服务端渲染的核心插件vue-server-renderer,安装:

1npm i -D vue-server-renderer 
2npm i -g cross-env

全局安装cross-env使得我们可以在package.json中添加服务端入口打包命令

1"scripts": {
2    "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"
3}

这样我们可以将参数WEBPACK_TARGET=node代入打包进程中,用以识别打包目标,也可以将客户端打包与此命令使用&&连接,一起执行。

打包配置大致如下

1// vue.config.js
2const path = require('path')
3const resolve = dir => path.join(__dirname, dir)
4const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
5const target = TARGET_NODE ? 'server' : 'client'
6const nodeExternals = require('webpack-node-externals')
7const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
8const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
9
10module.exports = {
11    // 将server-bundle.json与模板一起放在服务器,静态资源则打包到cdn中
12    outputDir: TARGET_NODE? resolve('dist') : resolve('../../www/statics/m/first-aid/'),
13    ...
14    chainWebpack: config => {
15        ...
16        // 两个入口
17        config.entry('app')
18            .clear()
19            .add(`./src/entry-${target}.js`)
20        config.target(TARGET_NODE ? 'node' : 'web')
21        config.output
22            .libraryTarget(TARGET_NODE ? 'commonjs2' : undefined)
23        if(TARGET_NODE){
24            config.externals(nodeExternals({
25                allowlist: [/\.css$/]
26            }))
27            config.optimization
28                .splitChunks(false)
29            config.module
30                .rule('vue')
31                .use('vue-loader')
32                .tap(options => {
33                    options.optimizeSSR = false
34                    return options
35                })
36        }
37        config.plugin('vue-server-renderer')
38            .use(TARGET_NODE ? VueSSRServerPlugin : VueSSRClientPlugin)
39        ...
40    },
41    css: {
42        sourceMap: true
43    }
44    ...
45}

其中与官方文档不同的几处:

  1. nodeExternals配置的whitelist属性是旧版的,新版要用allowlist
  2. 我们需要关闭vue-loader的optimizeSSR属性,否则你将遇到下面这样的错误,具体原因可查询 vue-loader文档 的相关部分


  3. 服务端打包的时候,你可能也会遇到这样的错误:

1(node:18696) UnhandledPromiseRejectionWarning: Error: Server-side bundle should have one single entry file.
2Avoid using CommonsChunkPlugin in the server config.

这时还需要关闭config.optimization.splitChunks,看来服务端渲染的时候对分包不是很友好,不过这也没有必要,所有的包都在服务器本地,当然不需要分包来优化下载。

  1. 关于vue-server-renderer/client-plugin插件还有一个bug, 查看gitHub issues,需要设置css: {sourceMap: true},否则服务端将会报以下错误导致不能渲染成功。

4. 服务器配置

1// server.js
2const express = require("express")
3const path = require("path")
4const resolve = dir => path.join(__dirname, dir)
5const fs = require("fs")
6const jsdom = require('jsdom')
7const { JSDOM } = jsdom
8
9const { createBundleRenderer } = require('vue-server-renderer')
10const serverBundle = require(resolve('dist/vue-ssr-server-bundle.json'))
11const clientManifest = require('../../www/statics/m/first-aid/vue-ssr-client-manifest.json')
12const renderer = createBundleRenderer(serverBundle, {
13    runInNewContext: false,
14    template: fs.readFileSync(resolve('template.html'), 'utf-8'),
15    clientManifest
16})
17
18const app = express()
19// 静态资源
20app.use(express.static(resolve('../../www')))
21
22app.use((req, res) => {
23    const context = { url: req.url }
24    const resourceLoader = new jsdom.ResourceLoader({
25        userAgent: req.headers['user-agent'],
26    });
27    const dom = new JSDOM('', {
28        url: req.protocol+'://'+req.hostname,
29        resources: resourceLoader
30    });
31    global.window = dom.window
32    global.document = window.document
33    global.navigator = window.navigator
34    window.nodeis = true
35    window.scrollTo = (x, y) => {
36        document.documentElement.scrollTop = y;
37    }
38    renderer.renderToString(context, (err, html) => {
39        if (err) {
40            console.log(err)
41            if (err.code === 404) res.status(404).end('Page not found')
42            else res.status(500).end('Internal Server Error')
43        } else res.end(html)
44    })
45})
46
47app.listen(80)

这边与官方文档差别不大,但是使用jsdom插件解决了不存在document全局变量的问题。当项目中用到像window这样的顶层对象时,服务器因为不存在此变量报错document is not defined错误,导致渲染失败

安装jsdom插件,使用请求来源客服端的UA创建虚拟DOM,从而模拟出顶级对象下的各属性与方法

5.最后

经过官方文档的改造,再通过这些重新配置后,一个服务端渲染的@vue/cli项目总算是运行正常了,再通过与vue-meta的配合,设置返回页面的title与description等,就达到我们解决SEO的目的。边角细节的配置就需要各位再慢慢摸索了。



改造@vue/cli项目为服务端渲染-ServerSideRender的更多相关文章

  1. 使用 Vue 2.0 实现服务端渲染的 HackerNews

    Vue 2.0 支持服务端渲染 (SSR),并且是流式的,可以做组件级的缓存,这使得极速渲染成为可能.同时, 和 2.0 也都能够配合 SSR 提供同构路由和客户端 state hydration.v ...

  2. 基于vue-cli项目添加服务端渲染

    两个示例的git地址: 1. 我的环境 2. 方式一:使用prerender-spa-plugin插件获得SSR的效果. 2.1 说明 2.2 初始化 1 vue init webpack vue-p ...

  3. 使用 PHP 来做 Vue.js 的 SSR 服务端渲染

    对于客户端应用来说,服务端渲染是一个热门话题.然而不幸的是,这并不是一件容易的事,尤其是对于不用 Node.js 环境开发的人来说. 我发布了两个库让 PHP 从服务端渲染成为可能.spatie/se ...

  4. 实例PK(Vue服务端渲染 VS Vue浏览器端渲染)

    Vue 2.0 开始支持服务端渲染的功能,所以本文章也是基于vue 2.0以上版本.网上对于服务端渲染的资料还是比较少,最经典的莫过于Vue作者尤雨溪大神的 vue-hacker-news.本人在公司 ...

  5. Vue服务端渲染和Vue浏览器端渲染的性能对比

    Vue 2.0 开始支持服务端渲染的功能,所以本文章也是基于vue 2.0以上版本.网上对于服务端渲染的资料还是比较少,最经典的莫过于Vue作者尤雨溪大神的 vue-hacker-news.本人在公司 ...

  6. Vue服务端渲染 VS Vue浏览器端渲染)

    Vue 2.0 开始支持服务端渲染的功能,所以本文章也是基于vue 2.0以上版本.网上对于服务端渲染的资料还是比较少,最经典的莫过于Vue作者尤雨溪大神的 vue-hacker-news.本人在公司 ...

  7. Vue.js 服务端渲染业务入门实践

    作者:威威(沪江前端开发工程师) 本文原创,转载请注明作者及出处. 背景 最近, 产品同学一如往常笑嘻嘻的递来需求文档, 纵使内心万般拒绝, 身体倒是很诚实. 接过需求,好在需求不复杂, 简单构思 后 ...

  8. Egg + Vue 服务端渲染工程化实现

    在实现 egg + vue 服务端渲染工程化实现之前,我们先来看看前面两篇关于Webpack构建和Egg的文章: 在 Webpack工程化解决方案easywebpack 文章中我们提到了基于 Vue ...

  9. Vue(服务端渲染)

    一.前言 1.服务端渲染图解                                                 2.简介服务端渲染                             ...

随机推荐

  1. SQL学习日记

    目录 SQL学习日记 1. 常见的数据库对象 2. DDL 定义语句 3. DML 操作语句 4. DQL 查询语句 5. DCL 控制语句 SQL学习日记 1. 常见的数据库对象 对象名 关键字 描 ...

  2. huawei 华为 ubuntu mysql 访问不了的原因

    标签:服务器   ins   查看   tar   安全组   service 一. 首先将3306端口添加至安全组, 确保端口没有被封掉 二. 开启服务器上的mysql 权限, 步骤如下 1.mys ...

  3. 模块 序列化模块:json pickle

    模块:一个模块就是一个包含了Python定义和声明的文件,文件名就是模块名字加上.py的后缀 模块的形象: 内置模块:安装Python解释器的时候一起安装上的 第三方模块(扩展模块):需要自己安装 自 ...

  4. RabbitMQ Go客户端教程3——发布/订阅

    本文翻译自RabbitMQ官网的Go语言客户端系列教程,本文首发于我的个人博客:liwenzhou.com,教程共分为六篇,本文是第三篇--发布/订阅. 这些教程涵盖了使用RabbitMQ创建消息传递 ...

  5. 搭建nuget服务器(二):制作nuget包

    生成nuget包可以使用nuget.exe或者下载nuget package explorer工具 nuget package explorer 下载地址:https://github.com/NuG ...

  6. Java并发机制(7)--线程池ThreadPoolExecutor的使用

    Java并发编程:线程池的使用整理自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3932921.html 1.什么是线程池,为什么要使用线程池: 1.1. ...

  7. springDataRedis忽略实体指定的属性

    如果是通过 RedisRepository定义的实体,可能存在想要忽略的属性,那么,就可以 使用 org.springframework.data.annotation.Transient 注解,就可 ...

  8. java线程池源码分析

    我们在关闭线程池的时候会使用shutdown()和shutdownNow(),那么问题来了: 这两个方法又什么区别呢? 他们背后的原理是什么呢? 线程池中线程超过了coresize后会怎么操作呢? 为 ...

  9. redis支持哪些数据类型?redis命令大全

    一.redis支持的数据类型 1)String 常用命令:set/get/decr/incr/mget等: 应用场景:String是最常用的一种数据类型,普通的key/value存储都可以归为此类: ...

  10. Flask-Migrate使用教程

    功能:flask-migrate是flask的一个扩展模块,主要是扩展数据库表结构的. 项目准备:一个干净的Flask项目,下载连接地址: https://pan.baidu.com/s/1WqdIN ...