Angular、TypeScript實作前端MVC架構
前言
在上卷中,我們已經安裝好必要的套件,並且將資料夾目錄都已建立完畢。本卷的重點在如何實作職責分離,以及將此架構結合入原本的ASP.NET MVC框架。
職責分離
從TypeScripts的資料夾內起始,前端已經是自成一格的架構,接下來就是來實作各層的程式碼。
Root
這裡會放一個叫做app.ts的檔案,裡面主要是建立有用到的angular module以及設定相依性。
下列程式碼代表的意思是,宣告了一個叫做Sample的命名空間,裡面有五個module,分別是Hank.Chen、Hank.Chen.Configs、Hank.Chen.Components、Hank.Chen.Controllers、Hank.Chen.Services。
而這裡特別說明一下[ ]代表的意思,如果是空的話,代表這個module沒跟其他module相依;反之,以Hank.Chen.Controllers為例子,他後面的[]寫了Hank.Chen.Services,這就代表假如要使用Hank.Chen.Controllers,就一定要定義Hank.Chen.Services,如果沒定義的話,因為兩者有相依,就會出現出現錯誤。
module Sample {
angular.module('Hank.Chen', [
'ui.router',
'Hank.Chen.Services',
'Hank.Chen.Controllers',
'Hank.Chen.Configs'
]);
angular.module('Hank.Chen.Configs', []);
angular.module('Hank.Chen.Components', []);
angular.module('Hank.Chen.Controllers', ['Hank.Chen.Services']);
angular.module('Hank.Chen.Services', []);
}
Configs
config相對比較好懂一點,可以把他想成類似web.config,可以設定一些基本設定檔,比如說api的root位置等等。
下列程式碼代表的意思是,宣告一個ConfigProvider的類別,實作angular的provider,所以被要求要實作$get方法,$get方法裡會把property config回傳。因為是強型別,所以需告知config的型別,這個例子是IConfig,所以要再宣告一個IConfig的類別,裡面只有一個property,SampleApiRoot。
最後一段是最重要的,如果漏了等於前面都白宣告了,必須把我們宣告好的類別註冊到angular的module裡。
module Hank.Chen.Configs {
export class IConfig {
SampleApiRoot :string
}
}
module Hank.Chen.Configs {
export class ConfigProvider implements ng.IServiceProvider {
config:IConfig = {
SampleApiRoot: 'http://localhost/sampleapi/api',
}
$get() {
return this.config;
}
}
angular.module('Hank.Chen.Configs')
.provider('Config', ConfigProvider)
}
Services
這裡主要是定義與後端api交互的相關方法,這類型的功能會把它放到Services的module裡。
下列程式碼代表的意思是,宣告一個MemberService的類別,並且用相依性注入的方式注入angular的$http以及自定義的Config類別,裡面提供了一個GetMember()的方法,負責用get的方式呼叫後端api取得資料。
補充說明,`${this.config.SampleApiRoot}/Member/`的字串中 ` 這個符號可以想像成string.format的效果,它會幫忙把${this.config.SampleApiRoot}取代成對應的字串,而不需要用字串相加的方式,這樣看起來會更直覺。
最後,仍然別忘了要把定義好的Services註冊到module中。
module Hank.Chen.Services {
export class MemberService {
static $inject = ['$http', 'Config']
constructor(
private $http: ng.IHttpService,
private config: Configs.IConfig
) {
}
GetMember(): ng.IHttpPromise<ViewModel.MemberViewModel> {
var url = `${this.config.SampleApiRoot}/Member/`;
return this.$http.get<ViewModel.MemberViewModel>(url);
}
}
angular.module('Hank.Chen.Services')
.service('MemberService', MemberService);
}
Controllers
這裡的想法跟原本MVC網站中的Controller資料夾定義有點類似,就是依據程式別來做拆分。
本例子是假設要撰寫一個MemberLogin相關的程式,因此會在controllers的資料夾下新增一個MemberLogin資料夾,裡面放置相關的controller和view的內容。
稍微描述一下此層的運作機制,在此層會註冊route、controller以及放置相關的html檔案。利用angular.ui-Route套件所提供的IStateProvider來監聽狀態,當監聽到的狀態與route所定義的內容match時,便會傳回對應的controller和html頁面。
以下方的例子而言, $stateProvider在state是member時會去找TypeScripts/Controllers/MemberLogin/member.base.html的頁面,而負責此頁面的controller則是MemberController,簡寫為MemberCtrl,因此,在member.base.html頁面上就可以呼叫MemberController定義好的功能,以及存取裡面的資料。
function MemberControllerRoute($stateProvider: angular.ui.IStateProvider) {
$stateProvider
.state('member',
{
url: '/member',
templateUrl: 'TypeScripts/Controllers/MemberLogin/member.html',
controller: 'MemberController',
controllerAs: 'MemberCtrl',
resolve: {
}
})
}
MemberControllerRoute.$inject = ['$stateProvider'];
angular.module('Hank.Chen.Controllers')
.config(MemberControllerRoute);
到這邊為止,只有完成原本MVC架構中的V 和 C的定義,嚴格說來,並沒有M,因為並沒有資料來源。
假設資料來源是後端API,下方例子是宣告一個GetMemberData的方法,MemberService是上方我們自定義的,就不多說。這裡比較需要說明的是IQService,它主要是幫助我們非同步取得後端api資料,在呼叫MemberService的GetMember方法後,只有先回傳一個deferred.promise(我跟你承諾我會回傳你物件),但實際上還沒有,真正回傳的時間是呼叫完GetMember success後的deferred.resolve(data),才會真正取得資料。
function GetMemberData($q: ng.IQService,
MemberService: Services.MemberService)
{
var deferred = $q.defer();
MemberService.GetMember()
.success((data) => {
deferred.resolve(data);
});
return deferred.promise;
}
GetMemberData.$inject = ['$q','MemberService'];
接下來,再把這個方法掛給$stateProvider的resolve屬性,請參考下方程式碼片段。
$stateProvider
.state('member',
{
url: '/member',
templateUrl: 'TypeScripts/Controllers/MemberLogin/member.base.html',
controller: 'MemberController',
controllerAs: 'MemberCtrl',
resolve: {
Member: GetMemberData
}
})
完成route的定義之後,再來才是定義controller,controller只有一個重點要提醒,就是前面route在resolve區塊內取得的資料要注入給controller這段需要自己來實作,實作的方式也很簡單,就用$inject注入就可以了,直接參考下方程式碼片段,名稱大小寫要一模一樣。
module Hank.Chen.Controllers {
export class MemberController {
static $inject = ['Member'];
constructor(
member: Hank.Chen.ViewModel.MemberViewModel
) {
}
}
angular.module('Hank.Chen.Controllers')
.controller('MemberController', MemberController);
}
最後,就是實作html,為了減少複雜性,直接最簡單秀出Member的Id即可。
<div>
{{MemberCtrl.member.Id}}
</div>
這裡有一個觀念要注意,如果只是這樣寫的話,html頁面會空白的,因為在controller裡面,你只有對它注入member物件,並沒有對它開放成屬性,因此,頁面上是讀不到的,要將它開放成屬性也不難,加個public即可,請參考下方程式碼片段。
constructor(
public member: Hank.Chen.ViewModel.MemberViewModel
) {
}
到這裡為止,前端的程式碼基本上已經做到了職責分離,在維護起來也會比較方便了。
啟動程式碼
到這裡為止程式碼就能work了嗎?..當然不行,還有幾個額外的動作要作。
其中最基本的當然是...要引用我們剛剛寫好的所有js檔,TypeScript其實它是在背後幫我們產生對應的js檔,而這些東西,我們必須手動把它加入到我們的網站中,一般都會直接在加在BundleConfig.cs中,可參考下列程式碼片段。
bundles.Add(new ScriptBundle("~/bundles/app").Include(
//// Root
"~/TypeScripts/app.js",
//// Config
"~/TypeScripts/Configs/config.js",
"~/TypeScripts/Configs/config.route.js",
//// Components
//// Service
"~/TypeScripts/Services/member.service.js",
//// Controller/Member
"~/TypeScripts/Controllers/memberLogin/member.controller.js",
"~/TypeScripts/Base/base.controller.js",
"~/TypeScripts/Controllers/memberLogin/member.route.js"
));
再來需要在TypeScripts的config資料夾內新增config.route.ts,基本上這裡會設定routing的規則,下面是偷懶的作法,單純作範例效果, $urlRouteProvider.otherwise("member")的意思是,沒有match到任何規則就幫我導到member。
module Hank.Chen.Configs {
angular.module('Hank.Chen.Configs')
.config([
'$stateProvider',
'$urlRouterProvider',
'$locationProvider',
(
$stateProvider: angular.ui.IStateProvider,
$urlRouteProvider: angular.ui.IUrlRouterProvider,
$locationProvider: ng.ILocaleService) => {
$urlRouteProvider.otherwise("member");
}
])
}
如果沒有更改MVC本身的route設定的話,它進來預設會自動導到Home/Index,因此我們還要修改Index.cshtml,記得要引用我們設定好的bundle,以及掛上ng-app屬性。
@{
Layout=null;
}
<div ng-app="Hank.Chen">
<div ui-view></div>
</div>
@Scripts.Render("~/bundles/angular")
@Scripts.Render("~/bundles/app")
整體架構約如下圖所示, HomeIndex裡的<div ui-view></div>內的內容會經由config的route導到member頁面,接著由member的route接手,處理相關的html和controller,然後把<div ui-view></div>給取代掉。
小結
我們目前做到的有將前端的程式碼作了職責分離,並且利用狀態監控機制來抽換我們的頁面,在TypeScript的威能下,前端程式碼不但享受到有智慧提示的好處,也會在編譯時期就能檢查我們的程式碼,大幅提升了前端程式的可維護性。
在下一篇文章將會描述如何打造基本建設,利用component的方式來構築網站,當基礎設施建設到一定程度時,未來有任何新的需求時,只需專注在需求的商業邏輯上,而不用不斷地重複造輪子和挖坑..待續