uniapp 微信小程序webview 踩坑

news2024/10/7 14:29:37

uniapp


微信小程序的存在许多功能上的限制和约束,有些情况不得不去使用webview进行开发实现需求,比如

原生无法满足(例如某团队维护SDK 只提供了WEB端jsSDK,且不维护小程序SDK)
H5可以同时适用多端(适用范围更广)
H5可以弥补小程序部分欠缺
微信生态有部分限制(包大小,设计规范等)

由于最近做的需求小程序不支持播放带有透明通道的视频,所以转到了webview

这里总结下完整的开发流程和现有的各种解决方案


: 开发阶段需要将不校验合法域名勾选上


webview样式

当小程序嵌入webview之后,会自动铺满一整个页面,设置宽高无效,并覆盖其他组件

并且会带有原生的导航栏,这个导航栏是无法消除的,即使在小程序端和webview端设置navigationStyle: custom都无效

在这里插入图片描述

导航栏的标题会先加载小程序端的pages.json中对应组件的navigationBarTitleText,如果没有则读globalStyle中的值

然后标题会刷新为webview端的对应组件的navigationBarTitleText,如果没有则读globalStyle中的值

如果webview端都没有navigationBarTitleText,则只会显示小程序端的值,不会刷新

如果都没有,则显示空白,可以改变其背景颜色,仅支持十六进制颜色码



微信小程序和webview通信

  • 小程序可以通过url拼接参数的方式向webview端传参

  • webview端向小程序传参现有只能通过wx.miniProgram.postMessage进行传参,但是这个api非常有局限性
    只有在特定时机(小程序后退、组件销毁、分享)触发组件的 message 事件,所以基本没用

  • 只能通过webSocket进行长连接发送接收消息,但是会增加服务器压力

小程序向web-view传参非常轻松,反过来则非常折磨

web-view 网页与小程序之间不支持除 JSSDK 提供的api之外的通信

如果调用JSSDK的api并不能解决问题,并且不想使用websocket,最好使用小程序单一实现

在 iOS 中,若存在 JSSDK 接口调用无响应的情况,可在 web-view 的 src 后面加个#wechat_redirect解决



JSSDK

想要在webview端调用小程序的接口,都需要jssdk 官方文档

绑定域名

如果想要在webview中使用jssdk,那么需要设置JS接口安全域名

由于我们使用的是webview(视为公众号),并不是小程序(小程序只是一个承载webview的壳子)

因此是在公众号设置的功能设置中填写JS接口安全域名,webview所在的域名设置进去后才允许鉴权

注意: 设置JS接口安全域名需要公网ICP备案了的服务器,也就是说本地的环境没办法使用,必须在线的服务器环境

在这里插入图片描述
如果webview部署所在的路径为www.abc.xx/page,那么设置域名也应该为www.abc.xx/page(没有测试过,最好直接www.abc.xx能直接访问webview为佳)

业务域名js接口安全域名网页授权域名可以一起设置为相同的,都为webview部署所在的域名即可

设置的时候需要保存一个配置文件到对应的目录,下载下来放进去即可,如果设置失败大概率是设置了代理跳转了没有读到文件

nginx的设置参考 nginx配置


引入JS文件

然后就是在webview中引入jssdk的js文件

然而如果通过<script>的方式引入,会出现wx.config is not a function

这是由于uniapp本身具有wx对象,因此会跟jssdk中的wx对象冲突


这里解决方案是在分别保存两个js文件然后通过import重命名进行引入

首先打开两个js文件的地址

https://res.wx.qq.com/open/js/jweixin-1.6.0.js
https://open.work.weixin.qq.com/wwopen/js/jwxwork-1.0.0.js

然后分别保存js到本地的js文件,通过import引入,然后将jssdk的wx变量重命名为jweixin

import jweixin from './jweixin.js'
import wxx from './jwxwork-1.0.0.js'

config注入权限

如果需要调用小程序的api则必须要通过授权并且注册需要使用的接口,才能进行调用

所以首先需要config进行权限校验

