标题:从零开始实现ASP.NET Core MVC的插件式开发(九) - 如何启用预编译视图

作者:Lamond Lu

地址:https://www.cnblogs.com/lwqlun/p/13992077.html

源代码:https://github.com/lamondlu/Mystique

适用版本:.NET Core 3.1, .NET 5

前景回顾

简介

在这个项目创建的时候,项目的初衷是使用预编译视图来呈现界面,但是由于多次尝试失败,最后改用了运行时编译视图,这种方式在第一次加载的时候非常的慢,所有的插件视图都要在运行时编译。这个问题困扰我很久。近日,在几位同道的共同努力下,终于实现了这种加载方式。


此篇要鸣谢网友 j4587698yang-er 对针对当前项目的支持,你们的思路帮我解决了当前项目针对不能启用预编译视图的2个主要的问题

  • 在当前项目目录结构下,启动时加载组件,组件预编译视图不能正常使用
  • 运行时加载组件之后,组件中的预编译视图不能正常使用

升级.NET 5

随着.NET 5的发布,当前项目也升级到了.NET 5版本。

整个升级的过程比我预想的简单的多,只是修改了一下项目使用的Target fremework。重新编译打包了一下插件程序,项目就可以正常运行了,整个过程中没有产生任何因为版本升级导致的编译问题。

预编译视图不能使用的问题

在升级了.NET 5之后,我重新尝试在启动时关闭了运行时编译,加载预编译视图View, 借此测试.NET 5对预编译视图的支持情况。

    public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration)
{
... IMvcBuilder mvcBuilder = services.AddMvc(); ServiceProvider provider = services.BuildServiceProvider();
using (IServiceScope scope = provider.CreateScope())
{
... foreach (ViewModels.PluginListItemViewModel plugin in allEnabledPlugins)
{
CollectibleAssemblyLoadContext context = new CollectibleAssemblyLoadContext(plugin.Name);
string moduleName = plugin.Name; string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName, $"{moduleName}.dll");
string viewFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName, $"{moduleName}.Views.dll");
string referenceFolderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName); _presets.Add(filePath);
using (FileStream fs = new FileStream(filePath, FileMode.Open))
{
Assembly assembly = context.LoadFromStream(fs);
context.SetEntryPoint(assembly); loader.LoadStreamsIntoContext(context, referenceFolderPath, assembly); MystiqueAssemblyPart controllerAssemblyPart = new MystiqueAssemblyPart(assembly);
mvcBuilder.PartManager.ApplicationParts.Add(controllerAssemblyPart);
PluginsLoadContexts.Add(plugin.Name, context); BuildNotificationProvider(assembly, scope);
} using (FileStream fsView = new FileStream(viewFilePath, FileMode.Open))
{
Assembly viewAssembly = context.LoadFromStream(fsView);
loader.LoadStreamsIntoContext(context, referenceFolderPath, viewAssembly); CompiledRazorAssemblyPart moduleView = new CompiledRazorAssemblyPart(viewAssembly);
mvcBuilder.PartManager.ApplicationParts.Add(moduleView);
} context.Enable();
}
}
} AssemblyLoadContextResoving(); ...
}

运行项目之后,你会发现项目竟然会得到一个无法找到视图的错误。

这里的结果很奇怪,因为参考第一章的场景,ASP.NET Core默认是支持启动时加载预编译视图的。在第一章的时候,我们创建了1个组件,在启动时,直接加载到主AssemblyLoadContext中,启动之后,我们是可以正常访问到视图的。

在仔细思考之后,我想到的两种可能性。

  • 一种可能是因为我们的组件加载在独立的AssemblyLoadContext中,而非主AssemblyLoadContext中,所以可能导致加载失败
  • 插件的目录结构与第一章不符合,导致加载失败

但是苦于不能调试ASP.NET Core的源码,所以这一部分就暂时搁置了。直到前几天,网友j4587698 在项目Issue中针对运行时编译提出的方案给我的调试思路。

在ASP.NET Core中,默认的视图的编译和加载使用了2个内部类DefaultViewCompilerProviderDefaultViewCompiler。但是由于这2个类是内部类,所以没有办法继承并重写,更谈不上调试了。

j4587698的思路和我不同,他的做法是,在当前主项目中,直接复制DefaultViewCompilerProviderDefaultViewCompiler2个类的代码,并将其定义为公开类,在程序启动时,替换默认依赖注入容器中的类实现,使用公开的DefaultViewCompilerProvider DefaultViewCompiler类,替换ASP.NET Core默认指定的内部类。

