一文讀懂NodeJS全棧開發利器:CabloyJS(萬字長文)

1 基本概念

1.1 CabloyJS是什麼

1.1.1 定義

CabloyJS是一款頂級NodeJS全棧業務開發框架

1.1.2 特點

  • CabloyJS是採用NodeJS進行全棧開發的最佳實踐
  • CabloyJS不重複造輪子,而是採用業界最新的開源技術,進行全棧開發的最佳組合
  • CabloyJS前端採用VueJS + Framework7 + WebPack,後端採用KoaJS + EggJS,數據庫採用MySQL
  • CabloyJS時刻跟蹤開源技術的最新成果,並持續優化,使整個框架時刻保持最佳狀態

1.1.3 理念

既可快速開發,又可靈活定製

爲了實現此理念,CabloyJS內置開發了大量核心模塊,使您可以在最短的時間內架構一個完整的Web項目。比如,當您新建一個Web項目時,就已經具備完整的用戶登錄與認證系統,也具有驗證碼功能,同時也具備用戶管理角色管理權限管理等功能

此外,這些內置模塊提供了靈活的定製特性,您也可以開發全新的模塊來替換內置模塊,從而實現系統的定製化

1.2 CabloyJS核心解決什麼問題

  1. 場景碎片化
  2. 業務模塊化

1.2.1 場景碎片化

1) 先說說Mobile場景

我們知道,隨着智能機的日益普及,咱們開發人員所面對的需求場景與開發場景日益碎片化,如瀏覽器、IOS、Android,還有大量第三方平臺:微信、企業微信、釘釘、Facebook、Slack等等

隨着智能設備性能越來越好,網速越來越快,針對如此衆多的開發場景,採用H5開發必將是大勢所趨。只需開發一套代碼,就可以在以上所有智能設備中運行,不僅可以顯著減少開發量,同時也可以顯著提升開發效率,對開發團隊和終端用戶均是莫大的福利

2) 再來談談PC場景

以上咱們說H5開發,只需開發一套代碼,就可以在所有智能設備中運行。但是還有一個開發場景沒有得到統一:那就是PC場景

由於屏幕顯示尺寸的不同,PC場景Mobile場景有着不同的操作風格。有些前端UI框架,採用“自適應”策略,爲PC場景開發的頁面,在Mobile場景下雖然也能查看和使用,但使用體驗往往差強人意

這也就是爲什麼有些前端框架總是成對出現的原因:如Element-UI和Mint-UI,如AntDesign和AntDesign-Mobile

這也就意味着,當我們同時面對PC場景Mobile場景時,仍然需要開發兩套代碼。在面對許多開發需求時,這些重複的工作量往往是難以接受的:

  1. 比如,我們在企業微信或釘釘上開發一些H5業務應用,同時也希望這些應用也可以在PC端瀏覽器中運行
  2. 比如,我們爲微信公共號開發了一些H5業務應用,同時也希望這些應用也可以在PC端瀏覽器中運行。同時,還可以在同一架構下開發後臺管理類功能,通過區別不同的登錄用戶、不同的使用場景,從而顯示不同的前端頁面
3) PC = MOBILE + PAD

CabloyJS前端採用Framework7框架,目前已同步升級到最新版Framework7 V4。CabloyJS在Framework7的基礎上進行了巧妙的擴展,將PC端的頁面切分爲多個區域,實現了多個Mobile和PAD同時呈現在一個PC端的效果。換句話說,你買了一臺Mac,就相對於買了多臺IPhone和IPad,用多個虛擬的移動設備同時工作,即顯著提升了工作效率,也提供了非常有趣的使用體驗

4) 實際效果
有圖有真相

pc-mobile-layout

也可PC端體驗

https://admin.cabloy.com

也可手機掃描體驗

cabloy-demo-qrcode

5) 如何實現的

CabloyJS是模塊化的全棧框架,爲了實現PC = MOBILE + PAD的風格,內置了兩個模塊:egg-born-module-a-layoutmobileegg-born-module-a-layoutpc。當前端框架加載完畢,會自動判斷當前頁面的寬度(稱爲breakpoint),如果小於800,使用Mobile佈局,如果大於800,使用PC佈局,而且breakpoint數值可以自定義

此外,這兩個佈局模塊本身也有許多參數可以自定義,甚至,您也可以開發自己的佈局模塊,替換掉內置的實現方式

下面分別貼出兩個佈局模塊的默認參數,相信您一看便知他們的用處

egg-born-module-a-layoutmobile

export default {
  layout: {
    login: '/a/login/login',
    loginOnStart: true,
    toolbar: {
      tabbar: true, labels: true, bottom: true,
    },
    tabs: [
      { name: 'Home', tabLinkActive: true, iconMaterial: 'home', url: '/a/base/menu/list' },
      { name: 'Atom', tabLinkActive: false, iconMaterial: 'group_work', url: '/a/base/atom/list' },
      { name: 'Mine', tabLinkActive: false, iconMaterial: 'person', url: '/a/user/user/mine' },
    ],
  },
};

