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

better template compilation error warnings

Evan You 10 лет назад
Родитель
Сommit
b02b1e5ba4

+ 1 - 0
flow/compiler.js

@@ -100,6 +100,7 @@ declare type ASTElement = {
 declare type ASTExpression = {
   type: 2,
   expression: string,
+  text: string,
   static?: boolean
 }
 

+ 38 - 0
src/compiler/error-detector.js

@@ -0,0 +1,38 @@
+/* @flow */
+
+import { dirRE } from './parser/index'
+
+// detect problematic expressions in a template
+export function detectErrors (ast: ASTNode): Array<string> {
+  const errors: Array<string> = []
+  checkNode(ast, errors)
+  return errors
+}
+
+function checkNode (node: ASTNode, errors: Array<string>) {
+  if (node.type === 1) {
+    for (const name in node.attrsMap) {
+      if (dirRE.test(name)) {
+        const value = node.attrsMap[name]
+        if (value) {
+          checkExpression(value, `${name}="${value}"`, errors)
+        }
+      }
+    }
+    if (node.children) {
+      for (let i = 0; i < node.children.length; i++) {
+        checkNode(node.children[i], errors)
+      }
+    }
+  } else if (node.type === 2) {
+    checkExpression(node.expression, node.text, errors)
+  }
+}
+
+function checkExpression (exp: string, text: string, errors: Array<string>) {
+  try {
+    new Function(exp)
+  } catch (e) {
+    errors.push(`- invalid expression: ${text}`)
+  }
+}

+ 0 - 1
src/compiler/helpers.js

@@ -82,7 +82,6 @@ export function getBindingAttr (
 export function getAndRemoveAttr (el: ASTElement, name: string): ?string {
   let val
   if ((val = el.attrsMap[name]) != null) {
-    el.attrsMap[name] = null
     const list = el.attrsList
     for (let i = 0, l = list.length; i < l; i++) {
       if (list[i].name === name) {

+ 7 - 1
src/compiler/index.js

@@ -11,10 +11,16 @@ export function compile (
   template: string,
   options: CompilerOptions
 ): {
+  ast: ?ASTElement,
   render: string,
   staticRenderFns: Array<string>
 } {
   const ast = parse(template.trim(), options)
   optimize(ast, options)
-  return generate(ast, options)
+  const code = generate(ast, options)
+  return {
+    ast,
+    render: code.render,
+    staticRenderFns: code.staticRenderFns
+  }
 }

+ 10 - 3
src/compiler/parser/index.js

@@ -15,7 +15,7 @@ import {
   baseWarn
 } from '../helpers'
 
-const dirRE = /^v-|^@|^:/
+export const dirRE = /^v-|^@|^:/
 const bindRE = /^:|^v-bind:/
 const onRE = /^@|^v-on:/
 const argRE = /:(.*)$/
@@ -194,9 +194,16 @@ export function parse (
       if (text) {
         let expression
         if (!inPre && text !== ' ' && (expression = parseText(text, delimiters))) {
-          currentParent.children.push({ type: 2, expression })
+          currentParent.children.push({
+            type: 2,
+            expression,
+            text
+          })
         } else {
-          currentParent.children.push({ type: 3, text })
+          currentParent.children.push({
+            type: 3,
+            text
+          })
         }
       }
     }

+ 30 - 5
src/entries/web-compiler.js

@@ -1,8 +1,9 @@
 /* @flow */
 
-import { extend, genStaticKeys } from 'shared/util'
+import { extend, genStaticKeys, noop } from 'shared/util'
 import { warn } from 'core/util/debug'
 import { compile as baseCompile } from 'compiler/index'
+import { detectErrors } from 'compiler/error-detector'
 import modules from 'web/compiler/modules/index'
 import directives from 'web/compiler/directives/index'
 import { isIE, isReservedTag, isUnaryTag, mustUseProp, getTagNamespace } from 'web/util/index'
@@ -49,7 +50,11 @@ export const baseOptions: CompilerOptions = {
 export function compile (
   template: string,
   options?: CompilerOptions
-): { render: string, staticRenderFns: Array<string> } {
+): {
+  ast: ?ASTElement,
+  render: string,
+  staticRenderFns: Array<string>
+} {
   options = options
     ? extend(extend({}, baseOptions), options)
     : baseOptions
@@ -58,7 +63,8 @@ export function compile (
 
 export function compileToFunctions (
   template: string,
-  options?: CompilerOptions
+  options?: CompilerOptions,
+  vm: Component
 ): CompiledFunctions {
   const cache = options && options.preserveWhitespace === false ? cache1 : cache2
   const key = options && options.delimiters
@@ -69,11 +75,30 @@ export function compileToFunctions (
   }
   const res = {}
   const compiled = compile(template, options)
-  res.render = new Function(compiled.render)
+  res.render = makeFunction(compiled.render)
   const l = compiled.staticRenderFns.length
   res.staticRenderFns = new Array(l)
   for (let i = 0; i < l; i++) {
-    res.staticRenderFns[i] = new Function(compiled.staticRenderFns[i])
+    res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])
+  }
+  if (process.env.NODE_ENV !== 'production') {
+    if (res.render === noop || res.staticRenderFns.some(fn => fn === noop)) {
+      const errors = compiled.ast ? detectErrors(compiled.ast, warn) : []
+      warn(
+        `failed to compile template:\n\n${template}\n\n` +
+        errors.join('\n') +
+        '\n\n',
+        vm
+      )
+    }
   }
   return (cache[key] = res)
 }
+
+function makeFunction (code) {
+  try {
+    return new Function(code)
+  } catch (e) {
+    return noop
+  }
+}

+ 1 - 1
src/entries/web-runtime-with-compiler.js

@@ -42,7 +42,7 @@ Vue.prototype.$mount = function (
         preserveWhitespace: config.preserveWhitespace,
         delimiters: options.delimiters,
         warn
-      })
+      }, this)
       options.render = render
       options.staticRenderFns = staticRenderFns
     }

+ 9 - 0
test/unit/features/options/template.spec.js

@@ -50,4 +50,13 @@ describe('Options template', () => {
     }).$mount()
     expect('invalid template option').toHaveBeenWarned()
   })
+
+  it('warn error in generated function', () => {
+    new Vue({
+      template: '<div v-if="!@">{{ a"" }}</div>'
+    }).$mount()
+    expect('failed to compile template').toHaveBeenWarned()
+    expect('invalid expression: v-if="!@"').toHaveBeenWarned()
+    expect('invalid expression: {{ a"" }}').toHaveBeenWarned()
+  })
 })