没有合适的资源?快使用搜索试试~ 我知道了~
大规模Tierless Web编程中的模块化与分离编译的支持
to respect a common interface between the two programs. Thisconstraint is usually not checked automatically, and it is the respon-sibility of the programmer to ensure that the two programs behavein a coherent manner. Of course, such checking is often error-prone.This issue, present in the Web since its inception, has become evenmore relevant in modern Web applications. Furthermore, the unit ofcomposition here is a whole file (or compilation unit): files containeither client code or server code but cannot be composed of bothclient and server code. Such composition is very coarse-grainedand hinders the modularity of Web programming libraries.One goal of a modern client-server Web application frameworkshould be to make it possible to build dynamic Web pages in acomposable way. One should be able to define on the server a func-tion that creates a fragment of a page together with its associatedclient-side behavior; this behavior might depend on the functionparameters. The so-called tierless languages aim to solve such mod-ularity issues by allowing to compose tiers inside expressions, byallowing to freely intersperse the client and server parts of the ap-plication in one language with seamless communication. For mostof these languages, the program is sliced in two: a part which runson the server and a part which is compiled to JavaScript and runson the client. This allows to write libraries and widgets with bothclient and server behaviors. It also provides static guarantees aboutclient-server separation and a fine-grained notion of composition.However, programming large-scale software and libraries stillrequires a form of larger-scale composition. Indeed, parts of a li-brary could be entirely on the server or on the client. Most tierlesslanguages do not support such modular approach to program ar-chitecture. Even worse, almost no tierless programming languagessupport separate compilation. Separate compilation, or its weakerform incremental compilation, is an essential-productivity boostfor programmers working on medium to large scale software.To solve these problems, we propose to leverage a well-knowntool: ML-style modules. By doing so, we gain a convenient para-digm for organizing large-scale software and support for separatecompilation on top of the gains provided by tierless programminglanguages. Our module system is built as a complement to Eliom,a tierless web programming language built on top of OCaml, andretains its good properties such as static typing of client-servercommunications and an efficient excution model.6810大规模Tierless Web编程0Gabriel Radanne University ofFreiburg Germanyradanne@informatik.uni-freiburg.de0Jérôme Vouillon IRIF UMR 8243 CNRS UnivParis Diderot, Sorbonne Paris Cité – CNRSBeSport, Paris, France jerome.vouillon@irif.fr0摘要0TierlessWeb编程语言允许在单个程序中结合客户端和服务器端编程。这样可以定义具有客户端和服务器部分的表达式,并同时提供关于客户端-服务器通信的良好静态保证。然而,这些良好的特性是有代价的:大多数Tierless语言对模块化和分离编译的支持非常差。为了恢复这种模块化并提供更大规模的组合概念,我们提议利用一个众所周知的工具:ML风格的模块。在现代ML语言中,模块系统是与表达式语言分离的一层。Eliom是OCaml的扩展,用于TierlessWeb编程,它提供了类型安全的通信和高效的执行模型。在本文中,我们介绍了Eliom模块系统如何将TierlessWeb编程语言的灵活性与强大的模块系统相结合,从而为抽象、模块化和分离编译提供良好的支持。我们还展示了在提供与OCaml及其生态系统的无缝集成的同时,我们可以提供所有这些优势。0CCS概念0• 软件及其工程 → 函数式语言;0关键词0Web;客户端/服务器;OCaml;ML;Eliom;函数式;模块0ACM参考格式:Gabriel Radanne和JérômeVouillon。2018年。大规模TierlessWeb编程。在2018年Web会议伴侣,2018年4月23日至27日,法国里昂。ACM,美国纽约,9页。https://doi.org/10.1145/3184558.318595301 引言0本论文采用知识共享署名4.0国际许可证(CC BY4.0)发布。作者保留在个人和公司网站上传播作品的权利,并附有适当的归属。WWW'18Companion,2018年4月23日至27日,法国里昂,© 2018IW3C2(国际万维网会议委员会),根据创作共用CC BY 4.0许可证发布。ACM ISBN978-1-4503-5640-4/18/04。https://doi.org/10.1145/3184558.318595301.1 模块0在现代ML语言中,模块语言与表达式语言是分开的。虽然表达式语言允许进行“小规模”编程,但模块语言允许进行“大规模”编程。在大多数语言中,模块是编译单元:一个文件中的简单类型和值声明的集合。SML模块语言[19]使用了这个声明集合的概念,并通过类型(模块规范或签名)、函数(参数化模块或函数器)和函数应用来扩展它,形成了一个小型的类型化函数语言。这样的模块系统可以实现分离编译[16],并提供数据类型抽象的支持[8,17],从而允许隐藏给定类型的实现以强制执行某些不变量。在ML编程语言的历史中,ML风格的模块已经被非正式地证明是非常有表现力的软件架构工具。特别是,函数器允许通过抽象整个模块来编写通用实现。OCaml语言提供了这样一个模块系统,还扩展了各种其他构造,如包类型[32](也称为一级模块)。一个独特的特点是OCaml中的模块是运行时实体。与MLton [23]等系统相比,它们不会在编译时被消除。0Track: Web Programming WWW 2018, 2018年4月23日至27日,法国里昂Eliom [29, 30] is an extension of OCaml for tierless programmingthat supports composable and typesafe client-server interactions. Itprovides fine-grained modularity by allowing to manipulate on theserver, as first class values, fragments of code which will be executedon the client. Eliom is part of the larger Ocsigen [2, 11] project,which also includes the compiler js_of_ocaml [38], a Web server,and various related libraries to build client-server applications.Besides the language presented here, Eliom comes with a completeset of modules, for server and/or client side Web programming,such as RPCs; a functional reactive library for Web programming;a GUI toolkit [25]; a powerful session mechanism and an advancedservice identification mechanism [1]. The Ocsigen project startedin 2004, as a research project, with the goal of building a completeindustrial-strength framework.6820Eliom[29,30]是用于无层次编程的OCaml扩展,支持可组合和类型安全的客户端-服务器交互。它通过允许在服务器上操作代码片段作为一等值来提供细粒度的模块化,这些代码片段将在客户端上执行。Eliom是更大的Ocsigen[2,11]项目的一部分,该项目还包括编译器js_of_ocaml[38]、Web服务器和各种相关库,用于构建客户端-服务器应用程序。除了这里介绍的语言,Eliom还提供了一套完整的模块,用于服务器端和/或客户端Web编程,例如RPC;用于Web编程的函数反应库;GUI工具包[25];强大的会话机制和先进的服务识别机制[1]。Ocsigen项目始于2004年,作为一个研究项目,旨在构建一个完整的工业级框架。01.2 Eliom01.3 用于无层次Web编程的模块0Ocsigen中的所有模块和库,特别是Eliom框架中的模块和库,都是在由Radanne等人描述的核心语言之上实现的[30]。这个核心语言的设计受到四个互补目标的指导:客户端和服务器代码的易组合性、客户端和服务器之间的类型安全通信、易于推理的显式通信和高效的执行模型。我们引入了驱动我们模块语言设计的其他属性:0与宿主语言的集成。Eliom是OCaml的扩展。我们应该能够利用OCaml的语言和生态系统。OCaml库可以在服务器端、客户端或两者上都很有用。因此,任何OCaml文件,即使使用常规的OCaml编译器编译,也是一个有效的Eliom模块。此外,我们可以指定是否要在客户端、服务器端或全局使用给定的库。抽象化。模块语言是非常强大的抽象化工具。通过只公开模块的一部分,程序员可以安全地隐藏实现细节并强制执行特定属性。Eliom利用模块抽象化为小部件和库提供封装和关注点分离。通过结合模块抽象化和无层次特性,库的作者可以提供良好的API,而不会向用户公开客户端-服务器通信的细节。0模块化和分离编译。现代Web应用程序远非简单的网站,其规模与常规桌面程序相当。模块化和分离编译是使程序员能够为大型应用程序提高生产力的关键工具。Eliom是唯一提供静态切片、高效执行和分离编译的无层次编程语言。0在本文的其余部分,我们将介绍Eliom模块系统如何为层次应用程序提供抽象和模块化。作为我们对Eliom语言探索的指导性示例,我们考虑一个评论系统的情况,这种系统可以在许多网站上找到。评论是由用户编写的HTML片段,并由唯一标识符标识。这样的评论库既具有服务器方面(存储和渲染评论)又具有客户端交互(浏览和搜索评论)。我们首先回顾一下OCaml模块系统(第2节)和小规模层次编程(第3节)。然后我们介绍Eliom模块语言(第4节)和其在实现中面临的挑战(第5节)。最后,我们在第6节中与现有工作进行了快速比较。02 评论和骆驼OCAML模块简介0OCaml模块系统形成了一个与表达式语言分开的第二种语言。虽然表达式语言允许“小规模”编程,但模块语言允许“大规模”编程。OCaml所属的ML风格的模块系统显著扩展了通常的模块语言,提供了模块类型(称为签名)和从模块到模块的函数(称为函数器)。模块系统隐式地用于任何类型的OCaml或Eliom编程:每个.ml和.eliom文件形成一个包含文件中包含的声明列表的结构。还可以通过添加.mli或eliomi文件为这样的模块指定签名。我们可以在OCaml模块中做更多的事情。例如,假设我们正在编写一个HTML库。我们想要将与事件相关的属性集中在一个单独的模块中。我们可以使用以下结构轻松实现这一点。01 module On = struct02 let click = ...03 let keypress = ...04 结束0然后可以通过限定访问来使用这些函数:01 open Html02 let mywidget = div ~a:[On.click myclickhandler] [ ... ]0我们HTML库的一些用户可能希望尝试使用新的自定义HTML元素。他们可以通过扩展Html模块轻松实现这一点:01 module HtmlPlus = struct02 include Html03 let blink elems = ...04 结束0在这里,我们声明了一个新的模块HtmlPlus,在其中包含了Html并定义了新的blink函数。include操作简单地将模块的所有字段添加到封闭模块中。这样,我们就得到了一个新的HtmlPlus模块,它可以在任何Html可以使用的地方使用,但也包括了新的函数。0Track: Web Programming WWW 2018, 2018年4月23日至27日,法国里昂3(* ... *)3(* ... *)One important thing to note here is that, to the outer world,these two modules have exactly the same type and can not bedistinguished. The type that implements the identifiers in the IDsignature is abstract: its implementation is only visible inside themodule and can not be used outside. It is also useful to note thatsuch abstraction can be provided after the fact. Declaring a moduleand abstracting its interface are completely distinct operations.Hiding the internal details of our ID modules is not only usefulfor modularity: it also allows to enforce abstraction boundaries. Forexample in the case of SequentialID, it is impossible to inadver-tently use the ID as an integer, since the fact that it is an integeris not revealed! We can use this fact to enforce numerous complexproperties, as we see in the next section.To implement our comment system, we sometimes need to findcomments by their ID. The idiomatic OCaml solution is to use maps,also called dictionaries. Such maps are implemented with BinarySearch Trees which require a comparison function on the keys ofthe map. Map.Make is a pre-defined functor in the OCaml standardlibrary that takes a module implementing the COMPARABLE signatureas argument and returns a module that implements dictionarieswhose keys are of the type t in the provided module. In Fig. 2, we usethis functor to create the IDMap module which defines dictionarieswith IDs as keys. This is very easy, since the ID signature is alreadya super-set of the COMPARABLE signature. We then define register,a function which associates a fresh id to a comment c.The Map.Make functor uses abstraction in two important ways.First, since the type of the map is abstract, it is impossible to modifyit through means not provided by the module. In particular, thisenforces that the binary tree is always balanced. Second, since thecomparison function is provided in advance by the argument ofthe functor, it is impossible to mix different comparison functions51let%server s = ...2let%client c = ...Track: Web Programming WWW 2018, April 23-27, 2018, Lyon, France68302.1 抽象和封装0现在我们想要构建一个简单的库来处理互联网评论。在我们的库中,评论是由唯一的数字标识的HTML片段(使用Html模块构建)。我们还不确定是否应该使用简单的顺序ID、基于日期的ID还是像UUID和Hashids [12]这样的其他东西。幸运的是,我们不必立即做出这个决定!为了编写我们引擎的其余部分,我们所需要的只是一个用于创建和使用标识符的接口。我们可以使用OCaml在模块中声明这样的接口,使用一个签名来描述实现唯一标识符的模块应该是什么样子的。下面,我们声明了ID签名,描述了一个实现唯一标识符的模块应该具有的特征。01 module type ID = sig02 type t (* id的类型 *)03 val compare : t -> t -> int (* 比较id *)04 val create : unit -> t (* 创建新的id *)05 val to_string : t -> string (* 显示id *)06 结束0然后我们可以创建实现这个规范的各种模块。在这里,我们声明了SequentialID和DateID模块。然后我们可以轻松地切换一个模块到另一个模块。01模块SequentialID:ID = struct02类型t = int04结束01模块DateID:ID = struct02类型t = date04结束02.2函数子01模块类型COMPARABLE = sig02类型t03 val compare:t -> t -> int04结束06模块Make(Key:COMPARABLE):sig07类型'a t08 val empty:'a t09 val add:Key.t -> 'a -> 'a t -> 'a t010结束 图1:Map模块01模块TheID = DateID(*我们选择的ID*)02模块IDMap = Map.Make(TheID)03让register c map:Html.t IDMap.t =04 IDMap.add(TheID.create())c map0图2:从ID到评论的字典0错误地应用函数子。实际上,将不同的模块应用于函数子将产生不同类型的映射。02.3进一步0这只是模块的一个简单示例。有关模块的更长(更好)的介绍,请参阅OCaml手册[18]或Real World OCaml书[22]。03层小部件0到目前为止,我们介绍了如何编写对我们的评论系统有用的库的各种元素。为此,我们以各种方式利用了OCaml模块系统的强大功能。现在我们想要编写呈现评论的小部件。为此,我们需要定义客户端和服务器代码,以及一些客户端-服务器通信,这正是层次化语言的优势所在。通过这个例子,我们提供了Eliom表达式语言的快速概述,以及有关层次化Web编程的一些基本概念。03.1 部分0部分注释允许程序员指定声明应该在哪里执行。程序员可以指定声明是在服务器上执行还是在客户端上执行,如下所示:0特别是,部分允许将相关代码分组在同一个文件中,而不管它在哪里执行。在本文的其余部分,我们使用以下颜色约定:客户端为黄色,服务器为蓝色,混合为绿色。然而,颜色对于理解本文的其余部分并不是必需的。03.2 客户端片段0虽然部分注释允许程序员在不同位置收集代码,但它们不允许方便的通信。为此,Eliom允许在服务器部分中包含客户端表达式:放置在[% client...]中的表达式将在接收到页面时在客户端上计算;但是表达式的最终客户端值可以立即作为一个黑盒在服务器上传递。这些表达式称为客户端片段。1let%server x : int fragment = [% client 1 + 3 ]1let%server s : int = 1 + 22let%client c : int = ~%s + 11let%server x : int fragment = [% client 1 + 3 ]2let%client c : int = 3 + ~%x1let%server make_comment commentid =2let content =3p ~a:[a_id commentid] [text (Comment.get commentid)]4in5let author = text ("Author: " ^ Comment.author commentid) in6let handler = [% client fun _ ->7let elem = get_element_by_id ~% commentid in8Css.toggle_hidden elem]9in10div ~a:[On.click handler] [author; content]1val%server make_comment : id -> Html.element1
2Author: MeTrack: Web Programming WWW 2018, April 23-27, 2018, Lyon, France6840在下面的示例中,表达式1 +3将在客户端上计算,但是可以在服务器端引用这个表达式的未来值(例如,将其放入一个列表中)。变量x只能在服务器端使用,类型为intfragment,应该读作“包含某个整数的片段”。客户端片段内的值无法在服务器上访问。03.3 注入0片段允许程序员在服务器上操作客户端值。我们还需要相反的方向。在服务器上计算的值可以通过在前面加上符号~%在客户端上使用。我们称之为注入。0在这里,表达式1 +2在服务器上被计算并绑定到变量s上。结果值3与Web页面一起传输到客户端。表达式~%s +1在客户端计算。注入使得可以在服务器上访问客户端片段。客户端片段内的值通过~%x提取,这里的值是4。03.4 评论小部件0这三个结构足以创建复杂的客户端-服务器交互。在这里,我们使用它们来构建一个非常简单的小部件来显示一个评论。我们的小部件由下面的make_comment函数实现,它还具有一个额外的功能,当用户点击它时,它将隐藏评论的内容。我们还希望HTML在服务器端生成并作为常规HTML页面发送到客户端。这样即使JavaScript无法运行,评论也可以被访问。实现、接口和生成的HTML片段如图3所示。为了实现我们的评论小部件,我们使用了一个HTML DSL[37],它提供了诸如div和a_onclick之类的组合子(分别创建HTML标签和HTML属性)。~a是OCaml语法中的命名参数。在这里,它用于HTML属性列表。我们首先创建一个包含评论文本和唯一id的p元素。文本包含在表示评论的div中。然后我们使用一个监听onclick事件的处理程序:由于点击是在客户端执行的,所以这个处理程序需要是一个片段内的客户端函数。在片段内部,使用注入来访问包含评论标识符的参数id。然后我们使用这个标识符来获取正确的元素并切换“hidden”CSS属性,将其隐藏起来。正如我们所见,这种类型不会暴露小部件行为的内部细节。特别是,服务器和客户端之间的通信不会泄漏在API中:这为客户端-服务器行为提供了适当的封装。此外,这个小部件很容易组合:嵌入式客户端状态既不能影响其他小部件,也不能受到其他小部件的影响,并且可以用来构建更大的小部件。03 < p id =1337>Eliom很棒! p >04 div > 图3:评论小部件 3.5 语义注释0在上面的示例中,我们展示了如何以相当任意的方式交错使用客户端和服务器表达式和通信。如果客户端和服务器之间的通信是天真地进行的,这将是代价高昂的。相反,服务器只在发送Web页面时发送数据一次。特别是,在上面介绍的评论小部件中,评论的id不会在每次点击时发送。这是因为客户端片段在服务器代码中遇到时不会立即执行。直观地说,语义如下。当执行服务器代码时,遇到的客户端代码不会立即执行;而是在Web页面发送到客户端后注册以供稍后执行。只有在此之后才会执行客户端代码。我们还保证客户端代码,无论是客户端部分还是片段,都按照在服务器上遇到的顺序执行。这种表述可能会让人觉得我们在服务器代码执行期间动态创建客户端代码。事实并非如此。与OCaml一样,Eliom是静态编译的,并在编译时将客户端代码和服务器代码分开。在编译过程中,我们静态提取片段中包含的代码,并将其作为客户端代码编译为JavaScript的一部分。这使我们能够提供既能最小化通信又能保持副作用顺序的高效执行方案,同时还提供易于理解的语义。我们还受益于js_of_ocaml编译器的优化,从而产生高效且紧凑的JavaScript代码。03.6 进一步阅读0我们只是简要介绍了Eliom引入的新语言构造可以做什么。Radanne等人[29]提供了许多高级示例,涵盖了许多Web编程习惯,如HTML、RPC、广播和其他通信模式。更正式地说,Radanne等人[30]详细介绍了Eliom表达式语言的类型系统、语义和编译方案。在本文的其余部分,我们将重点介绍通过模块进行大规模无层Web编程。04 无层模块化编程0我们现在有了两个工具。一方面,我们有一个丰富而表达力强大的非无层模块系统,如第2节所介绍的,它在库的抽象和模块化方面提供了支持。2(* ... *)46850另一方面,我们有一种强大的无层编程语言,如第3节所介绍的,它允许我们描述复杂的客户端-服务器行为。在本节中,我们将介绍如何将这两个工具结合起来,在无层设置中利用OCaml模块系统的众多优势。04.1 与OCaml的交互0Web编程从来都不仅仅是关于Web的。Web程序员需要外部库和一个丰富的生态系统,这是一门全新语言所无法提供的。在编写复杂的无层程序之前,让我们看看Eliom如何几乎透明地利用OCaml生态系统。通过使用一个称为base的第三方位置,可以与OCaml语言进行集成。位于base上的代码可以在客户端和服务器上都使用。01 let % base f x = "Hello " ^x^ "!"02 let % client a = f "client"03 let % server b = f "server"0不允许在基本代码中使用Eliom特定的功能,如片段和注入。实际上,基本代码完全对应于OCaml代码。这种等价性在理论上和实践中都成立,这意味着任何由纯OCaml编译器编译的OCaml库都可以直接作为位于基本位置的Eliom库进行重用。这使得与OCaml生态系统的集成非常顺畅。此外,给定的OCaml库可以根据用户的需求加载到基本位置、客户端或服务器上。例如,一个操作文件描述符的OCaml库可能最好只保留在服务器上,以避免误用。如果错误地在客户端上使用该库,类型检查器将引发错误。04.2 模块和位置0如第4.1节所示,OCaml模块(例如从标准库中获取的String模块)可以立即作为位于base上的Eliom模块使用。我们也可以在客户端或服务器上使用这样的模块。01 module % base TextHtml = Html02 module % server ServerHtml = TextHtml03 let % client l = Html.p [Html.text "Hello client!"]0编译器会检查位置。例如,在客户端上禁止使用服务器模块。01 let % client x = ServerHtml.text "hello client!" (* � 错误! *)0还可以自由地重用OCaml模块类型。例如,我们可能希望定义一个名为DomHtml的客户端模块,它与Html模块完全相同的API,但是使用客户端可用的文档对象模型进行实现。这样的模块的类型声明将非常简单,如下所示。01 module % client DomHtml : Html.Signature = struct03 结束0我们可以很容易地在一个位置上声明一个完全新的结构。约束是这些模块上的所有字段,包括子模块,都应该在同一个位置上。例如,客户端结构只能包含在客户端上声明的字段。下面的代码片段声明了一个包含JsMap的客户端模块0使用JavaScript字符串实现具有各种字段的字典数据结构。01 module % client JsMap : sig02 type key03 type 'a t05 val empty : 'a t06 val add : key -> 'a -> 'a t -> 'a t07 结束0我们还可以像在常规OCaml代码中一样在客户端和服务器代码中使用functor。考虑上面的JsMap模块。获得这样一个模块的最简单方法是使用第2.2节中介绍的Map.Makefunctor。例如,我们可以编写一个使用JavaScript本机支持日期的JsDate模块。然后,我们可以简单地将Map.Make应用于Fig.4中定义的JsDate模块,从而获得JsDateMap模块。如预期的那样,我们获得的模块直接位于客户端上。因此,我们可以使用无层次特性和纯OCaml模块混合和匹配客户端和服务器模块。这也适用于所有其他模块特性,如抽象、高阶functor和模块包含。在所有这些情况下,Eliom类型检查器确保模块始终位于适当的位置。01 module % client JsDate = struct02 type t = Js.date03 let compare x y = compare x## valueOf y## valueOf04 (** 按时间戳比较 *)05 结束06 module % client JsDateMap = Map.Make(JsDate)0图4: JsID和JsMap的定义04.3 混合模块0到目前为止,我们只定义了单位置模块,即基本模块、客户端模块或服务器模块。自然而然地,我们也希望编写包含基本、客户端和服务器声明的模块。我们将这些模块称为“混合模块”。01 module % mixed M = struct02 type % client t = int03 type % server t = t fragment04 let % server x: t = [% client 2]05 结束0就像章节一样,混合模块允许将语义相关的声明分组在一起,而不考虑客户端和服务器的边界。然而,将类型声明与混合模块和模块签名结合使用可以提供更多的好处。
下载后可阅读完整内容,剩余1页未读,立即下载
cpongm
- 粉丝: 5
- 资源: 2万+
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
最新资源
- 探索数据转换实验平台在设备装置中的应用
- 使用git-log-to-tikz.py将Git日志转换为TIKZ图形
- 小栗子源码2.9.3版本发布
- 使用Tinder-Hack-Client实现Tinder API交互
- Android Studio新模板:个性化Material Design导航抽屉
- React API分页模块:数据获取与页面管理
- C语言实现顺序表的动态分配方法
- 光催化分解水产氢固溶体催化剂制备技术揭秘
- VS2013环境下tinyxml库的32位与64位编译指南
- 网易云歌词情感分析系统实现与架构
- React应用展示GitHub用户详细信息及项目分析
- LayUI2.1.6帮助文档API功能详解
- 全栈开发实现的chatgpt应用可打包小程序/H5/App
- C++实现顺序表的动态内存分配技术
- Java制作水果格斗游戏:策略与随机性的结合
- 基于若依框架的后台管理系统开发实例解析
资源上传下载、课程学习等过程中有任何疑问或建议,欢迎提出宝贵意见哦~我们会及时处理!
点击此处反馈
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功