Hello,鸿蒙应用

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

开发者账号注册

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

安装IDE

HUAWEI DevEco Studio下载链接:https://developer.huawei.com/consumer/cn/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 重新登录

Element Plus 下拉框懒加载

需求背景

常见的需求中经常有下拉框作为搜索的条件之一,我们需要先调接口拉取数据供用户进行选择。当遇到数据量大,或考虑到随着时间推移,数据量会变得庞大的情况,通常接口会采取分页设计。

最佳实践

  1. 默认先只拉取第一页数据,供用户选择
  2. 当用户滚动至数据末尾,拉取下一页数据(数据懒加载)
  3. 支持关键词搜索
  4. 支持一键清空

实现细节

Vue项目我们团队基本上统一使用Element UI / Element Plus,本次针对 Element Plus的Select组件实现了公共方法 – 自定义指令完成懒加载。(Vue 3适用)

使用

// 引入
import vLoadMore from "@/utils/loadMore"

// 绑定指令
<el-select v-load-more="loadMoreData"></el-select>

原理

基于Element Plus Select组件滚动区域,监听滚动是否到达底部

懒加载逻辑

当元素内容的总高度 – 已滚动的高度 <= 元素的可见区域高度,代表当前滚动位置已经接近或到达底部,此时调用父组件传入回调方法即可。

初始状态

滚动到底部

Element Plus Select组件滚动区域选择

当页面中只用到一个下拉框,你可以直接使用类名定位。

当页面中有多个下拉框,上面的方法会导致数据错乱。查看Select DOM可以发现,不同的下拉框记录了不同的control id

而control id可以定位到对应的下拉框。举例:平台选择器的control id是”el-id-6389-3″,对应的下拉框DOM id也是”el-id-6389-3″

因此,我们可以利用以上属性定位到Select组件的实际滚动区域。

代码

/* Element Plus
   Select组件 自定义指令-触底加载更多
   使用:
       1. 页面中引入
       ```
        import vLoadMore from "@/utils/loadMore"
       ```
       2. 绑定自定义指令
       ```
       <el-select v-load-more="loadMoreData"></el-select>
       ```
 */
const vLoadMore = {
    mounted(el, binding) {
        const child = el.querySelector('.el-select__input')
        const id = child.getAttribute('aria-controls')
        const popper = document.getElementById(id)
        const SELECT_DOM = popper.parentElement

        SELECT_DOM.addEventListener('scroll', function () {
            const CONDITION = this.scrollHeight - this.scrollTop <= this.clientHeight
            if (CONDITION) {
                binding.value()
            }
        })
    }
}

export default vLoadMore

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>

迁移monorepo保留原仓库提交记录

将代码仓库转移到monorepo如果直接将代码复制到大仓中就会丢失原本的代码提交记录。而大多数情况下我们希望保留提交记录以方便后期问题定位等情况。

  • 在原仓库中进行以下操作,这里注意文件移动需要时间,确保全部文件移动完成后再进行下一步提交操作
# 移动文件
git mv -k * projects/{你的项目名}
git mv -k .* projects/{你的项目名} 

# 提交仓库
git commit && git push
  • 在迁移目标仓库中进行以下操作
git remote add -f my-app {原仓库git地址}

git merge my-app/{分支名} --allow-unrelated-histories

git remote remove my-app
git push

parseInt

parseInt永远将输入解析为字符串

parseInt({toString: () => 2, valueOf: () => 1}); // -> 2
Number({toString: () => 2, valueOf: () => 1}); // -> 1

一个常见的栗子

parseInt("apple"); // -> NaN
parseInt("apple", 16); // -> 10

上面这个例子是因为parseInt会一个个解析字符直到解析不了,所以解析十六进制中的a为10作为结果返回。

parseInt Infinity

parseInt("Infinity", 10); // -> NaN
// ...
parseInt("Infinity", 18); // -> NaN...
parseInt("Infinity", 19); // -> 18
// ...
parseInt("Infinity", 23); // -> 18...
parseInt("Infinity", 24); // -> 151176378
// ...
parseInt("Infinity", 29); // -> 385849803
parseInt("Infinity", 30); // -> 13693557269
// ...
parseInt("Infinity", 34); // -> 28872273981
parseInt("Infinity", 35); // -> 1201203301724
parseInt("Infinity", 36); // -> 1461559270678...
parseInt("Infinity", 37); // -> NaN

parseInt null

parseInt(null, 24); // -> 23

上面的结果都是一样的解析过程,按字母表排序,每个字符对应数字是固定的。比如,I对应18,在18进制中只能解析0-17,所以parseInt("Infinity", 18); // -> NaN,在19进制中就能解析出结果:parseInt("Infinity", 19); // -> 18

