浅谈 Dojo 中的 DTL 工具包

DTL 语言着力于为静态的文本提供足够的编程功能,如提供分支和循环等决定呈现相关的逻辑。它主要用于分割文档的表示和数据的字符串文本,模板定义了占位符和各种定义文档应该如何显示的基本逻辑(模板标签“template tag”),模板可用来生成 HTML 或者各种基于文本的格式。Dojo 的 DTL 工具包实现了 DTL 语法的解析,并提供了一系列简单易用的接口用于接收参数,生成和解析基于 DTL 的各种文本或 HTML 页面。这样一来,我们可以基于简单的 DTL 语法构造 HTML 模板,并基于 Dojo 的 DTL 接口实现代码的充分复用。这篇文章将重点介绍 Dojo 的 DTL 工具包以及他们的各种使用方式和技巧。


DTL 模板语言简介
DTL 模板语言是 Django 框架内置的一套包含特殊标签(tag)和过滤器(filters)的规则集。它包括 autoescape、block、comment、cycle、for、if、ifequal 等等标签和 add、capfirst、center、first、last、length、lower、safe 等等过滤器。这些标签和过滤器如同我们的编程语言一样,可以帮助我们实现各种各样的逻辑。Dojo 的 DTL 工具包基于这套规则实现了上述的大部分标签和过滤器的语法解析功能。所以,我们可以直接在 Dojo 中基于 DTL 模板语言实现我们的各种逻辑。

Dojo 的 DTL 工具包
Dojo 的 DTL 工具包提供了很多接口用于解析我们的“DTL 程序”,它不仅适用于文本的解析,也适用于 HTML 页面。对于之前已经使用 DTL 开发的工程师们,将已有代码移植到 Dojo 中是相当方便的,基本是无缝移植。
接下来我们来看看 Dojo 的 DTL 工具包的一些具体对象和接口。
Context 对象
Context 对象是 Dojo 的 DTL 工具包里用于存放内容的对象,Dojo 通过一些接口将该对象“注入”到模板里,其实 Context 有点类似于实参。来看一个简单示例:

清单 1. 简单 Context 示例
 var context = new dojox.dtl.Context(
   {foo: "foo", bar: "bar", get: function(key){ return key + "TEST"; }}); 
 var tpl = new dojox.dtl.Template("{{ foo }}-{{ bar }}"); 
 t.is("fooTEST-barTEST", tpl.render(context)); 

这里我们初始化了一个 Context 对象,并定义了它的“get”方法,之后我们用该对象“填充”之后创建的 Template。这里的“{{ foo }}”和“{{ bar }}”代表“foo”变量和“bar”变量,所以最后的返回值为"fooTEST-barTEST"。这就是 Context 和 Template 协同的工作模式。
在来看一个内容过滤器的例子:

清单 2. Context 过滤
 var context = new dojox.dtl.Context({ foo: "one", bar: "two", baz: "three" }); 
 var filtered = context.filter("foo", "bar"); 
 t.is(filtered.foo, "one"); 
 t.is(filtered.bar, "two"); 
 t.f(filtered.baz); 

 filtered = context.filter({ bar: true, baz: true }); 
 t.f(filtered.foo); 
 t.is(filtered.bar, "two"); 
 t.is(filtered.baz, "three"); 

 filtered = context.filter(new dojox.dtl.Context({ foo: true, baz: true })); 
 t.is(filtered.foo, "one"); 
 t.f(filtered.bar); 
 t.is(filtered.baz, "three"); 

这里我们提供了三种过滤的示例:
1. “context.filter("foo", "bar")”:直接通过属性名可以过滤掉除“foo”和“bar”以外的所有其它属性。
2. “context.filter({ bar: true, baz: true })”:也可以通过 true 或者 false 实现过滤。
3. “context.filter(new dojox.dtl.Context({ foo: true, baz: true }))”:新建一个 Context 对象过滤也是可以的。
除了过滤,我们的 Context 对象也可以被扩展:

