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


摘要:一個電話,我便開啟了為期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 前端技術框架切換原因

二、旅行攻略

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

2.1.1Web前端發展簡史

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

圖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前端工程化

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

圖2-2 前端工程化架構

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

1)制定各項規范,讓工作有章可循:編碼規范統一、開發流程規范、前后端接口規范等。

2)使用合適的前端技術和框架,提高生產效率:采用模塊化的方式組織代碼(ES6 Module);采用組件化的編程思想,處理UI層(React);將數據層分離管理(Redux);使用面向對象或者函數編程的方式組織架構。

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

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

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

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

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

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

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組件。

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

3)FMA組件划分

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

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

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

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

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

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

三、不一樣的風景

了解了前端發展史、搭建好了React項目工程、划分好了組件,那么如何寫一個React的組件?

3.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

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)在線分析模式和導出報告離線分析模式源碼分居兩個代碼倉;

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

4.2 無債一身輕

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

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

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

圖2-1 開發視圖分層

圖2-2 組件划分

3)為使導出和在線最大限度地通用業務組件,對來源不同、數據結構不同的數據,傳入業務組件前進行數據標准化、歸一化。

圖2-3 組件數據標准化、歸一化

五、后記

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

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

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

參考鏈接

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM