vue 服务端渲染简介和实践

什么是 ssr

ssr , Server Side Render的简称, 服务端渲染. 首先服务端渲染并不神秘, 在 ajax 兴起之前, 所有 web 应用都是服务端渲染, 服务器直接返回 html 文本给浏览器, 用户操作比如在 A 页面注册提交表单, 跳转到B 页面, 服务器需要返回两个页面. 这样的弊端显而易见, 加大了服务器的消耗, 随着 JavaScript 的发展, ajax 技术的出现, 客户端的操作通过请求接口的形式与服务器交互, 服务器不用返回整个页面, 而只是数据. 后来出现了后端模版, 比如 jsp, cshtml

<table>
    <c:forEach var="data" items="${datas}" varStatus="loop">
    <tr>
        <td>${loop.index + 1}</td>
        <td>${data.time}</td>
        <td>${data.msg}</td>
    </tr>
    </c:forEach>
</table>

用户在首次进入页面的时候, 通过服务端渲染给出 html, 用户操作使用 ajax 与服务端交互, 动静混合的形式.

后来随着 JavaScript 的发展, 前端模版和近年SPA 框架的发展, 呈现页面完全静态化, 动态内容交给前端(Javscript), 服务器只提供数据(一般以 json 的形式). 用户看到页面, 大致上需要如下过程(忽略 cdn 等)

1.浏览器加载所有静态资源(html,css,js等)--> 2.js 发起请求获取数据 --> 3.渲染页面 --> 呈现用户

好处是前后端完全分离(开发部署), 各司其职, 同时也节约服务器资源(只有数据交互).
此时用户所获取的 html 只是如下的片段


<!DOCTYPE html><html class=has-full>
<head><meta charset=utf-8>
<title>人事管理系统</title>
<link href=/static/css/app.0c7e1e58d27be30db979adc44f7cd4eb.css rel=stylesheet>
</head>
<body><div id=app></div>
<script type=text/javascript src=/static/js/manifest.ca2797291add890279b8.js></script>
<script type=text/javascript src=/static/js/vendor.ee32e29412ede428634a.js></script>
</body>
</html>

其中2, 3 步骤是最耗费时间的, 因为获取数据受到用户网络, 服务器带宽等条件的显示, 而且可以通过业务数据再次加载一些静态资源. 随着业务的复杂, 打包处理 bundle 逐渐增大, 用户看到页面的时间(首屏), 即内容到达时间(time-to-content)将延长, 降低用户体验, 对电商网站流量转换率影响比较明显.

ssr 所做的事情

借用 react ssr 的两张图说明问题( vue 的 ssr 和 react 同理)

ssr:


image
image

csr:


image

最大的差异是, 服务端直接返回的 渲染完毕html 页面, 获取业务数据, 填充业务组件都在服务端完成, 用户能够更快的看到页面内容, 同时也有利于爬虫抓取(SEO).

但是 ssr 也不是万能的, 需要 node 服务器, 很耗费性能, 需要做好缓存和优化, 相当于空间换时间. 全站 ssr 明显不可取, 现在流行较多的是 首屏 ssr ,甚至 首屏部分 ssr

参考资料

前后端渲染之争

Vue 全站服务器渲染 SSR 实践

Nuxt

有上述可知, ssr 应该有两个代码入口, 服务端和客户端, 通过 webpack 打包之后为别为 server-bundle 和 client-bundle, 页面第一次呈现, 通过 server-bundle , 获取业务数据, 填充数据, 渲染组件, 发送 html 给浏览器, 之后用户操作通过 client-bundle, 依旧是在浏览器范围内.

从零开始配置vue ssr 是比较困难的, 幸好有 nuxt
api.

nuxt预设了 vue 服务端渲染的一些配置, 约定大于配置,
pages 路由, vuex 模块划分

Quick start

vue init nuxt-community/starter-template my-project

# 安装依赖
yarn install
# 开发模式运行
yarn run dev
# build 生成环境
yarn run build
# 运行已 build 的代码
yarn run start

目录结构
layouts, middleware, pages, static, store 目录必须存在

配置文件nuxt.config.js link,
尽可能罗列了 nuxt.config.js可配置项和默认值

