C#/.NET/.NET Core优秀项目和框架2023年11月简报 - 追逐时光者 - 博客园

mikel阅读(335)

来源: C#/.NET/.NET Core优秀项目和框架2023年11月简报 – 追逐时光者 – 博客园

前言

公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(公众号每周至少推荐两个优秀的项目和框架当然节假日除外),公众号推文有项目和框架的介绍、功能特点以及部分截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:追逐时光者,第一时间获取每周精选分享资讯🔔)。

简报GitHub开源地址:https://github.com/YSGStudyHards/DotNetGuide/blob/main/docs/DotNet/DotNetProjectMonthly.md

CAP

  • 项目简介: CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务的解决方案,同样具有 EventBus 的功能,它具有轻量级、易使用、高性能等特点。CAP 是一个EventBus,同时也是一个在微服务或者SOA系统中解决分布式事务问题的一个框架。它有助于创建可扩展,可靠并且易于更改的微服务系统。
  • 项目源码地址: https://github.com/dotnetcore/CAP
  • 公众号详细介绍: https://mp.weixin.qq.com/s/ONM9bLKidVCS4pAwJbG9tg

ZEQP.WMS

HandyControl

NETCore.Encrypt

WinMemoryCleaner

Hello算法

  • 项目简介: Hello算法一个开源免费、新手友好的数据结构与算法入门教程。全书采用动画图解,内容清晰易懂、学习曲线平滑,引导初学者探索数据结构与算法的知识地图。源代码可一键运行,帮助读者在练习中提升编程技能,了解算法工作原理和数据结构底层实现。支持 Java, C++, Python, Go, JS, TS, C#, Swift, Rust, Dart, Zig 等语言。
  • 项目源码地址: https://github.com/krahets/hello-algo
  • 公众号详细介绍: https://mp.weixin.qq.com/s/9lb5iu6tGNiSGcIrf7fQ3A

PaddleOCRSharp

  • 项目简介: PaddleOCRSharp 是一个基于百度飞桨PaddleOCR的.NET版本OCR工具类库。项目核心组件PaddleOCR.dll,由C++编写,根据百度飞桨PaddleOCR的C++代码修改并优化而成。目前已经支持C++、.NET、Python、Golang、Rust等开发语言的直接API接口调用。项目包含文本识别、文本检测、表格识别功能。本项目针对小图识别不准的情况下做了优化,比飞桨原代码识别准确率有所提高。包含总模型仅8.6M的超轻量级中文OCR,单模型支持中英文数字组合识别、竖排文本识别、长文本识别。同时支持中英文、纯英文以及多种语言文本检测识别。
  • 项目源码地址: https://gitee.com/raoyutian/paddle-ocrsharp
  • 公众号详细介绍: https://mp.weixin.qq.com/s/9F_rSB8Wm69jLdgsH4ufvg

MrHuo.OAuth

CoreShop

  • 项目简介: 核心商城系统(CoreShop) 是基于 ASP.NET 7.0、Uni-App开发、支持可视化布局的小程序商城系统;前后端分离,支持分布式部署,跨平台运行;拥有分销、代理、团购秒杀、接龙、拼团、直播、优惠券、自定义表单等众多营销功能,拥有完整SKU、下单、售后、物流流程,支持可视化自定义首页模块布局效果。
  • 项目源码地址: https://github.com/CoreUnion/CoreShop
  • 公众号详细介绍: https://mp.weixin.qq.com/s/iRxmWUXrqArZD_Ax4i6wwg

ToastFish

DotNetGuide技术社区交流群

  • DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目推荐、招聘资讯和解决问题的平台。
  • 在这个社区中,开发者们可以分享自己的技术文章、项目经验、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。
  • 我们致力于构建一个积极向上、和谐友善的.NET技术交流平台,为广大.NET开发者带来更多的价值和成长机会。

.NET8 依赖注入 - xiaolipro - 博客园

mikel阅读(325)

来源: .NET8 依赖注入 – xiaolipro – 博客园

依赖注入(Dependency Injection,简称DI)是一种设计模式,用于解耦组件(服务)之间的依赖关系。它通过将依赖关系的创建和管理交给外部容器来实现,而不是在组件(服务)内部直接创建依赖对象。

​ 咱就是通过 IServiceCollection 和 IServiceProvider 来实现的,他们直接被收入到了runtime libraries,在整个.NET平台下通用!

3.1 ServiceCollection

​ IServiceCollection 本质是一个 ServiceDescriptor 而 ServiceDescriptor 则是用于描述服务类型,实现和生命周期

public interface IServiceCollection : 
    IList<ServiceDescriptor>,
    ICollection<ServiceDescriptor>,
    IEnumerable<ServiceDescriptor>,
    IEnumerable;

​ 官方提供一些列拓展帮助我们向服务容器中添加服务描述,具体在 ServiceCollectionServiceExtensions

builder.Services.AddTransient<StudentService>();
builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository>("a");
builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository2>("b");
builder.Services.AddTransient<TransientService>();
builder.Services.AddScoped<ScopeService>();
builder.Services.AddSingleton<SingletonService>();

3.2 ServiceProvider

​ IServiceProvider 定义了一个方法 GetService,帮助我们通过给定的服务类型,获取其服务实例

public interface IServiceProvider
{
  object? GetService(Type serviceType);
}

​ 下面是 GetService 的默认实现(如果不给定engine scope,则默认是root)

public object? GetService(Type serviceType) => GetService(ServiceIdentifier.FromServiceType(serviceType), Root);

​ 也就是

internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
{
    if (_disposed)
    {
        ThrowHelper.ThrowObjectDisposedException();
    }
    // 获取服务标识符对应的服务访问器
    ServiceAccessor serviceAccessor = _serviceAccessors.GetOrAdd(serviceIdentifier, _createServiceAccessor);
    // 执行解析时的hock
    OnResolve(serviceAccessor.CallSite, serviceProviderEngineScope);
    DependencyInjectionEventSource.Log.ServiceResolved(this, serviceIdentifier.ServiceType);
    // 通过服务访问器提供的解析服务,得到服务实例
    object? result = serviceAccessor.RealizedService?.Invoke(serviceProviderEngineScope);
    System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceIdentifier));
    return result;
}

​ 其中,服务标识符 ServiceIdentifier 其实就是包了一下服务类型,和服务Key(为了.NET8的键化服务)

internal readonly struct ServiceIdentifier : IEquatable<ServiceIdentifier>
{
    public object? ServiceKey { get; }
    public Type ServiceType { get; }
}

​ 显而易见的,我们的服务解析是由 serviceAccessor.RealizedService 提供,而创建服务访问器 serviceAccessor 只有一个实现 CreateServiceAccessor

private ServiceAccessor CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
{
    // 通过 CallSiteFactory 获取服务的调用点(CallSite),这是服务解析的一个表示形式。
    ServiceCallSite? callSite = CallSiteFactory.GetCallSite(serviceIdentifier, new CallSiteChain());
    
    // 如果调用站点不为空,则继续构建服务访问器。
    if (callSite != null)
    {
        DependencyInjectionEventSource.Log.CallSiteBuilt(this, serviceIdentifier.ServiceType, callSite);
        
        // 触发创建调用站点的相关事件。
        OnCreate(callSite);

        // 如果调用站点的缓存位置是根(Root),即表示这是一个单例服务。
        if (callSite.Cache.Location == CallSiteResultCacheLocation.Root)
        {
            // 直接拿缓存内容
            object? value = CallSiteRuntimeResolver.Instance.Resolve(callSite, Root);
            return new ServiceAccessor { CallSite = callSite, RealizedService = scope => value };
        }

        // 通过引擎解析
        Func<ServiceProviderEngineScope, object?> realizedService = _engine.RealizeService(callSite);
        return new ServiceAccessor { CallSite = callSite, RealizedService = realizedService };
    }
    
    // 如果调用点为空,则它的实现服务函数总是返回 null。
    return new ServiceAccessor { CallSite = callSite, RealizedService = _ => null };
}

3.2.1 ServiceProviderEngine

​ ServiceProviderEngine 是服务商解析服务的执行引擎,它在服务商被初始化时建立。有两种引擎,分别是动态引擎运行时引擎,在 NETFRAMEWORK || NETSTANDARD2_0 默认使用动态引擎。

        private ServiceProviderEngine GetEngine()
        {
            ServiceProviderEngine engine;

#if NETFRAMEWORK || NETSTANDARD2_0
            engine = CreateDynamicEngine();
#else
            if (RuntimeFeature.IsDynamicCodeCompiled && !DisableDynamicEngine)
            {
                engine = CreateDynamicEngine();
            }
            else
            {
                // Don't try to compile Expressions/IL if they are going to get interpreted
                engine = RuntimeServiceProviderEngine.Instance;
            }
#endif
            return engine;

            [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode",
                Justification = "CreateDynamicEngine won't be called when using NativeAOT.")] // see also https://github.com/dotnet/linker/issues/2715
            ServiceProviderEngine CreateDynamicEngine() => new DynamicServiceProviderEngine(this);
        }

​ 由于.NET Aot技术与dynamic技术冲突,因此Aot下只能使用运行时引擎,但动态引擎在大多情况下仍然是默认的。

动态引擎使用了emit技术,这是一个动态编译技术,而aot的所有代码都需要在部署前编译好,因此运行时无法生成新的代码。那运行时引擎主要使用反射,目标是在不牺牲太多性能的情况下,提供一个在aot环境中可行的解决方案。

​ 我们展开动态引擎来看看它是如何解析服务的。

public override Func<ServiceProviderEngineScope, object?> RealizeService(ServiceCallSite callSite)
{
    // 定义一个局部变量来跟踪委托被调用的次数
    int callCount = 0;
    return scope =>
    {
        // 当委托被调用时,先使用CallSiteRuntimeResolver.Instance.Resolve方法来解析服务。这是一个同步操作,确保在编译优化之前,服务可以被正常解析。
        var result = CallSiteRuntimeResolver.Instance.Resolve(callSite, scope);
        // 委托第二次被调用,通过UnsafeQueueUserWorkItem在后台线程上启动编译优化
        if (Interlocked.Increment(ref callCount) == 2)
        {
            // 将一个工作项排队到线程池,但不捕获当前的执行上下文。
            _ = ThreadPool.UnsafeQueueUserWorkItem(_ =>
            {
                try
                {
                    // 用编译优化后的委托替换当前的服务访问器,主要用到emit/expression技术
                    _serviceProvider.ReplaceServiceAccessor(callSite, base.RealizeService(callSite));
                }
                catch (Exception ex)
                {
                    DependencyInjectionEventSource.Log.ServiceRealizationFailed(ex, _serviceProvider.GetHashCode());
                    Debug.Fail($"We should never get exceptions from the background compilation.{Environment.NewLine}{ex}");
                }
            },
            null);
        }
        return result;
    };
}

这个实现的关键思想是,第一次解析服务时使用一个简单的运行时解析器,这样可以快速返回服务实例。然后,当服务再被解析,它会在后台线程上启动一个编译过程,生成一个更高效的服务解析委托。一旦编译完成,新的委托会替换掉原来的委托,以后的服务解析将使用这个新的、更高效的委托。这种方法可以在不影响应用程序启动时间的情况下,逐渐优化服务解析的性能。

3.2.2 ServiceProviderEngineScope

​ ServiceProviderEngineScope 闪亮登场,他是我们服务商的代言人,从定义不难看出他对外提供了服务商所具备的一切能力

internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IKeyedServiceProvider, 			IAsyncDisposable, IServiceScopeFactory
{
    // this scope中所有实现IDisposable or IAsyncDisposable的服务
    private List<object>? _disposables;
    // 解析过的服务缓存(其实就是scope生命周期的服务缓存,singleton生命周期的服务缓存都直接挂在调用点上了)
    internal Dictionary<ServiceCacheKey, object?> ResolvedServices { get; }
    // 实锤服务商代言人
    public IServiceProvider ServiceProvider => this;
    // 没错啦,通过root scope我们又能继续创建无数个scope,他们彼此独立
    public IServiceScope CreateScope() => RootProvider.CreateScope();
}

​ 我们来观察他获取服务的逻辑,可以发现他就是很朴实无华的用着我们根服务商 ServiceProvider,去解析服务,那 engine scope 呢,就是 this。现在我们已经隐约可以知道engine scope,就是为了满足scope生命周期而生。而 ResolvedServices 中存的呢,就是该scope中的所有scope生命周期服务实例啦,在这个scope中他们是唯一的。

public object? GetService(Type serviceType)
{
    if (_disposed)
    {
        ThrowHelper.ThrowObjectDisposedException();
    }
    return RootProvider.GetService(ServiceIdentifier.FromServiceType(serviceType), this);
}

​ 再来看另一个核心的方法:CaptureDisposable,实现disposable的服务会被添加到 _disposables。

internal object? CaptureDisposable(object? service)
{
    // 如果服务没有实现 IDisposable or IAsyncDisposable,那么不需要捕获,直接原路返回
    if (ReferenceEquals(this, service) || !(service is IDisposable || service is IAsyncDisposable))
    {
        return service;
    }
    bool disposed = false;
    lock (Sync)
    {
        if (_disposed) // 如果scope已经销毁则进入销毁流程
        {
            disposed = true;
        }
        else
        {
            _disposables ??= new List<object>();
            _disposables.Add(service);
        }
    }
    // Don't run customer code under the lock
    if (disposed) // 这表示我们在试图捕获可销毁服务时,scope就已经被销毁
    {
        if (service is IDisposable disposable)
        {
            disposable.Dispose();
        }
        else
        {
            // sync over async, for the rare case that an object only implements IAsyncDisposable and may end up starving the thread pool.
            object? localService = service; // copy to avoid closure on other paths
            Task.Run(() => ((IAsyncDisposable)localService).DisposeAsync().AsTask()).GetAwaiter().GetResult();
        }
        // 这种case会抛出一个ObjectDisposedException
        ThrowHelper.ThrowObjectDisposedException();
    }
    return service;
}

​ 在engine scope销毁时,其作用域中所有scope生命周期且实现了disposable的服务(其实就是_disposable)呢,也会被一同的销毁。

public ValueTask DisposeAsync()
{
    List<object>? toDispose = BeginDispose(); // 获取_disposable
    if (toDispose != null)
    {
        // 从后往前,依次销毁服务
    }
}

​ 那么有同学可能就要问了:单例实例既然不存在root scope中,而是单独丢到了调用点上,那他是咋销毁的?压根没看到啊,那不得泄露了?

​ 其实呀,同学们并不用担心这个问题。首先,单例服务的实例确实是缓存在调用点上,但 disable 服务仍然会被 scope 捕获呀(在下文会详细介绍)。在 BeginDispose 中的,我们会去判断,如果是 singleton case,且root scope 没有被销毁过,我们会主动去销毁喔~

if (IsRootScope && !RootProvider.IsDisposed()) RootProvider.Dispose();

3.3 ServiceCallSite

​ ServiceCallSite 的主要职责是封装服务解析的逻辑,它可以代表一个构造函数调用、属性注入、工厂方法调用等。DI系统使用这个抽象来表示服务的各种解析策略,并且可以通过它来生成服务实例。

internal abstract class ServiceCallSite
{
    protected ServiceCallSite(ResultCache cache)
	 {
	     Cache = cache;
	 }
    public abstract Type ServiceType { get; }
	 public abstract Type? ImplementationType { get; }
	 public abstract CallSiteKind Kind { get; }
	 public ResultCache Cache { get; }
	 public object? Value { get; set; }
	 public object? Key { get; set; }
	 public bool CaptureDisposable => ImplementationType == null ||
    	typeof(IDisposable).IsAssignableFrom(ImplementationType) ||
    	typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);
}

3.3.1 ResultCache

​ 其中 ResultCache 定义了我们如何缓存解析后的结果

public CallSiteResultCacheLocation Location { get; set; } // 缓存位置
public ServiceCacheKey Key { get; set; } // 服务key(键化服务用的)

​ CallSiteResultCacheLocation 是一个枚举,定义了几个值

  1. Root:表示服务实例应该在根级别的 IServiceProvider 中缓存。这通常意味着服务实例是单例的(Singleton),在整个应用程序的生命周期内只会创建一次,并且在所有请求中共享。
  2. Scope:表示服务实例应该在当前作用域(Scope)中缓存。对于作用域服务(Scoped),实例会在每个作用域中创建一次,并在该作用域内的所有请求中共享。
  3. Dispose:尽管这个名称可能会让人误解,但在 ResultCache 的上下文中,Dispose 表示着服务是瞬态的(每次请求都创建新实例)。
  4. None:表示没有缓存服务实例。

​ ServiceCacheKey 结构体就是包了一下服务标识符和一个slot,用于适配多实现的

internal readonly struct ServiceCacheKey : IEquatable<ServiceCacheKey>
{
    public ServiceIdentifier ServiceIdentifier { get; }
    public int Slot { get; } // 那最后一个实现的slot是0
}

3.3.2 CallSiteFactory.GetCallSite

​ 那我们来看看调用点是怎么创建的吧,其实上面已经出现过一次了:

private ServiceCallSite? CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
{
    if (!_stackGuard.TryEnterOnCurrentStack()) // 防止栈溢出
    {
        return _stackGuard.RunOnEmptyStack(CreateCallSite, serviceIdentifier, callSiteChain);
    }
    // 获取服务标识符对应的锁,以确保在创建调用点时的线程安全。
    // 是为了保证并行解析下的调用点也只会被创建一次,例如:
    // C -> D -> A
    // E -> D -> A
    var callsiteLock = _callSiteLocks.GetOrAdd(serviceIdentifier, static _ => new object());
    lock (callsiteLock)
    {
        // 检查当前服务标识符是否会导致循环依赖
        callSiteChain.CheckCircularDependency(serviceIdentifier);
        // 首先尝试创建精确匹配的服务调用站点,如果失败,则尝试创建开放泛型服务调用站点,如果还是失败,则尝试创建枚举服务调用站点。如果所有尝试都失败了,callSite将为null。
        ServiceCallSite? callSite = TryCreateExact(serviceIdentifier, callSiteChain) ??
                                   TryCreateOpenGeneric(serviceIdentifier, callSiteChain) ??
                                   TryCreateEnumerable(serviceIdentifier, callSiteChain);
        return callSite;
    }
}

​ 那服务点的创建过程我就简单概述一下啦

  1. 查找调用点缓存,存在就直接返回啦
  2. 服务标识符会被转成服务描述符 ServiceDescriptor (key化服务不指定key默认取last)
  3. 计算ServiceCallSite,依次是:
    1. TryCreateExact
      1. 计算 ResultCache
      2. 如果已经有实现实例了,则返回 ConstantCallSite:表示直接返回已经创建的实例的调用点。
      3. 如果有实现工厂,则返回 FactoryCallSite:表示通过工厂方法创建服务实例的调用点。
      4. 如果有实现类型,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。
    2. TryCreateOpenGeneric
      1. 根据泛型定义获取服务描述符 ServiceDescriptor
      2. 计算 ResultCache
      3. 使用服务标识符中的具体泛型参数来构造实现的闭合类型
      4. AOT兼容性测试(因为不能保证值类型泛型的代码已经生成)
      5. 如果成功闭合,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。
    3. TryCreateEnumerable
      1. 确定类型是 IEnumerable<T>
      2. AOT兼容性测试(因为不能保证值类型数组的代码已经生成)
      3. 如果 T 不是泛型类型,并且可以找到对应的服务描述符集合,则循环 TryCreateExact
      4. 否则,方向循环 TryCreateExact,然后方向循环 TryCreateOpenGeneric

3.4 CallSiteVisitor

​ 好了,有了上面的了解我们可以开始探索服务解析的内幕了。服务解析说白了就是引擎围着 CallSiteVisitor 转圈圈,所谓的解析服务,其实就是访问调用点了。

protected virtual TResult VisitCallSite(ServiceCallSite callSite, TArgument argument)
{
    if (!_stackGuard.TryEnterOnCurrentStack()) // 一些校验,分栈啥的
    {
        return _stackGuard.RunOnEmptyStack(VisitCallSite, callSite, argument);
    }
    switch (callSite.Cache.Location)
    {
        case CallSiteResultCacheLocation.Root: // 单例
            return VisitRootCache(callSite, argument);
        case CallSiteResultCacheLocation.Scope: // 作用域
            return VisitScopeCache(callSite, argument);
        case CallSiteResultCacheLocation.Dispose: // 瞬态
            return VisitDisposeCache(callSite, argument);
        case CallSiteResultCacheLocation.None: // 不缓存(ConstantCallSite)
            return VisitNoCache(callSite, argument);
        default:
            throw new ArgumentOutOfRangeException();
    }
}

​ 为了方便展示,我们这里的解析器都拿运行时来说,因为内部是反射,而emit、expression实在是难以观看。

3.4.1 VisitRootCache

​ 那我们来看看单例的情况下,是如何访问的:

protected override object? VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
{
    if (callSite.Value is object value)
    {
        // Value already calculated, return it directly
        return value;
    }
    var lockType = RuntimeResolverLock.Root;
    // 单例都是直接放根作用域的
    ServiceProviderEngineScope serviceProviderEngine = context.Scope.RootProvider.Root;
    lock (callSite)
    {
        // 这里搞了个双检锁来确保在多线程环境中,同一时间只有一个线程可以执行接下来的代码块。
        // Lock the callsite and check if another thread already cached the value
        if (callSite.Value is object callSiteValue)
        {
            return callSiteValue;
        }
        object? resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext
        {
            Scope = serviceProviderEngine,
            AcquiredLocks = context.AcquiredLocks | lockType
        });
        // 捕获可销毁的服务
        serviceProviderEngine.CaptureDisposable(resolved);
        // 缓存解析结果到调用点上
        callSite.Value = resolved;
        return resolved;
    }
}

​ 好,可以看到真正解析调用点的主角出来了 VisitCallSiteMain,那这里的 CallSiteKind 上面计算 ServiceCallSite 时呢已经讲的很清楚啦,咱对号入座就行了

