vueSsr快速采坑

因为最近写了一个小工具放在自己网站上,发现网速较慢的时候,空白显示时间比较长,虽然在初始的时候放置了 loading 效果,不过还是打算优化一下。

经过搜索发现用 ssr 同构来优化这一点比较好

  1. 可以解决 seo 的问题;
  2. 可以更快看到网页内容,优化首屏打开时间,因为无需通过加载 js 来渲染整个 dom 结构了

下面为了方便分享这个过程,所有的内容都是简化过的,不包含路由部分(这部分对照看文档就 OK 了),分享的部分主要包含两部分第一部分就是静态渲染,第二部分包含一个 ajax 的请求。

最终的源码可以点击查看Vue-ssr-demo

目录结构

.
├─ build
│    ├─ webpack.client.js
│    ├─ webpack.config.js
│    └─ webpack.server.js
├─ package.json
├─ server.js
├─ src
│    ├─ App.vue
│    ├─ api
│    │    └─ index.js
│    ├─ app.js
│    ├─ entry-client.js
│    ├─ entry-server.js
│    ├─ index.template.html
│    ├─ store.js
│    └─ utils
│           └─ service
│                  ├─ config.js
│                  └─ index.js
└─ static
       └─ favicon.ico

这里先把最终的项目结构放置出来,为了方便理解,下面讲解一些比较重要的文件和目录。

build 是 webpack 的配置文件,这里没有配置开发环境的代码,如果有需要可以参考官方给出的例子HackerNews Demo为了简洁,webpack 的配置文件就不放了,直接在我上面放出的地址找到build文件夹参考就可以了。

utils > serviceaxios的封装代码,需要注意一点,因为代码运行在服务器和客户端所以选用第三方库的时候最好是两端都支持,axios封装了 node 和浏览器一直的 api,这里就用它作为 ajax 通信工具。

起步

在使用 vueCli 开发项目的时候初始会有一个src/main.js入口文件,它的功能很简单执行一个new Vue然后挂载到#app上,不过这里显然是不行的,因为服务器上会持久运行,直接运行一个单例对象可能会导致污染,所以我们先从入口文件进行改造。

这里定义一个app.js它的作用返回一个通用的函数,这样每次运行的时候都是一个新的对象。

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

export function creatApp() {
  const app = new Vue({
    render: h => h(App),
  })
  return { app }
}

注意,我们并没有在这个 app 对象上执行$mount的操作,因为这里返回的是通用部分,执行$mount操作的时候是在客户端的时候。

之后定义两个文件entry-client.jsentry-server.js文件,分别定义客户端代码和服务器端代码

  • entry-client.js
import { creatApp } from './app'

const { app } = creatApp()
app.$mount('#app')

这里只让它执行挂载步骤就 OK 了

  • entry-server.js
import { creatApp } from './app'

export default context => {
  const { app } = creatApp()
  return app
}

这里简单返回一个 app 对象给服务器。

然后再来看一下index.template.html

<html lang="zh">
  <head>
    {{{meta}}}
    <title>{{title}}</title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

它的作用就是一个模板文件,具体内容请参考官方文档,它会在server.js文件中被我们使用,注意这里不需要定义<div id="app"></div>,取而代之的是必须有一个<!--vue-ssr-outlet-->它的作用就是作为注入的节点。

{{}}{{{}}}含义基本相同区别在于{{{}}}不会转义特殊字符。

App.vue

;<template>
  <div id="app">
    <p>这是一段计数器,初始值为1,后面每秒会累加一次,打开源代码看看渲染是否正确把:{{ count }}</p>
  </div>
</template>
export default {
  name: 'app',
  data() {
    return {
      count: 1,
    }
  },
  mounted() {
    setInterval(() => {
      this.count += 1
    }, 1000)
  },
}

上面结构很简单,就是一个定时器不断累加,不过有两个地方需要注意

  1. id="app"这个 id 是必须的,因为我们在entry-client.js文件中执行app.$mount('#app')实际上就是挂载到了这里

  2. mounted我把定时器的操作写到了mounted生命周期内,是因为在服务器我们要避免一些副作用的代码,举例来说如果我们写在了created中,因为服务器渲染没有销毁的钩子,我们这个定时器会一直执行下去,这样肯定就是错误的。 这里贴一下官方给出的编写通用代码指南,只要记住服务器只有beforeCreatecreated两个钩子即可,还有一些特定平台比如window等谨慎使用

server.js

