@Shel
2017-10-11T07:48:23.000000Z
字数 12730
阅读 881
项目名称: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 here
default:
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 argument
dispatch({
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 sent
return {
...state,
getKeywordsTotalPending: true,
getKeywordsTotalError: null,
};
case SOURCES_RPT_GET_KEYWORDS_TOTAL_SUCCESS:
// The request is success
return {
...state,
getKeywordsTotalPending: false,
getKeywordsTotalError: null,
keywordsTotal: action.keywordsTotal,
};
case SOURCES_RPT_GET_KEYWORDS_TOTAL_FAILURE:
// The request is failed
return {
...state,
getKeywordsTotalPending: false,
getKeywordsTotalError: action.data.error,
};
case SOURCES_RPT_GET_KEYWORDS_TOTAL_DISMISS_ERROR:
// Dismiss the request failure error
return {
...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
两种类型的组件,同时组件关联的数据区域也得到很好的体现,这能为你带来良好的开发体验。其改良思想类似于“一叶知秋”。后续开发请沿用。