微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

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

2015-02-27 18:11 前端大全
点击上方蓝字↑↑↑,轻松关注哦~

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


  • CoffeeScript:一个强大的小型语言,它受Ruby启发并被编译为JavaScript,它拥有无数的语法糖能够加快开发进度。

  • MiddleMan:一个静态的站点生成器,通过它,你可以使用现代网络开发中所有的快捷方法和工具。

  • Brunch.io:基于node.js的JavaScript任务运行器,是一种前端开发的自动化工具。


请记住,这些工具并不是必需的,你可以使用JavaScript,Grunt或Gulp来完成相同的成果。


准备工作


首先,让我们明确描述一下我们的目标。


我们有两个独立的Angular单页应用,比如说前者是供学生使用的,后者是供猎头使用的,它们被分别置于https://hunters.com/ 和 https://students.com/下。我们已经拥有一个第三方应用程序处理通用的asset,如CSS和JS。


以上片段允许我们通过一个特殊的存储于theenv对象中的属性对生产环境和开发环境进行区分,它可能是下面这样的:


# 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请求,获取那些已经编译并被置入其自身缓存的结果。


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组件。


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方法,对不对?


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状态中我们解决了片段加载,这使得我们能够等待所需的模版。


$stateProvider

.state 'anonymous',

abstract: true

resolve:

assetsPartials: ['AssetsPartialsLoader', (AssetsPartialsLoader) ->

AssetsPartialsLoader.load()

]


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文件中去,之后可以被编译进缓存中,仅仅需要包含:


activate :angular_templates


在config.rb中,能够获得angular片段html文件,它即将被编译和获取。


结果可能看上去类似下面这样:


<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的片段


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获取模版,并将其编译到模版缓存中去。


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


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


总结


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



原文出处:KamilLelonek

译文出处:伯乐在线 - Justin Wu



/////////////////


1. 『前端大全』分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯,欢迎关注。微信号:FrontDev

(长按二维码↑↑↑自动可扫描)

http://web.jobbole.com/all-posts/

2. 点击“阅读原文”,查看更多前端文章。


 
前端大全 更多文章 5个典型的JavaScript面试题(上) Limu:JavaScript的那些书 Web开发:我希望得到的编程学习路线图 JavaScript基础工具清单 常用排序算法之JavaScript实现
猜您喜欢 流量加密奈我何? 七夕节到了,单身的程序猿在等什么 使用缓存的9大误区(下) iOS,Android网络抓包教程之tcpdump 【好文推荐】七情六欲聊运营