larablog 系列文章 05 - 自定義視圖:Blade 擴展、側邊欄和靜態資源管理

本章將會繼續講如何構建前臺內容的呈現。我們將調整首頁,顯示有關博客文章評論的信息,并通過將標題內容加入到 URL 中來提升 SEO 效果。
還將在側邊欄添加 2 個常見的博客組件標簽云和最新評論。我們將了解如何為模板引擎進行擴展,以及如何管理網站中靜態資源文件。

顯示首頁評論數

到目前為止首頁會列出最新的文章列表,但并不會列出跟這些文章相關的評論。既然我們已經建立了評論模型,可以獲取文章相關的數據,那么可以回到首頁來呈現文章的評論信息。我們更新一下首頁的模板,打開位于 resources/views/pages/index.blade.php 的文件,根據以下所示調整相應內容:

<footer class="meta">
    <p>Comments: {{ $post->comments()->count() }}</p>
    <p>Posted by <span class="highlight">{{ $post->author }}</span> at {{ $post->created_at->toDateTimeString() }}</p>
    <p>Tags: <span class="highlight">{{ $post->tags }}</span></p>
</footer>

這里 $post->comments()->count() 是通過模型關系返回的結果,count() 即是返回對應關系模型的記錄數。
瀏覽器訪問 http://localhost:8000/ 你將會看到每篇的評論數量已經能夠顯示出來了。

構建側邊欄

目前 larablog 的側邊欄看起來有點空。我們將要給這個側邊欄增加兩個常用的部件,標簽云和最新評論。

標簽云

標簽云是顯示文章標簽的一種方式,將常用的標簽加粗顯示。要達到這個目的,我們需要獲取所有日志文章的標簽。

讓我們在 Post 類中新增方法來實現。更新 app/Post.php,添加如下方法:

public static function getTags()
{
    $blogTags = Post::lists('tags');

    $tags = [];
    foreach ($blogTags as $blogTag) {
        $tags = array_merge(explode(',', $blogTag), $tags);
    }

    return array_map(function ($item){ return trim($item); }, $tags);

    return $tags;
}

public static function getTagweights($tags)
{
    $tagWeights = [];

    if (empty($tags)) {
        return $tagWeights;
    }

    foreach($tags as $tag) {
        $tagWeights[$tag] = isset($tagWeights[$tag]) ? $tagWeights[$tag] + 1 : 1;
    }

    // Shuffle the tags
    uksort($tagWeights, function() {
        return rand() > rand();
    });

    $max = max($tagWeights);

    // Max of 5 weights
    $multiplier = ($max > 5) ? 5 / $max : 1;
    foreach ($tagWeights as &$tag) {
        $tag = ceil($tag * $multiplier);
    }

    return $tagWeights;
}

標簽是已逗號分隔的方式存儲在數據庫中,我們需要一個方式將它們分離并形成一個數組。這在 getTags() 方法中實現了。然后 getTagWeights() 方法可以使用這個數組來統計標簽的權重根據他們在數組中的熱門程度計算展示值。標簽經過隨機排序以顯示在頁面上。

現在有能力產生標簽云了,我們需要將它顯示出來。我們建立一個視圖組件用于呈現標簽云。

視圖組件就是在視圖被渲染前,會被調用的閉包或類方法。如果你想在每次渲染某些視圖時綁定數據,視圖組件可以幫你把這樣的程序邏輯都組織到同一個地方。
更多內容請參考 視圖組件

新建一個服務提供者 app/Providers/ComposerServiceProvider.php,我們通過它增加相應的視圖組件。其內容為:

<?php

namespace App\Providers;

use App\Post;
use Illuminate\Support\ServiceProvider;

class ComposerServiceProvider extends ServiceProvider
{
    /**
     * 在容器內注冊所有綁定。
     *
     * @return void
     */
    public function boot()
    {
        view()->composer('*.sidebar', function ($view) {
            $view->with('tags', Post::getTagweights(Post::getTags()));
        });
    }

