阅读视图

发现新文章,点击刷新页面。

智能化爆发前夜,智界汽车正在经历一场从1到100的品牌跃升战

2026年1月13日,前腾势销售事业部总经理赵长江官宣加盟智界,出任品牌执行董事及执行副总裁。

这则消息在年初的智能汽车行业掀起了不小的波澜——自2025年10月赵长江从比亚迪离职后,这位曾经的腾势销售总经理的去向,始终是行业热议的焦点。

有行业人士分析,赵长江选择智界绝非偶然的个人职业选择,而是智能汽车产业从电动化上半场,迈入智能化下半场的必然人才流向。当行业共识从“电动化替代燃油”转向“智能化定义价值”,具备量产交付经验与高端品牌操盘能力的管理者,正成为掌握核心智能技术的车企最稀缺的资源。赵长江的职业选择,精准踩中了行业变革的脉搏。

这一判断并非个体感知,而是产业演进的客观现实。2025年12月,工信部已正式公布我国首批L3级有条件自动驾驶车型准入许可,长安深蓝SL03、北汽极狐阿尔法S等车型相继开启试点运营,政策破冰为技术落地扫清了关键障碍。正如深度科技研究院院长张孝荣所言,L3商用破冰首次明确系统接管期车企担主责,为商业化扫清了核心制度障碍,智能汽车产业已迎来黄金发展期。

赵长江将华为视为“智能汽车最重要的推动者之一”,他看中的是华为在智能软硬件、生态构建上的全栈能力。而智界背靠的奇瑞近30年造车体系,则为技术落地提供了量产保障。这恰是智能汽车时代的核心生存逻辑:科技公司的创新力需要制造业的确定性托底,二者的深度融合才能在智能化浪潮中站稳脚跟。

智界

智界V9:智界实现从1-100跨越的关键一战

“智界起步不久,正是后来者大有可为之时。”赵长江的笃定背后,是对智界品牌定位的精准把握。赵长江尝试用“智能先锋”的标签,为智界打开差异化竞争的切口——当传统车企仍在电动化转型中补课,新势力陷入同质化内卷,智界依托华为赋能,更有希望站在智能化的第一梯队,走上从1-100的新阶段。

这一跨越的关键一战,落在了智界V9身上。智界V9不仅是鸿蒙智行体系的首款MPV,更是智界2026年的“开门红”之战,其战略意义远超一款车型的市场表现。这款基于华为DriveONE 800V碳化硅动力平台开发、整合华为最高端激光雷达与巨鲸电池的旗舰车型,被余承东寄予“超越市面上所有旗舰MPV”的厚望。

高端MPV赛道早已是硝烟弥漫的红海

2025年,别克GL8家族以超12.2万台的销量继续领跑,新能源版本占比超五成;腾势D9年销9.3万台稳居新能源MPV榜首,而魏牌高山、岚图梦想家等选手正强势崛起,2026年零跑D99等新车型的入局更将让竞争白热化。智界V9的突围,既要靠“纯血鸿蒙”的技术标签建立产品壁垒,更需要渠道、营销、服务的体系化能力加持。

有行业人士分析,腾势D9月均万台的稳定表现,证明了赵长江对高端MPV用户需求的深刻理解,而这种理解能否平移至智界,关键在于能否将腾势的成功经验与华为的智能生态基因深度融合。“产品是矛,体系是盾,智能化时代的竞争,从来都是矛与盾的协同作战”,正是智界V9突围的核心逻辑。

赵长江为智界规划了三大体系强化路径:2026年建立约200家专属渠道网络、构建全生命周期数字化用户体系、落实智界2.0战略强化品牌锐度。这三条路径精准命中了智界建立以来的“短板”。专属渠道网络的搭建,是要摆脱对现有销售体系的依赖,建立品牌专属的用户体验链路;数字化体系的核心是实现“用户声音的闭环流转”,让需求端的反馈直接驱动产品迭代与服务优化;而品牌锐化则是要在用户心智中夯实“智能先锋”的认知——三者共同构成了智界从“产品驱动”到“用户驱动”的转型框架。

智界出海第一站选定东南亚

在智界V9之外,行业或许需要更关注智界汽车的长期全球化布局发展趋势。“从中长期来看,未来的智界还有国际化的路要走,品牌的定位要落实,以及差异化的产品打造要实现。”智界或将出海的第一站选定东南亚,这一选择暗藏巧思。

智界汽车

东南亚市场既是中国智能汽车出海的必争之地,也是智界的战略练兵场。该区域新能源汽车渗透率正快速提升,用户对智能科技的接受度高,且暂无绝对强势的高端智能品牌,为智界提供了窗口期。

但赵长江也认识到,出海的挑战远大于机遇:品牌认知的建立需要长期投入,渠道服务体系的搭建考验本地化能力,而日系车在东南亚的传统优势更是不容忽视。因而他为智界的出海制定了“先品牌认知,再基础建设,后渠道服务”的思路,这符合新兴品牌出海的客观规律,但能否成功,关键在于能否将国内的用户运营经验与东南亚的市场特性相结合——毕竟,全球品牌的根基,永远是本地化的用户共鸣。

品牌焕新向上,是智界的另一中长期目标

赵长江谈到智界汽车的用户“是一群能够接受新鲜事物且能够去引领时代的人”,因此提出“跟华为一起去和用户共创交流,让用户参与到定义品牌的阶段”。这一理念切中了智能时代品牌建设的核心逻辑。

当用户从消费者转变为品牌共建者,将形成强大的口碑传播力与情感凝聚力。智界的用户画像决定了共创模式的可行性,但真正的挑战在于如何建立有效的共创机制——不是简单的意见收集,而是让用户深度参与产品定义、服务设计的全流程。正如赵长江所言,“用户认为你是谁才最关键”,这句话的背后,是品牌主权从企业向用户转移的时代趋势:智能化的竞争,最终是用户生态的竞争;品牌的价值,最终由用户的共识定义。

赵长江在官宣入职智界时公开表示——“与团队一起全心打造一个真正以用户为中心的全球化标杆智能品牌,构建贯穿产品、服务与体验的独树一帜的‘AI时代用户友好体系’”。2026年的智界汽车,正站在智能化爆发与品牌跃升的双重风口:华为的技术赋能提供了“智能领先”的底气,奇瑞的制造体系提供了“量产稳定”的保障,而赵长江的加入,则补上了“规模增长”与“用户运营”的短板。

面对当下极度白热化的市场竞争,智界能否顺利突围,不仅关乎一个品牌的命运,更将为“科技公司+传统车企”的合作模式提供新的行业样本。2026年,这场智能先锋的跃升之战,已然拉开序幕。

Vue 路由信息获取全攻略:8 种方法深度解析

Vue 路由信息获取全攻略:8 种方法深度解析

在 Vue 应用中,获取当前路由信息是开发中的常见需求。本文将全面解析从基础到高级的各种获取方法,并帮助你选择最佳实践。

一、路由信息全景图

在深入具体方法前,先了解 Vue Router 提供的完整路由信息结构:

// 路由信息对象结构
{
  path: '/user/123/profile?tab=info',    // 完整路径
  fullPath: '/user/123/profile?tab=info&token=abc',
  name: 'user-profile',                   // 命名路由名称
  params: {                               // 动态路径参数
    id: '123'
  },
  query: {                                // 查询参数
    tab: 'info',
    token: 'abc'
  },
  hash: '#section-2',                     // 哈希片段
  meta: {                                 // 路由元信息
    requiresAuth: true,
    title: '用户资料'
  },
  matched: [                              // 匹配的路由记录数组
    { path: '/user', component: UserLayout, meta: {...} },
    { path: '/user/:id', component: UserContainer, meta: {...} },
    { path: '/user/:id/profile', component: UserProfile, meta: {...} }
  ]
}

二、8 种获取路由信息的方法

方法 1:$route 对象(最常用)

<template>
  <div>
    <h1>用户详情页</h1>
    <p>用户ID: {{ $route.params.id }}</p>
    <p>当前标签: {{ $route.query.tab || 'default' }}</p>
    <p>需要认证: {{ $route.meta.requiresAuth ? '是' : '否' }}</p>
  </div>
</template>

<script>
export default {
  created() {
    // 访问路由信息
    console.log('路径:', this.$route.path)
    console.log('参数:', this.$route.params)
    console.log('查询:', this.$route.query)
    console.log('哈希:', this.$route.hash)
    console.log('元信息:', this.$route.meta)
    
    // 获取完整的匹配记录
    const matchedRoutes = this.$route.matched
    matchedRoutes.forEach(route => {
      console.log('匹配的路由:', route.path, route.meta)
    })
  }
}
</script>

特点:

  • ✅ 简单直接,无需导入
  • ✅ 响应式变化(路由变化时自动更新)
  • ✅ 在模板和脚本中都能使用

方法 2:useRoute Hook(Vue 3 Composition API)

<script setup>
import { useRoute } from 'vue-router'
import { watch, computed } from 'vue'

// 获取路由实例
const route = useRoute()

// 直接使用
console.log('当前路由路径:', route.path)
console.log('路由参数:', route.params)

// 计算属性基于路由
const userId = computed(() => route.params.id)
const isEditMode = computed(() => route.query.mode === 'edit')

// 监听路由变化
watch(
  () => route.params.id,
  (newId, oldId) => {
    console.log(`用户ID从 ${oldId} 变为 ${newId}`)
    loadUserData(newId)
  }
)

// 监听多个路由属性
watch(
  () => ({
    id: route.params.id,
    tab: route.query.tab
  }),
  ({ id, tab }) => {
    console.log(`ID: ${id}, Tab: ${tab}`)
  },
  { deep: true }
)
</script>

<template>
  <div>
    <h1>用户 {{ userId }} 的资料</h1>
    <nav>
      <router-link :to="{ query: { tab: 'info' } }" 
                   :class="{ active: route.query.tab === 'info' }">
        基本信息
      </router-link>
      <router-link :to="{ query: { tab: 'posts' } }"
                   :class="{ active: route.query.tab === 'posts' }">
        动态
      </router-link>
    </nav>
  </div>
</template>

方法 3:路由守卫中获取

// 全局守卫
router.beforeEach((to, from, next) => {
  // to: 即将进入的路由
  // from: 当前导航正要离开的路由
  
  console.log('前往:', to.path)
  console.log('来自:', from.path)
  console.log('需要认证:', to.meta.requiresAuth)
  
  // 权限检查
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next({
      path: '/login',
      query: { redirect: to.fullPath } // 保存目标路径
    })
  } else {
    next()
  }
})

// 组件内守卫
export default {
  beforeRouteEnter(to, from, next) {
    // 不能访问 this,因为组件实例还没创建
    console.log('进入前:', to.params.id)
    
    // 可以通过 next 回调访问实例
    next(vm => {
      vm.initialize(to.params.id)
    })
  },
  
  beforeRouteUpdate(to, from, next) {
    // 可以访问 this
    console.log('路由更新:', to.params.id)
    this.loadData(to.params.id)
    next()
  },
  
  beforeRouteLeave(to, from, next) {
    // 离开前的确认
    if (this.hasUnsavedChanges) {
      const answer = window.confirm('有未保存的更改,确定离开吗?')
      if (!answer) {
        next(false) // 取消导航
        return
      }
    }
    next()
  }
}

方法 4:$router 对象获取当前路由

export default {
  methods: {
    getCurrentRouteInfo() {
      // 获取当前路由信息(非响应式)
      const currentRoute = this.$router.currentRoute
      
      // Vue Router 4 中的变化
      // const currentRoute = this.$router.currentRoute.value
      
      console.log('当前路由对象:', currentRoute)
      
      // 编程式导航时获取
      this.$router.push({
        path: '/user/456',
        query: { from: currentRoute.fullPath } // 携带来源信息
      })
    },
    
    // 检查是否在特定路由
    isActiveRoute(routeName) {
      return this.$route.name === routeName
    },
    
    // 检查路径匹配
    isPathMatch(pattern) {
      return this.$route.path.startsWith(pattern)
    }
  },
  
  computed: {
    // 基于当前路由的复杂计算
    breadcrumbs() {
      return this.$route.matched.map(route => ({
        name: route.meta?.breadcrumb || route.name,
        path: route.path
      }))
    },
    
    // 获取嵌套路由参数
    nestedParams() {
      const params = {}
      this.$route.matched.forEach(route => {
        Object.assign(params, route.params)
      })
      return params
    }
  }
}

方法 5:通过 Props 传递路由参数(推荐)

// 路由配置
const routes = [
  {
    path: '/user/:id',
    component: UserDetail,
    props: true // 将 params 作为 props 传递
  },
  {
    path: '/search',
    component: SearchResults,
    props: route => ({ // 自定义 props 函数
      query: route.query.q,
      page: parseInt(route.query.page) || 1,
      sort: route.query.sort || 'relevance'
    })
  }
]

// 组件中使用
export default {
  props: {
    // 从路由 params 自动注入
    id: {
      type: [String, Number],
      required: true
    },
    // 从自定义 props 函数注入
    query: String,
    page: Number,
    sort: String
  },
  
  watch: {
    // props 变化时响应
    id(newId) {
      this.loadUser(newId)
    },
    query(newQuery) {
      this.performSearch(newQuery)
    }
  },
  
  created() {
    // 直接使用 props,无需访问 $route
    console.log('用户ID:', this.id)
    console.log('搜索词:', this.query)
  }
}

方法 6:使用 Vuex/Pinia 管理路由状态

// store/modules/route.js (Vuex)
const state = {
  currentRoute: null,
  previousRoute: null
}

const mutations = {
  SET_CURRENT_ROUTE(state, route) {
    state.previousRoute = state.currentRoute
    state.currentRoute = {
      path: route.path,
      name: route.name,
      params: { ...route.params },
      query: { ...route.query },
      meta: { ...route.meta }
    }
  }
}

// 在全局守卫中同步
router.afterEach((to, from) => {
  store.commit('SET_CURRENT_ROUTE', to)
})

// 组件中使用
export default {
  computed: {
    ...mapState({
      currentRoute: state => state.route.currentRoute,
      previousRoute: state => state.route.previousRoute
    }),
    
    // 基于路由状态的衍生数据
    pageTitle() {
      const route = this.currentRoute
      return route?.meta?.title || '默认标题'
    }
  }
}
// Pinia 版本(Vue 3)
import { defineStore } from 'pinia'

export const useRouteStore = defineStore('route', {
  state: () => ({
    current: null,
    history: []
  }),
  
  actions: {
    updateRoute(route) {
      this.history.push({
        ...this.current,
        timestamp: new Date().toISOString()
      })
      
      // 只保留最近10条记录
      if (this.history.length > 10) {
        this.history = this.history.slice(-10)
      }
      
      this.current = {
        path: route.path,
        fullPath: route.fullPath,
        name: route.name,
        params: { ...route.params },
        query: { ...route.query },
        meta: { ...route.meta }
      }
    }
  },
  
  getters: {
    // 获取路由参数
    routeParam: (state) => (key) => {
      return state.current?.params?.[key]
    },
    
    // 获取查询参数
    routeQuery: (state) => (key) => {
      return state.current?.query?.[key]
    },
    
    // 检查是否在特定路由
    isRoute: (state) => (routeName) => {
      return state.current?.name === routeName
    }
  }
})

方法 7:自定义路由混合/组合函数

// 自定义混合(Vue 2)
export const routeMixin = {
  computed: {
    // 便捷访问器
    $routeParams() {
      return this.$route.params || {}
    },
    
    $routeQuery() {
      return this.$route.query || {}
    },
    
    $routeMeta() {
      return this.$route.meta || {}
    },
    
    // 常用路由检查
    $isHomePage() {
      return this.$route.path === '/'
    },
    
    $hasRouteParam(param) {
      return param in this.$route.params
    },
    
    $getRouteParam(param, defaultValue = null) {
      return this.$route.params[param] || defaultValue
    }
  },
  
  methods: {
    // 路由操作辅助方法
    $updateQuery(newQuery) {
      this.$router.push({
        ...this.$route,
        query: {
          ...this.$route.query,
          ...newQuery
        }
      })
    },
    
    $removeQueryParam(key) {
      const query = { ...this.$route.query }
      delete query[key]
      this.$router.push({ query })
    }
  }
}

// 在组件中使用
export default {
  mixins: [routeMixin],
  
  created() {
    console.log('用户ID:', this.$getRouteParam('id', 'default'))
    console.log('是否首页:', this.$isHomePage)
    
    // 更新查询参数
    this.$updateQuery({ page: 2, sort: 'name' })
  }
}
// Vue 3 Composition API 版本
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'

export function useRouteHelpers() {
  const route = useRoute()
  const router = useRouter()
  
  const routeParams = computed(() => route.params || {})
  const routeQuery = computed(() => route.query || {})
  const routeMeta = computed(() => route.meta || {})
  
  const isHomePage = computed(() => route.path === '/')
  
  function getRouteParam(param, defaultValue = null) {
    return route.params[param] || defaultValue
  }
  
  function updateQuery(newQuery) {
    router.push({
      ...route,
      query: {
        ...route.query,
        ...newQuery
      }
    })
  }
  
  function removeQueryParam(key) {
    const query = { ...route.query }
    delete query[key]
    router.push({ query })
  }
  
  return {
    routeParams,
    routeQuery,
    routeMeta,
    isHomePage,
    getRouteParam,
    updateQuery,
    removeQueryParam
  }
}

// 在组件中使用
<script setup>
const {
  routeParams,
  routeQuery,
  getRouteParam,
  updateQuery
} = useRouteHelpers()

const userId = getRouteParam('id')
const currentTab = computed(() => routeQuery.tab || 'info')

function changeTab(tab) {
  updateQuery({ tab })
}
</script>

方法 8:访问 Router 实例的匹配器

export default {
  methods: {
    // 获取所有路由配置
    getAllRoutes() {
      return this.$router.options.routes
    },
    
    // 通过名称查找路由
    findRouteByName(name) {
      return this.$router.options.routes.find(route => route.name === name)
    },
    
    // 检查路径是否匹配路由
    matchRoute(path) {
      // Vue Router 3
      const matched = this.$router.match(path)
      return matched.matched.length > 0
      
      // Vue Router 4
      // const matched = this.$router.resolve(path)
      // return matched.matched.length > 0
    },
    
    // 生成路径
    generatePath(routeName, params = {}) {
      const route = this.findRouteByName(routeName)
      if (!route) return null
      
      // 简单的路径生成(实际项目建议使用 path-to-regexp)
      let path = route.path
      Object.keys(params).forEach(key => {
        path = path.replace(`:${key}`, params[key])
      })
      return path
    }
  }
}

三、不同场景的推荐方案

场景决策表

场景 推荐方案 理由
简单组件中获取参数 $route.params.id 最简单直接
Vue 3 Composition API useRoute() Hook 响应式、类型安全
组件复用/测试友好 Props 传递 解耦路由依赖
复杂应用状态管理 Vuex/Pinia 存储 全局访问、历史记录
多个组件共享逻辑 自定义混合/组合函数 代码复用
路由守卫/拦截器 守卫参数 (to, from) 官方标准方式
需要路由配置信息 $router.options.routes 访问完整配置

性能优化建议

// ❌ 避免在模板中频繁访问深层属性
<template>
  <div>
    <!-- 每次渲染都会计算 -->
    {{ $route.params.user.details.profile.name }}
  </div>
</template>

// ✅ 使用计算属性缓存
<template>
  <div>{{ userName }}</div>
</template>

<script>
export default {
  computed: {
    userName() {
      return this.$route.params.user?.details?.profile?.name || '未知'
    },
    
    // 批量提取路由信息
    routeInfo() {
      const { params, query, meta } = this.$route
      return {
        userId: params.id,
        tab: query.tab,
        requiresAuth: meta.requiresAuth
      }
    }
  }
}
</script>

响应式监听最佳实践

export default {
  watch: {
    // 监听特定参数变化
    '$route.params.id': {
      handler(newId, oldId) {
        if (newId !== oldId) {
          this.loadUserData(newId)
        }
      },
      immediate: true
    },
    
    // 监听查询参数变化
    '$route.query': {
      handler(newQuery) {
        this.applyFilters(newQuery)
      },
      deep: true // 深度监听对象变化
    }
  },
  
  // 或者使用 beforeRouteUpdate 守卫
  beforeRouteUpdate(to, from, next) {
    // 只处理需要的变化
    if (to.params.id !== from.params.id) {
      this.loadUserData(to.params.id)
    }
    next()
  }
}

四、实战案例:用户管理系统

<template>
  <div class="user-management">
    <!-- 面包屑导航 -->
    <nav class="breadcrumbs">
      <router-link v-for="item in breadcrumbs" 
                   :key="item.path"
                   :to="item.path">
        {{ item.title }}
      </router-link>
    </nav>
    
    <!-- 用户详情 -->
    <div v-if="$route.name === 'user-detail'">
      <h2>用户详情 - {{ userName }}</h2>
      <UserTabs :active-tab="activeTab" @change-tab="changeTab" />
      <router-view />
    </div>
    
    <!-- 用户列表 -->
    <div v-else-if="$route.name === 'user-list'">
      <UserList :filters="routeFilters" />
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState(['currentUser']),
    
    // 从路由获取信息
    userId() {
      return this.$route.params.userId
    },
    
    activeTab() {
      return this.$route.query.tab || 'profile'
    },
    
    routeFilters() {
      return {
        department: this.$route.query.dept,
        role: this.$route.query.role,
        status: this.$route.query.status || 'active'
      }
    },
    
    // 面包屑导航
    breadcrumbs() {
      const crumbs = []
      const { matched } = this.$route
      
      matched.forEach((route, index) => {
        const { meta, path } = route
        
        // 生成面包屑项
        if (meta?.breadcrumb) {
          crumbs.push({
            title: meta.breadcrumb,
            path: this.generateBreadcrumbPath(matched.slice(0, index + 1))
          })
        }
      })
      
      return crumbs
    },
    
    // 用户名(需要根据ID查找)
    userName() {
      const user = this.$store.getters.getUserById(this.userId)
      return user ? user.name : '加载中...'
    }
  },
  
  watch: {
    // 监听用户ID变化
    userId(newId) {
      if (newId) {
        this.$store.dispatch('fetchUser', newId)
      }
    },
    
    // 监听标签页变化
    activeTab(newTab) {
      this.updateDocumentTitle(newTab)
    }
  },
  
  created() {
    // 初始化加载
    if (this.userId) {
      this.$store.dispatch('fetchUser', this.userId)
    }
    
    // 设置页面标题
    this.updateDocumentTitle()
    
    // 记录页面访问
    this.logPageView()
  },
  
  methods: {
    changeTab(tab) {
      // 更新查询参数
      this.$router.push({
        ...this.$route,
        query: { ...this.$route.query, tab }
      })
    },
    
    generateBreadcrumbPath(routes) {
      // 生成完整路径
      return routes.map(r => r.path).join('')
    },
    
    updateDocumentTitle(tab = null) {
      const tabName = tab || this.activeTab
      const title = this.$route.meta.title || '用户管理'
      document.title = `${title} - ${this.getTabDisplayName(tabName)}`
    },
    
    logPageView() {
      // 发送分析数据
      analytics.track('page_view', {
        path: this.$route.path,
        name: this.$route.name,
        params: this.$route.params
      })
    }
  }
}
</script>

五、常见问题与解决方案

问题1:路由信息延迟获取

// ❌ 可能在 created 中获取不到完整的 $route
created() {
  console.log(this.$route.params.id) // 可能为 undefined
}

// ✅ 使用 nextTick 确保 DOM 和路由都就绪
created() {
  this.$nextTick(() => {
    console.log('路由信息:', this.$route)
    this.loadData(this.$route.params.id)
  })
}

// ✅ 或者使用 watch + immediate
watch: {
  '$route.params.id': {
    handler(id) {
      if (id) this.loadData(id)
    },
    immediate: true
  }
}

问题2:路由变化时组件不更新

// 对于复用组件,需要监听路由变化
export default {
  // 使用 beforeRouteUpdate 守卫
  beforeRouteUpdate(to, from, next) {
    this.userId = to.params.id
    this.loadUserData()
    next()
  },
  
  // 或者使用 watch
  watch: {
    '$route.params.id'(newId) {
      this.userId = newId
      this.loadUserData()
    }
  }
}

问题3:TypeScript 类型支持

// Vue 3 + TypeScript
import { RouteLocationNormalized } from 'vue-router'

// 定义路由参数类型
interface UserRouteParams {
  id: string
}

interface UserRouteQuery {
  tab?: 'info' | 'posts' | 'settings'
  edit?: string
}

export default defineComponent({
  setup() {
    const route = useRoute()
    
    // 类型安全的参数访问
    const userId = computed(() => {
      const params = route.params as UserRouteParams
      return params.id
    })
    
    const currentTab = computed(() => {
      const query = route.query as UserRouteQuery
      return query.tab || 'info'
    })
    
    // 类型安全的路由跳转
    const router = useRouter()
    function goToEdit() {
      router.push({
        name: 'user-edit',
        params: { id: userId.value },
        query: { from: route.fullPath }
      })
    }
    
    return { userId, currentTab, goToEdit }
  }
})

六、总结:最佳实践指南

  1. 优先使用 Props 传递 - 提高组件可测试性和复用性
  2. 复杂逻辑使用组合函数 - Vue 3 推荐方式,逻辑更清晰
  3. 适当使用状态管理 - 需要跨组件共享路由状态时
  4. 性能优化 - 避免频繁访问深层属性,使用计算属性缓存
  5. 类型安全 - TypeScript 项目一定要定义路由类型

快速选择流程图:

graph TD
    A[需要获取路由信息] --> B{使用场景}
    
    B -->|简单访问参数| C[使用 $route.params]
    B -->|Vue 3 项目| D[使用 useRoute Hook]
    B -->|组件需要复用/测试| E[使用 Props 传递]
    B -->|多个组件共享状态| F[使用 Pinia/Vuex 存储]
    B -->|通用工具函数| G[自定义组合函数]
    
    C --> H[完成]
    D --> H
    E --> H
    F --> H
    G --> H

记住黄金法则:优先考虑组件独立性,只在必要时直接访问路由对象。


思考题:在你的 Vue 项目中,最常使用哪种方式获取路由信息?遇到过哪些有趣的问题?欢迎分享你的实战经验!

Vue Watch 立即执行:5 种初始化调用方案全解析

Vue Watch 立即执行:5 种初始化调用方案全解析

你是否遇到过在组件初始化时就需要立即执行 watch 逻辑的场景?本文将深入探讨 Vue 中 watch 的立即执行机制,并提供 5 种实用方案。

一、问题背景:为什么需要立即执行 watch?

在 Vue 开发中,我们经常遇到这样的需求:

export default {
  data() {
    return {
      userId: null,
      userData: null,
      filters: {
        status: 'active',
        sortBy: 'name'
      },
      filteredUsers: []
    }
  },
  
  watch: {
    // 需要组件初始化时就执行一次
    'filters.status'() {
      this.loadUsers()
    },
    
    'filters.sortBy'() {
      this.sortUsers()
    }
  },
  
  created() {
    // 我们期望:初始化时自动应用 filters 的默认值
    // 但默认的 watch 不会立即执行
  }
}

二、解决方案对比表

方案 适用场景 优点 缺点 Vue 版本
1. immediate 选项 简单监听 原生支持,最简洁 无法复用逻辑 2+
2. 提取为方法 复杂逻辑复用 逻辑可复用,清晰 需要手动调用 2+
3. 计算属性 派生数据 响应式,自动更新 不适合副作用 2+
4. 自定义 Hook 复杂业务逻辑 高度复用,可组合 需要额外封装 2+ (Vue 3 最佳)
5. 侦听器工厂 多个相似监听 减少重复代码 有一定复杂度 2+

三、5 种解决方案详解

