Webpack是一个广受欢迎的模块打包器, 这个工具用来把程序源码打包到一些方便易用的块中,以便把这些代码从服务器加载到浏览器中。
它是我们在文档中到处使用的SystemJS的一个优秀替代品。这篇指南会带我们尝尝Webpack的滋味,并解释如何在Angular程序中使用它。
目录
Contents
你还可以点这里下载最终结果。
什么是Webpack?
What is Webpack?
Webpack是一个强力的模块打包器。 所谓包(bundle)就是一个JavaScript文件,它把一堆资源(assets)合并在一起,以便它们可以在同一个文件请求中发回给客户端。 包中可以包含JavaScript、CSS样式、HTML以及很多其它类型的文件。
Webpack会遍历你应用中的所有源码,查找import
语句,构建出依赖图谱,并产出一个(或多个)包。
通过插件和规则,Webpack可以对各种非JavaScript文件进行预处理和最小化(Minify),比如TypeScript、SASS和LESS文件等。
我们通过一个JavaScript配置文件webpack.config.js
来决定Webpack做什么以及如何做。
入口与输出
Entries and outputs
我们给Webpack提供一个或多个入口文件,来让它查找与合并那些从这些入口点发散出去的依赖。
在下面这个例子中,我们的入口点是该应用的根文件src/app.ts
:
webpack.config.js (single entry)
entry: {
'app': './src/main.ts'
},
Webpack探查那个文件,并且递归遍历它的import
依赖。
src/main.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent { }
这里,Webpack看到我们正在导入@angular/core
,于是就这个文件加入到它的依赖列表里,为(有可能)把该文件打进包中做准备。
它打开@angular/core
并追踪由该文件的import
语句构成的网络,直到构建出从main.ts
往下的整个依赖图谱。
然后它把这些文件输出到当前配置所指定的包文件app.js
中:
output: {
filename: 'app.js'
}
这个app.js
输出包是个单一的JavaScript文件,它包含程序的源码及其所有依赖。
后面我们将在index.html
中用<script>
标签来加载它。
多重包
Multiple bundles
我们可能不会希望把所有东西打进一个巨型包,而更喜欢把多变的应用代码从相对稳定的第三方提供商模块中分离出来。
所以要修改配置,以获得两个入口点:main.ts
和vendor.ts
:
entry: {
app: 'src/app.ts',
vendor: 'src/vendor.ts'
},
output: {
filename: '[name].js'
}
Webpack会构造出两个独立的依赖图谱,并产出两个包文件:一个叫做app.js
,它只包含我们的应用代码;另一个叫做vendor.js
,它包含所有的提供商依赖。
在输出文件名中出现的[name]
是一个Webpack的占位符,它将被一个Webpack插件替换为入口点的名字,分别是app
和vendor
。插件在本章的稍后部分讲解。
要想告诉Webpack哪些文件属于vendor包,可以添加一个vendor.ts
文件,它只导入该应用的第三方模块:
src/vendor.ts
// Angular
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
// RxJS
import 'rxjs';
// Other vendors for example jQuery, Lodash or Bootstrap
// You can import js, ts, css, sass, ...
加载器(Loader)
Loaders
Webpack可以打包任何类型的文件:JavaScript、TypeScript、CSS、SASS、LESS、图片、HTML以及字体文件等等。 但Webpack本身只认识JavaScript文件。 我们要通过加载器来告诉它如何把这些文件处理成JavaScript文件。 在这里,我们为TypeScript和CSS文件配置了加载器。
rules: [
{
test:/\.ts$/,
loader: 'awesome-typescript-loader'
},
{
test: /\.css$/,loaders: 'style-loader!css-loader'
}
]
当Webpack遇到如下所示的import
语句时,它就会调用正则表达式的test
方法。
import { AppComponent } from './app.component.ts';
import 'uiframework/dist/uiframework.css';
如果一个模式匹配上文件名,Webpack就用它所关联的加载器处理这个文件。
第一个import
文件匹配上了.ts
模式,于是Webpack就用awesome-typescript-loader
加载器处理它。
导入的文件没有匹配上第二个模式,于是它的加载器就被忽略了。
第二个import
匹配上了第二个.css
模式,它有两个用叹号字符(!
)串联起来的加载器。
Webpack会从右到左逐个应用串联的加载器,于是它先应用了css
加载器(用来平面化CSS的@import
和url(...)
语句),
然后应用了style
加载器(用来把css追加到页面上的<style>元素中)。
插件
Plugins
Webpack有一条构建流水线,它被划分成多个经过精心定义的阶段(phase)。
我们可以把插件(比如uglify
代码最小化插件)挂到流水线上:
plugins: [
new webpack.optimize.UglifyJsPlugin()
]
配置Webpack
Configuring Webpack
经过简短的培训之后,我们准备为Angular应用构建一份自己的Webpack配置了。
从设置开发环境开始。
创建一个新的项目文件夹。
mkdir angular-webpack
cd angular-webpack
把下列文件添加到根目录下:
{
"name": "angular2-webpack",
"version": "1.0.0",
"description": "A webpack starter for Angular",
"scripts": {
"start": "webpack-dev-server --inline --progress --port 8080",
"test": "karma start",
"build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
},
"license": "MIT",
"dependencies": {
"@angular/common": "~4.0.0",
"@angular/compiler": "~4.0.0",
"@angular/core": "~4.0.0",
"@angular/forms": "~4.0.0",
"@angular/http": "~4.0.0",
"@angular/platform-browser": "~4.0.0",
"@angular/platform-browser-dynamic": "~4.0.0",
"@angular/router": "~4.0.0",
"core-js": "^2.4.1",
"rxjs": "5.0.1",
"zone.js": "^0.8.4"
},
"devDependencies": {
"@types/node": "^6.0.45",
"@types/jasmine": "2.5.36",
"angular2-template-loader": "^0.6.0",
"awesome-typescript-loader": "^3.0.4",
"css-loader": "^0.26.1",
"extract-text-webpack-plugin": "2.0.0-beta.5",
"file-loader": "^0.9.0",
"html-loader": "^0.4.3",
"html-webpack-plugin": "^2.16.1",
"jasmine-core": "^2.4.1",
"karma": "^1.2.0",
"karma-chrome-launcher": "^2.0.0",
"karma-jasmine": "^1.0.2",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.1",
"null-loader": "^0.1.1",
"raw-loader": "^0.5.1",
"rimraf": "^2.5.2",
"style-loader": "^0.13.1",
"typescript": "~2.0.10",
"webpack": "2.2.1",
"webpack-dev-server": "2.4.1",
"webpack-merge": "^3.0.0"
}
}
这些文件很多都很眼熟,它们在其他文档里已经出现过,特别是TypeScript配置和npm包这两章里。
Webpack,包括它的插件以及加载器,也是以npm包的形式安装的,它们也列在了修改后的 package.json 中。
打开命令行窗口并安装这些npm包
npm install
Polyfills
我们在浏览器支持章节里解释过,Angular应用要能在大多数的浏览器里运行,它还需要一些polyfills。
Polyfills最好跟应用代码和vendor代码区分开来单独打包,所以我们需要在src/
文件夹里添加一个polyfills.ts
文件,代码如下:
src/polyfills.ts
import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');
if (process.env.ENV === 'production') {
// Production
} else {
// Development and test
Error['stackTraceLimit'] = Infinity;
require('zone.js/dist/long-stack-trace-zone');
}
polyfills.ts
文件里,zone.js
库须尽早引入,紧跟在ES6 shims和metadata shims之后。
由于这个包最先加载,所以polyfills.ts
非常适合用来配置浏览器环境,如生产环境配置或是开发环境。
通用配置
Common configuration
开发、生产、测试等不同的环境通常会分开配置,但实际上这些配置也有很多地方是通用的。
我们可以把这些通用的配置收归到一个文件,命名为webpack.common.js
。
config/webpack.common.js
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');
module.exports = {
entry: {
'polyfills': './src/polyfills.ts',
'vendor': './src/vendor.ts',
'app': './src/main.ts'
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
loaders: [
{
loader: 'awesome-typescript-loader',
options: { configFileName: helpers.root('src', 'tsconfig.json') }
} , 'angular2-template-loader'
]
},
{
test: /\.html$/,
loader: 'html-loader'
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'file-loader?name=assets/[name].[hash].[ext]'
},
{
test: /\.css$/,
exclude: helpers.root('src', 'app'),
loader: ExtractTextPlugin.extract({ fallbackLoader: 'style-loader', loader: 'css-loader?sourceMap' })
},
{
test: /\.css$/,
include: helpers.root('src', 'app'),
loader: 'raw-loader'
}
]
},
plugins: [
// Workaround for angular/angular#11580
new webpack.ContextReplacementPlugin(
// The (\\|\/) piece accounts for path separators in *nix and Windows
/angular(\\|\/)core(\\|\/)@angular/,
helpers.root('./src'), // location of your src
{} // a map of your routes
),
new webpack.optimize.CommonsChunkPlugin({
name: ['app', 'vendor', 'polyfills']
}),
new HtmlWebpackPlugin({
template: 'src/index.html'
})
]
};
webpack.common.js解读
Inside webpack.common.js
Webpack是基于NodeJS的一个工具,它能够从一个commonjs规范的JavaScript模块文件里读取配置。
这个配置文件是通过require
语句导入依赖,然后将多个对象作为module.exports
对象的属性导出。
-
entries
- 包体的入口文件。 -
resolve
- 省略扩展名时如何解释文件名。 -
module.rules
-module
是一个对象,里面的rules
属性用来决定文件如何加载。 -
plugins
- 创建插件的实例。
entry
如上所述,第一个导出的对象是entries:
config/webpack.common.js
entry: {
'polyfills': './src/polyfills.ts',
'vendor': './src/vendor.ts',
'app': './src/main.ts'
},
entry
对象定义了三个包:
-
polyfills
- 使得Angular应用能够运行在大多数的现代浏览器。 -
vendor
- 第三方依赖,如Angular、lodash和bootstrap.css。 -
app
- 应用代码。
resolve 无扩展名的文件导入
resolve extension-less imports
如果你的应用程序只须import
几十个JavaScript或TypeScript文件,而不是几百个,你可以在import
语句里完整写上扩展名,如:
import { AppComponent } from './app.component.ts';
但实际上大部分import
语句都不带扩展名,我们可以告诉Webpack,在查找这些没有扩展名的文件时,自动加上.ts
或者.js
扩展名来匹配。
config/webpack.common.js
resolve: {
extensions: ['.ts', '.js']
},
如果我们希望Webapck也能解析不带扩展名的样式和HTML文件,在列表里追加.css
和.html
即可。
module.rules
Rules用来告诉Webpack加载不同文件或模块时该用哪个加载器。
config/webpack.common.js
module: {
rules: [
{
test: /\.ts$/,
loaders: [
{
loader: 'awesome-typescript-loader',
options: { configFileName: helpers.root('src', 'tsconfig.json') }
} , 'angular2-template-loader'
]
},
{
test: /\.html$/,
loader: 'html-loader'
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'file-loader?name=assets/[name].[hash].[ext]'
},
{
test: /\.css$/,
exclude: helpers.root('src', 'app'),
loader: ExtractTextPlugin.extract({ fallbackLoader: 'style-loader', loader: 'css-loader?sourceMap' })
},
{
test: /\.css$/,
include: helpers.root('src', 'app'),
loader: 'raw-loader'
}
]
},
-
awesome-typescript-loader
- 一个用于把TypeScript代码转译成ES5的加载器,它会由tsconfig.json
文件提供指导 -
angular2-template-loader
- 用于加载Angular组件的模板和样式 -
html-loader
- 为组件模板准备的加载器 -
images/fonts
- 图片和字体文件也能被打包。 -
CSS - 第一个模式匹配应用级样式,第二个模式匹配组件局部样式(就是在组件元数据的
styleUrls
属性中指定的那些)。
第一个模式是给全局样式使用的,它排除了/src/app
目录下的.css
文件,因为那里放着我们的组件局部样式。
它只包含了那些位于/src/app
及其上级目录的.css
文件,那里是应用级样式。
ExtractTextPlugin
(后面会讲到)使用style
和css
加载器来处理这些文件。
第二个模式过滤器是给组件局部样式的,并通过raw
加载器把它们加载成字符串 —— 那是Angular期望通过元数据的styleUrls
属性来指定样式的形式。
多重加载器也能使用数组形式串联起来。
plugins
最后,创建三个插件实例:
config/webpack.common.js
plugins: [
// Workaround for angular/angular#11580
new webpack.ContextReplacementPlugin(
// The (\\|\/) piece accounts for path separators in *nix and Windows
/angular(\\|\/)core(\\|\/)@angular/,
helpers.root('./src'), // location of your src
{} // a map of your routes
),
new webpack.optimize.CommonsChunkPlugin({
name: ['app', 'vendor', 'polyfills']
}),
new HtmlWebpackPlugin({
template: 'src/index.html'
})
]
CommonsChunkPlugin
app.js
包应该只包含应用代码。所有第三方代码都应该放进vendor.js
包中。
当然,应用代码中还是要imports
第三方代码。
Webpack还没有智能到自动把提供商代码排除在app.js
包之外的程度。
CommonsChunkPlugin
插件能完成此工作。
CommonsChunkPlugin
标记出了三个块之间的等级体系:app
-> vendor
-> polyfills
。
当Webpack发现app
与vendor
有共享依赖时,就把它们从app
中移除。
在vendor
和polyfills
之间有共享依赖时也同样如此(虽然它们没啥可共享的)。
HtmlWebpackPlugin
Webpack生成了一些js和css文件。
虽然我们可以手动把它们插入到index.html
中,但那样既枯燥又容易出错。
Webpack可以通过HtmlWebpackPlugin
自动为我们注入那些script
和link
标签。
环境相关的配置
Environment-specific configuration
webpack.common.js
配置做了大部分繁重的工作。
通过合并它们特有的配置,我们可以基于webpack.common
为目标环境创建独立的、环境相关的配置文件。
这些文件越小越简单越好。
开发环境配置
Development configuration
下面是开发环境的而配置文件webpack.dev.js
:
config/webpack.dev.js
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
module.exports = webpackMerge(commonConfig, {
devtool: 'cheap-module-eval-source-map',
output: {
path: helpers.root('dist'),
publicPath: '/',
filename: '[name].js',
chunkFilename: '[id].chunk.js'
},
plugins: [
new ExtractTextPlugin('[name].css')
],
devServer: {
historyApiFallback: true,
stats: 'minimal'
}
});
开发环境下的构建依赖于Webpack的开发服务器,我们在靠近文件底部的地方配置了它。
虽然我们告诉Webpack把输出包放到dist
目录,但实际上开发服务器把这些包都放在了内存里,而不会把它们写到硬盘中。
所以在dist
目录下是找不到任何文件的(至少现在这个开发环境下构建时没有)。
HtmlWebpackPlugin
(由webpack.common.js
引入)插件使用了publicPath
和filename
设置,
来向index.html
中插入适当的<script>和<link>标签。
默认情况下,我们这些CSS样式会被埋没在JavaScript包中。ExtractTextPlugin
会把它们提取成外部.css
文件,
这样HtmlWebpackPlugin
插件就会转而把一个<link>标签写进index.html
了。
要了解本文件中这些以及其它配置项的详情,请参阅Webpack文档。
抓取本指南底部的应用代码,并试一试:
npm start
产品环境配置
Production configuration
产品环境下的配置和开发环境下的配置很相似……除了一些关键的改动。
config/webpack.prod.js
var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
module.exports = webpackMerge(commonConfig, {
devtool: 'source-map',
output: {
path: helpers.root('dist'),
publicPath: '/',
filename: '[name].[hash].js',
chunkFilename: '[id].[hash].chunk.js'
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new webpack.optimize.UglifyJsPlugin({ // https://github.com/angular/angular/issues/10618
mangle: {
keep_fnames: true
}
}),
new ExtractTextPlugin('[name].[hash].css'),
new webpack.DefinePlugin({
'process.env': {
'ENV': JSON.stringify(ENV)
}
}),
new webpack.LoaderOptionsPlugin({
htmlLoader: {
minimize: false // workaround for ng2
}
})
]
});
我们希望把应用程序及其依赖都部署到一个真实的产品服务器中。 而不希望部署那些只在开发环境下才用得到的依赖。
把产品环境的输出包放在dist
目录下。
Webpack生成的文件名中带有“缓存无效哈希(cache-busting hash)”。
感谢HtmlWebpackPlugin
插件,当这些哈希值变化时,我们不用去更新index.html
了。
还有一些别的插件:
-
NoEmitOnErrorsPlugin
- 如果出错就停止构建。*
UglifyJsPlugin
- 最小化(minify)生成的包。
-
ExtractTextPlugin
- 把内嵌的css抽取成外部文件,并为其文件名添加“缓存无效哈希”。 -
DefinePlugin
- 用来定义环境变量,以便我们在自己的程序中引用它。 -
LoaderOptionsPlugins
- 为特定的加载器提供选项。
感谢DefinePlugin和顶部定义的ENV
变量,我们就可以像这样启用Angular的产品模式了:
if (process.env.ENV === 'production') {
enableProdMode();
}
抓取本指南底部的应用代码,并试一试:
npm run build
测试环境配置
Test configuration
我们并不需要使用很多配置项来运行单元测试。
也不需要在开发环境和产品环境下引入的那些加载器和插件。
如果有可能拖慢执行速度,甚至都不需要在单元测试中加载和处理应用全局样式文件,所以我们用一个null
加载器来处理所有CSS。
我们可以把测试环境的配置合并到webpack.common
配置中,并且改写不想要或不需要的部分。
但是从一个全新的配置开始可能更简单。
config/webpack.test.js
var webpack = require('webpack');
var helpers = require('./helpers');
module.exports = {
devtool: 'inline-source-map',
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
loaders: [
{
loader: 'awesome-typescript-loader',
options: { configFileName: helpers.root('src', 'tsconfig.json') }
} , 'angular2-template-loader'
]
},
{
test: /\.html$/,
loader: 'html-loader'
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'null-loader'
},
{
test: /\.css$/,
exclude: helpers.root('src', 'app'),
loader: 'null-loader'
},
{
test: /\.css$/,
include: helpers.root('src', 'app'),
loader: 'raw-loader'
}
]
},
plugins: [
new webpack.ContextReplacementPlugin(
// The (\\|\/) piece accounts for path separators in *nix and Windows
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
helpers.root('./src'), // location of your src
{} // a map of your routes
)
]
}
重新配置Karma,让它使用webpack来运行这些测试:
config/karma.conf.js
var webpackConfig = require('./webpack.test');
module.exports = function (config) {
var _config = {
basePath: '',
frameworks: ['jasmine'],
files: [
{pattern: './config/karma-test-shim.js', watched: false}
],
preprocessors: {
'./config/karma-test-shim.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
stats: 'errors-only'
},
webpackServer: {
noInfo: true
},
reporters: ['kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: false,
browsers: ['Chrome'],
singleRun: true
};
config.set(_config);
};
我们不用预编译TypeScript,Webpack随时在内存中转译我们的TypeScript文件,并且把产出的JS直接反馈给Karma。 硬盘上没有任何临时文件。
karma-test-shim
告诉Karma哪些文件需要预加载,首要的是:带有“测试版提供商”的Angular测试框架是每个应用都希望预加载的。
config/karma-test-shim.js
Error.stackTraceLimit = Infinity;
require('core-js/es6');
require('core-js/es7/reflect');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
var appContext = require.context('../src', true, /\.spec\.ts/);
appContext.keys().forEach(appContext);
var testing = require('@angular/core/testing');
var browser = require('@angular/platform-browser-dynamic/testing');
testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting());
注意,我们并没有明确加载这些应用代码。
只是告诉Webpack查找并加载我们的测试文件(文件名以.spec.ts
结尾)。
每个规约(spec)文件都导入了所有(也只有)它测试所需的应用源码。
Webpack只加载那些特定的应用文件,而忽略所有其它我们不会测试到的。
抓取本指南底部的应用代码,并试一试:
npm test
试一试
Trying it out
这里是一个小型应用的全部源码,我们可以用本章中学到的Webpack技术打包它们。
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Angular With Webpack</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>
import { Component } from '@angular/core';
import '../assets/css/styles.css';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent { }
app.component.html
显示了这个可下载的Angular Logo
。
在项目的
assets
目录下创建一个名叫images
的文件夹,然后右键点击(Mac上是Cmd+点击)本图片,并把它下载到images
文件夹中。
这里又是TypeScript的入口点文件,它定义了polyfills
和vendor
这两个包。
import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');
if (process.env.ENV === 'production') {
// Production
} else {
// Development and test
Error['stackTraceLimit'] = Infinity;
require('zone.js/dist/long-stack-trace-zone');
}
重点:
Highlights
-
在
index.html
中没有<script>或<link>标签。HtmlWebpackPlugin
会在运行时动态插入它们。 -
app.component.ts
中的AppComponent
类简单的用一个import
语句导入了应用级css。 -
AppComponent
组件本身有它自己的HTML模板和CSS文件。Webpack通过调用require()
方法加载它们。Webpack还把那些组件内部的文件打包进了app.js
中。 我们在自己的源码中看不到这些调用,这些工作是由幕后的angular2-template-loader
插件完成的。 -
vendor.ts
由import
提供商依赖的语句组成,它最终决定了vender.js
的内容。 本应用也导入这些模块,如果没有CommonsChunkPlugin
插件检测出这种重叠,并且把它们从app.js
中移除,它们就会同时出现在app.js
包中。
总结
Conclusion
我们学到了刚好够用来在开发、测试、产品环境下构建一个小型Angular应用的Webpack配置知识。
但我们还能做得更多。搜索互联网来获得专家的建议,并扩展你对Webpack的认识。