server.js文件的作用就是讲打包后的 dist 文件读取,之后返回一串 html 的字符串给浏览器

const Renderer = require('vue-server-renderer')
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const statics = require('koa-static')
// 这里读取的`utf-8`不要省略
const template = fs.readFileSync(path.resolve(__dirname, './src/index.template.html'), 'utf-8')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')

const server = new Koa()
const renderer = Renderer.createBundleRenderer(serverBundle, {
  template,
  clientManifest,
  // 这里设置为false,因为我们已经用函数包装了,所以不需要
  runInNewContext: false,
})

server.use(statics('dist', { index: 'xxx.html' }))

server.use(async ctx => {
  const context = {
    title: 'hello Vue Ssr',
    meta: `
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
    `,
  }
  ctx.response.type = 'html'
  const html = await renderer.renderToString(context)
  ctx.body = html
})

server.listen(3000, () => {
  console.log('运行成功: http://localhost:3000/')
})

这里使用了koa作为服务器启动的框架,因为不包含路由所以请求任何的 url 地址都直接返回这个固定的html字符串给浏览器(对应的是server.use(async (ctx) => {}这一段代码)

因为 api 的格式比较固定没有太多要说的,不过还是讲一两个比较容易入坑的地方

  • entry-server.js我们打开entry-server.js文件,返回一个函数
// ...
export default context => {
  // ...
}

里面有一个context的函数参数,实际上这个context对应的正式我们上面server.jscontext对象,这个对象会用作index.template.html文件内部

  • server.use(statics('dist', { index: 'xxx.html' }));

这里我们打包的目录是 dist,如果直接访问 dist 的资源肯定会提示不存在,所以我们需要可以让它访问这个目录,这里用了 koa 的中间件,后面的{ index: 'xxx.html' }必不可少,因为我们打包了一个index.html的文件,而statics默认的 index 会导致跟我们打包的文件冲突,所以在后面任意修改一个不存在的名字就可以了。

执行到一步,我们运行 webpack 打包文件,之后启动server.js打开浏览器就应该可以看到一个被服务器渲染过的页面了。

ajax

上面部分应该还算比较容易理解,下面就来了我们的重头戏ajax

在这之前我们先准备一下要实现 demo 所需要的用到的

yarn add vuex

这里采用了 vuex 做状态管理,事实上这不是必须的(只要用类似的即可),之后再src定义一个store.js文件,它的作用就是真正执行ajax请求,把结果保存在state内,然后在App.vue内通过vuex来读取到请求的数据。

听我这样描述是不是很简单,哈哈,我们先写一个简单的接口,之后请求这个接口返回数据

// server.js
const Koa = require('koa')
const Router = require('koa-router')
const cors = require('koa-cors')

const api = new Koa()
const router = new Router()
router.get('/ancientPoetry', ctx => {
  const ancientPoetry = '古木阴中系短篷,\n杖藜扶我过桥东。\n沾衣欲湿杏花雨,\n吹面不寒杨柳风。'
  ctx.body = {
    status: 200,
    message: '操作成功',
    data: ancientPoetry,
  }
})
api
  .use(cors())
  .use(router.routes())
  .use(router.allowedMethods())

// 接口运行地址
api.listen(7000)

上面用到了两个中间件

yarn add koa-cors koa-router

OK,这样接口部分也完成了,之后请求这个地址就可以了。

下面定义store.js文件

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
// ancientPoetry是api访问的地址
import service, { ancientPoetry } from './utils/service'

Vue.use(Vuex)

export function createStore() {
  return new Vuex.Store({
    state: {
      poetry: '',
    },
    actions: {
      fetchItem({ commit }) {
        return service({
          method: 'get',
          url: ancientPoetry,
        }).then(item => {
          commit('setItem', item)
        })
      },
    },
    mutations: {
      setItem(state, item) {
        Vue.set(state, 'poetry', item)
      },
    },
  })
}

上面我们返回的依然是一个函数,之后我们要把这个函数注入到一些文件内部

app.js
import Vue from 'vue'
import App from './App.vue'
import { createStore } from './store'

Vue.config.productionTip = false

export function creatApp() {
  const store = createStore()
  const app = new Vue({
    asyncData({ store: s }) {
      return s.dispatch('fetchItem')
    },
    store,
    render: h => h(App),
  })
  return { app, store }
}

注意到asyncData这个函数,我们后面会需要用到

entry-server.js
import { creatApp } from './app'

export default async c => {
  const context = c
  const { app, store } = creatApp()
  if (app.$options.asyncData) {
    await app.$options.asyncData({ store })
    // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
    context.state = store.state
  }
  return app
}

因为我们这里就定义了一个App.vue的文件,所以! 我们直接在 app 下通过app.$options来检查asyncData存不存在,关于app.$options的定义官方说是new Vue的一些其它选项,这里你可以通过任意方式获取,但是一定要执行定义的函数,因为它的作用就是来让vuex来请求数据,注入到组件内部的。

之后我们更改contextstate,这里还记得context么,它会同时运行在客户端和浏览器,最初由server.js文件提供

entry-client.js
import { creatApp } from './app'

const { app, store } = creatApp()

// 将信息注入到客户端
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

app.$mount('#app')

这一步就比较简单了,直接把我们获取到的 vuex 数据替换即可store.replaceState执行的就是替换操作

App.vue

下面把异步的数据加上

<template>
  <div id="app">
    <p>这是一段计数器,初始值为1,后面每秒会累加一次,打开源代码看看渲染是否正确把:{{ count }}</p>
    <p>
      下面是一段ajax请求的异步结果
    </p>
    <p class="cs-item">
      <strong>
        {{ item }}
      </strong>
    </p>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      count: 1,
    };
  },
  computed: {
    // 从 store 的 state 对象中的获取 item。
    item() {
      return this.$store.state.poetry;
    },
  },
  mounted() {
    setInterval(() => {
      this.count += 1;
    }, 1000);
  },
};
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
.cs-item {
  white-space: pre-line;
}
</style>

