@Shel
2017-10-11T07:48:23.000000Z
字数 12730
阅读 1059
项目名称:hwa-fe
项目人员:xiaoqing liu
项目版本:2.4.0
注意事项:本文档作为项目的唯一文档使用,开发人员需保持文档更新。
本项目采用 git flow 作为日常开发流程。
备注统一格式:【类型】【模块名】“详细说明”。
小模块,大集合。即将一个大的应用程序划分为多个小模块,一个模块是应用程序的一些功能。
小模块:对应着项目中features子目录。每一个features都有自己的routing、reducers、store、component,因此每一个feature都是相对独立的。它很小,而且很好解耦,所以很容易理解和维护。
大集合:通过对小模块的组合,获得最顶级的集合应用。
|-- src| +-- common| |-- features| | +-- common| | |-- home| | | +-- redux| | | |-- index.js| | | |-- route.js| | | |-- ...| | +-- feature-1| | +-- feature-2| +-- less| +-- images|-- ...
应用以features分组,通常对应产品业务。每个feature包含自己的组件,操作,路由,数据。如上所示,features子目录中存在一个较为特殊的common,它并非是一个实际存在的业务模块,而是被共享的公共模块。
Feature
一个feature通常描述成一个功能点,通常可以和产品业务相对应。
一个feature通常包含多个动作、组件或路由规则。
common是一个特殊的feature,它不对应任何独立的功能点或者业务模块,相反的,它被用来存放所有跨功能的元素,例如组件、动作、状态等等。
Component
It's just React component,只是结合了redux与router。
根据Redux的说法,组件可以分为两类:container and presentational。
通过定义路由中的响应路由器规则,使得我们可以使用带响应路由器的组件。
因此,在思考一个组件时,大致可以分成四种类型:
通常情况下:有状态有路由的组件是页面级的;而无状态无路由的组件复用性最好,通常在common文件下定义,被多个功能页面引用;有状态无路由的组件,一般作为一个页面的一部分内容而存在;无状态有路由的组件,一般为纯静态的页面,比如404页。
Action
It's just Redux action.
有两种类型的操作:sync和async。即同步和异步。通常来说异步操作是涉及API请求的,而同步操作通常是对某数据的写操作。
在创建异步操作时,通常会建立请求等待,请求成功,请求失败动作类型。与之对应的状态requestPending,requestError用来反映这三种情况。
Reducer
It's just Redux reducer. 与官方的方式相比,本工程重新组织了代码结构,使得它更容易面向产品业务,与现实更为贴合。
一个action一个文件,并将相应的reducer放入相同的文件中。这使得我们更直观的看到action与state之间的关系。
并不对一个reducer赋予初始值,而是通过对整个Redux store赋予初始值的方式。
类型: Features
要求: 新增 “来源跟踪”模块,包含(所有来源、搜索引擎、关键字、关键词、外部链接)5个子功能模块,子模块功能暂不实现,具体参见原型。
步骤:
src/features目录下创建“来源跟踪”功能模块,并添加子模块文件,文件结构如下:
|-- src| +-- common| |-- features| | +-- common| | +-- ...| | |-- sources-rpt(来源跟踪)| | | |-- redux| | | | |-- actions.js| | | | |-- constants.js| | | | |-- initialState.js| | | | |-- reducer.js| | | |-- DefaultPage.js (所有来源)| | | |-- EnginePage.js (搜索引擎)| | | |-- ExternalLinksPage.js (外部链接)| | | |-- KeywordPage.js (关键字)| | | |-- KeywordsPage.js (关键词)| | | |-- index.js(入口文件)| | | |-- route.js(路由配置)| +-- less| +-- images|-- ...
export { default as DefaultPage } from './DefaultPage';export { default as EnginePage } from './EnginePage';export { default as KeywordsPage } from './KeywordsPage';export { default as KeywordPage } from './KeywordPage';export { default as ExternalLinksPage } from './ExternalLinksPage';
isIndex 属性表示是否为首页,它是一个拓展出来的属性。在项目的根路径文件中会将它处理成父组件的形式。
// 导入子功能页面import {DefaultPage,EnginePage,KeywordsPage,KeywordPage,ExternalLinksPage,} from './';// 导出路由配置export default {path: '/sources-rpt',name: 'Sources rpt',childRoutes: [{ path: 'default-page', name: 'Default page', component: DefaultPage, isIndex: true },{ path: 'engine-page', name: 'Engine page', component: EnginePage },{ path: 'keywords-page', name: 'Keywords page', component: KeywordsPage },{ path: 'keyword-page', name: 'Keyword page', component: KeywordPage },{ path: 'external-links-page', name: 'External links page', component: ExternalLinksPage },],};
按照传统的方式,初始值在reducer中写入。但当应用程序增长,reducer的数量越来越多,想要快速直观的掌握store管理的数据项,就变得非常困难了。
js的变量是松散的。这使得它非常灵活,同时也难以控制。ES6中的const弥补了这一点。在使用变量存储数据时,尽可能的在定义时描述出数据的类型,可以提高代码的可读性与可维护性。基于此,我们将初始状态定义提取到一个单独的模块中,这样就可以快速查看一个功能使用数据的情况。
// 初始时可以用空对象表示,随着应用增长而逐步完善。const initialState = {};export default initialState;
import initialState from './initialState';const reducers = [];export default function reducer(state = initialState, action) {let newState;switch (action.type) {// Handle cross-topic actions heredefault:newState = state;break;}return reducers.reduce((s, r) => r(s, action), newState);}
修改src/common中的文件,将新增功能与根应用关联起来,在原来的基础上修改 rootReducer.js 、routeConfig.js
rootReducer.js 汇总reducer
...import sourcesRptReducer from '../features/sources-rpt/redux/reducer';const reducerMap = {...sourcesRpt: sourcesRptReducer,};...
// routeConfig.js 汇总子路由生成根路由...import sourcesRptRoute from '../features/sources-rpt/route';const childRoutes = [...sourcesRptRoute,];...
小结:
创建一个新功能模块的基本流程如上,即配置页面路由,构建数据模型,关联数据与数据操作,根结点中注册该模块。
类型: State
要求: 结合原型与API文档,为“来源跟踪-关键词”子页面定义数据模型。
步骤:
分析API: 将涉及的API有3个,分别用于“获取搜索引擎列表”、“关键词 total”、“关键词 report”。
其中,“搜索引擎列表”将用于显示多选控件;“关键词 total”用于汇总面板;“关键词 report”用于数据表格, 表格汇总和分页信息。
“关键词 total”查询参数可以在common下的state里获取,这里无需额外存储。“关键词 report”查询参数排除开始日期、结束日期、报告类型,需要额外存储。
修改 initialState.js,这里应当尽可能的描述出数据结构,并赋初始值。
const initialState = {...sourcesEngineList: [],paramKeywords: {page: 1,pagesize: 30,orderType: 'desc',orderColumn: 'pageViews',searchEngineTypeId: [],word: '',wordType: 'ALL',},keywordsReports: [],keywordsReportsSummary: {pageViews: null,uniqueVisitors: null,visitViews: null},keywordsReportsPage: {current: 1,total: 0,pagesize: 30},keywordsTotal: {pageViews: null,avgDayUniqueVisitors: null,visitViews: null},};export default initialState;
类型: Action
要求: 为“来源跟踪-关键词”的数据paramKeywords创建sync action。
步骤:
分析:页面中,用户通过操纵多个控件改变查询参数,获取新的数据。查询参数的改变应该是局部的修改,而不是整体的替换。
更改 sources-rpt/redux 下的 constants.js (常量文件)。这里使用大写加下划线的命名方式。并以所在的功能模块打头
...export const SOURCES_RPT_SET_PARAM_KEYWORDS = 'SOURCES_RPT_SET_PARAM_KEYWORDS';
sources-rpt/redux 下新建 setParamKeywords.js ,用于定义并导出action以及reducer
import {SOURCES_RPT_SET_PARAM_KEYWORDS,} from './constants';export function setParamKeywords(args = {}) {return {type: SOURCES_RPT_SET_PARAM_KEYWORDS,args,};}export function reducer(state, action) {switch (action.type) {case SOURCES_RPT_SET_PARAM_KEYWORDS:return {...state,paramKeywords: {...state.paramKeywords,...action.args,}};default:return state;}}
sources-rpt/redux 下的 actions.js
...export { setParamKeywords } from './setParamKeywords';
sources-rpt/redux 下的 reducer.js
import initialState from './initialState';import { reducer as setParamKeywordsReducer } from './setParamKeywords'; // 重命名以避免冲突并增加可读性const reducers = [setParamKeywordsReducer,];export default function reducer(state = initialState, action) {let newState;...
类型: Action
要求: 为“来源跟踪-关键词”的数据keywordsTotal创建Async action。数据来源于API。
步骤:
initialState.js,用以描述请求的状态与错误。
const initialState = {...getKeywordsTotalPending: false,getKeywordsTotalError: null,};export default initialState;
sources-rpt/redux 下的 constants.js (常量文件)。对于一次异步请求,有开始与结束,而结束又分为成功或失败。另外,对于错误信息,配上清空错误信息的操作。
...export const SOURCES_RPT_GET_KEYWORDS_TOTAL_BEGIN = 'SOURCES_RPT_GET_KEYWORDS_TOTAL_BEGIN';export const SOURCES_RPT_GET_KEYWORDS_TOTAL_SUCCESS = 'SOURCES_RPT_GET_KEYWORDS_TOTAL_SUCCESS';export const SOURCES_RPT_GET_KEYWORDS_TOTAL_FAILURE = 'SOURCES_RPT_GET_KEYWORDS_TOTAL_FAILURE';export const SOURCES_RPT_GET_KEYWORDS_TOTAL_DISMISS_ERROR = 'SOURCES_RPT_GET_KEYWORDS_TOTAL_DISMISS_ERROR';
sources-rpt/redux 下新建 getKeywordsTotal.js ,用于定义并导出action以及reducer
import axios from 'axios'; // 用于创建请求import {SOURCES_RPT_GET_KEYWORDS_TOTAL_BEGIN,SOURCES_RPT_GET_KEYWORDS_TOTAL_SUCCESS,SOURCES_RPT_GET_KEYWORDS_TOTAL_FAILURE,SOURCES_RPT_GET_KEYWORDS_TOTAL_DISMISS_ERROR,} from './constants';import { formatParams } from '../../../common/formatUtils'; // 用于格式化时间参数export function getKeywordsTotal(args = {}, channelId = null) {return (dispatch) => { // optionally you can have getState as the second argumentdispatch({type: SOURCES_RPT_GET_KEYWORDS_TOTAL_BEGIN,});const uri = `/api/channels/${channelId}/sources/keywords/total`;const promise = new Promise((resolve, reject) => {axios.get(uri, {params: formatParams(args),}).then((res) => {const {code,result = {},message = '',} = res.data;if (code === '200000') {dispatch({type: SOURCES_RPT_GET_KEYWORDS_TOTAL_SUCCESS,keywordsTotal: result,});resolve();} else {dispatch({type: SOURCES_RPT_GET_KEYWORDS_TOTAL_FAILURE,data: { error: message },});reject(message);}}).catch((err) => {dispatch({type: SOURCES_RPT_GET_KEYWORDS_TOTAL_FAILURE,data: { error: err },});reject(err.message);});});return promise;};}// Async action saves request error by default, this method is used to dismiss the error info.// If you don't want errors to be saved in Redux store, just ignore this method.export function dismissGetKeywordsTotalError() {return {type: SOURCES_RPT_GET_KEYWORDS_TOTAL_DISMISS_ERROR,};}export function reducer(state, action) {switch (action.type) {case SOURCES_RPT_GET_KEYWORDS_TOTAL_BEGIN:// Just after a request is sentreturn {...state,getKeywordsTotalPending: true,getKeywordsTotalError: null,};case SOURCES_RPT_GET_KEYWORDS_TOTAL_SUCCESS:// The request is successreturn {...state,getKeywordsTotalPending: false,getKeywordsTotalError: null,keywordsTotal: action.keywordsTotal,};case SOURCES_RPT_GET_KEYWORDS_TOTAL_FAILURE:// The request is failedreturn {...state,getKeywordsTotalPending: false,getKeywordsTotalError: action.data.error,};case SOURCES_RPT_GET_KEYWORDS_TOTAL_DISMISS_ERROR:// Dismiss the request failure errorreturn {...state,getKeywordsTotalError: null,};default:return state;}}
sources-rpt/redux 下的 actions.js
...export { getKeywordsTotal, dismissGetKeywordsTotalError } from './getKeywordsTotal';
sources-rpt/redux 下的 reducer.js
...import { reducer as getKeywordsTotalReducer } from './getKeywordsTotal';const reducers = [...getKeywordsTotalReducer,];
类型: Component
要求: 在“来源跟踪”下,创建一个汇总面板组件,该组件在“关键词”、“关键字”、“外部链接”页面复用,UI参见原型。
步骤:
分析:由需求及原型,汇总面板共包含三项指标pv-uv-vv。由于该组件会在多个子功能页面中使用,我们考虑将它定义成无状态组件,即,不直接关联store,从父组件接收参数。
另外,参数的复杂程度与组件的灵活通用性成正比,参数结构越复杂,复用组件就越是繁琐,不必要的使用store以外的数据传递给组件。这里,为了让组件使用更加方便,我们考虑仅仅传递“汇总数据”,标题数量、顺序、文字显示固定不变。
在 sources-rpt/ 下新建 SummaryPanel.js 。命名方式采用首字母大写。
import React, { Component } from 'react';import PropTypes from 'prop-types';import { formatInt, formatFloat } from '../../common/formatUtils';/*** @class 汇总面板(pv:uv:vv)*/export default class SummaryPanel extends Component {// 在开始处定义参数的结构,// 1. 行文风格统一,系统整体一致// 2. 便于他人日后维护和使用,快速查看可配参数static propTypes = {data: PropTypes.object,};// 为参数设置默认值是非常必要的,// 1. 提高稳定性,// 2. 进一步描述参数的配置方式。static defaultProps = {data: {},};render() {const data = {pageViews: null,avgDayUniqueVisitors: null,visitViews: null,...this.props.data,};return (<div className="sources-rpt-summary-panel"><div className="summer-row" ><div className="summer-item"><p className="summer-title">浏览量(PV)</p><div className="summer-data">{formatInt(data.pageViews)}</div></div><div className="summer-item"><p className="summer-title">日均独立访问者(UV)</p><div className="summer-data">{formatFloat(data.avgDayUniqueVisitors)}</div></div><div className="summer-item"><p className="summer-title">访问次数(VV)</p><div className="summer-data">{formatInt(data.visitViews)}</div></div></div></div>);}}
index.js。
...export { default as SummaryPanel } from './SummaryPanel';
....sources-rpt-summary-panel {display: table;min-width: 100%;background-color: @B050;.summer-row {display: table-row;.summer-item {display: table-cell;padding: 18px;&:hover {background-color: @B200;}.summer-title {color: @B600;margin-bottom: 18px;}.summer-data {font: 30px lighter;line-height: 1.4em;color: @H500;}}}}
类型: Component
要求: 将“来源跟踪-关键词”中的表头工具栏拆成子组件,它可读、写store的数据,导出功能暂不实现,UI参见原型。
步骤:
分析:由需求及原型,通过操控表头工具栏右侧搜索控件,使得下方数据表格内容发生改变。由于该组件仅在“来源跟踪-关键词”中使用,我们考虑将它定义成有状态组件,即,直接关联store。
在 sources-rpt/ 下新建 KeywordsPageTableToolbar.js 。命名方式采用首字母大写。与实践五不同的是,我们在同一个文件内关联store。
import React, { Component } from 'react';import PropTypes from 'prop-types';import { bindActionCreators } from 'redux';import { connect } from 'react-redux';import { Dropdown, InputGroup, FormControl, } from 'rsuite';import { Alert } from 'rsuite-notification';import * as actions from './redux/actions';export class KeywordsPageTableToolbar extends Component {static propTypes = {common: PropTypes.object.isRequired, // 项目公用数据sourcesRpt: PropTypes.object.isRequired, // 模块专用数据actions: PropTypes.object.isRequired,};render() {const {channelId,reportConfig,} = this.props.common;const { paramKeywords } = this.props.sourcesRpt;const {setParamKeywords,getKeywordsReport,} = this.props.actions;/*** @function handleQueryReport 获取报告数据* @param nextParam*/const handleQueryReport = (nextParam) => {getKeywordsReport({ ...reportConfig, ...paramKeywords, ...nextParam }, channelId).catch((message) => { Alert.error(message); });setParamKeywords({ ...nextParam });};return (<div className="sources-rpt-keywords-page-table-toolbar"><div><Dropdown shape="default" title="导出报告"><Dropdown.Item>汇总报告</Dropdown.Item><Dropdown.Item>细分报告</Dropdown.Item></Dropdown></div><div className="flex-item-auto" /><div style={{ width: '300px' }}><InputGroup><Dropdown shape="default" activeKey={paramKeywords.wordType} onSelect={(eventKey) => { handleQueryReport({ wordType: eventKey }); }} select componentClass={InputGroup.Button}><Dropdown.Item eventKey="ALL" >全部 </Dropdown.Item><Dropdown.Item eventKey="KEYWORDS" >关键词 </Dropdown.Item></Dropdown><FormControl type="text" value={paramKeywords.word} onChange={(value) => { handleQueryReport({ word: value }); }} /></InputGroup></div></div>);}}/* 关联数据 */function mapStateToProps(state) {return {sourcesRpt: state.sourcesRpt,common: state.common,};}/* 关联数据操作 */function mapDispatchToProps(dispatch) {return {actions: bindActionCreators({ ...actions }, dispatch)};}export default connect(mapStateToProps,mapDispatchToProps)(KeywordsPageTableToolbar);
index.js。
...export { default as KeywordsPageTableToolbar } from './KeywordsPageTableToolbar';
....sources-rpt-keywords-page-table-toolbar {background-color: @B050;padding: 18px;display: inline-flex;line-height: 36px;width: 100%;.flex-item-auto { flex: auto }}
小结:
对比实践五与实践六,后者比前者多了关联数据的工作,未用独立的文件保存这部分工作。采用这种行文方式能够很好的区分 container and presentational 两种类型的组件,同时组件关联的数据区域也得到很好的体现,这能为你带来良好的开发体验。其改良思想类似于“一叶知秋”。后续开发请沿用。