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 前端架構解析


分享到:


相關文章: