口水巾 發表於 2023-1-13 10:31:00

react 高效高质量搭建后台系统 系列 —— 登录

<blockquote>
<p>其他章节请看:</p>
<p>react 高效高质量搭建后台系统 系列</p>
</blockquote>
<h2 id="登录">登录</h2>
<p>本篇将完成<code>登录模块</code>。效果和 spug 相同:</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230113014513_highqualitybacksystem-login-01.png"></p>
<p><code>需求</code>如下:</p>
<ul>
<li>登录页的绘制</li>
<li>支持普通登录和LDAP登录</li>
<li>登录成功后跳转到<code>主页</code>,没有登录的情况下访问系统会重定向到登录页,登录成功后再次回到<code>之前的页面</code>。系统会话过期后,请求会重定向到登录页。</li>
</ul>
<p><em>Tip</em>:<code>退出登录</code>在进入系统后进行,暂不不管。</p>
<h3 id="路由和登录">路由和登录</h3>
<p>登录页是进入系统的<code>门户</code>,登录页绘制逻辑比较简单(单个模块的开发比较简单)。</p>
<p>首先要<code>解决</code>:根据 url 不同,进入<code>登录页</code>还是<code>系统</code>主页。这里需要使用路由器。</p>
<h4 id="spug-中的路由和登录">spug 中的路由和登录</h4>
<blockquote>
<p>详情请看 react 路由、react 路由原理</p>
</blockquote>
<p><em>Tip</em>:实现的核心是 Router,以及 history 包。</p>
<p><code>需求</code>:浏览器输入 /(<code>http://localhost:3010/</code>) 进入登录页,其他路径进入系统。</p>
<p>实现如下:</p>
<ul>
<li>在入口页(index.js)中使用 <code>&lt;Router history={history}&gt;</code> 管理路由:</li>
</ul>
<pre><code class="language-javascript">// spug\src\index.js
import { history, updatePermissions } from 'libs';
// 权限、token 相关
updatePermissions();

ReactDOM.render(
// Router 是路由器,用于管理路由
// `history: object` 用来导航的 history 对象。
&lt;Router history={history}&gt;
    &lt;ConfigProvider locale={zhCN} getPopupContainer={() =&gt; document.fullscreenElement || document.body}&gt;
      &lt;App/&gt;
    &lt;/ConfigProvider&gt;
&lt;/Router&gt;,
document.getElementById('root')
);
</code></pre>
<p>其中 <code>history</code> 用于导航 history 对象(此用法在路由官网中)。执行 history.push 时不仅会改变浏览器的 url,而且路由也会发生变化(请看本篇“history={history} 的作用”章节)</p>
<ul>
<li>libs 模块代码如下:</li>
</ul>
<pre><code class="language-javascript">// spug\src\libs\index.js
import _http from './http';
// 仅对 history 包的导出
import _history from './history';

// 里面有 updatePermissions
export * from './functools';
export * from './router';
export const http = _http;
export const history = _history;
export const VERSION = 'v3.0.5';
</code></pre>
<p>history 仅对 history 包的导出,在 这里 中已介绍。</p>
<ul>
<li>主页(App.js)中定义了两个路由,如果 url 精确匹配 <code>/</code> 则进入登录页,否则进入系统( Layout 是 antd 中的 Layout 组件,对 404 的界面反馈也 Layout 模块中进行了处理)。</li>
</ul>
<pre><code class="language-javascript">// spug\src\App.js

class App extends Component {
    render() {
      return (
      // 只渲染其中一个 Route
      // exact 精确匹配
      // component={Login} 路由组件(不同于一般组件,其 props 中有路由相关方法。)
      &lt;Switch&gt;
            &lt;Route path="/" exact component={Login} /&gt;
            {/* 没有匹配则进入 Layout */}
            &lt;Route component={Layout} /&gt;
      &lt;/Switch&gt;
      );
    }
}

export default App;
</code></pre>
<h4 id="myspug-添加路由和登录">myspug 添加路由和登录</h4>
<ul>
<li>入口页增加 <code>&lt;Router history={history}&gt;</code>。</li>
</ul>
<p><em>Tip</em>: StrictMode(一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样) 仍旧保留。</p>
<pre><code class="language-javascript">// myspug\src\index.js
import React from 'react';
+import { Router } from 'react-router-dom';
+import { history } from '@/libs';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+// StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。
+// 严格模式检查仅在开发模式下运行;它们不会影响生产构建。
   &lt;React.StrictMode&gt;
-    &lt;ConfigProvider locale={zhCN}&gt;
-      &lt;App /&gt;
-    &lt;/ConfigProvider&gt;
+    &lt;Router history={history}&gt;
+      &lt;ConfigProvider locale={zhCN}&gt;
+      &lt;App /&gt;
+      &lt;/ConfigProvider&gt;
+    &lt;/Router&gt;

   &lt;/React.StrictMode&gt;
);
</code></pre>
<ul>
<li>新建 <code>libs/index.js</code>,主要是导出 history:</li>
</ul>
<pre><code class="language-javascript">// myspug\src\libs\index.js
import _http from './http';
import _history from './history';

export const http = _http;
export const history = _history;
export const VERSION = 'v1.0.0';
</code></pre>
<ul>
<li>在主页中配置好 Switch,如果 url 是 <code>/</code> 则进入登录页,如果是其他 url 则进入 HelloWorld(用来模拟 Layout)</li>
</ul>
<pre><code class="language-javascript">// myspug\src\App.js

import { Component } from 'react';
// 登录组件
import Login from './pages/login';
// 模拟 Layout 组件
import HelloWorld from './HelloWord'
import { Switch, Route } from 'react-router-dom';

// 定义一个类组件
class App extends Component {
render() {
    return (
      // 只渲染其中一个 Route
      // exact 精确匹配
      // component={Login} 路由组件(不同于一般组件,其 props 中有路由相关方法。)
      &lt;Switch&gt;
      &lt;Route path="/" exact component={Login} /&gt;
      {/* 没有匹配则进入 Layout */}
      &lt;Route component={HelloWorld} /&gt;
      &lt;/Switch&gt;
    );
}
}

export default App;
</code></pre>
<ul>
<li>登录页和 HelloWorld 都是最简单的组件。代码如下:</li>
</ul>
<pre><code class="language-javascript">// myspug\src\pages\login\index.js
export default function() {
    return &lt;div&gt;登录页&lt;/div&gt;
}
</code></pre>
<pre><code class="language-javascript">// myspug\src\HelloWord.js
export default function HelloWorld() {
    return &lt;div&gt;hello world!&lt;/div&gt;
}
</code></pre>
<p><code>测试</code>结果如下:</p>
<pre><code class="language-javascript">浏览器:http://localhost:3000/
显示:登录页

浏览器:http://localhost:3000/home
显示:hello world!
</code></pre>
<h4 id="historyhistory-的作用">history={history} 的作用</h4>
<p>在 请求数据 一文中我们曾有一个<code>疑惑</code>:spug 官网中执行 history.push 不仅可以切换url,而且路由也发生了变化。</p>
<p>笔者测试发现:是入口页 <code>&lt;Router history={history}&gt;</code> 中 history 的功劳。</p>
<p>验证步骤如下:</p>
<ul>
<li>将 history 导出到 window(例如在 http.js 中进行):</li>
</ul>
<pre><code class="language-javascript">// myspug\src\libs\http.js

import http from 'axios'
import history from './history'
// 将其导出
window._history = history;
</code></pre>
<ul>
<li>浏览器访问 <code>http://localhost:3000/</code> 并在控制台中输入:</li>
</ul>
<pre><code class="language-javascript">执行:_history.push('/home')
url 变成 http://localhost:3000/home 浏览器显示:hello world!

执行:_history.push('/')
url 变成 http://localhost:3000/   浏览器显示:登录页
</code></pre>
<p><em>Tip</em>:如果删除入口页的 <code>history={history}</code>,浏览器控制台将报错如下,提示没有 location 属性,无法进行路由匹配:</p>
<pre><code class="language-javascript">Warning: Failed prop type: The prop `history` is marked as required in `Router`, but its value is `undefined`.

Uncaught TypeError: Cannot read property 'location' of undefined
</code></pre>
<h3 id="spug-中登录模块的分析">spug 中登录模块的分析</h3>
<p>我们初步解决了登录页和主页(或系统)之间的跳转(或路由)。</p>
<p>下面我们完整分析 spug 中登录模块的实现,比如登录绘制、普通登录和LDAP登录...</p>
<p>登录模块代码都在 <code>spug/src/pages/login</code> 目录下,一个 js 文件,一个样式文件:</p>
<pre><code class="language-javascript">Administrator@-WK-10 MINGW /e/spug/src/pages/login
$ ls
bg.pngindex.jslogin.module.css
</code></pre>
<p>login.module.css 是登录模块的样式文件,前文 已分析过样式,这里不再冗余。</p>
<p>登录的<code>核心</code>全在 index.js 中。</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230113014519_highqualitybacksystem-login-02.png"></p>
<p>我们参照登录界面说一下 <code>index.js</code> 的结构:</p>
<ul>
<li>这是一个<code>函数式的组件</code>,返回的 div 包括两部分:登录信息输入区、网站底部统一信息区</li>
<li>Tabs 仅做样式,默认显示“普通登录”</li>
<li>表单与 Tabs 是独立的。表单使用 <code>Form.useForm</code> 创建表单数据域进行控制</li>
<li>验证码默认是关闭的,笔者这里将其开启</li>
<li>useEffect、useState是函数式组件中生命周期和状态的使用<code>语法</code></li>
<li>组件挂载后的一系列 <code>store</code> 的初始化用于对应模块的使用</li>
<li>里面的 setTimeout 用于重新获取<code>验证码</code>倒计时</li>
</ul>
<pre><code class="language-javascript">// spug\src\pages\login\index.js

/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) &lt;spug.dev@gmail.com&gt;
* Released under the AGPL-3.0 License.
*/
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Tabs, Modal, message } from 'antd';
import { UserOutlined, LockOutlined, CopyrightOutlined, GithubOutlined, MailOutlined } from '@ant-design/icons';
import styles from './login.module.css';
import history from 'libs/history';
import { http, updatePermissions } from 'libs';
// store 是 mobx 中的状态集中器。这里是初始化 pages 下的 config、deploy、exec、host等模块中的某字段
import envStore from 'pages/config/environment/store';
import appStore from 'pages/config/app/store';
import requestStore from 'pages/deploy/request/store';
import execStore from 'pages/exec/task/store';
import hostStore from 'pages/host/store';

// 函数组件
export default function () {
// FormInstance 经 Form.useForm() 创建的 form 控制实例。FormInstance 有一系列方法,例如
// 注:useForm 是 React Hooks 的实现,只能用于函数组件,class 组件请查看下面的例子(https://ant.design/components/form-cn#components-form-demo-control-hooks)
// Tip:我们推荐使用 Form.useForm 创建表单数据域进行控制。如果是在 class component 下,你也可以通过 ref 获取数据域。(https://ant.design/components/form-cn#components-form-demo-control-ref)
const = Form.useForm();
// 验证码倒计时
const = useState(0);
// 控制登录按钮
const = useState(false);
// 登录类型默认是 default
const = useState('default');
// 验证码。默认关闭
const = useState(!false);
const = useState(false);

// 组件挂载后执行。相当于 componentDidMount()
useEffect(() =&gt; {
    envStore.records = [];
    appStore.records = [];
    requestStore.records = [];
    requestStore.deploys = [];
    hostStore.records = null;
    hostStore.groups = {};
    hostStore.treeData = [];
    execStore.hosts = [];
}, [])

// 相当于 componentDidMount() 和 componentDidUpdate()(counter 变化时会执行)
// 定时器,重新获取验证码倒计时。
useEffect(() =&gt; {
    setTimeout(() =&gt; {
      // 默认是 0,故不会执行。当设置有效值时会执行,例如 30
      if (counter &gt; 0) {
      setCounter(counter - 1)
      }
    }, 1000)
}, )

// 登录
function handleSubmit() {
    // form 是 FormInstance。
    // getFieldsValue - 获取一组字段名对应的值,会按照对应结构返回
    const formData = form.getFieldsValue();
    // 如果显示了“验证码”却没有输入,提示
    if (codeVisible &amp;&amp; !formData.captcha) return message.error('请输入验证码');

    // 登录中...
    setLoading(true);
    // 设置登录类型:default 或 ldap
    formData['type'] = loginType;
    // formData2 {username: '1', password: '2', captcha: '3', type: 'default'}
    console.log('formData2', formData)
    http.post('/api/account/login/', formData)
      // 官网返回: {"data": {"id": 1, "access_token": "4b6f1a9b8d824908abb9613695de57f8", "nickname": "\u7ba1\u7406\u5458", "is_supper": true, "has_real_ip": true, "permissions": []}, "error": ""}
      .then(data =&gt; {
      // 某种处理逻辑
      if (data['required_mfa']) {
          setCodeVisible(true);
          setCounter(30);
          setLoading(false)
      // 用户请求时没有真实ip则安全警告
      } else if (!data['has_real_ip']) {
          Modal.warning({
            title: '安全警告',
            className: styles.tips,
            content: &lt;div&gt;
            未能获取到访问者的真实IP,无法提供基于请求来源IP的合法性验证,详细信息请参考
            &lt;a target="_blank"
                href="https://spug.cc/docs/practice/"
                rel="noopener noreferrer"&gt;官方文档&lt;/a&gt;。
            &lt;/div&gt;,
            onOk: () =&gt; doLogin(data)
          })
      } else {
          doLogin(data)
      }
      }, () =&gt; setLoading(false))
}

// 将登录返回的数据存入本地,并更新权限和 token
function doLogin(data) {
    // id
    localStorage.setItem('id', data['id']);
    // token
    localStorage.setItem('token', data['access_token']);
    // 昵称
    localStorage.setItem('nickname', data['nickname']);
    // is_supper
    localStorage.setItem('is_supper', data['is_supper']);
    // 权限
    localStorage.setItem('permissions', JSON.stringify(data['permissions']));

    // 权限和 token 相关。
    updatePermissions();
    // 登录成功则进入系统主页或未登录前访问的页面
    // 更具体就是:切换 Url。进入主页或登录前的页面(记录在 from 中)
    // react通过history.location.state来携带参数
    // 例如 spug\src\libs\http.js 中的:history.push('/', {from: history.location})
    if (history.location.state &amp;&amp; history.location.state['from']) {
      history.push(history.location.state['from'])
    } else {
      history.push('/home')
    }
}

// 获取验证码
function handleCaptcha() {
    // 请求中...
    setCodeLoading(true);
    const formData = form.getFieldsValue(['username', 'password']);
    formData['type'] = loginType;
    // formData {username: '1', password: '2', type: 'default'}
    console.log('formData', formData)
    http.post('/api/account/login/', formData)
      // 30 秒后获得验证码
      .then(() =&gt; setCounter(30))
      .finally(() =&gt; setCodeLoading(false))
}


return (
    &lt;div className={styles.container}&gt;
      &lt;div className={styles.formContainer}&gt;
      {/* 仅做样式,默认选中第一个 tabpane。没有选项卡内容 */}
      &lt;Tabs className={styles.tabs} onTabClick={v =&gt; setLoginType(v)}&gt;
          &lt;Tabs.TabPane tab="普通登录" key="default" /&gt;
          &lt;Tabs.TabPane tab="LDAP登录" key="ldap" /&gt;
      &lt;/Tabs&gt;
      {/* 使用 Form.useForm 创建表单数据域进行控制 */}
      &lt;Form form={form}&gt;
          &lt;Form.Item name="username" className={styles.formItem}&gt;
            &lt;Input
            size="large"
            // 关闭自动完成的选项
            autoComplete="off"
            placeholder="请输入账户"
            // 人头像的 icon
            prefix={&lt;UserOutlined className={styles.icon} /&gt;} /&gt;
          &lt;/Form.Item&gt;
          &lt;Form.Item name="password" className={styles.formItem}&gt;
            &lt;Input
            size="large"
            type="password"
            autoComplete="off"
            placeholder="请输入密码"
            // 按下回车的回调。即提交
            onPressEnter={handleSubmit}
            // 锁的icon
            prefix={&lt;LockOutlined className={styles.icon} /&gt;} /&gt;
          &lt;/Form.Item&gt;
          {/* 验证码。默认关闭 */}
          {/* 这里展示了 Form.Item 嵌套用法 */}
          &lt;Form.Item hidden={!codeVisible} name="captcha" className={styles.formItem}&gt;
            &lt;div style={{ display: 'flex' }}&gt;
            &lt;Form.Item noStyle name="captcha"&gt;
                &lt;Input
                  size="large"
                  autoComplete="off"
                  placeholder="请输入验证码"
                  prefix={&lt;MailOutlined className={styles.icon} /&gt;} /&gt;
            &lt;/Form.Item&gt;
            {counter &gt; 0 ? (
                &lt;Button disabled size="large" style={{ marginLeft: 8 }}&gt;{counter} 秒后重新获取&lt;/Button&gt;
            ) : (
                &lt;Button size="large" loading={codeLoading} style={{ marginLeft: 8 }}
                  onClick={handleCaptcha}&gt;获取验证码&lt;/Button&gt;
            )}
            &lt;/div&gt;
          &lt;/Form.Item&gt;
      &lt;/Form&gt;

      &lt;Button
          // block 属性将使按钮适合其父宽度。
          block
          size="large"
          type="primary"
          className={styles.button}
          loading={loading}
          onClick={handleSubmit}&gt;登录&lt;/Button&gt;
      &lt;/div&gt;
      {/* 网站底部统一信息。这里是`官网`、`github 地址`、`文档` */}
      &lt;div className={styles.footerZone}&gt;
      &lt;div className={styles.linksZone}&gt;
          &lt;a className={styles.links} title="官网" href="https://spug.cc" target="_blank"
            rel="noopener noreferrer"&gt;官网&lt;/a&gt;
          &lt;a className={styles.links} title="Github" href="https://github.com/openspug/spug" target="_blank"
            rel="noopener noreferrer"&gt;&lt;GithubOutlined /&gt;&lt;/a&gt;
          &lt;a title="文档" href="https://spug.cc/docs/about-spug/" target="_blank"
            rel="noopener noreferrer"&gt;文档&lt;/a&gt;
      &lt;/div&gt;
      &lt;div style={{ color: '#fff' }}&gt;Copyright &lt;CopyrightOutlined /&gt; {new Date().getFullYear()} By Spug&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
)
}
</code></pre>
<h3 id="myspug-登录模块的实现">myspug 登录模块的实现</h3>
<p><em>Tip</em>:登录样式(<code>pages\login\login.module.css</code>)仅仅是一些样式,直接从 spug 拷贝即可</p>
<h4 id="indexjs">index.js</h4>
<p>新建 <code>pages\login\index.js</code> 文件,内容如下:</p>
<p><em>Tip</em>: 与 spug 中 login\index.js 类似,微做如下调整:</p>
<ul>
<li>组件挂载后的一系列 store 的初始化,暂时不需要,删除</li>
<li>引用路径的调整:libs/history 改成 <code>@/libs/history</code></li>
</ul>
<pre><code class="language-javascript">// myspug\src\pages\login\index.js
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Tabs, Modal, message } from 'antd';
import { UserOutlined, LockOutlined, CopyrightOutlined, GithubOutlined, MailOutlined } from '@ant-design/icons';
import styles from './login.module.css';
// 调整下引用路径:libs/history 改成 @/libs/history
import history from '@/libs/history';
import { http, updatePermissions } from '@/libs';

// 函数组件
export default function () {
    // antd 官网:我们推荐使用 Form.useForm 创建表单数据域进行控制。如果是在 class component 下,你也可以通过 ref 获取数据域。(https://ant.design/components/form-cn#components-form-demo-control-ref)
    // FormInstance 经 Form.useForm() 创建的 form 控制实例。FormInstance 有一系列方法,例如
    // 注:useForm 是 React Hooks 的实现,只能用于函数组件,class 组件请查看下面的例子(https://ant.design/components/form-cn#components-form-demo-control-hooks)
    const = Form.useForm();
    // 验证码倒计时
    const = useState(0);
    // 控制登录按钮
    const = useState(false);
    // 登录类型默认是 default
    const = useState('default');
    // 验证码。默认关闭。笔者将其开启
    const = useState(!false);
    const = useState(false);

    // 相当于 componentDidMount() 和 componentDidUpdate()(counter 变化时会执行)
    // 定时器,重新获取验证码倒计时。
    useEffect(() =&gt; {
      setTimeout(() =&gt; {
            // 默认是 0,故不会执行。当设置有效值时会执行,例如 30
            if (counter &gt; 0) {
                setCounter(counter - 1)
            }
      }, 1000)
    }, )

    // 登录
    function handleSubmit() {
      // getFieldsValue - 获取一组字段名对应的值,会按照对应结构返回
      // form 是 FormInstance。
      const formData = form.getFieldsValue();
      // 如果显示了“验证码”却没有输入,提示
      if (codeVisible &amp;&amp; !formData.captcha) return message.error('请输入验证码');

      // 登录中...
      setLoading(true);
      // 设置登录类型:default 或 ldap
      formData['type'] = loginType;

      // formData2 {username: '1', password: '2', captcha: '3', type: 'default'}
      console.log('formData2', formData)

      http.post('/api/account/login/', formData)
            // 官网返回: {"data": {"id": 1, "access_token": "4b6f1a9b8d824908abb9613695de57f8", "nickname": "\u7ba1\u7406\u5458", "is_supper": true, "has_real_ip": true, "permissions": []}, "error": ""}
            .then(data =&gt; {
                // 某种处理逻辑,我们可以去除这个分支
                if (data['required_mfa']) {
                  setCodeVisible(true);
                  setCounter(30);
                  setLoading(false)
                } else if (!data['has_real_ip']) { // 用户请求时没有真实ip则安全警告
                  Modal.warning({
                        title: '安全警告',
                        className: styles.tips,
                        content: &lt;div&gt;
                            未能获取到访问者的真实IP,无法提供基于请求来源IP的合法性验证,详细信息请参考
                            &lt;a target="_blank"
                              href="https://spug.cc/docs/practice/"
                              rel="noopener noreferrer"&gt;官方文档&lt;/a&gt;。
                        &lt;/div&gt;,
                        onOk: () =&gt; doLogin(data)
                  })
                } else {
                  doLogin(data)
                }
            }, () =&gt; setLoading(false))
    }

    // 将登录返回的数据存入本地,并更新权限和 token
    function doLogin(data) {
      // id
      localStorage.setItem('id', data['id']);
      // token
      localStorage.setItem('token', data['access_token']);
      // 昵称
      localStorage.setItem('nickname', data['nickname']);
      // is_supper
      localStorage.setItem('is_supper', data['is_supper']);
      // 权限
      localStorage.setItem('permissions', JSON.stringify(data['permissions']));

      // 权限和 token 相关。
      updatePermissions();
      // 登录成功则进入系统主页或未登录前访问的页面
      // 更具体就是:切换 Url。进入主页或登录前的页面(记录在 from 中)
      // react通过history.location.state来携带参数
      // 例如 spug\src\libs\http.js 中的:history.push('/', {from: history.location})
      if (history.location.state &amp;&amp; history.location.state['from']) {
            history.push(history.location.state['from'])
      } else {
            history.push('/home')
      }
    }

    // 获取验证码
    function handleCaptcha() {
      // 请求中...
      setCodeLoading(true);
      const formData = form.getFieldsValue(['username', 'password']);
      formData['type'] = loginType;
      // formData {username: '1', password: '2', type: 'default'}
      console.log('formData', formData)
      http.post('/api/account/login/', formData)
            // 30 秒后获得验证码
            .then(() =&gt; setCounter(30))
            .finally(() =&gt; setCodeLoading(false))
    }

    return (
      &lt;div className={styles.container}&gt;
            &lt;div className={styles.formContainer}&gt;
                {/* 仅做样式,默认选中第一个 tabpane。没有选项卡内容 */}
                &lt;Tabs className={styles.tabs} onTabClick={v =&gt; setLoginType(v)}&gt;
                  &lt;Tabs.TabPane tab="普通登录" key="default" /&gt;
                  &lt;Tabs.TabPane tab="LDAP登录" key="ldap" /&gt;
                &lt;/Tabs&gt;
                {/* 使用 Form.useForm 创建表单数据域进行控制 */}
                &lt;Form form={form}&gt;
                  &lt;Form.Item name="username" className={styles.formItem}&gt;
                        &lt;Input
                            size="large"
                            // 关闭自动完成的选项
                            autoComplete="off"
                            placeholder="请输入账户"
                            // 人头像的 icon
                            prefix={&lt;UserOutlined className={styles.icon} /&gt;} /&gt;
                  &lt;/Form.Item&gt;
                  &lt;Form.Item name="password" className={styles.formItem}&gt;
                        &lt;Input
                            size="large"
                            type="password"
                            autoComplete="off"
                            placeholder="请输入密码"
                            // 按下回车的回调。即提交
                            onPressEnter={handleSubmit}
                            // 锁的icon
                            prefix={&lt;LockOutlined className={styles.icon} /&gt;} /&gt;
                  &lt;/Form.Item&gt;
                  {/* 验证码。默认关闭 */}
                  {/* 这里展示了 Form.Item 嵌套用法 */}
                  &lt;Form.Item hidden={!codeVisible} name="captcha" className={styles.formItem}&gt;
                        &lt;div style={{ display: 'flex' }}&gt;
                            &lt;Form.Item noStyle name="captcha"&gt;
                              &lt;Input
                                    size="large"
                                    autoComplete="off"
                                    placeholder="请输入验证码"
                                    prefix={&lt;MailOutlined className={styles.icon} /&gt;} /&gt;
                            &lt;/Form.Item&gt;
                            {counter &gt; 0 ? (
                              &lt;Button disabled size="large" style={{ marginLeft: 8 }}&gt;{counter} 秒后重新获取&lt;/Button&gt;
                            ) : (
                              &lt;Button size="large" loading={codeLoading} style={{ marginLeft: 8 }}
                                    onClick={handleCaptcha}&gt;获取验证码&lt;/Button&gt;
                            )}
                        &lt;/div&gt;
                  &lt;/Form.Item&gt;
                &lt;/Form&gt;

                &lt;Button
                  // block 属性将使按钮适合其父宽度。
                  block
                  size="large"
                  type="primary"
                  className={styles.button}
                  loading={loading}
                  onClick={handleSubmit}&gt;登录&lt;/Button&gt;
            &lt;/div&gt;
            {/* 网站底部统一信息。这里是`官网`、`github 地址`、`文档` */}
            &lt;div className={styles.footerZone}&gt;
                &lt;div className={styles.linksZone}&gt;
                  &lt;a className={styles.links} title="官网" href="https://spug.cc" target="_blank"
                        rel="noopener noreferrer"&gt;官网&lt;/a&gt;
                  &lt;a className={styles.links} title="Github" href="https://github.com/openspug/spug" target="_blank"
                        rel="noopener noreferrer"&gt;&lt;GithubOutlined /&gt;&lt;/a&gt;
                  &lt;a title="文档" href="https://spug.cc/docs/about-spug/" target="_blank"
                        rel="noopener noreferrer"&gt;文档&lt;/a&gt;
                &lt;/div&gt;
                &lt;div style={{ color: '#fff' }}&gt;Copyright &lt;CopyrightOutlined /&gt; {new Date().getFullYear()} By Spug&lt;/div&gt;
            &lt;/div&gt;
      &lt;/div&gt;
    )
}
</code></pre>
<h4 id="updatepermissions">updatePermissions</h4>
<p>登录页中引入了 updatePermissions(<code>import { http, updatePermissions } from '@/libs';</code>)。<br>
<em>Tip</em>:updatePermissions 的作用用于更新 functools.js 模块中的 X_TOKEN(spug中没有前端没有清除 X_TOKEN) 和 Permission变量。</p>
<p>我们将 spug 中的相关代码弄过来。步骤如下:</p>
<ul>
<li>在 functools.js 增加 updatePermissions:</li>
</ul>
<pre><code class="language-javascript">// myspug\src\libs\functools.js

