电竞比分网-中国电竞赛事及体育赛事平台

分享

前端開發(fā)體系建設(shè)日記

 集微筆記 2014-06-24

InfoQ編輯注:本文來(lái)自前端工程師張?jiān)讫埖?a >博客,由作者本人推薦至InfoQ進(jìn)行分享。目前本系列已經(jīng)發(fā)布了三個(gè)部分,本處分享的是第二部分,前端開發(fā)體系建設(shè)日記。建議在閱讀本文前先閱讀本文作者和其團(tuán)隊(duì)之前分享的《前端工程精粹》系列、、。


上周寫了一篇 文章 介紹前端集成解決方案的基本理論,很多同學(xué)看過(guò)之后大呼不過(guò)癮。

干貨 fuck things 在哪里!

本打算繼續(xù)完善理論鏈,形成前端工程的知識(shí)結(jié)構(gòu)。但鑒于如今的快餐文化,po主決定還是先寫一篇實(shí)戰(zhàn)介紹,讓大家看到前端工程體系能為團(tuán)隊(duì)帶來(lái)哪些好處,調(diào)起大家的胃口再說(shuō)。

ps: 寫完才發(fā)現(xiàn)這篇文章真的非常非常長(zhǎng),涵蓋了前端開發(fā)中的很多方面,希望大家能有耐心看完,相信一定會(huì)有所斬獲。。。

2014年02月12日 - 晴

新到松鼠團(tuán)隊(duì)的第二天,小伙伴 @nino 找到我說(shuō)

nino: 視頻項(xiàng)目打算重新梳理一下,希望能引入新的技術(shù)體系,解決現(xiàn)有的一些問(wèn)題。

po主不禁暗喜,好機(jī)會(huì),這是我專業(yè)啊,藍(lán)翔技校-前端集成解決方案學(xué)院-自動(dòng)化系-打包學(xué)專業(yè)的文憑不是白給的,于是自信滿滿的對(duì)nino說(shuō),有什么需求盡管提!

nino: 我的需求并不多,就這么幾條~~

  1. 模塊化開發(fā)。最好能像寫nodejs一樣寫js,很舒服。css最好也能來(lái)個(gè)模塊化管理!
  2. 性能要好。模塊那么多,得有按需加載,請(qǐng)求不能太多。
  3. 組件化開發(fā)。一個(gè)組件的js、css、模板最好都在一個(gè)目錄維護(hù),維護(hù)起來(lái)方便。
  4. handlebars 作為前端模板引擎。這個(gè)模板引擎不錯(cuò),logic-less(輕邏輯)。
  5. stylus 寫css挺方便,給我整一個(gè)。
  6. 圖片base64嵌入。有些小圖可能要以base64的形式嵌入到頁(yè)面、js或者css中使用。嵌入之前記得壓縮圖片以減小體積。
  7. js/css/圖片壓縮應(yīng)該都沒(méi)問(wèn)題吧。
  8. 要能與公司的ci平臺(tái)集。工具里最好別依賴什么系統(tǒng)庫(kù),ci的機(jī)器未必支持。
  9. 開發(fā)體驗(yàn)要好。文件監(jiān)聽,瀏覽器自動(dòng)刷新(livereload)一個(gè)都不能少。
  10. 我們用nodejs作為服務(wù)器,本地要能預(yù)覽,最好再能抓取線上數(shù)據(jù),方便調(diào)試。

我倒吸一口涼氣,但表面故作鎮(zhèn)定的說(shuō):恩,確實(shí)不多,讓我們先來(lái)看看第一個(gè)需求。。。

還沒(méi)等我說(shuō)完,nino打斷我說(shuō)

nino: 橋豆麻袋(稍等),還有一個(gè)最重要的需求!

松鼠公司的松鼠瀏覽器你知道吧,恩,它有很多個(gè)版本的樣子。
我希望代碼發(fā)布后能按照版本部署,不要彼此覆蓋。

舉個(gè)例子,代碼部署結(jié)構(gòu)可能是這樣的:

  release/
    - public/
      - 項(xiàng)目名
        - 1.0.0/
        - 1.0.1/
        - 1.0.2/
        - 1.0.2-alpha/
        - 1.0.2-beta/

讓歷史瀏覽器瀏覽歷史版本,沒(méi)事還能做個(gè)灰度發(fā)布,ABTest啥的,多好!

此外,我們將來(lái)會(huì)有多個(gè)項(xiàng)目使用這套開發(fā)模式,希望能共用一些組件或者模
塊,產(chǎn)品也會(huì)公布一些api模塊給第三方使用,所以共享模塊功能也要加上。

總的來(lái)說(shuō),還要追加兩個(gè)部署需求:

  1. 按版本部署,采用非覆蓋式發(fā)布
  2. 允許第三方引用項(xiàng)目公共模塊

nino: 怎么樣,不算復(fù)雜吧,這個(gè)項(xiàng)目很趕,3天搞定怎么樣?

我凝望著會(huì)議室白板上的這些需求,正打算爭(zhēng)辯什么,一扭頭發(fā)現(xiàn)nino已經(jīng)不見了。。。正在沮喪之際,小伙伴 @hinc 過(guò)來(lái)找我,跟他大概講了一下nino的需求,正想跟他抱怨工期問(wèn)題時(shí),hinc卻說(shuō)

hinc: 恩,這正是我們需要的開發(fā)體系,不過(guò)我這里還有一個(gè)需求。。。

  1. 我們之前積累了一些業(yè)務(wù)可以共用的模塊,放在了公司內(nèi)的gitlab上,采用 component 作為發(fā)布規(guī)范,能不能再加上這個(gè)組件倉(cāng)庫(kù)的支持?

3天時(shí)間,13項(xiàng)前端技術(shù)元素,靠譜么。。。

2014年02月13日 - 多云

一覺(jué)醒來(lái),輕松了許多,但還有任務(wù)在身,不敢有半點(diǎn)怠慢。整理一下昨天的需求,我們來(lái)做一個(gè)簡(jiǎn)單的劃分。

  • 規(guī)范
    • 開發(fā)規(guī)范
      • 模塊化開發(fā),js模塊化,css模塊化,像nodejs一樣編碼
      • 組件化開發(fā),js、css、handlebars維護(hù)在一起
    • 部署規(guī)范
      • 采用nodejs后端,基本部署規(guī)范應(yīng)該參考 express 項(xiàng)目部署
      • 按版本號(hào)做非覆蓋式發(fā)布
      • 公共模塊可發(fā)布給第三方共享
  • 框架
    • js模塊化框架,支持請(qǐng)求合并,按需加載等性能優(yōu)化點(diǎn)
  • 工具
    • 可以編譯stylus為css
    • 支持js、css、圖片壓縮
    • 允許圖片壓縮后以base64編碼形式嵌入到css、js或html中
    • 與ci平臺(tái)集成
    • 文件監(jiān)聽、瀏覽器自動(dòng)刷新
    • 本地預(yù)覽、數(shù)據(jù)模擬
  • 倉(cāng)庫(kù)
    • 支持component模塊安裝和使用

這樣一套規(guī)范、框架、工具和倉(cāng)庫(kù)的開發(fā)體系,服從我之前介紹的 前端集成解決方案 的描述。前端界每天都團(tuán)隊(duì)在設(shè)計(jì)和實(shí)現(xiàn)這類系統(tǒng),它們其實(shí)是有規(guī)律可循的。百度出品的 fis 就是一個(gè)能幫助快速搭建前端集成解決方案的工具。使用fis我應(yīng)該可以在3天之內(nèi)完成這些任務(wù)。

ps: 這不是一篇關(guān)于fis的軟文,如果這樣的一套系統(tǒng)基于grunt實(shí)現(xiàn)相信會(huì)有非常大量的開發(fā)工作,3天完成幾乎是不可能的任務(wù)。

不幸的是,現(xiàn)在fis官網(wǎng)所介紹的 并不是 fis,而是一個(gè)叫 fis-plus 的項(xiàng)目,該項(xiàng)目并不像字面理解的那樣是fis的加強(qiáng)版,而是在fis的基礎(chǔ)上定制的一套面向百度前端團(tuán)隊(duì)的解決方案,以php為后端語(yǔ)言,跟smarty有較強(qiáng)的綁定關(guān)系,有著 19項(xiàng) 技術(shù)要素,密切配合百度現(xiàn)行技術(shù)選型。絕大多數(shù)非百度前端團(tuán)隊(duì)都很難完整接受這19項(xiàng)技術(shù)選型,尤其是其中的部署、框架規(guī)范,跟百度前端團(tuán)隊(duì)相關(guān)開發(fā)規(guī)范、部署規(guī)范、以及php、smarty等有著較深的綁定關(guān)系。

