聊聊javascript中令人头大的this

大家新年好啊,这是 2023 年第一篇文章,对笔者来说也是一个新的技术尝试,看似标题党,但确实是在实话实说 ~

起因

虽然我司从长远打算上是准备使用 Rust 来做底层及逻辑层跨端架构,但这落地时间会拉的很长,且没有一个合适的切入点。

现状是年底的时候大家都🐑了(笔者很嘴硬的没阳,但带薪病假浪费了 (o_ _)ノ),需求上也基本是停滞状态,那就有空来折腾下 Rust,让自己锈一锈。

学习上从零开始拜读 《Rust 圣经》,但在基础入门篇看了一大半之后,实在是看不下去了...学而不思则惘,虽然搞明白了最关键的所有权和借用,但不到项目里用一下就等于啥也不会[手动狗头]。

刚好有一个由来已久的业务痛点,我司的埋点是否符合预期,这个验证在移动端上是较为复杂的,原因有以下几点:

  • 埋点传输过程中是加密的,很难通过抓包的形式看到埋点是否正确。
  • 埋点在移动端是有缓存的,并不会实时上报。
  • 埋点在数据入库的时候也是有延迟的,在平台上查询埋点也需要一定时间成本。更重要的是,埋点需要验证各种时机是否正确,那需要实时性更高的方式。

而在跨端开发越来越频繁的今天,矛盾也就越来越凸显,对于 H5 开发的同学或者 Flutter 开发的同学来讲,埋点出现查不到或者埋入错误的问题后,需要耗费大量的时间和精力来排查原因,所以迫切需要一个能实时查看埋点是否正确的方式。

思考

最早的想法是让埋点实时显示在 App 上,但这存在以下的问题:

  • App 屏幕实在不大,没有更多的空间能完整显示当前的埋点信息,如果做成需要打开页面来查看的方式,实时性也不高。
  • 需要额外开发页面,虽然可以用 Flutter 来减少双端开发成本,但也是需要一定的开发成本。

那有没有一种方式可以解决呢?

思路换一下,无论是 LookinServerFlipper 等 Debug 利器,还是 Flutter / Web Debug Tools,都是在电脑上调试 App。那我们也可以用类似的方式,把实时埋点数据显示在电脑上,不再局限于同一块屏幕。

通信方式

我们需要的是一个尽量简单的通信,所以目标上选定使用 WebSocket,且上述解决方案也大都是这样的选择。

技术选型

在大方向上,首先排除掉 iOS 、Android 原生开发的方式来实现。

而 Flutter 并不适合做底层及逻辑层,它更适合做页面。

Rust 更符合我们的需求,可以覆盖了所有终端,性能又高,而且不会加入到线上生产包中,很适合做 Rust 在移动端的第一步落地。

唯一的问题就是还不会,所以需要花时间摸索下。

当前效果

打开 Rust 服务端执行程序,等待连接:

image2022-12-28_18-46-9.png

在 App 上输入要连接的 IP 及 端口号:

IMG_5190.PNG

服务端执行程序会实时显示收到的埋点信息:

image.png

实现过程

笔者在之前已经搭建好了 Rust 的开发环境,也成功的跑通了 “Hello World”,所以配置部分并不在本文的介绍中。

因为是完全陌生的技术领域,所以对笔者来说有2个需要解决的难点:

  • Rust WebSocket 实现。
  • Rust 与 iOS / Android 通信。

WebSocket

笔者落地方案的时候并没有了解 Rust WebSocket 原理,本着快速落地的方式,先去找了 《Rust 圣经》里面的日常开发三方库精选,里面有这三个推荐库:

image.png

库好是好,也都是生产级的库,但笔者 Rust 功力几近于无,要不是 Demo 跑不动,要不是理解不了,改不动 (o_ _)ノ。

后面查到一个实现尤为简单的库,刚好能拿来就用。

[dependencies]
ws = "0.9.2"
复制代码

