WSL2+vue3+axios配合Django开发环境配置

目前有个需求是这样:要用vue开发前端,django开发后端,web服务器(apache或者nginx或者IIS)提供静态文件的服务,前端采用axios作为通信组件和后端的django通信,并动态更新页面。

所以我的解决方案是前后端通信只有json数据,前端从后端拿到数据后在vue里面更新页面,这样可以不需要后端设计模板、处理字符串拼接之类的任务,减轻后端的负担,前端的计算量对于大多数客户端而言问题不大。

因为我还有很多软件必须在Windows上运行,而windows上搭建Python环境很麻烦,所以为了使用的方法,我使用wsl2内的Linux来搭建Python环境。而windows上有nodejs,而且是完整的,所以就不需要在WSL2内的Linux环境了。所以整体来讲就是windows+nodejs+vue3+vue-router+pinia+axios作为前端开发环境;windows+wsl2+linux+python+django+redis作为后端开发环境

第1步是搭建vue3开发环境

1.1 第一步创建vue3项目

首先在系统内安装nodejs,从nodejs官网下载安装即可。

在vue2时代,官方推荐的是@vue/cli+webpack作为vue2的构建环境。到了vue3时代,官方推荐vite作为构建环境,vue2搭配的@vue/cli实际上已经进入维护模式,而且它所依赖的包也进入了维护模式。所以我也切换到了vite

根据官方的指导

开始 | Vite 官方中文文档

通过nodejs的命令行执行

npm create vite@latest

然后根据提示,一步步选择创建vue3项目即可。在vue3时代,很多前端项目采用typescript了,所以我也使用typescript。

另外vue3的store功能放弃了vuex,而采用pinia,这里也要变更过来。

1.2 多页面配置

因为软件功能比较多,所以必须要做成多页面应用MPA,而不是单页面应用SPA。在

https://github.com/vitejs/vite/issues/257

 的讨论中

https://github.com/vitejs/vite/issues/257#issuecomment-633652167

官方声称不会为vite加入多页面的功能,但是后面vite2加入了多页面的功能

构建生产版本 | Vite 官方中文文档

不过说实在的,官方的功能形同鸡肋。它需要以html作为入口文件,这意味着必须有这些html文件。如果将html文件放置其他目录,那么在访问时需要添加多余的中间目录。与此同时在打包后的文件目录也存在多余的中间目录。如果想要访问方便,那么开发时文件目录的组织就会不方便;反之如果想要开发时文件目录组织方便有序,那么生成的文件的链接和路径就会很繁琐。如果是开发APP的话,问题不大,可是我做的是一个纯web的东西,这个还是比较敏感的,尤其是我希望使用rest风格的地址。

于是有人开发了vite插件使vite支持了多页面,我比较喜欢这2个

vite-plugin-html - npm

以及

vite-plugin-page-html - npm

按照插件的文档

https://github.com/vbenjs/vite-plugin-html/blob/main/README.zh_CN.md

在index.html中添加EJS标签


  
  
  
  <%- title %>
  <%- injectScript %>

要注意的是,这两个插件不能同时使用,因为它们功能上是冲突的。另外它们使用的templete并不一样,并不通用,具体请查看两个插件的readme

然后在vite.config.js中如下配置,就可以生成多页面MPA

import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'

export default defineConfig({
  plugins: [
    createHtmlPlugin({
      minify: true,
      pages: [ // 在这里可以设置多页面
        {
          entry: 'src/main.ts', // 这是主typescript文件的路径
          filename: 'index.html', // 这是生成的html文件的路径,将来build的时候生成的文件会放在这里,如果涉及到跨文件的链接,也会按照这个来生成
          template: 'public/index.html', // 这里是模板的相对路径
          injectOptions: { // EJS的相关配置,因为我主要用vue3,所以没有使用这里的选项
            data: {
              title: 'index',
              injectScript: ``,
            },
            tags: [
              {
                injectTo: 'body-prepend',
                tag: 'div',
                attrs: {
                  id: 'tag1',
                },
              },
            ],
          },
        },
        { // 第2个页面,各选项和第1个页面的类似
          entry: 'src/other-main.ts',
          filename: 'other_foler/other.html',
          template: 'public/other.html',
          injectOptions: {
            data: {
              title: 'other page',
              injectScript: ``,
            },
            tags: [
              {
                injectTo: 'body-prepend',
                tag: 'div',
                attrs: {
                  id: 'tag2',
                },
              },
            ],
          },
        },
      ],
    }),
  ],
})

因为对于不同的页面可以给不同的entry/filename/templete,所以源码目录组织形式(templete的路径位置)和目标的路径组织形式(filename的路径位置)可以灵活安排。不同的页面也可以采用不同的entry file,理论上可以一个页面用vue3,另一个用react17,如果是多人协作项目不排除这种可能。但是我想大多数团队都会选择一致的技术栈,只不过会在不同的页面采用不同的vue3配置,比如说我就会在不同的页面采用不同的local storage配置。

这两个插件的vite.config.js基本上是通用的,可以不用改。

这样在

npm run dev

启动开发服务器时,可以直接通过url访问到不同的页面。

而且在

npm run build

编译最终生成产品时,生成的文件也是按照这个路径来生成的,文件中的链接也是按照这个来生成的。

第2步是搭建django开发环境

2.1 配置物理机使得wsl2能够访问物理机的数据库服务

django需要一个完整的python环境,在windows下,我没有找到完整好用的python环境。Anaconda算是做的比较好的,可是那上面的软件包的兼容性比较差,比如说freecad依赖python_abi-3.8,可是django-redis依赖python_abi-3.9,所以没法同时使用,而我需要同时使用。经过了多轮尝试以后我最后决定使用wsl2。

我用的是wsl2,安装的OpenSuse Tumbleweed。我比较喜欢使用系统自带的rpm或者deb来架设站点,因为这样比较稳定,而Tumbleweed的包都比较新,所以我选择了Tumbleweed。

wls1是直接在本地执行的代码。wls2用的是虚拟机技术,就是在本地系统上创建了一个虚拟机,所以虚拟机的功能更加完备,更详细的请参考微软官方介绍

比较 WSL 1 和 WSL 2 | Microsoft Docs

因为wsl2是虚拟机,所以其实和本地机有不同的ip地址,那么就不能通过localhost来访问,不论是从物理机访问wsl2,还是反过来从wsl2访问物理机。所以需要配置一下网络才能保证物理机和虚拟机之间的网络通讯

postgresql - How to connect to windows postgres Database from WSL - Stack Overflow

在wsl2中执行

ip route

会显示

default via 172.28.80.1 dev eth0
172.28.80.0/20 dev eth0 proto kernel scope link src 172.28.91.18

其中红色标记的IP地址就是在wsl2虚拟机和物理机之间的网桥地址,也就是说wsl2虚拟机通过这个地址来访问物理机。

默认情况下物理机的数据库服务都是只允许localhost访问的,现在必须要增加外部IP地址的访问权限。

进入物理机的PostgreSQL目录,找到pg_hba.conf文件,在里面添加一行

host    all             all             172.0.0.0/8             scram-sha-256

其中IP地址的部分要看前面wsl2里的IP地址,要保证地址段一致。另外PostgreSQL从14版开始,这个配置文件里的md5加密算法更新为了scram-sha-256。

然后还要把PostgreSQL软件中的postgres.exe文件加入物理机防火墙的白名单。

理论上讲,把数据库服务器放在WSL2虚拟机中也没问题。不过OpenSUSE Tumbleweed的数据库一升级就要进行复杂的迁移工作,而且Tumbleweed升级还很频繁,所以我还是把它放在了windows物理机上。

redis服务器的访问也可以采用类似的方法,不过因为redis只是临时存储的,所以如果Tembleweed升级了,那就直接重启服务就行,不需要什么额外的迁移工作。毕竟是开发环境,生产环境另说。所以我就直接用WSL2里面的redis服务就好了。

2.2 在WSL中配置systemd支持,以通过服务的方式运行redis

如果想要从WSL2中访问windows上的redis服务是可以按照上一小节的做法来做的。但是windows平台的redis可执行文件版本远远落后于Linux平台。所以直接在WSL2中运行redis服务,然后让开发的程序直接在WSL2中访问该服务成为一种切实的需求。虽然redis需要大内存,但是开发机仅仅是运行几个案例连接一下redis而已,内存占用量其实不大的,在WSL2中也可以运行的。

因为OpenSUSE启动服务管理器使用的是systemd,而WSL对systemd的支持需要做额外配置。在OpenSUSE中安装WSL支持的相关软件包

zypper in patterns-wsl*

这一步非常重要,如果不安装这个就进入下一步,那么WSL中安装好的OpenSUSE将会崩溃无法启动。

安装好之后编辑或者增加/etc/wsl.conf文件,添加内容

[boot]
systemd=true

 然后重启WSL2,在windows自带的命令行(cmd或者powershell都行),以管理员身份运行

sc stop LxssManager

sc start LxssManager

再重新进入WSL2,这个时候OpenSUSE启动会慢一些。然后

zypper in redis

安装redis软件包,此时服务并没有默认开启。然后以root身份进入/etc/redis目录

su
cd /etc/redis

把案例配置文件复制一份,以此为基础来编辑出正式的配置文件

cp default.conf.example default.conf 

因为是用root来操作的,所以新生成的default.conf的群组权限会有问题,需要调整一下

chgrp redis default.conf

然后编辑/etc/redis/default.conf文件,找到绑定IP的配置,如果不使用ipv6的话,需要把相关配置去除仅保留ipv4的配置,否则会出错。

# bind 127.0.0.1 ::1
bind 127.0.0.1

其他的配置按照开发者自己的喜好即可。然后

sudo systemctl start redis@default

启动redis服务。

如果不使用服务模式,而是使用直接运行redis可执行文件的方式也可以。

redis-server /your/config/filename.conf

不过这就要求这个窗口需要一直打开,一旦关闭,redis就也一并关掉了。

2.2.1 启动init V服务,以支持redis

如果是Debian,启动服务管理使用的是sysV init,而WSL不需要对init进行特殊的配置,直接

service redis-server start

 即可。

本文这个章节只是介绍一下如何让WSL2支持systemd

systemd和sysV init各有优劣,Debian和OpenSUSE也各有优劣,到底如何选择全凭开发者自选。

2.3 设置django项目

安装好wsl2基本系统后,设置好源的国内镜像,把需要的包都装好。然后

django-admin startproject backend

 即可创建新的django项目

python manage.py startapp app_name

就可以在新的django项目中添加应用

因为wls2是通过网桥访问物理机的,所以在django项目的settings.py文件中关于数据库的配置一段需要把HOST设定成网桥对应的IP地址,这样django项目在访问到数据库时才会到物理机上去访问数据库服务。

ALLOWED_HOSTS = ['127.0.0.1', '::1', 'localhost', '172.28.80.1']

根据

python - How can I access the Django server using WSL (Windows Subsystem for Linux)? - Stack Overflow

二楼的回答

运行

python manage.py runserver 0.0.0.0:8000

就可以开启django的开发服务器

django的开发服务器会提示访问地址

http://0.0.0.0:8000/

但是在物理机的浏览器里访问时不可以用这个地址,而是要用

http://localhost:8000/

才能访问django的开发服务器,查看django项目开发效果

注意django的开发服务器的端口,通常来讲和vite开发服务器的端口不同

第3步是配置vue项目使其在访问后台服务器时转发到django的开发服务器

3.1 服务器转发规则

按照vite官方文档

开发服务器选项 | Vite 官方中文文档

其开发服务器可以用作代理服务器,也就是在用户通过浏览器访问vite开发服务器时,如果有任何通信,可以通过vite开发服务器转发到后台的服务器。那么只要把这个后台服务器指定为django的开发服务器自然就可以让vite和django通信了,开发者就可以在一台电脑上同时进行前端和后端的开发。

编辑vite.config.js,加入以下内容

export default defineConfig({
server: {
        host: "127.0.0.1",
        port: 3000,
        proxy: {
            "/api": { // 当访问路径为/api/xxx时,会触发转发
                target: "http://127.0.0.1:8000", // 转发的目标服务器
                changeOrigin: true, // 转发时需要对url进行替换,因为后端服务器的url规范会和前端有所不同
                rewrite: (urlPath) => urlPath.replace(/^\/api/, ""), // 替换规则,这里是直接把/api/xxx替换为/xxx
                // secure: false,
                // ws: true,
            },
        },
    },
})

proxy就是django开发服务器,也就是wsl2网桥的地址。django服务开启后提示的访问地址是127.0.0.1,从物理机访问时如果按照这个地址是无法访问到django服务器的。这样vite的开发服务器就可以和django开发服务器通信了

如果不是在同一台电脑上,比如是两个人分别开发前端和后端,那就把开发后端的那个django开发服务器的地址写到前端的vue.config.js里。此时,还要对后端开发者的django开发服务器做一点调整,就是在settings.py中,把前端开发者的IP加入ALLOWED_HOSTS。这样不需要考虑wsl2的网桥。

ALLOWED_HOSTS = ['127.0.0.1', '::1', 'localhost', 'vite dev server IP']

这个配置是针对开发环境的。本质上vite.config.js文件中关于server的部分其实就是对vite开发服务器的配置,这个开发服务器是web服务器。在生产环境,自然是针对生产环境的web服务器进行配置,静态文件也就是vue的html/css/js文件由web服务器直接处理,动态内容转发到django服务器,如何配置就需要参看具体的web服务器软件了,nginx/Apache/IIS配置方法各不相同

