halo个人博客搭建及介绍

news2024/12/26 22:12:42

halo个人博客搭建及介绍

halo介绍

halo强大易用的开源建站工具,配合上丰富的模板与插件,帮助你构建你心中的理想站点。具体可以搜索下官网的搭建指南。

博客技术架构

后端

1.spring reactive ,响应式编程,代码风格简单及高并发队列优化相应

2.springboot + springdoc + webflux (RouterFunction )

3.themeleaf + spring _+ standardlect(方言) 4.r2db (兼容性,跨多中数据库,表字段二进制存储,结构单一,字段解析转到代码层面) 5.nginx

6.认证:webflux security 采用的cookie session 方案 ,webflux session 存在内存中

7.权限 角色 --rabc角色 模型 – 角色信息 、 菜单权限、接口 映射关系 默认的几种角色

/registry/roles/super-role 管理员

/registry/roles/anonymous 匿名

/registry/roles/authenticated 内部鉴权

api权限实力:前端登录后拿到用户信息,根据对应的权限控制页面 user–>this::me hasSecurityContext 查询权限

ReactiveAuthorizationManager
用户和密码鉴权之后调用,实现check 方法,鉴权角色(非anonymousUser用户的话,加上authenticated 角色和anonymous角色)具有的dependencies
rbac.authorization.halo.run/dependencies --依赖角色(卷积所有规则) --角色规则 
​
请求分为api resource 和非api resource
/**
     * @return true for requests to API resources, like /api/v1/nodes,
     * and false for non-resource endpoints like /api, /healthz
     */
    boolean isResourceRequest();
    
  规则匹配的的话 优先匹配原则 rabc role api verb(增删查改) who how what
  主要匹配api 路径
​

前端

1.console (业务框架(2.0最新版本) --前段代码和后端代码合一)

2.博客站点 themeLeaf 模板引擎 --代理 web 端口 ,实现跨域

代码风格,但有很多抽象,需要深入阅读

例如webflux 建立博客站点路由

  • org.springframework.web.servlet.ViewResolver
  • ViewResolvers 是负责为特定操作和区域设置获取 View 对象的对象。通常,控制器要求 ViewResolvers 转发到具有特定名称的视图(控制器方法返回的字符串),然后应用程序中的所有视图解析器按有序链执行,直到其中一个能够解析该视图,其中在返回 View 对象并将控制权传递给它以呈现 HTML 的情况下。
private RouterFunction<ServerResponse> createRouterFunction(RoutePattern routePattern) {
    return switch (routePattern.identifier()) {
        case POST -> postRouteFactory.create(routePattern.pattern());
        case ARCHIVES -> archiveRouteFactory.create(routePattern.pattern());
        case CATEGORIES -> categoriesRouteFactory.create(routePattern.pattern());
        case CATEGORY -> categoryPostRouteFactory.create(routePattern.pattern());
        case TAGS -> tagsRouteFactory.create(routePattern.pattern());
        case TAG -> tagPostRouteFactory.create(routePattern.pattern());
        case AUTHOR -> authorPostsRouteFactory.create(routePattern.pattern());
        case INDEX -> indexRouteFactory.create(routePattern.pattern());
        default ->
            throw new IllegalStateException("Unexpected value: " + routePattern.identifier());
    };
}
存储到 cachedRouters 来相应

搭建

参考官网搭建,建议使用niginx 搭建。

主题功能开发

以https://github.com/nineya/halo-theme-dream2.0/tree/1.0.5为基础开发,站在前人的肩膀上_

开发工具idea

安装npm管理工具nvm

安装node18(node与npm一一对应版本)

中间可能要设置淘宝镜像npm config get registry

  1. 开发环境准备

    • 安装 nodejs 版本需要在 15+
    • 主题目录下执行 npm i 安装依赖;
  2. npm 命令

    • npm run lint 执行代码风格校验。(windows /unix 开发风格)
    • npm run zip 执行安装包打包,在无须重新编译 js/css 时使用。
    • npm run build 执行主题打包操作,主题将被打包为压缩包文件存放在 dist/ 目录下,同时 source 目录下的文件也将被更新。
    • npm run build --devel 开发模式进行主题打包,jscss 不会被做压缩和混淆处理,方便排查问题。
    • npm run release --tag=$version 发布模式执行主题打包操作,将自动更新主题中的版本号,并使用这个版本标签重新创建 FreeCDN 清单文件。