wx.config({
  debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名
  jsApiList: [] // 必填,需要使用的JS接口列表
});
  • debug: 设置为true时,通过或不通过校验,调用api的结果都会进行弹窗提示,调试阶段可以先打开
  • appid : 这个appid以及后面校验需要用到的AppSecret都必须是公众号的
    如果使用了小程序的appid、AppSecret也能拿到access_token,但是后续签名会校验不通过(没有提示,非常坑)
  • timestamp、nonceStr: 这是自己定的值,随便填即可,但是后续签名生成也会用到,需要保证这两个地方用的值是一样的
  • jsApiList: 数组中填的就是后续开发需要用的api,只有在这里注册了,才能进行调用,里面可以填的值在文档下面附录2
  • signature: 重点,需要进行验证的签名,这个签名是根据一系列参数生成的

signature

需获取access_token,根据access_token去获取jsapi_ticket, 再根据jsapi_ticket去和其他参数加密获取signature

// 流程如下
const appid = "xxxx"  // 注意appid 和 secret必须是公众号的!
const secret = "xxxx"
const noncestr = "Wm3WZYTPz0wzccnW" // noncestr 和 timestamp随便写,这里用了文档的
const timestamp = 1414587457
const url = 'https://www.abc.xx:5173/' // webview所在的网址,我这里部署在5173端口 
// 因为上面js安全域名设置的www.abc.xx 所以其他端口也是没问题的
// 另外需要注意 这里填写的网址需要跟<web-view src=''>的src的值一致,如果src需要携带参数 那么需要再src中用#隔开
// 如 <web-view :src="`https://www.abc.xx:5173/#/?time=${123}`"></web-view>
// 如果没用#隔开直接`?time=${123}`, 会由于这里加密用的url和webview src的url 不一致(???) 而签名失败

// const wxApiUrl = 'https://api.weixin.qq.com/cgi-bin'
const wxApiUrl = 'https://www.abc.xx:5173/api' // 由于直接请求可能会有跨域问题, 因此还需要在nginx中设置代理(这里就省略了, 就是设置proxy_pass)

const encodeUTF8 = (s) => {
  var i, r = [], c, x;
  for (i = 0; i < s.length; i++)
    if ((c = s.charCodeAt(i)) < 0x80) r.push(c);
    else if (c < 0x800) r.push(0xC0 + (c >> 6 & 0x1F), 0x80 + (c & 0x3F));
    else {
      if ((x = c ^ 0xD800) >> 10 == 0) //对四字节UTF-16转换为Unicode
        c = (x << 10) + (s.charCodeAt(++i) ^ 0xDC00) + 0x10000,
          r.push(0xF0 + (c >> 18 & 0x7), 0x80 + (c >> 12 & 0x3F));
      else r.push(0xE0 + (c >> 12 & 0xF));
      r.push(0x80 + (c >> 6 & 0x3F), 0x80 + (c & 0x3F));
    };
  return r;
}

// 字符串加密成 hex 字符串
const sha1 = (s) => {
  var data = new Uint8Array(encodeUTF8(s))
  var i, j, t;
  var l = ((data.length + 8) >>> 6 << 4) + 16, s = new Uint8Array(l << 2);
  s.set(new Uint8Array(data.buffer)), s = new Uint32Array(s.buffer);
  for (t = new DataView(s.buffer), i = 0; i < l; i++)s[i] = t.getUint32(i << 2);
  s[data.length >> 2] |= 0x80 << (24 - (data.length & 3) * 8);
  s[l - 1] = data.length << 3;
  var w = [], f = [
    function () { return m[1] & m[2] | ~m[1] & m[3]; },
    function () { return m[1] ^ m[2] ^ m[3]; },
    function () { return m[1] & m[2] | m[1] & m[3] | m[2] & m[3]; },
    function () { return m[1] ^ m[2] ^ m[3]; }
  ], rol = function (n, c) { return n << c | n >>> (32 - c); },
    k = [1518500249, 1859775393, -1894007588, -899497514],
    m = [1732584193, -271733879, null, null, -1009589776];
  m[2] = ~m[0], m[3] = ~m[1];
  for (i = 0; i < s.length; i += 16) {
    var o = m.slice(0);
    for (j = 0; j < 80; j++)
      w[j] = j < 16 ? s[i + j] : rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1),
        t = rol(m[0], 5) + f[j / 20 | 0]() + m[4] + w[j] + k[j / 20 | 0] | 0,
        m[1] = rol(m[1], 30), m.pop(), m.unshift(t);
    for (j = 0; j < 5; j++)m[j] = m[j] + o[j] | 0;
  };
  t = new DataView(new Uint32Array(m).buffer);
  for (var i = 0; i < 5; i++)m[i] = t.getUint32(i << 2);

  var hex = Array.prototype.map.call(new Uint8Array(new Uint32Array(m).buffer), function (e) {
    return (e < 16 ? "0" : "") + e.toString(16);
  }).join("");
  return hex;
}

