网站后端_Python+Flask.0014.FLASK模版相关之使用JinJa2模版渲染?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网站后端_Python+Flask.0014.FLASK模版相关之使用JinJa2模版渲染?相关的知识,希望对你有一定的参考价值。
简单 说明:
说明: 之前章节中,视图函数直接返回文本,而实际生产环境很少这样,因为实际的页面大多是带有样式和复杂逻辑的HTML+CSS+JS代码,这可以让浏览器渲染出非常漂亮和复杂的效果,页面内容应该是可以重用的,而且需要更执行更高级的功能
自带 模版:
#!/usr/bin/env python # -*- coding: utf-8 -*- """ # # Authors: limanman # OsChina: http://xmdevops.blog.51cto.com/ # Purpose: # """ # 说明: 导入公共模块 from string import Template # 说明: 导入其它模块 # 方式二: 自定义替换符以及匹配字符 class AtTemplate(Template): delimiter = r‘@‘ idpattern = r‘[_a-z][_a-z0-9]*‘ if __name__ == ‘__main__‘: tp = Template(‘‘‘ name: $name age : $age ‘‘‘) # 方式一: 默认替换符为$,匹配规则为[_a-z][_a-z0-9]* print tp.substitute(name=u‘李满满‘, age=24) print tp.safe_substitute(name=u‘刘珍珍‘, age=25) tp = AtTemplate(‘‘‘ name: @name age : @age ‘‘‘) # 方式二: 自定义替换符为@匹配字符为[_a-z][_a-z0-9]* print tp.substitute(name=u‘李满满‘, age=24) print tp.safe_substitute(name=u‘刘珍珍‘, age=25)
说明: string.Template类,很简单的模版,支持有限,无法继承重用,这对于WEB开发远远不够,所以我们需要使用第三方专业模版系统,目前比较出名的是Jinja2和Mako模版系统
Jinja2模版:
1. 让页面逻辑独立于业务逻辑,开发的程序易于维护
2. 提供流程控制,继承等高级功能使得模版非常灵活,快速,安全
# API 使用
#!/usr/bin/env python # -*- coding: utf-8 -*- """ # # Authors: limanman # OsChina: http://xmdevops.blog.51cto.com/ # Purpose: # """ # 说明: 导入公共模块 from jinja2 import Template # 说明: 导入其它模块 if __name__ == ‘__main__‘: # 说明: 渲染字符串 template = Template(‘Hello {{ name }}!‘) print template.render(name=u‘李满满‘)
说明: 由于单独拿API来使用不太常用,如果单独拿API使用的话掌握如上写法即可,和string.Template类用法基本一致,至于其它渲染指定文件可用读取成字符串再渲染,有兴趣可学习官方API文档
# 强大模版
1. 模版支持任何基于文本的格式(HTML/XML/CSV/LaTex等等),并没有特定的扩展名
# 基础语法
{%- ... -%}
说明: 主要解析流程控制等
{#- ... -#}
说明: 主要添加代码注释等
{{ ... }}
说明: 主要解析模版表达式
# 数据类型
说明: 模版中支持字符串,数值,列表,元组,字典, boolean(true/false,注意是小写的),除此之外还支持全局函数/内置过滤器|/内置测试器is/in关键字/if关键字/字符连接符~
1. 支持算术运算符,+,-,/,//,%,*,**,
2. 支持比较运算符,==,!=,>,>=,<,<=
3. 支持逻辑运算符,and,or,not,(expr)
4. 支持其它运算符,in,is,|,~(连接字符串使用{{ ‘hello ‘ ~ name ~ ‘!‘ }},如果name是limanman则返回hello limanman!),()调用可调用量,./[]获取对象属性
# 变量相关
1. 应用可将任意类型变量传递给模版,访问带属性变量既可以加.访问,也支持[]访问,两者的区别是查找的先后顺序,前者先加.测试,然后再[]测试,后者先[]测试然后在.测试,所以根据具体传递变量类型决定
2. 模版中中可以为变量赋值,在顶层的(块,宏,循环之外)赋值是可导出的,也就是说可以从别的模块儿中导入使用
{%- set navigation = [(‘index.html‘, ‘Index‘), (‘about.html‘, ‘About‘)] -%} {%- set key, value = call_something() -%}
# with语句
1. 模版中支持with语句,和PY内置with类似,with后面常跟表达式,主要用于限制对象的作用域
{% with arr=range(10) %} {% for i in arr %} {{ i }} {% endfor %} {% endwith %}
# 过 滤 器
1. 过滤器和管道符|配合可链式修改变量,过滤器还支持传递参数,内置的过滤器列表可参考(http://docs.jinkan.org/docs/jinja2/templates.html#builtin-filters)
{{ v|abs }}
说明: 返回值的绝对值
{{ v|attr(name) }}
说明: 返回v对象的name属性值
{{ v|batch(linecount, fill_with=None) }}
说明: 将v序列linecount个元素分为一组,保证每组长度相同,如果不够用fill_with的值补充,常用于生成表格
<!--说明: 创建一个10行10列的表格,通过batch分割成10行,然后再次遍历生成10个td--> <table width="600px" style="border-collapse: collapse;margin: 0 auto;" border="1"> {%- for tr in table_tds|batch(10, " ") -%} <tr> {%- for td in tr -%} <td>{{ td }}</td> {%- endfor -%} </tr> {%- endfor -%} </table>
{{ v|capitalize }}
说明: 整个语句的首字母大写,其它字母小写
{{ v|default(default_value=‘‘, boolean=False) }}
说明: boolean为False,只有未定义时才返回default_value,boolean为True时,只要v为False就使用default_value.
{{ v|dictsort(case_sensitive=False, by=‘key‘) }}
说明: 对v字典进行排序case_sensitive设置是否大小写敏感,by指定键排序
{{ v|escape }}
说明: 对v进行安全转义,可以简写为e
{{ v|filesizeformat(binary=False) }}
说明: 格式化v为可读的文件大小默认以字节为单位
{{ v|first }}
说明: 返回序列对象的第一个元素
{{ v|float(default=0.0) }}
说明:转换value值为float类型,如果转换失败则返回default的值
{{ v|forceescape }}
说明: 不管v是否被转义过,一律进行html转义,可能会二次转义
{{ v|format(*args, **kwargs) }}
说明: 使用*args和**kwargs去填充格式化字符串v
{{ v|groupby(attr) }}
说明: 对序列v中的对象/字典按照attr分组,分组后组名存储在group.grouper里,而组员保存在group.list,如下是一个站点地图的简单实现
<ul> {%- for group in persons|groupby("gender") -%} <li>{{ group.grouper }}</li> <ul> {%- for person in group.list -%} <li>{{ person[‘name‘] }}, {{ person[‘age‘] }}</li> {%- endfor -%} </ul> {%- endfor -%} </ul>
{{ vint(default=0) }}
说明: 将v的值转换为int如果转换失败返回default的值
{{ v|join(d=‘‘, attribute=None) }}
说明: 使用d分割符拼接v,如果attribute为字符串则首先获取value的attribute属性然后用d分割符去拼接属性值
{{ v|last }}
说明: 返回序列对象v的最后一个元素
{{ v|length }}
说明: 说明返回对象的长度
{{ v|list }}
说明: 转换v为列表,如果value为字符串则转换为字符列表
{{ v|lower }}
说明: 转换为小写
{{ v|map(filter/attribute=‘‘) }}
说明: filter过滤器作用域v的每个元素,attribute过滤出v中包含指定属性的元素
{%- for val_unit in [1024, 2048, 4096]|map(‘filesizeformat‘) -%} <p>{{ val_unit }}</p> {%- endfor -%}
{{ v|pprint(verbose=False) }}
说明: 更优美的方式打印,用于调试
{{ v|random }}
说明: 从序列对象中随机返回一个元素
{{ v|replace(old, new, count=None) }}
说明: 将v中的old替换为new,如果指定了count定义替换n次
{{ v|reverse }}
说明: 反转value序列
{{ v|round(precision=0, method=‘common‘) }}
说明: 四舍五入v的值precision表示保留小数点几位,method的值可以为common/ceil/floor
{{ v|safe }}
说明: 标记value为安全字符串,按照正常规则去解析
{{ value|sort(reverse=False, case_sensitive=False, attribute=None) }}
说明: 如果指定attribute则按照attribute排序,reverse表示是否逆向排序,case_sensitive表示是否区分大小写
{%- for cur_item in data|sort(false, true, attribute=‘index‘) -%} <p>{{ cur_item[‘index‘] }}</p> {%- endfor -%}
{{ v|string }}
说明: 将v对象转换为字符串
{{ v|striptags }}
说明: 清除v中的html/xml等文档中的标签
{{ v|sum(attribute=None, start=0) }}
说明: 如果设置了attribute则会获取v中所有元素的attribute属性的值的总和,start为初始值
{{ v|title }}
说明: 让v中的每个字母的首字母大写
{{ v|trim }}
说明: 去除v两边的空白
{{ v|truncate(length=255, kilwords=False, end=‘...‘) }}
说明: v字符串超过length长度的部分用...代替,如果要精确到字符的话需设置killwards为true,否则设置为false
{{ v|tojson|safe }}
说明: 将v转换为json,但是为了不被转义,常常配合safe在Javascript脚本中使用
<script type="text/javascript"> var users = {{ users|tojson|safe }} for(var i=0; i<users.length; i++){ console.log(users[i].name) } </script>
{{ v|upper }}
说明: 把v中的所有字符转换为大写
{{ v|urlencode }}
说明: 将v进行URL编码
{{ v|urlize(trim_url_limit=None, nofollow=False) }}
说明: 转换v为可点击的url连接地址,trim_url_limit设置url显示的长度,超出长度用...填充
{{ v|wordcount }}
说明: 获取s中的单词的数量
2. 除了内置过滤器还支持自定义过滤器,非常简单可通过app.add_template_filter(func, name)函数或@@app.template_filter(name)修饰器来自定义过滤器,两种方法内部调用的都是app.jinja_env.filters[name] = func实现的,但并不推荐直接设置jinja2环境,所以还是推荐前两种方法
@monitor.app_template_filter(‘sub‘) def sub_filter(inputs, start, end, step): return inputs[start:end:step]
# 测试相关
1. 测试用于对照普通表达式测试一个变量,内置的列表可参考(http://docs.jinkan.org/docs/jinja2/templates.html#builtin-tests)
{%- object is callable -%}
说明:测试object是否可以被调用
{%- value is defined -%}
说明:测试value是否被定义
{%- value is divisibleby(num) -%}
说明:测试value是否可被num整除
{%- value is escaped -%}
说明:测试value是否被转义过
{%- value is even -%}
说明:测试value是否为奇数
{%- value is iterable -%}
说明:测试value是否可迭代
{%- value is lower -%}
说明:测试value是否小写
{%- value is mapping -%}
说明:测试value是否是字典
{%- value is none -%}
说明:测试value是否为none
{%- value is number -%}
说明:测试value是否为字符串
{%- value is odd -%}
说明:测试value是否为奇数
{%- value is sequence -%}
说明:测试value是否是序列对象
{%- value is string -%}
说明:测试value是否为字符串
{%- value is undefined -%}
说明:测试value是否未定义
{%- value is upper -%}
说明:测试value是否为大写
2. 除了内置测试器还支持自定义测试器,非常简单,通过app.add_template_test(func, name)函数或@app.template_test(name)修饰器来自定义过滤器,两种方法内部调用的都是app.jinja_env.tests[name] = func实现的,但并不推荐直接设置jinja2环境,所以还是推荐前两种方法
@monitor.app_template_test(‘endswith‘) def ends_width(s, suffix): return s.endswith(suffix)
# 全局函数
1. 如果说过滤器是一个变量转换函数,测试器是一个变了测试函数,那么全局函数就可以是任何函数,可以在任一场景使用,没有输入和输出值限制,全局函数可参考,http://docs.jinkan.org/docs/jinja2/templates.html#builtin-globals
range([start], stop[, step])
说明: 返回一个包含整等差级数的列表
dict(**items)
说明: 方便的字典替代品{‘foo‘: ‘bar‘}等价于dict(foo=bar)
lipsum(n=5, html=True, min=20, max=100)
说明: 在模版中生成lipsum乱数假文,默认会生成5段html,每段20到100词之间,如果html被禁用,则会返回常规文本,在测试布局时生成简单内容时很有用
joiner(sep)
说明: 初始化一个分割符,第一次调用返回空字符,以后调用返回分割符,常用于分割循环中的内容
<ul> {%- set sep = joiner(‘|‘) -%} {%- for title in [‘资产管理‘, ‘主机分组‘, ‘主机信息‘] -%} {%- set s = ‘<i>‘ ~ sep() ~ ‘</i>‘ if sep() -%} {{ s|safe }} <li><a href="#">{{ title }}</a></li> {% endfor %} </ul>
说明: 在模版中生成乱数假文,默认会生成5段html,每段在20到100词之间,如果HTML被禁用,会返回常规文本,在测试布局时生成简单内容很有用
cycler(*items)
说明: 周期性的从若干个值的循环,周期实例.next()返回当前项并跳到下一个,.reset()重置周期计到第一个项,.current返回当前项
{%- set cur_datas = range(1, 101, 1) -%} {%- set row_class = cycler(‘light‘, ‘dark‘) -%} <table style="margin: 0 auto;border-collapse: collapse" width="600px" border="1"> {%- for tr in cur_datas|slice(10) -%} <tr class="{{ row_class.next() }}"> {%- for td in tr -%} <td>{{ td }}</td> {%- endfor -%} </tr> {%- endfor -%} </table>
join(sep=‘,‘)
说明: 连接多个项
{{ range(10)|join(‘,‘) }}
2. 除了内置全局函数还支持自定义全局函数,非常简单,通过app.add_template_global(func, name)函数或@app.template_global(name)修饰器来自定义全局函数,两种方法内部调用的都是app.jinja_env.globals[name] = func实现的,但并不推荐直接设置jinja2环境,所以还是推荐前两种方法
@monitor.app_template_global(‘endswith‘) def ends_width(s, suffix): return s.endswith(suffix)
# 注释相关
1. 模版中注释默认使用{# ... #},可以注释多行,在调试或添加模版信息时非常有用
{# 说明: 创建宽度为600px并居中对齐的一个10行10列的表格,并隔行换色 <table style="margin: 0 auto;border-collapse: collapse" width="600px" border="1"> {%- for tr in table_tds|slice(10) -%} {% if loop.index is odd %} <tr bgcolor="#ffffff"> {%- for td in tr -%} <td>{{ td }}</td> {%- endfor -%} </tr> {%- else -%} <tr bgcolor="#a9a9a9"> {%- for td in tr -%} <td>{{ td }}</td> {%- endfor -%} </tr> {%- endif -%} {%- endfor -%} </table> #}
# 空白控制
1. 默认模版引擎不会对空白处理,所以模版中的代码块中的每个空白都会原封不动返回,可以通过{%- ... -%}手动剥离块前或块儿后的空白,但需要注意的是-和{% %}不能有空白,否则无效
{%- for num in range(10) -%} {{ num }} {%- endfor -%}
说明: 如上加-剥离空白后返回的是0123456789,而没有加-会发现返回的是0 1 2 3 4 5 6 7 8 9,每个项都多出一个空白,强烈推荐大家养成加-的习惯
# 转义相关
1.单行可用{{ ‘{{}}‘ }}作为原始字符串实现转义,多行可使用raw(如展示Jinja2语法的实例)
<li>{{ ‘{{‘ }}</li> {% raw %} {% for item in seq %} <li>{{ item }}</li> {% endfor %} {% endraw %}
# 模版继承
1. Jinja2最强大的部分就是模版继承,模版继承允许你构建一个包含你站点公共元素的基本模版‘骨架‘,并定义子模版可以覆盖的块
<!DOCTYPE html> <html {% block html_attribs %}{%- endblock -%}> {%- block html -%} <head> {%- block head -%} {% block meta %} {%- endblock -%} {% block styles %} {%- endblock -%} {%- endblock -%} </head> <body {%- block body_attribs -%}{%- endblock -%}> {%- block body -%} {%- block scripts -%} {%- endblock -%} {%- endblock -%} </body> {%- endblock -%} </html>
{%- extends ‘base.html‘ -%} {%- block head -%} {{ super() }} {#- 头部区 -#} {%- endblock -%} {%- block body -%} {{ super() }} {#- 主体区 -#} {%- endblock -%} {%- block styles -%} {{ super() }} {#- 样式区 -#} {%- endblock -%} {%- block scripts -%} {{ super() }} {#- 脚本区 -#} {%- endblock -%}
说明: {% block ... %} ... {% endblock %}表示子模板需要填充的块,{% extends ‘....html‘ %}表示此模版继承自另一个模版,所以首先会定位父模版(应该作为第一个标签),默认继承时从templates目录查找,模版引擎也支持相对路径,但是需要注意的是同一个模版不能出现同名block,但是可以通过{{ self.block_name() }}在同一个页面调用指定的block块儿
# 嵌套相关
1. {%- block ... -%}{{ var }}{%- endblock -%}块内的变量var不允许访问块外作用域的变量,如果需要添可为block添加修饰scoped才可以获取块外的变量的值
# 控制结构
1. for可遍历可迭代对象中的每项,在模版循环体中支持特殊变量访问
loop.index
说明: 当前循环迭代的次数(从1开始)
loop.index0
说明: 当前循环迭代的次数(从0开始)
loop.revindex
说明: 到循环结束需要迭代的次数(从1开始)
loop.revindex0
说明: 到循环结束需要迭代的次数(从0开始)
loop.first
说明: 如果是第一次迭代,返回True
loop.last
说明: 如果是最后一次迭代,返回True
loop.length
说明: 序列中的项目数
loop.cycle
说明: 在一串序列周期间取值
{%- for cur_item in data -%} <P>总计需要循环的次数 : {{ loop.length }}</P> <p>从data中随机取数字: {{ loop.cycle(*data) }}</p> {%- if loop.first -%} <p>loop start ....</p> {%- endif -%} <hr /> <p>当前循环计数(1开始): {{ loop.index }}</p> <p>当前循环计数(0开始): {{ loop.index0 }}</p> <p>剩余循环计数(1开始): {{ loop.revindex }}</p> <p>剩余循环计数(0开始): {{ loop.revindex0 }}</p> {%- if loop.last -%} <p>loop end....</p> {%- endif -%} {%- endfor -%}
说明: for循环中loop.cycle()辅助函数,伴随循环字符串/变量列表中周期取值
2. 模版中循环不支持break/continue,但支持联合if跳过,如果过滤了所有项目而没有执行循环可使用else渲染替换块儿
{%- for cur_item in data if cur_item is odd -%} <p>{{ cur_item }}</p> {%- else -%} <p>nothing</p> {%- endfor -%}
3. 模版支持递归循环,常用在站点地图,要使用递归循环,需要在循环定义中加上recursive修饰,并在你想递归的地方调用loop变量
<ul> {%- for cur_link in links recursive -%} <li><a href="{{ cur_link[‘href‘] }}">{{ cur_link[‘caption‘] }}</a></li> {%- if cur_link[‘children‘] -%} <ul> {{ loop(cur_link[‘children‘]) }} </ul> {%- endif -%} {% else %} <li>no item found.</li> {% endfor %} </ul>
注意: 如上代码{{ loop( ... ) }}外层必须还要包括一个ul,不然递归循环时生成的li依然放在第一层的ul中,页面的结构会错乱
4. if语句可以用于内联表达式并作为循环过滤,而且支持用elif和else来构建多个分支
{% extends layout_template if layout_template is defined else ‘master.html‘ %}
5. 宏类似常规编程语言中的函数,用于把常用的作为可重用函数,取代重复定义,如果需要宏在不同模版中引用需使用import导入,宏内部可以访问三个特殊变量
varargs
说明: 如果有多余宏位置参数,它们会作为列表的值保存在varargs变量上
kwargs
说明: 如果有多余宏键值参数,它们会作为列表的值保存在kwargs变量上
caller
说明: 如果宏通过call标签调用,调用者会作为可调用的宏存储在这个变量中
name
说明: 宏的名称
arguments
说明: 一个宏接受的参数名的元素
defaults
说明: 默认值的元素
catch_kwargs
说明: 如果宏接受额外的关键字参数,为True
catch_varargs
说明: 如果宏接受额外的位置参数,为True
caller
说明: 如果宏访问特殊的caller变量由call标签调用,为True
{%- macro list_user(users) -%} <table> <tr> <th>name</th> <th>option</th> </tr> {%- for user in users -%} <tr> <td>{{ user.name }}</td> <td>{{ caller(user.id) }}</td> </tr> {%- endfor -%} </table> {%- endmacro -%} {%- block body -%} {%- set users = [ {‘id‘: 1, ‘name‘: ‘李满满‘}, {‘id‘: 2, ‘name‘: ‘刘珍珍‘}, ] -%} {%- call(id) list_user(users) -%} <a href="#user_edt#{{ id }}">编辑</a> | <a href="#user_del#{{ id }}">删除</a> {%- endcall -%} {%- endblock -%}
注意:如果一个宏的名称以下划线开始,它无法被导入到其它模版,如上简单演示了macro宏利用caller()访问调用者内容的例子,caller()中可以添加参数也可以不添加参数,非常灵活
6. 宏常存放使用的代码,这些宏可以被导入,并在可通过import/from import导入但默认不传递到当前上下文,只作为宏的存放处,如果要实现类似include支持传递到当前上下文,需要结合with context
{% import ‘forms.html‘ as forms with context %} {% from ‘forms.html‘ import input as input_field, textarea with context %}
# 语句过滤
说明: 过滤器filter可以调用所有jinja2的过滤器,包含在filter节中的数据都会被渲染
{%- filter upper -%} <p>All html contents will be uppercase.</p> {%- endfilter -%}
# INCLOUD
说明: incloud可以包含单个文件也可以包含一个文件列表,ignore missing可以忽略文件不存在导致的异常,with context时incloud包含的可渲染变量会自动被渲染,当然without context自然是禁用它
{% include "sidebar.html" ignore missing %} {% include "sidebar.html" ignore missing with context %} {% include "sidebar.html" ignore missing without context %} {% include [‘page_detailed.html‘, ‘page.html‘] %} {% include [‘special_sidebar.html‘, ‘sidebar.html‘] ignore missing %}
# 上下环境
说明: FLASK每个请求都有生命周期,请求会携带请求上下文,作为在请求中渲染的模版,自然也是在请求的声明周期内,所以FLASK的模版自然可以使用到请求上下文的环境变量,及一些辅助函数
1. 模版中支持请求对象request所有的属性和方法,但是需要注意的是在非请求上下中这个对象是不可用的
{{ request.url }}
2. 模版中支持会话对象session所有的属性和方法,但是需要注意的是在非请求上下中这个对象是不可用的
{{ session.user }}
3. 模版中支持全局对象g所有的属性和方法,但是需要注意的是在非程序或请求上下中这个对象是不可用的
{{ g.user }}
4. 模版中支持配置对象config所有的属性和方法,但是需要注意的是在非程序或请求上下中这个对象是不可用的
{{ config.DEBUG }}
5. 模版中支持url_for()函数,可以快速获取以及构建URL,FLASK也将此函数引入模版中
<a href="{{ url_for(‘monitor.index‘) }}">monitor</a>
6. 模版中还支持自定义上下文变量和函数,非常简单只需要用@app.context_processor去修饰一个返回字典的函数,字典中的键就可以在模版中直接使用,而字典中的可以是任何值,可以是变量可以是函数
@monitor.context_processor def injection(): area = u‘国内‘ def hosts(): return [h for h in xrange(10)] return { ‘area‘: area, ‘hosts‘: hosts }
# 模版扩展
说明: 除了以上的功能外,JinJa2还支持扩展或插件开发,允许第三方通过开发新的扩展或插件,当然它自身也提供了一些扩展功能,只是默认没有开启,咱们只是简单讲下怎么启用扩展,至于怎么使用就自己看官网文档了~
extensions = [‘jinja2.ext.autoescape‘, ‘jinja2.ext.with_‘, ‘jinja2.ext.do‘, ‘jinja2.ext.i18n‘, ‘jinja2.ext.loopcontrols‘] jinja_env = Environment(extensions=extensions) for extension in extensions: app.jinja_env.add_extension(extension)
本文出自 “满满李 - 运维开发之路” 博客,请务必保留此出处http://xmdevops.blog.51cto.com/11144840/1868238
以上是关于网站后端_Python+Flask.0014.FLASK模版相关之使用JinJa2模版渲染?的主要内容,如果未能解决你的问题,请参考以下文章
网站后端_Python+Flask.0003.FLASK快速入门之Hello Word?
网站后端_Python+Flask.0014.FLASK模版相关之使用JinJa2模版渲染?
网站后端_Python+Flask.0012.FLASK域名相关之域名与动态子域名实现?
网站后端_Python+Flask.0007.FLASK构造跳转之301跳转与302重定向?