手機淘寶前端的圖片相關工作流程梳理
本文首發自 阿里無線前端博客
注:本文摘自阿里內網的無線前端博客《無線前端的圖片相關工作流程梳理》。其實是一個月前寫的,鑒于團隊在 中國第二屆 CSS Conf 上做了《手機淘寶 CSS 實踐啟示錄》的分享,而圖片工作流程梳理是其中的一個子話題,故在此一并分享出來,希望仍可以給大家一些經驗和啟發。另外,考慮到這是一篇公開分享,原版內容有部分刪節和調整,里面有一些經驗和產出是和我們的工作環境相關的,不完全具有普遍性,還請見諒。
今天很榮幸的跟大家分享一件事情,就是經過差不多半年多的努力,尤其是最近 2 周的“突擊掃尾”,無線前端團隊又在工具流程方面有了一個不小的突破:我們暫且稱其為“圖片工作流”梳理。
圖片!圖片!圖片!
要說最近 1 年里,無線前端開發的一線同學最“難搞”的幾件事,圖片處理絕對可以排在前三。
- 首先,我們首先要從視覺稿 (絕大部分出自 photoshop) 里把圖片合理的分解、測量、切割、導出——俗稱“切圖”
- 然后,我們要把切好的圖放入頁面代碼中,完成相關的本地調試
- 第三步,把本地圖片通過一個內部網站 (名叫 TPS) 上傳到我們的圖片 CDN 上,并復制圖片的 CDN 地址,把本地調試用的相對路徑替換掉
- 第四步,不同的圖片、不同的外部環境下 (比如 3g 還是 wifi),我們需要給圖片不一樣的尺寸、畫質展現,并有一系列的配置需要遵循
- 如果視覺稿有更改 (不要小看這件事,微觀上還是很頻繁的哦),不好意思,從第一步開始再重新走一遍……
這里面“難搞”在哪些地方呢?我們逐一分析一下:
- “切圖”的效率并不高,而且每一步都很容易出現返工或再溝通
- 打開 TPS 網站上傳圖片放到前端開發流程中并不是一個連貫流暢的步驟,而且 GUI 相比于命令行工具的缺陷在于無法和其它工具更好的集成
- 替換 CDN 圖片路徑的工作機械而繁瑣,并且代碼中替換后的圖片地址失去了原本的可讀性,非常容易造成后期的維護困惑甚至混亂
- 適配工作異常繁雜和辛苦,也很容易漏掉其中的某個環節
- 視覺變更的成本高,web 的快速響應的特點在喪失
所以可能把這些東西畫成一張圖表的話:
團隊的單點突破
在最近半年的一段時間里,無線前端團隊先后發起了下面幾項工作,從某個點上嘗試解決這些問題:
lib.flexible
首先,我們和 UED 團隊共同協商約定了一套 REM 方案 (后更名為 flexible 方案,進而演進為 lib.flexible 庫),通過對視覺稿的產出格式的約定,從工作流程的源頭把控質量,同時在技術上產出了配套的 lib.flexible 庫,可以“抹平”不同設備屏幕的尺寸差異,同時對清晰度進行了智能判斷。這部分工作前端的部分是 @wintercn 寒老師和 @terrykingcha 共同創建的。
視覺稿輔助工具普及
其次,我們于去年 12 月開始啟動了一個“視覺稿工具效率提升”的開放課題,由團隊的 @songsiqi 負責牽頭,我們從課題的一開始就確立了 KPI 和 roadmap,經過一段時間的調研和落實,收羅了很多實用的輔助工具幫助我們提升效率,同時布道給了整個團隊。比如 cutterman 、 parker 、 Size Marks 等
img-uploader
在 @hongru 去年主持完成的一系列 One-Request 前端工具集當中,有一個很有意義的名叫or-uploadimg的圖片上傳工具。它把 TPS 的圖片上傳服務命令化了。這給我們對圖片上傳工作批量化、集成化提供了一個非常重要的基礎!這個工具同時也和淘寶網前端團隊的另一個 TPS 圖片上傳工具有異曲同工之妙。大概用法是這樣的,大家可以感受一下:
var uploader = require('@ali/or-uploadimg'); // 上傳 glob 多張圖 uploader('./**/*.jpg', function (list) { console.log(list) }); // 上傳多張 uploader(['./1.jpg', './3d-base.jpg'], function (list1, list2) { console.log(list1, list2); }) // 上傳單張 uploader('./3d-base.jpg', function (list1) { console.log(list1) })
隨后團隊又出現了這一工具的 gulp 插件,可以對圖片上傳的工作流程做一個簡單的集成,具體集成方式是分析網頁的 html/css 代碼,找到其中的相對圖片地址并上傳+替換 CDN URL。
var gulp = require('gulp'); var imgex = require('@ali/gulp-imgex'); gulp.task('imgex', function() { gulp.src(['./*.html']) .pipe(imgex()) .pipe(gulp.dest('./')); gulp.src('./css/*.css') .pipe(imgex({ base64Limit: 8000, // base64化的圖片size上限,小于這個size會直接base64化,否則上傳cdn uploadDest: 'tps' // 或者 `mt` })) .pipe(gulp.dest('./css')); });
lib.img
lib.img是團隊 @chenerlang666 主持開發的一個基礎庫,它是一套圖片自動處理優化方案。可以同時解決屏幕尺寸判斷、清晰度判斷、網絡環境判斷、域名收斂、尺寸后綴計算、畫質后綴計算、銳化度后綴計算、懶加載等一系列圖片和性能相關的問題。這個庫的意義和實用性都非常之高,并且始終保持著快速的業務響應和迭代周期,也算是無線前端團隊的一個明星作品,也報送了當年度的無線技術金碼獎。
px2rem
px2rem 是 @songsiqi 主持開發的另一個小工具,它因 lib.flexible 方案而生,因為我們統一采用 rem 單位來最終記錄界面的尺寸,且對于個別1像素邊框、文本字號來說,還有特殊的規則作為補充 (詳見 lib.flexible 的文檔)。
同樣的,它也有 gulp / browser 的各種版本。
img4dpr
img4dpr則是一個可以把 CSS 中的 CDN URL 自動轉成 3 種 dpr 下不同的尺寸后綴。算是對 lib.img 的一個補充。如果你的圖片不是產生在<img>標簽或 JavaScript 中,而是寫在了 CSS 文件里,那么即使是 lib.img 恐怕也無能為力,img4dpr 恰恰是在解決這個問題。
完事兒了嗎?
看上去,團隊為團隊做了很多事情,每件事情都在單點上有所突破,解決了一定的問題。
但我們并沒有為此停止思考
有一個很明顯的改進空間在這里:今天我們的前端開發流程是一整套工程鏈路,每個環節之間都緊密相扣, 解決了單點的問題并不是終點,基于場景而不是功能點的思考方式,才能夠把每個環節都流暢的串聯起來,才能給前端開發者在業務支持的過程當中提供完美高效暢通無阻的體驗——這是我們為之努力的更大的價值!也是我認為真正“臨門一腳”的最終價值體現!
基于場景的思維方式
這種思維方式聽上去很玄幻,其實想做到很簡單,我們不要單個兒看某個工具好不好用,牛不牛掰,模擬真實工程場景,創建個新項目,從“切圖”的第一步連續走到發布的最后一步,看看中間哪里斷掉了?哪里銜接的不自然?哪里不完備?哪里重復設計了?哪里可以整合?通常這些問題都會變得一目了然。
首先,在 Photoshop 中“切圖”本身的過程對于后續的開發流程來說是相對獨立的,所以這里并沒有做更多的融合 (從另外一個角度看,這里其實有潛在的改造空間,如何讓“切圖”的工作也能集成到前端工具鏈路中,這值得我們長期思考)
然后,從圖片導出產生的那一刻起,它所經歷的場景大概會是這么幾種:
- 放入images文件夾
- to HTML
- src: (upload time) -> set[src]-> webpack require -> hash filename (upload time) -> file-loader
- data-src
- (upload time) -> set[data-src]-> lib.img (auto resize)
- to JavaScript: element.src, element.style.backgroundImage
- (upload time) -> set[data-src]data
- (upload time) -> set[src](manually resize)
- (upload time) -> setelement.style.background-> lib.img (manually resize)
- to CSS: background-image
- (upload time) -> setbackground-> postcss (upload time) -> px2rem, img4dpr
其中(upload time)指的是我有機會在這個時機把圖片上傳到 CDN 并把代碼里的圖片地址替換掉;(* resize)指的是我有機會在這個時機把圖片的域名收斂/尺寸/畫質/銳化度等需求處理掉。
經過這樣一整理,我們很容易發現問題:
- 圖片上傳存在很多種可選的時機,并沒有形成最佳實踐
- 有些鏈路完全沒有機會做必要的處理 (如 to HTML -> src 的鏈路無法優化圖片地址)
- 有些鏈路處理圖片的邏輯并不夠智能 (比如需要手動確定優化圖片選項的鏈路)
- 圖片上傳 CDN 之后必須手動替換掉源代碼里的圖片路徑,這個問題在任何一個鏈路里都沒有得到解決
- CSS 相關的小工具很多,比較零散,學習和使用的成本在逐步變高變復雜
- 沒有統一完善的項目腳手架,大家創建新項目都需要初始化好多小工具的 gulp 配置 (當然有個土辦法就是從就項目里 copy 一份package.json和一份gulpfile.js)
基于場景的“查漏補缺”
在完善場景的“最后一公里”,我們做了如下的工作:
- 把所有的 CSS 工具集成到了 postcss,再通過 postcss 的 gulp 插件、webpack 插件、browserify 插件令其未來有機會靈活運用到多種場景而不需要做多種工具鏈的適配,即:postcss-px2rem、postcss-img4dpr,同時額外的,借此機會引入 postcss-autoprefixer,讓團隊拜托舊的 webkit 前綴,擁抱標準的寫法
- 把圖片上傳的時機由最早的 or-imgex-gulp 在最后階段分析網頁的html/css代碼上傳替換其中的圖片,變為在images目錄下約定一個名為_cdnurl.json的文件,記錄圖片的 hash 值和線上 CDN 地址,并寫了一個@ali/gulp-img-uploader的 gulp 插件,每次運行的時候會便利images文件夾中的圖片,如果出現新的 hash 值,就自動上傳到 CDN,并把相應生成的 CDN URL 寫入_cdnurl.json
- 同時,這個文件可以引入到頁面的 JavaScript 環境中,引入到 img4dpr 工具中,引入到 lib.img 的邏輯中,讓 HTML/CSS/JavaScript 的各種使用圖片的場景都可以訪問到_cdnurl.json中記錄的本地圖片路徑和線上地址的對應關系
- 這也意味著 lib.img, img4dpr 需要做相應的改動,同時
- 頁面本身要默認把_cdnurl.json的信息引入以做準備
- 創建一個 lib.cdnurl 的庫,在圖片未上傳的情況下,返回本地路徑,在已經上傳的情況下,返回 CDN URL,這樣通過這個庫作支持,外加 lib.img、img4dpr,開發者可以做到在源代碼中完全使用本地路徑,源代碼的可讀性得到了最大程度的保證
- 基于 adam 創建一個包含全套工具鏈路的項目模板 (腳手架)
上述幾件事我們于上周一做了統一討論和分工,這里要感謝 @mingelz @songsiqi @chenerlang666 的共同努力!!
夾帶私貨 (偷笑)
我在這個過程中,融入了之前一段時間集中實踐的 vue 和 webpack 的工程體系 ,在 vue 的基礎上進行組件化開發,在 webpack 的基礎上管理資源打包、集成和發布,最終合并在了最新的 just-vue 的 adam template 里面。
之前不是在文章的最后賣了個“最后一公里”的關子嗎,這里介紹的圖片工作流改進就是其中的一部分:)
同時,我基于 lib.img 的思路,結合 vue.js 自身的特點,寫了一個v-src的 directive,在做到 lib.img 里[data-src]相同目的的同時,更好的融入了 vue.js 的體系,同時加入了更高集成度的功能,稍后會再介紹。
夾帶了私貨之后是不是我就沒法用了?
最后我想強調的是,除了自己的這些“私貨”之外,上面提到的幾個改進點和這些個人的內容是完全解耦的,如果你不選擇 vue.js 或 webpack 而是別的同類型工具或自己研發的一套工具,它依然可以靈活的融入你的工作流程中。
最終效果
我們在團隊內部把這些工作流程以腳手架的方式進行了沉淀,并放在了團隊內部叫做adam的 generator 平臺上 (后續會有介紹) 取名叫做just-vue(時間倉促,adam 和相關的 generator 未來會在適當的時機開放出來)。大致用法:
安裝 adam 和 just-vue 模板:
tnpm install -g @ali/adam adam tmpl add <just-vue git repo>
交互式初始化新項目:
$ adam ? Choose a template: just-vue ? Project Name: y ? Git User or Project Author: ... ? Your email address: ... Awesome! Your project is created! |--.gitignore |--components |--|--foo.vue |--gulpfile.js |--images |--|--_cdnurl.json |--|--logo.png |--|--one.png |--|--taobao.jpg |--lib |--|--lib-cdnurl.js |--|--lib-img.js |--|--vue-src.js |--package.json |--README.md |--src |--|--main.html |--|--main.js |--|--main.vue
目錄結構剖析
然后大家會看到項目目錄里默認就有:
- gulpfile.js,里面默認寫好了圖片批量上傳并更新_cdnurl.json、webpack 打包、htmlone 合并 等常見任務
- images目錄,里面放好了關鍵的_cdnurl.json,還有幾張圖片作為示例,它們的 hash 和 CDN URL 已經寫好了
- src/main.*,主頁面入口,包括一個 htmlone 文件 (main.html),一個 webpack 文件 (main.js) 和一個 vue 主文件 (main.vue),默認引入了需要的所有樣式和腳本,比如 lib.img, lib.flexible, lib.cdnurl, _cdnurl.json, v-src.js 等,我們將來主要的代碼都會從main.vue寫起——額外的,我們為 MT 模板開發者貼心的引入了默認的 mock 數據的<script data-mt-variable="data">標簽,不需要 MT 模板開發環境的將其刪掉即可
- components目錄,這里會把我們拆分下來的子組件都放在這里,我們示范性的放了一個foo.vue的組件在里面,并默認引入了 lib.cdnurl 庫
- lib這里默認放入了 lib.img, lib.cdnurl, v-src.js 幾個庫,這幾個庫在未來逐步穩定之后都會通過 tnpm + CommonJS 的方式進行管理,目前團隊 tnpm + CommonJS 的組件整合還需要一定時間,這里是個方便調整迭代的臨時狀態。
然后,我們來看一看main.vue里的細節,這才是真正讓你真切感受到未來開發體驗的地方。
圖片工作場景
首先,新產生任何圖片,盡管丟到images目錄,別忘了起個好理解的文件名
CSS 中的圖片
然后,在main.vue的第 11 行看到了一個 CSS 的 background-image 的場景,我們只是把url(../images/taobao.jpg)設為其背景圖片:
background-image: url(../images/taobao.jpg);
完成了!就這樣!你在發布之前不需要再關注額外的事情了。沒有手動上傳圖片、沒有另外的GUI、沒有重命名、沒有 CDN 地址替換、沒有圖片地址優化、沒有不可讀的代碼
HTML 中的圖片
我們再來看看 HTML 里的圖片,來到 39 行:
<img id="test-img" v-src="../images/one.png" size="cover">
一個[v-src]特性搞定!就這樣!你在發布之前不需要再關注額外的事情了 (這里[size]特性提供了更多的圖片地址優化策略,篇幅有限,大家感興趣可以移步到lib/vue-src.js看其中的實現原理)。
JavaScript 中的圖片
最后再看看在 JavaScript 里使用圖片,來到 68 行:
this.$el.style.backgroundImage = 'url(' + cdn('../images/logo.png') + ')'
只加入了一步cdn(...)的圖片生成,也搞定了!就這樣!你在發布之前不需要再關注額外的事情了。
發布
那有人可能會懷疑: “那你都說發布之前很方便,發布的時候會不會太麻煩啊?”
好問題,發布就兩行命令:
# 圖片增量上傳、webpack 打包、htmlone 合并,最終生成在 dist 目錄 gulp # 交互式上傳到 awp awp
正常的命令行反應是類似這樣的:
$ gulp [04:46:48] Using gulpfile ~/Sites/alibaba/samples/y/gulpfile.js [04:46:48] Starting 'images'... uploaded ../images/logo.png e1ea82cb1c39656b925012efe60f22ea http://gw.alicdn.com/tfscom/TB1SDNqIFXXXXaTaXXX7WcCNVXX-400-400.png uploaded ../images/one.png 64eb2181ebb96809c7202a162b9289fb http://gw.alicdn.com/tfscom/TB1G7JHIFXXXXbTXpXX_g.pNVXX-400-300.png uploaded ../images/taobao.jpg 4771bae84dfc0e57f841147b86844363 http://gw.alicdn.com/tfscom/TB1f2xSIFXXXXa1XXXXuLfz_XXX-1125-422.jpg [04:46:48] Finished 'images' after 46 ms [04:46:48] Starting 'bundle'... [04:46:49] Version: webpack 1.10.1 Asset Size Chunks Chunk Names main.js 17.1 kB 0 [emitted] main main.js.map 23.5 kB 0 [emitted] main [04:46:49] Finished 'bundle' after 1.28 s [04:46:49] Starting 'build'... "htmlone_temp/cdn_combo_1.css" downloaded! "htmlone_temp/cdn_combo_0.js" downloaded! [04:46:57] >> All html done! [04:46:57] Finished 'build' after 8.07 s [04:46:57] Starting 'default'... done [04:46:57] Finished 'default' after 130 μs $ awp (交互式過程略)
你甚至可以寫成一行:
gulp && awp
最終這個初始化工程的示例頁面的效果如下
設計變更了?
這條鏈路是我們之前最不愿意面對的,今天,我們來看看這條鏈路變成了什么,假設有一張設計圖要換:
- 在 Photoshop 里把圖重新切下來
- 同名圖片文件放入images文件夾
- 運行gulp && awp
就這樣!
額外的,如果尺寸有變化,就加一步:更改相應的 CSS 尺寸代碼
總結
在整個團隊架構的過程中,大家都在不斷嘗試,如何以更貼近開發者真實場景的方式,還原真實的問題,找出切實有效的解決方案,而不僅僅是單個功能或特性。這樣我們往往會找到問題的關鍵,用最精細有效的方式把工作的價值最大化。其實“基于場景的思維方式”不只是流程設計的專利,我們業務上的產品設計、交互設計更需要這樣的思維。我個人也正是受到了一些產品經理朋友們的思維方式的影響,把這種方式運用在了我自己的工作內容當中。希望我們產出的這套方案能夠給大家創造一些價值,更是向大家傳遞我們的心得體會,希望這樣的思維方式和做事方式可以有更多更廣的用武之地。