$location
服务用来解析浏览器地址栏中的URL(基于window.location),使得它在我们的程序中可用。当改变了地址栏中的URL,$location
中的数据会跟着变,反之亦然。
$location服务:
把当前浏览器地址栏的URL暴露出来,你可以:
* 观察和监视该URL.* 改变该URL.
当用户进行以下操作时,同步浏览器的URL
* 改变地址栏(什么意思?)* 点击了前进、后退或历史链接* 点击了页面上某链接
将URL对象以一组方法的形式展示出来(protocol, host, port, path, search, hash).
$location
与window.location
比较window.location | $location service | |
目的 | 允许对浏览器当前地址进行读写 | 相同 |
API | 暴露包含属性的"原始"对象,可直接修改 | 暴露jQuery风格的getters/setters |
与Anguar程序生命周期结合 | 无 | 了解所有内部生命周期,与`$watch`结合, ... |
与HTML5 API无缝集成 | 无 | 有 (并且与旧浏览器兼容) |
知道程序导入的根文档/上下文 | 否 - `window.location.path`返回"/docroot/actual/path" | 是 - `$location.path()`返回"/actual/path" |
任何你需要与当前URL交互的时候。
当浏览器URL改变的时候,不会重新导入整个页面。如果你想在改变URL时重载整个页面,可使用底层的API:$window.location.href
如果$location
初始化时的设置不同,则它的行为也不同。默认的设置对于大多数程序是合适的,你也可以自定义它以使用新的功能。
一旦$location
初始化完成,你就可以通过jQuery风格的getter/setter方法与它交互,以取得或改变浏览器当前的URL。
为了配置$location
服务,取得$locationProvider
并设置以下参数:
html5Mode(mode): {boolean}
* `true` - 使用HTML5模式* `false` - 使用Hashbang模式* 默认值: `false`
hashPrefix(prefix): {string}
* 在Hashbang URL使用的前缀 (在Hashbang模式,或者在旧浏览器中使用Html5模式)<br />* 默认值: `'!'`
$locationProvider.html5Mode(true).hashPrefix('!');
#### Getter/Setter
`$location` service为URL中的只读部分提供了getter方法(abUrl,protocal,host,port),为url,path,search,hash提供了getter/setter方法。
// get the current path
$location.path();
// change the path
$location.path('/newValue')
所有的setter方法都返回同一个`$location`对象以方便链式操作。例如,你可以把多个调用串起来:
$location.path('/newValue').search({key: value});
有一个特别的方法叫`replace`,可以让`$location` service在下次与浏览器同步的时候,不要创建新的历史记录,而是替换掉之前最后一个历史记录。这个功能在你实现跳转的时候很有用,因为你不这么做的话,就会破坏掉后退按钮的功能(因为它会反复触发跳转)。
示例如下:
$location.path('/someNewPath');
$location.replace();
// or you can chain these as: $location.path('/someNewPath').replace();
注意这个方法不会马上更新`window.location`,相反,`$location` service了解scope的生命周期,并把多次对`$location`的改变合并为一个操作,在`$digest`阶段提交给`window.location`。因此只用调用`replace()`一次就可以完成整个替换操作,而不会新境历史记录。当浏览器被更新以后,`replace()`将更新标识,以后的操作将会产生新的历史记录,直到再次调用`replace()`
##### Settters和字符集
你可以向`$location` service传特殊字符,它们会根据[RFC 3986](http://www.ietf.org/rfc/rfc3986.txt)中制定的规则被转码。当你访问以下方法时:
所有被传入到$location
的setter方法,如path()
, search()
, hash()
中的值,都被转码 对以下方法path()
, search()
, hash()
的getter调用(无参数)的返回值都将被解码。 当你调用absUrl()
方法时,返回值是一个被部分编码的完整url* 当你调用url()
方法时,返回值是一个由path,search,hash组成的字符串:/path?search=a&b=c#hash
。将被部分转码。
#!
)和HTML5模式$location
service有两个配置模式,用来控制浏览器地址栏中的URL的格式:默认的Hashbang模式和基于HTML5 History API的HTML5模式。使用不同模式时,我们使用的代码和API调用完全一致,$location
service会自动根据情况来生成不同的URL并管理历史记录。
注:hashbang中,hash是指#
,bang是指!
。这是一种使用浏览器端mvc的约定符号。
Hashbang模式 | HTML5模式 | |
配置 | 默认 | { html5Mode: true } |
URL格式 | 在所有的浏览器中使用hashbang URL | 在现在浏览器中使用常规URL,在旧浏览器中使用hashbang URL |
link重写 | 否 | 是 |
需要服务器端设置 | 否 | 是 |
在该模式下,$location
在所有的浏览器中都使用Hashbang URL
it('should show example', inject(
function($locationProvider) {
$locationProvider.html5mode = false;
$locationProvider.hashPrefix = '!';
},
function($location) {
// open http://host.com/base/index.html#!/a
$location.absUrl() == 'http://host.com/base/index.html#!/a'
$location.path() == '/a'
$location.path('/foo')
$location.absUrl() == 'http://host.com/base/index.html#!/foo'
$location.search() == {}
$location.search({a: 'b', c: true});
$location.absUrl() == 'http://host.com/base/index.html#!/foo?a=b&c'
$location.path('/new').search('x=y');
$location.absUrl() == 'http://host.com/base/index.html#!/new?x=y'
}
));
为了让搜索引擎能索引到你的ajax程序,你需要在head中添加一个特殊的meta标签:
这么做,搜索引擎爬虫会在请求的的参数中加上_escaped_fragment_
参数,你的服务器程序就可以识别这是一个爬虫,返回给它一个正常的HTML文件。关于这项技术的细节,可参考让你的AJAX程序可被索引。
在HTML5模式中,$location
service的getter/setter通过HTML5 history API与浏览器URL地址交互,允许我们使用正常的URL路径和search片断,而不用hashbang
格式。如果浏览器不支持HTML5 History API,则$location
service会自动降级使用hashbang URL。这样你就不需要担心你的程序在不同浏览器上的运行状态,$location
会自己选择最优方案。
在旧浏览器上打开一个正常的URL -> 跳转为hashbang URL* 在现代浏览器上打开一个hashbang URL -> 重写为一个正常的URL
it('should show example', inject(
function($locationProvider) {
$locationProvider.html5mode = true;
$locationProvider.hashPrefix = '!';
},
function($location) {
// in browser with HTML5 history support:
// open http://host.com/#!/a -> rewrite to http://host.com/a
// (replacing the http://host.com/#!/a history record)
$location.path() == '/a'
$location.path('/foo');
$location.absUrl() == 'http://host.com/foo'
$location.search() == {}
$location.search({a: 'b', c: true});
$location.absUrl() == 'http://host.com/foo?a=b&c'
$location.path('/new').search('x=y');
$location.url() == 'new?x=y'
$location.absUrl() == 'http://host.com/new?x=y'
// in browser without html5 history support:
// open http://host.com/new?x=y -> redirect to http://host.com/#!/new?x=y
// (again replacing the http://host.com/new?x=y history item)
$location.path() == '/new'
$location.search() == {x: 'y'}
$location.path('/foo/bar');
$location.path() == '/foo/bar'
$location.url() == '/foo/bar?x=y'
$location.absUrl() == 'http://host.com/#!/foo/bar?x=y'
}
));
当你使用History API模式时,虽然在不同的浏览器中需要不同的链接,但你只要使用正常URL就可以了,例如:<a href="/some?foo=bar">link</a>
当用户点击链接时:
在旧浏览器中,该URL变为:/index.html#!/some?foo=bar
在现代浏览器中,该URL变为: /some?foo=bar
在以下情况中,链接不会被重写。相反,浏览器会对原来的链接进行一次完全的页面重载。
链接中包含target
属性: <a href="/ext/link?a=b" target="_self">link</a>
指向不同域的绝对链接:<a href="http://angularjs.org/">link</a>
当base
定义了,链接以/
开头,并指向了一个不同的base: <a href="/not-my-base/link">link</a>
使用该模式,需要在服务器端重写URL。通常你需要把你所有的链接都指向程序的入口(如index.html
)
请注意检查你的全部相对链接、图片、脚本等。你要么得在入口html文件的head中增加一个base标签指明你的url base(<base href="/my-base">
),要么就在每个地方都使用绝对路径(以/
开头),因为相对路径会以文档的初始绝对url作为它的基准,而该初始绝对url通常与程序真正的根路径不同。
在Angular程序中从文档的root处启用History API,是强烈推荐的做法,因为它会处理好全部的相对路径的问题。
由于在HTML5模式中,会自动重写URL,所以你的用户可以在旧浏览器中打开正常的url,或者在现代浏览器中打开hashbang url:
在现代浏览器中,会自动把hashbang url重写为正常的url* 在旧浏览器中,会自动把正常的url重写为hashbang url
在这里你可以看到两个$location
实例,都在HTML5模式下,但在不同的浏览器上。所以你能看到它们的差别。这些$location
services都连接到一个假的浏览器上。每一个input控件代表了一个浏览器的地址栏。
注意当你向第一个浏览器中输入hashbang url时,它不会重写/跳转到常规url,反之亦然。因为这种转换仅仅发生在页面导入时解析初始URL的时候。
这个例子中,我们使用<base href="/base/index.html" />
index.html
<script src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
<script src="script.js"></script>
<div ng-non-bindable class="html5-hashbang-example">
<div id="html5-mode" ng-controller="Html5Cntl">
<h4>Browser with History API</h4>
<div ng-address-bar browser="html5"></div><br><br>
$location.protocol() = {{$location.protocol()}}<br>
$location.host() = {{$location.host()}}<br>
$location.port() = {{$location.port()}}<br>
$location.path() = {{$location.path()}}<br>
$location.search() = {{$location.search()}}<br>
$location.hash() = {{$location.hash()}}<br>
<a href="http://www.host.com/base/first?a=b">/base/first?a=b</a> |
<a href="http://www.host.com/base/sec/ond?flag#hash">sec/ond?flag#hash</a> |
<a href="/other-base/another?search">external</a>
</div>
<div id="hashbang-mode" ng-controller="HashbangCntl">
<h4>Browser without History API</h4>
<div ng-address-bar browser="hashbang"></div><br><br>
$location.protocol() = {{$location.protocol()}}<br>
$location.host() = {{$location.host()}}<br>
$location.port() = {{$location.port()}}<br>
$location.path() = {{$location.path()}}<br>
$location.search() = {{$location.search()}}<br>
$location.hash() = {{$location.hash()}}<br>
<a href="http://www.host.com/base/first?a=b">/base/first?a=b</a> |
<a href="http://www.host.com/base/sec/ond?flag#hash">sec/ond?flag#hash</a> |
<a href="/other-base/another?search">external</a>
</div>
</div>
script.js
function FakeBrowser(initUrl, baseHref) {
this.onUrlChange = function(fn) {
this.urlChange = fn;
};
this.url = function() {
return initUrl;
};
this.defer = function(fn, delay) {
setTimeout(function() { fn(); }, delay || 0);
};
this.baseHref = function() {
return baseHref;
};
this.notifyWhenOutstandingRequests = angular.noop;
}
var browsers = {
html5: new FakeBrowser('http://www.host.com/base/path?a=b#h', '/base/index.html'),
hashbang: new FakeBrowser('http://www.host.com/base/index.html#!/path?a=b#h', '/base/index.html')
};
function Html5Cntl($scope, $location) {
$scope.$location = $location;
}
function HashbangCntl($scope, $location) {
$scope.$location = $location;
}
function initEnv(name) {
var root = angular.element(document.getElementById(name + '-mode'));
angular.bootstrap(root, [function($compileProvider, $locationProvider, $provide){
$locationProvider.html5Mode(true).hashPrefix('!');
$provide.value('$browser', browsers[name]);
$provide.value('$document', root);
$provide.value('$sniffer', {history: name == 'html5'});
$compileProvider.directive('ngAddressBar', function() {
return function(scope, elm, attrs) {
var browser = browsers[attrs.browser],
input = angular.element('<input type="text">').val(browser.url()),
delay;
input.bind('keypress keyup keydown', function() {
if (!delay) {
delay = setTimeout(fireUrlChange, 250);
}
});
browser.url = function(url) {
return input.val(url);
};
elm.append('Address: ').append(input);
function fireUrlChange() {
delay = null;
browser.urlChange(input.val());
}
};
});
}]);
root.bind('click', function(e) {
e.stopPropagation();
});
}
initEnv('html5');
initEnv('hashbang');
$location
service仅仅允许你修改URL,不允许你重载页面。如果你想重载整个页面,或跳到另一个页面,使用底层API$window.location.href
$location
$location
知道Angular的scope
的生命周期。当浏览器中的URL改变了,它会更新$location
并调用$apply
,所以所有的$watch
/$observers
都会被通知到。
如果你想在$digest
阶段改变$location
,一切都很正常。$location
会把这个改变传播给浏览器,并通知$watchers/$observers.
但你在Angular之外(如DOM事件,或测试中)改变$location
,则你必须调用$apply
来传播这些改变。
一个路径总是以/
开头。$location.path()
setter始终会在需要的时候在路径前加一个/
。
注意hashbang模式中的!
前缀不是$location.path()
中的一部分。它实际上是hash前缀(hashPrefix)
当在测试中使用$location
service时,你就处于scope
的生命周期之外了。这表明,你必须手动调用scope.$apply()
describe('serviceUnderTest', function() {
beforeEach(module(function($provide) {
$provide.factory('serviceUnderTest', function($location){
// whatever it does...
});
});
it('should…', inject(function($location, $rootScope, serviceUnderTest) {
$location.path('/new/path');
$rootScope.$apply();
// test whatever the service should do...
}));
});
在Angrlar之前的版本中,$location
使用了hashPath
或hashSearch
来处理pyth和search方法。在当前的版本中,$location
自己处理path和search方法,然后在需要的时候,使用这些信息来拼装hashbang URL(例如http://server.com/#!/path?search=a
)
在程序中寻找 | 改为 |
$location.href = value $location.hash = value $location.update(value) $location.updateHash(value) |
$location.path(path).search(search) |
$location.hashPath = path | $location.path(path) |
$location.hashSearch = search | $location.search(search) |
Navigation outside the app | Use lower level API |
$location.href = value $location.update(value) |
$window.location.href = value |
$location[protocol | host | port | path | search] | $window.location[protocol | host | port | path | search] |
Read access | Change to |
$location.hashPath | $location.path() |
$location.hashSearch | $location.search() |
$location.href $location.protocol $location.host $location.port $location.hash |
$location.absUrl() $location.protocol() $location.host() $location.port() $location.path() + $location.search() |
$location.path $location.search |
$window.location.path $window.location.search |
Anguar编译器当前不支持方法的双向绑定(参看issue)。如果你需要$location对象的双向绑定(在input上使用ngModel
directive),你需要指定一个额外的model属性(e.g. locationPath
),并使用两个watchers向两边推送$location
的更新。
例如:
<pre>
// js - controller
$scope.$watch('locationPath', function(path) {
$location.path(path);
});
$scope.$watch('$location.path()', function(path) {
scope.locationPath = path;
});