HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

用 Agent 开发 uni-app,怎样让任务少停在半路?

技术分享

作者:XylemNode 团队

用 Agent 开发 uni-app,方便的地方在于:它可以读取现有项目,找到需要修改的文件,完成改动,再根据检查结果继续调整。

你交给它的是一项开发任务,中间的读文件、改代码、看报错,可以由具备相应工具和权限的 Agent 连着处理。

但开始这样用以后,模型接入这一步就值得多花点心思。一个任务可能要经过多轮模型调用,期间还会执行本地操作。如果中途停下来,开发者需要知道:是模型请求没完成,还是构建报错?文件已经改到哪一步?接下来能不能继续?

下面就从 Agent 接入模型服务说起,聊聊用它开发 uni-app 时,哪些地方提前处理好,能少一些来回折腾。

模型服务接在 Agent 这一侧

先把关系说明白:开发者在编程 Agent 里配置模型服务,Agent 调用模型分析任务,再通过自己的工具读取、修改 uni-app 项目。

可以把它理解成:

开发者提出任务 → Agent 调用模型 → Agent 执行项目操作 → 把执行结果交给模型继续处理

以接入 XylemNode 为例,模型接口配置在支持自定义服务的 Agent 中,通常涉及接口地址、API Key 和模型名称,具体填写方式取决于该工具支持的协议。

这些是开发工具的配置,不需要为了使用 Agent,把模型服务的密钥写进 uni-app 的页面代码。

接好以后,建议先跑一个范围很小、但步骤完整的任务:读取相关文件,完成一处修改,再执行项目已有的检查。重点看 Agent 能否正确发起工具调用、收到执行结果,并据此继续处理。

因为在这种工作方式里,模型和工具之间会有往返:模型提出操作,客户端执行,再把结果传回。Anthropic 的工具调用文档介绍了这一过程。一次普通对话成功,还没有覆盖这些环节。

动手之前,先让 Agent 看懂你的 uni-app 项目

接入没问题,也不代表 Agent 已经了解项目约定。

目标是 H5、App 还是微信小程序?使用哪个版本的 Vue?项目通过 HBuilderX 管理,还是已经有命令行构建流程?现有组件和目录应该怎么沿用?

这些信息会影响它怎么改代码、怎么验证。让它先读项目配置和已有实现,可以减少凭空补一套结构、引入不必要依赖的情况。

开始任务时,可以附上几句要求:

> 先检查项目结构、依赖和目标平台,再开始修改。沿用现有组件和代码风格,新增依赖前说明原因。修改后执行项目已有的检查,并列出实际执行的命令与结果。需要在 HBuilderX、模拟器或真机上验证的步骤,单独列出来。

这里不适合给所有项目套一条固定的构建命令。Agent 能运行哪些检查,要看项目本身的配置和它具备的工具。没有执行的步骤,就保留为待验证项。

任务停下来,先分清是哪一段在等

一个修改任务可能经过这样的过程:读页面和组件、分析依赖关系、修改文件、执行检查,再根据报错继续修正。

中间既有模型请求,也有本地工具执行。看到 Agent 暂时没动静,先看它当前停在哪一步。

如果面板显示模型调用超时,就保留错误详情和请求编号,检查调用状态。如果已经拿到构建报错,就先围绕报错处理。如果工具正在等待权限或人工操作,则需要完成对应动作。

分清这些情况,才能避免没有必要的操作。构建失败时反复换接口,解决不了代码或环境问题;模型请求中断时,也不能据此认定刚才的文件修改没有执行。

尤其是任务已经改过文件的情况,继续之前先看文件差异和执行记录。如果修改已经完成,只剩检查没有跑完,就先确认能否从检查环节接着处理。

通道选择这类工作,可以少让人手动处理

对于模型调用本身,还有一部分恢复工作可以交给网关。

当某条调用路径暂时异常,而系统中存在其他符合请求要求的可用路径时,可以由网关按规则选择路径、尝试恢复,减少开发者手动切换配置的次数。

以我们 XylemNode 的 Auto 分组为例,它结合通道健康状态按策略选路,遇到通道异常时支持跨组重试,调用方继续使用原来的模型名。具体说明可以看官网的 Auto 分组介绍。

放到 Agent 开发流程里,这项能力承担的是模型请求的通道选择。开发者仍然决定任务使用哪个模型,也仍需由 Agent 管理工具执行和项目进度。

因此,通道恢复不能直接等同于整个开发任务恢复。已经写入的代码、运行过的命令,都需要结合实际状态继续处理;流式响应已经返回一部分时,也不能把重新请求理解成自动无缝续写。

接入时还应了解 Agent 和网关各自的重试规则,给尝试次数和总等待时间留出明确边界。这样,自动处理才不会变成一直等待,却不知道任务有没有进展。

最后看交付结果,别停在一句“已经完成”

用 Agent 开发 uni-app,最后还是要回到项目本身:改了哪些文件,修改是否符合要求,执行过哪些检查,目标平台上还有哪些内容需要验证。

可以要求 Agent 在结束时给出一份简短交代:

  • 本次修改涉及哪些文件,分别解决了什么问题;
  • 实际运行了哪些检查,结果是什么;
  • 还有哪些页面表现或平台兼容性需要人工验证。

命令行检查通过,就记录这项结果。页面有没有显示异常、交互是否符合预期,以及目标设备上的表现,还要通过相应环境确认。

项目上下文交代清楚,Agent 才更容易按现有约定做事;模型接入和恢复规则理顺,才能减少一部分开发过程中的打断;检查结果留下来,下次继续任务也更有依据。

对开发者来说,省心的体验很具体:少一些来回改配置,任务中断后能看清进度,结束时拿得到可以检查的结果。


本文由 XylemNode 团队整理,AI 辅助起草,技术依据见文中链接。

继续阅读 »

作者:XylemNode 团队

用 Agent 开发 uni-app,方便的地方在于:它可以读取现有项目,找到需要修改的文件,完成改动,再根据检查结果继续调整。

你交给它的是一项开发任务,中间的读文件、改代码、看报错,可以由具备相应工具和权限的 Agent 连着处理。

但开始这样用以后,模型接入这一步就值得多花点心思。一个任务可能要经过多轮模型调用,期间还会执行本地操作。如果中途停下来,开发者需要知道:是模型请求没完成,还是构建报错?文件已经改到哪一步?接下来能不能继续?

下面就从 Agent 接入模型服务说起,聊聊用它开发 uni-app 时,哪些地方提前处理好,能少一些来回折腾。

模型服务接在 Agent 这一侧

先把关系说明白:开发者在编程 Agent 里配置模型服务,Agent 调用模型分析任务,再通过自己的工具读取、修改 uni-app 项目。

可以把它理解成:

开发者提出任务 → Agent 调用模型 → Agent 执行项目操作 → 把执行结果交给模型继续处理

以接入 XylemNode 为例,模型接口配置在支持自定义服务的 Agent 中,通常涉及接口地址、API Key 和模型名称,具体填写方式取决于该工具支持的协议。

这些是开发工具的配置,不需要为了使用 Agent,把模型服务的密钥写进 uni-app 的页面代码。

接好以后,建议先跑一个范围很小、但步骤完整的任务:读取相关文件,完成一处修改,再执行项目已有的检查。重点看 Agent 能否正确发起工具调用、收到执行结果,并据此继续处理。

因为在这种工作方式里,模型和工具之间会有往返:模型提出操作,客户端执行,再把结果传回。Anthropic 的工具调用文档介绍了这一过程。一次普通对话成功,还没有覆盖这些环节。

动手之前,先让 Agent 看懂你的 uni-app 项目

接入没问题,也不代表 Agent 已经了解项目约定。

目标是 H5、App 还是微信小程序?使用哪个版本的 Vue?项目通过 HBuilderX 管理,还是已经有命令行构建流程?现有组件和目录应该怎么沿用?

这些信息会影响它怎么改代码、怎么验证。让它先读项目配置和已有实现,可以减少凭空补一套结构、引入不必要依赖的情况。

开始任务时,可以附上几句要求:

> 先检查项目结构、依赖和目标平台,再开始修改。沿用现有组件和代码风格,新增依赖前说明原因。修改后执行项目已有的检查,并列出实际执行的命令与结果。需要在 HBuilderX、模拟器或真机上验证的步骤,单独列出来。

这里不适合给所有项目套一条固定的构建命令。Agent 能运行哪些检查,要看项目本身的配置和它具备的工具。没有执行的步骤,就保留为待验证项。

任务停下来,先分清是哪一段在等

一个修改任务可能经过这样的过程:读页面和组件、分析依赖关系、修改文件、执行检查,再根据报错继续修正。

中间既有模型请求,也有本地工具执行。看到 Agent 暂时没动静,先看它当前停在哪一步。

如果面板显示模型调用超时,就保留错误详情和请求编号,检查调用状态。如果已经拿到构建报错,就先围绕报错处理。如果工具正在等待权限或人工操作,则需要完成对应动作。

分清这些情况,才能避免没有必要的操作。构建失败时反复换接口,解决不了代码或环境问题;模型请求中断时,也不能据此认定刚才的文件修改没有执行。

尤其是任务已经改过文件的情况,继续之前先看文件差异和执行记录。如果修改已经完成,只剩检查没有跑完,就先确认能否从检查环节接着处理。

通道选择这类工作,可以少让人手动处理

对于模型调用本身,还有一部分恢复工作可以交给网关。

当某条调用路径暂时异常,而系统中存在其他符合请求要求的可用路径时,可以由网关按规则选择路径、尝试恢复,减少开发者手动切换配置的次数。

以我们 XylemNode 的 Auto 分组为例,它结合通道健康状态按策略选路,遇到通道异常时支持跨组重试,调用方继续使用原来的模型名。具体说明可以看官网的 Auto 分组介绍。

放到 Agent 开发流程里,这项能力承担的是模型请求的通道选择。开发者仍然决定任务使用哪个模型,也仍需由 Agent 管理工具执行和项目进度。

因此,通道恢复不能直接等同于整个开发任务恢复。已经写入的代码、运行过的命令,都需要结合实际状态继续处理;流式响应已经返回一部分时,也不能把重新请求理解成自动无缝续写。

接入时还应了解 Agent 和网关各自的重试规则,给尝试次数和总等待时间留出明确边界。这样,自动处理才不会变成一直等待,却不知道任务有没有进展。

最后看交付结果,别停在一句“已经完成”

