IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Cycle.js 7.0 with full support for TypeScript and multiple stream libraries

    shendao发表于 2016-06-28 16:11:01
    love 0

    Full support for TypeScript and multiple stream libraries

    Today we are releasing Cycle Diversity, the next big version of Cycle.js, after months of continuous development. The highlights of this release are:

    • Support for multiple stream libraries : RxJS v4 , RxJS v5 , most.js , xstream
    • xstream is a stream library custom built for Cycle.js
    • Rewritten entirely in TypeScript , so all type definitions are accurate and up-to-date
    • Cycle DOM rewritten in Snabbdom : faster performance, better lifecycle hooks API, smaller kB footprint
    • Cycle DOM isolation rewritten, does not depend anymore on classNames
    • Cycle HTTP has a better, simpler API with 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:

    • Using the new @cycle/____-run packages instead of @cycle/core
    • Migrating from RxJS to xstream ( optional but recommended )
    • Migrating from virtual-dom (Cycle DOM v9) to Snabbdom (Cycle DOM v10)

    Migration guide

    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'),  });

    From RxJS to xstream (optional)

    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.

    New Cycle DOM APIs

    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
    • The DOM Driver from 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.

    From virtual-dom to snabbdom

    The difference between these two underlying libraries is primarily noticed when you are creating virtual DOM elements with hyperscript.

    The same

    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')

    Different

    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 .

    Different SVG hyperscript

    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() ])

    New Cycle HTTP APIs

    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$$ .

    Examples

    To see up-to-date examples illustrating the use of Cycle Diversity, check theexamples repository.

    Some highlights are:

    • bmi-typescript is written in TypeScript
    • http-random-user is written in TypeScript

    For library authors

    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



沪ICP备19023445号-2号
友情链接