目录
Saving the token to browsers local storage
之前主要关注于后端,但前端目前还不支持后端用户管理。
目前前端能够展示已经存在的 Note,并且允许用户切换 Note 的重要程度。由于我们在第四章节的修改,新的 Note 不能再添加了:因为在新建 Note 前,后端现在需要 token 来验证用户。
现在将实现前台的用户管理功能的一部分。首先从用户登录开始,假设还不会从前端来添加用户。
登录表单已经添加到了页面顶端。添加 Note 的表单也已经移到了 Note 列表的顶部。
App 组件的代码如下:
const App = () => {
const [notes, setNotes] = useState([])
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
const [errorMessage, setErrorMessage] = useState(null)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
useEffect(() => {
noteService
.getAll().then(initialNotes => {
setNotes(initialNotes)
})
}, [])
// ...
const handleLogin = (event) => {
event.preventDefault()
console.log('logging in with', username, password)
}
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} />
<form onSubmit={handleLogin}>
<div>
username
<input
type="text"
value={username}
name="Username"
onChange={({ target }) => setUsername(target.value)}
/>
</div>
<div>
password
<input
type="password"
value={password}
name="Password"
onChange={({ target }) => setPassword(target.value)}
/>
</div>
<button type="submit">login</button>
</form>
// ...
</div>
)
}
export default App
当前应用状态有username 和 password 都存储在表单中。表单有事件处理逻辑,与App组件的状态保持同步。事件处理逻辑很简单:将一个对象作为参数传递给它们,它们将target 字段从对象里解构出来,将它的值保存为状态
({ target }) => setUsername(target.value)
handleLogin 方法负责发送表单,还没有实现。
通过api/login这个 HTTP POST 请求完成登录。让我们将它解耦到自己的 services/login.js 模块中
使用async/await 语法而不再使用 promises,代码如下:
import axios from 'axios'
const baseUrl = '/api/login'
const login = async credentials => {
const response = await axios.post(baseUrl, credentials)
return response.data
}
export default { login }
处理登录的方法可以按如下方式实现:
import loginService from './services/login'
const App = () => {
// ...
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [user, setUser] = useState(null)
const handleLogin = async (event) => {
event.preventDefault()
try {
const user = await loginService.login({
username, password,
})
setUser(user)
setUsername('')
setPassword('')
} catch (exception) {
setErrorMessage('Wrong credentials')
setTimeout(() => {
setErrorMessage(null)
}, 5000)
}
}
// ...
}
如果登录成功,表单 字段 被清空,并且服务器响应(包括 token 和用户信息)被存储到 应用状态的user 字段 。
如果登录失败,或者执行 loginService.login 产生了错误,则会通知用户。
总之用户登录成功是不会通知用户的。让我们将应用修改为,只有当用户没有登录时才显示登录表单,即 user === null 。只有当用户登录成功后才会显示添加新的 Note,这样 user 状态才会包含信息
增加两个 辅助函数给 App 组件来生成表单。
const App = () => {
// ...
const loginForm = () => (
<form onSubmit={handleLogin}>
<div>
username
<input
type="text"
value={username}
name="Username"
onChange={({ target }) => setUsername(target.value)}
/>
</div>
<div>
password
<input
type="password"
value={password}
name="Password"
onChange={({ target }) => setPassword(target.value)}
/>
</div>
<button type="submit">login</button>
</form>
)
const noteForm = () => (
<form onSubmit={addNote}>
<input
value={newNote}
onChange={handleNoteChange}
/>
<button type="submit">save</button>
</form>
)
return (
// ...
)
}
按照条件来渲染它们:
const App = () => {
// ...
const loginForm = () => (
// ...
)
const noteForm = () => (
// ...
)
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} />
{user === null && loginForm()} {user !== null && noteForm()}
<div>
<button onClick={() => setShowAll(!showAll)}>
show {showAll ? 'important' : 'all'}
</button>
</div>
<ul>
{notesToShow.map((note, i) =>
<Note
key={i}
note={note}
toggleImportance={() => toggleImportanceOf(note.id)}
/>
)}
</ul>
<Footer />
</div>
)
}
在 React 中十分常见的一个React trick ,即按条件渲染表单:
{
user === null && loginForm()
}
如果第一个表达式计算为 false 或falsy, 则不会执行第二个语句(生成表单)
我们可以使用条件运算conditional operator来让这个逻辑表达得更直白一些:
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage}/>
{user === null ?
loginForm() :
noteForm()
}
<h2>Notes</h2>
// ...
</div>
)
如果 user === null 是 truthy loginForm() 就会执行。如果不是,就执行 noteForm().
多做一点修改:如果用户登录,它们的名字就会展示在屏幕上:
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} />
{user === null ?
loginForm() :
<div>
<p>{user.name} logged-in</p>
{noteForm()}
</div>
}
<h2>Notes</h2>
// ...
</div>
)
Creating new notes
【创建新的 Note】
成功登录后,token 被返回并存储到了 user 的 token 状态中
const handleLogin = async (event) => {
event.preventDefault()
try {
const user = await loginService.login({
username, password,
})
setUser(user) setUsername('')
setPassword('')
} catch (exception) {
// ...
}
}
修复创建新 Note 的代码,来和后台对接好。也就是说把登录成功用户的 token 放到 HTTP 请求的认证头中。
noteService 模块修改如下:
import axios from 'axios'
const baseUrl = '/api/notes'
let token = null
const setToken = newToken => {
token = `bearer ${newToken}`}
const getAll = () => {
const request = axios.get(baseUrl)
return request.then(response => response.data)
}
const create = async newObject => {
const config = {
headers: { Authorization: token },
}
const response = await axios.post(baseUrl, newObject, config)
return response.data
}
const update = (id, newObject) => {
const request = axios.put(`${ baseUrl } /${id}`, newObject)
return request.then(response => response.data)
}
export default { getAll, create, update, setToken }
noteService 模块包含一个私有变量 token。它的值可以通过 setToken 函数来改变,这个函数通过模块对外开放。 create 方法现在利用 async/await 语法,将 token 塞到了认证头中。头信息作为第三个入参数放到了 axios 的 post 方法中。
登录的事件处理改为,对登录成功的用户执行 noteService.setToken(user.token)
const handleLogin = async (event) => {
event.preventDefault()
try {
const user = await loginService.login({
username, password,
})
noteService.setToken(user.token)
setUser(user)
setUsername('')
setPassword('')
} catch (exception) {
// ...
}
}
现在添加新的 Note 可以正常工作了
Saving the token to browsers local storage
【将 token 保存到浏览器的本地存储中】
现在的应用有一个缺陷,就是当页面重新渲染时,user 的登录信息就没了。这同样会降低开发速度。比如当我们想要测试创建一个新的 Note,我们每次都要重新登录。
通过将登录信息存储到一个本地浏览器的 key-value 数据库中,问题就能解决。
使用十分简单。一个值对应一个存储在数据库中的特定的键,通过 setItem方法进行保存,例如:
window.localStorage.setItem('name', 'juha tauriainen')
将字符串作为第二个参数,存储到了以name为键的键值对中。
该键的值可以通过getItem方法获得。
window.localStorage.getItem('name')
removeItem 可以删除一个键
即使页面刷新,存储中的值也会保留。这个存储是原生-指定的,所以每个 web 应用都有自己的存储空间。
将我们的应用扩展来将用户的登录信息存储到本地存储中。
存储到本地存储的值称为DOMstrings,不能存储一个 Javascript 对象。对象首先要通过 JSON.stringify 方法转换成 JSON。相应的,当从本地存储读取 JSON 对象时,还要使用 JSON.parse 来将其解析回 Javascript。
将登录方法改为如下方式:
const handleLogin = async (event) => {
event.preventDefault()
try {
const user = await loginService.login({
username, password,
})
window.localStorage.setItem(
'loggedNoteappUser', JSON.stringify(user)
)
noteService.setToken(user.token)
setUser(user)
setUsername('')
setPassword('')
} catch (exception) {
// ...
}
}
现在用户的详细信息被存储到本地存储了,并且能够在控制台看到。
也可以使用开发者工具来查看本地存储。在Chrome中,到 Application 标签页,选择Local Storage 。
仍然需要修改应用,以便当进入页面时,应用会检查是否能在本地存储中找到登录用户的详细信息,如果可以,将信息保存到应用的状态中,以及noteService中
正确的方式是用一个effect hook,我们可以有多个effect hook,所以我们来创建一个hook 来处理首次登录页面:
const App = () => {
const [notes, setNotes] = useState([])
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
const [errorMessage, setErrorMessage] = useState(null)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [user, setUser] = useState(null)
useEffect(() => {
noteService
.getAll().then(initialNotes => {
setNotes(initialNotes)
})
}, [])
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedNoteappUser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
noteService.setToken(user.token)
}
}, [])
// ...
}
这个作为事件参数的空数组确保在第一次组件渲染完成后被执行。
现在用户可以永久地保持登录状态了,再实现一个登出功能来删除登录信息。。
也可以通过控制台来登出用户,现在我们就用这种方法,执行以下命令来登出:
window.localStorage.removeItem('loggedNoteappUser')
或者完全清空本地存储:
window.localStorage.clear()