当前位置: 首页 > 软件库 > 程序开发 > >

ember-resources

授权协议 MIT License
开发语言 JavaScript
所属分类 程序开发
软件类型 开源软件
地区 不详
投 递 者 韦智刚
操作系统 跨平台
开源组织
适用人群 未知
 软件概览

ember-resources

CI

An implementation of Resources in Ember.JS without decorators.

Compatibility

  • Ember.js v3.25+
  • TypeScript v4.2+

NOTE: if you are also using ember-could-get-used-to-this, @use is not compatible withthis library's LifecycleResource, and useResource does not work with ember-could-get-used-to-this' Resource.However, both libraries can still be used in the same project.

Installation

npm install ember-resources
# or
yarn add ember-resources
# or
ember install ember-resources

Example

import { useFunction } from 'ember-resources';

class MyClass {
  data = useFunction(this, async () => {
    let response = await fetch('...');
    let json = await response.json();
    return json;
  }),
}
{{this.data.value}}

Usage

useResource

useResource takes either a Resource or LifecycleResource and an args thunk.

import { useResource } from 'ember-resources';

class MyClass {
  data = useResource(this, SomeResource, () => [arg list]);
}

When any tracked data in the args thunk is updated, the Resource will be updated as well

  • The this is to keep track of destruction -- so when MyClass is destroyed, all the resources attached to it can also be destroyed.

  • The resource will do nothing until it is accessed. Meaning, if you have a template that guardsaccess to the data, like:

    {{#if this.isModalShowing}}
       <Modal>{{this.data.someProperty}}</Modal>
    {{/if}}

    the Resource will not be instantiated until isModalShowing is true.

  • For more info on Thunks, scroll to the bottom of the README

useTask

This is a utility wrapper like useResource, but can be passed an ember-concurrency taskso that the ember-concurrency task can reactively be re-called whenever args change.This largely eliminates the need to start concurrency tasks from the constructor, modifiers,getters, etc.

A concurrency task accessed via useTask is only invoked when accessed, and automatically updateswhen it needs to.

import { useTask } from 'ember-resources';

class MyClass {
  myData = useTask(this, this._myTask, () => [args, to, task])

  @task
  *_myTask(args, to, task)  { /* ... */ }
}

Accessing this.myData will represent the last TaskInstance, so all the expected properties are available:value, isRunning, isFinished, etc.See: the TaskInstance docs for more info.

NOTE: ember-resources does not have a dependency on ember-concurrency

Making your own Resources with

Resource

Resources extending this base class have no lifecycle hooks to encourage data-derivation (via getters) andgenerally simpler state-management than you'd otherwise see with the typical lifecycle-hook-aware Resource.

For example, this is how you'd handle initial setup, updates, and teardown with a Resource

import { Resource } from 'ember-resources';
import { registerDestructor } from '@ember/destroyable';

class MyResource extends Resource {
  constructor(owner, args, previous) {
    super(owner, args, previous);

    if (!previous) {
      // initial setup
    } else {
      // update
    }

    registerDestructor(this, () => {
      // teardown function for each instance
    });
  }
}

This works much like useFunction, in that the previous instance is passed to the next instance and thereis no overall persisting instance of MyResource as the args update. This technique eliminates the needto worry about if your methods, properties, and getters might conflict with the base class's API, which isa common complaint among the anti-class folks.

Many times, however, you may not even need to worry about destruction, which is partially what makes optingin to having a "destructor" so fun -- you get to choose how much lifecycle your Resource has.

More info: @ember/destroyable

So why even have a class at all?

You may still want to manage state internal to your resource, such as if you were implementing a"bulk selection" resource for use in tabular data. This hypothetical resource may track its ownpartial / all / none selected state. If the args to this resource change, you get to decide if youwant to reset the state, or pass it on to the next instance of the selection resource.

import { Resource } from 'ember-resources';

class Selection extends Resource {
  @tracked state = NONE; /* or SOME, ALL, ALL_EXCEPT */

  constructor(owner, args, previous) {
    super(owner, args, previous);

    let { filterQueryString } = args.named;

    if (previous && previous.args.named.filterQueryString !== filterQueryString) {
      // reset the state when the consumer has changed which records we care about.
      this.state = NONE;
    }
  }

  @action selectAll() { this.state = ALL; }
  @action deselectAll() { this.state = NONE; }
  @action toggleItem(item) { /* ... */ }
  // etc
}

usage of this Resource could look like this:

// in either a component or route:
export default class MyComponent extends Component {
  @service router;

  get filter() {
    return this.router.currentRouter.queryParams.filter;
  }

  // implementation omitted for brevity -- could be passed to EmberTable or similar
  records = useResource(this, EmberDataQuery, () => ({ filter: this.filter }));

  // the `this.selection.state` property is re-set to NONE when `this.filter` changes
  selection = useResource(this, Selection, () => ({ filterQueryString: this.filter }))
}

For library authors, it may be a kindness to consumers of your library to wrap the useResourcecall so that they only need to manage one import -- for example:

// addon/index.js

// @private
import { Selection } from './wherever/it/is.js';

// @public
export function useSelection(destroyable, thunk) {
  return useResource(destroyable, Selection, thunk);
}

Another example of interacting with previous state may be a "load more" style data loader / pagination:

import { Resource } from 'ember-resources';
import { isDestroyed, isDestroying } from '@ember/destroyable';

class DataLoader extends Resource {
  constructor(owner, args, previous) {
    super(owner, args, previous);

    this.results = previous?.results;

    let { url, offset } = this.args.named;

    fetch(`${url}?offset=${offset}`)
      .then(response => response.json())
      .then(results => {
        if (isDestroyed(this) || isDestroying(this)) return;

        this.results = this.results.concat(result);
    });
  }
}

consumption of the above resource:

import { useResource } from 'ember-resources';

class MyComponent extends Component {
  @tracked offset = 0;

  data = useResource(this, DataLoader, () => ({ url: '...', offset: this.offset }));

  // when a button is clicked, load the next 50 records
  @action loadMore() { this.offset += 50; }
}

LifecycleResource

When possible, you'll want to favor Resource over LifecycleResource as Resource is simpler.

They key differences are that the LifecycleResource base class has 3 lifecycle hooks

  • setup - called upon first access of the resource
  • update - called when any tracked used during setup changes
  • teardown - called when the containing context is torn down

The main advantage to the LifecycleResource is that the teardown hook is for "last teardown",whereas with Resource, if a destructor is registered in the destructor, there is no way to knowif that destruction is the final destruction.

An example of when you'd want to reach for the LifecycleResource is when you're managing external long-livedstate that needs a final destruction call, such as with XState, which requires that the "State machine interpreter"is stopped when you are discarding the parent context (such as a component).

An example

import { LifecycleResource } from 'ember-resources';
import { createMachine, interpret } from 'xstate';

const machine = createMachine(/* ... see XState docs for this function this ... */);

class MyResource extends LifecycleResource {
  @tracked state;

  setup() {
    this.interpreter = interpret(machine).onTransition(state => this.state = state);
  }

  update() {
    this.interpreter.send('ARGS_UPDATED', this.args);
  }

  teardown() {
    this.interpreter.stop();
  }
}

Using this Resource is the exact same as Resource

import { useResource } from 'ember-resources';

class ContainingClass {
  state = useResource(this, MyResource, () => [...])
}

There is however a semi-unintuitive technique you could use to continue to use Resource for the final teardown:

import { Resource } from 'ember-resources';
import { registerDestructor, unregisterDestructior } from '@ember/destroyable';

class MyResource extends Resource {
  constructor(owner, args, previous) {
    super(owner, args, previous);

    registerDestructor(this, this.myFinalCleanup);

    if (previous) {
      // prevent destruction
      unregisterDestructor(prev, prev.myFinalCleanup);
    } else {
      // setup
    }
  }

  @action myFinalCleanup() { /* ... */ }
}

function Resources

While functions can be "stateless", Resources don't provide much value unlessyou can have state. function Resources solve this by passing the previousinvocation's return value as an argument to the next time the function is called.

Example:

import { useFunction } from 'ember-resources';

class StarWarsInfo {
  // access result on info.value
  info = useFunction(this, async (state, ...args) => {
    if (state) {
      let { characters } = state;

      return { characters };
    }

    let [ids] = args;
    let response = await fetch(`/characters/${ids}`) ;
    let characters = await response.json();

    return { characters };
  }, () => [this.ids /* defined somewhere */])
}

characters would be accessed via this.info.value.characters in the StarWarsInfo class

While this example is a bit contrived, hopefully it demonstrates how the state argworks. During the first invocation, state is falsey, allowing the rest of thefunction to execute. The next time this.ids changes, the function will be calledagain, except state will be the { characters } value during the first invocation,and the function will return the initial data.

This particular technique could be used to run any async function safely (as longas the function doesn't interact with this).

In this example, where the function is async, the "value" of info.value is undefined until thefunction completes.

To help prevent accidental async footguns, even if a function is synchronous, it is still ranasynchronously, therefor, the thunk cannot be avoided.

import { useFunction } from 'ember-resources';

class MyClass {
  @tracked num = 3;

  info = useFunction(this, () => {
    return this.num * 2;
  });
}

this.info.value will be undefined, then 6 and will not change when num changes.

Thunks

With the exception of the useResource + class combination, all Thunks are optional.The main caveat is that if your resources will not update without a thunk -- or consumingtracked data within setup / initialization (which is done for you with useFunction).

  • The thunk is "just a function" that allows tracked data to be lazily consumed by the resource.

The args thunk accepts the following data shapes:

() => [an, array]
() => ({ hello: 'there' })
() => ({ named: {...}, positional: [...] })

An array

when an array is passed, inside the Resource, this.args.named will be emptyand this.args.positional will contain the result of the thunk.

for function resources, this is the only type of thunk allowed.

An object of named args

when an object is passed where the key named is not present,this.args.named will contain the result of the thunk and this.args.positionalwill be empty.

An object containing both named args and positional args

when an object is passed containing either keys: named or positional:

  • this.args.named will be the value of the result of the thunk's named property
  • this.args.positional will be the value of the result of the thunk's positional property

This is the same shape of args used throughout Ember's Helpers, Modifiers, etc

Composition

These patterns are primarily unexplored so if you run in to any issues,please open a bug report / issue.

Composing class-based resources is expected to "just work", as classes maintain their own state.

useFunction + useFunction

import Component from '@glimmer/component';
import { useFunction } from 'ember-resources';

class MyComponent extends Component {
  rand = useFunction(this, () => {
    return useFunction(this, () => Math.random());
  });
}

Accessing the result of Math.random() would be done via:

{{this.rand.value.value}}

Something to note about composing resources is that if arguments passed to theouter resource change, the inner resources are discarded entirely.

For example, you'll need to manage the inner resource's cache invalidation yourself if you wantthe inner resource's behavior to be reactive based on outer arguments:

Example data fetching composed functions
import Component from '@glimmer/component';
import { useFunction } from 'ember-resources';

class MyComponent extends Component {
  @tracked id = 1;
  @tracked storeName = 'blogs';

  records = useFunction(this, (state, storeName) => {
      let result: Array<string | undefined> = [];

      if (state?.previous?.storeName === storeName) {
        return state.previous.innerFunction;
      }

      let innerFunction = useFunction(this, (prev, id) => {
        // pretend we fetched a record using the store service
        let newValue = `record:${storeName}-${id}`;

        result = [...(prev || []), newValue];

        return result;
        },
        () => [this.id]
      );

      return new Proxy(innerFunction, {
        get(target, key, receiver) {
          if (key === 'previous') {
            return {
              innerFunction,
              storeName,
            };
          }

          return Reflect.get(target, key, receiver);
        },
      });
    },
    () => [this.storeName]
  );
}
{{this.records.value.value}} -- an array of "records"

Public Types

import type { ArgsWrapper, Named, Positional } from 'ember-resources';

where:

ArgsWrapper

interface ArgsWrapper {
  positional?: unknown[];
  named?: Record<string, unknown>;
}

this is a utility interface that represents all of the args used throughoutEmber.

Example

class MyResource extends LifecycleResource { // default args type
  constructor(owner: unknown, args: ArgsWrapper) {
    super(owner, args);
  }
}

Shorthand for positional only

export interface Positional<T extends Array<unknown>> {
  positional: T;
}

Example:

class MyResource extends LifecycleResource<Positional<[number]>> {
}

Shorthand for named only

export interface Named<T extends Record<string, unknown>> {
  named: T;
}

Example:

class MyResource extends LifecycleResource<Named<{ bananas: number }>> {
}

These shorthands are 3 characters sharter than using the named: orpositional: keys that would be required if not using these shorthands...

Testing

If your resources are consumed by components, you'll want to continue totest using rendering tests, as things should "just work" with those style oftests.

Where things get interesting is when you want to unit test your resources.

There are two approaches:

new the resource directly

import { LifecycleResource } from 'ember-resources';

test('my test', function(assert) {
  class MyResource extends LifecycleResource {
    // ...
  }

  let instance = new MyResource(this.owner, { /* args wrapper */ });

  // assertions with instance
})

The caveat here is that the setup and update functions will have tobe called manually, because we aren't using useResource, which wraps theEmber-builtin invokeHelper, which takes care of reactivity for us. As aconsequence, any changes to the args wrapper will not cause updates tothe resource instance.

For the Resource base class, there is a static helper method which helps simulatethe update behavior.

import { Resource } from 'ember-resources';

test ('my test', function (assert) {
  class MyResource extends Resource {
    // ...
  }

  let instance = new MyResource(this.owner, { /* args wrapper */ });

  let nextInstance = MyResource.next(instance, { /* args wrapper */ });
});

Resource.next, however, does not destroy the instance. For that, you'll want to usedestroy from @ember/destroyable.

import { destroy } from '@ember/destroyable';

// ...

destroy(instance);

Create a wrapper context for reactive consumption

If, instead of creating MyResource directly, like in the example above,it is wrapped in a test class and utilizes useResource:

import { useResource } from 'ember-resources';

class TestContext {
  data = useResource(this, MyResource, () => { ... })
}

changes to args will trigger calls to setup and update.

NOTE: like with all reactivity testing in JS, it's important toawait settled() after a change to a reactive property so that you allowtime for the framework to propagate changes to all the reactive bits.

Example:

import { LifecycleResource, useResource } from 'ember-resources';

test('my test', async function (assert) {
  class Doubler extends LifecycleResource<{ positional: [number] }> {
    get num() {
      return this.args.positional[0] * 2;
    }
  }

  class Test {
    @tracked count = 0;

    data = useResource(this, Doubler, () => [this.count]);
  }

  let foo = new Test();

  assert.equal(foo.data.num, 0);

  foo.count = 3;
  await settled();

  assert.equal(foo.data.num, 6);

Related addons

List of addons that use and wrap ember-resources to provide more specific functionality:

  • ember-data-resources - resources for reactive data fetching with ember-data
  • ember-array-map-resource - provides a useArrayMap function which returns a resource that reactively maps data per-element, so that when the overall collection is dirtied, only the changed/new/removed elements affect the mapped collection

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.

Thanks

This library wouldn't be possible without the work of:

So much appreciate for the work both you have put in to Resources <3

  • 官方文档地址:http://emberjs.com/guides/routing/ 由于是从word考过来的,格式不是太好,大家可以直接去下载我的完整word。里面除了翻译还有原创内容。 http://download.csdn.net/detail/kevinwon1985/5230326 -----------------------------------------------------

  • Ember.js于2022年11月29号发布v4.9.0版本。 截止2022年11月30号,emberjs/ember.js目前拥有22.3k个Star,4.3k个Fork,license为MIT,Issues问题数量共有365个待解决。 通过微信小程序:MyGit。搜索Ember.js并关注,可及时掌握了解Ember.js最近更新信息,可收到微信提醒通知。 Ember.js - A JavaSc

 相关资料
  • Ember检查器是一个浏览器插件,用于调试Ember应用程序。 灰烬检查员包括以下主题 - S.No. 灰烬检查员方式和描述 1 安装Inspector 您可以安装Ember检查器来调试您的应用程序。 2 Object Inspector Ember检查器允许与Ember对象进行交互。 3 The View Tree 视图树提供应用程序的当前状态。 4 检查路由,数据选项卡和库信息 您可以看到检查

  • 英文原文: http://emberjs.com/guides/getting-ember/index/ Ember构建 Ember的发布管理团队针对Ember和Ember Data维护了不同的发布方法。 频道 最新的Ember和Ember Data的 Release,Beta 和 Canary 构建可以在这里找到。每一个频道都提供了一个开发版、最小化版和生产版。更多关于不同频道的信息可以查看博客

  • ember-emojione ember-emojione is your emoji solution for Ember, based on the EmojiOne project. EmojiOne version 2 is used, which is free to use for everyone (CC BY-SA 4.0), you're only required to giv

  • Ember 3D Ember 3D is an Ember addon for using Three.js - an easy to use, lightweight, javascript 3D library. It is designed to: Prescribe a solid file structure to Three.js code using ES6 modules. Ena

  • Ember Table An addon to support large data set and a number of features around table. Ember Table canhandle over 100,000 rows without any rendering or performance issues. Ember Table 3.x supports: Emb

  • vscode-ember This is the VSCode extension to use the Ember Language Server. Features All features currently only work in Ember-CLI apps that use classic structure and are a rough first draft with a lo