项目作者: foxbenjaminfox

项目描述 :
Async computed properties for Vue.js
高级语言: JavaScript
项目地址: git://github.com/foxbenjaminfox/vue-async-computed.git
创建时间: 2016-04-06T16:37:06Z
项目社区:https://github.com/foxbenjaminfox/vue-async-computed

开源协议:MIT License

下载


vue-async-computed



NPM Version



Build Status



Downloads



License

With this plugin, you can have computed properties in Vue that are computed asynchronously.

Without using this plugin, you can’t do this:

  1. export default {
  2. data () {
  3. return {
  4. userId: 1
  5. }
  6. },
  7. computed: {
  8. username () {
  9. return fetch(`/get-username-by-id/${this.userId}`)
  10. // This assumes that this endpoint will send us a response
  11. // that contains something like this:
  12. // {
  13. // "username": "username-goes-here"
  14. // }
  15. .then(response => response.json())
  16. .then(user => user.username)
  17. }
  18. }
  19. }

Or rather, you could, but it wouldn’t do what you’d want it to do. But using this plugin, it works just like you’d expect:

  1. export default {
  2. data () {
  3. return {
  4. userId: 1
  5. }
  6. },
  7. asyncComputed: {
  8. username () {
  9. return fetch(`/get-username-by-id/${this.userId}`)
  10. .then(r => r.json())
  11. .then(user => user.username)
  12. }
  13. }
  14. }

This is especially useful with ES7 async functions:

  1. export default {
  2. asyncComputed: {
  3. async someCalculation () {
  4. const x = await someAsycFunction()
  5. const y = await anotherAsyncFunction()
  6. return x + y
  7. }
  8. }
  9. }

Install

  1. npm install --save vue-async-computed

And then install vue-async-computed via app.use() to make it available for all your components:

  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. import AsyncComputed from 'vue-async-computed'
  4. const app = createApp(App)
  5. app.use(AsyncComputed)
  6. app.mount('#app')

Alternately, you can link it directly from a CDN:

  1. <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  2. <script src="https://unpkg.com/vue-async-computed@4.0.1"></script>
  3. <div id="app">
  4. <input type="number" v-model="x"> + <input type="number" v-model="y">
  5. = {{sum == null ? 'Loading' : sum}}
  6. </div>
  7. <script>
  8. const app = Vue.createApp({
  9. data () {
  10. return {
  11. x: 2,
  12. y: 3
  13. }
  14. },
  15. asyncComputed: {
  16. async sum () {
  17. const total = this.x + this.y
  18. await new Promise(resolve => setTimeout(resolve, 1000))
  19. return total
  20. }
  21. }
  22. })
  23. app.use(AsyncComputed)
  24. app.mount('#app')
  25. </script>

Usage example

  1. export default {
  2. data () {
  3. return {
  4. x: 2,
  5. y: 3
  6. }
  7. },
  8. /*
  9. When you create a Vue instance (or component),
  10. you can pass an object named "asyncComputed" as well as
  11. or instead of the standard "computed" option. The functions
  12. you pass to "asyncComputed" should return promises, and the values
  13. those promises resolve to are then asynchronously bound to the
  14. Vue instance as they resolve. Just as with normal computed
  15. properties, if the data the property depends on changes
  16. then the property is re-run automatically.
  17. You can almost completely ignore the fact that behind the
  18. scenes they are asynchronous. The one thing to remember is
  19. that until a asynchronous property's promise resolves
  20. for the first time, the value of the computed property is null.
  21. */
  22. asyncComputed: {
  23. /*
  24. Until one second has passed, vm.sum will be null. After that,
  25. vm.sum will be 5. If you change vm.x or vm.y, then one
  26. second later vm.sum will automatically update itself to be
  27. the sum of the values to which you set vm.x and vm.y the previous second.
  28. */
  29. async sum () {
  30. const total = this.x + this.y
  31. await new Promise(resolve => setTimeout(resolve, 1000))
  32. return total
  33. }
  34. }
  35. }