用 Agent 开发 uni-app,最后还是要回到项目本身:改了哪些文件,修改是否符合要求,执行过哪些检查,目标平台上还有哪些内容需要验证。

可以要求 Agent 在结束时给出一份简短交代:

  • 本次修改涉及哪些文件,分别解决了什么问题;
  • 实际运行了哪些检查,结果是什么;
  • 还有哪些页面表现或平台兼容性需要人工验证。

命令行检查通过,就记录这项结果。页面有没有显示异常、交互是否符合预期,以及目标设备上的表现,还要通过相应环境确认。

项目上下文交代清楚,Agent 才更容易按现有约定做事;模型接入和恢复规则理顺,才能减少一部分开发过程中的打断;检查结果留下来,下次继续任务也更有依据。

对开发者来说,省心的体验很具体:少一些来回改配置,任务中断后能看清进度,结束时拿得到可以检查的结果。


本文由 XylemNode 团队整理,AI 辅助起草,技术依据见文中链接。

收起阅读 »

HAR 文件怎么打开?别人发来的抓包文件怎么接着分析

iOS

'''同事发来一个文件:接口报错.har,问"你帮我看看这个请求为啥 500"。文件收到了,双击却不知道用什么打开——浏览器拖进去没反应,记事本打开是一大坨 JSON,翻半天才找到那条出错的请求。这类场景在联调里出现得不少:抓的人有抓的工具,收的人拿到 HAR 文件或 pcap 文件,第一步就卡在"用什么打开"。

要解决的不只是"打开"。文件打开之后,怎么看懂内容、怎么和手头的成功请求做对比、怎么再交给下一个环节,是一条完整的链。下面按"拿到文件之后"的顺序,把这条链走一遍。

先认清手上的是什么文件

HAR 是已经解析好的请求清单。 全称 HTTP Archive,本质是一个 JSON 文件——浏览器 F12 的 Network 面板右键导出,Fiddler 也能导,每个请求的方法、URL、请求头、响应头、耗时都按结构化字段存在里面。它记录的是应用层的事:这个请求发了什么、回了什么。

HAR 的骨架长这样:

{  
  "log": {  
    "version": "1.2",  
    "entries": [  
      {  
        "request":  { "method": "POST", "url": "https://api.example.com/order", "headers": [] },  
        "response": { "status": 500, "content": { "text": "...", "encoding": "base64" } }  
      }  
    ]  
  }  
}

注意 response.content 里的字段——encoding: base64 表示这段内容是二进制或压缩数据,原样看是乱的,得先解码。这个坑后面还会碰到。

pcap / pcapng 是网卡上的原始数据包。 Wireshark、tcpdump 抓完存的就是它,二进制格式,记录的是从网卡视角看到的原始包——一个 HTTP 请求在它眼里是若干 TCP 段,想看懂得先按连接把包重组起来。所以 pcap 的"打开"和 HAR 完全不是一回事:HAR 打开是看请求,pcap 打开先要解决"哪些包属于同一条连接、这条连接是不是 HTTPS 密文"。

还有一类是各家工具的私有会话格式:Charles 的 .chls、Fiddler 的 .saz,只能用原工具打开。给别人传文件时,导出成 HAR 比传私有格式通用。

把文件导进来

拿到文件,打开方式无非两条:找对应格式的工具,或者找一个都能吃下的。

分头解决的路线是:HAR 拖进 Chrome DevTools 的 Network 面板能看,pcap 交给 Wireshark,私有格式回原工具。这套分工能用,但文件一多就散——同事可能发来 HAR,也可能发来 Wireshark 存的 pcapng,得先判断"这归谁管"。

另一条路是用 TraceEagle 的"导入会话",HAR 1.2、.pcap / .pcapng、常见抓包工具导出的会话文件都从这一个入口进来。导入后请求就躺在列表里,跟本机抓的没有区别:切视图、看解码、查主机档案、拿去对比,都在同一套界面里。上次看同事给的 HAR 文件就是这么处理的,不用先想"该装什么软件",导进来直接看。

打开之后:三步读明白

导入只是开始,真正要回答的是"这个请求为什么出错"。

看内容。 先把乱码清零。HAR 里的 body 常是 base64 或压缩过的(开头那个 encoding: base64 就是),pcap 里非 HTTP 的流量本身是一堆原始字节。自动解压、自动识别格式、protobuf 免 .proto 解析、私有帧自动拆这一套在这里直接复用——导入的会话和本机抓的走同一套解码,不用因为"文件是别人抓的"就换流程。

查来源。 分析别人的包,经常要判断"这个域名、这个 IP 是什么来头"。HAR 里每个请求的主机名、pcap 里每个对端 IP,点开都有主机档案:归属地、ASN、证书信息、技术栈。查一个陌生 IP 的归属,不用对着地址段猜。

做对比。 这是导入场景里最实用的一步:同事抓的失败请求和你抓的成功请求,可以跨会话放在一起逐行比——参数、请求头、响应体哪里不一样。比"你把请求截图发我看看"来回几个回合高效得多,接口偶发失败这类问题,两份记录摆一起经常一眼出答案。

和常见工具怎么分工

这几个工具各管一段,位置挺清楚。

Chrome DevTools 看自己刚导出的 HAR 顺手,即开即用;但它开不了 pcap,也没法把两份 HAR 放一起比对。Wireshark 是 pcap 的专业户,包级分析(重传、时延、连接状态)没有替代品,代价是看 HTTP 内容要自己 follow stream,遇到 HTTPS 还得先有解密密钥。Charles、Fiddler 开自家会话最舒服,能改能重放,跨格式就弱一些。

TraceEagle 的位置在中间:HAR 和 pcap 都能进,进来之后解码、档案、对比、再导出一路通。实际用法多是组合——Wireshark 看完包级异常,把 pcap 导进来解出 HTTP 明文;或者同事用 Charles 抓的包导出 HAR,导进来和本地这份做对比。

问得最多的几件事

Chrome 导出的 HAR 怎么没有响应内容? 导出时选项没带对——浏览器的导出项里有"带内容"和"不带内容"两种,只有带内容的那份包含响应体。给别人导 HAR 文件排查问题时,记得选带内容的那种。

Wireshark 抓的 pcapng 导进来是明文还是密文? 看文件里有没有密钥。pcapng 可以把 TLS 解密密钥(SSLKEYLOGFILE 那套)一起写进文件,带了就能解出明文,没带就是原始密文,只能看包的特征。请对方导文件时,有条件的话顺手把密钥带上。

HAR 和 pcap 能互相转换吗? 不能直接转。HAR 是应用层的请求清单,pcap 是网卡层的原始包,信息不在一个层面——从 pcap 出 HAR 要先解密再完整重组,HAR 倒回 pcap 缺的是全部底层细节。两种格式都收的工具,内部是分别解析,不是转换。

同事发来的私有格式打不开怎么办? 请对方在原工具里导一份 HAR。HAR 1.2 是通用交换格式,浏览器和主流代理工具都认——你要把包发出去时,也可以照这个思路来。

收个尾

拿到抓包文件的分析路径可以固定下来:先看扩展名——.har 是应用层清单,找个能看 JSON 会话的工具或直接导进来;.pcap / .pcapng 是原始包,交给能重组连接的工具;私有格式请对方转 HAR。然后看、比、导:解码看清内容,对比找出差异,再导出交给下一个环节。

下次再收到同事发来的抓包文件,从"是 HAR 还是 pcap"判断起,后面的路就是一条直线。'''

继续阅读 »

'''同事发来一个文件:接口报错.har,问"你帮我看看这个请求为啥 500"。文件收到了,双击却不知道用什么打开——浏览器拖进去没反应,记事本打开是一大坨 JSON,翻半天才找到那条出错的请求。这类场景在联调里出现得不少:抓的人有抓的工具,收的人拿到 HAR 文件或 pcap 文件,第一步就卡在"用什么打开"。

要解决的不只是"打开"。文件打开之后,怎么看懂内容、怎么和手头的成功请求做对比、怎么再交给下一个环节,是一条完整的链。下面按"拿到文件之后"的顺序,把这条链走一遍。

先认清手上的是什么文件

HAR 是已经解析好的请求清单。 全称 HTTP Archive,本质是一个 JSON 文件——浏览器 F12 的 Network 面板右键导出,Fiddler 也能导,每个请求的方法、URL、请求头、响应头、耗时都按结构化字段存在里面。它记录的是应用层的事:这个请求发了什么、回了什么。

HAR 的骨架长这样:

{  
  "log": {  
    "version": "1.2",  
    "entries": [  
      {  
        "request":  { "method": "POST", "url": "https://api.example.com/order", "headers": [] },  
        "response": { "status": 500, "content": { "text": "...", "encoding": "base64" } }  
      }  
    ]  
  }  
}

注意 response.content 里的字段——encoding: base64 表示这段内容是二进制或压缩数据,原样看是乱的,得先解码。这个坑后面还会碰到。

pcap / pcapng 是网卡上的原始数据包。 Wireshark、tcpdump 抓完存的就是它,二进制格式,记录的是从网卡视角看到的原始包——一个 HTTP 请求在它眼里是若干 TCP 段,想看懂得先按连接把包重组起来。所以 pcap 的"打开"和 HAR 完全不是一回事:HAR 打开是看请求,pcap 打开先要解决"哪些包属于同一条连接、这条连接是不是 HTTPS 密文"。

还有一类是各家工具的私有会话格式:Charles 的 .chls、Fiddler 的 .saz,只能用原工具打开。给别人传文件时,导出成 HAR 比传私有格式通用。

把文件导进来

拿到文件,打开方式无非两条:找对应格式的工具,或者找一个都能吃下的。

分头解决的路线是:HAR 拖进 Chrome DevTools 的 Network 面板能看,pcap 交给 Wireshark,私有格式回原工具。这套分工能用,但文件一多就散——同事可能发来 HAR,也可能发来 Wireshark 存的 pcapng,得先判断"这归谁管"。

另一条路是用 TraceEagle 的"导入会话",HAR 1.2、.pcap / .pcapng、常见抓包工具导出的会话文件都从这一个入口进来。导入后请求就躺在列表里,跟本机抓的没有区别:切视图、看解码、查主机档案、拿去对比,都在同一套界面里。上次看同事给的 HAR 文件就是这么处理的,不用先想"该装什么软件",导进来直接看。

打开之后:三步读明白

导入只是开始,真正要回答的是"这个请求为什么出错"。

看内容。 先把乱码清零。HAR 里的 body 常是 base64 或压缩过的(开头那个 encoding: base64 就是),pcap 里非 HTTP 的流量本身是一堆原始字节。自动解压、自动识别格式、protobuf 免 .proto 解析、私有帧自动拆这一套在这里直接复用——导入的会话和本机抓的走同一套解码,不用因为"文件是别人抓的"就换流程。

查来源。 分析别人的包,经常要判断"这个域名、这个 IP 是什么来头"。HAR 里每个请求的主机名、pcap 里每个对端 IP,点开都有主机档案:归属地、ASN、证书信息、技术栈。查一个陌生 IP 的归属,不用对着地址段猜。

做对比。 这是导入场景里最实用的一步:同事抓的失败请求和你抓的成功请求,可以跨会话放在一起逐行比——参数、请求头、响应体哪里不一样。比"你把请求截图发我看看"来回几个回合高效得多,接口偶发失败这类问题,两份记录摆一起经常一眼出答案。

和常见工具怎么分工

这几个工具各管一段,位置挺清楚。

Chrome DevTools 看自己刚导出的 HAR 顺手,即开即用;但它开不了 pcap,也没法把两份 HAR 放一起比对。Wireshark 是 pcap 的专业户,包级分析(重传、时延、连接状态)没有替代品,代价是看 HTTP 内容要自己 follow stream,遇到 HTTPS 还得先有解密密钥。Charles、Fiddler 开自家会话最舒服,能改能重放,跨格式就弱一些。

TraceEagle 的位置在中间:HAR 和 pcap 都能进,进来之后解码、档案、对比、再导出一路通。实际用法多是组合——Wireshark 看完包级异常,把 pcap 导进来解出 HTTP 明文;或者同事用 Charles 抓的包导出 HAR,导进来和本地这份做对比。

问得最多的几件事

Chrome 导出的 HAR 怎么没有响应内容? 导出时选项没带对——浏览器的导出项里有"带内容"和"不带内容"两种,只有带内容的那份包含响应体。给别人导 HAR 文件排查问题时,记得选带内容的那种。

Wireshark 抓的 pcapng 导进来是明文还是密文? 看文件里有没有密钥。pcapng 可以把 TLS 解密密钥(SSLKEYLOGFILE 那套)一起写进文件,带了就能解出明文,没带就是原始密文,只能看包的特征。请对方导文件时,有条件的话顺手把密钥带上。

HAR 和 pcap 能互相转换吗? 不能直接转。HAR 是应用层的请求清单,pcap 是网卡层的原始包,信息不在一个层面——从 pcap 出 HAR 要先解密再完整重组,HAR 倒回 pcap 缺的是全部底层细节。两种格式都收的工具,内部是分别解析,不是转换。

同事发来的私有格式打不开怎么办? 请对方在原工具里导一份 HAR。HAR 1.2 是通用交换格式,浏览器和主流代理工具都认——你要把包发出去时,也可以照这个思路来。

收个尾

拿到抓包文件的分析路径可以固定下来:先看扩展名——.har 是应用层清单,找个能看 JSON 会话的工具或直接导进来;.pcap / .pcapng 是原始包,交给能重组连接的工具;私有格式请对方转 HAR。然后看、比、导:解码看清内容,对比找出差异,再导出交给下一个环节。

下次再收到同事发来的抓包文件,从"是 HAR 还是 pcap"判断起,后面的路就是一条直线。'''

收起阅读 »

Hbuilderx云打包IOS,修改manifest.json版本号无效的问题

iOS打包 云打包

Hbuilderx云打包应用时

云打包完成后,发现安卓版本号正常,ios却还是识别到了之前的版本号;

解决方式

1.将uni统计关了

"uniStatistics" : {  
      "enable" : false  
  }  

2.将 unpackage/cache/ 目录下关于ios的删了,很可能和使用hbuilder打包出的ios本地基座有关;

暂时我是这么解决的,如果有其他根治的问题可以多留言交流

继续阅读 »

Hbuilderx云打包应用时

云打包完成后,发现安卓版本号正常,ios却还是识别到了之前的版本号;

解决方式

1.将uni统计关了

"uniStatistics" : {  
      "enable" : false  
  }  

2.将 unpackage/cache/ 目录下关于ios的删了,很可能和使用hbuilder打包出的ios本地基座有关;

暂时我是这么解决的,如果有其他根治的问题可以多留言交流

收起阅读 »

关于 uni-push 小米厂商推送“个人订阅”迁移至“公信消息”的适配通知

unipush 小米

关于push,近期小米推送将上线订阅消息功能,原私信通道下的个人订阅类目将统一调整至公信消息。仅接入小米个人订阅消息的 App 开发者需关注,未使用无需处理。

1、订阅类消息仅支持使用 8 个专属订阅类目,规则详见小米推送2026年消息分类新规:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2321

2、2026年8月1日起原有私信渠道下的【个人订阅】通道将关闭申请入口,于2026年12月31日正式关停使用。

3、2026年8月1日之前已申请接入过【个人订阅】分类的应用,需要在2026年12月31日前完成订阅消息功能接入和订阅授权弹窗适配。具体接入流程请参考:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2320

4、2026年8月1日之后新申请订阅分类的需要按照新规接入发送

对接个推侧需要进行以下适配:
1、服务端:接入订阅类消息后推送提醒方式与私信消息相同,新订阅消息的channel_id与原有【个人订阅】分类的channel_id不同,在调用个推服务推送API下发订阅类消息时,必须同时设置小米消息分类和私信模板相关参数,接口设置参照文档【小米】私信模板id:https://docs.getui.com/getui/server/rest_v2/third_party/#doc-title-15
2、客户端:

  • Android小米厂商SDK 需要升级到 com.getui.opt:xmp:3.3.7 版本
  • 客户端需完成订阅消息功能接入,同步适配订阅授权弹窗,统一采用系统原生 API 调用实现,完整接入规范可参考小米推送订阅消息官方文档:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2320

以上请尽快完成适配,根据应用业务场景,发送不同类别的消息。如有疑问,可以添加微信客服群进行咨询,加群方式参考文档:https://uniapp.dcloud.net.cn/unipush-v2.html#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98

继续阅读 »

关于push,近期小米推送将上线订阅消息功能,原私信通道下的个人订阅类目将统一调整至公信消息。仅接入小米个人订阅消息的 App 开发者需关注,未使用无需处理。

1、订阅类消息仅支持使用 8 个专属订阅类目,规则详见小米推送2026年消息分类新规:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2321

2、2026年8月1日起原有私信渠道下的【个人订阅】通道将关闭申请入口,于2026年12月31日正式关停使用。

3、2026年8月1日之前已申请接入过【个人订阅】分类的应用,需要在2026年12月31日前完成订阅消息功能接入和订阅授权弹窗适配。具体接入流程请参考:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2320

4、2026年8月1日之后新申请订阅分类的需要按照新规接入发送

对接个推侧需要进行以下适配:
1、服务端:接入订阅类消息后推送提醒方式与私信消息相同,新订阅消息的channel_id与原有【个人订阅】分类的channel_id不同,在调用个推服务推送API下发订阅类消息时,必须同时设置小米消息分类和私信模板相关参数,接口设置参照文档【小米】私信模板id:https://docs.getui.com/getui/server/rest_v2/third_party/#doc-title-15
2、客户端:

  • Android小米厂商SDK 需要升级到 com.getui.opt:xmp:3.3.7 版本
  • 客户端需完成订阅消息功能接入,同步适配订阅授权弹窗,统一采用系统原生 API 调用实现,完整接入规范可参考小米推送订阅消息官方文档:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2320

以上请尽快完成适配,根据应用业务场景,发送不同类别的消息。如有疑问,可以添加微信客服群进行咨询,加群方式参考文档:https://uniapp.dcloud.net.cn/unipush-v2.html#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98

收起阅读 »

怎么用 BeeWare(Briefcase)把 Python 应用打包成 iOS App 并上架

iOS

'''BeeWare 走的是和 Flet 不一样的路子:它的 Toga 用的是各平台的原生控件,iOS 上就是真的 UIKit,不套 Flutter 或者 WebView。打包工具叫 Briefcase,briefcase create iOS 会生成一个 Xcode 工程,briefcase build iOS 编译,briefcase package iOS 出包。

因为最终还是 Xcode 在干活,这几条命令都得在 macOS 上跑。Windows 上 Briefcase 只能处理 Windows/Android 那些目标。这一步没有变通。

我的分工是:Python 代码在 Windows 上写,iOS 出包丢给 macOS 机器;证书、描述文件、IPA 上传用开心上架(AppUploader)在 Windows 上处理。

先说结论:哪一步真绕不开 Mac

环节 我实际怎么做的
注册 Bundle ID、签发证书、生成描述文件 Windows 上做完
写 Python 代码 Windows
briefcase create / build / package iOS 要 macOS + Xcode
提交 App Store Windows 上传
装到 iPhone 测试 扫码注册 UDID,再扫码装

说明一下:我用的签证书工具本身在 macOS 上也能用(网页版不挑系统,桌面客户端有 macOS 版)。表里"不需要 Mac"是指这一步不再需要 Xcode、钥匙串访问或 Transporter。

第一步:在 pyproject.toml 里定好 bundle

Briefcase 的配置全在 pyproject.toml 里。iOS 的包标识由 bundle 加 app 名拼出来:

[tool.briefcase]  
project_name = "My App"  
bundle = "com.yourcompany"  
version = "1.0.0"  

[tool.briefcase.app.myapp]  
formal_name = "My App"

这样最终的 Bundle Identifier 就是 com.yourcompany.myapp。拼出来的这个值要和 Apple 注册的 Bundle ID、描述文件绑定的 App ID 完全一致——注意是拼出来的结果,不是 bundle 字段本身,这点第一次配的时候容易搞错。

第二步:在 Windows 上签出证书和描述文件

注册 Bundle ID:开心上架的「Bundle ID」页面点「新建 Bundle ID」,Identifier 填拼出来的那个完整标识。创建后改不了,所以先把 bundle 和 app 名定下来再注册。

签发证书:「证书」页面点「新建证书」,上架选发布签名证书,真机调试选开发签名证书。绑定上面的 Bundle ID,填名称邮箱,设一个 .p12 保护密码——导入 Mac 钥匙串时要输,服务器不保存它,忘了只能吊销重签。勾上「使用开心上架服务同步证书」的话,出包用的那台 Mac 直接登录下载就行。
签发证书

生成描述文件:「描述文件」页面点「新建描述文件」,上架选 App Store 类型,内测选 Ad Hoc 并勾上测试设备,关联上面的 Bundle ID 和证书,提交后下载 .mobileprovision。

私钥和 CSR 都在你本机生成后封进 .p12,不用在 Mac 上走「钥匙串访问 → 从证书颁发机构请求证书」那一套——对只写 Python、平时压根不碰 Xcode 的人来说,这一步省下来挺值的。

第三步:在 macOS 上导入签名材料

security import dist.p12 -k ~/Library/Keychains/login.keychain-db -P "你的p12密码" -T /usr/bin/codesign  

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles  
cp AppStore.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/

第四步:生成工程、编译、出包

在 macOS 上依次跑:

briefcase create iOS  
briefcase build iOS  
briefcase package iOS

briefcase create iOS 会在 build/ 下生成一个 Xcode 工程。package 这一步会做签名,Briefcase 会让你选用哪个签名身份(identity),也可以用参数直接指定,具体参数名以 briefcase package iOS --help 输出的为准——Briefcase 各版本的命令行选项变动比较频繁。

签名这块有个实操建议:别让 Xcode 自动管理签名。生成的工程里如果开着 Automatically manage signing,Xcode 会跑去 Apple 那边替你申请新证书,而同类型证书一共就 3 个名额,几轮下来会占满。用 Xcode 打开生成的工程,把它关掉,手动选第二步那套证书和描述文件,再回来跑 briefcase package iOS。

改了 pyproject.toml 里跟原生工程相关的配置之后,记得重新 briefcase create iOS(或者带 -r 更新),不然生成的工程里还是旧配置。

第五步:把 IPA 传上去

拿到包之后就跟 Mac 无关了。我一般先在开心上架的「文件&提交」页面把 IPA 打开看一眼签名信息——Bundle ID 是不是拼出来的那个、描述文件类型对不对、证书有没有过期,确认了再点「提交 App Store」。

命令行版本,在 Windows 上直接跑:

appuploader-cli upload -f MyApp.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop

-p 是 Apple 生成的 App 专用密码(xxxx-xxxx-xxxx-xxxx),不是 Apple ID 登录密码。也可以用 API 密钥:

appuploader-cli upload -f MyApp.ipa --api-key <KeyID> --api-issuer <IssuerID> --private-key Auth_Key.p8

密钥方式只收 .ipa,且 App 要先在 App Store Connect 里建好。

第六步:发内测包

「设备」页面点「注册设备」,二维码发出去让人扫上报 UDID(一年 100 台,免费账号 3 台)。回描述文件那边点「重建」,生成一份包含这些设备的 Ad Hoc 描述文件,拷到 Mac 上替换掉旧的。

重新 package 一次,然后在「文件&提交」页面生成二维码让人扫码装。

写在最后

BeeWare 这条路对纯 Python 背景的人最友好的地方,是你几乎不用学 Objective-C 或 Swift。但签名这一环是躲不掉的 Apple 规则,和你用什么语言无关——把证书和描述文件在 Windows 上准备好,Mac 那边就只剩三条 briefcase 命令。'''

继续阅读 »

'''BeeWare 走的是和 Flet 不一样的路子:它的 Toga 用的是各平台的原生控件,iOS 上就是真的 UIKit,不套 Flutter 或者 WebView。打包工具叫 Briefcase,briefcase create iOS 会生成一个 Xcode 工程,briefcase build iOS 编译,briefcase package iOS 出包。

因为最终还是 Xcode 在干活,这几条命令都得在 macOS 上跑。Windows 上 Briefcase 只能处理 Windows/Android 那些目标。这一步没有变通。

我的分工是:Python 代码在 Windows 上写,iOS 出包丢给 macOS 机器;证书、描述文件、IPA 上传用开心上架(AppUploader)在 Windows 上处理。

先说结论:哪一步真绕不开 Mac

环节 我实际怎么做的
注册 Bundle ID、签发证书、生成描述文件 Windows 上做完
写 Python 代码 Windows
briefcase create / build / package iOS 要 macOS + Xcode
提交 App Store Windows 上传
装到 iPhone 测试 扫码注册 UDID,再扫码装

说明一下:我用的签证书工具本身在 macOS 上也能用(网页版不挑系统,桌面客户端有 macOS 版)。表里"不需要 Mac"是指这一步不再需要 Xcode、钥匙串访问或 Transporter。

第一步:在 pyproject.toml 里定好 bundle

Briefcase 的配置全在 pyproject.toml 里。iOS 的包标识由 bundle 加 app 名拼出来:

[tool.briefcase]  
project_name = "My App"  
bundle = "com.yourcompany"  
version = "1.0.0"  

[tool.briefcase.app.myapp]  
formal_name = "My App"

这样最终的 Bundle Identifier 就是 com.yourcompany.myapp。拼出来的这个值要和 Apple 注册的 Bundle ID、描述文件绑定的 App ID 完全一致——注意是拼出来的结果,不是 bundle 字段本身,这点第一次配的时候容易搞错。

第二步:在 Windows 上签出证书和描述文件

注册 Bundle ID:开心上架的「Bundle ID」页面点「新建 Bundle ID」,Identifier 填拼出来的那个完整标识。创建后改不了,所以先把 bundle 和 app 名定下来再注册。

签发证书:「证书」页面点「新建证书」,上架选发布签名证书,真机调试选开发签名证书。绑定上面的 Bundle ID,填名称邮箱,设一个 .p12 保护密码——导入 Mac 钥匙串时要输,服务器不保存它,忘了只能吊销重签。勾上「使用开心上架服务同步证书」的话,出包用的那台 Mac 直接登录下载就行。
签发证书

生成描述文件:「描述文件」页面点「新建描述文件」,上架选 App Store 类型,内测选 Ad Hoc 并勾上测试设备,关联上面的 Bundle ID 和证书,提交后下载 .mobileprovision。

私钥和 CSR 都在你本机生成后封进 .p12,不用在 Mac 上走「钥匙串访问 → 从证书颁发机构请求证书」那一套——对只写 Python、平时压根不碰 Xcode 的人来说,这一步省下来挺值的。

第三步:在 macOS 上导入签名材料

security import dist.p12 -k ~/Library/Keychains/login.keychain-db -P "你的p12密码" -T /usr/bin/codesign  

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles  
cp AppStore.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/

第四步:生成工程、编译、出包

在 macOS 上依次跑:

briefcase create iOS  
briefcase build iOS  
briefcase package iOS

briefcase create iOS 会在 build/ 下生成一个 Xcode 工程。package 这一步会做签名,Briefcase 会让你选用哪个签名身份(identity),也可以用参数直接指定,具体参数名以 briefcase package iOS --help 输出的为准——Briefcase 各版本的命令行选项变动比较频繁。

签名这块有个实操建议:别让 Xcode 自动管理签名。生成的工程里如果开着 Automatically manage signing,Xcode 会跑去 Apple 那边替你申请新证书,而同类型证书一共就 3 个名额,几轮下来会占满。用 Xcode 打开生成的工程,把它关掉,手动选第二步那套证书和描述文件,再回来跑 briefcase package iOS。

改了 pyproject.toml 里跟原生工程相关的配置之后,记得重新 briefcase create iOS(或者带 -r 更新),不然生成的工程里还是旧配置。

第五步:把 IPA 传上去

拿到包之后就跟 Mac 无关了。我一般先在开心上架的「文件&提交」页面把 IPA 打开看一眼签名信息——Bundle ID 是不是拼出来的那个、描述文件类型对不对、证书有没有过期,确认了再点「提交 App Store」。

命令行版本,在 Windows 上直接跑:

appuploader-cli upload -f MyApp.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop

-p 是 Apple 生成的 App 专用密码(xxxx-xxxx-xxxx-xxxx),不是 Apple ID 登录密码。也可以用 API 密钥:

appuploader-cli upload -f MyApp.ipa --api-key <KeyID> --api-issuer <IssuerID> --private-key Auth_Key.p8

密钥方式只收 .ipa,且 App 要先在 App Store Connect 里建好。

第六步:发内测包

「设备」页面点「注册设备」,二维码发出去让人扫上报 UDID(一年 100 台,免费账号 3 台)。回描述文件那边点「重建」,生成一份包含这些设备的 Ad Hoc 描述文件,拷到 Mac 上替换掉旧的。

重新 package 一次,然后在「文件&提交」页面生成二维码让人扫码装。

写在最后

BeeWare 这条路对纯 Python 背景的人最友好的地方,是你几乎不用学 Objective-C 或 Swift。但签名这一环是躲不掉的 Apple 规则,和你用什么语言无关——把证书和描述文件在 Windows 上准备好,Mac 那边就只剩三条 briefcase 命令。'''

收起阅读 »

JavaScript `this` 指南

JavaScript this

JavaScript this 指南

一、核心心法

this 不是在函数定义时确定的,而是在函数被调用时确定的。
判断 this,只需要看一件事:这个函数是怎么被调用的(调用点)。

唯一的例外是箭头函数——它根本没有自己的 this。


二、五种绑定规则

规则 1:默认绑定(独立函数调用)

function foo() {  
  console.log(this);  
}  

foo();  // 非严格模式: globalThis (浏览器 window / Node global)  
        // 严格模式:   undefined
'use strict';  
function foo() { console.log(this); }  
foo();  // undefined

顶层 this 的值取决于环境:

环境 顶层 this
浏览器 <script> window
ES Module (.mjs / type="module") undefined
CommonJS 模块 (Node .js) module.exports(即 {})
函数内(非严格) globalThis
函数内(严格) undefined
class 内部 永远严格模式

规则 2:隐式绑定(方法调用)

看调用时最后一个点前面是谁。

const obj = {  
  name: 'obj',  
  foo() { console.log(this.name); }  
};  

obj.foo();  // 'obj'

多层嵌套只看最后一层:

const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };  
a.b.foo();  // 'b'  ← 不是 'a'

方括号访问同理:

a.b['foo']();  // 'b'

规则 3:显式绑定(call / apply / bind)

function greet(greeting, punct) {  
  console.log(greeting + ', ' + this.name + punct);  
}  
const me = { name: 'Alice' };  

greet.call(me, 'Hi', '!');       // 逐个传参  
greet.apply(me, ['Hi', '!']);    // 数组传参 (Apply → Array)  
const bound = greet.bind(me, 'Hi');  // 返回新函数 + 支持柯里化  
bound('!');

