前情

最近在做活动编辑器的时候,考虑到运营人员很多都没有图片上传的权限,所以想在活动编辑器里面内置一个这样的功能,让运营可以使用上传控件或者托拽的方式上传图片到服务器,并在上传成功之后,自动把地址放到文本框里。

第一步要做的就是尝试着实现用FTP来上传一个文件到服务器的指定目录,同时基于文件的sha1值来命名上传之后的文件。

手上现有的两种实现路径是NodeJsPython,不过想到最近刚学Python,正需要这样一个机会,就先尝试python的方式。

开发

先是Google了一下,知道了一个ftplib的内置模块可以用来做FTP相关的功能。然后是之前在即刻上有看到余弦发的一个文章,说到用Python一句话查看文件的哈希值,上去找了找,也很快找到了那篇文章里面的相关代码。

折腾一下之后,实现代码如下:

import ftplib
import hashlib
import os

host = 'YOUR HOST'
username = 'YOUR NAME'
pwd = 'YOUR PWD'
port = 21
filePath = 'some/path/to/img.jpg'
serverPath = '/where/to/store/your/file/on/the/server/'

session = ftplib.FTP()

session.connect(host, port)

session.login(username, pwd)

session.cwd(serverPath)

file = open(filePath, 'rb')

filename, file_extension = os.path.splitext(filePath)

str_hash = hashlib.sha1(file.read()).hexdigest()

target_file_name = str_hash[:16]+file_extension

session.storbinary('STOR '+target_file_name, file)

file.close()

session.quit()

代码写完,就顺手执行看了下效果:文件顺利按的传到了预设的目录,也重命名为了指定的文件名。但是,文件的体积是0

调试

在Python的IDEL环境里面使用help命令查看了file相关的各种文档,最后把问题的方向指向了file.read。因为当时在IDEL环境里面,使用file.read读取了要上传的文件内容之后,再次调用file.read时,返回的内容为空。

于是在Google上面用python file read twice作为搜索词搜索了一下。果然找到了问题,文件里面有一个指针指向上一次读取的位置,默认是在最开始,也就是0。file.read会读取文件的全部内容,完成之后,指针就指向文件的最末尾。

所以,在调用过一次file.read之后,再次调用file.read,会从文件最末尾开始获取内容,这获取的内容自然就是空了。而ftp.storbinary在上传文件的时候,内部应该也是会使用类似file.read类似的方法来获取要上传的文件内容并传递给服务器。这个时候也自然就没法正常获取正确的文件内容

解决方式是在file.read之后,使用file.seek(0)来把文件的指针指回最开始。

最终代码:

import ftplib
import hashlib
import os

host = 'YOUR HOST'
username = 'YOUR NAME'
pwd = 'YOUR PWD'
port = 21
filePath = 'some/path/to/img.jpg'
serverPath = '/where/to/store/your/file/on/the/server/'

session = ftplib.FTP()

session.connect(host, port)

session.login(username, pwd)

session.cwd(serverPath)

file = open(filePath, 'rb')

filename, file_extension = os.path.splitext(filePath)

str_hash = hashlib.sha1(file.read()).hexdigest()

target_file_name = str_hash[:16]+file_extension

# 前面的file.read使得file的read cursor指向了文件的尾部
# 如果不手动调整指针的话,后面的文件上传,就会是一个空文件
file.seek(0)

session.storbinary('STOR '+target_file_name, file)

file.close()

session.quit()

参考文档

  1. Python Script Uploading files via FTP
  2. Extracting extension from filename in Python
  3. Why can’t I call read() twice on an open file?
  4. 我是如何 Python 一句话校验软件哈希值的

之前安装过Docker Toolbox来实现在Mac上面跑Docker,后来Docker又出了一个Docker for Mac,好奇心一起就卸了Docker Toolbox,装了它。

愿意尝试的原因是Docker Toolbox主要是用VirtualBox来放container,Mac环境里面相关的docker命令其实都是代理,最终的实现是在VirtualBox里面。而Docker for Mac用的是macOS的一个框架HyperKit来实现的,不需要使用VirtualBox来做中间代理,性能上会有很大的提升。当然,前提是你的系统是升级到了macOS10.11。

顺利安装和启动Docker for Mac之后,在命令行运行docker info命令,会出现如下错误。

could not read CA certificate "/Users/{{username}}/.docker/ca.pem": open /Users/{{username}}/.docker/ca.pem: no such file or directory

在网上找了不少解决方式,

bash_profile

尝试了下这个网址里面说的https://github.com/boot2docker/osx-installer/issues/126
我的用户目录下没有.bash_profile的文件,我是用的zsh,所以在.zsh_rc找了找(安装Docker Toolbox是很久之前的事情了,根本不记得当初怎么安装的),意外的也找到了当初配置的一些DOCKER_*的环境变量。注释,重启zsh,再次运行docker info,还是不行。

docker machine

然后又继续尝试上面网址里面,后续给的方法:

docker-machine regenerate-certs default

 egenerate TLS machine certs?  Warning: this is irreversible. (y/n): y
 Regenerating TLS certificates
 Host does not exist: "default"

失败。

既然是说default不存在,那我就来创建一个

查看了docker-machine相关的命令,刚好有一个create命令,执行了一下:

