yiiframework 简介 五

http://vmee.org/yiiframework%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E5%8D%81%E4%B8%89%E4%B9%8B%E7%BC%93%E5%AD%98

 

 

十三、缓存
缓存是用于提升网站性能的一种即简单又有效的途径。
通过存储相对静态的数据至缓存以备所需,我们可以省去生成这些数据的时间。
在 Yii 中使用缓存主要包括配置和访问缓存组件。
如下的应用配置指定了一个使用两台缓存服务器的 memcache 缓存组件:
array(
......
'components'=>array(
......
'cache'=>array(
'class'=>'system.caching.CMemCache',
'servers'=>array(
array('host'=>'server1', 'port'=>11211, 'weight'=>60),
array('host'=>'server2', 'port'=>11211, 'weight'=>40),
),
),
),
);
程序运行的时候可以通过 Yii::app()->cache 来访问缓存组件。
Yii
提供多种缓存组件以便在不同的媒介上存储缓存数据。
比如 CMemCache 组件封装了 PHP memcache
扩展,它使用内存作为存储缓存的媒介;CApcCache 组件封装了 PHP APC 扩展;CDbCache
组件在数据库里存储缓存数据。下面是各种缓存组件的简要说明:
CMemCache: 使用 PHP memcache 扩展。
CApcCache: 使用 PHP APC 扩展。
CXCache: 使用 PHP XCache 扩展。
CDbCache: 使用一张数据库表来存储缓存数据。
它默认在运行时目录建立并使用一个 SQLite3 数据库,你可以通过设置 connectionID 属性显式地指定一个数据库给它使用。
提示: 因为所有这些缓存组件都从同一个基础类 CCache 扩展而来,不需要修改使用缓存的代码即可在不同的缓存组件之间切换。
缓存可以在不同的级别使用。在最低级别,我们使用缓存来存储单个数据,比如一个变量,我们把它叫做 数据缓存。
往上一级,我们缓存一个由视图脚本生成的页面片断。在最高级别,我们存储整个页面以便需要的时候直接从缓存读取。
接下来我们将阐述如何在这些级别上使用缓存。
注意: 按定义来讲,缓存是一个不稳定的存储媒介,它不保证缓存一定存在——不管该缓存是否过期。
所以,不要使用缓存进行持久存储(比如,不要使用缓存来存储 SESSION 数据)。
一、数据缓存
数据缓存也就是在缓存中存储一些 PHP 变量,过一会再取出来。缓存基础类 CCache 提供了两个最常用的方法:set() 和 get()。
 
要在缓存中存储变量 $value,我们选择一个唯一 ID 并调用 set() 来存储它:
 
Yii::app()->cache->set($id, $value);
 
被缓存的数据会一直保留在缓存中,直到因一些缓存策略而被删除(比如缓存空间满了,删除最旧的数据)。要改变这一行为,我们还可以在调用 set() 时加一个过期参数,这样数据过一段时间就会自动从缓存中清除。
 
// 在缓存中保留该值最多 30 秒
Yii::app()->cache->set($id, $value, 30);
 
当我们稍后需要访问该变量时(不管是不是同一 Web 请求),我们调用 get() (传入 ID)来从缓存中获取它。如果返回值为 false,说明该缓存不可用,需要我们重新生成它。
 
$value=Yii::app()->cache->get($id);
if($value===false)
{
// 因为在缓存中没找到,重新生成 $value
// 再缓存一下以备下次使用
// Yii::app()->cache->set($id,$value);
}
为一个要缓存的变量选择 ID 时,确保该 ID 在应用中是唯一的。
不必保证 ID 在跨应用的情况下保证唯一,因为缓存组件有足够的智能来区分不同应用的缓存 ID。
要从缓存中删除一个缓存值,调用 delete();要清空所有缓存,调用 flush()。
调用 flush() 时要非常小心,因为它会把其它应用的缓存也清空。
提示: 因为 CCache 实现了 ArrayAccess 接口,可以像数组一样使用缓存组件。例如:
$cache=Yii::app()->cache;
$cache['var1']=$value1;  // 相当于: $cache->set('var1',$value1);
$value2=$cache['var2'];  // 相当于: $value2=$cache->get('var2');
 
