基於NX開發Angular專案

AaronJin發表於2018-01-25

專案依賴的安裝

Node和NPM的安裝

​ 網上教程較多,可以自行搜尋,下面給出2個連結:

mac的安裝

pc的安裝

NX的安裝

​ 全域性安裝NX CLI

npm install -g @nrwl/schematics
複製程式碼

​ 全域性安裝Angular CLI

npm install -g @angular/cli
複製程式碼

官網教程

ng-packagr的安裝

​ 在專案中安裝

> npm i ng-packagr --save-dev
複製程式碼

github

建立一個workspace專案

首先通過nx命令列建立一個workspace

> create-nx-workspace example
複製程式碼

​ 該過程中會自動安裝所需的各項依賴,請確保通過nx命令建立workspace,避免因使用webpack或ng-cli建立專案,帶來各項依賴的額外安裝。

當nx workspace建立之後,我們需要在example的app中建立對應的專案

> ng generate app myapp --routing
複製程式碼

​ 其中myapp是專案名稱,—routing是自動增加路由依賴引數,由於專案會使用到路由,因此我們在建立專案時帶上這個引數。如果忘記帶上routing引數,也可以後續手動新增,不影響實際開發。

有些時候當nx或一些依賴的版本發生更新時,會出現一些預料之外的錯誤

Error: Cannot find module '~/example/node_modules/prettier/bin/prettier.js'
複製程式碼

此時可以參考官網issue,此類問題一般是由於依賴更新導致的,更新一下依賴就可以解決類似問題

> npm install prettier@1.10.1 -D
複製程式碼

​ 當專案建立完成之後,我們可以看到在apps目錄下建立了myapp這個目錄,以及完整的程式碼結構。同時我們在vscode中會發現.angular-cli.json被更新了,其中被加入myapp的專案配置,一般情況下不需要去修改這些配置。

