Vue服務端渲染 VS Vue瀏覽器端渲染)

Vue 2.0 開始支持服務端渲染的功能,因此本文章也是基於vue 2.0以上版本。網上對於服務端渲染的資料仍是比較少,最經典的莫過於Vue做者尤雨溪大神的 vue-hacker-news。本人在公司作Vue項目的時候,一直苦於產品、客戶對首屏加載要求,SEO的訴求,也想過不少解決方案,本次也是針對瀏覽器渲染不足之處,採用了服務端渲染,而且作了兩個同樣的Demo做爲比較,更能直觀的對比Vue先後端的渲染。javascript

talk is cheap,show us the code!話很少說,咱們分別來看兩個Demo:(歡迎star 歡迎pull request)html

1.瀏覽器端渲染Demo: https://github.com/monkeyWangs/doubanMovievue

2.服務端渲染Demo:https://github.com/monkeyWangs/doubanMovie-SSRjava

兩套代碼運行結果都是爲了展現豆瓣電影的,運行效果也都是差很少,下面咱們來分別簡單的闡述一下項目的機理:node

1、瀏覽器端渲染豆瓣電影

首先咱們用官網的腳手架搭建起來一個vue項目webpack

1
2
3
4
5
npm install -g vue-cli
vue init webpack doubanMovie
cd doubanMovie
npm install
npm run dev

  

這樣即可以簡單地打起來一個cli框架,下面咱們要作的事情就是分別配置 vue-router, vuex,而後配置咱們的webpack proxyTable 讓他支持代理訪問豆瓣API。git

1.配置Vue-routeres6

咱們須要三個導航頁:正在上映、即將上映、Top250;一個詳情頁,一個搜索頁。這裏我給他們分別配置了各自的路由。在 router/index.js 下配置如下信息:github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import  Vue from  'vue'
import  Router from  'vue-router'
import  Moving from  '@/components/moving'
import  Upcoming from  '@/components/upcoming'
import  Top250 from  '@/components/top250'
import  MoviesDetail from  '@/components/common/moviesDetail'
 
import  Search from  '@/components/searchList'
 
Vue.use(Router)
/**
  * 路由信息配置
  */
export  default  new  Router({
   routes: [
     {
       path:  '/' ,
       name:  'Moving' ,
       component: Moving
     },
     {
       path:  '/upcoming' ,
       name:  'upcoming' ,
       component: Upcoming
     },
     {
       path:  '/top250' ,
       name:  'Top250' ,
       component: Top250
     },
     {
       path:  '/search' ,
       name:  'Search' ,
       component: Search
     },
     {
       path:  '/moviesDetail' ,
       name:  'moviesDetail' ,
       component: MoviesDetail
     }
 
   ]
})

  

這樣咱們的路由信息配置好了,而後每次切換路由的時候,儘可能避免不要重複請求數據,因此咱們還須要配置一下組件的keep-alive:在app.vue組件裏面。web

<keep-alive exclude="moviesDetail">
   <router-view></router-view>
</keep-alive>

 

這樣一個基本的vue-router就配置好了。

2.引入vuex

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調試功能。

簡而言之:Vuex 至關於某種意義上設置了讀寫權限的全局變量,將數據保存保存到該「全局變量」下,並經過必定的方法去讀寫數據。

Vuex 並不限制你的代碼結構。可是,它規定了一些須要遵照的規則:

  1. 應用層級的狀態應該集中到單個 store 對象中。

  2. 提交 mutation 是更改狀態的惟一方法,而且這個過程是同步的。

  3. 異步邏輯都應該封裝到 action 裏面。

對於大型應用咱們會但願把 Vuex 相關代碼分割到模塊中。下面是項目結構示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── index.html
├── main.js
├── api
│   └── ...  # 抽取出API請求
├── components
│   ├── App.vue
│   └── ...
└── store
     ├── index.js           # 咱們組裝模塊並導出 store 的地方
     └── moving             # 電影模塊
         ├── index.js       # 模塊內組裝,並導出模塊的地方
         ├── actions.js     # 模塊基本 action
         ├── getters.js     # 模塊級別 getters
         ├── mutations.js   # 模塊級別 mutations
         └── types.js       # 模塊級別 types

  

因此咱們開始在咱們的src目錄下新建一個名爲store 的文件夾 爲了後期考慮 咱們新建了moving 文件夾,用來組織電影,考慮到全部的action,getters,mutations,都寫在一塊兒,文件太混亂,因此我又給他們分別提取出來。

stroe文件夾建好,咱們要開始在main.js裏面引用vuex實例:

複製代碼
import store from './store'
new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App }
})
複製代碼

 

這樣,咱們即可以在全部的子組件裏經過 this.$store 來使用vuex了。

3.webpack proxyTable 代理跨域

webpack 開發環境可使用proxyTable 來代理跨域,生產環境的話能夠根據各自的服務器進行配置代理跨域就好了。在咱們的項目config/index.js 文件下能夠看到有一個proxyTable的屬性,咱們對其簡單的改寫

複製代碼
proxyTable: {
      '/api': {
        target: 'http://api.douban.com/v2',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
複製代碼

 

這樣當咱們訪問

localhost:8080/api/movie

 

的時候 其實咱們訪問的是

http://api.douban.com/v2/movie

 

這樣便達到了一種跨域請求的方案。

至此,瀏覽器端的主要配置已經介紹完了,下面咱們來看看運行的結果:

爲了介紹瀏覽器渲染是怎麼回事,咱們運行一下npm run build 看看咱們的發佈版本的文件,究竟是什麼鬼東西....

run build 後會都出一個dist目錄 ,咱們能夠看到裏面有個index.html,這個即是咱們最終頁面將要展現的html,咱們打開,能夠看到下面:

觀察好的小夥伴能夠發現,咱們並無多餘的dom元素,就只有一個div,那麼頁面要怎麼呈現呢?答案是js append,對,下面的那些js會負責innerHTML。而js是由瀏覽器解釋執行的,因此呢,咱們稱之爲瀏覽器渲染,這有幾個致命的缺點:

1.js放在dom結尾,若是js文件過大,那麼必然形成頁面阻塞。用戶體驗明顯很差(這也是我我在公司反覆被產品逼問的事情)

2.不利於SEO

3.客戶端運行在老的JavaScript引擎上

對於世界上的一些地區人,可能只能用1998年產的電腦訪問互聯網的方式使用計算機。而Vue只能運行在IE9以上的瀏覽器,你可能也想爲那些老式瀏覽器提供基礎內容 - 或者是在命令行中使用 Lynx的時髦的黑客

基於以上的一些問題,服務端渲染呼之欲出....

2、服務器端渲染豆瓣電影

先看一張Vue官網的服務端渲染示意圖

從圖上能夠看出,ssr 有兩個入口文件,client.js 和 server.js, 都包含了應用代碼,webpack 經過兩個入口文件分別打包成給服務端用的 server bundle 和給客戶端用的 client bundle. 當服務器接收到了來自客戶端的請求以後,會建立一個渲染器 bundleRenderer,這個 bundleRenderer 會讀取上面生成的 server bundle 文件,而且執行它的代碼, 而後發送一個生成好的 html 到瀏覽器,等到客戶端加載了 client bundle 以後,會和服務端生成的DOM 進行 Hydration(判斷這個DOM 和本身即將生成的DOM 是否相同,若是相同就將客戶端的vue實例掛載到這個DOM上, 不然會提示警告)。

具體實現:

咱們須要vuex,須要router,須要服務器,須要服務緩存,須要代理跨域....不急咱們慢慢來。

1.創建nodejs服務

首先咱們須要一個服務器,那麼對於nodejs,express是很好地選擇。咱們來創建一個server.js

const port = process.env.PORT || 8080
app.listen(port, () => {
  console.log(`server started at localhost:${port}`)
})

 

這裏用來啓動服務監聽 8080 端口。

而後咱們開始處理全部的get請求,當請求頁面的時候,咱們須要渲染頁面

複製代碼
app.get('*', (req, res) => {
  if (!renderer) {
    return res.end('waiting for compilation... refresh in a moment.')
  }

  const s = Date.now()

  res.setHeader("Content-Type", "text/html")
  res.setHeader("Server", serverInfo)

  const errorHandler = err => {
    if (err && 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)
    }
  }

  renderer.renderToStream({ url: req.url })
    .on('error', errorHandler)
    .on('end', () => console.log(`whole request: ${Date.now() - s}ms`))
    .pipe(res)
})
複製代碼

 

而後咱們須要代理請求,這樣才能進行跨域,咱們引入http-proxy-middleware模塊:

複製代碼
const proxy = require('http-proxy-middleware');//引入代理中間件
/**
 * proxy middleware options
 * 代理跨域配置
 * @type {{target: string, changeOrigin: boolean, pathRewrite: {^/api: string}}}
 */
var options = {
  target: 'http://api.douban.com/v2', // target host
  changeOrigin: true,               // needed for virtual hosted sites
  pathRewrite: {
    '^/api': ''
  }
};

var exampleProxy = proxy(options);
app.use('/api', exampleProxy);
複製代碼

 

這樣咱們的服務端server.js便配置完成。接下來 咱們須要配置服務端入口文件,還有客戶端入口文件,首先來配置一下客戶端文件,新建src/entry-client.js

複製代碼
import 'es6-promise/auto'
import { app, store, router } from './app'

// prime the store with server-initialized state.
// the state is determined during SSR and inlined in the page markup.
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

/**
 * 異步組件
 */
router.onReady(() => {
  // 開始掛載到dom上
  app.$mount('#app')
})

// service worker
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
}
複製代碼

 

