[譯] 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';
複製代碼
相關文章
相關標籤/搜索