0%

全栈开发入门 JavaScript、React 和 Django

关于 Python 系列其他文章的传送门

  1. 《 Python 基础语法 》

  2. 《 Python 进阶:网络、多线程、异步、数据库、WEB 》

  3. 《 Python 和 RabbitMQ 的那点事 》

  4. 《 Python 和 Redis 的那点事 》

  5. 《 Python 爬虫基础和包 XPath、BeautifulSoup4、PhantomJS、Selenium 》

  6. 《 边爬豆瓣,边学习 Python 爬虫框架 Scrapy 》

  7. 《 用了 Scrapy-Redis 分布式爬虫后,一个能打的都没有! 》

  8. 《 全栈开发入门 JavaScript、React 和 Django 》      您当前所在位置

  9. 《 Django REST framework 的那点事 》



logo

一、JavaScript

this 的坑

1
2
3
4
5
6
7
8
9
10
11
12
13
var school = {
name: 'monster',
getNamefunc: function (){
console.log(1, this.name)
return function (x, y){ // func 就等于这个函数
console.log(2, this === global);
console.log(2.5, school === this);
return this.name;
};
}
}
func = school.getNamefunc()
console.log(func())

上面这样调用的执行结果中,2 和 2.5 中的 this 不是期望的 school,而是 global 中的 this

原因
getNamefuncschool 的一个属性,调用时会隐含的传入一个 this,所以 1 的 this 肯定是 school。而 func 这样第二级调用,得到的只是一个普通的函数,根本没有传入 thisthis 自然也就不会是 school 了。

解决办法

1、显式传入:使用 callapplybind 传入指定的 this 来解决这个问题:

1
2
func.call(school, x, y)
func.apply(school, [x, y]) //和 call 的区别是:apply 需要把参数放在一个数组中,稍微麻烦了了一些

2、ES5 引入了 bind 方法

1
func.bind(school) // bind 函数返回的是一个新的函数,类似 python 中的 partial 函数

3、使用 ES6 引入的箭头函数,就不会有这种问题了

Babel

js 转译工具

注意:以下仅适用于 babel 6 版本,并不适用于最新版本

开发中可以使用较新的ES6语法,通过转译器转换为指定的某些版本代码。

官网 http://babeljs.io/

1. Node.js 项目初始化

1
2
3
npm init

# 之后在项目根目录下会生成 package.json 文件

2. 设置国内镜像加速

编辑 .npmrc 文件:

1
echo "registry=https://registry.npm.taobao.org" > .npmrc

可以放到 npm 的目录下 npmrc 文件中
可以放到用户家目录中
可以放到项目根目录中

3. 安装

项目根目录下执行

1
npm install babel-core babel-cli --save-dev

--save--save-dev 可以省掉你手动修改 package.json 文件的步骤。

  • --save 自动把模块和版本号添加到 dependencies 部分
  • --save-dev 自动把模块和版本号添加到 devdependencies 部分

4. 修改 package.json

替换 scripts 的部分为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "js",
"version": "1.0.0",
"description": "",
"main": "test.js",
"scripts": {
"build": "babel src -d lib"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0"
}
}

babel src -d lib 意思是从 src 目录中转译后的文件输出到 lib 目录。

5. 配置 babel 和安装依赖

在目录根目录下创建 .babelrc 文件,Json 格式。

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
"react",
"env",
"stage-0"
],
"plugins": [
"transform-decorators-legacy",
"transform-runtime"
]
}

env 的意思是可以根据当前环境自动选择。

安装依赖

1
npm install babel-preset-env --save-dev

6. 转换

在项目根目录下执行命令:

1
npm run build

会把 src 中的 js 文件进行转换,到 lib 目录。

Antd 样式自动加载

使用 babel-plugin-import 项目可以实现 Antd 组件样式自动加载
项目 Github:https://github.com/ant-design/babel-plugin-import

安装:

1
npm install babel-plugin-import -D

ant-design 引入样式的几种方式:

1、全局引用:

  • 引用的方式很容易,不过这实际上加载了全部的 antd 组件的样式文件,对前端性能是个隐患。
1
2
3
4
5
6
7
import React, { Component } from 'react';
import Button from 'antd/lib/button';
import './App.css';

// 在 App.css 文件头部引入 antd/dist/antd.css
// App.css
@import '~antd/dist/antd.css';

2、单个组件分别引入对应的样式文件:

  • 这样也算是”按需加载“,不过比较麻烦,每次写一个新组件,都要先载入 antd 组件,再载入对应 css 文件,代码冗余(而且写着也麻烦,你多引几个组件就要多写几行代码,程序员嘛都是要偷下懒的)
1
2
3
// 比如想要引入 Button 组件和其对应的 css 样式
import { Button } from 'antd'
import 'antd/lib/button/style'

3、使用 babel-plugin-import 实现按需加载:
.babelrc 配置文件中,添加以下配置代码:

1
2
3
4
5
6
7
8
9
10
11
  "plugins": [
["import", {
"libraryName": "antd",
"libraryDirectory": "lib", // libraryDirectory 默认为 lib
"style": "css" ,
}],
],
}

// "style": css 引入的是 css 文件
// "style": true 引入的是 less 文件

需要在 webpack.config.jsexclude 中,把 node_modules 去掉:

1
2
3
4
5
6
{
test: /\.css$/, // 或 .less$ 看你使用了那个

exclude: /node_module/, // 会忽略 node_module/antd,会报错,应删去
use: ['style-loader', 'css-loader'],
},

导入样式的方式就变成了:

1
2
// 会自动转换为 antd/lib/button,配合 style 属性可以做到模块样式按需自动加载,避免了代码冗余
import { Button } from 'antd'

阻止表单自动提交

要阻止页面刷新,其实就是阻止提交,使用 event.preventDefault()

阻止后,可由自己控制发起异步提交等操作。

1
event.preventDefault();

异步模块 Promise

Promise 对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。

简单说,就是处理异步请求的。

之所以叫做 Promise,就是我承诺,如果成功则怎么处理,失败则怎么处理。

1
2
3
4
5
6
7
8
// 语法 
new Promise(
/* executor */
function (resolve, reject) {
pass
}
);

关于 executor

  • executor 是一个带有 resolve 和 reject 两个参数的函数;
  • executor 函数在 Promise 的构造函数执行时,同步执行,并被传递 resolvereject 函数(executor 函数在 Promise 的构造函数返回新建对象前被调用);
  • executor 内部通常会执行一些异步操作,一旦完成,可以调用 resolve 函数来将 promise 状态改成 fulfilled 即完成,或者在发生错误时调用 reject 将它的状态改为 rejected 即失败;
  • 如果在 executor 函数中抛出一个错误,那么该 promise 状态为 rejected,executor 函数的返回值被忽略;
  • 在 executor 中,resolvereject 只能执行其中一个函数。

Promise 的状态

  • pending: 初始状态,不是成功或失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

Promise.then(onFulfilled, onRejected)

  • 参数是 2 个函数,根据 Promise 的状态来调用不同的函数:
    • fulfilled 状态走 onFulfilled 函数;
    • rejected 状态走 onRejected 函数。
  • then 的返回值是一个新的 promise 对象;
  • 调用任何一个参数后,其返回值会被新的 promise 对象来 resolve,向后传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 简单使用 
var myPromise = new Promise(
/* executor */
(resolve, reject) => {
resolve('hello'); // 执行,置状态为fulfilled
console.log('~~~~~~~~~~~~~~~~');
reject('world'); // 永远执行不到
}
);

console.log(myPromise);

myPromise.then(
/*如果成功则显示结果*/
(value) => console.log(1, myPromise, value),
/*如果失败则显示原因*/
(reason) => console.log(2, myPromise, reason)
);

axios 异步库

axios 是一个基于 Promise 的 HTTP 异步库,可以用在浏览器或 nodejs 中。

使用 axios 发起异步调用,完成 POST、GET 方法的数据提交。可参照官网的例子

中文说明 https://www.kancloud.cn/yunye/axios/234845

安装 npm:

1
npm install axios

导入:

1
import axios from 'axios';

GET:

1
2
3
4
5
6
7
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

POST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
axios.post(
'/api/user/login',
{
'email': email,
'passwd': passwd
}
).then( // get 成功后的调用
response => {
store.set('user', response.data.user);
store.set('token', response.data.token);
obj.setState({ 'ret': 200 });
}
).catch( // get 错误后的调用
(error) => { console.log('XXXXX', error) }
);

实例化(可以加 header 信息)

如要定义 header 信息,需要使用实例化的方式。

可以使用自定义配置新建一个 axios 实例,语法:

1
axios.create([config]) // 这里的 [] 指语法中的可选意思,不是 js 中的 []

示例:

1
2
3
4
5
var instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});

可以通过设置一个 baseURL,便于为 axios 实例的方法传递 URL 的相对路径。

它会自动在 baseURL 的后面加上 url,除非 url 自己就是一个绝对 URL。例如:

1
2
3
4
5
6
7
8
9
var instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
});

instance.get(
url: 'getall' // 等于 https://some-domain.com/api/getall
headers: {'X-Custom-Header': 'foobar'} // 头信息也可以放在这里
)

实例可用方法:

  • axios#request(config)
  • axios#get(url[, config])
  • axios#delete(url[, config])
  • axios#head(url[, config])
  • axios#post(url[, data[, config]])
  • axios#put(url[, data[, config]])
  • axios#patch(url[, data[, config]])

token 持久化 —— LocalStorage

推荐使用 LocalStorage 来存储 token。
LocalStorage 是浏览器端持久化方案之一,HTML5 标准增加的技术。
LocalStorage 是为了存储得到的数据,例如 Json。
数据存储就是键值对。
数据会存储在不同的域名下面。
不同浏览器对单个域名下存储数据的长度支持不同,有的最多支持 2MB。

SessionStorage 和 LocalStorage 差不多,不过 SessionStorage 是会话级的,浏览器关闭,会话结束,数据清除。

store.js

store.js 是一个兼容所有浏览器的 LocalStorage 包装器,不需要借助 Cookie 或者 Flash。

store.js 会根据浏览器自动选择使用 localStorage、globalStorage 或者 userData 来实现本地存储功能。

安装

1
npm i store

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import store from 'store';
// 兼容写法:
// let store = require('store');

// set & get
store.set('user', 'monster');
store.get('user');
store.get('user', 'a'); // 如 get 失败则返回 a

// 使用 attr 访问
store.set('user', { name: 'monster', age: 20 });
store.get('user').age

// remove
store.remove('user');

// 迭代器
store.set('a', 1);
store.set('b', 2);
store.each(
function (value, key) { // 注意这里 key、value 是反的
console.log(key, '-->', value)
}
);

// clear
store.clearAll();

安装 store 的同时,也安装了 expire 过期插件,可以在把 kv 对存储到 LS 中的时候顺便加入过期时长。

1
2
3
4
import store from 'store';

store.addPlugin(require('store/plugins/expire')); // 加载过期插件
store.set('token', res.data.token, (new Date()).getTime() + (8*3600*1000));

Mobx 状态管理

state 的控制起来有一下繁琐。可以使用社区提供的状态管理库,有 Redux 和 Mobx。

Redux 和 Mobx 对比:

  • Redux 代码优秀,使用严格的函数式编程思想,学习曲线陡峭,小项目使用的优势不明显;
  • Mobx,非常优秀稳定的库,简单方便,适合中小项目使用。使用面向对象的方式,容易学习和接受。现在在项目中使用也非常广泛。

当 Mobx 和 React 一起,是一对强力组合。

Mobx 官网 https://mobx.js.org/
中文网 http://cn.mobx.js.org/

MobX 是由 Mendix、Coinbase、Facebook 开源,它实现了 观察者模式

观察者模式:也称为发布订阅模式
观察者观察某个目标,如目标对象 Obserable
当状态发生了变化,就会通知已注册的观察者 Observer

状态管理的 3 中实现

当一个组件的 onClick 触发事件响应函数,此函数会调用后台服务。
但是后台服务可能比较耗时,等后台处理完,需要引起组件的渲染操作。
要组件重新渲染,需要改变组件的 propsstate

方案 1:同步调用

同步调用中,实际上就是等着耗时的函数返回,属于等待状态,页面看起来就行卡死一样,强烈不建议使用。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Service {
long_time() { // 耗时操作
return 这里进行一些很耗时的操作
}
}

class Root extends React.Component {

handleClick() {
let ret = new Service
ret.long_time(); // 阻塞同步等待返回值
}

render() {
return (
<div>
<button onClick={this.handleClick.bind(this)}>触发 handleClick 函数</button>
</div>
);
}
}

