完成上一教程的“额外任务”后,我们现在可以开始编写“我的”页面了。
页面分析
“我的”页面的页面布局与“发现”页面基本保持一致,应该可以从“发现”页面中“借鉴”一些代码;“我的”页面的页面内入主要是“当前用户”的事件列表。
我们需要从后台获取“当前用户”的事件列表,我们需要告诉服务器,“我是谁”。所以,我们需要带着代表用户身份的 Token 访问 API 服务器,API 服务器才能知道“我是谁”,进而拿到对应的数据。但是我们要怎样才能拿到代表用户身份的 Token 呢?这里我们需要使用 Feat.com 根据 “OAuth 2.0 Implicit Grant Type 认证机制” 提供的方法来获取。通过这个机制,认证服务会给我们的应用发放一个 Token ,我们的应用带着这个 Token 对 API 服务器进行访问,就可以拿到想要的数据了。
下图是 OAuth 2.0 Implicit Grant Type 的流程:
获取用户授权的准备工作 — 创建OAuth应用
访问 https://www.feat.com/application ,在 feat.com 上创建应用(你可能需要先注册登录)
- “Grant Type” 选择 “Implicit”。
- 填写
Redirect URIs
出于安全性的考虑中, feat.com 不支持将
Redirect URIs
设置为 `localhost`, `127.0.0.1` 这一类域名。所以在本地开发时,需要使用 `192.168.1.xxx` 这一类本地路由的ip(你可能需要对配置一下防火墙,确定浏览器可以通过外部IP地址访问开发服务器)。
组装授权连接
OAuth 应用创建完成之后,我们需要根据 featapi.com 上的文档说明,在应用内组合一个认证链接,当用户点击“认证”按钮时,页面需要转跳到 feat.com 上进行授权。
新建文件 src/AuthRequiredHint.js
,并编辑:
import React from 'react'
import { stringify } from 'qs'
const getAuthorizeLink = (next) => {
const authorizeUrl = process.env.REACT_APP_FEAT_AUTHORIZE_URL
const params = {
client_id: process.env.REACT_APP_FEAT_CLIENT_ID,
redirect_uri: window.location.origin + '/authorize'+(next ? `?${stringify({next})}` : '' ),
response_type: 'token'
}
return `${authorizeUrl}?${stringify(params)}`
}
function AuthRequiredHint(props) {
return (
<div className='Landing'>
<div className='Landing__inner'>
<div className='Landing__desc'>
应用需要获取到您在feat.com上的授权后,才能进行下一步操作
</div>
<div className='Landing__action'>
<a className='button' href={getAuthorizeLink(props.pathname)}>
开始授权
</a>
</div>
</div>
</div>
)
}
export default AuthRequiredHint;
上面的代码,我们使用 location.origin
来组装 redirect_uri
, 这样做是为了方便我们线上部署。假设我们将应用部署到http://filex-demo.featapi.com
,那么redirect_uri
就是 http://filex-demo.featapi.com/authorize
,我们只需要修改一下feat.com上的应用设置即可重新oauth认证对接好。
打开 .env.local
,在配置文件中添加两项
REACT_APP_FEAT_AUTHORIZE_URL=https://www.feat.com/authorize
REACT_APP_FEAT_CLIENT_ID=__Replace_This_With_Your_App_Client_ID__
__Replace_This_With_Your_App_Client_ID__
需要替换为你在feat.com上创建的应用的Client ID。修改完成之后,需要重启开发服务器。
打开 src/index.css
,在文件底部添加样式
.Landing {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background: white;
}
.Landing__inner {
width: 80%;
max-width: 280px;
}
.Landing__desc {
margin-bottom: 16px;
}
.Landing__action {
padding-top: 8px;
padding-bottom: 8px;
text-align: center;
}
最后,修改 src/Me.js
import React from 'react';
import AuthRequiredHint from './AuthRequiredHint'
function Me() {
return (
<AuthRequiredHint />
)
}
export default Me;
打开浏览器,访问 http://192.168.1.122/me
, 可以看到的效果如下图:
注意通过局域网IP进行访问,你的地址应该会跟这个不一样。
点击“开始授权”按钮,浏览器会打开 feat.com 上的授权页面,如无意外,浏览器的页面大致如下图所示:
如果出现 403 错误,请先确认应用不是通过 `localhost` 或者 `127.0.0.1` 等本机IP地址访问。
我们点击“授权”,页面最终应该会转跳回“发现”页面。那我们怎么能获取到Token呢?我们来分析一下。feat.com页面已经成功的将页面转跳回我们的应用,那说明feat.com上的授权动作是已经完成了,那我们的应用为什么会直接返回“发现”页面,而不是“我的”页面,或者其他页面(好像没有其他页面)呢。我们来检查一下我们的前端路由设置:
function App () {
return (
<Router>
<Switch>
<Route path='/explore' exact component={Explore} />
<Route path='/me' exact component={Me} />
<Route path='/user/:uid' exact component={UserPage} />
<Route path='/event/new' exact component={EventCreation} />
<Route path='/event/:id/comment' exact component={EventComment} />
<Route
path='/event/:id/comment/:cid/edit'
exact
component={CommentEdit}
/>
<Route
path='/event/:id/comment/:cid/reply'
exact
component={CommentReply}
/>
<Route path='/event/:id/comment/new' exact component={CommentCreate} />
<Redirect to='/explore' />
</Switch>
</Router>
)
}
我们可以看到,最底下有一个重定向的设置,当没有匹配到任何路由时,会回到“发现”页面。那很有可能我们的重定向地址没有在路由中进行声明。经过观察(实际上并没有观察,直接搜索/authorize
就好),我们的确没有在路由上面添加任何对应/authorize
路径的代码,
在重定向路由中,提取用户token信息
接下来,我们添加代码来处理/authorize
。
编辑 src/App.js
import Authorize from './Authorize'
function App () {
return (
<Router>
<Switch>
<Route path='/explore' exact component={Explore} />
<Route path='/me' exact component={Me} />
<Route path='/user/:uid' exact component={UserPage} />
<Route path='/event/new' exact component={EventCreation} />
<Route path='/event/:id/comment' exact component={EventComment} />
<Route
path='/event/:id/comment/:cid/edit'
exact
component={CommentEdit}
/>
<Route
path='/event/:id/comment/:cid/reply'
exact
component={CommentReply}
/>
<Route path='/event/:id/comment/new' exact component={CommentCreate} />
<Route path='/authorize' exact component={Authorize} />
<Redirect to='/explore' />
</Switch>
</Router>
)
}
新建src/Authorize.js
import React from 'react'
import { parse } from 'qs'
function Authorize(props) {
const {
location: {
search,
hash,
}
} = props;
const token = hash.slice(1);
const query = parse(search.slice(1));
console.log(token, query);
// 通过获取当前用户信息来验证是否已成功获取Token
fetch(`${process.env.REACT_APP_FEAT_API_ENDPOINT}/api/user/basic-info/`, {
method: 'GET',
headers: {
Authorization: `${query.token_type} ${token}`,
Accept: 'application/json',
}
})
.then((res) => res.json())
.then(({ data }) => {
console.log(data);
})
.catch((err) => {
console.log(err);
})
return (
<div>
Authorize
</div>
)
}
export default Authorize;
当前用户基本信息的API的相关说明,可以到 featapi.com 上查阅
打开浏览器,访问“我的”页面,完成认证流程,返回我们的应用,浏览器页面中可以看到:
在 console 中可以看到:
到这里,我们就已经把 OAuth 2.0 Implicit Grant Type 的授权对接上了。接下来,我们就要想办法将 Token 信息存起来,方便应用后续的调用。
保存用户 Token
在一个开始的时候,我们修改了 src/Me.js
的代码,直接显示AuthRequiredHint
的内容。其实完成的逻辑应该是:
function Me() {
if (hasTokenInfo) {
return (
<div>
My Event List
</div>
)
}
return <AuthRequiredHint />
}
接下来,我们就要在“全局”范围内,存储token信息,并考虑中提供一些辅助方法来帮助代码编写。现阶段,我们先使用 React 的 Context API 来存储 token 数据。
这里的“全局”不一定是指运行环境的global,只是想说这个应该在足够上层的地方存储 Token 信息,这样整个应用范围内都拿到相关的参数。
准备存储 token 信息的地方
新建 src/AuthInfoProvider.js
,并编辑
import React, { useReducer, useEffect } from 'react';
const defaultState = {
currentUser: null,
token: undefined,
}
const STORAGE_KEY = 'authInfo';
const reducer = (state, action) => {
switch (action.type) {
case 'set-token':
return {
...state,
token: action.payload,
}
case 'set-current-user':
return {
...state,
currentUser: action.payload,
};
case 'reset':
return defaultState;
default:
return state;
}
}
let initState;
try {
initState = JSON.parse(localStorage.getItem(STORAGE_KEY));
} catch {
}
initState = initState || defaultState;
export const AuthInfo = React.createContext(initState);
function AuthInfoProvider(props) {
const [state, dispatch] = useReducer(reducer, initState);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}, [state]);
return (
<AuthInfo.Provider
value={{
state,
dispatch,
}}
>
{props.children}
</AuthInfo.Provider>
)
}
export default AuthInfoProvider;
编辑 src/App.js
,将AuthInfoProvider
放置在最外层
import AuthInfoProvider from './AuthInfoProvider';
function App(props) {
return (
<AuthInfoProvider>
<Router>
{/* ...routes */}
</Router>
</AuthInfoProvider>
)
}
存储 token 信息
接下来,我们要将token信息,存放到AuthInfoContext当中。
打开src/Authorize.js
,并编辑
import React, { useEffect, useContext } from 'react'
import { parse } from 'qs'
import { Redirect } from 'react-router-dom';
import { AuthInfo } from './AuthInfoProvider';
import { fetchUserBasicInfo } from './requests';
function Authorize(props) {
const {
state,
dispatch
} = useContext(AuthInfo);
const {
location: {
search,
hash,
}
} = props;
const token = hash.slice(1);
const query = parse(search.slice(1));
useEffect(() => {
if (token) {
// 尝试获取数据
const tokenPayload = {
token_type: query.token_type,
access_token: token,
expires_in: query.expires_in,
}
dispatch({
type: 'set-token',
payload: tokenPayload
});
// 将原来的fetch请求,移动到 `requests.js` 中
fetchUserBasicInfo(tokenPayload)
.then(({ data }) => {
dispatch({
type: 'set-current-user',
payload: data,
})
})
.catch((err) => {
global.alert(err.message)
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 返回到认证起始页面
if (state.token) {
return <Redirect to={query.next || '/me'} />
}
if (!token) {
return <div>Invalid request</div>
}
return (
<div>
Authorize
</div>
)
}
export default Authorize;
编辑 src/requests.js
,添加 fetchUserBasicInfo
方法
export const fetchUserBasicInfo = async (token) => {
const url = `${API_ENDPOINT}/api/user/basic-info/`;
return await fetch(url, {
method: 'GET',
headers: {
Authorization: `${token.token_type} ${token.access_token}`
}
}).then(resHelper);
}
来到这里,我们发现认证完成之后,页面是可以回到“我的”页面了,但是还是显示“需要认证的提示”。这里我们可以使用 React Developer Tools 来查看我们的数据是否已经准备好了。
应用内读取 token 信息
接下来,我们编辑一下 src/Me.js
,根据 AuthInfo
的情况来渲染页面
import React from 'react';
import { AuthInfo } from './AuthInfoProvider';
import AuthRequiredHint from './AuthRequiredHint';
function Me() {
const { state } = useContext(AuthInfo);
if (state.token) {
return (
<div>MY Event List </div>
)
}
return <AuthRequiredHint />
}
此处可以直接修改
<AuthInfoProvider value={fakeData} />
的方式来验证逻辑是否完成
编辑完成之后,当我们再次访问的/me
页面的时候,我们可以看到如下图所示的页面
继续编辑“我的”页面
调整页面布局
我们从src/Explore.js
中抄来一部分代码,来完成三段式布局:
import React from 'react';
import { AuthInfo } from './AuthInfoProvider';
import AuthRequiredHint from './AuthRequiredHint';
function Me() {
const { state, dispatch } = useContext(AuthInfo);
if (state.token) {
return (
<div className='App'>
<div className="App__header">
<div className="PageHeader">
{state.currentUser ? (
<div className='AvatarStamp pl_12'>
<img className='AvatarStamp__avatar' src={state.currentUser.avatar} />
<span className='AvatarStamp__username'>{state.currentUser.username}</span>
</div>
) : <div className='pl_12'>...</div>}
<div className="PageHeader__right pr_12">
<button
className='button button_merge'
onClick={() => {
dispatch({
type: 'reset'
})
}}
>
退出
</button>
</div>
</div>
</div>
<div className="App__content">
MY Event List
</div>
<div className="App__footer">
<BottomBar />
</div>
</div>
)
}
return <AuthRequiredHint />
}
打开 src/index.css
,添加样式
.AvatarStamp {
display: flex;
align-items: center;
}
.AvatarStamp__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 8px;
}
调用 EventList
组件
继续从src/Explore.js
中抄来 EventList
的调用,然后修改为 EventList
组件提供的 request
方法。
import React, { useContext, useCallback } from 'react';
import { fetchEventList } from './requests'
function Me(props) {
const { state } = useContext(AuthInfo);
const handleListRequest = useCallback((pagination) => {
if (!state.token) {
return;
}
return fetchEventList(pagination, state.token);
}, [state.token]);
if (state.token) {
<div className='App'>
{/* ... */}
<EventList
request={handleListRequest}
showUserInfo={false}
/>
{/* ... */}
</div>
}
// ...
}
编辑src/requests.js
, 添加方法 fetchEventList
,相关API文档可查阅: FileX.Event: list
export const fetchEventList = async (params, token) => {
const query = params ? stringify(params) : '';
const baseURL = `${API_ENDPOINT}/api/xfile/event/`;
const url = query ? `${baseURL}?${query}` : baseURL;
const headers = {
Accept: 'application/json',
Authorization: `${token.token_type} ${token.access_token}`
}
return await fetch(url, {
headers
}).then(resHelper);
}
刷新“我的”页面,就可以看到当前用户的“事件”列表了:
小结
这个部分的教程中,我们主要介绍了:
- OAuth 2.0 Implicit Grant Type 的流程
- 如何使用 React 的 Context API 提供储存、调用应用内的“全局”数据
在下一篇教程中,我们将会继续深入了解 File-X API 提供的功能,并实现 File-X 事件的基本管理功能。