## 1. Project Overview & Quickstart (xiangyuecn/demo_ios)

# demo_ios

Open-source repository xiangyuecn/demo_ios

### Repository Details
- **Repository:** [xiangyuecn/demo_ios](https://github.com/xiangyuecn/demo_ios)
- **Primary Language:** Code

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (xiangyuecn/docs)

## File: README.md

[Binary file]

---

## File: H5/Hybrid小程序混合开发之路 - 数据交互.md

> HTML+CSS是历史悠久、超高自由度、控制精准、表现能力极强、编码简单、学习门槛超低、真跨平台的一种UI界面开发方式。
> 
> 本文介绍的是微信小程序和H5混合开发的一种数据交互方式。

## 很多应用在原生界面中混杂着HTML界面

记得xp时代的QQ，有些界面偶尔会弹出熟悉的js错误对话框，还能右键弹出熟悉的IE6的右键菜单，伪装的挺好，差点没认出来，现在的QQ就不知道了。

美团、淘宝这些拥有几乎无限界面的手机App，顶部进度条一亮，这是一个H5 没谁了！

Electron！好嗨哟~


## 数据交互

使用了HTML作界面，那么和原生程序之间的数据交互基本上是少不了的~（仅仅当做浏览器性质来用的就算了）~。

如原生程序要控制H5界面变化，就需要由原生程序传出数据到H5。

如H5界面用户点击动作触发涉及原生程序业务逻辑的调用，就需要由H5传入数据到原生程序。

这两种数据传输组合在一起就成了交互。在自行嵌入浏览器内核、Electron都可以有自己的接口实现，Android、水果~（滑稽，本文与此物种无关，下同）~都有成熟的解决方案。


## Hybrid App 与 Hybrid 小程序

Hybrid App并非一定是和H5的混合开发，但本文所说的专指H5。同一界面，编写一次代码，就能同时运行在Android、水果、浏览器之上，说没有诱惑力是假的。

小程序并非一定指的的某家的小程序，但本文所说的专指微信小程序。拥有一个强大的流量入口，嗯，就只这一点 ~（看好PWA）~。

自从微信小程序开放了`webview`，和Hybrid App就有了共性，然后生了个“Hybrid 小程序”，是我一个人这么叫吗？我不管，我就要叫！

然鹅，开发者对小程序底层的话语权几乎为0，小程序给什么我们就吃什么，嗯，真香。虽然提供了`webview`，但未提直接的供交互手段（记得要双向才能算交互吗，单向传递数据一边玩球）。

似乎Hybrid之路已然折戟。


## Hybrid 小程序数据交互

终极手段，合理合法！

> 注：发现有人采用的是`websocket`来实现的数据交互，原生小程序和H5都通过服务器来中转数据，点评：这也是个好办法。

### 数据传入
基本原理：H5使用`wx.miniProgram.navigateTo`传递数据给小程序原生程序代码。

### 数据传出
基本原理：通过改变`webview` `src`中的`hash`，来实现无刷新数据传入原生小程序。最初开放`webview`时可用，后面有版本不行了，改变`src`中任意字符会导致重新加载界面，神奇的是现在又恢复了改变`hash`不会刷新。

升级版：通过改变后台`webview` `src`中的`hash`，实现数据传入我们后台页面，不管有没有重新加载，`hash`始终能被我们的H5代码接收（而且省去了处理url变化的问题）。然后由后台页面传输数据给前台页面。



## 实战

举个支付吧，点击H5内的支付按钮，执行完业务逻辑，支付条件准备完毕，现在需要调起微信支付。

### 小程序启动

1. 小程序启动。
2. 启动`Response.wxml`，`webview`加载后台页面。
3. 启动`Main.wxml`，保留`Response.wxml`后台页面在后台运行，前台主界面加载页面。启动完毕。

### 开始支付
1. 主界面请求`jssdk` `wx.miniProgram.navigateTo`接口向`Request.wxml`界面发起请求，`path`中携带请求数据。
2. `Request.wxml`原生程序接收到请求，发现支付请求并不需要保留界面，立马返回上一个界面(前台主界面)；然后后台继续处理支付请求，调起微信支付。
3. 前台主界面H5此时应该是在等待`Request.wxml`的响应结果，并且微信已经弹出支付功能。
4. 用户支付完成，`Request.wxml`残留代码收到支付响应结果，立马通知`Response.wxml`修改后台页面`src`中的`hash`，响应结果数据包含在其中。
5. 后台页面监听到`hash change`事件，或 页面刚刚加载完毕事件。从`hash`中拿到响应数据。
6. 后台页面把响应数据写入`localStroage`，前台页面监听到`localStroage`数据。成功得到支付响应数据。


### 问题缺陷

第2步虽然立即返回了，但还是会有一个短暂的界面切换动画，不过这种数据交互本来就是重量级的，也不可能频繁调用，Android和水果都是一样的，所以用户体验就是这么可观。

改变`webview` `src` `hash`产生的行为是不可预测的 ，任性的版本，如果会刷新页面，延迟会比较大，甚至导致两次调用之间相互覆盖结果。


## 应用案例

笔者最近开发的小程序《祝福贺卡助手》，主功能采用的纯web网页，底层和微信小程序的数据交互就是本文介绍的这套模式。有2个比较重要的交互接口，一个是获取用户数据、另外一个是调起分享；当然还有一堆其他辅助类的接口。

### 扫码体验
网页版：


小程序版：


**界面和功能开发一遍，网页、小程序通吃~ 这就是Hybrid小程序的魅力！**

-------

围绕着小程序《祝福贺卡助手》的开发和审核上线过程，对于其中一些有趣的东西，我会分享出来。

下篇内容预告：Hybrid小程序混合开发之路 - H5录音和水果的对抗。

---

## File: H5/干货：小微个人如何接入免费短信验证码.md

[Binary file]

---

## File: H5/记录一下对swiper4.x.js在H5单页中的滑动优化.md

# 应用场景
仅仅应用于单页应用的滑动操作，用`swiper4.x`接管页面的滚动操作。用来支持顶部和尾部的回弹效果，进一步来支持常见那种下拉刷新动画效果。不适用于轮播图那种应用场景。

虽然只是针对`swiper4.x`，但相关原理，在别的框架中也是有参考意义的。


# 出现的问题

## 一、惯性动画不会在触摸时停止
快速滑动页面，手离开屏幕时产生的惯性动画还在运行时，此时触摸屏幕，动画不会停止。导致连续快速滑动页面，看起来有跳来跳去的感觉。

## 二、快速滑动手势大概率识别成慢滑动手势
连续快速多滑几下，有那么1、2下明显能够感知到滑动惯性动画变慢的情景，给人的感觉就是滑动不流畅。

## 三、惯性动画时长不合理
不管是很慢的滑动，还是很快的滑动，惯性动画时长只能设置一个固定值。慢慢移动松手后也会有一个很长的动画，快速滑的时候动画又有点短，综合看起来给人很卡的感觉。


# 解决手段

除了第一个问题，另外两个问题不修改`swiper4.x`源码似乎无法克服。

## 一、解决惯性动画不会在触摸时停止的问题
滑动时手离开屏幕时产生的惯性动画是`css动画`，并未直接提供停止动画的方法。动画还在运行时如果触摸屏幕，不改源代码，那我们就监听一下`touchStart`事件。

用`setTranslate(translate)`可以移除这个动画css，需要提供页面当前的滚动位置。用`getTranslate()`就行了：
 > 这与通过属性`mySwiper.translate` 获取到的数值稍有不同，即使是在过渡时（`animating`）也能获取到，而后者精度较高

### 解决方案代码
``` javascript
//在touchStart事件中执行
mySwiper.setTranslate(mySwiper.getTranslate());
```

## 二、解决快速滑动手势大概率识别成慢滑动手势问题
这个必须修改源代码才能解决，问题出在计算惯性动画起始速度时的计算参数精度不足。

### 原代码
```
if (params.freeModeMomentum) {
        if (data.velocities.length > 1) {
          var lastMoveEvent = data.velocities.pop();
          var velocityEvent = data.velocities.pop(); //此处取值方式会导致精度不够

          var distance = lastMoveEvent.position - velocityEvent.position;
          var time = lastMoveEvent.time - velocityEvent.time;
          swiper.velocity = distance / time;
          swiper.velocity /= 2;
```
可以看出，他计算起始速度`velocity`参考的是`onTouchMove`最后记录的两个点，单纯取最后两个点是不可靠的，可能因为最后两次`onTouchMove`触发间隔比较长，导致计算出来的速度过低。从而导致偶尔会快速滑动但惯性效果是慢滑动的效果。

如果触发间隔很短，导致动画速度变快，变快了其实感知上并无区别。最要命的还是变慢，感觉很卡一样。

### 解决方案
一个很短的时间内，人的滑动方向不太可能会产生变化，但这个短时间内产生的滑动位移能够很好的代表手势结尾的滑动速度。

经过反复尝试，用100毫秒内的位移来计算惯性起始速度最好，100毫秒内，如果是快速滑动，会触发多次`onTouchMove`，计算出来的速度是很接近实际的手势速度。

问题的解决指向了100毫秒内的第一个点。解决代码：
``` javascript
if (params.freeModeMomentum) {
        if (data.velocities.length > 1) {
			//fix 惯性动画
			var velos=data.velocities;
			var firstEvent=velos[0];
			var lastMoveEvent = velos.pop();
			var velocityEvent =velos.pop();
			for(var i=velos.length-1;i>=0;i--){//找出100毫秒内的起始位置
				var velo=velos[i];
				if(lastMoveEvent.time-velo.time>100){
					break;
				}
				velocityEvent=velo;
			}

          var distance = lastMoveEvent.position - velocityEvent.position;
          var time = lastMoveEvent.time - velocityEvent.time;
          swiper.velocity = distance / time;
          swiper.velocity /= 2;
```

## 三、解决惯性动画时长不合理问题
慢滑动时惯性应该很小，动画很短；快速滑动时惯性应该很大，动画很长。`swiper4.x`只能提供一个固定的惯性动画时长，不改源代码是解决不了的。

### 原代码在第二问代码下面一点点
``` javascript
var momentumDuration = 1000 * params.freeModeMomentumRatio; //写死了固定动画时长
```

### 优化动画
其实也简单，小的就小，大的就大，用初始速度`velocity`来做乘法运算即可达到效果。
``` javascript
var momentumDuration = Math.abs(swiper.velocity) * 1000 * params.freeModeMomentumRatio;
```
结果：比原代码好很多，但跟别的app里面的滑动还是区别蛮大，慢滑时还是太快了点。

App原生滑动动画，感知上是慢的更慢，快的更快。轻微滑动慢的要死，快速动一点点，飞快。



对数曲线！1-0范围蛮符合，缓的地方比线性的还缓，陡的地方奇陡无比。在上面优化的结果基础上用对数加持一下，效果非常不错。另外限定最长动画不超过3秒。
``` javascript
//取对数曲线优化一下0.5-1.5倍之间，慢的越慢，快的越快
momentumDuration*=-Math.log10(Math.min(0.3,Math.max(0.03,(3000-momentumDuration)/3000)));
```

### 解决方案代码
``` javascript
//根据初始速度来确定惯性动画时长
var momentumDuration = Math.min(3000,Math.abs(swiper.velocity) * 1000 * params.freeModeMomentumRatio);
//取对数曲线优化一下0.5-1.5倍之间，慢的越慢，快的越快
momentumDuration*=-Math.log10(Math.min(0.3,Math.max(0.03,(3000-momentumDuration)/3000)));
momentumDuration=Math.min(3000,momentumDuration);
```

# 最终结果
应用`swiper4.x`修改后的单页滚动效果，已经很接近App的滚动效果，虽然还是会有些细微的抖动卡顿感，但要做到完全和App一样的流畅效果，估计蛮难，当前算是已经蛮优了。

还是iscroll省心些，但已经上了`swiper4.x`这条船了就算了，迁移过来又迁移过去白折腾。

改良后的滚动效果，目前[2019-02-27]号以后可以到 [https://jiebian.life/start/xcx/tool_jieri](https://jiebian.life/) 体验（这个时间之前新版本应该还没有上线），小程序和H5共用的页面。

---

## File: H5/设计和编写一个异步通用Picker选择器，用于时间日期、城市、商品分类的选择.md

[Binary file]

---

## File: H5/静态集成腾讯TBS X5内核WebView，从微信提取新版30M浏览器内核打包进apk.md

[Binary file]

---

## File: Other/自己制作chrome便携版实现多版本共存.md

# 自己制作Chrome便携版实现多版本共存


> 本文只针对Windows下的Chrome浏览器的使用。
>
> 有时候我们需要使用老版本Chrome，或者仅仅体验一下最新版。
> 
> 上古时代有IETester用来测试多个IE版本，和本机的IE不冲突。
> 
> Chrome别人也制作了很多便携版，**但不知道有没有加料**。

本文介绍一个自己制作便携版的方法：
- 支持任意版本Chrome
- 自己存手工制作，简单安全可靠
- 不影响Windows系统内已安装的Chrome，便携版的数据存储在自己的目录内


## 原理

利用`GoogleChromePortable.exe`启动器来启动Chrome主程序，所有Chrome用户数据都指向本程序所在的`Data`目录，从而实现和系统安装的Chrome隔离。

## 制作步骤

### 【1】提取启动器

下载[`Google Chrome Portable` https://portableapps.com/apps/internet/google_chrome_portable](https://portableapps.com/apps/internet/google_chrome_portable)  ，不要安装，用7-Zip打开这个压缩包，根目录下面有一个`GoogleChromePortable.exe`文件，提取出来，这个文件就是我们需要的启动器。



> 注：你会发现这个文件的数字签名是`2016-11-19`，生命力顽强的一个软件。

另外这个安装包内有`help.html`，介绍了`GoogleChromePortable.exe`如何使用，和参数，可全部提取出来查看。

你可以不用自己提取，可以下载我提取好的， 373 k大小，可验证签名，[https://github.com/xiangyuecn/Docs/raw/master/Other/自己制作chrome便携版实现多版本共存_files/GoogleChromePortable.exe](https://github.com/xiangyuecn/Docs/raw/master/Other/%E8%87%AA%E5%B7%B1%E5%88%B6%E4%BD%9Cchrome%E4%BE%BF%E6%90%BA%E7%89%88%E5%AE%9E%E7%8E%B0%E5%A4%9A%E7%89%88%E6%9C%AC%E5%85%B1%E5%AD%98_files/GoogleChromePortable.exe)。


### 【2】提取Chrome主程序
下载需要的任意Chrome版本版本离线安装包，你可以自行搜索，这里有一个版本比较全的地址：https://www.chromedownloads.net/chrome64win-stable/ ，下载完后注意检查数字签名。

离线安装包下载好后，不要运行，我们同样用7-Zip打开这个压缩包，会发现里面有一个`chrome.7z`文件，我们把他提取出来。



> 注：如果你打开看到的是`102~`这种，不是`chrome.7z`的话，说明你下载的不是离线安装包，这种是离线升级安装的，从chrome官网下载到的一般是这种。
>
> 另外离线安装包的图标比升级包的丑很多，不信你看下面的图片



### 【3】制作便携版
**步骤：**
1. 新建一个文件夹，用来存放便携版，比如`41`文件夹 (我下载的Chrome 41这个版本)。
2. 复制`GoogleChromePortable.exe`到这个文件夹，可以改名成自己想要的名称，比如`Chrome41.exe`。
3. 新建`App`文件夹，把`chrome.7z`解压到这个目录内，注意只要`Chrome-bin`文件夹，完成后的目录结构应该是`/41/App/Chrome-bin`。

这样就完成制作了，非常简单。双击`GoogleChromePortable.exe (Chrome41.exe)`就能启动这个Chrome了。



第一次运行会在文件夹内生成`Data`目录，里面存放的是这个版本的用户数据，和系统内安装的Chrome不冲突，也不影响。

把`Chrome41.exe`生成一个快捷方式到桌面，多个Chrome想用哪个用哪个，本人独爱`41.0.22x`这个古董版本，因为有很多好用特性是新版本所废弃的。


## 温馨提示

涉及到的所有软件下载完成后记得检查数字签名，如果没有签名或者签名失效，请立即删除，重新去别的地方下载！！！不然本文没有意义。




`GoogleChromePortable.exe`运行后，把浏览器关闭后，这个进程可能不会自动退出，应该是秀逗了，哈哈，正常情况下应该是会和Chrome.exe主进程一块退出。

另外：[用户数据不能在多台电脑之间共享](https://portableapps.com/node/42637)，运行中安装的扩展和cookies等信息在另外一台电脑上打开时将会丢失。便携特性只针对Chrome主程序本身，不含用户数据；多版本共存不受此影响。

本文涉及到Github：https://github.com/xiangyuecn/Docs/tree/master/Other ，里面有本文的所有资源。



# :star:捐赠
如果这个库有帮助到您，请 Star 一下。

你也可以选择使用支付宝给我捐赠：

---

## File: SSL/开源HTML5单文件网页版ACME客户端.md

# 新开源HTML5单文件网页版ACME客户端，可在线申请Let's Encrypt、ZeroSSL免费HTTPS多域名通配符泛域名SSL/TLS证书（RSA/ECC/ECDSA）

之前写了两篇关于HTTPS证书的文章，一篇是本地自签证书，另一篇是Let's Encrypt开始支持免费通配符证书时通过`diafygi/gethttpsforfree`网页来申请证书，步骤均很繁琐；于2022年9月我写了我开发了一个操作非常简单的网页版ACME客户端：[ACME-HTML-Web-Browser-Client](https://xiangyuecn.gitee.io/acme-html-web-browser-client/ACME-HTML-Web-Browser-Client.html)，并在[GitHub](https://github.com/xiangyuecn/ACME-HTML-Web-Browser-Client)、[Gitee](https://gitee.com/xiangyuecn/ACME-HTML-Web-Browser-Client)上进行了开源。

整个源码仅一个静态HTML网页文件，可以直接保存到本地使用，或通过在线网址使用；支持向 `Let's Encrypt`、`ZeroSSL` 等支持 ACME 协议的证书颁发机构，免费申请获得用于 HTTPS 的 SSL/TLS 域名证书（`RSA`、`ECC/ECDSA`），支持多域名和通配符泛域名；只需在现代浏览器上操作即可获得 PEM 格式纯文本的域名证书，不依赖操作系统环境，无需下载和安装软件，无需注册登录，纯手动操作，**只专注于申请获得证书这一件事**。

> **证书过期风险提醒：**由于本网页客户端只能纯手动操作，不支持自动续期，需注意在证书过期前重新生申请新证书（免费证书普遍90天有效期，届时只需重复操作一遍即可），或使用 acme.sh 等客户端自动化续期。




# 开源项目的起源
我自己主要是在Windows里面用的多，并不介意甚至更偏向于纯手动操作获得证书，但官方ACME客户端列表里面的能单纯提供符合我需求的寥寥无几：依赖特定操作系统环境、需要安装特定的运行环境、要自己写代码开发、甚至有的还必须捆绑线上生产环境；**而我需求仅仅是单纯的想获得一个证书文件而已，但带来的是一整套全家桶，很难专注于只申请获得证书这一件事。**

常见的ACME客户端要么是需要`Bash`脚本环境，要么就是不同开发语言的源代码或者第三方库需要自己写代码；早先Let's Encrypt的列表里面还有提供网页版的客户端列表（我在里面找到的`gethttpsforfree`），现在官方因为`“一些浏览器内 ACME 客户端可用，但我们未在此处列出它们，因为它们鼓励手动续订工作流程，这会导致糟糕的用户体验并增加错过续订的风险”`而不再展示网页版客户端列表，我很失望；也有部分支持Windows的桌面程序可以直接使用，比如`win-acme`，不过看了文档也很难上手；有部分客户端或网站甚至会要求注册并登陆账户才能使用，还帮你管理私钥（存储到他们的服务器，恨不得把通讯录都拿走），哈 ~ 呸 ~~

长时间我都是使用`fszlin/certes`这个.Net的库来签发证书，自己C#写的一个控制台程序，双击程序就能直接到达验证阶段，配置好DNS验证记录就能申请到证书文件，但要给别人用就麻烦了，要改C#代码；更重要的是EXE程序还有一个信任的问题，双击一个EXE程序需要承担的风险很大；**网页版就完全不同了，网页本身就带有很大程度的开放性，我们完全可以审查代码是否有加料，网页天然适合做UI，人机交互的友好性远超脚本控制台程序**，对于用不上自动化续期、或自动化在服务器进行部署证书的需求，在网页上进行申请是最理想的操作方式；比如`diafygi/gethttpsforfree`我们完全可以看到他的网页源码，还能通过浏览器控制台完全监控到网络数据流向，最为简单和可靠，虽然这个网页也是开源的，但它需要的操作太繁琐了，早先我还给他提了个我自己在用的简化签名操作的方法 [issues/164](https://github.com/diafygi/gethttpsforfree/issues/164) 。


为了能用上一个符合我心里预期的并且操作简单的网页版ACME客户端，依托于现代浏览器对`RSA`、`ECC`加密、签名的直接支持，**因此我决定自己写！**好在以前积累了点基础的密码学知识，编写本客户端不存在知识盲点，很顺利的写完了全部HTML代码，并进行了开源。

# 项目地址
`GitHub:` [https://github.com/xiangyuecn/ACME-HTML-Web-Browser-Client](https://github.com/xiangyuecn/ACME-HTML-Web-Browser-Client)
`Gitee:` [https://gitee.com/xiangyuecn/ACME-HTML-Web-Browser-Client](https://gitee.com/xiangyuecn/ACME-HTML-Web-Browser-Client)

在线使用本客户端：
`国内访问gitee.io:` [https://xiangyuecn.gitee.io/acme-html-web-browser-client/ACME-HTML-Web-Browser-Client.html](https://xiangyuecn.gitee.io/acme-html-web-browser-client/ACME-HTML-Web-Browser-Client.html)
`不太稳定github.io:` [https://xiangyuecn.github.io/ACME-HTML-Web-Browser-Client/ACME-HTML-Web-Browser-Client.html](https://xiangyuecn.github.io/ACME-HTML-Web-Browser-Client/ACME-HTML-Web-Browser-Client.html)


## 使用方法
这里介绍的是完整的操作流程，请根据步骤进行操作，很容易就能申请得到证书文件；本文图片中显示的数据都是演示用的并非真实数据。

### 第一步：选择Let's Encrypt、ZeroSSL或其他证书颁发机构
根据上面的项目地址，打开本网页客户端（下载保存唯一的这一个html文件并双击打开、或者使用在线网址）；只要是支持ACME协议的证书颁发机构，都可以在第一步操作中填写对应地址，目前默认提供了`Let's Encrypt`、`ZeroSSL`两家的地址，根据自己需要直接选择即可。

> 不同证书颁发机构需要的操作不一定相同，请根据提示进行操作；`Let's Encrypt`直接到第二步操作，`ZeroSSL`由于他们的ACME服务对浏览器支持很不友好存在跨域问题，需要多操作几下（复制源码到他们服务地址页面中运行，消除跨域问题）。



### 第二步：证书配置，填写域名
在网页的第二步中填写需要申请证书的域名（支持多域名、通配符），和密钥等信息配置；不同证书颁发机构需要填写的配置不一定相同，请根据提示进行操作。

**填写域名列表：**比如要给你的域名`test123.com`申请证书，在域名中可以直接填写：`test123.com, *.test123.com`，带了一个通配符，这申请出来的证书在`test123.com`、`www.test123.com`、`app.test123.com`等域名都可以使用。注意：如果需要`xxx.app.test123.com`这种域名也能使用，需增加填写`*.app.test123.com`类似这种才行。

**证书私钥：**建议直接点击创建新的RSA私钥。

**ACME账户私钥：**如果你之前申请过，建议填写之前用过的私钥，没有申请过也同样的创建新的RSA私钥。

**ACME联系邮箱：**填写一个你常用的邮箱，证书颁发机构会在证书到期前给你推送邮件提醒续期，乱填邮箱收不到提醒。

**EAB凭据：**有些ACME服务会要求提供外部账号绑定凭据（`External Account Binding`、`externalAccountBinding`、`externalAccountRequired`），比如ZeroSSL：你可以在ZeroSSL的管理控制台的 Developer 中获得此凭据，所以你需要先注册一个ZeroSSL的账号；Let's Encrypt没有此选项。



### 第三步：完成域名所有权的验证
根据网页内显示的第三步内容，给每个域名配置好合适的域名所有权验证方式；常见的验证方式有：DNS解析中配置TXT记录进行验证、上传文件到服务器进行验证，通配符域名一般仅支持DNS验证；等全部配置完成后开始验证，验证通过后就能下载得到证书了。

**推荐使用DNS验证方式**，只需要登录你的域名解析管理后台，在域名解析中填写子域名TXT记录就行，简单快捷，并且不容易出错；比如`*.test123.com`，我们需要给子域名`_acme-challenge.test123.com`添加一条TXT记录（这一个子域名下可以添加多条TXT记录）。

**通过上传文件来验证**，不太推荐这个方式，因为必须要有服务器，不然ACME访问不到文件就会验证不通过；为了让上传的文件能正常访问，可能需要在服务器内进行一些配置（请自行搜索解决办法），测试URL地址你自己也能正常访问后，才能开始验证。




### 第四步：下载保存证书PEM文件
等域名所有权验证通过后，网页会显示第四步，提供证书文件下载，下载保存好；有3个文件需要下载（都是纯文本，可以用记事本打开）：
1. 证书文件（含完整证书链）
2. 证书私钥文件
3. 日志记录文件，下次申请时直接拖拽此文件进页面，自动填写相同配置

证书文件的后缀`.pem`可以改成`.crt`或`.cer`，这样在Windows里面可以直接双击打开查看此证书。

最后将下载保存的证书和私钥配置到你的网站中即可，不同系统程序的配置方法不同，请自行查找对应配置方法。





# 源码中一些可以把玩的代码
为了在浏览器上实现`RSA`、`ECC`签名操作，源码中用纯js代码实现了：
- `PKCS#1`的解析，PEM格式公钥和私钥
- `PKCS#8`的解析和生成，PEM格式公钥和私钥
- `PKCS#10`的生成，PEM格式证书请求CSR文件(Certification Request)

对应的功能封装在本客户端内的 `X509` 对象中，用 X509.CreateCSR 来生成CSR，用 X509.KeyGenerate 来创建PEM格式密钥，用 X509.KeyParse 来解析PEM格式密钥，用 X509.KeyExport 来导出PEM格式密钥。

这些功能都是根据相应的标准用js代码在二进制层面上实现的，二进制数据操作封装在了 `ASN1` 对象中：实现了 ASN.1 标准的二进制解析和封包，使用 ASN1.ParsePEM 方法可以解析任意的PEM格式密钥或证书。

**请打开本网页客户端，在浏览器控制台内执行一下代码观赏：**
``` javascript
console.log("生成一个512位的RSA私钥 PKCS#8：");
X509.DefaultType2_RSA="512";
privateKey=await new Promise(function(resolve,reject){
	X509.KeyGenerate("RSA", function(pem){resolve(pem)}, function(err){reject(new Error(err))})
});
console.log(privateKey);

console.log("解析一个pem格式密钥：");
keyInfo=await new Promise(function(resolve,reject){
	X509.KeyParse(privateKey, function(info){resolve(info)}, function(err){reject(new Error(err))})
});
console.log(keyInfo);

console.log("导出私钥的公钥 PKCS#8：");
publicKey=X509.KeyExport(keyInfo, true);
console.log(publicKey);

console.log("用私钥创建一个证书请求CSR PKCS#10：");
csr=await new Promise(function(resolve,reject){
	X509.CreateCSR(keyInfo, "test123.com"
		, ["test.com","*.test.com","*.app.test.com","test123.com","*.test123.com"]
		, function(csr){resolve(csr)}, function(err){reject(new Error(err))})
});
console.log(csr);

console.log("用ASN1解析CSR得到二进制结构：");
csrASN1=ASN1.ParsePEM(csr);
console.log(csrASN1);

console.log("用ASN1解析私钥得到二进制结构：");
privateKeyASN1=ASN1.ParsePEM(privateKey);
console.log(privateKeyASN1);

console.log("用ASN1解析公钥得到二进制结构：");
publicKeyASN1=ASN1.ParsePEM(publicKey);
console.log(publicKeyASN1);
```

【END】