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目录之下。
好啦 这次干货就分享到这里,还需要其他内容请在评论区留言哦,喜欢请点击关注谢谢哦!
閱讀更多 Java雜談 的文章