关于 Webpack @Angular 和杂项的说明
这是一篇关于 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:首次修订