摘要:一個電話,我便開啟了為期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 06:class LeftPanel extends Component { Line 07: constructor(props) { Line 08: super(props); Line 09: this.state = { Line 10: message: '', Line 11: totalPrice: 0, Line 12: appleNumber: 0 Line 13: } Line 14: this.applePrice = 2; Line 15: } Line 16: componentWillMount() { Line 17: this.setState({message: '左邊組件初始化完成!'}); Line 18: } Line 19: getDom = (dom) => { Line 20: } Line 21: onBuyApple = (value) => { Line 22: const totalPrice = getTotalPrice(value, this.applePrice); Line 23: this.setState({appleNumber: value, totalPrice}); Line 24: } Line 25: render() { Line 26: return ( 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 03: const 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 01: const { intl } = this.props; Line 12: const 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組件進行任務划分,並行開發,源碼上庫也不易造成沖突,提高開發質量及效率。
參考鏈接
- https://juejin.cn/post/6844903588553048077
- https://zhuanlan.zhihu.com/p/78472109
- https://segmentfault.com/a/1190000019759949#3