国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 編程 > JavaScript > 正文

簡(jiǎn)單的Vue SSR的示例代碼

2019-11-19 14:32:02
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

前言

最近接手一個(gè)老項(xiàng)目,典型的 Vue 組件化前端渲染,后續(xù)業(yè)務(wù)優(yōu)化可能會(huì)朝 SSR 方向走,因此,就先做些技術(shù)儲(chǔ)備。如果對(duì) Vue SSR 完全不了解,請(qǐng)先閱讀官方文檔。

思路

Vue 提供了一個(gè)官方 Demo,該 Demo 優(yōu)點(diǎn)是功能大而全,缺點(diǎn)是對(duì)新手不友好,容易讓人看蒙。因此,今天我們來(lái)寫一個(gè)更加容易上手的 Demo。總共分三步走,循序漸進(jìn)。

  1. 寫一個(gè)簡(jiǎn)單的前端渲染 Demo(不包含 Ajax 數(shù)據(jù));
  2. 將前端渲染改成后端渲染(仍然不包含 Ajax 數(shù)據(jù));
  3. 在后端渲染的基礎(chǔ)上,加上 Ajax 數(shù)據(jù)的處理;

第一步:前端渲染 Demo

這部分比較簡(jiǎn)單,就是一個(gè)頁(yè)面中包含兩個(gè)組件:Foo 和 Bar。

<!-- index.html --><body><div id="app"> <app></app></div><script src="./dist/web.js"></script> <!--這是 app.js 打包出來(lái)的 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>

最終渲染結(jié)果如下圖所示,源碼請(qǐng)參考這里。

第二步:后端渲染(不包含 Ajax 數(shù)據(jù))

第一步的 Demo 雖不包含任何 Ajax 數(shù)據(jù),但即便如此,要把它改造成后端渲染,亦非易事。該從哪幾個(gè)方面著手呢?

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

1. 拆分 JS 入口

在前端渲染的時(shí)候,只需要一個(gè)入口 app.js。現(xiàn)在要做后端渲染,就得有兩個(gè) JS 文件:entry-client.js 和 entry-server.js 分別作為瀏覽器和服務(wù)器的入口。

先看 entry-client.js,它跟第一步的 app.js 有什么區(qū)別嗎? → 沒(méi)有區(qū)別,只是換了個(gè)名字而已,內(nèi)容都一樣。

再看 entry-server.js,它只需返回 App.vue 的實(shí)例。

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

entry-server.js 與 entry-client.js 這兩個(gè)入口主要區(qū)別如下:

  1. entry-client.js 在瀏覽器端執(zhí)行,所以需要指定 el 并且顯式調(diào)用 $mount 方法,以啟動(dòng)瀏覽器的渲染。
  2. entry-server.js 在服務(wù)端被調(diào)用,因此需要導(dǎo)出為一個(gè)函數(shù)。

2. 拆分 Webpack 打包配置

在第一步中,由于只有 app.js 一個(gè)入口,只需要一份 Webpack 配置文件。現(xiàn)在有兩個(gè)入口了,自然就需要兩份 Webpack 配置文件:webpack.server.conf.js 和 webpack.client.conf.js,它們的公共部分抽象成 webpack.base.conf.js。

關(guān)于 webpack.server.conf.js,有兩個(gè)注意點(diǎn):

  1. libraryTarget: 'commonjs2' → 因?yàn)榉?wù)器是 Node,所以必須按照 commonjs 規(guī)范打包才能被服務(wù)器調(diào)用
  2. target: 'node' → 指定 Node 環(huán)境,避免非 Node 環(huán)境特定 API 報(bào)錯(cuò),如 document 等。

3. 編寫服務(wù)端渲染主體邏輯

Vue SSR 依賴于包 vue-server-render,它的調(diào)用支持兩種入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 組件為入口,后者以打包后的 JS 文件為入口,本文采取后者。

// server.js 服務(wù)端渲染主體邏輯// dist/server.js 就是以 entry-server.js 為入口打包出來(lái)的 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('服務(wù)器內(nèi)部錯(cuò)誤');  return; } res.end(html); })});server.listen(8002, () => { console.log('后端渲染服務(wù)器啟動(dòng),端口號(hào)為:8002');});

這一步的最終渲染效果如下圖所示,從圖中我們可以看到,組件已經(jīng)被后端成功渲染了。源碼請(qǐng)參考這里

第三步:后端渲染(預(yù)獲取 Ajax 數(shù)據(jù))

這是關(guān)鍵的一步,也是最難的一步。

