取势 明道 优术

作者为 扶 凯 发表


 

名称

Dancer::Cookbook – 一个快速的教你使用 Dancer 框架的指南

 


描述

这个中有一些小的例子帮助你快速的取得和使用 Dancer 的 Web 框架

 


开始使用 Dancer

 

你的第一个 Dancer 应用

Dancer 被设计成非常易于使用 – 对于写简单的Web应用程序来讲是微不足道的, 但它仍可以在较大的项目中很好的工作.首先,让我们做一个非常简单的 “Hello World” 的范例:

    #!/usr/bin/perl

    use Dancer;

    get '/hello/:name' => sub {
        return "Why, hello there " . params->{name};
    };

    dance;

没错 – 上面你所见到的这个,是一个功能完整的 Web 应用程序, 运行上面这个脚本将启动一个网络服务器,会打开默认端口(3000)监听,现在你可以打开这个看看:

    $ curl http://localhost:3000/hello/Bob
    Why, hello there Bob

(注意,如果不是在本地运行,上面的 Localhost 要换成你远程的名字),显示的内容会讲 "hello". 这个 :name 是一个名字做为参数放到路由指定的路由中处理,其值会从路由的路径中传送过来.

注意,您没有必要在 Dancer 中使用strictwarning,Dancer 默认会加载这二个. (如果你不想使用 warnings(例如,这可能会导致出现很多不受欢迎的警告),可以通过设置 import_warnings 为假值.

 

开始 Dancer 的项目

第一个简单的例子只合适一些微小项目,但对于任何更复杂一点的啦? 你想更易于维护的解决方案 – 就是使用 Dancer 的帮助脚本,它通过一个单行命令将为你的应用建立一个的应用程序的框架:

    + mywebapp
    + mywebapp/config.yml
    + mywebapp/environments
    + mywebapp/environments/development.yml
    + mywebapp/environments/production.yml
    + mywebapp/views
    + mywebapp/views/index.tt
    + mywebapp/views/layouts
    + mywebapp/views/layouts/main.tt
    + mywebapp/mywebapp.pl
    + mywebapp/lib
    + mywebapp/lib/mywebapp.pm
    + mywebapp/public
    + mywebapp/public/css
    + mywebapp/public/css/style.css
    + mywebapp/public/css/error.css
    + mywebapp/public/images
    + mywebapp/public/404.html
    + mywebapp/public/dispatch.fcgi
    + mywebapp/public/dispatch.cgi
    + mywebapp/public/500.html
    + mywebapp/Makefile.PL
    + mywebapp/t
    + mywebapp/t/002_index_route.t
    + mywebapp/t/001_base.t

正如你可以看到,它创建一个以应用程序的名称来命名的目录,连同一些配置文件,view 的目录(您的 templates 和 layouts),环境目录(在特定环境下的设置), 一个包含您的实际应用程序的模块(lib/mywebapp.pm),一个启动脚本(mywebapp.pl) – 或通过 Plack/ PSGI 运行您的Web应用程序.

 


Dancer 中的路径选择: 路由

 

声明路由

要控制你的 webapp 收到 Web 请求时会做什么样的处理,你需要先声明 routes 的路径.一个路径声明需要指出返回响应中,有效的HTTP 方法,它匹配的路径(例如/foo/bar),和要执行的 coderef.

    get '/hello/:name' => sub {
        return "Hi there " . params->{name};
    };

上面的路由声明指定,GET 的请求 “/hello/…' 的路径, 然后执行所提供的代码块.

 

处理多个 HTTP 的请求方法

路由使用 any 就可以匹配所有的 HTTP 的方法,或指定的 HTTP 方法列表.

以下将匹配任何 HTTP 请求,只要路径是 /myaction:

    any '/myaction' => sub {
        # code
    }

下面的方法只匹配到/myaction路径的 GET 和 POST 的方法的请求

    any ['get', 'post'] => '/myaction' => sub {
        # code
    };

为方便起见,any 路由会匹配任何 GET 请求 也同样匹配 HEAD 请求.

 

检索请求参数

这个 params 关键字返回请求参数的 hash 引用,将这些参数值存到查询字符串中和路径名本身(命名参数)内. 如果是HTTP POST请求,会包含 POST 正文内容.

 

在路由时路径声明的命名参数

如上所述,您可以使用 :somename 做为路由的路径,捕捉路径的一部分做为参数,这个实现也是通过调用 params..

因此,对于一个 web 应用程序,你要显示不同公司的信息,你可能会这样使用:

    get '/company/view/:companyid' => sub {
        my $company_id = params->{companyid};
        # Look up the company and return appropriate page
    };

 

通配符进行路径匹配和 splat

您也可以使用通配符来声明路径,后面可以 splat 关键字来找到他们嵌入的值:

    get '/*/*' => sub {
        my ($action, $id) = splat;
        if (my $action eq 'view') {
            return display_item($id);
        } elsif ($action eq 'delete') {
            return delete_item($id);
        } else {
            status 'not_found';
            return "What?";
        }
    };

 

请求前处理过滤

前过滤器 before 声明的代码,是用来处理一个请求传递进来后,还没给适当的路由前.

    before sub {
        var note => 'Hi there';
        request->path('/foo/oversee')
    };

    get '/foo/*' => sub {
        my ($match) = splat; # 'oversee';
        vars->{note}; # 'Hi there'
    };

上述声明过滤器,在路由处理之前,使用 var 设置一个变量.,然后修订路径为 /foo/oversee ,这意味着,无论用户输入任何路径,它将会是认为请求 /foo/oversee

更多可以查看 Dancer hook 的钩子相关的内容.

 

默认路由

如果你想避免出现 404 error 的错误,或以同样的方式处理多个路由,你不喜欢为所有的这些都配置一次,您可以设置一个默认的路由处理.

默认路由的处理程序,将会处理任何没找到的 URL.

所有你需要做的,设置 last 路由作为最后的默认的路由设置:

    any qr{.*} => sub {
        status 'not_found';
        template 'special_404', { path => request->path };
    };

你可以在你的 template 中这样写到配合上面:

    You tried to reach <% path %>, but it is unavailable at the moment.

    Please try again or contact us at our email at <...>.

 

使用 auto_page 的特性来自动创建路由

有一些简单的静态网页,我们只要简单的打开 auto_page 的配置中的这个设置就行了,这样你就没有必要声明相关的路由来处理这些内容.

如果请求的是 /foo/bar, Dancer 将匹配的视图 /foo/bar.tt ,检查这个 tt 和使用默认的 layout 等加载视图,如有关详情,请参阅 auto_page setting 的设置的文档

 

为什么要使用AJAX插件

Ajax 的查询仅仅只是一个 HTTP 查询,它类似于一个 GET 或 POST 的路由. 你可能会问自己为什么要使用 ajax 的关键字(要使用 Dancer::Plugin::Ajax),而不是简单的 get.

比如说,在你的应用程序中有一个这样的路径'/user/:user'.您可能希望能够输出普通的页面,布局和 HTML 内容.但是你可能还希望也能够调用一个 javascript 使用 Ajax 查询此相同的 URL.

因此,我们来先看看以前要使用的方法的代码:

    get '/user/:user' => sub {
         if (request->is_ajax) {
             # create xml, set headers to text/xml, blablabla
              header('Content-Type' => 'text/xml');
              header('Cache-Control' =>  'no-store, no-cache, must-revalidate');
              to_xml({...})
         }else{
             template users, {....}
         }
    };

现在我们可以写下面二段:

    get '/user/:user' => sub {
        template users, {...}
    }
 
也可以是:

    ajax '/user/:user' => sub {
         to_xml({...}, RootName => undef);
    }

ajax 查询,所以你知道你只需要返回一个 XML 内容,和设置响应的内容的类型属性.

 

使用前缀功能来分割您的应用程序

为了有更好的可维护性,您可能希望您的应用程序给组件分开到一些不同的包. 比方说,我们有一个简单的 Web 应用有个管理的部分,希望放在不同的包:

    package myapp;
    use Dancer ':syntax';
    use myapp::admin;

    prefix undef;

    get '/' => sub {...};

    1;

    package myapp::admin;
    use Dancer ':syntax';

    prefix '/admin';

    get '/' => sub {...};

    1;

他会为我们生成如下的路由的路径:

    - get /
    - get /admin/
    - head /
    - head /admin/

 


MUSCLE MEMORY: 存储数据

 

处理 sessions

你的 Web 应用程序要使用会话来保持状态,这是常用功能,例如,允许用户登录后,创建一个会话,并检查该会话的后续请求是否是登状态.

要使用的 sessions,你必须先启用 session 的引擎 – 选择您要使用的 session 引擎,然后在您的配置文件中声明并添加:

    session: Simple

the Dancer::Session::Simple manpage 后端实现非常的简单,在内存中存着 session 的信息.这对应用测试可以很方便快速和有用.但 session 不能持久存着.因为重起应用就会没有了.

你也可以在 Dancer 中使用 the Dancer::Session::YAML manpage, 它会给 session 的状态存在 YAML 的文件中 ( YAML 是对于人类来讲非常好读的格式,可以更加方便的检查会话.):

    session: YAML

当然,这个也需要选择 session 支持这个,直接在 Dancer 中写如下配置就行了,

    set session => 'YAML';

(控制的设置最好写进你的配置文件). "YAML"的例子是使用的 session 后端使用,这是 Dancer 中的 the Dancer::Session::YAML manpage 的简写. session 的后端,你可以使用其他的,例如 "the Dancer::Session::Memcache manpage",只是 YAML 做为 session 的后端简单,易于使用).

