Просмотр исходного кода

feat: add catchError option

also propagate error thrown in renderError() to global handler
Evan You 8 лет назад
Родитель
Сommit
b3cd9bc394

+ 11 - 4
src/core/instance/render.js

@@ -101,14 +101,21 @@ export function renderMixin (Vue: Class<Component>) {
     try {
       vnode = render.call(vm._renderProxy, vm.$createElement)
     } catch (e) {
-      handleError(e, vm, `render function`)
+      handleError(e, vm, `render`)
       // return error render result,
       // or previous vnode to prevent render error causing blank component
       /* istanbul ignore else */
       if (process.env.NODE_ENV !== 'production') {
-        vnode = vm.$options.renderError
-          ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
-          : vm._vnode
+        if (vm.$options.renderError) {
+          try {
+            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
+          } catch (e) {
+            handleError(e, vm, `renderError`)
+            vnode = vm._vnode
+          }
+        } else {
+          vnode = vm._vnode
+        }
       } else {
         vnode = vm._vnode
       }

+ 19 - 3
src/core/util/error.js

@@ -5,12 +5,28 @@ import { warn } from './debug'
 import { inBrowser } from './env'
 
 export function handleError (err: Error, vm: any, info: string) {
+  if (vm) {
+    let cur = vm
+    while ((cur = cur.$parent)) {
+      if (cur.$options.catchError) {
+        try {
+          const propagate = cur.$options.catchError.call(cur, err, vm, info)
+          if (!propagate) return
+        } catch (e) {
+          globalHandleError(e, cur, 'catchError')
+        }
+      }
+    }
+  }
+  globalHandleError(err, vm, info)
+}
+
+function globalHandleError (err, vm, info) {
   if (config.errorHandler) {
     try {
-      config.errorHandler.call(null, err, vm, info)
-      return
+      return config.errorHandler.call(null, err, vm, info)
     } catch (e) {
-      logError(e, null, 'errorHandler')
+      logError(e, null, 'config.errorHandler')
     }
   }
   logError(err, vm, info)

+ 2 - 2
test/unit/features/error-handling.spec.js

@@ -7,7 +7,7 @@ describe('Error handling', () => {
   // break parent component
   ;[
     ['data', 'data()'],
-    ['render', 'render function'],
+    ['render', 'render'],
     ['beforeCreate', 'beforeCreate hook'],
     ['created', 'created hook'],
     ['beforeMount', 'beforeMount hook'],
@@ -99,7 +99,7 @@ describe('Error handling', () => {
     const args = spy.calls.argsFor(0)
     expect(args[0].toString()).toContain('Error: render') // error
     expect(args[1]).toBe(vm.$refs.child) // vm
-    expect(args[2]).toContain('render function') // description
+    expect(args[2]).toContain('render') // description
 
     assertRootInstanceActive(vm).then(() => {
       Vue.config.errorHandler = null

+ 121 - 0
test/unit/features/options/catchError.spec.js

@@ -0,0 +1,121 @@
+import Vue from 'vue'
+
+describe('Options catchError', () => {
+  let globalSpy
+
+  beforeEach(() => {
+    globalSpy = Vue.config.errorHandler = jasmine.createSpy()
+  })
+
+  afterEach(() => {
+    Vue.config.errorHandler = null
+  })
+
+  it('should capture error from child component', () => {
+    const spy = jasmine.createSpy()
+
+    let child
+    let err
+    const Child = {
+      created () {
+        child = this
+        err = new Error('child')
+        throw err
+      },
+      render () {}
+    }
+
+    new Vue({
+      catchError: spy,
+      render: h => h(Child)
+    }).$mount()
+
+    expect(spy).toHaveBeenCalledWith(err, child, 'created hook')
+    // should not propagate by default
+    expect(globalSpy).not.toHaveBeenCalled()
+  })
+
+  it('should be able to render the error in itself', done => {
+    let child
+    const Child = {
+      created () {
+        child = this
+        throw new Error('error from child')
+      },
+      render () {}
+    }
+
+    const vm = new Vue({
+      data: {
+        error: null
+      },
+      catchError (e, vm, info) {
+        expect(vm).toBe(child)
+        this.error = e.toString() + ' in ' + info
+      },
+      render (h) {
+        if (this.error) {
+          return h('pre', this.error)
+        }
+        return h(Child)
+      }
+    }).$mount()
+
+    waitForUpdate(() => {
+      expect(vm.$el.textContent).toContain('error from child')
+      expect(vm.$el.textContent).toContain('in created hook')
+    }).then(done)
+  })
+
+  it('should propagate to global handler when returning true', () => {
+    const spy = jasmine.createSpy()
+
+    let child
+    let err
+    const Child = {
+      created () {
+        child = this
+        err = new Error('child')
+        throw err
+      },
+      render () {}
+    }
+
+    new Vue({
+      catchError (err, vm, info) {
+        spy(err, vm, info)
+        return true
+      },
+      render: h => h(Child, {})
+    }).$mount()
+
+    expect(spy).toHaveBeenCalledWith(err, child, 'created hook')
+    // should propagate
+    expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook')
+  })
+
+  it('should propagate to global handler if itself throws error', () => {
+    let child
+    let err
+    const Child = {
+      created () {
+        child = this
+        err = new Error('child')
+        throw err
+      },
+      render () {}
+    }
+
+    let err2
+    const vm = new Vue({
+      catchError () {
+        err2 = new Error('foo')
+        throw err2
+      },
+      render: h => h(Child, {})
+    }).$mount()
+
+    expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook')
+    expect(globalSpy).toHaveBeenCalledWith(err2, vm, 'catchError')
+  })
+})

+ 14 - 0
test/unit/features/options/renderError.spec.js

@@ -25,4 +25,18 @@ describe('Options renderError', () => {
       Vue.config.errorHandler = null
     }).then(done)
   })
+
+  it('should pass on errors in renderError to global handler', () => {
+    const spy = Vue.config.errorHandler = jasmine.createSpy()
+    const err = new Error('renderError')
+    const vm = new Vue({
+      render () {
+        throw new Error('render')
+      },
+      renderError () {
+        throw err
+      }
+    }).$mount()
+    expect(spy).toHaveBeenCalledWith(err, vm, 'renderError')
+  })
 })