根据他的思路,我新增了一个基于IServiceCollection的扩展类,追加了Replace方法来替换注入容器中的实现。

    public static class ServiceCollectionExtensions
{
public static IServiceCollection Replace<TService, TImplementation>(this IServiceCollection services)
where TImplementation : TService
{
return services.Replace<TService>(typeof(TImplementation));
} public static IServiceCollection Replace<TService>(this IServiceCollection services, Type implementationType)
{
return services.Replace(typeof(TService), implementationType);
} public static IServiceCollection Replace(this IServiceCollection services, Type serviceType, Type implementationType)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
} if (serviceType == null)
{
throw new ArgumentNullException(nameof(serviceType));
} if (implementationType == null)
{
throw new ArgumentNullException(nameof(implementationType));
} if (!services.TryGetDescriptors(serviceType, out var descriptors))
{
throw new ArgumentException($"No services found for {serviceType.FullName}.", nameof(serviceType));
} foreach (var descriptor in descriptors)
{
var index = services.IndexOf(descriptor); services.Insert(index, descriptor.WithImplementationType(implementationType)); services.Remove(descriptor);
} return services;
} private static bool TryGetDescriptors(this IServiceCollection services, Type serviceType, out ICollection<ServiceDescriptor> descriptors)
{
return (descriptors = services.Where(service => service.ServiceType == serviceType).ToArray()).Any();
} private static ServiceDescriptor WithImplementationType(this ServiceDescriptor descriptor, Type implementationType)
{
return new ServiceDescriptor(descriptor.ServiceType, implementationType, descriptor.Lifetime);
}
}

并在程序启动时,使用公开的MyViewCompilerProvider类,替换了原始注入类DefaultViewCompilerProvider

    public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration)
{
_serviceCollection = services; services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IMvcModuleSetup, MvcModuleSetup>();
services.AddScoped<IPluginManager, PluginManager>();
services.AddScoped<ISystemManager, SystemManager>();
services.AddScoped<IUnitOfWork, Repository.MySql.UnitOfWork>();
services.AddSingleton<INotificationRegister, NotificationRegister>();
services.AddSingleton<IActionDescriptorChangeProvider>(MystiqueActionDescriptorChangeProvider.Instance);
services.AddSingleton<IReferenceContainer, DefaultReferenceContainer>();
services.AddSingleton<IReferenceLoader, DefaultReferenceLoader>();
services.AddSingleton(MystiqueActionDescriptorChangeProvider.Instance); ... services.Replace<IViewCompilerProvider, MyViewCompilerProvider>();
}

MyViewCompilerProvider中, 直接返回了新定义的MyViewCompiler

    public class MyViewCompilerProvider : IViewCompilerProvider
{
private readonly MyViewCompiler _compiler; public MyViewCompilerProvider(
ApplicationPartManager applicationPartManager,
ILoggerFactory loggerFactory)
{
var feature = new ViewsFeature();
applicationPartManager.PopulateFeature(feature); _compiler = new MyViewCompiler(feature.ViewDescriptors, loggerFactory.CreateLogger<MyViewCompiler>());
} public IViewCompiler GetCompiler() => _compiler;
}

PS: 此处只是直接复制了ASP.NET Core源码中DefaultViewCompilerProvider DefaultViewCompiler2个类的代码,稍作修改,保证编译通过。

    public class MyViewCompiler : IViewCompiler
{
private readonly Dictionary<string, Task<CompiledViewDescriptor>> _compiledViews;
private readonly ConcurrentDictionary<string, string> _normalizedPathCache;
private readonly ILogger _logger; public MyViewCompiler(
IList<CompiledViewDescriptor> compiledViews,
ILogger logger)
{
...
} /// <inheritdoc />
public Task<CompiledViewDescriptor> CompileAsync(string relativePath)
{
if (relativePath == null)
{
throw new ArgumentNullException(nameof(relativePath));
} // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already
// normalized and a cache entry exists.
if (_compiledViews.TryGetValue(relativePath, out var cachedResult))
{ return cachedResult;
} var normalizedPath = GetNormalizedPath(relativePath);
if (_compiledViews.TryGetValue(normalizedPath, out cachedResult))
{ return cachedResult;
} // Entry does not exist. Attempt to create one. return Task.FromResult(new CompiledViewDescriptor
{
RelativePath = normalizedPath,
ExpirationTokens = Array.Empty<IChangeToken>(),
});
} private string GetNormalizedPath(string relativePath)
{
...
}
}

