#关于Vue SSR的一点看法

2,405 阅读8分钟

1.SSR是什么?

SSR,意为 Server Side Rendering(服务端渲染),目的是为了解决单页面应用的 SEO 和首屏渲染速度慢的问题,对于一般网站影响不大,但是对于需要做SEO的网站是致命的,搜索引擎无法抓取页面相关内容,也就是用户搜不到此网站的相关信息。

2.为什么要用SSR?

客户端渲染需要:加载html=>解析html=>加载js=>解析js=>生成dom节点=>插入html文档,所以很慢; 服务端渲染就是将首屏的html结构构建好直接返回,客户端只需要:加载html=>解析html。所以首屏打开速度大大提高,但同时对服务器的压力也比客户端渲染要大。

3.关于实现SSR的几种方式

目前Vue SSR的实现有两种实现,一种是基于官方Vue SSR指南文档的官方方案,一种是vue.js通用应用框架--NUXT。 官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中,也会对Vue SSR有更加深入的了解。 而NUXT提供了平滑的开箱即用的体验,它建立在同等的Vue技术栈之上,但抽象出很多模板,并提供了一些额外的功能,例如静态站点生成。通过NUXT可以根据约定的规则,快速的实现Vue SSR。

(1)nuxt

官方是这么介绍自己的: Nuxt.js 是一个基于 Vue 的通用应用框架。 通过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI渲染。

我们的目标是创建一个灵活的应用框架,你可以基于它初始化新项目的基础结构代码,或者在已有 Node.js 项目中使用 Nuxt.js。 Nuxt.js 预设了利用 Vue 开发服务端渲染的应用所需要的各种配置。

除此之外,我们还提供了一种命令叫:nuxt generate,为基于 Vue 的应用提供生成对应的静态站点的功能。

我们相信这个命令所提供的功能,是向开发集成各种微服务(miscroservices)的 Web 应用迈开的新一步。 作为框架,Nuxt.js 为 客户端/服务端 这种典型的应用架构模式提供了许多有用的特性,例如异步数据加载、中间件支持、布局支持等。

Nuxt.js是使用 Webpack 和 Node.js 进行封装的基于Vue的SSR框架,使用它,你可以不需要自己搭建一套 SSR 程序,而是通过其约定好的文件结构和API就可以实现一个首屏渲染的 Web 应用。

之所以叫 Nuxt.js 也是因为受到了 Next.js 的启发。 Nuxt.js 是一个 Node 程序,就像上面说的,我们是要把 Vue 跑在服务端,所以必须使用 Node 环境。

我们对 Nuxt.js 应用的访问,实际上是在访问这个 Node.js 程序的路由,程序输出首屏渲染内容 + 用以重新渲染的 SPA 的脚本代码,而路由是由 Nuxt.js 约定好的 pages 文件夹生成的。

所以,整体上,Nuxt.js 通过各个文件夹和配置文件的约束来管理我们的程序,而又不失扩展性,其有自己的插件机制。

  • .nuxt : Nuxt自动生成,临时的用于编辑的文件,build
  • assets:Webpack 编译的各类资源,// 用于组织未编译的静态资源入LESS、SASS 或 JavaScript
  • components:各组件,用于你自己管理公共组件或非公共组件 ,比如滚动组件,日历组件,分页组件
  • layouts:宿主布局页面模板组件,用于你可以把不同的页面指定使用不同的布局,不可更改。
  • middleware:中间件,首屏渲染和路由跳转前均执行对应中间件,可以返回promise或直接next(很实用!)
  • pages:各页面组件,用于生成对应路由,支持嵌套,支持动态路由,存放写的页面,我们主要的工作区域
  • plugins:插件,SPA中用的各类第三方组件和一些node模块,JavaScript插件放的地方
  • static :/ 用于存放静态资源文件,比如图片
  • store:内置了vuex,可以直接返回数据模块或返回一个自建vuex根对象,具体要翻文档,用于组织应用的Vuex 状态管理
  • .editorconfig : // 开发工具格式配置
  • .eslintrc.js : // ESLint的配置文件,用于检查代码格式
  • .gitignore : // 配置git不上传的文件
  • nuxt.config.json : // 用于组织Nuxt.js应用的个性化配置,已覆盖默认配置
  • package-lock.json : // npm自动生成,用于帮助package的统一性设置的,yarn也有相同的操作
  • package-lock.json : // npm自动生成,用于帮助package的统一性设置的,yarn也有相同的操作
  • package.json : // npm包管理配置文件
  • 其他:你可以自定义文件夹和别名映射,文档都有提及,这里有配置代码

nuxt.config.js对程序的扩展管理可大概分为以下类:

  • build:主要对应 Webpack 中的各配置项,可以对默认的 Webpack 配置进行扩展,如这里代码
  • cache:主要对应内置的组件缓存模块lru-cache的配置对象,有默认值,可选关闭
  • css:对应我们在SPA随处引用样式文件的require语句
  • dev:用于自定义配置环境变量,对应之前webpack.config.js相关文件中的变量语句
  • env:同上息息相关
  • generate:对generate命令执行时的行为做一些定制
  • head:对应vue-meta插件的全局配置,vue-meta用于VUE/SSR程序的文档元信息的管理
  • loading:用于定制化Nuxt.js内置的进度条组件
  • performance:用于配置Node.js服务器性能上的配置
  • plugins:用于管理和应用对应plugins文件夹中的插件
  • rootdir:用于设置 Nuxt.js 应用的根目录(这俩api有很大合并的意义)
  • srcdir:用于设置 Nuxt.js 应用的源码目录(这俩api有很大合并的意义)
  • router:用于对vue-router的扩展和定制,其中还包括了中间件的配置,但并不完美
  • transition:用于定制Nuxt.js内置的页面切换过渡效果的默认属性值
  • watchers:用于定制Nuxt.js内置的文件监听模块chokidar和 -Webpack 的相关配置项

