metabase 前端架构解析

metabase前端采用的是基于react的开发框架,并未用到前端应用开发框架如dva,其主要结合react-router、redux-actions、react-router-redux、reselect等主要组件结合而成,需要对上述的插件具有良好理解才能读懂前端源码,另外metabase前端也未采用组件库如antd,完全自己开发的组件。

一:前端store构建过程

(一)redux-actions

此组件主要用来创建action,其中主要API为createAction(s)​、handleAction(s)​,其中createAction负责创建标准action的payload,handleAction主要为包装reducer,负责处理createAction生成的payload,最后进行reducer处理,生成state。示例如下:

actions.js

export const CREATE_PUBLIC_LINK = "metabase/card/CREATE_PUBLIC_LINK";export const createPublicLink = createAction(CREATE_PUBLIC_LINK, ({ id }) => CardApi.createPublicLink({ id }),);

reducers.js

// the card that is actively being worked onexport const card = handleActions( { [RESET_QB]: { next: (state, { payload }) => null }, [CREATE_PUBLIC_LINK]: { next: (state, { payload }) => ({ ...state, public_uuid: payload.uuid }), } }, null,);

其中createAction和handleAction通过唯一表示type(即示例中的CREATE_PUBLIC_LINK)进行关联。handleActions的payload参数则为createAction的返回值,handleActions进行reducer处理,最后生成state变量card。

此外metabase封装了createThunkAction方法,它可接受redux-thunk style thunk,在thunk中可调用dispatch 和getState方法,createAction方法体中也可以进行异步。

// similar to createAction but accepts a (redux-thunk style) thunk and dispatches based on whether// the promise returned from the thunk resolves or rejects, similar to redux-promiseexport function createThunkAction(actionType, actionThunkCreator) { function fn(...actionArgs) { var thunk = actionThunkCreator(...actionArgs); return async function(dispatch, getState) { try { let payload = await thunk(dispatch, getState); let dispatchValue = { type: actionType, payload }; dispatch(dispatchValue);  return dispatchValue; } catch (error) { dispatch({ type: actionType, payload: error, error: true }); throw error; } }; } fn.toString = () => actionType; return fn;}

createThunkCreator主要返回了一个异步方法,在其中调用了异步的方法,然后dispatch相应的actionType,其中参数dispatch和getState则通过mapDispatchToProps传入。

这样通过redux-action组件中的createAction和handleAction以及自定义的createThunkAction,就建立起整个前台的state状态树。

另外action的写法除了createAction和createThunkAction之外,还可直接编写方法如frontend/metabase/quey_builder/actions.js中的initializeQB方法,其代码如下:

