Gatsby + realworld 案例实践 - 02 实现登录、同步用户登录状态、设置页面访问权限

实现登录

登录页面属于静态内容和动态内容混合的页面,登录之后的用户状态需要 Redux 管理。

安装和配置 Axios

发送请求需要安装 Axios 模块:npm i axios

配置 Axios 基础地址:

// gatsby-browser.js
// 用到 JSX 语法,所以要引入 React
const React = require("react")
// 组件是 ESM 语法,所以用 default 引入默认导出
const Layout = require("./src/components/Layout").default
// Redux
const { Provider } = require("react-redux")
const createStore = require("./src/store/createStore").default
// Axios
const axios = require("axios")
axios.defaults.baseURL = "https://conduit.productionready.io/api"

// element 是每个页面组件的内容
exports.wrapPageElement = ({ element }) => {
  return <Layout>{element}</Layout>
}

// element 是应用的最外层组件
exports.wrapRootElement = ({ element }) => {
  return <Provider store={createStore()}>{element}</Provider>
}

定义一个 Input 的自定义钩子函数

// src\hooks\useInput.js
import { useState } from "react"

export default function useInput(initialValue) {
  const [value, setValue] = useState(initialValue)

  return {
    input: {
      value,
      onChange(e) {
        setValue(e.target.value)
      },
    },
    setValue,
  }
}

创建存储登录状态的 Reducer

// src\store\reducers\auth.reducer.js
const initialState = {}
export default function (state = initialState, action) {
  switch (action.type) {
    case "loginSuccess":
      return {
        success: true,
        user: action.payload,
      }
      break
    case "loginFailed":
      return {
        success: false,
        errors: action.payload,
      }
      break
    default:
      return state
      break
  }
}

// src\store\reducers\root.reducer.js
import { combineReducers } from "redux"
import counterReducer from "./counter.reducer"
import authReducer from "./auth.reducer"

export default combineReducers({
  counterReducer,
  authReducer,
})

创建登录 Sage

// src\store\sagas\auth.saga.js
import { takeEvery, put } from "redux-saga/effects"
import axios from "axios"

// 执行登录
function* login({ payload }) {
  try {
    const { data } = yield axios.post("/users/login", payload)
    localStorage.setItem("token", data.user.token)
    yield put({ type: "loginSuccess", payload: data.user })
  } catch (ex) {
    const errors = ex.response.data.errors
    const message = []
    for (let attr in errors) {
      errors[attr].forEach(error => {
        message.push(`${attr} ${error}`)
      })
    }
    yield put({ type: "loginFailed", payload: message })
  }
}

export default function* authSaga() {
  yield takeEvery("login", login)
}

// src\store\sagas\root.saga.js
import { all } from "redux-saga/effects"
import counterSaga from "./counter.saga"
import authSaga from "./auth.saga"

export default function* rootSaga() {
  yield all([counterSaga(), authSaga()])
}

修改登录页

// src\pages\login.js
import React from "react"
import useInput from "../hooks/useInput"
import { useDispatch, useSelector } from "react-redux"
import { navigate } from "gatsby"

