alinode在centos 7.2服务器安装试用记录

#1 安装zsh | oh-my-zsh

1
2
yum install -y zsh
chsh -s /bin/zsh // 切换到zsh

#2 安装最新版git

1
2
3
4
5
yum install -y epel-release  
rpm -ivh https://centos7.iuscommunity.org/ius-release.rpm
yum list git2u
yum install -y git2u
git --version

#3 安装 oh-my-zsh

1
2
wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh
vim ~/.zshrc // 修改theme为ys

#3.1 安装nginx

1
2
3
4
yum install -y nginx
systemctl enable nginx
systemctl start nginx
// /etc/nginx/nginx.conf

开端口

1
2
3
4
systemctl start firewalld // 很多没开,所以需要开
firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --reload

#3.2 安装mariadb 以10.1老版本为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//先去https://downloads.mariadb.org/mariadb/repositories/ 生成一坨东西

# MariaDB 10.1 CentOS repository list - created 2019-05-10 16:05 UTC
# http://downloads.mariadb.org/mariadb/repositories/
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.1/centos7-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1

// 替换网址成阿里云的
# MariaDB 10.1 CentOS repository list - created 2019-05-10 16:05 UTC
# http://downloads.mariadb.org/mariadb/repositories/
[mariadb]
name = MariaDB
baseurl = http://mirrors.ustc.edu.cn/mariadb/yum/10.1/centos7-amd64/
gpgkey= http://mirrors.ustc.edu.cn/mariadb/yum/RPM-GPG-KEY-MariaDB
gpgcheck=1

// 在 /etc/yum.repos.d/ 下新建MariaDB.repo
cd /etc/yum.repos.d/
vim MariaDB.repo
// 把那坨东西复制进去,保存。然后运行安装命令
yum install -y MariaDB-server MariaDB-client
# 马上启动MariaDB
systemctl start mariadb

# 设置开机自动启动
systemctl enable mariadb
#初始化
/usr/bin/mysql_secure_installation
# 输入root账号密码(没有请直接回车)
Enter current password for root (enter for none):
# 为root设置密码? 按Y
Set root password? [Y/n]
// mimapassword
# 删除匿名用户,按Y
Remove anonymous users? [Y/n]
# 禁止root远程访问,需要远程管理,请按n
Disallow root login remotely? [Y/n]
# 删除test数据库及其访问权限,按Y
Remove test database and access to it? [Y/n]
# 重新加载访问权限,按Y
Reload privilege tables now? [Y/n]
# 修改授权
mysql -uroot -p
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'mimapassword' WITH GRANT OPTION;
FLUSH PRIVILEGES;

#4 安装nvm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

vim ~/.zshrc

// 把下面写入到最后一行
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion

// 还有这一堆,添加淘宝源的
# nvm
export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node

source ~/.zshrc // 刷新配置文件

5 安装tmux

1
2
3
4
5
6
7
8
9
10
# 安装更新
yum update
# 下载rpm源
wget https://centos7.iuscommunity.org/ius-release.rpm
# 安装rpm源
rpm -Uvh ius-release*rpm
# 使用yum 安装软件
yum install tmux2u
# 显示tmux 版本
tmux -V

#5 安装tnvm 看冲突不

1
2
3
4
5
6
7
8
9
10
11
12
13
14
wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash
// 在zshrc加入
vim ~/.zshrc

export TNVM_DIR="/root/.tnvm"
[ -s "$TNVM_DIR/tnvm.sh" ] && . "$TNVM_DIR/tnvm.sh" # This loads tnvm
// 然后
source ~/.zshrc

// 然后
tnvm install alinode-v4.7.2 (相当于10.15.3lts)
tnvm use alinode-v4.7.2

// 貌似没问题

#6 阿里云的性能平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// 安装专用
npm install @alicloud/agenthub -g # 安装 agenthub

// 创建 alinode.json
{
"appid":"*****",
"secret":"dec02***********e886b324f17ed75"
}
// 写入保存,然后启动
agenthub start ~/alinode.json

agenthub list
// 然后启动项目

ENABLE_NODE_LOG=YES pm2 start /data/node/project-test/www/production.js --name project8080

商务洽谈

docker在centos 7.2的安装笔记

安装docker CE (最新版)

1
2
3
4
5
6
7
8
9
// 卸载旧版本
yum remove docker docker-common docker-selinux docker-engine
// 安装依赖组件
yum install -y yum-utils device-mapper-persistent-data lvm2
// 添加安装源
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
// 安装
yum makecache fast
yum install docker-ce // 目前版本 17.09.1-ce, build 19e2cf6

设置阿里云加速

镜像地址自己去阿里云后台取,或者用其他国内镜像

1
2
3
4
5
6
7
8
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://****.mirror.aliyuncs.com"]
}
EOF
systemctl daemon-reload
systemctl restart docker

好了,可以开整了

商务洽谈

前端如何在header夹带token

  • jquery写法1:

直接post

1
2
3
4
5
6
$.ajax({
url: "http://localhost:8080/login",
type: 'GET',
// Fetch the stored token from localStorage and set in the header
headers: {"Authorization": localStorage.getItem('token')}
});
  • jquery写法2:
    从localStorage中捞出保存好的token,然后全局设置夹带
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var token = window.localStorage.getItem('token');

    if (token) {
    $.ajaxSetup({
    headers: {
    'x-access-token': token
    }
    });
    }

商务洽谈

git学习笔记

单机使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#初始
git init // 初始化,每个新项目都要来一次
git add // 修改后的文件加入暂存区
git commit -m 'xxx' // 保存add后的修改
git checktout . // 注意有个点号,会把未add的内容都咔嚓掉,回退到刚add时的状态
git reset head // 回退到未add的状态,这个实际上是 git reset --mixed (add的修改内容回退未add状态,但所有修改内容都还在)

#分支
git checkout dev // 切换到dev分支
git checkout -b dev //新建并切换到dev分支
git stash // 暂时保存dev的现有状态,这样切到其他分支不会把现有的一些未add 已add的修改也切过去。
git stash pop //把保存的状态恢复(如果之前dev有add,恢复回来需要重新add)
git branch -d br1 //删除br1分支

#切版本
git reset --soft commit_id // 暂存区保留((所有修改内容都还在,add修改内容不会回退状态),切到某个版本
git reset --hard commit_id // 暂存区不保留(add的修改内容,未add的修改内容都不在了),切到某个版本
git reflog show branch // 如果reset了不该reset的版本,可以用这个命令查看commit提交记录,越新的越下面,然后使用类似reset HEAD@{1}方法切回去
git checkout commit_id // 会导致 'detached head' 游离头指针的情况出现,这个时候会离开分支,不在任何一个分支上,然后head指向commit_id。此时还是可以进行add和commit
git merge // 把游离头指针以及此时的commit给合并回来

#查看对比
git diff --cache //暂存区和最新快照对比
git diff --cache commit_id //暂存区和快照对比
git diff commit_id commit_id // 快照和快照对比
git diff commit_id // 工作区和快照对比
git diff //工作区和暂存区对比
git diff HEAD //工作区和最新快照对比

#修改
git commit --amend -m 'xxxx' //可以修改最后一次提交的内容

#删除
git rm file //删除但有记录
git rm -f file //强制删除但有记录

#重命名
git mv oldname newname

#合并
git merge br1 // 把br1的分支合并到当前分支(比如master)
// 如果出现冲突,修改冲突文件保存,然后重新add冲突文件,重新commit就ok了

#远程
git remote add xxx https://xxx/yyy/test.git //添加远程地址,取名xxx
git remote -v // 查看远程地址
git push -u xxx orgin master // 推送代码到远程服务器xxx上,-u可以添加分支名字,这里是本地的orgin分支传到远程的master分支

git clone https://xxx/yyy/test.git // 从某仓库搞代码,顺便把仓库地址设置为远程地址,默认名字orgin

git pull // 从远程仓库拉代码,别人push了新的,我这边就拉
// 可能会有冲突,有冲突改掉,add && commit就行。

抄袭别人的

1
2
3
4
5
6
7
8
9
10
11
12
13
git add PATH  // 增加跟踪文件
git reset PATH // 取消跟踪文件
git commit // 提交
git reset HEAD~1 // 撤销提交
git checkout -b BRANCH // 创建分支并切换到分支
git branch BRANCH // 创建分支但不切换到分支
git push -u REMOTE BRANCH // 推送分支并设置上游
git pull -t REMOTE BRANCH // 拉取分支并设置上游
git checkout --ours PATH && git add -- PATH // 使用本地文件解决merge冲突
git checkout --theirs PATH && git add -- PATH // 使用本地文件解决rebase冲突
git checkout BRANCH PATH // 使用
git show-ref
git ls-remote

商务洽谈

微信昵称不完全显示解决方案

微信昵称的emoji来源有2个,一个是softbank类型的,一个是识别完整的Unicode类型。
比如 猪头表情,微信回传的utf8编码为EE 84 8B,转为code码为57611,16进制的话就是E10B,刚好是softbank的E10B的表情。E10B在Unicode编码里面属于私有部分(E000-F8FF),直接在html会显示框框
然后圆形月亮脸的表情,微信回传的utf8编码为F0 9F 8C 9D,这个是直接在Unicode编码表上列出的。
所以即便使用正确的数据库类型(mysql的utf8mb4)将微信昵称完整的保存下来,最后输出到网页上,也不一定能显示完全,能显示的只有unicode编码表上的那一部分,softbank所属表情无法显示。为了解决这个问题,需要将softbank所属表情编码给转换成unicode对应的表情编码,可以使用emoji.js这个库进行转换。
代码很简单,引入emoji.js库之后:

1
htmlText = jEmoji.softbankToUnified(nickname)

即可显示。不过这个显示出来的表情,可能和手机原有表情长相不太一致,html版本和app版差距较大,但好歹能显示了。


emoji.js库还有一种做法,可以将html版表情通过css和对应图片转换为app版,代码也简单,在原有基础上包一层即可。

1
html = jEmoji.unifiedToHTML(jEmoji.softbankToUnified(nickname))

注意这里输出的是html代码,需要用innerHtml的办法在网页上显示。

参考链接:
unicode表
emoji转换js

商务洽谈

Let’s Encrypt免费https证书证书申请与配置

https 证书申请与配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# 注:"www.yuntap.com"是我的域名,wwwroot那个是web静态目录,替换成自己的即可

// 安装acme
curl https://get.acme.sh | sh

// 重启bash
source ~/.zshrc

// 申请证书 域名+可访问web根目录
acme.sh --issue -d www.yuntap.com -w /data/wwwroot/www.yuntap.com


[Thu Jul 5 15:48:58 CST 2018] Registering account
[Thu Jul 5 15:48:59 CST 2018] Registered
[Thu Jul 5 15:48:59 CST 2018] ACCOUNT_THUMBPRINT='mh6jcBtaB6ozjEOa5rPpNhD5vpZPJJDUb9So3r9uK-Y'
[Thu Jul 5 15:48:59 CST 2018] Creating domain key
[Thu Jul 5 15:48:59 CST 2018] The domain key is here: /root/.acme.sh/www.yuntap.com/www.yuntap.com.key
[Thu Jul 5 15:48:59 CST 2018] Single domain='www.yuntap.com'
[Thu Jul 5 15:48:59 CST 2018] Getting domain auth token for each domain
[Thu Jul 5 15:48:59 CST 2018] Getting webroot for domain='www.yuntap.com'
[Thu Jul 5 15:48:59 CST 2018] Getting new-authz for domain='www.yuntap.com'
[Thu Jul 5 15:49:00 CST 2018] The new-authz request is ok.
[Thu Jul 5 15:49:00 CST 2018] Verifying:www.yuntap.com
[Thu Jul 5 15:49:04 CST 2018] Success
[Thu Jul 5 15:49:04 CST 2018] Verify finished, start to sign.
[Thu Jul 5 15:49:05 CST 2018] Cert success.
-----BEGIN CERTIFICATE-----

│MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
│ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODA3MDUwNjQ5MDRaFw0x
....(省略)
│MusElN6AVE3tjIQI
│-----END CERTIFICATE-----
[Thu Jul 5 15:49:05 CST 2018] Your cert is in /root/.acme.sh/www.yuntap.com/www.yuntap.com.cer
[Thu Jul 5 15:49:05 CST 2018] Your cert key is in /root/.acme.sh/www.yuntap.com/www.yuntap.com.key
[Thu Jul 5 15:49:05 CST 2018] The intermediate CA cert is in /root/.acme.sh/www.yuntap.com/ca.cer
[Thu Jul 5 15:49:05 CST 2018] And the full chain certs is there: /root/.acme.sh/www.yuntap.com/fullchain.cer

// 把证书挪到/etc目录
cd /etc
mkdir -p nginx/ssl
cd nginx/ssl
cp /root/.acme.sh/www.yuntap.com/www.yuntap.com.key /etc/nginx/ssl/www.yuntap.com.key
cp /root/.acme.sh/www.yuntap.com/fullchain.cer /etc/nginx/ssl/fullchain.cer


// 配置自动更新
acme.sh --installcert -d www.yuntap.com \
--keypath /etc/nginx/ssl/www.yuntap.com.key \
--fullchainpath /etc/nginx/ssl/fullchain.cer \
--reloadcmd "service nginx force-reload"


// 生成openssl的pem文件
openssl dhparam -out /root/.acme.sh/www.yuntap.com/dhparam.pem 2048


// nginx 配置

http {
# 新增
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
# 兼容其他老浏览器的 ssl_ciphers 设置请访问 https://wiki.mozilla.org/Security/Server_Side_TLS

server {

// 在具体的vhost里面加
listen 80 default_server;
# 新增
listen 443 ssl;
ssl_certificate /root/.acme.sh/www.yuntap.com/fullchain.cer;
ssl_certificate_key /root/.acme.sh/www.yuntap.com/www.yuntap.com.key;
# ssl_dhparam
ssl_dhparam /root/.acme.sh/www.yuntap.com/dhparam.pem;

# 其他省略
}
}


// 重启nginx
service nginx restart

// 检测定时任务对不对
crontab -l
// 有下面这条表示成功
25 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

// 运行更新流程测试
/root/.acme.sh//acme.sh --cron -f

// 如果显示:
...
[Fri Jul 6 10:49:15 CST 2018] Your cert is in /root/.acme.sh/www.yuntap.com/www.yuntap.com.cer
[Fri Jul 6 10:49:15 CST 2018] Your cert key is in /root/.acme.sh/www.yuntap.com/www.yuntap.com.key
[Fri Jul 6 10:49:15 CST 2018] The intermediate CA cert is in /root/.acme.sh/www.yuntap.com/ca.cer
[Fri Jul 6 10:49:15 CST 2018] And the full chain certs is there: /root/.acme.sh/www.yuntap.com/fullchai
n.cer
[Fri Jul 6 10:49:15 CST 2018] Installing key to:/etc/nginx/ssl/www.yuntap.com.key
[Fri Jul 6 10:49:15 CST 2018] Installing full chain to:/etc/nginx/ssl/fullchain.cer
[Fri Jul 6 10:49:15 CST 2018] Run reload cmd: service nginx force-reload
[ OK ]
[Fri Jul 6 10:49:16 CST 2018] Reload success
[Fri Jul 6 10:49:16 CST 2018] ===End cron===

// 表示成功

商务洽谈

浅析Listview组件03之一堆属性

前言:【我不生产代码,我只是官方案例的搬运工,当然还是改了点内容的,假装它是原创的吧】刷新数据搞定之后,现在需要研究ListView的那一堆属性了,比如有和样式有关的头部、底部、段(section)标题之类,还有一些事件回调方法等。

属性之样式

先贴demo链接:demo v5地址
运行出来的界面是这样的(界面有点丑,海涵):

这里是一次性把头部底部段标题都给搞定,所以相对来说内容要稍微多一点,但都比较容易理解。

提供数据及附属对象

首先我们需要可供显示的数据(data),这个数据的构成就比之前那个最简demo要复杂不少。因为它需要包含两类数据,一个是段标题(SectionHeader),一个是每段的实际数据集(rowDatas),数据集包含rowData和可能会有的rowID。ListView并没有定死我们这个data的格式是怎样的,但也有一定的规律在里面,我们等会还需要自己写方法来指定怎么取段标题以及具体数据。我现在提供的格式是这样的:

1
2
3
4
5
var data = {
Sone: [1001, 1002, 1003, 1004],
Stwo: [2001, 2002, 2003, 2004],
Sthree: {up:3001,down:3002,left:3003, right:3004}
};

这个data是一个对象,段标题就是对象的key(即Sone、Stwo),每个段的数据集是这个key的值,类型是数组(也可以是对象,比如第三段,其中rowID就是up、down这些key,rowData是对应的值3001、3002等),可以对照上面的界面看看。
然后我们还需要提供两个附属的数组,即段标题数组,和段内容的相关key(rowID)的数组,它们长这样:

1
2
var sectionIDs = ['Sone','Stwo','Sthree'];
var rowIDs = [[0,1,2,3],[3,2,1,0],['up','down','left','right']]

注意这个一定要和data的结构对应清楚,否则一会取不到值。rowIDs这个二维数组里面的每个数组都对应着每段数据集的key(rowID),如果数据集是数组,那么其rowID就是数组的下标,如果是对象,那就是对象的key。

新建dataSource对象

这次我们新建dataSource的方法也多点内容:

1
2
3
4
5
6
var ds = new ListView.DataSource({
getRowData: this.getRowData,
getSectionHeaderData: this.getSectionData,
rowHasChanged: (row1, row2) => row1 !== row2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});

那两个HasChanged方法就不用多说了,大家都知道干嘛的。我们来说说前面的两个方法getRowData(获取行数据),getSectionHeaderData(获取段标题),这个需要根据我们自己提供的数据构成来定制相应的方法。我这里方法是这么写的:

1
2
3
getRowData(dataBlob, sectionID, rowID){
return dataBlob[sectionID][rowID];
}

在这个获取行数据的方法中,dataBlob参数就是我们的data数据,sectionID就是我们刚提供的sectionIDs数组里面的值,rowID是我们提供的rowIDs的值。也就是说,我们返回的 dataBlob[sectionID][rowID]就是实际的行数据(rowData),比如data[‘Sone’][0]或者data[‘Sthree’][‘left’]

1
2
3
getSectionData(dataBlob, sectionID ){
return sectionID;
}

这个更简单,就是返回我们的段标题,这是从sectionIDs数组里面捞出来的。

赋值给state

这个赋值的方法也多了点东西:

1
2
3
this.state = {
dataSource: ds.cloneWithRowsAndSections(data, sectionIDs, rowIDs)
};

clone方法换成了参数更多的cloneWithRowsAndSections,它的参数就是我们之前提供的数据以及相关两个附属的数组。

ListView调用

这个组件调用是这么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<ListView
dataSource={this.state.dataSource}
style={styles.listview}
onChangeVisibleRows={(visibleRows, changedRows) => console.log({visibleRows, changedRows})}
renderHeader={this.renderHeader}
renderFooter={this.renderFooter}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}

initialListSize={10}
pageSize={10}
scrollRenderAheadDistance={2000}
/>

先重点关注那几个render相关的方法,renderHeader和renderFooter只需要提供两个返回组件界面的的方法就行,届时它们会渲染成相应顶栏和底栏(貌似没什么实际用途……)。
renderSectionHeader有两个参数,sectionData和sectionID,前者是先前我们写的那个getSectionData方法返回的内容,后者是我们提供的sectionIDs 数组的内容,我这个demo里面两个值是同一个东西。这个方法需要返回一个组件界面。
renderRow可以用三个参数,rowData, sectionID, rowID。分别是行数据,以及sectionIDs的对应内容和rowIDs数组的对应内容。他也需要返回一个组件界面。

然后我们就可以运行demo看效果了。

##利用默认实现简化代码
我估计大家都觉得这个demo理解起来不难,但那个数据的提供有点烦人,data这个是必须提供的,没法简化。但sectionIDs数组和rowIDs的二维数组是个什么鬼,数据多了手写会爆掉的吧。
其实如果我们不提供sectionIDs和rowIDs,cloneWithRowsAndSections这个方法的源代码里面是有默认实现的,默认方法返回的数据,和我们刚才手动提供的数据其实是一样的(当然,那个rowIDs里面的[3,2,1,0]会按[0,1,2,3]的顺序排列)。你们可以把demo的
cloneWithRowsAndSections方法中的后两个参数给去掉,不会影响任何效果。
另外,getRowData的相关代码也可以去掉,因为它也有默认实现,和demo里面提供的代码是一模一样的。getSectionData也有默认实现,不过它和我demo的代码有点不一样,它的默认实现是这样的:

1
2
3
getSectionData(dataBlob, sectionID ){
return dataBlob[sectionID];
}

如果要去掉,那么我的renderSectionHeader的代码里面,组件的Text部分就不能返回sectionData而是sectionID了(嗯,这个是我自己写demo的时候傻逼了,明明可以直接用sectionID的,懒得改demo了,大家明白意思就行了)。

这里为了让大家能对ListView的实现有更完整的理解,所以我一开始把代码都加上去了。如果没有其他定制需求,那么上面说的4个地方,你都可以删掉,使用ListView提供的默认实现。即便有定制需求,相信理解完全的你们也可以随便修改相关的代码。

属性之其他

renderSeparator
在每行之间夹一个类似分割线的组件,用法和renderHeader类似,写个组件方法,比如名为

1
2
3
4
5
6
7
renderSeparator(sectionID, rowID, highlightRow){
return(
<TouchableHighlight style={styles.separator} underlayColor='#dddddd'>
<Text>separator[{sectionID}][{rowID}]</Text>
</TouchableHighlight>
);
}

然后在ListView方面里面调用即可:

1
renderSeparator={this.renderSeparator}

不过你这样直接调用的话,会有个黄色警告出来。

然后我想消灭这个警告,却发现这特么是个坑,我网上搜了一圈儿,没看到有能用的办法能取消这个警告。 (哪位仁兄知道怎么解决还望留言)
唔,找到方法了,在返回的组件那里加个key:

1
2
3
4
5
6
7
renderSeparator(sectionID, rowID, highlightRow){
return(
<TouchableHighlight key={sectionID+rowID} style={styles.separator} underlayColor='#dddddd'>
<Text>separator[{sectionID}][{rowID}]</Text>
</TouchableHighlight>
);
}

至于为什么key是{sectionID+rowID},恐怕只有问源码了(现在源码看得一知半解),我也是从别人那里问来,试验成功。
追加:key={${sectionID}-${rowID}} 也是可以的,这是官网文档示例写的,用这个吧。

initialListSize
官方文档解释是:指定在组件刚挂载的时候渲染多少行数据。我测试了一下,比如设置为1,进入页面的时候能比较明显的看到先显示了第一个section的第一个行内容,然后才把后面剩下的给显示出来,影响用户体验。所以这个你自己看设计稿里面一屏上有多少行来设置吧。不设置的话,默认是10。
onEndReached
onEndReachedThreshold
这两个一起的,先设置好onEndReachedThreshold数据(这个是点数,比如iphone6的高度就是667个点),当屏幕底部距离列表底部距离为这个点数的时候,就会调用onEndReached指定的方法。就是说,你设置onEndReachedThreshold 的值为100,列表底部在屏幕下面(没显示出来),距离屏幕底部还有100点的距离时,就会触发onEndReached。不太好表达,试试就知道了,另外那个值可以设置为负的。
pageSize
每次事件循环(帧)时渲染的行数。默认是1,官方解释说,如果你的row并不独占一行,比如设定row的样式为小方块,屏幕上的一行可以排下好几个row。像这种情况,就需要把pageSize改高点,我大概测试了一下,好像并没有什么视觉上明显的不同。
renderScrollComponent
官方解释:指定一个函数,在其中返回一个可以滚动的组件。ListView将会在该组件内部进行渲染。默认情况下会返回一个包含指定属性的ScrollView。
说实话我没看懂这个解释,网上也没找到除了官方解释之外的详细解释,然后在源代码里面翻了一下,我理解成这样:ListView本来就是基于Scrollview做出来的一个特例。这个renderScrollComponent就是指定它是基于哪个Scrollview来渲染,默认实现就是一个最简单的Scrollview,代码是这样的:

1
renderScrollComponent: props => <ScrollView {...props} />

如果你想让你的Listview基于另外某个定制的ScrollView来实现,那就用这个属性另外指定一个ScrollView吧(应该是这个意思,不对的地方请指正)。

未完待续
scrollRenderAheadDistance
onChangeVisibleRows
removeClippedSubviews

浅析Listview组件02之刷新数据

前言:最简demo做出来了,开始增加玩法。首先是刷新数据,一般情况下,这种列表型的内容,都是会变化的。有些是因为用户的筛选或者其他操作,导致列表发生了变化。有些是因为数据本身就是从网络服务器获取的,当服务器发生了变化,这边获取的具体数据也会发生变化,从而需要让内容也发生变化。那么,怎么让Listview的界面因为数据的变化而变化呢?

修改dataSource刷新界面

继续先看个demo:demo v2地址
数据改变后刷新界面其实很简单,在React-native中,凡是要涉及到刷新界面的,一般都会和state打交道(另外还有个是父组件传props)。我们的最简demo中,一开始就设置了一个叫做dataSource的state,使用setState修改这个dataSource,我们的界面就会发生变化。比如这样:

1
2
3
4
5
6
7
changeds(){
console.log('ds data changing...');
data2 =[1,2,3];
this.setState(
{dataSource:ds.cloneWithRows(data2)}
);
}

我创建了一个按钮,给了个方法changeds,在这个方法中,先给了个新数组data2,然后调用setSate方法对dataSource重新赋值。此时界面就会刷新,这里重新赋值的方法是

1
ds.cloneWithRows(data2)

如果你把这个方法修改为:

1
this.state.dataSource.cloneWithRows(data2)

也是有效的,见demo v3地址。第二个按钮就是用的这个方法,界面效果看起来貌似没有任何区别。那你肯定要问,这两个真的没区别吗?哪个才是最好的?要解释这个,就需要扯到之前没扯到的rowHasChanged方法了。

rowHasChanged方法

先把代码贴出来:

1
2
3
4
5
var ds = new ListView.DataSource({
rowHasChanged: (r1, r2) =>{
return r1 !== r2
}
});

相信大家刚接触这个ListView的时候,都会对这个rowHasChanged方法有很大的疑惑。这个方法有啥用?里面一定要这么写吗?为了知道它的秘密,我给这个方法加了点东西。

1
2
3
4
5
6
7
8
var ds = new ListView.DataSource({
rowHasChanged: (r1, r2) =>{
if(r1 !== r2){
console.log('rowHasChanged!')
}else {
console.log('rowHasNotChanged')}
return r1 !== r2
}});

就是在之前的基础上,添加了一个判断输出log语句,看它啥时候会调用,调用时会输出什么。这些内容在我的demov3中已经添加好了,同时还给按钮和renderRow方法也写了log语句,测试结果是这样的:
首先按下第一个按钮,界面由1000,2000,3000变成了1,2,3 log语句输出为:

1
2
3
4
ds data changing...
renderRow is 1
renderRow is 2
renderRow is 3

然后重新reload这个app,按下第二个按钮,输出为:

1
2
3
4
5
6
7
state ds data changing...
rowHasChanged!
rowHasChanged!
rowHasChanged!
renderRow is 1
renderRow is 2
renderRow is 3

再次reload这个app,先按第一个按钮,再按第二个按钮,输出为:

1
2
3
4
5
6
7
8
ds data changing...
renderRow is 1
renderRow is 2
renderRow is 3
state ds data changing...
rowHasNotChanged
rowHasNotChanged
rowHasNotChanged

从测试结果我们看出,调用ds.cloneWithRows(data2)给state赋值(第一个按钮)并刷新界面,并不会触发rowHasChanged方法。它只有在调用this.state.dataSource.cloneWithRows(data2)给state赋值(第二个按钮)刷新时,才会被触发。而且,这个触发会根据data数据的变化与否返回不同的值,同时影响是否重新渲染每行的组件。你看我们的第三次测试,按第一个按钮用data2的值刷新页面后,再按第二个按钮,rowHasChanged返回了和第二次测试不一样的值,同时组件并没有被刷新。
我们都知道,ListView是一个消耗性能的大户,因为他需要同时刷新n个组件。如果能够在更新数据的时候,先比较一下新旧数据的不同,只刷新改变的那一行组件,可以极大的降低因刷新更多组件带来的性能消耗。通过上面的测试我们知道这个rowHasChanged方法就是用来判断新旧数据的异同的,就是用来优化Listview性能的。所以我们使用ListView最好使用

1
this.state.dataSource.cloneWithRows(data2)