bind 的三个特性:

  1. 返回一个新函数,不立即执行
  2. 支持参数预置(柯里化)
  3. 硬绑定:绑定后无法再被 call/apply 改变
const hard = greet.bind(me);  
hard.call({ name: 'Bob' }, 'Yo', '!');  // 依然是 Alice

传 null / undefined 的坑:

function foo() { console.log(this); }  
foo.call(null);  // 非严格: globalThis(被替换了!)  严格: null

安全做法——DMZ 空对象:

const DMZ = Object.create(null);   // 比 {} 更干净,无原型  
foo.apply(DMZ, [1, 2]);

原始值会被装箱(仅非严格模式):

function f() { console.log(typeof this); }  
f.call(1);              // 'object'  (Number 包装对象)  
// 严格模式下: 'number'

规则 4:new 绑定

new Foo() 做了四件事:

  1. 创建一个新的空对象
  2. 将该对象的 [[Prototype]] 指向 Foo.prototype
  3. 以该对象为 this 执行 Foo
  4. 如果 Foo 没有返回一个对象,则返回这个新对象
function Person(name) {  
  this.name = name;  
  // return { name: 'hacked' };  // 若返回对象,则 new 的结果是它  
  // return 42;                  // 返回原始值 → 被忽略,仍返回 this  
}  
const p = new Person('Alice');  // p.name === 'Alice'

规则 5:箭头函数(词法绑定)

箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。

const obj = {  
  name: 'obj',  
  regular() {  
    const arrow = () => console.log(this.name);  
    arrow();  // 'obj'  ← 继承 regular 的 this  
  },  
  arrowMethod: () => console.log(this?.name)  // ⚠️ 继承的是模块/全局的 this  
};  

obj.regular();      // 'obj'  
obj.arrowMethod();  // undefined  ← 经典陷阱!

箭头函数无法被改变:

const arrow = () => console.log(this);  
arrow.call({ a: 1 });   // call 无效  
arrow.bind({ a: 1 })(); // bind 无效  
new arrow();            // TypeError: arrow is not a constructor

箭头函数也没有 arguments、super、new.target。


三、优先级

new 绑定  >  显式绑定(bind/call/apply)  >  隐式绑定(obj.fn)  >  默认绑定

箭头函数不在此列——它压根不参与,直接看词法作用域。

验证 new > bind:

function Foo(a) { this.a = a; }  
const obj = {};  
const B = Foo.bind(obj);  

B(2);              // obj.a === 2      (bind 生效)  
const b = new B(3);// b.a === 3, obj.a 仍是 2   ← new 赢了

这正是 bind polyfill 里必须判断 new.target 的原因。


四、隐式丢失:最常见的 Bug 来源

1. 赋值给变量

const obj = { name: 'obj', foo() { console.log(this?.name); } };  
const fn = obj.foo;   // 只拿到了函数引用,丢掉了 obj  
fn();                 // undefined

2. 作为回调传递

setTimeout(obj.foo, 100);        // 丢失  
[1].forEach(obj.foo);            // 丢失  
btn.addEventListener('click', obj.foo);  // this 变成 btn

3. 解构

const { foo } = obj;  
foo();  // 丢失

这就是 React 中 const { setState } = this 会出问题、
以及 Vue3 用 ref 而非 this 的部分原因。

4. 间接引用

const o1 = { name: 'o1', foo() { console.log(this.name); } };  
const o2 = { name: 'o2' };  

o2.foo = o1.foo;  
o2.foo();          // 'o2'  (正常隐式绑定)  

(o2.foo = o1.foo)();  // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错

5. 解决方案汇总

// ① bind  
setTimeout(obj.foo.bind(obj), 100);  

// ② 箭头函数包裹(推荐,可读性好)  
setTimeout(() => obj.foo(), 100);  

// ③ 数组方法的 thisArg 参数  
[1, 2].forEach(obj.foo, obj);  

// ④ class field 箭头函数(见下文)

支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap

不支持的: reduce reduceRight sort(它们的第二参数另有用途)


五、Class 中的 this

类体内永远是严格模式

class Counter {  
  count = 0;  

  inc() { this.count++; }               // 原型方法 → this 会丢失  
  dec = () => { this.count--; };        // 类字段箭头函数 → this 永久绑定实例  
}  

const c = new Counter();  
const { inc, dec } = c;  
dec();   // ✅ 正常  
inc();   // ❌ TypeError: Cannot read properties of undefined

两者的本质区别:

原型方法 inc() 类字段 dec = () => {}
存放位置 Counter.prototype 每个实例上
内存 共享一份 每个实例一份(内存开销大)
this 调用时决定 定义时锁死为实例
可被 call 改变 是 否
可被子类 super 调用 是 否

推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:

class Counter {  
  constructor() {  
    this.inc = this.inc.bind(this);  // 等价效果  
  }  
}

静态方法中的 this

class A {  
  static create() { return new this(); }  // this === 类本身,支持子类继承  
}  
class B extends A {}  
B.create();  // 返回 B 的实例,而非 A

继承与 super

class Base {  
  constructor() { this.type = 'base'; }  
  hello() { console.log('base hello'); }  
}  
class Child extends Base {  
  constructor() {  
    // console.log(this);  // ❌ ReferenceError: 必须先调用 super()  
    super();               // 派生类中,super() 之前 this 处于 TDZ  
    console.log(this.type);// 'base'  
  }  
  hello() {  
    super.hello();         // super 方法调用时,this 仍是当前实例  
  }  
}

私有字段的坑

class A {  
  #secret = 1;  
  get() { return this.#secret; }  
}  
const a = new A();  
a.get.call({});  // TypeError: Cannot read private member #secret  
                 // 私有字段是"品牌检查",this 必须是真实实例

六、各类场景速查

DOM 事件

btn.addEventListener('click', function () {  
  console.log(this);  // === event.currentTarget (btn)  
});  

btn.addEventListener('click', () => {  
  console.log(this);  // 外层作用域的 this,不是 btn  
});

handleEvent 特殊接口(传对象而非函数):

const handler = {  
  count: 0,  
  handleEvent(e) { console.log(this.count++, e.type); }  // this === handler  
};  
btn.addEventListener('click', handler);

内联 onclick="..." 属性中的 this 也是元素本身。

定时器

setTimeout(function () {  
  console.log(this);  
  // 浏览器非严格: window  
  // Node:         Timeout 对象  
}, 0);

getter / setter

const obj = {  
  _v: 1,  
  get v() { return this._v; }   // this 是访问该属性的对象  
};

Proxy 中的 this 泄漏

const target = { greet() { return this; } };  
const proxy = new Proxy(target, {});  
proxy.greet() === proxy;  // true —— this 是 proxy,不是 target  
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错

标签模板 / 生成器 / async

async function f() { return this; }   // 与普通函数规则一致  
function* g() { yield this; }         // 同上

七、手写实现(面试高频)

Function.prototype.myCall = function (context, ...args) {  
  if (typeof this !== 'function') throw new TypeError('not a function');  
  context = context ?? globalThis;  
  if (Object(context) !== context) context = Object(context);  // 原始值装箱  

  const key = Symbol('fn');       // Symbol 避免属性名冲突  
  context[key] = this;  
  try {  
    return context[key](...args);  
  } finally {  
    delete context[key];  
  }  
};  

Function.prototype.myApply = function (context, args = []) {  
  return this.myCall(context, ...args);  
};  

Function.prototype.myBind = function (context, ...preArgs) {  
  const fn = this;  
  if (typeof fn !== 'function') throw new TypeError('not a function');  

  function bound(...args) {  
    // 关键:new 调用时忽略 context,使用新建的 this  
    return fn.apply(  
      new.target ? this : context,  
      [...preArgs, ...args]  
    );  
  }  
  // 维持原型链,使 instanceof 正确  
  bound.prototype = Object.create(fn.prototype || null);  
  return bound;  
};
// 手写 new  
function myNew(Ctor, ...args) {  
  const obj = Object.create(Ctor.prototype);  
  const ret = Ctor.apply(obj, args);  
  return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))  
    ? ret  
    : obj;  
}

八、综合练习(先自己作答)

var name = 'global';   // 注意:必须是 var,let/const 不挂载到 window  

const obj = {  
  name: 'obj',  
  f1() { console.log(this.name); },  
  f2: () => console.log(this?.name),  
  f3() {  
    return function () { console.log(this?.name); };  
  },  
  f4() {  
    return () => console.log(this.name);  
  }  
};  

obj.f1();          // ?  
obj.f2();          // ?  
obj.f3()();        // ?  
obj.f4()();        // ?  
const g = obj.f1;  
g();               // ?  
obj.f1.call({ name: 'x' });  // ?  
new obj.f1();      // ?

答案

obj.f1()          → 'obj'        隐式绑定  
obj.f2()          → 'global'/undefined  箭头函数继承顶层 this  
obj.f3()()        → 'global'/undefined  返回的函数独立调用,默认绑定  
obj.f4()()        → 'obj'        箭头函数继承 f4 的 this  
g()               → 'global'/undefined  隐式丢失  
obj.f1.call({...})→ 'x'          显式绑定  
new obj.f1()      → undefined    new 绑定,新对象上没有 name

(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)


九、终极判断流程图

函数是箭头函数?  
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)  
└─ 否 ↓  
   用 new 调用? ──→ 是 → this = 新创建的对象  
   ↓ 否  
   用 call/apply/bind 调用? ──→ 是 → this = 指定的对象  
   ↓ 否                              (非严格下 null/undefined 会替换为 globalThis)  
   通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)  
   ↓ 否  
   严格模式? ──→ 是 → this = undefined  
              └→ 否 → this = globalThis

十、最佳实践

  1. 优先用箭头函数写回调,避免 this 丢失
  2. 对象方法用简写语法 foo() {},不要用箭头函数(除非确实想要外层 this)
  3. class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器 bind
  4. 全项目开启严格模式 / 使用 ESM,让 this 错误尽早暴露为 undefined 报错
  5. TypeScript 用户:开启 noImplicitThis,并善用显式 this 参数:
    function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ }  
  6. 不确定时,打印 console.log(this) —— 比推理快十倍
继续阅读 »

JavaScript this 指南

一、核心心法

this 不是在函数定义时确定的,而是在函数被调用时确定的。
判断 this,只需要看一件事:这个函数是怎么被调用的(调用点)。

唯一的例外是箭头函数——它根本没有自己的 this。


二、五种绑定规则

规则 1:默认绑定(独立函数调用)

function foo() {  
  console.log(this);  
}  