egg-born-module-a-layoutpc

export default {
  layout: {
    login: '/a/login/login',
    loginOnStart: true,
    header: {
      buttons: [
        { name: 'Home', iconMaterial: 'dashboard', url: '/a/base/menu/list', target: '_dashboard' },
        { name: 'Atom', iconMaterial: 'group_work', url: '/a/base/atom/list' },
      ],
      mine:
        { name: 'Mine', iconMaterial: 'person', url: '/a/user/user/mine' },
    },
    size: {
      small: 320,
      top: 60,
      spacing: 10,
    },
  },
};

1.2.2 業務模塊化

NodeJS的蓬勃發展,爲前後端開發帶來了更順暢的體驗,顯著提升了開發效率。但仍有網友質疑NodeJS能否勝任大型Web應用的開發。大型Web應用的特點是隨着業務的增長,需要開發大量的頁面組件。面對這種場景,一般有兩種解決方案:

  1. 採用單頁面的構建方式,缺點是產生的部署包很大
  2. 採用頁面異步加載方式,缺點是頁面過於零散,需要頻繁從後端獲取JS資源

CabloyJS實現了第三種解決方案:

  1. 頁面組件按業務需求歸類,進行模塊化,並且實現了模塊的異步加載機制,從而彌合了前兩種解決方案的缺點,完美滿足大型Web應用業務持續增長的需求

在CabloyJS中,一切業務開發皆以業務模塊爲單位。比如,我們要開發一個CMS建站工具,就新建一個業務模塊,如已經實現的模塊egg-born-module-a-cms。該CMS模塊包含十多個Vue頁面組件,在正式發佈時,就會構建成一個JS包。在運行時,只需異步加載這一個JS包,就可以訪問CMS模塊中任何一個Vue頁面組件了。

因此,在一個大型的Web系統中,哪怕有數十甚至上百個業務模塊,按CabloyJS的模塊化策略進行代碼組織和開發,既不會出現單一巨大的部署包,也不會出現大量碎片化的JS構建文件。

CabloyJS的模塊化系統還有如下顯著的特點:

1) 零配置、零代碼

也就是說,前面說到的模塊化異步打包策略是已經精心調校好的系統核心特性,我們只需像平時一樣開發Vue頁面組件,在構建時系統會自動進行模塊級別的打包,同時在運行時進行異步加載

我們仍然以CMS模塊爲例,通過縮減的代碼直觀的看一下代碼風格,如果想了解進一步的細節,可以直接查看對應的源碼(下同,不再贅述)

如何查看源碼:進入項目的node_modules目錄,查看egg-born-爲前綴的模塊源碼即可

egg-born-module-a-cms/src/module/a-cms/front/src/routes.js

function load(name) {
  return require(`./pages/${name}.vue`).default;
}

export default [
  { path: 'config/list', component: load('config/list') },
  { path: 'config/site', component: load('config/site') },
  { path: 'config/siteBase', component: load('config/siteBase') },
  { path: 'config/language', component: load('config/language') },
  { path: 'config/languagePreview', component: load('config/languagePreview') },
  { path: 'category/list', component: load('category/list') },
  { path: 'category/edit', component: load('category/edit') },
  { path: 'category/select', component: load('category/select') },
  { path: 'article/contentEdit', component: load('article/contentEdit') },
  { path: 'article/category', component: load('article/category') },
  { path: 'article/list', component: load('article/list') },
  { path: 'article/post', component: load('article/post') },
  { path: 'tag/select', component: load('tag/select') },
  { path: 'block/list', component: load('block/list') },
  { path: 'block/item', component: load('block/item') },
];

可以看到,在前端頁面路由的定義中,仍然是採用平時的同步加載寫法

關於模塊的異步加載機制是由核心模塊egg-born-front來完成的,參見源碼egg-born-front/src/base/module.js

2) 模塊自洽、即插即用

每個業務模塊都是自洽的整體,包含與本模塊業務相關的前端代碼和後端代碼,而且採用前後端分離模式

模塊自洽既有利於自身的高度內聚,也有利於整個系統的充分解耦。業務模塊只需要考慮自身的邏輯實現,容易實現業務的充分沉澱與分享,達到即插即用的效果

舉一個例子:如果我們要開發文件上傳功能,當我們在網上找到合適的上傳組件之後,在自己的項目中使用時,仍然需要開發大量對接代碼。也就是說,在網上找到的上傳組件沒有實現充分的沉澱,不是自洽的,也就不能實現便利的分享,達到即插即用的效果

而CabloyJS內置的的文件上傳模塊egg-born-module-a-file就實現了功能的充分沉澱。爲什麼呢?因爲業務模塊本身就包含前端代碼和後端代碼,能夠施展的空間很大,可以充分細化上傳邏輯

