耗時1年的前端技術框架切換之旅

華為雲開發者社群發表於2021-05-11
摘要:一個電話,我便開啟了為期1年的前端技術框架切換之旅。

本文分享自華為雲社群《記一次難忘的前端技術框架切換之旅【WEB前端大作戰】》,原文作者:一顆白菜 。

一、旅行之始

2020年初,某個普通的工作日,正在聚精會神“搞事情”的我,接到MAE-Access前端技術專家的espace語音,被告知MAE-Access域使用的前端技術框架需要從AngularJS1.x切換到React,要求2020年底完成。接到訊息的我,憂喜交加,機會與挑戰並存,這次前端技術框架切換之旅在所難免,但該如何開始,又該如何結束。

問:MAE-Access切換前端技術框架,基站產品三部的FMA LTE,為何也“在所難免”?

原因大體可以總結為以下三點,圖示如圖1-1:

1)FMA LTE以FMA LTE Website和FMA LTE Service兩個微服務,整合在MAE-Access上,與整個MAE-Access域統一構建。

2)MAE-Access域統一為各Website微服務提供前端工程化解決方案,各Website微服務統一使用Cloudsop平臺自研的前端UI元件---eview 。一方面統一網管各UI介面風格;另一方面方便統一管理前端相關的開源及三方件,同時也便於統一構建。

3))Cloudsop提供的eview元件,有基於angularJs前端開源框架和react前端開源框架兩個版本的。angularJs版的eview因使用angularJs1.X,21B後便不再滿足開源三方件生命週期管理要求,需要統一切換為react版的eview 。

耗時1年的前端技術框架切換之旅

圖1-1 前端技術框架切換原因

二、旅行攻略

2.1目的地—React技術框架及前端工程化

2.1.1Web前端發展簡史

正式介紹React和前端工程化之前,先簡單瞭解下Web前端發展史。如圖2-1所示,Web前端發展主要經歷5個關鍵時代。

耗時1年的前端技術框架切換之旅

圖2-1 Web前端發展簡史

① 簡單明快的早期時代:適合小專案,不分前後端,頁面由JSP、PHP等在服務端生成,瀏覽器負責展現。

② 後端為主的MVC時代:為了降低複雜度,以後端為出發點,有了Web Server層的架構升級,比如Structs、Spring MVC等。

③ Ajax帶來的 SPA 時代:2005年Ajax正式提出,前端開發進入SPA(Single Page Application 單頁面應用)時代。

④ 前端為主的MVC、MV* 時代:為了降低前端開發複雜度,Backbone、EmberJS、KnockoutJS、AngularJS、React、Vue等大量前端框架湧現。

⑤ Node帶來的全棧時代:隨著Node.js的興起,為前端開發帶來一種新的開發模式。

縱觀5個時代的變遷,每個後時代都在嘗試解決前時代的痛點。

1)①、②時代,前端開發重度依賴開發環境;前後端職責依舊糾纏不清,可維護性越來越差。

2)③時代,SPA應用大多以功能互動型為主,存在大量JS程式碼的組織,與 View 層的繫結等,都不是容易的事情,需要進行前端負責度控制。

3)④、⑤時代,前後端職責清晰;前端開發複雜度可控,通過合理的分層,讓專案更可維護;部署相對獨立,產品體驗可以快速改進。

2.1.2React技術框架

從Web前端簡史來看,React其實是前端為主的MVC、MV* 時代的產物,為降低前端開發複雜度而生。

React官方解釋React是一個用於構建使用者介面的JavaScript庫,可以使建立互動式UI變的輕而易舉。通過使用React,可以建立擁有各種狀態的元件,再由這些元件構成更加複雜的UI,元件邏輯使用javascript編寫而非模板(此處不同於JSP、PHP),可以輕鬆地在應用中傳遞資料,使得狀態與DOM分離。

FMA廢除原本jQuery+AngularJs1.x混搭的多頁面iframe巢狀實現,進行React技術框架的切換,重新劃分並組織各個UI元件為SAP,需要對整個前端進行“換血”式重寫。

2.1.3前端工程化

耗時1年的前端技術框架切換之旅