方案 2:使用 Promise 异步执行

使用 Promise 异步执行,如果成功执行,将调用回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react'; import ReactDom from 'react-dom';

class Service {

handle(obj) {
// Promise
new Promise(
(resolve, reject) => {
// 一些非常耗时的操作
setTimeout(() => resolve('OK'), 5000);
}
).then(value => {
obj.setState({ ret: true }); // 如果成功执行,则更改 state 值,引发 dom 重绘使用
}
)
}
}

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 />
</div>
);
}
}

ReactDom.render(<Root service={new Service()} />, document.getElementById('root'));

只要 state 改变,都会触发 render 重绘。

方案 3:使用 Mobx 实现(推荐)

  • observable 装饰器:设置 被观察者
  • observer 装饰器:设置 观察者,将 React 组件转换为响应式组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from 'react';
import ReactDom from 'react-dom';
import {observable} from 'mobx';
import {observer} from 'mobx-react';

// 这是 Service 层的代码,一般会在单独的文件里,这里方便演示和 view 层代码放在了一起
class Service {

@observable ret = -100; // 定义被观察者

handle(obj) {
// Promise
new Promise(
(resolve, reject) => {
// 一些非常耗时的操作
setTimeout(() => resolve('OK'), 5000);
}
).then(value => {
this.ret = 1; // 如果成功执行,则更改被观察的值
}
)
}
}

@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>Service 中修改 state 的值是: {this.props.service.ret}</span>
{/* 如观察者 render 中不使用这个被观察者 ret,则 render 函数就不会调用 */}
</div>
);
}
}

ReactDom.render(<Root service={new Service()} />, document.getElementById('root'));

当 Service 中的 被观察者 ret 发生变化,会触发 观察者 调用 render 函数。

注意:如果在 render 中不使用这个被观察者 ret,render 函数就不会调用。

二、React

React 是 Facebook 开发并开源的前端框架。

当时他们的团队在市面上没有找到合适的 MVC 框架,就自己写了一个 Js 框架,用来假设大名鼎鼎的 Instagram (图片分享社交网络),在 2013 年将 React 开源。

React 解决的是前端 MVC 框架中的 View 视图层的问题。

webpack

这是一个符合 Commonjs,用来打包的模块,详细配置看 PDF。
React项目构建.pdf

url 重定向
类似 httpd、nginx 等的 rewrite 功能。
官方可以查到:https://webpack.js.org/configuration/dev-server/#devserver-proxy
使用 pathRewrite 可以完成路由重写,在配置文件中 webpack.config.dev.jsproxy 部分:

1
2
3
4
5
6
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
pathRewrite: { '\api': '' } // 重定向规则
}

组件状态 state

每一个 React 组件都有一个状态变量 state,它是一个 JavaScript 对象,可以为它定义属性来保存值。

如果状态变化了,会触发 UI 的重新渲染。使用 setState() 方法可以修改 state 值。不可以对还在更新中的 state 使用 setState()
注意:state 是组件自己内部使用的,是组件私有的属性。

属性 props

把 React 组件当做标签使用,可以为其增加属性,如下:

1
<Toggle name="school" parent={this} />

name = "school":这个属性会作为一个单一的对象传递给组件,加入到组件的 props 属性中。

构造器 constructor

使用 ES6 的构造器,要提供一个参数 props ,并把这个参数使用 super 传递给父类:

1
2
3
4
5
6
7
8
9
import React from 'react';
import ReactDom from 'react-dom';

class Toggle extends React.Component {
constructor(props) {
super(props); // 一定要调用super父类构造器,否则报错
this.state = { flag: true }; // 类中定义state
}
}

组件的生命周期

组件的生命周期可分成三个状态:

  • Mounting:已插入真实 DOM;
  • Updating:正在被重新渲染;
  • Unmounting:已移出真实 DOM。

react-state

无状态组件(函数式组件)

React 从 15.0 开始支持无状态组件,定义如下:

1
2
3
4
5
6
7
8
import React from 'react';
import ReactDom from 'react-dom';

function Root(props) {
return <div>{props.schoolName}</div>;
}

ReactDom.render(<Root schoolName="monster" />, document.getElementById('root'));

无状态组件,也叫 函数式组件
开发中,很多情况下,组件其实很简单,不需要 state 状态,也不需要使用生命周期函数。无状态组件很好的满足了需要。
无状态组件函数应该提供一个参数 props,返回一个 React 元素。
无状态组件函数本身就是 render 函数。

高阶组件 – 装饰器

新版 ES 2016 中增加了装饰器的支持,因此可以使用装饰器来改造上面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDom from 'react-dom';

let Wrapper = Component => props =>
(<div>
{props.schoolName} <hr />
<Component />
</div>);

@Wrapper
class Root extends React.Component {
render() {
return <div>Root</div>;
}
}

ReactDom.render(<Root schoolName="monster" />, document.getElementById('root'));

高阶组件 – 带参装饰器

想给装饰器函数增加一个 id 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDom from 'react-dom';

// 带参装饰器函数
let Wrapper = id => Component => props =>
(<div id={id}> {props.schoolName}
<hr />
<Component {...props} />
</div>);

@Wrapper('wrapper') // 带参
class Root extends React.Component {
render() {
return <div>{this.props.schoolName}</div>;
}
}

ReactDom.render(<Root schoolName="monster" />, document.getElementById('root'));

特别注意

搬到 React 组件中的时候,要将 class 属性改为 className,如:

1
2
3
4
5
// 在 js 中
<div class="test">

// 在 react 中
<div className="test">

所有标签,需要闭合。

React-Route 前端路由

官网文档 https://reactrouter.com/web/guides/philosophy
基本例子 https://reactrouter.com/web/example/basic

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React from 'react';
import ReactDom from 'react-dom';
import { Route, Link, BrowserRouter as Router } from 'react-router-dom';

// 无状态组件(函数组件)
const Home = () => (
<div>
<h2>Home</h2>
</div>
);

// 有状态组件
class About extends React.Component {
constructor(props) { // 现在前端路由有 react-router 管理,它匹配路径后,会注入组件的 props 中
super(props)
let { match, location } = this.props // 对象解构:从 props 中解构出 location 和 match 等其他信息
}

render() {
return (
<div>
<h2>Home</h2>
</div>
);
}
}

const App = () => (
<Router>
<div>
<Route exact path="/" component={Home} />

// 匹配的 query 信息会自动注入到组件的 props 中
<Route path="/about" component={About} />

// id 位置的匹配会自动传入 Getpage 组件,之后在 Getpage 中可以通过 props.match.params.id 提取;
<Route path="/getpage/:id" component={Getpage} />
</div>
</Router>
);

ReactDom.render(<App />, document.getElementById('root'));
  • Route 负责静态路由;
  • path 是匹配路径,如没有 path 则总是匹配;
  • component 是目标组件;
  • exact:布尔值,true 时要求路径 完全匹配
  • strict:布尔值,true 时,要求 严格匹配,但是 url 字符串可以是自己的子串。

注入参数举例:

  • <Route path="/about" component={About} />
    • 匹配的 query 信息会自动注入到组件的 props 中,之后在 About 中可以通过 props.location.search 提取 search;
  • <Route path="/getpage/:id" component={Getpage} />
    • id 位置的匹配会自动传入 Getpage 组件,之后在 Getpage 中可以通过 props.match.params.id 提取;

Redirect 重定向

使用 <Redirect> 可以设置重定向到其他 route 而不改变旧的 URL

  • from:你想由哪个路径进行重定向,包括动态段。
  • to:你想重定向的路径。
  • query:默认情况下,query 的参数只会经过,但如果你需要你可以指定它们。
1
2
3
4
/* /course/123/home -> /course/123/dashboard */}
function () {
return <Redirect from="home" to="dashboard" />
}
1
2
3
4
5
6
7
// 我们需要从 `/profile/123` 改变到 `/about/123`
// 并且由 `/get-in-touch` 重定向到 `/contact`
<Route component={App}>
<Route path="about/:userId" component={UserProfile}/>
{/* /profile/123 -> /about/123 */}
<Redirect from="profile/:userId" to="about/:userId" />
</Route>

使用 react-router 时刷新页面 404 问题解决方法

参考:https://www.jb51.net/article/131789.htm

使用 React 开发新项目时,遇见了刷新页面,直接访问二级或三级路由时,访问失败,出现 404 或资源加载异常的情况,本篇针对此问题进行分析并总结解决方案。

问题复现

举例:访问 localhost:9090 是可以正常加载页面和 js 等文件的,但是当我们需要访问二级甚至三级路由或者刷新页面时,如 localhost:9090/posts/92 时,可能会出现两种情况:

  • 页面加载失败,返回 Cannot Get(404)
  • 服务响应,但是没有返回 webpack 处理输出的 html 文件,导致无法加载 js 资源

分析问题

判断这个问题一般是两方面原因造成:

  • react-router 前端路由配置;
  • webpack-dev-server 服务配置;

解决办法

去查询 react-router 路由库相关配置信息,发现文档中提到了使用 browserHistory 时,会创建真实的 URL,处理初始 / 请求没有问题,但是对于跳转路由后,刷新页面或者直接访问该 URL 时,会发现无法正确相应,更多信息查看参考文档,文档中也提供了几种服务器配置解决方式:

Nginx
如果使用的是 nginx 服务器,则只需要使用 try_files 指令:

1
2
3
4
5
6
server {
...
location / {
try_files $uri /index.html;
}
}

Apache
如果使用 Apache 服务器,则需要在项目根目录创建 .htaccess 文件,文件包含如下内容:

1
2
3
4
5
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

以上就是针对服务器的配置解决。

博客项目中,高阶装饰器的转换过程

装饰器函数的整个演变过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import React from 'react';

// 1 组件原型
class Reg extends React.Component {
render() {
return <_Reg service={service} />;
}
}

// 2 匿名组件
const Reg = class extends React.Component {
render() {
return <_Reg 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} />;

简要概括版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React from 'react';


const service = new UserService()

// 原版
export default class inject extends React.Component {
render() {
return (
<Login service={service} />
)
}
}

// v1,提取出 obj (也就是 service) 和 Comm
const inject = obj => Comm => {
class extends React.Component {
render() {
return <Comm {...obj} />
}
}
}

// v2,提取出 props,变形为函数组件(无状态组件,无状态组件函数本身就是 render 函数。)
// function Root(props) {
// return <div>{props.schoolName}</div>;
// }
const inject = obj => Comm => props => <Comm {...obj} {...props} />;


@inject({ service }) // login = inject({ service:service })(Login)
@observer // 注意装饰器顺序,观察者装饰器要求靠近源码
export default class Login extends React.Component {
render() {
return (
<div className="login-page">
{this.props.service}
</div>
);
}
}

三、Django

概述

Django 采用 MVC 架构设计的开源的 WEB 快速开发框架。

优点:

  • 能够快速开发,如 Auth, Cache, 模板
  • MVC 设计模式
  • 实用的管理后台
  • 自带 ORM, Template, Form, Auth 核心
  • 组件
  • 简洁的 url 设计
  • 周边插件丰富

缺点:

  • 重,因为东西大而全
  • 同步阻塞

所以 Django 的设计目标就是一款大而全,便于企业快速开发项目的框架,因此企业应用较广。

安装、配置、初始化

安装 Django

1
pip install django

创建 django 项目

1
django-admin startproject blog .

重要文件说明

  • manage.py:本项目管理的命令行工具。应用创建、数据库迁移等,都使用它完成;
  • blog/settings.py:本项目的配置文件。数据库参数等;
  • blog/urls.py:URL 路径映射配置。默认情况下,只配置 /admin 的路由;
  • blog/wsgi:定义 WSGI 接口信息。一般无须改动。

数据库配置

使用数据库,需要修改默认的数据库配置。
在主项目的 settings.py 下的 DATABASES
默认使用的 sqlite,修改为 mysql。

1
2
3
4
5
6
7
8
9
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'blog',
'USER': 'monster',
'PASSWORD': 'monster',
'HOST': '192.168.x.x',
'PORT': '3306', }
}

Django 支持 MySQL 5.5+
Django 官方推荐使用本地驱动 mysqlclient:

1
pip install mysqlclient

创建应用

创建用户应用,这里以 user 这个名字为例,

1
python manage.py startapp user

创建应用后,项目根目录下产生一个以 user 为名的目录,有如下文件:

  • admin.py:管理站点模型的声明文件;
  • models.py:模型层 Model 类定义;
  • views.py:定义 URL 响应函数;
  • migrations:包数据迁移文件生成目录;
  • apps.py:应用的信息定义文件。

注册应用

settings.py 中,增加 user 应用。目的是为了后台管理 admin 使用,或迁移 migrate 使用。

1
2
3
4
5
6
7
8
9
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user', # 增加
]

模型 Model

字段类 说明
AutoField 自增的整数字段。 如果不指定,django 会为模型类自动增加主键字段。
BooleanField 布尔值字段,TrueFalse
对应表单控件 CheckboxInput。
NullBooleanField 比 BooleanField 多一个 null 值。
CharField 字符串,max_length 设定字符长度。
对应表单控件 TextInput。
TextField 大文本字段,一般超过 4000 个字符使用。
对应表单控件 Textarea。
IntegerField 整数字段。
BigIntegerField 更大整数字段,8 字节。
DecimalField 使用 Python 的 Decimal 实例表示十进制浮点数。
max_digits 总位数,decimal_places 小数点后的位数。
FloatField Python 的 Float 实例表示的浮点数
DateField 使用 Python 的 datetime.date 实例表示的日期。
auto_now=False 每次修改对象自动设置为当前时间。
auto_now_add=False 对象第一次创建时自动设置为当前时间。
auto_now_add、auto_now、default 互斥。
对应控件为 TextInput,关联了一个 Js 编写的日历控件。
TimeField 使用 Python 的 datetime.time 实例表示的时间,参数同上。
DateTimeField 使用 Python 的 datetime.datetime 实例表示的时间,参数同上。
FileField 一个上传文件的字段。
ImageField 继承了 FileField 的所有属性和方法,但是对上传的文件进行校验,确保是一个有效的图片。

字段选项

说明
db_column 表中字段的名称。如果未指定,则使用属性名。
primary_key 是否主键。
unique 是否是唯一键。
default 缺省值。这个缺省值不是数据库字段的缺省值,而是新对象产生的时候被填入的缺省值。
null 表的字段是否可为 null,默认为 False。
blank Django 表单验证中,是否可以不填写,默认为 False。
db_index 字段是否有索引。

关系类型字段类

说明
ForeignKey 外键,表示一对多 ForeignKey('production.Manufacturer') 自关联 ForeignKey('self ')
ManyToManyField 表示多对多。
OneToOneField 表示一对一。

一对多时,自动创建会增加 _id 后缀。

  • 从一访问多,使用 对象.小写模型类_set
  • 从一访问一,使用 对象.小写模型类

访问 id 对象.属性_id

创建 Model 类

Model 类:

  • 要从基类 models.Model 继承;
  • 表名不指定则默认使用 <appname>_<model_name>,使用 Meta 类修改表名

创建一个 User 的 Model 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User(models.Model):

class Meta:
db_table = 'user'

id = models.AutoField(primary_key=True)
name = models.CharField(max_length=48, null=False)
email = models.CharField(max_length=64, unique=True, null=False)
password = models.CharField(max_length=128, null=False)

def __repr__(self):
return '<user {} {}>'.format(self.id, self.name)

__str__ = __repr__

迁移 Migration

迁移:Django 会从模型定义生成数据库的表

1、生成迁移文件:

1
2
3
4
5
6
7
$ python manage.py makemigrations

生成如下文件
user
├─ migrations
├─ 0001_initial.py
└─ __init__.py

若之后修改过 Model 类,还需要再执行一次 python manage.py makemigrations 进行 migrate,迁移文件的序号会增加。

注意:迁移的应用必须在 settings.pyINSTALLED_APPS 中注册过。

2、执行迁移,生成数据库的表:

1
python manage.py migrate

执行迁移,Django 会连接到数据库,按照 model 创建 user 表,还会同时生成 admin 管理用的表。

本地化

settings.py 中设置语言、时区。

1
2
3
LANGUAGE_CODE = 'zh-Hans'
USE_TZ = True
TIME_ZONE = 'Asia/Shanghai'

语言名称可以查看 django\contrib\admin\locale 目录。

启动 WEB Server

1
python manage.py runserver

默认启动 8000 端口

Django 后台管理

1、创建管理员

1
manage.py createsuperuser

后台登录地址:http://127.0.0.1:8000/admin

路由

编写 WSGI 框架项目中,路由功能就是实现 URL 模式匹配和处理函数之间的映射。对于 Django 也是如此。
路由配置要在项目的 urls.py 中配置,也可以多级配置,在每一个应用中,建立一个 urls.py 文件配置路由映射。

url 函数
url(regex, view, kwargs=None, name=None) 进行模式匹配:

  • regex:正则表达式,与之匹配的 URL 会执行对应的第二个参数;
  • view:用于执行与正则表达式匹配的 URL 请求;
  • kwargs:视图使用的字典类型的参数;
  • name:用来反向获取 URL。

多级路由

为了避免项目的总 urls.py 文件中条目过多,也为了让应用自己管理路由。

使用 include 函数,参数写 应用.路由模块,例如 include('user.urls'),该函数就会动态导入指定的包的模块,从模块里面读取 urlpatterns,并返回三元组。

url 函数的第二参数如果不是可调用对象,而是元组或列表,则会从路径中除去已匹配的部分,将剩余部分与应用中的路由模块的 urlpatterns 进行匹配。

例如,要访问的路径为 /user/reg

  • 项目中的 urls.py 文件负责解析第一级:/user
  • 应用中的 urls.py 文件负责解析第二级 /reg
1
2
3
4
5
6
urlpatterns = [
path('admin/', admin.site.urls),
url(r'^$', index),
url(r'^user/', include('user.urls')), # 第一级匹配规则
# url 函数会做类似 startswitch 之类的操作,把开头匹配到的去掉,然后交给应用 user 中的 urls 去匹配
]

之后在应用 user 中新建 urls.py 文件:

1
2
3
4
5
6
7
8
9
10
11
from django.conf.urls import url

# 临时测试用 reg 视图函数
from django.http import HttpRequest, HttpResponse

def reg(request:HttpRequest):
return HttpResponse(b'user.reg')

urlpatterns = [
url(r'^reg$', reg) # 进行第二级匹配
]

在浏览器中输入 http://127.0.0.1:8000/user/reg 测试一下,可以看到响应的数据。

JSON 数据处理 – simplejson

simplejson 比标准库方便好用,功能强大。

1
pip install simplejson

解析的方式同 json 模块,但是 simplejson 更方便。

错误处理

Django 中有很多异常类,定义在 django.http 下,这些类都继承自 HttpResponse

所以如果返回异常,要返回实例,而不是异常类
而且要用 return HttpResponseBadRequest()
而不是 raise HttpResponseBadRequest()
前端是通过状态码来判断是否异常的,所以不能 raise,只能使用 return 返回状态码给前端。

模型操作(ORM)

Django 会为模型类提供一个 objects 对象,它是 django.db.models.manager.Manager 类型,用于与数据库交互。

当定义模型类的时候没有指定管理器,则 Django 会为模型类提供一个默认的 objects 的管理器。
如果在模型类中手动指定管理器后,Django 就不再提供默认的 objects 的管理器了。
不过大部分情况不会手动指定管理器,一般不会遇到。

Django 默认是在 save()delete() 的时候事务自动提交。

查询

查询集

查询会返回结果的集,它是 django.db.models.query.QuerySet 类型。
它是惰性求值,和 sqlalchemy 一样, 结果就是查询的集。
它是可迭代对象。

  1. 惰性求值:
    创建查询集不会带来任何数据库的访问,直到调用数据时,才会访问数据库。
    在迭代、序列化、if 语句中都会立即求值。

  2. 缓存:
    每一个查询集都包含一个缓存,来最小化对数据库的访问。
    新建查询集,缓存为空。首次对查询集求值时,会发生数据库查询,Django 会把查询的结果存在这个缓存中,并返回请求的结果。

  • 没有使用缓存,每次都要去查库,查了 2 次库:
1
2
[user.name for user in User.objects.all()]
[user.name for user in User.objects.all()]
  • 下面的语句使用缓存,因为使用同一个结果集:
1
2
3
qs = User.objects.all()
[user.name for user in qs]
[user.name for user in qs]

限制查询集(切片)

查询集对象可以直接使用索引下标的方式 (不支持负索引),相当于 SQL 语句中的 limitoffset 子句。
注意使用索引返回的新的结果集,依然是惰性求值,不会立即查询。

1
2
3
4
5
# LIMIT 20 OFFSET 20
qs = User.objects.all()[20:40]

# LIMIT 10 OFFSET 20
qs = User.objects.all()[20:30]

过滤器

返回 查询集 的方法,称为过滤器,如下:

名称 说明
all()
filter() 过滤,返回满足条件的数据
exclude() 排除,排除满足条件的数据
order_by()
values() 返回一个对象字典的列表,像 json。

filter(k1=v1).filter(k2=v2) 等价于 filter(k1=v1, k2=v2)

主键 pk

filter(pk=10) 这里 pk 指的就是主键,这样使用不用关心主键字段名,当然也可以使用使用主键名。

返回单个值的方法
名称 说明
get() 仅返回单个满足条件的对象 如果未能返回对象则抛出 DoesNotExist 异常; 如果能返回多条,抛出 MultipleObjectsReturned 异常。
count() 返回当前查询的总条数
first() 返回第一个对象
last() 返回最后一个对象
exist() 判断查询集中是否有数据,如果有则返回 True
1
2
3
4
5
user = User.objects.filter(email=email).get() # 期待查询集只有一行,否则抛出异常
user = User.objects.get(email=email) # 返回不是查询集,而是一个 User 实例,否则抛出异常
user = User.objects.get(id=1) # 更多的查询使用主键,也可以使用 pk=1
user = User.objects.first() # 使用 limit 1 查询,查到返回一个实例,查不到返回 None
user = User.objects.filter(pk=3, email=email).first() # and 条件

字段查询(Field Lookup)表达式

字段查询表达式可以作为 filter()exclude()get() 的参数,实现 where 子句的效果。

语法属性(字段)名称__比较运算符 = 值

注意:属性名和运算符之间使用双下划线

可用比较运算符如下:

名称 举例 说明
exact filter(isdeleted=False)
filter(isdeleted__exact=False)
严格等于,可省略不写
contains exclude(title__contains='天') 是否包含。
大小写敏感,等价于 like '% 天 %'
statswith
endswith
filter(title__startswith='天') 以什么开头或结尾,大小写敏感
isnull
isnotnull
filter(title__isnull=False) 是否为 null
iexact
icontains
istartswith
iendswith
i 的意思是忽略大小写
in filter(pk__in=[1,2,3,100] 是否在指定范围数据中
gt、gte
lt、lte
filter(id__gt=3)
filter(pk__lte=6)
filter(pub_date__gt=date(2000,1,1))
大于、小于、等于
year、month、day week_day
hour、minute、second
filter(pub_date__year=2000) 对日期类型属性处理

Q 对象

虽然 Django 提供传入条件的方式,但是不方便,它还提供了 Q 对象来解决。
Q 对象是 django.db.models.Q,可以使用 &(and)|(or) 操作符来组成逻辑表达式。~ 表示 not。

1
2
3
4
5
6
7
8
 
from django.db.models import Q
User.objects.filter(Q(pk__lt=6)) # 不如直接写 User.objects.filter(pk<6)

User.objects.filter(pk__gt=6).filter(pk_lt=10) # 与
User.objects.filter(Q(pk_gt=6) & Q(pk_lt=10)) # 与
User.objects.filter(Q(pk=6) | Q(pk=10)) # 或
User.objects.filter(~Q(pk__lt<6)) # 非

可使用 &| 和 Q 对象来构造复杂的逻辑表达式。
过滤器函数可以使用一个或多个Q对象。

返回字段的 “人类可读” 值

对于每一个设置了 choice 的字段,该对象将会有一个 get_FOO_display() 方法,其中 FOO 是字段的名称。该方法返回字段的 “人类可读” 值。

例如,model 中字段设置了 choice

1
2
3
4
5
6
7
8
9
10
from django.db import models

class Person(models.Model):
SHIRT_SIZES = (
('S', 'Small'),
('M', 'Medium'),
('L', 'Large'),
)
name = models.CharField(max_length=60)
shirt_size = models.CharField(max_length=2, choices=SHIRT_SIZES)
1
2
3
4
5
6
>>> p = Person(name="Fred Flintstone", shirt_size="L")
>>> p.save()
>>> p.shirt_size
'L'
>>> p.get_shirt_size_display()
'Large'

Django 模板技术

09Django模板技术.pdf

无 session 方案 – JWT

既然服务端就是需要一个 ID 来表示身份,那么不使用 session 也可以创建一个 ID 返回给客户端。但是,要保证客户端不可篡改。
服务端生成一个标识,并使用某种算法对标识签名。
服务端收到客户端发来的标识,需要检查签名。这种方案的缺点是,加密、解密需要消耗 CPU 计算资源,无法让浏览器自己主动检查过期的数据以清除。
这种技术称作 JWT(Json WEB Token)。

理解 JWT 的使用场景和优劣

别再使用 JWT 作为 Session 系统!问题重重且很危险。

pyjwt
JWT(Json WEB Token) 是一种采用 Json 方式安装传输信息的方式。
这次使用 PyJWT,它是 Python 对 JWT 的实现。

安装

1
pip install pyjwt

jwt 的原理演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import jwt

key = 'secret'
token = jwt.encode({'payload': 'abc123'}, key, 'HS256')
print('token 为:', token)
print('解密后:', jwt.decode(token, key, algorithms=['HS256']))
# token=b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXlsb2FkIjoiYWJjMTIzIn0.lZc1PBGdbYDKq9k43tNTt1f0MHy4DjwT8NGTnHEIaVE'
# token 分为 3 部分,用 . 断开


# 看一下三段分别是什么
header, payload, signature = token.split(b'.')
print('header ==', header)
print('payload ==', payload)
print('signature ==', signature)


import base64
def addeq(b: bytes):
'''为base64编码补齐等号'''
rem = len(b) % 4
return b + b'=' * rem


# 把三部分分别补上等号,并用 base64 解开
print('header 解密后: ', base64.urlsafe_b64decode(addeq(header)))
print('payload: 解密后', base64.urlsafe_b64decode(addeq(payload)))
print('signature: 解密后', base64.urlsafe_b64decode(addeq(signature)))


# 根据jwt算法,重新生成签名
# 1 获取算法对象
from jwt import algorithms

alg = algorithms.get_default_algorithms()['HS256'] # 得到一个算法对象
newkey = alg.prepare_key(key) # key 为 secret

# 2 获取前两部分:header.payload
signing_input, _, _ = token.rpartition(b'.') # rpartition 从右边按指定字符分割为两段
print(signing_input)

# 3 使用key 签名
signature = alg.sign(signing_input, newkey)
print('-------------------------')
print(signature) # 得到加密后的签名
print(base64.urlsafe_b64encode(signature)) # 转成 base64 看签名是否和第三段一致

import json

print(base64.urlsafe_b64encode(json.dumps({'payload': 'abc123'}).encode()))

由此,可知 jwt 生成的 token 分为三部分:

  1. header: 由数据类型、加密算法构成;
  2. payload: 负载就是要传输的数据,一般来说放入 python 对象即可,会被 json 序列化的;
  3. signature: 签名部分。是前面 2 部分数据分别 base64 编码后使用点号连接后,通过加密算法使用 key 计算好一个结果,再被 base64 编码,得到签名。

所有数据都是明文传输的,只是做了 base64,如果是敏感信息,请不要使用 jwt。
数据签名的目的不是为了隐藏数据,而是保证数据不被篡改。如果数据篡改了,发回到服务器端,服务器使用自己的 key 再计算一遍,然后进行签名比对,一定对不上签名。

过期时间
pyjwt 支持过期设定,在 decode 的时候,如果过期,则抛出异常。
需要在 payload 中增加一个 expexp 要求是一个整数 int 的时间戳。

1
2
3
4
5
6
7
8
9
10
11
12
AUTH_EXPIRE = 8 * 60 * 60 # 8 小时过期

data = jwt.encode(
{
'name': 'tom',
'age': 20,
'exp': int(datetime.datetime.now().timestamp()) + AUTH_EXPIRE # 增加一个 exp,超过这个时间即为过期
},
key
)

print(jwt.decode(data, key)) # 过期后,校验就会抛出异常 Signature has expired

密码加强方案 – bcrypt

早期,都是明文的密码存储。
后来,使用 MD5 存储,但是,目前也不安全,网上有很多 MD5 的网站,使用反查方式找到密码。

加盐,使用 hash(password + salt) 的结果存入数据库中,就算拿到数据库的密码反查,也没有用了。
如果是固定加盐,还是容易被找到规律,或者从源码中泄露。
使用随机加盐,每一次盐都变,就增加了破解的难度。

通过暴力破解,什么密码都不能保证不被暴力破解,例如穷举。
所以要使用慢 hash 算法,例如使用 bcrypt,就会让每一次计算都很慢,都是秒级的,这样穷举的时间就会很长,为破解一个密码,在当前 CPU 或者 GPU 的计算能力下可能需要几十年以上。

安装:

1
pip install bcrypt

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import bcrypt
import datetime

password = b'123456'

# 每次拿到盐都不一样
print(1, bcrypt.gensalt())
print(2, bcrypt.gensalt())

# 拿到的盐相同,计算得到的密文就相同
salt = bcrypt.gensalt()
print('=========same salt ==========')
x = bcrypt.hashpw(password, salt)
print(3, x)
x = bcrypt.hashpw(password, salt)
print(4, x)

# 如果每次拿到的盐不同,计算生成的密文也不一样
print('=========different salt ==========')
x = bcrypt.hashpw(password, bcrypt.gensalt())
print(5, x)
x = bcrypt.hashpw(password, bcrypt.gensalt())
print(6, x)

# 校验
print(bcrypt.checkpw(password, x), len(x))
print(bcrypt.checkpw(password + b' ', x), len(x))


# 计算加密的时长
start = datetime.datetime.now()
y = bcrypt.hashpw(password, bcrypt.gensalt())
delta = (datetime.datetime.now() - start).total_seconds()
print(10, 'duration={}'.format(delta))

# 计算检验的时长
start = datetime.datetime.now()
y = bcrypt.checkpw(password, x)
delta = (datetime.datetime.now() - start).total_seconds()
print(y)
print(11, 'duration={}'.format(delta))

start = datetime.datetime.now()
y = bcrypt.checkpw(b'1', x)
delta = (datetime.datetime.now() - start).total_seconds()
print(y)
print(12, 'duration={}'.format(delta))

从耗时看出,bcrypt 加密、验证非常耗时,所以如果穷举,非常耗时。
而且碰巧攻破一个密码,由于盐不一样,还得穷举另一个。

四、博客网站打包部署

项目打包

打包后端 python

在项目虚拟环境中,使用 pip 生成依赖包文件列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ pip freeze > requirements

asgiref==3.2.10
bcrypt==3.2.0
cffi==1.14.3
Django==3.1.2
mysqlclient==2.0.1
pip-autoremove==0.9.1
pycparser==2.20
PyJWT==1.7.1
pytz==2020.1
simplejson==3.17.2
six==1.15.0
sqlparse==0.3.1

创建 setup.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from setuptools import setup  # 内部返回了 return distutils.core.setup(**attrs)
import glob

setup(
name="blog",
version="0.1.1",
author="monster",
author_email="monster@admin.com",
description="This is Blog project, use Django and react.",
python_requires='>=3.8',
packages=['blog', 'post', 'user'], # 要打包的包。只会打包以 .py .pyc 结尾的文件(内部的目录也不会打包),如有其他类型的文件要打包,需要在 data_files 中指定
data_files=glob.glob('templates/*') + # glob 会返回一个列表,包含匹配到的文件(str)
[
'requirements', # 刚才生成的依赖包列表
'manage.py' # 默认不带,因为 runserver 只是一个测试的 web 服务
] +
glob.glob('post/migrations/*') + glob.glob('user/migrations/*') # 数据库迁移目录看实际情况,如果数据库已经创建好了则不需要打包
)

打包:

1
2
3
4
$ python setup.py sdist --format=gztar  # 指定打成 gz 格式

# 生成了:
dist/blog-0.1.1.tar.gz

打包前端 React 项目

webpack.config.prod.js 中增加 css 的 loader,否则编译可能出错

1
2
3
4
5
{
test: /\.css$/,
exclude: /node_modules/,
use: [ 'style-loader', 'css-loader' ]
},

在项目目录下编译:

1
npm run build

编译好的在 dist 目录下:

1
2
3
4
$ ls dist
- app-97046cdc.js
- app-97046cdc.js.map
- index.html

项目部署

后端:Django 和 uWSGI 部署

虚拟环境

别忘了在 Linux 系统中使用 virtualenv 创建一个 虚拟环境 目录,如未安装使用 pip3 install virtualenv

1
2
3
4
mkdir blog_deploy
cd blog_deploy
virtualenv 3.8 venv
source venv/bin/activate

在虚拟环境并解压包:

1
2
tar -xf blog-0.1.1.tar.gz
mv blog-0.1.1 blog

安装依赖:

1
root] pip install -r requirements
配置文件

