zoukankan      html  css  js  c++  java
  • 改造@vue/cli项目为服务端渲染-ServerSideRender

    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        sourceMaptrue
    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    runInNewContextfalse,
    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的目的。边角细节的配置就需要各位再慢慢摸索了。


  • 相关阅读:
    2020中国最好大学排名.py(亲测有效)
    手机号码归属地的自动查询.py(亲测有效)
    网络图片的爬取和存储.py(亲测有效)
    百度360搜索关键词提交.py(亲测有效)
    亚马逊商品页面爬取(使用headers字段).py(亲测有效)
    淘宝商品页面的爬取.py(亲测有效)
    爬取网页的通用代码框架.py(亲测有效)
    匿名函数function前面的! ~等符号作用小解
    ng之自定义指令
    用nodejs搭建一个简单的服务监听程序
  • 原文地址:https://www.cnblogs.com/qinyuandong/p/13650066.html
Copyright © 2011-2022 走看看