文件结构与模拟 API
调整文件结构、提取组件,编写模拟 API 加载示例数据
在 UI 布局与项目结构 中,我们使用 Semantic-UI-React 构建了 Project Mini-Mek 的初始硬编码 UI 布局,添加了一个由 Redux 控制的标签栏组件,并开始采用“以功能为中心”的文件夹结构来组织代码。在 使用 Redux-ORM 中,我们学习了如何使用 Redux-ORM 来管理 Redux 存储中的关联数据。现在,我们将把这些部分整合起来应用。在本节中,我们将为功能开发做好准备,使用 Redux-ORM 定义数据模型,利用这些模型将数据加载到存储中并展示出来,并添加对选中项的追踪功能。
保持文件结构的一致性
如前所述,我们采用“以功能为中心”的文件夹结构。然而,并非所有代码都属于某个“功能”。实际上,主要可以分为三类:通用的、在整个应用中重复使用的公共代码;特定于某个功能的代码;以及用于整合这些功能的应用级代码。
我们将进一步调整文件结构,以更好地体现这些概念:
移动后,项目的文件结构如下所示:
1- src 2 - app 3 - layout 4 - App.js, App.css 5 - reducers 6 - rootReducer.js 7 - store 8 - configureStore.js 9 - common 10 - components 11 - utils 12 - features 13 - mechs 14 - pilots 15 - tabs 16 - unitInfo 17 - unitOrganization 18 - index.js
从功能中提取组件
我们在第 4 部分创建的原始 UI 布局中,每个标签页面板都包含一个单独的组件。同时,这些布局中还直接嵌入了虚假数据。当我们要处理真实数据时,这种方式显然不可行。因此,在开始处理数据之前,我们需要先从这些面板中提取出一些组件。
观察我们的 <Pilots> 组件,它实际上由两大部分组成:飞行员列表和显示单个飞行员详情的表单。为此,我们应分别提取出 <PilotsList> 组件和 <PilotDetails> 组件。
此外,飞行员列表内部还有几个不同部分。标题区域是静态的,不会改变。我们可以将其保留在 <PilotsList> 的 render 方法中,但不妨趁此机会将其拆分为一个独立的 PilotsListHeader 组件。与此同时,我们肯定需要能够渲染每个飞行员条目的单独行,因此我们将创建一个 <PilotsListRow> 组件,用于在列表渲染时使用。
由于我们正从 UI 中的硬编码文本过渡到实际数据,因此应开始使用某种形式的示例数据。目前最简单的做法是将 <Pilots> 视为一个“容器”组件,在其状态中存储一个包含一项数据的数组,并将该数据传递给列表和详情组件。
现在,我们的 <Pilots> 组件主要部分如下(省略库导入):
1import PilotsList from "./PilotsList"; 2import PilotDetails from "./PilotDetails"; 3 4const pilots = [ 5 { 6 name : "娜塔莎·克伦斯基", 7 rank : "上尉", 8 age : 52, 9 gunnery : 2, 10 piloting : 3, 11 mechType : "WHM-6R", 12 } 13]; 14 15class Pilots extends Component { 16 state = { 17 pilots : pilots, 18 } 19 20 render() { 21 const {pilots} = this.state; 22 23 // 如果有数据,使用第一个飞行员作为当前显示对象。 24 const currentPilot = pilots[0] || {}; 25 26 return ( 27 <Segment> 28 <Grid> 29 <Grid.Column width={10}> 30 <Header as="h3">飞行员列表</Header> 31 <PilotsList pilots={pilots} /> 32 </Grid.Column> 33 <Grid.Column width={6}> 34 <Header as="h3">飞行员详情</Header> 35 <Segment > 36 <PilotDetails pilot={currentPilot} /> 37 </Segment> 38 </Grid.Column> 39 </Grid> 40 </Segment> 41 ); 42 } 43}
由于我们的 Mechs 列表目前结构基本相同,我们也对它进行同样的重构操作。
这个提交中有一个新且实用的小技巧。在《BattleTech》中,战斗机械人的重量范围从 20 吨到 100 吨,以 5 吨为单位递增。机械人通常根据其“重量级别”进行描述和分类:20-35 吨为“轻型”,40-55 吨为“中型”,60-75 吨为“重型”,而 80-100 吨的则为“突击型”。这一分类可以根据已知的机械人类型重量推导得出。因此,我们可以编写一个小型选择器函数,接收重量值并返回对应的重量类别名称:
features/mechs/mechSelectors.js
1const WEIGHT_CLASSES = [ 2 {name : "轻型", weights : [20, 25, 30, 35]}, 3 {name : "中型", weights : [40, 45, 50, 55]}, 4 {name : "重型", weights : [60, 65, 70, 75]}, 5 {name : "突击型", weights : [80, 85, 90, 95, 100]}, 6]; 7 8export function getWeightClass(weight) { 9 const weightClass = WEIGHT_CLASSES.find(wc => wc.weights.includes(weight)) || {name : "未知"}; 10 return weightClass.name; 11}
这里没有太复杂的逻辑。我们定义了一个数组,每种重量类别对应一个条目,并使用 Array.find() 方法查找第一个满足条件的项。筛选函数通过 Array.includes() 检查给定的重量是否属于该类别的重量列表。如果未找到匹配的类别,则返回默认值“未知”,以确保始终能返回 weightClass.name。这个选择器之后可以在 <MechsListRow> 和 <MechDetails> 中使用,为具体的机械人实例提供重量类别的显示文本。
添加模拟 API 用于示例数据
我们现在已进入需要开始处理数据的阶段。在一个更大的应用或更真实的场景中,这可能意味着搭建一个独立的后端服务器来返回响应,或者至少使用某种“模拟 API”工具。
为了使本教程应用尽可能聚焦,我们将遵循经典的“做最简单可行的事”原则 / “你不需要它”原则。在此情况下,这意味着 编写一个文件来存放我们的示例数据,直接将其导入源码,并编写一个模拟 API 查询函数,该函数仅通过 Promise 返回这些数据。
我们还需要一点 UI 来实际调用这个模拟 API 函数。由于我们已经设置了标签栏,最简单的做法是创建一个新的面板,其中包含一个获取数据的按钮,并将其作为一个新的标签页添加。我们暂时将其标记为“工具”面板。未来也可以在此处添加“导入”和“导出”按钮。
这里真正值得关注的部分是初始示例数据、模拟 API 函数以及我们的 thunk 动作创建器:
1const sampleData = { 2 unit : { 3 name : "黑寡妇连", 4 affiliation : "wd", 5 }, 6}; 7 8export default sampleData;
1import sampleData from "./sampleData"; 2 3export function fetchData() { 4 return Promise.resolve(sampleData); 5}
1import {fetchData} from "data/mockAPI"; 2 3import {DATA_LOADED} from "./toolConstants"; 4 5export function loadUnitData() { 6 return (dispatch, getState) => { 7 fetchData() 8 .then(data => { 9 dispatch({ 10 type : DATA_LOADED, 11 payload : data 12 }) 13 }); 14 } 15}
添加面板后,界面效果如下:

感受一下兴奋吧! :)