iOS(swift)与H5(Vue+ts)交互

本文介绍了如何在Vue项目中使用TypeScript(TS)和Swift的WKWebView进行H5与原生应用的交互,包括扩展window对象、组合式API的工具类实现、同步异步操作以及利用messageHandlers进行数据缓存和跨平台通信的详细步骤。

解决问题

1、ts中如何扩展window全局对象,给它增加方法和参数
2、组合式API如何构造一个工具类
3、同步、异步操作
4、组合式API 双向绑定,方法调用
5、H5和原生交互
6、WKWebView使用
7、window.webkit.messageHandlers使用说明

环境说明

VUE 组合式API + TS +原生Swift

功能说明

这是一个简单的示例,实现输入数据缓存到原生,并在适当的时候,又从原生取回来,模拟H5和原生数据交互。
请添加图片描述

实现步骤说明

vue ts中window挂载全局标识、方法

在单独的ts(如native.d.ts)文件中,给window挂载一个参数和一个方法

declare global {
  interface Window {
    /// 标识,用来区分是不是嵌入在原生里面
    AppNative: NativeClass;
    /// 方法,用来给原生调用,响应js端的回调
    callbackFun: (callbackFunction: string, callBackFucName: string) => void
  }
}

interface NativeClass {
  /// 方法,用来调用原生的方法
  callNative(service: string, data: string, callBackFucName:string) : void
}
export default {};

定义同步、异步的交互逻辑

在单独的ts(如nativeUtil.ts)文件中,实现与原生的交互逻辑

/// 所有的回调方法Map
const callbackFuncNameMap = new Map();
/// 交互方法
const callLocal = (
  // 访问的服务,其实就是定义一个标识,给到原生
  // 原生判断标识,去做双方确认好要干的事情,如:存数据,取数据
  service: string,
  // 传入给原生的参数
  data: string,
  // 回调方法
  callbackFunc: any
) => {
  /// 回调方法的函数都是:callbackFun,这里是给每个函数取个名字
  /// 然后存在map中,传到原生
  const callbackFuncName = service + "Callback"
  callbackFuncNameMap.set(callbackFuncName, callbackFunc);
  setTimeout(() => {
    if (window.AppNative) {
      window.AppNative.callNative(service, data, callbackFunction);
    } else {
      console.error('App native service not found!');
    }
  }, 0);
};

// 回调函数,拿给原生作为回调入口方法
// 这个方法可能会有多个地方调用,如存完数据要回调,取完数据要回调
// 所以需要给标识,告诉ts,是原生干完啥功能后在回调
// callbackFunction这个就是标识,它由
// *const callbackFunction = service + "Callback"*所确定
window.callbackFun = (callbackFunction: string, callbackData: string) => {
  const getFunc = callbackFuncNameMap.get(callbackFunction);
  getFunc(callbackData);
  callbackFuncNameMap.delete(callbackFunction);
};

// 异步方法
const asyncCallLocal = (service: string, data: string):Promise<any> => new Promise((resolve) => {
  callLocal(service, data, (res: any) => {
    // 回调
    resolve(res);
  });
});

// 同步方法
const syncCallLocal = async (
  // 服务名
  service: string,
  // 参数
  data: string,
) => {
  let result: any = null;
  // 同步
  await syncCallLocalNext(service, data).then((res) => {
    result = res;
  });
  return result;
};

const syncCallLocalNext = (
  // 服务名
  service: string,
  // 参数
  data: string,
) => new Promise((resolve) => {
  callLocal(service, data, (res: any) => {
    // 回调
    resolve(res);
  });
});

// 方法导出
export { asyncCallLocal, syncCallLocal };

swift 新建webView

实现webView方法和代理,并配置configuration

    lazy var webViewConfig: WKWebViewConfiguration = {
        let config = WKWebViewConfiguration()
        config.userContentController = WKUserContentController()
        config.userContentController.add(self, name: "H5Native")
        // 注入JS代码
        if let scriptPath = Bundle.main.path(forResource: "H5Bridge", ofType: "js"),
           let source = try? String(contentsOfFile: scriptPath) {
            let bridgeScript = WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: false)
            config.userContentController.addUserScript(bridgeScript)
        }
        return config
    }()

桥接H5Bridge.js文件

给H5的window上挂载AppNative标识,标明这是在原生之中,再通过window.webkit.messageHandlers[service] 访问原生监听的方法,使用postMessage触发传参