因此,在CabloyJS中要調用文件上傳功能,就會變得極其便捷。以CMS模塊爲例,上傳圖片並取得圖片URL,只需短短20行代碼

egg-born-module-a-cms/src/module/a-cms/front/src/pages/article/contentEdit.vue

...
    onUpload(mode, atomId) {
      return new Promise((resolve, reject) => {
        this.$view.navigate('/a/file/file/upload', {
          context: {
            params: {
              mode,
              atomId,
            },
            callback: (code, data) => {
              if (code === 200) {
                resolve({ text: data.realName, addr: data.downloadUrl });
              }
              if (code === false) {
                reject();
              }
            },
          },
        });
      });
    },
...
3) 模塊隔離

在大型Web項目中,不可避免的要考慮各類資源、各種變量、各個實體之間命名的衝突問題。針對這個問題,不同的開發團隊大都會規範各類實體的命名規範。隨着項目的擴充,這種命名規範仍然會變得很龐雜。如果我們面對的是一個開放的系統,使用的是來自不同團隊開發的模塊,所面臨的命名衝突的風險就會越發嚴重

CabloyJS使用了一個巧妙的設計,一勞永逸解決了命名衝突的隱患。在CabloyJS中,業務模塊採用如下命名規範:

egg-born-module-{providerId}-{moduleName}
  • providerId: 開發者Id,強烈建議採用Github的Username,從而確保貢獻到社區的模塊不會衝突
  • moduleName: 模塊名稱

由於模塊自洽的設計機制,我們只需要解決模塊命名的唯一性問題,在進行模塊開發時就不會再被命名衝突的困擾所糾纏了

比如,CMS模塊提供了一個前端頁面路由config/list。很顯然,如此簡短的路徑,在其他業務模塊中出現的概率非常高。但在CabloyJS中,如此命名就不會產出衝突。在CMS模塊內部進行頁面跳轉時,可以直接使用config/list,這稱之爲相對路徑引用。但是,如果其他業務模塊也想跳轉至此頁面就使用/a/cms/config/list,這稱之爲絕對路徑引用

再比如,前面的例子我們要調用上傳文件頁面,就是採用絕對路徑/a/file/file/upload

模塊隔離是業務模塊的核心特性。這是因爲,模塊前端和後端有大量實體都需要進行這種隔離。CabloyJS從系統層面完成了這種隔離的機制,從而使得我們在實際的模塊業務開發時可以變得輕鬆、便捷。

模塊前端隔離機制

模塊前端的隔離機制由模塊egg-born-front來完成,實現瞭如下實體的隔離:

  1. 前端頁面組件路由:參見
  2. 前端參數配置:參見
  3. 前端狀態管理:參見
  4. 前端國際化:參見
模塊後端隔離機制

模塊後端的隔離機制由模塊egg-born-backend來完成,實現瞭如下實體的隔離:

  1. 後端API接口路由:參見
  2. 後端Service:參見
後端Service隔離,不僅是解決命名衝突的需要,更是性能提升方面重要的考量。

比如有50個業務模塊,每個模塊有20個Service,這樣全局就有1000個Service。 在EggJS中,這1000個Service需要一次性預加載以便供Controller代碼調用。CabloyJS就在EggJS的基礎上做了隔離處理,如果是模塊A的Controller,只需要預加載模塊A的20個Service,供模塊A的Controller調用。這樣,就實現了一舉兩得:不僅命名隔離,而且性能提升,從而滿足大型Web系統開發的需求

  1. 後端Model:參見
後端Model是CabloyJS實現的訪問數據實體的便捷工具,在Model的定義和使用上,都比Sequelize簡潔、高效

與後端Service一樣,後端Model也實現了命名隔離,同時也只能被模塊自身的Controller和Service調用

  1. 後端參數配置:參見
  2. 後端Error處理:參見
  3. 後端國際化:參見
4) 快速的前端構建

CabloyJS採用WebPack進行項目的前端構建。由於CabloyJS項目是由一系列業務模塊組成的,因此,可以把模塊代碼提前預編譯,從而在構建整個項目的前端時就可以顯著提升構建速度

經實踐,如果一個項目包含40個業務模塊,如果按照普通的構建模式需要70秒構建完成。而採用預編譯的機制,則只需要20秒即可完成。這對於開發大型Web項目具有顯著的工程意義

5) 保護商業代碼

CabloyJS中的業務模塊,不僅前端代碼可以構建,後端代碼也可以用WebPack進行構建。後端代碼在構建時,也可以指定是否醜化,這種機制可以滿足保護商業代碼的需求

CabloyJS後端的基礎是EggJS,是如何做到可以編譯構建的呢?

CabloyJS後端在EggJS的基礎上進行了擴展,每個業務模塊都有一個入口文件main.js,通過main.js串聯後端所有JS代碼,因此可以輕鬆實現編譯構建

1.3 CabloyJS的開發歷程

1.3.1 兩階段