protected virtual TResult VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
{
    switch (callSite.Kind)
    {
        case CallSiteKind.Factory:
            return VisitFactory((FactoryCallSite)callSite, argument);
        case  CallSiteKind.IEnumerable:
            return VisitIEnumerable((IEnumerableCallSite)callSite, argument);
        case CallSiteKind.Constructor:
            return VisitConstructor((ConstructorCallSite)callSite, argument);
        case CallSiteKind.Constant:
            return VisitConstant((ConstantCallSite)callSite, argument);
        case CallSiteKind.ServiceProvider:
            return VisitServiceProvider((ServiceProviderCallSite)callSite, argument);
        default:
            throw new NotSupportedException(SR.Format(SR.CallSiteTypeNotSupported, callSite.GetType()));
    }
}

​ 我们就看看最经典的通过构造函数创建服务实例的调用点 ConstructorCallSite,很显然就是new嘛,只不过可能构造中依赖其它服务,那就递归创建呗。easy,其它几种太简单了大家自己去看看吧。

protected override object VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
{
    object?[] parameterValues;
    if (constructorCallSite.ParameterCallSites.Length == 0)
    {
        parameterValues = Array.Empty<object>();
    }
    else
    {
        parameterValues = new object?[constructorCallSite.ParameterCallSites.Length];
        for (int index = 0; index < parameterValues.Length; index++)
        {
            // 递归构建依赖的服务
            parameterValues[index] = VisitCallSite(constructorCallSite.ParameterCallSites[index], context);
        }
    }
    // new (xxx)
    return constructorCallSite.ConstructorInfo.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: parameterValues, culture: null);
}

3.4.2 VisitScopeCache

​ 在访问单例缓存的时候呢,仅仅通过了一个double check lock就搞定了,因为人家全局的嘛,咱再来看看访问作用域缓存,会不会有什么不一样

protected override object? VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
{
    // Check if we are in the situation where scoped service was promoted to singleton
    // and we need to lock the root
    return context.Scope.IsRootScope ?
        VisitRootCache(callSite, context) :
        VisitCache(callSite, context, context.Scope, RuntimeResolverLock.Scope);
}

​ 哈哈,它果然很不一般啊,上来就来检查我们是否是 root scope。如果是这种case呢,则走 VisitRootCache。但是奇怪啊,为什么访问 scope cache,所在 engine scope 能是 root scope?

​ 还记得 ServiceProvider 获取的服务实例的核心方法吗?engine scope 他是传进来的,如果我们给一个 root scope,自然就出现的这种case,只是这种 case 特别罕见。

internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)

​ VisitCache 的同步模型写的实在是酷,我们看 RuntimeResolverLock 的枚举就两个:Scope = 1 和 Root = 2

  • AcquiredLocks=Scope时
  • 那 AcquiredLocks&false==0 显然成立,申请锁,也就是尝试独占改作用域的ResolvedServices
  • 申请成功进入同步块,重新计算AcquiredLocks|true=1
  • 如此,在该engine scope 中这条链路上的调用点都被占有,直到结束
  • AcquiredLocks=Root 时
    • 那显然 engine scope 也应该是 root scope,那么走 VisitRootCache case
    • 在 VisitRootCache 通过DCL锁住 root scope 上链路涉及的服务点,直至结束

​ 至此我们应该不难看出这个设计的精妙之处,即在非 root scope(scope生命周期)中,scope之间是互相隔离的,没有必要像 root scope(singleton生命周期)那样,在所有scope中独占服务点。

private object? VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine
{
    bool lockTaken = false;
    object sync = serviceProviderEngine.Sync;
    Dictionary<ServiceCacheKey, object?> resolvedServices = serviceProviderEngine.ResolvedServices;

    if ((context.AcquiredLocks & lockType) == 0)
    {
        Monitor.Enter(sync, ref lockTaken);
    }
    try
    {
        // Note: This method has already taken lock by the caller for resolution and access synchronization.
        // For scoped: takes a dictionary as both a resolution lock and a dictionary access lock.
        if (resolvedServices.TryGetValue(callSite.Cache.Key, out object? resolved))
        {
            return resolved;
        }
        // scope服务的解析结果是放在engine scope的ResolvedServices上的,而非调用点
        resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext
        {
            Scope = serviceProviderEngine,
            AcquiredLocks = context.AcquiredLocks | lockType
        });
        serviceProviderEngine.CaptureDisposable(resolved);
        resolvedServices.Add(callSite.Cache.Key, resolved);
        return resolved;
    }
    finally
    {
        if (lockTaken)
        {
            Monitor.Exit(sync);
        }
    }
}

3.4.3 VisitDisposeCache

​ 我们看最后一个,也就是 Transient case

protected override object? VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
{
    return context.Scope.CaptureDisposable(VisitCallSiteMain(transientCallSite, context));
}
C# 复制 全屏

​ 异常的简单,我们沿用了scope的设计,但是我们没有进行任何缓存行为。即,每次都去访问调用点。

【ASP.NET Core】MVC过滤器:常见用法 - 东邪独孤 - 博客园

mikel阅读(297)

来源: 【ASP.NET Core】MVC过滤器:常见用法 – 东邪独孤 – 博客园

前面老周给大伙伴们演示了过滤器的运行流程,大伙只需要知道下面知识点即可:

1、过滤器分为授权过滤、资源访问过滤、操作方法(Action)过滤、结果过滤、异常过滤、终结点过滤。上一次咱们没有说异常过滤和终结点过滤,不过老周后面会说的。对这些过滤器,你有印象就行了。

2、所有过滤器接口都有同步版本和异步版本。为了让伙伴不要学得太累,咱们暂时只说同步版本的。

3、过滤器的应用可以分为全局和局部。全局先运行,局部后运行。全局在应用程序初始化时配置,局部用特性类来配置。

4、实际应用中,我们不需要实现所有过滤器接口,需要啥就实现啥即可。比如,你想在 Action 调用后修改一些东西,那实现 IActionFilter 接口就好了,其他不用管。

 

本篇咱们的重点在于“用”,光知道是啥是不行的,得拿来用才是硬道理。

我们先做第一个练习:阻止控制器的参数从查询字符串获取数据。

什么意思呢?咱们知道,MVC 在模型绑定时,会从 N 个 ValueProvider 中提取数据值,包括 QueryString、Forms、RouteData 等。其中,QueryString 就是URL的查询字符串。比如,咱们写一个这样的控制器:

复制代码
public class GameController : ControllerBase
{
    [HttpGet("game/play")]
    public string Play(Game g)
    {
        if(ModelState.IsValid == false)
        {
            return "你玩个寂寞";
        }
        return $"你正在玩{g.Year}年出的《{g.GameName}》游戏";
    }
}

public class Game
{
    /// <summary>
    /// 游戏序列号
    /// </summary>
    public string? GameSerial { get; set; }

    /// <summary>
    /// 游戏名称
    /// </summary>
    public string? GameName { get; set; }

    /// <summary>
    /// 谁发行的
    /// </summary>
    public string? Publisher { get; set; }

    /// <summary>
    /// 哪一年发行的
    /// </summary>
    public int Year { get; set; }
}
复制代码

这个通过 /game/play?gameserial=DDSBYCL-5K2FF&gamename=伏地魔三世&publisher=无德无能科技有限公司&year=2017 这样的URL就能传递数据给 g 参数。

这里我不做 HTML 页了,直接通过 MapGet 返回 HTML 内容。

