1. 程式人生 > >記一次fis3+react開發經歷

記一次fis3+react開發經歷

前言:

雖然說是記錄fis3+react的一次開發經歷。但是在專案的上線前幾天收到公司TC委員會的郵件,因為react的開源協議讓找到react的替代方案,並且逐步下線線上的react專案。真的是可以用“出師未捷身先死”來形容這次開發了。

不過經過調研以後發現在業界已經有了一些開源方案來替代react 。最有名的就是preact的了吧。而且按照官網的方案來做遷移的話,遷移的成本也挺小的。後續會介紹下遷移的情況。本文也是主要介紹下,使用react+fis3開發的一些經驗和其中遇到的問題的解決方案。

下面的文章也統一用react這個名詞。

1.為什麼要使用react+fis3

目前在選擇使用Vue

或者是React的時候,總要說些為什麼要用。其實我的想法很簡單。因為我們的產品是偏向業務型的,複雜的資料互動不多,但是流程多,而且很多業務要複用一些頁面流程。按照我們舊的開發模式來說,我們的模板檔案存在著很多的if...else來做流程判斷。這樣對於我們維護專案來說是非常的不方便的。我們希望引入一項技術來解決不同流程公用一些頁面的問題,而且可以在不同的流程中自定義這個頁面的表現的技術。

React的元件思想是一種解決方案。頁面的每一個元素都可以作為一個元件來抽象。擴大到一個頁面也可以作為一個元件。所以這是我選擇React來開發我的頁面。

按照業內主流做法使用react搭配webpack是最主流的。但是在公司內主推fis3的情況下還是選擇了fis3。而且fi3的維護團隊也產出過一篇引導文章和demo來介紹使用fi3開發react應用(參考文章見文章末尾)。

在專案開始前也在分析要不要引入redux。我們的頁面是偏向業務的,很多頁面都是表單的提交和驗證。並沒有很多的狀態需要維護,而且我們的資料來源也很單一。不過在經過一個頁面的開發後還是發現一些頁面引入redux會對開發工作帶來很大的便利。

2.專案的目錄結構和作用

react的開發模式基本上都差不多。因為這次我並沒有引入redux。所以我的目錄結構也相對簡單一些。如下:

.
├── components    //元件目錄
├── containers    //元件容器
├── language    //語言包目錄,為專案做國際化預留的能力
├── node_modules       //npm依賴
├── package-lock.json ├── package.json ├── page //頁面模組 ├── routes //路由模組 ├── state //因為沒有引入redux但是有一個頁面的狀態實在太多還是單獨為它做了一個檔案管理state ├── static //靜態檔案模組 └── yarn.lock

3.拆分頁面

使用react的第一項工作就是要拆分自己頁面。把自己的頁面拆分成“一塊一塊”
的元件。所以看下圖我的頁面結構。

我是這樣拆分我的頁面的,紅色的線就是我的拆分模式。

所以我的components目錄下的檔案是這樣的,分別對應這我對頁面拆分後的元件。

├── components
│   ├── Header.jsx
│   ├── Input.jsx
│   ├── PassBtn.jsx
│   ├── SafeCenter.jsx
│   ├── SafeList.jsx
│   ├── header.styl
│   ├── input.styl
│   ├── passbtn.styl
│   ├── safecenter.styl
│   └── safelist.styl

而我的comtainers目錄就是對應著我的頁面檔案,其實就是一個個零散元件的容器。

├── containers
│   ├── BankList.jsx
│   ├── EditUserInfor.jsx
│   ├── EditUserInfor.styl
│   ├── SafeCenterIndex.jsx
│   ├── VerifyBank.jsx
│   ├── VerifyRealName.jsx
│   ├── banklist.styl
│   ├── verifybank.styl
│   └── verifyrealname.styl

還有最有一個值得介紹的就是routes目錄,管理整個專案的路由。

├── routes
│   └── index.jsx

因為使用的preact-router這裡的寫法和react-router的有一些不同。程式碼如下:

export default (
    <Router history={createBrowserHistory()} >
        <SafeCenterIndex path="/v4/security/"/>
        <VerifyRealName path="/v4/security/verifyname"/>
        <VerifyBank path="/v4/security/verifybank"/>
    </Router>
);

4.寫react肯定會面臨的問題

寫react時,我們在享受著react的visual dom的高效能和不依賴dom的程式設計的便利時,面對最大的問題就是對元件的state的管理吧。當然最好的解決方案肯定是引入redux做狀態管理。但是當我們沒有引入redux時怎麼辦呢?