export default function Login() {
  const email = useInput("")
  const password = useInput("")
  const dispatch = useDispatch()
  const authReducer = useSelector(state => state.authReducer)

  // 如果已登录 跳转首页
  if (authReducer.success) {
    // navigate 是 Gatsby 提供的用于在代码内部导航的辅助函数
    navigate("/")
    return null
  }

  // 显示登录失败信息
  function displayErrors() {
    if (authReducer.errors) {
      return authReducer.errors.map((item, index) => (
        <li key={index}>{item}</li>
      ))
    }
    return null
  }

  // 提交表单
  const handleSubmit = e => {
    e.preventDefault()
    const passwordValue = password.input.value
    const emailValue = email.input.value

    dispatch({
      type: "login",
      payload: {
        user: {
          email: emailValue,
          password: passwordValue,
        },
      },
    })
  }
  return (
    <div className="auth-page">
      <div className="container page">
        <div className="row">
          <div className="col-md-6 offset-md-3 col-xs-12">
            <h1 className="text-xs-center">Sign up</h1>
            <ul className="error-messages">{displayErrors()}</ul>
            <form onSubmit={handleSubmit}>
              <fieldset className="form-group">
                <input
                  className="form-control form-control-lg"
                  type="text"
                  placeholder="Email"
                  {...email.input}
                />
              </fieldset>
              <fieldset className="form-group">
                <input
                  className="form-control form-control-lg"
                  type="password"
                  placeholder="Password"
                  {...password.input}
                />
              </fieldset>
              <button className="btn btn-lg btn-primary pull-xs-right">
                Sign up
              </button>
            </form>
          </div>
        </div>
      </div>
    </div>
  )
}

同步用户登陆状态

获取用户登录信息

// src\store\sagas\auth.saga.js
import { takeEvery, put } from "redux-saga/effects"
import axios from "axios"

// 执行登录
function* login({ payload }) {
  try {
    const { data } = yield axios.post("/users/login", payload)
    localStorage.setItem("token", data.user.token)
    yield put({ type: "loginSuccess", payload: data.user })
  } catch (ex) {
    const errors = ex.response.data.errors
    const message = []
    for (let attr in errors) {
      errors[attr].forEach(error => {
        message.push(`${attr} ${error}`)
      })
    }
    yield put({ type: "loginFailed", payload: message })
  }
}

// 获取登录信息
function* loadUser({ payload }) {
  const { data } = yield axios.get("/user", {
    headers: {
      Authorization: `Token ${payload}`,
    },
  })
  yield put({ type: "loadUserSuccess", payload: data.user })
}

export default function* authSaga() {
  yield takeEvery("login", login)
  yield takeEvery("loadUser", loadUser)
}

// src\store\reducers\auth.reducer.js
const initialState = {}
export default function (state = initialState, action) {
  switch (action.type) {
    case "loginSuccess":
    case "loadUserSuccess":
      return {
        success: true,
        user: action.payload,
      }
      break
    case "loginFailed":
      return {
        success: false,
        errors: action.payload,
      }
      break
    default:
      return state
      break
  }
}

修改 Header

// src\components\Header.js
import React, { useEffect } from "react"
import { useDispatch, useSelector } from "react-redux"

export default function Header() {
  const dispatch = useDispatch()
  const authReducer = useSelector(state => state.authReducer)

  useEffect(() => {
    const token = localStorage.getItem("token")
    if (token) {
      dispatch({
        type: "loadUser",
        payload: token,
      })
    }
  }, [])

  return (
    <nav className="navbar navbar-light">
      <div className="container">
        <a className="navbar-brand" href="index.html">
          conduit
        </a>
        <ul className="nav navbar-nav pull-xs-right">
          <li className="nav-item">
            {/* Add "active" class when you're on that page" */}
            <a className="nav-link active">Home</a>
          </li>

          {authReducer.success ? (
            <Login username={authReducer.user.username} />
          ) : (
            <Logout />
          )}
        </ul>
      </div>
    </nav>
  )
}

// 已登录显示的链接
function Login({ username }) {
  return (
    <>
      <li className="nav-item">
        <a className="nav-link">
          <i className="ion-compose" />
          &nbsp;New Post
        </a>
      </li>
      <li className="nav-item">
        <a className="nav-link">
          <i className="ion-gear-a" />
          &nbsp;Settings
        </a>
      </li>
      <li className="nav-item">
        <a className="nav-link">{username}</a>
      </li>
    </>
  )
}

// 未登录显示的链接
function Logout() {
  return (
    <>
      <li className="nav-item">
        <a className="nav-link">Sign in</a>
      </li>
      <li className="nav-item">
        <a className="nav-link">Sign up</a>
      </li>
    </>
  )
}

