用ES6編寫AngularJS程式是怎樣一種體驗
AngularJS不用我贅述,前端開發人員一定耳熟能詳。有人稱之為MVWhatever
框架,意思是使用AngularJS
,你可以參考任意正規化進行應用開發,無論是MVC
、還是MVVVM
都信手拈來,只要你懂,正規化在AngularJS
手下,都可以輕鬆適配。
隨著各種現代瀏覽器、以及node對ES6的支援,已經有越來越多的ES6
特性可以在程式中使用,她們給開發過程帶來的便利不言而喻,舉個小例子,我想從一個數組裡找一些符合條件的資料,放入另一個數組內,過去我們這麼寫:
var list = [],
i;
for (i = 0; i < originalList.length; i++) {
var item = originalList[i];
if (item.gender === 'male') {
list.push(item);
}
}
console.log(list); //符合條件的新陣列
如果改用陣列的高階函式,再配合ES6
的arrow
function,程式碼可以簡潔如斯:
var list = originalList.filter(item => item.gender === 'male');
console.log(list); //符合條件的新陣列
既然有如此優雅的語法糖能讓我們的開發變得high到爆,那過去我們認為屌炸天的AngularJS
Angular2
,React
, VUE
橫空出世)是不是可以用ES6
來寫?少年不要懷疑,真的可以哦!
一個良好、快速、簡潔的starter
工具有利於我們對ES6
編寫AngularJS
的深入認知,所以我要用一個骨架生成器generator-es6-angular來建立新專案,該generator
是依託於yeoman的腳手架。
安裝yo
npm install -g yo
請注意字首
sudo
,如果你使用的是unix
like作業系統的話
安裝generator-es6-angular
npm install -g generator-es6-angular
請注意字首
sudo
,如果你使用的是unix
like作業系統的話
使用generator-es6-angular
建立專案
先找個你喜歡的目錄,然後執行下面的命令,因為一會新專案會直接建立在該目錄下。
yo es6-angular
上面命令回車後,生成器會問你如下問題,老實作答即可(注意: 對單頁應用沒經驗的孩紙,在Use html5 model
這個問題上,請選擇No
;
當問你Which registry would you use?
時,國內使用者選擇第一個淘寶映象安裝速度會快很多)
當命令執行完畢後,你就能在當前目錄下看到剛才建立的專案了,本例中我使用的project name
是es6-demo
。
開啟除錯之旅
#進入剛建立的專案目錄
cd es6-demo
#啟動除錯服務
npm start
專案簡介
骨架結構
es6-demo
├── css
├── etc
├── img
├── js
| ├── features
| │ ├──common
| │ │ ├── logical
| │ │ └── ui
| │ ├── about
| │ │ ├── controller
| │ │ └── partials
| │ └── home
| │ ├── controller
| │ ├── partials
| │ └── service
| └── fw
| ├── config
| ├── ext
| ├── init
| ├── lib
| └── service
├── index.html_vm
├── package.json
├── webpack.config.dev.js
├── webpack.config.prod.js
-
css
, 這個不用多說吧,裡面有個main.css
,自己隨便改改看嘛。我這裡沒有預設引入less
或者sass
理由非常簡單,留給開發人員選擇自己喜愛的工具 -
etc
, 一些公共配置性內容,可以放在這裡,方便查詢、通用 -
img
, 用我多說麼?放圖片的啦 -
js
, 分為features
和fw
兩大部分。這個內容略多,我後面詳述吧。 -
index.html_vm
, 單頁應用html
模版,最終的html
會由webpack
根據這個模版生成 -
package.json
, 專案的npm
描述檔案,那些具體的工具命令(譬如剛才用過的npm start
,都在這裡面定義好了) -
webpack.config.dev.js
, 開發、除錯環境使用的webpack
配置 -
webpack.config.prod.js
, 正式執行環境使用的webpack
配置。npm run release
命令會用這個配置,生成的結果都會給檔名加hash
,javascript
檔案也會壓縮。
可用工具介紹
-
npm start
, 啟動除錯伺服器,使用webpack.config.dev.js
作為webpack
配置,不直接生成物理檔案,直接記憶體級別響應除錯伺服器資源請求。而且內建hot reload
,不用重啟服務,修改原始碼,瀏覽器即可重新整理看到新效果 -
npm run release
, 使用webpack.config.prod.js
作為webpack
配置,生成壓縮、去快取化的bundle
檔案到es6-demo/build
目錄下。也就是說,如果你要釋出到生產環境或者其它什麼測試環境,你應該提供的是es6-demo/build
目錄下生成的那堆東西,而不是原始碼。 -
npm run dev
, 使用webpack.config.dev.js
作為webpack
配置,生成物理檔案。
js
目錄介紹
features
common
那些通用的邏輯、UI元件可以通通放在這裡,譬如為了演示方便,我已經在features/common/ui
裡寫了一個Autofocus.js
的指令,拿去用,不要客氣。
features/common/logical
裡也預設放了一個公共的Header
,就是最上面的那個導航。
about
home
這兩個就是純粹為了演示“功能 <對應> 路由”這個小原則而做的,你可以分別在這兩個feature
下找到一個Routes.js
,裡面的內容就描述了該功能對應一個(或多個)路由,是何等的easy。至於最後這個路由會怎樣被這個骨架使用,小夥伴們,好好研究哦!
fw
這裡面都是些所謂"框架"級別的設定,有興趣的話挨個兒開啟瞧瞧嘛,沒什麼大不了的。
特別注意,大部分時候,你的開發都應該圍繞
features
目錄展開,之所以叫fw
,就是和具體業務無關,除非你需要修改框架啟動邏輯,路由控制系統。。。,否則不需要動這裡的內容
原始碼介紹
js/index.js
入口檔案
/**
*
* 這裡連用兩個ensure,是webpack的特性,可以強制在bundle時將內容拆成兩個部分
* 然後兩個部分還並行載入
*
*/
//第一個部分是一個很小的spinner,在並行載入兩個chunk時,這個非常小的部分90%會競速成功
//於是你就看到了傳說中的loading動畫
require.ensure(['splash-screen/dist/splash.min.css', 'splash-screen'], function(require) {
require('splash-screen/dist/splash.min.css').use();
require('splash-screen').Splash.enable('circular');
});
//由於這裡是真正的業務,程式碼多了太多,所以體積也更大,載入也更慢,於是在這個chunk載入完成前
//有個美好的loading動畫,要比生硬的白屏更優雅。
//放心,這個chunk載入完後,loading動畫也會被銷燬
require.ensure(['css/main.css', 'splash-screen', './main'], function(require) {
require('css/main.css').use();
//這裡啟動了真正的“框架”
var App = require('./main').default;
(new App()).run();
});
js/main.js
“框架”啟動器
//引入依賴部分
import angular from 'angular';
import Initializers from 'init/main';
import Extensions from 'ext/main';
import Configurators from 'config/main';
import Services from 'service/main';
import Features from 'features/main';
import {Splash} from 'splash-screen';
class App {
constructor() {
//這裡相當於ng-app的名字
this.appName = 'es6-demo';
//例項化所有features
Features.forEach(function(Feature) {
this.push(new Feature());
}, this.features = []);
}
//從features例項中提取AngularJS module name
//並將這些name作為es6-demo的依賴
//會在下面createApp時用到
findDependencies() {
this.depends = Extensions.slice(0);
var featureNames = this.features
.filter(feature => feature.export)
.map(feature => feature.export);
this.depends.push(...featureNames);
}
//啟用初始化器,個別操作希望在AngularJS app啟動前完成
beforeStart() {
Initializers.forEach(function(Initializer) {
(new Initializer(this.features)).execute();
}, this);
this.features.forEach(feature => feature.beforeStart());
}
//建立es6-demo應用例項
createApp() {
this.features.forEach(feature => feature.execute());
this.app = angular.module(this.appName, this.depends);
}
//配置es6-demo
configApp() {
Configurators.forEach(function(Configurator) {
(new Configurator(this.features, this.app)).execute();
}, this);
}
//註冊fw下的“框架”級service
registerService() {
Services.forEach(function(Service) {
(new Service(this.features, this.app)).execute();
}, this);
}
//看到了麼,這裡我會銷燬loading動畫,並做了容錯
//也就是說,即便你遇到了那微乎其微的狀況,loading動畫比業務的chunk載入還慢
//我也會默默的把它收拾掉的
destroySplash() {
var _this = this;
Splash.destroy();
require('splash-screen/dist/splash.min.css').unuse();
setTimeout(function() {
if (Splash.isRunning()) {
_this.destroySplash();
}
}, 100);
}
//啟動AngularJS app
launch() {
angular.bootstrap(document, [this.appName]);
}
//順序啟用所有模組
run() {
this.findDependencies();
this.beforeStart();
this.createApp();
this.configApp();
this.registerService();
this.destroySplash();
this.launch();
}
}
export default App;
用ES6寫Feature
features/home/main.js
//一個feature的main.js負責管理該feature所用到的所有模組
import FeatureBase from 'lib/FeatureBase';
//引入路由
import Routes from './Routes';
//引入Controller
import HomeController from './controller/HomeController';
//引入service
import HomeService from './service/HomeService';
//繼承自FeatureBase基類
class Feature extends FeatureBase {
constructor() {
//給feature一個名字,該名字在之前的findDependencies裡用到
super('home');
//將路由繫結到該feature上
this.routes = Routes;
}
execute() {
//註冊controller,下面我們繼續講如何寫ES6版的controller
this.controller('HomeController', HomeController);
//註冊service
this.service('HomeService', HomeService);
}
}
//採用預設匯出,別擔心,再上一級呼叫方會正確處理的
export default Feature;
用ES6寫路由
簡單到沒朋友
//引入路由對應的模版,還是因為webpack,將模版
//作為字串引入,就是這麼easy
import tpl from './partials/home.html';
export default [
{
id: 'home',
isDefault: true,//是否預設頁面
when: '/home',//路由path
controller: 'HomeController',//處理該路由的controller
controllerAs: 'home',//模版裡用的controller別名
template: tpl//模版字串
}
];
用ES6寫Controller
//ES6的引入機制,即便是圖片資源,因為有了webpack,獲
//取URL照樣萌萌噠
import logoUrl from 'img/AngularJS-large.png';
//注意,我們已經開始用ES6的類來宣告一個Controller了
class HomeController {
//這裡我們用ng-annotate提供的註解方式,可以快捷注
//入需要的依賴
/*@ngInject*/
constructor($scope, HomeService) {
this.$scope = $scope;
this.HomeService = HomeService;
this._init_();
this._destroy_();
}
_init_() {
this.logoUrl = logoUrl;
this.todos = [];
this.HomeService
.getInitTodos()
.then(todos => {
//又見arrow function
//注意這裡的this
this.todos = todos;
});
}
addTodo(e) {
if (e.keyCode !== 13) {
return;
}
this.todos.push({txt: e.target.value});
e.target.value = '';
}
toggleTodo(todo) {
todo.finished = !todo.finished;
}
remaining() {
//計算剩餘工作量,是不是還挺帥?
return this.todos.reduce((n, todo) => n + Number(!todo.finished), 0);
}
archive() {
//過濾,high不high
this.todos = this.todos.filter(todo => !todo.finished);
}
_destroy_() {
//如果有AngularJS未知的變數產生,記得手動銷燬哦
this.$scope.$on('$destroy', function() {});
}
}
//最後,採用預設匯出
export default HomeController;