AngularJS + Gulp → Webpack

  1. AngularJS + Gulp → Webpack
    1. 源起
    2. 技術上的現況
    3. 用 npm 安裝 AngularJS
    4. 加入 Webpack
      1. 了解原本 Gulp 的運作。
      2. 開始 Webpack
    5. 開始一一解決問題
      1. 模組相依順序
      2. 立即時執行函數前的 require 要加分號
      3. JS 套件相依順序
      4. angular-route 套件版本匹配問題
      5. bootstrap 要用的全域 jQuery
    6. 修正 router
      1. angular-router 的 hash 設定
      2. angular-route 套件版本匹配問題
    7. 模組劃分
      1. controller -> component 的 bindings 變數初始化問題
    8. 時間格式問題
    9. alasql 和 xlsx 的問題
    10. Webpack 編 prod 不會動, dev 正常
      1. 找線索
    11. 整理線索
    12. 檢視 route 可能出問題的地方
      1. Webpack 的 prod 編出來的 route 就會動了
    13. 最後

AngularJS + Gulp → Webpack

源起

目前有許多的舊專案,由於當年很潮人做了很潮的決定,使用了當時唯一首選 AngularJS。
也許種種原因,這個專案後來並沒有健康的跟著長大人與時俱進,到現在跑在 AngularJS 上。

也許是收得不夠所以不想幫專案更新?
也許是覺得大工程,所以接案不用這麼費力的更新這些?
也許,有太多的也許了…

但是這些也許在有一天就是要打破,混沌的世界將會有勇者來…不對。
這樣的專案累積下來的技術債,要評估你的專案生命週期是否還有維護價值,再決定是否要繼續之後的重構步驟。

技術上的現況

目前網路上的 AngularJS 教學,還在教你使用 gulp 和 bower 來進行套件管理與網站相依性管理。也許可以使用 babel 讓你使用新一代的 ES 語法,但是這樣是遠遠不足時代的需求。
就連 bower 都建議搬家,別再使用 bower 了

這篇來分享「如何將 AngularJS 放到 webpack 上」讓你的專案可以與時俱進,帶你經過困難的第一關!!!

經過這篇,你的 AngularJS 的工具使用如下:

  • 套件管理器會使用 npm
  • 網站打包工具使用 webpack

就如同現代前端框架的配備一樣先進呢!!!

用 npm 安裝 AngularJS

用 npm 安裝 AngularJS[1]

在此要加上版號,限制只安裝 AngularJS (最後的版本是 1.7.x)
(安裝 AngularJS 1.5 以上的版本,才可以使用 Component 未來框架的重構會更現代化)

$ npm install --save-dev angular@1.7

加入 Webpack

了解原本 Gulp 的運作。

先看看你自己的 Gulp 專案上的 task 怎麼設定的
在此,我手上的專案是將所有的 js 檔案串接成 all.js。(之後的 webpack 也是要做這件事)

var gulp = require('gulp');
var concat = require('gulp-concat');
var gulpLivereload = require('gulp-livereload');

gulp.task('all', () =>
  gulp
    .src(['./js/**/*.js','./app.js'])
    .pipe(concat('all.js'))
    .pipe(gulp.dest('./'))
    .pipe(gulpLivereload()));

gulp.task('watch', () => {
  gulpLivereload.listen();
  gulp.watch("./js/**/*.js", ["all"]);
  gulp.watch("./app.js", ["all"]);
})

開始 Webpack

安裝 webpack

$ npm install --save-dev webpack

安裝 dev-server

$ npm install --save-dev webpack-dev-server

config
在 webpack.config.js 中的 outputFileName 設定。

原本由 Gulp 將 *.js 編成 all.js
改成用 Webpack 編出 all.js

const path = require('path');

module.exports = {
  entry: './entryFileName.js',
  output: {
    filename: 'outputFileName.js',
    path: path.resolve(__dirname, '')
  },
  devServer: {
    contentBase: './'
  },
  mode: "production"
};

然後開始 try-error 的看看頁面是不是有正常運作。
下面是我遇到的一些問題,提供當案例參考。

開始一一解決問題

模組相依順序

主要是發生在這樣的 source code

angular.module('myApp', ['team']);

彷彿 angular module 的中括號會失去作用

瀏覽器 Console 的錯誤訊息

Uncaught Error: [$injector:modulerr]
Failed to instantiate module myApp due to:

Error: [$injector:modulerr]
Failed to instantiate module team due to:

Error: [$injector:nomod]
Module ‘team’ is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.

require('path/controller') 的相依性管理,將重新管理 angular.module(name, [controller]) 裡的 [controller] 套件
重新整理整個 project 的 javascript 檔的相依關係。

(不知道要不要刪掉,確定完再來刪)

example

require('./module1');   // 增加這幾行
require('./module2');   // 增加這幾行
require('./module3');  // 增加這幾行

angular.module("module", ['module1', 'module2', 'module3']);

立即時執行函數前的 require 要加分號

錯誤訊息

Uncaught TypeError: __webpack_require__(…) is not a function

原本的寫法若是立即執行

(function() {
  var app = angular.module("//...
  //...
})();

前面加上 require() 後,要記得加分號 ;

require('module1'); //沒問題
require('module1')  //出問題

(function() {
  var app = angular.module("//...
  //...
})();

JS 套件相依順序

使用 bower 的套件要改用 npm 安裝。(注意版本)
原本使用 html script tag 的方式引入,都要改由 javascript 的 require() 的方式引入。

angular-route 套件版本匹配問題

錯誤訊息

angular.js:15536 Possibly unhandled rejection: undefined

這是一個版本的問題,網路上教學建議使用下面的做法,是不好的[2]

$qProvider.errorOnUnhandledRejections(false)。

這個問題是一個已被解決的 bug[3]

package.json

原本

{
  "angular": "^1.7.4",
  "angular-route": "^1.5.7",
}

angular-route 換新版

{
  "angular": "^1.7.4",
  "angular-route": "^1.5.11",
}

AngularJS 適用的 angular-route 版本最新就是 1.5.11

bootstrap 要用的全域 jQuery

錯誤訊息

Uncaught ReferenceError: jQuery is not defined

發生在,把下面這段程式

window.jQuery = require('jquery');

改成這樣

import $ from 'jquery';
window.jQuery = $;

解決方式[4]

webpack.config.js

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      'jQuery': 'jquery'
    })
  ],
}

在 config 加上這個可以宣告成全域變數 window.jQuery

修正 router

網址是否樣子一樣?

angular-router 的 hash 設定

原本的網址

https://host.name/#/hash

出錯的網址

https://host.name/#!/hash

解決方式

加入 $locationProvider 處理網址[5]

angular.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
  $locationProvider.hashPrefix('');
  //...
}])

angular-route 套件版本匹配問題

錯誤訊息

angular.js:15536 Possibly unhandled rejection: undefined

這是一個版本的問題,網路上教學建議使用下面的做法,是不好的[2:1]

$qProvider.errorOnUnhandledRejections(false)。

這個問題是一個已被解決的 bug[3:1]

package.json

原本

{
  "angular": "^1.7.4",
  "angular-route": "^1.5.7",
}

angular-route 換新版

{
  "angular": "^1.7.4",
  "angular-route": "^1.5.11",
}

AngularJS 適用的 angular-route 版本最新就是 1.5.11

模組劃分

controller -> component 的 bindings 變數初始化問題

錯誤訊息

TypeError: Cannot read property ‘value’ of undefined

angular.module("dataTable", []).component("dTable", {
  //...
  controller: myController,
  bindings: {
    init: '<',
    //...
  },
  //...
});

function myController (/* ... */) {
  //...
  this.init.value //error
  //...
}

簡單的說就是 component 裡 bindings 的變數,並沒有在 controller function 的 this 裡。

解決方式

在 AngularJS 的 component 裡,bindings 的變數,必須要等 $onInit 時期,才會建構完成。

所以,有關 bindings 變數的操作都要放到 $onInit 裡~~,至於為什麼之前可以跑,我就不知道了~~,之前可以跑的原因,是因為 controller 並沒有 lifecycle 所以將 controller 這個 function 跑完就執行變數初始化。

function myController (/* ... */) {
  //...
  this.$onInit = function () {
    this.init.value
  }
  //...
}

時間格式問題

https://stackoverflow.com/questions/30537886/error-ngmodeldatefmt-expected-2015-05-29t190616-693209z-to-be-a-date-a

原本就有的 bug 暫不處理

alasql 和 xlsx 的問題

這個問題,發生在已加上 alasql 套件相依的檔案上,而且還有使用 XLSX 的 alasql 語法。

錯誤訊息

angular.js:15536 Error: Please include the xlsx.js library
    at getXLSX (alasql.js:4299)
    at Object.alasql.into.XLSX (alasql.js:17035)
    at alasql.Query.eval [as intoallfn] (eval at yy.Select.compile (alasql.js:NaN), <anonymous>:3:33)
    at queryfn3 (alasql.js:7179)
    at queryfn2 (alasql.js:6917)
    at Object.eval [as datafn] (eval at <anonymous> (alasql.js:NaN), <anonymous>:3:57)
    at eval (alasql.js:6865)
    at Array.forEach (<anonymous>)
    at queryfn (alasql.js:6861)
    at statement (alasql.js:8009) "Possibly unhandled rejection: {}"

要先知道, alasql 本來就有相依性於 xlsx

alasql 本身有個 bug

以 browser 執行環境為例!!

下面的 getXLSX() 一直都會 return null

var getXLSX = function() {
  var XLSX = alasql["private"].externalXlsxLib;

  if (XLSX) {
    return XLSX;
  }

  if (utils.isNode || utils.isBrowserify || utils.isMeteorServer) {
    /*not-for-browser/*
    XLSX = require('xlsx') || null;
    //*/
  } else {
    XLSX = utils.global.XLSX || null;
  }

  if (null === XLSX) {
    throw new Error('Please include the xlsx.js library');
  }

  return XLSX;
};

這一段程式碼出了問題,所以造成無法順利運作
目前尚未有好的解決方案。

不過有找到臨時解決方式[6],我自己是以下面的步驟解決

安裝

如果要用 alasql + xlsx 的話,一定要另外自己安裝 xlsx 套件。

$ npm install --save alasql xlsx

關於 xls 的套件,非常多。
有 xls, xlsjs, js-xlsx … 太多啦!這些是我誤以為而安裝上去的

webpack.config.js

resolve: {
  alias: {
    "@": path.resolve(__dirname, './')
  }
}

using alasql 的檔案.js

import alasql from '@/alasql';

/alasql/index.js

import alasql from 'alasql';
import XLSX from 'xlsx';

alasql.utils.isBrowserify = false;
alasql.utils.global.XLSX = XLSX;

export default alasql;

重點是在使用時,強制設定。

alasql.utils.isBrowserify = false;
alasql.utils.global.XLSX = XLSX;

只是我把它集中起來,要使用時,再宣告 import alasql from '@/alasql';

Webpack 編 prod 不會動, dev 正常

沒有錯誤訊息

現象描述

用 webpack 編譯 prod 之後,開啟頁面,發現從登入頁登入之後, menu 會改變,而 登入畫面不會被替換成其它的頁面內容。

初判

反反覆覆測試之後

  1. menu 的顯示與否是用 API 回傳值控制 ng-show
  2. 登入畫面內容,是由 ngView 渲染
<body>
  <!-- <ng-include src="'./partial/navbar.html'"></ng-include> -->
  <!-- ngInclude: -->
  <ng-include src="'./partial/navbarByFunc.html'" class="ng-scope">
    <!-- menu -->
  </ng-include>
  <!-- ngView: -->
  <div ng-view="" class="ng-scope">
    <!-- form -->
  </div>
</body>

找線索

懷疑過

  • 是不是 webpack 的設定不正確
  • 是不是 babel 的設定不正確,或者要加更多的套件?

當然,這就花了很多時間排除

在茫茫大海中,無意間改改下面這段程式碼…(沒錯!單純的只是基於操 code 看不順眼想修改)

angular.config(['$routeProvider', function($routeProvider){
  $routeProvider.when('/',{/*...*/})  // 剛進網站 要進入登入頁

  var originalWhen = $routeProvider.when;
  $routeProvider.when = function(path, route) {
    // 重新寫一個 when 並且呼叫原本的 when
    return originalWhen.call($routeProvider, path, route);
  };

  /* other route*/
  $routeProvider.when (/* ... */)
  //...
}]);

4~12 行可以和 29 行合併成這樣寫

$routeProvider.when (/* ... */).when().when()//...

但是竟然改變了出錯的行為

發現新線索

開新的畫面,除了 menu 之外,畫面其它的地方都空白
仔細觀察發現 ngView 並沒有渲染任何東西。

<body>
  <!-- <ng-include src="'./partial/navbar.html'"></ng-include> -->
  <!-- ngInclude: -->
  <ng-include src="'./partial/navbarByFunc.html'" class="ng-scope">
    <!-- menu -->
  </ng-include>
  <!-- ngView: -->
</body>

整理線索

  1. 發現從登入之後,登入畫面出錯(不會被替換成其它的頁面內容) -> 登入後,的 ngView 不渲染
  2. 整理 route 的 code 出錯方式不同,原本是登入畫面的地方,變成一片空白 -> 發現 ngView 不渲染

route 出問題的機率很高。

檢視 route 可能出問題的地方

恢復這段程式成修改前的樣子。

route.js

angular.config(['$routeProvider', function($routeProvider){
  $routeProvider.when('/', { //暫時放著
    templateUrl: './../pages/login.html',
    controller: 'loginController as Ctrl',
    resolve: {
      validateAuth: [
        'funcService',
        function(funcService) {
          funcService.validateAuth("/");
        }
      ]
    }
  })

  const originalWhen = $routeProvider.when;
  $routeProvider.when = function(path, route) {
    route.resolve || (route.resolve = {});
    angular.extend(route.resolve, {
      validateAuth: function(funcService) {
        return funcService.validateAuth();
      }
    });
    return originalWhen.call($routeProvider, path, route);
  };

  $routeProvider.when('/default', {
    templateUrl: '../pages/default.html'
  })
}]);

先看看 $routeProvider 的用法[7] ,在此有一個 resolve 的進階用法,但是一般的範例的 resolve 用法如下[8]

網路上的範例

$routeProvider.when("/news", {
  templateUrl: "newsView.html",
  controller: "newsController",
  resolve: {
    message: function(messageService){
        return messageService.getMessage();
    }
  }
})

如同官網文件[7:1]介紹,它是一個 object ,而且是記錄「為特定頁面載入前執行的程式碼」

前面覆寫的 $routeProvider.when 的情況也不知道是不是正確的,就先將這些 resolve 都註解掉。

Webpack 的 prod 編出來的 route 就會動了

但是因為被註解掉的 code 沒有運作,所以網站沒有正常執行,但是 route 會其它頁面渲染出來了!!

這真是太振奮人心了!!

接下來就以語法的角度來觀察,這段 code 是在寫些什麼。
它是在換頁面之前,check 帳號的權限 …

好吧,看起來就是應該放在 route 的 hook 上。
不是什麼特例邏輯,而是每一頁都要執行的程式

AngularJS route 有供什麼 hook,在 AngularJS 裡,它叫 Event[9]

它提供了這幾個

  • $routeChangeStart
  • $routeChangeSuccess
  • $routeChangeError
  • $routeUpdate

AngularJS 的文件沒有 $routeChangeStart 的範例程式,所以另外找了一下[10]

angular.run(function($rootScope, $location) {
  $rootScope.$on( "$routeChangeStart", function(event, next, current) {
    if ($rootScope.loggedInUser == null) {
      // no logged user, redirect to /login
      if ( next.templateUrl === "partials/login.html") {
      } else {
        $location.path("/login");
      }
    }
  });
});

將程式碼搬過去,宣告好。

angular.run(function($rootScope, $location, funcService) {
  $rootScope.$on( "$routeChangeStart", function(event, next, current) {
    funcService.validateAuth(next.originalPath);
  });
})

route 就會是依設計上的正常運作。

最後

成功 build 出 /dist 的檔案之後,就可以拔掉 gulp 了,剩下的就是設定 webpack 讓它功能正常運作。


  1. AngularJS 1.x系列:Node.js安装及npm常用命令(1)2.2 npm包管理, npm install:安装包 ↩︎

  2. $q: Unhandled rejections should not be stringified #14631 ↩︎ ↩︎

  3. 1.6.1 promise-rectification (2016-12-23), Bug Fixes, $q: Add traceback to unhandled promise rejections (174cb4 #14631) ↩︎ ↩︎

  4. ProvidePlugin, Usage: jQuery - webpack ↩︎

  5. Webpack with angular route configuration - browser has valid config, cant resolve file - Stack Overflow ↩︎

  6. Webpack, Browserify, Vue and React (Native) ↩︎

  7. $routeProvider - AngularJS ↩︎ ↩︎

  8. Using Resolve In AngularJS Routes - Ode to Code ↩︎

  9. $route - AngularJS ↩︎

  10. Listening on Route Changes to Implement a Login Mechanism Problem ↩︎