foo();  // 非严格模式: globalThis (浏览器 window / Node global)  
        // 严格模式:   undefined
'use strict';  
function foo() { console.log(this); }  
foo();  // undefined

顶层 this 的值取决于环境:

环境 顶层 this
浏览器 <script> window
ES Module (.mjs / type="module") undefined
CommonJS 模块 (Node .js) module.exports(即 {})
函数内(非严格) globalThis
函数内(严格) undefined
class 内部 永远严格模式

规则 2:隐式绑定(方法调用)

看调用时最后一个点前面是谁。

const obj = {  
  name: 'obj',  
  foo() { console.log(this.name); }  
};  

obj.foo();  // 'obj'

多层嵌套只看最后一层:

const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };  
a.b.foo();  // 'b'  ← 不是 'a'

方括号访问同理:

a.b['foo']();  // 'b'

规则 3:显式绑定(call / apply / bind)

function greet(greeting, punct) {  
  console.log(greeting + ', ' + this.name + punct);  
}  
const me = { name: 'Alice' };  

greet.call(me, 'Hi', '!');       // 逐个传参  
greet.apply(me, ['Hi', '!']);    // 数组传参 (Apply → Array)  
const bound = greet.bind(me, 'Hi');  // 返回新函数 + 支持柯里化  
bound('!');

bind 的三个特性:

  1. 返回一个新函数,不立即执行
  2. 支持参数预置(柯里化)
  3. 硬绑定:绑定后无法再被 call/apply 改变
const hard = greet.bind(me);  
hard.call({ name: 'Bob' }, 'Yo', '!');  // 依然是 Alice

传 null / undefined 的坑:

function foo() { console.log(this); }  
foo.call(null);  // 非严格: globalThis(被替换了!)  严格: null

安全做法——DMZ 空对象:

const DMZ = Object.create(null);   // 比 {} 更干净,无原型  
foo.apply(DMZ, [1, 2]);

原始值会被装箱(仅非严格模式):

function f() { console.log(typeof this); }  
f.call(1);              // 'object'  (Number 包装对象)  
// 严格模式下: 'number'

规则 4:new 绑定

new Foo() 做了四件事:

  1. 创建一个新的空对象
  2. 将该对象的 [[Prototype]] 指向 Foo.prototype
  3. 以该对象为 this 执行 Foo
  4. 如果 Foo 没有返回一个对象,则返回这个新对象
function Person(name) {  
  this.name = name;  
  // return { name: 'hacked' };  // 若返回对象,则 new 的结果是它  
  // return 42;                  // 返回原始值 → 被忽略,仍返回 this  
}  
const p = new Person('Alice');  // p.name === 'Alice'

规则 5:箭头函数(词法绑定)

箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。

const obj = {  
  name: 'obj',  
  regular() {  
    const arrow = () => console.log(this.name);  
    arrow();  // 'obj'  ← 继承 regular 的 this  
  },  
  arrowMethod: () => console.log(this?.name)  // ⚠️ 继承的是模块/全局的 this  
};  

obj.regular();      // 'obj'  
obj.arrowMethod();  // undefined  ← 经典陷阱!

箭头函数无法被改变:

const arrow = () => console.log(this);  
arrow.call({ a: 1 });   // call 无效  
arrow.bind({ a: 1 })(); // bind 无效  
new arrow();            // TypeError: arrow is not a constructor

箭头函数也没有 arguments、super、new.target。


三、优先级

new 绑定  >  显式绑定(bind/call/apply)  >  隐式绑定(obj.fn)  >  默认绑定

箭头函数不在此列——它压根不参与,直接看词法作用域。

验证 new > bind:

function Foo(a) { this.a = a; }  
const obj = {};  
const B = Foo.bind(obj);  

B(2);              // obj.a === 2      (bind 生效)  
const b = new B(3);// b.a === 3, obj.a 仍是 2   ← new 赢了

这正是 bind polyfill 里必须判断 new.target 的原因。


四、隐式丢失:最常见的 Bug 来源

1. 赋值给变量

const obj = { name: 'obj', foo() { console.log(this?.name); } };  
const fn = obj.foo;   // 只拿到了函数引用,丢掉了 obj  
fn();                 // undefined

2. 作为回调传递

setTimeout(obj.foo, 100);        // 丢失  
[1].forEach(obj.foo);            // 丢失  
btn.addEventListener('click', obj.foo);  // this 变成 btn

3. 解构

const { foo } = obj;  
foo();  // 丢失

这就是 React 中 const { setState } = this 会出问题、
以及 Vue3 用 ref 而非 this 的部分原因。

4. 间接引用

const o1 = { name: 'o1', foo() { console.log(this.name); } };  
const o2 = { name: 'o2' };  

o2.foo = o1.foo;  
o2.foo();          // 'o2'  (正常隐式绑定)  

(o2.foo = o1.foo)();  // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错

5. 解决方案汇总

// ① bind  
setTimeout(obj.foo.bind(obj), 100);  

// ② 箭头函数包裹(推荐,可读性好)  
setTimeout(() => obj.foo(), 100);  

// ③ 数组方法的 thisArg 参数  
[1, 2].forEach(obj.foo, obj);  

// ④ class field 箭头函数(见下文)

支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap

不支持的: reduce reduceRight sort(它们的第二参数另有用途)


五、Class 中的 this

类体内永远是严格模式

class Counter {  
  count = 0;  

  inc() { this.count++; }               // 原型方法 → this 会丢失  
  dec = () => { this.count--; };        // 类字段箭头函数 → this 永久绑定实例  
}  

const c = new Counter();  
const { inc, dec } = c;  
dec();   // ✅ 正常  
inc();   // ❌ TypeError: Cannot read properties of undefined

两者的本质区别:

原型方法 inc() 类字段 dec = () => {}
存放位置 Counter.prototype 每个实例上
内存 共享一份 每个实例一份(内存开销大)
this 调用时决定 定义时锁死为实例
可被 call 改变 是 否
可被子类 super 调用 是 否

推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:

class Counter {  
  constructor() {  
    this.inc = this.inc.bind(this);  // 等价效果  
  }  
}

静态方法中的 this

class A {  
  static create() { return new this(); }  // this === 类本身,支持子类继承  
}  
class B extends A {}  
B.create();  // 返回 B 的实例,而非 A

继承与 super

class Base {  
  constructor() { this.type = 'base'; }  
  hello() { console.log('base hello'); }  
}  
class Child extends Base {  
  constructor() {  
    // console.log(this);  // ❌ ReferenceError: 必须先调用 super()  
    super();               // 派生类中,super() 之前 this 处于 TDZ  
    console.log(this.type);// 'base'  
  }  
  hello() {  
    super.hello();         // super 方法调用时,this 仍是当前实例  
  }  
}

私有字段的坑

class A {  
  #secret = 1;  
  get() { return this.#secret; }  
}  
const a = new A();  
a.get.call({});  // TypeError: Cannot read private member #secret  
                 // 私有字段是"品牌检查",this 必须是真实实例

六、各类场景速查

DOM 事件

btn.addEventListener('click', function () {  
  console.log(this);  // === event.currentTarget (btn)  
});  

btn.addEventListener('click', () => {  
  console.log(this);  // 外层作用域的 this,不是 btn  
});

handleEvent 特殊接口(传对象而非函数):

const handler = {  
  count: 0,  
  handleEvent(e) { console.log(this.count++, e.type); }  // this === handler  
};  
btn.addEventListener('click', handler);

内联 onclick="..." 属性中的 this 也是元素本身。

定时器

setTimeout(function () {  
  console.log(this);  
  // 浏览器非严格: window  
  // Node:         Timeout 对象  
}, 0);

getter / setter

const obj = {  
  _v: 1,  
  get v() { return this._v; }   // this 是访问该属性的对象  
};

Proxy 中的 this 泄漏

const target = { greet() { return this; } };  
const proxy = new Proxy(target, {});  
proxy.greet() === proxy;  // true —— this 是 proxy,不是 target  
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错

标签模板 / 生成器 / async

async function f() { return this; }   // 与普通函数规则一致  
function* g() { yield this; }         // 同上

七、手写实现(面试高频)

Function.prototype.myCall = function (context, ...args) {  
  if (typeof this !== 'function') throw new TypeError('not a function');  
  context = context ?? globalThis;  
  if (Object(context) !== context) context = Object(context);  // 原始值装箱  

  const key = Symbol('fn');       // Symbol 避免属性名冲突  
  context[key] = this;  
  try {  
    return context[key](...args);  
  } finally {  
    delete context[key];  
  }  
};  

Function.prototype.myApply = function (context, args = []) {  
  return this.myCall(context, ...args);  
};  

Function.prototype.myBind = function (context, ...preArgs) {  
  const fn = this;  
  if (typeof fn !== 'function') throw new TypeError('not a function');  

  function bound(...args) {  
    // 关键:new 调用时忽略 context,使用新建的 this  
    return fn.apply(  
      new.target ? this : context,  
      [...preArgs, ...args]  
    );  
  }  
  // 维持原型链,使 instanceof 正确  
  bound.prototype = Object.create(fn.prototype || null);  
  return bound;  
};
// 手写 new  
function myNew(Ctor, ...args) {  
  const obj = Object.create(Ctor.prototype);  
  const ret = Ctor.apply(obj, args);  
  return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))  
    ? ret  
    : obj;  
}

八、综合练习(先自己作答)

var name = 'global';   // 注意:必须是 var,let/const 不挂载到 window  

const obj = {  
  name: 'obj',  
  f1() { console.log(this.name); },  
  f2: () => console.log(this?.name),  
  f3() {  
    return function () { console.log(this?.name); };  
  },  
  f4() {  
    return () => console.log(this.name);  
  }  
};  

obj.f1();          // ?  
obj.f2();          // ?  
obj.f3()();        // ?  
obj.f4()();        // ?  
const g = obj.f1;  
g();               // ?  
obj.f1.call({ name: 'x' });  // ?  
new obj.f1();      // ?

答案

obj.f1()          → 'obj'        隐式绑定  
obj.f2()          → 'global'/undefined  箭头函数继承顶层 this  
obj.f3()()        → 'global'/undefined  返回的函数独立调用,默认绑定  
obj.f4()()        → 'obj'        箭头函数继承 f4 的 this  
g()               → 'global'/undefined  隐式丢失  
obj.f1.call({...})→ 'x'          显式绑定  
new obj.f1()      → undefined    new 绑定,新对象上没有 name

(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)


九、终极判断流程图

