返回介绍

Flask 入门

发布于 2025-04-20 18:52:14 字数 18247 浏览 0 评论 0 收藏

安装 Flask

我们先安装 Flask:

(venv)> pip install Flask

为节省篇幅,之后“(venv)”这个前缀都省略掉。如无特殊说明,当前目录都是指/home/ubuntu/web_develop。

从 Hello World 开始

我们从最小的应用开始:

1 # coding=utf-8
2 from flask import Flask
3
4 app=Flask(__name__)
5
6
7 @app.route('/')
8 def hello_world():
9     return 'Hello World!'
10
11
12 if__name__=='__main__':
13    app.run(host='0.0.0.0', port=9000)

启动它:

> python chapter3/section1/hello.py
*Running on http://0.0.0.0:9000/(Press CTRL+C to quit)

打开浏览器,访问“http://127.0.0.1:9000/”,就可以看到熟悉的“Hello World!”了。

我们来深入地按行解析这段代码及其背后发生的事情。

  • 第 1 行,“# coding=utf-8”是声明 Python 源文件编码的语法。该编码信息后续会被 Python 解析器用于解析源文件。如果没有特殊的原因,应该统一地使用 utf-8,而不要使用 gb18030,gb2312 等类型。为节省篇幅,之后的实例都不再写出这个声明。
  • 第 2 行,引入 Flask 类,Flask 类实现了一个 WSGI 应用。
  • 第 4 行,app 是 Flask 的实例,它接收包或者模块的名字作为参数,但一般都是传递__name__。让 flask.helpers.get_root_path 函数通过传入这个名字确定程序的根目录,以便获得静态文件和模板文件的目录。
  • 第 7~9 行,使用 app.route 装饰器会将 URL 和执行的视图函数的关系保存到 app.url_map 属性上。处理 URL 和视图函数的关系的程序就是路由,这里的视图函数就是 hello_world。
  • 第 12 行,使用这个判断可以保证当其他文件引用这个文件的时候(例如“from hello import app”)不会执行这个判断内的代码,也就是不会执行 app.run 函数。
  • 第 13 行,执行 app.run 就可以启动服务了。默认 Flask 只监听虚拟机的本地 127.0.0.1 这个地址,端口为 5000。而我们对虚拟机做的端口转发端口是 9000,所以需要指定 host 和 port 参数,0.0.0.0 表示监听所有地址,这样就可以在本机访问了。服务器启动后,会调用 werkzeug.serving.run_simple 进入轮询,默认使用单进程单线程的 werkzeug.serving.BaseWSGIServer 处理请求,实际上还是使用标准库 Base-HTTPServer.HTTPServer,通过 select.select 做 0.5 秒的“while True”的事件轮询。当我们访问“http://127.0.0.1:9000/”,通过 app.url_map 找到注册的“/”这个 URL 模式,就找到了对应的 hello_world 函数执行,返回“Hello World!”,状态码为 200。如果访问一个不存在的路径,如访问“http://127.0.0.1:9000/a”,Flask 找不到对应的模式,就会向浏览器返回“Not Found”,状态码为 404。

这里需要说明的是,默认的 app.run 的启动方式只适合调试,不要在生产环境中使用,生产环境应该使用 Gunicorn 或者 uWSGI。

其他的 werkzeug 自带类型还包括 ThreadedWSGIServer 和 ForkingWSGIServer。

如果想让服务停止,可以发送终止信号或者按 Ctrl-C 键。

配置管理

复杂的项目需要配置各种环境。如果设置项很少,可以直接硬编码进来,比如下面的方式:

app=Flask(__name__)
app.config['DEBUG']=True

app.config 是 flask.config.Config 类的实例,继承自 Python 内置数据结构 dict,所以可以使用 update 方法:

app.config.update(
    DEBUG=True,
    SECRET_KEY='...'
)