為了高效高質量完成Web應用的迭代上線,出現了前端工程化解決方案及相關架構如圖2-1所示。

圖2-2 前端工程化架構

工程化解決的問題是,如何提高編碼、測試、維護階段的生產效率。前端工程化要解決的問題包括:

1)制定各項規範,讓工作有章可循:編碼規範統一、開發流程規範、前後端介面規範等。

2)使用合適的前端技術和框架,提高生產效率:採用模組化的方式組織程式碼(ES6 Module);採用元件化的程式設計思想,處理UI層(React);將資料層分離管理(Redux);使用物件導向或者函式程式設計的方式組織架構。

3)提高程式碼的可測試性,引入單元測試,提高程式碼質量。

4)通過使用各種自動化的工程工具(Gulp/Webpack),提升整個開發、部署效率。

FMA進行React技術框架切換的同時,引入業界流行的前端工程化解決方案,以元件化、模組化、自動化、規範化等手段,提升開發及維護效率。

綜上所述,分析此次前端技術框架切換將發生的變化,從③+④混搭到④+⑤相結合,再加上整合元件/模組的編譯構建、規範檢查、自動化持續整合、部署為一體的前端工程,實則是整個產品軟體工程技術的轉變與提升。

2.2 遊玩路線—技術框架切換關鍵步驟

耗時1年的前端技術框架切換之旅

遊玩路線—技術框架切換關鍵步驟

2.2.1React專案工程搭建

1)React專案工程搭建:React官網提供了一套建立React專案的腳手架工程Create React App,可以快速建立出一個新的單頁面的、且已經整合好標準前端構建流水線的React專案工程(可通過修改webpack等構建工具的引數配置,自定義打包、構建、除錯工程)。

(1)先要安裝Nodejs(一個javascript執行環境),上官網下載不同作業系統的版本,一鍵式安裝即可。

(2)再通過Nodejs的包管理器工具npm,安裝create-react-app腳手架工具(npm install -g create-react-app)

(3)在需要建立專案的位置開啟命令列,輸入create-react-app + 專案名稱的命令(create-react-app myProject),進行專案建立。

(4)至此,專案已經建立成功,可以進入專案(cd myproject),直接啟動(npm start)。 如果需要構建出包,則執行(npm build)。需要注意的是,npm指令碼在建立好的專案的packge.json檔案script中可以自行進行修改或擴充套件。

2.2.2開發檢視設計及元件目錄規劃

業界比較主流的相對通用的目錄結構如下表所示。具體業務開發時,需按照下述結構進行業務本身的目錄及檔案劃分,基本上自定義components及contaniners以下的目錄,進行元件劃分即可。

|   index.js // 入口js
|   router.js // 路由入口
|   base.css // 全域性樣式檔案
+---store  //redux
|   |    store.js // redux store 入口,此處可用以註冊中介軟體
|   |    reducers.js // reducers入口
+---services  //資料訪問 (通常為api) 各域按需使用,不做統一要求
+---contexts    //contexts
+---utils   //公⽤用⽅方法邏輯  
+---assets  //資原始檔
|   +---i18n  //多語言
|        images  //圖片
|        fonts   //字型資源
|        media    //媒體資源
+---constants  //公用常量 (通常為後端各種列舉) 
+---components // 通用展示元件目錄
|   +---Header
|   |       index.js
|   |       Header.less
|   \---NotFound
|           index.js
\---containers // 容器元件目錄
|   +---Todo // 宣告頁面的目錄
|   |       |---index.js // 頁面入口
|   |       +---components // 頁面通用元件
|   |       |   +---Button
|   |       |           index.js
|   |       |           Button.jsx //推薦用法
|   |       |           Button.less
|   |       |           Button.stories.js
|   |       |   +---Input
|   |       |           index.js
|   |       |           Input.jsx
|   |       |           Input.less
|   |       |           Input.stories.js
|   |       +---containers
|   |       |       Search.js
|   |       |       Body.js
|   |       +---store
|   |              types.js
|   |               action.js
|   |                  reducer.js
 \---test // 測試目錄  和src目錄的結果保持一致
     +---components // 通用展示元件目錄
     |   +---Header
     |   |       index.spec.js //對index.js的測試檔案
     \---containers
         +---Todo 
         |       +---components
         |       |   +---Button
         |       |           Button.spec.js //對Button.jsx的測試檔案
         |       |   +---Input
         |       |           Input.spec.js //對Input.jsx的測試檔案
         |       +---store
         |               reducer.spec.js //對reducer.js的測試檔案

