簡單的Vue SSR的範例程式碼,vuessr範例程式碼

來源:互聯網
上載者:User

簡單的Vue SSR的範例程式碼,vuessr範例程式碼
前言

最近接手一個老項目,典型的 Vue 組件化前端渲染,後續業務最佳化可能會朝 SSR 方向走,因此,就先做些技術儲備。如果對 Vue SSR 完全不瞭解,請先閱讀官方文檔。

思路

Vue 提供了一個官方 Demo,該 Demo 優點是功能大而全,缺點是對新手不友好,容易讓人看蒙。因此,今天我們來寫一個更加容易上手的 Demo。總共分三步走,循序漸進。

  1. 寫一個簡單的前端渲染 Demo(不包含 Ajax 資料);
  2. 將前端渲染改成後端渲染(仍然不包含 Ajax 資料);
  3. 在後端渲染的基礎上,加上 Ajax 資料的處理;
第一步:前端渲染 Demo

這部分比較簡單,就是一個頁面中包含兩個組件:Foo 和 Bar。

<!-- index.html --><body><div id="app"> <app></app></div><script src="./dist/web.js"></script> <!--這是 app.js 打包出來的 JS 檔案 --></body>// app.js,也是 webpack 打包入口import Vue from 'vue';import App from './App.vue';var app = new Vue({ el: '#app', components: { App }});
// App.vue<template> <div> <foo></foo> <bar></bar> </div></template><script> import Foo from './components/Foo.vue'; import Bar from './components/Bar.vue'; export default { components:{  Foo,  Bar } }</script>
// Foo.vue<template> <div class='foo'> <h1>Foo</h1> <p>Component </p> </div></template><style> .foo{ background: yellow; }</style>
// Bar.vue<template> <div class='bar'> <h1>Bar</h1> <p>Component </p> </div></template><style> .bar{ background: blue; }</style>

最終渲染結果如所示,源碼請參考這裡。

第二步:後端渲染(不包含 Ajax 資料)

第一步的 Demo 雖不包含任何 Ajax 資料,但即便如此,要把它改造成後端渲染,亦非易事。該從哪幾個方面著手呢?

  1. 拆分 JS 入口;
  2. 拆分 Webpack 打包配置;
  3. 編寫服務端渲染主體邏輯。

1. 拆分 JS 入口

在前端渲染的時候,只需要一個入口 app.js。現在要做後端渲染,就得有兩個 JS 檔案:entry-client.js 和 entry-server.js 分別作為瀏覽器和伺服器的入口。

先看 entry-client.js,它跟第一步的 app.js 有什麼區別嗎? → 沒有區別,只是換了個名字而已,內容都一樣。

再看 entry-server.js,它只需返回 App.vue 的執行個體。

// entry-server.jsexport default function createApp() { const app = new Vue({ render: h => h(App) }); return app; };

entry-server.js 與 entry-client.js 這兩個入口主要區別如下:

  1. entry-client.js 在瀏覽器端執行,所以需要指定 el 並且顯式調用 $mount 方法,以啟動瀏覽器的渲染。
  2. entry-server.js 在服務端被調用,因此需要匯出為一個函數。

2. 拆分 Webpack 打包配置

在第一步中,由於只有 app.js 一個入口,只需要一份 Webpack 設定檔。現在有兩個入口了,自然就需要兩份 Webpack 設定檔:webpack.server.conf.js 和 webpack.client.conf.js,它們的公用部分抽象成 webpack.base.conf.js。

關於 webpack.server.conf.js,有兩個注意點:

  1. libraryTarget: 'commonjs2' → 因為伺服器是 Node,所以必須按照 commonjs 規範打包才能被伺服器調用。
  2. target: 'node' → 指定 Node 環境,避免非 Node 環境特定 API 報錯,如 document 等。

3. 編寫服務端渲染主體邏輯

Vue SSR 依賴於包 vue-server-render,它的調用支援兩種入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 組件為入口,後者以打包後的 JS 檔案為入口,本文採取後者。

// server.js 服務端渲染主體邏輯// dist/server.js 就是以 entry-server.js 為入口打包出來的 JS const bundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.js'), 'utf-8'); const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf-8')});server.get('/index', (req, res) => { renderer.renderToString((err, html) => { if (err) {  console.error(err);  res.status(500).end('伺服器內部錯誤');  return; } res.end(html); })});server.listen(8002, () => { console.log('後端渲染伺服器啟動,連接埠號碼為:8002');});

