1. 程式人生 > >基於TVUE框架在中型移動端專案的直出同構實踐

基於TVUE框架在中型移動端專案的直出同構實踐

一、前言

TVUE框架是WONDER和harryxiang、mitnickliu、justynchen、yucongchen、roamye等小夥伴在vuejs框架基礎上結合業務本身做的一系列優化,封裝,改進的框架實踐,同時也學習借鑑了部分企鵝動漫專案組的一些優秀的思想。包含腳手架,基於QUI的VUE元件,最新的JS語法特性,PWA,內建SONIC加速方案,配套可擴充套件的編譯系統等。因為主要語言是用typescript編寫,所以故命名為「TVUE」框架,本文只闡述和直出同構相關部分的內容,其它框架內的內容另行介紹。

在WONDER的《vuejs+ts+webpack2專案實戰》中,我們SNG增值產品部個性化商城業務已經用上了基於typescript、vuejs、webpack2(現在應該是webpack3)、gulp的一整套開發流程。在之前的實踐中,我們是基於純前端的VUE使用,即CDN或伺服器返回純框架,非同步JS渲染整個頁面。不過這裡缺乏頁面直出&同構的實踐場景。中型移動端專案的最佳實踐,還是基於首屏頁面直出,其它屏以元件形式非同步載入的方式為佳,再結合比較成熟的SONIC加速方案提升頁面的開啟速度,提升使用者體驗,而且對SEO支援友好。

二、技術選型

大方向的技術選型WONDER在《vuejs+ts+webpack2專案實戰》中已經闡述得非常清楚了。具體細節選型,結合我們自身業務,有選擇的使用VUE提供的全家桶。

1、是否使用vue-router。根據我們自身業務的場景,比較適合用多頁面應用,路由採用後端路由,我們的後端server是TSW,後端框架是koa。使用koa在middleware中編寫router功能即可。所以我們的業務不太依賴vue-router,而且vue-router部分也可以通過快取和非同步元件自行實現。

2、是否使用vuex。對於我們的業務屬於中型專案,且我們的業務屬於多頁面應用,間接地把業務進行了二次細分。那麼VUEX反而繁瑣和不靈活,VUEX對於多頁的支援也需要改造。所以在我們的業務中,元件的傳遞都是通過props和global event bus來實現,足矣滿足我們的日常需求。

3、是否需要後端webpack打包。前端webpack打包肯定是必要的,一是檔案模組依賴的處理,二是各種loader語法的轉換。後端是否要webpack打包這個WONDER認為可選。不打包在後端來說也是沒有問題的。打包也可以,就是釋出檔案少,扔到伺服器即可用。可能也會做一些loader處理。我們的業務暫時沒有需要後端打包。

三、VUE同構

1、環境一致性

前後端同構語言一致這是基本。另外涉及到同構,就有兩個問題繞不開,一是採用ES6 modules 還是commonjs。二是環境不同,環境變數不同,請求訪問的方式不同。

1)第一個問題,首先很遺憾Node直到最新版本也沒有支援ES6 modules 的import語法。所以後端程式碼使用此語法,還需要babel等進行轉換成commonjs的模式。在我們的業務中用的是typescript的轉換能力。後端最終是commonjs,而前端要使用tree-shaking。那麼前後端最終兩者的編譯方式是不同的。

所以在我們業務中的解決方案是前端在開發環境中和後端一樣,使用commonjs的語法進行打包。然而在生產環境中,前端使用es6 modules進行打包,利用webpack的tree-shaking能力進行程式碼精簡和壓縮。

有壓縮&無tree-shaking的打包大小

image.png-33.7kB

有壓縮&有tree-shaking的打包大小

image.png-34.4kB

這裡tree-shaking的效果還是蠻明顯的,有接近40%的優化。

2)第二個問題,因為前後端環境不同,比如前端有window物件,document物件,後端沒有(這裡TSW有window物件,vue的識別出現問題)。如果有這方面的相容性問題,請處理好。

Net通訊並不完全一樣,前端使用的是http協議網路通訊,後端實際上從效能考慮,可以使用pb協議進行通訊,不需要到http協議。當然這些在使用中倒不是瓶頸。

另外不推薦使用官方推薦的axios,我們在實踐中發現一是axios程式碼非常多,原始碼多達近1600行,這在移動端確實有點浪費。另外axios還不支援常見的JSONP和getScript方式的請求方式。所以這塊建議大家根據自己需要用自己的Net庫代替。可以參考一下我們的Net庫,足夠滿足我們的業務需求。

核心程式碼200行。滿足5種請求方式:

export { get, post, getJSON, getJSONP, getScript }

net.ts-4.8kB

2、編寫同構程式碼

先看目錄結構,基本不需要額外的介紹,主要是方便文章中程式碼檔案的理解:

image.png-128kB

程式碼同構一直是我們的理想編碼方式,一份程式碼,前後端通用。結合VUE框架本身,VUE的SSR給我們提供了實現的可能。直出的本質無非是後端輸出一份字串,而且結合stream,進行檔案的流式輸出。程式碼類似下面這樣:

view/index.first.ts

    let app = new Vue({
        data: {
            firstData: firstData
        },
        template: '<firstScreen :firstData="firstData"></firstScreen>',
        components: {firstScreen}
    });

    let context = {
        env: '<script> window.ENV = ' + JSON.stringify(this.env) + '</script>',
        fisrtData: '<script> window.INITIAL_DATA = ' + JSON.stringify(firstData) + '</script>',
    };

    const renderer = require('vue-server-renderer').createRenderer({
        template: require('../html/index.html')
    });

    renderer.renderToString(app, context, (err, html) => {
        if (err) throw err;
        stream.write(html);
        stream.end();
    });

雖然程式碼行數不多,這裡要著重講一下,有很多細節在裡面。

1)後端部分的new Vue和前端部分的new Vue寫法略有不同:

後端部分的new Vue:

view/index.first.ts

import * as Vue from 'vue';
import firstScreen from '../comp/firstScreen'
let app = new Vue({
    data: {
        firstData: firstData
    },
    template: '<firstScreen :firstData="firstData"></firstScreen>',
    components: {firstScreen}
});

前端部分的new Vue:

js/index.first.ts

import * as Vue from 'vue';
import firstScreen from '../comp/firstScreen'

let app = new Vue({
    data: {
        firstData: window['INITIAL_DATA']
    },
    template: '<firstScreen :firstData="firstData"></firstScreen>',
    components: {firstScreen}
});

app.$mount('#main');

HTML部分

html/index.html

<div id="main">
    <!--vue-ssr-outlet-->    
</div>

後端部分的掛載點根據<!--vue-ssr-outlet-->,前端部分的掛載點根據元件中的id="main"

資料部分,後端的firstData由後端拼好資料,前端這裡有點講究。有涉及資料共享的部分。傳統的做法是通過vuex的store來實現,在我們的場景中,我們沒有使用vuex。只是首屏渲染部分我們採取全域性變數的方式來完成資料共享和一致性。

image.png-94.1kB

2)context的妙用

VUE中提供的context上下文來傳遞變數給到首屏頁面是個非常方便的東西,可以做很多初始化工作。

比如我們經常需要獲取會員資訊等,定義一個全域性變數可以很方便的任意地方進行使用。不需要非同步載入。

再比如我們頁面做效能測試的時候,需要badjs指令碼,蹦失率指令碼等,且需要進行灰度處理。這使用context再方便不過了。

後端:

view/index.first.ts

let context = {
    //灰度蹦失率的指令碼,QQ尾號為6進行蹦失率統計
    notifyWebStatus: utils.getUin() % 10 == 6 ? notifyWebStatus : '' 
};

前端模板:

html/index.html

image.png-188.9kB

四、VUE直出與CDN切換