页面访问权限

在 realworld 中有一部分页面只有在登陆后才能访问,为了将它们保护起来,添加权限控制,就需要用到客户端路由。

客户端路由有更多的操作权限,并且这些页面不用实现 SEO,所以可以做成动态的客户端路由。

这些路由是在用户登陆后,所有数据都将从 API 加载,不需要服务器渲染,所以它们应该是客户端专用路由(Client-only Routes)。

实现客户端专用路由

将受限页面组件移动到 pages 目录外

放在 src/pages 目录下的组件会在构建应用时创建不受限制的页面,所以要将受限路由对应的页面组件放到其它地方,如将 create.jssettings.js 移动到 src/components 目录下。

创建受限路由的页面

在根目录下创建 gatsby-node.js 文件,导出 onCreatePage 方法,该方法会在每次创建页面后被调用。

该方法内可以获取到页面信息,根据页面访问地址可以判断是否要调用 createPage 创建页面。

createPage 会创建并更新页面内容。

这一系列操作可以用插件 gatsby-plugin-create-client-paths 完成。

npm i gatsby-plugin-create-client-paths
// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: "gatsby-plugin-create-client-paths",
      options: {
        prefixes: ["/app/*"], // 指定客户端专用路由匹配规则
      },
    },
  ],
}

创建生成受限制页面的通用页面

接着在 src/pages 目录下创建 app.jsapp 是上面定义的匹配路径。

这是个通用页面,用于生成或配置受限制的页面。

页面使用 Gatsby 内置模块 @reach/routerRouter 组件配置路由,它会为配置的路由生成页面信息对象(包括路径、组件文件等),在 Gatsby 创建页面的时候传递给 onCreatePage 方法。

// src\pages\app.js
import React from "react"
import { Router } from "@reach/router"
import Settings from "../components/settings"
import Create from "../components/create"

export default function App() {
  return (
    <Router>
      <Settings path="app/settings" />
      <Create path="app/create" />
    </Router>
  )
}

配置权限

封装判断用户登录

将判断用户是否登录的操作封装到一个自定义钩子函数中

// src\hooks\useLogin.js
import { useState, useEffect } from "react"
import axios from "axios"

export default function useLogin() {
  const initialState = [
    false, // 是否已经登陆
    true, // 是否正在发送请求
  ]
  const [status, setStatus] = useState(initialState)

  useEffect(() => {
    const token = localStorage.getItem("token")
    if (token) {
      try {
        ;(async function () {
          await axios.get("/user", {
            headers: {
              Authorization: `Token ${token}`,
            },
          })
        })()
        setStatus([true, false])
      } catch (ex) {
        setStatus([false, false])
      }
    } else {
      setStatus([false, false])
    }
  }, [])

  return state
}

创建受保护的路由组件

创建一个组件,用于包装受限制的路由组件,在里面编写权限判断代码。

该组件接收路由组件属性,和需要传递给路由组件的全部属性:

// src\components\PrivateRoute.js
import React from "react"
import { navigate } from "gatsby"
import useLogin from "../hooks/useLogin"

export default function PrivateRoute({ component: Component, ...rest }) {
  const [isLogin, loading] = useLogin()

  if (loading) return null
  if (isLogin) return <Component {...rest} />
  navigate("/login")
  return null
}

修改通用页面

// src\pages\app.js
import React from "react"
import { Router } from "@reach/router"
import PrivateRoute from "../components/PrivateRoute"
import Settings from "../components/settings"
import Create from "../components/create"

