. DNS介紹
1.1什么是域名
域名(Domain Name) ,簡稱域名、網域,是由一串用點分隔的名字組成的Intemet上某一臺計算機或計算機組的名稱,用于在數據傳輸時標識計算機的電子方位。具有獨一無二,不可重復的特性。
1.2 什么是DNS?
域名系統(Domain Name System,縮寫: DNS)是互聯網的一項服務。域名解析是把域名指向網站空間IP,讓人們通過注冊的域名可以方便地訪問到網站的一種服務。IP地址是網絡上標識站點的數字地址,為了方便記憶,采用域名來代替IP地址標識站點地址。域名解析就是域名到IP地址的轉換過程。域名的解析工作由DNS服務器完成??梢岳斫鉃镈NS就是翻譯官。
正向解析: 域名 --> 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小時內與您取得聯系。