因此如果你的團(tuán)隊(duì)用的不是 php后端 && smarty模板 && modjs模塊化框架 && bingo框架 的話,請(qǐng)查看fis的文檔,或許不會(huì)有那么多困惑。

ps: fis是一個(gè)構(gòu)建系統(tǒng)內(nèi)核,很好的抽象了前端集成解決方案所需的通用工具需求,本身不與任何后端語(yǔ)言綁定。而基于fis實(shí)現(xiàn)的具體解決方案就會(huì)有具體的規(guī)范和技術(shù)選型了。

言歸正傳,讓我們基于 fis 開始實(shí)踐這套開發(fā)體系吧!

0. 開發(fā)概念定義

前端開發(fā)體系設(shè)計(jì)第一步要定義開發(fā)概念。開發(fā)概念是指針對(duì)開發(fā)資源的分類概念。開發(fā)概念的確立,直接影響到規(guī)范的定制。比如,傳統(tǒng)的開發(fā)概念一般是按照文件類型劃分的,所以傳統(tǒng)前端項(xiàng)目會(huì)有這樣的目錄結(jié)構(gòu):

  • js:放js文件
  • css:放css文件
  • images:放圖片文件
  • html:放html文件

這樣確實(shí)很直接,任何智力健全的人都知道每個(gè)文件該放在哪里。但是這樣的開發(fā)概念劃分將給項(xiàng)目帶來(lái)較高的維護(hù)成本,并為項(xiàng)目臃腫埋下了工程隱患,理由是:

  1. 如果項(xiàng)目中的一個(gè)功能有了問(wèn)題,維護(hù)的時(shí)候要在js目錄下找到對(duì)應(yīng)的邏輯修改,再到css目錄下找到對(duì)應(yīng)的樣式文件修改一下,如果圖片不對(duì),還要再跑到images目錄下找對(duì)應(yīng)的開發(fā)資源。
  2. images下的文件不知道哪些圖片在用,哪些已經(jīng)廢棄了,誰(shuí)也不敢刪除,文件越來(lái)越多。

ps: 除非你的團(tuán)隊(duì)只有1-2個(gè)人,你的項(xiàng)目只有很少的代碼量,而且不用關(guān)心性能和未來(lái)的維護(hù)問(wèn)題,否則,以文件為依據(jù)設(shè)計(jì)的開發(fā)概念是應(yīng)該被拋棄的。

以我個(gè)人的經(jīng)驗(yàn),更傾向于具有一定語(yǔ)義的開發(fā)概念。綜合前面的需求,我為這個(gè)開發(fā)體系確定了3個(gè)開發(fā)資源概念:

  • 模塊化資源:js模塊、css模塊或組件
  • 頁(yè)面資源:網(wǎng)站html或后端模板頁(yè)面,引用模塊化框架,加載模塊
  • 非模塊化資源:并不是所有的開發(fā)資源都是模塊化的,比如提供模塊化框架的js本身就不能是一個(gè)模塊化的js文件。嚴(yán)格上講,頁(yè)面也屬于一種非模塊化的靜態(tài)資源。

ps: 開發(fā)概念越簡(jiǎn)單越好,前面提到的fis-plus也有類似的開發(fā)概念,有組件或模塊(widget),頁(yè)面(page),測(cè)試數(shù)據(jù)(test),非模塊化靜態(tài)資源(static)。有的團(tuán)隊(duì)在模塊之中又劃分出api模塊和ui模塊(組件)兩種概念。

1. 開發(fā)目錄設(shè)計(jì)

基于開發(fā)概念的確立,接下來(lái)就要確定目錄規(guī)范了。我通常會(huì)給每種開發(fā)資源的目錄取一個(gè)有語(yǔ)義的名字,三種資源我們可以按照概念直接定義目錄結(jié)構(gòu)為:

project
  - modules       存放模塊化資源
  - pages         存放頁(yè)面資源
  - static        存放非模塊化資源

這樣劃分目錄確實(shí)直觀,但結(jié)合前面hinc說(shuō)過(guò)的,希望能使用component倉(cāng)庫(kù)資源,因此我決定將模塊化資源目錄命名為components,得到:

project
  - components    存放模塊化資源
  - pages         存放頁(yè)面資源
  - static        存放非模塊化資源

而nino又提到過(guò)模塊資源分為項(xiàng)目模塊和公共模塊,以及hinc提到過(guò)希望能從component安裝一些公共組件到項(xiàng)目中,因此,一個(gè)components目錄還不夠,想到nodejs用node_modules作為模塊安裝目錄,因此我在規(guī)范中又追加了一個(gè) component_modules 目錄,得到:

project
  - component_modules    存放外部模塊資源
  - components           存放項(xiàng)目模塊資源
  - pages                存放頁(yè)面資源
  - static               存放非模塊化資源

nino說(shuō)過(guò)今后大多數(shù)項(xiàng)目采用nodejs作為后端,express是比較常用的nodejs的server框架,express項(xiàng)目通常會(huì)把后端模板放到 views 目錄下,把靜態(tài)資源放到 public 下。為了迎合這樣的需求,我將page、static兩個(gè)目錄調(diào)整為 views 和 public,規(guī)范又修改為:

project
  - component_modules    存放外部模塊資源
  - components           存放項(xiàng)目模塊資源
  - views                存放頁(yè)面資源
  - public               存放非模塊化資源

考慮到頁(yè)面也是一種靜態(tài)資源,而public這個(gè)名字不具有語(yǔ)義性,與其他目錄都有概念沖突,不如將其與views目錄合并,views目錄負(fù)責(zé)存放頁(yè)面和非模塊化資源比較合適,因此最終得到的開發(fā)目錄結(jié)構(gòu)為:

project
  - component_modules    存放外部模塊資源
  - components           存放項(xiàng)目模塊資源
  - views                存放頁(yè)面以及非模塊化資源

2. 部署目錄設(shè)計(jì)

托nino的福,咱們的部署策略將會(huì)非常復(fù)雜,根據(jù)要求,一個(gè)完整的部署結(jié)果應(yīng)該是這樣的目錄結(jié)構(gòu):

release
  - public
    - 項(xiàng)目名
      - 1.0.0    1.0.0版本的靜態(tài)資源都構(gòu)建到這里
      - 1.0.1    1.0.1版本的靜態(tài)資源都構(gòu)建到這里
      - 1.0.2    1.0.2版本的靜態(tài)資源都構(gòu)建到這里
      ...
  - views
    - 項(xiàng)目名
      - 1.0.0    1.0.0版本的后端模板都構(gòu)建到這里
      - 1.0.1    1.0.1版本的后端模板都構(gòu)建到這里
      - 1.0.2    1.0.2版本的后端模板都構(gòu)建到這里
      ...

由于還要部署一些可以被第三方使用的模塊,public下只有項(xiàng)目名的部署還不夠,應(yīng)改把模塊化文件單獨(dú)發(fā)布出來(lái),得到這樣的部署結(jié)構(gòu):

release
  - public
    - component_modules   模塊化資源都部署到這個(gè)目錄下
      - module_a
        - 1.0.0
          - module_a.js
          - module_a.css
          - module_a.png
        - 1.0.1
        - 1.0.2
        ...
    - 項(xiàng)目名
      - 1.0.0    1.0.0版本的靜態(tài)資源都構(gòu)建到這里
      - 1.0.1    1.0.1版本的靜態(tài)資源都構(gòu)建到這里
      - 1.0.2    1.0.2版本的靜態(tài)資源都構(gòu)建到這里
      ...
  - views
    - 項(xiàng)目名
      - 1.0.0    1.0.0版本的后端模板都構(gòu)建到這里
      - 1.0.1    1.0.1版本的后端模板都構(gòu)建到這里
      - 1.0.2    1.0.2版本的后端模板都構(gòu)建到這里
      ...

由于 component_modules 這個(gè)名字太長(zhǎng)了,如果部署到這樣的路徑下,url會(huì)很長(zhǎng),這也是一個(gè)優(yōu)化點(diǎn),因此最終決定部署結(jié)構(gòu)為:

release
  - public
    - c                   模塊化資源都部署到這個(gè)目錄下
      - 公共模塊
        - 版本號(hào)
      - 項(xiàng)目名
        - 版本號(hào)
    - 項(xiàng)目名
      - 版本號(hào)             非模塊化資源都部署到這個(gè)目錄下
  - views
    - 項(xiàng)目名
      - 版本號(hào)             后端模板都構(gòu)建到這個(gè)目錄下

插一句,并不是所有團(tuán)隊(duì)都會(huì)有這么復(fù)雜的部署要求,這和松鼠團(tuán)隊(duì)的業(yè)務(wù)需求有關(guān),但我相信這個(gè)例子也不會(huì)是最復(fù)雜的。每個(gè)團(tuán)隊(duì)都會(huì)有自己的運(yùn)維需求,前端資源部署經(jīng)常牽連到公司技術(shù)架構(gòu),因此很多前端項(xiàng)目的開發(fā)目錄結(jié)構(gòu)會(huì)和部署要求保持一致。這也為項(xiàng)目間模塊的復(fù)用帶來(lái)了成本,因?yàn)榇a中寫的url通常是部署后的路徑,遷移之后就可能失效了。

解耦開發(fā)規(guī)范和部署規(guī)范是前端開發(fā)體系的設(shè)計(jì)重點(diǎn)。

好了,去吃個(gè)午飯,下午繼續(xù)。。。

3. 配置fis連接開發(fā)規(guī)范和部署規(guī)范

我準(zhǔn)備了一個(gè)樣例項(xiàng)目:

project
  - views
    - logo.png
    - index.html
  - fis-conf.js
  - README.md

fis-conf.js是fis工具的配置文件,接下來(lái)我們就要在這里進(jìn)行構(gòu)建配置了。雖然開發(fā)規(guī)范和部署規(guī)范十分復(fù)雜,但好在fis有一個(gè)非常強(qiáng)大的 roadmap.path 功能,專門用于分類文件、調(diào)整發(fā)布結(jié)構(gòu)、指定文件的各種屬性等功能實(shí)現(xiàn)。

所謂構(gòu)建,其核心任務(wù)就是將文件按照某種規(guī)則進(jìn)行分類(以文件后綴分類,以模塊化/非模塊化分類,以前端/后端代碼分類),然后針對(duì)不同的文件做不同的構(gòu)建處理。

閑話少說(shuō),我們先來(lái)看一下基本的配置,在 fis-conf.js 中添加代碼:

fis.config.set('roadmap.path', [
    {
        reg : '**.md',   //所有md后綴的文件
        release : false  //不發(fā)布
    }
]);

以上配置,使得項(xiàng)目中的所有md后綴文件都不會(huì)發(fā)布出來(lái)。release是定義file對(duì)象發(fā)布路徑的屬性,如果file對(duì)象的release屬性為false,那么在項(xiàng)目發(fā)布階段就不會(huì)被輸出出來(lái)。

在fis中,roadmap.pah是一個(gè)數(shù)組數(shù)據(jù),數(shù)組每個(gè)元素是一個(gè)對(duì)象,必須定義 reg 屬性,用以匹配項(xiàng)目文件路徑從而進(jìn)行分類劃分,reg屬性的取值可以是路徑通配字符串或者正則表達(dá)式。fis有一個(gè)內(nèi)部的文件系統(tǒng),會(huì)給每個(gè)源碼文件創(chuàng)建一個(gè) fis.File 對(duì)象,創(chuàng)建File對(duì)象時(shí),按照roadmap.path的配置逐個(gè)匹配文件路徑,匹配成功則把除reg之外的其他屬性賦給File對(duì)象,fis中各種處理環(huán)節(jié)及插件都會(huì)讀取所需的文件對(duì)象的屬性值,而不會(huì)自己定義規(guī)范。有關(guān)roadmap.path的工作原理可以看這里 以及 這里。

ok,讓md文件不發(fā)布很簡(jiǎn)單,那么views目錄下的按版本發(fā)布要求怎么實(shí)現(xiàn)呢?其實(shí)也是非常簡(jiǎn)單的配置:

fis.config.set('roadmap.path', [
    {
        reg : '**.md',   //所有md后綴的文件
        release : false  //不發(fā)布
    },
    {
        //正則匹配【/views/**】文件,并將views后面的路徑捕獲為分組1
        reg : /^\/views\/(.*)$/i,
        //發(fā)布到 public/proj/1.0.0/分組1 路徑下
        release : '/public/proj/1.0.0/$1'
    }
]);

roadmap.path數(shù)組的第二元素?fù)?jù)采用正則作為匹配規(guī)則,正則可以幫我們捕獲到分組信息,在release屬性值中引用分組是非常方便的。正則匹配 + 捕獲分組,成為目錄規(guī)范配置的強(qiáng)有力工具:

在上面的配置中,版本號(hào)被寫到了匹配規(guī)則里,這樣非常不方便工程師在迭代的過(guò)程中升級(jí)項(xiàng)目版本。我們應(yīng)該將版本號(hào)、項(xiàng)目名稱等配置獨(dú)立出來(lái)管理。好在roadmap.path還有讀取其他配置的能力,修改上面的配置,我們得到:

//開發(fā)部署規(guī)范配置
fis.config.set('roadmap.path', [
    {
        reg : '**.md',   //所有md后綴的文件
        release : false  //不發(fā)布
    },
    {
        reg : /^\/views\/(.*)$/i,
        //使用${xxx}引用fis.config的其他配置項(xiàng)
        release : '/public/${name}/${version}/$1'
    }
]);

//項(xiàng)目配置,將name、version獨(dú)立配置,統(tǒng)管全局
fis.config.set('name', 'proj');
fis.config.set('version', '1.0.0');

fis的配置系統(tǒng)非常靈活,除了 文檔 中提到的配置節(jié)點(diǎn),其他配置用戶可以隨便定義使用。比如配置的roadmap是系統(tǒng)保留的,而name、version都是用戶自己隨便指定的。fis系統(tǒng)保留的配置節(jié)點(diǎn)只有6個(gè),包括:

  1. project(系統(tǒng)配置)
  2. modules(構(gòu)建流程配置)
  3. settings(插件配置)
  4. roadmap(目錄規(guī)范與域名配置)
  5. deploy(部署目標(biāo)配置)
  6. pack(打包配置)

完成第一份配置之后,我們來(lái)看一下效果。

cd project
fis release --dest ../release

進(jìn)入到項(xiàng)目目錄,然后使用fis release命令,對(duì)項(xiàng)目進(jìn)行構(gòu)建,用 --dest <path> 參數(shù)指定編譯結(jié)果的產(chǎn)出路徑,可以看到部署后的結(jié)果:

ps: fis release會(huì)將處理后的結(jié)果發(fā)布到源碼目錄之外的其他目錄里,以保持源碼目錄的干凈。

fis系統(tǒng)的強(qiáng)大之處在于當(dāng)你調(diào)整了部署規(guī)范之后,fis會(huì)識(shí)別所有資源定位標(biāo)記,將他們修改為對(duì)應(yīng)的部署路徑。

fis的文件系統(tǒng)設(shè)計(jì)決定了配置開發(fā)規(guī)范的成本非常低。fis構(gòu)建核心有三個(gè)超級(jí)正則,用于識(shí)別資源定位標(biāo)記,把用戶的開發(fā)規(guī)范和部署規(guī)范通過(guò)配置完整連接起來(lái),具體實(shí)現(xiàn)可以看這里。

不止html,fis為前端三種領(lǐng)域語(yǔ)言都準(zhǔn)備了資源定位識(shí)別標(biāo)記,更多文檔可以看這里:在html中定位資源,在js中定位資源,在css中定位資源

接下來(lái),我們修改一下項(xiàng)目版本配置,再發(fā)布一下看看效果:

fis.config.set('version', '1.0.1');

再次執(zhí)行:

cd project
fis release --dest ../release

得到:

至此,我們已經(jīng)基本解決了開發(fā)和部署直接的目錄規(guī)范問(wèn)題,這里我需要加快一些步伐,把其他目錄的部署規(guī)范也配置好,得到一個(gè)相對(duì)比較完整的結(jié)果:

fis.config.set('roadmap.path', [
    {
        //md后綴的文件不發(fā)布
        reg : '**.md',
        release : false
    },
    {
        //component_modules目錄下的代碼,由于component規(guī)范,已經(jīng)有了版本號(hào)
        //我將它們直接發(fā)送到public/c目錄下就好了
        reg : /^\/component_modules\/(.*)$/i,
        release : '/public/c/$1'
    },
    {
        //項(xiàng)目模塊化目錄沒(méi)有版本號(hào)結(jié)構(gòu),用全局版本號(hào)控制發(fā)布結(jié)構(gòu)
        reg : /^\/components\/(.*)$/i,
        release : '/public/c/${name}/${version}/$1'
    },
    {
        //views目錄下的文件發(fā)布到【public/項(xiàng)目名/版本】目錄下
        reg : /^\/views\/(.*)$/,
        release : '/public/${name}/${version}/$1'
    },
    {
        //其他文件就不屬于前端項(xiàng)目了,比如nodejs的后端代碼
        //不處理這些文件的資源定位替換(useStandard: false)
        //也不用對(duì)這些資源進(jìn)行壓縮(useOptimizer: false)
        reg : '**',
        useStandard : false,
        useOptimizer : false
    }
]);

