本文並不會手把手從頭教學與打造一個網站出來,而是將我工作中的一個網頁切版專案做一些探討,主要會講述到一些起始步驟、工具、專案結構、依賴套件以及如何客製化 Bootstrap 4 Sass,發這篇文章是希望大家能夠學習到一些東西,降低現在大前端時代所前置準備動作的一些門檻,我並不是最好的作法,而是適合目前在工作 context 上能夠提升切版效率以及降低前後端溝通的一個處理方式。
先給各位看成品:104 人力銀行 – 整招版
我們公司現在面臨到首頁改版的需求,我這邊負責到的是整招版本,所以廣告輪播非常多,因為是 POC 專案,所以給的時間而是非常不多,一個禮拜內就得切版出來以及將素材套用完畢,後續會用 A/B Test 方式 5% Chrome 給使用者使用並透過 Hotjar 收集回饋。傳統公司的前端大大們都是完全實實在在的手刻切版網頁,那在我這個新進人員不免於對工作環境上的陌生,也不曉得有多少內部共用組件可以使用,所以提出了使用 CSS Framework:Bootstrap 4 的導入,希望可以有效率的在有限時間內將工作任務完成。
這裡表列一下我處理這個專案的時間分配 (1 day 是 8 小時工作量):
- 環境準備:.5 day
- 切版:1.5 day
- 素材套用:1 day ( + 2~3 hours)
- 後續修改:1 day ( + 2~3 hours)
什麼是 + 2~3 hours,就夜間加班嘛~沒什麼好說,素材與後續調整是超乎預期的…
也因為僅給 Chrome 使用者使用,就沒有什麼 IE 8、9、10、11 and Edge 的問題 (開薰 XDD),所以大方使用 Bootstrap v4 版本囉!
開發工具
- WebStorm
- Zeplin
- Chrome extension:Dimensions
- 網頁測量間距工具
- Mark Man
- 支持圖片測量間距、註解與顏色標記
- 因為設計提供 Zeplin 設計稿,所以這裡僅使用到註解,我會把各區塊的 HTML 結構標籤註解進來,以及標示 class name 以及 哪些會用到哪個套件,方便開發時可以參考
- iTerm
- Fork
開發環境
這是這個專案所採用的腳手架:gulp-bs4-jade-template
- Jade (Pug)
- Sass
- PostCSS
- AutoPrefixer
- Babel
- Concat
- UglifyJS
- MinifyCSS
- SourceMap
- ImageMin
- gulp-If
- Browser-sync
- gulp-data
使用 Jade 用來加速 HTML 開發外,也能做到組件模組化目的,面臨到大量修改時,你可以快速的進入調整區域,而不是在那裡從 <html> 標籤開始拉拉拉拉拉到你要改的地方,或者打開 find 工具。到後期套用動態資料時,也能方便因應。
採用 Sass 預處理器 (Scss),一樣是用來模組化你的 CSS ,並能做到變數設置以及流程控制,我在這個專案會編譯到 bootstrap 4 的 variables 以及 components,所以需要編譯工具,並與自己的 Scss 集體編譯到 all.css 給 HTML Link。
有使用到 PostCSS 與 AutoPrefixer,CSS 後處理器,優化 CSS 與加上 CSS 屬性前綴詞,能快速的因應不同瀏覽器的需求。
Babel 用來編譯 JavaScript,讓你可以寫瀏覽器尚未完全支援的 ES 6、ES 7 語法,不過這專案並未負擔到太多 JavaScript,僅有針對廣告輪播、卡片式連結與彈跳視窗的前處理。並使用到 Concat 組合多個 JS 檔案,達到單檔請求的目的。
Minify or Uglify 是用來壓縮 CSS 與 JS,讓網頁請求資源容量有所降低,也搭配了 SourceMap 用來做 DevTools debug 不受到壓縮後的指向問題。
Gulp-if 是用來區別正式環境與開發環境會有不一樣的 gulpfile 設置,譬如開發中就不需要壓縮 CSS 與 JS 等。
Browser-sync 則是用來架一個靜態資源的 server,並監看會修改的檔案做 live reload,節省去網站按 F5 的時間。
gulp-data 是後期才引入的,用來將素材 json 化,以方便素材調整 (or 給 PO、Designer 調整),也能讓後端的參考如何套用動態資料。我們目前未做到前後端分離,所以是將靜態資源提交給後端,再由後端轉成動態網頁與部署設定。
依賴套件
- bootstrap 4
- jQuery 3
這裡主要使用 bootstrap 4,一些 JS Plugins 需要 jQuery 的支援 (至少要 v1.9 以上),因為會用到 Ajax 套件,所以我們非使用 sim 版本。這裡使用 jQuery v3 的版本當然是因為不用支援舊瀏覽器了,更快速,檔案容量也相對較小。
我這裡會用 concat 將 jquery.min.js 與 bootstrap.bundle.min.js 組合成一個 vendor.js 檔案,而不跟自己寫的 js 檔案綁在一起,可以降低編譯時間。
專案架構
104jb-c-ipoc-ceta-f2e ├── src │ ├── data │ │ └── *.json │ ├── img │ ├── js │ │ └── all.js │ ├── partials │ │ ├── index │ │ │ └── *.jade │ │ ├── _footer.jade │ │ ├── _header.jade │ │ └── _scripts.jade │ ├── sass │ │ ├── base │ │ ├── components │ │ ├── helpers │ │ ├── layout │ │ ├── pages │ │ └── all.scss │ ├── _layout.jade │ └── index.jade ├── .gitignore ├── gulpfile.js ├── package.json ├── package-lock.json └── README.md
這裡簡單介紹一下專案架構:
這裡有 Jade、Sass、ES 6 與圖片需要透過 Gulp 編譯,從 src 資料夾進行編譯後會產生 public 資料夾,再將該 public 資料夾與內容檔案提供給後端套動態網站。
- src/data:放置 json 的資料夾,提供給 Jade 動態產生資料的檔案
- src/img:放置圖片的資料夾,會經由 Gulp 壓縮後放置到 public/img
- src/js:放置 JavaScript 檔案的資料夾,可撰寫 ES 6 / ES 7 的程式語法
- partials:放置 jade 部分區塊結構的地方,譬如 header、footer 與 scripts,將共用切在這裡外,裡面也會有特定頁面 (例如 index) 的資料夾供放置部分區塊結構的 jade,最後由 src/index.jade 做 include,並編譯後輸出成 public/index.html
- src/sass:
- base (放置基礎與共用的 scss)
- _mixin.scss
- _typography.scss
- components (放置組件,以 bootstrap 為模擬的自建立組件)
- helpers (覆寫 bootstrap scss file 的資料夾,取相同名稱,然後在 all.scss 改引入自己欲覆寫的檔案)
- _variables.scss
- _carousel.scss
- layout (共用組件、畫面或區塊用)
- _header.scss
- _footer.scss
- pages (特定畫面的 scss)
- _index.scss
- all.scss:用來引入 bootstrap 與自己的 scss 用,我這裡還會註解掉不會用到的 bootstrap components,藉以縮小編譯後的檔案
- base (放置基礎與共用的 scss)
- src/_layout.jade:共用佈景設定,e.g. <html>、<head>、<link>、<body> 與 <script> 等,每個頁面都會引入
- src/index.jade:首頁 jade
如何開始
先下載 gulp-bs4-jade-template 腳手架,然後開始建立上述資料夾,接著就可以開始切版啦!
好啦!其實沒那麼敷衍,這裡會做一個可以被接受的交待程度的 (幹話居多)。
1. 列印設計稿
首先你要先去構思這個設計稿轉成 HTML 的 結構、樣式與互動行為,標示出 HTML tag、CSS styles、class name 以及會套用的套件與組件,做一個前置規劃的準備。你要把網站想成是套樂高的組件化方式,畢竟現在的前端時代已經不是單一支 HTML、CSS 與 JS 就搞定了,模組化思維是很重要的一環,它會影響到你測試與維護上的困難程度,雖然這次是求快速上線的 POC 專案,並不需要力求好的架構思維,但我覺得基本處理還是需要的,更免於後端接手後不知你在切什麼,必須要幫你改甚至打掉重練的狀況。
因為公司設計稿是採用 Zeplin,畫面顏色的色碼與元素的內外間距像素都會自動幫你標示出來,其實你可以省很多前置動作,否則你會使用到 Photoshop 的尺規去測量,用滴管工具去查看色碼等工作。
在沒有高擬真設計稿的前提下,我會使用 MarkMan 幫我做畫面內外間距的測量、滴管色碼與加上我想要的註解,它可以吃非常多的文件格式,而且使用上非常簡單,之後標示好後我就會列印出來,以利開發時可參照規劃思考的架構去實現它。
2. 共用區塊擬定
再來就是先看設計稿有什麼區塊是共用的,譬如色調、字型與哪些區塊是出現兩次以上的,這些是需要獨立出來的,並設置一個有意義的名稱供開發期間去共用它們,這樣可以降低維護的成本,但要考慮清楚牽一髮而動全身的狀況。
有時其實是開發期間才會發現你兩次三次使用到了,才做共用抽離的動作,也不是不行,只是你得往前改之前未共用到的部分,會比較麻煩點,所以前置規劃做得妥不妥善,就在於現在這種狀況的多寡程度。
我真的不喜歡複製行為,受到了模組欲影響,但有些時候你還是需要複製,因為狀況可能只是現在很像,但將來會變的情形,斟酌模組化是一個困難的課題,切版就像藝術,有千萬種寫法可以滿足設計需求的畫面,但背後的處理方式卻有相當的不同,你究竟當下環境是要求到快速還是品質,這樣的平衡點會決定你專案維護上的難易。
在有 bootstrap 4 的幫助下,你很快就會看到設計的畫面是搭配哪些 class name,甚至是用了還要做些小微調的,那就註記下來在你列印的設計稿上,實際切版時就能參考。
盡量減少實際開發時 CSS 在那邊修來修去修到一個定位還不知為什麼突然好了,那代表你對 CSS 屬性還不太熟悉,需要再多磨練相關技巧。
3. 動手切版
腳手架的專案根目錄下在 command line 先執行
$ npm i -g gulp $ npm i $ npm start
如果有看到 bootstrap 4 的 alert 有顯示在畫面上,代表你第一步成功了。
我會先複製一個 bootstrap v4 的 _variables.scss 到 src/sass/helpers 的資料夾下 (腳手架已複製),因為我們勢必會動到 bootstrap 的預設樣式,再由編譯程式轉成你要的樣式去呈現畫面。你也可以在你自己的 scss 上透過權重覆蓋掉,但直接更動 bootstrap 提供的 scss 會更好,節省開發效率與執行時間,但缺點是你要考慮到 bootstrap 的版本升級問題 (or 就鎖定版本不升級就好)。
如果你經驗夠,你會在規劃策略期間就能看到哪些 bootstrap 4 的 components 是需要覆寫的,你就可以複製該 .scss 出來,就像我在這個專案會複製 _carousel.scss 出來,因為設計稿上我已經看到 prev 與 next 的要求有所不同了。
至於其他額外的共用樣式例如針對標籤的、針對共用類別的或一些特定區塊,我就會寫在 src/sass/base/_typography.scss 上。共用方法則寫到 src/sass/base/_mixin.scss。
上述提到的腳手架有預設幫你設置一些你會用到的資料夾與檔案出來,譬如 Jade 、sass 與 js,可以開始往下去擴增你自己的需求。
你會看到 all.scss 裏的檔案,我在 _variable.scss 是引入專案下的版本,所以你若有其他 .scss 需要客製,就也是依樣畫葫蘆。
以下是我在這次專案下的 all.scss:
@import "functions"; @import "helpers/variables"; @import "mixins"; @import "root"; @import "reboot"; @import "type"; @import "images"; //@import "code"; @import "grid"; //@import "tables"; @import "forms"; @import "buttons"; @import "transitions"; @import "dropdown"; @import "button-group"; @import "input-group"; //@import "custom-forms"; //@import "nav"; //@import "navbar"; @import "card"; //@import "breadcrumb"; //@import "pagination"; //@import "badge"; //@import "jumbotron"; //@import "alert"; //@import "progress"; @import "media"; @import "list-group"; //@import "close"; @import "modal"; //@import "tooltip"; //@import "popover"; @import "helpers/carousel"; @import "utilities"; //@import "print"; // custom styles @import "base/mixin"; @import "base/typography"; @import "layout/header"; @import "layout/footer"; @import "pages/index";
你會看到我註解掉一些我在該專案未使用到的 bootstrap 組件,這樣可以提升編譯速度與縮小編譯後的 css 檔案,提升用戶請求網頁的下載速度。
這邊會是長期的切版藝術工作,祝你好運!
5. 導入動態化資料
這要不要實現見仁見智,我這邊的構想是藉由 json 塞素材資料方式可以與後端人員做有效的溝通管道,你的 HTML 結構也不會寫死乃至於後端無法套版,Jade 本身就是 HTML Template engine,它能動態化標籤,這讓你開發效率上…… 是降低的 !!因為你會考慮到怎麼動態編寫 HTML 程式碼啊!不過這可大大提升維護品質,後端也不會太需要你跟他做很多方面的溝通甚至要你改結構,因為你已經動態化過了,而且 json 與相關 HTML 結構是後端的參考項目,那何來大量溝通之有呢?你就去看我給你的資源啊!前端很忙的!!
以下是 gulp-data 安裝與設置步驟:
$ npm i gulp-data --save
gulpfile.js 的 jade 設置:
gulp.task('jade', () => { return gulp.src([`.${srcPath}/**/[^_]*.jade`]) .pipe($.plumber()) .pipe($.data(function (file) { let jobSearch = require(`.${srcPath}/data/job-search.json`); let banner1 = require(`.${srcPath}/data/banner-1.json`); let strongestMain = require(`.${srcPath}/data/strongest-main.json`); let monthMain = require(`.${srcPath}/data/month-main.json`); let banner2 = require(`.${srcPath}/data/banner-2.json`); let brand = require(`.${srcPath}/data/brand.json`); let industry = require(`.${srcPath}/data/industry.json`); let hotVip = require(`.${srcPath}/data/hot-vip.json`); let workplaceFocus = require(`.${srcPath}/data/workplace-focus.json`); let workplaceSenior = require(`.${srcPath}/data/workplace-senior.json`); let source = { jobSearch, banner1, strongestMain, monthMain, banner2, brand, industry, hotVip, workplaceFocus, workplaceSenior }; console.log('jade', source); return source; })) .pipe($.jade({pretty: true})) .pipe(gulp.dest(`.${destPath}`)) .pipe(browserSync.reload({ stream: true, })); });
你會看到我導入了不少 json,那是針對各組件區塊所會使用到的資料。
運用到的 Bootstrap 4
- grid
- utilities
- button
- media object
- form
- input group
- list group
- card
- modal
- carousel
在這 首頁整招版 POC 網頁專案 並未納入 RWD 設計,所以我便固定了畫面寬度為 1024 像素,並自動置中的方式 (依據設計稿需求),像這種樣式規劃我就會放在 src/sass/base/_typography.scss。
對於 n 欄式排版我就採用 bootstrap grid,不需要考慮 media 相關的設置 (xs、sm、md、lg 等),因為不需要 RWD。
大量使用到 bootstrap 的 utilities 做一些畫面上的顏色、間距與版面調整,節省了我不少時間,只要套用相關 class 即可。
media object 就是針對左有圖右有字的版型做套用。
畫面上很多卡片式設計,所以 card 組件就運用得相當充分。而廣告輪播採用 carousel 組件,甚至有 card + carousel 的混合版產生。
modal 是針對舊版功能連結會彈出來提醒使用者。
不過因為設計稿上的寬高與距離拿捏的像素很特別,譬如訂一個 392、402 等單位像素,導致我還是會需要自己寫到相關 CSS 屬性。
客製化 Bootstrap 4 了什麼
_typography.scss
font-size
設計稿大量採用 font-size 是 14 像素,所以我在 _typography.scss 檔案裡面也設置了 font-size: 14px 在 html 元素屬性上。至於為什麼不能寫進 bootstrap v4 的 _variables.scss 裡面是因為 bootstrap 採用 1rem 方式設置,若改由像素處理會在其他 scss 變數依賴設置上出問題,所以需要透過權重方式處理。
html { font-size: 14px; }
a element
a 元素要有一些是共用樣式的,你也可以寫去 _variables.scss 上,在這裡我是透過權重方式處理:
a { color: $gray-20; &:hover { color: $gray-20; img { opacity: .8; } } } a.a_img-opaque { img { opacity: 1; } } a.btn.btn-primary, button.btn.btn-primary { color: #fff; &:hover, &:active { color: #fff; background-color: #FF7800; border-color: transparent; } }
form-control:focus
最主要是不要有 focus 的樣式發散邊框效果
.form-control:focus { box-shadow: none; }
modal 的 X
.modal > .modal-dialog > .modal-content > .modal-header > .button.close { cursor: pointer; }
custom
.text-gray-20 { color: $gray-20; } .text-gray-40 { color: $gray-40; } .bg-gray-20 { background-color: $gray-20; } .bg-gray-40 { background-color: $gray-40; } .border-color-gray-20 { border-color: $gray-20 !important; } .border-color-gray-40 { border-color: $gray-40 !important; } // ...
其他的就是該專案下的特定 class 了。
_variables.scss
font-family
字型的部分有從 Google Fonts 引入 ‘Noto Sans TC’,並將 ‘Arial’ 設為英文數字用,以及加上 ‘微軟正黑體’ 在 ‘Segoe UI’ 之前,所以會改到 _variables.scss 的 $font-family。
$font-family-sans-serif: Arial, 'Noto Sans TC', -apple-system, BlinkMacSystemFont, "Microsoft JhengHei", "Segoe UI", Roboto, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
primary color
主色:primary 我調整了類橘色在 _variables.scss 上,因為大量使用到該顏色。
$primary: #ff9000 !default;
other color
其他色:我增加了兩個 gray 20 與 40 的變數在 _variables.scss,一樣是因為大量使用到該顏色。
$gray-20: #333; $gray-40: #666;
carousel
Carousel 的左右 prev 與 next 箭頭因應設計稿需要調整顏色,以及相關間距:
$carousel-control-color: #999 !default; $carousel-control-width: 35px !default; $carousel-control-opacity: .7 !default; // ... $carousel-indicator-active-bg: rgba($white, .9) !default; $carousel-indicator-bg: #999 !default;
這裡有加一個 $carousel-indicator-bg 變數,是要給 _carousel.scss 使用的。
modal
這裡調整了 modal 的 z-index,因為套後端會有一個我們公司共用元件的黑 bar,裡面放特定連結與登入登出按鈕,而它的 class 是設置 z-index: 9999,大大超過了 bootstrap 4 對 model 預設的 z-index: 1050,所以我們需要比他有更高的值藉以往上提升位置:
$zindex-modal-backdrop: 10000 !default; // 我們要超過 bar_m104 class => z-index: 9999 的黑 bar! $zindex-modal: 10010 !default; // 我們要超過 bar_m104 class => z-index: 9999 的黑 bar!
_carousel.scss
.carousel-indicators { // ... li { // ... background-color: rgba($carousel-indicator-bg, .7); }
這裡調整了 carousel 下方的切換按鈕顏色。
接下來是針對 card 與 carousel 組件因應專案有做特定互動的方式:
Card
因應 Card 需要做到部分連結,甚至有做到混合連結 (連結圖片 + 標題 + 內容),而且要有相關組合是同 hover 效果,而 footer 是不要的,這在 bootstrap 沒有之外,要是在特定 HTML 結構上途加 a 連結標籤,反而畫面會壞掉 (因 bootstrap 結構對應 CSS 非常落實),所以需要 JS 客製化,以下是相關 code:
Jade:
mixin card(title, description, imgPath, imgDescription, url, subLinks) .card.border-0.js_card-new-tab(data-url=url, data-mouseoverclass="month-main_job-card-mouseover") img(src=imgPath, alt=imgDescription) .card-body.px-0.pt-3 h5.card-title.mb-2 #{title} p.card-text !{description} .card-footer.bg-white.border-top-0.px-0 ul.list-inline.month-main_job-title-list each sublink in subLinks +link(sublink.name, sublink.url)
Scss:
// _typography.scss .js_card-new-tab img, .js_card-new-tab .card-body { cursor: pointer; } .card-mouseover { img { opacity: .8; } .card-body { text-decoration: underline; } } // _month-main.scss .month-main_job-card-mouseover { img { opacity: .8; } .card-body h5 { text-decoration: underline; } }
JavaScript:
addNewTabEvent($('.js_card-new-tab img, .js_card-new-tab .card-body'), 'card'); function addNewTabEvent($elements, type) { $elements .on('mouseover', function () { let mouseoverclass = $(this).parents('.js_' + type + '-new-tab').data('mouseoverclass'); if (mouseoverclass) $(this).parents('.js_' + type + '-new-tab').addClass(mouseoverclass); }) .on('click', function () { console.log('click'); let url = $(this).parents('.js_' + type + '-new-tab').data('url'); if (url) window.open(url); }) .on('mouseout', function () { let mouseoverclass = $(this).parents('.js_' + type + '-new-tab').data('mouseoverclass'); if (mouseoverclass) $(this).parents('.js_' + type + '-new-tab').removeClass(mouseoverclass); }); }
Media Object
跟 Card 客製化很類似,也是僅要特定元素要掛連結 (圖片、標題、內容),而某些不要 ( footer )。以下是相關 code:
Jade:
.media.bg-white.banner-1_1.js_media-new-tab(data-url="https://goo.gl/pRu6Gn", data-mouseoverclass="media-mouseover") img.mr-4(src="img/ad_320X210.jpg", alt="ad_320X210") .media-body .media-content h5.mt-4.mb-3 #{banner1._1.title} p #{banner1._1.description} hr .media-footer ul.list-unstyled.unnew-tab each subLink in banner1._1.subLinks li a(href=subLink.url, target="_blank") #{subLink.name} .col-3
Scss:
// _typography.scss .js_media-new-tab img, .js_media-new-tab .media-body > .media-content { cursor: pointer; } .media-mouseover { img { opacity: .8; } .media-body > .media-content { text-decoration: underline; } }
JavaScript:
addNewTabEvent($('.js_media-new-tab img, .js_media-new-tab .media-content'), 'media'); function addNewTabEvent($elements, type) { $elements .on('mouseover', function () { let mouseoverclass = $(this).parents('.js_' + type + '-new-tab').data('mouseoverclass'); if (mouseoverclass) $(this).parents('.js_' + type + '-new-tab').addClass(mouseoverclass); }) .on('click', function () { console.log('click'); let url = $(this).parents('.js_' + type + '-new-tab').data('url'); if (url) window.open(url); }) .on('mouseout', function () { let mouseoverclass = $(this).parents('.js_' + type + '-new-tab').data('mouseoverclass'); if (mouseoverclass) $(this).parents('.js_' + type + '-new-tab').removeClass(mouseoverclass); }); }
Carousel
Carousel 廣告輪播則是滑鼠移過去才要顯示前後切換按鈕,所以這部分也需要客製化處理:
Jade:
#banner-1_2.banner-1_2.carousel.slide(data-ride="carousel") ol.carousel-indicators.mb-0 li.active(data-target="#banner-1_2", data-slide-to="0") li(data-target="#banner-1_2", data-slide-to="1") .carousel-inner.h-100 each item, idx in banner1._2 +carouselItem(idx, item.url, item.imgPath, item.imgDescription) a.carousel-control-prev.invisible(href="#banner-1_2", role="button", data-slide="prev") span.carousel-control-prev-icon(aria-hidden="true") span.sr-only Previous a.carousel-control-next.text-dark.invisible(href="#banner-1_2", role="button", data-slide="next") span.carousel-control-next-icon(aria-hidden="true") span.sr-only Next
JavaScript:
$('.carousel') .carousel({ 'interval': 10000, }) .on('mouseover', function () { $(this).find('.carousel-control-prev').removeClass('invisible'); $(this).find('.carousel-control-next').removeClass('invisible'); }) .on('mouseout', function () { $(this).find('.carousel-control-prev').addClass('invisible'); $(this).find('.carousel-control-next').addClass('invisible'); });
結語
其實這個 首頁整招版 POC 網頁專案 並沒有發揮到 Bootstrap 4 的最大效益,版面設計結構簡單且不需 RWD,所以在切版上分外輕鬆。在這裡我將其實現方式做一個實例探討,希望有幫助到對這塊比較模糊的前端大大們,畢竟現在大前端時代,在網頁切版上需要考慮到更多的東西以利團隊協作與維護,若能夠透過一些框架或工具藉以提升整個開發效率,實在是不免於要多學習與嘗試各項技術與技巧,彼此共同努力。