昨天一个话题说关于AngularJS2以后版本的两个小技巧,不料引出了另外一个话题,话题起始很简单:
“很多的前端框架并不复杂,比如JQuery,引入即用,实时看到效果,多好。到了Angular2一直到现在的版本5,一点改进没有,还要编译,还要部署,原有的JS脚本也不能用了。”
细想起来,这个话题的帽子并不小,至少牵扯出来一个关键,AngularJS2及以后的版本,其框架之下的JS代码,跟HTML中<script>
块之中的JS代码,到底是什么关系?
我试着来回答一下:
上面是从技术实现上的限制原因,实际上还有一个设计哲学逻辑上的原因:
————————————————————————————————————————————
那是不是原有的JS代码和技术都要作废掉,无法再使用了呢?
当然不是,你肯定早看到了,大量的第三方模块和代码库,通过NPM的管理,共存于这个架构中,彼此友好的相处。你原有的工作,完全可以用同样的方式来工作。
你也可能会说,可我有很多代码没有做到那么好的面向对象化包装,也不想做那么复杂,该怎么办呢?AngularJS也提供了至少3个方法,来完成两个世界的打通工作。
第一个方法,使用declare来预声明:
我们来先看一个例子,使用ng new testExtJS
来新建一个工程,接着cd testJS
进入项目目录,使用cnpm install
来初始化依赖包。用cnpm的原因是如果在中国,速度会快很多,这个在上一篇文章也说了。
接着修改index.html,这里只贴出最后的结果:
<head>
<meta charset="utf-8">
<title>TestExtJs</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<script>
var webGlObject = (function() {
return {
init: function() {
alert('webGlObject initialized');
}
}
})(webGlObject || {})
</script>
<app-root>Loading...</app-root>
</body>
</html>
注意中间的<script>
块是我们增加的部分,来模拟我们在html本地已经有了一段js代码。
然后在app.component.ts中增加声明和调用的部分:
import { Component } from '@angular/core';
declare var webGlObject: any;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app works!';
constructor() {
this.title="constructor works!"
webGlObject.init();
}
}
注意上面代码中的declare声明,和下面添加的constructor构造函数和其中对js对象的调用。 declare的意思就是告诉AngularJS,相信我,虽然现在你看不到对象webGlObject,但相信我,或早或晚,反正你一定会看到它的存在的,你正常编译、正常执行就好啦。当然这里的潜台词和副作用就是:诺,AngularJS,这部分代码我负责啦,你不用管它的对错,反正错了我也不会怪你。 使用这种方法,类似上一篇文章的问题,你也完全可以声明一个window对象,然后直接访问其中的userAgent:
...
declare var window:any;
...
console.log(window.navigator.userAgent);
问题又来了,既然直接能访问到window对象,那还用什么ng4-device-detector组件,直接从userAgent中判断设备类型不好吗? 这就牵涉到我上面解释的最后一条,将来这段AngularJS代码,很可能不是运行在一个浏览器,其中可能根本没有window/document对象,那时候,这段代码就出错了。当然你可能会说,不不不,我就是在浏览器运行,不考虑别的。OK,我也不较劲,你当我没说,你完全可以就这么用。 但是比较规范的办法,应当是把window对象以及你需要的其它类似对象,写成一个服务,然后注入到app.component之中,这样,即便将来运行环境有变化,只修改服务部分代码,你的主程序完全可以不用修改。 落实到代码,大致是这样,首先把window对象包装成一个服务:
import { Injectable } from '@angular/core';
function _window() : any {
// return the global native browser window object
return window;
}
@Injectable()
export class WindowRef {
get nativeWindow() : any {
return _window();
}
}
注册到provider:
import { WindowRef } from './WindowRef';
...
@NgModule({
...
providers: [ WindowRef ]
})
export class AppModule{}
在需要的组件中,引用这个服务,然后就可以使用了:
...
import { WindowRef } from './WindowRef';
...
@Component({...})
class MyComponent {
...
constructor(private winRef: WindowRef) {
// 得到window对象
console.log('Native window obj', winRef.nativeWindow);
}
...
}
我得承认,这样是麻烦了不少,不过规范、可复用的代码,本身的确就多了很多限制。 参考资料:https://juristr.com/blog/2016/09/ng2-get-window-ref/
————————————————————————————————————————————
AngularJS也一直在努力,尽力弥合这种鸿沟,其中HostListener和HostBinding就是具体的两个实现,也是我们开始所说的3个方法中的后两个。 HostListener 是属性装饰器,用来为宿主元素添加事件监听,这个行为表示html端某个元素的事件,产生到达TS脚本的调用动作。比如:
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: 'button[counting]'
})
class CountClicks {
numberOfClicks = 0;
@HostListener('click', ['$event.target'])
onClick(btn: HTMLElement) {
console.log('button', btn, 'number of clicks:', this.numberOfClicks++);
}
}
使用counting装饰的button按钮,每次点击,都会产生一次计数行为,并且打印到控制的日志中去。 HostBinding 是属性装饰器,用来动态设置宿主元素的属性值,这个跟上面的动作相反,表示首先标记在html某元素的某属性,然后在TS脚本端,对这个属性进行设置、赋值。比如:
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[exeButtonPress]'
})
export class ExeButtonPress {
@HostBinding('attr.role') role = 'button';
@HostBinding('class.pressed') isPressed: boolean;
@HostListener('mousedown') hasPressed() {
this.isPressed = true;
}
@HostListener('mouseup') hasReleased() {
this.isPressed = false;
}
}
上面的代码表示,如果某个html元素用exeButtonPress属性修饰之后,会有一个.pressed属性,可以监控到鼠标按下、抬起的事件,这表现了html元素到ts端双向的互动。 HostListener和HostBinding有一个简写的形式host,如下所示:
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[exeButtonPress]',
host: {
'role': 'button',
'[class.pressed]': 'isPressed'
}
})
export class ExeButtonPress {
isPressed: boolean;
@HostListener('mousedown') hasPressed() {
this.isPressed = true;
}
@HostListener('mouseup') hasReleased() {
this.isPressed = false;
}
}
看看,跟上一篇中快捷键绑定的方法很相似了? 这一部分的代码使用了https://segmentfault.com/a/1190000008878888的资料,这篇文章写的很细致,想详细了解的建议及早阅读。