1. 寫在前面
之前基於Electron寫過一個Markdown編輯器。就其功能而言,主要功能已經實現,一些小的不影響使用的功能由於時間關系還沒有完成;但就代碼而言,之前主要使用的是jQuery,由於本人非專業前段,代碼寫的自己都感覺是“一塌糊塗”,十分混亂。現在看到Angular2十分火爆,跑了跑它的The Tour of Heroes的例子,感覺非常不錯,代碼組織的井井有條,於是乎決定學習一下Angular2,然后用它將之前的NiceMark重寫一下。
2. 整體感知
它組織代碼的方式,引用其官方文檔里的話就是:
You write Angular applications by composing HTML templates with Angularized markup, writing component classes to manage those templates, adding application logic in services, and boxing components and services in modules. ——GUIDE- 4. Architecture
大意就是說,你能這么去寫一個Angular應用,用Angular擴展語法寫HTML模板,寫組件類去管理這些模板,用服務添加應用邏輯,用模塊打包組件和服務。
用官方文檔里的圖說明就是:
簡單解釋一下該圖:
- 圖的中間部分,在component的上面畫了Metadata和Template,是說一個Component(組件)主要由元數據(Metadata)和Template(模板)構成。Property Binding(屬性綁定)箭頭表示可以在component里定義property(屬性),這些參數可以作為模板渲染的數據,而binding(綁定)的意思是說,一旦模板里的數據(比如一個輸入框里數據)被修改了,它對應的property會自動地修改,或者反過來property被修改了,模板的相應內容也會跟着發生變化。Event Binding也類似,只不過針對的是Event(事件):你可以在模板中寫代碼去監聽某一事件(當然Angular有自己的“監聽語法”),並指定該事件被觸發時調用component類中哪個方法(函數)。想象一下,我們可以在該方法中去修改某些property,根據前面Property Binding描述的,模板也會跟着發生變化。是的,在Angular中,你再也不用(也不要)直接的去修改DOM,而是修改property,它就是一個變量,修改起來多么的直觀方便,你再也不必寫像jQuery那樣(比如,
$xxx.text('姓名: '+ user.name +',年齡:'+ user.age)
)又臭又長的“惡心”代碼了,盡管這可以用一些手段來優化,但總是不那么直觀,簡便。 - 圖中兩個虛線箭頭是解釋說明的意思,左邊箭頭解釋的是,我們在編寫一個component時,會依賴一些服務,而這些服務類不用自己去
new
,可以讓Angular注入進來。比如我們要編寫一個用來展示用戶信息列表的組件,為此我們需要這些用戶的信息,而這些用戶信息需要通過發送HTTP請求去服務器中獲取,這時我們可以寫一個UserService
類,寫一個getUsers
方法專門負責獲取這些用戶信息,component只需要調用UserService
的getUsers
方法,便可以拿到這些信息,而獲得UserService
實例的方式最好不要自己去new
,而是交給Angular去管理,讓它幫我們注入進來。這點跟Spring依賴注入的意思是一樣的。 - 右邊的虛線箭頭說的是,我們在編寫模板時會用到一些Directive(指令),這些指令被用來指導Dom的渲染。
- 最后還剩下圖的左上角部分,是一個一個的Module(模塊)。它們像集裝箱一樣將Component,Service等較小的構成元素封裝起來堆疊在一起,構成了一個完整的應用。
如果你還沒有接觸過Angular或者類似的框架,可能你還是不太明白Angular是怎么玩的 。不過沒關系,以上內容只是讓你對Angular有個大致的印象。實際上,它玩來玩去就是圍繞以上提到的幾個概念展開的,它們分別是:
- module (模塊)
- component (組件)
- template (模板)
- metadata (元數據)
- data binding (數據綁定)
- directive (指令)
- service (服務)
- dependency injection (依賴注入)
只要明白了以上幾個概念的意思,掌握了其用法,那就上道了。下文會結合幾個小例子來解釋這幾個概念的含義的用法。
2. Hello World
老規矩,從Hello World開始。
有一點忘記說了,下面學習的是Angular的TypeScript版本。TypeScript是JavaScript的超集,它在JavaScript的語法上做了擴展,如果你對TypeScript還不了解,建議先簡單看看TypeScript的語法,中文站和英文站都有。Angular的文檔也是既有中文站,也有英文站,本文就是學習自Angular中文站,個人感覺翻譯的非常好,並且點擊每段文字都會出現英文原文,這點非常不錯!😄。
先是環境的搭建。我用的是Ubuntu系統,IDE使用的是Idea。你需要提前裝好Nodejs和npm,這應該是前端人員的必備工具了,這里假設你已經裝好了。另外,npm最好使用淘寶鏡像,否則會很慢很慢……由於是Hello World,我們在最原始的工程上手動搭建。
首先是建立一個Static Web工程,點擊Next:
填好項目的名字,路徑后,項目默認是這樣,光禿禿的,什么也沒有:
我們先用npm初始化一下項目,生成配置文件。點擊底部的Terminal,輸入npm init
,回車:
然后會提示你填一些項目相關信息,填好后刷新一下項目,會生成一個package.json的文件,它是npm的項目配置文件,里面可以寫一些腳本命令和一些需要依賴的第三方庫(類似於Maven的pom.xml文件)。
下面添加Angular及其它庫,並編寫運行腳本。在package.json
中加入如下代碼:
{
...
"scripts": {
"start": "tsc && concurrently \"tsc -w\" \"lite-server\" ",
"e2e": "tsc && concurrently \"http-server -s\" \"protractor protractor.config.js\" --kill-others --success first",
"lint": "tslint ./app/**/*.ts -t verbose",
"lite": "lite-server",
"pree2e": "webdriver-manager update",
"test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"",
"test-once": "tsc && karma start karma.conf.js --single-run",
"tsc": "tsc",
"tsc:w": "tsc -w"
},
"dependencies": {
"@angular/common": "~2.4.0",
"@angular/compiler": "~2.4.0",
"@angular/core": "~2.4.0",
"@angular/forms": "~2.4.0",
"@angular/http": "~2.4.0",
"@angular/platform-browser": "~2.4.0",
"@angular/platform-browser-dynamic": "~2.4.0",
"@angular/router": "~3.4.0",
"angular-in-memory-web-api": "~0.2.4",
"systemjs": "0.19.40",
"core-js": "^2.4.1",
"rxjs": "5.0.1",
"zone.js": "^0.7.4"
},
"devDependencies": {
"concurrently": "^3.1.0",
"lite-server": "^2.2.2",
"typescript": "~2.0.10",
"canonical-path": "0.0.2",
"http-server": "^0.9.0",
"tslint": "^3.15.1",
"lodash": "^4.16.4",
"jasmine-core": "~2.4.1",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-cli": "^1.0.1",
"karma-jasmine": "^1.0.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~4.0.14",
"rimraf": "^2.5.4",
"@types/node": "^6.0.46",
"@types/jasmine": "^2.5.36"
}
}
最后package.json
的結構是這樣的:
接着在Terminal中執行npm install
,這個命令會根據當前目錄下的package.json
文件中的dependencies
和devDependencies
key的值去倉庫下載相應依賴庫,這些庫會下載到當前目錄的node_modules文件夾中。
做完以上工作,環境就搭建好了。下面可以開始寫Hello World。
首先在項目的根目錄下新建一個index.html
頁面。里面引入相關js庫,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function (err) {
console.error(err);
});
</script>
</head>
<body>
</body>
</html>
其中的systemjs.config.js
還沒有給出,如下,同樣也是放在根目錄下:
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
這個JS是用來引入Angular相關庫的,我們可以大致看一下里面的內容。首先它配置了npm管理的包所在的目錄是node_modules
;它還配置了我們app的根目錄為app目錄,因此待會我們需要在根目錄下新建一個app文件夾,用來放編寫的Angular相關文件;之后配置了Angular庫文件的位置。
這個文件來自這里。
以上就把Angular引入頁面了。下面我們在index.html
的body中加上一個標簽:
<app></app>
這個標簽是我們自己定義的,它對應着一個視圖和它包含的處理邏輯,比如一個用戶列表,列表可以進行增刪改查操作。可以想象,假如我們想要在頁面顯示兩個相同的用戶列表,我們只需要在相應的地方寫兩個<app></app>
標簽,這多么的優雅!記住這個標簽的名字,接下來我們要把它編寫出來。
以上代碼基本上是固定,無論你如何的修改你的功能,它們都可能不會變化。下面編寫Hello World的Angular代碼。
我們在systemjs.config.js
中配置了Angular的工作目錄app: 'app'
,因此我們需要在根目錄中創建一個叫app的文件夾;我們還指定了main: './main.js'
(相當於C語言或Java語言中指定主函數,它是程序運行的入口),因此我們在app文件夾下新建一個main.ts
文件,注意后綴是.ts
,它是一個TypeScript文件。然后在里面編寫:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
以上代碼的意思是將AppModule
模塊作為入口。代碼首選導入了platformBrowserDynamic
和AppModule
,其中platformBrowserDynamic
是Angular自身提供的;AppModule
是我們接下來要編寫的,它定義在app.module.ts文件中。注意import ... from ...
是TypeScript的語法,與Angular無關。
app.module.ts的代碼如下:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
上面的代碼先導入NgModule
和BrowserModule
兩個Angular內置的模塊,和我們之后要編寫的AppComponent
組件(定義在app.component.ts中)。然后定義了一個類AppModule
,並用export
關鍵字導出,這也是為什么我們在main.ts中用import { AppComponent } from './app.component';
能將其導入的原因。
注意到AppModule
類的上面寫了一個類似Java注解一樣的東西,它在TypeScript中被稱為decorator(裝飾器),其參數被稱為metadata(元數據),它有3個屬性,至於含義,先不用管。總之,我們現在要記住的是,要想定義一個模塊,需要在模塊類的上面加上NgModule
裝飾器。
上面我們導入過AppComponent
,下面是它所在的文件app.component.ts的編寫:
import { Component } from '@angular/core';
@Component({
selector: 'app',
template: `<h1>Hello {{name}}</h1>`
})
export class AppComponent {
name = 'World';
}
同樣先導入Component
裝飾器,被該裝飾器修飾的類便變成為了Angular中所謂的Component(組件),在該類中我們還定義了一個property(屬性)——name
,其值為World。
該元數據中定義了兩個屬性,注意其中selector
的值app
,就是我們在index.html
中寫的自定義標簽<app></app>
;該標簽對應的模板(template屬性)是<h1>Hello {{name}}</h1>
,其中用{{}}
括起來的變量name
正是我們在AppComponent
類中定義的屬性name
,其值為World
。
寫到這里,Hello World就OK了。上面說了這么多廢話,實際上只是在/app目錄下寫了3個文件,如下:
|-HelloWorld
|-app
|-app.component.ts
|-app.module.ts
|-main.ts
現在我們可以運行它了。運行之前需要將TypeScript文件翻譯成JavaScript文件,這需要先安裝TypeScript包:
npm install -g typescript
由於Angular使用了TypeScript的裝飾器,這一特性還沒有納入正式的標准,需要寫一個TypeScript的配置文件tsconfig.json
,里面做如下配置:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}
接下來,只要在根目錄執行tsc命令,便能將app目錄下的.ts文件翻譯成.js文件了。
然后我們執行./node_modules/lite-server/bin/lite-server
,會自動開啟瀏覽器,顯示Hello World。
你可能會想,每次我們改動代碼之后都要進行翻譯,重啟服務器操作,太麻煩了,有沒有更簡便的方法呢?當然有了,並且已經寫好了,就定義在package.json
中:
...
"scripts": {
"start": "tsc && concurrently \"tsc -w\" \"lite-server\" ",
...
}
...
因此我們只需要執行npm start
即可。並且在start
命令中,我們加了一個tsc -w
命令,它能夠監視當前目錄中文件的變化,然后通知lite-server
刷新頁面。其中還用到了另一個命令concurrently
,它可以讓多個命令同時運行,可以參考這里或這里(淘寶鏡像)了解相關信息。
以上便是Angular2的Hello World,你跑起來了嗎?如果沒有,對比一下這里的代碼。
下一篇會繼續學習Angular各個構成部件的作用和用法,寫一個TODO小應用。