针对DefaultViewCompiler,这里的重点是CompileAsync方法,它会根据传入的相对路径,在加载的编译视图集合中加载视图。下面我们在此处打上断点,并模拟进入DemoPlugin1的主页。

看完这个调试过程,你是不是发现了点什么,当我们访问DemoPlugin1的主页路由/Modules/DemoPlugin/Plugin1/HelloWorld的时候,ASP.NET Core尝试查找的视图相对路径是·

  • /Areas/DemoPlugin1/Views/Plugin1/HelloWorld.cshtml
  • /Areas/DemoPlugin1/Views/Shared/HelloWorld.cshtml
  • /Views/Shared/HelloWorld.cshtml
  • /Pages/Shared/HelloWorld.cshtml
  • /Modules/DemoPlugin1/Views/Plugin1/HelloWorld.cshtml
  • /Views/Shared/HelloWorld.cshtml

而当我们查看现在已有的编译视图映射是,你会发现注册的对应视图路径确是/Views/Plugin1/HelloWorld.cshtml

下面我们再回过头来看看DemoPlugin1的目录结构

由此我们推断出,预编译视图在生成的时候,会记录当前视图的相对路径,而在主程序加载的插件的过程中,由于我们使用了Area来区分模块,多出的一级目录,所以导致目录映射失败了。因此如果我们将DemoPlugin1的插件视图目录结构改为以上提示的6个地址之一,问题应该就解决了。

那么这里有没有办法,在不改变路径的情况下,让视图正常加载呢,答案是有的。参照之前的代码,在加载视图组件的时候,我们使用了内置类CompiledRazorAssemblyPart, 那么让我们来看看它的源码。

    public class CompiledRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider
{
/// <summary>
/// Initializes a new instance of <see cref="CompiledRazorAssemblyPart"/>.
/// </summary>
/// <param name="assembly">The <see cref="System.Reflection.Assembly"/></param>
public CompiledRazorAssemblyPart(Assembly assembly)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
} /// <summary>
/// Gets the <see cref="System.Reflection.Assembly"/>.
/// </summary>
public Assembly Assembly { get; } /// <inheritdoc />
public override string Name => Assembly.GetName().Name; IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems
{
get
{
var loader = new RazorCompiledItemLoader();
return loader.LoadItems(Assembly);
}
}
}

这个类非常的简单,它通过RazorCompiledItemLoader类对象从程序集中加载的视图, 并将最终的编译视图都存放在一个RazorCompiledItem类的集合里。

    public class RazorCompiledItemLoader
{
public virtual IReadOnlyList<RazorCompiledItem> LoadItems(Assembly assembly)
{
if (assembly == null)
{
throw new ArgumentNullException(nameof(assembly));
} var items = new List<RazorCompiledItem>();
foreach (var attribute in LoadAttributes(assembly))
{
items.Add(CreateItem(attribute));
} return items;
} protected virtual RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
} return new DefaultRazorCompiledItem(attribute.Type, attribute.Kind, attribute.Identifier);
} protected IEnumerable<RazorCompiledItemAttribute> LoadAttributes(Assembly assembly)
{
if (assembly == null)
{
throw new ArgumentNullException(nameof(assembly));
} return assembly.GetCustomAttributes<RazorCompiledItemAttribute>();
}
}

这里我们可以参考前面的调试方式,创建出一套自己的视图加载类,代码和当前的实现一模一样

MystiqueModuleViewCompiledItemLoader

    public class MystiqueModuleViewCompiledItemLoader : RazorCompiledItemLoader
{
public MystiqueModuleViewCompiledItemLoader()
{
} protected override RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
} return new MystiqueModuleViewCompiledItem(attribute);
} }

MystiqueRazorAssemblyPart

    public class MystiqueRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider
{
public MystiqueRazorAssemblyPart(Assembly assembly)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
AreaName = areaName;
} public Assembly Assembly { get; } public override string Name => Assembly.GetName().Name; IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems
{
get
{
var loader = new MystiqueModuleViewCompiledItemLoader();
return loader.LoadItems(Assembly);
}
}
}

MystiqueModuleViewCompiledItem

    public class MystiqueModuleViewCompiledItem : RazorCompiledItem
{
public override string Identifier { get; } public override string Kind { get; } public override IReadOnlyList<object> Metadata { get; } public override Type Type { get; } public MystiqueModuleViewCompiledItem(RazorCompiledItemAttribute attr, string moduleName)
{
Type = attr.Type;
Kind = attr.Kind;
Identifier = attr.Identifier; Metadata = Type.GetCustomAttributes(inherit: true).ToList();
}
}

