笔者最近在和小伙伴对vue项目进行ssr的升级,本文笔者将根据一个简单拿vue cli构建的客户端渲染的demo一步一步的教你们打造本身的ssr,拙见勿喷哈。javascript
在学习一项新技术的时候咱们首先要了解一下他是什么。这里引用官网的一句话:html
Vue.js 是构建客户端应用程序的框架。默认状况下,能够在浏览器中输出 Vue 组件,进行生成 DOM 和操做DOM。然而,也能够将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上彻底交互的应用程序。
知道是什么后咱们要知道这项技术对咱们现有的项目有什么好处,简单总结一下:vue
这里咱们用vue-cli去简单的作一个vue客户端渲染的demo,具体过程就不作赘述了。java
demo地址: https://github.com/LNoe-lzy/v...
这里咱们根据以前写好的客户端渲染的demo来一步一步的改形成服务端渲染。先甩下demo连接:node
demo地址: https://github.com/LNoe-lzy/v...
先附一张镇文之图,官网的构建流程:webpack
为了不单例的影响,咱们须要在每一个请求都建立一个新的vue的实例,从而避免请求状态的污染,咱们来封装一个createApp的工厂函数:git
import Vue from 'vue' import App from './App' export function createApp () { const app = new Vue({ render: h => h(App) }) return { app } }
跑在服务端的Vue中全部的生命周期钩子函数中,只有 beforeCreate 和 created 会在服务器端渲染过程当中被调用,而其余的钩子在客户端才会被调用,毕竟咱们的服务端是没法执行dom操做的,因此咱们要在路由匹配的组件上定义一个静态函数,这个函数要作的也很简单,就是去dispatch咱们的action从而异步获取数据:github
import { mapActions } from 'vuex' export default { asyncData ({ store }) { return store.dispatch('getNav') }, methods: { ...mapActions([ 'getList' ]) } // ... }
一样为了不单例的影响,咱们也须要用工厂函数封装咱们的router和storeweb
// router export function createRouter () { return new Router({ mode: 'history', routes: [] }) } // store export function createStore () { return new Vuex.Store({ state: {}, actions, mutations }) }
根据构建流程图咱们还须要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的版本哦。
服务端框架咱们采用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__
经过Performance咱们也能够看出在采用了ssr的应用中,咱们的首屏渲染并不依赖于客服端的js文件了,这就大大加快了首屏的渲染速度,毕竟传统的SPA应用时须要拿到客户端js文件后才能够进行虚拟dom的构建以及数据的获取工做才渲染页面的。
不使用vuex其实很头疼,但又有了点灵感,平时咱们在开发项目的时候是如何处理组件间通讯的,一个是vuex,另外一个是EventBus,EventBus就是个Vue的实例啊,数据存这里不也行么?
在此笔者的思路是:建立一个Vue的实例充当仓库,那么咱们能够用这个实例的data来存储咱们的预取数据,而用methods中的方法去作数据的异步获取,这样咱们只须要在须要预取数据的组件中去调用这个方法就能够了。demo很简单,戳这里
还有一个思路是在笔者学习的时候看别人博客学到的:只用了vuex的store和一些支持服务端渲染的api,没有走action、mutation那套,而是将数据手动写入state,为了表示对别人博客的尊重,细节就请转到做者的博客吧,戳这里
本文经过一个简单的客户端渲染demo来一步一步的交你们如何搭建属于本身的ssr程序,文笔拙略还请你们谅解了。
不过学习虽好,可是细节到使用上,你们仍是斟酌是否适合在本身的项目中。
多谢支持!