module.exports = {
  cache: {},
  css: [
    // 加载一个 node.js 模块
    //  'hover.css/css/hover-min.css',
    //  // 同样加载一个 node.js 模块,不过我们定义所需的预处理器
    //  { src: 'bulma', lang: 'sass' },
    //  // 项目中的 CSS 文件
    //  '~assets/css/main.css',
    //  // 项目中的 Sass 文件
    //  { src: '~assets/css/main.scss', lang: 'scss' } // 指定 scss 而非 sass
  ],

  // 默认 true
  dev: process.env.NODE_ENV !== 'production',

  // 创建环境变量
  env: {},

  // 配置 Nuxt.js 应用生成静态站点的具体方式。
  genetate: {
    dir: '',
    minify: '',
    routes: [],
  },


  /*
    * vue-meta
    * Headers of the page
    */
  head: {
    title: 'ssr-vue',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt.js project' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress bar color
  */
  loading: { color: '#3B8070' },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  },

  performance: {
    gzip: false,
    prefetch: true
  },

  // 引入 Vue.use 的插件
  plugins: [],

  // 默认当前路径
  rootDir: process.cwd(),

  router: {
    base: '',
    mode: 'history',
    linkActiveClass: 'nuxt-link-active',
    scrollBehavior: (to, from, savedPosition) => {
      // savedPosition 只有在 popstate 导航(如按浏览器的返回按钮)时可以获取。
      if (savedPosition) {
        return savedPosition
      } else {
        let position = {}
        // 目标页面子组件少于两个
        if (to.matched.length < 2) {
          // 滚动至页面顶部
          position = { x: 0, y: 0 }
        }
        else if (to.matched.some((r) => r.components.default.options.scrollToTop)) {
          // 如果目标页面子组件中存在配置了scrollToTop为true
          position = { x: 0, y: 0 }
        }
        // 如果目标页面的url有锚点,  则滚动至锚点所在的位置
        if (to.hash) {
          position = { selector: to.hash }
        }
        return position
      }
    },
    // default
    middleware: 'user-agent',
    // 扩展路由
    extendRoutes: () => {},

    // 默认同 rootDir
    srcDir: this.rootDir,

    transition: {
      name: 'page',
      mode: 'out-in'
    },
    watchers: {
      chokidar: {}, // 文件监控
      webpack: {
        aggregateTimeout: 300,
        poll: 1000
      }
    }
  }
}

pages 路由

路由, 约定大于配置, 支持动态, 嵌套, 动态嵌套路由, 过渡效果和中间件,
通过文件夹目录名称, 组件名称, 生成路由配置,
默认的 transitionName 为 page, 可在 assets 中添加全局的过渡效果

路由中间件:

在匹配页面之前执行;
nuxt.config.js --> 执行middleware --> 匹配布局 --> 匹配页面
路由中间件

视图

模版

默认的 html 模版: 应用根目录下的 app.html 文件, 没有改文件, 则采用默认的模版

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

页面

页面就是我们最熟悉的.vue文件, 单文件组件, 但是 nuxt 有一些不同的地方, 混入了 asyncData, fetch, head 三个方法, 还有 指定 layout, transition, scrollToTop, validate, middleware配置项

asyncData 和 fetch都是获取数据的方法, 不同的是, asyncData是请求接口的数据, fetch 是用于填充store 数据, 不会设置组件的数据, 两者都在页面加载之前调用,

代码build 之后有服务端和客户端两个入口, build 之后对应为 client.js 和 server.js,
asyncData,和 fetch 第一次在服务端执行, 第二次切换页面后在浏览器执行

head 方法相关使用方法, 可参考 vue-meta
nuxt.config.js 默认定义了全局的 mota 标签

页面相关 API

异步数据

通过 asyncData 获取异步数据, 第一个参数为上下文对象 context, 推荐使用 promise 或者 async/await

asyncData
context 对象

资源文件

项目中编写 js 文件和普通项目一样. 通过 webpack 处理, 对于一些不需要 webpack loader 处理的静态资源文件, 必须放在项目根目录下的static文件夹中, 项目中直接使用/引用相关资源,