2.2.3前端元件梳理劃分

1)元件劃分原則

(1)標準性:任何一個元件都應該遵守一套標準,可以使得不同區域的開發人員據此標準開發出一套標準統一的元件

(2)獨立性:描述了元件的細粒度,遵循單一職責原則,保持元件的純粹性,屬性配置等API對外開放,元件內部狀態對外封閉,儘可能的少與業務耦合。

(3)複用與易用:UI差異,消化在元件內部(注意並不是寫一堆if/else),輸入輸出友好,易用。避免暴露元件內部實現,避免直接操作DOM,避免使用ref。

2)元件分類及層次關係

(1)基礎元件:為了更關注業務邏輯的實現,可以結合自身業務,選擇適合的成熟的UI元件庫,作為整個專案的基礎元件庫。如,FMA選擇了平臺提供的eview UI元件。

(2)容器型元件(Container):一個容器性質元件,一般作為一個業務子模組的入口,如FMA的故障總覽元件;容器元件內的子元件通常具有業務或資料依賴關係;集中/統一進行狀態管理,向其他展示型/容器型元件提供資料(充當資料來源)和行為邏輯處理(接收回撥);如果使用了全域性狀態管理,那麼容器內部的業務元件可以自行呼叫全域性狀態處理業務;充當子級元件通訊的狀態中轉站,進行業務模組內子元件的通訊統籌,如故障總覽元件,儲存總覽分析的介面響應資料向子元件傳遞,同時也會儲存子元件當前的互動狀態,已協調與其它子元件之間的互動聯動;模版基本都是子級元件的集合,很少包含DOM標籤。

(3)展示型元件(stateless):主要表現為元件是怎樣渲染的,就像一個簡單的模版渲染過程;只通過props接受資料和回撥函式,不充當資料來源;可能包含展示和容器元件 並且一般會有Dom標籤和css樣式;通常用props.children(react) 或者slot(vue)來包含其他元件;可以有狀態,只在其生命週期內操縱並改變其內部狀態,職責單一,將不屬於自己的行為通過回撥傳遞出去,讓父級元件去處理。

(4)業務元件:通常是根據最小業務狀態抽象而出,有些業務元件也具有一定的複用性,但大多數是一次性元件。

(5)通用元件:可以在一個或多個APP內通用的元件。

(6)邏輯元件:不包含UI層的某個功能的邏輯集合,比如FMA中的時間處理元件、字串處理元件等。

(7)高階元件(HOC):類比函數語言程式設計中的組合,可以看做一個接收其它元件作為引數,並返回一個功能增強的元件的函式。如FMA中的ErrorBoundry元件。

耗時1年的前端技術框架切換之旅

(8)多數Web應用的元件層次關係,如下圖所示的樹狀關係。

3)FMA元件劃分

通常可根據業務進行劃分,或根據技術進行劃分。FMA根據業務設計並開發應用中的元件樹。

耗時1年的前端技術框架切換之旅

(1)切割模版(頁面結構模組化):主介面為入口容器元件;其次,分左右兩個皮膚容器元件;左皮膚根據業務功能,分為主topo業務元件和自定義topo業務元件;右皮膚根據業務功能,分為故障總覽、快速故障匹配等業務元件。以此類推,從外到內、從大到小、分層進行元件劃分,如下圖所示。

(2)設計並開發通用業務元件,或基礎元件,使得元件儘可能複用,如FMA特有的表格元件、畫圖元件等。

(3)明確各個元件的邊界,內部state的設計,props的設計以及與其他元件的關係

(4)明確各個元件的定位與職能劃分,設計好父子元件、兄弟元件的通訊機制

(5)搭架子,並開始填充

