vue服務(wù)端渲染的實(shí)例代碼
一、什么是服務(wù)端渲染
客戶端請求服務(wù)器,服務(wù)器根據(jù)請求地址獲得匹配的組件,在調(diào)用匹配到的組件返回Promise (官方是asyncData方法)來將需要的數(shù)據(jù)拿到。最后再通過window.__initial_state=data將其寫入網(wǎng)頁,最后將服務(wù)端渲染好的網(wǎng)頁返回回去。接下來客戶端將用新的store狀態(tài)把原來的store狀態(tài)替換掉,保證客戶端和服務(wù)端的數(shù)據(jù)同步。遇到?jīng)]被服務(wù)端渲染的組件,再去發(fā)異步請求拿數(shù)據(jù)。
服務(wù)端渲染的環(huán)境搭建
這是vue官網(wǎng)的服務(wù)端渲染的示意圖,ssr有兩個入口文件,分別是客戶端的入后文件和服務(wù)端的入口文件,webpack通過兩個入口文件分別打包成給服務(wù)端用的server bundle和給客戶端用的client bundle.當(dāng)服務(wù)器接收到了來自客戶端的請求之后,會創(chuàng)建一個渲染器bundleRenderer,這個bundleRenderer會讀取上面生成的server bundle文件,并且執(zhí)行它的代碼, 然后發(fā)送一個生成好的html到瀏覽器,等到客戶端加載了client bundle之后,會和服務(wù)端生成的DOM進(jìn)行Hydration(判斷這個DOM和自己即將生成的DOM是否相同,如果相同就將客戶端的vue實(shí)例掛載到這個DOM上)
實(shí)現(xiàn)步驟:
1、創(chuàng)建vue實(shí)例(main.js)
importVuefrom'vue' importAppfrom'./App.vue' importiViewfrom'iview'; import{createStore}from'./store' import{createRouter}from'./router' import{sync}from'vuex-router-sync' Vue.use(iView); export functioncreateApp() { conststore = createStore() constrouter = createRouter() sync(store,router) constapp =newVue({ router, store, render: h => h(App) }) return{app,router,store} }
因?yàn)橐龇?wù)端渲染,所以這里不需要再用el去掛載,現(xiàn)將app、router、store導(dǎo)出
2、服務(wù)端入口文件(entry-server.js)
import{ createApp }from'./main' constisDev = process.env.NODE_ENV !=='production' const{ app,router,store } = createApp() constgetAllAsyncData=function(component){ letstores = [] functionloopComponent(component) { if(typeofcomponent.asyncData !=='undefined') { for(letaofcomponent.asyncData({store,route: router.currentRoute})) { stores.push(a) } } if(typeofcomponent.components !=='undefined') { for(letcincomponent.components){ loopComponent(component.components[c]) } } } loopComponent(component) returnstores } export defaultcontext => { return newPromise((resolve,reject) => { consts = isDev && Date.now() const{url} = context constfullPath = router.resolve(url).route.fullPath if(fullPath !== url) { reject({url: fullPath }) } router.push(url) router.onReady(() => { constmatchedComponents = router.getMatchedComponents() if(!matchedComponents.length) { reject({code:404}) } letallAsyncData = getAllAsyncData(matchedComponents[0]) Promise.all(allAsyncData).then(() => { isDev && console.log(`data pre-fetch:${Date.now() - s}ms`) context.state = store.state resolve(app) }).catch(reject) },reject) }) }
這個文件的主要工作是接受從服務(wù)端傳遞過來的context參數(shù),context包含當(dāng)前頁面的url,用getMatchedComponents方法獲取當(dāng)前url下的組件,返回一個數(shù)組,遍歷這個數(shù)組中的組件,如果組件有asyncData鉤子函數(shù),則傳遞store獲取數(shù)據(jù),最后返回一個promise對象。
store.state的作用是將服務(wù)端獲取到的數(shù)據(jù)掛載到context對象上,后面在server.js文件里會把這些數(shù)據(jù)直接發(fā)送到瀏覽器端與客戶端的vue實(shí)例進(jìn)行數(shù)據(jù)(狀態(tài))同步。
3、客戶端入口文件(entry-client.js)
importVuefrom'vue' import'es6-promise/auto' import{ createApp }from'./main' importProgressBarfrom'./components/ProgressBar.vue' // global progress bar constbar = Vue.prototype.$bar =newVue(ProgressBar).$mount() document.body.appendChild(bar.$el) Vue.mixin({ beforeRouteUpdate(to,from,next) { const{ asyncData } =this.$options if(asyncData) { Promise.all(asyncData({ store:this.$store, route: to })).then(next).catch(next) }else{ next() } } }) const{ app,router,store } = createApp() if(window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } router.onReady(() => { router.beforeResolve((to,from,next) => { constmatched = router.getMatchedComponents(to) constprevMatched = router.getMatchedComponents(from) letdiffed =false constactivated = matched.filter((c,i) => { returndiffed || (diffed = (prevMatched[i] !== c)) }) constasyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) if(!asyncDataHooks.length) { returnnext() } bar.start() Promise.all(asyncDataHooks.map(hook => hook({ store,route: to }))) .then(() => { bar.finish() next() }) .catch(next) }) app.$mount('#app') }) if('https:'=== location.protocol && navigator.serviceWorker) { navigator.serviceWorker.register('/service-worker.js') }
if(window.INITIAL_STATE) { store.replaceState(window.INITIAL_STATE) }
這句的作用是如果服務(wù)端的vuex數(shù)據(jù)發(fā)生改變,就將客戶端的數(shù)據(jù)替換掉,保證客戶端和服務(wù)端的數(shù)據(jù)同步
Service Worker主要用于攔截并修改訪問和資源請求,細(xì)粒度地緩存資源。它運(yùn)行瀏覽器在后臺,運(yùn)行環(huán)境與普通頁面腳本不同,所以不能直接參與頁面交互。出于安全考慮,service worker只能運(yùn)行在HTTPS上,防止被人從中攻擊。
4、創(chuàng)建服務(wù)端渲染器(server.js)
constfs = require('fs') constpath = require('path') constLRU = require('lru-cache') constexpress = require('express') constcompression = require('compression') constresolve= file => path.resolve(__dirname,file) const{ createBundleRenderer } = require('vue-server-renderer') constisProd = process.env.NODE_ENV ==='production'|| process.env.NODE_ENV ==='beta' constuseMicroCache = process.env.MICRO_CACHE !=='false' constserverInfo = `express/${require('express/package.json').version}`+ `vue-server-renderer/${require('vue-server-renderer/package.json').version}` constapp = express() consttemplate = fs.readFileSync(resolve('./src/index.template.html'),'utf-8') functioncreateRenderer(bundle,options) { returncreateBundleRenderer(bundle,Object.assign(options,{ template, cache: LRU({ max:1000, maxAge:1000*60*15 }), basedir: resolve('./dist'), runInNewContext:false })) } letrenderer letreadyPromise if(isProd) { constbundle = require('./dist/vue-ssr-server-bundle.json') constclientManifest = require('./dist/vue-ssr-client-manifest.json') renderer = createRenderer(bundle,{ clientManifest }) }else{ readyPromise = require('./build/setup-dev-server')(app,(bundle,options) => { renderer = createRenderer(bundle,options) }) } constserve= (path,cache) => express.static(resolve(path),{ maxAge: cache && isProd ?1000*60*60*24*30:0 }) app.use(compression({threshold:0})) app.use('/dist',serve('./dist',true)) app.use('/static',serve('./static',true)) app.use('/service-worker.js',serve('./dist/service-worker.js')) constmicroCache = LRU({ max:100, maxAge:1000 }) constisCacheable= req => useMicroCache functionrender(req,res) { consts = Date.now() res.setHeader("Content-Type","text/html") res.setHeader("Server",serverInfo) consthandleError= err => { if(err.url) { res.redirect(err.url) }else if(err.code ===404) { res.status(404).end('404 | Page Not Found') }else{ // Render Error Page or Redirect res.status(500).end('500 | Internal Server Error') console.error(`error during render :${req.url}`) console.error(err.stack) } } constcacheable = isCacheable(req) if(cacheable) { consthit = microCache.get(req.url) if(hit) { if(!isProd) { console.log(`cache hit!`) } returnres.end(hit) } } constcontext = { title:'Vue DB',// default title url: req.url } renderer.renderToString(context,(err,html) => { if(err) { returnhandleError(err) } res.end(html) if(cacheable) { microCache.set(req.url,html) } if(!isProd) { console.log(`whole request:${Date.now() - s}ms`) } }) } app.get('*',isProd ? render : (req,res) => { readyPromise.then(() => render(req,res)) }) constport = process.env.PORT ||8888 app.listen(port,() => { console.log(`server started at localhost:${port}`) })
5、客戶端api文件create-api-client.js
/** * Created by lin on 2017/8/25. */ import axios from 'axios'; let api; axios.defaults.baseURL = process.env.API_URL; axios.defaults.timeout = 10000; axios.interceptors.response.use((res) => { if (res.status >= 200 && res.status < 300) { return res; } return Promise.reject(res); }, (error) => { return Promise.reject({message: '網(wǎng)絡(luò)異常,請刷新重試', err: error}); }); if (process.__API__) { api = process.__API__; } else { api = { get: function(url) { return new Promise((resolve, reject) => { axios.get(url).then(res => { resolve(res); }).catch((error) => { reject(error); }); }); }, post: function(target, options = {}) { return new Promise((resolve, reject) => { axios.post(target, options).then(res => { resolve(res); }).catch((error) => { reject(error); }); }); } }; } export default api;
6、服務(wù)端api文件create-api-server.js
/** * Created by lin on 2017/8/25. */ import axios from 'axios'; let cook = process.__COOKIE__ || ''; let api; axios.defaults.baseURL = 'https://api.douban.com/v2/'; axios.defaults.timeout = 10000; axios.interceptors.response.use((res) => { if (res.status >= 200 && res.status < 300) { return Promise.resolve(res); } return Promise.reject(res); }, (error) => { // 網(wǎng)絡(luò)異常 return Promise.reject({message: '網(wǎng)絡(luò)異常,請刷新重試', err: error, type: 1}); }); if (process.__API__) { api = process.__API__; } else { api = { get: function(target) { return new Promise((resolve, reject) => { axios.request({ url: encodeURI(target), method: 'get', headers: { 'Cookie': cook } }).then(res => { resolve(res); }).catch((error) => { reject(error); }); }); }, post: function(target, options = {}) { return new Promise((resolve, reject) => { axios.request({ url: target, method: 'post', headers: { 'Cookie': cook }, params: options }).then(res => { resolve(res); }).catch((error) => { reject(error); }); }); } }; } export default api;
六、那些年遇到的那些坑
問題1、window is not defined
答案1:給用到瀏覽器對象的地方加if (typeof window !== 'undefined') {},有一些插件里也用到了瀏覽器對象,在使用的地方也加一個條件判斷:
if (typeofwindow !== 'undefined') { Vue.use(VueAnalytics, { id: process.env.UA_TRACKING_ID, router }) }
問題2:用到非Vue系列的插件,如hello.all.js(三方登錄的插件),需要用的地方才引用,報(bào)的錯和問題1一樣。
答案2:這個時(shí)候不能再用import導(dǎo)入,需要使用require,
let hello
if (typeof window !== 'undefined') { hello = require('hello') }
問題3:引用bootstrap
答案3:將bootstrap.css和bootstrap.js加入webpack.base.config.js的entry中的vendor中
問題6:bootstap需要jquery,此時(shí)把jQuery加在vendor中沒用。
答案6:給webpack.base.config.js的plugins添加一個插件,如:
newwebpack.ProvidePlugin({ $ : "jquery", jQuery : "jquery", "window.jQuery" :"jquery" })
七、例子
https://github.com/linmoer/ssr-vue這是一個服務(wù)端渲的例子
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 詳解vue服務(wù)端渲染(SSR)初探
- 詳解基于 Nuxt 的 Vue.js 服務(wù)端渲染實(shí)踐
- 基于vue-ssr服務(wù)端渲染入門詳解
- 詳解Vue基于 Nuxt.js 實(shí)現(xiàn)服務(wù)端渲染(SSR)
- 詳解如何使用Vue2做服務(wù)端渲染
- Vue服務(wù)端渲染和Vue瀏覽器端渲染的性能對比(實(shí)例PK )
- 詳解Nuxt.js Vue服務(wù)端渲染摸索
- Vue 2.0 服務(wù)端渲染入門介紹
- 詳解基于vue的服務(wù)端渲染框架NUXT
- vue服務(wù)端渲染添加緩存的方法
- vue服務(wù)端渲染頁面緩存和組件緩存的實(shí)例詳解
- vue服務(wù)端渲染操作簡單入門實(shí)例分析
相關(guān)文章
vue 動態(tài)添加class,三個以上的條件做判斷方式
這篇文章主要介紹了vue 動態(tài)添加class,三個以上的條件做判斷方式,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11基于vue中解決v-for使用報(bào)紅并出現(xiàn)警告的問題
下面小編就為大家分享一篇基于vue中解決v-for使用報(bào)紅并出現(xiàn)警告的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03Vue實(shí)現(xiàn)動態(tài)樣式的多種方法匯總
本文要給大家介紹Vue實(shí)現(xiàn)動態(tài)樣式的多種方法,下面給大家?guī)韼讉€案列,需要的朋友可以借鑒研究一下。2021-06-06echarts.js 動態(tài)生成多個圖表 使用vue封裝組件操作
這篇文章主要介紹了echarts.js 動態(tài)生成多個圖表 使用vue封裝組件操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07如何用vue3+Element?plus實(shí)現(xiàn)一個完整登錄功能
要實(shí)現(xiàn)用戶的登錄功能,可以使用Vue3和Element?Plus,下面這篇文章主要給大家介紹了關(guān)于如何基于Vue3和Element?Plus組件庫實(shí)現(xiàn)一個完整的登錄功能,文中提供了詳細(xì)的代碼示例,需要的朋友可以參考下2023-10-10vue項(xiàng)目百度地圖如何自定義標(biāo)注marker
這篇文章主要介紹了vue項(xiàng)目百度地圖如何自定義標(biāo)注marker問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03Vue的filters(本地)或filter(全局)過濾常用數(shù)據(jù)類型解析
這篇文章主要介紹了Vue的filters(本地)或filter(全局)過濾常用數(shù)據(jù)類型,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07vue中jsencrypt與base64加密解密的實(shí)用流程
vue項(xiàng)目里面使用到的加密和解密的方法,本文主要介紹了vue中jsencrypt與base64加密解密的實(shí)用流程,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10