需要 webpack 处理的 静态文件,
可以覆盖 nuxt.config.js 中build 字段中 loaders 中 url-loader 或者 file-loader的 默认配置, 进行自定义设置

loaders 配置

[
  {
    test: /\.(png|jpe?g|gif|svg)$/,
    loader: 'url-loader',
    query: {
      limit: 1000, // 1KO
      name: 'img/[name].[hash:7].[ext]'
    }
  },
  {
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
    loader: 'url-loader',
    query: {
      limit: 1000, // 1 KO
      name: 'fonts/[name].[hash:7].[ext]'
    }
  }
]

npm 模块 & 插件

服务端渲染, node 直接返回 html 给客户端, 所以 npm模块和插件应在 整个应用 实例化之前运行, 并且必须支持 ssr(服务端获取不到 window 等对象), 组件的生命周期只有beforeCreatecreated在client 和 server 均调用. 其余钩子函数只在 client 调用.

node_modules 中安装的模块在组件中可以直接使用

import someModules from 'some-module'

避免多个组件引用同一个模块重复打包的问题, 需要在 nuxt.config.js 中配置 vendor(路径和 plugin一致), 尽量将第三方模块打包至单独的文件中去

module.exports = {
    build: {
        vendor: ['path/to/your/modules'],
    },
    plugins: ['path/to/your/modules']
}

可以区分server 端插件和 client 端plugin, ssr 为 true 则只在服务端使用, 为 false 则反之

 plugins: [{src: 'path/to/your/modules', ssr: false}]

ssr 部署及 pm2 的使用

使用 nuxt 官方模版新建的项目, 可以运行yarn build 命令进行构建, build 模板为项目根路径下的 .nuxt 文件夹, 其中 client.js 为客户端入口, server.js 为服务端入口, 通过命令nuxt start 启动 build 之后的代码.

通过简单命令启动在生成环境并不是好办法, 因此我们需要工具, 推荐pm2;

简单来说, PM2是node编写的, 进程管理工具,可以利用它来简化很多应用管理的繁琐任务,如性能监控、自动重启、负载均衡等。

安装之类的不在赘述, 具体参考文档 pm2

简单示例

在项目根目录新建 start.sh, 内容如下

#! /bin/bash
nuxt build
nuxt start

同样在项目根目录新建pm2.config.js, 内容如下

module.exports = {
  apps: [
    {
      name: 'test',
      script: './start.sh',
      env: {
        NODE_ENV: 'development'
      },
      env_production: {
        NODE_ENV: 'production'
      }
    }
  ]
}

参数设置可参考pm2 文档, 这只是个简单示例, 正式环境需要设置集群模式等
运行

pm2 start /path/to/pm2.config.js

即可开启 ssr 服务, 正式环境需要 nginx 代理到80或者443端口

实践遇坑小计

  1. nuxt.config.js 中 plugins 配置错误则项目在使用引入包时会运行失败, 报错信息显示和插件使用错误一样(比如传参错误), 容易误解.
  2. 引入的组件库必须配置 plugins, 但是有的组件库不支持 ssr.
  3. 未完待续
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 作者:威威(沪江前端开发工程师)本文原创,转载请注明作者及出处。 背景 最近, 产品同学一如往常笑嘻嘻的递来需求文...
    iKcamp阅读 3,247评论 0 33
  • Vue因其简单易懂的API、高效的数据绑定和灵活的组件系统,受到很多前端开发人员的青睐。国内很多公司都在使用vue...
    秋枫惋红阅读 5,103评论 7 15
  • 今日晨读很实用的一招,将所有资料都可电子化,这一点我觉得很妙,想想自己的办公效率还处在原始期,不禁汗颜,如果不是晨...
    阿蜜儿阅读 155评论 0 8
  • 她是让我心疼的一个姑娘。 她有一个哥哥,小的时候总是旷课打架,也总是被打的那一个。他常常全身上下粘着土,...
    唱小晚阅读 326评论 0 2
  • 感赏女儿今天语文课文背诵流利,感赏女儿自觉完成作业,感赏女儿帮我倒水,感赏女儿心情很好,投射自己越来越❤️自己,投...
    吴友友阅读 115评论 0 0