筆者最近在和小夥伴對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程序,文筆拙略還請你們諒解了。
不過學習雖好,可是細節到使用上,你們仍是斟酌是否適合在本身的項目中。
多謝支持!