复制代码
app.MapGet("/", async (HttpContext context) =>
{
    string html = """
    <!DOCTYPE html>
    <html>
        <head>
            <title>试试看</title>
            <style>
                label {
                    min-width: 100px;
                    display: inline-block;
                }
            </style>
        </head>
        <body>
            <div>
                <label for="gserial">游戏序列号:</label>
                <input id="gserial" type="text" />
            </div>
            <div>
                <label for="gname">游戏名称:</label>
                <input id="gname" type="text" />
            </div>
            <div>
                <label for="pub">发行者:</label>
                <input type="text" id="pub" />
            </div>
            <div>
                <label for="year">发行年份:</label>
                <input id="year" type="text"/>
            </div>
            <div>
                <button onclick="reqTest()">确定</button>
            </div>
            <p id="res"></p>

            <script>
                function reqTest() {
                    let serial= document.getElementById("gserial").value;
                    let name = document.getElementById("gname").value;
                    let pub = document.getElementById("pub").value;
                    let year = parseInt(document.getElementById("year").value, 10);
                    let result = document.getElementById("res");
                    const url = `/game/play?gameSerial=${serial}&gamename=${name}&publisher=${pub}&year=${year}`;
                    fetch(url, { method: "GET" })
                        .then(response => {
                            response.text().then(txt => {
                                result.innerHTML = txt;
                            });
                        });
                }
            </script>
        </body>
    </html>
    """;
    var response = context.Response;
    response.Headers.ContentType = "text/html; charset=UTF-8";
    await response.WriteAsync(html);
});
复制代码

设置响应的 Content-Type 头时一定要指定字符集是 UTF-8 编码,这样可以免去 99.999% 的乱码问题。向服务器发送请求是通过 fetch 函数实现的。

比如,咱们在页面上填写:

然后点一下“确定”按钮,提交成功后服务将响应:

你正在玩2022年出的《法外狂徒大冒险》游戏

模型绑定的数据是从查询字符串提取出来的。现在,咱们写一个过滤器,阻止 QueryStringValueProvider 提供查询字符串数据。而 QueryStringValueProvider 实例是由 QueryStringValueProviderFactory 工厂类负责创建的。因此,需要写一个过滤器,在模型绑定之前删除 QueryStringValueProviderFactory 对象,这样模型绑定时就不会读取 URL 中的查询字符串了。

于是,重点就落在选用哪种过滤器。关键点是:必须在模型绑定前做这项工作。所以,Action过滤器、结果过滤器就别指望了,而且肯定不是授权过滤器,那就剩下资源过滤器了。

咱们写一个自定义的资源过滤器—— RemoveQueryStringProviderFilter,实现的接口当然是 IResourceFilter 了。

复制代码
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;

public class RemoveQueryStringProviderFilter : IResourceFilter
{
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        // 空空如也
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var qsValueProviders = context.ValueProviderFactories.OfType<QueryStringValueProviderFactory>();
        if (qsValueProviders != null && qsValueProviders.Any())
        {
            context.ValueProviderFactories.RemoveType<QueryStringValueProviderFactory>();
        }
    }
}
复制代码

我们要做的事情是在模型绑定之前才有效,所以 OnResourceExecuted 方法不用管,留白即可。在 OnResourceExecuting 方法中,首先用 ValueProviderFactories.OfType<T> 方法找出现有的 QueryStringValueProviderFactory 对象,若找到,用 RemoveType 方法删除。

把这个过滤器应用于全局。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
    options.Filters.Add<RemoveQueryStringProviderFilter>();
});
var app = builder.Build();

现在,咱们再运行应用程序,输入游戏信息。

点击“确定”按钮后,发现服务未响应正确的内容。

你正在玩0年出的《》游戏

由于无法从查询字符串中提取到数据,所以返回默认属性值。为什么不是返回“你玩个寂寞”呢?因为模型绑定并没有出错,也未出现验证失败的值,只是未提供值而已,即 ModelState.IsValid 的值依然是 true 的。

 

下面咱们看看异常过滤器怎么用。

异常过滤器最好定义为局部的,而非全局。使用局部过滤器的好处是针对性好,全局的话你用一个万能异常处理好像过于简单。当然了,如果你的项目可以这样做,就随便用。

这里我定义一个局部的异常过滤器,通过特性方式应用到操作方法上。

复制代码
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CustExceptionFilterAttribute : Attribute, IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        // 看看有没有异常
        if (context.Exception is null || context.ExceptionHandled)
            return;
        // 有异常哦,修改一下返回结果
        ContentResult result = new();
        result.Content = "出错啦,伙计。错误消息:" + context.Exception.Message;
        // 设置返回结果
        context.Result = result;
        // 标记异常已处理
        context.ExceptionHandled = true;
    }
}
复制代码

首先,context 参数是一种上下文对象,它在多上异常过滤器间共享实例(你可能实现了很多个异常过滤器)。context.Exception 属性引用的是异常类对象;注意一个有趣的属性 ExceptionHandled,它是一个 bool 类型的值,表示“我这个异常过滤器是否已处理过了”。这个有啥用呢?由于上下文是共享的,当你的某个异常过滤器设置了 ExceptionHandled 为 true,那么,其他异常过滤器也可以读这个属性。这样就可以实现:啊,原来有人处理过这个异常了,那我就不处理了。即 A 过滤器处理异常后设置为已处理,B 过滤器可以检查这个属性的值,如果没必要处理就跳过。

下面写一个控制器,并在方法成员上应用前面定义的异常过滤器。

复制代码
public class AbcController : ControllerBase
{
    [HttpGet("calc/add"), CustExceptionFilter]
    public int Add(int x, int y)
    {
        int r = x + y;
        if(r >= 1000)
        {
            throw new Exception("计算结果必须在1000以内");
        }
        return r;
    }
}
复制代码

此处我设定抛出异常的条件是:x、y 相加结果大于或等于 1000。

咱们以 GET 方式调用,URL 为 /calc/add?x=599&y=699,这样会发生异常。服务器的响应为:

 

最后一个例子是结果过滤器。咱们要实现在响应消息中添加自定义 Cookie。

复制代码
public class SetCookieResultFilter : IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    {
        // 不能在这里写 Cookie
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        HttpResponse response = context.HttpContext.Response;
        // 设置Cookie
        response.Cookies.Append("X-VER", "3.0");
    }
}
复制代码

这里要注意,咱们要写 Cookie 必须在 OnResultExecuting 方法中处理,不能在 OnResultExecuted 方法中写。因为当 OnResultExecuted 方法执行时,响应消息头已经被锁定了,Cookie 是通过 set-cookie 标头实现的,此时无法修改 HTTP 头了,写 Cookie 会抛异常。所以只能在 OnResultExecuting  方法中写。

定义一个控制器。

public class DemoController : ControllerBase
{
    [HttpGet("abc/test")]
    public string Work() => "山重水复疑无路";
}

将自定义的结果过滤器添加为全局。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
    options.Filters.Add<SetCookieResultFilter>();
});
var app = builder.Build();

好,试试看。

 

Layui实现图片列表并且可以放大查看 - Core、陈 - 博客园

mikel阅读(373)

来源: Layui实现图片列表并且可以放大查看 – Core、陈 – 博客园

首先建一个DIV层

复制代码
 1 <div class="layui-row layui-col-space10">
 2     <div class="layui-col-md12">
 3         <div class="layui-card">
 4             <div class="layui-card-body">
 5                 <fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
 6                     <legend>图片列表</legend>
 7                 </fieldset>
 8                 <div class="layui-row layui-col-space30" style="height: 300px; overflow:auto" id="LAY_Images">                            
 9                 </div>