github cdn 的使用 cdn 和chrome servicework实践

fork一下源项目自己进行开发

将一些公共的静态资源放在github上,通过cdn引入,博客打开速度就正常了

jsDelivr是一个免费、开源的加速CDN公共服务,托管了许多大大小小的项目,可加速访问托管的项目目录或图片资源。 他支持提供npmGithuWordPress上资源cdn服务。

jsDelivr 跟其他同类型服务还有什么不同之处呢? jsDelivr 将重心放在更快速的网路连线,利用 CDN 技术来确保每个地区的使用者都能获得最好的连线速度。 依据 jsDelivr 的说明,它们也是首个「打通中国大陆与海外的免费 CDN 服务」,网页开发者无须担心GFW问题而影响连线。 此外,jsDelivr 可将不同的 JavaScript 或 CSS libraries 整合在一起,透过一段链结来载入网站,非常方便! 如果你正在寻找类似服务,jsDelivr 是个不错的选择。

// github
https://cdn.jsdelivr.net/gh/user/repo@version/file

free cdn使用

servicework原理与使用参考

https://blog.nineya.com/archives/103.html

主题原理实现

主题自定义,spring reactive + Thymeleaf standardlect(方言)

image-20230812204006603.png
请求获取渲染,根据主题https://81.69.254.72/themes/theme-guozi/assets/css/theme.min.css?mew=1.0.6

请求路径中的主题名称获取对应的ThymeleafTemplateEngine 进行渲染