函数是箭头函数?  
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)  
└─ 否 ↓  
   用 new 调用? ──→ 是 → this = 新创建的对象  
   ↓ 否  
   用 call/apply/bind 调用? ──→ 是 → this = 指定的对象  
   ↓ 否                              (非严格下 null/undefined 会替换为 globalThis)  
   通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)  
   ↓ 否  
   严格模式? ──→ 是 → this = undefined  
              └→ 否 → this = globalThis

十、最佳实践

  1. 优先用箭头函数写回调,避免 this 丢失
  2. 对象方法用简写语法 foo() {},不要用箭头函数(除非确实想要外层 this)
  3. class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器 bind
  4. 全项目开启严格模式 / 使用 ESM,让 this 错误尽早暴露为 undefined 报错
  5. TypeScript 用户:开启 noImplicitThis,并善用显式 this 参数:
    function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ }  
  6. 不确定时,打印 console.log(this) —— 比推理快十倍
收起阅读 »

Android离线打包,plus.runtime.install导致应用闪退

logcat日志如下:

Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run

参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674

在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:

dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}

继续阅读 »

logcat日志如下:

Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run

参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674

在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:

dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}

收起阅读 »

uni-starter模板代码更正

创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->

            <!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->  
            <uni-list-item  v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon  
             clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >  
            </uni-list-item>  
        </uni-list>  
    </view>  

这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。

继续阅读 »

创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->

            <!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->  
            <uni-list-item  v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon  
             clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >  
            </uni-list-item>  
        </uni-list>  
    </view>  

这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。

收起阅读 »

无Mac获取P12证书、描述文件、上传IPA

Appstore上传 ipa

​
1、首先进入我开发的网站:登录 - iOS 开发者工具,http://jqzm.w1.luyouxia.net/iOSTools,该工具集成了Bundle Id、证书、描述文件、添加设备、上传IPA
​​​​
2、然后使用邮箱注册并登陆

3、然后进入账号设置,将参数填写上并保存配置,保存后点一下同步数据

4、进入上传IPA功能,选择IPA文件上传即可

​

继续阅读 »

​
1、首先进入我开发的网站:登录 - iOS 开发者工具,http://jqzm.w1.luyouxia.net/iOSTools,该工具集成了Bundle Id、证书、描述文件、添加设备、上传IPA
​​​​
2、然后使用邮箱注册并登陆

3、然后进入账号设置,将参数填写上并保存配置,保存后点一下同步数据

4、进入上传IPA功能,选择IPA文件上传即可

​

收起阅读 »

在线教育知识付费直播源码免费下载|UniApp 多端可用

uniapp

仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open

凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。

凸知移动端 是一款基于 uni-app(Vue 2) TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。

系统演示:

端 演示地址 账号 密码
后台 https://tuzhi.mutouweb.com/admin.php admin 123456
移动端(用户) https://tuzhi.mutouweb.com 13222222222 123456
PC 端(用户) https://tuzhi.mutouweb.com/pc/#/ 13222222222 123456

一、功能特性

  • 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
  • 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
  • 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
  • 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
  • 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
  • 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
  • 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
  • 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
  • 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)

界面预览:

二、系统架构

凸知是一套完整的知识付费系统,由四个部分组成:

部分 技术栈 说明 源码开放
后端服务 ThinkPHP 5.0 FastAdmin MySQL 接口服务,承载支付、直播、分销等全部业务逻辑 购买授权
移动端(本仓库) uni-app(Vue 2) TDesign H5 / 微信小程序学员端(不支持 App 打包) 开源
管理后台 Vue 3 Arco Design 管理员 / 讲师运营后台 购买授权
PC 学员端 Vue 3 Arco Design 电脑端学员学习页面 购买授权

凸知终端技术结构:

整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。

本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格。

三、技术栈

分类 技术 说明
跨端框架 uni-app(Vue 2.x) vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包)
开发语言 JavaScript(Vue 2 语法) Sass 页面按 xxx.vue js.vue css.css 三文件拆分,样式用 CSS 规则
UI 组件库 TDesign(@tdesign/uniapp) 全局主题样式,小程序端内置构建适配补丁(scripts/)
uView(本地 components/uview-ui) 列表 / 弹窗 / 表单等通用组件
自研 components/tz/ 音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45 业务组件
qiun-data-charts 图表可视化
状态管理 Vuex 3 全局登录态 / 用户信息等
国际化 vue-i18n 8 内置 zh-CN / US 多语言
网络请求 flyio 自封装 $api $api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转
音视频播放 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js 直播与点播,支持倍速 / 弹幕 / 水印等组件
微信能力 jweixin-module 微信登录 / 分享 JS-SDK 封装
构建工具 @vue/cli 5 webpack 由 @dcloudio/vue-cli-plugin-uni 驱动多端编译
测试 Jest 多端单元测试(npm run test:*)

四、快速开始

环境要求:Node.js >= 14(建议 16/18)、npm

# ① 安装依赖  
npm install  

# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)  
npm run dev:h5

浏览器访问 http://localhost:8080 即可看到完整页面效果。

微信小程序编译:

# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)  
npm run dev:mp-weixin  

# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)  
npm run build:mp-weixin

用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。

对接真实后端:

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. 在 src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 src/manifest.json 中替换为自己的 appid。

五、构建

目标 命令
H5 npm run build:h5
微信小程序 npm run build:mp-weixin

小程序构建内置 TDesign 适配补丁(scripts/),构建成功后产物在 dist/build/ 下。

⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。

六、Mock 数据

  • H5 开发模式(NODE_ENV=development)默认关闭,全部走真实后端接口;
  • 开启方式:将 src/static/config.js 的 enableMock 改为 true(显式设置优先级最高),或通过 uni.setStorageSync('enableMock', true) / 环境变量 MOCK_ENABLE 切换;
  • 覆盖 200 接口,fixture 位于 src/mock/data/,按 module/action 路由镜像;
  • 未覆盖的接口自动透传真实网络并输出提示;
  • Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。

七、目录结构

src/  
├── pages/        页面(index 主包   public/app/user/order/course 五个分包,约 100  页面)  
│   ├── index/        首页(主包唯一页面,tabBar 入口)  
│   ├── launch/       启动页(加载与参数透传跳转,未在 pages.json 注册)  
│   ├── public/       公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等  
│   ├── app/          业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等  
│   ├── user/         用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等  
│   ├── order/        订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等  
│   ├── course/       课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等  
│   └── template/     页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)  
├── components/   组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)  
├── common/       公共层  
│   ├── request/  接口封装($api 点分路由、统一鉴权/错误处理)  
│   ├── utils/    工具(路由映射、支付、直播、OSS 签名等)  
│   ├── order/    订单状态处理  
│   └── wechat/   微信登录/分享 SDK 封装  
├── mock/         Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)  
├── static/       静态资源  
├── pages.json    页面路由与分包配置  
└── manifest.json 应用配置(appid 等)

八、开发约定

  • 页面采用 xxx.vue js.vue css.css 三文件拆分(<script src="./js.vue"> @import "./css.css"),逻辑与样式分离;
  • 接口调用统一走 this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理;
  • 业务组件优先复用 components/tz/ 自研组件库;
  • 提交信息遵循 Conventional Commits(feat: / fix: / chore:)。

九、相关链接

名称 链接
使用文档 https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh
功能列表 https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe
授权价格 https://www.tuzhi.ltd/index/pricing/version.html
功能演示 https://www.tuzhi.ltd/index/demo/index.html

十、软件安全

安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。

十一、使用许可

  • 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE;
  • 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
继续阅读 »

仓库地址 https://gitee.com/tuzhi-open/tuzhi-mobile-open

凸知(Tuzhi) 是一套完整的知识付费解决方案,支持私有化部署,覆盖课程售卖、直播授课、圈子社区、考试测评、分销营销、实物商品等完整业务场景,提供管理后台、移动端(H5 / 微信小程序)、PC 学员端等多端入口,帮助个人 IP、教培机构与品牌商家搭建自有卖课平台。

凸知移动端 是一款基于 uni-app(Vue 2) TDesign 开发的知识付费移动端前端,支持 H5 / 微信小程序 多端编译(不支持 App 打包)。覆盖课程点播、直播、专栏、圈子社区、订单支付(前端交互)、优惠券、余额红包、考试答题等完整学习场景。

系统演示:

端 演示地址 账号 密码
后台 https://tuzhi.mutouweb.com/admin.php admin 123456
移动端(用户) https://tuzhi.mutouweb.com 13222222222 123456
PC 端(用户) https://tuzhi.mutouweb.com/pc/#/ 13222222222 123456

一、功能特性

  • 终端覆盖:电脑端 / 微信公众号(H5)/ 微信小程序多端学习
  • 课程内容:图文 / 视频 / 音频 / 专栏售卖,支持试看试听、课件资料与内容保护
  • 互动教学:练习 / 考试测评 / 题库 / 试卷库 / 证书 / 表单 / 圈子社区
  • 直播能力:直播授课 / 伪直播 / 回放 / 直播带货 / 送礼物 / 发红包
  • 营销增长:分销推广 / 优惠券 / 每日签到 / 积分商城 / 付费会员卡 / 组合商品 / 卡密兑换
  • 实物商品:商品管理 / 多规格 SKU / 限购设置 / 售后退换
  • 交易支付:购物车 / 订单管理 / 余额支付 / 微信支付 / 支付宝支付
  • 用户数据:用户管理 / 订阅记录 / 学习分析 / 交易分析 / 订单管理
  • 系统安全:页面 DIY 装修 / 云存储 / 短信与消息推送 / 管理员权限 / 版权保护(跑马灯 / 防录屏)

界面预览:

二、系统架构

凸知是一套完整的知识付费系统,由四个部分组成:

部分 技术栈 说明 源码开放
后端服务 ThinkPHP 5.0 FastAdmin MySQL 接口服务,承载支付、直播、分销等全部业务逻辑 购买授权
移动端(本仓库) uni-app(Vue 2) TDesign H5 / 微信小程序学员端(不支持 App 打包) 开源
管理后台 Vue 3 Arco Design 管理员 / 讲师运营后台 购买授权
PC 学员端 Vue 3 Arco Design 电脑端学员学习页面 购买授权

凸知终端技术结构:

整体调用链路:移动端 / PC 学员端 / 管理后台 → 后端接口服务({code, msg, data} 协议)→ MySQL 数据库。

本仓库仅开源移动端前端源码;如需后端服务或管理后台等其余端的源码,需购买商业授权获取,详见「十一、使用许可」与授权价格。