CabloyJS從2016年啓動開發,主要歷經兩個開發階段:

1) 第一階段:EggBornJS

EggBornJS關注的核心就是實現一套完整的以業務模塊爲核心的全棧開發框架

比如模塊egg-born-front是框架前端的核心模塊,模塊egg-born-backend是框架後端的核心模塊,模塊egg-born是框架的命令行工具,用於創建項目骨架

這也是爲什麼所有業務模塊都是以egg-born-module-爲命名前綴的原因

2) 第二階段:CabloyJS

EggBornJS只是一個基礎的全棧開發框架,如果要進行業務開發,還需要考慮許多與業務相關的支撐特性,如:用戶管理角色管理權限管理菜單管理參數設置管理表單驗證登錄機制,等等。特別是在前後端分離的場景下,對權限管理的要求就提升到一個更高的水平

CabloyJS在EggBornJS的基礎上,提供了一套核心業務模塊,從而實現了一系列業務支撐特性,並將這些特性進行有機的組合,形成完整而靈活的上層生態架構,從而支持具體的業務開發進程

換句話說,從實質上看,CabloyJS是一組核心業務模塊的組合,從形式上看,CabloyJS是一組模塊依賴項。且看CabloyJS的package.json文件:

cabloy/package.json

{
  "name": "cabloy",
  "version": "2.1.2",
  "description": "The Ultimate Javascript Full Stack Framework",
  ...
  "author": "zhennann",
  "license": "ISC",
  ...
  "dependencies": {
    "egg-born-front": "^4.1.0",
    "egg-born-backend": "^2.1.0",
    "egg-born-bin": "^1.2.0",
    "egg-born-scripts": "^1.1.0",
    "egg-born-module-a-version": "^2.2.2",
    "egg-born-module-a-authgithub": "^2.0.3",
    "egg-born-module-a-authsimple": "^2.0.3",
    "egg-born-module-a-base-sync": "^2.0.10",
    "egg-born-module-a-baseadmin": "^2.0.3",
    "egg-born-module-a-cache": "^2.0.3",
    "egg-born-module-a-captcha": "^2.0.4",
    "egg-born-module-a-captchasimple": "^2.0.3",
    "egg-born-module-a-components-sync": "^2.0.5",
    "egg-born-module-a-event": "^2.0.2",
    "egg-born-module-a-file": "^2.0.2",
    "egg-born-module-a-hook": "^2.0.2",
    "egg-born-module-a-index": "^2.0.2",
    "egg-born-module-a-instance": "^2.0.2",
    "egg-born-module-a-layoutmobile": "^2.0.2",
    "egg-born-module-a-layoutpc": "^2.0.2",
    "egg-born-module-a-login": "^2.0.2",
    "egg-born-module-a-mail": "^2.0.2",
    "egg-born-module-a-markdownstyle": "^2.0.3",
    "egg-born-module-a-mavoneditor": "^2.0.2",
    "egg-born-module-a-progress": "^2.0.2",
    "egg-born-module-a-sequence": "^2.0.2",
    "egg-born-module-a-settings": "^2.0.2",
    "egg-born-module-a-status": "^2.0.2",
    "egg-born-module-a-user": "^2.0.3",
    "egg-born-module-a-validation": "^2.0.4",
    "egg-born-module-test-cook": "^2.0.2"
  }
}
相信您通過這些核心模塊的名稱,就已經猜到這些模塊的用處了

1.3.2 整體架構圖

根據前面兩階段的分析,我們就可以勾勒出框架的整體架構圖

cabloy

這種架構,讓整個體系變得層次分明,也讓實際的Web項目的源代碼文件組織結構變得非常簡潔直觀。大量的架構細節都封裝在EggBornJS中,而我們的Web項目只需要引用一個CabloyJS即可,CabloyJS負責引用架構中其他核心模塊

這種架構,也讓實際的Web項目的升級變得更加容易,具體如下:

1) 刪除現有模塊依賴項
$ rm -rf node_modules
2) 如果有此文件,建議刪除
$ rm -rf package-lock.json 
3) 重新安裝所有模塊依賴項
$ npm i

1.3.3 意義

有了EggBornJS,從此可複用的不僅僅是組件,還有業務模塊

有了CabloyJS,您就可以快速開發各類業務應用

2 數據版本與開發流程

業務模塊必然要處理數據並且存儲數據,當然也不可避免會出現數據架構的變動,比如新增表、新增字段、刪除字段、調整舊數據,等等

CabloyJS通過巧妙的數據版本控制,可以讓業務模塊在不斷的迭代過程中,無縫的完成模塊升級和數據升級

在數據版本的基礎上,再配合一套開發流程,從而不論是在開發環境還是生產壞境,都能有順暢的開發與使用體驗

2.1 數據版本

2.1.1 數據版本定義

可以通過package.json指定業務模塊的數據版本,以模塊egg-born-module-test-cook爲例