(function () {
    
    window.AppNative = {
        // service :H5要调用的服务名称,区分不同的原生功能
        // data:H5传递给原生的数据,json字符串
        // callBackFucName:将会在回调时原样传回给调用方
        callNative(service, data, callBackFucName) {
            let handler = window.webkit.messageHandlers[service];
            if (handler) {
                handler.postMessage({
                    data,
                    callBackFucName
                });
            }
        }
    };
})()

实现原生收发消息的处理

H5Native是事先双方定义好的标识,如我这里就被定义成为了存储和获取缓存数据事件,这是个自定义的可变项

class ViewController: UIViewController, WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "H5Native" {
            let base = try? BaseModel.decode(from: message.body)
            if let model = base {
                let cache = try? CacheParam.decode(from: model.data)
                if let info = cache, info.type == 0, let str = try? info.data.toJsonString() {/// 存数据
                    UserDefaults.standard.setValue(str, forKey: "kCache")
                } else {/// 取数据
                    if let cache = UserDefaults.standard.string(forKey: "kCache") {
                        let funcIndex = "'\(model.funcIndex)'"
                        let jsStr = """
                        callbackFun(\(funcIndex),\(cache));
                        """
                        webView.evaluateJavaScript(jsStr) { item, err in
                            print(err)
                        }

                    }

                }
            }

        }
    }
}

注意:
一、两边进行交互的数据都是JSON,所以在使用的时候需要转为对应的实体对象,
二、通过WKScriptMessage.body取出来的并不是我们传递的参数,而是H5Bridge.js中postMessage({data,callbackFuncName})**{data,callbackFuncName}**这个对象,所以要注意取数据时要多解开一层
三、我这里使用了decode这类扩展方法是为了把json转为对象,因为我只定义了一个方法,用来存取数据,所以我需要传参中的type字段类型来区分是:存/取

界面及方法调用

两个按钮及内容展示标签

      <div class="line">
        <p>账号:</p>
        <input type="text" v-model="userInput" placeholder="请输入用户名" />
      </div>
      <div class="line">
        <p>账号:</p>
        <input type="password" v-model="pwdInput" placeholder="请输入密码" />
      </div>
      <div class="btn-container">
        <button @click="saveToNative">保存到原生</button>
      </div>
      <div class="param-info">传递的参数:{{ JSON.stringify({user: userInput, password: pwdInput}) }}</div>
      <div class="btn-container">
        <button @click="getFromNative">从原生取数据</button>
      </div>
      <div class="param-info">回来的数据:{{ backString }}</div>
    </div>

script中方法调用及实现

const userInput = ref('')
const pwdInput = ref('')
const backString = ref('')
// 访问原生存储数据
const saveToNative = () => {
  const info = {
    user: userInput.value,
    password: pwdInput.value
  }
  if (window.AppNative) {
    asyncCallLocal("H5Native", JSON.stringify({type:0, data:info}))
  }
}
// 访问原生获取存储数据
const getFromNative = async () => {
  if (window.AppNative) {
    setTimeout(async () => {
      const backData = await syncCallLocal("H5Native", JSON.stringify({type:1, data:{}}))
      backString.value = backData
    }, 0);
  }
}

交互的核心

这个示例其实核心的就几句代码,一是桥接H5Bridge.js文件中:

window.webkit.messageHandlers.methodName.postMessage(data);

使用window.webkit.messageHandlers对象将消息发送到iOS原生代码中。
其中methodName是iOS原生代码中要执行的方法名(示例中的:H5Native),data是要传递给原生方法的数据。

二是原生调用挂载在window上的全局方法,完成回调:

window.methodName = function(data) { }

methodName就是示例中的callbackFun,data是要传递给H5的数据

完整项目示例

点我查看

<template> <list-view id="listview" style="flex: 1;background: #fff;" :refresher-enabled="false" :refresher-default-style="refresherStyle" :refresher-triggered="refresherTriggered" @scrolltolower="onScrollTolower" @scroll="scrollEvent" @touchstart="outsideTouchEvent" > <!-- 朋友圈背景图 --> <list-item class="top"> <image class="bg-image" src="/static/1.png" mode="aspectFill"></image> <view class="user-info uni-row" ref="user-info"> <text class="username">微信朋友圈</text> <image class="user-avatar" src="https://img0.baidu.com/it/u=1415523915,841919565&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto?sec=1713286800&t=a9e21a0e5650a672fa2cbd3b133ed7e0" mode="widthFix"></image> </view> </list-item> <!-- 视频朋友圈内容 --> <list-item class="item uni-row"> <image class="avatar" src="/static/1.png" mode="scaleToFill"></image> <view class="content uni-row uni-column"> <text class="name">UNIAPP X</text> <text class="text">{{ text }}</text> <!-- 图片 / 视频 --> <view class="video"> <video :controls="false" class="video" poster="/static/1.png" src="https://qiniu-web-assets.dcloud.net.cn/video/sample/2minute-demo.mp4"></video> </view> <!-- 时间/点赞/评论按钮 --> <comment-line></comment-line> <!-- 点赞/评论信息列表 --> <comment></comment> </view> </list-item> <!-- 图文朋友圈内容 --> <list-item class="item uni-row" v-for="index in item_count"> <image class="avatar" src="/static/1.png" mode="scaleToFill"></image> <view class="content uni-row uni-column"> <text class="name">UNIAPP X {{ index }}</text> <text class="text">{{ text }}</text> <!-- 图片 / 视频 --> <view class="images uni-row"> <uni-moments-image :imageList="imageList"></uni-moments-image> </view> <!-- 定位 --> <text class="address mt-5">上海市 · 静安寺</text> <!-- 时间/点赞/评论按钮 --> <comment-line></comment-line> <!-- 点赞/评论信息列表 --> <comment></comment> </view> </list-item> </list-view> <!-- 自定义导航栏 --> <moments-header ref="header" :scrollTop="scrollTop"></moments-header> <!-- 点赞/评论 --> <comment-popup ref="commentPopup"></comment-popup> <!-- 评论输入框 --> <comment-input ref="commentInput"></comment-input> </template> <script> import MomentsHeader from './components/momentsHeader.uvue'; import CommentPopup from './components/comment-popup.uvue'; import CommentLine from './components/comment-line.uvue'; import CommentInput from './components/comment-input.uvue'; import Comment from './components/comment.uvue'; import uniMomentsImage from '../../components/uni-moments-image/uni-moments-image.uvue'; export default { components: { CommentLine, Comment, MomentsHeader, CommentInput, CommentPopup, uniMomentsImage }, data() { return { item_count: 20, listViewElement: null as UniListViewElement | null, refresherTriggered: false, refresherStyle: "none", text: `uni-app x,是下一代 uni-app,是一个跨平台应用开发引擎。uts是一门类ts的、跨平台的、新语言。utsiOS端编译为swift、在Android端编译为kotlin、在Web端编译为js。`, imageList: [ "https://img1.baidu.com/it/u=1965663592,580944689&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto?sec=1713286800&t=5c73d7f8aaa2a6f5114e659dd64e769d", "https://img0.baidu.com/it/u=3838093562,4126749835&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1713286800&t=a2cd3d4ac8c0f2024a348772577a9d0f", "https://img0.baidu.com/it/u=256816879,771155532&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto?sec=1713286800&t=d5230b0cfdae75de3c452d973d302106", "https://img1.baidu.com/it/u=1965663592,580944689&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto?sec=1713286800&t=5c73d7f8aaa2a6f5114e659dd64e769d", "https://img0.baidu.com/it/u=3838093562,4126749835&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1713286800&t=a2cd3d4ac8c0f2024a348772577a9d0f", "https://img0.baidu.com/it/u=256816879,771155532&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto?sec=1713286800&t=d5230b0cfdae75de3c452d973d302106", ], scrollTop: 0, keyboardHeight: 200, // ios 默认键盘高度,这里占位 } }, onReady() { this.listViewElement = uni.getElementById<UniListViewElement>('listview'); // 监听点击评论事件 uni.$on('commentScrollEvent', (y: number) => this.listViewScrollByY(y - this.keyboardHeight + 40)); // 监听一次键盘高度回调 uni.$once('keyboardHeightEvent', (keyboardHeight: number) => { this.keyboardHeight = keyboardHeight; }); }, methods: { // 目标元素以外的touch事件 outsideTouchEvent() { // https://doc.dcloud.net.cn/uni-app-x/component/#methods (this.$refs['commentPopup'] as ComponentPublicInstance).$callMethod('hideCommentPopup'); // 隐藏点赞/评论 (this.$refs['commentInput'] as ComponentPublicInstance).$callMethod('hideKeyboard'); // 隐藏评论框键盘 }, onScrollTolower(_ : ScrollToLowerEvent) { setTimeout(() => { this.item_count += 20 }, 300) }, scrollEvent(e : UniScrollEvent) { this.scrollTop = e.detail.scrollTop; }, listViewScrollByY(y : number) { this.listViewElement?.scrollBy(0, y) } } } </script> <style lang="scss" scoped> @import './index.scss'; </style> 这是nvue 好像 帮我改为vue2 写法
07-18
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张子都

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值