. DNS介绍
1.1什么是域名
域名(Domain Name) ,简称域名、网域,是由一串用点分隔的名字组成的Intemet上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位。具有独一无二,不可重复的特性。
1.2 什么是DNS?
域名系统(Domain Name System,缩写: DNS)是互联网的一项服务。域名解析是把域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务。IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成。可以理解为DNS就是翻译官。
正向解析: 域名 --> IP地址
反向解析: IP地址 --> 域名 //邮件服务会用到。
1.3 域名的组成和分类
常见格式: www.baidu.com
完整格式: www.baidu.com.
. : 根域 ,可省略不写 ,全球13台 ,一台主根 ,其他都是辅根
com : 顶级域, 由ICANN 组织指定和管理。
分类:
国家地区域名: cn (中国) 、hk (香港) 、sg (新加坡)等
通用顶级域名: com (商业机构) I org (非营利组织) 、edu (教育机构)等。
新通用顶级域名: red (红色、热情) 、top (顶级、高端)等
baidu:二级域(注册域) ,可由个人或组织申请注册。
www: 三级域(子域) ,服务器网站名代表。
主机名: s1.www.atguigu.com.中的s1就是主机名,一般用来表示具体某一台主机。 //不常见
2. 域名解析过程
1. 客户机首先查看查找本地hosts文件,如果有则返回,否则进行下一步
2. 客户机查看本地缓存,是否存在本条目的缓存,如果有则直接返回,否则进行下一步。
3. 将请求转发给指向的DNS服务器。
4. 查看域名是否本地解析,是则本地解析返回,否则进行下一步。
5. 本地DNS服务器首先在缓存中查找,有则返回,无则进行下一步。 \这里的缓存是从其他dns服务器学习来的
6. 向全球13个根域服务器发起DNS请求,根域返回org域的地址列表。
7. 使用某一个org域的IP地址,发起DNS请求, org域返回kernel域服务器地址列表。
8. 使用某一个kernel域IP地址,发起DNS请求, kernel域返回www.kernel.org主机的IP地址,本地DNS服务收到后,返回给客户机,并在本地DNS服务器保存一份。
为了安全性,DNS服务器不是随便搭建的,我们最多搭建一个简单的DNS服务器。
3. DNS软件信息
软件名称
bind
服务名称
named
软件端口
UDP 53 数据通信(域名解析)
TCP 53 数据同步 (主从同步)
配置文件:
主配置文件: /etc/nameed.conf (服务器运行参数)
区域配置文件: /etc/named.rfc1912.zones (服务器解析的区域配置,正反向区域定义信息)
数据配置文件: /var/named/xx.xx (主机名和IP地址的对应解析关系,及主从同步信息)
记录类型:
A: | 地址记录,用来指定域名的IPv4地址的记录 |
CNAME: | 将域名指向另一个域名,再由另一个域名提供IP地址,就需要添加CNAME记录 |
TXT: | 可填写任何东西,长度限制255。绝大多数的TXT记录是用来做SPF的(反垃圾邮件) |
NS: | 域名服务器记录,如果需要把子域名教给其他DNS服务商解析,就需要添加NS记录。 |
AAAA: | 地址记录,用来指定域名的IPv6地址的记录 |
MX: | 邮件交换记录,如果需要设置邮箱,让邮箱能收到邮件,就需要添加MX记录。 |
4. DNS 实验搭建
4.1 DNS基本服务搭建
环境准备
ip | 节点 | 主机名 |
10.30.59.193 | master | dns1 |
10.30.59.194 | 测试 | dns2 |
基础准备
# 修改主机名
[root@localhost ~]# hostnamectl set-hostname dns1
[root@localhost ~]# bash
# 配置本地yum源
[root@dns1 ~]# mv /etc/yum.repos.d/* /media/
[root@dns1 ~]# vi /etc/yum.repos.d/local.repo
[centos]
name=centos
baseurl=file:///opt/centos
gpgcheck=0
enabled=1
[root@dns1 ~]# mkdir -p /opt/centos
[root@dns1 ~]# mount /dev/sr0 /opt/centos/
mount: /dev/sr0 is write-protected, mounting read-only
[root@dns1 ~]# yum repolist
Loaded plugins: fastestmirror
centos | 3.6 kB 00:00:00
(1/2): centos/group_gz | 155 kB 00:00:00
(2/2): centos/primary_db | 2.8 MB 00:00:00
Determining fastest mirrors
repo id repo name status
centos centos 3,723
repolist: 3,723
# 关闭防火墙
[root@dns1 ~]# systemctl stop firewalld
[root@dns1 ~]# systemctl disable firewalld
Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service.
Removed symlink /etc/systemd/system/basic.target.wants/firewalld.service.
[root@dns1 ~]# setenforce 0
[root@dns1 ~]# vi /etc/selinux/config
SELINUX=disabled
# 安装常用工具
[root@dns1 ~]# yum install -y vim net-tools bash-c*
服务搭建
# 安装服务
[root@dns1 ~]# yum -y install bind
# 配置注意事项 所有内容以;结尾 ,, 大括号两侧内用空格分隔
# 配置主配置文件
[root@dns1 named]# vim /etc/named.conf
options {
listen-on port 53 { any; }; //设置服务器监听网卡,any 所有的
listen-on-v6 port 53 { ::1; };
directory "/var/named"; // 数据文件保存位置
dump-file "/var/named/data/cache_dump.db";
statistics-file "/var/named/data/named_stats.txt";
memstatistics-file "/var/named/data/named_mem_stats.txt";
allow-query { any; }; //设置访问服务器的客户端地址, any 所有
include "/etc/named.rfc1912.zones"; // 最下面有一行配置文件,指定了区域配置文件。
# 配置区域配置文件
[root@dns1 ~]# vim /etc/named.rfc1912.zones
zone "bilibili.com" IN {
type master;
file "bilibili.localhost";
allow-update { none; };
};
zone "59.30.10.in-addr.arpa" IN {
type master;
file "bilibili.loopback";
allow-update { none; };
};
[root@dns1 ~]# cd /var/named/
[root@dns1 named]# ls
data dynamic named.ca named.empty named.localhost named.loopback slaves
# 注意与区域配置文件相一致
[root@dns1 named]# cp -a named.localhost bilibili.localhost
[root@dns1 named]# cp -a named.loopback bilibili.loopback
# 正向解析
[root@dns1 named]# vim bilibili.localhost
$TTL 1D
@ IN SOA bilibili.com. rname.invalid. (
0 ; serial
1D ; refresh
1H ; retry
1W ; expire
3H ) ; minimum
NS dns.bilibili.com.
dns A 10.30.59.193
www A 10.30.59.195
# 反向解析
[root@dns1 named]# vim bilibili.loopback
$TTL 1D
@ IN SOA bilibili.com. rname.invalid. (
0 ; serial
1D ; refresh
1H ; retry
1W ; expire
3H ) ; minimum
NS dns.bilibili.com.
93 PTR dns.bilibili.com.
95 PTR www.bilibili.com.
# 启动服务
[root@dns1 named]# systemctl restart named
[root@dns1 named]# netstat -ntlp |grep named
tcp 0 0 10.30.59.193:53 0.0.0.0:* LISTEN 12550/named
tcp 0 0 127.0.0.1:53 0.0.0.0:* LISTEN 12550/named
tcp 0 0 127.0.0.1:953 0.0.0.0:* LISTEN 12550/named
tcp6 0 0 ::1:53 :::* LISTEN 12550/named
tcp6 0 0 ::1:953 :::* LISTEN 12550/named
测试
# 使用第二台虚拟机, 配置dns为DNS服务器,测试
[root@localhost ~]# hostnamectl set-hostname dns2
[root@localhost ~]# bash
[root@dns2 ~]# vi /etc/sysconfig/network-scripts/ifcfg-eno16780032
DNS1=10.30.59.193
[root@dns2 ~]# systemctl restart network
# bilibili地址显示为10.30.59.195,说明配置成功。
[root@dns2 ~]# ping www.bilibili.com
PING www.bilibili.com (10.30.59.195) 56(84) bytes of data.
From 10.30.59.194 icmp_seq=1 Destination Host Unreachable
From 10.30.59.194 icmp_seq=2 Destination Host Unreachable
From 10.30.59.194 icmp_seq=3 Destination Host Unreachable
From 10.30.59.194 icmp_seq=4 Destination Host Unreachable
4.2主从DNS服务器
目的:
减轻主服务器的压力,备份
环境准备:
centos7-1511,关闭防火墙 selinux
ip | 节点 | 主机名 |
10.30.59.193 | master | dns1 |
10.30.59.194 | slave | dns2 |
10.30.59.195 | ceshi | localhost |
# 主从两个节点
# 配置本地yum源
[root@dns1 ~]# cat /etc/yum.repos.d/local.repo
[centos]
name=centos
baseurl=file:///opt/centos
gpgcheck=0
enabled=1
[root@dns2 ~]# cat /etc/yum.repos.d/local.repo
[centos]
name=centos
baseurl=file:///opt/centos
gpgcheck=0
enabled=1
# 安装服务
[root@dns1 ~]# yum install -y bind
[root@dns2 ~]# yum install -y bind
主节点配置
# 主节点配置
[root@dns1 ~]# vim /etc/named.conf
options {
listen-on port 53 { 10.30.59.193; };
listen-on-v6 port 53 { ::1; };
directory "/var/named";
dump-file "/var/named/data/cache_dump.db";
statistics-file "/var/named/data/named_stats.txt";
memstatistics-file "/var/named/data/named_mem_stats.txt";
allow-query { any; };
# 区域配置文件只保留一个模板文件
[root@dns1 ~]# vim /etc/named.rfc1912.zones
zone "bilibili.com" IN {
type master;
file "bilibili.localhost";
allow-update { 10.30.59.194; };
};
# 修改正向解析配置文件 相对于基本服务,这里多配置一个serial充当版本作用
[root@dns1 ~]# cd /var/named/
[root@dns1 named]# cp -a named.localhost bilibili.localhost
[root@dns1 named]# vim bilibili.localhost
$TTL 1D
@ IN SOA bilibili.com. rname.invalid. (
20211208 ; serial
1D ; refresh
1H ; retry
1W ; expire
3H ) ; minimum
NS dns.bilibili.com.
dns A 10.30.59.193
www A 10.30.59.195
[root@dns1 named]# service named restart
Redirecting to /bin/systemctl restart named.service
从节点配置
[root@dns2 ~]# vim /etc/named.conf
10 options {
11 listen-on port 53 { 10.30.59.194; };
12 listen-on-v6 port 53 { ::1; };
13 directory "/var/named";
14 dump-file "/var/named/data/cache_dump.db";
15 statistics-file "/var/named/data/named_stats.txt";
16 memstatistics-file "/var/named/data/named_mem_stats.tx t";
17 allow-query { any; };
# 配置区域配置文件 只保留一个模板文件
[root@dns2 ~]# vim /etc/named.rfc1912.zones
zone "bilibili.com" IN {
type slave;
masters { 10.30.59.193; };
file "slaves/bilibili.localhost";
allow-update { none; };
};
# 从服务器不需要配置解析文件,会自动同步master节点服务器文件到slaves目录下
# 启动前,确认为空目录
[root@dns2 ~]# ls /var/named/slaves/
# 启动服务
[root@dns2 ~]# service named start
Redirecting to /bin/systemctl start named.service
[root@dns2 ~]# ls /var/named/slaves/
bilibili.localhost
验证结果
给测试机配置dns为从服务器的地址
[root@localhost ~]# vim /etc/sysconfig/network-scripts/ifcfg-eno16780032
# 从节点的地址
DNS1=10.30.59.194
[root@localhost ~]# systemctl restart netwoprk
Failed to restart netwoprk.service: Unit netwoprk.service failed to load: No such file or directory.
[root@localhost ~]# systemctl restart network
[root@localhost ~]# ping www.bilibili.com
PING www.bilibili.com (10.30.59.195) 56(84) bytes of data.
64 bytes from 10.30.59.195: icmp_seq=1 ttl=64 time=0.028 ms
64 bytes from 10.30.59.195: icmp_seq=2 ttl=64 time=0.042 ms
64 bytes from 10.30.59.195: icmp_seq=3 ttl=64 time=0.040 ms
^C
--- www.bilibili.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev=0.028/0.036/0.042/0.009 ms
4.3DNS缓存服务器
目的:
加快解析速度,提高工作效率
实验软件:
dnsmasq
操作:
接上一个环境继续操作,把从服务器当作缓存服务器。
# 停掉从服务器
[root@dns2 ~]# service named stop
Redirecting to /bin/systemctl stop named.service
# 安装dnsmasq 可能已经安装。
yum install -y dnsmasq
# 修改配置文件
[root@dns2 ~]# vim /etc/dnsmasq.conf
domain=bilibili.com
server=10.30.59.193
cache-size=150
# 重启服务
[root@dns2 ~]# service dnsmasq restart
Redirecting to /bin/systemctl restart dnsmasq.service
# 测试 如果没有nslookup命令,下载bind-utils
[root@localhost ~]# nslookup www.bilibili.com
Server: 10.30.59.194
Address: 10.30.59.194#53
Name: www.bilibili.com
Address: 10.30.59.195
# 关掉主服务器
[root@dns1 ~]# systemctl stop named
# 再次测试 非权威回答
[root@localhost ~]# nslookup www.bilibili.com
Server: 10.30.59.194
Address: 10.30.59.194#53
Non-authoritative answer:
Name: www.bilibili.com
Address: 10.30.59.195
请求顺序
# 客户端从缓存服务器请求, 缓存服务器没有,去主服务查找, 主服务器没启动,测试没反应, 启动主服务器,再次测试, 缓存服务器再次请求主服务器,获取域名,返回给客户端。
# 主服务器没启动
[root@localhost ~]# nslookup dns.bilibili.com
^C
# 启动主服务器
[root@dns1 ~]# systemctl start named
[root@localhost ~]# nslookup dns.bilibili.com
Server: 10.30.59.194
Address: 10.30.59.194#53
Name: dns.bilibili.com
Address: 10.30.59.193
# 再次关闭,有缓存可以解析到。
[root@dns1 ~]# systemctl stop named
[root@localhost ~]# nslookup dns.bilibili.com
Server: 10.30.59.194
Address: 10.30.59.194#53
Non-authoritative answer:
Name: dns.bilibili.com
Address: 10.30.59.193
4.4 智能DNS(分离解析)
目的:
NDS分离解析即将相同域名解析为不同的IP地址,实现网络中一些网站为了让用户有更好的体验效果解析速度更快,就把来自不通运营商的用户解析到相对应的服务器,这样就大大提升了访问速度。
实验环境:
配置一台apache服务器,两个网卡模拟内外网, 两台测试机从内网外网分别访问。dns能够正确解析(内网地址访问解析内网地址,外网地址访问解析外网地址)。
节点 | IP | 主机名 | 备注 |
内网测试机 | 192.168.100.10 | int | 内外网选择网络模式:内: vmnat1 192.168.100.0/24外: vmnat 8 192.168.200.0/24 |
外网测试机 | 192.168.200.10 | ext | |
apache | 192.168.100.20 192.168.200.20 | apache | |
dns+路由 | 192.168.100.30 192.168.200.30 | dns |
实验大纲:
1、安装bind软件
2、内核配置文件开启路由转发,修改/etc/sysctl.conf
3、修改主配置文件/etc/named.conf 配置any,视图
4、生成自己定义的区域配置文件。
5、配置数据文件
内网正向解析,外网正向解析
6、重启服务
7、效果测试
nslookup
基础环境准备:
修改主机名 配置ip 所有节点关闭防火墙、selinux、配置本地yum。
# 内网测试机
[root@localhost ~]# hostnamectl set-hostname net1
[root@localhost ~]# bash
[root@int ~]# cat /etc/sysconfig/network-scripts/ifcfg-eno16777736
TYPE=Ethernet
BOOTPROTO=static
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
IPV6_FAILURE_FATAL=no
NAME=eno16777736
ONBOOT=yes
IPADDR=192.168.100.10
NETMASK=255.255.255.0
GATEWAY=192.168.100.30
DNS1=192.168.100.30
# 外网测试机
[root@localhost ~]# hostnamectl set-hostname ext
[root@localhost ~]# bash
[root@ext ~]# cat /etc/sysconfig/network-scripts/ifcfg-eno16777736
TYPE=Ethernet
BOOTPROTO=static
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
IPV6_FAILURE_FATAL=no
NAME=eno16777736
ONBOOT=yes
IPADDR=192.168.200.10
NETMASK=255.255.255.0
GATEWAY=192.168.200.2
DNS1=192.168.200.30
# 配置双网卡出了一个问题: 一开始本着第一块网卡仅主机模式,然后第二块网卡net模式,就调整第一块网卡为仅主机模式,新加的第二块网卡为net模式, 结果网络不行,就重新配置为第一块网卡为net模式,第二块网卡为net模式。 不影响本案例。
# apache
[root@localhost ~]# hostnamectl set-hostname apache
[root@localhost ~]# bash
[root@apache ~]# cat /etc/sysconfig/network-scripts/ifcfg-eno16777736
TYPE=Ethernet
BOOTPROTO=static
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
IPV6_FAILURE_FATAL=no
NAME=eno16777736
ONBOOT=yes
IPADDR=192.168.200.20
NETMASK=255.255.255.0
GATEWAY=192.168.200.2
DNS1=114.114.114.114
[root@apache ~]# cat /etc/sysconfig/network-scripts/ifcfg-eno33554984
TYPE=Ethernet
BOOTPROTO=static
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
NAME=eno33554984
ONBOOT=yes
IPADDR=192.168.100.20
NETMASK=255.255.255.0
# dns
[root@localhost ~]# hostnamectl set-hostname dns
[root@localhost ~]# bash
[root@dns ~]# cat /etc/sysconfig/network-scripts/ifcfg-eno16777736
TYPE=Ethernet
BOOTPROTO=static
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
IPV6_FAILURE_FATAL=no
NAME=eno16777736
ONBOOT=yes
IPADDR=192.168.200.30
NETMASK=255.255.255.0
GATEWAY=192.168.200.2
DNS1=114.114.114.114
[root@dns ~]# cat /etc/sysconfig/network-scripts/ifcfg-eno33554984
TYPE=Ethernet
BOOTPROTO=static
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
IPV6_FAILURE_FATAL=no
NAME=eno33554984
ONBOOT=yes
IPADDR=192.168.100.30
NETMASK=255.255.255.0
# apache 节点安装httpd
[root@apache ~]# yum install -y httpd
[root@apache ~]# vim /var/www/html/index.html
dns分离解析验证成功!
[root@apache ~]# systemctl start httpd
[root@apache ~]# systemctl enable httpd
Created symlink from /etc/systemd/system/multi-user.target.wants/httpd.service to /usr/lib/systemd/system/httpd.service.
# dns节点安装bind。
[root@dns ~]# yum install -y bind
测试一下直接地址访问看看有没有问题
分离解析配置
# dns节点永久开启路由转发
[root@dns ~]# vim /etc/sysctl.conf
net.ipv4.ip_forward=1
[root@dns ~]# sysctl -p
net.ipv4.ip_forward=1
#修改主配置文件
# 两个any 监听任意地址
# 加两个视图 分别监听内网和其他地址 ,视图匹配规则为自上而下匹配,注意顺序。 match-client匹配地址, include 指定区域配置文件。
[root@dns ~]# vim /etc/named.conf
listen-on port 53 { any; };
allow-query { any; };
view lan {
match-clients { 192.168.100.0/24; };
zone "." IN {
type hint;
file "named.ca";
};
include "/etc/lan.zones";
};
view wan {
match-clients { any; };
zone "." IN {
type hint;
file "named.ca";
};
include "/etc/wan.zones";
};
#include "/etc/named.rfc1912.zones";
# 修改区域配置文件
[root@dns ~]# cp -a /etc/named.rfc1912.zones /etc/lan.zones
[root@dns ~]# vim /etc/lan.zones
zone "bilibili.com" IN {
type master;
file "lan.localhost";
allow-update { none; };
};
[root@dns ~]# cp -a /etc/lan.zones /etc/wan.zones
[root@dns ~]# vim /etc/wan.zones
zone "bilibili.com" IN {
type master;
file "wan.localhost";
allow-update { none; };
};
# 修改解析文件
[root@dns etc]# cd /var/named/
[root@dns named]# ls
data dynamic named.ca named.empty named.localhost named.loopback slaves
[root@dns named]# cp -a named.localhost lan.localhost
[root@dns named]# vim lan.localhost
$TTL 1D
@ IN SOA bilibili.com. rname.invalid. (
0 ; serial
1D ; refresh
1H ; retry
1W ; expire
3H ) ; minimum
NS dns.bilibili.com.
dns A 192.168.100.30
www A 192.168.100.20
[root@dns named]# cp -a lan.localhost wan.localhost
[root@dns named]# vim wan.localhost
$TTL 1D
@ IN SOA bilibili.com. rname.invalid. (
0 ; serial
1D ; refresh
1H ; retry
1W ; expire
3H ) ; minimum
NS dns.bilibili.com.
dns A 192.168.200.30
www A 192.168.200.20
测试
# 启动服务
[root@dns named]# systemctl start named
# 内网机测试
[root@int ~]# curl www.bilibili.com
dns分离解析验证成功! \\ 查看是否显示你在主页显示的内容。
# 外网机测试
[root@ext ~]# curl www.bilibili.com
dns分离解析验证成功! \\查看是否同理
# 再使用nslookup测试
# 两个节点都下载
[root@ext ~]# yum install -y bind-utils
[root@int ~]# nslookup www.bilibili.com
Server: 192.168.100.30
Address: 192.168.100.30#53
Name: www.bilibili.com
Address: 192.168.100.20
[root@ext ~]# nslookup www.bilibili.com
Server: 192.168.200.30
Address: 192.168.200.30#53
Name: www.bilibili.com
Address: 192.168.200.20
# 可以发现内外网使用的dns地址不同,并且解析出来的ip不同。实验成功!
如果ip更多,就多写视图,多谢区域配置文件与解析文件。
拓展知识: 邮件服务
读前建议先了解以下知识点:
如果你对前面提到的先备知识有所了解, 你应该知道 JavaScript 中的函数其实是一种对象。 而作为对象, 函数是可以有方法的, 包括非常强大的 `Apply`, `Call`以及 `Bind` 方法。一方面, Apply 方法和 Call 方法的用途几乎相同, 在 JavaScript 中被频繁使用于方法借用和明确 this 关键字指向等场景. 我们也将 Apply 用于参数可变的函数; 在后文中你将会对此有更多的了解。 另一方面, 我们会使用 Bind 来给方法指定 this 指向值或者函数柯里化 (currying functions)。
我们会讨论在 JavaScript 中使用这三种方法的每一个场景。 Apply 和 Call 方法是在 ECMAScript 3 标准中出现的(可以在IE 6, 7, 8 以及现代浏览器中使用), ECAMScript 5 标准添加了 Bind 这个方法。由于三种方法都非常强大, 开发时你一定会用到其中一个。让我们先从 Bind 方法说起。
Bind 方法
我们用 Bind() 来实现在指明函数内部 this 指向的情况下去调用该函数, 换句话说, bind() 允许我们非常简单的在函数或者方法被调用时绑定 this 到指定对象上。
当我们在一个方法中用到了 this, 而这个方法调用于一个接收器对象, 我们会需要使用到 bind() 方法; 在这种情况下, 由于 this 不一定完全如我们所期待的绑定在目标对象上, 程序有时便会出错。
Bind 允许我们明确指定方法中的 this 指向
当以下按钮被点击的时候, 文本输入框会被随机填入一个名字。
```
// <button>Get Random Person</button>
// <input type="text">
var user={
data :[
{name:"T. Woods", age:37},
{name:"P. Mickelson", age:43}
],
clickHandler:function(event) {
var randomNum=((Math.random () * 2 | 0) + 1) - 1; // random number between 0 and 1
// 从 data 数组中随机选取一个名字填入 input 框内
$("input").val(this.data[randomNum].name + " " + this.data[randomNum].age);
}
}
// 给点击事件添加一个事件处理器
$("button").click(user.clickHandler);
```
当你点击按钮时, 会发现一个报错信息: 因为 clickHandler() 方法中的 this 绑定的是按钮 HTML 内容的上下文, 因为这才是 clickHandler 方法的执行时的调用对象。
在 JavaScript 中这种问题比较常见, JavaScript 框架中例如 Backbone.js, jQuery 都自动为我们做好了绑定的工作, 所以在使用时 this 总是可以绑定到我们所期望的那个对象上。
为了解决之前例子中存在的问题, 我们利用 bind() 方法将 `$("button").click(user.clickHandler);` 换成以下形式:
```
$("button").click(user.clickHandler.bind(user));
```
Tips: 再考虑另一个方法来修复 this 的值: 你可以给 click() 方法传递一个匿名回调函数, jQuery 会将匿名函数的 this 绑定到按钮对象上.bind() 函数在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。你可以部份地在脚本开头加入以下代码,就能使它运作,让不支持的浏览器也能使用 bind() 功能。
```
if (!Function.prototype.bind) {
Function.prototype.bind=function(oThis) {
if (typeof this !=="function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs=Array.prototype.slice.call(arguments, 1),
fToBind=this, // 此处的 this 指向目标函数
fNOP=function() {},
fBound=function() {
return fToBind.apply(this instanceof fNOP
? this // 此处 this 为 调用 new obj() 时所生成的 obj 本身
: oThis || this, // 若 oThis 无效则将 fBound 绑定到 this
// 将通过 bind 传递的参数和调用时传递的参数进行合并, 并作为最终的参数传递
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 将目标函数的原型对象拷贝到新函数中,因为目标函数有可能被当作构造函数使用
fNOP.prototype=this.prototype;
fBound.prototype=new fNOP();
return fBound;
};
}
```
继续之前的例子, 如果我们将包含 this 的方法赋值给一个变量, 那么 this 的指向也会绑定到另一个对象上, 如下所示:
```
// 全局变量 data
var data=[
{name:"Samantha", age:12},
{name:"Alexis", age:14}
]
var user={
// 局部变量 data
data :[
{name:"T. Woods", age:37},
{name:"P. Mickelson", age:43}
],
showData:function(event) {
var randomNum=((Math.random () * 2 | 0) + 1) - 1; // random number between 0 and 1
console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
}
}
// 将 user 对象的 showData 方法赋值给一个变量
var showDataVar=user.showData;
showDataVar(); // Samantha 12 (来自全局变量数组而非局部变量数组)
```
当我们执行 showDataVar() 函数时, 输出到 console 的数值来自全局 data 数组, 而不是 user 对象. 这是因为 showDataVar() 函数是被当做一个全局函数执行的, 所以在函数内部 this 被绑定位全局对象, 即浏览器中的 window 对象。
来, 我们用 bind 方法来修复这个 bug。
```
// Bind the showData method to the user object
var showDataVar=user.showData.bind(user);
```
Bind 方法允许我们实现函数借用
在 JavaScript 中, 我们可以传递函数, 返回函数, 借用他们等等, 而 bind() 方法使函数借用变得极其简单. 以下为一个函数借用的例子:
```
// cars 对象
var cars={
data:[
{name:"Honda Accord", age:14},
{name:"Tesla Model S", age:2}
]
}
// 我们从之前定义的 user 对象借用 showData 方法
// 这里我们将 user.showData 方法绑定到刚刚新建的 cars 对象上
cars.showData=user.showData.bind(cars);
cars.showData(); // Honda Accord 14
```
这里存在一个问题, 当我们在 cars 对象上添加一个新方法(showData)时我们可能不想只是简单的借用一个函数那样, 因为 cars 本身可能已经有一个方法或者属性叫做 showData 了, 我们不想意外的将这个方法覆盖了. 正如在之后的 *Apply 和 Call 方法* 章节我们会介绍, 借用函数的最佳实践应该是使用 Apply 或者 Call 方法。
Bind 方法允许我们柯里化一个函数
柯里化的概念很简单, 只传递给函数一部分参数来调用它, 让它返回一个函数去处理剩下的参数. 你可以一次性地调用 curry 函数, 也可以每次只传一个参数分多次调用, 以下为一个简单的示例. - [JS 函数是编程指南 第 4 章: 柯里化(curry)](https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch4.html)
```
var add=function(x) {
return function(y) {
return x + y;
};
};
var increment=add(1);
var addTen=add(10);
increment(2);
// 3
addTen(2);
// 12
```
现在, 我们使用 bind() 方法来实现函数的柯里化. 我们首先定义一个接收三个参数的 greet() 函数:
```
function greet(gender, age, name) {
// if a male, use Mr., else use Ms.
var salutation=gender==="male" ? "Mr. " : "Ms. ";
if (age > 25) {
return "Hello, " + salutation + name + ".";
}
else {
return "Hey, " + name + ".";
}
}
```
接着我们使用 bind() 方法柯里化 greet() 方法. bind() 接收的第一个参数指定了 this 的值:
```
// 在 greet 函数中我们可以传递 null, 因为函数中并未使用到 this 关键字
var greetAnAdultMale=greet.bind (null, "male", 45);
greetAnAdultMale("John Hartlove"); // "Hello, Mr. John Hartlove."
var greetAYoungster=greet.bind(null, "", 16);
greetAYoungster("Alex"); // "Hey, Alex."
greetAYoungster("Emma Waterloo"); // "Hey, Emma Waterloo."
```
当我们用 bind() 实现柯里化时, greet() 函数参数中除了最后一个参数都被预定义好了, 所以当我们调用柯里化后的新函数时只需要指定最后一位参数。
所以小结一下, bind() 方法允许我们明确指定对象方法中的 this 指向, 我们可以借用, 复制一个方法或者将方法赋值为一个可作为函数执行的变量. 我们以可以借用 bind 实现函数柯里化。
JavaScript 中的 Apply 和 Call 方法
作为 JavaScript 中最常用的两个函数方法, apply 和 call 允许我们借用函数以及在函数调用中指定 this 指向. 除此外, apply 函数允许我们在执行函数时传入一个参数数组, 以此使函数在执行可变参数的函数时可以将每个参数单独的传入函数并得到处理。
使用 apply 或者 call 设置 this
当我们使用 apply 或者 call 时, 传入的第一个参数为目标函数中 this 指向的对象, 以下为一个简单的例子:
```
// 全局变量
var avgScore="global avgScore";
// 全局函数
function avg(arrayOfScores) {
// 分数相加并返回结果
var sumOfScores=arrayOfScores.reduce(function(prev, cur, index, array) {
return prev + cur;
});
// 这里的 "this" 会被绑定到全局对象上, 除非使用 Call 或者 Apply 明确指定 this 的指向
this.avgScore=sumOfScores / arrayOfScores.length;
}
var gameController={
scores :[20, 34, 55, 46, 77],
avgScore:null
}
// 调用 avg 函数, this 指向 window 对象
avg(gameController.scores);
// 证明 avgScore 已经被设置为 window 对象的属性
console.log(window.avgScore); // 46.4
console.log(gameController.avgScore); // null
// 重置全局变量
avgScore="global avgScore";
// 使用 call() 方法明确将 "this" 绑定到 gameController 对象
avg.call(gameController, gameController.scores);
console.log(window.avgScore); // 全局变量 avgScore 的值
console.log(gameController.avgScore); // 46.4
```
以上例子中 call() 中的第一个参数明确了 this 的指向, 第二参数被传递给了 avg() 函数.
apply 和 call 的用法几乎相同, 唯一的差别在于当函数需要传递多个变量时, apply 可以接受一个数组作为参数输入, call 则是接受一系列的单独变量.。
在回掉函数中用 call 或者 apply 设置 this
以下为一个例子, 这种做法允许我们在执行 callback 函数时能够明确 其内部的 this 指向
```
// 定义一个方法
var clientData={
id: 094545,
fullName: "Not Set",
// clientData 对象中的一个方法
setUserName: function (firstName, lastName) {
this.fullName=firstName + " " + lastName;
}
}
function getUserInput(firstName, lastName, callback, callbackObj) {
// 使用 apply 方法将 "this" 绑定到 callbackObj 对象
callback.apply(callbackObj, [firstName, lastName]);
}
```
如下样例中传递给 callback 函数 中的参数将会在 clientData 对象中被设置/更新。
```
getUserInput("Barack", "Obama", clientData.setUserName, clientData);
console.log(clientData.fullName); // Barack Obama
```
使用 Apply 或者 Call 借用函数(必备知识)
相比 bind 方法, 我们使用 apply 或者 call 方法实现函数借用能够有很大的施展空间. 接下来我们考虑从 Array 中借用方法的问题, 让我们定义一个**类数组对象(array-like object)**然后从数组中借用方法来处理我们定义的这个对象, 不过在这之前请记住我们要操作的是一个对象, 而不是数组;
```
// An array-like object: note the non-negative integers used as keys
var anArrayLikeObj={0:"Martin", 1:78, 2:67, 3:["Letta", "Marieta", "Pauline"], length:4 };
```
接下来我们可以这样使用数组的原生方法:
```
// Make a quick copy and save the results in a real array:
// First parameter sets the "this" value
var newArray=Array.prototype.slice.call(anArrayLikeObj, 0);
console.log(newArray); // ["Martin", 78, 67, Array[3]]
// Search for "Martin" in the array-like object
console.log(Array.prototype.indexOf.call(anArrayLikeObj, "Martin")===-1 ? false : true); // true
// Try using an Array method without the call () or apply ()
console.log(anArrayLikeObj.indexOf("Martin")===-1 ? false : true); // Error: Object has no method 'indexOf'
// Reverse the object:
console.log(Array.prototype.reverse.call(anArrayLikeObj));
// {0: Array[3], 1: 67, 2: 78, 3: "Martin", length: 4}
// Sweet. We can pop too:
console.log(Array.prototype.pop.call(anArrayLikeObj));
console.log(anArrayLikeObj); // {0: Array[3], 1: 67, 2: 78, length: 3}
// What about push?
console.log(Array.prototype.push.call(anArrayLikeObj, "Jackie"));
console.log(anArrayLikeObj); // {0: Array[3], 1: 67, 2: 78, 3: "Jackie", length: 4}
```
这样的操作使得我们定义的对象既保留有所有对象的属性, 同时也能够在对象上使用数组方法.。
**arguments** 对象是所有 JavaScript 函数中的一个类数组对象, 因此 call() 和 apply() 的一个最常用的用法是从 arguments 中提取参数并将其传递给一个函数。
以下为 Ember.js 源码中的一部分, 加上了我的一些注释:
```
function transitionTo(name) {
// 因为 arguments 是一个类数组对象, 所以我们可以使用 slice()来处理它
// 参数 "1" 表示我们返回一个从下标为1到结尾元素的数组
var args=Array.prototype.slice.call(arguments, 1);
// 添加该行代码用于查看 args 的值
console.log(args);
// 注释本例不需要使用到的代码
//doTransition(this, name, this.updateURL, args);
}
// 使用案例
transitionTo("contact", "Today", "20"); // ["Today", "20"]
```
以上例子中, args 变量是一个真正的数组. 从以上案例中我们可以写一个得到快速得到传递给函数的所有参数(以数组形式)的函数:
```
function doSomething() {
var args=Array.prototype.slice.call(arguments);
console.log(args);
}
doSomething("Water", "Salt", "Glue"); // ["Water", "Salt", "Glue"]
```
考虑到字符串是不可变的, 如果使用 apply 或者 call 方法借用字符串的方法, 不可变的数组操作对他们来说才是有效的, 所以你不能使用类似 reverse 或者 pop 等等这类的方法. 除此外, 我们也可以用他们借用我们自定义的方法。
```
var gameController={
scores :[20, 34, 55, 46, 77],
avgScore:null,
players :[
{name:"Tommy", playerID:987, age:23},
{name:"Pau", playerID:87, age:33}
]
}
var appController={
scores :[900, 845, 809, 950],
avgScore:null,
avg :function() {
var sumOfScores=this.scores.reduce(function(prev, cur, index, array) {
return prev + cur;
});
this.avgScore=sumOfScores / this.scores.length;
}
}
// Note that we are using the apply() method, so the 2nd argument has to be an array
appController.avg.apply(gameController);
console.log(gameController.avgScore); // 46.4
// appController.avgScore is still null; it was not updated, only gameController.avgScore was updated
console.log(appController.avgScore); // null
```
这个例子非常简单, 我们定义的 gameController 对象借用了 appController 对象的 avg() 方法. 你也许会想, 如果我们借用的函数定义发生了变化, 那么我们的代码会发生什么变化. 借用(复制后)的函数也会变化么, 还是说他在完整复制后已经和原始的方法切断了联系? 让我们用下面这个小例子来说明这个问题:
```
appController.maxNum=function() {
this.avgScore=Math.max.apply(null, this.scores);
}
appController.maxNum.apply(gameController, gameController.scores);
console.log(gameController.avgScore); // 77
```
正如我们所期望的那样, 如果我们修改原始的方法, 这样的变化会在借用实例的方法上体现出来. 我们总是希望如此, 因为我们从来不希望完整的复制一个方法, 我们只是想简单的借用一下.。
使用 apply() 执行参数可变的函数
关于 Apply, Call 和 Bind 方法的多功能性和实用性, 我们将讨论一下Apply方法的一个很简单的功能: 使用参数数组执行函数。
Math.max() 方法是 JavaScript 中一个常见的参数可变函数:
```
console.log(Math.max(23, 11, 34, 56)); // 56
```
但如果我们有一个数组要传递给 Math.max(), 是不能这样做的:
```
var allNumbers=[23, 11, 34, 56];
console.log(Math.max(allNumbers)); // NaN
```
使用 apply 我们可以像下面这样传递数组:
```
var allNumbers=[23, 11, 34, 56];
console.log(Math.max.apply(null, allNumbers)); // 56
```
正如之前讨论, apply() 的第一个参数用于设置 this 的指向, 但是 Math.max() 并未使用到 this, 所以我们传递 null 给他。
为了更进一步解释 apply() 在 参数可变函数上的能力, 我们自定义了一个参数可变函数:
```
var students=["Peter Alexander", "Michael Woodruff", "Judy Archer", "Malcolm Khan"];
// 不定义参数, 因为我们可以传递任意多个参数进入该函数
function welcomeStudents() {
var args=Array.prototype.slice.call(arguments);
var lastItem=args.pop();
console.log("Welcome " + args.join (", ") + ", and " + lastItem + ".");
}
welcomeStudents.apply(null, students);
// Welcome Peter Alexander, Michael Woodruff, Judy Archer, and Malcolm Khan.
```
区别与注意事项
三个函数存在的区别, 用一句话来说的话就是: bind是返回对应函数, 便于稍后调用; apply, call则是立即调用. 除此外, 在 ES6 的箭头函数下, call 和 apply 的失效, 对于箭头函数来说:
更多关于箭头函数的介绍在这里就不做过多介绍了, 详情可以查看 [Arrow functions](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions).
总结
Call, Apply 和 Bind 在设置 this 指向, 声称与执行参数可变函数以及函数借用方面的强大之处已经非常明显. 作为一名 JavaScript 开发者, 你一定会经常见到这种用法, 或者在开发中尝试使用他. 请确保你已经很好的了解了如上所述的概念与用法.
原文链接:
http://javascriptissexy.com/javascript-apply-call-and-bind-methods-are-essential-for-javascript-professionals
、本地配置
在生产环境中的小程序中对外请求只能通过https,且需要在小程序后台配置了该请求的域名。在开发环境(本地),我们可以通过配置小程序,取消该限制。
点击开发工具“详情”,“本地配置” ,将“不校验合法域名、web-view(业务域名),TlS版本以及HTTPS证书”勾选即可请求我们的服务(如下图)。需要注意的是在生产环境中,必须按照微信官方的要求配置https.
二、通过前端请求后端拉取数据并展示
我们日常生活中见到的网页数据都不会硬编码在前端代码上,需要实时拉取数据库中的数据。操作数据库是需要密码的,将密码放在前端是一个十分危险的动作。故前端请求后端,后端拥有数据库的操作权限,将数据从数据库获取,返回到前端(当然后端不仅仅获取数据库数据,还有业务逻辑的处理)。本处我们就是在小程序中实现这个过程。
1.编写微信小程序的前端代码(index.wxml)
<view class="test_requests">
<view bindtap="click_fn">
<button> 请求后端 </button>
<text>{{title}}</text>
<text>{{time}}</text>
</view>
</view>
2, 编写动作函数(index.js)
Page({
// 定义点击事件
click_fn: function (params) {
var self=this
wx.request({
url: "http://192.168.5.59:80/forTest",
method: 'GET',
header: {
"content-type": "json"
},
success: function (res) {
console.log(res.data)
console.log(res.data.d)
self.setData({
title: res.data.d,
time:res.data.f
})
},
fail: function (error) {
// fail
console.log(error)
}
})
}
})
我们自定义了一个函数叫click_fn 。 看到html代码,将其通过关键字bindtap绑定到了这个button 块上。
当我们点击这个块时,触发click_fn 函数,向后端服务“http://192.168.5.59:7998/lz/api/v1/alive”发送了一个GET 请求。同时将后端返回的数据 通过setDate( ) 更新到我们的变量上,然后通过前端页面展示。这样我们便完成了一次前端请求后端,获取到数据后重新展示到前端页面。
这里有点小坑,按照上文的经验。使用 this.setData() 方法可以将数据进行更新到我们的变量中。但是在这个我们自定义的函数click_fn 里是不能直接使用 this。必须要声明一个变量来承接这个 this(这里有点想不明白,清楚的朋友可以讨论下)。
最后附上python编写的后端接口代码
*请认真填写需求信息,我们会在24小时内与您取得联系。