10             </div>
11         </div>
12     </div>
13 </div>
复制代码

 

然后写一个数据请求的方法

复制代码
 1          //请求图像数据
 2                 $.ajax({
 3                     url: "接口路径",
 4                     data: { 'Id': 1 },
 5                     type: "post",
 6                     dataType: "json",
 7                     headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 8                     success: function (data) {
 9                         $("#LAY_Images").empty();
10                         $.each(data.data, function (index, item) {
11                             $("#LAY_Images").append(
12                                 "<div class='layui-col-md2 ew-datagrid-item'>" +
13                                 "<div class='project-list-item'>" +
14                                 "<img class='project-list-item-cover' src='" + item.imgname + "' onclick='previewImg(this)' />" +
15                                 "</div>" +
16                                 "</div>"
17                             );
18                         })
19                         form.render($('#LAY_Images'));
20                     },
21                     error: function (data) {
22 
23                     }
24                 });
复制代码

 

这样就渲染好图片的列表了,如果不想要放大功能,去掉onclick事件就可以了,如果需要,加上下面的方法

复制代码
 1      //点击图片放大查看
 2         function previewImg(obj) {
 3             var img = new Image();
 4             img.src = obj.src;
 5             var height = img.height; //获取图片高度
 6             var width = img.width; //获取图片宽度
 7             if (height > 1000 || width > 800) {
 8                 height = height / 1.5;
 9                 width = width / 1.5;
10             }
11             var imgHtml = "<img src='" + obj.src + "' style='width: " + width + "px;height:" + height + "px'/>";
12             //弹出层
13             layer.open({
14                 type: 1,
16                 offset: 'auto',
17                 area: [width + 'px', height + 'px'],
18                 shadeClose: true,//点击外围关闭弹窗
19                 scrollbar: true,//不现实滚动条
20                 title: false, //不显示标题
21                 content: imgHtml, //捕获的元素,注意:最好该指定的元素要存放在body最外层,否则可能被其它的相对元素所影响
22                 cancel: function () {
23                     
24                 }
25             });
26         }
复制代码

这样基本就可以做出一个图片列表框出来了

 

原生js使用FileReader将文件转成base64_js filereader base64-CSDN博客

mikel阅读(278)

来源: 原生js使用FileReader将文件转成base64_js filereader base64-CSDN博客

一、FileReader介绍
FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容。可以使用File对象指定要读取的文件。

File对象可以是来自用户在一个type类型为file的input元素上选择文件后返回的FileList对象。

<input type=”file” name=”” id=””>

注意: FileReader仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中直接使用路径名简单地读取文件。

二、FileReader的使用
FileReader是一个构造函数,需要new之后拿到FileReader的实例。

var reader = new FileReader();
1
实例的方法:
reader.abort():中止读取操作。
reader.readAsArrayBuffer():读取完成后,触发onload事件,result 属性中保存的是文件的 ArrayBuffer 数据对象。
reader.readAsDataURL():读取完成后,触发onload事件,result属性中保存的是Base64文件内容。
reader.readAsText():读取完成后,触发onload事件,result属性中保存的是字符串文件内容。

实例的事件:

reader.onabort:在读取操作被中断时触发。
reader.onerror:在读取操作发生错误时触发。
reader.onload:在读取操作完成时触发。
reader.onloadstart:在读取操作开始时触发。
reader.onloadend:在读取操作结束时(要么成功,要么失败)触发。
实例的属性:

reader.error:返回读取文件时的错误信息
reader.readyState:表示FileReader状态的数字。0:还没有加载;1:正在加载;2:加载完成
reader.result:文件的内容
三、使用范例
obtn.onclick = function(){

var reader = new FileReader();
reader.readAsDataURL(f.files[0]); // 解析成base64格式
reader.onload = function () {
console.log(this.result); // 解析后的数据,如下图
}

}

obtn.onclick = function(){

var reader = new FileReader();
reader.readAsArrayBuffer(f.files[0]);// 解析成ArrayBuffer格式
reader.onload = function () {
console.log(this.result); // 解析后的数据,如下图
}

}

obtn.onclick = function(){

var reader = new FileReader();
reader.readAsText(f.files[0]); // 解析成Text格式
reader.onload = function () {
console.log(this.result); // 解析后的数据,如下图
}

}

以上,如有错误,请留言交流…
————————————————
版权声明:本文为CSDN博主「杨树林er」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_41636483/article/details/112589158

【Windows系统】查看和关闭139、445端口的方法_关闭139端口_jin_study的博客-CSDN博客

mikel阅读(294)

来源: 【Windows系统】查看和关闭139、445端口的方法_关闭139端口_jin_study的博客-CSDN博客

文章目录
前言
一、 Windows查看139、445端口的方法
二、关闭445端口的方法
三、关闭139端口的方法
前言
“航天派”公众号上一期文章介绍了“麒麟操作系统查看和关闭139、445端口的方法”,今天介绍“Windows系统查看和关闭139、445端口的方法”。

前期,尝试使用一些软件关闭139、445端口,结果效果不理想;尝试使用网络上的一些操作方法,效果也不佳。今天介绍的关闭端口方法是经过多次实践验证过的,是真实有效的。需要提前说明,使用本文关闭139、445端口的方法,要在操作完成之后进行系统重启才能生效。

一、 Windows查看139、445端口的方法
按Windows+R键,弹出运行窗口,输入cmd回车,然后在命令提示符窗口输入如下命令,查看139、445端口是否存在:

netstat -na | findstr 139
netstat -na | findstr 445
1
2
如下图,可以看到该计算机TCP协议下均存在139、445端口,且处于LISTENING状态,接下来将封闭该计算机的139、445端口。

二、关闭445端口的方法
(1)在命令提升符号窗口接着输入regedit,将弹出注册表编辑器。

(2)在注册表编辑器找到HKEY_LOCAL_MACHINE—>SYSTEM—>ControlSet001—>services,如下图。

(3)在services中,找到NetBT—>Parameters,然后选中Parameters右键新建DWORD(32-位)值,如下图。

(4)将新建的DWORD命名为“SMBDericeEnabled”,并将其值修改为0(默认就是0,可以不用改),如下图。

(5)打开控制面板,选择管理工具—>服务—>Server—属性,如下图。

(6)在属性窗口将启动类型改为“禁用”,将服务状态选为“暂停”,然后点击应用、确定即可,如下图。

(7)完成以上操作之后,重启计算机,再次使用netstat -na | findstr 445查询,发现445端口已经关闭。当然可以等关闭完139端口之后,一起重启计算机,下面介绍关闭139端口的方法。

三、关闭139端口的方法
关闭139端口的方法,相比关闭445端口的方法,要简单很多。

(1)通过控制面板或者直接双击状态栏中网络连接图标,打开“网络和共享中心”,选择“更改适配器设置”—>本地连接—>属性,然后选择Internet协议版本4(TCP/IPv4)—>高级—>WINS—>禁用TCP/IP上的NetBIOS(S),然后点击确定即可,如下图。

(2)完成以上操作之后,重启计算机,然后在命令提示符窗口输入netstat -na | findstr 139、netstat -na | findstr 445查看139、445端口是否已关闭。如下图,经查询,139、445端口已经不存在,已经被关闭。

以上介绍了Windows系统查看和关闭139、445端口的方法,是经过多次实践验证过的,方法靠谱,值得分享。
关注“航天派”微信公众号,了解以下更好实用内容:
————————————————
版权声明:本文为CSDN博主「jin_study」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jin_study/article/details/124198383

