65.9K
CodeProject 正在变化。 阅读更多。
Home

关于 Webpack @Angular 和杂项的说明

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2017年12月10日

CPOL

8分钟阅读

viewsIcon

9771

downloadIcon

164

这是一篇关于 Webpack、@Angular 及其他主题的笔记。

引言

这是一篇关于 Webpack、@Angular 及其他主题的笔记。在这篇笔记中,我将给出两个启动 Angular 应用程序的示例。一个使用 "systemjs",另一个使用 webpack

背景

Angular CLI 可以打包一个 Angular SPA 以便部署,从而解决了长期以来备受诟病的极其庞大的 "node_modules" 目录。尽管 "ng build" 是一个简单的命令,但仍然值得我们自己手动设置 webpack 来打包 Angular 应用程序。

  • 我们将了解 Angular 应用程序中需要打包的内容
  • 我们将了解如何使用 webpack 打包 Angular 应用程序,并从中获得一些关于 Angular CLI 如何工作的见解

我将使用 Node 作为本文档示例中的 Web 服务器。

哪个 package.json & 哪个 TSC 编译器

在查看示例之前,我将讨论一个使用 NPM/Node 时简单但经常被忽视的主题。

  • 运行 "npm install" 时如何指定 "package.json" 的位置
  • 如果我们同时安装了全局和项目级别的 "devDependencies" 来用于 "typescript",我们如何确定哪个被用来编译 typescript 代码?

在 zip 文件 "which-p-which-c" 中附加了一个简单的 "package.json" 文件。

{
  "name": "which-p-which-c-client",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "tsc": "tsc"
  },
  "devDependencies": {
    "typescript": "~2.4.2"
  }
}

此 "package.json" 文件位于 "which-p-which-c" -> "client" 目录下。

哪个是 package.json

如果我们从 "which-p-which-c" 目录运行 "NPM",我们可以发出以下命令

npm -prefix client install

"-prefix client" 参数告诉 NPM 在 "client" 目录中查找 "package.json" 文件,并将 "node_modules" 目录放在 "client" 目录中。

哪个是 tsc 编译器

为了检查使用哪个 "tsc",我们可以先全局安装 "tsc"。

npm install typescript@2.6.2 -g

然后发出命令检查 "tsc" 版本。

tsc -v

版本是 "Version 2.6.2",这告诉我们正在使用全局安装。如果我们想使用下载到 "node_modules" 目录作为 "devDependencies" 的 "typescript~2.4.2",我们可以发出以下命令

npm -prefix client run tsc -- -v
  • "-prefix client" 告诉 NPM 在 "client" 目录中查找 "package.json" 文件并运行 "tsc" 脚本。
  • "--" 允许我们向 "tsc" 指定进一步的参数。在这种情况下,我们只希望 "-v" 打印出版本。

此时 "tsc" 的版本是 "Version 2.4.2",这证实了使用了项目级别的安装。

"good/old" "systemjs"

在 Angular 的最新版本中,管理 Angular SPA 的首选方式是 Angular CLI。但根据我对附加 zip 文件 "the-good-old-systemjs" 的经验,使用 "systemjs" 启动 Angular 应用程序的方式仍然适用于 Angular 5。创建使用 "systemjs" 的示例的原因是,当我们使用 webpack 打包 Angular 应用程序时,可以更好地理解我们需要打包哪些内容。

附加的是一个简单的 Node 应用程序。在 "app.js" 中,我们告诉 Node 从 "client" 目录提供静态内容。

{
  "name": "the-good-old-systemjs",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "4.16.2",
    "errorhandler": "1.5.0"
  }
var express = require('express'),
    http = require('http'),
    path = require('path'),
    errorhandler = require('errorhandler');
    
var app = express();
    
app.set('port', 3000);
    
app.use(function (req, res, next) {
    res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
    res.header('Expires', '-1');
    res.header('Pragma', 'no-cache');
    next();
});
    
app.use(express.static(path.join(__dirname, 'client')));
    
app.use(errorhandler());
    
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

"client" 目录是我们用来找出 Angular 应用程序运行的关键部分的 Angular 应用程序。

此示例应用程序有 2 个 Angular 组件

  • "app.component" 是将在 "app.module" 中 "引导" 的顶级组件。它使用 "sub.component" 来显示一些文本消息。
  • "sub.component" 使用 "text.service" 获取一些文本并将其绑定到 UI。

由于这是一个标准的 Angular 应用程序,我只在本笔记中列出重要内容。以下是 "sub.component.ts"。