方案 1:使用 immediate: true(最常用)

export default {
  data() {
    return {
      searchQuery: '',
      searchResults: [],
      loading: false
    }
  },
  
  watch: {
    // 基础用法:立即执行 + 深度监听
    searchQuery: {
      handler(newVal, oldVal) {
        this.performSearch(newVal)
      },
      immediate: true,    // ✅ 组件创建时立即执行
      deep: false         // 默认值,可根据需要开启
    },
    
    // 监听对象属性
    'filters.status': {
      handler(newStatus) {
        this.applyFilter(newStatus)
      },
      immediate: true
    },
    
    // 监听多个源(Vue 2.6+)
    '$route.query': {
      handler(query) {
        // 路由变化时初始化数据
        this.initFromQuery(query)
      },
      immediate: true
    }
  },
  
  methods: {
    async performSearch(query) {
      this.loading = true
      try {
        this.searchResults = await api.search(query)
      } catch (error) {
        console.error('搜索失败:', error)
      } finally {
        this.loading = false
      }
    },
    
    initFromQuery(query) {
      // 从 URL 参数初始化状态
      if (query.search) {
        this.searchQuery = query.search
      }
    }
  }
}

进阶技巧:动态 immediate

export default {
  data() {
    return {
      shouldWatchImmediately: true,
      value: ''
    }
  },
  
  watch: {
    value: {
      handler(newVal) {
        this.handleValueChange(newVal)
      },
      // 动态决定是否立即执行
      immediate() {
        return this.shouldWatchImmediately
      }
    }
  }
}

方案 2:提取为方法并手动调用(最灵活)

export default {
  data() {
    return {
      pagination: {
        page: 1,
        pageSize: 20,
        total: 0
      },
      items: []
    }
  },
  
  created() {
    // ✅ 立即调用一次
    this.handlePaginationChange(this.pagination)
    
    // 同时设置 watch
    this.$watch(
      () => ({ ...this.pagination }),
      this.handlePaginationChange,
      { deep: true }
    )
  },
  
  methods: {
    async handlePaginationChange(newPagination, oldPagination) {
      // 避免初始化时重复调用(如果 created 中已调用)
      if (oldPagination === undefined) {
        // 这是初始化调用
        console.log('初始化加载数据')
      }
      
      // 防抖处理
      if (this.loadDebounce) {
        clearTimeout(this.loadDebounce)
      }
      
      this.loadDebounce = setTimeout(async () => {
        this.loading = true
        try {
          const response = await api.getItems({
            page: newPagination.page,
            pageSize: newPagination.pageSize
          })
          this.items = response.data
          this.pagination.total = response.total
        } catch (error) {
          console.error('加载失败:', error)
        } finally {
          this.loading = false
        }
      }, 300)
    }
  }
}

优势对比:

// ❌ 重复逻辑
watch: {
  pagination: {
    handler() { this.loadData() },
    immediate: true,
    deep: true
  },
  filters: {
    handler() { this.loadData() },  // 重复的 loadData 调用
    immediate: true,
    deep: true
  }
}

// ✅ 提取方法,复用逻辑
created() {
  this.loadData()  // 初始化调用
  
  // 多个监听复用同一方法
  this.$watch(() => this.pagination, this.loadData, { deep: true })
  this.$watch(() => this.filters, this.loadData, { deep: true })
}

方案 3:计算属性替代(适合派生数据)

export default {
  data() {
    return {
      basePrice: 100,
      taxRate: 0.08,
      discount: 10
    }
  },
  
  computed: {
    // 计算属性自动响应依赖变化
    finalPrice() {
      const priceWithTax = this.basePrice * (1 + this.taxRate)
      return Math.max(0, priceWithTax - this.discount)
    },
    
    // 复杂计算场景
    formattedReport() {
      // 这里会立即执行,并自动响应 basePrice、taxRate、discount 的变化
      return {
        base: this.basePrice,
        tax: this.basePrice * this.taxRate,
        discount: this.discount,
        total: this.finalPrice,
        timestamp: new Date().toISOString()
      }
    }
  },
  
  created() {
    // 计算属性在 created 中已可用
    console.log('初始价格:', this.finalPrice)
    console.log('初始报告:', this.formattedReport)
    
    // 如果需要执行副作用(如 API 调用),仍需要 watch
    this.$watch(
      () => this.finalPrice,
      (newPrice) => {
        this.logPriceChange(newPrice)
      },
      { immediate: true }
    )
  }
}

方案 4:自定义 Hook/Composable(Vue 3 最佳实践)

// composables/useWatcher.js
import { watch, ref, onMounted } from 'vue'

export function useImmediateWatcher(source, callback, options = {}) {
  const { immediate = true, ...watchOptions } = options
  
  // 立即执行一次
  if (immediate) {
    callback(source.value, undefined)
  }
  
  // 设置监听
  watch(source, callback, watchOptions)
  
  // 返回清理函数
  return () => {
    // 如果需要,可以返回清理逻辑
  }
}

// 在组件中使用
import { ref } from 'vue'
import { useImmediateWatcher } from '@/composables/useWatcher'

export default {
  setup() {
    const searchQuery = ref('')
    const filters = ref({ status: 'active' })
    
    // 使用自定义 Hook
    useImmediateWatcher(
      searchQuery,
      async (newQuery) => {
        await performSearch(newQuery)
      },
      { debounce: 300 }
    )
    
    useImmediateWatcher(
      filters,
      (newFilters) => {
        applyFilters(newFilters)
      },
      { deep: true, immediate: true }
    )
    
    return {
      searchQuery,
      filters
    }
  }
}

Vue 2 版本的 Mixin 实现:

// mixins/immediateWatcher.js
export const immediateWatcherMixin = {
  created() {
    this._immediateWatchers = []
  },
  
  methods: {
    $watchImmediate(expOrFn, callback, options = {}) {
      // 立即执行一次
      const unwatch = this.$watch(
        expOrFn,
        (...args) => {
          callback(...args)
        },
        { ...options, immediate: true }
      )
      
      this._immediateWatchers.push(unwatch)
      return unwatch
    }
  },
  
  beforeDestroy() {
    // 清理所有监听器
    this._immediateWatchers.forEach(unwatch => unwatch())
    this._immediateWatchers = []
  }
}

// 使用
export default {
  mixins: [immediateWatcherMixin],
  
  created() {
    this.$watchImmediate(
      () => this.userId,
      (newId) => {
        this.loadUserData(newId)
      }
    )
  }
}

方案 5:侦听器工厂函数(高级封装)

// utils/watchFactory.js
export function createImmediateWatcher(vm, configs) {
  const unwatchers = []
  
  configs.forEach(config => {
    const {
      source,
      handler,
      immediate = true,
      deep = false,
      flush = 'pre'
    } = config
    
    // 处理 source 可以是函数或字符串
    const getter = typeof source === 'function' 
      ? source 
      : () => vm[source]
    
    // 立即执行
    if (immediate) {
      const initialValue = getter()
      handler.call(vm, initialValue, undefined)
    }
    
    // 创建侦听器
    const unwatch = vm.$watch(
      getter,
      handler.bind(vm),
      { deep, immediate: false, flush }
    )
    
    unwatchers.push(unwatch)
  })
  
  // 返回清理函数
  return function cleanup() {
    unwatchers.forEach(unwatch => unwatch())
  }
}

// 组件中使用
export default {
  data() {
    return {
      filters: { category: 'all', sort: 'newest' },
      pagination: { page: 1, size: 20 }
    }
  },
  
  created() {
    // 批量创建立即执行的侦听器
    this._cleanupWatchers = createImmediateWatcher(this, [
      {
        source: 'filters',
        handler(newFilters) {
          this.applyFilters(newFilters)
        },
        deep: true
      },
      {
        source: () => this.pagination.page,
        handler(newPage) {
          this.loadPage(newPage)
        }
      }
    ])
  },
  
  beforeDestroy() {
    // 清理
    if (this._cleanupWatchers) {
      this._cleanupWatchers()
    }
  }
}

四、实战场景:表单初始化与验证

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.email" @blur="validateEmail" />
    <input v-model="form.password" type="password" />
    
    <div v-if="errors.email">{{ errors.email }}</div>
    <button :disabled="!isFormValid">提交</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      form: {
        email: '',
        password: ''
      },
      errors: {
        email: '',
        password: ''
      },
      isInitialValidationDone: false
    }
  },
  
  computed: {
    isFormValid() {
      return !this.errors.email && !this.errors.password
    }
  },
  
  watch: {
    'form.email': {
      handler(newEmail) {
        // 只在初始化验证后,或者用户修改时验证
        if (this.isInitialValidationDone || newEmail) {
          this.validateEmail()
        }
      },
      immediate: true  // ✅ 初始化时触发验证
    },
    
    'form.password': {
      handler(newPassword) {
        this.validatePassword(newPassword)
      },
      immediate: true  // ✅ 初始化时触发验证
    }
  },
  
  created() {
    // 标记初始化验证完成
    this.$nextTick(() => {
      this.isInitialValidationDone = true
    })
  },
  
  methods: {
    validateEmail() {
      const email = this.form.email
      if (!email) {
        this.errors.email = '邮箱不能为空'
      } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
        this.errors.email = '邮箱格式不正确'
      } else {
        this.errors.email = ''
      }
    },
    
    validatePassword(password) {
      if (!password) {
        this.errors.password = '密码不能为空'
      } else if (password.length < 6) {
        this.errors.password = '密码至少6位'
      } else {
        this.errors.password = ''
      }
    }
  }
}
</script>

五、性能优化与注意事项

1. 避免无限循环

export default {
  data() {
    return {
      count: 0,
      doubled: 0
    }
  },
  
  watch: {
    count: {
      handler(newVal) {
        // ❌ 危险:可能导致无限循环
        this.doubled = newVal * 2
        
        // 在某些条件下修改自身依赖
        if (newVal > 10) {
          this.count = 10  // 这会导致循环
        }
      },
      immediate: true
    }
  }
}

2. 合理使用 deep 监听

export default {
  data() {
    return {
      config: {
        theme: 'dark',
        notifications: {
          email: true,
          push: false
        }
      }
    }
  },
  
  watch: {
    // ❌ 过度使用 deep
    config: {
      handler() {
        this.saveConfig()
      },
      deep: true,  // 整个对象深度监听,性能开销大
      immediate: true
    },
    
    // ✅ 精确监听
    'config.theme': {
      handler(newTheme) {
        this.applyTheme(newTheme)
      },
      immediate: true
    },
    
    // ✅ 监听特定嵌套属性
    'config.notifications.email': {
      handler(newValue) {
        this.updateNotificationPref('email', newValue)
      },
      immediate: true
    }
  }
}

3. 异步操作的防抖与取消

export default {
  data() {
    return {
      searchInput: '',
      searchRequest: null
    }
  },
  
  watch: {
    searchInput: {
      async handler(newVal) {
        // 取消之前的请求
        if (this.searchRequest) {
          this.searchRequest.cancel('取消旧请求')
        }
        
        // 创建新的可取消请求
        this.searchRequest = this.$axios.CancelToken.source()
        
        try {
          const response = await api.search(newVal, {
            cancelToken: this.searchRequest.token
          })
          this.searchResults = response.data
        } catch (error) {
          if (!this.$axios.isCancel(error)) {
            console.error('搜索错误:', error)
          }
        }
      },
      immediate: true,
      debounce: 300  // 需要配合 debounce 插件
    }
  }
}

六、Vue 3 Composition API 特别指南

<script setup>
import { ref, watch, watchEffect } from 'vue'

const userId = ref(null)
const userData = ref(null)
const loading = ref(false)

// 方案1: watch + immediate
watch(
  userId,
  async (newId) => {
    loading.value = true
    try {
      userData.value = await fetchUser(newId)
    } finally {
      loading.value = false
    }
  },
  { immediate: true }  // ✅ 立即执行
)

// 方案2: watchEffect(自动追踪依赖)
const searchQuery = ref('')
const searchResults = ref([])

watchEffect(async () => {
  // 自动追踪 searchQuery 依赖
  if (searchQuery.value.trim()) {
    const results = await searchApi(searchQuery.value)
    searchResults.value = results
  } else {
    searchResults.value = []
  }
})  // ✅ watchEffect 会立即执行一次

// 方案3: 自定义立即执行的 composable
function useImmediateWatch(source, callback, options = {}) {
  const { immediate = true, ...watchOptions } = options
  
  // 立即执行
  if (immediate && source.value !== undefined) {
    callback(source.value, undefined)
  }
  
  return watch(source, callback, watchOptions)
}

// 使用
const filters = ref({ category: 'all' })
useImmediateWatch(
  filters,
  (newFilters) => {
    applyFilters(newFilters)
  },
  { deep: true }
)
</script>

七、决策流程图

graph TD
    A[需要初始化执行watch] --> B{场景分析}
    
    B -->|简单监听,逻辑不复杂| C[方案1: immediate:true]
    B -->|复杂逻辑,需要复用| D[方案2: 提取方法]
    B -->|派生数据,无副作用| E[方案3: 计算属性]
    B -->|Vue3,需要组合复用| F[方案4: 自定义Hook]
    B -->|多个相似监听器| G[方案5: 工厂函数]
    
    C --> H[完成]
    D --> H
    E --> H
    F --> H
    G --> H
    
    style C fill:#e1f5e1
    style D fill:#e1f5e1

八、总结与最佳实践

核心原则:

  1. 优先使用 immediate: true - 对于简单的监听需求
  2. 复杂逻辑提取方法 - 提高可测试性和复用性
  3. 避免副作用在计算属性中 - 保持计算属性的纯函数特性
  4. Vue 3 优先使用 Composition API - 更好的逻辑组织和复用

代码规范建议:

// ✅ 良好实践
export default {
  watch: {
    // 明确注释为什么需要立即执行
    userId: {
      handler: 'loadUserData', // 使用方法名,更清晰
      immediate: true // 初始化时需要加载用户数据
    }
  },
  
  created() {
    // 复杂初始化逻辑放在 created
    this.initializeComponent()
  },
  
  methods: {
    loadUserData(userId) {
      // 可复用的方法
    },
    
    initializeComponent() {
      // 集中处理初始化逻辑
    }
  }
}

