前端
實體載入器
如果您正在開發新功能,或通常需要在前端取得一些應用程式資料,實體載入器會是您的好幫手。它們可以抽象化 API 呼叫、處理載入和錯誤狀態、快取先前載入的物件、使快取失效(在某些情況下),並讓您輕鬆執行更新或建立新項目。
實體載入器的良好用途
- 我需要取得特定的 X (使用者、資料庫等) 並顯示它。
- 我需要取得 X (資料庫、問題等) 的列表並顯示它。
目前可用的實體
- 問題、儀表板、脈衝
- 集合
- 資料庫、表格、欄位、區隔、指標
- 使用者、群組
- 完整的實體列表在此處:https://github.com/metabase/metabase/tree/master/frontend/src/metabase/entities
有兩種使用載入器的方式,一種是作為 React "render prop" 元件,另一種是作為 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 的資料庫。如果您想改用 render prop 元件,您的程式碼會像這樣。
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
)通常採用物件的options
陣列,其中包含name
和value
屬性。 - 名稱類似
FooModal
和FooPopover
的元件通常指的是應在Modal
/ModalWithTrigger
或Popover
/PopoverWithTrigger
內使用的 modal/popover *內容*。 -
名稱類似
FooWidget
的元件通常在PopoverWithTrigger
內包含FooPopover
,並帶有一些觸發元素,通常是FooName
。 - 如果您需要在類別中繫結方法(而不是建構函式中的
this.method = this.method.bind(this);
),請使用箭頭函式實例屬性,但前提是函式需要繫結(例如,如果您將其作為 prop 傳遞給 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
和 "atomic" / "utility-first" CSS 類別。 - 偏好使用
grid-styled
的Box
和Flex
元件,而不是原始div
。 - 元件通常應將其
className
prop 傳遞給元件的根元素。可以使用classnames
套件中的cx
函式將其與其他類別合併。 - 為了使元件更具可重複使用性,元件應僅將類別或樣式套用至元件的根元素,這會影響其自身內容的版面配置/樣式,但 *不* 會影響其自身在其父容器內的版面配置。例如,它可以包含 padding 或
flex
類別,但不應包含 margin 或flex-full
、full
、absolute
、spread
等。這些應由元件的使用者透過className
或style
props 傳遞,使用者知道元件應如何在其自身內定位。 - 避免在單個元件中將 JSX 分解為單獨的方法呼叫。偏好內聯 JSX,以便您可以更好地了解
render
方法傳回的 JSX 與元件的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
s、entities
、services
、Redux 檔案等。
- 外部函式庫(
- 偏好使用
const
而非let
(並且永遠不要使用var
)。僅當您有重新指派識別符的特定原因時才使用let
(注意:現在由 ESLint 強制執行)。 - 偏好將 箭頭函式 用於內聯函式,特別是當您需要從父作用域參考
this
時(幾乎永遠不需要執行const self = this;
等),但通常即使您不這樣做也是如此(例如array.map(x => x * 2)
)。 - 偏好將
function
宣告用於頂層函式,包括 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) {
// ...
}
- 對於常數使用 ALL_CAPS。
// 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);
在處理商業邏輯時,您不希望擔心語言的細節。您應該引入類似 getQueryFromCard(card)
的函式,而不是撰寫 const query = new Question(card).query();
這樣的程式碼,因為這需要實例化一個新的 Question
實例,並在該實例上呼叫 query
方法。如此一來,實作者就可以避免思考如何從卡片中取得 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 Modules (已棄用)
: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} />;
Popover
Popover 是彈出視窗或對話框。
在 Metabase core 中,它們在視覺上具有響應性:它們會出現在觸發其出現的元素上方或下方。它們的高度會自動計算,以使其符合螢幕大小。
在使用者旅程中何處尋找 Popover
建立自訂問題時
- 從首頁點擊
New
,然後點擊Question
- 👀 自動在
Pick your starting data
旁邊開啟的選項選擇器是一個<Popover />
。 - 如果尚未選取,請選擇
Sample Database
- 選擇任何表格,例如
People
在此,點擊以下項目將開啟 <Popover />
元件
Pick columns
(標示為Data
的區段中FieldsPicker
控制項右側的箭頭)- 標示為
Data
區段下方帶有 + 號的網格灰色圖示 新增篩選條件以縮小您的答案範圍
選擇您想看到的指標
選擇要分組的欄位
- 位於
Visualize
按鈕上方的Sort
圖示,帶有向上和向下箭頭
單元測試
設定模式
我們使用以下模式來單元測試元件
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
新增應用程式使用的 providers,包括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
呼叫 helpers 以為您的資料設定端點
閱讀其他版本的 Metabase 文件。