• 项目完成小结 - Django-React-Docker-Swag部署配置


    前言#

    最近有个项目到一段落,做个小结记录。

    内容可能会多次补充,在博客上实时更新哈~

    如果是在公众号阅读这篇文章,可以点击「查看原文」访问最新版本~

    这个项目是前后端分离,后端为了快,依然用我的DjangoStarter框架。前端一开始是小程序,后面突然换成公众号H5的形式,还好我用了Taro,大差不差。

    不过Taro目前没啥好用成熟的组件库,前一个项目本来用着Taroify,不过用了一半项目还没做完,Taroify的作者就跑路不维护了~ 虽然但是,还是能用,把旧项目的一些代码复用一下,也不是不行。

    总体的开发体验就是很一般,虽说React写前端舒服多了,但组件库实在是拉胯… 如果下个项目依然要用Taro的话,估计得试试新出的NutUI-React了。

    说回正题,这次我从Web开发、部署这几方面对这个项目做个小总结。

    后端#

    后端用DjangoStarter模板,自从我上次升级了v2版本之后,还没实战过,这次项目上使用了,还是稳得一批,(所以点star的同学可以放心用哈~)

    之前oauth部分只有企业微信,微信登录还是todo,这次因为接入公众号,我顺手也把微信登录做了,其实跟企业微信基本没啥区别。

    "双标"的ModelViewSet#

    drf的ModelViewSet,可以快速生成crud接口,不过默认权限控制很粗糙,只能选择三种:

    • 已登录可访问
    • 管理员可访问
    • 任何人可访问

    但正常的场景是,假设有个文章接口,用户只能管理自己写的文章,管理员可以访问全部文章。也就是要对不同角色的用户区别对待~

    要实现的话可以这样,重写 ModelViewSetget_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
    }
    

    父组件使用的时候

    
    

    日历组件向父组件传值,也就是触发事件

    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,遇到挺多坑的,也查了很多资料,最终完美搞定~

    很多时候虽然文档很齐备,但因为各种条件不一致,很难一下子搞起来。

    首先在域名控制台添加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.comns2.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配置里面有俩环境变量,PUIDPGID,swag内部都配置好了,指定了这俩,容器启动的时候就会以指定的用户和用户组运行,而不是默认以root运行,这样会安全一些,而且挂载了 volume 出来的文件,也不是root权限,当前登录用户不用 sudo 就能修改。

    在 django 的docker里加入nginx的时候我有尝试改成不用root运行,根据官方指引使用了 nginxinc/nginx-unprivileged 这个镜像,也测试了在docker-compose配置里传入 user 参数,好像都没什么效果。

    折腾了半天只好暂时放弃,后续有进展再继续更新。

    小结#

    这次项目说实在的没啥技术含量,CRUD罢了,收获的话就一点点:

    • 又熟悉了一些react的写法
    • 把swag配好了,以后其他服务器可以依样画葫芦,极大提高生产力

    参考资料#

  • 相关阅读:
    微信小程序
    如何全局引入js生成的scss代码
    Android 按上/下键,焦点会移动到第一个控件上面或最后一个控件下面的解决办法
    JavaScript对象方法
    EE trade:炒伦敦金的注意事项及交易指南
    SPA项目开发之首页导航+左侧菜单
    【datawhale202206】pyTorch推荐系统:多任务学习 ESMM&MMOE
    JDBC详解
    图解 STP
    基于极化码(Polar Code)的加密
  • 原文地址:https://www.cnblogs.com/deali/p/16961771.html