毕设踩坑整理
本文最后更新于:2022年3月27日 晚上
说是踩坑,实际上只是整理我在毕设中遇到的一些问题和解决方案,不过不知道以后还有没有再写 flask 的机会……
此外写毕设的一个感触就是,往往构思的时间要比实际写代码的时间要多得多。同时,自己可能由于过分想要避免重复造轮子,而且总是想找到解决某个问题的 best practice,常常过分纠结于工具的选择,而浪费了大量的时间,实际上一些比较小的问题,自己动手去做个适配,或者撸个轮子能够很好 work 就可以了。
CSS tricks
用得最多的应该是margin-left: auto,自动居右
前后端
python 删除目录
可以使用 shutil.rmtree(path),不过缺点是有时你只是想删除目录中的内容,而非将目录本身也删去。虽然可以选择重新创建目录,但是如果目录是在挂载在 Docker 中可能会出现一些问题,所以我的做法是
1 | |
topdown=False是因为rmdir()需要目录为空,可以参考相关文档[1]
前后端下载文件
前端下载文件
1 | |
我在 SO 搜索时看到很多人推荐 file-saver,其对浏览器的兼容性更好,但是据作者所言,其更加适用于 client-side 的文件(即直接在浏览器端生成的文件),如果想要下载后的文件是以后端给出的 filename 命名的话,有一些麻烦。
如果希望下载后的文件是以后端给出的 filename 命名的话,可以在 Response 中指定 content-disposition
1 | |
在这里我一开始遇到了返回的 Response 中读不到content-disposition的问题,这里是由于我的 flask 使用了 flask_cors 来进行跨域设置,而该 header 默认未被暴露,需要cors.init_app(app, expose_headers=["Content-Disposition"])进行设置
而在前端则使用正则来获取文件名
1 | |
流式下载
如果提供的文件比较大,可能很难全部 load 到内存中(像data = f.readlines()那样),flask 提供了 Streaming Content 的支持[2],这边又涉及到如何 line by line 的去读一个大文件,这边参考该回答[3]
1 | |
也可以通过 chunkSize 进行更细粒度的控制,而前端的代码其实没有什么要改的。
一般来说,我们时常遇到的下载方式分为两种:
- 直接访问服务器的文件地址,自动下载文件
- 返回 blob 文件流,再对文件流进行处理和下载
在这里使用了后者,这种方式的缺陷是用户无法感知文件流的传输状态,会造成一些困扰(无法确定当前下载操作是否生效),尤其是在下载的文件比较大,不能很快下载完的情况下。
这时可以设置 axios 的 onDownloadProgress 回调来进行处理
1 | |
这边使用到了 progressEvent.total,在 flask 返回的 Response 需要添加content-length该 header, 否则前端获取到的 total 值会为 0,在 python 中可以使用os.path.getsize(file_path)得到文件大小
file-saver 的作者还有构建了个 StreamSaver,在该篇回答中有所提及[4]
文件下载后删除
前面提到我使用了 blob 文件流,是因为下载文件是接受到用户请求后,在服务器上对一些文件打包成 zip 生成的,在下载结束后需要继续删除,我参考了这篇回答[5],由于我正好使用了 Streaming Content, 所以在generate()中在读取后进行删除即可
1 | |
当然我也认为,最后所提到的,使用 APScheduler 或者 Celery 这样的异步定时任务清理更加 elegant 和 robust
一些补充
@davidism 在他的回答[6]中提到了静态文件可以使用send_from_directory()以及支持 X-SendFile 的如 Nginx 这样的服务器,不过尽管项目发送给用户的 zip 文件会实际生成在临时文件夹中,但是总归要删除的,而且大概率不会被多次请求,不知道 Nginx 对它进行缓存是否会成为一种浪费
Axios Network Error
前面提到了引入 progress 的一个原因就是不知道 blob 文件流下载操作是否生效,我在项目编写中遇到过这样的一个问题, flask 后端显示的状态码是 200,但是浏览器 console 中看到的是 ERR_INVALID_RESPONSE或是ERR_CONNECTION_RESET, axios 的 issues 有个讨论帖,可惜其中的方法目前对于我来说并不 work, 所以我还是通过加个进度提醒,期望用户看到操作无反应后,多尝试几次。
容器化
在开发时,使用flask run会自动加载项目目录下的.env和.flaskenv;在考虑部署时,flask 自带的服务器不适用于生产环境,往往需要使用 gunicron 或者 uwsgi 这样的 WSGI Server.在非容器化部署时,应用并不会自动加载环境文件(因为此时使用的不再是flask run),则需要在项目启动的文件(常规命名是app.py或wsgi.py)中,给create_app()传入production作为参数,并利用 dotenv 的load_dotenv()手动加载环境文件。
但是在容器化部署时,可以将环境变量文件直接传递给容器进行加载,相对来说更为方便一点。
参考
- https://docs.python.org/3/library/os.html#os.walk] ↩
- https://flask.palletsprojects.com/en/2.0.x/patterns/streaming/ ↩
- https://stackoverflow.com/a/8009942/11381693 ↩
- https://stackoverflow.com/a/39685380/11381693 ↩
- https://stackoverflow.com/a/24613980/11381693 ↩
- https://stackoverflow.com/a/24318158/11381693 ↩
- https://github.com/axios/axios/issues/1827 ↩