我先來舉個簡單的例子。我們有以下一個對像來管理頁面的顯示狀態:


var obj= {
    "aaa": {

        "bbb": {
            "ccc": {
                "ddd": {
                    "header": "istrue"
                }
            }
        }
    }
}

當我們的一個state巢狀太深時,按照我們使用js的一般做法要更新header的值的做法如下:


 obj.aaa.bbb.ccc.ddd.header='isfalse'; 

有時候我們為了能夠讓程式碼更健壯可能會這麼寫:

obj.aaa || obj.aaa.bbb || obj.aaa.bbb.ccc || obj.aaa.bbb.ccc.ddd || obj.aaa.bbb.ccc.ddd.header = 'isfalse'

會發現這樣做真的繁瑣。當然我在做的時候也面臨著這樣的問題。通過查詢相關資料,最佳的解決方案當然還是引入redux。那麼次佳的解決方案呢。其實就是引入第三方庫,最具代表性的就是facebook自己的facebook/immutable-js。這個庫能讓我們方便、安全、高效的更新一個層次較深的state。但是也有一個缺點就是檔案體積較大。當然與之相對應的就是開源大神做了優化後的版本immutability-helperimmutability-helper

這三個庫中文分析介紹的文件挺多的可以自行搜尋瞭解。當然最後我上面的都沒有用。因為之前的我的專案中已經引入了lodash這個開源庫。而它也提供了較安全的更新一個深層次object的方法

如果使用lodash更新上面header的值,寫法如下:

import * as _ form 'lodash';

_.set(obj, 'aaa.bbb.ccc.ddd.header', 'isfalse');   //更新header的值

var header =  _.get(obj, 'aaa.bbb.ccc.ddd.header') //獲取header的值

還有一個值得注意的地方就是在我們更新state之前,都是“克隆”而且是“深克隆”一個state去更新。“深克隆”肯定是會影響程式效能的,所以facebook的facebook/immutable-js提供了高效的方法去“深克隆”一個物件。

當然使用lodash也會更方便一些。但是這樣的操作不應該經常的發生。

import * as _ form 'lodash';

var initState = {};

var deepCloneState = _.cloneDeep(initState);   // 我們操作的其實都是這個clone的備份

第三個,出現的比較坑的問題。瀏覽器或者是webview快取GET請求。

這個問題主要發生在需要多次以GET的方式請求同一個介面。解決的方案也挺簡單就是在我們發起的GET請求後面加上時間戳。以使用axios為例:

axios.get('/v4/xxx/action?v=' + (new Date).getTime())
    .then(data => {})
    .catch(err => {})

5.打包工具的配置

本來想統一的使用typescript外掛來編譯jsx的。因為遷移preact原因,要修改全域性pragma。所以編譯前端的jsx就使用了babel-5.x外掛,以下是全域性的配置檔案介紹。

// 定義一個全域性的變數目錄
const dirList = '{actions,components,constants,routes,containers,page,state,language,reducers,store}';

fis.match('/client/(' + dirList + '/**.{js,es,jsx,ts,tsx})', {
        parser: fis.plugin('babel-5.x', {
            sourceMaps: false,
            optional: ['es7.decorators', 'es7.classProperties'],
            jsxPragma: 'h'   // 這裡也是最重要的遷移preact後必須加的一個引數
        }),
        // 頁面中顯示的url並且加上自定義的v4字首
        url: '/v4${static}/${namespace}/$1$2$3$4$5$6$7$8$9$10',
        isJsXLike: true,
        // 設定位模組目錄最後的編譯結果都是會用define包裹
        isMod: true
    })
    // 這裡都是為了給靜態檔案加上v4自定義字首
    .match('/client/({components,containers}/**.styl)', {
        url: '/v4${static}/${namespace}/$1',
    })
    // 這裡都是為了給靜態檔案加上v4自定義字首
    .match('/client/static/({img,js,styl}/**.{png,js,ico,styl})', {
        url: '/v4${static}/${namespace}/static/$1$2$3'
    })

    // 因為使用的stylus做位css的預編譯工具,這裡的配置是編譯stylus的配置
    .match('*.styl', {
        rExt: '.css',
        parser: fis.plugin('stylus', {
            sourcemap: false
        }),
        preprocessor: fis.plugin('autoprefixer', {
            'browsers': ['Android >= 2.1', 'iOS >= 4', 'ie >= 8', 'firefox >= 15'],
            'cascade': false
        })

    })

