,
在开始之前,我想先介绍三个工具,我们将使用这些工具达到预期目标。
请记住,这些工具并不是必需的,你可以使用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'
, [
''
,
'
,
''
].join(
"n"
));
}])
.run([
'$templateCache'
,
function
($templateCache) {
return
$templateCache.put(
'/partials/footer.html'
, [
''
,
'
' : endReached}">'
,
'
,
''
,
''
].join(
"n"
));
}])
|
记住,这些仅仅是包含HTML代码的常规JS字符串,这能保证模版在$templateCache中能通过特殊的路径被访问到。
感谢这个解决方案,我们能够预先在$templateCache中填充内容,这样$http.get就可以只在需要的时候执行(当请求的模版丢失时,这意味着它们应该由asset应用程序处理)。
如果你使用middleman的话,我们必须找到另一种颇为不同的解决方案。虽然我们拥有与应用程序相关的模版,但是它们在最开始的阶段是没有被编译的,因此$templateCache也是空的。
结果就是,每个诸如
我们需要即时从远程服务器下载并编译模版,而不是通过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
)
]
|
我们遇到了一些问题,值得在此描述。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前,我们已经拥有了填充过的模版缓存。
我们能够使用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
|
|
像上面这样的HTML可以直接被编译到angular的$templateCache以及特殊的片段中,同时它也可以通过每个脚本的相应id访问。
虽说我们信任自己的代码,但我们仍然需要建立测试保证其能如期运行,对于测试工作,我们使用Jasmine建立两个测试用例:
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
it
'puts templates into cache'
, inject ($templateCache) ->
$templateCache.put(
'cacheKey'
,
expect($templateCache.
get
(
'cacheKey'
)).toEqual
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
$templateCache.
get
'notExistingCacheKey'
$httpBackend.flush()
expect($compile).toHaveBeenCalledWith
|
我们还想测试一下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 =
''
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
|
现在,我们可以确信,一切都尽在掌握,可以部署到生产环境上了。
我们走了很长一段路,提取公共代码,并分离了两个能够共享可重用模版的单页应用。这确实是值得的,因为通过这个“一次性”工作,我们实现了一个解决方案,能够在任何项目中应用。建议您举一反三,尝试在你的应用程序中利用我们的成果。最终我们都希望将每一块巨石分解为更小更简单的微应用,是不是?