class useStorage {
    /**
     * 额外设置一条 `key__expires__: 时间戳` 的storage来判断过期时间
     * @param {string} key
     * @param {any} value
     * @param {number} expired 过期时间 以分钟为单位
     * @returns {any}
     */
    
    setItem(key, value, expired) {
        uni.setStorageSync(key, JSON.stringify(value))
        if (expired) {
            uni.setStorageSync(`${key}__expires__`, Date.now() + 1000 * 60 * expired)
        }
        return value;
    }

    /**
     * 获取storage时先获取`key__expires__`的值判断时间是否过期 
     * 过期则清空该两条storage 返回空
     * @param {string} key
     * @returns {any}
     */
    getItem(key) {
        let expired = uni.getStorageSync(`${key}__expires__`) || Date.now + 1;
        const now = Date.now();

        if (now >= expired) {
            uni.removeStorageSync(key)
            uni.removeStorageSync(`${key}__expires__`)
            return;
        }
        return uni.getStorageSync(key) ? JSON.parse(uni.getStorageSync(key)) : uni.getStorageSync(key);
    }
}

const storage = new useStorage();

const fetchUrl = (url) => {
    return fetch(url).then((response) => {
        return response.json()
    })
}

// 主要逻辑  为了便于理解把工具函数全部整合到一块代码了 实际上可以把工具函数进行抽离封装
const fetToken = async () => {
    let save_ticket = storage.getItem('TICKET')
    console.log('ticket', save_ticket)
    if(!save_ticket){
        try{
            const {access_token: token} = await fetchUrl(`${wxApiUrl}/token?grant_type=client_credential&appid=${appid}&secret=${secret}`)
            const {ticket} = await fetchUrl(`${wxApiUrl}/ticket/getticket?access_token=${token}&type=jsapi`)
            save_ticket = ticket
            storage.setItem('TICKET', ticket, 100)
        }catch{
            uni.showToast({
                title: '授权异常',
                duration: 1000
            })
        }
    }
    const result = `jsapi_ticket=${save_ticket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`
    console.log('result', result)
    return sha1(result)
}

得到签名后可以在 微信校验网址 中对比自己生成的签名有没有问题

实际上校验加密这个步骤应该放在服务端,这里为了方便就放在了前端来实现

 onLoad() {
    // 获取ticket并注册jsApi
    fetToken().then((res) => {
        if (jweixin) {
            jweixin.config({
                debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                appId: appid, // 必填,公众号的唯一标识
                timestamp: timestamp, // 必填,生成签名的时间戳
                nonceStr: noncestr, // 必填,生成签名的随机串
                signature: res, // 必填,签名
                jsApiList: ['startRecord', 'stopRecord', 'uploadVoice', 'downloadVoice'] // 必填,需要使用的JS接口列表
            });
        }
    })
},

进行到这一步权限如果获取成功基本就完成配置了,如果微信弹出config: ok就是成功了

小程序和小程序的公众号网页调试工具都是可以查看到config的校验结果的

小程序

在这里插入图片描述
在这里插入图片描述

公众号

在这里插入图片描述
输入网址后跳转

在这里插入图片描述

然后使用jweixin.startRecord调用api即可,如果配置失败则检查哪个字段有错误


jssdk比较脆弱, 且很多方法都是异步的,需要控制调用频率不能过快

比如startRecordstopRecord的调用,如果同时触发是可能 stopRecord先执行完的,导致回调不触发

具体详情可以看这篇博客,这里不过多赘述




其他问题

webview中长按图片会弹出系统菜单

这个会影响uniapp的touchstarttouchend事件,也不太美观

解决方案是在img标签全部添加上csspointer-events:none即可


webview中手指滑动判断方向