以上的配置檔案是開發環境的配置,在頁面載入的時候也是對靜態檔案逐條載入的。

而且頁面的載入時間也比較長不符合我們線上的載入靜態檔案的需求。

緊接著對打包指令碼進行優化

fis.media('prod')
   // 壓縮css 
    .match('*.{styl,css}', {
        'useHash': true,
        'optimizer': fis.plugin('clean-css')
    })
    .match('/client/node_modules/**.{js,jsx}', {
        'isMod': true
    })
    .match('/client/**.{js,es,jsx,ts,tsx}', {
        'preprocessor': [
            fis.plugin('js-require-file'),
            fis.plugin('js-require-css')
        ]
    })

    //合併靜態檔案
    .match('::packager', {
        'packager': fis.plugin('deps-pack', {
            // 將所有的npm依賴打包成一個檔案
            '/client/pkg/npm/bundle.js': [
                '/client/page/index.js:deps',
                '!/client/' + dirList + '/**'
            ],
            //將所有的業務程式碼打包成一個檔案
            '/client/pkg/npm/index.js': [
                '/client/page/index.js',
                '/client/page/index.js:deps'
            ],
            //將所有的css檔案打包成一個檔案
            '/client/pkg/npm/bundle.css': [
                '/client/**.{styl,css}',
                '!/client/static/**.{styl,css}'
            ]
        })
    })
    //給所有打包完的檔案加字首
    .match('/client/(pkg/npm/**.{js,css})', {
        'url': '/v4${static}/${namespace}/$1',
    });

這樣做最後線上的頁面載入時,載入的靜態檔案(除了圖片)只有3個。頁面的載入時間也保留在200ms以內。而所有的npm依賴最後的bundle檔案也只有80kb的大小。這個對於現代的前端網路是可以接受的。

排除非首屏的載入,使用快取載入頁面的。這個時間已經縮短的極小了。

以下是我在一APP內開啟頁面後,每次都使用快取檔案載入的結果。所有的靜態檔案都使用了本地快取,http狀態碼都是304

在這個過程中也發現一個問題,就是使用時間戳的方式和使用hash戳的方式,快取靜態檔案。觀察下面的截圖發現,使用時間戳的方式,並不能有效的快取我們的靜態檔案,每次進入頁面,靜態檔案都重新發起了請求。因為又沒有使用CDN加速,這樣其實也間接的對我們的伺服器造成壓力。

遷移preact

最後就是介紹下遷移react到preact我都是做了那些工作吧。當然按照官網提供的步驟一步一步走肯定是沒有錯的。

首先是修改庫的引入方式


import {h, render, Component} from 'preact';

因為使用了preact-router。所以路由的配置方式和react-router的有些不一樣的

import {h, render, Component} from 'preact';
import {Router} from 'preact-router';
// import {Router} from 'react-router';
import SafeCenterIndex from '../containers/SafeCenterIndex';
import VerifyRealName from '../containers/VerifyRealName';
import VerifyBank from '../containers/VerifyBank';
import {createBrowserHistory} from 'history';



export default (
    //這裡的寫法和react-router不一樣。
    <Router history={createBrowserHistory()} >
        <SafeCenterIndex path="/v4/security/"/>
        <VerifyRealName path="/v4/security/verifyname"/>
        <VerifyBank path="/v4/security/verifybank"/>
    </Router>
);

第二就是修改編譯的打包指令碼。按照官網的介紹就是要修改最後編譯結果的jsx的包裹方式。在前面的關於打包指令碼的配置已經介紹過了.主要就是配置jsxPragma屬性。

fis.match('/client/(' + dirList + '/**.{js,es,jsx,ts,tsx})', {
        parser: fis.plugin('babel-5.x', {
            sourceMaps: false,
            optional: ['es7.decorators', 'es7.classProperties'],
            jsxPragma: 'h'
        }),
        url: '/v4${static}/${namespace}/$1$2$3$4$5$6$7$8$9$10',
        isJsXLike: true,
        isMod: true
    })

後續的計劃

  1. 引入redux做狀態管理
  2. 因為使用了preact,它預設是不提供prop-type做型別檢查的。所以以後準備在專案引入typescript編寫程式碼。因為它預設提供了靜態型別檢查的機制。
  3. 因為react的特點。當我們切換頁面的時候其實是在不同的view(或者說是state)間進行切換。我們並沒有重新請求頁面伺服器。所以頁面切換的時候可以做一些類似原生的切換動畫。
  4. 將靜態檔案的載入走cdn加速域名。

參考文章: