前言
最近有個專案到一段落,做個小結記錄。
內容可能會多次補充,在部落格上實時更新哈~
如果是在公眾號閱讀這篇文章,可以點選「檢視原文」訪問最新版本~
這個專案是前後端分離,後端為了快,依然用我的DjangoStarter框架。前端一開始是小程式,後面突然換成公眾號H5的形式,還好我用了Taro,大差不差。
不過Taro目前沒啥好用成熟的元件庫,前一個專案本來用著Taroify,不過用了一半專案還沒做完,Taroify的作者就跑路不維護了~ 雖然但是,還是能用,把舊專案的一些程式碼複用一下,也不是不行。
總體的開發體驗就是很一般,雖說React寫前端舒服多了,但元件庫實在是拉胯… 如果下個專案依然要用Taro的話,估計得試試新出的NutUI-React了。
說回正題,這次我從Web開發、部署這幾方面對這個專案做個小總結。
後端
後端用DjangoStarter模板,自從我上次升級了v2版本之後,還沒實戰過,這次專案上使用了,還是穩得一批,(所以點star的同學可以放心用哈~)
之前oauth部分只有企業微信,微信登入還是todo,這次因為接入公眾號,我順手也把微信登入做了,其實跟企業微信基本沒啥區別。
"雙標"的ModelViewSet
drf的ModelViewSet,可以快速生成crud介面,不過預設許可權控制很粗糙,只能選擇三種:
- 已登入可訪問
- 管理員可訪問
- 任何人可訪問
但正常的場景是,假設有個文章介面,使用者只能管理自己寫的文章,管理員可以訪問全部文章。也就是要對不同角色的使用者區別對待~
要實現的話可以這樣,重寫 ModelViewSet
的 get_queryset
方法,根據使用者的身份來生成對應 queryset
def get_queryset(self):
user: User = self.request.user
if user.is_superuser:
return super().get_queryset()
else:
return super().get_queryset().filter(user=user)
然後再新增和更新的時候也要修改一下
比如重寫一下 create
方法,最前面加上當前使用者的id
def create(self, request: Request, *args, **kwargs):
request.data['user'] = request.user.id
# 後面程式碼就省略了
Model Field 擴充套件
本次用了兩個擴充套件
- tinymce 的
HTMLField
: 用於富文字編輯,也就是前面說的文章功能 - MultiSelectField :用於多選欄位(雖然Django+PgSql可以儲存列表資料,但跟這好像倆回事)
tinymce之前的文章有介紹過,我還封裝了一個 contrib
包,後面有空整合到DjangoStarter裡面
MultiSelectField使用也簡單,可以把一個 Choices
作為欄位的值,在Django Admin裡面,表現為一個多選列表,編輯和使用都比較方便~
一些架構設計的問題
本來在做DjangoStarter v2版本的時候,我把相關的程式碼都放在 django_starter
包裡,就是為了開發者不需要去修改這部分程式碼,這樣在DjangoStarter版本有更新的時候,可以直接覆蓋升級。
但我又把 oauth 和 UserProfile (使用者資訊)所在的 auth 這兩個app都放在了 django_starter/contrib
包裡面。
但往往一個專案中,難免會對使用者資訊做一些擴充套件,這樣就得修改到這個 django_starter
下面的程式碼,這不符合設計規範~
這部分也是我在v2版本設計的問題,看來可能要把這個使用者資訊相關的程式碼都放回到 apps
下面,讓使用者(開發者)自行決定這部分程式碼是否使用。
前端
前端使用React+TypeScript,開發體驗還可以,儘管之前經常吐槽TypeScript,但熟悉之後還是能愉快使用的,畢竟和C#同一個作者,質量有保障~
雖說Taro坑很多,元件庫質量也不高
但… 沒有的元件,就自己造輪子!
好吧,在造輪子這件事上,我把自己坑了一下… 我自己做了個日曆元件,不得不說,日曆元件確實有點小複雜,專案開發過程中,這元件就出了兩次坑爹的bug,花了我不少時間折騰~
說回來,就用Taro提供的最基本的 view 元件,再配合scss,可以說元件庫裡缺什麼,自己造什麼,雖然我不是專門做前端的,樣式寫得很菜,但… 勉強能看吧
我感覺React用上手了比vue舒服一些(非引戰),可能跟我之前經常用Flutter習慣了宣告式UI有關~
掏出幾個常用的hook(useEffect / useRef / useRouter),開發體驗很絲滑。
這次我還多學了一個 useLayoutEffect,用來解決頁面閃爍。
全域性狀態管理沒用redux,改用輕量級mobx,舒服~ 不過除了使用者管理,其他的基本上可以用路由傳參解決,全域性狀態用得很少。
路由管理
好訊息,這次我終於沒有手寫路由地址了
終於搞了個 RouterMap
export const RouterMap = {
announcementDetail: 'pages/announcement/detail',
announcementList: 'pages/announcement/index',
home: '/pages/index/index',
feedback: 'pages/user/feedback',
login: '/pages/user/login',
order: 'pages/user/order'
}
需要跳轉的時候就
Taro.navigateTo({url: RouterMap.login})
不過在必須登入才可以訪問的頁面上,我還是用最原始的判斷跳轉,很不優雅
useEffect(() => {
if (!myUserStore.isLogin) {
Taro.redirectTo({url: RouterMap.login})
return
}
}, [])
看了「前端帶師」的 remax-router
,對路由做了hack,直接在框架路由處做攔截,真羨慕啊,等會學會這個操作我也要這樣做。
多寫元件
雖然我自己造輪子埋了不少坑,但還是鼓勵多用元件,現代前端就是元件化開發嘛,都寫在一個頁面也太醜了,都給我拆成元件!
於是,我的src
目錄下就有倆放元件的目錄,一個是 components
,一個是 ui
。
前者放只在本專案內用的元件,後者放通用元件,可複用的那種,以後有空做成NPM包的那種。
元件間的通訊很方便,父元件向子元件傳遞,直接props傳值;子到父,直接在props裡定義個事件就行了。
比如我這個日曆元件
export interface CalendarSmallProps extends ViewProps {
days: Array<DayPlan>
value?: Date
onChange?(value: DoctorDayPlan): void
}
父元件使用的時候
<CalendarSmall days={days} value={currentDate} onChange={handleDayChange}/>
日曆元件向父元件傳值,也就是觸發事件
function setDay(item: DoctorDayPlan) {
setValue(item.date)
props.onChange?.(item)
}
我們就是說,這個 xxx?.()
的語法真是妙 (連「前端帶師(coppy)」都讚不絕口,能不妙嗎?)
然後每個元件建立個目錄,比如這個日曆元件,我放在 ui/calendar_small
下。倆個檔案:
index.tsx
:主要程式碼index.scss
:樣式
然後在 ui
目錄下再來個 index.ts
裡面匯出一下
export * from './calendar_small'
這樣在使用的時候只要 import {CalendarSmall} from "@/ui";
即可,方便得很啊!
部署
前面寫了那麼多,我都差點忘了部署才是本次專案重點想記錄的。
前段時間我買了個新域名 dealiaxy.com,新專案也搞了個新的伺服器,這次部署想實現的效果是 *.dealiaxy.com 泛域名解析,且全部走HTTPS。
之前看同學部落格的時候發現有個叫swag的映象,把 Let's Encrypt 都折騰配置好了,開箱即用,這次來試試看。
使用swag配置HTTPS
因為這是我第一次用swag映象部署 Let's Encrypt 的泛域名HTTPS,遇到挺多坑的,也查了很多資料,最終完美搞定~
很多時候雖然文件很齊備,但因為各種條件不一致,很難一下子搞起來。
- 官方文件: https://docs.linuxserver.io/general/swag
- 國內有人搞了箇中文文件,也很不錯: https://linuxserver.watercalmx.com/general/swag.html
首先在域名控制檯新增A記錄的解析,把 @
和 *
都指向這臺伺服器,然後準備個空目錄來部署swag容器。
docker 部署
繼續用docker-compose,有幾個關鍵配置。
- Let's Encrypt 有多種驗證方式,因為我要用泛域名證照,所以配置
VALIDATION
為 dns 方式 - 時區
TZ
設定為Asia/Shanghai
- 子域名
SUBDOMAINS
設定為wildcard
(萬用字元) DNSPLUGIN
是DNS提供商,是配置重點,後面說- 掛載一下
/config
目錄,後面swag跑起來之後需要在裡面配置域名和網站資訊
version: "2"
services:
swag:
image: linuxserver/swag
container_name: swag
cap_add:
- NET_ADMIN
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- URL=dealiaxy.com
- SUBDOMAINS=wildcard
- VALIDATION=dns
- DNSPLUGIN=cloudflare
volumes:
- ./config:/config
ports:
- 443:443
- 80:80
restart: unless-stopped
networks:
default:
name: swag
DNSPLUGIN 配置
swag支援很多DNS提供商,比如阿里雲、騰訊雲、cloudflare這些。具體的可以看 config/dns-conf
裡面的配置。
我這個域名是國外買的,恰好那家服務商也沒有在swag的支援列表裡面,一開始還有點暈頭轉向不知道咋辦,後面看到swag支援阿里和騰訊的dnspod,於是我在阿里DNS上看了一下,可以配置解析,瞬間悟了,域名在哪買的不重要,域名的DNS提供商可以隨便換的。
根據阿里DNS的指引,在域名控制檯裡面把Name Server改成阿里的 ns1.alidns.com
和 ns2.alidns.com
就行了。
然後在阿里雲的控制檯裡生成一下 access_key 和 secret,編輯 config/dns-conf/aliyun.ini
放進去,再啟動swag容器就行了。
tips:阿里雲DNS需要域名有備案才提供解析,未備案的話慎用~ 可以試試Cloudflare,據說很好用。
用其他的DNS提供商同理,操作很類似。
docker 網路配置
docker容器直接預設是不能直接連線的,所以反向代理也就無從說起。
swag和後端是倆不同的docker容器,要能互相連線,得先加入同一docker網路才行。
推薦portainer這個工具,可以很方便管理docker~
使用docker-compose啟動swag,會自動生成一個swag_default的網路,拿這個來用就行了,我先把它改名成swag,方便記憶。
然後再修改一下後端的docker-compose配置,增加網路配置
networks:
swag:
external:
name: swag
然後,我這個docker-compose裡有redis和django兩個容器,只有django需要加入swag,所以在django下面配置一下網路
web:
networks:
- swag
- default
這樣就行了~ (當然我後面還要再改一下,這樣寫只是方便理解)
反向代理配置
泛域名證照配置搞定了,接下來可以配置網站
靜態檔案放在 config/www
裡面
後端需要做反向代理,配置在 config/nginx/proxy-confs
裡面
這裡面有個比較難受的地方,swag預設提供了一堆反向代理的模板(檔名 .example
結尾),這個目錄一開啟裡面一堆檔案,很影響我找到我已經配置好的,解決辦法是 ls
的時候用正則匹配一下檔名。
ll | grep .conf$
這樣就只顯示以 .conf
結尾的檔案了。
假設我的應用域名是 app1.dealiaxy.com
,那配置檔名就是 app1.subdomain.conf
附上我的反向代理配置:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name app1.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
# enable for ldap auth, fill in ldap details in ldap.conf
#include /config/nginx/ldap.conf;
# enable for Authelia
#include /config/nginx/authelia-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable the next two lines for ldap auth
#auth_request /auth;
#error_page 401 =200 /ldaplogin;
# enable for Authelia
#include /config/nginx/authelia-location.conf;
include /config/nginx/proxy.conf;
resolver 127.0.0.11 valid=30s;
set $upstream_app app1_nginx;
set $upstream_port 8001;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
主要就看這幾行:
set $upstream_app app1_nginx; # 容器名稱
set $upstream_port 8001; # 容器埠,容器裡面開啟的埠,不是透過 ports 對映的
set $upstream_proto http; # 協議,還有其他比如 uwsgi, https 之類的
再看看接下來的Django部署,就會一目瞭然了~
Django部署
Django部署依然是用之前很熟悉的docker部署,不過這次我又做了一些修改。
之前是一個nginx服務直接裝在系統上,若干個docker容器跑服務,這種情況下每個容器只需要提供web應用功能,不用管靜態檔案,直接在nginx裡面配置靜態檔案就行了。
但是現在,nginx也裝進了docker(swag),那就沒辦法隨意訪問到整個系統的檔案,如果每增加一個應用,都去掛載一個新的volume到swag裡,那也太折騰了。
所以我選擇在Django的docker-compose裡整合nginx。
docker-compose.yaml
version: "3"
services:
redis:
image: redis
container_name: app1_redis
restart: always
nginx:
image: nginx:stable-alpine
container_name: app1_nginx
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./media:/code/media:ro
- ./static_collected:/code/static_collected:ro
depends_on:
- web
networks:
- default
- swag
web:
build: .
container_name: app1_web
restart: always
environment:
- ENVIRONMENT=docker
- URL_PREFIX=
- DEBUG=false
command: uwsgi uwsgi.ini
volumes:
- .:/code
depends_on:
- redis
networks:
- default
networks:
swag:
external:
name: swag
就是在DjangoStarter原有docker-compose配置的基礎上增加了nginx的配置,使用官方的nginx映象: https://hub.docker.com/_/nginx
nginx.conf
上面的uwsgi.ini沒貼出來,也沒啥好說的,裡面開放的埠是8000,所以nginx配置裡面 upstream
寫的埠要對應 8000。
upstream django {
ip_hash;
server web:8000; # Docker-compose web服務埠 (也就是uwsgi的埠)
}
server {
listen 8001; # 監聽8001埠
server_name localhost; # 可以是nginx容器所在ip地址或127.0.0.1,不能寫宿主機外網ip地址
charset utf-8;
client_max_body_size 100M; # 限制使用者上傳檔案大小
location /static {
alias /code/static_collected; # 靜態資源路徑
}
location /media {
alias /code/media; # 媒體資源,使用者上傳檔案路徑
}
location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass django;
uwsgi_read_timeout 600;
uwsgi_connect_timeout 600;
uwsgi_send_timeout 600;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
}
}
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
server_tokens off;
可以看到這個django應用內嵌的nginx配置開啟的8001埠
再回看上面的swag反向代理配置
set $upstream_app app1_nginx;
set $upstream_port 8001;
set $upstream_proto http;
就對應上了
這樣配置之後,docker compose up
啟動swag,再訪問 app1.dealiaxy.com
就可以了~
全站HTTPS太舒服了,瀏覽器再也不太提示不安全了~
許可權問題
可以留意到swag的docker-compose配置裡面有倆環境變數,PUID
和 PGID
,swag內部都配置好了,指定了這倆,容器啟動的時候就會以指定的使用者和使用者組執行,而不是預設以root執行,這樣會安全一些,而且掛載了 volume 出來的檔案,也不是root許可權,當前登入使用者不用 sudo 就能修改。
在 django 的docker里加入nginx的時候我有嘗試改成不用root執行,根據官方指引使用了 nginxinc/nginx-unprivileged
這個映象,也測試了在docker-compose配置裡傳入 user
引數,好像都沒什麼效果。
折騰了半天只好暫時放棄,後續有進展再繼續更新。
小結
這次專案說實在的沒啥技術含量,CRUD罷了,收穫的話就一點點:
- 又熟悉了一些react的寫法
- 把swag配好了,以後其他伺服器可以依樣畫葫蘆,極大提高生產力
參考資料
- LinuxServer.io | 中文文件 - https://linuxserver.watercalmx.com/
- Docker Compose 網路設定 - https://juejin.cn/post/6844903976534540296
- 大江狗的Docker完美部署Django Uwsgi+Nginx+MySQL+Redis - https://zhuanlan.zhihu.com/p/145364353