Angular2 - 與 Webpack 整合環境設定 - 1
前言
今年年初的時候,看著官方的 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 還沒啟動 )
參考資料
- https://angular.io/docs/ts/latest/quickstart.html
- https://github.com/angularclass/angular2-webpack-starter
- [https://github.com/angular/angular/issues/5755]((https://github.com/angular/angular/issues/5755)
- https://github.com/typings/typings
- http://www.typescriptlang.org/docs/handbook/tsconfig.json.html
- https://github.com/s-panferov/awesome-typescript-loader