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

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

前言

前篇我們已經可以透過 webpack + Angular2 跑出 Hot Reload ,但其實我們還有許多事情要繼續整合處理, 例如我們希望可以透過 npm start 進行打包 ( webpack-dev-server 是不會產出實體檔案的.. ) 或是能分開 webpack.config 的設定等等,所以,這篇我們會繼續針對這些繼續設定下去。

注意事項

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

分離 webpack.config.js

在原始的 source code 裡面,大神是將 webpack.config.js 進行分離,分別有

  • webpack.common.js - 負責共用的設定
  • webpack.dev.js - 負責開發用的設定
  • webpack.prod.js - 負責正式環境的設定
  • webpack.test.js - 負責測試環境的設定

而我們這邊,會先來看看,如何拆出 webpack.common.js 和 webpack.dev.js。

安裝 webpack-merge

基本上,webpack 沒有提供這種合併多個 config 的功能,所以我們要透過 webpack-merge package 來達到此需求。

同樣的,我們直接看 package.json ,這樣大家才可以得知,目前這篇文章的所有 package 版本與目前安裝狀況。

這邊我們多增加了 "webpack-merge": "^0.8.4" 。

完整 package.json

{
  "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",
    "webpack-merge": "^0.8.4"
  }
}

完成後,執行 npm install 安裝。

分離 webpack.config.js

接下來,其實沒甚麼技巧可言,大家可以依據自己的需求,將常共用的 webpack.config 移到 webpack.common.js 裡面, 但我們這邊依照大神的 source 來做這件事情。

首先我們先把 webpack.config.js 拷貝一份到 config 目錄底下,並且改名為 webpack.common.js 如下圖:

01

因為是 common ,所以我們移除

const DefinePlugin = require('webpack/lib/DefinePlugin');

既然 DefinePlugin require 移除了,那 plugins 底下的 DefinePlugin 自然也移除了

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

接下來,也把 tslint 和 devServer 移除

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

P.S 如果大家有去看 Source ,會發現有一個 Metadata 如下兩個 source code , 神人作者對於 Metadata 其實有兩個用途,一個是拿來輸出到網頁上,例如 title . 另外一個是拿來使用設定環境變數;基本上輸出 title 的部分,我就會直接拿掉了。

/*
 * Webpack Constants
 */
const METADATA = {
  title: 'Angular2 Webpack Starter by @gdi2290 from @AngularClass',
  baseUrl: '/'
};
 /*
   * Static metadata for index.html
   *
   * See: (custom attribute)
   */
  metadata: METADATA,

完整的 webpack.common.js

/**
 * @author: @AngularClass
 */

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

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

/*
 * 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/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'])
    }),
  ],
  
  node: {
    global: 'window',
    crypto: 'empty',
    process: true,
    module: false,
    clearImmediate: false,
    setImmediate: false
  }
};

那接下來,我們就要調整原始的 webpack.config.js

調整 webpack.config.js

我們改成如下,首先,我們會引用 webpack-merga;另外,我們這邊開始啟用 ENV , HRM , METADATA 環境的變數。

P.S 在原本的 Source Code 裡面,有使用到 webpackMerge(commonConfig.metadata, 整合 commonConfig 的 metadata, 但是在原本的 Source Code 裡面,MetaData 的定義 ,主要是用來顯示於 index 上。 基本上這是不需要的,所以 webpack.config.js 的這邊這段,我就移除了 webpackMerge 。

const METADATA = {
  title: 'Angular2 Webpack Starter by @gdi2290 from @AngularClass',
  baseUrl: '/'
};

另外,要特別注意 require 的路徑,因為路徑換了,所以要加上 config

P.S 或許大家有注意到,node,debug, devtool, output 都有重複.. 為什麼需要重複,其實小弟我沒有在去做額外的嘗試,但 source 是有保留的。

完整的 webpack.config.js

/**
 * @author: @AngularClass
 */

const helpers = require('./config/helpers');
const webpackMerge = require('webpack-merge'); // used to merge webpack configs
const commonConfig = require('./config/webpack.common.js'); // the settings that are common to prod and dev

/**
 * Webpack Plugins
 */
const DefinePlugin = require('webpack/lib/DefinePlugin');

/**
 * Webpack Constants
 */
const ENV = process.env.ENV = process.env.NODE_ENV = 'development';
const HMR = helpers.hasProcessFlag('hot');
const METADATA = {
  host: 'localhost',
  port: 3000,
  ENV: ENV,
  HMR: HMR
};

/**
 * Webpack configuration
 *
 * See: http://webpack.github.io/docs/configuration.html#cli
 */