fis.config.set('name', 'proj');
fis.config.set('version', '1.0.2');

我構(gòu)造了一個(gè)相對(duì)完整的目錄結(jié)構(gòu),然后進(jìn)行了一次構(gòu)建,效果還不錯(cuò):

不管部署規(guī)則多么復(fù)雜都不用擔(dān)心,有fis強(qiáng)大的資源定位系統(tǒng)幫你在開發(fā)規(guī)范和部署規(guī)范之間建立聯(lián)系,設(shè)計(jì)開發(fā)體系不在受制于工具的實(shí)現(xiàn)能力。

你可以盡情發(fā)揮想象力,設(shè)計(jì)出最優(yōu)雅最合理的開發(fā)規(guī)范和最高效最貼合公司運(yùn)維要求的部署規(guī)范,最終用fis的roadmap.path功能將它們連接起來(lái),實(shí)現(xiàn)完美轉(zhuǎn)換。

fis的roadmap功能實(shí)際上提供了項(xiàng)目代碼與部署規(guī)范解耦的能力。

從前面的例子可以看出,開發(fā)使用相對(duì)路徑即可,fis會(huì)在構(gòu)建時(shí)會(huì)根據(jù)fis-conf.js中的配置完成開發(fā)路徑到部署路徑的轉(zhuǎn)換工作。這意味著在fis體系下開發(fā)的模塊將具有天然的可移植性,既能滿足不同項(xiàng)目的不同部署需求,又能允許開發(fā)中使用相對(duì)路徑進(jìn)行資源定位,工程師再不用把部署路徑寫到代碼中了。

愉快的一天就這么過(guò)去了,睡覺(jué)!

2014年02月14日 - 陰轉(zhuǎn)多云

每到周五總是非常愜意的感覺(jué),不管這一周多么辛苦,周五就是一個(gè)解脫,更何況今天還是個(gè)特別的日子——情人節(jié)!

昨天主要解決了開發(fā)概念、開發(fā)目錄規(guī)范、部署目錄規(guī)范以及初步的fis-conf.js配置。今天要進(jìn)行前端開發(fā)體系設(shè)計(jì)的關(guān)鍵任務(wù)——模塊化框架。

模塊化框架是前端開發(fā)體系中最為核心的環(huán)節(jié)。

模塊化框架肩負(fù)著模塊管理、資源加載、性能優(yōu)化(按需,請(qǐng)求合并)等多種重要職責(zé),同時(shí)它也是組件開發(fā)的基礎(chǔ)框架,因此模塊化框架設(shè)計(jì)的好壞直接影響到開發(fā)體系的設(shè)計(jì)質(zhì)量。

很遺憾的說(shuō),現(xiàn)在市面上已有的模塊化框架都沒(méi)能很好的處理模塊管理、資源加載和性能優(yōu)化三者之間的關(guān)系。這倒不是框架設(shè)計(jì)的問(wèn)題,而是由前端領(lǐng)域語(yǔ)言特殊性決定的??蚣茉O(shè)計(jì)者一般在思考模塊化框架時(shí),通常站在純前端運(yùn)行環(huán)境角度考慮,基本功能都是用原生js實(shí)現(xiàn)的,因此一個(gè)模塊化開發(fā)的關(guān)鍵問(wèn)題不能被很好的解決。這個(gè)關(guān)鍵問(wèn)題就是依賴聲明。

seajs 為例(無(wú)意冒犯),seajs采用運(yùn)行時(shí)分析的方式實(shí)現(xiàn)依賴聲明識(shí)別,并根據(jù)依賴關(guān)系做進(jìn)一步的模塊加載。比如如下代碼:

define(function(require) {
  var foo = require("foo");
  //...
});

當(dāng)seajs要執(zhí)行一個(gè)模塊的factory函數(shù)之前,會(huì)先分析函數(shù)體中的require書寫,具體代碼在這里這里,大概的代碼邏輯如下:

Module.define = function (id, deps, factory) {
    ...
    //抽取函數(shù)體的字符串內(nèi)容
    var code = factory.toString();
    //設(shè)計(jì)一個(gè)正則,分析require語(yǔ)句
    var reg = /\brequire\s*\(([.*]?)\)/g;
    var deps = [];
    //掃描字符串,得到require所聲明的依賴
    code.replace(reg, function(m, $1){
        deps.push($1);
    });
    //加載依賴,完成后再執(zhí)行factory
    ...
};

由于框架設(shè)計(jì)是在“純前端實(shí)現(xiàn)”的約束條件下,使得模塊化框架對(duì)于依賴的分析必須在模塊資源加載完成之后才能做出識(shí)別。這將引起兩個(gè)性能相關(guān)的問(wèn)題:

  1. require被強(qiáng)制為關(guān)鍵字而不能被壓縮。否則factory.toString()的分析將沒(méi)有意義。
  2. 依賴加載只能串行進(jìn)行,當(dāng)一個(gè)模塊加載完成之后才知道后面的依賴關(guān)系。

第一個(gè)問(wèn)題還好,尤其是在gzip下差不多多少字節(jié),但是要配置js壓縮器保留require函數(shù)不壓縮。第二個(gè)問(wèn)題就比較麻煩了,雖然seajs有seajs-combo插件可以一定程度上減少請(qǐng)求,但仍然不能很好的解決這個(gè)問(wèn)題。舉個(gè)例子,有如下seajs模塊依賴關(guān)系樹:

ps: 圖片來(lái)源 @raphealguo

采用seajs-combo插件之后,靜態(tài)資源請(qǐng)求的效果是這樣的:

  1. http://www./page.js
  2. http://www./a.js,b.js
  3. http://www./c.js,d.js,e.js
  4. http://www./f.js

工作過(guò)程是

  1. 框架先請(qǐng)求了入口文件page.js
  2. 加載完成后分析factory函數(shù),發(fā)現(xiàn)依賴了a.js和b.js,然后發(fā)起一個(gè)combo請(qǐng)求,加載a.js和b.js
  3. a.js和b.js加載完成后又進(jìn)一步分析源碼,才發(fā)現(xiàn)還依賴了c.js、d.js和e.js,再發(fā)起請(qǐng)求加載這三個(gè)文件
  4. 完成c、d、e的加載之后,再分析,發(fā)現(xiàn)f.js依賴,再請(qǐng)求
  5. 完成f.js的請(qǐng)求,page.js的依賴全部滿足,執(zhí)行它的factory函數(shù)。

雖然combo可以在依賴層級(jí)上進(jìn)行合并,但完成page.js的請(qǐng)求仍需要4個(gè)。很多團(tuán)隊(duì)在使用seajs的時(shí)候,為了避免這樣的串行依賴請(qǐng)求問(wèn)題,會(huì)自己實(shí)現(xiàn)打包方案,將所有文件直接打包在一起,放棄了模塊化的按需加載能力,也是一種無(wú)奈之舉。

原因很簡(jiǎn)單

以純前端方式來(lái)實(shí)現(xiàn)模塊依賴加載不能同時(shí)解決性能優(yōu)化問(wèn)題。

歸根結(jié)底,這樣的結(jié)論是由前端領(lǐng)域語(yǔ)言的特點(diǎn)決定的。前端語(yǔ)言缺少三種編譯能力,前面講目錄規(guī)范和部署規(guī)范時(shí)其實(shí)已經(jīng)提到了一種能力,就是“資源定位的能力”,讓工程師使用開發(fā)路徑定位資源,編譯后可轉(zhuǎn)換為部署路徑。其他語(yǔ)言編寫的程序幾乎都沒(méi)有web這種物理上分離的資源部署策略,而且大多具都有類似'getResource(path)'這樣的函數(shù),用于在運(yùn)行環(huán)境下定位當(dāng)初的開發(fā)資源,這樣不管項(xiàng)目怎么部署,只要getResource函數(shù)運(yùn)行正常就行了??上岸苏Z(yǔ)言沒(méi)有這樣的資源定位接口,只有url這樣的資源定位符,它指向的其實(shí)并不是開發(fā)路徑,而是部署路徑。

這里可以簡(jiǎn)單列舉出前端語(yǔ)言缺少三種的語(yǔ)言能力:

  • 資源定位的能力:使用開發(fā)路徑進(jìn)行資源定位,項(xiàng)目發(fā)布后轉(zhuǎn)換成部署路徑
  • 依賴聲明的能力:聲明一個(gè)資源依賴另一個(gè)資源的能力
  • 資源嵌入的能力:把一個(gè)資源的編譯內(nèi)容嵌入到另一個(gè)文件中

以后我會(huì)在完善前端開發(fā)體系理論的時(shí)候在詳細(xì)介紹這三種語(yǔ)言能力的必要性和原子性,這里就暫時(shí)不展開說(shuō)明了。

fis最核心的編譯思想就是圍繞這三種語(yǔ)言能力設(shè)計(jì)的。

要兼顧性能的同時(shí)解決模塊化依賴管理和加載問(wèn)題,其關(guān)鍵點(diǎn)在于

不能運(yùn)行時(shí)去分析模塊間的依賴關(guān)系,而要讓框架提前知道依賴樹。

了解了原因,我們就要自己動(dòng)手設(shè)計(jì)模塊化框架了。不要害怕,模塊化框架其實(shí)很簡(jiǎn)單,思想、規(guī)范都是經(jīng)過(guò)很多前輩總結(jié)的結(jié)果,我們只要遵從他們的設(shè)計(jì)思想去實(shí)現(xiàn)就好了。

參照已有規(guī)范,我定義了三個(gè)模塊化框架接口:

  • 模塊定義接口:define(id, factory);
  • 異步加載接口:require.async(ids, callback);
  • 框架配置接口:require.config(options);

利用構(gòu)建工具建立模塊依賴關(guān)系表,再將關(guān)系表注入到代碼中,調(diào)用require.config接口讓框架知道完整的依賴樹,從而實(shí)現(xiàn)require.async在異步加載模塊時(shí)能提前預(yù)知所有依賴的資源,一次性請(qǐng)求回來(lái)。

以上面的page.js依賴樹為例,構(gòu)建工具會(huì)生成如下代碼:

require.config({
    deps : {
        'page.js' : [ 'a.js', 'b.js' ],
        'a.js'    : [ 'c.js' ],
        'b.js'    : [ 'd.js', 'e.js' ],
        'c.js'    : [ 'f.js' ],
        'd.js'    : [ 'f.js' ]
    }
});

當(dāng)執(zhí)行require.async('page.js', fn);語(yǔ)句時(shí),框架查詢config.deps表,就能知道要發(fā)起一個(gè)這樣的combo請(qǐng)求:

http://www./f.js,c.js,d.js,e.js,a.js,b.js,page.js

從而實(shí)現(xiàn)按需加載和請(qǐng)求合并兩項(xiàng)性能優(yōu)化需求。

根據(jù)這樣的設(shè)計(jì)思路,我請(qǐng) @hinc 幫忙實(shí)現(xiàn)了這個(gè)框架,我告訴他,deps里不但會(huì)有js,還會(huì)有css,所以也要兼容一下。hinc果然是執(zhí)行能力非常強(qiáng)的小伙伴,僅一個(gè)下午的時(shí)間就搞定了框架的實(shí)現(xiàn),我們給這個(gè)框架取名為 scrat.js,僅有393行。

前面提到fis具有資源依賴聲明的編譯能力。因此只要工程師按照f(shuō)is規(guī)定的書寫方式在代碼中聲明依賴關(guān)系,就能在構(gòu)建的最后階段自動(dòng)獲得fis系統(tǒng)整理好的依賴樹,然后對(duì)依賴的數(shù)據(jù)結(jié)構(gòu)進(jìn)行調(diào)整、輸出,滿足框架要求就搞定了!fis規(guī)定的資源依賴聲明方式為:在html中聲明依賴,在js中聲明依賴,在css中聲明依賴

接下來(lái),我要寫一個(gè)配置,將依賴關(guān)系表注入到代碼中。fis構(gòu)建是分流程的,具體構(gòu)建流程可以看這里。fis會(huì)在postpackager階段之前創(chuàng)建好完整的依賴樹表,我就在這個(gè)時(shí)候?qū)懸粋€(gè)插件來(lái)處理即可。

編輯fis-conf.js

//postpackager插件接受4個(gè)參數(shù),
//ret包含了所有項(xiàng)目資源以及資源表、依賴樹,其中包括:
//   ret.src: 所有項(xiàng)目文件對(duì)象
//   ret.pkg: 所有項(xiàng)目打包生成的額外文件
//   reg.map: 資源表結(jié)構(gòu)化數(shù)據(jù)
//其他參數(shù)暫時(shí)不用管
var createFrameworkConfig = function(ret, conf, settings, opt){
    //創(chuàng)建一個(gè)對(duì)象,存放處理后的配置項(xiàng)
    var map = {};
    //依賴樹數(shù)據(jù)
    map.deps = {};
    //遍歷所有項(xiàng)目文件
    fis.util.map(ret.src, function(subpath, file){
        //文件的依賴數(shù)據(jù)就在file對(duì)象的requires屬性中,直接賦值即可
        if(file.requires && file.requires.length){
            map.deps[file.id] = file.requires;
        }
    });
    console.log(map.deps);
};
//在modules.postpackager階段處理依賴樹,調(diào)用插件函數(shù)
fis.config.set('modules.postpackager', [createFrameworkConfig]);

我們準(zhǔn)備一下項(xiàng)目代碼,看看構(gòu)建的時(shí)候發(fā)生了什么:

執(zhí)行fis release查看命令行輸出,可以看到consolog.log的內(nèi)容為:

{
    deps: {
        'components/bar/bar.js': [
            'components/bar/bar.css'
        ],
        'components/foo/foo.js': [
            'components/bar/bar.js',
            'components/foo/foo.css'
        ]
    }
}

可以看到j(luò)s和同名的css自動(dòng)建立了依賴關(guān)系,這是fis默認(rèn)進(jìn)行的依賴聲明。有了這個(gè)表,我們就可以把它注入到代碼中了。我們?yōu)轫?yè)面準(zhǔn)備一個(gè)替換用的鉤子,比如約定為__FRAMEWORK_CONFIG__,這樣用戶就可以根據(jù)需要在合適的地方獲取并使用這些數(shù)據(jù)。模塊化框架的配置一般都是寫在非模塊化文件中的,比如html頁(yè)面里,所以我們應(yīng)該只針對(duì)views目錄下的文件做這樣的替換就可以。所以我們需要給views下的文件進(jìn)行一個(gè)標(biāo)記,只有views下的html或js文件才需要進(jìn)行依賴樹數(shù)據(jù)注入,具體的配置為:

fis.config.set('roadmap.path', [
    {
        reg : '**.md',
        release : false
    },
    {
        reg : /^\/component_modules\/(.*)$/i,
        release : '/public/c/$1'
    },
    {
        reg : /^\/components\/(.*)$/i,
        release : '/public/c/${name}/${version}/$1'
    },
    {
        reg : /^\/views\/(.*)$/,
        //給views目錄下的文件加一個(gè)isViews屬性標(biāo)記,用以標(biāo)記文件分類
        //我們可以在插件中拿到文件對(duì)象的這個(gè)值
        isViews : true,
        release : '/public/${name}/${version}/$1'
    },
    {
        reg : '**',
        useStandard : false,
        useOptimizer : false
    }
]);

var createFrameworkConfig = function(ret, conf, settings, opt){
    var map = {};
    map.deps = {};
    fis.util.map(ret.src, function(subpath, file){
        if(file.requires && file.requires.length){
            map.deps[file.id] = file.requires;
        }
    });
    //把配置文件序列化
    var stringify = JSON.stringify(map, null, opt.optimize ? null : 4);
    //再次遍歷文件,找到isViews標(biāo)記的文件
    //替換里面的__FRAMEWORK_CONFIG__鉤子
    fis.util.map(ret.src, function(subpath, file){
        //有isViews標(biāo)記,并且是js或者h(yuǎn)tml類文件,才需要做替換
        if(file.isViews && (file.isJsLike || file.isHtmlLike)){
            var content = file.getContent();
            //替換文件內(nèi)容
            content = content.replace(/\b__FRAMEWORK_CONFIG__\b/g, stringify);
            file.setContent(content);
        }
    });
};
fis.config.set('modules.postpackager', [createFrameworkConfig]);

//項(xiàng)目配置
fis.config.set('name', 'proj');     //將name、version獨(dú)立配置,統(tǒng)管全局
fis.config.set('version', '1.0.3');

我在views/index.html中寫了這樣的代碼:

<!doctype html>
<html>
<head>
    <title>hello</title>
</head>
<body>
    <script type="text/javascript" src="scrat.js"></script>
    <script type="text/javascript">
        require.config(__FRAMEWORK_CONFIG__);
        require.async('components/foo/foo.js', function(foo){
            //todo
        });
    </script>
</body>
</html>

執(zhí)行 fis release -d ../release 之后,得到構(gòu)建后的內(nèi)容為:

<!doctype html>
<html>
<head>
    <title>hello</title>
</head>
<body>
    <script type="text/javascript" src="/public/proj/1.0.3/scrat.js"></script>
    <script type="text/javascript">
        require.config({
            "deps": {
                "components/bar/bar.js": [
                    "components/bar/bar.css"
                ],
                "components/foo/foo.js": [
                    "components/bar/bar.js",
                    "components/foo/foo.css"
                ]
            }
        });
        require.async('components/foo/foo.js', function(foo){
            //todo
        });
    </script>
</body>
</html>

在調(diào)用 require.async('components/foo/foo.js') 之際,模塊化框架已經(jīng)知道了這個(gè)foo.js依賴于bar.js、bar.css以及foo.css,因此可以發(fā)起兩個(gè)combo請(qǐng)求去加載所有依賴的js、css文件,完成后再執(zhí)行回調(diào)。

現(xiàn)在模塊的id有一些問(wèn)題,因?yàn)槟K發(fā)布會(huì)有版本號(hào)信息,因此模塊id也應(yīng)該攜帶版本信息,從前面的依賴樹生成配置代碼中我們可以看到模塊id其實(shí)也是文件的一個(gè)屬性,因此我們可以在roadmap.path中重新為文件賦予id屬性,使其攜帶版本信息:

fis.config.set('roadmap.path', [
    {
        reg : '**.md',
        release : false,
        isHtmlLike : true
    },
    {
        reg : /^\/component_modules\/(.*)$/i,
        //追加id屬性
        id : '$1',
        release : '/public/c/$1'
    },
    {
        reg : /^\/components\/(.*)$/i,
        //追加id屬性,id為【項(xiàng)目名/版本號(hào)/文件路徑】
        id : '${name}/${version}/$1',
        release : '/public/c/${name}/${version}/$1'
    },
    {
        reg : /^\/views\/(.*)$/,
        //給views目錄下的文件加一個(gè)isViews屬性標(biāo)記,用以標(biāo)記文件分類
        //我們可以在插件中拿到文件對(duì)象的這個(gè)值
        isViews : true,
        release : '/public/${name}/${version}/$1'
    },
    {
        reg : '**',
        useStandard : false,
        useOptimizer : false
    }
]);

重新構(gòu)建項(xiàng)目,我們得到了新的結(jié)果:

<!doctype html>
<html>
<head>
    <title>hello</title>
</head>
<body>
    <img src="/public/proj/1.0.4/logo.png"/>
    <script type="text/javascript" src="/public/proj/1.0.4/scrat.js"></script>
    <script type="text/javascript">
        require.config({
            "deps": {
                "proj/1.0.4/bar/bar.js": [
                    "proj/1.0.4/bar/bar.css"
                ],
                "proj/1.0.4/foo/foo.js": [
                    "proj/1.0.4/bar/bar.js",
                    "proj/1.0.4/foo/foo.css"
                ]
            }
        });
        require.async('proj/1.0.4/foo/foo.js', function(foo){
            //todo
        });
    </script>
</body>
</html>

you see?所有id都會(huì)被修改為我們指定的模式,這就是以文件為中心的編譯系統(tǒng)的威力。

以文件對(duì)象為中心構(gòu)建系統(tǒng)應(yīng)該通過(guò)配置指定文件的各種屬性。插件并不自己實(shí)現(xiàn)某種規(guī)范規(guī)定,而是讀取file對(duì)象的對(duì)應(yīng)屬性值,這樣插件的職責(zé)單一,規(guī)范又能統(tǒng)一起來(lái)被用戶指定,為完整的前端開發(fā)體系設(shè)計(jì)奠定了堅(jiān)實(shí)規(guī)范配置的基礎(chǔ)。

接下來(lái)還有一個(gè)問(wèn)題,就是模塊名太長(zhǎng),開發(fā)中寫這么長(zhǎng)的模塊名非常麻煩。我們可以借鑒流行的模塊化框架中常用的縮短模塊名手段——?jiǎng)e名(alias)——來(lái)降低開發(fā)中模塊引用的成本。此外,目前的配置其實(shí)會(huì)針對(duì)所有文件生成依賴關(guān)系表,我們的開發(fā)概念定義只有components和component_modules目錄下的文件才是模塊化的,因此我們可以進(jìn)一步的對(duì)文件進(jìn)行分類,得到這樣配置規(guī)范:

fis.config.set('roadmap.path', [
    {
        reg : '**.md',
        release : false,
        isHtmlLike : true
    },
    {
        reg : /^\/component_modules\/(.*)$/i,
        id : '$1',
        //追加isComponentModules標(biāo)記屬性
        isComponentModules : true,
        release : '/public/c/$1'
    },
    {
        reg : /^\/components\/(.*)$/i,
        id : '${name}/${version}/$1',
        //追加isComponents標(biāo)記屬性
        isComponents : true,
        release : '/public/c/${name}/${version}/$1'
    },
    {
        reg : /^\/views\/(.*)$/,
        isViews : true,
        release : '/public/${name}/${version}/$1'
    },
    {
        reg : '**',
        useStandard : false,
        useOptimizer : false
    }
]);

然后我們?yōu)橐恍┠Kid建立別名:

var createFrameworkConfig = function(ret, conf, settings, opt){
    var map = {};
    map.deps = {};
    //別名收集表
    map.alias = {};
    fis.util.map(ret.src, function(subpath, file){
        //添加判斷,只有components和component_modules目錄下的文件才需要建立依賴樹或別名
        if(file.isComponents || file.isComponentModules){
            //判斷一下文件名和文件夾是否同名,如果同名則建立一個(gè)別名
            var match = subpath.match(/^\/components\/(.*?([^\/]+))\/\2\.js$/i);
            if(match && match[1] && !map.alias.hasOwnProperty(match[1])){
                map.alias[match[1]] = file.id;
            }
            if(file.requires && file.requires.length){
                map.deps[file.id] = file.requires;
            }
        }
    });
    var stringify = JSON.stringify(map, null, opt.optimize ? null : 4);
    fis.util.map(ret.src, function(subpath, file){
        if(file.isViews && (file.isJsLike || file.isHtmlLike)){
            var content = file.getContent();
            content = content.replace(/\b__FRAMEWORK_CONFIG__\b/g, stringify);
            file.setContent(content);
        }
    });
};
fis.config.set('modules.postpackager', [createFrameworkConfig]);

再次構(gòu)建,在注入的代碼中就能看到alias字段了:

require.config({
    "deps": {
        "proj/1.0.5/bar/bar.js": [
            "proj/1.0.5/bar/bar.css"
        ],
        "proj/1.0.5/foo/foo.js": [
            "proj/1.0.5/bar/bar.js",
            "proj/1.0.5/foo/foo.css"
        ]
    },
    "alias": {
        "bar": "proj/1.0.5/bar/bar.js",
        "foo": "proj/1.0.5/foo/foo.js"
    }
});

這樣,代碼中的 require('foo'); 就等價(jià)于 require('proj/1.0.5/foo/foo.js');了。

還剩最后一個(gè)小小的需求,就是希望能像寫nodejs一樣開發(fā)js模塊,也就是要求實(shí)現(xiàn)define的自動(dòng)包裹功能,這個(gè)可以通過(guò)文件編譯的 postprocessor 插件完成。配置為:

//在postprocessor對(duì)所有js后綴的文件進(jìn)行內(nèi)容處理:
fis.config.set('modules.postprocessor.js', function(content, file){
    //只對(duì)模塊化js文件進(jìn)行包裝
    if(file.isComponents || file.isComponentModules){
        content = 'define("' + file.id + 
                  '", function(require,exports,module){' +
                  content + '});';
    }
    return content;
});

所有在components目錄和component_modules目錄下的js文件都會(huì)被包裹define,并自動(dòng)根據(jù)roadmap.path中的id配置進(jìn)行模塊定義了。

最煎熬的一天終于過(guò)去了,睡一覺(jué),擁抱一下周末。

2014年02月15日 - 超晴

周末的天氣非常好哇,一覺(jué)睡到中午才起,這么好的天氣寫碼豈不是很loser?!

2014年02月16日 - 小雨

居然浪費(fèi)了一天,剩下的時(shí)間不多了,今天要抓緊?。。?!

