摘要:一個電話,我便開啟了為期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