传送门(github.com/housleyjk/w…

服务端

服务端的作用就是接收 App 来的消息并显示,用配套的 ws-rs 库 Rust 服务端代码,很容易实现:

if let Err(error) = listen(address, |out| {
    ...
}) {
    // 通知用户故障
    println!("创建Websocket失败,原因: {:?}", error);
}
复制代码

address 是 ip + 端口号,顺便打印出来,让 App 输入连入。

let address = format!("{}:{}", get_ip().unwrap(), get_available_port());
println!("当前地址为:{}", address);
复制代码

get_ip() 获取本机 ip 地址。

get_available_port() 获取本机可用的端口号。

方法网上搜的,可能不是是最佳的方法:

image.png

最后把收到的信息格式化输出即可,用 serde_json 库做 json 解析:

...
        // 处理程序需要获取 out 的所有权,因此我们使用 move
        move |msg: ws::Message| {
            let text = msg.as_text();
            match text {
                Ok(_t) => {
                    // 格式化
                    let json: Value = serde_json::from_str(_t).unwrap();
                    println!(
                        "触发埋点\n event_id: {}\n event_name: {}\n attributes: {}\n\n",
                        json["event_id"],
                        json["event"],
                        serde_json::to_string(&json["attributes"]).unwrap()
                    );

                    // 使用输出通道发送消息
                    out.send(msg)
                }
                Err(_) => todo!(),
            }
        }
...
复制代码

然后执行 cargo run --release 打包成执行程序。

image2022-12-28_18-45-52.png

客户端

客户端代码也很简单,连接服务端的 ip + 端口,并发送消息即可。

...
    if let Err(error) = connect(c_host, |out| {
        // 将WebSocket打开时要发送的消息排队
        if out.send(c_message).is_err() {
            println!("[gaoding-log-view-kit]: 无法初始消息排队")
        }
        // 关闭连接
        out.close(CloseCode::Normal)
    })
...
复制代码

这里感叹下,Rust 中真的是轮子众多,集成度又高,真的是不懂所以然也可以拆箱即用。

Native 通信

上面的 websocket 过程很顺利的完成了,但如何在 App 中发送 ws 消息?网上包括掘金里有大量的资料,但基本都是同一份来源,看翻译的不如看原文更详细,能少踩坑。

iOS 传送门

Android 传送门

这2篇文章以及 git 源码 会教你 App 如何让 Rust 打印个 “Hello World”,但里面也有一些遇到的弯弯绕绕需要注意。

Rust + iOS

先来看 iOS 是如何做的。

前面的配置过程文章讲的很详细,这里略过。

比较关键的几个环节:

库模式配置

[lib]
crate-type = ["staticlib", "cdylib"]
复制代码

入口更改为 src/lib.rs,没有就创建一个。(这里可以不用 lib.rs 做文件名吗?笔者还没具体了解,有了解的同学可以评论告知下)

暴露方法

use std::{ffi::CStr, os::raw::c_char};
...

#[no_mangle]
pub extern "C" fn send_wind_info(message: *const c_char, host: *const c_char) {
...
}
复制代码

暴露 send_wind_info(...) 发送埋点信息方法,且提供2个参数,一个是当前消息、一个是发送的域名。

理论上,host 参数在 Rust 的内存持有即可,但这还写不出来 ... 当然我们把这个持有放到 Native 来做即可以绕过去。

打包及制造头文件

终端执行 cargo lipo --release 命令就可拿到 .a 文件,Rust 还是很方便的。

image.png

然后我们需要提供文件来给 iOS 调用:

api.h

#include <stdint.h>

/// 发送埋点信息
void send_wind_info(const char *message, const char *host);
复制代码

Cocoapods

按文章上说的硬链到项目里,太令人难受了,我们当然选择用 pod 本地库引用的形式。

image.png

podspec 关键部分:

...
  s.source_files = 'GDLogViewKit/Classes/**/*.{m,h}'
  s.vendored_libraries = 'GDLogViewKit/Library/**/*.a'
  s.public_header_files = 'GDLogViewKit/Classes/*.h'
...
复制代码

再封装

GDLogViewKit.h / GDLogViewKit.m 提供的就是 api.h 的封装,毕竟外部直接调用 api.h 的 send_wind_info(...) 方法实在不够优雅。

#import "api.h"

static NSString *connectAddress;
...
+ (void)sendWindMessage:(NSString *)message {
  if (!connectAddress || !message) {
    return;
  }
  send_wind_info([message cStringUsingEncoding:NSUTF8StringEncoding], [connectAddress cStringUsingEncoding:NSUTF8StringEncoding]);
}
复制代码

connectAddress 就是用于保存 ip + 端口的静态变量。


iOS 上还是很简单的,也可能因为笔者毕竟是一个 iOSer [手动狗头]。整体上就是构造一个 ObjectiveC - C - Rust 通信过程。

Rust + Android

Android 就坎坷了很多,笔者对 Android 并不算熟,遇到了挺多问题,这里一一记录下。

NDK 配置

首先要找到自己 Android SDK 的安装目录,然后找到里面的 NDK 文件夹

image.png

然后执行文章中的语句来生成到一个目录下(这个目录根据文章所说生成到项目目录下)。

image.png

但是,

根据文章来执行这三句总是失败 - -!

我也尝试用 make-standalone-toolchain.sh shell 脚本也不行

image.png

ERROR: Failed to create toolchain

image.png

这个错误网上搜了下也没有人解释原因,最后想到,我在 python 开发上用的是 python3, 会不会是这个原因?

python3 'xxx/make_standalone_toolchain.py' --api 26 --arch arm64 --install-dir NDK/arm64
复制代码

成功了 ...

再往下照文章执行即可。

JNI

Android 与 iOS 不同的是,多了一层 JNI,相当于 JAVA - JNI - C - Rust,这一部分以前并没有接触过,一开始靠硬写,后面发现可以通过 javac 来生成。

先在 Cargo.toml 上增加 jni 工具配置,能简便我们编写 jni

[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.20.0", default-features = false }
复制代码

在 src/lib.rs 增加 Android 胶水代码:

image.png

打包 so 库

学文章的方式,增加一个 cargo-config.toml 再执行拷贝命令 cp cargo-config.toml ~/.cargo/config(但这个很奇怪,是每个项目都要执行吗?有懂的同学评论告知下)。

image.png

然后调用生成 so 库命令

cargo build --target aarch64-linux-android --release           
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release
复制代码

Gradle

同样,我们也制作一个独立的 gradle 库

image.png

将 so 库放入到 jniLibs 目录下,这里也有一个坑

image.png

文章上是放到这个位置,但我们项目里需要放入到如下位置才能被读取到

image.png

据 Android 开发同学说是不同的项目上依赖包配置不同导致的。

再封装

跟 iOS 一样,封装的工具方法:

GDLogViewKit.java

package com.gaoding.log.view.kit;

/**
 * 日志可视化工具包
 */
public class GDLogViewKit {

    static {
        // 库引用
        System.loadLibrary("gaoding_log_view_kit");
    }
  
    private static String kAddress = null;
    
    // native 方法,对应 JNI 
    private static native void sendWindInfo(final String message, final String host);

    ...
    
    public static void sendWindMessage(String message) {
        if (kAddress == null) {
          return;
        }
        GDLogViewKit.sendWindInfo(message, kAddress);
    }
}
复制代码

总结

一个简单的埋点实时可视化的轮子就算是做好了,相信它能在项目中起到应有的作用,真的要再感叹 Rust 生态环境的强大,也坚定了后续学习 Rust 的决心。

后续扩展上,可以抛弃 Rust 的服务端,WS 服务上到内部平台上,来把轮子做大。也可以扩展到其他日志信息,甚至可以提供控制指令来操作 App。因为是 Rust,所以能很方便的集成到 Web、桌面等其他终端应用上。

不足的点,还是因为 Rust 没入门,继续啃圣经,先尝试把 ip 地址缓存的功能放到 Rust 中,待修炼飞升后,把上述过程加入到跨端工具链全家桶中 ...

另外,以上代码已开源 git 传送门,有兴趣的可以了解。


感谢阅读,如果对你有用请点个赞 ❤️

中秋节GIF动图引导在看提示.gif

猜你喜欢

转载自juejin.im/post/7218438681060999226