**寫在前面**
該系列文章是為具有開發能力的朋友寫作的,目的是幫助他們在刮擦3.0的基礎上開發一套完整的集刮擦3.0編程工具,用戶社區和作品云端存儲及分享,品牌集成于一體的刮擦編程平臺。如果您不是開發者,但想要擁有自己的教育平臺和品牌,也歡迎學習交流和洽談合作。
所以如果您是想學習scratch少兒編程課程,那請忽略該系列的文章。
**前言**
前面我們將scratch-gui工程成功建造運行起來,并且成功植入了我們的品牌徽標,這讓我們對整個項目有了初步的認識。
現在我們已經有了scratch編程工具,但是我們還剩下兩個主要的后臺,用戶社區后臺和GUI的存儲后臺。目前Scratch3.0團隊還沒有發現社區替換和GUI的存儲部分是否有開源計劃,考慮到Scratch2.0的重新初始化開源,3.0社區初始化開源的可能也不大。
scratch-www項目提供了用戶社區的功能,但是需要通過接口去分析它的后臺的數據存儲的結構,我覺得比較麻煩,不如我們自己來開發一個,集成到我們的編程工具scratch-gui中。
所以接下來我們的工作是自己來提供相關的兩個前提平臺,并與GUI集成到一起。
**約會用戶登錄狀態**
我們先一步一步來,先做一個比較簡單的用戶系統,再一步一步迭代。
這一章,我們先來改造一下前面的scratch-gui,約會用戶登錄狀態的檢測。
在進入項目時,檢測用戶是否登錄,如果用戶未登錄,則在右上角顯示登錄按鈕,否則顯示用戶頭像和姓名等基本信息。
先在減少器目錄中創建user-state.js文件,用作記錄用戶的信息。
添加如下內容:
> import keyMirror from 'keymirror';
>
> const UserState = keyMirror({
>
> ? ? NOT\_LOGINED: null,
>
> ? ? LOGINED: null
>
> });
>
> const UserStates = Object.keys(UserState)
>
> const initialState = {
>
> ? ? error: null,
>
> ? ? userData: null,
>
> ? ? loginState: UserState.NOT\_LOGINED
>
> };
>
> const getIsLogined = loginState => (
>
> ? ? loginState === UserState.LOGINED
>
> );
>
> const reducer = function (state, action) {
>
> ? ? if (typeof state === 'undefined') state = initialState;
>
> }
在減少器/gui.js中,作為項目的用戶相關的初始化信息。
在reducer / gui.js中日期用戶狀態:
> ???
>
> ~~~
> import userStateReducer, {userStateInitialState} from './user-state';
> ```
> 加入到initialState中:
> ```
> const guiInitialState = {
> ? ? alerts: alertsInitialState,
> ? ? assetDrag: assetDragInitialState,
> ? ? blockDrag: blockDragInitialState,
> ? ? cards: cardsInitialState,
> ? ? colorPicker: colorPickerInitialState,
> ? ? connectionModal: connectionModalInitialState,
> ? ? customProcedures: customProceduresInitialState,
> ? ? editorTab: editorTabInitialState,
> ? ? mode: modeInitialState,
> ? ? hoveredTarget: hoveredTargetInitialState,
> ? ? stageSize: stageSizeInitialState,
> ? ? menus: menuInitialState,
> ? ? micIndicator: micIndicatorInitialState,
> ? ? modals: modalsInitialState,
> ? ? monitors: monitorsInitialState,
> ? ? monitorLayout: monitorLayoutInitialState,
> ? ? projectChanged: projectChangedInitialState,
> ? ? projectState: projectStateInitialState,
> ? ? projectTitle: projectTitleInitialState,
> ? ? fontsLoaded: fontsLoadedInitialState,
> ? ? restoreDeletion: restoreDeletionInitialState,
> ? ? targets: targetsInitialState,
> ? ? timeout: timeoutInitialState,
> ? ? toolbox: toolboxInitialState,
> ? ? vm: vmInitialState,
> ? ? vmStatus: vmStatusInitialState,
> ? ? userState: userStateInitialState
> };
> ```
> 將reducer加入到guiReducer中:
> ```
> const guiReducer = combineReducers({
> ? ? alerts: alertsReducer,
> ? ? assetDrag: assetDragReducer,
> ? ? blockDrag: blockDragReducer,
> ? ? cards: cardsReducer,
> ? ? colorPicker: colorPickerReducer,
> ? ? connectionModal: connectionModalReducer,
> ? ? customProcedures: customProceduresReducer,
> ? ? editorTab: editorTabReducer,
> ? ? mode: modeReducer,
> ? ? hoveredTarget: hoveredTargetReducer,
> ? ? stageSize: stageSizeReducer,
> ? ? menus: menuReducer,
> ? ? micIndicator: micIndicatorReducer,
> ? ? modals: modalReducer,
> ? ? monitors: monitorReducer,
> ? ? monitorLayout: monitorLayoutReducer,
> ? ? projectChanged: projectChangedReducer,
> ? ? projectState: projectStateReducer,
> ? ? projectTitle: projectTitleReducer,
> ? ? fontsLoaded: fontsLoadedReducer,
> ? ? restoreDeletion: restoreDeletionReducer,
> ? ? targets: targetReducer,
> ? ? timeout: timeoutReducer,
> ? ? toolbox: toolboxReducer,
> ? ? vm: vmReducer,
> ? ? vmStatus: vmStatusReducer,
> ? ? userState: userStateReducer
> });
> ~~~
>
>
下面去container / gui.jsx中為里面定義的GUI Component添加loginState這個道具,使用標識用戶是否登錄:
>
>
> ~~~
> GUI.propTypes = {
> ? ? assetHost: PropTypes.string,
> ? ? children: PropTypes.node,
> ? ? cloudHost: PropTypes.string,
> ? ? error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
> ? ? fetchingProject: PropTypes.bool,
> ? ? intl: intlShape,
> ? ? isError: PropTypes.bool,
> ? ? isLoading: PropTypes.bool,
> ? ? isScratchDesktop: PropTypes.bool,
> ? ? isShowingProject: PropTypes.bool,
> ? ? loadingStateVisible: PropTypes.bool,
> ? ? onProjectLoaded: PropTypes.func,
> ? ? onSeeCommunity: PropTypes.func,
> ? ? onStorageInit: PropTypes.func,
> ? ? onUpdateProjectId: PropTypes.func,
> ? ? onUpdateProjectTitle: PropTypes.func,
> ? ? onUpdateReduxProjectTitle: PropTypes.func,
> ? ? onVmInit: PropTypes.func,
> ? ? projectHost: PropTypes.string,
> ? ? projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
> ? ? projectTitle: PropTypes.string,
> ? ? telemetryModalVisible: PropTypes.bool,
> ? ? vm: PropTypes.instanceOf(VM).isRequired,
> ? ? loginState: PropTypes.bool
> };
> ~~~
>
>
這個`loginState`道具的狀態值來自于user-state.js中getIsLogined中檢測當前的loginState(指狀態中的)是否等于UserState.LOGINED:
>
>
> ~~~
> const mapStateToProps = state => {
> ? ? const loadingState = state.scratchGui.projectState.loadingState;
> ? ? const loginState = state.scratchGui.userState.loginState;
> ? ? return {
> ? ? ? ? activeTabIndex: state.scratchGui.editorTab.activeTabIndex,
> ? ? ? ? alertsVisible: state.scratchGui.alerts.visible,
> ? ? ? ? backdropLibraryVisible: state.scratchGui.modals.backdropLibrary,
> ? ? ? ? blocksTabVisible: state.scratchGui.editorTab.activeTabIndex === BLOCKS_TAB_INDEX,
> ? ? ? ? cardsVisible: state.scratchGui.cards.visible,
> ? ? ? ? connectionModalVisible: state.scratchGui.modals.connectionModal,
> ? ? ? ? costumeLibraryVisible: state.scratchGui.modals.costumeLibrary,
> ? ? ? ? costumesTabVisible: state.scratchGui.editorTab.activeTabIndex === COSTUMES_TAB_INDEX,
> ? ? ? ? error: state.scratchGui.projectState.error,
> ? ? ? ? isError: getIsError(loadingState),
> ? ? ? ? isFullScreen: state.scratchGui.mode.isFullScreen,
> ? ? ? ? isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
> ? ? ? ? isRtl: state.locales.isRtl,
> ? ? ? ? isShowingProject: getIsShowingProject(loadingState),
> ? ? ? ? loadingStateVisible: state.scratchGui.modals.loadingProject,
> ? ? ? ? projectId: state.scratchGui.projectState.projectId,
> ? ? ? ? soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX,
> ? ? ? ? targetIsStage: (
> ? ? ? ? ? ? state.scratchGui.targets.stage &&
> ? ? ? ? ? ? state.scratchGui.targets.stage.id === state.scratchGui.targets.editingTarget
> ? ? ? ? ),
> ? ? ? ? telemetryModalVisible: state.scratchGui.modals.telemetryModal,
> ? ? ? ? tipsLibraryVisible: state.scratchGui.modals.tipsLibrary,
> ? ? ? ? vm: state.scratchGui.vm,
> ? ? ? ? loginState: getIsLogined(loginState)
> ? ? };
> };
> ~~~
>
>
現在container / gui.jsx中定義的Component GUI具有登錄狀態屬性了,我們要把它傳到menu-bar中,因為我們要在menu-bar中去控制右上角的顯示狀態。
在這個GUI組件中使用了components / gui / gui.jsx定義的GUIComponent這個組件,GUIComponent定義了整個項目的基本樣式結構中,可以找到對MenuBar的使用。
首先,在GUIComponent的定義中日期之前定義的`loginState`:
>
>
> ~~~
> const GUIComponent = props => {
> ? ? const {
> ? ? ? ? accountNavOpen,
> ? ? ? ? activeTabIndex,
> ? ? ? ? alertsVisible,
> ? ? ? ? authorId,
> ? ? ? ? authorThumbnailUrl,
> ? ? ? ? authorUsername,
> ? ? ? ? basePath,
> ? ? ? ? backdropLibraryVisible,
> ? ? ? ? backpackHost,
> ? ? ? ? backpackVisible,
> ? ? ? ? blocksTabVisible,
> ? ? ? ? cardsVisible,
> ? ? ? ? canCreateNew,
> ? ? ? ? canEditTitle,
> ? ? ? ? canRemix,
> ? ? ? ? canSave,
> ? ? ? ? canCreateCopy,
> ? ? ? ? canShare,
> ? ? ? ? canUseCloud,
> ? ? ? ? children,
> ? ? ? ? connectionModalVisible,
> ? ? ? ? costumeLibraryVisible,
> ? ? ? ? costumesTabVisible,
> ? ? ? ? enableCommunity,
> ? ? ? ? intl,
> ? ? ? ? isCreating,
> ? ? ? ? isFullScreen,
> ? ? ? ? isPlayerOnly,
> ? ? ? ? isRtl,
> ? ? ? ? isShared,
> ? ? ? ? loading,
> ? ? ? ? renderLogin,
> ? ? ? ? onClickAccountNav,
> ? ? ? ? onCloseAccountNav,
> ? ? ? ? onLogOut,
> ? ? ? ? onOpenRegistration,
> ? ? ? ? onToggleLoginOpen,
> ? ? ? ? onUpdateProjectTitle,
> ? ? ? ? onActivateCostumesTab,
> ? ? ? ? onActivateSoundsTab,
> ? ? ? ? onActivateTab,
> ? ? ? ? onClickLogo,
> ? ? ? ? onExtensionButtonClick,
> ? ? ? ? onProjectTelemetryEvent,
> ? ? ? ? onRequestCloseBackdropLibrary,
> ? ? ? ? onRequestCloseCostumeLibrary,
> ? ? ? ? onRequestCloseTelemetryModal,
> ? ? ? ? onSeeCommunity,
> ? ? ? ? onShare,
> ? ? ? ? onTelemetryModalCancel,
> ? ? ? ? onTelemetryModalOptIn,
> ? ? ? ? onTelemetryModalOptOut,
> ? ? ? ? showComingSoon,
> ? ? ? ? soundsTabVisible,
> ? ? ? ? stageSizeMode,
> ? ? ? ? targetIsStage,
> ? ? ? ? telemetryModalVisible,
> ? ? ? ? tipsLibraryVisible,
> ? ? ? ? vm,
> ? ? ? ? loginState,
> ? ? ? ? ...componentProps
> ? ? } = omit(props, 'dispatch');
> ? ? ...
> ~~~
>
>
再在使用MenuBar的地方也為MenuBar定義`loginState`屬性,它的值就是GUIComponent傳進來的`loginState`的值:
>
>
>
最后修改components / menu-bar.jsx中的MenuBar組件的顯示,將右上角替換成:
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? {this.props.loginState ? (
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? className={classNames(
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? styles.menuBarItem,
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? styles.hoverable,
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? styles.mystuffButton
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? )}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? >
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? className={styles.mystuffIcon}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? src={mystuffIcon}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? />
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? id="account-nav"
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? place={this.props.isRtl ? 'right' : 'left'}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? >
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? className={classNames(
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? styles.menuBarItem,
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? styles.hoverable,
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? styles.accountNavMenu
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? )}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? >
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? className={styles.profileIcon}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? src={profileIcon}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? />
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? {'scratch-cat'}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? className={styles.dropdownCaretIcon}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? src={dropdownCaret}
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? />
>
> ? ? ? ? ? ? ? ? ? ? ? ? ? ? ) : Login}
如果用戶已登錄,就顯示頭像和姓名的樣式(具體的用戶信息需要跟后臺打通,我們后面再實現):

否則顯示登錄按鈕:

我們可以通過修改reducers / user-state.js中的loginState的初始值來查看效果:
> loginState: UserState.NOT\_LOGINED
> loginState: UserState.LOGINED
這個值我們會在后面根據用戶登錄的token去獲取。
為了與項目整體風格一致,我們修改這個登錄按鈕的樣式,在菜單欄目錄中添加login-button.css和login-button.jsx文件,內容分別如下:
> ??
>
> ~~~
> @import "../../css/colors.css";
>
> .login-button { background: $data-primary;}
> ~~~
>
>
> import classNames from 'classnames';
>
> import {FormattedMessage} from 'react-intl';
>
> import PropTypes from 'prop-types';
>
> import React from 'react';
>
> import Button from '../button/button.jsx';
>
> import styles from './login-button.css';
>
> const LoginButton = ({
>
> ? ? className,
>
> ? ? onClick
>
> }) => (
>
> ? ? ? ? className={classNames(
>
> ? ? ? ? ? ? className,
>
> ? ? ? ? ? ? styles.loginButton
>
> ? ? ? ? )}
>
> ? ? ? ? onClick={onClick}
>
> ? ? >
>
> ? ? ? ? ? ? defaultMessage="Login"
>
> ? ? ? ? ? ? description="Label for login"
>
> ? ? ? ? ? ? id="gui.menuBar.login"
>
> ? ? ? ? />
>
> );
>
> LoginButton.propTypes = {
>
> ? ? className: PropTypes.string,
>
> ? ? onClick: PropTypes.func
>
> };
>
> LoginButton.defaultProps = {
>
> ? ? onClick: () => {}
>
> };
>
> export default LoginButton;
然后在menu-bar.jsx中如下使用:
>
這樣看起來就好看多了:

好了,這里接收完成后,我們接下來就可以實現一個后臺系統,然后對接后臺系統登錄和獲取用戶信息了。