Like with regular synchronous computed properties, you can pass an object
with a get method instead of a function, but unlike regular computed
properties, async computed properties are always getter-only. If the
object provided has a set method it will be ignored.

Async computed properties can also have a custom default value, which will
be used until the data is loaded for the first time:

  1. export default {
  2. data () {
  3. return {
  4. postId: 1
  5. }
  6. },
  7. asyncComputed: {
  8. blogPostContent: {
  9. // The `get` function is the same as the function you would
  10. // pass directly as the value to `blogPostContent` if you
  11. // didn't need to specify a default value.
  12. async get () {
  13. const post = await fetch(`/post/${this.postId}`)
  14. .then(response => response.json())
  15. return post.postContent
  16. },
  17. // The computed property `blogPostContent` will have
  18. // the value 'Loading...' until the first time the promise
  19. // returned from the `get` function resolves.
  20. default: 'Loading...'
  21. }
  22. }
  23. }
  24. /*
  25. Now you can display {{blogPostContent}} in your template, which
  26. will show a loading message until the blog post's content arrives
  27. from the server.
  28. */

You can instead define the default value as a function, in order to depend on
props or on data:

  1. export default {
  2. data () {
  3. return {
  4. postId: 1
  5. }
  6. },
  7. asyncComputed: {
  8. blogPostContent: {
  9. async get () {
  10. const post = await fetch(`/post/${this.postId}`)
  11. .then(response => response.json())
  12. return post.postContent
  13. },
  14. default () {
  15. return `Loading post ${this.postId}...`
  16. }
  17. }
  18. }
  19. }

You can also set a custom global default value in the options passed to app.use:

  1. app.use(AsyncComputed, {
  2. default: 'Global default value'
  3. })

Recalculation

Just like normal computed properties, async computed properties keep track of their dependencies, and are only
recalculated if those dependencies change. But often you’ll have an async computed property you’ll want to run again
without any of its (local) dependencies changing, such as for instance the data may have changed in the database.

You can set up a watch property, listing the additional dependencies to watch.
Your async computed property will then be recalculated also if any of the watched
dependencies change, in addition to the real dependencies the property itself has:

  1. export default {
  2. data () {
  3. return {
  4. postId: 1,
  5. timesPostHasBeenUpdated: 0
  6. }
  7. },
  8. asyncComputed: {
  9. // blogPostContent will update its contents if postId is changed
  10. // to point to a diffrent post, but will also refetch the post's
  11. // contents when you increment timesPostHasBeenUpdated.
  12. blogPostContent: {
  13. async get () {
  14. const post = await fetch(`/post/${this.postId}`)
  15. .then(response => response.json())
  16. return post.postContent
  17. },
  18. watch: ['timesPostHasBeenUpdated']
  19. }
  20. }
  21. }

Just like with Vue’s normal watch, you can use a dotted path in order to watch a nested property. For example, watch: ['a.b.c', 'd.e'] would declare a dependency on this.a.b.c and on this.d.e.

You can trigger re-computation of an async computed property manually, e.g. to re-try if an error occurred during evaluation. This should be avoided if you are able to achieve the same result using a watched property.

  1. export default {
  2. asyncComputed: {
  3. blogPosts: {
  4. async get () {
  5. return fetch('/posts')
  6. .then(response => response.json())
  7. }
  8. }
  9. },
  10. methods: {
  11. refresh() {
  12. // Triggers an immediate update of blogPosts
  13. // Will work even if an update is in progress.
  14. this.$asyncComputed.blogPosts.update()
  15. }
  16. }
  17. }

Conditional Recalculation

