5

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

 3 years ago
source link: https://www.cnblogs.com/huaweiyun/p/14754300.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

摘要:一个电话,我便开启了为期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 。

v2-06b32b1d61faec3de38b4e5c557508ba_720w.jpg

图1-1 前端技术框架切换原因

二、旅行攻略

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

2.1.1Web前端发展简史

正式介绍React和前端工程化之前,先简单了解下Web前端发展史。如图2-1所示,Web前端发展主要经历5个关键时代。

v2-5ee226045d2f93d783c8bdc0c9c3b25d_720w.jpg

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

v2-88a04b969b1de43494aac2edfb7eb285_720w.jpg

为了高效高质量完成Web应用的迭代上线,出现了前端工程化解决方案及相关架构如图2-1所示。

图2-2 前端工程化架构

工程化解决的问题是,如何提高编码、测试、维护阶段的生产效率。前端工程化要解决的问题包括:

1)制定各项规范,让工作有章可循:编码规范统一、开发流程规范、前后端接口规范等。

2)使用合适的前端技术和框架,提高生产效率:采用模块化的方式组织代码(ES6 Module);采用组件化的编程思想,处理UI层(React);将数据层分离管理(Redux);使用面向对象或者函数编程的方式组织架构。

3)提高代码的可测试性,引入单元测试,提高代码质量。

4)通过使用各种自动化的工程工具(Gulp/Webpack),提升整个开发、部署效率。

FMA进行React技术框架切换的同时,引入业界流行的前端工程化解决方案,以组件化、模块化、自动化、规范化等手段,提升开发及维护效率。

综上所述,分析此次前端技术框架切换将发生的变化,从③+④混搭到④+⑤相结合,再加上集成组件/模块的编译构建、规范检查、自动化持续集成、部署为一体的前端工程,实则是整个产品软件工程技术的转变与提升。

2.2 游玩路线—技术框架切换关键步骤

v2-de1ba722caec4997b9c3de9d752848bb_720w.jpg

游玩路线—技术框架切换关键步骤

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

v2-e73a9e6584fc9dbcc6e4683f6797739a_720w.jpg

(8)多数Web应用的组件层次关系,如下图所示的树状关系。

3)FMA组件划分

通常可根据业务进行划分,或根据技术进行划分。FMA根据业务设计并开发应用中的组件树。

v2-4e0190caac700421caf94b8bf24be558_720w.jpg

(1)切割模版(页面结构模块化):主界面为入口容器组件;其次,分左右两个面板容器组件;左面板根据业务功能,分为主topo业务组件和自定义topo业务组件;右面板根据业务功能,分为故障总览、快速故障匹配等业务组件。以此类推,从外到内、从大到小、分层进行组件划分,如下图所示。

(2)设计并开发通用业务组件,或基础组件,使得组件尽可能复用,如FMA特有的表格组件、画图组件等。

(3)明确各个组件的边界,内部state的设计,props的设计以及与其他组件的关系

(4)明确各个组件的定位与职能划分,设计好父子组件、兄弟组件的通信机制

(5)搭架子,并开始填充

三、不一样的风景

了解了前端发展史、搭建好了React项目工程、划分好了组件,那么如何写一个React的组件?

3.1单个组件目录

v2-e3baa62b56d48fa9929c6fe73287ad71_720w.jpgv2-7961d71eba2f1a8f1ab13660eff88d37_720w.jpg

首先,对于单个组件来说,标准的组件目录要有,但可通过组件分类进行目录裁剪。创建一个组件,需要建一个单独的文件夹。文件夹通常包含主文件入口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

v2-50ddc3bb92879788669e7aec690d64e2_720w.jpg

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)在线分析模式和导出报告离线分析模式源码分居两个代码仓;

v2-ce340733016ec77f6519c33fd4cd06c7_720w.jpg

3)多个功能模块400+函数小函数堆积成“上帝类”,代码重复率44%,相同业务逻辑的增加、删除、修改等扩展维护工作,存在重复劳动、修改遗漏引入缺陷等问题。

4.2 无债一身轻

在切换前端技术框架(React、单页面、UI组件化)的背景下,进行以下几点重构,在线导出源码共仓、相同业务功能共用业务组件,代码重复率从44%降低到4.8%,减少重复代码1W+。

1) 应用“MVC分层原则”,将数据封装保存(model)、业务逻辑(controller)、界面显示(controller)进行开发视图分层归类,如图2-1所示;

2) 应用“单一职责原则”以及“最少知道”原则,对“上帝类”进行梳理拆分,将平铺堆积的功能函数,按功能职责,抽取封装成一个个高内聚低耦合的可插拔的组件类。同时按照组件功能,进一步分组归类为偏底层的基础组件、偏上层的业务组件、以及用来进行数据处理的工具组件。上层业务组件可按需“组合”使用其他业务组件或基础组件。在线分析和导出报告组件,同理按需组合使用各业务组件或基础组件,如图2-1、2-2所示。

v2-206651e04f775b19a16fedcd4c004309_720w.jpg

图2-1 开发视图分层

v2-3090c3071b33d682ead7072bb8605364_720w.jpgv2-3ec35a76b0467c0f4974c4fc2e83aca8_720w.jpg

图2-2 组件划分

3)为使导出和在线最大限度地通用业务组件,对来源不同、数据结构不同的数据,传入业务组件前进行数据标准化、归一化。

v2-f4639c82864ae88d0b453e9698cffe05_720w.jpg

图2-3 组件数据标准化、归一化

本次前端技术框架切换,事务本身比较被动,好在能够主动识别交付难点。提前梳理工作量,主动管理切换过程。最终,及时、有效、高质量完成交付,确保FMA前端开源组件满足生命周期管理要求的同时,提升FMA组前端软件技术,从无到有建立FMA前端工程化能力。

1)结合交互界面框图,将功能模块的业务逻辑及交互界面,进行组件化封装后,在线和导出分析模式可高度通用业务组件,不再需要同时对两套代码,进行相同或相似功能点的开发维护,避免重复“造轮子”,提高开发效率,提升可维护性、易维护性,同时,避免因代码修改漏合,引入功能缺陷。

2)业务组件的设计开发,可高度内聚,使其功能单一,易维护。且多人协同开发同一功能模块时,可按小粒度的UI组件进行任务划分,并行开发,源码上库也不易造成冲突,提高开发质量及效率。

点击关注,第一时间了解华为云新鲜技术~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK