InfoQ編輯注:本文來自前端工程師張云龍的博客,由作者本人推薦至InfoQ進行分享。目前本系列已經(jīng)發(fā)布了三個部分,本處分享的是第二部分,前端開發(fā)體系建設(shè)日記。建議在閱讀本文前先閱讀本文作者和其團隊之前分享的《前端工程精粹》系列一、二、三。
上周寫了一篇 文章 介紹前端集成解決方案的基本理論,很多同學看過之后大呼不過癮。
干貨
fuck things在哪里!
本打算繼續(xù)完善理論鏈,形成前端工程的知識結(jié)構(gòu)。但鑒于如今的快餐文化,po主決定還是先寫一篇實戰(zhàn)介紹,讓大家看到前端工程體系能為團隊帶來哪些好處,調(diào)起大家的胃口再說。
ps: 寫完才發(fā)現(xiàn)這篇文章真的非常非常長,涵蓋了前端開發(fā)中的很多方面,希望大家能有耐心看完,相信一定會有所斬獲。。。
新到松鼠團隊的第二天,小伙伴 @nino 找到我說
nino: 視頻項目打算重新梳理一下,希望能引入新的技術(shù)體系,解決現(xiàn)有的一些問題。
po主不禁暗喜,好機會,這是我專業(yè)啊,藍翔技校-前端集成解決方案學院-自動化系-打包學專業(yè)的文憑不是白給的,于是自信滿滿的對nino說,有什么需求盡管提!
nino: 我的需求并不多,就這么幾條~~
我倒吸一口涼氣,但表面故作鎮(zhèn)定的說:恩,確實不多,讓我們先來看看第一個需求。。。
還沒等我說完,nino打斷我說
nino: 橋豆麻袋(稍等),還有一個最重要的需求!
松鼠公司的松鼠瀏覽器你知道吧,恩,它有很多個版本的樣子。我希望代碼發(fā)布后能按照版本部署,不要彼此覆蓋。舉個例子,代碼部署結(jié)構(gòu)可能是這樣的: release/ - public/ - 項目名 - 1.0.0/ - 1.0.1/ - 1.0.2/ - 1.0.2-alpha/ - 1.0.2-beta/讓歷史瀏覽器瀏覽歷史版本,沒事還能做個灰度發(fā)布,ABTest啥的,多好!此外,我們將來會有多個項目使用這套開發(fā)模式,希望能共用一些組件或者模塊,產(chǎn)品也會公布一些api模塊給第三方使用,所以共享模塊功能也要加上。
總的來說,還要追加兩個部署需求:
nino: 怎么樣,不算復(fù)雜吧,這個項目很趕,3天搞定怎么樣?
我凝望著會議室白板上的這些需求,正打算爭辯什么,一扭頭發(fā)現(xiàn)nino已經(jīng)不見了。。。正在沮喪之際,小伙伴 @hinc 過來找我,跟他大概講了一下nino的需求,正想跟他抱怨工期問題時,hinc卻說
hinc: 恩,這正是我們需要的開發(fā)體系,不過我這里還有一個需求。。。
3天時間,13項前端技術(shù)元素,靠譜么。。。
一覺醒來,輕松了許多,但還有任務(wù)在身,不敢有半點怠慢。整理一下昨天的需求,我們來做一個簡單的劃分。
這樣一套規(guī)范、框架、工具和倉庫的開發(fā)體系,服從我之前介紹的 前端集成解決方案 的描述。前端界每天都團隊在設(shè)計和實現(xiàn)這類系統(tǒng),它們其實是有規(guī)律可循的。百度出品的 fis 就是一個能幫助快速搭建前端集成解決方案的工具。使用fis我應(yīng)該可以在3天之內(nèi)完成這些任務(wù)。
ps: 這不是一篇關(guān)于fis的軟文,如果這樣的一套系統(tǒng)基于grunt實現(xiàn)相信會有非常大量的開發(fā)工作,3天完成幾乎是不可能的任務(wù)。
不幸的是,現(xiàn)在fis官網(wǎng)所介紹的 并不是 fis,而是一個叫 fis-plus 的項目,該項目并不像字面理解的那樣是fis的加強版,而是在fis的基礎(chǔ)上定制的一套面向百度前端團隊的解決方案,以php為后端語言,跟smarty有較強的綁定關(guān)系,有著 19項 技術(shù)要素,密切配合百度現(xiàn)行技術(shù)選型。絕大多數(shù)非百度前端團隊都很難完整接受這19項技術(shù)選型,尤其是其中的部署、框架規(guī)范,跟百度前端團隊相關(guān)開發(fā)規(guī)范、部署規(guī)范、以及php、smarty等有著較深的綁定關(guān)系。
因此如果你的團隊用的不是 php后端 && smarty模板 && modjs模塊化框架 && bingo框架 的話,請查看fis的文檔,或許不會有那么多困惑。
ps: fis是一個構(gòu)建系統(tǒng)內(nèi)核,很好的抽象了前端集成解決方案所需的通用工具需求,本身不與任何后端語言綁定。而基于fis實現(xiàn)的具體解決方案就會有具體的規(guī)范和技術(shù)選型了。
言歸正傳,讓我們基于 fis 開始實踐這套開發(fā)體系吧!
前端開發(fā)體系設(shè)計第一步要定義開發(fā)概念。開發(fā)概念是指針對開發(fā)資源的分類概念。開發(fā)概念的確立,直接影響到規(guī)范的定制。比如,傳統(tǒng)的開發(fā)概念一般是按照文件類型劃分的,所以傳統(tǒng)前端項目會有這樣的目錄結(jié)構(gòu):
這樣確實很直接,任何智力健全的人都知道每個文件該放在哪里。但是這樣的開發(fā)概念劃分將給項目帶來較高的維護成本,并為項目臃腫埋下了工程隱患,理由是:
ps: 除非你的團隊只有1-2個人,你的項目只有很少的代碼量,而且不用關(guān)心性能和未來的維護問題,否則,以文件為依據(jù)設(shè)計的開發(fā)概念是應(yīng)該被拋棄的。
以我個人的經(jīng)驗,更傾向于具有一定語義的開發(fā)概念。綜合前面的需求,我為這個開發(fā)體系確定了3個開發(fā)資源概念:
ps: 開發(fā)概念越簡單越好,前面提到的fis-plus也有類似的開發(fā)概念,有組件或模塊(widget),頁面(page),測試數(shù)據(jù)(test),非模塊化靜態(tài)資源(static)。有的團隊在模塊之中又劃分出api模塊和ui模塊(組件)兩種概念。
基于開發(fā)概念的確立,接下來就要確定目錄規(guī)范了。我通常會給每種開發(fā)資源的目錄取一個有語義的名字,三種資源我們可以按照概念直接定義目錄結(jié)構(gòu)為:
project - modules 存放模塊化資源 - pages 存放頁面資源 - static 存放非模塊化資源
這樣劃分目錄確實直觀,但結(jié)合前面hinc說過的,希望能使用component倉庫資源,因此我決定將模塊化資源目錄命名為components,得到:
project - components 存放模塊化資源 - pages 存放頁面資源 - static 存放非模塊化資源
而nino又提到過模塊資源分為項目模塊和公共模塊,以及hinc提到過希望能從component安裝一些公共組件到項目中,因此,一個components目錄還不夠,想到nodejs用node_modules作為模塊安裝目錄,因此我在規(guī)范中又追加了一個 component_modules 目錄,得到:
project - component_modules 存放外部模塊資源 - components 存放項目模塊資源 - pages 存放頁面資源 - static 存放非模塊化資源
nino說過今后大多數(shù)項目采用nodejs作為后端,express是比較常用的nodejs的server框架,express項目通常會把后端模板放到 views 目錄下,把靜態(tài)資源放到 public 下。為了迎合這樣的需求,我將page、static兩個目錄調(diào)整為 views 和 public,規(guī)范又修改為:
project - component_modules 存放外部模塊資源 - components 存放項目模塊資源 - views 存放頁面資源 - public 存放非模塊化資源
考慮到頁面也是一種靜態(tài)資源,而public這個名字不具有語義性,與其他目錄都有概念沖突,不如將其與views目錄合并,views目錄負責存放頁面和非模塊化資源比較合適,因此最終得到的開發(fā)目錄結(jié)構(gòu)為:
project - component_modules 存放外部模塊資源 - components 存放項目模塊資源 - views 存放頁面以及非模塊化資源
托nino的福,咱們的部署策略將會非常復(fù)雜,根據(jù)要求,一個完整的部署結(jié)果應(yīng)該是這樣的目錄結(jié)構(gòu):
release - public - 項目名 - 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 - 項目名 - 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下只有項目名的部署還不夠,應(yīng)改把模塊化文件單獨發(fā)布出來,得到這樣的部署結(jié)構(gòu):
release - public - component_modules 模塊化資源都部署到這個目錄下 - module_a - 1.0.0 - module_a.js - module_a.css - module_a.png - 1.0.1 - 1.0.2 ... - 項目名 - 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 - 項目名 - 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 這個名字太長了,如果部署到這樣的路徑下,url會很長,這也是一個優(yōu)化點,因此最終決定部署結(jié)構(gòu)為:
release - public - c 模塊化資源都部署到這個目錄下 - 公共模塊 - 版本號 - 項目名 - 版本號 - 項目名 - 版本號 非模塊化資源都部署到這個目錄下 - views - 項目名 - 版本號 后端模板都構(gòu)建到這個目錄下
插一句,并不是所有團隊都會有這么復(fù)雜的部署要求,這和松鼠團隊的業(yè)務(wù)需求有關(guān),但我相信這個例子也不會是最復(fù)雜的。每個團隊都會有自己的運維需求,前端資源部署經(jīng)常牽連到公司技術(shù)架構(gòu),因此很多前端項目的開發(fā)目錄結(jié)構(gòu)會和部署要求保持一致。這也為項目間模塊的復(fù)用帶來了成本,因為代碼中寫的url通常是部署后的路徑,遷移之后就可能失效了。
解耦開發(fā)規(guī)范和部署規(guī)范是前端開發(fā)體系的設(shè)計重點。
好了,去吃個午飯,下午繼續(xù)。。。
我準備了一個樣例項目:
project - views - logo.png - index.html - fis-conf.js - README.md
fis-conf.js是fis工具的配置文件,接下來我們就要在這里進行構(gòu)建配置了。雖然開發(fā)規(guī)范和部署規(guī)范十分復(fù)雜,但好在fis有一個非常強大的 roadmap.path 功能,專門用于分類文件、調(diào)整發(fā)布結(jié)構(gòu)、指定文件的各種屬性等功能實現(xiàn)。
所謂構(gòu)建,其核心任務(wù)就是將文件按照某種規(guī)則進行分類(以文件后綴分類,以模塊化/非模塊化分類,以前端/后端代碼分類),然后針對不同的文件做不同的構(gòu)建處理。
閑話少說,我們先來看一下基本的配置,在 fis-conf.js 中添加代碼:
fis.config.set('roadmap.path', [ { reg : '**.md', //所有md后綴的文件 release : false //不發(fā)布 }]);
以上配置,使得項目中的所有md后綴文件都不會發(fā)布出來。release是定義file對象發(fā)布路徑的屬性,如果file對象的release屬性為false,那么在項目發(fā)布階段就不會被輸出出來。
在fis中,roadmap.pah是一個數(shù)組數(shù)據(jù),數(shù)組每個元素是一個對象,必須定義 reg 屬性,用以匹配項目文件路徑從而進行分類劃分,reg屬性的取值可以是路徑通配字符串或者正則表達式。fis有一個內(nèi)部的文件系統(tǒng),會給每個源碼文件創(chuàng)建一個 fis.File 對象,創(chuàng)建File對象時,按照roadmap.path的配置逐個匹配文件路徑,匹配成功則把除reg之外的其他屬性賦給File對象,fis中各種處理環(huán)節(jié)及插件都會讀取所需的文件對象的屬性值,而不會自己定義規(guī)范。有關(guān)roadmap.path的工作原理可以看這里 以及 roadmap.path數(shù)組的第二元素據(jù)采用正則作為匹配規(guī)則,正則可以幫我們捕獲到分組信息,在release屬性值中引用分組是非常方便的。正則匹配 + 捕獲分組,成為目錄規(guī)范配置的強有力工具:
在上面的配置中,版本號被寫到了匹配規(guī)則里,這樣非常不方便工程師在迭代的過程中升級項目版本。我們應(yīng)該將版本號、項目名稱等配置獨立出來管理。好在roadmap.path還有讀取其他配置的能力,修改上面的配置,我們得到:
//開發(fā)部署規(guī)范配置fis.config.set('roadmap.path', [ { reg : '**.md', //所有md后綴的文件 release : false //不發(fā)布 }, { reg : /^\/views\/(.*)$/i, //使用${xxx}引用fis.config的其他配置項 release : '/public/${name}/${version}/$1' }]);//項目配置,將name、version獨立配置,統(tǒng)管全局fis.config.set('name', 'proj');fis.config.set('version', '1.0.0');
fis的配置系統(tǒng)非常靈活,除了 文檔 中提到的配置節(jié)點,其他配置用戶可以隨便定義使用。比如配置的roadmap是系統(tǒng)保留的,而name、version都是用戶自己隨便指定的。fis系統(tǒng)保留的配置節(jié)點只有6個,包括:
完成第一份配置之后,我們來看一下效果。
cd projectfis release --dest ../release
進入到項目目錄,然后使用fis release命令,對項目進行構(gòu)建,用 --dest <path> 參數(shù)指定編譯結(jié)果的產(chǎn)出路徑,可以看到部署后的結(jié)果:
ps: fis release會將處理后的結(jié)果發(fā)布到源碼目錄之外的其他目錄里,以保持源碼目錄的干凈。
fis系統(tǒng)的強大之處在于當你調(diào)整了部署規(guī)范之后,fis會識別所有資源定位標記,將他們修改為對應(yīng)的部署路徑。
fis的文件系統(tǒng)設(shè)計決定了配置開發(fā)規(guī)范的成本非常低。fis構(gòu)建核心有三個超級正則,用于識別資源定位標記,把用戶的開發(fā)規(guī)范和部署規(guī)范通過配置完整連接起來,具體實現(xiàn)可以看這里。
不止html,fis為前端三種領(lǐng)域語言都準備了資源定位識別標記,更多文檔可以看這里:在html中定位資源,在js中定位資源,
再次執(zhí)行:
cd projectfis release --dest ../release得到:
至此,我們已經(jīng)基本解決了開發(fā)和部署直接的目錄規(guī)范問題,這里我需要加快一些步伐,把其他目錄的部署規(guī)范也配置好,得到一個相對比較完整的結(jié)果:
fis.config.set('roadmap.path', [ { //md后綴的文件不發(fā)布 reg : '**.md', release : false }, { //component_modules目錄下的代碼,由于component規(guī)范,已經(jīng)有了版本號 //我將它們直接發(fā)送到public/c目錄下就好了 reg : /^\/component_modules\/(.*)$/i, release : '/public/c/$1' }, { //項目模塊化目錄沒有版本號結(jié)構(gòu),用全局版本號控制發(fā)布結(jié)構(gòu) reg : /^\/components\/(.*)$/i, release : '/public/c/${name}/${version}/$1' }, { //views目錄下的文件發(fā)布到【public/項目名/版本】目錄下 reg : /^\/views\/(.*)$/, release : '/public/${name}/${version}/$1' }, { //其他文件就不屬于前端項目了,比如nodejs的后端代碼 //不處理這些文件的資源定位替換(useStandard: false) //也不用對這些資源進行壓縮(useOptimizer: false) reg : '**', useStandard : false, useOptimizer : false }]);fis.config.set('name', 'proj');fis.config.set('version', '1.0.2');我構(gòu)造了一個相對完整的目錄結(jié)構(gòu),然后進行了一次構(gòu)建,效果還不錯:
不管部署規(guī)則多么復(fù)雜都不用擔心,有fis強大的資源定位系統(tǒng)幫你在開發(fā)規(guī)范和部署規(guī)范之間建立聯(lián)系,設(shè)計開發(fā)體系不在受制于工具的實現(xiàn)能力。
你可以盡情發(fā)揮想象力,設(shè)計出最優(yōu)雅最合理的開發(fā)規(guī)范和最高效最貼合公司運維要求的部署規(guī)范,最終用fis的roadmap.path功能將它們連接起來,實現(xiàn)完美轉(zhuǎn)換。
fis的roadmap功能實際上提供了項目代碼與部署規(guī)范解耦的能力。
從前面的例子可以看出,開發(fā)使用相對路徑即可,fis會在構(gòu)建時會根據(jù)fis-conf.js中的配置完成開發(fā)路徑到部署路徑的轉(zhuǎn)換工作。這意味著在fis體系下開發(fā)的模塊將具有天然的可移植性,既能滿足不同項目的不同部署需求,又能允許開發(fā)中使用相對路徑進行資源定位,工程師再不用把部署路徑寫到代碼中了。
愉快的一天就這么過去了,睡覺!
2014年02月14日 - 陰轉(zhuǎn)多云
每到周五總是非常愜意的感覺,不管這一周多么辛苦,周五就是一個解脫,更何況今天還是個特別的日子——情人節(jié)!
昨天主要解決了開發(fā)概念、開發(fā)目錄規(guī)范、部署目錄規(guī)范以及初步的fis-conf.js配置。今天要進行前端開發(fā)體系設(shè)計的關(guān)鍵任務(wù)——模塊化框架。
模塊化框架是前端開發(fā)體系中最為核心的環(huán)節(jié)。
模塊化框架肩負著模塊管理、資源加載、性能優(yōu)化(按需,請求合并)等多種重要職責,同時它也是組件開發(fā)的基礎(chǔ)框架,因此模塊化框架設(shè)計的好壞直接影響到開發(fā)體系的設(shè)計質(zhì)量。
很遺憾的說,現(xiàn)在市面上已有的模塊化框架都沒能很好的處理模塊管理、資源加載和性能優(yōu)化三者之間的關(guān)系。這倒不是框架設(shè)計的問題,而是由前端領(lǐng)域語言特殊性決定的??蚣茉O(shè)計者一般在思考模塊化框架時,通常站在純前端運行環(huán)境角度考慮,基本功能都是用原生js實現(xiàn)的,因此一個模塊化開發(fā)的關(guān)鍵問題不能被很好的解決。這個關(guān)鍵問題就是依賴聲明。
以 seajs 為例(無意冒犯),seajs采用運行時分析的方式實現(xiàn)依賴聲明識別,并根據(jù)依賴關(guān)系做進一步的模塊加載。比如如下代碼:
define(function(require) { var foo = require("foo"); //...});當seajs要執(zhí)行一個模塊的factory函數(shù)之前,會先分析函數(shù)體中的require書寫,具體代碼在這里和這里,大概的代碼邏輯如下:
Module.define = function (id, deps, factory) { ... //抽取函數(shù)體的字符串內(nèi)容 var code = factory.toString(); //設(shè)計一個正則,分析require語句 var reg = /\brequire\s*\(([.*]?)\)/g; var deps = []; //掃描字符串,得到require所聲明的依賴 code.replace(reg, function(m, $1){ deps.push($1); }); //加載依賴,完成后再執(zhí)行factory ...};由于框架設(shè)計是在“純前端實現(xiàn)”的約束條件下,使得模塊化框架對于依賴的分析必須在模塊資源加載完成之后才能做出識別。這將引起兩個性能相關(guān)的問題:
- require被強制為關(guān)鍵字而不能被壓縮。否則factory.toString()的分析將沒有意義。
- 依賴加載只能串行進行,當一個模塊加載完成之后才知道后面的依賴關(guān)系。
第一個問題還好,尤其是在gzip下差不多多少字節(jié),但是要配置js壓縮器保留require函數(shù)不壓縮。第二個問題就比較麻煩了,雖然seajs有seajs-combo插件可以一定程度上減少請求,但仍然不能很好的解決這個問題。舉個例子,有如下seajs模塊依賴關(guān)系樹:
ps: 圖片來源 @raphealguo
采用seajs-combo插件之后,靜態(tài)資源請求的效果是這樣的:
- http://www.example.com/page.js
- http://www.example.com/a.js,b.js
- http://www.example.com/c.js,d.js,e.js
- http://www.example.com/f.js
工作過程是
- 框架先請求了入口文件page.js
- 加載完成后分析factory函數(shù),發(fā)現(xiàn)依賴了a.js和b.js,然后發(fā)起一個combo請求,加載a.js和b.js
- a.js和b.js加載完成后又進一步分析源碼,才發(fā)現(xiàn)還依賴了c.js、d.js和e.js,再發(fā)起請求加載這三個文件
- 完成c、d、e的加載之后,再分析,發(fā)現(xiàn)f.js依賴,再請求
- 完成f.js的請求,page.js的依賴全部滿足,執(zhí)行它的factory函數(shù)。
雖然combo可以在依賴層級上進行合并,但完成page.js的請求仍需要4個。很多團隊在使用seajs的時候,為了避免這樣的串行依賴請求問題,會自己實現(xiàn)打包方案,將所有文件直接打包在一起,放棄了模塊化的按需加載能力,也是一種無奈之舉。
原因很簡單
以純前端方式來實現(xiàn)模塊依賴加載不能同時解決性能優(yōu)化問題。
歸根結(jié)底,這樣的結(jié)論是由前端領(lǐng)域語言的特點決定的。前端語言缺少三種編譯能力,前面講目錄規(guī)范和部署規(guī)范時其實已經(jīng)提到了一種能力,就是“資源定位的能力”,讓工程師使用開發(fā)路徑定位資源,編譯后可轉(zhuǎn)換為部署路徑。其他語言編寫的程序幾乎都沒有web這種物理上分離的資源部署策略,而且大多具都有類似'getResource(path)'這樣的函數(shù),用于在運行環(huán)境下定位當初的開發(fā)資源,這樣不管項目怎么部署,只要getResource函數(shù)運行正常就行了??上岸苏Z言沒有這樣的資源定位接口,只有url這樣的資源定位符,它指向的其實并不是開發(fā)路徑,而是部署路徑。
這里可以簡單列舉出前端語言缺少三種的語言能力:
- 資源定位的能力:使用開發(fā)路徑進行資源定位,項目發(fā)布后轉(zhuǎn)換成部署路徑
- 依賴聲明的能力:聲明一個資源依賴另一個資源的能力
- 資源嵌入的能力:把一個資源的編譯內(nèi)容嵌入到另一個文件中
以后我會在完善前端開發(fā)體系理論的時候在詳細介紹這三種語言能力的必要性和原子性,這里就暫時不展開說明了。
fis最核心的編譯思想就是圍繞這三種語言能力設(shè)計的。
要兼顧性能的同時解決模塊化依賴管理和加載問題,其關(guān)鍵點在于
不能運行時去分析模塊間的依賴關(guān)系,而要讓框架提前知道依賴樹。
了解了原因,我們就要自己動手設(shè)計模塊化框架了。不要害怕,模塊化框架其實很簡單,思想、規(guī)范都是經(jīng)過很多前輩總結(jié)的結(jié)果,我們只要遵從他們的設(shè)計思想去實現(xiàn)就好了。
參照已有規(guī)范,我定義了三個模塊化框架接口:
- 模塊定義接口:define(id, factory);
- 異步加載接口:require.async(ids, callback);
- 框架配置接口:require.config(options);
利用構(gòu)建工具建立模塊依賴關(guān)系表,再將關(guān)系表注入到代碼中,調(diào)用require.config接口讓框架知道完整的依賴樹,從而實現(xiàn)require.async在異步加載模塊時能提前預(yù)知所有依賴的資源,一次性請求回來。
以上面的page.js依賴樹為例,構(gòu)建工具會生成如下代碼:
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' ] }});當執(zhí)行require.async('page.js', fn);語句時,框架查詢config.deps表,就能知道要發(fā)起一個這樣的combo請求:
http://www.example.com/f.js,c.js,d.js,e.js,a.js,b.js,page.js
從而實現(xiàn)按需加載和請求合并兩項性能優(yōu)化需求。
根據(jù)這樣的設(shè)計思路,我請 @hinc 幫忙實現(xiàn)了這個框架,我告訴他,deps里不但會有js,還會有css,所以也要兼容一下。hinc果然是執(zhí)行能力非常強的小伙伴,僅一個下午的時間就搞定了框架的實現(xiàn),我們給這個框架取名為 scrat.js,僅有393行。
前面提到fis具有資源依賴聲明的編譯能力。因此只要工程師按照fis規(guī)定的書寫方式在代碼中聲明依賴關(guān)系,就能在構(gòu)建的最后階段自動獲得fis系統(tǒng)整理好的依賴樹,然后對依賴的數(shù)據(jù)結(jié)構(gòu)進行調(diào)整、輸出,滿足框架要求就搞定了!fis規(guī)定的資源依賴聲明方式為:在html中聲明依賴,在js中聲明依賴,在css中聲明依賴。
接下來,我要寫一個配置,將依賴關(guān)系表注入到代碼中。fis構(gòu)建是分流程的,具體構(gòu)建流程可以看這里。fis會在postpackager階段之前創(chuàng)建好完整的依賴樹表,我就在這個時候?qū)懸粋€插件來處理即可。
編輯fis-conf.js
//postpackager插件接受4個參數(shù),//ret包含了所有項目資源以及資源表、依賴樹,其中包括:// ret.src: 所有項目文件對象// ret.pkg: 所有項目打包生成的額外文件// reg.map: 資源表結(jié)構(gòu)化數(shù)據(jù)//其他參數(shù)暫時不用管var createFrameworkConfig = function(ret, conf, settings, opt){ //創(chuàng)建一個對象,存放處理后的配置項 var map = {}; //依賴樹數(shù)據(jù) map.deps = {}; //遍歷所有項目文件 fis.util.map(ret.src, function(subpath, file){ //文件的依賴數(shù)據(jù)就在file對象的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]);我們準備一下項目代碼,看看構(gòu)建的時候發(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自動建立了依賴關(guān)系,這是fis默認進行的依賴聲明。有了這個表,我們就可以把它注入到代碼中了。我們?yōu)轫撁鏈蕚湟粋€替換用的鉤子,比如約定為__FRAMEWORK_CONFIG__,這樣用戶就可以根據(jù)需要在合適的地方獲取并使用這些數(shù)據(jù)。模塊化框架的配置一般都是寫在非模塊化文件中的,比如html頁面里,所以我們應(yīng)該只針對views目錄下的文件做這樣的替換就可以。所以我們需要給views下的文件進行一個標記,只有views下的html或js文件才需要進行依賴樹數(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目錄下的文件加一個isViews屬性標記,用以標記文件分類 //我們可以在插件中拿到文件對象的這個值 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標記的文件 //替換里面的__FRAMEWORK_CONFIG__鉤子 fis.util.map(ret.src, function(subpath, file){ //有isViews標記,并且是js或者html類文件,才需要做替換 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]);//項目配置fis.config.set('name', 'proj'); //將name、version獨立配置,統(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)知道了這個foo.js依賴于bar.js、bar.css以及foo.css,因此可以發(fā)起兩個combo請求去加載所有依賴的js、css文件,完成后再執(zhí)行回調(diào)。
現(xiàn)在模塊的id有一些問題,因為模塊發(fā)布會有版本號信息,因此模塊id也應(yīng)該攜帶版本信息,從前面的依賴樹生成配置代碼中我們可以看到模塊id其實也是文件的一個屬性,因此我們可以在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為【項目名/版本號/文件路徑】 id : '${name}/${version}/$1', release : '/public/c/${name}/${version}/$1' }, { reg : /^\/views\/(.*)$/, //給views目錄下的文件加一個isViews屬性標記,用以標記文件分類 //我們可以在插件中拿到文件對象的這個值 isViews : true, release : '/public/${name}/${version}/$1' }, { reg : '**', useStandard : false, useOptimizer : false }]);重新構(gòu)建項目,我們得到了新的結(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都會被修改為我們指定的模式,這就是以文件為中心的編譯系統(tǒng)的威力。
以文件對象為中心構(gòu)建系統(tǒng)應(yīng)該通過配置指定文件的各種屬性。插件并不自己實現(xiàn)某種規(guī)范規(guī)定,而是讀取file對象的對應(yīng)屬性值,這樣插件的職責單一,規(guī)范又能統(tǒng)一起來被用戶指定,為完整的前端開發(fā)體系設(shè)計奠定了堅實規(guī)范配置的基礎(chǔ)。
接下來還有一個問題,就是模塊名太長,開發(fā)中寫這么長的模塊名非常麻煩。我們可以借鑒流行的模塊化框架中常用的縮短模塊名手段——別名(alias)——來降低開發(fā)中模塊引用的成本。此外,目前的配置其實會針對所有文件生成依賴關(guān)系表,我們的開發(fā)概念定義只有components和component_modules目錄下的文件才是模塊化的,因此我們可以進一步的對文件進行分類,得到這樣配置規(guī)范:
fis.config.set('roadmap.path', [ { reg : '**.md', release : false, isHtmlLike : true }, { reg : /^\/component_modules\/(.*)$/i, id : '$1', //追加isComponentModules標記屬性 isComponentModules : true, release : '/public/c/$1' }, { reg : /^\/components\/(.*)$/i, id : '${name}/${version}/$1', //追加isComponents標記屬性 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){ //判斷一下文件名和文件夾是否同名,如果同名則建立一個別名 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'); 就等價于 require('proj/1.0.5/foo/foo.js');了。
還剩最后一個小小的需求,就是希望能像寫nodejs一樣開發(fā)js模塊,也就是要求實現(xiàn)define的自動包裹功能,這個可以通過文件編譯的 postprocessor 插件完成。配置為:
//在postprocessor對所有js后綴的文件進行內(nèi)容處理:fis.config.set('modules.postprocessor.js', function(content, file){ //只對模塊化js文件進行包裝 if(file.isComponents || file.isComponentModules){ content = 'define("' + file.id + '", function(require,exports,module){' + content + '});'; } return content;});所有在components目錄和component_modules目錄下的js文件都會被包裹define,并自動根據(jù)roadmap.path中的id配置進行模塊定義了。
最煎熬的一天終于過去了,睡一覺,擁抱一下周末。
2014年02月15日 - 超晴
周末的天氣非常好哇,一覺睡到中午才起,這么好的天氣寫碼豈不是很loser?!
2014年02月16日 - 小雨
居然浪費了一天,剩下的時間不多了,今天要抓緊啊?。?!
讓我們來回顧一下已經(jīng)完成了哪些工作:
- 規(guī)范
- 開發(fā)規(guī)范
模塊化開發(fā),js模塊化,css模塊化,像nodejs一樣的模塊化開發(fā)組件化開發(fā),js、css、handlebars維護在一起- 部署規(guī)范
采用nodejs后端,基本部署規(guī)范應(yīng)該參考 express 項目部署按版本號做非覆蓋式發(fā)布公共模塊可發(fā)布給第三方共享- 框架
js模塊化框架,支持請求合并,按需加載等性能優(yōu)化點- 工具
- 可以編譯stylus為css
- 支持js、css、圖片壓縮
- 允許圖片壓縮后以base64編碼形式嵌入到css、js或html中
- 與ci平臺集成
- 文件監(jiān)聽、瀏覽器自動刷新
- 本地預(yù)覽、數(shù)據(jù)模擬
- 倉庫
- 支持component模塊安裝和使用
剩下的幾個需求中有些是fis默認支持的,比如base64內(nèi)嵌功能,圖片會先經(jīng)過編譯流程,得到壓縮后的內(nèi)容fis再對其進行base64化的內(nèi)嵌處理。由于fis的內(nèi)嵌功能支持任意文件的內(nèi)嵌,所以,這個語言能力擴展可以同時解決前端模板和圖片base64內(nèi)嵌需求,比如我們有這樣的代碼:
project - components - foo - foo.js - foo.css - foo.handlebars - foo.png無需配置,既可以在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也非常簡單,fis在 parser 階段處理非標準語言,這個階段可以把非標準的js(coffee/前端模板)、css(less/sass/stylus)、html(markdown)語言轉(zhuǎn)換為標準的js、css或html。處理之后那些文件還能和標準語言一起經(jīng)歷預(yù)處理、語言能力擴展、后處理、校驗、測試、壓縮等階段。
所以,要支持stylus的編譯,只要在fis-conf.js中添加這樣的配置即可:
//依賴開源的stylus包var stylus = require('stylus');//編譯插件只負責處理文件內(nèi)容var stylusParser = function(content, file, conf){ return stylus(content, conf).render();};//配置編譯流程,styl后綴的文件經(jīng)過編譯插件函數(shù)處理fis.config.set('modules.parser.styl', stylusParser);//告訴fis,styl后綴的文件,被當做css處理,編譯后后綴也是cssfis.config.set('roadmap.ext.styl', 'css');這樣我們項目中的*.styl后綴的文件都會被編譯為css內(nèi)容,并且會在后面的流程中被當做css內(nèi)容處理,比如壓縮、csssprite等。
文件監(jiān)聽、自動刷新都是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)聽、自動刷新,則使用:
fis release -wL壓縮、打包、文件監(jiān)聽、自動刷新、發(fā)布到output目錄,則使用:
fis release -opwLd output構(gòu)建工具不需要那么多命令,或者develop、release等不同狀態(tài)的配置文件,應(yīng)該從命令行切換編譯參數(shù),從而實現(xiàn)開發(fā)/上線構(gòu)建模式的切換。
另外,fis是命令行工具,各種內(nèi)置的插件也是完全獨立無環(huán)境依賴的,可以與ci平臺直接對接,并在各個主流操作系統(tǒng)下運行正常。
利用fis的內(nèi)置的各種編譯功能,我們離目標又近了許多:
- 規(guī)范
- 開發(fā)規(guī)范
模塊化開發(fā),js模塊化,css模塊化,像nodejs一樣的模塊化開發(fā)組件化開發(fā),js、css、handlebars維護在一起- 部署規(guī)范
采用nodejs后端,基本部署規(guī)范應(yīng)該參考express項目部署按版本號做非覆蓋式發(fā)布公共模塊可發(fā)布給第三方共享- 框架
js模塊化框架,支持請求合并,按需加載等性能優(yōu)化點- 工具
可以編譯stylus為css支持js、css、圖片壓縮允許圖片壓縮后以base64編碼形式嵌入到css、js或html中與ci平臺集成文件監(jiān)聽、瀏覽器自動刷新- 本地預(yù)覽、數(shù)據(jù)模擬
- 倉庫
- 支持component模塊安裝和使用
剩下兩個,我們可以通過擴展fis的命令行插件來實現(xiàn)。fis有11個編譯流程擴展點,還有一個命令行擴展點。要擴展命令行插件很簡單,只要我們將插件安裝到與fis同級的node_modules目錄下即可。比如:
node_modules - fis - fis-command-say那么執(zhí)行 fis say 這個命令,就能調(diào)用到那個fis-command-say插件了。剩下的這個component模塊安裝,我就利用了這個擴展點,結(jié)合component開源的 component-installer 包,我就可以把component整合當前開發(fā)體系中,這里我們需要創(chuàng)建一個npm包來提供擴展,而不能直接在fis-conf.js中擴展命令行,插件代碼我就不貼了,可以看 這里。
眼前我們有了一個差不多100行的fis-conf.js文件,還有幾個插件,如果我把這樣一個零散的系統(tǒng)交付團隊使用,那么大家使用的步驟差不多是這樣的:
- 安裝fis,npm install -g fis
- 安裝component安裝用的命令行插件,npm insatll -g fis-command-component
- 安裝stylus編譯插件,npm install -g fis-parser-stylus
- 下載一份配置文件,fis-conf.js,修改里面的name、version配置
這種情況讓團隊用起來會有很多問題。首先,安裝過程太過麻煩,其次如果項目多,那么fis-conf.js不能同步升級,這是非常嚴重的問題。grunt的gruntfile.js也是如此。如果說有一個項目用了某套grunt配置感覺很爽,那么下個項目也想用這套方案,復(fù)制gruntfile.js是必須的操作,項目用的多了,同步gruntfile的成本就變得非常高了。
因此,fis提供了一種“包裝”的能力,它允許你將fis作為內(nèi)核,包裝出一個新的命令行工具,這個工具內(nèi)置了一些fis的配置,并且把所有命令行調(diào)用的參數(shù)傳遞給fis內(nèi)核去處理。
我準備把這套系統(tǒng)打包為一個新的工具,給它取名為 scrat,也是一只松鼠。這個新工具的目錄結(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)一管理//項目中就不用再寫了fis.config.merge({...});將這個npm包發(fā)布出來,我們就有了一個全新的開發(fā)工具,這個工具可以解決前面說的13項技術(shù)問題,并提供一套完整的集成解決方案,而你的團隊使用的時候,只有兩個步驟:
- 安裝這個工具,npm install -g scrat
- 項目配置只有兩項,name和version
使用新工具的命令、參數(shù)幾乎和fis完全一樣:
scrat release [options]scrat server startscrat install <name@version> [options]而scrat這個工具所內(nèi)置的配置將變成規(guī)范文檔描述給團隊同學,這套系統(tǒng)要比grunt那種松散的構(gòu)建系統(tǒng)組成方式更容易被多個團隊、多個項目同時共享。
熬了一個通宵,基本算是完成了。。。
2014年02月17日 - 多云
終于到了周一,交付了一個新的開發(fā)工具——scrat,及其使用 文檔。
然而,過去的三天,為了構(gòu)造這套前端開發(fā)體系,都寫了哪些代碼呢?
- 基于fis的一套規(guī)范及插件配置,274行;
- scrat install命令行插件,用于安裝component模塊,74行;
- scrat server命令行插件,用于啟動nodejs的服務(wù)器,203行
- 編譯stylus的插件,10行
- 編譯handlebars的插件,6行
- 一個模塊化框架 scrat.js,393行
一共 960行 代碼,用了4人/天。
總結(jié)
不可否認,為大規(guī)模前端團隊設(shè)計集成解決方案需要花費非常多的心思。
如果說只是實現(xiàn)一個簡單的編譯+壓縮+文件監(jiān)+聽自動刷新的常規(guī)構(gòu)建系統(tǒng),基于fis應(yīng)該不超過1小時就能完成一個,但要實踐完整的前端集成解決方案,確實需要點時間。
如之前一篇 文章 所講,前端集成解決方案有8項技術(shù)要素,除了組件倉庫,其他7項對于企業(yè)級前端團隊來說,應(yīng)該都需要完整實現(xiàn)的。即便暫時不想實現(xiàn),也會隨著業(yè)務(wù)發(fā)展而被迫慢慢完善,這個完善過程是普適的。
對于前端集成解決方案的實踐,可以總結(jié)出這些設(shè)計步驟:
- 設(shè)計開發(fā)概念,定義開發(fā)資源的分類(模塊化/非模塊化)
- 設(shè)計開發(fā)目錄,降低開發(fā)、維護成本(開發(fā)規(guī)范)
- 根據(jù)運維和業(yè)務(wù)要求,設(shè)計部署規(guī)范(部署規(guī)范)
- 設(shè)計工具,完成開發(fā)目錄和部署目錄的轉(zhuǎn)換(開發(fā)-部署轉(zhuǎn)換)
- 設(shè)計模塊化框架,兼顧性能優(yōu)化(開發(fā)框架)
- 擴展工具,支持開發(fā)框架的構(gòu)建需求(框架構(gòu)建需求)
- 流程整合(開發(fā)、測試、聯(lián)調(diào)、上線等流程接入)
我們可以看看業(yè)界已有團隊提出的各種解決方案,無不以這種思路來設(shè)計和發(fā)展的:
- seajs開發(fā)體系,支付寶團隊前端開發(fā)體系,以 spm 為構(gòu)建和包管理工具
- fis-plus,百度絕大多數(shù)前端團隊使用的開發(fā)體系,以fis為構(gòu)建工具內(nèi)核,以lights為包管理工具
- edp,百度ecomfe前端開發(fā)體系,以 edp 為構(gòu)建和包管理工具
- modjs,騰訊AlloyTeam團隊出品的開發(fā)體系
- yeoman,google出品的解決方案,以grunt為構(gòu)建工具,bower為包管理工具
縱觀這些公司出品的前端集成解決方案,深入剖析其中的框架、規(guī)范、工具和流程,都可以發(fā)現(xiàn)一些共通的影子,設(shè)計思想殊途同歸,不約而同的朝著一種方向前進,那就是前端集成解決方案。嘗試將前端工程孤立的技術(shù)要素整合起來,解決常見的領(lǐng)域問題。
或許有人會問,不就是寫頁面么,用得著這么復(fù)雜?
在這里我不能給出肯定或者否定的答復(fù)。
因為單純從語言的角度來說,html、js、css(甚至有人認為css是數(shù)據(jù)結(jié)構(gòu),而非語言)確實是最簡單最容易上手的開發(fā)語言,不用模塊化、不用工具、不用壓縮,任何人都可以快速上手,完成一兩個功能簡單的頁面。所以說,在一般情況下,前端開發(fā)非常簡單。
在規(guī)模很小的項目中,前端技術(shù)要素彼此不會直接產(chǎn)生影響,因此無需集成解決方案。
但正是由于前端語言這種靈活松散的特點,使得前端項目規(guī)模在達到一定規(guī)模后,工程問題凸顯,成為發(fā)展瓶頸,各種技術(shù)要素彼此之間開始出現(xiàn)關(guān)聯(lián),要用模塊化開發(fā),就必須對應(yīng)某個模塊化框架,用這個框架就必須對應(yīng)某個構(gòu)建工具,要用這個工具,就必須對應(yīng)某個包管理工具……這個時候,完整實踐前端集成解決方案就成了不二選擇。
當前端項目達到一定規(guī)模后,工程問題將成為主要瓶頸,原來孤立的技術(shù)要素開始彼此產(chǎn)生影響,需要有人從比較高的角度去梳理、尋找適合自己團隊的集成解決方案。
所以會出現(xiàn)一些框架或工具在小項目中使用的好好的,一旦放到團隊里使用就非常困難的情況。
前端入門雖易工程不易,且行寫珍惜!