你可以使用 session 的关键字来操纵 session;

 

在 session 上存储的数据

在 session 中存储的数据是非常容易,因为:

    session varname => 'value';

 

从 session 中检索数据

从 session 中检索数据也是非常容易,因为:

    session('varname')

也可以直接:

    session->{varname}

 

控制要存储的 session

对于基于硬盘的 session 象 the Dancer::Session::YAML manpage, the Dancer::Session::Storable manpage 等等, session 文件是写到 session 的目录,可以使用 session_dir 来指定这个目录.如果没有设置,默认的这个是 appdir/sessions .

如果你想控制这个文件的位置,你可以直接在你的配置文件中写如下的信息:

    session_dir: /tmp/dancer-sessions

如果您指定的目录不存在,Dancer 将尝试创建它.

 

摧毁一个会话

当你不在需要这个 session 时,你可以调用下面的方法:

    session->destroy

 

session 和登录

常见的实际应用要求是这样,检查用户登录,如果没有,要求他们进入登陆界面登录,然后再继续.

实现这可以很容易,只要使用前过滤器,以检查每个会话:

    before sub {

        if (! session('user') && request->path_info !~ m{^/login}) {
            var requested_path => request->path_info;
            request->path_info('/login');
        }
    };

    get '/login' => sub {
        # Display a login page; the original URL they requested is available as
        # vars->{requested_path}, so could be put in a hidden field in the form
        template 'login', { path => vars->{requested_path} };
    };

    post '/login' => sub {
        # Validate the username and password they supplied
        if (params->{user} eq 'bob' && params->{pass} eq 'letmein') {
            session user => params->{user};
            redirect params->{path} || '/';
        } else {
            redirect '/login?failed=1';
        }
    };