讓我們來(lái)回顧一下已經(jīng)完成了哪些工作:

  • 規(guī)范
    • 開發(fā)規(guī)范
      • 模塊化開發(fā),js模塊化,css模塊化,像nodejs一樣的模塊化開發(fā)
      • 組件化開發(fā),js、css、handlebars維護(hù)在一起
    • 部署規(guī)范
      • 采用nodejs后端,基本部署規(guī)范應(yīng)該參考 express 項(xiàng)目部署
      • 按版本號(hào)做非覆蓋式發(fā)布
      • 公共模塊可發(fā)布給第三方共享
  • 框架
    • js模塊化框架,支持請(qǐng)求合并,按需加載等性能優(yōu)化點(diǎn)
  • 工具
    • 可以編譯stylus為css
    • 支持js、css、圖片壓縮
    • 允許圖片壓縮后以base64編碼形式嵌入到css、js或html中
    • 與ci平臺(tái)集成
    • 文件監(jiān)聽、瀏覽器自動(dòng)刷新
    • 本地預(yù)覽、數(shù)據(jù)模擬
  • 倉(cāng)庫(kù)
    • 支持component模塊安裝和使用

剩下的幾個(gè)需求中有些是fis默認(rèn)支持的,比如base64內(nèi)嵌功能,圖片會(huì)先經(jīng)過(guò)編譯流程,得到壓縮后的內(nèi)容fis再對(duì)其進(jìn)行base64化的內(nèi)嵌處理。由于fis的內(nèi)嵌功能支持任意文件的內(nèi)嵌,所以,這個(gè)語(yǔ)言能力擴(kuò)展可以同時(shí)解決前端模板和圖片base64內(nèi)嵌需求,比如我們有這樣的代碼:

project
  - components
    - foo
      - foo.js
      - foo.css
      - foo.handlebars
      - foo.png

無(wú)需配置,既可以在js中嵌入資源,比如 foo.js 中可以這樣寫:

//依賴聲明
var bar =  require('../bar/bar.js');
//把handlebars文件的字符串形式嵌入到j(luò)s中
var text = __inline('foo.handlebars');
var tpl = Handlebars.compile(text);
exports.render = function(data){
    return tpl(data);
};

//把圖片的base64嵌入到j(luò)s中
var data = __inline('foo.png');
exports.getImage = function(){
    var img = new Image();
    img.src = data;
    return img;
};

編譯后得到:

define("proj/1.0.5/foo/foo.js", function(require,exports,module){
//依賴聲明
var bar =  require('proj/1.0.5/bar/bar.js');
//把handlebars文件的字符串形式嵌入到j(luò)s中
var text = "<h1>{{title}}</h1>";
var tpl = Handlebars.compile(text);
exports.render = function(data){
    return tpl(data);
};

//把圖片的base64嵌入到j(luò)s中
var data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoA...';
exports.getImage = function(){
    var img = new Image();
    img.src = data;
    return img;
};

});

支持stylus也非常簡(jiǎn)單,fis在 parser 階段處理非標(biāo)準(zhǔn)語(yǔ)言,這個(gè)階段可以把非標(biāo)準(zhǔn)的js(coffee/前端模板)、css(less/sass/stylus)、html(markdown)語(yǔ)言轉(zhuǎn)換為標(biāo)準(zhǔn)的js、css或html。處理之后那些文件還能和標(biāo)準(zhǔn)語(yǔ)言一起經(jīng)歷預(yù)處理、語(yǔ)言能力擴(kuò)展、后處理、校驗(yàn)、測(cè)試、壓縮等階段。

所以,要支持stylus的編譯,只要在fis-conf.js中添加這樣的配置即可:

//依賴開源的stylus包
var stylus = require('stylus');
//編譯插件只負(fù)責(zé)處理文件內(nèi)容
var stylusParser = function(content, file, conf){
    return stylus(content, conf).render();
};
//配置編譯流程,styl后綴的文件經(jīng)過(guò)編譯插件函數(shù)處理
fis.config.set('modules.parser.styl', stylusParser);
//告訴fis,styl后綴的文件,被當(dāng)做css處理,編譯后后綴也是css
fis.config.set('roadmap.ext.styl', 'css');

這樣我們項(xiàng)目中的*.styl后綴的文件都會(huì)被編譯為css內(nèi)容,并且會(huì)在后面的流程中被當(dāng)做css內(nèi)容處理,比如壓縮、csssprite等。

文件監(jiān)聽、自動(dòng)刷新都是fis內(nèi)置的功能,fis的release命令集合了所有編譯所需的參數(shù),

  fis release -h

  Usage: release [options]

  Options:

    -h, --help             output usage information
    -d, --dest <names>     release output destination
    -m, --md5 [level]      md5 release option
    -D, --domains          add domain name
    -l, --lint             with lint
    -t, --test             with unit testing
    -o, --optimize         with optimizing
    -p, --pack             with package
    -w, --watch            monitor the changes of project
    -L, --live             automatically reload your browser
    -c, --clean            clean compile cache
    -r, --root <path>      set project root
    -f, --file <filename>  set fis-conf file
    -u, --unique           use unique compile caching
    --verbose              enable verbose output

這些參數(shù)是可以隨意組合的,比如我們想文件監(jiān)聽、自動(dòng)刷新,則使用:

fis release -wL

壓縮、打包、文件監(jiān)聽、自動(dòng)刷新、發(fā)布到output目錄,則使用:

fis release -opwLd output

構(gòu)建工具不需要那么多命令,或者develop、release等不同狀態(tài)的配置文件,應(yīng)該從命令行切換編譯參數(shù),從而實(shí)現(xiàn)開發(fā)/上線構(gòu)建模式的切換。

另外,fis是命令行工具,各種內(nèi)置的插件也是完全獨(dú)立無(wú)環(huán)境依賴的,可以與ci平臺(tái)直接對(duì)接,并在各個(gè)主流操作系統(tǒng)下運(yùn)行正常。

利用fis的內(nèi)置的各種編譯功能,我們離目標(biāo)又近了許多:

  • 規(guī)范
    • 開發(fā)規(guī)范
      • 模塊化開發(fā),js模塊化,css模塊化,像nodejs一樣的模塊化開發(fā)
      • 組件化開發(fā),js、css、handlebars維護(hù)在一起
    • 部署規(guī)范
      • 采用nodejs后端,基本部署規(guī)范應(yīng)該參考express項(xiàng)目部署
      • 按版本號(hào)做非覆蓋式發(fā)布
      • 公共模塊可發(fā)布給第三方共享
  • 框架
    • js模塊化框架,支持請(qǐng)求合并,按需加載等性能優(yōu)化點(diǎn)
  • 工具
    • 可以編譯stylus為css
    • 支持js、css、圖片壓縮
    • 允許圖片壓縮后以base64編碼形式嵌入到css、js或html中
    • 與ci平臺(tái)集成
    • 文件監(jiān)聽、瀏覽器自動(dòng)刷新
    • 本地預(yù)覽、數(shù)據(jù)模擬
  • 倉(cāng)庫(kù)
    • 支持component模塊安裝和使用

剩下兩個(gè),我們可以通過(guò)擴(kuò)展fis的命令行插件來(lái)實(shí)現(xiàn)。fis有11個(gè)編譯流程擴(kuò)展點(diǎn),還有一個(gè)命令行擴(kuò)展點(diǎn)。要擴(kuò)展命令行插件很簡(jiǎn)單,只要我們將插件安裝到與fis同級(jí)的node_modules目錄下即可。比如:

node_modules
  - fis
  - fis-command-say

那么執(zhí)行 fis say 這個(gè)命令,就能調(diào)用到那個(gè)fis-command-say插件了。剩下的這個(gè)component模塊安裝,我就利用了這個(gè)擴(kuò)展點(diǎn),結(jié)合component開源的 component-installer 包,我就可以把component整合當(dāng)前開發(fā)體系中,這里我們需要?jiǎng)?chuàng)建一個(gè)npm包來(lái)提供擴(kuò)展,而不能直接在fis-conf.js中擴(kuò)展命令行,插件代碼我就不貼了,可以看 這里。

眼前我們有了一個(gè)差不多100行的fis-conf.js文件,還有幾個(gè)插件,如果我把這樣一個(gè)零散的系統(tǒng)交付團(tuán)隊(duì)使用,那么大家使用的步驟差不多是這樣的:

  1. 安裝fis,npm install -g fis
  2. 安裝component安裝用的命令行插件,npm insatll -g fis-command-component
  3. 安裝stylus編譯插件,npm install -g fis-parser-stylus
  4. 下載一份配置文件,fis-conf.js,修改里面的name、version配置

這種情況讓團(tuán)隊(duì)用起來(lái)會(huì)有很多問(wèn)題。首先,安裝過(guò)程太過(guò)麻煩,其次如果項(xiàng)目多,那么fis-conf.js不能同步升級(jí),這是非常嚴(yán)重的問(wèn)題。grunt的gruntfile.js也是如此。如果說(shuō)有一個(gè)項(xiàng)目用了某套grunt配置感覺(jué)很爽,那么下個(gè)項(xiàng)目也想用這套方案,復(fù)制gruntfile.js是必須的操作,項(xiàng)目用的多了,同步gruntfile的成本就變得非常高了。