OK,到这一个简单的 ajax 请求页面就出来了

原文链接:juejin.im

上一篇:渐进增强的 Promise
下一篇:多页面+SSR+同构实践小记

相关推荐

  • 项目能快速复用的vue-demo

    前言 今天是个好日子,大家六一快乐; vuecli生成的template还需要配置axios,vuex,element等插件,该项目中将这些常用插件进行了配置; 项目开发中template可...

    2 年前
  • 重学前端学习笔记(二十八)--通过四则运算的解释器快速理解编译原理

    笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系(https://time.geekbang.org/column/i...

    10 个月前
  • 通过 41 个 问答方式快速了解学习 Git

    为了保证的可读性,本文采用意译而非直译。 1. 你最喜欢的 Git 命令是什么 个人比较喜欢 这增加了“补丁模式”的变化,这是一个内置的命令行程序。它遍历了每个更改,并要求确认是否要执行它们。

    5 个月前
  • 这是一个基于 Vue SSR Genesis 框架快速开发的模板例子

    介绍 这是一个基于 Vue SSR Genesis 框架快速开发的模板例子 启动 关于 Genesis 这是一个为 Followme 5.0诞生的Vue SSR框架,也许上线后我们会开源,也许...

    1 个月前
  • 路由快速入门

    路由是 Angular 应用程序的核心,它加载与所请求路由相关联的组件,以及获取特定路由的相关数据。这允许我们通过控制不同的路由,获取不同的数据,从而渲染不同的页面。

    9 天前
  • 论一个前端工程师如何快速学习,成长。准备自己的35岁 【-原创精读】

    clipboard.png(https://img.javascriptcn.com/5581ba67edbaa03b1fdaf0af674fd3aa "clipboard.png") 前端工程师...

    7 个月前
  • 让前端也能快速的合成图片

    (简介)简介: 在业务中,经常遇到各种合成图片的需求,如贴纸的合成,合成文字,添加水印等,因为这些业务经常需要进行各种位置,状态等参数的计算,写起来并不是那么方便。

    2 年前
  • 表单快速入门

    主要内容 第一节 创建最简单的输入框 第二节 添加简单的验证功能 第三节 显示验证失败的错误信息 第四节 创建表单 第五节 ngModelGroup简介 第六节 表单添加验证状态样式 第...

    9 天前
  • 经典计算机网络面试题,快速收藏!

    一、讲一下 HTTP 与 HTTPS 的区别 HTTP 和 HTTPS 的主要区别在于 HTTP 协议传递的是明文数据,而 HTTPS 传递的是加密过的数据,也就是说 HTTPS 更具有安全性。

    4 个月前
  • 纯 JS 实现放大缩小拖拽采坑之旅

    本文首发于政采云前端团队博客:纯 JS 实现放大缩小拖拽踩坑之旅(https://www.zoo.team/article/scaling) 前言 最近团队需要做一个智能客服悬浮窗功能,需要支持...

    4 个月前

官方社区

扫码加入 JavaScript 社区