• Node.js ORM框架Sequelize实践小结

    摘自 2023/4 笔记

    对象-关系映射(Object-Relational Mapping,简称ORM),用于实现面向对象编程语言里不同类型系统之间数据的转换。

    面向对象是从软件工程基本原则(如耦合、聚合、封装)的基础上发展起来的,而关系数据库则是从数学理论发展而来的,两套理论存在显著的区别。为了解决这个不匹配的现象,对象关系映射技术应运而生。

    简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。

    参考:https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping

    为什么要使用ORM框架

    1. 提高开发效率,降低开发成本
    2. 使开发更加对象化
    3. 可以很方便地引入数据缓存等附加功能

    Sequelize

    Sequelize是一个基于 promise 的 Node.js ORM框架, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能。

    • 安装
    npm install --save egg-sequelize mysql2
    • 引入插件, config/plugin.js文件引入egg-sequelize
    exports.sequelize = {
      enable: true,
      package: 'egg-sequelize',
    };
    • config/config.default.js配置数据库连接
    exports.sequelize = {
      dialect: 'mysql',
      host: '127.0.0.1',
      port: 3306,
      database: 'database',
      username: 'username',
      password: '*******',
      define: {
        freezeTableName: true, //禁止转换为复数
      },
    };

    踩坑:默认情况下,当未提供表名时,Sequelize 会自动将模型名复数并将其用作表名,导致sql语句报错找不到对应的表,可以使用 freezeTableName: true 参数停止自动复数化

    定义Model

    app/model/user.js为例

    sequelize.define(modelName, attributes, options),第一个参数为模型名;第二个为定义的模型字段,基本为表格内字段,也可以定义表格中不存在的字段;第三个参数为配置。

    'use strict';
    
    module.exports = (app) => {
      const { STRING, INTEGER, DATE } = app.Sequelize;
    
      const User = app.model.define('t_user', {
        id: { type: INTEGER, primaryKey: true, autoIncrement: true },
        name: STRING(30),
        age: INTEGER
      });
    
      return User;
    };

    如果模型名和表名不一致,可以直接配置表名称:

    sequelize.define('User', {
      // ... (属性)
    }, {
      tableName: 'Employees'
    });

    如果需要修改返回字段,举例:数据库中字段为user_id,想直接返回id,可以修改映射关系

    id: {
        type: NUMBER,
        field: 'user_id'
    },

    设置默认值

    sequelize.define('User', {
      name: {
        type: DataTypes.STRING,
        defaultValue: "Nick"
      },
      time: {
        type: DataTypes.DATETIME,
        defaultValue: DataTypes.NOW // 当前时间
      }
    });

    配置校验方法

    const User = sequelize.define('user', {
      name: {
        type: STRING,
        validate: {
          len: [3, 6]
        }
      }
    });

    定义数据表中不存在的字段,引入VIRTUAL类型,定义getter和setter

    const { STRING, VIRTUAL } = app.Sequelize;
    
    const User = sequelize.define('user', {
      firstName: STRING,
      lastName: STRING,
      fullName: {
        type: VIRTUAL,
        get() {
          return `${this.firstName} ${this.lastName}`;
        },
        set(value) {
          throw new Error('不要尝试设置 `fullName` 的值!');
        }
      }
    });

    另外,创建时间和更新时间可以交给sequelize管理,这样在代码实现过程中,你不需要对创建时间和更新时间字段进行更新维护。配置options字段:

    {
      createdAt: 'create_time',
      updatedAt: 'update_time'
    }

    踩坑:默认情况下,Sequelize 自动向每个模型添加 createdAt 和 updatedAt 字段。

    如果你并不需要这两个字段,需要在Model层中配置timestamps: false

    {
      timestamps: false
    }

    如果只要其中一个

      // 启用时间戳!
      timestamps: true,
    
      // 不想要 createdAt
      createdAt: false,
    
      // 想要 updatedAt 但是希望名称叫做 updateTimestamp
      updatedAt: 'updateTimestamp'

    模型查询

    INSERT

    // 创建一个新用户
    const jane = await User.create({ firstName: "Jane", lastName: "Doe" });

    批量创建

    Model.bulkCreate 方法接收数组对象。

    const users = await User.bulkCreate([
      { name: 'Jack Sparrow' },
      { name: 'Davy Jones' }
    ]);

    默认情况下,bulkCreate 不会在要创建的每个对象上运行验证,可以通过配置validate: true, 但这会降低性能.

    UPDATE

    update方法第一个参数为更新字段,第二个传入被更新的记录条件

    // 将所有没有姓氏的人更改为 "Doe"
    await User.update({ lastName: "Doe" }, {
      where: {
        lastName: null
      }
    });

    DELETE

    // 删除所有名为 "Jane" 的人 
    await User.destroy({
      where: {
        firstName: "Jane"
      }
    });

    可以使用 TRUNCATE SQL,销毁所有内容。销毁后自增ID会重新从1开始

    await User.destroy({
      truncate: true
    });

    SELECT

    findByPk

    提供主键从表中仅获得一个条目

    const project = await Project.findByPk(123);

    findOne

    获得符合条件的第一个条目

    const project = await Project.findOne({ where: { title: 'My Title' } });

    fineOrCreate

    如果能找到一个满足查询参数的结果则返回,否则创建一个条目

    const [user, created] = await User.findOrCreate({
      where: { username: 'sdepold' }
    })

    findAll

    返回符合条件所有条目

    // 查询所有用户
    const users = await User.findAll();
    
    //  查询年龄为25的所有用户
    const users = await User.findAll({
        where: { age: 25 }
    });

    findAndCountAll

    结合了 findAll 和 count 的便捷方法,在处理与分页有关的查询时非常有用

    const { count, rows } = await Project.findAndCountAll({
      where: {
        title: {
          [Op.like]: 'foo%'
        }
      },
      offset: 10,
      limit: 2
    });

    联表查询

    Model层定义关联关系

    • 一对一关系:hasOnebelongsTo 关联一起使用
    • 一对多关系:hasManybelongsTo 关联一起使用
    • 多对多关系:两个 belongsToMany 调用一起使用

    定义关联键

    如果没有自定义外键,默认使用id进行关联。如果要使用其他键值,需要在Model中配置。

    举例,系统中业务下有很多个模块,在模块表中记录的外键是project_id,对应业务表中的id

    app.model.Project.hasMany(app.model.Module, {
      as: 'module',
      foreignKey: 'project_id',
      sourceKey: 'id',
    });
    
    app.model.Module.belongsTo(app.model.Project, {
        as: 'project',
        foreignKey: 'project_id',
        sourceKey: 'id',
    });

    联表查询

    可以在同一模型之间定义多个关联. 需要为它们定义不同的别名

    举例,查询模块时将所属业务信息也返回给前端

     await Module.findOne({
        where: {
            id
        },
        include: {
            model:  ctx.model.Project,
            as: 'project'
        }
    })

    事务

    默认情况下,Sequelize 不使用事务. 但是,对于 Sequelize 的生产环境使用,你绝对应该将 Sequelize 配置为使用事务。

    Sequelize 支持两种使用事务的方式:

    • 非托管事务: 提交和回滚事务应由用户手动完成(通过调用适当的 Sequelize 方法)
    • 托管事务: 如果引发任何错误,Sequelize 将自动回滚事务,否则将提交事务

    官方非托管事务示例:

    // 首先,我们开始一个事务并将其保存到变量中
    const t = await sequelize.transaction();
    
    try {
      // 进行调用时将此事务作为参数传递:
      const user = await User.create({
        firstName: 'Bart',
        lastName: 'Simpson'
      }, { transaction: t });
    
      await user.addSibling({
        firstName: 'Lisa',
        lastName: 'Simpson'
      }, { transaction: t });
    
      // 如果执行到此行,且没有引发任何错误.
      // 我们提交事务.
      await t.commit();
    
    } catch (error) {
      // 如果执行到达此行,则抛出错误.
      // 我们回滚事务.
      await t.rollback();
    }

    踩坑:如果按照这个教程编写事务代码,就会报错TypeError: Cannot read property ‘transaction‘ of undefined。原因是这里的sequelize需要从数据连接对象获取,而不是直接require('sequelize'),官方文档这里没有写清楚,导致初次使用的人基本上都踩坑了。

    解决:将数据库连接封装成一个单独的模块后导出实例给事务使用

    const { Sequelize } = require('sequelize')
    
    const sequelize = new Sequelize({
        dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
        database: 'database',
        host: '10.133.145.xxx',
        port: 3306,
        username: 'xxx',
        password: 'xxx'
    })
    
    module.exports = {
        sequelize
    }

  • 淘宝小程序开发经验小结

    摘自 2023/4/26 笔记,已过滤业务敏感数据。

    淘宝小程序在UI、API、事件等都和微信小程序非常相似,如果你之前有微信小程序的开发经验,可以很快上手淘宝小程序。

    申请小程序

    淘宝小程序准入门槛较高。申请淘宝小程序要求你至少是淘宝的一名商家;其中很多类目还需要提供相应的资质文件才可以申请。

    API 请求

    和微信小程序不同,淘宝小程序无法直接请求第三方服务器,我们只能通过淘宝提供的云服务方案来实现:云函数和云应用。

    目前我们的实现是:在云应用中实现一个请求的代理转发功能,通过代理作为一个中间桥梁实现云与淘宝小程序的连接。

    更严格的发布审核

    淘宝小程序非常重视安全和性能,如果性能和安全审核不通过则无法进行发布

    提前对性能审核的标准有一个清楚的认识,避免赶着deadline发布却卡在审核通不过。

    淘宝开发者工具看不了异步请求,每次发起请求需要手动将请求参数、返回结果等打印在控制台进行调试。

    淘宝原不支持修改昵称,近两年才支持用户修改昵称,但云服务返回的混淆昵称(mix_nick)大部分仍为旧的淘宝名。如果你改了淘宝名,开通权限和开通会员通时大概率仍需要使用旧的淘宝名。

    • 淘宝大促期间(618和双十一等)禁止发布,尽管写了有紧急情况可以发邮件,但尝试过发邮件也一直没有回复,和运营、产品提前沟通好,尽量避免在大促期间安排发布重要功能。

    小程序开发文档:https://open.taobao.com/docCenter?spm=a219a.15212433.0.0.6a3b669ahlfPYP#/


  • 在安卓系统画一个三角形

    摘自 2023/11/7 笔记

    需求背景:完成如下图原型,设计同学只给了”锁”的切图,三角形我们一看就知道可以用CSS Border完成

    <div class='triangle'></div>
    .triangle {
      width: 0;
      height: 0;
      border-right: 24px solid #1362FF;
      border-bottom: 24px solid #00000000;
      border-top-right-radius: 4px;
    }

    PC端效果符合预期,但到真机预览效果的时候,安卓端会显示成一个矩形( IOS 正常)。

    CSS border transparent 属性在安卓端有兼容性问题。

    经过一番折腾(试了网上一些奇技淫巧都没能解决),最后决定用clip-path自己画一个三角形。

    实现如下

    .triangle {
      width: 24px;
      height: 24px;
      clip-path: polygon(0 0, 100% 0, 100% 100%);
      background-color: #1362FF;
    }

    即定义一个24 * 24的盒子,连接三个转折点并填充颜色

    x    y
    
    0    0    -  上左
    
    100% 0    -  上右
    
    100% 100% -  下右

    真机预览正常!


  • 渐进式图片加载

    摘自 2023/7/25 笔记,很多图片失效了。。。后续要补充

    渐进式图片加载(Progressive Loading)可以在展示图片的时候让图片从一张模糊的图片过渡到一张清晰的图片。

    • 非渐进式:像扫描仪一样从上往下逐渐显示
    • 渐进式:先显示大致的轮廓,在加载z过程中再逐渐变清晰

    渐进式加载不能从根本上提高图片的加载速度,但因为用户看到图片的时间提前了,可以给用户一种“看上去加载得比较快”的错觉,从而提升使用体验。实际开发中,渐进式图片可以配合懒加载一起使用,提升网站性能。

    图片的渐进式解码

    渐进式图片加载依赖编码/解码。

    渐进式解码是从不完整的图像文件增量解码图像部分的功能。解码完整图像所需的传递数取决于图像文件格式和用于创建图像的编码过程。(传递数的意思就是经过几次模糊的轮廓到清晰的图片这个过程)

    图像满足渐进式解码条件:

    • 图像格式必须支持渐进式解码:JPEG、PNG 和GIF
    • 图像文件必须编码为渐进式图像
    • 编解码器必须支持渐进式解码

    如何判断图片是否为渐进式

    图片编码

    以JPEG图片格式为例,JPEG图片编码以FFD8开头,FFD9结尾,FFDA代表一个帧的开头,传统编码只有一个FFDA,渐进式编码则有多个FFDA

    参考:https://en.wikipedia.org/wiki/JPEG

    Linux

    执行命令,输出为Plane则为渐进式图片

    identify -verbose filename.jpg | grep Interlace

    保存/转换为渐进式图片

    PhotoShop

    存储为“web所用格式”,选择“连续”就是渐进式 JPEG

    PHP

    <?php
        $im = imagecreatefromjpeg('pic.jpg');
        imageinterlace($im, 1);
        imagejpeg($im, './php_interlaced.jpg', 100);
        imagedestroy($im);
    ?>

    Python

    import PIL
    from exceptions import IOError
    img = PIL.Image.open("./test.jpg")
    destination = "./dest.jpeg"
    try:
        img.save(destination, "JPEG", quality=80, optimize=True, progressive=True)
    except IOError:
        PIL.ImageFile.MAXBLOCK = img.size[0] * img.size[1]
        img.save(destination, "JPEG", quality=80, optimize=True, progressive=True)

    参考文档:https://cloudinary.com/blog/progressive_jpegs_and_green_martians


  • 理解浏览器network stalled状态

    摘自 2023/11/07 笔记

    最近在回归直播插件状态上报情况时,发现时不时会出现一次耗时6分钟的请求,让人匪夷所思

    一开始以为是接口问题,联系接口负责同学排查。重启服务后还是多次出现这种情况。查看请求 `timeline` 发现阻塞发生在”stalled”状态

    学习一下官方文档中对network请求各个阶段的解释

    Stalled. The request could be stalled after connection start for any of the reasons described in Queueing.

    翻译一下:由于排队中描述的任何原因,请求可能会在连接启动后停滞,似懂非懂,在搜一下 stack overflow 怎么说的:

    Time the request spent waiting before it could be sent. This time is inclusive of any time spent in proxy negotiation. Additionally, this time will include when the browser is waiting for an already established connection to become available for re-use, obeying Chrome’s maximum six TCP connection per origin rule.


    翻译一下:Stalled 是请求在发送前等待所花费的时间(也就是说跟接口一点关系都没有)。 该时间包括协商代理所花费的任何时间(检查本地代理!)。

    此外,这个时间还包括浏览器等待已建立的连接可供重复使用的时间,遵守 Chrome 的每个源最多 6 个 TCP 连接的规则。(理解为连接池,如果6个连接都被占满了,就只能等待资源释放)。

    如果是网络不稳定造成的等待,就不会每次都是相同的6分钟,每次都是等待6分钟,对应Chrome 6个TCP连接规则。破案了!

    解决

    为每个请求设置超时时间,当超过设置等待时间则cancel请求,减少资源占用。

    const xhr = new XMLHttpRequest();
    xhr.timeout = 60 * 1000;     // 设置超时时间
    xhr.open('POST', url);

  • Windows Server Cheat Sheet

    raw 镜像转换至 vhdx

    linux – How do I convert an .img file to vhd? – Super User

    直接简单粗暴扔给 qemu-utils 处理就可以了。

    qemu-img convert ./debian-12-nocloud-amd64-20250428-2096.raw -O vhdx ./debian-12-nocloud-amd64-20250428-2096.vhdx

    可以添加 -o subformat=fixed-o subformat=dynamic 来配置目标 vhdx 是否为动态大小。

    创建类似 VMware 中 NAT 类型的虚拟交换机

    Set up a NAT network | Microsoft Learn

    Get-NetNat (NetNat) | Microsoft Learn

    Hyper-V NAT 网络设置固定 IP / DHCP – wswind – 博客园

    创建虚拟网卡和配置其静态 IP 可以在可视化界面中完成,也可以用 PowerShell:

    New-VMSwitch -SwitchName "SwitchName" -SwitchType Internal
    PS C:\> Get-NetAdapter
    
    Name                  InterfaceDescription               ifIndex Status       MacAddress           LinkSpeed
    ----                  --------------------               ------- ------       ----------           ---------
    vEthernet (intSwitch) Hyper-V Virtual Ethernet Adapter        24 Up           00-15-5D-00-6A-01      10 Gbps
    Wi-Fi                 Marvell AVASTAR Wireless-AC Net...      18 Up           98-5F-D3-34-0C-D3     300 Mbps
    Bluetooth Network ... Bluetooth Device ...                    21 Disconnected 98-5F-D3-34-0C-D4       3 Mbps
    New-NetIPAddress -IPAddress <NAT Gateway IP> -PrefixLength <NAT Subnet Prefix Length> -InterfaceIndex <ifIndex>

    创建 NAT,没找到对应的可视化工具。

    New-NetNat -Name <NATOutsideName> -InternalIPInterfaceAddressPrefix <NAT subnet prefix>

    创建 DNAT 规则:

    Add-NetNatStaticMapping -ExternalIPAddress "0.0.0.0/0" -ExternalPort 6881 -Protocol TCP -InternalIPAddress "192.168.1.100" -InternalPort 6881 -NatName Default

    启用 CIFS/SMB 在 Linux 中挂载

    samba – How do I mount a CIFS share via FSTAB and give full RW to Guest – Ask Ubuntu

    How to mount a windows/samba windows share under Linux? – Unix & Linux Stack Exchange

    安装依赖,然后挂载测试:

    apt install cifs-utils
    mount -t cifs -o "username=debian" -o "password=password" //192.168.1.1/assets /mnt/assets

    确认无误后写入 /etc/fstab

    //192.168.1.1/assets /mnt/assets cifs username=username,password=password,uid=1000,gid=1000 0 0

    别忘了验证,防止影响操作系统启动:

    umount /mnt/assets
    systemctl daemon-reload
    mount -a

  • 腾讯云轻量对象存储的简单体验

    腾讯云给轻量服务器锐驰型的用户发了相同时长的轻量对象存储 50G 免费额度,正好来体验一下。

    控制台上挂载到轻量服务器上使用

    控制台上面非常简洁,看起来基本上就是只希望客户使用这种方式了。使用门槛很低,只需要配置相同地域的服务器(不一定非要锐驰型)和路径就会自动帮忙挂载。

    底层也是 cosfs 实现的,应该是用 Agent 自动下发的挂载命令,对新手很友好。搭配 200M 的机器,对于个人开发者来说可以省下一笔不少的费用。

    这种方式没有什么更多要说的了,唯一可能要注意的事情是:部分修改文件可能会导致整个文件重传。更多类似限制,可以查阅 cosfs 的相关文档。另外,cosfs 也有了支持 posix 的后继者 GooseFS-Lite,参见这里。但未来趋势可能还是存储网关。

    当然了,对象存储更多的使用场景还是得通过 API 来进行存取,下面简单来尝试一下。还是有所要注意的地方的。

    通过 API/SDK 调用 – 以 Loki 为例

    看轻量对象存储的文档的话,会发现其实它还是支持 API/SDK 调用的,Endpoint 和标准的 COS 是一样的:

    那么我们就先来创建子账号和对应所需的权限。经过尝试,如果按照 lhcos(轻量对象存储)进行授权的话,通过 S3 兼容接口调用还是会报错 403,这里按 cos(标准对象存储)授权就可以了。这里也吐槽一下,由于这个 appid 不是用户主账号 uid,就没法使用对象存储的诊断工具了。

    {
        "statement": [
            {
                "action": [
                    "cos:GetBucket",
                    "cos:GetObject",
                    "cos:PutObject",
                    "cos:DeleteObject"
                ],
                "effect": "allow",
                "resource": [
                    "qcs::cos:<region>:uid/<uid>:<bucketName>-<uid>/*"
                ]
            }
        ],
        "version": "2.0"
    }

    配置 Loki 使用轻量对象存储,这里给出关键的配置片段:

    loki:
      storage:
        type: s3
        s3:
          endpoint: cos.ap-hongkong.myqcloud.com
          region: ap-hongkong
          accessKeyId: <ak>
          secretAccessKey: <sk>
        bucketNames:
          chunks: <bucketName>-<uid>
          ruler: <bucketName>-<uid>
          admin: <bucketName>-<uid>
      schemaConfig:
        configs:
          - from: 2024-04-01
            object_store: s3
            store: tsdb
            schema: v13
            index:
              prefix: index_
              period: 24h

    由于我这里是跨云的集群,如果 CoreDNS 不在内网会导致解析对象存储的 Endpoint 是公网 IP,会把账单打爆,所以这里还需要配置 DNS:(说明一下,这里不知道什么毛病,dnsConfig 居然用了 tpl 方法但是 values.yaml 里面又是个 map,所以只能喂个 string,然后部署的时候会有警告)

    singleBinary:
      replicas: 1
      nodeSelector:
        kubernetes.io/hostname: hkg-qcloud
      dnsConfig: |-
        nameservers:
          - 183.60.83.19
          - 183.60.82.98

    启动后发现 Loki 有报错,日志没有持久化:

    level=error ts=2025-04-23T08:34:33.088568269Z caller=flush.go:261 component=ingester loop=29 org_id=xxx msg="failed to flush" retries=1 err="failed to flush chunks: store put chunk: InvalidArgument: invalid x-cos-storage-class for role mode bucket, only support intellingent tiering or default\n\tstatus code: 400, request id: xxx, host id: , num_chunks: 1, labels: {app=\"loki\", component=\"gateway\", container=\"nginx\", filename=\"/var/log/pods/monitoring_loki-gateway-6fb5686c6f-42wcc_ce7d81e7-a5fb-4436-b2e8-51abf866f056/nginx/0.log\", instance=\"loki\", job=\"monitoring/loki\", namespace=\"monitoring\", node_name=\"hkg-qcloud\", pod=\"loki-gateway-6fb5686c6f-42wcc\", service_name=\"loki\", stream=\"stderr\"}"

    看起来是 Loki 在调用上传的时候,加了对象的预期存储类型,轻量对象存储可能刚好简化了这个类型。尝试从代码里面找的话, 可以发现确实支持进行配置:

    文档中也给出了相应说明:Grafana Loki configuration parameters | Grafana Loki documentation。所以我们可以新增配置:

    loki:
      storage_config:
        aws:
          storage_class: INTELLIGENT_TIERING

    问题解决。Loki 日志显示开始正常存取日志块,并且轻量对象存储控制台上也能看见相应的文件了。

    附录

    完整的 Loki values.yaml

    deploymentMode: SingleBinary
    loki:
      commonConfig:
        replication_factor: 1
      storage:
        type: s3
        s3:
          endpoint: cos.ap-hongkong.myqcloud.com
          region: ap-hongkong
          accessKeyId: <ak>
          secretAccessKey: <sk>
        bucketNames:
          chunks: <bucketName>-<uid>
          ruler: <bucketName>-<uid>
          admin: <bucketName>-<uid>
      storage_config:
        aws:
          storage_class: INTELLIGENT_TIERING
      schemaConfig:
        configs:
          - from: 2024-04-01
            object_store: s3
            store: tsdb
            schema: v13
            index:
              prefix: index_
              period: 24h
      limits_config:
        retention_period: 4320h
      compactor:
        retention_enabled: true
        delete_request_store: s3
    singleBinary:
      replicas: 1
      nodeSelector:
        kubernetes.io/hostname: hkg-qcloud
      dnsConfig: |-
        nameservers:
          - 183.60.83.19
          - 183.60.82.98
    chunksCache:
      enabled: false
    gateway:
      nodeSelector:
        kubernetes.io/hostname: hkg-qcloud
    resultsCache:
      enabled: false
    read:
      replicas: 0
    backend:
      replicas: 0
    write:
      replicas: 0

  • domain 的自言自语 2504

    Linux

    深入理解Linux Netlink机制:进程间通信的关键 – 知乎

    The proc/net/tcp and proc/net/tcp6 variables — The Linux Kernel documentation

    kernel.org/doc/Documentation/networking/proc_net_tcp.txt

    How to increase swap space? – Ask Ubuntu

    GitHub – canonical/cloud-utils: This package provides a useful set of utilities for interacting with a cloud.

    apt install cloud-guest-utils -y
    growpart /dev/sda 1
    e2fsck -f /dev/sda1
    resize2fs /dev/sda1

    读书

    大教堂与集市 (豆瓣)

    网络

    HTTPS 温故知新(四) —— 直观感受 TLS 握手流程(下)

    Submarine Cable Map

    杂项

    calculator-app – Chad Nauseam Home
    搞个计算器是真的不容易。

    Site Names in Google Search | Google Search Central  |  Documentation  |  Google for Developers
    发现搜索的时候,站点名称没有展示,而是显示为域名。

    找到了从哪里获取的就比较好办了,这段可以直接在 functions.php 里面注入:

    add_action('wp_head', function () {
        echo '<script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","name":"我们的笔记","url":"' . home_url() . '"}</script>';
    });

  • linuxserver/openssh-server mods

    最近希望给一个服务器启动一个独立的 openssh-server 容器用于安全转发内部接口请求,发现 linuxserver/openssh-server 镜像非常适合这一场景。

    不过这个镜像默认是不开启 TCP 转发的,官方提供了一个较为优雅的模块插件机制来实现类似功能,模块可以在这里找到:Linuxserver Container Mods。只要配置环境变量 DOCKER_MODS=linuxserver/mods:openssh-server-ssh-tunnel 就可以自动加载了。

    看起来模块只是一些脚本,具体的实现在这里:docker-mods/root/etc/s6-overlay/s6-rc.d/init-mod-openssh-server-ssh-tunnel-setup/run at f7fc561d103d6832bb75a4cb4f575b1166180430 · linuxserver/docker-mods · GitHub

    那么模块是怎么被拉取的呢,离线的私有化场景中怎么处理?可以在这里找到:docker-mods/docker-mods.v3 at cac9e7450a0698f19d750b67db61c4aa214d5290 · linuxserver/docker-mods

    也就是说,模块实际上都是 Docker 镜像,并且这个脚本已经支持了自定义制品库拉取,我们应该可以编写自己的模块,放到私仓,然后在离线环境中拉取并加载使用了。有空的时候来测试一下这个场景。

    引用

    For the ones who don’t know about the existence of Linuxserver Docker mods : r/selfhosted


  • webpack loader

    webpack 只能理解 JavaScript 和 JSON 文件。

    在 webpack 中,loader 用于对模块的源代码进行转换。可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。

    配置

    module.rules 允许你在 webpack 配置中指定多个 loader。

    loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)

    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              { loader: 'style-loader' },
              {
                loader: 'css-loader',
                options: {
                  modules: true,
                },
              },
              { loader: 'sass-loader' },
            ],
          },
        ],
      },
    };

    在以上例子中:

    1. 执行 sass-loader :Sass 或 SCSS 文件编译成 CSS 代码。
    2. 执行 css-loader:解析第一步生成的 CSS 文件,将 CSS 代码转换成 JavaScript 模块;options: { modules: true } 启用 CSS 模块化功能,这意味着每个 CSS 文件都会被视为一个独立的模块,可以避免样式冲突。
    3. 执行 style-loader :将 css-loader 处理后的 CSS 代码动态地插入到 HTML 页面的 <style> 标签中。

    特性

    • 链式调用:一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。
    • loader 可以是同步的,也可以是异步的
    • loader 运行在 Node.js 中,并且能够执行任何操作
    • loader 可以通过 options 对象配置
    • 除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块。
    • 插件(plugin)可以为 loader 带来更多特性。
    • loader 能够产生额外的任意文件。

    类型

    • loader: 核心的转换器,负责将源文件转换成 webpack 可以理解的模块。
    • preLoader: 在普通 loader 之前执行的 loader,主要用于预处理文件,例如代码检查(linting)。
    • postLoader: 在普通 loader 之后执行的 loader,主要用于后处理文件,例如优化或添加额外功能。

    执行顺序

    webpack 在处理模块时,loader 的执行顺序如下:

    1. preLoader
    2. loader
    3. postLoader

    举个例子

    preLoader 示例:ESLint 代码检查

    module: {
      rules: [
        {
          enforce: 'pre', // 指定为 preLoader
          test: /\.js$/,
          exclude: /node_modules/,
          loader: 'eslint-loader',
          options: {
            // eslint 配置选项
          },
        },
        // 其他 loader 配置
      ],
    },

    postLoader 示例:添加版权信息

    • 创建一个自定义的 postLoader,在代码处理完成后,自动添加版权信息到 JavaScript 文件的顶部。
    //copyright-loader.js
    module.exports = function(source) {
      const copyright = '// Copyright (c) 2024 Your Company\n';
      return copyright + source;
    };
    module: {
      rules: [
        {
          enforce: 'post',  // 指定为 postLoader
          test: /\.js$/,
          exclude: /node_modules/,
          use:{
            loader:path.resolve(__dirname,'copyright-loader.js')
          }
        },
        // 其他 loader 配置
      ],
    },