import { Component, OnInit, Inject } from '@angular/core';
import { TextService } from '../../services/text.service';
    
@Component({
    moduleId: module.id,
    providers: [TextService],
    selector: 'sub-component',
    templateUrl: './sub.component.html',
    styleUrls: ['./sub.component.css']
})
export class SubComponent implements OnInit {
    public Text: string = 'This is OK';
        
    constructor(@Inject(TextService) private textService: TextService) { }
        
    ngOnInit(): void {
        this.Text = this.textService.getText();
    }
}

以下是 "sub.component.ts" 使用的 "text.service.ts" 来获取要显示的文本。

import { Injectable } from '@angular/core';
    
@Injectable()
export class TextService {
    constructor() { }

    getText() {
        return 'Hello from NG 5 ...';
    }
}

此 Angular 应用程序从 "main.ts" 启动,并通过 "systemjs.config.js" "引导" 到 "index.html"。

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
    
enableProdMode();
let platform = platformBrowserDynamic();
    
platform.bootstrapModule(AppModule)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular-example</title>
    
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
    
<script src="/systemjs.config.js"></script>
    
<script>
    System.import('app').catch(function(err){ console.error(err); });
</script>
    
</head>
<body>
    <app></app>
</body>
</html
(function (global) {
  System.config({
    paths: { 'npm:': '/node_modules/' },
    map: {
      'app': '/app/',
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser':
             'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic':
             'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
      'rxjs': 'npm:rxjs'
    },
    packages: {
      app: { main: './main.js', defaultExtension: 'js' },
      rxjs: { defaultExtension: 'js' }
    }
  });
})(this);

要运行应用程序,您需要发出以下命令来为服务器和客户端目录安装 "node_modules" 目录。

npm install
npm -prefix client install

您还需要使用以下命令编译 "typescript" 文件

npm -prefix client run tsc

要启动 Node 服务器,您可以发出以下命令

node app.js

如果您查看显示此简单页面的网络流量,Angular 实际上发起了 56 个对服务器的请求。

Webpack 和 @Angular

通过浏览加载 Angular 应用程序的 "systemjs" 示例,我们可以发现除了 "index.html" 之外的 56 个文件属于 3 类。

  • Polyfills - 在我的示例中,我只添加了 "zone.js"。如果您想支持更多浏览器,您可能需要添加更多 polyfill 文件。
  • 来自 "node_modules" 的 JavaScript 文件,应用程序代码使用它们。
  • 我们自己创建的应用程序文件。它们是我们创建的 JavaScript、HTML 和 CSS 文件,"main.ts" 文件直接或间接依赖它们。

附加的 zip 文件 "the-webpack-ng" 与上面完全相同的 Angular 应用程序,但我们将使用 webpack 来打包和启动它。

考虑到这 3 类文件,我们已准备好使用 webpack 将它们打包。要使用 webpack,我们需要在 "package.json" 中除了 "typescript" 和 "@types/node" 之外,再添加一些 "devDependencies"。

{
  "name": "the-good-old-systemjs-client",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "tsc": "tsc",
    "webpack": "webpack --config ./webpack.config.js"
  },
  "dependencies": {
    "@angular/animations": "^5.0.0",
    "@angular/common": "^5.0.0",
    "@angular/compiler": "^5.0.0",
    "@angular/core": "^5.0.0",
    "@angular/forms": "^5.0.0",
    "@angular/http": "^5.0.0",
    "@angular/platform-browser": "^5.0.0",
    "@angular/platform-browser-dynamic": "^5.0.0",
    "@angular/router": "^5.0.0",
    "core-js": "^2.4.1",
    "rxjs": "^5.5.2",
    "zone.js": "^0.8.14",
    "systemjs": "0.20.19"
  },
  "devDependencies": {
    "typescript": "~2.4.2",
    "@types/node": "~6.0.60",
    
    "webpack": "2.2.1",
    "html-webpack-plugin": "^2.16.1",
    "awesome-typescript-loader": "^3.0.4",
    "raw-loader": "^0.5.1",
    "file-loader": "^0.9.0",
    "html-loader": "^0.4.3",
    "css-loader": "^0.26.1",
    "style-loader": "^0.13.1",
    "extract-text-webpack-plugin": "2.0.0-beta.5",
    "angular2-template-loader": "^0.6.0"
  }
}

"polyfills.ts" 指定了 polyfill 是什么。在此示例中,我只添加了 "zone.js"。

import 'zone.js/dist/zone';