n对应23,在23进制中解析不了,只返回18,在24进制中就是18 * (24 ^ 5) + 23 * (24 ^ 4)+ 15 * (24 ^ 3)+ 18 * (24 ^ 2) + 23 * 24 + 18 = 151176378,其他依此类推。

八进制

parseInt("08"); // -> 8, 支持ES5
parseInt("08"); // -> 0, 不支持ES5

以0开头的数字可以被解析为10进制或8进制,ECMAScript 5规定为10进制,但不是所有浏览器都支持它。所以使用parseInt时指定进制是好习惯,有时可以避免不必要的bug。

浮点数

parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1/1999999); // -> 5

parseInt 接受一个字符串参数并返回一个指定基数的整数。 parseInt 还会去除字符串参数中第一个非数字之后的任何内容。0.000001转换为字符串后是"0.000001",0.0000001转换后是"1e-7",所以返回1,1/1999999转换后是5.00000250000125e-7,返回结果是5。

本地开发debug npm包

使用pnpm包管理

1. 在本地npm包目录下执行

pnpm link --global

2. 如果是cli,可直接在其他项目目录下调用

3. 如果需要在其他项目中引入npm包,在其他项目目录下执行

pnpm link --global npm-package-name

使用npm包管理

1. 在npm包目录下执行

npm link

2. 在其他目录使用

npm link 本地npm包绝对路径

官方文档:https://docs.npmjs.com/cli/v9/commands/npm-link

async/await class constructor

背景:期望在新建类时调后台接口对类进行配置,配置成功后才可以执行其他操作。

尝试封装类的构造函数为async/await,报错

Class constructor may not be an async method

原因:async函数返回值为promise,而构造函数返回值为object,同一个函数不可能返回值同时为promiseobject

解决:参考 jQuery's ready()方法

1. 类实现

    class myClass {
        constructor () {

        }

        async ready (callback) {
            await yourHttpRequest();
            // 回调函数绑定作用域
            callback.bind(this)();
        }
    }

2. 调用

    const myObj = new myClass();
    myObj.ready(function() {
        // 保证配置完才能执行其他操作
    });

参考:https://stackoverflow.com/questions/43431550/async-await-class-constructor

Vue 3 TypeScript项目报错类型{}不存在属性

搜了一下tsconfig.json文件需要配置"jsx": "preserve",但我的config文件原先就有这个配置。因为项目一开始是可以正常报错的,迁移到大仓后就有问题了。中间也考虑了是否是配置文件位置的问题,调整了位置也没有解决问题。

最后看到tsconfig.json文件另一个报错“找不到node的类型定义文件”

原因是我没有安装@types/node,装上后重新打开IDE”类型{}不存在属性“和“找不到node的类型定义文件”报错都没有了。

// npm
npm i @types/node --save-dev

// pnpm
pnpm add @types/node

【Rust】Hello World

创建main.rs文件来编写一个将文本打印到屏幕上的小程序Hello, world!

fn main() {
    println!("Hello, world!");
}

Rust 文件总是以 .rs 扩展名结尾。如果在文件名中使用多个单词,惯例是使用下划线将它们分开。例如,使用hello_world.rs而不是helloworld.rs。

编译和运行

Linux或macOS

rustc main.rs
./main
编译
编译成功后,Rust 输出二进制可执行文件
运行

Windows

rustc main.rs

.\main.exe

编译后,使用 Windows 上的 PowerShell执行 ls 或者 使用 CMD 时执行

 dir /B %= the /B option says to only show the file names =%

都可以看到三个文件:

main.exe
main.pdb
main.rs

扩展名为.rs的源代码文件、可执行文件(在 Windows 上为main.exe,但在所有其他平台上为main),以及使用 Windows 时包含调试信息且扩展名为.pdb的文件。

程序剖析

函数main始终是每个可执行 Rust 程序中运行的第一段代码。

  1. 函数体包裹在{}。Rust 要求所有函数体都用大括号括起来。
  2. Rust 风格是用四个空格缩进,而不是制表符。
  3. 使用 a!意味着您正在调用宏而不是普通函数,并且宏并不总是遵循与函数相同的规则。上面代码中println!调用 Rust 宏,如果它调用了一个函数,输入为println(不带!)
  4. 分号结束一行,表示表达式已结束

编译和运行是分开的步骤

Rust 是一种提前编译语言,这意味着可以编译一个程序并将可执行文件提供给其他人,他们可以在没有安装 Rust 的情况下运行它。

如果给别人一个.rb.py.js文件,他们需要安装 Ruby、Python 或 JavaScript 实现(分别)。但是在这些语言中,只需要一个命令来编译和运行程序。一切都是语言设计的权衡。