这里我们在MystiqueModuleViewCompiledItem类的构造函数部分打上断点。

重新启动项目之后,你会发现当加载DemoPlugin1的视图时,这里的Identifier属性其实就是当前编译试图项的映射目录。这样我们很容易就想到在此处动态修改映射目录,为此我们需要将模块名称通过构造函数传入,以上3个类的更新代码如下:

MystiqueModuleViewCompiledItemLoader

    public class MystiqueModuleViewCompiledItemLoader : RazorCompiledItemLoader
{
public string ModuleName { get; } public MystiqueModuleViewCompiledItemLoader(string moduleName)
{
ModuleName = moduleName;
} protected override RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
} return new MystiqueModuleViewCompiledItem(attribute, ModuleName);
}
}

MystiqueRazorAssemblyPart

    public class MystiqueRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider
{
public MystiqueRazorAssemblyPart(Assembly assembly, string moduleName)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
ModuleName = moduleName;
} public string ModuleName { get; } public Assembly Assembly { get; } public override string Name => Assembly.GetName().Name; IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems
{
get
{
var loader = new MystiqueModuleViewCompiledItemLoader(ModuleName);
return loader.LoadItems(Assembly);
}
}
}

MystiqueModuleViewCompiledItem

    public class MystiqueModuleViewCompiledItem : RazorCompiledItem
{
public override string Identifier { get; } public override string Kind { get; } public override IReadOnlyList<object> Metadata { get; } public override Type Type { get; } public MystiqueModuleViewCompiledItem(RazorCompiledItemAttribute attr, string moduleName)
{
Type = attr.Type;
Kind = attr.Kind;
Identifier = "/Modules/" + moduleName + attr.Identifier; Metadata = Type.GetCustomAttributes(inherit: true).Select(o =>
o is RazorSourceChecksumAttribute rsca
? new RazorSourceChecksumAttribute(rsca.ChecksumAlgorithm, rsca.Checksum, "/Modules/" + moduleName + rsca.Identifier)
: o).ToList();
}
}

PS: 这里有个容易疏漏的点,就是MystiqueModuleViewCompiledItem中的MetaData, 它使用了Identifier属性的值,所以一旦Identifier属性的值被动态修改,此处的值也要修改,否则调试会不成功。

修改完成之后,我们重启项目,来测试一下。

编译视图的映射路径动态修改成功,页面成功被打开了,至此启动时的预编译视图加载完成。

运行时加载编译视图

最后我们来到了运行加载编译视图的问题,有了之前的调试方案,现在调试起来就轻车熟路。

为了测试,我们再运行时加载编译视图,我们首先禁用掉DemoPlugin1, 然后重启项目,并启用DemoPlugin1

通过调试,很明显问题出在预编译视图的加载上,在启用组件之后,编译视图映射集合没有更新,所以导致加载失败。这也证明了我们之前第三章时候的推断。当使用IActionDescriptorChangeProvider重置Controller/Action映射的时候,ASP.NET Core不会更新视图映射集合,从而导致视图加载失败。

    MystiqueActionDescriptorChangeProvider.Instance.HasChanged = true;
MystiqueActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

那么解决问题的方式也就很清楚了,我们需要在使用IActionDescriptorChangeProvider重置Controller/Action映射之后,刷新视图映射集合。为此,我们需要修改之前定义的MyViewCompilerProvider, 添加Refresh方法来刷新映射。

    public class MyViewCompilerProvider : IViewCompilerProvider
{
private MyViewCompiler _compiler;
private ApplicationPartManager _applicationPartManager;
private ILoggerFactory _loggerFactory; public MyViewCompilerProvider(
ApplicationPartManager applicationPartManager,
ILoggerFactory loggerFactory)
{
_applicationPartManager = applicationPartManager;
_loggerFactory = loggerFactory;
Refresh();
} public void Refresh()
{
var feature = new ViewsFeature();
_applicationPartManager.PopulateFeature(feature); _compiler = new MyViewCompiler(feature.ViewDescriptors, _loggerFactory.CreateLogger<MyViewCompiler>());
} public IViewCompiler GetCompiler() => _compiler;
}

Refresh方法是借助ViewsFeature来重新创建了一个新的IViewCompiler, 并填充了最新的视图映射。

PS: 这里的实现方式参考了DefaultViewCompilerProvider的实现,该类是在构造中填充的视图映射。