在登录页面的模板中,你需要用户名的文本字段和密码字段和一个隐藏的命名路径,这是用来请求的路径,向这个来发送POST提交,一旦你登录,可用于 POST 路由来重定向页面.

当然,你可能需要从数据库来验证您的用户密码,或通过 IMAP/LDAP/SSH/POP3/本地系统帐户,在这里使用 the Authen::Simple manpage 是一个很好的起点,! 一个工作中常用的实例就是通过你自己的数据库的表来认证.(使用 database 关键字来调用 the Dancer::Plugin::Database manpage .然后使用 the Crypt::SaltedHash manpage 来进行密码的 hash (你不会想给你的密码存成明文的对吧?)).

    post '/login' => sub {
        my $user = database->quick_select('users', 
            { username => params->{user} }
        );
        if (!$user) {
            warning "Failed login for unrecognised user " . params->{user};
            redirect '/login?failed=1';
        } else {
            if (Crypt::SaltedHash->validate($user->{password}, params->{pass}))
            {
                debug "Password correct";
                # Logged in successfully
                session user => $user;
                redirect params->{path} || '/';
            } else {
                debug("Login failed - password incorrect for " . params->{user});
                redirect '/login?failed=1';
            }
        }
    };

 

找回存储在Session中的 hash

取得存起来的 session 中的 hash 如下:

    my $hash = session;

 


外观

 

使用模板 templates – 视图views 和布局 layouts

返回清澈的内容是所有好应用,所以我们很快会想使用模板,以维护和实现代码和内容的分离.Dancer 使得这非常容易.