"vendor.ts" 指定了从 "node_modules" 目录运行 Angular 应用程序所需文件的入口点。

// Angular
import '@angular/core';
import '@angular/common';
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/http';
import '@angular/router';
import '@angular/forms';
      
// RxJS
import 'rxjs';

"webpack.config.js" 文件告诉 webpack 我们希望它如何打包应用程序。

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var path = require('path');
    
module.exports = {
  entry: {
    'polyfills': path.join(__dirname, './app/webpack/polyfills.ts'),
    'vendor': path.join(__dirname, './app/webpack/vendor.ts'),
    'app': path.join(__dirname, './app/main.ts')
  },
  output: {
    path: path.join(__dirname, './dist'),
          filename: '[name].bundle.js'
  },
  resolve: {
    extensions: ['.ts', '.js']
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loaders: [
          {
            loader: 'awesome-typescript-loader',
            options: { configFileName: path.join(__dirname, './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: path.join(__dirname, './app'),
        loader: ExtractTextPlugin.extract({ fallbackLoader: 'style-loader',
            loader: 'css-loader?sourceMap' })
      }
      ,{
        test: /\.css$/,
        include: path.join(__dirname, './app'),
        loader: 'raw-loader'
      }
    ]
  },
    
  plugins: [
    // Workaround for angular/angular#11580
    new webpack.ContextReplacementPlugin(
      /angular(\\|\/)core(\\|\/)@angular/,
      path.join(__dirname, './app'),
      {}
    ),
    
    new webpack.ContextReplacementPlugin(
        /(.+)?angular(\\|\/)core(.+)?/,
        path.join(__dirname, './app'),
        {}
    ),

    new webpack.optimize.CommonsChunkPlugin({
      name: ['app', 'vendor', 'polyfills']
    }),
    
    // This will minimize the bundle size
    // But it take longer time to finish. It is better not to add
    // this plugin in the development environment
    new webpack.optimize.UglifyJsPlugin()
  ]
};

"webpack.config.js" 是一个复杂的配置文件,但能帮助我们入门的重要内容如下

  • "entry" 属性告诉 webpack 要打包什么。在此示例中,我们希望 webpack 打包 polyfills、应用程序所需的 "node_modules" 文件,以及 "main.ts" 文件使用的所有应用程序文件;
  • "output" 属性告诉 webpack 将 bundles 放在哪里以及 bundle 文件的名称;
  • "CommonsChunkPlugin" 告诉 webpack 从 vendor bundle 中删除已打包在 "polyfill" bundle 中的任何文件,并从 app bundle 中删除已打包在 vendor bundle 中的任何文件,尽管我们知道 polyfills 和 vendor bundle 实际上不共享任何文件。
  • "UglifyJsPlugin" 告诉 webpack 最小化 bundles 的大小。

"index.html" 文件使用 bundles 来启动 Angular 应用程序。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular-example</title>
    
<script src="/dist/polyfills.bundle.js"></script>
<script src="/dist/vendor.bundle.js"></script>
    
</head>
<body>
    <app></app>
</body>
    
<script src="/dist/app.bundle.js"></script>
</html>

重要的是要知道,我们需要从 Angular 组件定义中删除 "moduleId: module.id"。webpack 和 "systemjs" 启动 Angular 应用程序的方式之间存在细微差别。

要运行应用程序,您需要发出以下命令来为服务器和客户端目录安装 "node_modules" 目录。

npm install
npm -prefix client install

您还需要使用以下命令创建 bundle 文件

npm -prefix client run webpack

要启动 Node 服务器,您可以发出以下命令。

node app.js

当您将应用程序加载到浏览器中时,您会发现下载的文件数量从 56 个减少到 4 个,并且应用程序启动速度大大加快。值得注意的是,我们不必将 vendors bundle 与 app bundle 分开。如果我们无意在不同的 SPA 之间共享 vendors bundle,我们可以简单地创建一个单一的 app bundle 来提供 Angular 应用程序。

"moduleId: module.id"

Webpack 减小了包的大小。但如果我们要让组件在 webpack 中正常工作,我们需要从 "@Component" 装饰器中删除 "moduleId: module.id"。如果我们希望组件在 webpack 包和 "systemJs" 环境中都能正常工作,我们可以使用 " string-replace-loader" (功劳 - Stackoverflow)。

"string-replace-loader": "1.3.0"

我们可以将 "string-replace-loader" 添加到 package.json 文件中作为 "devDependencies" 之一。在 webpack 配置文件中,我们可以在 "module" 部分使用它来从包中删除 "moduleId: module.id"。

module: {
    rules: [
        { test: /\.ts$/,
              loader: 'string-replace-loader',
              include: path.join(__dirname, './app'),
              query: { search: 'moduleId: module.id,', replace: '' } 
        },
      {
        test: /\.ts$/,
        loaders: [
          {
            loader: 'awesome-typescript-loader',
            options: { configFileName: path.join(__dirname, './tsconfig.json') }
          } , 'angular2-template-loader'
        ]
      },
      Other rules ...
    ]
  }

此解决方案可能不是最好的,但至少它允许我们在组件中保留 "moduleId: module.id"。

将图片打包到 CSS 中

有时,您的组件级 CSS 文件中可能包含图片引用。例如,如果 "sub.component.css" 引用了 "blue.jpg" 文件,我们将需要将图片打包到包中。

.greeting {
    background-image: url("./images/blue.jpg");
    font-family: verdana;
    font-size: 24px;
}

要打包图片文件 "blue.jpg",我们可以使用 "url-loader"。以下是 "webpack.config.js" 文件。

var webpack = require('webpack');
var path = require('path');

module.exports = {
  entry: {
    'app': path.join(__dirname, './app/main.ts')
  },
  output: {
      path: path.join(__dirname, './dist/app'),
      filename: '[name].bundle.js'
  },
  resolve: { extensions: ['.ts', '.js'] },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loaders: [
          {
            loader: 'awesome-typescript-loader',
            options: { configFileName: path.join(__dirname, './tsconfig.json') }
          } , 'angular2-template-loader'
        ]
      },
      {
        test: /\.html$/,
        loader: 'html-loader'
      },
      {
          test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
          loader: 'url-loader'
      },
      {
          test: /\.css$/,
          include: path.join(__dirname, './app'),
          use: [ 'to-string-loader', 'css-loader' ]
        }
    ]
  },
  plugins: [
    // Workaround for angular/angular#11580
    new webpack.ContextReplacementPlugin(
      /angular(\\|\/)core(\\|\/)@angular/,
      path.join(__dirname, './app'),
      {}
    ),
    
    new webpack.ContextReplacementPlugin(
        /(.+)?angular(\\|\/)core(.+)?/,
        path.join(__dirname, './app'),
        {}
    ),
    
    new webpack.optimize.UglifyJsPlugin()
  ]
};

