在前端项目里,组件通信只能解决父子层级的问题,一旦数据要跨页面共享、路由要按登录状态放行、接口实例要全局复用,就需要把 mixin、插件、Vuex、本地存储和 Vue Router 组合起来用。下面按“复用逻辑—全局能力—状态集中—路由控制”的顺序,梳理一套 Vue 2 项目里可直接落地的实现方式,文中的命令、配置与代码逻辑均来自原文示例。
一、组件通信的边界在哪里
父子组件通信的基础手段是 props 向下传值、$emit 向上通知,父组件需要命令式操作子组件时用 $refs。当数据层级较深、多个页面都要读写同一份数据时,继续靠 props 逐层透传就会失控,这时应交给 Vuex 或 Pinia 集中管理。这是后面所有方案的前提判断:局部数据留在组件内,跨页面共享数据才进 store。
二、用 mixin 抽取重复选项
混入适合把多个组件重复出现的 data、方法和生命周期逻辑提取出来。组件引入混入后,混入对象会与组件自身选项合并,同名冲突时组件自身选项优先,生命周期钩子则依次执行。
- // src/mixins/userMixin.js
- export default {
- data() {
- return { userName: '访客' }
- },
- methods: {
- resetUserName() {
- this.userName = '访客'
- }
- }
- }
- // 组件内引入
- import userMixin from '@/mixins/userMixin'
- export default {
- name: 'ProfileView',
- mixins: [userMixin]
- }
复制代码
也可以在 main.js 中通过 Vue.mixin() 注册全局混入,但它会作用到每一个组件,容易带来隐式依赖和命名冲突。除非逻辑足够通用,否则优先用局部混入或组合式工具函数。
三、插件:把全局能力收口到 install
插件的本质通常是带有 install(Vue, options) 方法的对象,调用 Vue.use(plugin) 时 Vue 会自动执行它。常见用途包括注册组件、注册自定义指令、挂载原型方法。下面把 axios 实例封装成插件:
- // src/plugins/http.js
- import axios from 'axios'
- const http = axios.create({
- baseURL: 'http://127.0.0.1:8000',
- timeout: 5000
- })
- export default {
- install(Vue) {
- // Vue 2 中通过原型提供 this.$http
- Vue.prototype.$http = http
- }
- }
- // main.js
- import Vue from 'vue'
- import App from './App.vue'
- import httpPlugin from '@/plugins/http'
- Vue.use(httpPlugin)
- new Vue({
- render: h => h(App)
- }).$mount('#app')
复制代码
注册后组件里直接 this.$http.get(...) 即可,无需每个文件重复创建实例。指令同样可以放在插件中注册,例如自动聚焦:
- Vue.directive('focus', {
- inserted(element) {
- element.focus()
- }
- })
复制代码
Vue Router、Vuex、Element UI 这些第三方库也是通过 Vue.use() 接入的。插件挂到原型上的能力建议统一使用 $ 前缀命名,降低与组件自身 data、methods 撞名的概率。
四、插槽:子组件定布局,父组件填内容
默认插槽与具名插槽让父组件向子组件注入内容,子组件只负责整体结构,标题、正文、按钮等可变部分由父组件提供,适合卡片、弹窗、布局类组件。
- <!-- AboutView.vue -->
- <template>
- <ContentCard>
- <template slot="header"><h2>关于页面</h2></template>
- <p>这里是父组件传入的正文。</p>
- <template slot="footer"><button>确定</button></template>
- </ContentCard>
- </template>
- <!-- ContentCard.vue,Vue 2 写法 -->
- <template>
- <section class="card">
- <header><slot name="header">默认标题</slot></header>
- <main><slot>默认内容</slot></main>
- <footer><slot name="footer"></slot></footer>
- </section>
- </template>
复制代码
五、Vuex:state、getters、mutations、actions 的分工
Vuex 把多组件共享的数据集中放进 store,用 mutations 同步修改状态,用 actions 承载异步逻辑或业务流程。Vue 3 项目也能用 Vuex,但新项目一般优先 Pinia。
创建 store 的写法:
- // src/store/index.js
- import Vue from 'vue'
- import Vuex from 'vuex'
- Vue.use(Vuex)
- export default new Vuex.Store({
- state: {
- count: 100
- },
- getters: {
- doubleCount(state) {
- return state.count * 2
- }
- },
- mutations: {
- add(state, amount) {
- state.count += amount
- }
- },
- actions: {
- addCount({ state, commit }, amount) {
- if (state.count + amount > 105) return
- commit('add', amount)
- }
- }
- })
复制代码
这里的角色划分很明确:state 存共享数据,getters 负责派生计算,mutations 是同步改状态的唯一推荐入口,actions 可以放异步逻辑并通过 commit 提交 mutation。上面的 addCount 演示了在 action 里先做上限校验再提交,避免无效的状态变更。
入口注册 store:
- // main.js
- import Vue from 'vue'
- import App from './App.vue'
- import store from './store'
- new Vue({
- store,
- render: h => h(App)
- }).$mount('#app')
复制代码
组件中读取与派发:
- <template>
- <div>
- <p>数量:{{ $store.state.count }}</p>
- <p>两倍数量:{{ $store.getters.doubleCount }}</p>
- <button @click="increase">增加</button>
- </div>
- </template>
- <script>
- export default {
- methods: {
- increase() {
- this.$store.dispatch('addCount', 1)
- }
- }
- }
- </script>
复制代码
需要强调的是不要在组件里直接改 this.$store.state,绕过 mutation 后状态变化难以追踪。另外小型项目不必把所有变量都塞进 Vuex,只有跨页面或跨组件共享、且需要统一修改规则的状态才值得集中管理。
六、本地存储的封装与 token 取舍
浏览器常见存储有三类:Cookie 可设置过期时间且会随请求发送;sessionStorage 只在当前标签页会话内有效;localStorage 长期保存,除非主动删除或清理站点数据。三者都不适合存放密码等高敏感信息。
localStorage 与 sessionStorage 只能存字符串,数组和对象必须先序列化:
- const user = { name: 'lqz', age: 19 }
- localStorage.setItem('user', JSON.stringify(user))
- const savedUser = JSON.parse(localStorage.getItem('user') || '{}')
- sessionStorage.setItem('draft', '临时内容')
- sessionStorage.removeItem('draft')
- localStorage.removeItem('user')
复制代码
直接 JSON.parse 空值或脏数据会抛异常,因此建议封装一层读写方法统一处理:
- export function setStorage(key, value) {
- localStorage.setItem(key, JSON.stringify(value))
- }
- export function getStorage(key, fallback = null) {
- const value = localStorage.getItem(key)
- if (value === null) return fallback
- try {
- return JSON.parse(value)
- } catch {
- return value
- }
- }
复制代码
登录 token 存哪里要结合安全策略决定:localStorage 使用简单,但 XSS 可以读取其中的 token;HttpOnly Cookie 能降低脚本读取风险,但需要配合 CSRF 防护。永久保存不等于安全保存,这一点在选型时常被忽略。
七、Vue Router 安装、配置与跳转参数
Vue Router 负责把 URL 映射到页面组件:router-view 是页面组件的渲染位置,router-link 是声明式导航,this.$router 用于代码跳转,this.$route 表示当前路由信息。
Vue 2 对应 Vue Router 3,安装命令如下:
- // src/router/index.js
- import Vue from 'vue'
- import VueRouter from 'vue-router'
- import HomeView from '@/views/HomeView.vue'
- import AboutView from '@/views/AboutView.vue'
- Vue.use(VueRouter)
- export default new VueRouter({
- mode: 'history',
- routes: [
- { path: '/', name: 'home', component: HomeView },
- { path: '/about', name: 'about', component: AboutView }
- ]
- })
复制代码- <!-- App.vue -->
- <template>
- <div id="app">
- <router-link to="/">首页</router-link>
- <router-link to="/about">关于</router-link>
- <router-view />
- </div>
- </template>
复制代码
最后把 router 注入根实例:
- import router from './router'
- new Vue({
- router,
- render: h => h(App)
- }).$mount('#app')
复制代码
传参方面,query 适合筛选、分页这类可选信息,不会改变路由匹配的组件;params 配合路径参数更适合标识某个资源:
- // JavaScript 跳转
- this.$router.push({
- name: 'about',
- query: { name: '张三', age: 19 }
- })
- this.$router.push({
- name: 'user',
- params: { id: 10 }
- })
- // 路由配置:路径参数
- { path: '/user/:id', name: 'user', component: UserView }
- // UserView.vue 中读取
- const id = this.$route.params.id
- const name = this.$route.query.name
复制代码
push 会新增浏览历史,replace 替换当前记录,back() 与 go(-1) 返回上一页。需要记住:路由参数来自地址栏,不能直接当作权限依据,敏感操作仍要由后端认证。
八、嵌套路由支撑后台布局
后台管理页面通常共享顶部和侧边栏,只替换右侧内容,这时用嵌套路由。父组件里必须放 router-view,子路由才有渲染位置。
- const routes = [
- {
- path: '/backend',
- component: () => import('@/views/BackendView.vue'),
- children: [
- { path: '', component: () => import('@/views/backend/IndexView.vue') },
- { path: 'orders', component: () => import('@/views/backend/OrderView.vue') },
- { path: 'goods', component: () => import('@/views/backend/GoodsView.vue') }
- ]
- }
- ]
复制代码- <!-- BackendView.vue -->
- <template>
- <div class="backend">
- <aside>
- <router-link to="/backend">首页</router-link>
- <router-link to="/backend/orders">订单</router-link>
- <router-link to="/backend/goods">商品</router-link>
- </aside>
- <main><router-view /></main>
- </div>
- </template>
复制代码
子路由的空 path 对应默认页,切换 orders、goods 时只有 main 内的 router-view 内容变化,侧边栏不会重新渲染。
九、路由守卫做登录保护
全局前置守卫在页面跳转前执行,可用来检查 token、角色或权限。它只负责前端导航体验,接口层权限必须由后端再次校验。
- router.beforeEach((to, from, next) => {
- const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
- const token = localStorage.getItem('access')
- if (requiresAuth && !token) {
- next({ name: 'login', query: { redirect: to.fullPath } })
- } else if (to.name === 'login' && token) {
- next({ name: 'home' })
- } else {
- next()
- }
- })
复制代码
需要保护的路由通过 meta 标记,to.matched 会匹配嵌套路由链上的所有记录,因此父级标记即可覆盖其子路由:
- {
- path: '/backend',
- component: BackendView,
- meta: { requiresAuth: true }
- }
复制代码
从上面代码可以看到,每个分支都要为 next 给出明确结果:未登录带着 redirect 跳到登录页,已登录再访问登录页则回到首页。登录成功后读取 redirect 查询参数可以回到用户原本要访问的页面,但必须限制跳转为本站路径,否则会形成开放重定向风险。
十、Hash 与 History 模式的选择
Hash 模式用 URL 中 # 之后的内容表示前端路由,例如 /#/about。浏览器不会把 hash 发送给服务器,部署简单、兼容性好,但地址不够简洁。History 模式使用 /about 这样的正常路径,地址美观,但刷新页面时服务器必须把未知路径回退到 index.html,否则直接 404。选模式时前端需求与后端部署配置要一并考虑:没有可配置的 fallback 规则时,Hash 模式更稳妥。
十一、落地检查清单
1. 重复的 data、方法、生命周期逻辑抽到局部 mixin,慎用 Vue.mixin 全局注册。
2. 全局 axios 实例与自定义指令通过插件 install 统一挂载,原型方法加 $ 前缀。
3. 跨页面共享状态进 Vuex,修改只走 mutations,异步与业务校验放 actions。
4. 存储读写统一封装,JSON 解析异常要有兜底,token 存放方式按 XSS、CSRF 与过期策略权衡。
5. 后台布局用嵌套路由,父组件保留 router-view;受保护路由用 meta.requiresAuth 标记。
6. 守卫只解决导航体验,redirect 只允许站内跳转,接口权限交由后端校验。
按这套结构实现一个带登录保护的后台管理页面:登录后进入嵌套路由,刷新仍能恢复 token,退出后所有受保护路由都跳回登录页,就是一次完整的状态管理与权限控制串联练习。 |