- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
其他章节请看:
react 高效高质量搭建后台系统 系列 。
本篇将完成 登录模块 。效果和 spug 相同:
需求 如下:
主页
,没有登录的情况下访问系统会重定向到登录页,登录成功后再次回到 之前的页面
。系统会话过期后,请求会重定向到登录页。 Tip : 退出登录 在进入系统后进行,暂不不管.
登录页是进入系统的 门户 ,登录页绘制逻辑比较简单(单个模块的开发比较简单).
首先要 解决 :根据 url 不同,进入 登录页 还是 系统 主页。这里需要使用路由器.
详情请看 react 路由 、 react 路由原理 。
Tip :实现的核心是 Router,以及 history 包.
需求 :浏览器输入 /( http://localhost:3010/ ) 进入登录页,其他路径进入系统.
实现如下:
<Router history={history}>
管理路由:
// spug\src\index.js
import { history, updatePermissions } from 'libs';
// 权限、token 相关
updatePermissions();
ReactDOM.render(
// Router 是路由器,用于管理路由
// `history: object` 用来导航的 history 对象。
<Router history={history}>
<ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>
<App/>
</ConfigProvider>
</Router>,
document.getElementById('root')
);
其中 history 用于导航 history 对象(此用法在路由官网中)。执行 history.push 时不仅会改变浏览器的 url,而且路由也会发生变化(请看本篇“history={history} 的作用”章节) 。
// 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';
history 仅对 history 包的导出,在 这里 中已介绍.
/
则进入登录页,否则进入系统( Layout 是 antd 中的 Layout 组件,对 404 的界面反馈也 Layout 模块中进行了处理)。
// spug\src\App.js
class App extends Component {
render() {
return (
// 只渲染其中一个 Route
// exact 精确匹配
// component={Login} 路由组件(不同于一般组件,其 props 中有路由相关方法。)
<Switch>
<Route path="/" exact component={Login} />
{/* 没有匹配则进入 Layout */}
<Route component={Layout} />
</Switch>
);
}
}
export default App;
<Router history={history}>
。 Tip : StrictMode(一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样) 仍旧保留.
// 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。它为其后代元素触发额外的检查和警告。
+ // 严格模式检查仅在开发模式下运行;它们不会影响生产构建。
<React.StrictMode>
- <ConfigProvider locale={zhCN}>
- <App />
- </ConfigProvider>
+ <Router history={history}>
+ <ConfigProvider locale={zhCN}>
+ <App />
+ </ConfigProvider>
+ </Router>
</React.StrictMode>
);
libs/index.js
,主要是导出 history:
// 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';
/
则进入登录页,如果是其他 url 则进入 HelloWorld(用来模拟 Layout)
// 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 中有路由相关方法。)
<Switch>
<Route path="/" exact component={Login} />
{/* 没有匹配则进入 Layout */}
<Route component={HelloWorld} />
</Switch>
);
}
}
export default App;
// myspug\src\pages\login\index.js
export default function() {
return <div>登录页</div>
}
// myspug\src\HelloWord.js
export default function HelloWorld() {
return <div>hello world!</div>
}
测试 结果如下:
浏览器:http://localhost:3000/
显示: 登录页
浏览器:http://localhost:3000/home
显示: hello world!
在 请求数据 一文中我们曾有一个 疑惑 :spug 官网中执行 history.push 不仅可以切换url,而且路由也发生了变化.
笔者测试发现:是入口页 <Router history={history}> 中 history 的功劳.
验证步骤如下:
// myspug\src\libs\http.js
import http from 'axios'
import history from './history'
// 将其导出
window._history = history;
http://localhost:3000/
并在控制台中输入:
执行:_history.push('/home')
url 变成 http://localhost:3000/home 浏览器显示:hello world!
执行:_history.push('/')
url 变成 http://localhost:3000/ 浏览器显示:登录页
Tip :如果删除入口页的 history={history} ,浏览器控制台将报错如下,提示没有 location 属性,无法进行路由匹配:
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
我们初步解决了登录页和主页(或系统)之间的跳转(或路由).
下面我们完整分析 spug 中登录模块的实现,比如登录绘制、普通登录和LDAP登录... 。
登录模块代码都在 spug/src/pages/login 目录下,一个 js 文件,一个样式文件:
Administrator@-WK-10 MINGW /e/spug/src/pages/login
$ ls
bg.png index.js login.module.css
login.module.css 是登录模块的样式文件, 前文 已分析过样式,这里不再冗余.
登录的 核心 全在 index.js 中.
我们参照登录界面说一下 index.js 的结构:
函数式的组件
,返回的 div 包括两部分:登录信息输入区、网站底部统一信息区 Form.useForm
创建表单数据域进行控制 语法
store
的初始化用于对应模块的使用 验证码
倒计时
// spug\src\pages\login\index.js
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* 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] = Form.useForm();
// 验证码倒计时
const [counter, setCounter] = useState(0);
// 控制登录按钮
const [loading, setLoading] = useState(false);
// 登录类型默认是 default
const [loginType, setLoginType] = useState('default');
// 验证码。默认关闭
const [codeVisible, setCodeVisible] = useState(!false);
const [codeLoading, setCodeLoading] = useState(false);
// 组件挂载后执行。相当于 componentDidMount()
useEffect(() => {
envStore.records = [];
appStore.records = [];
requestStore.records = [];
requestStore.deploys = [];
hostStore.records = null;
hostStore.groups = {};
hostStore.treeData = [];
execStore.hosts = [];
}, [])
// 相当于 componentDidMount() 和 componentDidUpdate()(counter 变化时会执行)
// 定时器,重新获取验证码倒计时。
useEffect(() => {
setTimeout(() => {
// 默认是 0,故不会执行。当设置有效值时会执行,例如 30
if (counter > 0) {
setCounter(counter - 1)
}
}, 1000)
}, [counter])
// 登录
function handleSubmit() {
// form 是 FormInstance。
// getFieldsValue - 获取一组字段名对应的值,会按照对应结构返回
const formData = form.getFieldsValue();
// 如果显示了“验证码”却没有输入,提示
if (codeVisible && !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 => {
// 某种处理逻辑
if (data['required_mfa']) {
setCodeVisible(true);
setCounter(30);
setLoading(false)
// 用户请求时没有真实ip则安全警告
} else if (!data['has_real_ip']) {
Modal.warning({
title: '安全警告',
className: styles.tips,
content: <div>
未能获取到访问者的真实IP,无法提供基于请求来源IP的合法性验证,详细信息请参考
<a target="_blank"
href="https://spug.cc/docs/practice/"
rel="noopener noreferrer">官方文档</a>。
</div>,
onOk: () => doLogin(data)
})
} else {
doLogin(data)
}
}, () => 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 && 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(() => setCounter(30))
.finally(() => setCodeLoading(false))
}
return (
<div className={styles.container}>
<div className={styles.formContainer}>
{/* 仅做样式,默认选中第一个 tabpane。没有选项卡内容 */}
<Tabs className={styles.tabs} onTabClick={v => setLoginType(v)}>
<Tabs.TabPane tab="普通登录" key="default" />
<Tabs.TabPane tab="LDAP登录" key="ldap" />
</Tabs>
{/* 使用 Form.useForm 创建表单数据域进行控制 */}
<Form form={form}>
<Form.Item name="username" className={styles.formItem}>
<Input
size="large"
// 关闭自动完成的选项
autoComplete="off"
placeholder="请输入账户"
// 人头像的 icon
prefix={<UserOutlined className={styles.icon} />} />
</Form.Item>
<Form.Item name="password" className={styles.formItem}>
<Input
size="large"
type="password"
autoComplete="off"
placeholder="请输入密码"
// 按下回车的回调。即提交
onPressEnter={handleSubmit}
// 锁的icon
prefix={<LockOutlined className={styles.icon} />} />
</Form.Item>
{/* 验证码。默认关闭 */}
{/* 这里展示了 Form.Item 嵌套用法 */}
<Form.Item hidden={!codeVisible} name="captcha" className={styles.formItem}>
<div style={{ display: 'flex' }}>
<Form.Item noStyle name="captcha">
<Input
size="large"
autoComplete="off"
placeholder="请输入验证码"
prefix={<MailOutlined className={styles.icon} />} />
</Form.Item>
{counter > 0 ? (
<Button disabled size="large" style={{ marginLeft: 8 }}>{counter} 秒后重新获取</Button>
) : (
<Button size="large" loading={codeLoading} style={{ marginLeft: 8 }}
onClick={handleCaptcha}>获取验证码</Button>
)}
</div>
</Form.Item>
</Form>
<Button
// block 属性将使按钮适合其父宽度。
block
size="large"
type="primary"
className={styles.button}
loading={loading}
onClick={handleSubmit}>登录</Button>
</div>
{/* 网站底部统一信息。这里是`官网`、`github 地址`、`文档` */}
<div className={styles.footerZone}>
<div className={styles.linksZone}>
<a className={styles.links} title="官网" href="https://spug.cc" target="_blank"
rel="noopener noreferrer">官网</a>
<a className={styles.links} title="Github" href="https://github.com/openspug/spug" target="_blank"
rel="noopener noreferrer"><GithubOutlined /></a>
<a title="文档" href="https://spug.cc/docs/about-spug/" target="_blank"
rel="noopener noreferrer">文档</a>
</div>
<div style={{ color: '#fff' }}>Copyright <CopyrightOutlined /> {new Date().getFullYear()} By Spug</div>
</div>
</div>
)
}
Tip :登录样式( pages\login\login.module.css )仅仅是一些样式,直接从 spug 拷贝即可 。
新建 pages\login\index.js 文件,内容如下:
Tip : 与 spug 中 login\index.js 类似,微做如下调整
@/libs/history
// 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] = Form.useForm();
// 验证码倒计时
const [counter, setCounter] = useState(0);
// 控制登录按钮
const [loading, setLoading] = useState(false);
// 登录类型默认是 default
const [loginType, setLoginType] = useState('default');
// 验证码。默认关闭。笔者将其开启
const [codeVisible, setCodeVisible] = useState(!false);
const [codeLoading, setCodeLoading] = useState(false);
// 相当于 componentDidMount() 和 componentDidUpdate()(counter 变化时会执行)
// 定时器,重新获取验证码倒计时。
useEffect(() => {
setTimeout(() => {
// 默认是 0,故不会执行。当设置有效值时会执行,例如 30
if (counter > 0) {
setCounter(counter - 1)
}
}, 1000)
}, [counter])
// 登录
function handleSubmit() {
// getFieldsValue - 获取一组字段名对应的值,会按照对应结构返回
// form 是 FormInstance。
const formData = form.getFieldsValue();
// 如果显示了“验证码”却没有输入,提示
if (codeVisible && !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 => {
// 某种处理逻辑,我们可以去除这个分支
if (data['required_mfa']) {
setCodeVisible(true);
setCounter(30);
setLoading(false)
} else if (!data['has_real_ip']) { // 用户请求时没有真实ip则安全警告
Modal.warning({
title: '安全警告',
className: styles.tips,
content: <div>
未能获取到访问者的真实IP,无法提供基于请求来源IP的合法性验证,详细信息请参考
<a target="_blank"
href="https://spug.cc/docs/practice/"
rel="noopener noreferrer">官方文档</a>。
</div>,
onOk: () => doLogin(data)
})
} else {
doLogin(data)
}
}, () => 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 && 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(() => setCounter(30))
.finally(() => setCodeLoading(false))
}
return (
<div className={styles.container}>
<div className={styles.formContainer}>
{/* 仅做样式,默认选中第一个 tabpane。没有选项卡内容 */}
<Tabs className={styles.tabs} onTabClick={v => setLoginType(v)}>
<Tabs.TabPane tab="普通登录" key="default" />
<Tabs.TabPane tab="LDAP登录" key="ldap" />
</Tabs>
{/* 使用 Form.useForm 创建表单数据域进行控制 */}
<Form form={form}>
<Form.Item name="username" className={styles.formItem}>
<Input
size="large"
// 关闭自动完成的选项
autoComplete="off"
placeholder="请输入账户"
// 人头像的 icon
prefix={<UserOutlined className={styles.icon} />} />
</Form.Item>
<Form.Item name="password" className={styles.formItem}>
<Input
size="large"
type="password"
autoComplete="off"
placeholder="请输入密码"
// 按下回车的回调。即提交
onPressEnter={handleSubmit}
// 锁的icon
prefix={<LockOutlined className={styles.icon} />} />
</Form.Item>
{/* 验证码。默认关闭 */}
{/* 这里展示了 Form.Item 嵌套用法 */}
<Form.Item hidden={!codeVisible} name="captcha" className={styles.formItem}>
<div style={{ display: 'flex' }}>
<Form.Item noStyle name="captcha">
<Input
size="large"
autoComplete="off"
placeholder="请输入验证码"
prefix={<MailOutlined className={styles.icon} />} />
</Form.Item>
{counter > 0 ? (
<Button disabled size="large" style={{ marginLeft: 8 }}>{counter} 秒后重新获取</Button>
) : (
<Button size="large" loading={codeLoading} style={{ marginLeft: 8 }}
onClick={handleCaptcha}>获取验证码</Button>
)}
</div>
</Form.Item>
</Form>
<Button
// block 属性将使按钮适合其父宽度。
block
size="large"
type="primary"
className={styles.button}
loading={loading}
onClick={handleSubmit}>登录</Button>
</div>
{/* 网站底部统一信息。这里是`官网`、`github 地址`、`文档` */}
<div className={styles.footerZone}>
<div className={styles.linksZone}>
<a className={styles.links} title="官网" href="https://spug.cc" target="_blank"
rel="noopener noreferrer">官网</a>
<a className={styles.links} title="Github" href="https://github.com/openspug/spug" target="_blank"
rel="noopener noreferrer"><GithubOutlined /></a>
<a title="文档" href="https://spug.cc/docs/about-spug/" target="_blank"
rel="noopener noreferrer">文档</a>
</div>
<div style={{ color: '#fff' }}>Copyright <CopyrightOutlined /> {new Date().getFullYear()} By Spug</div>
</div>
</div>
)
}
登录页中引入了 updatePermissions( import { http, updatePermissions } from '@/libs'; )。 Tip :updatePermissions 的作用用于更新 functools.js 模块中的 X_TOKEN(spug中没有前端没有清除 X_TOKEN) 和 Permission变量.
我们将 spug 中的相关代码弄过来。步骤如下:
// 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) {
+
+ }
+}
// myspug\src\libs\index.js
// 导出一切。注:没有导出默认值
export * from './functools';
// myspug\src\index.js
import React from 'react';
import { history, updatePermissions } from '@/libs';
const root = ReactDOM.createRoot(document.getElementById('root'));
+ // 权限和 token 相关。
+ updatePermissions();
验证步骤如下:
http://localhost:3000/
进入登录页
在登录页输入登录信息,登录成功进入主页 。
修改浏览器 url( http://localhost:3000/log )回车进入系统,控制台执行 _history.push('/', { from: _history.location }) 模拟请求过期重置到登录页,再次输入登录信息登录,回到原来页面(log) 。
myspug 登录页有一个小bug,Tabs 下没有显示哪个选中了。就像这样:
发现是选中的进度条没有动态设置宽度,width 一直为 0。怀疑是 myspug 中 antd-按需引入-css 有问题,但 antd 其他组件(例如分页、form等)没有问题,Tabs也仅发现这一个样式问题,去除按需引入 css 也没解决.
笔者暂时未深入,或许简化环境,从头开始可以找到问题 。
spug 前端这里 普通登录 和 LDAP 登录 是相同处理的,都是输入用户名和密码.
只要公司给员工分配了 LDAP(可实现公司内部多系统的统一登录) 的用户名和密码,该员工则可直接使用 LDAP 方式登录系统,无需再重复注册.
spug 中输入用户名、密码,登录成功后,后端返回数据中包含 token(即后端分配给用户的一个 登录标识 ),前端将其保存在 localStorage 中,后续前端所有的请求都将会带上这个标识(token),后端通过这个标识识别用户否有权限访问该请求,如果 token 过期,则返回 401 告诉前端“会话过期,请重新登录”.
spug 中有个模块(functools.js),定义了一个私有变量,导出了两个变量.
// 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;
}
在登录模块中仅导入 updatePermissions,登录成功后会执行该方法,会给 X_TOKEN 赋值。而在其他模块(例如 http.js)仅导入 X_TOKEN,这时 X_TOKEN 就会有值.
笔者测试如下:
// myspug\src\HelloWord.js
import { X_TOKEN } from "./libs/functools"
export default function HelloWorld() {
return <div>hello world!。token = {X_TOKEN}</div>
}
hello world!。token = xxxxxxxx...
其他章节请看:
react 高效高质量搭建后台系统 系列 。
最后此篇关于react高效高质量搭建后台系统系列——登录的文章就讲到这里了,如果你想了解更多关于react高效高质量搭建后台系统系列——登录的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
为了构建 CentOS 6.5 OSM 切片服务器,我正在寻找一些文档和/或教程。 我试过this one正如我在我的 previous post 中所说的那样但它适用于 Ubuntu 14.04,而
我正在寻找可用于集成任何源代码控制管理系统的通用 git 桥(如 git-svn、git-p4、git-tfs)模板。 如果没有这样的模板,至少有一些关于如何在 git 端集成基本操作的说明(对于其他
1、前言 redis在我们企业级开发中是很常见的,但是单个redis不能保证我们的稳定使用,所以我们要建立一个集群。 redis有两种高可用的方案: High availabilit
简介 前提条件: 确保本机已经安装 VS Code。 确保本机已安装 SSH client, 并且确保远程主机已安装 SSH server。 VSCode 已经安装了插件 C/
为什么要用ELK ELK实际上是三个工具,Elastricsearch + Logstash + Kibana,通过ELK,用来收集日志还有进行日志分析,最后通过可视化UI进行展示。一开始业务量比
在日常办公当中,经常会需要一个共享文件夹来存放一些大家共享的资料,为了保证文件数据的安全,最佳的方式是公司内部服务器搭建FTP服务器,然后分配多个用户给相应的人员。今天给大家分享FileZilla搭
最近由于业务需要,开始进行 Flutter 的研究,由于 Flutter 的环境搭建在官网上有些细节不是很清楚,笔者重新整理输出 1. 配置镜像 由于在国内访问 Flutter
目录 1. 安装go软件包 2. 配置系统变量 3. 安装git 4. 设置go代理 5. 下载gin框架 6. 创建项目 7.
Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任
上篇文章给大家介绍了使用docker compose安装FastDfs文件服务器的实例详解 今天给大家介绍如何使用 docker compose 搭建 fastDFS文件服务器,内容详情如下所示:
目录 1.创建Maven 2.Maven目录和porm.xml配置 3.配置Tomcat服务器 1.创建Maven
laravel 官方提供 homestead 和 valet 作为本地开发环境,homestead 是一个官方预封装的 vagrant box,也就是一个虚拟机,但是跟 docker 比,它占用体积
这个tutorial显示了 Razor Pages 在 Asp.Net Core 2 中的实现。但是,当我运行 CLI 命令时: dotnet aspnet-codegenerator razorp
我创建了一个单独的类库项目来存储数据库上下文和模型类。在同一解决方案中,我创建了一个 ASP.NET MVC 项目并引用了类库项目,并在项目的 Web.config 文件中包含了数据库上下文的连接字符
关于代码托管,公司是基于Gitlab自建的,它功能全而强大,但是也比较重,我个人偏向于开源、小巧、轻便、实用,所以就排除了Github,在Gogs和Gitea中选者。Gogs在Github有38
目录 1、高可用简介 1.1 高可用整体架构 1.2 基于 QJM 的共享存储系统的数据同步机制分析 1.3 NameNode 主
Nginx 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,它已经在该站点运行超过两年半了。Igor 将源代码以类BSD许可证的形式发布。 在高并发连接的情况
对于我们的 ASP.NET Core 项目,我们使用包管理器控制台中的 Scaffold-DbContext 搭建现有数据库。 每次我们做脚手架时,上下文类与所有实体一起生成,它包含调用 option
我正在使用 .net 核心 2.0。我已经安装了以下 nuget 包:1: Microsoft.AspNetCore.All2: Microsoft.EntityFrameworkCore.Tools
我正在使用 NetBeans 及其 RAD 开发功能开发 JEE6 JSF 应用程序。我想使用脚手架来节省更新 Controller 和模型 View 的时间。 OneToMany 关联在 View
我是一名优秀的程序员,十分优秀!