根据以上修改,在使用IActionDescriptorChangeProvider重置Controller/Action映射之后, 我们使用Refresh方法来刷新映射。

    private void ResetControllActions()
{
MystiqueActionDescriptorChangeProvider.Instance.HasChanged = true;
MystiqueActionDescriptorChangeProvider.Instance.TokenSource.Cancel(); var provider = _context.HttpContext
.RequestServices
.GetService(typeof(IViewCompilerProvider)) as MyViewCompilerProvider;
provider.Refresh();
}

最后,我们重新启动项目,再次在运行时启用DemoPlugin1,进入插件主页面,页面正常显示了。

至此运行时加载与编译视图的场景也顺利解决了。

从零开始实现ASP.NET Core MVC的插件式开发(九) - 升级.NET 5及启用预编译视图的更多相关文章

  1. 从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件

    标题:从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/112 ...

  2. 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图

    标题:从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图 作者:Lamond Lu 地址:http://www.cnblogs ...

  3. 从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装

    标题:从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11260750. ...

  4. 从零开始实现ASP.NET Core MVC的插件式开发(五) - 插件的删除和升级

    标题:从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除 作者:Lamond Lu 地址:https://www.cnb ...

  5. 从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用

    标题:从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用. 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1171 ...

  6. 从零开始实现ASP.NET Core MVC的插件式开发(七) - 近期问题汇总及部分解决方案

    标题:从零开始实现ASP.NET Core MVC的插件式开发(七) - 问题汇总及部分解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/12 ...

  7. 从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案

    标题:从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun ...

  8. 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板

    标题:从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11155 ...

  9. asp.net mvc5轻松实现插件式开发

    在研究Nopcommece项目代码的时候,发现Nop.Admin是作为独立项目开发的,但是部署的时候却是合在一起的,感觉挺好 这里把他这个部分单独抽离出来, 主要关键点: 确保你的项目是MVC5 而不 ...

随机推荐

  1. Python基础笔记2-ruamel.yaml读写yaml文件

    上一篇笔记记录了Python中的pyyaml库对yaml文件进行读写,但了解到ruamel.yaml也能对yaml文件进行读写,于是想尝试一下它的用法. 一,注意 这里首先要更正一下网上大部分博客的说 ...

  2. JAVA基础 随机点名器案例

    1.1      案例介绍 随机点名器,即在全班同学中随机的找出一名同学,打印这名同学的个人信息. 此案例在我们昨天课程学习中,已经介绍,现在我们要做的是对原有的案例进行升级,使用新的技术来实现. 我 ...

  3. Js电子时钟

    简单版电子时钟,需要以下几个步骤 1. 封装一个函数 返回当前的时分秒 2. 使用定时器使当前以获取到的系统时间走动,每间隔一面调用 3. 把获取到的时间放到span盒子里,添加样式 效果展示  实现 ...

  4. bash 括号使用

    Bash 括号多种使用方式 ${} 变量初始化 ${param:-string} 若变量param为空或者未定义,则用在命令行中用string来替换${param:-string} 否则变量param ...

  5. Go语言基础知识01-用Go打个招呼

    每一种编程语言,从读一本好书开始 每一种编程语言,也从Helloworld开始 1. 环境准备 1.1 安装golang 在Ubuntu下,直接输入命令可以安装最新版本: $ sudo apt-get ...

  6. kafka-manage管理工具

    1 github地址   https://github.com/sheepkiller/kafka-manager-docker   2 启动   将参数传递给kafka-manager   对于版本 ...

  7. 【应用服务 App Service】App Service 中部署Java应用中文乱码现象

    问题情形 有时候部署在 Azure  App Service的 Java应用会出现乱码 详细日志 无 问题原因 因为 App Service默认的编码为gbk,所以在显示页面或传递中文字符时就会出现乱 ...

  8. MySQL备份和恢复[1]-概述

    备份类型 完全备份,部分备份 完全备份:整个数据集 部分备份:只备份数据子集,如部分库或表 完全备份.增量备份.差异备份 增量备份:仅备份最近一次完全备份或增量备份(如果存在增量)以来变化的数据,备份 ...

  9. Jmeter入门(6)- 参数化

    一.什么是参数化 为什么要参数化? 在发送大量的请求时,键对值是写死的,每次请求都需要去修改,无法实现快速添加的需求.想要快速实现该需求,就需要用到参数化. 什么是参数化? 根据需求动态获取数据并进行 ...

  10. JS DIV列表自动滚动带停顿,滚动到底部后自动滚动到顶部

    setInterval -- 间隔执行函数:element.scrollTop -- 元素滚动条距头部的距离: 因为执行代码需要时间,所以最终动态时间会比设置的要慢 var slide = new S ...