export default function App() {
  return (
    <Router>
      <PrivateRoute component={Settings} path="app/settings" />
      <PrivateRoute component={Create} path="app/create" />
    </Router>
  )
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
<p>在帝国软件官网创建15周年之际,帝国CMS7.5版发布了。帝国CMS经历过18个版本更新,7.5是第19个版本。我们一直不断在实践中突破创新,帝国CMS7.2版在移动互联实现与全面安全防御实现突破创新,让网站发展无后顾之忧。而帝国CMS7.5版则是全面完善,功能更加强大实用,做精品中的精品,只为让您用上更放心、更省心的产品。在原来7.2版本的基础上更加完善,打造更完美的产品。安全稳定的程序为网站发展与壮大提供更加可靠的保障!</p><p> </p><p>帝国CMS 7.5版新增了:</p><p>多访问端升级:支持在主端后台刷新生成各访问端静态页面,多访问端用静态模式下使用更方便。</p><p>多访问端升级:支持任意切换各访问端后台,无需重新登录,多访问端管理超方便。</p><p>多访问端新增静态页面同步功能,支持多端定时同步、双端实时同步。</p><p>多访问端支持设置子端是否开启后台权限,更高安全性,维护更方便。</p><p>后台支持直接修改模板ID,方便使用多访问端功能模板ID不一致处理。</p><p>动态页面新增支持缓存,让采用动态页面模式的网站访问速度更快,效率更高。</p><p>新增对PHP7.*系列运行环境的支持,兼容性更好。</p><p>针对信息访问、下载等权限进行升级,新增“访问组”这一新概念,控制会员访问权限更细、更灵活、更实用。</p><p>新增会员内部组,方便对会员进行内部分类,客户管理更方便。</p><p>新增会员管理组和会员管理员设置,为以后前台管理员和客服系统管理员设置打下基础。</p><p>后台新增浏览器USER-AGENT验证功能,更高安全性。</p><p>新增支持HTTPS传输协议,更安全。</p><p>COOKIE安全设置升级,更上一台阶。</p><p>后台认证码新增预加密验证功能,超安全。</p><p>编辑器升级为CKEditor编辑器,兼容性更好、功能更实用。</p><p>支持自定义后台登录文件,安全性更高。</p><p>“来源HASH验证”功能升级,防护更强。</p><p>来源地址新增严格模式验证,更严谨,防护外部提交更严格。</p><p>TAGS功能升级,更完善。</p><p>各系统模型新增记录信息审核人功能,对查看责任人和绩效考核更方便。</p><p>视频播放器更新,做视频站更方便。</p><p>自定义工作流功能升级,更完善。</p><p>会员充值有效期升级,更完善。让网站内容商业化更顺畅。</p><p>栏目新增单页内容设置,直接用栏目做公司简介、联系方式等单页更方便。</p><p>对WAP模块进行升级,做展示类手机站更方便。</p><p>采用全新的日期时间和颜色选择模块,更方便。</p><p>用户发布信息统计升级,统计项更详细。让管理员更方便的查看编辑发布绩效。</p><p>采集功能升级,更灵活。</p><p>备份系统升级,兼容性更高。</p><p>验证码功能升级,更有效防止灌水机。</p><p>系统模型新增发布后和修改后处理函数扩展。</p><p>DIGG顶功能升级,可依网站实际情况设置,更实用。</p><p>栏目列表式页面新增支持附加SQL条件设置,可依特殊情况设定栏目列表显示特定的信息,更个性化。</p><p>信息头条和推荐级别由原来支持的9级头条和9级推荐,升级为最大支持255级头条和255级推荐,组织信息更灵活。</p><p>新增更多一键关闭模块功能,将网站不需要的模块关闭更方便。</p><p>支持设置某些会员组才能拥有会员空间。</p><p>后台信息发布限制可设置更严格,对网站内部管理员要求安全性更高的可以增加限制。</p><p>针对后台内部管理员操作的安全性完善更新。</p><p>专题子类支持自定义文件名,让页面更个性化。</p><p>支持限制单个栏目单个会员发布信息数。</p><p>新增详细管理会员列表功能,审核内容更方便。</p><p>新增更多安全特性,系统更牢固。</p>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值