关于前端:耗时1年的前端技术框架切换之旅

40次阅读

共计 11943 个字符,预计需要花费 30 分钟才能阅读完成。

摘要: 一个电话,我便开启了为期 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/684490…
  • https://zhuanlan.zhihu.com/p/…
  • https://segmentfault.com/a/11…

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0