ASP.NET Core依赖注入详解

目录
  • 一、什么是依赖注入
  • 二、使用框架提供的服务
  • 三、注册服务
  • 四、生命周期
  • 五、请求服务
  • 六、设计你的依赖服务

ASP.NET Core的底层设计支持和使用依赖注入。ASP.NET Core应用程序可以利用内置的框架服务将它们注入到启动类的方法中,并且应用程序服务能够配置注入。由ASP.NET Core提供的默认服务容器提供了最小功能集,并不是要取代其它容器。

一、什么是依赖注入

依赖注入(Dependency injection,DI)是一种实现对象及其合作者或依赖项之间松散耦合的技术。将类用来执行其操作的这些对象以某种方式提供给该类,而不是直接实例化合作者或使用静态引用。通常,类会通过它们的构造函数声明其依赖关系,允许它们遵循显示依赖原则。这种方法被称为“构造函数注入”。

当类的设计使用DI思想时,它们的耦合更加松散,因为它们没有对它们的合作者直接硬编码的依赖。这遵循“依赖倒置原则(Dependency Inversion Principle)”,其中指出,“高层模块不应该依赖于低层模块;两者都应该依赖于抽象”。类要求在它们构造时向其提供抽象(通常是interfaces),而不是引用特定的实现。提取接口的依赖关系和提供这些接口的实现作为参数也是“策略设计模式”的一个示例。

当系统被设计使用DI,很多类通过它们的构造函数(或属性)请求其依赖关系,当一个类被用来创建这些类及其相关的依赖关系是很有帮助的。这些类被称为“容器(containers)”,或者更具体地被称为“控制反转(Inversion of Control,IOC)容器”或者“依赖注入(Dependency injection,DI)容器”。容器本质上是一个工厂,负责提供向它请求的类型实例。如果一个给定类型声明它具有依赖关系,并且容器已经被配置为提供依赖类型,那么它将把创建依赖关系作为创建请求实例的一部分。通过这种方式,可以向类型提供复杂的依赖关系而不需要任何硬编码的类型构造。除了创建对象的依赖关系外,容器通常还会管理应用程序中对象的生命周期。

ASP.NET Core包含了一个默认支持构造函数注入的简单内置容器(由IServiceProvider接口表示),并且ASP.NET Core使某些服务可以通过DI获取。ASP.NET Core的容器指的是它管理的类型为services。services是指由ASP.NET Core的IOC容器管理的类型。我们可以在应用程序Startup类的ConfigureServices方法中配置内置容器的服务。

二、使用框架提供的服务

Startup类中的ConfigureServices方法负责定义应用程序将使用的服务,包括平台功能,比如EntityFramework Core和ASP.NET Core MVC。最初,IServiceCollection只向ConfigureServices提供了几个服务定义。如下面的例子:

除了使用默认提供的几个服务定义,我们还可以自己添加。下面是一个如何使用一些扩展方法(如AddDbContext,AddIdentity)向容器中添加额外服务的例子:

public void ConfigureServices(IServiceCollection services)
{
    // 添加EntityFrameworkCore服务
    services.AddDbContext<AppDbContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    });
    // 添加MVC服务
    services.AddControllersWithViews();
}

ASP.NET提供的功能和中间件,例如MVC,遵循约定使用一个单一的AddService扩展方法来注册所有该功能所需的服务。

当然,除了使用各种框架功能配置应用程序外,还可以使用ConfigureServices来配置自己的应用程序服务。

三、注册服务

可以按照下面的方式注册自己的应用程序服务。第一个泛型类型表示将要从容器中请求的类型(这里的类型通常是一个接口)。第二个泛型类型表示将由容器实例化并且用于完成这些请求的具体类型:

// 添加自己的服务
// IRepository是一个接口,表示要请求的类型
// UserRepository表示IRepository接口的具体实现类型
services.AddTransient<IRepository, UserRepository>();

每个services.Add<service>调用添加服务。例如,services.AddControllersWithViews()表示添加MVC需要的服务。

