普通视图

发现新文章,点击刷新页面。
今天 — 2025年6月8日首页

管理不同权限用户的左侧菜单展示以及权限按钮的启用 / 禁用之其中一种解决方案

2025年6月7日 21:42

一、权限管理方案设计

1. 权限模型

推荐采用 RBAC(基于角色的访问控制)  模型,把用户分配到不同角色,角色再关联对应的权限。

  • 权限项:是最小的控制单元,像 view_useredit_product 这种。
  • 角色:由多个权限项组合而成,例如 admineditorviewer
  • 用户:和一个或多个角色相对应。

2. 权限数据存储

  • 前端存储:登录成功后,从后端获取权限数据,然后存到 Vuex/Pinia 或者 localStorage 中。

  • 权限数据结构示例

javascript

{
  user: { id: 1, name: "张三", role: "admin" },
  permissions: ["view_user", "edit_user", "view_product", "edit_product"]
}

二、左侧菜单动态展示

1. 菜单配置

创建一个菜单配置文件,把权限和菜单项关联起来。

javascript

// src/config/menu.js
export const menuList = [
  {
    path: "/dashboard",
    name: "Dashboard",
    icon: "dashboard",
    permission: "view_dashboard" // 访问该菜单所需权限
  },
  {
    path: "/user",
    name: "用户管理",
    icon: "user",
    permission: "view_user",
    children: [
      {
        path: "/user/list",
        name: "用户列表",
        permission: "view_user"
      },
      {
        path: "/user/add",
        name: "添加用户",
        permission: "add_user"
      }
    ]
  },
  // 其他菜单项...
];

2. 菜单组件实现

在组件里依据用户权限过滤菜单项。

vue

<!-- src/components/Sidebar.vue -->
<template>
  <div class="sidebar">
    <el-menu :default-active="activeMenu" mode="vertical">
      <template v-for="item in filteredMenu">
        <!-- 一级菜单 -->
        <el-menu-item 
          v-if="!item.children && hasPermission(item.permission)"
          :key="item.path"
          :index="item.path"
        >
          <i :class="item.icon"></i>
          <span slot="title">{{ item.name }}</span>
        </el-menu-item>
        
        <!-- 子菜单 -->
        <el-submenu 
          v-else-if="item.children && hasPermission(item.permission)"
          :key="item.path"
          :index="item.path"
        >
          <template slot="title">
            <i :class="item.icon"></i>
            <span>{{ item.name }}</span>
          </template>
          <el-menu-item
            v-for="child in item.children"
            :key="child.path"
            :index="child.path"
            v-if="hasPermission(child.permission)"
          >
            {{ child.name }}
          </el-menu-item>
        </el-submenu>
      </template>
    </el-menu>
  </div>
</template>

<script>
import { mapState } from "vuex";
import { menuList } from "@/config/menu";

export default {
  computed: {
    ...mapState(["permissions"]),
    // 过滤后的菜单项
    filteredMenu() {
      return menuList.filter(item => this.hasPermission(item.permission));
    }
  },
  methods: {
    // 权限检查方法
    hasPermission(permission) {
      // 如果没有设置权限,默认可见
      if (!permission) return true;
      // 检查用户是否拥有该权限
      return this.permissions.includes(permission);
    }
  }
};
</script>

三、权限按钮的展示与禁用

1. 自定义指令实现

借助自定义指令来控制按钮的显示和禁用状态。

javascript

// src/directives/permission.js
export const permission = {
  inserted(el, binding, vnode) {
    const { value } = binding;
    const permissions = vnode.context.$store.state.permissions;
    
    if (value) {
      // 检查是否有该权限
      const hasPermission = permissions.includes(value);
      
      if (!hasPermission) {
        // 没有权限:隐藏按钮
        el.parentNode && el.parentNode.removeChild(el);
        // 或者禁用按钮(根据需求选择)
        // el.disabled = true;
        // el.classList.add('is-disabled');
      }
    } else {
      console.error('需要指定权限标识!');
      el.parentNode && el.parentNode.removeChild(el);
    }
  }
};

2. 全局注册指令

在 main.js 里全局注册这个指令。

javascript

// src/main.js
import Vue from "vue";
import { permission } from "./directives/permission";

Vue.directive("permission", permission);

3. 在组件中使用

vue

