1. 程式人生 > >用ES6編寫AngularJS程式是怎樣一種體驗

用ES6編寫AngularJS程式是怎樣一種體驗

AngularJS不用我贅述,前端開發人員一定耳熟能詳。有人稱之為MVWhatever框架,意思是使用AngularJS,你可以參考任意正規化進行應用開發,無論是MVC、還是MVVVM都信手拈來,只要你懂,正規化在AngularJS手下,都可以輕鬆適配。

隨著各種現代瀏覽器、以及nodeES6的支援,已經有越來越多的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); //符合條件的新陣列

如果改用陣列的高階函式,再配合ES6arrow function,程式碼可以簡潔如斯:

var list = originalList.filter(item => item.gender === 'male');

console.log(list); //符合條件的新陣列

既然有如此優雅的語法糖能讓我們的開發變得high到爆,那過去我們認為屌炸天的AngularJS

(現在也屌炸天,只不過還有Angular2,ReactVUE橫空出世)是不是可以用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 namees6-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, 分為featuresfw兩大部分。這個內容略多,我後面詳述吧。

  • index.html_vm, 單頁應用html模版,最終的html會由webpack根據這個模版生成

  • package.json, 專案的npm描述檔案,那些具體的工具命令(譬如剛才用過的npm start,都在這裡面定義好了)

  • webpack.config.dev.js, 開發、除錯環境使用的webpack配置

  • webpack.config.prod.js, 正式執行環境使用的webpack配置。npm run release命令會用這個配置,生成的結果都會給檔名加hashjavascript檔案也會壓縮。

可用工具介紹

  • 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;

最後,你可能還有其它問題,直接來看看這裡,或者Github上給我提issue也未嘗不可呢