    /**
     * 注冊服務提供者。
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

接下來,我們要將這個新建的服務提供者加入到在 config/app.php 配置文件內的 providers 數組中。

'providers' => [
    // ...

    App\Providers\ComposerServiceProvider::class,
],

ComposerServiceProvider 中你會注意到,視圖的 composer 方法可以接受 * 作為通配符,這里是對 *.sidebar 引入了側邊欄的視圖附加相應的視圖內容??梢钥吹?,我們給相應的視圖附加了 tags 數據。

打開側邊欄的視圖 resources/views/partials/sidebar.blade.php,將內容修改為以下所示:

@section('sidebar')
    <section class="section">
        <header>
            <h3>Tag Cloud</h3>
        </header>
        <p class="tags">
            @forelse($tags as $tag => $weight)
                <span class="weight-{{ $weight }}">{{ $tag }}</span>
            @empty
                <p>There are no tags</p>
            @endforelse
        </p>
    </section>
@show

這個模板不難理解,將標簽數據循環輸出,根據標簽的權重顯示不同的粗細樣式。

最后我們給標簽云添加相應的樣式,新建樣式表 public/css/sidebar.css,內容如下:

.sidebar .section { margin-bottom: 20px; }
.sidebar h3 { line-height: 1.2em; font-size: 20px; margin-bottom: 10px; font-weight: normal; background: #eee; padding: 5px;  }
.sidebar p { line-height: 1.5em; margin-bottom: 20px; }
.sidebar ul { list-style: none }
.sidebar ul li { line-height: 1.5em }
.sidebar .small { font-size: 12px; }
.sidebar .comment p { margin-bottom: 5px; }
.sidebar .comment { margin-bottom: 10px; padding-bottom: 10px; }
.sidebar .tags { font-weight: bold; }
.sidebar .tags span { color: #000; font-size: 12px; }
.sidebar .tags .weight-1 { font-size: 12px; }
.sidebar .tags .weight-2 { font-size: 15px; }
.sidebar .tags .weight-3 { font-size: 18px; }
.sidebar .tags .weight-4 { font-size: 21px; }
.sidebar .tags .weight-5 { font-size: 24px; }

我們找到應用的基礎布局模板 resources/views/layouts/app.blade.php 將側邊欄的樣式引入進來:

@section('stylesheets')
    <link href="{{ asset('css/screen.css') }}" type="text/css" rel="stylesheet" />
    <link href="{{ asset('css/blog.css') }}" type="text/css" rel="stylesheet" />
    <link href="{{ asset('css/sidebar.css') }}" type="text/css" rel="stylesheet" />
@show

如果你現在刷新頁面查看網站將看到標簽云可以正確顯示在你們面前。它根據不同的標簽的權重來顯示為相應的大小,你可以試試通過數據庫修改文章中的標簽數據來看看是否有相應的變化。

最近評論

現在標簽云已經添加到了側邊欄,接下來我們要將最近評論加入其中。

Comment 類,文件 app/Comment.php 中加入以下代碼:

public static function getLatest($limit = 10) 
{
    return Comment::orderBy('id', 'DESC')->take($limit)->get();
}

然后,我們和標簽云的做法一樣,來增加視圖組件,打開 app/Providers/ComposerServiceProvider.php,修改 boot 方法:

<?php

namespace App\Providers;

use App\Post;
use App\Comment;
use Illuminate\Support\ServiceProvider;

class ComposerServiceProvider extends ServiceProvider
{
    /**
     * 在容器內注冊所有綁定。
     *
     * @return void
     */
    public function boot()
    {
        view()->composer('*.sidebar', function ($view) {
            $view->with('tags', Post::getTagweights(Post::getTags()));
            $view->with('latestComments', Comment::getLatest());
        });
    }

