這篇文章介紹一套使用 Laravel 配合 Jade (Pug) 作為樣板引擎的工作流程,這套流程我們已經使用了近三年,解決了許多工程師與設計師在合作上的重工,希望對你們有幫助 :)

傳統的設計師 => 工程師的合作流程

傳統上,設計師在製作完網頁設計圖後,會由切版人員(這個人員可能是設計師自己,也有可能是前端工程師)將設計圖製作成靜態的 HTML 及 CSS,接下來再交給工程師套程式。

1
設計 (圖片) -> 切版 (HTML/CSS)-> 套版(程式)

Frontend Flow

Laravel blade

Laravel 預設是使用 blade 作為樣板引擎作為動態網頁的樣板,它具有良好的 extend 及 include 設計,以下例子說明了 include 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
@include('share.head')
</head>
<body>
@include('share.header')
<div class="container">
@yield('content')
</div>
@include('share.footer')
</body>
</html>

layout - view 結構中的 layout。

問題

對 laravel 工程師來說,製作畫面的方法是寫 blade,並且使用 blade 的 layout 與 view 使得我們在修改共用的畫面時只需要修改一次(因為共用的畫面皆有專屬的 blade 代表)。

但真正實行時,才發現此處還隱藏了一個大部分的人習以為常的重工 — 通常 html 及 css 是由設計師提供的,如果頁面已經套完 blade 了,設計師再次修改 html 與 css 會怎麼樣呢?工程師需要將設計師修改的 html、css 與 blade 作比對,將兩著差異更新到 blade 的 layout - view 結構,而且這樣的修改會一再發生。

要怎麼解決這樣的重工呢?我們想到的解法是

「讓設計師學會基本的 laravel 及 blade,與工程師一同修改 blade」

那麽如果設計師喜歡使用 jade (pug) 呢?使用 blade 就沒有 jade (pug) 優美的語法及方便的 mixin 了(註: Laravel 5.4 有 view component 可以使用)。而且使用 blade 必須要有 php 執行環境,如果設計師只想快速驗證畫面,不想要跑起 php server 呢?

於是我們製作了一套將 jade 轉換爲 blade 的工作流程:

改善的流程 - 使用 jade (pug) 產生 blade

pug to blade flow
見上圖

設計師與工程師寫 jade 及 less ,放在 fe-src ( FrontEnd Source) 目錄中,經過編譯 (compile, 或更貼切地應稱為 transpile) 轉為 html 及 css 至 fe-dest (frontend destination) 目錄,我們也將 javascript 及 image 放到此目錄,於是 fe-dest 目錄就是一個可以直接用瀏覽器預覽的靜態網站了。

在流程的最後,我們也同時自動將這些生成的 html 複製並命名為 .blade.php 到 laravel 的 view 路徑下,同時將靜態資源複製到 public 中,即完成了這套流程。

要如何使用這套流程?

目錄結構

1
2
3
4
5
6
7
8
fe-src
├── less
│ ├── _variables.less
│ └── app.less
└── pug
├── _layout
│ └── app.pug
└── index.pug
1
2
3
4
5
fe-dest
├── css
│ └── app.css
└── pug
└── index.html
1
2
3
4
5
6
7
public
└── css
└── app.css
resources
└── views
└── index.blade.php

Makefile

1
2
3
4
5
6
7
8
init:
yarn add gulp gulp-pug gulp-less gulp-filter gulp-notify gulp-sourcemaps gulp-autoprefixer browser-sync @unisharp/gulp-pug-inheritance
fe-build:
./node_modules/.bin/gulp
watch:
./node_modules/.bin/gulp watch

gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const gulp = require('gulp');
const pug = require('gulp-pug');
const less = require('gulp-less');
const filter = require('gulp-filter');
const notify = require('gulp-notify');
const sourcemaps = require('gulp-sourcemaps');
const autoprefixer = require('gulp-autoprefixer');
const browserSync = require('browser-sync').create();
const pugInheritance = require('@unisharp/gulp-pug-inheritance');
gulp.task('pug', () =>
gulp.src('fe-src/pug/**/*.pug')
.pipe(filter(file => !/\/fe-src\/pug\/_/.test(file.path)))
.pipe(pug({ pretty: true }))
.on('error', notify.onError('Error: <%= error.message %>'))
.pipe(gulp.dest('fe-dest'))
.pipe(notify('File: fe-dest/<%= file.relative %> Compiled!'))
.pipe(rename({ extname: '.blade.php' }))
.pipe(gulp.dest('resources/views'))
.pipe(notify('File: resources/views/<%= file.relative %> Compiled!'))
);
gulp.task('less', () =>
gulp.src('fe-src/less/app.less')
.pipe(sourcemaps.init())
.pipe(sass())
.on('error', notify.onError('Error: <%= error.message %>'))
.pipe(autoprefixer({ browsers: ['last 2 versions'] }))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('fe-dest/css'))
.pipe(notify('File: fe-dest/css/<%= file.relative %> Compiled!'))
.pipe(browserSync.stream({ match: '**/*.css' }))
.pipe(gulp.dest('public/css'))
.pipe(notify('File: public/css/<%= file.relative %> Compiled!'))
);
gulp.task('watch', () => {
browserSync.init({
host: '0.0.0.0',
server: 'fe-dest',
open: false
});
gulp.watch('fe-src/pug/**/*.pug', e =>
gulp.src(e.path, { base: 'fe-src/pug' })
.pipe(pugInheritance('fe-src/pug/**/*.pug'))
.pipe(filter(file => !/\/fe-src\/pug\/_/.test(file.path)))
.pipe(pug({ pretty: true }))
.on('error', notify.onError('Error: <%= error.message %>'))
.pipe(notify('File: fe-dest/<%= file.relative %> Compiled!'))
.pipe(rename({ extname: '.blade.php' }))
.pipe(gulp.dest('resources/views'))
.pipe(notify('File: resources/views/<%= file.relative %> Compiled!'))
.pipe(browserSync.reload({ stream: true }))
);
gulp.watch('./fe-src/less/**/*.less', ['less']);
});
gulp.task('default', ['pug', 'less']);

第一次執行

1
make init

編譯

1
make fe-build

LiveReload

1
make watch