还有文章说在vite中调整配置文件以实现生产环境下跨域,我觉得不太好。如果是一个页面上的静态内容从网关web服务器获得,然后在运行时在ajax中访问别的服务器,这种模式用跨域解决确实可以,不过既然可以通过网关web服务器转发,还是转发比较安全,对于client没有跨域访问自然要安全一些,同时对于django服务器也是一样,如果允许任何IP访问,django服务器直接暴露在外网实在是不安全。毕竟web网关不仅性能比django的要好得多,而且功能也要丰富得多,比如防火墙之类。另外对于CSRF问题,很多老网站是使用HTTP Referer防御的,这更是不好跨域处理。

3.2 前端设置

对于开发服务器而言,为了能够和后端通信,所以才要在url中加入一些明确的标识来告诉vite服务器“需要转发”,而在生产环境则直接通过url访问后端服务器即可。此时要么在网关服务器设置转发规则,要么从前端删除这个标识,我比较喜欢从前端删除这个标识。具体方法很简单,一般来说在前端开发时使用axios,只要

export const httpRequest = axios.create({
    baseURL: "/api",
    xsrfCookieName: CSRF_COOKIE_NAME,
    xsrfHeaderName: CSRF_HEADER_NAME,
    withCredentials: true,
});

设置baseURL就可以自动在每次axios访问时添加这个前缀,这个就可以作为转发的标识。另外直接把Django的settings.py中的CSRF_COOKIE_NAME和CSRF_HEADER_NAME引入到axios中作为xsrfCookieName和xsrfHeaderName就可以保证前后端通信时在CSRF配置上的一致性。在编译生产环境的文件时,直接把baseURL设置为一个空字符串就相当于从前端删除这个标识。

第4步是设置django在某些接口关闭CSRF验证

CSRF是web中一种典型的漏洞,现代化的防御方式是token令牌验证。不过对于某些页面如果加入验证就不对了,最典型的就是登陆页面肯定不应该加验证,一般都是登陆后由后端发送token令牌给前端以便后面的通信,注销或者会话过期后后端删除token令牌。如果不处理那么当vite开发服务器访问django开发服务器时,django服务器就会返回HTTP 403错误,并通过vite开发服务器一直转发到客户端浏览器。

对django而言,只需要在配置文件settings.py的中间件段加入CSRF验证中间件就可以自带token令牌验证防御,以免受到CSFR攻击,较新版本的django在创建新项目时默认都会加

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware', # 就是这行
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

然后在login的函数加上修饰器,就可以在用户访问这个页面时绕开CSRF验证,类似的,在其他不需要验证的位置,也可以这样绕开CSRF验证

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def login(request):
    """
    处理登录内容
    """

关于这个问题的处理,更多内容可以参考我的另外一篇文章

Django中在CBV(基于类的视图)中添加装饰器_django 类装饰器_silent_missile的博客-CSDN博客

第5步是确定前后端通信的Content-Type

为保证前后端顺畅通信,数据发送头信息中的的Content-Type、实际传输的内容、接收方的处理方式这3者必须要匹配,否则就会通信失败

5.1 前端对后端的请求格式

对于GET请求,其实没有什么太多的,直接请求即可

但是对于POST请求常用的有3种类型

第1种

multipart/form-data

一般用于文件上传,如果说有什么特殊的,就是slice了,不过几乎不会和其他的弄混

而另外两种则很可能会弄混

第2种

application/x-www-form-urlencoded

是表单类型,html上form元素的submit会发送内容时会采用这个Content-Type

XMLHttpRequest类send()时默认也是这个Content-Type

一般来讲这个Content-Type兼容性最好

第3种

也就是最后一种则是

application/json

其内部内容是JSON.stringify()输出的格式,也就是含有JSON内容的一个字符串

这种Content-Type不是表单类型,不能和浏览器DOM元素交互,所以仅能用于ajax通信,然后再利用JavaScript来影响DOM。

axios默认是这种Content-Type,而很多web服务程序尤其是年代比较早的,出于通用性的考虑大部分都是用表单格式的,所以很容易出现通信失败

5.2 实际应用

如果前后端双方协议采用表单格式application/x-www-form-urlencoded

首先需要把要发送的数据变为application/x-www-form-urlencoded格式,其内容其实是

"k1=v1&k2=v2..."

看得出来,其实就是GET请求的格式,只不过在POST中作为数据来处理,而不是明文放在头信息的url里。