常见陷阱提醒:

  1. 不要immediate 回调中修改依赖数据(可能导致循环)
  2. 谨慎使用 deep: true,特别是对于大型对象
  3. 记得清理手动创建的侦听器(避免内存泄漏)
  4. 考虑 SSR 场景下 immediate 的执行时机

Vue 三剑客:组件、插件、插槽的深度辨析

Vue 三剑客:组件、插件、插槽的深度辨析

组件、插件、插槽是 Vue 生态中的三个核心概念,理解它们的差异是掌握 Vue 架构设计的关键。让我们通过一个完整的对比体系来彻底搞懂它们。

一、核心概念全景图

graph TB
    A[Vue 核心概念] --> B[Component 组件]
    A --> C[Plugin 插件]
    A --> D[Slot 插槽]
    
    B --> B1[UI 复用单元]
    B --> B2[局部作用域]
    B --> B3[父子通信]
    
    C --> C1[全局功能扩展]
    C --> C2[一次配置]
    C --> C3[多组件共享]
    
    D --> D1[内容分发]
    D --> D2[灵活占位]
    D --> D3[模板组合]
    
    B --> E[使用插件]
    B --> F[包含插槽]
    C --> G[增强组件]
    D --> H[扩展组件]

二、三者的本质区别:一句话概括

概念 本质 类比
组件 可复用的 UI 单元 乐高积木块
插件 全局功能扩展包 乐高工具箱
插槽 组件的内容占位符 乐高积木上的接口

三、组件 (Component) - Vue 的基石

定义与核心特征

组件是 Vue 应用的构建块,每个组件都是自包含的、可复用的 Vue 实例。

<!-- UserCard.vue - 组件示例 -->
<template>
  <div class="user-card">
    <img :src="avatar" alt="用户头像" />
    <h3>{{ name }}</h3>
    <p>{{ bio }}</p>
    <!-- 使用插槽提供扩展点 -->
    <slot name="actions"></slot>
  </div>
</template>

<script>
export default {
  // 组件定义
  name: 'UserCard',
  props: {
    name: String,
    avatar: String,
    bio: String
  },
  // 局部状态
  data() {
    return {
      isActive: false
    }
  },
  // 生命周期
  mounted() {
    console.log('组件已挂载')
  }
}
</script>

<style scoped>
.user-card {
  border: 1px solid #ccc;
  padding: 20px;
}
</style>

组件的核心能力:

// 1. 组件注册
// 全局注册
Vue.component('global-component', {
  template: '<div>全局组件</div>'
})

// 局部注册
const LocalComponent = {
  template: '<div>局部组件</div>'
}

new Vue({
  components: {
    'local-component': LocalComponent
  }
})

// 2. 组件通信体系
const ParentComponent = {
  template: `
    <child-component 
      :title="parentTitle" 
      @child-event="handleChildEvent"
    />
  `,
  methods: {
    handleChildEvent(payload) {
      // 处理子组件事件
    }
  }
}

四、插件 (Plugin) - Vue 的扩展系统

定义与核心特征

插件是对 Vue 的全局增强,用于添加全局级的功能。

// my-plugin.js - 自定义插件
const MyPlugin = {
  install(Vue, options) {
    // 1. 添加全局方法或属性
    Vue.myGlobalMethod = function() {
      console.log('全局方法')
    }
    
    // 2. 添加全局资源(指令/过滤器/组件)
    Vue.directive('my-directive', {
      bind(el, binding) {
        // 指令逻辑
      }
    })
    
    // 3. 注入组件选项
    Vue.mixin({
      created() {
        console.log('所有组件都会执行')
      }
    })
    
    // 4. 添加实例方法
    Vue.prototype.$myMethod = function() {
      console.log('实例方法')
    }
  }
}

// 使用插件
Vue.use(MyPlugin, { someOption: true })

常见插件类型:

// 1. UI 组件库插件
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)

// 2. 功能增强插件
import VueRouter from 'vue-router'
Vue.use(VueRouter)

import Vuex from 'vuex'
Vue.use(Vuex)

// 3. 工具类插件
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
  loading: '/loading.gif'
})

插件 vs 组件的关键差异:

对比维度 组件 插件
作用范围 局部(需要显式引入) 全局(一次配置,处处可用)
主要目的 构建 UI 界面 增强 Vue 本身的能力
使用频率 高频率、多次使用 一次性配置
典型示例 Button、Modal、Form Router、Vuex、i18n

五、插槽 (Slot) - 组件的灵活扩展点

定义与核心特征

插槽是组件的内容分发出口,让父组件可以向子组件传递模板内容。

<!-- BaseLayout.vue - 包含插槽的组件 -->
<template>
  <div class="container">
    <header>
      <!-- 具名插槽 -->
      <slot name="header"></slot>
    </header>
    
    <main>
      <!-- 默认插槽 -->
      <slot>
        <!-- 后备内容(当父组件不提供内容时显示) -->
        <p>默认内容</p>
      </slot>
    </main>
    
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

插槽的三种类型:

<!-- 父组件使用 -->
<template>
  <BaseLayout>
    <!-- 具名插槽 -->
    <template v-slot:header>
      <h1>页面标题</h1>
    </template>
    
    <!-- 默认插槽(简写) -->
    <p>主要内容区域</p>
    <p>这是默认插槽的内容</p>
    
    <!-- 作用域插槽 -->
    <template v-slot:footer="slotProps">
      <p>页脚: {{ slotProps.year }} 年</p>
    </template>
    
    <!-- 动态插槽名 -->
    <template v-slot:[dynamicSlotName]>
      动态内容
    </template>
  </BaseLayout>
</template>

作用域插槽(高级模式):

<!-- TodoList.vue -->
<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <!-- 作用域插槽:向父组件暴露数据 -->
      <slot :todo="todo" :index="index">
        <!-- 默认显示 -->
        {{ todo.text }}
      </slot>
    </li>
  </ul>
</template>

<!-- 父组件接收数据 -->
<template>
  <TodoList :todos="todos">
    <template v-slot:default="slotProps">
      <span :class="{ completed: slotProps.todo.done }">
        {{ slotProps.index + 1 }}. {{ slotProps.todo.text }}
      </span>
    </template>
  </TodoList>
</template>

六、三者协同工作的完整示例

让我们通过一个实战项目理解三者如何协同:

// 1. 首先安装路由插件
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)  // 🔴 插件:全局启用路由功能

// 2. 定义可复用的布局组件
const AppLayout = {
  template: `
    <div class="app-layout">
      <slot name="navbar"></slot>
      <div class="content">
        <!-- 默认插槽用于显示页面内容 -->
        <slot></slot>
      </div>
      <slot name="footer"></slot>
    </div>
  `
}

// 3. 创建页面组件
const HomePage = {
  template: `
    <AppLayout>
      <template v-slot:navbar>
        <!-- 向布局组件传递自定义导航栏 -->
        <NavBar title="首页" />
      </template>
      
      <!-- 默认插槽内容 -->
      <h1>欢迎访问</h1>
      <ProductList>
        <!-- 作用域插槽自定义产品显示 -->
        <template v-slot:product="props">
          <ProductCard :product="props.product" />
        </template>
      </ProductList>
      
      <template v-slot:footer>
        <AppFooter />
      </template>
    </AppLayout>
  `,
  components: {
    AppLayout,      // 🔵 组件:布局组件
    NavBar,         // 🔵 组件:导航栏组件
    ProductList,    // 🔵 组件:产品列表
    ProductCard,    // 🔵 组件:产品卡片
    AppFooter       // 🔵 组件:页脚组件
  }
}

// 4. 配置路由(使用插件提供的功能)
const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: HomePage  // 使用组件作为路由页面
    }
  ]
})

// 5. 创建Vue实例
new Vue({
  router,  // 🔴 插件提供的路由实例
  template: '<router-view></router-view>'  // 🟡 插槽:路由视图占位
}).$mount('#app')

七、设计模式与最佳实践

何时使用什么?

graph LR
    A[需求分析] --> B{需要什么?}
    
    B -->|构建UI界面| C[使用组件]
    B -->|全局功能扩展| D[使用插件]
    B -->|自定义组件内容| E[使用插槽]
    
    C --> F{组件需要灵活性?}
    F -->|是| G[在组件中添加插槽]
    F -->|否| H[创建完整组件]
    
    D --> I{功能需要复用?}
    I -->|是| J[开发为插件]
    I -->|否| K[使用局部混入]

组件设计原则:

<!-- 好的组件设计示例 -->
<template>
  <!-- 提供清晰的插槽接口 -->
  <div class="card">
    <div class="card-header" v-if="$slots.header">
      <slot name="header"></slot>
    </div>
    
    <div class="card-body">
      <slot>
        <!-- 合理的默认内容 -->
        <p>暂无内容</p>
      </slot>
    </div>
    
    <!-- 作用域插槽提供数据 -->
    <div class="card-footer" v-if="$slots.footer">
      <slot name="footer" :data="footerData"></slot>
    </div>
  </div>
</template>

插件开发规范:

// 良好的插件结构
const WellDesignedPlugin = {
  install(Vue, options = {}) {
    // 1. 参数验证
    if (!options.requiredConfig) {
      console.warn('插件需要配置 requiredConfig')
    }
    
    // 2. 安全的全局扩展
    const version = Number(Vue.version.split('.')[0])
    if (version >= 2) {
      Vue.prototype.$safeMethod = function() {
        // 兼容性处理
      }
    }
    
    // 3. 提供卸载方法
    const originalDestroy = Vue.prototype.$destroy
    Vue.prototype.$destroy = function() {
      // 清理逻辑
      originalDestroy.call(this)
    }
  }
}

八、常见误区与澄清

误区1:插件可以替代组件

// ❌ 错误:用插件实现UI组件
Vue.use({
  install(Vue) {
    Vue.prototype.$showModal = function(content) {
      // 这应该是组件,不是插件
    }
  }
})

// ✅ 正确:组件实现UI,插件封装工具
// Modal.vue - 作为组件
// modal-plugin.js - 如果需要全局调用,可以包装为插件

误区2:插槽就是子组件

<!-- ❌ 误解:插槽是子组件 -->
<Parent>
  <Child />  <!-- 这是组件,不是插槽内容 -->
</Parent>

<!-- ✅ 正确理解 -->
<Parent>
  <!-- 这是插槽内容,会被分发到Parent的<slot>位置 -->
  <template v-slot:default>
    <Child />
  </template>
</Parent>

误区3:过度使用混入(Mixin)

// ❌ 过度使用:应该用插槽或组合式API代替
Vue.mixin({
  data() {
    return {
      globalData: '应该避免'
    }
  }
})

// ✅ 更好的方式:组合式函数(Vue 3)
// 或使用作用域插槽传递数据

九、Vue 3 中的演进

组合式 API 的影响:

<!-- Vue 3 中三者关系更加清晰 -->
<script setup>
// 1. 组件 - 更简洁的定义
import { defineComponent } from 'vue'

// 2. 插件 - 通过 provide/inject 更好地集成
import { provide } from 'vue'
provide('pluginData', data)

// 3. 插槽 - 更灵活的用法
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <!-- 插槽作用域解构 -->
  <slot name="item" v-bind="{ id, name }"></slot>
</template>

十、总结:三位一体的 Vue 架构

概念 角色 关键特征 最佳实践
组件 构建者 局部作用域、props/events接口、可复用 单一职责、合理拆分、明确接口
插件 增强者 全局作用域、一次配置、功能扩展 轻量封装、提供选项、良好文档
插槽 连接者 内容分发、模板组合、作用域暴露 明确命名、提供后备、作用域数据

记住这个核心公式:

应用 = 插件增强的Vue实例 + 组件构建的UI树 + 插槽连接的组件关系

最终决策指南:

  1. 当你需要...

    • 复用UI片段 → 创建组件
    • 添加全局功能 → 开发插件
    • 自定义组件内部结构 → 使用插槽
  2. 在架构中...

    • 插件在最外层配置全局能力
    • 组件在中间层构建功能模块
    • 插槽在最内层实现灵活定制
  3. 进化方向...

    • Vue 2:Options API + 三者分明
    • Vue 3:Composition API + 更灵活的组合

Vue 组件模板的 7 种定义方式:从基础到高级的完整指南

Vue 组件模板的 7 种定义方式:从基础到高级的完整指南

模板是 Vue 组件的核心视图层,但你可能不知道它竟有如此多灵活的定义方式。掌握这些技巧,让你的组件开发更加得心应手。

一、模板定义全景图

在深入细节之前,先了解 Vue 组件模板的完整知识体系:

graph TD
    A[Vue 组件模板] --> B[单文件组件 SFC]
    A --> C[内联模板]
    A --> D[字符串模板]
    A --> E[渲染函数]
    A --> F[JSX]
    A --> G[动态组件]
    A --> H[函数式组件]
    
    B --> B1[&lttemplate&gt标签]
    B --> B2[作用域 slot]
    
    D --> D1[template 选项]
    D --> D2[内联模板字符串]
    
    E --> E1[createElement]
    E --> E2[h 函数]
    
    G --> G1[component:is]
    G --> G2[异步组件]

下面我们来详细探讨每种方式的特点和适用场景。

二、7 种模板定义方式详解

1. 单文件组件(SFC)模板 - 现代 Vue 开发的标准

<!-- UserProfile.vue -->
<template>
  <!-- 最常用、最推荐的方式 -->
  <div class="user-profile">
    <h2>{{ user.name }}</h2>
    <img :src="user.avatar" alt="Avatar" />
    <slot name="actions"></slot>
  </div>
</template>

<script>
export default {
  props: ['user']
}
</script>

<style scoped>
.user-profile {
  padding: 20px;
}
</style>

特点:

  • ✅ 语法高亮和提示
  • ✅ CSS 作用域支持
  • ✅ 良好的可维护性
  • ✅ 构建工具优化(如 Vue Loader)

