本文首發(fā)于Array_Huang的技術(shù)博客——
實(shí)用至上,非經(jīng)作者同意,請(qǐng)勿轉(zhuǎn)載。
原文地址:https://segmentfault.com/a/1190000007301770
如果您對(duì)本系列文章感興趣,歡迎關(guān)注訂閱這里:https://segmentfault.com/blog/array_huang
前言
本文介紹如何在多項(xiàng)目間共用同一套基礎(chǔ)設(shè)施,又或是某種層次的框架。
基礎(chǔ)設(shè)施是什么?
一個(gè)完整的網(wǎng)站,不可能只包含一個(gè)jQuery,或是某個(gè)MVVM框架,其中必定包含了許多解決方案,例如:如何上傳?如何兼容IE?如何跨域?如何使用本地存儲(chǔ)?如何做用戶(hù)信息反饋?又或者具體到如何選擇日期?等等等等……這里面必定包含了UI框架、JS框架、各種小工具庫(kù),不論是第三方的還是自己團(tuán)隊(duì)研發(fā)的。而以上所述的種種,就構(gòu)成了一套完整的解決方案,也稱(chēng)基礎(chǔ)設(shè)施。
基礎(chǔ)設(shè)施有個(gè)重要的特征,那就是與業(yè)務(wù)邏輯無(wú)關(guān),不論是OA還是CMS又或是CRM,只要整體產(chǎn)品形態(tài)類(lèi)似,我們就可以使用同一套基礎(chǔ)設(shè)施。
框架
框架這個(gè)概念很泛,泛得讓人心生困惑,但抽象出來(lái)說(shuō),框架就是一套定義代碼在哪里寫(xiě)、怎么寫(xiě)的規(guī)則。不能說(shuō)我們要怎么去用框架,反倒是框架控制我們?cè)趺慈?strong>填代碼。
本系列前面的十來(lái)篇文章,分開(kāi)來(lái)看是不同的點(diǎn),但如果所有文章合起來(lái),并連同示例項(xiàng)目(Array-Huang/webpack-seed),實(shí)際上闡述的就是一套完整的多頁(yè)應(yīng)用框架(或稱(chēng)架構(gòu))。這套框架規(guī)定了整個(gè)應(yīng)用的方方面面,舉幾個(gè)例子:
- 每個(gè)頁(yè)面的文件放在哪個(gè)目錄?
- 頁(yè)面的HTML、入口文件、css、圖片等等應(yīng)該怎么放?
- 編碼規(guī)范(由ESLint來(lái)保證)。
當(dāng)然,這只是我的框架,我希望你們可以看懂了,然后根據(jù)自己的需求來(lái)調(diào)整,變成你們的框架。甚至說(shuō),我自己在做不同類(lèi)型的項(xiàng)目時(shí),整體架構(gòu)也都會(huì)有不少的變化。
為什么要共用基礎(chǔ)設(shè)施/框架/架構(gòu)?
緣起
數(shù)月前,我找同事要了一個(gè)他自己寫(xiě)的地區(qū)選擇器,拉回來(lái)一看遍地都是ESLint的報(bào)錯(cuò)(他負(fù)責(zé)的項(xiàng)目沒(méi)有用ESLint,比較隨意),我這人有強(qiáng)迫癥的怎么看得過(guò)眼,卷起袖子就開(kāi)始改,改好也就正常使用了。過(guò)了一段時(shí)間,來(lái)了新需求,同事在他那改好了地區(qū)選擇器又發(fā)了一份給我,我一看頭都大了,又是滿(mǎn)地報(bào)錯(cuò),這不是又要我再改一遍嗎?當(dāng)時(shí)我就懵了,只好按著他的思路,對(duì)我的版本做了修改。從此,也確立了我們公司會(huì)有兩份外觀功能都一致,但是實(shí)現(xiàn)卻不一樣的地區(qū)選擇器。
很坑爹是吧?
多項(xiàng)目共享架構(gòu)變動(dòng)
上面說(shuō)的是組件級(jí)的,下面我們來(lái)說(shuō)架構(gòu)級(jí)別的。
我在公司主要負(fù)責(zé)的項(xiàng)目有兩個(gè),在我的不懈努力下,已經(jīng)做到跟我的腳手架項(xiàng)目Array-Huang/webpack-seed大體上同構(gòu)了。但維持同構(gòu)顯然是要付出代價(jià)的,我在腳手架項(xiàng)目試驗(yàn)過(guò)的改進(jìn),小至改個(gè)目錄路徑,大至引入個(gè)plugin啊loader啊什么的,都要分別在公司的兩個(gè)項(xiàng)目里各做一遍,超煩噠(嫌棄臉
試想只是兩個(gè)項(xiàng)目就已經(jīng)這樣了,如果是三個(gè)、四個(gè),甚至六個(gè)、七個(gè)呢?堪憂(yōu)啊堪憂(yōu)??!
快速創(chuàng)建新項(xiàng)目
不知道你們有沒(méi)有這樣子的經(jīng)驗(yàn):接到新項(xiàng)目時(shí),靈機(jī)一動(dòng)“這不就是我的XX項(xiàng)目嗎?”,然后趕緊搬出XX項(xiàng)目的源碼,然后刪掉業(yè)務(wù)邏輯,保留可復(fù)用的基礎(chǔ)設(shè)施。
也許你會(huì)說(shuō),這不已經(jīng)比從零開(kāi)始要好多了嗎?總體上來(lái)說(shuō),是吧,但還不夠好:
- 你需要花時(shí)間重溫整個(gè)項(xiàng)目的架構(gòu),搞清楚哪些要?jiǎng)h、哪些要留。
- 畢竟是快刀斬亂麻,清理好的架構(gòu)比不上原先的思路那么清晰。
- 清理完代碼想著跑跑看,結(jié)果一大堆報(bào)錯(cuò),一個(gè)一個(gè)來(lái)調(diào)煩的要命,而且還很可能是刪錯(cuò)了什么了不得的東西,還要去原先額項(xiàng)目里搬回來(lái)。
以上這些問(wèn)題,你每創(chuàng)建一個(gè)新項(xiàng)目都要經(jīng)歷一遍,我問(wèn)你怕了沒(méi)有。
腳手架不是可以幫助快速創(chuàng)建新項(xiàng)目嗎?
是的沒(méi)錯(cuò),腳手架本身就算是一整套基礎(chǔ)設(shè)施了,但依然有下列問(wèn)題:
- 維護(hù)一套腳手架你知道有多麻煩嗎?公司項(xiàng)目一忙起來(lái),加班都做不完,哪顧得上腳手架啊。最后新建項(xiàng)目的時(shí)候發(fā)現(xiàn)腳手架已經(jīng)落后N多了,你到底是用呢還是不用呢?
- 甭跟我提Github上開(kāi)源的腳手架,像我這么有個(gè)性的人,會(huì)直接用那些妖艷賤貨嗎?
- 不同類(lèi)型的項(xiàng)目技術(shù)選型不一樣,比如說(shuō):需不需要兼容低版本IE;是web版的還是Hybrid App的;是前臺(tái)還是后臺(tái)。每一套技術(shù)選型就是一套腳手架,難道你要維護(hù)這么多套腳手架嗎?
上述問(wèn)題,通過(guò)共用基礎(chǔ)設(shè)施,都能解決
- 既然共用了基礎(chǔ)設(shè)施,要怎么改肯定都是所有項(xiàng)目一起共享的了,不論是組件層面的還是架構(gòu)本身。
- 假設(shè)你每個(gè)不同類(lèi)型的項(xiàng)目都已經(jīng)準(zhǔn)備好了與其它項(xiàng)目共用基礎(chǔ)設(shè)施,那么,你根本不需要花費(fèi)多余的維護(hù)成本,創(chuàng)建新項(xiàng)目的時(shí)候看準(zhǔn)了跟之前哪個(gè)項(xiàng)目是屬于同一類(lèi)型的,湊一腳就行了唄,輕松。
怎么實(shí)現(xiàn)多項(xiàng)目共用一套基礎(chǔ)設(shè)施呢?
示例項(xiàng)目
在之前的文章里,我使用的一直都是Array-Huang/webpack-seed這個(gè)腳手架項(xiàng)目作為示例,而為了實(shí)踐多項(xiàng)目共用基礎(chǔ)設(shè)施,我對(duì)該項(xiàng)目的架構(gòu)做了較大幅度的調(diào)整,升級(jí)為2.0.0版本。為免大家看前面的文章時(shí)發(fā)現(xiàn)示例項(xiàng)目貨不對(duì)板,感到困惑,我新開(kāi)了一個(gè)repo來(lái)存放調(diào)整后的腳手架:Array-Huang/webpack-seed-v2(https://github.com/Array-Huang/webpack-seed-v2),并且,我在兩個(gè)項(xiàng)目的README里我都注明了相應(yīng)的內(nèi)容,大家可不要混淆了哈。
下面就以從Array-Huang/webpack-seed到Array-Huang/webpack-seed-v2的改造過(guò)程來(lái)介紹如何實(shí)現(xiàn)多項(xiàng)目共用基礎(chǔ)設(shè)施。
改造思路
改造思路其實(shí)很簡(jiǎn)單,就是把預(yù)想中多個(gè)項(xiàng)目都能用得上的部分從現(xiàn)有項(xiàng)目里抽離出來(lái)。
如何抽離
抽離的說(shuō)法是針對(duì)原項(xiàng)目的,如果單純從文件系統(tǒng)的角度來(lái)說(shuō),只不過(guò)是移動(dòng)了某些文件和目錄。
移動(dòng)到哪里了呢?自然是移動(dòng)到與項(xiàng)目目錄同級(jí)的地方,這樣就方便多個(gè)項(xiàng)目引用這個(gè)核心了。
如果你跟我一樣,在原項(xiàng)目中定義了大量路徑和alias的話(huà),移動(dòng)這些文件/目錄就只是個(gè)改變量的活了:
選自webpack-seed/webpack-config/base/dir-vars.config.js:
var path = require('path');
var moduleExports = {};
// 源文件目錄
moduleExports.staticRootDir = path.resolve(__dirname, '../../'); // 項(xiàng)目根目錄
moduleExports.srcRootDir = path.resolve(moduleExports.staticRootDir, './src'); // 項(xiàng)目業(yè)務(wù)代碼根目錄
moduleExports.vendorDir = path.resolve(moduleExports.staticRootDir, './vendor'); // 存放所有不能用npm管理的第三方庫(kù)
moduleExports.dllDir = path.resolve(moduleExports.srcRootDir, './dll'); // 存放由各種不常改變的js/css打包而來(lái)的dll
moduleExports.pagesDir = path.resolve(moduleExports.srcRootDir, './pages'); // 存放各個(gè)頁(yè)面獨(dú)有的部分,如入口文件、只有該頁(yè)面使用到的css、模板文件等
moduleExports.publicDir = path.resolve(moduleExports.srcRootDir, './public-resource'); // 存放各個(gè)頁(yè)面使用到的公共資源
moduleExports.logicDir = path.resolve(moduleExports.publicDir, './logic'); // 存放公用的業(yè)務(wù)邏輯
moduleExports.libsDir = path.resolve(moduleExports.publicDir, './libs'); // 與業(yè)務(wù)邏輯無(wú)關(guān)的庫(kù)都可以放到這里
moduleExports.configDir = path.resolve(moduleExports.publicDir, './config'); // 存放各種配置文件
moduleExports.componentsDir = path.resolve(moduleExports.publicDir, './components'); // 存放組件,可以是純HTML,也可以包含js/css/image等,看自己需要
moduleExports.layoutDir = path.resolve(moduleExports.publicDir, './layout'); // 存放UI布局,組織各個(gè)組件拼起來(lái),因應(yīng)需要可以有不同的布局套路
// 生成文件目錄
moduleExports.buildDir = path.resolve(moduleExports.staticRootDir, './build'); // 存放編譯后生成的所有代碼、資源(圖片、字體等,雖然只是簡(jiǎn)單的從源目錄遷移過(guò)來(lái))
module.exports = moduleExports;
選自webpack-seed/webpack-config/resolve.config.js:
var path = require('path');
var dirVars = require('./base/dir-vars.config.js');
module.exports = {
// 模塊別名的配置,為了使用方便,一般來(lái)說(shuō)所有模塊都是要配置一下別名的
alias: {
/* 各種目錄 */
iconfontDir: path.resolve(dirVars.publicDir, 'iconfont/'),
configDir: dirVars.configDir,
/* vendor */
/* bootstrap 相關(guān) */
metisMenu: path.resolve(dirVars.vendorDir, 'metisMenu/'),
/* libs */
withoutJqueryModule: path.resolve(dirVars.libsDir, 'without-jquery.module'),
routerModule: path.resolve(dirVars.libsDir, 'router.module'),
libs: path.resolve(dirVars.libsDir, 'libs.module'),
/* less */
lessDir: path.resolve(dirVars.publicDir, 'less'),
/* components */
/* layout */
layout: path.resolve(dirVars.layoutDir, 'layout/html'),
'layout-without-nav': path.resolve(dirVars.layoutDir, 'layout-without-nav/html'),
/* logic */
cm: path.resolve(dirVars.logicDir, 'common.module'),
cp: path.resolve(dirVars.logicDir, 'common.page'),
/* config */
configModule: path.resolve(dirVars.configDir, 'common.config'),
bootstrapConfig: path.resolve(dirVars.configDir, 'bootstrap.config'),
},
// 當(dāng)require的模塊找不到時(shí),嘗試添加這些后綴后進(jìn)行尋找
extentions: ['', 'js'],
};
抽離對(duì)象
抽離的方法很簡(jiǎn)單,那么關(guān)鍵就看到底是哪些部分可以抽離、需要抽離了,這一點(diǎn)看我抽離后的成果就比較清晰了:
先來(lái)看根目錄:
├─ core # 抽離出來(lái)的基礎(chǔ)設(shè)施,或稱(chēng)“核心”
├─ example-admin-1 # 示例項(xiàng)目1,被抽離后剩下的
├─ example-admin-2 # 示例項(xiàng)目2,嗯,簡(jiǎn)單起見(jiàn),直接復(fù)制了example-admin-1,不過(guò)還是要做一點(diǎn)調(diào)整的,比如說(shuō)配置
├─ npm-scripts # 沒(méi)想到npm-scripts也能公用吧?
├─ vendor # 無(wú)法在npm上找到的第三方庫(kù)
├─ .eslintrc # ESLint的配置文件
├─ package.json # 所有的npm庫(kù)依賴(lài)建議都寫(xiě)到這里,不建議寫(xiě)到具體項(xiàng)目的package.json里
再來(lái)看看core目錄
├─ _webpack.dev.config.js # 整理好公用的開(kāi)發(fā)環(huán)境webpack配置,以備繼承
├─ _webpack.product.config.js # 整理好公用的生產(chǎn)環(huán)境webpack配置,以備繼承
├─ webpack-dll.config.js # 用來(lái)編譯Dll文件用的webpack配置文件
├─ manifest.json # Dll文件的資源目錄
├─ package.json # 沒(méi)有什么實(shí)質(zhì)內(nèi)容,我這里就放了個(gè)編譯Dll用的npm script
├─components # 各種UI組件
│ ├─footer
│ ├─header
│ ├─side-menu
│ └─top-nav
├─config # 公共配置,有些是提供給具體項(xiàng)目的配置來(lái)繼承的,有些本身就有用(比如說(shuō)“核心”部分本身需要的配置)
├─dll # 之前的文章里就說(shuō)過(guò),我建議把各種第三方庫(kù)(包括npm庫(kù)也包括非npm庫(kù))都打包成Dll來(lái)加速webpack編譯過(guò)程,這部分明顯就屬于基礎(chǔ)設(shè)施了
├─iconfont # 字體圖標(biāo)能不能公用,這點(diǎn)我也是比較猶豫的,看項(xiàng)目實(shí)際需要吧,不折騰的話(huà)還是推薦公用
├─layout # 布局,既然是同類(lèi)型項(xiàng)目,布局肯定是基本一樣的
│ ├─layout
│ └─layout-without-nav
├─less # 樣式基礎(chǔ),在我這項(xiàng)目里就是針對(duì)bootstrap的SB-Admin主題做了修改
│ ├─base-dir
│ └─components-dir
├─libs # 自己團(tuán)隊(duì)研發(fā)的一些公共的方法/庫(kù),又或是針對(duì)第三方庫(kù)的適配器(比如說(shuō)對(duì)alert庫(kù)封裝一層,后面要更換庫(kù)的時(shí)候就方便了)
├─npm-scripts # 與根目錄下的npm-scripts目錄不一樣,這里的不是用來(lái)公用的,而是“核心”使用到的script,比如我在這里就放了編譯dll的npm script
└─webpack-config # 公用的webpack配置,尤其是關(guān)系到“核心”部分的配置,比如說(shuō)各第三方庫(kù)的alias。這里的配置是用來(lái)給具體項(xiàng)目來(lái)繼承的,老實(shí)說(shuō)我現(xiàn)在繼承的方法也比較復(fù)雜,回頭看看有沒(méi)有更簡(jiǎn)單的方法。
├─base
├─inherit
└─vendor
最后總結(jié)一下,是哪些資源被抽離出來(lái)了:
- webpack配置中屬于架構(gòu)的部分,比如說(shuō)各種loader、plugin、“核心”部分的alias。
- “核心”部分所需的配置,比如我這項(xiàng)目里為了定制bootstrap而建的配置。
- 各種與UI相關(guān)的資源,比如UI框架/樣式、UI組件、字體圖標(biāo)。
- 第三方庫(kù),以Dll文件的形式存在。
- 自研庫(kù)/適配器。
結(jié)構(gòu)圖
上傳上來(lái)以后發(fā)現(xiàn)圖被壓小了,請(qǐng)到這里看原圖
附系列文章目錄(同步更新)
- webpack多頁(yè)應(yīng)用架構(gòu)系列(一):一步一步解決架構(gòu)痛點(diǎn):
https://segmentfault.com/a/1190000006843916 - webpack多頁(yè)應(yīng)用架構(gòu)系列(二):webpack配置常用部分有哪些?:
https://segmentfault.com/a/1190000006863968 - webpack多頁(yè)應(yīng)用架構(gòu)系列(三):怎么打包公共代碼才能避免重復(fù)?:
https://segmentfault.com/a/1190000006871991 - webpack多頁(yè)應(yīng)用架構(gòu)系列(四):老式j(luò)Query插件還不能丟,怎么兼容?:
https://segmentfault.com/a/1190000006887523 - webpack多頁(yè)應(yīng)用架構(gòu)系列(五):聽(tīng)說(shuō)webpack連less/css也能打包?:
https://segmentfault.com/a/1190000006897458 - webpack多頁(yè)應(yīng)用架構(gòu)系列(六):聽(tīng)說(shuō)webpack連圖片和字體也能打包?:
https://segmentfault.com/a/1190000006907701 - webpack多頁(yè)應(yīng)用架構(gòu)系列(七):開(kāi)發(fā)環(huán)境、生產(chǎn)環(huán)境傻傻分不清楚?:
https://segmentfault.com/a/1190000006952432 - webpack多頁(yè)應(yīng)用架構(gòu)系列(八):教練我要寫(xiě)ES6!webpack怎么整合Babel?:
https://segmentfault.com/a/1190000006992218 - webpack多頁(yè)應(yīng)用架構(gòu)系列(九):總有刁民想害朕!ESLint為你阻擊垃圾代碼:
https://segmentfault.com/a/1190000007030775 - webpack多頁(yè)應(yīng)用架構(gòu)系列(十):如何打造一個(gè)自定義的bootstrap:
https://segmentfault.com/a/1190000007043716 - webpack多頁(yè)應(yīng)用架構(gòu)系列(十一):預(yù)打包Dll,實(shí)現(xiàn)webpack音速編譯:
https://segmentfault.com/a/1190000007104372 - webpack多頁(yè)應(yīng)用架構(gòu)系列(十二):利用webpack生成HTML普通網(wǎng)頁(yè)&頁(yè)面模板:
https://segmentfault.com/a/1190000007126268 - webpack多頁(yè)應(yīng)用架構(gòu)系列(十三):構(gòu)建一個(gè)簡(jiǎn)單的模板布局系統(tǒng):
https://segmentfault.com/a/1190000007159115 - webpack多頁(yè)應(yīng)用架構(gòu)系列(十四):No復(fù)制粘貼!多項(xiàng)目共用基礎(chǔ)設(shè)施
- webpack多頁(yè)應(yīng)用架構(gòu)系列(十五):論前端如何在后端渲染開(kāi)發(fā)模式下夾縫生存
本文首發(fā)于Array_Huang的技術(shù)博客——
實(shí)用至上,非經(jīng)作者同意,請(qǐng)勿轉(zhuǎn)載。
原文地址:https://segmentfault.com/a/1190000007301770
如果您對(duì)本系列文章感興趣,歡迎關(guān)注訂閱這里:https://segmentfault.com/blog/array_huang