跳转至

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.routeserving/{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.routestatic_url_pathhtml 文件中。这个参数的定义为:

  • 当存在 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参数理解

Back to top