<!-- 使用示例 -->
<template>
  <div>
    <!-- 有权限时显示 -->
    <el-button 
      v-permission="'add_user'"
      type="primary"
      @click="addUser"
    >
      添加用户
    </el-button>
    
    <!-- 无权限时禁用 -->
    <el-button 
      :disabled="!hasPermission('edit_user')"
      type="success"
      @click="editUser"
    >
      编辑用户
    </el-button>
  </div>
</template>

<script>
export default {
  methods: {
    hasPermission(permission) {
      return this.$store.state.permissions.includes(permission);
    }
  }
};
</script>

四、路由权限控制

对路由访问权限进行控制,防止用户手动输入 URL 访问受限页面。

javascript

// src/router/index.js
import router from "./router";
import store from "./store";

router.beforeEach((to, from, next) => {
  // 获取用户权限
  const permissions = store.state.permissions;
  
  // 检查路由是否需要权限
  if (to.meta.permission) {
    if (permissions.includes(to.meta.permission)) {
      next(); // 有权限,放行
    } else {
      next({ path: "/403" }); // 无权限,跳转到403页面
    }
  } else {
    next(); // 无需权限,直接放行
  }
});

五、权限管理流程

  1. 用户登录:用户输入账号密码登录系统。
  2. 权限验证:后端验证用户身份,返回用户角色和权限信息。
  3. 权限存储:前端把权限信息存到 Vuex/Pinia 或者 localStorage 中。
  4. 菜单渲染:根据用户权限动态渲染左侧菜单。
  5. 按钮控制:在组件里通过自定义指令或者方法控制按钮的显示和禁用。
  6. 路由拦截:对路由进行拦截,防止未授权访问。

六、优缺点分析

优点

  • 可扩展性强:能够轻松添加新的角色和权限,而不用修改大量代码。
  • 维护便捷:权限配置集中管理,降低了维护成本。
  • 安全性高:从菜单、按钮、路由三个层面进行权限控制,有效防止越权访问。

缺点

  • 初期配置复杂:需要设计合理的权限模型和数据结构。
  • 性能影响:在复杂应用中,频繁的权限检查可能会对性能产生一定影响。
昨天 — 2025年6月7日首页

都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理

2025年6月6日 21:56

一、Vue 的响应式原理

(一)Vue 2 中的响应式实现

Vue 2 使用Object.defineProperty来实现数据的响应式。当我们将一个普通的 JavaScript 对象传给 Vue 实例的data选项时,Vue 会遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转换为getter/setter。例如:

let data = {
  message: 'Hello, Vue!'
};
Object.keys(data).forEach(key => {
  let value = data[key];
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`Getting ${key}: ${value}`);
      return value;
    },
    set(newValue) {
      if (newValue!== value) {
        value = newValue;
        console.log(`Setting ${key} to: ${newValue}`);
        // 这里触发视图更新的相关逻辑
      }
    }
  });
});

在上述代码中,通过Object.defineProperty为data对象的message属性添加了getter和setter。当获取message属性时,会触发getter,打印相关信息;当设置message属性时,会触发setter,在setter中判断新值与旧值是否不同,若不同则更新值并可以触发视图更新的逻辑(实际 Vue 中更为复杂,这里仅作示意)。

(二)Vue 3 中的响应式实现

Vue 3 改用Proxy来实现响应式。Proxy可以直接监听对象上属性的添加和删除,弥补了Object.defineProperty的一些不足。示例如下:

let data = {
  message: 'Hello, Vue 3!'
};
let handler = {
  get(target, key) {
    console.log(`Getting ${key}: ${target[key]}`);
    return target[key];
  },
  set(target, key, value) {
    if (target[key]!== value) {
      target[key] = value;
      console.log(`Setting ${key} to: ${value}`);
      // 这里触发视图更新的相关逻辑
      return true;
    }
    return false;
  }
};
let proxy = new Proxy(data, handler);

在这个例子中,通过Proxy创建了一个代理对象proxy,对data对象的属性访问和设置进行了拦截。当获取或设置proxy对象的属性时,会执行handler中的get和set方法,同样可以在set方法中触发视图更新逻辑。

(三)依赖收集与更新

无论是 Vue 2 还是 Vue 3,响应式系统的核心都包括依赖收集和更新。当数据被访问时,会进行依赖收集,将依赖该数据的 “观察者”(Watcher)添加到一个依赖容器(如 Vue 2 中的Dep)中。当数据发生变化时,依赖容器会通知所有相关的Watcher进行更新,从而重新渲染视图。例如在 Vue 2 中,每个组件实例都对应一个Watcher实例,它会在组件渲染的过程中把 “接触” 过的数据属性记录为依赖。之后当依赖项的setter触发时,会通知Watcher,使它关联的组件重新渲染。