最佳实践:

<template>
  <!-- 始终使用单个根元素(Vue 2) -->
  <div class="container">
    <!-- 使用 PascalCase 的组件名 -->
    <UserProfile :user="currentUser" />
    
    <!-- 复杂逻辑使用计算属性 -->
    <p v-if="shouldShowMessage">{{ formattedMessage }}</p>
  </div>
</template>

2. 字符串模板 - 简单场景的轻量选择

// 方式1:template 选项
new Vue({
  el: '#app',
  template: `
    <div class="app">
      <h1>{{ title }}</h1>
      <button @click="handleClick">点击</button>
    </div>
  `,
  data() {
    return {
      title: '字符串模板示例'
    }
  },
  methods: {
    handleClick() {
      alert('按钮被点击')
    }
  }
})

// 方式2:内联模板字符串
const InlineComponent = {
  template: '<div>{{ message }}</div>',
  data() {
    return { message: 'Hello' }
  }
}

适用场景:

  • 简单的 UI 组件
  • 快速原型开发
  • 小型项目或演示代码

注意事项:

// ⚠️ 模板字符串中的换行和缩进
const BadTemplate = `
<div>
  <p>第一行
  </p>
</div>  // 缩进可能被包含

// ✅ 使用模板字面量保持整洁
const GoodTemplate = `<div>
  <p>第一行</p>
