用ES6語法來開發angularjs項目,webpack進行按需加載模塊打包

最近公司用webpack和angular2來開發手機app頁面,發現ES6的寫法很是方便和清晰,因而我想到在angular1中也能夠用ES6語法寫,webpack來進行打包編輯。因而我查閱了相關資料以後終於實現了ES6語法來寫angular1。css

Webpack

關於webpack的配置,網上有許多的教程,在github上也有至關多的項目例子能夠借鑑,我這就不詳細說webpack的配置了。若是你是新手,我推薦webpack的官網,若是你對英文不感冒你能夠查看webpack的中文文檔學習相關知識。固然若是你實在對webpack的配置煩惱的話,我推薦GitHub上AngularClass組織的angular-starter項目,這是webpack配置angualr2的例子,它裏面的配置很是詳細,對每一個插件的使用有相關的說明,對學習webpack的配置頗有幫助,本項目也是參照該例子,主要的目錄結構以下。html

angular-webpack/
 ├──config/                        * 配置文件目錄
 │   ├──helpers.js                 * helper 方法用來獲取目錄以及一些相關配置參數
 │   ├──webpack.common.js          * webpack 通用的配置信息
 │   ├──webpack.dev.js             * webpack 配置開發環境文件
 │   ├──webpack.prod.js            * webpack 配置生產環境文件
 │   └──webpack.test.js            * webpack 配置測試環境文件
 ├──src/                           * web文件的主目錄 
 │   ├──app.module.js              * angularjs 主module
 │   │
 │   ├──index.html                 * 項目的主入口
 │   │
 │   ├──app/                       * 文件主目錄
 │   │    ├──config/               * angularjs的config文件
 │   │    │         
 │   │    ├──router/               * angularjs的路由配置
 │   │    │
 │   │    └──views/                * angularjs的頁面
 │   │   
 │   └──style/                     * 項目的樣式文件  
 │
 ├──package.json                   * npm 配置文件
 └──webpack.config.js              * webpack 主配置文件

angularjs目錄結構

對一個項目來講,目錄組織是很是重要的,它能讓你一目瞭然的瞭解項目信息。在angularjs的項目中,有不一樣的目錄搭建,對於有些人喜歡把module、controller、directive、service等文件分別放在module、controller、directive、service目錄下統一管理。html頁面統一放在HTML目錄下它的目錄結構看起來是這樣的:webpack

app/                     
 ├──controller/             
 │      ├──*.controller.js 
 │      ├──*.controller.js 
 │      ├──*.controller.js
 │      │....
 ├──directive/  
 │      ├──*.directive.js 
 │      ├──*.directive.js 
 │      │....
 ├──html/            
 │      ├──*.html
 │      ├──*.html 
 │      │....
 ├──module/            
 │      ├──*.module.js
 │      ├──*.module.js
 │      │....
 │ 
 ├──service/ 
 │      ├──*.service.js 
 │      ├──*.service.js 
 │      │....

這一種目錄結構相對來講不是很好,尤爲是對模塊化打包來講結構很不清楚,不利於模塊化,頁面一多很難管理,另外一種目錄結構是把每個相關的頁面controller、service、module、html放到對應的頁面目錄下,它的結構是這樣的:git

app/                     
 ├──login/             
 │      ├──login.module.js 
 │      ├──login.controller.js 
 │      ├──login.directive.js
 │      ├──login.router.js
 │      ├──login.html
 │      ├──login.css
 │
 ├──home/  
 │      ├──home.module.js 
 │      ├──home.controller.js 
 │      ├──home.directive.js
 │      ├──home.router.js
 │      ├──home.html
 │      ├──home.css
 ├──other/ 
 │      ├──other.module.js 
 │      ├──other.controller.js 
 │      ├──other.directive.js
 │      ├──other.router.js
 │      ├──other.html   
 │      ├──other.css

這樣的目錄結構有利於模塊的移植。angularjs

angualrjs 文件構建

在ES6中有了新方法:Class來聲明類,class寫法看起來更簡潔,方便。在angular1中把function方法定義的controller,service等修改成Class聲明,具體各修改方式以下:github

module

ES6關於angualr的module寫法沒有大的變化,注意ES6中須要對angular模塊導入以後才能使用angular,其餘模塊的注入也是同樣的。web

//--------ES6以前
   angular.module('webapp',['oc.lazyLoad',]);

//--------ES6
   
   import angular from 'angular'; //---- 或者  var angular = require('angular');
   require('oclazyload');

   angular.module('webapp',['oc.lazyLoad',]);

controller

<div ng-controller='loginController as vm'>     
    <input type="tel" placeholder="手機號" ng-model="vm.username">
    <input type="password" placeholder="密碼" ng-model="vm.userPassword">
    <input type="button" value="登陸" ng-click="vm.login()">
</div>

login.controller.jsnpm

//ES5 controller的通常寫法
LoginController.$inject = ['$state', 'loginService'];
ngModule.controller('loginController', LoginController);

function LoginController($state, loginService) {

    var vm = this;
    vm.login = login;

    function login() {
        if (!vm.username) {
            alert('請輸入手機號');
            return;
        }
        if (!vm.userPassword) {
            alert('請輸入密碼');
            return;
        }
        loginService.login({
            username: vm.username,
            userPassword: vm.userPassword
        }).then(function(res){
            $state.go('page');
        }, function(res){
            alert('登陸失敗');
        })
    }

}

//------------------ES6-----------------------
class LoginController {
    constructor($state, loginService) {
        this.loginService = loginService;   //把依賴注入的方法放到this中,變爲this的屬性,使用this."property"來調用該變量
                                            //與以前比最大的不一樣是依賴注入不在是該函數內的「全局變量」
        this.$state = $state;
    }

    login() {
        if (!this.username) {
            alert('請輸入手機號');
            return;
        }
        if (!this.userPassword) {
            alert('請輸入密碼');
            return;
        }
        this.loginService.login({
            username: this.username,
            userPassword: this.userPassword
        }).then((res) => {
            this.$state.go('page');
        }, (res) => {
            alert('登陸失敗');
        })
    }
}

LoginController.$inject = ['$state', 'loginService'];

ngModule.controller('loginController', LoginController);

注意在function聲明中LoginController.$inject = ['$state', 'loginService']不論是放在function前面仍是後面都不會有影響,可是在class聲明中LoginController.$inject = ['$state', 'loginService']只能放在class後面定義,主要緣由是function會在做用域中進行提高而class不會。json

service

angular中service的寫法與controller類似,能夠說沒有大區別數組

login.service.js

//ES5 service寫法
LoginService.$inject = ['$http'];
function LoginService($http){
    var self=this;
    self.login=login;

    function login(params) {
        /*return $http({
            url: '****!/!***!/login',
            method: 'GET',
            params: params
        })*/
        /*用ES6 Promise方法模仿$http,也可用angular中的$q實現*/
        return new Promise(function (resolve, reject) {
            if (params) {
                resolve('success');
            } else {
                reject('fail');
            }
        });
    }
}

//------------------ES6-----------------------
class LoginService {
    constructor($http) {
        this.$http = $http;
    }

    login(params) {
        /*return this.$http({
            url: '****!/!***!/login',
            method: 'GET',
            params: params
        })*/
        /*用ES6 Promise方法模仿$http,也可用angular中的$q實現*/
        return new Promise((resolve, reject) => {
            if (params) {
                resolve('success');
            } else {
                reject('fail');
            }
        });
    }
}

LoginService.$inject = ['$http'];

ngModule.service('loginService', LoginService);

provider

//------------------ES6-----------------------
class LoginProvider  {
    constructor($http) {
        this.$http = $http;
    }
    $get(){
         let that=this;         
         login(params) {
            return that.$http({    //注意this的指向問題
                url: '****!/!***!/login',
                method: 'GET',
                params: params
            })
        }    
        return {
            login:login;
        }
    }
}
LoginProvider.$inject = ['$http']
ngModule.provider('LoginProvider', LoginProvider);

注意,在provider中須要定義$get函數

factory

關於factory的寫法稍微與前面的不一樣,不少人想固然的會這和service同樣寫factory

class LoginFactory {
    constructor($http) {
        this.$http = $http;
    }

    login(params) {
       ...
       ...
       ...
    }
}

LoginFactory.$inject = ['$http'];
ngModule.factory('LoginFactory', LoginFactory);   //TypeError: Cannot call a class as a function

class的定義是類似的,可是在module引入factory時若是按上面的寫法寫就會報TypeError: Cannot call a class as a function錯誤

可能有人會很奇怪,爲何controller,service,provider能夠,可是到了factory就會錯誤。這個和factory的機制有關。編譯器轉ES6語法到ES5後class會變成這樣:

var Loginfactory= function () {
    function Loginfactory($http) {
        _classCallCheck(this, Loginfactory);
        this.$http= $http;
    }

    _createClass(Loginfactory, [{
        key: 'login',
        value: function login() {
            
        }
    }]);

    return Loginfactory;
}();

關鍵是_classCallCheck方法,用來判斷this是否爲Loginfactory的實例,可是在factory中,angular內部更改了factory的返回對象,使this不在是factory的實例,因此會報錯。咱們須要手動new factory()方法,應該改爲這樣:

//LoginFactory.$inject = ['$http'];不須要了
ngModule.factory('LoginFactory', ['$http',($http)=>new LoginFactory($http)]);

注:controller,service,provider也能夠按factory的注入方法寫

但是這麼寫若是有好多依賴要注入,會使ngModule.factory('LoginFactory', ['$http'....,($http,...)=>new LoginFactory($http,...)])看起來很是長並且不易閱讀,怎樣才能像controller同樣以LoginFactory.$inject = ['$http']注入,能夠先獲取Class上的$inject的注入數組,在由該數組和Class的實例組成['a','b',function(a,b){}]樣式,具體實現以下:

function constructorFn(configFn) {
    let args = configFn.$inject || [];
    let factoryFunction = (...args) => new configFn(...args);
    /**
     * 主要檢測controller、directive的注入
     * service、factory、provider、config在程序運行時只運行一次
     * controller、directive每次頁面載入時會從新注入,須要把上一個注入的function從數組中移除
     * */
    if(typeof args[args.length-1]==='function'){
        args.splice(-1,1);
    }
    /**
     * args.push(factoryFunction)相似於['a',function(a){}]
     * */
    return args.push(factoryFunction) && args;  //return args;
}

LoginFactory.$inject = ['$http'];
ngModule.factory('LoginFactory', constructorFn(LoginFactory));

在寫factory時我還發現一個小問題,在寫angular的Http攔截器時候會出現意料以外的狀況

export class HttpInterceptor {
    constructor($q, $location) {
        this.$q = $q;
        this.$location = $location;
    }
    request(config){
        console.log(this.$q);    //error
        //this對象不是HttpInterceptor的實例而是undefined
        return config;    
    }
}
HttpInterceptor.$inject = ['$q', '$location'];

在調用request、requestError等方法時,this指向丟失了,應該是angular在調用時,this對象被修改了。若是你在request、requestError等方法中沒有用到this對象,能夠這麼寫。若是用到this對象,則須要變換一下:

export class HttpInterceptor {
    constructor($q, $location) {
        this.$q = $q;
        this.$location = $location;
        this.request=(config)=>{
            console.log(this.$q);    //success
            return config;    
        };
    }
    
}
HttpInterceptor.$inject = ['$q', '$location'];

注意,this指向更改可能出現的狀況不止在這裏,須要當心

directive

directive寫法稍有不一樣,因爲directive須要返回一個包含restrict、template 、scope 等屬性的對象,因此須要在constructor中定義這些熟悉,還有directive會有link,compile等函數方法

//vm.title
<my-page page-title='vm.title'></my-page>
//------------------ES6-----------------------
class PageDirective {
    constructor($timeout) {
        this.restrict = "EA";
        this.template = `{{vm.name}} Hello {{vm.pageTitle}}`;
        this.controller = PageDController;
        this.controllerAs = "vm";
        this.bindToController = true;
        this.scope = {
            pageTitle:'='
        };
        this.$timeout=$timeout;
    }

    link(scope,element,attr){
        this.$timeout(()=>scope.vm.name='',1000);
    }
   
}

PageDirective.$inject = ['$timeout'];

class PageDController{
    constructor(){
        this.name='pageDirective'
    }
}
ngModule.directive('myPage', constructorFn(PageDirective));

oclazyload 按需加載路由

webpack打包時會把HTML和js代碼都放到一個js文件中,使這個文件很是大,致使打開頁面加載會很慢,影響用戶體驗。因此咱們須要把HTML以及部分js代碼抽出放到塊文件中。幸運的是咱們能夠用oclazyload配合webpack進行anglar路由的按需加載,oclazyload的官方文檔有詳細的說明和例子,我這邊列了一個簡單的例子:

路由配置
login.router.js

import angular from 'angular'
//import LoginModule from './login.module'
{
    state: 'login',
    url: '/login',
    controller: 'loginController',
    controllerAs: 'vm',
    /*template: require('./login.html'),*/
    templateProvider: ['$q', ($q)=> {                //templateProvider來動態的加載template
        let defer = $q.defer();                      //require.ensure:webpack的方法在須要的時候才下載依賴的模塊,
         require.ensure([''], () => {                //只有require函數調用後再執行下載的模塊        
            let template = require('./login.html');  //require調用後加載login頁面
            defer.resolve(template);
        });
        return defer.promise;
    }],
    title: '登陸',
    resolve: {
        deps: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad)=> {
            let defer = $q.defer();
            require.ensure([], ()=> {
                /**
                *let module = LoginModule(angular);  
                *注意import導入LoginModule方法與require('./login.module')直接引用LoginModule方法是有區別的,
                *import導入LoginModule方法不能分離js
                */
                let module = require('./login.module').LoginModule(angular);   //動態加載Module
                $ocLazyLoad.load({
                    name: 'login'                                              //name就是你module的名稱
                });
                defer.resolve(module);
            });
            return defer.promise;
    }],
}

login.module.js

import {loginControllerFunc} from "./login.controller";
import {loginServiceFunc} from "./login.service";

export function LoginModule(Angular) {
    const loginModule = Angular.module('login', []);   //login名稱就是$ocLazyLoad.load中的name;
    loginControllerFunc(loginModule);   //注入模塊controller
    loginServiceFunc(loginModule);      //注入模塊service
}

注:$ocLazyLoad動態加載的module不須要在其餘模塊中引入,如angular.module('webapp',['login']),這是錯誤的

到這裏用ES6構建anguar1項目就基本告一段落了。若是有更好的ES6寫法,歡迎各位評說。
文章有不對的地方,望大神們指出。
項目地址請戳這裏

相關文章
相關標籤/搜索