webpack多頁(yè)應(yīng)用架構(gòu)系列(十四):No復(fù)制粘貼!多項(xiàng)目共用基礎(chǔ)設(shè)施

本文首發(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-seedArray-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)到這里看原圖

Array-Huang-webpack-seed-v2 結(jié)構(gòu)圖
Array-Huang-webpack-seed-v2 結(jié)構(gòu)圖

附系列文章目錄(同步更新)

本文首發(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容