</div>`

3. 内联模板 - 快速但不推荐

<!-- 父组件 -->
<div id="parent">
  <child-component inline-template>
    <!-- 直接在 HTML 中写模板 -->
    <div>
      <p>来自子组件: {{ childData }}</p>
      <p>来自父组件: {{ parentMessage }}</p>
    </div>
  </child-component>
</div>

<script>
new Vue({
  el: '#parent',
  data: {
    parentMessage: '父组件数据'
  },
  components: {
    'child-component': {
      data() {
        return { childData: '子组件数据' }
      }
    }
  }
})
</script>

⚠️ 警告:

  • ❌ 作用域难以理解
  • ❌ 破坏组件封装性
  • ❌ 不利于维护
  • ✅ 唯一优势:快速原型

4. X-Templates - 分离但老式

<!-- 在 HTML 中定义模板 -->
<script type="text/x-template" id="user-template">
  <div class="user">
    <h3>{{ name }}</h3>
    <p>{{ email }}</p>
  </div>
</script>

<script>
// 在 JavaScript 中引用
Vue.component('user-component', {
  template: '#user-template',
  props: ['name', 'email']
})
</script>

特点:

  • 🟡 模板与逻辑分离
  • 🟡 无需构建工具
  • ❌ 全局命名空间污染
  • ❌ 无法使用构建工具优化

5. 渲染函数 - 完全的 JavaScript 控制力

// 基本渲染函数
export default {
  props: ['items'],
  render(h) {
    return h('ul', 
      this.items.map(item => 
        h('li', { key: item.id }, item.name)
      )
    )
  }
}

// 带条件渲染和事件
export default {
  data() {
    return { count: 0 }
  },
  render(h) {
    return h('div', [
      h('h1', `计数: ${this.count}`),
      h('button', {
        on: {
          click: () => this.count++
        }
      }, '增加')
    ])
  }
}

高级模式 - 动态组件工厂:

// 组件工厂函数
const ComponentFactory = {
  functional: true,
  props: ['type', 'data'],
  render(h, { props }) {
    const components = {
      text: TextComponent,
      image: ImageComponent,
      video: VideoComponent
    }
    
    const Component = components[props.type]
    return h(Component, {
      props: { data: props.data }
    })
  }
}

// 动态 slot 内容
const LayoutComponent = {
  render(h) {
    // 获取具名 slot
    const header = this.$slots.header
    const defaultSlot = this.$slots.default
    const footer = this.$slots.footer
    
    return h('div', { class: 'layout' }, [
      header && h('header', header),
      h('main', defaultSlot),
      footer && h('footer', footer)
    ])
  }
}

6. JSX - React 开发者的福音

// .vue 文件中使用 JSX
<script>
export default {
  data() {
    return {
      items: ['Vue', 'React', 'Angular']
    }
  },
  render() {
    return (
      <div class="jsx-demo">
        <h1>JSX 在 Vue 中</h1>
        <ul>
          {this.items.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
        {/* 使用指令 */}
        <input vModel={this.inputValue} />
        {/* 事件监听 */}
        <button onClick={this.handleClick}>点击</button>
      </div>
    )
  }
}
</script>

配置方法:

// babel.config.js
module.exports = {
  presets: ['@vue/cli-plugin-babel/preset'],
  plugins: [
    '@vue/babel-plugin-jsx' // 启用 Vue JSX 支持
  ]
}

JSX vs 模板:

// JSX 的优势:动态性更强
const DynamicList = {
  props: ['config'],
  render() {
    const { tag: Tag, items, itemComponent: Item } = this.config
    
    return (
      <Tag class="dynamic-list">
        {items.map(item => (
          <Item item={item} />
        ))}
      </Tag>
    )
  }
}

7. 动态组件 - 运行时模板决策

<template>
  <!-- component:is 动态组件 -->
  <component 
    :is="currentComponent"
    v-bind="currentProps"
    @custom-event="handleEvent"
  />
</template>

<script>
import TextEditor from './TextEditor.vue'
import ImageUploader from './ImageUploader.vue'
import VideoPlayer from './VideoPlayer.vue'

export default {
  data() {
    return {
      componentType: 'text',
      content: ''
    }
  },
  computed: {
    currentComponent() {
      const components = {
        text: TextEditor,
        image: ImageUploader,
        video: VideoPlayer
      }
      return components[this.componentType]
    },
    currentProps() {
      // 根据组件类型传递不同的 props
      const baseProps = { content: this.content }
      
      if (this.componentType === 'image') {
        return { ...baseProps, maxSize: '5MB' }
      }
      
      return baseProps
    }
  }
}
</script>

三、进阶技巧:混合模式与优化

1. 模板与渲染函数结合

<template>
  <!-- 使用模板定义主体结构 -->
  <div class="data-table">
    <table-header :columns="columns" />
    <table-body :render-row="renderTableRow" />
  </div>
</template>

<script>
export default {
  methods: {
    // 使用渲染函数处理复杂行渲染
    renderTableRow(h, row) {
      return h('tr', 
        this.columns.map(column => 
          h('td', {
            class: column.className,
            style: column.style
          }, column.formatter ? column.formatter(row) : row[column.key])
        )
      )
    }
  }
}
</script>

2. 高阶组件模式

// 高阶组件:增强模板功能
function withLoading(WrappedComponent) {
  return {
    render(h) {
      const directives = [
        {
          name: 'loading',
          value: this.isLoading,
          expression: 'isLoading'
        }
      ]
      
      return h('div', { directives }, [
        h(WrappedComponent, {
          props: this.$attrs,
          on: this.$listeners
        }),
        this.isLoading && h(LoadingSpinner)
      ])
    },
    data() {
      return { isLoading: false }
    },
    mounted() {
      // 加载逻辑
    }
  }
}

3. SSR 优化策略

// 服务端渲染友好的模板
export default {
  // 客户端激活所需
  mounted() {
    // 仅客户端的 DOM 操作
    if (process.client) {
      this.initializeThirdPartyLibrary()
    }
  },
  
  // 服务端渲染优化
  serverPrefetch() {
    // 预取数据
    return this.fetchData()
  },
  
  // 避免客户端 hydration 不匹配
  template: `
    <div>
      <!-- 避免使用随机值 -->
      <p>服务器时间: {{ serverTime }}</p>
      
      <!-- 避免使用 Date.now() 等 -->
      <!-- 服务端和客户端要一致 -->
    </div>
  `
}

四、选择指南:如何决定使用哪种方式?

场景 推荐方式 理由
生产级应用 单文件组件(SFC) 最佳开发体验、工具链支持、可维护性
UI 组件库 SFC + 渲染函数 SFC 提供开发体验,渲染函数处理动态性
高度动态 UI 渲染函数/JSX 完全的 JavaScript 控制力
React 团队迁移 JSX 降低学习成本
原型/演示 字符串模板 快速、简单
遗留项目 X-Templates 渐进式迁移
服务端渲染 SFC(注意 hydration) 良好的 SSR 支持

决策流程图:

graph TD
    A[开始选择模板方式] --> B{需要构建工具?}
    B -->|是| C{组件动态性强?}
    B -->|否| D[使用字符串模板或X-Templates]
    
    C -->|是| E{团队熟悉JSX?}
    C -->|否| F[使用单文件组件SFC]
    
    E -->|是| G[使用JSX]
    E -->|否| H[使用渲染函数]
    
    D --> I[完成选择]
    F --> I
    G --> I
    H --> I

五、性能与最佳实践

1. 编译时 vs 运行时模板

// Vue CLI 默认配置优化了 SFC
module.exports = {
  productionSourceMap: false, // 生产环境不生成 source map
  runtimeCompiler: false, // 不使用运行时编译器,减小包体积
}

2. 模板预编译

// 手动预编译模板
const { compile } = require('vue-template-compiler')

const template = `<div>{{ message }}</div>`
const compiled = compile(template)

console.log(compiled.render)
// 输出渲染函数,可直接在组件中使用

3. 避免的常见反模式

<!-- ❌ 避免在模板中使用复杂表达式 -->
<template>
  <div>
    <!-- 反模式:复杂逻辑在模板中 -->
    <p>{{ user.firstName + ' ' + user.lastName + ' (' + user.age + ')' }}</p>
    
    <!-- 正确:使用计算属性 -->
    <p>{{ fullNameWithAge }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    fullNameWithAge() {
      return `${this.user.firstName} ${this.user.lastName} (${this.user.age})`
    }
  }
}
</script>

六、Vue 3 的新变化

<!-- Vue 3 组合式 API + SFC -->
<template>
  <!-- 支持多个根节点(Fragment) -->
  <header>{{ title }}</header>
  <main>{{ content }}</main>
  <footer>{{ footerText }}</footer>
</template>

<script setup>
// 更简洁的语法
import { ref, computed } from 'vue'

const title = ref('Vue 3 组件')
const content = ref('新特性介绍')

const footerText = computed(() => `© ${new Date().getFullYear()}`)
</script>

总结

Vue 提供了从声明式到命令式的完整模板方案光谱:

  1. 声明式端:SFC 模板 → 易读易写,适合大多数业务组件
  2. 命令式端:渲染函数/JSX → 完全控制,适合高阶组件和库
  3. 灵活选择:根据项目需求和团队偏好选择合适的方式

记住这些关键原则:

  • 默认使用 SFC,除非有特殊需求
  • 保持一致性,一个项目中不要混用太多模式
  • 性能考量:生产环境避免运行时编译
  • 团队协作:选择团队最熟悉的方式

深入理解 Vue 生命周期:created 与 mounted 的核心差异与实战指南

深入理解 Vue 生命周期:created 与 mounted 的核心差异与实战指南

掌握生命周期钩子,是 Vue 开发从入门到精通的关键一步。今天我们来深度剖析两个最容易混淆的钩子:createdmounted

一、生命周期全景图:先看森林,再见树木

在深入细节之前,让我们先回顾 Vue 实例的完整生命周期:

graph TD
    A[new Vue()] --> B[Init Events & Lifecycle]
    B --> C[beforeCreate]
    C --> D[Init Injections & Reactivity]
    D --> E[created]
    E --> F[Compile Template]
    F --> G[beforeMount]
    G --> H[Create vm.$el]
    H --> I[mounted]
    I --> J[Data Changes]
    J --> K[beforeUpdate]
    K --> L[Virtual DOM Re-render]
    L --> M[updated]
    M --> N[beforeDestroy]
    N --> O[Teardown]
    O --> P[destroyed]

理解这张图,你就掌握了 Vue 组件从出生到消亡的完整轨迹。而今天的主角——createdmounted,正是这个旅程中两个关键的里程碑。

二、核心对比:created vs mounted

让我们通过一个表格直观对比:

特性 created mounted
执行时机 数据观测/方法/计算属性初始化后,模板编译前 模板编译完成,DOM 挂载到页面后
DOM 可访问性 ❌ 无法访问 DOM ✅ 可以访问 DOM
$el 状态 undefined 已挂载的 DOM 元素
主要用途 数据初始化、API 调用、事件监听 DOM 操作、第三方库初始化
SSR 支持 ✅ 在服务端和客户端都会执行 ❌ 仅在客户端执行

三、实战代码解析:从理论到实践

场景 1:API 数据获取的正确姿势

export default {
  data() {
    return {
      userData: null,
      loading: true
    }
  },
  
  async created() {
    // ✅ 最佳实践:在 created 中发起数据请求
    // 此时数据观测已就绪,可以设置响应式数据
    try {
      this.userData = await fetchUserData()
    } catch (error) {
      console.error('数据获取失败:', error)
    } finally {
      this.loading = false
    }
    
    // ❌ 这里访问 DOM 会失败
    // console.log(this.$el) // undefined
  },
  
  mounted() {
    // ✅ DOM 已就绪,可以执行依赖 DOM 的操作
    const userCard = document.getElementById('user-card')
    if (userCard) {
      // 使用第三方图表库渲染数据
      this.renderChart(userCard, this.userData)
    }
    
    // ✅ 初始化需要 DOM 的第三方插件
    this.initCarousel('.carousel-container')
  }
}

关键洞察:数据获取应尽早开始(created),DOM 相关操作必须等待 mounted。

场景 2:计算属性与 DOM 的微妙关系

<template>
  <div ref="container">
    <p>容器宽度: {{ containerWidth }}px</p>
    <div class="content">
      <!-- 动态内容 -->
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    }
  },
  
  computed: {
    // ❌ 错误示例:在 created 阶段访问 $refs
    containerWidth() {
      // created 阶段:this.$refs.container 是 undefined
      // mounted 阶段:可以正常访问
      return this.$refs.container?.offsetWidth || 0
    }
  },
  
  created() {
    // ✅ 安全操作:初始化数据
    this.items = this.generateItems()
    
    // ⚠️ 注意:computed 属性在此阶段可能基于错误的前提计算
    console.log('created 阶段宽度:', this.containerWidth) // 0
  },
  
  mounted() {
    console.log('mounted 阶段宽度:', this.containerWidth) // 实际宽度
    
    // ✅ 正确的 DOM 相关初始化
    this.observeResize()
  },
  
  methods: {
    observeResize() {
      // 使用 ResizeObserver 监听容器大小变化
      const observer = new ResizeObserver(entries => {
        this.handleResize(entries[0].contentRect.width)
      })
      observer.observe(this.$refs.container)
    }
  }
}
</script>

四、性能优化:理解渲染流程避免常见陷阱

1. 避免在 created 中执行阻塞操作

export default {
  created() {
    // ⚠️ 潜在的渲染阻塞
    this.processLargeData(this.rawData) // 如果处理时间过长,会延迟首次渲染
    
    // ✅ 优化方案:使用 Web Worker 或分块处理
    this.asyncProcessData()
  },
  
  async asyncProcessData() {
    // 使用 requestIdleCallback 避免阻塞主线程
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        this.processInBackground()
      })
    } else {
      // 回退方案:setTimeout 让出主线程
      setTimeout(() => this.processInBackground(), 0)
    }
  }
}

2. 理解异步更新队列

export default {
  mounted() {
    // 情景 1:直接修改数据
    this.someData = 'new value'
    console.log(this.$el.textContent) // ❌ 可能还是旧值
    
    // 情景 2:使用 $nextTick
    this.someData = 'new value'
    this.$nextTick(() => {
      console.log(this.$el.textContent) // ✅ 更新后的值
    })
    
    // 情景 3:多个数据变更
    this.data1 = 'new1'
    this.data2 = 'new2'
    this.data3 = 'new3'
    
    // Vue 会批量处理,只触发一次更新
    this.$nextTick(() => {
      // 所有变更都已反映到 DOM
    })
  }
}

五、高级应用:SSR 场景下的特殊考量

export default {
  // created 在服务端和客户端都会执行
  async created() {
    // 服务端渲染时,无法访问 window、document 等浏览器 API
    if (process.client) {
      // 客户端特定逻辑
      this.screenWidth = window.innerWidth
    }
    
    // 数据预取(Universal)
    await this.fetchUniversalData()
  },
  
  // mounted 只在客户端执行
  mounted() {
    // 安全的浏览器 API 使用
    this.initializeBrowserOnlyLibrary()
    
    // 处理客户端 hydration
    this.handleHydrationEffects()
  },
  
  // 兼容 SSR 的数据获取模式
  async fetchUniversalData() {
    // 避免重复获取数据
    if (this.$ssrContext && this.$ssrContext.data) {
      // 服务端已获取数据
      Object.assign(this, this.$ssrContext.data)
    } else {
      // 客户端获取数据
      const data = await this.$axios.get('/api/data')
      Object.assign(this, data)
    }
  }
}

六、实战技巧:常见问题与解决方案

Q1:应该在哪个钩子初始化第三方库?

export default {
  mounted() {
    // ✅ 大多数 UI 库需要 DOM 存在
    this.$nextTick(() => {
      // 确保 DOM 完全渲染
      this.initSelect2('#my-select')
      this.initDatepicker('.date-input')
    })
  },
  
  beforeDestroy() {
    // 记得清理,防止内存泄漏
    this.destroySelect2()
    this.destroyDatepicker()
  }
}

Q2:如何处理动态组件?

<template>
  <component :is="currentComponent" ref="dynamicComponent" />
</template>

<script>
export default {
  data() {
    return {
      currentComponent: 'ComponentA'
    }
  },
  
  watch: {
    currentComponent(newVal, oldVal) {
      // 组件切换时,新的 mounted 会在下次更新后执行
      this.$nextTick(() => {
        console.log('新组件已挂载:', this.$refs.dynamicComponent)
      })
    }
  },
  
  mounted() {
    // 初次挂载
    this.initializeCurrentComponent()
  }
}
</script>

七、最佳实践总结

  1. 数据初始化 → 优先选择 created
  2. DOM 操作 → 必须使用 mounted(配合 $nextTick 确保渲染完成)
  3. 第三方库初始化mounted + beforeDestroy 清理
  4. 性能敏感操作 → 考虑使用 requestIdleCallback 或 Web Worker
  5. SSR 应用 → 注意浏览器 API 的兼容性检查

写在最后

理解 createdmounted 的区别,本质上是理解 Vue 的渲染流程。记住这个核心原则:

created 是关于数据的准备,mounted 是关于视图的准备。

随着 Vue 3 Composition API 的普及,生命周期有了新的使用方式,但底层原理依然相通。掌握这些基础知识,能帮助你在各种场景下做出更合适的架构决策。

Vuex日渐式微?状态管理的三大痛点与新时代方案

作为Vue生态曾经的“官方标配”,Vuex在无数项目中立下汗马功劳。但近年来,随着Vue 3和Composition API的崛起,越来越多的开发者开始重新审视这个老牌状态管理库。

Vuex的设计初衷:解决组件通信难题

回想Vue 2时代,当我们的应用从简单的单页面逐渐演变成复杂的中大型应用时,组件间的数据共享成为了一大痛点。

// 经典的Vuex store结构
const store = new Vuex.Store({
  state: {
    count0,
    usernull
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    async fetchUser({ commit }) {
      const user = await api.getUser()
      commit('SET_USER', user)
    }
  },
  getters: {
    doubleCountstate => state.count * 2
  }
})

这种集中式的状态管理模式,确实在当时解决了:

  • • 多个组件共享同一状态的问题
  • • 状态变更的可追溯性
  • • 开发工具的时间旅行调试

痛点浮现:Vuex的三大“时代局限”

1. 样板代码过多,开发体验繁琐

这是Vuex最常被诟病的问题。一个简单的状态更新,需要经过actionmutationstate的完整流程:

// 定义部分
const actions = {
  updateUser({ commit }, user) {
    commit('SET_USER', user)
  }
}

const mutations = {
  SET_USER(state, user) {
    state.user = user
  }
}

// 使用部分
this.$store.dispatch('updateUser', newUser)

相比之下,直接的状态赋值只需要一行代码。在中小型项目中,这种复杂度常常显得“杀鸡用牛刀”。

2. TypeScript支持不友好

虽然Vuex 4改进了TS支持,但其基于字符串的dispatchcommit调用方式,始终难以获得完美的类型推断:

// 类型安全较弱
store.commit('SET_USER', user) // 'SET_USER'字符串无类型检查

// 需要额外定义类型
interface User {
  idnumber
  namestring
}

// 但定义和使用仍是分离的

3. 模块系统复杂,代码组织困难

随着项目增大,Vuex的模块系统(namespaced modules)带来了新的复杂度:

// 访问模块中的状态需要命名空间前缀
computed: {
  ...mapState({
    userstate => state.moduleA.user
  })
}

// 派发action也需要前缀
this.$store.dispatch('moduleA/fetchData')

动态注册模块、模块间的依赖关系处理等问题,让代码维护成本逐渐升高。

新时代的解决方案:更轻量、更灵活的选择

方案一:Composition API + Provide/Inject

Vue 3的Composition API为状态管理提供了全新思路:

// 使用Composition API创建响应式store
export function useUserStore() {
  const user = ref<User | null>(null)
  
  const setUser = (newUser: User) => {
    user.value = newUser
  }
  
  return {
    user: readonly(user),
    setUser
  }
}

// 在组件中使用
const { user, setUser } = useUserStore()

优点

  • • 零依赖、零学习成本
  • • 完美的TypeScript支持
  • • 按需导入,Tree-shaking友好

方案二:Pinia——Vuex的现代继承者

Pinia被看作是“下一代Vuex”,解决了Vuex的许多痛点:

// 定义store
export const useUserStore = defineStore('user', {
  state() => ({
    usernull as User | null,
  }),
  actions: {
    async fetchUser() {
      this.user = await api.getUser()
    },
  },
})

// 使用store
const userStore = useUserStore()
userStore.fetchUser()

Pinia的进步

  • • 移除mutations,actions可直接修改状态
  • • 完整的TypeScript支持
  • • 更简洁的API设计
  • • 支持Composition API和Options API

实战建议:如何选择?

根据我的项目经验,建议如下:

继续使用Vuex的情况

  • • 维护已有的Vue 2大型项目
  • • 团队已深度熟悉Vuex,且项目运行稳定
  • • 需要利用Vuex DevTools的特定功能

考虑迁移/使用新方案的情况

  • • 新项目:优先考虑Pinia
  • • Vue 3项目:中小型可用Composition API,大型推荐Pinia
  • • 对TypeScript要求高:直接选择Pinia

迁移策略:平稳过渡

如果你决定从Vuex迁移到Pinia,可以采取渐进式策略:

  1. 1. 并行运行:新旧store系统共存
  2. 2. 模块逐个迁移:按业务模块逐步迁移
  3. 3. 工具辅助:利用官方迁移指南和工具
// 迁移示例:将Vuex模块转为Pinia store
// Vuex版本
const userModule = {
  state: { name'' },
  mutations: { SET_NAME(state, name) { state.name = name } }
}

// Pinia版本
const useUserStore = defineStore('user', {
  state() => ({ name'' }),
  actions: {
    setName(name: string) {
      this.name = name
    }
  }
})

写在最后

技术总是在不断演进。Vuex作为特定历史阶段的优秀解决方案,完成了它的使命。而今天,我们有更多、更好的选择。

核心不是追求最新技术,而是为项目选择最合适的工具。

对于大多数新项目,Pinia无疑是更现代、更优雅的选择。但对于已有的Vuex项目,除非有明确的痛点需要解决,否则“稳定压倒一切”。

本周将裁员1000人?花旗回应

有消息称,花旗集团本周将裁员1000人左右,这是首席执行官范洁恩(Jane Fraser)为控制成本而采取的举措之一。花旗方面回应称:“我们将在2026年继续缩减人力规模。此番调整,既为确保人员配置、办公选址及专业技能与当前业务需求精准适配,也得益于技术赋能带来的效率提升,并且印证了我们转型工作的阶段性成果,目前已临近目标状态。” (每经网)

从“死了么”到“活着记”:用Gmeek在数字世界留下思想印记

本文从近期热议的“死了么”App入手,探讨现代人对数字安全与思想存续的双重需求,详细介绍基于GitHub的极简博客框架Gmeek,阐述在数字时代通过博客记录思想、对抗遗忘的重要意义,鼓励读者建立个人数字思想家园。

github_gmeek.png

github_gmeek.png

引言

一款名为“死了么”的App近期引发广泛讨论。它以直白甚至略显生硬的名字,精准地切中了当代社会一个真实且规模庞大的群体需求。

可以说,“死了么”的火爆,是产品创意、社会情绪与网络传播共同作用的结果。它如同一面棱镜,折射出当代独居生活的潜在隐忧。

如今,独居者人数众多。他们往往独自奋斗,习惯“一个人扛下所有”,最担心的莫过于在突发疾病或意外时无人知晓。这款App之所以能够走红,恰恰在于它敏锐地捕捉到了现代人一种微妙而普遍的心理——“不愿日常打扰他人,却渴望在异常状况下被关注”。用户无需复杂的社交,仅通过简单的每日打卡,就能为自己构筑一道最低成本的“安全防线”。其付费下载量的快速增长,也印证了这一需求真实而强烈。

然而,人终有一死。那么,我们该如何留下生活过的痕迹?如何在日常中安顿内心、与自己对话?活着,不仅仅意味着没有死去,而是要有思想、有记录、有回响地活着。博客,作为一种数字化的表达方式,正成为越来越多人记录自我、分享见解、沉淀思想的平台。

最近读到一篇文章,颇受启发。People Die, but Long Live GitHub

people-die-but-long-live-github.png

people-die-but-long-live-github.png

最近,我还在 GitHub 上发现了一个开源项目——Gmeek。它是一个超轻量级的个人博客框架,完全基于 GitHub Pages、GitHub Issues 与 GitHub Actions 构建,可谓“All in GitHub”。无需本地部署,从搭建到写作,整个过程只需三步:前两步用 18 秒完成博客搭建,第三步即可开始书写。今天我们就来介绍下如何使用这款开源项目构建个人github博客。

Gmeek:三步构建你的数字思想家园

一个博客框架,超轻量级个人博客模板。完全基于Github PagesGithub IssuesGithub Actions。不需要本地部署,从搭建到写作,只需要18秒,2步搭建好博客,第3步就是写作。

github地址:github.com/Meekdai/Gme…

文档博客:blog.meekdai.com/tag.html#Gm…

gmeek_star.png

gmeek_star.png

gmeek_doc_blog.png

gmeek_doc_blog.png

快速开始

  1. 【创建仓库】点击通过模板创建仓库,建议仓库名称为XXX.github.io,其中XXX为你的github用户名。

xiuji_github_blog.png

xiuji_github_blog.png

  1. 【启用Pages】在仓库的SettingsPages->Build and deployment->Source下面选择Github Actions
  2. 【开始写作】打开一篇issue,开始写作,并且必须添加一个标签Label(至少添加一个),再保存issue后会自动创建博客内容,片刻后可通过XXX.github.io 访问(可进入Actions页面查看构建进度)。
  3. 【手动全局生成】这个步骤只有在修改config.json文件或者出现奇怪问题的时候,需要执行。
通过Actions->build Gmeek->Run workflow->里面的按钮全局重新生成一次

[!NOTE] issue必须添加一个标签Label(至少添加一个)

到此,提交完issue之后Actions页面构建完成之后就可以看到我们的博客了。

博主的github博客地址:xiuji008.github.io/

配置及使用

config.json 文件就是配置文件,在创建的仓库内可以找到,对应修改为自己的即可。

配置可参考Gmeek作者博文:blog.meekdai.com/post/Gmeek-…

static文件夹使用

  1. 在自己的仓库根目录下新建一个文件夹,名称必须是static。
  2. 然后在static文件内上传一些自己的文件,比如博客图片、插件js等。
  3. 通过手动全局生成一次成功后,你就可以通过 xxx.github.io/your.png 访问了

插件功能的使用

为了使得Gmeek的功能更加的丰富,Gmeek作者添加了插件的功能,目前已经有几个插件可以使用。大家可以直接复制文章中的配置代码使用,也可以把对应的插件文件拷贝到自己的static文件夹下使用。

计数工具 Vercount

  1. 全站添加计数工具Vercount,只需要在config.json文件内添加配置
"allHead":"<script src='https://blog.meekdai.com/Gmeek/plugins/GmeekVercount.js'></script>",

2. 单个文章页添加Vercount,只需要在文章最后一行添加如下

<!-- ##{"script":"<script src='https://blog.meekdai.com/Gmeek/plugins/GmeekVercount.js'></script>"}## -->

gmeek_plugin_vercount.png

gmeek_plugin_vercount.png

TOC目录

  1. 所有文章页添加TOC目录,只需要在config.json文件内添加配置
"script":"<script src='https://blog.meekdai.com/Gmeek/plugins/GmeekTOC.js'></script>",

2. 单个文章页添加TOC目录,只需要在文章最后一行添加如下

<!-- ##{"script":"<script src='https://blog.meekdai.com/Gmeek/plugins/GmeekTOC.js'></script>"}## -->

gmeek_plugins_toc.png

gmeek_plugins_toc.png

灯箱插件

[!TIP] 此插件由Tiengming编写,可以放大浏览文章中的图片,适合一些图片较多的文章。

  1. 所有文章页添加lightbox,只需要在config.json文件内添加配置
"script":"<script src='https://blog.meekdai.com/Gmeek/plugins/lightbox.js'></script>",

2. 单个文章页添加lightbox,只需要在文章最后一行添加如下

<!-- ##{"script":"<script src='https://blog.meekdai.com/Gmeek/plugins/lightbox.js'></script>"}## -->

看板娘(花里胡哨)

[!TIP] 此插件从github开源项目live2d-widget引入,纯属页面展示

  1. 所有文章页添加lightbox,只需要在config.json文件内添加配置
"script":"<script src='https://fastly.jsdelivr.net/npm/live2d-widgets@1.0.0/dist/autoload.js'></script>",

gmeek_plugins_live2d-widget.png

gmeek_plugins_live2d-widget.png

对看板娘项目感兴趣的伙伴也可以研究下

看板娘项目github地址:github.com/stevenjoezh…

github_live2d_widget.png

github_live2d_widget.png

多插件使用

同时在所有文章页使用TOC目录、灯箱插件及其它插件,需要这样添加配置文件:

    "allHead":"<script src='https://xiuji008.github.io/plugins/gmeekVercount.js'></script><script src='https://xiuji008.github.io/plugins/lightbox.js'></script><script src='https://xiuji008.github.io/plugins/gmeekTOC.js'></script><script src='https://fastly.jsdelivr.net/npm/live2d-widgets@1.0.0/dist/autoload.js'></script>",

其它使用说明

issue添加中文标签

  1. 点击 issue页签, 点击右侧 Labels 后边的设置按钮,点击Edit labels

issues_labels_setting.png

issues_labels_setting.png

  1. Labels 页面则可以新增或修改标签

issues_labels_edit.png

issues_labels_edit.png

置顶博客文章

只需要Pin issue后,手动全局生成一次即可。

issues_pin.png

issues_pin.png

评论 utteranc报错

如果在评论里面登录后评论报错,可直接按照提示安装utteranc app即可

Error: utterances is not installed on xxx/xxx.github.io. If you own this repo, install the app. Read more about this change in the PR.

删除文章

只需要Close issue或者Delete issue后,再手动全局生成一次即可。

结语:在数字时代留下有温度的痕迹

“死了么”关注的是物理存在的安全,而Gmeek这样的工具关注的是思想存在的延续。两者看似无关,实则都回应了现代人对存在感的深层渴望。

在这个算法主导、注意力碎片化的时代,拥有一个属于自己的数字角落,定期记录、整理、输出,不仅是对抗遗忘的方式,更是一种积极的生活态度——主动塑造自己的数字身份,而非被动地被平台定义。

从担心“无人知晓的离去”到主动“留下有思想的痕迹”,或许正是数字时代给予我们的一种平衡:既通过工具获得安全感,也通过表达实现自我确认。

你的思想值得被记录,你的声音值得被听见。现在,只需18秒,就可以开始在GitHub上建造你的数字思想家园。

HarmonyOS 多模块项目中的公共库治理与最佳实践

鸿蒙(HarmonyOS)多模块项目 中,如果你希望 避免在每个模块(Module)中重复集成同一个三方库或公共库,可以将该库 提升到项目级别(Project-level)进行统一管理。以下是标准做法,适用于 Stage 模型 + ArkTS + DevEco Studio 的工程结构。


✅ 目标

将公共库(如 @ohos/utils、自研工具库、第三方 npm 包等)只声明一次,供多个模块(entry、feature、service 等)共享使用


📁 鸿蒙项目结构回顾

MyHarmonyProject/
├── build-profile.json5        ← 项目级构建配置
├── oh-package.json5           ← 项目级依赖(关键!)
├── modules/
│   ├── entry/                 ← 主模块
│   ├── feature_news/          ← 功能模块1
│   └── feature_ebook/         ← 功能模块2
└── libs/                      ← (可选)本地 aar/har 公共库

✅ 正确做法:在 项目根目录的 oh-package.json5 中声明依赖

步骤 1:在项目根目录的 oh-package.json5 中添加依赖

{
  "devDependencies": {
    // 开发依赖(如 types)
  },
  "dependencies": {
    // 👇 把公共库放在这里(项目级)
    "@ohos/utils": "1.0.0",
    "some-third-party-lib": "^2.3.0"
  }
}

✅ 这样,所有子模块都可以继承使用这些依赖,无需在每个 module/xxx/oh-package.json5 中重复声明。


步骤 2:删除各子模块中的重复依赖

确保 modules/entry/oh-package.json5modules/feature_news/oh-package.json5不再包含 已提升到项目级的依赖。

例如,不要entry/oh-package.json5 中再写:

{
  "dependencies": {
    "@ohos/utils": "1.0.0"  // ❌ 删除这行!
  }
}

步骤 3:在子模块代码中正常 import 使用

// 在 entry 或 feature_news 模块中
import { ZGJYBAppearanceColorUtil } from '@ohos/utils';

// ✅ 可以正常使用,因为依赖已由项目级提供

⚠️ 注意事项

1. 仅适用于 npm 类型的包(通过 ohpm 安装)

  • 如果你是通过 ohpm install @ohos/utils 安装的库,它会被记录在 oh-package.json5
  • 这种方式支持 依赖提升(hoisting) ,类似 npm/yarn 的 workspace。

2. 本地 .har.hap 库不能这样共享

  • 如果你的“库”是一个 本地开发的 .har(HarmonyOS Archive)模块,则需要:

    • 将其放在 libs/ 目录下;
    • 每个需要使用的模块module.json5 中声明 deps 引用;
    • 或者将其发布为私有 ohpm 包,再通过 oh-package.json5 引入。

示例:引用本地 har(仍需逐模块配置)

// modules/entry/module.json5
{
  "deps": [
    "../libs/my-common-utils.har"
  ]
}

❌ 这种情况无法完全避免重复声明,但你可以通过脚本或模板减少工作量。


3. 确保 DevEco Studio 同步了依赖

  • 修改 oh-package.json5 后,点击 “Sync Now” 或运行:

    ohpm install
    

    在项目根目录执行,会安装所有模块共享的依赖。


✅ 最佳实践总结

场景 推荐方案
公共 npm/ohpm 库(如 @ohos/utils ✅ 在 项目根目录 oh-package.json5 中声明一次
自研公共逻辑(TS 工具函数) ✅ 创建一个 shared 模块,发布为 ohpm 私有包,再在项目级引入
本地 .har ⚠️ 需在每个模块的 module.json5 中引用,但可统一放在 libs/ 目录管理
避免重复代码 ✅ 抽象公共组件/工具到独立模块,通过依赖注入使用

🔧 附加建议:创建 shared 模块(高级)

  1. 新建模块:File > New > Module > Static Library (HAR)

    • 命名为 shared
  2. 在其中放置公共工具类、常量、网络封装等

  3. shared/oh-package.json5 中定义包名:

    { "name": "@myorg/shared", "version": "1.0.0" }
    
  4. 在项目根目录运行:

    ohpm install ./modules/shared --save
    
  5. 然后在 oh-package.json5 中就会出现:

    "dependencies": {
      "@myorg/shared": "file:./modules/shared"
    }
    
  6. 所有模块即可通过 import { xxx } from '@myorg/shared' 使用。

✅ 这是最接近“项目级公共库”的鸿蒙官方推荐方案。


✅ 结论

把公共库写在项目根目录的 oh-package.json5dependencies 中,即可实现“一次集成,多模块共享”

只要你的库是通过 ohpm 管理的(包括本地 file: 引用),就支持这种共享机制。这是 HarmonyOS 多模块项目的标准依赖管理方式。

中国黄金:2025年净利同比预降55%-65%

36氪获悉,中国黄金发布2025年业绩预告。报告显示,中国黄金预计2025年归属于母公司所有者的净利润为2.86亿元-3.68亿元,同比减少55.00%到65.00%。扣除非经常性损益后的净利润为2.46亿元-3.28亿元,同比减少58.44%至68.80%。业绩下滑主要因黄金市场波动及新政影响,投资类与消费类黄金产品销售承压,门店客流下降,叠加金价上涨快于存货周转,导致公允价值变动损益对利润产生负向影响。

达美航空订购多达60架波音787梦幻客机

达美航空订购多达60架波音787梦幻客机,以扩大并现代化宽体机队。最新采购使达美的订单库达到130架波音飞机,以建设未来机队。787-10可容纳最多336名乘客,燃油消耗比所替代飞机低25%,是所有宽体机中每座位运营成本最低的。(财联社)

美股大型科技股盘前涨跌不一,Arm跌超2%

36氪获悉,美股大型科技股盘前涨跌不一,截至发稿,Arm跌超2%,苹果跌0.42%,微软跌0.35%,亚马逊跌0.13%,英伟达跌0.01%;谷歌涨近1%,特斯拉涨0.16%,Meta、奈飞涨0.01%。

锋龙股份:股票交易异常波动,将停牌核查

36氪获悉,锋龙股份公告,公司股票自2025年12月25日至2026年1月13日已连续12个交易日涨停,价格涨幅为213.97%,已严重背离公司基本面。为维护投资者利益,公司将就股票交易波动情况进行停牌核查,预计停牌时间不超过3个交易日。公司提醒广大投资者注意二级市场交易风险。

Vue插槽

一、先明确核心概念

  1. 具名插槽:给 <slot> 标签添加 name 属性,用于区分不同位置的插槽,让父组件可以精准地将内容插入到子组件的指定位置,解决「默认插槽只能插入一处内容」的问题。
  2. 默认插槽:没有 name 属性的 <slot>,是具名插槽的特殊形式(默认名称为 default),父组件中未指定插槽名称的内容,会默认插入到这里。
  3. 插槽默认内容:在子组件的 <slot> 标签内部写入内容,当父组件未给该插槽传递任何内容时,会显示这份默认内容;若父组件传递了内容,会覆盖默认内容,提升组件的复用性和容错性。
  4. 作用域插槽:子组件通过「属性绑定」的方式给 <slot> 传递内部私有数据,父组件在使用插槽时可以接收这些数据并自定义渲染,解决「父组件无法访问子组件内部数据」的问题,实现「子组件供数、父组件定制渲染」。

二、分步实例演示

第一步:实现最基础的「具名插槽 + 默认插槽」

核心需求:创建一个通用的「页面容器组件」,包含「页头」「页面内容」「页脚」三个部分,其中「页面内容」用默认插槽,「页头」「页脚」用具名插槽。

1. 子组件:定义插槽(文件名:PageContainer.vue

<template>
  <!-- 通用页面容器样式(简单美化,方便查看效果) -->
  <div class="page-container" style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin: 20px 0;">
    <!-- 具名插槽:页头(name="header") -->
    <div class="page-header" style="border-bottom: 1px dashed #e0e0e0; padding-bottom: 10px; margin-bottom: 10px;">
      <slot name="header" />
    </div>

    <!-- 默认插槽:页面核心内容(无name属性,对应default) -->
    <div class="page-content" style="margin: 20px 0; min-height: 100px;">
      <slot />
    </div>

    <!-- 具名插槽:页脚(name="footer") -->
    <div class="page-footer" style="border-top: 1px dashed #e0e0e0; padding-top: 10px; margin-top: 10px; text-align: right;">
      <slot name="footer" />
    </div>
  </div>
</template>

<script setup>
// 子组件无需额外逻辑,仅定义插槽结构即可
</script>

2. 父组件:使用插槽(传递内容,文件名:App.vue

父组件通过 v-slot:插槽名(简写:#插槽名)指定内容要插入的具名插槽,未指定的内容默认插入到默认插槽。

<template>
  <h2>基础具名插槽 + 默认插槽演示</h2>

  <!-- 使用子组件 PageContainer -->
  <PageContainer>
    <!-- 给具名插槽 header 传递内容(简写 #header,完整写法 v-slot:header) -->
    <template #header>
      <h3>这是文章详情页的页头</h3>
      <nav>首页 > 文章 > Vue 插槽教程</nav>
    </template>

    <!-- 未指定插槽名,默认插入到子组件的默认插槽 -->
    <div>
      <p>1. 具名插槽可以让父组件精准控制内容插入位置。</p>
      <p>2. 默认插槽用于承载组件的核心内容,使用更简洁。</p>
      <p>3. 这部分内容会显示在页头和页脚之间。</p>
    </div>

    <!-- 给具名插槽 footer 传递内容(简写 #footer) -->
    <template #footer>
      <span>发布时间:2026-01-13</span>
      <button style="margin-left: 20px; padding: 4px 12px;">收藏文章</button>
    </template>
  </PageContainer>
</template>

<script setup>
// 导入子组件
import PageContainer from './PageContainer.vue';
</script>

3. 运行效果与说明

  • 页头区域显示「文章详情页标题 + 面包屑导航」(对应 #header 插槽内容)。
  • 中间区域显示核心正文(对应默认插槽内容)。
  • 页脚区域显示「发布时间 + 收藏按钮」(对应 #footer 插槽内容)。
  • 关键:父组件的 <template> 标签包裹插槽内容,通过 #插槽名 绑定子组件的具名插槽,结构清晰,互不干扰。

第二步:实现「带默认内容的插槽」

核心需求:优化上面的 PageContainer.vue,给「页脚插槽」添加默认内容(默认显示「返回顶部」按钮),当父组件未给 footer 插槽传递内容时,显示默认按钮;若传递了内容,覆盖默认内容。

1. 修改子组件:给插槽添加默认内容(PageContainer.vue

仅修改 footer 插槽部分,在 <slot name="footer"> 内部写入默认内容:

<template>
  <div class="page-container" style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin: 20px 0;">
    <!-- 具名插槽:页头 -->
    <div class="page-header" style="border-bottom: 1px dashed #e0e0e0; padding-bottom: 10px; margin-bottom: 10px;">
      <slot name="header" />
    </div>

    <!-- 默认插槽:页面核心内容 -->
    <div class="page-content" style="margin: 20px 0; min-height: 100px;">
      <slot />
    </div>

    <!-- 具名插槽:页脚(带默认内容) -->
    <div class="page-footer" style="border-top: 1px dashed #e0e0e0; padding-top: 10px; margin-top: 10px; text-align: right;">
      <slot name="footer">
        <!-- 插槽默认内容:父组件未传递footer内容时,显示该按钮 -->
        <button style="padding: 4px 12px;" @click="backToTop">返回顶部</button>
      </slot>
    </div>
  </div>
</template>

<script setup>
// 定义默认内容的点击事件(返回顶部)
const backToTop = () => {
  window.scrollTo({
    top: 0,
    behavior: 'smooth' // 平滑滚动
  });
};
</script>

2. 父组件演示两种场景(App.vue

分别演示「不传递 footer 内容」和「传递 footer 内容」的效果:

<template>
  <h2>带默认内容的插槽演示</h2>

  <!-- 场景1:父组件不传递 footer 插槽内容,显示子组件的默认「返回顶部」按钮 -->
  <h4>场景1:未传递页脚内容(显示默认按钮)</h4>
  <PageContainer>
    <template #header>
      <h3>这是未传递页脚的页面</h3>
    </template>
    <p>该页面父组件没有给 footer 插槽传递内容,所以页脚会显示子组件默认的「返回顶部」按钮。</p>
  </PageContainer>

  <!-- 场景2:父组件传递 footer 插槽内容,覆盖默认按钮 -->
  <h4 style="margin-top: 40px;">场景2:传递页脚内容(覆盖默认按钮)</h4>
  <PageContainer>
    <template #header>
      <h3>这是传递了页脚的页面</h3>
    </template>
    <p>该页面父组件给 footer 插槽传递了自定义内容,会覆盖子组件的默认「返回顶部」按钮。</p>
    <template #footer>
      <span>作者:Vue 小白教程</span>
      <button style="margin-left: 20px; padding: 4px 12px;">点赞</button>
      <button style="margin-left: 10px; padding: 4px 12px;">评论</button>
    </template>
  </PageContainer>
</template>

<script setup>
import PageContainer from './PageContainer.vue';
</script>

3. 运行效果与说明

  • 场景1:页脚显示「返回顶部」按钮,点击可实现平滑滚动到页面顶部(默认内容生效)。
  • 场景2:页脚显示「作者 + 点赞 + 评论」,默认的「返回顶部」按钮被覆盖(自定义内容生效)。
  • 核心价值:插槽默认内容让组件更「健壮」,无需父组件每次都传递所有插槽内容,减少冗余代码,提升组件复用性。

第三步:实际业务场景综合应用(卡片组件)

核心需求:创建一个通用的「商品卡片组件」,使用具名插槽实现「商品图片」「商品标题」「商品价格」「操作按钮」的自定义配置,其中「操作按钮」插槽带默认内容(默认「加入购物车」按钮)。

1. 子组件:商品卡片(GoodsCard.vue

<template>
  <div class="goods-card" style="width: 280px; border: 1px solid #f0f0f0; border-radius: 12px; padding: 16px; margin: 16px; float: left; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
    <!-- 具名插槽:商品图片 -->
    <div class="goods-img" style="width: 100%; height: 180px; margin-bottom: 12px; text-align: center;">
      <slot name="image" />
    </div>

    <!-- 具名插槽:商品标题 -->
    <div class="goods-title" style="font-size: 16px; font-weight: 500; margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
      <slot name="title" />
    </div>

    <!-- 具名插槽:商品价格 -->
    <div class="goods-price" style="font-size: 18px; color: #ff4400; margin-bottom: 16px;">
      <slot name="price" />
    </div>

    <!-- 具名插槽:操作按钮(带默认内容) -->
    <div class="goods-actions" style="text-align: center;">
      <slot name="action">
        <!-- 默认内容:加入购物车按钮 -->
        <button style="width: 100%; padding: 8px 0; background: #ff4400; color: #fff; border: none; border-radius: 8px; cursor: pointer;">
          加入购物车
        </button>
      </slot>
    </div>
  </div>
</template>

<script setup>
// 无需额外逻辑,仅提供插槽结构和默认内容
</script>

2. 父组件:使用商品卡片组件(App.vue

自定义不同商品的内容,演示插槽的灵活性:

<template>
  <h2>实际业务场景:商品卡片组件</h2>
  <div style="overflow: hidden; clear: both;">
    <!-- 商品1:使用默认操作按钮(加入购物车) -->
    <GoodsCard>
      <template #image>
        <img src="https://picsum.photos/240/180?random=1" alt="商品图片" style="width: 240px; height: 180px; object-fit: cover; border-radius: 8px;">
      </template>
      <template #title>
        小米手机 14 旗舰智能手机
      </template>
      <template #price>
        ¥ 4999
      </template>
      <!-- 未传递 #action 插槽,显示默认「加入购物车」按钮 -->
    </GoodsCard>

    <!-- 商品2:自定义操作按钮(立即购买 + 收藏) -->
    <GoodsCard>
      <template #image>
        <img src="https://picsum.photos/240/180?random=2" alt="商品图片" style="width: 240px; height: 180px; object-fit: cover; border-radius: 8px;">
      </template>
      <template #title>
        苹果 iPad Pro 平板电脑
      </template>
      <template #price>
        ¥ 7999
      </template>
      <!-- 自定义 #action 插槽内容,覆盖默认按钮 -->
      <template #action>
        <button style="width: 48%; padding: 8px 0; background: #0071e3; color: #fff; border: none; border-radius: 8px; cursor: pointer; margin-right: 4%;">
          立即购买
        </button>
        <button style="width: 48%; padding: 8px 0; background: #f0f0f0; color: #333; border: none; border-radius: 8px; cursor: pointer;">
          收藏
        </button>
      </template>
    </GoodsCard>
  </div>
</template>

<script setup>
import GoodsCard from './GoodsCard.vue';
</script>

3. 运行效果与说明

  • 商品1:操作按钮显示默认的「加入购物车」,快速实现基础功能。
  • 商品2:操作按钮显示「立即购买 + 收藏」,满足自定义需求。
  • 业务价值:通过具名插槽,打造了「通用可复用」的商品卡片组件,父组件可以根据不同商品场景,灵活配置各个区域的内容,既减少了重复代码,又保证了灵活性。

第四步:实现「作用域插槽」

核心需求:基于现有商品卡片组件优化,让子组件持有私有商品数据,通过作用域插槽传递给父组件,父组件自定义渲染格式(如给高价商品加「高端」标识、显示商品优惠信息)。

1. 修改子组件:定义作用域插槽,传递内部数据(GoodsCard.vue

子组件新增内部私有数据,通过「属性绑定」给插槽传递数据(:数据名="子组件内部数据"):

vue

<template>
  <div class="goods-card" style="width: 280px; border: 1px solid #f0f0f0; border-radius: 12px; padding: 16px; margin: 16px; float: left; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
    <!-- 作用域插槽:商品图片(暴露商品id和图片地址) -->
    <div class="goods-img" style="width: 100%; height: 180px; margin-bottom: 12px; text-align: center;">
      <slot name="image" :goodsId="goods.id" :imgUrl="goods.imgUrl" />
    </div>

    <!-- 作用域插槽:商品标题(暴露商品名称和价格) -->
    <div class="goods-title" style="font-size: 16px; font-weight: 500; margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
      <slot name="title" :goodsName="goods.name" :goodsPrice="goods.price" />
    </div>

    <!-- 作用域插槽:商品价格(暴露价格和优惠信息) -->
    <div class="goods-price" style="font-size: 18px; color: #ff4400; margin-bottom: 16px;">
      <slot name="price" :price="goods.price" :discount="goods.discount" />
    </div>

    <!-- 具名插槽:操作按钮(带默认内容) -->
    <div class="goods-actions" style="text-align: center;">
      <slot name="action">
        <!-- 默认内容:加入购物车按钮 -->
        <button style="width: 100%; padding: 8px 0; background: #ff4400; color: #fff; border: none; border-radius: 8px; cursor: pointer;">
          加入购物车
        </button>
      </slot>
    </div>
  </div>
</template>

<script setup>
// 子组件内部私有数据(模拟接口返回,父组件无法直接访问)
const goods = {
  id: 1001,
  name: "小米手机 14 旗舰智能手机",
  price: 4999,
  imgUrl: "https://picsum.photos/240/180?random=1",
  discount: "立减200元,支持分期免息"
};
</script>

2. 父组件:接收并使用作用域插槽数据(App.vue

父组件通过 template #插槽名="插槽数据对象" 接收子组件暴露的数据,支持解构赋值简化代码,自定义渲染逻辑:

vue

<template>
  <h2>进阶:作用域插槽演示(子组件供数,父组件定制渲染)</h2>
  <div style="overflow: hidden; clear: both; margin-top: 40px;">
    <GoodsCard>
      <!-- 接收图片插槽的作用域数据:slotProps(自定义名称,包含goodsId、imgUrl) -->
      <template #image="slotProps">
        <img :src="slotProps.imgUrl" :alt="'商品' + slotProps.goodsId" style="width: 240px; height: 180px; object-fit: cover; border-radius: 8px;">
        <!-- 利用子组件传递的goodsId,添加自定义标识 -->
        <span style="position: absolute; top: 8px; left: 8px; background: red; color: #fff; padding: 2px 8px; border-radius: 4px; z-index: 10;">
          编号:{{ slotProps.goodsId }}
        </span>
      </template>

      <!-- 接收标题插槽的作用域数据:解构赋值(更简洁,推荐) -->
      <template #title="{ goodsName, goodsPrice }">
        {{ goodsName }}
        <!-- 父组件自定义逻辑:价格高于4000加「高端」标识 -->
        <span v-if="goodsPrice > 4000" style="color: #ff4400; font-size: 12px; margin-left: 8px;">
          高端
        </span>
      </template>

      <!-- 接收价格插槽的作用域数据:结合优惠信息渲染 -->
      <template #price="{ price, discount }">
        <span>¥ {{ price }}</span>
        <!-- 渲染子组件传递的优惠信息,自定义样式 -->
        <p style="font-size: 12px; color: #999; margin-top: 4px; text-align: left;">
          {{ discount }}
        </p>
      </template>
    </GoodsCard>
  </div>
</template>

<script setup>
import GoodsCard from './components/GoodsCard.vue';
</script>

3. 运行效果与说明

  • 父组件成功获取子组件私有数据(goodsIddiscount 等),并实现自定义渲染(商品编号、高端标识、优惠信息);
  • 核心语法:子组件「属性绑定传数据」,父组件「插槽数据对象接收」,支持解构赋值简化代码;
  • 核心价值:通用组件(列表、卡片、表格)既保留内部数据逻辑,又开放渲染格式定制权,极大提升组件灵活性和复用性;
  • 注意:作用域插槽本质仍是具名 / 默认插槽,只是增加了「子向父」的数据传递能力。

三、总结(核心知识点回顾,加深记忆)

  1. 使用步骤
  • 子组件:用 <slot name="xxx"> 定义具名插槽(内部可写默认内容),用 :数据名="内部数据" 给插槽传递数据(作用域插槽);
  • 父组件:用 <template #xxx> 给指定具名插槽传内容,用 <template #xxx="slotProps"> 接收作用域插槽数据,未指定插槽名的内容默认插入到 <slot>(默认插槽)。
  1. 核心语法
  • v-slot:插槽名 可简写为 #插槽名,仅能用于 <template> 标签或组件标签上;
  • 作用域插槽数据支持解构赋值,可设置默认值(如 #title="{ goodsName = '默认商品', goodsPrice = 0 }")避免报错。
  1. 插槽体系
  • 基础层:默认插槽(单一区域)、具名插槽(多区域精准定制);
  • 增强层:插槽默认内容(提升健壮性)、作用域插槽(子供数 + 父定制,进阶核心)。

中远海控:子公司订造十二艘1.8万TEU型LNG双燃料动力集装箱船和六艘3000TEU新型宽体船

36氪获悉,中远海控公告,公司全资子公司中远资产与江南船厂、中船贸易签订合计十二份造船协议,订造十二艘18,000TEU型LNG双燃料动力集装箱船,每艘合同造价13.99亿元,本次交易总价为167.88亿元。本次订造船舶交付后,计划投入东西流向主干航线运营,以提升相关航线服务品质。此外,中远资产与舟山重工签订合计六份造船协议,订造六艘3000TEU型集装箱船,总价为19.8亿元。本次订造的3000TEU新型宽体船交付后,计划投入国际区域支线运营,可为相关航线提供稳定的运力保障。
❌