2、Python3 博客前端开发
2.1、开发环境设置
解压 webpackjs.rar 脚手架,更名为 frontend。在 src 中新增 component、service、css 目录。
注:没有特别说明,js 开发都在 src 目录下。
frontend/
|-- babel.config.js
|-- config
| `-- webpack.config.js
|-- package-lock.json
|-- package.json
|-- public
| `-- index.html
`-- src
|-- App.jsx
|-- component
|-- css
|-- main.js
`-- service
修改项目信息:
{
"name": "blog",
"version": "1.0.0",
"description": "blog project",
"author": "Brinnatt",
}
webpack.config.js
proxy: {
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true
}
}
proxy 指定访问 /api
开头路径都代理到 http://127.0.0.1:8080
。
安装依赖:
$ npm install
npm 会按照 package.json 的定义安装 devDependencies 和 dependencies 依赖。注意必须科学上网才能安装顺利,安装过程还会请求 github 之类的网站。
我这里的打包文件中已安装,解压后可以直接使用。
2.2、开发
2.2.1、前端路由
前端路由使用 react-router 组件完成。
官网文档 https://reactrouter.com/en/main/start/tutorial
基本例子 https://reactrouter.com/en/main/start/examples
使用 react-router,更改 src/main.js。
import * as React from "react";
import { createRoot } from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
Link,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
<h1>This is Home</h1>
<Link to="about">To About</Link>
</div>
),
},
{
path: "about",
element: <div>This is About</div>,
},
]);
createRoot(document.getElementById("app")).render(
<RouterProvider router={router} />
);
在地址栏里面输入 http://localhost:3000/ 或 http://localhost:3000/about 试试看,能够看到页面的变化
以上是 react-router v6.x 的新语法,请参见官方文档。
2.2.2、登录组件
在 component 目录下构建 react 组件。
登录页模板:https://codepen.io/colorlib/pen/rxddKy?q=login&limit=all&type=type-pens
<div class="login-page">
<div class="form">
<form class="register-form">
<input type="text" placeholder="name"/>
<input type="password" placeholder="password"/>
<input type="text" placeholder="email address"/>
<button>create</button>
<p class="message">Already registered? <a href="#">Sign In</a></p>
</form>
<form class="login-form">
<input type="text" placeholder="username"/>
<input type="password" placeholder="password"/>
<button>login</button>
<p class="message">Not registered? <a href="#">Create an account</a></p>
</form>
</div>
</div>
使用这个 HTML 模板来构建组件。
特别注意:搬到 React 组件中的时候,要将 class 属性改为 className。所有标签,需要闭合。
login.js
在 component 目录下建立 login.js 的登录组件。使用上面 HTML 模板中的登录部分,挪到 render 函数中。
- 修改 class 为 className。
- 将
<a>
标签替换成<Link to="?">
组件。 - 注意标签闭合问题。
import React from 'react';
import { Link } from 'react-router-dom';
export default class Login extends React.Component {
render() {
return (<div className="login-page">
<div className="form">
<form className="login-form">
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<button>登录</button>
<p className="message">还未注册? <Link to="/reg">请注册</Link></p>
</form>
</div>
</div>);
}
}
index.js
在 component 目录下建立 index.js 的路由实例,增加登录组件。
import * as React from "react";
import { createBrowserRouter, Link } from "react-router-dom";
import Login from "./login";
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
<h1>This is Home</h1>
<Link to="/about">To About</Link>
</div>
),
},
{
path: "/about",
element: <div>This is About</div>,
},
{
path: "/login",
element: <Login />,
},
]);
export default router;
main.js
import * as React from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import router from "./component/index"
createRoot(document.getElementById("app")).render(
<RouterProvider router={router} />
);
访问 http://localhost:3000/login 就可以看到登录界面了。但是没有样式。
样式表,在 src/css 中,创建 login.css,放入以下内容,然后在 login.js 中导入样式。
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #FFFFFF;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: #4CAF50;
width: 100%;
border: 0;
padding: 15px;
color: #FFFFFF;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form button:hover,.form button:active,.form button:focus {
background: #43A047;
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: #4CAF50;
text-decoration: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before, .container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #EF3B3A;
}
body {
background: #76b852; /* fallback for old browsers */
background: rgb(141,194,111);
background: linear-gradient(90deg, rgba(141,194,111,1) 0%, rgba(118,184,82,1) 50%);
font-family: "Roboto", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
在 login.js 中导入样式表 import '../css/login.css'
,再一次访问 http://localhost:3000/login 就可以看到如下界面:
2.2.3、注册组件
与登录组件编写方式差不多,创建 component/reg.js,使用 login.css。
import React from 'react';
import { Link } from 'react-router-dom';
import '../css/login.css'
const Reg = () => (
<div className="login-page">
<div className="form">
<form className="register-form">
<input type="text" placeholder="姓名" />
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<input type="password" placeholder="确认密码" />
<button>注册</button>
<p className="message">如果已经注册<Link to="/login">请登录</Link></p>
</form>
</div>
</div>
);
export default Reg;
在 index.js 中增加一条静态路由。
import * as React from "react";
import { createBrowserRouter, Link } from "react-router-dom";
import Login from "./login";
import Reg from './reg'
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
<h1>This is Home</h1>
<Link to="/about">To About</Link>
</div>
),
},
{
path: "/about",
element: <div>This is About</div>,
},
{
path: "/login",
element: <Login />,
},
{
path: "/reg",
element: <Reg />,
},
]);
export default router;
输入 http://localhost:3000/reg 可以看到如下界面:
2.2.4、导航栏组件
在 index.js 中增加导航栏组件,方便页面切换。createBrowserRouter 是创建路由实例的函数,不能直接加导航标签,修改 index.js:
import * as React from "react";
import { Link, useRoutes } from "react-router-dom";
import Login from "./login";
import Reg from './reg'
const App = () => {
const routes = useRoutes([
{
path: "/",
element: (
<div>
<h1>This is Home</h1>
<Link to="/about">To About</Link>
</div>
),
},
{
path: "/about",
element: <div>This is About</div>,
},
{
path: "/login",
element: <Login />,
},
{
path: "/reg",
element: <Reg />,
},
]);
return (
<div>
<nav>
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/login">登陆</Link>
</li>
<li>
<Link to="/reg">注册</Link>
</li>
</ul>
</nav>
{routes}
</div>
);
}
export default App;
修改 main.js:
import * as React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./component/index";
const root = createRoot(document.getElementById("app"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
接下来,我们实现分层前端开发:
视图层,负责显示数据,每一个 React 组件一个 xxx.js。
服务层,负责业务数据处理逻辑,命名为 xxxService.js。
Model 层,负责数据处理,数据从后端取。
2.3、登录功能实现
view 层,登录组件和用户交互。当 button 点击触发 onClick,调用事件响应函数 handleClick,handleClick 中调用服务 service 层的 login 函数。
service 层,负责业务逻辑处理。调用 Model 层数据操作函数。
在 src/service/user.js
export default class UserService {
login (email, password) {
// TODO
}
}
src/component/login.js
import React from 'react';
import { Link } from 'react-router-dom';
import '../css/login.css'
// const Login = () => (
// <div className="login-page">
// <div className="form">
// <form className="login-form">
// <input type="text" placeholder="邮箱" />
// <input type="password" placeholder="密码" />
// <button>登录</button>
// <p className="message">还未注册? <Link to="/reg">请注册</Link></p>
// </form>
// </div>
// </div>
// );
// export default Login;
export default class Login extends React.Component {
handlerClick(event) {
console.log(event.target)
}
render() {
return (
<div className="login-page">
<div className="form">
<form className="login-form">
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<button onClick={this.handlerClick.bind(this)}>登录</button>
<p className="message">还未注册? <Link to="/reg">请注册</Link></p>
</form>
</div>
</div>
);
}
}
这一次发现有一些问题,按钮点击会提交,导致页面刷新了。要阻止页面刷新,其实就是阻止提交。使用 event.preventDefault()。
如何拿到邮箱和密码?
-
event.target.form
返回按钮所在表单,可以看作一个数组。 -
fm[0].value
和fm[0].value
就是文本框的值。
如何在 Login 组件中使用 UserService 实例呢?
-
可以在 Login 的构造器中通过属性注入;
-
也可以在外部使用 props 注入。使用这种方式。
import React from 'react';
import { Link } from 'react-router-dom';
import '../css/login.css'
import UserService from '../service/user';
const userService = new UserService();
export default class Login extends React.Component {
render() {
return <Login0 service={userService} />;
}
}
class Login0 extends React.Component {
handlerClick(event) {
event.preventDefault();
let fm = event.target.form;
this.props.service.login(
fm[0].value, fm[1].value
);
}
render() {
return (
<div className="login-page">
<div className="form">
<form className="login-form">
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<button onClick={this.handlerClick.bind(this)}>登录</button>
<p className="message">还未注册? <Link to="/reg">请注册</Link></p>
</form>
</div>
</div>
);
}
}
2.3.1、UserService 的 login 方法实现
2.3.1.1、代理配置
修改 webpack.config.js 文件中 proxy 部分。注意,修改这个配置,需要重启 dev server。
devServer: {
open: true,
host: "localhost",
port: 3000,
hot: true,
compress: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true
}
}
},
2.3.1.2、axios 异步库
axios 是一个基于 Promise 的 HTTP 异步库,可以用在浏览器或 nodejs 中。
使用 axios 发起异步调用,完成 POST、GET 方法的数据提交。可参照官网的例子。
官方仓库:https://github.com/axios/axios
官方文档:https://axios-http.com/docs/intro
service/user.js 修改如下:
import axios from "axios";
export default class UserService {
login (email, password) {
console.log(email, password);
axios.post('/api/user/login', {
email:email,
password:password
})/* dev server会代理 */
.then(
function(response) {
console.log(response);
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
}
).catch(
function(error){
console.log(error);
}
)
}
}
填入邮箱、密码,点击登录按钮,返回 404,查看 Python 服务端,访问地址是 /api/user/login ,也就是多了 /api。如何解决?
-
修改 blog server 的代码的路由匹配规则?
不建议这么做,影响有点大。
-
rewrite?
类似 httpd、nginx 等的 rewrite 功能。本次测试使用的是 dev server,去官方看看。
可以查到 pathRewrite 可以完成路由重写。
devServer: { open: true, host: "localhost", port: 3000, hot: true, compress: true, historyApiFallback: true, proxy: { '/api': { target: 'http://127.0.0.1:8000', pathRewrite: {"^/api": ""}, changeOrigin: true } } },
重启 dev server。使用正确的邮箱、密码登录,返回了 json 数据,response.data 中可以看到 token、user。
2.3.1.3、token 持久化(LocalStorage)
使用 LocalStorage 来存储 token。LocalStorage 是浏览器端持久化方案之一,HTML5 标准增加的技术。LocalStorage 是为了存储得到的数据,例如 Json。
数据存储就是键值对。数据会存储在不同的域名下面。不同浏览器对单个域名下存储数据的长度支持不同,有的最多支持 2MB。
Chrome 浏览器中查看,如下:
SessionStorage 和 LocalStorage 差不多,它是会话级的,浏览器关闭,会话结束,数据清除。
IndexedDB:
-
一个域一个 datatable。
-
key-value 的检索方式
-
建立在关系型的数据模型之上,具有索引表、游标、事务等概念。
store.js:
-
store.js 是一个兼容所有浏览器的 LocalStorage 包装器,不需要借助 Cookie 或者 Flash。
-
store.js 会根据浏览器自动选择使用 localStorage、globalStorage 或者 userData 来实现本地存储功能。
安装:
$ npm i store
测试代码:
let store = require('store')
store.set('user', 'brinnatt');
console.log(1, '-->', store.get('user'));
store.remove('user');
console.log(2, '-->', store.get('user'));
console.log(2.1, '-->', store.get('user', 'a'));
store.set('user', { 'name': 'brinnatt', age: 30 });
console.log(3, '-->', store.get('user').age);
let i = 10
store.set('school', { 'name': 'www.brinnatt.com' })
store.each(function (value, key) {
console.log(++i, key, '-->', value)
});
store.clearAll();
console.log(20, '-->', store.get('user'));
安装 store 的同时,也安装了 expire 过期插件,当 kv 对存储到 LS 中的时候,顺便加入过期时长。
store.addPlugin(require('store/plugins/expire')); // 过期插件
store.set('token', res.data.token, (new Date()).getTime() + (8 * 3600 * 1000));
2.3.2、Mobx 状态管理
TodoList 项目中,感觉基本功能都实现了,但是,state 的控制有点麻烦。社区提供的状态管理库,有 Redux 和 Mobx。
-
Redux 代码优秀,使用严格的函数式编程思想,学习曲线陡峭,小项目使用的优势不明显。
-
Mobx,非常优秀稳定的库,简单方便,适合中小项目使用。使用面向对象的方式,容易学习和接受。现在在项目中使用也非常广泛。Mobx 和 React 是一对强力组合。
Mobx 官网 https://mobx.js.org/README.html
MobX 是由 Mendix、Coinbase、Facebook 开源,它实现了观察者模式。
观察者模式,也称为发布订阅模式,观察者观察某个目标,目标对象(Obserable)状态发生了变化,就会通知自己内部注册了的观察者 Observer。
状态管理需求:
一个组件的 onClick 触发事件响应函数,此函数会调用后台服务。但是后台服务比较耗时,等处理完,需要引起组件的渲染操作。
要组件渲染,需要改变组件的 props 或 state。
2.3.2.1、同步调用
同步调用中,实际上就是等着耗时的函数返回。
class Service {
handle(n) {
// 同步
console.log('pending~~~~~~~~')
for (let s = new Date(); new Date() - s < n * 1000;);
console.log('done');
return Math.random();
}
}
class Root extends React.Component {
state = { ret: null }
handleClick(event) {
// 同步返回值
let ret = this.props.service.handle(4);
this.setState({ ret: ret });
}
render() {
console.log('***********************');
return (
<div>
<button onClick={this.handleClick.bind(this)}>触发handleClick函数</button><br />
<span style={{ color: 'red' }}>{new Date().getTime()} Service的handle函数返回值是:
{this.state.ret}</span>
</div>);
}
}
这里使用一个死循环来模拟同步调用,来模拟耗时的等待返回的过程。
2.3.2.2、异步调用
思路一,使用 setTimeout,有 2 个问题。
-
无法向内部的待执行函数传入参数,比如传入 Root 实例。
-
延时执行函数的返回值无法取到,所以无法通知 Root。
思路二,Promise 异步执行。Promise 异步执行,如果成功执行,将调用回调。
class Service {
handle(obj) {
// Promise
new Promise((resolve, reject) => {
setTimeout(() => resolve('OK'), 5000);
}).then(
value => { // 使用obj
obj.setState({ ret: (Math.random() * 100) });
}
)
}
}
class Root extends React.Component {
state = { ret: null }
handleClick(event) {
// 异步不能直接使用返回值
this.props.service.handle(this);
}
render() {
console.log('***********************');
return (
<div>
<button onClick={this.handleClick.bind(this)}>触发handleClick函数</button><br />
<span style={{ color: 'red' }}>{new Date().getTime()} Service中修改state的值是:
{this.state.ret}</span>
</div>);
}
}
不管 render 中是否显示 state 的值,只要 state 改变,都会触发 render 执行。
2.3.2.3、Mobx 实现
属性,完整的对象,数组,Maps 和 Sets 都可以被转化为可观察对象。 使得对象可观察的基本方法是使用 makeObservable
为每个属性指定一个注解。 最重要的注解如下:
observable
定义一个存储 state 的可追踪字段。action
将一个方法标记为可以修改 state 的 action。computed
标记一个可以由 state 派生出新的值并且缓存其输出的 getter。
import { makeObservable, observable, action } from "mobx"
class Todo {
id = Math.random()
title = ""
finished = false
constructor(title) {
makeObservable(this, {
title: observable,
finished: observable,
toggle: action
})
this.title = title
}
toggle() {
this.finished = !this.finished
}
}
observer 装饰器:设置观察者,将 React 组件转换为响应式组件。
import React from 'react';
import ReactDom from 'react-dom';
import { observable, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
class Service {
ret = -100;
x = 1;
constructor() {
makeObservable(this, {
ret: observable
});
}
handle() {
// Promise
new Promise((resolve, reject) => {
setTimeout(() => resolve('OK'), 2000);
}).then(
value => {
this.ret = (Math.random() * 100);
console.log(this.ret);
}
);
}
}
@observer // 将react组件转换为响应式组件
class Root extends React.Component {
//state = {ret:null} // 不使用state了
handleClick(event) {
// 异步不能直接使用返回值
this.props.service.handle(this);
}
render() {
console.log('***********************');
return (
<div>
<button onClick={this.handleClick.bind(this)}>触发handleClick函数</button><br />
<span style={{ color: 'red' }}>{new Date().getTime()} Service中修改state的值是:
{/*this.props.service.ret*/} - {this.props.service.x++}</span>
</div>);
}
}
ReactDom.render(<Root service={new Service()} />, document.getElementById('app'));
MobX 还有很多用法,请参见官方文档 https://mobx.js.org/README.html
Service 中被观察者 ret 变化,导致了观察者调用了 render 函数。
被观察者变化不引起渲染的情况:将上例中的 this.props.service.ret
注释 /*this.props.service.ret*/
。可以看到,如果在 render 中不使用这个被观察者,render 函数就不会调用。
在观察者的 render 函数中,一定要使用这个被观察对象。
2.3.3、login 登录功能代码实现
./src/service/user.js
import axios from "axios";
import store from 'store';
import {observable, makeObservable} from 'mobx';
//过期插件
store.addPlugin(require('store/plugins/expire'));
export default class UserService {
loggedin = false;
constructor () {
makeObservable(this, {
loggedin: observable
});
};
login (email, password) {
console.log(email, password);
axios.post('/api/user/login', {
email:email,
password:password
})/* dev server会代理 */
.then(
response => { // 此函数要注意this的问题
console.log(response);
console.log(response.data);
console.log(response.status);
//+ 存储token,注意需要重开一次chrome的调试窗口才能看到
store.set('token',
response.data.token,
(new Date()).getTime() + (8*3600*1000));
this.loggedin = true; //+ 修改被观察者
}
).catch(
function(error){
console.log(error);
}
);
}
};
./src/component/login.js
import React from 'react';
import { Link, Navigate } from 'react-router-dom';
import '../css/login.css';
import UserService from '../service/user';
import { observer } from 'mobx-react';
const userService = new UserService();
export default class Login extends React.Component {
render() {
return <Login0 service={userService} />;
}
}
@observer
class Login0 extends React.Component {
handlerClick(event) {
event.preventDefault();
let fm = event.target.form;
this.props.service.login(
fm[0].value, fm[1].value
);
}
render() {
if (this.props.service.loggedin) {
return (
<div>
<Navigate to="/" replace={true} />
</div>
);
}
else {
return (
<div className="login-page">
<div className="form">
<form className="login-form">
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<button onClick={this.handlerClick.bind(this)}>登录</button>
<p className="message">还未注册? <Link to="/reg">请注册</Link></p>
</form>
</div>
</div>
);
}
}
}
注意,测试时,开启 Django 所写后台服务。
测试成功,成功登录,写入 Localstorage,也实现了跳转。
2.4、注册功能实现
在 ./src/service/user.js
中增加 reg 注册函数。
import axios from "axios";
import store from 'store';
import { observable, makeObservable } from 'mobx';
//过期插件
store.addPlugin(require('store/plugins/expire'));
export default class UserService {
loggedin = false;
constructor() {
makeObservable(this, {
loggedin: observable
});
};
login(email, password) {
console.log(email, password);
axios.post('/api/user/login', {
email: email,
password: password
})/* dev server会代理 */
.then(
response => { // 此函数要注意this的问题
console.log(response);
console.log(response.data);
console.log(response.status);
//+ 存储token,注意需要重开一次chrome的调试窗口才能看到
store.set('token',
response.data.token,
(new Date()).getTime() + (8 * 3600 * 1000));
this.loggedin = true; //+ 修改被观察者
}
).catch(
function (error) {
console.log(error);
}
);
};
reg(name, email, password) {
console.log(email, password);
axios.post("/api/user/reg", {
email: email,
password: password,
name: name
}) /* dev server会代理 */
.then(
response => { // 此函数要注意this的问题
console.log(response.data);
console.log(response.status);
//+ 存储token
store.set('token',
response.data.token,
(new Date()).getTime() + (8 * 3600 * 1000)
);
// 注册成功,返回token即成功登录
this.loggedin = true; //+ 修改被观察者
}
).catch(
function (error) {
console.log(error);
}
);
}
};
./src/component/reg.js
组件类:
import React from 'react';
import { Link, Navigate } from 'react-router-dom';
import '../css/login.css';
import UserService from '../service/user';
import { observer } from 'mobx-react';
const userService = new UserService();
export default class Reg extends React.Component {
render() {
return <Reg0 service={userService} />;
}
}
@observer
class Reg0 extends React.Component {
handlerClick(event) {
event.preventDefault();
let fm = event.target.form;
this.props.service.reg(fm[0].value, fm[1].value, fm[2].value);
}
render() {
if (this.props.service.loggedin) {
return (
<div>
<Navigate to="/" replace={true} />
</div>);
}
else {
return (
<div className="login-page">
<div className="form">
<form className="register-form">
<input type="text" placeholder="姓名" />
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<input type="password" placeholder="确认密码" />
<button onClick={this.handlerClick.bind(this)}>注册</button>
<p className="message">如果已经注册<Link to="/login">请登录</Link></p>
</form>
</div>
</div>
);
}
}
}
2.5、Ant Design
Ant Design 蚂蚁金服开源的 React UI 库。
官网 https://ant.design/index-cn
官方文档 https://ant.design/docs/react/introduce-cn
安装:
$ npm install antd
使用:
import React from "react";
import { createRoot } from "react-dom/client";
import { Button, DatePicker, Space, version } from "antd";
import 'antd/dist/reset.css';
import "./css/login.css";
const App = () => {
return (
<div className="login-page">
<h1>antd version: {version}</h1>
<Space>
<DatePicker />
<Button type="primary">Primary Button</Button>
</Space>
</div>
);
};
const root = createRoot(document.getElementById("app"));
root.render(<App />);
每一种组件的详细使用例子,官网都有,需要时查阅官方文档即可。
2.5.1、信息显示
网页开发中,不管操作成功与否,有很多提示信息,目前信息都是控制台输出,用户看不到。
使用 Antd 的 message 组件显示友好信息提示。在 service/user.js
中增加一个被观察对象。
import axios from "axios";
import store from 'store';
import { observable, makeObservable } from 'mobx';
//过期插件
store.addPlugin(require('store/plugins/expire'));
export default class UserService {
loggedin = false;
errMsg = '';
constructor() {
makeObservable(this, {
loggedin: observable,
errMsg: observable
});
};
login(email, password) {
console.log(email, password);
axios.post('/api/user/login', {
email: email,
password: password
})/* dev server会代理 */
.then(
response => { // 此函数要注意this的问题
console.log(response);
console.log(response.data);
console.log(response.status);
//+ 存储token,注意需要重开一次chrome的调试窗口才能看到
store.set('token',
response.data.token,
(new Date()).getTime() + (8 * 3600 * 1000));
this.loggedin = true; //+ 修改被观察者
}
).catch(
function (error) {
console.log(error);
this.errMsg = '登陆失败'; //+ 信息显示
}
);
};
reg(name, email, password) {
console.log(email, password);
axios.post("/api/user/reg", {
email: email,
password: password,
name: name
}) /* dev server会代理 */
.then(
response => { // 此函数要注意this的问题
console.log(response.data);
console.log(response.status);
//+ 存储token
store.set('token',
response.data.token,
(new Date()).getTime() + (8 * 3600 * 1000)
);
// 注册成功,返回token即成功登录
this.loggedin = true; //+ 修改被观察者
}
).catch(
function (error) {
console.log(error);
this.errMsg = '注册失败'; //+ 信息显示
}
);
}
};
./src/component/login.js
import React from 'react';
import { Link, Navigate } from 'react-router-dom';
import '../css/login.css';
import UserService from '../service/user';
import { observer } from 'mobx-react';
import { message } from 'antd';
import 'antd/lib/message/style';
const userService = new UserService();
export default class Login extends React.Component {
render() {
return <Login0 service={userService} />;
}
}
@observer
class Login0 extends React.Component {
handlerClick(event) {
event.preventDefault();
let fm = event.target.form;
this.props.service.login(
fm[0].value, fm[1].value
);
}
render() {
// 这里增加错误提示函数
if (this.props.service.errMsg) {
message.info(this.props.service.errMsg, 3,
() => setTimeout(() => this.props.service.errMsg = ''), 1000);
}
if (this.props.service.loggedin) {
return (
<div>
<Navigate to="/" replace={true} />
</div>
);
}
else {
return (
<div className="login-page">
<div className="form">
<form className="login-form">
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<button onClick={this.handlerClick.bind(this)}>登录</button>
<p className="message">还未注册? <Link to="/reg">请注册</Link></p>
</form>
</div>
</div>
);
}
}
}
./src/component/reg.js
import React from 'react';
import { Link, Navigate } from 'react-router-dom';
import '../css/login.css';
import UserService from '../service/user';
import { observer } from 'mobx-react';
import { message } from 'antd';
import 'antd/lib/message/style';
const userService = new UserService();
export default class Reg extends React.Component {
render() {
return <Reg0 service={userService} />;
}
}
@observer
class Reg0 extends React.Component {
handlerClick(event) {
event.preventDefault();
let fm = event.target.form;
this.props.service.reg(fm[0].value, fm[1].value, fm[2].value);
}
render() {
// 这里增加错误提示函数
if (this.props.service.errMsg) {
message.info(this.props.service.errMsg, 3,
() => setTimeout(() => this.props.service.errMsg = ''), 1000);
}
if (this.props.service.loggedin) {
return (
<div>
<Navigate to="/" replace={true} />
</div>);
}
else {
return (
<div className="login-page">
<div className="form">
<form className="register-form">
<input type="text" placeholder="姓名" />
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<input type="password" placeholder="确认密码" />
<button onClick={this.handlerClick.bind(this)}>注册</button>
<p className="message">如果已经注册<Link to="/login">请登录</Link></p>
</form>
</div>
</div>
);
}
}
}
以上代码,在逻辑上没有问题,但是会报错,如下:
TypeError
Cannot set properties of undefined (setting 'errMsg')
Call Stack
undefined
main.js:712:19
× Close
原因在这个函数上:
function (error) {
console.log(error);
this.errMsg = '登陆失败'; //+ 信息显示
}
在这个回调函数中,我们尝试设置 this.errMsg
属性,但是 this
在这里并不指向 UserService
类的实例。这是因为在普通函数中,this
的上下文会发生变化。
为了解决这个问题,我们可以使用箭头函数来保持正确的 this
上下文。箭头函数会继承父级作用域的 this
值。修改代码如下:
.catch((error) => {
console.log(error);
this.errMsg = '登陆失败'; //+ 信息显示
});
同样的更改也需要应用于 reg
方法中的 .catch
回调函数。
2.6、高阶组件装饰器
装饰器函数的整个演变过程如下:
import React from 'react';
// 1 组件原型
class Reg extends React.Component {
render() {
return <Reg0 service={service} />;
}
}
// 2 匿名组件
const Reg = class extends React.Component {
render() {
return <Reg0 service={service} />;
}
};
// 3 提参数
function inject(Comp) {
return class extends React.Component {
render() {
return <Comp service={service} />;
}
};
}
// 继续提参数
function inject(service, Comp) {
return class extends React.Component {
render() {
return <Comp service={service} />;
}
};
}
// 4 props
function inject(obj, Comp) {
return class extends React.Component {
render() {
return <Comp {...obj} />;
}
};
}
// 5 柯里化
function inject(obj) {
function wrapper(Comp) {
return class extends React.Component {
render() {
return <Comp {...obj} />;
}
};
}
return wrapper;
}
// 变形
function inject(obj) {
return function wrapper(Comp) {
return class extends React.Component {
render() {
return <Comp {...obj} />;
}
};
};
}
// 6 箭头函数简化
const inject = obj => {
return Comp => {
return class extends React.Component {
render() {
return <Comp {...obj} />;
}
};
};
}
// 继续简化
const inject = obj => Comp => {
return class extends React.Component {
render() {
return <Comp {...obj} />;
}
};
};
// 7 函数式组件简化
const inject = obj => Comp => {
return props => <Comp {...obj} />;
}
const inject = obj => Comp => props => <Comp {...obj} />;
const inject = obj => Comp => props => <Comp {...obj} {...props} />;
新建 src/utils.js
,放入以下内容:
import React from 'react';
const inject = obj => Comp => props => <Comp {...obj} {...props} />;
export { inject };
将登陆、注册组件装饰一下:
// login.js修改如下:
import React from 'react';
import { Link, Navigate } from 'react-router-dom';
import '../css/login.css';
import UserService from '../service/user';
import { observer } from 'mobx-react';
import { message } from 'antd';
import 'antd/lib/message/style';
import { inject } from '../utils';
const service = new UserService();
// export default class Login extends React.Component {
// render() {
// return <Login0 service={userService} />;
// }
// }
// 注意装饰器顺序
@inject({service}) // {service}会转化成{service:(new UserService())}
@observer
export default class Login extends React.Component {
handlerClick(event) {
event.preventDefault();
let fm = event.target.form;
this.props.service.login(
fm[0].value, fm[1].value
);
}
render() {
if (this.props.service.errMsg) {
message.info(this.props.service.errMsg, 3,
() => setTimeout(() => this.props.service.errMsg = ''), 1000);
}
if (this.props.service.loggedin) {
return (
<div>
<Navigate to="/" replace={true} />
</div>
);
}
else {
return (
<div className="login-page">
<div className="form">
<form className="login-form">
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<button onClick={this.handlerClick.bind(this)}>登录</button>
<p className="message">还未注册? <Link to="/reg">请注册</Link></p>
</form>
</div>
</div>
);
}
}
}
// reg.js修改如下:
import React from 'react';
import { Link, Navigate } from 'react-router-dom';
import '../css/login.css';
import UserService from '../service/user';
import { observer } from 'mobx-react';
import { message } from 'antd';
import 'antd/lib/message/style';
import { inject } from '../utils';
const service = new UserService();
// export default class Reg extends React.Component {
// render() {
// return <Reg0 service={userService} />;
// }
// }
// 注意装饰器顺序
@inject({service}) // {service}会转化成{service:(new UserService())}
@observer
export default class Reg extends React.Component {
handlerClick(event) {
event.preventDefault();
let fm = event.target.form;
this.props.service.reg(fm[0].value, fm[1].value, fm[2].value);
}
render() {
if (this.props.service.errMsg) {
message.info(this.props.service.errMsg, 3,
() => setTimeout(() => this.props.service.errMsg = ''), 1000);
}
if (this.props.service.loggedin) {
return (
<div>
<Navigate to="/" replace={true} />
</div>);
}
else {
return (
<div className="login-page">
<div className="form">
<form className="register-form">
<input type="text" placeholder="姓名" />
<input type="text" placeholder="邮箱" />
<input type="password" placeholder="密码" />
<input type="password" placeholder="确认密码" />
<button onClick={this.handlerClick.bind(this)}>注册</button>
<p className="message">如果已经注册<Link to="/login">请登录</Link></p>
</form>
</div>
</div>
);
}
}
}
Mobx 的 observer 装饰器有要求,所以装饰的顺序要注意一下。
2.7、博文功能实现
2.7.1、导航菜单
参见 Antd 官网:
$ npm install @ant-design/icons
./src/component/index.js
import * as React from "react";
import { Link, useRoutes } from "react-router-dom";
import Login from "./login";
import Reg from './reg';
import Pub from "./pub";
import { LoginOutlined, RobotOutlined, InfoOutlined } from '@ant-design/icons';
import { Menu } from 'antd';
import { Layout, Space } from 'antd';
const { Header, Footer, Content } = Layout;
const headerStyle = {
textAlign: 'center',
color: '#fff',
height: 64,
paddingInline: 50,
lineHeight: '64px',
backgroundColor: '#00888f',
};
const contentStyle = {
textAlign: 'center',
minHeight: 120,
lineHeight: '60px',
color: '#fff',
backgroundColor: '#00888f',
};
const footerStyle = {
textAlign: 'center',
color: '#fff',
backgroundColor: '#000000',
};
const items = [
{
label: (
<Link to="/">首页</Link>
),
key: 'board',
icon: <RobotOutlined />,
},
{
label: (
<Link to="/about">关于</Link>
),
key: 'about',
icon: <InfoOutlined />,
},
{
label: (
<Link to="/login">登陆</Link>
),
key: 'login',
icon: <LoginOutlined />,
},
{
label: (
<Link to="/reg">注册</Link>
),
key: 'reg',
icon: <LoginOutlined />,
},
{
label: (
<Link to="/pub">发布</Link>
),
key: 'pub',
icon: <LoginOutlined />,
},
];
const App = () => {
const routes = useRoutes([
{
path: "/",
element: (
<div>
<h1>This is Home</h1>
<Link to="/about">To About</Link>
</div>
),
},
{
path: "/about",
element: <div>This is About</div>,
},
{
path: "/login",
element: <Login />,
},
{
path: "/reg",
element: <Reg />,
},
{
path: "pub",
element: <Pub />,
},
]);
return (
<Space
direction="vertical"
style={{
width: '100%',
}}
size={[0, 48]}
>
<Layout>
<Header style={headerStyle}>
<Menu mode="horizontal" items={items} />
</Header>
<Content style={contentStyle}>
{routes}
</Content>
<Footer style={footerStyle}>
Brinnatt Schoole www.brinnatt.com.
</Footer>
</Layout>
</Space>
);
};
export default App;
布局:采用上中下布局,参考 https://ant.design/components/layout-cn/
2.7.2、博文业务
/post/pub POST 提交博文的 title、content,成功返回 Json,post_id。
/post/id GET 返回博文详情,返回 Json,post_id、title、author、author_id、postdate(时间戳)、content。
/post/ GET 返回博文列表。
2.7.2.1、业务层
创建 service/post.js 文件,新建 PostService 类。
import axios from "axios";
import {observable,makeObservable} from 'mobx';
export default class PostService {
msg = "";
constructor() {
makeObservable(this, {
msg: observable
});
};
pub(title, content) {
console.log(title);
axios.post('/api/post/pub', {
title:title,
content:content
})
.then(
response => {
console.log(response.data);
console.log(response.status);
this.msg = '博文提交成功';
}
).catch (
error => {
console.log(error);
this.msg = '登陆失败';
}
);
}
}
2.7.2.2、发布组件
使用 Form 组件,参见 https://ant.design/components/form-cn/
import React from 'react';
import { observer } from 'mobx-react';
import { message } from 'antd';
import { inject } from '../utils';
import { Form, Input, Button } from 'antd';
import FormItem from 'antd/lib/form/FormItem'; // 不在antd中单独导
import PostService from '../service/post';
import 'antd/lib/message/style';
import 'antd/lib/form/style';
import 'antd/lib/input/style';
import 'antd/lib/button/style';
const { TextArea } = Input;
const service = new PostService();
@inject({ service })
@observer
export default class Pub extends React.Component {
handleSubmit = (values) => {
console.log(values);
console.log(values.title);
console.log(values.content);
this.props.service.pub(values.title, values.content);
};
render() {
if (this.props.service.msg) {
message.info(this.props.service.msg, 3,
() => setTimeout(() => this.props.service.msg = ''), 1000);
}
return (
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
<Form layout="vertical" onFinish={this.handleSubmit}>
<FormItem label="标题" name="title" labelCol={{ span: 4 }} rules={[{ required: true, message: '请输入标题' }]}>
<Input placeholder="标题" />
</FormItem>
<FormItem label="内容" name="content" labelCol={{ span: 4 }} rules={[{ required: true, message: '请输入内容' }]}>
<TextArea rows={20} />
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit">提交</Button>
</FormItem>
</Form>
</div>
);
}
}
2.7.2.3、业务层改进
header 中的 Jwt。
由于与后台 Django Server 通信,身份认证需要 Jwt,这个要放到 request header 中。使用 axios 的 API 发送。
import axios from "axios";
import {observable,makeObservable} from 'mobx';
import store from 'store';
export default class PostService {
msg = "";
constructor() {
makeObservable(this, {
msg: observable
});
this.axios = axios.create({
baseURL:'/api/post/'
});
};
getJwt(){
return store.get('token', null);
}
pub(title, content) {
console.log(title);
this.axios.post('pub', {
title:title,
content:content
}, {headers:{'Jwt':this.getJwt()}})
.then(
response => {
console.log(response.data);
console.log(response.status);
this.msg = '博文提交成功';
}
).catch (
error => {
console.log(error);
this.msg = '登陆失败';
}
);
}
}
2.7.2.4、文章列表页组件
创建 component/list.js,创建 List 组件。在 index.js 中添加菜单项和路由。
// 部分代码
const items = [
...
{
label: (
<Link to="/pub">发布</Link>
),
key: 'pub',
icon: <LoginOutlined />,
},
{
label: (
<Link to="/list">文章列表</Link>
),
key: 'list',
icon: <LoginOutlined />,
},
];
const App = () => {
const routes = useRoutes([
...
{
path: "/list",
element: <L />,
},
]);
return (
<Space
direction="vertical"
style={{
width: '100%',
}}
size={[0, 48]}
>
<Layout>
<Header style={headerStyle}>
<Menu mode="horizontal" items={items} />
</Header>
<Content style={contentStyle}>
{routes}
</Content>
<Footer style={footerStyle}>
Brinnatt Schoole www.brinnatt.com.
</Footer>
</Layout>
</Space>
);
};
export default App;
2.7.2.4.1、查询字符串处理
用户请求的 URL 是 http://localhost:3000/list?page=2
,要被转换成 /api/post/?page=2,如何提取查询字符串?
现在前端路由有 react-router 管理,它匹配路径后,才会路由。react-router v5 版本将匹配的数据注入组件的 props 中,也可以使用解构提取 const { match, location } = this.props
。react-router v6 版本只能使用 useLocation() 钩子函数获取。
location 也是一个对象,pathname 表示路径,search 表示查询字符串。pathName:"/list", search:"?page=2"
。拿到查询字符串后,可以使用 URLSearchParams 解析它。本次将查询字符串直接拼接发往后端,有 Django 服务器端判断。
var params = new URLSearchParams(url.search);
console.log(params.get('page'), params.get('size'))
2.7.2.4.2、List 组件
component/list.js
,前面我们用的都是类组件,主要是为了利旧以前的代码,但是我们使用的 React 18.x 并不向前兼容,类组件全部废弃了,变成函数式组件和钩子函数。
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { Link, useLocation } from 'react-router-dom';
import { List } from 'antd';
import PostService from '../service/post';
import 'antd/lib/message/style';
import 'antd/lib/list/style';
const service = new PostService();
const L = observer((props) => {
const location = useLocation();
useEffect(() => {
console.log(location);
// 将查询字符串向后传
props.service.list(location.search);
}, [location, props.service]);
let data = props.service.posts;
if (data.length) {
return (
<List bordered={true} dataSource={data} renderItem={
item => (
<List.Item>
<Link to={'/post/'+item.post_id}>{item.title}</Link>
</List.Item>
)
} />
);
} else {
return <div></div>;
}
});
export default inject({ service })(L);
List 列表组件,bordered 有边线,dataSource 给定数据源,renderItem 渲染每一行,给定一个单参函数迭代每一行,List.Item 每一行的组件。
使用 Link 组件增加链接:
<List bordered={true} dataSource={data} renderItem={
item => (<List.Item>
<Link to={'/post/' + item.post_id}>{item.title}</Link>
</List.Item>)
} />
如果需要根据复杂的效果可以这样:
<List bordered={true} dataSource={data} renderItem={
item => (<List.Item>
<List.Item.Meta title={<Link to={'/post/' + item.post_id}>{item.title}</Link>} />
</List.Item>)
} />
<Link to={'/post/' + item.post_id}>{item.title}</Link> 这是详情页的链接
PostService 部分代码如下:
import axios from "axios";
import { observable, makeObservable } from 'mobx';
import store from 'store';
export default class PostService {
msg = "";
posts = []; // 博文列表
pagination = { page: 1, size: 20, pages: 0, count: 0 }; // 分页信息
constructor() {
makeObservable(this, {
msg: observable,
posts: observable,
pagination: observable
});
this.axios = axios.create({
baseURL: '/api/post/'
});
};
getJwt() {
return store.get('token', null);
}
pub(title, content) {/*省略*/}
list(search) {
this.axios.get(search)
.then(
response => {
console.log(response.data);
console.log(response.status);
this.posts = response.data.posts;
this.pagination = response.data.pagination;
}
).catch(
error => {
console.log(error);
this.msg = '文章列表加载失败'; //+ 信息显示
}
);
}
}
2.7.2.5、分页功能
分页还是需要解析查询字符串的,我们自己写一个解析函数,把这个函数放入 utils.js 中。
let url = '?id=5&page=1&size=20&id=&age-20&name=abc&name=汤姆=&测试=1'
function parse_qs(qs, re = /(\w+)=([^&]+)/) {
let obj = {};
if (qs.startsWith('?')){
qs = qs.substr(1);
}
console.log(qs);
qs.split('&').forEach(element => {
let match = re.exec(element);
//console.log(match)
if (match) obj[match[1]] = match[2];
});
return obj;
}
console.log(parse_qs(url))
分页使用了 Pagination 组件,在 L 组件的 render 函数的 List 组件中使用 pagination 属性,这个属性内放入一个 pagination 对象,有如下属性:
- current ,当前页。
- pageSize, 页面内行数。
- total,记录总数。
- onChange,页码切换时调用,回调函数为
(pageNo, pageSize) => {}
,即切换是获得当前页码和页内行数。
component/list.js 代码修改如下:
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import { inject} from '../utils';
import { Link, useLocation } from 'react-router-dom';
import { List } from 'antd';
import PostService from '../service/post';
import 'antd/lib/message/style';
import 'antd/lib/list/style';
const service = new PostService();
const L = observer((props) => {
const location = useLocation();
useEffect(() => {
console.log(location);
// 将查询字符串向后传
props.service.list(location.search);
}, [location, props.service]);
const handleChange = (pageNo, pageSize) => {
console.log(pageNo, pageSize);
// 不管以前查询字符串是什么,重新拼接 查询字符串 向后传
let search = '?page=' + pageNo + '&size=' + pageSize;
props.service.list(search);
};
let data = props.service.posts;
if (data.length) {
const pagination = props.service.pagination;
return (
<List bordered={true} dataSource={data} renderItem={
item => (
<List.Item>
<List.Item.Meta title={<Link to={'/post/' + item.post_id}>{item.title}</Link>} />
</List.Item>
)
}
pagination={{
current: pagination.page,
pageSize: pagination.size,
total: pagination.count,
onChange: handleChange
}}
/>
);
} else {
return <div></div>;
}
});
export default inject({ service })(L);
测试可以切换页面。但是鼠标放到左右两端发现上一页、下一页是英文,如何修改?国际化。
2.7.2.5.1、国际化
index.js 修改如下:
import * as React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./component/index";
import { ConfigProvider } from "antd";
import zhCN from 'antd/locale/zh_CN';
const root = createRoot(document.getElementById("app"));
root.render(
<ConfigProvider locale={zhCN}>
<BrowserRouter>
<App />
</BrowserRouter>
</ConfigProvider>
);
将 App 这个根组件包裹住就行了,再看分页组件就显示中文了。
2.7.2.5.2、浏览器地址变动
基本上没有什么问题了,但是,如果在地址栏里面输入 http://localhost:3000/list?size=2&page=2
后,再切换分页,地址栏 URL 不动,不能和当前页一致。
这个问题的解决有一定的难度。需要定义 itemRender 属性,定义一个函数,这个函数有 3 个参数。
- current,当前 pageNo。
- type,当前类型,上一页为 prev,下一页为 next,页码为 page。
- originalElement,不要动这个参数,直接返回就行了。
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import { inject, parse_qs } from '../utils';
import { Link, useLocation } from 'react-router-dom';
import { List } from 'antd';
import PostService from '../service/post';
import 'antd/lib/message/style';
import 'antd/lib/list/style';
const service = new PostService();
const L = observer((props) => {
const location = useLocation();
useEffect(() => {
console.log(location);
// 将查询字符串向后传
props.service.list(location.search);
}, [location, props.service]);
const handleChange = (pageNo, pageSize) => {
console.log(pageNo, pageSize);
// 不管以前查询字符串是什么,重新拼接 查询字符串 向后传
let search = '?page=' + pageNo + '&size=' + pageSize;
props.service.list(search);
};
const getUrl = (cur) => {
let obj = parse_qs(location.search);
let { size = 20 } = obj;
return '/list?page=' + cur + '&size=' + size;
};
const itemRender = (current, type, originalElement) => {
if (type === 'page')
return <Link to={getUrl(current)}>{current}</Link>;
return originalElement;
};
let data = props.service.posts;
if (data.length) {
const pagination = props.service.pagination;
return (
<List bordered={true} dataSource={data} renderItem={
item => (
<List.Item>
<List.Item.Meta title={<Link to={'/post/' + item.post_id}>{item.title}</Link>} />
</List.Item>
)
}
pagination={{
current: pagination.page,
pageSize: pagination.size,
total: pagination.count,
onChange: handleChange,
itemRender: itemRender
}}
/>
);
} else {
return <div></div>;
}
});
export default inject({ service })(L);
基本解决问题。但是,上一页、下一页点击不能改变浏览器地址栏。
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import { inject, parse_qs } from '../utils';
import { Link, useLocation } from 'react-router-dom';
import { List } from 'antd';
import PostService from '../service/post';
import 'antd/lib/message/style';
import 'antd/lib/list/style';
const service = new PostService();
const L = observer((props) => {
const location = useLocation();
useEffect(() => {
console.log(location);
// 将查询字符串向后传
props.service.list(location.search);
}, [location, props.service]);
const handleChange = (pageNo, pageSize) => {
console.log(pageNo, pageSize);
// 不管以前查询字符串是什么,重新拼接 查询字符串 向后传
let search = '?page=' + pageNo + '&size=' + pageSize;
props.service.list(search);
};
const getUrl = (cur) => {
let obj = parse_qs(location.search);
let { size = 20 } = obj;
return '/list?page=' + cur + '&size=' + size;
};
const itemRender = (current, type, originalElement) => {
if (current === 0) return originalElement; // 竟然返回0,只能屏蔽它
if (type === 'page')
return <Link to={getUrl(current)}>{current}</Link>;
if (type === 'next')
return <Link to={getUrl(current)} className='ant-pagination-item-link'></Link>;
if (type === 'prev')
return <Link to={getUrl(current)} className='ant-pagination-item-link'></Link>;
return originalElement;
};
let data = props.service.posts;
if (data.length) {
const pagination = props.service.pagination;
return (
<List bordered={true} dataSource={data} renderItem={
item => (
<List.Item>
<List.Item.Meta title={<Link to={'/post/' + item.post_id}>{item.title}</Link>} />
</List.Item>
)
}
pagination={{
current: pagination.page,
pageSize: pagination.size,
total: pagination.count,
onChange: handleChange,
itemRender: itemRender
}}
/>
);
} else {
return <div></div>;
}
});
export default inject({ service })(L);
至此,分页问题解决。
2.7.2.6、详情页组件
index.jsp
import Detail from "./post";
const App = () => {
const routes = useRoutes([
{
path: "/",
element: (
<div>
<h1>This is Home</h1>
<Link to="/about">To About</Link>
</div>
),
},
// 省略
{
path: "/post/:id",
element: <Detail />,
},
]);
return (
<Space
direction="vertical"
style={{
width: '100%',
}}
size={[0, 48]}
>
<Layout>
<Header style={headerStyle}>
<Menu mode="horizontal" items={items} />
</Header>
<Content style={contentStyle}>
{routes}
</Content>
<Footer style={footerStyle}>
Brinnatt Schoole www.brinnatt.com.
</Footer>
</Layout>
</Space>
);
};
service/post.js
增加 getPost 服务代码:
import axios from "axios";
import { observable, makeObservable } from 'mobx';
import store from 'store';
export default class PostService {
msg = "";
posts = []; // 博文列表
pagination = { page: 1, size: 20, pages: 0, count: 0 }; // 分页信息
post = {}; // 添加博客详情监控属性
constructor() {
makeObservable(this, {
msg: observable,
posts: observable,
pagination: observable,
post: observable
});
this.axios = axios.create({
baseURL: '/api/post/'
});
};
getJwt() {
return store.get('token', null);
}
pub(title, content) {
console.log(title);
this.axios.post('pub', {
title: title,
content: content
}, { headers: { 'Jwt': this.getJwt() } })
.then(
response => {
console.log(response.data);
console.log(response.status);
this.msg = '博文提交成功';
}
).catch(
error => {
console.log(error);
this.msg = '登陆失败';
}
);
}
list(search) {
this.axios.get(search)
.then(
response => {
console.log(response.data);
console.log(response.status);
this.posts = response.data.posts;
this.pagination = response.data.pagination;
}
).catch(
error => {
console.log(error);
this.msg = '文章列表加载失败'; //+ 信息显示
}
);
}
getPost(id) { // 获取博客详情
this.axios.get(id).then(
response => {
console.log(response.data);
console.log(response.status);
this.post = response.data.post;
}
).catch(
error => {
console.log(error);
this.msg = "详情页加载失败";
}
);
}
}
新建 component/post.js
,创建 Detail 组件。使用 antd Card 布局。
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import { inject } from '../utils';
import { useParams } from 'react-router-dom';
import { Card, message } from 'antd';
import PostService from '../service/post';
import 'antd/lib/message/style';
import 'antd/lib/list/style';
const service = new PostService();
const Detail = observer((props) => {
const params = useParams();
useEffect(() => {
console.log(params);
let { id = -1 } = params; // {id:'3'}
props.service.getPost(id);
}, [params, props.service]);
if (props.service.msg) {
message.info(
props.service.msg,
3,
() => setTimeout(
() => props.service.msg = '', 1000
)
);
}
let post = props.service.post;
if (post.title) {
return <Card title={post.title} bordered={true} style={{ width: 800, margin: 'auto'}}>
<p>{post.author} {new Date(post.postdate * 1000).toLocaleDateString()}</p>
<p>{post.content}</p>
</Card>;
} else {
return (<div></div>);
}
});
export default inject({ service })(Detail);
至此,前后端分离的博客系统基本框架搭好了,去看看页面的成果。
2.8、Django 部署
2.8.1、Django 打包
构建 setup.py 文件:
from distutils.core import setup
import glob
setup(name='blog',
version='1.0',
description='This is a demo for web dev',
author='Brinnatt',
author_email='brinnatt@gmail.com',
url='https://www.brinnatt.com',
packages=['blog', 'post', 'user'],
py_modules=['manage'], # 可以不打包manage.py
data_files=glob.glob('templates/*.html') + ['requirements']
)
# 应用程序的根目录下打包
$ pip freeze > requirements
$ python setup.py sdist --formats=gztar # gz
在 Linux 系统中创建一个 python 虚拟环境目录。
[python@brinnatt ~]$ pyenv virtualenv 3.9.10 blog3_9_10
[python@brinnatt ~]$ pyenv local blog3_9_10
(blog3_9_10) [python@brinnatt ~]$
(blog3_9_10) [python@brinnatt ~]$ pip list
Package Version
---------- -------
pip 21.2.4
setuptools 58.1.0
(blog3_9_10) [python@brinnatt ~]$ tar xf blog-1.0.tar.gz
(blog3_9_10) [python@brinnatt ~]$ cd blog-1.0/
# yum install python-devel mysql-devel # CentOS需要root权限安装,mysqlclient依赖
(blog3_9_10) [python@brinnatt blog-1.0]$ pip install -r requirements
(blog3_9_10) [python@brinnatt blog-1.0]$ pip list
Package Version
------------ -------
asgiref 3.6.0
bcrypt 4.0.1
cffi 1.15.1
cryptography 40.0.2
Django 4.0
mysqlclient 2.1.1
pip 21.2.4
pycparser 2.21
PyJWT 2.7.0
setuptools 58.1.0
simplejson 3.19.1
sqlparse 0.4.4
tzdata 2023.3
(blog3_9_10) [python@brinnatt blog-1.0]$ sed -i -e 's/DEBUG.*/DEBUG = False/' -e 's/ALLOWED_HOSTS.*/ALLOWED_HOSTS = ["*"]/' blog/settings.py # 修改Django配置
(blog3_9_10) [python@brinnatt blog-1.0]$ python manage.py runserver 0.0.0.0:8000 # 启动服务
使用 http://192.168.136.128:8000/post/?page=2&size=2
成功返回数据,说明Django应用成功。
至此,Django 应用部署完成。Django 带了个开发服务器 Web Server,生产环境不要用,需要借助其它 Server。
注意:ALLOWED_HOSTS = ["*"]
这是所有客户都可以访问,生产环境应指定具体可以访问的 IP。
2.8.2、WSGI
Web Server Gateway Interface,是 Python 中定义的 Web Server 与应用程序的接口定义。
应用程序有 WSGI 的 Django 框架负责,WSGI Server 谁来做?
2.8.3、uWSGI 项目
uWSGI 是一个 C 语言的项目,提供一个 WEB 服务器,它支持 WSGI 协议,可以和 Python 的 WSGI 应用程序通信。
官方文档 https://uwsgi-docs.readthedocs.io/en/latest/
uWSGI 可以直接启动 HTTP 服务,接收 HTTP 请求,并调用 Django 应用。
安装:
(blog3_9_10) [python@brinnatt blog-1.0]$ pip install uwsgi
(blog3_9_10) [python@brinnatt blog-1.0]$ uwsgi --help
2.8.4、uWSGI + Django 部署
在 Django 项目根目录下,运行 $ uwsgi --http :8000 --wsgi-file blog/wsgi.py --stats :8001
,使用下面的链接测试:
http://192.168.136.128:8000/post/?page=1&size=2
运行正常。
2.9、React 部署
2.9.1、React 项目打包
$ npm run build
$ npm run build
编译成功。查看项目目录中的 dist 目录。
2.9.2、nginx 部署
安装 nginx:
[root@brinnatt ~]# yum install nginx -y
[root@brinnatt ~]# rm -rf /usr/share/nginx/html/*
[root@brinnatt ~]# systemctl start nginx
[root@brinnatt ~]# systemctl enable nginx
将前端发布的 dist 目录下的所有文件都拷贝至 /usr/share/nginx/html 目录下。刷新首页,一切从此开始。
2.9.3、uwsgi 部署
将 nginx 和 uWSGI 之间的通信配置成 uwsgi 协议通信。
本次 pyenv 的虚拟目录是 /home/python,其下放 uWSGI 的配置文件 blog.ini。
(blog3_9_10) [python@brinnatt ~]$ pwd
/home/python
(blog3_9_10) [python@brinnatt ~]$ vim blog.ini
(blog3_9_10) [python@brinnatt ~]$ cat blog.ini
[uwsgi]
socket = 127.0.0.1:9000
chdir = /home/python/blog-1.0
wsgi-file = blog/wsgi.py
(blog3_9_10) [python@brinnatt ~]$ uwsgi blog.ini
socket = 127.0.0.1:9000
使用 uwsgi 协议通信。
chdir = /home/python/blog-1.0
Django 项目根目录。
wsgi-file = blog/wsgi.py
指定 App 文件,blog 下 wsgi.py。
在 nginx 中配置 uwsgi:
[root@brinnatt ~]# vim /etc/nginx/conf.d/WsgiDjango.conf
[root@brinnatt ~]# cat /etc/nginx/conf.d/WsgiDjango.conf
server {
listen 80;
server_name localhost;
location ^~ /api/ {
rewrite ^/api(/.*) $1 break;
#proxy_pass http://127.0.0.1:8000
include uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
}
}
[root@brinnatt ~]# systemctl restart nginx.service
重新装载 nginx 配置文件,成功运行。至此,前后端分离的开发、动静分离部署的博客项目大功告成。
参看 https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html
uwsgi 协议 https://uwsgi-docs.readthedocs.io/en/latest/Protocol.html
2.9.4、MVC 设计
MVC 设计模式:
Controller 控制器:负责接收用户请求,调用 Model 完成数据处理,调用 view 完成对用户的响应。
Model 模型:负责业务数据的处理。
View 视图:负责用户的交互界面。