Angular项目间的模板共享:如何跨多项目重用公共HTML

在开始之前,我想先介绍三个工具,我们将使用这些工具达到预期目标。

  • 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的片段
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    describe '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
           <a href= "http://www.jobbole.com/members/template/" rel= "nofollow" >@template</a> = '<div></div>'
     
         it 'puts templates into cache' , inject ($templateCache) ->
           $templateCache.put( 'cacheKey' , <a href= "http://www.jobbole.com/members/template/" rel= "nofollow" >@template</a>)
           expect($templateCache. get ( 'cacheKey' )).toEqual <a href= "http://www.jobbole.com/members/template/" rel= "nofollow" >@template</a>
     
         it '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 <a href= "http://www.jobbole.com/members/template/" rel= "nofollow" >@template</a>
     
           $templateCache. get 'notExistingCacheKey'
           $httpBackend.flush()
           expect($compile).toHaveBeenCalledWith <a href= "http://www.jobbole.com/members/template/" rel= "nofollow" >@template</a>

我们还想测试一下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

现在,我们可以确信,一切都尽在掌握,可以部署到生产环境上了。

总结

我们走了很长一段路,提取公共代码,并分离了两个能够共享可重用模版的单页应用。这确实是值得的,因为通过这个“一次性”工作,我们实现了一个解决方案,能够在任何项目中应用。建议您举一反三,尝试在你的应用程序中利用我们的成果。最终我们都希望将每一块巨石分解为更小更简单的微应用,是不是?

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值