分类: 编程

  • 在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 static _ins: APP
    
        /**
         * 容器对象
         */
        private container: Container
    
        /**
         * 服务提供者列表
         */
        private providers: ServiceProvider[] = []
    
        /**
         * 运行应用
         */
        static run() {
            if (APP._ins == null) {
                APP._ins = new APP()
            }
    
            return APP._ins
        }
    
        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))
            )
        }
    }
    
  • Caddy配置sts(Strict-Transport-Security)

    example.com {
        # 启用 HSTS,有效期 1 年(31536000 秒)
        header {
            Strict-Transport-Security max-age=31536000
        }
    
        # 其他配置...
        reverse_proxy localhost:8080
    }
    
  • 创建strapi插件

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

    1. 创建基础的目录结构

    # 作为项目根目录
    mkdir xxx
    
    # 进入项目目录
    cd xxx
    
    # 创建strapi 项目
    npx create-strapi@latest strapi
    
    # 创建插件
    npx @strapi/sdk-plugin init strapi-plugin
    

    2. 配置工作区

     # xxx目录中添加workspace配置
    {
         "workspaces": [
             "strapi",
             "strapi-plugin"
         ],
         "dependencies": [
             "strapi-plugin": "workspace:*"
         ]
     }
    

    3. strapi目录引入依赖

     {
         ...
         "dependencies": {
             ...
             "strapi-plugin": "0.0.0"
         }
     }
    

    4. 插件目录监听文件修改

    # 根目录执行
    npm run watch --workspace=strapi-plugin
    

    5. 运行strapi

    npm run develop --workspace=strapi
    
  • Parse Platform任务调度

    说起来也挺奇怪的,Parse Platform本身基于js实现,Node生态里的任务调度实现起来并不麻烦,Parse却选择了不集成,使用第三方提供的任务调度。这里使用node-cron来实现Parse Server中的Job调度。

    一、Cloud Code定义Job

    Parse.Cloud.job("test", async (request) =>  {
        // params: passed in the job call
        // headers: from the request that triggered the job
        // log: the ParseServer logger passed in the request
        // message: a function to update the status message of the job object
        const { message } = request
        message('done')
    })
    

    二、定义一个调用Job的函数,方便后面使用

    async function callParseJob(parseServerUrl: string, name: string, data?: any) {
      console.log(`node-cron: callParseJob ${name}`)
      const options: RequestInit = {
        method: 'POST',
        headers: {
          'X-Parse-Application-Id': process.env.APP_ID ?? '',
          'X-Parse-Master-Key': process.env.MASTER_KEY ?? ''
        }
      }
      if (data != null) {
        options.body = JSON.stringify(data)
      }
      const res = await fetch(`${parseServerUrl}/jobs/${name}`, options)
      console.log(`node-cron: job ${name} finished, ${res.statusText}`)
      return await res.json()
    }
    

    三、使用node-cron调度Job

    这里选择通过单独的进程来运行调度程序

    if (cluster.isPrimary) {
      cron.schedule('* * * * *', async () => await callParseJob(process.env.SERVER_URL ?? '', 'test'))
      cluster.fork() // 根据cpu个数fork子进程
    
      cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' exited')
        cluster.fork() //新建一个worker
      })
    } else {
      const app = express()
        //...启动Server相关的代码,这里省略
    }
    
  • 为什么mongoDB在国内发展的没redis好?

    mongo用户应该集中在前端转全栈,通过Node操作mongodb,可以保持之前对于JSON的理解,不用接触传统数据库的知识。

    国内用Node作为后端的用户就不多,Serverless发展也一般,搞Node全栈的集中在独立开发者或者大型公司的某个团队,这个体量确实也不是很大。

    如果是纯后端,我很难理解引入Mongo的意义,完全可以把Postgres的表搞成id+json的形式来模拟Mongo无架构,甚至微软搞了一个DocumentDB,兼容mongo协议,就是基于PostgreSQL的。从扩展性,功能,性能任何方面看,这样用pg都是优于采用MongoDB的。

    DocumentDB:开源公告 – Microsoft 开源博客

    Redis不一样,

    1. 大部分应用考虑扩展和性能的情况下,都需要一个缓存中间件,这个首先mongo就不适合
    2. 简单消息队列
    3. 发布订阅
    4. 不提供内置有序集合的语言,有序集合也省的自己写了
    5. 高频读写的小数据也可以开持久化作为主数据库

     

    https://www.zhihu.com/question/478485839/answer/1965357311661965488

  • Caddy配置CloudBeaver使用Authelia认证

    Cloudbeaver反向代理认证配置文档:https://github.com/dbeaver/cloudbeaver/wiki/Reverse-proxy-header-authentication

    每个地方单独设置账号就太麻烦了,这里记录Caddy配置Cloudbeaver使用Authelia认证的方式。重点其实就是传递到后端时添加X-User和X-Team两个请求头,caddy推荐的authelia配置使用的是Remote-User和Remote-Groups。

    forward_auth authelia:9091 {
        uri /api/authz/forward-auth
        
        ## The following commented line is for configuring the Authelia URL in the proxy. We strongly suggest
        ## this is configured in the Session Cookies section of the Authelia configuration.
        # uri /api/authz/forward-auth?authelia_url=https://auth.example.com/
        copy_headers {
            # Remote-User Remote-Groups Remote-Email Remote-Name
            Remote-User>X-User
            Remote-Groups>X-Team
        }
    }
    

     

  • 阿里云OSS与Cloudflare R2价格对比

    场景:存储位置美西硅谷,存储量200GB,时间一年,写操作3万/月,读操作30万/月,每月传输200GB流量。

    1. 阿里云

    1. 存储费用:100GB资源包+100GB按量 = 162 + 0.136 元/GB/月 * 100 * 12 = 162 + 163.2 = 325.2(每月免费5GB,单价合¥0.136 元/GB/月
    2. 写操作:0(地域500万次/月免费,超出0.1/万次)
    3. 读操作:0(地域2000万次/月免费,超出0.1/万次)
    4. 流量费用:200 * 0.5 * 12 = 1200
    5. 合计:1525.2

    2. Cloudflare

    1. 存储费用:(200 – 10) * 0.015 * 12 = $34.2(每月免费10GB,合¥0.109/GB/月)
    2. 写操作:0(地域100万次/月免费,超出$4.5/百万次,合0.33/万次)
    3. 读操作:0(地域1000万次/月免费,超出0.36/百万次, 合0.026/万次)
    4. 流量费用:0
    5. 合计折合人民币7.13 * 34.2 = 243.846

    3. 腾讯云

    1. 存储费用:212.16(0.089 元/GB/月
    2. 读写操作:0.01/月/万次 * (30 + 3)万次 * 12 = 3.96
    3. 流量费用:200GB/月资源包 = 1029
    4. 合计: 1245.12

    境外走Cloudflare的CDN的话,三方都没有流量费,伴随使用量增加,估计最终Cloudflare整体的价格相对会更低一些,但是需要增加到一定的用量以后。

  • 优化WordPress后台加载速度

    1. PHP预加载脚本

    <?php
    
    /**
    *使用OPcache优化WordPress的预加载脚本。
    *将此文件放在WordPress安装的根目录中。
     */
    
    // Define the base path for WordPress
    define('WP_ROOT_DIR', __DIR__);
    
    // Define ABSPATH (required by WordPress core files)
    if ( ! defined( 'ABSPATH' ) ) {
        define( 'ABSPATH', WP_ROOT_DIR . '/' );
    }
    
    // Define WPINC (required by wp-includes/functions.php)
    if ( ! defined( 'WPINC' ) ) {
        define( 'WPINC', 'wp-includes' );
    }
    
    // Define WP_DEBUG (required by wp-includes/functions.php)
    if ( ! defined( 'WP_DEBUG' ) ) {
        define( 'WP_DEBUG', false );
    }
    
    // 仅预加载最基本的核心文件
    require WP_ROOT_DIR . '/wp-includes/default-constants.php';
    require WP_ROOT_DIR . '/wp-includes/rewrite.php';
    require WP_ROOT_DIR . '/wp-includes/theme.php';
    require WP_ROOT_DIR . '/wp-includes/post.php';
    require WP_ROOT_DIR . '/wp-includes/meta.php';
    require WP_ROOT_DIR . '/wp-includes/user.php';
    require WP_ROOT_DIR . '/wp-includes/cache.php';
    require WP_ROOT_DIR . '/wp-includes/capabilities.php';
    require WP_ROOT_DIR . '/wp-includes/shortcodes.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-query.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-widget.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-roles.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-user.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-post.php';
    
    // 后台管理模块的类
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-media-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-users-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-themes-list-table.php';
    
    // Preload database-related files
    require WP_ROOT_DIR . '/wp-includes/wp-db.php';
    // require WP_ROOT_DIR . '/wp-includes/class-wpdb.php'; // 如果不需要,可以注释
    
  • Go语言获取指定年份生肖

    根据给定年份,返回生肖字符串,公元前使用负值即可。(比如2022年,调用使用GetShengXiao(2022),公元前21年,调用使用GetShengXiao(-21))。
    
    
    // 获取生肖索引
    func GetShengXiaoIndex(year int) int {
        // 不存在0年
        if year == 0 {
            panic("err: invalid year")
        }
    
        // 公元前补1
        if year < 0 {
            year += 1
        }
    
        idx := (year - 4) % 12
    
        if idx < 0 {
            idx += 12
        }
    
        return idx
    }
    
    // 根据给定年份获取生肖
    func GetShengXiao(idx int) string {
        return [12]string{"鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"}[idx]
    }
    
  • WordPress添加关键词和描述标签

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

    一、实现代码

    实现思路是使用标签来作为关键词,使用文章摘要作为页面描述,找到使用主题的functions.php文件,添加以下代码即可实现。

    /**
     * 添加SEO相关的Keywords和Description标签
     */
    function add_seo_meta_tags() {
        if (is_home()) {
            $tags = get_tags([
                'number' => 15,
                'orderby' => 'count',
                'order' => 'DESC'
            ]);
            $tags = array_map(function ($item) {
                return $item->name;
            }, $tags);
            if (count($tags) > 0) {
                ?>
                <meta name="keywords" content="<?php echo get_bloginfo('name'); ?>,<?php echo implode(',', $tags); ?>">
                <?php
            }
            ?>
                <meta name="description" content="<?php echo get_bloginfo('description'); ?>">
            <?php
        } else if (is_category() || is_tag()) {
            ?>
                <meta name="keywords" content="<?php echo single_cat_title(); ?>">
                <meta name="description" content="<?php echo strip_tags(category_description()); ?>">
            <?php
        } else if (is_singular()) {
            ?>
                <meta name="description" content="<?php echo strip_tags(get_the_excerpt()); ?>">
            <?php
    
            $tags = array_map(function ($item) {
                return $item->name;
            }, get_the_tags() ?: []);
            if (count($tags) > 0) {
                ?>
                    <meta name="keywords" content="<?php echo implode(',', $tags); ?>">
                <?php
            }
        }
    }
    add_action( 'wp_head', 'add_seo_meta_tags' );
    

    二、使用的前置要求

    请注意,需要保证主题的header.php调用过wp_head()函数,类似下面:

    <!DOCTYPE html>
    <html <?php language_attributes(); ?> class="no-js">
    <head>
    <meta charset="<?php bloginfo( 'charset' ); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <?php wp_head(); ?>
    </head>
    

    OK,这样我们就给所有页面都添加上关键词和描述标签了~

    相关链接:

    1. wordpress关于meta标签的说明
  • 使用SSH转发服务器端口到本地

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

    function forwardPort() {
        PROC_NAME="ssh -fR $1"
        ProcNumber=`ps -ef |grep -w "$PROC_NAME"|grep -v grep|wc -l`
        if [ $ProcNumber -le 0 ];then
            echo "$1 is not forward.."
            ssh -fCNR $1:localhost:$1 root@0.0.0.0 -p 1234 -o ServerAliveInterval=30
        else
            echo "$1 has forwarded.."
        fi
    }
    for port in 1234 4567
    do
        forwardPort $port
    done
    

    注意,如果需要外网访问转发的端口,需要在远程服务器的配置文件(/etc/ssh/sshd_config)中添加:

    AllowTcpForwarding yes
    
  • 使用Joplin作为博客后端-使用marked转换joplin笔记中资源文件的地址

    joplin笔记中资源文件默认使用的是id,但是如果渲染到网页,需要转换为网络地址,记录一下处理过程。

    export async function transformMarkdown(markdown?: string) {
        if (markdown == null || markdown === '') {
            return ''
        }
    
        const renderer = new marked.Renderer()
    
        marked.use({
            async: true,
            async walkTokens(token) {
                if (token.type === "image") {
                    const id = token.href.replace(':/', '')
                    const resource = await joplinResource(id)
                    token.href = `${process.env.SITE_URL}/resources/${id}.${resource.file_extension}`
                    token.title = resource.title
                }
            },
            renderer: {
                image({ href, text, title }) {
                    return `<img src="${href}" alt="${text}" title="${title}" loading="lazy" />`
                }
            }
        })
    
        return await marked.parse(markdown, {
            renderer,
            gfm: true,
            breaks: false,
            pedantic: false
        })
    }
    
  • 使用Joplin作为博客后端-基于Joplin Terminal部署服务端REST API

    Joplin Server本身虽然有接口,但是并不能直接获取笔记数据,了解后发现Joplin Terminal是支持Data API的,这样的话,可以通过在服务器部署一套Joplin Terminal程序来从服务器提供笔记数据,唯一的问题可能就是会导致服务器存储两份笔记数据,不过也不是很大的问题,这里以通过Docker容器部署为例。

    一、安装Joplin Terminal

    # 创建数据存储文件夹
    mkdir -p joplin/cli
    
    cd joplin/cli
    
    npm i joplin
    
    # 同步配置参考官方文档 https://joplinapp.org/help/apps/terminal/
    
    # 同步数据
    npx joplin sync
    

    二、启动Joplin的Data API Server

    修改项目的package.json,增加一个启动脚本。

    {
      "type": "module",
      "scripts": {
        "joplin": "joplin server start",
        ...
      },
      "dependencies": {
        "joplin": "^3.4.1"
      }
    }
    
    npm run joplin
    

    三、docker compose配置

    services:
        ...
        joplin-api:
            image: node:22.12.0
            restart: unless-stopped
            # 这个应该根据实际的uid和gid设置,与挂载目录的/home/test有关
            user: 1000:1000
            volumes:
                # 挂载这个文件主要是为了保持权限一致,避免本地修改容器不能运行
                - /etc/passwd:/etc/passwd:ro
                # 挂载项目代码
                - ./joplin/cli:/app
                # joplin数据保存位置
                - ./joplin/data:/home/test/.config/joplin
                - ./joplin/.npm:/home/test/.npm/
            # 这个start命令参考下面
            command: ["sh", "-c", "chdir /app && npm start"]
    

    四、转发请求,配置定时任务

    joplin terminal启动的data api默认是写死的绑定127.0.0.1,需要本地启动一个反向代理服务器来做一下请求转发,同时也配置一下定时任务。
    我这里服务器用的h3,用什么其实都可以,感觉JS的Web框架语法都差不太多,这里选h3主要是为了资源占用少。

    import { H3 } from "h3"
    import { createServer } from 'node:http'
    import { toNodeHandler } from "h3/node"
    import cron from 'node-cron'
    import { exec } from "node:child_process"
    
    /**
     * 运行node-cron定时任务
     * /30 * * * /path/to/joplin sync
     */
    cron.schedule('*/15 * * * *', joplinSync)
    
    /**
     * 同步数据
     */
    export async function joplinSync() {
      exec('/app/node_modules/.bin/joplin sync', (_, stdout) => {
        console.log('joplin sync: ', stdout)
      })
    }
    
    export const app = new H3()
    
    /**
     * 反向代理外界请求到本机的41184端口
     * joplin Data API Server默认监听41184,因为Docker环境,也不太可能出现冲突的问题
     */
    app.use(
      async (event) => {
        const { url } = event.req
    
        const proxyUrl = new URL(url)
        proxyUrl.protocol = 'http'
        proxyUrl.host = 'localhost:41184'
        const proxy = await fetch(proxyUrl.toString())
        return proxy.body
      }
    )
    
    createServer(toNodeHandler(app)).listen(3000, '0.0.0.0')
    

    五、完善启动脚本

    {
      "type": "module",
      "scripts": {
        "proxy": "node --experimental-strip-types index.ts",
        "joplin": "joplin server start",
        "start": "npm run proxy & npm run joplin"
      },
      "dependencies": {
        "h3": "^2.0.1-rc.2",
        "joplin": "^3.4.1",
        "node-cron": "^4.2.1"
      },
      "devDependencies": {
        "@types/node": "^24.7.1"
      }
    }
    

    六、后续

    后面其实就简单了,感觉直接开放到公共网络不是很安全,最好还是通过容器间通信使用,真正的权限鉴定放在h3这一层。

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

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

    #!/bin/bash
    dir="."
    target="./target.txt"
    # 过滤指定文件或文件夹
    filter=(node_modules out dist $target)
    
    listfile() {
        filelist=`ls $1`
        for file in $filelist
        do
            if [[ "${filter[@]}" =~ "$file" ]];then
                continue
            fi
    
            if [ -d $1/$file ];then
                listfile $1/$file
            else
                cat $1/$file >> $target
            fi
        done
    }
    
    listfile $dir
    
  • MySQL常用SQL语句

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

    1. 查看MySQL数据库磁盘占用大小

    select 
    TABLE_SCHEMA as '数据库', 
    concat(truncate(sum(data_length)/1024/1024,2),'MB') as '数据容量(MB)',
    concat(truncate(sum(index_length)/1024/1024,2),'MB') as '索引容量(MB)'
    from information_schema.tables
    group by TABLE_SCHEMA
    ORDER BY data_size desc
    

    2. 查看MySQL数据库中表的磁盘占用

    select  
    table_schema as '数据库',  
    table_name as '表名',  
    table_rows as '记录数',  
    truncate(data_length/1024/1024, 2) as '数据容量(MB)',  
    truncate(index_length/1024/1024, 2) as '索引容量(MB)'  
    from information_schema.tables  
    where table_schema='bwc_plsedu_com'
    order by data_length desc, index_length desc;
    

    3. MySQL创建、删除用户,授权、撤销授权

    create user user@host identified by 'password';
    # 授权
    grant all privileges on db.table to user@host;
    # 取消授权
    revoke all privileges on test.* from 'user'@'host';
    #删除用户
    drop user 'test'@'127.0.0.1';
    # 刷新权限使授权生效
    flush privileges;
    

    4. 性能检查

    -- 查看当前连接
    SHOW PROCESSLIST;
    
  • 网页、APP跳转应用商店

    安卓

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

    IOS

    1. 应用页面:itms-apps://itunes.apple.com/app/id 114211089
  • Linux有线未托管问题

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

    sudo vim /etc/netplan/00-installer-config.yaml
    
    #文件中添加
    renderer: NetworkManager
    
    sudo netplan generate
    sudo netplan apply
    sudo reboot
    
  • 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
    
  • Google Chrome扩展开发

    Chrome扩展开发者控制台

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

    参考:

    1. Chrome开发者网站
  • Electron中数据持久化的选择

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

    文件存储

    本地文件适合用来存储一些配置相关的信息,常见的可用格式比如JSON、INI、Yaml、Toml等。

    IndexedDB

    IndexedDB,我觉得更适合用于调用服务端接口的缓存,或者极少在主线程使用的数据,否则来回传递感觉性能可能不太高(未经测试,但是结论应该不会有错)。

    有朋友之前问到怎么在主线程中使用IndexedDB,直接使用是不可能的哈,毕竟那是暴露在浏览器中的,并没有相关的Node实现。不过,其实IndexedDB在Chrome中也是使用SQLite实现的,如果需要保持同构,只需要实现一个简单的数据库中间层来隐藏底层的API或者按照IndexedDB的API来封装一下SQLite的调用即可。

    SQLite

    使用SQLite作为数据库可以让Electron应用程序更加轻量级和易于管理。SQLite是一种基于文件的数据库系统,它可以在不需要安装任何额外软件的情况下,在本地运行和管理数据库。这意味着,开发人员可以在Electron应用程序中使用SQLite数据库,而不必考虑复杂的数据库管理和同步问题。此外,SQLite还支持多种数据模型和查询语言,这可以让开发人员更加方便地存储和管理各种数据。 使用SQLite作为数据库还可以让Electron应用程序更加安全。由于SQLite是一种基于文件的数据库系统,它不会占用过多的系统资源,因此可以在不影响应用程序性能的情况下,存储和管理大量的数据。此外,SQLite还支持数据加密和数据备份,这可以让开发人员更加方便地保护应用程序数据的安全。 总之,使用SQLite作为数据库可以让Electron应用程序更加轻量级和易于管理,同时还可以让应用程序更加安全。如果您正在使用Electron开发桌面应用程序,并且需要存储和管理大量的数据,那么使用SQLite数据库将是一个非常不错的选择。

    其他(LocalStorage/SessionStorage)

    当然渲染进程还是可以使用LocalStorage这些,使用相对IndexedDB就方便很多,只是有大小限制,适合存储用户级别的个性化缓存数据(主题、语言等),其他类型的数据就不是很推荐了。