docker-machine create default
 Creating CA: /Users/{{username}}/.docker/machine/certs/ca.pem
 Creating client certificate: /Users/{{username}}/.docker/machine/certs/cert.pem
 Running pre-create checks...
 Error with pre-create check: "exit status 126"

搜了一下关于exit status 126 的相关错误,说的是没安装VirtualBox,这时候我觉得不对劲了,因为跑Docker for Mac是不需要VirtualBox的啊。

正式看了一下docker machine是什么东西,原来是一个用来方便的管理多种类型的docker主机的一个工具,可以是虚拟机,本地主机和云平台,具体可以看看这个链接:https://yeasy.gitbooks.io/docker_practice/content/machine/usage.html

顺便推荐一下这本电子书《Docker —— 从入门到实践》

看完docker machine的作用,可以看的出来,这和我现在想做的事情和遇到的问题,没有太大干系。我是安装完了Docker的环境,然后在执行docker命令的时候出现了问题,不是在已经有container的时候在维护上出了问题。

环境变量

这时候我开始回到官网来追根溯源了,很幸运的找到一篇深入比较Docker for Mac和Docker Toolbox的文章:Docker for Mac vs. Docker Toolbox

这个对于Docker for Mac和Docker Toolbox各自的实现机制说的十分透彻,同时还意外的有一个Setting up to run Docker for Mac章节,说了如何配置Docker for Mac的环境,在你已经或者曾经安装过Docker Toolbox的情况下。

按照上面说的执行命令

env | grep DOCKER

顺利的找到了所有的DOCKER_*的环境变量

对对应的变量执行 unset

 unset DOCKER_TLS_VERIFY
 unset DOCKER_CERT_PATH
 unset DOCKER_MACHINE_NAME
 unset DOCKER_HOST

再次执行

env | grep DOCKER

保证环境变量已经清除。

然后执行

docker info

一个正常的信息列表出来了

Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 17.03.1-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host ipvlan macvlan null overlay
Swarm: inactive
Runtimes: runc
Default Runtime: runc
...

以上就是整个debug的过程。其实这么折腾,主要还是自己不是很了解docker,得抽空再好好看看上面说的那本电子书才行了。

在PC时代,调用QQ, 微博之类的三方登录,对于前端来说是比较轻松的(如果不需要异步登录的话),而且体验也不错,QQ那边可以自动识别是否登录,对于用户只是一个确认和点击的流程。

但是到了现在,移动互联的时代,这样的三方登录完全都是APP那边前端的事情,Web前端无能为力。毕竟,让用户在一个小设备上的小网页里面输入他已经在登录状态的QQ的账号密码,是挺难为人也挺不现实的。

缘于产品庆庆同学的执着,偶然发现在QQ内置的浏览器里面进行QQ登录的话,是也可以跟PC那样自动识别到已登录状态的,这让事情有了一线曙光。不过这种便利也只能在QQ的内置浏览器里面

下面就来说说如何实现web网页在QQ内置浏览器里面的登录

识别QQ的内置浏览器

首先得识别当前所在的浏览器是不是QQ内置的。

QQ浏览器分两种情况,一个是独立的浏览器产品,一个是内置在QQ里面的webview。识别的方式,用userAgent。

先贴出对应平台对应分类的UA(使用v2ex提供的一个工具页面可以方便的打印UA )

安卓平台下的独立的QQ浏览器