public static class HaloView extends ThymeleafReactiveView {
​
    @Autowired
    private TemplateEngineManager engineManager;
​
    @Autowired
    private ThemeResolver themeResolver;
​
    @Override
    public Mono<Void> render(Map<String, ?> model, MediaType contentType,
        ServerWebExchange exchange) {
        return themeResolver.getTheme(exchange).flatMap(theme -> {
            // calculate the engine before rendering
            setTemplateEngine(engineManager.getTemplateEngine(theme));
            exchange.getAttributes().put(PageCacheWebFilter.REQUEST_TO_CACHE, true);
            return super.render(model, contentType, exchange);
        });
    }
    ......

主题配置实现原理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

定义配置内容,数据映射的前端的组件,通过接口安装主题后,回显到控制台页面

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

后端逻辑对应解析主题的yaml文件,持久化下来

static List<Unstructured> loadThemeResources(Path themePath) {
    try (Stream<Path> paths = Files.list(themePath)) {
        List<FileSystemResource> resources = paths
            .filter(path -> {
                String pathString = path.toString();
                return pathString.endsWith(".yaml") || pathString.endsWith(".yml");
            })
            .filter(path -> {
                String pathString = path.toString();
                for (String themeManifest : THEME_MANIFESTS) {
                    if (pathString.endsWith(themeManifest)) {
                        return false;
                    }
                }
                return true;
            })
            .map(FileSystemResource::new)
            .toList();
        return new YamlUnstructuredLoader(resources.toArray(new Resource[0]))
            .load();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

image-20230812213820188.png

插件功能开发

https://docs.halo.run/2.9.0-SNAPSHOT/developer-guide/plugin/introduction--插件介绍

image-20230819171252690.png

  1. 安装插件

    • https://81.69.254.72/apis/api.console.halo.run/v1alpha1/plugins/install

      private Mono<Plugin> installFromFile(Mono<FilePart> filePartMono,
          Function<Path, Mono<Plugin>> resourceClosure) {
          //将插件流转为jar文件存储到服务器
          var pathMono = filePartMono.flatMap(this::transferToTemp);
          // resourceClosure 文件存储成功后创建数据库记录,文件
          return Mono.usingWhen(pathMono, resourceClosure, this::deleteFileIfExists);
      }
      
  2. 启动插件

    HALO的框架利用监听观察者模式,对于指定对象增删查改,有对应的watcher 进行处理,这个得于ReactiveExtensionClient通用的数据库增删查改。

    @Override
    public <E extends Extension> Mono<E> create(E extension) {
        return Mono.just(extension)
            .doOnNext(ext -> {
                var metadata = extension.getMetadata();
                // those fields should be managed by halo.
                metadata.setCreationTimestamp(Instant.now());
                metadata.setDeletionTimestamp(null);
                metadata.setVersion(null);
    ​
                if (!hasText(metadata.getName())) {
                    if (!hasText(metadata.getGenerateName())) {
                        throw new IllegalArgumentException(
                            "The metadata.generateName must not be blank when metadata.name is "
                            + "blank");
                    }
                    // generate name with random text
                    metadata.setName(metadata.getGenerateName() + randomAlphabetic(5));
                }
                extension.setMetadata(metadata);
            })
            .map(converter::convertTo)
            .flatMap(extStore -> client.create(extStore.getName(), extStore.getData())
                .map(created -> converter.convertFrom((Class<E>) extension.getClass(), created))
                .doOnNext(watchers::onAdd)) // 调用对应存储对象的watcher进行处理
            .retryWhen(Retry.backoff(3, Duration.ofMillis(100))
                // retry when generateName is set
                .filter(t -> t instanceof DataIntegrityViolationException
                             && hasText(extension.getMetadata().getGenerateName())));
    }
    
    @Override
    public void onAdd(Extension extension) {
        if (isDisposed() || !predicates.onAddPredicate().test(extension)) {
            return;
        }
        // 存储对象对应的处理队列
        queue.addImmediately(new Request(extension.getMetadata().getName()));
    }
    
    @Override
    public Result reconcile(Request request) {
       //处理队列,利用插件管理器启动插件
        try {
            return client.fetch(Plugin.class, request.name())
                .map(plugin -> {
                    if (plugin.getMetadata().getDeletionTimestamp() != null) {
                        cleanUpResourcesAndRemoveFinalizer(request.name());
                        return Result.doNotRetry();
                    }
                    addFinalizerIfNecessary(plugin);//走事件模式解耦其他处理
    ​
                    // if true returned, it means it is not ready
                    if (readinessDetection(request.name())) {
                        return new Result(true, null);
                    }
    ​
                    reconcilePluginState(plugin.getMetadata().getName());
                    return Result.doNotRetry();
                })
                .orElse(Result.doNotRetry());
        } catch (DoNotRetryException e) {
            log.error("Failed to reconcile plugin: [{}]", request.name(), e);
            persistenceFailureStatus(request.name(), e);
            return Result.doNotRetry();
        }
    }
    
    void doStart(String name) {
        PluginWrapper pluginWrapper = getPluginWrapper(name);
        // Check if this plugin version is match requires param.
        if (!haloPluginManager.validatePluginVersion(pluginWrapper)) {
            PluginDescriptor descriptor = pluginWrapper.getDescriptor();
            String message = String.format(
                "Plugin requires a minimum system version of [%s], and you have [%s].",
                descriptor.getRequires(), haloPluginManager.getSystemVersion());
            throw new IllegalStateException(message);
        }
    ​
        if (PluginState.DISABLED.equals(pluginWrapper.getPluginState())) {
            throw new IllegalStateException(
                "The plugin is disabled for some reason and cannot be started.");
        }
    ​
        client.fetch(Plugin.class, name).ifPresent(plugin -> {
            final Plugin.PluginStatus status = plugin.statusNonNull();
            final Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(status);
            // 调用插件管理器启动插件
            PluginState currentState = haloPluginManager.startPlugin(name);
            if (!PluginState.STARTED.equals(currentState)) {
                PluginStartingError staringErrorInfo = getStaringErrorInfo(name);
                log.debug("Failed to start plugin: " + staringErrorInfo.getDevMessage());
                throw new IllegalStateException(staringErrorInfo.getMessage());
            }
    ​
            plugin.statusNonNull().setLastStartTime(Instant.now());
    ​
            final String pluginVersion = plugin.getSpec().getVersion();
            String jsBundlePath =
                BundleResourceUtils.getJsBundlePath(haloPluginManager, name);
            jsBundlePath = applyVersioningToStaticResource(jsBundlePath, pluginVersion);
            status.setEntry(jsBundlePath);
    ​
            String cssBundlePath =
                BundleResourceUtils.getCssBundlePath(haloPluginManager, name);
            cssBundlePath = applyVersioningToStaticResource(cssBundlePath, pluginVersion);
            status.setStylesheet(cssBundlePath);
    ​
            status.setPhase(currentState);
            Condition condition = Condition.builder()
                .type(PluginState.STARTED.toString())
                .reason(PluginState.STARTED.toString())
                .message("Started successfully")
                .lastTransitionTime(Instant.now())
                .status(ConditionStatus.TRUE)
                .build();
            Plugin.PluginStatus.nullSafeConditions(status)
                .addAndEvictFIFO(condition);
            if (!Objects.equals(oldStatus, status)) {
                client.update(plugin);
            }
        });
    

tips:

该插件还带chatgpt联调功能,不过提供的模型的token需要充值到openai账号获取token

image-20230819195848853.png

插件原理实现

为什么插件能够自动加载新的bean以及重新加载前端主题文件?

先回答第一个问题

try {
    // load and inject bean 加载和注入bean,封装了另外的plugincontext
    pluginApplicationInitializer.onStartUp(pluginId);
​
    // create plugin instance and start it
    pluginWrapper.getPlugin().start();
​
    requestMappingManager.registerHandlerMappings(pluginWrapper);
    // 启动插件
    pluginWrapper.setPluginState(PluginState.STARTED);
    startedPlugins.add(pluginWrapper);
    // 判断记载的不同类处理不同事件 走的插件的上下文,反向注入到主程序的context中
    //1.加载路由 
    //2.Register finders for a plugin.(Template model data finder for theme.)
    //3.controllerManager 处理
    rootApplicationContext.publishEvent(new HaloPluginStartedEvent(this, pluginWrapper));
} catch (Exception e) {
    log.error("Unable to start plugin '{}'",
        getPluginLabel(pluginWrapper.getDescriptor()), e);
    pluginWrapper.setPluginState(PluginState.FAILED);
    startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of(
        pluginWrapper.getPluginId(), e.getMessage(), e.toString()));
    releaseAdditionalResources(pluginId);
} finally {
    firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
}
return pluginWrapper.getPluginState();
​

pluginWrapper是包装了org.pf4j.Plugin的pluginmanager 的一个封装类,用于加载插件包(org.pf4j.Plugin 这个开源插件可以用来获得指定插件执行,属于另外一套机制 可以参考github)

说一插件上下文和pf4j用的是用一个classloader,加载的是相同的类文件

// * The generic IOC container for plugins.
* The plugin-classes loaded through the same plugin-classloader will be put into the same
* {@link PluginApplicationContext} for bean creation.
// 初始化插件context 及注入bean contexts将springReactive 的context区分
private void initApplicationContext(String pluginId) {
    if (contextRegistry.containsContext(pluginId)) {
        log.debug("Plugin application context for [{}] has bean initialized.", pluginId);
        return;
    }
    StopWatch stopWatch = new StopWatch();
​
    stopWatch.start("createPluginApplicationContext");
    PluginApplicationContext pluginApplicationContext =
        createPluginApplicationContext(pluginId);
    stopWatch.stop();
​
    stopWatch.start("findCandidateComponents");
    Set<Class<?>> candidateComponents = findCandidateComponents(pluginId);
    stopWatch.stop();
​
    stopWatch.start("registerBean");
    for (Class<?> component : candidateComponents) {
        log.debug("Register a plugin component class [{}] to context", component);
        pluginApplicationContext.registerBean(component);
    }
    stopWatch.stop();
​
    stopWatch.start("refresh plugin application context");
    pluginApplicationContext.refresh();
    stopWatch.stop();
​
    contextRegistry.register(pluginId, pluginApplicationContext);
​
    log.debug("initApplicationContext total millis: {} ms -> {}",
        stopWatch.getTotalTimeMillis(), stopWatch.prettyPrint());
}

创建插件上下文

private PluginApplicationContext createPluginApplicationContext(String pluginId) {
    PluginWrapper plugin = haloPluginManager.getPlugin(pluginId);
    // Plugin的classcloder 类由pefj的加载
    ClassLoader pluginClassLoader = plugin.getPluginClassLoader();
​
    StopWatch stopWatch = new StopWatch("initialize-plugin-context");
    stopWatch.start("Create PluginApplicationContext");
    PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
    pluginApplicationContext.setClassLoader(pluginClassLoader);
​
    if (sharedApplicationContextHolder != null) {
        pluginApplicationContext.setParent(sharedApplicationContextHolder.getInstance());
    }
​
    // populate plugin to plugin application context
    pluginApplicationContext.setPluginId(pluginId);
    stopWatch.stop();
​
    stopWatch.start("Create DefaultResourceLoader");
    DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader(pluginClassLoader);
    pluginApplicationContext.setResourceLoader(defaultResourceLoader);
​
    var mutablePropertySources = pluginApplicationContext.getEnvironment().getPropertySources();
    resolvePropertySources(pluginId, pluginApplicationContext)
        .forEach(mutablePropertySources::addLast);
​
    stopWatch.stop();
    // 获取bean加载工厂类
    // BeanDefinition 是对 Bean 的定义,其保存了 Bean 的各种信息,如属性、构造方法参数、是否单例、是否延迟加载等。这里的注册 Bean 是指将 Bean 定义成 BeanDefinition,之后放入 Spring 容器中,我们常说的容器其实就是 Beanfactory 中的一个 Map,key 是 Bean 的名称,value 是 Bean 对应的 BeanDefinition,这个注册 Bean 的方法由 BeanFactory 子类实现。
    DefaultListableBeanFactory beanFactory =
        (DefaultListableBeanFactory) pluginApplicationContext.getBeanFactory();
​
    stopWatch.start("registerAnnotationConfigProcessors");
    AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);
    stopWatch.stop();
​
    beanFactory.registerSingleton("pluginWrapper", haloPluginManager.getPlugin(pluginId));
​
    populateSettingFetcher(pluginId, beanFactory);
​
    log.debug("Total millis: {} ms -> {}", stopWatch.getTotalTimeMillis(),
        stopWatch.prettyPrint());
​
    return pluginApplicationContext;
}

插件context 共享上下文,会作为父级上下文共享bean

/**
 * Set the parent of this application context, also setting
 * the parent of the internal BeanFactory accordingly.
 * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#setParentBeanFactory
 */
@Override
public void setParent(@Nullable ApplicationContext parent) {
   super.setParent(parent);
   this.beanFactory.setParentBeanFactory(getInternalParentBeanFactory());
}
//Beans in the Core that need to be shared with plugins will be injected into this
SharedApplicationContext createSharedApplicationContext() {
    // TODO Optimize creation timing
    SharedApplicationContext sharedApplicationContext = new SharedApplicationContext();
    sharedApplicationContext.refresh();
​
    DefaultListableBeanFactory beanFactory =
        (DefaultListableBeanFactory) sharedApplicationContext.getBeanFactory();
​
    // register shared object here
    var extensionClient = rootApplicationContext.getBean(ExtensionClient.class);
    var reactiveExtensionClient = rootApplicationContext.getBean(ReactiveExtensionClient.class);
    beanFactory.registerSingleton("extensionClient", extensionClient);
    beanFactory.registerSingleton("reactiveExtensionClient", reactiveExtensionClient);
​
    DefaultSchemeManager defaultSchemeManager =
        rootApplicationContext.getBean(DefaultSchemeManager.class);
    beanFactory.registerSingleton("schemeManager", defaultSchemeManager);
    beanFactory.registerSingleton("externalUrlSupplier",
        rootApplicationContext.getBean(ExternalUrlSupplier.class));
    beanFactory.registerSingleton("serverSecurityContextRepository",
        rootApplicationContext.getBean(ServerSecurityContextRepository.class));
    beanFactory.registerSingleton("attachmentService",
        rootApplicationContext.getBean(AttachmentService.class));
    // TODO add more shared instance here
​
    return sharedApplicationContext;
}
//反向将reactive context里面的fetcher 注入到插件context里面
private void populateSettingFetcher(String pluginName,
    DefaultListableBeanFactory listableBeanFactory) {
    ReactiveExtensionClient extensionClient =
        rootApplicationContext.getBean(ReactiveExtensionClient.class);
    ReactiveSettingFetcher reactiveSettingFetcher =
        new DefaultReactiveSettingFetcher(extensionClient, pluginName);
    listableBeanFactory.registerSingleton("settingFetcher",
        new DefaultSettingFetcher(reactiveSettingFetcher));
    listableBeanFactory.registerSingleton("reactiveSettingFetcher", reactiveSettingFetcher);
}

总结下:halo 用了单独的上下文,其中继承了部分主程序上下文的bean,作为parentcontext,也就是上文中的SharedApplicationContext,然后插件中的bean,通过plugin机制,加载到单独的上下文中。主题的话是单独加载的js文件。

官方文档有个详细的例子讲解插件的使用,结合该例子可以加深理解

https://docs.halo.run/2.9.0-SNAPSHOT/developer-guide/plugin/examples/todolist

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/972633.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

android studio cmake生成.a文件(静态库)及调用(c c++)静态库.a

第一步生成静态库.a文件&#xff1a; cmake 语法如何生成静态库&#xff0c;就不介绍了&#xff0c;比较简单&#xff0c;我下文列出的参考资料里面有详细介绍。 add_library(${CMAKE_PROJECT_NAME} STATICsrc/CalculStatic.cpp)这一步有坑&#xff0c;我刚开始的时候&#x…

数学建模之图论

目录 1 图的基本概念2 如何做图2.1 直接做图2.2 编程做图 3 权重邻接矩阵3.1 无向图3.2 有向图 4 Dijkstra 算法4.1 算法概述4.2 代码实现 5 Floyd 算法5.1 算法概述5.2 代码实现 6 思考题 1 图的基本概念 图论中的图&#xff08;Graph&#xff09;是由若干给定的点及连接两点的…

mkp勒索病毒的介绍和防范,勒索病毒解密,数据恢复

mkp勒索病毒是一种新兴的电脑病毒&#xff0c;它会对感染的电脑进行加密&#xff0c;并要求用户支付一定的赎金才能解锁。这种病毒已经引起了全球范围内的关注&#xff0c;因为它不仅具有高危害性&#xff0c;而且还有很强的传播能力。本文将对mkp勒索病毒进行详细介绍&#xf…

群辉NAS:J1900系统盘安装SATA固态硬盘方案【自留记录】

群辉NAS&#xff1a;J1900系统盘安装SATA固态硬盘方案 设备介绍&#xff1a; DSM版本&#xff1a;918 主板CPU&#xff1a;蜗牛星际J1900板 内存&#xff1a;8G DDR3 固态&#xff1a;移速SATA固态&#xff08;msata在win微桌面识别&#xff0c;群晖安装时候识别不到&#xf…

pdf用什么软件打开?介绍几种常用打开方法

pdf用什么软件打开&#xff1f;PDF是一种广泛使用的文件格式&#xff0c;由于其跨平台和易于共享的特点&#xff0c;它已成为许多人在日常工作和学习中使用的首选文件格式。但是&#xff0c;有时候我们可能会遇到一些问题&#xff0c;比如不知道用什么软件打开PDF文件&#xff…

Hadoop生态之hive

一 概述与特点 之所以把Hive放在Hadoop生态里面去写,是因为它本身依赖Hadoop。Hive是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张数据库表,并提供类 SQL 查询功能。 其本质是将 SQL 转换为 MapReduce/Spark 的任务进行运算,底层由 HDFS 来提供…

软件测试/测试开发丨Web自动化 PageObject设计模式

点此获取更多相关资料 本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接&#xff1a;https://ceshiren.com/t/topic/27167 一、page object 模式简介 马丁福勒个人博客 selenium 官网 1.1、传统 UI 自动化的问题 无法适应 UI 频繁变化无法清晰表达业务用例场景大量的样…

微任务创建 -- queueMicrotask()

微任务创建方式&#xff1a; Promise.then(()>{})Mutation Observer()queueMicrotask() 本文主要介绍queueMicrotask()的使用。 queueMicrotask的使用 Window 或 Worker 接口的 queueMicrotask() 方法&#xff0c;将微任务加入队列以在控制返回浏览器的事件循环之前的安全…

Git 版本回退 超神步骤

Git 版本回退 一. 背景 多版本分支开发&#xff0c;合并版本问题太多&#xff0c;需要回滚到某次版本。我的git客服端工具是 sourcetree 二.操作步骤 2.1 切到当前需要回退版本的分支 2.2 右击需要具体某一个分支&#xff0c;这个分支就是你想切到的分支版本&#xff0c;具体…

正版软件 | CloudDrive 多云盘本地挂载管理工具

前言&#xff1a; CloudDrive 是一个强大的多云盘管理工具&#xff0c;提供一站式的多云盘解决方案&#xff0c;包括云盘本地挂载。旨在无缝集成多个云存储服务&#xff0c;统一整合到一个界面。轻松管理和访问所有云存储服务&#xff0c;无需在不同的应用程序和界面之间切换。…

虚拟现实vr元宇宙井下危险隐患排查模拟实训稳固企业生产

数字化时代&#xff0c;职业教育正面临着前所未有的挑战和机遇&#xff0c;元宇宙的兴起&#xff0c;借助元宇宙平台进行钻井虚拟教学实验&#xff0c;基于元宇宙数字空间搭建更丰富、逼真、安全、灵活的实验环境&#xff0c;成为石油行业教育创新的催化剂。 一、降低实验成本 …

Excel·VBA二维数组组合函数的应用实例

看到一个问题《关于#穷举#的问题&#xff0c;如何解决&#xff1f;(语言-开发语言)》&#xff0c;对同一个数据存在“是/否”2种状态&#xff0c;判断其是否参与计算&#xff0c;并输出一系列数据的“是/否”状态的结果 目录 方法1&#xff1a;二维数组组合函数结果 方法2&am…

树上钟同步

#include<cstdio> #include<cstring> #include<vector> using namespace std;const int N 2505; int ori[N], f[N]; vector<int> edge[N]; // 邻接表的简单实现形式void dfs(int u, int fa) {for (int v : edge[u]) {if (v fa) continue;dfs(v, u);f…

网络协议从入门到底层原理学习(一)—— 简介及基本概念

文章目录 网络协议从入门到底层原理学习&#xff08;一&#xff09;—— 简介及基本概念一、简介1、网络协议的定义2、网络协议组成要素3、广泛的网络协议类型网络通信协议网络安全协议网络管理协议 4、网络协议模型对比图 二、基本概念1、网络互连模型2、计算机之间的通信基础…

Prompt Tuning训练过程

目录 0. 入门 0.1. NLP发展的四个阶段&#xff1a; Prompt工程如此强大&#xff0c;我们还需要模型训练吗&#xff1f; - 知乎 Prompt learning系列之prompt engineering(二) 离散型prompt自动构建 Prompt learning系列之训练策略篇 - 知乎 ptuning v2 的 chatglm垂直领域训练记…

【zookeeper】zookeeper日常运维

本文将分享一些zookeeper在日常使用中一些维护经验。 zookeeper清理快照 脚本或者命令清理 zookeeper长时间运行&#xff0c;快照逐渐增多可能造成服务器磁盘被占满的情况&#xff0c;但我们不能贸然用rm命令删除快照文件&#xff0c;如果直接删完会导致丢失好多数据&#x…

【Yellowbrick】特征可视化分析

Yellowbrick特征可视化分析 ⭐Yellowbrick⭐特征分析可视化⭐Rank1D⭐Rank2D ⭐Yellowbrick Yellowbrick是一个用于可视化机器学习模型和评估性能的Python库。它提供了一系列高级可视化工具&#xff0c;帮助数据科学家和机器学习从业者更好地理解、调试和优化他们的模型。 它在…

【【STM32-29正点原子版本串口发送传输实验】

STM32-29正点原子版本串口发送传输实验 通过串口接收或发送一个字符 例程目的 开发板上我们接入的是实现异步通信的UART接口 USB转串口原理图 我们一步步分析 PA9是串口1 的发送引脚 PA10是串口1 的接受引脚 。因为我们现在只是用到异步收发器功能&#xff0c;所以我们现…

应用可视化流程设计,实现提质增效流程化办公!

如果想要实现提高办公效率的目的&#xff0c;显然采用传统的办公方式是无法实现的。如今&#xff0c;在低代码技术平台深入无纸化办公的当下&#xff0c;应用可视化流程设计软件&#xff0c;可以借助其灵活、易操作、可视化、轻量级等优势特点&#xff0c;助力广大用户实现流程…

图解SQL查询之去重技巧:如何使用DISTINCT对数据进行去重

使用 SQL 的 DISTINCT 关键字&#xff0c;可以去除重复的数据记录&#xff0c;只保留不同的记录。 以下是用到的表。 例如&#xff0c;要获取所有学生所在班级的唯一班级ID。