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