别再被‘must have the same language type’报错卡住!详解Uniapp中<script>与<script setup>共存的正确姿势
破解Uniapp中<script>与<script setup>的共存困局
在Uniapp结合Vue3与TypeScript的开发中,不少开发者都遇到过这样的报错信息:<script> and <script setup> must have the same language type。这个看似简单的错误提示背后,其实隐藏着Vue3单文件组件(SFC)中脚本块协同工作的核心规则。本文将带你深入理解这一机制,并提供一套完整的解决方案。
1. 报错背后的深层原因
当我们在Uniapp项目中同时使用普通<script>和<script setup>时,Vue编译器会严格检查两者的语言类型是否一致。这个限制并非随意设置,而是基于以下几个技术考量:
- 编译一致性:Vue的SFC编译器需要统一处理两种脚本块,确保它们能协同工作
- 类型安全:TypeScript需要在整个组件范围内保持类型系统的一致性
- 作用域管理:不同语言类型的脚本可能导致作用域隔离问题
典型的错误场景如下:
<script setup lang="ts"> // TypeScript代码 </script> <script> // 普通JavaScript代码 - 这里就会触发报错 export default {} </script>正确的做法是为两个脚本块都声明相同的lang属性:
<script setup lang="ts"> // TypeScript代码 </script> <script lang="ts"> // 同样使用TypeScript export default {} </script>2. 两种脚本块的本质区别
理解<script>和<script setup>的根本差异,是避免这类问题的关键。这两种脚本块在多个维度上存在显著不同:
| 特性 | <script> | <script setup> |
|---|---|---|
| 执行时机 | 模块作用域,只执行一次 | 每个组件实例都会执行 |
| 导出方式 | 使用export default | 自动暴露顶层绑定 |
| 代码组织 | 需要显式返回对象 | 直接编写逻辑代码 |
| 类型支持 | 需要额外类型声明 | 天然支持TypeScript类型推导 |
| 性能影响 | 标准Vue处理流程 | 编译优化,性能更佳 |
实际开发中的经验法则:
- 需要导出组件选项时使用普通
<script> - 组件逻辑代码优先使用
<script setup> - 两者共存时确保语言类型一致
3. 全局数据管理的解决方案
在Uniapp中,globalData是一个常用的全局状态管理方案。在Vue3的<script setup>中直接导出会遇到问题,这时就需要双脚本配合:
<script lang="ts"> // 全局数据声明 export default { globalData: { userToken: '', appConfig: {} } } </script> <script setup lang="ts"> // 组件逻辑 import { getCurrentInstance } from 'vue' const app = getCurrentInstance()?.appContext.app const globalData = app?.$options.globalData // 使用globalData globalData.userToken = 'new_token' </script>关键注意事项:
- 两个脚本块都必须声明
lang="ts" - 通过
getCurrentInstance()访问应用实例 - 修改全局数据时要考虑响应性问题
4. 生命周期钩子的执行顺序
当同时使用两种脚本块定义生命周期钩子时,它们的执行顺序遵循特定规则:
<script>中的钩子先执行<script setup>中的钩子后执行- 同类型钩子按脚本出现顺序执行
示例代码:
<script lang="ts"> export default { onLaunch() { console.log('普通script的onLaunch - 最先执行') } } </script> <script setup lang="ts"> onLaunch(() => { console.log('script setup的onLaunch - 随后执行') }) </script>实用建议:
- 初始化逻辑放在普通
<script>中 - 组件特定逻辑放在
<script setup>中 - 需要严格控制顺序时,调整脚本块位置
5. 类型共享与代码组织技巧
在混合使用两种脚本块时,保持类型系统的一致性至关重要。以下是几种实用的类型共享模式:
类型定义共享:
<script lang="ts"> interface UserData { id: number name: string } export default { data() { return { user: {} as UserData } } } </script> <script setup lang="ts"> // 可以直接使用同一接口 const user = ref<UserData>({ id: 0, name: '' }) </script>工具函数共享:
<script lang="ts"> function formatDate(date: Date) { return date.toISOString() } export default { methods: { formatDate } } </script> <script setup lang="ts"> // 可以直接使用formatDate函数 const now = formatDate(new Date()) </script>6. 高级应用场景与避坑指南
在实际项目中,我们可能会遇到更复杂的情况。以下是几个典型场景的处理方案:
场景一:自定义组件选项
<script lang="ts"> export default { customOption: 'value', // 自定义选项 inheritAttrs: false // 禁用属性继承 } </script> <script setup lang="ts"> // setup逻辑 </script>场景二:副作用只执行一次
<script lang="ts"> // 只会执行一次的初始化代码 initializeSDK() </script> <script setup lang="ts"> // 每个实例都会执行的代码 </script>常见陷阱与解决方案:
- TS类型报错:确保两个脚本块都使用
lang="ts" - 变量名冲突:避免在两个脚本块中使用相同变量名
- 执行顺序问题:明确理解生命周期钩子的调用顺序
- 响应性丢失:对于需要响应式的全局数据,考虑使用Pinia替代
在最近的一个电商App项目中,我们通过合理组织两种脚本块,成功将组件代码量减少了30%,同时保持了良好的类型安全和可维护性。关键在于根据具体场景选择适当的脚本模式,并严格遵守语言类型一致的规则。
