第2章 創建一個簡單的新聞閱讀器
本章內容包含:創建第一個控制器,用於展示新聞條目列表和詳情;學習控制器和視圖之間的交互;自定義視圖的佈局。
本章結構如下:
- 創建控制器和動作
- 創建用於展示新聞列表的視圖
- 控制器是如何將數據傳送到視圖的
- 例子——創建一個控制器,展示靜態新聞條目列表和詳情
- 將常用視圖內容分割成多個可複用視圖
- 例子——在視圖中進行部分渲染
- 創建靜態頁面
- 在視圖和佈局之前共享數據
- 例子——根據URL參數更換佈局背景
- 使用動態模塊佈局
- 例子——添加展示廣告信息的動態盒
- 使用多個佈局
- 例子——使用不同的佈局給同一個視圖創建響應式和非響應式佈局
創建控制器和動作
爲了處理一個請求,第一件要做的事就是創建一個新的控制器。
在創建一個控制器文件時,需要注意如下一些事項:
- 命名空間在最上邊(在基礎應用中一般是app\controllers)
use
引用會使用的類- 控制器類必須繼承
yii\web\Controller
類 - 控制器中的函數,即動作,需要以
action
開頭,並且每一個單詞的首字母大寫
在文件夾basic\controllers
中創建文件NewsController.php
。
然後創建一個和文件名一樣的類,並繼承yii\web\Controller
類,創建一個actionIndex
函數,這個函數對應的請求就是news/index
:
<?php
// 1. specify namespace at the top (in basic application usually app\controllers);
namespace app\controllers;
// 2. specify 'use' path for used class;
use Yii;
use yii\web\Controller;
// 3. controller class must extend yii\web\Controller class;
// This line is equivalent to
// class NewsController extends yii\web\Controller
class NewsController extends Controller
{ // 4. actions are handled from controller functions whose name starts with 'action' and the first letter of each word is uppercase;
public function actionIndex()
{
echo "this is my first controller";
}
}
使用瀏覽器訪問http://hostname/basic/web/index.php?r=news/index
,我們將會看到一個空白頁面,並提示我們this is my first controller.
現在讓我們看看,如果我們忽略了先前提到的需要注意的四件事中的某些時,將會發生什麼錯誤。
命名空間定義了應用中使用到的名稱的等級結構。如果我們忘記使用命名空間,並且web/index.php
中的YII_DEBUG
設置爲true,我們將會看到如下錯誤信息:
控制器命名空間缺失
Yii2能非常棒的展示錯誤信息,提示我們檢查是否丟失了命名空間。
use
用於指明某個類在應用中的詳細路徑。類都有一個類似這樣的完整路徑path/to/class/ClassName
。如果們在命名空間之後定義添加use path/to/class/ClassName
,那麼我們在使用時只需要引用ClassName
。
但是,如果我們在只是使用ClassName
,但沒有添加use path/to/class/ClassName
,將會出現如下錯誤:
對於初學者來說,這個錯誤非常容易解釋,但是很難發現。
在這個例子中,從截圖中可以看出,在第9行extends
之後使用了Controller
類。但是因爲這不是一個完整的Controller
類名,Yii2會嘗試從app\controllers
下去尋找這個Controller
類,但是並沒有找到。
爲了解決這個問題,我們必須將第9行的Controller
修改爲yii\web\Controller
,或者在文件頂部使用use path/to/class/ClassName
。
控制器通常是yii\web\Controller
的子類。
創建一個展示新聞列表的視圖
現在我們在一個名叫itemsList
的視圖中創建一個簡單的新聞列表。然後從NewsController
中指向這個視圖,我們需要做下面一些事情:
- 在
basic\views
中創建一個名叫news
的文件夾,控制器NewsController
默認在這個文件夾中尋找需要渲染的視圖。 - 在
basci\views\news
中創建文件itemsList.php
打開文件basci\views\news\itemsList.php
,創建一個數據數組和一個展示數據的表格:
<?php
$newsList = [
[ 'title' => 'First World War', 'date' => '1914-07-28' ],
[ 'title' => 'Second World War', 'date' => '1939-09-01' ],
[ 'title' => 'First man on the moon', 'date' => '1969-07-20' ]
];
?>
<table>
<tr>
<th>Title</th>
<th>Date</th>
</tr>
<?php foreach($newsList as $item) { ?>
<tr>
<td><?php echo $item['title'] ?></td>
<td><?php echo $item['date'] ?></td>
</tr>
<?php } ?>
</table>
然後我們需要在控制器中創建一個actionItemsList
的動作函數,可以通過http://hostname/basic/web/index.php?r=news/items-list
訪問到它。
建議
下載示例代碼
你可以在http://www.packtpub.com上下載你購買的書的示例代碼。如果你在別的地方買了這本書,你可以去http://www.packtpub.com/support這裏尋求幫助。
注意
注意路徑、控制器和動作的名字:
- 這個動作的路徑是
news/items-list
(小寫,單詞之間用短橫線連接); - 控制器類的名稱是
NewsController
(單詞首字母大寫,最後跟着Controller); NewsController
中的動作函數是actionItemsList
(其中action
是一個固定前綴,沒有路徑中的短橫線,且每個單詞的首字母大寫);
NewsController
中的函數如下所示:
public function actionItemsList()
{
return $this->render('itemsList');
}
其中render()
方法屬於\yii\web\Controller
,用於展示第一個參數所代表的視圖,框架查找這個視圖的時候,會給參數添加上.php
的後綴,並在basic\views\news
文件夾中尋找。
現在我們可以訪問http://hostname/basic/web/index.php?r=news/items-list
看到效果啦!
控制器是如何將數據傳送到視圖的
我們已經看到了如何展示視圖內容。但是,視圖的作用只是展示數據,而不是操縱數據。因此,所有的數據都應該在控制器動作中準備好,然後才傳入到視圖中。
控制器動作中的render()
方法還有第二個參數,它是一個向量,該向量的一系列key是變量的名稱,value是這些變量的值,這些變量可以在視圖中使用。
現在我們修改之前的代碼,將itemsList
需要的數據在控制器中準備。
下邊是控制器中actionItemsList()
的內容:
public function actionItemsList()
{
$newsList = [
[ 'title' => 'First World War', 'date' => '1914-07-28' ],
[ 'title' => 'Second World War', 'date' => '1939-09-01' ],
[ 'title' => 'First man on the moon', 'date' => '1969-07-20' ]
];
return $this->render('itemsList', ['newsList' => $newsList]);
}
文件views/news/itemsList.php
中的代碼是:
<?php // $newsList is from actionItemsList ?>
<table>
<tr>
<th>Title</th>
<th>Date</th>
</tr>
<?php foreach($newsList as $item) { ?>
<tr>
<th><?php echo $item['title'] ?></th>
<th><?php echo $item['date'] ?></th>
</tr>
<?php } ?>
</table>
這樣我們就將控制器和視圖分開了。
例子——創建一個控制器,使用bootstrap模板展示靜態新聞條目列表和詳情
接下來的目標是在另外一個頁面中完成新聞新聞閱讀器的單條新聞的詳情展示部分。
因爲列表和詳情使用的是同一套數據,我們將$newsList
數據從控制器動作中拿出來,進而可以用在別的動作中。
在NewsController
中,添加如下的代碼:
public function dataItems()
{
$newsList = [
[ 'title' => 'First World War', 'date' => '1914-07-28' ],
[ 'title' => 'Second World War', 'date' => '1939-09-01' ],
[ 'title' => 'First man on the moon', 'date' => '1969-07-20' ]
];
return $newsList;
}
public function actionItemsList()
{
$newsList = $this->dataItems();
return $this->render('itemsList', ['newsList' => $newsList]);
}
然後在NewsController
中創建一個新的動作actionItemDetail
,用於處理新聞條目詳情的請求。這個函數有一個參數,指明需要展示$newsList
哪條新聞,例如通過標題。
public function actionItemDetail($title)
{
$newsList = $this->dataItems();
$item = null;
foreach($newsList as $n)
{
if($title == $n['title']) $item = $n;
}
return $this->render('itemDetail', ['item' => $item]);
}
接下來在views\news
中創建一個新的視圖文件itemDetail.php
:
<?php // $item is from actionItemDetail ?>
<h2>News Item Detail</h2>
<br />
Title: <b><?php echo $item['title'] ?></b>
<br />
Date: <b><?php echo $item['date'] ?></b>
如果訪問http://hostname/basic/web/index.php?r=news/item-detail
,沒有指出標題參數,那麼將會返回如下錯誤:
頁面展示了一個錯誤信息,提示我們缺少標題參數。
修正URL,訪問http://hostname/basic/web/index.php?r=news/item-detail&title=First%20World%20War
,將會輸出如下結果:
這就是我們期望的!
最後,將itemsList
和itemDetail
聯繫起來,在views\news\itemsList.php
中,我們做如下修改:
<?php // $newsList is from actionItemsList ?>
<table>
<tr>
<th>Title</th>
<th>Date</th>
</tr>
<?php foreach($newsList as $item) { ?>
<tr>
<th><a href="<?php echo Yii::$app->urlManager->createUrl(['news/item-detail' , 'title' => $item['title']]) ?>"><?php echo $item['title'] ?></a></th>
<th><?php echo $item['date'] ?></th>
</tr>
<?php } ?>
</table>
使用組件urlManager
可以創建一個鏈接,具體是使用createUrl()
方法。createUrl()
的參數是一個向量,其中包含了路徑和需要通過URL傳遞的參數。關於這個方法更多的介紹,可以去這裏查看http://www.yiiframework.com/doc-2.0/yii-web-urlmanager.html#createUrl%28%29-detail。
在這裏例子中,請求的路徑是news\item-detail
,並且傳遞了參數title
。
注意
可以用於內置格式化組件格式化日期。例如,爲了以d/m/Y的格式展示日期,echo \Yii::$app->formatter->asDatetime('2010-01-23', "php:d/m/Y");
將會輸出23/01/2010
。
建議爲每條新聞指定一個唯一的編號,這裏我們爲數據添加一個id
項。
下面是NewsController
的內容:
public function dataItems()
{
$newsList = [
[ 'id' => 1, 'title' => 'First World War', 'date' => '1914-07-28' ],
[ 'id' => 2, 'title' => 'Second World War', 'date' => '1939-09-01' ],
[ 'id' => 3, 'title' => 'First man on the moon', 'date' => '1969-07-20' ]
];
return $newsList;
}
public function actionItemsList()
{
$newsList = $this->dataItems();
return $this->render('itemsList', ['newsList' => $newsList]);
}
public function actionItemDetail($id)
{
$newsList = $this->dataItems();
$item = null;
foreach($newsList as $n)
{
if($id == $n['id']) $item = $n;
}
return $this->render('itemDetail', ['item' => $item]);
}
然後將views\news\itemsList.php
中createUrl
的參數修改爲:
<table>
<tr>
<th>Title</th>
<th>Date</th>
</tr>
<?php foreach($newsList as $item) { ?>
<tr>
<th><a href="<?php echo Yii::$app->urlManager->createUrl(['news/item-detail' , 'id' => $item['id']]) ?>"><?php echo $item['title'] ?></a></th>
<th><?php echo Yii::$app->formatter->asDatetime($item['date'], "php:d/m/Y"); ?></th>
</tr>
<?php } ?>
</table>
將常用視圖內容分割成多個可複用視圖
有些時候,若干視圖會有部分相同的內容。例如,itemsList
和itemDetail
都需要展示版權信息。
爲了減少工作量,我們需要將這些共有的部分抽取出來,並使用控制器的renderPartial()
方法調用(http://www.yiiframework.com/doc-2.0/yii-base-controller.html#renderPartial%28%29-detail)。這個函數和render()
有相同的參數;兩者的區別主要是render()會將視圖內容放在佈局中,而renderPartial()只是輸出視圖內容。
例子——在視圖中進行部分渲染
在這個例子中,我們爲itemsList
和itemDetail
創建版權信息。
在views\news
中創建文件_copyright.php
。
注意
在Yii2的應用中,可複用視圖文件通常以下劃線開始。
將如下內容寫入文件views\news\_copyright.php
文件中:
<div>
This is text about copyright data for news items
</div>
然後分別在itemsList
和itemDetail
視圖中展示版權信息。
修改views\news\itemsList.php
的內容爲:
<?php echo $this->context->renderPartial('_copyright'); ?>
<table>
<tr>
<th>Title</th>
<th>Date</th>
</tr>
<?php foreach($newsList as $item) { ?>
<tr>
<th><a href="<?php echo Yii::$app->urlManager->createUrl(['news/item-detail' , 'id' => $item['id']]) ?>"> <?php echo $item['title'] ?> </a></th>
<th><?php echo Yii::$app->formatter->asDatetime($item['date'], 'php:d/m/Y'); ?></th>
</tr>
<?php } ?>
</table>
修改views\news\itemsDetail.php
的內容爲:
<?php // $item is from actionItemDetail ?>
<?php echo $this->context->renderPartial('_copyright'); ?>
<h2>News Item Detail</h2>
<br />
Title: <b><?php echo $item['title'] ?></b>
<br />
Date: <b><?php echo $item['date'] ?></b>
在這兩個文件中我們都添加了下面的內容:
<?php echo $this->context->renderPartial('_copyright'); ?>
注意
注意!因爲renderPartial()
是Controller
的方法,$this
代指視圖文件中的View
類,所以爲了使用renderPartial()
必須使用上下文成員context,它是View
對象中的Controller
對象。
創建靜態網頁
所有的網站都包含靜態網頁,他們的內容是靜態的。
爲了以更常見的方式創建,我們需要做如下工作:
- 在
Controller
中創建一個函數(action
) - 創建一個靜態內容視圖
將如下內容放在Controller
中:
public function actionInfo()
{
return $this->render('info');
}
然後創建一個視圖文件views/controller/action-name.php
。這個過程比較長,並且比較冗餘。
Yii2提供了另外一種更爲便捷的方式,添加如下內容到Controller
的action()
中:
public function actions()
{
return [
'pages' => [
'class' => 'yii\web\ViewAction',
],
];
}
然後我們就可以將靜態內容網頁放在views/controllerName/pages
中了。
最後,可以通過訪問http://hostname/basic/web/index.php?r=controllerName/pages&view=name_of_view
來查看相關的靜態網頁。
例子——添加一個聯繫人頁
在瞭解瞭如何創建靜態網頁以後,我們來創建一個聯繫人頁。
在views/site/pages/contact.php
中添加如下內容:
To contact us, please write to [email protected]
然後,在Controller
的action()
方法中添加一個page
屬性,這裏我們在SiteController
中做如下修改:
原來的內容:
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
修改後:
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
'pages' => [
'class' => 'yii\web\ViewAction',
],
];
}
設置好以後,所有對site/pages
的請求都會使用ViewAction
類,具體就是方法相對應的靜態視圖。
訪問網址http://hostname/basic/web/index.php?r=site/pages&view=contact
可以看到如下內容:
我們可以對上述路徑做如下自定義修改:
Controller
中actions()
的屬性名稱- 修改
ViewAction
的viewPrefix
屬性,來聲明我們想使用的URL - 修改
views/controllerName
的子文件夾名稱
例如,我們希望在SiteController
中使用static
來訪問靜態網頁。
通過訪問http://hostname/basic/web/index.php?r=site/static&view=contact
查看聯繫人頁面。
那麼可以在SiteController
中actions()
添加如下內容:
'static' => [
'class' => 'yii\web\ViewAction',
'viewPrefix' => 'static'
],
同時我們需要添加文件夾view/site/static
,這樣我們就可以訪問http://hostname/basic/web/index.php?r=site/static&view=contact
頁面了。
在視圖和佈局之前共享數據
在視圖與佈局之間共享數據,Yii2提供了一種標準的方法,即通過視圖控件的params
屬性。
注意
這是一個標準的方法,因爲params
存在於所有的視圖中。
params
屬性是一個數組,沒有任何使用限制。
例如我們希望保存麪包屑的數據,用於展示在導航路徑中。
打開主視圖views/layouts/main.php
;找到有關麪包屑的代碼:
<div class="container">
<?= Breadcrumbs::widget([
'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
]) ?>
</div>
在文件views/site/index.php
中添加如下代碼:
$this->params['breadcrumbs'][] = 'My website';
注意
因爲這是在視圖文件中,$this
代表視圖控件。
訪問http://hostname/basic/web/index.php?r=site/index
可以看到如下圖所示的麪包屑導航:
例子——根據URL參數更換佈局背景
視圖和佈局之間進行通信的另外一個例子是,基於URL參數改變佈局背景。
通過向路徑爲site/index
的URL傳遞參數bckg
來修改背景。
因此,我們必須在代碼views/site/index.php
中添加如下內容:
<?php
$backgroundColor = isset($_REQUEST['bckg'])?$_REQUEST['bckg']:'#FFFFFF';
$this->params['background_color'] = $backgroundColor;
如果沒有傳遞參數bckg
,這行代碼會將$backgroundColor
設置爲#FFFFFF
(白色);否則會被設置爲指定的值。
打開views/layout/main.php
,在body
標籤中,根據params['background_color']
修改它的style。
<?php
$backgroundColor = isset($this->params['background_color'])?$this->params['background_color']:'#FFFFFF'; ?>
<body style="background-color:<?php echo $backgroundColor ?>">
訪問http://hostname/basic/web/index.php?r=site/index&bckg=yellow
可以看到背景是黃色的,訪問http://hostname/basic/web/index.php?r=site/index&bckg=#FF0000
可以看到背景是紅色的。
注意
在這個例子中,我們只在views/site/index.php
中給params
添加了background_color
參數。如果我們不在佈局文件中檢查background_color
屬性是否被設置了,那麼有可能會收到一個錯誤。
使用動態模塊佈局
params
參數用於視圖和佈局之前的通信,這一般只適用於簡單的情況,如果遇到比較複雜的情況時,我們需要傳遞HTML塊。
例如,佈局中的廣告區(使用模板中的左列或者右列),它可以根據需要展示的視圖來改變。
在這個條件下,我們需要將HTML整塊從視圖傳遞到佈局中。
爲了這個目的,框架提供了Block,我們可以在這裏定義需要從視圖傳遞到佈局的數據。
使用Blocks意味着定義在一個視圖中定義它,並在另一個視圖中進行展示,通常是在佈局中。
我們可以按如下方式定義Block
:
<?php $this->beginBlock('block1'); ?>
...content of block1...
$this->endBlock(); ?>
這裏beginBlock
和endBlock
定義了block1
的開始和結束。其中的內容被保存到的視圖控件中的block1
的屬性中。
我們可以使用$view>blocks[$blockID]
在任意一個視圖中獲取這個塊,包含佈局。
爲了在佈局視圖中使用這個塊,可以使用如下代碼:
<?php if(isset($this->blocks['block1']) { ?>
<?php echo $this->blocks['block1'] ?>
<?php } else { ?>
… default content if missing block1 attribute
<?php } ?>
例子——添加展示廣告信息的動態盒
在這個例子中,我們會展示一個廣告信息塊,且數據是從視圖傳遞過來的。
首先添加一個展示數據的塊。
打開views/layouts/main.php
並修改做如下修改:
<div class="container">
<?= Breadcrumbs::widget([
'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
]) ?>
<div class="well">
This is content for blockADV from view
<br />
<?php if(isset($this->blocks['blockADV'])) { ?>
<?php echo $this->blocks['blockADV']; ?>
<?php } else { ?>
<i>No content available</i>
<?php } ?>
</div>
<?= $content ?>
</div>
然後在NewsController
中創建一個新的動作,advTest
。
創建文件views/news/advTest.php
:
<span>
This is a test where we display an adv box in layout view
</span>
<?php $this->beginBlock('blockADV'); ?>
<b>Buy this fantastic book!</b>
<?php $this->endBlock(); ?>
我們可以在這個block中創建任何內容。這裏我們添加了一些文本。
注意
在視圖中創建block的位置是不重要的。
然後,打開NewsController
添加如下內容:
public function actionAdvTest()
{
return $this->render('advTest');
}
訪問http://hostname/basic/web/index.php?r=news/adv-test
將會看到如下結果:
在其它頁面上將會看到no content available
。
使用多個佈局
在創建一個網站或者一個網站應用時,通常需要在不同的佈局中渲染不同的視圖。例如,本章中製作的新聞列表和詳情。
佈局是由Controller
的$layout
屬性管理的;main
是這個屬性的缺省值。
修改這個屬性的值,就可以改變佈局。
關於$layout
屬性有如下一些規則:
- 路徑別名(例如,
@app/views/layouts/main
)。 - 絕對路徑(例如,
/main
),以一個斜線開始。佈局文件將會在應用佈局路徑中尋找,這個路徑的缺省值是@app/views/layouts
。 - 相對路徑(例如,
main
),將會在當前模塊的佈局路徑中尋找,該路徑的缺省值是當前模塊的views/layouts
。 - 布爾值false表示沒有使用佈局文件。
注意
如果佈局值不含有文件擴展名,它將會使用缺省值.php
。
例子——使用不同的佈局給同一個視圖創建響應式和非響應式佈局
在這個例子中,我們會在NewsController
中創建一個新的動作,它根據URL傳遞的參數修改佈局。
public function actionResponsiveContentTest()
{
$responsive = Yii::$app->request->get('responsive', 0);
if($responsive)
{
$this->layout = 'responsive';
}
else
{
$this->layout = 'main';
}
return $this->render('responsiveContentTest', ['responsive' => $responsive]);
}
在這個動作中,我們根據URL中的參數responsive
設置變量$responsive
,缺省值是0。
然後根據$responsive
動態修改Controller
的$layout
,並將這個變量傳遞給視圖。
創建一個新的視圖views/news/responsiveContentTest.php
:
<?php if($responsive) { ?>
This layout contains responsive content
<?php } else { ?>
This layout does not contain responsive content
<?php } ?>
根據$responsive
展示不同的文本塊。
最後,將view/layouts/main.php
複製爲views/layouts/responsive.php
,並在新文件中作如下修改:
<div class="container"> in <div class="container-fluid" style="padding-top:60px">
這個改變將div容器修改爲響應式的。
如果訪問http://hostname/basic/web/index.php?r=news/responsive-content-test
,我們將會看到一個固定的佈局。如果我們將responsive
的參數設置爲1,http://hostname/basic/web/index.php?r=news/responsive-content-test&responsive=1
,我們將會看到一個寬屏佈局。
總結
在本章中,在瞭解了Yii2應用的結構以後,我們創建我們的第一個控制器和相關的視圖。我已經看到的動態和靜態視圖,我們已經學習瞭如何在佈局中渲染視圖,並從控制器中傳遞數據到視圖中,以及如何複用視圖。
最後,我們操作了佈局,並根據條件修改它。
在下一章中,我們將會用更好的方式展示URL,這對搜索引擎優化(SEO)非常重要。然後,我們將會學習如何創建一個自定義URL句柄來管理任何需要的URL自定義。