Step-by-step,打造属于本身的vue ssr

笔者最近在和小伙伴对vue项目进行ssr的升级,本文笔者将根据一个简单拿vue cli构建的客户端渲染的demo一步一步的教你们打造本身的ssr,拙见勿喷哈。javascript

what ? why ?

What ?

在学习一项新技术的时候咱们首先要了解一下他是什么。这里引用官网的一句话:html

Vue.js 是构建客户端应用程序的框架。默认状况下,能够在浏览器中输出 Vue 组件,进行生成 DOM 和操做DOM。然而,也能够将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上彻底交互的应用程序。

Why ?

知道是什么后咱们要知道这项技术对咱们现有的项目有什么好处,简单总结一下:vue

  • 利于SEO
  • 利于首屏渲染,vue-ssr会把拿到的数据渲染成html,不用等待所有的js资源都完成下载才显示咱们的页面。

do ? how to do ?

这里咱们用vue-cli去简单的作一个vue客户端渲染的demo,具体过程就不作赘述了。java

demo地址: https://github.com/LNoe-lzy/v...

这里咱们根据以前写好的客户端渲染的demo来一步一步的改形成服务端渲染。先甩下demo连接:node

demo地址: https://github.com/LNoe-lzy/v...

First step:理解下原理

先附一张镇文之图,官网的构建流程:
构建步骤webpack

这些都是个啥?

  • app.js用来构建咱们的vue实例,这个实例会跑在客户端和服务端;
  • server entry是咱们的服务端entry,用来导出一个函数在每次请求中调用,也作组件匹配和初始化渲染数据的获取。webpack会将其打包成server bundle;
  • client entry是咱们客户端的entry,用来挂载咱们的vue实例到指定的dom元素上。webpack会将其打包成client bundle;

这些都作了啥?

  • 首先咱们的entry-server会获取到当前router匹配到的组件,调用组件上asyncData方法,将数据存到服务端的vuex中,而后服务端vuex中的这些数据传给咱们的context。
  • Node.js服务器经过renderToString将须要首屏渲染的html字符串send道咱们的客户端上,这其中混入了window.__INITIAL_STATE__ 用来存储咱们服务端vuex的数据。
  • 而后entry-client,此时服务端渲染时候拿到的数据写入客户端的vuex中。
  • 最后就是客户端和服务端的组件作diff了,更新状态更新的组件。

Secound step:main.js的改造

为了不单例的影响,咱们须要在每一个请求都建立一个新的vue的实例,从而避免请求状态的污染,咱们来封装一个createApp的工厂函数:git

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

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

Third step:组件的改造

跑在服务端的Vue中全部的生命周期钩子函数中,只有 beforeCreate 和 created 会在服务器端渲染过程当中被调用,而其余的钩子在客户端才会被调用,毕竟咱们的服务端是没法执行dom操做的,因此咱们要在路由匹配的组件上定义一个静态函数,这个函数要作的也很简单,就是去dispatch咱们的action从而异步获取数据:github

import { mapActions } from 'vuex'

export default {
  asyncData ({ store }) {
    return store.dispatch('getNav')
  },
  methods: {
    ...mapActions([
      'getList'
    ])
  }
  // ...
}

Fourth step:router和store的改造

一样为了不单例的影响,咱们也须要用工厂函数封装咱们的router和storeweb

// router
export function createRouter () {
  return new Router({
    mode: 'history',
    routes: []
  })
}

// store
export function createStore () {
  return new Vuex.Store({
    state: {},
    actions,
    mutations
  })
}

Fifth step:两个entry

根据构建流程图咱们还须要webpack去构建两个bundle,服务端根据Server Bundle去作ssr,浏览器根据Client Bundle去混合静态标记。vue-router

为此咱们在src目录下新建两个文件,entry-server.js 和 entry-client.js。前者在每次渲染中须要重复调用,执行服务端的路有匹配和数据预取逻辑。后者负责挂载DOM节点,以及先后端vuex数据状态的同步。

// entry-server.js
import { createApp } from './main'

