分类: 编程

  • 在electron中基于容器和服务提供者扩展应用核心能力

    应用自身可能提供多种不同的能力,结合服务提供者概念和容器,我们可以实现类似插件的扩展机制,并且通过容器来统一管理服务对象,方便后续扩展。

    一、容器

    容器提供注册同步/异步工厂的方法、同步/异步获取指定服务对象的方法,移除某服务的方法和清空全部服务对象的方法。

    export class Container {
      private instances: Map<string, any> = new Map()
      private factories: Map<string, () => any> = new Map()
      private asyncFactories: Map<string, () => Promise<any>> = new Map()
      private resolvingPromises: Map<string, Promise<any>> = new Map()
      private singleton: Set<string> = new Set()
    
      // 同步注册工厂(默认为单例)
      register<T>(token: string, factory: () => T, options: { singleton?: boolean } = { singleton: true }) {
        this.factories.set(token, factory)
        if (options.singleton) {
          this.singleton.add(token)
        } else {
          this.singleton.delete(token)
        }
      }
    
      // 异步注册工厂(默认为单例)
      registerAsync<T>(token: string, factory: () => Promise<T>, options: { singleton?: boolean } = { singleton: true }) {
        this.asyncFactories.set(token, factory)
        if (options.singleton) {
          this.singleton.add(token)
        } else {
          this.singleton.delete(token)
        }
      }
    
      // 同步解析
      resolve<T>(token: string): T {
        // 如果是单例且已存在实例,直接返回
        if (this.singleton.has(token) && this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        // 查找工厂函数
        const factory = this.factories.get(token)
        if (!factory) {
          // 如果没有工厂但有实例,可能是异步实例已完成
          if (this.instances.has(token)) {
            return this.instances.get(token)
          }
          throw new Error(`Service not found: ${token}`)
        }
    
        // 创建实例
        const instance = factory()
    
        // 如果是单例,保存实例
        if (this.singleton.has(token)) {
          this.instances.set(token, instance)
        }
    
        return instance
      }
    
      // 异步解析
      async resolveAsync<T>(token: string): Promise<T> {
        // 如果是单例且已存在实例,直接返回
        if (this.singleton.has(token) && this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        // 检查是否已经有正在解析的Promise
        let resolvingPromise = this.resolvingPromises.get(token)
        if (resolvingPromise) {
          // 如果有,直接返回这个Promise的结果
          return resolvingPromise
        }
    
        // 先检查是否有异步工厂
        const asyncFactory = this.asyncFactories.get(token)
        if (asyncFactory) {
          // 创建一个新的解析Promise并存储
          resolvingPromise = (async () => {
            try {
              const resolved = await asyncFactory()
              // 如果是单例,保存实例
              if (this.singleton.has(token)) {
                this.instances.set(token, resolved)
              }
              return resolved
            } finally {
              // 解析完成后从resolvingPromises中移除
              this.resolvingPromises.delete(token)
            }
          })()
    
          // 如果是单例,存储Promise以避免重复创建
          if (this.singleton.has(token)) {
            this.resolvingPromises.set(token, resolvingPromise)
          }
          return resolvingPromise
        }
    
        // 再检查是否有同步工厂
        const factory = this.factories.get(token)
        if (factory) {
          try {
            const resolved = factory()
            // 如果是单例,保存实例
            if (this.singleton.has(token)) {
              this.instances.set(token, resolved)
            }
            return resolved
          } catch (error) {
            if (error instanceof Error) {
                throw new Error(`Failed to resolve service '${token}': ${error.message}`)
            } else {
                throw error
            }
          }
        }
    
        // 最后检查是否已有实例(可能是之前异步解析完成的)
        if (this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        throw new Error(`Service not found: ${token}`)
      }
    
      // 检查是否存在指定token的服务
      has(token: string): boolean {
        return this.instances.has(token) ||
               this.factories.has(token) ||
               this.asyncFactories.has(token)
      }
    
      // 清除指定服务的实例(不移除工厂)
      clearInstance(token: string): void {
        this.instances.delete(token)
        this.resolvingPromises.delete(token)
      }
    
      // 移除服务(包括工厂和实例)
      remove(token: string): void {
        this.instances.delete(token)
        this.factories.delete(token)
        this.asyncFactories.delete(token)
        this.resolvingPromises.delete(token)
        this.singleton.delete(token)
      }
    }

    二、服务提供者

    服务提供者接口比较简单,区分register和boot主要是为了确保boot中可以调用任何其他提供者。

    import type { Container } from "../container"
    
    export default interface ServiceProvider {
        /**
         * 应用启动后立即调用
         * 建议仅用于注册服务,不要执行其他操作,其他操作可以放在boot方法中
         *
         * @param container
         */
        register?(container: Container): void
    
        /**
         * boot函数在所有服务注册完成后调用
         * 可以执行一些初始化操作
         *
         * @param container
         */
        boot?(container: Container): void | Promise<void>
    }

    三、应用对象

    import { Container } from './container'
    
    export default class APP {
    
        /**
         * 容器对象
         */
        private container: Container
    
        /**
         * 服务提供者列表
         */
        private providers: ServiceProvider[] = []
    
    
        constructor() {
            // 初始化容器并注册服务提供者
            this.container = new Container()
    
            // 注册服务
            this.registerProviders()
    
            // 启动服务
            this.bootProviders()
        }
    
        registerProviders() {
            for (const provider of this.providers) {
                provider.register?.(this.container)
            }
        }
    
        bootProviders() {
            return Promise.all(
                this.providers.map(provider => provider.boot?.(this.container))
            )
        }
    }

  • 创建strapi插件

    需要开发一个插件来实现一些自定义功能,官方文档写的实在是难以理解了。

    (更多…)
  • 使用SSH转发服务器端口到本地

    注意远程地址、远程端口号、本地地址、本地端口号需要按照实际情况修改。

    (更多…)

  • WordPress添加关键词和描述标签

    关键词和描述标签作为SEO的基础配置,在wordpress中没有默认添加,这里记录一下在Wordpress中自动添加关键词和描述标签的方法。

    (更多…)

  • Google Chrome扩展开发

    Chrome扩展开发者控制台

    1. 需要支付一次性的5美元
    2. 需要使用非国内卡

    参考:

    1. Chrome开发者网站
  • Go语言获取指定年份生肖

    根据给定年份,返回生肖字符串,公元前使用负值即可。(比如2022年,调用使用GetShengXiao(2022),公元前21年,调用使用GetShengXiao(-21))。

    (更多…)

  • Electron中数据持久化的选择

    Electron是一个基于Chromium的桌面应用程序框架,它可以让开发人员在不需要熟练掌握Web开发技术的情况下,快速地开发出高质量的桌面应用程序。在Electron中,开发人员可以使用各种各样的数据存储方式,包括文件系统、数据库等。其中,数据库是一种非常常见的数据存储方式,它可以方便地存储和管理各种数据,包括文本、图片、音频、视频等。

    (更多…)

  • Shell遍历文件夹下所有文件,并将文件内容写入一个文件中

    软件著作权要求提供代码文档,这里提供使用Shell遍历文件夹下所有文件,并将文件内容写入一个文件中的方法。

    (更多…)

  • PostgreSQL常用SQL语句

    PostgreSQL与MySQL语法有一些细微差异,记录一下PostgreSQL常用的SQL语句。

    (更多…)

  • MySQL8 GTID双主配置

    记录一下MySQL8中配置GTID双主的方式。

    (更多…)

  • MySQL常用SQL语句

    记录一些常用的MySQL语句,方便查找翻阅。

    (更多…)

  • 使用微信小程序来实现扫码登录网站

    微信小程序本身提供了openid等信息的无感知获取,基于此来实现微信扫码登录,主要包含以下几个步骤:

    (更多…)

  • Linux服务器的安全相关配置

    记录一下Linux服务器一般常用的安全相关配置,避免被简单的黑掉,更复杂的配置暂不考虑深入研究了。

    (更多…)

  • 在WordPress中给没有封面图的文章增加默认缩略图

    {$default_images[$index]}””/>”;
    }
    return $html;
    }

  • Linux服务器Swap配置

    简单记录文件形式的Swap使用与配置方式。

    1. 交换文件大小配置原则

    1. 内存<2G,配置实际内存的两倍
    2. 内存>2G,配置为4G即可
    3. 内存>4G,追求极致性能,不需要配置交换

    2. 创建交换文件

    # 创建一个名称为swap的文件,大小为1GB
    # if 输入文件名称,此处使用/dev/zero即可
    # of 输出文件名称,使用期望的文件名即可
    # bs 同时设置读入/输出的块大小为多少个字节
    # count 拷贝多少个块,块大小等于bs指定的字节数
    dd if=/dev/zero of=/swap bs=1M count=1024
    
    # 配置交换文件权限
    chmod 0600 /swap
    
    # 将文件设置为交换文件
    mkswap /swap
    
    # 启用指定交换文件
    swapon /swap
    
    # 查看交换文件状态
    swapon -s
    
    # 添加交换文件自动挂载
    echo "/swap swap swap defaults 0 0" >> /etc/fstab

    3. 删除交换文件

    # 关闭指定交换文件
    swapoff /swap
    
    # 删除文件
    rm /swap
    
    # 删除自动挂载配置
    vi /etc/fstab
  • PHP内置服务器与Serverless

    PHP从5.4版本开始就提供了一个内置的WEB服务器,可以通过一个简单的命令`php -S`启动一个WEB服务器,极大简化了开发环境的搭建。

    到目前为止,官网文档对于内置服务器的使用依然建议用于开发环境,不建议用于生产环境,原因倒是很容易理解,主要有两个方面:

    1. 支持的MIME类型很少,5.4版本放出时只支持.htm和.svg(从5.5版本完善了大部分常见的MIME类型支持)

    2. 仅实现了基本功能,基本没有任何优化,是一个单线程进程(不过从7.4版本开始,内置服务器支持多进程的运行方式)

    从传统开发角度看,这样性能和功能的服务器确实很难应用于生产环境,但是伴随着Serverless的发展,感觉内置服务器的限制突然不是那么重要了。

    使用Serverless服务,不管是AWS Lambda,Google Function,还是国内阿里云的函数计算、腾讯云的云函数,我们关注的点不再聚焦于单机性能释放,而是变成了以下四个方面,我们要做的本质上变成了降低单请求的资源占用和执行时间

    1. 调用次数

    2. CPU时间

    3. 内存占用

    4. 执行时间

    5. 带宽

    我们可以逐个对比以下,

    1. 调用次数显然很难因为WEB服务器的变化有什么变化

    2. CPU时间上,内置服务器作为一个单进程应用,同样的逻辑在函数计算这样的环境下,较少了Nginx与FPM交互的网络开销、Nginx的运行开销,理论上内置服务器应该表现更好

    3. 内存占用方面,内置服务器不再需要运行Nginx,同样逻辑,应该也比传统部署方式占用更少一些

    4. 执行时间,Nginx+FPM需要启动两个进程,需要两个进程间的通信,很难与直接启动PHP进程更快

    5. 带宽基本不用对比,应该不会有什么变化,gzip完全可以在CDN层来实现

    从Serverless的角度看,内置服务器并不算是一个很差的选择,对比传统的运行方式可能更加合适一些,就是不太清楚不建议生产环境使用是否有除性能外的其他原因,回头去翻一翻PHP的issue。

    参考文章

    1. PHP-Built-in web server(https://www.php.net/manual/en/features.commandline.webserver.php

  • Linux有线未托管问题

    今天发现在用的一个Linux开发机一直不能自动连接有线网络,显示“有线未托管”,查找测试了很多文章的解决方案都无法使用,可能更新后有了一些变化或者各自情况不同,这里记录一下个人生效的处理方式。

    sudo vim /etc/netplan/00-installer-config.yaml
    
    #文件中添加
    renderer: NetworkManager
    
    sudo netplan generate
    sudo netplan apply
    sudo reboot
  • PHP文件上传$_FILES无内容

    一般是以下两个参数的配置问题。

    upload_max_filesize='100M'
    post_max_size='100M'

  • 网页、APP跳转应用商店

    安卓

    1. 跳转到应用页面:market://details?id=
    2. 跳转到搜索:market://search?q=

    IOS

    1. 应用页面:itms-apps://itunes.apple.com/app/id 114211089
  • MySQL给已存在的主键字段添加自增AUTO_INCREMENT

    每次都记不起来,记录一下…

    # 添加自增约束
    alter table table_name modify column COLUMN_NAME COLUMN_TYPE auto_increment;
    # 配置自增起始值
    alter table table_name auto_increment=10000;