+// 准许。权限相关。模块私有
+let Permission = {
+    isReady: false,
+    isSuper: false,
+    permissions: []
+};
+

// 由 updatePermissions() 更新
export let X_TOKEN;
+
+
+// 被入口页(src/index.js)和登录页(src/pages/login/index.js)调用
+export function updatePermissions() {
+    // 读取 localStorage 项
+    // 只在登录时设置:localStorage.setItem('token'
+    X_TOKEN = localStorage.getItem('token');
+    Permission.isReady = true;
+    Permission.isSuper = localStorage.getItem('is_supper') === 'true';
+    try {
+      Permission.permissions = JSON.parse(localStorage.getItem('permissions') || '[]');
+    } catch (e) {
+
+    }
+}
</code></pre>
<ul>
<li>在 libs/index.js 中将 functools.js 模块导出:</li>
</ul>
<pre><code class="language-javascript">// myspug\src\libs\index.js

// 导出一切。注:没有导出默认值
export * from './functools';
</code></pre>
<ul>
<li>入口页更新权限:</li>
</ul>
<pre><code class="language-javascript">// myspug\src\index.js

import React from 'react';
import { history, updatePermissions } from '@/libs';

const root = ReactDOM.createRoot(document.getElementById('root'));

+ // 权限和 token 相关。
+ updatePermissions();
</code></pre>
<h3 id="myspug-登录模块的验证">myspug 登录模块的验证</h3>
<p>验证步骤如下:</p>
<ul>
<li>输入 <code>http://localhost:3000/</code> 进入登录页</li>
</ul>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230113014519_highqualitybacksystem-login-02.png"></p>
<ul>
<li>
<p>在登录页输入登录信息,登录成功进入主页</p>
</li>
<li>
<p>修改浏览器 url(<code>http://localhost:3000/log</code>)回车进入系统,控制台执行 <code>_history.push('/', { from: _history.location })</code> 模拟请求过期重置到登录页,再次输入登录信息登录,回到原来页面(log)</p>
</li>
</ul>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230113014529_highqualitybacksystem-login-04.png"></p>
<h3 id="补充">补充</h3>
<h4 id="myspug-登录bug">myspug 登录bug</h4>
<p>myspug 登录页有一个小bug,Tabs 下没有显示哪个选中了。就像这样:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_230113014524_highqualitybacksystem-login-03.png"></p>
<p>发现是选中的进度条没有动态设置宽度,width 一直为 0。怀疑是 myspug 中 antd-按需引入-css 有问题,但 antd 其他组件(例如分页、form等)没有问题,Tabs也仅发现这一个样式问题,去除按需引入 css 也没解决。</p>
<p>笔者暂时未深入,或许简化环境,从头开始可以找到问题</p>
<h4 id="普通登录和-ldap-登录">普通登录和 LDAP 登录</h4>
<p>spug 前端这里<code>普通登录</code>和 <code>LDAP 登录</code>是相同处理的,都是输入用户名和密码。</p>
<p>只要公司给员工分配了 LDAP(可实现公司内部多系统的统一登录) 的用户名和密码,该员工则可直接使用 LDAP 方式登录系统,无需再重复注册。</p>
<h4 id="登录标识-token">登录标识 token</h4>
<p>spug 中输入用户名、密码,登录成功后,后端返回数据中包含 token(即后端分配给用户的一个<code>登录标识</code>),前端将其保存在 localStorage 中,后续前端所有的请求都将会带上这个标识(token),后端通过这个标识识别用户否有权限访问该请求,如果 token 过期,则返回 401 告诉前端“会话过期,请重新登录”。</p>
<h4 id="x_token-跨模块">X_TOKEN 跨模块</h4>
<p>spug 中有个模块(functools.js),定义了一个私有变量,导出了两个变量。</p>
<pre><code class="language-javascript">// myspug\src\libs\functools.js
let Permission = {
    isReady: false,
    ...
};

// 由 updatePermissions() 更新
export let X_TOKEN;

export function updatePermissions() {
    X_TOKEN = localStorage.getItem('token');
    Permission.isReady = true;
   
}
</code></pre>
<p>在登录模块中仅导入 updatePermissions,登录成功后会执行该方法,会给 X_TOKEN 赋值。而在其他模块(例如 http.js)仅导入 X_TOKEN,这时 X_TOKEN 就会有值。</p>
<p>笔者测试如下:</p>
<ul>
<li>在 HelloWord.js 中引入 X_TOKEN:</li>
</ul>
<pre><code class="language-javascript">// myspug\src\HelloWord.js

import { X_TOKEN } from "./libs/functools"

export default function HelloWorld() {
    return &lt;div&gt;hello world!。token = {X_TOKEN}&lt;/div&gt;
}
</code></pre>
<ul>
<li>登录成功后,页面会显示 <code>hello world!。token = xxxxxxxx...</code></li>
</ul>
<blockquote>
<p>其他章节请看:</p>
<p>react 高效高质量搭建后台系统 系列</p>
</blockquote>


</div>
<div id="MySignature" role="contentinfo">
    <section class="m-reprintStatement" style="white-space:normal;/* 防止break-all与nowrap矛盾 */word-break:break-all;">
    作者:彭加李<br>
    出处:https://www.cnblogs.com/pengjiali/p/17048644.html<br>
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
</section><br><br>
来源:https://www.cnblogs.com/pengjiali/p/17048644.html
頁: [1]
查看完整版本: react 高效高质量搭建后台系统 系列 —— 登录