egg-born-module-test-cook/package.json

{
  "name": "egg-born-module-test-cook",
  "version": "2.0.2",
  "eggBornModule": {
    "fileVersion": 1,
    "dependencies": {
      "a-base": "1.0.0"
    }
  },
  ...
}

模塊當前的數據版本fileVersion1。當這個模塊正式發佈出去之後,爲1的數據版本就處於封閉狀態。當有新的迭代,需要改變模塊的數據架構時,就需要將fileVersion遞增爲2。以此類推,從而完成模塊數據架構的自動無縫升級

2.1.1 數據版本升級

當CabloyJS後端服務在啓動時,會自動檢測每個業務模塊的數據版本,當存在數據版本變更時,就會自動調用業務模塊的升級代碼,從而完成自動升級。仍以模塊egg-born-module-test-cook爲例,其數據版本升級代碼如下:

egg-born-module-test-cook/backend/src/service/version.js

...
    async update(options) {
      if (options.version === 1) {
        let sql = `
          CREATE TABLE testCook (
            id int(11) NOT NULL AUTO_INCREMENT,
            createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updatedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            deleted int(11) DEFAULT '0',
            iid int(11) DEFAULT '0',
            atomId int(11) DEFAULT '0',
            cookCount int(11) DEFAULT '0',
            cookTypeId int(11) DEFAULT '0',
            PRIMARY KEY (id)
          )
        `;
        await this.ctx.model.query(sql);

        sql = `
          CREATE TABLE testCookType (
            id int(11) NOT NULL AUTO_INCREMENT,
            createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updatedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            deleted int(11) DEFAULT '0',
            iid int(11) DEFAULT '0',
            name varchar(255) DEFAULT NULL,
            PRIMARY KEY (id)
          )
        `;
        await this.ctx.model.query(sql);

        sql = `
          CREATE VIEW testCookView as
            select a.*,b.name as cookTypeName from testCook a
              left join testCookType b on a.cookTypeId=b.id
        `;
        await this.ctx.model.query(sql);

        sql = `
          CREATE TABLE testCookPublic (
            id int(11) NOT NULL AUTO_INCREMENT,
            createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updatedAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            deleted int(11) DEFAULT '0',
            iid int(11) DEFAULT '0',
            atomId int(11) DEFAULT '0',
            PRIMARY KEY (id)
          )
        `;
        await this.ctx.model.query(sql);
      }
    }
...

當數據版本變更時,CabloyJS後端調用方法update,通過判斷屬性options.version的值,進行對應版本的數據架構變更

2.2 開發流程

2.2.1 背景

那麼問題來了?在模塊開發階段,如果需要變更數據架構怎麼辦呢?因爲模塊還沒有正式發佈,所以,不需要鎖定數據版本。也就是說,如果當前數據版本fileVersion1,那麼在正式發佈之前,不論進行多少次數據架構變更,fileVersion仍是1

一方面,我們肯定要修改方法update,加入架構變更的代碼邏輯,比如添加表、添加字段等等

另一方面,我們還要修改當前測試數據庫中的數據架構。因爲fileVersion是沒有變化的,所以當重啓CabloyJS後端服務時,方法update並不會再次執行

針對這種情況,首先想到的是手工修改測試數據庫中的數據架構。而CabloyJS提供了更優雅的機制

2.2.2 運行環境

我們知道EggJS提供了三個運行環境:測試環境開發環境生產環境。CabloyJS在EggJS的基礎上,對這三個運行環境賦予了進一步的意義

1) 測試環境
  • 測試環境的參數配置如下

{項目目錄}/src/backend/config/config.unittest.js

module.exports = appInfo => {
  const config = {};
  ...
  // mysql
  config.mysql = {
    clients: {
      // donnot change the name
      __ebdb: {
        host: '127.0.0.1',
        port: '3306',
        user: 'root',
        password: '',
        database: 'sys', // donnot change the name
      },
    },
  };
  ...
  return config;
};
  • 命令行如下:
$ npm run test:backend

由於我們將測試環境的數據庫名稱設爲sys,那麼CabloyJS就會自動刪除舊的測試數據庫,建立新的數據庫。因爲是重新創建數據庫,那麼也就意味着fileVersion0升級爲1,從而觸發方法update的執行,進而自動完成數據架構的升級

2) 開發環境
  • 開發環境的參數配置如下

{項目目錄}/src/backend/config/config.local.js

module.exports = appInfo => {
  const config = {};
  ...
  // mysql
  config.mysql = {
    clients: {
      // donnot change the name
      __ebdb: {
        host: '127.0.0.1',
        port: '3306',
        user: 'root',
        password: '',
        database: 'sys', // recommended
      },
    },
  };
  ...
  return config;
};
  • 命令行如下:
$ npm run dev:backend

雖然我們也將開發環境的數據庫名稱設爲sys,但是CabloyJS會自動尋找最新創建的測試數據庫,然後一直使用它

