Vue项目SSR改造实战
我們先看“療效”,你可以打開(kāi)我的博客u3xyz.com,通過(guò)查看源代碼來(lái)看SSR直出效果。我的博客已經(jīng)快上線一年了,但不吹不黑,訪問(wèn)量非常地小,我也一直在想辦法提升訪問(wèn)量(包括在sf寫(xiě)文章,哈哈)。當(dāng)然,在PC端,搜索引擎一直都是一個(gè)重要的流量來(lái)源。這里就不得不提到SEO。下圖是我的博客以前在百度的快照:
細(xì)心的朋友會(huì)發(fā)現(xiàn),這個(gè)快照非常簡(jiǎn)單,簡(jiǎn)單到幾乎什么都沒(méi)有。這也是沒(méi)辦法的事,博客是基于Vue的SPA頁(yè)面,整個(gè)項(xiàng)目本來(lái)就是一個(gè)“空架子”,這個(gè)快照從博客2月份上線以來(lái)就一直是上面的樣子,直到最近上線SSR。搜索引擎蜘蛛每次來(lái)抓取你的網(wǎng)站都是一個(gè)樣子,慢慢得,它也就不會(huì)來(lái)了,相應(yīng)的,網(wǎng)站的權(quán)重,排名肯定不會(huì)好。到目前為此,我的博客不用網(wǎng)址進(jìn)行搜索都搜不到。在上線了SSR后,再加上一些SEO優(yōu)化,百度快照終于更新了:
為什么要做SSR
文章開(kāi)始基本已經(jīng)回答了為什么要做SSR這個(gè)問(wèn)題,當(dāng)然,還有另一個(gè)原因是SSR概念現(xiàn)在在前端非常火,無(wú)奈在實(shí)際項(xiàng)目中沒(méi)有機(jī)會(huì),也只有拿博客來(lái)練手了。下面將詳細(xì)介紹本博客項(xiàng)目SSR全過(guò)程。
SSR改造實(shí)戰(zhàn)
總的來(lái)說(shuō)SSR改造還是相當(dāng)容易的。推薦在動(dòng)手之前,先了解官方文檔和官方Vue SSR Demo,這會(huì)讓我們事半功倍。
1. 構(gòu)建改造
上圖是Vue官方的SSR原理介紹圖片。從這張圖片,我們可以知道:我們需要通過(guò)Webpack打包生成兩份bundle文件:
- Client Bundle,給瀏覽器用。和純Vue前端項(xiàng)目Bundle類(lèi)似
- Server Bundle,供服務(wù)端SSR使用,一個(gè)json文件
不管你項(xiàng)目先前是什么樣子,是否是使用vue-cli生成的。都會(huì)有這個(gè)構(gòu)建改造過(guò)程。在構(gòu)建改造這里會(huì)用到 vue-server-renderer 庫(kù),這里要注意的是 vue-server-renderer 版本要與Vue版本一樣。下圖是我的構(gòu)建文件目錄:
- util.js 提供一些公共方法
- webpack.base.js是公共的配置
- webpack.client.js 是生成Client Bundle的配置。核心配置如下:
- webpack.server.js 是生成Server Bundle的配置,核心配置如下:
2. 代碼改造
2.1 必須使用VueRouter, Vuex。ajax庫(kù)建議使用axios
可能你的項(xiàng)目沒(méi)有使用VueRouter或Vuex。但遺憾的是,Vue-SSR必須基于 Vue + VueRouter + Vuex。Vuex官方?jīng)]有提,但其實(shí)文檔和Demo都是基于Vuex。我的博客以前也沒(méi)有用Vuex,但經(jīng)過(guò)一翻折騰后,還是乖乖加上了Vuex。另外,因?yàn)榇a要能同時(shí)在瀏覽器和Node.js環(huán)境中運(yùn)行,所以ajax庫(kù)建議使用axios這樣的跨平臺(tái)庫(kù)。
2.2 兩個(gè)打包入口(entry),重構(gòu)app, store, router, 為每個(gè)對(duì)象增加工廠方法createXXX
每個(gè)用戶(hù)通過(guò)瀏覽器訪問(wèn)Vue頁(yè)面時(shí),都是一個(gè)全新的上下文,但在服務(wù)端,應(yīng)用啟動(dòng)后就一直運(yùn)行著,處理每個(gè)用戶(hù)請(qǐng)求的都是在同一個(gè)應(yīng)用上下文中。為了不串?dāng)?shù)據(jù),需要為每次SSR請(qǐng)求,創(chuàng)建全新的app, store, router。
上圖是我的項(xiàng)目文件目錄。
- app.js, 通用的啟動(dòng)Vue應(yīng)用代碼
- App.vue,Vue應(yīng)用根組件
- entry.client.js,瀏覽器環(huán)境入口
- entry.server.js,服務(wù)器環(huán)境入口
- index.html,html模板
再看一下具體實(shí)現(xiàn)的核心代碼:
// app.jsimport Vue from 'vue' import App from './App.vue' // 根組件 import {createRouter} from './routers/index' import {createStore} from './vuex/store' import {sync} from 'vuex-router-sync' // 把當(dāng)VueRouter狀態(tài)同步到Vuex中// createApp工廠方法 export function createApp (ssrContext) {let router = createRouter() // 創(chuàng)建全新router實(shí)例let store = createStore() // 創(chuàng)建全新store實(shí)例// 同步路由狀態(tài)到store中sync(store, router)// 創(chuàng)建Vue應(yīng)用const app = new Vue({router,store,ssrContext,render: h => h(App)})return {app, router, store} } // entry.client.js import Vue from 'vue' import { createApp } from './app'const { app, router, store } = createApp()// 如果有__INITIAL_STATE__變量,則將store的狀態(tài)用它替換 if (window.__INITIAL_STATE__) {store.replaceState(window.__INITIAL_STATE__) }router.onReady(() => {// 通過(guò)路由勾子,執(zhí)行拉取數(shù)據(jù)邏輯router.beforeResolve((to, from, next) => {// 找到增量組件,拉取數(shù)據(jù) const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) let diffed = falseconst activated = matched.filter((c, i) => {return diffed || (diffed = (prevMatched[i] !== c))})// 組件數(shù)據(jù)通過(guò)執(zhí)行asyncData方法獲取const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)if (!asyncDataHooks.length) {return next()}// 要注意asyncData方法要返回promise,asyncData調(diào)用的vuex action也必須返回promisePromise.all(asyncDataHooks.map(hook => hook({ store, route: to }))).then(() => {next()}).catch(next)})// 將Vue實(shí)例掛載到dom中,完成瀏覽器端應(yīng)用啟動(dòng)app.$mount('#app') }) // entry.server.js import { createApp } from './app'export default context => {return new Promise((resolve, reject) => {const { app, router, store } = createApp(context)// 設(shè)置路由router.push(context.url)router.onReady(() => {const matchedComponents = router.getMatchedComponents()if (!matchedComponents.length) {return reject({ code: 404 })}// 執(zhí)行asyncData方法,預(yù)拉取數(shù)據(jù)Promise.all(matchedComponents.map(Component => {if (Component.asyncData) {return Component.asyncData({store: store,route: router.currentRoute})}})).then(() => {// 將store的快照掛到ssr上下文上context.state = store.stateresolve(app)}).catch(reject)}, reject)}) } // createStoreimport Vue from 'vue' import Vuex from 'vuex' // ...Vue.use(Vuex)// createStore工廠方法 export function createStore () {return new Vuex.Store({// rootstatestate: {appName: 'appName',title: 'home'},modules: {// ...},strict: process.env.NODE_ENV !== 'production' // 線上環(huán)境關(guān)閉store檢查}) } // createRouterimport Vue from 'vue' import Router from 'vue-router' Vue.use(Router)// createRouter工廠方法 export function createRouter () {return new Router({mode: 'history', // 注意這里要使用history模式,因?yàn)閔ash不會(huì)發(fā)送到服務(wù)端fallback: false,routes: [{path: '/index',name: 'index',component: () => System.import('./index/index.vue') // 代碼分片},{path: '/detail/:aid',name: 'detail',component: () => System.import('./detail/detail.vue')},// ...{path: '/',redirect: '/index'}]}) }3. 重構(gòu)組件獲取數(shù)據(jù)方式
關(guān)于狀態(tài)管理,要嚴(yán)格遵守Redux思想。建議把應(yīng)用所有狀態(tài)都存于store中,組件使用時(shí)再mapState下來(lái),狀態(tài)更改嚴(yán)格使用action的方式。另一個(gè)要提一點(diǎn)的是,action要返回promise。這樣我們就可以使用asyncData方法獲取組件數(shù)據(jù)了
const actions = {getArticleList ({state, commit}, curPageNum) {commit(FETCH_ARTICLE_LIST, curPageNum)// action 要返回promisereturn apis.getArticleList({data: {size: state.pagi.itemsPerPage,page: curPageNum}}).then((res) => {// ...})} }// 組件asyncData實(shí)現(xiàn) export default {asyncData ({ store }) {return store.dispatch('getArticleList', 1)} }3. SSR服務(wù)器實(shí)現(xiàn)
在完成構(gòu)建和代碼改造后,如果一切順利。我們能得到下面的打包文件:
這時(shí),我們可以開(kāi)始實(shí)現(xiàn)SSR服務(wù)端代碼了。下面是我博客SSR實(shí)現(xiàn)(基于Koa)
// server.js const Koa = require('koa') const path = require('path') const logger = require('./logger') const server = new Koa() const { createBundleRenderer } = require('vue-server-renderer') const templateHtml = require('fs').readFileSync(path.resolve(__dirname, './index.template.html'), 'utf-8')let distPath = './dist'const renderer = createBundleRenderer(require(`${distPath}/vue-ssr-server-bundle.json`), { runInNewContext: false,template: templateHtml, clientManifest: require(`${distPath}/vue-ssr-client-manifest.json`) })server.use(function * (next) {let ctx = thisconst context = { url: ctx.req.url, pageTitle: 'default-title' }// cgi請(qǐng)求,前端資源請(qǐng)求不能轉(zhuǎn)到這里來(lái)。這里可以通過(guò)nginx做if (/\.\w+$/.test(context.url)) {return yield next}// 注意這里也必須返回promise return new Promise((resolve, reject) => {renderer.renderToString(context, function (err, html) {if (err) {logger.error(`[error][ssr-error]: ` + err.stack)return reject(err)}ctx.status = 200ctx.type = 'text/html; charset=utf-8'ctx.body = htmlresolve(html)})}) })// 錯(cuò)誤處理 server.on('error', function (err) {logger.error('[error][server-error]: ' + err.stack) })let port = 80server.listen(port, () => {logger.info(`[info]: server is deploy on port: ${port}`) })4. 服務(wù)器部署
服務(wù)器部署,跟你的項(xiàng)目架構(gòu)有關(guān)。比如我的博客項(xiàng)目在服務(wù)端有2個(gè)后端服務(wù),一個(gè)數(shù)據(jù)庫(kù)服務(wù),nginx用于請(qǐng)求轉(zhuǎn)發(fā):
5. 遇到的問(wèn)題及解決辦法
加載不到組件的JS文件 [vue-router] Failed to resolve async component default: Error: Cannot find module 'js\main1.js' [vue-router] uncaught error during route navigation:解決辦法:
去掉webpack配置中的output.chunkFilename: getFileName('js/main[name]-$hash.js')
if you are using CommonsChunkPlugin, make sure to use it only in the client config because the server bundle requires a single entry chunk.所以對(duì)webpack.server.js不要對(duì)配置CommonsChunkPlugin,也不要設(shè)置output.chunkFilename
代碼高亮codeMirror使用到navigator對(duì)象,只能在瀏覽器環(huán)境運(yùn)行把執(zhí)行邏輯放到mounted回調(diào)中。實(shí)現(xiàn)不行,就封裝一個(gè)異步組件,把組件的初始化放到mounted中:
mounted () {let paragraph = require('./paragraph.vue')Vue.component('paragraph', paragraph)new Vue().$mount('#paragraph') }, 串?dāng)?shù)據(jù)dispatch的action沒(méi)有返回promise,保證返回promise即可
路由跳轉(zhuǎn)路由跳轉(zhuǎn)使用router方法或<router-link />標(biāo)簽,這兩種方式能自適應(yīng)瀏覽器端和服務(wù)端,不要使用a標(biāo)簽
小結(jié)
本文主要記錄了我的博客u3xyz.comSSR過(guò)程:
- 構(gòu)建webpack改造
- 代碼改造
- server端SSR實(shí)現(xiàn)
- 上線部署
最后希望文章能對(duì)大家有些許幫助!
愿文地址:Vue項(xiàng)目SSR改造實(shí)戰(zhàn)
總結(jié)
以上是生活随笔為你收集整理的Vue项目SSR改造实战的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: TokuDB在生产环境的应用场景(zab
- 下一篇: Android鬼点子 CirclePro