三、不一樣的風景

瞭解了前端發展史、搭建好了React專案工程、劃分好了元件,那麼如何寫一個React的元件?

3.1單個元件目錄

耗時1年的前端技術框架切換之旅耗時1年的前端技術框架切換之旅

首先,對於單個元件來說,標準的元件目錄要有,但可通過元件分類進行目錄裁剪。建立一個元件,需要建一個單獨的資料夾。資料夾通常包含主檔案入口index.js(檢視層的邏輯);樣式採用的是scss或less css預編譯語言,寫在module.scss/module.less中,webpack會自動把scss或less編譯成css檔案,並且會解決掉瀏覽器兼用的差異;常量的定義為type.js;邏輯處理呼叫介面函式寫在actions.js中;如果需要使用redux,定義在reducers.js檔案中;如果該元件包含其它業務元件,可直接巢狀一個新的元件資料夾。如,下面的輔助恢復業務元件目錄。

3.2 元件主檔案index.js的基本結構

Line 01:import React, {Component} from 'react';
Line 02:import Spinner from '@huawei/eview-react/Spinner';
Line 03:import {injectIntl} from 'react-intl';
Line 04:import './module.css';
Line 05:import {getTotalPrice} from './actions'

Line 06class LeftPanel extends Component {

Line 07:  constructor(props) {
Line 08:    super(props);
Line 09this.state = {
Line 10:      message: '',
Line 11:      totalPrice: 0,
Line 12:      appleNumber: 0
Line 13:    }
Line 14this.applePrice = 2;
Line 15:  }

Line 16:  componentWillMount() {
Line 17this.setState({message: '左邊元件初始化完成!'});
Line 18:  }

Line 19:  getDom = (dom) => {
Line 20:  }

Line 21:  onBuyApple = (value) => {
Line 22const totalPrice = getTotalPrice(value, this.applePrice);
Line 23this.setState({appleNumber: value, totalPrice});
Line 24:  }

Line 25:  render() {
Line 26return (
Line 27:      <div className={"ev_layout_fix left-panel"} ref={this.getDom}>
Line 28:       <p>{this.state.message}</p>
Line 29:        <p>蘋果的單價:{this.applePrice}¥</p>
Line 30:        <p>購買的蘋果的數量:<Spinner
Line 31:          value={this.state.appleNumber}
Line 32:          min={0}
Line 33:          max={100}
Line 34:          step={1}
Line 35:          onChange={this.onBuyApple}/></p>
Line 36:        <p>共花去:{this.state.totalPrice}¥
Line 37:        </p>
Line 38:      </div>
Line 39:    )
Line 40:  }
Line 41:}

1)line01-05引入react庫:import React, {Component} from 'react';包括引入的需要的第三方的元件,自己定義的元件、函式、常量、css檔案、圖片等靜態資原始檔等。

2)line06-41進行元件類宣告實現:javascript其實是沒有類的概念的,es6的class其實是一種語法糖,本質是建構函式Function。Constructor可以省略,不寫也會預設存在,建議在有狀態元件下中新增,然後在Constructor做初始化的功能。

3)line25-40 render函式相當於我們angularjs中的template,用來渲染到瀏覽器上面的檢視。需要注意的是,這裡使用的是React jsx語法,樣式定義使用className屬性而非class,style的定義格式為style={{marginLeft:’2rem’}},最終return的元素有且僅有一個Element。

4)view層變數的定義與更新是固定的。分為自動觸發檢視層更新和不觸發檢視層更新兩種。自動觸發檢視層更新相關的state變數,初始化定義如line09-13,state變數重新賦值如line17,必須使用setState函式。其他不觸發檢視層更新的變數直接定義即可。

5)react提供了元件在進行初始載入,引數變更,登出等動作時的鉤子函式(亦稱生命週期函式),類似angularjs中的$onInit、$onChange、$postLink。其中,componentWillMount方法在mounting和render()之前呼叫,因此在此方法中setState不會觸發重新渲染,所以可以在這個週期使用setState來更改state值;componentWillReceiveProps方法在一個mounted的元件接收到並賦值新props前被呼叫,如果我們需要通過prop來更新state,可以在此方法中比較this.props和nextProps不相等時,再使用this.setState來更改state,以此減少元件的不必要渲染次數,達到效能優化的目的。