這一步的最終渲染效果如所示,我們可以看到,組件已經被後端成功渲染了。源碼請參考這裡。

第三步:後端渲染(預擷取 Ajax 資料)

這是關鍵的一步,也是最難的一步。

假如第二步的組件各自都需要請求 Ajax 資料的話,該怎麼處理呢?官方文檔給我們指出了思路,我簡要概括如下:

  1. 在開始渲染之前,預先擷取所有需要的 Ajax 資料(然後存在 Vuex 的 Store 中);
  2. 後端渲染的時候,通過 Vuex 將擷取到的 Ajax 資料分別注入到各個組件中;
  3. 把全部 Ajax 資料埋在 window.INITIAL_STATE 中,通過 HTML 傳遞到瀏覽器端;
  4. 瀏覽器端通過 Vuex 將 window.INITIAL_STATE 裡面的 Ajax 資料分別注入到各個組件中。

下面談幾個重點。

我們知道,在常規的 Vue 前端渲染中,組件請求 Ajax 一般是這麼寫的:“在 mounted 中調用 this.fetchData,然後在回調裡面把返回資料寫到執行個體的 data 中,這就 ok 了。”

在 SSR 中,這是不行的,因為伺服器並不會執行 mounted 周期。那麼我們是否可以把 this.fetchData

提前到 created 或者 beforeCreate 這兩個生命週期中執行?同樣不行。原因是:this.fetchData 是非同步請求,請求發出去之後,沒等資料返回呢,後端就已經渲染完了,無法把 Ajax 返回的資料也一併渲染出來。

所以,我們得提前知道都有哪些組件有 Ajax 請求,等把這些 Ajax 請求都返回了資料之後,才開始組件的渲染。

// store.jsfunction fetchBar() { return new Promise(function (resolve, reject) { resolve('bar ajax 返回資料'); });}export default function createStore() { return new Vuex.Store({ state: {  bar: '', }, actions: {  fetchBar({commit}) {  return fetchBar().then(msg => {   commit('setBar', {msg})  })  } }, mutations:{  setBar(state, {msg}) {  Vue.set(state, 'bar', msg);  } } })}
// Bar.uveasyncData({store}) { return store.dispatch('fetchBar');},computed: { bar() { return this.$store.state.bar; }}

組件的 asyncData 方法已經定義好了,但是怎麼索引到這個 asyncData 方法呢?先看我的根組件 App.vue 是怎麼寫的。

// App.vue<template> <div> <h1>App.vue</h1> <p>vue with vue </p> <hr> <foo1 ref="foo_ref"></foo1> <bar1 ref="bar_ref"></bar1> <bar2 ref="bar_ref2"></bar2> </div></template><script> import Foo from './components/Foo.vue'; import Bar from './components/Bar.vue'; export default { components: {  foo1: Foo,  bar1: Bar,  bar2: Bar } }</script>

從根組件 App.vue 我們可以看到,只需要解析其 components 欄位,便能依次找到各個組件的 asyncData 方法了。

// entry-server.js export default function (context) { // context 是 vue-server-render 注入的參數 const store = createStore(); let app = new Vue({ store, render: h => h(App) }); // 找到所有 asyncData 方法 let components = App.components; let prefetchFns = []; for (let key in components) { if (!components.hasOwnProperty(key)) continue; let component = components[key]; if(component.asyncData) {  prefetchFns.push(component.asyncData({  store  })) } } return Promise.all(prefetchFns).then((res) => { // 在所有組件的 Ajax 都返回之後,才最終返回 app 進行渲染 context.state = store.state; // context.state 賦值成什麼,window.__INITIAL_STATE__ 就是什麼 return app; });};

還有幾個問題比較有意思:

1、是否必須使用 vue-router?→ 不是。雖然官方給出的 Demo 裡面用到了 vue-router,那隻不過是因為官方 Demo 是包含多個頁面的 SPA 罷了。一般情況下,是需要用 vue-router 的,因為不同路由對應不同的組件,並非每次都把所有組件的 asyncData 都執行的。但是有例外,比如我的這個老項目,就只有一個頁面(一個頁面中包含很多的組件),所以根本不需要用到 vue-router,也照樣能做 SSR。主要的區別就是如何找到那些該被執行的 asyncData 方法:官方 Demo 通過 vue-router,而我通過直接解析 components 欄位,僅此而已。