来刷新state。这里我只是用比较简单的测试来给大家讲了为什么要用rowHasChanged,那个r1和r2就代表不同时期的同一行的数据,两者相等就代表不用刷新,返回false,两者不等就返回true,让本行组件刷新。至于再细节一点,有兴趣的朋友还是去翻翻源代码吧。

this.state.dataSource.cloneWithRows()的坑

我们刚知道ListView刷新界面的正确方式,就是使用this.state.dataSource.cloneWithRows(data)方法来更新数据,但这个方法有个坑。使用不慎会出大问题,我给大家演示一下。
刚才我更新数据是用的完全不同的两个data数组,我现在不这么干,我就直接修改第一个data的内容,然后用这个data来更新界面,见demo v4链接
这个demo在v3的基础上又加了一个按钮,按钮把data的第一个数给改了,然后setState刷新。

1
2
3
4
5
6
7
changeds3(){
console.log('state ds data changing problem...');
data[0] = 1 ;
this.setState(
{dataSource:this.state.dataSource.cloneWithRows(data)}
);
}

你会发现虽然按钮上方显示data本身的已经变成 1 2000 3000 了。但最上面我们展示ListView的地方完全没变化。这个坑就在这里,使用this.state.dataSource.cloneWithRows(data)更新数据的时候,参数data不能是原有数组对象上修改,必须是一个新的数组。当然,如果你使用ds.cloneWithRows方法来刷新界面,其参数是可以用原数组对象的,但这样用又没法提升ListView的性能。

总结

  • 刷新数据界面,使用setSate方法更新dataSource数据源。
  • dataSource数据使用this.state.dataSource.cloneWithRows方法赋值
  • this.state.dataSource.cloneWithRows方法的参数必须是新的数组对象

浅析Listview组件01之最简实例

前言:Navigator简单告一段落(复杂的以后再慢慢整,实际上是目前整不出来),接下来捣鼓捣鼓ListView,这也是个大家都会用到的重量级组件。这玩意儿因为说明比较少,所以好多初学者都整得来简单的,但稍微改点需求就有点凌乱了。尤其是偶尔还要掺杂Navigator一起上,外加上那个坑爹的this在捣乱,就更加懵比了。所以我打算和Navigator那样,从最简单的例子开始,慢慢加需求完成代码,同时慢慢分析坑点,争取理解透,免得摔坑里半天爬不起来。

最简demo

老规矩:demo地址
这个最简demo是从官方文档里面抄的。

界面很丑,将就看了,ListView像这样调用的:

1
2
3
4
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{rowData}</Text>}
/>

通过上面代码我们可以看出,一个ListView组件至少需要两个东西。一个是数据源dataSource,一个是每行的渲染组件renderRow。
首先是数据源dataSource,它需要一个创建好的ListView.DataSource的实例,创建代码如下:

1
2
let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {dataSource: ds.cloneWithRows(['row 1', 'row 2','row 3'])}

先new出一个ListView.DataSource(里面那个rowHasChanged一会再说,先抄着用)的实例。使用该实例的cloneWithRows方法创建出需要的dataSource。cloneWithRows方法的参数就是你真正的数据,这个数据的格式一般是个数组,或者字符串也行(一般不会用这个……)。数组中的每个值,就是ListView每行能够调用的值。
然后是renderRow,顾名思义,就是渲染一行(或生成一行)。也就是说,数据源提供了n行的数据,然后我这个renderRow就把每行的数据拿出来,变成一行行组件,然后从上到下排列出来给大家看。所以renderRow需要一个能返回组件对象的函数方法,比如我这里就返回一个最简单的Text组件,然后函数的参数可以使用以下这几个:rowData, sectionID, rowID, highlightRow。demo中使用了rowData,表示每行的数据对象(就是之前数组里面的一个值),剩下的参数等会用到再说。

换一种说法就是,我有一坨数据(数组),通过上面的DataSource方法以及cloneWithRows方法变成了Listview能够调用的数据源,然后再使用renderRow方法一行行渲染成界面给大家看。

好了,最简单的ListView已经搭出来了,接下来一边加内容一边解释之前没说到的东西。

浅析react-native的Navigator组件03之动画切换

前言:【初级内容】传数据搞定了,接下来是这个动画切换了。前面传数据,因为理论上对象里面啥都能装,所以只需要能传个对象就行了。关于动画切换就没有那么好的事情了,大家的需求会更加多样化。比如希望每个页面都换一种切换方式,比如希望能修改默认的动画,更高级的想自己定制动画等。有需求就实现,当然也只能简单实现……

默认动画介绍

在初始化语句中,那句

1
2
3
4
5
configureScene={
(route)=> {
return Navigator.SceneConfigs.VerticalDownSwipeJump;
}
}

就是用来指定切换动画的,默认动画都写在Navigator.SceneConfigs这个对象里面。目前默认动画有如下几种:

