写在前面该系列文章是为具有开发能力的朋友写作的,目的是帮助他们在scratch 3.0的基础上开发一套完整的集scratch 3.0编程工具、用户社区和作品云端存储及、品牌集成于一体的scratch编程平台。如果您不是开发者,但想要拥有自己的教育平台和品牌,也欢迎学习交流和洽谈合作。所以如果您是想学习scratch少儿编程课程,那请忽略该系列的文章。前言前面我们完成了登录弹窗按钮组件,现在我们开始对接登录接口。开始实现我们先到reducers / user - state.js中添加相关states和actions。
const DONE_USER_LOGIN = 'DONE_USER_LOGIN'; // 标识登录过程成功完成const ERROR_USER_LOGIN = 'ERROR_USER_LOGIN'; // 标识登录过程失败
const initialState = {
error: '',
// 登录错误信息 userData: null, // 用户初始信息 loginState: UserState.NOT_LOGINED, token: '' // 登录获取的token};登录的actions:
const getUserSuccess = data = & gt;
({
type: DONE_USER_LOGIN,
data: data
});
const getUserError = () = & gt;
({
type: ERROR_USER_LOGIN
});
reducer处理登录成功: const reducer = function(state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case DONE_USER_LOGIN:
return ob
ject.assign({}, state, { loginState: UserState.LOGINED,
token: action.data.token,
userData: action.data.user
});
default:
return state;
}
};我们还有一个需要export给提交按钮先来SubmitLoginButton调用的方法,在用户提交时,获取参数然后向后台获取用户信息:
const loginFromServer = (dispatch, account, password) = & gt; {从后台获取用户数据.then(({
data
}) = & gt; {
console.log({
data
});
dispatch(getUserSuccess(data.login));
}).
catch (() = & gt; dispatch(getUserError()));
};从后台获取数据我们稍后实现。记得在最后export这个方法: export {
reducer as
default, initialState as userStateInitialState, UserState, UserStates, getIsLogined, loginFromServer
};再来SubmitLoginButton中,实现我们之前预留的on
click()方法,提交用户输入的信息。 SubmitLoginButton在被时,需要从账号和密码输入框获取用户输入提交。所以它的on
click()方法需要取得LoginModal中的其他表单元素的信息,很显然我们需要将这个处理过程定义到LoginModal中,然后将这一处理过程作为SubmitLoginButton的on click属性值赋给它。这里我们把之前的LoginModal组件进行改造: const LoginModal = props = & gt; ( & lt; Modal className = {
styles.modalContent
}
contentLabel = {
props.title
}
id = & quot; loginModal & quot; onRequestClose = {
props.onCancel
} & gt; & lt; Box & gt; & lt; input className = {
styles.minInput
}
name = & quot; account & quot; placeholder = & quot;账号 & quot; type = & quot; text & quot;
/><br / & gt; & lt; input className = {
styles.minInput
}
name = & quot; password & quot; placeholder = & quot;密码 & quot; type = & quot; password & quot;
/><br / & gt; & lt; SubmitLoginButton className = {
styles.btnSubmit
}
/> </Box & gt; & lt;
/Modal>);之前我们是以方法定义的方式实现它的,如上面。
现在我们用`extends Reactponent`的方式将它改造成受控组件来完成表单内容的接收和提交:
class LoginModal extends React.Component {
constructor(props) {
super(props);
bindAll(this, ['handleAccountChange', 'handlePasswordChange', 'handleSubmit']);
this.state = {
account: '',
password: ''
};
}
handleAccountChange(e) {
this.setState({
account: e.target.value
});
}
handlePasswordChange(e) {
this.setState({
password: e.target.value
});
}
handleSubmit() {
if (this.state.account.trim() === '' || this.state.password.trim() === '') {
alert('账号和密码不能为空');
return;
}
this.props.on
submit(this.state.account, this.state.password); }
render() {
return ( & lt; Modal className = {
styles.modalContent
}
contentLabel = {
this.props.title
}
id = & quot; loginModal & quot; onRequestClose = {
this.props.onCancel
} & gt; & lt; Box & gt; & lt; input className = {
styles.minInput
}
name = & quot; account & quot; placeholder = & quot;账号 & quot; type = & quot; text & quot; value = {
this.state.account
}
on
change = { e = & gt;
this.handleAccountChange(e)
}
/ & gt; & lt; br / & gt; & lt; input className = {
styles.minInput
}
name = & quot; password & quot; placeholder = & quot;密码 & quot; type = & quot; password & quot; value = {
this.state.password
}
on
change = { e = & gt;
this.handlePasswordChange(e)
}
/><br / & gt; & lt; SubmitLoginButton className = {
styles.btnSubmit
}
on
click = { this.handleSubmit
}
/> </Box & gt; & lt;
/Modal> ); }}LoginModal.propTypes = { onCancel: PropTypes.func.isRequired, on
submit: PropTypes.func.isRequired, title: PropTypes.string.isRequired};const mapDispatchToProps = dispatch => ({ onCancel: () => dispatch(closeLoginModal()), on submit: (account, password) => loginFromServer(dispatch, account, password)});export default connect( null, mapDispatchToProps)(LoginModal);上面是我们改造后的LoginModal组件。 里面定义两个state的值用来记录输入的账号和密码。三个方法handleAccountChange,handlePasswordChange和handleSubmit分别处理账号密码的输入和提交请求。
props中的on
submit映射到刚才我们定义的longFromServer方法,将账号和密码传过去,然后与后台服务进行交互。 现在来实现与后台的交互过程。
我们使用apollo client来完成graphql接口与后台的交互。
react-apollo库有完善的使用graphql与后台进行交互并控制组件更新的功能,还包括缓存控制等等,非常强大。但是完整地使用它的话会涉及到对我们scratch里已有的组件的数据绑定和使用的修改,这个工作量不小,而且还可能引入意想不到的问题,所以我们这里只通过apollo client获取数据,更新state来控制我们组件的更新,就像传统的react-redux的工作方式一样。这可能让我们失去使用graphql的一些优势,但是综合考虑成本,我们还是需要适当取舍,毕竟我们是在二次开发别人的项目。
先安装相关的npm包:
npm install graphql apollo - client apollo - cache - inmemory apollo - li
nk - http graphql - tag在lib目录下新建一个apollo.js文件定义apollo client: import { ApolloClient
}
from 'apollo-client';
import {
InMemoryCache
}
from 'apollo-cache-inmemory';
import {
createHttpli
nk }
from 'apollo-li
nk-http'; const client = new ApolloClient({
li
nk: createHttpli nk({ uri: 'localhost:8602/graphql'
}),
cache: new InMemoryCache()
});
export
default client;
localhost:
8602 / graphql就是我们之前实现的后台用户社区服务的graphql接口地址,先暂时把它写死在代码里面。在lib中新建文件夹queries用于存放我们所有的graphql请求,现在在queries下新建目前user用户归类用户相关的请求,在user下新建login.jsx文件,定义我们的登录请求的graphql的query:
import gql from '
graphql - tag ';const LOGIN = gql`query login($account: String!, $password: String!) { login(accountaccount, passwordpassword){ user{ id name account avatar } token }}`;export default LOGIN;现在回到user-state.js中,完善loginFromServer请求后台数据的过程:
const loginFromServer = (dispatch, account, password) => { client.query({ query: LOGIN, variables: { account: account, password: password } }, ).then(({data}) => { console.log({data}); dispatch(getUserSuccess(data.login)); dispatch(closeLoginModal()); }).catch(() => dispatch(getUserError()));};这里的LOGIN就是前面我们定义的graphql query,要注意对照我们之前实现的后台接口返回的数据的结构来获取相应的数据:
{ "data": { "login": { "user": { "id": "3", "name": "馬沙拉", "account": "mashala" "avatar": "https://scratch-store.oss-cn-qingdao.aliyuncs/default-avatar.png" }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjQwNDgzMDMsImlhdCI6MTU2NDA0NDcwMywiaWQiOjIsInVzZXJuYW1lIjoibWFkYXJvdSJ9._rDpgQbOdoI95K7kCdN6K-taHwUF18XTIIn724grIPY" } }}现在我们开启后台服务,重新编译运行scratch-gui,进入浏览器(打开浏览器控制台),登录按钮,输入我们事先通过接口注册的账号和密码,提交,控制台中可以看到成功发送了请求并获取到结果:
MenuBar的右上角的登录按钮消失,出现了默认的scratch-cat的头像和姓名:
这说明我们的基本的登录流程已经成功打通了。只是我们还没有将用户信息应用到MenuBar上面来。
现在我们就来完成这个过程。
前面我们已经将获取到的用户信息记录在了state中的userData中。现在我们要像使用`loginState`一样在gui.jsx中使用它。
在containers/gui.jsx中像之前`loginState`一样映射userData state到组件属性:
loginState: getIsLogined(loginState),userData: state.scratchGui.userState.userData然后在components/gui/gui.jsx中定义的GUIComponentz组件中使用它:
const { accountNavOpen, activeTabIndex, alertsVisible, authorId, authorThumbnailUrl, authorUsername, ba
sePath, 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, on clickAccountNav, onCloseAccountNav, onLogOut, onOpenRegistration, onToggleLoginOpen, onUpdateProjectTitle, on activateCostumesTab, on activateSoundsTab, on activateTab, on clickLogo, onExtensionButton click, onProjectTelemetryEvent, onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, onRequestCloseTelemetryModal, onSeeCommunity, onShare, onTelemetryModalCancel, onTelemetryModalOptIn, onTelemetryModalOptOut, showComingSoon, showLoginModal, soundsTabVisible, stageSizeMode, targetIsStage, telemetryModalVisible, tipsLibraryVisible, vm, loginState, userData, ..ponentProps } = omit(props, ' dispatch ');在GUIComponent的Menubar中使用它:
<MenuBar accountNavOpen={accountNavOpen} authorId={authorId} authorThumbnailUrl={authorThumbnailUrl} authorUsername={authorUsername} canCreateCopy={canCreateCopy} canCreateNew={canCreateNew} canEditTitle={canEditTitle} canRemix={canRemix} canSave={canSave} canShare={canShare} className={styles.menuBarPosition} enableCommunity={enableCommunity} isShared={isShared} loginState={loginState} renderLogin={renderLogin} showComingSoon={showComingSoon} userData={userData} on
clickAccountNav={on clickAccountNav} on clickLogo={on clickLogo} onCloseAccountNav={onCloseAccountNav} onLogOut={onLogOut} onOpenRegistration={onOpenRegistration} onProjectTelemetryEvent={onProjectTelemetryEvent} onSeeCommunity={onSeeCommunity} onShare={onShare} onToggleLoginOpen={onToggleLoginOpen} onUpdateProjectTitle={onUpdateProjectTitle} />再为components/menu-bar/menu-bar.jsx中定义的MenuBar组件添加属性userData: userData: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, account: PropTypes.string.isRequired, avatar: PropTypes.string.isRequired }),最后在之前根据loginState的值展示头像和姓名的地方,将默认头像和名字scratch-cat,替换成userData中的avatar和name:
好,现在我们来整理运行看看效果,输入账号密码,提交登录成功,登录框消失,出现了登录用户的名字和头像(这里我使用了scratch-gui提供的小猫作为没有设置头像的用户的默认头像):
整个登录的流程就按计划打通了。
我们最后再来完善一下登录的错误处理。
如果用户登录失败,登录窗不消失,在登录窗显示登录失败的错误信息。
在前面我们已经定义了登录失败的action:
const getUserError = () => ({ type: ERROR_USER_LOGIN});现在需要在reducer中处理它。
先修改`loginFromServer`的代码,增加后台请求错误的处理:
const loginFromServer = (dispatch, account, password) => { client.query({ query: LOGIN, variables: { account: account, password: password } }, ).then(({data}) => { console.log(`login success${data}`); dispatch(getUserSuccess(data.login)); dispatch(closeLoginModal()); }) .catch(error => { console.log(`login error${error}`); if (error.graphQLErrors.length > 0) { dispatch(getUserError(error.graphQLErrors[0].message)); } else { dispatch(getUserError(error.message)); } });};其中分别处理了graphql的错误,这种错误是业务层面的,比如我们的用户名密码错误这些;另外还有一类更宽泛的错误,如网络出错等。
action `getUserError`现在需要接收错误消息:
const getUserError = data => ({ type: ERROR_USER_LOGIN, data: data});reducer处理错误的情况:
const reducer = function (state, action) { if (typeof state === '
undefined ') state = initialState; switch (action.type) { case DONE_USER_LOGIN: return ob
ject.assign({}, state, { loginState: UserState.LOGINED, token: action.data.token, userData: action.data.user }); case ERROR_USER_LOGIN: return ob ject.assign({}, state, { loginState: UserState.NOT_LOGINED, token: '', userData: null, error: action.data }); default: return state; }};现在我们需要在登录弹窗展示错误信息。 回到我们的LoginModal组件,增加一个展示错误信息的label:
同时我们定义loginError属性和映射它的值到state中的error:
LoginModal.propTypes = { loginError: PropTypes.string.isRequired, onCancel: PropTypes.func.isRequired, on
submit: PropTypes.func.isRequired, title: PropTypes.string.isRequired};const mapStateToProps = state => ({ loginError: state.scratchGui.userState.error});下面我们重新运行程序,输入错误的用户名密码登录,看到出现了错误信息提示: 我们再输入正确的账号密码,成功登录。
好了,我们总算按计划对接完成了整个登录流程。
但是还有一个地方需要我们去完善,为了保证用户刷新页面或者在token过期前重新打开新的页面也能成功获取用户信息,避免不必要的重复登录,我们需要将token存到浏览器本地,并且在页面加载时尝试自动获取用户信息。
我们留到下一章来完成这一过程。