客戶端入口文件很簡單,同步服務端發送過來的數據,而後把 vue 實例掛載到服務端渲染的 DOM 上。

再配置一下服務端入口文件:src/entry-server.js

複製代碼
import { app, router, store } from './app'

const isDev = process.env.NODE_ENV !== 'production'

// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default context => {
  const s = isDev && Date.now()

  return new Promise((resolve, reject) => {
    // set router's location
    router.push(context.url)

    // wait until router has resolved possible async hooks
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // no matched routes
      if (!matchedComponents.length) {
        reject({ code: 404 })
      }
      // Call preFetch hooks on components matched by the route.
      // A preFetch hook dispatches a store action and returns a Promise,
      // which is resolved when the action is complete and store state has been
      // updated.
      Promise.all(matchedComponents.map(component => {
        return component.preFetch && component.preFetch(store)
      })).then(() => {
        isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)
        // After all preFetch hooks are resolved, our store is now
        // filled with the state needed to render the app.
        // Expose the state on the render context, and let the request handler
        // inline the state in the HTML response. This allows the client-side
        // store to pick-up the server-side state without having to duplicate
        // the initial data fetching on the client.
        context.state = store.state
        resolve(app)
      }).catch(reject)
    })
  })
}
複製代碼

 

server.js 返回一個函數,該函數接受一個從服務端傳遞過來的 context 的參數,將 vue 實例經過 promise 返回。context 通常包含 當前頁面的url,首先咱們調用 vue-router 的 router.push(url) 切換到到對應的路由, 而後調用 getMatchedComponents 方法返回對應要渲染的組件, 這裏會檢查組件是否有 fetchServerData 方法,若是有就會執行它。

下面這行代碼將服務端獲取到的數據掛載到 context 對象上,後面會把這些數據直接發送到瀏覽器端與客戶端的vue 實例進行數據(狀態)同步。

context.state = store.state

 

而後咱們分別配置客戶端和服務端webpack,這裏能夠在個人github上fork下來參考配置,裏面每一步都有註釋,這裏再也不贅述。

接着咱們須要建立app.js:

複製代碼
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from './router'
import { sync } from 'vuex-router-sync'
import Element from 'element-ui'
Vue.use(Element)

// sync the router with the vuex store.
// this registers `store.state.route`
sync(store, router)

/**
 * 建立vue實例
 * 在這裏注入 router  store 到全部的子組件
 * 這樣就能夠在任何地方使用 `this.$router` and `this.$store`
 * @type {Vue$2}
 */
const app = new Vue({
  router,
  store,
  render: h => h(App)
})

/**
 * 導出 router and store.
 * 在這裏不須要掛載到app上。這裏和瀏覽器渲染不同
 */
export { app, router, store }
複製代碼

 

這樣 服務端入口文件和客戶端入口文件便有了一個公共實例Vue, 和咱們之前寫的vue實例差異不大,可是咱們不會在這裏將app mount到DOM上,由於這個實例也會在服務端去運行,這裏直接將 app 暴露出去。

接下來建立路由router,建立vuex跟客戶端都差很少。詳細的能夠參考個人項目...

到此,服務端渲染配置 就簡單介紹完了,下面咱們啓動項目簡單的看下:

這裏跟服務端界面同樣,不同的是url已經不是以前的 #/而變成了請求形式 /

這樣每當瀏覽器發送一個頁面的請求,會有服務器渲染出一個dom字符串返回,直接在瀏覽器段顯示,這樣就避免了瀏覽器端渲染的不少問題。

提及SSR,其實早在SPA (Single Page Application) 出現以前,網頁就是在服務端渲染的。服務器接收到客戶端請求後,將數據和模板拼接成完整的頁面響應到客戶端。 客戶端直接渲染, 此時用戶但願瀏覽新的頁面,就必須重複這個過程, 刷新頁面. 這種體驗在Web技術發展的當下是幾乎不能被接受的,因而愈來愈多的技術方案涌現,力求 實現無頁面刷新或者局部刷新來達到優秀的交互體驗。可是SEO倒是致命的,因此一切看應用場景,這裏只爲你們提供技術思路,爲vue開發提供多一種可能的方案。

相關文章
相關標籤/搜索