修改 Django 配置文件:blog/settings.py,为了:

  1. 关闭 DEBUG 功能;
  2. 设置允许访问的主机;
  3. 设置实际的数据库地址。
1
2
# 修改 Django 配置文件:`blog/settings.py`
$ sed -i -e 's/DEBUG.*/DEBUG = False/' -e 's/ALLOWED_HOSTS.*/ALLOWED_HOSTS = ["*"]/' -e "s/'HOST':.*/'HOST': '192.168.50.30',/" blog/settings.py

注意ALLOWED_HOSTS = ["*"] 这是所有都可以访问,生产环境应指定具体可以访问的 IP,而不是所有。

在 Mac 上,sed 命令有一些区别,sed 命令直接操作文件的时候,必须指定备份的格式:

1
2
$ sed -i '' -e 's/DEBUG.*/DEBUG = False/' -e 's/ALLOWED_HOSTS.*/ALLOWED_HOSTS = ["*"]/' -e "s/'HOST':.*/'HOST': '192.168.50.30',/" blog/settings.py
# 在 -i 之后加上一对引号,来指定备份格式,如果不需要备份,引号里的内容可以为空。
uWSGI 部署

uWSGI 是一个 C 语言的项目,提供一个 WEB 服务器,它支持 WSGI 协议,可以和 Python 的 WSGI 应用程序通信。

在这里,uWSGI 负责在 socket 上监听并接收 Nginx 发来的 uWSGI 协议的请求,并通过 WSGI 协议调用 Django 应用。

安装:

1
pip install uwsgi $ uwsgi --help

项目目录下放 uWSGI 的配置文件 uwsgi.ini

1
2
3
4
[uwsgi]
socket = 0.0.0.0:9000 # 使用 uwsgi 协议通信,生成环境应缩小权限
chdir = /blog/ # 安全方面配置
wsgi-file = /blog/blog/wsgi.py # 指定要代理的 django App 文件,blog 下 wsgi.py

启动 uWSGI:

1
uwsgi blog.ini

至此,后端 Django 应用部署完成。

测试服务,如成功返回数据,说明 Django 应用成功部署:

1
curl http://192.168.50.30:8000/post?page=2&size=2
当使用 DockerFile 时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM python:3

WORKDIR /blog/
COPY blog/ ./blog/
COPY post/ ./post/
COPY user/ ./user/
COPY templates/ ./templates/
COPY *.py ./
COPY blog.ini ./
COPY requirements ./requirements
RUN sed -i -e 's/DEBUG.*/DEBUG = False/' -e 's/ALLOWED_HOSTS.*/ALLOWED_HOSTS = ["*"]/' -e "s/'HOST':.*/'HOST': '192.168.50.30',/" blog/settings.py

RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r /blog/requirements
RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple uwsgi
CMD [ "uwsgi", "./blog.ini" ]

构建镜像:

1
2
docker build -t blog-app ./blog_deploy
docker run -it --rm -p 9000:9000 --name blog blog-app

前端:React 和 Nginx 部署

Nginx 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 80;
server_name localhost;
location ^~ /api/ { // 左前缀匹配
rewrite ^/api(/.*) $1 break; // 重写请求的 path
include uwsgi_params;
uwsgi_pass 192.168.50.30:9000;
}

location / {
try_files $uri /index.html; // 加一条这个,以防止访问二级路由或刷新时会 404
root /usr/share/nginx/html;
index index.html index.htm;
}
}
静态文件部署

把之前编译好的静态文件拷贝到 html 目录,按照以下结构:

1
2
3
4
5
$ tree html
.
├── assets
│   └── app-97046cdc.js
└── index.html
启动 Nginx
1
systemctl start nginx
当使用 Docker 时
1
docker run --name nginx -v ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro -v ./nginx/html:/usr/share/nginx/html:ro -p80:80 -d nginx

启动数据库的参考:

1
docker run --name db -v ./mariadb:/var/lib/mysql -p192.168.50.30:3306:3306 -e MYSQL_ROOT_PASSWORD=123qwe -e TZ=Asia/Shanghai mariadb