2、是否必須使用 Vuex? → 是,但也不是,請看尤大的回答。為什麼必須要有類似 Vuex 的存在?我們來分析一下。

2.1. 當預先擷取到的 Ajax 資料返回之後,Vue 組件還沒開始渲染。所以,我們得把 Ajax 先存在某個地方。

2.2. 當 Vue 組件開始渲染的時候,還得把 Ajax 資料拿出來,正確地傳遞到各個組件中。

2.3. 在瀏覽器渲染的時候,需要正確解析 window.INITIAL_STATE ,並傳遞給各個組件。

因此,我們得有這麼一個獨立於視圖以外的地方,用來儲存、管理和傳遞資料,這就是 Vuex 存在的理由。

3、後端已經把 Ajax 資料轉化為 HTML 了,為什麼還需要把 Ajax 資料通過 window.INITIAL_STATE 傳遞到前端? → 因為前端渲染的時候仍然需要知道這些資料。舉個例子,你寫了一個組件,給它綁定了一個點擊事件,點擊的時候列印出 this.msg 欄位值。現在後端是把組件 HTML 渲染出來了,但是事件的綁定肯定得由瀏覽器來完成啊,如果瀏覽器拿不到跟伺服器端同樣的資料的話,在觸發組件的點擊事件的時候,又上哪兒去找 msg 欄位呢?

至此,我們已經完成了帶 Ajax 資料的後端渲染了。這一步最為複雜,也最為關鍵,需要反覆思考和嘗試。具體渲染如下所示,源碼請參考這裡。

效果

大功告成了嗎?還沒。人們都說 SSR 能提升首屏渲染速度,下面我們對比一下看看到底是不是真的。(同樣在 Fast 3G 網路條件下)。

官方思路的變形

行文至此,關於 Vue SSR Demo便已經結束了。後面是我結合自身項目特點的一些變形,不感興趣的讀者可以不看。

第三步官方思路有什麼缺點嗎?我認為是有的:對老項目來說,改造成本比較大。需要顯式的引入 vuex,就得走 action、mutations 那一套,無論是代碼改動量還是新人學習成本,都不低。

有什麼辦法能減少對舊有前端渲染項目的改動量的嗎?我是這麼做的。

// store.js// action,mutations 那些都不需要了,只定義一個空 stateexport default function createStore() { return new Vuex.Store({ state: {} })}// Bar.vue// tagName 是組件執行個體的名字,比如 bar1、bar2、foo1 等,由 entry-server.js 注入export default { prefetchData: function (tagName) { return new Promise((resolve, reject) => {  resolve({  tagName,  data: 'Bar ajax 資料'  }); }) }}
// entry-server.jsreturn Promise.all(prefetchFns).then((res) => { // 拿到 Ajax 資料之後,手動將資料寫入 state,不通過 action,mutation 那一套 // state 內部區分的 key 值就是 tagName,比如 bar1、bar2、foo1 等 res.forEach((item, key) => { Vue.set(store.state, `${item.tagName}`, item.data); }); context.state = store.state; return app;});
// ssrmixin.js// 將每個組件都需要的 computed 抽象成一個 mixin,然後注入export default { computed: { prefetchData () {  let componentTag = this.$options._componentTag; // bar1、bar2、foo1  return this.$store.state[componentTag]; } }}

至此,我們就便得到了 Vue SSR 的一種變形。對於組件開發人員而言,只需要把原來的 this.fetchData 方法抽象到 prefetchData 方法,然後就可以在 DOM 中使用 {{prefetchData}} 拿到到資料了。這部分的代碼請參考這裡。

總結

Vue SSR 確實是個有趣的東西,關鍵在於靈活運用。此 Demo 還有一個遺留問題沒有解決:當把 Ajax 抽象到 prefetchData,做成 SSR 之後,原先的前端渲染就失效了。能不能同一份代碼同時支援前端渲染和後端渲染呢?這樣當後端渲染出問題的時候,我就可以隨時切回前端渲染,便有了兜底的方案。

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援幫客之家。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.