面试题
1、什么是委托?
面试官您好,委托是C#中一种可以指向方法的类型,简单来说就是方法的“类型安全指针”。它能把方法当作参数传递、赋值给变量,核心作用是实现回调逻辑和代码解耦。
比如我们可以定义一个委托,让它指向不同的打招呼方法,调用委托时就会执行对应的方法。实际开发中我们基本不用自定义委托,.NET已经提供了两个通用泛型委托:Action用于无返回值的方法,Func用于有返回值的方法,最多支持16个参数,能覆盖绝大多数场景。
委托也是Lambda表达式和LINQ的基础,Lambda本质上就是匿名委托的简化写法。
2、什么是Lambda?
面试官您好,Lambda是C#中用来快速创建匿名函数的语法糖,它能极大简化委托的书写,也是LINQ查询的核心语法。
它的核心格式是(参数) => 方法体,=>读作“goes to”,左边是输入参数,右边是执行逻辑。Lambda有几个非常实用的简化规则:编译器能自动推断参数类型,所以可以省略参数类型;如果只有一个参数,还能省略参数的圆括号;如果方法体只有一行代码,有返回值时可以省略花括号和return关键字,无返回值时也能省略花括号。
比如原来写匿名委托需要Func<int, int> fn = delegate(int i) { return i + 6; },用Lambda就能简化成Func<int, int> fn = i => i + 6。现在Lambda在LINQ查询、ASP.NET Core中间件、事件处理等场景中被大量使用,让代码更简洁易读。
3、什么是扩展方法?
面试官您好,扩展方法是C#的语法糖,它能在不修改原有类型源码、不通过继承的前提下,给现有类型“添加”新的方法。
定义扩展方法有三个必须遵守的规则:第一,扩展方法所在的类必须是静态类;第二,方法本身必须是public static修饰的静态方法;第三,方法的第一个参数必须加this关键字,用来指定要扩展的目标类型。比如我们可以给string类型扩展一个StringToInt方法,之后就能直接用"123".StringToInt()这样的方式调用。
其实LINQ的本质就是一堆扩展方法,比如我们常用的Where、Select、OrderBy,都是给IEnumerable<T>接口写的扩展方法,放在System.Linq命名空间的静态类里,所以所有实现了这个接口的集合,都能直接使用LINQ进行数据查询。
4、什么是控制反转与依赖注入?控制反转有什么好处?
面试官您好,控制反转也就是IoC,它是一种核心的设计思想,不是具体的技术。简单来说,就是把对象的创建、属性赋值、依赖关系管理这些控制权,从我们手写的业务代码里,转移到外部的IoC容器中。传统开发中我们需要自己new对象、手动组装依赖,比如自己读配置创建数据库连接;现在变成了“我声明需要什么对象,容器自动给我提供”。
而依赖注入简称DI,是控制反转最常用、最主流的实现方式。它的原理是IoC容器通过反射分析出类的依赖关系,自动把依赖的对象注入到需要的位置,比如.NET里默认用的就是构造函数注入。
控制反转的好处很明显:第一是解耦,比如换数据库只需要替换实现类,不用改业务逻辑;第二是降低开发负担,不用关心对象的创建细节,能专注写业务代码;第三是便于测试和维护,单元测试可以轻松mock依赖,对象的生命周期也能由容器统一管理。
5、依赖注入生命周期介绍
面试官您好,依赖注入的服务生命周期决定了每次获取服务时,是创建新对象还是复用已有对象,主要分为三种:
第一种是瞬态(Transient) ,每次请求服务都会创建一个全新的对象。它适合轻量、无状态的服务,但不能滥用,否则会产生过多对象浪费内存。
第二种是范围(Scoped) ,在同一个范围内共享同一个实例,不同范围则是不同的实例。最典型的就是ASP.NET Core中,一次HTTP请求就是一个默认范围,同一次请求里多次注入同一个服务,拿到的都是同一个对象,非常适合数据库连接这类和请求绑定的服务。
第三种是单例(Singleton) ,整个应用程序的生命周期内只有一个实例,全局共享。它能节省创建对象的资源开销,适合无状态的工具类、配置类,但一定要注意线程安全,避免并发修改问题。
选择上,无状态服务优先用单例,请求内共享的用范围,每次需要新实例的用瞬态。
6、控制反转实现方式说明?
面试官您好,控制反转主要有两种实现方式,分别是服务定位器和依赖注入,其中依赖注入是目前更推荐的方式。
第一种是服务定位器,核心是有一个全局的服务定位器对象,比如.NET里的ServiceProvider。我们需要主动调用它的GetService或GetRequiredService方法,向容器“索要”需要的服务对象。
第二种是依赖注入(DI) ,和服务定位器相反,它是容器主动把依赖的对象注入到我们的类中。我们不需要主动去获取,只需要在类里通过构造函数、属性等方式,声明需要什么类型的依赖,容器会自动完成注入。
两者相比,依赖注入的代码更简洁,和容器的耦合度更低,所以优先使用依赖注入。只有在依赖注入无法满足需求的场景,比如需要动态获取服务的时候,才会考虑使用服务定位器。
7、面向接口编程好处
面试官您好,面向接口编程的核心是把功能的契约和具体实现分离:接口定义了类应该具备的功能规范,实现类负责写具体的业务逻辑。它的好处主要有这几点:
第一是强解耦,代码依赖的是接口而不是具体实现。比如文档里的IConfig接口,当需要把配置从文件读取改成从数据库读取时,只需要新增一个DBConfigImpl实现类,修改服务注册即可,不用改动日志、控制器的业务代码。
第二是符合开闭原则,新增功能只需要添加新的实现类,不用修改原有代码,大大提高了系统的可扩展性。
第三是便于单元测试,我们可以轻松mock接口的实现,不用依赖真实的数据库、文件等外部资源,让测试更简单、更独立。
第四是提升协作和维护效率,接口明确了功能契约,团队协作时大家只需要关注接口定义,不用关心内部实现;后续维护时,也能快速通过接口理清模块的功能边界。同时,面向接口编程也是依赖注入能够顺利实现的基础。
8、.NET Core中配置系统实现方式
面试官您好,.NET Core的配置系统相比传统.NET Framework的Web.config更加灵活,支持JSON、XML、INI、环境变量、命令行等多种配置源,核心基于IConfigurationRoot实现,主要有三种主流方式,其中选项模式是官方推荐的生产级方案。
第一种是基础键值对读取。先通过ConfigurationBuilder加载配置文件,比如调用AddJsonFile,这里有两个关键参数:optional设为false可以提前发现文件缺失错误,reloadOnChange设为true支持配置热更新。读取时可以用索引器config["name"],分级配置用冒号分隔比如config["proxy:address"],还有专门的GetConnectionString方法读取数据库连接串。这种方式简单,但配置项多了会很繁琐。
第二种是绑定类读取。需要安装Microsoft.Extensions.Configuration.Binder包,定义和配置节点结构一致的自定义类,属性名和配置项名对应,然后通过config.GetSection("proxy").Get<Proxy>()直接把配置映射到对象,也可以用config.Get<Config>()绑定整个根配置,比逐个读取高效很多。
第三种是选项模式,也是最常用的。它和依赖注入深度结合,需要安装Options和DI相关包,通过三个泛型接口使用:IOptions<T>不支持热更新,重启才生效,资源占用最少;IOptionsMonitor<T>支持实时热更新,但同一个请求内配置可能不一致;IOptionsSnapshot<T>支持热更新,且同一个范围内配置保持一致,最符合大部分业务场景。使用时先定义配置模型类,在DI容器中用Configure<T>()绑定配置节点,最后在类的构造函数注入对应的接口,通过Value属性获取配置即可。
9、什么是日志?在.NET Core MVC与.NET Core WebAPI中怎样实现日志?
面试官您好,日志是程序运行的“黑匣子”,用来记录程序运行过程中的状态、事件和异常信息,是排查问题、监控系统运行的核心手段。.NET Core的日志系统是抽象化的,MVC和WebAPI基于同一套通用主机,日志实现方式完全一致,主要分为内置日志和第三方日志框架两类。
首先说日志级别,从低到高依次是Trace、Debug、Information、Warning、Error、Critical。Trace最详细但包含敏感信息,不建议生产使用;Critical是最高级别,对应系统崩溃等生死攸关的问题;日常开发中常用Information记录常规流程,Warning记录潜在风险,Error记录业务异常。
然后是实现方式。
第一种是内置控制台日志,适合开发环境。先安装Microsoft.Extensions.Logging核心包和Microsoft.Extensions.Logging.Console控制台输出包,在DI容器中调用AddLogging(logBuilder => logBuilder.AddConsole())注册服务,还可以通过SetMinimumLevel设置最低输出级别。使用时在类的构造函数注入泛型ILogger<T>接口,T填当前类名,这样日志会自动带上类名方便定位,然后调用LogInformation、LogError等方法输出即可。
第二种是第三方日志框架,生产环境常用NLog,因为内置日志不支持文件持久化。步骤很简单:先安装NLog.Extensions.Logging包,在项目根目录创建nlog.config配置文件并设置为“如果较新则复制”,在配置文件里定义输出目标,比如按日期命名的日志文件,设置单个文件大小和最大归档数量防止磁盘占满,再定义输出规则实现按级别、按命名空间分类日志。最后在AddLogging中调用AddNLog()方法,就能同时输出到控制台和文件了。NLog还支持配置自动重载、写入数据库、发送邮件等高级功能,完全满足生产环境的需求。
10. 什么是ORM?
ORM全称是对象关系映射,简单说就是在C#对象和关系数据库之间搭一座桥。以前我们用ADO.NET要手写SQL语句,现在用ORM可以直接操作C#对象来完成数据库操作。比如创建一个User对象,调用Save方法,ORM会自动帮我们生成INSERT语句插入数据库;写LINQ查询,ORM会转成对应的SELECT语句。
EF Core就是微软官方的ORM框架,底层其实还是封装了ADO.NET,它支持SQL Server、MySQL、Oracle等主流数据库,能大大提升开发效率。
11. EFCore性能比较低,你是怎么理解的?
我觉得说EF Core性能低是一个常见的误解。首先,如果开发人员对EF Core有深入了解,完全可以写出高性能的代码,EF Core本身也提供了很多性能优化的功能。其次,EF Core支持直接执行原生SQL语句,遇到性能瓶颈或者复杂查询的场景,我们可以手写优化后的SQL来解决。
其实EF Core最大的优势是开发效率极高,能帮我们快速完成项目。实际开发中我们要综合考虑性能、开发效率和可维护性,大部分常规场景EF Core的性能完全够用,只有少数瓶颈环节需要针对性优化。
12. EFCore中一对多,多对多,一对一配置
EF Core配置关系统一用HasXXX().WithYYY()的模式:
- 一对多:比如文章和评论,在评论的配置类里写
builder.HasOne<Article>(c => c.Article).WithMany(a => a.Comments),EF Core会自动在评论表生成外键。 - 一对一:比如订单和快递信息,必须显式指定外键在哪边,写法是
builder.HasOne<Delivery>(o => o.Delivery).WithOne(d => d.Order).HasForeignKey<Delivery>(d => d.OrderId)。 - 多对多:比如学生和老师,写法是
builder.HasMany<Teacher>(s => s.Teachers).WithMany(t => t.Students).UsingEntity(a => a.ToTable("T_Students_Teachers")),EF Core会自动生成中间表,我们不需要写中间表的实体类。
13. 什么情况下使用单向导航?
当一个实体被很多其他实体引用的时候,适合用单向导航。比如用户实体,可能被请假单、采购单、提货单等很多实体引用,如果用双向导航,用户实体里就要加很多对应的集合属性,会变得非常臃肿。
这时候我们只在从实体(比如请假单)里添加指向用户的导航属性,用户实体里不添加任何反向导航。配置的时候WithMany()方法不传参数就行。一般来说,被很多表引用的基础表,都推荐用单向导航。
14. IEnumerable与IQueryable的区别
首先IQueryable继承自IEnumerable,它们最核心的区别是数据过滤的位置不同:
- IEnumerable是在应用程序内存中过滤数据,它会先把数据库里的所有数据加载到内存,然后再执行过滤条件。
- IQueryable是把过滤条件转换成SQL语句,在数据库服务器上执行过滤,只返回符合条件的数据。
举个例子,如果我们写ctx.Comments.Where(c => c.Message.Contains("好")),如果转成IEnumerable,会查所有评论到内存再过滤;如果用IQueryable,会生成带WHERE子句的SQL,性能好很多。所以EF Core开发中尽量用IQueryable。
15. IQueryable的延迟性
IQueryable代表一个待执行的查询,它不会立即执行,只有调用立即执行方法的时候,才会生成SQL语句访问数据库。
立即执行方法包括ToList、ToArray、Count、FirstOrDefault、Min、Max等,这些方法的返回值不是IQueryable类型。而Where、OrderBy、GroupBy、Include、Skip、Take这些方法是非立即执行的,它们只是拼接查询条件,返回IQueryable。
延迟执行最大的好处是可以动态拼接查询条件,比如根据用户输入的不同参数,动态添加Where、OrderBy,最后一次执行查询,不用手动拼接SQL字符串。
16. IQueryable遍历读取数据的时候,用的是DataReader的方式还是DataTable的方式?怎样进行验证的?
IQueryable遍历用的是类似DataReader的流式读取方式,它会分批次从数据库读取数据,不会一次性把所有数据加载到内存。
验证方法很简单:在遍历IQueryable的过程中,断开数据库连接,程序会报错,因为它还在从数据库读取数据。如果我们先调用ToList()方法,会一次性把所有数据加载到内存,这时候再断开连接,遍历就不会报错了。
另外,多个IQueryable嵌套遍历会报错,因为同一个数据库连接不能同时打开多个DataReader,这也能证明它用的是DataReader方式。
17、以下程序执行结果
static IQueryable<Student> QueryStudents()
{
using (TestContext ctx =new TestContext())
{
return ctx.Students.Where(s => s.Id > 0);
}
}
foreach (var student in QueryStudents())
{
Console.WriteLine(student.Name);
}
这段代码会抛出异常:Cannot access a disposed context instance。
原因是IQueryable的延迟执行特性:QueryStudents方法里用了using语句,方法执行结束后,TestContext对象就被释放了。而返回的IQueryable只是一个查询计划,并没有真正执行。当我们在foreach里遍历的时候,才会去数据库查询数据,这时候Context已经销毁,无法建立数据库连接,所以会报错。
解决方法是在方法里调用ToList(),把数据加载到内存再返回,返回类型改成IEnumerable<Student>。
18. 为啥FirstOrDefault有对应的异步方式,而GroupBy,OrderBy,Where等方法没有异步方式?
异步方法的作用是处理耗时的IO操作,避免线程阻塞,提升系统并发量。
FirstOrDefault、Count这些是立即执行方法,它们会生成SQL语句访问数据库,属于IO密集型操作,所以需要对应的异步方法。而GroupBy、OrderBy、Where这些是非立即执行方法,它们只是在内存中拼接表达式树,不会访问数据库,没有IO操作,执行速度非常快,所以不需要异步方法。
19. EFCore优化方式?
我常用的优化方式有这几种:
- AsNoTracking:如果查询出来的数据只用于展示,不修改,就加上AsNoTracking禁用变更跟踪,能减少内存和CPU开销。
- 投影查询:用Select只查询需要的列,不要查整个实体的所有字段。
- 显式外键:只需要外键值的时候,不用Include关联主表,直接查外键字段。
- 批量操作:EF Core 7+提供了ExecuteUpdateAsync和ExecuteDeleteAsync,不用先查再改,直接批量更新删除。
- 原生SQL:复杂查询或性能瓶颈处,用参数化的原生SQL。
- 合理使用Include:避免过度Include不需要的关联数据。
20. 除了EFCore还使用哪些ORM框架?与EFCore相比优势是什么?
我还用过Dapper,它是一个轻量级的ORM框架。和EF Core相比,Dapper最大的优势是性能极高,接近原生ADO.NET,因为它只做了最基本的对象映射,没有EF Core的变更跟踪、自动迁移这些复杂功能。
Dapper的学习成本很低,代码非常灵活,特别适合写复杂的SQL查询,比如报表查询。不过它需要自己手写SQL,没有自动迁移功能,开发效率比EF Core低。
实际项目中我们一般混合使用:常规的CRUD用EF Core,复杂查询和性能要求高的地方用Dapper。
21、以下代码为什么会生成update这条sql语句
var student =await ctx.Students.FirstOrDefaultAsync();
student.Name = "张三三";
await ctx.SaveChangesAsync();
这是因为EF Core默认使用快照更改跟踪机制。当我们用FirstOrDefault查询出student对象的时候,DbContext会创建这个对象的一个快照,保存所有属性的原始值。
当我们修改Name属性后,调用SaveChangesAsync时,EF Core会把当前对象的属性值和快照里的原始值进行比较,发现Name属性发生了变化,就会生成对应的UPDATE语句,把修改同步到数据库。
22. 实体类有哪些状态?含义分别是什么?在实际开发中有什么用?
EF Core的实体有5种状态:
- Added:已添加,上下文正在跟踪,数据库中还没有这条数据,SaveChanges时会执行INSERT。
- Unchanged:未改变,上下文跟踪,数据存在于数据库,属性值和原始值一致。
- Modified:已修改,上下文跟踪,部分属性已更改,SaveChanges时执行UPDATE。
- Deleted:已删除,上下文跟踪,SaveChanges时执行DELETE。
- Detached:分离,上下文不跟踪这个实体,和数据库无关。
实际开发中,我们可以手动设置实体状态来优化性能,比如直接创建一个对象,把状态设为Deleted,不用先查询再删除,减少一次数据库查询。
23. 什么是悲观锁?什么是乐观锁?有什么区别?EFCore如何处理并发?
- 悲观锁:认为并发冲突一定会发生,操作数据时先加排他锁,其他操作必须等待锁释放才能执行。优点是不会有冲突,缺点是性能差,容易死锁,不适合高并发。EF Core没有封装悲观锁,需要手写原生SQL,比如加
WITH (UPDLOCK)。 - 乐观锁:认为并发冲突很少发生,操作时不加锁,更新时检查数据是否被别人修改过。如果冲突就抛出异常。优点是性能高,适合读多写少的场景。
EF Core实现乐观锁有两种方式:一是把并发字段用IsConcurrencyToken()标记为并发令牌;二是SQL Server可以用IsRowVersion(),数据库会自动维护这个列,每次更新自动生成新值。
24. 什么是表达式树?
表达式树是用树形数据结构来表示代码逻辑的技术,在.NET中对应Expression<TDelegate>类型。
当我们写一个Lambda表达式,比如b => b.Price > 5,如果把它赋值给Expression<Func<Book, bool>>类型,编译器会把它解析成一棵抽象语法树,也就是表达式树。EF Core就是通过遍历这棵树,把C#的查询逻辑转换成对应的SQL语句。
如果把Lambda赋值给普通的Func委托,就不会生成表达式树,这时候EF Core无法转成SQL,会把所有数据加载到内存再过滤。
25. 什么是事务?事务的特性是什么?EFCore中怎样使用事务?
事务是数据库操作的最小工作单元,是一组要么全部成功、要么全部失败的操作。事务有四个特性,简称ACID:原子性、一致性、隔离性、持久性。
事务的ACID特性:
- 原子性(Atomicity) :事务是“最小执行单元”,不可拆分。一个事务要么全部成功,要么全部失败回滚(如转账中“扣款失败”则“加款”也回滚)。
- 一致性(Consistency) :事务执行前后,数据库状态保持一致(如转账前后,双方总金额不变)。
- 隔离性(Isolation) :多个事务并发执行时,彼此隔离,互不干扰(如事务A修改数据时,事务B看不到未提交的修改)。
- 持久性(Durability) :事务提交后,修改永久保存到数据库,即使数据库崩溃也不会丢失(如提交后断电,重启后数据仍存在)。
EF Core中有三种使用事务的方式:
- SaveChanges自带事务:所有变更会在一个事务中提交,要么全成要么全回滚,适合单上下文的简单场景。
- DbContextTransaction:手动开启事务,用
db.Database.BeginTransaction(),适合多个SaveChanges或执行原生SQL的情况。 - TransactionScope:分布式事务,支持多个DbContext或多个数据库,需要开启MSDTC服务。
26. 怎样保证在EFCore中对多个数据的操作,在一个事务中?
分三种情况:
- 同一个DbContext,单次SaveChanges:默认就是事务,所有操作要么一起成功要么一起失败。
- 同一个DbContext,多次SaveChanges:用
DbContextTransaction,先BeginTransaction()开启事务,所有操作完成后Commit(),异常时Rollback()。 - 多个DbContext或多个数据库:用
TransactionScope,把所有操作包在using (var trans = new TransactionScope())里,最后调用trans.Complete(),异常会自动回滚,需要开启MSDTC服务。
27. 简述EF Core的工作原理
EF Core是一个ORM框架,底层封装了ADO.NET,工作流程大概是这样的:
- 我们先定义实体类和DbContext,用Fluent API或数据注解配置实体和数据库表的映射关系。
- 通过迁移命令把实体模型生成对应的数据库表。
- 操作DbSet进行增删改查时,LINQ查询会被解析成表达式树。
- EF Core把表达式树转换成对应的SQL语句,通过ADO.NET发送给数据库。
- 数据库执行SQL返回结果,EF Core把结果映射成实体对象。
- 变更跟踪机制跟踪实体状态,SaveChanges时根据状态生成增删改SQL,在事务中提交。
28. 为什么使用EFCore而不使用原生Ado.net
主要有这几个原因:
- 开发效率高:不用写大量重复的SQL和数据映射代码,用面向对象的方式操作数据库,减少样板代码。
- 可维护性好:业务逻辑用C#写,比SQL更容易重构和维护,支持强类型检查,编译时就能发现错误。
- 数据库无关性:切换数据库只需要改连接字符串和提供程序,不用修改业务代码。
- 内置功能丰富:自动迁移、变更跟踪、事务、关联查询等功能都不用自己实现。
- 性能平衡:大部分场景性能足够,瓶颈处可以用原生SQL,兼顾开发效率和性能。
29. 怎样查看EFCore生成的SQL语句?
常用的有两种方法:
- 简单日志:在DbContext的OnConfiguring方法里加一行
optionsBuilder.LogTo(Console.WriteLine),这样所有执行的SQL语句都会打印到控制台,开发时非常方便。 - SQL Server Profiler:SQL Server自带的工具,可以捕获所有发送到数据库的SQL语句,适合查看复杂查询或生产环境的SQL。
30. 什么是SQL注入,如何防止SQL注入攻击?EF Core中执行sql语句怎样防止sql注入?
SQL注入是攻击者在用户输入中插入恶意SQL代码,让数据库执行非预期操作,比如删除表、窃取数据。防止SQL注入的核心是使用参数化查询,绝对不要用字符串拼接SQL。
EF Core中防止SQL注入的方法:
- 常规LINQ查询是安全的,EF Core会自动生成参数化SQL。
- 执行原生SQL时,一定要用
ExecuteSqlInterpolatedAsync和FromSqlInterpolated方法,这两个方法接受FormattableString类型,会自动把插值变量转换成SQL参数。比如$"insert into T_Teachers values ({uname})",这里的{uname}会变成@p0参数,不会有注入问题。 - 不要用
ExecuteSqlRaw,它需要自己处理参数,很容易出错。
31、什么是MVC?MVC是三层吗?
MVC是Web应用的表示层设计模式,将应用拆分为三个核心部分:
- Model(模型) :存储视图需要展示的数据,也叫ViewModel,只包含页面用到的字段
- View(视图) :用户看到的页面,负责渲染HTML、CSS、JS内容
- Controller(控制器) :中间层,接收用户请求、处理业务逻辑,再把数据交给视图返回
MVC不是三层架构:三层指数据访问层、业务逻辑层、表示层,MVC是表示层内部的拆分方式,用来解决表示层代码耦合、职责不清的问题。
32、get请求与post请求区别
核心区别有5点:
- 数据位置:get把数据拼在URL后,会暴露在地址栏;post把数据放在请求体中
- 安全性:get明文传输,不适合传密码等敏感信息;post相对安全
- 数据大小:get受浏览器限制,一般只能传几KB;post理论上无大小限制,支持传文件
- 用途:get是幂等的,用来从服务器获取数据;post用来向服务器提交数据(新增、修改)
- 缓存:get请求会被浏览器缓存;post请求不支持缓存
33、什么是http协议?有什么特点?
HTTP全称超文本传输协议,是W3C制定的浏览器与服务器之间数据传输的标准规范,属于应用层协议。 核心特点:
- 基于TCP协议:面向连接,保证数据传输的可靠性
- 请求-响应模型:一次请求对应一次响应,没有请求就没有响应
- 无状态协议:协议本身不保存之前的请求/响应信息,每次请求都是独立的,需要用Cookie、Session来保持用户状态
34、http协议的组成
HTTP协议分为请求协议(浏览器发服务器)和响应协议(服务器发浏览器)两部分,结构类似:
-
请求协议:
- 请求行:包含请求方法、URL、HTTP版本
- 请求头:键值对形式,描述请求信息(如User-Agent、Accept)
- 空行:分隔请求头和请求体
- 请求体:post请求的数据在此,get请求无请求体
-
响应协议:
- 响应行:包含HTTP版本、状态码、状态描述
- 响应头:键值对形式,描述响应信息(如Content-Type、Date)
- 空行:分隔响应头和响应体
- 响应体:返回的实际内容(如HTML、JSON、文件)
35、常见状态码含义
状态码是三位数字,分为5大类,核心常见的:
- 1xx(信息提示) :服务器正在处理请求,如100 Continue
- 2xx(成功) :请求处理成功,如200 OK
- 3xx(重定向) :需要客户端进一步操作,如301永久重定向、302临时重定向、304资源未修改(用本地缓存)
- 4xx(客户端错误) :请求有问题,如404资源不存在、403无权限访问、405请求方法不允许
- 5xx(服务器错误) :服务器处理出错,如500内部服务器错误、503服务不可用
36、什么是https协议?(描述加密过程)
HTTPS是HTTP的安全版本,在HTTP和TCP之间加了SSL/TLS加密层,解决了HTTP明文传输易被窃听、篡改的问题。 加密过程采用混合加密(结合非对称和对称加密的优点):
- 服务器生成公钥+私钥,将公钥和域名交给可信认证机构(CA)
- CA用自己的私钥加密服务器信息,生成数字证书,发给服务器
- 浏览器发起请求,服务器返回数字证书
- 浏览器用系统内置的CA公钥解密证书,验证合法性,得到服务器公钥
- 浏览器生成随机会话密钥(对称加密密钥),用服务器公钥加密后发给服务器
- 服务器用自己的私钥解密,得到会话密钥
- 后续双方用会话密钥进行对称加密通信
37、文件上传注意事项
ASP.NET Core文件上传的核心注意点:
- 前端表单必须设置
enctype="multipart/form-data",否则无法传输文件 - 后端用
IFormFile接口接收,参数名必须和前端文件域的name一致;多文件用IFormFile[] - 文件名必须唯一,推荐用Guid+原扩展名,避免重名覆盖
- 严格限制文件大小:Kestrel默认28.6MB,可通过配置调整,业务层也要做校验
- 校验文件类型,只允许上传白名单格式,防止上传exe等恶意文件
- 文件存到
wwwroot目录下,才能通过静态文件中间件访问 - 处理用户未选择文件就提交的边界情况,给出友好提示
38、说一下AutoMapper的作用
AutoMapper是.NET生态中常用的对象映射库,核心作用是自动完成两个对象之间的属性映射,替代手动写大量赋值代码。 主要价值:
- 减少重复代码:不用手动逐个赋值属性,提高开发效率
- 分层解耦:将数据库实体(Entity)和前端需要的DTO分离,只暴露必要字段,提高安全性
- 支持复杂映射场景:前缀/后缀映射、不同名属性手动配置、集合映射、嵌套对象映射、双向映射等
39、什么是缓存?缓存使用方式?
缓存是系统优化最常用的手段,是一个高速数据存储区域,将经常访问且不常变化的数据存起来,下次访问直接从缓存读取,不用再查数据库,能极大提高响应速度、降低数据库压力。 ASP.NET Core中常用的缓存方式:
- 客户端响应缓存:给控制器方法加
[ResponseCache]特性,让浏览器缓存响应内容 - 服务端内存缓存:用
IMemoryCache接口,数据存在服务器内存,常用GetOrCreateAsync方法自动处理缓存命中/未命中 - 分布式缓存:如Redis,适合多服务器部署场景,保证缓存数据一致性
40、缓存过期策略
缓存过期策略用来解决缓存与数据库数据不一致的问题,常用两种:
- 绝对过期时间:从设置缓存开始,到指定时间自动过期,无论这段时间内有没有被访问
- 滑动过期时间:如果在过期时间内缓存被访问,过期时间自动续期;只有连续一段时间未被访问才会过期
适用场景:
- 绝对过期:大部分场景优先使用,能保证数据最多过期指定时间,不会长期不一致
- 滑动过期:适合访问频率极高、且数据几乎不变化的场景,能提高缓存命中率
41、缓存穿透问题
定义:用户大量请求数据库中根本不存在的数据,这些数据在缓存中也没有,导致每次请求都绕过缓存直接打到数据库,给数据库造成巨大压力,甚至被恶意攻击搞垮。 常见原因:恶意用户用不存在的ID频繁请求、业务逻辑错误。 解决方法:
- 空值缓存:将查询结果为空的情况也存入缓存,设置较短的过期时间(如1分钟),ASP.NET Core的
GetOrCreateAsync会自动处理null值 - 布隆过滤器:提前将所有存在的key存入布隆过滤器,请求时先过滤,不存在的直接返回,不用查缓存和数据库
42、缓存雪崩问题
定义:大量缓存数据在同一时间集中过期,导致这一瞬间所有请求都直接打到数据库,数据库压力瞬间飙升,甚至崩溃。 常见原因:缓存预热时所有数据设置了相同的过期时间、缓存服务器单点故障。 解决方法:
- 过期时间加随机偏移:给每个缓存的基础过期时间加一个随机值(如10分钟基础+0-5分钟随机),让缓存分散过期
- 高可用缓存集群:搭建Redis集群,防止缓存服务器单点故障
- 熔断降级:当数据库压力过大时,暂时返回默认值或友好提示,保护数据库不被打垮
43、什么是Filter? 什么是Aop?
面试官您好,Filter是ASP.NET Core提供的面向切面编程(AOP)机制,允许我们在请求处理的特定节点插入自定义代码,比如控制器方法执行前做数据校验、执行后统一处理结果,或是全局捕获异常。 而AOP(面向切面编程)的核心是解耦通用逻辑与业务逻辑,把日志、授权、异常、事务这些和业务无关的重复代码抽成独立“切面”,既减少代码冗余,又让业务代码更纯粹。Filter就是ASP.NET Core中实现AOP最常用的方式。
44、在项目中使用过Filter吗?具体描述一下
用过,我主要用了异常筛选器和ActionFilter两种:
- 异常筛选器:替代了每个控制器方法里的try-catch,全局捕获未处理异常。开发环境返回完整错误堆栈方便调试,生产环境返回友好提示,同时自动把异常信息写入NLog日志,大幅减少重复代码。
- ActionFilter:做了两个核心功能:一是自动事务封装,用TransactionScope实现,给需要事务的方法自动开启、提交/回滚事务,不用手动写事务代码;二是IP限流,限制同一个IP1秒内最多请求1次,超过返回429状态码,防止恶意刷接口消耗服务器资源
45、描述一下ActionFilter执行流程
ActionFilter基于管道模型执行,以异步的IAsyncActionFilter为例: 每个Filter实现OnActionExecutionAsync方法,包含两个参数:context(Action上下文,可获取请求路径、参数)和next(委托,指向下一个Filter或控制器方法)。 执行顺序是:按注册顺序执行所有Filter的next前逻辑→执行控制器方法→按注册逆序执行所有Filter的next后逻辑。 比如注册了Filter1、Filter2,流程就是:Filter1前→Filter2前→控制器方法→Filter2后→Filter1后。如果某个Filter不调用next(),会直接终止流程,控制器方法不会执行。
46、什么是中间件?
中间件是ASP.NET Core的核心请求处理组件,多个中间件会串联成一个“请求管道”。每个中间件都由三部分组成:前逻辑(请求进来时执行)、next委托(传递请求给下一个中间件)、后逻辑(响应返回时执行) 。 ASP.NET Core本身只做最基础的HTTP报文解析,所有业务相关的处理(静态文件、身份验证、路由、MVC、异常处理)都由中间件实现。我们可以自由调整中间件顺序、添加自定义中间件,非常灵活。
47、描述一下中间件执行流程
中间件也是管道模型,举个简单例子:假设注册了Use1、Use2、Run三个中间件。
- 请求进来后,先匹配对应的管道(比如
Map("/test")只处理/test路径的请求); - 按注册顺序执行:Use1前逻辑→调用next→Use2前逻辑→调用next→Run(管道终点,执行核心逻辑,不再往后传);
- 响应返回时,按逆序执行后逻辑:Use2后逻辑→Use1后逻辑→返回给客户端。 如果某个中间件不调用
next(),会发生“短路”,比如静态文件中间件找到文件后直接返回,不会走到后面的MVC中间件。
48、中间件与Filter有什么区别?
核心区别是所处层级不同:中间件是ASP.NET Core基础层的机制,Filter是MVC中间件内部的功能,属于MVC管道的一部分。由此带来三个关键差异:
- 处理范围:中间件能处理所有请求(静态文件、API、控制器等),Filter只能处理控制器/Action的请求;
- 执行效率:中间件更底层,比如限流用中间件,超限请求会在最早期被拦截,不用走路由、模型绑定等步骤,更省资源;
- 适用场景:全局通用功能(全局日志、异常、限流)优先用中间件;和MVC强相关的功能(需要Action信息、模型验证结果)用Filter。实际项目中一般结合使用。
49、什么是cookie? 可以使用cookie解决什么问题?
Cookie是存储在客户端浏览器的一小段明文文本,和特定网站绑定。当浏览器向该网站发请求时,会自动把对应Cookie放到请求头中发送给服务器。 它主要解决HTTP协议无状态的问题(每个请求都是独立的,服务器无法识别是否来自同一个用户)。常见场景:记住登录状态(“记住我”)、保存用户偏好(主题、语言)、跟踪用户行为、存储Session的会话标识(SessionID)。
50、请求是子域的网页,那么主域的cookie会不会发送到后台?
会的。请求子域时,主域下设置的Cookie会自动随请求发送到后台;但反过来,请求主域时,子域的Cookie默认不会发送。 如果想让子域的Cookie在请求主域时也生效,只需要在设置Cookie时,把Domain属性显式设为主域即可。比如主域是example.com,子域是a.example.com,设置Domain=example.com,这个Cookie在两个域名下都能被访问。
51、cookie有什么问题?
Cookie主要有5个问题:
- 安全性差:明文存储在客户端,容易被窃取、篡改,绝对不能存密码、身份证等敏感信息;
- 大小限制:单个Cookie最大4KB,每个网站最多存20-50个,不适合存大量数据;
- 性能损耗:每个请求都会自动携带Cookie,即使是静态资源请求,增加了请求体积;
- 跨域限制:受同源策略限制,不同域名默认不能共享Cookie;
- 可被禁用:用户可以关闭浏览器的Cookie功能,导致依赖Cookie的功能(登录、Session)失效。
52、什么是session?用来解决什么问题
Session是服务端的会话存储机制,同样用来解决HTTP无状态的问题。 用户第一次访问时,服务器会生成唯一的SessionID,通过Cookie发给客户端。后续每次请求,客户端都会带上这个SessionID,服务器通过它找到对应用户的Session数据,从而在多个请求之间保持用户状态。 主要用来存储用户相关的临时数据:登录信息、购物车、验证码、临时操作状态等。比如用户登录后,把用户信息存在Session里,后续请求不用每次都输密码。
53、session与cookie有什么区别?
| 维度 | Cookie | Session |
|---|---|---|
| 存储位置 | 客户端浏览器 | 服务器内存/分布式存储 |
| 安全性 | 低(明文易篡改) | 高(数据在服务端) |
| 存储容量 | 单个最大4KB | 无限制(受服务器资源) |
| 生命周期 | 可设置长期过期 | 默认会话级(关闭浏览器失效) |
| 存储类型 | 只能存字符串 | 可存任意对象(需序列化) |
| 依赖关系 | 独立 | 依赖Cookie存储SessionID |
54、session有什么问题?
Session主要有4个问题:
- 服务器内存压力:默认存在服务器内存,用户量大会占用大量内存,影响性能;
- 分布式部署问题:多台服务器时,默认Session不能跨服务器共享,需要用Redis等做分布式Session;
- 依赖Cookie:SessionID存在Cookie中,用户禁用Cookie会导致Session失效;
- 过期问题:默认20分钟无操作就过期,用户需要重新登录,影响体验;且SessionID被窃取后,攻击者可冒充用户操作。
55、怎样将一个对象存储到session中?
ASP.NET Core的Session默认只能存字符串和字节数组,所以需要先序列化对象再存储,读取时反序列化。常用两种方式:
- JSON序列化:用Newtonsoft.Json。存的时候
JsonConvert.SerializeObject(obj)转成JSON字符串,调用Session.SetString(key, json);取的时候Session.GetString(key)再反序列化。 - Protobuf序列化:二进制格式,比JSON更小更快。给实体类加
[ProtoContract]和[ProtoMember]特性,用Serializer.Serialize(ms, obj)转成字节数组,调用Session.Set(key, bytes);读取时反序列化。 一般会封装SessionHelper类,提供泛型的Set<T>和Get<T>方法,简化调用。
56、什么是ajax?有什么好处?有什么问题?
AJAX全称是异步JavaScript和XML,是一种在不刷新整个网页的情况下,与服务器交换数据并更新局部页面的技术。 好处:① 局部刷新,用户体验好;② 减少不必要的网络传输,节省带宽和服务器资源;③ 实现更丰富的交互(实时搜索、表单提交不刷新、无限滚动)。 问题:① 破坏浏览器前进后退功能;② 对搜索引擎不友好,爬虫无法爬取动态内容;③ 存在跨域问题,受同源策略限制;④ 调试相对麻烦,需要查看网络请求。
57、ajax的核心对象
AJAX的核心是XMLHttpRequest(XHR)对象,所有AJAX请求都是基于它实现的。 XHR提供了完整的HTTP请求能力:用open()设置请求方法和URL,setRequestHeader()设置请求头,send()发送请求,通过onload事件监听响应,用response获取服务器返回的数据。 现在常用的axios、fetch等库,都是对XHR的封装,简化了API,提供了Promise支持,同时解决了浏览器兼容性问题。
58、什么是工作单元模式?有什么好处?在项目中是怎样使用的,解决了什么问题?
工作单元模式是一种数据访问设计模式,核心是维护一个统一的事务上下文,将同一个业务请求内的多个数据库操作打包成一个原子事务,要么全部成功提交,要么全部失败回滚。
它的核心好处有三点:一是保证数据一致性,避免部分操作成功、部分失败导致的数据错乱;二是提升性能,减少数据库连接和提交次数,避免频繁调用SaveChanges;三是解耦业务与持久化,业务层不用手动管理事务,专注于业务逻辑。
在我们的CMS项目中,我通过ActionFilter+自定义Attribute实现了轻量的工作单元:首先创建UnitOfWorkAttribute,只标记需要事务的控制器方法;然后编写UnitOfWorkFilter,在控制器方法执行完成后自动获取DbContext并统一提交,如果方法抛出异常则终止提交。
这解决了原来的痛点:之前每个仓储方法都单独调用SaveChanges,比如同时修改两个用户信息会提交两次数据库,不仅性能差,还可能出现一个成功一个失败的不一致问题。现在一个请求内的所有数据库操作只提交一次,既保证了事务,又简化了代码。
59、静态页有什么好处?
静态页是将动态生成的页面(比如从数据库查数据渲染的文章页)提前生成固定的HTML文件,用户访问时直接由Web服务器返回,不用经过后端应用和数据库。
它的核心优势非常突出:
- 访问速度极快:静态文件由Nginx/IIS直接返回,响应时间比动态页快几十上百倍,用户体验更好;
- 大幅降低服务器负载:不用执行后端代码、不用查询数据库,能支撑更高的并发量;
- 提升SEO效果:搜索引擎对静态页面的收录和排名更友好,更利于网站推广;
- 安全性更高:静态页面不会触发后端代码漏洞,从根源上避免了SQL注入、命令执行等风险;
- 稳定性极强:即使后端应用服务器或数据库宕机,静态页面依然能正常访问,保证网站基础服务可用。
在我们的CMS项目中,文章发布时会用NVelocity模板引擎自动生成静态HTML,用户访问文章时直接读取静态文件,性能提升非常明显。
60、什么是XSS?
XSS全称跨站脚本攻击(Cross-Site Scripting) ,是最常见的Web安全漏洞之一。它的原理是:攻击者在网页中注入恶意JavaScript脚本,当其他用户访问该页面时,脚本会在用户的浏览器中执行,从而窃取用户的Cookie、会话令牌,或者篡改页面内容、诱导用户跳转到钓鱼网站。
XSS主要分为三类:
- 存储型:最危险,恶意脚本会永久存储在服务器数据库中(比如文章内容、评论区),所有访问该页面的用户都会被攻击;
- 反射型:脚本通过URL参数传递,后端直接将参数返回给前端执行,通常通过钓鱼链接传播;
- DOM型:脚本直接修改页面的DOM结构,攻击过程完全在前端完成,不经过后端。
防护的核心措施包括:对用户输入进行严格过滤和转义(比如转义<、>、&等特殊字符)、前端渲染时进行HTML编码、使用CSP内容安全策略限制外部脚本加载、给Cookie设置HttpOnly属性防止被窃取。
61、什么是线程安全和线程非安全?
线程安全和线程非安全是针对多线程环境下共享资源的访问来说的。
线程安全指的是:当多个线程同时访问同一个共享资源(变量、方法、对象)时,不会出现数据不一致或结果错误的情况。这是因为线程安全的实现会通过加锁、原子操作、CAS等机制,保证同一时间只有一个线程能操作该资源。比如C#中的ConcurrentDictionary、Java中的Vector和StringBuffer都是线程安全的。
线程非安全则相反:多线程同时访问时没有任何同步机制,会导致数据错乱。比如C#中的List、StringBuilder,Java中的ArrayList都是非线程安全的。
需要注意的是,线程安全不是没有代价的:加锁会带来性能开销。因此在单线程环境下,使用非线程安全的类性能更好;只有在多线程共享资源的场景下,才需要使用线程安全的实现。
62、ASP.NET Core中如何读取静态文件
在ASP.NET Core里,静态文件默认存放在项目的wwwroot目录下,读取主要分为前端直接访问和后端代码读取两种场景,在这个CMS项目中,这两种方式我们都有实际使用。
首先是最常用的前端直接访问:第一步要在Program.cs里开启静态文件中间件,也就是添加app.UseStaticFiles();这行代码,开启后默认会映射wwwroot目录。比如我们项目里的用户头像、miniui的js和css文件、文章静态HTML,都放在wwwroot对应的子目录中,前端直接用/images/xxx.jpg、/js/miniui.js这种相对路径就能访问。如果要访问wwwroot之外的自定义目录,也可以在UseStaticFiles里配置FileProvider和RequestPath,指定物理路径和访问路由,就能对外暴露自定义文件夹里的静态文件。
然后是后端代码中读取静态文件:比如我们生成文章静态页时读取模板文件、上传头像时处理图片文件,这时候需要注入IWebHostEnvironment接口,通过它的WebRootPath属性拿到wwwroot的绝对物理路径,再用Path.Combine拼接文件的相对路径,最后通过FileStream、File.ReadAllText这类API,就能读取到文件的完整内容。
另外有个关键注意点,静态文件中间件必须放在路由、鉴权这些中间件之前,否则会导致静态文件无法正常访问。
63、什么是REST/Restful
REST和Restful本质是同一个概念,只是叫法不同,它是一套Web API的设计风格与架构规范,核心就是严格遵循HTTP协议的原生语义来设计接口,和传统把HTTP只当传输通道的RPC风格接口有本质区别。
它的核心设计原则主要有这几点:
- 域名:应该尽量将API部署在专用域名之下。如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。(我们默认创建的WebApi项目符合这个规范)
- 版本:应该将
API的版本号放入URL。 - 路径:用URL定位资源,路径里只用名词(通常用复数),不用动词,和数据库的表名对应,比如
/users、/users/5; - 对于资源的具体操作类型用HTTP谓词定义操作,GET对应查询、POST对应新增、PUT对应全量更新、PATCH对应局部更新、DELETE对应删除;
- 状态码:用标准HTTP状态码表达接口处理结果,比如201代表创建成功、404代表资源不存在,让接口具备自描述性,语义更清晰,也降低了前后端的协作成本。
64、REST优缺点
REST风格的优势很明确,主要有三点:第一,语义清晰、接口自描述性强,通过URL就能知道操作的资源,通过HTTP谓词就能知道操作类型,不用看详细文档也能理解接口用途,大幅降低前后端协作成本;第二,规范统一,用标准HTTP状态码做结果反馈,错误处理机制统一,团队开发的接口风格一致,后续维护性更好;第三,天然支持服务扩展,REST接口是无状态的,服务端不保存会话信息,非常适合分布式架构和服务的横向扩展。
它的缺点也同样突出:第一,对设计人员的业务和技术能力要求很高,真实业务里的资源关系非常复杂,很难把所有业务都完美抽象成“资源+HTTP谓词”的形式;第二,灵活性不足,很多包含多步操作的复合业务,没法简单对应到某一个固定的HTTP谓词;第三,表达能力有限,HTTP标准状态码数量有限,复杂的业务错误场景,仅靠状态码没法完整描述;最后,纯REST风格在国内落地场景有限,大多团队都会根据业务做裁剪,不会完全严格遵循。
65、put请求与patch请求有什么区别?
PUT和PATCH都是HTTP协议里用于资源更新的请求方式,核心区别在于更新的粒度、语义和使用场景完全不同。
首先是核心语义和更新粒度不同:PUT是全量更新,它要求客户端必须传递资源的完整字段,相当于SQL里的全字段UPDATE,请求里没传递的字段,会被覆盖成空值或默认值;而PATCH是局部更新,客户端只需要传递想要修改的字段,没传递的字段会保留数据库里的原有值,不会被改动。
其次是使用场景不同:如果要修改资源的全部信息,比如完整的用户资料编辑页,用PUT更符合语义;如果只需要修改个别字段,比如只改用户昵称、只重置密码,用PATCH更合适,不用传递完整的资源数据,也能减少网络传输的开销。
最后还有一个细节,PUT请求是天然幂等的,多次执行相同的PUT请求,最终的资源结果是一致的;而PATCH请求通常也会设计成幂等的,但如果用了增量更新的逻辑,可能会出现非幂等的情况,这是接口设计时需要注意的点。
66、jwt的组成有那几部分
JWT全称是JSON Web Token,是一种用于前后端身份认证的令牌规范,它由三部分组成,之间用英文点号分隔,分别是Header头部、Payload负载、Signature签名。
第一部分是Header头部,它是一个JSON对象,主要描述JWT的元数据,核心有两个字段:alg代表签名使用的算法,默认是HS256;typ代表令牌类型,固定值为JWT,最终这个JSON会经过Base64URL编码成字符串。
第二部分是Payload负载,也是JSON对象,是JWT的核心数据体,用来存放需要传递的信息。里面分为两类字段,一类是JWT官方预留的标准字段,比如签发人iss、过期时间exp、签发时间iat;另一类是我们自定义的私有字段,比如用户ID、用户名。这部分同样会经过Base64URL编码,这里要特别注意,Base64只是编码不是加密,所以Payload里绝对不能存放密码这类敏感信息。
第三部分是Signature签名,它是对前两部分编码后的内容,用服务端独有的密钥、配合头部指定的算法生成的哈希签名。它的核心作用是防止数据被篡改,确保令牌的完整性,只有持有密钥的服务端,才能生成和验证合法的JWT签名。
67、jwt的安全性有哪些建议
JWT的安全性防护,核心围绕防篡改、防泄露、防滥用三个核心方向,主要有这几点关键建议:
第一,做好密钥的安全管理。签名用的密钥必须足够复杂、长度足够,不能用简单字符串,而且密钥必须只保存在服务端,绝对不能泄露到前端,同时要建立密钥的定期轮换机制,降低密钥泄露后的风险。
第二,严控Payload的内容。Payload的Base64编码是可反向解码的,不是加密内容,所以绝对不能在里面存放密码、身份证号这类敏感信息,只放用户ID这类必要的非敏感标识信息。
第三,设置合理的过期时间。必须给JWT设置较短的exp过期时间,避免令牌被盗用后长期有效;同时可以搭配刷新令牌refresh token的机制,平衡安全性和用户体验,不用用户频繁登录。
第四,强制HTTPS协议传输。JWT必须在HTTPS协议下传输,防止在网络传输过程中被中间人劫持、窃取,避免令牌直接泄露。
第五,完善服务端的校验机制。服务端必须严格校验JWT的签名、签发者、受众、过期时间等所有字段,避免被伪造的令牌绕过校验;同时可以建立令牌黑名单机制,处理用户登出、权限变更后,还未过期的无效令牌。
68、什么是数据塑形?有什么好处?
数据塑形是后端API开发中的一项实用技术,简单来说,就是让客户端可以自定义选择后端接口返回的字段,前端通过URL的查询参数,指定自己需要的字段名,后端接口只返回这些指定的字段,而不是固定返回整个DTO对象的全量字段。
它的核心好处主要有三点: 第一,大幅减少网络传输开销,提升接口性能。比如一个用户DTO有20个字段,前端页面只需要用户名和手机号,通过数据塑形就只返回这两个字段,接口返回的数据量大幅减少,响应速度更快,在列表查询、移动端弱网场景下,效果会特别明显。
第二,提升接口的灵活性和通用性。不用为了不同的前端页面、不同的业务场景,单独开发多个返回不同字段的接口,一个接口就能适配不同的前端字段需求,减少了重复开发工作,也降低了后期接口的维护成本。
第三,降低数据泄露的风险,提升安全性。可以避免把前端不需要的敏感字段、内部业务字段返回给前端,减少了核心业务数据的暴露面,比如用户的内部状态标识、关联的敏感数据,前端不需要就不会返回,进一步提升了数据安全性。
69、什么是MVVM?
MVVM是Model-View-ViewModel的缩写,是Vue遵循的核心架构模式。 其中Model是数据模型,对应我们从接口获取的业务数据;View是视图,也就是页面DOM结构;ViewModel是视图模型,是MVVM的核心,它作为中间层连接了View和Model,实现了两者的解耦。 它的核心特点是双向数据绑定:Model数据变化时,ViewModel会自动通知View更新;View里用户的操作触发数据变化时,ViewModel也会自动同步给Model。和传统手动操作DOM的开发模式不同,MVVM实现了数据驱动,开发者只需要关注数据逻辑,不用再手动操作DOM,大幅提升了开发效率。
70、什么是Vue组件化开发?组件化开发有什么好处?
Vue组件化开发,是基于封装的思想,把页面上可复用的功能模块,拆分成一个个独立的组件,每个组件都包含专属的HTML结构、CSS样式和JS逻辑,遵循单一功能原则,一个完整的页面就是由多个小组件层层组合而成的。 它的核心好处有三点:第一是易维护,每个组件的代码独立隔离,出现问题可以快速定位到对应组件,修改功能也不会影响其他模块;第二是可复用,重复的业务功能只需要封装一次,就能在项目里任意复用,不用重复写代码,大幅提升开发效率;第三是低耦合,组件之间职责清晰,多人协作开发互不干扰,也便于做单元测试,非常适合中大型项目的开发。
71、什么是SPA?这种开发方式的优点与缺点是什么?
SPA是单页应用程序(Single Page Application)的缩写,指的是整个应用的所有功能都在一个HTML页面上实现,页面切换不会触发浏览器整页刷新,只是通过路由动态切换组件、更新页面局部内容,Vue开发的项目默认就是SPA模式。 它的优点很突出:第一,页面切换无整页刷新,用户体验更流畅,每次请求只获取需要的业务数据,没有冗余的资源加载;第二,前后端完全分离,开发效率高,前端专注于页面逻辑,后端专注于数据接口,数据传递也更便捷;第三,组件化复用性强,代码维护成本更低。 同时它也有对应的缺点:第一,首屏加载速度较慢,因为首次打开需要加载整个应用的路由和资源;第二,不利于SEO,页面内容大多是JS动态渲染的,搜索引擎爬虫很难抓取到有效内容;第三,开发成本更高,需要学习路由、状态管理等相关的框架知识。
72、v-show与v-if的区别
v-show和v-if都是Vue里用来控制元素显示隐藏的指令,核心区别体现在实现原理、编译特性和使用场景上。 第一,实现原理不同:v-show是通过控制元素的CSS样式display: none来实现隐藏,元素始终存在于DOM树中;v-if是通过动态创建和销毁DOM节点来实现,隐藏时元素会被完全从DOM树中移除。 第二,编译特性不同:v-if是惰性的,初始值为false时,元素不会被编译和渲染;v-show不管初始值是什么,都会编译渲染元素,只是切换CSS样式。 第三,使用场景不同:如果元素需要频繁切换显示隐藏,优先用v-show,因为只修改CSS,性能开销很小;如果元素要么显示要么隐藏、不会频繁切换,优先用v-if,初始值为false时能节省初始渲染的开销。 另外补充一点,v-if可以配合v-else、v-else-if实现多分支逻辑,v-show不支持这个用法。
73、什么是v-model? 原理是什么?
v-model是Vue提供的、专门用于表单元素的指令,核心作用是实现双向数据绑定。双向绑定指的是:绑定的数据变化时,视图会自动更新;用户在表单里修改内容时,绑定的数据也会自动同步更新,能帮我们快速收集和设置表单数据。 它的底层原理是语法糖,本质是v-bind绑定属性 + v-on监听事件的组合。比如给文本输入框用v-model="msg",等价于:value="msg"绑定输入框的value属性,加上@input="msg = $event.target.value"监听输入框的input事件,实时同步输入内容。 针对不同的表单元素,v-model会自动适配对应的属性和事件:比如复选框会绑定checked属性、监听change事件,下拉选择框会绑定value属性、监听change事件,不用我们手动分别处理。
74、什么是虚拟DOM?Diff算法的作用
虚拟DOM,本质上是一个用来描述真实DOM结构的普通JS对象,它用最少的属性,记录了节点的类型、属性、子节点等和渲染相关的信息,和页面上的真实DOM一一对应。 之所以需要虚拟DOM,是因为真实DOM的属性非常多,直接遍历对比真实DOM找差异,性能损耗极大;而虚拟DOM只关注渲染相关的属性,对比的成本要低很多。 Diff算法,是对比新旧两棵虚拟DOM树差异的核心算法。它的核心作用是:当数据变化生成新的虚拟DOM后,Diff算法会高效对比新旧虚拟DOM的差异,只把有变化的部分更新到真实DOM上(也就是打补丁),而不是全量重绘整个DOM树,极大减少了DOM操作的次数,提升了页面渲染性能。 Diff算法有两个核心策略:一是只做同层级对比,不跨层级比较节点;二是兄弟节点对比时,通过key属性标识节点唯一性,提升节点复用的效率。
75、Composition API设计动机是什么?
Composition API是Vue3推出的组合式API,核心设计动机是解决Vue2中Options API(选项式API)在大型复杂项目中的核心痛点。 首先,Options API开发复杂组件时,同一个功能的逻辑代码,会被拆分到data、methods、computed、watch等不同的选项里,代码量变大之后,开发者需要反复滚动页面才能找到同一个功能的相关代码,可读性和可维护性极差。而Composition API可以把同一个功能的相关代码聚合在一起,逻辑更内聚、更易读。 其次,Options API的逻辑复用方案(比如mixin)存在明显缺陷:多个mixin之间容易出现命名冲突,而且复用的数据来源不清晰,不知道数据来自哪个mixin。Composition API可以通过自定义组合函数来复用逻辑,完全没有命名冲突的问题,数据来源也清晰可追溯。 另外,Composition API对TypeScript的类型推导非常友好,适配TS的成本比Options API低很多,更适合大型企业级项目的开发。
76、reactive与ref的区别?
reactive和ref都是Vue3中用来创建响应式数据的核心API,核心区别体现在适用数据类型、实现原理、使用方式三个方面。 第一,适用数据类型不同:reactive只支持引用类型数据,比如对象、数组、Map、Set,无法处理字符串、数字等基本数据类型;ref既支持所有基本数据类型,也支持引用类型数据。 第二,实现原理不同:reactive底层是通过ES6的Proxy实现响应式劫持,直接代理整个传入的对象;ref的底层本质还是reactive,它会把传入的基本类型数据包装成{ value: 数据 }的对象,再用reactive给这个对象做响应式代理。 第三,使用方式不同:在JS/TS代码中,ref创建的数据必须通过.value属性来访问和修改,模板中会自动解包,不用写.value;reactive创建的数据可以直接访问属性,不需要加.value。 另外还有一个关键差异:reactive不能直接替换整个对象,否则会丢失响应式;而ref可以直接给整个对象重新赋值,不会丢失响应式。
77、计算属性的作用?有什么优势?
计算属性是Vue中用来处理依赖数据动态计算的特殊属性,核心作用是基于已有的响应式数据,派生出需要经过计算才能得到的新值,比如列表的总分平均分、数据的筛选格式化、多条件判断等场景。 它的核心优势有两点: 第一,自带缓存机制,性能更优。计算属性会基于它的响应式依赖进行缓存,只有当依赖的数据发生变化时,才会重新执行计算逻辑;如果依赖没有变化,不管访问多少次,都会直接返回之前缓存的结果,不用重复计算,相比每次调用方法执行计算,性能提升非常明显。 第二,声明式写法更简洁,可维护性更强。把复杂的计算逻辑封装在计算属性里,模板里只需要直接使用属性名即可,不用在模板里写复杂的JS表达式,代码可读性更高,后期修改计算逻辑也只需要改一处,维护成本更低。 另外,计算属性默认是只读的,也可以通过getter和setter实现可写,适配双向绑定等更多场景。
78、侦听器的作用?
侦听器(watch)是Vue中用来监听响应式数据变化的API,核心作用是:当监听的目标数据发生变化时,自动执行我们预设的回调函数,在回调里处理数据变化后的后续操作。 它的核心使用场景,是处理数据变化带来的副作用逻辑:比如用户输入的搜索关键词变化时,发送异步ajax请求获取搜索结果;监听路由变化,重新加载页面数据;监听深层对象的属性变化,执行对应的业务逻辑;还有数据变化后操作DOM、设置定时器、清理副作用等场景。 同时watch提供了丰富的配置项:配置immediate: true可以实现页面初始化时就立即执行回调;配置deep: true可以实现对复杂对象的深度监听,不管嵌套多深的属性变化都能监听到,灵活性非常高。
79、计算属性与侦听器的区别?
计算属性和侦听器都是Vue中监听数据变化、处理响应式逻辑的能力,核心区别体现在定位场景、缓存特性、使用方式三个方面。 第一,核心定位和适用场景完全不同。计算属性的定位是“一个值由其他值派生而来”,专注于计算出一个新值,适合同步的、无副作用的数值计算、数据格式化、列表筛选等场景;侦听器的定位是“一个值变化了,要去做一些其他事情”,专注于数据变化后的副作用处理,比如异步请求、DOM操作、定时器等有副作用的逻辑,这是计算属性做不到的。 第二,缓存特性不同。计算属性有缓存机制,依赖的数据不变,就不会重新计算;侦听器没有缓存,每次监听的数据发生变化,都会执行对应的回调函数。 第三,使用方式不同。计算属性必须有返回值,返回值就是它的属性值,默认是只读的;侦听器不需要返回值,回调里只需要执行对应的业务逻辑即可,还支持深度监听、立即执行等配置。 日常开发中,能用计算属性实现的场景,优先用计算属性,性能更优;涉及异步、副作用的场景,必须用侦听器。
80、常见的组件通信方式
Vue中组件通信的方案,会根据组件之间的关系(父子、兄弟、跨层级、全局)来选择,常见的有这几种: 第一,父子组件通信,这是最基础的场景。父传子通过props属性,子组件通过props配置项接收父组件传递的数据;子传父通过自定义事件,子组件用$emit(setup里用emit)触发事件并传递数据,父组件通过v-on/@监听事件接收数据。另外也可以通过ref获取子组件实例,直接访问子组件的属性和方法。 第二,兄弟组件通信。简单场景可以通过共同的父组件做中转,先子传父、再父传子;更通用的方案是用mitt等第三方事件总线库,通过发布订阅的模式实现通信。 第三,跨层级/祖孙组件通信。用provide/inject依赖注入,父组件通过provide提供数据和方法,后代组件不管层级多深,都能通过inject接收,不用层层传递props。 第四,全局状态通信。用Vuex/Pinia全局状态管理库,集中管理所有组件都需要共享的状态,比如用户信息、购物车数据,所有组件都能访问和修改,适合无关联组件、多组件共享数据的场景。
81、dynamic动态组件应用场景
动态组件是Vue通过<component>标签配合is属性实现的能力,可以在同一个位置动态切换不同组件的渲染,核心应用场景是同一个容器需要频繁切换多个组件显示隐藏的场景。 最典型的场景有这几个: 第一,Tab标签页切换。比如页面顶部有多个Tab选项,下方同一个容器里,点击不同Tab要切换对应的内容组件,用动态组件比写多个v-if/v-show的写法更简洁、易维护,还能配合keep-alive缓存组件状态,避免切换时重复创建销毁。 第二,弹窗/抽屉的内容动态切换。比如同一个通用弹窗里,需要根据不同的业务步骤、不同的状态,切换展示表单组件、详情组件、结果组件,用动态组件能大幅简化代码。 第三,配置化页面渲染。比如后台管理系统的低代码页面,根据后端返回的模块类型、页面配置,动态渲染对应的业务组件,实现页面的配置化生成。 第四,复杂的步骤条/向导页。同一个页面里,不同步骤对应不同的表单组件,用动态组件切换步骤对应的内容,逻辑更清晰。
82、nextTick函数能解决什么问题
nextTick是Vue提供的全局API,核心解决的是Vue异步更新DOM机制带来的,数据修改后无法立即获取到最新DOM的问题。 首先要明确,Vue更新DOM是异步执行的:当我们修改了响应式数据,Vue不会立即同步更新DOM,而是会把所有数据修改操作缓存起来,等同一事件循环里的所有数据变化都完成后,再统一执行DOM更新,这样能避免频繁操作DOM,提升渲染性能。 但这就带来了一个问题:我们修改完数据后,马上同步去获取对应的DOM,拿到的还是更新前的旧内容。而nextTick的作用,就是把我们要执行的回调函数,延迟到DOM更新完成之后再执行。 它的典型使用场景有:1. 修改数据后,需要立即操作、获取更新后的DOM内容、尺寸、属性,比如获取列表的滚动高度;2. 组件创建/更新后,需要立即让输入框自动聚焦、初始化依赖DOM的第三方库(比如图表、富文本编辑器)。
83、插槽的应用场景
插槽(slot)是Vue的组件内容分发机制,核心作用是让父组件可以向子组件内部传递自定义的HTML结构/模板内容,实现组件内容的灵活定制,解决了组件内容写死、自定义性差的问题。 根据插槽的类型,有三个核心应用场景: 第一,默认插槽。用于组件只有一处需要自定义内容的场景,比如封装通用的对话框、卡片、按钮组件,组件的框架、基础样式是固定的,但主体内容需要父组件自定义传入,这时候用默认插槽就能实现。 第二,具名插槽。用于组件有多处需要分别自定义内容的场景,比如封装页面的布局组件,有头部、侧边栏、主体、底部多个区域,每个区域都需要父组件传入不同的内容,通过name属性给插槽命名,就能实现定向分发内容,分别定制不同区域。 第三,作用域插槽。用于父组件自定义内容时,需要使用子组件内部的数据的场景,比如封装通用的表格、列表组件,子组件里有列表数据,父组件需要根据每一行的数据,自定义单元格的渲染结构,这时候子组件可以通过插槽把内部数据传递给父组件,父组件就能基于子组件的数据自定义模板。
84、Vue生命周期分为哪几个阶段?对应的函数分别是什么?在这些函数中主要完成了哪些工作
Vue的组件生命周期,指的是组件从创建、挂载、更新到销毁的整个过程,分为4个核心阶段,每个阶段对应2个生命周期钩子函数,Vue3组合式API里对应on开头的钩子。
- 创建阶段,对应
beforeCreate和created。beforeCreate是组件实例刚初始化完成,data、methods、props都还没初始化,开发中基本不用;created里组件的data、methods、props都已经初始化完成,能正常访问数据和方法,这里可以发送不依赖DOM的异步ajax请求,做页面数据的初始化。 - 挂载阶段,对应
beforeMount和mounted。beforeMount里虚拟DOM已经创建完成,但还没渲染到真实页面,模板里的表达式还没被替换,开发中很少使用;mounted里组件已经挂载到页面,真实DOM渲染完成,这里可以操作DOM、获取DOM元素,发送依赖DOM的异步请求,初始化第三方DOM库。 - 更新阶段,对应
beforeUpdate和updated。beforeUpdate是响应式数据已经修改,即将重新渲染DOM,能拿到更新前的DOM;updated是DOM已经根据数据变化完成了重新渲染,能拿到更新后的最新DOM,这里可以做数据变化后的DOM操作,注意不要在这里修改数据,避免触发死循环。 - 销毁阶段,对应
beforeUnmount(Vue2是beforeDestroy)和unmounted(Vue2是destroyed)。beforeUnmount是组件即将销毁,实例还完全可用,这里可以做清理工作,比如清除定时器、解绑全局事件、取消订阅;unmounted是组件已经完全销毁,DOM也被移除,这里可以做最后的收尾工作,比如上报埋点。
85、什么是Vuex?Vuex解决了什么问题?
Vuex是Vue官方专为Vue.js设计的集中式状态管理工具,也就是一个全局的状态仓库,用来管理Vue应用中多个组件需要共享的响应式数据,并且规定了统一、可预测的方式来修改这些数据。 它主要解决了两个核心问题: 第一,解决了复杂组件之间的通信问题。尤其是跨层级组件、兄弟组件、无关联组件之间的数据共享和通信,不用再通过层层props传递、事件中转,所有组件都能直接从仓库里获取共享数据,修改也遵循统一的流程,数据流向更清晰。 第二,解决了多组件共享状态的维护问题。比如用户登录信息、购物车数据、全局配置这些,多个组件都要用到,如果分散在各个组件里,数据修改无法追踪,很容易出现数据不一致的问题。Vuex把这些数据集中管理,所有修改都必须通过提交mutation完成,配合Devtools能追踪到每一次数据变化,可维护性、可调试性大大提升。 另外要注意,Vuex适合大型单页应用,如果是小项目,没有大量全局共享数据,不用强行使用。
86、Vuex有哪些成员?这些成员的作用?
Vuex有5个核心成员,分别是State、Getter、Mutation、Action、Module,每个成员都有明确的职责,遵循单向数据流的规则。
- State:Vuex的数据源,用来存储全局的共享状态,所有组件都能访问这里的数据,它是响应式的,数据变化会自动同步到所有使用的组件。
- Getter:相当于Vuex里的计算属性,基于State里的数据派生出新的值,比如对列表做筛选、格式化,并且自带缓存机制,只有依赖的State数据变化才会重新计算。
- Mutation:Vuex里唯一允许修改State数据的方式,必须是同步函数,所有修改State的操作都要通过提交Mutation来完成,保证每一次数据变化都能被追踪,配合Devtools做调试。
- Action:用来处理异步操作,比如发送ajax请求,它不能直接修改State,内部需要通过提交Mutation来修改State数据。组件里通过dispatch来触发Action,适合处理包含异步逻辑的业务流程。
- Module:Vuex的模块化方案,当应用越来越大,全局状态越来越多时,可以把仓库拆分成多个独立的模块,每个模块都有自己的State、Getter、Mutation、Action,甚至可以嵌套子模块,解决了单一仓库状态臃肿、难以维护的问题。
87、说一下Vuex的模块化
Vuex的模块化,就是当Vue应用变得复杂、全局状态越来越多时,把单一的Store仓库,按照业务模块拆分成多个独立的子模块,每个子模块管理自己业务相关的状态和逻辑。 之所以需要模块化,是因为如果所有全局状态都放在一个Store里,随着项目变大,State会变得非常臃肿,所有的Mutation、Action都堆在一起,代码难以维护和阅读,模块化就是为了解决这个问题。 它的核心特点有这几点: 第一,每个模块都拥有自己独立的State、Getter、Mutation、Action,甚至可以嵌套子模块,和根模块的结构完全一致,职责和业务一一对应,比如用户模块、购物车模块、商品分类模块,代码结构更清晰。 第二,模块可以开启命名空间namespaced: true,开启后,模块的Getter、Mutation、Action都会被限制在模块内,访问、提交、派发的时候都需要加上模块名,避免了不同模块之间的命名冲突,实际项目里基本都会开启命名空间。 第三,模块化可以实现更灵活的持久化,配合vuex-persistedstate插件,可以指定只持久化部分模块的数据,比如只持久化用户模块和购物车模块,不用把整个仓库都持久化。
88、Vue中发送ajax请求一般在生命周期哪个函数中发送?
可以在created、beforeMount、mounted这三个钩子函数中发送异步请求,因为这三个钩子里,组件的data已经创建完成,可以把服务端返回的数据赋值给响应式数据。 其中最推荐的分两种情况: 第一种,如果异步请求不需要依赖页面的DOM元素,推荐在created钩子函数里发送。优点是能更早地获取到服务端数据,减少页面的loading时间,让用户更快看到内容,这也是实际开发中最常用的方式。 第二种,如果异步请求需要依赖DOM元素,比如请求回来的数据需要用来初始化图表、富文本编辑器等第三方DOM库,或者需要获取DOM的尺寸、位置作为请求参数,就需要在mounted钩子函数里发送,因为mounted阶段真实DOM已经渲染完成,能正常操作DOM了。 beforeMount里也可以发送请求,但实际开发中用的很少,created和mounted已经覆盖了绝大多数场景。
89、怎样理解 Vue 的单项数据流
Vue的单向数据流,指的是数据从父组件流向子组件的单向数据流动规则,也是父子组件props传递必须遵循的核心原则。 具体来说有两点核心规则:第一,父组件的数据发生变化时,会自动向下流动,同步更新到所有使用这个数据的子组件里,子组件会自动接收到最新的props数据。第二,子组件绝对不能直接修改父组件传递过来的props数据,props对于子组件来说是只读的,如果强行修改,Vue会在控制台抛出警告。 设计这个规则的核心目的,是为了保证数据流向的清晰和可追踪,避免多个子组件都修改同一个父组件的数据,导致数据变化的来源无法追溯,出现bug难以排查,尤其是项目变大之后,会让整个应用的数据流变得混乱。 如果子组件需要修改props传递过来的数据,有两种合规的处理方式:1. 把props的数据作为初始值,定义一个子组件自己的本地响应式数据来保存,后续修改这个本地数据;2. 通过触发自定义事件,把要修改的值传递给父组件,由父组件来修改源数据,再通过props同步回子组件,依然遵循单向数据流的规则。
90、Vue的父子组件生命周期钩子函数执行顺序
父子组件的生命周期执行顺序,核心规律是父组件先开始执行生命周期,子组件先完成生命周期,分4个核心场景:
- 首次加载渲染过程:父beforeCreate → 父created → 父beforeMount → 子beforeCreate → 子created → 子beforeMount → 子mounted → 父mounted。也就是父组件先走到挂载前,然后子组件走完完整的创建和挂载流程,最后父组件才完成挂载。
- 子组件更新过程:父beforeUpdate → 子beforeUpdate → 子updated → 父updated。子组件要更新,父组件先触发更新前钩子,子组件完成更新后,父组件再完成更新。
- 父组件更新过程(不影响子组件) :只会执行父beforeUpdate → 父updated,子组件的钩子不会触发。
- 销毁过程:父beforeUnmount → 子beforeUnmount → 子unmounted → 父unmounted。父组件先触发销毁前钩子,子组件先完成销毁,最后父组件再完成销毁。
91、Vue3.x 响应式数据原理是什么
Vue3的响应式原理,核心是基于ES6的Proxy代理配合Reflect反射实现的,替代了Vue2的Object.defineProperty,解决了Vue2响应式的诸多缺陷。 具体来说,Vue3通过Proxy创建一个代理对象,来劫持整个原始对象,而不是对象的单个属性,能够拦截对象属性的读取、修改、删除、新增,还有数组的下标修改、长度修改等所有操作;同时配合Reflect来执行对原始对象的默认操作,保证this指向的正确性。当读取代理对象的属性时,Vue会收集对应的依赖;当修改属性时,会触发对应的依赖更新视图,实现数据驱动。 相比Vue2的Object.defineProperty,它有三个核心优势: 第一,能劫持整个对象,而不是单个属性,不需要深度遍历对象的所有属性,对于嵌套对象采用懒劫持的方式,只有访问到嵌套属性的时候才会做代理,性能更好。 第二,完美支持数组的响应式,能监听到数组下标修改、长度修改,还有push、pop等所有数组方法的操作,解决了Vue2无法监听数组下标的问题。 第三,支持监听对象动态新增的属性、删除的属性,Vue2里需要通过Vue.set/Vue.delete才能实现,Vue3原生就支持。
92、Vuex 页面刷新数据丢失怎么解决?
Vuex里的数据是存储在内存中的,页面刷新的时候,整个应用会重新初始化,Vuex仓库里的数据就会重置丢失。解决这个问题的核心思路,是把Vuex里需要持久化的数据,同步保存到本地存储(localStorage/sessionStorage)里,页面刷新后再从本地存储里读取数据,恢复到Vuex中。 主要有两种实现方式: 第一种,手动实现原生持久化。在Vuex的Mutation里,每次修改State数据的时候,同步把数据保存到localStorage里;然后在Store初始化的时候,从localStorage里读取数据,作为State的初始值。这种方式不用依赖第三方插件,适合简单的场景。 第二种,使用vuex-persistedstate插件实现,这是项目里最常用、最便捷的方式。先安装插件,然后在Store的配置里引入并注册插件,可以自定义本地存储的key名,还能指定只持久化哪些模块的数据,比如只持久化用户模块和购物车模块。插件会自动监听State的变化,同步到本地存储,页面刷新后自动恢复数据,不用手动写存储和读取的逻辑。 另外要注意,token这类敏感数据,建议配合加密处理,或者用sessionStorage,关闭标签页就会清除,安全性更高。
93、v-for中的key的理解
v-for里的key属性,是Vue给循环渲染的节点设置的唯一标识,是虚拟DOM的Diff算法里非常核心的属性。 首先,Vue的Diff算法在对比新旧虚拟DOM的同级兄弟节点时,默认是按照下标来对比复用的;如果给节点设置了key,Diff算法就会按照相同的key来对比新旧节点,而不是按照下标。key的核心作用,是提高虚拟DOM对比的复用性能,让Diff算法能更精准、更快速地找到需要更新的节点,减少不必要的DOM操作。 关于key的使用,有几个核心要点: 第一,key的值必须是字符串或者数字类型,而且要保证唯一性,不能重复,最标准的做法是用后端返回的唯一id作为key。 第二,绝对不要用数组的index下标作为key。因为当数组发生元素新增、删除、排序的时候,元素的下标会发生变化,key就会和对应的元素失去绑定,Diff算法会错误地复用节点,导致渲染出错、状态混乱,比如表单输入框内容错位、复选框选中状态错误等问题。 第三,key能让Vue准确地判断节点的身份,当列表数据变化时,能保留未变化的节点,只更新变化的节点,避免整个列表重新渲染,提升渲染性能。
94、vue-router实现懒加载的好处
vue-router的懒加载,也叫路由按需加载,指的是不把所有路由对应的组件都打包到同一个JS文件里,而是把每个路由组件拆分成独立的代码块,只有当用户访问到这个路由的时候,才会去加载对应的组件代码。 它的核心好处有三点: 第一,大幅提升首屏加载速度,这是最核心的好处。如果不使用懒加载,打包的时候会把所有路由组件都打包到一个app.js文件里,文件体积会非常大,用户首次打开页面需要加载完整个大文件才能渲染首屏,白屏时间会很长;懒加载把首屏用不到的组件都拆分出去,首屏只需要加载当前路由的代码,文件体积大大减小,首屏加载速度大幅提升。 第二,减少浏览器的资源消耗。用户访问哪个路由就加载哪个路由的资源,不用一次性加载和解析所有的组件代码,节省了浏览器的内存和CPU消耗,页面运行更流畅。 第三,优化打包后的资源缓存。每个路由组件都会打包成独立的chunk文件,业务路由组件修改后,只会重新打包对应的chunk文件,不会影响其他已经缓存的文件,用户再次访问时,不用重新加载所有资源。
95、路由跳转和location.href的区别
vue-router的路由跳转,和原生的location.href跳转,核心区别体现在页面刷新、实现原理、性能体验、数据保留四个方面: 第一,页面刷新机制不同。vue-router的路由跳转,不管是hash模式还是history模式,都不会刷新整个浏览器页面,只是通过路由切换动态更新页面里的组件内容,属于单页应用内的跳转;location.href是原生的页面跳转,会让浏览器重新向服务器发起请求,加载整个新的HTML页面,页面会整页刷新,出现白屏。 第二,实现原理不同。vue-router的hash模式,是通过监听window的hashchange事件,根据hash值的变化切换组件,不会向服务器发请求;history模式是基于HTML5的History API,通过pushState和replaceState方法修改浏览器的历史记录,配合popstate事件实现路由切换,也不会整页刷新。location.href是直接修改浏览器的地址栏,触发浏览器的页面跳转和资源请求全流程。 第三,性能和用户体验不同。vue-router跳转只更新页面局部内容,没有整页刷新的白屏,用户体验更流畅,而且只需要加载路由对应的组件数据,不用重新加载整个页面的静态资源,性能开销小。location.href跳转需要重新加载整个页面的所有资源,性能开销大,用户体验差。 第四,数据状态保留不同。vue-router跳转时,整个应用的全局状态(比如Vuex里的数据)、组件的状态都会保留;location.href跳转后,整个页面重新加载,所有的JS内存数据都会丢失,状态无法保留。
96、Vue 的优点
Vue作为国内最主流的前端框架,核心优点有这几点: 第一,渐进式框架,上手门槛极低。Vue不强求开发者一次性掌握所有的语法和特性,可以学一点用一点,从小型项目到大型应用都能适配;API设计简洁易懂,官方文档是全中文的,对国内开发者非常友好,新手能快速入门。 第二,数据驱动视图,开发效率极高。基于MVVM架构实现了数据驱动,开发者不用再手动操作DOM,只需要关注数据逻辑,视图会自动更新;v-model实现的双向数据绑定,让表单数据的处理变得非常简洁,大幅减少了重复代码,提升开发效率。 第三,组件化开发,复用性和可维护性强。Vue的组件化思想非常完善,每个组件包含自己的结构、样式、逻辑,遵循单一功能原则,既能实现高复用,又能让代码结构更清晰,便于多人协作开发和后期维护。 第四,性能优异。Vue3采用Proxy实现响应式,配合虚拟DOM、Diff算法优化、静态提升、按需编译等特性,渲染性能和包体积都有大幅提升,页面运行流畅,首屏加载速度快。 第五,生态完善,社区活跃。Vue有官方的路由vue-router、状态管理Vuex/Pinia,还有Element Plus、Ant Design Vue等大量成熟的UI组件库,以及丰富的第三方插件,能覆盖绝大多数业务场景;国内社区非常活跃,遇到问题能快速找到解决方案。 第六,对TypeScript支持友好。Vue3完全用TS重写,对TS的类型推导支持非常完善,能适配大型企业级项目的类型安全需求。
97、如何让 CSS 只在当前的组件中起作用
要让CSS只在当前组件中生效,只需要给组件里的style标签添加scoped属性即可,这是Vue提供的原生组件样式隔离能力。 具体的实现原理是:给style标签加上scoped后,Vue会做两件事:第一,给当前组件内的所有HTML标签,添加一个唯一的data-v-开头的hash属性,这个属性值每个组件都是唯一的,不会重复;第二,给style标签里写的所有CSS选择器,都加上对应的[data-v-xxxx]属性选择器,让样式只能匹配到带有这个属性的元素。 这样一来,样式就只会作用于当前组件内的元素,不会污染全局样式,也不会和其他组件的样式冲突。 另外有几个补充要点: 第一,scoped样式默认不会作用于插槽传入的子组件内部元素,如果需要修改子组件的内部样式,可以用深度选择器,Vue3里用:deep(子组件选择器)来实现。 第二,一个组件里可以写多个style标签,给其中一个加scoped实现局部样式,另一个不加scoped写全局样式。 第三,scoped不会改变CSS的优先级规则,只是通过属性选择器实现了样式隔离。
98、路由中router与route 的区别
router和route是vue-router里两个核心的对象,核心区别是:router是路由的实例对象,用来操作路由;route是当前路由的信息对象,用来读取路由信息。 具体的区别有这几点: 第一,本质和作用不同。$router(组合式API里是useRouter()返回的对象)是整个vue-router的实例对象,是整个路由的管理者,包含了所有的路由规则、跳转方法、导航守卫等,用来做路由的跳转、前进、后退、动态添加路由规则等操作,是用来“操作路由”的。$route(组合式API里是useRoute()返回的对象)是当前激活的路由的信息对象,是一个只读的对象,里面保存了当前路由的所有信息,比如路由路径path、路由参数query/params、路由名称name、路由元信息meta等,是用来“读取当前路由信息”的。 第二,使用场景不同。router的使用场景,比如编程式导航跳转页面router.push()、路由的前进后退router.back()、动态添加路由router.addRoute()等。route的使用场景,比如获取路由跳转传递的参数route.query.id、获取当前路由路径做权限判断、获取路由meta设置页面标题等。 第三,实例数量不同。整个Vue应用里,router实例是唯一的,只有一个全局的路由实例;route对象是不唯一的,每切换一个路由,都会生成一个新的当前路由的route对象。
99、说一下ref的使用场景
ref是Vue里非常常用的API,核心有两大使用场景,分别是创建响应式数据和获取DOM元素/组件实例,其中Vue3里创建响应式数据是最核心的场景。 具体的使用场景分这几点: 第一,Vue3中创建基本类型的响应式数据。这是ref最核心的使用场景,因为reactive只能处理引用类型数据,字符串、数字、布尔、undefined、null这些基本类型的响应式,必须通过ref来创建。比如定义页面的标题、数字计数器、开关状态等,都用ref。 第二,Vue3中创建引用类型的响应式数据。ref也可以处理对象、数组等引用类型,底层会自动用reactive做代理,而且ref创建的引用类型数据,可以直接替换整个对象,不会丢失响应式,这是reactive做不到的,很多场景下会用ref来管理引用类型数据。 第三,获取真实DOM元素。在模板里给DOM元素添加ref属性,然后在JS里创建一个和ref属性同名的ref对象,组件挂载完成后,这个ref对象的.value就会指向对应的真实DOM元素,就能操作DOM了,比如让输入框自动聚焦、获取DOM的尺寸、操作canvas元素等。 第四,获取子组件的实例对象。给子组件标签添加ref属性,JS里创建对应的ref对象,挂载完成后,就能通过ref.value获取到子组件的实例,从而调用子组件里的方法、访问子组件里的数据,实现父子组件通信。
100、请分别说出vue修饰符trim、number、lazy的作用
这三个都是v-model指令常用的修饰符,作用是对表单输入的数据做预处理,简化开发,分别的作用是: 第一,trim修饰符:作用是自动去除用户输入内容首尾的空白字符。比如用户在输入框里输入了前后带空格的用户名,用v-model.trim绑定后,绑定的数据会自动去掉首尾的空格,只保留中间的有效内容,不用我们手动写代码处理,非常适合用户名、标题、搜索关键词这类需要去除首尾空格的输入场景。 第二,number修饰符:作用是自动将用户输入的内容转换成数值类型。因为input输入框里输入的内容,哪怕是纯数字,默认也会是字符串类型,用v-model.number绑定后,会自动用parseFloat把输入的内容转换成数字;如果输入的内容无法转换成数字,就会返回原始的输入内容。适合年龄、价格、数量、分数这类只需要数字的输入场景,不用手动做类型转换。 第三,lazy修饰符:作用是改变v-model的触发时机,从input事件触发改成change事件触发。默认情况下,v-model会在用户每次输入内容的时候,实时同步输入框的内容到绑定的数据里;加了lazy修饰符后,只有当输入框失去焦点,或者用户按下回车键的时候,才会把输入的内容同步到绑定的数据里。适合不需要实时同步的场景,比如大段的文本输入、表单输入项,能减少数据更新的次数,提升性能,也能避免实时校验给用户带来的干扰。
101、JWT的好处
JWT全称是JSON Web Token,是一种轻量级的跨域身份认证方案,它的核心优势主要有这几点: 第一,无状态,这是最核心的优势。用户信息都存在Token本身,服务端不需要存储会话信息,天然支持分布式集群,不用做Session共享,大幅降低服务端存储和扩容成本。 第二,天然支持跨域,Token放在请求头传递,不像Cookie有跨域限制,完美适配前后端分离、多服务架构的场景。 第三,安全性可控,通过签名算法防篡改,可设置过期时间、权限字段,同时不依赖Cookie,能规避CSRF攻击风险。 第四,通用性强,基于JSON格式,不限制开发语言,前后端、不同技术栈都能轻松解析,适配性极高。
102、JWT使用步骤
实际项目中,JWT的使用是前后端配合的标准化流程,主要分为四步: 第一步,登录生成Token:用户提交账号密码,后端校验通过后,提取用户ID、角色、权限等核心信息,搭配过期时间、签名密钥,通过HS256等算法生成JWT Token,返回给前端。 第二步,前端存储与传递:前端拿到Token后,一般存在localStorage中,后续每次发起请求,都将Token放在HTTP请求的Authorization头中,按规范带上Bearer前缀传递给后端。 第三步,后端校验与放行:后端拦截器会拦截所有需鉴权的请求,取出Token后,先校验签名是否合法、有无被篡改,再校验是否过期;校验通过则解析用户信息放入请求上下文,放行业务请求,校验失败直接返回401未授权。 第四步,过期续期处理:一般会搭配刷新Token机制,主Token设较短过期时间,刷新Token设较长过期时间,主Token过期后,前端用刷新Token换发新Token,避免用户频繁登录。
103、为什么要使用依赖注入?
依赖注入也就是我们常说的DI,是Spring的核心特性之一,简单说就是把对象的创建、依赖关系的管理交给Spring容器,而非代码里硬编码new对象。我们使用它,核心是解决传统开发的痛点,主要有这几点: 第一,核心是解耦,彻底解决类与类之间的强耦合问题。传统代码里依赖硬编码写死,依赖类一旦修改,所有调用处都要改动;用DI后,依赖由容器注入,我们只需面向接口编程,不用关心依赖对象的创建细节,完全符合开闭原则。 第二,大幅提升可测试性,做单元测试时,可直接通过注入Mock对象替代真实依赖,不用修改业务代码就能完成测试,这是硬编码无法实现的。 第三,提升可维护性与扩展性,对象生命周期全由容器统一管理,不用手写单例、创建销毁逻辑,减少大量重复代码;后续要替换实现类,只需改配置或注解,不用动业务代码,迭代更灵活。 第四,降低代码复杂度,业务代码不用处理依赖的创建和关联,只需专注核心业务逻辑,代码更干净、可读性更高。
104、AOP应用场景(公共非业务逻辑)
AOP即面向切面编程,核心是把和核心业务无关的公共通用逻辑抽离出来,与业务代码解耦,避免重复代码,项目中常用的场景主要有这些: 第一,统一日志记录,这是最常用的场景。比如接口入参、出参、请求耗时、调用方IP等,不用在每个Controller里重复写打印逻辑,通过切面统一拦截所有接口,集中记录日志,问题排查效率极高。 第二,统一权限校验,不用在每个业务方法里写权限判断代码,通过切面拦截请求,先校验用户是否有对应接口的访问权限,校验通过再放行,不通过直接拒绝。 第三,统一事务管理,也是Spring最经典的AOP应用,比如@Transactional注解,就是通过AOP实现事务的开启、提交、回滚,不用手动写事务处理代码,避免手动操作的失误。 第四,接口限流与重复提交校验,通过切面拦截高频请求,统一做限流判断,超过阈值直接拒绝;同时可拦截表单提交,做重复提交校验,完全不侵入业务代码。 除此之外,操作审计日志、方法执行耗时统计、接口参数统一校验、服务熔断降级等,都是AOP的典型应用场景。
105、AOP有哪些优势?
AOP面向切面编程,是对OOP面向对象编程的重要补充,核心优势主要有这几点: 第一,非侵入式编程,极致解耦,这是最核心的优势。它把公共通用逻辑和核心业务逻辑完全分离,写切面逻辑完全不用修改业务代码,业务代码也感知不到切面的存在,彻底避免公共逻辑与业务逻辑的耦合,符合开闭原则。 第二,消除重复代码,提升开发效率,像日志、权限、事务这类通用逻辑,不用在每个业务方法里重复编写,只需写一次切面,就能应用到所有需要的地方,大幅减少冗余代码,开发和迭代效率显著提升。 第三,提升代码可维护性与可读性,所有公共逻辑都集中在切面里统一管理,而非散落在各个业务方法中,后续要修改日志规则、权限逻辑,只需改切面一处,不用逐行修改业务代码,问题定位和维护成本极低。 第四,极强的可扩展性,后续要新增通用功能,比如新增接口限流、操作审计,只需新增一个切面即可,完全不用改动现有业务代码,不会影响原有业务的稳定性,扩展非常灵活。
106、什么是DTO?
DTO全称是Data Transfer Object,也就是数据传输对象,它是专门用来在系统不同层之间、或者不同服务之间传输数据的Java对象,核心定位就是数据传输的载体。 它本身不包含任何业务逻辑,只有属性和对应的get、set方法,简单说就是为数据传输而生的。比如前后端交互时,Controller层接收前端参数、给前端返回数据,用的就是DTO;微服务之间的远程调用,传递入参和返回结果,也会用DTO。 它和数据库对应的Entity实体类是完全隔离的,我们不会直接把Entity拿来做数据传输,避免把数据库的表结构、字段信息直接暴露出去。
107、为什么要使用数据传输对象DTO?
我们使用DTO,核心是解决直接用数据库实体类Entity做数据传输带来的各类问题,核心原因有这几点: 第一,隔离内部结构,保障数据安全,这是最核心的一点。如果直接把Entity传给前端,等于把数据库表结构完全暴露,有极大的安全风险;用DTO可以只传输业务需要的字段,隐藏密码、身份证号等敏感字段,避免敏感数据泄露。 第二,解耦,避免业务变动互相影响,数据库表结构和前端需要的参数结构,变动频率完全不同。如果直接用Entity,表加字段、改字段,前后端都要跟着改;用DTO后,Entity和DTO相互隔离,两边的变动互不影响,符合开闭原则。 第三,灵活适配传输需求,减少无效传输,不同接口需要的字段不同,比如列表接口只需要id、名称,详情接口需要全量字段,用DTO可以给不同接口定制专属对象,只传必要字段,不用传输Entity全量字段,减少网络传输体积,提升接口性能。 第四,避免非法参数传递,提升接口稳定性,比如新增接口时,创建时间、主键ID这类自动生成的字段,不希望前端传递,用DTO就可以直接去掉这些字段,避免前端传入非法参数导致业务异常。
108、DTO和ViewModel(视图模型)是一回事吗?
DTO和ViewModel不是一回事,二者的定位、使用场景、核心职责有明确的区别,虽然都是承载数据的对象,但完全不能混为一谈。 首先说DTO,也就是数据传输对象,它的核心职责是跨层、跨服务的数据传输,关注点是数据的传递,不绑定任何特定视图。它的使用场景是前后端接口数据传输、微服务之间的远程调用,是系统各层数据交互的通用载体,只关心要传输什么数据,不关心数据给哪个页面、哪个视图使用。 然后是ViewModel,也就是视图模型,它的核心职责是为特定的视图页面服务,完全和视图绑定,关注点是视图的渲染。它里面的字段、数据格式,完全匹配前端某个具体页面的展示需求,比如页面的按钮状态、下拉框选项、格式化后的日期、拼接好的用户全称等,都是ViewModel处理的,它是专门给视图层用的,只为特定视图服务。 简单总结:DTO是通用的传输载体,不绑定视图;ViewModel是专属的视图载体,完全绑定特定页面,很多时候前端拿到DTO后,会把DTO转换成对应的ViewModel,再给页面渲染使用。
109、跨域
首先,跨域的本质是浏览器的同源策略限制,同源指的是请求的协议、域名、端口号三者完全一致,只要有一个不一样,就会触发跨域限制。浏览器的同源策略是为了安全,防止恶意网站窃取其他网站的敏感数据,它限制的是跨域请求的响应结果被前端页面拿到,而不是阻止请求发送出去。 我们日常开发中,前后端分离项目最容易出现跨域,比如前端跑在localhost:8080,后端跑在localhost:8081,端口不同,就会触发跨域;前端域名和后端接口域名不同,也会出现跨域问题。 目前行业主流的跨域解决方案有两个: 第一个是CORS跨域资源共享,也是最标准、最推荐的方案,通过后端配置Access-Control-Allow-Origin等响应头,告诉浏览器哪些域名允许跨域访问,SpringBoot项目里只需加@CrossOrigin注解,或者写全局CORS配置类就能实现。 第二个是Nginx反向代理,线上环境用的很多,通过Nginx把前端和后端的请求都代理到同一个域名端口下,比如前端请求/api路径时,Nginx转发到后端服务,浏览器识别为同源,就不会触发跨域限制。 除此之外,开发环境也会用Node前端代理,而JSONP因为只支持GET请求、有安全风险,现在基本已经淘汰了。
110、Redis
Redis 是一个开源的、基于内存的键值对 NoSQL 数据库,也叫数据结构服务器,是目前互联网项目里必备的组件,核心特点和应用场景都非常明确。
它的核心特点有这几点:第一,性能极强,因为是基于内存存储,读写速度远超基于磁盘的关系型数据库,单节点 QPS 能达到十万级别,完美支撑高并发场景。第二,支持丰富的数据类型,除了基础的 String 类型,还有 Hash、List、Set、Sorted Set,以及 BitMap、HyperLogLog、GeoSpatial 这些拓展类型,能适配各种不同的业务场景。第三,兼顾性能和数据安全,支持 RDB 和 AOF 两种持久化机制,可以把内存里的数据持久化到磁盘,避免服务器断电数据丢失。第四,支持高可用和水平扩展,有主从复制、哨兵机制实现高可用,还有 Redis Cluster 集群方案,能实现分片存储,水平扩容,支撑海量数据。
日常开发中,它最核心的应用场景是热点数据缓存,比如商品详情、首页数据、用户信息缓存,把热点数据存在 Redis 里,大幅减轻数据库的访问压力;除此之外,分布式锁、接口限流、计数器、排行榜、会话共享、消息队列这些场景,也都是 Redis 的高频应用场景。