6)圖片等靜態資源的引用和元件的引用是一樣的,通過import關鍵字進行匯入,通過屬性變數進行引用。如Import iconImg from ‘圖片路徑’; <img src={iconImg} alt=”” />。圖片資源建議直接存放到當前的元件目錄下面,避免引用目錄太深。

3.3 元件國際化

1)使用第三方外掛react-intl

耗時1年的前端技術框架切換之旅

2)資源配置:建立i18n目錄,配置國際化資原始檔。

3)資源初始化與應用:在專案入口的index.js檔案中引入 import { injectIntl } from 'react-intl'; 在render中新增<IntlProvider locale={ lang.locale} messages={ lang.messages}> </IntlProvider>。Locale採用的是語言,messages,需要國際化的語言配置。

Line 01: import { lang, messages } from './asserts/i18n/index';
Line 02: import App from './containers/MainContainer';
Line 03const rootNode = document.getElementById('root');
Line 04:ReactDOM.render(
Line 05:    <IntlProvider locale={ lang.locale} messages={ lang.messages}>
Line 06:        <Provider store={store}>
Line 07:            <div style={{ height: '100%', width: '100%'
Line 08:                <App />
Line 09:            </div>
Line 10:        </Provider>
Line 11:    </IntlProvider>,
Line 12:    rootNode
Line 13:);
4)匯出國際化元件export default injectIntl(元件名);
5)在元件的具體函式中,使用國際化資源項如line01-02
Line 01const { intl } = this.props;
Line 12const loadingWaitLabel = intl.formatMessage({ id: 'loadingWait' })

3.4 後臺資料請求

後臺資料請求使用第三方元件axios(一個基於promise的HTTP庫,可以用在瀏覽器和 node.js中)。

1)axios特性:從瀏覽器中建立 XMLHttpRequests;從 node.js 建立 http 請求;支援Promise API;攔截請求和響應;轉換請求資料和響應資料;取消請求;自動轉換 JSON 資料;客戶端支援防禦 XSRF。

2)axios請求例項:

(1)get

// 為給定 ID 的 user 建立請求
axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

(2)Post

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

(3)執行多個併發請求

function getUserAccount() {
  return axios.get('/user/12345');
}

function getUserPermissions() {
  return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
    // 兩個請求現在都執行完成
  }));

3.5 Redux使用

3.5.1 什麼時候Redux

Redux的作用就是為了解決平行元件,或者沒有父子關係元件的之間的通訊。因此,當兩個元件無法通過狀態提升,將通訊訊息通過父元件進行中轉時,就需要使用Redux技術進行訊息通訊。

3.5.2 Redux配置使用

1)定義store檔案並進行store樹掛接。

import {combineReducers} from 'redux';
import {routerReducer} from 'react-router-redux';
import leftPanelReducer from './containers/Home/LeftPanelContainer/reducers';

export default combineReducers({router: routerReducer, leftPanel: leftPanelReducer});

2)應用入口index.js全域性store上下文配置,<Provider store={store}></Provider>

3)leftPanelReducer.js定義:types.js+reducers.js+actions.js

(1)types.js

const ACTION_TYPE = {
    SET_CAT_NAME: 'SET_CAT_NAME '
};

export { ACTION_TYPE };

(2)Reducers.js

import { ACTION_TYPE } from './types';
const initState = {
    catName: “ketty” 
    }
};
export default (state = initState, action) => {
    switch (action.type) {
        case ACTION_TYPE.SET_CAT_NAME: {
            return {
                ...state,
                catName: action.data
            };
        }
        default: {
            return state;
        }
    }
};

(3)actions.js

import { ACTION_TYPE } from './types';
export const setCatName= catName => dispatch => {
    dispatch({
        type: ACTION_TYPE.SET_CAT_NAME,
        data: catName
    });
};

4)使用redux傳遞全域性資料,通知所有接收方全域性資料的更新

