Webpack 简介

Webpack是一个广受欢迎的模块打包器, 这个工具用来把程序源码打包到一些方便易用的中,以便把这些代码从服务器加载到浏览器中。

它是我们在文档中到处使用的SystemJS的一个优秀替代品。这篇指南会带我们尝尝Webpack的滋味,并解释如何在Angular程序中使用它。

目录

Contents

你还可以点这里下载最终结果

什么是Webpack?

Webpack是一个强力的模块打包器。 所谓包(bundle)就是一个JavaScript文件,它把一堆资源(assets)合并在一起,以便它们可以在同一个文件请求中发回给客户端。 包中可以包含JavaScript、CSS样式、HTML以及很多其它类型的文件。

Webpack会遍历你应用中的所有源码,查找import语句,构建出依赖图谱,并产出一个(或多个)。 通过插件和规则,Webpack可以对各种非JavaScript文件进行预处理和最小化(Minify),比如TypeScript、SASS和LESS文件等。

我们通过一个JavaScript配置文件webpack.config.js来决定Webpack做什么以及如何做。

入口与输出

我们给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>标签来加载它。

多重包

我们可能不会希望把所有东西打进一个巨型包,而更喜欢把多变的应用代码从相对稳定的第三方提供商模块中分离出来。

所以要修改配置,以获得两个入口点:main.tsvendor.ts

entry: {
  app: 'src/app.ts',
  vendor: 'src/vendor.ts'
},

output: {
  filename: '[name].js'
}

Webpack会构造出两个独立的依赖图谱,并产出两个包文件:一个叫做app.js,它只包含我们的应用代码;另一个叫做vendor.js,它包含所有的提供商依赖。

在输出文件名中出现的[name]是一个Webpack的占位符,它将被一个Webpack插件替换为入口点的名字,分别是appvendor。插件在本章的稍后部分讲解。

要想告诉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)

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的@importurl(...)语句), 然后应用了style加载器(用来把css追加到页面上的<style>元素中)。

插件

Webpack有一条构建流水线,它被划分成多个经过精心定义的阶段(phase)。 我们可以把插件(比如uglify代码最小化插件)挂到流水线上:

plugins: [
  new webpack.optimize.UglifyJsPlugin()
]

配置Webpack

经过简短的培训之后,我们准备为Angular应用构建一份自己的Webpack配置了。

从设置开发环境开始。

创建一个新的项目文件夹。

mkdir angular-webpack
cd    angular-webpack

把下列文件添加到根目录下:

  1. {
  2. "name": "angular2-webpack",
  3. "version": "1.0.0",
  4. "description": "A webpack starter for Angular",
  5. "scripts": {
  6. "start": "webpack-dev-server --inline --progress --port 8080",
  7. "test": "karma start",
  8. "build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
  9. },
  10. "license": "MIT",
  11. "dependencies": {
  12. "@angular/common": "~4.0.0",
  13. "@angular/compiler": "~4.0.0",
  14. "@angular/core": "~4.0.0",
  15. "@angular/forms": "~4.0.0",
  16. "@angular/http": "~4.0.0",
  17. "@angular/platform-browser": "~4.0.0",
  18. "@angular/platform-browser-dynamic": "~4.0.0",
  19. "@angular/router": "~4.0.0",
  20. "core-js": "^2.4.1",
  21. "rxjs": "5.0.1",
  22. "zone.js": "^0.8.4"
  23. },
  24. "devDependencies": {
  25. "@types/node": "^6.0.45",
  26. "@types/jasmine": "2.5.36",
  27. "angular2-template-loader": "^0.6.0",
  28. "awesome-typescript-loader": "^3.0.4",
  29. "css-loader": "^0.26.1",
  30. "extract-text-webpack-plugin": "2.0.0-beta.5",
  31. "file-loader": "^0.9.0",
  32. "html-loader": "^0.4.3",
  33. "html-webpack-plugin": "^2.16.1",
  34. "jasmine-core": "^2.4.1",
  35. "karma": "^1.2.0",
  36. "karma-chrome-launcher": "^2.0.0",
  37. "karma-jasmine": "^1.0.2",
  38. "karma-sourcemap-loader": "^0.3.7",
  39. "karma-webpack": "^2.0.1",
  40. "null-loader": "^0.1.1",
  41. "raw-loader": "^0.5.1",
  42. "rimraf": "^2.5.2",
  43. "style-loader": "^0.13.1",
  44. "typescript": "~2.0.10",
  45. "webpack": "2.2.1",
  46. "webpack-dev-server": "2.4.1",
  47. "webpack-merge": "^3.0.0"
  48. }
  49. }

这些文件很多都很眼熟,它们在其他文档里已经出现过,特别是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');
}
Loading polyfills

