<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>律回彼境</title><description>Melody echoes...</description><link>https://miku.top/</link><language>zh_CN</language><item><title>Python 导入机制的探索</title><link>https://miku.top/posts/pyq1-python-import-system/</link><guid isPermaLink="true">https://miku.top/posts/pyq1-python-import-system/</guid><description>Python 导入系统的机制与原理</description><pubDate>Tue, 06 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Python 导入机制的探索&lt;/h1&gt;
&lt;p&gt;最近做项目时，围绕 python 的导入系统和导入机制进行了一些探索，顺便发现了一些有趣的现象。&lt;/p&gt;
&lt;p&gt;我把这个现象整理为一个小思考题，并在不少 qq 群留下了这个小思考题，如图所示：（长图预警，图片过小请点击放大，或使用 ctrl + 放大）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/1.B7J1wDLc_Z1dTaTm.webp&quot; alt=&quot;1 w-42%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/2.CU8_-Zx4_hSeyw.webp&quot; alt=&quot;2 w-42%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;读者可以先自行思考一番，以下为此问题的解答：（长图预警，图片过小请点击放大，或使用 ctrl + 放大）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/3.DcjZ-pxG_Zb4YgM.webp&quot; alt=&quot;3 w-42%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/4.jgdh_8Bj_Z1aCvf.webp&quot; alt=&quot;4 w-42%&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>ArchLinux 折腾指南&amp;记录</title><link>https://miku.top/posts/archlinux-note/</link><guid isPermaLink="true">https://miku.top/posts/archlinux-note/</guid><description>ArchLinux 安装、配置记录</description><pubDate>Wed, 15 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;ArchLinux 折腾指南&amp;amp;记录&lt;/h1&gt;
&lt;p&gt;谨以此篇文章记录最近一个半月，折腾 ArchLinux 的一些流程，以及记录一些常见问题和自己的解决方案。&lt;/p&gt;
&lt;p&gt;:::tip
2023.12.13 备注：本文介绍的桌面环境配置部分，适用于使用 X 协议的 KDE5。但 Linux 社区正逐步放弃对 X 协议的支持，并过渡到 Wayland。因此&amp;lt;strong&amp;gt;未来安装桌面环境时，本文的桌面配置方案未必再有参考价值&amp;lt;/strong&amp;gt;。如即将发布的 KDE6 和 GTK5，即将默认启用 Wayland 而不是 X 协议。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终效果&lt;/strong&gt;:
&lt;img src=&quot;https://miku.top/_astro/show-1.BuAtmI4u_1cELqH.webp&quot; alt=&quot;show-1.png&quot; /&gt;
&lt;img src=&quot;https://miku.top/_astro/show-2.DeDI2nmg_ZzF8xu.webp&quot; alt=&quot;show-2.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;〇、准备工作&lt;/h2&gt;
&lt;p&gt;arch install iso 链接：&lt;a href=&quot;https://archlinux.org/download/&quot;&gt;Arch Linux - Downloads&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;安装盘烧录和安装盘加载，这里不赘述。记得主板启用 UEFI 引导 + 关闭安全启动（Secure Boot）。&lt;/p&gt;
&lt;h2&gt;一、连接网络和时区配置&lt;/h2&gt;
&lt;p&gt;真正的第一步是设置字体，个人比较喜欢这一款：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setfont /usr/share/kbd/consolefonts/LatGrkCyr-12x22
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 iwctl 连接 wifi：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iwctl
device list
station &amp;lt;device&amp;gt; scan
station &amp;lt;device&amp;gt; get-networks
station &amp;lt;device&amp;gt; connect &amp;lt;ssid&amp;gt;
exit
dhcpcd &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然如果是有线连接，一般来说是会自动配置的。&lt;/p&gt;
&lt;p&gt;也可以通过 systemd-networkd 的配置来自定义，这里做一个固定 IP 分配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 移除所有默认网络配置 rm -rf /etc/systemd/network/*
# nano /etc/systemd/network/&amp;lt;网络设备代号&amp;gt;.network
# 网络设备代号代号通过 ip link 查看
[Match]
Name=ens33
 
[Network]
DHCP=no
Address=192.168.1.2/24
DNS=8.8.8.8
 
[Route]
Gateway=192.168.1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启网络服务并检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart systemd-networkd
ping baidu.com
curl https://www.baidu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 ssh 在其他电脑上远程操作安装。出了问题好复制查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 其他计算机上
ssh root@192.168.1.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置时区和时间同步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timedatectl set-timezone Asia/Shanghai
timedatectl set-ntp true
timedatectl
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、分区、格式化和挂载&lt;/h2&gt;
&lt;p&gt;个人习惯用 fdisk 分为三个基础分区：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/boot&lt;/code&gt; fat32 分区
&lt;ul&gt;
&lt;li&gt;/boot 单独分区是 archwiki 推荐的分法之一&lt;/li&gt;
&lt;li&gt;推荐至少 &amp;gt;1G&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swap&lt;/code&gt; linux swap 分区
&lt;ul&gt;
&lt;li&gt;和物理内存一样大，或为物理内存 2 倍&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; ext4 分区
&lt;ul&gt;
&lt;li&gt;如果没有其他分区，那剩下所有分给它&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然你还可以单独把 /home 分出来，方便备份和数据迁移。不过后面的步骤里要记得挂载。也可以用 btrfs 或其他文件系统，然后开子卷和其他一些高级特性。&lt;/p&gt;
&lt;p&gt;分区主要流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新建 GPT 分区表（如果是空盘）&lt;/li&gt;
&lt;li&gt;删除/新建分区&lt;/li&gt;
&lt;li&gt;打印检查&lt;/li&gt;
&lt;li&gt;保存写入、退出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例分区结果：(按顺序分别为 /boot、swap 和 /）
&lt;img src=&quot;https://miku.top/_astro/image-20231115184402955.sQaf0LPv_ofscy.webp&quot; alt=&quot;image-20231115184402955.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;格式化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkfs.fat -F32 /dev/nvme0n1p1
mkswap /dev/nvme0n1p2
mkfs.ext4 /dev/nvme0n1p3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;挂载并检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 一定记得先挂载 / 到 /mnt，然后才执行其他操作！
# 否则可能会发生覆盖 mount
mount /dev/nvme0n1p3 /mnt
mkdir /mnt/boot
mount /dev/nvme0n1p1 /mnt/boot
swapon /dev/nvme0n1p2
lsblk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例 lsblk 结果：
&lt;img src=&quot;https://miku.top/_astro/image-20231115185309412.mZSsoxfk_dIx48.webp&quot; alt=&quot;image-20231115185309412.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;三、安装系统&lt;/h2&gt;
&lt;p&gt;安装内核和基础软件包。同时根据需求装一些进系统后实用的软件。&lt;/p&gt;
&lt;p&gt;intel 和 amd 的处理器，分别选择安装一种微码支持：&lt;code&gt;intel-ucode&lt;/code&gt;、&lt;code&gt;amd-ucode&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pacman -Sy
pacstrap /mnt base base-devel amd-ucode linux linux-firmware linux-headers vi vim nano git wget tmux openssh networkmanager htop neofetch ntfs-3g yay
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;装完后可以检查一下：（应该基本目录结构和引导文件都生成了）
&lt;img src=&quot;https://miku.top/_astro/image-20231115191404855.8HjATMnY_5wer8.webp&quot; alt=&quot;image-20231115191404855.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;补全一下 fstab 的自动挂载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;genfstab -U /mnt &amp;gt;&amp;gt; /mnt/etc/fstab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;change root 到安装的系统里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;arch-chroot /mnt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进去后可以再使用 lsblk 检查一下挂载正不正确。&lt;/p&gt;
&lt;h2&gt;四、系统基本配置&lt;/h2&gt;
&lt;p&gt;设置时区和同步硬件时钟：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
hwclock --systohc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成本地化标识，一般英文和中文都生成一个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo -e &quot;en_US.UTF-8 UTF-8\nzh_CN.UTF-8 UTF-8&quot; &amp;gt; /etc/locale.gen
locale-gen
# 先这样设置，防止 tty 乱码。安装桌面环境后再改回来
echo &apos;LANG=en_US.UTF-8&apos; &amp;gt; /etc/locale.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成 hostname 和 hosts：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;melorenae&apos; &amp;gt; /etc/hostname
# nano /etc/hosts
127.0.0.1	localhost
::1		localhost
127.0.1.1	melorenae.localdomain	melorenae
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;root 密码设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加普通用户，并加入 wheel 权限组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useradd -m -G wheel melodyecho
passwd melodyecho
# 编辑 sudoers，实现 su 权限执行所有命令和 sudo 免密码
EDITOR=nano visudo
# 取消以下两行的注释即可：
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20231115192905585.2TD5XbbE_ZTv9wz.webp&quot; alt=&quot;image-20231115192905585.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;启用 networkmanager 和 sshd 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable NetworkManager
systemctl enable sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 grub 引导：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pacman -Sy
pacman -S grub efibootmgr os-prober
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=archlinux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启用 os-prober：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# nano /etc/default/grub
# 取消这一行注释
GRUB_DISABLE_OS_PROBER=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成 grub 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grub-mkconfig -o /boot/grub/grub.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;退出、递归取消挂载、重启，准备进入新系统：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exit
umount -R /mnt
reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五、桌面环境安装&lt;/h2&gt;
&lt;p&gt;登录进入新系统。因为没有桌面环境，所以初始网络配置依然要重新做一遍。不过之前安装了 NetworkManager，可以直接用 nmcli 或 nmtui。之后再安装桌面环境，用桌面环境的设置配置一次，以后网络环境就会自动就绪了。&lt;/p&gt;
&lt;p&gt;当然这里还是使用刚才的固定 ip 分配，即再写一次配置。&lt;/p&gt;
&lt;p&gt;不过重启网络服务时注意启动解析服务，要不然 dns 解析没法工作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart systemd-networkd
sudo systemctl start systemd-resolved
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后还是切换到远程连接上操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 其他计算机上
ssh melodyecho@192.168.1.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加 archlinux-cn 源，并安装其密钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sudo nano /etc/pacman.conf
# 末行添加
[archlinuxcn]
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinuxcn/$arch
sudo pacman -Sy
sudo pacman -S archlinuxcn-keyring
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;/etc/pacman.conf 文件中建议启用配置 Color：
&lt;img src=&quot;https://miku.top/_astro/image-20231115201816980.WCkQogfj_1ALWhi.webp&quot; alt=&quot;image-20231115201816980.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;安装音频驱动支持：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S pipewire pipewire-alsa pipewire-jack pipewire-pulse pipewire-media-session gst-plugin-pipewire
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 xorg 套件、桌面环境、登录管理器和一些实用程序，这里以 sddm 和 kde5 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S xorg
sudo pacman -S plasma sddm konsole dolphin kate okular spectacle partitionmanager ark filelight gwenview
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启用 sddm 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable sddm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启系统进入桌面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;六、桌面环境配置&lt;/h2&gt;
&lt;p&gt;:::tip
2023.12.13 备注：再次提示，以下内容仅适用于：&amp;lt;strong&amp;gt;使用 X 协议的、sddm 登录验证的 KDE5 桌面环境&amp;lt;/strong&amp;gt;。同时，如果未来官方源更新 KDE 至 KDE6，请自行判断以下操作是否适用。
:::&lt;/p&gt;
&lt;h3&gt;1、网络配置&lt;/h3&gt;
&lt;p&gt;在 GUI 界面，用系统设置再配置一次。&lt;/p&gt;
&lt;h3&gt;2、中文和输入法支持&lt;/h3&gt;
&lt;p&gt;首先重新指定 LANG：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sudo nano /etc/locale.conf
LANG=zh_CN.UTF-8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装中文字体和其他实用字体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S adobe-source-han-sans-otc-fonts adobe-source-han-serif-otc-fonts noto-fonts noto-fonts-cjk noto-fonts-emoji
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后设置里把语言改成中文，重启就有中文了。&lt;/p&gt;
&lt;p&gt;中文输入法一般 aur 源的搜狗是比较快和稳妥的方案，而且也还算好用。追求自定义化的话，可以去搞 rime。或者用 fcitx5 也是不错的。&lt;/p&gt;
&lt;p&gt;安装搜狗输入法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yay -S fcitx-sogoupinyin
sudo pacman -S fcitx-configtool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置相关环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# nano ~/.xprofile
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
export INPUT_METHOD=fcitx
export SDL_IM_MODULE=fcitx
export GLFW_IM_MODULE=fcitx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注销重新登录。然后运行 &lt;code&gt;fcitx-configtool&lt;/code&gt; 可以进行一些配置，比如把 fcitx 自带的其他输入法删掉。如果没正常工作，运行 &lt;code&gt;fcitx-diagnose&lt;/code&gt; 检查。&lt;/p&gt;
&lt;p&gt;如果某些 QT 应用仍然无法使用输入法，可以尝试以下命令并全选安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S fcitx-im
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样会同时安装 fcitx 的 qt5 和 qt6 支持。防止有些应用找不到对应版本的库文件（如 telegram 需要 fcitx-qt6，只有 fcitx-qt5 是不行的 ）。&lt;/p&gt;
&lt;h3&gt;3、shell 和终端模拟器配置&lt;/h3&gt;
&lt;p&gt;看你自己喜欢什么 shell 和终端咯。终端我一般用 KDE 自带的 konsole，然后主题从 kde 的商店里下一个。终端字体的话我喜欢用 Monaco，我做了一个 Nerd Fonts 的 patch，需要的话自取：&lt;a href=&quot;/upload/articles/archlinux-note/Monaco_Nerd_Font_Mono-Regular.ttf&quot;&gt;Monaco_Nerd_Font_Mono-Regular.ttf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;shell 的话，我一般用 zsh，然后搭配 oh-my-zsh 和 powerlevel10k 主题。另外，我常用以下 zsh 插件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;zsh-autosuggestions&lt;/li&gt;
&lt;li&gt;zsh-completions&lt;/li&gt;
&lt;li&gt;zsh-history-substring-search&lt;/li&gt;
&lt;li&gt;zsh-syntax-highlighting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;shell 和终端配置其实很随心所欲的，详细的就不介绍了。&lt;/p&gt;
&lt;h3&gt;4、显卡驱动安装&lt;/h3&gt;
&lt;p&gt;xorg 显卡驱动安装参照 wiki：&lt;a href=&quot;https://wiki.archlinux.org/title/Xorg&quot;&gt;Xorg - ArchWiki&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果是 n 卡，特别注意仔细阅读此文档：&lt;a href=&quot;https://wiki.archlinux.org/title/NVIDIA&quot;&gt;NVIDIA - ArchWiki&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果是双显卡，需要配置管理方案。流行的管理方案有 optimus manager、prime offload 和 nvidia-xrun。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;optimus-manager：&lt;a href=&quot;https://github.com/Askannz/optimus-manager&quot;&gt;GitHub - Askannz/optimus-manager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;prime-render-offload：&lt;a href=&quot;https://wiki.archlinux.org/title/PRIME#PRIME_render_offload&quot;&gt;PRIME - ArchWiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;nvidia-xrun：&lt;a href=&quot;https://wiki.archlinux.org/title/Nvidia-xrun&quot;&gt;nvidia-xrun - ArchWiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;其他：&lt;a href=&quot;https://wiki.archlinux.org/title/NVIDIA_Optimus#Use_switchable_graphics&quot;&gt;Use switchable graphics - ArchWiki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;个人比较喜欢 prime 管理方案。如下：（非 prime 勿按照以下操作）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S nvidia-prime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要使用 n 卡运行时，只需要在前面加上 &lt;code&gt;prime-run&lt;/code&gt; 即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prime-run xxx
# 例如查看当前的 opengl 渲染器，自然应该输出 n 卡相关信息：
prime-run glxinfo | grep &quot;OpenGL renderer&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/prime-run.B4UK5WkH_ZyEbEO.webp&quot; alt=&quot;prime-run.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然你也可以选择手动注入 prime 相关的环境变量。这本质上和 &lt;code&gt;prime-run&lt;/code&gt; 是等价的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export __NV_PRIME_RENDER_OFFLOAD=1
export __VK_LAYER_NV_optimus=NVIDIA_only
export __GLX_VENDOR_LIBRARY_NAME=nvidia
export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外如果需要在内核阶段早加载驱动（Early KMS Start），参考：&lt;a href=&quot;https://wiki.archlinux.org/title/Kernel_mode_setting#Early_KMS_start&quot;&gt;Kernel mode setting - ArchWiki&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;5、CPU 调频和电源管理&lt;/h3&gt;
&lt;p&gt;根据自己的硬件情况和需求自行搜索解决。在 archwiki 有很多文档可以参考。&lt;/p&gt;
&lt;p&gt;我自己的 cpu 是移动端 amd r7 4800H。电源管理我省事就没做了，但是我用 cpupower-gui 这个软件调频：（你也可以试试）
&lt;img src=&quot;https://miku.top/_astro/cpu-power.OzsZZZes_ZJAHv8.webp&quot; alt=&quot;cpupower-gui w-45%&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6、常用软件安装&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;qq&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yay -S linuxqq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;微信&lt;/strong&gt;：（个人推荐这个）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yay -S com.qq.weixin.spark
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;telegram&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S telegram-desktop
# 遇到 fcitx 输入法无法启动问题，使用：
sudo pacman -S fcitx-qt6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;腾讯会议&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yay -S wemeet-bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;wps&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yay -S ttf-wps-fonts wps-office
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若出现 pdf 导出失败、字体过粗问题，参照 wiki 解决： &lt;a href=&quot;https://wiki.archlinux.org/title/WPS_Office&quot;&gt;WPS Office - ArchWiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;如果需要国内特供版 wps 也参考上面的 wiki&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;百度网盘&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yay -S baidunetdisk-electron
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;md 编辑器 &lt;code&gt;Typora&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S typora
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;vscode&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yay -S visual-studio-code-bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;steam&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;首先确保你已经安装好了显卡驱动，以及相关的 32 位及 64 位的 opengl, vulkan 包。参考：&lt;a href=&quot;https://wiki.archlinux.org/title/Xorg#Driver_installation&quot;&gt;Driver_installation - ArchWiki&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S steam
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关于字体支持和其他一些安装配置细节，参考：&lt;a href=&quot;https://wiki.archlinux.org/title/Steam&quot;&gt;Steam - ArchWiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;安装完成后建议启动 steam-runtime 而不是 steam-native，前者会使用 steam 自带的运行库文件&lt;/li&gt;
&lt;li&gt;如果需要使用兼容层 proton，参照：&lt;a href=&quot;https://www.bilibili.com/read/cv20979435&quot;&gt;【爱折腾】SteamDeck完全折腾指南-E1-安装Proton订制兼容层 - 哔哩哔哩&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;vlc&lt;/strong&gt; 播放器安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S vlc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;obs&lt;/strong&gt; 录屏软件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 如果用不到 浏览器源采集 功能，直接从官方源安装：
sudo pacman -S obs-studio
# 否则推荐这个 aur 包，或者安装 flatpak 版的 obs：
yay -S obs-studio-tytan652
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;timeshift&lt;/strong&gt; 备份和恢复软件：（搭配 btrfs 使用更佳）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S timeshift
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实用截图/贴图软件 &lt;strong&gt;flameshot&lt;/strong&gt;：（类似 Snipaste）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S flameshot
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7、Windows 分区挂载&lt;/h3&gt;
&lt;p&gt;可以在 &lt;code&gt;/etc/fstab&lt;/code&gt; 里配置自动 mount，但是某些情况下，这样的自动 mount 会导致启动时间剧增。（可使用 &lt;code&gt;sudo systemd-analyze blame&lt;/code&gt; 查看系统启动占用时间）&lt;/p&gt;
&lt;p&gt;所以更推荐写成一个脚本，在用户登录后自动执行挂载。脚本放置在 &lt;code&gt;~/.config/autostart/&lt;/code&gt; 即可在用户登录后执行。挂载命令示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# uid, gid 通过命令 id 查看
# umask, dmask, fmask 可以控制 mount 后的文件的权限
sudo mount -t ntfs-3g -o uid=1000,gid=998,umask=0,dmask=027,fmask=137 /dev/nvme0n1p3 /mnt/c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 mount 后的权限为：所有者为当前用户。所有者可读写可执行，组内用户可读可执行，其他用户无权读写和执行。&lt;/p&gt;
&lt;h3&gt;8、桌面很卡，xxx 进程占用太高&lt;/h3&gt;
&lt;p&gt;这个问题其实蛮随机的。有时候运行几个游戏再退出来，或者从休眠恢复，都可能变卡。得看你具体占用高的进程的日志。搜一下日志，然后去找解决方案吧。&lt;/p&gt;
&lt;p&gt;如果实在看日志都找不到问题，建议重启 Kwin 或者注销重新登录，还不行就重启。重启 Kwin 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setsid kwin_x11 --replace &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 Kwin 还是很快的，总比注销和重启快，而且大多数时候有效。如果你懒得敲这段命令，在 .bashrc/.zshrc 做个 alias。&lt;/p&gt;
&lt;h3&gt;9、KDE 桌面美化/优化&lt;/h3&gt;
&lt;p&gt;几点建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;尽量少做美化。特别是很 hack 的美化要少搞，会不稳定（除非是非常成熟的项目）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风格统一是&quot;美观&quot;的必要条件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;主题安装参照对应 github 页面 README 进行，否则可能安装不完全。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;dock 栏支持应用 latte-dock 安装：（&lt;strong&gt;注意这个在 KDE6 已经完全不可用&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S latte-dock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基于 Steam Wallpaper Engine 和 kde 插件的动态壁纸方案：&lt;a href=&quot;https://github.com/catsout/wallpaper-engine-kde-plugin&quot;&gt;GitHub - wallpaper-engine-kde-plugin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;某些 Wallpaper Engine 的壁纸可能会导致 kde 反复崩溃，这时删除 &lt;code&gt;~/.config/plasma-org.kde.plasma.desktop-appletsrc&lt;/code&gt; 中的 &lt;code&gt;WallpaperSource&lt;/code&gt; 一行并重启 KDE 即可修复。&lt;/p&gt;
&lt;p&gt;推荐的其他关于桌面美化的资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/354427013&quot;&gt;KDE 桌面环境定制指南 | Linux 中国&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1bz4y1z7oV&quot;&gt;Linux KDE美化plasma moe主题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV15x4y1u7Ui&quot;&gt;【Archlinux+KDE】Mac风美化+安装后配置，超详细教程！&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Gh411R7Q4&quot;&gt;笔记本屏幕小窗口开多太混乱？优雅kde改平铺，何必一定i3 dwm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/460826583&quot;&gt;Manjaro KDE 调教配置及美化（2022.01.23）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/pipci/p/14861412.html&quot;&gt;KDE常用桌面插件总结 - pipci - 博客园&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/read/cv11183026&quot;&gt;Manjaro+kde简单配置美化教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jianshu.com/p/51a054b3bf93&quot;&gt;KDE 那些实用有趣的桌面插件 - 简书&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10、运行 Windows 应用/游戏&lt;/h3&gt;
&lt;p&gt;一般就是要去折腾 wine。但是 wine 的坑实在不是一般的多。甚至再单独写几篇文章都写不完。推荐使用这个可视化工具自动配置：&lt;a href=&quot;https://github.com/bottlesdevs/Bottles&quot;&gt;Github - Bottles&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;用这个不用操心一些 dll 和运行库缺失的问题，而且还有一键硬件加速和显卡切换，可以说非常实用了。&lt;/p&gt;
&lt;p&gt;如果 wine 应用 alt-tab 切换后丢失焦点，参照：&lt;a href=&quot;https://askubuntu.com/questions/299286/how-to-recover-focus-after-losing-it-while-using-wine&quot;&gt;AskUbuntu - 299286&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>安全异步读写原理与实现</title><link>https://miku.top/posts/safe-async-rw/</link><guid isPermaLink="true">https://miku.top/posts/safe-async-rw/</guid><description>安全异步读写，是并发控制中的经典问题</description><pubDate>Fri, 01 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;安全异步读写原理与实现&lt;/h1&gt;
&lt;h3&gt;1、基本原理&lt;/h3&gt;
&lt;p&gt;异步读写问题与常规异步问题不同，不是单纯的互斥问题。对于安全的异步读写，为了同时保证安全性和并发性能，需要实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取时：不允许写入。但可以再并行/并发地读取。&lt;/li&gt;
&lt;li&gt;写入时：不允许读取。也不允许其他写入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结合操作系统原理课的知识，我们可以得到用 PV 原语表示的，并发读写的伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 写信号量
Semaphore WS = 1
# 当前正在读的计数变量
int RN = 0
# 变量 RN 的操作锁
Mutex L


def safe_write():
    P(WS)
    # 写操作
    write()
    V(WS)


def safe_read():
    P(L)
        if RN == 0:
            P(WS)
        RN += 1
    V(L)
    # 读操作
    read()
    P(L)
        RN -= 1
        if RN == 0:
            V(WS)
    V(L)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，在某些情况下，可能需要限制读的并发度，那么可以再添加一个信号量变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 写信号量
Semaphore WS = 1
# 读信号量，值为一个常量
Semaphore RS = N
# 当前正在读的计数变量
int RN = 0
# 变量 RN 的操作锁
Mutex L


def safe_write():
    P(WS)
    # 写操作...
    write()
    V(WS)


def safe_read():
    P(RS)
        P(L)
            if RN == 0:
                P(WS)
            RN += 1
        V(L)
        # 读操作...
        read()
        P(L)
            RN -= 1
            if RN == 0:
                V(WS)
        V(L)
    V(RS)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、代码实现&lt;/h3&gt;
&lt;p&gt;以下所有实现，均使用 Python。&lt;/p&gt;
&lt;p&gt;在 Python 中，PV 原语有线程版、协程版、进程版的对应实现。因此可以在各种情景下很方便地做到安全异步读写。&lt;/p&gt;
&lt;p&gt;不过需要注意的是，由于 Python GIL 的存在，任何时候只有一个线程在运行。因此如果使用多线程，读无法实现并行，最多实现并发。当然如果使用多进程，就可以并行了。以下是多线程异步安全读写的实现示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import threading
import random
import time

write_semaphore = threading.Semaphore(1)
# 设置读的并发度为 2
read_semaphore = threading.Semaphore(2)
read_num = 0
read_num_lock = threading.Lock()
test_var = &quot;initial value&quot;

def safe_write():
    def _write():
        global test_var
        test_var =  random.randint(1, 1000000)
        # 模拟写操作需要的时间
        time.sleep(0.5)
        print(f&quot;Changed to {test_var}&quot;)
    write_semaphore.acquire()
    _write()
    write_semaphore.release()

def safe_read():
    def _read():
        global test_var
        print(test_var)
    global read_num
    read_semaphore.acquire()
    with read_num_lock:
        if read_num == 0:
            write_semaphore.acquire()
        read_num += 1
    _read()
    with read_num_lock:
        read_num -= 1
        if read_num == 0:
            write_semaphore.release()
    read_semaphore.release()

if __name__ == &quot;__main__&quot;:
    threads = []
    for i in range(5):
        threads.append(threading.Thread(target=safe_read))
        threads.append(threading.Thread(target=safe_write))
    # 打乱列表，模拟读写线程的先后到来
    random.shuffle(threads)
    for t in threads:
        t.start()
    for t in threads:
        t.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上述代码，获得的结果比较随机。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Changed to 556931
Changed to 938396
938396
Changed to 925837
Changed to 436704
Changed to 608686
608686
608686608686

608686
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单分析一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读线程打印的值不会和 &quot;Changed...&quot; 重合，说明读写互斥&lt;/li&gt;
&lt;li&gt;读线程打印的值会重合，且最多重合两次，说明可以并发执行，且并发度最大为 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;符合我们预设的目标。&lt;/p&gt;
&lt;p&gt;如果使用 &lt;code&gt;asyncio&lt;/code&gt; ，则可以实现多协程的版本，代码很类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import random

write_semaphore = asyncio.Semaphore(1)
read_semaphore = asyncio.Semaphore(2)
read_num = 0
read_num_lock = asyncio.Lock()
test_var = &quot;initial value&quot;

async def safe_write():
    async def _write():
        global test_var
        test_var =  random.randint(1, 1000000)
        await asyncio.sleep(0.5)
        print(f&quot;Changed to {test_var}&quot;)
    await write_semaphore.acquire()
    await _write()
    write_semaphore.release()

async def safe_read():
    def _read():
        global test_var
        print(test_var)
    global read_num
    await read_semaphore.acquire()
    async with read_num_lock:
        if read_num == 0:
            await write_semaphore.acquire()
        read_num += 1
    _read()
    async with read_num_lock:
        read_num -= 1
        if read_num == 0:
            write_semaphore.release()
    read_semaphore.release()

async def main():
    coros = []
    for i in range(5):
        coros.append(safe_read())
        coros.append(safe_write())
    random.shuffle(coros)
    tasks = [asyncio.create_task(coro) for coro in coros]
    await asyncio.wait(tasks)

if __name__ == &quot;__main__&quot;:
    asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 &lt;code&gt;asyncio&lt;/code&gt; 是单线程实现的多协程，因此所有操作实际都在一个线程上。不会出现对线程不安全的 &lt;code&gt;print()&lt;/code&gt; 的同时调用，因此打印出来的结果很整洁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Changed to 17815
Changed to 405116
405116
405116
Changed to 678826
Changed to 88906
Changed to 989941
989941
989941
989941
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、实际使用&lt;/h3&gt;
&lt;p&gt;在实际业务场景中，读和写的方法可能千变万化，需要按需提供。对此可以使用 Python 的上下文管理器，将实际的读写方法解耦出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import contextlib
import threading

write_semaphore = threading.Semaphore(1)
read_semaphore = threading.Semaphore(2)
read_num = 0
read_num_lock = threading.Lock()

# 如果异常没有在上下文中（即 with 子句下）捕获，则抛出
@contextlib.contextmanager
def safe_read():
    global read_num
    read_semaphore.acquire()
    with read_num_lock:
        if read_num == 0:
            write_semaphore.acquire()
        read_num += 1
    try:
        yield
    finally:
        with read_num_lock:
            read_num -= 1
            if read_num == 0:
                write_semaphore.release()
            read_semaphore.release()

@contextlib.contextmanager
def safe_write():
    write_semaphore.acquire()
    try:
        yield
    finally:
        write_semaphore.release()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用时只需要：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;with safe_read():
    ...
with safe_write():
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得注意的是，此时所有使用 &lt;code&gt;safe_read&lt;/code&gt;、&lt;code&gt;safe_write&lt;/code&gt; 的代码，都会共用一套读写控制逻辑，这显然不太合适。我们更希望对一个或一类资源，有独立的读写控制。这提示我们可以把它封装为类来进一步解耦：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import contextlib
import threading

class RWController:
    def __init__(self, max_read_num: int=None) -&amp;gt; None:
        # max_read_num 如果不为 None，意味着有读的并发限制
        # 注意这是 __init__() 方法下的函数
        @contextlib.contextmanager
        def safe_read():
            nonlocal read_num, read_semaphore, write_semaphore, read_num_lock
            if read_semaphore:
                read_semaphore.acquire()
            with read_num_lock:
                if read_num == 0:
                    write_semaphore.acquire()
                read_num += 1
            try:
                yield
            finally:
                with read_num_lock:
                    read_num -= 1
                    if read_num == 0:
                        write_semaphore.release()
                    if read_semaphore:
                        read_semaphore.release()

        @contextlib.contextmanager
        def safe_write():
            nonlocal write_semaphore
            write_semaphore.acquire()
            try:
                yield
            finally:
                write_semaphore.release()
			
        write_semaphore = threading.Semaphore(1)
        if max_read_num:
            read_semaphore = threading.Semaphore(max_read_num)
        else:
            read_semaphore = None
        read_num = 0
        read_num_lock = threading.Lock()
        self.safe_read = safe_read
        self.safe_write = safe_write
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时外部就可以初始化这个类，获得一个独立的读写控制器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rwc1 = RWController()
rwc2 = RWController(max_read_num=5)

with rwc1.safe_read():
    ...
with rwc2.safe_read():
    ...
with rwc1.safe_write():
    ...
with rwc2.safe_write():
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样依然需要外部手动调用 &lt;code&gt;with&lt;/code&gt; 来进行上下文管理，好处是非常灵活。&lt;/p&gt;
&lt;p&gt;但如果只是需要某个方法获得安全的异步读写，另一个比较好的思路是通过装饰器来处理。那怎么使用装饰器处理呢？&lt;/p&gt;
&lt;p&gt;首先让我们来看一个类的源码。它是 &lt;code&gt;contextlib.ContextDecorator&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ContextDecorator(object):
    def _recreate_cm(self):
        return self

    def __call__(self, func):
        @wraps(func)
        def inner(*args, **kwds):
            with self._recreate_cm():
                return func(*args, **kwds)
        return inner
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不难发现，继承该类并实现 &lt;code&gt;__enter__()&lt;/code&gt; 和 &lt;code&gt;__exit__()&lt;/code&gt; 方法后，产生的对象如果被用作装饰器方法，会执行 &lt;code&gt;__call__()&lt;/code&gt; 这个装饰器函数。之后的执行逻辑就是：通过内部的 with 语句，在执行 &lt;code&gt;__enter__()&lt;/code&gt; 后，执行被装饰的函数，最后再调用 &lt;code&gt;__exit__()&lt;/code&gt; ，实现自动的上下文管理。&lt;/p&gt;
&lt;p&gt;让我们用上这个类，来实现刚才的目标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import contextlib
import threading

class RWController:
    def __init__(self, max_read_num: int=None) -&amp;gt; None:
        super().__init__()
        # max_read_num 不为 None，意味着有读的并发限制
        if max_read_num:
            self.read_semaphore = threading.Semaphore(max_read_num)
        else:
            self.read_semaphore = None
        self.write_semaphore = threading.Semaphore(1)
        self.read_num = 0
        self.read_num_lock = threading.Lock()
        self.safe_read = RWController.ReadController(
            self.read_semaphore, self.write_semaphore, 
            self.read_num_lock, self.read_num
        )
        self.safe_write = RWController.WriteController(self.write_semaphore)

    class ReadController(contextlib.ContextDecorator):
        def __init__(self, read_semaphore, write_semaphore, read_num_lock, read_num) -&amp;gt; None:
            super().__init__()
            self.read_semaphore: threading.Semaphore = read_semaphore
            self.write_semaphore: threading.Semaphore = write_semaphore
            self.read_num_lock: threading.Lock = read_num_lock
            self.read_num = read_num

        def __enter__(self) -&amp;gt; None:
            # 读信号量不为 None，意味着有读的并发限制
            if self.read_semaphore:
                self.read_semaphore.acquire()
            with self.read_num_lock:
                if self.read_num == 0:
                    self.write_semaphore.acquire()
                self.read_num += 1

        def __exit__(self, *exc) -&amp;gt; None:
            with self.read_num_lock:
                self.read_num -= 1
                if self.read_num == 0:
                    self.write_semaphore.release()
                # 读信号量不为 None，意味着有读的并发限制
                if self.read_semaphore:
                    self.read_semaphore.release()
    
    class WriteController(contextlib.ContextDecorator):
        def __init__(self, write_semaphore) -&amp;gt; None:
            super().__init__()
            self.write_semaphore: threading.Semaphore = write_semaphore

        def __enter__(self) -&amp;gt; None:
            self.write_semaphore.acquire()
        
        def __exit__(self, *exc) -&amp;gt; None:
            self.write_semaphore.release()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用的时候，只要这样就可以了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rwc = RWController()

@rwc.safe_write
def custom_write():
    ...

@rwc.safe_read
def custom_read():
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理，你也可以使用多进程、多协程来实现上面的这些具体方案。对于多协程实现，需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;contextmanager&lt;/code&gt; 有其对应的协程异步版本：&lt;code&gt;asynccontextmanager&lt;/code&gt;，可以直接使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ContextDecorator&lt;/code&gt; 类没有其对应的协程异步版本，需要自己实现。原理类似，不过是改用 &lt;code&gt;async with&lt;/code&gt;，以及 &lt;code&gt;__aenter__()&lt;/code&gt;、&lt;code&gt;__aexit__()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、后话&lt;/h3&gt;
&lt;p&gt;第三方模块中，已经有比较成熟的安全异步读写解决方案了。例如基于 &lt;code&gt;asyncio&lt;/code&gt; 多协程的实现：aiorwlock。&lt;/p&gt;
&lt;p&gt;感兴趣的话，可以自行阅读源码了解。&lt;/p&gt;
</content:encoded></item><item><title>C# 二周目学习 —— 异步编程</title><link>https://miku.top/posts/csharp-learn-2-async/</link><guid isPermaLink="true">https://miku.top/posts/csharp-learn-2-async/</guid><description>C# 的异步主要包括基于线程、线程池和 Task 调度实现的协程</description><pubDate>Thu, 16 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;C# 二周目学习 —— 异步编程&lt;/h1&gt;
&lt;h2&gt;一、多线程&lt;/h2&gt;
&lt;h3&gt;1、Thread 基本方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 引用：
using System.Threading;

// 本质是传递委托
var t = new Thread(方法名);
t.Start();

// 带参执行，参数需要封装为一个 object
new Thread(带参数方法名);
t.Start(参数);

// 休眠和阻塞
t.Sleep(5000);
t.Join();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、阻塞判断&lt;/h3&gt;
&lt;p&gt;​    通过 ThreadState flag 枚举判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool blocked = (someThread.ThreadState &amp;amp; ThreadState.WaitSleep) != 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    线程状态图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20230214163331824.DfyYkP69_ZvvJAX.webp&quot; alt=&quot;image-20230214163331824&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​    它大部分的枚举值都没什么用，下面的代码将 ThreadState 剥离为四个最有用的值之一：Unstarted、Running、 WaitSleepJoin 和 Stopped：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ts 是传递过来的 ThreadState
ts &amp;amp; (ThreadState.Unstarted | ThreadState.WaitSleepJoin | ThreadState.Stopped);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、线程本地、共享状态和线程安全&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;本地独立
&lt;ul&gt;
&lt;li&gt;每个线程的本地变量是独立的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;共享
&lt;ul&gt;
&lt;li&gt;如果多个线程都引用到同一个对象的实例，那么它们就共享了数据。&lt;/li&gt;
&lt;li&gt;被 Lambda 表达式或匿名委托所捕获的本地变量，会被编译器转化为字段（field），所以也会被共享。&lt;/li&gt;
&lt;li&gt;静态字段（field）也会在线程间共享数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    加锁方法：（使用 lock 语句加锁，锁可以基于&lt;strong&gt;任何引用类型&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static readonly object _locker = new object();
// 使用时：
lock (_locker) {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、向线程传递参数&lt;/h3&gt;
&lt;p&gt;​    使用 lambda 表达式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void Main() {
    string abc = &quot;123&quot;;
    Thread t = new Thread(() =&amp;gt; Print(abc));
}

static void Print(string msg) {
    Console.WriteLine(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    或者直接把逻辑也写在 lambda 表达式中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void Main() {
    new Thread(() =&amp;gt; {
        Console.WriteLine(&quot;Hello&quot;);
    }).Start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    注意这样写 lambda 表达式不要捕获到外面的变量。&lt;/p&gt;
&lt;h3&gt;5、异常处理&lt;/h3&gt;
&lt;p&gt;​    要在线程内部捕获异常，而不是在创建、启动的地方。&lt;/p&gt;
&lt;p&gt;​    同时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 WPF、WinForm 里，可以订阅全局异常处理事件：
&lt;ul&gt;
&lt;li&gt;Application.DispatcherUnhandledException&lt;/li&gt;
&lt;li&gt;Application.ThreadException&lt;/li&gt;
&lt;li&gt;在通过消息循环调用的，程序的任何部分发生未处理的异常（这相当于应用程序处于活动状态时在主线程上运行的所有代码）后，将触发这些异常。&lt;/li&gt;
&lt;li&gt;但是非UI线程上的未处理异常，并不会触发它。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不过，任何线程有任何未处理的异常都会触发 AppDomain.CurrentDomain.UnhandledException&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6、前台线程和后台线程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认情况下，你手动创建的线程就是前台线程。&lt;/li&gt;
&lt;li&gt;只要有前台线程在运行，那么应用程序就会一直处于活动状态。
&lt;ul&gt;
&lt;li&gt;但是后台线程却不行。一旦所有的前台线程停止，那么应用程序就停止了，任何的后台线程也会突然终止。&lt;/li&gt;
&lt;li&gt;应用程序无法正常退出的一个常见原因就是还有&lt;strong&gt;活跃的前台线程&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果后台线程最后强行终止，其内部的 finally 块也是不会执行的&lt;/li&gt;
&lt;li&gt;注：线程的前台、后台状态与它的优先级无关（所分配的执行时间）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    可以通过 &lt;code&gt;t.IsBackGround&lt;/code&gt; 属性来判断和修改线程的前后台状态。&lt;/p&gt;
&lt;h3&gt;7、线程优先级&lt;/h3&gt;
&lt;p&gt;​    线程的优先级（Thread 的 Priority 属性）决定了相对于操作系统中其它活跃线程所占的执行时间。优先级分为：&lt;code&gt;enum ThreadPriority{Lowest，BelowNormal，Normal，AboveNormal，Highest}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;​    特别注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果想让某线程（Thread）的优先级比其它进程（Process）中的线程（Thread）
高，那就必须提升进程（Process）的优先级&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 System.Diagnostics 下的 Process 类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;using (Process p = Process.GetCurrentProcess())
    p.PriorityClass = ProcessPriorityClass.High;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;提高优先级很好地用于&lt;strong&gt;只做少量工作且需要较低延迟&lt;/strong&gt;的非 UI 进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于需要大量计算的应用程序（尤其是有UI的应用程序），提高进程优先级可能会使其他进程饿死，从而降低整个计算机的速度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8、信号&lt;/h3&gt;
&lt;p&gt;​    有时，你需要让某线程一直处于等待的状态，直至接收到其它线程发来的通知。这就叫做 signaling（发送信号）。最简单的信号结构就是 ManualResetEvent。&lt;/p&gt;
&lt;p&gt;​    调用它上面的 WaitOne 方法会阻塞当前的线程，直到另一个线程通过调用 Set 方法来开启信号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void Main() {
    var signal = new ManualResetEvent(false);
    new Thread(() =&amp;gt; {
        Console.WriteLine(&quot;Waiting for signal...&quot;);
        signal.WaitOne();
        signal.Dispose();
        Console.WriteLine(&quot;finish&quot;);
    }).Start();
    
    Thread.Sleep(3000);
    signal.Set();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    之后可以调用 Reset 方法来再次将其关闭。&lt;/p&gt;
&lt;h3&gt;9、富客户端 App 的线程&lt;/h3&gt;
&lt;p&gt;​    在 WPF，UWP，WinForm 等类型的程序种，如果在主线程执行耗时的操作，就会导致整个程序无响应。因为主线程同时还需要处理消息循环，而渲染和鼠标键盘事件处理等工作都是消息循环来执行的。&lt;/p&gt;
&lt;p&gt;​    针对这种耗时的操作，一种流行的做法是启用一个 worker 线程。执行完操作后，再更新到 UI。&lt;/p&gt;
&lt;p&gt;​    富客户端应用的线程模型通常是：Ul 元素和控件只能从创建它们的线程来进行访问（通常是主 UI 线程）·当想从 worker 线程更新UI的时候，你必须把请求交给 UI 线程。&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;比较底层的实现是&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WPF，在元素的 Dispatcher 对象上调用 Beginlnvoke或 Invoke。&lt;/li&gt;
&lt;li&gt;WinForm，调用空间的 Beginlnvoke或 Invoke。&lt;/li&gt;
&lt;li&gt;UWP，调用 Dispatcher 对象上的 RunAsync或 Invoke。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    所有这些方法都接受一个委托。&lt;/p&gt;
&lt;p&gt;​    但是注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Beginlnvoke 或RunAsync 通过将委托排队到 UI 线程的消息队列来执行工作。&lt;/li&gt;
&lt;li&gt;Invoke执行相同的操作，但随后会进行阻塞，直到UI线程读取并处理消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    因此，Invoke 允许从方法中获取返回值。如果不需要返回值，Beginlnvoke/RunAsync 更可取，因为它们不会阻塞调用方，也不会引入死锁的可能性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 以 wpf 为例
private void Button_Click(object sender, RoutedEventArgs e) {
    new Thread(Work).Start();
}
void Work() {
    // 模拟耗时操作
    Thread.Sleep(5000);
    UpdateMessage(&quot;The msg&quot;);
}
void UpdateMessage(string message) {
    Action action = () =&amp;gt; TxtMessage.Text = message;
    Dispatcher.BeginInvoke(action);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10、同步上下文&lt;/h3&gt;
&lt;p&gt;​    在 System.ComponentModel 下有一个抽象类：SynchronizationContext，它使得 Thread Marshaling 得到泛化。针对移动、桌面（WPF，UWP，WinForms）等富客户端应用的API，它们都定义和实例化了 SynchronizationContext 的子类。&lt;/p&gt;
&lt;p&gt;​    可以通过静态属性 SynchronizationContext.Current 来获得（当运行在 UI 线程时）。捕获该属性让你可以在稍后的时候从 worker 线程向 UI 线程发送数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public partial class MainWindow: Window {
    SynchronizationContext _uiSyncCtx;

    public MainWindow() {
        InitializeComponent();
        _uiSyncCtx = SynchronizationContext.Current;
        new Thread(Work).Start();
    }

    void Work() {
        Thread.Sleep(5000);
        UpdateMessage(&quot;The msg&quot;);
    }

    void UpdateMessage(string message) {
        // 把委托 marshal 给 UI 线程
        _uiSyncCtx.Post(_ =&amp;gt; TxtMessage.Text = message, null);
        // 调用 Post 相当于调用 Dispatcher 或 Control 上的 BeginInvoke 方法
        // 还可以调用一个 Send 方法，等价于 Invoke 方法
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;11、线程池&lt;/h3&gt;
&lt;p&gt;​    特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不可以设置池线程的 Name&lt;/li&gt;
&lt;li&gt;池线程都是后台线程&lt;/li&gt;
&lt;li&gt;阻塞池线程会使性能降级&lt;/li&gt;
&lt;li&gt;你可以自由的更改池线程的优先级，当它释放回池的时候优先级将还原为正常状态&lt;/li&gt;
&lt;li&gt;可以通过 Thread.CurrentThread.IsThreadPoolThread 属性来判断是否执行在池线程上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    最简单的、显式的在线程池运行代码的方式是使用 Task.Run（细节稍后再说）。&lt;/p&gt;
&lt;p&gt;​    保持线程池的整洁很重要，如果遵循以下两点，CLR 对线程池的调度策略将发挥出最佳效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作项大多是短时间运行的（&amp;lt;250ms，理想情况下&amp;lt;100ms）&lt;/li&gt;
&lt;li&gt;大部分时间都被阻塞的工作项不会主宰线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Task&lt;/h2&gt;
&lt;h3&gt;1、开始一个 Task&lt;/h3&gt;
&lt;p&gt;​    Thread 是用来创建并发的一种低级别工具。限制主要体现在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从线程中获取数据比较麻烦，需要共享字段&lt;/li&gt;
&lt;li&gt;如果抛出异常，捕获和传播异常也很麻烦&lt;/li&gt;
&lt;li&gt;让线程在结束时，执行回调也很麻烦&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    而 Task 就可以很好的解决以上问题。Task 是一个高级的抽象，代表了一个并发操作。Task 在底层可能会由 Thread 支持，也有可能不是，取决于一些具体情况。同时 Task 是可组合的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Threading.Tasks;

class Test{
    static void Main(string[] args) {
        Task.Run(() =&amp;gt; Console.WriteLine(123)); 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    注：Task 默认使用线程池，也就是后台线程。所以主线程执行完成后，Task 所在线程直接终止。上述例子将会没有任何输出。&lt;/p&gt;
&lt;p&gt;​    Task.Run 之后，创建的是&quot;热&quot;任务，立即准备调度执行。也可以通过 Task 构造函数创建&quot;冷&quot;任务，但是一般不这样做。&lt;/p&gt;
&lt;p&gt;​    调用 task 上的 Wait 方法将阻塞直到操作完成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task.Run(() =&amp;gt; Console.WriteLine(123));
Console.WriteLine(task.IsCompleted);
task.Wait();
Console.WriteLine(task.IsCompleted);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    默认情况下，CLR 在线程池中运行 Task，这很适合短时间的 compute-bound 任务。但对于长时间的任务或阻塞操作，可以不采用线程池：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task task = Task.Factory.StartNew(() =&amp;gt; {
    Thread.Sleep(3000);
    Console.WriteLine(&quot;Foo&quot;);
}, TaskCreationOptions.LongRunning);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    但如果需要运行多个长时间任务，还有更好的替代方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IO-bound 通过 TaskCompletetionSource 和异步函数，基于回调实现并发（细节稍后再说）&lt;/li&gt;
&lt;li&gt;Compute-bound 可以使用生产者消费者队列限流，避免饿死其他线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    特别注意：&lt;strong&gt;任何等待 task 的行为，超时之后是不会自动中止 task 的，如果需要中止需要 CancellationToken 协助操作，这个后面会说&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;2、Task 返回值&lt;/h3&gt;
&lt;p&gt;​    Task 有一个泛型子类 &lt;code&gt;Task&amp;lt;TResult&amp;gt;&lt;/code&gt; ，允许 Task 返回一个返回值。&lt;/p&gt;
&lt;p&gt;​    使用 &lt;code&gt;Func&amp;lt;TResult&amp;gt;&lt;/code&gt; 委托或兼容的 lambda 表达式来调用 Task.Run 就可以得到 &lt;code&gt;Task&amp;lt;TResult&amp;gt;&lt;/code&gt; ，随后使用 Result 属性就可以获得返回值，如果还没有完成操作，访问该属性将会阻塞。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task&amp;lt;int&amp;gt; task = Task.Run(() =&amp;gt; {
    Console.WriteLine(&quot;Task running&quot;);
    // 计算质数数，是一个耗时操作
    Enumerable.Range(2, 3000000).Count(n =&amp;gt; 
        Enumerable.Range(2, (int) Math.Sqrt(n)-1).All(i =&amp;gt; n % i &amp;gt; 0));
});
int result = task.Result;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、Task 异常&lt;/h3&gt;
&lt;p&gt;​    与 Thread 不一样，Task 可以很方便的传播异常。如果你的 task 里面抛出了一个未处理的异常（故障），那么该异常就会重新被抛出给：调用了Wait 的地方或访问 &lt;code&gt;Task&amp;lt;TResult&amp;gt;.Result&lt;/code&gt; 的地方。同时异常会被包裹在 &lt;code&gt;AggregateException&lt;/code&gt; 中传递。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task task = Task.Run(() =&amp;gt; { throw null; });
try {
    task.Wait();
}
catch (AggregateException aex)  {
    if (aex.InnerException is NullReferenceException) {
        Console.WriteLine(&quot;Null&quot;);
    }
    else {
        throw;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    如果不想传递异常，只是想知道 task 是否发生故障，可以通过 Task 的 IsFaulted 和 IsCanceled 属性检测。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果两个属性都返回 false，那么没有错误&lt;/li&gt;
&lt;li&gt;如果 IsCanceled 为 true，那么说明被取消，异常抛出为 OperationCanceledException&lt;/li&gt;
&lt;li&gt;如果 IsFaulted 为 true，说明另一个类型的异常被抛出了，而 Exception 属性将会指明错误&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    &lt;strong&gt;自治的 Task&lt;/strong&gt;：不通过 Wait，Result 或 continuation 会合的任务。对于这种任务，需要显式的处理异常，否则就是&quot;未观察到的异常&quot;。对于这种异常，可以通过全局的 TaskScheduler.UnobservedTaskException 来订阅未观察到的异常。&lt;/p&gt;
&lt;p&gt;​    注意：使用超时进行等待的 Task，如果在超时后发生故障，那么它将会产生一个&quot;未观察到的异常&quot;。&lt;/p&gt;
&lt;h3&gt;4、Continuation&lt;/h3&gt;
&lt;p&gt;​    Continuation 一般代指 task 完成后，紧接着执行的操作（一般用一个委托表示）。一般是通过回调来实现的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task&amp;lt;int&amp;gt; primeNumberTask = Task.Run(() =&amp;gt;
    Enumerale.Range(2, 3000000).Count(n =&amp;gt;
        Enumerable.Range(2, (int) Math.Sqrt(n)-1).All(i =&amp;gt; i % n &amp;gt; 0)));

var awaiter = primeNumberTask.GetAwaiter();
awaiter.OnCompleted(() =&amp;gt; {
    int result = awaiter.GetResult();
    Console.WriteLine(result);
});
Console.WriteLine();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    任何可以暴露下列两个方法和一个属性的对象就是 awaiter：（没有接口或父类来统一这些成员）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OnCompleted 方法（实现 INotifyCompletion）&lt;/li&gt;
&lt;li&gt;GetResult 方法&lt;/li&gt;
&lt;li&gt;IsCompleted 属性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    &lt;strong&gt;关于异常&lt;/strong&gt;：如果之前的任务发生故障，那么当 Continuation 代码调用 awaiter.GetResult 的时候，异常会被重新抛出。但调用 GetResult 的好处是，如果 task 发生故障，那么异常会被直接抛出。&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;关于同步上下文&lt;/strong&gt;：如果同步上下文出现了，那么 &lt;strong&gt;OnCompleted 会自动捕获它，并将 Continuation 提交到这个上下文中&lt;/strong&gt;。这一点在富客户端 App 中非常有用，因为它会把 Continuation 放回到 UI 线程中。不过如果是开发库，应该避免在方法调用间切换线程。可以使用 ConfigureAwait 来避免这种行为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var awaiter = task.ConfigureAwait(false).GetAwaiter();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    另一种附加 continuation 的方法，是使用 ContinueWith：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task&amp;lt;int&amp;gt; primeNumberTask = Task.Run(() =&amp;gt;
    Enumerale.Range(2, 3000000).Count(n =&amp;gt;
        Enumerable.Range(2, (int) Math.Sqrt(n)-1).All(i =&amp;gt; i % n &amp;gt; 0)));

primeNumberTask.ContinueWith(t =&amp;gt; {
    int result = awaiter.GetResult();
    Console.WriteLine(result);
});
Console.WriteLine();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    因为 ContinueWith 返回一个 task，所以可以在其后附加更多的 Continuation。但这时需要处理 AggregateException。&lt;/p&gt;
&lt;p&gt;​    注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 task 发生故障，需要写额外的代码来把 Continuation 封装（marshal）到 Ul 应用上。&lt;/li&gt;
&lt;li&gt;在非 UI 上下文中，若想让 Continuation 和 task 执行在同一个线程上，必须在 ContinueWith 时指定 TaskContinuationOptions.ExecuteSynchronously，否则它将弹回到线程池。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5、TaskCompletionSource&lt;/h3&gt;
&lt;p&gt;​    TaskCompletionSource 是创建 Task 的另一种方式。&lt;/p&gt;
&lt;p&gt;​    TaskCompletionSource 让你在稍后开始和结束的任意操作中创建 Task。它会为你提供一个可手动执行的&quot;从属 Task，这种方式对于 IO-bound 类工作比较理想。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static Task&amp;lt;TResult&amp;gt; Run&amp;lt;TResult&amp;gt;(Func&amp;lt;TResult&amp;gt; function) {
    var tcs = new TaskCompletionSource&amp;lt;TResult&amp;gt;();
    new Thread(() =&amp;gt; {
        try {
            tcs.SetResult(function());
        }
        catch (System.Exception ex) {
            tcs.SetException(ex);
        }
    }).Start();
    return tcs.Task;
}

static void Main() {
    Task&amp;lt;int&amp;gt; task = Run(() =&amp;gt; {
        Thread.Sleep(5000);
        return 42;
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    TaskCompletionSource 的另一个好处是：它创建 Task，但不占用线程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用 TaskCompletionSource 实现一个用于延时的 Task
// 与 Task.Delay 方法类似，都相当于异步的 Thread.Sleep
static void Main() {
    Delay(5000).GetAwaiter().OnCompleted(() =&amp;gt; Console.WriteLine(42));
    // 5s 后，Continuation 开始的时候，才占用线程
}

static Task Delay(int milliseconds) {
    var tcs = new TaskCompletionSource&amp;lt;object&amp;gt;();
    var tiemr = new System.Timers.Timer(milliseconds) {AutoReset = false};
    timer.Elapsed += delegate { timer.Dispose(); tcs.GetResult(); };
    timer.Start();
    return tcs.Task;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    甚至，我们可以实现既可以同步等待，又可以异步等待的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class WaitSyncOrAsync&amp;lt;TResult&amp;gt; {
    private readonly TaskCompletionSource&amp;lt;TResult&amp;gt; _source = 
        new TaskCompletionSource&amp;lt;TResult&amp;gt;();
    
    public WaitSyncOrAsync(Func&amp;lt;TResult&amp;gt; function) {
        new Thread(() =&amp;gt; {
            try {
                _source.SetResult(function());
            }
            catch (System.Exception ex) {
                _source.SetException(ex);
            }
        }).Start();
    }
    public Task WaitAsync() =&amp;gt; _source.Task;
    public TResult Wait() =&amp;gt; _source.Task.GetAwaiter().GetResult();
}

// 使用时：
var demo = new WaitSyncOrAsync&amp;lt;int&amp;gt;(() =&amp;gt; 42);
int res = await demo.WaitAsync();
int res2 = demo.Wait();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、async 和  await&lt;/h2&gt;
&lt;h3&gt;1、使用&lt;/h3&gt;
&lt;p&gt;​    async 和 await 关键字可以让你写出和同步代码一样简洁且结构相同的异步代码。&lt;/p&gt;
&lt;p&gt;​    关于 await 关键字：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;await 关键字简化了附加 continuation 的过程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一般会 await Task 对象。但也可以 await 自定义对象，该对象要满足：有一个 GetAwaiter 方法返回一个 awaiter 对象。注意：如果需要，GetAwaiter 其实可以返回自身，然后顺便实现 awaiter 要求的所有方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有时候方法返回 void，此时如果想要实现异步，可以标注返回类型为 Task，此时编译器内部会隐式的返回 Task（实际上，是通过 TaskCompletionSource 封装的）&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var result = await expression;
statement(s);

//等价于：
var awaiter = expression.GetAwaiter();
awaiter.OnCompleted(() =&amp;gt; {
    var result = awaiter.GetResult();
    statement(s);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    关于 async 关键字：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;async 修饰符会让编译器把 await当作关键字而不是标识符&lt;/li&gt;
&lt;li&gt;async 修饰符只能应用于方法（包括 lambda 表达式）。
&lt;ul&gt;
&lt;li&gt;该方法可以返回 &lt;code&gt;void&lt;/code&gt;、&lt;code&gt;Task&lt;/code&gt;、&lt;code&gt;Task&amp;lt;TResult&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;async 修饰符对方法的签名和 public 元数据没有影响（和 unsafe 一样），它只会影响方法内部。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    &lt;strong&gt;await 后在哪个线程上执行&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在富客户端应用的 UI 线程上，await 后同步上下文会保证后续是在原线程上执行；&lt;/li&gt;
&lt;li&gt;如果没有同步上下文，就会在 task 结束的线程上继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    异步调用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static async void Main() {
    // 这里不加 await 得到的就是 Task&amp;lt;int&amp;gt;，而不是 int，此时将是并行
    int res = await GetAnswerToLife();
}

async Task&amp;lt;int&amp;gt; GetAnswerToLife() {
    await Task.Delay(5000);
    int answer = 42;
    return answer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    异步 lambda 表达式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static async void Main() {
    Func&amp;lt;Task&amp;gt; unnamed = async () =&amp;gt; {
        await Task.Delay(1000);
        Console.WriteLine(&quot;foo&quot;);
    };
    await unnamed();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、取消&lt;/h3&gt;
&lt;p&gt;​    使用取消标志来实现对并发进行取消。为了实现取消，CLR 提供了 CancellationToken 这个类。但是具体的 Cancel 方法并不存在于这个类上，而是出于安全考虑，将取消方法暴露在 CancellationTokenSource 这个类上。具体使用如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var cancelSource = new CancellationTokenSource();
Task foo = Foo(cancelSource.Token);
// 一段时间后，或某些条件下：
cancelSource.Cancel();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    CLR 中大部分的异步方法都是支持附带 CancellationToken 的。而某些同步方法，也是可以支持取消的（如 Task 的 Wait 方法，可以附带 token）&lt;/p&gt;
&lt;p&gt;​    而实例化 CancellationTokenSource 时，也可以传入一个时间间隔，以便到时后自动取消，这样也类比实现了超时的效果。&lt;/p&gt;
&lt;p&gt;​    另外，CancellationToken 上提供了一个 Register 方法，通过这个方法，我们可以注册取消后回调的委托。&lt;/p&gt;
&lt;p&gt;​    特别注意：token 可以很方便的在 task 调度执行前立即进行取消。但是任务一旦开始执行，token 使得任务终止的逻辑是：&lt;strong&gt;不能使任务立即中止，而是要由任务内部定时检测 token 是否处于取消状态，并抛出 OperationCanceledException 异常&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​    具体操作有以下三类方法，记得先向 task 内部传入 token：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 如果是 IO-bound task，在 task 内部，推荐使用这种方式：
// 记得外部捕获 OperationCanceledException，task 之后将变为 canceled 状态
var IOCloseMethod = ... // 对应具体的关闭 IO 操作
using (token.Register(IOCloseMethod)) {
    ... // 具体的操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 如果是 compute-bound task，建议每循环一段时间进行检查：
// 记得外部捕获 OperationCanceledException，task 之后将变为 canceled 状态
for (int i = 0; i &amp;lt; 10000; i++) {
    ... // 具体的操作
    if (i % 1000 == 0) token.ThrowIfCancellationRequested();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 如果是一段无法插入检查的阻塞型代码需要取消
// 可以直接选择终止线程（这种方法一定要慎用，注意资源安全问题，以及不要终止到主线程）
// 外部无需捕获异常，此时外部状态将为 faulted
Thread t = Thread.CurrentThread;
using (token.Register(t.Abort)) {
    ... // 具体的操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、进度报告&lt;/h3&gt;
&lt;p&gt;​    有时，可能希望异步操作在运行时能实时地反馈进度。一个简单的解决办法是向异步方法传入一个 Action 委托，当进度变化的时候触发方法调用。但是在富客户端 App 中，在 worker 线程报告进度，可能会引发线程安全问题。&lt;/p&gt;
&lt;p&gt;​    因此 CLR 提供了一对类型来解决这个问题：&lt;code&gt;IProgress&amp;lt;T&amp;gt;&lt;/code&gt; 和 &lt;code&gt;Progress&amp;lt;T&amp;gt;&lt;/code&gt; （分别是接口和对应的实现类）。&lt;code&gt;Progress&amp;lt;T&amp;gt;&lt;/code&gt; 的一个构造函数可以接受 &lt;code&gt;Action&amp;lt;T&amp;gt;&lt;/code&gt;类型的委托。&lt;/p&gt;
&lt;p&gt;​    当然，也可以使用空构造，不过这个时候要绑定委托，要绑定在 &lt;code&gt;Progress&amp;lt;T&amp;gt;&lt;/code&gt; 对象的 ProgressChanged 事件上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 实例化
var processObj = new Progress&amp;lt;int&amp;gt;(reportMethod);

// 调用
Task Foo(IProgress&amp;lt;int&amp;gt; progressObj) {
    return Task.Run(() =&amp;gt; {
        for (int i = 0; i &amp;lt; 1000; i++) {
            if (i % 10 == 0)
                progressObj.Report(i / 10);
        }
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、TAP 模式&lt;/h3&gt;
&lt;p&gt;​    TAP 模式（Task-Based Asynchronous Pattern），即基于 Task 的异步模式。&lt;/p&gt;
&lt;p&gt;​    CLR 暴露了数百个返回 Task 且可以 await 的异步方法（主要和 I/O 相关）。其中，大多数方法都遵循一个模式，叫做基于Task的异步模式（TAP）。TAP 方法有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回一个&quot;热&quot;（运行中的）&lt;code&gt;Task&lt;/code&gt; 或 &lt;code&gt;Task&amp;lt;TResult&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;方法名以 Async 结尾（除了像 Task 组合器等情况）&lt;/li&gt;
&lt;li&gt;有重载，以便接受 &lt;code&gt;CancellationToken&lt;/code&gt; 或（和）&lt;code&gt;IProgress&amp;lt;T&amp;gt;&lt;/code&gt;，如果支持相关操作的话&lt;/li&gt;
&lt;li&gt;快速返回调用者（只有很小的初始化同步阶段）&lt;/li&gt;
&lt;li&gt;如果是 I/O 绑定，则不会绑定线程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、Task 组合器&lt;/h3&gt;
&lt;p&gt;​    可以组合 Task，但不关心 Task 做什么的函数，主要用于调度一堆 Task。&lt;/p&gt;
&lt;p&gt;​    WhenAny 组合器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 一个完成就返回，结果存储在 
Task&amp;lt;int&amp;gt; winningTask = await Task.WhenAny(func1(), func2(), func3());
int res = await winningTask;

// 也可以这样做
int res = await await Task.WhenAny(func1(), func2(), func3());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;没胜出的 task 后续如果发生异常，那么异常不会被观察到，除非后续进行单独 await 或查询 Exception 属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WhenAny 也可以添加超时，同时，可以利用这一点为不支持超时或取消的操作添加超时或取消：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;Task&amp;lt;string&amp;gt; task = SomeAsyncFunc();
Task winner = await (Task.WhenAny(task, Task.Delay(5000)));
if (winner != task) throw new TimeoutException();
string result = await task;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    WhenAll 组合器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 只有所有 task 都完成后，Task.WhenAll 才会返回一个 Task
Task&amp;lt;int[]&amp;gt; wrapTask = await Task.WhenAll(func1(), func2(), func3());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WhenAll 在等待过程中，即使某些 task 发生了异常，也依然会等待所有全部完成&lt;/li&gt;
&lt;li&gt;如果存在多个异常，各个 task 的异常会包裹在 wrapTask 的 AggregateException 中。
&lt;ul&gt;
&lt;li&gt;但注意，如果使用 await wrapTask 方式，则抛出子 task 中，入参顺序最靠前的 task 的异常&lt;/li&gt;
&lt;li&gt;如果使用 wrapTask.Wait() 方式，则抛出 AggregateException 异常&lt;/li&gt;
&lt;li&gt;因此建议直接捕获 Exception，然后直接判断 wrapTask.Exception.InnerExceptions.Count&lt;/li&gt;
&lt;li&gt;同时，如果任务有多种返回类型，wrapTask 不会有 Result 属性，需要自行遍历任务&lt;/li&gt;
&lt;li&gt;如果都是相同类型的任务，其中一个发生异常，wrapTask 的 Result 将会是 null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    除此之外，还有 WaitAll、WaitAny 方法，行为与上述方法一致，不过它们是同步的。&lt;/p&gt;
&lt;p&gt;​    特别注意：&lt;strong&gt;任何 task 组合器，获得结果时，不会自动使内部的其他任务中止，需要手动使用 CancellationToken 进行协助中止&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;5、自定义 task 组合器&lt;/h3&gt;
&lt;p&gt;​    例如一个 task 出错，所有 task 也出错的 WhenAll：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static async Task&amp;lt;TResult[]&amp;gt; WhenAllOrError&amp;lt;TResult&amp;gt;(params Task&amp;lt;TResult&amp;gt;[] tasks) {
    var killJoy = new TaskCompletionSource&amp;lt;TResult[]&amp;gt;();
    foreach(var task in tasks)
        #pragma warning disable 4014
        task.ContinueWith(ant =&amp;gt; {
            if (ant.IsCanceled)
                killJoy.TrySetCanceled();
            else if (ant.IsFaulted)
                killJoy.TrySetException(ant.Exception.InnerException);
        });
        #pragma warning enable 4014
    return await await Task.WhenAny(
        killJoy.Task, Task.WhenAll(tasks)
    ).ConfigureAwait(false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    例如为 WhenAll 添加超时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static async Task&amp;lt;TResult[]&amp;gt; WhenAll&amp;lt;TResult&amp;gt;(
    Task&amp;lt;TResult&amp;gt;[] tasks,
    float? seconds=null,
    CancellationTokenSource? tokenSrc=null
)
{
    if (seconds == null) return await Task.WhenAll(tasks).ConfigureAwait(false);

    Task timerTask = Task.Delay((int) seconds * 1000);
    timerTask.ContinueWith(t =&amp;gt; {
        if (tokenSrc != null) tokenSrc.Cancel();
    });

    Task&amp;lt;TResult[]&amp;gt; workTask = Task.WhenAll(tasks);
    Task t = await Task.WhenAny(timerTask, workTask).ConfigureAwait(false);
    if (t == timerTask) {
        throw new TimeoutException(&quot;超时，任务组中有任务尚未完成！&quot;);
    }
    else {
        if (tokenSrc != null) tokenSrc.Cancel();
        return await workTask.ConfigureAwait(false);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C# 一周目学习 —— 基本语法</title><link>https://miku.top/posts/csharp-learn-1/</link><guid isPermaLink="true">https://miku.top/posts/csharp-learn-1/</guid><description>C# 是一种基于 .NET 框架的、面向对象的高级编程语言</description><pubDate>Sat, 04 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;C# 一周目学习 —— 基本语法&lt;/h1&gt;
&lt;h2&gt;零、输入输出&lt;/h2&gt;
&lt;h3&gt;1、输入&lt;/h3&gt;
&lt;p&gt;​    主要使用 &lt;code&gt;Console.Read()&lt;/code&gt; 和 &lt;code&gt;Console.ReadLine()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h3&gt;2、输出&lt;/h3&gt;
&lt;p&gt;​    主要使用 &lt;code&gt;Console.Write()&lt;/code&gt; 和 &lt;code&gt;Console.WriteLine()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;​    关于字符串格式化，参见：https://www.cnblogs.com/fskong/p/16940917.html&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、类与命名空间&lt;/h2&gt;
&lt;h3&gt;1、概念&lt;/h3&gt;
&lt;p&gt;​    类（class）构成程序的主体&lt;/p&gt;
&lt;p&gt;​    名称空间（namespace）以树型结构组织类（和其他类型）&lt;/p&gt;
&lt;p&gt;​    能在命名空间中定义的元素有：类(Class)，接口(Interface)，结构(struct)，委托(delegate)，枚举(enum)&lt;/p&gt;
&lt;h3&gt;2、类库和引用&lt;/h3&gt;
&lt;p&gt;​    类库引用是使用名称空间的物理基础。不同技术类型的项目会默认引用不同的类库。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DLL 引用（黑盒引用，无源代码）&lt;/li&gt;
&lt;li&gt;项目引用（白盒引用，有源代码）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、类的三大成员&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;属性&lt;/li&gt;
&lt;li&gt;方法&lt;/li&gt;
&lt;li&gt;事件
&lt;ul&gt;
&lt;li&gt;类或对象通知其它类或对象的机制，为C#所特有&lt;/li&gt;
&lt;li&gt;善用事件机制非常重要&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    各种类的侧重：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型类或对象重在属性，如 Entity Framework&lt;/li&gt;
&lt;li&gt;工具类或对象重在方法，如 Math，Console&lt;/li&gt;
&lt;li&gt;通知类或对象重在事件，如各种 Timer&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、静态成员与实例成员&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;静态（Static）成员在语义上表示它是&quot;类的成员&quot;&lt;/li&gt;
&lt;li&gt;实例（非静态）成员在语义表示它是&quot;对象的成员&quot;&lt;/li&gt;
&lt;li&gt;绑定（Binding）指的是编译器如果把一个成员与类或对象关联起来&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;二、数据类型&lt;/h2&gt;
&lt;h3&gt;1、五大类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;类（classes）如：Window, Form, Console, String&lt;/li&gt;
&lt;li&gt;结构体（structures）如：Int32, Int64, Single, Double&lt;/li&gt;
&lt;li&gt;枚举（Enumerations）如：HorizontalAlignment, Visibility&lt;/li&gt;
&lt;li&gt;接口（Interfaces）&lt;/li&gt;
&lt;li&gt;委托（Delegates）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    类型的派生谱系：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20230122174232276.BfXsvLEA_Z1tHvA8.webp&quot; alt=&quot;image-20230122174232276&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、基本数据类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;范围&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;布尔值&lt;/td&gt;
&lt;td&gt;true 或 false&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;byte&lt;/td&gt;
&lt;td&gt;8 位无符号整数&lt;/td&gt;
&lt;td&gt;0 到 255&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;char&lt;/td&gt;
&lt;td&gt;16 位 Unicode 字符&lt;/td&gt;
&lt;td&gt;U +0000 到 U +ffff&lt;/td&gt;
&lt;td&gt;&apos;\0&apos;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decimal&lt;/td&gt;
&lt;td&gt;128 位精确的十进制值，28-29 有效位数&lt;/td&gt;
&lt;td&gt;(-7.9 x 1028 到 7.9 x 1028) / 100 到 28&lt;/td&gt;
&lt;td&gt;0.0M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;64 位双精度浮点型&lt;/td&gt;
&lt;td&gt;(+/-)5.0 x 10-324 到 (+/-)1.7 x 10308&lt;/td&gt;
&lt;td&gt;0.0D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;float&lt;/td&gt;
&lt;td&gt;32 位单精度浮点型&lt;/td&gt;
&lt;td&gt;-3.4 x 1038 到 + 3.4 x 1038&lt;/td&gt;
&lt;td&gt;0.0F&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;32 位有符号整数类型&lt;/td&gt;
&lt;td&gt;-2,147,483,648 到 2,147,483,647&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;long&lt;/td&gt;
&lt;td&gt;64 位有符号整数类型&lt;/td&gt;
&lt;td&gt;-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807&lt;/td&gt;
&lt;td&gt;0L&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sbyte&lt;/td&gt;
&lt;td&gt;8 位有符号整数类型&lt;/td&gt;
&lt;td&gt;-128 到 127&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;short&lt;/td&gt;
&lt;td&gt;16 位有符号整数类型&lt;/td&gt;
&lt;td&gt;-32,768 到 32,767&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uint&lt;/td&gt;
&lt;td&gt;32 位无符号整数类型&lt;/td&gt;
&lt;td&gt;0 到 4,294,967,295&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ulong&lt;/td&gt;
&lt;td&gt;64 位无符号整数类型&lt;/td&gt;
&lt;td&gt;0 到 18,446,744,073,709,551,615&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ushort&lt;/td&gt;
&lt;td&gt;16 位无符号整数类型&lt;/td&gt;
&lt;td&gt;0 到 65,535&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;​    另外：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以使用 &lt;code&gt;var&lt;/code&gt; 关键字在定义时使用类型推导。&lt;/li&gt;
&lt;li&gt;可以使用 &lt;code&gt;dynamic&lt;/code&gt; 关键字定义&quot;动态类型&quot;的变量，将不会在编译期进行类型检查。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;三、运算符&lt;/h2&gt;
&lt;h3&gt;1、运算符种类与特点&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20230123165129073.DfdEGqJ7_X0lDt.webp&quot; alt=&quot;image-20230123165129073&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​    运算符重载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person {
    public static operator +(Person a, Person b) {
        // place your code
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    default 运算符：https://blog.csdn.net/wsnbbdbbdbbdbb/article/details/125786005&lt;/p&gt;
&lt;p&gt;​    checked, unchecked 是用来检查溢出异常的，可以以上下文形式使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checked {
    try {
        uint y = uint.MaxValue + 1;
        Console.WriteLine(y);
    }
    catch (OverflowException e) {
        Console.WriteLine(&quot;发生溢出&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    delegate 是用于实现委托的运算符，也可以被用来实现匿名方法，不过目前更推荐使用 lambda 表达式。&lt;/p&gt;
&lt;p&gt;​    new 运算符的一些用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;staic void Main(string[] args) {
    // 使用初始化器
    Form myForm = new Form() { Text=&quot;Hello World&quot; }
    // 创建匿名类对象
    var person = new { Name=&quot;Person88&quot;, Age=34 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、类型转换&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;隐式（implicit）类型转换&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不丢失精度的转换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;子类向父类的转换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;装箱&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显式（explicit）类型转换&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有可能丢失精度（甚至发生错误）的转换，即 cast&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拆箱&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用Convert类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ToString 方法与各数据类型的 Parse/TryParse 方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自定义类型转换操作符&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// cast:
int a = (int) double_value;
// Convert 类方法：
Convert.ToDouble(the_str);
// ToString 方法:
the_int.ToString();
// Parse 和 TryParse:
double.Parse();        // 失败直接抛出异常
double.TryParse();    // 返回值为 bool 值，判断是否转换成功
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 自定义显式转换 A var1 = (A) var2;
public static explicit operator A(B var2) {
    // 转换方法，返回 A 类对象
}
// 自定义隐式转换 A var1 = var2;
public static implicit operator A(B var2) {
    // 转换方法，返回 A 类对象
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、is 和 as&lt;/h3&gt;
&lt;p&gt;​    is 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 假设 Teacher 类继承于 Human
Teacher t = new Teacher();
Console.WriteLine(t is Human);    // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    as 示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;object o = new Teacher();

if (o is Teacher) {
    Teacher t = (Teacher) o;
    t.Teach();
}
// 等价于：
Teacher t = o as Teacher;
if (t != null) {
    t.Teach();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、可空类型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 定义 x 为可空 int
int? x = null;
x = 100;
Console.WriteLine(x);
Console.WriteLine(x.HasValue);
// 如果是 null，赋值为 1
int y = x ?? 1;
Console.WriteLine(y);
// 当不为空时，和 2 比较是否相等
x?.Equals(2);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、表达式和语句&lt;/h2&gt;
&lt;h3&gt;1、标签语句和块语句&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;staic void Main(string[] args) {
    // 一个块语句是独立的上下文环境
    {
        hello: Console.WriteLine(&quot;Hello~&quot;);
        goto hello;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、switch 语句&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;switch(score) {
    case score &amp;gt;= 80 &amp;amp;&amp;amp; score &amp;lt;= 100:
        // ...
        break;
    case score &amp;gt;= 60 &amp;amp;&amp;amp; score &amp;lt; 80:
        // ...
        break;
    // case 为 1 和 2 共用一个 section
    case 1:
    case 2:
        // ...
        break;
    default:
        // throw error
        break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、try-catch-finally 语句&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
    // ...
}
catch (aException e) {
    // e 是异常对象
    Console.WriteLine(e.Message);
}
catch (bException e) {
    // ...
}
finally {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    抛出异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;catch(xxx e) {
    throw;
}
// 或
catch (xxx e) {
    throw e;
}
// 或
catch (xxx e) {
    throw new xxx(&quot;custom message&quot;);
}
// 或
throw new xxx;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    详细区别见于：https://blog.csdn.net/zwb_578209160/article/details/119384988&lt;/p&gt;
&lt;h3&gt;4、循环迭代&lt;/h3&gt;
&lt;p&gt;​    &lt;code&gt;while&lt;/code&gt;、&lt;code&gt;do-while&lt;/code&gt;、&lt;code&gt;for&lt;/code&gt; 语句与 C 基本类似。&lt;/p&gt;
&lt;p&gt;​    关于 C# 的迭代器和迭代实质：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;int&amp;gt; intList = new List&amp;lt;int&amp;gt;() {1, 2, 3, 4, 5, 6};
IEnumerator iterator = intList.GetEnumerator();
while (iterator.MoveNext()) {
    Console.WriteLine(iterator.Current);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    而 &lt;code&gt;foreach&lt;/code&gt; 方法则是内部实现了这个过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;int&amp;gt; intList = new List&amp;lt;int&amp;gt;() {1, 2, 3, 4, 5, 6};
foreach(var elem in intList) {
    Console.WriteLine(elem);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、方法参数传递&lt;/h2&gt;
&lt;h3&gt;1、值参数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;传递前，必须先明确赋值&lt;/li&gt;
&lt;li&gt;对于值类型，参数创建变量的副本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于引用类型，参数赋值前是引用，赋值后是新的对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于引用类型，直接修改参数，则外部对象随之改变&lt;/strong&gt;（此时参数和外部值&lt;strong&gt;指向的地址是相同的&lt;/strong&gt;，都指向于堆内存的同一块区域）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2、引用参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void hasSideEffect(ref int x, ref int y) {
    x = 1;
    y = 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;传递前，必须先明确赋值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于值类型，参数不创建变量的副本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对于引用类型，参数赋值前是引用，赋值后将会覆盖外部对象。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对于引用类型，直接修改参数，则外部对象随之改变。&lt;/strong&gt;（此时参数和外部值&lt;strong&gt;指向的地址是不同的&lt;/strong&gt;，参数先指向外部值，外部值指向堆内存中的一块区域）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一般用作&quot;改变&quot;操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、输出参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class DoubleParser {
    public static bool TryParse(string input, out double? result) {
        try {
            result = double.Parse(input);
            return true;
        } catch {
            result = null;
            return false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 使用
double x;
bool status = DoubleParser.TryParse(&quot;123.45&quot;, out x);
if (!status)
    throw new Exception(&quot;转换错误&quot;);
Console.WriteLine(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;传递前，可以先不赋值，&lt;strong&gt;但是在方法内部必须有赋值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;对于值类型和引用类型的相关特点，和引用参数类似。&lt;/li&gt;
&lt;li&gt;一般用作&quot;输出&quot;操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、数组参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static int GetSum(params int[] intArr) {
    int sum = 0;
    foreach(var elem in intArr) {
        sum += elem;
    }
    return sum;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 以下传值方法皆可：
GetSum(1, 2, 3);
GetSum(new int[] {1, 2, 3});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;只能有一个且必须是形参列表中的最后一个，由 params 修饰&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5、具名使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Student stu = new Student(name: &quot;123&quot;, age: 18);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;参数位置不再受约束&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6、可选参数（默认参数）&lt;/h3&gt;
&lt;p&gt;​    和其他语言类似。&lt;/p&gt;
&lt;h3&gt;7、扩展方法&lt;/h3&gt;
&lt;p&gt;​    在不修改源码，或重新编译 dll 的情况下，进行方法扩展：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 声明静态类
static class DoubleExtension {
    public static double Round(this double input, int digits) {
        return Math.Round(input, digits);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 使用
double x = 3.1415926535d;
x.Round(4);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;方法必须是公有、静态的，即被 &lt;code&gt;public static&lt;/code&gt; 所修饰&lt;/li&gt;
&lt;li&gt;必须是形参列表中的第一个，由 &lt;code&gt;this&lt;/code&gt; 修饰&lt;/li&gt;
&lt;li&gt;必须由一个静态类（一般类名为 xxxExtension ）来统一收纳对 xxx 类型的扩展方法&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;六、类、枚举和结构体&lt;/h2&gt;
&lt;h3&gt;1、构造，析构和重载&lt;/h3&gt;
&lt;p&gt;​    &lt;strong&gt;重载&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student {
    public int ID;
    public string Name;
    
    public Student() {
        this.ID = 1;
        this.Name = &quot;Hello&quot;;
    }
    public Student(int id, string name) {
        this.ID = id;
        this.Name = name;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    基本原理和细节与 C++ 类似。&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;静态构造函数&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 静态构造函数只能用于构造静态成员，且必然会先被调用
class Student {
    static Student() {
        Console.WriteLine(&quot;静态构造方法必然先被调用&quot;);
    }
    public Student(string str) {
        Console.WriteLine(str + &quot;你好呀&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    &lt;strong&gt;析构&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student {
    ~Student() {
        Console.WriteLine(&quot;析构完成&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    析构无修饰符，无参，无继承和重载。&lt;/p&gt;
&lt;h3&gt;2、默认访问权限&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;默认访问权限&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;命令空间中只能使用 public 和 internal 两种访问修饰符，默认为 internal。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;枚举、接口默认访问权限为 public，不能修改且不能使用访问修饰符。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类默认为 internal 访问修饰符。 内部默认为 private 访问修饰符。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;析构函数不能显式使用，无修饰符。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类的成员默认访问修饰符为 private。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;嵌套类型的默认访问修饰符为 private。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;派生类的可访问性不能高于基类。成员的可访问性决不能高于包含类的可访问性。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、方法重载&lt;/h3&gt;
&lt;p&gt;​    方法重载需要的条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法名相同&lt;/li&gt;
&lt;li&gt;具有不同的参数个数或参数类型或参数顺序，与参数名无关&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传值参数同时可以与引用参数、输出参数中的一个重载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法重写和覆写与重载不同&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、partial&lt;/h3&gt;
&lt;p&gt;​        partial 可以将类、接口或接口的定义拆分到多个源文件中。方便扩展和管理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Book.cs
namespace BookStore {
    public partial class Book {
        public int ID {get; set;}
        public string Name {get; set;}
        public double Price {get; set;}
        public string Author {get; set;}
    }
}

// BookExtends.cs
namespace BookStore {
    public partial class Book {
        public void Show() {
            Console.WriteLine($&quot;Book {Name}&apos;s price is {Price}.&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、类其他知识&lt;/h3&gt;
&lt;p&gt;​    &lt;strong&gt;类访问权限&lt;/strong&gt;：https://blog.csdn.net/qq_45037155/article/details/123658777&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;关于继承&lt;/strong&gt;：https://blog.csdn.net/QWD8596/article/details/120819502&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;关于静态成员继承&lt;/strong&gt;：https://blog.csdn.net/WuLex/article/details/119342955&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;关于密封（sealed）&lt;/strong&gt;：https://blog.csdn.net/qq_43024228/article/details/89885161&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;关于重载和多态&lt;/strong&gt;：默认不需要 virtual 和 override 关键字，子类也可以实现重写。&lt;strong&gt;但是这些是实现多态的重要手段&lt;/strong&gt;。此部分参考：http://t.zoukankan.com/dotgua-p-6287926.html&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;关于字段，属性和访问器&lt;/strong&gt;：https://blog.csdn.net/weixin_44023930/article/details/123447507（为区分，字段首字母小写，而属性首字母大写）&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;关于抽象类和抽象方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象方法只在抽象类中定义&lt;/li&gt;
&lt;li&gt;继承的实现类通过 override 修饰实现虚方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    &lt;strong&gt;关于抽象属性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只在抽象类中定义&lt;/li&gt;
&lt;li&gt;至少一个访问器&lt;/li&gt;
&lt;li&gt;详细参见：https://blog.csdn.net/chenweicode/article/details/98849554&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6、枚举&lt;/h3&gt;
&lt;p&gt;​    枚举：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;人为限定取值范围的整数&lt;/li&gt;
&lt;li&gt;整数值的对应&lt;/li&gt;
&lt;li&gt;比特位式用法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;enum Level {
    Employee,
    Manager,
    Boss,
    BigBoss
}

// 使用
static void Main() {
    Level lvl = Level.Boss;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    注：默认从 0 开始编号，以 1 递增&lt;/p&gt;
&lt;p&gt;​    也可以自己赋值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 此时 BigBoss 为 301
enum Level {
    Employee=100,
    Manager=200,
    Boss=300,
    BigBoss
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    比特位用法：（用于标记和识别是否具有某些特质）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Skill {
    Drive = 1,
    Cook = 1&amp;lt;&amp;lt;1,
    Program = 1&amp;lt;&amp;lt;2,
    Teach = 1&amp;lt;&amp;lt;3
}

static void Main() {
    Skill HasSkill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach;
    // 判断是否会编程
    Console.WriteLine((HasSkill &amp;amp; Skill.Program) != 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7、结构体&lt;/h3&gt;
&lt;p&gt;​    结构体：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;值类型，可装箱、拆箱&lt;/li&gt;
&lt;li&gt;可实现接口，但不能派生自类或结构体&lt;/li&gt;
&lt;li&gt;不能有显式无参构造器，但可以有显式有参构造器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;interface ISpeak {
    void Speak();
}

struct Student: ISpeak {
    public int ID {get; set;}
    public string Name {get; set;}
    public void Speak() {
        Console.WriteLine($&quot;I&apos;m {ID} student {Name}.&quot;);
    }
}

static void Main() {
    Student stu = new Student() {ID=101, Name=&quot;MelodyEcho&quot;};
    // 装箱和拆箱
    object obj = stu;
    Student stu2 = (Student) obj;
    // 拷贝生成新的值，而不是产生引用
    Student stu3 = stu;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、字段、属性、索引器和常量&lt;/h2&gt;
&lt;h3&gt;1、字段&lt;/h3&gt;
&lt;p&gt;​	简述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字段（field）是一种表示与对象或类型（类与结构体）关联的变量。&lt;/li&gt;
&lt;li&gt;尽管字段声明带有分号，但它不是语句&lt;/li&gt;
&lt;li&gt;字段的名字一定是名词&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    字段的初始值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无显式初始化时，字段获得其类型的默认值，&lt;strong&gt;所以字段永远都不会未被初始化&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    字段的修饰符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;new&lt;/li&gt;
&lt;li&gt;public, protected, internal, private&lt;/li&gt;
&lt;li&gt;static&lt;/li&gt;
&lt;li&gt;readonly&lt;/li&gt;
&lt;li&gt;volatile&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    &lt;code&gt;new&lt;/code&gt; 修饰符用于隐藏子类中继承的父类成员，以实现重写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Base {
    public class Nested {};
    public int var1;
    public void method() {}
}

class Sub: Base {
    new public class Nested {};
    new int var1;
    new void method() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    &lt;code&gt;volatile&lt;/code&gt; ：多个线程同时访问一个变量，CLR 为了效率，允许每个线程进行本地缓存，这就导致了变量的不一致性。volatile 就是为了解决这个问题，volatile 修饰的变量，不允许线程进行本地缓存，每个线程的读写都是直接操作在共享内存上，这就保证了变量始终具有一致性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public volatile int x;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    &lt;code&gt;readonly&lt;/code&gt;：只读成员。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与 &lt;code&gt;get{}&lt;/code&gt; 区别：
&lt;ul&gt;
&lt;li&gt;readonly 只能够初始化一次，即在定义或者构造方法初始化时。而 &lt;code&gt;get{}&lt;/code&gt; 虽然也起到的只读的作用，但可以通过 &lt;code&gt;set{}&lt;/code&gt; 进行多次修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;与 &lt;code&gt;const&lt;/code&gt; 对比：
&lt;ul&gt;
&lt;li&gt;const 常量必须要有初始值，而 readonly 可以没有&lt;/li&gt;
&lt;li&gt;readonly 可以在构造方法中进行赋值，而 const 不行。const 一旦确定值就不可以改变了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2、属性&lt;/h3&gt;
&lt;p&gt;​    属性（property）：是一种用于访问对象或类型的特征的成员，特征反映了状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;field 更偏向于实例对象在内存中的布局，property 更偏向于反映现实世界对象的特征&lt;/li&gt;
&lt;li&gt;对外：暴露数据，数据可以是存储在字段里的，也可以是动态计算出来的&lt;/li&gt;
&lt;li&gt;对内：保护字段不被非法值&quot;污染&quot;&lt;/li&gt;
&lt;li&gt;属性大多数情况下是字段的包装器（wrapper）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    &lt;strong&gt;建议&lt;/strong&gt;：永远使用属性（而不是字段）来暴露数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person {
    // 完整声明
    private int age;
    public int Age {
        get{ return this.age; }
        set {
            if (value &amp;gt;= 0 &amp;amp;&amp;amp; value &amp;lt;= 120) {
                this.age = value;
            }
            else {
                throw new Exception(&quot;超出范围的值&quot;)
            }
        }
    }
    // 简略声明
    public int Height { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 控制访问级别：
public int Height { set; private set; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、索引器&lt;/h3&gt;
&lt;p&gt;​    索引器（indexer）：一种成员。它使对象能够用与数组相同的方式（即使用下标）进行索引。&lt;/p&gt;
&lt;p&gt;​    索引器的声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class StuScore {
    private Dictionary&amp;lt;string, int&amp;gt; dict = new Dictionary&amp;lt;string, int&amp;gt;();
    public int ? this[string subject] {
        get {
            if (this.dict.ContainsKey(subject))
                return this.dict[subject];
            else 
                return null;
        }
        set {
            if (!value.HasValue)
                throw new Exception(&quot;不能为空值&quot;);
            if (this.dict.ContainsKey(subject))
                this.dict[subject] = value.Value;
            else
                this.dict.Add(subject, value.Value);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;StuScore score = new StuScore();
Console.WriteLine(score[&quot;Math&quot;]);
score[&quot;Math&quot;] = 123;
Console.WriteLine(score[&quot;Math&quot;]);
score[&quot;Math&quot;] = null;
Console.WriteLine(score[&quot;Math&quot;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、常量&lt;/h3&gt;
&lt;p&gt;​    常量（constant）：是表示常量值的类成员。（即可以在编译时计算的值）&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;常量隶属于类型而不是对象，即没有&quot;实例常量&quot;&lt;/strong&gt;。&quot;实例常量&quot;的角色由只读实例字段来担当。&lt;/p&gt;
&lt;p&gt;​    同时注意：当希望成为常量的值其类型不能被常量声明接受时（类/自定义结构体），应该使用静态只读字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Building {
    // MetaInfo 是自定义的类
    public static readonly MetaInfo Meta = new MetaInfo();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;八、泛型&lt;/h2&gt;
&lt;p&gt;​    泛型可以很好地解决类型膨胀和成员膨胀的问题。&lt;/p&gt;
&lt;h3&gt;1、泛型类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Apple {
    public string Color {get; set;}
}

class Book {
    public string Name {get; set;}
}

class Box&amp;lt;TCargo&amp;gt; {
    public TCargo Cargo {get; set;}
}

static void Main() {
    var apple = new Apple() {Color=&quot;Red&quot;};
    var book = new Book() {Name=&quot;New Book&quot;};
    // 特化
    Box&amp;lt;Apple&amp;gt; box = new Box&amp;lt;Apple&amp;gt;() {Cargo=apple};
    box&amp;lt;Book&amp;gt; box2 = new Box&amp;lt;Book&amp;gt;() {Cargo=book};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、泛型接口&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface IUnique&amp;lt;T&amp;gt; {
    T ID {get; set;}
}

class Student&amp;lt;T&amp;gt;: IUnique&amp;lt;T&amp;gt; {
    public T ID {get; set;}
    public string Name {get; set;}
}

// 也可以直接在实现时特化
class StudentSecond: IUnique&amp;lt;ulong&amp;gt; {
    public ulong ID {get; set;}
    public string Name {get; set;}
}

static void Main() {
    Student&amp;lt;int&amp;gt; stu = new Student&amp;lt;int&amp;gt;();
    stu.ID = 123;
    var stu2 = new Student&amp;lt;string&amp;gt;();
    stu2.ID = &quot;100&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、泛型方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static T[] Zip&amp;lt;T&amp;gt;(T[] a, T[] b) {
    T[] zippped = new T[a.Length + b.Length];
    int ai = 0, bi = 0, zi = 0;
    do {
        if (ai &amp;lt; a.Length) zipped[zi++] = a[ai++];
        if (bi &amp;lt; b.Length) zipped[zi++] = b[bi++];
    }
    while (ai &amp;lt; a.Length || bi &amp;lt; b.Length);
    return zipped;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;九、委托&lt;/h2&gt;
&lt;p&gt;​    委托（delegate）是函数指针的&quot;升级版&quot;。&lt;/p&gt;
&lt;h3&gt;1、使用内置委托类型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 无返回值、无参数委托
// 此处传递的方法为无返回值、无参数方法
Action action = new Action(Calculator.ReportMethodsNum);
// 触发，以下两种写法皆可
action.Invoke();
action();

// 无返回值、有参数委托
Action&amp;lt;string&amp;gt; action = new Action(StringManager.input);
action.Invoke();
action();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 有返回值委托
// 假设传递的方法参数类型为：int, int，返回值类型为：float
Func&amp;lt;int, int, float&amp;gt; func = new Func&amp;lt;int, int, float&amp;gt;(Calculator.Div);
// 触发，以下两种写法皆可
func.Invoke();
func();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、自定义委托&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;委托是一种类（class）。&lt;/li&gt;
&lt;li&gt;类是数据类型所以委托也是一种数据类型&lt;/li&gt;
&lt;li&gt;一般推荐声明在命令空间下，而不是嵌套在类中&lt;/li&gt;
&lt;li&gt;委托与所封装方法必须类型兼容
&lt;ul&gt;
&lt;li&gt;返回值数据类型一致&lt;/li&gt;
&lt;li&gt;参数列表在个数和数据类型上一致，参数名不需要一致&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public delegate double Calc(double x, double y);
class Program {
    class Calculator {
        public double Add(double x, double y) {
            return x + y;
        }
    }

    static void Main() {
        Calculator calculator = new Calculator();
        Calc calc1 = new Calc(calculator.Add);
        Console.WriteLine(calc1.Invoke(1, 2));
        Console.WriteLine(calc1(1, 2));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    主要作用：把方法当作参数传给另一个方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板方法，&quot;调用&quot;指定的外部方法来产生结果&lt;/li&gt;
&lt;li&gt;回调（callback）方法，调用指定的外部方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、自定义泛型委托&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public delegate T MyDele&amp;lt;T&amp;gt;(T a, T b);

static void Main() {
    MyDele&amp;lt;int&amp;gt; deleAdd = new MyDele&amp;lt;int&amp;gt;(Add);
    int res = deleAdd(100, 200);
    MyDele&amp;lt;double&amp;gt; delMul = new MyDele&amp;lt;double&amp;gt;(Mul);
    double res2 = delMul(100.2, 300.5);
}

static int Add(int a, int b) {
    return a + b;
}

static double Mul(double a, double b) {
    return a * b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、多播委托与委托隐式异步&lt;/h3&gt;
&lt;p&gt;​    多播委托（同步调用）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Action act1 = new Action(stu1.DoHomework);
Action act2 = new Action(stu2.DoHomework);
act1 += act2;
// 此后执行顺序按合并顺序
act1.Invoke();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    委托隐式异步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 每一个 act 都在一个子线程中执行，默认不加锁
act1.BeginInvoke(null, null);
act2.BeginInvoke(null, null);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、Lambda 表达式与匿名函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Func&amp;lt;int, int, int&amp;gt; func = (int a, int b) =&amp;gt; { return a + b; };
// 因为指定了委托的参数和返回值类型，所以可以不写类型
Func&amp;lt;int, int, int&amp;gt; func = (a, b) =&amp;gt; a+b;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    组合使用：（泛型方法、泛型委托参数）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void DoSomeCalc&amp;lt;T&amp;gt;(Func&amp;lt;T, T, T&amp;gt; func, T x, T y) {
    T res = func(x, y);
    Console.WriteLine(res);
}
// 使用时：
DoSomeCalc((a, b) =&amp;gt; a+b, 1, 2);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6、Linq&lt;/h3&gt;
&lt;p&gt;​    Linq：语言集成化查询（Language Integrated Query）&lt;/p&gt;
&lt;p&gt;​    参考：https://learn.microsoft.com/zh-cn/dotnet/csharp/linq/write-linq-queries&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;十、事件&lt;/h2&gt;
&lt;p&gt;​    角色：使对象或类具备通知能力的成员&lt;/p&gt;
&lt;p&gt;​    使用：用于对象或类间的动作协调与信息传递（消息推送）&lt;/p&gt;
&lt;p&gt;​    五个组成部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事件的拥有者（event source，对象）&lt;/li&gt;
&lt;li&gt;事件成员（event，成员）&lt;/li&gt;
&lt;li&gt;事件的响应者（event subscriber，对象）&lt;/li&gt;
&lt;li&gt;事件处理器（event handler，成员）—— 本质上是一个回调方法&lt;/li&gt;
&lt;li&gt;事件订阅——把事件处理器与事件关联在一起，本质上是一种以委托类型为基础的&quot;约定&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    特点：&lt;strong&gt;是委托的封装。保证了委托不在外部被随意的触发和调用，而只是暴露添加和移除事件处理器的功能&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1、事件常见类型&lt;/h3&gt;
&lt;p&gt;​    事件拥有者和事件响应者在不同的类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Program {
    static void Main() {
        Form form = new Form();
        Controller controller = new Controller(form);
        form.ShowDialog();
    }
}

class Controller {
    private Form form;
    public Controller(Form form) {
        if (form != null) {
            this.form = form;
            this.form.Click += this.FormClicked;
        }
    }
    private void FormClicked(object sender, EventArgs e) {
        this.form.Text = DateTime.Now.ToString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    事件拥有者和事件响应者是同一对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Program {
    static void Main() {
        MyForm form = new MyForm();
        form.Click += form.FormClicked;
        form.ShowDialog();
    }
}

class MyForm: Form {
    internal void FormClicked(object sender, EventArgs e) {
        this.Text = DateTime.Now.ToString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    事件拥有者是事件响应者的成员：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Program {
    static void Main() {
        MyForm form = new MyForm();
        form.ShowDialog();
    }
}

class MyForm: Form {
    private TextBox textbox;
    private Button button;
    
    public MyForm() {
        this.textBox = new TextBox();
        this.button = new Button();
        this.Controls.Add(this.textBox);
        this.Controls.Add(this.button);
        this.button.Click += this.ButtonClicked;
    }
    private void ButtonClicked(object sender, EventArgs e) {
        this.textBox.text = DateTime.Now.ToString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、事件完整声明&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Program {
    static void Main() {
        Customer customer = new Customer();
        Waiter waiter = new Waiter();
        customer.WhenOrder += waiter.AcceptOrder;
        customer.Order();
        customer.Pay();
    }
}

// 传递事件消息的类（定义事件发生时传递的参数）
public class OrderEventArgs: System.EventArgs {
    public string DishName {get; set;}
    public string Size {get; set;}
}

// 事件提供者
public class Customer {
    // 委托声明（定义事件处理器的规格）
    public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);
    // 委托字段（用于保存所有的委托，即事件处理器）
    private OrderEventHandler oeHandler;
    // 事件成员
    public event OrderEventHandler WhenOrder {
        add {
            this.oeHandler += value;
        }
        remove {
            this.oeHandler -= value;
        }
    }

    public double Bill {get; set;}
    public void Pay() {
        Console.WriteLine(&quot;The customer Pay ${0}.&quot;, this.Bill);
    }
    // 事件触发
    public void Order() {
        if (this.oeHandler != null) {
            OrderEventArgs e = new OrderEventArgs() {
                DishName = &quot;Kongpao Chicken&quot;,
                Size = &quot;large&quot;
            };
            this.oeHandler.Invoke(this, e);
        }
    }
}

// 事件响应者
public class Waiter {
    public void AcceptOrder(Customer customer, OrderEventArgs e) {
        Console.WriteLine(&quot;The waiter had served the dish:\n{0}, size: {1}&quot;, e.DishName, e.Size);
        double price = 10;
        switch (e.Size) {
            case &quot;small&quot;:
                price *= 0.5;
                break;
            case &quot;large&quot;:
                price *= 1.5;
                break;
            default:
                break;
        }
        customer.Bill += price;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、事件简略声明&lt;/h3&gt;
&lt;p&gt;​    简略声明类似于字段式声明（field-like）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * 其他部分相同，只需要更改以下部分
 */

// 只定义事件字段
// 此时不显式定义委托字段，因此委托字段使用同一名字访问
// 这种写法是一个语法糖，但实际在编译后，内部是有着委托字段的
public event OrderEventHandler WhenOrder;

// 事件触发
public void Order() {
    if (this.WhenOrder != null) {
        OrderEventArgs e = new OrderEventArgs() {
            DishName = &quot;Kongpao Chicken&quot;,
            Size = &quot;large&quot;
        };
        this.WhenOrder.Invoke(this, e);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十一、接口与抽象类&lt;/h2&gt;
&lt;h3&gt;1、简述&lt;/h3&gt;
&lt;p&gt;​    概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象类是未完全实现逻辑的类（可以有字段和非 public 成员，它们代表了&quot;具体逻辑&quot;）
&lt;ul&gt;
&lt;li&gt;为复用而生，也具有解耦功能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;接口是完全未实现逻辑的&quot;类&quot;（即纯虚类，只有函数成员，成员全部 public）
&lt;ul&gt;
&lt;li&gt;为解耦而生，方便单元测试&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    &lt;strong&gt;共同点&lt;/strong&gt;：都不能实例化，只能用来声明变量、引用具体类的实例&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;区别和联系&lt;/strong&gt;：https://www.infoworld.com/article/2928719/when-to-use-an-abstract-class-vs-interface-in-csharp.html&lt;/p&gt;
&lt;p&gt;​    &lt;strong&gt;使用范例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface IVehicle {
    void Stop();
    void Fill();
    void Run();
}

abstract class Vehicle: IVehicle {
    public void Stop() {
        Console.WriteLine(&quot;Stopped.&quot;);
    }
    public void Fill() {
        Console.WriteLine(&quot;Pay and fill.&quot;);
    }
    abstract public void Run();
}

class Car: Vehicle {
    public override void Run() {
        Console.WriteLine(&quot;Car is running.&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    &lt;strong&gt;覆写关系&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20230203202926838.DxTAliZa_UBtn5.webp&quot; alt=&quot;image-20230203202926838.webp w-80%&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、接口显式实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface ISecret {
    void DoSomethingSecret();
}

interface IPublic {
    void DoSomethingExplicit();
}

class Person: ISecret, IPublic {
    public void DoSomethingExplicit() {
        Console.WriteLine(&quot;The thing that Everyone knows.&quot;);
    }
    void ISecret.DoSomethingSecret() {
        Console.WriteLine(&quot;The thing that only I know. &quot;);
    }
}

static void Main() {
    var publicPerson = new Person();
    publicPerson.DoSomethingExplicit();
    // 也可以使用强制类型转换
    var privatePerson = publicPerson as ISecret;
    privatePerson.DoSomethingSecret();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    &lt;code&gt;DoSomethingSecret()&lt;/code&gt; 方法显式实现，只有在以 &lt;code&gt;ISecret&lt;/code&gt; 类角色实现时，才可以调用此方法。&lt;/p&gt;
&lt;h3&gt;3、依赖反转&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface IPhone {
    void Dial();
    void PickUp();
    void Send();
    void Receive();
}

class PhoneUser {
    private IPhone _phone;
    public PhoneUser(IPhone phone) {
        _phone = phone;
    }
    public void UsePhone() {
        _phone.Dial();
        _phone.PickUp();
        _phone.Send();
        _phone.Receive();
    }
}

class XiaoMiPhone: IPhone {
    public void Dial() { Console.WriteLine(&quot;XiaoMi Dial working.&quot;); }
    public void PickUp() { Console.WriteLine(&quot;XiaoMi PickUp working.&quot;); }
    public void Send() { Console.WriteLine(&quot;XiaoMi Send working.&quot;); }
    public void Receive() { Console.WriteLine(&quot;XiaoMi Receive working.&quot;); }
}

static void Main() {
    // 用户与手机就实现了解耦。接口是用户和手机的&quot;契约&quot;
    // 如果有其他手机类，只需要实例化不同的手机即可，phone 并不需要更改代码
    var user = new PhoneUser(new XiaoMiPhone());
    user.UsePhone();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、接口隔离&lt;/h3&gt;
&lt;p&gt;​    尽量避免&quot;胖接口&quot;带来的冗余与不兼容性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Driver {
    // driver 只需要实现驾驶的功能，因此只需要实现 IVehicle 内的功能
    private IVehicle _vehicle;
    public Driver(IVehicle vehicle) {
        _vehicle = vehicle;
    }
    public void Drive() {
        _vehicle.Run();
    }
}

interface IVehicle {
    void Run();
}

interface IWeapon {
    void Fire();
}

class Car: IVehicle {
    public void Run() {
        Console.WriteLine(&quot;Car running.&quot;);
    }
}

class Tank: IVehicle, IWeapon {
    public void Run() {
        Console.WriteLine(&quot;Tank running.&quot;);
    }
    public void Fire() {
        Console.WriteLine(&quot;Tank fired!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    如果 &lt;code&gt;Fire()&lt;/code&gt;、&lt;code&gt;Run()&lt;/code&gt; 整合到一个接口中，会导致接口粒度过大，无法对使用方提供很好的兼容效果。&lt;/p&gt;
&lt;h3&gt;5、反射与依赖注入&lt;/h3&gt;
&lt;p&gt;​    反射：以不变应万变。&lt;/p&gt;
&lt;p&gt;​    反射的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Reflection;

class Program {
    static void Main() {
        ITank tank = new Tank();
        
        var t = tank.GetType();
        object o = Activator.CreateInstance(t);
        MethodInfo fireMi = t.GetMethod(&quot;Fire&quot;);
        MethodInfo runMi = t.GetMethod(&quot;Run&quot;);
        fireMi.Invoke(o, null);
        fireMi.Invoke(o, null);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    依赖反转：如果在类 A 的内部去实例化类 B，那么两者之间会出现较高的耦合。要解决这个问题，就要把 A类对 B 类的控制权抽离出来，交给一个第三方去做，把&lt;strong&gt;控制权反转给第三方（一般称 IOC 容器，即实现 IOC 的组件或框架），就称作控制反转（IOC Inversion Of Control）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​    依赖注入：是实现依赖反转的一种方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.Extensions.DependencyInjection;

class Program {
    static void Main() {
        var sc = new ServiceCollection();
        sc.AddScoped(typeof(ITank), typeof(MediumTank));
        sc.AddScoped(typeof(IVehicle), typeof(Car));
        sc.AddScoped&amp;lt;Driver&amp;gt;();
        var sp = sc.BuildServiceProvider();
        
        // 实例化 tank 时，借助 IOC 容器
        var tank = sp.GetService&amp;lt;Itank&amp;gt;();
        tank.Fire();
        tank.Run();
        // 实例化 driver 时，由于 Driver 内有实现 IVehicle 的成员，
        // 而 IVehicle 已经注册实例化 Car，因此 driver 内将组合 Car 类对象
        var driver = sp.GetService&amp;lt;Driver&amp;gt;();
        driver.Drive();
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>个人项目 melobot 发布~</title><link>https://miku.top/posts/melobot-release/</link><guid isPermaLink="true">https://miku.top/posts/melobot-release/</guid><description>melobot 是支持多协议、多路 IO 和丰富功能的机器人开发框架</description><pubDate>Thu, 15 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;个人项目 melobot 发布&lt;/h1&gt;
&lt;p&gt;具体参见项目和对应的文档吧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目地址：https://github.com/meloland/melobot&lt;/li&gt;
&lt;li&gt;项目文档：https://docs.melobot.org&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Python 并发编程</title><link>https://miku.top/posts/python-concurrency/</link><guid isPermaLink="true">https://miku.top/posts/python-concurrency/</guid><description>Python 基于多进程、多线程和多协程的并发编程</description><pubDate>Sun, 27 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Python 并发编程&lt;/h1&gt;
&lt;h2&gt;一、简述&lt;/h2&gt;
&lt;h3&gt;1、多线程、多进程、多协程&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;CPU 密集计算&lt;/strong&gt;：也叫计算密集型，是指 I/O 在很短的时间就可以完成，CPU 需要大量的计算和处理，特点是 CPU 占用率相当高。例如：压缩解压缩、加密解密、正则表达式搜索。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;IO 密集型计算&lt;/strong&gt;：IO 密集型指的是系统运作大部分的状况是 CPU 在等 I/O（硬盘/内存）的读/写操作，CPU 占用率仍然较低。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;多进程（multiprocessing）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：可以利用多核 CPU 进行计算&lt;/li&gt;
&lt;li&gt;缺点：占用资源最多、可启动数目比线程少&lt;/li&gt;
&lt;li&gt;适用于：CPU 密集型计算&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;多线程（threading）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：相比进程，更轻量级、占用资源少&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;相比进程：多线程只能并发执行，不能利用多个 CPU（由于 GIL 的存在）&lt;/li&gt;
&lt;li&gt;相比协程：启动数目有限制，占用内存资源，有线程切换开销&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;适用于：IO 密集型计算、同时运行的任务数目并不多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;多协程（asyncio）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：内存开销最少、启动协程数量最多&lt;/li&gt;
&lt;li&gt;缺点：支持的库有限制、代码实现复杂&lt;/li&gt;
&lt;li&gt;适用于：IO 密集型计算、需要超多任务运行，但有现成库支持的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	三者的选择：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20221126232845638.BD9vidEp_Z1YDEVa.webp&quot; alt=&quot;image-20221126232845638&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、GIL 全局解释锁&lt;/h3&gt;
&lt;p&gt;​	全局解释器锁（Global Interpreter Lock）是计算机程序设计语言解释器用于同步线程的一种机制，&lt;strong&gt;它使得任何时刻仅有一个线程在执行&lt;/strong&gt;。即便在多核心处理器上，使用 GIL 的解释器&lt;strong&gt;也只允许同一时间执行一个线程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20221126233243737.CUou-V2f_26W1rq.webp&quot; alt=&quot;image-20221126233243737 w-50%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	GIL 是 Python 解释器 Cpython 为了规避多线程并发问题，而遗留下来的历史性问题。但是正由于 Python 在 IO 期间会释放 GIL，因此多线程用于 IO 密集计算依然可以大幅提高速度，但用于 CPU 密集计算，只会拖慢速度。&lt;/p&gt;
&lt;p&gt;​	而对于 GIL 的劣势，Python 提供了 multiprocessing 来支持多进程机制。&lt;/p&gt;
&lt;h2&gt;二、多线程&lt;/h2&gt;
&lt;h3&gt;1、线程生命周期与基本方法&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20221127011547401.CQ3DqP1E_Z16EQ9H.webp&quot; alt=&quot;image-20221127011547401 w-50%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	基本方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import threading

# 一个需要放入其他线程执行的函数
def func(a, b):
    pass
t = threading.Thread(target=func, args=(100, 200))
# 设置 t 为守护线程
t.daemon = True
# 线程活动方法（不会直接使用，可在子类重载）
t.run()
# 启动线程
t.start()
# 阻塞，等待 t 线程完成
t.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;code&gt;join()&lt;/code&gt; 方法：主线程 A 中，创建了子线程 B，并且在主线程 A 中调用了 B.join()，那么，主线程 A 会在调用的地方等待，直到子线程 B 完成操作后，才可以接着往下执行，那么在调用这个线程时可以使用被调用线程的 join 方法。&lt;/p&gt;
&lt;p&gt;​	&lt;code&gt;setDaemon()&lt;/code&gt; 方法或设置 &lt;code&gt;daemon = True&lt;/code&gt;：主线程 A 中，创建了子线程 B，并且在主线程 A 中调用了 B.setDaemon() 或设置 daemon 属性为 True，那么就是把主线程 B 设置为守护线程（后台线程）。此时，如果主线程 A 执行结束，无论子线程 B 是否结束，一并和主线程 A 退出。&lt;strong&gt;注意该操作要先于 start 方法调用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	&lt;code&gt;run()&lt;/code&gt; 方法：&lt;strong&gt;代表线程活动的方法，而创建线程的 target 是用于该方法的可调用对象&lt;/strong&gt;。我们也称 run 方法体为线程体。如果直接去调用 run 方法，&lt;strong&gt;是一次普通函数调用&lt;/strong&gt;。程序还是顺序执行，要等待 run 方法体执行完毕后，才可继续执行之后的代码。这样做程序中只有主线程这一个线程， 其程序执行路径还是只有一条， 就没有达成写线程加速的目的。因此一般只会在子类重载，实现某些特殊需求。&lt;/p&gt;
&lt;p&gt;​	&lt;code&gt;start() &lt;/code&gt; 方法： &lt;strong&gt;它在一个线程里最多只能被调用一次&lt;/strong&gt;。一般通过调用 start 方法来启动一个线程， &lt;strong&gt;这时此线程是处于就绪状态， 并没有运行&lt;/strong&gt;，调度后会通过 run 方法来完成其运行操作，之后 CPU 再调度其它线程（但注意期间可能发生线程切换）。&lt;strong&gt;start 方法执行时无需等待 run 方法体代码执行完毕，可以直接继续执行下面的代码，但 join 方法设置的阻塞除外&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	&lt;code&gt;start()&lt;/code&gt; 和 &lt;code&gt;run()&lt;/code&gt; 方法总结：若调用 start，则先执行主线程，后执行子线程（join 引起的阻塞除外）。若调用 run，相当于函数调用，按照程序的顺序执行。&lt;/p&gt;
&lt;p&gt;​	更多方法和详细说明：&lt;a href=&quot;https://docs.python.org/zh-cn/3/library/threading.html?highlight=#threading.Thread&quot;&gt;threading.Thread&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;2、线程安全&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;线程安全&lt;/strong&gt;：指某个函数、函数库在多线程环境中被调用时，能够正确地处理&lt;strong&gt;多个线程之间的共享变量&lt;/strong&gt;，使程序功能正确完成。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;线程不安全&lt;/strong&gt;：由于线程的执行随时会发生切换，就造成了不可预料的结果，出现线程不安全。&lt;/p&gt;
&lt;p&gt;​	线程不安全的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import threading
import time

class Account:
    def __init__(self, balance):
        self.balance = balance

def withdraw(account, amount):
    if account.balance &amp;gt;= amount:
        # 强制线程阻塞，导致线程切换
        time.sleep(0.1)
        print(threading.current_thread().name, &quot;取钱成功&quot;)
        account.balance -= amount
        print(threading.current_thread().name, &quot;余额&quot;, account.balance)
    else:
        print(threading.current_thread().anme, &quot;余额不足，取钱失败&quot;)

account = Account(1000)
ta = threading.Thread(name=&quot;ta&quot;, target=withdraw, args=(account, 800))
tb = threading.Thread(name=&quot;tb&quot;, target=withdraw, args=(account, 800))
ta.start()
tb.start()
print(&apos;主线程结束~（会优先执行）&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	可以使用 Lock 解决线程安全的问题，也就是将原始的非原子操作块，定义为原子操作，避免了对数据操作过程中的不一致性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import threading

# try-finally 模式（不推荐）
lock = threading.Lock()
lock.acquire()
try:
    # do something
finally:
    lock.release()
    
# with 上下文管理（推荐）
lock = threading.Lock()
with lock:
    # do something
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、线程间数据通信&lt;/h3&gt;
&lt;p&gt;​	多线程数据通信可以使用 queue.Queue 类实现。&lt;strong&gt;Queue 的相关操作是线程安全的&lt;/strong&gt;，因此使用时不需要做额外加锁处理。&lt;strong&gt;同时 Queue 默认是阻塞的&lt;/strong&gt;，可以保证在队列取元素时，如果没有元素就可以进入等待状态；在队列放元素时，元素不满也可以进入等待状态，而不是直接报异常。&lt;/p&gt;
&lt;p&gt;​	基本方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import queue

# 实例化
q = queue.Queue()
# 添加和获取（这里的获取是 pop 的意思）
q.put(item)
q.get()
# 判长、空和满
q.qsize()
q.empty()
q.full()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	但是：在实现类似&quot;生产者-消费者&quot;模型时，&lt;strong&gt;必须使用 task_done 和 join 方法以保证生产者线程在生产完所有产品后，阻塞以等待消费者线程消费完所有产品&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	以下是规范的生产者-消费者模型的多线程实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from threading import Thread, Lock
from queue import Queue
import random, time

def producer(q, name, lock):
    for i in range(2):
        # 模拟 IO 操作
        time.sleep(random.random())
        fd = &apos;%s-%s&apos; % (name, i+1)
        q.put(fd)
        with lock:
            print(&apos;%s生产了 %s&apos; % (name, fd))
    # 该线程阻塞，直至等到队列计数为 0（本质作用是等待队列消费完毕）
    q.join()

def consumer(q, name, lock):
    while True:
        food = q.get()
        # 模拟 IO 操作
        time.sleep(random.random())
        with lock:
            print(&apos;%s吃了%s&apos; % (name, food))
        # 是一个已经消费的信号，让队列计数 -1
        q.task_done()

if __name__ == &apos;__main__&apos;:
    q = Queue()
    # 加锁以保证 print 输出不乱序
    lock = Lock()
    p1 = Thread(target=producer, args=(q, &apos;p1&apos;, lock))
    p2 = Thread(target=producer, args=(q, &apos;p2&apos;, lock))
    p3 = Thread(target=producer, args=(q, &apos;p3&apos;, lock))
    p1.start()
    p2.start()
    p3.start()
    c1 = Thread(target=consumer, args=(q, &apos;c1&apos;, lock))
    c2 = Thread(target=consumer, args=(q, &apos;c2&apos;, lock))
    # 设置为守护线程，因为 consumer 内部是死循环
    c1.daemon = True
    c2.daemon = True
    c1.start()
    c2.start()
    # 主线程阻塞等待生产者线程执行完毕
    p1.join()
    p2.join()
    p3.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	执行过程为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主线程执行，依次让消费者、生产者线程就绪，并设置消费者线程为守护线程&lt;/li&gt;
&lt;li&gt;主线程阻塞等待所有生产者线程&lt;/li&gt;
&lt;li&gt;生产者线程执行并不断生产产品。期间会切换至消费者线程消费产品&lt;/li&gt;
&lt;li&gt;生产者线程结束，但进入等待队列为空的阻塞状态&lt;/li&gt;
&lt;li&gt;切换至消费者线程，消费至队列为空&lt;/li&gt;
&lt;li&gt;生产者线程不再阻塞，生产者线程结束&lt;/li&gt;
&lt;li&gt;主线程不再阻塞，主线程结束&lt;/li&gt;
&lt;li&gt;所有非守护线程结束，守护线程（消费者线程）结束&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、线程池&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;线程池基本原理&lt;/strong&gt;：新建线程时系统需要分配资源，终止线程时系统需要回收咨源。如果可以重用线程，则可以减去新建/终止的开销。创建线程池，就可以充分地重用线程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20221127011910698.DMq3cpIM_Mgi8x.webp&quot; alt=&quot;image-20221127011910698 w-50%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提升性能：减去了大量新建、终止线程的开销，重用了线程资源&lt;/li&gt;
&lt;li&gt;适用场景：适合处理突发性大量请求或需要大量线程完成任务、但实际任务处理时间较短&lt;/li&gt;
&lt;li&gt;防御功能：能有效避免系统因为创建线程过多，而导致系统负荷过大相应变慢等问题&lt;/li&gt;
&lt;li&gt;代码优势：使用线程池的语法比自己新建线程、执行线程更加简洁&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;from concurrent.futures import ThreadPoolExecutor, as_completed

# 第一种写法：结果与参数列表顺序对应
with ThreadPoolExecutor() as pool:
    # 注意这里的 args 是参数列表
    results = pool.map(func, args)
    
    for result in results:
        print(result)

# 第二种写法：可以实现更精细的控制，对每个函数的参数进行指定
with ThreadPoolExecutor() as pool:
    futures = [ pool.submit(func, arg) for arg in args]
    # 结果与提交顺序对应
    for future in futures:
        print(future.result())
    # 使用 as_completed 后，先结束的，先返回结果
    for future in as_completed(futures):
        print(future.result())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、多进程&lt;/h2&gt;
&lt;h3&gt;1、进程基本方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing import Process

p = Process(target=f, args=(&apos;some arg&apos;, ...))
p.daemon = True
p.run()
p.start()
p.join()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、进程加锁&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing import Lock

lock = Lock()
with lock:
    # do something
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、进程间数据通信&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing import Queue
q = Queue()
q.put()
item = q.get()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;注意此时不能使用 &lt;code&gt;queue.Queue&lt;/code&gt;，要使用适用于多进程通信的 &lt;code&gt;multiprocessing.Queue&lt;/code&gt;&lt;/strong&gt;。二者区别见于：https://www.jianshu.com/p/0c07967aa0f7&lt;/p&gt;
&lt;p&gt;​	同时，如果需要实现类似&quot;生产者-消费者&quot;模型，不能直接使用 &lt;code&gt;multiprocessing.Queue&lt;/code&gt;，而应该使用 &lt;code&gt;multiprocessing.JoinableQueue&lt;/code&gt; ，因为 &lt;code&gt;multiprocessing.Queue&lt;/code&gt; 不提供 &lt;code&gt;task_done&lt;/code&gt; 和 &lt;code&gt;join&lt;/code&gt; 方法。其他细节基本一致，不再赘述。&lt;/p&gt;
&lt;h3&gt;4、进程池&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor() as executor:
    # 方法一
    results = executor.map(func, args)
    # 方法二
    futures = executor.submit(func, arg)
    result = future.result()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、多协程&lt;/h2&gt;
&lt;p&gt;​	多协程相关内容参见：&lt;a href=&quot;/posts/python-coroutine/&quot;&gt;Python 并发编程之多协程&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Python 并发编程之多协程</title><link>https://miku.top/posts/python-coroutine/</link><guid isPermaLink="true">https://miku.top/posts/python-coroutine/</guid><description>通过 asyncio 在 Python 实现基于协程的并发编程</description><pubDate>Fri, 25 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Python 并发编程之多协程&lt;/h1&gt;
&lt;h2&gt;一、协程简述&lt;/h2&gt;
&lt;p&gt;协程不是计算机提供，程序员人为创造。&lt;/p&gt;
&lt;p&gt;协程（Coroutine），也可以被称为微线程，是一种用户态内的上下文切换技术。简而言之，其实就是通过一个线程实现代码块相互切换执行。实现协程的方法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;greenlet、早期模块&lt;/li&gt;
&lt;li&gt;yield 关键字&lt;/li&gt;
&lt;li&gt;asyncio 装饰器（py3.4）&lt;/li&gt;
&lt;li&gt;async、await 关键字（py3.5）【推荐】&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1、greenlet 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install greenlet
from greenlet import greenlet
 
def func1():
    print(1)
    gr2.switch()
    print(2)
    gr2.switch()
    
def func2():
    print(3)
    gr1.switch()
    print(4)
 
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、yield 关键字实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def func1():
    yield 1
    yield from func2()
    yield 2
 
def func2():
    yield 3
    yield 4
 
f1 = func1()
for item in f1:
    print(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、asyncio 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
 
@asyncio.coroutine
def func1():
    print(1)
    # 遇到 IO 耗时操作，自动切换到 tasks 中的其他任务
    yield from asyncio.sleep(2)
    print(2)
 
@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2)
    print(4)
 
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2()),
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特别之处：遇到 IO 阻塞就自动切换&lt;/p&gt;
&lt;h3&gt;4、async&amp;amp;await 关键字实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
 
async def func1():
    print(1)
    # 遇到 IO 耗时操作，自动切换到 tasks 中的其他任务
    # 这里是设置该协程的休眠
    await asyncio.sleep(2)
    print(2)
 
async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)
 
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2()),
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、基于协程的异步&lt;/h2&gt;
&lt;h3&gt;1、事件循环&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 伪代码
任务列表 = [任务1, 任务2, 任务3]
while True：
    可执行任务列表，已完成任务列表 = 检查列表中所有任务，将可执行和已完成的任务返回
    for 就绪任务 in 可执行的任务列表：
    	执行已就绪的任务
    for 已完成的任务 in 已完成的任务列表：
    	在任务列表中移除已完成的任务
    如果任务列表中的任务都已完成，则终止循环
import asyncio
# 生成事件循环
loop = asyncio.get_event_loop()
# 将任务放到任务列表
loop.run_until_complete(任务)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、async 关键字&lt;/h3&gt;
&lt;p&gt;​
协程函数：使用 async 关键字定义的函数。&lt;/p&gt;
&lt;p&gt;​协程对象：执行协程函数得到的对象。&lt;/p&gt;
&lt;p&gt;​注：执行协程函数会创建协程对象，函数内部代码不会执行。如果想要运行协程函数内部代码，需要将协程对象交给事件循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
 
async def func():
    print(&apos;执行 async 函数内部代码~&apos;)
asyncio.run(func())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、await 关键字&lt;/h3&gt;
&lt;p&gt;​
await 后要跟可等待对象。（包括协程对象、Future对象、Task对象）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
 
async def other():
    print(&quot;start&quot;)
    await asyncio.sleep(2)
    print(&quot;end&quot;)
    return &apos;value&apos;
 
async def func():
    print(&quot;执行协程函数内部代码&quot;)
    resp = await other()
    print(&quot;阻塞结束，结果为：&quot;, resp)
 
asyncio.run(func())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​
await 就是等待值获取后再往下执行，等待时事件循环切换到其他任务。&lt;/p&gt;
&lt;h3&gt;4、Task 对象&lt;/h3&gt;
&lt;p&gt;​Task 是对协程的包装。在事件循环中添加多个任务，即可并发调度协程。而且任务是可以包含各种状态的，便于对异步操作状态的控制。&lt;/p&gt;
&lt;p&gt;​Task 用于并发调度协程，通过 asyncio.create_task(协程对象) 的方式创建 Task 对象，可以让协程加入事件循环中等待被调度执行。除了使用 asyncio.create_task() 函数以外，还可以用低层级的 loop.create_task() 或 asyncio.ensure_future() 函数。不建议手动实例化 Task 对象。&lt;/p&gt;
&lt;p&gt;​注意：asyncio.create_task() 函数在 Python3.7 中被加入。在 Python3.7 之前，可以改用低层级的 asyncio.ensure_future() 函数。&lt;/p&gt;
&lt;p&gt;​关于 Task 对象，更多详见：&lt;a href=&quot;https://docs.python.org/3/library/asyncio-task.html?highlight=#asyncio.Task&quot;&gt;asyncio.Task&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
 
async def func():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return &quot;value&quot;
 
async def main():
    print(&quot;main 开始&quot;)
    # 可以分别 await 多个任务，但添加多任务时，一般像下面这样写：
    # 同时可以取名方便最后区分
    task_list = [
        asyncio.create_task(func(), name=&apos;f1&apos;),
        asyncio.create_task(func(), name=&apos;f2&apos;)
    ]
    print(&quot;main 添加任务结束&quot;)
    # 可以设置超时，超时未完成即在 pending 中
    # 从 &amp;gt;py3.8 开始，向该方法传递协程的可迭代对象将导致混淆行为，所以最好像这里一样传递 Task 可迭代对象
    done, pending = await asyncio.wait(task_list, timeout=None)
    for t in done:
        print(t.get_name(), t.result())
 
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​
也可以对 Task 对象使用 add_done_callback()添加回调。​对于某些情况，可能会先定义 task_list，此时的写法将会有所不同：（由于上面所述原因，py 3.8 后开始已经不支持这么做，因此下面的代码仅限于 &amp;lt;=py 3.8 使用）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
 
async def func():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return &quot;value&quot;
 
task_list = [
    func(), func()
]
# 会先启动 event_loop，再添加任务
done, pending = asyncio.run(asyncio.wait(task_list))
print(done)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​
所以最好创建一个 main() 协程函数，在其内添加 Task，最后让 event_loop 接管 main 协程。&lt;/p&gt;
&lt;h3&gt;5、Future 对象&lt;/h3&gt;
&lt;p&gt;Future 是一个特殊的低级可等待对象，表示异步操作的最终结果。&lt;/p&gt;
&lt;p&gt;​Task 类继承于 Future 类，同时 Task 对象内部 await 结果的处理基于 Future 对象。&lt;/p&gt;
&lt;h3&gt;6、asyncio + 同步模块的异步实现&lt;/h3&gt;
&lt;p&gt;​
以一个爬虫为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import requests
 
async def download_image(url):
    print(&apos;开始下载&apos;, url)
    loop = asyncio.get_event_loop()
    # 同步模块不支持异步协程，因此使用进程池
    future = loop.run_in_executor(None, requests.get, url)
    resp = await future
    print(&apos;下载完成&apos;)
    # 之后进行一些处理/保存工作
    
async def main():
    url_list = [
        &apos;https://www.xxx.com&apos;,
        &apos;https://www.yyy.com&apos;,
        &apos;https://www.zzz.com&apos;
    ]
    tasks = [asyncio.create_task(download_image(url)) for url in url_list]
   	done, pending = await asyncio.wait(tasks)
    
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7、异步迭代器&lt;/h3&gt;
&lt;p&gt;​
异步迭代器：实现了 &lt;code&gt;__aiter__()&lt;/code&gt; 和 &lt;code&gt;__anext__()&lt;/code&gt; 方法的对象。 &lt;code&gt;__anext__()&lt;/code&gt; 必须返回一个 awaitable 对象。 async for 会处理异步迭代器的 &lt;code&gt;__anext__()&lt;/code&gt; 方法所返回的可等待对象，直到其引发一个 &lt;code&gt;StopAsyncrIteration&lt;/code&gt; 异常。&lt;/p&gt;
&lt;p&gt;​ 异步可迭代对象：可在 async for 语句中被使用的对象。必须通过它的 &lt;code&gt;__aiter__()&lt;/code&gt; 方法返回一个异步迭代器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
 
class Reader:
    &quot;&quot;&quot;自定义异步迭代器&quot;&quot;&quot;
    def __init__(self):
        self.count = 0
    async def readline(self):
        self.count += 1
        if self.count == 100:
            return None
        return self.count
    def __aiter__(self):
        return self
    async def __anext__(self):
        val = await self.readline()
        if val == None:
            raise StopAsyncIteration
        return val
 
async def func():
	obj = Reader()
	# async 关键字只能在协程函数中使用
	async for item in obj:
		print(item)
asyncio.run(func())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8、异步上下文管理器&lt;/h3&gt;
&lt;p&gt;​
此种对象通过 &lt;code&gt;__aenter__()&lt;/code&gt; 和 &lt;code&gt;__aexit__()&lt;/code&gt; 方法来对 async with 语句中的环境进行控制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
 
class AsyncDbManager:
    def __init__(self):
        self.conn = conn
    async def do_something(self):
        # 异步操作数据库 
        return 666
    async def __aenter__(self):
        # 异步链接数据库
        self.conn = await asyncio.sleep(1)
        return self
    async def __aexit__(self, exc_type, exc, tb):
        # 异步关闭数据库
        await asyncio.sleep(1)
 
async def main():
    async with AsyncDbManager() as f:
        result = await f.do_something()
        print(result)
        
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、uvloop&lt;/h2&gt;
&lt;p&gt;​uvloop 可以使 asyncio 更快。事实上，它至少比 nodejs、gevent 和其他 Python 异步框架要快两倍 。基于 uvloop 的 asyncio 的速度几乎接近了 Go 程序的速度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install uvloop
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# 其他异步 asyncio 的代码，与之前一致
...
# 内部的事件循环会自动变为 uvloop
asyncio.run(...)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Python 设计模式</title><link>https://miku.top/posts/python-patterns/</link><guid isPermaLink="true">https://miku.top/posts/python-patterns/</guid><description>基于 Python 的常见设计模式实现</description><pubDate>Fri, 25 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Python 设计模式&lt;/h1&gt;
&lt;h2&gt;一、简述&lt;/h2&gt;
&lt;p&gt;​	&lt;strong&gt;设计模式&lt;/strong&gt;：对软件设计中普遍存在（反复出现）的各种问题，所提出的解决方案。每一个设计模式系统地命名、解释和评价了面向对象系统中一个重要的和重复出现的设计。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;接口&lt;/strong&gt;：若干抽象方法的集合。作用是限制实现接口的类必须按照接口给定的调用方式实现这些方法；对高层模块隐藏了类的内部实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod

# 这里的 Payment 方法就是接口类
class Payment(ABC):
    @abstractmethod
    def pay(self, money):
        pass

class Alipay(Payment):
    def pay(self, money):
        def pay(self, moeny):
            print(&apos;支付宝支付 %d 元&apos; %money)

p = Alipay()
p.pay(100)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;面向对象设计的 SOLID 原则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开放封闭原则：一个软件实体如类、模块和函数应该对扩展开放，对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。&lt;/li&gt;
&lt;li&gt;里氏替换原则：所有引用父类的地方必须能透明地使用其子类的对象。(引用了用户类的地方一定要兼容 VIP 用户类)&lt;/li&gt;
&lt;li&gt;依赖倒置原则：高层模块不应该依赖低层模块，二者都应该依赖其抽象（依赖于接口）；抽象不应该依赖细节；细节应该依赖抽象。换言之，要针对接口编程，而不是针对实现编程。&lt;/li&gt;
&lt;li&gt;接口隔离原则：使用多个专门的接口，而不使用单一的总接口，即客户端不应该依赖那些它不需要的接口。（接口的粒度应该足够小）&lt;/li&gt;
&lt;li&gt;单一职责原则：不要存在多于一个导致类变更的原因。通俗的说，即一个类只负责一项职责。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;设计模式分类&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建型模式（关注对象的创建）（5种）：工厂方法模式、抽象工厂模式、创建者模式、原型模式、单例模式&lt;/li&gt;
&lt;li&gt;结构型模式（关注类与类之间的结构）（7种）：适配器模式、桥模式、组合模式、装饰模式、外观模式、享元模式、代理模式&lt;/li&gt;
&lt;li&gt;行为型模式（关注实现具体行为）（11种）：解释器模式、责任链模式、命令模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、访问者模式、模板方法模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;名词解释&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端：指来自更高层的调用方&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、创建型模式&lt;/h2&gt;
&lt;h3&gt;1、简单工厂模式&lt;/h3&gt;
&lt;p&gt;​	内容：不直接向客户端暴露对象创建的实现细节，而是&lt;strong&gt;通过一个工厂类来负责创建产品类的实例&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工厂角色（Creator）&lt;/li&gt;
&lt;li&gt;抽象产品角色（Product）&lt;/li&gt;
&lt;li&gt;具体产品角色（Concrete Product）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 抽象产品
class Payment(ABC):
    @abstractmethod
    def pay(self, money): pass
# 具体产品
class Alipay(Payment):
    def pay(self, money): pass
class WechatPay(Payment):
    def pay(self, money): pass
# 工厂
class PaymentFactory:
    def create_payment(self, method):
        if method == &apos;alipay&apos;:
            return Alipay()
        elif method == &apos;wechatpay&apos;:
            return WechatPay()
        else:
            raise TypeError

pf = PaymentFactory()
p = pf.create_payment(&apos;alipay&apos;).pay(100)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;隐藏了对象创建的实现细节&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;客户端不需要修改代码&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;违反了单一职责原则&lt;/li&gt;
&lt;li&gt;将创建逻辑几种到一个工厂类里当添加新产品时，需要修改工厂类代码，违反了开闭原则&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2、工厂方法模式&lt;/h3&gt;
&lt;p&gt;​	内容：定义一个用于创建对象的接口（工厂接口），让子类决定实例化哪一个产品类。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象工厂角色（Creator）&lt;/li&gt;
&lt;li&gt;具体工厂角色（Concrete Creator）&lt;/li&gt;
&lt;li&gt;抽象产品角色（Product）&lt;/li&gt;
&lt;li&gt;具体产品角色（Concrete Product）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 抽象产品
class Payment(ABC):
    @abstractmethod
    def pay(self, money): pass
# 具体产品
class Alipay(Payment):
    def pay(self, money): pass
class WechatPay(Payment):
    def pay(self, money): pass
# 抽象工厂
class PaymentFactory:
    @abstractmethod
    def create_payment(self):
        pass
# 具体工厂
class AlipayFactory(PaymentFactory):
    def create_payment(self):
    	return Alipay()
class WechatPayFactory(PaymentFactory):
    def create_payment(self):
    	return WechatPay()

pf = WechatPayFactory()
p = pf.create_payment().pay(100)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个具体产品都对应一个具体工厂类，不需要修改工厂类代码&lt;/li&gt;
&lt;li&gt;隐藏了对象创建的实现细节&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每增加一个具体产品类，就必须增加一个相应的具体工厂类&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、抽象工厂模式&lt;/h3&gt;
&lt;p&gt;​	内容：定义一个工厂类接口，让工厂子类来创建一系列相关或相互依赖的对象。&lt;/p&gt;
&lt;p&gt;​	重点：&lt;strong&gt;让一个工厂生产几类对象，同时加以约束，使其形成一套产品&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 抽象产品
class PhoneShell(ABC):
    @abstractmethod
    def show_shell(self): pass
class CPU(ABC):
    @abstractmethod
    def show_CPU(self): pass
class OS(ABC):
    @abstractmethod
    def show_OS(self): pass
# 具体产品
class SmallShell(PhoneShell): def show_shell(self): pass
class SnapDragonCPU(CPU): def show_CPU(self): pass
class Android(OS): def show_os(self): pass
# 抽象工厂
class PhoneFactory(ABC):
    def make_shell(self): pass
    def make_CPU(self): pass
    def make_OS(self): pass
# 具体工厂
class MiFactory(PhoneFactory):
    def make_shell(self): return SmallShell()
    def make_CPU(self): return SnapDragonCPU()
    def make_OS(self): return Android()

# 客户端
class Phone:
    def __init__(self, cpu, shell, os):
        self.cpu, self.shell, self.os = cpu, shell, os
    def show_info(self):
        self.cpu.show_CPU()
        self.shell.show_shell()
        self.os.show_os()
def make_phone(factory):
    cpu = factory.make_cpu()
    shell = factory.make_shell()
    os = factory.make_os()
    return Phone(cpu, shell, os)

p1 = make_phone(MiFactory())
p1.show_info()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将客户端与类的具体实现相分离&lt;/li&gt;
&lt;li&gt;每个工厂创建了一个完整的产品系列，使得易于交换产品系列&lt;/li&gt;
&lt;li&gt;有利于产品的一致性（即产品之间的约束关系）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;难以支持新种类的（抽象）产品（比如本例中，要加入新种类的产品&quot;屏幕&quot;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、建造者模式&lt;/h3&gt;
&lt;p&gt;​	内容：将一个复杂对象的构建与它的表示分离，使得同样的构建过程可以创建不同的表示。&lt;/p&gt;
&lt;p&gt;​	重点：建造者模式与抽象工厂模式相似，也用来创建复杂对象。主要区别是建造者模式着重&lt;strong&gt;一步步构造一个复杂对象&lt;/strong&gt;，而抽象工厂模式着重于多个系列的产品对象。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象建造者（Builder）&lt;/li&gt;
&lt;li&gt;具体建造者（Concrete Builder）&lt;/li&gt;
&lt;li&gt;指挥者（Director）&lt;/li&gt;
&lt;li&gt;产品（Product）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 产品
class Player:
    def __init__(self, face=None, body=None):
        self.face, self.body = face, body
    def __str__(self): return &apos;%s %s&apos; %(self.face, self.body)
# 抽象建造者
class PlayerBuilder(ABC):
    @abstractmethod
    def build_face(self): pass
    @abstractmethod
    def build_body(self): pass
# 具体建造者
class SexyGirlBuilder(PlayerBuilder):
    def __init__(self):
        self.player = Player()
    def build_face(self):
        self.player.face = &apos;very nice&apos;
    def build_body(self):
        self.player.body = &apos;slim&apos;
# 指挥者
class PlayerDirector:
    # 控制组装顺序
    def build_player(self, builder):
        builder.build_body()
        builder.build_face()
        return build.player

# 客户端
builder = SexyGirlBuilder()
director = PlayerDirector()
p = director.build_player(builder)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隐藏了一个产品的内部结构和装配过程&lt;/li&gt;
&lt;li&gt;将构造代码与表示代码分开&lt;/li&gt;
&lt;li&gt;可以对构造过程进行更精细的控制&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5、单例模式&lt;/h3&gt;
&lt;p&gt;​	内容：保证一个类只有一个实例，并提供一个访问它的全局访问点。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单例（Singleton）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Singleton:
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, &apos;_instance&apos;):
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

class MyClass(Singleton):
    def __init__(self, a):
        self.a = a

a = MyClass(10)
# 不会重新创建实例，但是会重新初始化
b = MyClass(20)
print(id(a), id(b), a.a)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对唯一实例的受控访问&lt;/li&gt;
&lt;li&gt;单例相当于全局变量，但防止了命名空间被污染&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6、总结&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;抽象工厂模式和建造者模式相比于简单工厂模式和工厂方法模式而言更灵活也更复杂。&lt;/li&gt;
&lt;li&gt;通常情况下设计以简单工厂模式或工厂方法模式开始，当你发现设计需要更大的灵活性时，则像更复杂的设计模式演化。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;三、结构型模式&lt;/h2&gt;
&lt;h3&gt;1、适配器模式&lt;/h3&gt;
&lt;p&gt;​	内容：将一个类的接口转换成客户希望的另一个接口。适配器模式使得&lt;strong&gt;原本由于接口不兼容而不能一起工作的那些类可以一起工作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	两种实现方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类适配器：使用多继承&lt;/li&gt;
&lt;li&gt;对象适配器：使用组合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标接口（Target）&lt;/li&gt;
&lt;li&gt;待适配的类（Adaptee）&lt;/li&gt;
&lt;li&gt;适配器（Adapter）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Payment(ABC):
    @abstractmethod
    def pay(self, money): pass
class Alipay(Payment):
    def pay(self, money): pass

# 需要被适配的类（因需求或代码兼容目的产生，但不能直接改，因为可能与高层模块存在耦合）
class BankPay:
    def cost(self, money): pass

# 专用的适配器类，即类适配器（多继承思想），被适配类依然可用原有其他方法
class NewBankPay(Payment, BankPay):
    def pay(self, money): self.cost(money)

p = NewBankPay()
p.pay(100)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Payment(ABC):
    @abstractmethod
    def pay(self, money): pass
class Alipay(Payment):
    def pay(self, money): pass

class BankPay:
    def cost(self, money): pass
class ApplePay:
    def cost(self, money): pass

# 适配多个类的适配器类，即对象适配器（组合思想），被适配类不可用原有其他方法，除非单独配置
class PaymentAdapter(Payment):
    def __init__(self, payment):
        self.payment = payment
    def pay(self, money):
        self.payment.cost(money)

p = PaymentAdapter(ApplePay())
p.pay(100)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、桥模式&lt;/h3&gt;
&lt;p&gt;​	内容：将一个事物的两个维度分离，使其都可以独立变化。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象（Abstraction）&lt;/li&gt;
&lt;li&gt;细化抽象（RefinedAbstraction）&lt;/li&gt;
&lt;li&gt;实现者（Implementor）&lt;/li&gt;
&lt;li&gt;具体实现者（Concretelmplementor）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	应用场景：当事物有两个维度上的表现，两个维度都可能扩展时。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 抽象
class Shape(ABC):
    def __init__(self, color):
        self.color = color
    @abstractmethod
    def draw(self): pass
# 实现
class Color(ABC):
    @abstractmethod
    def paint(self, shape): pass
# 细化抽象
class Circle(Shape):
    name = &apos;circle&apos;
    def draw(self):
        self.color.paint(self)
# 具体实现
class Blue(Color):
    def paint(self, shape): print(&apos;a blue %s&apos; %shape.name)

shape = Circle(Blue())
shape.draw()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	解释：这里的&quot;抽象&quot;，指的并非&quot;抽象类&quot;或&quot;接口&quot;，而是被抽象出来的一套&quot;类库&quot;，它只包含骨架代码，真正的业务逻辑需要委派给定义中的&quot;实现&quot;来完成。而定义中的&quot;实现&quot;，也并非&quot;接口的实现类&quot;，而是的一套独立的&quot;类库&quot;。&quot;抽象&quot;和&quot;实现&quot;独立开发，通过对象之间的组合关系，组装在一起。&lt;/p&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象和实现相分离&lt;/li&gt;
&lt;li&gt;优秀的扩展能力&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、组合模式&lt;/h3&gt;
&lt;p&gt;​	内容：将对象&lt;strong&gt;组合成树形结构以表示&quot;部分-整体&quot;的层次结构&lt;/strong&gt;。组合模式使得用户对单个对象和组合对象的使用具有一致性。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象组件（Component）&lt;/li&gt;
&lt;li&gt;叶子组件（Leaf）&lt;/li&gt;
&lt;li&gt;复合组件（Composite）&lt;/li&gt;
&lt;li&gt;客户端（Client）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表示对象的&quot;部分-整体&quot;层次结构（特别是结构是递归的）&lt;/li&gt;
&lt;li&gt;希望用户忽略组合对象与单个对象的不同，用户统一地使用组合结构中的所有对象&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 抽象组件
class Graphic(ABC):
    @abstractmethod
    def draw(self): pass
# 叶子组件
class Point(Graphic):
    def __init__(self, x, y):
        self.x, self.y = x, y
    def draw(self):
        print(&apos;绘制点(%s, %s)&apos; %(self.x, self.y))
class Line(Graphic):
    def __init__(self, p1, p2):
        self.p1, self.p2 = p1, p2
    def draw(self):
        print(&apos;绘制线段[(%s, %s), (%s, %s)]&apos; %(self.p1.x, self.p1.y, self.p2.x, self.p2.y))
# 复合组件
class Picture(Graphic):
    def __init__(self, iterable):
        self.children = []
        for g in iterable:
            self.add(g)
    def add(self, graphic):
        self.children.append(graphic)
    def draw(self):
        for g in self.children:
            g.draw()

p1 = Point(2, 3)
l1 = Line(Point(3, 4), p1)
pic1 = Picture([p1, l1])
pic2 = Picture([pic1, Point(5, 6)])
pic2.draw()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义了包含基本对象和组合对象的类层次结构&lt;/li&gt;
&lt;li&gt;简化客户端代码，即客户端可以一致地使用组合对象和单个对象&lt;/li&gt;
&lt;li&gt;更容易增加新类型的组件&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、外观模式&lt;/h3&gt;
&lt;p&gt;​	内容：为子系统中的一组接口提供一个一致的界面，外观模式定义了一个高层接口，这个接口使得这一子系统更加容易使用。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;外观（facade）&lt;/li&gt;
&lt;li&gt;子系统类（subsystem classes）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 子系统类
class CPU:
    def runCmd(): pass
    def stop(): pass
class Disk:
    def work(): pass
    def close(): pass
class Mem:
    def powerOn(): pass
    def powerOff(): pass
# 外观
class Computer:
    def __init__(self):
        self.cpu, self.disk, self.mem = CPU(), Disk(), Mem()
    def run(self):
        self.cpu.runCmd()
        self.disk.work()
        self.mem.powerOn()
    def stop(self):
        self.cpu.stop()
        self.disk.close()
        self.mem.powerOff()

c = Computer()
c.run()
c.stop()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;减少系统相互依赖&lt;/li&gt;
&lt;li&gt;提高了灵活性&lt;/li&gt;
&lt;li&gt;提高了安全性&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5、代理模式&lt;/h3&gt;
&lt;p&gt;​	内容：为其他对象提供一种代理以控制对这个对象的访问。&lt;/p&gt;
&lt;p&gt;​	应用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;远程代理：为远程的对象提供代理&lt;/li&gt;
&lt;li&gt;虚代理：根据需要创建很大的对象&lt;/li&gt;
&lt;li&gt;保护代理：控制对原始对象的访问，用于对象有不同访问权限时&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象实体（Subject）&lt;/li&gt;
&lt;li&gt;实体（RealSubject）&lt;/li&gt;
&lt;li&gt;代理（Proxy）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 抽象实体
class Subject(ABC):
    @abstractmethod
    def get_content(self): pass
    def set_content(self): pass
# 实体
class RealSubject(Subject):
    def __init__(self, filepath):
        self.filepath = filepath
        with open(filepath) as fp:
            self.content = fp.read()
    def get_content(self): return self.content
    def set_content(self, content):
        with open(filepath, &apos;w&apos;) as fp:
       		fp.write(content)
# 虚代理
class VirtualProxy(Subject):
    def __init__(self, filepath):
        self.filepath = filepath
        self.subj = None
    def get_content(self):
        if not self.subj:
            self.subj = RealSubject()
        return self.subj.get_content()
# 保护代理
class ProtectedProxy(Subject):
    def __init__(self, filepath):
        self.subj = RealSubject(filepath)
    def get_content(self):
        return self.subj.get_content()
    def set_content(self):
        # 这里不一定是拒绝，也可以是验权等操作
        raise PermissionError(&quot;Set Permission denied&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;远程代理：可以隐藏对象位于远程地址空间的事实&lt;/li&gt;
&lt;li&gt;虚代理：可以&lt;strong&gt;进行优化&lt;/strong&gt;，例如根据要求创建对象&lt;/li&gt;
&lt;li&gt;保护代理：允许在访问一个对象时有一些&lt;strong&gt;附加的内务处理&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四、行为型模式&lt;/h2&gt;
&lt;h3&gt;1、责任链模式&lt;/h3&gt;
&lt;p&gt;​	内容：使多个对象都有机会处理请求，从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链，并沿着这条链传递该请求，直到有一个对象处理它为止。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 抽象处理者
class Handler(ABC):
    @abstractmethod
    def HandleEvent(self, condition):
        pass
# 具体处理者
class Level_1_Handler(Handler):
    def HandleEvent(self, condition):
        # 本级处理逻辑
        pass
class Level_2_Handler(Handler):
    def __init__(self):
        self.next = Level_1_Handler()
    def HandleEvent(self, condition):
        # 随便给的一个函数，表示本层可以 handle 时，进入这一分支
        if CanHandleEvent(self, condition): 
            # 本级处理逻辑
            pass
        else: 
            self.next.HandleEvent(condition)
    
h = Level_2_Handler()
h.HandleEvent(the_condition)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象处理者（Handler）&lt;/li&gt;
&lt;li&gt;具体处理者（ConcreteHandler）&lt;/li&gt;
&lt;li&gt;客户端（Client）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2、观察者模式&lt;/h3&gt;
&lt;p&gt;​	内容：定义对象间的一种一对多的依赖关系，&lt;strong&gt;当一个对象的状态发生改变时，所有依赖于它的对象都得到通知并被自动更新&lt;/strong&gt;。观察者模式又称&quot;发布-订阅&quot;模式。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象主题（Subject）&lt;/li&gt;
&lt;li&gt;具体主题（ConcreteSubject）—— 发布者&lt;/li&gt;
&lt;li&gt;抽象观察者（Observer）&lt;/li&gt;
&lt;li&gt;具体观察者（ConcreteObserver）—— 订阅者&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当一个抽象模型有两方面，其中一个方面依赖于另一个方面。&lt;strong&gt;将这两者封装在独立对象中以使它们可以各自独立地改变和复用&lt;/strong&gt;。（主要场景）&lt;/li&gt;
&lt;li&gt;当对一个对象的改变需要同时改变其它对象，而不知道具体有多少对象有待改变。（客户端后期动态指定通知对象）&lt;/li&gt;
&lt;li&gt;当一个对象必须通知其它对象，而它又不能假定其它对象是谁。换言之，你不希望这些对象是紧密耦合的。（客户端后期动态指定通知对象）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 抽象观察者（订阅者）
class Observer(ABC):
    @abstractmethod
    def update(self, notice):
        pass
# 具体观察者
class StatusObserver(Observer):
    def __init__(self):
        # 未绑定观察关系前的初始状态
        self.status = &apos;the init observer status/data&apos;
    def update(self, subject):
        self.status = subject.status
# 抽象主题（发布者）
class Subject(ABC):
    @abstractmethod
    def attach(self, obs): pass
    @abstractmethod
    def detach(self, obs): pass
    @abstractmethod
    def notify(self): pass
# 具体主题
class RealSubject(Subject):
    def __init__(self, status=None):
        self.observers = []
        self.__status = status
    def attach(self, obs):
        self.observers.append(obs)
        # 绑定时进行一次发布，若不需要，注释该行代码
        self.notify()
    def detach(self, obs):
        self.observers.remove(obs)
    def notify(self):
        for obs in self.observers:
            obs.update(self)
    # 下面使用装饰器，实现对指定私有属性赋值，即刻通知观察者同步值
    # 但在实际情况中，也有可能是发布者某一行为触发观察者另一行为，但把握住方法的核心就好：发布者此时调用自己的 notify() 方法，同时保证观察者需要的发布者属性或方法能被访问到即可（一般比较方便的是传递观察者的对象引用）。
    @property
    def status(self): return self.__status
    @status.setter
    def status(self, new_status):
        self.__status = new_status
        self.notify()

s = RealSubject(&apos;the init subject status/data&apos;)
a= StatusObserver()
print(a.status)
s.attach(a)
print(a.status)
s.status = &apos;the changed subject status/data&apos;
print(a.status)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标和观察者之间的&lt;strong&gt;抽象耦合最小&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;支持&lt;strong&gt;广播&lt;/strong&gt;通信&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、策略模式&lt;/h3&gt;
&lt;p&gt;​	内容：定义一系列的算法，把它们一个个封装起来，并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象策略（Strategy）&lt;/li&gt;
&lt;li&gt;具体策略（ConcreteStrategy）&lt;/li&gt;
&lt;li&gt;上下文（Context）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 抽象策略
class Strategy(ABC):
    @abstractmethod
    def execute(self, data):
        pass
# 一个策略
class OneStrategy(Strategy):
    def execute(self, data):
        print(&apos;使用一个策略解决问题%s&apos; %data)
# 另一个策略
class AnotherStrategy(Strategy):
    def execute(self, data):
        print(&apos;使用另一个策略解决问题%s&apos; %data)
# 上下文
class Context:
    def __init__(self, strategy, data):
        self.data = data
        self.strategy = strategy
    def set_strategy(self, strategy):
        self.strategy = strategy
    def do_strategy(self):
        self.strategy.execute(data)

s1 = OneStrategy()
s2 = AnotherStrategy()
data = &apos;xxxxx&apos;
c = Context(s1, data)
c.do_strategy()
c.set_strategy()
c.do_strategy()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义了一系列可重用的算法和行为&lt;/li&gt;
&lt;li&gt;消除了一些条件语句&lt;/li&gt;
&lt;li&gt;可以提供相同行为的不同实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户必须了解不同的策略&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、模板方法模式&lt;/h3&gt;
&lt;p&gt;​	内容：&lt;strong&gt;定义一个操作中的算法的骨架，而将一些步骤延迟到子类中&lt;/strong&gt;。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。&lt;/p&gt;
&lt;p&gt;​	角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抽象类（AbstractClass）：定义抽象的原子操作（钩子操作）；实现一个模板方法作为算法的骨架。&lt;/li&gt;
&lt;li&gt;具体类（ConcreteClass）：实现原子操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一次性实现一个算法的不变的部分&lt;/li&gt;
&lt;li&gt;各个子类中的公共行为应该被提取出来并集中到一个公共父类中以避免代码重复&lt;/li&gt;
&lt;li&gt;控制子类扩展&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 抽象类
class Window(ABC):
    @abstractmethod
    def start(self): pass
    @abstractmethod
    def repaint(self): pass
    @abstractmethod
    # 原子/钩子操作
    def stop(self): pass
    @abstractmethod
    # 模板方法（不变或必需的操作）
    def run(self):
        self.start()
        while True:
            try:
                self.repaint()
            except KeyboardInterrupt:
                break
        self.stop()
# 具体类
class MyWindow(Window):
    def __init__(self, msg):
        self.msg = msg
    def start(self):
        print(&quot;窗口开始运行&quot;)
    def stop(self):
        print(&quot;窗口结束运行&quot;)
    def repaint(self):
        print(self.msg)

MyWindow(&quot;Hello World!&quot;).run()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>停止 Windows 更新的新方法</title><link>https://miku.top/posts/stop-win-update-2/</link><guid isPermaLink="true">https://miku.top/posts/stop-win-update-2/</guid><description>停止 win11 更新的新方法</description><pubDate>Tue, 13 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;停止 Windows 更新的新方法&lt;/h1&gt;
&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;:::tip
本文于 2022 年发布，具体方法是否依然适用请多加鉴别。
:::&lt;/p&gt;
&lt;p&gt;:::tip
本文章方法适用于将 win11 停止更新。win10 停止更新老方法依旧有效，请自行上网搜索。关于停止更新，我之前一篇文章有比较详尽地说过了。见于：&lt;a href=&quot;/posts/stop-win-update/&quot;&gt;如何彻底停止 Windows 更新
&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;​ 但这个方案其实还不是很好，因为它有着以下两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;移动了系统文件，可能导致系统不稳定，而且提升了操作风险&lt;/li&gt;
&lt;li&gt;操作后，微软商店服务无法使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​ 因此我又进行了一些探索。现在找到了一个新的方法，可以同时解决以上两个问题。但是这个方法只适用于预览版的系统。（Dev 和 Beta 渠道）&lt;/p&gt;
&lt;h2&gt;二、方法&lt;/h2&gt;
&lt;h3&gt;1、原理简述&lt;/h3&gt;
&lt;p&gt;​
这次会直接使用一个工具，相对于之前的方法，简单很多。甚至不懂电脑的小白也可以很好的操作。&lt;/p&gt;
&lt;p&gt;​简单说一下这个工具的原理，它主要利用了预览版的渠道回滚机制。使用这个工具，可以让 Dev 回滚到 Beta 或 Release 渠道，让 Beta 回滚到 Release 渠道。而因为 Dev、Beta 渠道的系统内核版本一定是比 Release 更高的，所以回滚后，不会收到任何有效的更新推送。这样操作后，虽然还是会有更新检查，但就不会触发更新的操作了。&lt;/p&gt;
&lt;h3&gt;2、使用&lt;/h3&gt;
&lt;p&gt;​直接访问这个项目：https://github.com/abbodi1406/offlineinsiderenroll ，下载它的 release。解压出来应该是一个 bat 批处理脚本（记得以管理员身份打开）。进入脚本后，输入数字再回车，即可选择回滚的渠道。&lt;/p&gt;
&lt;p&gt;按照我们刚才的原则，更高级别的渠道，回滚至更低的渠道即可。操作后，再打开 设置 &amp;gt; Windows 更新 &amp;gt; Windows 预览体验计划，显示的渠道与刚才选择的渠道一致，即为操作成功。&lt;/p&gt;
&lt;h2&gt;三、注意事项&lt;/h2&gt;
&lt;p&gt;​
注意，预览版的系统都是有使用期限的。可以使用在控制台使用 &lt;code&gt;winver&lt;/code&gt; 命令获知这个时间期限。&lt;/p&gt;
&lt;p&gt;到达这个期限后，该版本的预览版系统会无法正常使用。因此，在此时间节点前，一定要回滚至原来的渠道，完成一次更新，以切换至最新版本的系统。一般来说，最新版本的系统，因为刚发布，所以过期时间会在几个月或一年之后。&lt;/p&gt;
</content:encoded></item><item><title>Vuex 学习笔记</title><link>https://miku.top/posts/vuex-learn/</link><guid isPermaLink="true">https://miku.top/posts/vuex-learn/</guid><description>Vuex 是实现组件全局状态（数据）管理的一种方式</description><pubDate>Mon, 01 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Vuex 学习笔记&lt;/h1&gt;
&lt;p&gt;本文以适用于 Vue3 的 Vuex4 为例。&lt;/p&gt;
&lt;h2&gt;一、Vuex 是什么&lt;/h2&gt;
&lt;p&gt;​Vuex 是实现组件全局状态（数据）管理的一种机制，可以方便的实现组件之间数据的共享。&lt;/p&gt;
&lt;p&gt;​好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能够在 vuex 中集中管理共享的数据，易于开发和后期维护&lt;/li&gt;
&lt;li&gt;能够高效地实现组件之间的数据共享，提高开发效率存储&lt;/li&gt;
&lt;li&gt;在 vuex 中的数据都是响应式的，能够实时保持数据与页面的同步
​
注：一般情况下，只有组件之间共享的数据，才有必要存储到 vuex 中；对于组件中的私有数据，依旧存储在组件自身的 data 中即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、Vuex 核心概念&lt;/h2&gt;
&lt;h3&gt;1、state&lt;/h3&gt;
&lt;p&gt;​state 提供唯一的公共数据源，所有共享的数据都要统一放到 store 的 state 中进行存储。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createStore } from &apos;vuex&apos;
 
export default createStore({
    state: () =&amp;gt; ({
        count: 0;
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 组件中访问 state 中数据的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.$store.state.全局数据名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 按以下方式，可以绑定到计算属性上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;computed(){
    count(){
        return this.$store.state.count;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 但过于冗余，可以用 mapState 简化操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { mapState } from &apos;vuex&apos;
 
computed: {
    ...mapState([&apos;count&apos;])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、mutations&lt;/h3&gt;
&lt;p&gt;​
mutation用于变更 store 中的数据。&lt;/p&gt;
&lt;p&gt;只能通过 mutation 变更 store 数据，不可以直接操作 store 中的数据。通过这种方式虽然操作起来稍微繁琐一些，但是可以集中监控所有数据的变化。store.js 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createStore } from &apos;vuex&apos;
 
export default createStore({
    state: () =&amp;gt; ({
        count: 0;
    }),
    mutations: {
        // 传递参数
        add(state, step) {
            state.count += step;
        }
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​组件中触发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;methods: {
    handle1() {
        this.$store.commit(&apos;add&apos;, 2);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​也可以使用 mapMutations 触发 mutations，映射为方法：（更简洁）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { mapMutations } from &apos;vuex&apos;
 
methods: {
    ...mapMutations([&apos;add&apos;, &apos;addN&apos;])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​注：不要在 mutations 中执行异步操作，会导致调试器数据不同步！&lt;/p&gt;
&lt;h3&gt;3、actions&lt;/h3&gt;
&lt;p&gt;​actions 用于处理异步任务。&lt;/p&gt;
&lt;p&gt;​如果通过异步操作变更数据，必须通过 actions，而不能使用 mutations，但是在 actions 中还是要通过触发 mutations 的方式间接变更数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const store = createStore({
    mutations: {
        add(state) {
            state.count++;
        }
    },
    actions: {
        addNAsnyc(context, step) {
            // 携带参数
            setTimeout(() =&amp;gt; {
                context.commit(&apos;addN&apos;, step);
            }, 1000)
        }
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 触发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;methods: {
    handle() {
        this.$state.dispatch(&apos;addAsync&apos;, 5);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​同样地，我们也可以用 mapActions 映射其到方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { mapActions } from &apos;vuex&apos;
 
methods: {
    ...mapActions([&apos;addAsync&apos;, &apos;addNAsync&apos;]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、getters&lt;/h3&gt;
&lt;p&gt;​getters 用于对 store 中的数据进行加工处理形成新的数据。&lt;/p&gt;
&lt;p&gt;getters 可以对 store 中已有的数据加工处理之后形成新的数据，类似计算属性。store 中数据发生变化，getters 的数据也会跟着变化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const store = createStore({
    state: () =&amp;gt; ({
        count: 0;
    }),
    getters: {
        showNum: state =&amp;gt; {
            return `当前最新数量值：${state.count}`;
        }
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.$store.getters.showNum
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​映射至计算属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { mapGetters } from &apos;vuex&apos;
 
computed: {
    ...mapGetters([&apos;showNum&apos;])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、modules&lt;/h3&gt;
&lt;p&gt;​业务逻辑不复杂时用不到。参见：https://vuex.vuejs.org/zh/guide/modules.html&lt;/p&gt;
</content:encoded></item><item><title>Vue 学习笔记</title><link>https://miku.top/posts/vue-learn/</link><guid isPermaLink="true">https://miku.top/posts/vue-learn/</guid><description>使用 Vue 构建用户界面，极大的提高了前端开发的效率和体验</description><pubDate>Thu, 28 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Vue 笔记&lt;/h1&gt;
&lt;h2&gt;一、Vue 基础&lt;/h2&gt;
&lt;h3&gt;1、Vue 简介&lt;/h3&gt;
&lt;h4&gt;（1）概述&lt;/h4&gt;
&lt;p&gt;​	使用 vue 构建用户界面，解决了 jQuery + 模板引擎的诸多痛点，极大的提高了前端开发的效率和体验。&lt;/p&gt;
&lt;p&gt;​	官方给 vue 的定位是前端框架，因为它提供了构建用户界面的一整套解决方案（俗称 vue 全家桶）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;vue（核心库）&lt;/li&gt;
&lt;li&gt;vue-router（路由方案）&lt;/li&gt;
&lt;li&gt;vuex（状态管理方案）&lt;/li&gt;
&lt;li&gt;vue 组件库（快速搭建页面 UI 效果的方案）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	以及辅助 vue 项目开发的一系列工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;vue-cli（ npm 全局包：一键生成工程化的 vue 项目-基于 webpack、大而全）&lt;/li&gt;
&lt;li&gt;vite（ npm 全局包：一键生成工程化的 vue 项目-小而巧）&lt;/li&gt;
&lt;li&gt;vue-devtools（浏览器插件：辅助调试的工具）&lt;/li&gt;
&lt;li&gt;vetur（ vscode 插件：提供语法高亮和智能提示）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	总结：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220514121234994.ZI-ZEVrp_10FuVs.webp&quot; alt=&quot;image-20220514121234994&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;（2）Vue 特性&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;数据驱动&lt;/strong&gt;：在使用了vue 的页面中，vue 会监听数据的变化，从而自动重新渲染页面的结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220514121438573.BRvAKqIv_Z1GRzYo.webp&quot; alt=&quot;image-20220514121438573 w-80%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;双向数据绑定&lt;/strong&gt;：在填写表单时，双向数据绑定可以辅助开发者在不操作 DOM 的前提下，自动把用户填写的内容同步到数据源中。示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220514121534071.B7S9I6yK_ZKM7mG.webp&quot; alt=&quot;image-20220514121534071 w-80%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220514121604201.CsdhPvhI_12SV92.webp&quot; alt=&quot;image-20220514121604201 w-80%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​		意义：开发者不再需要手动操作DOM元素，来获取表单元素最新的值！&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;MVVM&lt;/strong&gt;：MVVM（Model-View-ViewModel） 是 vue 实现数据驱动视图和双向数据绑定的核心原理。它把每个 HTML 页面都拆分成了如下三个部分：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220514121721221.CW58sF3o_1Rq3L4.webp&quot; alt=&quot;image-20220514121721221&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	在MVVM概念中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;View 表示当前页面所渲染的 DOM 结构。&lt;/li&gt;
&lt;li&gt;Model 表示当前页面渲染时所依赖的数据源。&lt;/li&gt;
&lt;li&gt;ViewModel 表示 vue 的实例，它是 MVVM 的核心。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（3）Vue 版本&lt;/h4&gt;
&lt;p&gt;​	3.x 版本的 vue 是未来企业级项目开发的趋势，2.x 版本的 vue 在未来（1~2年内）会被逐渐淘汰。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;对比&lt;/strong&gt;：vue2.x 中绝大多数的 API 与特性，在 vue3.x 中同样支持。同时，vue3.x 中还新增了 3.x 所特有的功能、并废弃了某些 2.x 中的旧功能。&lt;/p&gt;
&lt;h3&gt;2、基本使用（Vue2 为例）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;导入 vue.js 的 script 脚本文件&lt;/li&gt;
&lt;li&gt;在页面中声明一个将要被 vue 所控制的 DOM 区域&lt;/li&gt;
&lt;li&gt;创建 vm 实例对象（vue实例对象）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 声明 vue 控制的 dom 区域--&amp;gt;
&amp;lt;div id=&quot;app&quot;&amp;gt;{{ username }}&amp;lt;/div&amp;gt;
&amp;lt;!-- 导入 Vue 脚本 --&amp;gt;
&amp;lt;script src=&quot;https://cdn.bootcdn.net/ajax/libs/vue/2.6.2/vue.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;!-- 创建 Vue 实例 --&amp;gt;
&amp;lt;script&amp;gt;
    const vm = new Vue({
        el: &apos;#app&apos;,
        data: {
            username: &apos;MelodyEcho&apos;,
        },
    })
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	代码与 MVVM 的对应关系：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220514124131934.EuOfWRhc_19w3m9.webp&quot; alt=&quot;image-20220514124131934&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3、指令&lt;/h3&gt;
&lt;p&gt;​	指令（Directives）是 vue 为开发者提供的模板语法，用于辅助开发者渲染页面的基本结构。&lt;/p&gt;
&lt;p&gt;​	有以下六类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容渲染指令&lt;/li&gt;
&lt;li&gt;属性绑定指令&lt;/li&gt;
&lt;li&gt;事件绑定指令&lt;/li&gt;
&lt;li&gt;双向绑定指令&lt;/li&gt;
&lt;li&gt;条件渲染指令&lt;/li&gt;
&lt;li&gt;列表渲染指令&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（1）内容渲染指令&lt;/h4&gt;
&lt;p&gt;​	常用的有 &lt;code&gt;v-text&lt;/code&gt;、&lt;code&gt;{{}}&lt;/code&gt;、&lt;code&gt;v-html&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 把username对应的值，渲染到第一个p标签中 --&amp;gt;
&amp;lt;p v-text=&quot;username&quot;&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;!-- 把gender对应的值，渲染到第二个p标签中 --&amp;gt;
&amp;lt;!-- 注意：第二个p标签中，默认的文本“性别”会被 gender的值覆盖掉 --&amp;gt;
&amp;lt;p v-text=&quot;gender&quot;&amp;gt;性别&amp;lt;/p&amp;gt;

&amp;lt;script&amp;gt;
	const vm = new Vue({
        el: &apos;#app&apos;,
        data: {
            username: &apos;MelodyEcho&apos;,
            gender: &apos;female&apos;
        }
    })
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 不会覆盖默认的内容 --&amp;gt;
&amp;lt;p&amp;gt;姓名：{{username}}&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;性别：{{gender}}&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;&lt;code&gt;v-text&lt;/code&gt; 指令和插值表达式只能渲染纯文本内容&lt;/strong&gt;。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素，则需要用到 &lt;code&gt;v-html&lt;/code&gt; 这个指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- description = &apos;&amp;lt;h2&amp;gt;二级标题&amp;lt;/h2&amp;gt;&apos; --&amp;gt;
&amp;lt;p v-html=&quot;description&quot;&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）属性绑定指令&lt;/h4&gt;
&lt;p&gt;​	如果需要为元素的属性动态绑定属性值，则需要用到 &lt;code&gt;v-bind&lt;/code&gt; 属性绑定指令。用法示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;text&quot; v-bind:placeholder=&quot;inputValue&quot; /&amp;gt;
&amp;lt;img v-bind:src=&quot;imgSrc&quot; alt=&quot;&quot; /&amp;gt;
&amp;lt;script&amp;gt;
	...
    data : {
        inputValue: &apos;输入框提示&apos;,
        imgSrc: &apos;./img/bg.jpg&apos;,
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;简写形式：&lt;code&gt;v-bind:xxx&lt;/code&gt; -&amp;gt; &lt;code&gt;:xxx&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;（3）渲染时使用 js 表达式&lt;/h4&gt;
&lt;p&gt;​	如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;
    &amp;lt;span&amp;gt;{{ string?&apos;True&apos;:&apos;False&apos; }}&amp;lt;/span&amp;gt;
    &amp;lt;span&amp;gt;{{ msg.split(&apos;&apos;).reverse().join(&apos;&apos;) }}&amp;lt;/span&amp;gt;
    &amp;lt;span :id=&quot;&apos;list-&apos; + id&quot;&amp;gt;&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）事件对象绑定指令&lt;/h4&gt;
&lt;p&gt;​	vue 提供了 &lt;code&gt;v-on&lt;/code&gt; 事件绑定指令，用来辅助程序员为 DOM 元素绑定事件监听。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button v-on:click=&quot;addCount&quot;&amp;gt;+1&amp;lt;/button&amp;gt;
&amp;lt;!-- 由于函数简单，也可以直接使用 js 表达式 --&amp;gt;
&amp;lt;!-- &amp;lt;button @click=&quot;count += 1&quot;&amp;gt;+1&amp;lt;/button&amp;gt; --&amp;gt;
&amp;lt;script&amp;gt;
	...
    data : { count = 0 },
    methods: {
        // 事件监听在 method 节点
        addCount() {
            // this 表示当前的 vw 对象
            // 通过 this 可以访问到 data 中的数据
            this.count += 1
        }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注意：原生DOM对象有 onclick、oninput、onkeyup 等原生事件，替换为 vue 的事件绑定形式后，分别为：v-on:click、v-oninput、v-on:keyup&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;简写形式：&lt;code&gt;v-on:click&lt;/code&gt; -&amp;gt; &lt;code&gt;@click&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;事件对象&lt;/strong&gt;：（同样可以接收到事件对象）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;methods: {
    addCount(e) {
        const color = e.target.style.backgroundColor
        console.log(color)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;绑定传参&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button @click=&quot;addNewCount(2)&quot;&amp;gt;+2&amp;lt;/button&amp;gt;
&amp;lt;script&amp;gt;
	...
    methods: {
        addNewCount(step) { this.count += step }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;如果同时需要参数和事件对象&lt;/strong&gt;：（需要使用 $event 特殊变量）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button @click=&quot;addNewCount(2, $event)&quot;&amp;gt;+2&amp;lt;/button&amp;gt;
&amp;lt;script&amp;gt;
	...
    methods: {
        addNewCount(step, e) {
            const color = e.target.style.backgroundColor
            console.log(color)
            this.count += step
        }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（5）事件、按键修饰符&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;事件修饰符&lt;/strong&gt;：在事件处理函数中调用 &lt;code&gt;preventDefault()&lt;/code&gt; 或 &lt;code&gt;stopPropagation()&lt;/code&gt; 是非常常见的需求。因此， vue 提供了事件修饰符的概念，来辅助程序员更方便的对事件的触发进行控制。常用的 5 个事件修饰符如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;时间修饰符&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.prevent&lt;/td&gt;
&lt;td&gt;阻止默认行为（例如：阻止 a 链接的跳转、阻止表单的提交等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.stop&lt;/td&gt;
&lt;td&gt;阻止事件冒泡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.capture&lt;/td&gt;
&lt;td&gt;以捕获模式触发当前的事件处理函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.once&lt;/td&gt;
&lt;td&gt;绑定的事件只触发 1 次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.self&lt;/td&gt;
&lt;td&gt;只有在 event.target 是当前元素自身时，才触发事件处理函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;https://example.com&quot; @click.prevent=&quot;onLinkClick&quot;&amp;gt;去往 example.com&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;按键修饰符&lt;/strong&gt;：在监听键盘事件时，我们经常需要判断详细的按键。可以为键盘相关的事件添加按键修饰符，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input @keyup.enter=&quot;submit&quot;&amp;gt;
&amp;lt;input @keyup.esc=&quot;clearInput&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（6）双向绑定指令&lt;/h4&gt;
&lt;p&gt;​	vue 提供了 &lt;code&gt;v-model&lt;/code&gt; 双向数据绑定，用来辅助开发者在不操作 DOM 的前提下，快速获取表单的数据。只能配合表单元素使用！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;用户名是：{{username}}&amp;lt;/p&amp;gt;
&amp;lt;input type=&quot;text&quot; v-model=&quot;username&quot;/&amp;gt;

&amp;lt;p&amp;gt;选中的省份是：{{province}}&amp;lt;/p&amp;gt;
&amp;lt;select v-model=&quot;province&quot;&amp;gt;
    &amp;lt;!-- 这里绑定的是 option 的 value --&amp;gt;
    &amp;lt;option value=&quot;&quot;&amp;gt;请选择&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;1&quot;&amp;gt;北京&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;2&quot;&amp;gt;河北&amp;lt;/option&amp;gt;
    &amp;lt;option value=&quot;3&quot;&amp;gt;黑龙江&amp;lt;/option&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	有三种修饰符，协助快速格式化数据类型：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;修饰符&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.number&lt;/td&gt;
&lt;td&gt;自动将用户的输入值转为数值类型&lt;/td&gt;
&lt;td&gt;&amp;lt;input v-model.number=&quot;age&quot; /&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.trim&lt;/td&gt;
&lt;td&gt;自动过滤用户输入的首尾空白字符&lt;/td&gt;
&lt;td&gt;&amp;lt;input v-model.trim=&quot;msg&quot; /&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.lazy&lt;/td&gt;
&lt;td&gt;在“change&quot;时而非“input”时更新&lt;/td&gt;
&lt;td&gt;&amp;lt;input v-model.lazy=&quot;msg&quot; /&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;（7）条件渲染指令&lt;/h4&gt;
&lt;p&gt;​	条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个，分别是：&lt;code&gt;v-if&lt;/code&gt; 和 &lt;code&gt;v-show&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;span v-if=&quot;flag&quot;&amp;gt;一个想被隐藏的元素&amp;lt;/span&amp;gt;
&amp;lt;span v-show=&quot;flag&quot;&amp;gt;一个想被隐藏的元素&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;区别&lt;/strong&gt;：&lt;code&gt;v-if&lt;/code&gt; 会操作 DOM 节点来控制显示，&lt;code&gt;v-show&lt;/code&gt; 则通过样式来控制显示。但是 &lt;code&gt;v-show&lt;/code&gt; 有很高的初始渲染开销。因此如果&lt;strong&gt;需要频繁切换，则使用 &lt;code&gt;v-show&lt;/code&gt;&lt;/strong&gt; ，如果&lt;strong&gt;运行时条件很少改变，则应该使用 &lt;code&gt;v-if&lt;/code&gt;&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;配套的 &lt;code&gt;v-else&lt;/code&gt; 指令&lt;/strong&gt;：（类似于 php）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div v-if=&quot;Math.random() &amp;gt; 0.5&quot;&amp;gt;随机数 &amp;gt; 0.5&amp;lt;/div&amp;gt;
&amp;lt;div v-else&amp;gt;随机数 &amp;amp;lt;= 0.5&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;当然还有 &lt;code&gt;v-else-if&lt;/code&gt; 指令&lt;/strong&gt; ，用法类似。&lt;/p&gt;
&lt;h4&gt;（8）列表渲染指令&lt;/h4&gt;
&lt;p&gt;​	 vue 提供了&lt;code&gt;v-for&lt;/code&gt; 指令，用来辅助开发者基于一个数组来循环渲染相似的 UI 结构。&lt;code&gt;v-for&lt;/code&gt; 指令需要使用 item in items 的特殊语法，其中：items 是待循环的数组，item 是当前的循环项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
    &amp;lt;li v-for=&quot;person in list&quot;&amp;gt;姓名是: {{ person.name }}&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&amp;lt;script&amp;gt;
	...
    data: {
        list: [
            {id: 1, name: &quot;MelodyEcho&quot;},
            {id: 2, name: &quot;MelodyScend&quot;},
        ]
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	也可以携带索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
    &amp;lt;li v-for=&quot;(item, i) in list&quot;&amp;gt;索引：{{ i }}, 姓名: {{ item.name }}&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（9）使用 key 属性维护列表状态&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;问题&lt;/strong&gt;：当列表的数据变化时，默认情况下，vue 会尽可能的复用已存在的 DOM 元素，从而提升渲染的性能。但这种默认的性能优化策略，会导致有状态的列表无法被正确更新。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;因此&lt;/strong&gt;：为了给 vue 一个提示，以便它能跟踪每个节点的身份，从而在保证有状态的列表被正确更新的前提下，提升渲染的性能。此时，需要为每个元素提供一个&lt;strong&gt;唯一的 key 属性&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
    &amp;lt;li v-for=&quot;(user, i) in list&quot; :key=&quot;user.id&quot;&amp;gt; 
        &amp;lt;input type=&quot;checkbox&quot; /&amp;gt; {{ user.name }}
    &amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&amp;lt;script&amp;gt;
	...
    data: {
        list: [
            {id: 1, name: &quot;MelodyEcho&quot;},
            {id: 2, name: &quot;MelodyScend&quot;},
        ]
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;key 只能为字符串或数字类型&lt;/li&gt;
&lt;li&gt;key 必须唯一&lt;/li&gt;
&lt;li&gt;建议将原数据的 id 特征值作为 key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要使用索引，因为索引会随状态而更新！&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建议使用 &lt;code&gt;v-for&lt;/code&gt; 时，都指定 key 的值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、过滤器（Vue3 弃用）&lt;/h3&gt;
&lt;p&gt;​	注：过滤器已经在 Vue3 弃用，建议使用计算属性替代。&lt;/p&gt;
&lt;h4&gt;（1）定义&lt;/h4&gt;
&lt;p&gt;​	过滤器（Filters）常用于文本的格式化。例如：&lt;/p&gt;
&lt;p&gt;​	hello -&amp;gt; Hello&lt;/p&gt;
&lt;p&gt;​	过滤器应该被添加在 JavaScript 表达式的尾部，由“管道符”进行调用，示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 通过“管道符”调用 capitalize 过滤器，对 message 的值进行格式化 --&amp;gt;
&amp;lt;p&amp;gt;{{ message | capitalize }}&amp;lt;/p&amp;gt;
&amp;lt;!-- 在 v-bind 中通过“管道符”调用 formatId 过滤器，对 rawId 的值进行格式化 --&amp;gt;
&amp;lt;div v-bind:id=&quot;rawId | formatId&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
// 在 filters 节点定义过滤器
...
	filters: {
        capitalize(str) {
            return str.charAt(0).toUpperCase() + str.slice(1)
        }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：过滤器可以用在两个地方：&lt;strong&gt;插值表达式&lt;/strong&gt;和 &lt;strong&gt;&lt;code&gt;v-bind&lt;/code&gt; 属性绑定&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;（2）私有过滤器和全局过滤器&lt;/h4&gt;
&lt;p&gt;​	在 filters 节点下定义的过滤器，称为“私有过滤器”，因为它只能在当前 vm 实例所控制的 el 区域内使用。如果希望在多个 vue 实例之间共享过滤器，则可以按照如下的格式定义全局过滤器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 全局过滤器-独立于每个 vm 实例之外
// Vue.filter() 方法接收两个参数：
// 第 1 个参数，是全局过滤器的“名字”
// 第 2 个参数，是全局过滤器的“处理函数”
Vue.filter(&apos;capitalize&apos;, (str)=&amp;gt;{
	return str.charAt(0).toupperCase() + str.slice(1)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	当全局和私有过滤器同名，私有过滤器优先。&lt;/p&gt;
&lt;h4&gt;（3）连续调用多个过滤器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 调用方向为从左到右 --&amp;gt;
&amp;lt;p&amp;gt;{{ text | capitalize | maxLength }}&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）过滤器传参&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 第一个参数永远是管道符前待处理的值，第二个参数开始，才是调用传递的参数 --&amp;gt;
&amp;lt;span&amp;gt;{{ message | fillterA(arg1, arg2) }}&amp;lt;/span&amp;gt;
&amp;lt;script&amp;gt;
	...
    filters: {
        filterA(msg, arg1, arg2) {}
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、组件基础（上）&lt;/h2&gt;
&lt;h3&gt;1、单页面应用程序&lt;/h3&gt;
&lt;p&gt;​	单页面应用程序（英文名：Single Page Application）简称 SPA，顾名思义，指的是一个 Web 网站中只有唯一的一个 HTML 页面，所有的功能与交互都在这唯一的一个页面内完成。&lt;/p&gt;
&lt;p&gt;​	单页面应用程序将所有的功能局限于一个 web 页面中，仅在该 web页面初始化时加载相应的资源（HTML、JavaScript 和 CSS）。一旦页面加载完成了，SPA 不会因为用户的操作而进行页面的重新加载或跳转。而是利用 JavaScript 动态地变换 HTML 的内容，从而实现页面与用户的交互。&lt;/p&gt;
&lt;h4&gt;（1）优缺点&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;良好的交互体验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单页应用的内容的改变不需要重新加载整个页面&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取数据也是通过 Ajax 异步获取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有页面之间的跳转，不会出现“白屏现象”&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;良好的前后端工作分离模式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;减轻服务器压力&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首屏慢（但可以解决）&lt;/li&gt;
&lt;li&gt;不利于 SEO（可以使用 SSR 服务器端渲染）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（2）快速创建 Vue 的 SPA 项目&lt;/h4&gt;
&lt;p&gt;​	vue 官方提供了两种快速创建工程化的 SPA 项目的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于 vite 创建 SPA 项目（仅支持 vue3.x，小而巧）&lt;/li&gt;
&lt;li&gt;基于 vue-cli 创建 SPA 项目（支持 vue3.x 和 vue2.x，大而全）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2、Vite 的基本使用&lt;/h3&gt;
&lt;p&gt;​	按照顺序执行如下的命令，即可基于 vite 创建 vue3.x 的工程化项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm init vite-app 项目名称
cd 项目名称
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（1）项目基本结构&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;public 是公共的静态资源目录&lt;/li&gt;
&lt;li&gt;src 是项目的源代码目录（程序员写的所有代码都要放在此目录下）
&lt;ul&gt;
&lt;li&gt;assets 目录用来存放项目中所有的静态资源文件（css、fonts等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;components 目录用来存放项目中所有的自定义组件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App.vue 是项目的根组件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;index.css 是项目的全局样式表文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;main.js 是整个项目的打包入口文件&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;index.html 是 SPA 单页面应用程序中唯一的 HTML 页面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220514233249819.Bxp79qKm_2i1WXP.webp&quot; alt=&quot;image-20220514233249819&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;（2）vite 项目运行流程&lt;/h4&gt;
&lt;p&gt;​	在工程化的项目中，vue 要做的事情很单纯：通过 main.js 把 App.vue 渲染到 index.html 的指定区域中。&lt;/p&gt;
&lt;p&gt;​	其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;App.vue 用来编写待渲染的模板结构&lt;/li&gt;
&lt;li&gt;index.html 中需要预留一个 el 区域&lt;/li&gt;
&lt;li&gt;main.js 把 App.vue 渲染到了 index.html 所预留的区域中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	main.js 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1.从 vue 中按需导入 createApp 函数，
// createApp 函数的作用：创建 vue 的“单页面应用程序实例”
import { createApp } from &apos;vue&apos;
// 2.导入待渲染的 App 组件
import App from &apos;./App.vue&apos;
// 3.调用 createApp 函数，返回值是“单页面应用程序的实例”，用常量 spa_app 进行接收，
// 同时把 App 组件作为参数传给 createApp 函数，表示要把 App 渲染到 index.html 页面上 
const spa_app = createApp(App)
// 4.调用spa_app 实例的 mount 方法，用来指定 vue 实际要控制的区域
spa_app.mount(&apos;#app&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、组件化开发&lt;/h3&gt;
&lt;p&gt;​	组件化开发指的是：根据封装的思想，把页面上可重用的部分封装为组件，从而方便项目的开发和维护。&lt;/p&gt;
&lt;p&gt;​	vue 是一个完全支持组件化开发的框架。vue 中规定组件的后缀名是 .vue。之前接触到的 App.vue 文件本质上就是一个 vue 的组件。&lt;/p&gt;
&lt;h4&gt;（1）vue 组件构成&lt;/h4&gt;
&lt;p&gt;​	每个 .vue 组件都由 3 部分构成，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;template -&amp;gt; 组件的模板结构&lt;/li&gt;
&lt;li&gt;script -&amp;gt; 组件的 JavaScript 行为&lt;/li&gt;
&lt;li&gt;style -&amp;gt; 组件的样式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	注：script 和 style 省略。&lt;/p&gt;
&lt;h4&gt;（2）组件 template 节点&lt;/h4&gt;
&lt;p&gt;​	vue 规定：每个组件对应的模板结构，需要定义到 &amp;lt;template&amp;gt; 节点中。它是 vue 提供的容器，只起到包裹的作用，不会被真正渲染为 DOM 元素。&lt;/p&gt;
&lt;p&gt;​	在 template 中，支持使用指令来渲染 DOM 结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;button @click=&quot;showInfo&quot;&amp;gt;按钮&amp;lt;/button&amp;gt;
    &amp;lt;span :title=&quot;new Date().toLocalTimeString()&quot;&amp;gt;只是一个 span 元素&amp;lt;/span&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：vue2.x 中 template 只支持单个根节点，但 vue3.x 支持多个根节点。&lt;/p&gt;
&lt;h4&gt;（3）组件 script 节点&lt;/h4&gt;
&lt;p&gt;​	vue规定：组件内的 &amp;lt;script&amp;gt; 节点是可选的，开发者可以在 &amp;lt;script&amp;gt; 节点中封装组件的 JavaScript 业务逻辑。结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
// 今后，组件相关的 data 数据、methods 方法等，
// 都需要定义到 export default 所导出的对象中。
export default{
    // name 属性指向的是当前组件的名称（建议每个首字母大写），可以在调试工具看到这个名称
    name: &apos;MyApp&apos;,
    // 渲染期间用到的数据，注意组件中一定要使用函数：
    data() {
        return {
            count: 0,
            username: &apos;MelodyEcho&apos;,
        }
    },
    // 绑定方法
    methods: {
        addCount() {
            this.count++
        }
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）组件 style 节点&lt;/h4&gt;
&lt;p&gt;​	vue 规定：组件内的 &amp;lt;style&amp;gt; 节点是可选的，开发者可以在 &amp;lt;style&amp;gt; 节点中编写样式美化当前组件的 UI 结构。&lt;/p&gt;
&lt;p&gt;​	其中 &amp;lt;style&amp;gt; 标签上的 lang=&quot;css&quot; 属性是可选的，它表示所使用的样式语言。默认只支持普通的 css 语法，可选值还有 less、scss 等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style lang=&quot;css&quot;&amp;gt;
    h1 {
        font-weight: normal;
    }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	使用 less 语法前需要先配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运行 &lt;code&gt;npm install less -D&lt;/code&gt; 命令安装依赖包，从而提供 less 语法的编译支持&lt;/li&gt;
&lt;li&gt;在 &amp;lt;style&amp;gt; 标签上添加 lang=&quot;less&quot; 属性，即可使用 less 语法编写组件的样式&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、组件使用&lt;/h3&gt;
&lt;p&gt;​	组件之间可以进行相互的引用。引用原则：先注册，再引用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220515093958145.DoT-u3tz_ZT43Xn.webp&quot; alt=&quot;image-20220515093958145 w-80%&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;（1）注册组件&lt;/h4&gt;
&lt;p&gt;​	全局注册和局部注册：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;被全局注册的组件，可以在全局任何一个组件内被使用&lt;/li&gt;
&lt;li&gt;局部注册的组件，只能在当前注册的组件范围内使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;全局注册&lt;/strong&gt;：（在 main.js 中，要在 mount 前注册）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 导入需要被全局注册的组件
import Swiper from &apos;./components/Swiper.vue&apos;
// 使用 app.component() 方法
app.component(&apos;my-swiper&apos;, Swiper)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 此时就可以在其他组件中这样导入使用：--&amp;gt;
&amp;lt;my-swiper&amp;gt;&amp;lt;/my-swiper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;局部注册&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;h1&amp;gt;组件注册测试 &amp;lt;/h1&amp;gt;
    &amp;lt;TextBox&amp;gt;&amp;lt;/TextBox&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
import TextBox from &apos;./components/textBox.vue&apos;
export default {
    // 通过该节点完成私有组件的注册
    components: {
        // 键值对完全一致也可直接省略键，只写值
        TextBox,
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;通过 name 属性注册组件&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.component(Swiper.name, Swiper)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）组件命名法&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;使用 kebab-case 命名法（俗称短横线命名法，例如 my-swiper 和 my-search ）&lt;/li&gt;
&lt;li&gt;使用 PascalCase 命名法（俗称帕斯卡命名法或大驼峰命名法，例如 MySwiper 和 MySearch ）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	注：短横线命名法必须严格按照短横线使用，但帕斯卡命名法可以兼容短横线写法使用。&lt;strong&gt;因此推荐使用帕斯卡命名法，这样适应性更强&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;（3）组件样式冲突问题&lt;/h4&gt;
&lt;p&gt;​	默认情况下，写在 .vue 组件中的样式会全局生效，因此很容易造成多个组件之间的样式冲突问题。这个问题的根本原因是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单页面应用程序中，所有组件的 DOM 结构，都是基于唯一的 index.html 页面进行呈现的&lt;/li&gt;
&lt;li&gt;每个组件中的样式，都会影响整个 index.html 页面中的 DOM 元素&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;解决方案&lt;/strong&gt;：（通过为每个组件所有元素分配自定义属性）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;div class=&quot;container&quot; data-v-001&amp;gt;
        &amp;lt;span data-v-001&amp;gt;&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;style&amp;gt;
    .container[data-v-001] {
        font-size: 20px;
    }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	但是这样太麻烦，因此 vue 提供了对应的自动化的方案：（为 style 添加 scoped 属性即可）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style scoped&amp;gt;
    /* 会自动分配唯一的“自定义属性” */
    .container {
        font-size: 18px;
    }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）:deep() 样式穿透&lt;/h4&gt;
&lt;p&gt;​	如果给当前组件的 style 节点添加了 scoped 属性，则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效，可以使用 :deep() 深度选择器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style scoped&amp;gt;
	/* 不加 /deep/ 时，生成格式为： .title[data-v-001] */
    .title {
    	color: blue;
    }
    /* 加了之后，生成格式为： [data-v-001] .title */
    :deep(title) {
        color: blue;
    }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、组件的 props&lt;/h3&gt;
&lt;p&gt;​	为了提高组件的复用性，在封装 vue 组件时需要遵守如下的原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组件的 DOM 结构、Style 样式要&lt;strong&gt;尽量复用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;组件中要展示的数据，&lt;strong&gt;尽量由组件的使用者提供&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	因此为了方便使用者为组件提供要展示的数据，vue 组件提供了 props 的概念。&lt;/p&gt;
&lt;h4&gt;（1）什么是组件的 props&lt;/h4&gt;
&lt;p&gt;​	props 是组件的自定义属性，组件的使用者可以通过 props 把数据传递到子组件内部，供子组件内部进行使用。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 通过两个自定义属性传递了数据 --&amp;gt;
&amp;lt;my-article title=&quot;面朝大海，春暖花开&quot; author=&quot;海子&quot;&amp;gt;&amp;lt;/my-article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：&lt;strong&gt;props 对于子组件是只读的&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;（2）在组件中声明 props&lt;/h4&gt;
&lt;p&gt;​	在封装 vue 组件时，可以把动态的数据项声明为 props 自定义属性。&lt;strong&gt;自定义属性可以在当前组件的模板结构中被直接使用&lt;/strong&gt;。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;span&amp;gt;标题：{{ title }}&amp;lt;/span&amp;gt;
	&amp;lt;span&amp;gt;作者：{{ author }}&amp;lt;/span&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
export default {
    // 父组件传递给该组件的数据，必须在 props 节点声明
    props: [&apos;title&apos;, &apos;author&apos;]
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）动态绑定 props 的值&lt;/h4&gt;
&lt;p&gt;​	可以使用 &lt;code&gt;v-bind&lt;/code&gt; 的形式动态指定值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;my-article :title=&quot;info.title&quot; :author=&quot;&apos;post by &apos; + info.author&quot;&amp;gt;&amp;lt;/my-article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）props 命名法&lt;/h4&gt;
&lt;p&gt;​	组件中如果使用  “camelCase（驼峰命名法）” 声明了 props 属性的名称，则有两种方式为其绑定属性的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 子组件中 --&amp;gt;
&amp;lt;script&amp;gt;
export default {
    props: [&apos;pubTime&apos;],		// 驼峰命名法
}
&amp;lt;/script&amp;gt;

&amp;lt;!-- 父组件中这两种写法都可以 --&amp;gt;
&amp;lt;my-article pubTime=&quot;1989&quot;&amp;gt;&amp;lt;/my-article&amp;gt;
&amp;lt;!-- 或 --&amp;gt;
&amp;lt;my-article pub-time=&quot;1989&quot;&amp;gt;&amp;lt;/my-article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;特别注意&lt;/strong&gt;：&lt;strong&gt;只是父组件绑定可以用两种方式，子组件中使用数据时不能更改名字！&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;6、动态样式操作&lt;/h3&gt;
&lt;h4&gt;（1）动态绑定 HTML 的 class&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h3 class=&quot;thin&quot; :class=&quot;isItalic? &apos;italic&apos; : &apos;&apos;&quot;&amp;gt;字体粗细切换组件&amp;lt;/h3&amp;gt;
&amp;lt;button @click=&quot;isItalic=!isItalic&quot;&amp;gt;切换字体样式&amp;lt;/button&amp;gt;
...
data() {
	return { isItalic: true }
}
...
&amp;lt;style scoped&amp;gt;
    .thin {
        font-weight: 200;
    }
    .italic {
        font-style: italic;
    }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	如果需要同时动态绑定多个 class 类名，可以使用数组的语法格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h3 class=&quot;thin&quot; :class=&quot;[isItalic? &apos;italic&apos;: &apos;&apos;, isBold ? &apos;bold&apos; : &apos;&apos;]&quot;&amp;gt;
    字体粗细切换组件
&amp;lt;/h3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	但这样会过于臃肿，可以使用对象语法绑定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 特别注意，此时 classObj 的键就是类名了，将会根据布尔值决定是否添加该类名 --&amp;gt;
&amp;lt;h3 class=&quot;thin&quot; :class=&quot;classObj&quot;&amp;gt;字体粗细切换组件&amp;lt;/h3&amp;gt;
&amp;lt;button @click=&quot;classObj.italic=!classObj.italic&quot;&amp;gt;切换字体样式&amp;lt;/button&amp;gt;
...
data() {
	return {
		classObj: {
			italic: true,
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）对象语法绑定内联 style&lt;/h4&gt;
&lt;p&gt;​	语法十分简洁，但注意样式名驼峰命名或使用字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div :style=&quot;{color: active, fontSize: fsize+&apos;px&apos;, &apos;background-color&apos;: bgColor}&quot;&amp;gt;测试文本&amp;lt;/div&amp;gt;
...
data() {
	return {
		active: &apos;red&apos;,
		fsize: 30,
		bgColor: &apos;pink&apos;,
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、组件基础（下）&lt;/h2&gt;
&lt;h3&gt;1、props 验证&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;props 验证&lt;/strong&gt;：在封装组件时对外界传递过来的 props 数据进行合法性的校验，从而防止数据不合法的问题。&lt;/p&gt;
&lt;p&gt;​	之前我们使用的是数组类型的 props 节点，但存在缺点：&lt;strong&gt;无法为每个prop指定具体的数据类型&lt;/strong&gt;。因此现在我们需要使用对象类型的 props 节点，它可以实现对数据类型的校验。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220515185626823.Bo0aXWoM_ZPfNIf.webp&quot; alt=&quot;image-20220515185626823 w-80%&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;状态：{{ state }}&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;数量：{{ count }}&amp;lt;/p&amp;gt;
...
props: {
    state: Boolean,
    count: Number,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（1）基础类型校验&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	props: {
        // 支持的类型如下：
        propA: String,
        propB: Number,
        PropC: Boolean,
        PropD: Array,
        propE: Object,
        propF: Date,
        propG: Function,
        propH: Symbol,
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）多个可能类型&lt;/h4&gt;
&lt;p&gt;​	如果 prop 属性值不唯一：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	props: {
        propA: [Number, String],
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）必填项校验&lt;/h4&gt;
&lt;p&gt;​	如果某个 prop 属性是必填项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	props: {
        propB: {
            type: String,
            required: true,
        }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）属性默认值&lt;/h4&gt;
&lt;p&gt;​	在封装组件时，可以为某个 prop 属性指定默认值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	props: {
        propB: {
            type: Number,
            default: 100
        }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;（5）自定义验证函数&lt;/h5&gt;
&lt;p&gt;​	在封装组件时，可以为 prop 属性指定自定义的验证函数，从而对 prop 属性的值进行更加精确的控制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	props: {
        propB: {
            // 利用 validator 函数
            validator(value) {
                // true 验证成功，false 验证失败
                return [&apos;success&apos;, &apos;warning&apos;, &apos;danger&apos;].indexOf(value) !== -1
            }
        }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、计算属性&lt;/h3&gt;
&lt;p&gt;​	计算属性本质上就是一个 function 函数，它可以实时监听 data 中数据的变化，并 return 一个计算后的新值，供组件渲染 DOM 时使用。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;计算属性需要以 function 函数的形式（且只能是 function 函数形式）声明到组件的 computed 选项中，示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;text&quot; v-model.number=&quot;count&quot; /&amp;gt;
&amp;lt;p&amp;gt; {{ count }} 乘以 2 的值为：{{ plus }}&amp;lt;/p&amp;gt;
...
computed: {
	plus() {
		return this.count * 2
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：计算属性侧重于得到一个计算的结果，因此计算属性中必须&lt;strong&gt;有 return 返回值&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;​	相对于方法来说，&lt;strong&gt;计算属性会缓存计算的结果&lt;/strong&gt;，只有计算属性的依赖项发生变化时，才会重新进行运算。因此计算属性的性能更好。&lt;/p&gt;
&lt;h3&gt;3、自定义事件&lt;/h3&gt;
&lt;p&gt;​	在封装组件时，为了让&lt;strong&gt;组件的使用者&lt;/strong&gt;可以监听到&lt;strong&gt;组件内状态的变化&lt;/strong&gt;，此时需要用到组件的自定义事件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220515194050837.DPwxqBVG_ZGzh2.webp&quot; alt=&quot;image-20220515194050837 w-80%&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;（1）使用&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;声明自定义事件&lt;/strong&gt;：开发者为自定义组件封装的自定义事件，必须事先在 emits 节点中声明，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
&amp;lt;script&amp;gt;
export default {
    // 组件声明的自定义事件，必须先定义在 emits 节点中
    emits: [&apos;change&apos;],
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;触发自定义事件&lt;/strong&gt;：在 emits 节点下声明的自定义事件，可以通过 &lt;code&gt;this.$emit(自定义事件名称)&lt;/code&gt; 方法进行触发，示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
&amp;lt;script&amp;gt;
export default {
    // 组件声明的自定义事件，必须先定义在 emits 节点中
    emits: [&apos;change&apos;],
    methods: {
        onBtnClick() {
            // 当点击了按钮，调用该方法，触发事件
            this.$emit(&apos;change&apos;)
        }
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;监听自定义事件&lt;/strong&gt;：在使用自定义的组件时，可以通过 &lt;code&gt;v-on&lt;/code&gt; 的形式监听自定义事件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;my-counter @change=&quot;getCount&quot;&amp;gt;&amp;lt;/my-counter&amp;gt;
...
methods: {
	getCount() {
		console.log(&apos;监听到了 count 值的变化&apos;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）传参&lt;/h4&gt;
&lt;p&gt;​	在调用 &lt;code&gt;this.$emit(自定义事件名称)&lt;/code&gt; 方法触发自定义事件时，可以通过第 2 个参数为自定义事件传参，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 子组件 script:
this.$emit(&apos;change&apos;, this.count)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 父组件 --&amp;gt;
&amp;lt;my-counter @change=&quot;getCount&quot;&amp;gt;&amp;lt;/my-counter&amp;gt;
...
methods: {
	getCount(num) {
		console.log(&apos;监听到了 count 值的变化&apos;, num)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、组件上的 v-model&lt;/h3&gt;
&lt;p&gt;​	v-model 是双向数据绑定指令，除表单元素数据绑定外，当需要维护组件内外数据的同步时，&lt;strong&gt;可以在组件上使用 v-model 指令。如使用 v-model 实现父子的双向数据绑定&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220515200000392.Bfp94YJY_ZGOgw1.webp&quot; alt=&quot;image-20220515200000392 w-80%&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;（2）具体实现&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;父组件向子组件传递数据&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;父组件通过 &lt;code&gt;v-bind&lt;/code&gt; 属性绑定的形式，把数据传递给子组件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;子组件中通过 props 接收父组件传递过来的数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220515201100324.C1DwiQ3H_Z2hlR9H.webp&quot; alt=&quot;image-20220515201100324&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;子组件向父组件传递数据&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;v-bind&lt;/code&gt; 指令之前添加 &lt;code&gt;v-model&lt;/code&gt; 指令&lt;/li&gt;
&lt;li&gt;在子组件中声明 &lt;code&gt;emit&lt;/code&gt; 自定义事件，格式为 &lt;code&gt;update:xxx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;$emit()&lt;/code&gt; 触发自定义事件，更新父组件中的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220515201253500.Db5-wvBE_1TYde7.webp&quot; alt=&quot;image-20220515201253500&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	第二部分代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;p&amp;gt;子组件计数：{{ number }}&amp;lt;/p&amp;gt;
  &amp;lt;button @click=&quot;add&quot;&amp;gt;+1&amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
  ...
  props: {
    number: Number,
  },
  emits: [&apos;update:number&apos;],
  methods: {
    add() {
      // 这里不能给 this.number 加 1，事件传递给父组件后，由父组件接受并设置加 1 后的值，然后再将值传递给子组件（因为 props 是只读的）
      this.$emit(&apos;update:number&apos;, this.number + 1)
    }
  },
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、组件高级（上）&lt;/h2&gt;
&lt;h3&gt;1、watch 监听器&lt;/h3&gt;
&lt;h4&gt;（1）基础&lt;/h4&gt;
&lt;p&gt;​	watch 侦听器允许开发者监视数据的变化，从而针对数据的变化做特定的操作。例如，监视用户名的变化并发起请求，判断用户名是否可用。&lt;/p&gt;
&lt;p&gt;​	基本语法，在 watch 节点下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;input type=&quot;text&quot; class=&quot;form-control&quot; v-model.trim=&quot;username&quot;&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
export default {
	data() {
        return {
            username: &apos;&apos;,
        }
    },
	watch: {
		async username(newVal, oldVal) {
             // 解构 axios 响应对象
			const {data:res} = await axios.get(&apos;https://localhost/api/user&apos;);
             console.log(res);
		}
	}
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）immediate 选项&lt;/h4&gt;
&lt;p&gt;​	默认情况下，组件在初次加载完毕后不会调用  watch 侦听器。如果想让 watch 侦听器立即被调用，则需要使用 immediate 选项。实例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
watch: {
    username: {
         // 注意 handler 属性是默认写法
         async handler(newVal, oldVal) {
             const {data:res} = await axios.get(`https://localhost/api/user/${newVal}`);
             console.log(res);
         },
         // 组件加载完毕后立即调用当前 watch 监听器
         immediate: true,
    },
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）deep 选项&lt;/h4&gt;
&lt;p&gt;​	当 watch 侦听的是一个对象，如果&lt;strong&gt;对象中的属性值发生了变化，则无法被监听到&lt;/strong&gt;。此时需要使用 deep 选项，代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
watch: {
    info: {
         // 注意 handler 属性是默认写法
         async handler(newVal, oldVal) {
             const {data:res} = await axios.get(`https://localhost/api/user/${newVal.username}`);
             console.log(res);
         },
         // 使用 deep 选项
         deep: true,
    },
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）监听对象单个属性&lt;/h4&gt;
&lt;p&gt;​	代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
watch: {
    // 只监听 info.username
    &apos;info.username&apos;: {
         async handler(newVal) {
             const {data:res} = await axios.get(`https://localhost/api/user/${newVal}`);
             console.log(res);
         },
    },
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（5）计算属性和监听器&lt;/h4&gt;
&lt;p&gt;​	计算属性和侦听器侧重的应用场景不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算属性侧重于监听多个值的变化，最终计算并&lt;strong&gt;返回一个新值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;侦听器侧重于监听单个数据的变化，最终执行特定的业务处理，&lt;strong&gt;不需要有任何返回值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2、组件生命周期&lt;/h3&gt;
&lt;h4&gt;（1）组件运行过程&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220712112816010.snmC0q77_2jLpOW.webp&quot; alt=&quot;image-20220712112816010&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	组件的生命周期指的是：组件从创建-&amp;gt;运行（渲染）-&amp;gt;销毁的整个过程，强调的是一个时间段。&lt;/p&gt;
&lt;h4&gt;（2）监听组件的不同时刻&lt;/h4&gt;
&lt;p&gt;​	vue 框架为组件内置了不同时刻的生命周期函数，生命周期函数会&lt;strong&gt;伴随着组件的运行而自动调用&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当组件在内存中被创建完毕之后，会自动调用 created 函数
&lt;ul&gt;
&lt;li&gt;适用于发 ajax 初次请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当组件第一次被成功地渲染到页面上之后，会自动调用 mounted 函数
&lt;ul&gt;
&lt;li&gt;适用于最早操作 dom 元素（此时刚好渲染完成）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当组件被销毁完毕之后，会自动调用 unmounted 函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default {
    ...
    created() {...},
    mounted() {...},
    unmounted() {...},
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）监听组件的更新&lt;/h4&gt;
&lt;p&gt;​	当组件的 data 数据更新之后，vue 会自动重新渲染组件的 DOM 结构，从而保证 View 视图展示的数据和 Model 数据源保持一致。
​	当组件被&lt;strong&gt;重新渲染完毕&lt;/strong&gt;之后，会自动调用 updated 生命周期函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default {
    ...
    updated() {...},
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）全部生命周期函数&lt;/h4&gt;
&lt;p&gt;​	beforeCreate、created、&lt;/p&gt;
&lt;p&gt;​	beforeMount、mounted、&lt;/p&gt;
&lt;p&gt;​	beforeUpdate、updated、&lt;/p&gt;
&lt;p&gt;​	beforeUnmount、Unmounted&lt;/p&gt;
&lt;p&gt;​	注：不在 beforeCreate 中发起 ajax 请求是因为此时组件还没创建好，请求到的数据是无法挂载到组件上的。&lt;/p&gt;
&lt;h3&gt;3、组件间的数据共享&lt;/h3&gt;
&lt;h4&gt;（1）组件关系&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;父子关系&lt;/li&gt;
&lt;li&gt;兄弟关系&lt;/li&gt;
&lt;li&gt;后代关系&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（2）父子数据共享&lt;/h4&gt;
&lt;p&gt;​	父-&amp;gt;子：参见笔记前面部分&lt;/p&gt;
&lt;p&gt;​	子-&amp;gt;父：参见笔记前面部分&lt;/p&gt;
&lt;p&gt;​	子父数据同步：参见笔记前面部分&lt;/p&gt;
&lt;h4&gt;（3）兄弟间的数据共享&lt;/h4&gt;
&lt;p&gt;​	兄弟组件之间实现数据共享的方案是 EventBus。可以借助于第三方的包 mitt 来创建 eventBus 对象，从而实现兄弟组件之间的数据共享。示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220712115347627.CNSlQdS9_Z1BVHtj.webp&quot; alt=&quot;image-20220712115347627&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i mitt@2.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	创建 eventBus.js：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import mitt from &apos;mitt&apos;
const bus = mitt()
export default bus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	在数据接收方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import bus from &apos;../eventBus.js&apos;

export default {
    ...
    created() {
    	bus.on(&apos;countChange&apos;, count =&amp;gt; {
        	this.count = count;
    	})
	}
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	在数据发送方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import bus from &apos;../eventBus.js&apos;
    
export default {
    ...
    methods: {
        add() {
            this.count++;
            bus.emit(&apos;countChange&apos;, this.count);
        }
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）后代关系组件的数据共享&lt;/h4&gt;
&lt;h5&gt;a. 非响应数据共享&lt;/h5&gt;
&lt;p&gt;​	后代关系组件之间共享数据，指的是&lt;strong&gt;父节点的组件向其子孙组件共享数据&lt;/strong&gt;。此时组件之间的嵌套关系比较复杂，可以使用 provide 和 inject 实现后代关系组件之间的数据共享。&lt;/p&gt;
&lt;p&gt;​	父节点通过 provide 共享数据 color：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default {
    ...
    provide() {
        return {
            color: this.color,
        }
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	子孙节点使用 Inject 数组接收数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;span&amp;gt;{{ color }}&amp;lt;/span&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
export default {
    ...
    inject: [&apos;color&apos;],
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;b. 响应数据共享&lt;/h5&gt;
&lt;p&gt;​	思路：结合 computed 函数向下共享：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
import { computed } from &apos;vue&apos;
    
export default {
    ...,
    provide() {
        return {
            color: computed(() =&amp;gt; this.color);
        }
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;span&amp;gt;{{ color.value }}&amp;lt;/span&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）全局数据共享 vuex&lt;/h4&gt;
&lt;p&gt;​	vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中，vuex 可以让组件之间的数据共享变得高效、清晰、且易于维护。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220712121504720.Dfmw9DzN_MNfBR.webp&quot; alt=&quot;image-20220712121504720&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	此处暂不提及，后文有专门的章节记录。&lt;/p&gt;
&lt;h3&gt;4、vue3.x 全局配置 axios&lt;/h3&gt;
&lt;p&gt;​	在实际项目开发中，几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个组件中都需要导入 axios（代码臃肿）&lt;/li&gt;
&lt;li&gt;每次发请求都需要填写完整的请求路径（不利于后期的维护）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	main.js 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 配置根路径
axios.defaults.baseURL = &apos;https://example.com/api&apos;;
// 挂载为 app 全局自定义属性
app.config.globalProperties.$axios = axios;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	任意组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const {data:usersRes} = await this.$axios.get(&apos;/users&apos;);
const {data:newsRes} = await this.$axios.post(&apos;/news&apos;, {type: &apos;tech&apos;});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、组件高级（下）&lt;/h2&gt;
&lt;h3&gt;1、ref 引用和 $nextTick 函数&lt;/h3&gt;
&lt;p&gt;​	ref 用来辅助开发者在不依赖于 jQuery 的情况下，获取 DOM 元素或组件的引用。&lt;/p&gt;
&lt;p&gt;​	每个 vue 的组件实例上，都包含一个 $refs 对象，里面存储着对应的 DOM 元素或组件的引用。默认情况下，组件的 $refs 指向一个空对象。&lt;/p&gt;
&lt;p&gt;​	使用 DOM 或 组件的 ref 引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;span ref=&quot;spanRef&quot;&amp;gt;&amp;lt;/span&amp;gt;
	&amp;lt;my-comp ref=&quot;compRef&quot;&amp;gt;&amp;lt;/my-comp&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
    ...
	getRef() {
        console.log(this.$refs.spanRef);
        console.log(this.$refs.compRef);
        // 引用到组件的实例后，可以直接调用其上的 methods 方法
        this.$refs.compRef.action();
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注意：由于组件是异步执行 DOM 更新的，所以操作 ref 引用时要注意等到 DOM 元素渲染后再获取。&lt;/p&gt;
&lt;p&gt;​	可以使用 $nextTick() 函数解决这个问题。组件的 $nextTick(cb) 方法，会把 cb 回调推迟到下一个 DOM 更新周期之后执行。通俗的理解是：等组件的 DOM 异步地重新渲染完成后，再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
...
showInput() {
    this.inputVisible = true;
    this.$nextTick(() =&amp;gt; {
        // 更新后再获取 DOM 元素
        this.$refs.ipt.focus();
    })
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、动态组件&lt;/h3&gt;
&lt;p&gt;​	动态组件指的是动态切换组件的显示与隐藏。vue 提供了一个内置的 &amp;lt;component&amp;gt; 组件，专门用来实现组件的动态渲染。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;component&amp;gt; 是组件的占位符&lt;/li&gt;
&lt;li&gt;通过 is 属性动态指定要渲染的组件名称&lt;/li&gt;
&lt;li&gt;&amp;lt;companent is=&quot;要渲染的组件的名称&quot;&amp;gt;&amp;lt;/component&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;component :is=&quot;compName&quot;&amp;gt;&amp;lt;/component&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
...
data() {
    return {
        compName: &apos;HomeComp&apos;,
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：默认情况下，切换动态组件时无法保持组件的状态。此时可以使用vue内置的 &amp;lt;keep-alive&amp;gt; 组件保持动态组件的状态。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;keep-alive&amp;gt;
    	&amp;lt;component :is=&quot;compName&quot;&amp;gt;&amp;lt;/component&amp;gt;
    &amp;lt;/keep-alive&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、插槽&lt;/h3&gt;
&lt;p&gt;​	插槽（Slot）是 vue 为组件的封装者提供的能力。允许开发者在封装组件时，把不确定的、希望由用户指定的部分定义为插槽。如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220712220758669.CNKQmitu_14ypDz.webp&quot; alt=&quot;image-20220712220758669&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	可以把插槽认为是组件封装期间，为用户预留的内容的占位符。&lt;/p&gt;
&lt;h4&gt;（1）插槽基本用法&lt;/h4&gt;
&lt;p&gt;​	在封装组件时，可以通过 &amp;lt;slot&amp;gt; 元素定义插槽，从而为用户预留内容占位符。示例代码如下：&lt;/p&gt;
&lt;p&gt;​	组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;p&amp;gt;这是前一个 p 标签&amp;lt;/p&amp;gt;
	&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
	&amp;lt;p&amp;gt;这是后一个 p 标签&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	使用组件时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;my-comp&amp;gt;
    	&amp;lt;p&amp;gt;这是自定义的内容&amp;lt;/p&amp;gt;
    &amp;lt;/my-comp&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：如果封装时没有预留任何的 slot 插槽，则任何提供的自定义内容都会被丢弃&lt;/p&gt;
&lt;h4&gt;（2）插槽默认内容&lt;/h4&gt;
&lt;p&gt;​	封装组件时，可以为预留的 &amp;lt;slot&amp;gt; 插槽提供后备内容（默认内容）。如果组件的使用者没有为插槽提供任何内容，则后备内容会生效。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;p&amp;gt;这是前一个 p 标签&amp;lt;/p&amp;gt;
	&amp;lt;slot&amp;gt;这是备用默认内容&amp;lt;/slot&amp;gt;
	&amp;lt;p&amp;gt;这是后一个 p 标签&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）具名插槽&lt;/h4&gt;
&lt;p&gt;​	如果在封装组件时需要预留多个插槽节点，则需要为每个 &amp;lt;slot&amp;gt; 插槽指定具体的 name 名称。这种带有具体名称的插槽叫做“具名插槽”。示例代码如下：&lt;/p&gt;
&lt;p&gt;​	组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;header&amp;gt;
    	&amp;lt;slot name=&quot;header&quot;&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/header&amp;gt;
    &amp;lt;main&amp;gt;
    	&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/main&amp;gt;
    &amp;lt;footer&amp;gt;
    	&amp;lt;slot name=&quot;footer&quot;&amp;gt;&amp;lt;/slot&amp;gt;
    &amp;lt;/footer&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：没有指定 name 名称的插槽，会有隐含的名称叫做“default&quot;。&lt;/p&gt;
&lt;p&gt;​	使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;my-comp&amp;gt;
	&amp;lt;template v-slot:header&amp;gt;
    	&amp;lt;h1&amp;gt;诗歌标题&amp;lt;/h1&amp;gt;
    &amp;lt;/template&amp;gt;
    &amp;lt;template v-slot:default&amp;gt;
    	&amp;lt;h1&amp;gt;诗歌内容&amp;lt;/h1&amp;gt;
    &amp;lt;/template&amp;gt;
    &amp;lt;template v-slot:footer&amp;gt;
    	&amp;lt;h1&amp;gt;诗歌作者&amp;lt;/h1&amp;gt;
    &amp;lt;/template&amp;gt;
&amp;lt;/my-comp&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	实际上，就是说，只有默认插槽在使用时可以省略 &amp;lt;template&amp;gt; 标签。但具名插槽也有其简写形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;my-comp&amp;gt;
	&amp;lt;template #header&amp;gt;
    	&amp;lt;h1&amp;gt;诗歌标题&amp;lt;/h1&amp;gt;
    &amp;lt;/template&amp;gt;
    ...
&amp;lt;/my-comp&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）作用域插槽&lt;/h4&gt;
&lt;p&gt;​	在封装组件的过程中，可以为预留的 &amp;lt;slot&amp;gt; 插槽绑定数据，这种带有数据的 &amp;lt;slot&amp;gt; 叫做“作用域插槽”。示例代码如下：&lt;/p&gt;
&lt;p&gt;​	组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
    &amp;lt;h3&amp;gt;这是 TEST 组件&amp;lt;/h3&amp;gt;
    &amp;lt;slot :info=&quot;information&quot; :msg=&quot;message&quot;&amp;gt;&amp;lt;/slot&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	使用：（接受插槽给的数据）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;my-test&amp;gt;
   &amp;lt;template v-slot:default=&quot;scope&quot;&amp;gt;
   	info: {{ scope.info }}
   	msg: {{ scope.msg }}
   &amp;lt;/template&amp;gt;
&amp;lt;/my-test&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	或解构使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;my-test&amp;gt;
	&amp;lt;template v-slot:default=&quot;{ msg, info }&quot;&amp;gt;
    	info: {{ msg }}
		msg: {{ info }}
    &amp;lt;/template&amp;gt;
&amp;lt;/my-test&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、自定义指令&lt;/h3&gt;
&lt;h4&gt;（1）私有自定义指令&lt;/h4&gt;
&lt;p&gt;​	vue 官方提供了 v-for、v-model、v-if 等常用的内置指令。除此之外 vue 还允许开发者自定义指令。&lt;/p&gt;
&lt;p&gt;​	vue 中的自定义指令分为两类，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;私有自定义指令&lt;/li&gt;
&lt;li&gt;全局自定义指令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	在每个 vue 组件中，可以在 directives 节点下声明私有自定义指令。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;input type=&quot;text&quot; class=&quot;form-control&quot; v-focus&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
...
directive: {
    // 自定义一个私有指令
    focus: {
        // el 指向绑定的元素。当被绑定的元素插入到 DOM 中时，自动触发 mounted 函数
        mounted(el) {
            el.focus();	// 让元素自动获得焦点
        }
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）全局自定义指令&lt;/h4&gt;
&lt;p&gt;​	main.js 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const app = Vue.createApp()

app.directive(&apos;focus&apos;, {
    mounted(el) {
        el.focus();
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）updaeted 函数&lt;/h4&gt;
&lt;p&gt;​	mounted 函数只在元素第一次插入 DOM 时被调用，当 DOM 更新时 mounted 函数不会被触发。updated 函数会在每次 DOM 更新完成后被调用。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.directive(&apos;focus&apos;, {
    mounted(el) {
        el.focus();
    },
    updated(el) {
        el.focus();
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：如果 mounted 和 update 中的逻辑完全相同，可以简写为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.directive(&apos;focus&apos;, el =&amp;gt; {
    el.focus();
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（4）指令的参数值&lt;/h4&gt;
&lt;p&gt;​	在绑定指令时，可以通过“等号”的形式为指令绑定具体的参数值，示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;input type=&quot;text&quot; v-color=&quot;&apos;red&apos;&quot;&amp;gt;
    &amp;lt;p v-color=&quot;&apos;cyan&apos;&quot;&amp;gt;一段文本&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	main.js 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.directive(&apos;color&apos;, (el, binding) =&amp;gt; {
    el.style.color = binding.value;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、路由&lt;/h2&gt;
&lt;h3&gt;1、前端路由概念与原理&lt;/h3&gt;
&lt;p&gt;​	路由（英文：router）就是对应关系。路由分为两大类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后端路由&lt;/li&gt;
&lt;li&gt;前端路由&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	后端路由：指的是请求方式、请求地址与 function 处理函数之间的对应关系。&lt;/p&gt;
&lt;p&gt;​	前端路由：通俗易懂的概念：Hash 地址与组件之间的对应关系。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://xxx.com/spa#/index
https://xxx.com/spa#/info
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（1）SPA 与前端路由&lt;/h4&gt;
&lt;p&gt;​	SPA 指的是一个 web 网站只有唯一的一个 HTML 页面，所有组件的展示与切换都在这唯一的一个页面内完成。
​	此时，不同组件之间的切换需要通过前端路由来实现。
​	结论：在 SPA 项目中，不同功能之间的切换，要依赖于前端路由来完成！&lt;/p&gt;
&lt;h4&gt;（2）前端路由的工作方式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;用户点击了页面上的路由链接&lt;/li&gt;
&lt;li&gt;导致了 URL 地址栏中的 Hash 值发生了变化&lt;/li&gt;
&lt;li&gt;前端路由监听了到 Hash 地址的变化&lt;/li&gt;
&lt;li&gt;前端路由把当前 Hash 地址对应的组件渲染都浏览器中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220714122627028.ClSJfUfS_ZFIOOK.webp&quot; alt=&quot;image-20220714122627028&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	结论：前端路由，指的是 Hash 地址与组件之间的对应关系！&lt;/p&gt;
&lt;p&gt;​	注：我们可以手动实现简单的前端路由，但实际开发中我们一般使用一些集成的路由解决方案。&lt;/p&gt;
&lt;h3&gt;2、vue-router4.x 基本用法&lt;/h3&gt;
&lt;p&gt;​	vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用，能够轻松的管理 SPA 项目中组件的切换。vue-router 3.x 只能结合 vue2 进行使用 vue-router 4.x 只能结合 vue3 进行使用。&lt;/p&gt;
&lt;h4&gt;（1）安装&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;npm i vue-router@4.0.3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）声明路由连接和占位符&lt;/h4&gt;
&lt;p&gt;​	可以使用 &amp;lt;router-link&amp;gt; 标签来声明路由链接，并使用 &amp;lt;router-view&amp;gt; 标签来声明路由占位符。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;h1&amp;gt;App 根组件&amp;lt;/h1&amp;gt;
	&amp;lt;!-- 路由连接 --&amp;gt;
	&amp;lt;router-link to=&quot;/home&quot;&amp;gt;首页&amp;lt;/router-link&amp;gt;
	&amp;lt;router-link to=&quot;/detail&quot;&amp;gt;详情页&amp;lt;/router-link&amp;gt;
	&amp;lt;!-- 路由组件占位符 --&amp;gt;
	&amp;lt;router-view&amp;gt;&amp;lt;/router-view&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）创建 router.js 并挂载&lt;/h4&gt;
&lt;p&gt;​	router.js：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// createRouter 方法用于创建路由的实例对象
// createwebHashHistory 用于指定路由的工作模式（hash模式）
import { createRouter，createWebHashHistory } from &apos;vue-router&apos;
import Home from &apos;./component/Home.vue&apos;
import Movie from &apos;./component/Movie.vue&apos;
import About from &apos;./component/About.vue&apos;

// 创建路由实例对象
const router = createRouter({
// history 属性指定路由的工作模式
    history: createWebHistory(),
// routes 数组，指定路由规则
    routes: [
// path 是 hash 地址，component 是要展示的组件
		{ path:&apos;/home&apos;, component:Home },
		{ path:&apos;/movie&apos;, component:Movie },
		{ path:&apos;/about&apos;, component:About },
	],
})

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	main.js 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import router from &apos;./router.js&apos;
app.use(router);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、路由重定向&lt;/h3&gt;
&lt;p&gt;​	路由重定向指的是：用户在访问地址 A 的时候，强制用户跳转到地址 C，从而展示特定的组件页面。通过路由规则的 redirect 属性，指定一个新的路由地址，可以很方便地设置路由的重定向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const router = createRouter({
    history: createWebHistory(),
    routes: [
		{ path:&apos;/&apos;, redirect:&apos;/home&apos; },
		{ path:&apos;/home&apos;, component:Home },
		{ path:&apos;/movie&apos;, component:Movie },
		{ path:&apos;/about&apos;, component:About },
	],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、路由高亮&lt;/h3&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写 &lt;code&gt;.router-link-active&lt;/code&gt; 类的样式&lt;/li&gt;
&lt;li&gt;通过 linkActiveClass 属性自定义 路由高亮的 class 类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const router = createRouter({
    ...
    // 默认的类样式会被覆盖
    linkActiveClass: &apos;router-active&apos;,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、嵌套路由&lt;/h3&gt;
&lt;p&gt;​	通过路由实现组件的嵌套展示，叫做嵌套路由。&lt;/p&gt;
&lt;p&gt;​	方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明子路由链接和子路由占位符&lt;/li&gt;
&lt;li&gt;在父路由规则中，通过 children 属性嵌套声明子路由规则&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	在组件中声明子路由链接和占位符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;router-link to=&quot;/about/tab1&quot;&amp;gt;tab1&amp;lt;/router-link&amp;gt;
	&amp;lt;router-link to=&quot;/about/tab2&quot;&amp;gt;tab2&amp;lt;/router-link&amp;gt;
	&amp;lt;router-view&amp;gt;&amp;lt;/router-view&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	在 router.js 中导入子组件，然后使用 children 属性声明子路由规则。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Tab1 from &apos;./component/tabs/Tab1.vue&apos;
import Tab2 from &apos;./component/tabs/Tab2.vue&apos;

const router = create Router({
    ...
    routes: [
        {// about 页面的路由规则
            path: &apos;/about&apos;,
            component: About,
            children: [// 通过 children 属性子级嵌套路由规则，注意这里不要加&apos;/&apos;
                { path: &apos;tab1&apos;, component: Tab1 },
                { path: &apos;tab2&apos;, component: Tab2 },
            ]
        },
        { path:&apos;/home&apos;, component:Home },
        ...
    ]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	此时的路由重定向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
{// about 页面的路由规则
	path: &apos;/about&apos;,
    component: About,
    redirect: &apos;/about/tab1&apos;,
    children: [
    	{ path: &apos;tab1&apos;, component: Tab1 },
    	{ path: &apos;tab2&apos;, component: Tab2 },
	]
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6、动态路由&lt;/h3&gt;
&lt;p&gt;​	动态路由指的是：把 Hash 地址中可变的部分定义为参数项，从而提高路由规则的复用性。在 vue-router 中使用英文的冒号 : 来定义路由的参数项。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ path: &apos;/movie/:id&apos;, component: Movie },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	通过 $route.params 获取动态路由的参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;h3&amp;gt;参数：{{ $route.params.id }}&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	同时，为了简化路由参数的获取形式，vue-router 允许在路由规则中开启 props 传参：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ path: &apos;/movie/:id&apos;, component: Movie, props: true },
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
	&amp;lt;h3&amp;gt;参数：{{ id }}&amp;lt;/h3&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
export default {
    ...
    props: [&apos;id&apos;],
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	关于参数传递，更详细的说明，见于：https://blog.csdn.net/hyk521/article/details/105479333/&lt;/p&gt;
&lt;h3&gt;7、编程式导航&lt;/h3&gt;
&lt;p&gt;​	通过调用 API 实现导航的方式，叫做编程式导航。与之对应的，通过点击链接实现导航的方式，叫做声明式导航。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通网页中点击 &amp;lt;a&amp;gt; 链接、vue 项目中点击 &amp;lt;router-link&amp;gt;都属于声明式导航&lt;/li&gt;
&lt;li&gt;普通网页中调用 location.href 跳转到新页面的方式，属于编程式导航&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	vue-router 提供的编程式导航的 api 有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this.$router.push(&apos;/movie/3&apos;)&lt;/li&gt;
&lt;li&gt;this.$router.go(-1)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8、命名路由&lt;/h3&gt;
&lt;p&gt;​	通过 name 属性为路由规则定义名称的方式，叫做命名路由。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	path: &apos;/movie/:id&apos;,
	name: &apos;mov&apos;,
	component: Movie,
	props: true,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：命名路由的 name 不能重复。&lt;/p&gt;
&lt;p&gt;​	作用：&lt;/p&gt;
&lt;p&gt;​	实现声明式导航：为 &amp;lt;router-link&amp;gt; 标签动态绑定 to 属性的值，并通过 name 属性指定要跳转到的路由规则。期间还可以用 params 属性指定跳转期间要携带的路由参数。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;router-link :to=&quot;{ name: &apos;mov&apos;, params: { id: 2 } }&quot;&amp;gt;&amp;lt;/router-link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	实现编程式导航：调用 push 函数期间指定一个配置对象，name 是要跳转到的路由规则、params 是携带的路由参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.$router.push({ name: &apos;mov&apos;, params: { id: 2 } })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9、导航守卫&lt;/h3&gt;
&lt;p&gt;​	导航守卫可以控制路由的访问权限。示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220714170020958.DJFQILsC_Z15USx8.webp&quot; alt=&quot;image-20220714170020958&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	全局导航守卫会拦截每个路由规则，从而对每个路由进行访问权限的控制。可以按照如下的方式定义全局导航守卫：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const router = createRouter({...,});

// 调用路由实例对象的 beforeEach 函数，声明”全局前置守卫&quot;
// fn 必须是一个函数，每次拦截到路由的请求，都会调用 fn 进行处理
// 因此fn叫做”守卫方法&quot;
router.beforeEach(fn);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（1）守卫方法的 3 个形参&lt;/h4&gt;
&lt;p&gt;​	全局导航守卫的守卫方法中接收 3 个形参，格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;router.beforeEach((to, from, next) =&amp;gt; {
    // to 目标路由对象，from 当前导航正要离开的路由对象，next 为表示放行的函数
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不声明 next 形参，则默认允许用户访问每一个路由！&lt;/li&gt;
&lt;li&gt;在守卫方法中如果声明了 next 形参，&lt;strong&gt;则必须调用 next() 函数&lt;/strong&gt;，否则不允许用户访问任何一个路由！&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	next 函数的三种调用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;next()				// 直接放行
next(false)			// 强制停留在当前页面
next(&apos;/login&apos;)		// 强制跳转到指定页面
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）结合 token 控制后台主页的访问权限&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;router.beforeEach((to, from, next) =&amp;gt; {
    // 这里只是演示，实际使用时要做 token 值的校验
    const token = localStorage.getItem(&apos;token&apos;);
    if (to.path === &apos;/main&apos; &amp;amp;&amp;amp; !token) {
        next(&apos;/login&apos;);
    }
    else {
        next();
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、综合案例&lt;/h2&gt;
&lt;h3&gt;1、vue-cli&lt;/h3&gt;
&lt;p&gt;​	vue-cli（俗称：vue 脚手架）是 vue 官方提供的、快速生成 vue工程化项目的工具。&lt;/p&gt;
&lt;p&gt;​	特点：开箱即用、基于 webpack。&lt;/p&gt;
&lt;p&gt;​	注：在 vue-cli 中 &apos;@&apos; 代表项目 src 路径。&lt;/p&gt;
&lt;h4&gt;（1）创建项目&lt;/h4&gt;
&lt;p&gt;​	命令行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vue create ProjectName
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	vue ui 创建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vue ui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;vue ui 创建项目时建议手动选择配置，勾选以下配置项：choose vue version、Babel、CSS 预处理器、使用配置文件（新手不建议开启 Linter/Formatter）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2、vue 组件库&lt;/h3&gt;
&lt;p&gt;​	在实际开发中，前端开发者可以把自己封装的 .vue 组件整理、打包、并发布为 npm 的包，从而供其他人下载和使用。这种可以直接下载并在项目中使用的现成组件，就叫做 vue 组件库。&lt;/p&gt;
&lt;p&gt;​	这里以 element ui 为例。&lt;/p&gt;
&lt;h4&gt;（1）element ui 引入&lt;/h4&gt;
&lt;p&gt;​	分为全局引入和按需引入，参照官方文档。&lt;/p&gt;
&lt;h3&gt;3、Proxy 跨域代理&lt;/h3&gt;
&lt;p&gt;​	通过 vue-cli 创建的项目在遇到接口跨域问题时，可以通过代理的方式来解决：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220726213158665.B5KXL2cw_Zboyia.webp&quot; alt=&quot;image-20220726213158665&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 axios 的请求根路径设置为 vue 项目的运行地址（接口请求不再跨域）&lt;/li&gt;
&lt;li&gt;vue 项目发现请求的接口不存在，把请求转交给 proxy 代理&lt;/li&gt;
&lt;li&gt;代理把请求根路径替换为 devServer.proxy 属性的值，发起真正的数据请求&lt;/li&gt;
&lt;li&gt;代理把请求到的数据，转发给 axios&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	注：devServer.proxy 提供的代理功能，仅在开发调试阶段生效。项目上线发布时，依旧需要 API 接口服务器开启 CORS 跨域资源共享&lt;/p&gt;
</content:encoded></item><item><title>彻底停止 windows 更新</title><link>https://miku.top/posts/stop-win-update/</link><guid isPermaLink="true">https://miku.top/posts/stop-win-update/</guid><description>介绍彻底停止 win11 更新的方法</description><pubDate>Fri, 22 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;彻底停止 windows 更新&lt;/h1&gt;
&lt;h2&gt;一、现状&lt;/h2&gt;
&lt;p&gt;:::tip
本文于 2022 年发布，具体方法是否依然适用请多加鉴别。
:::&lt;/p&gt;
&lt;p&gt;:::tip
本文章方法适用于将 win11 停止更新。win10 停止更新老方法依旧有效，请自行上网搜索。
​从 win 11 开始，系统的更新机制就变得更加变态。过去常用的组策略禁用和注册表大法都不好使了。因为与 Windows 更新相关的服务更多了，也出现了专门防止自动更新功能被篡改、修正自动更新的机制。
:::&lt;/p&gt;
&lt;h2&gt;二、方法&lt;/h2&gt;
&lt;p&gt;:::tip
如果你的系统是预览版，而不是正式版，不推荐停止更新。因为预览版都是有使用时限的，长时间不更新，超过时限系统将无法正常使用，俗称&quot;定时炸弹&quot;。另外由于系统更新组件和微软商店的组件是集成的，所以停止系统更新，可能会造成微软商店软件下载、更新功能的异常。
:::&lt;/p&gt;
&lt;p&gt;:::warning
以下的操作将涉及对系统文件的修改，你应当理解这些操作是极具危险性的。在进行操作前，建议进行数据备份和全量注册表备份。（当然你不备份也行，一般不容易出问题，但不保证...）
:::&lt;/p&gt;
&lt;h3&gt;1、NSudoLG 工具提权&lt;/h3&gt;
&lt;p&gt;​
我们后面需要操作系统文件，因此要先提升权限。这里我们使用 Nsudo工具进行权限提升。它可以 System 或 TrustedInstaller 的权限运行命令。项目地址：https://github.com/M2TeamArchived/NSudo&lt;/p&gt;
&lt;p&gt;​解压后，进入 NSudo Launcher/x64，启动 NSudoLG.exe：&lt;/p&gt;
&lt;p&gt;​进入程序后，用户选择 TrustedInstaller，并选择命令提示符，点击运行。&lt;/p&gt;
&lt;h3&gt;2、停止服务&lt;/h3&gt;
&lt;p&gt;​在命令提示符窗口输入 services.msc 回车（可能需要稍微等几秒），随后弹出服务组件管理的窗口。按照顺序依次选中，右键停止以下三个服务：（若已经停止则为灰色）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 更新医生服务&lt;/li&gt;
&lt;li&gt;更新 Orchestrator 服务&lt;/li&gt;
&lt;li&gt;Windows 更新&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、注册表备份&lt;/h3&gt;
&lt;p&gt;:::warning
前面说的全量注册表备份是可选项，但这里的注册表备份请一定要做。如果不做备份，很容易导致后期注册表被杀毒软件识别为无效注册表项误删！
:::&lt;/p&gt;
&lt;p&gt;​在刚才打开的命令提示符窗口输入：regedit，然后在地址栏输入：&lt;code&gt;HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services&lt;/code&gt; 后回车。&lt;/p&gt;
&lt;p&gt;​ 再点击 文件 -&amp;gt; 导出，导出注册表备份文件。（名字和路径自己决定）&lt;/p&gt;
&lt;h3&gt;4、移除更新组件&lt;/h3&gt;
&lt;p&gt;​回到命令提示符窗口，依次运行命令移动更新组件的几个关键 dll 和 exe：（也可以先写成一个 bat，再执行）。建议写命令时，目录后加 &lt;code&gt;\&lt;/code&gt;，如：&lt;code&gt;D:\dll_backup\&lt;/code&gt;。 防止写错路径造成文件覆盖。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;move C:\windows\system32\SIHClient.exe D:\update_comp_backup\
move C:\windows\system32\UsoClient.exe D:\update_comp_backup\
move C:\windows\system32\usosvc.dll D:\update_comp_backup\
move C:\windows\system32\WaaSMedicAgent.exe D:\update_comp_backup\
move C:\windows\system32\WaaSMedicSvc.dll D:\update_comp_backup\
move C:\windows\system32\wuauclt.exe D:\update_comp_backup\
move C:\windows\system32\wuaueng.dll D:\update_comp_backup\
move C:\windows\system32\wuauserv.dll D:\update_comp_backup\
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 后面的 &lt;code&gt;D:\dll_backup\&lt;/code&gt; 是我新建的一个目录。你可以自己新建一个目录（名称随意），用来保存移出的文件，方便后面需要恢复更新功能时，再进行还原。&lt;/p&gt;
&lt;h3&gt;5、检查是否成功&lt;/h3&gt;
&lt;p&gt;​
打开系统更新界面，出现转圈圈或显示未知错误即表明更新已被停止。&lt;/p&gt;
&lt;h2&gt;三、恢复&lt;/h2&gt;
&lt;p&gt;​
若出现问题，或需要恢复更新功能，可进行以下操作。&lt;/p&gt;
&lt;h3&gt;1、注册表恢复&lt;/h3&gt;
&lt;p&gt;​
如果服务项对应注册表被误删或无故消失，但又需要恢复，那就双击我们之前生成的注册表备份文件，进行还原。&lt;/p&gt;
&lt;h3&gt;2、更新功能恢复&lt;/h3&gt;
&lt;p&gt;​
如果需要重新启用更新、或使用微软商店的软件下载和更新功能，可运行以下命令将所有组件复位：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;move D:\update_comp_backup\* C:\windows\system32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ &lt;code&gt;D:\update_comp_backup\&lt;/code&gt; 对应你自己创建的目录。&lt;/p&gt;
</content:encoded></item><item><title>药品知识图谱问答系统设计</title><link>https://miku.top/posts/medical-kgqa-system/</link><guid isPermaLink="true">https://miku.top/posts/medical-kgqa-system/</guid><description>药品知识图谱问答系统设计</description><pubDate>Thu, 23 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;药品知识图谱问答系统设计&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;该项目是我们小组的知识图谱大作业。&lt;/p&gt;
&lt;p&gt;项目地址：https://github.com/AiCorein/Medical-KGQA-System&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、数据处理&lt;/h2&gt;
&lt;p&gt;首先将老师提供的 &lt;a href=&quot;https://github.com/aicorein/Medical-KGQA-System/blob/main/files/medical_ner_entities.json&quot;&gt;medical_ner_entities.json&lt;/a&gt; 进行格式化，方便知识图谱的建立：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623085428124.Dd2dN-hy_Z2gIolx.webp&quot; alt=&quot;image-20220623085428124&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查看处理后的 json 文件，可见于：&lt;a href=&quot;https://github.com/aicorein/Medical-KGQA-System/blob/main/files/processed_medical.json&quot;&gt;processed_medical.json&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其后，使用 &lt;code&gt;rmlmapper&lt;/code&gt; 将其转化为 turtle 格式的 rdf 数据：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623090028372.D6wI4jyo_ZINg9v.webp&quot; alt=&quot;image-20220623090028372&quot; /&gt;&lt;/p&gt;
&lt;p&gt;rdf 数据见于：&lt;a href=&quot;https://github.com/AiCorein/Medical-KGQA-System/blob/main/back-end/KG/medicals.ttl&quot;&gt;medicals.ttl&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;二、知识图谱查询服务构建&lt;/h2&gt;
&lt;p&gt;使用 Apache Jena 构建知识图谱查询服务。&lt;/p&gt;
&lt;p&gt;有关配置可参考：&lt;a href=&quot;/posts/apache-jena/&quot;&gt;Apache jena 配置和使用&lt;/a&gt;，配置常见问题可参考：&lt;a href=&quot;/posts/apache-jena-qa/&quot;&gt;Apache Jena 查询配置 FAQ&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;保证查询服务可以正常运行即可。&lt;/p&gt;
&lt;h2&gt;三、问句解析和 python 端查询&lt;/h2&gt;
&lt;p&gt;主要使用 python ltp 模块进行问句解析。（该部分文件：&lt;a href=&quot;https://github.com/AiCorein/Medical-KGQA-System/blob/main/back-end/parseQuery.py&quot;&gt;parseQuery.py&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;之后，我们使用 SPARQLWrapper 模块，对问句解析返回的 token，拼接 SPARQL 查询字符串，交给 Apache Jena 服务查询。（该部分文件：&lt;a href=&quot;https://github.com/AiCorein/Medical-KGQA-System/blob/main/back-end/Query.py&quot;&gt;Query.py&lt;/a&gt;）最后，我们拼接出的查询字符串，可以支持以下三类问句的结果查询：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些药品有某种属性/物质
&lt;ul&gt;
&lt;li&gt;哪些药含有麝香？&lt;/li&gt;
&lt;li&gt;哪些药可以治月经不调？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;某种药品是否有某种属性/物质
&lt;ul&gt;
&lt;li&gt;乌鸡白凤丸含有乌鸡吗？&lt;/li&gt;
&lt;li&gt;乌鸡白凤丸能治疗腰膝酸软吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;某种药品的某类信息
&lt;ul&gt;
&lt;li&gt;八珍益母丸的药物成分有哪些？&lt;/li&gt;
&lt;li&gt;哪些症状可以吃乌鸡白凤丸？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四、前后端构建&lt;/h2&gt;
&lt;h3&gt;1、后端&lt;/h3&gt;
&lt;p&gt;后端很简单，使用 python flask 构建。&lt;/p&gt;
&lt;p&gt;主要是以下两个路由：（根路径返回网页，/query 路径接受 POST 请求传送的提问问句）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623092219875.D90IO6h4_vxP8X.webp&quot; alt=&quot;image-20220623092219875&quot; /&gt;&lt;/p&gt;
&lt;p&gt;后端主控文件见于：&lt;a href=&quot;https://github.com/AiCorein/Medical-KGQA-System/blob/main/back-end/app.py&quot;&gt;app.py&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;同时，SPARQL 查询返回的结果并不够结构化，不能直接交付给前端，因此后端在返回数据前，还需要对数据进行格式化。我们格式化时，根据三类问句，分别格式化数据即可。（该部分文件：&lt;a href=&quot;https://github.com/AiCorein/Medical-KGQA-System/blob/main/back-end/queryProcess.py&quot;&gt;queryProcess.py&lt;/a&gt;）&lt;/p&gt;
&lt;h3&gt;2、前端&lt;/h3&gt;
&lt;p&gt;前端使用 Vue3 + Vite + Element plus 构建。&lt;/p&gt;
&lt;p&gt;前端业务逻辑部分也并不复杂。获取到问题字符串后，传递给后端即可获得问句的回答。&lt;/p&gt;
&lt;p&gt;最终交互界面如下。&lt;/p&gt;
&lt;p&gt;注：这里使用国药准字精准查询是因为，同名药品可能由不同厂家生产，它们的各种信息可能是不同的（如成分、治疗症状）。输入国药准字即可精准确认药品是否含有属性/物质，也可以精准获得它的某类信息（如成分、治疗症状）。而下方的 &quot;回答&quot; 部分则是一个笼统的、整合了所有同名药品的回答。&lt;/p&gt;
&lt;p&gt;第一类问句问答示例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623093116867.BwxjkbG0_trU9n.webp&quot; alt=&quot;image-20220623093116867&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一类问句使用国药准字检测：（这里使用一个十味手参散的国药准字，它是含有麝香这种成分的）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623094008141.DrZvC54V_afc95.webp&quot; alt=&quot;image-20220623094008141&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第二类问句问答示例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623093202348.DsTOUy3J_ZfTMDd.webp&quot; alt=&quot;image-20220623093202348&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第二类问句使用国药准字检测：（这里使用一个乌鸡白凤丸的国药准字）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623094214664.DmxCn7qo_Z13RhIz.webp&quot; alt=&quot;image-20220623094214664&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第三类问句问答示例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623093400469.DuyWVa3Y_Zm59il.webp&quot; alt=&quot;image-20220623093400469&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第三类问句使用国药准字精准筛选药品信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220623094337901.DlJXDPhF_AsbKn.webp&quot; alt=&quot;image-20220623094337901&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Apache Jena 查询配置 FAQs</title><link>https://miku.top/posts/apache-jena-qa/</link><guid isPermaLink="true">https://miku.top/posts/apache-jena-qa/</guid><description>Apache Jena 查询配置 FAQs</description><pubDate>Thu, 16 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Apache Jena 查询配置 FAQs&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本文章是站长自己，应知识图谱老师的要求编写的。感谢老师的信任与支持。&lt;/p&gt;
&lt;p&gt;关于 Apache Jena 详细的配置方法，我也写过另一篇文章，参见：&lt;a href=&quot;/posts/apache-jena/&quot;&gt;Apache jena 配置和使用&lt;/a&gt; （文章中 Jena 版本是 4.5.0，貌似 3.x 版本按这个方法有一些问题）&lt;/p&gt;
&lt;p&gt;本文主要列举一些常见的问题，供大家快速查阅。&lt;/p&gt;
&lt;p&gt;有什么建议、想法，或解决问题的经验，可以在评论区留言，任何留言会立即同步到我的邮箱，因此我一定会及时回复。欢迎大家分享新的 Q&amp;amp;A，我会加在这篇文章中，同时注明你的名字。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h4&gt;这里提供目录，供大家快速查找：（点击跳转）&lt;/h4&gt;
&lt;p&gt;1、学会自己分析报错&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#h2-1-h4-1&quot;&gt;Q：为什么这么多报错啊，救命，我又不会弄了！ qwq&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2、配置、查询常见错误&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-1&quot;&gt;Q：运行 &lt;code&gt;SPARQL.bat&lt;/code&gt; 提示找不到或无法加载主类。&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-2&quot;&gt;Q：配置任一环节，出现 &lt;code&gt;because it is already locked by the process with PID xxx.&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-3&quot;&gt;Q：运行 &lt;code&gt;fuseki-server.bat&lt;/code&gt; 报错，提示 &lt;code&gt;Expected xxx&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-4&quot;&gt;Q：运行 &lt;code&gt;fuseki-server.bat&lt;/code&gt; 报错，提示 &lt;code&gt;Expected xxx ... found a At &apos;:TriplesMap a&apos; &lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-5&quot;&gt;Q：运行 &lt;code&gt;fuseki-server.bat&lt;/code&gt; ，发生含有 &lt;code&gt;matches no grammar rules of URIs/IRIs&lt;/code&gt;，或 &lt;code&gt;Bad URIs&lt;/code&gt; 的的警告&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-6&quot;&gt;Q：运行 &lt;code&gt;fuseki-server.bat&lt;/code&gt; ，提示含有与 &lt;code&gt;path&lt;/code&gt; 相关的错误&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-7&quot;&gt;Q：为什么查询，查到的都是一些和数据无关的东西？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-8&quot;&gt;Q：为什么我完全查询不到任何东西？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-2-h4-9&quot;&gt;Q：为什么数据原有内容可以查询，规则推理的新规则查不到？&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3、使用 Python 包装查询常见错误&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#h2-3-h4-1&quot;&gt;Q：报错 &lt;code&gt;No module named &apos;SPARQLWrapper&apos;&lt;/code&gt; 或 &lt;code&gt;name &apos;SPARQLWrapper&apos; is not defined&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-3-h4-2&quot;&gt;Q：报错 &lt;code&gt;ConnectionRefusedError&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-3-h4-3&quot;&gt;Q：先是报错 &lt;code&gt;HTTPError: HTTP Error 400: Bad Request&lt;/code&gt;，又有报错 &lt;code&gt;QueryBadFormed&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#h2-3-h4-4&quot;&gt;Q：查询得到的结果是一堆乱麻麻的东西（HTML 字符串）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-1&quot;&amp;gt;&amp;lt;h2&amp;gt;1、学会自己分析报错&amp;lt;/h2&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-1-h4-1&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：为什么这么多报错啊，救命，我又不会弄了！ qwq&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;很多报错并不可怕，只要稍微阅读报错信息即可解决。如在 Jena 配置及查询配置时，我们大概率会遇到类似下面的报错情况：（点击图片放大）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220616215048.DKxVYs_p_Z2oyAoX.webp&quot; alt=&quot;微信图片_20220616215048&quot; /&gt;&lt;/p&gt;
&lt;p&gt;初看确实很吓人。这里的报错是会显示运行时所有错误的，所以看起来就很多。&lt;strong&gt;这是 Java 的异常抛出栈，错误发生的次序是从下到上的&lt;/strong&gt;。大多数时候，我们只需要看最开始的报错即可，因此定位到报错的最上层。（这里注意一点：&lt;strong&gt;如果是 python 的报错，反而要从最下层看起&lt;/strong&gt;。因为最下面打印的，才是最先发生错误的地方）&lt;/p&gt;
&lt;p&gt;最上层的关键提示是：&lt;code&gt;Failed to get canoncial path&lt;/code&gt;。那一看，大家应该可以很容易发现这是一个路径报错。那回到我们这行命令的执行处：一检查，哦，原来是两个路径字符串没有空格，连在一起了。之后一改掉这个错，就好了。&lt;/p&gt;
&lt;p&gt;所以，在整个 FAQ 中，我把分析报错这一环节放在最前面。&lt;strong&gt;就是希望大家能够主动地去分析报错，并尝试解决。无论是在现在还是在未来，这都是一种极其重要的能力&lt;/strong&gt;。当你冷静下来排错时，很多问题并没有那么可怕，是可以很快解决的。即使是遇到了稍微难解决的问题，也应该多自己尝试解决，一味地逃避是不可取的。&lt;/p&gt;
&lt;p&gt;当然如果真的遇到了特别难、无法解决的问题，也就不用浪费时间，向他人求助就行。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2&quot;&amp;gt;&amp;lt;h2&amp;gt;2、配置、查询常见错误&amp;lt;/h2&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-1&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：运行 SPARQL.bat 提示找不到或无法加载主类。&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;尝试更换 jdk。如从 openjdk11 更换到 jdk11。同时特别注意：如果你使用的 Apahce Jena 版本为 4.5，Java 版本需要 &amp;gt;= 11。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-2&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：配置任一环节，出现 because it is already locked by the process with PID xxx.&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;发生进程资源占用了，不要同时开多个 Jena 或 fuseki 的程序，一次只开一个。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-3&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：运行 fuseki-server.bat 报错，提示 Expected xxx：&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;根据报错提供的文件名、行号、列号信息去检查对应的文件是否有语法错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-4&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：运行 fuseki-server.bat 报错，提示 Expected xxx ... found a At &apos;:TriplesMap a&apos;  ：&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;看到这个 &lt;code&gt;:TriplesMap&lt;/code&gt;，我就知道你肯定把第二次作业用来生成 rdf 数据的那个规则 ttl 拿去用了。所以你理解有误，我们用 ttl 生成 tdb 数据，用的是第二次作业生成出来的那个 rdf 数据。（它是 rdf 数据，但后缀应该为 ttl）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-5&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：运行 fuseki-server.bat ，发生含有 matches no grammar rules of URIs/IRIs，或 Bad URIs 的的警告：&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这种警告，有时会影响正常工作，有时候又不会。如果 fuseki 正常可以查询，那就不用管。（&lt;strong&gt;一大法则：可以正常运行就不要动&lt;/strong&gt;）我个人的初步判断是，你配置文件的写法和你这个版本的 Jena 不匹配。具体是什么地方有问题，这个不好判断。建议按以下博客文章重新写一下配置文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3.x.x 版本：https://zhuanlan.zhihu.com/p/460129220&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/apache-jena/&quot;&gt;4.x.x 版本&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-6&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：运行 fuseki-server.bat ，提示含有与 path 相关的错误：&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;检查一下配置文件中的数据源路径、规则推理或 OWL 本体文件路径。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-7&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：为什么查询，查到的都是一些和数据无关的东西，像下面这样：&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20220616215342.DDo3zsnH_2qpvep.webp&quot; alt=&quot;微信图片_20220616215342&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你肯定把第二次作业用来生成 rdf 数据的那个规则 ttl 拿去用了。所以你理解有误，我们用 ttl 生成 tdb 数据，用的是第二次作业生成出来的那个 rdf 数据。（它是 rdf 数据，但后缀应该为 ttl）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-8&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：为什么我完全查询不到任何东西？&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;首先返回你的 &lt;code&gt;fuseki-server.bat&lt;/code&gt; 窗口，仔细检查是否有报错的情况，有则对应具体情况，查看其它 Q&amp;amp;A 项进行处理。若没有，请看看你是不是以下几类情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是不是把第二次作业用来生成 rdf 数据的那个规则 ttl 拿去生成 tdb 数据了？&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如果用的 tdb 数据是那个，自然也查不到和演员、电影名这些内容。你理解有误，我们用 ttl 生成 tdb 数据，用的是第二次作业生成出来的那个 rdf 数据。（它是 rdf 数据，但后缀应该为 ttl）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;是不是粗心没有写查询用到的前缀，或写错前缀了？&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如果是这样，那程序自然不认识前缀，也就查不到任何东西。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-2-h4-9&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：为什么数据原有内容可以查询，规则推理的新规则查不到？&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;首先返回你的 &lt;code&gt;fuseki-server.bat&lt;/code&gt; 窗口，仔细检查是否有报错的情况，有则对应具体情况，查看其它 Q&amp;amp;A 项进行处理。若没有，请先检查是不是规则 ttl 的语法问题，有则改正。如果还不能解决问题，大概率是你配置文件的写法和你这个版本的 Jena 不匹配。具体是什么地方有问题，这个不好判断。建议按以下博客文章重新写一下配置文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3.x.x 版本：https://zhuanlan.zhihu.com/p/460129220&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/apache-jena/&quot;&gt;4.x.x 版本&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-3&quot;&amp;gt;&amp;lt;h2&amp;gt;3、使用 Python 包装查询常见错误&amp;lt;/h2&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-3-h4-1&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：报错 No module named &apos;SPARQLWrapper&apos; 或 name &apos;SPARQLWrapper&apos; is not defined&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个问题其实不用回答了吧......，可以问问自己：pip 安装这个模块了吗？import 这个模块了吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-3-h4-2&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：报错 ConnectionRefusedError&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;检查自己的 fuseki 服务有没有正常运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-3-h4-3&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：先是报错 HTTPError: HTTP Error 400: Bad Request，又有报错 QueryBadFormed&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;查询字符串语法有问题，检查一下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;div id=&quot;h2-3-h4-4&quot;&amp;gt;&amp;lt;h4&amp;gt;Q：查询得到的结果是一堆乱麻麻的东西（HTML 字符串）：&amp;lt;/h4&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/IMG_20220615_124353.uLdvQAnW_ZzliSQ.webp&quot; alt=&quot;IMG_20220615_124353&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SPARQLWrapper 实例化的时候，传的 url 要和 endpoint 匹配，不用带 /query 这些路径。如下图：
&lt;img src=&quot;https://miku.top/_astro/image-20220616233550775.DnVcj45P_2uVle7.webp&quot; alt=&quot;image-20220616233550775&quot; /&gt;
&lt;img src=&quot;https://miku.top/_astro/image-20220616233558208.xuEIw1oW_Zmsi2b.webp&quot; alt=&quot;image-20220616233558208&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Apache jena 配置和使用</title><link>https://miku.top/posts/apache-jena/</link><guid isPermaLink="true">https://miku.top/posts/apache-jena/</guid><description>使用 Apache Jena 构建知识图谱查询服务</description><pubDate>Sun, 05 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Apache jena 配置和使用&lt;/h1&gt;
&lt;p&gt;​	使用 Apache jena 可以较为方便的构建知识图谱的查询服务。&lt;/p&gt;
&lt;h2&gt;1、查询数据准备&lt;/h2&gt;
&lt;p&gt;​	首先要保证你的数据 ttl 是没有问题的。
​	示例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220604213113899.DN84mokG_20mNcN.webp&quot; alt=&quot;image-20220604213113899&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	一般来说，为不影响后面查询和规则的配置，你的数据 ttl 要满足以下条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不存在空值&lt;/li&gt;
&lt;li&gt;object 指定了前缀或 URI&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2、下载、配置 apache jena&lt;/h2&gt;
&lt;p&gt;​	下载 apache jena 和对应环境变量的配置方法，此处不再赘述。请参照其他博客文章配置。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;注：本文章演示使用的 apache jena 版本是 4.5.0。其他版本按照本文章方法配置可能会出现问题！&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​	安装完成后，需要先启动一次 fuseki server。&lt;/p&gt;
&lt;p&gt;​	进入 &lt;code&gt;/fuseki&lt;/code&gt; 目录，双击 &lt;code&gt;fuseki-server.bat&lt;/code&gt; 或在 cmd 运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fuseki-server.bat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	初次运行结束后，会在该目录生成 &lt;code&gt;run&lt;/code&gt; 目录。&lt;/p&gt;
&lt;h2&gt;3、将 ttl 数据转化为 tdb 形式数据&lt;/h2&gt;
&lt;p&gt;​	cmd 运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tdbloader --loc=&quot;生成 tdb 的存放路径&quot; &quot;数据 ttl 路径位置&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：ttl、tdb 数据存放位置没有特别要求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220604215259223.D70eqTzm_24qzzu.webp&quot; alt=&quot;image-20220604215259223&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4、查询配置&lt;/h2&gt;
&lt;p&gt;​	配置查询需要我们编辑 &lt;code&gt;/fuseki/run&lt;/code&gt; 下的 &lt;code&gt;config.ttl&lt;/code&gt; 。（这里的配置可以理解为查询服务的主配置）&lt;/p&gt;
&lt;p&gt;​	也可以直接在 &lt;code&gt;/fuseki/run/configuration&lt;/code&gt; 新建配置的 ttl 文件。（注意，这里写配置的话，配置文件名可以随意命名。）（这里的配置可以理解为查询服务的额外配置，该文件目录下可以写多个配置。）&lt;/p&gt;
&lt;p&gt;​	我们可以在不同的配置文件中写不同的规则，实行多个并列的查询服务的部署。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;一般而言，我们只配置一个查询服务，只需要修改 &lt;code&gt;/fuseki/run/config.ttl&lt;/code&gt; 即可。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	配置文件的参考写法：（在 config.ttl）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@prefix :      &amp;lt;http://base/#&amp;gt; .
@prefix tdb:   &amp;lt;http://jena.hpl.hp.com/2008/tdb#&amp;gt; .
@prefix rdf:   &amp;lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&amp;gt; .
@prefix ja:    &amp;lt;http://jena.hpl.hp.com/2005/11/Assembler#&amp;gt; .
@prefix rdfs:  &amp;lt;http://www.w3.org/2000/01/rdf-schema#&amp;gt; .
@prefix fuseki: &amp;lt;http://jena.apache.org/fuseki#&amp;gt; .

# 配置一些基本信息，如查询服务的接口等
:service1        a                fuseki:Service ;
fuseki:dataset                    &amp;lt;#dataset&amp;gt; ;
fuseki:name                       &quot;movie&quot; ; 		   # 查询 endpoint，可以自定义
fuseki:serviceQuery               &quot;query&quot; , &quot;sparql&quot; ;
fuseki:serviceReadGraphStore      &quot;get&quot; ;
fuseki:serviceReadWriteGraphStore &quot;data&quot; ;
fuseki:serviceUpdate              &quot;update&quot; ;
fuseki:serviceUpload              &quot;upload&quot; .


&amp;lt;#dataset&amp;gt; rdf:type ja:RDFDataset ;
    ja:defaultGraph &amp;lt;#model_inf&amp;gt; ; .

&amp;lt;#model_inf&amp;gt; a ja:InfModel ;
    ja:baseModel &amp;lt;#tdbGraph&amp;gt; ; .

&amp;lt;#tdbGraph&amp;gt; rdf:type tdb:GraphTDB ;
    tdb:dataset &amp;lt;#tdbDataset&amp;gt; ; .

&amp;lt;#tdbDataset&amp;gt; rdf:type tdb:DatasetTDB ;
	# 下面对应刚才生成的 tdb 数据的那个路径
    tdb:location &quot;C:/Users/15742/code/第三次/fuseki/run/datasets/tdb&quot; ; .
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;注：该配置文件在 jena 4.5.0 版本测试有效，其他版本尚且未知是否有兼容性问题。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​	运行 &lt;code&gt;fuseki-server.bat&lt;/code&gt; ，再打开 &lt;code&gt;localhost:3030&lt;/code&gt; 进入查询：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220604220420135.CoekLbqI_Z26nEyn.webp&quot; alt=&quot;image-20220604220420135&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	注：我这里写了三个配置 ttl 文件，所以会有三个查询服务路径。大家一般只需要写一个即可。&lt;/p&gt;
&lt;p&gt;​	现在应该可以正常查询：（点击图片放大）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220604220644254.Bqfd_eVv_ZIxHRy.webp&quot; alt=&quot;image-20220604220644254&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;5、推理规则配置&lt;/h2&gt;
&lt;p&gt;​	首先我们需要把推理规则写在一个 ttl 里，ttl 路径位置自定。&lt;/p&gt;
&lt;p&gt;​	这里我命名为 &lt;code&gt;rules.ttl&lt;/code&gt; ，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@prefix : &amp;lt;https://movie.org/ontology/&amp;gt; .
@prefix movie: &amp;lt;https://movie.org/ontology/&amp;gt; .

[rule: (?m movie:com ?c) -&amp;gt; (?c movie:produce ?m)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	特别注意：&lt;strong&gt;这里的关系和前缀是和你的数据 ttl 对应的。同时，你可以尝试创造其他的推理规则。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	配置好推理规则后，重写一下配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@prefix :      &amp;lt;http://base/#&amp;gt; .
@prefix tdb:   &amp;lt;http://jena.hpl.hp.com/2008/tdb#&amp;gt; .
@prefix rdf:   &amp;lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&amp;gt; .
@prefix ja:    &amp;lt;http://jena.hpl.hp.com/2005/11/Assembler#&amp;gt; .
@prefix rdfs:  &amp;lt;http://www.w3.org/2000/01/rdf-schema#&amp;gt; .
@prefix fuseki: &amp;lt;http://jena.apache.org/fuseki#&amp;gt; .

:service1        a                fuseki:Service ;
fuseki:dataset                    &amp;lt;#dataset&amp;gt; ;
fuseki:name                       &quot;movie&quot; ;
fuseki:serviceQuery               &quot;query&quot; , &quot;sparql&quot; ;
fuseki:serviceReadGraphStore      &quot;get&quot; ;
fuseki:serviceReadWriteGraphStore &quot;data&quot; ;
fuseki:serviceUpdate              &quot;update&quot; ;
fuseki:serviceUpload              &quot;upload&quot; .


&amp;lt;#dataset&amp;gt; rdf:type ja:RDFDataset ;
    ja:defaultGraph &amp;lt;#model_inf&amp;gt; ; .

&amp;lt;#model_inf&amp;gt; a ja:InfModel ;
    ja:baseModel &amp;lt;#tdbGraph&amp;gt; ;
	#开启规则推理机
    ja:reasoner [
        ja:reasonerURL &amp;lt;http://jena.hpl.hp.com/2003/GenericRuleReasoner&amp;gt; ; 
        # 对应你规则 ttl 的路径位置
        ja:rulesFrom &amp;lt;file:///C:/Users/15742/code/第三次/fuseki/run/datasets/rules.ttl&amp;gt; ; ] .

&amp;lt;#tdbGraph&amp;gt; rdf:type tdb:GraphTDB ;
    tdb:dataset &amp;lt;#tdbDataset&amp;gt; ; .

&amp;lt;#tdbDataset&amp;gt; rdf:type tdb:DatasetTDB ;
    tdb:location &quot;C:/Users/15742/code/第三次/fuseki/run/datasets/tdb&quot; ; .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	重启 &lt;code&gt;fuseki-server.bat&lt;/code&gt; ，验证推理规则的查询，此时推理规则查询应该已经成功：（点击图片放大）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220604221659239.D1j9PTdi_JcNLO.webp&quot; alt=&quot;image-20220604221659239&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;6、使用 python 包装查询&lt;/h2&gt;
&lt;p&gt;​	如果我们的应用程序想要发起 SPARQL 查询，那应该怎么做？&lt;/p&gt;
&lt;p&gt;​	这里以 python 为例，要想在 python 运行 SPARQL 查询，需要 pip 这个模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install SPARQLWrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	如何在 python 运行查询并获得结果，可以参考下面的示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# SPARQL 查询串，将会通过查询接口发给 fuseki 服务器
query_string = &quot;&quot;&quot;
PREFIX dbo: &amp;lt;http://dbpedia.org/ontology/&amp;gt;
PREFIX movie: &amp;lt;https://movie.org/ontology/&amp;gt;
PREFIX rdf: &amp;lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&amp;gt;
PREFIX rdfs: &amp;lt;http://www.w3.org/2000/01/rdf-schema#&amp;gt;
SELECT ?com ?movie WHERE {
    ?com movie:produce ?movie.
}
&quot;&quot;&quot;

# 查询接口指定，注意路径要对应你之前写的 endpoint！
sparql = SPARQLWrapper(&quot;http://localhost:3030/movie/&quot;)
sparql.setQuery(query_string)
sparql.setReturnFormat(JSON)
# dict 函数转化一下，这样得到的结果就是字典变量（便于操作）
results = dict(sparql.query().convert())
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Webpack 学习笔记</title><link>https://miku.top/posts/webpack-learn/</link><guid isPermaLink="true">https://miku.top/posts/webpack-learn/</guid><description>Webpack 是用于现代 JavaScript 程序的模块打包工具</description><pubDate>Fri, 13 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Webpack 笔记&lt;/h1&gt;
&lt;h2&gt;一、前端工程化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;模块化（js 的模块化、Css 的模块化、资源的模块化）&lt;/li&gt;
&lt;li&gt;组件化（复用现有的 UI 结构、样式、行为）&lt;/li&gt;
&lt;li&gt;规范化（目录结构的划分、编码规范化、接口规范化、文档规范化、Git 分支管理）&lt;/li&gt;
&lt;li&gt;自动化（自动化构建、自动部署、自动化测试）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;二、webpack 基本使用&lt;/h2&gt;
&lt;p&gt;​	webpack 是前端项目工程化的具体解决方案。&lt;/p&gt;
&lt;p&gt;​	主要功能：它提供了友好的前端模块化开发支持，以及代码压缩混淆、处理浏览器端 JavaScript 的兼容性、性能优化等强大的功能。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;安装&lt;/strong&gt;：（记得先初始化 node.js 项目）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install webpack@5.42.1 webpack-cli@4.9.0 -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;配置&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;​	在项目根目录中，创建名为 webpack.config.js 的 webpack 配置文件，并初始化如下的基本配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
    mode: &apos;development&apos;		// 也可以选择 production，此时体积小，有压缩混淆，且有性能优化
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	在 package.json 的 scripts 节点下，增加脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;script&quot;: {
    &quot;dev&quot;: &quot;webpack&quot;	// 通过 npm run dev 即可执行命令
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;使用&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 运行以下命令进行打包构建：
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、自定义打包入口和出口&lt;/h2&gt;
&lt;p&gt;​	注：在 webpack 4.x 和 5.x 的版本中，有如下的默认约定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认的打包入口文件为 &lt;code&gt;src&lt;/code&gt; -&amp;gt; &lt;code&gt;index.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;默认的输出文件路径为 &lt;code&gt;dist&lt;/code&gt; -&amp;gt; &lt;code&gt;main.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	也可以通过配置来修改。&lt;/p&gt;
&lt;p&gt;​	在 webpack.config.js 配置文件中，通过 entry 节点指定打包的入口。通过 output 节点指定打包的出口。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const path = require(&apos;path&apos;)
module.exports = {
    entry: path.join(__dirname, &apos;./src/index.js&apos;),	// 打包入口
    output: {
        path: path.join(__dirname, &apos;./dist&apos;),	    // 出口
        filename: &apos;bundle.js&apos;		// 输出文件名称
    }
    devtool: &apos;inline-source-map&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、webpack 插件&lt;/h2&gt;
&lt;h3&gt;1、webpack-dev-server 插件&lt;/h3&gt;
&lt;p&gt;​	安装 webpack-dev-server，可自动打包代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install webpack-dev-server@3.11.2 -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	然后更改在 package.json 的 scripts 节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;script&quot;: {
    &quot;dev&quot;: &quot;webpack serve&quot;	// 通过 npm run dev 即可执行命令
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	但是注意：编译后的 js 文件是不会在本地的，而是在内存中，但可以通过 dev-server 提供的路径访问到。（默认路径是项目的根目录）&lt;/p&gt;
&lt;h3&gt;2、html-webpack-plugin 插件&lt;/h3&gt;
&lt;p&gt;​	html-webpack-plugin 是 webpack 中的 HTML 插件，可以通过此插件自定制 index.html 页面的内容。（实现 html 文件复制到根目录下，方便访问）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install html-webpack-plugin@5.1.0 -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	配置 webpack.config.js ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 导入 html 插件，得到一个构造函数
const HtmlPlugin = require(&apos;html-webpack-plugin&apos;)

const htmlPlugin = new HtmlPlugin({
    template: &apos;./src/index.html&apos;,	// 指定原文件存放路径
    filename: &apos;./index.html&apos;		// 指定生成文件存放路径
})

module.exports = {
    ...
    plugins: [htmlPlugin]	// 通过该节点，挂载 htmlPlugin 插件
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过该插件复制到项目根目录中的 index.html，&lt;strong&gt;也被放到了内存中&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;该插件在生成的 index.html 页面的底部，&lt;strong&gt;自动注入了打包的 js 文件&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、devServer 节点&lt;/h3&gt;
&lt;p&gt;​	在 webpack.config.js 配置文件中，可以通过 devServer 节点对 webpack-dev-server 插件进行更多的配置，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;devServer: {
    open: true,		// 初次打包完成后自动打开浏览器
    host: &apos;127.0.0.1&apos;,
   	port: 80,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、webpack 中的 loader&lt;/h2&gt;
&lt;p&gt;​	在实际开发过程中，webpack 默认只能打包处理以 .js 后缀名结尾的模块。其他非 .js 后缀名结尾的模块，webpack 默认处理不了，需要&lt;strong&gt;调用 loader 加载器&lt;/strong&gt;才可以正常打包，否则会报错！&lt;/p&gt;
&lt;p&gt;​	loader加载器的作用：协助 webpack 打包处理特定的文件模块。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;css-loader 可以打包处理 .css 相关的文件&lt;/li&gt;
&lt;li&gt;less-loader可以打包处理 .less 相关的文件&lt;/li&gt;
&lt;li&gt;babel-loader 可以打包处理 webpack 无法处理的高级 JS 语法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220513190139737.B2AyYhM8_ZzrrCh.webp&quot; alt=&quot;image-20220513190139737&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1、打包处理 css 文件&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;安装&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i style-loader@2.0.0 css-loader@5.0.1 -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;配置&lt;/strong&gt;：（在 webpack.config.js 的 module-&amp;gt;rules 数组中，添加 loader 规则如下）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {	// 所有第三方模块的配置规则
    rules: [	// 匹配规则
        { test: /\.css$/, use:[&apos;style-loader&apos;, &apos;css-loader&apos;]}
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：其中，test 表示匹配的文件类型，use 表示对应要调用的 loader。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;使用&lt;/strong&gt;：（在 js 中 import 对应的 css 即可）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;./css/index.css&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、打包处理 less 文件&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;安装&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i less-loader@7.1.0 less@3.12.2 -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;配置&lt;/strong&gt;：（在 webpack.config.js 的 module-&amp;gt;rules 数组中，添加 loader 规则如下）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {
    rules: [
        { test: /\.less$/, use:[&apos;style-loader&apos;, &apos;css-loader&apos;, &apos;less-loader&apos;]}
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：其中，test 表示匹配的文件类型，use 表示对应要调用的 loader。&lt;/p&gt;
&lt;h3&gt;3、打包处理样式表中与 url 路径相关的文件&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;安装&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i url-loader@4.1.1 file-loader@6.2.0 -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	配置：（在 webpack.config.js 的 module-&amp;gt;rules 数组中，添加 loader 规则如下）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {
    rules: [
        { test: /\.jpg|jpeg|png|gif$/, use: &apos;url-loader?limit=22229&apos;}
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;参数 limit 含义&lt;/strong&gt;： 图片大小 &amp;lt;= 22229 bytes，则转化为 base64 直接内嵌。&lt;/p&gt;
&lt;p&gt;​	参数配置也可以采用以下的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {
    rules: [
        {
		test: /\.jpg|jpeg|png|gif$/,
		use: { loader: &apos;url-loader&apos;, options: { limit: 22229 } }
        },
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、打包处理 js 文件中的高级语法&lt;/h3&gt;
&lt;p&gt;​	webpack 只能打包处理一部分高级的 JavaScript 语法。对于那些 webpack 无法处理的高级 js 语法，需要借助于 babel-loader 进行打包处理。例如 webpack 无法处理下面的 JavaScript 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person {
    static info = &apos;person info&apos;
}
console.log(Person.info)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	因此需要安装下面这些包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i babel-loader@8.2.1 @babel/core@7.12.3 @babel/plugin-proposal-class-properties@7.12.1 -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	添加规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    test: /\.js$/,
    // exclude 为排除项，表示只需要处理开发者编写的 js 文件，不需要处理 node_modules 中的
    exclude: /node_modules/,
    use: {
        loader: &apos;babel-loader&apos;,
        options: {
            // 声明一个 babel 插件，用来转化
            plugins: [&apos;@babel/plugin-proposal-class-properties&apos;],
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、打包发布&lt;/h2&gt;
&lt;p&gt;​	&lt;strong&gt;配置&lt;/strong&gt;：（在package.json文件的scripts节点下，新增build命令如下：）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;script&quot;: {
    ...
    &quot;build&quot;: &quot;webpack --mode production&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	运行命令 &lt;code&gt;npm run build&lt;/code&gt; 即可，完成在 dist 文件夹。&lt;/p&gt;
&lt;h3&gt;1、指定 js 文件和图片文件生成到目录&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;output: {
    path: path.join(__dirname, &apos;dist&apos;),
    // 明确存放到 dist/js 中
    filename: &apos;js/bundle.js&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
    test: /\.jpg|jpeg|png|gif$/,
    use: {
        loader: &apos;url-loader&apos;,
        options: {
            limit: 22228,
            outputPath: &apos;image&apos;,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、自动清理 dist 目录&lt;/h3&gt;
&lt;p&gt;​	安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install clean-webpack-plugin@3.0.0 -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 2.按需导入插件、得到插件的构造函数之后，创建插件的实例对象
const { CleanWebpackPlugin } = require(&apos;clean-webpack-plugin&apos;)
const cleanPlugin = new CleanWebpackPlugin()

//3.把创建的cleanPlughn 插件实例对象，挂载到plugins节点中
plugins: [htmlPlugin，cleanPlugin]，//挂载插件
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、Source Map&lt;/h2&gt;
&lt;p&gt;​	Source Map 就是一个信息文件，里面储存着位置信息。也就是说，Source Map 文件中存储着代码压缩混淆前后的对应关系。在开发环境下，webpack 默认启用了 Source Map 功能。当程序运行出错时，可以直接在控制台提示错误行的位置，并定位到具体的源代码。&lt;/p&gt;
&lt;p&gt;​	但是：开发环境下默认生成的 Source Map ，记录的是生成后的代码的位置。会导致运行时报错的行数与源代码的行数不一致的问题。示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220513223701339.CxJHCHMu_ZY8QSa.webp&quot; alt=&quot;image-20220513223701339.webp w-70%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;解决方案&lt;/strong&gt;：添加以下配置即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
    mode: &apos;development&apos;,
    // eval-source-map 仅限在&quot;开发模式&quot;下使用，不建议在&quot;生产模式&quot;下使用。
    // 此选项生成的 Source Map 能够保证&quot;运行时报错的行数&quot;与&quot;源代码的行数&quot;保持一致
    devtool: &apos;eval-source-map&apos;
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	在生产环境下，如果只想定位报错的具体行数，且不想暴露源码。此时可以将 devtool 的值设置为 nosources-source-map。实际效果如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220513225029547.B2HLyAHh_Z11N9Hh.webp&quot; alt=&quot;image-20220513225029547&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;八、注意&lt;/h2&gt;
&lt;p&gt;​	实际应用中我们不会直接自己从零书写 webpack 配置，可以使用一些现成的 CLI 工具。但我们需要了解这些配置的作用。&lt;/p&gt;
</content:encoded></item><item><title>Node.js 学习笔记</title><link>https://miku.top/posts/nodejs-learn/</link><guid isPermaLink="true">https://miku.top/posts/nodejs-learn/</guid><description>Node.js —— 让 JavaScript 成为与等服务端语言平起平坐的脚本语言</description><pubDate>Thu, 12 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Node.js 笔记&lt;/h1&gt;
&lt;h2&gt;一、Node.js 初识与内置模块&lt;/h2&gt;
&lt;h3&gt;1、初识&lt;/h3&gt;
&lt;h4&gt;（1）简介&lt;/h4&gt;
&lt;p&gt;​	Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。&lt;/p&gt;
&lt;h4&gt;（2）Node.js 中的 JS 运行环境&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220508101054872.DS9aNtGs_11SP6y.webp&quot; alt=&quot;image-20220508101054872&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	注：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器是 JavaScript 的前端运行环境。&lt;/li&gt;
&lt;li&gt;Node.js 是 JavaScript 的后端运行环境。&lt;/li&gt;
&lt;li&gt;Node.js 中&lt;strong&gt;无法调用 DOM 和 BOM 等浏览器内置 API&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（3）环境安装&lt;/h4&gt;
&lt;p&gt;​	网址：&lt;a href=&quot;https://nodejs.org/en&quot;&gt;Node.js 官网&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;（4）node 环境执行 JS&lt;/h4&gt;
&lt;p&gt;​	执行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node xxx.js
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;2、fs 文件系统模块&lt;/h3&gt;
&lt;p&gt;​	fs 模块是 Node.js 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性，用来满足用户对文件的操作需求。&lt;/p&gt;
&lt;p&gt;​	&lt;code&gt;fs.readFile())&lt;/code&gt; 方法，用来读取指定文件中的内容。&lt;/p&gt;
&lt;p&gt;​	&lt;code&gt;fs.writeFile() &lt;/code&gt;方法，用来向指定的文件中写入内容。&lt;/p&gt;
&lt;h4&gt;（1）读取文件&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&apos;fs&apos;)
fs.readFile(&apos;./test.txt&apos;, &apos;utf-8&apos;, (err, dataStr) =&amp;gt; {
    // 失败结果
    console.log(err)
    consloe.log(&apos;-----&apos;)
    // 读取的结果
    console.log(&apos;dataStr&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：如果读取失败，err 为错误对象，dataStr 为 undefined。&lt;/p&gt;
&lt;h4&gt;（2）写入文件&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&apos;fs&apos;)
fs.writeFile(&apos;./test.txt&apos;, &apos;Hello Node.js!&apos;, &apos;utf-8&apos;, err =&amp;gt; {
    // 如果写入成功，err 为 null
    console.log(err)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：写入失败，err 为错误对象。&lt;/p&gt;
&lt;h4&gt;（3）路径动态拼接问题&lt;/h4&gt;
&lt;p&gt;​	node 运行的 js 中拼接相对路径时，会以执行 node 的目录为基准，而不是被执行的 js 目录为准。&lt;/p&gt;
&lt;p&gt;​	可以使用 &lt;code&gt;__dirname&lt;/code&gt; 来指定路径，它指向当前 js 文件所处目录。&lt;/p&gt;
&lt;p&gt;​	那么这样解决拼接问题即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;path = __dirname + &apos;./test.txt&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：这里是为了演示，实际使用中更推荐使用 &lt;code&gt;path.join() &lt;/code&gt;方法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;3、path 路径模块&lt;/h3&gt;
&lt;p&gt;​	path 模块是 Nodejs 官方提供的、用来处理路径的模块。它提供了一系列的方法和属性，用来满足用户对路径的处理需求。&lt;/p&gt;
&lt;p&gt;​	&lt;code&gt;path.join() &lt;/code&gt;方法，用来将多个路径片段拼接成一个完整的路径字符串。&lt;/p&gt;
&lt;p&gt;​	&lt;code&gt;path.basename()&lt;/code&gt; 方法，用来从路径字符串中，将文件名解析出来。&lt;/p&gt;
&lt;h4&gt;（1）路径拼接&lt;/h4&gt;
&lt;p&gt;​	使用 &lt;code&gt;path.join&lt;/code&gt; 方法可以将&lt;strong&gt;多个路径片段&lt;/strong&gt;拼接成完整的路径字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const pathStr = path.join(&apos;/a&apos;, &apos;/b/c&apos;, &apos;../&apos;, &apos;./d&apos;, &apos;e&apos;);
console.log(pathStr);

const pathStr2 = path.join(__dirname, &apos;./files/1.txt&apos;);
console.log(pathStr2);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）获取路径中文件名&lt;/h4&gt;
&lt;p&gt;​	使用 &lt;code&gt;path.basename&lt;/code&gt; 方法可以从一个文件路径中提取文件名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const path = require(&apos;path&apos;);

const fpath = &apos;a/b/c/index.html&apos;
var fullName = path.basename(fpath);
console.log(fullName)	// output: index.html

var nameWithoutExt = path.basename(fpath, &apos;.html&apos;);
console.log(nameWithoutExt);	// output: index
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）获取路径中文件扩展名&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const path = require(&apos;path&apos;);

const fext = path.extname(fpath);
console.log(fext);	// 输出 .html
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;4、http 模块&lt;/h3&gt;
&lt;p&gt;​	http 模块是Node.js 官方提供的、用来创 web 服务器的模块。通过 http 模块提供的 &lt;code&gt;http.createServer() &lt;/code&gt; 方法，就能方便的把一台普通的电脑，变成一台 Web 服务器，从而对外提供 Web 资源服务。&lt;/p&gt;
&lt;h4&gt;（1）创建基本的 web 服务器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;导入 http 模块&lt;/li&gt;
&lt;li&gt;创建 web 服务器实例&lt;/li&gt;
&lt;li&gt;为服务器实例绑定 request 事件，监听客户端请求&lt;/li&gt;
&lt;li&gt;启动服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const http = require(&apos;http&apos;)

const server = http.createServer()
server.on(&apos;request&apos;, (req, res) =&amp;gt; {
    console.log(&apos;Someone visit the web server.&apos;)
})

server.listen(80, () =&amp;gt; {
    console.log(&apos;Server runnnig at http://127.0.0.1:80&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）req 请求对象与 res 响应对象&lt;/h4&gt;
&lt;p&gt;​	只要服务器接收到了客户端的请求，就会调用通过 &lt;code&gt;server.on()&lt;/code&gt; 为服务器绑定的 request 事件处理函数。
​	如果想在事件处理函数中，访问与客户端相关的数据或属性，可以使用如下的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server.on(&apos;request&apos;, (req) =&amp;gt; {
    const url = req.url;
    const method = req.method;
    const str = `The request url is ${url}, the method is ${method}`
    console.log(str);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	在服务器的 request 事件处理函数中，如果想访问与服务器相关的数据或属性，可以使用如下的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server.on(&apos;request&apos;, (req, res) =&amp;gt; {
    const str = `你请求的地址是：${req.url}, 方法是：${req.method}`
    res.setHeader(&apos;Content-Type&apos;, &apos;text/html; charset=utf-8&apos;)
    res.end(str)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、模块化&lt;/h2&gt;
&lt;h3&gt;1、Node.js 中的模块化&lt;/h3&gt;
&lt;h4&gt;（1）分类&lt;/h4&gt;
&lt;p&gt;​	分为三大类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内置模块（内置模块是由 Node.js 官方提供的，例如 fs、path、http 等）&lt;/li&gt;
&lt;li&gt;自定义模块（用户创建的每个 js 文件，都是自定义模块）&lt;/li&gt;
&lt;li&gt;第三方模块（由第三方开发出来的模块，并非官方提供的内置模块，也不是用户创建的自定义模块，使用前需要先下载）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（2）加载模块&lt;/h4&gt;
&lt;p&gt;​	使用强大的 &lt;code&gt;require()&lt;/code&gt; 方法，可以加载需要的内置模块、用户自定义模块、第三方模块进行使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&apos;fs&apos;)
const custom = require(&apos;./custom.js&apos;)	// 可以省略 js 后缀名
const electron = require(&apos;electron&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	使用 &lt;code&gt;require()&lt;/code&gt; 方法加载其它模块时，会执行被加载模块中的代码。&lt;/p&gt;
&lt;h4&gt;（3）模块作用域&lt;/h4&gt;
&lt;p&gt;​	和函数作用域类似，在自定义模块中定义的变量、方法等成员，&lt;strong&gt;只能在当前模块内被访问&lt;/strong&gt;，这种模块级别的访问限制，叫做模块作用域。&lt;/p&gt;
&lt;p&gt;​	如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1.js 中：
const username = &apos;MelodyEcho&apos;

// 2.js 中：
const custom = require(&apos;./1.js&apos;)
console.log(custom);	// 会发现打印出来是空对象
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	模块作用域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以防止全局变量污染&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（4）向外共享模块作用域的成员&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;module 对象&lt;/strong&gt;：在每个js自定义模块中都有一个module对象，它里面存储了和当前模块有关的信息。&lt;/p&gt;
&lt;p&gt;​	结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Module {
  id: &apos;.&apos;,
  path: &apos;c:\\Users\\15742\\Desktop\\Front-end(new)\\Node.js\\code&apos;,
  exports: {},
  filename: &apos;c:\\Users\\15742\\Desktop\\Front-end(new)\\Node.js\\code\\test.js&apos;,
  loaded: false,
  children: [],
  paths: [
    &apos;c:\\Users\\15742\\Desktop\\Front-end(new)\\Node.js\\code\\node_modules&apos;,
    &apos;c:\\Users\\15742\\Desktop\\Front-end(new)\\Node.js\\node_modules&apos;,
    &apos;c:\\Users\\15742\\Desktop\\Front-end(new)\\node_modules&apos;,
    &apos;c:\\Users\\15742\\Desktop\\node_modules&apos;,
    &apos;c:\\Users\\15742\\node_modules&apos;,
    &apos;c:\\Users\\node_modules&apos;,
    &apos;c:\\node_modules&apos;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;module.exports 对象&lt;/strong&gt;：在自定义模块中，可以使用 module.exports 对象，将模块内的成员共享出去，供外界使用。外界用 &lt;code&gt;require()&lt;/code&gt; 方法导入自定义模块时，得到的就是 module.exports 所指向的对象。它默认是为空的。&lt;/p&gt;
&lt;p&gt;​	示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1.js 中：
module.exports.username = &apos;MelodyEcho&apos;
module.exports.echo = function(melody) {
    console.log(`Melody ${melody} has been echoed.`)
}
const age = 18
module.exports.age = age;

// 2.js 中：
const m = require(&apos;./1.js&apos;)
console.log(m);		
// 输出：{ username: &apos;MelodyEcho&apos;, echo: [Function (anonymous)], age: 18 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：模块导入的结果，&lt;strong&gt;永远以 module.exports 指向的对象为准&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	由于 module.exports 单词写起来比较复杂，为了简化向外共享成员的代码，Node 提供了 exports 对象。默认情况下，exports 和 module.exports 指向同一个对象。最终共享的结果，还是以 module.exports 指向的对象为准。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(exports === module.exports)		// True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	一些使用案例的结果：（原理是 JS 引用机制，这里不再赘述）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220508211641439.D8gUgdKy_ZlMWSf.webp&quot; alt=&quot;image-20220508211641439&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220508211654345.Bfl9x5Mo_8Katc.webp&quot; alt=&quot;image-20220508211654345&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;总结&lt;/strong&gt;：为了防止混乱，最好不要在同一个模块中同时使用 exports 和 module.exports 。&lt;/p&gt;
&lt;h4&gt;（5）模块化规范&lt;/h4&gt;
&lt;p&gt;​	Nodejs 遵循了 CommonJS 模块化规范，CommonJS 规定了模块的特性和各模块之间如何相互依赖。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个模块内部，module 变量代表当前模块。&lt;/li&gt;
&lt;li&gt;module 变量是一个对象，它的 exports 属性（即 module.exports ）是对外的接口。&lt;/li&gt;
&lt;li&gt;加载某个模块，其实是加载该模块的 module.exports 属性。&lt;code&gt;require()&lt;/code&gt; 方法用于加载模块。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2、npm 与包&lt;/h3&gt;
&lt;h4&gt;（1）包&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;简介&lt;/strong&gt;：Node.js 中的第三方模块又叫做包。包是基于内置模块封装出来的，提供了更高级、更方便的APl，极大的提高了开发效率。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;来源&lt;/strong&gt;：不同于 Node.js 中的内置模块与自定义模块，包是由第三方个人或团队开发出来的，免费供所有人使用。Node.js 中的包都是免费且开源的，不需要付费即可免费下载使用。&lt;/p&gt;
&lt;p&gt;​	搜索包：https://www.npmjs.com/&lt;/p&gt;
&lt;p&gt;​	下载包（使用 npm 工具）：https://registry-mpmis.org/&lt;/p&gt;
&lt;h4&gt;（2）npm 使用&lt;/h4&gt;
&lt;p&gt;​	安装命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install 包1 [包2]
npm i 包1 [包2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	卸载命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm uninstall 包名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	文件分析：初次装包完成后，在项目文件夹下多一个叫做 node_modules 的文件夹和 package-lock.json 的配置文件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;node_modules 文件夹用来存放所有已安装到项目中的包。&lt;code&gt;require()&lt;/code&gt; 导入第三方包时，就是从这个目录中查找并加载包。&lt;/li&gt;
&lt;li&gt;package-lock.json 配置文件用来记录 node_modules 目录下的每一个包的下载信息，例如包的名字、版本号、下载地址等。&lt;/li&gt;
&lt;li&gt;特别注意，如果当前目录上级目录中有 package.json 文件，那么就&lt;strong&gt;不会在当前目录添加包，而是在那一层上级目录。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	安装指定版本包：默认情况下，使用npm install命令安装包的时候，会自动安装最新版本的包。如果需要安装指定版本的包，可以在包名之后，通过@符号指定具体的版本，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i moment@2.22.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）npm 包的语义化版本规范&lt;/h4&gt;
&lt;p&gt;​	包的版本号是以&quot;点分十进制&quot;形式进行定义的，总共有三位数字，例如 2.24.0。
​	其中每一位数字所代表的的含义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第1位数字：大版本&lt;/li&gt;
&lt;li&gt;第2位数字：功能版本&lt;/li&gt;
&lt;li&gt;第3位数字：Bug修复版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	规则：只要前面的版本号增长了，则后面的版本号归零。&lt;/p&gt;
&lt;h4&gt;（4）npm 包管理配置文件&lt;/h4&gt;
&lt;p&gt;​	npm 规定，在项目根目录中，&lt;strong&gt;必须提供&lt;/strong&gt;一个叫做 package.json 的包管理配置文件。用来记录与项目有关的一些配置信息。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目的名称、版本号、描述等&lt;/li&gt;
&lt;li&gt;项目中都用到了哪些包&lt;/li&gt;
&lt;li&gt;哪些包只在开发期间会用到&lt;/li&gt;
&lt;li&gt;那些包在开发和部署时都需要用到&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​	快速在当前目录下创建 package.json：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm init -y
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;目录路径不要有中文和空格&lt;/li&gt;
&lt;li&gt;只需要运行一次，之后再进行安装时，npm 会自动更新该文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;package.json 中的 dependencies 节点&lt;/strong&gt;：专门用来记录使用 npm install 命令安装了哪些包。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;一次性安装所有包&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 执行npm install 命令时，npm 包管理工具会先读取 package.json 中的dependencies 节点，读取到记录的所有依赖包名称和版本号之后，npm 包管理工具会把这些包一次性下载到项目中。
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;package.json 中的 devDependencies 节点&lt;/strong&gt;：如果某些包只在项目开发阶段会用到，在项目上线之后不会用到，则建议把这些包记录到devDependencies节点中。与之对应的，如果某些包在开发和项目上线之后都需要用到，则建议把这些包记录到 dependencies 节点中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装指定的包，并记录到devDependencies节点中
npm i 包名 -D
# 注意：上述命令是简写形式，等价于下面完整的写法：
npm install包名--save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（5）切换 npm 下载镜像源&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 查看当前下载源
npm config get registry
# 切换为淘宝镜像源
npm config set registry=https://registry.npm.taobao.org/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（6）nrm 工具&lt;/h4&gt;
&lt;p&gt;​	为了更方便的切换下包的镜像源，我们可以安装 nrm 这个小工具，利用 nrm 提供的终端命令，可以快速查看和切换下包的镜像源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 通过npm包管理器，将 nrm 安装为全局可用的工具
npm i nrm -g
# 查看所有可用的镜像源
nrm ls
# 将下包的镜像源切换为 taobao 镜像
nrm use taobao
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;3、包的分类与包发布&lt;/h3&gt;
&lt;h4&gt;（1）项目包&lt;/h4&gt;
&lt;p&gt;​	那些被安装到项目的 node_modules 目录中的包，都是项目包。
​	项目包又分为两类，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发依赖包（被记录到 devDependencies 节点中的包，只在开发期间会用到）&lt;/li&gt;
&lt;li&gt;核心依赖包（被记录到 dependencies 节点中的包，在开发期间和项目上线之后都会用到）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（2）全局包&lt;/h4&gt;
&lt;p&gt;​	在执行 npm install 命令时，如果提供了-g 参数，则会把包安装为全局包。&lt;/p&gt;
&lt;p&gt;​	默认安装位置为：&lt;code&gt;%AppData%/npm/node_modules&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​	注：只有工具性质的包，才有全局安装的必要性。因为它们提供了好用的终端命令。至于是否全局安装，可以参考官方说明。&lt;/p&gt;
&lt;h4&gt;（3）规范的包结构&lt;/h4&gt;
&lt;p&gt;​	在清楚了包的概念、以及如何下载和使用包之后，接下来，我们深入了解一下包的内部结构。&lt;/p&gt;
&lt;p&gt;​	一个规范的包，它的组成结构，必须符合以下3点要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;包必须以&lt;strong&gt;单独的目录&lt;/strong&gt;而存在&lt;/li&gt;
&lt;li&gt;包的顶级目录下要&lt;strong&gt;必须包含 package.json&lt;/strong&gt; 这个包管理配置文件&lt;/li&gt;
&lt;li&gt;package.json 中&lt;strong&gt;必须包含 name，version，main 这三个属性&lt;/strong&gt;，分别代表包的名字、版本号、包的入口。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	注：以上3点要求是一个规范的包结构必须遵守的格式，关于更多的约束，可以参考如下网址：
​	https://yarnpkg.com/zh-Hans/docs/package-json&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;4、模块的加载机制&lt;/h3&gt;
&lt;h4&gt;（1）优先从缓存加载&lt;/h4&gt;
&lt;p&gt;​	模块在第一次加载后会被缓存。这也意味着多次调用 &lt;code&gt;require()&lt;/code&gt; 不会导致模块的代码被执行多次。同时，内置模块、用户自定义模块、还是第三方模块，它们都会优先从缓存中加载，从而提高模块的加载效率。&lt;/p&gt;
&lt;h4&gt;（2）内置模块加载机制&lt;/h4&gt;
&lt;p&gt;​	内置模块是由 Node.js 官方提供的模块，同名时内置模块的加载优先级最高。&lt;/p&gt;
&lt;h4&gt;（3）自定义模块加载机制&lt;/h4&gt;
&lt;p&gt;​	使用 &lt;code&gt;require()&lt;/code&gt; 加载自定义模块时，必须指定以 ./ 或 ../ 开头的路径标识符。在加载自定义模块时，如果没有指定 ./ 或 ../ 这样的路径标识符，node.js 会把它当作内置模块或第三方模块进行加载。&lt;/p&gt;
&lt;p&gt;​	同时，在使用 &lt;code&gt;require()&lt;/code&gt; 导入自定义模块时，如果省略了文件的扩展名，则 Node.js 会按顺序分别尝试加载以下的文件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按照确切的文件名进行加载&lt;/li&gt;
&lt;li&gt;补全 js 扩展名进行加载&lt;/li&gt;
&lt;li&gt;补全 json 扩展名进行加载&lt;/li&gt;
&lt;li&gt;补全 node 扩展名进行加载&lt;/li&gt;
&lt;li&gt;加载失败，终端报错&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	如果传递给 &lt;code&gt;require()&lt;/code&gt; 的模块标识符不是一个内置模块，也没有以 ./ 或 ../ 开头，则 Node.js 会从当前模块的父目录开始，尝试从 /node_modules 文件夹中加载第三方模块。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;如果没有找到对应的第三方模块，则移动到再上一层父目录中，进行加载，直到文件系统的根目录。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;（4）目录作为模块&lt;/h4&gt;
&lt;p&gt;​	当把目录作为模块标识符，传递给 &lt;code&gt;require()&lt;/code&gt; 进行加载的时候，有三种加载方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在被加载的目录下查找一个叫做 package.json 的文件，并寻找 main 属性，作为 &lt;code&gt;require()&lt;/code&gt; 加载的入口&lt;/li&gt;
&lt;li&gt;如果目录里没有 package.json 文件，或者 main 入口不存在或无法解析，则 Node.js 将会试图加载目录下的 index.js 文件。&lt;/li&gt;
&lt;li&gt;如果以上两步都失败了，则 Node.js 会在终端打印错误消息，报告模块的缺失：Error: Cannot find module &apos;xxx&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;三、express 模块&lt;/h2&gt;
&lt;h3&gt;1、初识 express&lt;/h3&gt;
&lt;p&gt;​	Express 的作用和 Node.js 内置的 http 模块类似，是专门用来创建 Web 服务器的。http 内置模块用起来很复杂，开发效率低；Express 是基于内置的 http 模块进一步封装出来的，能够极大的提高开发效率。&lt;/p&gt;
&lt;p&gt;​	对前端开发者来说，使用 Express，我们可以方便、快速的创建 &lt;strong&gt;Web 网站的服务器&lt;/strong&gt;或 &lt;strong&gt;API 接口的服务器&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2、基本使用&lt;/h3&gt;
&lt;p&gt;​	安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i express@4.17.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	创建基本的 web 服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 导入 express
const express = require(&apos;express&apos;)
// 启动 web 服务器
const app = express()
// 启动 web 服务器
app.listen(80, () =&amp;gt; {
    console.log(&apos;express server running at http://localhost&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	监听 GET 和 POST 请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数1：客户端请求的URL地址
// 参数2：请求对应的处理函数
// req：请求对象（包含了与请求相关的属性与方法）
// res：响应对象（包含了与响应相关的属性与方法）
app.get(&apos;请求URL&apos;，function(req, res) {...})
app.post(&apos;请求URL&apos;，function(req, res) {...})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	把内容响应给客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;res.send({name: &apos;melodyecho&apos;, age: 18})
res.send(&apos;Hello World!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	获取 URL 中携带的查询参数（如无则为空对象）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(req.query);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	获取 URL 中的动态参数值（如 /user/:id 或 /users/:id/:name）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(req.params);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;3、托管静态资源&lt;/h3&gt;
&lt;h4&gt;（1）托管单个资源目录&lt;/h4&gt;
&lt;p&gt;​	express 提供了一个非常好用的函数，叫做 &lt;code&gt;express.static()&lt;/code&gt;，通过它，我们可以非常方便地创建一个静态资源服务器，例如，通过如下代码就可以将 public 目录下的图片、CSS 文件、JavaScript 文件对外开放访问了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use(express.static(&apos;public&apos;))
// 比如 public 里有文件夹：images
// 那么访问路径： /images/bg.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：Express 在指定的静态目录中查找文件，并对外提供资源的访问路径。&lt;strong&gt;因此，存放静态文件的目录名不会出现在 URL 中&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;（2）托管多个资源目录&lt;/h4&gt;
&lt;p&gt;​	多次调用 &lt;code&gt;express.use()&lt;/code&gt; 方法即可，只是注意查找顺序是按书写顺序的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use(express.static(&apos;public&apos;))
app.use(express.static(&apos;files&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	按 URL 区分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use(&apos;/users&apos;, express.static(&apos;./user-info&apos;))
app.use(&apos;/&apos;, express.static(&apos;./assets&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;4、路由&lt;/h3&gt;
&lt;p&gt;​	其实就是映射关系。&lt;/p&gt;
&lt;p&gt;​	在 Express 中，路由指的是客户端的请求与服务器处理函数之间的映射关系。Express 中的路由分 3 部分组成，分别是请求的类型、请求的 URL 地址、处理函数，格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app.METHOD(PATH, HANDLER)
app.get(&apos;/&apos;, function(req, res) =&amp;gt; {
	res.send(&apos;Hello Wrold!&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（1）路由匹配过程&lt;/h4&gt;
&lt;p&gt;​	每当一个请求到达服务器之后，需要先经过路由的匹配，只有匹配成功之后，才会调用对应的处理函数。&lt;/p&gt;
&lt;p&gt;​	在匹配时，会按照路由的顺序进行匹配，如果请求类型和请求的 URL 同时匹配成功，则 Express 会将这次请求，转交给对应的函数进行处理。&lt;/p&gt;
&lt;h4&gt;（2）模块化路由&lt;/h4&gt;
&lt;p&gt;​	为了方便对路由进行模块化的管理，Express 不建议将路由直接挂载到 app 上，而是推荐将路由抽离为单独的模块。&lt;/p&gt;
&lt;p&gt;​	步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建路由模块对应的 js 文件&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;express.Router()&lt;/code&gt; 函数创建路由对象&lt;/li&gt;
&lt;li&gt;向路由对象上挂载具体的路由&lt;/li&gt;
&lt;li&gt;使用 module.exports 向外共享路由对象&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;app.use()&lt;/code&gt; 函数注册路由模块&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	路由模块书写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// router.js:
const express = require(&apos;express&apos;)
const router = express.Router()

router.get(&apos;/user/list&apos;, () =&amp;gt; {
    res.send(&apos;Get user list.&apos;)
})
router.post(&apos;/user/add&apos;, () =&amp;gt; {
    res.send(&apos;Add new user.&apos;)
})

module.exports = {&quot;router&quot;: router}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注册路由模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// server.js:
const { router } = require(&apos;./router&apos;)
app.use(router)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）路由模块前缀&lt;/h4&gt;
&lt;p&gt;​	如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//1.导入路由模块
const userRouter = require(&apos;./router/user.js&apos;)
//2.使用 app.use() 注册路由模块，并添加统一的访问前缀 /api
app.use(&apos;/api&apos;，userRouter)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;5、中间件&lt;/h3&gt;
&lt;h4&gt;（1）概述&lt;/h4&gt;
&lt;p&gt;​	当一个请求到达 Express 的服务器之后，可以连续调用多个中间件，从而对这次请求进行预处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220511152436480.BAGNJKE7_Z9ac5Y.webp&quot; alt=&quot;image-20220511152436480.webp&amp;quot; w-80%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;格式&lt;/strong&gt;：Express 的中间件，本质上就是一个 function 处理函数，Express 中间件的格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.get(&apos;/&apos;, function(req, res, next) {
    next();
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：中间件函数的形参列表中，&lt;strong&gt;必须包含 next 参数&lt;/strong&gt;。而路由处理函数中只包含 req 和 res。next 函数是实现多个中间件连续调用的关键，&lt;strong&gt;它表示把流转关系转交给下一个中间件或路由&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;（2）中间件的作用&lt;/h4&gt;
&lt;p&gt;​	多个中间件之间，共享同一份 req 和 res。基于这样的特性，我们可以&lt;strong&gt;在上游的中间件中，统一为 req 或 res 对象添加自定义的属性或方法&lt;/strong&gt;，共享给下游的中间件或路由进行使用。&lt;/p&gt;
&lt;h4&gt;（3）全局中间件&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;定义&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 常量 mw 所指向的，就是一个中间件函数
// 注意：在当前中间件的业务处理完毕后，必须调用 next() 函数
// 表示把流转关系转交给下一个中间件或路由
const mw = function(req, res, next) {
    console.log(&apos;This is a simple middleware func.&apos;)
    next()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;全局生效中间件&lt;/strong&gt;：客户端发起的任何请求，到达服务器之后，都会触发的中间件，叫做全局生效的中间件。通过调用 &lt;code&gt;app.use(中间件函数)&lt;/code&gt;，即可定义一个全局生效的中间件，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use(mw)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;全局定义简化形式&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use((req, res, next) =&amp;gt; {
    console.log(&apos;This is a simple middleware func.&apos;)
    next()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;连续定义&lt;/strong&gt;：按顺序书写，按顺序调用。&lt;/p&gt;
&lt;h4&gt;（4）局部中间件&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;局部生效中间件&lt;/strong&gt;：该中间件被挂载到指定路由上。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 该路由挂载了 mw1 中间件
app.get(&apos;/user&apos;, mw1, function(req, res) {...})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;定义多个局部中间件&lt;/strong&gt;：按顺序给定或使用装到一个数组里（完全等价）。按书写顺序调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.get(&apos;/user&apos;, mw1, mw2, function(req, res) {...})
app.get(&apos;/user&apos;, [mw1, mw2], function(req, res) {...})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（5）中间件注意事项&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;一定在&lt;strong&gt;路由注册前&lt;/strong&gt;注册中间件&lt;/li&gt;
&lt;li&gt;客户端发送过来的请求，&lt;strong&gt;可以连续调用&lt;/strong&gt;多个中间件处理&lt;/li&gt;
&lt;li&gt;执行完中间件的代码，&lt;strong&gt;不要忘记调用 &lt;code&gt;next()&lt;/code&gt; 函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;为了防止代码逻辑混乱，不要在 &lt;code&gt;next()&lt;/code&gt; 函数后写任何代码&lt;/li&gt;
&lt;li&gt;连续调用多个中间件时，多个中间件之间，共享 req 和 res 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（6）中间件分类&lt;/h4&gt;
&lt;p&gt;​	为了方便大家理解和记忆中间件的使用，Express 官方把常见的中间件用法，分成了 5 大类。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;应用级别的中间件&lt;/strong&gt;：通过 &lt;code&gt;app.use()&lt;/code&gt; 或 &lt;code&gt;app.get()&lt;/code&gt; 或  &lt;code&gt;app.post()&lt;/code&gt;，&lt;strong&gt;绑定到app 实例上&lt;/strong&gt;的中间件。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;路由级别的中间件&lt;/strong&gt;：绑定到 &lt;code&gt;express.Router()&lt;/code&gt; 实例上的中间件，叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过，应用级别中间件是绑定到 app 实例上，&lt;strong&gt;路由级别中间件绑定到 router 实例上&lt;/strong&gt;，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var router = express.Router()
router.use(中间件函数)
app.use(&apos;/&apos;, router)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;错误级别中间件&lt;/strong&gt;：专门用来捕获整个项目中发生的异常错误，从而防止项目异常崩溃的问题。不过其对应的处理函数有 4 个参数：&lt;code&gt;err, req, res, next&lt;/code&gt;。注意：&lt;strong&gt;这种中间件要在注册在所有路由之后！&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.get(&apos;/&apos;, (req, res) =&amp;gt; {
    throw new Error(&apos;自定义错误&apos;)
    res.send(&apos;Home page.&apos;)
})
app.use((err, req, res, next) =&amp;gt; {
    console.log(`发生了错误：${err.message}`)
    res.send(`Error! ${err.message}`)
    next();
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;Express 内置的中间件&lt;/strong&gt;：自 Express 4.16.0版本开始，Express 内置了 3 个常用的中间件，极大的提高了 Express 项目的开发效率和体验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;express.static&lt;/code&gt; 快速托管静态资源的内置中间件，例如：HTML 文件、图片、CSS 样式等（无兼容性）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;express.json&lt;/code&gt; 解析 JSON 格式的请求体数据（有兼容性，仅在 4.16.0+ 版本中可用）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;express.urlencoded&lt;/code&gt; 解析 URL-encoded 格式的请求体数据（有兼容性，仅在 4.16.0+ 版本中可用）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 配置解析 application/json 格式数据的内置中间件
app.use(express.json())
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({ extended: false }))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 注：这样可以支持解析两种表单数据格式。因为某种格式解析失败不会修改 req.body
app.use(express.json())
app.use(express.urlencoded({ extended: false }))

app.post(&apos;/user&apos;, (req, res) =&amp;gt; {
    // 在服务器，可以使用 req.body 这个属性来接受请求体数据
    // 默认情况下，如果不配置解析表单数据的中间件，req.body 默认等于 undefined
    console.log(req.body)
    res.send(&apos;OK&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;第三方中间件&lt;/strong&gt;：非 Express 官方内置的，而是由第三方开发出来的中间件，叫做第三方中间件。在项目中，大家可以按需下载并配置第三方中间件，从而提高项目的开发效率。&lt;/p&gt;
&lt;p&gt;​	如在 express@4.16.0 之前的版本中，经常使用 body-parser 这个第三方中间件来解析请求体数据。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;自定义中间件&lt;/strong&gt;：将其封装为自定义模块，引用即可。&lt;/p&gt;
&lt;h2&gt;​&lt;/h2&gt;
&lt;h3&gt;6、CORS 跨域资源共享实现&lt;/h3&gt;
&lt;h4&gt;（1）方法&lt;/h4&gt;
&lt;p&gt;​	使用 cors 中间件可以解决跨域问题。&lt;/p&gt;
&lt;p&gt;​	cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件，可以很方便地解决跨域问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i cors
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 一定要在路由注册前配置这个中间件
const cors = require(&apos;cors&apos;)
app.use(cors())
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）CORS 相关三个请求头&lt;/h4&gt;
&lt;p&gt;​	请求头：&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​	说明：origin 参数的值指定了允许访问该资源的外域URL。示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;res.setHeader(&apos;Access-Control-Allow-Origin&apos;, &apos;http://example.com&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​	请求头：&lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​	说明：默认情况下，CORS 仅支持客户端向服务器发送如下的 9 个请求头：&lt;code&gt;Accept&lt;/code&gt;、&lt;code&gt;Accept-Language&lt;/code&gt;、&lt;code&gt;Content-Language&lt;/code&gt;、&lt;code&gt;DPR&lt;/code&gt;、&lt;code&gt;Downlink&lt;/code&gt;，&lt;code&gt;Save-Data&lt;/code&gt;、&lt;code&gt;Viewport-Width&lt;/code&gt;、&lt;code&gt;Width&lt;/code&gt;、&lt;code&gt;Content-Type&lt;/code&gt;（值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一）如果客户端向服务器发送了额外的请求头信息，则&lt;strong&gt;需要在服务器端，通过 Access-Control-Allow-Headers 对额外的请求头进行声明&lt;/strong&gt;，否则这次请求会失败！示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 如允许客户端额外向服务器发送 Content-Type 请求头和 X-Custom-Header 请求头
// 注：多个请求头之间使用英文的逗号进行分割
res.setHeader(&apos;Access-Control-Allow-Headers&apos;, &apos;Content-Type，X-Custom-Header&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	请求头：&lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​	说明：默认情况下，CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源，则需要在服务器端，通过 Access-Control-Alow-Methods 来指明实际请求所允许使用的 HTTP 方法。示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 只允许 POST、GET、DELETE、HEAD 请求方法
res.setHeader(&apos;Access-Control-Allow-Methods&apos;, &apos;POST，GET，DELETE，HEAD&apos;)
// 允许所有的 HTTP 请求方法
res.setHeader(&apos;Access-Control-A1low-Methods&apos;, &quot;*&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（3）CROS 请求分类&lt;/h4&gt;
&lt;p&gt;​	客户端在请求CORS接口时，根据请求方式和请求头的不同，可以将 CORS 的请求分为两大类，分别是：简单请求和预检请求。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;简单请求&lt;/strong&gt;：同时满足以下两大条件的请求，就属于简单请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;请求方式：GET、POST、HEAD 三者之一&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTTP 头部信息不超过以下几种字段：无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type（只有三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;预检请求&lt;/strong&gt;：符合以下任意一个条件的请求，都需要先进行预检请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求方式为 GET、POST、HEAD 之外的请求类型&lt;/li&gt;
&lt;li&gt;请求头中包含自定义头部字段&lt;/li&gt;
&lt;li&gt;向服务器发送了 application/json 格式的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	注：在浏览器与服务器正式通信之前，浏览器会先发送 OPTION 请求进行预检，以获知服务器是否允许该实际请求，所以&lt;strong&gt;这一次的 OPTION 请求称为&quot;预检请求&quot;&lt;/strong&gt;。服务器成功响应预检请求后，才会发送真正的请求，并且携带真实数据。&lt;/p&gt;
&lt;h4&gt;（4）jsonp 接口&lt;/h4&gt;
&lt;p&gt;​	概念：浏览器端通过 &amp;lt;script&amp;gt; 标签的 src 属性，请求服务器上的数据，同时，服务器返回一个函数的调用。这种请求数据的方式叫做 JSONP。&lt;/p&gt;
&lt;p&gt;​	特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JSONP 不属于真正的 Ajax 请求，因为它没有使用 XMLHttpRequest 这个对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSONP 仅支持 GET 请求&lt;/strong&gt;，不支持 POST、PUT、DELETE 等请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	注：如果项目中已经配置了 CORS 跨域资源共享，为了防止冲突，必须在配置 CORS 中间件之前声明 JSONP 的接口。否则 JSONP 接口会被处理成开启了 CORS 的接口。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、数据库与身份认证&lt;/h2&gt;
&lt;p&gt;​	关于数据库与 mysql 数据库的知识、操作这里不再赘述。&lt;/p&gt;
&lt;h3&gt;1、操作数据库&lt;/h3&gt;
&lt;p&gt;​	这里以 mysql 数据库为例。&lt;/p&gt;
&lt;p&gt;​	安装：想要操作 mysql 数据库，首先需要通过 npm 安装 mysql 模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	在使用前需要配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mysql = require(&apos;mysql&apos;)
const db = mysql.createPool({
    host: &apos;loclhost&apos;,
    user: &apos;root&apos;,
    password: &apos;123&apos;,
    database: &apos;sample&apos;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;查询语句&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query_str = &apos;select author, count(*) as poetNum from tang\
            group by author\
            order by count(*) desc\
            limit 0, 10&apos;
db.query(query_str, (err, results) =&amp;gt; {
    if (err) return console.error(err.message)
    console.log(results)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：查询执行成功返回的是数组。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;插入语句&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const user = { id: &apos;2&apos;, name: &apos;MelodyScend&apos;, password: &apos;12345&apos;}
// ? 表示占位符
const sqlStr = &apos;insert into user(`id`, `name`, `password`) values(?, ?, ?)&apos;
// 使用数组形式，可以依次将值填充到 ? 位置，如果只有一个占位符，可以直接作为参数
db.query(sqlStr, [user.id, user.name, user.password], (err, results) =&amp;gt; {
    if (err) return console.error(err.message)
    console.log(results);
    if (results.affactedRows === 1) console.log(&apos;插入成功&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	更新和删除语句操作类似。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2、web 开发模式概述&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;服务端渲染&lt;/strong&gt;：服务器发送给客户端的 HTML 页面，是在服务器通过字符串的拼接，动态生成的。因此，客户端不需要使用 Ajax 这样的技术额外请求页面的数据。优点：前端耗时少，有利于 SEO。缺点：服务器压力大、前后端不分离、开发效率低。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;前后端分离&lt;/strong&gt;：开发体验好。前端专注于 UI 页面的开发，后端专注于 api 的开发，且前端有更多的选择性。优点：开发体验好、用户体验好、减轻了服务端压力。缺点：不利于 SEO。&lt;/p&gt;
&lt;p&gt;​	注：具体选择什么开发模式，取决于业务场景。且选择什么开发模式也不是固定的，可以两种方式都使用。（如首屏服务端渲染，其他页面前后端分离）&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;3、身份认证&lt;/h3&gt;
&lt;p&gt;​	身份认证（Authentication）又称&quot;身份验证&quot;、&quot;鉴权&quot;，是指通过一定的手段，完成对用户身份的确认。&lt;/p&gt;
&lt;p&gt;​	不同开发模式下的身份认证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端渲染推荐使用 Session 认证机制&lt;/li&gt;
&lt;li&gt;前后端分离推荐使用 JWT 认证机制&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;4、session 认证机制&lt;/h3&gt;
&lt;h4&gt;（1）HTTP 无状态与 Cookie&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;HTTP 协议的无状态性&lt;/strong&gt;：客户端的每次 HTTP 请求都是独立的，连续多个请求之间没有直接的关系，服务器不会主动保留每次 HTTP 请求的状态。&lt;/p&gt;
&lt;p&gt;​	可以通过 Cookie 解决这个问题。Cookie 是存储在用户浏览器中的一段不超过 4KB 的字符串。它由一个名称（Name）、一个值（Value）和其它几个用于控制 Cookie &lt;strong&gt;有效期、安全性、使用范围&lt;/strong&gt;的可选属性组成。&lt;/p&gt;
&lt;p&gt;​	注意：不同域名下的 Cookie &lt;strong&gt;各自独立&lt;/strong&gt;，每当客户端发起请求时，会自动把当前域名下**所有未过期的 Cookie **一同发送到服务器。&lt;/p&gt;
&lt;p&gt;​	总结 Cookie 的几大特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动发送&lt;/li&gt;
&lt;li&gt;域名独立&lt;/li&gt;
&lt;li&gt;过期时限&lt;/li&gt;
&lt;li&gt;4kb 限制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;重点&lt;/strong&gt;：由于 Cookie 是存储在浏览器中的，而且浏览器也提供了读写 Cookie 的 APl，因此 Cookie 很容易被伪造，不具有安全性。因此不建议服务器将重要的隐私数据，通过 Cookie 的形式发送给浏览器。&lt;/p&gt;
&lt;h4&gt;（2）提高身份认证的安全性&lt;/h4&gt;
&lt;p&gt;​	如同&quot;会员卡+刷卡认证&quot;的机制，我们引入 cookie + session 的机制。&lt;/p&gt;
&lt;p&gt;​	原理如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220511213035928.CEBiaMzO_Z1aNY8H.webp&quot; alt=&quot;image-20220511213035928&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;（3）express-session 中间件&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;安装&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i express-session
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;配置&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var session = require(&apos;express-session&apos;)
app.use(session({
    secret: &apos;a cat&apos;,
    resave: false,			// 固定写法
    saveUninitialized: true  // 固定写法
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;向 session 中存入数据&lt;/strong&gt;：当 express-session 中间件配置成功后，即可通过 req.session 来访问和使用 session 对象，从而存储用户的关键信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.post(&apos;/api/login&apos;, (req, res) =&amp;gt; {
    if (req.body.username !== &apos;admin&apos; || req.body.password !== &apos;000&apos;) {
        return res.send({ status: 1, msg: &apos;login failed.&apos;})
    }
    req.session.user = req.body		// 存储用户信息
    req.session.islogin = true		// 存储用户登录状态
    res.send({ status: 1, msg: &apos;login success.&apos;})
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;从 session 中取出数据&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.get(&apos;/api/username&apos;, (req, res) =&amp;gt; {
    if (!req.session.islogin) {
        return res.send({ status: 0, msg: &apos;failed&apos;})
    }
    res.send({ status: 1, msg: &apos;success&apos;})
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;清空 session&lt;/strong&gt;：调用 &lt;code&gt;req.session.destroy()&lt;/code&gt; 函数，即可清空服务器保存当前用户的 session 信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.post(&apos;/api/logout&apos;, (req, res) =&amp;gt; {
    req.session.destroy()
    res.send({
        status: 0,
        msg: &apos;退出登录成功&apos;
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;5、JWT 认证机制&lt;/h3&gt;
&lt;h4&gt;（1）概述&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;Session 认证机制缺点&lt;/strong&gt;：需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问，所以，当涉及到前端跨域请求后端接口的时候，需要做很多额外的配置，才能实现跨域 Session 认证。此时推荐使用 JWT认证机制。&lt;/p&gt;
&lt;p&gt;​	 &lt;strong&gt;JWT（JSON Web Token） 认证&lt;/strong&gt;：目前最流行的跨域认证解决方案。&lt;/p&gt;
&lt;p&gt;​	工作原理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220511220216869.DFMhUFq4_1GvDJF.webp&quot; alt=&quot;image-20220511220216869&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	总结：用户的信息通过加密的 token 字符串存储在客户端，服务器还原来进行验证。&lt;/p&gt;
&lt;p&gt;​	JWT 组成部分：通常由三部分组成，分别是Header（头部）、Payload（有效荷载）、Signature（签名）。如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Header.Payload.Signature
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;eyJhbGcioiJIUzI1NiIsInRScCI6IkpXVCJ9.eyJpZCI6MSwidXN1cm5hbwUi0iJhZ61pbiIsInBhc3N3b3JkIjoiliwibmlja25hbwUioiLms6X1t7Tlt7QiLCJ1bwFpbCI6Im5pYmFiYUBpdGNhc3QuY24iLCJ1c2VyX3BpYyI6iIsImlhdCI6MTU30DAzNjY4MiwizXhwIjoxNTc4MlDcyNjgyfQ.Mwq7GqCXJPK-EA8LNrtNG0411KdZ33S9KBL3XeuBxuI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	说明：&lt;strong&gt;Payload 部分才是真正的用户信息&lt;/strong&gt;，它是用户信息经过加密之后生成的字符串。Header 和 Signature 是安全性相关的部分，只是为了保证 Token 的安全性。&lt;/p&gt;
&lt;p&gt;​	使用方式：放在请求头的 &lt;code&gt;Authorization&lt;/code&gt; 字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）安装和使用&lt;/h4&gt;
&lt;p&gt;​	&lt;strong&gt;安装&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i jsonwebtoken express-jwt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jsonwebtoken 用于生成JWT字符串&lt;/li&gt;
&lt;li&gt;express-jwt 用于将 JWT 字符串解析还原成 JSON 对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	&lt;strong&gt;使用&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 导入（最近发生了大版本迭代，express-jwt 用法和以前不同了）
const jwt = require(&apos;jsonwebtoken&apos;)
const { expressjwt } = require(&apos;express-jwt&apos;)

// 设置 secret 密钥，用于加解密
const secretKey = &apos;example.com&apos;

// 注册将 token 字符串还原成 JSON 对象的中间件，同时填写不需要验证的路径（路径要写全），这里也可以用正则表达式实现对路由验证
// 配置成功该中间件，可以通过 req.auth 访问到 token 字符串中的信息对象
app.use(expressjwt({
    secret: secretKey,
    algorithms: [&apos;HS256&apos;],		// 指定加密算法，必需
    credentialsRequired:true })
.unless({ path: [/^\/api\//] }))

// 登录
app.post(&apos;/api/login&apos;, (req, res) =&amp;gt; {
    // 进行用户名和密码验证
    if (req.body.username != &apos;MelodyEcho&apos; || req.body.password != &apos;18&apos;) {
        return res.send({
            status: 401,
            msg: &apos;invalid&apos;,
        })
    }
    // 生成 token 字符串，参数：信息对象，密钥，有效期(秒)。注意 token 中不要放入密码！
	const token = jwt.sign({ username: req.body.username }, secretKey, { expiresIn: 60 })
	console.log(`用户 ${req.body.username} 的 token 已分配`)
	res.send({
        status: 200,
        msg: &apos;success&apos;,
        token: token
	})
})

// 获取信息
app.get(&apos;/admin/getinfo&apos;, (req, res) =&amp;gt; {
    // req.auth.iat 与 req.auth.exp 是自动添加的属性
    console.log(`用户 ${req.auth.username} 发起了信息获取，于 ${req.auth.iat} 注册，于 ${req.auth.exp} 过期`)
    res.send({
        status: 200,
        msg: &apos;success&apos;,
        data: req.auth
    })
})

...

// 在路由之后注册一个全局错误中间件，捕获 token 过期或不合法的情况
app.use((err, req, res, next) =&amp;gt; {
    if (err.name == &apos;UnauthorizedError&apos;) {
        console.log(&apos;客户端请求携带了一个无效或过期的 token&apos;)
        return res.send({ status: 401, msg: &apos;expired or invalid&apos;})
    }
    return res.send({status: 500, msg: &apos;error&apos;})
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>axios 学习笔记</title><link>https://miku.top/posts/axios-learn/</link><guid isPermaLink="true">https://miku.top/posts/axios-learn/</guid><description>Axios，基于 promise 封装的 HTTP 库</description><pubDate>Thu, 05 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;axios 笔记&lt;/h1&gt;
&lt;h2&gt;一、axios 概述、配置&lt;/h2&gt;
&lt;p&gt;主要功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器端发送 AJAX 请求&lt;/li&gt;
&lt;li&gt;Node.js 支持下发送 HTTP 请求&lt;/li&gt;
&lt;li&gt;支持 Promise API&lt;/li&gt;
&lt;li&gt;请求和响应拦截&lt;/li&gt;
&lt;li&gt;请求和响应的转发&lt;/li&gt;
&lt;li&gt;取消请求&lt;/li&gt;
&lt;li&gt;客户端防御 XSRF&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;https://cdn.bootcdn.net/ajax/libs/axios/0.27.2/axios.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;npm install axios
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;二、axios 基本使用&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;!function Action() {
    axios({
        method: &apos;xxx&apos;,	// 方法类型
        url: &apos;xxx&apos;,
        // 请求体，如果需要的话
        data: {
            a: xxx,
            b: xxx,
        },
    }).then(resp =&amp;gt; {
        console.log(resp);
    })
}();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、axios 其他请求方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;axios.request(config);
axios.post(url, [data], [config]);
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	更多见于官方文档。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、axios 响应结果结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
    config: ...,	// axios 配置
    data: ...,		// 响应数据
    headers: ...,	// 响应头
    request: ...,	// 原生 AJAX 实例
    status: xxx,
    statusText: &quot;xxx&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、axios config 对象&lt;/h2&gt;
&lt;p&gt;​	&lt;code&gt;config&lt;/code&gt; 对象体常用值如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    url: &apos;xxx&apos;,
    method: &apos;xxx&apos;,
    baseURL: &apos;xxx&apos;,		// axios 会将它与 URL 自动拼接
    // 下面两个都是做预处理
    transformRequest: [function (data, headers) {
        ...
        return data;
    }],
    transformResponse: [function(data) {
        ...
        return data;
    }],
    headers: {xxx},
    params: {xxx},
    data: {xxx},	// 或这样：data: &apos;Country=Brasil&amp;amp;City=Belo Horizonte&apos;,
    timeout: xxx,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、axios 默认设置&lt;/h2&gt;
&lt;p&gt;​	方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;axios.defaults.method = &apos;GET&apos;;
axios.defaults.baseURL = &apos;http://localhost:3000&apos;;
axios.defaults.params = {id: 300};
axios.defaults.timeout = 3000;

axios({
    url: &apos;/posts&apos;,
}).then(resp =&amp;gt; {
    console.log(resp);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、axios 创建实例对象发送请求&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 实例化
const obj = axios.create({
    baseURL: &apos;https://api.apiopen.top&apos;,
    timeout: 2000
});
// 调用方法，也可以直接 obj(config)，使用与 axios 对象是很类似的
obj.get(&apos;/getJoke&apos;).then(resp =&amp;gt; {
    console.log(resp);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：创建实例对象的好处是，可以&lt;strong&gt;给几类不同的请求指定不同的默认配置&lt;/strong&gt;。而不是使用上面的全局默认配置。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;八、拦截器&lt;/h2&gt;
&lt;p&gt;​	简单来说，就是函数。可以在请求或响应前拦截进行检测。（也就是实现一些预处理）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 请求拦截器
axios.interceptors.request.use(config =&amp;gt; {
    // 这里的 config 就是 config 对象
    console.log(&apos;请求拦截器 成功&apos;);
    return config;
}, err =&amp;gt; {
    console.log(&apos;请求拦截器 失败&apos;);
    return Promise.reject(err);
});
// 响应拦截器
axios.interceptors.response.use(resp =&amp;gt; {
    console.log(&apos;响应拦截器 成功&apos;);
    return resp;
}, err =&amp;gt; {
    console.log(&apos;响应拦截器 失败&apos;);
    return Promise.reject(err);
})

// 具体的某个请求
axios.get(&apos;https://example.com/api/get&apos;).then(resp =&amp;gt; {
    console.log(resp);
}).catch(err =&amp;gt; {
    console.warn(err);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	提示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求拦截器可以用于检查并修改配置&lt;/li&gt;
&lt;li&gt;响应拦截器可以用于对原生的 axios 响应对象进行过滤&lt;/li&gt;
&lt;li&gt;其中任何一个地方抛出错误或出现问题，都会按以下链传递：&lt;code&gt;请求拦截器&lt;/code&gt; -&amp;gt; &lt;code&gt;响应拦截器&lt;/code&gt; -&amp;gt; &lt;code&gt;请求的自定义 catch&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;九、axios 取消请求&lt;/h2&gt;
&lt;p&gt;​	一个例子：（发送请求前先进行检查，若有，则取消重发）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var cancel = null;

sendBtn.onclick = function() {
    // 判断是否有未完成请求
    if (cancel !== null) {
        cancel();
    }
    axios({
        method: &apos;GET&apos;,
        url: &apos;http://localhost:3000/posts&apos;,
        // 添加 cancelToken 配置项
        cancelToken: new axios.CancelToken(c =&amp;gt; {
        	// 将 c 的值赋给 cancel
            cancel = c;
    	})
    }).then(resp =&amp;gt; {
        console.log(resp);
        // 请求完成，重置
        cancel = null;
    })
}

cancelBtn.onclick = function() {
    cancel();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十、多个并发请求（弃用）&lt;/h2&gt;
&lt;p&gt;​	例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getUserAccount() {
  return axios.get(&apos;/user/12345&apos;);
}

function getUserPermissions() {
  return axios.get(&apos;/user/12345/permissions&apos;);
}

// 参数为所有 axios 请求函数的数组
axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
    // 两个请求现在都执行完成
  }));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：现推荐使用 &lt;code&gt;Promise.all()&lt;/code&gt; 替代以上方法。&lt;/p&gt;
</content:encoded></item><item><title>Promise 学习笔记</title><link>https://miku.top/posts/promise-learn/</link><guid isPermaLink="true">https://miku.top/posts/promise-learn/</guid><description>Promise —— JS 异步编程新解决方案</description><pubDate>Thu, 05 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Promise 学习笔记&lt;/h1&gt;
&lt;h2&gt;一、Promise 简介&lt;/h2&gt;
&lt;p&gt;​	Promise 是 JS 中进行异步编程的新解决方案。（旧方案是单纯使用回调函数）&lt;/p&gt;
&lt;p&gt;​	优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指定回调函数更加灵活&lt;/li&gt;
&lt;li&gt;支持链式调用，可以解决毁回调地狱问题&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Promise 初体验&lt;/h2&gt;
&lt;p&gt;​	resolve、reject 两者都是函数类型的数据，分别为成功和失败后的回调。&lt;/p&gt;
&lt;p&gt;​	在内部封装时可以携带参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const p = new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
        if (xxx) {
            resolve(arg1);	//将 promise 对象的状态设置为成功
        }
        else {
            reject(arg2);	//将 promise 对象的状态设置为失败
        }
    }, 1000)
})

// 第一个为成功回调，第二个为失败回调
p.then((arg) =&amp;gt; {
    alert(&apos;success, the arg is &apos; + arg);
}, (arg) =&amp;gt; {
    alert(&apos;failed&apos; + arg);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	实际使用中也可以这样封装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function asyncTask(arg) {
    return new Promise((resolve, reject) =&amp;gt; {
        if (xxx) {
            resolve();
        }
        else {
            reject();
        }
    })
}

const obj = asyncTask(xxx);
obj.then((arg1) =&amp;gt; {
    ...
}, (arg2) =&amp;gt; {
    ...
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;code&gt;util.promisify&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 可以借助 node.js 的该方法来将函数转化为 promise 风格
const util = require(&apos;util&apos;);
const fs = require(&apos;fs&apos;);

let mineReadFile = util.promisify(fs.readFile);
mineReadFile(...).then(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、Promise 状态与值&lt;/h2&gt;
&lt;p&gt;​	指实例对象中的一个属性：&lt;code&gt;PromiseState&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​	只有以下两种转变情况：&lt;/p&gt;
&lt;p&gt;​		&lt;code&gt;pending&lt;/code&gt; -&amp;gt; &lt;code&gt;fulfilled&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​		&lt;code&gt;pending&lt;/code&gt; -&amp;gt;&lt;code&gt;rejected&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​	且一个 promise 对象只能改变一次状态。无论变为成功还是失败，都会有一个结果数据。成功的结果数据一般称为 value，失败的结果数据一般称为 reason。&lt;/p&gt;
&lt;p&gt;​	Promise 对象的值：&lt;/p&gt;
&lt;p&gt;​		实例对象中的另一个属性 &lt;code&gt;PromiseResult&lt;/code&gt;，保存着对象成功/失败的结果&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、Promise 的基本流程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/image-20220418213510140.DY5NZGZr_1Ap5AR.webp&quot; alt=&quot;image-20220418213510140&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、Promise API&lt;/h2&gt;
&lt;h3&gt;1、构造函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;executor = (resolve, reject) =&amp;gt; {};
Promise(excutor) {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：executor 函数会在 Promise 内部立即同步调用，异步操作在执行器中执行。&lt;/p&gt;
&lt;h3&gt;2、Promise.prototype.then()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Promise.prototype.then(onResolved，onRejected) =&amp;gt; {};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onResolved&lt;/code&gt; 函数：成功的回调函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onRejected&lt;/code&gt; 函数：失败的回调函数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	注：指定用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调，返回一个新的 promise 对象&lt;/p&gt;
&lt;h3&gt;3、Promise.prototype.catch()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Promise.prototype.catch(reason =&amp;gt; {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	只用于指定失败的回调。&lt;/p&gt;
&lt;h3&gt;4、Promise.resolve()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var obj = Promise.resolve(arg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	如果传入的是非 Promise 对象，则返回结果为成功 Promise 对象。&lt;/p&gt;
&lt;p&gt;​	如果传入的是 Promise 对象，参数的结果决定了 resolve 的结果。&lt;/p&gt;
&lt;p&gt;​	如下方返回一个成功 Promise 对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p1 = Promise.resolve(new Promise((resolve, reject) =&amp;gt; {
    // resolve(&apos;It\&apos; OK.&apos;);
    reject(&apos;Error&apos;);
}))
console.log(p1);
// catch
p1.catch(reason =&amp;gt; {
    console.log(reason);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、Promise.reject()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var obj = Promise.reject(arg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	传入任何类型值，返回都为失败 Promise 对象，而&lt;strong&gt;失败的结果是传递的值或对象&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	如下方返回一个&lt;strong&gt;失败 Promise 对象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p2 = Promise.resolve(new Promise((resolve, reject) =&amp;gt; {
    resolve(&apos;It\&apos; OK.&apos;);
}))
console.log(p2);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6、Promise.all()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Promise.all((promises) =&amp;gt; {})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	说明：返回一个新的 promise，只有所有的 promise 都成功才成功，只要有一个失败了就直接失败。&lt;/p&gt;
&lt;p&gt;​	成功返回结果是所有 promise 对象返回结果的数组，&lt;/p&gt;
&lt;p&gt;​	失败返回结果是失败 promise 对象的返回结果。&lt;/p&gt;
&lt;h3&gt;7、Promise.race()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Promise.race((promises) =&amp;gt; {})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	说明：返回一个新的 promise，第一个完成（改变状态）的 promise 的结果状态就是最终的结果状态。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、Promise 关键问题&lt;/h2&gt;
&lt;h3&gt;1、Promise 状态改变的方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;resolve（value）：如果当前是 pending 就会变为 resolved&lt;/li&gt;
&lt;li&gt;reject（reason）：如果当前是 pending 就会变为 rejected&lt;/li&gt;
&lt;li&gt;抛出异常：如果当前是 pending 就会变为 rejected&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;let p = new Promsie((resolve, reject) =&amp;gt; {
    // resolve
    resolve(&apos;OK&apos;);
    // reject
    reject(&apos;error&apos;);
    // throw exception
    throw &quot;Exception!&quot;;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;2、指定多个成功/失败回调&lt;/h3&gt;
&lt;p&gt;​	一个 Promise 指定多个成功/失败回调，当promise改变为对应状态时&lt;strong&gt;都会调用&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p = new Promise((resolve, reject) =&amp;gt; {
    resolve(&apos;OK&apos;);
})
p.then(value =&amp;gt; {
    console.log(value+&apos;!&apos;);
})
p.then(value =&amp;gt; {
    console.log(value+&apos;!!&apos;);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;3、改变状态与指定回调谁先谁后&lt;/h3&gt;
&lt;p&gt;​	都有可能。但一般执行器内是异步任务，因此一般是先指定回调。&lt;/p&gt;
&lt;p&gt;​	若要先改变状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行器内直接改变状态&lt;/li&gt;
&lt;li&gt;延迟更长时间再调用 &lt;code&gt;then()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	得到数据的时间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果先指定的回调，那当状态发生改变时，回调函数就会调用，得到数据。&lt;/li&gt;
&lt;li&gt;如果先改变的状态，那&lt;strong&gt;当指定回调时，回调函数就会调用，得到数据。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;4、then() 方法返回结果&lt;/h3&gt;
&lt;p&gt;​	主要有三种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let res = p.then(value =&amp;gt; {
    // 1. throw exception，那么失败，结果是异常
    throw &apos;An exception!&apos;;
    // 2. 返回结果是非 Promise 对象，那么成功，结果是该值
    return 521;
    // 3. 返回结果是 Promise 对象，那么成功和返回值都由该对象决定
    return new Promise((resolve, reject) =&amp;gt; {
        // resolve(&apos;OK&apos;);
        reject(&apos;error&apos;);
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;5、Promise 串联任务&lt;/h3&gt;
&lt;p&gt;​	比如串联两个异步任务与一个同步任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p = new Promise((resolve, reject) =&amp;gt; {
    // 第一个异步 Promise
    setTimeout(() =&amp;gt; {
        resolve(&apos;First OK&apos;);
    }, 1000);
}).then(value =&amp;gt; {
    // 成功后告知第一个异步 Promise 完成，输出 &apos;First OK&apos;
    console.log(value);
    // 回调第二个异步 Promise
    return new Promise((resolve, reject) =&amp;gt; {
        setTimeout(() =&amp;gt; {
            resolve(&apos;Second OK&apos;);
        }, 2000);
    })
}).then(value =&amp;gt; {
    // 成功后告知第二个异步 Promise 完成，输出 &apos;Second OK&apos;
    console.log(value);
    // 该回调返回成功 Promise
}).then(value =&amp;gt; {
    // 第三个 Promise 回调开始，输出 undefined
    console.log(value);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;6、Promise 异常穿透&lt;/h3&gt;
&lt;p&gt;​	Promise 链中任意层的失败可以穿透至最后层，而被捕获的现象。&lt;/p&gt;
&lt;p&gt;​	如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 中间任意层可以不指定失败捕获
new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
        resolve(&apos;OK&apos;);
    }, 1000);
}).then(value =&amp;gt; {
    throw &apos;Second layer exception!&apos;;
}).then(value =&amp;gt; {
    console.log(3);
}).then(value =&amp;gt; {
    console.log(4);
}).catch(reason =&amp;gt; {
    console.err(reason);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;7、中断 Promise 链&lt;/h3&gt;
&lt;p&gt;​	有且只有一个方法：返回一个 pending 状态的 Promise 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
.then(value =&amp;gt; {
    ...
    return new Promise(() =&amp;gt; {});
})
...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、async 与 await&lt;/h2&gt;
&lt;h3&gt;1、async 函数&lt;/h3&gt;
&lt;p&gt;​	async 函数的返回值为 Promise 对象，且其结果同样由函数执行的返回值决定。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2、await 表达式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;await 右侧的表达式一般为 promise 对象，但也可以是其它的值&lt;/li&gt;
&lt;li&gt;如果表达式是 promise 对象，await 返回的是 promise 成功的值&lt;/li&gt;
&lt;li&gt;如果表达式是其它值，直接将此值作为 await 的返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	注：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;await 必须写在 async 函数中，但 async 函数中可以没有 await&lt;/li&gt;
&lt;li&gt;如果 await 的 Promise 失败了，就会抛出异常，需要捕获&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;3、结合使用的例子&lt;/h3&gt;
&lt;p&gt;​	异步依次读取文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&apos;fs&apos;);
const util = require(&apos;util&apos;);
const mineReadFile = util.promisify(fs.readFile)

!async function main() {
    try {
        let data1 = await mineReadFile(&apos;./1.txt&apos;);
        let data2 = await mineReadFile(&apos;./2.txt&apos;);
        let data3 = await mineReadFile(&apos;./3.txt&apos;);
        console.log(data1 + data2 + data3);
    }
    catch(e) {
        console.warn(e);
    }
}();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Electron 学习笔记（常用功能记录）</title><link>https://miku.top/posts/electron-learning/</link><guid isPermaLink="true">https://miku.top/posts/electron-learning/</guid><description>Electron，使用 JS、H5C3 构建跨平台桌面应用</description><pubDate>Thu, 31 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Electron 学习笔记（常用功能记录）&lt;/h1&gt;
&lt;h2&gt;一、简介&lt;/h2&gt;
&lt;p&gt;​	&lt;code&gt;Electron&lt;/code&gt; = &lt;code&gt;Chromium&lt;/code&gt; + &lt;code&gt;Node.js&lt;/code&gt; + &lt;code&gt;Native API&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Hello World 示例&lt;/h2&gt;
&lt;p&gt;​	写好 &lt;code&gt;index.html&lt;/code&gt; 后，写一个 &lt;code&gt;main.js&lt;/code&gt; 作主进程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var electron = require(&apos;electron&apos;);

var app = electron.app;
var BrowserWindow = electron.BrowserWindow;
var mainWindow = null;

app.on(&apos;ready&apos;, () =&amp;gt; {
    mainWindow = new BrowserWindow({width: 800, height: 800});
    mainWindow.loadFile(&apos;index.html&apos;);
    mainWindow.on(&apos;closed&apos;, () =&amp;gt; {
        mainWindow = null;
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、渲染进程和本地文件读取&lt;/h2&gt;
&lt;p&gt;​	渲染进程：&lt;/p&gt;
&lt;p&gt;​	使用渲染进程，则初始化 &lt;code&gt;mainWindow&lt;/code&gt; 时，需要添加一个参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mainWindow = new BrowserWindow({
    width: 800, 
    height: 800,
    webPreferences: {nodeIntegration: true}
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	读取文件的渲染进程写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var fs = require(&apos;fs&apos;);
window.onload = function() {
    fs.readFile(&apos;xxx.txt&apos;, (err, data) =&amp;gt; {
        // action
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;四、Remote 模块&lt;/h2&gt;
&lt;p&gt;​	使用 remote 模块可以让渲染进程使用主进程的方法，如新建页面。&lt;/p&gt;
&lt;p&gt;​	先进行安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save @electron/remote
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;code&gt;main.js&lt;/code&gt; 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
const remote = require(&apos;@electron/remote/main&apos;)
remote.initialize(); // 初始化
...

app.on(&apos;ready&apos;, () =&amp;gt; {
    ...
    remote.enable(mainWindow.webContents);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;code&gt;render.js&lt;/code&gt; 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const {BrowserWindow} = require(&quot;@electron/remote&quot;);

window.onload = function() {
    btn.onclick = () =&amp;gt; {
        newWin = new BrowserWindow({
            width: 500,
            height: 500
        })
        newWin.on(&apos;closed&apos;, function() {
            newWin = null;
        })
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、创建主菜单&lt;/h2&gt;
&lt;p&gt;​	新建一个 &lt;code&gt;menu.js&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;​	注：&lt;code&gt;menu.js&lt;/code&gt; 的功能在主进程中完成，因此&lt;strong&gt;不需要 remote&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { Menu } = require(&apos;electron&apos;);
var template = [
    {
        label: &apos;Outer Menu1&apos;,
        submenu: [
            {
                label: &apos;Inner Menu11&apos;,
                // 绑定快捷键
                accelerator: &apos;ctrl+n&apos;,
                // 绑定点击事件
                click: () =&amp;gt; {
                    ...
                }
            },
            {label: &apos;Inner Menu12&apos;},
        ]
    },
    {
        label: &apos;Outer Menu2&apos;,
        submenu: [
            {label: &apos;Inner Menu21&apos;},
            {label: &apos;Inner Menu22&apos;},
        ]
    }
]

var m = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(m);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、创建右键菜单&lt;/h2&gt;
&lt;p&gt;​	在主 html 引用的 js 中绑定右键事件。&lt;/p&gt;
&lt;p&gt;​	注：由于 html 属于渲染进程，因此此处&lt;strong&gt;要使用 remote&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 引用 js 中：
const { BrowserWindow, Menu, getCurrentWindow } = require(&quot;@electron/remote&quot;);

...
//固定写法：
var rightMenuTemplate = [
    {label: &apos;复制&apos;, accelerator: &apos;ctrl+c&apos;},
    {label: &apos;粘贴&apos;, accelerator: &apos;ctrl+v&apos;},
]

var m = Menu.buildFromTemplate(rightMenuTemplate);
window.addEventListener(&apos;contextmenu&apos;, function(e) {
    e.preventDefault();
    m.popup({window: getCurrentWindow()});
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、通过链接打开浏览器&lt;/h2&gt;
&lt;p&gt;​	使用 shell 可以完成该需求。&lt;/p&gt;
&lt;p&gt;​	对应 js 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { shell } = require(&apos;electron&apos;);

var theLink = document.querySelector(&apos;#website&apos;);
theLink.onclick = function(e) {
    e.preventDefault();
    shell.openExternal(this.getAttribute(&apos;href&apos;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;八、嵌入网页与打开子窗口&lt;/h2&gt;
&lt;p&gt;​	嵌入网页（在 &lt;code&gt;main.js&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { BrowserView } = require(&apos;electron&apos;);

// 以 BrowserView 形式嵌入子网页
var view = new BrowserView();
mainWindow.setBrowserView(view);
view.setBounds({ x: 0, y: 100, width: 700, height: 500 });
view.webContents.loadURL(&apos;https://example.com&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	传统 html 打开新窗口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 打开新窗口
var theLink = document.querySelector(&apos;#website&apos;);

var linkBtn = document.querySelector(&apos;#website-tab&apos;);
linkBtn.onclick = function() {
    window.open(theLink.getAttribute(&apos;href&apos;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;九、父子窗口通信&lt;/h2&gt;
&lt;p&gt;​	这里涉及的都是传统的 js 方法&lt;/p&gt;
&lt;p&gt;​	特别注意：使用 &lt;code&gt;window.open()&lt;/code&gt; 创建的子窗口&lt;strong&gt;方法受限&lt;/strong&gt;，无法使用诸如 &lt;code&gt;openDevtools()&lt;/code&gt; 等功能。&lt;/p&gt;
&lt;p&gt;​	子窗口 js：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var popbtn = window.document.querySelector(&apos;#popbtn&apos;);
popbtn.onclick = function(e) {
    window.opener.postMessage(&apos;这是一条来自子窗口的信息&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	父窗口 js：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&apos;message&apos;, (msg) =&amp;gt; {
    let mytext = document.querySelector(&apos;#mytext&apos;);
    // 注意 msg 是一个 json 对象
    mytext.innerHTML = JSON.stringify(msg.data);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十、选择文件对话框&lt;/h2&gt;
&lt;p&gt;​	使用 &lt;code&gt;dialog.showOpenDialog()&lt;/code&gt; 方法来实现。有两个参数，一个是基本属性，一个是回调。&lt;/p&gt;
&lt;p&gt;​	注意：这里的对话框并不会提供文件载入功能，只是让用户确定打开的文件路径。&lt;/p&gt;
&lt;p&gt;​	基本参数有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;title&lt;/li&gt;
&lt;li&gt;defaultPath&lt;/li&gt;
&lt;li&gt;buttonLabel&lt;/li&gt;
&lt;li&gt;filters&lt;/li&gt;
&lt;li&gt;properties（打开文件的属性）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 渲染进程中使用：
const { dialog } = require(&apos;@electron/remote&apos;);

fileBtn.onclick = function() {
    dialog.showOpenDialog({
        // 参数列表
        title: &apos;选择存档~&apos;,
        defaultPath: &apos;desktop&apos;,
        filters:[
            {name: &apos;img&apos;, extensions:[&apos;jpg&apos;, &apos;png&apos;, &apos;gif&apos;]},
            {name: &apos;text file&apos;, extensions:[&apos;c&apos;, &apos;cpp&apos;, &apos;py&apos;, &apos;txt&apos;, &apos;config&apos;]},
        ],
        buttonLabel: &apos;载入存档&apos;
    }).then(result =&amp;gt; {
        // 打开之后的操作
        console.log(result);
        ...
    }).catch(err =&amp;gt; {
        // 异常处理
        console.error(err);
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十一、保存文件对话框&lt;/h2&gt;
&lt;p&gt;​	使用 &lt;code&gt;showSaveDialog()&lt;/code&gt; 方法，与 &lt;code&gt;showOpenDialog()&lt;/code&gt; 用法基本一致。&lt;/p&gt;
&lt;p&gt;​	特别注意：触发对话框的元素&lt;strong&gt;最好不要被遮挡&lt;/strong&gt;，否则可能会出现不可预知的崩溃！&lt;/p&gt;
&lt;p&gt;​	保存文件可搭配 fs 模块使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&apos;fs&apos;);
fs.writeFileSync(path, string);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十二、消息对话框操作&lt;/h2&gt;
&lt;p&gt;​	使用 &lt;code&gt;showMessageBox&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;​	参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;type：有 warning、info、question 和 error&lt;/li&gt;
&lt;li&gt;title：标题&lt;/li&gt;
&lt;li&gt;message：消息内容&lt;/li&gt;
&lt;li&gt;buttons：提供的选项（字符串列表）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const { dialog } = require(&apos;@electron/remote&apos;);

infoBtn.onclick = function() {
    dialog.showMessageBox({
        type: &apos;info&apos;,
        title: &apos;信息。&apos;,
        message: &apos;只是一条提示信息。&apos;,
        buttons: [&apos;哦？&apos;, &apos;啊这...&apos;],
    }).then(result =&amp;gt; {
        console.log(result)
    }).catch(err =&amp;gt; {
        console.error(err);
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十三、断网提醒功能&lt;/h2&gt;
&lt;p&gt;​	原生 html 事件监听实现实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&apos;online&apos;, function() {
    alert(&apos;又有网络了，好耶！&apos;);
})
window.addEventListener(&apos;offline&apos;, function() {
    alert(&apos;好像断线了哦...&apos;);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十四、底部消息通知&lt;/h2&gt;
&lt;p&gt;​	原生 html 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var notifyBtn = document.querySelector(&apos;#notify-btn&apos;);
var option = {
    title: &apos;这是通知的标题&apos;,
    body: &apos;这是通知的内容&apos;,
};
notifyBtn.onclick = function() {
    new window.Notification(option.title, option);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十五、注册全局快捷键&lt;/h2&gt;
&lt;p&gt;​	写在主进程中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { globalShortcut } = require(&apos;electron&apos;);

app.whenReady().then(() =&amp;gt; {
    ...
	// 全局快捷键
    globalShortcut.register(&apos;ctrl+g+l&apos;, function() {
        view.webContents.loadURL(&apos;https://bilibili.com&apos;);
    })
    // 检测是否注册成功
    let isReisterMsg = globalShortcut.isRegistered(&apos;ctrl+g+l&apos;)?true:false;
    console.log(isReisterMsg);
    ...
})

// 注销全局快捷键
app.on(&apos;will-quit&apos;, () =&amp;gt; {
    globalShortcut.unregister(&apos;ctrl+g+l&apos;);
    globalShortcut.unregisterAll();
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;十六、剪贴板功能&lt;/h2&gt;
&lt;p&gt;​	使用 clipboard 模块的 &lt;code&gt;writeText()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { clipboard } = require(&apos;electron&apos;);

var copyBtn = document.querySelector(&apos;#copy-btn&apos;);
// 除 writetext，也可以用其他方法实现复制图片等操作
copyBtn.onclick = function() {
    clipboard.writeText(&apos;这是你复制的文本！~&apos;);
    alert(&apos;复制成功！&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>AJAX 技术学习记录</title><link>https://miku.top/posts/ajax-note/</link><guid isPermaLink="true">https://miku.top/posts/ajax-note/</guid><description>AJAX —— 不刷新网页情况下的数据交互</description><pubDate>Mon, 01 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;AJAX 技术&lt;/h1&gt;
&lt;h2&gt;一、原生 AJAX 技术&lt;/h2&gt;
&lt;h3&gt;1、AJAX 简介&lt;/h3&gt;
&lt;p&gt;​	通过它可以在浏览器中向服务器发送异步请求，最大的优势：&lt;strong&gt;无刷新获取数据&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;2、xml 标记语言&lt;/h3&gt;
&lt;p&gt;​	xml 早期是 AJAX 早期传输数据的一种格式，现在很多情况下已被 JSON 替代。&lt;/p&gt;
&lt;h3&gt;3、AJAX 的特点&lt;/h3&gt;
&lt;p&gt;​	（1）优点：&lt;/p&gt;
&lt;p&gt;a. 无需刷新页面即可与服务端通信&lt;/p&gt;
&lt;p&gt;b. 允许根据用户事件来更新网页内容&lt;/p&gt;
&lt;p&gt;​	（2）缺点：&lt;/p&gt;
&lt;p&gt;a. 没有浏览历史，无法回退&lt;/p&gt;
&lt;p&gt;b. 存在跨域问题（要在同源下）&lt;/p&gt;
&lt;p&gt;c. SEO 不友好&lt;/p&gt;
&lt;h3&gt;4、HTTP 请求与响应报文结构&lt;/h3&gt;
&lt;p&gt;​	下面主要讨论格式与参数。&lt;/p&gt;
&lt;p&gt;​	HTTP 请求报文：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;行：请求类型 / 查询字符串 / HTTP 协议版本
头：host、cookie、user-agent...
&amp;lt;空行&amp;gt;
体：（当为 POST 请求时，请求体不能为空）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	HTTP 响应报文：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;行：HTTP/1.1   200   OK
头：content-type、content-length...
&amp;lt;空行&amp;gt;
体：示例：（&amp;lt;html&amp;gt;...&amp;lt;/html&amp;gt;）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、一些测试辅助框架的安装和使用&lt;/h3&gt;
&lt;p&gt;​	在工作目录下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# express 框架可提供服务端环境
npm init --yes
npm i exprees
# 使用：
node [服务端路由文件].js

# nodemon 框架可提供 node 脚本的实时刷新重启
npm install g nodemon
# 使用：
nodemon [服务端路由文件].js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	服务端路由文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 引入
const express = require(&apos;express&apos;);

// 创建应用对象
const app = express();

// 创建路由规则
// request 是对请求报文的封装
// response 是对响应报文的封装
app.get(&apos;/&apos;, (request, response) =&amp;gt; {
    response.send(&apos;HELLO EXPRESS&apos;);
})

// 监听端口启动服务
app.listen(5000, () =&amp;gt; {
    console.log(&apos;服务已经启动，运行在 5000 端口。&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、AJAX 请求基本操作&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;btn.onclick = function() {
    // 创建对象
    const xhr = new XMLHttpRequest();
    
    // 初始化，设置请求方法和 url
    xhr.open(&apos;GET&apos;, &apos;http://127.0.0.1:8000/server&apos;);
    xhr.send();
    
    // 事件绑定
    // readystate 是 xhr 对象中的属性，表示状态 0 1 2 3 4
    // 分别对应：
    
    /* 初始状态、
    open 调用完毕、
    send 方法调用完毕、
    服务端返回了部分结果
    服务端返回了所有结果 */
    
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status &amp;gt;= 200 &amp;amp;&amp;amp; xhr.status &amp;lt; 300) {
                // 正常响应区间
                // handle...
            }
            else {
                // handle...
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	以下是 &lt;code&gt;xhr&lt;/code&gt; 对象常用的属性、方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xhr.status						// 状态码
xhr.statusText					// 状态字符串
xhr.getAllResponseHeaders()		// 所有响应头
xhr.response					// 响应体
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、AJAX 设置请求参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var query = {
    a: 100,
    b: 200
}
var base_url = &apos;http://127.0.0.1:5000&apos;;

// 处理参数函数，拼接 url
var process_query = (url, query) =&amp;gt; {
    url += &apos;?&apos;;
	for (var k in query) {
    	url += k + &apos;=&apos; + query[k];
    	url += &apos;&amp;amp;&apos;;
	}
    url = url.slice(0, url.length - 1);
    return url
}
var url = process_query(base_url, query);

xhr.open(url);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6、AJAX 发送 POST 请求&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;xhr.open(&apos;POST&apos;, &apos;http://127.0.0.1:5000&apos;);

// 设置请求参数
var formdata = new formData();
formdata.append(&apos;a&apos;, &apos;100&apos;);
formdata.append(&apos;b&apos;, &apos;200&apos;);

xhr.send(formdata);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7、AJAX 设置请求头信息&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 设置预定义的请求头
xhr.setRequestHeaders(&apos;Content-Type&apos;, &apos;application/x-www-form-urlencoded&apos;);

// 设置自定义的请求头
xhr.setRequestHeaders(&apos;name&apos;, &apos;AiCorein&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8、AJAX 服务端响应 JSON 数据&lt;/h3&gt;
&lt;p&gt;​	封装和解析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Server:
...
const data = {
    name: &apos;melodyEcho&apos;;
}
let str = JSON.stringify(data);
response.send(str);
...

// Client:（手动转换方式）
...
let data = JSON.parse(xhr.response);
console.log(data);
...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 自动转换:
xhr.responseType = &apos;json&apos;;
let data = xhr.response;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9、AJAX-IE 缓存问题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 添加参数即可
xhr.open(&apos;GET&apos;, &apos;http://127.0.0.1&apos; + Date.now());
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10、AJAX 超时与网络异常处理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 设置超时时长
xhr.timeout = 2000;


// 设置超时回调
xhr.ontimeout = function() {
    // handle...


// 网络异常回调
xhr.onerror = function() {
    // handle...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;11、AJAX 请求取消&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 取消：
xhr.abort();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;12、AJAX 请求重复发送问题&lt;/h3&gt;
&lt;p&gt;​	可以通过设置一个 &quot;正在发送请求&quot; 标志来避免请求的短时间重复发送。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let isSending = false;
let xhr = null;

btns[0].onclick = function() {
    if (isSending) xhr.abort();
    xhr = new XMLHttpRequest();
    isSending = true;
    xhr.open(...);
    xhr.send(...);
    xhr.onreadystatechange = function() {
        if (xhr.readystate == 4) {
            isSending = false;
            ...
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、jQuery 发送 AJAX 请求&lt;/h2&gt;
&lt;h3&gt;1、jQuery 发送 GET、POST 请求&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// get 方法
$.get(&apos;http://127.0.0.1:8000/get&apos;, {a: 100, b: 200}, function() {
    console.log(data);
})

// post 方法
$.get(&apos;http://127.0.0.1:8000/post&apos;, {a: 100, b: 200}, function() {
    console.log(data);
}, &apos;json&apos;);		// 指定返回数据类型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	更多详细细节：&lt;a href=&quot;https://www.runoob.com/jquery/ajax-get.html&quot;&gt;$.get() 方法&lt;/a&gt;, &lt;a href=&quot;https://www.runoob.com/jquery/ajax-post.html&quot;&gt;$.post() 方法&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;2、jQuery 发送请求通用方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$.ajax({
    url: &apos;http://127.0.0.1:8000/server&apos;,
    data: {a: 100, b: 200},
    type: &apos;GET&apos;,
    dataType: &apos;json&apos;,
    success: function(data) {
        console.log(data);
    },
    timeout: 2000,
    error: function() {
        console.log(&apos;Error&apos;);
    },
    // 注：自定义请求头需要 Server 端有对应的
    // Access-Control-Allow-Headers 策略
    headers: {
        c: 300,
        d: 400
    }
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>jQuery 常用方法</title><link>https://miku.top/posts/jquery-note/</link><guid isPermaLink="true">https://miku.top/posts/jquery-note/</guid><description>jQuery —— 前端使用最为广泛的 JS 库</description><pubDate>Mon, 01 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;jQuery&lt;/h2&gt;
&lt;h4&gt;1、javascript 库&lt;/h4&gt;
&lt;p&gt;​	javascript 库即 library，是一个封装好的特定的集合（方法和函数）。从封装一大堆函数的角度理解库，就是在这个库中，封装了许多预先定义好的函数。&lt;/p&gt;
&lt;p&gt;​	常见的 javascript 库：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jQuery&lt;/li&gt;
&lt;li&gt;Prototype&lt;/li&gt;
&lt;li&gt;YUI&lt;/li&gt;
&lt;li&gt;Dojo&lt;/li&gt;
&lt;li&gt;Ext js&lt;/li&gt;
&lt;li&gt;移动端的 zepto&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2、jQuery概述&lt;/h4&gt;
&lt;p&gt;​	优点：轻量，跨浏览器兼容，链式编程、隐式迭代，扩展性强等。&lt;/p&gt;
&lt;h4&gt;3、jQuery 入口函数&lt;/h4&gt;
&lt;p&gt;​	等待页面 DOM 加载完毕再执行 js 代码，相当于原生 js 中的 &lt;code&gt;DOMContentLoaded&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(document).ready(function() {
    ...
})

// 或

$(function() {
    ...
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、jQuery 顶级对象&lt;/h4&gt;
&lt;p&gt;​	（1）$ 是 jQuery 的别称，在代码汇总可以使用 jQuery 代替 $&lt;/p&gt;
&lt;p&gt;​	（2）$ 是 jQuery的顶级对象，相当于原生 js 中的 window&lt;/p&gt;
&lt;h4&gt;5、jQuery 对象和 DOM 对象&lt;/h4&gt;
&lt;p&gt;​	DOM 对象：用原生 js 获取的对象&lt;/p&gt;
&lt;p&gt;​	jQuery对象：用 jQuery 获取的对象，本质是对 DOM 对象的封装&lt;/p&gt;
&lt;p&gt;​	注：&lt;strong&gt;两种不同的对象只能使用各自对应的属性和方法&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;6、jQuery 对象和 DOM 对象的互相转化&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var div = document.querySelector(&apos;div&apos;);
$(div).hide();	// 此时就已经转化为 jQuery 对象


// 使用索引转化为 DOM 对象
$(&apos;video&apos;)[0].play();
$(&apos;video&apos;).get(0).play();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、jQuery 基本和层级选择器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;选择器&apos;) // 填入 CSS 选择器即可
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、jQuery 隐式迭代&lt;/h4&gt;
&lt;p&gt;​	遍历内部 DOM 元素（伪数组存储）的过程就叫做&lt;strong&gt;隐式迭代&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	简单理解：给匹配到的所有元素进行循环遍历，执行相应的方法，而不用手动再进行循环，简化我们的操作，方便我们调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用 jq 获取多个 div 元素
var divs = $(&apos;div&apos;);

// 将它们的背景都设为天蓝色，这里就是隐式迭代
divs.css(&apos;background&apos;, &apos;pink&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9、jQuery 筛选选择器&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;用法&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:first&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;li:first&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取第一个 li 元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:last&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;li:last&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取最后一个 li 元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:eq(index)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;li:eq(index)&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取到的 li 元素中，选择索引为 2 的元素，索引从 0 开始&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:odd&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;li:odd&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取到的 li 元素中，选择索引号为奇数的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:even&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;li:even&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取到的 li 元素中，选择索引号为偶数的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:checked&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;input:checked&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取到的 input 元素中，选择被选择的表单元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;10、jQuery 筛选方法&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;用法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parent()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;li&apos;).parent()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查找父级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;children(selector)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;li&apos;).children()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相当于$(&apos;ul&amp;gt;li&apos;)，最近一级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find(selector)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;ul&apos;).find(&apos;li&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相当于$(&apos;ul li&apos;)，后代选择器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;siblings(selector)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;.first&apos;).siblings(&apos;li&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查找兄弟节点，不包括自己&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nextAll([selector])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;.first&apos;).nextAll()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查找当前元素之前的同辈元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prevAll([selector])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;.last&apos;).prevAll()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查找当前元素之后的同辈元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hasClass(className)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;div&apos;).hasClass(&apos;protected&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查当前的元素是否含有某个特定的类，如果有，返回 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eq(index)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;li&apos;).eq(2)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相当于$(&apos;li:eq(2)&apos;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parents()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(&apos;div&apos;).parents()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回指定选择器条件的父级，无参数返回所有父级&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;​	注：更推荐使用 &lt;code&gt;eq(index)&lt;/code&gt; 的写法，因为 index 为变量时不需要字符串转化&lt;/p&gt;
&lt;p&gt;​	注：&lt;strong&gt;获取元素当前索引号可使用 &lt;code&gt;$(this).index()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;11、jQuery 链式编程&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;$(this).css(&apos;color&apos;, &apos;blue&apos;);
$(this).siblings().css(&apos;color&apos;, &apos;&apos;);

// 可简写为：

$(this).css(&apos;color&apos;, &apos;blue&apos;).siblings().css(&apos;color&apos;, &apos;&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;12、jQuery 样式操作&lt;/h4&gt;
&lt;h5&gt;（1）操作 CSS 方法&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 参数只写属性名，则返回值
returnVal = $(this).css(&apos;color&apos;);

// 参数是属性名，属性值；则是设置属性
$(this).css(&apos;color&apos;, &apos;blue&apos;);

// 参数可以是对象形式，可设置多组样式
$(this).css({
    backgroundColor: &quot;skyblue&quot;,
    &quot;font-size&quot;: &quot;20px&quot;,
    width: 200,
    height: 200,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;（2）设置类样式&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;div&apos;).addClass(&apos;current&apos;);		// 添加类名

$(&apos;div&apos;).removeClass(&apos;current&apos;);	// 删除类名

$(&apos;div&apos;).toggleClass(&apos;current&apos;);	// 切换类，无则添加，有则删除
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：jQuery 的类操作与原生 JS 的不同在于：&lt;strong&gt;只是对指定类操作，不会影响其他类名&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;13、jQuery 效果&lt;/h4&gt;
&lt;h5&gt;（1）显示隐藏效果&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 显示效果语法规范，其他类似
$(&apos;div&apos;).show([speed, [easing], [callback]]);

...
$(&apos;div&apos;).hide(...)
$(&apos;div&apos;).toggle(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		注：参数都可省略，即无动画直接显示&lt;/p&gt;
&lt;p&gt;​			a. speed：三种预定速度之一的字符串（&quot;show&quot;，&quot;normal&quot;，&quot;fast&quot;）或动画时长的毫秒数值（如：1000）&lt;/p&gt;
&lt;p&gt;​			b. easing：（Optional）用来指定切换效果，默认为 &quot;swing&quot;，可用参数 &quot;linear&quot;&lt;/p&gt;
&lt;p&gt;​			c. callback：回调函数，动画完成时执行的函数，每个元素执行一次。&lt;/p&gt;
&lt;h5&gt;（2）滑动效果&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;div&apos;).slideUp(speed, callback);

...
$(&apos;div&apos;).slideDown(speed, callback);
$(&apos;div&apos;).slideToggle(speed, callback);

// 参数含义与显示隐藏效果类似
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;（3）事件切换&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 此时经过触发 fn1, 离开触发 fn2
$(&apos;div&apos;).hover(fn1, fn2);

// 此时经过和离开都只触发 fn
$(&apos;div&apos;).hover(fn);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;（4）停止动画排队函数&lt;/h5&gt;
&lt;p&gt;​	a. 动画或动画队列及其停止排队方法&lt;/p&gt;
&lt;p&gt;​			动画或者效果一旦触发就会执行，如果短时间内触发多个动画或效果，将会造成它们的同时执行。&lt;/p&gt;
&lt;p&gt;​			解决方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;div&apos;).stop()...[后面跟动画效果]

// ep:
$(&apos;div&apos;).hover(function() {
    $(this).children(&apos;ul&apos;).stop().slideToggle(200);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​			关于 &lt;code&gt;stop()&lt;/code&gt; 函数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;stopAll&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;可选。规定是否停止被选元素的所有加入队列的动画。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;goToEnd&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;可选。规定是否允许完成当前的动画。该参数只能在设置了 stopAll 参数时使用。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;​			对应四种情况：&lt;/p&gt;
&lt;p&gt;​				&lt;code&gt;stop(true)&lt;/code&gt;等价于&lt;code&gt;stop(true,false)&lt;/code&gt;:  停止被选元素的所有（包括当前动画）加入队列的动画。&lt;/p&gt;
&lt;p&gt;​				&lt;code&gt;stop(true,true)&lt;/code&gt;:  停止被选元素的所有加入队列的动画，但允许完成当前动画。&lt;/p&gt;
&lt;p&gt;​				&lt;code&gt;stop()&lt;/code&gt;等价于&lt;code&gt;stop(false,false)&lt;/code&gt;:  停止被选元素当前的动画，但允许完成以后队列的所有动画。&lt;/p&gt;
&lt;p&gt;​				&lt;code&gt;stop(false,true)&lt;/code&gt;:  立即结束&lt;em&gt;&lt;strong&gt;当前的动画&lt;/strong&gt;&lt;/em&gt; 到最终效果，然后完成&lt;em&gt;&lt;strong&gt;以后队列&lt;/strong&gt;&lt;/em&gt; 的所以动画。&lt;/p&gt;
&lt;h5&gt;（5）淡入淡出效果&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;fadeIn([speed, easing], [callback]);
fadeOut(...);
fadeToggle(...);

fadeTo([speed], opacity, [easing], [callback]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;（6）自定义动画效果&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;animate(params, [speed], [easing], [callback]);

// 示例：
$(&apos;div&apos;).animate({
    left: 200px,
    right: 100px
}, fast, linear)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	参数说明： &lt;code&gt;params&lt;/code&gt;：想要更改的样式属性，必须写，以对象形式传递。符合属性使用驼峰命名法。&lt;/p&gt;
&lt;p&gt;​		其他参数类似上面的函数。&lt;/p&gt;
&lt;h4&gt;14、jQuery 属性操作&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 获取固有属性或设置
$(&apos;div&apos;).prop(&apos;width&apos;);
$(&apos;div&apos;).prop(&apos;width&apos;, 200);

// 获取自定义属性值
$(&apos;div&apos;).attr(&apos;index&apos;);
$(&apos;div&apos;).attr(&apos;index&apos;, 2);

// 设置元素数据缓存，注意数据缓存是不在 DOM 树中体现的
$(&apos;div&apos;).data(&apos;name&apos;, &apos;melodyEcho&apos;);

// 获取元素 h5 新 data 类属性
// 如获取 属性 data-index:
$(&apos;div&apos;).data(&apos;index&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;15、jQuery 获取内容文本值&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 获取设置元素内容，类似 innerHTML
$(&apos;div&apos;).html();
$(&apos;div&apos;).html(&apos;&amp;lt;a href=&quot;example.com&quot;&amp;gt;点击&amp;lt;/a&amp;gt;&apos;)

// 获取设置元素文本内容，类似 innerText()
$(&apos;div&apos;).text();
$(&apos;div&apos;).text(&apos;只是一段文本内容&apos;);

// 获取设置表单值
$(&apos;input&apos;).val();
$(&apos;input&apos;).val(32);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;16、jQuery 元素操作&lt;/h4&gt;
&lt;h5&gt;（1）jQuery 遍历对象 each 方法&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 写法
$(&apos;div&apos;).each(function(index, domEle) { xxx; });


// 示例：
var colorArr = [&apos;red&apos;, &apos;green&apos;, &apos;blue&apos;];
$(&apos;div&apos;).each(function(i, domEle) {
    console.log(i);	// 打印索引
    
    // 注意返回的是 dom 对象，要进行转化
    $(domEle).css(&apos;color&apos;, arr[i]);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;（2）jQuery 遍历处理数据&lt;/h5&gt;
&lt;p&gt;​			&lt;code&gt;$.each()&lt;/code&gt; 主要用于遍历元素中的数据值，如获取数组或对象中的数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 写法
$.each(object, function(index, element) { xxx });

// 示例：
// 遍历数组：
arr = [1, 2, 3]
$.each(arr, function(i , val) {
    console.log(i);
    console.log(val);
})
// 遍历对象：
$.each({
    name: &quot;melodyEcho&quot;,
    age: 18
}), function(i, val) {
    console.log(i);		// 输出属性名
    console.log(val);	// 输出属性值
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;（3）创建、添加和删除元素&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 创建元素
var span = $(&apos;&amp;lt;span&amp;gt;创建的一个 span 元素&amp;lt;/span&amp;gt;&apos;);
var p = $(&apos;&amp;lt;p&amp;gt;创建的 p 元素&amp;lt;/p&amp;gt;&apos;);

/* 添加元素 */
// 内部添加
$(&apos;div&apos;).append(span, p);
$(&apos;div&apos;).prepend(span, p);

// 外部添加（同级）
$(&apos;div&apos;).before(p, span);
$(&apos;div&apos;).after(p, span);

// 删除元素
$(&apos;div&apos;).remove();
$(&apos;ul&apos;).empty()		// 删除内部所有子节点
$(&apos;ul&apos;).html(&apos;&apos;);	// 也是删除内部所有子节点
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;17、jQuery尺寸、位置操作&lt;/h4&gt;
&lt;h5&gt;（1）尺寸&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;用法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;width() / height()&lt;/td&gt;
&lt;td&gt;只计算 width、height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;innerWidth() / innerHeight()&lt;/td&gt;
&lt;td&gt;含 padding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;outerWidth() / outerHeight()&lt;/td&gt;
&lt;td&gt;含 padding、border&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;outerWidth(true) / outerHeight(true)&lt;/td&gt;
&lt;td&gt;含 padding、border、margin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;（2）位置&lt;/h5&gt;
&lt;p&gt;​		&lt;strong&gt;1. Offset 方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;div&apos;).offset();

$(&apos;div&apos;).offset({
    top: 10,
    left: 30
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：a. offset() 方法设置或返回被选元素相对于文档的偏移坐标，跟父级没有关系&lt;/p&gt;
&lt;p&gt;​			b. 该方法有 2 个属性：left、top，可直接使用 &lt;code&gt;offset.top()&lt;/code&gt; 或 &lt;code&gt;offset.left()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​			c. 可用于设置元素偏移&lt;/p&gt;
&lt;p&gt;​		&lt;strong&gt;2. position 方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;div&apos;).position();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	注：a. 用于返回被选择元素相对于带有定位的父级偏移坐标&lt;/p&gt;
&lt;p&gt;​			b. &lt;strong&gt;不能用于设置偏移&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​		&lt;strong&gt;3. scrollTop、scrollLeft 方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(window.scrollTop());
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;18、jQuery 事件&lt;/h4&gt;
&lt;h5&gt;（1）事件注册&lt;/h5&gt;
&lt;p&gt;​		1. 单个事件注册方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这种方式一次只能注册一个事件
$(&apos;div&apos;).click(function() {
    $(this).css(&apos;backgroundColor&apos;, &apos;skyblue&apos;);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​		&lt;strong&gt;2. 使用 on() 方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;div&apos;).on(events, [selection], fn);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		&lt;code&gt;events&lt;/code&gt;：一个或多个事件分隔的事件类型，如 &quot;click keydown&quot;&lt;/p&gt;
&lt;p&gt;​		&lt;code&gt;selector&lt;/code&gt;：元素的子元素选择器&lt;/p&gt;
&lt;p&gt;​		&lt;code&gt;fn&lt;/code&gt;：回调&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传入对象参数
$(&apos;div&apos;).on({
    mouseenter: function() {
        $(this).css(&apos;background&apos;, &apos;skyblue&apos;);
    },
    click: function() {
        $(this).css(&apos;background&apos;, &apos;pink&apos;);
    }
})

// 普通方式
$(&apos;div&apos;).on(&apos;mouseenter click&apos;, function() {
    $(this).toggleClass(&apos;current&apos;);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		使用 on 方法实现事件委派（委托）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注意：事件绑定在了 ul 上，由 li 来冒泡触发
$(&apos;ul&apos;).on(&apos;click&apos;, &apos;li&apos;, function() {
    alert(&apos;你点击了 li!&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		使用 on 方法的事件委派，可对后来动态添加元素绑定事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 后来动态添加的 li 依然可以触发事件
$(&apos;ol&apos;).on(&apos;click&apos;, &apos;li&apos;, function() {
    alert(&apos;你点击了 li!&apos;)
})
$(&apos;ol&apos;).append($(&apos;&amp;lt;li&amp;gt;一个 li 元素&amp;lt;/li&amp;gt;&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		综上：on 方法的优势：&lt;strong&gt;可实现对多个事件响应的统一定义，可实现事件委派，又可通过事件委派来实现动态元素的事件。&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;（2）事件解绑&lt;/h5&gt;
&lt;p&gt;​		使用 off() 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;p&apos;).off();			// 解绑所有事件处理程序
$(&apos;p&apos;).off(&apos;click&apos;);		// 解绑点击事件
$(&apos;p&apos;).off(&apos;click&apos;, foo)	// 移除指定侦听函数的事件
$(&apos;ul&apos;).off(&apos;click&apos;, li)	// 解除以 ul 委托的，li 的事件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		事件若只触发一次，可使用 one() 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(&apos;p&apos;).one(&apos;click&apos;, function() {
    alert(&apos;Hello World!&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>HTML、CSS 及 JavaScript 基础</title><link>https://miku.top/posts/h5c3-and-js-note/</link><guid isPermaLink="true">https://miku.top/posts/h5c3-and-js-note/</guid><description>H5C3、JS —— 前端知识大厦的根基</description><pubDate>Mon, 18 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;html基础&lt;/h1&gt;
&lt;h2&gt;一、标题标签&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;...&amp;lt;/h1&amp;gt;
...
&amp;lt;h6&amp;gt;...&amp;lt;/h6&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、段落和换行标签&lt;/h2&gt;
&lt;h4&gt;1、定义一个段落&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;这是一个段落&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;文本在一个段落中会根据浏览器窗口的大小自动换行&lt;/li&gt;
&lt;li&gt;段落和段落之间保有空隙&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、文本的强制换行&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;后面有换行哦！&amp;lt;br /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它是单标签&lt;/li&gt;
&lt;li&gt;只是简单的开始新的一行，与段落不同，它不会插入垂直间距&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;三、文本格式化标签&lt;/h2&gt;
&lt;p&gt;加粗，倾斜，删除线，下划线&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;strong&amp;gt;这段文字被加粗&amp;lt;/strong&amp;gt;     &amp;lt;b&amp;gt;这段文字也是加粗的&amp;lt;/b&amp;gt;
&amp;lt;em&amp;gt;这段文字是倾斜的&amp;lt;/em&amp;gt;         &amp;lt;i&amp;gt;这段文字也是倾斜的&amp;lt;/i&amp;gt;
&amp;lt;del&amp;gt;这段文字加了删除线&amp;lt;/del&amp;gt;       &amp;lt;s&amp;gt;这段文字也加了删除线&amp;lt;/s&amp;gt;
&amp;lt;ins&amp;gt;这段文字加了下划线&amp;lt;/ins&amp;gt;       &amp;lt;u&amp;gt;这段文字也加了下划线&amp;lt;/u&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、div和span标签&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;div标签用来布局，但是一行只能放一个div，放下后就不能放其他东西，是个大盒子&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;span标签也用来布局，一行可以有多个span，是个小盒子&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
这是一个大盒子，这一行再没有其他东西
&amp;lt;/div&amp;gt;
&amp;lt;span&amp;gt;小盒子1&amp;lt;/span&amp;gt;
&amp;lt;span&amp;gt;小盒子2&amp;lt;/span&amp;gt;
&amp;lt;p&amp;gt;
上面的两个小盒子其实都在一行
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五、图像标签和路径（重点）&lt;/h2&gt;
&lt;h4&gt;1、图像标签&lt;/h4&gt;
&lt;p&gt;使用img标签定义HTML页面中的图像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;there should be the location of the source&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：src是img标签的必须属性，它用于指定图像文件的路径和名称&lt;/p&gt;
&lt;p&gt;其他一些常用属性值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;alt：图像不能显示时的替换文本&lt;/li&gt;
&lt;li&gt;title：提示文本，光标放在图片上显示的文字&lt;/li&gt;
&lt;li&gt;width：宽度&lt;/li&gt;
&lt;li&gt;height：高度&lt;/li&gt;
&lt;li&gt;border：图像的边框粗细&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：图像宽高若只修改一个，则等比例缩放&lt;/p&gt;
&lt;h4&gt;2、路径&lt;/h4&gt;
&lt;p&gt;路径采用Windows通用路径规范&lt;/p&gt;
&lt;h2&gt;六、超链接标签（重点）&lt;/h2&gt;
&lt;p&gt;通过a标签实现&lt;/p&gt;
&lt;h4&gt;1、超链接的语法格式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;跳转目标&quot; target=&quot;目标窗口的弹出方式&quot;&amp;gt;文本或图像&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个属性的作用如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;href&lt;/td&gt;
&lt;td&gt;指定链接目标的url，必填属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;target&lt;/td&gt;
&lt;td&gt;用于指定链接页面的打开方式，_self为默认值, _blank为新窗口中打开&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;2、链接分类&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;外部链接&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;https://www.baidu.com&quot;&amp;gt;百度&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;内部链接（网站内部页面之间的链接）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 直接链接内部页面名称即可 --&amp;gt;
&amp;lt;a href=&quot;index.html&quot;&amp;gt;主页&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;空链接（当没有确定链接目标时）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;#&quot;&amp;gt;空链接&amp;lt;/a&amp;gt;
&amp;lt;!-- 暂时没有链接对象时，更建议使用： --&amp;gt;
&amp;lt;a href=&quot;javascript:void(0)&quot;&amp;gt;空链接&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;下载链接（链接到一个下载地址或本地文件）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;file.zip&quot;&amp;gt;下载&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;锚点链接（点击链接快速定位到页面某个位置）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- href属性设置为#id的形式,可跳转到有该id属性的元素的位置 --&amp;gt;
&amp;lt;!-- 此处可使用CSS选择器 --&amp;gt;
&amp;lt;a href=&quot;#two&quot;&amp;gt;第二集&amp;lt;/a&amp;gt;
&amp;lt;!-- 就跳转到下面这个h3标签的位置了 --&amp;gt;
&amp;lt;h3 id=&quot;two&quot;&amp;gt;第二集详情介绍&amp;lt;/h3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;七、注释标签和特殊字符&lt;/h2&gt;
&lt;h4&gt;1、注释标签&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 这个就不用说了吧 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、特殊字符&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特殊字符&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;字符的代码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;（space空格)&lt;/td&gt;
&lt;td&gt;半角不断行空格符&lt;/td&gt;
&lt;td&gt;&amp;amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;&lt;/td&gt;
&lt;td&gt;小于号&lt;/td&gt;
&lt;td&gt;&amp;amp;lt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;大于号&lt;/td&gt;
&lt;td&gt;&amp;amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;amp;&lt;/td&gt;
&lt;td&gt;和号&lt;/td&gt;
&lt;td&gt;&amp;amp;amp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(1/2中文宽度空格)&lt;/td&gt;
&lt;td&gt;半角空格符&lt;/td&gt;
&lt;td&gt;&amp;amp;ensp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(中文宽度空格)&lt;/td&gt;
&lt;td&gt;全角空格符&lt;/td&gt;
&lt;td&gt;&amp;amp;emsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;八、表格标签&lt;/h2&gt;
&lt;h4&gt;1、表格的作用&lt;/h4&gt;
&lt;p&gt;用于显示和展示数据&lt;/p&gt;
&lt;h4&gt;2、表格的基本语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
  &amp;lt;td&amp;gt;单元格数据&amp;lt;/td&amp;gt;
    ...
&amp;lt;/tr&amp;gt;
...
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、表头单元格标签&lt;/h4&gt;
&lt;p&gt;特点：表头单元格文字会加粗居中显示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
  &amp;lt;th&amp;gt;姓名&amp;lt;/th&amp;gt;
    ...
&amp;lt;/tr&amp;gt;
...
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、表格属性（不常用，因为一般使用CSS调整）&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;align&lt;/td&gt;
&lt;td&gt;left、center、right&lt;/td&gt;
&lt;td&gt;规定表格相对于周围元素的对齐方式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;border&lt;/td&gt;
&lt;td&gt;1或&quot;&quot;&lt;/td&gt;
&lt;td&gt;规定表格单元是否拥有边框，默认为&quot;&quot;，表示没有边框&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cellpadding&lt;/td&gt;
&lt;td&gt;像素值&lt;/td&gt;
&lt;td&gt;规定单元边沿与内容之间的距离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cellspacing&lt;/td&gt;
&lt;td&gt;像素值&lt;/td&gt;
&lt;td&gt;规定单元格之间的空白，默认2像素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;width&lt;/td&gt;
&lt;td&gt;像素值或百分比&lt;/td&gt;
&lt;td&gt;规定表格的宽度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;像素值或百分比&lt;/td&gt;
&lt;td&gt;规定表格的高度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;5、表格结构标签（为更好的表示语义使用）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;thead&amp;gt;&amp;lt;/thead&amp;gt;    &amp;lt;!-- 用于定义表格的头部 --&amp;gt;
&amp;lt;tbody&amp;gt;&amp;lt;/tbody&amp;gt;    &amp;lt;!-- 用于定义表格的主体，主要用于放数据主体 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、合并单元格&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先确定是跨行合并还是跨列合并&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;找到目标单元格，写上合并代码（数字为合并几个单元格）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;td colspan=&quot;2&quot;&amp;gt;&amp;lt;/td&amp;gt;    &amp;lt;!-- 跨列合并，基准为左 --&amp;gt;
&amp;lt;tc rowspan=&quot;2&quot;&amp;gt;&amp;lt;/tc&amp;gt;    &amp;lt;!-- 跨行合并，基准为上 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;删除多余的单元格&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;九、列表标签（重要）&lt;/h2&gt;
&lt;p&gt;列表：用来布局&lt;/p&gt;
&lt;h4&gt;1、无序列表（使用ul标签定义，使用li定义列表项）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;列表项1&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;列表项2&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;列表项3&amp;lt;/li&amp;gt;
...
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;无序列表之间各项没有顺序级别之分，是并列的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;ul&amp;gt;&amp;lt;/ul&amp;gt;只能直系嵌套&amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;，在&amp;lt;ul&amp;gt;&amp;lt;/ul&amp;gt;中直接嵌套其他标签是不被允许的。但可以把其他标签放入&amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;中&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;相当于一个容器，可以容纳所有元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、有序列表（使用ul标签定义，使用li定义列表项）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ol&amp;gt;
&amp;lt;li&amp;gt;列表项1&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;列表项2&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;列表项3&amp;lt;/li&amp;gt;
...
&amp;lt;/ol&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;ol&amp;gt;&amp;lt;/ol&amp;gt;只能直系嵌套&amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;，在&amp;lt;ol&amp;gt;&amp;lt;/ol&amp;gt;中直接嵌套其他标签是不被允许的。但可以把其他标签放入&amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;中&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;相当于一个容器，可以容纳所有元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有序列表会带有自己的样式属性，实际使用中会使用CSS调整&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;给li去圆点的方法:style=&quot;list-style: none;&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、自定义列表（常用于对术语或名词进行解释和描述，列表项前没有任何项目符号）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dl&amp;gt;
&amp;lt;dt&amp;gt;名词1&amp;lt;/dt&amp;gt;
&amp;lt;dd&amp;gt;对名词1的解释或扩展1&amp;lt;/dd&amp;gt;
&amp;lt;dd&amp;gt;对名词1的解释或扩展2&amp;lt;/dd&amp;gt;
&amp;lt;dt&amp;gt;名词2&amp;lt;/dt&amp;gt;
&amp;lt;dd&amp;gt;对名词2的解释或扩展1&amp;lt;/dd&amp;gt;
&amp;lt;dd&amp;gt;对名词2的解释或扩展2&amp;lt;/dd&amp;gt;
...
&amp;lt;/dl&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;dl&amp;gt;&amp;lt;/dl&amp;gt;只能直系嵌套&amp;lt;dt&amp;gt;和&amp;lt;/dd&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;dt&amp;gt;和&amp;lt;dd&amp;gt;的个数没有限制，经常是一个&amp;lt;dt&amp;gt;对应多个&amp;lt;dd&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;十、表单标签（重要且内容多）&lt;/h2&gt;
&lt;p&gt;为什么需要表单标签：&lt;/p&gt;
&lt;p&gt;与用户进行交互，收集信息（如用户信息）&lt;/p&gt;
&lt;h4&gt;1、表单的组成&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;表单域&lt;/li&gt;
&lt;li&gt;表单控件（表单元素）&lt;/li&gt;
&lt;li&gt;提示信息（表单元素的提示信息）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2、表单域&lt;/h4&gt;
&lt;p&gt;表单域是一个包含表单元素的区域&lt;/p&gt;
&lt;p&gt;使用&amp;lt;form&amp;gt;定义表单域，以实现对用户信息的收集和传递（&amp;lt;form&amp;gt;会把它范围内的表单元素信息提交给服务器）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form action=&quot;url地址&quot; method=&quot;提交方式&quot; name=&quot;表单域名称&quot;&amp;gt;
各种表单元素控件
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;action&lt;/td&gt;
&lt;td&gt;url地址&lt;/td&gt;
&lt;td&gt;用于指定接受并处理表单数据的服务器程序的url地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;method&lt;/td&gt;
&lt;td&gt;get/post&lt;/td&gt;
&lt;td&gt;用于设置表单数据的提交方式，其取值为get/post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;名称&lt;/td&gt;
&lt;td&gt;用于指定表单的名称，以区分同一个页面中的多个表单域&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;#3、表单控件（表单元素）&lt;/h4&gt;
&lt;p&gt;在表单域中可以定义各种表单元素，这些表单元素就是允许用户在表单中输入或选择的内容控件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&amp;lt;input&amp;gt;输入类标签（单标签）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;type 属性&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;定义单行的输入字段，用户可在其中输入文本，默认长度为20字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;password&lt;/td&gt;
&lt;td&gt;定义密码字段。该字段中的字符被掩码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;radio&lt;/td&gt;
&lt;td&gt;定义单选按钮（一组单选的name值必须相同）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;checkbox&lt;/td&gt;
&lt;td&gt;定义复选框（一组多选的name值必须相同）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;button&lt;/td&gt;
&lt;td&gt;定义可点击按钮（多数情况下通过JavaScript实现功能）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;file&lt;/td&gt;
&lt;td&gt;定义浏览按钮和提示信息，供文件上传&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reset&lt;/td&gt;
&lt;td&gt;定义重置按钮，会清除表单中的所有数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;submit&lt;/td&gt;
&lt;td&gt;定义提交按钮，将会把表单数据提交给服务器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其他属性：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;(由开发人员定义)&lt;/td&gt;
&lt;td&gt;定义input元素的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;value&lt;/td&gt;
&lt;td&gt;(由开发人员定义)&lt;/td&gt;
&lt;td&gt;定义input 元素的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;checked&lt;/td&gt;
&lt;td&gt;checked&lt;/td&gt;
&lt;td&gt;规定input元素首次加载时应被选中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maxlength&lt;/td&gt;
&lt;td&gt;正整数&lt;/td&gt;
&lt;td&gt;规定输入字段中字符的最大长度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;&amp;lt;label&amp;gt;标注标签（双标签）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;p&gt;a、为input元素定义标注&lt;/p&gt;
&lt;p&gt;b、&lt;strong&gt;绑定一个表单元素，当点击&amp;lt;label&amp;gt;标签内的文本时，自动将光标转到对应的表单元素上，可以提升用户体验&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- &amp;lt;label&amp;gt;标签的for属性与&amp;lt;input&amp;gt;标签的id相匹配即可 --&amp;gt;
&amp;lt;label for=&quot;sex&quot;&amp;gt;男&amp;lt;/label&amp;gt;
&amp;lt;input type=&quot;radio&quot; name=&quot;sex&quot; id=&quot;sex&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&amp;lt;select&amp;gt;下拉列表元素（双标签）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用场景：&lt;em&gt;有多个选项让用户选择，为了避免占用页面空间，可使用下拉列表&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select&amp;gt;
&amp;lt;option&amp;gt;北京&amp;lt;/option&amp;gt;
&amp;lt;option&amp;gt;天津&amp;lt;/option&amp;gt;
&amp;lt;option&amp;gt;河北&amp;lt;/option&amp;gt;
&amp;lt;option&amp;gt;山东&amp;lt;/option&amp;gt;
...
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;a、至少包含一对option（双标签）&lt;/p&gt;
&lt;p&gt;b、若某一个option定义selected属性为&quot;selected&quot;，则该项为默认选中项&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&amp;lt;textarea&amp;gt;多行文本输入标签（双标签）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;属性：cols=&quot;字符数&quot;，rows=&quot;字符数&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过这两个属性可以规定不创建滑动条可以容纳的字符数，也就规定了文本框的大小&lt;/strong&gt;（但这两个属性不常用，因为一般使用CSS调整）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;textarea&amp;gt;
  这里写默认显示的文字
&amp;lt;/textarea&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h1&gt;CSS&lt;/h1&gt;
&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;&amp;lt;h4&amp;gt;1、HTML主要做结构，显示元素内容&amp;lt;/h4&amp;gt;
&amp;lt;h4&amp;gt;2、CSS美化HTML，布局网页&amp;lt;/h4&amp;gt;
&amp;lt;h4&amp;gt;3、CSS最大价值：由HTML专注去做结构呈现，样式交给CSS，即结构与样式相分离&amp;lt;/h4&amp;gt;&lt;/p&gt;
&lt;h2&gt;二、语法规范&lt;/h2&gt;
&lt;h4&gt;1、结构&lt;/h4&gt;
&lt;p&gt;css由两个主要部分构成：选择器和声明&lt;/p&gt;
&lt;p&gt;如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p {
  color: white;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、CSS代码风格&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;展开格式和紧凑格式&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;p {
  color: pink;
  font-size: 20px;
}

/* -------------------------*/

p {color: pink; font-size: 20px;}
div {position: relative; color: skyblue; width: 300px; height: 200px;}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;样式小写（特殊情况除外）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空格规范&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;a、属性值前，冒号后保留一个空格&lt;/p&gt;
&lt;p&gt;b、选择器和大括号中间保留空格&lt;/p&gt;
&lt;h2&gt;三、CSS选择器&lt;/h2&gt;
&lt;h4&gt;1、基础选择器&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;标签选择器 （选择全部某种标签）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;div {
color: pink;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;**类选择器 **（选择有相应类属性的标签）（长词组可使用-间隔）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 单类名 --&amp;gt;
&amp;lt;!-- 技巧：把标签要更改的样式作为属性值写入class --&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;style&amp;gt;
    .red {
        color: red;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;p class=&quot;red&quot;&amp;gt;这个段落需要变成红色&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 多类名，类名用空格隔开 --&amp;gt;
&amp;lt;!-- 技巧：元素的公共样式可以被统一的定义和修改，提高了效率 --&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;style&amp;gt;
    .box {
        width: 150px;
        height: 100px;
    }
    .red {
        color: red;
    }
    .green {
        color: green;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;box red&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;box green&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;id选择器（&lt;strong&gt;每个元素的id是唯一的&lt;/strong&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
&amp;lt;style&amp;gt;
    #mainpara {
        color: red;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;p id=&quot;mainpara&quot;&amp;gt;主要段落需要变成红色&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;通配符选择器（匹配所有元素，可以用来清除浏览器默认样式）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;* {
margin: 0;
padding: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、CSS字体属性&lt;/h2&gt;
&lt;h4&gt;1、文本字体&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;p {
font-family： Arial, &quot;Microsoft YaHei&quot;, &quot;Hiragino Sans GB&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;各种字体必须使用半角逗号隔开&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字体名有空格最好用引号包含&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常见字体:body{font-family: &quot;Microsoft YaHei&quot;, tahoma, arial, &quot;Hiragino Sans GB&quot;}&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、字体大小&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;p {
font-size: 20px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不同浏览器字号大小可能不一致，给一个确定的值&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以给body指定文字大小，以控制整个页面的文字大小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;body中标题标签比较特殊，需要单独设置文字大小&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、字体粗细&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;p {
font-weight: bold;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;可选值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;font-weight&lt;/td&gt;
&lt;td&gt;normal(400)，bold(700)，bolder，lighter，&lt;em&gt;number&lt;/em&gt;(100~900)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;4、文字样式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;p {
font-style: ltalic;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;可选值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;font-style&lt;/td&gt;
&lt;td&gt;normal，italic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;5、字体的复合属性（字体连写）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/* 格式如下：
p {
font : font-style font-weight font-size/line-height font-family;
}
*/
p {
font: italic 700 16px &quot;Microsoft yahei&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;顺序不能颠倒！&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不要的属性可以不写，但只要使用font复合属性font-size和font-family必须写&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;p {
font: 16px &quot;Microsoft yahei&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五、CSS文本属性&lt;/h2&gt;
&lt;h4&gt;1、文本颜色&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;div {
color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表示&lt;/th&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;预定义的颜色值&lt;/td&gt;
&lt;td&gt;red、green、blue...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;十六进制&lt;/td&gt;
&lt;td&gt;#66ccff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RGB代码&lt;/td&gt;
&lt;td&gt;(255, 0, 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RGBA代码&lt;/td&gt;
&lt;td&gt;(255, 0, 0, .7)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;十六进制表示出现两两相同的情况，可以简写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如：&lt;strong&gt;#66CCFF ---&amp;gt; #6CF&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2、对齐文本&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;div {
text-align: center
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种属性值：left、center、right&lt;/p&gt;
&lt;h4&gt;3、装饰文本&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;div {
text-decoration: underline;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;默认，没有装饰线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;underline&lt;/td&gt;
&lt;td&gt;下划线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;overline&lt;/td&gt;
&lt;td&gt;上划线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;line-through&lt;/td&gt;
&lt;td&gt;删除线（不常用）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;4、文本缩进&lt;/h4&gt;
&lt;p&gt;通常用来指定段落的首行缩进&lt;/p&gt;
&lt;p&gt;指定按像素值缩进：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p {
text-indent: 32px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按em为单位缩进，就是当前元素（font-size）1个文字的大小，若当前元素没有设置大小，则按照父元素一个文字的大小&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p {
text-indent: 2em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、行间距&lt;/h4&gt;
&lt;p&gt;用于设置行间的距离（行高），可以控制文字行与行之间的距离&lt;/p&gt;
&lt;p&gt;注：&lt;strong&gt;完整的行间距包括上间距、文本高度和下间距&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p {
line-height: 28px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;六、CSS的引入方式&lt;/h2&gt;
&lt;h4&gt;1、内部样式表（内嵌样式表）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
&amp;lt;style&amp;gt;
    p {
        text-indent: 2em;
        color: blue;
    }
    ...
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、行内样式表（内联样式表）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p style=&quot;color: blue; font-size: 18px;&quot;&amp;gt;
这个段落使用了行内样式表
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、外部样式表（外链式引入）（最常用）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;css文档位置&quot; /&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/*css文档*/
p {
text-decoration: underline;
font-style: italic;
font-family: &quot;Microsoft YaHei&quot;;
}
a {
text-decoration: none;
color: #000;
font-size: 28px;
}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;link&amp;gt;标签的属性值：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rel&lt;/td&gt;
&lt;td&gt;定义当前文档与被链接文档之间的关系，若为css文档则指定为“stylesheet&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;href&lt;/td&gt;
&lt;td&gt;定义所链接的外部文件的url，可以是相对或绝对路径&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;七、Emmet语法&lt;/h2&gt;
&lt;h4&gt;1、Emmet语法（快速生成html结构）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;输入标签名按tab可直接生成一对标签&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;div + &amp;lt;kbd style=&quot;font-weight: bold;&quot;&amp;gt;tab&amp;lt;/kbd&amp;gt; ---&amp;gt;  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生成多个相同标签，如生成3个div&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;div*3  ---&amp;gt;  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若需要生成父子级关系标签&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;div&amp;gt;p  ---&amp;gt;  &amp;lt;div&amp;gt; &amp;lt;p&amp;gt;&amp;lt;/p&amp;gt; &amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若需要生成兄弟级关系的标签&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;div+p  ---&amp;gt;  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生成带有类名或id的标签（不指明标签默认生成div）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;.demo  ---&amp;gt;  &amp;lt;div class=&quot;demo&quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;p#mainpara  ---&amp;gt;  &amp;lt;p id=&quot;mainpara&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果生成的结构带有数字顺序，可使用$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;p.para$*2  ---&amp;gt;  &amp;lt;p class=&quot;para1&quot;&amp;gt;&amp;lt;/p&amp;gt; &amp;lt;p class=&quot;para2&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若想要在生成标签内部写内容，可使用{}&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;p.para${$}*2  ---&amp;gt;  &amp;lt;p class=&quot;para1&quot;&amp;gt;1&amp;lt;/p&amp;gt; &amp;lt;p class=&quot;para2&quot;&amp;gt;2&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h2&gt;八、CSS复合选择器（重要）&lt;/h2&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;可以更准确、更高效的选择目标元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复合选择器由两个或多个基础选择器，通过不同的方式组合而成&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;1、后代选择器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/*选择ul下的所有li*/
ul li {
color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不仅是直系选择，可以跨级选择&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;以空格隔开的各级元素可以是任意基础选择器&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、子选择器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/*只选择ul下直系的li*/
ul &amp;gt; li {
color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、并集选择器&lt;/h4&gt;
&lt;p&gt;特点：&lt;strong&gt;可以选择多组标签，常用于集体声明&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;h1,
h2,
h3,
ul li &amp;gt; p {
color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语法规范：竖着写&lt;/p&gt;
&lt;h4&gt;4、伪类选择器&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;链接伪类选择器&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;a {
color: #333;
font-decoration: none;
}
a:hover {
color: skyblue;
font-decoration: underline;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;链接伪类选择器&lt;/th&gt;
&lt;th&gt;选择的链接&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a:link&lt;/td&gt;
&lt;td&gt;所有未被访问的链接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a:visited&lt;/td&gt;
&lt;td&gt;所有已被访问的链接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;a:hover&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;鼠标指针悬停的链接&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a:active&lt;/td&gt;
&lt;td&gt;活动链接（鼠标按下未弹起的链接）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在书写时严格遵守LVHA顺序规则，否则不会生效！&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;focus伪类选择器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;特点：用于选择获取&lt;strong&gt;焦点（光标）&lt;strong&gt;的&lt;/strong&gt;表单元素&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一般&amp;lt;input&amp;gt;类表单元素才能获取焦点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input:focus {
background-color: skyblue;
color: white;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;九、CSS元素的显示模式&lt;/h2&gt;
&lt;h4&gt;1、什么是元素显示模式&lt;/h4&gt;
&lt;p&gt;元素显示模式就是元素以什么方式进行显示&lt;/p&gt;
&lt;p&gt;HTML元素一般分为块元素和行内元素两种类型&lt;/p&gt;
&lt;h4&gt;2、块元素（h1~h6, p, div, ul, ol, li等。div最典型）&lt;/h4&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;独占一行&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高度，宽度，外边距和内边距都可以控制&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;宽度默认是容器（父级宽度）的100%&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;是一个容器（盒子），里面可以放其他元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;文字类元素内部不能放块级元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、行内元素（内联元素）（a, strong, b, em, i, del, s, ins, u, span等。span最为典型）&lt;/h4&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;相邻元素可在一行上，一行可以显示多个&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高宽直接设置是无效的&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;默认宽度就是它本身内容的宽度&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;行内元素只能容纳文本或其他行内元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：链接中不能再放链接&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**但特殊情况下&amp;amp;lt;a&amp;amp;gt;里面可以放块级元素，但是将&amp;amp;lt;a&amp;amp;gt;转化为块级模式最安全**（在实际开发中通常转化为块级元素）
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、行内块元素（特殊）（img, input, td)&lt;/h4&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;一行可以显示多个（行内元素特点）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认宽度由内容决定（行内元素特点）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;宽高，内外边距都可控制（块级元素特点）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5、元素显示模式的转换&lt;/h4&gt;
&lt;p&gt;需求：特殊情况下，我们需要元素模式的转换，即一种模式的元素需要另一种模式元素的特性&lt;/p&gt;
&lt;p&gt;方法：在CSS中添加样式:&lt;/p&gt;
&lt;p&gt;转化为块元素：display: block;（重点）&lt;/p&gt;
&lt;p&gt;转化为行内元素：display: inline;&lt;/p&gt;
&lt;p&gt;转化为行内块元素：display: inline-block;（重点）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a {
display: block;
}
div {
display: inline;
}
span {
display: inline-block;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、小技巧：单行文字垂直居中&lt;/h4&gt;
&lt;p&gt;方法：&lt;strong&gt;让文字的行高等于盒子的高度，这样即可垂直居中&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a {
display: block;
width: 100px;
height: 100px;
text-indent: 100px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;十、CSS背景&lt;/h2&gt;
&lt;h4&gt;1、背景颜色&lt;/h4&gt;
&lt;p&gt;使用background-color属性定义&lt;/p&gt;
&lt;p&gt;默认为transparent，值的写法和color类似&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
background-color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、背景图片&lt;/h4&gt;
&lt;p&gt;使用background-image属性定义，可用于超大的背景图片装饰或一些小的标志&lt;/p&gt;
&lt;p&gt;默认为none，可填url(url)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
background-image: url(bd.jpg)
}
span {
background-image: url(https://www.xxx.com/...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、背景平铺&lt;/h4&gt;
&lt;p&gt;使用background-repeat定义&lt;/p&gt;
&lt;p&gt;默认为repeat，可选no-repeat、repeat-x、repeat-y&lt;/p&gt;
&lt;p&gt;注：页面元素可以同时添加颜色和背景图片，但背景图片会盖住颜色&lt;/p&gt;
&lt;h4&gt;4、背景图片位置&lt;/h4&gt;
&lt;p&gt;使用background-position定义，使用场景：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;为方便调整图片位置，小的标志图案或大的背景图片可以以背景图片形式插入&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ul div {
background-position: left center;
}
ol div {
background-position: 20px 40px;
}
dl div {
background-position: center 45px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;length&lt;/td&gt;
&lt;td&gt;百分数，浮点数和单位表示符组成的长度值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;position&lt;/td&gt;
&lt;td&gt;top | center | bottom | left | center | right&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;只使用方位名词：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;若指定的都是方位名词，则顺序是可颠倒的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;若只指定了一个方位名词，则另一个默认设置为居中&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只使用具体参数值：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;若都设置为具体参数值，则顺序绝对不能颠倒&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;若只指定一个具体参数值，则它一定是x坐标&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;混合使用：第一个值一定表x坐标，第二个值一定y坐标&lt;/p&gt;
&lt;h4&gt;#5、背景图像固定&lt;/h4&gt;
&lt;p&gt;使用background-attachment定义,使用场景：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;制作视差滚动效果&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;默认为scroll，可选fixed&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
background-attachment: fixed;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、背景的复合写法（类似font）&lt;/h4&gt;
&lt;p&gt;顺序：&lt;strong&gt;背景颜色 背景图片地址 图片平铺 背景图像滚动 背景图片位置&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
background: #6cf url(bd.png) repeat-x fixed center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、背景颜色半透明（CSS3新特性）&lt;/h4&gt;
&lt;p&gt;使用rgba(r, g, b, a)定义，a表示alpha透明度，从0~1（1为绝对不透明）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
background-color: rgba(0, 255, 0, 0.5);
}
ul &amp;gt; li span {
background: rgba(0, 255, 0, 0.5) url(bd.png) no-repeat fixed center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、背景图像尺寸&lt;/h4&gt;
&lt;p&gt;使用 background-size 调整&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
background-size: 500px 500px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单位：px | 百分比 | cover | contain&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;cover：通过缩放使盒子完全被覆盖即停止&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;contain：宽和高一边缩放覆盖完全即停止&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;9、背景线性渐变&lt;/h4&gt;
&lt;p&gt;在 background-image 属性中使用 linear-gradient 属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
background-image: linear-gradient(...);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一些示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 从上到下，蓝色渐变到红色 */
linear-gradient(blue, red);
 
/* 渐变轴为45度，从蓝色渐变到红色 */
linear-gradient(45deg, blue, red);
 
/* 从左上到右下、从蓝色渐变到红色 */
linear-gradient(left top, blue, red);

/* 从右下到左上、从蓝色渐变到红色 */
linear-gradient(to left top, blue, red);
 
/* 从下到上，从蓝色开始渐变、到高度40%位置是绿色渐变开始、最后以红色结束 */
linear-gradient(0deg, blue, green 40%, red);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于角度的解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;0deg 将创建一个从下到上的渐变，90deg 将创建一个从左到右的渐变。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/linear-gradient-angle.CRf6TRu4_Z1LUC87.webp&quot; alt=&quot;linear-gradient-angle&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同时注意：线性渐变&lt;strong&gt;可以使用透明度，也可以多个渐变叠加&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
background: linear-gradient(217deg, rgba(255,0,0,.8),rgba(255,0,0,0) 70.71%),
    linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),
      linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;十一、CSS三大特性（核心）&lt;/h2&gt;
&lt;h4&gt;1、层叠性&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;完全相同的选择器若设置了相同的样式，就会发生层叠。此时哪个样式离结构近，就使用哪个样式。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
&amp;lt;style&amp;gt;
    div {
        background-color: red;
    }
    div {
        background-color: blue;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div&amp;gt;
    这个div背景是蓝色的，而不是红色
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、继承性&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;子元素可以继承父元素的某些样式（text-，font-，line- 及color这些样式可以继承）（而长宽和边距一般不可继承，但一定注意行高是可以继承的！！）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;关于行高的继承和行高的倍率写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
&amp;lt;style&amp;gt;
    body {
        font: 12px/1.5 &quot;Microsoft YaHei&quot;;
    }
    p {
        font: 16px &quot;Microsoft YaHei&quot;;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;p&amp;gt;
    这个段落最终的行高是16*1.5px，体现了行高的继承和行高相对于字体大小的设置方法
&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;body 行高 1.5 写法的优势：&lt;em&gt;&lt;strong&gt;不独立设置样式的子元素会继承body的样式，而独立设置字体大小样式的子元素，行高就自动是其自定义字体大小的1.5倍&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;3、优先级（特别重要）&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择器&lt;/th&gt;
&lt;th&gt;权重&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;继承或通配符(*)&lt;/td&gt;
&lt;td&gt;0,0,0,0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元素选择器&lt;/td&gt;
&lt;td&gt;0,0,0,1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;类选择器（包含伪类）&lt;/td&gt;
&lt;td&gt;0,0,1,0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id选择器&lt;/td&gt;
&lt;td&gt;0,1,0,0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;行内样式&lt;/td&gt;
&lt;td&gt;1,0,0,0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!important修饰&lt;/td&gt;
&lt;td&gt;&amp;lt;span style=&quot;font-size: 25px;&quot;&amp;gt;∞&amp;lt;/span&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;权重优先从高位比较，同时永远不会有进位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;继承的权重是0，同时继承的权重是最低的。不管父元素优先级有多高，子元素得到的权重都是0&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复合选择器可通过权重计算比较优先级，即权重加和大优先级大。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;计算方法：&lt;strong&gt;各位相加，但不进位，最后从高位开始依次比较&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 继承权重为0的理解 --&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;style&amp;gt;
    div {
        color: red !important;
    }
    p {
        color: blue;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;p&amp;gt;
        这个段落的文字不会继承div的颜色，即使它是!important修饰的。
        因为继承权重为0，小于元素选择器权重，元素选择器优先为蓝色。
    &amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;
        实际上继承权重最低的另一个例子是写body的样式并不会直接影响a的样式,
        因为a有默认的浏览器样式，其优先级高于继承，所以a为蓝色+有下划线。
        标题标签也是类似
    &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 复合选择器权重计算 --&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;style&amp;gt;
    div .demo {
        /* 权重(0,0,0,1) + (0,0,1,0) = (0,0,1,1) */
        color: blue;
    }
    div ul {
        /* 权重(0,0,0,1) + (0,0,0,1) = (0,0,0,2) */
        color: red;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;ul class=&quot;demo&quot;&amp;gt;
        &amp;lt;li&amp;gt;从权重计算可以看出第一个选择器优先级更高，所以这里的文字是蓝色的&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;好耶！我也是蓝色(ﾉﾟ▽ﾟ)ﾉ&amp;lt;/li&amp;gt;
        ...
    &amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;十二、盒子模型&lt;/h2&gt;
&lt;h4&gt;1、看透网页布局的本质&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先准备好相关的网页元素，网页元素基本都是盒子Box&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;利用CSS设置好盒子样式，然后摆放到相应位置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;往盒子里装内容&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;网页布局的核心：&lt;strong&gt;利用CSS摆盒子&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2、盒子模型的组成&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/box-model.D88maPsx_1wTP6c.webp&quot; alt=&quot;盒子模型组成图&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3、边框（border）&lt;/h4&gt;
&lt;p&gt;三个属性：  boder-width、border-style、border-color&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;border-width通常使用px单位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;border-style有实线(solid)、虚线(dashed)和点线(dotted)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;border-color按常规颜色填写即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;合并简写：没有相关顺序&lt;/p&gt;
&lt;p&gt;若需要设置单边的边框：（其余同理）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
border-top: 5px solid blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但若只需要设置单边边框的样式，可四边先设置为一个样式再覆盖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 后一句样式部分覆盖了前一句的样式，层叠性的体现 */
div {
border: 5px solid blue;
border-top: 5px solid pink;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;border会额外增加盒子的实际大小（类似外描边），使用时注意测量即可&lt;/strong&gt;（也可使用 box-sizing: border-box; 调整）&lt;/p&gt;
&lt;h4&gt;#4、表格边框绘制方式（border-collapse）&lt;/h4&gt;
&lt;p&gt;将相邻的表格边框合并在一起，而不是直接像素叠加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;table {
border-collapse: collapse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、内边距（padding）&lt;/h4&gt;
&lt;p&gt;padding用于设置内边距，即边框与内容间的距离&lt;/p&gt;
&lt;p&gt;可设置四个属性：padding-left、padding-right、padding-top 和 padding-bottom（单位为px）&lt;/p&gt;
&lt;p&gt;复合简写：（四种写法都是重点）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
padding: 5px;    /* 上下左右 */
}

div {
padding: 5px 10px;    /* 上下 左右 */
}

div {
padding: 5px 10px 20px;    /* 上 左右 下 */
}

div {
padding: 5px 10px 20px 30px;    /* 上 右 下 左 （顺时针）*/
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;padding会额外增加盒子的实际大小（向外扩展出内边距） ，测量使用时注意即可&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但注意若盒子的长/宽没有指定， 那对应的长或宽就不会向外扩展，反而是向内扩展！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;padding的应用：&lt;em&gt;&lt;strong&gt;设置padding让盒子长宽自然随content长宽变化，而不是设置固定的盒子长宽值&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;6、外边距（margin）&lt;/h4&gt;
&lt;p&gt;margin用于设置外边距，即控制盒子和盒子间的距离&lt;/p&gt;
&lt;p&gt;可设置四个属性：margin-left、margin-right、margin-top和margin-bottom（一般使用px衡量）&lt;/p&gt;
&lt;p&gt;可复合简写，写法与padding一致&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
margin: 20px 30px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用场景：&lt;em&gt;margin可以让块级盒子水平居中&lt;/em&gt;，但必须满足两个条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;盒子必须指定width&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;盒子左右外边距设置为auto&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
margin: 0 auto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;strong&gt;以上方法是让块元素水平居中，而如果需要行内元素或行内块元素水平居中，可以对元素使用text-align: center;&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;#7、margin在垂直和嵌套时的塌陷（重难点）&lt;/h4&gt;
&lt;p&gt;相邻块元素垂直外边距的合并：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当上下相邻的两个块元素相遇时，如果上面的元素有下外边距margin-bottom&lt;/li&gt;
&lt;li&gt;下面的元素有上外边距margin-top，则他们之间的垂直间距不是margin-bottom与margin-top之和&lt;/li&gt;
&lt;li&gt;取两个值中的较大者这种现象被称为相邻块元素垂直外边距的合并（也称外边距塌陷）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/outside-margin-collapse.DmHxFXQy_Z1GLMPN.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解决方法：&lt;strong&gt;尽量只给一个盒子添加margin&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;嵌套块元素垂直外边距的合并（塌陷）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于两个嵌套关系的块元素，如果父元素没有上内边距及边框&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;父元素的上外边距会与子元素的上外边距发生合并&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合并后的外边距为两者中的较大者&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/inside-margin-collapse.BKKZrt5G_ZIoGbJ.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解决方法：&lt;strong&gt;给父元素添加样式：overflow: hidden;&lt;/strong&gt; 或&lt;strong&gt;对子元素使用浮动&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;8、清除默认padding和margin&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;* {
padding: 0;
margin: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;行内元素为照顾兼容性，尽量只设置左右的内外边距，不要设置上下的内外边距。若实在需要，可以转化为块元素或行内块元素&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;9、行内块及行内元素排列空隙的解决方案（标准流下的解决方案）&lt;/h4&gt;
&lt;p&gt;行内块和行内元素间因有换行，会被保留为一个空格&lt;/p&gt;
&lt;p&gt;解决方法：&lt;em&gt;主要思路都是避免产生空白文本节点：各元素的标签的尖括号换到下一行，或将所有元素写在一行，或者最后进行代码压缩&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;如下方的h4和em标签&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;info&quot;&amp;gt;
&amp;lt;h4 class=&quot;product-name&quot;&amp;gt;
    &amp;lt;a href=&quot;https://www.mi.com&quot; target=&quot;_blank&quot;&amp;gt;Redmi AirDots真无线蓝...&amp;lt;/a&amp;gt;
&amp;lt;/h4
&amp;gt;&amp;lt;em&amp;gt;|&amp;lt;/em
&amp;gt;&amp;lt;span class=&quot;price&quot;&amp;gt;99.9元&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;10、圆角边框&lt;/h4&gt;
&lt;p&gt;在CSS3中，新增了圆角边框样式。&lt;/p&gt;
&lt;p&gt;使用border-radius设置，对应的值越大，弧度越为明显&lt;/p&gt;
&lt;p&gt;单位：px 或 %&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
border-radius: 5px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四个角单独的圆角属性名：border-top-left-radius、border-top-right-radius、border-bottom-right-radius和border-bottom-left-radius（顺序为顺时针）（border-radius可以看做是合并简写，写法和margin类似）&lt;/p&gt;
&lt;p&gt;注：&lt;strong&gt;将border-radius设置为长和宽的一半，矩形的边将会完全被弧线所替代，这时矩形将变成圆或椭圆&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;#11、盒子阴影&lt;/h4&gt;
&lt;p&gt;使用box-shadow定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
box-shadow: h-shadow v-shadow blur spread color inset;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;h-shadow&lt;/td&gt;
&lt;td&gt;必需，水平阴影的位置，允许负值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v-shadow&lt;/td&gt;
&lt;td&gt;必需，垂直阴影的位置，允许负值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blur&lt;/td&gt;
&lt;td&gt;可选，模糊距离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spread&lt;/td&gt;
&lt;td&gt;可选，阴影的尺寸&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;color&lt;/td&gt;
&lt;td&gt;可选，阴影的颜色&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inset&lt;/td&gt;
&lt;td&gt;可选，将外部阴影改为内部内部阴影&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;默认是外部阴影，但是不可以写outset这个值，否则阴影会无效&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;盒子阴影不占用空间，不影响其他盒子的布局&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;#12、文字阴影&lt;/h4&gt;
&lt;p&gt;使用text-shadow定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p {
text-shadow: h-shadow v-shadow blur color
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各值含义与盒子阴影一致&lt;/p&gt;
&lt;h2&gt;十三、CSS浮动&lt;/h2&gt;
&lt;h4&gt;1、传统网页布局的三种方式：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;标准流（普通流/文档流）：标签按照规定好的默认方式排列，是最基本的网页布局方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浮动：可以改变标签默认的排列方式，实现复杂的布局需求（浮动最典型的应用：可以让多个块级元素一行内排列显示）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定位：可以随意改变元素的位置，实现更加复杂的布局需求&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实际开发中，一个页面基本都包含了这三种布局方式&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2、浮动&lt;/h4&gt;
&lt;p&gt;网页布局第一准则：&lt;strong&gt;多个块级元素纵向排列找标准流，多个块级元素横向排列找浮动&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;float属性用于创建浮动框，将其移动到一边，直到左边缘或右边缘触及包含块或另一个浮动框的边缘。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
float: left;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;元素不浮动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;left&lt;/td&gt;
&lt;td&gt;元素向左浮动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;td&gt;元素向右浮动&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;3、浮动特性（重点难点）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;浮动特性会脱离标准流（脱标），&lt;strong&gt;浮动的盒子不再保留原来的位置&lt;/strong&gt;，这时其他标准流的盒子就&lt;strong&gt;会占用它的位置&lt;/strong&gt;，发生&lt;strong&gt;重叠&lt;/strong&gt;的现象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/float-standard.BvjMiIa-_Z1VyMfX.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;浮动的元素会&lt;strong&gt;一行显示并且按元素顶端对齐&lt;/strong&gt;，若父元素宽度不能容纳，则另起一行对齐&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浮动元素会&lt;strong&gt;具有行内块元素的特性&lt;/strong&gt;（宽度由内容决定等）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4、浮动元素的使用&lt;/h4&gt;
&lt;p&gt;为了约束浮动元素的位置，网页布局的一般策略是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先用标准流的父元素排列上下位置，之后内部子元素采取浮动排列左右位置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;网页布局第二准则：&lt;strong&gt;先设置盒子的大小，再调整盒子的位置&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;5、常见网页布局&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/webpage-structure1.CxgeRTXZ_Z1cQGRS.webp&quot; alt=&quot;webpage-structure1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/webpage-structure2.CybGWTI__Zd6roe.webp&quot; alt=&quot;webpage-structure2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以上为两种不太常用的布局方式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/webpage-structure3.Ctc5tfX-_1juNAU.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以上是最常见、最常用的布局方式&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;6、浮动注意事项&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;浮动和标准流的父盒子搭配&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一个元素浮动了，理论上其余的兄弟元素也要浮动&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为：&lt;strong&gt;浮动盒子只会影响浮动盒子后面的标准流的布局，不会影响前面的标准流布局&lt;/strong&gt;；若其中一个不浮动，盒子排列可能就会错行&lt;/p&gt;
&lt;h4&gt;7、清除浮动&lt;/h4&gt;
&lt;p&gt;问题：在子元素长宽或数量不确定的情况下，写死父盒子的高度是不方便的。但若不写父盒子高度，因为子元素是浮动的不占用布局空间，父元素高度就会变为0，影响整体布局&lt;/p&gt;
&lt;p&gt;因此：&lt;strong&gt;此时应该清除浮动&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;清除浮动的本质：本质是清除浮动元素造成的影响。&lt;/p&gt;
&lt;p&gt;清除浮动的作用：清除后，父级会根据浮动的子盒子自动检测高度，父级有了高度，就不会影响整体布局了&lt;/p&gt;
&lt;p&gt;注：&lt;strong&gt;父盒子如果已有高度，就不需要清除浮动&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;方法详解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;额外标签法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在最后一个浮动元素后添加一个&lt;strong&gt;带clear属性的块元素&lt;/strong&gt;空标签，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
.float-div {
    float: left;
}
&amp;lt;/style&amp;gt;
&amp;lt;div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;1&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;2&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;3&amp;lt;/div&amp;gt;
&amp;lt;div style=&quot;clear: both;&quot;&amp;gt;&amp;lt;/div&amp;gt;    
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺点：添加了无意义的标签，结构化较差&lt;/p&gt;
&lt;p&gt;原理：空标签clear属性要求它自己不被浮动元素所覆盖，而它又是没有高度的块级元素，浏览器渲染时必然会将它置于最末尾，而它又是包含于父元素的标准流，父元素必然会被撑开&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给父级添加overflow属性&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;给父级添加overflow属性，值可以为hidden、auto、scroll，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
.box {
    overflow: auto;
}
.float-div {
    float: left;
}
&amp;lt;/style&amp;gt;
&amp;lt;div class=&quot;box&quot;&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;1&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;2&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;3&amp;lt;/div&amp;gt; 
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺点：溢出的部分的显示会受影响&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;::after伪元素法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;定义clearfix伪元素类样式，父级引用这个类样式即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
.clearfix::after {
    content: &quot;&quot;;
    display: block;
    height: 0;
    clear: both;
    visibility: hidden;
}
.clearfix {
    *zoom: 1;  /*IE6、7专用*/
}
.float-div {
    float: left;
}
&amp;lt;/style&amp;gt;
&amp;lt;div class=&quot;box clearfix&quot;&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;1&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;2&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;3&amp;lt;/div&amp;gt; 
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;双伪元素清除浮动&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;和第三种方法类似，给父元素添加一些样式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
  .clearfix:before,
.clearfix:after {
    content: table;
}
.clearfix:after {
    clear: both;
}
.clearfix {
    *zoom: 1;
}
.float-div {
    float: left;
}
&amp;lt;/style&amp;gt;
&amp;lt;div class=&quot;box clearfix&quot;&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;1&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;2&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;float-div&quot;&amp;gt;3&amp;lt;/div&amp;gt; 
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、CSS属性书写顺序&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;布局定位属性：display / position / float / clear / visibility / overflow（建议display最先写，其关系模式）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自身属性：width / height / margin / padding / border / background&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文本属性：color / font / text-decoration / text-align / vertical-align / white-space / break-word&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他属性（CSS3属性）：content / cursor / border-radius / box-shadow / text-shadow / background / linear-gradient...&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*这里空行是为了划分四个部分的属性，实际书写合在一起写即可*/
.jdc {
display: block;
positon: relative;
float: left;

width: 100px;
height: 100px;
margin: 0 10px;
padding: 20px 0;

font-family: Arial, &quot;Helvetica Neue&quot;, Helvetica, sans-serif;
color: #333;

background: rgba(0, 0, 0, .5);
border-radius: 10px; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;十四、CSS定位&lt;/h2&gt;
&lt;h4&gt;1、定位&lt;/h4&gt;
&lt;p&gt;当网页元素需要自由移动位置或固定在屏幕某个位置时，使用标准流和浮动是无法实现效果的。这时便需要使用定位&lt;/p&gt;
&lt;h4&gt;2、定位组成&lt;/h4&gt;
&lt;p&gt;定位：将盒子定在某个位置。因此定位也是摆盒子，只是按照定位的方式来进行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定位 = 定位模式 + 边偏移&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定位模式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;定位模式决定元素的定位方式，使用position属性定义&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;语义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;static&lt;/td&gt;
&lt;td&gt;静态定位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;relative&lt;/td&gt;
&lt;td&gt;相对定位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;absolute&lt;/td&gt;
&lt;td&gt;绝对定位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fixed&lt;/td&gt;
&lt;td&gt;固定定位&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;边偏移就是定位的盒子移动到的最终位置&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;边偏移属性&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;top&lt;/td&gt;
&lt;td&gt;top: 80px&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;顶端&lt;/strong&gt;偏移量，定义盒子相对于其父元素&lt;strong&gt;上边线的距离&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bottom&lt;/td&gt;
&lt;td&gt;bottom: 80px&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;底部&lt;/strong&gt;偏移量，定义盒子相对于其父元素&lt;strong&gt;下边线的距离&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;left&lt;/td&gt;
&lt;td&gt;left: 80px&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;左侧&lt;/strong&gt;偏移量，定义盒子相对于其父元素&lt;strong&gt;左边线的距离&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;td&gt;right: 80px&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;右侧&lt;/strong&gt;偏移量，定义盒子相对于其父元素&lt;strong&gt;右边线的距离&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;3、静态定位static（了解）&lt;/h4&gt;
&lt;p&gt;静态定位是元素默认定位方式，其实就是无定位&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
position: static;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;静态定位按照标准流特性摆放位置，所以没有边偏移&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;静态定位在页面布局中很少用到&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4、相对定位relative（重要）&lt;/h4&gt;
&lt;p&gt;相对定位是元素在移动位置时，相对于&lt;strong&gt;它原来的位置&lt;/strong&gt;作移动&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/relative-position.DBrhYRpN_Z1xxMKT.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
posiotion: relative;
top: 100px;
left: 50px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;相对定位的元素（无论是否有边偏移量），后面的盒子不会占用它的位置（即相对定位元素不脱标，仍然占有原来的布局空间）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相对定位的主要使用情形：&lt;em&gt;父级添加相对定位，子级的绝对定位就会以它为基准&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5、绝对定位absolute（重要）&lt;/h4&gt;
&lt;p&gt;绝对定位是元素在移动位置时，是相对于它的&lt;strong&gt;祖先元素&lt;/strong&gt;（不一定是父元素）来移动的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
.father {
    position: relative;
    width: 500px;
    height: 500px;
}
.son {
    position: absolute;
    top: 100px;
    left: 50px;
    width: 100px;
    height: 100px;
}
&amp;lt;/style&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;father&quot;&amp;gt;
    &amp;lt;div class=&quot;son&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果没有&lt;strong&gt;祖先元素&lt;/strong&gt;或&lt;strong&gt;祖先元素没有定位&lt;/strong&gt;，则以浏览器为准定位（Document文档）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果祖先元素有定位（相对、绝对或固定定位），则以最近一级的有定位的祖先元素为基准&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只要加绝对定位（无论是否有边偏移量）就不再占用原来的布局空间（即绝对定位元素是脱标的）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;6、子绝父相的由来&lt;/h4&gt;
&lt;p&gt;含义：&lt;strong&gt;子级若使用绝对定位，则父级一般使用相对定位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;子级绝对定位，不会占有位置，可以放到父盒子里的任何地方&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为了使子级绝对定位以父级为基准，父级必须有定位。而父级不能也使用绝对定位，因为父级脱标后会影响整体布局，所以父级只能使用相对定位&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：子绝父相不是绝对的，只是最常用的方法，有时也可以使用子绝父绝&lt;/p&gt;
&lt;h4&gt;7、固定定位fixed（重要）&lt;/h4&gt;
&lt;p&gt;固定定位是将元素&lt;strong&gt;固定于浏览器可视区&lt;/strong&gt;的位置，主要应用场景：&lt;em&gt;浏览器页面滚动时元素的位置不会发生改变&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
position: fixed;
top: 100px;
right: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;以浏览器的可视窗口为参照移动元素（&lt;strong&gt;和父元素无关&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;固定定位元素不再占用原来的布局空间（即是脱标的）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;8、固定定位小技巧：固定在版心右侧位置&lt;/h4&gt;
&lt;p&gt;算法设计：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;让固定定位的盒子left: 50%，走到浏览器可视区的一半位置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;让固定定位的盒子margin-left: 版心宽度的一半。（再走一半的版心宽度即是版心右侧）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;9、粘性定位sticky（了解）&lt;/h4&gt;
&lt;p&gt;粘性定位可以被认为是相对定位和固定定位的混合&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
position: sticky;
top: 10px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;以浏览器的可视窗口为参照移动元素（固定定位特点）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;粘性定位&lt;strong&gt;占有原先的位置&lt;/strong&gt;（相对定位特点）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;必须添加边偏移才有效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;兼容性差，IE不支持&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;表现：当粘性定位元素与浏览器可视窗口的边距大于粘性定位的边偏移时，此时元素表现出相对定位的特性：占有指定区域的布局空间。当与浏览器可视窗口的边距等于粘性定位的边偏移时，元素表 现出固定定位的特点，使元素和浏览器可视窗口的边距不小于边偏移量&lt;/p&gt;
&lt;h4&gt;10、定位叠放次序z-index&lt;/h4&gt;
&lt;p&gt;在使用定位布局时，可能会出现盒子重叠的情况，此时可使用z-index控制盒子的显示顺序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 按照层叠规律，默认是靠后的盒子显示优先级更高 */
div {
z-index: 1;
position: relative;
width: 100px;
height: 100px;
}
span {
position: relative;
width: 100px;
height: 100px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;数值可以是正整数、负整数和0，默认是auto，&lt;strong&gt;数值越大，优先级越高&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数字后面不能跟单位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只有定位的盒子才有z-index属性&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;#11、定位拓展&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;绝对定位的盒子如何水平垂直居中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以水平居中为例：&lt;/p&gt;
&lt;p&gt;算法设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a、left: 50%（让盒子的左侧移动到父级元素的水平中心位置）

b、margin-left: -盒子宽度的一半（让盒子移动自身宽度的一半）(或使用 transform: tranlate(-50%, 0); )
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定位特殊特性&lt;/p&gt;
&lt;p&gt;a、&lt;strong&gt;行内元素添加绝对或固定定位后，就可以直接设置高度和宽度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;b、&lt;strong&gt;块级元素添加绝对或固定定位，如果不给宽度或高度，默认大小是内容的大小&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;脱标的盒子不会触发外边距塌陷&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;绝对定位 / 固定定位会完全压住盒子&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;浮动元素不同，他只会压住他下面标准流的盒子，但不会压住标准流盒子中的图片或文字，借此原理可用浮动实现文字环绕的效果&lt;/p&gt;
&lt;p&gt;而绝对定位 / 固定定位会完全压住它下方标准流的所有内容&lt;/p&gt;
&lt;h2&gt;十五、CSS隐藏&lt;/h2&gt;
&lt;h4&gt;1、三种隐藏方法&lt;/h4&gt;
&lt;p&gt;display 显示隐藏&lt;/p&gt;
&lt;p&gt;visibility 显示隐藏&lt;/p&gt;
&lt;p&gt;overflow 溢出隐藏&lt;/p&gt;
&lt;h4&gt;2、display属性（重点）&lt;/h4&gt;
&lt;p&gt;display用于设置一个元素如何显示，其用途及其广泛，搭配 JS 可以做很多特效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
display: none;  /* 隐藏对象 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;display: block; 除了有转换为块级元素的功能，同时还有显示元素的意思&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;display 隐藏元素之后，不再占有原来的布局空间&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、visibility 可见性&lt;/h4&gt;
&lt;p&gt;visibility 也用于设置元素的可见性（相对display属性用的较少）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
visibility: hidden;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;行为&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;inherit&lt;/td&gt;
&lt;td&gt;继承父元素的可见性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;visible&lt;/td&gt;
&lt;td&gt;对象可视&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hidden&lt;/td&gt;
&lt;td&gt;对象隐藏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;collapse&lt;/td&gt;
&lt;td&gt;用于隐藏表格的行和列（不常用）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：&lt;strong&gt;visibility 隐藏元素后，继续占有原来的位置&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;4、overflow 溢出&lt;/h4&gt;
&lt;p&gt;overflow 属性指定了如果内容溢出一个元素的框（超过指定的宽或高）时会发生什么&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
overflow: hidden;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;visible&lt;/td&gt;
&lt;td&gt;不剪切内容也不添加滚动条&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hidden&lt;/td&gt;
&lt;td&gt;不显示超过对象尺寸的内容，超出的部分直接隐藏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scroll&lt;/td&gt;
&lt;td&gt;无论内容是否超出尺寸限制，都强制添加滚动条&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;td&gt;内容超出尺寸限制，则强制添加滚动条&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：一般情况下我们不希望盒子中的内容溢出，因此会采用 overflow: hidden; &lt;strong&gt;但如果是有定位的盒子，慎用overflow: hidden; 因为它会剪切所有超出边界的内容&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;十六、CSS高级技巧&lt;/h2&gt;
&lt;h4&gt;1、精灵图&lt;/h4&gt;
&lt;p&gt;为了有效的减少服务器接受和发送请求的次数，提高网页的加载速度，出现了CSS精灵技术（&lt;/p&gt;
&lt;p&gt;CSS Sprites / CSS 雪碧）&lt;/p&gt;
&lt;p&gt;核心原理：&lt;strong&gt;将网页中的一些小背景图像整合到一张大图片上，这样只需要向浏览器请求这一张图片即可&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;精灵图主要针对小的背景图片使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主要使用 background-posiotion 来调整位置对齐（一般为负值）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 假设我们有一张精灵图，我们需要的图标的参数：
width: 30px
height: 30px
x-position: 145px
y-position: 150px */

/* 那么如果将其设为某个盒子的背景 */
div {
width: 30px;
height: 30px;
background: url(path) -145px -150px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、字体图标&lt;/h4&gt;
&lt;p&gt;使用场景：主要用于显示网页中通用、常用的一些小图标&lt;/p&gt;
&lt;p&gt;对比精灵图，精灵图的缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;图片文件还是比较大的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;图片本身放大和缩小会失真&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;精灵图一旦制作完成，更换起来就很复杂&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时一种技术可以解决以上问题：字体图标（iconfont），&lt;strong&gt;其展示的是图标，但本质是字体&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;字体图标的优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;轻量级：字体图标文件大小比图像小，一旦字体加载了，字体图标也会立即渲染出来&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;灵活性：&lt;strong&gt;本质是文字，可以随意轻松地修改颜色，产生阴影，做透明效果等&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;兼容性：几乎支持所有浏览器&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果需要使用一些结构和样式简单的小图标，选择字体图标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果需要使用一些结构和样式比较复杂的小图标，选择精灵图&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;下载字体图标（推荐icomoon和阿里iconfont字库）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将字体图标引入html页面,建议使用link导入它的样式表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字体图标追加（使用selection.json)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、CSS三角&lt;/h4&gt;
&lt;p&gt;使用场景：网页中一些常见的三角，可以直接用CSS画出来&lt;/p&gt;
&lt;p&gt;原理：&lt;strong&gt;给一个没有宽高的盒子四个方向描边，每个方向都会占一块三角形面积，这时设置其他三边的描边为透明即可&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/triangle.oItPwV16_uRvhk.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
width: 0;
height: 0;
line-height: 0;  /* 照顾兼容性 */
font-size: 0;  /* 照顾兼容性 */
border: 50px solid transparent;
border-left: 50px solid skyblue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;#4、CSS用户界面样式&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;鼠标样式 cursor&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;div {
cursor: pointer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;小白（默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pointer&lt;/td&gt;
&lt;td&gt;小手&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;move&lt;/td&gt;
&lt;td&gt;移动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;光标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;not-allowed&lt;/td&gt;
&lt;td&gt;禁止&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;轮廓线去除 outline（去掉表单默认的边框）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;input {
outline: none;
}
/* 或 */
input {
outline: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;防止拖拽文本域 resize&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实际开发中，我们不希望textarea的区域可被拖动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;textarea {
resize: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;#5、vertical-align 居中对齐&lt;/h4&gt;
&lt;p&gt;vertical-align 常用于设置图片或者表单（行内块元素）和文字垂直对齐的方式&lt;/p&gt;
&lt;p&gt;官方解释： &lt;strong&gt;用于设置一个元素的垂直对齐方式，但只针对于行内元素和行内块元素&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/text-lines.CrZjY3K__1VywdW.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;img {
vertical-align: middle;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;默认，元素和父元素下标基线对齐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;top&lt;/td&gt;
&lt;td&gt;把元素顶端与行中最高元素的顶端对齐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;middle&lt;/td&gt;
&lt;td&gt;使该元素中线在父元素基线上一半x-height的距离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bottom&lt;/td&gt;
&lt;td&gt;把元素底端与行中最低元素的底端对齐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;super&lt;/td&gt;
&lt;td&gt;把元素和父元素上标基线对齐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;使用 vertical-align 解决图片底部默认空白缝隙的问题：&lt;/p&gt;
&lt;p&gt;原因：行内块会和文字的基线对齐&lt;/p&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;给图片添加 vertical-align: middle、top、bottom等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;把图片转化为块级元素 display: block;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;6、溢出文字省略号显示&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;单行文字的省略显示&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;div {
/* 1、先强制一行内显示文本 */
white-space: nowrap;
/* 2、超出的部分隐藏 */
overflow: hidden;
/* 3、溢出的文字使用省略号替代 */
text-overflow: ellipsis;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;多行文字的省略显示&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;多行文本溢出显示省略号，有较大兼容性问题，适合于webkit浏览器和移动端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
overflow: hidden;
text-overflow: ellipsis;
/* 弹性伸缩盒子模型展示 */
display: -webkit-box;
/* 限制在一个块元素显示的文本的行数 */
-webkit-line-clamp: 2;
/* 设置或检索伸缩盒对象的子元素的排列方式 */
-webkit-box-orient: vertical;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;#7、常见布局技巧&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;margin 负值的运用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;a、问题：一排盒子浮动时若左右都有边框则会发生边框重叠&lt;/p&gt;
&lt;p&gt;解决方法：让每个盒子 margin 往左侧移动负的一个边框厚度，就正好抵消了边框重叠的影响&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div ul li {
float: left;
width: 150px;
height: 150px;
border: 1px solid skyblue;
margin-left: -1px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;b、问题：一排浮动盒子都有边框时，如何实现经过某一个盒子变化边框颜色&lt;/p&gt;
&lt;p&gt;提示：加边框使用上面的方法，若直接使用边框变色会导致中间的盒子只有三条边有变色效果，因为另一条边被后面的盒子压住了&lt;/p&gt;
&lt;p&gt;解决方法（提高当前盒子的层级）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;i、给当前盒子添加相对定位，提高层级

ii、若一排盒子都已有相对定位，则使用z-index提高层级
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* 没有相对定位的情况 */
div ul li:hover {
position: relative;
border-color: skyblue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* 都有相对定位的情况 */
div ul li:hover {
position: relative;
z-index: 1;
border-color: skyblue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;文字围绕浮动元素&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;原理：先向父元素中填入文字，再添加浮动的图片，而浮动不会压住文字，就会造成文字环绕显示的效果&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;巧妙使用 inline-block 元素&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;应用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用 inline-block 元素，元素之间会自动带有空隙&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;元素的居中对父元素使用 text-align: center; 即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CSS三角的巧妙运用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;a、CSS绘制直角三角形&lt;/p&gt;
&lt;p&gt;如：（只描边两侧，而两侧描边依然要组合成矩形，因此每一侧的描边都是一个直角三角形）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/right-triangle.CVXvYsov_Z1J0CGy.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
width: 0;
  height: 0;
  border-color: pink skyblue transparent transparent;
  border-style: solid;
  border-width: 160px 120px 0 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;b、CSS绘制任意形状的三角形&lt;/p&gt;
&lt;p&gt;同样使用上面的原理，我们使用两对边的描边长度构造三角形上顶点距下方两顶点的水平距离。然后使用另一边描边限制高度，则可以绘制出任意形状的三角形，同时可搭配 transform: rotate(deg); 使用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/unsorted-triangle.DPg5RlA2_ZBcpIR.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
  width: 0;
  height: 0;
  border-color: transparent pink skyblue purple;
  border-style: solid;
  border-width: 0 160px 120px 240px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、CSS初始化（CSS reset）&lt;/h4&gt;
&lt;p&gt;简单理解：CSS初始化是指重设浏览器的样式&lt;/p&gt;
&lt;p&gt;每个网页都必须首先进行CSS初始化&lt;/p&gt;
&lt;p&gt;京东主页的CSS初始化分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* {
margin: 0;
padding: 0;
}

/* em 和 i 斜体的文字不倾斜 */
em,
i {
font-style: normal;
}

/* 去掉 li 的小圆点 */
li {
list-style: none;
}

img {
border: 0;  /* 照顾兼容性 */
vertical-align: middle;  /* 去除图片底端的空白缝隙 */
}

button {
cursor: pointer;
}

a {
color: #666;
text-decoration: none;
}

a: hover {
color: #01a4ff;  /* 网站特有的设置 */
}

button,
input {
font-family:  Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, &quot;\588B\4F53&quot;, sans-serif;
}

body {
-webkit-font-smoothing: antialiased;
background-color: #fff;
/* 将宋体的中文名称转为unicode编码，防止解释时乱码 */
font: 12px/1.5 Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, &quot;\588B\4F53&quot;, sans-serif;
color: #666;
}

.hide,
.none {
display: none;
}

/* 清除浮动 */
.clearfix::after {
visibility: hidden;
clear: both;
display: block;
content: &quot;.&quot;;
height: 0;
}

.clearfix {
*zoom: 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h1&gt;JavaScript&lt;/h1&gt;
&lt;h2&gt;一、初识JavaScript&lt;/h2&gt;
&lt;h4&gt;1、JS是什么&lt;/h4&gt;
&lt;p&gt;是一种&lt;strong&gt;运行在客户端&lt;/strong&gt;的脚本语言&lt;/p&gt;
&lt;p&gt;现在也可以基于Node.js技术进行服务器端编程&lt;/p&gt;
&lt;h4&gt;2、JS的作用&lt;/h4&gt;
&lt;p&gt;表单动态验证（如密码强度检测）（JS最初产生的目的）&lt;/p&gt;
&lt;p&gt;网页特效&lt;/p&gt;
&lt;p&gt;服务端开发（Node.js）&lt;/p&gt;
&lt;p&gt;桌面程序（Electron）&lt;/p&gt;
&lt;p&gt;App（Cordova）&lt;/p&gt;
&lt;p&gt;控制硬件——物联网（Ruff）&lt;/p&gt;
&lt;p&gt;游戏开发（cocos2d-js）&lt;/p&gt;
&lt;h4&gt;3、浏览器执行JS的过程&lt;/h4&gt;
&lt;p&gt;浏览器分为两部分：&lt;strong&gt;渲染引擎&lt;/strong&gt;和 &lt;strong&gt;JS 引擎&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;渲染引擎：用来解析HTML和CSS，俗称内核，比如chrome的blink，老版本的webkit&lt;/p&gt;
&lt;p&gt;JS引擎：也成为JS解释器，用来读取网页中的JS代码，对齐处理后运行，比如chrome浏览器的V8&lt;/p&gt;
&lt;p&gt;注：&lt;strong&gt;浏览其本身不会执行JS，而是通过内置的JS引擎来解释JS代码&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;4、JS的组成&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;ECMAScript （规定了JS语法）&lt;/li&gt;
&lt;li&gt;DOM（页面文档模型）&lt;/li&gt;
&lt;li&gt;BOM（浏览器对象模型）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;DOM&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;文档对象模型（Document Object Model，简称DOM），是W3C组织推荐的处理可扩展标记语言的标准编程接口&lt;/p&gt;
&lt;p&gt;通过DOM提供的接口可以对页面上的各种元素进行操作（大小、位置、颜色等）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;BOM&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;BOM（Browser Object Model，简称BOM）是指浏览器对象模型，它提供了独立于内容的、可以与浏览器窗口进行互动的对象结构。通过BOM可以操作浏览器窗口，比如弹出框、控制浏览器跳转、获取分辨率等&lt;/p&gt;
&lt;h4&gt;5、JS的书写位置&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;行内式的JS&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;button&quot; value=&quot;BLCU&quot; onclick=&quot;alert(&apos;北京语言大学&apos;)&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;内嵌式的JS&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  alert(&apos;Hello World！&apos;)
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;外部JS引入&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 若确定使用外部引入方法，script标签内不要写任何内容 --&amp;gt;
&amp;lt;script src=&quot;control.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：写JS代码时建议使用单引号，而不是双引号&lt;/p&gt;
&lt;h4&gt;6、JS注释&lt;/h4&gt;
&lt;p&gt;如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  // 单行注释  Ctrl + /

/* 多行  可更改为 Ctrl + Shift + /
注释 */
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、JS输入输出语句&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;prompt方法：弹出输入框（返回的是字符串）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;prompt(&apos;请输入您的年龄：&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;alert方法：弹出警示框&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;alert(&apos;计算的结果是：&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;console.log方法：控制台打印输出信息&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Hello World!&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、变量&lt;/h2&gt;
&lt;h4&gt;1、变量的使用&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;声明变量&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var age;  //声明一个名为age的变量，var是关键字
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;赋值&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;age = 18;  //给变量赋值10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var age = 18;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、同时声明多个变量&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var start = 1,
end = 2,
age = 16;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、声明变量的特殊情况&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;情况&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;var age; console.log(age);&lt;/td&gt;
&lt;td&gt;只声明，不赋值&lt;/td&gt;
&lt;td&gt;undefined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;console.log(age);&lt;/td&gt;
&lt;td&gt;不声明，也不赋值&lt;/td&gt;
&lt;td&gt;报错&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;age = 10; console.log(age);&lt;/td&gt;
&lt;td&gt;不声明，直接赋值&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;4、变量命名规则&lt;/h4&gt;
&lt;p&gt;与其他编程语言类似&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只是注意一点最好不要使用 name 作为变量名&lt;/strong&gt;，在一些浏览器中它也是有特殊用处的&lt;/p&gt;
&lt;h2&gt;三、数据类型（重要）&lt;/h2&gt;
&lt;h4&gt;1、变量的数据类型&lt;/h4&gt;
&lt;p&gt;JS是一种弱类型（动态类型）语言，因此&lt;strong&gt;变量类型不用提前声明，在程序运行中变量类型会随数据而被确定&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var age = 18;
var age = &apos;18&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、数字型 number&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var num1 = 10;  // 十进制10
var num2 = 010;  // 八进制8(前面加0)
var num3 = 0xf;  // 十六进制16(前面加0x)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;数字型最大值和最小值&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;alert(Number.MAX_VALUE);
alert(Number.MIN_VALUE);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;三个特殊值：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;alert(Infinity);  // 无穷大
alert(-Infinity);  // 无穷小
alert(NaN);  // Not a number，非数值
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;isNaN( )方法判断是否为非数字型&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;console.log(isNaN(12));  // 返回false
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、字符串类型 string&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var Msg = &apos;提示信息：...&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：转义字符类似其他编程语言。&lt;/p&gt;
&lt;p&gt;同时字符串可相加，&lt;strong&gt;字符串和任何其他类型相加，最后结果都为字符串&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;length属性实现字符串长度检测&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var strMsg = &apos;Hello World!&apos;;
console.log(str.length);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、布尔型 boolean&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var flag1 = true;
var flag2 = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;strong&gt;JS中的布尔型可与数字型运算，这时 true 看做1，而 false 看做0&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;5、未定义型 undefined&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var variable = undefined;

//任何类型与字符串相加转化为字符串型
console.log(variable + &apos;blue&apos;);  // 返回undefinedblue

console.log(variable + 12);  // 返回NaN
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、空类型 null&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var variable = null;

console.log(variable + &apos;blue&apos;);  // 返回nullblue

console.log(variable + 12);  // 返回12
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、使用 typeof 关键字检测数据类型&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var num = 10;
console.log(typeof num);  // 返回&apos;number&apos;

var str = &apos;blue&apos;;
console.log(typeof str);  // 返回&apos;string&apos;

var flag = true;
console.log(typeof flag);  // 返回&apos;boolean&apos;

var variable = undefined;
console.log(typeof variable);  // 返回&apos;undefined&apos;

var timer = null;
console.log(typeof timer);  // 返回&apos;object&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、数据类型的转换&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;转化为字符串型&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var num = 10;

var str = num.toString();  // 使用数字型的toString方法
var str = String(num);  // 使用String函数
var str = &apos;&apos; + num;  // 与空字符串相加（隐式转换）
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;转化为数字型（重点）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;a、使用 parseInt 和 parseFloat 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var numStr = &apos;10.62&apos;;
var widthStr = &apos;120px&apos;;
var dirty = &apos;em120px&apos;;

var num = parseInt(numStr);  // 向下取整为10
var width = parseInt(widthStr);  //也可以用来去单位(这里返回120)
var dirty = parseInt(dirty);  //返回NaN(无法做到在任意字符串中取数字)

var num = parseFloat(numStr)  //  取浮点为10.62
var width = parseFloat(widthStr);  //也可以用来去单位(返回120)
var dirty = parseFloat(dirty);  // 返回NaN(无法做到在任意字符串中取数字)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;b、使用 Number 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var numStr = &apos;10&apos;;
var num = Number(numStr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;c、利用算术运算符（ - * / 隐式转换）（不能使用 + ）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var numStr = &apos;10&apos;;
var num = numStr - 0;
var num = numStr * 1;
var num = numStr / 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;转换为 boolean 型&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只有一种方法：使用 Boolean 函数&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代表空、否定的值（&apos;&apos;、0、NaN、null、undefined）会被转换为：false&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其余值都会被转化为 true&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var flag = Boolean(&apos;&apos;);  // 为false
var another_flag = Boolean(666);  // 为true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、JS运算符&lt;/h2&gt;
&lt;h4&gt;1、算术运算符&lt;/h4&gt;
&lt;p&gt;与其他编程语言类似&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;/ %&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若需要取整可通过 / ，然后使用parseInt取整得到&lt;/p&gt;
&lt;h4&gt;2、递增、递减运算符&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 前置,先运算再返回值
var num = 10, num2 = 10;
++ num;
-- num2;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 后置，先返回值再运算
var num = 10, num2 = 10;
num ++;
num2 --;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、关系运算符&lt;/h4&gt;
&lt;p&gt;与其他编程语言类似&lt;/p&gt;
&lt;p&gt;但注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;== 会将等式两边数据类型转为相同类型（隐式转换）（&lt;strong&gt;但注意若有一侧为布尔类型，则不会进行隐式转换&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特有的比较运算符： === 和 !== （值和类型都判断）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4、逻辑运算符&lt;/h4&gt;
&lt;p&gt;符号与 C 语言一致&lt;/p&gt;
&lt;p&gt;特点分析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;逻辑与的实现：&lt;strong&gt;逻辑与短路运算&lt;/strong&gt;（从左到右依次判断是否为 false，若为 false 则短路返回当前表达式的值，否则继续向右判断）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑或的实现：&lt;strong&gt;逻辑或短路运算&lt;/strong&gt;（从左到右依次判断是否为 true，若为 true 则短路返回当前表达式的值，否则继续向右判断）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5、赋值运算符&lt;/h4&gt;
&lt;p&gt;符号与 C 语言一致&lt;/p&gt;
&lt;h4&gt;6、运算符优先级&lt;/h4&gt;
&lt;p&gt;小括号 &amp;gt; 一元运算符(含非) &amp;gt; 算术运算符 &amp;gt; 比较运算符 &amp;gt; 相等运算符 &amp;gt; 与 &amp;gt; 或 &amp;gt; 赋值运算符 &amp;gt; 逗号运算符&lt;/p&gt;
&lt;h2&gt;五、流程控制&lt;/h2&gt;
&lt;h4&gt;1、if-else（与C语言基本一致）&lt;/h4&gt;
&lt;h4&gt;2、if-else if（与C语言基本一致）&lt;/h4&gt;
&lt;h4&gt;3、三元表达式（与C语言基本一致）&lt;/h4&gt;
&lt;h4&gt;4、switch 语句（与C语言基本一致）&lt;/h4&gt;
&lt;h4&gt;5、for 循环（与C语言基本一致）&lt;/h4&gt;
&lt;h4&gt;6、代码调试：&lt;/h4&gt;
&lt;p&gt;使用开发工具中的Source选项卡，定位到代码调试&lt;/p&gt;
&lt;h4&gt;7、while 循环（与C语言基本一致）&lt;/h4&gt;
&lt;h4&gt;8、do while 循环（与C语言基本一致）&lt;/h4&gt;
&lt;h4&gt;9、continue break 循环（与C语言基本一致）&lt;/h4&gt;
&lt;h2&gt;六、JS数组&lt;/h2&gt;
&lt;h4&gt;1、数组的创建方式&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;利用 new 创建数组&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var an_array = new Array()
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;通过数组字面量来创建数组（这种创建数组的方法与 Python 类似，但记住创建时要加 var）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var an_array = [1, true, &apos;an_string&apos;, undefined]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、数组访问方式&lt;/h4&gt;
&lt;p&gt;与其他语言基本一致&lt;/p&gt;
&lt;p&gt;但注意：&lt;strong&gt;直接使用不存在的数组下标访问并不会报错，而是返回 undefined&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;3、数组长度&lt;/h4&gt;
&lt;p&gt;访问可通过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(an_array.length)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、数组元素的追加&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;修改数组的 length 属性&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;an_array.length = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;直接给超出索引范围的下标对应元素赋值&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/* 假设数组已有三个元素 */
an_array[3] = &apos;这是新增的元素  ddddd&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;七、函数&lt;/h2&gt;
&lt;h4&gt;1、函数的定义&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function funcName(arg) {
// do something here
return result // if it need return...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;strong&gt;JS 的函数可以嵌套定义，但嵌套的函数只能在外层函数中使用，不可被外界直接引用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function outer() {
function inner() {
    ...
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、函数的参数&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;若实参个数多于形参，并不会报错，只是最后一个实参不参与运算&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;若实参个数小于形参，也并不会报错，只是缺少的参数没有接收值，默认为 undefined&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、函数 return 注意事项&lt;/h4&gt;
&lt;p&gt;若函数没有 return ，则返回的是 undefined&lt;/p&gt;
&lt;h4&gt;4、arguments对象&lt;/h4&gt;
&lt;p&gt;所有函数都有 arguments 对象，&lt;strong&gt;其存储了传递给函数的所有实参&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其为一个类数组对象（只支持 length属性和下标访问），可通过下标直接访问传入的各个参数&lt;/p&gt;
&lt;p&gt;通过 arguments 对象，可以实现函数可传入任意个数的参数，同时定义时也不必再写实参&lt;/p&gt;
&lt;h4&gt;5、函数的两种声明方式&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;利用函数关键字声明&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;function fn() {
console.log(&apos;第一种声明方式&apos;)
}
fn();
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;函数表达式（将函数定义赋值给一个变量）（匿名函数，类似于 Python 中的 lambda）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var fn = function() {
console.log(&apos;第二种声明方式&apos;)
}
fn();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、立即执行函数&lt;/h4&gt;
&lt;p&gt;在声明后即刻执行&lt;/p&gt;
&lt;p&gt;两种声明方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(function() {})();
// 或
(function() {} ());
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;(function(a, b) {
console.log(a + b);
})(1, 2);
// 或
(function(a, b) {
console.log(a + b);
} (1, 2));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、箭头函数&lt;/h4&gt;
&lt;p&gt;ES6 允许使用 “箭头” 定义函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var f = v =&amp;gt; v * v;

// 等同于：
var f = function(v) {
return v * v;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果箭头函数不需要参数或需要多个参数，就使用一个圆括号代表参数部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var f = () =&amp;gt; 5;

var sum = (num1, num2) =&amp;gt; num1 + num2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果箭头函数的代码块部分多于一条语句，就要使用大括号将它们括起来，并且使用 &lt;code&gt;return &lt;/code&gt; 语句返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var sum = (num1, num2) =&amp;gt; { return num1 + num2; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于大括号被解释为代码块，所以如果箭头函数直接返回一个对象，必须在对象外面加上括号，否则会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let getTempItem = id =&amp;gt; ({ id: id, name: &quot;Temp&quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;六、JS 作用域&lt;/h2&gt;
&lt;h4&gt;1、全局作用域&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;整个 script 标签，或是一个单独的js文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;局部作用域：函数或其他结构内部&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、变量的作用域&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;全局变量（与C语言类似）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;局部变量 （与C语言类似）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：&lt;strong&gt;在特殊情况下，在函数内不使用 var 声明的变量也是全局变量（不建议这样使用）&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;3、JS 块级作用域&lt;/h4&gt;
&lt;p&gt;注：在 ES6 的方法中才有块级作用域，同时新增了 let 和 const 关键字&lt;/p&gt;
&lt;p&gt;对 let 关键字：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;let不能在定义之前访问该变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;let不能被重新定义&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下为例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  if (1) {
  var num = 10
  }
  console.log(num)  // 此时外部是可以引用 num 变量的，因为 num 使用 var 初始化，作用范围是函数作用域
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  if (1) {
  let num = 10
  }
  console.log(num)  // 此时外部不可以引用 num 变量，因为 num 使用 let 初始化，作用范围是块级作用域
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  if (1) {
  const var num = 10
  }
  console.log(num)  // 此时不可以改变 num 的值
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、作用域链&lt;/h4&gt;
&lt;p&gt;内部函数访问外部函数的变量，采用的是链式查找的方式来决定取哪个变量，这种结构就是作用域链&lt;/p&gt;
&lt;p&gt;如：函数嵌套定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function outer() {  // 外层函数
var num = 20
function inner() {
    console.log(num)  // 内层函数
}
inner()
}
outer()  // 最后输出为20
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;七、JS 预解析&lt;/h2&gt;
&lt;p&gt;JS 引擎运行代码分为两步：预解析和代码执行&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;预解析：js 引擎会把 js 里面所有的 var 声明和 function 声明提升到当前作用域的最前面&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码执行：按照代码书写顺序从上往下执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;hi()
function hi() {
console.log(&apos;hi&apos;)
}
// 最后输出hi，这里是function声明的提升
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：预解析分为变量预解析（变量提升）和函数预解析（函数提升），&lt;strong&gt;在提升过程中，只提升声明，若同时有赋值操作是不会被提升的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(num)
var num = 10
// 最后输出 undefined，因为赋值不被提升
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;hi()
var hi = function() {
console.log(&apos;hi&apos;)
}
// 会直接报错，函数表达式中的函数不会被提升，那么只提升hi变量，但不进行赋值，因此hi对象没有被赋值函数
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;八、JS 面向对象&lt;/h2&gt;
&lt;h4&gt;1、对象&lt;/h4&gt;
&lt;p&gt;对象是一组无序的相关属性和方法的集合&lt;/p&gt;
&lt;p&gt;对象是由属性和方法组成的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;属性&lt;/strong&gt;：事物的特征，通常用名词表示&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法&lt;/strong&gt;：事物的行为，通常用动词来表示&lt;/p&gt;
&lt;h4&gt;2、创建对象&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;利用对象字面量创建对象&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var obj = {
person_name : &apos;Ironic&apos;,
height : 170,
weight : 120,
strengthening : function() {
    obj.height += 1
    obj.weight += 2
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用上述方法就创建了一个名为 obj 的对象，调用如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(obj.height)  // 常规方法
console.log(obj[&apos;height&apos;])  // 或使用类似 python 字典的方式调用属性

obj.strengthening()  // 调用方法
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用 new Object 创建对象&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var obj = new Object()  // 创建一个空对象
obj.uname = &apos;Ironic&apos;
obj.age = 18
obj.gender = &apos;male&apos;
obj.Hi = function() {
console.log(&apos;Hi~&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用构造函数创建对象（类似于定义类）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用前两种方法创建对象， 每次只能创建一个对象。构造函数方法可解决这个问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 构造函数（类似于其他语言的类定义）
// 类似于类定义，但不需要传入类的实例参数，会自动传值
function Human(_name, gender, height, weight) {
this._name = _name
this.gender = gender
this.height = height
this.weight = weight
this.strengthening = function() {
    this.height += 1
    this.weight += 10
}
}
var XiaoMing = new Human()  // 实例化一个人类对象...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更建议使用一下写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 更符合类与对象的思想
class Human {
// 此方法类似于 Python 中的__init__()方法
constructor(_name, gender, height, weight) {
    this._name = _name
  this.gender = gender
  this.height = height
  this.weight = weight
}
strengthening() {
    this.height += 1
    this.weight += 10
}
}
var XiaoMing = new Human()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、遍历对象&lt;/h4&gt;
&lt;p&gt;使用 for in 遍历对象的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 假设已有对象 obj
for (var k in obj) {
console.log(k, obj[k])  // 直接取得到的是属性名，若需要访问值，使用obj[k]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;九、JS内置对象&lt;/h2&gt;
&lt;h4&gt;1、JS 内置对象&lt;/h4&gt;
&lt;p&gt;JS 中对象分为3种：自定义对象、内置对象和浏览器对象&lt;/p&gt;
&lt;h4&gt;2、查文档&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查阅该方法的功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看里面参数的意义和类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看返回值的意义和类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 demo 测试&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、Math 对象（无需构造）&lt;/h4&gt;
&lt;p&gt;Math 对象不是构造函数，它具有数学常数和函数的属性方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Math.PI                    // 圆周率
Math.floor()               // 向下取整
Math.ceil()                // 向上取整
Math.round()               // 四舍五入
Math.abs()                 // 绝对值
Math.max() / Math.min()    // 求最大值和最小值
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、Math 对象随机数方法&lt;/h4&gt;
&lt;p&gt;使用 Math.random( ) 函数返回 [0, 1) 之间的浮点数&lt;/p&gt;
&lt;p&gt;若需要得到两个整数间的随机整数，可使用下面的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 范围：[min, max]
function randint(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、Date 对象（需要构造）&lt;/h4&gt;
&lt;p&gt;无参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var today = new Date()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var one_day = new Date(&apos;2019-10-1 8:8:8&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、Date 对象方法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var date = new Date()

date.getFullYear()

date.getMonth() + 1  // 默认返回的月份是[0, 11]中的，所以需要加1

date.getDate()  // 返回几号

date.getDay()  // 返回星期几,但注意周一到周六是1到6，而周日是0

date.getHours()  // 24h制

date.getMinutes()

date.getSeconds()

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、Date 对象获取毫秒时间戳&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var date = new Date()
date.valueOf()
// 或 
date.getTime()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;var date1 = +new Date()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Date.now()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、倒计时&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function countDown(givenTime) {
var currentTime = +new Date()
var givenTime = +new Date(givenTime)
time = (givenTime - currentTime) / 1000

var days = parseInt(time / 60 / 60 / 24)
days = days &amp;lt; 10 ? &apos;0&apos; + days : days
var hours = parseInt(time / 60 / 60 % 24)
hours = hours &amp;lt; 10 ? &apos;0&apos; + hours : hours
var mins = parseInt(time / 60 % 60)
mins = mins &amp;lt; 10 ? &apos;0&apos; + mins : mins
var secs = parseInt(time % 60)
secs = secs &amp;lt; 10 ? &apos;0&apos; + secs : secs

return days + &apos;天&apos; + hours + &apos;时&apos; + mins + &apos;分&apos; + secs + &apos;秒&apos;
}
console.log(countDown(&apos;2021-5-5 18:20:00&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9、数组对象&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 创建一个长度为2的空数组
var arr = new Array(2)

// 创建并初始化数组为 [1, 2]
var arr = new Array(1, 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9、数组常用方法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;push(element1, elements2, ...)  // 末尾添加一个或多个元素，返回新数组长度
pop()  // 与其他编程语言 pop() 类似
unshift(element1, element2, ...) // 开头添加一个或多个元素，返回新数组长度
shift()  // 弹出第一个元素
reverse()  // 对原数组操作

concat(arr1, arr2, ...)  // 连接数组
slice(a, b)  // 类似 Python 中作数组的切片，切片范围 [a, b)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;splice ( ) 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;splice(index, howmany, item1, item2, ...)
// index 为操作起始索引
// howmany 为删除多少个元素，为0则不删除元素
// itemX 新添加的元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sort( ) 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sort()  // 无参数按字符编码顺序排序

// 为使其按照数字大小排序：
sort(function(a, b) {
return a - b // 升序,需要降序替换为下行
// return b - a
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特别注意：在 JS 中函数参数若是数组或对象，&lt;strong&gt;传递的参数实际上是这些参数的地址&lt;/strong&gt;，函数内的操作会直接影响这些变量，&lt;strong&gt;这时应该复制对象，而不是直接赋值传递引用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面是常用的复制方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用扩展符 ... 复制
var arr1 = [1, 2, 3, 4, 5]
var arr2 = [...arr1, 6]
console.log(arr1)
console.log(arr2)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 使用 foreach() 方法，其对每个数组元素依次执行方法下函数定义的方法
// 原型： array.forEach(function(item, index, arr), thisValue)
// thisValue 指定 this 应该对应哪个对象，若为空，则对于 callback，this 为 undefined
// item 指数组元素的值
// index 指数组元素的索引
// arr 指操作的数组
var arr1 = [1, 2, 3, 4, 5]
var arr2 = new Array()
arr1.forEach(function(item) {
arr2.push(item)
})
arr2.push(6)
console.log(arr1)
console.log(arr2)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 使用 slice() 做全切片
// 这个简单，不写了ヽ(￣▽￣)ﾉ
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;10、数组索引方法&lt;/h4&gt;
&lt;p&gt;indexfOf( ) 方法 和 lastIndexOf( ) 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 函数原型： indexOf(value, index),从 index 开始匹配
// 从左到右查询第一个匹配的数组元素的索引，若不存在返回-1
array.indexOf(&apos;blue&apos;)

// 从左到右查询第一个匹配的数组元素的索引，若不存在返回-1
array.lastIndexOf(&apos;blue&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;11、数组转化为字符串方法&lt;/h4&gt;
&lt;p&gt;toString( ) 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array = [1, 2]
console.log(array.toString())  // 用逗号分隔
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;join( ) 方法 （类似 Python ）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array.join() // 无参数默认采用逗号分隔
array.join(&apos;/&apos;)  // 采用其他符号分隔
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;12、字符串对象&lt;/h4&gt;
&lt;p&gt;1、字符串具有不可变性&lt;/p&gt;
&lt;p&gt;字符串的值不能改变，修改字符串变量好像修改了字符串的值，但实际并没有。&lt;strong&gt;只是新开辟了内存，存储了新的字符串，重新改变了变量指向的内存空间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此：&lt;strong&gt;请尽量少使用字符串拼接，因为每拼接一次，就会产生一个新的字符串&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;13、字符串方法&lt;/h4&gt;
&lt;p&gt;注：所有字符串方法不会修改原字符串，只会返回新的字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 索引方法，类似数组使用即可
indexOf(index)
lastIndexOf(index)

// 查找方法，根据位置返回字符
str.charAt(index)、str[index]  // 作用一致
str.charCodeAt(index)  // 返回 ASCII 值，可判断按下了什么键

// 操作方法
concat(str1, str2, ...)
substr(start, length)  // 从 start 开始，取 length 个
slice(start, end)
substring(start, end)  // 基本和 slice 相同，但不接受负值

// 替换字符串
replace(searchValue, replaceValue)  // 默认只替换第一个字符，可使用 RegExp

// 字符串替换为数组，结合数组转化为字符串可实现数组复制
split(sep)  // 返回的是数组

// 大小写转换
toUpperCase()
toLowerCase()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;十、JS 简单类型与复杂类型&lt;/h2&gt;
&lt;h4&gt;1、简单类型和复杂类型&lt;/h4&gt;
&lt;p&gt;又称&lt;strong&gt;基本数据类型&lt;/strong&gt;或&lt;strong&gt;值类型&lt;/strong&gt;，复杂类型又叫做&lt;strong&gt;引用类型&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;值类型&lt;/strong&gt;&lt;/em&gt;：内存中存储的是本身&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例： String值, Number值, Boolean值, undefined, null&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但 null 比较特殊，返回的是一个空对象&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;引用类型&lt;/strong&gt;&lt;/em&gt;：复杂数据类型，在存储变量是存储的仅仅是地址（类似于指针）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过 new 关键字创建的对象（系统对象，自定义对象），如 Object、Array、Date等&lt;/p&gt;
&lt;h4&gt;2、堆和栈&lt;/h4&gt;
&lt;p&gt;简单数据类型存放在栈里面&lt;/p&gt;
&lt;p&gt;复杂数据类型存放在堆里面&lt;/p&gt;
&lt;h2&gt;十一、Web APIs&lt;/h2&gt;
&lt;p&gt;Web API 是浏览器提供的一套&lt;strong&gt;操作浏览器功能&lt;/strong&gt;和&lt;strong&gt;页面元素&lt;/strong&gt;的 API （BOM 和DOM）&lt;/p&gt;
&lt;h2&gt;十二、DOM&lt;/h2&gt;
&lt;h4&gt;1、DOM 树&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/dom-tree.vrBYfdre_12M8qt.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;文档：一个页面就是一个文档，在 DOM 中用 document 表示&lt;/p&gt;
&lt;p&gt;元素：页面所有标签都是元素，在 DOM 中使用 element 表示&lt;/p&gt;
&lt;p&gt;节点：网页中所有内容都是节点（标签、属性、文本、注释等），在 DOM 中用 node 表示&lt;/p&gt;
&lt;p&gt;注：DOM 把以上内容都看做对象&lt;/p&gt;
&lt;h4&gt;2、获取元素&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;根据 ID 获取（返回 element 对象，找不到返回 null ）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var element = document.getElementById(&apos;special element&apos;)
console.dir(element)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;根据标签名获取&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 返回对象的集合，以伪数组的形式存储，找不到返回空伪数组
// 注意：得到的元素对象是动态的
// 不一定从 document 起始，可以从任意父元素起始
var ols = document.getElementsByTagName(&apos;ol&apos;)
var lis = ols[0].getElementsByTagName(&apos;li&apos;)
console.dir(lis)
console.dir(lis[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;H5 新增方法获取&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 根据类名获取
var boxs = document.getElementsByClassName(&apos;box&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 根据指定选择器选择第一个匹配元素
var firstSection = document.querySelector(&apos;#wrapper &amp;gt; section&apos;)
// 根据指定选择器选择所有匹配节点，返回一个节点伪数组（注意是静态的）
var sections = document.querySelectorAll(&apos;#wrapper &amp;gt; section&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、获取特殊元素（body、html）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;获取 body 元素&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var body = document.body
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;获取 html 元素&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var html = document.documentElement
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、事件三要素&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;事件是可以被 JS 侦测到的行为&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;简单理解：触发——响应机制&lt;/p&gt;
&lt;p&gt;页面中每个元素都可以产生可以触发 JS 的事件，如点击按钮后执行某个操作&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;事件源（事件被触发的对象）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var btn = document.getElementById(&apos;btn&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;事件类型（即如何触发）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事件处理程序（通过一个函数赋值的方式完成）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;btn.onclick = function() {
alert(&apos;你点击了这个按钮！&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、执行事件的步骤&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;获取事件源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注册事件（绑定事件）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加事件处理程序（采取函数赋值的形式）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;6、常见的鼠标事件&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;鼠标事件&lt;/th&gt;
&lt;th&gt;触发条件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;onclick&lt;/td&gt;
&lt;td&gt;鼠标点击左键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onmouseover&lt;/td&gt;
&lt;td&gt;鼠标经过触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onmouseout&lt;/td&gt;
&lt;td&gt;鼠标离开触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onfoucs&lt;/td&gt;
&lt;td&gt;获得鼠标焦点触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onblur&lt;/td&gt;
&lt;td&gt;失去鼠标焦点触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onmousemove&lt;/td&gt;
&lt;td&gt;鼠标移动触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onmouseup&lt;/td&gt;
&lt;td&gt;鼠标弹起触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onmousedown&lt;/td&gt;
&lt;td&gt;鼠标按下触发&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;7、改变元素内容&lt;/h4&gt;
&lt;p&gt;innerText : 过滤 html 标签，同时文本格式（空格和换行）也会被去除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;element.innerText
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;innerHTML : 若有 HTML 标签，会被解释，同时保留文本格式（空白和换行）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;element.innerHTML
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、改变元素属性&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;element.attr  // 原有属性可使用此方法获取

// 对于自定义属性：
element.hasAttribute(&apos;data-2a3805c&apos;)  // 判断是否有某个自定义属性
element.getAttribute(&apos;data-2a3805f&apos;)  // 获取自定义属性 传入属性名
element.setAttribute(&apos;data-2a3806b&apos;, &apos;&apos;)  //设置自定义属性 传入要设置的属性名和属性值
element.removeAttribute(&apos;data-2a380ff&apos;)  // 传入要删除的属性名

// h5 新增获取属性的方法
// dataset 存放了所有以 data 开头的自定义属性
// 如
// &amp;lt;div data-index=&quot;1&quot; data-list-name=&quot;first&quot;&amp;gt;&amp;lt;/div&amp;gt;

// 取 data-index 属性：
div.dataset.index
div.dataset[&apos;index&apos;]

// 取 data-list-name 属性（注意使用驼峰命名法）:
div.dataset.listName
div.dataset[&apos;listName&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用 DOM 可以操作表单元素的以下属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type、value、checked、selected、disabled
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9、样式属性操作&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;element.style  // 行内样式操作
element.className  // 类名样式操作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;strong&gt;JS 中的样式采用驼峰命名法，如： fontSize&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;启发&lt;/strong&gt;：&lt;em&gt;使用 JS 操作样式属性，可批量设定精灵图中各图标的坐标 （使用循环计算坐标）&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;10、排他思想&lt;/h4&gt;
&lt;p&gt;思想：如果有同一组元素，我们只想要某一个元素实现某种样式，则需要用到排他思想算法&lt;/p&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;所有元素全部清除样式（即恢复默认样式）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;给当前元素设置样式&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;十三、节点操作&lt;/h2&gt;
&lt;h4&gt;1、为什么学习节点操作&lt;/h4&gt;
&lt;p&gt;原因：使用 DOM 提供的方法获取元素每个元素需要单独获取，逻辑性不强、繁琐。&lt;/p&gt;
&lt;p&gt;节点层级关系获取元素的优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;可利用父子兄弟节点关系获取元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑性强，但是兼容性稍差&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、节点概述&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;（节点定义参照 DOM 树一节的内容）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;节点至少拥有的基本属性&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;nodeType 节点类型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nodeName 节点名称&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nodeValue 节点值&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、nodeType 属性&lt;/h4&gt;
&lt;p&gt;元素节点 nodeType 为1&lt;/p&gt;
&lt;p&gt;属性节点 nodeType 为2&lt;/p&gt;
&lt;p&gt;文本节点 nodeType 为3（文本节点包括文字、空格和换行）&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;节点操作中主要是操作元素节点&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;4、节点层级操作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;父级节点&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var node = child.parentNode  // 取最近父节点，找不到返回 null
var element = child.parentElement // 取最近父元素节点
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;子节点&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var nodes = lis.childNodes  // 标准，返回Nodelist，其中包含了所有的子节点（属性节点，文本节点也在其中）
var elements = lis.children // 非标准，但各浏览器都支持，返回HTMLCollections（只包含元素节点）
// 注：二者获取的集合都是动态的

var node = lis.firstChild
var node = lis.lastChild
// 注：可匹配所有类型的节点

var element = lis.firstElementChild
var element = lis.lastElementChild
// 这里就是只匹配元素节点了，但 IE9 以下不支持
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用 childNodes 如果想要只获得其中的元素节点，则需要专门处理，所以一般不提倡使用 childNodes，而是使用 children&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;兄弟节点&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var node = div.nextSibling  // 向后的兄弟
var node = div.previousSibling // 向前的兄弟
// 都是匹配所有类型节点

var element = div.nextElementSibling
var element = div.previousElementSibling
// 有兼容性问题...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、节点创建和添加&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;节点创建&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 创建节点
var li = document.createElement(&apos;li&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：节点创建后要添加到指定位置才能体现在页面中&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;节点添加&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 追加节点
ul.appendChild(li)

// 插入节点
ul.insertBefore(li, ul.children[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;节点删除&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;theReturnNode = node.removeChild(child)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;复制（克隆）节点&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cloneNode = node.cloneNode()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果参数为空或为 false，则&lt;strong&gt;只是浅拷贝，只复制节点，不复制子节点&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果参数为 true，&lt;strong&gt;则是深拷贝&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;6、三种动态创建元素区别&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;document.write(&apos;&amp;lt;div&amp;gt;123&amp;lt;/div&amp;gt;&apos;)
element.innerHTML
element.createElement
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;区别：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;document.write 是直接将内容写入页面的内容流，&lt;strong&gt;但是文档流执行完毕，页面将会重绘&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;innerHTML 是将内容写入某个DOM节点，不会导致页面完全重绘。&lt;strong&gt;它创建多个元素效率最高&lt;/strong&gt;（但不要拼接字符串，采取数组形式拼接），结构较为复杂&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;var array=[]
for (var i = 0; i &amp;lt; 1000; i++) {
array.push(&apos;&amp;lt;div&amp;gt;content&amp;lt;/div&amp;gt;&apos;);
}
document.body.innerHTML = array.join(&apos;&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;createElement &lt;strong&gt;创建多个元素效率稍低，但结构清晰&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总结：&lt;strong&gt;不同浏览器下，innerHTML 效率比 createElement 效率更高&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;十四、事件高级&lt;/h2&gt;
&lt;h4&gt;1、注册事件概述&lt;/h4&gt;
&lt;p&gt;给元素添加事件，称为&lt;strong&gt;注册事件&lt;/strong&gt;或者&lt;strong&gt;绑定事件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注册事件两种方式：&lt;strong&gt;传统方式&lt;/strong&gt;或者&lt;strong&gt;方法监听注册方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注：以下注册方法均为传统方式：&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button onclick=&quot;alert(&apos;hi&apos;);&quot;&amp;gt;&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;btn.onclick = function() {
alert(&apos;hi&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特点：&lt;strong&gt;注册的事件具有唯一性&lt;/strong&gt;，同一个元素同一个事件只能设置一个处理函数，最后注册的处理函数会覆盖前面注册的处理函数&lt;/p&gt;
&lt;h4&gt;2、方法监听注册方式（推荐）&lt;/h4&gt;
&lt;p&gt;特点：同一个元素同一个时间可以注册多个监听器，按注册顺序依次执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eventTarget.addEventListener(type, listener[, useCapture]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;eventTarget.addEventListener( ) 方法将指定的监听器注册到 eventTarget（目标对象）上，当该对象触发指定的事件时，就会执行事件处理函数&lt;/p&gt;
&lt;p&gt;接受三个参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;type：时间类型字符串，&lt;strong&gt;这里不要带 on&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;listener：事件处理函数（&lt;strong&gt;此处传递函数名&lt;/strong&gt;），事件发生时，会调用该监听函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;useCapture：可选参数，是一个布尔值，默认是 false&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、删除事件的方式&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;传统注册的事件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;eventTarget.onclick = null // 赋值为 null
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;方法监听注册方式&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;eventTarget.removeEventListener(type, listener[, Capture]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采用方法监听注册方式的事件若需要在某个运行阶段删除，那么&lt;strong&gt;在注册时事件处理函数不要使用匿名函数，因为删除事件时需要提供函数名&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div.addEventListener(&apos;click&apos;, toAlert);
function toAlert() {
alert(&apos;hi~&apos;);
div.removeEventListener(&apos;click&apos;, toAlert);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、DOM 事件流&lt;/h4&gt;
&lt;p&gt;事件流描述的是从页面中接受事件的顺序&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;事件发生时会在元素节点之间按照特定的顺序传播，这个传播过程即 DOM 事件流&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DOM 事件分为三个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;捕获阶段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前目标阶段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;冒泡阶段&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/dom-eventFlow1.CKm0Eqze_1UB3TN.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/dom-eventFlow2.BBjMazln_TKCOb.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JS 代码只能执行捕获或冒泡其中一个阶段&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传统注册方式和 attachEvent 都是作用于冒泡阶段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;addEventListener 第三个参数如果是 true，表示在捕获阶段调用事件处理程序，如果为空或为 false，表示在冒泡阶段调用事件处理程序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实际开发中很少使用事件捕获，&lt;strong&gt;更关注事件冒泡&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有些事件是没有冒泡的，如 onblur、onfoucs、onmouseover、onmouseleave、load、unload&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5、事件对象&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;在注册事件时，事件对象会被系统自动创建，并传递给事件处理函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;事件处理函数若有形参，则第一个形参必是 event 对象，名称可以随意命名，一般命名为 e 或 event&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;事件对象只有有了事件才会存在，它是系统给我们创建的，不需要我们传递参数&lt;/p&gt;
&lt;p&gt;事件对象会&lt;strong&gt;包含一系列与该事件相关的属性和方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div.onclick = function(e) {
console.log(e);
}

div.addEventListener(&apos;click&apos;, function(e) {
console.log(e);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事件对象常见属性和方法：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;事件对象属性方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;e.target&lt;/td&gt;
&lt;td&gt;返回&lt;strong&gt;触发&lt;/strong&gt;事件的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e.type&lt;/td&gt;
&lt;td&gt;返回事件的类型（如 click，不带 on）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e.preventDefault()&lt;/td&gt;
&lt;td&gt;该方法阻止触发默认事件行为（如让 a 不跳转链接）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e.stopPropagation()&lt;/td&gt;
&lt;td&gt;阻止冒泡&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;e.target 说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ul.addEventListener(&apos;click&apos;, function(e) {
  console.log(e.target);  // 触发事件的对象（如从 li 触发）
  console.log(this);  // 绑定事件的对象（如为 ul 绑定）
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;e.preventPropagation( ) 说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div.addEventListener(&apos;click&apos;, function(e) {
  ...
  e.stopPropagation();
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补充：return false（会同时阻止默认行为和冒泡）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div.addEventListener(&apos;click&apos;, function(e) {
  ...
  return false
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、事件委托&lt;/h4&gt;
&lt;p&gt;事件委托，也称事件委派，事件代理。&lt;/p&gt;
&lt;p&gt;事件委托的原理：&lt;strong&gt;不是每个子节点单独设置事件监听器，而是事件监听器设置在其父节点上，然后利用冒泡原理影响每个子节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;优越性：减少了 DOM 访问次数，提高了效率&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ul.addEventListener(&apos;click&apos;, function(e) {
  alert(&apos;点击了一个li&apos;);
  e.target.style.backgroundColor = &apos;skyblue&apos;;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、常用的鼠标事件&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;禁止鼠标右键菜单&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;contextmenu 控制应该何时显示上下文菜单，主要用于程序员取消默认的上下文菜单&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.addEventListener(&apos;contextmenu&apos;, function(e) {
  e.preventDefault();
})
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;禁止鼠标选中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当光标开始选中时，执行默认事件 selectstart&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.addEventListener(&apos;selectstart&apos;, function(e) {
  e.preventDefault();    
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、鼠标事件对象&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;鼠标事件对象&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;e.clientX&lt;/td&gt;
&lt;td&gt;返回鼠标相对于浏览器窗口可视区的 X 坐标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e.clientY&lt;/td&gt;
&lt;td&gt;返回鼠标相对于浏览器窗口可视区的 Y 坐标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e.screenX&lt;/td&gt;
&lt;td&gt;返回鼠标相对于电脑屏幕的 X 坐标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e.screenY&lt;/td&gt;
&lt;td&gt;返回鼠标相对于电脑屏幕的 Y 坐标&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;9、常用键盘事件&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;键盘事件名称&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;keyup&lt;/td&gt;
&lt;td&gt;某个按键松开时触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keydown&lt;/td&gt;
&lt;td&gt;某个按键按下时触发，一直按着不断触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keypress&lt;/td&gt;
&lt;td&gt;与 onkeypress 类似，&lt;strong&gt;但不识别功能键&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三个事件执行顺序：keydown、keypress、keyup&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;10、键盘事件对象&lt;/h4&gt;
&lt;p&gt;使用 keyCode 属性可以得到相应键的 ASCII 值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function(e) {
console.log(e.keyCode);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;keyup 和 keydown 不区分字母大小写，而 keypress 区分字母大小写&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;keydown 和 keypress  在文本框的特点：先于字符落入文本框触发；而 keyup 触发时，字符已经落入文本框了&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;11、其他一些事件&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;transitionend&lt;/code&gt; 事件，在过渡完成后触发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div.addEventListener(&apos;transitionend&apos;, function() {
...
// do something here
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;12、事件触发 dispatchEvent&lt;/h4&gt;
&lt;p&gt;有时我们需要直接去触发事件，此时可使用事件触发 dispatchEvent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// new Event(eventType);
let e = new Event(&quot;click&quot;);
// event.initEvent(eventType,canBubble,cancelable)
e.initEvent(e.type, true, true);
btn.dispatchEvent(e);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;十五、BOM&lt;/h2&gt;
&lt;h4&gt;1、BOM 概述&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;什么是 BOM&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;BOM 即浏览器对象模型，它提供了&lt;strong&gt;独立于内容而与浏览器窗口进行交互的对象&lt;/strong&gt;，其核心对象是 window&lt;/p&gt;
&lt;p&gt;注：&lt;strong&gt;BOM 缺乏标准&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;BOM 的构成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;BOM 比 DOM 更大，包含了 DOM&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/BOM-structure.BnMfgyC2_1tjrWn.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;window 对象是浏览器的顶级对象，具有双重角色：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;它是 JS 访问浏览器窗口的一个接口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;它是一个全局对象，&lt;strong&gt;定义在全局作用域中的变量、函数都会变成 window 的属性和方法&lt;/strong&gt;，只是在调用时可以省略 window&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、window 页面加载事件&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;window.onload&lt;/code&gt; 当&lt;strong&gt;文档内容完全加载完&lt;/strong&gt;触发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.onload  = function() {}; // 不推荐，会发生覆盖

window.addEventListener(&apos;load&apos;, function() {});
// 推荐，可添加多个行为
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;window.onpageshow&lt;/code&gt; 事件类似于 &lt;code&gt;window.onload&lt;/code&gt;事件，onload 事件在页面第一次加载时触发， onpageshow 事件在每次加载页面时触发，即 onload 事件在页面从浏览器缓存中读取时不触发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.onpageshow  = function() {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看页面是从服务器上载入还是从缓存中读取，可以使用事件对象 persisted 属性来判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&apos;pageshow&apos;, function(e) {
console.log(e.persisted);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DOMContentLoaded&lt;/code&gt; 事件当 DOM 加载完毕就触发，相对效率比 load 事件更高&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&apos;DOMContentLoaded&apos;, function() {});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、调整窗口大小事件&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;window.onresize&lt;/code&gt; 是调整窗口大小加载事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.onresize = function() {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;只要窗口大小发生像素变化，就会触发这个事件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们经常利用这个事件完成响应式布局&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4、定时器&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;一次性：&lt;code&gt;window.setTimeout( )&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;用于设置一个定时器，该定时器在到期后执行调用函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.setTimeout(callbackFunc, timeout);

// callbackFunc：回调函数
// timeout：延时时间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;页面可能有很多定时器，我们常给定时器加标识符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var timer1 = setTimeout(callback1, 3000);
var timer2 = setTimeout(callback2, 2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止 &lt;code&gt;setTimeout( )&lt;/code&gt; 计时器：&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;clearTimeout(timer)&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var timer = setTimerout(callback, 5000);
btn.addEventListener(&apos;click&apos;, function() {
clearTimeout(timer);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;重复调用：&lt;code&gt;setInterval( )&lt;/code&gt; 函数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;window.setInterval(callback, timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（使用方法类似）&lt;/p&gt;
&lt;h4&gt;5、this 指向问题&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;this 的指向在函数定义的时候是确定不了的，只有函数执行才能确定 this 到底指向谁，一般 this 最终指向的是哪个调用它的对象&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 全局作用域或普通函数中 this 指向全局对象 window （定时器中的 this 因此也是指向 window 的） --&amp;gt;
&amp;lt;script&amp;gt;
// 以下 this 全部指向 window 对象
  console.log(this);

function fn() {
    console.log(this);
}
fn();

setTimeout(function() {
    console.log(this);
}, 1000);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 方法调用中谁调用方法， this 指向谁 --&amp;gt;
&amp;lt;script&amp;gt;
// 以下 this 指向调用方法的对象
  var obj = {
    sayHi: function() {
        console.log(this);
    }
}
obj.sayHi();

var btn = document.querySelector(&apos;button&apos;);
btn.onclick = function() {
    console.log(this);
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 构造函数中 this 指向构造函数的实例 --&amp;gt;
&amp;lt;script&amp;gt;
// 以下 this 指向构造函数的实例
  function Func() {
    console.log(this);
}
var fun = new Func();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、JS 执行队列&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;JS 一大特点：单线程&lt;/strong&gt;。因此，同一个时间只能做一件事，这导致的问题是：如果 JS&lt;/p&gt;
&lt;p&gt;执行中遇到阻塞操作，会造成页面渲染的不连贯，导致页面渲染加载阻塞。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因此在新标准中，允许 JS 创建多个线程，JS 中出现了同步和异步。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;同步任务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同步任务都在主线程上执行，形成一个执行栈&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;异步任务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;JS 的异步都是通过回调函数实现的&lt;/p&gt;
&lt;p&gt;一般而言，异步任务分为以下三种类型：&lt;/p&gt;
&lt;p&gt;a、普通事件，如 click，resize 等&lt;/p&gt;
&lt;p&gt;b、资源加载，如 load，error 等&lt;/p&gt;
&lt;p&gt;c、定时器，包括 setInterval，setTimeout 等&lt;/p&gt;
&lt;p&gt;异步任务相关&lt;strong&gt;回调函数&lt;/strong&gt;添加到&lt;strong&gt;任务队列&lt;/strong&gt;中&lt;/p&gt;
&lt;p&gt;机制：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/javascript-execution-mechanism.DZOdmdyB_ZEm0mE.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;7、location 对象&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;URL&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;统一资源定位符（Uniform Resource Locator）是互联网上标准资源的地址，互联网上的每个文件都有一个唯一的URL，它包含的信息指出文件的位置以及浏览器应该怎么处理它。&lt;/p&gt;
&lt;p&gt;语法格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol://host[:port]/path/[?query]#fragment
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组成&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;protocol&lt;/td&gt;
&lt;td&gt;通信协议，如 htttp，ftp 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;host&lt;/td&gt;
&lt;td&gt;主机（域名）www.example.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;port&lt;/td&gt;
&lt;td&gt;端口号，可选，省略时使用默认端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;path&lt;/td&gt;
&lt;td&gt;路径由零或多个 &apos;/&apos; 符号分隔的字符串，一般用来表示主机上的一个目录或文件地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;query&lt;/td&gt;
&lt;td&gt;参数，以键值对的形式组成，用 &amp;amp; 分隔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fragement&lt;/td&gt;
&lt;td&gt;片段，常见于链接锚点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;location 对象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;window 对象给我们提供了一个 location 属性用于获取或设置窗体的 URL，并且可以用于解析 URL，因为该属性返回的是一个对象，所以也将该属性称为 location 对象&lt;/p&gt;
&lt;p&gt;location 对象常用属性：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;返回值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;location.href&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;获取或设置整个 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;location.host&lt;/td&gt;
&lt;td&gt;返回主机（域名）www.example.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;location.port&lt;/td&gt;
&lt;td&gt;返回端口号，若端口号为空返回空&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;location.pathname&lt;/td&gt;
&lt;td&gt;返回路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;location.search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;返回参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;location.hash&lt;/td&gt;
&lt;td&gt;返回片段 #后面内容常见于链接锚点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;locating 对象常用方法：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;location.assign( )&lt;/td&gt;
&lt;td&gt;跟 href 一样，可以跳转页面（也称重定向页面）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;location.replace( )&lt;/td&gt;
&lt;td&gt;替换当前页面，但不记录历史&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;location.reload( )&lt;/td&gt;
&lt;td&gt;重载页面，参数为 true 为强制刷新&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;8、navigator 对象&lt;/h4&gt;
&lt;p&gt;navigator 对象包含了有关浏览器的信息，他有很多属性，最常用的是 userAgent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.navigator.userAgent
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9、history 对象&lt;/h4&gt;
&lt;p&gt;常用方法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;history.back( )&lt;/td&gt;
&lt;td&gt;后退&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;history.forward( )&lt;/td&gt;
&lt;td&gt;前进&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;history.go(num)&lt;/td&gt;
&lt;td&gt;通过正负和数值大小控制前进后退&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;十六、PC 端网页特效&lt;/h2&gt;
&lt;h4&gt;1、元素偏移量 offset 系列&lt;/h4&gt;
&lt;p&gt;我们会用 offset 系列相关属性可以动态的得到该元素的位置（偏移）、大小等&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;返回的值都不带单位&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;offseet 系列属性&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;element.offsetParent&lt;/td&gt;
&lt;td&gt;返回作为该元素带有定位的父级元素，若父级都没有定位，返回body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.offsetTop&lt;/td&gt;
&lt;td&gt;返回元素相对带有定位父元素上方的偏移&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.offsetLeft&lt;/td&gt;
&lt;td&gt;返回元素相对带有定位父元素左侧的偏移&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.offsetWidth&lt;/td&gt;
&lt;td&gt;返回自身宽度，包括 padding，content，border&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.offsetHeight&lt;/td&gt;
&lt;td&gt;返回自身高度，包括 padding，content，border&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;offset 和 style 获取的区别：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/offset%20and%20style.CWbU0I2q_Z2dxSuL.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2、元素可视区 client 系列&lt;/h4&gt;
&lt;p&gt;client 翻译过来就是客户端，我们使用 client 系列的相关属性来获取元素可视区的相关信息，&lt;strong&gt;通过 client 系列属性可以动态的得到元素的可视区大小和边框大小&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;client 系列属性&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;element.clientTop&lt;/td&gt;
&lt;td&gt;返回元素上边框大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.clientLeft&lt;/td&gt;
&lt;td&gt;返回元素左边框大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.clientWidth&lt;/td&gt;
&lt;td&gt;返回元素可视区大小，即不包含 border 及其外部分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.clientHeight&lt;/td&gt;
&lt;td&gt;返回元素可视区大小，即不包含 border 及其外部分&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;3、元素滚动 scroll 系列&lt;/h4&gt;
&lt;p&gt;使用 scroll 属性可动态得到元素的大小，滚动距离等&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scroll 属性&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;element.scrollTop&lt;/td&gt;
&lt;td&gt;返回被卷去的上侧距离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.scrollLeft&lt;/td&gt;
&lt;td&gt;返回被卷去的下侧距离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.scrollWidth&lt;/td&gt;
&lt;td&gt;返回自身实际宽度（以溢出的宽度为准）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;element.scrollHeight&lt;/td&gt;
&lt;td&gt;返回自身实际高度（以溢出的高度为准）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;页面被卷去的头部可以用 window.pageYOffset 获得，被卷去的左侧可以用 window.pageXoffset 获得&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;附（判断竖向滚动条是否到达底部，横向类似）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parseInt(scrollY) + document.documentElement.clientHeight == document.documentElement.scrollHeight
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;window&lt;/code&gt; 没有获取整高方法，因此使用 &lt;code&gt;html&lt;/code&gt; 的 &lt;code&gt;scrollHeight&lt;/code&gt;，但与此搭配最好使用 &lt;code&gt;html&lt;/code&gt; 的 &lt;code&gt;clientHeight&lt;/code&gt;，而不是 &lt;code&gt;window.innerHeight&lt;/code&gt;，因为如果页面下方存在横向滚动条，&lt;code&gt;window.innerHeight&lt;/code&gt; &amp;gt; &lt;code&gt;document.documentElement.clientHeight&lt;/code&gt;，此时就不准了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而 &lt;code&gt;scrollY&lt;/code&gt;，&lt;code&gt;pageYoffset&lt;/code&gt;，&lt;code&gt;document.documentElement.scrollTop&lt;/code&gt; 三者永远一致且返回值都为浮点数，因此选择最简单的并取整即可&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;4、mouseseenter 和 mouseover 区别&lt;/h4&gt;
&lt;p&gt;移动到元素之上都会触发两种事件，但 mouseover 移动到子盒子上还会触发（有冒泡的特性），mouseseenter 只会经过自身盒子触发（因为不冒泡）&lt;/p&gt;
&lt;p&gt;类似的 mouseleave（不冒泡） 和 mouseout（冒泡） 也是一样&lt;/p&gt;
&lt;h4&gt;5、简单动画函数封装&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function animate(obj, target) {
clearInterval(obj.timer);  // 每次点击前清除一次计时器，防止多次点击
obj.timer = setInterval(function() {
    if (obj.offsetLeft &amp;gt;= target) { // 封装到属性中，防止开辟过多内存空间
        clearInterval(obj.timer);
    }
    obj.style.left = obj.offsetLeft + 1 + &apos;px&apos;;
}, 30);
}

// 下方为调用实例：
animate(div, 400);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、缓动动画原理&lt;/h4&gt;
&lt;p&gt;公式：&lt;strong&gt;（目标值 - 当前位置）/  倍率&lt;/strong&gt;（倍率越小变化越快）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function animate(obj, target) {
clearInterval(obj.timer);
obj.timer = setInterval(function() {
    var step = (target - obj.offsetLeft) / 10; // 计算步长
    step = step &amp;gt; 0 ? Math.ceil(step) : Math.floor(step);
    // 根据运动方向，需要设置不同类型的取整，以保证元素可以刚好运行至指定目标
    if (obj.offsetLeft == target) {
        clearInterval(obj.timer);
    }
    obj.style.left = obj.offsetLeft + step + &apos;px&apos;;
}, 30);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;在实际使用时可将函数封装在 JS 中&lt;/p&gt;
&lt;h4&gt;7、缓动动画添加回调&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function animate(obj, target, callback) {
clearInterval(obj.timer);
obj.timer = setInterval(function() {
    var step = (target - obj.offsetLeft) / 10;
    step = step &amp;gt; 0 ? Math.ceil(step) : Math.floor(step);
    if (obj.offsetLeft == target) {
        clearInterval(obj.timer);
        callback &amp;amp;&amp;amp; callback();  // 添加回调
    }
    obj.style.left = obj.offsetLeft + step + &apos;px&apos;;
}, 30);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、网页轮播图（焦点图）&lt;/h4&gt;
&lt;p&gt;功能需求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;鼠标经过轮播图模块，左右按钮显示，离开隐藏&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击按钮，图片向相应方向滚动一张&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;图片播放时，下面小圆圈模块跟随一起变化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击小圆圈，可以播放相应图片&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不经过轮播图，也会自动播放&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;鼠标经过，轮播图模块自动播放停止&lt;/em&gt;（该项可以不实现）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;十七、移动端特效&lt;/h2&gt;
&lt;h4&gt;1、触摸事件&lt;/h4&gt;
&lt;p&gt;移动端独有事件：触屏事件 tuoch。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;常见触屏事件&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;touchstart&lt;/td&gt;
&lt;td&gt;受制触摸到一个 DOM 元素时触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;touchmove&lt;/td&gt;
&lt;td&gt;受制放在一个 DOM 元素上滑动时触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;touchend&lt;/td&gt;
&lt;td&gt;受制从一个 DOM  元素上移开时触发&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;2、触摸事件对象（TouchEvent）&lt;/h4&gt;
&lt;p&gt;三个触屏事件都有各自事件对象&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;触摸列表&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;touches&lt;/td&gt;
&lt;td&gt;正在触摸屏幕的所有手指的一个列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;targetTouches&lt;/td&gt;
&lt;td&gt;正在触摸当前 DOM 元素上的手指的一个列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;changedTouches&lt;/td&gt;
&lt;td&gt;手指状态发生了改变的列表，从无到有，从有到无变化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;3、classList 属性&lt;/h4&gt;
&lt;p&gt;classList 是 HTML5 新增的一个属性。返回元素的类名。&lt;/p&gt;
&lt;p&gt;该属性用于在元素中添加，移除及切换 CSS 类。&lt;/p&gt;
&lt;p&gt;有以下方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4、// 追加类，不会发生覆盖
div.classList.add(&apos;new-name&apos;);

// 移除类
div.classList.remove(&apos;past-name&apos;);

// 切换类
div.classList.toggle(&apos;another-name&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、移动端 click 延时解决方案&lt;/h4&gt;
&lt;p&gt;移动端 click 事件由于需要判断是否是双击，因此需要等待 300ms，会造成延迟。&lt;/p&gt;
&lt;p&gt;解决方案：&lt;em&gt;建议通过 fastclick 插件解决&lt;/em&gt;。&lt;/p&gt;
&lt;h4&gt;5、移动端插件的使用&lt;/h4&gt;
&lt;p&gt;轮播图插件：swiper&lt;/p&gt;
&lt;p&gt;视频插件：zy.media.js&lt;/p&gt;
&lt;p&gt;其他常见插件：superslide，iscroll&lt;/p&gt;
&lt;h4&gt;6、移动端开发框架&lt;/h4&gt;
&lt;p&gt;bootstrap，jQuery，vue，angular，react 等&lt;/p&gt;
&lt;h2&gt;十八、本地存储&lt;/h2&gt;
&lt;h4&gt;1、本地存储&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;数据存储在用户浏览器中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置、读取方便、甚至页面刷新不丢失数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;容量较大，sessionStorage 约5M，localStorage 约20M&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只能存储字符串，可以将对象 JSON.stringify() 编码后存储&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4、seesionStorage&lt;/h4&gt;
&lt;p&gt;对应对象：&lt;code&gt;window.sessionStorage&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生命周期为关闭浏览器窗口&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在同一个窗口（页面）下数据可以共享&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以键值对的形式存储使用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 存储数据
sessionStorage.setItem(key, value);

// 获取数据
sessionStorage.getItem(key);

// 删除数据
sessionStorage.removeItem(key);

// 清除所有数据
sessionStorage.clear();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、localStorage&lt;/h4&gt;
&lt;p&gt;对应对象：&lt;code&gt;window.localStorage&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;声明周期永久生效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以多窗口（页面）共享（同一浏览器可共享）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以键值对的形式存储使用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;方法与 sessionStorage 类似&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h1&gt;HTML5、CSS3 提高&lt;/h1&gt;
&lt;h2&gt;一、HTML5&lt;/h2&gt;
&lt;h4&gt;1、HTML5新增的语义化标签&lt;/h4&gt;
&lt;p&gt;以前布局，我们基本都是用div来做，但div对搜索引擎来说，是没有语义的&lt;/p&gt;
&lt;p&gt;因此可以使用下面这些标签：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/h5-new-labels.BF_k07ix_2hvYut.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;header&amp;gt;  &amp;lt;!-- 头部标签 --&amp;gt;
&amp;lt;nav&amp;gt;    &amp;lt;!-- 导航标签 --&amp;gt;
&amp;lt;article&amp;gt;  &amp;lt;!-- 内容标签 --&amp;gt;
&amp;lt;section&amp;gt;  &amp;lt;!-- 定义文档某个区域 --&amp;gt;
&amp;lt;aside&amp;gt;    &amp;lt;!-- 侧边栏标签 --&amp;gt;
&amp;lt;footer&amp;gt;  &amp;lt;!-- 尾部标签 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;这些语义化标准主要是针对搜索引擎的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这些标签在页面中可以&lt;strong&gt;使用多次&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在IE9中，需要将这些标签转化为块级元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动端使用更多&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;#2、HTML5新增的多媒体标签&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;视频 &amp;lt;video&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;支持三种格式： mp4、webm、ogg（尽量使用mp4)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;autoplay&lt;/td&gt;
&lt;td&gt;autoplay&lt;/td&gt;
&lt;td&gt;视频就绪自动播放（谷歌浏览器需要添加muted&amp;lt;br /&amp;gt;来解决自动播放问题）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;controls&lt;/td&gt;
&lt;td&gt;controls&lt;/td&gt;
&lt;td&gt;向用户提供播放控件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;width&lt;/td&gt;
&lt;td&gt;(pixels)&lt;/td&gt;
&lt;td&gt;设置播放宽度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;(pixels)&lt;/td&gt;
&lt;td&gt;设置播放器高度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;设置是否循环播放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src&lt;/td&gt;
&lt;td&gt;(url)&lt;/td&gt;
&lt;td&gt;视频url地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;poster&lt;/td&gt;
&lt;td&gt;(Imgurl)&lt;/td&gt;
&lt;td&gt;等待加载的画面图片&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;muted&lt;/td&gt;
&lt;td&gt;muted&lt;/td&gt;
&lt;td&gt;静音播放&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;音频&amp;lt;audio&amp;gt;标签&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;支持三种格式：mp3、wav、ogg（尽量使用ogg）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;autoplay&lt;/td&gt;
&lt;td&gt;autoplay&lt;/td&gt;
&lt;td&gt;如果出现该属性，则音频就绪后就播放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;controls&lt;/td&gt;
&lt;td&gt;controls&lt;/td&gt;
&lt;td&gt;如果出现该属性，则显示播放控件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;loop&lt;/td&gt;
&lt;td&gt;如果出现该属性，则循环播放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src&lt;/td&gt;
&lt;td&gt;(url)&lt;/td&gt;
&lt;td&gt;音频的url&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;谷歌浏览器同样也将audio的自动播放禁用了，目前没有好的解决方案，需要使用 JS 解决&lt;/p&gt;
&lt;h4&gt;3、HTML5新增表单及属性&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;HTML5新增的 input 表单&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;table style=&quot;text-align: center;&quot;&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;th&amp;gt;属性值&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;说明&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;email&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;限制Email类型&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;url&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;限制URL类型&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;date&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;限制日期类型&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;time&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;限制时间类型&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;month&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;限制月类型&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;week&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;限制周类型&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;number&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;限制数字类型&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;tel&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;限制手机号码&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;search&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;搜索框&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;type=&quot;color&quot;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;生成一个颜色选择表单&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;HTML5新增的表单属性&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;使表单不可为空提交（也就是表单必填）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;placeholder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;（提示文本）&lt;/td&gt;
&lt;td&gt;表单的提示信息，存在默认值将不再显示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autofoucs&lt;/td&gt;
&lt;td&gt;autofocus&lt;/td&gt;
&lt;td&gt;自动聚焦属性，页面加载完自动聚焦到指定表单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autocomplete&lt;/td&gt;
&lt;td&gt;off / on&lt;/td&gt;
&lt;td&gt;当用户在字段开始键入时，浏览器基于之前键入过的值，应该显示出在字段中填写的选项。&amp;lt;br/&amp;gt;默认已经打开，如autocomplete=&quot;on&quot;，关闭 autocomplete=&quot;off&quot;&amp;lt;br/&amp;gt;需要放在表单内，同时加上name属性，同时成功提交&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;multiple&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;multiple&lt;/td&gt;
&lt;td&gt;可以多选文件提交&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;二、CSS3新特性&lt;/h2&gt;
&lt;h4&gt;1、属性选择器&lt;/h4&gt;
&lt;p&gt;属性选择器可以根据元素特定属性来选择元素，这样就可以不借助于类选择器或 id 选择器&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择符&lt;/th&gt;
&lt;th&gt;简介&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E[attr]&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;选择带有 attr 属性的E元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E[attr=value]&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;选择带有attr属性，且值为value的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E[attr^=value]&lt;/td&gt;
&lt;td&gt;匹配带有attr属性，且值以value开头的E元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E[attr$=value]&lt;/td&gt;
&lt;td&gt;匹配带有attr属性，且值以value结尾的E元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E[attr*=value]&lt;/td&gt;
&lt;td&gt;匹配带有attr属性，且值中含有value的E元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;input[value] {
color: skyblue;
}
input[type=&quot;text&quot;] {
color: pink;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;strong&gt;类选择器、属性选择器、伪类选择器，权重都为10&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2、结构伪类选择器（重难点）&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择符&lt;/th&gt;
&lt;th&gt;简介&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E:first-child&lt;/td&gt;
&lt;td&gt;匹配父元素的&lt;strong&gt;序号为1的子元素E&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E:last-child&lt;/td&gt;
&lt;td&gt;匹配父元素的&lt;strong&gt;序号为最后一个的子元素E&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E:nth-child(n)&lt;/td&gt;
&lt;td&gt;匹配父元素的&lt;strong&gt;序号为指定规则运算结果的子元素E&lt;/strong&gt;&amp;lt;br /&amp;gt;n可以为数字，也可以是关键字或公式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：&lt;strong&gt;在编序号时无论子元素是否为E，都会被标上序号&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 伪类前不加元素则默认选择第一个元素 */
div :first-child {
background-color: skyblue;
}
/* 伪类前加一个元素则选择第一个此类型的元素 */
ul li:last-child {
background-color: pink;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* 选择ul中的第五个li */
ul li:nth-child(5) {
background-color: pink;
}
/* 关键字使用，可以使用even或odd */
ul li:nth-child(even) {
background-color: skyblue;
}
/* 从0开始，每次加1往后计算，计算结果超出范围的忽略 */
ol li:nth-child(2n+1) {
background-color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择符&lt;/th&gt;
&lt;th&gt;简介&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E:first-of-type&lt;/td&gt;
&lt;td&gt;选择&lt;strong&gt;指定类型元素E的第1个&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E:last-of-type&lt;/td&gt;
&lt;td&gt;选择&lt;strong&gt;指定类型元素E的最后一个&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E:nth-of-type(n)&lt;/td&gt;
&lt;td&gt;选择&lt;strong&gt;指定类型的，序号由指定规则运算出的元素E&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：除序号编制上与 child 不同外，用法与其相同&lt;/p&gt;
&lt;p&gt;从实现原理上看二者的区别：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;nth-child 对父元素里面所有子元素进行排序再选择，再看元素是否和序号匹配，若匹配则选择，不匹配则不选择&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nth-of-type 对父元素里面指定子元素进行排序再选择，即先匹配了所有的 E 元素，再根据序号去找第 n 个 E&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3、伪元素选择器&lt;/h4&gt;
&lt;p&gt;伪元素选择器可以帮助我们利用CSS创建新标签元素，而不需要使用HTML标签，从而简化类HTML结构&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选择符&lt;/th&gt;
&lt;th&gt;简介&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;::before&lt;/td&gt;
&lt;td&gt;在元素内部的前面插入内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;::after&lt;/td&gt;
&lt;td&gt;在元素内部的后面插入内容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
div::before {
    content: &apos;这是before伪类选择器&apos;;
}
div::after {
    content: &apos;这是after伪类选择器&apos;;
}
&amp;lt;/style&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div&amp;gt;
    这段文字前后各有一个伪类选择器
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;::before 和 ::after 创建一个元素，这个元素&lt;strong&gt;属于行内元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;该元素在文档树中找不到&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;::before 和 ::after &lt;strong&gt;必须有 content 属性&lt;/strong&gt;，不需要内容时可以留空&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;::before &lt;strong&gt;在父元素内容前&lt;/strong&gt;创建元素，::after 在后面创建&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;伪元素和标签选择器一样，权重为1&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;6、CSS3盒子模型&lt;/h4&gt;
&lt;p&gt;CSS3中可以通过 box-sizing 来指定盒子模型，这样我们计算盒子大小的方法就发生了改变&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;content-box&lt;/td&gt;
&lt;td&gt;盒子大小为 width（默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;border-box&lt;/td&gt;
&lt;td&gt;盒子大小为 width + padding + border&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;div {
box-sizing: border-box;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：若我们选择了 border-box 盒子模型，border、margin 和 padding 就不会撑开盒子了（前提是margin 和 padding 不超过 width）&lt;/p&gt;
&lt;h4&gt;7、CSS3其他特性（了解）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;CSS3滤镜 filter&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;filter CSS属性将模糊或颜色偏移等图形效果应用于元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;img { 
filter: blur(5px);  /* 模糊处理 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;CSS3 calc函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;calc() 函数让你在声明CSS属性值时执行一些计算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
width: calc(100% - 10px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;括号内可以使用 + - * / 运算&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CSS3过渡（重点）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;transition是CSS3中具有颠覆性的特征之一，可以在不使用 JS 或 Flash 的情况下，为元素从一种样式转变到另一种样式添加过渡&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;经常和 :hover 搭配使用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* transition: 要过渡的属性 花费时间 运动曲线 何时开始; */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各参数详解：&lt;/p&gt;
&lt;p&gt;a、&lt;strong&gt;属性&lt;/strong&gt;：想要变化的CSS属性，宽高、背景颜色、内外边框都可以，想要所有属性都变化过渡，写一个 all 就可以&lt;/p&gt;
&lt;p&gt;b、&lt;strong&gt;花费时间&lt;/strong&gt;：单位是秒（必须写单位），比如0.5s&lt;/p&gt;
&lt;p&gt;c、&lt;strong&gt;运动曲线&lt;/strong&gt;：默认是ease（可以忽略）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/motion-curve.Bit6bkDP_ZHpq3K.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;d、&lt;strong&gt;何时开始&lt;/strong&gt;：单位是秒（必须写单位），可以设置延迟触发时间，默认是0s（可以省略）&lt;/p&gt;
&lt;p&gt;注：&lt;strong&gt;过渡加在元素的选择器上，而不是元素的 hover 伪类选择器上&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
width: 100px;
height: 100px;
transition: width .5s ease .1s;
}
div:hover {
width: 300px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* 若需要同时过渡多个属性，用逗号分隔 */
div {
width: 100px;
height: 100px;
transition: width .5s ease .1s, height .5s ease .1s;
}
div:hover {
width: 300px;
height: 300px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;e、速度曲线补充：steps( )步长：分几步来完成（简单理解：打断连续的变化过程，实现指定次数的分段变化效果，但总用时仍不变）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;应用：*实现打字机效果*
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;div {
  width: 0px;
height: 100px;
transition: all 5s steps(10) .1s;
}
div:hover {
width: 100px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、CSS3提高&lt;/h2&gt;
&lt;h4&gt;#1、2D转换之移动 translate&lt;/h4&gt;
&lt;p&gt;2D移动是2D转换中的一种功能，可以改变元素在页面中的位置，类似于相对定位&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 合写 */
div {
transform: translate(100px, 100px);
}
/* 分开写 */
div {
transform: translateX(100px) translateY(100px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;translate 最大的优点：不会影响其他元素的布局&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;若使用百分比单位，其参照对象是自身&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对行内标签没有效果&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、2D转换之旋转 rotate&lt;/h4&gt;
&lt;p&gt;2D旋转指的是让元素在2维平面内顺时针旋转或逆时针旋转&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
transform: rotate(45deg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;度数单位是deg, 如：rotate(45deg);&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;默认旋转中心点是元素的中心点&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;旋转中心的设置&lt;/strong&gt;：使用 transform-origin 属性&lt;/p&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用关键字（center left right top bottom)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用像素值或百分比设置&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;div {
transform-origin: 10px 20px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用场景：&lt;em&gt;下拉菜单的三角可由带边框透明背景的盒子旋转得到&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;3、2D转换之缩放scale&lt;/h4&gt;
&lt;p&gt;缩放，顾名思义，可以放大和缩小&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
transform: scale(x, y);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;x 和 y 位置填缩放倍数，若两个参数一致，可只写一个参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;scale 最大的优势：&lt;strong&gt;可以设置任意点为基准缩放，同时缩放不影响其他盒子&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4、transform 连写&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/* 使用空格隔开 */
div {
transform: translate() rotate() scale();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;strong&gt;一定要先写位移属性，因为其他属性会改变坐标轴方向&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;5、CSS3动画（重点）&lt;/h4&gt;
&lt;p&gt;动画（Animation）是CSS3中具有颠覆性的特征之一，可通过设置多个节点来精确控制一个或一组动画，常用来实现复杂的动画效果&lt;/p&gt;
&lt;p&gt;动画使用分为两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定义动画&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用动画&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用keyframe定义动画：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@keyframes move-animation {
/* 0%和100%可使用from、to代替 */
0% {
    width: 100px;
}
50% {
    width: 175px;
}
100% {
    width: 200px;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;百分比必须是整数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;元素使用动画：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
width: 100px;
height: 100px;
animation-name: move-animation;
animation-duration: 5s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若元素同时要执行多个动画，可以使用逗号分隔：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
...
animation: one 1s ease infinite, two 2s ease-in-out forwards;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、CSS3动画属性&lt;/h4&gt;
&lt;p&gt;&amp;lt;table&amp;gt;
&amp;lt;tr style=&quot;text-align: center;&quot;&amp;gt;
&amp;lt;th&amp;gt;属性&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;描述&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr style=&quot;font-weight: 700&quot;&amp;gt;
&amp;lt;td&amp;gt;@keyframes（必需）&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定动画&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;animation&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;所有动画的连写属性，除了animation-play-state&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr style=&quot;font-weight: 700&quot;&amp;gt;
&amp;lt;td&amp;gt;animation-name（必需）&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定动画名称&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr style=&quot;font-weight: 700&quot;&amp;gt;
&amp;lt;td&amp;gt;animation-duration（必需）&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定动画一个周期用时&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;animation-timing-function&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定动画速度曲线，类似transform&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr style=&quot;font-weight: 700&quot;&amp;gt;
&amp;lt;td&amp;gt;animation-delay&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定动画何时开始&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr style=&quot;font-weight: 700&quot;&amp;gt;
&amp;lt;td&amp;gt;animation-iteration-count&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定播放次数(默认normal，infinite无限次)&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;animation-direction&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定是否在下一周期逆向播放(默认normal，alternate逆向播放)&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;animation-play-state&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定动画是否正在运行或暂停(running,pause)&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;animation-fill-mode&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;规定动画结束后状态(保持forwards,回到起始backwards)&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;p&gt;属性简写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
animation: 动画名称 持续时间 运动曲线 何时开始 播放次数 是否反方向 动画起始或结束时状态
*/

div {
animation: move 5s ease 1s infinite alternate;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、3D转换坐标结构&lt;/h4&gt;
&lt;p&gt;网页三维坐标结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/3D-transform.C2aa27g5_1blCjz.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;注意x、y、z三轴的正方向&lt;/p&gt;
&lt;h4&gt;8、3D移动 translate&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;div {
transform: translateZ(100px);
}
/* 或 */
div {
transform: translate3d(100px, 200px, 300px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特别注意：&lt;strong&gt;translateZ 不同于 translateX 和 translateY，它一般使用精确值设置&lt;/strong&gt;，同时注意：&lt;strong&gt;translateZ 只设置不添加透视是无法看出z轴平移效果的。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;9、透视 perspective&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/perspective.CFQo6ey2_1RljeY.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;透视：在 2D 平面差生近大远小视觉立体，效果只是二维的&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;透视我们也称为视距：视距就是人的眼睛到屏幕的距离&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;距离视觉点越近的在电脑平面成像越大，越远成像越小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;透视的单位是像素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;透视添加给需要透视的元素的父元素&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;10、3D 旋转 rotate3d&lt;/h4&gt;
&lt;p&gt;按各轴旋转方法：（&lt;strong&gt;注意要添加透视才有立体感&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transform: rotateX(45deg) /* 沿单轴旋转 */
transform: rotateY(45deg)
transform: rotateZ(45deg)

transform: rotate3d(x, y, z, deg) /* 叠加旋转，需要旋转的轴为1，不需要的为0 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;各轴旋转正方向可由左手定则确定：拇指轴正向，四指旋转向&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;11、3D 呈现 transform-style&lt;/h4&gt;
&lt;p&gt;控制子元素是否开启三维立体环境，&lt;strong&gt;若不开启，默认优先实现子元素 3D 变换，然后直接叠加在父元素变换上，而不是呈现真实的三维效果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也可以理解为：不开启所有子元素在 2D 空间呈现，开启表示所有子元素在 3D 空间呈现&lt;/p&gt;
&lt;p&gt;属性默认为 flat&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 注意，该属性加给父级 */
div {
transform-style: preserve-3d;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;12、浏览器私有前缀&lt;/h4&gt;
&lt;p&gt;浏览器私有前缀是为了兼容老版本的写法，比较新版本的浏览器无需添加&lt;/p&gt;
&lt;p&gt;-moz-  ：代表 firefox 浏览器私有属性&lt;/p&gt;
&lt;p&gt;-ms-  ：代表 ie 浏览器私有属性&lt;/p&gt;
&lt;p&gt;-webkit-  ：代表 Safari、Chrome 私有属性&lt;/p&gt;
&lt;p&gt;-o-  ：代表 Opera 私有属性&lt;/p&gt;
&lt;p&gt;提倡的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
-o-border-radius: 10px;
border-radius: 10px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h1&gt;移动端 H5C3&lt;/h1&gt;
&lt;h2&gt;一、移动端基础&lt;/h2&gt;
&lt;h4&gt;1、移动端现状&lt;/h4&gt;
&lt;p&gt;大多基于 webkit 内核，&lt;strong&gt;兼容移动端浏览器，处理 webkit 内核浏览器即可&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2、移动端设备屏幕现状&lt;/h4&gt;
&lt;p&gt;移动端设备屏幕尺寸非常多，碎片化严重&lt;/p&gt;
&lt;h4&gt;3、移动端调试&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;谷歌浏览器模拟手机调试&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;搭建 web 服务器，通过手机访问调试&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;二、视口&lt;/h2&gt;
&lt;p&gt;视口（viewport）就是浏览器显示页面内容的屏幕区域，视口可以分为布局视口、视觉视口和理想视口&lt;/p&gt;
&lt;h4&gt;1、布局视口（layout viewport）&lt;/h4&gt;
&lt;p&gt;一般移动设备的浏览器都默认设置了一个布局视口，早期用于解决 PC 端网页在移动设备上的显示问题&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般布局视口分辨率为 980 px&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/layout-viewport.Co2qv6fY_26tnCO.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2、视觉视口（visual viewport）&lt;/h4&gt;
&lt;p&gt;字面意思：用户&lt;strong&gt;能看到的网站的区域&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/visual-viewport._UvpCIxz_eI4Ms.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3、理想视口（ideal viewport）&lt;/h4&gt;
&lt;p&gt;为了使网站在移动端有最理想的浏览和阅读宽度而设定&lt;/p&gt;
&lt;p&gt;需要通过 meta 视口标签设定&lt;/p&gt;
&lt;h4&gt;4、meta 视口标签&lt;/h4&gt;
&lt;p&gt;标准视口标签的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&quot;viewport&quot; 
  content=&quot;width=device-width, user-scalable=no,initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0&quot; &amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、物理像素比&lt;/h2&gt;
&lt;p&gt;物理像素点：指屏幕显示的最小颗粒，物理上是真实存在的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在移动端开发时 1px 不一定等于 物理上的 1像素&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个 px 能显示的物理像素数，称为物理像素比或屏幕像素比&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;四、二倍图&lt;/h2&gt;
&lt;p&gt;对于 50px * 50px 的图片，在 Retina 屏中打开，按照物理像素比会放大倍数，将造成图片模糊，&lt;strong&gt;因此在物理像素比为2的设备中可使用二倍像素的图，然后调整缩放为 50%，其他比例设备类似&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;多倍图切图可使用之前提到的 ps 插件&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;1、图片二倍图&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/* 以 2 倍图为例 */
img {
/* 原始图片 100 px */
width: 50px;
height: 50px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、背景二倍图&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;div {
/* 原始图片 100 px */
width: 50px;
height: 50px;
 background: url(./img/1.jpg) no-repeat;
background-size: 50px 50px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、二倍精灵图&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;测量等比缩放为 1/2 的精灵图的坐标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码添加 &lt;code&gt;background-size&lt;/code&gt; ，长宽分别为原始精灵图的一半&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;img {
width: 20px;
height: 20px;
background: url(./img/background-sprite.png) no-repeat -10px 0;
background-size: 200px 200px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五、移动端开发选择&lt;/h2&gt;
&lt;h4&gt;1、单独制作移动端页面（主流）&lt;/h4&gt;
&lt;p&gt;通常情况下，网址域名前加 m(mobile) 可以打开移动端，判断如果为移动端，则打开移动端页面&lt;/p&gt;
&lt;h4&gt;2、响应式页面兼容移动端&lt;/h4&gt;
&lt;p&gt;缺点：制作麻烦，需要调整很多兼容性问题&lt;/p&gt;
&lt;h2&gt;六、移动端技术解决方案&lt;/h2&gt;
&lt;h4&gt;1、浏览器兼容性&lt;/h4&gt;
&lt;p&gt;内核以 webkit 为主，私有前缀考虑 webkit 即可&lt;/p&gt;
&lt;h4&gt;2、CSS 初始化&lt;/h4&gt;
&lt;p&gt;推荐使用 normalize.css&lt;/p&gt;
&lt;h4&gt;3、盒子模型&lt;/h4&gt;
&lt;p&gt;移动端可全部使用 CSS3 盒子模型&lt;/p&gt;
&lt;h4&gt;4、特殊样式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;div,
span {
box-sizing: border-box;
-webkit-box-sizing: border-box;
}

a {
/* 去除点击高亮 */
-webkit-tap-highlight-color: transparent;
}

img,
a {
/* 禁用长按页面时的弹出菜单 */
-webkit-touch-callout: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、移动端技术选型&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;单独制作移动端页面（主流）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;a、流式布局（百分比布局）&lt;/p&gt;
&lt;p&gt;b、flex 弹性布局&lt;/p&gt;
&lt;p&gt;c、less + rem + 媒体查询布局&lt;/p&gt;
&lt;p&gt;d、混合布局&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;响应式页面兼容移动端&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;a、媒体查询&lt;/p&gt;
&lt;p&gt;b、bootstrap&lt;/p&gt;
&lt;h2&gt;七、流式布局&lt;/h2&gt;
&lt;p&gt;也称百分比布局，非固定像素布局，是比较常见的布局方式&lt;/p&gt;
&lt;p&gt;通过盒子的宽度设置为百分比，根据屏幕的宽度来进行伸缩，不受固定像素的限制，内容向两侧填充&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;为保护盒子内内容会设置 max-width 和 min-width&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
  float: left;
width: 50%;
height: 50%;
max-width: 500px;
min-width: 200px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心思想就是通过百分比划分各元素的空间，其他布局细节类似 PC 端布局&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;八、flex 布局&lt;/h2&gt;
&lt;h4&gt;1、布局原理&lt;/h4&gt;
&lt;p&gt;flex 布局意为“弹性布局”，用来为盒状模型提供最大的灵活性，&lt;strong&gt;任何一个容器都可以指定为 flex 布局&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当我们为父盒子设为 flex 布局后，子元素的 float、clear 和 vertical-align 属性将会失效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;伸缩布局 = 弹性布局 = 伸缩盒布局 = 弹性盒布局 = flex 布局&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 flex 布局，更改 display 属性为 flex&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;采用 flex 布局的元素，称为 flex 容器，简称容器，它的子元素自动称为容器成员，称为 flex 项目，简称“项目”，子容器可以横向排列也可以纵向排列&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/flex-model.B3qqySpT_Z2uMJE.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;flex 布局原理总结：&lt;strong&gt;通过给父盒子添加 flex 属性，从而控制子盒子的位置和排列方式&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2、flex-direction 设置主轴的方向&lt;/h4&gt;
&lt;p&gt;flex-direction 属性决定主轴的方向（即项目的排列方向）&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;主轴和侧轴是会变化的，就看 flex-direction 设置谁为主轴，另一轴就为侧轴，而我们的子元素按主轴方向来排列的&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;row&lt;/td&gt;
&lt;td&gt;从左到右（默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;row-reverse&lt;/td&gt;
&lt;td&gt;从右到左&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;column&lt;/td&gt;
&lt;td&gt;从上到下&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;column-reverse&lt;/td&gt;
&lt;td&gt;从下到上&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;3、justify-content 设置主轴上的排列方式&lt;/h4&gt;
&lt;p&gt;justify-content 属性定义了项目在主轴上的对齐方式&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;flex-start&lt;/td&gt;
&lt;td&gt;从头部开始排列（默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flex-end&lt;/td&gt;
&lt;td&gt;从尾部开始排列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;center&lt;/td&gt;
&lt;td&gt;在主轴居中对齐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;space-around&lt;/td&gt;
&lt;td&gt;平分剩余空间给各个项目&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;space-between&lt;/td&gt;
&lt;td&gt;先两侧贴边，再平分空间&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;4、flex-wrap 设置子元素是否换行&lt;/h4&gt;
&lt;p&gt;默认情况下，项目都排在一条线（轴线）上，&lt;strong&gt;为实现一行，甚至会修改项目的宽度&lt;/strong&gt;，因为flex-wrap 属性默认定义是不换行的&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nowrap&lt;/td&gt;
&lt;td&gt;不换行（默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wrap&lt;/td&gt;
&lt;td&gt;换行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;5、align-items 设置侧轴上的子元素排列（单行单列）&lt;/h4&gt;
&lt;p&gt;该属性是控制子项在侧轴（默认是 y 轴）上的排列方式，&lt;strong&gt;在子项为单行或单列的时候使用&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;flex-start&lt;/td&gt;
&lt;td&gt;从头到尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flex-end&lt;/td&gt;
&lt;td&gt;从尾到头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;center&lt;/td&gt;
&lt;td&gt;挤在一起居中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stretch&lt;/td&gt;
&lt;td&gt;拉伸（默认），注意不要设置高度（宽度）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;a href=&quot;https://www.runoob.com/try/playit.php?f=playcss_align-items&amp;amp;preval=center&quot;&gt;演示示例&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;6、align-content 设置侧轴上的子元素的排列（多行多列）&lt;/h4&gt;
&lt;p&gt;设置子项在侧轴上的排列方式，并且只能用于子项出现换行的情况（多行多列），&lt;strong&gt;在单行是没有效果的&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;flex-start&lt;/td&gt;
&lt;td&gt;侧轴头部开始排列（默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flex-end&lt;/td&gt;
&lt;td&gt;侧轴尾部开始排列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;center&lt;/td&gt;
&lt;td&gt;侧轴中部居中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;space-arouond&lt;/td&gt;
&lt;td&gt;子项平分剩余空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;space-between&lt;/td&gt;
&lt;td&gt;先贴边，再平分剩余空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stretch&lt;/td&gt;
&lt;td&gt;设置多行子项拉伸以平分父元素轴长&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;a href=&quot;https://www.runoob.com/try/playit.php?f=playcss_align-content&amp;amp;preval=stretch&quot;&gt;演示示例&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;7、flex-flow 属性&lt;/h4&gt;
&lt;p&gt;flex-flow 是 flex-direction 和 flex-wrap 属性的复合属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
display: flex;
flex-direction: column;
flex-wrap: wrap;
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可写为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
display: flex;
flex-flow: column wrap;
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8、flex 子项属性：flex&lt;/h4&gt;
&lt;p&gt;flex 属性定义子项目&lt;strong&gt;分配剩余空间时占的份数&lt;/strong&gt;，可使用&lt;strong&gt;数字或百分比&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实例1：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/flex-items-flex-property1.CsCXqlI8_NelMd.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说明：两侧盒子指定了宽度，中间的盒子不指定宽度，设置其 flex 属性为1，表明分配剩余空间为 1 份给它。&lt;/p&gt;
&lt;p&gt;实例2：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/flex-items-flex-property2.CpVkP2sm_Z1B969n.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说明：三个盒子都不指定宽度，粉色盒子设置 flex 属性为1，而紫色盒子设置 flex 属性为 2，从而实现了左右占空间 25%，中间占空间 50% 的效果。&lt;/p&gt;
&lt;h4&gt;9、flex 子项属性：align-self&lt;/h4&gt;
&lt;p&gt;align-self 属性允许单个项目有与其他项目不一样的对齐方式，可覆盖 align-items 属性，默认值为 auto，表示继承父元素的 align-self 属性，如果没有父元素，等同于 stretch&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;span:nth-child(2) {
align-self: flex-end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;10、flex 子项属性：order&lt;/h4&gt;
&lt;p&gt;order 属性定义了项目的排列顺序&lt;/p&gt;
&lt;p&gt;数值越小，排列越靠前，默认为 0&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;和 z-index 不一样&lt;/p&gt;
&lt;h2&gt;九、rem 初探&lt;/h2&gt;
&lt;h4&gt;1、rem 布局的优势&lt;/h4&gt;
&lt;p&gt;以下布局问题可采用 rem 适配方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;页面布局文字随屏幕大小而变化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高度布局&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;屏幕变化时元素高度和宽度等比例缩放&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2、rem 单位&lt;/h4&gt;
&lt;p&gt;rem （root em）是一个相对单位，类似于 em，em 是父元素字体大小。不同的是 rem 的基准是相对于 html 元素的字体大小。&lt;/p&gt;
&lt;p&gt;优势（重要特性）：&lt;strong&gt;可通过修改 html 元素的文字大小来对页面元素进行整体缩放&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;4、媒体查询&lt;/h4&gt;
&lt;p&gt;媒体查询（media query）是 CSS3 新语法&lt;/p&gt;
&lt;p&gt;语法规范：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media mediatype and|not|only (media feature) {
CSS-Code;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;mediatype 查询类型&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;用于所有设备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;print&lt;/td&gt;
&lt;td&gt;用于打印机和打印预览&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;screen&lt;/td&gt;
&lt;td&gt;用于电脑屏幕，平板电脑，智能手机等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;关键字：将媒体类型或多个媒体特性连接到一起作为媒体查询的条件，三个关键字：and，or，only&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;媒体特性：每种媒体类型都具有各自不同的特性，根据不同媒体类型的媒体特性可设置不同的展示风格。下方暂且展示三个。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;width&lt;/td&gt;
&lt;td&gt;定义输出设备中页面可见区域的宽度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min-width&lt;/td&gt;
&lt;td&gt;定义输出设备中页面最小可见区域宽度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max-width&lt;/td&gt;
&lt;td&gt;定义输出设备中页面最大可见区域宽度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media screen and (max-width: 800px) {
body {
    background-color: skyblue; /* 小于 800px 的设备背景显示天蓝色 */
}
}
@media screen and (max-width: 500px) {
body {
    background-color: pink; /* 小于 500px 的设备背景显示粉色 */
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、rem 实现元素动态大小变化&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@media screen and (min-width: 320px) {
html {
    font-size: 50px;
}
}

@media screen and (min-width: 640px) {
html {
    font-size: 100px;
}
}

.header {
height: 1rem;
font-size: .5rem;
background-color: green;
color: #fff;
text-align: center;
line-height: 1rem;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、媒体查询引入资源&lt;/h4&gt;
&lt;p&gt;引入资源就是针对不同的屏幕尺寸，调用不同的 css 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style320.css&quot; media=&quot;screen and (min-width: 320px)&quot; /&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style640.css&quot; media=&quot;screen and (min-width: 640px)&quot; /&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、CSS 弊端&lt;/h4&gt;
&lt;p&gt;CSS 的弊端：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;代码冗余度高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不方便维护、扩展和复用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有很好的计算能力&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;十、less 预处理&lt;/h2&gt;
&lt;h4&gt;1、less 基础&lt;/h4&gt;
&lt;p&gt;less 是 CSS 的预处理语言，它扩展了 CSS 的动态特性。&lt;/p&gt;
&lt;p&gt;作为 CSS 的一种形式的扩展，它并没有减少 CSS 的功能，而是在其基础上，加入了程序语言的特性。&lt;/p&gt;
&lt;h4&gt;2、less 变量&lt;/h4&gt;
&lt;p&gt;变量是可以变化的量。CSS 中一些颜色和数值需要更改可以使用变量。&lt;/p&gt;
&lt;p&gt;（变量定义规则和其他编程语言类似）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@my_blue: #6cf;

body {
background-color: @my_blue;
}
div {
color: @my_blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、less 编译&lt;/h4&gt;
&lt;p&gt;我们需要把 less 文件，编译生成 css 文件，这样 HTML 页面才能使用&lt;/p&gt;
&lt;p&gt;&lt;em&gt;在 vscode 中可使用 easy LESS 插件实现&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;4、less 嵌套&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;子父元素嵌套&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;不加 &amp;amp; 就解释为父元素后代，加 &amp;amp; 解释为伪类或伪元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 后代选择器
.header {
background: #6cf;
.header-son {
    font-size: 18px;
    color: white;
}
}

// 伪类伪元素选择器
a {
&amp;amp;:hover {
    // hover 代码
}
&amp;amp;::after {
    // after 代码
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、less 运算&lt;/h4&gt;
&lt;p&gt;任何数字、颜色或变量都可以参与运算。&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运算符中间有空格隔开&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于两个不同单位的值之间的运算，取第一个值的单位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若两值只有一值有单位，则运算结果取该单位&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;十一、rem 适配布局&lt;/h2&gt;
&lt;h4&gt;1、技术方案选型&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;技术方案 1 （less、媒体查询、rem）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;技术方案 2 （flexible.js、rem）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h1&gt;其他&lt;/h1&gt;
&lt;h2&gt;一、web服务器&lt;/h2&gt;
&lt;h4&gt;1、什么是web服务器&lt;/h4&gt;
&lt;p&gt;web服务器一般指网站服务器，&lt;strong&gt;根据在网络中所在的位置的不同，又可分为本地服务器和远程服务器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;远程服务器是通常是别的公司为我们提供的一台电脑（主机），我们只要把网站项目上传到这台电脑上，任何人都可以利用域名访问我们的网站了&lt;/p&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h1&gt;开发细节及注意事项&lt;/h1&gt;
&lt;h2&gt;一、网页整体布局思路&lt;/h2&gt;
&lt;p&gt;&amp;lt;h4&amp;gt;1、首先必须确定页面的版心（可视区）&amp;lt;/h4&amp;gt;
&amp;lt;h4&amp;gt;2、分析页面中的行模块，以及各个行模块中的列模块。对应页面布局第一准则&amp;lt;/h4&amp;gt;
&amp;lt;h4&amp;gt;3、一行中的列模块经常浮动布局，先确定每个列的大小，之后确定列的位置。对应页面布局第二准则&amp;lt;/h4&amp;gt;
&amp;lt;h4&amp;gt;4、制作HTML结构，遵循先有结构，后有样式的原则&amp;lt;/h4&amp;gt;
&amp;lt;h4&amp;gt;5、要理清楚布局结构，再写代码。（经验需要积累）&amp;lt;/h4&amp;gt;&lt;/p&gt;
&lt;h2&gt;二、HTML结构&lt;/h2&gt;
&lt;h4&gt;1、导航栏制作细节&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;不直接使用a来堆砌，而是使用 li + a 的方式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;原因：a. li + a 语义更清晰，一看就知道是有条理的列表型内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    b. **直接使用a，搜索引擎容易鉴别为有堆砌关键字的嫌疑**
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、favicon图标&lt;/h4&gt;
&lt;p&gt;favicon.ico 一般作为缩略的网站标志，它显示在浏览器的地址栏或者标签上&lt;/p&gt;
&lt;p&gt;目前主要的浏览器都支持 favicon.ico 图标&lt;/p&gt;
&lt;p&gt;三个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;制作 favicon 图标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;favicon 图标放在网站根目录下&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTML 页面引入favicon 图标&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;引入方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
...
&amp;lt;link rel=&quot;shortcut icon&quot; href=&quot;favicon.ico&quot; /&amp;gt;
...
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、网站TDK三大标签的SEO优化&lt;/h4&gt;
&lt;p&gt;三个标签为：title、description、keyword&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;title具有不可替代性，是我们内页第一个重要标签，是搜索引擎了解网页的入口和对网页主    题归属判断的最佳断点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;建议：&lt;strong&gt;网站名（产品名）- 网站的介绍&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;title&amp;gt;小米商城 - 小米5s、红米Note4、小米MIX、小米笔记本官方网站&amp;lt;/title&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;description 简要说明我们网站主要是做什么的。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们提倡，description作为网站的总体业务和主题概括，多采用“我们是…”、“我们提供..&quot;、“xx×网    作为…”、“电话：010..”之类语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&quot;description&quot; content=&quot;京东JD.COM-专业的综合网上购物商城，销售家电、数码通讯、电脑、家居百货、服装服饰、母婴、图书、食品等数万个品牌优质商品.便捷、诚信的服务，为您提供愉悦的网上购物体验！&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;keywords 是页面关键字，是搜索引擎的关注点之一&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;keywords 最好限制为6 ~ 8 个关键词，关键词之间用英文逗号隔开，采用关键词1，关键词2的形    式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&quot;keywords&quot; content=&quot;网上购物，网上商城，手机，笔记本，电脑，MP3，CD，VCD，DV，相机，数码配件，手表，存储卡，京东&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、LOGO的SEO优化&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/logo-to_SEO-improvement.4FOTvA_i_1LVqWa.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;5、tab栏选项卡结构&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://miku.top/_astro/tab-structure.f8-Ne3r2_Z2le8NT.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;结构上分为 tab-list 和 tab-content&lt;/p&gt;
&lt;p&gt;每个tab-list-item 对应一个 tab-list-content-item&lt;/p&gt;
&lt;h4&gt;6、注册页不使用SEO优化&lt;/h4&gt;
&lt;p&gt;注：注册页面比较隐私，&lt;strong&gt;为保护用户信息，不需要对该页面做SEO优化&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;三、CSS设计&lt;/h2&gt;
&lt;h4&gt;1、关于&amp;lt;a&amp;gt;标签的模式问题&lt;/h4&gt;
&lt;p&gt;在开发中&amp;lt;a&amp;gt;常常会包含其他元素，为了不影响效果的呈现，&lt;/p&gt;
&lt;p&gt;在开发中为了兼容性，&lt;strong&gt;通常会将&amp;lt;a&amp;gt;转化为块级元素&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2、因添加边框造成的盒子抖动问题&lt;/h4&gt;
&lt;p&gt;元素若原本没有边框，再触发条件后添加边框会改变盒子布局，从而导致盒子抖动&lt;/p&gt;
&lt;p&gt;这时可在事件触发前&lt;strong&gt;设置为透明边框&lt;/strong&gt;，因为边框本来就存在，因此只是改变颜色不会影响布局&lt;/p&gt;
</content:encoded></item></channel></rss>