(1)首先在傳遞資料元件中引入redux相關元件,及修改全域性資料的函式setCatName

import {combineReducers} from 'redux';
import { connect } from 'react-redux ';
import { setCatName } from './actions;

(2)匯出元件時,使用connect中介軟體進行元件屬性和全域性store關聯,此時setCatName函式相當於掛在this.props上,使用時直接呼叫this.props.setCatName (name),進行全域性資料的修改更新,通知動作則由整個Redux機制執行。

const mapDispatchToProps = dispatch => bindActionCreators({
  setCatName
}, dispatch);

export default connect(null, mapDispatchToProps)(injectIntl(LeftPanel)) 

5)使用redux監聽全域性資料的更新,接受最新值,類似於資料傳遞。

(1)首先在元件中引入redux相關元件,

import {combineReducers} from 'redux';
import { connect } from 'react-redux ';

(2)匯出元件時,使用connect中介軟體進行元件屬性和全域性store關聯,此時全域性資料catName掛在了this.props上,使用時直接呼叫this.props.catName,資料的及時性由整個Redux機制保障。

const mapStateToProps = state => ({catName: state.leftPanel.catName});
export default connect(mapStateToProps)(injectIntl(LeftPanel)) 

四、到達終點後的意外收穫

4.1 歷史債務

1)AngularJs(不滿足生命週期管理要求)/ jQuery框架混搭;

2)線上分析模式和匯出報告離線分析模式原始碼分居兩個程式碼倉;

耗時1年的前端技術框架切換之旅

3)多個功能模組400+函式小函式堆積成“上帝類”,程式碼重複率44%,相同業務邏輯的增加、刪除、修改等擴充套件維護工作,存在重複勞動、修改遺漏引入缺陷等問題。

4.2 無債一身輕

在切換前端技術框架(React、單頁面、UI元件化)的背景下,進行以下幾點重構,線上匯出原始碼共倉、相同業務功能共用業務元件,程式碼重複率從44%降低到4.8%,減少重複程式碼1W+。

1) 應用“MVC分層原則”,將資料封裝儲存(model)、業務邏輯(controller)、介面顯示(controller)進行開發檢視分層歸類,如圖2-1所示;

2) 應用“單一職責原則”以及“最少知道”原則,對“上帝類”進行梳理拆分,將平鋪堆積的功能函式,按功能職責,抽取封裝成一個個高內聚低耦合的可插拔的元件類。同時按照元件功能,進一步分組歸類為偏底層的基礎元件、偏上層的業務元件、以及用來進行資料處理的工具元件。上層業務元件可按需“組合”使用其他業務元件或基礎元件。線上分析和匯出報告元件,同理按需組合使用各業務元件或基礎元件,如圖2-1、2-2所示。

耗時1年的前端技術框架切換之旅

圖2-1 開發檢視分層

耗時1年的前端技術框架切換之旅耗時1年的前端技術框架切換之旅

圖2-2 元件劃分

3)為使匯出和線上最大限度地通用業務元件,對來源不同、資料結構不同的資料,傳入業務元件前進行資料標準化、歸一化。

耗時1年的前端技術框架切換之旅

圖2-3 元件資料標準化、歸一化

五、後記

本次前端技術框架切換,事務本身比較被動,好在能夠主動識別交付難點。提前梳理工作量,主動管理切換過程。最終,及時、有效、高質量完成交付,確保FMA前端開源元件滿足生命週期管理要求的同時,提升FMA組前端軟體技術,從無到有建立FMA前端工程化能力。

1)結合互動介面框圖,將功能模組的業務邏輯及互動介面,進行元件化封裝後,線上和匯出分析模式可高度通用業務元件,不再需要同時對兩套程式碼,進行相同或相似功能點的開發維護,避免重複“造輪子”,提高開發效率,提升可維護性、易維護性,同時,避免因程式碼修改漏合,引入功能缺陷。

2)業務元件的設計開發,可高度內聚,使其功能單一,易維護。且多人協同開發同一功能模組時,可按小粒度的UI元件進行任務劃分,並行開發,原始碼上庫也不易造成衝突,提高開發質量及效率。

參考連結

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章