在示例中,有一个名称为CharactersController的控制器。它的Index方法显示已经存储在应用程序的当前字符列表,并且,如果它不存在的话,则初始化具有少量字符的集合。值得注意的是:虽然应用程序使用Entity Framework Core和AppDbContext类作为持久化工具,这在控制器中都不是显而易见的。相反,具体的数据访问机制被抽象在遵循仓储模式的ICharacterRepository接口后面。ICharacterRepository实例是通过构造函数注入的,并且分配给一个私有字段,然后用来访问所需的字符:

using System.Linq;
using DependencyInjectionDemo.Model;
using DependencyInjectionDemo.Repository;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionDemo.Controllers
{
    public class CharactersController : Controller
    {
        // 定义私有的只读字段
        private readonly ICharacterRepository _characterRepository;

        /// <summary>
        /// 通过构造函数注入并且给私有字段赋值
        /// </summary>
        /// <param name="characterRepository"></param>
        public CharactersController(ICharacterRepository characterRepository)
        {
            _characterRepository = characterRepository;
        }

        public IActionResult Index()
        {
            return View();
        }

        private void PopulateCharactersIfNoneExist()
        {
            // 如果不存在则添加
            if(!_characterRepository.ListAll().Any())
            {
                _characterRepository.Add(new Character("Tom"));
                _characterRepository.Add(new Character("Jack"));
                _characterRepository.Add(new Character("Kevin"));
            }
        }
    }
}

ICharacterRepository接口中只定义了控制器需要使用的Character实例的两个方法:

using DependencyInjectionDemo.Model;
using System.Collections.Generic;

namespace DependencyInjectionDemo.Repository
{
    public interface ICharacterRepository
    {
        IEnumerable<Character> ListAll();
        int Add(Character character);
    }
}

这个接口在运行时需要使用一个具体的CharacterRepository类型来实现。

在CharacterRepository类中使用DI的方式是一个可以在你的应用程序服务遵循的通用模型,不只是在“仓储”或者数据访问类中:

using DependencyInjectionDemo.Context;
using DependencyInjectionDemo.Model;
using System.Collections.Generic;
using System.Linq;

namespace DependencyInjectionDemo.Repository
{
    public class CharacterRepository : ICharacterRepository
    {
        // 定义私有字段
        private readonly AppDbContext _dbContext;

        /// <summary>
        /// 通过构造函数注入,并且给私有字段赋值
        /// </summary>
        /// <param name="dbContext"></param>
        public CharacterRepository(AppDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public int Add(Character character)
        {
            // 添加
            _dbContext.Characters.Add(character);
            // 保存
            return _dbContext.SaveChanges();
        }

        public IEnumerable<Character> ListAll()
        {
            return _dbContext.Characters.AsEnumerable();
        }
    }
}

需要注意的是,CharacterRepository需要一个AppDbContext在它的构造函数中。依赖注入用于像这样的链式方法并不少见,每个请求依次请求它的依赖关系。容器负责解析所有的依赖关系,并返回完全解析后的服务。

创建请求对象和它需要的所有对象,以及那些需要的所有对象,有时称为一个对象图。同样的,必须解析依赖关系的集合通常称为依赖树或者依赖图。

在这种情况下,ICharacterRepository和AppDbContext都必须在Startup类的ConfigureServices方法的服务容器中注册。AppDbContext配置调用AddDbContex<T>扩展方法。下面的代码展示了ICharacterRepository和AppDbContext类型的注册:

public void ConfigureServices(IServiceCollection services)
{
    // 添加EntityFrameworkCore服务
    // 这里是注册AppDbContext使用AddDbContext<T>的形式
    services.AddDbContext<AppDbContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    });

    // 添加自己的服务
    // IRepository是一个接口,表示要请求的类型
    // UserRepository表示IRepository接口的具体实现类型
    services.AddTransient<IRepository, UserRepository>();

    // 注册ICharacterRepository类型
    services.AddTransient<ICharacterRepository, CharacterRepository>();
    // 添加MVC服务
    services.AddControllersWithViews();
}

Entity Framework Core的数据上下文应当使用Scope的生命周期添加到服务容器中。如果使用上面的AddDbContext<T>方法则会自动处理。仓储将使用与Entity Framework Core相同的生命周期。