polyfills.ts文件里,zone.js库须尽早引入,紧跟在ES6 shims和metadata shims之后。

由于这个包最先加载,所以polyfills.ts非常适合用来配置浏览器环境,如生产环境配置或是开发环境。

通用配置

开发、生产、测试等不同的环境通常会分开配置,但实际上这些配置也有很多地方是通用的。

我们可以把这些通用的配置收归到一个文件,命名为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解读

Webpack是基于NodeJS的一个工具,它能够从一个commonjs规范的JavaScript模块文件里读取配置。

这个配置文件是通过require语句导入依赖,然后将多个对象作为module.exports对象的属性导出。

entry

如上所述,第一个导出的对象是entries

config/webpack.common.js

  entry: {
    'polyfills': './src/polyfills.ts',
    'vendor': './src/vendor.ts',
    'app': './src/main.ts'
  },

entry对象定义了三个包:

resolve 无扩展名的文件导入

如果你的应用程序只须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'
    }
  ]
},

第一个模式是给全局样式使用的,它排除了/src/app目录下的.css文件,因为那里放着我们的组件局部样式。 它只包含了那些位于/src/app及其上级目录的.css文件,那里是应用级样式。 ExtractTextPlugin(后面会讲到)使用stylecss加载器来处理这些文件。

第二个模式过滤器是给组件局部样式的,并通过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发现appvendor有共享依赖时,就把它们从app中移除。 在vendorpolyfills之间有共享依赖时也同样如此(虽然它们没啥可共享的)。

HtmlWebpackPlugin

Webpack生成了一些js和css文件。 虽然我们可以手动把它们插入到index.html中,但那样既枯燥又容易出错。 Webpack可以通过HtmlWebpackPlugin自动为我们注入那些scriptlink标签。

环境相关的配置

webpack.common.js配置做了大部分繁重的工作。 通过合并它们特有的配置,我们可以基于webpack.common为目标环境创建独立的、环境相关的配置文件。

这些文件越小越简单越好。

开发环境配置

下面是开发环境的而配置文件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引入)插件使用了publicPathfilename设置, 来向index.html中插入适当的<script>和<link>标签。

默认情况下,我们这些CSS样式会被埋没在JavaScript包中。ExtractTextPlugin会把它们提取成外部.css文件, 这样HtmlWebpackPlugin插件就会转而把一个<link>标签写进index.html了。

要了解本文件中这些以及其它配置项的详情,请参阅Webpack文档

抓取本指南底部的应用代码,并试一试:

npm start

产品环境配置

产品环境下的配置和开发环境下的配置很相似……除了一些关键的改动。

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了。

还有一些别的插件:

UglifyJsPlugin - 最小化(minify)生成的包。

感谢DefinePlugin和顶部定义的ENV变量,我们就可以像这样启用Angular的产品模式了:

if (process.env.ENV === 'production') {
  enableProdMode();
}

抓取本指南底部的应用代码,并试一试:

npm run build

测试环境配置

我们并不需要使用很多配置项来运行单元测试。 也不需要在开发环境和产品环境下引入的那些加载器和插件。 如果有可能拖慢执行速度,甚至都不需要在单元测试中加载和处理应用全局样式文件,所以我们用一个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

试一试

这里是一个小型应用的全部源码,我们可以用本章中学到的Webpack技术打包它们。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <base href="/">
  5. <title>Angular With Webpack</title>
  6. <meta charset="UTF-8">
  7. <meta name="viewport" content="width=device-width, initial-scale=1">
  8. </head>
  9. <body>
  10. <my-app>Loading...</my-app>
  11. </body>
  12. </html>
  1. import { Component } from '@angular/core';
  2. import '../assets/css/styles.css';
  3. @Component({
  4. selector: 'my-app',
  5. templateUrl: './app.component.html',
  6. styleUrls: ['./app.component.css']
  7. })
  8. export class AppComponent { }

app.component.html显示了这个可下载的Angular Logo 。 在项目的assets目录下创建一个名叫images的文件夹,然后右键点击(Mac上是Cmd+点击)本图片,并把它下载到images文件夹中。

这里又是TypeScript的入口点文件,它定义了polyfillsvendor这两个包。

  1. import 'core-js/es6';
  2. import 'core-js/es7/reflect';
  3. require('zone.js/dist/zone');
  4. if (process.env.ENV === 'production') {
  5. // Production
  6. } else {
  7. // Development and test
  8. Error['stackTraceLimit'] = Infinity;
  9. require('zone.js/dist/long-stack-trace-zone');
  10. }

重点:

总结

我们学到了刚好够用来在开发、测试、产品环境下构建一个小型Angular应用的Webpack配置知识。

但我们还能做得更多。搜索互联网来获得专家的建议,并扩展你对Webpack的认识。

回到顶部