module.exports = webpackMerge(commonConfig, {

  metadata: METADATA,

  debug: true,

  devtool: 'cheap-module-eval-source-map',

  output: {
    path: helpers.root('dist'),
    filename: '[name].bundle.js',
    sourceMapFilename: '[name].map',
    chunkFilename: '[id].chunk.js'

  },

  plugins: [

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

  tslint: {
    emitErrors: false,
    failOnHint: false,
    resourcePath: 'src'
  },

  devServer: {
    port: METADATA.port,
    host: METADATA.host,
    historyApiFallback: true,
    watchOptions: {
      aggregateTimeout: 300,
      poll: 1000
    }
  },
  
  node: {
    global: 'window',
    crypto: 'empty',
    process: true,
    module: false,
    clearImmediate: false,
    setImmediate: false
  }

});

最後,我們還希望做一件事情,調整一下,npm 的 scripts,讓我們能更輕鬆地執行測試環境。

調整 package.json

我們重新調整了 npm scripts 裡面的項目,我們把 tsc、tsc:w、lite 拿掉, 因為這幾個都整合到 webpack,或是不使用了... 而增加了 server 的區段,未來我們就可以使用

  • npm run server:dev 來啟動開發環境 ( 不包含 hot reload )
  • npm run server:dev:hmr 來啟動開發環境 ( 包含 hot reload )
  • npm run server:prod 來啟動正式環境

( npm run server:prod 現階段會無法使用,這篇文章的最後,會裝上 http-server 就可以正常運作了 )

{
  "name": "angular2-quickstart",
  "version": "1.0.0",
  "scripts": {
    "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "typings": "typings",
    "postinstall": "typings install",
    
    "server": "npm run server:dev",
      "server:dev": "webpack-dev-server --config webpack.config.js --inline --progress --profile --colors --watch --display-error-details --display-cached --content-base src/",
      "server:dev:hmr": "npm run server:dev -- --hot",
      "server:prod": "http-server dist --cors"
    
  },
  "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",
    "webpack-merge": "^0.8.4"
  }
}

完成後就可以 npm run server:dev:hmr 試試看。

增加 webpack.dev.js

到這邊,已經越來越方便了,但我們正式環境,可能會有一組設定檔,或是其他環境有其他的設定檔, 所以我們希望把 webpack.config.js 也放到 config 目錄底下。

所以我們就將 webpack.config.js 移到 config 目錄底下,並且改名為 webpack.dev.js

02

因為目錄改變了,所以要調整一下 require 的路徑,把 config 拿掉。

完整 webpack.dev.js 如下

/**
 * @author: @AngularClass
 */

const helpers = require('./helpers');
const webpackMerge = require('webpack-merge'); // used to merge webpack configs
const commonConfig = require('./webpack.common.js'); // the settings that are common to prod and dev

/**
 * Webpack Plugins
 */
const DefinePlugin = require('webpack/lib/DefinePlugin');

/**
 * Webpack Constants
 */
const ENV = process.env.ENV = process.env.NODE_ENV = 'development';
const HMR = helpers.hasProcessFlag('hot');
const METADATA = {
  host: 'localhost',
  port: 3000,
  ENV: ENV,
  HMR: HMR
};

/**
 * Webpack configuration
 *
 * See: http://webpack.github.io/docs/configuration.html#cli
 */
module.exports = webpackMerge(commonConfig, {

  metadata: METADATA,

  debug: true,

  devtool: 'cheap-module-eval-source-map',

  output: {
    path: helpers.root('dist'),
    filename: '[name].bundle.js',
    sourceMapFilename: '[name].map',
    chunkFilename: '[id].chunk.js'

  },

  plugins: [

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

  tslint: {
    emitErrors: false,
    failOnHint: false,
    resourcePath: 'src'
  },

  devServer: {
    port: METADATA.port,
    host: METADATA.host,
    historyApiFallback: true,
    watchOptions: {
      aggregateTimeout: 300,
      poll: 1000
    }
  },
  
  node: {
    global: 'window',
    crypto: 'empty',
    process: true,
    module: false,
    clearImmediate: false,
    setImmediate: false
  }

});

調整 package.json

我們就在 server:dev --config 改成 config/webpack.dev.js

{
  "name": "angular2-quickstart",
  "version": "1.0.0",
  "scripts": {
    "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "typings": "typings",
    "postinstall": "typings install",
    
    "server": "npm run server:dev",
      "server:dev": "webpack-dev-server --config config/webpack.dev.js --inline --progress --profile --colors --watch --display-error-details --display-cached --content-base src/",
      "server:dev:hmr": "npm run server:dev -- --hot",
      "server:prod": "http-server dist --cors"
    
  },
  "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",
    "webpack-merge": "^0.8.4"
  }
}

完成後,可以在 npm run server:dev:hmr 來測試看看

增加編譯環境

接下來,我們要準備增加 build 的設定,我們希望透過

  • npm run build:dev 編譯出 dev 環境的檔案
  • npm run build:prod 編譯出 正式環境的檔案

修改 package.json

所以我們先修改 package.json,這邊增加了 build 的相關設定,比較需要注意的是,prebuild:dev 和 prebuild:prod 與裡面。

prebuild:dev 代表著,執行 build:dev 之前,他會先跑 prebuild:dev ,而 prebuild:dev 裡面的 npm run clean:dist 其實就是要清除舊的檔案資訊,而 clean:dist 就是 clean 那個區段的設定,目的就是刪除 dist 目錄的所有檔案。

