RxJS 管道运算符
从5.5版开始,我们已经发布了“可移植运算符”,可以在 进行访问 rxjs/operators
(请注意复数的“运算符”)中 。 与 找到的“ patch”运算符相比,这些方法是仅引入所需运算符的更好方法 rxjs-compat
打包中 。
注意 :使用 rxjs
或 rxjs/operators
不更改构建过程可能会导致捆绑包增大。 请参阅 已知问题” 下面的“ 部分。
重命名运营商
由于使运算符独立于 Observable 可用,因此运算符名称不能与 JavaScript 关键字限制冲突。 因此,某些运算符的可移植版本的名称已更改。 这些运算符是:
do
->tap
catch
->catchError
switch
->switchAll
finally
->finalize
该 let
运算符现在是 一部分, Observable
as 的 pipe
无法导入。
source$.let(myOperator) -> source$.pipe(myOperator)
请参阅下面的“ 建立自己的运营商 ”。
前 toPromise()
“操作员”已被删除 因为运算符返回 Observable
, 不是一个 Promise
。 现在有一个 Observable.toPromise()
实例方法。
为什么?
修补的点链运算符的问题是:
- 导入修补程序运算符的任何库 增加
Observable.prototype
都将为该库的所有使用者 ,从而造成盲目依赖。 如果库删除了它们的用法,它们将在不知不觉中破坏其他所有人。 使用 pipeable,必须将所需的运算符导入使用它们的每个文件中。
- 直接 到原型的操作员无法 “摇晃树”汇总 通过汇总或 Webpack 之类的工具 。 可管道运算符将像直接从模块中引入的函数一样。
应用程序中导入的未使用的运算符无法通过任何类型的构建工具或 lint 规则可靠地检测到。 这意味着您可以导入 scan
,但停止使用它,并且仍将其添加到您的输出包中。 对于可管道运算符,如果您不使用它,则可以使用皮棉规则为您选择它。
功能组成很棒。 建立您自己的自定义运算符变得容易得多,现在它们可以正常工作,并且看起来就像 rxjs 中的所有其他运算符一样。 您不需要扩展 Observable 或覆盖它 lift
。
什么?
什么是管道运算符? 简而言之,可以与当前 一起使用的函数 let
运算符 。 它曾经是名称(“ lettable”)的起源,但是这令人困惑,因此我们现在将它们称为“ pipeable”,因为它们打算与 一起使用 pipe
实用程序 。 可管道运算符基本上是任何返回带有签名的函数的函数: <T, R>(source: Observable<T>) => Observable<R>
。
有一个 的 pipe
内置 方法 Observable
现在, Observable.prototype.pipe
可以用来像与点链接所用的相似方式组成运算符(如下所示)。
还 pipe
可以从导入实用程序功能 import { pipe } from 'rxjs';
。 该 pipe
功能可用于从其他管道运算符构建可重用的管道运算符。 例如:
import { pipe } from 'rxjs';
import { map } from 'rxjs/operators';
const mapTwice = <T,R>(fn: (value: T, index: number) => R) => pipe(map(fn), map(fn));
用法
您可以在 下从一个位置引入所需的任何运算符 'rxjs/operators'
( 复数! ) 。 还建议直接使用所需的 Observable 创建方法,如下所示 range
:
import { range } from 'rxjs';
import { map, filter, scan } from 'rxjs/operators';
const source$ = range(0, 10);
source$.pipe(
filter(x => x % 2 === 0),
map(x => x + x),
scan((acc, x) => acc + x, 0)
)
.subscribe(x => console.log(x))
// Logs:
// 0
// 4
// 12
// 24
// 40
轻松建立自己的运营商
实际上,您 始终 来 可以使用 做到这一点, let
... 但是构建自己的运算符现在就像编写函数一样简单。 注意,您可以将自定义运算符与其他 rxjs 运算符无缝组合在一起。
import { Observable, interval } from 'rxjs';
import { filter, map, take, toArray } from 'rxjs/operators';
/**
* an operator that takes every Nth value
*/
const takeEveryNth = (n: number) => <T>(source: Observable<T>) =>
new Observable<T>(observer => {
let count = 0;
return source.subscribe({
next(x) {
if (count++ % n === 0) observer.next(x);
},
error(err) { observer.error(err); },
complete() { observer.complete(); }
})
});
/**
* You can also use an existing operator like so
*/
const takeEveryNthSimple = (n: number) => <T>(source: Observable<T>) =>
source.pipe(filter((value, index) => index % n === 0 ))
/**
* And since pipeable operators return functions, you can further simplify like so
*/
const takeEveryNthSimplest = (n: number) => filter((value, index) => index % n === 0);
interval(1000).pipe(
takeEveryNth(2),
map(x => x + x),
takeEveryNthSimple(3),
map(x => x * x),
takeEveryNthSimplest(4),
take(3),
toArray()
)
.subscribe(x => console.log(x));
// Logs:
// [0, 2304, 9216]
已知的问题
打字稿<2.4
在 TypeScript 2.3及更低版本中,由于无法在 TypeScript 2.4之前推断类型,因此需要将类型添加到传递给运算符的函数中。 在 TypeScript 2.4中,类型将通过合成正确推断。
TS 2.3 and under
range(0, 10).pipe(
map((n: number) => n + '!'),
map((s: string) => 'Hello, ' + s),
).subscribe(x => console.log(x))
TS 2.4 以上
range(0, 10).pipe(
map(n => n + '!'),
map(s => 'Hello, ' + s),
).subscribe(x => console.log(x))
建造和摇树
从清单文件(或重新导出)导入时,应用程序捆绑有时会增长。 现在可以从中导入可管道运算符 rxjs/operators
,但是在不更改生成过程的情况下这样做通常会导致应用程序包更大。 这是因为默认情况下 rxjs/operators
将解析为 rxjs 的 CommonJS 输出。
为了使用新的管道运算符而不增加捆绑包的大小,您将需要更改 Webpack 配置。 这仅适用于 Webpack 3+,因为它依赖于 的新 ModuleConcatenationPlugin
Webpack 3 中 功能。
路径映射
与 rxjs 5.5 一起发布的是带有 ES5 和 ES2015 语言级别的 ECMAScript 模块格式(导入和导出)的 rxjs 构建。 您可以在 找到这些分布 node_modules/rxjs/_esm5
和中 node_modules/rxjs/_esm2015
(“ esm”代表 ECMAScript 模块,数字“ 5”或“ 2015”代表 ES 语言级别)。 在应用程序源代码中,应从导入 rxjs/operators
,但在 Webpack 配置文件中,将需要将导入重新映射为 ESM5(或 ESM2015)版本。
如果是 require('rxjs/_esm5/path-mapping')
,您将收到一个函数,该函数返回键-值对的对象,将每个输入映射到其在磁盘上的文件位置。 使用此映射,如下所示:
webpack.config.js
配置简单:
const rxPaths = require('rxjs/_esm5/path-mapping');
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: 'index.js',
output: 'bundle.js',
resolve: {
// Use the "alias" key to resolve to an ESM distribution
alias: rxPaths()
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
更完整的配置(更接近实际场景):
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const DashboardPlugin = require('webpack-dashboard/plugin');
const nodeEnv = process.env.NODE_ENV || 'development';
const isProd = nodeEnv === 'production';
const rxPaths = require('rxjs/_esm5/path-mapping');
var config = {
devtool: isProd ? 'hidden-source-map' : 'cheap-eval-source-map',
context: path.resolve('./src'),
entry: {
app: './index.ts',
vendor: './vendor.ts'
},
output: {
path: path.resolve('./dist'),
filename: '[name].bundle.js',
sourceMapFilename: '[name].map',
devtoolModuleFilenameTemplate: function (info) {
return "file:///" + info.absoluteResourcePath;
}
},
module: {
rules: [
{ enforce: 'pre', test: /\.ts$|\.tsx$/, exclude: ["node_modules"], loader: 'ts-loader' },
{ test: /\.html$/, loader: "html" },
{ test: /\.css$/, loaders: ['style', 'css'] }
]
},
resolve: {
extensions: [".ts", ".js"],
modules: [path.resolve('./src'), 'node_modules'],
alias: rxPaths()
},
plugins: [
new webpack.DefinePlugin({
'process.env': { // eslint-disable-line quote-props
NODE_ENV: JSON.stringify(nodeEnv)
}
}),
new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(),
new HtmlWebpackPlugin({
title: 'Typescript Webpack Starter',
template: '!!ejs-loader!src/index.html'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity,
filename: 'vendor.bundle.js'
}),
new webpack.optimize.UglifyJsPlugin({
mangle: false,
compress: { warnings: false, pure_getters: true, passes: 3, screw_ie8: true, sequences: false },
output: { comments: false, beautify: true },
sourceMap: false
}),
new DashboardPlugin(),
new webpack.LoaderOptionsPlugin({
options: {
tslint: {
emitErrors: true,
failOnHint: true
}
}
})
]
};
module.exports = config;