MVC将Base64 保存为图片 - 跑着的小强 - 博客园

mikel阅读(328)

来源: MVC将Base64 保存为图片 – 跑着的小强 – 博客园

前台传来Base64字符串。本来可以直接保存数据库返回给图片 但是这样对数据库负担太重 传输也费时间。一搬都是存个地址在数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public ActionResult Injpg(string base64str ) { 
        string data=base64str//要处理下字符串 ,之前的要截取掉 不然会报错
            byte[] arr = Convert.FromBase64String(data);
            using (MemoryStream ms = new MemoryStream(arr))
            {
                Bitmap bmp = new Bitmap(ms);
                string p = "/text.jpg";
                var w = Server.MapPath(p);
                bmp.Save(w, System.Drawing.Imaging.ImageFormat.Jpeg);
                //bmp.Save(@"d:\"test.bmp", ImageFormat.Bmp);
                //bmp.Save(@"d:\"test.gif", ImageFormat.Gif);
                //bmp.Save(@"d:\"test.png", ImageFormat.Png);
                ms.Close();
                return Content(p);
            }

解锁新技能 哈哈哈 不用后台压缩了,,,整理下代码  搭建微动态数据库表了

JS图片多个上传,并压缩为Base64 - 跑着的小强 - 博客园

mikel阅读(280)

来源: JS图片多个上传,并压缩为Base64 – 跑着的小强 – 博客园

首先是JS 批量上传

HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="Pic_pass">
           <p style="font-size: 20px;font-weight: bold;">请上传照片 </p>
           <p><span style="color: red">注:最多可以传3张</span></p>
           <div class="picDiv">
               <div class="addImages">
                   <input type="file" class="file" id="fileInput" multiple="" accept="image/png, image/jpeg, image/gif, image/jpg">
                   <div class="text-detail">
                       <span>+</span>
                       <p>点击上传</p>
                   </div>
               </div>
           </div>
       </div>

样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
.imageDiv {
          display:inline-block;
          width:160px;
          height:130px;
          -webkit-box-sizing:border-box;
          -moz-box-sizing:border-box;
          box-sizing:border-box;
          border:1px dashed darkgray;
          background:#f8f8f8;
          position:relative;
          overflow:hidden;
          margin:10px
      }
      .cover {
          position:absolute;
          z-index:1;
          top:0;
          left:0;
          width:160px;
          height:130px;
          background-color:rgba(0,0,0,.3);
          display:none;
          line-height:125px;
          text-align:center;
          cursor:pointer;
      }
      .cover .delbtn {
          color:red;
          font-size:20px;
      }
      .imageDiv:hover .cover {
          display:block;
      }
      .addImages {
          display:inline-block;
          width:160px;
          height:130px;
          -webkit-box-sizing:border-box;
          -moz-box-sizing:border-box;
          box-sizing:border-box;
          border:1px dashed darkgray;
          background:#f8f8f8;
          position:relative;
          overflow:hidden;
          margin:10px;
      }
      .text-detail {
          margin-top:40px;
          text-align:center;
      }
      .text-detail span {
          font-size:40px;
      }
      .file {
          position:absolute;
          top:0;
          left:0;
          width:160px;
          height:130px;
          opacity:0;
      }

JS代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
var userAgent = navigator.userAgent; //用于判断浏览器类型
         $(".file").change(function () {
             //获取选择图片的对象
             var cou = $('.imageDiv').length
             console.log(cou)
             var docObj = $(this)[0];
             var picDiv = $(this).parents(".picDiv");
             var fileList = docObj.files;
             console.log(fileList)
             console.log(fileList.length)
             console.log(picDiv)
             //循环遍历
             for (var i = 0; i < fileList.length; i++) {
                 //动态添加html元素
                 var picHtml = "<div class='imageDiv' > <img id='img" + fileList[i].name + "' /> <div class='cover'><i class='delbtn'>删除</i></div></div>";
                 picDiv.prepend(picHtml);
                 //获取图片imgi的对象
                 var imgObjPreview = document.getElementById("img" + fileList[i].name);
                 if (fileList && fileList[i]) {
                     formData.append("file", docObj.files[i]);
                     
                     //图片属性
                     imgObjPreview.style.display = 'block';
                     imgObjPreview.style.width = '160px';
                     imgObjPreview.style.height = '130px';
                     //imgObjPreview.src = docObj.files[0].getAsDataURL();
                     //火狐7以上版本不能用上面的getAsDataURL()方式获取,需要以下方式
                     if (userAgent.indexOf('MSIE') == -1) {
                         //IE以外浏览器
                         imgObjPreview.src = window.URL.createObjectURL(docObj.files[i]); //获取上传图片文件的物理路径;
                         
                         console.log(docObj.files[i]);
                         // var msgHtml = '<input type="file" id="fileInput" multiple/>';
                     else {
                         //IE浏览器
                         if (docObj.value.indexOf(",") != -1) {
                             var srcArr = docObj.value.split(",");
                             imgObjPreview.src = srcArr[i];
                             console.log(srcArr[i]);
                         else {
                             imgObjPreview.src = docObj.value;
                         }
                     }
                 }
             }
            
             $('#fliei1').click(function () {
                 console.log($(".file").files)
             })
             /*删除功能*/
             $(".delbtn").click(function () {
                 var _this = $(this);
                 _this.parents(".imageDiv").remove();
             });
         });
     })

效果

 

但是图片都是没有压缩的 、

1
 

 

本来JS 是没有办法压缩的 但是H5的画布可以 可以用画布压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
   function dealImage(path, obj) {
            var img = new Image();
            img.src = path;
            img.onload = function () {
                var that = this;
                var w = that.width,
                    h = that.height,
                    scale = w / h;
                w = obj.width || w;
                h = obj.height || (w / scale);
                var quality = 0.85; //压缩质量 1是不压缩
                var canvas = document.createElement('canvas');
                var ctx = canvas.getContext('2d');
                var anw = document.createAttribute("width");
                anw.nodeValue = w;
                var anh = document.createAttribute("height");
                anh.nodeValue = h;
                canvas.setAttributeNode(anw);
                canvas.setAttributeNode(anh);
                ctx.drawImage(that, 0, 0, w, h);
                if (obj.quality && obj.quality <= 1 && obj.quality > 0) {
                    quality = obj.quality;
                }
                var base64 = canvas.toDataURL('image/jpeg', quality);
                //最后返回压缩后的base64编码
                console.log(base64)
            }
        }
<br>
<br>              var imgReaderI = new FileReader();<br>               //转换base64编码
                        imgReaderI.readAsDataURL(docObj.files[i]);
                        console.log(imgReaderI)
                        var size=docObj.files[i].size
                        imgReaderI.onloadend = function (evt) {
                            //压缩
                                dealImage(this.result, { width: 180 });
                            }
                        }

有一点 docObj.files[i] 这个表示选择的图片可以传到后台直接保存 但是没有压缩。

 

1个1.9M 压缩过后28.2K 还是特别客观的 接下来 我们就可以吧这个字符串通过AJAX方式传入后台。进行保存或者直接放入数据库,下一篇 我会写后台保存方法MVC 保存到服务器 返回相对路径

前端图片上传使用,base64直接上传,base64转化成file上传,form表单直接上传文件,element-ui上传图片_base64 如何上传 fs-CSDN博客

mikel阅读(285)

来源: 前端图片上传使用,base64直接上传,base64转化成file上传,form表单直接上传文件,element-ui上传图片_base64 如何上传 fs-CSDN博客