export default context => {
  // 可能为异步组件,返回一个promise
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()
    const { url } = context
    const { fullPath } = router.resolve(url).route

    if (fullPath !== url) {
      return reject(new Error(`error: ${fullPath}`))
    }
    router.push(url)
    // 须要等到的异步组件和钩子函数解析完
    router.onReady(() => {
      // 获取匹配到的组件
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
        store,
        route: router.currentRoute
      }))).then(() => {
        // 将预取的数据从store中取出放到context中
        context.state = store.state
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}

这里咱们须要注意两点,一个是咱们的数据预取是调用组件的asyncData方法,因此须要Promise.all来保证拿到所有的预渲染数据;另外一点是context.state = store.state,这时候服务端拿到的预渲染数据会封在window.__INITIAL_STATE__中经过node服务器send到客户端。

import Vue from 'vue'
import { createApp } from './main'
const { app, router, store } = createApp()

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

// 也是处理异步组件
router.onReady(() => {
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to)
    const prevMatched = router.getMatchedComponents(from)
    let diffed = false
    // 筛选发生更新的组件
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })

    const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)
    if (!asyncDataHooks.length) {
      return next()
    }

    Promise.all(asyncDataHooks.map(hook => hook({ store, route: to })))
      .then(() => {
        next()
      })
      .catch(next)
  })
  console.log('router ready')
  app.$mount('#app')
})

看到window.__INITIAL_STATE__咱们就能够知道了客户端拿到了预取的数据,而后去存到客户端的vuex中,这也就是你们常常谈论的经过vuex实现先后端的状态共享。

至于vuex是否是必须的,固然不是(尤大issuse有说),题外话,笔者也实现了没有vuex的版本哦。

Sixth step:编写服务端代码

服务端框架咱们采用Express(固然Koa2也是能够的):

const express = require('express')
const fs = require('fs')
const path = require('path')
const {
  createBundleRenderer
} = require('vue-server-renderer')

const app = express()
const resolve = file => path.resolve(__dirname, file)
// 生成服务端渲染函数
const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
  runInNewContext: false,
  template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
  clientManifest: require('./dist/vue-ssr-client-manifest.json'),
  basedir: resolve('./dist')
})

// 引入静态资源
app.use(express.static(path.join(__dirname, 'dist')))
// 分发路由

app.get('*', (req, res) => {
  res.setHeader('Content-Type', 'text/html')

  const handleError = err => {
    if (err.url) {
      res.redirect(err.url)
    } else if (err.code === 404) {
      res.status(404).send('404 | Page Not Found')
    } else {
      // Render Error Page or Redirect
      res.status(500).send('500 | Internal Server Error')
      console.error(`error during render : ${req.url}`)
      console.error(err.stack)
    }
  }

  const context = {
    title: 'Vue SSR demo', // default title
    url: req.url
  }
  renderer.renderToString(context, (err, html) => {
    console.log('render')
    if (err) {
      return handleError(err)
    }
    res.send(html)
  })
})

app.on('error', err => console.log(err))
app.listen(3000, () => {
  console.log(`vue ssr started at localhost:3000`)
})

经过观察localhost咱们能够很清楚的发现,经过服务端send过来的html字符串仅包括咱们根据数据预取渲染出来的dom结构以及服务端混入的window.__INITIAL_STATE__
服务端渲染html

经过Performance咱们也能够看出在采用了ssr的应用中,咱们的首屏渲染并不依赖于客服端的js文件了,这就大大加快了首屏的渲染速度,毕竟传统的SPA应用时须要拿到客户端js文件后才能够进行虚拟dom的构建以及数据的获取工做才渲染页面的。

ssr

不仅是题外话

  • vue-router不是必须的,不用router其实作个vue的preRender就能够了,彻底不必作ssr;
  • vuex不是必须的,vuex是实现咱们客户端和服务端的状态共享的关键,咱们能够不使用vuex,可是咱们得去实现一套数据预取的逻辑;

不使用vuex其实很头疼,但又有了点灵感,平时咱们在开发项目的时候是如何处理组件间通讯的,一个是vuex,另外一个是EventBus,EventBus就是个Vue的实例啊,数据存这里不也行么?

在此笔者的思路是:建立一个Vue的实例充当仓库,那么咱们能够用这个实例的data来存储咱们的预取数据,而用methods中的方法去作数据的异步获取,这样咱们只须要在须要预取数据的组件中去调用这个方法就能够了。demo很简单,戳这里

还有一个思路是在笔者学习的时候看别人博客学到的:只用了vuex的store和一些支持服务端渲染的api,没有走action、mutation那套,而是将数据手动写入state,为了表示对别人博客的尊重,细节就请转到做者的博客吧,戳这里


写在最后

本文经过一个简单的客户端渲染demo来一步一步的交你们如何搭建属于本身的ssr程序,文笔拙略还请你们谅解了。

不过学习虽好,可是细节到使用上,你们仍是斟酌是否适合在本身的项目中。

多谢支持!

相关文章
相关标签/搜索