假如第二步的組件各自都需要請(qǐng)求 Ajax 數(shù)據(jù)的話,該怎么處理呢?官方文檔給我們指出了思路,我簡(jiǎn)要概括如下:

  1. 在開(kāi)始渲染之前,預(yù)先獲取所有需要的 Ajax 數(shù)據(jù)(然后存在 Vuex 的 Store 中);
  2. 后端渲染的時(shí)候,通過(guò) Vuex 將獲取到的 Ajax 數(shù)據(jù)分別注入到各個(gè)組件中;
  3. 把全部 Ajax 數(shù)據(jù)埋在 window.INITIAL_STATE 中,通過(guò) HTML 傳遞到瀏覽器端;
  4. 瀏覽器端通過(guò) Vuex 將 window.INITIAL_STATE 里面的 Ajax 數(shù)據(jù)分別注入到各個(gè)組件中。

下面談幾個(gè)重點(diǎn)。

我們知道,在常規(guī)的 Vue 前端渲染中,組件請(qǐng)求 Ajax 一般是這么寫的:“在 mounted 中調(diào)用 this.fetchData,然后在回調(diào)里面把返回?cái)?shù)據(jù)寫到實(shí)例的 data 中,這就 ok 了。”

在 SSR 中,這是不行的,因?yàn)榉?wù)器并不會(huì)執(zhí)行 mounted 周期。那么我們是否可以把 this.fetchData

提前到 created 或者 beforeCreate 這兩個(gè)生命周期中執(zhí)行?同樣不行。原因是:this.fetchData 是異步請(qǐng)求,請(qǐng)求發(fā)出去之后,沒(méi)等數(shù)據(jù)返回呢,后端就已經(jīng)渲染完了,無(wú)法把 Ajax 返回的數(shù)據(jù)也一并渲染出來(lái)。

所以,我們得提前知道都有哪些組件有 Ajax 請(qǐng)求,等把這些 Ajax 請(qǐng)求都返回了數(shù)據(jù)之后,才開(kāi)始組件的渲染。

// store.jsfunction fetchBar() { return new Promise(function (resolve, reject) { resolve('bar ajax 返回?cái)?shù)據(jù)'); });}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 方法已經(jīng)定義好了,但是怎么索引到這個(gè) 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 字段,便能依次找到各個(gè)組件的 asyncData 方法了。

// entry-server.js export default function (context) { // context 是 vue-server-render 注入的參數(shù) 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 進(jìn)行渲染 context.state = store.state; // context.state 賦值成什么,window.__INITIAL_STATE__ 就是什么 return app; });};

還有幾個(gè)問(wèn)題比較有意思:

1、是否必須使用 vue-router?→ 不是。雖然官方給出的 Demo 里面用到了 vue-router,那只不過(guò)是因?yàn)楣俜?Demo 是包含多個(gè)頁(yè)面的 SPA 罷了。一般情況下,是需要用 vue-router 的,因?yàn)椴煌酚蓪?duì)應(yīng)不同的組件,并非每次都把所有組件的 asyncData 都執(zhí)行的。但是有例外,比如我的這個(gè)老項(xiàng)目,就只有一個(gè)頁(yè)面(一個(gè)頁(yè)面中包含很多的組件),所以根本不需要用到 vue-router,也照樣能做 SSR。主要的區(qū)別就是如何找到那些該被執(zhí)行的 asyncData 方法:官方 Demo 通過(guò) vue-router,而我通過(guò)直接解析 components 字段,僅此而已。

2、是否必須使用 Vuex? → 是,但也不是,請(qǐng)看尤大的回答。為什么必須要有類似 Vuex 的存在?我們來(lái)分析一下。

2.1. 當(dāng)預(yù)先獲取到的 Ajax 數(shù)據(jù)返回之后,Vue 組件還沒(méi)開(kāi)始渲染。所以,我們得把 Ajax 先存在某個(gè)地方。

2.2. 當(dāng) Vue 組件開(kāi)始渲染的時(shí)候,還得把 Ajax 數(shù)據(jù)拿出來(lái),正確地傳遞到各個(gè)組件中。

2.3. 在瀏覽器渲染的時(shí)候,需要正確解析 window.INITIAL_STATE ,并傳遞給各個(gè)組件。

因此,我們得有這么一個(gè)獨(dú)立于視圖以外的地方,用來(lái)存儲(chǔ)、管理和傳遞數(shù)據(jù),這就是 Vuex 存在的理由。