因此,fis提供了一種“包裝”的能力,它允許你將fis作為內(nèi)核,包裝出一個(gè)新的命令行工具,這個(gè)工具內(nèi)置了一些fis的配置,并且把所有命令行調(diào)用的參數(shù)傳遞給fis內(nèi)核去處理。

我準(zhǔn)備把這套系統(tǒng)打包為一個(gè)新的工具,給它取名為 scrat,也是一只松鼠。這個(gè)新工具的目錄結(jié)構(gòu)是這樣的:

scrat
  - bin
    - scrat
  - node_modules
    - fis
    - fis-parser-handlebars
    - fis-lint-jshint
    - scrat-command-install
    - scrat-command-server
    - scrat-parser-stylus
  - index.js
  - package.json

其中,index.js的內(nèi)容為:

//require一下fis模塊
var fis = module.exports = require('fis');

//聲明命令行工具名稱
fis.cli.name = 'scrat';

//定義插件前綴,允許加載scrat-xxx-xxx插件,或者fis-xxx-xxx插件,
//這樣可以形成scrat自己的插件系統(tǒng)
fis.require.prefixes = [ 'scrat', 'fis' ];

//把前面的配置都寫在這里統(tǒng)一管理
//項(xiàng)目中就不用再寫了
fis.config.merge({...});

將這個(gè)npm包發(fā)布出來(lái),我們就有了一個(gè)全新的開發(fā)工具,這個(gè)工具可以解決前面說(shuō)的13項(xiàng)技術(shù)問(wèn)題,并提供一套完整的集成解決方案,而你的團(tuán)隊(duì)使用的時(shí)候,只有兩個(gè)步驟:

  1. 安裝這個(gè)工具,npm install -g scrat
  2. 項(xiàng)目配置只有兩項(xiàng),name和version

使用新工具的命令、參數(shù)幾乎和fis完全一樣:

scrat release [options]
scrat server start
scrat install <name@version> [options]

而scrat這個(gè)工具所內(nèi)置的配置將變成規(guī)范文檔描述給團(tuán)隊(duì)同學(xué),這套系統(tǒng)要比grunt那種松散的構(gòu)建系統(tǒng)組成方式更容易被多個(gè)團(tuán)隊(duì)、多個(gè)項(xiàng)目同時(shí)共享。

熬了一個(gè)通宵,基本算是完成了。。。

2014年02月17日 - 多云

終于到了周一,交付了一個(gè)新的開發(fā)工具——scrat,及其使用 文檔。

然而,過(guò)去的三天,為了構(gòu)造這套前端開發(fā)體系,都寫了哪些代碼呢?

  • 基于fis的一套規(guī)范及插件配置,274行;
  • scrat install命令行插件,用于安裝component模塊,74行;
  • scrat server命令行插件,用于啟動(dòng)nodejs的服務(wù)器,203行
  • 編譯stylus的插件,10行
  • 編譯handlebars的插件,6行
  • 一個(gè)模塊化框架 scrat.js,393行

一共 960行 代碼,用了4人/天。

總結(jié)

不可否認(rèn),為大規(guī)模前端團(tuán)隊(duì)設(shè)計(jì)集成解決方案需要花費(fèi)非常多的心思。

如果說(shuō)只是實(shí)現(xiàn)一個(gè)簡(jiǎn)單的編譯+壓縮+文件監(jiān)+聽自動(dòng)刷新的常規(guī)構(gòu)建系統(tǒng),基于fis應(yīng)該不超過(guò)1小時(shí)就能完成一個(gè),但要實(shí)踐完整的前端集成解決方案,確實(shí)需要點(diǎn)時(shí)間。

如之前一篇 文章 所講,前端集成解決方案有8項(xiàng)技術(shù)要素,除了組件倉(cāng)庫(kù),其他7項(xiàng)對(duì)于企業(yè)級(jí)前端團(tuán)隊(duì)來(lái)說(shuō),應(yīng)該都需要完整實(shí)現(xiàn)的。即便暫時(shí)不想實(shí)現(xiàn),也會(huì)隨著業(yè)務(wù)發(fā)展而被迫慢慢完善,這個(gè)完善過(guò)程是普適的。

對(duì)于前端集成解決方案的實(shí)踐,可以總結(jié)出這些設(shè)計(jì)步驟:

  1. 設(shè)計(jì)開發(fā)概念,定義開發(fā)資源的分類(模塊化/非模塊化)
  2. 設(shè)計(jì)開發(fā)目錄,降低開發(fā)、維護(hù)成本(開發(fā)規(guī)范)
  3. 根據(jù)運(yùn)維和業(yè)務(wù)要求,設(shè)計(jì)部署規(guī)范(部署規(guī)范)
  4. 設(shè)計(jì)工具,完成開發(fā)目錄和部署目錄的轉(zhuǎn)換(開發(fā)-部署轉(zhuǎn)換)
  5. 設(shè)計(jì)模塊化框架,兼顧性能優(yōu)化(開發(fā)框架)
  6. 擴(kuò)展工具,支持開發(fā)框架的構(gòu)建需求(框架構(gòu)建需求)
  7. 流程整合(開發(fā)、測(cè)試、聯(lián)調(diào)、上線等流程接入)

我們可以看看業(yè)界已有團(tuán)隊(duì)提出的各種解決方案,無(wú)不以這種思路來(lái)設(shè)計(jì)和發(fā)展的:

  • seajs開發(fā)體系,支付寶團(tuán)隊(duì)前端開發(fā)體系,以 spm 為構(gòu)建和包管理工具
  • fis-plus,百度絕大多數(shù)前端團(tuán)隊(duì)使用的開發(fā)體系,以fis為構(gòu)建工具內(nèi)核,以lights為包管理工具
  • edp,百度ecomfe前端開發(fā)體系,以 edp 為構(gòu)建和包管理工具
  • modjs,騰訊AlloyTeam團(tuán)隊(duì)出品的開發(fā)體系
  • yeoman,google出品的解決方案,以grunt為構(gòu)建工具,bower為包管理工具

縱觀這些公司出品的前端集成解決方案,深入剖析其中的框架、規(guī)范、工具和流程,都可以發(fā)現(xiàn)一些共通的影子,設(shè)計(jì)思想殊途同歸,不約而同的朝著一種方向前進(jìn),那就是前端集成解決方案。嘗試將前端工程孤立的技術(shù)要素整合起來(lái),解決常見的領(lǐng)域問(wèn)題。

或許有人會(huì)問(wèn),不就是寫頁(yè)面么,用得著這么復(fù)雜?

在這里我不能給出肯定或者否定的答復(fù)。

因?yàn)閱渭儚恼Z(yǔ)言的角度來(lái)說(shuō),html、js、css(甚至有人認(rèn)為css是數(shù)據(jù)結(jié)構(gòu),而非語(yǔ)言)確實(shí)是最簡(jiǎn)單最容易上手的開發(fā)語(yǔ)言,不用模塊化、不用工具、不用壓縮,任何人都可以快速上手,完成一兩個(gè)功能簡(jiǎn)單的頁(yè)面。所以說(shuō),在一般情況下,前端開發(fā)非常簡(jiǎn)單。

在規(guī)模很小的項(xiàng)目中,前端技術(shù)要素彼此不會(huì)直接產(chǎn)生影響,因此無(wú)需集成解決方案。

但正是由于前端語(yǔ)言這種靈活松散的特點(diǎn),使得前端項(xiàng)目規(guī)模在達(dá)到一定規(guī)模后,工程問(wèn)題凸顯,成為發(fā)展瓶頸,各種技術(shù)要素彼此之間開始出現(xiàn)關(guān)聯(lián),要用模塊化開發(fā),就必須對(duì)應(yīng)某個(gè)模塊化框架,用這個(gè)框架就必須對(duì)應(yīng)某個(gè)構(gòu)建工具,要用這個(gè)工具,就必須對(duì)應(yīng)某個(gè)包管理工具……這個(gè)時(shí)候,完整實(shí)踐前端集成解決方案就成了不二選擇。

當(dāng)前端項(xiàng)目達(dá)到一定規(guī)模后,工程問(wèn)題將成為主要瓶頸,原來(lái)孤立的技術(shù)要素開始彼此產(chǎn)生影響,需要有人從比較高的角度去梳理、尋找適合自己團(tuán)隊(duì)的集成解決方案。

所以會(huì)出現(xiàn)一些框架或工具在小項(xiàng)目中使用的好好的,一旦放到團(tuán)隊(duì)里使用就非常困難的情況。

前端入門雖易工程不易,且行寫珍惜!

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多