1
2
3
4
5
6
7
8
9
PushFromRight //push时候新页面从右边滑进来,pop时当前页往右边滑走。支持ios的屏幕左边缘往右滑的返回(调用pop,以下简称边缘返回),但不知道安卓是啥情况。
FloatFromRight //和PushFromRight 是一样的(至少0.17版的源代码是一样的,不知道之后的版本改没改)
FloatFromLeft //和FloatFromRight 方向是反过来的,支持边缘返回。
FloatFromBottom //push时新页面从下往上滑,pop就反过来。支持手势的下拉返回(调用pop方法)
FloatFromBottomAndroid //动画方向和FloatFromBottom是一样的,但动画效果很细微,只能看到很短的滑动就切换了。不支持手势。
FadeAndroid //渐隐式切换,不支持手势。
HorizontalSwipeJump //push时候新页面从右边滑进来,pop时当前页往右边滑走,支持手势左右滑动切换(但需要你进入过新页面,就是route栈里面已经有内容,因为手势触发的是jumpFoward和jumpback的方法)
VerticalUpSwipeJump //push的时候新页面从下往上滑进来,pop时当前页从上往下滑走。支持手势从左往右滑返回(jumpBack)。
VerticalDownSwipeJump ////push的时候新页面从上往下滑进来,pop时当前页从下往上滑走。支持手势从左往右滑返回(jumpBack)。

切换时指定动画

demo1: 链接 (基于0.17,ios,跳第二页和第三页分别用了不同的动画)
demo2: 链接 (遍历出SceneConfigs里面所有的切换动画跳转第二页)
这个需求实现起来很简单,就是利用之前那个传递属性的功能,把SceneConfigs的动画名称传给configureScene就行了。比如我把这个动画属性取名为scene。初始化代码修改成这样:

1
2
3
4
5
6
7
configureScene={
(route)=> {
return route.scene || Navigator.SceneConfigs.FadeAndroid;
}
}

//加了个route.scene,如果我们没有传递route.scene的值,就使用后面这个FadeAndroid作为默认切换动画。

其他页面想要临时指定切换动画时,修改push代码如下:

1
2
3
4
5
navigator.push({
component:Secondpage,
scene: Navigator.SceneConfigs.FloatFromLeft,
})
//这样push页面的时候会使用FloatFromLeft的动画进行切换。记得当前页面需要导入Navigator组件。

修改默认动画

对于切换动画的自定义的需求,我估计应该有不少。自己写的测试demo倒是无所谓,但如果是商业产品,丢出来给大家用的,要求(或称逼格)高一点的,大都会让你改。怎么改呢?
最直接的方法,就是修改NavigatorSceneConfigs.js这个文件,它放在你的项目文件的node_modules/react-native/Libraries/CustomComponents/Navigator/的目录下面,里面就是这个切换动画的整个实现源代码(注,Navigator的实现代码也在里面,有兴趣的可以看看),理论上来说你看哪个不顺眼直接改就是了,我先来试试。
代码里面大概分了几个部分,最前面是定义animation动画的一些方法,然后是关于手势(Gesture)的一些方法,最后才是NavigatorSceneConfigs这个对象的完成,对象每个key的值就是把动画和手势结合在一起,凑成一个具体切换动画的对象。前面动画和手势的代码目前还搞不太懂,只能改改这个结合部分。具体切换动画对象的示例代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FloatFromBottom: {
...BaseConfig, // 先继承一个基础切换动画的设定
gestures: { // 覆盖基础设定里面的手势部分
pop: { //这个表示调用pop方法时候的手势是是怎样的
...BaseLeftToRightGesture,
edgeHitWidth: 150,
direction: 'top-to-bottom',
fullDistance: SCREEN_HEIGHT,
}
},
animationInterpolators: { //这个表示页面切入(push)和页面切出(一般是pop)时的动画应该是什么样子
into: buildStyleInterpolator(FromTheFront), //切入时调用哪段动画
out: buildStyleInterpolator(ToTheBack), //切出时调用哪段动画
},

当然,里面的那些参数也不能乱改,比如切入的动画,在默认的实现里面,你基本上就只能用FromXXXX系列,而切出去的时候只能用ToThexxx或者xxToTheXXX。比如我们想把前面哪个FloateFromRight的特效换一下:

1
2
3
4
5
6
7
8
FloatFromRight: {
...BaseConfig,
animationInterpolators: {
into: buildStyleInterpolator(FadeIn),
out: buildStyleInterpolator(ToTheLeft),
},
// We will want to customize this soon
},

再试试是不是就和默认的不一样了,如果这里需要再扯清楚点,就必须要扯到animation和guesture这两个可能比Navigator更复杂的玩意儿了,一两篇都写不完那种。所以这次就到此为止吧,有兴趣的可以自己去看源代码。

这里代码里面又出现了神奇的三个点,它还是ES7那个展开对象的功能,就说把BaseConfig的给展开,同时,如果后面有重名的Key,后面的会把BaseConfig里面的内容给覆盖掉,就刚好可以让我们拿来做修改功能的需求。