最近项目中使用到了图片上传功能,作为前端开发对此应该不陌生,正常来说图片会有一个单独存储的服务,例如现在公司使用minio统一集中管理,直接部署在docker上面非常方便,下面记录一下使用经历过的图片上传

1.base64直接上传
这种base64直接上传给后端,然后后端拿到数据之后就能自行处理,一般前端会对base64数据前面拼接 “data:image/jpeg;base64,” + base64数据;代表图片格式,其余后端自行处理,相对来说这种方式对前端更加友好,上传方式跟普通数据上传完全相同,然后后端返回图片存储的相对路径,前端进行拼接ip前缀就能正常展示;如下面代码:

$.ajax({
url: uploadFea,
type: “POST”,
dataType: “JSON”,
processData: false,
beforeSend: function () {
console.log(“正在上传特征值文件,请稍候”);
},
headers: {
“Content-Type”: “application/json;charset=UTF-8”,
},
data: {
platform: “web”,
uploadFile: feaFile,
name: fileName + “_” + guid(),
},
success: (res) => {
console.log(res.fileSuccess);

},
});
2.form表单方式的提交
这种是类似于form表单提交,这样其实是类似于element-ui中的上传,本质就是表单上传,而且方法提供的非常全面直接复制就行,这样一般也会有几种情况;

一种是直接提交给后端,一种是传base64数据给后端,还一种情况是前端获取到的图片数据是硬件提供的数据(例如身份证读卡器)一般是直接给你base64数据,但是后端接口不支持直接传base64,这样就需要前端童鞋处理一下base64数据了,然后再以表单提交文件形式传给后端了

1.直接提交给后端就是如下:

<form action=”http://192.168.10.94:3050/serverData/upload” method=”POST” enctype=”multipart/form-data”>
<input type=”file” name=”tupian” multiple=”multiple”>
<input type=”text” name=”id” id=”name”>
<input type=”submit”value=”提交”id=”btnn”>
</form>
这是自己本地使用node做的一个简单图片上传接口,接口能接收到文件数据,然后通过重命名移动等操作将处理后的图片文件保存到对应服务器目录,后端能直接获取到文件名称后缀以及数据等信息,自习看下element-ui其实本质就是一个封装好的表单提交,如下:

<el-upload class=”avatar-uploader” :style=”styles” :http-request=”uploadImg” :on-progress=”uploadVideoProcess” :disabled=”isDisabled”>
<el-progress type=”circle” v-show=”videoFlag” :percentage=”videoUploadPercent” class=”circle”></el-progress>

</el-upload>
2.element-ui中提供了 :http-request 方法我们能够使用,可以直接上传文件或者上传base64数据如:

//后端不需要base64
uploadImg (file) {
//这里file其实就是上传的文件,如果后端需要此类上传我们就能直接使用
$.ajax({
type: ‘post’,
url: upLodUrl,
data: file,
dataType: ‘text’,
processData: false, // bu要去处理发送的数据
contentType: false, //不要去设置Content-Type请求头
success: function(res) {
url = res.data
}
})
}

//后端需要base64,就需要对获取到的数据进行处理
uploadImg (file) {
let reader = new FileReader()
reader.readAsDataURL(file.file)
reader.onload =()=>{
let dataURL = reader.result // 获取到base64编码 包括前缀data:image/jpeg;base64,
}
//此时可以使用最上面直接上传base64的方式进行上传了
}
3.最后一种是最坑的一种,由于图片数据是硬件获取到的,或者是通过摄像头拍照拿到的数据,一般都是base64数据,恰好后端又不支持前端直接上传图片,此时就需要前端对base数据处理,转化成能直接上传的file,就类似于转化成 new FormData()

let bytes = window.atob(base64)
let ab = new ArrayBuffer(bytes.length)
let ia = new Uint8Array(ab)
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i)
}
let imgs = new File([ab], name, { type: ‘image/jpg’ })
const formData = new FormData()
formData.append(‘file’, imgs)

//上传操作 下面是前面几篇文章中讲的axios封装的方法,并无不同
const data =await upload(formData)
console.log(data)
//此处上传无论是直接使用ajax还是axios上传都一样,直接把formData放到data那里就行,不需要键值对形势
//ajax上传就类似于第二步中的 $.ajax上传,formData就是file,直接使用即可
最后第三种需要注意点,let imgs = new File([ab], name, { type: ‘image/jpg’ }) 中的name指的是你自己定义图片的名称,然后生成的formData就包含name以及自己定义的type:’image/jpg’ ,一般直接form表单上传name是包含后缀名的,例如图片.jpg等,后端可能会根据后缀去定义图片格式,前端上传命名时候需要注意点,可以自己随意添加后缀名或者直接让后端从type中去取,或者是让后端自己随便定义即可

补充直接上传二进制文件

let fileData = file.file; // 拿到file后,转为FormData格式,这样再发送给后端就可以了
let formData = new FormData();
formData.append(“img”, fileData);
api_upload(formData) // 发起请求
.then((res)=>{
console.log(res)
})
.catch((err)=>{
console.log(err)
})

//此处直接上传file.file 为二进制文件,不需要将数据转成字符串或其他

————————————————
版权声明:本文为CSDN博主「一梦成回忆」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/H_Elie/article/details/115471125

JS,base64编码的图片上传_js res.photobase64 如何上传-CSDN博客

mikel阅读(269)

来源: JS,base64编码的图片上传_js res.photobase64 如何上传-CSDN博客

公司的项目要拍照上传图片,但用了第三方平台,拍照接口返回的是照片的base64编码,如何把base64编码的图片上传到服务器呢?思路上来说:首先,要将base64编码转换成file,再将file塞到from中,提交到后台即可。直接上代码

/**
* imageURI为图片的base64编码,不包含头部部分:data:img/jpg;base64,
**/
var fileName=(new Date()).getTime()+”.jpeg”; //随机文件名
var imgfile=convertBase64UrlToImgFile(imageURI,fileName,’image/jpeg’); //转换成file
var formData = new FormData();
formData.append(‘file’, imgfile); //放到表单中,此处的file要和后台取文件时候的属性名称保持一致
$.ajax({
url:上传文件的后台处理url,
type:”POST”,
cache:false,
data:formData,
processData:false, //不对参数进行转换序列号
contentType:false, //fromData上传文件时将其设置为false
success:function(data){
//成功执行的代码逻辑
},
error:function(data){
//失败执行的代码逻辑
}
});
其中convertBase64UrlToImgFile函数代码如下:

/**
* 有些浏览器(如edge)不支持new File,所以为了兼容,base64要先转换成blob再设置type,name,lastModifiedDate
* 属性间接转换成文件,而不推荐直接new File()的方式
**/
function convertBase64UrlToImgFile(urlData,fileName,fileType) {
var bytes = window.atob(urlData); //转换为byte
//处理异常,将ascii码小于0的转换为大于0
var ab = new ArrayBuffer(bytes.length);
var ia = new Int8Array(ab);
var i;
for (i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
//转换成文件,添加文件的type,name,lastModifiedDate属性
var blob=new Blob([ab], {type:fileType});
blob.lastModifiedDate = new Date();
blob.name = fileName;
return blob;
}
但在调用时,后台始终获取不到设置的filename,一直是blob。查资料发现,需要在formdata.append时加上文件名,如下:

formData.append(‘file’, imgfile,fileName);
如此,问题解决。
————————————————
版权声明:本文为CSDN博主「southArbor」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010295735/article/details/89029399