前言:前段時間學習了一下vue的服務端渲染,對某個不知名項目進行了SSR改造,遇到了許多神坑,撰文以記之,但願你們之後遇到相似的問題,再也不迷茫!在成長的道路上,再也不悲傷!
一、SSR簡單入門介紹
SSR,全稱server-side-render,即服務端渲染,顧名思義,就是在服務端直接「直出」渲染咱們頁面的數據,把數據直接填充在html中吐回前端。其實和改革開放時期的jsp渲染,以及後來node端的模板引擎jade、handlerbars等相似,都是把數據填充進了頁面,不一樣的是,之前咱們還要再服務端維護一份代碼,以往的方案有如下兩個缺點:css
- 一、不利於前端後分離。
- 二、前端修改後後臺需同步,帶來額外的工做量。
再到後來,21世紀了,你們開始採用vue、react等應用框架來開發頁面了,也再也不有前面說到的「直出」了,但這畢竟也帶來了其餘問題,一個是爬蟲爬取頁面再也不那麼方便了,由於拉回來的是一個空的html頁面,裏面沒有數據了,其次,數據拉取要等到資源加載結束,框架初始化完畢後才能拉取,在PV較高、對性能要求較高的業務場景下,對用戶形成很差的用戶體驗。
因此vue和react都相應實現了SSR方案來知足這種需求,讓你的框架代碼能夠跑在服務端,同構直出,方便SEO的同時,也縮短頁面的首屏展現時間。
服務端渲染和客戶端渲染時序圖以下所示:
引自:https://www.jianshu.com/p/10b...html
二、學習以前的疑問
在作SSR前,但願讀者們嘗試思考一下,若是須要你實現一套SSR的解決方案,須要完成什麼工做,會遇到什麼問題,諸如如何讓服務端跑同一份代碼?有哪些細節須要考慮到?筆者我的整理問題以下:前端
- 一、SSR方案是否只優化了首屏渲染?
- 二、SSR在服務端跑客戶端代碼,要初始化vue框架,要生成虛擬DOM,最後直出HTML,這個過程是否會對服務端的性能形成影響?
- 三、客戶端代碼開發完後,是否是須要在服務端中保留一份完整代碼給服務端跑?假如服務端和客戶端是分開的兩個項目,那豈不是要在服務端安裝客戶端須要的npm依賴包?
- 四、根據每一個頁面的不一樣路徑,都要作相應的直出?
- 五、先後端請求要一致,前端請求有攜帶session,後端請求要保證一致的狀況下,還須要解決session的問題
- 六、後臺直出html後,那JS、css依賴呢,由誰來填入?
- 七、後臺本質僅僅直出的是HTML字符串,因此前端vue代碼初始化的時候,須要接管後臺吐出來的html,從新生成虛擬DOM,以及接管後臺吐回來的store數據。
若是想要知道以上問題的答案,最後揭曉!vue
三、SSR原理
前面問了這麼多問題,是時候該進入正題了,由官方提供的示意圖能夠看到,咱們的客戶端代碼,store、components等,經過兩個入口文件分別進行打包,生成兩個bundle,server-bundle和client-bundle,而server-bundle負責運行在服務端,生成直出的HTML,渲染在前端,而client-bundle則會被混入到HTML中,最後由node-server吐出處理好的HTML。node
因爲你要讓你的客戶端代碼跑在服務端,你要進行如下操做:react
- 一、編寫通用代碼,服務端生成虛擬dom的時候,會執行created和beforeCreate函數,固然,你能夠在這裏拉請求渲染數據,可是不建議在這裏作,後面會解釋到。因此在初始化,從入口文件進來的每一步,你都要格外當心,避免訪問window、document等客戶端纔有的全局對象,若是不可避免,能夠經過假如if else的判斷語句來判斷是在客戶端仍是服務端,避免代碼執行報錯。
- 二、對原有的store、router、app進行改造,從官網你們也能夠看到,咱們要避免單例模式,因爲webpack打包出來最後模塊是以module.exports的形式暴露給其餘文件引用,若是你暴露出來的是一個對象,意味着後面的每一次引用,都會返回的是一個對象,但是不一樣的用戶渲染的但是不一樣的頁面,你怎麼能讓他們共享同一個app、store、router呢?顯然不合理,因此須要對原有的store、router等進行改造,封裝成工廠函數的形式,避免單例模式。(注意:router要改爲history模式,要讓請求走到後臺,而不是用默認的hash)
- 三、將組件的請求邏輯提取到asyncdata函數中,爲何呢?爲何我不在created裏作這件事呢?緣由很簡單,服務端直出頁面是一個同步的過程,中間渲染到某個組件後,發起了某個異步請求,最後直出了HTML了,請求還沒響應,數據還沒填充進html,你就返回給用戶了,那豈不是根本沒有起到服務端渲染數據的做用?因此提取到asyncdata函數的本質目的,是進行請求的統一入口管理,經過手動觸發asyncdata函數,return promise對象,讓你的應用知道數據返回了,填充好了,以後再把HTML直出吐給用戶。
由官網的示例能夠看到,至於爲何要傳遞一個store進來,而不經過this.$store進行訪問,小夥子,你仍是太年輕了,由於那個時候this.$store尚未掛載store對象,你只能這樣傳進來給函數調用了~沒辦法~webpack
再看上面這段代碼,經過在entry-server中,調用命中路由組件的asyncdata方法,以後再resolve應用app出去。
在這以前,把store的state賦值給context的state,是爲了最後HTML直出的時候,把store的狀態序列化後填寫進全局的__INITAIL_STATE__變量中。因此相應的,你須要在entry-client文件中,加入如下代碼,判斷是否讓vue接管原有的服務端直出的store數據。
ios
說完以上3點,接下來是你要在配置文件上 以及後臺路由上的修改:
一、配置webpack文件,分別打包後臺bundle,和前端bundle,兩種配置不一樣,且入口文件不一樣,前面說了,有兩個entry文件。
二、編寫通用的路由中間件,用於命中非靜態資源、非API接口之外的頁面接口,在這個中間件中寫入你的直出邏輯,包括調用相應的API,傳入相應的bundle文件,初始化fetch,讓它攜帶上客戶端帶過來的session。web
看到這裏,是的,這篇文章並非一篇配置教程,若是要看配置教程直接看官網便可~vuex
四、你可能會經歷的坑
這裏做者整理了全部在接入SSR的過程當中所遇到的問題。
- 一、cannot read useragent of undefined
global.navigator = {
userAgent: 'node',
}
- 二、打包後臺bundle的時候一直報錯,ERROR:Server-side bundle should have one single entry file.Avoid using CommonsChunkPlugin in the server config。 但是我也沒有使用多個入口文件啊?也沒有使用CommonChunk啊?最後比較配置文件發現,是用了splitchunk致使,因此修改代碼以下:
- 三、後來又發現本地的devserver跑不起來了,頁面空白了?是怎麼回事呢,原來仍是配置文件出了問題,配置了nodeExternal致使本地沒有把依賴包打包進去,因此前端頁面天然就白屏了。
- 四、因爲服務端代碼和客戶端代碼分開了兩個項目,各自維護,致使服務端代碼找不到依賴。因此要麼把服務端和客戶端代碼放一塊兒,要麼把nodeexternal配置去掉,把依賴打包好。
解決方法: 引入 node-fetch 提供Header接口
- 六、docunment is NOT DEFINED
首先,排查本身的客戶端代碼有沒有在執行過程當中引入document對象致使在服務端執行的,其次,css的一個plugin插件也可能致使webpack打包過程當中引入了document對象。
- 七、一點要保證你的服務端代碼容許的時候,window是undefined的,否則vue代碼會走進客戶端邏輯,調用document的方法,因爲做者用了內部的一個服務端框架,致使引入了window對象而報錯。
- 八、這裏做者還遇到了一個問題,在直出HTML的時候,vue-server-render源碼的裏執行報錯undefined,在分析文件依賴的時候除了問題,數組中多了個undefined,做者手動改了源碼剔除了undefined的狀況,很是的 粗暴
- 九、對服務端fetch進行改造,因爲客戶端發請求使用fetch的沒有用axios。要請求作到一致,因此你要在加上url前綴以及加上cookie。
- 十、對於數據拉取須要放置到asyncdata函數中,返回promise對象,其次,須要給函數注入store對象。promise resolve後才能夠返回HTML,不然返回空頁面。
- 十一、後來又爆了Error in callback for watcher function()... do not mutate vuex store state outside mutation handlers. 實際上是因爲在createStore的時候,我提取了一個對象在工廠函數外,致使這個對象被共用了,由於模塊只會初始化一次,這個對象因爲閉包的緣由被保留了下來,被全部用戶請求到來的時候共用了,因此要避免這個問題,就是要把全部的store對象涉及到的state,老老實實的放進工廠函數中,你們不共用。
五、總結
故事說到了這裏,你可能已經對SSR有了更深刻的思考或者瞭解,或者 更懵逼了,但這都沒有關係。
接下來我解答一下前面發問的問題:
答:是的。
- 二、SSR在服務端跑客戶端代碼,要初始化vue框架,要生成虛擬DOM,最後直出HTML,這個過程是否會對服務端的性能形成影響?
答:因爲要在服務端執行vue代碼,而node自己是單線程的,因此請求上來了,自己仍是會形成性能影響的,因此要作好取捨。
- 三、客戶端代碼開發完後,是否是須要在服務端中保留一份完整代碼給服務端跑?假如服務端和客戶端是分開的兩個項目,那豈不是要在服務端安裝客戶端須要的npm依賴包?
答:生成不一樣的bundle文件,在生成服務端的bundle文件的時候,能夠把依賴包都打進去,不配置nodeExternal。
- 四、根據每一個頁面的不一樣路徑,都要作相應的直出?
答:是的,須要爲router設置history模式,讓請求能夠走到後臺。
- 五、先後端請求要一致,前端請求有攜帶session,後端請求要保證一致的狀況下,還須要解決session的問題。
答:是的,須要再處理請求前,對fetch進行改寫,加上urlprefix以及session。
- 六、後臺直出html後,那JS、css依賴呢,由誰來填入?
答:因爲打包前端代碼的時候,會生成一個json文件,提供給服務端,因此服務端直出的時候也會把相應的依賴填寫進去。
- 七、後臺本質僅僅直出的是HTML字符串,因此前端vue代碼初始化的時候,須要接管後臺吐出來的html,從新生成虛擬DOM,以及接管後臺吐回來的store數據。
答:是的。
本期講堂到此結束,請你們踊躍發言!歡迎討論!謝謝!
舒適提示:將會在評論中選出一個幸運兒!獎勵一個價值不超過1塊錢的紅包哦!