四、生命周期

ASP.NET Core服务可以配置为以下三种生命周期:

  • Transient:瞬时生命周期。瞬时生命周期服务在它们每次请求时被创建。这一生命周期适合轻量级的、无状态的服务。
  • Scoped:作用域生命周期。作用域生命周期服务在每次请求时被创建一次。
  • Singleton:单例生命周期。单例生命周期服务在它们第一次被请求时创建,并且每个后续请求将使用相同的实例。如果你的应用程序需要单例行为,则建议让服务容器管理服务的生命周期,而不是在自己的类中实现单例模式和管理对象的生命周期。

服务可以用多种方式在容器中注册。我们已经看到了如何通过指定具体类型来注册一个给定类型的服务实现。除此之外,可以指定一个工厂,它将被用来创建需要的实例。第三种方式是直接指定要使用的类型的实例。在这种情况下,容器将永远不会尝试创建一个实例。

为了说明这些生命周期和注册选项之间的差异,考虑一个简单的接口将一个或多个任务表示为有一个唯一标识符OperationId的操作。根据我们配置这个服务的生命周期的方法,容器将为请求的类提供相同或不同的服务实例。为了弄清楚哪一个生命周期被请求,我们需要创建每一个生命周期选项的类型。我们先定义一个接口,里面定义基接口和三种注入模式的接口:

using System;

namespace DependencyInjectionDemo.Repository
{
    /// <summary>
    /// 基接口
    /// </summary>
    public interface IOperationRepository
    {
        Guid GetOperationId();
    }

    /// <summary>
    /// 瞬时接口
    /// </summary>
    public interface IOperationTransientRepository: IOperationRepository
    {

    }

    /// <summary>
    /// 作用域接口
    /// </summary>
    public interface IOperationScopeRepository : IOperationRepository
    {

    }

    /// <summary>
    /// 单例接口
    /// </summary>
    public interface IOperationSingletonRepository : IOperationRepository
    {

    }
}

我们使用OperationRepository类来实现这些接口:

using System;

namespace DependencyInjectionDemo.Repository
{
    public class OperationRepository : IOperationRepository
    {
        private readonly Guid _guid;

        public OperationRepository()
        {
            _guid = Guid.NewGuid();
        }

        public  Guid GetOperationId()
        {
            return _guid;
        }
    }

    public class OperationTransientRepository : OperationRepository, IOperationTransientRepository
    {

    }

    public class OperationScopeRepository : OperationRepository, IOperationScopeRepository
    {

    }

    public class OperationSingletonRepository : OperationRepository, IOperationSingletonRepository
    {

    }
}

然后在Startup类的ConfigureServices中,每一个类型根据它们命名的生命周期被添加到容器中:

public void ConfigureServices(IServiceCollection services)
{
    // 添加EntityFrameworkCore服务
    // 这里是注册AppDbContext使用AddDbContext<T>的形式
    services.AddDbContext<AppDbContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    });

    // 添加自己的服务
    // IRepository是一个接口,表示要请求的类型
    // UserRepository表示IRepository接口的具体实现类型
    services.AddTransient<IRepository, UserRepository>();

    // 注册ICharacterRepository类型
    services.AddTransient<ICharacterRepository, CharacterRepository>();

    // 添加瞬时生命周期
    services.AddTransient<IOperationTransientRepository, OperationTransientRepository>();
    // 添加作用域生命周期
    services.AddScoped<IOperationScopeRepository, OperationScopeRepository>();
    // 添加单例生命周期
    services.AddSingleton<IOperationSingletonRepository, OperationSingletonRepository>();
    // 添加MVC服务
    services.AddControllersWithViews();
}

然后添加一个控制器:

