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

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

前言

前篇介紹到,透過 Webpack 進行 ts 的編譯,這篇,我們就來談談 Hot Reload.

注意事項

  • 此文章的版本為 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 裡面有很多的註解,但本篇文章,因為篇幅關係,小弟會把註解拿掉

將 app 目錄放到 src 目錄

當然,這一步不是必要的,但為了配合 angular2-webpack-stater 的目錄結構 ( 而且小弟我覺得這樣也不錯 ) 所以我們就先進行小幅度調整一下。

首先,我們就先把整個 app 目錄,拷貝到 src 目錄底下。

01

接著,調整一下 webpack.config.js,將 main 的地方改成 'main': './src/app/main.ts', exclude 也改成 exclude: [helpers.root('src/app/index.html')]

完整 webpack.config.js

/**
 * @author: @AngularClass
 */

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

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': './src/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('src/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
  }
};

完成後,執行 webpack-dev-server 測試看看。

開啟 Hot Reload

既然要開啟 Hot Reload ,那我們就必須修改一下 webpack.config.js ,我們可以從 DefinePlugin 找到 HMR ,並改成 true。

 new DefinePlugin({
      'ENV': JSON.stringify('development'),
      'HMR': true,
      'process.env': {
        'ENV': JSON.stringify('development'),
        'NODE_ENV': JSON.stringify('development'),
        'HMR': true,
      }
    })

完整的 webpack.config.js

/**
 * @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');

/*
 * Webpack configuration
 *
 * See: http://webpack.github.io/docs/configuration.html#cli
 */
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': './src/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('src/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': true,
      'process.env': {
        'ENV': JSON.stringify('development'),
        'NODE_ENV': JSON.stringify('development'),
        'HMR': true,
      }
    })

  ],
  
  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
  }
};

接著,要新增 angular2-hmr package

{
  "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",
    "angular2-hmr": "~0.5.5"
  }
}

加入後,使用 npm install 安裝。

接下來,我們要修改 main.ts , 我們要把原本的 bootstrap 註解掉,改由 hotModuleReplacement 啟動。

import {bootstrap}    from 'angular2/platform/browser';
import {AppComponent} from './app.component';

//bootstrap(AppComponent);

export function main(initialHmrState?: any): Promise<any> {

  return bootstrap(AppComponent, [
    //...PROVIDERS,
    //...ENV_PROVIDERS,
    //...DIRECTIVES,
    //...PIPES,
    //...APP_PROVIDERS
  ])
  .catch(err => console.error(err));

}

let ngHmr = require('angular2-hmr');
ngHmr.hotModuleReplacement(main, module);

完成後 webpack-dev-server --inline --hot 測試一下,這時候我們只要改 app.component.ts ,畫面就會即時更改 ( 不刷新 )

調整 CSS 位置

最後,因為我們的 CSS 位置還被放在根目錄下,但我們希望 CSS 也能移到 src 底下,所以我們就在細部的調整一下。

首先,我們把 CSS 移到 src 底下的 assets 底下的 css ( 目錄請自行建立喔 ) ,完成如下

02

接著,我們就可以調整 index.html 的 link rel 改成 src/assets/css/styles.css, 但是,多了 src ,基本上很醜...我們希望外面更本就不知道 src 這個目錄 ( 甚至未來把這堆資源綁到 dist 目錄下面... ) 所以我們希望的位置是 assets/css/styles.css 。 如下:

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

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

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

但這個時候執行,畫面的 css 就找不到了,因為這也合理,畢竟少了 src ,當然找不到。

所以我們要改一下執行命令,改成 webpack-dev-server --inline --content-base src/ --hot

content-base 的意思就是,他會把 src 目錄當成根目錄,所以所有資源就會從 src 底下開始。

把 index.html 也丟到 src 底下

等等 !! index.html 還在根目錄阿,大家都搬家搬走了,把 index.html 留在那邊,他粉可憐的。 所以我們這篇文章的最後一個步驟,就是也把 index.html 搬過去....

首先,我們一樣,把 index.html 移到 src 底下,如下:

03

接著,我們要講一下,我們前面沒有提到的 webpack HtmlWebpackPlugin。 這是一個 webpacl plugin ,透過這個 plugin ,可以輕鬆地產生出 bundle 後的樣板。( 例如可以協助調整要載入那些 script 之類的... ) 並且未來置放到指定的輸出目錄下。

而透過 template 變數,我們可以指定我們預設的 html 位置,他就會針對此 html 進行調整。

而 chunksSortMode 可以協助我們,依據未來要載入 bundle 後的 js ,定義載入順序。 ( vendor 目前我們還沒定義到 )

所以,我們要修改一下 webpack.config.js 把 HtmlWebpackPlugin 裡面的 template 參數調整成 template: 'src/index.html' 如下

 new HtmlWebpackPlugin({
      template: 'src/index.html',
      chunksSortMode: helpers.packageSort(['polyfills', 'vendor', 'main'])
    }),

完整的 webpack.config.js

/**
 * @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');

/*
 * Webpack configuration
 *
 * See: http://webpack.github.io/docs/configuration.html#cli
 */
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': './src/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('src/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',
      chunksSortMode: helpers.packageSort(['polyfills', 'vendor', 'main'])
    }),
    new DefinePlugin({
      'ENV': JSON.stringify('development'),
      'HMR': true,
      'process.env': {
        'ENV': JSON.stringify('development'),
        'NODE_ENV': JSON.stringify('development'),
        'HMR': true,
      }
    })

  ],
  
  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
  }
};

完成之後,我們就可以進行測試了。

參考資料

Sky & Study4.TW