你的路由处理程序可以使用 template 关键字来呈现模板

 

Views视图

所有可能呈现的模板的内容与动作,这就是所谓的 view 视图. “appdir/views”目录就是放置 view 所在的地方.

您可以更改这个位置来改变设置“views”.默认内部使用的 template 的引擎是 the Dancer::Template::Simple manpage.但如果你想升级成 Template::Toolkit. 如果你想这样.你需要加打 the Dancer::Template::TemplateToolkit manpage 这个引擎,你需要导入这个 Template 模块到你的应用的代码中.

注意,默认 Dancer 配置使用的 Template::Toolkit 的模板引擎是使用的 <% %> 替换掉了默认 TT 的 [% %],你可以在你的配置文件中加入如下的内容

    template: template_toolkit

    engines:
        template_toolkit:
            start_tag: '[%'
            stop_tag: '%]'

所有 view 的模板文件,必须有一个".tt"的后缀.这可能在未来改变.

为了渲染视图,只需调用 template|Dancer/template 的关键字,然后 加上视图名称和要插入的 hashref (请注意:为方便起见“request, session, params 和 vars”在 view, anmed request, session, paramsvars 中是自动的可以访问的) 例如:

    before => sub { var time => scalar(localtime) };

    get '/hello/:name' => sub {
        my $name = params->{name};
        template 'hello.tt', { name => $name };
    };

这个 'hello.tt' 的模板要包含下面的内容:

    <p>Hi there, <% name %>!</p>
    <p>You're using <% request.user_agent %></p>
    <% IF session.username %>
        <p>You're logged in as <% session.username %>
    <% END %>
    It's currently <% vars.time %>

这样,上面 name 这个列表就会自动的在 hello.tt 的模板中显示 (象 session, requestvars, 引用 the Dancer::Template::Abstract manpage)

 

Layouts布局

布局是一种特殊的 view视图,在位于“layouts”目录(在 views 的目录中),其中必须有个作为标志的名字叫“content”.该名字的标志的地方来呈现动作视图.在这你可以定义了一个全局的布局,做为默认的布局,并有每个各自的 view 都包含仅有的具体内容.这个非常好,可以避免大量的不必要的重复的HTML:)

这是一个例子,布局文件在 views/layouts/main.tt:

    <html>
        <head>...</head>
        <body>
        <div id="header">
        ...
        </div>

        <div id="content">
        <% content %>
        </div>

        </body>
    </html>

你也可以直接告诉你的应用,你可以在你的配置文件中,直接告诉这的布局文件使用的 layout: name ,也可以在你的应用中写

    set layout => 'main';

您可以控制特定的请求的布局(或通过 template 关键字作为第三个参数的选项 hashref,而不改变布局设置,所以并不在所有布局中生效):

    template 'index.tt', {}, { layout => undef };

如果你的应用没有 moute 到 root(/),你可以使用 before_template 替换和硬编码路径到你的应用中用来处理 css,image 和 javascript.

    before_template sub {
        my $tokens = shift;
        $tokens->{uri_base} = request->base->path;
    };

然后在你的布局文件中,修改你的 css 导入的地址:

    <link rel="stylesheet" href="<% uri_base %>/css/style.css" />

现在,你可以使用任何地址,在你的应用中,都能自动的修改 css 的路径了.

 

template 和 unicode

如果您使用 Plack,有一些与您 Dancer 应用程序相关的 unicode 问题,要记的检查是否已设置您的模板引擎使用 Unicode,并设置默认的字符集为UTF-8.所以,如果你正在使用template toolkit,你config.yml会看起来像这:

    charset: UTF-8
    engines:
      template_toolkit:
        ENCODING: utf8

 

TT's WRAPPER directive in Dancer (META variables, SETs)

Dancer 已经提供了 WRAPPER 那样的能力,我们称之为“布局”.我们不使用TT的WRAPPER(这也使得它与它不兼容)的原因是,并非所有的模板系统支持.

然而,你可能还是想使用它,并能够定义元变量和 the Template::Toolkit manpage 的变量.

这几个步骤,将让你可以这样:

完成了!一切都将正常工作.

 


设置阶段:配置和日志记录

 

配置和环境