需求是手指往左划会弹出菜单,往右则收回菜单,但是uniapp只支持判断滑动和停止滑动,因此需要我们加个判断

<view @touchstart="touchMoveStart" @touchmove="chatExpandMove" @touchend="chatMoveEnd"></view>
export default {
        data() {
            return {
				touchingFlag: false, // 是否正在滑动
	            moveingPosition: { // 正在滑动的手指位置
	                X: 0,
	                Y: 0
	            },
	            startPosition: { // 滑动开始时点击的位置
	                X: 0,
	                Y: 0
	            },
	            endPosition: { // 滑动结束时手指的位置
	                X: 0,
	                Y: 0
	            },
	            Xflag: false, // 是否在x轴滑动
	            Yflag: false, // 是否在y轴滑动
	            startMoveTime: 0, // 开始滑动的时间 用于判断手指滑动的时间 过短则不操作
			}
		},
		methods: {
			touchMoveStart() {
			    this.startMoveTime = Date.now()
			    this.startPosition.X = event.changedTouches[0].clientX
			    this.startPosition.Y = event.changedTouches[0].clientY
			},
			/**
			 * 通过当前手指的位置和开始滑动时点击的位置来判断手指滑动的方向
			 */
			chatExpandMove() {
			    this.touchingFlag = true //移动进行
			
			    this.moveingPosition.X = event.changedTouches[0].clientX
			    this.moveingPosition.Y = event.changedTouches[0].clientY
			
			    let Xlength = parseInt(Math.abs(this.moveingPosition.X - this.startPosition.X))
			    let Ylength = parseInt(Math.abs(this.moveingPosition.Y - this.startPosition.Y))
			
			    if (Xlength > Ylength && Xlength > 10 && this.Yflag == false) { //x轴方向
			        this.Xflag = true
			        let direction = this.moveingPosition.X - this.startPosition.X > 0 ? "right" : "left"
			
			        if (direction === 'right') {
			            // 右
			        } else if (direction === 'left') {
			            // 左
			        }
			    }
			    if (Xlength < Ylength && Ylength > 10 && this.Xflag == false) { //Y轴方向
			        this.Yflag = true
			        let direction = this.moveingPosition.Y - this.startPosition.Y > 0 ? "down" : "up"
			        
			        if (direction === "up") {
			            // 上
			        } else if (direction === 'down') {
			            // 下
			        }
			    }
			},
			/**
			 * 滑动结束时判断手指滑动的距离 如果时间过短或距离过短 则取消影响
			 */
			chatMoveEnd() {
			    //关闭手指移动的标识
			    this.touchingFlag = false
			    const endTime = Date.now()
			    if (endTime - this.startTime < 300) {
			        // 如果手指滑动的距离超过0.3s 就默认不合法
			        return;
			    }
			    //获取滑动结束后的坐标
			    this.endPosition.X = event.changedTouches[0].clientX
			    this.endPosition.Y = event.changedTouches[0].clientY
			    if (Math.abs(this.endPosition.X - this.startPosition.X) > 10 && this.Xflag) { //大于10个单位才有效
			        //long的滑动长度绝对值作为视频s的值。
			        let long = Math.abs(this.endPosition.X - this.startPosition.X)
			        let height = Math.abs(this.endPosition.Y - this.startPosition.Y)
			        //left向前 right向后 
			        let elePosition = this.endPosition.X - this.startPosition.X > 0 ? "right" : "left"

					// 结束移动时与开始时的 距离 和 高度 
			    }
			    //复原互斥开关
			    this.Xflag = false
			    this.Yflag = false
			},
		}
}

webview上点击获得视频地址并播放

onload时获取到video对象,获取到地址时调用play即可

这个方法并不是100%有效,当我将video所在的组件迁移到一个子组件中时就失效了,具体原因未知