同时,Nuxt.js 支持以generate命令将程序直接构建为静态 html ,就像上面说的,可以作为静态资源直接输出。 打包 npm run generate

开发中遇到的坑点

一开始用generate打包上线,发现首屏打开速度超慢,在测服上面大概需要10s+才能打开首页,但打开其他页面都是秒开,然后开始找原先,压缩vendor.js和app.js之后速度大约块一两s,此时首屏打开任很慢,仍然需要8s+才能打开页面,慢点的情况下可能需要30s+,实在不能忍,打开浏览器debug模式,发现首次加载的时候加载了所有的页面的js,这样首页打开速度慢.其他页面秒开就解释的通了,如下图,

原因找到了,接下来就是怎么解决了,于是我翻看官方文档,尝试了以下方法:

在 nuxt.config.js 里面设置:

module.exports = {
render: {

resourceHints: false,
},
......
......
}

nuxt官方文档上是这样说明的

nuxt的坑还有很多,但是还是值得尝试的

(2)vue-server-renderer

让我们先创建一个项目并安装一些依赖项:

1. mkdir vue-ssr
2. cd vue-ssr
3. npm init
4 .npm i vue@next vue-server-renderer --save

package.json:

{
 "name": "vue-ssr",
 "version": "1.0.0",
 "dependencies": {
   "vue": "^2.5.17",
   "vue-server-renderer": "^2.5.17"
 }
}

注意:vue版本号要和vue-server-renderer版本号保持一致

webpack.base.config.js


module.exports = {
  module: {
    rules: [{
      test: /\.vue$/,
      loader: 'vue-loader',
    },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  plugins: [],
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
}

webpack.server.conf.js

const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
 
const path = require('path');
const root= path.resolve(__dirname, '..');
 
module.exports = merge(baseConfig, {
  // 将 entry 指向应用程序的 server entry 文件
  entry: path.join(root, 'entry/entry-server.js'),
 
  // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
  // 并且还会在编译 Vue 组件时,
  // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
  target: 'node',
 
  // 对 bundle renderer 提供 source map 支持
  devtool: 'source-map',
 
  // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
  output: {
    libraryTarget: 'commonjs2',
    path: path.join(root, 'dist'),
    filename: 'bundle.server.js'
  },
 
  // https://webpack.js.org/configuration/externals/#function
  // https://github.com/liady/webpack-node-externals
  // 外置化应用程序依赖模块。可以使服务器构建速度更快,
  // 并生成较小的 bundle 文件。
  externals: nodeExternals({
    // 不要外置化 webpack 需要处理的依赖模块。
    // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    whitelist: /\.css$/
  }),
 
  // 这是将服务器的整个输出
  // 构建为单个 JSON 文件的插件。
  // 默认文件名为 `vue-ssr-server-bundle.json`
  plugins: [
    new VueSSRServerPlugin()
  ]
})

webpack.client.conf.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
 
const path = require('path');
const root= path.resolve(__dirname, '..');
module.exports = merge(baseConfig, {
  entry: path.join(root, 'entry/entry-client.js'),
  output: {
    path: path.join(root, 'dist'),
    filename: 'bundle.client.js'
  },
  plugins: [
    // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
    // 以便可以在之后正确注入异步 chunk。
    // 这也为你的 应用程序/vendor 代码提供了更好的缓存。
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      filename: 'manifest.js',
      minChunks: Infinity
    }),
    // 此插件在输出目录中
    // 生成 `vue-ssr-client-manifest.json`。
    new VueSSRClientPlugin()
  ]
})

entry-server.js

/* entry-server.js */
import { createApp } from '../src/app'
 
export default context => {
  return new Promise((resolve, reject) => {
    const {app, router} = createApp()
    // 更改路由
    router.push(context.url)
    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = app.$router.getMatchedComponents()
      // 匹配不到的路由,执行 reject 函数,并返回 404
      if (!matchedComponents.length) { return reject({code: 404}) }
      // Promise 应该 resolve 应用程序实例,以便它可以渲染
      resolve(app)
    }, reject)
  })
 
}

entry-client.js

import { createApp } from '../src/app'
// 客户端特定引导逻辑……
const {app} = createApp()
// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')

server.js

const path = require('path')
const express = require('express')
const app = express()
const { createBundleRenderer } = require('vue-server-renderer')
 
// 创建renderer
const template = require('fs').readFileSync('./index.ssr.html', 'utf-8')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json') // 这个可以动态将生成的js文件渲染到html模版中
const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false, // 推荐
  template: template,
  clientManifest: clientManifest
})
 
app.use(express.static(path.join(__dirname, 'dist')))
// 响应路由请求
app.get('*', (req, res) => {
  const context = { url: req.url }
  // 创建vue实例,传入请求路由信息
  renderer.renderToString(context, (err, html) => {
    if (err) {
      return res.state(500).end('运行时错误')
    }
    res.send(html)
  })
})
 
// 服务器监听地址
app.listen(8099, () => {
  console.log('服务器已启动!')
})

基本上到这个地方关键性构建以及服务模块代码补充完成后,一个简单的基于vue-server-renderer例子就可以运行起来了。