Project Completion Summary - November New Project - Clinic Project
Foreword Recently, a project reached a milestone, so I’m doing a brief recap to document it. The content may be updated multiple times, and I’ll keep it up to date on my blog in real-time~ If you’re reading this article on a WeChat Official Account, you can click "View Original Text" to access the latest version~ This project involved front-end and back-end separation. For the back-end, I still used my DjangoStarter framework for speed. The front-end initially started as a Mini Program but later switched to an H5 format for WeChat Official Accounts. Fortunately, since I used Taro, it was pretty much the same. However, Taro currently lacks a mature and user-friendly component library. In the previous project, I was using Taroify, but halfway through, the project wasn’t even...
目录20 个章节
Preface
Recently, a project has reached a milestone, so I'm making a small summary to document it.
The content may be updated multiple times, and I'll keep it updated in real-time on my blog~
If you're reading this article on a WeChat Official Account, you can click "Read Original" to access the latest version~
This project uses a front-end and back-end separation architecture. For the back-end, to speed things up, I still used my DjangoStarter framework. The front-end initially was a WeChat Mini Program (小程序), but later it suddenly switched to a WeChat Official Account H5 (公众号H5) format. Fortunately, I used Taro, so it wasn't a big deal.
However, Taro currently doesn't have a mature and well-functioning component library. In the previous project, I initially used Taroify, but halfway through, the author of Taroify abandoned the project and stopped maintaining it~ Even so, it's still usable. Reusing some code from the old project isn't out of the question.
Overall, the development experience was just average. While React makes front-end development much more comfortable, the component libraries are really lacking... If the next project still requires Taro, I'll probably try the newly released NutUI-React.
Back to the main topic, I'll give a brief summary of this project from the perspectives of web development and deployment.
Backend
The backend uses the DjangoStarter template. Since I upgraded to v2 last time, I hadn't used it in a real project until now. It's still incredibly stable—so anyone thinking about giving it a star can go ahead with confidence~
Previously, the OAuth section only supported WeCom, and WeChat Login was still on the to-do list. This time, because I integrated a public account, I also went ahead and implemented WeChat Login. It's pretty much the same as WeCom, really.
"Double Standard" ModelViewSet
DRF's ModelViewSet can quickly generate CRUD interfaces, but the default permission control is quite rough, offering only three choices:
- Accessible to logged-in users
- Accessible to administrators
- Accessible to anyone
However, in a typical scenario, let's say there's an article API. Users should only manage articles they've written, while administrators should be able to access all articles. This means different users need to be treated differently based on their roles.
To implement this, you can override the get_queryset method of ModelViewSet to generate the appropriate queryset based on the user's identity.
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)
Additionally, you need to make modifications when adding and updating.
For example, you can override the create method to prepend the current user's ID.
def create(self, request: Request, *args, **kwargs):
request.data['user'] = request.user.id
# The rest of the code is omitted
Model Field Extensions
This time, two extensions were used:
- tinymce's
HTMLField: Used for rich text editing, which is the article functionality mentioned earlier. - MultiSelectField: Used for multiple choice fields (although Django+PgSql can store list data, this seems to be a different matter).
I've introduced tinymce in previous articles. I've also wrapped it into a contrib package, and I plan to integrate it into DjangoStarter when I have time.
MultiSelectField is also easy to use. It can take a Choices as the field's value. In Django Admin, it is displayed as a multiple-select list, making it convenient for both editing and use.
Some Architecture Design Issues
When I was working on DjangoStarter v2, I placed the related code in the django_starter package. This was so that developers wouldn’t need to modify that part of the code, allowing for direct overwrite upgrades whenever DjangoStarter releases new versions.
However, I also placed the oauth app and the auth app, which contains UserProfile (用户信息), inside the django_starter/contrib package.
In practice, though, a project often requires extending user information. This inevitably leads to modifying code under django_starter, which does not align with design specifications.
This was also a design flaw I encountered in v2. It seems that the code related to user information should probably be moved back under the apps directory, letting users (developers) decide for themselves whether to use that part of the code.
Frontend
The frontend uses React+TypeScript. The development experience was pretty good. Although I used to complain about TypeScript a lot, after getting familiar with it, it's actually pleasant to use. After all, it shares the same creator as C#, so the quality is guaranteed~
Although Taro has many pitfalls and the component library quality isn't high...
But... If a component doesn't exist, just build your own wheel!
Okay, I kind of shot myself in the foot with that whole "building my own wheel" thing... I made a calendar component myself, and I have to say, calendar components are indeed a bit complex. During the project development, this component had two ridiculous bugs that took me quite a bit of time to sort out~
Anyway, using the most basic view component provided by Taro, combined with SCSS, you can basically build whatever is missing from the component library. Although I'm not a dedicated frontend developer and my styling skills are pretty weak... it's passable, I guess.
I feel like once you get the hang of React, it's more comfortable than Vue (not trying to start a debate). Maybe it's because I was previously used to declarative UI from using Flutter a lot~
I pulled out a few commonly used hooks (useEffect / useRef / useRouter), and the development experience was very smooth.
This time, I also learned a new hook, useLayoutEffect, to solve page flickering issues.
For global state management, I didn't use Redux. Instead, I used the lightweight MobX, which is much more comfortable~ However, besides user management, most other things can be handled with route parameters, so global state is rarely used.
Route Management
Good news—this time I finally didn't have to write route paths manually.
I finally created a 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'
}
When a redirect is needed, simply use:
Taro.navigateTo({url: RouterMap.login})
However, for pages that require login access, I still resort to the most basic conditional redirect—not very elegant.
useEffect(() => {
if (!myUserStore.isLogin) {
Taro.redirectTo({url: RouterMap.login})
return
}
}, [])
I came across the remax-router by the "front-end master," which implements a router hack by intercepting directly at the framework's routing level. I really admire that approach—I’m going to learn this trick and implement it myself as well.
Use More Components
Although I dug quite a few holes while reinventing the wheel, I still encourage using more components. After all, modern frontend development is all about component-based architecture. Dumping everything onto a single page is just messy—so split them all into components!
As a result, my src directory has two folders for components: components and ui.
The first holds components meant only for use within this project, while the second contains generic, reusable components—the kind you might package into an NPM module when you get the chance.
Communication between components is super convenient. For parent-to-child data flow, just pass values directly via props. For child-to-parent communication, simply define an event within the props.
For example, here’s my calendar component:
export interface CalendarSmallProps extends ViewProps {
days: Array<DayPlan>
value?: Date
onChange?(value: DoctorDayPlan): void
}
When the parent component uses it:
<CalendarSmall days={days} value={currentDate} onChange={handleDayChange}/>
And when the calendar component needs to pass data back to the parent (i.e., trigger an event):
function setDay(item: DoctorDayPlan) {
setValue(item.date)
props.onChange?.(item)
}
I mean, the xxx?.() syntax is pure genius (even the “frontend guru (coppy)” can’t stop praising it—how could it not be brilliant?).
Each component gets its own directory. For example, this calendar component is placed under ui/calendar_small with two files:
index.tsx: Main codeindex.scss: Styles
Then, inside the ui directory, there’s an index.ts that exports everything:
export * from './calendar_small'
This way, when using it, you can simply do import {CalendarSmall} from "@/ui";—super convenient!
Deployment
I've written so much above that I almost forgot deployment is the key thing I wanted to document for this project.
Some time ago, I bought a new domain name dealiaxy.com, and for the new project, I also set up a new server. The goal of this deployment is to achieve wildcard domain resolution for *.dealiaxy.com, all using HTTPS.
Previously, when I was reading a classmate's blog, I found an image called swag that has Let's Encrypt all configured and is out of the box; this time, I'll give it a try.
Configuring HTTPS with SWAG
Since this is my first time using the SWAG image to deploy Let's Encrypt with a wildcard domain for HTTPS, I encountered quite a few challenges and had to consult many resources. Eventually, I got everything working perfectly.
Sometimes, even though documentation is quite comprehensive, varying conditions can make it difficult to get things set up quickly.
- Official Documentation: https://docs.linuxserver.io/general/swag
- There's also a Chinese documentation created by someone domestically, which is also quite good: https://linuxserver.watercalmx.com/general/swag.html
First, go to your domain control panel and add A records for both @ and *, pointing them to this server. Then, prepare an empty directory to deploy the SWAG container.
Docker Deployment
Continuing to use Docker Compose, here are a few key configurations:
- Let's Encrypt supports multiple validation methods. Since I want to use a wildcard certificate, I set
VALIDATIONto the DNS method. - Set the timezone
TZtoAsia/Shanghai. - Set the subdomain
SUBDOMAINStowildcard(for the wildcard certificate). DNSPLUGINis the DNS provider and is the focus of the configuration—I’ll explain it later.- Mount the
/configdirectory, as you will need to configure domain and website information within it after SWAG is running.
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 Configuration
SWAG supports many DNS providers, such as Alibaba Cloud, Tencent Cloud, and Cloudflare. You can check the specific configurations in config/dns-conf.
My domain was purchased from an overseas provider that wasn’t on SWAG’s supported list, which initially left me a bit confused. Later, I saw that SWAG supports Alibaba and Tencent's DNSPod. I checked on Alibaba’s DNS and found that it could configure resolution—it clicked immediately! It doesn’t matter where you bought your domain; you can easily switch its DNS provider.
Following Alibaba DNS’s guidance, I just changed the Name Server in the domain control panel to Alibaba's ns1.alidns.com and ns2.alidns.com.
Then, generate the access key and secret in the Alibaba Cloud console, edit config/dns-conf/aliyun.ini to input them, and restart the SWAG container.
Tips: Alibaba Cloud DNS requires the domain to have ICP filing to provide resolution. Be cautious if it’s not filed—you can try Cloudflare, which is said to work very well.
The process is similar for other DNS providers—the steps are quite comparable.
Docker Network Configuration
Docker containers cannot directly connect by default, so reverse proxying is out of the question.
SWAG and the backend are two separate Docker containers. To enable them to connect, they need to be added to the same Docker network first.
Portainer is recommended for managing Docker conveniently~
When you start SWAG with Docker Compose, it automatically generates a network named swag_default. You can use this directly. I renamed it to swag for easier memorization.
Then, modify the backend’s Docker Compose configuration to add the network configuration:
networks:
swag:
external:
name: swag
In my case, this Docker Compose file contains both Redis and Django containers. Only Django needs to join the SWAG network, so I configure the network under the Django service:
web:
networks:
- swag
- default
This should work! (Of course, I’ll modify it later for clarity, but this format is easier to understand for now.)
Update on 2023-3-9
The above method is outdated and will generate the following warning:
network.external.name is deprecated in favor of network.nameDocker officially recommends using the new format:
networks: default: name: app_name swag: name: swag external: true
Reverse Proxy Configuration
Once the wildcard certificate is configured, you can set up your website.
Static files go into config/www.
For the backend, you need to set up a reverse proxy, with configurations in config/nginx/proxy-confs.
There’s a slightly inconvenient aspect here: SWAG provides a bunch of reverse proxy templates by default (files ending with .example). Opening this directory reveals many files, which can make it hard to find the ones you’ve already configured. The solution is to use regular expressions when listing files with ls.
ll | grep .conf$
This will only show files ending with .conf.
Assuming my application domain is app1.dealiaxy.com, the configuration file name would be app1.subdomain.conf.
Here’s my reverse proxy configuration:
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;
}
}
Focus on these lines:
set $upstream_app app1_nginx; # Container name
set $upstream_port 8001; # Container port (the port opened inside the container, not the one mapped via ports)
set $upstream_proto http; # Protocol, others include uwsgi, https, etc.
Looking at the subsequent Django deployment will make it clear at a glance~
Django Deployment
For Django deployment, I still use the familiar Docker deployment method, but with some modifications this time.
Previously, I had one nginx service installed directly on the system and several Docker containers running services. In that case, each container only needed to provide the web application functionality, without worrying about static files—they were configured directly in nginx.
However, now nginx is also installed in Docker (swag), so it cannot freely access the entire system's files. If every new application requires mounting a new volume into swag, that would be too much hassle.
So I chose to integrate nginx into the Django docker-compose configuration.
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
This is based on the original DjangoStarter docker-compose configuration with the addition of nginx configuration, using the official nginx image: https://hub.docker.com/_/nginx
nginx.conf
The uwsgi.ini mentioned above is not shown, but there's not much to say about it. The port exposed in it is 8000, so the port specified in the nginx configuration for upstream should correspond to 8000.
upstream django {
ip_hash;
server web:8000; # Docker-compose web service port (i.e., uwsgi port)
}
server {
listen 8001; # Listen on port 8001
server_name localhost; # Can be the IP address of the nginx container or 127.0.0.1, cannot be the host machine's external IP address
charset utf-8;
client_max_body_size 100M; # Limit the size of user-uploaded files
location /static {
alias /code/static_collected; # Static resources path
}
location /media {
alias /code/media; # Media resources, user-uploaded files path
}
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;
As you can see, this nginx configuration embedded in the Django application opens port 8001.
Looking back at the swag reverse proxy configuration above:
set $upstream_app app1_nginx;
set $upstream_port 8001;
set $upstream_proto http;
It corresponds.
After configuring this, run docker compose up to start swag, and then you can access app1.dealiaxy.com~
Full-site HTTPS is very satisfying—the browser no longer warns about insecurity~
Permission Issues
You may notice that in the swag docker-compose configuration, there are two environment variables, PUID and PGID. Swag is pre-configured to use these. By specifying them, the container starts with the designated user and group instead of running as root, which is more secure. Additionally, files mounted with volumes are not owned by root, so the currently logged-in user can modify them without using sudo.
When I added nginx to the Django Docker container, I tried to make it run without root privileges. I followed the official guide and used the nginxinc/nginx-unprivileged image, and also tested passing the user parameter in the docker-compose configuration, but neither seemed to have any effect.
After tinkering for a while, I had to temporarily give up. I'll update if there are any developments later.
Summary
Honestly, this project didn't involve much technical depth—it was just CRUD. As for takeaways, here are a few:
- Got more familiar with some React coding practices
- Got Swagger configured properly, so other servers can follow the same pattern in the future, greatly boosting productivity
References
- LinuxServer.io | Chinese Documentation - https://linuxserver.watercalmx.com/
- Docker Compose Network Setup - https://juejin.cn/post/6844903976534540296
- DaJiangGou's Docker Perfect Deployment of Django Uwsgi+Nginx+MySQL+Redis - https://zhuanlan.zhihu.com/p/145364353
_EOF_
Project Completion Summary - November New Project - Clinic Project
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
项目完成小结:使用Blazor和gRPC开发大模型客户端
其实几年前我就有轻度使用了一下 Blazor 这个技术,一开始使用 Blazor ,我是有点嫌弃的,我还是比较倾向于传统的前后端分离,AspNetCore用来做后端,用 React 做前端,生态很丰富,要做啥组件都容易。这次只是个小 Demo 项目,试用了一下 Blazor ,从一开始的非常别扭,到越来越顺手。感觉 Blazor Server 写小项目还是挺好用的,后面继续完善项目,持续发掘 B
2023-07-14 · 项目开发小结系列 · 约 1 分钟
项目完成小结:使用DjangoStarter v3和Taro开发的微信小程序
后端DjangoStarter v3秒杀开发,ninja API写起来真香。前端Taro做微信小程序?呵呵,微信烂得一坨但居然有护城河?django-filer和tailwindcss还行,分享搞定,就是拖太久没动代码。
2024-09-12 · 项目开发小结系列 · 约 1 分钟
项目小结:使用Docker迁移服务到离线服务器
最近遇到的这个场景,需要把之前开发的一套系统迁移到一个离线的服务器上,这个服务器有点麻烦,接入VPN后通过堡垒机才能访问,速度也很慢,遇到不少坑,本文记录一下迁移过程。
2023-07-12 · 项目开发小结系列 · 约 1 分钟
项目开发小结-8月新项目-校车项目
Model/数据库设计建议 from django.db import models from django.utils import timezone class ModelManager(models.Manager): def get_queryset(self, *args, **kwargs): return super().get_queryset(*args
2022-08-28 · 项目开发小结系列 · 约 1 分钟
讨论
共 0 条加载评论中…