app.config 内置的全部配置变量可以参看 Builtin Configuration Values(http://bit.ly/28UUgW3 )。如果设置选项很多,想要集中管理设置项,应该将它们存放到一个文件里面。app.config 支持多种更新配置的方式。假设现在有个叫作 settings.py 的配置文件,其中的内容如下:

A=1

可以选择如下三种方式加载。

1.通过配置文件加载。

app.config.from_object('settings') #通过字符串的模块名字
#或者引用之后直接传入模块对象
import settings
app.config.from_object(settings)

2.通过文件名字加载。直接传入文件名字,但是不限于只使用.py 后缀的文件名。

app.config.from_pyfile('settings.py', silent=True) #默认当配置文件不存在时
     会抛出异常,使用 silent=True 的时候只是返回 False,但不会抛出异常

3.通过环境变量加载。这种方式依然支持 silent 参数,获得路径后其实还是使用 from_pyfile 的方式加载。

> export YOURAPPLICATION_SETTINGS='settings.py'
app.config.from_envvar('SETTINGS')

调试模式

虽然 app.run 这样的方式适用于启动本地的开发服务器,但是每次修改代码后都要手动重启的话,既不方便也不够优雅。如果启用了调试模式,服务器会在代码修改后自动重新载入,并在发生错误时提供一个能获得错误上下文及可执行代码的调试页面。

有两种途径来启用调试模式。

1.直接在应用对象上设置

app.debug=True
app.run()

2.作为 run 的参数传入

app.run(debug=True)

需要注意,开启调试模式会成为一个巨大的安全隐患,因此它绝对不能用于生产环境中。

Werkzeug 从 0.11 版本开始默认启用了 PIN(全称 Personal Identification Number)码的身份验证,旨在让调试环境下的攻击者更难利用调试器。启动程序时可以看到类似的启动提示:

> python chapter3/section1/debug.py
*Running on http://0.0.0.0:9000/(Press CTRL+C to quit)
*Restarting with stat
*Debugger is active!
*Debugger pin code:146-867-947

当程序有异常而进入错误堆栈模式,第一次点击某个堆栈想查看对应变量的值的时候,浏览器会弹出一个要求你输入这个 PIN 值的输入框。这个时候需要在输入框中输入 146-867-947,然后确认,Werkzeug 会把这个 PIN 作为 cookie 的一部分存起来(失效时间默认是 8 小时),失效之前不需要重复输入。而这个 PIN 码攻击者是无法知道的。

当然,也可以使用指定 PIN 码的值:

> WERKZEUG_DEBUG_PIN=123 python chapter3/section1/debug.py

动态 URL 规则

URL 规则可以添加变量部分,也就是将符合同种规则的 URL 抽象成一个 URL 模式,如/item/1/、/item/2/、/item/3/......假如不抽象,我们就得这样写:

@app.route('/item/1/')
@app.route('/item/2/')
@app.route('/item/3/')
    def item(id):
        return 'Item:{}'.format(id)

正确的用法是:

@app.route('/item/<id>/')
def item(id):
    return 'Item:{}'.format(id)

尖括号中的内容是动态的,凡是匹配到/item/前缀的 URL 都会被映射到这个路由上,在内部把 id 作为参数而获得。

它使用了特殊的字段标记<variable_name>,默认类型是字符串。如果需要指定参数类型需要标记成<converter:variable_name>这样的格式,converter 有下面几种。

  • string:接受任何没有斜杠“/”的文本(默认)。
  • int:接受整数。
  • float:同 int,但是接受浮点数。
  • path:和默认的相似,但也接受斜杠。
  • uuid:只接受 uuid 字符串。
  • any:可以指定多种路径,但是需要传入参数。
@app.route('/<any(a, b):page_name>/')

访问/a/和访问/b/都符合这个规则,/a/对应的 page_name 就是 a。

如果不希望定制子路径,还可以通过传递参数的方式。比如/people/?name=a,/peo-ple/?name=b,这样就可以通过“name=request.args.get('name')”获得传入的 name 值。

如果使用 POST 方法,表单参数需要通过 request.form.get('name') 获得。

自定义 URL 转换器

Reddit 可以通过在 URL 中用一个加号(+)隔开各个社区名字,方便同时查看来自多个社区的帖子。比如访问“http://reddit.com/r/flask+lisp”,就可以同时看 flask 和 lisp 两个社区的帖子。我们自定义一个转换器来实现这个功能,它还可以设置所使用的分隔符,不一定要用加号“+”。

import urllib

from flask import Flask
from werkzeug.routing import BaseConverter

app=Flask(__name__)


class ListConverter(BaseConverter):

    def__init__(self, url_map, separator='+'):
        super(ListConverter, self).__init__(url_map)
        self.separator=urllib.unquote(separator)
    
    def to_python(self, value):
        return value.split(self.separator)
    
    def to_url(self, values):
        return self.separator.join(BaseConverter.to_url(value)
                                   for value in values)


app.url_map.converters['list']=ListConverter


@app.route('/list1/<list:page_names>/')
def list1(page_names):
    return 'Separator:{}{}'.format('+', page_names)


@app.route('/list2/<list(separator=u"|"):page_names>/')
def list2(page_names):
    return 'Separator:{}{}'.format('|', page_names)

这样我们访问“/list1/a+b/”和“/list2/a|b/”就能实现同样的功能了。自定义转换器需要继承至 BaseConverter,要设置 to_python 和 to_url 两个方法。

  • to_python:把路径转换成一个 Python 对象。
  • to_url:把参数转换成为符合 URL 的形式。

HTTP 方法

HTTP 有多个访问 URL 方法,默认情况下,路由只回应 GET 请求,但是通过 app.route 装饰器传递 methods 参数可以改变这个行为:

@app.route('/login', methods=['GET', 'POST'])
@app.route('/j/item/<id>', methods=['DELETE', 'POST'])

如果存在 GET,那么也会自动地添加 HEAD 方法,无须干预。它会确保遵照 HTTP RFC(描述 HTTP 协议的文档)(http://bit.ly/2932IiA )处理 HEAD 请求,所以你完全可以忽略这部分的 HTTP 规范。从 Flask 0.6 起,它也实现了 OPTIONS 的自动处理。

下面简要介绍 HTTP 方法和使用场景。

  • GET:获取资源,GET 操作应该是幂等的。
  • HEAD:想要获取信息,但是只关心消息头。应用应该像处理 GET 请求一样来处理它,但是不返回实际内容。
  • POST:创建一个新的资源。
  • PUT:完整地替换资源或者创建资源。PUT 操作虽然有副作用,但应该是幂等的。
  • DELETE:删除资源。DELETE 操作有副作用,但也是幂等的。
  • OPTIONS:获取资源支持的所有 HTTP 方法。
  • PATCH:局部更新,修改某个已有的资源。

幂等表示在相同的数据和参数下,执行一次或多次产生的效果是一样的。

唯一 URL

Flask 的 URL 规则基于 Werkzeug 的路由模块。这个模块背后的思想是基于 Apache 以及更早的 HTTP 服务器的主张,希望保证优雅且唯一的 URL。

举个例子:

@app.route('/projects/')
def projects():
    return 'The project page'

上述例子很像一个文件系统中的文件夹,访问一个结尾不带斜线的 URL 会被重定向到带斜线的规范的 URL 上去,这样也有助于避免搜索引擎索引同一个页面两次。

再看一个例子:

@app.route('/about')
def about():
    return 'The about page'

URL 结尾不带斜线,很像文件的路径,但是当访问带斜线的 URL(/about/)会产生一个 404“Not Found”错误。

构造 URL

用 url_for 构建 URL,它接受函数名作为第一个参数,也接受对应 URL 规则的变量部分的命名参数,未知的变量部分会添加到 URL 末尾作为查询参数。构建 URL 而不选择直接在代码中拼 URL 的原因有两点:在未来有更改的时候只需要一次性修改 URL,而不用到处去替换;URL 构建会转义特殊字符和 Unicode 数据,这些工作不需要我们自己处理。

感受下面这个例子:

from flask import Flask, url_for
app=Flask(__name__)


@app.route('/item/1/')
def item(id):
    pass


with app.test_request_context():
    print url_for('item', id='1')
    print url_for('item', id=2, next='/')

test_request_context 帮助我们在交互模式下产生请求上下文。

执行它:

> python chapter3/section1/url.py
/item/1/?id=1
/item/1/?id=2&next=%2F

跳转和重定向

跳转(状态码 301)多用于旧网址在废弃前转向新网址以保证用户的访问,有页面被永久性移走的概念。重定向(状态码 302)表示页面是暂时性的转移。但是也不建议经常性使用重定向。在 Flask 中它们都是通过 flask.redirect 实现的:

redirect(location) #默认是 302
redirect(location, code=301) #通过 code 参数可以指定状态码

Flask 还支持 303、305、307 重定向,但是较少被用到。

基于前面所讲的内容,我们来看一个更全面的例子。首先是存放配置的 config.py:

DEBUG=False

try:
    from local_settings import*
except ImportError:
    pass

local_settings.py 文件是可选存在的,它不进入版本库。这是常用的通过本地配置文件重载版本库配置的方式。

基于上面所讲的内容,我们看一个更复杂的应用(simple.py):

1 from flask import Flask, request, abort, redirect, url_for
2
3 app=Flask(__name__)
4 app.config.from_object('config')
5
6
7 @app.route('/people/')
8 def people():
9      name=request.args.get('name')
10     if not name:
11         return redirect(url_for('login'))
12     user_agent=request.headers.get('User-Agent')
13     return 'Name:{0};UA:{1}'.format(name, user_agent)
14
15
16 @app.route('/login/', methods=['GET', 'POST'])
17 def login():
18     if request.method=='POST':
19         user_id=request.form.get('user_id')
20         return 'User:{}login'.format(user_id)
21     else:
22         return 'Open Login page'
23
24
25 @app.route('/secret/')
26 def secret():
27     abort(401)
28     print 'This is never executed'
29
30
31 if__name__=='__main__':
32     app.run(host='0.0.0.0', port=9000, debug=app.debug)

这个例子有如下细节。

  • 第 7 行,访问/people 的请求会被 301 跳转到/people/上,保证了 URL 的唯一性。
  • 第 12 行,request.headers 存放了请求的头信息,通过它可以获取 UA 值。
  • 第 18 行,request.method 的值就是请求的类型。
  • 第 27 行,执行 abort(401) 会放弃请求并返回错误代码 401,表示禁止访问。之后的语句永远不会被执行。
  • 第 32 行,能使用 debug=app.debug 是因为 flask.config.ConfigAttribute 在 app 中做了配置的代理,目前存在的配置代理项有:
app.debug->DEBUG
app.testing->TESTING
app.secret_key->SECRET_KEY
app.session_cookie_name->SESSION_COOKIE_NAME
app.permanent_session_lifetime->PERMANENT_SESSION_LIFETIME
app.use_x_sendfile->USE_X_SENDFILE
app.logger_name->LOGGER_NAME

上面例子中的 app.debug 其实就是 app.config['DEBUG']。

响应

视图函数的返回值会被自动转换为一个响应对象,转换的逻辑如下:

  • 如果返回的是一个合法的响应对象,它会从视图直接返回。
  • 如果返回的是一个字符串,会用字符串数据和默认参数创建以字符串为主体,状态码为 200,MIME 类型是 text/html 的 werkzeug.wrappers.Response 响应对象。
  • 如果返回的是一个元组,且元组中的元素可以提供额外的信息。这样的元组必须是(response, status, headers) 的形式,但是需要至少包含一个元素。status 值会覆盖状态代码,headers 可以是一个列表或字典,作为额外的消息头。
  • 如果上述条件均不满足,Flask 会假设返回值是一个合法的 WSGI 应用程序,并通过 Response.force_type(rv, request.environ) 转换为一个请求对象。

下面的视图函数:

@app.errorhandler(404)
def not_found(error):
    return render_template('error.html'), 404

可以改成如下显式地调用 make_response 的方式:

@app.errorhandler(404)
def not_found(error):
    resp=make_response(render_template('error.html'), 404)
    return resp

第二种方法很灵活,可以添加一些额外的工作,比如设置 cookie、头信息等。

API 都是返回 JSON 格式的响应,需要包装 jsonify。可以抽象一下,让 Flask 自动帮我们做这些工作(app_response.py):

from flask import Flask, jsonify
from werkzeug.wrappers import Response
app=Flask(__name__)


class JSONResponse(Response):
    @classmethod
    def force_type(cls, rv, environ=None):
        if isinstance(rv, dict):
            rv=jsonify(rv)
        return super(JSONResponse, cls).force_type(rv, environ)

app.response_class=JSONResponse


@app.route('/')
def hello_world():
    return{'message':'Hello World!'}


@app.route('/custom_headers')
def headers():
    return{'headers':[1, 2, 3]}, 201, [('X-Request-Id', '100')]

if__name__=='__main__':
    app.run(host='0.0.0.0', port=9000)

启动它之后,就可以在另一个终端看看自定义头信息的效果了。看效果之前,先安装 httpie (https://github.com/jkbrzt/httpie ):

> pip install httpie

httpie 是一个使用 Python 编写的,提供了语法高亮、JSON 支持,可以替代 curl 的工具,它也可方便地集成到 Python 项目中。本书绝大多数命令行下请求数据时都使用它。

现在请求/custom_headers:

> http http://0.0.0.0:9000/custom_headers
HTTP/1.0 201 CREATED
Content-Length:44
Content-Type:application/json
Date:Thu, 26 May 2016 17:34:54 GMT
Server:Werkzeug/0.11.10 Python/2.7.11+
X-Request-Id:100

{
    "headers":[
        1,
        2,
        3
    ]
}

视图中也可以直接指定状态字符串,如使用'201 CREATED'替代数字的 201。

静态文件管理

Web 应用大多会提供静态文件服务以便给用户更好的访问体验。静态文件主要包含 CSS 样式文件、JavaScript 脚本文件、图片文件和字体文件等静态资源。Flask 也支持静态文件访问,默认只需要在项目根目录下创建名字为 static 的目录,在应用中使用“/static”开头的路径就可以访问。但是为了获得更好的处理能力,推荐使用 Nginx 或者其他 Web 服务器管理静态文件。

不要直接在模板中写死静态文件路径,应该使用 url_for 生成路径。举个例子:

url_for('static', filename='style.css')

生成的路径就是“/static/style.css”。当然,我们也可以定制静态文件的真实目录:

app=Flask(__name__, static_folder='/tmp')

那么访问“http://localhost:9000/static/style.css”,也就是访问/tmp/style.css 这个文件。

即插视图

即插视图的灵感来自 Django 的基于类而不是函数的通用视图方式,这样的视图就可以支持继承了。视图类型有两种类型。

标准视图

标准视图需要继承 flask.views.View,必须实现 dispatch_request。看一个例子(app_view.py):

from flask import Flask, request, render_template
from flask.views import View

app=Flask(__name__, template_folder='../../templates')


    class BaseView(View):
        def get_template_name(self):
            raise NotImplementedError()

        def render_template(self, context):
            return render_template(self.get_template_name(),**context)

        def dispatch_request(self):
            if request.method !='GET':
                return 'UNSUPPORTED!'
            context={'users':self.get_users()}
            return self.render_template(context)


    class UserView(BaseView):

        def get_template_name(self):
            return 'chapter3/section1/users.html'

        def get_users(self):
            return [{
                'username':'fake',
                'avatar':'http://lorempixel.com/100/100/nature/'
        }]


    app.add_url_rule('/users', view_func=UserView.as_view('userview'))

    if__name__=='__main__':
        app.run(host='0.0.0.0', port=9000)

模板存放在~/web_develop/templates 下,使用__name__来获取模板目录,template_folder 是相对于 app.py 文件的,需要设置成'../../templates'才能找到正确的模板目录。

基于调度方法的视图

flask.views.MethodView 对每个 HTTP 方法执行不同的函数(映射到对应方法的小写的同名方法上),这对 RESTful API 尤其有用。看一个例子(app_api.py):

from flask import Flask, jsonify
from flask.views import MethodView

app=Flask(__name__)


class UserAPI(MethodView):

    def get(self):
        return jsonify({
            'username':'fake',
            'avatar':'http://lorempixel.com/100/100/nature/'
        })

    def post(self):
        return 'UNSUPPORTED!'


app.add_url_rule('/user', view_func=UserAPI.as_view('userview'))

if__name__=='__main__':
    app.run(host='0.0.0.0', port=9000)

通过装饰 as_view 的返回值来实现对于视图的装饰功能,常用于权限的检查、登录验证等:

def user_required(f):
    def decorator(*args,**kwargs):
        if not g.user:
            abort(401)
        return f(*args,**kwargs)
    return decorator

view=user_required(UserAPI.as_view('users'))
app.add_url_rule('/users/', view_func=view)

从 Flask 0.8 开始,还可以通过在继承 MethodView 的类中添加 decorators 属性来实现对视图的装饰:

class UserAPI(MethodView):
    decorators=[user_required]

蓝图

蓝图(Blueprint)实现了应用的模块化,使用蓝图让应用层次清晰,开发者可以更容易的开发和维护项目。蓝图通常作用于相同的 URL 前缀,比如/user/:id、/user/profile 这样的地址,都以/user 开头,那么它们就可以放在一个模块中。看一个最简单的示例(user.py):

from flask import Blueprint

bp=Blueprint('user',__name__, url_prefix='/user')


@bp.route('/')
def index():
    return 'User"s Index page'

每个模块都会暴露一个全局变量 bp。再看主程序(app_bp.py):

from flask import Flask
import user

app=Flask(__name__)
app.register_blueprint(user.bp)


if__name__=='__main__':
    app.run(host='0.0.0.0', port=9000)

使用 register_blueprint 注册模块,如果想去掉模块只需要去掉对应的注册语句即可。

子域名

现在许多 SaaS 应用为用户提供一个子域名来访问,可以借助 subdomain 来实现同样的功能(app_subdomain.py):

from flask import Flask, g

app=Flask(__name__)
app.config['SERVER_NAME']='example.com:9000'


@app.url_value_preprocessor
def get_site(endpoint, values):
    g.site=values.pop('subdomain')
@app.route('/', subdomain='<subdomain>') 
def index():
    return g.site


if__name__=='__main__':
    app.run(host='0.0.0.0', port=9000)

在虚拟机上绑定一下域名,也就是在/etc/hosts 添加一行:

127.0.0.1 a.example.com b.example.com

现在验证它:

> http http://b.example.com:9000--print b # b 表示只输出响应的主体
b

命令行接口

在 Flask 0.11 之前,启动的应用的端口、主机地址以及是否开启 DEBUG 模式,都需要在代码中明确指定,一个比较好的方式是使用第三方扩展 Flask-Script 管理。从 Flask 0.11 开始, Flask 集成了 Click,现在可以直接在命令行直接执行 flask 命令启动 Flask 应用了:

> export FLASK_APP=chapter3/section1/hello.py
> export FLASK_DEBUG=1
> flask run-h 0.0.0.0-p 9000

这种命令行启动的方式要灵活得多,命令 flask 还支持 shell 子命令:

> flask shell
Python 2.7.11+(default, Apr 17 2016, 14:00:29)
[GCC 5.3.1 20160413] on linux2
App:hello [debug]
Instance:/home/ubuntu/web_develop/chapter3/section1/instance
>>>app
<Flask 'hello'>

新的方案甚至能替换 Flask-Script。现在我们基于 flask.cli 模块添加两个子命令(app_cli.py )。

第一个子命令 initdb,用来初始化数据库,这里没有实际的逻辑,只为了演示 click 的使用方法:

import click
from flask import Flask

app=Flask(__name__)
@app.cli.command()
def initdb():
    click.echo('Init the db')

先指定 FLASK_APP 变量:

> export FLASK_APP=chapter3/section1/app_cli.py

现在就可以使用 initdb 了:

> flask initdb
Init the db

再复杂些,实现一个叫作 new_shell 的子命令,它使用 IPython 环境(除非没有安装 IPython 或者强制要求使用默认的交互环境):

from flask.cli import with_appcontext

try:
    import IPython # noqa
    has_ipython=True
except ImportError:
    has_ipython=False


def plain_shell(user_ns, banner):
    sys.exit(code.interact(banner=banner, local=user_ns))


def ipython_shell(user_ns, banner):
    IPython.embed(banner1=banner, user_ns=user_ns)


@app.cli.command('new_shell', short_help='Runs a shell in the app context.')
@click.option('--plain', help='Use Plain Shell', is_flag=True)
@with_appcontext
def shell_command(plain):
    from flask.globals import_app_ctx_stack
    app=_app_ctx_stack.top.app
    banner='Python%s on%s\nApp:%s%s\nInstance:%s'%(
        sys.version,
        sys.platform,
        app.import_name,
        app.debug and ' [debug]' or '',
        app.instance_path,
)
user_ns=app.make_shell_context()
use_plain_shell=not has_ipython or plain
if use_plain_shell:
    plain_shell(user_ns, banner)
else:
    ipython_shell(user_ns, banner)

其中 app.cli.command 用来指定子命令的名字和帮助信息,click.option 给子命令添加参数,由于 new_shell 需要使用 app 这个上下文,所以需要添加 with_appcontext 这个装饰器。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。