昨天一個話題說關於AngularJS2以後版本的兩個小技巧,不料引出了另外一個話題,話題起始很簡單: “很多的前端框架並不複雜,比如JQuery,引入即用,實時看到效果,多好。到了Angular2一直到現在的版本5,一點改進沒有,還要編譯,還要部署,原有的JS腳本也不能用了。” 細想起來,這個話題的 ...
昨天一個話題說關於AngularJS2以後版本的兩個小技巧,不料引出了另外一個話題,話題起始很簡單:
“很多的前端框架並不複雜,比如JQuery,引入即用,實時看到效果,多好。到了Angular2一直到現在的版本5,一點改進沒有,還要編譯,還要部署,原有的JS腳本也不能用了。”
細想起來,這個話題的帽子並不小,至少牽扯出來一個關鍵,AngularJS2及以後的版本,其框架之下的JS代碼,跟HTML中<script>
塊之中的JS代碼,到底是什麼關係?
我試著來回答一下:
- 首先,在AngularJS2框架之中實際使用的是ES6,全稱ECMAScript6,是Javascript的下一個版本。官方的例子則是基本採用TS,全稱TypeScript,是JS的一個超集。之所以用起來沒有明顯區別的感覺,因為的確從常用語法上,跟當前使用的JS,或者叫ES5 JS,差別很小,但即便再小,那也算的上不同的語言了。
- 為什麼採用新的語言,而不是沿用當前的ES5,官網和社區已經有了很多解釋了,新語言當然有新語言的優勢,比如定義變數,可以指定類型,而在程式中用錯類型,則會在編譯過程中就給出警告,不至於等到上線了才發現BUG。這些優勢非常多,這裡就不畫蛇添足了。反正你肯定能理解,新當然有新的好處。
- 既然採用了新的語言,為了跟當前的瀏覽器系統相容,當然就有一個翻譯過程,準確的說,甭管是TS還是ES6,甚至將來可能的ES7,在當下,都要翻譯成ES5,才能在當前流行的瀏覽器之中運行。這個翻譯,行話上講,也就是“編譯”。
- 事實上,編譯不僅僅乾這麼一點事,很多的優化工作、查錯工作,也是在這個階段完成的,比如你使用了沒有定義的變數、函數;比如你用錯了函數類型;比如你使用了某個函數庫但只是用了其中一小部分,那麼多沒用的部分應當排除掉避免占用寶貴的下載帶寬,這些都是在編譯過程做到的。
- 好了,既然經過了這麼複雜的動作,這個編譯也必不可少,那麼實際上答案已經出來了:那就是,很多原有理所應當存在的東西,就比如你在HTML中定義的JS對象、變數、函數,那些都是在執行環節,瀏覽器中才存在的。而在編譯階段,那些東西還只是停留在字元狀態,AngularJS當然並不知道他們存在,也就無法直接的、像原來我們使用HTML-JS一樣來使用它們了,這就如同上面那張圖,看上去海天一色,互相映襯,但在根本上,它們是在兩個世界。
上面是從技術實現上的限制原因,實際上還有一個設計哲學邏輯上的原因:
- AngularJS設計之初就不是為了單純的在桌面瀏覽器中運行,還希望能夠在手機、移動設備甚至其它設備上執行。你可能會說,現在的手機瀏覽器也很發達啊,至少比很多IE6/IE7之流要強多了,稍等,這裡說的移動設備、其它設備,可不一定是指僅僅瀏覽器,從這種設計邏輯出發,AngularJS成為一種跨平臺的開發框架,直接編譯成各種系統原生的代碼,完全是有可能實現的。試想,在那種情況下,你原來的JS代碼很可能是連存在的空間都沒有,又如何讓AngularJS訪問到呢?
————————————————————————————————————————————
那是不是原有的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的資料,這篇文章寫的很細緻,想詳細瞭解的建議及早閱讀。