我们需要在 "package.json" 中包含以下 "devDependencies"。

"devDependencies": {
    "typescript": "~2.4.2",
    "@types/node": "~6.0.60",
    "webpack": "3.10.0",
    "awesome-typescript-loader": "^3.0.4",
    "angular2-template-loader": "^0.6.0",
    "file-loader": "^0.9.0",
    "html-loader": "^0.4.3",
    "css-loader": "^0.26.1",
    "to-string-loader": "1.1.5",
    "url-loader": "0.6.2"
  }

在 webpack 成功运行后,我们可以将打包好的 JavaScript 文件加载到 "index.html" 中。我们可以看到背景图片已成功应用于 Angular 组件。

如果您现在在浏览器中检查 Angular 组件,您会看到图片文件被 Base64 编码到 CSS 中并打包到 bundle 中。

Webpack 性能

Webpack 要完成将所有必需文件打包到 bundle 中的工作量很大,因此需要一些时间才能完成。但如果您使用监视器启动它,它会在第一次运行后针对速度进行优化。

webpack --config ./webpack.config.js --watch

如果您仍然觉得速度不够快,可以从配置文件中删除该行,并将最小化任务交给发布构建。

new webpack.optimize.UglifyJsPlugin()

Eclipse IDE & "node_modules"

如果您想将项目加载到 Eclipse 中,您可能需要将 "node_modules" 和 "dest" 目录从资源过滤器中排除。您可以右键单击项目 -> Properties -> Resource -> Resource Filters 来添加过滤器。 "node_modules" 目录可能会变得非常大,以至于 Eclipse 无法处理,而且我们很可能永远不需要查看这些目录。

关注点

  • 这是一篇关于 Webpack、@Angular 及其他主题的笔记;
  • 尽管 Angular CLI 是管理 Angular 应用程序的首选方式,但如果我们能够自己设置 webpack 仍然很好,这样我们就能确切地知道 Angular CLI 如何构建应用程序。
  • 希望您喜欢我的博文,并希望这篇笔记能以某种方式帮助您。

历史

  • 2017/12/6:首次修订
© . All rights reserved.