二、Vue 的双向绑定原理

(一)双向绑定的概念

双向绑定的核心是让数据的变化自动反映到视图上,而视图的变化也能自动同步回数据。它主要应用于表单元素(如、等)。Vue 的双向绑定原理建立在响应式系统和事件监听机制上。

(二)实现流程

以为例,双向绑定的实现流程如下:

  1. 初始化绑定:当 Vue 实例被创建时,会对data选项中的message数据进行响应式处理,为其设置getter和setter。v-model指令会将message的初始值绑定到元素中,使视图显示message的当前值。
  1. 数据变化更新视图:当message的值发生改变时,Vue 的响应式系统会触发message的setter。setter通知依赖于message的Watcher去更新视图,Watcher会重新渲染依赖message的 DOM 元素,使显示的新值与数据同步。
  1. 视图变化更新数据:在使用v-model时,Vue 会为元素添加一个input事件监听器。当用户在输入框中输入内容时,会触发input事件,Vue 捕获到这个事件后,将输入框的值赋给message,触发message的setter,数据会被更新为用户输入的内容。由于数据被更新,Vue 会再次触发响应式更新过程,如果有其他依赖于message的 DOM 元素或计算属性,它们也会同步更新。

三、v-model 的实现原理

(一)v-model 是语法糖

v-model本质上是一个语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行特殊处理。它会忽略所有表单元素的value、checked、selected attribute 的初始值,而总是将 Vue 实例的数据作为数据来源。我们应该通过 JavaScript 在组件的data选项中声明初始值。

(二)v-model 在不同表单元素上的实现

  1. text 和 textarea 元素:使用value property 和input事件。例如,等价于<input type="text" :value="message" @input="message = event.target.value">:value="message"实现了数据到视图的单向绑定,将message的值绑定到输入框的value属性上;@input="message=event.target.value">。:value="message"实现了数据到视图的单向绑定,将message的值绑定到输入框的value属性上;@input="message = event.target.value"监听输入事件,当用户输入内容时,将新的值赋给message,实现视图到数据的同步。
  1. radio 和 checkbox 元素:使用checked property 和change事件。对于多个复选框,需要手动设置value值,以便在事件处理函数中拿到新数据来更新数据。而单个复选框绑定的是布尔值,在其事件处理函数中可以直接拿到新数据进行更新。例如:
<input type="checkbox" v-model="isChecked">
<input type="checkbox" v-model="checkboxValues" value="option1">
<input type="checkbox" v-model="checkboxValues" value="option2">
  1. select 字段:将value作为 prop 并将change作为事件。例如:
<select v-model="selectedOption">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
</select>

等价于:

<select :value="selectedOption" @change="selectedOption = $event.target.value">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
</select>

(三)v-model 在组件中的应用

在组件中,一个组件上的v-model默认会利用名为value的属性和名为input的事件(Vue 2),在 Vue 3 中v-model默认使用modelValue属性和update:modelValue事件来实现双向数据绑定。实现双向绑定的核心步骤如下:

  1. 父传子:数据通过父组件的props传递给子组件,子组件内将v-model拆解绑定数据。
  1. 子传父:通过自定义事件,子组件将新值传递给父组件修改。例如在 Vue 2 中:
<!-- 父组件 -->
<template>
  <MyComponent v-model="parentValue"></MyComponent>
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
  components: {
    MyComponent
  },
  data() {
    return {
      parentValue: ''
    };
  }
};
</script>
<!-- 子组件MyComponent.vue -->
<template>
  <input :value="value" @input="$emit('input', $event.target.value)">
</template>
<script>
export default {
  props: ['value']
};
</script>

在 Vue 3 中:

<!-- 父组件 -->
<template>
  <MyComponent v-model="parentValue"></MyComponent>
</template>
<script setup>
import MyComponent from './MyComponent.vue';
let parentValue = ref('');
</script>
<!-- 子组件MyComponent.vue -->
<template>
  <input :modelValue="modelValue" @update:modelValue="(newValue) => $emit('update:modelValue', newValue)">
</template>
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>

总结:

 以下是对 Vue 响应式原理、双向绑定原理及 `v-model` 实现原理的总结,帮助快速梳理核心要点:

一、响应式原理

