Vue项目SSR改造实战
我們先看“療效”,你可以打開我的博客u3xyz.com,通過查看源代碼來看SSR直出效果。我的博客已經快上線一年了,但不吹不黑,訪問量非常地小,我也一直在想辦法提升訪問量(包括在sf寫文章,哈哈)。當然,在PC端,搜索引擎一直都是一個重要的流量來源。這里就不得不提到SEO。下圖是我的博客以前在百度的快照:
細心的朋友會發現,這個快照非常簡單,簡單到幾乎什么都沒有。這也是沒辦法的事,博客是基于Vue的SPA頁面,整個項目本來就是一個“空架子”,這個快照從博客2月份上線以來就一直是上面的樣子,直到最近上線SSR。搜索引擎蜘蛛每次來抓取你的網站都是一個樣子,慢慢得,它也就不會來了,相應的,網站的權重,排名肯定不會好。到目前為此,我的博客不用網址進行搜索都搜不到。在上線了SSR后,再加上一些SEO優化,百度快照終于更新了:
為什么要做SSR
文章開始基本已經回答了為什么要做SSR這個問題,當然,還有另一個原因是SSR概念現在在前端非?;?#xff0c;無奈在實際項目中沒有機會,也只有拿博客來練手了。下面將詳細介紹本博客項目SSR全過程。
SSR改造實戰
總的來說SSR改造還是相當容易的。推薦在動手之前,先了解官方文檔和官方Vue SSR Demo,這會讓我們事半功倍。
1. 構建改造
上圖是Vue官方的SSR原理介紹圖片。從這張圖片,我們可以知道:我們需要通過Webpack打包生成兩份bundle文件:
- Client Bundle,給瀏覽器用。和純Vue前端項目Bundle類似
- Server Bundle,供服務端SSR使用,一個json文件
不管你項目先前是什么樣子,是否是使用vue-cli生成的。都會有這個構建改造過程。在構建改造這里會用到 vue-server-renderer 庫,這里要注意的是 vue-server-renderer 版本要與Vue版本一樣。下圖是我的構建文件目錄:
- util.js 提供一些公共方法
- webpack.base.js是公共的配置
- webpack.client.js 是生成Client Bundle的配置。核心配置如下:
- webpack.server.js 是生成Server Bundle的配置,核心配置如下:
2. 代碼改造
2.1 必須使用VueRouter, Vuex。ajax庫建議使用axios
可能你的項目沒有使用VueRouter或Vuex。但遺憾的是,Vue-SSR必須基于 Vue + VueRouter + Vuex。Vuex官方沒有提,但其實文檔和Demo都是基于Vuex。我的博客以前也沒有用Vuex,但經過一翻折騰后,還是乖乖加上了Vuex。另外,因為代碼要能同時在瀏覽器和Node.js環境中運行,所以ajax庫建議使用axios這樣的跨平臺庫。
2.2 兩個打包入口(entry),重構app, store, router, 為每個對象增加工廠方法createXXX
每個用戶通過瀏覽器訪問Vue頁面時,都是一個全新的上下文,但在服務端,應用啟動后就一直運行著,處理每個用戶請求的都是在同一個應用上下文中。為了不串數據,需要為每次SSR請求,創建全新的app, store, router。
上圖是我的項目文件目錄。
- app.js, 通用的啟動Vue應用代碼
- App.vue,Vue應用根組件
- entry.client.js,瀏覽器環境入口
- entry.server.js,服務器環境入口
- index.html,html模板
再看一下具體實現的核心代碼:
// 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' // 把當VueRouter狀態同步到Vuex中// createApp工廠方法 export function createApp (ssrContext) {let router = createRouter() // 創建全新router實例let store = createStore() // 創建全新store實例// 同步路由狀態到store中sync(store, router)// 創建Vue應用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的狀態用它替換 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 = falseconst activated = matched.filter((c, i) => {return diffed || (diffed = (prevMatched[i] !== c))})// 組件數據通過執行asyncData方法獲取const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)if (!asyncDataHooks.length) {return next()}// 要注意asyncData方法要返回promise,asyncData調用的vuex action也必須返回promisePromise.all(asyncDataHooks.map(hook => hook({ store, route: to }))).then(() => {next()}).catch(next)})// 將Vue實例掛載到dom中,完成瀏覽器端應用啟動app.$mount('#app') }) // entry.server.js import { createApp } from './app'export default context => {return new Promise((resolve, reject) => {const { app, router, store } = createApp(context)// 設置路由router.push(context.url)router.onReady(() => {const matchedComponents = router.getMatchedComponents()if (!matchedComponents.length) {return reject({ code: 404 })}// 執行asyncData方法,預拉取數據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' // 線上環境關閉store檢查}) } // createRouterimport Vue from 'vue' import Router from 'vue-router' Vue.use(Router)// createRouter工廠方法 export function createRouter () {return new Router({mode: 'history', // 注意這里要使用history模式,因為hash不會發送到服務端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. 重構組件獲取數據方式
關于狀態管理,要嚴格遵守Redux思想。建議把應用所有狀態都存于store中,組件使用時再mapState下來,狀態更改嚴格使用action的方式。另一個要提一點的是,action要返回promise。這樣我們就可以使用asyncData方法獲取組件數據了
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實現 export default {asyncData ({ store }) {return store.dispatch('getArticleList', 1)} }3. SSR服務器實現
在完成構建和代碼改造后,如果一切順利。我們能得到下面的打包文件:
這時,我們可以開始實現SSR服務端代碼了。下面是我博客SSR實現(基于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請求,前端資源請求不能轉到這里來。這里可以通過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)})}) })// 錯誤處理 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. 服務器部署
服務器部署,跟你的項目架構有關。比如我的博客項目在服務端有2個后端服務,一個數據庫服務,nginx用于請求轉發:
5. 遇到的問題及解決辦法
加載不到組件的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.所以對webpack.server.js不要對配置CommonsChunkPlugin,也不要設置output.chunkFilename
代碼高亮codeMirror使用到navigator對象,只能在瀏覽器環境運行把執行邏輯放到mounted回調中。實現不行,就封裝一個異步組件,把組件的初始化放到mounted中:
mounted () {let paragraph = require('./paragraph.vue')Vue.component('paragraph', paragraph)new Vue().$mount('#paragraph') }, 串數據dispatch的action沒有返回promise,保證返回promise即可
路由跳轉路由跳轉使用router方法或<router-link />標簽,這兩種方式能自適應瀏覽器端和服務端,不要使用a標簽
小結
本文主要記錄了我的博客u3xyz.comSSR過程:
- 構建webpack改造
- 代碼改造
- server端SSR實現
- 上線部署
最后希望文章能對大家有些許幫助!
愿文地址:Vue項目SSR改造實戰
總結
以上是生活随笔為你收集整理的Vue项目SSR改造实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TokuDB在生产环境的应用场景(zab
- 下一篇: Android鬼点子 CirclePro