3、后端已經(jīng)把 Ajax 數(shù)據(jù)轉(zhuǎn)化為 HTML 了,為什么還需要把 Ajax 數(shù)據(jù)通過(guò) window.INITIAL_STATE 傳遞到前端? → 因?yàn)榍岸虽秩镜臅r(shí)候仍然需要知道這些數(shù)據(jù)。舉個(gè)例子,你寫了一個(gè)組件,給它綁定了一個(gè)點(diǎn)擊事件,點(diǎn)擊的時(shí)候打印出 this.msg 字段值。現(xiàn)在后端是把組件 HTML 渲染出來(lái)了,但是事件的綁定肯定得由瀏覽器來(lái)完成啊,如果瀏覽器拿不到跟服務(wù)器端同樣的數(shù)據(jù)的話,在觸發(fā)組件的點(diǎn)擊事件的時(shí)候,又上哪兒去找 msg 字段呢?

至此,我們已經(jīng)完成了帶 Ajax 數(shù)據(jù)的后端渲染了。這一步最為復(fù)雜,也最為關(guān)鍵,需要反復(fù)思考和嘗試。具體渲染效果圖如下所示,源碼請(qǐng)參考這里

效果

大功告成了嗎?還沒(méi)。人們都說(shuō) SSR 能提升首屏渲染速度,下面我們對(duì)比一下看看到底是不是真的。(同樣在 Fast 3G 網(wǎng)絡(luò)條件下)。

官方思路的變形

行文至此,關(guān)于 Vue SSR Demo便已經(jīng)結(jié)束了。后面是我結(jié)合自身項(xiàng)目特點(diǎn)的一些變形,不感興趣的讀者可以不看。

第三步官方思路有什么缺點(diǎn)嗎?我認(rèn)為是有的:對(duì)老項(xiàng)目來(lái)說(shuō),改造成本比較大。需要顯式的引入 vuex,就得走 action、mutations 那一套,無(wú)論是代碼改動(dòng)量還是新人學(xué)習(xí)成本,都不低。

有什么辦法能減少對(duì)舊有前端渲染項(xiàng)目的改動(dòng)量的嗎?我是這么做的。

// store.js// action,mutations 那些都不需要了,只定義一個(gè)空 stateexport default function createStore() { return new Vuex.Store({ state: {} })}// Bar.vue// tagName 是組件實(shí)例的名字,比如 bar1、bar2、foo1 等,由 entry-server.js 注入export default { prefetchData: function (tagName) { return new Promise((resolve, reject) => {  resolve({  tagName,  data: 'Bar ajax 數(shù)據(jù)'  }); }) }}
// entry-server.jsreturn Promise.all(prefetchFns).then((res) => { // 拿到 Ajax 數(shù)據(jù)之后,手動(dòng)將數(shù)據(jù)寫入 state,不通過(guò) action,mutation 那一套 // state 內(nèi)部區(qū)分的 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// 將每個(gè)組件都需要的 computed 抽象成一個(gè) mixin,然后注入export default { computed: { prefetchData () {  let componentTag = this.$options._componentTag; // bar1、bar2、foo1  return this.$store.state[componentTag]; } }}

至此,我們就便得到了 Vue SSR 的一種變形。對(duì)于組件開(kāi)發(fā)者而言,只需要把原來(lái)的 this.fetchData 方法抽象到 prefetchData 方法,然后就可以在 DOM 中使用 {{prefetchData}} 拿到到數(shù)據(jù)了。這部分的代碼請(qǐng)參考這里

總結(jié)

Vue SSR 確實(shí)是個(gè)有趣的東西,關(guān)鍵在于靈活運(yùn)用。此 Demo 還有一個(gè)遺留問(wèn)題沒(méi)有解決:當(dāng)把 Ajax 抽象到 prefetchData,做成 SSR 之后,原先的前端渲染就失效了。能不能同一份代碼同時(shí)支持前端渲染和后端渲染呢?這樣當(dāng)后端渲染出問(wèn)題的時(shí)候,我就可以隨時(shí)切回前端渲染,便有了兜底的方案。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持武林網(wǎng)。

發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 彰武县| 虎林市| 平昌县| 大兴区| 南雄市| 怀远县| 淄博市| 肇源县| 昌宁县| 沾益县| 遂平县| 兰坪| 屏南县| 吴桥县| 合山市| 洪江市| 安龙县| 平谷区| 应用必备| 兴安盟| 衡阳县| 莎车县| 龙门县| 安溪县| 上栗县| 汝州市| 玉屏| 渭南市| 宣汉县| 博爱县| 观塘区| 平原县| 徐水县| 璧山县| 津南区| 武宁县| 新昌县| 盐边县| 遵义县| 广丰县| 星子县|