而他主要是用到了 rimraf 這個 package ,所以底下的 package 也要再加上 "rimraf": "^2.5.2" 。 並且在 scripts 裡面也加上 rimraf ,不然 npm run rimraf 會錯誤。

另外,等下的 webpack.prod.js 會使遇到以下 plugin ,所以我們也一併裝上

{
  "name": "angular2-quickstart",
  "version": "1.0.0",
  "scripts": {
    "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "rimraf": "rimraf",
    "typings": "typings",
    "postinstall": "typings install",
    
    "clean": "npm cache clean && npm run rimraf -- node_modules doc typings coverage dist",
      "clean:dist": "npm run rimraf -- dist",
    
    "build": "npm run build:dev",
        "prebuild:dev": "npm run clean:dist",
      "build:dev": "webpack --config config/webpack.dev.js --progress --profile --colors --display-error-details --display-cached",
        "prebuild:prod": "npm run clean:dist",
      "build:prod": "webpack --config config/webpack.prod.js  --progress --profile --colors --display-error-details --display-cached --bail",
    
    "server": "npm run server:dev",
      "server:dev": "webpack-dev-server --config config/webpack.dev.js --inline --progress --profile --colors --watch --display-error-details --display-cached --content-base src/",
      "server:dev:hmr": "npm run server:dev -- --hot",
      "server:prod": "http-server dist --cors"
    
  },
  "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",
    "webpack-merge": "^0.8.4",
    "rimraf": "^2.5.2",
    "compression-webpack-plugin": "^0.3.1",
    "webpack-md5-hash": "^0.0.5",
    "http-server": "^0.9.0"
  }
}

完成後,先用 npm install 裝一下 rimraf

P.S 原始文件的 clean 其實還有其他設定,但似乎用不到,所以我就先移除了。

{
 "clean": "npm cache clean && npm run rimraf -- node_modules doc typings coverage dist",
      "clean:dist": "npm run rimraf -- dist",
   "preclean:install": "npm run clean",
      "clean:install": "npm set progress=false && npm install",
        "preclean:start": "npm run clean",
      "clean:start": "npm start",
}

加上 webpack.prod.js 檔案

最後,我們於 config 加上 webpack.prod.js 上。

/**
 * @author: @AngularClass
 */

const helpers = require('./helpers');
const webpackMerge = require('webpack-merge'); // used to merge webpack configs
const commonConfig = require('./webpack.common.js'); // the settings that are common to prod and dev

/**
 * Webpack Plugins
 */
const ProvidePlugin = require('webpack/lib/ProvidePlugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const DedupePlugin = require('webpack/lib/optimize/DedupePlugin');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const CompressionPlugin = require('compression-webpack-plugin');
const WebpackMd5Hash = require('webpack-md5-hash');

/**
 * Webpack Constants
 */
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 8080;
const METADATA = {
  host: HOST,
  port: PORT,
  ENV: ENV,
  HMR: false
};

module.exports = webpackMerge(commonConfig, {

  debug: false,
  devtool: 'source-map',
  output: {
    path: helpers.root('dist'),
    filename: '[name].[chunkhash].bundle.js',
    sourceMapFilename: '[name].[chunkhash].bundle.map',
    chunkFilename: '[id].[chunkhash].chunk.js'
  },

  plugins: [

    new WebpackMd5Hash(),
    new DedupePlugin(),
    new DefinePlugin({
      'ENV': JSON.stringify(METADATA.ENV),
      'HMR': METADATA.HMR,
      'process.env': {
        'ENV': JSON.stringify(METADATA.ENV),
        'NODE_ENV': JSON.stringify(METADATA.ENV),
        'HMR': METADATA.HMR,
      }
    }),
    new UglifyJsPlugin({
      // beautify: true, //debug
      // mangle: false, //debug
      // dead_code: false, //debug
      // unused: false, //debug
      // deadCode: false, //debug
      // compress: {
      //   screw_ie8: true,
      //   keep_fnames: true,
      //   drop_debugger: false,
      //   dead_code: false,
      //   unused: false
      // }, // debug
      // comments: true, //debug

      beautify: false, //prod

      mangle: {
        screw_ie8 : true,
        keep_fnames: true
      }, //prod

      compress: {
        screw_ie8: true
      }, //prod

      comments: false //prod
    }),

    new CompressionPlugin({
      regExp: /\.css$|\.html$|\.js$|\.map$/,
      threshold: 2 * 1024
    })

  ],

  tslint: {
    emitErrors: true,
    failOnHint: true,
    resourcePath: 'src'
  },

  htmlLoader: {
    minimize: true,
    removeAttributeQuotes: false,
    caseSensitive: true,
    customAttrSurround: [
      [/#/, /(?:)/],
      [/\*/, /(?:)/],
      [/\[?\(?/, /(?:)/]
    ],
    customAttrAssign: [/\)?\]?=/]
  },

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

});

加上後,我們就可以 npm run build:prod 來產生正式的檔案 ( 會放在 dist 下 )

然後就可以使用 npm run server:prod 來啟動 Server

參考資料

Sky & Study4.TW