3) 生產環境
  • 生產環境的參數配置如下

{項目目錄}/src/backend/config/config.prod.js

module.exports = appInfo => {
  const config = {};
  ...
  // mysql
  config.mysql = {
    clients: {
      // donnot change the name
      __ebdb: {
        host: '127.0.0.1',
        port: '3306',
        user: 'root',
        password: '',
        database: '{實際數據庫名}',
      },
    },
  };
  ...
  return config;
};
  • 命令行如下:
$ npm run start:backend

因爲生產環境存儲的都是實際業務數據,所以在生產環境就要設置實際的數據庫名稱了

2.2.3 開發流程的最佳實踐

根據前面數據版本運行環境的分析,我們就可以規劃出一套關於開發流程的最佳實踐:

  1. 當項目創建後,先執行一次npm run test:backend,用於自動創建一個測試數據庫
  2. 在進行常規開發時,執行npm run dev:backend來啓動項目後端服務,用於調試
  3. 如果模塊數據版本需要變更,在修改完屬性fileVersion和方法update之後,再一次執行npm run test:backend,從而重建一個新的測試數據庫
  4. 當項目需要在生產環境運行時,則運行npm run start:backend來啓動後端服務

3 特性鳥瞰

3.1 多實例與多域名

CabloyJS通過多實例的概念來支持多域名站點的開發。啓動一個服務,可以支持多個實例運行。實例共享數據表架構,但運行中產生的數據是相互隔離的

這有什麼好處呢?比如您用CabloyJS開發了一款CRM的SAAS服務,那麼只需開發並運行一個服務,就可以同時服務多個不同的客戶。每個客戶一個實例,用一個單獨的域名進行區分即可。

再比如,要想開發一款基於微信公共號的營銷平臺,提供給不同的客戶使用,多實例與多域名是最自然、最有效的架構設計。

具體信息,請參見

3.2 數據庫事務

3.2.1 EggJS事務處理方式

const conn = await app.mysql.beginTransaction(); // 初始化事務

try {
  await conn.insert(table, row1);  // 第一步操作
  await conn.update(table, row2);  // 第二步操作
  await conn.commit(); // 提交事務
} catch (err) {
  // error, rollback
  await conn.rollback(); // 一定記得捕獲異常後回滾事務!!
  throw err;
}

3.2.2 CabloyJS事務處理方式

CabloyJS在EggJS的基礎上進行了擴展,使得數據庫事務處理變得更加自然,甚至可以說是無痛處理

在CabloyJS中,實際的代碼邏輯不用考慮數據庫事務,如果哪個後端API路由需要啓用數據庫事務,直接在API路由上聲明一箇中間件transaction即可,以模塊egg-born-module-test-cook爲例

egg-born-module-test-cook/backend/src/routes.js

...
  { method: 'get', path: 'test/echo/:id', controller: test, action: 'echo', middlewares: 'transaction' },
...

3.3 完美的用戶與身份認證分離體系

3.3.1 通用的身份認證

CabloyJS把用戶系統身份認證系統完全分離,有如下好處:

  1. 支持衆多身份認證機制:用戶名/密碼認證、手機認證、第三方認證(Github、微信)等等
  2. 可完全定製登錄頁面,自由組合各種身份認證機制
  3. 網站用戶也可以自由添加不同的身份認證機制,也可以自由的刪除
比如,用戶A先通過用戶名/密碼註冊的身份,以後還可以添加Github、微信等認證方式

比如,用戶B先通過Github註冊的身份,以後還可以添加用戶名/密碼等認證方式

3.3.2 通用的驗證碼機制

CabloyJS把驗證碼機制抽象了出來,並且提供了一個缺省的驗證碼模塊egg-born-module-a-captchasimple,您也可以按統一規範開發自己的驗證碼模塊,然後掛接到系統中

3.3.3 通用的郵件發送機制

CabloyJS也實現了通用的郵件發送功能,基於成熟的nodemailer。由於nodemailer內置了一個測試服務器,因此,在開發環境中,不需要真實的郵件發送賬號,也可以進行系統的測試與調試

3.4 模塊編譯與發佈

前面我們談到CabloyJS中的業務模塊是自洽的,可以單獨編譯打包,既可以顯著提升整體項目打包的效率,也可以滿足保護商業代碼的需求。這裏我們看看模塊編譯與發佈的基本操作

3.4.1 如何編譯模塊

$ cd /path/to/module
  1) 構建前端代碼
$ npm run build:front
  2) 構建後端代碼
$ npm run build:backend

3.4.2 編譯參數

  1. 前端編譯:爲了提升整體項目打包的效率,模塊前端編譯默認開啓醜化處理
  2. 後端編譯:默認關閉醜化處理,可通過修改編譯參數開啓醜化選項
後端爲什麼默認關閉醜化選項呢?

