Today we are releasing Cycle Diversity, the next big version of Cycle.js, after months of continuous development. The highlights of this release are:
HTTPSource.select(category)
Because Cycle.js is split in packages, the packages and their versions which constitute "Cycle Diversity" are:
@cycle/rx-run
or @cycle/core
v7.0.0 or greater @cycle/rxjs-run
v3.0.0 or greater @cycle/most-run
v3.0.0 or greater @cycle/xstream-run
v3.0.0 or greater @cycle/dom
v10.0.0 or greater @cycle/http
v9.0.0 or greater @cycle/jsonp
v6.0.0 or greater @cycle/storage
v3.0.0 or greater Development of Cycle Diversity started 5 months ago, whenTylor andAndre had decided to merge the two projects Cycle.js andMotorcycle.js. The goal was to allow a Cycle.js user (with RxJS) to write their app as normally, while utilizing a Motorcycle.js driver (written in most.js), and vice-versa. This lead to the creation of a "Cycle.run" package for each stream library @cycle/rx-run
and @cycle/most-run
. Each of these run functions knows how to convert the streams from drivers (potentially written with other stream libraries). This also made it possible to create Cycle.run for other stream libraries like the recent RxJS v5, which now has @cycle/rxjs-run
.
Since Cycle Diversity is not anymore centralized around RxJS, this opened up the possibility to build a stream library meant specifically for Cycle.js. Four months ago, Andre and Tylor started experiments, which culminated in the creation of xstream . The highlights of xstream are: (1) all streams are hot (since cold/hot distinction has been a major pain point for Cycle.js users), (2) only a few operators to choose from (making it easier to learn and choose operators), (3) fast performance and very small kB footprint. Particularly the third reason makes xstream suitable for driver libraries. All official drivers are now written with xstream, and we recommend using xstream also for Cycle.js users building apps. We will keep supporting libraries like RxJS and most.js, but xstream is the future of Cycle.js and Cycle.js is the future of xstream.
While rewriting the entire Cycle.js infrastructure for multiple stream libraries, we also used the opportunity to do the rewrite in TypeScript. Plenty of users have previously asked for TypeScript support. Now that the source code itself is in TypeScript, the type definitions are guaranteed to always be up-to-date. We also warmly recommend JavaScript programmers to give TypeScript plus Cycle.js a try. It is easy to set up, and makes the development experience smoother, as you can notice mistakes as soon as possible. We also provide a couple of examples.
Last but not least, Motorcycle.js had a DOM Driver based on Snabbdom, and people have found it to be better than virtual-dom. Snabbdom is faster, smaller, and has an API for hooks which is based on simple functions, not on classes, besides being extensible with Snabbdom modules. We took the best ideas of that DOM Driver with Snabbdom and we rewrote the official Cycle.js with Snabbdom and TypeScript. During this process, we ended up fixing several bugs the previous DOM Driver had, like#226, #227 , #229 , #288 , #291 ,#306. The new Cycle DOM v10 is very solid, tested, fast, fully supports TypeScript, and has some API improvements.
Below you will find some guides on how to migrate to Cycle Diversity. We cover issues like:
@cycle/____-run
packages instead of @cycle/core
The most important thing to know for all applications is that there are now multiple "Cycle Core" packages:
Before | After |
---|---|
@cycle/core |
@cycle/rx-run or @cycle/core |
– | @cycle/xstream-run |
– | @cycle/rxjs-run |
– | @cycle/most-run |
If you npm install the ____-run
package for a stream library, you must also npm install that stream library separately
Package | Must also install |
---|---|
@cycle/rx-run |
rx |
@cycle/xstream-run |
xstream |
@cycle/rxjs-run |
rxjs |
@cycle/most-run |
most |
For instance, in your package.json:
"dependencies": { "@cycle/rxjs-run": "3.0.0", "rxjs": "^5.0.0-beta.8" },
Drivers may use a stream library different to the one you’re using.Pay attention to the requirements that each driver has regarding the stream library they are using. It is usually an npm peer dependency and will show in your terminal when you npm install
. However, you can assume that most official drivers use xstream
.
For instance, if you install Cycle DOM or Cycle HTTP, you must also install xstream
:
"dependencies": { "@cycle/rxjs-run": "3.0.0", "rxjs": "^5.0.0-beta.8", + "@cycle/dom": "^10.0.0", + "xstream": "^5.0.6" },
When the stream library, run
, and drivers are installed, using them together is basically like before:
-import Cycle from '@cycle/core'; +import {run} from '@cycle/rxjs-run'; import {makeDOMDriver} from '@cycle/dom'; // ... -Cycle.run(main, { +run(main, { DOM: makeDOMDriver('#app'), });
If you choose to use xstream but have an existing application written in RxJS, here are some hints that may help you convert the code.
RxJS | xstream |
---|---|
Cold by default | Hot only |
The biggest difference between RxJS and xstream is the cold/hot issue. When migrating, you will notice this by how you won’t need to .share()
in xstream code. There is no .share()
in xstream because all streams are already "shared".
Sometimes, though, a chain of cold streams in RxJS won’t "work" when converted to xstream. This happens specially if you have a Y-shaped dependency. For instance:
a$ -> b$ -> c$
and
a$ -> b$ -> d$
where all of these are cold. The most common case for this is where b$
is a state$
, returned from a model()
function. In RxJS, the entire chain is cold, so there are actually two separate executions of b$
, and that’s why both c$
and d$
get incoming events.
In xstream, there would be just one shared execution of b$
, and if it sent out an initial value, only the first chain with c$
would see it, while d$
would miss it. This will happen if b$
has a .startWith()
or something similar, which emits an initial event synchronously. Usually this is solved by the equivalent of .shareReplay()
in xstream, which is called .remember()
. This operator returns aMemoryStream, which is like an RxJS ReplaySubject.
As a rule of thumb, if a stream represents a "value over time", you should make sure to apply .remember() to make it a MemoryStream.You can do this for every stream that acts like a "value over time" in your code. You don’t need to wait for a bug to happen to only then apply remember()
. A "value over time" is different to an event stream because at any point in time you always expect some value to exist. An example of a "value over time" is a person’s age, while an example of an event stream is a person’s birthday events. Every living person has an age value at any point in time. However, there is no point in talking about "your current birthday", because these are just a stream of events that happen periodically every year. It’s possible to convert from one to the other, though: age$ = birthday$.remember()
. A typical "value over time" in a Cycle.js app is state$
, so make sure these are defined with .remember()
in the end.
RxJS | xstream |
---|---|
.shareReplay(1) |
.remember() |
Those are the largest obstacles. Otherwise, the operator API in xstream is well compatible with RxJS. Compare these:
RxJS | xstream |
---|---|
.map(x => x * 10) |
.map(x => x * 10) |
.map(10) |
.mapTo(10) |
.filter(x => x === 1) |
.filter(x => x === 1) |
.take(1) |
.take(1) |
.last() |
.last() |
.startWith('init') |
.startWith('init') |
Observable.never() |
xs.never() |
Observable.empty() |
xs.empty() |
Observable.throw(err) |
xs.throw(err) |
Observable.of(1, 2, 3) |
xs.of(1, 2, 3) |
Observable.merge(a$, b$, c$) |
xs.merge(a$, b$, c$) |
Observable.fromPromise(p) |
xs.fromPromise(p) |
Some operators and methods, though, are slightly different or have different names:
RxJS | xstream |
---|---|
Observable.interval(1000) |
xs.periodic(1000) |
.subscribe(observer) |
.addListener(listener) |
subscription.unsubscribe() |
.removeListener(listener) |
.skip(3) |
.drop(3) |
.takeUntil(b$) |
.endWhen(b$) |
.catch(fn) |
.replaceError(fn) |
.do(fn) |
.debug(fn) |
.let(fn) |
.compose(fn) |
.scan(fn, seed) |
.fold(fn, seed) |
Observable.combineLatest |
xs.combine |
switch |
flatten |
mergeAll |
flattenConcurrently |
concatAll |
flattenSequentially |
Subject onNext |
shamefullySendNext |
Subject onError |
shamefullySendError |
Subject onComplete |
shamefullySendComplete |
It’s very important to note the difference between scan
and fold
is not just naming. xstream fold
has startWith(seed)
embedded internally. So xstream a$.fold((acc, x) => acc + x, 0)
is equivalent to RxJS a$.startWith(0).scan((acc, x) => acc + x)
. We noticed that in most cases where RxJS scan was used in Cycle.js apps, it was preceded by startWith
, so we built fold
so that it has both together. If you don’t want the seed value emitted initially, then just apply .drop(1)
after fold
.
RxJS | xstream |
---|---|
a$.startWith(0).scan((acc, x) => acc + x) |
a$.fold((acc, x) => acc + x, 0) |
combineLatestin xstream is a bit different. It’s called combine
, and only takes streams as arguments. The output of combine is an array of values, so it usually requires a map
operation after combine
to take the array of values and apply a transformation. It’s usually a good idea to use ES2015 array destructuring on the parameter of the transformation function. E.g. .map(([a,b]) => a+b)
not .map(arr => arr[0] + arr[1])
.
RxJS | xstream |
---|---|
Observable.combineLatest(a$, b$, (a,b) => a+b)) |
xs.combine(a$, b$).map(([a,b]) => a+b) |
Also important to note that xstream has no flatMap
nor flatMapLatest
/ switchMap
, but instead you should apply two operators: map
+ flatten
or map
+ flattenConcurrently
:
// RxJS var b$ = a$.flatMap(x => Observable.of(x+1, x+2) ); // xstream var b$ = a$.map(x => xs.of(x+1, x+2) ).compose(flattenConcurrently);
// RxJS var b$ = a$.flatMapLatest(x => Observable.of(x+1, x+2) ); // xstream var b$ = a$.map(x => xs.of(x+1, x+2) ).flatten();
Pay careful attention to the difference in naming:
RxJS | xstream |
---|---|
flatMapLatest | map + flatten |
flatMap | map + flattenConcurrently |
concatMap | map + flattenSequentially |
If you were using the Proxy Subject technique in Cycle.js for building circularly dependent Observables, xstream makes that easier with imitate()
, built in the library specifically for circularly dependent streams:
-var proxy$ = new Rx.Subject(); +var proxy$ = xs.create(); var childSinks = Child({DOM: sources.DOM, foo: proxy$}); -childSinks.actions.subscribe(proxy$); +proxy$.imitate(childSinks.actions);
For more information on xstream, check thedocumentation.
Cycle DOM Driver has slightly new APIs.
Cycle DOM v9 | Cycle DOM v10 |
---|---|
DOMSource.select().observable |
DOMSource.select().elements() |
makeDOMDriver(container, {onError: fn}) |
makeDOMDriver(container) |
makeDOMDriver(container) |
makeDOMDriver(container, {transposition: true}) |
mockDOMSource(mockConfig) |
mockDOMSource(streamAdapter, mockConfig) |
makeHTMLDriver() |
makeHTMLDriver(effectsCallback, options) |
DOMSource.select().elements()
is a simple rename of observable
to elements()
as a function call The new DOM Source conforms to the following API:
interface DOMSource { select(selector: string): DOMSource; elements(): MemoryStream<Element>; events(eventType: string, options?: EventsFnOptions): Stream<Event>; } interface EventsFnOptions { useCapture?: boolean; }
Where the corresponding stream library used above was xstream, but is an Observable if you are using RxJS.
makeDOMDriver
no longer takes an error callback as an option, because top-level errors are handled by Cycle.run makeDOMDriver
will no longer apply transposition of the virtual DOM tree if you don’t opt-in with the option transposition: true
Transposition was a niche feature in Cycle DOM, which enabled you to put a stream as a child of a virtual DOM node , like this:
div([ Observable.interval(1000).map(i => h2('Crazy dynamic header #' + i)), h2('Just a normal header') ])
Then the DOM Driver would take care of flattening those structures for you, so the outcome would be a normal virtual DOM tree. Transposition is now optionally enabled, and we recommend people try to build applications without relying on transposition because it may feel too magical.
mockDOMSource
requires the first parameter to be a Stream Adapter, which is either the object imported from @cycle/rx-adapter
or @cycle/xstream-adapter
or @cycle/rxjs-adapter
or @cycle/most-adapter
. Choose the adapter you want so that mockDOMSource
will produce streams that match the stream library you are using. You don’t need to know anything about adapters, other than importing and giving them to mockDOMSource
makeHTMLDriver
was previously not a real driver because it did not produce any side effects, it just transformed the virtual DOM to HTML as a string. Now, you must provide a callback function effectsCallback
that takes a string of HTML as input and should perform a side effect. Check theisomorphic example to see how to use this feature. Most of the new APIs of Cycle DOM are due to the migration from virtual-dom
to snabbdom
, so read next about these two libraries.
The difference between these two underlying libraries is primarily noticed when you are creating virtual DOM elements with hyperscript.
virtual-dom (Cycle DOM v9) | snabbdom (Cycle DOM v10) |
---|---|
h('h1', 'Hello world') |
h('h1', 'Hello world') |
h1('Hello world') |
h1('Hello world') |
span('.foo', 'Hello world') |
span('.foo', 'Hello world') |
Attributes or properties of elements are expressed differently in Snabbdom hyperscript:
virtual-dom (Cycle DOM v9) | snabbdom (Cycle DOM v10) |
---|---|
div({attributes: {'data-d': 'foo'}}) |
div({attrs: {'data-d': 'foo'}}) |
input({type: 'text'}) |
input({attrs: {type: 'text'}}) or input({props: {type: 'text'}}) |
div({'data-hook': new MyHook()} |
div({hook: {update: myHookFn}}) |
Read more about Snabbdom hyperscript here .
Cycle DOM v10 uses Snabbdom to provide better SVG helper functions. Here are some highlights of the differences:
virtual-dom (Cycle DOM v9) | snabbdom (Cycle DOM v10) |
---|---|
svg('svg') |
svg() |
svg('g') |
svg.g() |
svg('g', {attributes: {'class': 'child'}}) |
svg.g({attrs: {'class': 'child'}}) |
svg('svg', [ svg('g') ]) |
svg([ svg.g() ]) |
Cycle HTTP driver comes with some small differences too. The biggest is the addition of the .select()
API for HTTP Sources, which is similar to .select()
in the DOM Source.
Before (Cycle HTTP v8) | After (Cycle HTTP v9) |
---|---|
httpSource.filter(res$ => res$.request.category === 'foo').response$$ |
httpSource.select('foo') |
It’s also important to notice that in HTTP v8, httpSource
was an Observable of Observables. In HTTP v9, it is an "HTTP Source", an object with functions, just like the DOM Source has. select('foo')
returns the stream of response streams that belong to the requests that had the category field 'foo'
attached to them. This is how you should give a category to a request object:
let request$ = xs.of({ url: 'http://localhost:8080/hello', // GET method by default category: 'hello', });
Categories in the HTTP Driver are like classNames in the DOM Driver. The HTTP Source conforms to this API:
interface HTTPSource { response$$: StreamOfResponseStreams; filter(predicate: (response$: ResponseStream) => boolean): HTTPSource; select(category: string): StreamOfResponseStreams; }
Notice how httpSource.filter()
is a function that returns a new HTTPSource. It is not a filter function over streams yet. To get an actual stream in your corresponding stream library of use, call select()
or response$$
.
To see up-to-date examples illustrating the use of Cycle Diversity, check theexamples repository.
Some highlights are:
How to make your driver Diversity-compliant.
Suppose your driver uses rxjs
as the stream library.
+import RxJSAdapter from '@cycle/rxjs-adapter'; function makeMyDriver() { function myDriver() { // ... } + myDriver.streamAdapter = RxJSAdapter; return myDriver; }
转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Cycle.js 7.0 with full support for TypeScript and multiple stream libraries