在页面初始化时直接播放视频仍然无法生效,会报错(DOMException: play() failed because the user didn't interact with the document first)

<video :src="videoPath" autoplay="true" :controls='false'  id='myVideo' @ended='videoEnd'></video>
onLoad() {
     // 获取video对象 存储起来 在ios中通过这种方法自动播放 
     // 这个wx不是jweixin 时uniapp中的wx
     if (wx) this.videoContext = wx.createVideoContext('myVideo', this);
}

// 点击播放
this.videoContext && this.videoContext.play()

webview上播放视频

在webview中时无法实现v-show控制视频播放一段时间后再显示的,如果视频处于隐藏状态,那么视频将不会播放

v-show切换为true时也不会在显示时开始播放,会一直处于暂停状态

如果需要video播放一段时间后显示,只能使用css的opacity 为0或1控制其显示或隐藏




总而言之,在uniapp上的开发体验非常糟糕,社区解决问题的效率堪忧,网上的资料也非常零散,浪费了非常多的时间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/953802.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

需求分析入门

认识管理软件 什么是管理软件 管理软件就是用来辅助企业进行管理的软件&#xff0c;既包括对企业“人、财、物”相关的资产信息的管理&#xff0c;也包括对企业“供、产、销”相关的业务活动信息的管理。管理软件的重点在于管理信息的收集、流转&#xff0c;资源的共享、集成…

【Redis】Redis 的学习教程(六)Redis 的缓存问题

在服务端中&#xff0c;数据库通常是业务上的瓶颈&#xff0c;为了提高并发量和响应速度&#xff0c;我们通常会采用 Redis 来作为缓存&#xff0c;让尽量多的数据走 Redis 查询&#xff0c;不直接访问数据库。 同时 Redis 在使用过程中&#xff08;高并发场景下&#xff09;也…

JavaScript中详解数组的算法

在 JavaScript 中&#xff0c;数组是一种常见的数据结构&#xff0c;它可以存储多个元素&#xff0c;并且可以通过索引来访问和修改这些元素。数组算法是对数组进行各种操作和处理的方法和技巧。下面是一些常见的数组算法&#xff1a; 遍历数组&#xff1a;可以使用 for 循环、…

谁爱待在Android谁待,再也卷不动了

在当前经济环境下&#xff0c;Android开发行业确实面临着竞争激烈、岗位减少的困境。因此&#xff0c;寻求具有更多岗位和良好市场前景的开发方向变得尤为重要。在此背景下&#xff0c;音视频开发和车载开发无疑是两个值得关注的领域。 音视频开发的前景 互联网和智能手机的普…

每日一题——旋转图像

旋转图像 题目链接 方法一&#xff1a;利用辅助数组 通过对示例的观察和分析&#xff0c;我们可以得到这样的结论&#xff1a; 对于原数组的下标为i行元素&#xff0c;顺时针旋转九十度后&#xff0c;都变成了下标为&#xff08;n-1-i&#xff09;列元素。如图所示&#xff…

代理模式 静态代理和动态代理(jdk、cglib)——Java入职第十一天

一、代理模式 一个类代表另一个类去完成扩展功能,在主体类的基础上,新增一个代理类,扩展主体类功能,不影响主体,完成额外功能。比如买车票,可以去代理点买,不用去火车站,主要包括静态代理和动态代理两种模式。 代理类中包含了主体类 二、静态代理 无法根据业务扩展,…

蜜桃星球 | 主理人,轻创业翻身副业,情趣赛道行业陪跑服务

我们为什么能在年纪轻轻的时候赚到钱&#xff1f; 一个重要原因就是&#xff0c;接触互联网后&#xff0c;我们所进入的所有行业&#xff0c;都是轻资产领域。 从流量到运营&#xff0c;所有的行业都是轻资产行业&#xff0c;都是不需要囤货的生意&#xff0c;只需要一根网线…

代替forever下一个部署node的持久化工具---pm2

最近有个后端项目&#xff0c;用的是node&#xff0c;在持久化的时候会挂掉&#xff0c;详细了解到用的是nohup&#xff0c;然后先详细了解了一下nohup nohup是一个Linux命令&#xff0c;用于在系统后台不挂断地运行命令&#xff0c;退出终端不会影响程序的运行1nohup的英文全称…

react学习之路:InputNumber的parser在ts里面报类型错误

错误提示&#xff1a; Type ‘(value: string | undefined) > string’ is not assignable to type ‘(displayValue: string | undefined) > 0 | 2 | 20’. Type ‘string’ is not assignable to type ‘0 | 2 | 20’. 代码示例&#xff1a; <InputNumbermin{0}m…

电视盒子哪款好?数码党私藏网络电视盒子排行榜

电视盒子称得上是家家户户必备了&#xff0c;但是不同品牌和不同产品之间的体验差异较大&#xff0c;让大家在挑选电视盒子时都会纠结电视盒子哪款好&#xff0c;我身为资深数码粉&#xff0c;接下来将给各位分享数码粉心中最值得入手的网络电视盒子排行榜&#xff0c;看看电视…

关于xml中返回string类型代码中用list接收的问题,扫描

1.结论,xml中返回为string的话,在list中只会取出来第一个元素 //根据value查询GetMapping("getTest")public List<HashMap> getTest() {List<HashMap> list dictService.getTest();return list;} <select id"getTest" resultType"jav…

伦敦银交易时间怎么选择?

伦敦银和伦敦金都是全球性的交易品种&#xff0c;一般的现货贵金属交易平台&#xff0c;都可以同时经营这两个品种&#xff0c;而且它们的交易时间是一致的&#xff0c;以香港市场的平台为例&#xff0c;基本上交易时间都会从北京周一的早上7点&#xff0c;延续到周六凌晨5点左…

Shell脚本进阶:提升你的自动化脚本编程技巧

摘要&#xff1a;本文将介绍一些Shell脚本进阶技巧&#xff0c;帮助你提高自动化脚本编程的效率和可靠性。我们将涵盖一些常用的Shell脚本编程技巧&#xff0c;并提供相关的代码示例&#xff0c;以便读者更好地理解和应用这些技巧。 1. 函数的使用 Shell脚本中的函数可以帮助我…

【Day-24慢就是快】代码随想录-二叉树-二叉树的层序遍历

给你一个二叉树&#xff0c;请你返回其按 层序遍历 得到的节点值。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 ———————————————————————————————————————— 借助辅助队列来实现层序遍历。也就是图论中的广…

AI助乡行——点燃乡村振兴新引擎

随着数字化浪潮的袭来&#xff0c;乡村振兴战略的推进离不开数字化、智慧化等现代化治理能力和方式&#xff0c;人工智能等高新技术正不断与农村经济、社会、治理等加速融合。在智慧农业的背景下&#xff0c;我们可以解决一系列困扰农民的问题&#xff0c;包括如何增加经济作物…

vue去掉循环数组中的最后一组的某个样式style/class

vue去掉循环数组中的最后一组的某个样式style/class 需求&#xff1a;要实现这样的排列 现状 发现&#xff0c;最后一个格子并没有跟下面绿色线对齐。 最后发现 是因为 每个格子都给了 margin-right&#xff1a;36px&#xff0c;影响到了最后一个格子 所以要 将最后一个格子的…

安装并使用srs直播

一、安装srs sudo docker run -d -p 1935:1935 -p 1985:1985 -p 8080:8080 --name srs registry.cn-hangzhou.aliyuncs.com/ossrs/srs:v4.0.34二、vue展示 1、引入库 npm install --save flv.js2、导包 import flvjs from "flv.js";3、完整案例 <template><…

Python中的迭代器和生成器介绍

一、迭代器&#xff08;Iterators&#xff09; 迭代器是Python中用于遍历数据集合的一种机制。它是一个实现了迭代协议的对象&#xff0c;可以通过iter()函数来获得迭代器。迭代器需要实现两个方法&#xff1a;__iter__()和__next__()。其中&#xff0c;__iter__()返回迭代器自…

NI RF 无线设计与测试产品 ,你所需要了解的一切

无线设计与测试 随着无线通信的界限不断突破&#xff0c;NI专门针对快速原型验证和生产测试提供了各种软件无线电设备、发生器、分析仪和收发仪。 矢量信号收发仪 VSTRF信号发生器软件无线电 USRP网络分析仪频谱和信号分析仪RF和微波开关功率传感器RF信号调理 矢量信号收发仪…

大数据精准营销怎么满足用户的个性化需求?

近年来在AI和媒体的带动下&#xff0c;大数据分析不断介入&#xff0c;各行各业都开始陆续依仗大数据营销这棵大树&#xff0c;以此来更加高效、便捷、智能、精准的服务于用户。 这就像追求恋人一样&#xff0c;投其所好方能成为眷属。 大数据精准营销的好处&#xff1a; 相…