[译] Pipeable 操做符

原文连接: github.com/ReactiveX/r…css

本文为 RxJS 中文社区 翻译文章,如需转载,请注明出处,谢谢合做!html

若是你也想和咱们一块儿,翻译更多优质的 RxJS 文章以奉献给你们,请点击【这里】node

写在前面的话: (非正文) 去年9月,RxJS5.5beta版本发布了 lettable 操做符这个新特性,而就在几天前由于没法忍受此名称,又将 lettable 更为为 pipeable,详细请见 PR#3224webpack

前段时间忙于翻译 PWA 一书,所以 RxJS 的专栏搁置了一小段时间,周末抽了些时间出来将官方文档 Pipeable Operators 翻译出来同步至中文文档。此特性仍是比较重要,能够说是 RxJS 将来的走向,下面请看正文。git

从5.5版本开始咱们提供了 “pipeable 操做符”,它们能够经过 rxjs/operators 来访问 (注意 "operators" 是复数)。相比较于经过在 rxjs/add/operator/* 中以“打补丁”的方式来获取须要用到的操做符,这是一种更好的方式。github

注意: 若是使用 rxjs/operators 而不修改构建过程的话会致使更大的包。详见下面的已知问题一节。web

重命名的操做符数组

因为操做符要从 Observable 中独立出来,因此操做符的名称不能和 JavaScript 的关键字冲突。所以一些操做符的 pipeable 版本的名称作出了修改。这些操做符是:bash

  1. do -> tap
  2. catch -> catchError
  3. switch -> switchAll
  4. finally -> finalize

pipeObservable 的一部分,不须要导入,而且它能够替代现有的 let 操做符。app

source$.let(myOperator) -> source$.pipe(myOperator)

参见下面的“构建本身的操做符”。

以前的 toPromise() “操做符”已经被移除了,由于一个操做符应该返回 Observable,而不是 Promise 。如今使用 Observable.toPromise() 的实例方法来替代。

由于 throw 是关键字,你能够在导入时使用 _throw,就像这样: import { _throw } from 'rxjs/observable/throw'

若是前缀_使你困扰的话 (由于通常前缀_表示“内部的 - 不要使用”) ,你也能够这样作:

import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
...
const e = ErrorObservable.create(new Error('My bad'));
const e2 = new ErrorObservable(new Error('My bad too'));
复制代码

为何须要 pipeable 操做符?

打补丁的操做符主要是为了链式调用,但它存在以下问题:

  1. 任何导入了补丁操做符的库都会致使该库的全部消费者的 Observable.prototype 增大,这会建立一种依赖上的盲区。若是此库移除了某个操做符的导入,这会在无形之中破坏其余全部人的使用。使用 pipeable 操做符的话,你必须在每一个用到它们的页面中都导入你所须要用到的操做符。

  2. 经过打补丁的方式将操做符挂在原型上是没法经过像 rollup 或 webpack 这样的工具进行“摇树优化” ( tree-shakeable ) 。而 pipeable 操做符只是直接从模块中提取的函数而已。

  3. 对于在应用中导入的未使用过的操做符,任何类型的构建工具或 lint 规则都没法可靠地检测出它们。例如,好比你导入了 scan,但后来再也不使用了,但它仍会被添加到打包后的文件中。使用 pipeable 操做符的话,若是你再也不使用它的简化,lint 规则能够帮你检测到。

  4. 函数组合 ( functional composition )很棒。建立自定义操做符也变得很是简单,它们就像 rxjs 中的其余全部操做符同样。你再也不须要扩展 Observable 或重写 lift

什么是 pipeable 操做符?

简而言之,就是能够与当前的 let 操做符一块儿使用的函数。不管名称起的是否合适,这就是它的由来。基本上来讲,pipeable 操做符能够是任何函数,可是它须要返回签名为 <T, R>(source: Observable<T>) => Observable<R> 的函数。

如今 Observable 中有一个内置的 pipe 方法 (Observable.prototype.pipe),它能够用相似于以前的链式调用的方式来组合操做符 (以下所示)。

There is also a pipe utility function at rxjs/util/pipe that can be used to build reusable pipeable operators from other pipeable operators.

rxjs/util/pipe 中还有一个名为 pipe 的工具函数,它可用于构建基于其余 pipeable 操做符的可复用的 pipeable 操做符。

用法

你只需在 'rxjs/operators' (注意是复数!) 中便能提取出所须要的任何操做符。还推荐直接导入所需的 Observable 建立操做符,以下面的 range 所示:

import { range } from 'rxjs/observable/range';
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))
复制代码

轻松建立自定义操做符

实际上,你能够一直用 let 来完成...,可是如今建立自定义操做符就像写个函数同样简单。注意,你能够将你的自定义操做符和其余的 rxjs 操做符无缝地组合起来。

import { interval } from 'rxjs/observable/interval';
import { filter, map, take, toArray } from 'rxjs/operators';

/** * 取每第N个值的操做符 */
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(); }
    })
  });

/** * 还可使用现有的操做符 */
const takeEveryNthSimple = (n: number) => <T>(source: Observable<T>) =>
  source.pipe(filter((value, index) => index % n === 0 ))

/** * 由于 pipeable 操做符返回的是函数,还能够进一步简化 */
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));
// [0, 12, 24]
复制代码

已知问题

TypeScript < 2.4

在2.3及如下版本的 TypeScript 中,须要在传递给操做符的函数中添加类型,由于 TypeScript 2.4以前的版本没法推断类型。在TypeScript 2.4中,类型能够经过组合来正确地推断出来。

TS 2.3及如下版本

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 导入 pipeable 操做符,但若是不更新构建过程的话,会常常致使应用的打包文件更大。这是由于默认状况下 rxjs/operators 会解析成 rxjs 的 CommonJS 输出。

为了使用新的 pipeable 操做符而不增长打包尺寸,你须要更新 Webpack 配置。这只适用于 Webpack 3+ ,由于须要依赖 Webpack 3中的新插件 ModuleConcatenationPlugin

路径映射

伴随 rxjs 5.5版本一同发布的是使用ES5 和 ES2015 两种语言级别的 ECMAScript 模块格式 (导入和导出)。你能够在 node_modules/rxjs/_esm5node_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: {
    // 使用 "alias" 键来解析成 ESM 分发版
    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;
复制代码

没法控制构建过程

若是你没法控制构建过程(或者没法更新至 Webpack 3+)的话,上述解决方案将不适合你。因此,从 rxjs/operators 导入极可能让应用的打包文件尺寸更大。但仍是有解决办法的,你须要使用更深一层的导入,有点相似于5.5版本以前导入 pipeable 操做符的方式。

将:

import { map, filter, reduce } from 'rxjs/operators';
复制代码

变成:

import { map } from 'rxjs/operators/map';
import { filter } from 'rxjs/operators/filter';
import { reduce } from 'rxjs/operators/reduce';
复制代码
相关文章
相关标签/搜索