清单 3. Context 扩展
 var context = new dojox.dtl.Context({ foo: "one" }); 
 var extended = context.extend({ bar: "two", baz: "three" }); 
 t.is(extended.foo, "one"); 
 t.is(extended.bar, "two"); 
 t.is(extended.baz, "three"); 


 extended = context.extend({ barr: "two", bazz: "three" }); 
 t.is(extended.foo, "one"); 
 t.f(extended.bar); 
 t.f(extended.baz); 
 t.is(extended.barr, "two"); 
 t.is(extended.bazz, "three"); 


 t.f(context.bar) 
 t.f(context.baz); 
 t.f(context.barr); 
 t.f(context.bazz); 
通过“extend”方法,我们可以在已有 Context 对象的基础上扩展新的属性。“context.extend({ bar: "two", baz: "three" })”扩展了“bar”和“baz”属性,其值分别为“two”和“three”。这里需要注意:原来的 Context 对象(这里是“context”变量)是没有任何变化的,改变的只是扩展后的那个变量(这里是“extended”变量)。
文本模板之标签(Tag)
介绍完了 Context 对象,我们要来说说模板了。Context 内容对象是和模板(Template)对象协同工作的,并且它们基于 DTL 模板语言。我们先来看一个简单的文本模板示例:
清单 4. 简单文本模板
 var dd = dojox.dtl; 
 var template = new dd.Template(
  '{% extends "../../dojox/dtl/tests/templates/pocket.html" %}
   {% block pocket %}Simple{% endblock %}'); 
 t.is("Simple Pocket", template.render()); 
这里我们建立了一个模板,里面包含了“extends”和“block”,所以很明显这里是 DTL 里面的一个简单的模板继承语法,用子模板“pocket ”:“{% block pocket %}Simple{% endblock %}”来替代父模板“pocket.html”(“{% block pocket %}Hot{% endblock %} Pocket”)。通过“template.render()”来执行语法的解析,最后的结果为"Simple Pocket",完全符合 DTL 语法的解析结果。
再来看几个复杂点的例子:

清单 5. 简单文本模板进阶  
 var dd = dojox.dtl; 

 // 参数传递 1 
 var context = new dd.Context({ 
 parent: "../../dojox/dtl/tests/templates/pocket.html"
 }) 
 template = new dd.Template('
 {% extends parent %}{% block pocket %}Variabled{% endblock %}'); 
 t.is("Variabled Pocket", template.render(context)); 


 // 参数传递 2 
 context.parent = dojo.moduleUrl("dojox.dtl.tests.templates", "pocket.html"); 
 template = new dd.Template('
 {% extends parent %}{% block pocket %}Slightly More Advanced{% endblock %}'); 
 t.is("Slightly More Advanced Pocket", template.render(context)); 


 // 参数传递 3 
 context.parent = { 
 url: dojo.moduleUrl("dojox.dtl.tests.templates", "pocket.html") 
 } 
 template = new dd.Template('
 {% extends parent %}{% block pocket %}Super{% endblock %}'); 
 t.is("Super Pocket", template.render(context)); 
这里列出了三种参数传递方式。我们不用像之前那样将 HTML 文件的路径写在 Template 里面了,可以通过 Context 的参数传递到 Template 里,通过 moduleUrl 或者拥有 url 属性的对象传递均可。
除了传递模板 HTML,Context 也支持其它各种方式的实参传递:

清单 6. 文本模板传参  
 var dd = dojox.dtl; 

 var context = new dd.Context({ 
 parent: dojo.moduleUrl("dojox.dtl.tests.templates", "pocket2.html"), 
 items: ["apple", "banana", "lemon" ] 
 }); 

 var template = new dd.Template("
 {% extends parent %}{% block pocket %}My {{ item }}{% endblock %}"); 
 t.is("(My apple) (My banana) (My lemon) Pocket", template.render(context)); 
这里有两个参数:“parent”和“items”,我们来看看父模板的内容:

清单 7. 父模板 pocket2.html
				 
 {% for item in items %}( 
 {% block pocket %} 
 Hot 
 {% endblock %} 
 ) {% endfor %} 
 Pocket 

根据 DTL 语法的规则,父模板的 block--“pocket ”会被子模板的 block--“pocket ”替代,然后通过解析 Context 传入的 items 参数,最后的执行结果应该为:"(My apple) (My banana) (My lemon) Pocket"。
其实,我们也可以通过“block.super”在子模板里面访问父模板的内容:“{% extends parent %}{% block pocket %}My {{ item }} {{ block.super }} { % endblock %}”。
当然,DTL 语句也支持注释:
清单 8. 文本模板注释
				 
 var template = new dd.Template('
 Hot{% comment %}<strong>Make me disappear</strong>{% endcomment %} Pocket'); 
 t.is("Hot Pocket", template.render()); 
通过“{% comment %}”和“{% endcomment %}”来添加注释,注释中的代码不会被执行。
再来看看 Cycle 标签:

清单 9. 文本模板 Cycle 标签
 var context = new dd.Context({ 
 items: ["apple", "banana", "lemon"], 
 unplugged: "Torrey"
 }); 
 var template = new dd.Template("
 {% for item in items %}{% cycle 'Hot' 'Diarrhea' unplugged 'Extra' %} 
   Pocket. {% endfor %}"); 
 t.is("Hot Pocket. Diarrhea Pocket. Torrey Pocket. ", template.render(context)); 
Cycle 标签表示循环在列表里面取值,由于“items”里面只有三个元素,所以这里的取值应该是"Hot Pocket. Diarrhea Pocket. Torrey Pocket.",最后的“Extra”不会被取到。
再来看一个关于 Cycle 的稍微复杂一点的例子:

清单 10. 文本模板 Cycle 标签进阶
 context = new dojox.dtl.Context({ unplugged: "Torrey" }); 
 template = new dd.Template("
 {% cycle 'Hot' 'Diarrhea' unplugged 'Extra' as steakum %} 
 Pocket. {% cycle steakum %} Pocket. {% cycle steakum %} Pocket."); 
 t.is("Hot Pocket. Diarrhea Pocket. Torrey Pocket.", template.render(context)); 

这里的 Cycle 我们通过“as steakum”来给这个 Cycle 定义了一个引用,这样,我们可以在以后的代码里重复利用该 Cycle,通过“{% cycle steakum %}”来取它列表里面的下一个值。
接下来我们介绍一下“filter”标签:


清单 11. 文本模板 Filter 标签   
 var template = new dd.Template('
  {% filter lower|center:"15" %}Hot Pocket{% endfilter %}'); 
 t.is("  hot pocket   ", template.render()); 

这里的 Filter 标签用于格式化文本内容,“{% filter lower|center:"15" %}”表示将字体转为小写,居中并保持 15 个字符(不足补空格)。
再来看看“firstof”标签:
清单 12. 文本模板 Firstof 标签
 var dd = dojox.dtl; 

 var context = new dd.Context({ 
 found: "unicorn"
 }); 

 var template = new dd.Template("{% firstof one two three four found %}"); 
 t.is("unicorn", template.render(context)); 

 context.four = null; 
 t.is("null", template.render(context)); 

 context.three = false; 
 t.is("false", template.render(context)); 
这里的“firstof”会取得第一个有值的变量,由于只有“found”被赋值,所以这里的“{% firstof one two three four found %}”应为“unicorn”,当然,如果您手动给 context 变量赋值,该语句的运行结果也会有相应改变。
关于“include”标签,示例如下:

清单 13. 文本模板 Include 标签
 var dd = dojox.dtl; 


 var context = new dd.Context({ 
 hello: dojo.moduleUrl("dojox.dtl.tests.templates", "hello.html"), 
 person: "Bob", 
 people: ["Charles", "Ralph", "Julia"] 
 }); 


 var template = new dd.Template("{% include hello %}"); 
 t.is("Hello, <span>Bob</span>", template.render(context)); 
和 HTML 里面的“include”标签一样,它用来引入外部的文本内容。

清单 14. 文本模板 Spaceless 标签
var dd = dojox.dtl; 
var template = new dd.Template("
{% spaceless %}<ul> \n <li>Hot</li> \n\n<li>
Pocket </li>\n </ul>{% endspaceless %}"); 
t.is("
<ul><li>Hot</li><li>Pocket </li></ul>",
 template.render()); 
这里的“spaceless”标签用于去除所有空格,只留下非空格字符。
接下来还有各种 Dojo 支持的标签,如:for,if,ifchanged,ifequal,include,with,withratio 等等,它们的语法与 DTL 一样,有兴趣的读者可以参考 DTL 的语法介绍,我们这里不再一一介绍。
文本模板之过滤(Filter)
接下来我们要介绍过滤器,过滤器在 DTL 里面更多的是一种辅助的操作,我们先来看一个例子:

清单 15. 文本过滤 Add 操作
 var dd = dojox.dtl; 


 var context = new dd.Context({ four: 4 }); 
 tpl = new dd.Template('{{ four|add:"6" }}'); 
 t.is("10", tpl.render(context)); 
 context.four = "4"; 
 t.is("10", tpl.render(context)); 
 tpl = new dd.Template('{{ four|add:"six" }}'); 
 t.is("4", tpl.render(context)); 
 tpl = new dd.Template('{{ four|add:"6.6" }}'); 
 t.is("10", tpl.render(context)); 
注意这里的“add”操作:“{{ four|add:"6" }} ”,由于“context”里面指定了“four”的值为“4”,所以这里的输出为“10”。
其实这里的“add”操作兼容范围很广泛,它会强制转换相应的操作对象,如果不行,则会按照一种默认的方式实现“加”操作,适用于整数,小数甚至数组或列表型数据。
再来看一个“cut”的示例

清单 16. 文本过滤 Cut 操作
 var dd = dojox.dtl; 


 var context = new dd.Context({ uncut: "Apples and oranges" }); 
 var tpl = new dd.Template('{{ uncut|cut }}'); 
 t.is("Apples and oranges", tpl.render(context)); 
 tpl = new dd.Template('{{ uncut|cut:"A" }}'); 
 t.is("pples and oranges", tpl.render(context)); 
 tpl = new dd.Template('{{ uncut|cut:" " }}'); 
 t.is("Applesandoranges", tpl.render(context)); 
 tpl = new dd.Template('{{ uncut|cut:"e" }}'); 
 t.is("Appls and orangs", tpl.render(context)); 
"cut"主要用于删除相关字符,这里的“{{ uncut|cut:"A" }}”用于删除“uncut”变量值里面的所有“A”字符,注意:这里是区分大小写的。

清单 17. 文本过滤 Default
 var dd = dojox.dtl; 


 var context = new dd.Context(); 
 tpl = new dd.Template('{{ empty|default }}'); 
 t.is("", tpl.render(context)); 
 tpl = new dd.Template('{{ empty|default:"full" }}'); 
 t.is("full", tpl.render(context)); 
 context.empty = "not empty"; 
 t.is("not empty", tpl.render(context)); 
“default”这里的基本原理是:如果变量有值,则取变量值,否则取“default”的值。
以上是几个比较简单的过滤操作,再来看几个稍微复杂的:


清单 18. 文本过滤 Dictsort
 var dd = dojox.dtl; 


 var context = new dd.Context({ 
 fruit: [ 
 { name: "lemons", toString: function(){ return this.name; } }, 
 { name: "apples", toString: function(){ return this.name; } }, 
 { name: "grapes", toString: function(){ return this.name; } } 
 ] 
 }); 
 tpl = new dd.Template('{{ fruit|dictsort|join:"|" }}'); 
 t.is("lemons|apples|grapes", tpl.render(context)); 
 tpl = new dd.Template('{{ fruit|dictsort:"name"|join:"|" }}'); 
 t.is("apples|grapes|lemons", tpl.render(context)); 
“dictsort”主要用于排序,这里对“fruit”排序并通过“|”连接起来(join:“|”),所以结果为:“lemons|apples|grapes”。如果有多个属性,可以指定属性排序:|dictsort:“name”,即基于“name”属性排序。

清单 19. 文本过滤 Truncatewords
 var dd = dojox.dtl; 


 var context = new dd.Context({ word: "potted meat writes a lot of tests" }); 
 var tpl = new dd.Template("{{ word|truncatewords }}"); 
 t.is(context.word, tpl.render(context)); 


 tpl = new dd.Template('{{ word|truncatewords:"1" }}'); 
 t.is("potted", tpl.render(context)); 


 tpl = new dd.Template('{{ word|truncatewords:"2" }}'); 
 t.is("potted meat", tpl.render(context)); 


 tpl = new dd.Template('{{ word|truncatewords:20" }}'); 
 t.is(context.word, tpl.render(context)); 


 context.word = "potted \nmeat   \nwrites  a lot of tests"; 
 tpl = new dd.Template('{{ word|truncatewords:"3" }}'); 
 t.is("potted \nmeat   \nwrites", tpl.render(context)); 
顾名思义,“truncatewords”操作主要用于截取文字:“{{ word|truncatewords:"2" }}”相当于截取前两个文字,所以其结果为“potted meat”,这种操作在我们日常开发中,尤其是页面排版中经常需要用到。
关于过滤器还有很多很多的 Dojo 支持的操作,如:filesizeformat,fix_ampersands,iriencode,pluralize,removetags,slice,urlencode 等等,它们的语法也与 DTL 一样,有兴趣的读者可以参考 DTL 的关于 Filter 相关内容,我们这里也不再一一介绍了。
DOM 模板
之前介绍的一些标签和过滤器是基于文本的,接下来我们会介绍 Dojo 的 DTL 工具包的针对 DOM 的一些标签和过滤相关的接口,这些接口会默认您的模板是标准 HTML 格式的,如果不是,它会检测到并抛出异常。这些接口非常适用于将我们之前写好的基于 DTL 的 HTML 模板无缝的集成到 Dojo 中来。

清单 20. DOM 模板错误检测
 var dd = dojox.dtl; 
 var template; 


 var found = false; 
 try { 
 template = new dd.DomTemplate('No div'); 
 dd.tests.dom.util.render(template); 
 }catch(e){ 
 t.is("Text should not exist outside of the root node in template", e.message); 
 found = true; 
 } 
 t.t(found); 
 t.is("potted meat", tpl.render(context)); 




 template = new dd.DomTemplate('<div></div>extra content'); 
 found = false; 
 try { 
 dd.tests.dom.util.render(template); 
 }catch(e){ 
 t.is("Content should not exist outside of the root node in template", e.message); 
 found = true; 
 } 
 t.t(found); 
这里我们的内容为“No div”,显然不符合 HTML 的规范,所以 Dojo 的接口会抛出异常:“文本不能在根节点以外存在”。同样,对于“<div></div>extra content”的情况也是如此。
再来看两个关于报错的示例:

清单 21. DOM 模板错误检测进阶
 template = new dd.DomTemplate('<div></div><div></div>'); 
 found = false; 
 try { 
 dd.tests.dom.util.render(template); 
 }catch(e){ 
 t.is("Content should not exist outside of the root node in template", e.message); 
 found = true; 
 } 
 t.t(found); 




 template = new dd.DomTemplate('{% if missing %}<div></div>{% endif %}'); 
 found = false; 
 try { 
 dd.tests.dom.util.render(template); 
 }catch(e){ 
 t.is("Rendered template does not have a root node", e.message); 
 found = true; 
 } 
 t.t(found); 
可见,“<div></div><div></div>”这种没有独立根节点,或者说又多余一个的根节点的情况也是不被允许的。同样,“{% if missing %}<div></div>{% endif %}”这种在根节点外存在逻辑控制的情况也是禁止的。
我们可以尽可能的利用 DTL 语言的方便为我们构建“动态”的 HTML 页面,一下是一个关于元素属性的示例:

清单 22. DOM 模板元素属性
 var dd = dojox.dtl; 


 var template = new dd.DomTemplate('
 <div>{% for item in items %}<a index="{{forloop.counter0}}"
  id="id_{{item.param}}">{{item.param}}</a>{% endfor %}</div>'); 
 var context = new dd.Context({ 
 items: [ 
 { 
 name: "apple", 
 param: "appleparam"
 }, 
 { 
 name: "banana", 
 param: "bananaparam"
 }, 
 { 
 name: "orange", 
 param: "orangeparam"
 } 
 ] 
 }); 
 doh.is('<div><a index="0" id="id_appleparam">appleparam</a><a index="1"
  id="id_bananaparam">bananaparam</a><a index="2" 
  id="id_orangeparam">orangeparam</a></div>', 
  dd.tests.dom.util.render(template, context)); 
可以看到,这里的 HTML 元素的各种属性都是可以通过变量和逻辑构建出来的:“<a index="{{forloop.counter0}}" id="id_{{item.param}}">”就是一个构建“index”属性和“id”属性的示例。
当然,也可以添加自定义的属性:


清单 23. DOM 模板元素自定义属性
 var dd = dojox.dtl; 


 var context = new dd.Context({frag: {start: 10, stop: 20}}); 


 var template = new dd.DomTemplate('
 <div startLine="{{ frag.start }}" stopLine="{{ frag.stop }}">abc</div>'); 
 doh.is('
 <div startline="10" 
 stopline="20">abc</div>', 
 dd.tests.dom.util.render(template, context)); 

注意这里我们定义的是嵌套的 Context,所以使用的时候也应该通过“{{ frag.start }}”这种方式来使用。
关于 DOM 模板的相关标签和过滤还有很多,这里我们不再一一介绍,有兴趣的读者可以参考 Dojo 的 DTL 使用案例。

基于 DTL 模板的 Widget
前面我们主要介绍了如何使用 DTL 的基本接口,接下来我们会介绍一下它的一个进阶用法:构建基于 DTL 模板的自定义 Widget。
我们知道,Dojo 的 Widget 通常都有一个 HTML 模板,用来构建该 Widget 的初始基本结构。但是有时我们的 Widget 比较大,比较复杂,这回导致我们的 Widget 的 HTML 模板也比较复杂,由于 Widget 的 HTML 模板是比较“静态”的,所以如果 Widget 的 HTML 模板一旦复杂,势必导致我们的模板 HTML 代码量的剧增。如果我们能把 DTL 引入到模板中,使我们的模板具有一定的逻辑控制能力,即“动态”的模板,这样便能大大简化我们的模板 HTML 代码复杂度,使代码更易于维护,同时也可降低网络请求 HTML 模板需要的带宽,提高网络性能。


清单 24. 基于 DTL 模板的 Widget
 dojo.declare("Fruit", [dijit._WidgetBase, dojox.dtl._DomTemplated], { 
 widgetsInTemplate: true, 
 items: ["apple", "banana", "orange"], 
 keyUp: function(e){ 
 if((e.type == "click" || e.keyCode == dojo.keys.ENTER) && 
 this.input.value){ 
 console.debug(this.button); 
 var i = dojo.indexOf(this.items, this.input.value); 
 if(i != -1){ 
 this.items.splice(i, 1); 
 }else{ 
 this.items.push(this.input.value); 
 } 
 this.input.value = ""; 
 this.render(); 
 } 
 }, 


 templateString: dojo.cache("dojox.dtl.demos.templates", "Fruit.html"), 
 }); 

大家注意这里的“items”参数:“items: ["apple", "banana", "orange"]”和“templateString”参数,这里的“items”变量的内容会被注入到 Widget 的模板“Fruit.html”中,从而构建最终的 Widget 的 HTML 模板。来看看“Fruit.html”的内容:


清单 25. 基于 DTL 的 HTML 模板
 <div> 
    <input dojoAttachEvent="onkeyup: keyUp" dojoAttachPoint="input"> 
    <button dojoType="dijit.form.Button" dojoAttachPoint="button" 
    dojoAttachEvent="onClick: keyUp"> 
        Add/Remove Item 
    </button> 
    <div id="pane" dojoType="dijit.layout.ContentPane parsed"> 
        <ul> 
            {% for item in items %} 
            <li> 
                <button dojoType="dijit.form.Button parsed" title="Fruit: {{ item }}" 
                otherAttr2="x_{{item}}"> 
                    {{ item }} 
                    <script type="dojo/connect" event="onClick" args="e"> 
                    console.debug("
                    You clicked", this.containerNode.innerHTML);</' + 'script> 
                </button> 
            </li> 
            {% endfor %} 
        </ul> 
    </div> 
 </div> 
可以看到,这里的“{% for item in items %}”会去遍历我们“Fruit”的 Widget 中的“items”成员变量,从而构建“Fruit”这个 Widget 的最终模板。

结束语

这篇文章介绍了 Dojo 中 DTL 工具包的一些特性,首先从 DTL 语言本身入手,介绍了 DTL 语言的出处,基本规则和特点,然后引出 Dojo 的 DTL 工具包,并由浅入深的逐步介绍了 Dojo 的 DTL 工具包的各种对象和接口,如 Context 对象,Template 对象以及 Render 方法等等。从文本标签,文本过滤器和 DOM 模板三个方面分别阐述了这些接口的特点和使用方式。最后,介绍了如何基于 Dojo 的 DTL 工具包构建基于 DTL 模板的 Widget。这些接口对我们的日常开发都很有帮助,建议大家平时可以多关注一下。


本文首发于IBM Developer Works:http://www.ibm.com/developerworks/cn/web/1206_zhouxiang_dojodtl/

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值