<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                [TOC] # 什么是vue-ssr SSR是Server-Side Rendering的簡寫,即由服務端負責渲染頁面直出,亦即同構應用。程序的大部分代碼都可以在服務端和客戶端運行。在服務端vue組件渲染為html字符串,在客戶端生成dom和操作dom。 <br> 能在服務端渲染為html字符串得益于vue組件結構是基于vnode的。vnode是dom的抽象表達,它不是真實的dom,它是由js對象組成的樹,每個節點代表了一個dom。因為vnode所以在服務端vue可以把js對象解析為html字符串。同樣在客戶端vnode因為是存在內存之中的,操作內存總比操作dom快的多,每次數據變化需要更新dom時,新舊vnode樹經過diff算法,計算出最小變化集,大大提高了性能。 <br> ![](https://img.kancloud.cn/60/34/603484171e562e7da1ca56b54fd4a27b_1946x892.png) <br> <br> # 實現 ## 返回html文本 ~~~JavaScript import Koa2 from 'koa'; import { createRenderer } from 'vue-server-renderer'; import Vue from 'vue'; const renderer = createRenderer(); const app = new Koa2(); /** * 應用接管路由 */ app.use(async function(ctx) { const vm = new Vue({ template:"<div>hello world</div>" }); ctx.set('Content-Type', 'text/html;charset=utf-8'); const htmlString = await renderer.renderToString(vm); ctx.body = `<html> <head> </head> <body> ${htmlString} </body> </html>`; }); app.listen(3000); ~~~ 我們現在在服務器端創建了一個`vue`實例`vm`。vm是一個對象,對象是不能直接發送給瀏覽器的,發送前必須轉換為字符串。 `vue-server-renderer` 把一個`vue`實例轉化成字符串,通過`renderer.renderToString`這個方法,將`vm`作為參數傳遞進去運行,便很輕松的返回了`vm`轉化后的字符串,如下。 ~~~text <div data-server-rendered="true">hello world</div> ~~~ 從上面的案例,可以從宏觀上把握服務器端渲染的整個脈絡. * 首先是要獲取到當前這個請求路徑是想請求哪個`vue`組件 * 將組件數據內容填充好轉化成字符串 * 最后把字符串拼接成`html`發送給前端. <br> ## 打包 這里客戶端和服務端的入口不一樣,webpack配置也不一樣 客戶端 ~~~ { mode: 'development', entry: './src/client/index.js', output: { filename: 'index.js', path: path.resolve(__dirname, 'public'), }, ... } ~~~ 服務端 ~~~ { return { target: 'node', mode: 'development', entry: './src/index.js', devtool: 'eval-source-map', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'build'), libraryTarget: 'commonjs2', }, ... } ~~~ <br> <br> ## 路由集成 在實現`srr`的任務里,主要工作是為了在客戶端發送請求后能找出當前的請求路徑是匹配哪個`vue`組件。 * 使用`createRouter()`方法創建一個路由實例對象`router`,把它注入到`Vue`實例中. * router.onready 方法把一個回調排隊,在路由完成初始導航時調用,這意味著它可以解析所有的異步進入鉤子和路由初始化相關聯的異步組件。 這可以有效確保服務端渲染時服務端和客戶端輸出的一致。 route.js ~~~ import Vue from 'vue'; import Router from 'vue-router'; import List from './pages/List'; import Search from './pages/Search'; Vue.use(Router); export const createRouter = () => { return new Router({ mode: 'history', routes: [ { path: '/list', component: List, }, { path: '/search', component: Search, }, ], }); }; export const routerReady = async (router) => { return new Promise((resolve) => { router.onReady(() => { resolve(null); }); }); }; ~~~ <br> <br> index.js * 執行`router.push(req.url)`,這一步非常關鍵.相當于告訴`Vue`實例,當前的請求路徑已經傳給你了,你快點根據路徑尋找要渲染的頁面組件. * `await routerReady(router);`執行完畢后,就已經可以得到當前請求路徑匹配的頁面組件了. * `matchedComponents.length`如果等于`0`,說明當前的請求路徑和我們定義的路由沒有一個匹配上,那么這里應該要定制一個精美的`404`頁面返回給瀏覽器. * `matchedComponents.length`不等于`0`,說明當前的`vm`已經根據請求路徑讓匹配的頁面組件占據了視口.接下來只需要將`vm`轉化成字符串發送給瀏覽器就可以了. ~~~ import Koa2 from 'koa'; // 靜態文件處理 import staticFiles from 'koa-static'; import { createRenderer } from 'vue-server-renderer'; import Vue from 'vue'; //?Vue部分 import App from './App.vue'; import { createRouter, routerReady } from './route.js'; const renderer = createRenderer(); const app = new Koa2(); app.use(staticFiles('public')); app.use(async function (ctx) { const req = ctx.request; // 創建路由 const router = createRouter(); const vm = new Vue({ // 添加路由 router, render: (h) => h(App), }); // 告訴vue 渲染 當前所需組件 router.push(req.url); // 等到 router 鉤子函數解析完 await routerReady(router); //獲取匹配的頁面組件 const matchedComponents = router.getMatchedComponents(); if (!matchedComponents.length) { ctx.body = '沒有找到該網頁,404'; return; } ctx.set('Content-Type', 'text/html;charset=utf-8'); let htmlString try { htmlString = await renderer.renderToString(vm); } catch (error) { ctx.status = 500; ctx.body = 'Internal Server Error'; } ctx.body = `<html> <head> </head> <body> ${htmlString} </body> // 引入頁面js <script src="./index.js"></script> </html>`; }); app.listen(3000); ~~~ <br> client/index.js ~~~ import Vue from 'vue'; import VueMeta from 'vue-meta'; import App from '../App.vue'; import { createRouter } from '../route'; Vue.config.productionTip = false; Vue.use(VueMeta); //創建路由 const router = createRouter(); new Vue({ router, render: (h) => h(App), }).$mount('#root', true); ~~~ <br> <br> ## Vuex集成 路由集成后雖然能夠根據路徑渲染指定的頁面組件,但是服務器渲染也存在局限性。 <br> 比如你在頁面組件模板上加一個`v-click`事件,結果會發現頁面在瀏覽器上渲染完畢后事件無法響應,這樣肯定會違背我們的初衷。事件綁定, <br> 點擊鏈接跳轉這些都是瀏覽器賦予的能力。因此可以借助客戶端渲染來幫助我們走出困境。 <br> 整個流程可以設計如下. * 瀏覽器輸入鏈接請求服務器,服務器端將包含頁面內容的`html`返回,但是在`html`文件下要加上客戶端渲染的`js`腳本. * `html`開始在瀏覽器上加載,頁面上已經呈現出靜態內容了.當線程走到`html`文件下的`script`標簽,開始請求客戶端渲染的腳本并執行. * 此時客戶端腳本里面的`vue`實例開始接管了整個應用,它開始賦予原本后端返回的靜態`html`各種能力,比如讓標簽上的事件綁定開始生效. <br> store/index.js ~~~ import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export function createStore() { return new Vuex.Store({ state: { list: [], name: 'kay', }, actions: { getList({ commit }, params) { return new Promise((resolve)=>{ commit("setList",[{ name:"廣州" },{ name:"深圳" }]); resolve(); },2000) }, }, mutations: { setList(state, data) { state.list = data || []; }, }, }); } ~~~ page/list/index.vue ~~~ <template> <div class="list"> <p>當前頁:列表頁</p> <a @click="jumpSearch()">go搜索頁</a> <ul> <li v-for="item in list" :key="item.name"> <p>城市: {{ item.name }}</p> </li> </ul> </div> </template> <script> export default { // 服務端獲取異步數據公共方法 asyncData({ store, route }) { return store.dispatch("getList"); }, }; </script> ~~~ index.js ~~~ import Koa2 from 'koa'; import staticFiles from 'koa-static'; import { createRenderer } from 'vue-server-renderer'; import Vue from 'vue'; import App from './App.vue'; import { createRouter, routerReady } from './route.js'; import { createStore } from './vuex/store'; const renderer = createRenderer(); const app = new Koa2(); app.use(staticFiles('public')); app.use(async function (ctx) { const req = ctx.request; const router = createRouter(); // 創建Store const store = createStore(); const vm = new Vue({ router, store, render: (h) => h(App), }); router.push(req.url); await routerReady(router); const matchedComponents = router.getMatchedComponents(); if (!matchedComponents.length) { ctx.body = '沒有找到該網頁,404'; return; } ctx.set('Content-Type', 'text/html;charset=utf-8'); let htmlString try { // 匹配到的組件執行 asyncData方法,調用dispatch來更新store await Promise.all( matchedComponents.map((Component) => { if (Component.asyncData) { Component.asyncData({ store, route: router.currentRoute, }); } }) ); htmlString = await renderer.renderToString(vm); } catch (error) { ctx.status = 500; ctx.body = 'Internal Server Error'; } ctx.body = `<html> <head> </head> <body> ${htmlString} </body> <script src="./index.js"></script> </html>`; }); app.listen(3000); ~~~ <br> ### 脫水 現在ssr和客戶端都配置了vuex,但區別是服務端的store里面放著List.vue需要的遠程請求的數據,而客戶端的store是空的. <br> srr返回的靜態html是帶著城市列表的,一旦客戶端的vue接管了整個應用就會展開各種各樣的初始化操作.客戶端也要配置vuex,由于它的數據倉庫是空的所以重新引發了頁面渲染.致使原本來含有城市列表的頁面部分消失了. <br> 為了解決這個問題,就要想辦法讓ssr遠程請求來的數據也給客戶端的store發一份.這樣客戶端即使接管了應用,但發現此時store存儲的城市列表數據和頁面保持一致也不會造成閃爍問題. ~~~ ctx.body = `<html> <head> </head> <body> ${htmlString} // 注入服務端strore的數據 <script> var context = { state: ${JSON.stringify(store.state)} } </script> <script src="/index.js"></script> </body> </html>`; ~~~ ### 注水 服務器端將數據放入了js腳本里,客戶端此時就可以輕松拿到這份數據. <Br> 在客戶端入口文件里加上 store.replaceState(window.context.state); 如果發現window.context.state存在,就把這部分數據作為vuex的初始數據,這個過程稱之為注水. client/index.js ~~~ import Vue from 'vue'; import App from '../App.vue'; import { createRouter } from '../route'; import VueMeta from 'vue-meta'; import { createStore } from '../vuex/store'; Vue.config.productionTip = false; Vue.use(VueMeta); const router = createRouter(); // 創建Store const store = createStore(); // 若有 window.context.state,更新客戶端store if (window.context && window.context.state) { store.replaceState(window.context.state); } new Vue({ router, store, render: (h) => h(App), }).$mount('#root', true); ~~~ <br> <br> ## 裝載真實數據 上面在`vuex`里是使用定時器模擬的請求數據,接下來利用網上的一些開放`API`接入真實的數據. 對`vuex`里的`action`方法做如下修改. ~~~text actions: { getList({ commit }, params) { const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0'; return axios.get(url).then((res)=>{ commit("setList",res.data.location); }) } } ~~~ <br> `asyncData`一運行就會走到上面`actions`里面的`getList`,它就會對上面那個`url`地址發起請求.但仔細觀察發現這個`url`是沒有寫域名的,這樣訪問肯定會報錯. 那把遠程域名給它加上去行不行呢?如果這樣硬加是會出現問題的.有一種場景就是客戶端接管應用它也可以調用`getList`方法,我們寫的這部分`vuex`代碼可是服務端和客戶端共用的.那如果客戶端直接訪問帶有遠程域名的路徑就會引起跨域. 那如何解決這一問題呢?這里的`url`最好不要加域名,以`/`開頭.那樣客戶端訪問這個路徑就會引向`node`服務器.此時只要加一個接口代理轉發就搞定了. ~~~text import proxy from 'koa-server-http-proxy'; export const proxyHanlder = (app)=>{ app.use(proxy('/api', { target: 'https://geoapi.qweather.com', //網上尋找的開放API接口,支持返回地理數據. pathRewrite: { '^/api': '' }, changeOrigin: true })); } ~~~ 定義一個中間件函數,在執行服務器端渲染前添加到`koa2`上. 這樣`node`服務器只要看到以`/api`開頭的請求路徑就會轉發到遠程地址上獲取數據,不會再走后面服務器端渲染的邏輯. ### 服務器端路徑請求的問題 使用上面的代理轉發之后又會帶來新的問題,設想一種場景.如果瀏覽器輸入`localhost:3000/list`后,`node`解析請求發現要加載`List.vue`這個頁面組件,而這個組件又有一個`asyncData`異步方法,因此就運行異步方法獲取數據. ~~~text actions: { getList({ commit }, params) { const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0'; return axios.get(url).then((res)=>{ commit("setList",res.data.location); }) } } ~~~ 這個異步方法就是`getList`,注意此時執行這段腳本的是`node`服務器,不是客戶端的瀏覽器. 瀏覽器如果請求以`/`開頭的`url`,請求會發給`node`服務器.`node`服務器現在需要自己請求自己,只要請求了自己設置的代理就能把請求轉發給遠程服務器,而如今`node`服務器請求以`/`開頭的路徑是絕對無法請求到自己的,這個時候只能用絕對路徑. 我們上面提到這部分的`vuex`代碼是客戶端和服務端共用的,最好不用絕對路徑寫死.還有一個更優雅的方法,就是對`axios`的`baseURL`進行配置生成帶有域名的`axios`實例來請求.那這部分代碼就可以改成如下. ~~~text export function createStore(_axios) { return new Vuex.Store({ state: { list: [], name: 'kay', }, actions: { getList({ commit }, params) { const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0'; return _axios.get(url).then((res)=>{ commit("setList",res.data.location); }) }, }, mutations: { setList(state, data) { state.list = data || []; }, }, }); } ~~~ `_axios`是配置基礎域名后的實例對象,客戶端會生成一個`_axios`,服務端也會生成一個,只不過客戶端是不用配置`baseURL`的. ~~~text import axios from "axios"; //util/getAxios.js /** * 獲取客戶端axios實例 */ export const getClientAxios = ()=>{ const instance = axios.create({ timeout: 3000, }); return instance; } /** * 獲取服務器端axios實例 */ export const getServerAxios = (ctx)=>{ const instance = axios.create({ timeout: 3000, baseURL: 'http://localhost:3000' }); return instance; } ~~~ <br> index.js ~~~ import { getServerAxios } from "./util/getAxios"; import { proxyHanlder } from "./middleware/proxy"; proxyHanlder(app); app.use(async function (ctx) { // ... const store = createStore(getServerAxios(ctx)); }) ~~~ 通過生成兩份`axios`實例既保持了`vuex`代碼的統一性,另外還解決了`node`服務器自己訪問不了自己的問題. <br> ### cookie如何處理 使用了接口代理之后,怎么確保每次接口轉發都能把`cookie`也一并傳給遠程的服務器.可以按如下配置. 在`ssr`的入口文件里. ~~~text ***省略 ** * 應用接管路由,服務器端渲染代碼 */ app.use(async function(ctx) { const req = ctx.request; //圖標直接返回 if (req.path === '/favicon.ico') { ctx.body = ''; return false; } const router = createRouter(); //創建路由 const store = createStore(getServerAxios(ctx)); //創建數據倉庫 ***省略 }) ~~~ 在創建`ctx`和`axios`實例的時候將`ctx`傳遞進去. ~~~text /** * 獲取服務器端axios實例 */ export const getServerAxios = (ctx)=>{ const instance = axios.create({ timeout: 3000, headers:{ cookie:ctx.req.headers.cookie || "" }, baseURL: 'http://localhost:3000' }); return instance; } ~~~ 將`ctx`中的`cookie`取出來賦值給`axios`的`headers`,這樣就確保`cookie`被攜帶上了. <br> <br> ## 樣式處理 `.vue`頁面的文件通常把代碼分成三個標簽`<template>`,`<script>`和`<style>`. `<style scoped lang="scss"></style>`上還可以添加一些屬性. 和客戶端渲染相比,實現`ssr`的過程要多處理一步.即將`<style>`里面的樣式內容提取出來,再渲染到`html`的`<head>`里面. 在`ssr`入口文件`index.js`添加如下代碼. ~~~text ...省略 const context = {}; //創建一個上下文對象 htmlString = await renderer.renderToString(vm, context); ctx.body = `<html> <head> ${context.styles ? context.styles : ''} </head> <body> ${htmlString} <script> var context = { state: ${JSON.stringify(store.state)} } </script> <script src="./bundle.js"></script> </body> </html>`; ~~~ 服務端提取樣式的過程非常簡單,定義一個上下文對象`context`. `renderer.renderToString`函數的第二個參數里傳入`context`,該函數執行完畢后,`context`對象的`styles`屬性就會擁有頁面組件的樣式.最后將這份樣式拼接到`html`的`head`頭部里即可. <br> ## **Head信息處理** 常規的`html`文件的`head`里面不僅包含樣式,它可能還需要設置`<title>`和`<meta />`.如何針對每個頁面設置個性化的頭部信息,可以利用`vue-meta`插件. 現在需要給`List.vue`頁面組件添加一些頭信息,可以按如下設置. ~~~text <script> export default { metaInfo: { title: "列表頁", meta: [ { charset: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, ], }, asyncData({ store, route }) { return store.dispatch("getList"); } ...省略 } ~~~ 在導出的對象上添加一個屬性`metaInfo`,在其中分別設置`title`和`meta`; 在`ssr`的入口文件處加入如下代碼. ~~~text import Koa2 from 'koa'; import Vue from 'vue'; import App from './App.vue'; import VueMeta from 'vue-meta'; Vue.use(VueMeta); /** * 應用接管路由 */ app.use(async function(ctx) { ...省略 const vm = new Vue({ router, store, render: (h) => h(App), }); const meta_obj = vm.$meta(); // 生成的頭信息 router.push(req.url); ...省略 htmlString = await renderer.renderToString(vm, context); const result = meta_obj.inject(); const { title, meta } = result; ctx.body = `<html> <head> ${title ? title.text() : ''} ${meta ? meta.text() : ''} ${context.styles ? context.styles : ''} </head> <body> ${htmlString} <script> var context = { state: ${JSON.stringify(store.state)} } </script> <script src="./index.js"></script> </body> </html>`; }); app.listen(3000); ~~~ 通過`vm.$meta()`生成頭信息`meta_obj`,待到`vue`實例加載完畢后,執行`meta_obj.inject()`獲取被渲染頁面組件的`meta`和`title`數據,再將它們填充到`html`字符串即可. # 參考資料 [從原理上實現Vue的ssr渲染](https://zhuanlan.zhihu.com/p/346674458)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看