在很多方法可以用来做舞者的应用配置.最简单的一个(也许是最肮脏的),就是把你所有的设置语句,写在你的脚本的顶部和调用 dancer() 方法之前. 其他一些常用方法,你可以定义在文件中'appdir/config.yml“来设置所有的设置.但对于这一点,你必须有安装的YAML模块,才能写在YAML配置文件.

这比第一个方案好,但它仍然不是完美,因为你不能在多个环境中轻松切换,而不需要重新编写一个config.yml的文件.

更好的方法是一个全局设置默认config.yml文件,有类似下面的:

    # appdir/config.yml
    logger: 'file'
    layout: 'main'

然后写环境配置 appdir/environments .这样,适当的环境配置文件将被加载运行环境(如果没有指定,这将是“development”).

注意,你可以改变运行环境,使用 --environment 命令行来修改.

通常情况下,你要在开发用的配置文件设置以下值:

    # appdir/environments/development.yml
    log: 'debug'
    startup_info: 1
    show_errors:  1

另外生产环境需要:

    # appdir/environments/production.yml
    log: 'warning'
    startup_info: 0
    show_errors:  0

 

访问您的应用程序的配置信息

Dancer 的应用可以非常容易的使用 ‘config’ 的关键字来在配置文件中设置信息,可以方便地访问在其配置文件的设置,例如:

    get '/appname' => sub {
        return "This is " . config->{appname};
    };

这使得您的应用程序的设置都保存在一个地方,简单,易于.

 

从一个单独的脚本中访问配置信息

你可能想从你的 webapp 外访问您的 webapp 的配置.当然,您可以使用您所选择的YAML模块和加载的webapps的config.yml,但这都是不方便的.

使用 Dancer 对象模型,可以使用 config.yml 的本身的值和一些额外的默认值的值:

        # bin/script1.pl
        use Dancer ':syntax';
        print "template:".config->{template}."\n"; #simple
        print "log:".config->{log}."\n"; #undef

请注意,config->{log}结果是 undef error,因为你没有加载的环境,默认的日志中定义在环境中,并不在config.yml的文件中,因此UNDEF.

如果你想加载一个环境,你需要告诉 Dancer 在哪里找到这个.这样做,是要告诉 Dancer .从 Dacner 中取得减去 config.yml 文件(通常 $webapp/config.yml)

        # bin/script2.pl
        use FindBin;
        use Cwd qw/realpath/;
        use Dancer ':syntax';

        #tell the Dancer where the app lives
        my $appdir=realpath( "$FindBin::Bin/..");

        Dancer::Config::setting('appdir',$appdir);
        Dancer::Config::load();

        #getter
        print "environment:".config->{environment}."\n"; #development
        print "log:".config->{log}."\n"; #value from development environment

默认 Dancer 是从 $webapp/environment/development.yml 来读入开发的环境配置的. 对比之前的例子,你现在更新了一个环境变量的值.注意这个例子中使用了 Cwd和 FindBin . 这可以用在任何写到的 Dancer 的项目中,如果没有必要全部进行这种修改.你只是想简单的修改一个应用的路径,你可以这样:

        Dancer::Config::setting('appdir','/path/to/app/dir');

如果你想在其它的地方也加载这个环境变量,只要试着象下面这样就行:

        # bin/script2.pl
        use Dancer ':syntax';

        #tell the Dancer where the app lives
        Dancer::Config::setting('appdir','/path/to/app/dir');

        #which environment to load
        config->{environment}='production';

        Dancer::Config::load();

        #getter
        print "log:".config->{log}."\n"; #has value from production environment

顺便说一下,你不仅可以获得值,也可以像我们上面的方法来简单的设定 config->{environment}='production'. .当然,这个值不写入任何文件,它只是在内存中.

 

日志

 

配置日志

我们可能想记录由应用程序和 Dancer 本身产生的一些信息到日志中.

要开始记录日志,你需要启用日志记录的引擎,只要设置 logger 的配置,Dancer 包括二种方法输出日志 fileconsole ,这分别记录日志到日志文件和直接打印出日志.

打开 logging 的选项,在配置文件 config.yml 中这样写:

    logger: 'file'

你可以改变日志打印的级别:

    log: 'core'      # will log debug, warning and errors, and messages from
                     # Dancer itself
    log: 'debug'     # will log debug, warning and errors
    log: 'warning'   # will log warning and errors
    log: 'error'     # will log only errors