    /**
     * 注冊服務提供者。
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

最后我們需要再次修改視圖模板,加入最新評論的顯示代碼。打開文件 resources/views/partials/sidebar.blade.php,修改為如下內容:

@section('sidebar')
    <section class="section">
        <header>
            <h3>Tag Cloud</h3>
        </header>
        <p class="tags">
            @forelse($tags as $tag => $weight)
                <span class="weight-{{ $weight }}">{{ $tag }}</span>
            @empty
                <p>There are no tags</p>
            @endforelse
        </p>
    </section>

    <section class="section">
        <header>
            <h3>Latest Comments</h3>
        </header>
        @forelse($latestComments as $latestComment)
            <article class="comment">
                <header>
                    <p class="small"><span class="highlight">{{ $latestComment->user }}</span> commented on
                        <a href="/posts/{{ $latestComment->post->id }}#comment-{{ $latestComment->id }}">
                            {{ $latestComment->post->title }}
                        </a>
                        [<em>{{ $latestComment->created_at->toDateTimeString('Y-m-d h:iA') }}</time></em>]
                    </p>
                </header>
                <p>{{ $latestComment->comment }}</p>
                </p>
            </article>
        @empty
            <p>There are no recent comments</p>
        @endforelse
    </section>
@show

至此,如果你打開瀏覽器刷新頁面,你將會看到最近的評論位于標簽云的下方,也能正確的顯示出來了。

模板引擎擴展

到目前為止,我們一直以標準的日期格式顯示博客的評論日期。一個更好的方法將是顯示評論被發布多久,如 3 小時前發布。我們可以向評論模型中添加一個方法來達到目的,但是我們在別的模型也可能要用到這個功能,因此我們通過擴充模版引擎來實現會更好。Laravel 的模板引擎 Blade 提供擴展接口可以讓我們定義自己的模板命令。

我們將創建一個新的模板標記,可以使用如下:

@datetime($comment->created_at)

添加擴展

打開文件 app/Providers/AppServiceProvider.php,編輯內容如下所示:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        \Blade::directive('datetime', function($expression) {
            return "<?php echo with{$expression}->diffForHumans(); ?>";
        });
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

我們通過自定義命令,使用 directive 方法注冊命令。當 Blade 編譯器遇到該命令時,它將會帶參數調用提供的回調函數。with 輔助函數會簡單地返回指定的對象或值,并允許使用便利的鏈式調用。最后此命令生成的 PHP 會是:

<?php echo with($var)->diffForHumans(); ?>

更多詳細內容可以閱讀文檔 擴充 Blade

更新視圖

現在我們可以更新側邊欄上評論的顯示時間,使用剛建立的模板命令來讓時間更為直觀。打開文件 resources/views/partials/sidebar.blade.php,將輸出時間信息的標記部分更改更改為如下所示:

[<em>@datetime($latestComment->created_at)</time></em>]

如果你現在訪問首頁,刷新頁面,你可以看到最新評論已經可以更佳直觀的顯示評論發布的時間。

讓我們也來更新一下文章的評論列表的視圖,打開位于 resources/views/comments/index.blade.php,將其內容更新為如下所示:

@forelse($comments as $i => $comment)
    <article class="comment {{ $i % 2 == 0 ? 'odd' : 'even' }}" id="comment-{{ $comment->id }}">
        <header>
            <p><span class="highlight">{{ $comment->user }}</span> commented {{ $comment->created_at->format('l, F j, Y') }}</p>
        </header>
        <p>{{ $comment->comment }}</p>
    </article>
@empty
    <p>There are no comments for this post. Be the first to comment...</p>
@endforelse()

自此我們通過擴展模板引擎讓評論的發布時間顯示更為直觀友好了。

格式化 URL

目前每個博客文章的網址只是通過編號表示來顯示,雖然從功能的角度來完全可以接受,但它對 SEO 并不好。例如 url http://localhost:8000/1 并沒有提供關于博客內容的任何信息,像 http://localhost:8000/a-day-with-laravel 則會好得多。為了達到這個目的,我們將把這個博客標題格式化并將其作為這個 URL 的一部分。標題將刪除所有非 ASCII 字符,并用 - 替換它們。

更新路由

首先,我們打開路由配置文件 app/Http/routes.php,增加新的文章詳情頁的路由配置,在 routes.php 中增加如下配置:

Route::get('{id}/{slug}', 'PostsController@show');

格式化

緊接著我們打開 Post 類,文件位于 app/Post.php 新增如下方法:

public static function slugify($text)
{
    // replace non letter or digits by -
    $text = preg_replace('#[^\\pL\d]+#u', '-', $text);

    // trim
    $text = trim($text, '-');

    // transliterate
    if (function_exists('iconv'))
    {
        $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
    }

    // lowercase
    $text = strtolower($text);

    // remove unwanted characters
    $text = preg_replace('#[^-\w]+#', '', $text);

    if (empty($text))
    {
        return 'n-a';
    }

    return $text;
}

模型和數據遷移

由于我們要通過格式化的標題也能訪問文章信息,所以我們要儲相應的格式化信息,那么我們就需要給文章表增加一個新的字段 slug。
我們通過一下命令建立一個新的遷移腳本,目的是向 posts 表中添加這個新的字段:

php artisan make:migration add_slug_to_posts_table --table=posts

然后打開遷移文件 xxxx_xx_xx_xxxxxx_add_slug_to_posts_table.php,更改其內容為如下所示:

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddSlugToPostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->string('slug');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropColumn('slug');
        });
    }
}

讓我們執行腳本遷移讓改動立即生效:

php artisan migrate

為了讓設置文章標題時能自動添加格式化的 slug 信息,我們可以給模型 Post 定義一個屬性修改器,打開 app/Post.php,添加如下方法:

public function setTitleAttribute($value)
{
    $this->attributes['title'] = $value;
    $this->attributes['slug'] = self::slugify($value);
}

我們定義了 title 的屬性修改器,會當我們嘗試在模型上設置 title 的值時自動調用此修改器。
我們利用這個屬性修改器來自動設置 slug 的值。

接下來,我們通過數據庫客戶端訪問數據庫,將 postscomments 表清空(truncate 操作)。
然后重新執行我們之前的數據填充操作:

php artisan db:seed

這樣我們的我們重新生成了 Post 數據,可以看到數據庫中每條文章的 slug 字段都已經自動生成好了。

更新視圖中的路由信息

接下來,我們要對首頁的文章和側邊欄的文章鏈接進行修改,使用我們的格式化 URL 來替換原來的鏈接地址。
打開 resources/views/pages/index.blade.php,找到博客文章鏈接的位置修改為如下內容:

<header>
    <h2><a href="/{{ $post->id }}/{{ $post->slug }}">{{ $post->title }}</a></h2>
</header>

緊接著我們打開側邊欄的視圖文件 resources/views/partials/sidebar.blade.php,將文章鏈接修改為如下所示:

<header>
    <p class="small"><span class="highlight">{{ $latestComment->user }}</span> commented on
        <a href="/{{ $latestComment->post->id }}/{{ $latestComment->post->slug }}#comment-{{ $latestComment->id }}">
            {{ $latestComment->post->title }}
        </a>
        [<em>@datetime($latestComment->created_at)</time></em>]
    </p>
</header>

最后我們還需要評論控制器 CommentsController 作出一些調整,目的是讓用戶發表評論后跳轉到相應的評論所在位置。
打開 app/Http/Controllers/CommentsController.php,修改 store 方法為如下所示:

public function store(CommentRequest $request, $postId)
{
    $post = Post::findOrFail($postId);

    $comment = new Comment;
    $comment->user = $request->get('user');
    $comment->comment = $request->get('comment');
    $comment->post()->associate($post);
    $comment->save();

    return redirect("/{$post->id}/{$post->slug}#comment-{$comment->id}");
}

現在你可以刷新瀏覽器,訪問首頁來點擊博客文章看看我們是否將格式化的標題信息加入了到了 URL 當中,同時我們在發布完評論之后,也會立即定位到所在評論信息。

資源管理

在我們的項目開發中涉及到一些靜態資源的管理,比如 css,js 的管理。這時我們可以利用到 Laravel Elixir。

Laravel Elixir 是官方推薦的靜態資源管理工具,此工具合理的定義項目的開發流程,尤其針對前端開發,解決了很多通用問題,如;Sass 編譯器,靜態資源文件的版本與緩存清除等。

它提供了簡潔流暢的 API,讓你能夠在你的 Laravel 應用程序中定義基本的 Gulp 任務。Elixir 支持許多常見的 CSS 與 JavaScrtip 預處理器,甚至包含了測試工具。

安裝及配置

在開始使用 Elixir 之前,你必須先確定你的機器上有安裝 Node.js。

Node 可以通過下面的鏈接下載:
https://nodejs.org/en/download/

Mac 系統的用戶建議通過 brew install node 進行安裝。

安裝完成后,可以通過下面命令查看:

node -v

接著,你需要全局安裝 Gulp 的 NPM 擴展包:

npm install --global gulp

最后的步驟就是安裝 Elixir,進入你項目目錄,你會發現根目錄有個名為 package.json 的文件。想像它就如同你的 composer.json 文件,只不過它定義的是 Node 的依賴擴展包,而不是 PHP 的。

編輯 package.json 文件,將內容修改為如下所示:

{
  "private": true,
  "devDependencies": {
    "gulp": "^3.8.8"
  },
  "dependencies": {
    "laravel-elixir": "^4.0.0"
  }
}

執行下面的命令安裝依賴包:

npm install

優化 NPM 安裝 Gulp 和 Laravel Elixir 的下載速度

樣式表

我們將位于 public/css 目錄下的 screen.css,blog.csssidebar.css 文件移動到 resources/assets/css 下。
接下來我們做的是將多個 CSS 樣式合并成單個的文件,我們打開項目目錄下的 gulpfile.js 定義 Elixir 任務:

elixir(function(mix) {
    mix.styles([
        'screen.css',
        'blog.css',
        'sidebar.css'
    ]);
});

styles 方法的默認讀取路徑為 resources/assets/css 目錄,而生成的 CSS 會被放置于 public/css/all.css。
可以通過傳遞第二個參數至 styles 方法,將生成的文件輸出至指定的位置:
mix.styles(['normalize.css','main.css'], 'public/assets/css');

在項目目錄下執行 gulp 來執行 Elixir 任務,我們可以看到我們生成了預期的 public/css/all.css 文件。

打開我們的視圖布局模板文件 resources/views/layouts/app.blade.php,可以修改 stylesheets 部分的內容,使用我們合并后的 CSS 文件:

@section('stylesheets')
    <link href="{{ asset('css/all.css') }}" type="text/css" rel="stylesheet" />
@show

JS

至于 Javascript,雖然我們這里沒有涉及到 JS 的內容,不過如果你之后也需要對 JS 有合并的需求也可以這么添加 Elixir 任務:

elixir(function(mix) {
    mix.scripts([
        'jquery.js',
        'app.js'
    ]);
});

scripts 方法假設所有的路徑都相對于 resources/assets/js 目錄,且默認會將生成的 JavaScript 放置于 public/js/all.js。

壓縮

當你應用發布到生產環境時,你可以通過下面的命令來壓縮所有 CSS 及 JavaScript:

// 運行所有任務并壓縮所有 CSS 及 JavaScript...
gulp --production

壓縮這些靜態資源的好處最明顯的就是縮小其文件大小提高傳輸相應速度。

其它

這里我們只是使用了 Elixir 來做簡單的合并和壓縮,它的功能不止這些,Elixir 支持許多常見的 CSS 與 JavaScrtip 預處理器,甚至包含了測試工具。使用鏈式調用,Elixir 還能讓你流暢地定義開發流程。這非常適合前端的開發流程化,是個不可多得好工具。
更多內容還請關注相應的文檔內容 Laravel Elixir

總結

通過這個部分的實踐,我們又接觸到了 Laravel 的許多新內容,包括 Laravel 視圖組件以及靜態資源文件的管理。我么還對主頁進行了改進,并在側邊欄添加了一些組件。

在下一章我們繼續探討測試相關的內容。我們將使用 PHPUnit 進行單元和功能測試。我們還會編寫模擬 Web 請求的功能測試,填寫表單和點擊鏈接,然后檢查返回的響應。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容