前端
实体加载器
如果您正在开发新功能或者通常需要在前端获取一些应用数据,实体加载器将成为您的良友。它们抽象掉了调用API、处理加载和错误状态、缓存之前加载的对象(在某些情况下)、使缓存失效,并让您轻松地进行更新或创建新项。
实体加载器的良好用途
- 我需要获取特定的X(用户、数据库等)并显示。
- 我需要获取X的列表(数据库、问题等)并显示。
当前可用的实体
- 问题、仪表板、脉冲
- 集合
- 数据库、表、字段、分段、指标
- 用户、群组
- 完整当前实体列表在这里:https://github.com/metabase/metabase/tree/master/frontend/src/metabase/entities
使用加载器有两种方式,要么作为React“渲染属性”组件,要么作为React组件类装饰器(高阶组件)。
对象加载
在这个例子中,我们将为新页面加载有关特定数据库的信息。
import React from "react";
import Databases from "metabase/entities/databases";
@Databases.load({ id: 4 })
class MyNewPage extends React.Component {
render() {
const { database } = this.props;
return (
<div>
<h1>{database.name}</h1>
</div>
);
}
}
此示例使用类装饰器请求并显示ID为4的数据库。如果您想使用渲染属性组件,则代码将如下所示。
import React from "react";
import Databases from "metabase/entities/databases";
class MyNewPage extends React.Component {
render() {
const { database } = this.props;
return (
<div>
<Databases.Loader id={4}>
{({ database }) => <h1>{database.name}</h1>}
</Databases.Loader>
</div>
);
}
}
现在,您很可能不仅仅想显示一个静态项,所以在某些值可能是动态的情况下,您可以使用函数来获取props并返回所需的值。如果您使用的是组件方法,您可以像通常传递动态值一样传递props。
@Databases.load({
id: (state, props) => props.params.databaseId
}))
列表加载
通过应用loadList
装饰器可以轻松加载项目列表。
import React from "react";
import Users from "metabase/entities/users";
@Users.loadList()
class MyList extends React.Component {
render() {
const { users } = this.props;
return <div>{users.map(u => u.first_name)}</div>;
}
}
类似于对象加载器的id
参数,您也可以传递一个query
对象(如果API支持)
@Users.loadList({
query: (state, props) => ({ archived: props.showArchivedOnly })
})
对加载和错误状态的控制
默认情况下,EntityObject
和EntityList
加载器将通过使用底层的LoadingAndErrorWrapper
为您处理加载状态。如果出于某种原因您想自己处理加载,您可以通过设置loadingAndErrorWrapper: false
来禁用此行为。
包装对象
如果您将wrapped: true
传递给加载器,则对象或对象将被包装在辅助类中,这些类允许您执行诸如user.getName()
、user.delete()
或user.update({ name: "new name" })
等操作。动作已自动绑定到dispatch
。
如果有很多对象,这可能会产生性能开销。
在实体的objectSelectors
或objectActions
中定义的任何附加选择器和动作将作为包装对象的方法出现。
高级用法
您还可以直接使用 Redux 动作和选择器,例如,dispatch(Users.actions.loadList())
和 Users.selectors.getList(state)
。
风格指南
设置 Prettier
我们使用 Prettier 格式我们的 JavaScript 代码,并由 CI 强制执行。我们建议将您的编辑器设置为“保存时格式化”。您还可以使用 yarn prettier
格式代码,并使用 yarn lint-prettier
验证其是否已正确格式化。
我们使用 ESLint 强制执行额外的规则。它集成到 Webpack 构建,或者您可以手动运行 yarn lint-eslint
进行检查。
React 和 JSX 风格指南
我们大部分遵循 Airbnb React/JSX 风格指南。ESLint 和 Prettier 应该会处理 Airbnb 风格指南中的大多数规则。例外情况将在本文件中注明。
- 优先使用 React 函数组件而不是类组件
- 避免在
containers
文件夹中创建新组件,因为这种做法已被弃用。相反,将连接和视图组件都存储在components
文件夹中,以实现更统一和高效的组织。如果连接组件显著增大,并且需要提取视图组件,请选择使用View
后缀。 - 对于控件组件,我们通常使用
value
和onChange
。具有选项(例如Radio
、Select
)的控件通常接受一个具有name
和value
属性的对象数组options
。 - 类似于
FooModal
和FooPopover
的组件通常指的是模态/弹出框的 内容,应在Modal
/ModalWithTrigger
或Popover
/PopoverWithTrigger
内部使用。 -
类似于
FooWidget
的组件通常包含一个FooPopover
,它位于PopoverWithTrigger
内,并带有某种触发元素,通常是FooName
。 - 如果需要在类中绑定方法(而不是在构造函数中使用
this.method = this.method.bind(this);
),请使用箭头函数实例属性,但仅当函数需要绑定时(例如,如果您将其作为属性传递给 React 组件)。
class MyComponent extends React.Component {
constructor(props) {
super(props);
// NO:
this.handleChange = this.handleChange.bind(this);
}
// YES:
handleChange = e => {
// ...
};
// no need to bind:
componentDidMount() {}
render() {
return <input onChange={this.handleChange} />;
}
}
- 对于样式组件,我们目前使用
styled-components
和 “原子”/“实用优先” CSS 类 的混合。 - 优先使用
grid-styled
的Box
和Flex
组件而不是原始的div
。 - 组件通常应将其
className
属性传递给组件的根元素。它可以使用来自classnames
包的cx
函数与额外的类合并。 - 为了使组件更具可重用性,组件应只对其根元素应用类或样式,这些类或样式会影响其自身内容的布局/样式,但 不应 影响其父容器内部的布局。例如,它可以包含填充或
flex
类,但不应该包含边距或flex-full
、full
、absolute
、spread
等。这些应该通过消费者组件的className
或style
属性传递,该消费者组件知道组件应如何在其内部定位。 - 避免在单个组件中将 JSX 分解成单独的方法调用。建议内联 JSX,以便更好地了解 JSX 与组件的
render
方法返回值之间的关系,以及与组件的state
或props
之间的关系。通过内联 JSX,您还可以更好地了解哪些部分应该或不应该作为单独的组件。
// don't do this
render () {
return (
<div>
{this.renderThing1()}
{this.renderThing2()}
{this.state.thing3Needed && this.renderThing3()}
</div>
);
}
// do this
render () {
return (
<div>
<button onClick={this.toggleThing3Needed}>toggle</button>
<Thing2 randomProp={this.props.foo} />
{this.state.thing3Needed && <Thing3 randomProp2={this.state.bar} />}
</div>
);
}
JavaScript 规范
import
应按类型排序,通常是- 外部库(
react
常常是第一个,以及其他如ttags
、underscore
、classnames
等) - Metabase 的顶级 React 组件和容器(
metabase/components/*
、metabase/containers/*
等) - Metabase 的针对此应用部分特定的 React 组件和容器(
metabase/*/components/*
等) - Metabase 的
lib
、entities
、services
、Redux 文件等
- 外部库(
- 建议使用
const
而不是let
(永远不要使用var
)。只有在有特定理由重新分配标识符时才使用let
(注意:现在 ESLint 强制执行此操作) - 建议使用箭头函数进行内联函数,尤其是当您需要从父作用域引用
this
时(几乎永远不需要做const self = this;
等),但通常即使不这样做也是如此(例如array.map(x => x * 2)
)。 - 建议为顶级函数(包括 React 函数组件)使用函数声明。例外情况是一行函数返回值
// YES:
function MyComponent(props) {
return <div>...</div>;
}
// NO:
const MyComponent = props => {
return <div>...</div>;
};
// YES:
const double = n => n * 2;
// ALSO OK:
function double(n) {
return n * 2;
}
- 建议使用原生
Array
方法而不是underscore
的。我们 polyfill 所有 ES6 功能。对于未原生实现的功能,使用 Underscore。 - 建议使用
async
/await
而不是直接使用promise.then(...)
等。 - 您可以使用赋值解构或参数解构,但应避免深层嵌套解构,因为它们可能难以阅读,并且
prettier
有时会用额外的空白格式化它们。- 避免从“实体”类对象中解构属性,例如,不要这样做:
const { display_name } = column;
- 不要直接解构
this
,例如,const { foo } = this.props; const { bar } = this.state;
而不是const { props: { foo }, state: { bar } } = this;
- 避免从“实体”类对象中解构属性,例如,不要这样做:
- 避免使用嵌套的三元运算符,因为它们通常会生成难以阅读的代码。如果你的代码中有依赖于字符串值的逻辑分支,则更倾向于使用对象作为多个值的映射(当评估很简单时)或
switch
语句(当评估更复杂时,例如在返回哪个React组件时进行分支)
// don't do this
const foo = str == 'a' ? 123 : str === 'b' ? 456 : str === 'c' : 789 : 0;
// do this
const foo = {
a: 123,
b: 456,
c: 789,
}[str] || 0;
// or do this
switch (str) {
case 'a':
return <ComponentA />;
case 'b':
return <ComponentB />;
case 'c':
return <ComponentC />;
case 'd':
default:
return <ComponentD />;
}
如果你的嵌套三元运算符是以评估为布尔值的谓词形式出现的,则更倾向于一个被隔离到单独的纯函数中的if/if-else/else
语句
const foo = getFoo(a, b);
function getFoo(a, b, c) {
if (a.includes("foo")) {
return 123;
} else if (a === b) {
return 456;
} else {
return 0;
}
}
- 对代码库中添加的注释要谨慎。注释不应用作提醒或待办事项–通过在GitHub中创建新问题来记录这些。理想情况下,代码应该以能够清晰地说明其自身的方式编写。如果不能,首先尝试重写代码。如果出于任何原因无法清楚地编写某些内容,请添加注释来解释“为什么”。
// don't do this--the comment is redundant
// get the native permissions for this db
const nativePermissions = getNativePermissions(perms, groupId, {
databaseId: database.id,
});
// don't add TODOs -- they quickly become forgotten cruft
isSearchable(): boolean {
// TODO: this should return the thing instead
return this.isString();
}
// this is acceptable -- the implementer explains a not-obvious edge case of a third party library
// foo-lib seems to return undefined/NaN occasionally, which breaks things
if (isNaN(x) || isNaN(y)) {
return;
}
- 避免在if语句内部使用复杂的逻辑表达式
// don't do this
if (typeof children === "string" && children.split(/\n/g).length > 1) {
// ...
}
// do this
const isMultilineText =
typeof children === "string" && children.split(/\n/g).length > 1;
if (isMultilineText) {
// ...
}
- 使用全部大写字母表示常量
// do this
const MIN_HEIGHT = 200;
// also acceptable
const OBJECT_CONFIG_CONSTANT = {
camelCaseProps: "are OK",
abc: 123,
};
- 优先使用命名导出而不是默认导出
// this makes it harder to search for Widget
import Foo from "./Widget";
// do this to enforce using the proper name
import { Widget } from "./Widget";
- 避免使用魔法字符串和数字
// don't do this
const options = _.times(10, () => ...);
// do this in a constants file
export const MAX_NUM_OPTIONS = 10;
const options = _.times(MAX_NUM_OPTIONS, () => ...);
编写声明式代码
你应该考虑其他工程师编写代码,因为其他工程师花在阅读代码上的时间比你写(和重写)的时间要多。代码更易于阅读,当它告诉计算机“做什么”而不是“怎么做”时。避免使用像for循环这样的命令式模式
// don't do this
let foo = [];
for (let i = 0; i < list.length; i++) {
if (list[i].bar === false) {
continue;
}
foo.push(list[i]);
}
// do this
const foo = list.filter(entry => entry.bar !== false);
在处理业务逻辑时,你不希望关心语言的特定细节。例如,不要写 const query = new Question(card).query();
这意味着实例化一个新的Question
实例并在该实例上调用query
方法,你应该引入一个像getQueryFromCard(card)
这样的函数,这样实现者就可以避免思考如何从一个卡片中获取query
值。
组件样式树环
经典/全局CSS与BEM样式选择器(已弃用)
.Button.Button--primary {
color: -var(--mb-color-brand);
}
原子/工具CSS(不建议使用)
.text-brand {
color: -var(--mb-color-brand);
}
const Foo = () => <div className="text-brand" />;
内联样式(不建议使用)
const Foo = ({ color ) =>
<div style={{ color: color }} />
CSS模块(已弃用)
:local(.primary) {
color: -var(--mb-color-brand);
}
import style from "./Foo.css";
const Foo = () => <div className={style.primary} />;
Emotion
import styled from "@emotion/styled";
const Foo = styled.div`
color: ${props => props.color};
`;
const Bar = ({ color }) => <Foo color={color} />;
弹出框
弹出框是弹出窗口或模态窗口。
在Metabase核心中,它们在视觉上具有响应性:它们出现在触发其出现元素的上方或下方。它们的高度会自动计算以适应屏幕。
在哪里找到弹出框在用户旅程中的位置
在创建自定义问题时
- 从主页点击
新建
然后点击问题
- 👀 自动打开并位于“选择您的起始数据”旁边的选项选择器是一个
<Popover />
。 - 如果尚未选择,请选择
示例数据库
- 选择任何表格,例如
People
在这里,点击以下内容将打开<Popover />
组件
选择列
(在标记为“数据”的部分中的FieldsPicker
控件右侧的箭头)- 标记为“数据”部分的灰色网格加号图标
添加筛选器以缩小答案范围
选择您想要看到的度量标准
选择要按其分组的列
- 在“可视化”按钮上方带有上下箭头的
排序
图标
单元测试
设置模式
我们使用以下模式对组件进行单元测试
import React from "react";
import userEvent from "@testing-library/user-event";
import { Collection } from "metabase-types/api";
import { createMockCollection } from "metabase-types/api/mocks";
import { renderWithProviders, screen } from "__support__/ui";
import CollectionHeader from "./CollectionHeader";
interface SetupOpts {
collection: Collection;
}
const setup = ({ collection }: SetupOpts) => {
const onUpdateCollection = jest.fn();
renderWithProviders(
<CollectionHeader
collection={collection}
onUpdateCollection={onUpdateCollection}
/>,
);
return { onUpdateCollection };
};
describe("CollectionHeader", () => {
it("should be able to update the name of the collection", () => {
const collection = createMockCollection({
name: "Old name",
});
const { onUpdateCollection } = setup({
collection,
});
await userEvent.clear(screen.getByDisplayValue("Old name"));
await userEvent.type(screen.getByPlaceholderText("Add title"), "New title");
await userEvent.tab();
expect(onUpdateCollection).toHaveBeenCalledWith({
...collection,
name: "New name",
});
});
});
关键点
setup
函数renderWithProviders
添加了应用程序使用的提供者,包括redux
请求模拟
我们使用 fetch-mock
来模拟请求
import fetchMock from "fetch-mock";
import { setupCollectionsEndpoints } from "__support__/server-mocks";
interface SetupOpts {
collections: Collection[];
}
const setup = ({ collections }: SetupOpts) => {
setupCollectionsEndpoints({ collections });
// renderWithProviders and other setup
};
describe("Component", () => {
it("renders correctly", async () => {
setup();
expect(await screen.findByText("Collection")).toBeInTheDocument();
});
});
关键点
setup
函数- 从
__support__/server-mocks
调用辅助函数来设置您的数据端点
阅读关于其他 Metabase 版本的文档。