using DependencyInjectionDemo.Repository;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionDemo.Controllers
{
    public class OperationController : Controller
    {
        // 定义私有字段
        private readonly IOperationTransientRepository _transientRepository;
        private readonly IOperationScopeRepository _scopeRepository;
        private readonly IOperationSingletonRepository _singletonRepository;

        /// <summary>
        /// 通过构造函数实现注入
        /// </summary>
        /// <param name="transientRepository"></param>
        /// <param name="scopeRepository"></param>
        /// <param name="singletonRepository"></param>
        public OperationController(IOperationTransientRepository transientRepository,
            IOperationScopeRepository scopeRepository,
            IOperationSingletonRepository singletonRepository)
        {
            _transientRepository = transientRepository;
            _scopeRepository = scopeRepository;
            _singletonRepository = singletonRepository;
        }

        public IActionResult Index()
        {
            // ViewBag赋值
            ViewBag.TransientGuid = _transientRepository.GetOperationId();
            ViewBag.ScopedGuid = _scopeRepository.GetOperationId();
            ViewBag.SingletonGuid = _singletonRepository.GetOperationId();
            return View();
        }
    }
}

对应的Index视图代码:

<div class="row">
    <div>
        <h2>GuidItem Shows</h2>

        <h3>TransientGuid: @ViewBag.TransientGuid</h3>

        <h3>ScopedGuid: @ViewBag.ScopedGuid</h3>

        <h3>SingletonGuid: @ViewBag.SingletonGuid</h3>
    </div>
</div>

然后我们打开两个浏览器,刷新多次,只会发现“TransientGuid” 和“ScopedGuid”的值在不断变化,而“SingletonGuid”的值是不会变化的,这就体现了单例模式的作用,如下图所示:

但是这样还不够,要知道我们的Scoped的解读是“生命周期横贯整次请求”,但是现在演示起来和Transient好像没有什么区别(因为两个页面每次浏览器请求仍然是独立的,并不包含于一次中),所以我们采用以下代码来演示下(同一请求源):

@*引入命名空间*@
@using DependencyInjectionDemo.Repository

@*通过该inject引入*@
@inject IOperationTransientRepository OperationTransientRepository
@inject IOperationScopeRepository OperationScopeRepository
@inject IOperationSingletonRepository OperationSingletonRepository

<div class="row">
    <div>
        <h2>GuidItem Shows</h2>
        <h3>TransientGuid: @OperationTransientRepository.GetOperationId()</h3>
        <h3>ScopedGuid: @OperationScopeRepository.GetOperationId()</h3>
        <h3>SingletonGuid: @OperationSingletonRepository.GetOperationId()</h3>
    </div>
</div>

然后修改Index视图:

<div class="row">
    <div>
        @Html.Partial("GuidPartial")
        <h2>**************************</h2>
        <h2>GuidItem Shows</h2>
        <h3>TransientGuid: @ViewBag.TransientGuid</h3>
        <h3>ScopedGuid: @ViewBag.ScopedGuid</h3>
        <h3>SingletonGuid: @ViewBag.SingletonGuid</h3>
    </div>
</div>

在运行程序执行:

可以看到:每次请求的时候Scope生命周期在同一请求中是不变的,而Transient生命周期还是会不断变化的。

  • 瞬时(Transient):对象总是不同的,向每一个控制器和每一个服务提供了一个新的实例(同一个页面内的Transient也是不同的)。
  • 作用域(Scoped):对象在一次请求中是相同的,但在不同请求中是不同的(在同一个页面内多个Scoped是相同的,在不同页面中是不同的)。
  • 单例(Singleton):对象对每个对象和每个请求是相同的(无论是否在ConfigureServices中提供实例)。

五、请求服务

来自HttpContext的一次ASP.NET请求中,可用的服务是通过RequestServices集合公开的。

请求服务将你配置的服务和请求描述为应用程序的一部分。在你的对象指定依赖关系后,这些满足要求的对象可通过查找RequestServices中对应的类型得到,而不是ApplicationServices。

通过,不应该直接使用这些属性,而是通过类的构造函数请求需要的类的类型,并且让框架来注入依赖关系。这将会生成更易于测试的和更松散耦合的类。

六、设计你的依赖服务

应该设计你的依赖注入服务来获取它们的合作者。这意味着在你的服务中,避免使用有状态的静态方法调用和直接实例化依赖的类型。

如果你的类有太多的依赖关系被注入时该怎么办?这通常表明你的类试图做太多,并且可能违反了单一职责原则。看看是否可以通过转移一些职责到一个新的类来重构。

