[譯] Pipeable 操作符

SangKa發表於2018-01-16

原文連結: github.com/ReactiveX/r…

本文為 RxJS 中文社群 翻譯文章,如需轉載,請註明出處,謝謝合作!

如果你也想和我們一起,翻譯更多優質的 RxJS 文章以奉獻給大家,請點選【這裡】

寫在前面的話: (非正文) 去年9月,RxJS5.5beta版本釋出了 lettable 操作符這個新特性,而就在幾天前因為無法忍受此名稱,又將 lettable 更為為 pipeable,詳細請見 PR#3224

[譯] Pipeable 操作符

前段時間忙於翻譯 PWA 一書,因此 RxJS 的專欄擱置了一小段時間,週末抽了些時間出來將官方文件 Pipeable Operators 翻譯出來同步至中文文件。此特性還是比較重要,可以說是 RxJS 未來的走向,下面請看正文。

從5.5版本開始我們提供了 “pipeable 操作符”,它們可以通過 rxjs/operators 來訪問 (注意 "operators" 是複數)。相比較於通過在 rxjs/add/operator/* 中以“打補丁”的方式來獲取需要用到的操作符,這是一種更好的方式。

注意: 如果使用 rxjs/operators 而不修改構建過程的話會導致更大的包。詳見下面的已知問題一節。

重新命名的操作符

由於操作符要從 Observable 中獨立出來,所以操作符的名稱不能和 JavaScript 的關鍵字衝突。因此一些操作符的 pipeable 版本的名稱做出了修改。這些操作符是:

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

pipeObservable 的一部分,不需要匯入,並且它可以替代現有的 let 操作符。

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';
複製程式碼

相關文章