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 on
export 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-promise
export 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,
};
........................................
// 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目录之下。