注意,你的Controller类应该重点关注用户界面(UI),因此业务规则和数据访问实现细节应该保存在这些适合单独关注的类中。

关于数据访问,如果你已经在Startup类中配置了EF,那么你能够方便地注入Entity Framework的DBContext类型到你的控制器中。然而,最好不要在你的UI项目中直接依赖DBContext。相反,应该依赖于一个抽象(比如一个仓储接口),并且限定使用EF(或其他任何数据访问技术)来实现这个接口。这将减少应用程序和特定的数据访问策略之间的耦合,并且使你的应用程序代码更容易测试。

GitHub示例代码:https://github.com/jxl1024/DependencyInjection

到此这篇关于ASP.NET Core依赖注入的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 理解ASP.NET Core 依赖注入(Dependency Injection)

    目录 依赖注入 什么是依赖注入 依赖注入有什么好处 ASP.NET Core内置的依赖注入 服务生存周期 服务释放 TryAdd{Lifetime}扩展方法 解析同一服务的多个不同实现 Replace && Remove 扩展方法 Autofac 服务解析和注入 构造函数注入 方法注入 属性注入 一些注意事项 框架默认提供的服务 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可. 微软官方文档-DI 依赖注入有什么好处 依赖

  • ASP.NET Core依赖注入系列教程之服务的注册与提供

    前言 在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象.ASP.NET Core中的DI容器体现为一个实现了IServiceProvider接口的对象. ServiceProvider与ServiceDescriptor 服务的注册与提供     利用ServiceProvider来提供服务     提供一个服务实例的集合     获取ServiceProvider自身对

  • ASP.NET Core 依赖注入详细

    目录 一.控制反转 二.好莱坞法则 三.流程控制 四.三种依赖注入方式 1.构造器注入 2.属性注入 3.方法注入 五.生命周期 六.ASP.Net Core 中自带的注入 前言: ASP.NET Core 应用在启动过程中会依赖各种组件提供服务,而这些组件会以接口的形式标准化,这些组件这就是我们所说的服务,ASP.NET Core框架建立在一个底层的依赖注入框架之上,它使用容器提供所需的服务.要了解依赖注入容器以及它的机制,我们需要了解什么是依赖注入. 一.控制反转 说道依赖注入就不得不提控制

  • ASP.NET Core 过滤器中使用依赖注入知识点总结

    如何给过滤器ActionFilterAttribute也用上构造函数注入呢? 一般自定义的过滤器直接用特性方式标识就能使用 [ContentFilter] 因为构造函数在使用的时候要求传参,然后我们可以使用这个 ServiceFilter 在ASP.NET Core里,我们可以使用ServiceFilter来完成这个需求. ServiceFilter允许我们解析一个已经添加IoC容器的服务,因此我们需要把ContentFilter注册一下. services.AddScoped<ContentF

  • 详解asp.net core 依赖注入

    前言 好久没有写微博了,因为前段时间由于家庭原因决定从工作了3年多的北京转移到上海去.依赖注入在学习net core的时候也有写过类似的东西,只是实践的较少,结果来到上海新公司系统框架涉及到了这块知识点,所以在了解完自己的项目之后决定做一些相关的总结.接下来就让我们先来了解hewi依赖注入. 什么是依赖注入 依赖注入,全称是"依赖注入到容器", 容器(IOC容器)是一个设计模式,它也是个对象,你把某个类(不管有多少依赖关系)放入这个容器中,可以"解析"出这个类的实例

  • ASP.NET Core MVC创建控制器与依赖注入讲解

    默认的IControllerActivator 在 ASP.NET Core 中,当 MVC 中间件接收到请求时,通过路由选择要执行的控制器和操作方法.为了实际的执行操作, MVC 中间件必须创建所选控制器的实例. 创建控制器的过程依赖众多不同的提供者和工厂类,但最终是由实现IControllerActivator接口的实例来决定的.实现类只需要实现两个方法: public interface IControllerActivator { object Create(ControllerCont

  • ASP.NET Core实现自动依赖注入

    在开发.NET Core web服务的时候,我们习惯使用自带的依赖注入容器来进行注入. 于是就会经常进行一个很频繁的的重复动作:定义一个接口->写实现类->注入 有时候会忘了写Add这一步,看到屏幕上的报错一脸懵逼,然后瞬间反应过来忘了注入了.赶紧补上serviceCollection.AddXXX这句话 虽然说有很多开源框架已经实现了类似的工作,比如AutoFac,Unity等依赖注入框架.但是这些库都太庞大了,我个人还是喜欢轻量级的实现. 定义一个枚举 [AttributeUsage(At

  • ASP.NET Core  依赖注入框架的使用

    目录 一.IoC框架 二.IoC-Autofac 三..NET Core中自带DI的使用 四.Autofac 使用 五.批量注入 前言: 还记得上篇文章中ASP.NET Core 依赖注入详细最后提及到,假如服务越来越多怎么处理呢,本篇文章将会带来解决办法.这篇是接上一篇文章的,概念方面的可以参考上一篇文章. 一.IoC框架 先说说常见的Ioc框架吧. Autofac: 目前net用的比较多,好多大佬的项目比较优先选择的框架. Ninject: 已经很少用了,还时在很早的文章中见过. Unity

  • ASP.NET Core依赖注入详解

    目录 一.什么是依赖注入 二.使用框架提供的服务 三.注册服务 四.生命周期 五.请求服务 六.设计你的依赖服务 ASP.NET Core的底层设计支持和使用依赖注入.ASP.NET Core应用程序可以利用内置的框架服务将它们注入到启动类的方法中,并且应用程序服务能够配置注入.由ASP.NET Core提供的默认服务容器提供了最小功能集,并不是要取代其它容器. 一.什么是依赖注入 依赖注入(Dependency injection,DI)是一种实现对象及其合作者或依赖项之间松散耦合的技术.将类

  • ASP.NET Core中HttpContext详解与使用

    “传导体” HttpContext 要理解 HttpContext 是干嘛的,首先,看图 图一 内网访问程序 图二 反向代理访问程序 ASP.NET Core 程序中,Kestrel 是一个基于 libuv 的跨平台ASP.NET Core web服务器.不清楚 Kerstrel 没关系,以后慢慢了解. 我们可以理解成,外部访问我们的程序,通过 Http 或者 Https 访问,例如https://localhost:44337/Home/Index,需要通过一个网址,来寻向访问特定的页面. 访

  • .net程序开发IOC控制反转和DI依赖注入详解

    目录 IOC控制反转 DI依赖注入 服务生命周期 其它 IOC控制反转 大部分应用程序都是这样编写的:编译时依赖关系顺着运行时执行的方向流动,从而生成一个直接依赖项关系图. 也就是说,如果类 A 调用类 B 的方法,类 B 调用 C 类的方法,则在编译时,类 A 将取决于类 B,而 B 类又取决于类 C 应用程序中的依赖关系方向应该是抽象的方向,而不是实现详细信息的方向.而这就是控制反转的思想. 应用依赖关系反转原则后,A 可以调用 B 实现的抽象上的方法,让 A 可以在运行时调用 B,而 B

  • ASP.NET Core依赖注入(DI)讲解

    ASP.NET Core的底层设计支持和使用依赖注入.ASP.NET Core 应用程序可以利用内置的框架服务将服务注入到启动类的方法中,并且应用程序服务也可以配置注入.由ASP.NET Core 提供的默认服务容器提供了最小功能集,并不是取代其他容器. 1.浅谈依赖注入 依赖注入(Dependency injection,DI)是一种实现对象和依赖者之间松耦合的技术,将类用来执行其操作的这些对象以注入的方式提供给该类,而不是直接实例化依赖项或者使用静态引用.一般情况,类会通过构造函数声明器2依

  • Vue 2源码阅读 Provide Inject 依赖注入详解

    目录 Provide/Inject 初始化 1. initInjections 依赖初始化 2. initProvide 注入数据初始化 总结 Provide/Inject 初始化 1. initInjections 依赖初始化 该步骤其实发生在 initState 之前,但是由于 provide/inject 一般是配合使用,所以这里调整了一下顺序. 该函数的定义与过程都比较简单: export function initInjections(vm: Component) { const re

随机推荐