三、技术栈

分类 技术 说明
跨端框架 uni-app(Vue 2.x) vue-cli 工程,一套代码编译 H5 / 微信小程序等多端(不支持 App 打包)
开发语言 JavaScript(Vue 2 语法) Sass 页面按 xxx.vue js.vue css.css 三文件拆分,样式用 CSS 规则
UI 组件库 TDesign(@tdesign/uniapp) 全局主题样式,小程序端内置构建适配补丁(scripts/)
uView(本地 components/uview-ui) 列表 / 弹窗 / 表单等通用组件
自研 components/tz/ 音频 / 视频 / 媒体编辑 / 上传 / 文件预览等 45 业务组件
qiun-data-charts 图表可视化
状态管理 Vuex 3 全局登录态 / 用户信息等
国际化 vue-i18n 8 内置 zh-CN / US 多语言
网络请求 flyio 自封装 $api $api('module.action') 点分路由,统一鉴权 / 错误处理 / 401 跳转
音视频播放 阿里云播放器 Aliplayer(H5,本地化资源)、hls.js / flv.js 直播与点播,支持倍速 / 弹幕 / 水印等组件
微信能力 jweixin-module 微信登录 / 分享 JS-SDK 封装
构建工具 @vue/cli 5 webpack 由 @dcloudio/vue-cli-plugin-uni 驱动多端编译
测试 Jest 多端单元测试(npm run test:*)

四、快速开始

环境要求:Node.js >= 14(建议 16/18)、npm

# ① 安装依赖  
npm install  

# ② H5 开发(默认不启用 Mock,对接真实后端;如需 Mock 见「Mock 数据」)  
npm run dev:h5

浏览器访问 http://localhost:8080 即可看到完整页面效果。

微信小程序编译:

# ③ 小程序开发模式(监听编译,产物在 dist/dev/mp-weixin)  
npm run dev:mp-weixin  

# ④ 小程序生产构建(内置 TDesign 适配补丁,产物在 dist/build/mp-weixin)  
npm run build:mp-weixin

用微信开发者工具「导入项目」选择对应产物目录(开发选 dist/dev/mp-weixin,生产选 dist/build/mp-weixin),并在 src/manifest.json 中替换为自己的 appid。

对接真实后端:

  1. 修改 src/siteinfo.js 中的 siteroot 为你的后端地址;
  2. 在 src/common/request/api_list.js 中核对接口地址与 {code, msg, data} 协议;
  3. 微信小程序构建需在 src/manifest.json 中替换为自己的 appid。

五、构建

目标 命令
H5 npm run build:h5
微信小程序 npm run build:mp-weixin

小程序构建内置 TDesign 适配补丁(scripts/),构建成功后产物在 dist/build/ 下。

⚠️ 注意:当前版本不支持 App 打包,仅支持 H5 与微信小程序两个平台。

六、Mock 数据

  • H5 开发模式(NODE_ENV=development)默认关闭,全部走真实后端接口;
  • 开启方式:将 src/static/config.js 的 enableMock 改为 true(显式设置优先级最高),或通过 uni.setStorageSync('enableMock', true) / 环境变量 MOCK_ENABLE 切换;
  • 覆盖 200 接口,fixture 位于 src/mock/data/,按 module/action 路由镜像;
  • 未覆盖的接口自动透传真实网络并输出提示;
  • Mock 数据由仓库内 mock-crawler 工具采集真实后端响应生成,发布前已做敏感字段脱敏(token/openid/手机号等)。

七、目录结构

src/  
├── pages/        页面(index 主包   public/app/user/order/course 五个分包,约 100  页面)  
│   ├── index/        首页(主包唯一页面,tabBar 入口)  
│   ├── launch/       启动页(加载与参数透传跳转,未在 pages.json 注册)  
│   ├── public/       公共分包:登录 / 注册 / 找回密码 / 协议 / 搜索 / 消息 / 结果页等  
│   ├── app/          业务功能分包:活动 / 圈子 / 考试 / 优惠券 / 积分 / 签到 / 证书 / 分销 / 直播带货等  
│   ├── user/         用户分包:个人中心 / 资产 / 余额 / 地址 / 收藏 / 学习记录 / 订阅 / 红包等  
│   ├── order/        订单分包:订单列表 / 详情 / 提交 / 评价 / 售后 / 物流等  
│   ├── course/       课程分包:课程详情 / 分类 / 专栏 / 小组 / 直播等  
│   └── template/     页面模板(新建页面的三文件拆分参考,未在 pages.json 注册)  
├── components/   组件(tz/ 自研组件库、uview-ui、qiun-data-charts、视频播放器等)  
├── common/       公共层  
│   ├── request/  接口封装($api 点分路由、统一鉴权/错误处理)  
│   ├── utils/    工具(路由映射、支付、直播、OSS 签名等)  
│   ├── order/    订单状态处理  
│   └── wechat/   微信登录/分享 SDK 封装  
├── mock/         Mock 数据(H5 开发模式默认关闭,开启方式见「六、Mock 数据」)  
├── static/       静态资源  
├── pages.json    页面路由与分包配置  
└── manifest.json 应用配置(appid 等)

八、开发约定

  • 页面采用 xxx.vue js.vue css.css 三文件拆分(<script src="./js.vue"> @import "./css.css"),逻辑与样式分离;
  • 接口调用统一走 this.$api('module.action', data),登录态、401 跳转、错误提示由拦截器统一处理;
  • 业务组件优先复用 components/tz/ 自研组件库;
  • 提交信息遵循 Conventional Commits(feat: / fix: / chore:)。

九、相关链接

名称 链接
使用文档 https://wood-soft.feishu.cn/wiki/KKLkwOPBHiButJkTluTcoXKenjh
功能列表 https://wood-soft.feishu.cn/wiki/YGFYw0k8PiJDWxketgHcBNKNnxe
授权价格 https://www.tuzhi.ltd/index/pricing/version.html
功能演示 https://www.tuzhi.ltd/index/demo/index.html

十、软件安全

安全问题请通过 Issue(需注明"安全")或邮件私下报告给项目维护团队,请勿在公开渠道描述可利用的漏洞细节。我们将在确认后尽快修复并同步发布。

十一、使用许可

  • 本软件遵循 凸知开源协议 V1.0(非商业用途免费,商业用途需购买商业授权),详见 LICENSE;
  • 第三方组件许可声明见 THIRD-PARTY-NOTICES.md(发布前请核对);
收起阅读 »

等保测评wgt打包有高危漏洞!

应用打wgt升级包,使用压缩工具打开wgt文件后,在app-service.js中可以看到云服务空间的accessKey和secretKey等信息,被检测为高危漏洞

应用打wgt升级包,使用压缩工具打开wgt文件后,在app-service.js中可以看到云服务空间的accessKey和secretKey等信息,被检测为高危漏洞

App Store Connect API 密钥是什么,为什么推荐用它登录?

iOS

'''# App Store Connect API 密钥是什么,为什么推荐用它登录?

App Store Connect API 密钥是 Apple 提供的一种无密码登录凭据,通过 Issuer ID、Key ID 和一把私钥(.p8 文件)生成短期 JWT 令牌来认证访问 App Store Connect,不需要输入 Apple ID 密码,也不会触发双重验证,且长期有效——适合团队协作、多设备使用或接入自动化流程。

和账号密码登录相比的三个优势

对比项 账号密码登录 API 密钥登录
是否需要密码 需要 不需要
是否触发双重验证 会(首次) 不会
是否长期有效、可多端共用 依赖登录会话 是,生成一次可在任意设备导入使用

怎么获取

在「开心上架(AppUploader)」的「账号概览」页面:

  1. 用账号密码方式先登录一次,进入「App Store Connect API 密钥」区域。
  2. 点击「生成密钥」,填写密钥名称,提交后会生成一把 ADMIN 权限、可访问全部 App 的密钥。
  3. 私钥(.p8)只能获取这一次,务必立即复制或下载为 .json 密钥文件保存好。
  4. 之后可以直接拿这份 .json 文件,在登录页选择「添加账号 → API 密钥 → 密钥文件」导入,在任意设备上登录同一个 App Store Connect 账号。

密钥列表随时支持「导出密钥文件」(前提是本机保存过该密钥的私钥)或「吊销」——吊销后由它派生的所有账号会立即无法认证,且不可恢复,请谨慎操作。

什么场景必须用 API 密钥

「开心上架」中部分功能明确要求使用 App Store Connect API 密钥或开发者门户账号登录,其他登录方式暂不支持,例如「应用」模块中的分类/年龄分级管理、内购项目管理。如果你需要用到这些能力,建议直接用 API 密钥方式登录。'''

继续阅读 »

'''# App Store Connect API 密钥是什么,为什么推荐用它登录?

App Store Connect API 密钥是 Apple 提供的一种无密码登录凭据,通过 Issuer ID、Key ID 和一把私钥(.p8 文件)生成短期 JWT 令牌来认证访问 App Store Connect,不需要输入 Apple ID 密码,也不会触发双重验证,且长期有效——适合团队协作、多设备使用或接入自动化流程。

和账号密码登录相比的三个优势

对比项 账号密码登录 API 密钥登录
是否需要密码 需要 不需要
是否触发双重验证 会(首次) 不会
是否长期有效、可多端共用 依赖登录会话 是,生成一次可在任意设备导入使用

怎么获取

在「开心上架(AppUploader)」的「账号概览」页面:

  1. 用账号密码方式先登录一次,进入「App Store Connect API 密钥」区域。
  2. 点击「生成密钥」,填写密钥名称,提交后会生成一把 ADMIN 权限、可访问全部 App 的密钥。
  3. 私钥(.p8)只能获取这一次,务必立即复制或下载为 .json 密钥文件保存好。
  4. 之后可以直接拿这份 .json 文件,在登录页选择「添加账号 → API 密钥 → 密钥文件」导入,在任意设备上登录同一个 App Store Connect 账号。

密钥列表随时支持「导出密钥文件」(前提是本机保存过该密钥的私钥)或「吊销」——吊销后由它派生的所有账号会立即无法认证,且不可恢复,请谨慎操作。

什么场景必须用 API 密钥

「开心上架」中部分功能明确要求使用 App Store Connect API 密钥或开发者门户账号登录,其他登录方式暂不支持,例如「应用」模块中的分类/年龄分级管理、内购项目管理。如果你需要用到这些能力,建议直接用 API 密钥方式登录。'''

收起阅读 »