# 開發 Pagekit 擴展
<p class="uk-article-lead">以下教程包含創建和開發一個在 Pagekit 管理界面管理待辦事務(Todo)的擴展程序的所有完整步驟。你將學習擴展的基本概念、控制器、路由、視圖渲染以及 Vue.js 框架等知識。</p>
[TOC=2]
如果你更愿意看視頻教程,可以看這里: [如何創建 Pagekit 擴展](http://www.bilibili.com/video/av6824636/index_1.html) ,已經包含此教程內容了。
**Note** 本案例的 [最終成品](https://github.com/pagekit/example-todo) 就在 Github 上。
## Step 1: 使用 Modules 來擴展 Pagekit
作為開發者,你可以輕松擴展 Pagekit 已提供的功能。無論你是想自定義主題還是要制作包含附加功能的擴展,都是建立在相同的方法上的。第一步,我們會介紹 *包(package)* 和*模塊(module)* 這兩個 Pagekit 的核心概念。
<a href="http://www.bilibili.com/video/av6824636/index_2.html" class="uk-button">在影片中了解此步驟</a>
### 術語:Packages 和 modules
主題和擴展可能在外部看起來有所不同,但它們都我們稱之為 *包/package*。Package 其實是包含多個文件和存儲元數據的 `composer.json` 文件的一個文件夾。
要在 Pagekit 中注冊任意事物,都需要包含一個 `index.php` 文件。它包含了我們對 *模塊/module* 的定義。模塊是 Pagekit 代碼中最重要的構建單元。模塊是由多個自包含的功能單元組成的,并形成了整個系統。模塊在 Pagekit 核心和任意第三方擴展中都會用到。
### 把 package 文件放在哪里
每個包都有它獨立的提供者名字,這樣你就能在很清楚地查看全新安裝的 Pagekit 目錄結構。
核心包是 `pagekit` 命名空間的一部分,位于 `/packages` 目錄中相應的子目錄內。任意第三方包(包括你的)都有其自有的提供者名字,并放在單獨的子目錄里。
```
/packages
/pagekit
/blog
/theme-one
/your-vendor
/your-theme
/your-extension
```
在每個包中,你至少能發現兩個文件: `composer.json` 和 `index.php`。
### 1. composer.json: 包的元數據
在 `composer.json` 中包含了 package 的元數據。這些數據會在你把自己的擴展上傳到 Pagekit 商店時,顯示在 Pagekit 后臺。
`packages/pagekit/todo/composer.json`
```
{
"name": "pagekit/todo",
"version": "0.1",
"type": "pagekit-extension",
"title": "ToDo management"
}
```
### 2. index.php: 模塊的定義文件
Pagekit 內部的模塊捆綁了功能,并且是與 Pagekit 系統掛鉤的方式。從代碼的角度來說,模塊的定義就是一個包含屬性設置的 PHP 數組。`index.php` 必須 `return` 一個有效的數組。
```
<?php
use Pagekit\Application;
// packages/pagekit/todo/index.php
return [
'name' => 'todo',
'type' => 'extension',
// 在 Pagekit 初始化模塊時調用
'main' => function (Application $app) {
echo "It's alive";
}
];
?>
```
要測試功能,需要確保已在后臺啟用了擴展。如果擴展已被啟用,你會在屏幕上方看到輸出的結果。
這個最小的例子顯示了全功能模塊可以有多小。它訪問了 `Application` 實例。使用這個對象,可以訪問所有服務,觸發事件和監聽由其他模塊觸發的事件。
## Step 2: 路由和控制器
對于基本的擴展程序結構,常用的任務是注冊你自己的控制器并將菜單條目添加到管理界面中。為此,我們將查看一些附加的屬性,這些屬性可以添加到 `index.php` 中的模塊定義里。
<a href="http://www.bilibili.com/video/av6824636/index_3.html" class="uk-button">在影片中了解此步驟</a>
### 添加控制器
Pagekit 中的控制器只是簡單的 PHP 類。每個正確命名的公共方法(例如: `someAction()`)都由 Pagekit 的路由進行嵌入(例如: `/todo/some`)。
用下面的代碼創建文件 `packages/pagekit/todo/src/Controller/TodoController.php` 。
```
<?php
namespace Pagekit\Todo\Controller;
/**
* @Access(admin=true)
*/
class TodoController
{
public function indexAction()
{
return "Yay.";
}
}
```
要限制對管理區域的訪問,并將此控制器嵌入管理員頁面 URL 中,我們使用 `@Access(admin=true)` 這句注釋(Annotation)。 這里說到的注釋是位于類或方法上方注釋塊中的關鍵字。了解更多關于注釋的知識,查看[路由](224133)中“注釋”這部分。
要使用這個控制器,我們需要自動加載命名空間并將控制器嵌入到路由中。添加以下屬性到 `index.php` 中的配置數組。
```
// ...
// 從給定的目錄中自動加載命名空間的數組
'autoload' => [
'Pagekit\\Example\\' => 'src'
],
// 路由的數組
'routes' => [
// 從你的代碼中應用路由的標識符
'@todo' => [
// 此擴展要嵌入到的路徑
'path' => '/todo',
// 要嵌入的控制器
'controller' => 'Pagekit\\Todo\\Controller\\TodoController'
]
],
// ...
```
你現在就能使用 `<pagekit_path>/admin/todo` 這個 URL 來訪問新的控制器了。注意,這個 URL 由4個部分生成:
1. `<pagekit_path>` URL 以你的 URL 開頭
1. `@Access(admin=true)` resulted in an admin url `/admin`
2. 此控制器以 `/todo` 的形式嵌入
3. The `indexAction` is the default route at that url.
如果不能訪問此路由,需要[清除緩存](223152)。因為 Pagekit 為了更好的性能緩存了路由。
### 調試工具欄
要在開發過程中看到所有已注冊的路由,可以在 Pagekit 系統設置中啟用*調試工具欄* 。工具欄顯示在屏幕的下方,顯示所有已注冊的路由一起其他有用的信息。
記得在上線的網站中關掉它。
### 添加菜單條目
在模塊定義中使用 `menu` 屬性來添加菜單條目。在 `index.php` 中添加下面這些代碼:
```
// ...
'menu' => [
'example' => [
'label' => 'ToDo',
'icon' => 'app/system/assets/images/placeholder-icon.svg',
'url' => '@todo',
]
],
// ...
```
刷新 Pagekit 后臺,你就能看到鏈接到 `@todo` 路由的新菜單條目了。
## Step 3: 視圖渲染和模塊配置
<p class="uk-article-lead">在上一步,我們討論了模塊和路由的基礎。然而,我們的第一個控制器只能返回簡單的字符串。在這一步,我們了解實際的視圖渲染。</p>
<a href="http://www.bilibili.com/video/av6824636/index_4.html" class="uk-button">在影片中了解此步驟</a>
### 通過控制器操作來渲染視圖
要傳遞參數給視圖渲染器,控制器需要返回一個 PHP 數組。在 `$view` 鍵中有兩個視圖渲染器的參數,比如要渲染的視圖文件的 `name`。
```
public function indexAction()
{
return [
'$view' => [
'title' => 'TODO management',
'name' => 'todo:views/admin/index.php'
],
'message' => 'Hello how\'s it going?'
];
}
```
渲染后的視圖文件可以是純 PHP 模板。簡單的參數 (i.e. `message`) 可以用作 PHP 變量 (i.e. `$message`)。
`packages/pagekit/todo/views/admin/index.php`:
```
<h1><?php echo $message; ?></h1>
```
### 資源簡寫
我們可以使用簡寫語法,而不是引用文件的完整路徑。`packages/pagekit/todo/views/admin/index.php` 可以簡寫為 `todo:views/admin/index.php`。這能更快地輸入也更易于閱讀。
在 `index.php` 中注冊簡寫的目標路徑。在這個例子中,我們想要將 `todo` 指向 `index.php` 的當前路徑:
```
'resources' => [
'todo:' => ''
],
```
### 模塊配置
模塊可以由一個配置數組來保存各種設置。我們可以用它作為 TODO 條目的簡單存儲。
```
'config' => [
'entries' => [
['message' => 'Buy milk.', 'done' => false], // ...
]
],
```
在控制器中,我們可以將此配置作為模塊實例的一個屬性來訪問。
`src/Controller/TodoController.php`:
```
use Pagekit\Application as App;
// ...
$module = App::module('todo');
$config = $module->config;
// ...
```
可以將模塊配置的修改都存儲到數據庫中。模塊的默認配置與已存儲的修改進行合并,控制器就始終能擁有可用的配置了。
```
// 修改模塊配置
App::config('todo')->set('entries', $entries);
```
## Step 4: 在 Pagekit 擴展中使用 Vue.js
<p class="uk-article-lead">在為管理區域創建自有的頁面時,可以使用任意開發庫。但由于 Pagekit 已經自帶了 Vue.js,如果 Vue.js 合你口味就可以使用它了。在這一步,我們將介紹在 Pagekit 內部使用 Vue.js 的基礎概念。</p>
<a href="http://www.bilibili.com/video/av6824636/index_5.html" class="uk-button">在影片中了解此步驟</a>
### 1. 傳遞數據給 JavaScript
在前文中,我們了解了如何在 PHP 控制器中訪問數據。要在 JavaScript 中使用該數據,使用 `$data` 關鍵字將 PHP 數組傳遞給視圖渲染器。Pagekit 會自動將它轉換為 JSON 形式,并在生成的 HTML 的 head 部分渲染出來。
```
// packages/pagekit/todo/src/Controller/TodoController.php
// ...
public function indexAction()
{
$module = App::module('todo');
return [
'$view' => [
'name' => 'todo:views/admin/index.php'
],
'$data' => $module->config
];
}
```
下面的代碼將被渲染到 `<head>` 部分。
```
<script>var $data = {"entries": [{"message":"Buy milk","done":false},{"message":"Drink coffee","done":true}] };</script>
```
### 2. 組合視圖和模型
ViewModel 是一個 `Vue` 實例,用于在模型和視圖界面尚同步數據。這就是所謂的 *實時(reactivity)* ,它是 Vue 的關鍵特性之一。正確地使用它,將幫助你保持 JavaScript 組件的小巧和可讀。
可以將 `Vue` 實例附加到 DOM 元素(`el: '#todo'`)。模型即是你傳遞給 `data` 參數的任意數據。采用已渲染視圖的全局 `window.$data` 對象來使用 Pagekit 的數據。
可以在模板中調用你創建的任意 `methods` 。我們將在下一步中創建模板標簽。
```
// packages/pagekit/todo/js/todo.js
$(function(){
var vm = new Vue({
el: '#todo',
data: {
entries: window.$data.entries,
},
methods: {
add: function(e) {
// ...
},
toggle: function(entry) {
entry.done = !entry.done;
},
remove: function(entry) {
this.entries.$remove(entry);
},
save: function() {
// ...
}
}
});
});
```
### 2. Vue 的標簽
對于 PHP,我們仍然渲染視圖文件 `views/admin/index.php`。在這里,我們引入了 JavaScript 文件。要讓 Vue 在腳本中可用,需要添加 `vue` 依然作為第三個參數。
```
<?php $view->script('todo', 'todo:js/todo.js', 'vue') ?>
```
在 HTML 中,包含 Vue 實例的 DOM 元素會查找 (`<div id="todo"></div>`)。在元素內部可以使用 Vue 指令。指令是指那些可以告訴 Vue 如何處理元素的關鍵字。
```
<p v-if="entry.done">This will be displayed if the item has been done.</p>
```
使用 `@click` 可以綁定點擊事件,并調用視圖模型的方法。
要從模型中輸出值,可以使用花括號 (`{{ entry.message }}`)。Pagekit 提供了一個 `trans` 過濾器,如果所選區域有可用的語言包,它將自動將其替換為相應的語言。
```
{{ 'Save' | trans }}
```
這是一個簡單的視圖的樣子:
```
<!-- packages/pagekit/todo/views/admin/index.php -->
<?php $view->script('todo', 'todo:js/todo.js', 'vue') ?>
<div id="todo">
<button @click="save()">{{ 'Save' | trans }}</button>
<ul>
<li v-for="entry in entries">
{{ entry.message }}
<button @click="toggle(entry)">{{ entry.done ? 'Undo' : 'Do' }}</button>
</li>
</ul>
</div>
```
### 了解更多 Vue.js
Vue.js 是一款強大的庫,可以用來輕松構建靈活的 web 界面。了解更多,查看 [官方手冊](http://vuejs.org/guide/)。強烈推薦 Laracasts 上的[videos on Vue.js](https://laracasts.com/series/learning-vue-step-by-step).。兩者都是學習 Vue 的好去處。
<a id="complete"></a>
## 成品示例
在最終完成的示例中,我們實現了真實地保存到后臺,并清理了源代碼。它綜合前面談到的所有制作 Todo 擴展的步驟。成品就放在 [Github](https://github.com/pagekit/example-todo)上供你參考。