Mozilla/5.0 (Linux; U; Android 4.4.4; zh-cn; HM 1S Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 Chrome/37.0.0.0 MQQBrowser/7.4 Mobile Safari/537.36

安卓平台下的内置的QQ浏览器

Mozilla/5.0 (Linux; Android 4.4.4; HM 1S Build/KTU84P; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043124 Safari/537.36 V1_AND_SQ_6.7.1_500_YYB_D QQ/6.7.1.3105 NetType/WIFI WebP/0.3.0 Pixel/720

IOS平台下的独立的QQ浏览器

Mozilla/5.0 (iPhone 5SGLOBAL; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 MQQBrowser/7.4 Mobile/14E304 Safari/8536.25 MttCustomUA/2 QBWebViewType/1

IOS平台下内置的QQ浏览器(TIM内)

Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Mobile/14E304 QQ/6.5.5.0 TIM/1.0.5.9 V1_IPH_SQ_6.5.5_1_TIM_D Pixel/640 Core/UIWebView NetType/4G QBWebViewType/1

这里因为测试设备有限,没法确切知道所有版本的QQ下的情况,只能等以后慢慢完善。

从上面的UA可以找到两个关键字QQMQQBrowser。本来两个关键字是分开的,独立浏览器里面是MQQBrowser,内置的是QQ,但是这在安卓的内置里面同时实现了。

所以判断方式需要绕一点:先判断MQQBrowser,如果存在,判断QQ是否存在,存在则是内置,不存在则是独立;如果不存在MQQBrowser,则判断QQ是否存在,存在是内置,不存在,就都不是。

不过我们这次的需求只需要判断内置,所以简单,判断QQ是否存在即可:navigator.userAgent.match(/QQ\/([\d.]+)/)

登录的实现

一开始是打算继续用PC时代用的那个登录SDK好实现一个优雅的异步免刷新的登录效果。在实际测试的时候,也的确是可以做到在登录页QQ自动识别状态只需要用户点击登录按钮确认,可是在登录回跳这块卡住了。

在PC,SDK是用window.open新开一个弹窗来实现登录和各种跳转,在新窗口的最终落地页和主窗口通信返回登录成功获取的AccessToken,然后关闭弹出窗口。

但是内置浏览器里面window.open的效果是替换当前网页展示,类似于location.href赋值跳转,这使得最终落地页一直联系不到主窗口,然后卡着了。

很自然的,就想到了用iframe,把window.open打开的地址放到iframe里面,不就可以实现两个window的通信了吗?说干就干,QQ登录界面顺利的展现了,点击登录,结果主界面跳了,跟之前一样,再次无法通信。看来QQ在登录成功之后是用window.top来跳转的。不明白QQ这样做是出于什么考虑的,安全性?可是登录成功的回调地址不是已经被限制了只能是对应域名下的了吗?唯一知道的就是,这个方法行不通……

所以,目前还是只能走以前的老路,通过页面跳转的方式来实现登录。主要的策略就是在QQ登录成功之后的回调页再跳回之前的发起登录的页面。

本来这个需求是由后端来实现,在回跳发起页之前,在cookie里面记录用户的登录信息,这样发起页就能知道用户登录成功了。

不过我这次的需求,有两个麻烦的地方:

  1. 后端目前还没有在移动站点配置登录功能
  2. 我需要知道QQ用户的OpenIDAccessToken

所以改为由后端配置好回调页路由,具体页面里面的跳转和信息记录由前端自行控制。

然后具体的实现步骤是:
1. 用户点击发起页上的QQ登录按钮
2. 前端在后端给的redirect_url里面加上一个search query字段redirect,方便后面回跳回来
3. 前端根据appIdredirect_url整合出跳转QQ登录的地址(https://graph.qq.com/oauth2.0/authorize? response_type=token&client_id={{appId}}&scope=all&display=mobile&redirect_uri={{回调地址}})并跳转
4. 在QQ登录页面,QQ识别到当前用户的登录状态,自动填充信息并添加一键登录按钮
5. 用户点击登录
6. QQ开始回跳到redirect_url并在redirect_url后附加hash query,这个query里面有 access_token expires_in
7. 这个回调页,js抓取token和token过期时间存储在localStorage,同时还在localStorage记录两个个临时的key: TempQQLoginSuccessTempQQLoginSuccessTime
8. 回调页识别search query里面的redirect字段,获取发起页地址并跳转过去。
9. 发起页初始化的时候,查看localStorage里面有没有token和expire_in对应的字段,如果没有则是未登录状态,执行一次退出操作(退出操作是尝试清空回调页记录的四个key,下文同理)。如果存在,则判断当前时间是否超过expire_in对应的时间,如果超过,则执行退出操作。没有超过,则此时用户是登录状态。
10. 确定用户登录之后,再判断回调页记录的两个临时字段是否存在。如果有,判断TempQQLoginSuccessTime存储的时间和当前时间的时间差是否有超过一定的阈值。如果没有超过则说明上一次的发起页有登录行为,可以提示登录成功,如果超过了,则直接显示登录后的状态即可。同时不管超过与否,清空这两个临时key。

以上,整个登录流程就通了,成功的获取了用户的accessToken。

后面再使用accssToken作为参数,以jsonp的方式访问QQ的接口https://graph.qq.com/oauth2.0/me?access_token={{accessToken}}就能顺利的获取到用户的openId。这里要注意的一点是,QQ的这个接口里面callback的函数名写死了是callback,所以在使用jsonp的时候,以Zepto为例,jsonpCallback需要设定为callback

关于这个项目,具体实现代码在这:https://github.com/shuizhongyueming/fe-qq-login,有兴趣的可以看看

之前看曹政的书《你凭什么做好互联网》,里面关于技术人员有一个描述

大部分技术人员分享时候的盲区,自说自话,完全不考虑受众

这里摘取的不全面,不过大体意思是有的,就是很多技术人员会有的糟糕的口才,而我,正巧,也是其中一员。

不同于写作,演讲、分享、辩论,这些口语形式的思想表述,需要你在很短的时间同时掌控好思维和舌头,流畅的展现你的思想,没有编辑的概念。

而我总是想的太快,舌头跟不上,说出来的话和自己真实想表达的意思要打个折。大信息量的涌入使得舌头的性能降低,说出来的话还会有点口齿不清。

很早就意识到这个问题,可是却难有改观。

比如这次的一个活动页开发

因为业务逻辑和交互复杂的不输于一般的单页面应用,所以一改以往收集DOM操作为一个动作的抽象方式,这次用了MVVM架构的一些数据驱动视图的思路,同时还基于Redux的思想做了数据的管理。一番调整之后,自己还是有点小得意的,DOM操作和VM的完美结合。

再说点尴尬的背景,这个活动分移动和PC两个页面,功能一样,UI大体也是类似,但是却由我和同事小磊分别开发……

我知道这是一件很没脸说的事情,两个有经验的前端做了这样没动脑子的决定。虽说我是后加入这个活动开发的,可是在参与进来的时候,花一两小时好好熟悉一下项目,然后再一起讨论分工合作是一个完全合理而且必要的流程。

继续说前面的调整,因为那点小创意,所以我在开发进度上有点领先(UI和很多前期代码是直接从小磊那边拷贝过来的)。小磊那边在管理UI上的数据变动这块遇到了一些问题,业务逻辑有点复杂,各种弹窗,还有多个模块之间的数据通信,纯DOM操作对于掌控全局变化就有点吃力。

这时候为了进度,我尝试跟小磊介绍一下我的处理问题的思路。我想跟他说数据驱动,订阅者,MVVM,React,Redux……

是的,面对一个分享将会带来的荣耀,我一瞬间就想了这么多。打开我的代码,开始了介绍:“这个活动很复杂,所以咱们得用数据驱动的方式开发,你看我这个dataCenter,我把数据都放这里面用getter和action维护,然后action里面发布事件,再看这块,main这块,对数据更新做了响应。再看这块,抽奖回调里面调用action来更新数据。还不错吧,其实这个是借鉴redux里面的思想,store”。话没说完,小磊同志不耐烦了:“越说越懵,所以我这个问题该怎么解决?”

尬了一下,小磊向来耿直。

“那就先不看我这个实现,你这现在是哪有问题了”。是的,我还没弄清楚具体哪出问题了。

后面仔细看了看小磊的代码,指出了一些模版里面基于数据的逻辑判断问题,再给了一个全量更新列表模版(数据量少,就几条)的思路,总算是博得一句“哦,原来这样啊”

一点总结

伤痛中总是伴随着成长,不好好想想说出去的话,早晚是会遭殃。

在这件事上总算对自己糟糕的口才又有一次认识,也知道区分在什么场景下做技术分享,什么场景下是bugFix。做技术分享要精要细,bugFix就不要废话太多,讲究快准狠,以解决问题为只要目的。

PS:值得安慰的事第二天一上班,小磊就过来说:“我昨天晚上仔细想了想,你的那个dataCenter真是太有必要了”

这两天公司开始发力做APP的推广,很多活动页和移动端站点需要加上打开APP的功能,如果没安装则提示用户下载。

首先,基于隐私保护的目的,浏览器是不会提供接口给网页去获取系统上安装了的APP的列表

然后很多产品经理会在提需求的时候有一种洁癖,希望网页上那个可供用户点击的按钮,在已安装APP的时候是打开,在未安装的时候是下载

早先的做法

为了应对这样的需求,前端程序员们想出了一个很棒的方法,利用浏览器进驻后台之后会暂停js的执行这一特性,顺利实现了对是否安装了自己公司APP的探测。具体实现如下:

在用户点击或者进入页面时候,记录一个当前时间time1,然后设置一个300ms的timeout,在timeout的回调里面记录时间time2,接着就用location.href直接指向对应APP已经配置的url scheme。

如果time1time2的时间差超过400ms(考虑到setTimeout本身的误差)就认为安装了APP,不需要处理,否则则是没安装,提示下载或者自动下载。

因为,如果用户安装了对应的APP,那么会自动的跳转到APP的界面,浏览器这边的计时会被中断,等下次用户偶然再使用浏览器的时候,计时继续,会触发setTimeout的回调,不过这个时候time1time2的时间差就远大于300ms了。

如果用户没有安装对应APP就不会发生跳转,setTimeout也就自然在规定时间内运行回调了。

这种方法曾经是非常有效果的,不过现在会有些问题了。

首先是安卓下,有些浏览器,比如UC会在网页试图打开APP的时候阻断这个行为并弹出一个select弹窗,让用户选择是否打开,是否始终打开等等。

UC的select弹窗

但是这个弹窗并没有像alert那样阻塞js的执行,也就是说setTimeout会正常进行,在用户犹豫怎么选择的时候,300ms就过了,页面就自动跳转了下载页。

接着是IOS这边,到IOS10之后,如果已经安装了对应的APP,浏览器也会在网页尝试打开APP的时候弹出一个confirm窗,让用户选择是否打开对应的APP。

已安装:确认窗提示是否打开APP

如果没有安装对应APP,就会实现一个很丑陋的网页无法打开此链家的错误alert窗。

未安装:提示网址无效

而且这两个窗口也是没有阻断js脚本的执行的,setTimeout也是同样正常进行中。

尝试过把300ms的时间改成3000ms,去预想用户可以在3秒内在浏览器的选择窗内做好选择。可是仔细一想,这种预想又是非常不靠谱的,永远没法猜测用户的未知行为,而且3000ms的响应延迟也是非常不好的交互体验。

是时候去调整思路了

如果浏览器不希望我们知道设备上是否已经安装了对应的APP,那就不尝试去知晓。

目前我们的做法是:在用户通过点击或者别的行为触发了打开APP的策略的时候,首先是尝试直接访问url scheme,在这同时,展现一个弹窗,里面包含一个APP的介绍文案,然后底部是两个按钮,下载APP打开APP

下面是天猫的做法,很值得借鉴:

先尝试跳转
同时显示下载和打开

这样做的好处是:如果用户已经安装了APP,那么就直接跳转了,弹窗对用户不会有任何的影响;如果没有安装,或者用户安装了,但是因为不想跳转或者误操作没有跳转,则用户在关闭了浏览器的错误提示之后,看到弹窗,能大概理解之前的浏览器的行为是什么意思,同时可以再次选择打开或者下载APP。

同时,如果是进入页面立马就跳转的行为,最好能有一个次数限制,不然一个高频访问的页面,每进入一次都有这样一个弹窗,明摆着是想赶用户走了啊

从一个Electron项目说起

这两天在折腾用Electron来写一个弹幕助手,方便用PC直播的主播能够看到用户发的弹幕并且进行回复和相关管理操作。

主要的开发任务就是对PC站点已有功能的搬迁和调整。

在最后实现退出功能的时候,希望是在菜单栏上有一个退出按钮,用户点击就能直接退出。

下面是mainProcess里面配置菜单的代码,针对退出登录按钮注册了事件,点击的时候,通知对应的web页去实现退出登录功能。

const template = [
    {
        label: '操作',
        submenu: [
            {
                label: '关闭助手',
                role: 'quit'
            },
            {
                label: '退出登录',
                click: function(){
                    mainWindow.webContents.send('main-process-message', '{action: "quit-login"}');
                }
            }
        ]
    }
];

然后是renderProcess这边监听事件并对应处理

const ipc = require('electron').ipcRenderer;
ipc.on('main-process-message', (event, data) => {
    switch(data.action) {
        case "quit-login": 
            user.quit();
            console.log('quit');
            break;
    }
});

一个意外的错误

项目这块,renderProcess的代码都是用webpack进行编译的。

在renderProcess里面引入electron模块之后,立马就报了一个问题:

ERROR in ./~/.1.6.2@electron/index.js
Module not found: Error: Can't resolve 'fs' in '/xxx/electron-quick-start/node_modules/.1.6.2@electron'
@ ./~/.1.6.2@electron/index.js 1:9-22
@ ./js/index.js

上网看了一圈,发现github上的webpack项目里面有人遇到了类似的问题:

Error: Cannot resolve module ‘fs’ with electron and js

然后底下有人给出了解决方式,在webpack.config.js里面加上target: 'electron'就可以的。

试了一下,发现果然是可以的。

深入了解target属性

解决问题之后,出于好奇,就去webpack官网看了target配置的文档

webpack可以为js的各种不同的宿主环境提供编译功能,为了能正确的进行编译,就需要开发人员在配置里面正确的进行配置。

默认情况下,target的值是web,也就是为类浏览器的环境提供编译。

完整的target属性值及对应作用的列表如下:

拷贝自https://github.com/webpack/webpack.js.org/edit/master/content/configuration/target.md

target Description
async-node Compile for usage in a Node.js-like environment (uses fs and vm to load chunks asynchronously)
~~atom~~ Alias for electron-main
~~electron~~ Alias for electron-main
electron-main Compile for Electron for main process.
electron-renderer Compile for Electron for renderer process, providing a target using JsonpTemplatePlugin, FunctionModulePlugin for browser environments and NodeTargetPlugin and ExternalsPlugin for CommonJS and Electron built-in modules.
node Compile for usage in a Node.js-like environment (uses Node.js require to load chunks)
node-webkit Compile for usage in WebKit and uses JSONP for chunk loading. Allows importing of built-in Node.js modules and nw.gui (experimental)
web Compile for usage in a browser-like environment (default)
webworker Compile as WebWorker

所以,把target设为electron,也就能正确的对Electron环境下的代码进行编译了,不过我的代码是写在renderProcess里面的,所以把targets设置为electron-render才是更加合理的选择。

这个是在一次加班加点完成活动之后,发给设计同学的一个建议,放上来,给大家也看看

以下是原文:

建议的目的:主要是希望通过在设计上的调整,能让前端这边轻松快速的完成活动页的切图部分。一个活动,特别是现在的移动端活动,前端的很多精力都会花费在切图上。

一些事先定义:

前端这边切页面,特别是活动页面,对于一些描述性的不会变动的内容,会切成图片。

对于一些有交互效果,内容会随着时间或者用户操作变化的内容,比如排行榜,比如登录和非登录状态的切换展示区域,比如一些弹出层,是需要切的非常细致。这样才能适应需求的灵活性。

所以希望设计同学们在设计的时候,千万留意则个。在不会变动的区域,可以任意发挥。但是在会变动的区域,则尽量能考虑一下下面的建议。

后面的内容区域特指内容会变动的部分。

背景

背景这块,在页面顶部可以自由发挥,到时候前端直接横切就可以。但是到内容区域之后,就需要注意了。

第一个是,尽量让变动区域和非变动区域之间,没有图像上的连接。因为背景的居中和内容的居中,浏览器的计算方式会有些不一样。如果变动区域和非变动区域必须严密对齐才能好看,就会比较麻烦。

第二个是:在内容区域的左右背景,最好能不要有超出内容区宽度范围的装饰性的图案,这个是可以有,不过切起来麻烦,还望手下留情。

第三个:整个内容区域的背景,如果不是效果需要,尽量保证颜色是一致的,不要渐变也不要分块的颜色。这样就可以不用切背景图,直接用css写一个颜色值实现。

字体

好看的字体是好看,不过在内容区域,文字是直接写上去而不是却的图片。这个时候,就不能肆意的使用字体。

设计的系统上会安装几百个字体。可是大部分的用户的系统上并没有。如果前端按照psd上面的要求设置了一个生僻的字体,在用户那边的效果就没法和设计稿保持一致,甚至会有很大偏差。

微软雅黑,tahoma,arial,Hiragino Sans GB,宋体

这个是前端这边,目前设置的字体展示优先级。设计同学在做内容区这块的时候,字体尽量就用上面这几种。因为他们的安装量比较大,可以保证在大部分用户的浏览器上的展示效果。

按钮

按钮这块,主要的也是字体问题,尽量不要字体的阴影和描边。

另外就是按钮的风格和尺寸,尽量统一,这样前端就可以只切一个按钮然后导出应用。按钮的背景,如果可以的话,就不要渐变色和阴影。这样前端图片都不用切,直接用css就能画出相同效果的按钮了。

移动端设计要注意的一些问题

这块指的web的移动端。原生的可能会有不一样的地方

设计稿的尺寸750

移动端,因为我们的框架的问题,设计的时候最好用750px作为宽度,这样我们好进行自适应的配置。

设计风格简洁

移动端,跟PC的设计有些不一样的地方就是,它的布局不是固定的,会随着用户屏幕的不同,做对应的调整,也就是自适应。

在自适应这块,用css来做比较好,用图片就比较麻烦。因为图片的放大缩小,有些时候不是那么尽如人意的。所以在移动端的活动页面,在内容区域,除非是奖品图,头像等内容性质的图,否则一些装饰性的内容,尽量简洁一点,这样前端就好用css来实现了

布局,多排,不要多列

自适应面对的主要问题就是宽度的变化。而宽度影响最大的就是多列内容的布局。

为了能快速进行活动页面的开发,设计同学在设计活动页面的时候,就尽量减少内容的分列,改列为排。

这样处理起来就比较轻松了。

在13年的时候, 写过一篇博文–关于数据备份. 里面讲到了我悲惨的数据丢失和找回的经历, 也说了一些对数据备份的方案. 关于云备份, 只是大致的说了点.

就在今天, 先是群里面有人发了张照片, 说金山快盘要关闭了

kuaipan-down

一开始是有点不相信的, 因为我从快盘刚出现的时候, 基本就开始使用了, 看着它一个平台一个平台的支持, 性能也是一天天的好, 给的空间也是一天天的大. 中间自己还提过不少的反馈. 而且是金山公司的. 怎么可能突然就要关闭了.

kuaipan-storage

然后就访问官网, 还真是要关闭了, 而且所属公司, 也不知道什么时候变成迅雷了…

对数据的分级

所以我现在就得重新考虑我这几十个G的数据的何去何从了. 也算是来一次”重构”吧.

首先, 就得对数据做一个划分, 不能再跟以前一样, 所有的数据都是大杂烩的放一起了, 存储是要钱的(后面会说到选择收费的云存储)

按隐私,重要性,可维护性,体积来划分:

  • 很隐私的文件: 不能被他人看到, 也不能被政府等第三方随意抽查
  • 比较隐私的文件: 可以接受被第三方做安全和和合法性的检查, 但是不能被抓取和利用文件中的内容. 比如一些工作文件,系统备份
  • 不是隐私文件: 虽然不是隐私文件, 但是也想能够收集起来, 方便查阅
  • 很重要的文件: 绝对不能丢失
  • 比较重要的文件: 平时收集的一些网上的文档和程序. 丢失很可惜, 但是也是可以再次收集的
  • 方便维护的文件: 数量相对比较少, 而且划分的维度不多. 比如手机照片, 基本一个文件夹, 可劲的塞进去都行
  • 不方便维护的文件: 比如媳妇做设计收集的图片和psd, 各种分类各种划分, 人工维护不方便. 还有系统的备份, 手机的备份
  • 大文件: 体积在200M以及往上的文件 软件和视频
  • 中等文件: 1M到20M 多是音乐和图片
  • 小文件: 个人文档, 代码等等

按范围分: 个人文件和公司文件.

按类型分: 图片, 文档, 软件, 字体, 音乐, 视频, 备份, 代码

这些划分不是独立的, 四个维度可以综合的来描述某一类文件.

云存储的分级

云存储的公司很多, 需要一个好的筛选维度去进行划分, 这样才好按照自己的需要进行选择.

按隐私, 持久, 价格和便捷性来划分:

  • 隐私程度: 存储的内容不会被云存储公司自身或者政府翻阅的可能性 国外的大于国内的 要翻墙的大于不要翻墙的
  • 持久: 这个云存储能保证有效的时间, 像这次金山快盘的突然关闭, 来几次会让人崩溃, 所以要挑选能长久有效的云存储. 一般来说, 收费的大于免费的, 价格高的大于价格低的
  • 价格: 今天看了下. Dropbox(€9.99/月 1TB) > GoogleDrive ($1.99/月 100GB) > OneDrive(¥15/月 50GB) ~ 坚果云(30GB起 增量1GB/月 不封顶 ¥16.66/月) > 百度云盘(免费, 存储按T算, 基本算不封顶) = 微云(免费, 存储按T算, 基本算不封顶) = 360云盘(免费, 存储按T算, 基本算不封顶)
  • 便捷性: 速度,是否需要翻墙,对各个系统的支持,对文件的格式的支持. 这个基本上对系统的支持, 对文件格式的支持都差不多. 所以主要看速度和是不是需要翻墙. 个人感觉 OneDrive > 国内云 > Dropbox > GoogleDrive

最终的筛选

从以上的划分, 最后决定如下

  • 针对很隐私很重要的文件(手机的日常照片,视频; 个人的一些重要的有纪念意义的文档), 不论体积和数量, 放Dropbox, 尽量使用它的免费空间. 针对邮件, 邮件附件, 基于Google系列产品构建的文件, 使用GoogleDrive.
  • 针对比较隐私的文件. 对不方便维护的Mac系统备份, 采用自购的移动硬盘, 用Mac的TimeMachine. 对手机, 则使用iPhone的备份到Mac功能. 对工作文件, 比较重要, 往往不方便维护, 是小文件或者中文件, 比如设计用的字体, psd, 图片; 以前早起的没有版本系统的一些没有太大实际意义, 纪念意义更重的代码. 用OneDrive, 如果空间不够, 可以增加容量.
  • 针对比较随意的内容, 比如一些软件备份, 一些电影资源, 一些音乐(ape等高清资源或者一般的mp3) 就用百度云盘, 速度不错, 短期不会倒闭, 而且有算法能对重复内容做识别, 可以免上传.

希望通过这样的划分方式能比较方便又安全的进行个人数据的管理. 如果大家有更好的方案, 也期待分享交流.

一点小广告:

如果大家想用OneDrive而且没有注册过, 可以用这个链接https://onedrive.live.com?invref=031414c742a7d364&invscr=90 进行访问, 这样我也能得到0.5G的奖励^_^

安装的时候,最好能参照此官方教程进行https://docs.docker.com/installation/centos/

升级内核

CentOS6.7的默认内核版本是2.6.32-431.23.3.el6.x86_64 可以通过uname -r命令查看

而docker要求的内核是至少3.10往上

可以参考此篇文章来进行内核升级使用yum快速升级CentOS 6.5内核到 3.10.28

虽然说的是CentOS6.5 但是也同样适用于CentOS6.7

升级完毕之后,重启电脑,执行uname -r命令,确定内核已经正确切换为3.10的

安装docker

安装比较简单

先使用 sudo yum update 来确保所有包是最新的

然后使用官方的curl -sSL https://get.docker.com/ | sh 进行docker的安装

启用和配置docker

如果你的运气不错的话,执行sudo service docker start就可以顺利启动docker。

尝试着跑一个hello-world的image,sudo docker run hello-world。如果一切正常。那后面就不需要看。

很不幸,看到这块的都是有问题了。

问题和解决方法

内网IP被占

如果执行docker -d看到的错误类似如下

INFO[0000] Listening for HTTP on unix (/var/run/docker.sock)
INFO[0000] [graphdriver] using prior storage driver "devicemapper"
WARN[0000] Running modprobe bridge nf_nat failed with message: , error: exit status 1
FATA[0000] Error starting daemon: Error initializing network controller: Error creating default "bridge" network: can't find an address range for interface "docker0"

那可以尝试一下我的一些操作

参考了http://www.baijinping.com/pages/2015/07/19/zai-a-li-yun-shang-yun-xing-dockerfu-wu.htmlhttp://www.cnblogs.com/MicroTeam/p/see-docker-run-in-debian-with-aliyun-ecs.html

编辑网络配置文件 vim /etc/sysconfig/network-scripts/route-eth0

注释掉有172.16.0.0的一行

# 172.16.0.0/12 via 10.116.111.247 dev eth0

执行命令route del -net 172.16.0.0 netmask 255.240.0.0 删除掉已经加载的路由信息

然后执行 /etc/init.d/network restart 重启network服务

最后再次启动docker service start docker 成功!

无法限制docker内存

当然,如果还是很不幸的,又有问题,在启动的时候提示

WARNING: WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.

这是提示无法对docker进行内存限制。

可以修改/etc/grub.conf 在当前使用的内核的kernel项的最后加上cgroup_enable=memory swapaccount=1

reboot

然后 cat /proc/cmdline 就会发现,配置生效了

这下就真的没问题了。久违的hello, world

参考资料

介绍

Array.sort([compareFunction])

对Array里面的元素进行升序排序

关于排序的规则

默认的排序

如果compareFunction没有提供的时候,默认把所有Array里面的元素转换为字符串,然后取第一个字符,比较它们的Unicode值,进行正序排序。

这个转为字符串再比较第一个字符的Unicode值,很关键,很多数字的比较,经常就是这样出错的。


[1,2,3].sort();  // ==> [1,2,3]  正确

[9, 80].sort();  // ==> [9, 80]  错误

[9, 80].sort();  // ==> [80, 9]  正确

上面例子中,数组[9, 80]进行sort的时候,先转为为字符串 “9”, “80”, 再比较第一个字符, “9”和”8″

根据这个比较结果进行排序,所以最后80会在9前面,而不是按照它们的数值大小进行排序的。

想自行测试的,可以看看这个demo(http://www.icondownloader.com/demo/sort-stable.html]

compareFunction的排序

进行sort的时候,如果compareFunction提供了,会往compareFunction传两个数组的元素(类似compareFunction(a, b))

取compareFunction的返回值,如果小于0,则表明a小于b;大于0,则a大于b

最后对结果进行升序排序,如果a小于b,则[a, b]; 如果a大于b 则[b, a]

这里有个技巧,在排序的时候,如果想实现降序排序,可以在compareFuction里面实现类似:return !(a-b);

sort stable

关于排序,有个stable的说法,也就是,排序完成之后,对于值相同的元素,在排序结束之后,能否保留其在排序之前的顺序。

sort stable

具体的概念可以参考维基百科https://en.wikipedia.org/wiki/Sorting_algorithm#Stability

目前的主流浏览器对stable的sort的支持情况如下:

Browser Sort is stable Sort is unstable
Firefox 8 all lengths
Safari 5.1.1 all lengths
Opera 11.52 all lengths
Internet Explorer 6, 7, 8 all lengths
Internet Explorer 9 all lengths
Android 2.3 Browser all lengths
Chrome 15 .. 17.0.942.0 length <= 10 length > 10

可以参考此网站,检测浏览器的sort情况http://ofb.net/~sethml/is-sort-stable.html

sort的效率

可以尝试着对compare function在执行的时候,进行一个计数。这样可以知道每次sort,总共执行了多少次compare function。

我这边写了个demo,有兴趣的可以在自己本机的各个浏览器上测试看看。http://www.icondownloader.com/demo/sort-compare-function-call-times.html

这边我贴出我自己Mac OSX Yosemite 10.10.4下的浏览器的运行结果

## chrome 版本 42.0.2311.135 (64-bit)
Array length is: 5 and sort call compare 7 times;
Array length is: 10 and sort call compare 23 times;
Array length is: 11 and sort call compare 22 times;
Array length is: 12 and sort call compare 19 times;
Array length is: 13 and sort call compare 27 times;
Array length is: 14 and sort call compare 25 times;
Array length is: 15 and sort call compare 24 times;
Array length is: 20 and sort call compare 29 times;
Array length is: 50 and sort call compare 80 times;
Array length is: 100 and sort call compare 206 times;
Array length is: 1000 and sort call compare 1996 times;
Array length is: 10000 and sort call compare 20206 times;

## Safari 版本 8.0.7 (10600.7.7)
Array length is: 5 and sort call compare 8 times;
Array length is: 10 and sort call compare 23 times;
Array length is: 11 and sort call compare 28 times;
Array length is: 12 and sort call compare 31 times;
Array length is: 13 and sort call compare 35 times;
Array length is: 14 and sort call compare 40 times;
Array length is: 15 and sort call compare 44 times;
Array length is: 20 and sort call compare 65 times;
Array length is: 50 and sort call compare 232 times;
Array length is: 100 and sort call compare 568 times;
Array length is: 1000 and sort call compare 8996 times;
Array length is: 10000 and sort call compare 123583 times;

## Firefox (Developer Edition) 40.0a2 (2015-05-29)
Array length is: 5 and sort call compare 6 times;
Array length is: 10 and sort call compare 26 times;
Array length is: 11 and sort call compare 24 times;
Array length is: 12 and sort call compare 33 times;
Array length is: 13 and sort call compare 48 times;
Array length is: 14 and sort call compare 48 times;
Array length is: 15 and sort call compare 37 times;
Array length is: 20 and sort call compare 70 times;
Array length is: 50 and sort call compare 245 times;
Array length is: 100 and sort call compare 574 times;
Array length is: 1000 and sort call compare 8600 times;
Array length is: 10000 and sort call compare 114046 times;

从上面的数据可以看出两个结论:

  1. chrome的执行效率明显高于Safari和Firefox。但是chrome的sort是unstable的,而Safari和Firefox是stable的。是不是可以认为,因为chrome不需要考虑stable,所以提高了执行效率。

  2. 可以看出随着数组长度的增加,比较的次数是指数增加的

用map来提高提高sort的性能

从上面的结论2可以知道,在数组长度很长的时候,compareFunction的调用次数是很多的,这个时候,提高compareFunction的效率就很有必要性了。

现在咱们构建一个数组

var arr = [],
    arrLen = 1000,
    i = 0;

function makeWord(){
    var word = [],
        words = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'],
        wordsLength = words.length,
        arrMinLen = 3,
        arrLen = arrMinLen + Math.floor( Math.random()*9 ),

        i = 0;

    for (i = 0; i < arrLen; i++){
        word.push( words[Math.floor( Math.random()*wordsLength )] );
    }

    return word.join('');
}

for (i = 0; i < arrLen; i++) {
    arr.push( makeWord() );
}

然后比较一下它们小写字母状态下的排序

// 未优化
arr.sort(function(a, b){
    return +(a.toLowerCase() > b.toLowerCase()) || +(a.toLowerCase() === b.toLowerCase())-1;
});

// 优化后

// 用一个临时数组来保存位置和计算后的数值
var mapped = list.map(function(el, i) {
  return { index: i, value: el.toLowerCase() };
})

// 排序这个已经计算后的临时数组
mapped.sort(function(a, b) {
  return +(a.value > b.value) || +(a.value === b.value) - 1;
});

// 根据位置信息 对应映射生成一个排序后的数组
var result = mapped.map(function(el){
  return list[el.index];
});
// 不支持map的时候的兼容方法

var tmpArr = [],
    result = [],
    i = 0,
    len = arr.length;

for (i = 0; i < len; i++) {
    tmpArr[i] = {index: i, value: arr[i].toLowerCase()};
}

tmpArr.sort(function(a, b) {
  return +(a.value > b.value) || +(a.value === b.value) - 1;
});

for (i = 0; i < len; i++) {
    result[i] = arr[tmpArr[i].index];
}

具体的执行效果可以看看jsperf里面的这个地址http://jsperf.com/arraysortperform

参考文章:
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types#Unicode
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#String_literals
+ http://ofb.net/~sethml/is-sort-stable.html
+ http://stackoverflow.com/questions/3026281/array-sort-sorting-stability-in-different-browsers