1. Vue 2(Object.defineProperty

  • 核心机制:通过 Object.defineProperty 为数据对象的属性添加 getter/setter,拦截属性的读取和修改。
  • 依赖收集:当属性被读取时(触发 getter),收集当前组件的 Watcher 作为依赖。
  • 更新触发:当属性被修改时(触发 setter),通知所有依赖的 Watcher 重新渲染视图。
  • 局限性:无法监听数组索引和对象新增属性的变化,需通过 Vue.set 等方法手动处理。

2. Vue 3(Proxy

  • 核心机制:使用 Proxy 代理原始数据对象,拦截所有属性操作(读取、修改、删除等)。
  • 依赖收集:通过 Proxy 的 get 拦截器收集依赖(Watcher)。
  • 更新触发:通过 set 拦截器触发依赖更新,自动处理数组和对象新增属性的响应式。
  • 优势:更高效、更全面,原生支持数组和对象的所有操作。

二、双向绑定原理

核心逻辑

  • 数据 → 视图:基于响应式系统,数据变化时通过 Watcher 自动更新视图。
  • 视图 → 数据:通过监听表单元素的事件(如 inputchange),将用户输入的值同步回数据。
  • 典型场景:表单元素(如 <input><textarea><select>)的值与 Vue 实例的数据同步。

实现流程

  1. 初始化绑定:将数据初始值渲染到视图(如 :value="data")。
  2. 数据更新视图:数据变化时,响应式系统触发视图重新渲染。
  3. 视图更新数据:表单元素触发事件(如 input)时,将新值赋值给数据(如 data = $event.target.value)。

三、v-model 实现原理

1. 本质:语法糖

  • 简化表单元素的双向绑定操作,等价于 :value(或 :checked 等)与事件监听(如 @input@change)的组合。

2. 不同元素的实现

元素类型 绑定属性 监听事件 等价代码示例
input/textarea :value @input <input :value="data" @input="data=$event.target.value">
checkbox :checked @change <input :checked="data" @change="data=$event.target.checked">
radio :checked @change <input :checked="data" @change="data=$event.target.value">
select :value @change <select :value="data" @change="data=$event.target.value">

3. 组件中的 v-model

  • Vue 2:通过 props 接收父组件数据(默认属性 value),通过 $emit('input', 新值) 通知父组件更新。
  • Vue 3:通过 props 接收 modelValue,通过 $emit('update:modelValue', 新值) 实现双向绑定。

四、常见误区与注意事项

  1. 响应式边界

    • Vue 2 中,直接修改数组索引或对象新增属性不会触发更新,需用 Vue.set 或数组变异方法(如 pushsplice)。
    • Vue 3 中,Proxy 原生支持数组和对象的所有操作,无需额外处理。
  2. v-model 与单向绑定的区别

    • v-model 是双向绑定,同时包含数据到视图(:)和视图到数据(@)的逻辑。
    • 单向绑定(如 :value)仅实现数据到视图的更新,需手动添加事件监听才能反向更新数据。
  3. 组件开发注意点

    • 子组件使用 v-model 时,需显式声明 props 和触发对应事件(input 或 update:modelValue)。
    • 避免在子组件中直接修改 props 传递的数据,应通过事件通知父组件修改。

总结对比

特性 Vue 2(Object.defineProperty Vue 3(Proxy
响应式实现 getter/setter 拦截属性 Proxy 代理对象所有操作
数组 / 对象新增属性 需手动处理(Vue.set 自动支持
v-model 本质 语法糖(:value + @input 等) 同上,但组件中使用 modelValue 和 update:modelValue
昨天以前首页

TS常规面试题1

2025年6月5日 23:08

以下是 TypeScript(TS)常见的面试题分类及参考答案,覆盖基础语法、类型系统、高级特性等核心知识点:

一、基础概念

1. TypeScript 与 JavaScript 的区别?

  • TypeScript:JavaScript 的超集,静态类型检查,提供接口、枚举、类等面向对象特性,编译后生成纯 JS 代码。
  • JavaScript:动态类型,无编译时类型检查,灵活性高但容易出现运行时错误。

2. 什么是静态类型和动态类型?

  • 静态类型:编译时检查类型,如 TS、Java;
  • 动态类型:运行时检查类型,如 JS、Python。

二、类型系统

1. 如何定义基本类型?

typescript

let num: number = 10;
let str: string = 'hello';
let isDone: boolean = false;
let arr: number[] = [1, 2, 3]; // 或 Array<number>
let tuple: [string, number] = ['a', 1]; // 元组

2. 什么是联合类型(Union Type)和交叉类型(Intersection Type)?

  • 联合类型A | B,值可以是 A 或 B 类型(如 string | number)。
  • 交叉类型A & B,值同时拥有 A 和 B 的所有属性(常用于接口合并)。

3. 如何实现类型守卫(Type Guard)?

  • typeof:判断基本类型(如 typeof x === 'string')。
  • instanceof:判断类实例(如 x instanceof Person)。
  • 自定义守卫:通过函数返回类型谓词(如 function isString(x: any): x is string)。

三、接口与类

1. 接口(Interface)和类型别名(Type Alias)的区别?

  • 接口:只能定义对象结构,支持继承(extends),自动合并同名接口。
  • 类型别名:可定义基本类型、联合类型等,通过 & 合并类型,不可重复定义。

2. 如何实现类的继承和多态?

typescript

class Animal {
  constructor(protected name: string) {}
  speak(): string { return 'Animal sound'; }
}

class Dog extends Animal {
  speak(): string { return `Dog ${this.name} barks`; } // 重写方法
}

const dog: Animal = new Dog('Buddy');
console.log(dog.speak()); // 多态:输出 "Dog Buddy barks"

四、高级特性

1. 泛型(Generics)的作用?

  • 示例

    typescript

    function identity<T>(arg: T): T { return arg; }
    const num = identity<number>(10); // 或省略类型参数,自动推断
    
  • 作用:创建可复用组件,支持多种数据类型,避免重复代码。

2. 什么是装饰器(Decorator)?

  • 示例

    typescript

    function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value;
      descriptor.value = function(...args: any[]) {
        console.log(`Calling ${propertyKey} with ${args}`);
        return originalMethod.apply(this, args);
      };
    }
    
    class Calculator {
      @log
      add(a: number, b: number) { return a + b; }
    }
    
  • 作用:在不修改原代码的情况下,扩展类、方法、属性的功能。

3. 如何处理可选链(Optional Chaining)和空值合并(Nullish Coalescing)?

  • 可选链obj?.prop 或 obj?.method(),避免 obj.prop 因 obj 为 null/undefined 报错。
  • 空值合并a ?? b,仅当 a 为 null/undefined 时返回 b(区别于 ||)。

五、类型体操

1. 如何实现 Partial<T> 工具类型?

typescript

type Partial<T> = { [P in keyof T]?: T[P] }; // 将所有属性变为可选

2. 如何获取函数返回值类型?

typescript

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;

function fetchData(): Promise<string> { return Promise.resolve('data'); }
type Data = ReturnType<typeof fetchData>; // 类型为 Promise<string>

六、工程实践

1. tsconfig.json 中常见配置项的作用?

  • compilerOptions

    • target:指定 JS 版本(如 ESNext)。
    • module:指定模块系统(如 ESNext)。
    • strict:启用所有严格类型检查(如 noImplicitAny)。
    • outDir:编译输出目录。
    • moduleResolution:模块解析策略(如 Node)。

2. 如何处理第三方库的类型声明?

  • 安装官方类型包:npm install @types/xxx

  • 自定义类型声明文件(.d.ts):

    typescript

    declare module 'library-name' {
      export function func(): string;
    }
    

七、常见场景

1. 如何处理异步函数的返回类型?

typescript

async function getData(): Promise<string> {
  return 'data';
}

// 使用时需考虑 Promise 包装
const result: string = await getData(); // 正确
const wrong: string = getData(); // 错误,类型为 Promise<string>

2. 如何在 React 中使用 TypeScript?

tsx

import React, { FC, useState } from 'react';

interface Props {
  name: string;
  age?: number;
}

const App: FC<Props> = ({ name, age = 18 }) => {
  const [count, setCount] = useState<number>(0);
  return <div>Hello {name}, age {age}, count {count}</div>;
};

八、设计模式

1. 如何实现单例模式(Singleton)?

typescript

class Singleton {
  private static instance: Singleton;
  private constructor() {} // 私有构造函数

  static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

九、性能优化

1. TypeScript 会影响运行时性能吗?

  • 编译后:TS 代码被编译为纯 JS,不会影响运行时性能。
  • 编译时:类型检查会增加构建时间,但可通过 tsconfig.json 配置优化(如 incremental 选项)。

十、易错点

1. any 和 unknown 的区别?

  • any:禁用类型检查,可赋值给任意类型。
  • unknown:安全的 “未知类型”,使用前必须先断言或类型守卫。

2. 如何避免类型断言滥用?

  • 优先使用类型守卫或泛型。
  • 仅在确定类型但编译器无法推断时使用断言(如 as Type)。
❌
❌