{
      "name": "myapp",
      "root": "apps/myapp/src",
      "outDir": "dist/apps/myapp",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "../../../test.js",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "../../../tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
複製程式碼

建立完myapp之後,我們可以先執行一下例項,看看是否能夠跑通

> ng serve -a=myapp
複製程式碼

​ 鍵入命令列之後,我們能夠通過http://localhost:4200/地址來進行檢視,4200是ng serve的預設埠,也可以通過 -port=埠號 進行修改。-a=myapp是指定執行myapp,如果有其他專案也是用相同的方式來開啟。當我們愉快的看見瀏覽器中的NX標誌後,意味著我們的專案已經跑通了。

為APP加點料

建立為myapp服務的lib

​ 在沒有workspace概念之前,一般的開發過程是先在app中實施,而後再將完成的程式碼拆分到另一個庫,並且打成NPM包。這個方法及其難以維護,即使通過git的submodule來做處理依然不是非常的流暢。而現在NX為我們提供了一種快捷的實現方式。

>ng g lib myapp-feature --routing
複製程式碼

​ 幾乎和建立app同樣的方式,我們在libs目錄下建立了名為myapp-feature的lib,因為需要路由,我們把路由也帶上了。觀察一下.angular-cli.json,同樣,新的lib也被更新了。

    {
      "name": "myapp-feature",
      "root": "libs/myapp-feature/src",
      "test": "../../../test.js",
      "appRoot": ""
    }
複製程式碼

​ 接下來就是在myapp中引用lib,NX為我們提供了一個方便的別名引用方式。

import { MyappFeatureModule } from '@ebiz-example/myapp-feature';
複製程式碼

​ 我們在app.module.ts下加入引用,其中ebiz-example是我們在package.json的name配置項的值,myapp-feature是libs下的目錄。在app和lib的module下的constructor裡分別新增console,然後執行app,我們可以發現lib已經被順利引入。

為myapp新增基於ngrx的state manage

​ 對於Component間的通訊,我們需要尋找一個state manage的方案,而繼承了redux思路的ngrx是個非常不錯的選擇,很幸運的,NX為我們整合了快速新增ngrx的方式。

> ng g ngrx root -m=apps/myapp/src/app/app.module.ts --root
複製程式碼

​ 命令完成後,我們會發現在app目錄下多出了+state目錄,因為使用了-m所以在app.module.ts檔案中為我們加入了相應的程式碼,—root則為我們指定了使用forRoot方式。

import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { rootReducer } from './+state/root.reducer';
import { rootInitialState } from './+state/root.init';
import { RootEffects } from './+state/root.effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { StoreRouterConnectingModule } from '@ngrx/router-store';

@NgModule({
  imports: [
    StoreModule.forRoot({ root: rootReducer }, { initialState: { root: rootInitialState } }),
    EffectsModule.forRoot([RootEffects]),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
    StoreRouterConnectingModule
  ],
})

複製程式碼

​ 其中不僅有@ngrx/store和@ngrx/effects,Devtools也為我們準備好了,接下來啟動一下伺服器,開啟Chrome的Redux Devtools,我們可以看到ngrx已經順利的啟動了。

​ 除了app之外,lib也會有state manage的需求,我們一樣可以通過ng-cli來進行新增,我們先為lib新增一個module,併為module新增state。

> ng g module subfeature -a=myapp-feature —routing
> ng g component subfeature -a=myapp-feature
> ng g ngrx userinfo -m=libs/myapp-feature/src/subfeature/subfeature.module.ts
複製程式碼

​ 我們順利的新增了module和component,如果需要自動注入myapp-feature,也可以加上-m來做到。在新增了ngrx後,同樣的+state被加入了程式碼,其中在subfeature.module.ts有個細微的差別是需要注意的。

@NgModule({
  imports: [
    StoreModule.forFeature('userinfo', userinfoReducer, { initialState: userinfoInitialState }),
    EffectsModule.forFeature([UserinfoEffects])
  ]
})
複製程式碼

​ 在子module中,我們應該使用forFeature,只有在根module中我們才會使用—root。

​ 將subfeature注入myapp-feature再次啟動服務,我們開啟Redux Devtools,可以確認userinfo已經被加入到state tree。

為分享Lib做準備

將開發成熟的Lib交付出去

​ 當專案開發到一定階段,很多時候,我們會希望將libs目錄下的功能模組分享給其他的團隊來使用,如果是希望將程式碼分享出去,比較建議的方式是使用git的submodule,具體命令可以參考help。

> git submodule --help
複製程式碼

將程式碼封包並進行管理

​ 有的時候我們並不希望其他團隊來修改我們的程式碼,因此將編制完成的Lib打包成NPM,提供給對方進行依賴是一個比較好的方案。

​ 對於Private Lib,一般我們不會提交給Public repo,因此你需要自己搭建一個NPM repo,比如Nexus。當Nexus安裝完後,我們還需要對package.json進行設定,主要是2個配置項。

  "publishConfig": {
    "registry": "http://private/repository/ebiz-npm-private/"
  },
複製程式碼
  "private": false,
複製程式碼

​ 這2項配置確保了你的npm包是可以釋出的,並被正確釋出到Private NPM repo。

使用ng-packagr來進行打包

​ 如果是一個es的函式庫,我們可以很愉快的直接使用npm publish命令,如果是一個ts的函式庫,我們也可以先執行一個tsc進行編譯,然後愉快的npm publish。

​ 然而作為一個angular的lib,特別是帶有各種Component的lib,這個過程就會麻煩許多。我們都知道Component的templateUrl和styleUrls都是以字元來獲取相應的資原始檔,而這在打包時會顯然會領我們變得不那麼愉快,因此需要進行額外處理,比如用gulp預處理程式碼,而後打包。

​ 幸運的是,我們還能使用ng-packagr來進行打包,首先我們來新建ng-package.json檔案。

{
  "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
  "lib": {
    "entryFile": "public_api.ts",
    "externals": {
      "primeng/primeng": "primeng",
      "primeng/components/common/messageservice":
        "primeng/components/common/messageservice",
      "rxjs": "Rx",
      "@nrwl/nx": "@nrwl/nx",
      "@ngrx":"@ngrx",
      "@ngrx/store":"@ngrx/store",
      "@ngrx/store-devtools":"@ngrx/store-devtools",
      "@ngrx/effects":"@ngrx/effects"
    }
  }
}
複製程式碼

​ 這裡定義了ng-packagr的schema,以及一些外部引用時希望能正確認知的第三方庫。有一個比較關鍵的是entryFile,這個配置項是我們設定的一個入口,為此我們新建了一個public_api.ts檔案。

	export * from "./libs/myapp-feature";
複製程式碼

​ 這裡我們可以指定將一個或多個lib打入一個NPM包中,所以你希望打包什麼就在這個檔案進行定義吧。

​ 在這2個檔案定義完成之後,我們可以執行打包命令了,先將這個命令寫進package.json中,方便執行。

"scripts": {
    "packagr": "ng-packagr -p ng-package.json",
    }
複製程式碼

​ 執行npm run packagr,有的時候一些引用可能會查詢錯誤,我們需要顯式的引用這些依賴。

BUILD ERROR
Error at /Users/aaronjin/eBizprise/ebiz-example/.ng_pkg_build/ebiz-example/ts/libs/myapp-feature/src/subfeature/+state/userinfo.effects.ts:11:3: Public property 'loadData' of exported class has or is using name 'Observable' from external module "/Users/aaronjin/eBizprise/ebiz-example/node_modules/rxjs/Observable" but cannot be named.
複製程式碼

​ 比如這樣的錯誤,就需要去新增引用。

import { Observable } from 'rxjs/Observable';
複製程式碼

​ 有些可以通過ng-package.json的externals配置來解決,基本上ng-packagr在打包時會碰到的問題,都是類似第三方的載入不能判別。

釋出到Nexus

​ 如果程式碼沒有問題,你可以在專案下找到dist目錄了,這個就是我們打包出來的程式碼了。

​ 程式碼的命名是根據package.json裡的name來設定的,我們不需要去修改它,檢視dist目錄下的package.json。

    "name": "ebiz-example",
    "version": "0.0.0",
    "license": "MIT",
    "publishConfig": {
        "registry": "http://dkh01.ebizprise.com/repository/ebiz-npm-private/"
    },
複製程式碼

​ 我們將ebiz-example修改為@ebiz-ex/feature,以區分線上和本地專案,並且將version修改為0.0.1,,當然也是可以在pre-publish用npm version patch來提升版本。

​ 然後我們執行npm publish dist,返回瞭如下資訊,我們就會發現NPM已經發布成功了。

+ @ebiz-ex/feature@0.0.1
複製程式碼

通過NPM引用打包好的lib

​ 使用Private lib之前,我們要切換到Nexus伺服器,平時則可以使用標準官網地址來加快訪問速度

> npm config set registry  http://private/repository/ebiz-npm-all/
或
> npm config set registry  http://registry.npmjs.org
複製程式碼

​ 使用npm命令將已經打包好的lib包引入專案。

> npm i @ebiz-ex/feature --save
複製程式碼

​ 將app.module.ts中的引用進行修改。

import { MyappFeatureModule } from '@ebiz-ex/feature';
複製程式碼

​ 再次執行ng server,我們發現使用打包後的NPM包和我們使用在nx專案中的lib包是一致的。

通過檔案引用打包好的lib

​ 有些時候我們沒有Private repo,那麼能不能用打包好的NPM哪?答案是肯定的,把dist目錄打包成tgz,然後放在專案中,新增package.json的引用。

        "@ebiz-ex/feature": "file:.feature-0.0.1.tgz",
複製程式碼

​ 將app.module.ts中的引用進行修改。

import { MyappFeatureModule } from '@ebiz-ex/feature';
複製程式碼

​ 再次執行ng server,我們發現使用打包後的NPM包和我們使用在nx專案中的lib包是一致的。

是否使用這套方案

Mono Repo or Muti Repo

​ 兩個方案的優劣不需要再做過多闡述,這裡只想說明NX是一個基於Mono Repo的Workspace方案,如果專案更偏向Muti Repo,則完全不要考慮NX。

​ NX帶來最大的優勢是統一管理,易於部署,並且不需要為不同的App或Lib反覆同步NPM包,並且可以較為方便的將程式碼拆分開。如果這些能直擊痛點,那麼不要猶豫的選擇NX吧。

NX的侵入性

​ NX整合了較多的功能,有些或許是未必需要用到的,比如不使用@ngrx或者不想使用@ngrx/effects,這些都是需要考慮的範疇。不過NX最大的問題在於對ng-cli的侵入,我們檢視package.json,可以發現使用了file的呼叫方式。

"@angular/cli": "file:.angular_cli.tgz",
複製程式碼

​ 而這一系列的侵入導致了另一個問題,當Lib處於專案下時,可以正常的使用AOT進行編譯,而當我們用ng-packagr進行NPM打包之後再引入,進行AOT就會出現問題。

ERROR in ./apps/myapp/src/main.ts
Module not found: Error: Can't resolve './app/app.module.ngfactory' in '/Users/aaronjin/eBizprise/ebiz-example/apps/myapp/src'
resolve './app/app.module.ngfactory' in '/Users/aaronjin/eBizprise/ebiz-example/apps/myapp/src'
複製程式碼

​ 這個問題是由於ng、ts的版本衝突引起,具體的解決方案仍未找到,因此使用這套方案仍需謹慎。

ng-packagr的不足

​ 這個打包方案除了版本衝突的可能,仍有一些其他的不足,比如使用ng router的loadchildren非同步機制,就會出現這樣的後設資料丟失情況。

No NgModule metadata found for 'MyappFeatureModule'.
複製程式碼

github地址

NX-packagr

相關文章