export const initializeQB = (location, params) => { return async (dispatch, getState) => { // do this immediately to ensure old state is cleared before the user sees it dispatch(resetQB()); dispatch(cancelQuery());  const { currentUser } = getState();  let card, databasesList, originalCard; let uiControls: UiControls = { isEditing: false, isShowingTemplateTagsEditor: false, };  // always start the QB by loading up the databases for the application try { await dispatch(fetchDatabases()); databasesList = getDatabasesList(getState()); } catch (error) { console.error("error fetching dbs", error);  // if we can't actually get the databases list then bail now dispatch(setErrorPage(error));  return { uiControls }; }  // load up or initialize the card we'll be working on let options = {}; let serializedCard; // hash can contain either query params starting with ? or a base64 serialized card if (location.hash) { let hash = location.hash.replace(/^#/, ""); if (hash.charAt(0) === "?") { options = querystring.parse(hash.substring(1)); } else { serializedCard = hash; } } const sampleDataset = _.findWhere(databasesList, { is_sample: true });  let preserveParameters = false; if (params.cardId || serializedCard) { // existing card being loaded try { // if we have a serialized card then unpack it and use it card = serializedCard ? deserializeCardFromUrl(serializedCard) : {};  // load the card either from `cardId` parameter or the serialized card if (params.cardId) { card = await loadCard(params.cardId); // when we are loading from a card id we want an explicit clone of the card we loaded which is unmodified originalCard = Utils.copy(card); // for showing the "started from" lineage correctly when adding filters/breakouts and when going back and forth // in browser history, the original_card_id has to be set for the current card (simply the id of card itself for now) card.original_card_id = card.id; } else if (card.original_card_id) { // deserialized card contains the card id, so just populate originalCard originalCard = await loadCard(card.original_card_id); // if the cards are equal then show the original if (cardIsEquivalent(card, originalCard)) { card = Utils.copy(originalCard); } }  MetabaseAnalytics.trackEvent( "QueryBuilder", "Query Loaded", card.dataset_query.type, );  // if we have deserialized card from the url AND loaded a card by id then the user should be dropped into edit mode uiControls.isEditing = !!options.edit;  // if this is the users first time loading a saved card on the QB then show them the newb modal if (params.cardId && currentUser.is_qbnewb) { uiControls.isShowingNewbModal = true; MetabaseAnalytics.trackEvent("QueryBuilder", "Show Newb Modal"); }  if (card.archived) { // use the error handler in App.jsx for showing "This question has been archived" message dispatch( setErrorPage({ data: { error_code: "archived", }, context: "query-builder", }), ); card = null; }  preserveParameters = true; } catch (error) { console.warn("initializeQb failed because of an error:", error); card = null; dispatch(setErrorPage(error)); } } else if (options.tutorial !== undefined && sampleDataset) { // we are launching the QB tutorial card = startNewCard("query", sampleDataset.id);  uiControls.isShowingTutorial = true; MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Start", true); } else { // we are starting a new/empty card // if no options provided in the hash, redirect to the new question flow if ( !options.db && !options.table && !options.segment && !options.metric ) { await dispatch(redirectToNewQuestionFlow()); return; }  const databaseId = options.db ? parseInt(options.db) : undefined; card = startNewCard("query", databaseId);  // initialize parts of the query based on optional parameters supplied if (options.table != undefined && card.dataset_query.query) { card.dataset_query.query.source_table = parseInt(options.table); }  if (options.segment != undefined && card.dataset_query.query) { card.dataset_query.query.filter = [ "AND", ["SEGMENT", parseInt(options.segment)], ]; }  if (options.metric != undefined && card.dataset_query.query) { card.dataset_query.query.aggregation = [ "METRIC", parseInt(options.metric), ]; }  MetabaseAnalytics.trackEvent( "QueryBuilder", "Query Started", card.dataset_query.type, ); }  /**** All actions are dispatched here ****/  // Update the question to Redux state together with the initial state of UI controls dispatch.action(INITIALIZE_QB, { card, originalCard, uiControls, });  // Fetch alerts for the current question if the question is saved card && card.id && dispatch(fetchAlertsForQuestion(card.id));  // Fetch the question metadata card && dispatch(loadMetadataForCard(card));  const question = card && new Question(getMetadata(getState()), card);  // if we have loaded up a card that we can run then lets kick that off as well if (question) { if (question.canRun()) { // NOTE: timeout to allow Parameters widget to set parameterValues setTimeout( () => // TODO Atte Keinänen 5/31/17: Check if it is dangerous to create a question object without metadata dispatch(runQuestionQuery({ shouldUpdateUrl: false })), 0, ); }  // clean up the url and make sure it reflects our card state const originalQuestion = originalCard && new Question(getMetadata(getState()), originalCard); dispatch( updateUrl(card, { dirty: !originalQuestion || (originalQuestion && question.isDirtyComparedTo(originalQuestion)), replaceState: true, preserveParameters, }), ); } };};

如果通过dispatch调用相关的action,方法如下:

(1)dispatch(resetQB()); 其中resetQB为creatAction方法。

(2)dispatch.action(SET_CARD_AND_RUN, { card, originalCard });直接调用reducers中的handleAction.

(二)reselect组件

示例如下:

export const getIsDirty = createSelector( [getCard, getOriginalCard], (card, originalCard) => { return isCardDirty(card, originalCard); },);

reselect组件具有缓存功能,其首先会调用参数一集合中的方法,各返回结果作为参数二方法的集合,如果返回结果较上一次调用没有变化,则不会调用参数二方法进行计算。

集合react-redux组件中的mapStateToProps方法,通过selector计算state状态,最后作为props传递给react组件。

二:TypeScript

metabase前端代码中还采用了TypeScript方法,并对前端用的概念实体进行了type定义,路径为:/frontend/metabase/meta目录之下。

好啦 这次干货就分享到这里,还需要其他内容请在评论区留言哦,喜欢请点击关注谢谢哦!

metabase 前端架构解析


分享到:


相關文章: