Angular2 - 與 Webpack 整合環境設定 - 1

20 April 2016 — Written by Sky Chang
#JavaScript#Angular2#Webpack#TypeScript

前言

今年年初的時候,看著官方的 Quickstart, 迅速的學習了 Angular2 的架構,但對於官方文件提供的 Systemjs 和 lite-server,老實說覺得真的很難用.... ( 大概被 React + Webpack 養壞了.... ) 後來畢竟 Angular 也沒正式發佈,所以就丟在那邊, 而最近,因緣巧合的過程下,剛好被我看到國外的大神,已經把 Webpack 和 Angular2 做了整合 ( 而且還有 Hot Reolad !! ) 所以又興高采烈地回頭看一下,於是這篇文章就產生了....

這篇文章的目的,主要是自己在看裡面架構的部分,從無到有的重新建立過程,有興趣的朋友可以看看,如果覺得只想快點使用, 可以直接去 angular2-webpack-stater,下載修改。

注意事項

  • 此文章的版本為 angular2 beta 15 ,未來應該也不會跟著版本更新...請看的朋友注意
  • 裡面 Webpack 版本為 1.x , 2.x 可能會有幅度變動,請直接參考 angular2-webpack-stater
  • 此文章壟長,且未必能 100% 成功 ( 因 Package 相依性、Webpack 版本、angular2 版本 可能會造成差異 )
  • 有興趣的朋友,可以直接參考 angular2-webpack-stater Source Code
  • 文章內容的所有 Soucre 來源為 angular2-webpack-stater ,小弟只是針對內容進行解析紀錄
  • 文章開頭會以 Quickstart 為起始
  • 官方 Source Code 裡面有很多的註解,但本篇文章,因為篇幅關係,小弟會把註解拿掉

使用 Core.js 和 es7-reflect-metadata

在 angular2-webpack-stater 裡面,除了不使用 systemjs 和 lite-server 外,另外一個明顯的差異, 就是使用 es7-reflect-metadata 來取代 reflect-metadata ,而 Core.js 來取代 es6-shim。 ( Core.js 取代 es6-shim ,並非取代 angular 核心的 es6-shim ,目前 angular2 的核心還是使用 es6-shim, 這邊的 Core.js 只是取代我們編寫 TS , 轉譯成 JS 的部分,關於 Core.js 和 es6-shim 的討論)

這邊,我們就不用指令了,也不針對 npm 多多解說了,我們直接看 package.json 的差異。

調整 package.json

首先,es7-reflect-metadata 和 core-js 。其餘的就先不動...

{
  "name": "angular2-quickstart",
  "version": "1.0.0",
  "scripts": {
    "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "typings": "typings",
    "postinstall": "typings install"
  },
  "license": "ISC",
  "dependencies": {
    "angular2": "2.0.0-beta.15",
    "core-js": "^2.2.2",
    "systemjs": "0.19.26",
    "es6-shim": "^0.35.0",
    "es7-reflect-metadata": "^1.6.0",
    "rxjs": "5.0.0-beta.2",
    "zone.js": "0.6.10"
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "lite-server": "^2.2.0",
    "typescript": "^1.8.10",
    "typings":"^0.7.12"
  }
}

完成後 npm install

調整 typings.json

接下來,我們要調整 typings.json, ( typings Detail )

首先,我們要刪除 typings 目錄,然後調整如下,移除 es6-shim ,加上 core-js 和 zone.js。 會加入這兩個的原因,主要是等下會有流程 ( 開發、測試環境 ) 需要用到, 不加上的話, .ts 檔案會編輯不過。

另外,因為未來我們 coding ,都會直接使用 core-js ,所以就不用 es6-shim 了。

{
  "dependencies": {
    "zone.js": "github:gdi2290/typed-zone.js#66ea8a3451542bb7798369306840e46be1d6ec89"
  },
  "devDependencies": {},
  "ambientDependencies": {
    "core-js": "registry:dt/core-js#0.0.0+20160317120654",    
    "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#4b36b94d5910aa8a4d20bdcd5bd1f9ae6ad18d3c"
  }
}

完成後 npm install 讓 typings 重新抓。

調整 tsconfig.json

這邊,我們移除 "removeComments": false, 和 "noImplicitAny": false , 關於這兩個,可以參考下面的敘述,或是 tsconfig Detail

  • compilerOptions 為 Compiler 的選項。
  • target: "es5",
  • module: "commonjs" -- 定義模組為 module ( 官方使用 system.js 所以定義為 module": "system" )
  • moduleResolution: node - 判斷模型是如何解析 ( 如果是 node 表示為 Node.js/io.js style )
  • removeComments: false 關閉 移除註解
  • noImplicitAny: false 關閉 對表達式和聲明有一個隱含的'any'類型的錯誤
  • emitDecoratorMetadata: true - 使用 Decorator Metadata
  • experimentalDecorators: true - 啟用 ES7 的 Decorators
  • sourceMap: true - 產生 .map 檔案.

再往下看為 Atom-Typescript 的設定,目前 tsconfig 有提供給大家自行擴展,只要不要影響到原本的就可以, 所以有神人針對 Atom 編輯器,進行了與 TypeScript 整合,詳細可以參考 Atom-Typescript Detail

而這邊,感覺像是針對 Atom 環境,進行關閉,以利 Webpack 進行整合。

詳細的設定如下 : Detail

  • filesGlob: To make it easier for you to just add / remove files in your project we add filesGlob which accepts an array of glob / minimatch / RegExp patterns (similar to grunt) to specify source files.
  • compileOnSave : Should AtomTS compile on save
  • buildOnSave : Should AtomTS build on save
  • atom : Configuration specific to Atom.
  • rewriteTsconfig which prevents Atom from rewriting a project's tsconfig.json
  • awesomeTypescriptLoaderOptions 為 awesome-typescript-loader 之設定項目,主要提供 Webpack 進行設定。
  • resolveGlobs (string) (default=true) Invoke glob resolver using 'filesGlob' and 'exclude' sections of tsconfig.
  • forkChecker (boolean) (default=false) Do type checking in a separate process, so webpack doesn't need to wait. Significantly improves development workflow with tools like react-hot-loader.
{
  "compilerOptions": {
    "target": "es5",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
  ],
  "filesGlob": [
    "./src/**/*.ts",
    "./test/**/*.ts",
    "!./node_modules/**/*.ts",
    "src/custom-typings.d.ts",
    "typings/browser.d.ts"
  ],
  "awesomeTypescriptLoaderOptions": {
    "resolveGlobs": true,
    "forkChecker": true
  },
  "compileOnSave": false,
  "buildOnSave": false,
  "atom": { "rewriteTsconfig": false }
}

完成後,可以 npm start 看看,如果沒有錯誤,原本的應用程式是要還能執行的。

到這邊,我們還沒進行與 Webpack 的整合,只是切換了 Core.js 和 es7-reflect-metadata, 並且針對設定檔,預先進行準備。

安裝 Webpack

接下來,我們就要安裝 Webpack 和相關 Package

安裝 Webpack 相關 package

除了 Webpack 和 webpack-dev-server 外,我們首先要移除 concurrently , lite-server , Systemjs, 因為我們使用 Webpack 後,這三個都用不到了....

然後加上 copy-webpack-plugin , html-webpack-plugin , awesome-typescript-loader , source-map-loader , json-loader, raw-loader

{
  "name": "angular2-quickstart",
  "version": "1.0.0",
  "scripts": {
    "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "typings": "typings",
    "postinstall": "typings install"
  },
  "license": "ISC",
  "dependencies": {
    "angular2": "2.0.0-beta.15",
    "core-js": "^2.2.2",
    "es6-shim": "^0.35.0",
    "es7-reflect-metadata": "^1.6.0",
    "rxjs": "5.0.0-beta.2",
    "zone.js": "0.6.10"
  },
  "devDependencies": {
    "typescript": "^1.8.10",
    "typings":"^0.7.12",
    "webpack": "^1.13.0",
    "webpack-dev-server": "^1.14.1",
    "copy-webpack-plugin": "^1.1.1",
    "html-webpack-plugin": "^2.15.0",
    "awesome-typescript-loader": "~0.16.2",
    "source-map-loader": "^0.1.5",
    "json-loader": "^0.5.4",
    "raw-loader": "0.5.1"
  }
}

完成後 npm install 重新安裝

調整 tsconfig.json

接著,我們再調整一下 tsconfig.json ,把 "module": "system", 改成 "module": "commonjs", 因為我們的 module 改成 commonjs 了 ( 配合 Webpack )

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
  ],
  "filesGlob": [
    "./src/**/*.ts",
    "./test/**/*.ts",
    "!./node_modules/**/*.ts",
    "src/custom-typings.d.ts",
    "typings/browser.d.ts"
  ],
  "awesomeTypescriptLoaderOptions": {
    "resolveGlobs": true,
    "forkChecker": true
  },
  "compileOnSave": false,
  "buildOnSave": false,
  "atom": { "rewriteTsconfig": false }
}

調整 typings.json

接著,我們調整一下 typings.json ,加上 node 和 webpack,理由一樣等下會利用到。

{
  "dependencies": {
    "zone.js": "github:gdi2290/typed-zone.js#66ea8a3451542bb7798369306840e46be1d6ec89"
  },
  "devDependencies": {},
  "ambientDependencies": {
    "core-js": "registry:dt/core-js#0.0.0+20160317120654",    
    "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#4b36b94d5910aa8a4d20bdcd5bd1f9ae6ad18d3c",
    "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#8cf8164641be73e8f1e652c2a5b967c7210b6729",
    "webpack": "github:DefinitelyTyped/DefinitelyTyped/webpack/webpack.d.ts#95c02169ba8fa58ac1092422efbd2e3174a206f4"  
  }
}

完成後,執行 npm install

增加 helper.js

helper.js 是國外神人寫的 helper 工具,主要針對路徑去做一些 function,以利後面可以方便取得路徑。

我們只需要在根目錄下,建立一個 config 的目錄,並且把 helper.js 放進去。

/**
 * @author: @AngularClass
 */

var path = require('path');

// Helper functions
// __dirname 指向 目前 js 文件的目錄
// 底下類似 目前目錄,然後 cd ..
var _root = path.resolve(__dirname, '..');

console.log('root directory:', root());

// 判斷參數是否有 hot , 有的話為 True
function hasProcessFlag(flag) {
  // process.argv 為取得參數 ( 例如 node process-2.js one two=three four )
  // argv 陣列第一個為 node
  // join 會將陣列轉成字串,但字串中間會有 , 
  // 例如 [a,b] 會轉成 'a,b' ,如果寫成 join('') ,則會變成 ab 
  // 簡單的說,就是把 ',' 拿掉了...
  // 而底下 Code 就是代表將傳入進來的參數,從 node 開始,全部併成一串...然後看看裡面有沒有 flag 這個變數的值
  // 如果有,則回傳 true
  return process.argv.join('').indexOf(flag) > -1;
}

function root(args) {
  // 基本上 call 和 apply 都是改變上下文的方法。
  // http://wechat.kanfb.com/archives/60676
  // 也就是說會本質上,我們是呼叫 Array.prototype.slice() 這個方法,
  // 但是後面加上 call 後,記憶體的狀態,就變成 call 的第一個參數的狀態了..
  // 所以 Array.prototype.slice.call(arguments, 0); 的意思就是說
  // 使用 slice 方法,但讀取的是 arguments 這個陣列,而後面的參數 0 ,其實就是 slice 的第一個參數
  // 而 slice(0) 就是獲得所有資料
  // 為什麼要用 Call 來讓 arguments 呼叫 slice 方法呢?
  // 是因為 arguments 不是真的 array ,typeof arguments === "Object" 而不是 "Array"
  // 所以透過這個方法,我們就可以把 object 透過像是 Array 的方法做操作了
  
  // 而 arguments 的意思是 只要是 js 的 function 參數,都會變成 arguments global 變數
  // 總結的意思就是 : 將 arguments 裡面的東西,從 0 開始,全部輸出到 args 這個 array 裡面去
  args = Array.prototype.slice.call(arguments, 0);
  
  // 透過 [] 將 _root 文字塞到陣列 ,並且把 args 整併到陣列裡面
  // 所以,[_root].concat(args) 會產生包含 _root 字串的新陣列。
  // 而同樣的, apply 的意思和 Call 一樣。
  // 所以意思就是呼叫了 path.join 方法,但實際上的上下文為 path ( apply 裡面的第一個參數 )
  // 而帶入的參數,就是由 [_root].concat(args) 產生的陣列。
  // 所以這邊就是指把 path 路徑,在串上陣列裡面的字串。
  return path.join.apply(path, [_root].concat(args));
}

function rootNode(args) {
  args = Array.prototype.slice.call(arguments, 0);
  return root.apply(path, ['node_modules'].concat(args));
}

function prependExt(extensions, args) {
  args = args || [];
  if (!Array.isArray(args)) { args = [args] }
  return extensions.reduce(function(memo, val) {
    return memo.concat(val, args.map(function(prefix) {
      return prefix + val;
    }));
  }, ['']);
}

function packageSort(packages) {
  // packages = ['polyfills', 'vendor', 'main']
  var len = packages.length - 1;
  var first = packages[0];
  var last = packages[len];
  return function sort(a, b) {
    // polyfills always first
    if (a.names[0] === first) {
      return -1;
    }
    // main always last
    if (a.names[0] === last) {
      return 1;
    }
    // vendor before app
    if (a.names[0] !== first && b.names[0] === last) {
      return -1;
    } else {
      return 1;
    }
  }
}

function reverse(arr) {
  return arr.reverse();
}

exports.reverse = reverse;
exports.hasProcessFlag = hasProcessFlag;
exports.root = root;
exports.rootNode = rootNode;
exports.prependExt = prependExt;
exports.prepend = prependExt;
exports.packageSort = packageSort;

增加 webpack.config.js

接下來,我們在根目錄加上 webpack.config.js ,這主要是提供 webpack.config 設定。

/**
 * @author: @AngularClass
 */

const webpack = require('webpack');
const helpers = require('./config/helpers');

/*
 * Webpack Plugins
 */
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin;
const DefinePlugin = require('webpack/lib/DefinePlugin');

module.exports = {

  debug: true,
  devtool: 'cheap-module-eval-source-map',
  entry: {

    'polyfills': './src/polyfills.ts',
    //'vendor': './src/vendor.ts',
    //'main': './src/main.browser.ts'
    'main': './app/main.ts'
  },
  output: {
    path: helpers.root('dist'),
    filename: '[name].bundle.js',
    sourceMapFilename: '[name].map',
    chunkFilename: '[id].chunk.js'
  },
  resolve: {
    extensions: ['', '.ts', '.js'],
    root: helpers.root('src'),
    modulesDirectories: ['node_modules']
  },

  module: {
    preLoaders: [
      {
        test: /\.js$/,
        loader: 'source-map-loader',
        exclude: [
          // these packages have problems with their sourcemaps
          helpers.root('node_modules/rxjs'),
          helpers.root('node_modules/@angular2-material')
        ]
      }
    ],

    loaders: [
      {
        test: /\.ts$/,
        loader: 'awesome-typescript-loader',
        exclude: [/\.(spec|e2e)\.ts$/]
      },
      {
        test: /\.json$/,
        loader: 'json-loader'
      },
      {
        test: /\.css$/,
        loader: 'raw-loader'
      },
      {
        test: /\.html$/,
        loader: 'raw-loader',
        //exclude: [helpers.root('src/index.html')]
        exclude: [helpers.root('app/index.html')]
      }
    ]
  },

  plugins: [
    new ForkCheckerPlugin(),
    new webpack.optimize.OccurenceOrderPlugin(true),
    new webpack.optimize.CommonsChunkPlugin({
      name: helpers.reverse(['polyfills', 'vendor'])
    }),
    new CopyWebpackPlugin([{
      from: 'src/assets',
      to: 'assets'
    }]),
    new HtmlWebpackPlugin({
      //template: 'src/index.html',
      template: 'index.html',
      chunksSortMode: helpers.packageSort(['polyfills', 'vendor', 'main'])
    }),
    new DefinePlugin({
      'ENV': JSON.stringify('development'),
      'HMR': false,
      'process.env': {
        'ENV': JSON.stringify('development'),
        'NODE_ENV': JSON.stringify('development'),
        'HMR': false,
      }
    })

  ],
  
  tslint: {
    emitErrors: false,
    failOnHint: false,
    resourcePath: 'src'
  },
  
  devServer: {
    port: 3000,
    host: 'localhost',
    historyApiFallback: true,
    watchOptions: {
      aggregateTimeout: 300,
      poll: 1000
    }
  },

  node: {
    global: 'window',
    crypto: 'empty',
    process: true,
    module: false,
    clearImmediate: false,
    setImmediate: false
  }
};

增加 D.ts 定義檔

接下來,我們新增一個 src 目錄,並於裡面增加一個 custom-typings.d.ts ,此為自定義的 ts 定義檔。

// Extra variables that live on Global that will be replaced by webpack DefinePlugin
declare var ENV: string;
declare var HMR: boolean;
interface GlobalEnvironment {
  ENV;
  HMR;
}

interface WebpackModule {
  hot: {
    data?: any,
    idle: any,
    accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void;
    decline(dependencies?: string | string[]): void;
    dispose(callback?: (data?: any) => void): void;
    addDisposeHandler(callback?: (data?: any) => void): void;
    removeDisposeHandler(callback?: (data?: any) => void): void;
    check(autoApply?: any, callback?: (err?: Error, outdatedModules?: any[]) => void): void;
    apply(options?: any, callback?: (err?: Error, outdatedModules?: any[]) => void): void;
    status(callback?: (status?: string) => void): void | string;
    removeStatusHandler(callback?: (status?: string) => void): void;
  };
}

interface WebpackRequire {
  context(file: string, flag?: boolean, exp?: RegExp): any;
}


interface ErrorStackTraceLimit {
  stackTraceLimit: number;
}



// Extend typings
interface NodeRequire extends WebpackRequire {}
interface ErrorConstructor extends ErrorStackTraceLimit {}
interface NodeModule extends WebpackModule {}
interface Global extends GlobalEnvironment  {}


declare namespace Reflect {
  function decorate(decorators: ClassDecorator[], target: Function): Function;
  function decorate(
    decorators: (PropertyDecorator | MethodDecorator)[],
    target: Object,
    targetKey: string | symbol,
    descriptor?: PropertyDescriptor): PropertyDescriptor;

  function metadata(metadataKey: any, metadataValue: any): {
    (target: Function): void;
    (target: Object, propertyKey: string | symbol): void;
  };
  function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
  function defineMetadata(
    metadataKey: any,
    metadataValue: any,
    target: Object,
    targetKey: string | symbol): void;
  function hasMetadata(metadataKey: any, target: Object): boolean;
  function hasMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
  function hasOwnMetadata(metadataKey: any, target: Object): boolean;
  function hasOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
  function getMetadata(metadataKey: any, target: Object): any;
  function getMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
  function getOwnMetadata(metadataKey: any, target: Object): any;
  function getOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
  function getMetadataKeys(target: Object): any[];
  function getMetadataKeys(target: Object, targetKey: string | symbol): any[];
  function getOwnMetadataKeys(target: Object): any[];
  function getOwnMetadataKeys(target: Object, targetKey: string | symbol): any[];
  function deleteMetadata(metadataKey: any, target: Object): boolean;
  function deleteMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
}


// We need this here since there is a problem with Zone.js typings
interface Thenable<T> {
  then<U>(
    onFulfilled?: (value: T) => U | Thenable<U>,
    onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
  then<U>(
    onFulfilled?: (value: T) => U | Thenable<U>,
    onRejected?: (error: any) => void): Thenable<U>;
  catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
}

增加 polyfills.js

同樣,我們增加一個 polyfills.js 檔案到 src 底下,這主要來協助我們選擇 es6 ,es7 轉換 Package 的部分

// Polyfills
// (these modules are what are in 'angular2/bundles/angular2-polyfills' so don't use that here)

// import 'ie-shim'; // Internet Explorer
// import 'es6-shim';
// import 'es6-promise';
// import 'es7-reflect-metadata';

// Prefer CoreJS over the polyfills above
import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');

if ('production' === ENV) {
  // Production


} else {
  // Development

  Error.stackTraceLimit = Infinity;

  require('zone.js/dist/long-stack-trace-zone');

}

修改 index.html

接下來,index.html 的內容,可以通通砍掉,只留下底下就可以了。

index.html

<html>
  <head>
    <title>Angular 2 QuickStart</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">    
    <link rel="stylesheet" href="styles.css">
    
  </head>

  <script src="/webpack-dev-server.js"></script>

  <body>
    <my-app>Loading...</my-app>
  </body>
</html>

執行 webpack !!

完成之後,我們只要執行 webpack-dev-server 就可以啟動了!!! 透過 Webpack,可以再過程中幫我們編譯 TS to JS ,並且打包成一包而已~~

此時我們改個 angular component ,目前預設也擁有 Live-Reload ( Hot Reload 還沒啟動 )

參考資料

Sky & Study4.TW