如果您使用的是记录日志到文件中,会自动生成一个目录 appdir/logs,每一个环境会生成一个日志文件.日志消息会有当前的时间,当前进程的PID,一些信息和调用信息(那个文件和行生成的日志).

 

记录自己的日志消息

只要调用 debug, warning or error 在你的应用中的信息前面:

    debug "This is a debug message from my app.";

 


REST

 

写 REST 的应用

我们在 Dancer 写 REST 的应用也很容易,Dancer 提供了一此帮助我们序列化和反序列化数据格式的功能:

JSON
YAML
XML
Data::Dumper

要启用这些特性,你需要设置 serializer 的这个选项.在配置文件中的格式如下:

   serializer: JSON

在代码中启动也是可以的:

   set serializer => 'JSON';

现在,全部的 hash 的引用,和数组的引用,在路由处理中,都会自动的序列化成你设置的格式.全部的 POST or PUT 请求发送过来的数据也会被自动的反序列化.

    get '/hello/:name' => sub {
        # this structure will be returned to the client as
        # {"name":"$name"}
        return {name => params->{name}};
    };

它可以让客户选择他要使用序列化.对于这个功能,我们可以使用 mutable 可变的序列化器,并根据 Content-Type 来选择适当的序列化程序.

使用 send_error 关键字,它可以返回自定义的错误,…当你不使用一个序列,这个 send_error 会将一个字符串作为第一个参数和一个可选的 HTTP 代码.使用序列化时,该消息可以是一个字符串,一个 arrayref 或一个 hashref

    get '/hello/:name' => sub {
        if (...) {
           send_error("you can't do that");
           # or
           send_error({reason => 'access denied', message => "no"});
        }
    };

出错的信息的内容也将被序列化.

 

部署 Dancer 应用

有关部署 Dancer 应用(包括 standalone,代理/负载均衡软件,使用通用的 Web 服务器 Apache 运行通过 CGI/ FastCGI 的等的例子,请看 the Dancer::Deployment manpage.

 


Dancer 部署的过程

 

Plack 中间件

如果与 Plack 一起部署,并想使用一些 Plack 的中间件,你可以让他们直接用 Dancer 的配置文件来启用.

 

通用中间件

在 Dancer 中要启用中间件,你只要设置 plack_middlewares 就可以了:

    set plack_middlewares => [
        [ 'SomeMiddleware' => [ qw(some options for somemiddleware) ]],
    ];

例如,你在 Dancer 中想启用 the Plack::Middleware::Debug manpage ,我们只需要这样写 plack_middlewares

    set plack_middlewares => [
        [ 'Debug' => [ 'panels' => qw(DBITrace Memory Timer) ]],
    ];

当然,你也可以写到你的 config.yml 的配置文件中,要么写到你的环境的配置文件中:

    # environments/development.yml
    ...
    plack_middlewares:
      -
        - Debug          # first element of the array is the name of the middleware
        - panels         # following elements are the configuration ofthe middleware
        -
          - DBITrace
          - Memory
          - Timer

 

基于路径来应用中间件

如果你想设置一个特定的路径应用不同中间件,你可以使用 plack_middlewares_map. 你需要 the Plack::App::URLMap manpage 来实现这个.

    plack_middlewares_map:
        '/':      ['Debug']
        '/timer': ['Timer'],

 


AUTHORS

Dancer contributors – see AUTHORS file. 译者: 扶凯

 


NOTE

本文由个人根据 cpan 翻译,可能由于水平有限,译文质量望读者见谅,不屑一顾者请绕道远行,英文好的请直接看原著(最好也帮助翻译). — PerlChina 重建计划作品

来了就留个评论吧! 3个评论



    woosley 2011年11月6日 的 13:09

    PerlChina在讨论如何发展国内的Perl社区[1],
    看来是从翻译开始[2],
    我觉得如果你有兴趣,可以去那里转转看。

    [1]http://groups.google.com/group/perlchina/browse_thread/thread/ea7989eeef01ebb0
    [2]https://github.com/PerlChina/POD_CN

    陈子 2011年11月6日 的 13:58

    前天在看Dancer::Plugin::WebSocket的时候刚看到plack_middlewares_map这个了,说明很少~

    laird 2011年11月23日 的 02:13

    哈哈。。。dancer 我喜欢这个东西。。