Flask 加载不出 js 等静态文件
这个问题一共出现了两次:
- 测试:在云平台服务器上的
localhost
端口测试的时候 - 部署:部署到云平台上的 API 的时候
测试
问题描述
当前文件夹目录如下:
.
|-- templates
|-- index.css
`-- index.html
`-- server.py
运行 server.py
这个文件,使用 Flask 的 render_template("index.html", base_dir=base_dir)
将 index.html
加载出来,会发现在 index.html
中引用的 <link rel="stylesheet" type='text/css' href="{{base_dir}}/index.css">
加载不出来,即报 404 错误,找不到文件,如下所示:
127.0.0.1 - - [17/Mar/2022 09:34:53] "GET /index.css HTTP/1.1" 404 -
但是使用浏览器直接打开 index.html
是正常的。这是因为当我们直接使用浏览器打开 index.html
时,并没有服务端和客户端,浏览器作为一个工具会根据本地文件中的“相对链接”在文件系统中进行搜索。而当使用 Flask 启动项目之后,程序是一个 Browser/Server 的架构。用户访问某个页面,并不是直接拿到所有的信息,而是通过若干次发包和得到回复包,再将结果渲染到本地浏览器上。以本次报错为例,浏览器通过地址请求到 index.html
,随后通过解析 index.html
,发现缺少 index.css
文件,浏览器继续发送请求,获取 index.css
文件,此时浏览器的请求路径从 index.html
中获得,所以如果 index.html
中写的是相对路径,即 <link rel="stylesheet" type='text/css' href="{{base_dir}}/index.css">
,那么浏览器将会在服务器的 home 目录下找 index.css
文件,显然是找不到的,因为相对于 home 目录,此时 index.css
的路径是 /templates/index.css
而不是 /index.css
。
解决方法
暴力的解决方法是将 <link rel="stylesheet" type='text/css' href="{{base_dir}}/index.css">
修改成 <link rel="stylesheet" type='text/css' href="{{base_dir}}/templates/index.css">
,但是考虑到文件夹的有序性,更推荐如下做法。
创建静态文件夹 static
,将 index.css
移到 static 下,即
.
|-- static
`-- index.css
|-- templates
`-- index.html
`-- server.py
并将 <link rel="stylesheet" type='text/css' href="{{base_dir}}/index.css">
替换成 <link rel="stylesheet" type='text/css' href="{{base_dir}}/static/index.css">
。
更多:关于 Flask 默认静态文件夹,可参考 Flask 中文文档 (2.0.2) 。
部署
当将项目部署到云平台时,即 Fork 项目之后部署在线推理,运行部署好的项目,我们会获得到一个外网可以访问的 API 地址,以实现快速部署和访问我们在云服务器上测试的模型。这个 API 地址默认格式为 https://gateway.platform.oneflow.cloud/serving/{id}/v1/index
。
首先注意到 serving/{id}/
这部分,可以称之为 哈希值,它的存在是因为云平台部署的路由分两层:
- 用户请求发送到 flow 的第一层网关,第一层网关通过分析 url 的哈希值来确定项目来源,也即提供服务的服务器;
- 确定项目来源之后,根据其后的后缀
v1/index
到服务器上请求对应的服务
哈希值的存在带来两个问题:
- 问题一:该 API 地址到服务器上请求服务时,
@app.route
是serving/{id}/v1/index
; - 问题二:请求静态文件时,相对于具体服务器而言,
Flask(__name__)
默认静态文件夹目录为/static
,当请求/static/index.css
时应当发送/static/index.css
,但是云平台发送静态文件请求时的 url 为/serving/{id}/static/index.css
可以通过环境变量 BASE_DIR
的形式把哈希前缀传到服务端代码以解决上述问题。代码示例:
import os
from flask import Flask, render_template
base_dir = os.environ.get("BASE_DIR", "")
app = Flask(__name__, static_url_path=base_dir+'/static') # 解决问题二
@app.route(f"{base_dir}/v1/index", methods=["GET"]) # 解决问题一
def home():
return render_template("index.html", base_dir=base_dir) # 解决问题二
if __name__ == "__main__":
app.run("0.0.0.0")
如上代码所示,我们增加了参数 base_dir
,Flask 将 base_dir
传到 @app.route
、static_url_path
、 html 文件中。这个参数的定义为:
- 当存在
BASE_DIR
时,即在云服务器上部署时,获取到BASE_DIR
即上文所说的serving/{id}/
- 否则
base_dir
设置为空
base_dir
在 html 文件的使用示例如下:
<link rel="stylesheet" type='text/css' href="{{base_dir}}/static/css/index.css">
此处我们会发现,针对问题二,我们不仅将 static_url_path
值更改了,还将 html 中的请求路径(如 href
)更改了,那不同时更改这两个路径不也能对应上静态文件吗?其实是不可以的。因为我们需要通过哈希值定位提供服务的服务器,即实现第一层网关,所以哈希值是必须存在的,href
必须带上哈希前缀才能实现第一层网关,而正是由于 href
带有哈希前缀,因此需要更改 static_url_path
。
最后,注意这个 API 默认格式为 https://gateway.platform.oneflow.cloud/serving/{id}/v1/index
,这意味着在 Flask 模块,我们需要将我们想被访问到的页面设置为 @app.route(f"{base_dir}/v1/index", ……)
,否则可能会对用户产生困扰。当然,也可以自定义 url。例如如果你添加了页面 A 到 @app.route(f"{base_dir}/", ……)
,直接访问 https://gateway.platform.oneflow.cloud/serving/{id}
可以得到页面 A。
更多:关于
static_url_path
,可参考 Flask 创建app 时候传入的 static_folder 和 static_url_path参数理解