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

前言

前篇介紹到,透過 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 目錄底下。

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

完整 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/**
* @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。

1
2
3
4
5
6
7
8
9
new DefinePlugin({
'ENV': JSON.stringify('development'),
'HMR': true,
'process.env': {
'ENV': JSON.stringify('development'),
'NODE_ENV': JSON.stringify('development'),
'HMR': true,
}
})

完整的 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"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 啟動。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 ( 目錄請自行建立喔 ) ,完成如下

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 底下,如下:

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

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

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

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

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

完整的 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* @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
}
};

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

參考資料