性能调优
在本章中,我们将会讨论如下话题:
- 使用最佳实践
- 加速session处理
- 使用缓存依赖和chains
- profiling一个Yii应用
- Leveraging HTTP缓存
- 合并和最小化资源
- 在HHVM上运行Yii2
Yii是现有最快的框架中的一个。然后,当开发和部署一个应用时,有一些免费的额外性能和遵守最佳实践是有好处的。在本章中,你将会看到如何配置Yii来获取额外的性能。此外,你将会了解到一些最佳实践,可用于开发你的应用,能顺畅的运行,知道你有非常高的负载。
使用最佳实践
在本小节中,你将会看到如何配置Yii2,得到最好的性能,以及额外的创建响应式应用的原则。这些原则既是常用的也是Yii相关的。因此,我们将能使用这些原则,甚至不使用Yii2时也可以。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
如何做…
- 更新你的PHP到最新的稳定版本。PHP的主发布版可能会带来非常大的性能提升。关掉调试模式,并设置为
prod环境。这可以通过编辑web/index.php:
defined('YII_DEBUG') or define('YII_DEBUG', false);defined('YII_ENV') or define('YII_ENV', 'prod');
注意:在yii2-app-advanced应用框架中,你可以使用shell命令php init,以及选择生产环境,用于加载优化的index.php和配置文件。
- 激活
cache组件:
'components' => ['cache' => ['class' => 'yii\caching\FileCache',],],
你可以使用任何缓存存储,不只是FileCache。此外,你可以注册多个缓存应用组件,并使用Yii::$app->cache和Yii::$app->cache2来获取不同的数据类型:
'components' => ['cache' => ['class' => 'yii\caching\MemCache','useMemcached' => true,],'cache2' => ['class' => 'yii\caching\FileCache',],],
这个框架默认在它自己的类中使用cache组件。
- 为
db组件激活表schema缓存:
return [// ...'components' => [// ...'cache' => ['class' => 'yii\caching\FileCache',],'db' => ['class' => 'yii\db\Connection','dsn' => 'mysql:host=localhost;dbname=mydatabase','username' => 'root','password' => '','enableSchemaCache' => true,// Optional. Default value is 3600 seconds'schemaCacheDuration' => 3600,// Optional. Default value is 'cache''schemaCache' => 'cache',],],];
- 使用纯数据,而不是ActiveRecord对象来列出元素的集合。
$categoriesArray = Categories::find()->asArray()->all();
- 在
foreach中使用each()而不是all()来获取大量的结果:
foreach (Post::find()->each() as $post) {// ...}
- 因为Composer的autoloader被用于包含大部分的第三方类文件,你应该考虑通过如下命令优化它:
composer dump-autoload -o
工作原理…
当YII_DEBUG被设置为false时,Yii关闭了所有trace级别的日志,并使用较少的错误处理代码。此外,当你设置YII_ENV为prod时,你的应用不会加载Yii和Debug面板模块。
设置schemaCachingDuration为一个以秒为单位的数字,允许Yii的ActiveRecord缓存数据的schema。对于生产环境,我们非常建议这样做,它会大幅提高ActiveRecord的性能。为了使它能功能,你需要正确的配置cache:
'cache' => ['class' => 'yii\cache\FileCache',],
激活缓存对其它Yii组件也有正面的影响。例如,Yii路由或者urlManager从cache路由开始。
当然,你可以进入到一种情况,先前的设置对于显著的提升性能没有帮助。在大部分情况下,这意味着这个应用本身是一个瓶颈,你需要更多的硬件。
- 服务端性能只是重点中的一部分:服务端性能只是所有能影响全局性能中的一个点。通过优化客户端,例如CSS、图像和Javascript文件,正确的缓存和减少HTTP请求的数量,可以有一个很好的可见的性能提升,即使是没有优化PHP代码。
- 不使用Yii做事:有些事情如果不使用Yii可以很好的完成。例如,实时修改图像大小在一个独立的PHP脚本中进行会更快,可以避免额外的负载。
- Active Record和Query Builder以及SQL对比:在对性能比较敏感的应用部分使用Query Builder和SQL。一般情况下,AR对于添加和编辑记录非常有用,因为它添加一个很方便的校验层,但当查询记录时并没有什么用。
- 经常检查慢查询:如果开发者意外忘记给一个表格添加索引,当数据库经常被读取时,数据库就会成为性能瓶颈;反之亦然,如果添加太多的索引,而又要经常写数据。同样的事情会发生在选择不必要的数据以及不需要的JOINs。
- 缓存或者保存重型过程的结果:如果你可以在每一个页面加载过程中避免运行一个重型过程,这最好了。例如,保存或者缓存解析的markdown文本,净化一次后(这是一个非常耗资源的过程),以后就是可以直接用于展示的HTML了。
- 处理太多的过程:有时有太多的过程需要立即处理。它可以创建复杂的报告,或者只是简单的发送电子邮件(如果你的项目加载的很重)。在这种情况下,最好将它放入到队列中,然后使用cron或者其它指定的工具来处理。
参考
欲了解更多关于性能调优和缓存的信息,参考如下地址:
- http://www.yiiframework.com/doc-2.0/guide-tutorial-performance-tuning.html
- http://www.yiiframework.com/doc-2.0/guide-caching-overview.html
加速session处理
在PHP中原生的session处理在大部分情况下已经非常好了。但至少有两个可能原因,你希望改变session的处理方式:
- 当使用多个服务器时,需要有统一的session存储。
- 默认的PHP session使用文件,所以最大的性能瓶颈在磁盘I/O上。
- 默认的PHP session是阻塞并发的session存储。在这个小节中,我们将会看到如何使用Yii做高效的session存储。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。并安装Memcache服务器和memcache PHP扩展。
如何做…
我们使用apache的ab工具对网站做压力测试。它是和apache二进制文件一起发布,所以如果正在使用apache,你将会在bin文件夹中找到它。
- 运行如下命令,并将网址替换成你的正在使用的网站的网址:
ab -n 1000 -c 5 http://yii-book.app/index.php?r=site/contact
这将会发送1000次请求,一次发送5个,并会得到如下输出统计:
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd,http://www.zeustech.net/Licensed to The Apache Software Foundation,http://www.apache.org/...Server Software: nginxServer Hostname: yii-book.appServer Port: 80Document Path: /index.php?r=site/contactDocument Length: 14866 bytesConcurrency Level: 5Time taken for tests: 10.961 secondsComplete requests: 1000Failed requests: 0Total transferred: 15442000 bytesHTML transferred: 14866000 bytesRequests per second: 91.24 [#/sec] (mean)Time per request: 54.803 [ms] (mean)Time per request: 10.961 [ms] (mean, across allconcurrent requests)Transfer rate: 1375.84 [Kbytes/sec] receivedConnection Times (ms)min mean[+/-sd] median maxConnect: 0 0 0.0 0 0Processing: 18 55 324.9 29 4702Waiting: 15 41 255.1 24 4695Total: 18 55 324.9 29 4702
我们对每秒请求次数指标(requests-per-second,简称QPS)感兴趣。这个值意味着这个网站在并发数为5的情况下,每秒可以处理91.24次请求。
注意:注意调试并没有关闭,因为我们对修改session处理速度感兴趣。
- 现在添加如下代码到
/config/web.php组件部分:
'session' => array('class' => 'yii\web\CacheSession','cache' => 'sessionCache',),'sessionCache' => array('class' => 'yii\caching\MemCache',),
- 再次以相同的设置运行
ab。这次,你应该能得到更好的结果。在我的例子中,QPS是139.07。这意味着Memcache,作为一个session处理器,相对于基于文件的session处理器提升了52%的性能。
注意:不要依赖于这里提供的精确的结果。它依赖于软件版本、设置和使用的硬件。经常尝试在你即将部署应用的环境中,运行所有的测试。
- 通过选择正确的session处理后端,你可以得到一个显著的性能提升。Yii支持更多的缓存后端out-of-the-box,包括WinCache、XCache和Zend Data Cache,它来自于Zend Server。而且,你可以实施你自己的缓存后端,来使用快速的noSQL存储,例如Redis。
工作原理…
默认情况下,Yii使用原生PHP session;这意味着大部分情况下使用文件系统。文件系统并不能高效的处理高并发请求。
Memcache或者其它平台在如下情况下,能很好的执行:
'session' => array('class' => 'yii\web\CacheSession','cache' => 'sessionCache',),'sessionCache' => array('class' => 'yii\caching\MemCache',),
在先前的配置部分,我们在Yii中使用CacheSession作为一个session处理器。使用这个组件,我们可以委托session处理器为cache中指定的缓存组件。这次我们使用MemCache。
当使用一个memcached后端,你应该考虑到这个事实,当使用这些解决方案时,当缓存达到最大存储容量时,应用用户可能丢失session。
注意:注意到,当为一个session使用一个缓存后端时,你不能依赖于一个session作为一个临时数据存储,因为在memcached中将不会有更多内存来存储更多数据。在这个例子中,只需要清理所有的数据,并清除其中的一部分。
如果你在使用多个服务器,你不能使用文件存储。没有办法来分享多个服务器之间的session数据。在memcached的例子中,这非常容易,因为它可以被多个服务器访问。
此外,对于分享session数据,你可以使用DbSession:
return [// ...'components' => ['session' => ['class' => 'yii\web\DbSession',],],];
现在,在你的数据库中创建一个张新表:
CREATE TABLE session (id CHAR(40) NOT NULL PRIMARY KEY,expire INTEGER,data BLOB)
更多…
尽可能关闭session是一个好主意。如果你不想在当前的请求中在session中存储任何数据,你甚至可以在你的控制器动作一开始就关闭它。这样,在你的应用中即使是使用文件作为存储也是没关系的。
使用如下命令:
Yii:$app->session->close();
参考
欲了解更多关于性能和缓存的信息,参考如下地址:
- http://www.yiiframework.com/doc-2.0/guide-tutorial-performance-tuning.html
- http://www.yiiframework.com/doc-2.0/guide-caching-overview.html
使用缓存依赖和chains
Yii支持需要缓存后端,但是使Yii缓存灵活的是依赖和依赖chaining支持。有一些情况,你不能简单的只缓存1个小时的数据,因为信息随时可能会边。
在这个小节中,我们将会学习如何缓存整个页面,并能在有更新时获取最新的数据。这个页面是一个仪表盘类型的,将会展示5个最新添加的文章,以及总数。
注意:注意一个操作不能被编辑 as it is added,但是一个文章可以。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
- 在
config/web.php中激活缓存组件:
return [// ...'components' => ['cache' => ['class' => 'yii\caching\FileCache',],],];
- 设置一个新的数据库,并将它配置到
config/db.php中: - 运行如下migration:
<?phpuse yii\db\Schema;use yii\db\Migration;class m160308_093233_create_example_tables extends Migration{public function up(){$tableOptions = null;if ($this->db->driverName === 'mysql') {$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';}$this->createTable('{{%account}}', ['id' => Schema::TYPE_PK,'amount' => Schema::TYPE_DECIMAL . '(10,2) NOT NULL',], $tableOptions);$this->createTable('{{%article}}', ['id' => Schema::TYPE_PK,'title' => Schema::TYPE_STRING . ' NOT NULL','text' => Schema::TYPE_TEXT . ' NOT NULL',], $tableOptions);}public function down(){$this->dropTable('{{%article}}');$this->dropTable('{{%account}}');}}
- 使用Yii为account和article表生成模型。
- 创建
protected/controllers/DashboardController.php:
<?phpnamespace app\controllers;use app\models\Account;use app\models\Article;use yii\web\Controller;class DashboardController extends Controller{public function actionIndex(){$total = Account::find()->sum('amount');$articles = Article::find()->orderBy('id DESC')->limit(5)->all();return $this->render('index', array('total' => $total,'articles' => $articles,));}public function actionRandomOperation(){$rec = new Account();$rec->amount = rand(-1000, 1000);$rec->save();echo 'OK';}public function actionRandomArticle(){$n = rand(0, 1000);$article = new Article();$article->title = "Title #".$n;$article->text = "Text #".$n;$article->save();echo 'OK';}}
- 创建
views/dashboard/index.php:
<?phpuse yii\helpers\Html;/* @var $this yii\web\View *//* @var $total int *//* @var $articles app\models\Article[] */?><h1>Total: <?= $total ?></h1><h2>5 latest articles:</h2><?php foreach($articles as $article): ?><h3><?= Html::encode($article->title) ?></h3><div><?= Html::encode($article->text) ?></div><?php endforeach ?>
- 运行
dashboard/random-operation和dashboard/random-article几次,然后,运行dashboard/index你将会看到如下所示的截图:

- 在页面的底部,点击调试面板上数据库查询的数量:

看到一个查询列表:

如何做…
执行如下步骤:
- 我们需要修改控制器的代码:
<?phpnamespace app\controllers;use app\models\Account;use app\models\Article;use yii\caching\DbDependency;use yii\caching\TagDependency;use yii\web\Controller;class DashboardController extends Controller{public function behaviors(){return ['pageCache' => ['class' => 'yii\filters\PageCache','only' => ['index'],'duration' => 24 * 3600 * 365, // 1 year'dependency' => ['class' => 'yii\caching\ChainedDependency','dependencies' => [new TagDependency(['tags' =>['articles']]),new DbDependency(['sql' => 'SELECT MAX(id) FROM ' . Account::tableName()])]],],];}public function actionIndex(){$total = Account::find()->sum('amount');$articles = Article::find()->orderBy('id DESC')->limit(5)->all();return $this->render('index', array('total' => $total,'articles' => $articles,));}public function actionRandomOperation(){$rec = new Account();$rec->amount = rand(-1000, 1000);$rec->save();echo 'OK';}public function actionRandomArticle(){$n = rand(0, 1000);$article = new Article();$article->title = "Title #".$n;$article->text = "Text #".$n;$article->save();TagDependency::invalidate(\Yii::$app->cache,'articles');echo 'OK';}}
- 完成了。现在,在加载
dashboard/index几次以后,你将会看到只有1个查询,如下所示:

此外,尝试运行dashboard/random-operation或者dashboard/randomarticle,并刷新dashboard/index。数据将会改变:

工作原理…
为了修改最少的代码,并达到最高的性能,我们使用一个过滤器来作全页缓存:
public function behaviors(){return ['pageCache' => ['class' => 'yii\filters\PageCache','only' => ['index'],'duration' => 24 * 3600 * 365, // 1 year'dependency' => ['class' => 'yii\caching\ChainedDependency','dependencies' => [new TagDependency(['tags' => ['articles']]),new DbDependency(['sql' => 'SELECT MAX(id) FROM account'])]],],];}
先前的代码意味着我们在index动作中应用一个全页缓存。这个页面将会缓存1年,并且如果数据改变了,这个缓存将会刷新。因此,一般情况下,依赖工作如下:
- 按依赖中的描述,第一次运行会获取最新的数据,保存以备之后调用,并更新缓存
- 按依赖中的描述,获取最新的数据,获取保存的数据,然后比较两者
- 如果相等,使用缓存的数据
- 如果不相等,更新缓存,使用最新的数据,并保存最新依赖数据,以备以后调用
在我们的例子中,使用了两个类型的依赖——标签和DB。一个标签依赖使用自定义字符串标签标记数据,并检查它,来决定缓存是否无效,一个DB依赖使用SQL查询结果来达到相同的目的。
现在你可能有的问题是,“为什么有时候使用DB而有时候使用标签?”这是一个好问题。
使用DB依赖的目的是,替换重的计算,并选择一个轻的查询,尽可能获取少的数据。关于这种依赖最好的事情是,我们不需要在已有的代码中嵌入任何额外的逻辑。在我们的例子中,我们可以使用这种类型的依赖用于账户操作,但是不能用于文章,因为文章的内容会改变。因此,对于文章,我们设置一个全局标签,名叫文章,它表示我们可以手动调用来刷新文章缓存:
TagDependency::invalidate(\Yii::$app->cache, 'articles');
参考
欲了解更多关于缓存的信息,并使用缓存依赖,参考http://www.yiiframework.com/doc-2.0/guide-caching-overview.html。
使用Yii profiling一个应用
如果在部署一个Yii应用时,你使用了所有的最佳实践,但是你仍然得不到你想要的性能,很有可能是应用本身存在一些性能瓶颈。在处理这些性能瓶颈时最主要的原则是你不应该假设任何事请,并在尝试优化它之前去测试和profile代码。
在本小节中,我们将会尝试找出Yii2最小应用的性能瓶颈。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
- 设置数据库连接,并应用如下migration:
<?phpuse yii\db\Migration;class m160308_093233_create_example_tables extends Migration{public function up(){$tableOptions = null;if ($this->db->driverName === 'mysql') {$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';}$this->createTable('{{%category}}', ['id' => $this->primaryKey(),'name' => $this->string()->notNull(),], $tableOptions);$this->createTable('{{%article}}', ['id' => $this->primaryKey(),'category_id' => $this->integer()->notNull(),'title' => $this->string()->notNull(),'text' => $this->text()->notNull(),], $tableOptions);$this->createIndex('idx-article-category_id','{{%article}}', 'category_id');$this->addForeignKey('fk-article-category_id','{{%article}}', 'category_id', '{{%category}}', 'id');}public function down(){$this->dropTable('{{%article}}');$this->dropTable('{{%category}}');}}
- 在Yii中为每一个表生成模型。
- 写如下控制台命令:
<?phpnamespace app\commands;use app\models\Article;use app\models\Category;use Faker\Factory;use yii\console\Controller;class DataController extends Controller{public function actionInit(){$db = \Yii::$app->db;$faker = Factory::create();$transaction = $db->beginTransaction();try {$categories = [];for ($id = 1; $id <= 100; $id++) {$categories[] = ['id' => $id,'name' => $faker->name,];}$db->createCommand()->batchInsert(Category::tableName(), ['id', 'name'], $categories)->execute();$articles = [];for ($id = 1; $id <= 100; $id++) {$articles[] = ['id' => $id,'category_id' => $faker->numberBetween(1, 100),'title' => $faker->text($maxNbChars = 100),'text' => $faker->text($maxNbChars = 200),];}$db->createCommand()->batchInsert(Article::tableName(), ['id', 'category_id', 'title', 'text'], $articles)->execute();$transaction->commit();} catch (\Exception $e) {$transaction->rollBack();throw $e;}}}
并执行它:
./yii data/init
- 添加
ArticleController类:
<?phpnamespace app\controllers;use Yii;use app\models\Article;use yii\data\ActiveDataProvider;use yii\web\Controller;class ArticleController extends Controller{public function actionIndex(){$query = Article::find();$dataProvider = new ActiveDataProvider(['query' => $query,]);return $this->render('index', ['dataProvider' => $dataProvider,]);}}
- 添加
views/article/index.php视图:
<?phpuse yii\helpers\Html;use yii\widgets\ListView;/* @var $this yii\web\View *//* @var $dataProvider yii\data\ActiveDataProvider */$this->title = 'Articles';$this->params['breadcrumbs'][] = $this->title;?><div class="article-index"><h1><?= Html::encode($this->title) ?></h1><?= ListView::widget(['dataProvider' => $dataProvider,'itemOptions' => ['class' => 'item'],'itemView' => '_item',]) ?></div>
然后添加views/article/_item.php:
<?phpuse yii\helpers\Html;/* @var $this yii\web\View *//* @var $model app\models\Article */?><div class="panel panel-default"><div class="panel-heading"><?= Html::encode($model->title);?></div><div class="panel-body">Category: <?= Html::encode($model->category->name) ?></div></div>
如何做…
跟随如下步骤,profile基于Yii的应用:
- 打开文章页面:

- 打开
views/article/index.php并在ListView小部件之前和之后添加profiler调用:
<div class="article-index"><h1><?= Html::encode($this->title) ?></h1><?php Yii::beginProfile('articles') ?><?= ListView::widget(['dataProvider' => $dataProvider,'itemOptions' => ['class' => 'item'],'itemView' => '_item',]) ?><?php Yii::endProfile('articles') ?></div>
现在刷新这个页面。
- 展开页面底部的调试面板,点击timing badge(在我们的例子中是73ms):

现在检查Profiling报告:

我们可以看到我们的文章块花费了将近40ms。
- 打开我们的控制器,并为文章的
category关系添加主动加载:
class ArticleController extends Controller{public function actionIndex(){$query = Article::find()->with('category');$dataProvider = new ActiveDataProvider(['query' => $query,]);return $this->render('index', ['dataProvider' => $dataProvider,]);}}
- 回到网站,刷新页面,再次打开Profiling报告:

现在这个文章列表花费了将近25ms,因为应用使用主动加载做了更少的SQL查询。
工作原理…
你可以使用Yii::beginProfile和Yii::endProfile查看源代码的任何片段:
Yii::beginProfile('articles');// ...Yii::endProfile('articles');
在执行过页面以后,你可以在调试模块的Profiling页面看到这个报告,有所有的执行时间。
此外,你可以使用嵌套profiling调用:
Yii::beginProfile('outer');Yii::beginProfile('inner');// ...Yii::endProfile('inner');Yii::endProfile('outer');
注意:注意要正确的打开和关闭调用,以及正确命名block名称。如果你忘记调用Yii::endProfile,或者颠倒了Yii::endProfile('inner')和Yii::endProfile('outer')的嵌套顺序,性能Profiling将不会工作。
参考
- 欲了解更多关于logger的信息,参考http://www.yiiframework.com/doc-2.0/guide-runtime-logging.html#performance-profiling
- 关于应用性能的调优,参考http://www.yiiframework.com/doc-2.0/guide-tutorial-performance-tuning.html
Leveraging HTTP缓存
不只是服务端的缓存,你通过设置HTTP头可以使用客户端缓存。
在这个小节中,我们将会讨论基于Last-Modified和ETag头的全页缓存。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
- 创建并运行migration:
<?phpuse yii\db\Migration;class m160308_093233_create_example_tables extends Migration{public function up(){$this->createTable('{{%article}}', ['id' => $this->primaryKey(),'created_at' => $this->integer()->unsigned()->notNull(),'updated_at' =>$this->integer()->unsigned()->notNull(),'title' => $this->string()->notNull(),'text' => $this->text()->notNull(),]);}public function down(){$this->dropTable('{{%article}}');}}
- 创建
Article模型:
<?phpnamespace app\models;use Yii;use yii\behaviors\TimestampBehavior;use yii\db\ActiveRecord;class Article extends ActiveRecord{public static function tableName(){return '{{%article}}';}public function behaviors(){return [TimestampBehavior::className(),];}}
- 创建带有如下动作的博客控制器:
<?phpnamespace app\controllers;use app\models\Article;use yii\web\Controller;use yii\web\NotFoundHttpException;class BlogController extends Controller{public function actionIndex(){$articles = Article::find()->orderBy(['id' =>SORT_DESC])->all();return $this->render('index', array('articles' => $articles,));}public function actionView($id){$article = $this->findModel($id);return $this->render('view', array('article' => $article,));}public function actionCreate(){$n = rand(0, 1000);$article = new Article();$article->title = 'Title #' . $n;$article->text = 'Text #' . $n;$article->save();echo 'OK';}public function actionUpdate($id){$article = $this->findModel($id);$n = rand(0, 1000);$article->title = 'Title #' . $n;$article->text = 'Text #' . $n;$article->save();echo 'OK';}private function findModel($id){if (($model = Article::findOne($id)) !== null) {return $model;} else {throw new NotFoundHttpException('The requested page does not exist.');}}}
- 添加
views/blog/index.php视图:
<?phpuse yii\helpers\Html;$this->title = 'Articles';;$this->params['breadcrumbs'][] = $this->title;?><?php foreach($articles as $article): ?><h3><?= Html::a(Html::encode($article->title), ['view','id' => $article->id]) ?></h3><div>Created <?= Yii::$app->formatter->asDatetime($article->created_at) ?></div><div>Updated <?= Yii::$app->formatter->asDatetime($article->updated_at) ?></div><?php endforeach ?>
- 添加视图
views/blog/view.php:
<?phpuse yii\helpers\Html;$this->title = $article->title;$this->params['breadcrumbs'][] = ['label' => 'Articles', 'url' => ['index']];$this->params['breadcrumbs'][] = $this->title;?><h1><?= Html::encode($article->title) ?></h1><div>Created <?= Yii::$app->formatter->asDatetime($article->created_at) ?></div><div>Updated <?= Yii::$app->formatter->asDatetime($article->updated_at) ?></div><hr /><p><?= Yii::$app->formatter->asNtext($article->text) ?></p>
如何做…
执行如下步骤来leverage HTTP缓存:
- 访问
http://yii-book.app/index.php?r=blog/create三次,来创建三个文章。 - 打开如下博客地址:

- 在你的浏览器中打开开发者控制台,每次刷新页面都可以看到
200 OK的响应状态:

- 打开
BlogController并附加如下行为:
<?phpclass BlogController extends Controller{public function behaviors(){return [['class' => 'yii\filters\HttpCache','only' => ['index'],'lastModified' => function ($action, $params) {return Article::find()->max('updated_at');},],['class' => 'yii\filters\HttpCache','only' => ['view'],'etagSeed' => function ($action, $params) {$article = $this->findModel(\Yii::$app->request->get('id'));return serialize([$article->title, $article->text]);},],];}// ...}
- 接下来,刷新页面几次,并检查服务器返回的是
304 Not Modified,而不是200 OK:

- 使用如下URL打开相关页面,更新相关文章:
http://yiibook.app/index.php?r=blog/update。 - 更新过博客页面以后,检查服务器首次返回的是
200 OK,接着就是304 Not Modified,并确认你在页面上看到了新的更新时间:

- 从我们的页面上打开任何页面:

确认服务器首次返回的是200 OK,以及接下来的请求是304 Not Modified。
工作原理…
在HTTP头的帮助下,你的浏览器用基于时间和基于内容的方法,来检查缓存响应内容的可用性。
上次修改时间
这个方法建议服务端必须返回每个文章的上次修改时间。在存储这个这个日期之后,我们的浏览器可以在接下来的每次请求中,在If-Modified-Since头设置这个值。
我们必须附加这个action过滤器到我们的控制器中,并指定lastModified回到:
<?phpclass BlogController extends Controller{public function behaviors(){return [['class' => 'yii\filters\HttpCache','only' => ['index'],'lastModified' => function ($action, $params) {return Article::find()->max('updated_at');},],// ...];}// ...}
\yii\filters\HttpCache类调用这个回调,并将返回值和$_SERVER['HTTP_IF_MODIFIED_SINCE']系统变量进行比较。如果这个文章没有改变,HttpCache将会发送一个轻量级的304响应头,而且不需要运行这个动作。
但是,如果文档更新了,这个缓存将会失效,服务端将会返回一个完整的响应。

作为Last-Modified的备选或者补充,你可以使用ETag。
Entity标签
如果我们没有存储上次修改时间,我们可以使用自定义hash,它可以基于文章的内容生成。
例如,我们可以为我们的文档使用一个内容标题,来做了一个hash值:
class BlogController extends Controller{public function behaviors(){return [['class' => 'yii\filters\HttpCache','only' => ['view'],'etagSeed' => function ($action, $params) {$article =$this->findModel(\Yii::$app->request->get('id'));return serialize([$article->title,$article->text]);},],];}// ...}
这个HttpCache过滤器会将这个tag附加到服务器响应的ETag头变量上。
在存储了ETag之后,我们的浏览器会为接下来的每次请求附加它在If-None-Match头上。
如果这个文档仍然为改变,HttpCache将会发送一个轻量级的304响应头,并且不需要运行这个动作。


当这个缓存是合法的,我们的应用将会发送304 Not Modified响应头,而不是页面内容,而且不会重复运行控制器和动作。
参考
- 欲了解更多关于HTTP缓存,参考https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching
- 对于Yii2中的HTTP缓存,参考http://www.yiiframework.com/doc-2.0/guide-cachinghttp.html
和并和最小化assets
如果你的页面包含很多CSS和/或Javascript文件,这个页面将会打开的比较慢,因为浏览器发送了大量的HTTP请求来下载每一个文件。为了减少请求和连接的数量,我们可以在生产模式下合并和压缩多个CSS/Javascript文件到一个或者非常少的几个文件,然后将这些压缩的文件包含在页面上。
准备
- 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的
yii2-app-basic应用。 - 从https://developers.google.com/closure/compiler/下载
compiler.jar文件 - 从https://github.com/yui/yuicompressor/releases下载
yuicompressor.jar文件 - 从http://www.java.com下载jre。
如何做…
跟随如下步骤,来和并和最小化资源:
- 打开你的应用
index页面的源HTML代码。检查是否和如下结构比较相似:
<!DOCTYPE html><html lang="en-US"><head>...<title>My Yii Application</title><link href="/assets/9b3b2888/css/bootstrap.css"rel="stylesheet"><link href="/css/site.css" rel="stylesheet"></head><body>...<script src="/assets/25f82b8a/jquery.js"></script><script src="/assets/f4307424/yii.js"></script><script src="/assets/9b3b2888/js/bootstrap.js"></script></body></html>
这个页面包含三个Javascript文件。
- 打开
config/console.php文件,并添加@webroot和@webalias定义:
<?phpYii::setAlias('@webroot', __DIR__ . '/../web');Yii::setAlias('@web', '/');
- 打开一个控制台,并运行如下命令:
yii asset/template assets.php
- 打开生成的
assets.php文件,并按如下配置:
<?phpreturn ['jsCompressor' => 'java -jar compiler.jar --js {from}--js_output_file {to}','cssCompressor' => 'java -jar yuicompressor.jar --type css{from} -o {to}','bundles' => ['app\assets\AppAsset','yii\bootstrap\BootstrapPluginAsset',],'targets' => ['all' => ['class' => 'yii\web\AssetBundle','basePath' => '@webroot/assets','baseUrl' => '@web/assets','js' => 'all-{hash}.js','css' => 'all-{hash}.css',],],'assetManager' => ['basePath' => '@webroot/assets','baseUrl' => '@web/assets',],];
- 运行合并命令
yii asset assets.php config/assets-prod.php。如果成功,你就能得到带有如下配置的config/assets-prod.php文件:
<?phpreturn ['all' => ['class' => 'yii\\web\\AssetBundle','basePath' => '@webroot/assets','baseUrl' => '@web/assets','js' => ['all-fe792d4766bead53e7a9d851adfc6ec2.js',],'css' => ['all-37cfb42649f74eb0a4bfe0d0e715c420.css',],],'yii\\web\\JqueryAsset' => ['sourcePath' => null,'js' => [],'css' => [],'depends' => ['all',],],'yii\\web\\YiiAsset' => ['sourcePath' => null,'js' => [],'css' => [],'depends' => ['yii\\web\\JqueryAsset','all',],],'yii\\bootstrap\\BootstrapAsset' => ['sourcePath' => null,'js' => [],'css' => [],'depends' => ['all',],],'app\\assets\\AppAsset' => ['sourcePath' => null,'js' => [],'css' => [],'depends' => ['yii\\web\\YiiAsset','yii\\bootstrap\\BootstrapAsset','all',],],'yii\\bootstrap\\BootstrapPluginAsset' => ['sourcePath' => null,'js' => [],'css' => [],'depends' => ['yii\\web\\JqueryAsset','yii\\bootstrap\\BootstrapAsset','all',],],];
- 在
config/web.php文件中为assetManager组件添加配置:
'components' => [// ...'assetManager' => ['bundles' => YII_ENV_PROD ? require(__DIR__ . '/assets-prod.php') : [],],],
- 在
web/index.php打开生产模式:
defined('YII_ENV') or define('YII_ENV', 'prod');
- 在你的浏览器中刷新这个页面,就能看到HTML代码。现在应该有一条包含我们压缩文件的一行:
<!DOCTYPE html><html lang="en-US"><head>...<title>My Yii Application</title><link href="/assets/all-37cfb42649f74eb0a4bfe0d0e715c420.css" rel="stylesheet"></head><body>...<script src="/assets/all-fe792d4766bead53e7a9d851adfc6ec2.js"></script></body></html>
工作原理…
首先,我们的页面有包含文件的集合:
<link href="/assets/9b3b2888/css/bootstrap.css" rel="stylesheet"><link href="/css/site.css" rel="stylesheet">...<script src="/assets/25f82b8a/jquery.js"></script><script src="/assets/f4307424/yii.js"></script><script src="/assets/9b3b2888/js/bootstrap.js"></script>
接下来,我们生成assets.php配置文件,并制定需要压缩的东西:
'bundles' => ['app\assets\AppAsset','yii\bootstrap\BootstrapPluginAsset',],
注意:我们可以指定所有中间资源包,例如yii\web\JqueryAsset和yii\web\YiiAsset,但是这些资源已经作为AppAsset和BootstrapPluginAsset的依赖被指定了,这个压缩命令会自动解析所有的依赖。
AssetManager发布所有的资源到web/assets经典子文件夹中,在发布过以后,它会运行压缩器,将所有的CSS和JS文件压缩到all-{hash}.js和all-{hash}.css文件中。
检查这个CSS文件是否包含其它带有相对路径的资源,例如bootstrap.css文件中:
@font-face {font-family: 'Glyphicons Halflings';src: url('../fonts/glyphicons-halflings-regular.eot');}
如果是这样的话,在和并的文件中,我们的压缩器会修改所有的相对路径:
@font-face{font-family: 'Glyphicons Halflings';src: url('9b3b2888/fonts/glyphicons-halflings-regular.eot');}
处理过以后,我们得到了assets-prod.php文件,里边有assetManager组件的配置。它定义了新的virtual资源作为原始包的干净拷贝:
return ['all' => ['class' => 'yii\\web\\AssetBundle','basePath' => '@webroot/assets','baseUrl' => '@web/assets','js' => ['all-fe792d4766bead53e7a9d851adfc6ec2.js',],'css' => ['all-37cfb42649f74eb0a4bfe0d0e715c420.css',],],'yii\\web\\JqueryAsset' => ['sourcePath' => null,'js' => [],'css' => [],'depends' => ['all',],],// ...]
现在,我们可以require这个配置到config/web.php文件中:
'components' => [// ...'assetManager' => ['bundles' => require(__DIR__ . '/assets-prod.php'),],],
或者,我们可以只在生产环境中require这个文件:
'components' => [// ...'assetManager' => ['bundles' => YII_ENV_PROD ? require(__DIR__ . '/assets-prod.php') : [],],],
注意:不要忘记在更新了原始资源后重新生成所有的压缩和合并文件。
参考
- 欲了解更多关于assets的信息,参考http://www.yiiframework.com/doc-2.0/guide-structure-assets.html
- Closure Compiler的信息,参考https://developers.google.com/closure/compiler/
- 对于YUI压缩器的信息,参考https://github.com/yui/yuicompressor/
在HHVM上运行Yii2
HipHop Virtual Machine (HHVM)是一个处理虚拟机器,来自Facebook,基于just-in-time(JIT)编译。HHVM将PHP代码翻译成功中间的HipHop bytecode (HHBC),并动态翻译PHP代码为机器码,它可以被优化并原生的执行。
准备
按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。
如何做…
根据如下步骤,在HHVM上运行Yii:
- 安装Apache2或者Nginx web服务器:
- 跟随这个指南https://docs.hhvm.com/hhvm/installation/introduction,在Linux或者Mac上安装HHVM。例如在Ubuntu上,你需要运行如下命令:
sudo apt-get install software-properties-commonsudo apt-key adv --recv-keys --keyserverhkp://keyserver.ubuntu.com:80 0x5a16e7281be7a449sudo add-apt-repository "deb http://dl.hhvm.com/ubuntu$(lsb_release -sc) main"sudo apt-get updatesudo apt-get install hhvmAfter installing, you will see the following tips in yourterminal:********************************************************************* HHVM is installed.** Running PHP web scripts with HHVM is done by having your* webserver talk to HHVM over FastCGI. Install nginx or Apache,* and then:* $ sudo /usr/share/hhvm/install_fastcgi.sh* $ sudo /etc/init.d/hhvm restart* (if using nginx) $ sudo /etc/init.d/nginx restart* (if using apache) $ sudo /etc/init.d/apache restart** Detailed FastCGI directions are online at:* https://github.com/facebook/hhvm/wiki/FastCGI** If you're using HHVM to run web scripts, you probably want it* to start at boot:* $ sudo update-rc.d hhvm defaults** Running command-line scripts with HHVM requires no specialsetup:* $ hhvm whatever.php** You can use HHVM for /usr/bin/php even if you have php-cli* installed:* $ sudo /usr/bin/update-alternatives \* --install /usr/bin/php php /usr/bin/hhvm 60********************************************************************
- 尝试为你的网站手动启动内置服务器:
cd webhhvm -m server -p 8080
在你的浏览器中打开localhost:8080:

现在你就可以使用HHVM来开发你的项目了。
- 如果你使用Nginx或者Apache2服务器,HHVM会在
/etc/nginx和/etc/apache2目录中自动创建它自己的配置文件。在Nginx的例子中,它会创建/etc/nginx/hhvm.conf模板,来包含你的项目的配置。例如,我们来创建一个新的虚拟托管名叫yii-book-hhvm.app:
server {listen 127.0.0.1:80;server_name .yii-book-hhvm.app;root /var/www/yii-book-hhvm.app/web;charset utf-8;index index.php index.html index.htm;include /etc/nginx/hhvm.conf;}
添加hostname到你的/etc/hosts:
127.0.0.1 yii-book-hhvm.app
现在重启这个Nginx服务器:
sudo service nginx restart
最后,在浏览器中打开这个新的host:

你的服务器被成功设置。
工作原理…
你可以在fastcgi模式下使用HHVM作为PHP处理的备选项。默认情况下,它会监听9000端口。你可以在/etc/hhvm/server.ini文件中修改fastcgi进程的这个默认端口:
hhvm.server.port = 9000
在/etc/hhvm/php.ini文件中配置这个指定的PHP选项:
参考
欲了解更多关于安装HHVM的信息,参考如下地址:
欲了解更多关于HHVM使用方法的信息,参考https://docs.hhvm.com/hhvm/
