作者: Allison

  • 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);
  • 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 配置
      ],
    },

  • SSH: no matching host key type found.

    最近在使用 SSH 连接服务器时有报错:

    Unable to negotiate with xx.xx.xx.xx port 22: no matching host key type found. Their offer: ssh-rsa,ssh-dss

    主要原因是服务端较旧,客户端较新,服务端提供的密钥算法在客户端均已经默认被禁用。其中:

    这里只能进行一个 workaround,在连接时临时允许特定算法/密钥。

    ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa user@host

    如果是 Git 推拉代码的场景,也是同理:

    GIT_SSH_COMMAND="ssh -oHostKeyAlgorithms=+ssh-rsa" git clone ssh://user@host/repo

    引用

    OpenSSH: Legacy Options

    web hosting – Unable to negotiate with XX.XXX.XX.XX: no matching host key type found. Their offer: ssh-dss – Stack Overflow

    Git error no matching host key type found. Their offer: ssh-rsa – Stack Overflow

  • Hello,鸿蒙应用

    感觉鸿蒙应用文档很多,初学者看起来有点乱乱的,记录一下快速开发方法。

    开发者账号注册

    你需要提前注册好华为开发者联盟账号,否则在后续开发调试应用过程中无法完成自动签名、发布等操作

    安装IDE

    HUAWEI DevEco Studio下载链接

    Hello World

    新建Project,在应用中选择第一个模版项目

    真机调试

    准备好你的华为手机,使用USB数据线连接到电脑。

    • 打开开发者模式
    • 打开“USB调试”开关

    基础知识

    应用名称修改

    1. 配置文件:src/resources/zh_CN/element/string.json
    2. 修改nameEntryAbility_label对应的value

    应用图标修改

    1. 配置文件:src/resources/base/media

    入口文件

    1. 配置文件:src/main/module.json5
    2. 查看 srcEntry 指向的文件
    3. 查看onWindowStageCreate方法中加载的页面为入口文件

    新建页面

    1. 配置文件:src/resources/base/profile/main_pages.json
    2. 在src/ets/pages目录下新建同名ets文件

    页面之间跳转

    1. 在A页面中注册按钮,绑定点击事件,使用router.pushUrl点击即跳转到第二个页面
    2. 在B页面中注册按钮,绑定点击事件,router.back()点击跳回A页面
    // Page A
    Button() {
      Text('Next')
        .fontSize(30)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
    }
    .type(ButtonType.Capsule)
    .backgroundColor('#409EFF')
    .width('40%')
    .height('5%')
    .onClick(() => {
      router.pushUrl({url: 'pages/Second'}).then(() => {
        console.info('Jumping to second page')
      }).catch((err) => {
        console.error(`Failed to jump to the second page. Code is ${err.code}, message is ${err.message}`)
      })
    // Page B
    Button() {
      Text('Back')
        .fontSize(25)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
    }
    .type(ButtonType.Normal)
    .backgroundColor('#409EFF')
    .width('40%')
    .height('5%')
    .onClick(() => {
      router.back()
    })

    常见问题

    真机运行报错:

    Failure[MSG_ERR_INSTALL_FAILED_NO_BUNDLE_SIGNATURE]

    解决:单击Open signing configs 或 通过File -> Project Structure -> Signing Config 重新登录

  • macOS部署Comfy UI

    安装Python

    brew install python

    安装PyTorch

    pip install torch torchvision torchaudio
    // or
    python3 -m pip install torch torchvision torchaudio

    克隆Comfy UI仓库

    git clone https://github.com/comfyanonymous/ComfyUI
    cd ComfyUI

    安装依赖

    pip install -r requirements.txt

    运行Comfy UI

    python main.py
    // or
    python3 main.py

    下载模型

    liblibhuggingface下载模型后,放在Comfy UI仓库 models/checkpoints 目录下,即可选择模型,运行工作流出图。

  • 离开页面时调接口

    场景:当前页面正在执行异步任务,当用户点击导航栏或直接关闭页面时,需调接口取消任务,减少资源浪费。

    问题:如果在beforeunload事件中调ajax 请求,则会出现浏览器cancel掉请求的情况。

    解决:使用sendBeacon方法,适用于发送少量数据到服务器。

    Navigator.sendBeacon()

    这个方法主要用于满足统计和诊断代码的需要,这些代码通常尝试在卸载(unload)文档之前向 Web 服务器发送数据。过早的发送数据可能导致错过收集数据的机会。然而,对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在 unload 事件处理器中产生的异步 XMLHttpRequest

    Vue 3代码如下

    <script setup>
    import { onBeforeUnmount, onMounted } from 'vue';
    
    // 取消任务的API函数
    function cancelTask() {
      navigator.sendBeacon('/api/cancel-task', JSON.stringify({ task: 'taskId' }));
    }
    
    onMounted(() => {
      // 添加 beforeunload 事件监听器
      window.addEventListener('beforeunload', cancelTask);
    });
    
    onBeforeUnmount(() => {
      // 组件卸载前移除监听器
      window.removeEventListener('beforeunload', cancelTask);
    });
    </script>