处理这种格式的一种较好的方法是URLSearchParams(),这个JavaScript函数虽然暂时还没有进入标准,但是各大浏览器——除了IE——都对它支持不错

let sendData= new URLSearchParams()
sendData.append('k1','v1')
sendData.append('k2','v2')

还有一种方法是使用qs,效率要差一点,不过兼容性更好。

import qs from 'qs'

sendData=qs.stringify({
  k1: v1,
  k2: v2
})

axios要这样发送数据

httpRequest({
   method: 'post',
   url: '/django/api',
   headers: {
     'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
   },
   data: sendData
   }).then((data: AxiosResponse): void => {
     console.log(data)
   }).catch((error: AxiosError): void => {
        console.log(error)
    }

其中url是django服务器上的路由识别url,因为vue.config.js中已经设定了django开发服务器的地址和端口,这里就不需要输入了。其实质上相当于访问http://localhost:8000/django/api,也就是把vue.config.js中的proxy和这里的url拼接起来。vue的开发服务器在转发到django开发服务器时,服务器会自动处理把url映射到django的路由上。

headers是POST请求的头信息,里面包含了Content-Type

django接收时数据内容会被存放在request.POST中,以字典格式存放,使用时通过

value = request.POST.get('key')
# 或者
value = request.POST['key']

来获取

在下面这篇文章中

axios 发 post 请求,后端接收不到参数的解决方案_NeverYu-CSDN博客_axios post请求参数

作者提供了好多种方法来解决axios发送数据到后端时后端收不到的问题,其根本思路在于把axios数据格式调整为表单格式application/x-www-form-urlencoded

由于表单一直是http和DOM的一部分,所以早期web开发中大家都是用表单格式application/x-www-form-urlencoded,而且这种惯性伴随着很多后台程序继承沿袭下来,大多数后台系统都是按照这个格式来配置的,而且这个接口可能不是前端开发者能够决定的,只能是前端跟着后端走,所以按照那篇文章来处理也并无不妥。

不过后来伴随着ajax的兴起,这种情况有所变化,JSON格式application/json应用越来越广了。毕竟表单格式的key=>value格式只能嵌套一层,而JSON可以嵌套多层,所携带的数据可以更丰富。而且axios默认是这种格式的。

如果前后端双方协议采用JSON格式application/json

那么发送方不需要做什么特殊处理

      let sendData = {
        username: username,
        password: password
      }
      httpRequest ({
        method: 'post',
        url: '/django/loginapi/',
        headers: {
            "Content-Type": "application/json", // 这是axios的默认配置,不加也可以
        }
        data: sendData
      }).then((response) => {
        console.log(response)
      }).catch((error) => {
        console.log(error)
      })

django接收时,数据不是存放在request.POST中,而是存放在request.body中,而且还需要用json库才能把它读出来

import json

postBody = request.body # 实质上是个字符串
json_result = json.loads(postBody) # json库处理后变为字典格式,内部有嵌套层级

for key in json_result:
    print(key + ':' + json_result[key])

 如果你是用的不是Django自带的request/view,而是使用了djangorestframework的APIView或者其子代,那么前端发过来的这些数据都会被djangorestframework自动处理掉,放在request.data里,开发者不需要关心是在request.POST里还是在request.body里。

djangorestframework提供了很不错的功能,适合内容展示类的站点。但是我的站点不是用于内容展示的,所以我其实没有选择djangorestframework,因为它对各种HTTP method的预设根本无法满足我的需求,而且它会使得框架变得更重,速度降低不少。rest作为一种风格约定,对于简单的内容展示还够用也方便,但是对于更加复杂的应用,其实不太够用,自然也就没有方便可言了。

5.3 前端对后端返回数据的处理

对于从django返回的数据,axios接收的数据可以在then中的实参得到,通过打印到控制台就可以很清楚的看到其内部的数据结构,也都是包含了header等信息的,其中的数据部分是response.data

如果django是通过HttpResponse或者是render返回的数据,Content-Type都是text/html,response.data本质上是符合html规范的一个很长的字符串,可以按照html字符串来处理,最简单的就是用innerHTML

如果django是通过JsonResponse返回的数据,Content-Type就是application/json,response.data本质上是符合JSON规范的一个很长的字符串,前端的JavaScript只要通过

obj=JSON.parse(response.data)

即可得到其数据,也就是可以在axios的then里面处理

如果使用axios的话,axios会自动完成这一步,这个response其实是一个AxiosResponse类。所以直接处理即可

console.log(response.data)

你可能感兴趣的:(vue,web,django,django,vue.js,前端)