Web全栈开发学习笔记—Part5 测试 React 应用—a.完成前台的登录功能

目录

Creating new notes

Saving the token to browsers local storage


之前主要关注于后端,但前端目前还不支持后端用户管理。

目前前端能够展示已经存在的 Note,并且允许用户切换 Note 的重要程度。由于我们在第四章节的修改,新的 Note 不能再添加了:因为在新建 Note 前,后端现在需要 token 来验证用户。

现在将实现前台的用户管理功能的一部分。首先从用户登录开始,假设还不会从前端来添加用户。

登录表单已经添加到了页面顶端。添加 Note 的表单也已经移到了 Note 列表的顶部。

fullstack content

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) {
      // ...
    }
  }

现在用户的详细信息被存储到本地存储了,并且能够在控制台看到。

fullstack content

也可以使用开发者工具来查看本地存储。在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()

猜你喜欢

转载自blog.csdn.net/qq_39389123/article/details/112343876