在開始之前,我想先介紹三個工具,我們將使用這些工具達到預期目標。
- CoffeeScript:一個強大的小型語言,它受Ruby啟發並被編譯為JavaScript,它擁有無數的語法糖能夠加快開發進度。
- MiddleMan:一個靜態的站點生成器,通過它,你可以使用現代網路開發中所有的快捷方法和工具。
- Brunch.io:基於node.js的JavaScript任務執行器,是一種前端開發的自動化工具。
請記住,這些工具並不是必需的,你可以使用JavaScript,Grunt或Gulp來完成相同的成果。
準備工作
首先,讓我們明確描述一下我們的目標。
我們有兩個獨立的Angular單頁應用,比如說前者是供學生使用的,後者是供獵頭使用的,它們被分別置於https://hunters.com/ 和 https://students.com/下。我們已經擁有一個第三方應用程式處理通用的asset,如CSS和JS。
以上片段允許我們通過一個特殊的儲存於theenv物件中的屬性對生產環境和開發環境進行區分,它可能是下面這樣的:
1 2 3 4 |
# development: env = { ASSETS_HOST: 'http://localhost:8888' } # production: env = { ASSETS_HOST: 'http://assets.com' } |
在middleman中可以使用dotenv gem來管理環境變數,同樣的,在brunch.io中可以使用jsenv。
應用案例
我們不僅需要公共的JavaScript和樣式表,還需要通用的HTML模版。因此我們必須在兩個應用程式間提取可重用的片段(partials),並將其儲存於asset伺服器上。
程式碼
我們為$templateCache建立一個簡單的封裝get和set方法的裝飾者,通過這個裝飾者,我們試圖從本地快取中獲取模版,如果存在的話就將其返回。此外,它還會在asset伺服器上執行一個http請求,獲取那些已經編譯並被置入其自身快取的結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
Extensions = angular.module 'MyApplication.ExtensionsModule', [] # ... Extensions.factory '$templateCache', [ '$cacheFactory', '$http', '$injector', 'SecurityConstants', ($cacheFactory, $http, $injector, SecurityConstants) -> cache = $cacheFactory('templates') promise = undefined info: cache.info get: (url) -> fromCache = cache.get(url) return fromCache if fromCache unless promise promise = $http.get("#{SecurityConstants.assetsHost}/templates/partials.html") .then((response) -> $injector.get('$compile') response.data response ) promise.then (response) -> status: response.status data: cache.get(url) put: (key, value) -> cache.put key, value ] |
為什麼能夠工作?
在brunch.io中,我們使用了一個出色的外掛:jade-angularjs-brunch,它將所有的HTML模版編譯為javascript檔案,表示一個稱為partials的angular元件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
angular.module('partials', []) .run(['$templateCache', function($templateCache) { return $templateCache.put('/partials/content.html', [ '', '<div class="main-content">', ' <div ui-view="Main"></div>', ' <div class="push"></div><span ng-include="'/partials/footer.html'"></span>', '</div>',''].join("n")); }]) .run(['$templateCache', function($templateCache) { return $templateCache.put('/partials/footer.html', [ '', '<footer id="footer" sh-footer ng-class="{'footer--show' : endReached}">', ' <div class="container">', ' <p><span translate="footer.rights"></span><span> StudentHunter Team.</span></p>', ' </div>', '</footer>',''].join("n")); }]) |
記住,這些僅僅是包含HTML程式碼的常規JS字串,這能保證模版在$templateCache中能通過特殊的路徑被訪問到。
感謝這個解決方案,我們能夠預先在$templateCache中填充內容,這樣$http.get就可以只在需要的時候執行(當請求的模版丟失時,這意味著它們應該由asset應用程式處理)。
另一種途徑
如果你使用middleman的話,我們必須找到另一種頗為不同的解決方案。雖然我們擁有與應用程式相關的模版,但是它們在最開始的階段是沒有被編譯的,因此$templateCache也是空的。
結果就是,每個諸如<ng-include=”‘partials/template.html’”>這樣的請求都需要asset應用程式處理,因為快取中還什麼都沒有。在後面的請求中,它才會用獲取到的模版填充快取,而不是那些本應儲存在基於middleman的應用程式中的東西。
我們需要即時從遠端伺服器下載並編譯模版,而不是通過http發出請求來獲得使用應用程式模版的可能性 。與使用我們之前談到的裝飾著相比,我們也可以利用run方法,對不對?
1 2 3 4 5 6 |
app.run ['$http', '$injector', 'SecurityConstants', ($http, $injector, SecurityConstants) -> $http.get("#{SecurityConstants.assetsHost}/templates/partials.html").then((response) -> $injector.get('$compile') response.data response ) ] |
問題以及UI-Router解決方案
我們遇到了一些問題,值得在此描述。run方法中的$http.get能夠非同步載入asset,這表明模版有時候會在應用程式執行後編譯,結果是在部分需要共享模版的應用程式中,模版會丟失或在DOM中根本不存在。
UI Router帶來了解決方案
我們在應用程式中堅定地使用UI router,因此我們決定繼續用其獲取外部依賴,在root狀態中我們解決了片段載入,這使得我們能夠等待所需的模版。
1 2 3 4 5 6 7 |
$stateProvider .state 'anonymous', abstract: true resolve: assetsPartials: ['AssetsPartialsLoader', (AssetsPartialsLoader) -> AssetsPartialsLoader.load() ] |
1 2 3 4 5 6 7 |
angular.module('StudentHunter.ExtensionsModule').factory 'AssetsPartialsLoader', ['$http', '$injector', 'SecurityConstants', ($http, $injector, SecurityConstants) -> load: -> $http.get("#{SecurityConstants.assetsHost}/templates/partials.html").then (response) -> $injector.get('$compile') response.data response ] |
現在,在開始構建Angular DOM前,我們已經擁有了填充過的模版快取。
Assets app
我們能夠使用middleman-angular-templates gem將模版新增到一個HTML檔案中去,之後可以被編譯進快取中,僅僅需要包含:
1 |
activate :angular_templates |
在config.rb中,能夠獲得angular片段html檔案,它即將被編譯和獲取。
結果可能看上去類似下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<body class="templates templates_partials"> <script id="templates/shared/translate/lang_switch.html" type="text/ng-template"> <ul class="lang-switch" ng-controller="TranslateCtrl as translate"> <li class="lang-switch__lang lang-switch__lang--en"> <button class="lang-switch__btn" ng-class="{'lang-switch__lang--current': translate.isCurrentLang('en')}" ng-click="translate.changeLang('en')" type="button"></button> </li> <li class="lang-switch__lang lang-switch__lang--pl"> <button class="lang-switch__btn" ng-class="{'lang-switch__lang--current': translate.isCurrentLang('pl')}" ng-click="translate.changeLang('pl')" type="button"></button> </li> </ul> </script> <!-- ... --> </body> |
像上面這樣的HTML可以直接被編譯到angular的$templateCache以及特殊的片段中,同時它也可以通過每個指令碼的相應id訪問。
測試
雖說我們信任自己的程式碼,但我們仍然需要建立測試保證其能如期執行,對於測試工作,我們使用Jasmine建立兩個測試用例:
- 從$templateCache中獲取模版
- 解析來自遠端url的片段
123456789101112131415161718192021222324describe 'ExtensionsModule', ->beforeEach module 'StudentHunter.Constants'beforeEach module 'StudentHunter.ExtensionsModule'beforeEach module 'StudentHunter.SecurityModule'beforeEach module 'ui.router'describe '$templateCache decorator', ->beforeEach module ($provide) ->$provide.decorator '$compile', ($delegate) ->return jasmine.createSpy $delegate@template = '<div></div>'it 'puts templates into cache', inject ($templateCache) ->$templateCache.put('cacheKey', @template)expect($templateCache.get('cacheKey')).toEqual @templateit 'calls assets partials and compile response if cache key not found', inject ($injector, $templateCache, SecurityConstants) ->$httpBackend = $injector.get '$httpBackend'$compile = $injector.get '$compile'$httpBackend.whenGET("#{SecurityConstants.assetsHost}/templates/partials.html").respond @template$templateCache.get 'notExistingCacheKey'$httpBackend.flush()expect($compile).toHaveBeenCalledWith @template
我們還想測試一下AssetsPartialsLoader能否通過$http.get獲取模版,並將其編譯到模版快取中去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
describe 'ExtensionsModule', -> beforeEach module 'StudentHunter.Constants' beforeEach module 'StudentHunter.SecurityModule' beforeEach module 'StudentHunter.ExtensionsModule' describe "AssetsPartialsLoader load", -> beforeEach module ($provide) -> $provide.decorator '$compile', ($delegate) -> return jasmine.createSpy $delegate beforeEach inject (@$compile, @AssetsPartialsLoader, $injector, @SecurityConstants) -> @$httpBackend = $injector.get '$httpBackend' @assetsPartialsHost = "#{@SecurityConstants.assetsHost}/templates/partials.html" @fakeTemplate = '<div></div>' it 'should call assets partials API when assetsPartialsLoaded flag is falsy', -> @$httpBackend.expectGET(@assetsPartialsHost).respond @fakeTemplate @AssetsPartialsLoader.load() @$httpBackend.flush() it 'should compile loaded templates', -> @$httpBackend.whenGET(@assetsPartialsHost).respond @fakeTemplate @AssetsPartialsLoader.load() @$httpBackend.flush() expect(@$compile).toHaveBeenCalledWith @fakeTemplate |
現在,我們可以確信,一切都盡在掌握,可以部署到生產環境上了。
總結
我們走了很長一段路,提取公共程式碼,並分離了兩個能夠共享可重用模版的單頁應用。這確實是值得的,因為通過這個“一次性”工作,我們實現了一個解決方案,能夠在任何專案中應用。建議您舉一反三,嘗試在你的應用程式中利用我們的成果。最終我們都希望將每一塊巨石分解為更小更簡單的微應用,是不是?