1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        服務(wù)端渲染SSR及實現(xiàn)原理

        共 10173字,需瀏覽 21分鐘

         ·

        2022-01-16 14:45


        本文首發(fā)于政采云前端團隊博客:服務(wù)端渲染SSR及實現(xiàn)原理

        https://www.zoo.team/article/web-ssr

        前言

        在日常前端開發(fā)中,在需要首屏渲染速度優(yōu)化的場景下,大家或多或少都聽到過服務(wù)端渲染( SSR )。本文將結(jié)合 Vue 來對 SSR 的實現(xiàn)邏輯來進行解讀。通過閱讀本文你將了解到:

        • 服務(wù)端渲染的使用場景
        • Vue SSR 的實現(xiàn)原理
        • 可開箱即用的 SSR 腳手架

        服務(wù)端渲染

        服務(wù)端渲染 SSR (Server-Side Rendering),是指在服務(wù)端完成頁面的 html 拼接處理, 然后再發(fā)送給瀏覽器,將不具有交互能力的 html 結(jié)構(gòu)綁定事件和狀態(tài),在客戶端展示為具有完整交互能力的應(yīng)用程序。

        適用場景

        以下兩種情況 SSR 可以提供很好的場景支持

        • 需更好的支持 SEO

          優(yōu)勢在于同步。搜索引擎爬蟲是不會等待異步請求數(shù)據(jù)結(jié)束后再抓取信息的,如果 SEO 對應(yīng)用程序至關(guān)重要,但你的頁面又是異步請求數(shù)據(jù),那 SSR 可以幫助你很好的解決這個問題。

        • 需更快的到達(dá)時間

          優(yōu)勢在于慢網(wǎng)絡(luò)和運行緩慢的設(shè)備場景。傳統(tǒng) SPA 需完整的 JS 下載完成才可執(zhí)行,而SSR 服務(wù)器渲染標(biāo)記在服務(wù)端渲染 html 后即可顯示,用戶會更快的看到首屏渲染頁面。如果首屏渲染時間轉(zhuǎn)化率對應(yīng)用程序至關(guān)重要,那可以使用 SSR 來優(yōu)化。

        不適用場景

        以下三種場景 SSR 使用需要慎重

        • 同構(gòu)資源的處理

          劣勢在于程序需要具有通用性。結(jié)合 Vue 的鉤子來說,能在 SSR 中調(diào)用的生命周期只有 beforeCreatecreated,這就導(dǎo)致在使用三方 API 時必須保證運行不報錯。在三方庫的引用時需要特殊處理使其支持服務(wù)端和客戶端都可運行。

        • 部署構(gòu)建配置資源的支持

          劣勢在于運行環(huán)境單一。程序需處于 node.js server 運行環(huán)境。

        • 服務(wù)器更多的緩存準(zhǔn)備

          劣勢在于高流量場景需采用緩存策略。應(yīng)用代碼需在雙端運行解析,cpu 性能消耗更大,負(fù)載均衡和多場景緩存處理比 SPA 做更多準(zhǔn)備。

        我們來結(jié)合 Vue.js 來看看 Vue 是如何實現(xiàn) SSR 的。

        Vue SSR 的實現(xiàn)原理

        先決條件

        組件基于 Vnode 來實現(xiàn)渲染

        VNode 本身是 js 對象,兼容性極強,不依賴當(dāng)前的執(zhí)行的環(huán)境,從而可以在服務(wù)端渲染及原生渲染。虛擬 DOM 頻繁修改,最后比較出真實 DOM 需要更改的地方,可以達(dá)到局部渲染的目的,減少性能損耗。

        vue-server-renderer

        是一個具有獨立渲染應(yīng)用程序能力的包,是 Vue 服務(wù)端渲染的核心代碼。

        本文下面的源碼也結(jié)合這個包展開,此處不多冗余介紹。

        SSR 渲染架構(gòu)

        我們結(jié)合官網(wǎng)圖項目架構(gòu)兩個維度來整體了解一下 SSR 全貌

        項目架構(gòu)

        src
        ├──?components
        ├──?App.vue
        ├──?app.js?----通用?entry
        ├──?entry-client.js?----僅運行于瀏覽器
        └──?entry-server.js?----僅運行于服務(wù)器

        app.js導(dǎo)出 createApp 函數(shù)工廠,此函數(shù)是可以被重復(fù)執(zhí)行的,從根 Vue 實例注入,用于創(chuàng)建 router,store 以及應(yīng)用程序?qū)嵗?/p>

        import?Vue?from?'vue'
        import?App?from?'./App.vue'
        //?導(dǎo)出一個工廠函數(shù),用于創(chuàng)建新的應(yīng)用程序、router?和?store?實例
        export?function?createApp?()?{
        ??const?app?=?new?Vue({
        ????render:?h?=>?h(App)
        ??})
        ??return?{?app?}
        }

        entry-client.js負(fù)責(zé)創(chuàng)建應(yīng)用程序,掛載實例 DOM ,僅運行于瀏覽器。

        import?{?createApp?}?from?'./app'
        const?{?app?}?=?createApp()
        //?#app?為根元素,名稱可替換
        app.$mount('#app')

        entry-server.js創(chuàng)建返回應(yīng)用實例,同時還會進行路由匹配和數(shù)據(jù)的預(yù)處理,僅運行于服務(wù)器。

        import?{?createApp?}?from?'./app'
        export?default?context?=>?{
        ??const?{?app?}?=?createApp()
        ??return?app
        }

        服務(wù)端和客戶端代碼編寫原則

        作為同構(gòu)框架,應(yīng)用代碼編譯過程 Vue SSR 提供了兩個編譯入口,來作為抹平由于環(huán)境不同的代碼差異。Client entry 和 Server entry 中編寫代碼邏輯的區(qū)分有兩條原則

        1. 通用型代碼 可通用性的代碼,由于鑒權(quán)邏輯和網(wǎng)關(guān)配置不同,需要在 webpack resolve.alias 中配置不同的模塊環(huán)境應(yīng)用。

        2. 非通用性代碼 Client entry ?負(fù)責(zé)掛載 DOM 節(jié)點代碼,以及三方包引入和具有兼容性庫的加載。

        Server entry 只生成 Vue 對象。

        兩個編譯產(chǎn)物

        經(jīng)過 webpack 打包之后會有兩個 bundle 產(chǎn)物

        server bundle 用于生成 vue-ssr-server-bundle.json,我們熟悉的 sourceMap 和需要在服務(wù)端運行的代碼列表都在這個產(chǎn)物中。

        vue-SSR-server-bundle.json
        {?
        ??"entry":?,?
        ??"files":?{
        ??? A:包含了所有要在服務(wù)端運行的代碼列表
        ??? B:入口文件
        ??}?
        }

        client Bundle 用于生成 vue-SSR-client-manifest.json,包含所有的靜態(tài)資源,首次渲染需要加載的 script 標(biāo)簽,以及需要在客戶端運行的代碼。

        vue-SSR-client-manifest.json
        {?
        ??"publicPath":?公共資源路徑文件地址,?
        ??"all":?資源列表
        ??"initial":輸出?html?字符串
        ??"async":?異步加載組件集合
        ??"modules":?moduleIdentifier?和?all?數(shù)組中文件的映射關(guān)系
        }

        先決條件中我們提到了一個重要的包 vue-server-renderer,那我們來重點看看這個包里面的值得我們學(xué)習(xí)關(guān)注的內(nèi)容。

        vue-server-renderer

        是 Vue SSR 的核心代碼,值得我們關(guān)注的是應(yīng)用初始化應(yīng)用輸出。兩個階段提供了完整的應(yīng)用層代碼編譯和組裝邏輯。

        應(yīng)用初始化

        在應(yīng)用初始化過程中,重點展開介紹實例化流程防止交叉污染。

        首先我們先來看看一個 Vue SSR 的應(yīng)用是如何被初始化的。

        實例化流程

        1. 生成 Vue 對象
        const?Vue?=?require('vue')
        const?app?=?new?Vue()
        1. 生成 renderer,值得關(guān)注的兩個對象 render 和 templateRenderer
        const?renderer?=?require('vue-server-renderer').createRenderer()
        // createRenderer 函數(shù)中有兩個重要的對象:render 和 templateRenderer
        function?createRenderer?(ref)?{
        ??//?render:?渲染?html?組件
        ??var?render?=?createRenderFunction(modules,?directives,?isUnaryTag,?cache);
        ??//?templateRenderer:?模版渲染,clientManifest?文件
        ??var?templateRenderer?=?new?TemplateRenderer({
        ????template:?template,
        ????inject:?inject,
        ????shouldPreload:?shouldPreload,
        ????shouldPrefetch:?shouldPrefetch,
        ????clientManifest:?clientManifest,
        ????serializer:?serializer
        ??});

        經(jīng)過這個過程的 render 和 templateRenderer 并沒有被調(diào)用,這兩個函數(shù)真正的調(diào)用是在項目實例化 createBundleRenderer 函數(shù)的時候,即第三步創(chuàng)建的函數(shù)。

        1. 創(chuàng)建沙盒 vm,實例化 Vue 的入口文件
        var?vm?=?require('vm');
        //?調(diào)用?createBundleRunner?函數(shù)實例對象,rendererOptions?支持可配置
        var?run?=?createBundleRunner(?
        ??entry,?----入口文件集合
        ??files,?----打包文件集合
        ??basedir,?
        ? rendererOptions.runInNewContext。
        );}

        在 createBundleRunner 方法的源碼到其實舉例了一個叫 compileModule 的一個方法,這個方法中有兩個函數(shù):getCompiledScriptevaluateModule

        function?createBundleRunner?(entry,?files,?basedir,?runInNewContext)?{
        ??//觸發(fā)?compileModule?方法,找到?webpack?編譯形成的?code
        ??var?evaluate?=?compileModule(files,?basedir,?runInNewContext);
        }

        getCompiledScript:編譯 wrapper ,找到入口文件的 files 文件名及 script 腳本的編譯執(zhí)行

        function?getCompiledScript?(filename)?{
        ????if?(compiledScripts[filename])?{
        ??????return?compiledScripts[filename]
        ????}
        ????//?在入口文件?files?中找到對應(yīng)的文件名稱
        ????var?code?=?files[filename];
        ????var?wrapper?=?NativeModule.wrap(code);
        ????//?在沙盒上下文中執(zhí)行構(gòu)建?script?腳本
        ????var?script?=?new?vm.Script(wrapper,?{
        ??????filename:?filename,
        ??????displayErrors:?true
        ????});
        ????compiledScripts[filename]?=?script;
        ????return?script
        ??}

        evaluateModule:根據(jù) runInThisContext 中的配置項來決定是在當(dāng)前上下文執(zhí)行還是單獨上下文執(zhí)行。

        function?evaluateModule?(filename,?sandbox,?evaluatedFiles)?{
        ????if?(?evaluatedFiles?===?void?0?)?evaluatedFiles?=?{};
        ????if?(evaluatedFiles[filename])?{
        ??????return?evaluatedFiles[filename]
        ????}
        ????var?script?=?getCompiledScript(filename);
        ????//?用于判斷是在當(dāng)前的那種模式下面執(zhí)行沙盒上下文,此時存在兩個函數(shù)的相互調(diào)用
        ????var?compiledWrapper?=?runInNewContext?===?false
        ????????script.runInThisContext()
        ??????:?script.runInNewContext(sandbox);
        ????//?m:?函數(shù)導(dǎo)出的?exports?數(shù)據(jù)
        ????var?m?=?{?exports:?{}};
        ????//?r:?替代原生?require?用來解析?bundle?中通過?require?函數(shù)引用的模塊
        ????var?r?=?function?(file)?{
        ??????...
        ??????return?require(file)
        ????};
        ???}

        上述的函數(shù)執(zhí)行完成之后會調(diào)用 compiledWrapper.call,傳參對應(yīng)上面的 exports、require、module, 我們就能拿到入口函數(shù)。

        1. 錯誤拋出容錯和全局錯誤監(jiān)聽 renderToString: 在沒有 cb 函數(shù)時做了 promise 的返回,那說明我們在調(diào)用次函數(shù)的時候可以直接做 try catch的處理,用于全局錯誤的拋出容錯。
        renderToString:?function?(context,?cb)?{
        ????var?assign;
        ????if?(typeof?context?===?'function')?{
        ??????cb?=?context;
        ??????context?=?{};
        ????}
        ????var?promise;
        ????if?(!cb)?{
        ??????((assign?=?createPromiseCallback(),?promise?=?assign.promise,?cb?=?assign.cb));
        ????}
        ????...
        ????return?promise
        ??},
        }

        renderToStream:對拋錯做了監(jiān)聽機制, 拋錯的鉤子函數(shù)將在這個方法中觸發(fā)。

        ?renderToStream:?function?(context)?{
        ????var?res?=?new?PassThrough();
        ????run(context).catch(function?(err)?{
        ??????rewriteErrorTrace(err,?maps);
        ??????//?此處做了監(jiān)聽器的容錯
        ??????process.nextTick(function?()?{
        ????????res.emit('error',?err);
        ??????});
        ????}).then(function?(app)?{
        ??????if?(app)?{
        ????????var?renderStream?=?renderer.renderToStream(app,?context);
        ????????...?
        ??????}
        ????}
        ?}

        防止交叉污染

        Node.js 服務(wù)器是一個長期運行的進程,在客戶端編寫的代碼在進入進程時,變量的上下文將會被保留,導(dǎo)致交叉請求狀態(tài)污染。因此不可共享一個實例,所以說 createApp 是一個可被重復(fù)執(zhí)行的函數(shù)。其實在包內(nèi)部,變量之間也存在防止交叉污染的能力。

        防止交叉污染的能力是由 rendererOptions.runInNewContext 這個配置項來提供的,這個配置支持 true, false,和 once 三種配置項傳入。

        //?rendererOptions.runInNewContext?可配置項如下
        ??true:?
        ??新上下文模式:創(chuàng)建新上下文并重新評估捆綁包在每個渲染上。
        ??確保每個應(yīng)用程序的整個應(yīng)用程序狀態(tài)都是新的渲染,但會產(chǎn)生額外的評估成本。
        ??false:
        ??直接模式:
        ??每次渲染時,它只調(diào)用導(dǎo)出的函數(shù)。而不是在上重新評估整個捆綁包
        ??模塊評估成本較高,但需要結(jié)構(gòu)化源代碼
        ??once:?
        ??初始上下文模式
        ??僅用于收集可能的非組件 vue 樣式加載程序注入的樣式。

        特別說明一下 false 和 once 的場景, 為了防止交叉污染,在渲染的過程中對作用域要求很嚴(yán)格,以此來保證在不同的對象彼此之間不會形成污染。

        if?(!runner)?{
        ???var?sandbox?=?runInNewContext?===?'once'
        ????????createSandbox()
        ??????:?global;
        ????initialContext?=?sandbox.__VUE_SSR_CONTEXT__?=?{};
        ????runner?=?evaluate(entry,?sandbox);
        ????//在后續(xù)渲染中,_VUE_SSR_CONTEXT_uu?將不可用
        ????//防止交叉污染
        ????delete?sandbox.__VUE_SSR_CONTEXT__;
        ????if?(typeof?runner?!==?'function')?{
        ??????throw?new?Error(
        ????????'bundle?export?should?be?a?function?when?using?'?+
        ????????'{?runInNewContext:?false?}.'
        ??????)
        ????}
        ??}

        應(yīng)用輸出

        在應(yīng)用輸出這個階段中,SSR 將更多側(cè)重加載腳本內(nèi)容模版渲染,在模版渲染時在代碼中是否定義過模版引擎源碼將提供不同的 html 拼接結(jié)構(gòu)

        加載腳本內(nèi)容

        此過程會將上個階段構(gòu)造的 reader 和 templateRender 方法實現(xiàn)數(shù)據(jù)綁定。

        templateRenderer:負(fù)責(zé) html 封裝,其原型上會有如下幾個方法, 這些函數(shù)的作用如下圖。值得一提的是:bindRenderFns 函數(shù)是將 4 個 render 函數(shù)綁定到用戶上下文的 context 中,用戶在拿到這些內(nèi)容之后就可以做內(nèi)容的自定義組裝和渲染。

        render: 函數(shù)會被遞歸調(diào)用按照從父到子的順序,將組件全部轉(zhuǎn)化為 html。

        function?createRenderFunction?(
        ??modules,
        ??directives,
        ??isUnaryTag,
        ??cache
        )?
        {
        ??return?function?render?(
        ????component,
        ????write,
        ????userContext,
        ????done
        ??
        )?
        {
        ????warned?=?Object.create(null);
        ????var?context?=?new?RenderContext({
        ??????activeInstance:?component,
        ??????userContext:?userContext,
        ??????write:?write,?done:?done,?renderNode:?renderNode,
        ??????isUnaryTag:?isUnaryTag,?modules:?modules,?directives:?directives,
        ??????cache:?cache
        ????});
        ????installSSRHelpers(component);
        ????normalizeRender(component);
        ????//?渲染?node?節(jié)點,綁定用戶作用上下文
        ????var?resolve?=?function?()?{
        ??????renderNode(component._render(),?true,?context);
        ????};
        ????//?等待組件?serverPrefetch?執(zhí)行完成之后,_render?生成子節(jié)點的?vnode?進行渲染
        ????waitForServerPrefetch(component,?resolve,?done);
        ??}
        }

        在經(jīng)過上面的編譯流程之后,我們已經(jīng)拿到了 html 字符串,但如果要在瀏覽器中展示頁面還需js, css 等標(biāo)簽與這個 html 組裝成一個完整的報文輸出到瀏覽器中, 因此需要模版渲染階段來將這些元素實現(xiàn)組裝。

        模版渲染

        經(jīng)過應(yīng)用初始化階段,代碼被編譯獲取了 html 字符串,context 渲染需要依賴的 templateRenderer.prototype.bindRenderFns 中綁定的 state, script , styles 等資源。

        TemplateRenderer.prototype.bindRenderFns?=?function?bindRenderFns?(context)?{
        ??var?renderer?=?this
        ??;['ResourceHints',?'State',?'Scripts',?'Styles'].forEach(function?(type)?{
        ????context[("render"?+?type)]?=?renderer[("render"?+?type)].bind(renderer,?context);
        ??});
        ? context.getPreloadFiles = r**erer.ge****:**reloadFiles.bind(renderer, context);
        };

        在具體渲染模版時,會有以下兩種情況:

        • 未定義模版引擎 渲染結(jié)果會被直接返回給 renderToString 的回調(diào)函數(shù),而頁面所需要的腳本依賴我們通過用戶上下文 context 的 renderStyles,renderResourceHints、renderState、renderScripts 這些函數(shù)分別獲得。

        • 定義了模版引擎 templateRender 會幫助我們進行 html 組裝

        TemplateRenderer.prototype.render?=?function?render?(content,?context)?{
        //?parsedTemplate?用于解析函數(shù)得到的包含三個部分的?compile?對象,
        //?按照順序進行字符串模版的拼接
        ??var?template?=?this.parsedTemplate;
        ??if?(!template)?{
        ????throw?new?Error('render?cannot?be?called?without?a?template.')
        ??}
        ??context?=?context?||?{};
        ?
        ??if?(typeof?template?===?'function')?{
        ????return?template(content,?context)
        ??}
        ?
        ??if?(this.inject)?{
        ????return?(
        ??????template.head(context)?+
        ??????(context.head?||?'')?+
        ??????this.renderResourceHints(context)?+
        ??????this.renderStyles(context)?+
        ??????template.neck(context)?+
        ??????content?+
        ??????this.renderState(context)?+
        ??????this.renderScripts(context)?+
        ??????template.tail(context)
        ????)
        ??}?else?{
        ????...
        ??}
        };

        至此我們了解了 Vue SSR 的整體架構(gòu)邏輯和 vue-server-renderer 的核心代碼,當(dāng)然 SSR 也是有很多開箱即用的腳手架來供我們選擇的。

        開箱即用的SSR腳手架

        目前前端流行的三種技術(shù)棧 React, Vue 和 Angula,已經(jīng)孵化出對應(yīng)的服務(wù)端渲染框架,開箱即用,感興趣的同學(xué)可以自主學(xué)習(xí)使用。

        • React: Next.js
        • Vue: ?Nuxt.js
        • Angula: Nest.js

        總結(jié)

        服務(wù)端渲染 ( SSR ) 是一個同構(gòu)程序,是否使用 SSR 取決于內(nèi)容到達(dá)時間對應(yīng)用程序的重要程度。如果對初始加載的幾百毫秒可接受,SSR 的使用就有點小題大做了。

        對于源碼的學(xué)習(xí)可以幫助更好借鑒優(yōu)秀的程序?qū)懛ê图ぐl(fā)對日常代碼編程架構(gòu)的思考,如果你更傾向箱即用的解決方案,那可以使用現(xiàn)有的 SSR 腳手架來搭建項目,這些腳手架的模版抽象和額外的功能擴展可以提供平滑的開箱體驗。

        參考文獻

        • Vue SSR 官網(wǎng) (https://ssr.vuejs.org/zh)
        • Vue 使用指南 (https://www.w3cschool.cn/vuessr/vuessr-jep83epx.html)
        • Vue SSR 源碼解析 (https://juejin.cn/post/6844903812700831757)

        看完兩件事

        如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我兩件小事

        1.點個「在看」,讓更多人也能看到這篇內(nèi)容(點了在看」,bug -1 ??

        2.關(guān)注公眾號「前端Sharing」,持續(xù)為你推送精選好文

        瀏覽 53
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            亚洲中文字幕在线观看视频 | 让人看了下面流水的视频 | 国产高清黄色视频 | 公交车她被揉的开始呻吟起来视频 | 波多野结衣三级在线观看 | 五月天色片 | 国产男女激情视频 | 亚洲丁香婷婷 | 国语对白刺激高潮videos | 久久久天堂国产精品女人 |