答:CabloyJS所有內置的核心模塊都是關閉醜化選項的,這樣便於您直觀的調試整個系統的源代碼,也可以很容易走進CabloyJS,發現一些更有趣的架構設計

{模塊目錄}/build/config.js

module.exports = {
  productionSourceMap: true,
  uglify: false,
};

3.4.3 模塊發佈

當項目中的模塊代碼穩定後,可以將模塊公開發布,貢獻到開源社區。也可以在公司內部建立npm私有倉庫,然後把模塊發佈到私有倉庫,形成公司資產,便於重複使用

$ cd /path/to/module
$ npm publish

4 業務開發

到目前爲止,實話說,前面談到的概念大多屬於EggBornJS的層面。CabloyJS在EggBornJS的基礎上,開發了大量核心業務模塊,從而支持業務層面的快速開發。下面我們就介紹一些基本概念

4.1 原子的概念

4.1.1 原子是什麼

原子是CabloyJS最基本的要素,如文章、公告、請假單,等等

爲什麼叫原子?在化學反應中,原子是最基本的粒子。在CabloyJS中,通過原子的組合,就可以實現任何想要的功能,如CMS、OA、CRM、ERP,等等

比如,您所看到的這篇文章就是一個原子

4.1.2 原子的意義

正由於從各種業務模型中抽象出來一個通用的原子概念,因而,CabloyJS爲原子實現了許多通用的特性和功能,從而可以便利的爲各類實際業務賦能

比如,模塊CMS中的文章可以發表評論,可以點贊,支持草稿搜索功能。這些都是CabloyJS核心模塊egg-born-module-a-base-sync提供的通用特性與功能。只要新建一個原子類型,這些原子都會被賦能

這就是抽象的力量

4.1.3 統一存儲

所有原子數據都會有一些相同的字段屬性,也會有與業務相關的字段屬性。相同的字段都統一存儲到數據表aAtom中,與業務相關的字段存儲在具體的業務表中,aAtom業務表是一對一的關係

這種存儲機制體現了共性差異性的有機統一,有如下好處:

  1. 可統一配置數據權限
  2. 可統一支持增刪改查等操作
  3. 可統一支持星標、標籤、草稿、搜索等操作

關於原子的更多信息,請參見

4.2 角色體系

角色是面向業務系統開發最核心的功能之一,CabloyJS提供了既簡潔又靈活的角色體系

4.2.1 角色模型

CabloyJS的角色體系不同於網上流行的RBAC模型

RBAC模型沒有解決業務開發中資源範圍授權的問題。比如,Mike是軟件部的員工,只能查看自己的日誌;Jone是軟件部經理,可以查看本部門的日誌;Jimmy是企業負責人,可以查看整個企業的日誌

RBAC模型概念複雜,在實際應用中,又往往引入新的概念(用戶組、部門、崗位等),使得角色體系疊牀架屋,理解困難,維護繁瑣

4.2.2 概念辨析

涉及到角色體系,往往會有這些概念:用戶用戶組角色部門崗位授權對象等等

而CabloyJS設計的角色體系只有用戶角色授權對象等概念,概念精簡,層次清晰,靈活高效,既便於理解,又便於維護

1) 部門即角色

部門從本質上來說,其實就是角色,如:軟件部財務部等等

2) 崗位即角色

崗位從本質上來說,其實也就是角色,如:軟件部經理軟件部設計崗軟件部開發崗等等

3) 資源範圍即角色

資源範圍也是角色。如:Jone是軟件部經理,可以查看軟件部的日誌。其中,軟件部就是資源範圍

4.2.3 角色樹

CabloyJS針對各類業務開發的需求,提煉了一套內置角色,並形成一個規範的角色樹。實際開發中,可通過對角色樹的擴充和調整,滿足各類角色相關的需求

  • root

    • anonymous
    • authenticated

      • template
      • registered
      • activated
      • superuser
      • organization

        • internal
        • external
名稱 說明
root 角色根節點,包含所有角色
anonymous 匿名角色,凡是沒有登錄的用戶自動歸入匿名角色
authenticated 認證角色
template 模版角色,可爲模版角色配置一些基礎的、通用的權限
registered 已註冊角色
activated 已激活角色
superuser 超級用戶角色,如用戶root屬於超級用戶角色
organization 組織角色
internal 內部組織角色,如可添加軟件部財務部等子角色
external 外部組織角色,可爲合作伙伴提供角色資源

4.3 API接口權限

CabloyJS是前後端分離的模式,對API接口權限的控制需求就提升到一個更高的水平。CabloyJS提供了一個非常自然直觀的權限控制方式

比如模塊egg-born-module-a-baseadmin有一個API接口role/children,是要查詢某角色的子角色清單。這個API接口只允許管理員用戶訪問,我們可以這樣做

4.3.1 功能與API接口的關係

我們把需要授權的對象抽象爲功能。這樣處理有一個好處:就是一個功能可以綁定1個或多個API接口。當我們對一個功能賦予了權限,也就對這一組綁定的API接口進行了訪問控制

4.3.2 功能定義

先定義一個功能role

egg-born-module-a-baseadmin/backend/src/meta.js

...
      functions: {
        role: {
          title: 'Role Management',
        },
      },
...  

4.3.3 功能綁定

再將功能API接口綁定

egg-born-module-a-baseadmin/backend/src/routes.js

...
  { method: 'post', path: 'role/children', controller: role,
  meta: { right: { type: 'function', name: 'role' } }
},
...
名稱 說明
right 全局中間件right,默認處於開啓狀態,只需配置參數即可
type function: 判斷功能授權
name 功能的名稱

4.3.4 功能授權

接下來,我們就需要把功能role授權給角色superuser,而管理員用戶歸屬於角色superuser,也就擁有了訪問API接口role/children的權限

功能授權有兩種途徑:

  1. 調用API直接授權
  2. CabloyJS已經實現了功能授權的管理界面:用管理員身份登錄系統,進入工具 > 功能權限管理,進行授權配置即可

4.4 數據訪問權限

前面談到,針對各類業務數據,CabloyJS抽象出來原子的概念。對數據訪問授權,也就是對原子授權

原子授權主要解決這類問題:能對哪個範圍內原子數據執行什麼操作,基本格式如下:

角色 原子類型 原子指令 資源範圍
superuser todo read 財務部
角色superuser僅能讀取財務部todo數據

更詳細信息,強烈建議參見

4.5 簡單流程

在實際的業務開發中,難免會遇到一些流程需求。比如,CMS中的文章,在作者提交之後,可以轉入審覈員進行審覈,審覈通過之後方能發佈

當原子數據進入流程時,在不同的節點,處於不同的狀態(審覈中、已發佈),只能由指定的角色進行節點的操作

CabloyJS通過原子標記原子指令的配合實現了一個簡單的流程機制。也就是說,對於大多數簡單流程場景,不需要複雜的流程引擎,就可以在CabloyJS中很輕鬆的實現

更詳細信息,強烈建議參見

5 解決方案

前面說到CabloyJS研發經歷了兩個階段:

  1. EggBornJS
  2. CabloyJS

如果說還有第三階段的話,那就是解決方案階段。EggBornJS構建了完整的NodeJS全棧開發體系,CabloyJS提供了大量面向業務開發的核心模塊。那麼,在EggBornJS和CabloyJS的基礎上,接下來就可以針對不同的業務場景,研發相應的解決方案,解決實際的業務問題

5.1 Cabloy-CMS

CabloyJS是一個單頁面、前後端分離的框架,而有些場景(如博客社區等)更看重SEO、靜態化

CabloyJS針對這類場景,專門開發了一個模塊egg-born-module-a-cms,提供了一個文章的靜態渲染機制。CabloyJS本身天然的成爲CMS的後臺管理系統,從而形成動靜結合的特點,主要特性如下:

  • 內置多站點、多語言支持
  • 不同語言可單獨設置主題
  • 內置SEO優化,自動生成Sitemap文件
  • 文章在線撰寫、發佈
  • 文章發佈時實時渲染靜態頁面,不必整站輸出,提升整體性能
  • 內置文章查看計數器
  • 內置評論系統
  • 內置全文檢索
  • 文章可添加附件
  • 自動合併並最小化CSS和JS
  • JS支持ES6語法,並在合併時自動Babel編譯
  • 首頁圖片延遲加載,自動匹配設備像素比
  • 調試便捷

具體信息,請參見

5.2 Cabloy-Community

CabloyJS以CMS模塊爲基礎,開發了一個社區模塊egg-born-module-cms-sitecommunity,配置方式與CMS模塊完全一樣,只需選用不同的社區主題即可輕鬆搭建一個交流社區(論壇)

6 未來規劃與社區建設

Atwood定律: 凡是可以用JavaScript來寫的應用,最終都會用JavaScript來寫

CabloyJS未來規劃的核心之一,就是持續輸出高質量的解決方案,爲提升廣大研發團隊的開發效率不懈努力

CabloyJS以及所有核心模塊均已開源,歡迎大家加入CabloyJS,發Issue,點Star,提PR,更希望您能開發更多的業務模塊,共建CabloyJS的繁榮生態

7 名稱由來

最後再來聊聊框架名稱的由來

7.1 EggBornJS

這個名稱的由來比較簡單,因爲有了Egg,所以就有了EggBorn。有一部動畫片叫《天書奇譚》,裏面的萌主就叫“蛋生”,我很喜歡看(不小心暴露了年齡😅)

7.2 CabloyJS

Cabloy來自藍精靈的魔法咒語,只有拼對了Cabloy這個單詞纔會有神奇的效果。同樣,CabloyJS是有關JS的魔法,基於模塊的組合與生化反應,您將實現您想要的任何東西

8 結語

親,您也可以拼對Cabloy吧!這可是神奇的魔法喲!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章