原文連結: github.com/ReactiveX/r…
本文為 RxJS 中文社群 翻譯文章,如需轉載,請註明出處,謝謝合作!
如果你也想和我們一起,翻譯更多優質的 RxJS 文章以奉獻給大家,請點選【這裡】
寫在前面的話: (非正文) 去年9月,RxJS5.5beta版本釋出了 lettable 操作符這個新特性,而就在幾天前因為無法忍受此名稱,又將 lettable 更為為 pipeable,詳細請見 PR#3224 。
前段時間忙於翻譯 PWA 一書,因此 RxJS 的專欄擱置了一小段時間,週末抽了些時間出來將官方文件 Pipeable Operators 翻譯出來同步至中文文件。此特性還是比較重要,可以說是 RxJS 未來的走向,下面請看正文。
從5.5版本開始我們提供了 “pipeable 操作符”,它們可以通過 rxjs/operators
來訪問 (注意 "operators" 是複數)。相比較於通過在 rxjs/add/operator/*
中以“打補丁”的方式來獲取需要用到的操作符,這是一種更好的方式。
注意: 如果使用 rxjs/operators
而不修改構建過程的話會導致更大的包。詳見下面的已知問題一節。
重新命名的操作符
由於操作符要從 Observable 中獨立出來,所以操作符的名稱不能和 JavaScript 的關鍵字衝突。因此一些操作符的 pipeable 版本的名稱做出了修改。這些操作符是:
do
->tap
catch
->catchError
switch
->switchAll
finally
->finalize
pipe
是 Observable
的一部分,不需要匯入,並且它可以替代現有的 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 操作符?
打補丁的操作符主要是為了鏈式呼叫,但它存在如下問題:
-
任何匯入了補丁操作符的庫都會導致該庫的所有消費者的
Observable.prototype
增大,這會建立一種依賴上的盲區。如果此庫移除了某個操作符的匯入,這會在無形之中破壞其他所有人的使用。使用 pipeable 操作符的話,你必須在每個用到它們的頁面中都匯入你所需要用到的操作符。 -
通過打補丁的方式將操作符掛在原型上是無法通過像 rollup 或 webpack 這樣的工具進行“搖樹優化” ( tree-shakeable ) 。而 pipeable 操作符只是直接從模組中提取的函式而已。
-
對於在應用中匯入的未使用過的操作符,任何型別的構建工具或 lint 規則都無法可靠地檢測出它們。例如,比如你匯入了
scan
,但後來不再使用了,但它仍會被新增到打包後的檔案中。使用 pipeable 操作符的話,如果你不再使用它的簡化,lint 規則可以幫你檢測到。 -
函式組合 ( 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/_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: {
// 使用 "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';
複製程式碼