在做了VUE同構直出之後,我們驚喜地發現我們自然而然的具備了直出和CDN頁面任意切換的能力,我們只需要稍微改造一下就能實現。

1、首屏資料部分進行一次同構,讓後端和前端都可以通過同樣的CGI取到相同的資料

common/model.ts

async function getFirstData() {
    if (window['INITIAL_DATA']) {
        return window['INITIAL_DATA'];
    } else {
        return await net.get('/sign/cgi/getFirstData');
    }
}

2、後端改為:

view/index.first.ts

let firstData = await model.getFirstData();
let app = new Vue({
    data: {
        firstData: firstData
    },
    template: '<firstScreen :firstData="firstData"></firstScreen>',
    components: {firstScreen}
});

3、前端改為:

js/index.first.ts

model.getFirstData().then((firstData) => {
    let app = new Vue({
        data: {
            firstData: firstData
        },
        template: '<firstScreen :firstData="firstData"></firstScreen>',
        components: {firstScreen}
    });
    app.$mount('#main');
});

4、那麼我們新建一個名為index_cdn.html檔案

這個檔案是放在CDN的,唯一和直出文件不同的地方就是一個

直出版本:

html/index.html

<div id="main">
    <!--vue-ssr-outlet-->
</div>

CDN版本:

html/index_cdn.html

<div id="main">
    <firstScreen></firstScreen>
</div>

其它完全一模一樣!

這樣我們做的事情就可以在直出Server抗不住的情況下,輕鬆切到CDN啦,只不過內容全部都是非同步拉取的了。對於暫時的使用者體驗來說並沒有太大影響,避免出現Server過載,業務出現無法訪問的情況。

通過此方案我們可以制定一個流量控制策略,輕鬆在直出和CDN兩者間切換自如。

五、VUE直出與SONIC的結合

不過實際上由於VUE的一些BUG導致接入會出現問題,好在WONDER為大家把坑填平了。

1、VUE的SSR部分無法保留註釋

看過Sonic原理和方案的同學知道Sonic是依賴註釋來拆分模板和資料的。但是因為VUE的SSR部分程式碼有個BUG,導致無法保留註釋。

這個問題在官方文件2.4版本已提供comments引數來解決,並且github上也有相關討論https://github.com/vuejs/vue/pull/5951。但實際上,What a sad,並沒有徹底解決,程式碼是有BUG的。

既SSR部分即使設定comments:true也是不行的。WONDER修改了兩處vue-server-render的程式碼,修復了這裡的問題。準備提PR給Vue官方,看他們準備如何處理。修改如下:

6871行,原始碼為:

Object.assign(vm.$options, compileToFunctions(template, {
    scopeId: _scopeId
}));

修改程式碼為:

Object.assign(vm.$options, compileToFunctions(template, {
    scopeId: _scopeId,
    comments: vm.$options.comments
}));

3012行,原始碼為:

options.comment(html.substring(4, commentEnd));

修改程式碼為:

options.comment(html.substring(0, commentEnd+3));

改完程式碼,只需要輕鬆宣告一下注釋保留即可comments: true

元件宣告部分:

@Component({
    template: require('./index.html'),
    props: {
        firstData: Object
    },
    comments: true,
    components: {paybar, item}
})

2、處理好原始碼的BUG之後,我們就可以開心地使用VUE和SONIC的結合啦

1)將資料塊包裹在sonicdiff標籤中

<!--sonicdiff-itemList-->
    <item :module="firstModule" :splitBanner="firstData.splitBanner"></item>
<!--sonicdiff-itemList-end-->

2)引入sonic_differ模組

import * as differ from 'sonic_differ';

3)在輸出字串的地方用differ模組處理一下

renderer.renderToString(app, context, (err, html) => {
    if (err) throw err;

    let buffer = differ(this, html);
    stream.write(buffer.data.toString());
    stream.end();
});

4)在頁面的URL引數上加一個sonicMode=3,表示在手Q環境中開啟SONIC模式(如果是非手Q環境,請按照SONIC的文件進行接入。)