Using watch it is possible to force the computed property to run again unconditionally.
If you need more control over when the computation should be rerun you can use shouldUpdate:

  1. export default {
  2. data () {
  3. return {
  4. postId: 1,
  5. // Imagine pageType can be one of 'index', 'details' and 'edit'.
  6. pageType: 'index'
  7. }
  8. },
  9. asyncComputed: {
  10. blogPostContent: {
  11. async get () {
  12. const post = await fetch(`/post/${this.postId}`)
  13. .then(response => response.json())
  14. return post.postContent
  15. },
  16. // Will update whenever the pageType or postId changes,
  17. // but only if the pageType is not 'index'. This way the
  18. // blogPostContent will be refetched only when loading the
  19. // 'details' and 'edit' pages.
  20. shouldUpdate () {
  21. return this.pageType !== 'index'
  22. }
  23. }
  24. }
  25. }

The main advantage over adding an if statement within the get function is that the old value is still accessible even if the computation is not re-run.

Lazy properties

Normally, computed properties are both run immediately, and re-run as necessary when their dependencies change.
With async computed properties, you sometimes don’t want that. With lazy: true, an async computed
property will only be computed the first time it’s accessed.

For example:

  1. export default {
  2. data () {
  3. return {
  4. id: 1
  5. }
  6. },
  7. asyncComputed: {
  8. mightNotBeNeeded: {
  9. lazy: true,
  10. async get () {
  11. return fetch(`/might-not-be-needed/${this.id}`)
  12. .then(response => response.json())
  13. .then(response => response.value)
  14. }
  15. // The value of `mightNotBeNeeded` will only be
  16. // calculated when it is first accessed.
  17. }
  18. }
  19. }

Computation status

For each async computed property, an object is added to $asyncComputed that contains information about the current computation state of that object. This object contains the following properties:

  1. {
  2. // Can be one of updating, success, error
  3. state: 'updating',
  4. // A boolean that is true while the property is updating.
  5. updating: true,
  6. // The property finished updating without errors (the promise was resolved) and the current value is available.
  7. success: false,
  8. // The promise was rejected.
  9. error: false,
  10. // The raw error/exception with which the promise was rejected.
  11. exception: null
  12. }

It is meant to be used in your rendering code to display update / error information:

  1. <script>
  2. export default {
  3. asyncComputed: {
  4. async posts() {
  5. return fetch('/posts').then(r => r.json())
  6. }
  7. }
  8. }
  9. </script>
  10. <template>
  11. <!-- This will display a loading message every time the posts are updated: -->
  12. <template v-if="$asyncComputed.posts.updating">Loading...</template>
  13. <!-- You can display an error message if loading the posts failed. -->
  14. <template v-else-if="$asyncComputed.posts.error">
  15. Error while loading posts: {{ $asyncComputed.posts.exception }}
  16. <button @click="$asyncComputed.posts.update()">Retry</button>
  17. </template>
  18. <!-- Or finally, display the result: -->
  19. <template v-else>
  20. {{ posts }}
  21. </template>
  22. </template>

Note: If you want to display a special message the first time the posts load, you can use the fact that the default value is null:

  1. <div v-if="$asyncComputed.posts.updating && posts === null"> Loading posts </div>

Global error handling

By default, in case of a rejected promise in an async computed property, vue-async-computed will take care of logging the error for you.

If you want to use a custom logging function, the plugin takes an errorHandler
option, which should be the function you want called with the error information.
By default, it will be called with only the error’s stack trace as an argument,
but if you register the errorHandler with useRawError set to true the
function will receive the raw error, a reference to the Vue instance that
threw the error and the error’s stack trace.

For example:

  1. app.use(AsyncComputed, {
  2. errorHandler (stack) {
  3. console.log('Hey, an error!')
  4. console.log('---')
  5. console.log(stack)
  6. }
  7. })
  8. // Or with `useRawError`:
  9. app.use(AsyncComputed, {
  10. useRawError: true,
  11. errorHandler (err, vm, stack) {
  12. console.log('An error occurred!')
  13. console.log('The error message was: ' + err.msg)
  14. console.log('And the stack trace was:')
  15. console.log(stack)
  16. }
  17. })

You can pass false as the errorHandler in order to silently ignore rejected promises.

License

MIT © Benjamin Fox