缓存依赖除了过期设置,缓存数据还会因某些依赖条件发生改变而失效。
如果我们缓存了某文件的内容,而该文件后来又被更新了,我们应该让缓存中的拷贝失效,从文件中读取最新内容(而不是从缓存)。
我们把一个依赖关系表现为一个 CCacheDependency 或它的子类的实例,调用 set() 的时候把依赖实例和要缓存的数据一起传入。
// 缓存将在 30 秒后过期
// 也可能因依赖的文件有更新而更快失效
Yii::app()->cache->set($id, $value, 30, new CFileCacheDependency('FileName'));
如果我们现在调用 get() 从缓存中获取 $value,缓存组件将检查依赖条件。如果有变,我们会得到 false 值——数据需要重新生成。
下面是可用的缓存依赖的简要说明:
CFileCacheDependency: 该依赖因文件的最近修改时间发生改变而改变。
CDirectoryCacheDependency: 该依赖因目录(或其子目录)下的任何文件发生改变而改变。
CDbCacheDependency: 该依赖因指定的 SQL 语句的查询结果发生改变而改变。
CGlobalStateCacheDependency: 该依赖因指定的全局状态值发生改变而改变。
全局状态是应用中跨请求、跨 SESSION 的持久变量,它由 CApplication::setGlobalState() 来定义。
CChainedCacheDependency: 该依赖因依赖链中的任何一环发生改变而改变。
二、片段缓存(Fragment Caching)
片段缓存指缓存网页某片段。
例如,如果一个页面在表中显示每年的销售摘要,我们可以存储此表在缓存中,减少每次请求需要重新产生的时间。
要使用片段缓存,在控制器视图脚本中调用CController::beginCache() 和CController::endCache() 。
这两种方法开始和结束包括的页面内容将被缓存。
类似data caching ,我们需要一个编号,识别被缓存的片段。
...别的HTML内容...
beginCache($id)) { ?>
...被缓存的内容...
endCache(); } ?>
...别的HTML内容...
在上面的,如果beginCache() 返回false,缓存的内容将此地方自动插入; 否则,在if语句内的内容将被执行并在endCache()触发时缓存。
1. 缓存选项(Caching Options)
当调用beginCache(),可以提供一个数组由缓存选项组成的作为第二个参数,以自定义片段缓存。
事实上为了方便,beginCache() 和 endCache()方法是 COutputCache widget的包装。
因此COutputCache的所有属性都可以在缓存选项中初始化。
2. 有效期(Duration)
也许是最常见的选项是duration,指定了内容在缓存中多久有效。和CCache::set()过期参数有点类似。下面的代码缓存内容片段最多一小时:
...其他HTML内容...
beginCache($id, array('duration'=>3600))) { ?>
...被缓存的内容...
endCache(); } ?>
...其他HTML内容...
如果我们不设定期限,它将默认为60 ,这意味着60秒后缓存内容将无效。
3. 依赖(Dependency)
像data caching ,内容片段被缓存也可以有依赖。
例如,文章的内容被显示取决于文章是否被修改。
要指定一个依赖,我们建立了dependency选项,可以是一个实现ICacheDependency的对象或可用于生成依赖对象的配置数组。
下面的代码指定片段内容取决lastModified 列的值是否变化:
...其他HTML内容...
beginCache($id, array('dependency'=>array(
'class'=>'system.caching.dependencies.CDbCacheDependency',
'sql'=>'SELECT MAX(lastModified) FROM Post')))) { ?>
...被缓存的内容...
endCache(); } ?>
...其他HTML内容...
4. 变化(Variation)
缓存的内容可根据一些参数变化。
例如,每个人的档案都不一样。缓存的档案内容将根据每个人ID变化。这意味着,当调用beginCache()时将用不同的ID。
COutputCache内置了这一特征,程序员不需要编写根据ID变动内容的模式。以下是摘要。
varyByRoute: 设置此选项为true ,缓存的内容将根据route变化。因此,每个控制器和行动的组合将有一个单独的缓存内容。
varyBySession: 设置此选项为true ,缓存的内容将根据session ID变化。因此,每个用户会话可能会看到由缓存提供的不同内容。
varyByParam:
设置此选项的数组里的名字,缓存的内容将根据GET参数的值变动。例如,如果一个页面显示文章的内容根据id的GET参数,我们可以指定
varyByParam为array('id'),以使我们能够缓存每篇文章内容。如果没有这样的变化,我们只能能够缓存某一文章。
5. 请求类型(Request Types)
有时候,我们希望片段缓存只对某些类型的请求启用。
例如,对于某张网页上显示表单,我们只想要缓存initially requested表单(通过GET请求)。
任何随后显示(通过POST请求)的表单将不被缓存,因为表单可能包含用户输入。
要做到这一点,我们可以指定
requestTypes 选项:
...其他HTML内容...
beginCache($id, array('requestTypes'=>array('GET')))) { ?>
...被缓存的内容...
endCache(); } ?>
...其他HTML内容...
6. 嵌套缓存(Nested Caching)
片段缓存可以嵌套。就是说一个缓存片段附在一个更大的片段缓存里。
例如,意见缓存在内部片段缓存,而且它们一起在外部缓存中在文章内容里缓存。
...其他HTML内容...
beginCache($id1)) { ?>
...外部被缓存内容...
beginCache($id2)) { ?>
...内部被缓存内容...
endCache(); } ?>
...外部被缓存内容...
endCache(); } ?>
...其他HTML内容...
嵌套缓存可以设定不同的缓存选项。
例如,
在上面的例子中内部缓存和外部缓存可以设置时间长短不同的持续值。
当数据存储在外部缓存无效,内部缓存仍然可以提供有效的内部片段。
然而,反之就不行了。如果外部缓存包含有效的数据, 它会永远保持缓存副本,即使内容中的内部缓存已经过期。
三、页面缓存
页面缓存指的是缓存整个页面的内容。页面缓存可以发生在不同的地方。
例如,通过选择适当的页面头,客户端的浏览器可能会缓存网页浏览有限时间。 
Web应用程序本身也可以在缓存中存储网页内容。 在本节中,我们侧重于后一种办法。
页面缓存可以被看作是 片段缓存 (/doc/guide/caching.fragment)一个特殊情况。
由于网页内容是往往通过应用布局来生成,如果我们只是简单的在布局中调用 beginCache()和endCache(),将无法正常工作。
这是因为布局在CController::render()方法里的加载是在页面内容产生之后。
缓存整个页面,我们应该跳过产生网页内容的动作执行。
我们可以使用 COutputCache 作为动作 过滤器 (/doc/guide/basics.controller#filter)来完成这一任务。
下面的代码演示如何配置缓存过滤器:
public function filters()
{
return array(
array(
'system.web.widgets.COutputCache',
'duration'=>100,
'varyByParam'=>array('id'),
),
);
}
上述过滤器配置会使过滤器适用于控制器中的所有行动。我们可能会限制它在一个或几个行动通过使用插件操作器。更多的细节中可以看过滤器(/doc/guide/basics.controller#filter) 。
提示:我们可以使用 COutputCache 作为一个过滤器,因为它从CFilterWidget继承过来
,这意味着它是一个工具(widget)和一个过滤器。
事实上, widge的工作方式和过滤器非常相似:工具widget(过滤器filter)是在action动作里的内容执行前执行,在执行后结束。
四、动态内容(Dynamic Content)
当使用fragment caching或page caching,我们常常遇到的这样的情况整个部分的输出除了个别地方都是静态的。
例如,帮助页可能会显示静态的帮助信息,而用户名称显示的是当前用户的。
解决这个问题,我们可以根据用户名匹配缓存内容,但是这将是我们宝贵空间一个巨大的浪费,因为缓存除了用户名其他大部分内容是相同的。
我们还可以把网页切成几个片段并分别缓存,但这种情况会使页面和代码变得非常复杂。
更好的方法是使用由 CController 提供的动态内容dynamic content功能 。
动态内容是指片段输出即使是在片段缓存包括的内容中也不会被缓存。
即使是包括的内容是从缓存中取出,为了使动态内容在所有时间是动态的,每次都得重新生成。
出于这个原因,我们要求动态内容通过一些方法或函数生成。
调用CController::renderDynamic()在你想的地方插入动态内容。
...别的HTML内容...
beginCache($id)) { ?>
...被缓存的片段内容...
renderDynamic($callback); ?>
...被缓存的片段内容...
endCache(); } ?>
...别的HTML内容...
在上面的, $callback指的是有效的PHP回调。
它可以是指向当前控制器类的方法或者全局函数的字符串名。
它也可以是一个数组名指向一个类的方法。其他任何的参数,将传递到renderDynamic()方法中。回调将返回动态内容而不是仅仅显示它。

 

 

 

 

http://vmee.org/yiiframework%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E5%8D%81%E5%9B%9B%E4%B9%8B%E6%89%A9%E5%B1%95

 

 

十四、扩展Yii
在开发中扩展Yii是一个很常见的行为.例如,当你写一个新的控制器时,你通过继承 CController  类扩展了Yii; 
当你编写一个新的组件时,
你正在继承 CWidget 或者一个已存在的组件类.如果扩展代码是由第三方开发者为了复用而设计的,我们则称之为extension(扩展)。
一个扩展通常是为了一个单一的目的服务的.在 Yii 中,他可以按照如下分类:
* 应用的部件
* 组件
* 控制器
* 动作
* 过滤器
* 控制台命令
* 校验器: 校验器是一个继承自 CValidator 类的部件。
* 辅助器: 辅助器是一个只具有静态方法的类.它类似于使用类名作为命名空间的全局函数。
* 模块: 模块是一个有着若干个类文件和相应特长文件的包.一个模块通常更高级,比一个单一的部件具备更先进的功能.例如我们可以拥有一个具备整套用户管理功能的模块。
扩展也可以是不属于上述分类中的任何一个的部件。事实上,Yii 是设计得很谨慎的,以至于几乎它的每段代码都可以被扩展和订制以适用于特定需求。
一、使用扩展
使用扩展通常包含了以下三步:
1. 从 Yii 的 扩展库 下载扩展。
2. 解压到 应用程序的基目录 的子目录 extensions/xyz 下,这里的 xyz 是扩展的名称。
3. 导入, 配置和使用扩展。
每个扩展都有一个所有扩展中唯一的名称标识。把一个扩展命名为 xyz ,我们也可以使用路径别名定位到包含了 xyz 所有文件的基目录。
不同的扩展有着不同的导入,配置,使用要求.以下是我们通常会用到扩展的场景,按照他们在 概述 中的描述分类。
1、应用的部件
使用 应用的部件, 首先我们需要添加一个新条目到 应用配置 的 components 属性, 如下所示:
return array(
// 'preload'=>array('xyz',...),
'components'=>array(
'xyz'=>array(
'class'=>'application.extensions.xyz.XyzClass',
'property1'=>'value1',
'property2'=>'value2',
),
// 其他部件配置
),
);
然后,我们可以在任何地方通过使用 Yii::app()->xyz 来访问部件.部件将会被 惰性创建(就是,仅当它第一次被访问时创建.) , 除非我们把它配置到 preload 属性里。
2、组件
组件 主要用在 视图 里.假设组件类 XyzClass 属于 xyz 扩展,我们可以如下在视图中使用它:
// 组件不需要主体内容
widget('application.extensions.xyz.XyzClass', array(
'property1'=>'value1',
'property2'=>'value2')); ?>
 
// 组件可以包含主体内容
beginWidget('application.extensions.xyz.XyzClass', array(
'property1'=>'value1',
'property2'=>'value2')); ?>
 
...组件的主体内容...
 
endWidget(); ?>
 
3、动作
动作 被 控制器 用于响应指定的用户请求.假设动作的类 XyzClass 属于 xyz 扩展,我们可以在我们的控制器类里重写 CController::actions 方法来使用它:
class TestController extends CController
{
public function actions()
{
return array(
'xyz'=>array(
'class'=>'application.extensions.xyz.XyzClass',
'property1'=>'value1',
'property2'=>'value2',
),
// 其他动作
);
}
}
然后,我们可以通过 路由 test/xyz 来访问。
4、过滤器
过滤器 也被 控制器 使用。过滤器主要用于当其被 动作 挂起时预处理,提交处理用户请求。
假设过滤器的类 XyzClass 属于 xyz 扩展,我们可以在我们的控制器类里重写 CController::filters 方法来使用它:
class TestController extends CController
{
public function filters()
{
return array(
array(
'application.extensions.xyz.XyzClass',
'property1'=>'value1',
'property2'=>'value2',
),
// 其他过滤器
);
}
}
在上述代码中,我们可以在数组的第一个元素离使用加号或者减号操作符来限定过滤器只在那些动作中生效。更多信息,请参照文档的 CController。
5、控制器
控制器 提供了一套可以被用户请求的动作。我们需要在 应用配置 里设置 CWebApplication::controllerMap 属性,才能在控制器里使用扩展:
return array(
'controllerMap'=>array(
'xyz'=>array(
'class'=>'application.extensions.xyz.XyzClass',
'property1'=>'value1',
'property2'=>'value2',
),
// 其他控制器
),
);
然后, 一个在控制里的 a 行为就可以通过 路由 xyz/a 来访问了。
6、校验器
校验器主要用在 模型类 (继承自 CFormModel 或者 CActiveRecord) 中.假设校验器类 XyzClass 属于 xyz 扩展,我们可以在我们的模型类中通过 CModel::rules 重写 CModel::rules 来使用它:
class MyModel extends CActiveRecord // or CFormModel
{
public function rules()
{
return array(
array(
'attr1, attr2',
'application.extensions.xyz.XyzClass',
'property1'=>'value1',
'property2'=>'value2',
),
// 其他校验规则
);
}
}
7、控制台命令
控制台命令扩展通常使用一个额外的命令来增强 yiic 的功能.假设命令控制台 XyzClass 属于 xyz 扩展,我们可以通过设定控制台应用的配置来使用它:
return array(
'commandMap'=>array(
'xyz'=>array(
'class'=>'application.extensions.xyz.XyzClass',
'property1'=>'value1',
'property2'=>'value2',
),
// 其他命令
),
);
 
然后,我们就能使用配备了额外命令 xyz 的 yiic 工具了。
注意: 控制台应用通常使用了一个不同于 Web 应用的配置文件.如果使用了 yiic webapp 命令创建了一个应用,这样的话,控制台应用的
protected/yiic 的配置文件就是 protected/config/console.php 了,而Web应用的配置文件 则是protected/config/main.php。
8、模块
模块通常由多个类文件组成,且往往综合上述扩展类型。因此,你应该按照和以下一致的指令来使用模块。
9、通用部件
使用一个通用 部件, 我们首先需要通过使用
Yii::import('application.extensions.xyz.XyzClass');
来包含它的类文件。然后,我们既可以创建一个类的实例,配置它的属性,也可以调用它的方法。我们还可以创建一个新的子类来扩展它。
二、创建扩展
由于扩展意味着是第三方开发者使用,需要一些额外的努力去创建它。以下是一些一般性的指导原则:
*扩展最好是自己自足。也就是说,其外部的依赖应是最少的。如果用户的扩展需要安装额外的软件包,类或资源档案,这将是一个头疼的问题。
*文件属于同一个扩展的,应组织在同一目录下,目录名用扩展名称。
*扩展里面的类应使用一些单词字母前缀,以避免与其他扩展命名冲突。
*扩展应该提供详细的安装和API文档。这将减少其他开发员使用扩展时花费的时间和精力。
*扩展应该用适当的许可。如果您想您的扩展能在开源和闭源项目中使用,你可以考虑使用许可证,如BSD的,麻省理工学院等,但不是GPL的,因为它要求其衍生的代码是开源的。
在下面,我们根据 overview中所描述的分类,描述如何创建一个新的扩展。当您要创建一个主要用于在您自己项目的component部件,这些描述也适用。
1、Application Component(应用部件)
一个application component 应实现接口IApplicationComponent或继承CApplicationComponent。
主要需要实现的方法是 IApplicationComponent::init,部件在此执行一些初始化工作。
此方法在部件创建和属性值(在application configuration里指定的 )被赋值后调用。
默认情况下,一个应用程序部件创建和初始化,只有当它首次访问期间要求处理。
如果一个应用程序部件需要在应用程序实例被创建后创建,它应要求用户在CApplication::preload 的属性中列出他的编号。
2、Widget(小工具)
widget应继承CWidget或其子类。 A widget should extend from CWidget or its child classes.
最简单的方式建立一个新的小工具是继承一个现成的小工具和重载它的方法或改变其默认的属性值。
例如,如果您想为CTabView使用更好的CSS样式,您可以配置其CTabView::cssFile属性,当使用的小工具时。
您还可以继承CTabView如下,让您在使用小工具时,不再需要配置属性。
class MyTabView extends CTabView
{
public function init()
{
if($this->cssFile===null)
{
$file=dirname(__FILE__).DIRECTORY_SEPARATOR.'tabview.css';
$this->cssFile=Yii::app()->getAssetManager()->publish($file);
}
parent::init();
}
}
在上面的,我们重载CWidget::init方法和指定CTabView::cssFile的
URL到我们的新的默认CSS样式如果此属性未设置时。我们把新的CSS样式文件和MyTabView类文件放在相同的目录下,以便他们能够封装成扩展。
由于CSS样式文件不是通过Web访问,我们需要发布作为一项asset资源。
要从零开始创建一个新的小工具,我们主要是需要实现两个方
法:CWidget::init 和CWidget::run。第一种方法是当我们在视图中使用 $this->beginWidget
插入一个小工具时被调用,第二种方法在$this->endWidget被调用时调用。如果我们想在这两个方法调用之间捕捉和处理显示的内容,我们
可以开始output buffering在CWidget::init 和在CWidget::run中回收缓冲输出作进一步处理。 
If we want to capture and process the content displayed between these two method invocations, we can start output buffering in CWidget::init and
retrieve the buffered output in CWidget::run for further processing.
 
在网页中使用的小工具,小工具往往包括CSS,java script或其他资源文件。
我们叫这些文件assets,因为他们和小工具类在一起,而且通常Web用户无法访问。
为了使这些档案通过Web访问,我们需要用CWebApplication::assetManager发布他们,例如上述代码段所
示。此外,如果我们想包括CSS或java script文件在当前的网页,我们需要使用CClientScript注册 :
class MyWidget extends CWidget
{
protected function registerClientScript()
{
// ...publish CSS or java script file here...
$cs=Yii::app()->clientScript;
$cs->registerCssFile($cssFile);
$cs->registerScriptFile($jsFile);
}
}
小工具也可能有自己的视图文件。如果是这样,创建一个目录命名views在包括小工具类文件的目录下,并把所有的视图文件放里面。在小工具类中使用$this->render('ViewName') 来render渲染小工具视图,类似于我们在控制器里做。
3、Action(动作)
action应继承CAction或者其子类。action要实现的主要方法是IAction::run 。
4、Filter(过滤器)
filter应继承CFilter 或者其子类。filter要实现的主要方法是CFilter::preFilter和CFilter::postFilter。前者是在action之前被执行,而后者是在之后。
class MyFilter extends CFilter
{
protected function preFilter($filterChain)
{
// logic being applied before the action is executed
return true; // false if the action should not be executed
}
 
protected function postFilter($filterChain)
{
// logic being applied after the action is executed
}
}
参数$filterChain的类型是CFilterChain,其包含当前被filter的action的相关信息。
5、Controller(控制器)
controller
要作为扩展需继承CExtController,而不是 CController。主要的原因是因为CController 认定控制器视图文件位于application.views.ControllerID 下,
而CExtController认定视图文件在views目录下,也是包含控制器类目录的一个子目录。因此,很容易重新分配控制器,因为它的视图文件和控制类是在一起的。
6、Validator(验证)
Validator需继承CValidator和实现CValidator::validateAttribute方法。
class MyValidator extends CValidator
{
protected function validateAttribute($model,$attribute)
{
$value=$model->$attribute;
if($value has error)
$model->addError($attribute,$errorMessage);
}
}
7、Console Command(控制台命令)
console command 应继承CConsoleCommand和实现CConsoleCommand::run方法。 或者,我们可以重载CConsoleCommand::getHelp来提供一些更好的有关帮助命令。
class MyCommand extends CConsoleCommand
{
public function run($args)
{
// $args gives an array of the command-line arguments for this command
}
 
public function getHelp()
{
return 'Usage: how to use this command';
}
}
8、Module(模块)
请参阅modules一节中关于就如何创建一个模块。
一般准则制订一个模块,它应该是独立的。模块所使用的资源文件(如CSS , java script ,图片),应该和模块一起分发。还有模块应发布它们,以便可以Web访问它们 。
9、Generic Component(通用组件)
开发一个通用组件扩展类似写一个类。还有,该组件还应该自足,以便它可以很容易地被其他开发者使用。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章