六、VUE直出與測速優化

測速優化是老生常談的問題,在接入VUE的同構方案之後,我們對測速還是需要進行優化的。不過這些優化都可以在編譯流程中完成。

關於前端的測速核心還是網路耗時+頁面耗時(首屏可互動)

1、網路耗時

網路耗時包含伺服器的耗時+純網路耗時。

首先直出的頁面和CDN頁面相比,服務端有渲染的耗時問題。我們之前在使用VUE直出的時候還擔心這裡會有效能問題,但實際在中型專案中使用,實驗室資料還可以,如下圖所示:

image.png-97kB

純網路耗時比較好的思想是充分利用快取,那麼SONIC方案就是一個很好的方案,上面已經詳細介紹過了。主要優勢體現在區域性重新整理和完全Cache上面。測速資料如下:

image.png-150.3kB

2、頁面耗時

關於頁面耗時,我們先看我們的頁面結構分解

image.png-433.4kB

由於我們使用VUE同構,並且對底層庫進行了大量的重構。我們的業務完全脫離zepto,只使用qqapi的140行核心程式碼。也就是我們只依賴vue作為我們的庫。

那麼VUE庫採用手Q離線包的方案,將公共庫快取到手Q裡,減少公共庫載入的阻塞。

index.first.js 標記為「inline」,編譯系統通過任務和外掛先進行webpack的打包和tree-shaking,再識別標識「inline」,將檔案替換為本地檔案並打在html裡面。

index.entry.js 標記為「hash」,編譯系統通過任務和外掛先進行webpack的打包和tree-shaking,再識別標識「hash」,讀取webpack的依賴宣告檔案「profile.json」,將檔案替換為hash檔案。

整個流程通過編譯系統來處理,然後交給釋出系統進行釋出。

此時目前我們實驗室的資料,頁面耗時在330ms左右。

3、黑科技

首屏可互動的點在index.first.js中,儘管公共庫有離線包的存在,但是還是會有一些阻塞。並且index.first.js也是有執行耗時。更徹底的辦法是通過外掛將首屏需要用到的監聽事件和方法抽離出來,不依賴vue公共庫,即可直接事件響應。

此處和QQ動漫團隊學習交流了一下。核心思路是把資料和小chunk方法提前到vue公共庫以前,這樣可以在沒有vue公共庫的情況下,也可以完成簡單的互動(比如跳轉,對話方塊,選中態等),因為在沒有VUE驅動的情況下,核心思想是需要資料和事件方法的。

我們的業務是直出同構,有一個window的全域性變數「INITIAL_DATA」,首屏的所有需要用到的資料都在全域性變數裡面。那麼理論上首屏的事件只要方法提取出來,那麼即可完成首屏的事件操作。

但目前WONDER這邊還沒有研究出來如何方便接入。

預計再提升150ms。

七、結束語

此篇為系列文章中的四篇中的第三篇。短期總共規劃應該有四篇,分別是:

1、《TVUE框架的腳手架&IDE環境搭建&新手必備踩坑》(作者:harryxiang, justynchen, wonderhwang)(9月初完成,目前草稿)

此篇為新手入門必備

2、《TVUE框架的初級實踐》(作者:wonderhwang)(已完成),其實就是《vuejs+ts+webpack2專案實戰》

此篇學習之後可以完成簡單的前端開發

3、《TVUE框架的中型移動端專案直出同構實踐》(作者:wonderhwang, mitnickliu, justynchen, layenlin)(已完成)。

就是本文,此篇學習後可以完成中型移動端專案的前端開發,並且提供經過線上檢驗的效能優化方案。

4、《TVUE框架的QUI》(作者:yucongchen, roamye)(十月初完成,目前草稿)

主要結合QUI元件進行快速元件開發

希望可以在基於VUE的架構之上深度挖掘,最終能提高效能和效能。早點下班回家~~