<?xml version="1.0" encoding="utf-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Zander Hsueh</title><link>https://xuezenghui.com/</link><description>Zander Hsueh 的个人博客。</description><generator>Hugo 0.72.0 https://gohugo.io/</generator><language>zh-CN</language><managingEditor>xuezenghui6@gmail.com (Zander Hsueh)</managingEditor><webMaster>xuezenghui6@gmail.com (Zander Hsueh)</webMaster><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><lastBuildDate>Sun, 07 Apr 2024 08:39:19 +0000</lastBuildDate><atom:link rel="self" type="application/rss+xml" href="https://xuezenghui.com/rss.xml"/><item><title>K8S 中使用非 root 用户运行前端项目</title><link>https://xuezenghui.com/posts/run-frontend-container-as-non-root-in-k8s/</link><guid isPermaLink="true">https://xuezenghui.com/posts/run-frontend-container-as-non-root-in-k8s/</guid><pubDate>Sun, 07 Apr 2024 13:47:50 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>Kubernetes 中的镜像一般根据项目类型进行构建，前端项目的最佳实践是将 Dockerfile、项目的 Nginx 配置文件与代码文件统一管理在 Git 仓库中，Dockerfile 中使用的镜像若为 Nginx 的基本镜像 &lt;a href="https://hub.docker.com/_/nginx">&lt;code>nginx&lt;/code>&lt;/a> 则不符合安全性的要求，因为该镜像监听的端口是 80，而 0-1024 为特权端口，&lt;strong>只能使用 root 用户来运行&lt;/strong>。换句话说，你跑在 k8s 中的前端项目的 Pod 也将以 root 用户运行，Pen-test 给出的不安全评级为 Medium：&lt;/p>
&lt;p>A number of containers were running as the ‘root’ user (this is the default setting) which could make it easier for an attacker with access to the containers to break out to the underlying host.
The following was identified:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">id&lt;/span>
&lt;span class="n">uid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">gid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">groups&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>解决方案分为两步：&lt;/p>
&lt;ol>
&lt;li>使用 Nginx 的非特权镜像 &lt;a href="https://hub.docker.com/r/nginxinc/nginx-unprivileged">nginx-unprivileged&lt;/a> 作为前端项目的基础镜像&lt;/li>
&lt;li>部署到 K8s 时设置 Pod 的&lt;a href="https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/security-context/">安全上下文&lt;/a> &lt;code>runAsNonRoot&lt;/code> 配置&lt;/li>
&lt;/ol>
&lt;h2 id="更换-nginx-基础镜像">更换 Nginx 基础镜像&lt;/h2>
&lt;p>从 nginx-unprivileged 镜像的文档和&lt;a href="https://github.com/nginxinc/docker-nginx-unprivileged/blob/main/stable/debian/Dockerfile">源代码&lt;/a>中可以得到以下几个重要信息：&lt;/p>
&lt;ul>
&lt;li>监听的端口由 80 更换为 8080&lt;/li>
&lt;li>运行容器的新用户为 &lt;code>nginx&lt;/code>&lt;/li>
&lt;li>&lt;code>nginx&lt;/code> 用户的 &lt;strong>UID 为 101&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>更新 Dockerfile 文件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-docker" data-lang="docker">&lt;span class="k">FROM&lt;/span>&lt;span class="s"> nginxinc/nginx-unprivileged:latest&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="c"># 替换项目中的 Nginx 配置文件&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="s"> root&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">ADD&lt;/span> ./dist /usr/share/nginx/html&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> rm -rf /etc/nginx/conf.d/default.conf&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> ./nginx.conf /etc/nginx/conf.d/default.conf&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> chmod -R &lt;span class="m">777&lt;/span> /etc/nginx/&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> id nginx&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="s"> nginx&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;nginx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-g&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;daemon off;&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>由于更换了新的端口，修改旧的 Nginx 配置文件监听端口为 8080：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="k">server&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kn">listen&lt;/span> &lt;span class="mi">8080&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kn">root&lt;/span> &lt;span class="s">/usr/share/nginx/html&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kn">location&lt;/span> &lt;span class="s">/&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kn">try_files&lt;/span> &lt;span class="nv">$uri&lt;/span> &lt;span class="nv">$uri/&lt;/span> &lt;span class="s">/index.html&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kn">location&lt;/span> &lt;span class="p">~&lt;/span>&lt;span class="sr">*&lt;/span> &lt;span class="s">^.+\.(ico|gif|jpg|jpeg|png|js|css|svg)&lt;/span>$ &lt;span class="p">{&lt;/span>
&lt;span class="kn">access_log&lt;/span> &lt;span class="no">off&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kn">add_header&lt;/span> &lt;span class="s">Cache-Control&lt;/span> &lt;span class="s">&amp;#34;immutable&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kn">expires&lt;/span> &lt;span class="s">1y&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kn">location&lt;/span> &lt;span class="p">~&lt;/span>&lt;span class="sr">*&lt;/span> &lt;span class="s">^.+\.(otf)&lt;/span>$ &lt;span class="p">{&lt;/span>
&lt;span class="kn">add_header&lt;/span> &lt;span class="s">Cache-Control&lt;/span> &lt;span class="s">&amp;#34;immutable&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kn">access_log&lt;/span> &lt;span class="no">off&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kn">expires&lt;/span> &lt;span class="s">1y&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="修改-k8s-配置">修改 K8s 配置&lt;/h2>
&lt;p>安全上下文（Security Context）有两个层级：Pod 层级和 Container 层级。Pod 层级的安全上下文会应用到 Pod 中所有的 Container，Container 层级的安全上下文只影响设置的 Container，且如果 Pod 层级上同时也设置了，则会以 Container 层级的设置为准，重写 Pod 层级的设置。&lt;/p>
&lt;p>所以要根据项目的情况来选择，但常见情况下前端项目只有一个 Container，设置在两个层级没有差别，此处以设置在 Pod 层级为例，&lt;strong>设置 &lt;code>runAsNonRoot&lt;/code> 为 &lt;code>true&lt;/code>，&lt;code>runAsUser&lt;/code> 为上面得到的 &lt;code>nginx&lt;/code> 用户 UID 101&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="k">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>apps/v1&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>Deployment&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>frontend-demo&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">securityContext&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">runAsNonRoot&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">runAsUser&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">101&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="k">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>frontend-demo&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>frontend-demo-image&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>同时将端口相关的配置由 80 改为 8080，如 &lt;strong>containerPort 、探针检测的端口&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="w"> &lt;/span>...&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="k">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>frontend-demo&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="k">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>8080tcp&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>TCP&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">livenessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">failureThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">httpGet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>/index.html&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">scheme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>HTTP&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">90&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">successThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">timeoutSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">startupProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">failureThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">httpGet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>/index.html&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">scheme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>HTTP&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">60&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">successThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">timeoutSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>以及作为服务发现的 Service 的端口：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="k">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>v1&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>Service&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>frontend-demo-service&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="k">nodePort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">32329&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8946&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>TCP&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="验证">验证&lt;/h2>
&lt;p>在 Container 中执行 &lt;code>id&lt;/code> 命令：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">id&lt;/span>
&lt;span class="n">uid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">101&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">nginx&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">gid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">101&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">nginx&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">groups&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">101&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">nginx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>得解。&lt;/p></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/kubernetes/">Kubernetes</category><category domain="https://xuezenghui.com/tags/security/">Security</category></item><item><title>❤️Ten Years</title><link>https://xuezenghui.com/posts/ten-years/</link><guid isPermaLink="true">https://xuezenghui.com/posts/ten-years/</guid><pubDate>Mon, 20 Nov 2023 01:12:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>暮雪夜，友朋间，华年里伴歌酌庆酒。&lt;/p>
&lt;p>花语酿，膝下金，热血时洒泪惘白首。&lt;/p>
&lt;p>十年青葱，&lt;/p>
&lt;p>学堂郎朗，有全然不顾留满园懵懂与念想，&lt;/p>
&lt;p>骊山相隔，怀自由激烈奔赴两地相见芬芳。&lt;/p>
&lt;p>长江千里，虽难忍小别但见未来心意两厢。&lt;/p>
&lt;p>今，&lt;/p>
&lt;p>少年沉性薄发可独当一面，&lt;/p>
&lt;p>萧娘爱人有知万事皆轻盈。&lt;/p>
&lt;p>问十载一瞬，何顾何愿？&lt;/p>
&lt;p>顾，瓶颈之期，予她予己难达所望。&lt;/p>
&lt;p>愿，廿年之日，不悔不惑静心还向。&lt;/p></description><category domain="https://xuezenghui.com/categories/life/">Life</category><category domain="https://xuezenghui.com/tags/poetry/">poetry</category></item><item><title>Linux 自动压缩 log 脚本</title><link>https://xuezenghui.com/posts/bash-shell-for-log-compress-on-linux/</link><guid isPermaLink="true">https://xuezenghui.com/posts/bash-shell-for-log-compress-on-linux/</guid><pubDate>Fri, 03 Jun 2022 18:31:22 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>线上应用一般会产生大量日志文件，如 Java 应用不同级别的 log、Nginx 的 access.log 和 error.log，日积月累会极其占用磁盘空间，因此需要定期对 log 进行压缩、备份和删除，本文先分享我们的应用日志和 Nginx 日志的压缩策略和方式。&lt;/p>
&lt;h2 id="编写-bash-shell-压缩脚本">编写 Bash Shell 压缩脚本&lt;/h2>
&lt;p>先了解两个概念：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Bash Shell&lt;/strong>：Unix、Linux 系统的一种脚本文件，是大多数 Linux 发行版中的默认 Shell，以 &lt;code>.sh&lt;/code> 结尾，使用 &lt;code>$ sh name.sh&lt;/code> 命令执行。&lt;/li>
&lt;li>&lt;strong>&lt;code>.tar.gz&lt;/code> 格式文件&lt;/strong>：&lt;code>.tar&lt;/code> 为多个文件归档为一个文件的格式，会保留文件的元数据信息，&lt;code>.gz&lt;/code> 为将单个文件使用 &lt;a href="https://zh.m.wikipedia.org/zh-hans/Gzip">Gzip&lt;/a> 压缩后的格式。通过 Tar + Gzip 压缩比 Zip 压缩效率高且节省空间更多，环境上也更为兼容。&lt;/li>
&lt;/ol>
&lt;h3 id="java-app-log-bash-shell">Java APP log Bash Shell&lt;/h3>
&lt;p>为了更好的排查 log，我们将 Micro Service 架构下 Java 应用的 log 组织为了如下示例结构（应用中使用 &lt;a href="https://logback.qos.ch/">Logback&lt;/a> 工具）：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="n">log&lt;/span>
├── &lt;span class="n">micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>
│   ├── &lt;span class="m">2022-06-01&lt;/span>
│   │   ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│   │   └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│   ├── &lt;span class="m">2022-06-02&lt;/span>
│   │   ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│   │   └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│   └── &lt;span class="m">2022-06-03&lt;/span>
│   ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│   └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
└── &lt;span class="n">micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service2&lt;/span>
├── &lt;span class="m">2022-06-01&lt;/span>
│   ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│   └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
├── &lt;span class="m">2022-06-02&lt;/span>
│   ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│   └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
└── &lt;span class="m">2022-06-03&lt;/span>
├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
└── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Bash Shell 也以此结构为基准进行编写：&lt;/p>
&lt;p>&lt;strong>1. 新建 Bash Shell 文件 &lt;code>java-log-compress.sh&lt;/code>，指定解释器路径，定义初始变量&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;span class="cp">&lt;/span>
&lt;span class="c1"># log 文件目录绝对路径&lt;/span>
&lt;span class="nv">base_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/log&amp;#34;&lt;/span>
&lt;span class="c1"># 当前日期&lt;/span>
&lt;span class="nv">date&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date &lt;span class="s2">&amp;#34;+%Y-%m-%d&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 循环 Micro Service 文件列表&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="c1"># 进入 log 目录&lt;/span>
&lt;span class="nb">cd&lt;/span> &lt;span class="nv">$base_dir&lt;/span>
&lt;span class="c1"># 将以 micro-service 开头的所有目录名存入变量&lt;/span>
&lt;span class="nv">services&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>ls -f micro-service*/&lt;span class="k">)&lt;/span>
&lt;span class="c1"># 循环目录名 list&lt;/span>
&lt;span class="k">for&lt;/span> service in &lt;span class="nv">$services&lt;/span>
&lt;span class="k">do&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;service: &lt;/span>&lt;span class="nv">$service&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="k">done&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>使用变量时 &lt;code>{}&lt;/code> 符号可选，与字符串拼接时必须加，否则会识别错误。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>3. 循环 log 日期列表&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="k">for&lt;/span> service in &lt;span class="nv">$services&lt;/span>
&lt;span class="k">do&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;service: &lt;/span>&lt;span class="nv">$service&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="c1"># 进入 log 目录&lt;/span>
++ &lt;span class="nb">cd&lt;/span> &lt;span class="nv">$base_dir&lt;/span>
&lt;span class="c1"># 判断是否存在 micro service 目录&lt;/span>
++ &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -d &lt;span class="nv">$service&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;span class="c1"># 进入 micro service 目录&lt;/span>
++ &lt;span class="nb">cd&lt;/span> &lt;span class="nv">$service&lt;/span>
&lt;span class="c1"># 将当前路径存入变量&lt;/span>
++ &lt;span class="nv">service_path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>
++ &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;start compress&amp;#34;&lt;/span>
&lt;span class="c1"># 将所有日期目录名存入变量&lt;/span>
++ &lt;span class="nv">date_dirs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>ls -d *&lt;span class="k">)&lt;/span>
&lt;span class="c1"># 循环日期目录 list&lt;/span>
++ &lt;span class="k">for&lt;/span> dir in &lt;span class="nv">$date_dirs&lt;/span>
++ &lt;span class="k">do&lt;/span>
++ &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;date_dir: &lt;/span>&lt;span class="nv">$dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
++ &lt;span class="k">done&lt;/span>
++ &lt;span class="k">fi&lt;/span>
&lt;span class="k">done&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>在 service 目录里重新 &lt;code>cd $base_dir&lt;/code> 而不直接 &lt;code>cd $service&lt;/code> 的目的是防止下次循环在该 service 目录直接去 &lt;code>cd&lt;/code> 下一个 service 目录导致报错。&lt;/p>
&lt;/blockquote>
&lt;p>此时执行 &lt;code>sh java-log-compress.sh&lt;/code> 的打印：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="n">service&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="n">micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">/&lt;/span>
&lt;span class="n">start&lt;/span> &lt;span class="n">compress&lt;/span>
&lt;span class="n">date_dir&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="m">2022-06-01&lt;/span>
&lt;span class="n">date_dir&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="m">2022-06-02&lt;/span>
&lt;span class="n">date_dir&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="m">2022-06-03&lt;/span>
&lt;span class="n">service&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="n">micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service2&lt;/span>&lt;span class="o">/&lt;/span>
&lt;span class="n">start&lt;/span> &lt;span class="n">compress&lt;/span>
&lt;span class="n">date_dir&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="m">2022-06-01&lt;/span>
&lt;span class="n">date_dir&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="m">2022-06-02&lt;/span>
&lt;span class="n">date_dir&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="m">2022-06-03&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 执行压缩&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-sh" data-lang="sh"> &lt;span class="k">for&lt;/span> date_dir in &lt;span class="nv">$date_dirs&lt;/span>
&lt;span class="k">do&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;date_dir: &lt;/span>&lt;span class="nv">$date_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="c1"># 判断该日期是否为今天&lt;/span>
++ &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> &lt;span class="nv">$date_dir&lt;/span> !&lt;span class="o">=&lt;/span> &lt;span class="nv">$date&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
++ &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;run tar -zcvf &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">.tar.gz &lt;/span>&lt;span class="nv">$date_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="c1"># 将日期目录压缩为 .tar.gz 文佳&lt;/span>
++ tar -zcvf &lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>.tar.gz &lt;span class="nv">$date_dir&lt;/span>
&lt;span class="k">fi&lt;/span>
&lt;span class="k">done&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此时执行 &lt;code>sh java-log-compress.sh&lt;/code> 后的目录结构如下，..日期不为今天..的 log 目录（存量数据）都被压缩完成，可使用 &lt;code>tar -zxvf *.tar.gz&lt;/code> 解压查看 log 内容是否正常。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="n">log&lt;/span>
├── &lt;span class="n">micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>
│ ├── &lt;span class="m">2022-06-01&lt;/span>
│ │ ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│ │ └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│ ├── &lt;span class="m">2022-06-01&lt;/span>&lt;span class="n">.tar.gz&lt;/span>
│ ├── &lt;span class="m">2022-06-02&lt;/span>
│ │ ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│ │ └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│ ├── &lt;span class="m">2022-06-02&lt;/span>&lt;span class="n">.tar.gz&lt;/span>
│ └── &lt;span class="m">2022-06-03&lt;/span>
│ ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│ └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
└── &lt;span class="n">micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service2&lt;/span>
├── &lt;span class="m">2022-06-01&lt;/span>
│ ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│ └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
├── &lt;span class="m">2022-06-01&lt;/span>&lt;span class="n">.tar.gz&lt;/span>
├── &lt;span class="m">2022-06-02&lt;/span>
│ ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│ └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
├── &lt;span class="m">2022-06-02&lt;/span>&lt;span class="n">.tar.gz&lt;/span>
└── &lt;span class="m">2022-06-03&lt;/span>
├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
└── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>5. 删除已被压缩的 log 文件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-sh" data-lang="sh"> &lt;span class="nb">cd&lt;/span> &lt;span class="nv">$service&lt;/span>
&lt;span class="nv">service_path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;start compress&amp;#34;&lt;/span>
&lt;span class="nv">date_dirs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>ls -d *&lt;span class="k">)&lt;/span>
&lt;span class="k">for&lt;/span> date_dir in &lt;span class="nv">$date_dirs&lt;/span>
&lt;span class="k">do&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;date_dir: &lt;/span>&lt;span class="nv">$date_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> &lt;span class="nv">$date_dir&lt;/span> !&lt;span class="o">=&lt;/span> &lt;span class="nv">$date&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;run tar -zcvf &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">.tar.gz &lt;/span>&lt;span class="nv">$date_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
tar -zcvf &lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>.tar.gz &lt;span class="nv">$date_dir&lt;/span>
&lt;span class="c1"># 判断是否存在压缩完成的文件&lt;/span>
++ &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="si">${&lt;/span>&lt;span class="nv">service_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>.tar.gz &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;span class="c1"># 删除源文件夹&lt;/span>
++ rm -rf &lt;span class="si">${&lt;/span>&lt;span class="nv">service_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>
++ &lt;span class="k">fi&lt;/span>
&lt;span class="k">fi&lt;/span>
&lt;span class="k">done&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>执行 &lt;code>sh java-log-compress.sh&lt;/code> 后的目录结构如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="n">log&lt;/span>
├── &lt;span class="n">micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>
│   ├── &lt;span class="m">2022-06-01&lt;/span>&lt;span class="n">.tar.gz&lt;/span>
│   ├── &lt;span class="m">2022-06-02&lt;/span>&lt;span class="n">.tar.gz&lt;/span>
│   └── &lt;span class="m">2022-06-03&lt;/span>
│   ├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
│   └── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
└── &lt;span class="n">micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service2&lt;/span>
├── &lt;span class="m">2022-06-01&lt;/span>&lt;span class="n">.tar.gz&lt;/span>
├── &lt;span class="m">2022-06-02&lt;/span>&lt;span class="n">.tar.gz&lt;/span>
└── &lt;span class="m">2022-06-03&lt;/span>
├── &lt;span class="n">error.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
└── &lt;span class="n">info.micro&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">service1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">pod&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id.log&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>完整的 Bash Shell：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;span class="cp">&lt;/span>
&lt;span class="nv">base_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/log&amp;#34;&lt;/span>
&lt;span class="nv">date&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date &lt;span class="s2">&amp;#34;+%Y-%m-%d&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="nb">cd&lt;/span> &lt;span class="nv">$base_dir&lt;/span>
&lt;span class="nv">services&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>ls -d micro-service*/&lt;span class="k">)&lt;/span>
&lt;span class="k">for&lt;/span> service in &lt;span class="nv">$services&lt;/span>
&lt;span class="k">do&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;service: &lt;/span>&lt;span class="nv">$service&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="nb">cd&lt;/span> &lt;span class="nv">$base_dir&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -d &lt;span class="nv">$service&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;span class="nb">cd&lt;/span> &lt;span class="nv">$service&lt;/span>
&lt;span class="nv">service_path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;start compress&amp;#34;&lt;/span>
&lt;span class="nv">date_dirs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>ls -d *&lt;span class="k">)&lt;/span>
&lt;span class="k">for&lt;/span> date_dir in &lt;span class="nv">$date_dirs&lt;/span>
&lt;span class="k">do&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;date_dir: &lt;/span>&lt;span class="nv">$date_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> &lt;span class="nv">$date_dir&lt;/span> !&lt;span class="o">=&lt;/span> &lt;span class="nv">$date&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;run tar -zcvf &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">.tar.gz &lt;/span>&lt;span class="nv">$date_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
tar -zcvf &lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>.tar.gz &lt;span class="nv">$date_dir&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="si">${&lt;/span>&lt;span class="nv">service_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>.tar.gz &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
rm -rf &lt;span class="si">${&lt;/span>&lt;span class="nv">service_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">date_dir&lt;/span>&lt;span class="si">}&lt;/span>
&lt;span class="k">fi&lt;/span>
&lt;span class="k">fi&lt;/span>
&lt;span class="k">done&lt;/span>
&lt;span class="k">fi&lt;/span>
&lt;span class="k">done&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="nginx-log-bash-shell">Nginx log Bash Shell&lt;/h3>
&lt;p>Nginx 的 access.log 和 error.log 默认为单文件，为方便日常排查问题和节省磁盘空间同样推荐使用 Bash 脚本切割和压缩 log 文件。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="cp">#!bin/bash
&lt;/span>&lt;span class="cp">&lt;/span>
&lt;span class="c1"># 目标目录&lt;/span>
&lt;span class="nv">logs_path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/usr/local/nginx/logs/history&amp;#34;&lt;/span>
&lt;span class="c1"># 当前 log 目标&lt;/span>
&lt;span class="nv">current_logs_path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/usr/local/nginx/logs&amp;#34;&lt;/span>
&lt;span class="c1"># 昨天的日期&lt;/span>
&lt;span class="nv">yesterday&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>date -d &lt;span class="s2">&amp;#34;yesterday&amp;#34;&lt;/span> +%Y-%m-%d&lt;span class="k">)&lt;/span>
&lt;span class="c1"># 新建日期文件夹&lt;/span>
mkdir &lt;span class="si">${&lt;/span>&lt;span class="nv">logs_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">yesterday&lt;/span>&lt;span class="si">}&lt;/span>
&lt;span class="c1"># 如果存在 log，则移动当前 log 至备份目录&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="si">${&lt;/span>&lt;span class="nv">current_logs_path&lt;/span>&lt;span class="si">}&lt;/span>/access.log &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
mv &lt;span class="si">${&lt;/span>&lt;span class="nv">current_logs_path&lt;/span>&lt;span class="si">}&lt;/span>/access.log &lt;span class="si">${&lt;/span>&lt;span class="nv">logs_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">yesterday&lt;/span>&lt;span class="si">}&lt;/span>/access.log
&lt;span class="k">fi&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="si">${&lt;/span>&lt;span class="nv">current_logs_path&lt;/span>&lt;span class="si">}&lt;/span>/error.log &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
mv &lt;span class="si">${&lt;/span>&lt;span class="nv">current_logs_path&lt;/span>&lt;span class="si">}&lt;/span>/error.log &lt;span class="si">${&lt;/span>&lt;span class="nv">logs_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">yesterday&lt;/span>&lt;span class="si">}&lt;/span>/error.log
&lt;span class="k">fi&lt;/span>
&lt;span class="c1"># 执行压缩&lt;/span>
&lt;span class="nb">cd&lt;/span> &lt;span class="nv">$logs_path&lt;/span>
tar -zcvf &lt;span class="si">${&lt;/span>&lt;span class="nv">yesterday&lt;/span>&lt;span class="si">}&lt;/span>.tar.gz &lt;span class="si">${&lt;/span>&lt;span class="nv">yesterday&lt;/span>&lt;span class="si">}&lt;/span>
&lt;span class="c1"># 删除源文件夹&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="si">${&lt;/span>&lt;span class="nv">logs_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">yesterday&lt;/span>&lt;span class="si">}&lt;/span>.tar.gz &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
rm -rf &lt;span class="si">${&lt;/span>&lt;span class="nv">logs_path&lt;/span>&lt;span class="si">}&lt;/span>/&lt;span class="si">${&lt;/span>&lt;span class="nv">yesterday&lt;/span>&lt;span class="si">}&lt;/span>
&lt;span class="k">fi&lt;/span>
&lt;span class="c1"># 向 Nginx 主进程发送 USR1 信号以打开新的 log 文件&lt;/span>
&lt;span class="nv">nginx_pid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>cat /usr/local/nginx/logs/nginx.pid&lt;span class="k">)&lt;/span>
&lt;span class="nb">kill&lt;/span> -USR1 &lt;span class="nv">$nginx_pid&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="cron-job-自动执行脚本">Cron job 自动执行脚本&lt;/h2>
&lt;p>&lt;a href="https://zh.wikipedia.org/zh-cn/Cron">Cron&lt;/a> 是 Linux 中基于时间的任务管理工具，也就是&lt;strong>定时任务&lt;/strong>。使用以下命令可新增/编辑 Cron job：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">crontab&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">e&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>新增以下两个定时任务（需确保两个 Bash shell 文件具有..可执行..权限，若无权限可使用 &lt;code>chmod +x filename&lt;/code> 命令添加）：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="c1"># 凌晨3点执行压缩 Java 应用 log 脚本&lt;/span>
&lt;span class="m">0&lt;/span> &lt;span class="m">3&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">local&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">bash&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">shell&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">java&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">compress.sh&lt;/span> &lt;span class="o">&amp;gt;/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">null&lt;/span> &lt;span class="m">2&lt;/span>&lt;span class="o">&amp;gt;&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span>
&lt;span class="c1"># 0点执行 Nginx log 切分、压缩&lt;/span>
&lt;span class="m">0&lt;/span> &lt;span class="m">0&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">local&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">bash&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">shell&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">nginx&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">compress.sh&lt;/span> &lt;span class="o">&amp;gt;/&lt;/span>&lt;span class="n">dev&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">null&lt;/span> &lt;span class="m">2&lt;/span>&lt;span class="o">&amp;gt;&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;code>&amp;gt;/dev/null 2&amp;gt;&amp;amp;1&lt;/code> 表示将 Cron job 错误级别的输出（&lt;code>2&amp;gt;&lt;/code>）等同于（&lt;code>&amp;amp;&lt;/code>）标准级别的输出（&lt;code>1&lt;/code>），都丢到黑洞里（&lt;a href="https://zh.m.wikipedia.org/zh-hans//dev/null">&lt;code>/dev/null&lt;/code>&lt;/a>），表示不记录输出，想要记录输出内容可设定为 &lt;code>&amp;gt;/var/log/cron.log 2&amp;gt;&amp;amp;1&lt;/code>。&lt;/p>
&lt;/blockquote>
&lt;p>添加成功后查看已有的 Cron job：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">crontab&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">l&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://wangdoc.com/bash/index.html">Bash 脚本教程 | WangDoc.com&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://askanydifference.com/difference-between-zip-and-gzip/">Difference Between Zip and Gzip (With Table) | AskAnyDifference.com&lt;/a>&lt;/li>
&lt;/ol></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/linux/">Linux</category><category domain="https://xuezenghui.com/tags/bash-shell/">Bash Shell</category></item><item><title>《中台产品经理宝典》读书笔记</title><link>https://xuezenghui.com/posts/reading-notes-for-zhong-tai-chan-pin-jing-li-bao-dian/</link><guid isPermaLink="true">https://xuezenghui.com/posts/reading-notes-for-zhong-tai-chan-pin-jing-li-bao-dian/</guid><pubDate>Sun, 07 Nov 2021 21:20:20 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="食之有味">食之有味&lt;/h2>
&lt;h6 id="p4-产业基本发展规律">P4. 产业基本发展规律：&lt;/h6>
&lt;p>所谓“三段式产业发展规律”，就是指：如果站在一定高度来看，对于包括互联网在内的任意一个产业，其产业周期都可以根据企业核心竞争力的中心而划分为&lt;strong>技术时代&lt;/strong>、&lt;strong>产品时代&lt;/strong>、&lt;strong>市场时代&lt;/strong> 3 个发展阶段，并不断循环更替。&lt;/p>
&lt;blockquote>
&lt;p>之前和领导聊天的时候也聊到过这里，范围缩小一点来说，一个互联网产品，最开始是由技术人员决定它的成长与高度甚至是成败与否，在产品投入市场后产品经理就是一个相当重要的角色了，他链接产品与用户，使得产品在用户体验、满意度等方面进一步提升发展，最终达到像今天我们使用的微信、抖音等成熟产品一样，由市场说了算，比拼企业的市场运作能力（反面教材快手），分别对应如上的三个发展阶段。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h6 id="p35-中台模式">P35. 中台模式：&lt;/h6>
&lt;p>中台的核心本质就是向前台业务提供服务共享，目标是更好地支持前台业务方进行规模化创新或大规模试错，从而更好地响应市场需求。&lt;/p>
&lt;blockquote>
&lt;p>中台模式是..业务层面..的「微服务架构」，而中台本身相当于 Spring Cloud 中的 Nacos 服务注册与发现的角色，用以调配对接各个业务模块之间或业务模块与不同终端之间的集成使用。支持大规模试错这一点与 MVP&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/reading-notes-for-zhong-tai-chan-pin-jing-li-bao-dian/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> 的概念也类似。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h6 id="p58-中台要把握的一个度">P58. 中台要把握的一个度：&lt;/h6>
&lt;p>怎么样真正让这个中台能够&lt;strong>横向服务好每个业务&lt;/strong>，而不是变成一个障碍性的枢纽，这是最难的。&lt;/p>
&lt;blockquote>
&lt;p>的确，类比一个轮子，封装地错综复杂不易用，就会变成一个障碍，然后被反复造，各行其道，那就失去了轮子的意义，失去了中台的意义。因此需要有一个能把握这个度的高业务眼界、高技术深度的人来引领把控方向，那个人在阿里可能是上面那段话的作者——张勇，在未来也未尝没有可能是你，是我。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h6 id="p108-商业模式画布">P108. 商业模式画布：&lt;/h6>
&lt;p>假设我们要开发一款社交软件，首先我们要了解为哪类目标群体提供服务（&lt;strong>用户细分&lt;/strong>），再确定他们的核心需求（&lt;strong>价值主张&lt;/strong>），思考这款软件如何能被她们发现并使用（&lt;strong>渠道通路&lt;/strong>），在用户开始使用软件后我们要怎么实现盈利（&lt;strong>收入来源&lt;/strong>）以及与他们保持什么样的关系（&lt;strong>用户关系&lt;/strong>），凭借什么特殊优势保证我们的这款软件的特殊性不会被竞争者超越（&lt;strong>核心资源&lt;/strong>），为了达成这个目标我们需要采取什么活动（&lt;strong>关键业务&lt;/strong>），最后能支持这个商业模式的合作方都有谁（&lt;strong>重要合作&lt;/strong>），在整个计划开始前我们需要预估整个流程的综合成本会是多少和是否有盈利的可能性（&lt;strong>成本结构&lt;/strong>）。&lt;/p>
&lt;blockquote>
&lt;p>商业模式画布适用任何行业的任何企业，厘清企业中的这九个模块有助于针对性地从某个模块切入或扩大利润或降低成本来为企业优化创收达成商业目标。&lt;/p>
&lt;/blockquote>
&lt;h2 id="余味回甘">余味回甘&lt;/h2>
&lt;p>一本全方位解读「中台」的好书，后半部分内容可作为中台系统建设的参考纲要。目前虽没有参与过中台的建设，但只从「解耦」这一个层面来讲，从开发的微服务架构中就已经窥到、意识到了其长远优势，常见的用户中心、鉴权中心，其实都算是中台的部分实现。再舍身处境地将自己当前遇到的一些其它业务难题带入到中台架构中，也的确会得到不少优化解。中台，难在实践，重在实践，建设成本不可谓不小。&lt;/p>
&lt;h5 id="heading">&lt;/h5>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>最简可行性产品，见&lt;a href="https://zh.wikipedia.org/wiki/%E6%9C%80%E7%B0%A1%E5%8F%AF%E8%A1%8C%E7%94%A2%E5%93%81">最簡可行產品&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/reading-notes-for-zhong-tai-chan-pin-jing-li-bao-dian/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/reading-notes/">reading notes</category></item><item><title>《阿里云运维架构实践秘籍》读书笔记</title><link>https://xuezenghui.com/posts/reading-notes-for-a-li-yun-yun-wei-jia-gou-shi-jian-mi-ji/</link><guid isPermaLink="true">https://xuezenghui.com/posts/reading-notes-for-a-li-yun-yun-wei-jia-gou-shi-jian-mi-ji/</guid><pubDate>Sat, 16 Oct 2021 22:43:40 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="食之有味">食之有味&lt;/h2>
&lt;h6 id="p73-cap-定理">P73. CAP 定理：&lt;/h6>
&lt;p>CAP 定理（CAP theorem）指出，对于一个..分布式..系统来说，其不可能..同时满足..以下 3 点：&lt;/p>
&lt;ul>
&lt;li>Consistency（一致性）：所有节点在同一时间具有相同的数据。&lt;/li>
&lt;li>Availability（可用性）：保证每个请求不管成功或者失败都得到响应。&lt;/li>
&lt;li>Partition tolerance（分区容错性）：系统中任意信息的丢失或失败不会影响系统的继续运作。&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>这里推荐阮一峰的 &lt;a href="https://www.ruanyifeng.com/blog/2018/07/cap.html">《CAP 定理的含义》&lt;/a>协助理解，各数据库模型的实现其实就是对 CAP 定理三个特性的不同取舍。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h6 id="p79-sql-语句优化">P79. SQL 语句优化：&lt;/h6>
&lt;ul>
&lt;li>80% 的数据库性能问题都出在 ..SQL语句..上。&lt;/li>
&lt;li>80% 的 SQL 语句性能问题都是由..索引..引起的。&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>这让我又想起曾经遇到的&lt;a href="../projects/#%E5%A7%94%E5%A4%96%E8%AE%BE%E5%A4%87%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F">业务问题&lt;/a>，事出正是慢查询，而处理这个性能问题时也正是通过先优化 MongoDB 的查询语句——前置业务查询条件及分页查询条件，再优化数据库索引得以缓解。这属实是慢查询的最佳实践了🎉。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h6 id="p192-存储引擎与服务器配置">P192. 存储引擎与服务器配置：&lt;/h6>
&lt;p>由于 MongoDB 存储引擎基于&lt;strong>内存映射&lt;/strong>，因此..高内存..的配置能有效提升 MongoDB 的性能。&lt;/p>
&lt;hr>
&lt;h6 id="p254-k8s-部署应用">P254. K8S 部署应用：&lt;/h6>
&lt;p>在使用容器编排技术时，为了让对应容器高可用，也为了让机器性能的利用率更高，因此容器会根据状态是否存活，以及机器性能使用高低的情况，&lt;strong>在不同的节点之间调度漂移&lt;/strong>。加上本身 K8S 就会消耗机器性能，所以总体来说，&lt;strong>K8S 不适合部署对性能配置依赖很高的应用&lt;/strong>。这就间接说明了为什么我们一般很少将数据库部署在应用中，而是把业务代码部署在容器中，因为这样可以实现快速部署。&lt;/p>
&lt;hr>
&lt;h6 id="p278-云端-devops-最佳实践">P278. 云端 DevOps 最佳实践：&lt;/h6>
&lt;p>GitLab + Jenkins + K8S + 微服务是云端 DevOps 的最佳实践。&lt;/p>
&lt;blockquote>
&lt;p>没错，正是我们当下在做的！&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h6 id="p333-缓存五分钟法则">P333. 缓存五分钟法则：&lt;/h6>
&lt;p>即如果一条记录被频繁访问，就应该考虑放到缓存里。否则的话客户端就按需要直接去访问数据库，而这个临界点就是&lt;strong>五分钟&lt;/strong>。&lt;/p>
&lt;h2 id="余味回甘">余味回甘&lt;/h2>
&lt;h6 id="使用缓存牺牲了什么">使用缓存牺牲了什么？&lt;/h6>
&lt;p>书中一直有一个说法：「缓存是一种典型的以牺牲数据时效性换取访问性能的技术」，我不太赞成这种说法。因为这样的表达是在说：使用缓存的缺点是&lt;strong>没有或者减少了数据的时效性&lt;/strong>，我觉得这没有结合实际业务中使用缓存的前提——开发者肯定是在需要使用缓存的业务情景下才使用缓存的，也就是说我们为某个数据使用了缓存是..知其可用性..的，并没有牺牲什么实质性的时效性，这不算作是使用缓存的一种牺牲，缓存，就是临时存储在内存中的一组数据，该用时用，不该用时不用。&lt;/p></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/reading-notes/">reading notes</category><category domain="https://xuezenghui.com/tags/devops/">DevOps</category><category domain="https://xuezenghui.com/tags/%E6%9E%B6%E6%9E%84/">架构</category></item><item><title>我的爷爷</title><link>https://xuezenghui.com/posts/my-grandfather/</link><guid isPermaLink="true">https://xuezenghui.com/posts/my-grandfather/</guid><pubDate>Tue, 13 Jul 2021 21:43:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>近来因琐碎家事，思绪万千，难以入眠。也理所当然地忆起了一个带给我童年欢乐与依靠感，并足以影响我一生的人——我的爷爷，便想着写篇文字，一是在这繁乱嘈杂的世界中趁记忆还尚未消散将其持久化，以后看来，&lt;strong>笑也好，苦也罢，总是值得&lt;/strong>。二是借现代互联网的特性，谨纪念一位几十年如一日无怨无悔奉献自我、影响他人的人民教师、至善之人，&lt;strong>任何事物被历史淹没都是必然，但我也想让他能泛起些浪花&lt;/strong>。&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/my-grandfather/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;hr>
&lt;p>我的爷爷生于 1936 年 9 月，普通的穷苦农民家庭，那时家里虽过得艰难，爷爷自幼却努力刻苦，识字读书，受了教育。19 岁还在上学的爷爷经人介绍与奶奶结婚，20 岁投身教育事业，在家乡的中学教授语文，后也带过班级当了很多年班主任。至 1998 年退休共执教四十余载，不说桃李满天下，但也倍有才人出。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/my-grandfather_honour.webp" alt="honour.webp" title="乡村教育贡献荣誉">&lt;/p>
&lt;p>工作所需他借此也算游过大江南北，登顶华山走过长空栈道，坐过渡轮去达杭州西湖，这些阅历在那个年代不可谓不丰富。爷爷是个极其顾家的读书人，文革期间，爷爷的转正受到了影响，每月 27 元工资拿了十数年，中间许多人因为工资难以糊口不做老师了，爷爷念叨着不做了不做了却一直坚持，只不过空闲时拼了命下田干活挣工分，得以养家。可有人看不下去了，咬定爷爷家是“富农”成分，为此爷爷只身一人前往北京上诉，才得以平反。我的记忆里，他还有着这几大爱好：&lt;/p>
&lt;p>一、&lt;strong>看书&lt;/strong>。常听奶奶念叨爷爷年轻时爱买书，可能因为勤俭持家的缘故，这个爱好在我的记忆里已经不太鲜明了，但从家里堆的几箱书也可以见得不假，有我小时候也翻过的《鲁迅全集》，有竖着排版的《聊斋志异》，有我小学作工具书用的《汉语言字典》（可惜这些书由于一些原因见不到了，一大憾事）。爱看书我是真的切有感受，老头子经常在院子里戴着自诩舒服的“石框眼镜”看得入迷呢，不管报纸书籍甚至领来的广告小刊。&lt;/p>
&lt;p>二、&lt;strong>写字&lt;/strong>。为什么爱写毛笔字这个问题我问过爷爷，得到的答案是——教书时在学校闲着没事干，但这没事干练出来的字可是被乡里乡亲大大认可了，无论红白喜事的礼单执事单，还是逢年过节的春联灵位，都得找薛老师提笔落墨。为了方便他还收集素材，把知道的见到的好联都抄记在小本子上用时查阅，不仅如此，还教了我祖宗灵位的书写格式，当时我记录在 QQ 空间，得以如此，这些年过年祭祀祖宗时家里和几个同乡邻居家都用上了。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/my-grandfather_mark.webp" alt="mark.webp" title="记录在 QQ 空间的内容">&lt;/p>
&lt;p>我看得出来，爷爷是喜欢写字的，看鲁迅的他肯定知道文字是有力量的。想来也是，在邻居侵占门口公共道路用地私建“自家”院落时，爷爷也曾写下多封举报信诉其无法无德，想以文字散发力量，可惜彼时小县城的相关机构腐败无能，投诉无果，只靠公序良俗又怎么能限制得了这小乡村的流氓泼妇呢？最终只落下一句“人善被人欺”。&lt;/p>
&lt;p>三、&lt;strong>捡柴禾&lt;/strong>。这个爱好和职业无关，但深深刻在我的印象里。以前家里烧火做饭，免不了需要柴禾。爷爷是个闲不下的人，每逢下雨河里发水便去捞从山里冲下来的树枝木棍，到现在想来都尽是一副穿着背心挽着裤腿的形象，结果就是家里房檐下院落中总是整整齐齐堆着劈好的烧不完的柴禾。与此同时，因为总在冰冷的河水里待着，爷爷的腿被刺激得血管凸起落下毛病，尽管如此家人还是劝说不动，他把这当成了一个爱好、一种付出。&lt;/p>
&lt;hr>
&lt;p>要说爷爷与我，那趣事可真的一堆呢。我是家中最小的孩子，从小受爷爷奶奶的宠爱也自然多些，很小的时候，爷爷家养了头大黄牛，犁地拉车的算是为这个家做过不少贡献，爷爷可是很爱护这头功臣的，青草面汤好生“伺候”，还经常拉出去溜溜。可架不住我非要骑上一骑这大黄牛啊，一番撒泼打滚后爷爷抱起我让我两腿一跨骑在那大黄牛上一顿“驾！”，那时一翁一童的放牛日子真是悠哉，走在山间路旁，听他给我讲为什么这个坡叫刺坡，为什么那个沟叫大烟囱沟。&lt;/p>
&lt;p>龙口&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/my-grandfather/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>的对岸是刘家，荒野山下只有一个小房子住着一个老头——刘家老汉，孤苦伶仃只有一条狼狗作伴，靠着门前的桃树、杏树、梅李换取生活所需。爷爷喜欢过去跟他闲谝，夏天还经常带我过去玩，抱着个大西瓜拿过去给刘家老汉，老汉门前没熟的梅李现在想想都酸！后来刘家老汉去世了，村民这才想起他家还有那么多果树，掠夺殆尽。&lt;/p>
&lt;p>我小时候不懂事，在现在看来是个极其残忍之人，经常用各种道具残杀门口弱小的蚂蚁🐜，火烧水淹无所不用其极，爷爷看到了一定会制止我，脾气极好、一本端正地告诉我这样做不对，蚂蚁再小也是条命儿。也许是由于这样的敦敦教诲，也许是心智成熟了，后面的我看见蚂蚁飞虫都不忍踩踏，甚至还有些怯惧。&lt;/p>
&lt;p>六年级我转学去了县里，周五总是爷爷骑着那辆听他说一次驼过几百斤粮食的二八自行车接我回家，他不紧不慢地蹬着自行车走在还未通车的高速路上，我坐在后座问着小脑袋里冒出的各种古怪问题。到了初中以后，每个月爷爷都用他攒来的工资（记得我很小的时候爷爷工资一个月才 30 元）和退休金拨给我们几个孩子生活费，不厌其烦地教导我们要好好学习，将来做一个对社会有用的人。&lt;/p>
&lt;hr>
&lt;p>在我高中的时候，爷爷的身体就不好了，再到后面就病重卧床了，这段艰难的日子他只有半夜疼醒时才会对奶奶诉说自己的病痛，他人再问都是“不要紧”来搪塞。让我忘不掉的是每每我周末回家时，爷爷都拉着我的手说最放不下的就是我，盼我出息，毕业后有个好的安顿，与对象的事能有个定数。于我而言，这段时间也是最让我追悔的，一是陪伴爷爷的日子太少了，二是没带对象去看看他，让他安心。最让我痛惜的是 2016 丙申年的春节前夕，爷爷走的那天我正好不在，从此便深知&lt;strong>珍惜眼前人，莫使人去空流泪&lt;/strong>。&lt;/p>
&lt;p>他操劳一生，生前就安排好了自己后山的葬处，一些事更是在走后才知道，好衣服舍不得穿都压在箱底，给奶奶专门留了养老钱怕受委屈……虽然我是一个无神论者，但心里还是希望在那边的爷爷能多心疼心疼自己，也希望他能看到今天的笔者，得以欣慰，甚至骄傲。也许未来从我的整个人生历程来看，有爷爷的陪伴的日子并不很长，但我对爷爷的印象却远超父亲，无论是做人还是做事，都对我影响至深。&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>还有一部分原因是受温总理&lt;a href="https://2newcenturynet.blogspot.com/2021/04/blog-post_35.html">《我的母亲》&lt;/a>一文影响，也想抒发一些情感。 &lt;a href="https://xuezenghui.com/posts/my-grandfather/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>家乡门前河流上游的一个大水潭名。 &lt;a href="https://xuezenghui.com/posts/my-grandfather/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/life/">Life</category><category domain="https://xuezenghui.com/tags/mine/">mine</category></item><item><title>前端工程化之 npm 私服</title><link>https://xuezenghui.com/posts/private-npm/</link><guid isPermaLink="true">https://xuezenghui.com/posts/private-npm/</guid><pubDate>Wed, 28 Apr 2021 16:30:30 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>工程化系列的&lt;a href="../component-based-development">上篇文章&lt;/a>讲到了组件的发布及使用，这篇文章就来看看如何..高效..搭建..易用..的&lt;strong>私有组件仓库&lt;/strong>。&lt;/p>
&lt;p>组件有跨项目共用的需求，也就要考虑到组件代码的存储和传播了，排除了 Bit 后满足这两点的最适宜内部组件存储的介质就是 npm 私有仓库了，它具有以下几个特质：&lt;/p>
&lt;ul>
&lt;li>安全性：只有内网可访问，避免代码泄露&lt;/li>
&lt;li>复用性：与 npm 公共库一样便于下载传播&lt;/li>
&lt;li>版本管理：统一管理组件库版本，语义化版本号&lt;/li>
&lt;/ul>
&lt;p>再综合考虑其它因素如搭建复杂度、框架量级、业务场景，我们选择了使用 &lt;a href="https://verdaccio.org/">Verdaccio&lt;/a> 来搭建公共组件仓库。&lt;/p>
&lt;h2 id="verdaccio-介绍">Verdaccio 介绍&lt;/h2>
&lt;p>Verdaccio 是一个基于 Node.js 的轻量化私有 npm proxy registry，proxy registry 对应的便是 npm 官方的 public registry，具体概念见 &lt;a href="https://docs.npmjs.com/cli/v7/using-npm/registry">registry | npm&lt;/a>。简而言之，Verdaccio 的本质作用是管理发布上来的 npm 包，并且使之与官方包隔离，易用性主要体现在这些地方：&lt;/p>
&lt;ul>
&lt;li>配置方式简单&lt;/li>
&lt;li>权限控制清晰&lt;/li>
&lt;li>开箱即用的 Web 界面&lt;/li>
&lt;/ul>
&lt;h2 id="搭建组件仓库">搭建组件仓库&lt;/h2>
&lt;h3 id="安装-verdaccio">安装 Verdaccio&lt;/h3>
&lt;p>私有的组件仓库要能被其他开发人员访问，就要放在服务器上了。确保服务器安装好 Node 环境后，全局安装 Verdaccio：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">g&lt;/span> &lt;span class="n">verdaccio&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>安装好后终端输入 &lt;code>verdaccio&lt;/code> 即可启动服务，由于 Verdaccio 是一个 Node 服务，会随着终端关闭终止服务，可使用 PM2 管理：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">pm2&lt;/span> &lt;span class="n">start&lt;/span> &lt;span class="n">verdaccio&lt;/span> &lt;span class="c1"># 启动服务&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">pm2&lt;/span> &lt;span class="n">list&lt;/span> &lt;span class="c1"># 查看服务列表&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">pm2&lt;/span> &lt;span class="n">log&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="c1"># 查看日志&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">pm2&lt;/span> &lt;span class="n">stop&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="c1"># 关闭服务&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>成功启动服务后 log 中会显示四条主要信息：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-shell" data-lang="shell">warn --- config file - /root/.config/verdaccio/config.yaml &lt;span class="c1"># 配置文件位置&lt;/span>
warn --- Plugin successfully loaded: verdaccio-htpasswd &lt;span class="c1"># 鉴权插件&lt;/span>
warn --- Plugin successfully loaded: verdaccio-audit &lt;span class="c1"># 审核依赖插件&lt;/span>
warn --- http address - http://localhost:4873/ - verdaccio/5.0.1 &lt;span class="c1"># 服务入口&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此时浏览器访问服务器的 4873 端口（云服务器需确保在安全组开启该端口）可进入私有仓库的 Web 界面，当然了，目前还是比较简陋的。&lt;/p>
&lt;h3 id="配置-verdaccio">配置 Verdaccio&lt;/h3>
&lt;p>Web 界面、插件、权限等配置都在启动时日志显示的配置文件中修改，编辑该文件为：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="c"># /root/.config/verdaccio/config.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="c"># 数据的存储位置&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>/root/.local/share/verdaccio/storage&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="c"># 插件目录位置&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">plugins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>./plugins&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="c"># web 界面配置&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 网页标题&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>Verdaccio&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">favicon&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>https&lt;span class="p">:&lt;/span>//xuezenghui.com/favicon.ico&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># web 界面语言&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">i18n&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>zh-CN&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">htpasswd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 存储用户认证信息的文件地址&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>./htpasswd&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 最大用户数，设为-1表示不允许自行注册&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">max_users&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="c"># 私有仓库没有找到包时的查找路径&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">uplinks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">npmjs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>https&lt;span class="p">:&lt;/span>//registry.npmjs.org/&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">packages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">&amp;#39;**&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">access&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>$all&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">publish&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>$authenticated&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">proxy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>npmjs&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">keepAliveTimeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">60&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">middlewares&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">audit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">logs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="k">type: stdout, format: pretty, level&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>http&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="初始化组件仓库">初始化组件仓库&lt;/h3>
&lt;p>接下来需要将上次封装好的组件发布到配置好的私有 npm 仓库中，具体步骤：&lt;/p>
&lt;p>&lt;strong>1. 本地注册 Verdaccio 用户&lt;/strong>&lt;/p>
&lt;p>本地注册用户需执行 &lt;code>$ npm adduser --registry &amp;lt;IP 地址:4873&amp;gt;&lt;/code> 并按提示输入用户名、密码和邮箱，完成后可在 Web 界面进行登陆查看仓库内容，也才能在本地发布/使用私有的 npm 包。&lt;/p>
&lt;p>但我们不能允许任何人都可以注册用户，所以通常需要设置配置文件中的 &lt;code>max_users&lt;/code> 属性为 -1 禁止自行注册，需要注册时可使用官方工具 &lt;a href="https://hostingcanada.org/htpasswd-generator/">Htpasswd Generator&lt;/a> 创建密钥信息，由管理员添加进&lt;code>htpasswd&lt;/code>文件中，避免反复打开/关闭配置文件中的属性。&lt;/p>
&lt;p>&lt;strong>2. 项目文件及配置&lt;/strong>&lt;/p>
&lt;p>先给公共组件项目创建软件包的代码仓库 &lt;code>lib&lt;/code> 目录，创建 &lt;code>lib/components.js&lt;/code> 文件来处理仓库中的 Vue 组件，文件内容与 &lt;code>src/utils/global.js&lt;/code> ..基本..一致：&lt;/p>
&lt;blockquote>
&lt;p>要注意的是&lt;code>lib&lt;/code>目录里不能使用别名导入文件，比如表示 &lt;code>src&lt;/code> 目录的 &lt;code>@&lt;/code>，需替换成相对路径。&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">upperFirst&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;lodash/upperFirst&amp;#39;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">camelCase&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;lodash/camelCase&amp;#39;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">requireComponent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;../src/components&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sr">/\.vue$/&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">requireComponent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileName&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">componentConfig&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">requireComponent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileName&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">componentName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">upperFirst&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">camelCase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileName&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">pop&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sr">/\.\w+$/&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">)));&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">component&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`Z&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">componentName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">componentConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">default&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">componentConfig&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>将 Vuetify 配置一并导出，确保仓库中的组件与组件仓库里穿着同样的「衣服👔」：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vuetify&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vuetify/lib&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="s1">&amp;#39;@mdi/font/css/materialdesignicons.css&amp;#39;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="s1">&amp;#39;@/assets/sass/main.scss&amp;#39;&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">Vuetify&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">theme&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">primary&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#6DA4D8&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">secondary&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#FDDB55&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">error&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#EF3A61&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">success&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#51AD5A&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">info&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#6DA4D8&amp;#39;&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">theme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">themes&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">dark&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">theme&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">light&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">theme&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">icons&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">iconfont&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;mdi&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Vuetify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">options&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>创建包的入口文件 &lt;code>lib/main.js&lt;/code>，引入样式和所有组件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="s1">&amp;#39;../src/assets/sass/main.scss&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="s1">&amp;#39;./components&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后在&lt;code>package.json&lt;/code>文件中设置好包名&lt;code>name&lt;/code>、版本号&lt;code>version&lt;/code>等描述信息，添加&lt;code>main&lt;/code>指定包的入口文件，添加&lt;code>files&lt;/code>指定库中包含的文件目录，并修改属性&lt;code>private&lt;/code>为&lt;code>false&lt;/code>保证包可被发布，这部分与 npm 发布包一致，参考 &lt;a href="https://docs.npmjs.com/cli/v7/configuring-npm/package-json">package.json | npm Docs&lt;/a>。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="o">+-&lt;/span> &lt;span class="s2">&amp;#34;private&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;lib/main.js&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="s2">&amp;#34;files&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="s2">&amp;#34;lib&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="s2">&amp;#34;src/assets&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="s2">&amp;#34;src/components&amp;#34;&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="p">]&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 发布公共组件&lt;/strong>&lt;/p>
&lt;p>指定 register 地址发布包到私有仓库：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">publish&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">registry&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">IP&lt;/span> 地址&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>发布成功后会显示包的一些元数据：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/private-npm:publish.webp" alt="publish.webp" title="发布成功">&lt;/p>
&lt;p>访问 Web 界面可看到包的基本信息：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/private-npm:web.webp" alt="web.webp" title="Web 界面">&lt;/p>
&lt;hr>
&lt;p>当然了，组件库应该设置一定的读写权限来&lt;strong>控制包的使用和发布权限&lt;/strong>，Verdaccio 中包的使用权限逻辑默认是这样的：&lt;/p>
&lt;p>如果你配置了账户，那么你就能登入 Web 界面查看库中软件包的详细信息，有了包名和 registry 地址，你就能安装使用了。也就是说，&lt;strong>有无账户体现在对包的可见性&lt;/strong>。&lt;/p>
&lt;p>权限在配置文件的&lt;code>packages&lt;/code>下添加：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="k">packages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">&amp;#39;my-components&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用权限&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">access&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>$authenticated&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 发布权限&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">publish&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>zander&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>权限设为 &lt;code>$all&lt;/code> 表示任何人对这个包有相关权限，&lt;code>$authenticated&lt;/code>表示只有经过验证的有权限，此处发布权限设为了固定的用户，在进行 publish 操作时 Verdaccio 会根据用户注册时自动生成的&lt;code>authToken&lt;/code>（可通过&lt;code>$ npm config ls -l&lt;/code>命令查看）进行鉴权。&lt;/p>
&lt;/blockquote>
&lt;h2 id="使用公共组件">使用公共组件&lt;/h2>
&lt;p>OK，至此，NPM 私服就已经搭建完成了，而后其它项目中便可以像使用普通 npm 包一样简单地使用企业内部公共组件了，先安装组件 npm 包：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">my&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">components&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">registry&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">IP&lt;/span> 地址&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">D&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>本案例中使用了 Vuetify 组件库，因此使用公共组件的项目中要正确显示组件样式需确保安装完整 Vuetify 的相关依赖，推荐使用 &lt;code>$ vue add vuetify&lt;/code>。&lt;/p>
&lt;p>最后，在 &lt;code>src/main.js&lt;/code> 中引入组件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="s1">&amp;#39;my-components&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;em>Now, enjoy your components in the global Vue!&lt;/em>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">z-btn&lt;/span> &lt;span class="na">large&lt;/span> &lt;span class="na">color&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;yellow&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>🎉🎉&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">z-btn&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="结语">结语&lt;/h2>
&lt;p>这篇文章拖了挺久的，学习和再总结的过程花费了不少时间，但工程化相关的探索似乎才刚刚开始，也如我所说，在项目迭代、人员变动等影响因素下去实际运用抽离的组件，推动团队的组件化进程才是步履维艰的。&lt;/p>
&lt;p>组件管理也应有一个合理的流程，协调好 UI 设计者、组件开发者、测试者和使用者等参与人员的流程关系同样重要，我总结了一个组件定义的流程图，我们团队目前在遵循此流程：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/private-npm:flow.webp" alt="flow.png" title="共用组件定义流程">&lt;/p>
&lt;p>另，本文涉及的代码位于我的 GitHub：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/Xuezenghuigithub/my-storybook">Xuezenghuigithub/my-storybook&lt;/a>&lt;/li>
&lt;/ul></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/npm/">npm</category><category domain="https://xuezenghui.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/">前端工程化</category></item><item><title>夸克之细节设计</title><link>https://xuezenghui.com/posts/the-detailed-design-of-quark/</link><guid isPermaLink="true">https://xuezenghui.com/posts/the-detailed-design-of-quark/</guid><pubDate>Mon, 01 Mar 2021 10:25:49 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>想众友已多闻 IOS 与 Android 皆有一极简好用之浏览器具，名曰 Quark —— 夸克。众所周知，于移动端而言，因原生应用之体验深度高、功能集中性强，浏览器多于搜查问题、访问站点等场景时，作工具之用。&lt;/p>
&lt;p>既是工具，但求用其时..有惊人之效速，无广告之烦扰..，简约简洁，达意即可。也正因如此，夸克得以脱颖而出，广受好评。&lt;/p>
&lt;p>且慢，本文并无强行安利之意，而是近日，缘追剧之因，余见其一细节之设计，深得吾意，遂尝分享。&lt;/p>
&lt;p>是时，于网站内用夸克之内置视频播放器观剧，吾初右手持机，点击屏幕右侧，锁屏等按钮现于右侧，并无惊奇：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/the-detailed-design-of-quark:right.webp" alt="right.PNG" title="锁屏等按钮位于右侧">&lt;/p>
&lt;p>且当我换左手持之后，便只可触碰到屏幕左侧之区域，欲靠右侧来解锁屏幕相当不便，然当左手点击左侧屏幕时锁屏等按钮即现于左侧😺，妙哉妙哉：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/the-detailed-design-of-quark:left.webp" alt="left.PNG" title="锁屏等按钮位于左侧">&lt;/p>
&lt;p>此功能虽仅靠监听点击事件以判断区域即可实现，然于一普通用户而言，&lt;strong>设计即是设计，与实现方式并无牵连，与华丽外表形意不大&lt;/strong>，且至少，于其它播放介质之上，皆不见如此为用户之考虑。&lt;/p>
&lt;p>末，以乔布斯之言了结罢：&lt;/p>
&lt;p>&lt;strong>&lt;center>“The design is not just what it looks like and feels like. The design is how it works.”&lt;/center>&lt;/strong>&lt;/p></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/design/">design</category></item><item><title>前端工程化之组件化开发</title><link>https://xuezenghui.com/posts/component-based-development/</link><guid isPermaLink="true">https://xuezenghui.com/posts/component-based-development/</guid><pubDate>Tue, 09 Feb 2021 11:31:30 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>时隔一年，&lt;a href="../bit">Bit&lt;/a> 情理之中地没有🔥起来。So, 我们团队经过对其它组件管理工具的 POC，同时把 Bit 做不到的事情作为重点来考量，确定了 Storybook + NPM 的技术方案来开发和管理前端共用组件，并且正巧撞上了 &lt;a href="https://medium.com/storybookjs/storybook-6-0-1e14a2071000">Storybook 6.0&lt;/a>，组件的定义方式和组件文档的撰写方式都有很大程度的突破，更适合前端开发人员了&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/component-based-development/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>，这点我是通过不同版本的使用切实体会到了👍。6.0 同时应广大使用者的吐槽重写了 Storybook 的官方文档，可在目前来看依然存在一些问题，比如没有中文版本、没有 Vue 的示例代码，着实让我们在实践的过程中费了不少功夫，但好在终九转功成，集成组件预览、动态更新、参数说明等功能的自动化前端共用组件平台成功搭建运转🎉。&lt;/p>
&lt;p>组件化其实并不难，只需要考虑三个问题：&lt;/p>
&lt;ol>
&lt;li>如何..定义..组件？&lt;/li>
&lt;li>如何..展示..组件？&lt;/li>
&lt;li>如何..共用..组件？&lt;/li>
&lt;/ol>
&lt;p>1难在如何根据业务维度抽离组件，同时也考验组件封装的能力，也就是代码的基本功了；2难在展示工具的选取和实际用法，很庆幸，这趟浑水我已经替看到这篇文章的你淌了😌，答案就是 Storybook；第3个问题先抛开技术难度，它更考量整个团队的协作度去实际运用抽离的组件，共同推进组件化进程，这无疑才是最难实现的，否则整个工程化又有什么意义呢？(自省)&lt;/p>
&lt;h2 id="初始化组件文档">初始化组件文档&lt;/h2>
&lt;p>组件文档的目的是展示共用的 UI 组件，将来需要部署至线上，因此它本质上就是一个站点，而 Storybook 就是生成这个站点工具，它能与项目的主程序隔离构建和运行。Storybook 做的其实很简单，它能给你的每一个组件添加一个..故事..，在这个故事里可任由你对组件点缀发挥，最后它将这一个个故事放在生成的站点上，就组成了《组件文档》。&lt;/p>
&lt;h3 id="创建项目安装依赖">创建项目，安装依赖&lt;/h3>
&lt;p>既然组件文档本质就是站点，那么按一个 Vue 项目来创建，再添加需要用到的依赖项：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">vue&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="n">my&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">storybook&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">cd&lt;/span> &lt;span class="n">my&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">storybook&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">vue&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="n">vuetify&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">npx&lt;/span> &lt;span class="n">sb&lt;/span> &lt;span class="n">init&lt;/span> &lt;span class="c1"># 安装Storybook&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>本文基于 &lt;a href="https://v2.vuetifyjs.com/zh-Hans/">Vuetify 2.4.0&lt;/a> 进行组件抽离封装，其它 UI 组件库需自行验证与 Storybook 的结合方式。&lt;/p>
&lt;/blockquote>
&lt;p>成功后执行 &lt;code>$ npm run storybook&lt;/code> 会启动 Storybook 并自动在浏览器打开网页，默认使用的是6006端口，不与 Vue 冲突，所以开发组件时可以使用 Vue 项目实时预览组件的改动，开发完成后再引入 Storybook。&lt;/p>
&lt;h3 id="目录及配置">目录及配置&lt;/h3>
&lt;h4 id="storybook-配置">Storybook 配置&lt;/h4>
&lt;p>&lt;code>.storybook&lt;/code> 目录下存放 Storybook 的配置文件，修改主配置文件 &lt;code>main.js&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="cm">/* 文件位置：.storybook/main.js */&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;path&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 故事文件的放置目录
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">stories&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;../src/stories/**/*.stories.js&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="c1">// 要用到的插件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">addons&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;@storybook/addon-actions&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;@storybook/addon-links&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="c1">// Storybook的webpack配置
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">webpackFinal&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">async&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">config&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">configType&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 配置可在故事文件里使用别名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">config&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resolve&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">alias&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;@&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">__dirname&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;..&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;src&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 配置解析 Sass
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">config&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">rules&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">test&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sr">/\.s(a|c)ss$/&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">use&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;style-loader&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;css-loader&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;sass-loader&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nx">include&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">path&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">__dirname&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;../&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">config&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="vuetify-配置">Vuetify 配置&lt;/h4>
&lt;p>主题色、辅色等 Vuetify 相关的配置在自动生成的 &lt;code>src/plugins/vuetify.js&lt;/code> 文件中。这里建议直接引入 &lt;code>vuetify/lib&lt;/code>，因为默认的 &lt;code>vuetify/lib/framework&lt;/code> 内不包含样式部分，Vue 里是通过 &lt;a href="https://www.npmjs.com/package/vuetify-loader">vuetify-loader&lt;/a> 编译的，而后面 Storybook 里没有 vuetify-loader，会导致样式失常，直接导入 &lt;code>vuetify/lib&lt;/code> 一劳永逸😎。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="cm">/* 文件位置：src/plugins/vuetify.js */&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vuetify&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vuetify/lib&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">Vuetify&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">theme&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">primary&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#6DA4D8&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">secondary&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#FDDB55&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">error&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#EF3A61&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">success&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#51AD5A&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">info&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#6DA4D8&amp;#39;&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">theme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">themes&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">dark&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">theme&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">light&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">theme&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">icons&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">iconfont&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;mdi&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Vuetify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">options&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="装饰器">装饰器&lt;/h4>
&lt;p>基于特定 UI 库的组件需要搭配&lt;a href="https://storybook.js.org/docs/vue/writing-stories/decorators">装饰器 Decorators&lt;/a> 来在 Storybook 中渲染，创建全局装饰器文件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="cm">/* 文件位置：.storybook/preview.js */&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vuetify&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vuetify&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">options&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;@/plugins/vuetify&amp;#39;&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">Vuetify&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">parameters&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 自动为组件文档中的事件匹配参数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">actions&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">argTypesRegex&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^on[A-Z].*&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">vuetify&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Vuetify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">options&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">decorators&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">(&lt;/span>&lt;span class="nx">story&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 包装组件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">wrapped&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">story&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="c1">// 返回 Vue 子类，表示每一个故事在Storybook里渲染出来都是一个完整的Vue实例
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">extend&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">vuetify&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">components&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">wrapped&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">template&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`
&lt;/span>&lt;span class="sb"> &amp;lt;v-app&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-main&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;wrapped /&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;/v-main&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;/v-app&amp;gt;
&lt;/span>&lt;span class="sb"> `&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>如果你在 Vuetify 的配置文件里非要引入 &lt;code>vuetify/lib/framework&lt;/code>，那么需在这里另外引入样式文件 &lt;code>import 'vuetify/dist/vuetify.min.css';&lt;/code>。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;p>上面就是确保 Storybook 可正常渲染 Material Design 风格组件最基本的配置了，当前步骤的目录可参考：&lt;/p>
&lt;pre>&lt;code>.
├─ .storybook/
│ ├─ preview.js
│ └─ main.js
├─ doc/
├─ node_modules
├─ public/
├─ src/
│ ├─ assets/
│ ├─ components/
│ ├─ plugins/
│ │ └─ vuetify.js
│ ├─ stories/
│ ├─ App.vue
│ └─ main.js
└─ README.md
&lt;/code>&lt;/pre>&lt;h2 id="抽离封装组件">抽离封装组件&lt;/h2>
&lt;p>前端抽离划分组件的目的是降低页面的耦合度，解决页面内或页面间的代码复用性问题，但并不是说要划分得越细越好，无脑地抽离导致遍地皆组件没有什么意义，而是一定要&lt;strong>从业务出发&lt;/strong>，考虑业务使用的场景和逻辑的合理性，不同公司、不同业务甚至不同项目之间组件的可代入性和可替换性都是无法确定的。所以这里只从技术角度出发，以按钮组件 Button 重点看看..封装..组件的部分。&lt;/p>
&lt;h3 id="设置全局组件">设置全局组件&lt;/h3>
&lt;p>为了不用在抽离组件时频繁引入组件、注册组件来预览，先配置 &lt;code>src/componnets/&lt;/code> 目录下所有组件为全局组件：&lt;/p>
&lt;ol>
&lt;li>安装 lodash：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">lodash&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ol start="2">
&lt;li>创建文件 &lt;code>src/utils/global.js&lt;/code>：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">upperFirst&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;lodash/upperFirst&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">camelCase&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;lodash/camelCase&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1">// 引入目录下的全部组件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">requireComponent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;@/components&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sr">/\.vue$/&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">requireComponent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileName&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 获取组件配置
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">componentConfig&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">requireComponent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileName&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 处理获取组件名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">componentName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">upperFirst&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">camelCase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileName&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">pop&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sr">/\.\w+$/&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">)));&lt;/span>
&lt;span class="c1">// 全局注册组件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">component&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`Z&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">componentName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">componentConfig&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">default&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">componentConfig&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ol start="3">
&lt;li>引入文件：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="cm">/* 文件位置：src/main.js */&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="kr">import&lt;/span> &lt;span class="s1">&amp;#39;./utils/global.js&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样处理后，在组件目录下编写的所有 Vue 组件在任意位置都可直接使用：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">z-button&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="封装-button-组件">封装 Button 组件&lt;/h3>
&lt;p>..二次封装..要求一定要对&lt;a href="https://v2.vuetifyjs.com/zh-Hans/components/buttons/">原组件&lt;/a>属性和方法的使用了如指掌，必要时也需要深入源码探究组件功能的实现方式。先从业务角度出发确定按钮的使用场景，随之也就基本确定了需要用到的属性和方法，如：&lt;/p>
&lt;ul>
&lt;li>&lt;code>color&lt;/code>：按钮颜色&lt;/li>
&lt;li>&lt;code>width&lt;/code>：宽度&lt;/li>
&lt;li>&lt;code>disabled&lt;/code>：禁用&lt;/li>
&lt;li>&lt;code>icon&lt;/code>：指定为图标按钮&lt;/li>
&lt;li>...&lt;/li>
&lt;/ul>
&lt;p>所以，Component Z-Button v0.0.1：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-btn&lt;/span>
&lt;span class="nt">:color&lt;/span>&lt;span class="s">=&amp;#34;color&amp;#34;&lt;/span>
&lt;span class="nt">:width&lt;/span>&lt;span class="s">=&amp;#34;width&amp;#34;&lt;/span>
&lt;span class="nt">:color&lt;/span>&lt;span class="s">=&amp;#34;color&amp;#34;&lt;/span>
&lt;span class="nt">:width&lt;/span>&lt;span class="s">=&amp;#34;width&amp;#34;&lt;/span>
&lt;span class="nt">:x-small&lt;/span>&lt;span class="s">=&amp;#34;xSmall&amp;#34;&lt;/span>
&lt;span class="nt">:small&lt;/span>&lt;span class="s">=&amp;#34;small&amp;#34;&lt;/span>
&lt;span class="nt">:large&lt;/span>&lt;span class="s">=&amp;#34;large&amp;#34;&lt;/span>
&lt;span class="nt">:x-large&lt;/span>&lt;span class="s">=&amp;#34;xLarge&amp;#34;&lt;/span>
&lt;span class="nt">:icon&lt;/span>&lt;span class="s">=&amp;#34;icon&amp;#34;&lt;/span>
&lt;span class="nt">:disabled&lt;/span>&lt;span class="s">=&amp;#34;disabled&amp;#34;&lt;/span>
&lt;span class="na">v&lt;/span>&lt;span class="nt">-on&lt;/span>&lt;span class="err">=&amp;#34;$&lt;/span>&lt;span class="na">listeners&lt;/span>&lt;span class="err">&amp;#34;&lt;/span>
&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">slot&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">slot&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/v-btn&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Button&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">props&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="k">default&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;primary&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">Number&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nx">icon&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Boolean&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">disabled&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Boolean&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">small&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Boolean&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">xSmall&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Boolean&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">large&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Boolean&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">xLarge&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Boolean&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>其中，&lt;code>v-on=&amp;quot;$listeners&amp;quot;&lt;/code> 直接监听组件的原生事件，比如点击事件直接在使用时绑定即可。&lt;code>&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;&lt;/code> 插槽给组件提供了较高的可自定义程度。预览组件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="cm">/* 文件位置：src/App.vue */&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-app&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-main&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">z-btn&lt;/span> &lt;span class="na">color&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;info&amp;#34;&lt;/span> &lt;span class="na">width&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;100&amp;#34;&lt;/span> &lt;span class="nt">@click&lt;/span>&lt;span class="s">=&amp;#34;hello&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-icon&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="nx">mdi&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nx">heart&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/v-icon&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/z-btn&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/v-main&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/v-app&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;App&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">methods&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">hello&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;hello&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="编写组件文档">编写组件文档&lt;/h3>
&lt;p>抽离封装好组件后再转到 Storybook 的部分，开始之前先完善两件事：&lt;/p>
&lt;p>&lt;strong>1. 安装用于生成文档的插件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">dev&lt;/span> &lt;span class="o">@&lt;/span>&lt;span class="n">storybook&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">addon&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">essentials&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="cm">/* 文件位置：.storybook/main.js */&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="nx">addons&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;@storybook/addon-actions&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;@storybook/addon-links&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;@storybook/addon-essentials&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>是的，这是 Storybook 6.0 的第一个“哇塞”，它可以让你零配置即可体验..组件文档..的奇妙，包括自动生成的文档、参数控制、操作记录等。&lt;/p>
&lt;p>&lt;strong>2. 处理 icon&lt;/strong>&lt;/p>
&lt;p>默认使用的 icon 是通过 &lt;code>public/index.html&lt;/code> 引入的，所以 Storybook 内无法显示，需手动安装：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">@&lt;/span>&lt;span class="n">mdi&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">font&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在 &lt;code>src/main.js&lt;/code> 和 &lt;code>.storybook/preview.js&lt;/code> 中&lt;strong>分别&lt;/strong>引入：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="o">++&lt;/span> &lt;span class="kr">import&lt;/span> &lt;span class="s1">&amp;#39;@mdi/font/css/materialdesignicons.css&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>然后为你的组件创建故事吧🤡：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="cm">/* 文件位置：src/stories/Button.stories.js */&lt;/span>
&lt;span class="c1">// 导入事件处理
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">action&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;@storybook/addon-actions&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1">// 导入组件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="nx">ZBtn&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;@/components/Btn.vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Vuetify Components/Button&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">component&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">ZBtn&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="c1">// 详细的参数文档
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">argTypes&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;组件颜色，默认为主题色&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">table&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">defaultValue&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">summary&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;primary&amp;#39;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;组件宽度&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">xSmall&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;x-small&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;小尺寸按钮&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="c1">// 定义组件模板
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">Template&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">args&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">argTypes&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">components&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">ZBtn&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">props&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">argTypes&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="nx">template&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;&amp;lt;z-btn v-bind=&amp;#34;$props&amp;#34; @click=&amp;#34;clickBtn&amp;#34;&amp;gt;按钮&amp;lt;/z-btn&amp;gt;&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">methods&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">clickBtn&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">action&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">)}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="c1">// 导出默认的组件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">Default&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Template&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bind&lt;/span>&lt;span class="p">({});&lt;/span>
&lt;span class="c1">// 添加不同风格的组件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">SizeBtn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">components&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">ZBtn&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">template&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`&amp;lt;v-row align=&amp;#34;center&amp;#34;&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn x-small @click=&amp;#34;click&amp;#34;&amp;gt;x-small&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn small&amp;gt;small&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn&amp;gt;default&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn large&amp;gt;large&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn x-large&amp;gt;x-large&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn disabled x-large&amp;gt;x-large&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;/v-row&amp;gt;`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">methods&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">click&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">action&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">)}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">IconBtn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">components&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">ZBtn&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">template&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`&amp;lt;v-row align=&amp;#34;center&amp;#34;&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn icon x-small&amp;gt;&amp;lt;v-icon&amp;gt;mdi-plus&amp;lt;/v-icon&amp;gt;&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn icon small&amp;gt;&amp;lt;v-icon&amp;gt;mdi-plus&amp;lt;/v-icon&amp;gt;&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn icon&amp;gt;&amp;lt;v-icon&amp;gt;mdi-plus&amp;lt;/v-icon&amp;gt;&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn icon large&amp;gt; &amp;lt;v-icon&amp;gt;mdi-plus&amp;lt;/v-icon&amp;gt; &amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn icon x-large&amp;gt;&amp;lt;v-icon&amp;gt;mdi-plus&amp;lt;/v-icon&amp;gt;&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;v-col&amp;gt; &amp;lt;z-btn disabled icon x-large&amp;gt;&amp;lt;v-icon&amp;gt;mdi-plus&amp;lt;/v-icon&amp;gt;&amp;lt;/z-btn&amp;gt; &amp;lt;/v-col&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;/v-row&amp;gt;`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">methods&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">click&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">action&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">)},&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这时会发现用于预览组件的容器太高了，没有自适应组件的高度，原因是 &lt;code>.storybook/preview.js&lt;/code> 文件中用于包裹组件的 &lt;code>&amp;lt;v-app&amp;gt;&amp;lt;/v-app&amp;gt;&lt;/code> 有最低高度 100vh 的默认样式，修改之：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-scss" data-lang="scss">&lt;span class="cm">/* 文件位置：src/assets/sass/main.js */&lt;/span>
&lt;span class="c1">// 取消最低高度
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nc">.v-application--wrap&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">min-height&lt;/span>&lt;span class="nd">:&lt;/span> &lt;span class="nt">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="c1">// 取消按钮英文大写
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nc">.v-btn&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">text-transform&lt;/span>&lt;span class="nd">:&lt;/span> &lt;span class="nt">none&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="cm">/* 文件位置：src/plugins/vuetify.js */&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="kr">import&lt;/span> &lt;span class="s1">&amp;#39;@/assets/sass/main.scss&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后，输入 &lt;code>$ npm run storybook&lt;/code> 启动 Storybook 欣赏你的组件故事吧🎉🎉：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/storybook:button.gif" alt="button.gif" title="Button 组件文档">&lt;/p>
&lt;h2 id="结语">结语&lt;/h2>
&lt;p>完成组件和故事的编写后可将 Storybook 构建成静态 Web 应用程序来&lt;a href="https://storybook.js.org/docs/vue/workflows/publish-storybook">发布&lt;/a>，以供团队之间共享，还可以利用 GitLab CI/CD 为其设置自动构建及部署，总之，它能做的真的很多，网上也不乏很多&lt;a href="https://www.wix-style-react.com/?path=/story/introduction-getting-started--getting-started">优质的组件文档&lt;/a>值得我们学习。&lt;/p>
&lt;p>写到这里发现篇幅拉的有些大了，而创建 NPM 私有仓库、将组件发布为 Package 以及组件的使用要说的还不少😌……也罢，那就且听下回分解。本文案例放在了我的 GitHub 以供参考：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/Xuezenghuigithub/my-storybook">Xuezenghuigithub/my-storybook&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://morphatic.com/2020/09/30/configuring-storybook-6-for-vue-2-vuetify-2-3/">Configuring Storybook 6 for Vue 2 + Vuetify 2.3 | Morgan Benton&lt;/a>&lt;/li>
&lt;/ol>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>&amp;quot;The 6.0 release retools Storybook for professional frontend developers.&amp;quot; ——&lt;a href="https://medium.com/storybookjs/storybook-6-0-1e14a2071000">Storybook 6.0&lt;/a> &lt;a href="https://xuezenghui.com/posts/component-based-development/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/vue.js/">Vue.js</category><category domain="https://xuezenghui.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/">前端工程化</category><category domain="https://xuezenghui.com/tags/storybook/">Storybook</category></item><item><title>前端工程化之 CI/CD</title><link>https://xuezenghui.com/posts/gitlab-ci-cd/</link><guid isPermaLink="true">https://xuezenghui.com/posts/gitlab-ci-cd/</guid><pubDate>Tue, 17 Nov 2020 15:20:01 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>在项目开展的过程中，我经常做功能分支-测试分支-部署分支之间的代码合并工作，虽然说我们团队有着合理的 Git Flow、Commit 规范，但确保不了每个开发人员在每次提交的时候都严格遵守，且在分支复杂、提交时间节点不确定的情况下，合并分支时产生冲突、处理冲突似乎成了不可避免的事，从开发的整个生命周期来看对产出效率的影响是不小的。&lt;/p>
&lt;p>「工程化」还要考量的一个角度便是..生产效率..，也就是「自动化」，任何重复次数多于两次的简单机械劳动都应该交给机器来完成，这也就是前端工程化中“自动化”的过程，当然，不止前端了，任何需要频繁交付应用的团队都希望有一个工具、一种方法能应对“&lt;a href="https://www.solutionsiq.com/agile-glossary/integration-hell/">集成地狱&lt;/a>”和机械化的部署工作。&lt;/p>
&lt;h2 id="cicd-概要">CI/CD 概要&lt;/h2>
&lt;h3 id="ci">CI&lt;/h3>
&lt;p>Continuous Integration，持续集成。集成这个动作指软件开发过程中合并代码的过程，而持续集成指的就是我们需要的自动化的过程——在开发人员提交新代码后，自动进行构建、测试并合并代码到代码仓库中。&lt;/p>
&lt;p>&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/gitlab-ci-cd/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;img src="https://xuezenghui.com/images/gitlab-ci-cd_ci.png" alt="ci.png" title="持续集成">&lt;/p>
&lt;h3 id="cd">CD&lt;/h3>
&lt;p>CD 有两种含义，Continuous Delivery 持续交付和 Continuous Deployment 持续部署。持续交付指在完成应用的测试、集成和构建后因为团队要求或业务要求，自动将应用部署到..准生产环境..，能部署到准生产环境也就意味着可以直接部署到正式的生产环境，所以，&lt;strong>持续交付意味着任何的代码修改可以在任何想要部署的时候实施部署，表示一种能力&lt;/strong>。&lt;/p>
&lt;p>而&lt;strong>持续部署则是一种实践，一个最终动作&lt;/strong>，表示在通过前面的所有阶段后自动将改动投入生产环境。不同的团队对持续交付和持续部署有一定要求，比如我司要求在部署到生产环境前先部署到准生产环境进一步测试。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab-ci-cd_cd.png" alt="cd.png" title="持续部署">&lt;/p>
&lt;h2 id="gitlab-cicd-实践">GitLab CI/CD 实践&lt;/h2>
&lt;p>CI/CD 是 GitLab 的内置工具，不需要第三方工具就可以拿来即用。GitLab CI/CD 会根据用户创建的特定 YAML 文件使用 GitLab Runner 自动执行脚本，文件中的脚本即是我们需要执行的操作，如添加依赖项、构建、单元测试等等。&lt;/p>
&lt;h3 id="配置-gitlab-runner">配置 GitLab Runner&lt;/h3>
&lt;p>存放脚本的 YAML 文件是关键，但首先需要有执行脚本的工具 &lt;a href="https://docs.gitlab.com/runner/">GitLab Runner&lt;/a>，每个要实行 CI/CD 的项目都必须指定一个特定的 Runner，Runner 是一个使用 Go 语言编写的类似终端的工具，可以运行在 Linux、Docker、maocOS 等众多环境中，可以选择配置三种不同类型的 Runner：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Specific Runners&lt;/strong>：项目特定的 Runner，适用于需要执行部署作业等有特定要求的项目&lt;/li>
&lt;li>&lt;strong>Shared Runners&lt;/strong>：由 GitLab 管理员安装、注册的可用于所有项目的 Runner&lt;/li>
&lt;li>&lt;strong>Group Runners&lt;/strong>：同样由 GitLab 管理员来安装，不同的是可以给特定的 Group 设置特定的 Runner&lt;/li>
&lt;/ul>
&lt;p>因为需要进行持续&lt;strong>部署&lt;/strong>，部署的目标服务器需要和 GitLab 中的代码仓库通信，符合“需要执行有特定要求的项目”，先来创建一个 Specific Runners。此处以部署 Vue 项目为例，在云服务器（Ubuntu 18.04.5）上要做..安装..和..注册..两个步骤，注册即将安装的 Runner 与 GitLab 绑定以使其可通过 API 通信：&lt;/p>
&lt;p>&lt;strong>1. 添加 GitLab 官方存储库&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">curl&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">L&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">packages.gitlab.com&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">install&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">repositories&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">runner&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">gitlab&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">runner&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">script.deb.sh&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">bash&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 安装 GitLab Runner&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">export&lt;/span> &lt;span class="n">GITLAB_RUNNER_DISABLE_SKEL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">true&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">E&lt;/span> &lt;span class="n">apt&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">get&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">gitlab&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">runner&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 获取 URL 和 token&lt;/strong>&lt;/p>
&lt;p>进入要添加 CI/CD 的 GitLab 项目的 &lt;code>Setting&lt;/code> ➡️ &lt;code>CI/CD&lt;/code> 页面（前提是你要有该项目的 Maintainer 权限），展开 Runners 面板，在 Specific Runners 找到 &lt;strong>GitLab URL&lt;/strong> 和 &lt;strong>token&lt;/strong>。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab-ci-cd_runner-token.png" alt="runner-token.png" title="Runner Token">&lt;/p>
&lt;p>&lt;strong>3. 注册 Runner&lt;/strong>&lt;/p>
&lt;p>在 Ubuntu 中运行 &lt;code>$ sudo gitlab-runner register&lt;/code>，按照指令依次输入 GitLab URL、token、描述、标签、执行者（本例输入 &lt;code>shell&lt;/code>）&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/gitlab-ci-cd/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>。&lt;/p>
&lt;p>注册完成后在 Runners 面板下即可看到绑定成功的 Specific Runner：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab-ci-cd_runner.png" alt="runner.png" title="添加成功的 Runner">&lt;/p>
&lt;blockquote>
&lt;p>更换 Runner 后需执行 &lt;code>$ sudo gitlab-runner restart&lt;/code> 重启以生效。&lt;/p>
&lt;/blockquote>
&lt;h3 id="编写脚本文件">编写脚本文件&lt;/h3>
&lt;p>&lt;code>.gitlab-ci.yml&lt;/code> 文件需要在项目根目录下手动创建，这个 YAML 文件中的配置叫做 GitLab CI/CD 的&lt;strong>管道 Pipeline&lt;/strong>，每当代码库更新时，GitLab 都会先按照管道中的内容在 Runner 上执行相应的作业 Jobs。Jobs 是 &lt;code>.gitlab-ci.yml&lt;/code> 文件中的最基本单元，用来定义执行一个操作的条件和位于 &lt;code>script&lt;/code> 子句内的操作具体内容，如：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yml" data-lang="yml">&lt;span class="k">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 显式声明各阶段，并指定执行顺序&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- job1&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- ...&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">job1&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 自定义的 Job 名&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>test&lt;span class="w"> &lt;/span>&lt;span class="c"># 当前 Job 的阶段&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 由 Runner 执行的 Shell 脚本&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- echo&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Hello, Zander!&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;code>stage&lt;/code> 可以决定各个 Jobs 的执行顺序——同一阶段的 Jobs 并行运行，不同阶段的 Jobs 依照书写顺序执行，如果前面的 Job 失败，则将提交标记为 &lt;code>failed&lt;/code> 并且不执行后续的 Job.。不止 &lt;code>stage&lt;/code> 和 &lt;code>script&lt;/code>，一个 Job 内可用的参数见 &lt;a href="https://docs.gitlab.com/ee/ci/yaml/#job-keywords">Job keywords&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;p>在项目根目录下新建（最好是在本地工作区创建而不是线上，因为本地提交更改 push 后会直接运行管道操作，方便查看效果）&lt;code>.gitlab-ci.yml&lt;/code> 文件，编写脚本：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yml" data-lang="yml">&lt;span class="k">stages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- build&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- deploy&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">build_site&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>build&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 打印内容&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- echo&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Start build&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 安装依赖&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- rm&lt;span class="w"> &lt;/span>-fr&lt;span class="w"> &lt;/span>./node_modules&lt;span class="w"> &lt;/span>&lt;span class="cp">&amp;amp;&amp;amp;&lt;/span>&lt;span class="w"> &lt;/span>npm&lt;span class="w"> &lt;/span>install&lt;span class="w"> &lt;/span>--registry=https&lt;span class="p">:&lt;/span>//registry.npm.taobao.org&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 打包构建&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- npm&lt;span class="w"> &lt;/span>run&lt;span class="w"> &lt;/span>build&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- echo&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Build done&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">only&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- master&lt;span class="w"> &lt;/span>&lt;span class="c"># 只在 master 分支代码发生变化时才执行该 Job&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- test&lt;span class="w"> &lt;/span>&lt;span class="c"># 指定 Runner 的 tag（注册 Runner 时指定的）&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">artifacts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="k">expire_in&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w"> &lt;/span>week&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="k">paths&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- dist/&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">deploy_site&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>deploy&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- echo&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Start deploy&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 将构建好后的 dist 目录拷贝到 NGINX 的挂载目录上&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- rm&lt;span class="w"> &lt;/span>-rf&lt;span class="w"> &lt;/span>/var/www/cicd-test&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- cp&lt;span class="w"> &lt;/span>-r&lt;span class="w"> &lt;/span>./dist&lt;span class="w"> &lt;/span>/var/www/cicd-test&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">only&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- master&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">dependencies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- build_site&lt;span class="w"> &lt;/span>&lt;span class="c"># 接收 build_site 传递的产物&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- test&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>按思路，build 阶段应执行打包产生一个 dist 目录，再在接下来的 deploy 阶段挂载到 NGINX 的目录上就完成了项目部署，但管道的设定是每个 Job 启动时都会同步仓库中的 &lt;code>.gitignore&lt;/code> 文件并自动删除其中声明的文件——dist 目录在 deploy 阶段开始的时候就没了。&lt;/p>
&lt;p>所以脚本文件中需要使用到另一个参数 &lt;a href="https://docs.gitlab.com/ee/ci/yaml/README.html#artifacts">&lt;code>artifacts&lt;/code>&lt;/a>，我将它译为“&lt;strong>产物&lt;/strong>”，表示&lt;strong>在当前 Job 执行成功/失败后可以将指定的文件或目录保存到 GitLab&lt;/strong>，这样就可以在下面的 Jobs 中使用了，上面还指定了产物的过期时间。dependencies 用于在当前的 Job 中接收前面的产物，默认前面的所有 Jobs 里的产物都会往后传递，设为空数组则不接收任何产物。&lt;/p>
&lt;p>push 之前还需要设定一下 &lt;code>/var/www&lt;/code> 目录的权限，该目录默认属于 root 用户，而 Runner 运行时的用户是 gitlab-runner，权限交接：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">chown&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">hR&lt;/span> &lt;span class="n">gitlab&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">runner&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">gitlab&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">runner&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">www&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>推送更改后，在项目的 CI/CD 面板中可查看管道和 Jobs 的详细情况：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab-ci-cd_gitlab-ui.png" alt="gitlab-ui.png" title="CI/CD 面板">&lt;/p>
&lt;p>点击单独的 Job 也可查看详细的执行情况，UI 上的操作就不多介绍了，当管道内所有的阶段都执行完毕后，Vue 项目即部署成功：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab-ci-cd_deploy-success.png" alt="deploy-success.png" title="部署成功">&lt;/p>
&lt;p>还有一点值得一提，我在将新的 dist 目录拷贝到 NGINX 挂载目录时先是用的 &lt;code>cp&lt;/code> 命令结合 &lt;code>-fr&lt;/code> 参数，表示复制并..覆盖..原有的目录。出现的问题是每次管道运行完成后都不能及时地更新应用的内容，后&lt;a href="https://cloud.tencent.com/developer/article/1179348">查询得知&lt;/a>是由 &lt;code>cp&lt;/code> 命令的底层原理导致的，简单来说就是 &lt;strong>&lt;code>cp -fr&lt;/code> 并不能保证安全地替换原有文件&lt;/strong>，遂使用了 &lt;code>rm&lt;/code> + &lt;code>cp&lt;/code> 命令，一切正常🎉。&lt;/p>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://docs.gitlab.com/ee/ci/">GitLab CI/CD | GitLab&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.redhat.com/zh/topics/devops/what-is-ci-cd">CI/CD是什么？如何理解持续集成、持续交付和持续部署 | Red Hat&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.jjonline.cn/linux/238.html">给产品经理讲讲，什么是持续交付和DevOps | acejoy&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.logrocket.com/how-to-auto-deploy-a-vue-application-using-gitlab-ci-cd-on-ubuntu/">How to auto deploy a Vue application using GitLab CI/CD on Ubuntu 18.04 | Michael Okoh&lt;/a>&lt;/li>
&lt;/ol>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>图源 &lt;a href="https://www.mindtheproduct.com/what-the-hell-are-ci-cd-and-devops-a-cheatsheet-for-the-rest-of-us/">https://www.mindtheproduct.com/what-the-hell-are-ci-cd-and-devops-a-cheatsheet-for-the-rest-of-us/&lt;/a> &lt;a href="https://xuezenghui.com/posts/gitlab-ci-cd/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>执行者可以决定 Runner 执行器可以进行的操作，详见 &lt;a href="https://docs.gitlab.com/runner/executors/README.html">Executors&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/gitlab-ci-cd/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/gitlab/">GitLab</category><category domain="https://xuezenghui.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/">前端工程化</category><category domain="https://xuezenghui.com/tags/ci/cd/">CI/CD</category></item><item><title>前端工程化之定制 Vue CLI</title><link>https://xuezenghui.com/posts/custom-vue-cli/</link><guid isPermaLink="true">https://xuezenghui.com/posts/custom-vue-cli/</guid><pubDate>Tue, 22 Sep 2020 15:12:22 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>想来也算来有一段时间没有更博了，总是在有一些想法的时候被各种乱七八糟的事情打乱，具体说来话长了，不如这样总结吧——..职能有变..， ..有好有坏..。虽然能用来自我支配的时间变少了，但也不算毫无收获，列出几条最近做的还算系统性的工作：&lt;/p>
&lt;ol>
&lt;li>编写产出团队开发协作规范&lt;/li>
&lt;li>关于 UI 零件化的思考及简报分享&lt;/li>
&lt;li>抽离封装前端共用组件，发布至 &lt;a href="http://eportal-guide.belstar.com.cn/">Storybook&lt;/a>&lt;/li>
&lt;li>定制适用于团队的 Vue CLI&lt;/li>
&lt;/ol>
&lt;p>之所以说这几件事比较系统化是因为我统一把它们归在了&lt;strong>前端工程化&lt;/strong>这一类别中，目的都是“统一开发模式，简化开发过程”。当然，工程化这一议程和工作并不是一蹴而就的，工程化过程中产出成果是一回事，推广整个团队遵循各 Guidelines 又是另外一回事了。索性过程中是有一些比较有意义的成果可以拿出来记录分享的，比如本文要详述的&lt;strong>如何定制适用于自己/团队的 Vue CLI&lt;/strong>。&lt;/p>
&lt;hr>
&lt;p>&lt;a href="https://cli.vuejs.org/zh/">Vue CLI&lt;/a> 一定都不陌生了，Vue 官方提供的脚手架工具，是一套包含集成、构建、插件配置、图形化等功能的完整工具集。官方脚手架的目的是更轻量更简单，追求使用的..广度..，也就是要适用更多的开发者，当面临下面问题时就稍显无力，需要开发者做更多的工作，甚至是反复的更多工作：&lt;/p>
&lt;ol>
&lt;li>团队对项目目录结构有严格的规范要求&lt;/li>
&lt;li>定制化要求比较高，如需要使用特定的 UI 框架及工具类库&lt;/li>
&lt;li>项目配置文件较多，如 ESLint 配置项、Apollo 配置项等&lt;/li>
&lt;li>项目内包含预定义的共用组件&lt;/li>
&lt;li>懒……&lt;/li>
&lt;/ol>
&lt;h2 id="定制-vue-cli">定制 Vue CLI&lt;/h2>
&lt;p>定制化的 Vue CLI 是基于 Vue CLI 的，所以开展之前应确保本地安装了 Vue CLI 并可以使用 &lt;code>$ vue create &amp;lt;porect_name&amp;gt;&lt;/code> 命令成功创建项目。实现的原理是 &lt;a href="https://cli.vuejs.org/zh/guide/plugins-and-presets.html#preset">Vue CLI preset&lt;/a>，即在创建新项目时使用预定义的配置和要用到的插件，而这些预定义的内容支持放在 Git 上（包括 GitHub、GitLab 等），使用远程仓库中的 Preset 创建 Vue 项目时需要加入特殊的选项：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="c1"># GitHub&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">vue&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">preset&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="o">&amp;gt;/&amp;lt;&lt;/span>&lt;span class="n">repo&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">vue_project_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;span class="c1"># GitLab 私有服务器&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">vue&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">preset&lt;/span> &lt;span class="n">gitlab&lt;/span>&lt;span class="o">:&amp;lt;&lt;/span>&lt;span class="n">my&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">gitlab&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">server.com&lt;/span>&lt;span class="o">&amp;gt;:&amp;lt;&lt;/span>&lt;span class="n">group&lt;/span>&lt;span class="o">&amp;gt;/&amp;lt;&lt;/span>&lt;span class="n">project_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">clone&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">vue_project_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="初始化-preset-目录结构">初始化 Preset 目录结构&lt;/h3>
&lt;p>以 GitLab 为例，创建一个新的 Project 后 clone 到本地，第一步需要初始化项目的目录结构，一般 Preset 由以下四个部分组成，当然加上 README 更好了：&lt;/p>
&lt;pre>&lt;code>.
├─ template/
│ └─ ...
├─ generator.js
├─ preset.json
├─ prompts.js
└─ README.md
&lt;/code>&lt;/pre>&lt;blockquote>
&lt;p>开发的时候需要频繁地执行创建项目的命令来测试改动，使用 Git 仓库无疑太麻烦了，Vue CLI 也支持根据本地的文件来创建项目：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-shell" data-lang="shell">$ vue create --preset ./&amp;lt;my_preset&amp;gt; &amp;lt;project_name&amp;gt;
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/blockquote>
&lt;h3 id="添加预定义文件">添加预定义文件&lt;/h3>
&lt;p>preset.json 是使用 &lt;code>$ vue create&lt;/code> 命令时自动生成的预定义选项的 JSON 文件，MacOS 中位于 &lt;code>~/.vuerc&lt;/code>。文件里定义的内容在命令行中不进行交互提示，比如执行 &lt;code>$ rm ~/.vuerc&lt;/code> 删除该文件后执行 &lt;code>$ vue create hello&lt;/code> 会提示是否使用淘宝的 NPM 镜像提升下载速度及使用 Yarn or NPM 作为包管理工具，创建完项目后查看 .vuerc 文件的内容会显示选择的内容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;useTaobaoRegistry&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;packageManager&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;yarn&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>之后创建项目时就会采用该预定义项，不再进行提示。所以可根据需求来在 preset.json 中定义需要添加哪些插件，以及一些简单的配置项：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;useConfigFiles&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;cssPreprocessor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sass&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;plugins&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;@vue/cli-plugin-babel&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;span class="nt">&amp;#34;@vue/cli-plugin-router&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;historyMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nt">&amp;#34;@vue/cli-plugin-vuex&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;span class="nt">&amp;#34;@vue/cli-plugin-eslint&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;prettier&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;lintOn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span> &lt;span class="s2">&amp;#34;save&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;commit&amp;#34;&lt;/span> &lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="定义模板文件">定义模板文件&lt;/h3>
&lt;p>&lt;code>template/&lt;/code> 目录用来定义使用脚手架创建 Vue 项目后生成的目录结构，即模板，比如我们团队 Guideline 要求的目录是这样的：&lt;/p>
&lt;pre>&lt;code>.
├── build/ # 生成压缩包
├── public/ # 静态资源，不需要 webpack 处理
│ ├── favicon.ico
│ └── index.html
└── src/
│ ├── assets/
│ │ ├── fonts/ # 字体文件
│ │ ├── images/ # 图片文件
│ │ └── styles/ # reset 样式及定义的常量文件
│ ├── components/ # 共用组件
│ │ └── base/ # 全局注册组件
│ ├── layout/ # 整体布局组件
│ ├── plugins/ # 插件文件
│ ├── filters/ # 全局过滤器
│ ├── router/ # 路由及拦截器
│ ├── store/ # 管理 vuex 全局状态
│ ├── utils/ # 工具类函数
│ ├── views/ # Vue 页面
│ │ └── Home.vue
│ ├── App.vue
│ └── main.js
├── .eslintrc.js # ESLint 配置
├── .gitignore # Git 忽略文件配置
├── .prettierrc.js # Prettier 代码风格配置
├── babel.config.js # Babel 配置
├── vue.config.js # Webpack 配置
└── README.md
&lt;/code>&lt;/pre>&lt;p>可以先简单地将 template 下定义的目录和需要的目录保持一致，但有几点需要注意：&lt;/p>
&lt;ol>
&lt;li>如果模板中没有定义 Vue CLI 本身就会生成的文件，则默认采用原来的，如 &lt;code>view/&lt;/code> 目录下的 Home.vue 和 About.vue 等&lt;/li>
&lt;li>以 &lt;code>.&lt;/code> 开头的模板文件需要将 &lt;code>.&lt;/code> 改为 &lt;code>_&lt;/code>，以 &lt;code>_&lt;/code> 开头的模板文件要定义成 &lt;code>__&lt;/code>，否则无法正确渲染&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/custom-vue-cli/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/li>
&lt;li>空文件夹不会被正确渲染&lt;/li>
&lt;/ol>
&lt;p>还有一处踩的坑耗费了不少时间，因为我们项目中添加并配置了 Vuetify，需要在 main.js 中引入并注册到 Vue 实例中，但如果将整个文件替换为新的内容，会报错 router、store 等..重复定义..。经验证发现&lt;strong>在 preset.json 中预定义的插件，不需要在模板下的 main.js 中自行导入和注册&lt;/strong>，只需要添加需要的内容即可，如：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 路径：template/src/main.js
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">App&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./App.vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">config&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">productionTip&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">vuetify&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./plugins/vuetify&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">new&lt;/span> &lt;span class="nx">Vue&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">vuetify&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">render&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">h&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">h&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="p">}).&lt;/span>&lt;span class="nx">$mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;#app&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>因为 preset.json 中预定义了 vue-router 和 vuex，最终生成的 main.js 是完整的：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">App&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./App.vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">store&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./store&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">config&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">productionTip&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">vuetify&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./plugins/vuetify&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">router&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./router&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">new&lt;/span> &lt;span class="nx">Vue&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">store&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">vuetify&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">router&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">render&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">h&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">}).&lt;/span>&lt;span class="nx">$mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;#app&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="扩展依赖包">扩展依赖包&lt;/h3>
&lt;p>genarator.js 文件用来为项目添加其它依赖，比如 UI 框架、工具类库等等，渲染 template 模板的操作也需要在该文件内完成。该文件需要导出一个函数，包含三个参数：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>api：&lt;a href="https://cli.vuejs.org/dev-guide/generator-api.html">generator 实例&lt;/a>，函数中可以操作该实例，比如扩展依赖、检查插件、查看版本等&lt;/p>
&lt;/li>
&lt;li>
&lt;p>options：定制 Vue CLI 时与交互式命令行结合使用，用来接收答案参数&lt;/p>
&lt;/li>
&lt;li>
&lt;p>rootOptions：预定义的所有内容，也就是 preset.json 中的所有内容，并且包含项目名称、src/main.js 中配置的说明等：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="p">{&lt;/span>
&lt;span class="nx">projectName&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;my-vue&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">useConfigFiles&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">cssPreprocessor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;sass&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">plugins&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s1">&amp;#39;@vue/cli-plugin-babel&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;span class="s1">&amp;#39;@vue/cli-plugin-router&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">historyMode&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="s1">&amp;#39;@vue/cli-plugin-vuex&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;span class="s1">&amp;#39;@vue/cli-plugin-eslint&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">config&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;prettier&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">lintOn&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nb">Array&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="s1">&amp;#39;/Users/zander/Desktop/zander-vue-cli&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">_isPreset&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">prompts&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="s1">&amp;#39;src/main.js&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s1">&amp;#39;router&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;store&amp;#39;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>1. 添加依赖项&lt;/strong>&lt;/p>
&lt;p>为项目添加脚本和依赖项需要使用 generator 实例的 &lt;a href="https://cli.vuejs.org/dev-guide/generator-api.html#extendpackage">&lt;code>extendPackage()&lt;/code>&lt;/a> 方法，内容和 package.json 无异，根据依赖的类型声明 NPM 包名和版本即可，创建项目时会自行安装声明的依赖项：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">api&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">options&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">rootOptions&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">extendPackage&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">scripts&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;serve&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;vue-cli-service serve&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;build&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;vue-cli-service build&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;lint&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;vue-cli-service lint&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">dependencies&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;vue&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^2.6.11&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;vuetify&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^2.3.2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;@mdi/font&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^5.5.55&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">devDependencies&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;@babel/core&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^7.11.4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;@babel/preset-env&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^7.11.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;@vue/cli-service&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;~4.5.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;vue-cli-plugin-vuetify&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^2.0.5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;vuetify-loader&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^1.3.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;sass&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^1.26.10&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;sass-loader&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^8.0.2&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 渲染模板&lt;/strong>&lt;/p>
&lt;p>使用 &lt;a href="https://cli.vuejs.org/dev-guide/generator-api.html#render">&lt;code>render()&lt;/code>&lt;/a> 方法来渲染 template 中定义的模板，该方法实际使用 EJS 进行渲染，可以传入一个相对路径的字符串，会将原本的目录直接替换。也可以传入 Hash 对象，文件对应文件来渲染（不能是文件夹），写多个 &lt;code>rander()&lt;/code> 的话会依次执行：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js"> &lt;span class="nx">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;./template&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">render&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s1">&amp;#39;./.eslintrc.js&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;./template/_eslintrc.js&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s1">&amp;#39;./.gitignore&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;./template/_gitignore&amp;#39;&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 修改主文件&lt;/strong>&lt;/p>
&lt;p>使用 API 同样可以实现对主文件 main.js 内容的修改，但……实在是有些复杂呢🤪，感兴趣的可参考&lt;a href="https://cli.vuejs.org/zh/dev-guide/plugin-dev.html#%E4%BF%AE%E6%94%B9%E4%B8%BB%E6%96%87%E4%BB%B6">官方文档&lt;/a>，个人更推荐按上面的方式在模板里更改或继续往下看使用 EJS 的语法更改🎉。&lt;/p>
&lt;h3 id="交互式命令行">交互式命令行&lt;/h3>
&lt;p>很多命令行操作都涉及对话的情境，比如 Git 操作、各种 CLI 操作，看起来比较 Geek，实现原理是 Node.js 的交互式命令行 &lt;a href="https://github.com/SBoudrias/Inquirer.js">Inquirer.js&lt;/a>。要想自定义 Vue CLI 的对话内容需要用到 prompts.js 文件，该文件内应导出一个与 &lt;code>inquirer.prompt()&lt;/code> 参数相同数据结构的..数组..，数组内每一个对象都作为一个命令行中的问题[^3]：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;confirm&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 问题类型
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;vuex&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 存储答案的 key
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">message&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;是否使用 vuex 进行全局状态管理?&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 问题的内容
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">default&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 未选择时的默认值
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">choices&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span> &lt;span class="c1">// 可选项
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;是&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 选项
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="c1">// 选项对应的值
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;否&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 选项
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span> &lt;span class="c1">// 选项对应的值
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在命令行创建项目时 Invoking 阶段会进行交互式的对话：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/vue-cli:prompt.png" alt="prompt.png" title="交互式命令行">&lt;/p>
&lt;p>当然，完成对话后应该按照不同的答案执行不同的操作，上面 genarator.js 文件中的函数的 &lt;code>options&lt;/code> 参数就起到作用了，可以在函数中打印 &lt;code>options&lt;/code>，答案以 Key-value 的形式保存在一个对象内：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span> &lt;span class="err">vuex:&lt;/span> &lt;span class="err">true&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>那么就可以根据该对象结合 generator 实例的各方法来操作了：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">options&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vuex&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">extendPackage&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="c1">// 添加依赖
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">dependencies&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;vuex&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;^3.4.0&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="nx">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">render&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;./src/store/index.js&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;./template/store/index.js&amp;#39;&lt;/span>&lt;span class="p">});&lt;/span> &lt;span class="c1">// 渲染 store 文件
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="nx">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">injectImports&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">entryFile&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sb">`import store from &amp;#39;./store&amp;#39;`&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// main.js 中导入
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;span class="nx">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;./template/default&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 默认渲染的内容
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此时虽然使用 &lt;code>injectImports()&lt;/code> 方法导入了 store 到 main.js，但还没有注册到 Vue 实例中。因为 &lt;code>render()&lt;/code> 的实质是通过 EJS 来渲染，所以可以在文件中使用 EJS 的语法实现更细粒度的控制：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 路径：template/default/src/main.js
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">App&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./App.vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">config&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">productionTip&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">vuetify&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./plugins/vuetify&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">new&lt;/span> &lt;span class="nx">Vue&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">vuetify&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="o">&amp;lt;%&lt;/span>&lt;span class="nx">_&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">options&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vuex&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="o">%&amp;gt;&lt;/span>
&lt;span class="nx">store&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="o">&amp;lt;%&lt;/span>&lt;span class="nx">_&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="o">%&amp;gt;&lt;/span>
&lt;span class="nx">render&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">h&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">h&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="p">}).&lt;/span>&lt;span class="nx">$mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;#app&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>当然，能自定义的内容还有很多，比如二次封装 Axios、&lt;del>添加 NPM 私服的依赖&lt;/del>、自定义默认的布局等等，结合以上步骤依据具体情境添加即可。&lt;/p>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://github.com/cklwblove/vue-cli3-template">github.com/cklwblove/vue-cli3-template | GitHub&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://segmentfault.com/a/1190000016389996">如何使用 vue-cli 3 的 preset 打造基于 git repo 的前端项目模板 | SegmentFault&lt;/a>&lt;/li>
&lt;/ol>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>见 &lt;a href="https://cli.vuejs.org/zh/dev-guide/plugin-dev.html#%E6%96%87%E4%BB%B6%E5%90%8D%E7%9A%84%E8%BE%B9%E7%95%8C%E6%83%85%E5%86%B5">Vue CLI | 文件名的边界情况&lt;/a>说明。 &lt;a href="https://xuezenghui.com/posts/custom-vue-cli/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/vue.js/">Vue.js</category><category domain="https://xuezenghui.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/">前端工程化</category></item><item><title>说 Markdown</title><link>https://xuezenghui.com/posts/about-markdown/</link><guid isPermaLink="true">https://xuezenghui.com/posts/about-markdown/</guid><pubDate>Sun, 19 Jul 2020 16:19:39 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>自大学接触前端开始也就慢慢接触了 Markdown，最开始在 &lt;a href="https://blog.csdn.net/Xue_zenghui">CSDN&lt;/a> 上用着默认的富文本编辑器，当时对富文本是没有什么优劣认知的，毕竟不是文本编辑器的深度使用者，转战 Markdown 是因为彼时网课老师的普及和多见的 GitHub 项目 README.md 文件。&lt;/p>
&lt;p>现在，日复一日阅读和使用 Markdown 已一载有余，就来谈谈对 Markdown 的些许看法和使用建议。&lt;/p>
&lt;hr>
&lt;p>&lt;a href="https://daringfireball.net/projects/markdown/">Markdown&lt;/a> 语言在2004年由&lt;a href="https://zh.wikipedia.org/wiki/%E7%B4%84%E7%BF%B0%C2%B7%E6%A0%BC%E9%AD%AF%E4%BC%AF">约翰·格鲁伯(John Gruber)&lt;/a>创建，初衷是希望「使用易于阅读、易于撰写的纯本文格式进行编写，然后可以转换为结构上有效的 XHTML（或 HTML）」。&lt;/p>
&lt;p>Markdown 没有 RTF 格式&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/about-markdown/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>复杂的语法，也没有 HTML 繁琐的 &lt;code>&amp;lt;&amp;gt;&lt;/code> 结构（作为一名前端开发人员，真的厌恶这个符号），不使用任何编译器或编辑器也能很容易地识别要表达的内容。甚至使用过程中会让人感受不到这是一门..语言..、一个..规范..，它的确做到了简洁地让人只专注于创作，仅仅使用键盘多敲几个符号就解决了&lt;strong>基本的&lt;/strong>排版问题。解决了基本的排版问题，但还有些问题是让人不能不提的：&lt;/p>
&lt;p>&lt;strong>1. 图片无法指定大小&lt;/strong>&lt;/p>
&lt;p>众所周知，在 Markdown 中使用 &lt;code>![alt](url)&lt;/code> 语法可以插入图片，但对图片的大小控制完全无力，很多时候都会因为图片问题严重影响文档的美观程度。而要想自行控制图片大小，只能又在 Markdown 文件中使用 HTML 语法—— &lt;code>&amp;lt;image src=&amp;quot;&amp;quot; width=&amp;quot;&amp;quot;&amp;gt;&lt;/code>，这不就又偏离初衷迷失本意了吗？&lt;/p>
&lt;p>&lt;strong>2. 过渡依赖空格、缩进和空行进行排版&lt;/strong>&lt;/p>
&lt;p>虽然每次编写时也都不会忘记在 Markdown 符号后追加空格，但缩进和空行可就说不准了。在程序员编写 md 时会经常在列表下编辑代码段，就像这样：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>代码示例：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Hello World&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>书写这样的格式要求代码段必须与上面的列表采用&lt;strong>空行间隔 + 缩进&lt;/strong>，缩进的多少对排版格式也有着莫大的影响（上面为缩进四个空格的结果），规范的不明确让人又回头思索起了如何排版的问题🤯。&lt;/p>
&lt;p>其次，对空行的要求很高，少敲一个回车会导致排版混乱，多敲一个又会导致空行极多，往往一篇文章的空行数量有近..三分之一..，而这又是不可避免的……&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/markdown:br-error.png" alt="br-error.png" title="缺少空行导致排版混乱">&lt;/p>
&lt;p>还有一个不是 Markdown 的锅但同样让人头大的问题，书写中文文章时需要频繁地中英文切换，全角半角符号经常性切换不及，写错了还不易发现。&lt;/p>
&lt;hr>
&lt;blockquote>
&lt;p>说实话不太理解和认同 Gruber 拒绝标准化的想法和做法。&lt;/p>
&lt;/blockquote>
&lt;p>接下来推荐两个笔者常用的 Markdown 编辑器吧：&lt;/p>
&lt;p>&lt;strong>1. &lt;a href="https://macdown.uranusjr.com/">Macdown&lt;/a>&lt;/strong>&lt;/p>
&lt;p>一款 MacOS 上开源的 Markdown 编辑器，优点是界面简洁，..所见即所得..的编写模式，支持 LaTeX 公式、Mermaid，也支持图片以 Base64 拖入并显示于文档（奉劝不要尝试这种粗鲁的做法哈哈哈哈）。缺点是文档内容多了时上下滚屏会卡顿，还有如上图所示，对空行的要求较严格。&lt;/p>
&lt;p>&lt;strong>2. &lt;a href="https://typora.io/">Typora&lt;/a>&lt;/strong>&lt;/p>
&lt;p>开源，支持 MacOS、Windows 和 Linux 三端，..实时预览..模式，同样支持 LaTeX 公式、Mermaid，还有文件目录、文档目录等功能，但这都不是最重要的，它内置的一款主题 Newsprint 实在是深得我意，与本博客的主题不谋而合，让人能极其舒适地码字：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/markdown:typora.png" alt="typora.png" title="Typora Newsprint 主题">&lt;/p>
&lt;p>Typora 还有效避免了缺少空行导致的排版问题，它会在隔离的 Markdown 符号上下都自动加入空行，但这种处理方式又导致要实现上面的「列表下编辑代码段」会特麻烦……总之还是上面总结的几点 Markdown 语言的问题导致的。&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>Rich Text Format，富文本格式。 &lt;a href="https://xuezenghui.com/posts/about-markdown/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/markdown/">markdown</category></item><item><title>不会吧？不会吧？不会有人还搞不懂 Vue 双向数据绑定原理吧？</title><link>https://xuezenghui.com/posts/principle-of-vue-two-way-data-binding/</link><guid isPermaLink="true">https://xuezenghui.com/posts/principle-of-vue-two-way-data-binding/</guid><pubDate>Tue, 14 Jul 2020 09:04:24 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;blockquote>
&lt;p>我猜测这篇文章的访问量在博客 Analytics 的页面访问量榜单里应该是列位不低的，因为——我标题党了😳。&lt;/p>
&lt;/blockquote>
&lt;p>双向数据绑定一直被认为是各 MVVM 框架的核心特性，在开发中也是无时不刻都在使用。而我一直是作为一个看客去了解别人博文中的 Vue 双向数据绑定，没有自己去深究并手动实现过（反省）。这篇文章，我将从..设计模式..、 ..技术原理..、 ..原生实现..等多个方面去解析 Vue 中的双向数据绑定，当然了，在 Vue3 发布之际，也应了解下 Vue3 下的双向数据绑定。&lt;/p>
&lt;h2 id="概述">概述&lt;/h2>
&lt;p>在 MVVM 架构下，数据层 Model 和视图层 View 通过 View Model 层进行连接和交互，这个 ViewModel 即为连接视图和数据的桥梁，作数据和逻辑处理工作，指的正是&lt;strong>双向数据绑定&lt;/strong>。&lt;/p>
&lt;p>双向数据绑定实现的效果就是在更新 JavaScript 中的数据（定义的变量）时同步修改视图（DOM 节点），修改视图时也同步回数据层：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/two-way-data-binding:show.gif" alt="show.gif" title="双向数据绑定演示">&lt;/p>
&lt;p>以一个开发者的角度应该不难想到，视图到数据方向的改动需要监听 DOM 的变化再同步赋值给 JavaScript 的变量，比如给 &lt;code>&amp;lt;input&amp;gt;&lt;/code> 添加 &lt;code>change&lt;/code> 或 &lt;code>input&lt;/code> 监听事件并在事件处理函数中给变量赋值，实际这也正是 &lt;code>v-model&lt;/code> 指令做的很重要的一件事。&lt;/p>
&lt;p>另一个方向，关键词还是「监听」——监听 JavaScript 数据的变化，再去相应更新 DOM，此处的监听及处理就是 Vue 的双向数据绑定实现方式——&lt;strong>发布订阅模式&lt;/strong> + &lt;strong>数据劫持&lt;/strong>。&lt;/p>
&lt;h2 id="设计模式">设计模式&lt;/h2>
&lt;p>发布订阅模式也称为观察者模式，是一种行为设计模式，设计意图也很明显了，允许定义一种..订阅..的机制，在对象发生一定的事件时通知观察着这个对象的其它对象。所以发布订阅模式一定存在以下三个模型对象：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>目标对象&lt;/strong>：被观察的对象。&lt;/li>
&lt;li>&lt;strong>发布者&lt;/strong>：当目标对象发生变化时，向其他对象发出通知。&lt;/li>
&lt;li>&lt;strong>订阅者&lt;/strong>：可以执行一些操作来反应发布者的通知。&lt;/li>
&lt;/ul>
&lt;p>真实世界中的发布订阅模式例子非常之多，比如你（订阅者）订阅了我的博客（目标对象），就毋需每次打开我的博客站点来查看是否更新了新博文，而我（发布者）会在每次发布新文章时都通过 RSS 在 RSS Feed Reader 等订阅工具上通知你。&lt;/p>
&lt;p>换置到双向数据绑定中，订阅者就是一个用来操作各 DOM 节点的公共方法，需要观察数据的改动来改动自己（作出反应），数据就是被观察的目标对象，那发布者是谁呢？&lt;del>是尤大啊&lt;/del>哈哈哈，其实是实现数据劫持的 &lt;code>Object.defineProperty()&lt;/code> 方法。&lt;/p>
&lt;h2 id="技术原理">技术原理&lt;/h2>
&lt;h3 id="objectdefineproperty">Object.defineProperty()&lt;/h3>
&lt;p>实现数据劫持的核心方法就是 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty">&lt;code>Object.defineProperty()&lt;/code>&lt;/a> 了，它是 &lt;a href="http://www.ecma-international.org/ecma-262/5.1/index.html#sec-15.2.3.6">ES5&lt;/a> 加入的标准对象方法，作用是给一个对象添加或修改属性。我们平时添加/修改对象属性更多使用的是点语法：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">obj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span> &lt;span class="p">};&lt;/span>
&lt;span class="nx">obj&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;18&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 添加属性
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;Bob&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 修改属性
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Object.defineProperty() 方法能让我们更细粒度地控制对象的属性，其可传入三个参数：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>obj&lt;/strong>：要操作的对象，Object 类型&lt;/li>
&lt;li>&lt;strong>prop&lt;/strong>：要添加或修改的属性名，String 类型&lt;/li>
&lt;li>&lt;strong>descriptor&lt;/strong>：属性描述符对象，Object 类型&lt;/li>
&lt;/ul>
&lt;p>&lt;code>descriptor&lt;/code> 被称为属性描述符，个人觉得官方文档描述的有些难以理解，其实它就是对象属性的一个标签、一个定义，总共有两种标签类型：&lt;strong>数据描述符&lt;/strong>和&lt;strong>存取描述符&lt;/strong>，规定一个属性只能有一个标签，并且：&lt;/p>
&lt;ul>
&lt;li>如果这个属性是数据描述符类型，那么它的 &lt;code>descriptor&lt;/code> 对象不能设置 &lt;code>get&lt;/code> 和 &lt;code>set&lt;/code> 属性&lt;/li>
&lt;li>如果这个属性是存取描述符类型，那么它的 &lt;code>descriptor&lt;/code> 对象不能设置 &lt;code>value&lt;/code> 和 &lt;code>writable&lt;/code> 属性&lt;/li>
&lt;/ul>
&lt;p>&lt;code>descriptor&lt;/code> 对象总共有这些属性：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>configurable&lt;/strong>：属性是否可被删除，默认为 &lt;code>false&lt;/code>&lt;/li>
&lt;li>&lt;strong>enumerable&lt;/strong>：属性是否可枚举，默认为 &lt;code>false&lt;/code>&lt;/li>
&lt;li>&lt;strong>value&lt;/strong>：属性对应的值&lt;/li>
&lt;li>&lt;strong>writable&lt;/strong>：属性是否可重写，默认为 &lt;code>false&lt;/code>&lt;/li>
&lt;li>&lt;strong>get&lt;/strong>：属性的 getter 函数，用于读取属性的值&lt;/li>
&lt;li>&lt;strong>set&lt;/strong>：属性的 setter 函数，用于设置属性的值&lt;/li>
&lt;/ul>
&lt;p>看实例：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">obj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span> &lt;span class="p">};&lt;/span>
&lt;span class="c1">// 添加具有数据描述符的属性，相当于 obj.age = 18
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">defineProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;age&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">configurable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 可删除
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">enumerable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 可枚举
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">writable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="c1">// 可重写
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">})&lt;/span>
&lt;span class="c1">// 添加具有存取描述符的属性
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">defineProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;age&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">configurable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">enumerable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">get&lt;/span>&lt;span class="p">(){&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">age&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">newVal&lt;/span>&lt;span class="p">){&lt;/span> &lt;span class="nx">age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">newVal&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>要着重看的是后者，具有存取描述符的属性的同样可以通过点语法来访问和重写属性的值，但是，是通过设置的 get 和 set 方法来实现的，这一点打印一下 &lt;code>obj&lt;/code> 就可以验证：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/two-way-data-binding:obj.png" alt="obj.png" title="具有 get 和 set 实例方法的对象">&lt;/p>
&lt;p>也就是说，无论是读取还是改动属性的值都会执行定义的方法，那……岂不是可以为所欲为了？比如在改动数据时作为发布者角色将改动通知给订阅者，数据劫持搞定？&lt;/p>
&lt;h3 id="documentfragment">DocumentFragment&lt;/h3>
&lt;p>频繁地操作 DOM 节点会引起页面的重排及重绘，这会极大地影响页面性能，Vue 中进行模板的解析&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/principle-of-vue-two-way-data-binding/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>就需要操作 DOM，使用到了 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment">DocumentFragment&lt;/a> 文档片段接口，它相当于一个 HTML 节点的..容器..，让我们可以在处理完子节点后都先存入该容器中，最后一次性将容器内&lt;strong>所有的&lt;/strong>子节点都添加或插入到真实的 DOM 中，从而将操作 DOM 的次数减少为只有一次，很大地提升了页面性能。&lt;/p>
&lt;p>比如要给一个 ul 标签添加 10 个 li，你可能会这样做：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">ul&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;list&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">ul&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">names&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Bob&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Tom&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Simon&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Paul&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Mary&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Lisa&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Ruth&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Susan&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Linda&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">ul&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;list&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">names&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">li&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;li&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">li&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">innerHTML&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">ul&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">li&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这就需要操作 DOM 多达 10 次，而使用 DocumentFragment 只需要操作一次 DOM：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">names&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Bob&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Tom&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Simon&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Paul&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Mary&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Lisa&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Ruth&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Susan&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Linda&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">ul&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;list&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">fragment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createDocumentFragment&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="nx">names&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">li&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;li&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">li&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">innerHTML&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">fragment&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">li&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">list&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fragment&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="原生实现">原生实现&lt;/h2>
&lt;h3 id="极简实现">极简实现&lt;/h3>
&lt;p>先不考虑设计模式种种，单单使用表单监听 + 数据劫持来实现极简的双向数据绑定：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="cp">&amp;lt;!DOCTYPE html&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">html&lt;/span> &lt;span class="na">lang&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;zh-CN&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">charset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;UTF-8&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Zander&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;input&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">obj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{};&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 数据劫持，实现数据-&amp;gt;视图的绑定
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">defineProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">configurable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">enumerable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">get&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">newVal&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">newVal&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">innerHTML&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">newVal&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="c1">// 监听输入框，实现视图-&amp;gt;数据的绑定
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;keyup&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">obj&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/two-way-data-binding:simple.gif" alt="simple.gif" title="双向数据绑定极简实现">&lt;/p>
&lt;h3 id="较完整实现">较完整实现&lt;/h3>
&lt;p>从上面的极简实现可以总结一下实现双向数据绑定要注意的点：&lt;/p>
&lt;ol>
&lt;li>整个数据层应该是 &lt;strong>Object 类型&lt;/strong>，才能对每个数据进行数据劫持（所以 Vue 组件中的 &lt;code>data&lt;/code> 必须是个对象或函数）&lt;/li>
&lt;li>使用同一个数据的 DOM 节点可能是多个，也就是说目标对象和订阅者对象是&lt;strong>一对多&lt;/strong>的关系&lt;/li>
&lt;/ol>
&lt;p>较完整的实现目标是这样的：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;app&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="na">v-model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;name&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
{{ name }}
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">vm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">ZVue&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">el&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#app&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 实例的挂载元素
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 数据
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>综合以上，要实现双向数据绑定在编码层面我们需要：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Observer 观察者函数&lt;/strong>：监听所有数据的变化，当数据变动时获取最新的值并通知给订阅者（数据劫持）&lt;/li>
&lt;li>&lt;strong>Watcher 订阅者函数&lt;/strong>：当接受到观察者的通知和提供的数据后同步更新视图&lt;/li>
&lt;li>&lt;strong>Compile 解析器函数&lt;/strong>：解析 DOM 元素上的 &lt;code>v-model&lt;/code> 指令和 &lt;code>{{}}&lt;/code> 语法&lt;/li>
&lt;/ul>
&lt;p>具体实现步骤：&lt;/p>
&lt;p>&lt;strong>1. 维护一个订阅者..们..数组&lt;/strong>&lt;/p>
&lt;p>因为使用同一个数据的 DOM 节点可能是多个，所以需要维护一个订阅者..们..数组，&lt;code>update()&lt;/code> 是订阅者的原型方法。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">class&lt;/span> &lt;span class="nx">Dep&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">constructor&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">subs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span> &lt;span class="c1">// 订阅者数组
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 添加订阅者 */&lt;/span>
&lt;span class="nx">addSub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">watcher&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">subs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">watcher&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 通知订阅者 */&lt;/span>
&lt;span class="nx">notify&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">subs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">watcher&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">watcher&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">update&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 更新视图
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 实现 Observer 观察者函数&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">class&lt;/span> &lt;span class="nx">Observer&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* data 处理 */&lt;/span>
&lt;span class="nx">observer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">data&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="k">typeof&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s1">&amp;#39;object&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// data 必须为 Object 类型
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">key&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">defineReactive&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]);&lt;/span> &lt;span class="c1">// 递归处理 data 中的 Object 类型
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 数据劫持 */&lt;/span>
&lt;span class="nx">defineReactive&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">that&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">dep&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Dep&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 每一个数据都对应一个订阅者们数组
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">defineProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">configurable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 可删除
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">enumerable&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 可枚举
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">get&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;span class="nx">Dep&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">dep&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addSub&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">Dep&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 每当 DOM 中使用该值时，添加一个订阅者
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">newVal&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="k">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">newVal&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">that&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">newVal&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 如果修改的新值是 Object 类型，递归进行数据劫持
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">newVal&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">dep&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">notify&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 通知所有订阅者数据更新了
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>注意 &lt;code>data&lt;/code> 必须是对象才能进行数据劫持，递归处理保证所有数据均被管理。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>3. 实现 Watcher 订阅者函数&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">class&lt;/span> &lt;span class="nx">Watcher&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">callback&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">vm&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 实例
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 数据当前值
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">callback&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">callback&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 调用的更新视图的方法
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">oldValue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 数据的旧值
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 获取数据旧值 */&lt;/span>
&lt;span class="nx">get&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">Dep&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 将订阅者实例赋值给 target
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">compileUtils&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">Dep&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="cm">/* 更新视图 */&lt;/span>
&lt;span class="nx">update&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">newVal&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">compileUtils&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">oldVal&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">oldValue&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">callback&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">newVal&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 实现 Compile 解析器函数&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">class&lt;/span> &lt;span class="nx">Compile&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">vm&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">isElementNode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">vm&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1">// 进行编译
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 使用文档片段存储节点
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">let&lt;/span> &lt;span class="nx">fragment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">node2fragment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 执行解析器函数，识别 v-model 和 {{}} 语法
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">compile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fragment&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 将编译完成的节点插入到 DOM 中
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fragment&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 判断传入的挂载元素是否是 HTML 元素 */&lt;/span>
&lt;span class="nx">isElementNode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">node&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">nodeType&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="cm">/* 将 el 中的节点放入文档片段中 */&lt;/span>
&lt;span class="nx">node2fragment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">fragment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createDocumentFragment&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 是子节点才插入
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">fragment&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">fragment&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="cm">/* 编译节点 */&lt;/span>
&lt;span class="nx">compile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fragment&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">childNodes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Array&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">from&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fragment&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">childNodes&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="nx">childNodes&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">nodeType&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 元素节点
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">compileElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">nodeType&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 文本节点
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">compileText&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="cm">/* 编译元素，识别 v-model */&lt;/span>
&lt;span class="nx">compileElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">attrs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Array&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">from&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">attributes&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 获取元素绑定的所有属性
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">attrs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">includes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;v-&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 判断是否为自定义的指令
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">attr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 属性的值
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">let&lt;/span> &lt;span class="nx">type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">attr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;-&amp;#39;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">];&lt;/span>
&lt;span class="nx">compileUtils&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">type&lt;/span>&lt;span class="p">](&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="cm">/* 编译文本，识别 {{}} */&lt;/span>
&lt;span class="nx">compileText&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">reg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sr">/\{\{([^}]+)\}\}/g&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// {{}} 正则表达式
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">node&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textContent&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">reg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">compileUtils&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;text&amp;#39;&lt;/span>&lt;span class="p">](&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>5. 编译中使用的具体方法&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">compileUtils&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="cm">/* 获取 data 中数据对应的值 */&lt;/span>
&lt;span class="nx">getValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 所有的数据都是 data 的属性
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">reduce&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">prev&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">prev&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">next&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nx">vm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 获取编译后的文本内容 */&lt;/span>
&lt;span class="nx">getTextValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sr">/\{\{([^}]+)\}\}/g&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(...&lt;/span>&lt;span class="nx">arguments&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">arguments&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span>
&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 给数据设置新值 */&lt;/span>
&lt;span class="nx">setVal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">newValue&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">reduce&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">prev&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">currentIndex&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">currentIndex&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">prev&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">next&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">newValue&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">prev&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">next&lt;/span>&lt;span class="p">];&lt;/span>
&lt;span class="p">},&lt;/span> &lt;span class="nx">vm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* v-model 处理*/&lt;/span>
&lt;span class="nx">model&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">doUpdate&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">updater&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">modelUpdater&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义更新视图的方法
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Watcher&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">newValue&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 实例化一个观察者
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">doUpdate&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">doUpdate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">));&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="c1">// 绑定 input 事件，输入值时更新数据
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">node&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">newValue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setVal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">newValue&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 文本处理 */&lt;/span>
&lt;span class="nx">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">doUpdate&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">updater&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textUpdater&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义更新视图的方法
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">pureValue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getTextValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 获取 {{}} 内的纯值
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sr">/\{\{([^}]+)\}\}/g&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(...&lt;/span>&lt;span class="nx">arguments&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 实例化一个观察者
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Watcher&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">arguments&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">newValue&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">doUpdate&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">doUpdate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getTextValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">vm&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">));&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">doUpdate&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">doUpdate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">pureValue&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 更新 DOM */&lt;/span>
&lt;span class="nx">updater&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">textUpdater&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">node&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textContent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">modelUpdater&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">node&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>6. 初始化 ZVue，添加代理&lt;/strong>&lt;/p>
&lt;p>因为数据都挂载在了 &lt;code>$data&lt;/code> 对象上，要访问需要通过 &lt;code>this.$data.name&lt;/code> 方式访问，而要通过 &lt;code>this.name&lt;/code> 方式访问需要添加一层代理，原理还是 &lt;code>Object.defineProperty&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">class&lt;/span> &lt;span class="nx">ZVue&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">options&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">options&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">options&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">new&lt;/span> &lt;span class="nx">Observer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">proxyData&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">new&lt;/span> &lt;span class="nx">Compile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="cm">/* 将 this.$data 上的数据代理到 this 上 */&lt;/span>
&lt;span class="nx">proxyData&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">defineProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">get&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">newValue&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">newValue&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>7. 实例化，挂载数据&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">vm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">ZVue&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">el&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#app&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/two-way-data-binding:result.gif" alt="result.gif" title="双向数据绑定较完整实现">&lt;/p>
&lt;h2 id="vue3-扩展">Vue3 扩展&lt;/h2>
&lt;blockquote>
&lt;p>&amp;quot;Bye Object.defineProperty!&amp;quot;&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/principle-of-vue-two-way-data-binding/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/p>
&lt;/blockquote>
&lt;p>Vue3 在数据劫持上选择了比 Object.defineProperty() 效率更高、性能更优的 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy">Proxy&lt;/a>。Proxy 对象的专属功能就是属性的查找、赋值、函数调用等，实例化时需要两个参数：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>target&lt;/strong>：目标对象，可以是 Object、Array、Function 等类型。&lt;/li>
&lt;li>&lt;strong>handler&lt;/strong>：由处理函数组成的对象，处理函数是一系列操作目标对象时触发的钩子函数。&lt;/li>
&lt;/ul>
&lt;p>简单使用 Proxy 实现数据劫持：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">obj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">hobbies&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">sport&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;running&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">game&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;LOL&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">proxy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nb">Proxy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">propertyKey&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">receiver&lt;/span>&lt;span class="p">){&lt;/span> &lt;span class="c1">// 分别为需要取值的目标对象、需要获取的键值、当前 proxy 实例（可选）
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`读取了&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">propertyKey&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">属性的值`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">propertyKey&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">receiver&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 通过函数调用实现取值，详见 MDN
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">propertyKey&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">receiver&lt;/span>&lt;span class="p">){&lt;/span> &lt;span class="c1">// 分别为设置属性的目标对象、属性名称、设置的值、当前 proxy 实例（可选）
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`设置了&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">propertyKey&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">属性的值`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">Reflect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">propertyKey&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">receiver&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="nx">proxy&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// log: 读取了age属性的值
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="nx">proxy&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">hobbies&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">game&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;MineSweeper&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// log: 读取了hobbies属性的值
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Proxy 的一些妙处：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Proxy 的监听目标是..整个对象..，而 Object.defineProperty() 一次只能操作..一个对象的一个属性..，要监听整个对象需要多层的迭代和递归。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Object.defineProperty() 无法监听..数组类型..属性的变化&lt;sup id="fnref:3">&lt;a href="https://xuezenghui.com/posts/principle-of-vue-two-way-data-binding/#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>，但 Proxy 🉑️。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>可操作的钩子函数有 ..13.. 个之多。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://ustbhuangyi.github.io/vue-analysis/">Vue.js 技术揭秘 | HuangYi&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://refactoringguru.cn/design-patterns/observer">观察者模式 | Refactoring.Guru&lt;/a>&lt;/li>
&lt;/ol>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>将 HTML 模板转换为 &lt;a href="https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9">AST&lt;/a> 的过程，比如解析 Vue 中的 &lt;code>{{}}&lt;/code> 语法。 &lt;a href="https://xuezenghui.com/posts/principle-of-vue-two-way-data-binding/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>见尤雨溪 Vue3.0 Updates 主题演讲 &lt;a href="https://docs.google.com/presentation/d/1yhPGyhQrJcpJI2ZFvBme3pGKaGNiLi709c37svivv0o/edit#slide=id.g42acc26207_0_129">PPT&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/principle-of-vue-two-way-data-binding/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3" role="doc-endnote">
&lt;p>Vue2 中的解决方法是重写数组的操作方法，详见 &lt;a href="https://github.com/vuejs/vue/blob/dev/src/core/observer/array.js">Github&lt;/a>，但通过下标修改数组的情况仍无法监测。 &lt;a href="https://xuezenghui.com/posts/principle-of-vue-two-way-data-binding/#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/vue.js/">Vue.js</category></item><item><title>MEVN 项目部署</title><link>https://xuezenghui.com/posts/deploy-mevn-project/</link><guid isPermaLink="true">https://xuezenghui.com/posts/deploy-mevn-project/</guid><pubDate>Wed, 17 Jun 2020 16:13:25 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>有一个计划已经躺在我的 &lt;a href="https://todo.microsoft.com/tasks/">Microsoft To Do&lt;/a> 里许久了——“完成一个图片合成工具”，解释一下，这个工具的作用是&lt;strong>合成图片&lt;/strong>（???还用你解释）。哈哈哈，因为在阅读技术文章的时候发现很多文章的配图为&lt;strong>多个技术的 Logo 组成的架构图&lt;/strong>，尤其 &lt;a href="https://medium.com/">Medium&lt;/a> 中的文章，比如&lt;a href="https://medium.com/@shrikarvk/creating-a-docker-container-for-spring-boot-app-d5ff1050c14f">这个&lt;/a>，&lt;a href="https://medium.com/swlh/how-to-create-your-first-mern-mongodb-express-js-react-js-and-node-js-stack-7e8b20463e66">这个&lt;/a>，当然了，谁让我又偏爱 Medium 的风格呢，所以还有&lt;a href="https://xuezenghui.com/posts/graphql/#graphql--nodejs--mongodb">这个&lt;/a>。那有没有一个方便的方式可以快速地生成这样瘠薄（技术博主）常用的图片呢？没找到……那就自己写一个罢。&lt;/p>
&lt;p>功能开发上并不难，图片合成使用 canvas 实现，剩下就是各技术 Logo 的管理了，相当于一个简单的图床。5天，基本功能实现，传送门——&lt;a href="">Psoon&lt;/a>，名字是 Picture synthesis soon，意为快速地 Ps 😳，技术架构为 MEVN&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/deploy-mevn-project/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>，源码见我的 &lt;a href="https://github.com/Xuezenghuigithub/psoon">GitHub&lt;/a>。&lt;/p>
&lt;p>因为自己平常与开发以外的环节接触不多，Linux 和 Docker 也都只学了皮毛，整个项目下来问题就几乎都出在了部署上，比如 Vue 项目的部署问题：&lt;/p>
&lt;ul>
&lt;li>字体文件太大，导致首屏加载时间极长&lt;/li>
&lt;li>路由采用 History 模式，直接访问子路由或刷新页面导致 404&lt;/li>
&lt;li>样式问题&lt;/li>
&lt;/ul>
&lt;p>但因为 Vue 项目的基本部署已在&lt;a href="../ecs-server/#%E9%83%A8%E7%BD%B2-vue-%E9%A1%B9%E7%9B%AE">前文&lt;/a>记录，这里就不详述了，针对上面几个问题会视其必要性辟文新立，先说说部署后台的主要工作。&lt;/p>
&lt;h2 id="部署方式">部署方式&lt;/h2>
&lt;p>诚言，在服务器上裸机部署 Node.js + MongoDB 的 Web 应用是没有什么难度的，
前文也有简单地记录过。为了更接近生产环境，也为了检测自己 Docker 的学习效果，决定采用 Docker 部署的方式。&lt;/p>
&lt;p>简单说一下 &lt;a href="https://docs.docker.com/">Docker&lt;/a>，它基于 Go 语言开发，是一个基于 Linux 容器虚拟化技术的用于构建、部署和共享应用程序的工具，做到了使 APP 及其运行环境“一次封装，到处运行”，大大避免了诸如“在我电脑上可以跑啊”之类的言论🌚，现如今 Docker 在后端的江湖地位也可谓举足轻重了。&lt;/p>
&lt;h2 id="环境安装">环境安装&lt;/h2>
&lt;p>环境主要是 Docker 工具集的安装，这里介绍如何在 CentOS 8 中安装 Docker 及 &lt;a href="https://docs.docker.com/compose/">Docker Compose&lt;/a>，Docker Compose 是用于定义和运行多个容器的一个 Docker 子工具，多用于有关联关系的容器之间的联调部署，譬如 Express 应用和 MongoDB 之间的..通信互联..。&lt;/p>
&lt;h3 id="安装-docker">安装 Docker&lt;/h3>
&lt;p>Docker 分为社区版 Docker CE 和企业版 Docker EE，这里安装的为社区版。&lt;/p>
&lt;p>&lt;strong>1. 添加 Docker CE 存储库&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">dnf&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">manager&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">repo&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">download.docker.com&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">linux&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">centos&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">docker&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">ce.repo&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 查看可安装的软件包版本&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">dnf&lt;/span> &lt;span class="n">list&lt;/span> &lt;span class="n">docker&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">ce&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 安装最新版本的 Docker CE&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">dnf&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">docker&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">ce&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">nobest&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">y&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 验证安装结果&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">docker&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">version&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="安装-docker-compose">安装 Docker Compose&lt;/h3>
&lt;p>&lt;strong>1. 安装 curl&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">dnf&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">curl&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">y&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 下载安装 Docker Compose&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">curl&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">L&lt;/span> &lt;span class="s">&amp;#34;https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)&amp;#34;&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">o&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">local&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">bin&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">docker&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">compose&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>1.26.0 为截止文章撰写之日的最新版本，后续请关注 &lt;a href="https://github.com/docker/compose/releases">GitHub releases&lt;/a> 及时更新替换版本。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>3. 为下载的二进制文件添加可执行权限&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">chmod&lt;/span> &lt;span class="o">+&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">local&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">bin&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">docker&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">compose&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 验证结果&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">docker&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">compose&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">version&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="部署应用">部署应用&lt;/h2>
&lt;h3 id="创建-dockerfile-文件">创建 Dockerfile 文件&lt;/h3>
&lt;p>简单点说，使用 Docker 部署应用即运行相应的 Docker 容器，需要先创建容器，创建容器需要使用 Docker 映像，而要构建映像，就需要使用 &lt;a href="https://docs.docker.com/engine/reference/builder/">Dockerfile&lt;/a>。在 Node.js 应用根目录下创建 Dockerfile 文件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-docker" data-lang="docker">&lt;span class="c"># 基础映像&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> node:latest&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="c"># 作者信息&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">MAINTAINER&lt;/span>&lt;span class="s"> Zander&amp;lt;xuezenghui@gmail.com&amp;gt;&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="c"># 设定运行容器后终端所在的工作路径&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /usr/src/psoon&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="c"># 拷贝依赖文件到映像文件夹&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> package.json /usr/src/psoon/&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="c"># 安装依赖&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> npm install&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="c"># 拷贝项目文件和目录到映像文件夹&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> . /usr/src/psoon&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="c"># 暴露端口号&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">EXPOSE&lt;/span>&lt;span class="s"> 2000&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="c"># 启动容器&lt;/span>&lt;span class="err">
&lt;/span>&lt;span class="err">&lt;/span>&lt;span class="k">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;npm&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;start&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>你可能会有疑问：复制全部文件不是已经包含 package.json 文件了吗？为什么要..执行两次.. &lt;code>COPY&lt;/code> 命令？&lt;/p>
&lt;p>原因是分作两步可以利用 Docker 的&lt;strong>缓存层&lt;/strong>，防止每次更改应用程序源码后都重新构建 node_modules 模块，详参&lt;a href="https://nodejs.org/zh-cn/docs/guides/nodejs-docker-webapp/#dockerfile">《把一个 Node.js web 应用程序给 Docker 化》&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;h3 id="创建-docker-compose-文件">创建 Docker Compose 文件&lt;/h3>
&lt;p>如果不使用 Docker Compose 则需要通过 Dockerfile 手动构建映像，再通过映像启动容器，而有了 Docker Compose 只需要创建用于启动容器服务的 Docker Compose 文件，同样的，在项目根目录下创建 docker-compose.yml 文件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yml" data-lang="yml">&lt;span class="k">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;3.4&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Compose file 版本&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">psoon&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 服务名&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>psoon&lt;span class="w"> &lt;/span>&lt;span class="c"># 容器名&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>always&lt;span class="w"> &lt;/span>&lt;span class="c"># 失败时重新运行容器&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>.&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用当前目录中的 Dockerfile 文件构建映像&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 主机和容器的端口映射&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;2000:2000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">links&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 链接的容器&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- mongo&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mongo&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>mongo&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>mongo&lt;span class="w"> &lt;/span>&lt;span class="c"># mongo 使用官方映像&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 指定容器数据卷，数据持久化&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- ./data&lt;span class="p">:&lt;/span>/data/db&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;27017:27017&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>注：Node 和 MongoDB 暴露的端口服务器都要配置对外开放。&lt;/p>
&lt;/blockquote>
&lt;h3 id="上传项目至服务器">上传项目至服务器&lt;/h3>
&lt;p>本地创建好项目构建部署所需的文件后需要将项目上传至 CentOS 8，有些文章中建议建立一个 .dockerignore 文件，其作用与 .gitignore 类似，用来排除构建 Docker 映像时不需要的文件和目录，比如 node_modules、npm-debug.log 文件。但我这里项目文件是由本地上传至服务器，手动删除此类文件不上传至服务器即可（变相节省服务器内存???），个人理解 .dockerignore 文件应多作用于 CI/CD&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/deploy-mevn-project/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> 时。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="c1"># 项目根目录下起步执行&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">rm&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">rf&lt;/span> &lt;span class="n">node_modules&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">cd&lt;/span> &lt;span class="n">..&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">scp&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">r&lt;/span> &lt;span class="n">psoon&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="o">@&amp;lt;&lt;/span>公网 &lt;span class="n">IP&lt;/span> 地址&lt;span class="o">&amp;gt;:/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">src&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">psoon&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="构建镜像启动服务">构建镜像，启动服务&lt;/h3>
&lt;p>登录服务器后进入项目目录，执行 Docker Compose 的构建启动命令：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">cd&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">src&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">psoon&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">docker&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">compose&lt;/span> &lt;span class="n">up&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">d&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;code>-d&lt;/code> 指定启动容器后不进入交互模式，以守护式进程运行。&lt;/p>
&lt;/blockquote>
&lt;p>构建过程：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-shell" data-lang="shell">Building psoon
Step 1/8 : FROM node:latest
---&amp;gt; dcda6cd5e439
Step 2/8 : MAINTAINER Zander&amp;lt;xuezenghui@gmail.com&amp;gt;
---&amp;gt; Running in 9c2e8d8a4df1
Removing intermediate container 9c2e8d8a4df1
---&amp;gt; ab2f34e52d9f
Step 3/8 : WORKDIR /usr/src/psoon
---&amp;gt; Running in 184b939e4b09
Removing intermediate container 184b939e4b09
---&amp;gt; f16a6e357920
Step 4/8 : COPY package.json /usr/src/psoon/
---&amp;gt; 4e9485e1a306
Step 5/8 : RUN npm install
---&amp;gt; Running in ce459058cd8a
......
Removing intermediate container ce459058cd8a
---&amp;gt; c05c31eabb12
Step 6/8 : COPY . /usr/src/psoon
---&amp;gt; 66b86989d3ba
Step 7/8 : EXPOSE &lt;span class="m">2000&lt;/span>
---&amp;gt; Running in 67332d77017b
Removing intermediate container 67332d77017b
---&amp;gt; fc863845b8a8
Step 8/8 : CMD &lt;span class="o">[&lt;/span>&lt;span class="s2">&amp;#34;npm&amp;#34;&lt;/span>, &lt;span class="s2">&amp;#34;start&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span>
---&amp;gt; Running in 022afc431683
Removing intermediate container 022afc431683
---&amp;gt; bacb1e3a5dd8
Successfully built bacb1e3a5dd8
Successfully tagged psoon_psoon:latest
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>在这里不得不提一个踩了一下午的坑🥶，在我执行 &lt;code>$ docker-compose up&lt;/code> 命令后构建总是卡在 Step 5 &lt;code>RUN npm install&lt;/code> 处，然后报错如下：&lt;/p>
&lt;pre>&lt;code>npm ERR! code EAI_AGAIN
npm ERR! errno EAI_AGAIN
npm ERR! request to https://registry.npmjs.org/connect-history-api-fallback failed, reason: getaddrinfo EAI_AGAIN registry.npmjs.org
npm ERR! A complete log of this run can be found in:
npm ERR! /root/.npm/_logs/2020-06-18T07_52_25_346Z-debug.log
ERROR: Service 'psoon' failed to build: The command '/bin/sh -c npm install' returned a non-zero code: 1
&lt;/code>&lt;/pre>&lt;p>尝试更改 npm 镜像源、更改服务器 DNS 等等方法皆无效，最终解决方案为指定 Docker 容器的网络为 &lt;a href="https://docs.docker.com/network/host/">host&lt;/a>，即宿主机的网络（默认为 &lt;a href="https://docs.docker.com/network/bridge/">bridge 网桥网络&lt;/a>），具体方式：&lt;/p>
&lt;ol>
&lt;li>手动构建镜像：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">docker&lt;/span> &lt;span class="n">build&lt;/span> &lt;span class="n">. &lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">network&lt;/span> &lt;span class="n">host&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ol start="2">
&lt;li>使用 Docker Compose 构建，则需要更改 docker-compose.yml 文件：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yml" data-lang="yml">&lt;span class="k">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;3.4&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Compose file 版本&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="k">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">psoon&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 服务名&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>psoon&lt;span class="w"> &lt;/span>&lt;span class="c"># 容器名&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>always&lt;span class="w"> &lt;/span>&lt;span class="c"># 失败时重新运行容器&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>.&lt;span class="w"> &lt;/span>&lt;span class="c"># 使用当前目录中的 Dockerfile 文件构建映像&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="sd">+ network: host # 指定容器使用的网络&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 主机和容器的端口映射&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;2000:2000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">links&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 链接的容器&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- mongo&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">mongo&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>mongo&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>mongo&lt;span class="w"> &lt;/span>&lt;span class="c"># mongo 使用官方映像&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 指定容器数据卷，数据持久化&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- ./data&lt;span class="p">:&lt;/span>/data/db&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;27017:27017&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>成功后使用 &lt;code>$ docker images&lt;/code> 和 &lt;code>$ docker ps&lt;/code> 命令可查看构建成功的镜像和已经启动的两个容器，在浏览器中输入 &lt;code>IP:2000&lt;/code> 或 &lt;code>IP:27017&lt;/code> 也就能访问到相应的服务了😚。&lt;/p>
&lt;p>最后，不要忘记在 NGINX 中配置接口请求地址～&lt;/p>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://medium.com/statuscode/dockerising-a-node-js-and-mongodb-app-d22047e2806f">Dockerising a Node.js and MongoDB App | Medium&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://medium.com/@nur_islam/complete-node-js-project-setup-from-docker-to-testing-docker-restfull-apis-with-node-js-9f384e06734a">Complete Node js Project Setup from Docker to Testing | Medium&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/StefanScherer/dockerfiles-windows/issues/270">npm install error - getaddrinfo EAI_AGAIN registry.npmjs.org:443 | GitHub&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://bitjudo.com/blog/2014/03/13/building-efficient-dockerfiles-node-dot-js/">Building Efficient Dockerfiles - Node.js | bitJudo&lt;/a>&lt;/li>
&lt;/ol>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>MongoDB + Express.js + Vue.js + Node.js 技术架构。 &lt;a href="https://xuezenghui.com/posts/deploy-mevn-project/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>持续集成、持续交付和持续部署。 &lt;a href="https://xuezenghui.com/posts/deploy-mevn-project/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/linux/">Linux</category></item><item><title>"Updating Homebrew..."</title><link>https://xuezenghui.com/posts/updating-homebrew/</link><guid isPermaLink="true">https://xuezenghui.com/posts/updating-homebrew/</guid><pubDate>Fri, 05 Jun 2020 09:30:55 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="问题">问题&lt;/h2>
&lt;p>&lt;a href="https://brew.sh/index_zh-cn">Homebrew&lt;/a> 是 MacOS 下一个极为方便的包管理工具，但是国内在使用的时候往往由于&lt;strong>网络环境&lt;/strong>的原因会出现这么一个问题——在执行 &lt;code>$ brew update&lt;/code> 或 &lt;code>$ brew upgrade&lt;/code> 等操作时，终端会卡死在 &lt;code>Updating Homebrew...&lt;/code> 状态，导致无法更新/下载 Homebrew 和本地软件。&lt;/p>
&lt;h2 id="解决">解决&lt;/h2>
&lt;p>和大多数网络问题导致的此类情况解决方法类似——要么..换源..，要么..科学上网..。&lt;/p>
&lt;h3 id="更换-homebrew-源">更换 Homebrew 源&lt;/h3>
&lt;p>换源算是最常规的解决办法了，像 Npm 换 cnpm 镜像、Docker 换阿里云镜像等等，Homebrew 的有效镜像推荐&lt;a href="http://mirrors.ustc.edu.cn/help/brew.git.html">中科大开源软件镜像&lt;/a>和&lt;a href="https://mirror.tuna.tsinghua.edu.cn/help/homebrew/">清华大学开源软件镜像&lt;/a>，以 USTC 镜像为例更换 Homebrew 源步骤：&lt;/p>
&lt;p>&lt;strong>1. 更换 brew 程序本身的源&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">cd&lt;/span> &lt;span class="s">&amp;#34;$(brew --repo)&amp;#34;&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">git&lt;/span> &lt;span class="n">remote&lt;/span> &lt;span class="n">set&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="n">origin&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">mirrors.ustc.edu.cn&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">brew.git&lt;/span>
&lt;span class="c1"># 还原为官方源&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">git&lt;/span> &lt;span class="n">remote&lt;/span> &lt;span class="n">set&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="n">origin&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">github.com&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Homebrew&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">brew.git&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 更换 homebrew-core 源&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">cd&lt;/span> &lt;span class="s">&amp;#34;$(brew --repo)/Library/Taps/homebrew/homebrew-core&amp;#34;&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">git&lt;/span> &lt;span class="n">remote&lt;/span> &lt;span class="n">set&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="n">origin&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">mirrors.ustc.edu.cn&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">homebrew&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">core.git&lt;/span>
&lt;span class="c1"># 还原为官方源&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">git&lt;/span> &lt;span class="n">remote&lt;/span> &lt;span class="n">set&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="n">origin&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">github.com&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Homebrew&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">homebrew&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">core.git&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>homebrew-core 为 Homebrew 的核心软件仓库。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>3. 更换 homebrew-cask 源&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">cd&lt;/span> &lt;span class="s">&amp;#34;$(brew --repo)&amp;#34;&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Library&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Taps&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">homebrew&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">homebrew&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cask&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">git&lt;/span> &lt;span class="n">remote&lt;/span> &lt;span class="n">set&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="n">origin&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">mirrors.ustc.edu.cn&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">homebrew&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cask.git&lt;/span>
&lt;span class="c1"># 还原为官方源&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">git&lt;/span> &lt;span class="n">remote&lt;/span> &lt;span class="n">set&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="n">origin&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">github.com&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Homebrew&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">homebrew&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cask.git&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>homebrew-cask 是提供 macOS 应用程序的软件仓库。&lt;/p>
&lt;/blockquote>
&lt;h3 id="配置终端科学上网">配置终端科学上网&lt;/h3>
&lt;p>一般只更换上述的三个源就可以解决 Homebrew 软件的下载和更新问题了，但是这并不是一劳永逸的办法。你是否在执行 &lt;code>$ git clone&lt;/code> 时每秒几 kb 的下载速度？你是否在使用 Npm / Yarn 安装依赖时等得头大……&lt;/p>
&lt;p>并且，因为终端只支持 HTTP 代理，而主机使用的是 Sock 代理，即便主机使用了代理工具可以科学上网，但终端的网络环境还是一样的糟糕，这时，就需要配置终端科学上网😎。&lt;a href="https://www.privoxy.org/">Privoxy&lt;/a> 代理工具可实现终端的 HTTP 代理，具体配置步骤：&lt;/p>
&lt;blockquote>
&lt;p>在进行以下步骤前，请确保本机可以科学上网，因为 Privoxy 是一个..客户端代理工具..，单独不能进行翻墙。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>1. 安装 Privoxy&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">brew&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">privoxy&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 配置监听端口&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">nano&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">local&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">privoxy&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">config&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>追加配置内容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yaml" data-lang="yaml">listen-address&lt;span class="w"> &lt;/span>&lt;span class="m">0.0.0.0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">8118&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 监听8118端口&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>forward-socks5&lt;span class="w"> &lt;/span>/&lt;span class="w"> &lt;/span>localhost&lt;span class="p">:&lt;/span>&lt;span class="m">1080&lt;/span>&lt;span class="w"> &lt;/span>.&lt;span class="w"> &lt;/span>&lt;span class="c"># 转发至socks端口&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>localhost 后的端口号为本机 Sock 监听端口，可在本机代理工具中查看，以 V2rayU 为例，进入&lt;code>偏好设置&lt;/code>➡️&lt;code>Advance&lt;/code>即可查看。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>3. 启动 Privoxy 服务&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">local&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">sbin&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">privoxy&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">usr&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">local&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">privoxy&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">config&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 查看网络连接状况&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="n">netstat&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">na&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">grep&lt;/span> &lt;span class="m">8118&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>如打印以下内容即为端口监听成功：&lt;/p>
&lt;pre>&lt;code>tcp4 0 0 *.8118 *.* LISTEN
tcp4 0 0 127.0.0.1.8118 *.* LISTEN
&lt;/code>&lt;/pre>&lt;p>&lt;strong>5. 开启 Privoxy 代理&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="c1"># 开启&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">export&lt;/span> &lt;span class="n">http_proxy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;http://localhost:8118&amp;#39;&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">export&lt;/span> &lt;span class="n">https_proxy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;http://localhost:8118&amp;#39;&lt;/span>
&lt;span class="c1"># 关闭&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">unset&lt;/span> &lt;span class="n">http_proxy&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">unser&lt;/span> &lt;span class="n">https_proxy&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>至此，..当前..终端已经可以进行科学上网了，在执行以上开启代理命令的前后分别执行 &lt;code>$ curl cip.cc&lt;/code> 可查看网络 IP 地址的变化。但只针对当前打开的终端有效，新建的终端窗口需要重复执行步骤5来开启代理，当然，是有便捷方法的——配置打开/关闭代理的快捷命令：&lt;/p>
&lt;p>配置 &lt;a href="https://ohmyz.sh/">Oh My Zsh&lt;/a> 的配置文件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">nano&lt;/span> &lt;span class="o">~/&lt;/span>&lt;span class="n">.zshrc&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>内容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yaml" data-lang="yaml">function&lt;span class="w"> &lt;/span>proxy_on()&lt;span class="w"> &lt;/span>{&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>export&lt;span class="w"> &lt;/span>no_proxy=&lt;span class="s2">&amp;#34;localhost,127.0.0.1,localaddress,.localdomain.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>export&lt;span class="w"> &lt;/span>http_proxy=&lt;span class="s2">&amp;#34;http://localhost:8118&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>export&lt;span class="w"> &lt;/span>https_proxy=&lt;span class="s2">&amp;#34;http:/localhost:8118&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>echo&lt;span class="w"> &lt;/span>-e&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;已开启代理&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>}&lt;span class="w">
&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>function&lt;span class="w"> &lt;/span>proxy_off(){&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>unset&lt;span class="w"> &lt;/span>http_proxy&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>unset&lt;span class="w"> &lt;/span>https_proxy&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>echo&lt;span class="w"> &lt;/span>-e&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;已关闭代理&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>}&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后使用 &lt;code>$ source ~/.zshrc&lt;/code> 命令执行更改。&lt;/p>
&lt;blockquote>
&lt;p>Oh My Zsh 这么棒的工具你不会没安装吧？Alright，如果没安装，使用的是默认的终端，配置 &lt;code>$ nano ~/.bash_profile&lt;/code>，内容无异，&lt;code>source&lt;/code> 命令的文件路径也作相应更改即可。&lt;/p>
&lt;/blockquote>
&lt;p>Bingo～现在，在终端输入 &lt;code>$ proxy_on&lt;/code> 即可打开终端代理功能，&lt;code>$ proxy_off&lt;/code> 即关闭🤟，问题得解～&lt;/p></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/macos/">MacOS</category><category domain="https://xuezenghui.com/tags/debug/">debug</category></item><item><title>Linux 的些个操作</title><link>https://xuezenghui.com/posts/linux-tips/</link><guid isPermaLink="true">https://xuezenghui.com/posts/linux-tips/</guid><pubDate>Mon, 25 May 2020 13:28:59 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>记录使用 Linux 时的些个操作，CenteOS 8.0 下。&lt;/p>
&lt;h2 id="配置非-root-用户">配置非 root 用户&lt;/h2>
&lt;p>直接使用 root 权限操作 Linux 是极为不安全的，也是非常不提倡的，应该为不同的角色配置不同的用户身份，可为用户配置一定的管理权限，在需要执行管理操作时输入 &lt;code>sudo&lt;/code> 前缀即可拥有 root 的管理权限，具体步骤：&lt;/p>
&lt;p>&lt;strong>1. 使用 root 用户身份登录 Linux&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">ssh&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">service_ip&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 创建新用户&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">adduser&lt;/span> &lt;span class="n">zander&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 配置新用户的密码&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">passwd&lt;/span> &lt;span class="n">zander&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 配置管理权限&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">usermod&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">aG&lt;/span> &lt;span class="n">wheel&lt;/span> &lt;span class="n">zander&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;code>usermod&lt;/code> 命令用于修改用户账号，&lt;code>-aG&lt;/code> 选项为用户指定&lt;strong>附加组&lt;/strong>，wheel 组是 CentOS 8 中默认存在的拥有特殊权限的群组，群组中的成员可使用 &lt;code>sudo&lt;/code> 命令执行 root 权限的操作。&lt;/p>
&lt;/blockquote>
&lt;h2 id="免密登陆">免密登陆&lt;/h2>
&lt;p>每次本机连接 Linux 都需要输入用户密码，这可真是糟透了，是个懒人没错:)。为本机和服务器配对 SSH 公私密钥即可实现免密登录，只需要一行命令：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">ssh&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">copy&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">id&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">i&lt;/span> &lt;span class="o">~/&lt;/span>&lt;span class="n">.ssh&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">id_rsa.pub&lt;/span> &lt;span class="n">username&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="n">service_ip&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>当然前提是你本机已经生成了公私钥文件，但都看到这儿了你怎么可能没有呢🤨！&lt;/p>
&lt;/blockquote>
&lt;h2 id="keep-alive">Keep Alive&lt;/h2>
&lt;p>终端中通过 SSH 连接上 Linux 后，若几分钟内不使用会导致连接失活，原因是防火墙在一定时间内未监测到会话活动后会自动断开 SSH 连接。可配置本机 SSH，使本地每隔一段时间发送生命周期信号发送至服务器，防止连接失活。同样的，也可配置远程服务器，使其定时向客户端发送数据来保证持久连接。&lt;/p>
&lt;h3 id="方法一配置客户端">方法一、配置客户端&lt;/h3>
&lt;p>&lt;strong>1. 创建 ~/.ssh/config 文件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">touch&lt;/span> &lt;span class="o">~/&lt;/span>&lt;span class="n">.ssh&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">config&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 配置该文件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-py" data-lang="py">&lt;span class="n">Host&lt;/span> &lt;span class="o">*&lt;/span>
&lt;span class="n">ServerAliveInterval&lt;/span> &lt;span class="mi">60&lt;/span>
&lt;span class="n">ServerAliveCountMax&lt;/span> &lt;span class="mi">10&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此配置表示客户端每分钟向所有已连接的服务器发送一次数据，发送10次后如果仍未收到响应则关闭该连接。&lt;/p>
&lt;h3 id="方法二配置服务器">方法二、配置服务器&lt;/h3>
&lt;p>&lt;strong>编辑 /etc/ssh/ssh_config 文件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">nano&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">ssh&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">ssh_config&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>内容为：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-py" data-lang="py">&lt;span class="n">ClientAliveInterval&lt;/span> &lt;span class="mi">60&lt;/span>
&lt;span class="n">ClientAliveCountMax&lt;/span> &lt;span class="mi">10&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="配置-docker-命令不需加-sudo">配置 Docker 命令不需加 &lt;code>sudo&lt;/code>&lt;/h2>
&lt;p>Docker 守护进程默认由 root 用户掌管，其他用户使用 Docker 命令时必须加 &lt;code>sudo&lt;/code> 才可成功执行。可添加配置 docker 用户组使得组内用户使用 Docker 时不需加 &lt;code>sudo&lt;/code> 命令。&lt;/p>
&lt;p>&lt;strong>1. 添加 docker 用户组&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">groupadd&lt;/span> &lt;span class="n">docker&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 将用户加入用户组&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">sudo&lt;/span> &lt;span class="n">usermod&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">aG&lt;/span> &lt;span class="n">docker&lt;/span> &lt;span class="n">username&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 重新登录，或切换用户组以更新权限&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">newgrp&lt;/span> &lt;span class="n">docker&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="添加-root-权限用户">添加 root 权限用户&lt;/h2>
&lt;p>&lt;strong>1. 添加用户&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">useradd&lt;/span> &lt;span class="n">username&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 修改密码&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">passwd&lt;/span> &lt;span class="n">username&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 修改 sudoers 文件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">visudo&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>添加以下内容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-yml" data-lang="yml">&lt;span class="c"># 用户/组 主机=(允许切换到哪些用户或组) 允许执行的命令&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>username&lt;span class="w"> &lt;/span>ALL=(ALL)&lt;span class="w"> &lt;/span>ALL&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/linux/">Linux</category></item><item><title>How 与 Why</title><link>https://xuezenghui.com/posts/how-and-why/</link><guid isPermaLink="true">https://xuezenghui.com/posts/how-and-why/</guid><pubDate>Fri, 24 Apr 2020 09:23:13 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>最近有一个困惑，不是技术上的困惑，而是思想上的困惑——&lt;strong>特别喜欢问「为什么」，特别想知道「为什么」&lt;/strong>。&lt;/p>
&lt;p>无论是学习新技术的过程中还是解决问题的时候，只要技术点中有没听过不熟悉的，就想要去搞个明白，就算能力和时间有限，不能入木三分也要知其大概。这导致了什么结果呢？&lt;/p>
&lt;p>最大的影响是..效率..，比如之前在同样技术难度水平上的一篇博文花不了几个小时就可以完成（一般2~6个小时），而现在，由于学习过程中不断去查找新东西，又在新东西中看到新东西，又去了解……一篇博客甚至花了三天时间。&lt;/p>
&lt;p>仔细想了想，是什么时候自己变得过分地不求甚解，大概是从自己抛弃百度使用 Google 后逐渐演变的。可能你不太理解，举个例子，昨天我的 Npm 缓存损坏了，百度的&lt;a href="https://www.baidu.com/s?wd=npm%20ERR!%20Unexpected%20end%20of%20JSON%20input%20while%20parsing%20near%20%27...YvMsuFgqukIR69rROAcWe%27&amp;amp;rsv_spt=1&amp;amp;rsv_iqid=0xfa9e87e300032da2&amp;amp;issp=1&amp;amp;f=8&amp;amp;rsv_bp=1&amp;amp;rsv_idx=2&amp;amp;ie=utf-8&amp;amp;rqlang=cn&amp;amp;tn=baiduhome_pg&amp;amp;rsv_enter=0&amp;amp;rsv_dl=tb&amp;amp;oq=Unexpected%2520end%2520of%2520JSON%2520input%2520while%2520parsing%2520near&amp;amp;rsv_btype=t&amp;amp;inputT=145582&amp;amp;rsv_t=e428JJZ%2B%2BeWRwoYJP2FZGxc8R5PRhSE2%2FbqNC%2F4x7WLotAsYE4Op6heUytbJdXJIpazQ&amp;amp;rsv_pq=863b86ac0000c6e7&amp;amp;rsv_sug3=14&amp;amp;rsv_sug2=0&amp;amp;rsv_sug4=145726">搜索结果&lt;/a>&lt;del>几乎&lt;/del>全都是 CSDN、简书文章上的简单解决办法 &lt;code>$ npm cache clean --force&lt;/code>（广告、与搜索内容不符等问题也就不说了），潦草几句。Google 的&lt;a href="https://www.google.com.hk/search?q=npm+ERR!+Unexpected+end+of+JSON+input+while+parsing+near+%27...YvMsuFgqukIR69r&amp;amp;oq=npm+ERR!+Unexpected+end+of+JSON+input+while+parsing+near+%27...YvMsuFgqukIR69r&amp;amp;aqs=chrome..69i57j69i60.883j0j4&amp;amp;sourceid=chrome&amp;amp;ie=UTF-8">搜索结果&lt;/a>讨论更多的则是「Why」，甚至因为原理上的东西在&lt;a href="https://stackoverflow.com/questions/56801803/npm-err-unexpected-end-of-json-input-while-parsing-near">这里&lt;/a>产生分歧和碰撞，是的，它引导你去了解更多、更本质的知识，而不是解决了就一了了之。&lt;/p>
&lt;p>不是我崇洋媚外，因为在我看来技术没有国界，思想更没有；也不是我非要褒一贬一，我没有否定百度，只是我更喜欢 Google 的氛围；这也并不是个例，长久以来的现象才形成现在的..习惯..。再说回来，对于一个问题，只了解「How」真的就够了吗？&lt;strong>远远不够&lt;/strong>，知其然定要知其所以然，可能自己的问题在于没能把握好「How」与「Why」的天平吧。&lt;/p>
&lt;p>那何以使得只追求「How」体现在了百度上的现象化结果？大概因为：&lt;/p>
&lt;p>GFW&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/how-and-why/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> 并不只是封锁了互联网，它阻碍的是技术的进步，禁锢的是思想的延伸💔。&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>Great Firewall。 &lt;a href="https://xuezenghui.com/posts/how-and-why/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/life/">Life</category><category domain="https://xuezenghui.com/tags/thought/">thought</category></item><item><title>图片预览</title><link>https://xuezenghui.com/posts/preview-image/</link><guid isPermaLink="true">https://xuezenghui.com/posts/preview-image/</guid><pubDate>Wed, 22 Apr 2020 09:04:15 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="需求">需求&lt;/h2>
&lt;p>今天来看一个挺常见的需求，上传图片时页面展示选择的图片，具体实现效果可以点击下面的「上传图片」按钮试试👇～（只能选择 .png 或 .jpeg 格式图片）&lt;/p>
&lt;blockquote>
&lt;p>按钮设计源自 &lt;a href="https://codepen.io/sfoxy/pen/XpOoJe">CodePen&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;style>
@import url('https://fonts.googleapis.com/css2?family=Zhi+Mang+Xing&amp;display=swap');
.btn {
width: 180px;
height: 45px;
border: none;
display: block;
text-align: center;
text-transform: uppercase;
outline: none;
overflow: hidden;
position: relative;
color: #fff;
font-family: 'Zhi Mang Xing', cursive;
font-size: 30px;
background-color: #222;
margin: 0 auto;
box-shadow: 0 5px 15px rgba(0,0,0,0.20);
}
.btn label {
display: block;
width: 100%;
height: 100%;
position: relative;
z-index: 1;
line-height: 45px;
cursor: pointer;
}
.btn:after {
content: "";
position: absolute;
left: 0;
top: 0;
height: 490%;
width: 140%;
background: #78c7d2;
-webkit-transition: all .5s ease-in-out;
transition: all .5s ease-in-out;
-webkit-transform: translateX(-98%) translateY(-25%) rotate(45deg);
transform: translateX(-98%) translateY(-25%) rotate(45deg);
}
.btn:hover:after {
-webkit-transform: translateX(-9%) translateY(-25%) rotate(45deg);
transform: translateX(-9%) translateY(-25%) rotate(45deg);
}
.my_input {
display: none;
}
&lt;/style>
&lt;input type="file" class="my_input" id="inputFile" accept="image/png, image/jpeg">
&lt;button class="btn">&lt;label for="inputFile">上传图片&lt;/label>&lt;/button>
&lt;img id="previewImg" />
&lt;script>
document.getElementById('inputFile').addEventListener('change', handleChange, false);
function handleChange(e) {
let fileList = e.target.files;
if (fileList[0].type !== 'image/png' &amp;&amp; fileList[0].type !== 'image/jpeg') return;
/* 方式一、FileReader */
const reader = new FileReader(); // 实例化对象
reader.readAsDataURL(fileList[0]); // 开始异步读取文件
reader.onload = function (e) { // 读取完文件后触发的函数
document.getElementById('previewImg').src = e.target.result; // 将图片的 base64 编码赋值给容器的 src 属性
}
/* 方式二、对象 URL */
// document.getElementById('previewImg').src = (URL || webkitURL).createObjectURL(fileList[0]);
// (URL || webkitURL).revokeObjectURL(fileList[0]); // 释放内存
}
&lt;/script>
&lt;h2 id="实现">实现&lt;/h2>
&lt;h3 id="html">HTML&lt;/h3>
&lt;p>首先是要使用的标签，上传文件需要用到 &lt;code>&amp;lt;input&amp;gt;&lt;/code> 标签，并且指定属性 &lt;code>type&lt;/code> 的值为 &lt;code>file&lt;/code>。&lt;/p>
&lt;p>既然是预览图片，那么还需要指定选择文件的类型，使用 &lt;code>accept&lt;/code> 属性可以规定..期望..的文件类型，为啥是「期望」呢？因为这只表示触发控件后默认允许选中的文件类型，比如在 MacOS 上，刁钻的用户仍可以在唤出的选择文件窗口中通过&lt;code>选项&lt;/code>➡️&lt;code>格式：所有文件&lt;/code>来选择其它类型的文件上传👿：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/preview-image:choose-file.png" alt="choose-file.png" title="仍可选择非指定类型的文件">&lt;/p>
&lt;p>这就要求在 JavaScript 中还要判断用户选择的文件类型是否合法，后面会说到，先指定期望的类型为 PNG 或 JPG 格式的图片。并且，使用 &lt;code>&amp;lt;input&amp;gt;&lt;/code> 标签时常用的做法是与 &lt;code>&amp;lt;label&amp;gt;&lt;/code> 标签合用并且设置 &lt;code>&amp;lt;input&amp;gt;&lt;/code> 为不可见，这样做的好处有两点：&lt;/p>
&lt;ol>
&lt;li>&lt;code>&amp;lt;input&amp;gt;&lt;/code> 的样式不易调整，而 &lt;code>&amp;lt;label&amp;gt;&lt;/code> 的样式可以为所欲为。&lt;/li>
&lt;li>&lt;code>&amp;lt;label&amp;gt;&lt;/code> 增加了 &lt;code>&amp;lt;input&amp;gt;&lt;/code>的命中区域。&lt;/li>
&lt;/ol>
&lt;p>所以现在的 HTML 是这样的：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;file&amp;#34;&lt;/span> &lt;span class="na">accept&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;image/png, image/jpg&amp;#34;&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;inputFile&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">label&lt;/span> &lt;span class="na">for&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;inputFile&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>上传图片&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">label&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="css">CSS&lt;/h3>
&lt;p>样式上要做的一点是隐藏 &lt;code>&amp;lt;input&amp;gt;&lt;/code>，剩下的就完全自由化了：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="nt">input&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>另外，&lt;a href="https://codepen.io">CodePen&lt;/a> 上的各种按钮样式可真是美哭我了，比那些在线设计按钮样式的网站好太多了🤫，强烈安利。&lt;/p>
&lt;h3 id="javascript">JavaScript&lt;/h3>
&lt;p>到了功能实现的环节了，首先需要拿到选择图片的相关信息，在用户选择了文件时可以触发 &lt;code>&amp;lt;input&amp;gt;&lt;/code> 的 &lt;code>change&lt;/code> 事件，&lt;code>change&lt;/code> 事件的参数内包含一个属性 &lt;code>files&lt;/code>，其对应的值 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/FileList">&lt;code>FileList&lt;/code>&lt;/a> 是一个..类数组..，由一个个 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/File">&lt;code>File&lt;/code>&lt;/a> 对象组成，对象内存储着用户选择的文件的信息，比如名字、大小、类型等。&lt;/p>
&lt;blockquote>
&lt;p>因为 &lt;code>&amp;lt;input&amp;gt;&lt;/code> 有一个 &lt;code>multiple&lt;/code> 属性，设置后可以选择多个文件，所以 &lt;code>FileList&lt;/code> 的类型为类数组。&lt;/p>
&lt;/blockquote>
&lt;p>能获取到文件的类型就可以做我们上面说的&lt;strong>校验文件合法性&lt;/strong>了：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;inputFile&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handleChange&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">handleChange&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">fileList&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">files&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 文件对象类数组
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileList&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">type&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s1">&amp;#39;image/png&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">fileList&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">type&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s1">&amp;#39;image/jpeg&amp;#39;&lt;/span>&lt;span class="p">){&lt;/span> &lt;span class="c1">// 校验文件类型
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;文件类型错误&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>接下来需要在页面中显示图片，需要一个展示图片的容器，当然也可以使用 JavaScript 直接插入到页面，但前者更容易设置图片的样式。显示图片有两种方式——&lt;strong>FileReader&lt;/strong> 和&lt;strong>对象 URL&lt;/strong>。&lt;/p>
&lt;p>&lt;strong>1. FileReader&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader">FileReader&lt;/a> 对象用来..异步..读取文件的内容，可读取的目标正是获取到的 File 对象。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">reader&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">FileReader&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 实例化对象
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">reader&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">readAsDataURL&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileList&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]);&lt;/span> &lt;span class="c1">// 开始异步读取文件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">reader&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">onload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 读取完文件后触发的函数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;previewImg&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">src&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 将图片的 base64 编码赋值给容器的 src 属性
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsDataURL">&lt;code>readAsDataURL()&lt;/code>&lt;/a> 方法可读取传入的 File 对象，读取完成后会触发 &lt;code>onload&lt;/code> 事件，事件的参数中就包含&lt;strong>图片的 base64 编码&lt;/strong>，然后将它设置为页面中用于展示图片的 &lt;code>&amp;lt;img&amp;gt;&lt;/code> 标签的 &lt;code>src&lt;/code> 属性，图片成功显示～&lt;/p>
&lt;p>&lt;strong>2. 对象 URL&lt;/strong>&lt;/p>
&lt;p>对象 URL 指的是 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL">&lt;code>window.URL.createObjectURL()&lt;/code>&lt;/a> 和 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/URL/revokeObjectURL">&lt;code>window.URL.revokeObjectURL()&lt;/code>&lt;/a> 方法，&lt;code>window.URL.createObjectURL()&lt;/code> 方法同样可传入一个 File 对象，但是返回的并不是图片的 base64 编码，而是一个特殊的字符串&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/preview-image/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>，长这个亚子：&lt;/p>
&lt;pre>&lt;code>blob:null/1a92aff8-90a4-4421-966c-4b151078da0f
&lt;/code>&lt;/pre>&lt;p>不管它叫啥，它只能由浏览器内部生成，将 &lt;code>&amp;lt;img&amp;gt;&lt;/code> 标签的 &lt;code>src&lt;/code> 属性设置成它也可以显示图片：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;previewImg&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">src&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">URL&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">webkitURL&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">createObjectURL&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fileList&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;code>(URL || webkitURL).createObjectURL()&lt;/code> 为兼容写法。&lt;/p>
&lt;/blockquote>
&lt;h2 id="结语">结语&lt;/h2>
&lt;p>效果就不用再展示了，完整方法和上面炫酷按钮的案例源码都在我的 &lt;a href="https://raw.githubusercontent.com/Xuezenghuigithub/xuezenghui.com/master/content/posts/%E5%9B%BE%E7%89%87%E9%A2%84%E8%A7%88.md">GitHub&lt;/a> 中。那么到底要选择使用哪个方式呢？&lt;/p>
&lt;p>首先，这两个 API 的兼容性都是差不多的，IE 10+ 和所有主流浏览器都支持。从性能上来说，&lt;code>FileReader.readAsDataURL()&lt;/code> 是异步执行，而 &lt;code>createObjectURL()&lt;/code> 是同步执行，而 Blob URLs 占用的内存比 base64 更小，所以总体来讲 &lt;strong>&lt;code>createObjectURL()&lt;/code> 占优&lt;/strong>，但其关闭标签页时才会释放内存，使用的时候应考虑是否要用 &lt;code>window.URL.revokeObjectURL()&lt;/code> 方法手动释放内存。&lt;/p>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/File/Using_files_from_web_applications">在 web 应用程序中使用文件 | MDN&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://stackoverflow.com/questions/4459379/preview-an-image-before-it-is-uploaded">Preview an image before it is uploaded | Stack Overflow&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://stackoverflow.com/questions/30864573/what-is-a-blob-url-and-why-it-is-used">What is a blob URL and why it is used? | Stack Overflow&lt;/a>&lt;/li>
&lt;/ol>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>W3C 称它为 &lt;a href="https://w3c.github.io/FileAPI/#blob-section">Blob URLs&lt;/a>，MDN 称它为 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL#Parameters">Object-URLs&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/preview-image/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/javascript/">JavaScript</category></item><item><title>ECS 服务器配置及部署实践</title><link>https://xuezenghui.com/posts/ecs-server/</link><guid isPermaLink="true">https://xuezenghui.com/posts/ecs-server/</guid><pubDate>Mon, 13 Apr 2020 16:24:34 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>前端切图仔终于在今天也有自己的服务器了😳（还不是趁着没满24岁再薅两把阿里的羊毛）——2G 单核 1M 带宽云服务 ECS ，再入门不过了，也是，自己本来就是用来入门的。所以呢，在此记录服务器从零配置到部署前端项目（Vue.js）和后端项目（Node.js）的过程～&lt;/p>
&lt;h2 id="前置工作">前置工作&lt;/h2>
&lt;p>&lt;a href="https://promotion.aliyun.com/ntms/act/campus2018.html?utm_content=se_1004747724">学生优惠&lt;/a>真的是超划算了，想当年大学时学 Java 的室友就给我疯狂安利，当时的我还嗤之以鼻——“我一学前端的要那玩意儿干哈，用不上”，现在看来真是蛮香的了🤤，建议有闲钱的续他个十年八年的……&lt;/p>
&lt;p>服务器呢建议购买云服务器 ECS 而不是轻量应用服务器——如果你也想和我一样从零开始配置服务器，完整地体会整个过程的话。服务器的预装环境笔者选择 CentOS 8.0 64位。&lt;/p>
&lt;p>购买完成后关键的一步是&lt;strong>设置实例密码&lt;/strong>，在阿里云控制台中的实例列表页面➡️实例的右侧&lt;code>更多&lt;/code>➡️&lt;code>密码/密钥&lt;/code>➡️&lt;code>重置实例密码&lt;/code>，这个密码用于后续连接远程服务器，其他的配置如安全组规则等按照阿里云的文档来操作即可，确保开通了80和443端口。&lt;/p>
&lt;p>还需要说明的是我本机操作系统为 macOS Catalina 10.15.3，以下操作都基于此。&lt;/p>
&lt;h2 id="连接服务器及目录说明">连接服务器及目录说明&lt;/h2>
&lt;h3 id="连接服务器">连接服务器&lt;/h3>
&lt;p>macOS 连接服务器实例是极为方便的，在阿里云控制台查看实例的公网 IP 地址，然后在终端中通过 SSH 连接远程实例并指定为 root 用户身份：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">ssh&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="o">@&amp;lt;&lt;/span>公网 &lt;span class="n">IP&lt;/span> 地址&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>接着会要求输入实例的密码，当输出 &lt;code>Welcome to Alibaba Cloud Elastic Compute Service !&lt;/code> 时就成功连接上了 ECS 实例。&lt;/p>
&lt;blockquote>
&lt;p>Windows 系统及其它环境可参考&lt;a href="https://help.aliyun.com/document_detail/25434.html?spm=a2c4g.11186623.2.20.187d6c82wePGwX#concept-rsl-2vx-wdb">阿里云帮助文档&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;h3 id="linux-目录说明">Linux 目录说明&lt;/h3>
&lt;p>Linux 操作系统遵循 &lt;a href="http://www.pathname.com/fhs/">FHS&lt;/a> 的标准，根目录下默认存在以下次级目录：&lt;/p>
&lt;p>&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/ecs-server/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;img src="https://xuezenghui.com/images/ecs-server:directory.gif" alt="directory.gif" title="Linux 目录树">&lt;/p>
&lt;p>成功连接实例后位于 &lt;code>/root&lt;/code> 目录下，使用 &lt;code>$ cd ..&lt;/code> 命令可回到根目录，再使用 &lt;code>$ ls&lt;/code> 就可看到上图中的目录了。&lt;/p>
&lt;blockquote>
&lt;p>使用 &lt;code>$ pwd&lt;/code> 命令可查看当前所处路径，&lt;code>$ ls&lt;/code> 命令查看当前目录下的可见文件，&lt;code>$ cd&lt;/code> 命令切换所处目录。&lt;/p>
&lt;/blockquote>
&lt;h2 id="部署前端项目">部署前端项目&lt;/h2>
&lt;h3 id="安装-nginx">安装 NGINX&lt;/h3>
&lt;p>为什么要使用 &lt;a href="https://www.nginx.com">NGINX&lt;/a>？因为前端项目是纯静态的资源，要大家都能通过浏览器去访问它，就需要一个 Web 服务器，NGINX 正是一个合适且优异的免费开源高性能 HTTP 服务器和反向代理服务器。实际上之前我司安排过 NGINX 的相关学习任务，但学习内容还是偏概念性，现在实践终于来了～&lt;/p>
&lt;p>首先明确&lt;strong>服务器版本为 CentOS 8&lt;/strong>，因为在 CentOS 8 中安装 NGINX 的方法与在 CentOS 7 中安装的有些差异，CentOS 8 中的默认软件包管理工具是 &lt;code>dnf&lt;/code>，它是 &lt;code>yum&lt;/code> 的下一代版本（但 CentOS 8 中没有直接弃用 &lt;code>yum&lt;/code>），安装 NGINX 只需要一个命令：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">dnf&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">nginx&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>因为我直接使用 root 用户身份登陆的服务器，所以安装命令不需要添加 &lt;code>sudo&lt;/code>，设置非 root 用户身份并配置 sudo 特权可参考 &lt;a href="https://www.digitalocean.com/community/tutorials/initial-server-setup-with-centos-8">Initial Server Setup with CentOS 8&lt;/a> 或我的另一篇文章 &lt;a href="../linux-tips/#%E9%85%8D%E7%BD%AE%E9%9D%9E-root-%E7%94%A8%E6%88%B7">Linux 的些个操作&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;p>然后在浏览器中访问服务器的公网 IP 就可以看到 NGINX 的欢迎界面啦🎉：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/ecs-sever:nginx.png" alt="nginx.png" title="NGINX 启动成功">&lt;/p>
&lt;h3 id="管理-nginx">管理 NGINX&lt;/h3>
&lt;p>其实我没想让这篇文章也成为 NGINX 的安装使用教程，无奈自己在这方面也几乎是小白，就权当作学习记录罢了……&lt;/p>
&lt;p>Systemd 是 Linux 的自带系统工具，用来管理系统进程，若进一步学习推荐阮一峰老师的 &lt;a href="http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html">Systemd 入门教程&lt;/a>。NGINX 进程也是通过 Systemd 的命令来管理的，以下是常用的一些操作：&lt;/p>
&lt;p>&lt;strong>1. 停止运行 NGINX&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">systemctl&lt;/span> &lt;span class="n">stop&lt;/span> &lt;span class="n">nginx&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 启动 NGINX&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">systemctl&lt;/span> &lt;span class="n">start&lt;/span> &lt;span class="n">nginx&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 重启 NGINX&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">systemctl&lt;/span> &lt;span class="n">restart&lt;/span> &lt;span class="n">nginx&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 在不停止运行的情况下重新加载配置&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">systemctl&lt;/span> &lt;span class="n">reload&lt;/span> &lt;span class="n">nginx&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="配置-nginx">配置 NGINX&lt;/h3>
&lt;p>要部署的项目文件一般都位于 &lt;code>/var/www/&lt;/code> 目录下，先创建一个示例网站的目录：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">mkdir&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">p&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">www&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">zander&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">html&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;code>-p&lt;/code> 选项表示确保每个层级的目录都存在，不存在则自动创建。&lt;/p>
&lt;/blockquote>
&lt;p>也就是说，网站要显示的内容就取决于此目录下的文件，那文件呢？创建呗～我是比较习惯使用 nano 文本编辑器的，简单好用，先安装：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">dnf&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">nano&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后创建网站主要内容的 HTML 文件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">nano&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">www&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">zander&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">html&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">index.html&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在文件中加入内容，可以外部复制后在 nano 中使用 &lt;code>cmd + v&lt;/code> 直接粘贴：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Zander&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Hello, I am Zander!&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Welcome to my site.&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后使用 &lt;code>control + x&lt;/code> 关闭文件，nano 会提示是否保存内容，输入 y 后按 Enter 键确认保存。&lt;/p>
&lt;p>Linux 中几乎所有的配置文件都位于 &lt;code>/etc&lt;/code> 目录中，而 NGINX 的所有配置文件都位于 &lt;code>/etc/nginx/&lt;/code> 目录，&lt;code>/etc/nginx/nginx.conf&lt;/code> 是 NGINX 全局配置文件，但 NGINX 服务中通常不止托管一个网站，当有多个网站时常用的做法是在 &lt;code>/etc/nginx/conf.d/&lt;/code> 目录下创建每个网站相应的配置文件，如：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">nano&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">nginx&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">conf.d&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">zander.conf&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后编辑配置的具体内容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-py" data-lang="py">&lt;span class="n">server&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1"># 监听 ipv4 端口&lt;/span>
&lt;span class="n">listen&lt;/span> &lt;span class="mi">80&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1"># 监听 ipv6 端口&lt;/span>
&lt;span class="n">listen&lt;/span> &lt;span class="p">[::]:&lt;/span>&lt;span class="mi">80&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1"># 项目文件根目录&lt;/span>
&lt;span class="n">root&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">www&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">zander&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">html&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1"># 默认显示页&lt;/span>
&lt;span class="n">index&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">html&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">htm&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">nginx&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">debian&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">html&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1"># 地址，没有域名可先不设置，默认为公网 IP&lt;/span>
&lt;span class="n">server_name&lt;/span> &lt;span class="n">_&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1"># 请求的 URL 过滤&lt;/span>
&lt;span class="n">location&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="n">try_files&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="n">uri&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="n">uri&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="mi">404&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>还有关键的一步不能忽略，我在这块儿就被坑惨了😢，虽然全局配置文件中已经通过 &lt;code>include /etc/nginx/conf.d/*.conf;&lt;/code> 引入了我们自己添加的配置文件，但其实全局配置中默认是存在一个 server 配置的，如果不删除这个配置它就会覆盖我们自定义的配置，所以，请无情地&lt;strong>将 &lt;code>/etc/nginx/nginx.conf&lt;/code> 文件中的 &lt;code>server { ... }&lt;/code> 这段全部删掉&lt;/strong>。&lt;/p>
&lt;p>修改完成后使用 &lt;code>$ nginx -t&lt;/code> 可检查 NGINX 的各文件中是否有语法错误，然后使用 &lt;code>$ systemctl restart nginx&lt;/code> 重启服务。&lt;/p>
&lt;p>最后，快乐地在浏览器访问公网 IP 就可以看到网站的内容了：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/ecs-server:test-site.png" alt="test-site.png" title="网站内容">&lt;/p>
&lt;h3 id="部署-vue-项目">部署 Vue 项目&lt;/h3>
&lt;p>上面..简单..介绍了一个 HTML 文件的部署，其实 Vue 项目部署的原理和这是完全相同的，两个基本的步骤：一、在服务器创建网站内容；二、配置 NGINX。&lt;/p>
&lt;p>&lt;strong>1. 打包上传 Vue 项目&lt;/strong>&lt;/p>
&lt;p>因为 Vue CLI 基于 Webpack 构建，所以使用 Vue CLI 创建的项目默认都有一个打包构建的命令——&lt;code>$ npm run build&lt;/code>，执行完后会生成一个 &lt;strong>dist 目录&lt;/strong>，这个目录和上例中的 &lt;code>/var/www/zander/html&lt;/code> 一样，都是用于存放网站的内容，不同的是现在需要将本地的这个文件上传到服务器的 &lt;code>/var/www/&lt;/code> 目录下（更常见的做法应是将代码存储仓库的项目上传至服务器部署，也就是在服务器上使用 Git）。&lt;/p>
&lt;p>Linux 中的 &lt;code>scp&lt;/code> 命令可用来传输文件，macOS 中也是可以直接使用的，将打包构建好的 dist 目录上传至服务器：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="c1"># 项目根目录下执行&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">scp&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">r&lt;/span> &lt;span class="n">dist&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="o">@&amp;lt;&lt;/span>公网 &lt;span class="n">IP&lt;/span> 地址&lt;span class="o">&amp;gt;:/&lt;/span>&lt;span class="n">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">www&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">vue&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;code>-r&lt;/code> 表示复制目录，递归传输目录内所有的文件，&lt;code>:&lt;/code> 后为服务器中的存放位置。&lt;/p>
&lt;/blockquote>
&lt;p>然后输入实例密码开始传输，传输过程如下：&lt;/p>
&lt;pre>&lt;code># 传输过程
favicon.ico 100% 4286 136.5KB/s 00:00
index.html 100% 776 26.6KB/s 00:00
app.8aaa5807.css 100% 428 14.3KB/s 00:00
app.3b5b763e.js.map 100% 27KB 472.7KB/s 00:00
about.79329b10.js.map 100% 1350 43.2KB/s 00:00
chunk-vendors.ae64d4e9.js 100% 92KB 891.5KB/s 00:00
app.3b5b763e.js ` 100% 6073 186.9KB/s 00:00
about.79329b10.js 100% 455 14.7KB/s 00:00
chunk-vendors.ae64d4e9.js.map 100% 464KB 3.6MB/s 00:00
logo.82b9c7a5.png 100% 6849 213.9KB/s 00:00
&lt;/code>&lt;/pre>&lt;p>&lt;strong>2. 配置 NGINX&lt;/strong>&lt;/p>
&lt;p>上传完成后在服务器中就能看到对应的目录文件了，如上面所说，NGINX 托管部署的每个网站在 &lt;code>/etc/nginx/conf.d/&lt;/code> 目录下都应有一个自己的配置文件，创建之：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">nano&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">etc&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">nginx&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">conf.d&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">vue&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test.conf&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>编辑内容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-py" data-lang="py">&lt;span class="n">server&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="n">listen&lt;/span> &lt;span class="mi">80&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="n">listen&lt;/span> &lt;span class="p">[::]:&lt;/span>&lt;span class="mi">80&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="n">root&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">www&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">vue&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="n">index&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">html&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">htm&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">nginx&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">debian&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">html&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="n">server_name&lt;/span> &lt;span class="n">_&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="n">location&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="n">try_files&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="n">uri&lt;/span> &lt;span class="err">$&lt;/span>&lt;span class="n">uri&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="mi">404&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>server_name&lt;/code> 仍保持默认则会..覆盖..之前的网站内容，即现在还是通过公网 IP 地址来访问部署的 Vue 项目。重启 Nginx 服务后部署成功，熟悉的页面😉：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/ecs-server:deploy-vue.png" alt="deploy-vue.png" title="部署成功的 Vue 项目">&lt;/p>
&lt;h2 id="部署后端项目">部署后端项目&lt;/h2>
&lt;p>Node.js 项目部署到服务器的目的是让其&lt;strong>持续提供 Web 服务&lt;/strong>，其实原理很简单，我们在本地启动项目就只能在本地访问，而在服务器部署后一是能保证项目..持续..运行，二是可通过 ..IP.. 来访问项目提供的 Web 服务。&lt;/p>
&lt;h3 id="安装-nodejs">安装 Node.js&lt;/h3>
&lt;p>CentOS 8 中..快捷..安装 Node.js 和其包管理工具 NPM 的方式有两种，从 CentOS 储存库安装或使用 NVM 安装，这里只介绍第一种方式。输入以下命令查看储存库中可安装的 Node.js 版本列表：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">yum&lt;/span> &lt;span class="n">module&lt;/span> &lt;span class="n">list&lt;/span> &lt;span class="n">nodejs&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>应该是可以看到一个稳定版本（v10.x）和一个开发版本（v12.x），安装默认版本的 Node.js，同时 NPM 也会被自动安装：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">yum&lt;/span> &lt;span class="n">module&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">nodejs&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>使用 &lt;code>$ node --version&lt;/code> 可查看安装的 Node.js 版本。&lt;/p>
&lt;h3 id="安装-pm2">安装 PM2&lt;/h3>
&lt;p>说是部署，其实就是在服务器上运行罢了，但主要保证的就是..持续..运行，所以还需要用到 &lt;a href="https://pm2.keymetrics.io/">PM2&lt;/a> 进行&lt;strong>进程守护&lt;/strong>以保证项目不间断运行。使用 NPM 全局安装 PM2：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">pm2&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">g&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>鉴于篇幅原因此处不再详细介绍 PM2 具体使用方法，官方文档很清晰啦～&lt;/p>
&lt;/blockquote>
&lt;h3 id="部署-node-项目">部署 Node 项目&lt;/h3>
&lt;p>使用 express-generator 简单起一个项目，将项目从本地上传至服务器，注意需要&lt;strong>删掉 node_modules 文件夹&lt;/strong>不要上传：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="c1"># 项目的父级目录下执行&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">scp&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">r&lt;/span> &lt;span class="n">deploy&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">node&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="o">@&amp;lt;&lt;/span>公网 &lt;span class="n">IP&lt;/span> 地址&lt;span class="o">&amp;gt;:/&lt;/span>&lt;span class="n">opt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>进入服务器的项目目录下安装依赖：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">cd&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">opt&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">test&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>注意咯，现在还不能直接运行项目，因为项目的默认端口是 3000，但服务器的 3000 端口还没有对外开放，所以需要先去服务器管理控制台给实例添加一条 3000 端口的安全组规则。&lt;/p>
&lt;p>然后使用 PM2 启动项目：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="c1"># 项目根目录下执行&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">pm2&lt;/span> &lt;span class="n">start&lt;/span> &lt;span class="n">./bin&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">www&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>最后，在浏览器输入 &lt;code>&amp;lt;公网 IP 地址&amp;gt;:3000&lt;/code> 可访问到 Web 服务🎉：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/ecs-server:deploy-node.png" alt="deploy-node.png" title="部署成功的 Node.js 项目">&lt;/p>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-centos-8">How to Install Nginx on CentOS 8 | DigitalOcean&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://linuxize.com/post/how-to-install-node-js-on-centos-8/">How to Install Node.js and npm on CentOS 8 | Linuxize&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>图源&lt;a href="http://cn.linux.vbird.org/linux_basic/0210filepermission_3.php#dir_tree">《鸟哥的 Linux 私房菜》&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/ecs-server/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/linux/">Linux</category><category domain="https://xuezenghui.com/tags/vue.js/">Vue.js</category><category domain="https://xuezenghui.com/tags/node.js/">Node.js</category></item><item><title>从浏览器工作原理探闭包</title><link>https://xuezenghui.com/posts/closure-from-browser/</link><guid isPermaLink="true">https://xuezenghui.com/posts/closure-from-browser/</guid><pubDate>Tue, 31 Mar 2020 16:43:41 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>历时一个多月，终于学习完了早就应该掌握的浏览器工作原理知识&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>，受益匪浅的地方主要在于两点：&lt;/p>
&lt;ol>
&lt;li>从原理上理解了编程中常见的技巧及误区（知其然也知其所以然）&lt;/li>
&lt;li>扩展了视野，能从更高更广的维度审视页面（百尺竿头更进一步）&lt;/li>
&lt;/ol>
&lt;p>闭包这个概念在前端眼中丝毫不陌生，倒不是因为它的使用率有多么高，而是它的运作和原理关联到了 ECMAScript 的多个概念，如作用域、作用域链、GC&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> 等，恰恰如此，它也是前端面试中必不可少的考点，同时也是被大家讲烂了的东西。那为什么我还要写？因为从底层的角度来窥探闭包的真面目简直不要太清晰😎。&lt;/p>
&lt;h2 id="作用域">作用域&lt;/h2>
&lt;p>要说闭包就要说到作用域链，而要说作用域链就要说说作用域了。&lt;/p>
&lt;p>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Glossary/Scope">作用域&lt;/a>指在程序中定义变量的区域，而这个区域决定了变量的生命周期。换句话说，&lt;strong>作用域控制着变量和函数的可见性和生命周期&lt;/strong>。&lt;/p>
&lt;p>ES6 之后 JavaScript 有三种作用域：&lt;/p>
&lt;p>&lt;strong>1. 全局作用域&lt;/strong>&lt;/p>
&lt;p>全局作用域即使用 &lt;code>var&lt;/code> 定义的变量&lt;sup id="fnref:3">&lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>，在代码中任何地方都可以访问到，它的生命周期就是整个页面的生命周期。&lt;/p>
&lt;p>&lt;strong>2. 函数作用域&lt;/strong>&lt;/p>
&lt;p>函数作用域就是定义在函数内部的变量或函数，而且定义的变量和函数只能在函数内部访问，函数执行结束后这些变量都会被销毁。&lt;/p>
&lt;p>&lt;strong>3. 块级作用域&lt;/strong>&lt;/p>
&lt;p>块级作用域就是使用一对大括号包裹的区域，如函数、判断语句、循环语句等都是块级作用域，块级作用域中的变量和函数只能在其内部访问使用。而 ES6 之前只能使用 &lt;code>var&lt;/code> 定义变量，并不支持块级作用域：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kd">var&lt;/span> &lt;span class="nx">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">a&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 输出2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>ES6 中加入了 &lt;code>let&lt;/code> 和 &lt;code>const&lt;/code> 关键字，JavaScript 才终于有了块级作用域，才得以解决变量提升带来的种种问题✌：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kd">var&lt;/span> &lt;span class="nx">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">a&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 输出1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="作用域链">作用域链&lt;/h2>
&lt;p>在我最早学习 JavaScript 的时候看到了一个很贴切的例子，至今还躺在我的笔记本里，是这么解释作用域链的：&lt;/p>
&lt;blockquote>
&lt;p>假如你家四世同堂，你、你爸爸、你爷爷、你太爷爷，这时候你需要一笔钱买辆车，当然你自己是没有的😑，于是你问你老爸要，你老爸没有，然后又问你爷爷要，你爷爷也没有，最后问你太爷爷要，你太爷爷也没有，那就是真没有了。&lt;/p>
&lt;/blockquote>
&lt;p>例子中需要的「钱」就是作用域中的变量，而一代一代的关系指的是内外嵌套的作用域关系，递进的..链式结构..即是作用域链。的确，对于最粗浅的函数嵌套函数式的作用域链来说以这个例子来辅助理解是合理的，但是如果碰到了这样的情况就需要结合底层原理解释了：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kd">function&lt;/span> &lt;span class="nx">bar&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">myName&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">foo&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">myName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Tom&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">bar&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">myName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">foo&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>来分析一下当执行到 &lt;code>bar()&lt;/code> 函数时整段代码的调用栈&lt;sup id="fnref:4">&lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>情况：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/closure:call-stack.png" alt="call-stack.png" title="调用栈情况">&lt;/p>
&lt;p>可以看到执行到 &lt;code>bar()&lt;/code> 函数内部时两个地方存在 &lt;code>myName&lt;/code> 变量——&lt;code>foo()&lt;/code> 函数的执行上下文&lt;sup id="fnref:5">&lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>中和全局执行上下文中，那么 &lt;code>bar()&lt;/code> 中要输出的 &lt;code>myName&lt;/code> 应该选择哪个呢？&lt;/p>
&lt;p>当代码中使用一个变量时，会先在当前的执行上下文中查找该变量，比如上例中会先在 &lt;code>bar()&lt;/code> 函数内部查找是否存在 &lt;code>myName&lt;/code>，显然是没有的。但是，在每个执行上下文的变量环境中都存在一个&lt;strong>外部引用 outer&lt;/strong>，outer 总是指向当前执行上下文的..外部..执行上下文，如果在当前的执行上下文中没有找到需要的变量会沿着 outer 所指向的执行上下文继续查找，上例中的「外部执行上下文」即全局上下文，所以 &lt;code>myName&lt;/code> 即为 &lt;code>Zander&lt;/code>，而这个&lt;strong>通过 outer 连接的链条就是作用域链&lt;/strong>。&lt;/p>
&lt;p>但为什么 &lt;code>bar()&lt;/code> 是在 &lt;code>foo()&lt;/code> 函数内部执行的还访问不到 &lt;code>foo()&lt;/code> 函数中的变量呢？这是因为&lt;strong>词法作用域&lt;/strong>的存在：&lt;strong>作用域是由代码中函数声明的位置决定的，和函数在什么时候调用是无关的，按照声明时的结构，内部函数可以访问外部函数中的变量&lt;/strong>。&lt;/p>
&lt;p>为了更进一步理解，改变一下代码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="p">{&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">bar&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">myName&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">foo&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">myName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Tom&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">bar&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">myName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Bob&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">myName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">foo&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这里通过一对大括号创建了一个块级作用域，块级作用域中通过 &lt;code>let&lt;/code> 关键字又定义了一个 &lt;code>myName&lt;/code>，此时，&lt;code>bar()&lt;/code> 和 &lt;code>foo()&lt;/code> 的函数执行上下文中的 outer 指向的就是这个..外部..的块级作用域的执行上下文了，块级作用域的执行上下文中的 outer 才指向的是全局执行上下文，所以 &lt;code>bar()&lt;/code> 的输出结果为 &lt;code>Bob&lt;/code>（不信你删掉 &lt;code>let&lt;/code> 那行试试结果是不是 &lt;code>Zander&lt;/code>😛～）。&lt;/p>
&lt;blockquote>
&lt;p>需要注意的是通过 &lt;code>let&lt;/code> 和 &lt;code>const&lt;/code> 定义的变量会存放于执行上下文的..词法环境..中，而在一个执行上下文中查找变量的规则是：&lt;strong>先沿着词法环境的栈顶向下查询，找到则返回，找不到则继续在变量环境中查找&lt;/strong>。&lt;/p>
&lt;/blockquote>
&lt;h2 id="闭包">闭包&lt;/h2>
&lt;p>先来看段代码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kd">function&lt;/span> &lt;span class="nx">foo&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">bar&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">bar&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 报错：name is not defined
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">hello&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">foo&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="nx">hello&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 输出：Zander
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>根据作用域及作用域链的概念就可知 &lt;code>foo()&lt;/code> 函数外获取不到函数内定义的 &lt;code>name&lt;/code>，所以第一个输出会报错，那为什么同样是在函数外部的 &lt;code>hello()&lt;/code> 可以获取到 &lt;code>name&lt;/code>？因为&lt;strong>闭包&lt;/strong>（closure）的存在。&lt;/p>
&lt;h3 id="宏观角度">宏观角度&lt;/h3>
&lt;p>从宏观角度来看，产生闭包的本质有两点——&lt;strong>词法作用域&lt;/strong>和&lt;strong>函数当作值传递&lt;/strong>，函数当作值传递很简单，就像上面代码中 &lt;code>foo()&lt;/code> 函数将 &lt;code>bar()&lt;/code> 这个内部函数当作值返回了，此时，这个返回的值就相当于一个可以访问这个函数【&lt;code>bar()&lt;/code>】词法作用域中的变量【&lt;code>name&lt;/code>】的&lt;strong>通道&lt;/strong>，通过这个通道获取到的所有变量就是&lt;strong>闭包&lt;/strong>。&lt;/p>
&lt;p>换言之，当一个外部函数返回一个内部函数后，即使外部函数执行结束了，内部函数中使用的外部函数的变量依然保存在内存中，把这些变量的集合就叫做闭包。&lt;/p>
&lt;blockquote>
&lt;p>举个例子，我家花钱的方式是我妈让我去买菜，而且我家只买菜，也就是说如果别人想拿到我家的钱，就得我妈让我去买菜，然后从我这里拿到我家的钱。&lt;/p>
&lt;p>「我家」就是一个局部作用域，「钱」就是作用域中的内部变量，外部只能通过「我」这个作用域中返回的函数来获取作用域中的内部变量。&lt;/p>
&lt;/blockquote>
&lt;h3 id="微观角度">微观角度&lt;/h3>
&lt;p>上面的文字可能还是比较晦涩，那就从底层原理的角度再分析，当执行到 &lt;code>return bar&lt;/code> 时调用栈是这样的：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/closure:return-call-stack.png" alt="return-call-stack.png" title="执行到 return bar 时的调用栈情况">&lt;/p>
&lt;p>词法作用域规定内部函数总是可以访问外部函数的变量，所以任何时候在 &lt;code>bar()&lt;/code> 函数中都可以获取到 &lt;code>name&lt;/code>，正因为如此，当把 &lt;code>foo()&lt;/code> 函数的执行结果 &lt;code>bar&lt;/code> 赋值给 &lt;code>hello&lt;/code> 时，虽然 &lt;code>foo()&lt;/code> 函数已经执行完毕，但其内的变量 &lt;code>name&lt;/code> 并没有被销毁掉，仍可以通过 &lt;code>bar&lt;/code> 直接或间接访问。&lt;code>foo()&lt;/code> 函数执行完后调用栈情况：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/closure:closure-call-stack.png" alt="closure-call-stack.png" title="foo 函数执行完后的调用栈情况">&lt;/p>
&lt;p>当 &lt;code>foo()&lt;/code> 函数执行完后其执行上下文就会从调用栈弹出，但 &lt;code>name&lt;/code> 变量还保存于内存中，特殊的是，只能通过 &lt;code>foo&lt;/code> 返回的 &lt;code>bar&lt;/code> 才能访问它。可以通过 Chrome 开发者工具的 Soures 面板打断点来查看闭包：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/closure:chrome-debug.png" alt="chrome-debug.png" title="Chrome 开发者工具中的闭包">&lt;/p>
&lt;p>闭包在作用域链中所处的位置也很明显：Local（当前执行上下文）➡️Closure（闭包）➡️Global（全局执行上下文）。&lt;/p>
&lt;h3 id="内存泄漏">内存泄漏&lt;/h3>
&lt;p>与闭包常常一同谈起的还有一个词——&lt;strong>内存泄漏&lt;/strong>，内存泄漏是指不再使用的变量没有及时地释放，内存没有合理地被回收。但其实在现代浏览器中闭包通常不会导致内存泄漏，只有..滥用..闭包使得很多变量都被保存在内存中无法释放才容易导致内存泄漏。&lt;/p>
&lt;p>合理使用闭包，从容应对面试～&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>具体的学习内容是极客时间的&lt;a href="https://time.geekbang.org/column/intro/216">浏览器工作原理与实践&lt;/a>课程，需要资源请留言。 &lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>Garbage Collecation，垃圾回收机制。 &lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3" role="doc-endnote">
&lt;p>虽然不使用任何关键字定义的隐式全局变量也存在于全局作用域，但这种“野路子”并不在考虑范畴之内，原因参 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/var">MDN&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4" role="doc-endnote">
&lt;p>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Glossary/Call_stack">Call stack&lt;/a>，用来管理函数调用关系的一种数据结构。 &lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5" role="doc-endnote">
&lt;p>&lt;a href="http://ecma-international.org/ecma-262/6.0/#sec-execution-contexts">执行上下文&lt;/a>是 JavaScript 执行一段代码时的运行环境。 &lt;a href="https://xuezenghui.com/posts/closure-from-browser/#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/javascript/">JavaScript</category></item><item><title>用 GraphQL 玩 GitLab API</title><link>https://xuezenghui.com/posts/gitlab-graphql-api/</link><guid isPermaLink="true">https://xuezenghui.com/posts/gitlab-graphql-api/</guid><pubDate>Thu, 12 Mar 2020 10:26:14 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>其实这篇文章的题目让我纠结了许久，在我浅显地看了 &lt;a href="https://xuezenghui.com/posts/graphql/">GraphQL&lt;/a> 和 &lt;a href="https://xuezenghui.com/posts/use-gitlab-api/">GitLab API&lt;/a> 之后我司终于布置了相关的开发任务，这才发觉「纸上得来终觉浅」。没错，这更偏向于是一篇 GraphQL + GitLab API 的实践，也算是 Vue Apollo 的一个进阶和 GitLab API 的一次扩展。总之，这次实践既打破了自己之前文章中的一些观点（打脸），也是对之前知识点的一个补充，更是记录自己第一次的 GraphQL 从理论到实战🤟。&lt;/p>
&lt;h2 id="项目初始化">项目初始化&lt;/h2>
&lt;h3 id="客户端">客户端&lt;/h3>
&lt;p>Vue 中采用 &lt;a href="https://vue-apollo.netlify.com/zh-cn/">Vue Apollo&lt;/a> 来集成 GraphQL 服务，安装使用 Vue Apollo 的方式有三种：&lt;/p>
&lt;p>&lt;strong>1. Vue CLI 插件&lt;/strong>&lt;/p>
&lt;p>Vue Apollo 的开发者 &lt;a href="https://github.com/Akryum">Guillaume Chau&lt;/a> 制作的 &lt;a href="https://vue-cli-plugin-apollo.netlify.com/">Vue CLI 插件&lt;/a>，一行命令就可安装 Apollo——&lt;code>$ vue add apollo&lt;/code>，安装成功后 Apollo 的相关配置都位于自动生成的 &lt;code>vue-apollo.js&lt;/code> 文件中，需要做的配置可参考&lt;a href="https://xuezenghui.com/posts/graphql/#%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%AE%9E%E7%8E%B0-vue-apollo">此文&lt;/a>。&lt;/p>
&lt;p>&lt;strong>2. Apollo Boost&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span> &lt;span class="n">vue&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">apollo&lt;/span> &lt;span class="n">graphql&lt;/span> &lt;span class="n">apollo&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">boost&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>一种折中的安装方式，apollo-boost 中包含了 Apollo 的一些核心包：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.apollographql.com/docs/react/">&lt;code>apollo-client&lt;/code>&lt;/a>：Apollo 客户端&lt;/li>
&lt;li>&lt;a href="https://www.apollographql.com/docs/react/caching/cache-configuration/">&lt;code>apollo-cache-inmemory&lt;/code>&lt;/a>：官方推荐的缓存包&lt;/li>
&lt;li>&lt;a href="https://www.apollographql.com/docs/link/links/http/">&lt;code>apollo-link-http&lt;/code>&lt;/a>：用于通过 HTTP 链接获取 GraphQL 服务器的数据&lt;/li>
&lt;li>&lt;a href="https://www.apollographql.com/docs/link/links/error/">&lt;code>apollo-link-error&lt;/code>&lt;/a>：用于检查和处理 GraphQL API 的错误&lt;/li>
&lt;li>&lt;a href="https://github.com/apollographql/graphql-tag">&lt;code>graphql-tag&lt;/code>&lt;/a>：用于在 JavaScript 中使用模版字符串的方式编写 GraphQL 查询&lt;/li>
&lt;/ul>
&lt;p>安装成功后只需要在 &lt;code>main.js&lt;/code> 中添加一个 ApolloClient 实例来指定 GraphQL 入口：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">ApolloClient&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;apollo-boost&amp;#39;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">apolloClient&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">ApolloClient&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">uri&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;https://git.zander.com/api/graphql&amp;#39;&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此处的 uri 为示例 GitLab API 请求入口，如果你有 GitLab 账号可先参考&lt;a href="https://xuezenghui.com/posts/use-gitlab-api/">此文章&lt;/a>进行 GitLab 授权认证的相关操作，但是有一个&lt;strong>坑点&lt;/strong>是需要注意的，GitLab Application 的回调地址中..不能..有 &lt;code>#&lt;/code> 符号，冲突在于 Vue 中如果使用 hash 模式的 Vue Router 则路径中必定包含 &lt;code>#&lt;/code> 符号，所以在创建 Vue 项目时应&lt;strong>采用 History 模式的路由&lt;/strong>。&lt;/p>
&lt;p>&lt;strong>3. 自行安装&lt;/strong>&lt;/p>
&lt;p>没错，就是把 Apollo Boost 中包含的所有包及其它依赖包手动安装：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span> &lt;span class="n">vue&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">apollo&lt;/span> &lt;span class="n">graphql&lt;/span> &lt;span class="n">apollo&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">client&lt;/span> &lt;span class="n">apollo&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">link&lt;/span> &lt;span class="n">apollo&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">link&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">http&lt;/span> &lt;span class="n">apollo&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">inmemory&lt;/span> &lt;span class="n">graphql&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">tag&lt;/span> &lt;span class="n">axios&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>其中 &lt;a href="https://www.apollographql.com/docs/link/">apollo-link&lt;/a> 是发送 GraphQL 请求或者获取 GraphQL 数据时的中间件，可进行请求的授权、错误处理等，是 &lt;code>apollo-link-http&lt;/code> 的依赖包。&lt;code>axios&lt;/code> 就不用多说了哈～&lt;/p>
&lt;p>为了更清晰地明确各个配置项的作用，这次呢，就手动安装相关依赖，然后进行相关的配置：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>首先建立一个与 &lt;code>main.js&lt;/code> 同级的 Apollo 配置文件 &lt;code>vue-apollo.js&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">ApolloClient&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;apollo-client&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">createHttpLink&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;apollo-link-http&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">InMemoryCache&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;apollo-cache-inmemory&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">VueApollo&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue-apollo&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vue&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vue&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">VueApollo&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 连接 GraphQL 服务
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">httpLink&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">createHttpLink&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">uri&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;https://git.zander.com/api/graphql&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="c1">// 实现缓存
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">cache&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">InMemoryCache&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="c1">// 创建 Apollo 客户端
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">apolloClient&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">ApolloClient&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">link&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">httpLink&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">cache&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="c1">// 创建用于挂载到所有组件中的 Apollo 客户端实例
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">apolloProvider&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">VueApollo&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">defaultClient&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">apolloClient&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="nx">apolloProvider&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>在 &lt;code>mian.js&lt;/code> 中导入并挂载到 Vue 中&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">apolloProvider&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;./vue-apollo&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">new&lt;/span> &lt;span class="nx">Vue&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">router&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">apolloProvider&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">render&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">h&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">h&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}).&lt;/span>&lt;span class="nx">$mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;#app&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>由于 Apollo 的生态实在庞大，不夸张地说，apollo-cache-inmemory、apolo-link 等任意一个模块拉出来分析都可以成为长篇大论，本文就只以应用的层面进行实践，要想了解更多建议研读 &lt;a href="https://www.apollographql.com">Apollo 官网&lt;/a>，当然，文章中传送门也是很贴心的😎。&lt;/p>
&lt;/blockquote>
&lt;h3 id="服务端">服务端&lt;/h3>
&lt;p>了解 GitLab API 的你一定知道为什么需要服务端了——&lt;strong>授权&lt;/strong>，虽然要使用基于 GraphQL 的 GitLab API，但是它的授权与认证方式还是不变的，授权认证原理请参考&lt;a href="https://xuezenghui.com/posts/use-gitlab-api/">此文&lt;/a>。&lt;/p>
&lt;p>使用 express-generator 搭建服务端，安装用于发送请求的依赖&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/gitlab-graphql-api/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span> &lt;span class="n">request&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">promise&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="授权与认证">授权与认证&lt;/h2>
&lt;p>准备工作做好了，接下来开始玩 API 了，授权还是第一步，先来说下思路：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>在进入展示 GitLab 数据的页面时判断 localStorage 中是否有请求 GitLab API 所需要的 Access Token；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>有 Token 则直接进行 GraphQL 查询并展示数据；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>没有 Token 则询问是否去 GitLab 授权（肯定是咯🤷‍♂️）；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>前往 GitLab 授权页面授权，拿到用于获取 Token 的 code；&lt;/p>
&lt;/li>
&lt;li>
&lt;p>返回数据展示的页面，获取 Token，进行 GraphQL 查询并展示数据。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="服务端-1">服务端&lt;/h3>
&lt;p>&lt;strong>1. 用于返回 GitLab 授权 URL 的接口&lt;/strong>&lt;/p>
&lt;p>GitLab 授权页面的 URL 需要后端返回给前端，这样便于 GitLab Applications 的管理（不用在前端改授权 URL 的参数了），这个接口很容易，只是简单的字符串拼接：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="cm">/* GET 获取授权 URL */&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">APP_ID&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// GitLab 应用 ID
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">REDIRECT_URI&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/data&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 重定向 URI
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">STATE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 用于确认请求的字段
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">SCOPE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;api&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 权限
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/codeRequestUrl&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">state&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="nx">STATE&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">501&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">msg&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;非法访问&amp;#34;&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sb">`https://git.zander.com.cn/oauth/authorize?client_id=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">APP_ID&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">&amp;amp;redirect_uri=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">REDIRECT_URI&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">&amp;amp;response_type=code&amp;amp;state=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">state&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">&amp;amp;scope=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">SCOPE&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">result&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">url&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 用 code 获取 Access Token 的接口&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">rp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;request-promise&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://git.zander.com.cn/oauth/token&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 获取 Access Token 的请求 url 地址
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">SECRET&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/token&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">code&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">code&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">bodyData&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">client_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">APP_ID&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">client_secret&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">SECRET&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">code&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">grant_type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;authorization_code&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">redirect_uri&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">REDIRECT_URI&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">url&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">url&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">method&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">headers&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">//设置请求头
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;content-type&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">body&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">bodyData&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">json&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">rp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">options&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">then&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">result&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">data&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">501&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">msg&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">message&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="s1">&amp;#39;请求出错&amp;#39;&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="客户端-1">客户端&lt;/h3>
&lt;p>👌，现在就没服务端啥事儿了，专心做一个页面仔，按照上面的思路来吧～&lt;/p>
&lt;p>&lt;strong>1. 获取 Access Token&lt;/strong>&lt;/p>
&lt;p>首先 Access Token 是存于 localStorage 中的，需要判断其是否存在。优先考虑不存在的情况，也就是用户第一次进入页面时，需要在 &lt;code>mounted&lt;/code> 钩子函数中判断，但是——只判断有没有 Token 就够了吗？如果用户完成了 GitLab 授权自动返回的页面（Redirect URI）还是此页面，那还没等用返回的 code 换 Token 呢就又被拉去授权了，就会陷入死循环，所以判断有没有 Token 的同时还需判断有没有 code，总结一下：&lt;/p>
&lt;ol>
&lt;li>有 Token，发请求，拿数据（已经授权过了也存入 Token 了）&lt;/li>
&lt;li>没 Token 也没 code，去授权，拿 code，换 Token（第一次进入页面）&lt;/li>
&lt;li>没 Token，有 code，换 Token（刚从授权页面返回）&lt;/li>
&lt;/ol>
&lt;p>核心代码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span> &lt;span class="nt">@click&lt;/span>&lt;span class="s">=&amp;#34;goToAuthorize&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="na">前往授权&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">mounted&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getToken&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">methods&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// 获取 Access Token
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="nx">getToken&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">apolloToken&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">localStorage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getItem&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;apollo-token&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">apolloToken&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$route&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">code&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 没有 Token 也没有 code
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dialog&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$route&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">code&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$axios&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/api/token&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">code&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$route&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">code&lt;/span> &lt;span class="p">});&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">200&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">access_token&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">localStorage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setItem&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;apollo-token&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">access_token&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;获取 Access Token 失败&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="c1">// 获取请求 code 的 url
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="nx">goToAuthorize&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dialog&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">params&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">state&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span> &lt;span class="p">};&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$axios&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/api/codeRequestUrl&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">params&lt;/span> &lt;span class="p">});&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">location&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">href&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>&lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;获取 url 失败&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 使用 Access Token&lt;/strong>&lt;/p>
&lt;p>在配置文件 &lt;code>vue-apollo.js&lt;/code> 中给 GraphQL 请求加入 Header：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 获取 GitLab API Token
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">apolloToken&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">localStorage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getItem&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;apollo-token&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">httpLink&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">createHttpLink&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">uri&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;https://git.zander.com.cn/api/graphql&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="nx">headers&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="nx">Authorization&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`Bearer &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">apolloToken&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>
&lt;span class="o">++&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="使用-gitlab-graphql-api-获取数据">使用 GitLab GraphQL API 获取数据&lt;/h2>
&lt;p>如果基本了解 GraphQL 你就会知道，只要有了一个东西，API 使用起来就跟玩似的——&lt;strong>GraphiQL&lt;/strong>，也就是 GraphQL 的 Playground，因为你的所有 GraphQL 接口文档都位于 Playground 中的 &lt;strong>Docs&lt;/strong> 中。这也是 GraphQL 的一个重要特点和魅力体现吧——API 文档完全依赖于代码生成，而不是由开发人员自我发挥。&lt;/p>
&lt;p>&lt;a href="https://docs.gitlab.com/ee/api/graphql/getting_started.html">GitLab GraphQL API&lt;/a> 的 Playground 主要依赖于你的 GitLab 网址，比如 &lt;code>https://git.zander.com.cn/-/graphql-explorer&lt;/code>，只需替换中间的 url 为你的 GitLab url 即可，当然，你也可以先在&lt;a href="https://gitlab.com/-/graphql-explorer">这里&lt;/a>体验一下～&lt;/p>
&lt;h3 id="查询数据">查询数据&lt;/h3>
&lt;p>是的，..数据的查询方式..就是和之前记录的内容有出入的地方，先来康康此实例中的数据查询步骤：&lt;/p>
&lt;p>&lt;strong>1. 编写 gql 查询&lt;/strong>&lt;/p>
&lt;p>&lt;code>src&lt;/code> 目录下新建 &lt;code>/graphql/queries.js&lt;/code> 文件，引入 gql，使用模板字符串语法编写查询语句：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">gql&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;graphql-tag&amp;#39;&lt;/span> &lt;span class="c1">//引入graphql
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">queriesAPI&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">queriesAPI&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">project&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">gql&lt;/span>&lt;span class="sb">`
&lt;/span>&lt;span class="sb"> query project($fullPath: ID!){
&lt;/span>&lt;span class="sb"> project(fullPath: $fullPath){
&lt;/span>&lt;span class="sb"> id,
&lt;/span>&lt;span class="sb"> issues{
&lt;/span>&lt;span class="sb"> nodes{
&lt;/span>&lt;span class="sb"> title
&lt;/span>&lt;span class="sb"> author{
&lt;/span>&lt;span class="sb"> name
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> createdAt
&lt;/span>&lt;span class="sb"> labels{
&lt;/span>&lt;span class="sb"> nodes{
&lt;/span>&lt;span class="sb"> title,
&lt;/span>&lt;span class="sb"> color
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb">}
&lt;/span>&lt;span class="sb">`&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="nx">queriesAPI&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>GitLab GraphQL API 中的大部分查询都需要 &lt;code>fullPath&lt;/code> 参数，即你 GitLab 中的项目地址，如项目完整的 url 为 &lt;code>https://git.zander.com.cn/ZanderXue/gitlab-api-test&lt;/code>，&lt;code>fullPath&lt;/code> 即为 &lt;code>ZanderXue/gitlab-api-test&lt;/code>，问题是没有用来获取全部 &lt;code>fullPath&lt;/code> 的接口，所以这个 &lt;code>fullPath&lt;/code> 参数可能需要手动管理了。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>2. Vue 组件中执行查询&lt;/strong>&lt;/p>
&lt;p>先引入定义好的查询语句：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">queriesAPI&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;../graphql/queries&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>整个 Vue 中都可以使用 apolloProvider 中的全局对象 &lt;code>$apollo&lt;/code>，它是连接 Vue 和 Apollo 的桥梁。比如现在需要执行的查询功能，可使用 &lt;code>this.$apollo.query()&lt;/code> 方法：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">async&lt;/span> &lt;span class="nx">getData&lt;/span>&lt;span class="p">(){&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">res&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$apollo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">query&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">queriesAPI&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">project&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">variables&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">fullPath&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;ZanderXue/gitlab-api-test&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>方法中的 &lt;code>query&lt;/code> 参数为 GraphQL 查询语句，&lt;code>variables&lt;/code> 参数是一个参数对象，更多参数可参考 &lt;a href="https://vue-apollo.netlify.com/zh-cn/api/smart-query.html#%E9%80%89%E9%A1%B9">Vue Apollo 智能查询&lt;/a>。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab-graphql-api:data.png" alt="data.png" title="页面数据渲染">&lt;/p>
&lt;h2 id="一些问题">一些问题&lt;/h2>
&lt;h3 id="踩坑">踩坑&lt;/h3>
&lt;p>在实践过程中发现的 GitLab GraphQL API 的数个坑点：&lt;/p>
&lt;p>&lt;strong>1. GraphQL 的查询默认只能嵌套三层，多于三层则报错&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-gql" data-lang="gql">&lt;span class="py">project&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="py">fullPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;ZanderXue/gitlab-api-test&amp;#34;&lt;/span>&lt;span class="p">){&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nc">issues&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">nodes&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">notes&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">nodes&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">discussion&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">notes&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">nodes&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">author&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">name&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;errors&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Recursive query - too many of fields &amp;#39;{\&amp;#34;nodes\&amp;#34;=&amp;gt;3}&amp;#39; detected in single branch of the query&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>报错内容如上，这也引出了新的问题——错误捕捉及处理，下文会详述 Apollo 的错误处理，但三层嵌套的问题暂无解决方法，只能尽量避免😢。&lt;/p>
&lt;p>&lt;strong>2. 数据中如果某字段没有值，那么那一整条数据都会为 &lt;code>null&lt;/code>&lt;/strong>&lt;/p>
&lt;p>比如获取 project 中参与 issue 评论的用户的头像 url，如果用户没有主动设置头像（使用 GitLab 默认头像），那么用户的其他信息也将获取不到&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-gql" data-lang="gql">&lt;span class="py">project&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="py">fullPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;ZanderXue/gitlab-api-test&amp;#34;&lt;/span>&lt;span class="p">){&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nc">issues&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">nodes&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">notes&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">nodes&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">author&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">username&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="py">avatarUrl&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;project&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;issues&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;nodes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;notes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;nodes&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;author&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ZanderXue&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;username&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;zander.xue&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;avatarUrl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/uploads/-/system/user/avatar/249/avatar.png&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>恕我直言，这一设计真的是有些蠢……只能放弃显示用户头像的需求了，不然难道要求用户必须设置头像😒？&lt;/p>
&lt;p>&lt;strong>3. url 路径问题&lt;/strong>&lt;/p>
&lt;p>估计你也注意到了，上面 &lt;code>avatarUrl&lt;/code> 的路径是不完整的，缺少 &lt;code>fullPath&lt;/code>。不仅如此，所有的图片、文件、链接的路径都是不完整的，导致获取到数据后还需要做二次处理。&lt;/p>
&lt;p>比如我的开发任务中，需要在页面渲染所有的 issue 评论，要用到 &lt;code>notes&lt;/code> 中所有的 &lt;code>bodyHtml&lt;/code> 字段（String 类型），就需要把字符串中所有 &lt;code>a&lt;/code> 标签的 &lt;code>href&lt;/code> 属性拼接完整，所有 &lt;code>img&lt;/code> 标签的 &lt;code>src&lt;/code> 属性拼接完整……哦对了，&lt;code>img&lt;/code> 标签返回的是 base64 编码，当然，也是不完整的&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/gitlab-graphql-api/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>🙂，还要将 &lt;code>src&lt;/code> 属性中的路径替换成 &lt;code>data-src&lt;/code> 的路径再拼接完整。&lt;/p>
&lt;p>&lt;strong>4. Access Token 过期处理&lt;/strong>&lt;/p>
&lt;p>GitLab GraphQL API 目前无法对非法或过期的 Access Token 做出对应的反应（如返回不同的状态码或返回错误），而只是让 API 返回的数据为 &lt;code>null&lt;/code>，原因是 GraphQL 的 resolve 函数只能返回固定的 Type 的数据，除非是 GraphQL 类型的错误才可直接返回相应的 &lt;code>error&lt;/code> 信息。&lt;/p>
&lt;p>因此解决方案只能从返回的数据入手了，比如判断是否为 &lt;code>null&lt;/code>，是 &lt;code>null&lt;/code> 则重新获取 Access Token。&lt;/p>
&lt;h3 id="进阶">进阶&lt;/h3>
&lt;p>&lt;strong>1. 错误捕捉及处理&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://www.apollographql.com/docs/link/links/error/">&lt;code>apollo-link-error&lt;/code>&lt;/a> 用来捕捉和处理 GraphQL 类型的错误（GraphQLErrors）或网络类型的错误（newworkErrors）。&lt;/p>
&lt;p>安装：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span> &lt;span class="n">apollo&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">link&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">error&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在 &lt;code>vue-apollo.js&lt;/code> 文件中引入并配置：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">onError&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;apollo-link-error&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">errorLink&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">onError&lt;/span>&lt;span class="p">(({&lt;/span> &lt;span class="nx">graphQLErrors&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">networkError&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">graphQLErrors&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="k">of&lt;/span> &lt;span class="p">)&lt;/span>
&lt;span class="nx">graphQLErrors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(({&lt;/span> &lt;span class="nx">message&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">locations&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">path&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;GraphQL 类型的错误处理&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">);&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">networkError&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;网络类型错误的处理&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">apolloClient&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">ApolloClient&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">link&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">errorLink&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">concat&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">httpLink&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="nx">cache&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>onError&lt;/code> 函数的参数对象包括以下五个属性：&lt;/p>
&lt;ul>
&lt;li>&lt;code>GraphQLErrors&lt;/code>：GraphQL 服务端错误，数组类型&lt;/li>
&lt;li>&lt;code>networkError&lt;/code>：网络类型的错误&lt;/li>
&lt;li>&lt;code>operation&lt;/code>：发生错误的操作&lt;/li>
&lt;li>&lt;code>forward&lt;/code>：一个方法，可以传入 &lt;code>operation&lt;/code>，表示重新执行这次错误的操作&lt;/li>
&lt;li>&lt;code>response&lt;/code>：接口返回的结果&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://www.apollographql.com/docs/link/links/error/#retrying-failed-requests">官方示例&lt;/a>中表示可以利用 &lt;code>apollo-link-error&lt;/code> 检测出认证相关的错误并进行相关处理，但是我尝试了下，Access Token 非法或失效..并没有..被捕捉到并认定为 GraphQLErrors，所以我暂且将其归类到「踩坑」标题下，该模块还需要进一步学习从而完善本文。&lt;/p>
&lt;hr>
&lt;p>🎱案例 GitHub 地址：&lt;a href="https://github.com/Xuezenghuigithub/GitLab-GraphQL-API">GitLab-GraphQL-API&lt;/a>&lt;/p>
&lt;hr>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>request 已被&lt;a href="https://github.com/request/request#deprecated">弃用&lt;/a>，此处使用 &lt;a href="https://github.com/request/request-promise">request-promise&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/gitlab-graphql-api/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>StackOverflow 上也有人提出&lt;a href="https://stackoverflow.com/questions/59249310/gitlab-api-post-base64-image">此问题&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/gitlab-graphql-api/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/graphql/">GraphQL</category><category domain="https://xuezenghui.com/tags/gitlab/">GitLab</category></item><item><title>JSON Web Token</title><link>https://xuezenghui.com/posts/jwt/</link><guid isPermaLink="true">https://xuezenghui.com/posts/jwt/</guid><pubDate>Mon, 02 Mar 2020 05:29:45 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>几乎任何的现代商用网站都离不开..用户认证..，先来一段 Rap 解释一下用户认证——你说你是客户端，名叫小憨憨，你问服务端怎么看，它说这事儿必须验，你证明了自己有点儿憨，它把数据放心传……🤤也就是说，认证是为了&lt;strong>保证服务端把数据安全、正确地传递给客户端&lt;/strong>。&lt;/p>
&lt;p>身份认证的方式有基于 Cookie 的认证，如 Session，还有基于 Token 的认证，最著名也是最常用的就是 &lt;a href="https://jwt.io/">JSON Web Token&lt;/a>（简称 JWT）了。&lt;/p>
&lt;h2 id="json-web-token">JSON Web Token&lt;/h2>
&lt;p>首先要明确的还是概念，JSON Web Token 并不是一项依赖于哪个编程语言的技术，而是一套开放的标准（&lt;a href="https://tools.ietf.org/html/rfc7519">RFC 7519&lt;/a>），是一种技术实现的规范，依赖 JWT 思想规范的各种库才是具体的技术实现，如 &lt;a href="https://github.com/auth0/node-jsonwebtoken">jsonwebtoken&lt;/a>、&lt;a href="https://github.com/auth0/express-jwt">express-jwt&lt;/a> 等。JWT 定义了一种简洁且独立的方式，用于在客户端和服务端之间安全地传输 JSON 格式的数据。&lt;/p>
&lt;h3 id="jwt-原理">JWT 原理&lt;/h3>
&lt;p>&lt;img src="https://xuezenghui.com/images/jwt:flow.png" alt="flow.png" title="JWT 原理">&lt;/p>
&lt;ol>
&lt;li>
&lt;p>客户端用户使用用户名和密码通过 POST 请求登录或注册&lt;/p>
&lt;/li>
&lt;li>
&lt;p>服务端确认用户合法，生成一个 JWT&lt;/p>
&lt;/li>
&lt;li>
&lt;p>将 JWT 返回给客户端，客户端将其保存在本地（一般保存在 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage">localStorage&lt;/a> 中）&lt;/p>
&lt;/li>
&lt;li>
&lt;p>之后客户端向服务端发送 HTTP 请求需要将 JWT 加入请求头中&lt;/p>
&lt;/li>
&lt;li>
&lt;p>服务器解析 JWT，检查是否合法且有效&lt;/p>
&lt;/li>
&lt;li>
&lt;p>根据检查结果对客户端做出响应&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="jwt-结构">JWT 结构&lt;/h3>
&lt;p>服务端生成的 JWT 是一段很长的字符串，由三部分组成，分别为 Header（头部）、Payload（负载）和 Signature（签名），中间用&lt;code>.&lt;/code>分隔：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/jwt:jwt.png" alt="jwt.png" title="JWT">&lt;/p>
&lt;p>&lt;strong>1. Header&lt;/strong>&lt;/p>
&lt;p>头部 Header 是经过 Base64Url&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/jwt/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> 编码的 JSON 对象：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;alg&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;HS256&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;typ&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;JWT&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>其中，&lt;code>alg&lt;/code>是 JWT 所使用的签名算法，默认为&lt;code>HS256&lt;/code>，&lt;code>typ&lt;/code>即 Token 的类型。&lt;/p>
&lt;p>&lt;strong>2. Payload&lt;/strong>&lt;/p>
&lt;p>负载 Payload 是一个包含传递数据的 JSON 对象，同样经过了 Base64Url 编码，包含以下可选属性：&lt;/p>
&lt;ul>
&lt;li>iss：发行人&lt;/li>
&lt;li>sub：主题&lt;/li>
&lt;li>aud：受众群体&lt;/li>
&lt;li>exp：到期时间&lt;/li>
&lt;li>nbf：生效时间&lt;/li>
&lt;li>iat：签发时间&lt;/li>
&lt;li>jti：JWT ID&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>详情请参考 &lt;a href="https://tools.ietf.org/html/rfc7519#section-4.1">Registered Claim Names&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;p>Payload 中的属性也可以自行添加，但由于 JWT 的内容对任何人都可见，除非对 JWT 进行二次加密，否则不能将任何机密的数据写入其中。&lt;/p>
&lt;p>&lt;strong>3. Signature&lt;/strong>&lt;/p>
&lt;p>Signature 签名操作是在获取到了 Header 和 Payload 后进行的，比如 Header 中指定的是 HS256 算法，那么会通过以下方式创建 Signature：&lt;/p>
&lt;pre>&lt;code>HMACSHA256(
base64UrlEncode(header) + &amp;quot;.&amp;quot; +
base64UrlEncode(payload),
secret)
&lt;/code>&lt;/pre>&lt;p>其中的&lt;code>secret&lt;/code>为保存在服务端的密钥，一般需要定期更新。完成签名后就将这三部分拼接在一起返回给客户端：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">JWT&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">Header&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">.&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">Payload&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">.&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">Signature&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="nodejs-中实现-jwt">NodeJS 中实现 JWT&lt;/h2>
&lt;p>JWT 在各种语言中都有实现，如 java-jwt、angular2-jwt、go-jwt-middleware 等，更多的实现可在 &lt;a href="https://auth0.com/docs/">auth0-docs&lt;/a> 中查看，此处以 NodeJS 中的实现 &lt;a href="https://github.com/auth0/node-jsonwebtoken">jsonwebtoken&lt;/a> 为例进行用户的认证。&lt;/p>
&lt;h3 id="初始化项目">初始化项目&lt;/h3>
&lt;p>先使用 &lt;a href="https://github.com/expressjs/generator">express-generator&lt;/a> 创建项目，安装需要的依赖：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">mongoose&lt;/span> &lt;span class="n">jsonwebtoken&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>连接 MongoDB 后添加 users 集合的数据库模型：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">mongoose&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;mongoose&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">Schema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Schema&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">userSchema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Schema&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s2">&amp;#34;username&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">required&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="s2">&amp;#34;password&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">required&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;User&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nx">userSchema&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;users&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="生成-jwt">生成 JWT&lt;/h3>
&lt;p>为了更好地复用，完成一个公共的生成 JWT 方法：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">jwt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;jsonwebtoken&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">secret&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;zander&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">createJWT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">sub&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">exp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">strTimer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">jwt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">jwt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sign&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">user&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 用户名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">sub&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">sub&lt;/span> &lt;span class="c1">// 主题
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="nx">secret&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">expiresIn&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">exp&lt;/span>&lt;span class="si">}${&lt;/span>&lt;span class="nx">strTimer&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span> &lt;span class="c1">// 过期时间
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">})&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">jwt&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>1. 注册接口&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/register&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">password&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">userData&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">password&lt;/span> &lt;span class="p">};&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">saveData&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">userData&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">save&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">jwt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">createJWT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;register&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;h&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">saveData&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">token&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">jwt&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>使用 Postman 测试接口：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/jwt:register.png" alt="register.png" title="注册接口测试">&lt;/p>
&lt;p>&lt;strong>2. 登录接口&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/login&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">password&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">userData&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findOne&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">password&lt;/span> &lt;span class="p">});&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">userData&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">501&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">msg&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;登录信息有误&amp;#39;&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">jwt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">createJWT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;login&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;d&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">userData&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">token&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">jwt&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/jwt:login.png" alt="login.png" title="登录接口测试">&lt;/p>
&lt;h3 id="验证-jwt">验证 JWT&lt;/h3>
&lt;p>服务端生成了 JWT 后不保存，直接发送给客户端，而客户端拿到了 JWT 后将其保存下来，在发送其它请求时需要将 JWT 加入到请求头中：&lt;/p>
&lt;pre>&lt;code>Authorization: Bearer &amp;lt;token&amp;gt;
&lt;/code>&lt;/pre>&lt;p>然后服务端验证 JWT 是否合法且有效：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 验证token方法
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">verifyJWT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">token&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">decoded&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">decoded&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">jwt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">verify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">token&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34; &amp;#34;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nx">secret&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">message&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">decoded&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="c1">// 根据用户名获取密码接口
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/password&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">token&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">headers&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;authorization&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">username&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">token&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">501&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">msg&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Not authorized&amp;#39;&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">info&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">verifyJWT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">token&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">typeof&lt;/span> &lt;span class="nx">info&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">501&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">msg&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">info&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">userData&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findOne&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">username&lt;/span> &lt;span class="p">});&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">userData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">password&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/jwt:jwt-illegal.png" alt="jwt-illegal.png" title="非法 JWT">
&lt;img src="https://xuezenghui.com/images/jwt:jwt-legal.png" alt="jwt-legal.png" title="合法 JWT">&lt;/p>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;a href="https://jwt.io">JSON Web Token | Auth0&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html">JSON Web Token 入门教程 | 阮一峰&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://zhuanlan.zhihu.com/p/27370773">Server 端的认证神器——JWT(一) | 知乎&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://medium.com/@sherryhsu/session-vs-token-based-authentication-11a6c5ac45e4">Session vs Token Based Authentication | Medium&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>Base64Url 算法将 Base64 编码中的特殊字符替换为不影响在 URL 中传输的字符。 &lt;a href="https://xuezenghui.com/posts/jwt/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/jwt/">JWT</category><category domain="https://xuezenghui.com/tags/node.js/">Node.js</category></item><item><title>叹，漫漫</title><link>https://xuezenghui.com/posts/sigh-long-way/</link><guid isPermaLink="true">https://xuezenghui.com/posts/sigh-long-way/</guid><pubDate>Sun, 12 Jan 2020 03:32:06 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;p>脱襁入世已半载，&lt;/p>
&lt;p>时捉襟来时也慷慨。&lt;/p>
&lt;p>友常欢，小君贤，&lt;/p>
&lt;p>何事尤能惹卿烦？&lt;/p>
&lt;p>操劳操劳几多得，&lt;/p>
&lt;p>庆年少，叹前路漫漫，无心快活。&lt;/p></description><category domain="https://xuezenghui.com/categories/life/">Life</category><category domain="https://xuezenghui.com/tags/poetry/">poetry</category></item><item><title>MongoDB bulkWrite() 实操</title><link>https://xuezenghui.com/posts/mongodb-bulkwrite/</link><guid isPermaLink="true">https://xuezenghui.com/posts/mongodb-bulkwrite/</guid><pubDate>Fri, 10 Jan 2020 09:17:05 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="需求">需求&lt;/h2>
&lt;p>一切技术都只是实现需求的载体，先不开门见山，来看一个实际项目中遇到的需求：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/mongodb-bulkwrite:batch-update.png" alt="batch-update.png" title="批量操作">&lt;/p>
&lt;p>要求多选后点击计算按钮发送请求，更新 MongoDB 数据库员工集合中每个所选员工文档的一个字段，而每个文档这个字段要更新的目标值都是..个性化..的，比如 &lt;em>Leon&lt;/em> 的这个字段要更新为 500，&lt;em>Paul&lt;/em> 的要更新为 999，&lt;em>Zander&lt;/em> 的要更新为 100。&lt;/p>
&lt;p>能为此次数据库操作提供的数据是这样的：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Leon&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">value_index&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">500&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Paul&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">value_index&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">999&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Zander&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">value_index&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">100&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">];&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="实现">实现&lt;/h2>
&lt;blockquote>
&lt;p>以下操作数据库的方法都是 MongoDB 提供的，&lt;a href="https://mongoosejs.com/">Mongoose&lt;/a> 实际是进行了移植和支持，两者原理及实现方式相同，为究其本质、探其实操，实例代码使用的是 Mongoose。&lt;/p>
&lt;/blockquote>
&lt;p>你可能会想到使用&lt;a href="https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/#db.collection.updateMany">&lt;code>updateMany()&lt;/code>&lt;/a>，它支持将多个 Document 的字段更新为..同一个值..或者按照..相同的条件..来更新：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">Employee&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">updateMany&lt;/span>&lt;span class="p">(&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">$in&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">employeeNameArray&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">$set&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;value_index&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">500&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>但如我所说，需求要求更新的目标值是个性化的，不同的目标值之间是没有规律的。&lt;/p>
&lt;p>那可否循环访问 MongoDB 一条一条地更新数据呢？否否否否否！虽然它的确能够实现需求，但是这个操作会严重影响数据库性能，&lt;strong>在任何数据库中都要避免循环访问数据库&lt;/strong>。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">Employee&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findOneAndUpdate&lt;/span>&lt;span class="p">(&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;value_index&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">value_index&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">)};&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>主角登场，&lt;a href="https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/">&lt;code>bulkWrite()&lt;/code>&lt;/a>是 &lt;a href="https://github.com/mongodb/docs/tree/v3.2">MongoDB v3.2&lt;/a>中加入的新功能，能以特定的顺序执行 MongoDB 的写入操作，而这里的&lt;a href="https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/#bulkwrite-write-operations">..写入操作..&lt;/a>包括 insert、update、replace 等。使用方法：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">Model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bulkWrite&lt;/span>&lt;span class="p">(&lt;/span>
&lt;span class="p">[&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">operation&lt;/span> &lt;span class="mi">1&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="p">,&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">operation&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="p">,&lt;/span> &lt;span class="p">...],&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">writeConcern&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nb">document&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="p">,&lt;/span>
&lt;span class="nx">ordered&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="kr">boolean&lt;/span> &lt;span class="o">&amp;gt;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>参数一是一个数组，表示一组数据库写操作，支持以下6个操作：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/#insertone">&lt;code>insertOne()&lt;/code>&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">Model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bulkWrite&lt;/span>&lt;span class="p">([&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="nx">insertOne&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;document&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">])&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany">&lt;code>updateOne()&lt;/code>和&lt;code>updateMany()&lt;/code>&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">Model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bulkWrite&lt;/span>&lt;span class="p">([&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="nx">updateOne&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;filter&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="c1">// 查询条件，与 find()方法相同
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;update&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;value_index&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="c1">// 要执行的更新操作，可以是文档，也可以是聚合管道（如$set）
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;upsert&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 可选，Boolean类型，表示如果没有查询到该文档是否执行新增操作，默认为false
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;arrayFilters&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">],&lt;/span> &lt;span class="c1">// 可选，更新文档中的嵌套数组
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;collation&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="c1">// 可选，指定更新操作的排序规则
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="c1">// 可选，指定要更新文档的索引
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">])&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/#deleteone-and-deletemany">&lt;code>deleteOne()&lt;/code>和&lt;code>deleteMany()&lt;/code>&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">Model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bulkWrite&lt;/span>&lt;span class="p">([&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="nx">deleteOne&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;filter&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="s2">&amp;#34;collation&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">])&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/#replaceone">&lt;code>replaceOne()&lt;/code>&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">Model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bulkWrite&lt;/span>&lt;span class="p">([&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="nx">replaceOne&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;filter&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="s2">&amp;#34;replacement&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Handsome&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;value_index&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="c1">// 替换文档
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;upsert&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;collation&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="s2">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">])&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>参数二是一个对象，其中包含两个设置项，&lt;code>writeConcern&lt;/code>和&lt;code>ordered&lt;/code>，&lt;a href="https://docs.mongodb.com/manual/reference/write-concern/">&lt;code>writeConcern&lt;/code>&lt;/a>写入关注用以设置 MongoDB 写入操作关注程度的高低，采用默认即可。&lt;a href="https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/#execution-of-operations">&lt;code>ordered&lt;/code>&lt;/a>用来指定是否按照顺序执行参数一中的写入操作，默认为&lt;code>true &lt;/code>，若设置为无序，MongoDB 会对操作进行重排序以提高性能，具体应取决于你的写入操作是否是有序的。&lt;/p>
&lt;p>使用&lt;code>bulkWrite()&lt;/code>完成上面的需求：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kd">let&lt;/span> &lt;span class="nx">operations&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">updateObj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s1">&amp;#39;updateOne&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s1">&amp;#39;filter&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s1">&amp;#39;_id&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="s1">&amp;#39;update&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s1">&amp;#39;value_index&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">value_index&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="nx">operations&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">updateObj&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="nx">Employee&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bulkWrite&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">operations&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">message&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="s1">&amp;#39;更新出错&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>bulkWrite()&lt;/code>操作的返回值包括：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 此次操作时候成功
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;writeErrors&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[],&lt;/span> &lt;span class="c1">//操作的错误异常
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;writeConcernErrors&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[],&lt;/span> &lt;span class="c1">// 写入关注错误
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;insertedIds&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[],&lt;/span> &lt;span class="c1">// 插入数据的id
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;nInserted&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 直接插入操作计数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;nUpserted&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 更新插入操作计数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;nMatched&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 查询操作计数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;nModified&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 修改操作计数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;nRemoved&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 删除操作计数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;upserted&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[]&lt;/span> &lt;span class="c1">// 更新插入操作的id
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;code>insert()&lt;/code>和&lt;code>upsert()&lt;/code>操作的区别在于前者不需要进行查询操作，直接将数据插入集合中，而后者会根据条件先在集合中查找数据，找到了则更新，找不到再执行插入操作。&lt;/p>
&lt;/blockquote>
&lt;p>还有一点是&lt;code>bulkWrite()&lt;/code>需要使用&lt;code>try..catch&lt;/code>捕获错误，原因是在执行&lt;code>bulkWrite()&lt;/code>过程中，如果遇到了 Write Concern 以外的错误（即不可控的错误），有序操作（&lt;code>ordered&lt;/code>为&lt;code>true&lt;/code>）将在发生错误后..停止..并返回这单个错误，无序条件（&lt;code>ordered&lt;/code>为&lt;code>false&lt;/code>）下则..跳过..遇到的错误..继续处理..操作队列中所有剩余的写操作，最后在 writeErrors 中返回每个错误。&lt;/p></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/mongodb/">MongoDB</category><category domain="https://xuezenghui.com/tags/mongoose/">Mongoose</category></item><item><title>GitLab API 授权认证及使用</title><link>https://xuezenghui.com/posts/use-gitlab-api/</link><guid isPermaLink="true">https://xuezenghui.com/posts/use-gitlab-api/</guid><pubDate>Thu, 26 Dec 2019 10:15:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>通常，GitLab 的操作都是通过 Web 界面或命令行来完成的，但 GitLab 也提供了简单而强大的开放 API 来自动执行 GitLab 相关操作，查看项目信息、提交 Issues、合并分支……统统都在 &lt;a href="https://docs.gitlab.com/ee/api/README.html">docs.gitlab.com&lt;/a>，GitLab API 大多为 REST API，但也同样支持 &lt;a href="https://docs.gitlab.com/ee/api/graphql/">GraphQL API&lt;/a>，并且 v5 版本的 API 将全部基于 GraphQL API，&lt;em>as Facebook does&lt;/em>.&lt;/p>
&lt;h2 id="授权认证">授权认证&lt;/h2>
&lt;p>绝大多数的 GitLab API 都是需要身份验证的，这毋庸置疑，其它公司的内部数据不能随随便便就被获取到，即使是公司内部也会有各个 Group 或 Project 的权限设置。&lt;/p>
&lt;p>再以使用 GitLab API 的目的出发——主要是为了在公司 Portal 网站中..显示.. GitLab 中的项目信息、成员信息、Issues 等，这就要求在用户登录 Portal 网站时获取其 GitLab 内具体权限，以显示其权限对应的 GitLab 数据。问题出来了：Portal 网站如何经过 GitLab 的同意来获取用户数据？&lt;/p>
&lt;p>API 的使用无非 CRUD，按照 GitLab API 清晰完整的文档来就可以了，如何进行身份验证才是重头戏，GitLab API 的身份验证有四种方法：&lt;/p>
&lt;h3 id="1-oauth2-tokenshttpsdocsgitlabcomeeapireadmehtmloauth2-tokens">1. &lt;a href="https://docs.gitlab.com/ee/api/README.html#oauth2-tokens">OAuth2 tokens&lt;/a>&lt;/h3>
&lt;p>&lt;a href="https://zh.wikipedia.org/wiki/%E5%BC%80%E6%94%BE%E6%8E%88%E6%9D%83">OAuth2&lt;/a> 即 OAuth 2.0版本，是一个关于授权的开放网络标准，也是目前应用最广泛的授权认证方式。它的运行流程是这样的：&lt;/p>
&lt;p>&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/use-gitlab-api/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;img src="https://xuezenghui.com/images/gitlab:oauth-workflow.png" alt="oauth-workflow.png" title="AOuth 2.0 Workflow">&lt;/p>
&lt;p>简单来说就是在第三方应用（客户端 Client）需要资源服务器（Resource Server）的数据时，资源拥有者（Resource Owner）同意后..授权..给客户端，客户端向认证服务器（Authorization Server）申请令牌（Token），认证服务器确认后发放令牌，客户端就可以拿着令牌去获取资源服务器的数据了。&lt;/p>
&lt;hr>
&lt;p>这些步骤中最重要的又在于客户端如何得到用户的授权（Authorization Grant）从而拿到令牌（Access Token），OAuth 2.0提供了&lt;a href="https://oauth.net/2/grant-types/">四种授权方式&lt;/a>，其中授权码模式（Authorization Code Grant）是最严密完整的，也是绝大多数网站作为资源服务器时采用的授权方式（包括 GitLab）。授权码模式流程图：&lt;/p>
&lt;p>&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/use-gitlab-api/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;img src="https://xuezenghui.com/images/gitlab:authorization-code.png" alt="authorization-code.png" title="Authorization Code Workflow">&lt;/p>
&lt;p>以 GitLab 的 OAuth2 验证方式解释一下此流程（开发者视角）：&lt;/p>
&lt;p>&lt;strong>第一步、创建应用&lt;/strong>&lt;/p>
&lt;p>在 GitLab Web 界面的 Setting ➡️ Applications 中注册用于提供 OAuth 验证的应用。重定向 URI（Redirect URI）本应设为第三方应用（本例中即为公司 Portal 网站）的线上 URI，处于开发阶段时也可设为本地应用运行后的访问路径，如&lt;code>http://localhost:8080/login&lt;/code>，此重定向 URI 的作用下文会详述。页面还会要求选择 Scopes，表示此应用的..授权范围..，应根据第三方应用的具体需求选择，我选 &lt;strong>api&lt;/strong>，嘿嘿嘿，应用成功创建后会显示其具体信息，包括应用 Id &lt;code>Application Id&lt;/code>、应用密钥&lt;code>Secret&lt;/code>、回调 URL&lt;code>Callback url&lt;/code> 和权限 &lt;code>Scopes&lt;/code>。&lt;/p>
&lt;blockquote>
&lt;p>GitLab 要求 Redirect URI 中不能包含一些特殊字符，如 &lt;code>#&lt;/code>。在 Vue 中如果 vue-router 采用了 hash 模式，就与 Redirect URI 的格式要求冲突了，因此 vue-router 应改为采用 history 模式，详参 &lt;a href="https://router.vuejs.org/zh/guide/essentials/history-mode.html">HTML5 History 模式&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>第二步、请求授权码&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab:eportal-login.png" alt="authorization-code.png" title="Partol 中的授权按钮">&lt;/p>
&lt;p>点击 Portal 中 「GitLab 授权」按钮时使用 &lt;code>location.href&lt;/code> 跳转至授权页面：&lt;/p>
&lt;pre>&lt;code>https://gitlab.zander.com/oauth/authorize?client_id=4e1fe77ba1d43b151428d907574er866a48af8dbc8766ea839a84a88c6dace39&amp;amp;redirect_uri=http://localhost:8080/login&amp;amp;response_type=code&amp;amp;state=zander&amp;amp;scope=api
&lt;/code>&lt;/pre>&lt;p>URI 中的参数包括：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="center">参数&lt;/th>
&lt;th align="center">是否必须&lt;/th>
&lt;th align="center">含义&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="center">client_id&lt;/td>
&lt;td align="center">true&lt;/td>
&lt;td align="center">注册 GitLab 应用成功后的 Application Id&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">redirect_uri&lt;/td>
&lt;td align="center">true&lt;/td>
&lt;td align="center">注册应用时设置的重定向 URI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">response_type&lt;/td>
&lt;td align="center">true&lt;/td>
&lt;td align="center">返回的类型，授权码模式即为&lt;code>code&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">state&lt;/td>
&lt;td align="center">false&lt;/td>
&lt;td align="center">用于确认请求和回调的状态，OAuth 建议以此来防止 &lt;a href="https://owasp.org/www-community/attacks/csrf">CSRF 攻击&lt;/a>&lt;sup id="fnref:3">&lt;a href="https://xuezenghui.com/posts/use-gitlab-api/#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">scope&lt;/td>
&lt;td align="center">false&lt;/td>
&lt;td align="center">权限设置，范围不得超出创建应用时的配置，以空格分隔&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>第三步、用户授权&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab:authorize.png" alt="authorize.png" title="授权页面">&lt;/p>
&lt;p>发送请求后网页会跳转到 GitLab 的授权页面，先要求用户登录 GitLab，然后询问用户是否同意授权🥺。用户点击同意后页面便会返回一个&lt;strong>包含授权码 code 和参数 state（如果你传了它的话）的重定向 URI&lt;/strong> 并跳转至对应的网页，即网页的地址栏变成了这样：&lt;/p>
&lt;pre>&lt;code>http://localhost:8080/login?code=90792302acc2a0724d44c74f43d0fd77f005723c9ae5def965b02675f532949a&amp;amp;state=zander
&lt;/code>&lt;/pre>&lt;p>&lt;strong>第四步、获取令牌 Token&lt;/strong>&lt;/p>
&lt;p>既然拿到了 code，嘿嘿嘿😎，只需要一个 Post 请求就能拿到可任意调用 GitLab API 大军的虎符🐯—— Access Token。要注意的是，获取 Token 的操作是需要在第三方应用的..后台..完成的，以保证数据的安全性。&lt;/p>
&lt;pre>&lt;code>POST https://gitlab.zander.com/oauth/token
&lt;/code>&lt;/pre>&lt;p>参数包括：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="center">参数&lt;/th>
&lt;th align="center">是否必须&lt;/th>
&lt;th align="center">含义&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="center">client_id&lt;/td>
&lt;td align="center">true&lt;/td>
&lt;td align="center">注册应用的 Application Id&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">client_secret&lt;/td>
&lt;td align="center">true&lt;/td>
&lt;td align="center">注册应用的 Secret&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">code&lt;/td>
&lt;td align="center">true&lt;/td>
&lt;td align="center">上面获取到的授权码，但是其有效期很短，一般为10min&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">grant_type&lt;/td>
&lt;td align="center">true&lt;/td>
&lt;td align="center">授权方式，&lt;code>authorization_code&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">redirect_uri&lt;/td>
&lt;td align="center">true&lt;/td>
&lt;td align="center">颁发令牌后的回调网址&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>GitLab 收到此请求后便会向参数中的&lt;code>redirect_uri&lt;/code>网址发送一段 JSON 数据，虎符在此：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;a7e514632722f45a9edfe4e8624ec3fcd826ebbcb830055f180efee4533a50dd&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;token_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;bearer&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;refresh_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;360c6864b42247fafeaac4715fc524f939ca4545f8400126705144d7e37b5042&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;scope&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;api&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;created_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1577427939&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="2-personal-access-tokenshttpsdocsgitlabcomeeapireadmehtmlpersonal-access-tokens">2. &lt;a href="https://docs.gitlab.com/ee/api/README.html#personal-access-tokens">Personal access tokens&lt;/a>&lt;/h3>
&lt;p>&lt;img src="https://xuezenghui.com/images/gitlab:personal-access-tokens.png" alt="personal-access-tokens.png" title="生成 Personal access token">&lt;/p>
&lt;p>GitLab Web 界面中进入 Seting ➡️ Access Tokens，输入&lt;strong>名字&lt;/strong>和&lt;strong>到期日期&lt;/strong>就可以生成对应的 Access Token，注意生成后需要保存好 Token，因为生成的这条 Token 不会再出现第二次，虽然你可以继续生成新的 Token😑。最简单的一种验证方式，但是此方式要求用户&lt;del>必须登入 GitLab Web 页面进特定操作&lt;/del>，不可取，自己玩玩倒是很方便。&lt;/p>
&lt;h3 id="3-session-cookiehttpsdocsgitlabcomeeapireadmehtmlsession-cookie">3. &lt;a href="https://docs.gitlab.com/ee/api/README.html#session-cookie">Session cookie&lt;/a>&lt;/h3>
&lt;p>登录 GitLab 应用程序时会生成 Session cookie，之后 GitLab 中的 API 都通过此 cookie 进行身份验证，也就是人家官网使用的验证方式，不能通过特定 API 生成这个 cookie，排除。&lt;/p>
&lt;h3 id="4-gitlab-ci-job-tokenhttpsdocsgitlabcomeeapireadmehtmlgitlab-ci-job-token">4. &lt;a href="https://docs.gitlab.com/ee/api/README.html#gitlab-ci-job-token">GitLab CI job token&lt;/a>&lt;/h3>
&lt;p>在 GitLab 内置的持续集成工具 GitLab CI 的 Job 中使用，每个 Job 可配置一个 CI job token，使用方式类似用户名和密码。&lt;/p>
&lt;h2 id="使用">使用&lt;/h2>
&lt;p>有了 Token，就有了使用 GitLab API 的🔑，但是不同 Token 的使用方式也不同：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>AOuth 2.0获取的 Token 类型是 &lt;a href="https://oauth.net/2/bearer-tokens/">bearer-tokens&lt;/a>，需要在 GitLab API 请求中加入 Key 为&lt;code>Authorization&lt;/code>，Value 为 &lt;code>Bearer &amp;lt;Token&amp;gt;&lt;/code>的 Header。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Personal access tokens 获取的是 Private-Token，需要加入 Key 为&lt;code>Private-Token&lt;/code>、Value 为 Token 值的请求 Header。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>所有的 GitLab API 前缀均为&lt;code>https://gitlab.example.com/api/v4/&lt;/code>，使用方式与常见 API 无异，例：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-text" data-lang="text">GET https://gitlab.example.com/api/v4/prjects
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此 API 可获取到用户在 GitLab 上所有的可见项目列表：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1548&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;demo&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Hello&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name_with_namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Zander Xue / Hello&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;hello&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;path_with_namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;zander/hello&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;created_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2019-12-26T06:01:59.746Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;default_branch&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;tag_list&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;span class="nt">&amp;#34;ssh_url_to_repo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;git@gitlab.example.com:zander/hello&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;http_url_to_repo&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://gitlab.example.com/zander/hello.git&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;web_url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://gitlab.example.com/zander/hello&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;avatar_url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;star_count&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">999&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;forks_count&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">888&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;last_activity_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2019-12-26T06:01:59.746Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;_links&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;self&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://gitlab.example.com/api/v4/projects/1548&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;issues&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://gitlab.example.com/api/v4/projects/1548/issues&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;merge_requests&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://gitlab.example.com/api/v4/projects/1548/merge_requests&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;repo_branches&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://gitlab.example.com/api/v4/projects/1548/repository/branches&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;labels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://gitlab.example.com/api/v4/projects/1548/labels&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;events&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://gitlab.example.com/api/v4/projects/1548/events&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;members&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://gitlab.example.com/api/v4/projects/1548/members&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nt">&amp;#34;archived&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;visibility&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;internal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;owner&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">268&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Zander Hsueh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;username&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;active&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;avatar_url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;null&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;web_url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://gitlab.example.com/zander&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nt">&amp;#34;resolve_outdated_diff_discussions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;container_registry_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;issues_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;merge_requests_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;wiki_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;jobs_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;snippets_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;shared_runners_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;lfs_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;creator_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">268&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">666&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;full_path&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;parent_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nt">&amp;#34;import_status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;open_issues_count&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;public_jobs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;ci_config_path&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;shared_with_groups&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;span class="nt">&amp;#34;only_allow_merge_if_pipeline_succeeds&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;request_access_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;only_allow_merge_if_all_discussions_are_resolved&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;printing_merge_request_link_enabled&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;permissions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;project_access&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;group_access&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html">理解 OAuth 2.0 | 阮一峰&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html">OAuth 2.0的四种方式 | 阮一峰&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://juejin.im/post/5b8659b8e51d4538a67aa484">OAuth 2.0协议入门 | 掘金&lt;/a>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>来源：&lt;a href="https://tools.ietf.org/html/rfc6749#section-1.2">https://tools.ietf.org/html/rfc6749#section-1.2&lt;/a> &lt;a href="https://xuezenghui.com/posts/use-gitlab-api/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>来源：&lt;a href="https://tools.ietf.org/html/rfc6749#section-4.1">https://tools.ietf.org/html/rfc6749#section-4.1&lt;/a> &lt;a href="https://xuezenghui.com/posts/use-gitlab-api/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3" role="doc-endnote">
&lt;p>详见 &lt;a href="https://tools.ietf.org/html/rfc6749#section-10.12">https://tools.ietf.org/html/rfc6749#section-10.12&lt;/a> &lt;a href="https://xuezenghui.com/posts/use-gitlab-api/#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/gitlab/">GitLab</category><category domain="https://xuezenghui.com/tags/oauth-2.0/">OAuth 2.0</category></item><item><title>Vuelidate</title><link>https://xuezenghui.com/posts/vuelidate/</link><guid isPermaLink="true">https://xuezenghui.com/posts/vuelidate/</guid><pubDate>Wed, 18 Dec 2019 20:30:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>&lt;a href="https://vuelidate.js.org/">Vuelidate&lt;/a> 是 &lt;a href="https://cn.vuejs.org/">Vue.js 2.0&lt;/a> 的一款表单验证工具，首先要表明的是选择其来做表单验证并是因为其相比于其它 Vue 表单验证工具有多么优秀，而是因为项目用到了一款基于 &lt;a href="https://material.io/">Material Design&lt;/a> 风格的 UI 框架 &lt;a href="https://vuetifyjs.com/en/">Vuetify&lt;/a>，此 UI 框架 &lt;strong>v1.5.x&lt;/strong> 版本推荐使用的表单验证方式就是 Vuelidate（说商业胡吹吧 Vuelidate 官网也没提你 Vuetify 啊，是这个原因导致 Vuetify v2.0.x 版本又推荐了 &lt;a href="https://logaretm.github.io/vee-validate/">Vee-validate&lt;/a> 么？哈哈哈哈😄）。&lt;/p>
&lt;h2 id="用法">用法&lt;/h2>
&lt;h3 id="安装">安装&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">vuelidate&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="导入">导入&lt;/h3>
&lt;p>在&lt;code>main.js&lt;/code>中引入并使用：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vuelidate&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vuelidate&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">Vue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">Vuelidate&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="基础使用">基础使用&lt;/h3>
&lt;p>&lt;strong>1. 在组件中引入验证规则&lt;/strong>&lt;/p>
&lt;p>使用插件最方便之处就在于不用自己逐个去写正则表达式，Vuelidate 提供了很多常见的内置验证规则：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>验证规则&lt;/th>
&lt;th>参数&lt;/th>
&lt;th>作用&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>required&lt;/td>
&lt;td>无&lt;/td>
&lt;td>非空数据验证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>requiredIf&lt;/td>
&lt;td>flag&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/vuelidate/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/td>
&lt;td>当 flag 为&lt;code>true&lt;/code>时验证非空&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>requiredUnless&lt;/td>
&lt;td>flag&lt;/td>
&lt;td>当 flag 为&lt;code>false&lt;/code>时验证非空&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>minLength&lt;/td>
&lt;td>length&lt;/td>
&lt;td>最小长度验证，包含 length 值&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>maxLength&lt;/td>
&lt;td>length&lt;/td>
&lt;td>最大长度验证，包含 length 值&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>minValue&lt;/td>
&lt;td>value&lt;/td>
&lt;td>数字或日期最小值验证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>maxValue&lt;/td>
&lt;td>value&lt;/td>
&lt;td>数字或日期最大值验证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>between&lt;/td>
&lt;td>minValue, maxVulue&lt;/td>
&lt;td>数字或日期指定区间验证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>alpha&lt;/td>
&lt;td>无&lt;/td>
&lt;td>仅接受字母和字符&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>alphaNum&lt;/td>
&lt;td>无&lt;/td>
&lt;td>仅接受字母和数字&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>numeric&lt;/td>
&lt;td>无&lt;/td>
&lt;td>仅接受数字（不包含负数）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>integer&lt;/td>
&lt;td>无&lt;/td>
&lt;td>仅接受正负整数&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>decimal&lt;/td>
&lt;td>无&lt;/td>
&lt;td>仅接受正负十进制数&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>email&lt;/td>
&lt;td>无&lt;/td>
&lt;td>电子邮箱验证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ipAddress&lt;/td>
&lt;td>无&lt;/td>
&lt;td>&lt;a href="https://zh.wikipedia.org/wiki/IPv4">IPv4地址&lt;/a>验证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>macAddress&lt;/td>
&lt;td>分隔符，如&lt;code>:&lt;/code>、&lt;code>' '&lt;/code>&lt;/td>
&lt;td>&lt;a href="https://zh.wikipedia.org/wiki/MAC%E5%9C%B0%E5%9D%80">MAC地址&lt;/a>验证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>sameAs&lt;/td>
&lt;td>flag&lt;/td>
&lt;td>验证与给定变量是否相等&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>url&lt;/td>
&lt;td>无&lt;/td>
&lt;td>网址验证&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>or&lt;/td>
&lt;td>多个验证规则&lt;/td>
&lt;td>当通过给定的至少一个验证规则时通过&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>and&lt;/td>
&lt;td>多个验证规则&lt;/td>
&lt;td>当通过所有的验证规则时通过&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>not&lt;/td>
&lt;td>验证规则&lt;/td>
&lt;td>当不通过给定的验证规则时通过，常与 sameAs 合用&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>要使用这些内置的验证规则只需在组件中按需引入：&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">required&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">numeric&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;vuelidate/lib/validators&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 验证表单&lt;/strong>&lt;/p>
&lt;p>Vuelidate 最便捷的验证方式是基于 Vue 的&lt;code>v-model&lt;/code>数据来验证。也就是说，Vuelidate 和 Vue 采用的是相同的代码与页面交互方式——数据驱动视图，Vuelidate 通过监听双向绑定的数据来动态判断其是否通过验证规则，从而再将验证通过与否的不同结果反馈给用户。&lt;/p>
&lt;p>&lt;strong>第一步、在 validations 中挂载需要验证的表单 data 数据&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;HelloWorld&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">validations&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">zander&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">required&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">numeric&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">zander&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>第二步、用 Vuelidate 的&lt;code>$model&lt;/code>管理表单数据&lt;/strong>&lt;/p>
&lt;p>&lt;code>$model&lt;/code>相当于&lt;code>v-model&lt;/code>的替代，是对原始 data 数据的引用，在其它地方使用该数据时无需改变什么，&lt;code>this.$v.data.$model&lt;/code>的值与&lt;code>this.data&lt;/code>完全相同。被&lt;code>$model&lt;/code>管理的数据含有一个 Boolean 类型的标志&lt;code>$dirty&lt;/code>来表示数据是否改变，很容易理解——如果数据改动了，那它就是肮脏的🤣，&lt;code>$dirty&lt;/code>置为 true。每当被验证的数据改变时 Vuelidate 都会验证一次数据是否通过验证规则。&lt;/p>
&lt;p>&lt;code>$model&lt;/code>的&lt;code>$touch()&lt;/code>方法可以将&lt;code>$dirty&lt;/code>的值手动设为 true，怎么用呢？如果你不想把 Vue 中的&lt;code>v-model=&amp;quot;data&amp;quot;&lt;/code>改为 Vuelidate 的&lt;code>v-model=&amp;quot;$v.data.$model&amp;quot;&lt;/code>，那就可以为输入框添加 change 方法，在 change 方法中再调用&lt;code>$touch()&lt;/code>方法，两种方式的处理结果是完全相同的。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="nt">v-model&lt;/span>&lt;span class="s">=&amp;#34;$v.zander.$model&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="na">or&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="nt">v-model&lt;/span>&lt;span class="s">=&amp;#34;zander&amp;#34; @change=&amp;#34;$v.zander.$touch()&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>如果觉得..每当用户输入一个字符..就验证一次太频繁，可以加上&lt;code>v-model&lt;/code>的&lt;a href="https://cn.vuejs.org/v2/guide/forms.html#lazy">&lt;code>.lazy&lt;/code>&lt;/a>修饰符来使验证频率改为..每当用户输入完成..后验证。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>第三步、添加验证不通过的反馈&lt;/strong>&lt;/p>
&lt;p>验证规则不通过自然要有反馈动作来提示用户哪里出错了或应该输入什么内容，如果在普通 Vue 项目&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/vuelidate/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>中使用 Vuelidate，要显示验证不通过的错误信息需要自己手动添加反馈信息的容器和样式，根据验证结果控制容器的显示与隐藏即可：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="nt">v-model&lt;/span>&lt;span class="s">=&amp;#34;$v.zander.$model&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="nt">v-if&lt;/span>&lt;span class="s">=&amp;#34;!$v.zander.required&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="na">不能为空&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="nt">v-if&lt;/span>&lt;span class="s">=&amp;#34;!$v.zander.numeric&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="na">请输入数字&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/vuelidate:required-numeric.gif" alt="required&amp;numeric.gif" title="没有样式的验证反馈">&lt;/p>
&lt;p>而至此，我方才明白为什么 Vuetify 推荐使用 Vuelidate 了：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/vuelidate:vuetify-vuelidate.gif" alt="vuetify-vuelidate.gif" title="Vuetify 中 Vuelidate 的验证反馈">&lt;/p>
&lt;p>项目中每类表单的验证反馈几乎都需要统一的样式、合理的过渡动画，而这些，Vuetify 都替你做了🥰。相对的，在 Vuetify 中设置 Vuelidate 反馈的方式也有所不同了，也更简单了，只需要在给输入框绑定一个错误信息的属性，然后使用计算属性动态地设置验证反馈的具体文字：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-v" data-lang="v">&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">field&lt;/span> &lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;$v.zander.$model&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span>&lt;span class="n">error&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;zanderErrors&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">field&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="n">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nl">computed:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="n">zanderErrors&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">const&lt;/span> &lt;span class="n">errors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">zander&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">$dirty&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="n">errors&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 验证通过则返回空数组
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">zander&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">required&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">errors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;不能为空&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 短路语法，如果非空验证不通过则加入此反馈信息
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">zander&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">numeric&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">errors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;请输入数字&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 短路语法，如果数字验证不通过则加入此反馈信息
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="n">errors&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="进阶操作">进阶操作&lt;/h3>
&lt;p>上面三步已经基本完成了大多数简单表单验证，下面就再介绍几个我在使用过程中碰到的需要注意的点。&lt;/p>
&lt;p>&lt;strong>1. 验证对象的某个属性&lt;/strong>&lt;/p>
&lt;p>就像上文中的第二个 GIF，我们给两个输入框绑定的是同一个对象的两个属性，想要给每个属性都添加不同的验证规则，只需要绑定时使用&lt;code>.&lt;/code>语法访问对象的属性，挂载时使用嵌套的形式即可：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-v" data-lang="v">&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">field&lt;/span> &lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;$v.zanderObj.memo.$model&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span>&lt;span class="n">error&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;memoErrors&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">field&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">field&lt;/span> &lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;$v.zanderObj.amount.$model&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span>&lt;span class="n">error&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;amountErrors&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">field&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="n">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nl">validations:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nl">zanderObj:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nl">memo:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">required&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nl">amount:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">required&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">numeric&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nl">data:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nl">zanderObj:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nl">memo:&lt;/span> &lt;span class="p">&amp;#39;&amp;#39;,&lt;/span>
&lt;span class="nl">amount:&lt;/span> &lt;span class="p">&amp;#39;&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 自定义验证规则&lt;/strong>&lt;/p>
&lt;p>Vuelidate 内置验证规则固然好用，但往往并不能满足我们的实际需求，比如需要一个调整薪资数目的验证规则，需要验证正负整数及正负小数，这就需要我们去自定义验证规则，Vuelidate 提供了帮助我们自定义基于正则表达式验证规则的方法&lt;code>regex()&lt;/code>。&lt;/p>
&lt;p>首先需要引入 Vuelidate 的 helpers 对象，然后就可以使用&lt;code>helpers.regex()&lt;/code>函数自定义符合我们要求的正则表达式规则，函数有两个参数，分别为该验证规则的描述和正则表达式。最后，像使用内置验证规则一样简单地使用自定义规则吧：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">helpers&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;vuelidate/lib/validators&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">money&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">helpers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">regex&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;plusOrMinusFloatNum&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sr">/^(\-|\+)?\d+(\.\d+)?$/&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="nx">validations&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">adjustSalary&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">required&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">money&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">computed&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">amountErrors&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">errors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">adjustSalary&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$dirty&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="o">!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">adjustSalary&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">money&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;请输入正确的调整数目&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 提交表单的验证&lt;/strong>&lt;/p>
&lt;p>当然，表单验证的目的是让其提交正确格式的数据，这就需要在点击提交按钮时判断数据的正确性，如果不正确则不向后台发送请求等。&lt;code>$invalid&lt;/code>是用于表示数据是否通过验证的 Boolean 类型状态值，其值为 true 时表示验证不通过，此次提交操作无效。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="nt">v-model&lt;/span>&lt;span class="s">=&amp;#34;$v.zander.$model&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span> &lt;span class="nt">@click&lt;/span>&lt;span class="s">=&amp;#34;submit&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="na">提交&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="nt">v-if&lt;/span>&lt;span class="s">=&amp;#34;!$v.zander.required&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="na">不能为空&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="nt">v-if&lt;/span>&lt;span class="s">=&amp;#34;!$v.zander.numeric&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="na">请输入数字&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">required&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">numeric&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;vuelidate/lib/validators&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;HelloWorld&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">validations&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">zander&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">required&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">numeric&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">zander&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">methods&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">submit&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$touch&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$invalid&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">confirm&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;无效提交&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/vuelidate:submit-invalid.gif" alt="submit-invalid.gif" title="提交不成功">&lt;/p>
&lt;p>还有一个常用的方法是&lt;code>$reset()&lt;/code>，它的作用与&lt;code>$touch()&lt;/code>正相反，将所有被验证数据的&lt;code>$dirty&lt;/code>标志置为 false 状态，常用于重置表单操作：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">...&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span> &lt;span class="nt">@click&lt;/span>&lt;span class="s">=&amp;#34;reset&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="na">重置&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="nx">methods&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">reset&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$reset&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 数组类型的验证&lt;/strong>&lt;/p>
&lt;p>是的，通过一段时间的实践运用，我觉得有必要也记录一下数组类型表单的验证。也就是在 Vue 中使用 &lt;code>v-for&lt;/code> 渲染的表单，要验证数组中每一个对象的某个属性的 value，就需要用到一个新的指令 &lt;code>$each&lt;/code>，意思显而易见——“每一项”，看实例：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="nt">v-for&lt;/span>&lt;span class="s">=&amp;#34;(item, index) in $v.arr.$each.$iter&amp;#34; &lt;/span>&lt;span class="nt">:key&lt;/span>&lt;span class="s">=&amp;#34;index&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;{{&lt;/span> &lt;span class="nx">item&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">}}&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="nt">:error-messages&lt;/span>&lt;span class="s">=&amp;#34;arrErrors&amp;#34;&lt;/span> &lt;span class="na">v&lt;/span>&lt;span class="nt">-model&lt;/span>&lt;span class="na">.trim&lt;/span>&lt;span class="err">=&amp;#34;&lt;/span>&lt;span class="na">item.age.&lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="na">model&lt;/span>&lt;span class="err">&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">validations&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">arrCount&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">$each&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">required&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">numeric&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">arr&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Chris&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">computed&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">arrErrors&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">errors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">arr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 因为是数组，所以要循环判断每一项是否通过验证
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">arr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$each&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">age&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$dirty&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="o">!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">arr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$each&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">age&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">required&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;不能为空&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="o">!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">arr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$each&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">age&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">numeric&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;请输入数字&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">errors&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>要用 Vuelidate 使用数据双向绑定的方式管理数组确实是有点繁琐了哈...所以，替代的便易办法是使用 &lt;code>change&lt;/code> 事件监听输入框内容的变化，在变化的时候执行 &lt;code>touch()&lt;/code> 方法让其对内容进行验证，在要验证的表单中添加 Vuelidate 的 &lt;code>@input&lt;/code> 或 &lt;code>@blur&lt;/code> 实现：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">v-for&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;(item, index) in arr&amp;#34;&lt;/span> &lt;span class="na">:key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;index&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>{{ item.name }}&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">:error-messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;arrErrors&amp;#34;&lt;/span> &lt;span class="err">@&lt;/span>&lt;span class="na">input&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;$v.arr.$touch()&amp;#34;&lt;/span> &lt;span class="na">v-model&lt;/span>&lt;span class="err">.&lt;/span>&lt;span class="na">trim&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;item.age&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>但是在 Vuetify 中验证数组表单有一个..小瑕疵..，如果数组中只有一项不通过验证，那么错误反馈会应用到数组表单中的每个要验证的属性，比如上面例子中的两个 &lt;code>age&lt;/code> 的 &lt;code>&amp;lt;input&amp;gt;&lt;/code> 标签都会提示错误，而不是只提示不通过验证的这一项。&lt;/p>
&lt;hr>
&lt;p>Vuelidate 的功能远远不止于此，这里就只列举一些常见的用法和范例了。话说回来，其实项目中整体使用下来觉得 Vuelidate 也没有多么糟糕，具体情境具体考虑嘛！但个人觉得相对而言 &lt;a href="https://logaretm.github.io/vee-validate/">Vee-validate&lt;/a> 更为轻量，且验证的写法也更为简洁（Star 也更多😌）。当然，自身技术允许的话，撸一个 Vue 表单验证工具针对性地用于具体的项目是最好不过了。&lt;/p>
&lt;hr>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>flag 可以是变量或函数，用于动态地改变是否采用此验证规则。 &lt;a href="https://xuezenghui.com/posts/vuelidate/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>这里的「普通」指的是没有采用任何 UI 框架，使用原生 HTML 语言编写的 Vue 项目。 &lt;a href="https://xuezenghui.com/posts/vuelidate/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/vue.js/">Vue.js</category></item><item><title>GraphQL</title><link>https://xuezenghui.com/posts/graphql/</link><guid isPermaLink="true">https://xuezenghui.com/posts/graphql/</guid><pubDate>Thu, 28 Nov 2019 13:04:32 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;center>**“一种用于API的查询语言。”**&lt;/center>
&lt;hr>
&lt;p>🌚看到 &lt;a href="https://graphql.cn/">GraphQL 官网&lt;/a>的这句介绍大概人人都是一脸懵逼的，写过 API、用过数据库查询语言，还就没见过&lt;strong>用于 API 的查询语言&lt;/strong>。大概是因为我们平常所见的大多都是 &lt;a href="http://www.ruanyifeng.com/blog/2011/09/restful.html">RESTful API&lt;/a>，而大量 B/S 模式的应用程序也让我们只倾向于「客户端发送请求获取数据，服务端处理请求返回数据」、客户端与服务端交互的方式只能利用 &lt;a href="https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE#HTTP/2">HTTP 协议&lt;/a>中 GET、POST、PUT、DELETE 等 HTTP 动词的传统认知。&lt;/p>
&lt;p>而 GraphQL 正是要打破这种认知的技术。在 GraphQL 中，客户端可以..不多不少..地获得其想要的数据，因为 GraphQL 中控制返回数据的是客户端，而不是 RESTful API 中完全取决于服务端（前端出人头地的时候到了？🧐）。其次，前端与后端交互的方式也由 HTTP 动词转变为 GraphQL 提供的 &lt;a href="https://graphql.cn/learn/queries/">Query&lt;/a> 和 &lt;a href="https://graphql.cn/learn/queries/#mutations">Mutation&lt;/a> 等。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:address.png" alt="address.png" title="GraphQL 在应用中所处的位置">&lt;/p>
&lt;h2 id="实例体验">实例体验&lt;/h2>
&lt;hr>
&lt;p>开始之前先推荐一个开放 API——美国太空探索技术公司 &lt;a href="https://www.spacex.com/">SpaceX&lt;/a> 提供的&lt;a href="https://github.com/r-spacex/SpaceX-API">开源 REST API&lt;/a>，应有尽有的数据，详细完整的文档，还支持一键导入 Postman😏。
&lt;img src="https://xuezenghui.com/images/graphql:spaceX.jpeg" width=400 title="大火箭🚀">&lt;/p>
&lt;hr>
&lt;p>&lt;strong>1. 使用 &lt;a href="http://www.expressjs.com.cn/starter/generator.html">express-generator&lt;/a> 搭建项目&lt;/strong>&lt;/p>
&lt;p>&lt;strong>2. 安装使用 GraphQL 需要的依赖&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">graphql&lt;/span> &lt;span class="n">express&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">graphql&lt;/span> &lt;span class="n">axios&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此处安装 &lt;a href="http://www.axios-js.com/">axios&lt;/a> 是为了直接在后台发送请求获取数据，也可选择使用 Postman 中的 GraphQL 功能测试。&lt;/p>
&lt;p>&lt;a href="https://github.com/graphql/express-graphql">express-graphql&lt;/a> 可将 Express 服务端中的 HTTP 请求使用 GraphQL 管理。&lt;/p>
&lt;p>&lt;strong>3. 管理 HTTP 请求&lt;/strong>&lt;/p>
&lt;p>在&lt;code>app.js&lt;/code>文件中设置路由，表示所有的客户端请求都由 GraphQL 的 &lt;code>requst handler&lt;/code> 处理：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">graphqlHTTP&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;express-graphql&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">schema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;./schema&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/graphql&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">graphqlHTTP&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">schema&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">graphiql&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>graphqlHTTP()&lt;/code>用于处理 GraphQL 的查询请求，它接收一个 options 参数，其中 &lt;code>schema&lt;/code> 是一个 GraphQL Schema 实例，&lt;a href="https://github.com/graphql/graphiql">&lt;code>graphiql&lt;/code>&lt;/a> 设置为 &lt;code>true&lt;/code> 可以在浏览器中直接对 GraphQL 进行调试。&lt;/p>
&lt;p>&lt;strong>4. Schema&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://spec.graphql.cn/#sec-Type-System-">Schema&lt;/a> 是 GraphQL 的类型系统，用于参数验证和返回数据格式的设定，共有8种类型。&lt;/p>
&lt;p>新建&lt;code>schema.js&lt;/code>文件，定义两个对象类型：LaunchType 和 RocketType：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">GraphQLInt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">GraphQLBoolean&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">GraphQLList&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">GraphQLSchema&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;graphql&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">LaunchType&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Launch&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;发射的相关数据💨&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">flight_number&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLInt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;发射编号&amp;#39;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">mission_name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;任务代号&amp;#39;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">launch_date_local&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;发射时间&amp;#39;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">launch_success&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLBoolean&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;是否成功&amp;#39;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">rocket&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">RocketType&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">RocketType&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Rocket&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;火箭的相关数据🚀&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">rocket_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">rocket_name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">rocket_type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>5. 获取数据，定义查询入口&lt;/strong>&lt;/p>
&lt;p>使用 axios 发送 HTTP 请求获取 SpaceX 官方 API 的数据，定义&lt;code>RootQuery&lt;/code>作为所有查询的入口，处理并返回数据（此举实为模拟从数据库中获取数据）：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">axios&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;axios&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">RootQuery&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;RootQueryType&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">launches&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">LaunchType&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">axios&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;https://api.spacexdata.com/v3/launches&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">then&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">res&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLSchema&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">query&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">RootQuery&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>6. 使用 GraphiQL 测试&lt;/strong>&lt;/p>
&lt;p>项目文件夹下&lt;code>npm start&lt;/code>，浏览器中输入 &lt;a href="http://localhost:5000/graphql">http://localhost:5000/graphql &lt;/a>（端口号可在/bin目录夹下&lt;code>www&lt;/code>文件中自行指定）启动 GraphiQL。&lt;/p>
&lt;ul>
&lt;li>查询所有的&lt;code>flight_number&lt;/code>:&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:query-flight-number.png" alt="query-flight-number.png">&lt;/p>
&lt;ul>
&lt;li>查询想要的更多数据：&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:query-more.png" alt="query-more.png">&lt;/p>
&lt;p>&lt;strong>7. 指定参数实现单条数据查询&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// schema.js
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">RootQuery&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;RootQueryType&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="nx">launch&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 新的查询
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">LaunchType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 添加参数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">flight_number&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLInt&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">axios&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`https://api.spacexdata.com/v3/launches/&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">flight_number&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">then&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">res&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:query-by-args.png" alt="query-by-args.png">&lt;/p>
&lt;h2 id="graphql--nodejs--mongodb">GraphQL + NodeJS + MongoDB&lt;/h2>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:graphql&amp;amp;node&amp;amp;mongodb.jpeg" alt="graphql&amp;node&amp;mongodb.jpeg" title="GraphQL + NodeJS + MongoDB">&lt;/p>
&lt;p>上述实例只是验证了 GraphQL 中的强大查询可以通过 Query 轻松地实现，但还有两件事需要去做：①连接数据库使用自己的数据；②新增、更新、删除操作，下面通过一个综合实例来完成。&lt;/p>
&lt;blockquote>
&lt;p>某些操作上方实例体验中已涉及到，此处不再赘述😑&lt;/p>
&lt;/blockquote>
&lt;h3 id="设置项目">设置项目&lt;/h3>
&lt;p>&lt;strong>1. 搭建项目目录&lt;/strong>&lt;/p>
&lt;p>使用 express-generator 搭建项目，添加&lt;code>/models&lt;/code>目录定义 MongoDB 集合的模型，添加&lt;code>/graphql/schema.js&lt;/code>目录来完成 GraphQL 相关操作，最终目录结构：&lt;/p>
&lt;pre>&lt;code>.
├─ app.js
├─ bin/
│ └─ www
├─ package.json
├─ node_modules
├─ public
├─ images
├─ javascripts
├─ stylesheets/
│ └─ style.css
├─ models/
│ ├─ author.js
│ └─ book.js
├─ graphql/
│ └─ schema.js
└─ views/
├─ error.pug
├─ index.pug
└─ layout.pug
&lt;/code>&lt;/pre>&lt;p>&lt;strong>2. 安装所需依赖项&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="n">express&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">graphql&lt;/span> &lt;span class="n">graphql&lt;/span> &lt;span class="n">mongoose&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="连接数据库并定义集合的模型">连接数据库并定义集合的模型&lt;/h3>
&lt;p>&lt;strong>1. 使用 Mongoose 连接 MongoDB&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 文件位置：app.js
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">mongoose&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;mongoose&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;useFindAndModify&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">connect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;mongodb://127.0.0.1:27017/demo&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">useNewUrlParser&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">connection&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;connected&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;连接成功&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">connection&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;出错&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">connection&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;disconnected&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;连接断开&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 定义 Mongoose 数据模型&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 文件位置：models/author.js
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">mongoose&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;mongoose&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">Schema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Schema&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">authorSchema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Schema&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;age&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Number&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Author&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">authorSchema&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;authors&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 文件位置：models/book.js
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">mongoose&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;mongoose&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">Schema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Schema&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">bookSchema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Schema&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;page&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Number&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;authorId&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Types&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ObjectId&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Book&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">bookSchema&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;books&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="定义类型完成-graphql-接口">定义类型，完成 GraphQL 接口&lt;/h3>
&lt;p>Express 中传统的 RESTful 接口使用&lt;code>express-router&lt;/code>来管理路由，并在不同路由中完成相应的数据库操作，而要结合 GraphQL 就不能使用这种方式了，需要使用 GraphQL 中的方法管理所有的 HTTP 请求，然后在 GraphQL 的接口中完成相应的数据库操作。&lt;/p>
&lt;p>&lt;strong>1. 定义请求入口，使用 GraphQL 管理所有的 HTTP 请求&lt;/strong>&lt;/p>
&lt;p>&lt;strong>2. 定义对象类型和字段&lt;/strong>&lt;/p>
&lt;p>此处的 Schema 才真正决定请求返回的是怎样的数据结构，与 Mongoose 的 Schema 完全不同，后者实际只是为了定义 Model 完成数据库操作，比如&lt;code>author&lt;/code>集合中本没有&lt;code>books&lt;/code>字段，而在 GraphQL 的对象类型中定义以后客户端就可以拿到定义的相应数据。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;span class="lnt">78
&lt;/span>&lt;span class="lnt">79
&lt;/span>&lt;span class="lnt">80
&lt;/span>&lt;span class="lnt">81
&lt;/span>&lt;span class="lnt">82
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 文件位置：graphql/schema.js
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">graphql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;graphql&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">Author&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;../models/author&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 引入作者模型
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">Book&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;../models/book&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 引入书籍模型
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 定义GrapQL中Schema的类型
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">GraphQLID&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">GraphQLInt&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">GraphQLSchema&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">GraphQLList&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">GraphQLNonNull&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">graphql&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="c1">// 定义Book的Schema，决定了其可以返回的数据包括哪些
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">BookType&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Book&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;书籍信息&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLID&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="c1">// _id: {
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="c1">// type: GraphQLID,
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="c1">// name: &amp;#34;也是id吗？&amp;#34;
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="c1">// },
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;书名&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 此处的name用于在GraphiQL Query栏输入字段时显示
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;书名&amp;#34;&lt;/span> &lt;span class="c1">// 此处的description用于在GraphiQL Docs中显示
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">page&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLInt&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;页数&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;页数&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">author&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">AuthorType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;书的作者&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;书的作者&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Author&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">authorId&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="c1">// 定义Author的Schema
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">AuthorType&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Author&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;作者信息&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLID&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;作者名&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;作者名&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLInt&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;作者年龄&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;作者年龄&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">books&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">BookType&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;著作&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;著作&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Book&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">authorId&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">parent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ol>
&lt;li>
&lt;p>字段的&lt;code>name&lt;/code>属性和&lt;code>description&lt;/code>属性可设置在 GraphiQL 的 Query 栏中输入字段时或在 Docs 中显示对应的说明&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>resove()&lt;/code>方法才是掌控返回具体数据的关键，如果不设置则根据字段名对应，&lt;code>BookType&lt;/code>的&lt;code>author&lt;/code>字段和&lt;code>AuthorType&lt;/code>的&lt;code>books&lt;/code>字段都是通过&lt;code>resove()&lt;/code>进行了数据的关联，常用参数：&lt;/p>
&lt;ul>
&lt;li>&lt;code>parent&lt;/code>：上一级对象，如 &lt;code>author&lt;/code> 字段&lt;code>resove()&lt;/code>中的&lt;code>parent&lt;/code>为 &lt;code>Book&lt;/code>，&lt;code>parent.authorId&lt;/code>即为 book 集合中的&lt;code>authorId&lt;/code>字段&lt;/li>
&lt;li>&lt;code>args&lt;/code>：请求的参数，通常在 Query 和 Mutation 操作中使用&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>3. 定义具体接口，完成数据库操作&lt;/strong>&lt;/p>
&lt;p>GraphQL 中的 Mutation 操作用于对数据进行新增、更改和删除操作，用法与 Query 类似。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;span class="lnt">69
&lt;/span>&lt;span class="lnt">70
&lt;/span>&lt;span class="lnt">71
&lt;/span>&lt;span class="lnt">72
&lt;/span>&lt;span class="lnt">73
&lt;/span>&lt;span class="lnt">74
&lt;/span>&lt;span class="lnt">75
&lt;/span>&lt;span class="lnt">76
&lt;/span>&lt;span class="lnt">77
&lt;/span>&lt;span class="lnt">78
&lt;/span>&lt;span class="lnt">79
&lt;/span>&lt;span class="lnt">80
&lt;/span>&lt;span class="lnt">81
&lt;/span>&lt;span class="lnt">82
&lt;/span>&lt;span class="lnt">83
&lt;/span>&lt;span class="lnt">84
&lt;/span>&lt;span class="lnt">85
&lt;/span>&lt;span class="lnt">86
&lt;/span>&lt;span class="lnt">87
&lt;/span>&lt;span class="lnt">88
&lt;/span>&lt;span class="lnt">89
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">RootQuery&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="c1">// 相当于js中定义了一个对象，然后在对象中添加各种方法
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;RootQueryType&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">books&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">BookType&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;获取所有的书籍信息&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 此处的description用于在GraphiQL Query中显示
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">resolve&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Book&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">book&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">BookType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;根据书名获取书籍信息&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 定义参数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Book&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findOne&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">author&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">AuthorType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;根据作者id获取作者信息&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLID&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Author&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">authors&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">AuthorType&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;获取所有作者信息&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Author&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">Mutation&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Mutation&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">addAuthor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">AuthorType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLNonNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="c1">//GraphQLNonNull作用与Mongoose Schema中的required类似，设置参数为必须值
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLNonNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">GraphQLInt&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">author&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Author&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">author&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">save&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">updateAuthor&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">AuthorType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLNonNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLNonNull&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">GraphQLInt&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">updateObj&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">age&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Author&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findOneAndUpdate&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="nx">updateObj&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">deleteBook&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">BookType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">){&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Book&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">deleteOne&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLSchema&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">query&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">RootQuery&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">mutation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Mutation&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="执行请求">执行请求&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>查询所有作者信息：
&lt;img src="https://xuezenghui.com/images/graphql:query-authors.png" alt="query-authors.png">&lt;/p>
&lt;/li>
&lt;li>
&lt;p>根据书籍名查找书籍信息：
&lt;img src="https://xuezenghui.com/images/graphql:query-book.png" alt="query-book.png">&lt;/p>
&lt;/li>
&lt;li>
&lt;p>新增作者：
&lt;img src="https://xuezenghui.com/images/graphql:add-author.png" alt="add-author.png">&lt;/p>
&lt;/li>
&lt;li>
&lt;p>更新作者信息：
&lt;img src="https://xuezenghui.com/images/graphql:update-author.png" alt="update-author.png">&lt;/p>
&lt;/li>
&lt;li>
&lt;p>删除作者：
&lt;img src="https://xuezenghui.com/images/graphql:delete-book.png" alt="delete-book.png">&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="dataloader">DataLoader&lt;/h2>
&lt;p>不知你是否发现了惊奇的一点：Mongoose 定义的&lt;code>authorSchema&lt;/code>中并没有书籍相关的字段，所有操作数据库的方法中也没有用到&lt;code>populate&lt;/code>及&lt;code>aggregate&lt;/code>关联数据，但是上方「查询所有作者信息」的接口&lt;code>authors&lt;/code>返回了..书籍..的所有信息。&lt;/p>
&lt;p>没错，这就是 GraphQL 优越所在——在 Type 中自由地定义返回的数据（&lt;code>AuthorType&lt;/code>的&lt;code>books&lt;/code>字段）。但是问题也随之来了，这类简单的..关联查询..实际会导致严重的 N + 1查询性能问题。&lt;/p>
&lt;h3 id="n--1">N + 1&lt;/h3>
&lt;p>一旦你学习完 GraphQL 的基础知识就大概率会看到大家在谈论 N + 1问题，N + 1是什么呢？为了理解起来更简单，我新建了 persons 和 friends 集合，其数据结构如下：&lt;/p>
&lt;p>persons 数据：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;_id&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">ObjectId&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;5df49a5856652a298949e313&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;age&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;alive&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;friends&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="nx">ObjectId&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;5df49a7556652a298949e31d&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="nx">ObjectId&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;5df49aa256652a298949e331&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>friends 数据：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;_id&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">ObjectId&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;5df49a7556652a298949e31d&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Tom&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;tel&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;120&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;email&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;tom@gmail.com&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;_id&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">ObjectId&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;5df49aa256652a298949e331&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Jerry&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;tel&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;110&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;email&amp;#34;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;jerry@gmail.com&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>接下来是同样的步骤——新建 personType 和 friendType，再建立简单的 personQuery：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">personQuery&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;personQueryType&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;查询人物信息&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">person&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">personType&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;获取人物及朋友信息&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findOne&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后在 GraphiQL 中执行这个简单的 Query：&lt;/p>
&lt;pre>&lt;code>{
person(name: &amp;quot;Zander&amp;quot;){
id,
name,
age,
alive,
friends{
name,
tel,
email
}
}
}
&lt;/code>&lt;/pre>&lt;p>按照 GraphQL 的机制会这样执行查询流程：&lt;/p>
&lt;p>第一步：先查询 persons 集合中 &lt;code>name&lt;/code> 为 &lt;code>Zander&lt;/code> 的信息：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">resolve&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findOne&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>第二步：对于 Zander 的 friends 数据，GraphQL 会拿着&lt;code>friends&lt;/code>数组中的 id 去匹配 friends 集合的&lt;code>_id&lt;/code>字段，执行的查询大概是这样：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">resolve_1&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Friend&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nx">_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">parent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id_1&lt;/span>&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">resolve_2&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Friend&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nx">_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">parent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id_2&lt;/span>&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="nx">resolve_n&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">arg&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Friend&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nx">_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">parent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id_n&lt;/span>&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>如此，便产生了 对数据库的 N + 1次请求。&lt;/p>
&lt;blockquote>
&lt;p>我倒觉得叫1 + N 问题更合适🌚，因为总是先进行1次主集合数据查询，然后再去查询关联的 N 条数据。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;em>Whatever!&lt;/em> 先来解决问题吧～&lt;/p>
&lt;h3 id="解决问题">解决问题&lt;/h3>
&lt;p>对于 N + 1问题，GraphQL 的开发者 &lt;a href="https://zh.wikipedia.org/wiki/Facebook">Facebook&lt;/a> 提供了 &lt;a href="https://github.com/graphql/dataloader">DataLoader&lt;/a> 来作为通用的解决方案，为什么说是「通用」呢？因为几乎每种语言都有 DataLoader 的实现方式——JavaScript、Java、Python、PHP、Ruby......。DataLoader 通过&lt;strong>批处理&lt;/strong>和&lt;strong>缓存&lt;/strong>来减少 API 对数据库的访问次数。&lt;/p>
&lt;p>&lt;strong>批处理&lt;/strong>是 DataLoader 的主要功能，作用是如果需要多次访问数据库，则将这些功能类似的请求合并处理。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:batching.png" alt="batching.png" title="批处理">&lt;/p>
&lt;p>使用 DataLoader 的批处理函数需要满足两点：&lt;/p>
&lt;ul>
&lt;li>批处理函数接受一个数组参数，返回的查询结果数组长度与参数数组长度相同且索引对应&lt;/li>
&lt;li>返回的数组必须为 Promise 对象&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>1. 安装 DataLoader&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">dataloader&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">save&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 引入 Dataloader，定义 Dataloader 对象，将其挂载到所有请求的上下文中&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">DataLoader&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;dataloader&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/graphql&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">graphqlHTTP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">friendLoader&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">DataLoader&lt;/span>&lt;span class="p">(&lt;/span>
&lt;span class="nx">keys&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">Friend&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nx">_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nx">$in&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">keys&lt;/span>&lt;span class="p">}})&lt;/span>
&lt;span class="p">)&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">loaders&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">friend&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">friendLoader&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">context&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nx">loaders&lt;/span>&lt;span class="p">},&lt;/span>
&lt;span class="nx">schema&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">graphiql&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}));&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>网上很多案例都对返回的查询结果做了&lt;code>Promise.all()&lt;/code>处理，但是在 Mongoose 中，所有的数据库操作返回的结果都是一个 Mongoose Documents，本身就是一个 Promise 对象，因此不用做相应的处理。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>3. 修改获取 friends 数据的方法&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">personType&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLObjectType&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;person&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;人物信息&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">fields&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;span class="nx">id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLID&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLString&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;姓名&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;姓名&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLInt&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;年龄&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;年龄&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">alive&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">GraphQLBoolean&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;是否活着&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;是否活着&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">friends&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">GraphQLList&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">friendType&lt;/span>&lt;span class="p">),&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;朋友&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">description&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;朋友们的信息&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">resolve&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nx">loaders&lt;/span>&lt;span class="p">})&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="c1">// return Friend.find({_id: {$in: parent.friends}}); // 不使用Dataloader
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">loaders&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">friend&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">loadMany&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">friends&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>你以为这就完了吗？是的没错🤪然而就这点简单的代码竟花费了我数天的时间，原因是网上竟没有找到完完全全的 Express + Mongoose + MongoDB + GraphQL + DataLoader 实例，完成这个实例确是摸石头过河，报了很多错、踩了很多坑才终取得真经。&lt;/p>
&lt;p>然而如何去验证成功使用 DataLoader 解决了 N + 1是个问题，也就是目前还不知道如何监控 MongoDB ..集合..的查询次数、时间等信息，使用 mongostat、mongotop 等监控方法都没能达成此目的。&lt;/p>
&lt;h2 id="apollo">Apollo&lt;/h2>
&lt;p>&lt;a href="https://www.apollographql.com/">Apollo GraphQL&lt;/a> 是一个用于创建 GraphQL 客户端和服务器的完整独立系统，其完整性和独立性体现在不管你服务端使用的是 Java、Node.js、Python 或其它语言，也不管你客户端运用的是 React，React Native，Vue 还是 Angular，它不依赖于特定语言和框架，能很好地满足你对 GraphQL 的实现，并且是一套成熟完整的生态系统。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:apollo-graphql.png" alt="apollo-graphql.png" title="Apollo GraphQL 生态">&lt;/p>
&lt;h3 id="服务端实现apollo-server">服务端实现——Apollo Server&lt;/h3>
&lt;p>Apollo Server 可以与流行的几个 Node.js 框架集成，包括 &lt;a href="http://expressjs.com/">Express&lt;/a>、&lt;a href="https://www.fastify.io/">Fastify&lt;/a>、&lt;a href="https://koa.bootcss.com/">Koa&lt;/a> 和 &lt;a href="https://hapi.dev/">Hapi&lt;/a>，下面介绍如何在 Express 中搭建 Apollo Server。&lt;/p>
&lt;p>&lt;strong>1. 设置项目&lt;/strong>&lt;/p>
&lt;p>还是使用 express-generator 搭建项目目录，使用 Mongoose 连接 MongoDB，接下来安装依赖项：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="n">apollo&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">server&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">express&lt;/span> &lt;span class="n">graphql&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 初始化 Apollo Server&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">ApolloServer&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;apollo-server-express&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">typeDefs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;./schema/schema&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// GraphQL的Schema
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">resolvers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;./schema/resolvers&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// API方法
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">server&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">ApolloServer&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">typeDefs&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">resolvers&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">playground&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 配置playground
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">settings&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s1">&amp;#39;editor.theme&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;light&amp;#39;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">server&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">applyMiddleware&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">app&lt;/span> &lt;span class="p">});&lt;/span> &lt;span class="c1">// 应用中间件，传递数据到express的app，必须位于`const app = express();`下方
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">listen&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">port&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">4000&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`🚀 Server ready at http://localhost:4000&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">server&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">graphqlPath&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 添加 Schema&lt;/strong>&lt;/p>
&lt;p>Apollo Server 中内置了 &lt;a href="https://www.apollographql.com/docs/apollo-server/api/apollo-server/#gql">gql&lt;/a> 模板字符串，新建 schema/schema.js 目录，定义 GraphQL 的 Schema（类型系统）：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">gql&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;apollo-server-express&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">typeDefs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">gql&lt;/span>&lt;span class="sb">`
&lt;/span>&lt;span class="sb"> type Person {
&lt;/span>&lt;span class="sb"> id: String
&lt;/span>&lt;span class="sb"> name: String
&lt;/span>&lt;span class="sb"> age: Int
&lt;/span>&lt;span class="sb"> alive: Boolean
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> type Friend {
&lt;/span>&lt;span class="sb"> id: String
&lt;/span>&lt;span class="sb"> name: String
&lt;/span>&lt;span class="sb"> tel: String
&lt;/span>&lt;span class="sb"> email: String
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> type Query {
&lt;/span>&lt;span class="sb"> allPerson: [Person]
&lt;/span>&lt;span class="sb"> person(name: String!): Person
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">typeDefs&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 添加返回数据的方法&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://www.apollographql.com/docs/tutorial/resolvers/#what-is-a-resolver">resolvers&lt;/a> 用于定义 GraphQL 操作（Query、Mutation、Subscriptoin）返回的具体数据，在 /src/schema/resolvers.js 中添加：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">Person&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;../models/person&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">resolvers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">Query&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">allPerson&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">person&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">parent&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">Person&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findOne&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">args&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">});&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">resolvers&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>5. 在 playground 中测试 GraphQL API&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:allPerson.png" alt="allPerson.png" title="所有人物信息">&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:person.png" alt="person.png" title="通过姓名查询人物信息">&lt;/p>
&lt;h3 id="客户端实现vue-apollo">客户端实现——Vue Apollo&lt;/h3>
&lt;p>&lt;a href="https://vue-apollo.netlify.com/zh-cn/">Vue Apollo&lt;/a> 通过声明式查询将 Apollo 集成到 Vue 组件中，是 Vue 中使用 GraphQL 的官方实现方法。&lt;/p>
&lt;p>&lt;strong>1. 安装&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://cli.vuejs.org/">Vue CLI 3&lt;/a> 中安装 Apollo 十分简单，直接添加插件即可：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">vue&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="n">apollo&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>建议可选项：&lt;/p>
&lt;pre>&lt;code>? Add example code? No
? Add a GraphQL API Server? No
? Configure Apollo Engine? No
&lt;/code>&lt;/pre>&lt;p>当然你要是头铁（依赖项实在太多）也可以选择&lt;a href="https://vue-apollo.netlify.com/zh-cn/guide/installation.html#%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85">手动安装&lt;/a>。&lt;/p>
&lt;p>&lt;strong>2. 配置 vue-apollo&lt;/strong>&lt;/p>
&lt;p>生成目录中的 vue-apollo.js 是 apollo 的配置文件，需要做的有两点：&lt;/p>
&lt;ul>
&lt;li>由于服务端中没有设置 &lt;a href="https://www.ibm.com/developerworks/cn/java/j-lo-WebSocket/index.html">WebSocket&lt;/a> 端点，需要将配置文件中的 &lt;code>wsEndpoint&lt;/code> 设置为&lt;code>null&lt;/code>。&lt;/li>
&lt;li>设置 http 端点 &lt;code>httpEndpoint&lt;/code> 为 Apollo 服务端中所设置的 GraphQL 请求入口 URL，由于我服务端并没有特殊配置入口 URL，此处无需改动。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. 在 Vue 组件中使用 GraphQL 查询语句&lt;/strong>&lt;/p>
&lt;p>在组件中使用 GraphQL 查询有三种方式，而使用 GraphQL API 返回的数据就和使用 data 中的数据一样简单：&lt;/p>
&lt;p>**方式一、**在组件中引入 gql 模板字符串语法，然后在组件中声明 apollo 查询来定义查询语句：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">v-for&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;person in allPerson&amp;#34;&lt;/span> &lt;span class="na">:key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;person.id&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
name: {{ person.name }},
age: {{ person.age }},
alive: {{ person.alive === true? &amp;#34;是&amp;#34;:&amp;#34;否&amp;#34; }}
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">gql&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;graphql-tag&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;HelloWorld&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">apollo&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">allPerson&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">gql&lt;/span>&lt;span class="sb">`
&lt;/span>&lt;span class="sb"> query {
&lt;/span>&lt;span class="sb"> allPerson {
&lt;/span>&lt;span class="sb"> id
&lt;/span>&lt;span class="sb"> name
&lt;/span>&lt;span class="sb"> age
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> }
&lt;/span>&lt;span class="sb"> `&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>**方式二、**为了查询语句的可重用性和可维护性，建议采用引用公共 gql 语句的方式。新建 /src/graphql/ 目录，在目录下新建&lt;code>.gql&lt;/code>文件来定义项目所需的 GraphQL 操作，然后在组件中引入并使用：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 文件位置：/src/graphql/allPerson.gql
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">query&lt;/span> &lt;span class="nx">allPerson&lt;/span>&lt;span class="p">{&lt;/span>
&lt;span class="nx">allPerson&lt;/span>&lt;span class="p">{&lt;/span>
&lt;span class="nx">id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">age&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">alive&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在组件中使用：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">allPerson&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;../graphql/allPerson.gql&amp;#39;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;HelloWorld&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">apollo&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">allPerson&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">allPerson&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>**方式三、**使用 &lt;a href="https://vue-apollo.netlify.com/zh-cn/guide/components/">Apollo 组件&lt;/a>也是一种办法，这种方式的优点在于可以脱离 Vue 组件的&lt;code>&amp;lt;script&amp;gt;&lt;/code>标签，适用于在 Vue ..公共组件..中使用，但也太不优雅了🙃：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hello&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">ApolloQuery&lt;/span>
&lt;span class="na">:query&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;gql =&amp;gt; gql`
&lt;/span>&lt;span class="s"> query {
&lt;/span>&lt;span class="s"> allPerson {
&lt;/span>&lt;span class="s"> id
&lt;/span>&lt;span class="s"> name
&lt;/span>&lt;span class="s"> }
&lt;/span>&lt;span class="s"> }`&amp;#34;&lt;/span>
&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span> &lt;span class="na">v-slot&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{ result: { loading, error, data } }&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">v-if&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">v-for&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;person in data.allPerson&amp;#34;&lt;/span> &lt;span class="na">:key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;person.id&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
name: {{ person.name }},
age: {{ person.age }},
alive: {{ person.alive === true? &amp;#34;是&amp;#34;:&amp;#34;否&amp;#34; }}
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">ApolloQuery&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>当然，这三种方式带来的结果是相同的：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/graphql:vue-apollo-result.png" alt="vue-apollo-result.png" title="页面渲染数据">&lt;/p>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;p>&lt;strong>GraphQL &amp;amp; DataLoader：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;a href="https://www.youtube.com/watch?v=UBGzsb2UkeY&amp;amp;feature=youtu.be">Zero to GraphQL in 30 Minutes | YouTube&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://medium.com/slite/avoiding-n-1-requests-in-graphql-including-within-subscriptions-f9d7867a257d">Avoiding n+1 requests in GraphQL, including within subscriptions | Medium&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://stackoverflow.com/questions/52783010/how-to-use-mongoose-with-graphql-and-dataloader">How to use Mongoose with GraphQL and DataLoader? | Stack Overflow&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Apollo：&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;a href="https://juejin.im/post/5c015a5af265da612577d89a">使用 NodeJS 创建一个 GraphQL 服务器 | 掘金&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://www.youtube.com/watch?v=8JtmnsolNq8">Learn GraphQL with Vue Apollo in 20 minutes! | YouTube&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://alligator.io/vuejs/vue-apollo-graphql/">Using Apollo / GraphQL with Vue.js | Alligator&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/graphql/">GraphQL</category></item><item><title>JavaScript 此间道理汝明了之？</title><link>https://xuezenghui.com/posts/javascript-note/</link><guid isPermaLink="true">https://xuezenghui.com/posts/javascript-note/</guid><pubDate>Wed, 27 Nov 2019 13:04:32 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>了解了语法拿来就用？搜到了例子照搬不误？NoNoNo！作为21世纪的前端新势力应该树立“书写合格且优雅的规范代码”之最佳典范，高举“大前端是未来趋势”之伟大旗帜，秉持“JavaScript 是世界上最好的语言”之行业思想……以下关于 JavaScript 中各语法在各业务情境中的使用都是博主一坑一坑踩过来的🥺，一为..记录..，二为..分享..，愿我们下（qiāo）笔（xià）写（jiàn）出（pán）的都是极具可读性、可扩展性和良好健壮性的高质量代码。&lt;/p>
&lt;blockquote>
&lt;p>🍽食用方法：正文中的三级标题为用法的简要，..🤔..后是该用法的原理及相关思考，..🔗..为推荐阅读的相关链接。&lt;/p>
&lt;/blockquote>
&lt;h2 id="干货">干货&lt;/h2>
&lt;h3 id="创建空对象的正确方式objectcreatenull">创建空对象的正确方式—&lt;code>Object.create(null)&lt;/code>&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>🤔：JavaScript 编程中的最佳实践，&lt;code>Object.create(null)&lt;/code>创建的对象不会继承 Object 原型的&lt;code>toString()&lt;/code>、&lt;code>hasOwnProperty()&lt;/code>等方法，真正的..空对象..，干净而优雅🤤～而使用&lt;code>{}&lt;/code>则反之。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>🔗：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://juejin.im/post/5acd8ced6fb9a028d444ee4e">详解 Object.create(null) | 掘金&lt;/a>&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;h3 id="慎用delete来删除对象的属性">慎用&lt;code>delete&lt;/code>来删除对象的属性&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>🤔️：JavaScript V8 引擎内部机制导致&lt;code>delete&lt;/code>操作会先耗费大量时间去检查对象中的各个属性，从而大大影响程序的执行速度。简单的方式是直接将不需要的属性设为&lt;code>undefined&lt;/code>，但这种方式实际只保证了属性不显示，而不是真正意义上的..删除..（会导致 &lt;a href="https://eslint.bootcss.com/">ESlint&lt;/a> 报错），Lodash 的 &lt;a href="https://www.lodashjs.com/docs/latest#_pickobject-props">pick()&lt;/a> 或 &lt;a href="https://lodash.com/docs/4.17.15#omit">omit()&lt;/a> 方法实为最优解。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>🔗：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://stackoverflow.com/questions/43594092/slow-delete-of-object-properties-in-js-in-v8/44008788">Slow delete of object properties in JS in V8 | Stack Overflow
&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://segmentfault.com/a/1190000020081647">为什么说在 JS 中要避免使用 delete | SegmentFault&lt;/a>&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;h3 id="最优雅地实现数组去重并排序">最优雅地实现数组去重并排序&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">arr&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[...&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">arr&lt;/span>&lt;span class="p">)].&lt;/span>&lt;span class="nx">sort&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">a&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">a&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>
&lt;p>🤔：ES6 中的&lt;code>Set&lt;/code>数据结构类似于数组，其内所有元素的值都是唯一的，不含重复值，扩展运算符&lt;code>...&lt;/code>将 Set 实例转为了数组，然后再用数组排序方法&lt;code>sort()&lt;/code>进行从小到大的排序。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>🔗：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="http://es6.ruanyifeng.com/#docs/set-map#Set">Set 和 Map 数据结构 | ECMAScript 6 入门 - 阮一峰&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://es6.ruanyifeng.com/#docs/array#%E6%89%A9%E5%B1%95%E8%BF%90%E7%AE%97%E7%AC%A6">数组的扩展 | ECMAScript 6 入门 - 阮一峰&lt;/a>&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;h3 id="利用短路语法替代简单的条件语句">利用短路语法替代简单的条件语句&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">zander&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;developer&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;He is cool!&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="c1">// ⬇️
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">zander&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;developer&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;He is cool!&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">length&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">zander&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="p">[]).&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>
&lt;p>🧐：&lt;code>&amp;amp;&amp;amp;&lt;/code>运算保证第一个参数为 true 时才会执行后面的代码，&lt;code>||&lt;/code>运算保证第一个参数为 false 时才会执行后面的代码。短路运算的效率略高于&lt;code>if&lt;/code>条件语句，但只适用于简单的&lt;code>if..do...&lt;/code>语句优化，如参数验证、设置默认值等。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>🔗：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Logical_Operators">逻辑运算符 | MDN&lt;/a>&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;h3 id="高效地格式化数字">高效地格式化数字&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">toDecimalMark&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">num&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">num&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toLocaleString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;en-US&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">toDecimalMark&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">19971122.05&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// &amp;#39;19,971,122.05&amp;#39;
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>
&lt;p>🧐：这是一个格式化数字函数，可将数字高效地转换为逗号分隔的数字..字符串..，而不用写一堆正则去处理。&lt;code>toLocaleString()&lt;/code> 是 ECMAScript 中的标准方法，用于返回特定语言环境下格式化后的字符串。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>🔗：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString">Number.prototype.toLocaleString() | MDN&lt;/a>&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;h3 id="根据对象的-value-获取对应的-key">根据对象的 Value 获取对应的 Key&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kd">function&lt;/span> &lt;span class="nx">getKey&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">compare&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">a&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">a&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">element&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">compare&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">element&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="p">));&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>
&lt;p>🧐：要通过 Value 找到 Key 是离不开遍历的，先使用 &lt;code>Object.keys()&lt;/code> 方法返回对象可枚举属性组成的数组，数组的 &lt;code>find()&lt;/code> 方法可在遍历时完成键值的匹配。但这只针对无相同 Value 的对象，否则只会返回匹配到的第一个 Key，如果没有匹配到的 Key 则返回 &lt;code>undefined&lt;/code>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>🔗：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/keys">Object.keys() | MDN&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/find">Array.prototype.find() | MDN&lt;/a>&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;h3 id="最平滑之滚动">最平滑之滚动&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">scrollToTop&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">c&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">scrollTop&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">scrollTop&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">c&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">requestAnimationFrame&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scrollToTop&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">scrollTo&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">c&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">c&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>
&lt;p>🧐：&lt;code>scrollTop&lt;/code> 获取了当前滚动条距离顶部的垂直距离，如果这个距离大于 0，调用神奇的 &lt;code>requestAnimationFrame&lt;/code> API，它传入一个回调函数，让..浏览器..根据其重绘周期和屏幕帧数来执行回调、一帧一帧地更新动画，使动画更平滑，而这里的动画就是 &lt;code>scrollTo&lt;/code>——滚动事件。（注：使用此方法需考虑&lt;a href="https://caniuse.com/?search=requestAnimationFrame">兼容性问题&lt;/a>）&lt;/p>
&lt;/li>
&lt;li>
&lt;p>🔗：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollTop">Element.scrollTop | MDN&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame">window.requestAnimationFrame | MDN&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://caibaojian.com/requestanimationframe.html">requestAnimationFrame 详解以及无线页面优化 | 前端开发博客&lt;/a>&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/javascript/">JavaScript</category></item><item><title>Bit</title><link>https://xuezenghui.com/posts/bit/</link><guid isPermaLink="true">https://xuezenghui.com/posts/bit/</guid><pubDate>Tue, 19 Nov 2019 20:18:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>现在几乎每个前端都可以在嘴边挂着组件化、模块化，但是真正做到组件化、模块化开发的项目..寥寥无几..。比如我们接触的每个 Vue 项目中几乎都有 &lt;code>components&lt;/code> 目录，但是其中的 Vue 组件都和这个项目有着不可切割的关系，如果某天要把这个所谓的..公共组件..用于其它项目上，就会发现——压根儿拔不动、抽不出。&lt;/p>
&lt;p>究其根本，造成这个不理想的结果原因很多。代码层面上，不同项目中的 UI 设计风格不同、组件中包含特定项目的依赖（如各种 UI 框架）、组件之间存在复杂的相互依赖关系，都导致组件难以抽离；人为层面上，开发人员在组件化开发概念上不够深入、组件的拆分上做的并不合理，也让组件化的发展停滞不前，而更重要的一个原因在于&lt;strong>没有一个合适的用来管理组件的工具&lt;/strong>，组件化很关键的一个因素在于组件的管理，包括组件的发布、修改、详细使用文档、demo 展示等，而现有的代码管理工具如 Git、NPM 却很难在..组件..的层面上有效管理代码。&lt;/p>
&lt;h2 id="bit-来了">Bit 来了&lt;/h2>
&lt;p>&lt;a href="https://docs.bit.dev/docs/quick-start">Bit&lt;/a> 是一个强大的组件协作平台，它能跨项目、跨存储库地隔离并管理组件，可在众多框架及工具中使用，如 React、Vue、Angular、Node.js、&lt;a href="https://mochajs.org/">Mocha&lt;/a>、&lt;a href="https://jestjs.io/">Jest&lt;/a> 等。&lt;/p>
&lt;p>&lt;strong>“ &lt;em>Bit loves Git&lt;/em> ”&lt;/strong>—— yep，这是 Bit 官方文档的原话。&lt;a href="https://git-scm.com/">Git&lt;/a> 大家再熟悉不过了，世界上最先进的 DVCS&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/bit/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>，「近朱者赤」，大概这就是 Bit 喜欢 Git 的原因了吧 hhhh～😆&lt;/p>
&lt;p>Just joking ，其实是因为 Bit 的工作流和 Git ..很相似..，我们通常用 Git 来管理源文件、源代码，而 Bit 也是用来管理代码的，但不同的是 Git 不管你代码的语义结构，一股脑儿来进行管理。而 Bit 则是将代码..分组件..管理的，换句话说—— &lt;strong>Bit 用来管理组件&lt;/strong>。&lt;/p>
&lt;p>&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/bit/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;img src="https://xuezenghui.com/images/bit:workflow.svg" alt="workflow.png" title="Bit workflow">&lt;/p>
&lt;p>Bit 的工作流和 Git 怎么个类似法呢？&lt;/p>
&lt;ol>
&lt;li>
&lt;p>首先，安装 Bit 后可以将本地的代码分组件发布到 Bit Server 中的 Collection 中，类比 Git 将本地仓库代码推送到远程仓库。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>在远程的 Bit Server 中可以预览、管理发布的组件，类比在 GitHub、GitLab 等代码管理工具上查看、管理远程仓库中的文件。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>可以在别的代码区安装并引入发布的组件，可跨机器使用、跨项目使用，甚至跨语言使用已发布的组件，这点和 NPM 的方式倒很类似，继续往下看你会发现 Bit 和 NPM 确是有着千丝万缕的关系。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="安装">安装&lt;/h2>
&lt;p>&lt;strong>使用 NPM/Yarn 安装：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">bit&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bin&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">g&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">yarn&lt;/span> &lt;span class="n">global&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="n">bit&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bin&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>MacOS 也可使用 Homebrew 安装：&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">brew&lt;/span> &lt;span class="n">install&lt;/span> &lt;span class="n">bit&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="使用-bit-管理-vue-组件">使用 Bit 管理 Vue 组件&lt;/h2>
&lt;p>组件，简单点说就是&lt;strong>可重复使用的代码段&lt;/strong>，包含但不限于：&lt;/p>
&lt;ul>
&lt;li>React、Vue、Angular 组件&lt;/li>
&lt;li>公共样式文件，如 CSS、SCSS 文件&lt;/li>
&lt;li>重复使用的函数、方法&lt;/li>
&lt;/ul>
&lt;p>当 Bit 存储一个组件时会存储这段代码的三个元素：&lt;/p>
&lt;p>&lt;strong>1. 源代码&lt;/strong>&lt;/p>
&lt;p>源代码是组件内容的根本，Bit 将源代码映射到组件并指定一个 &lt;code>main&lt;/code> 文件作为组件的入口。&lt;/p>
&lt;p>&lt;strong>2. 依赖关系图&lt;/strong>&lt;/p>
&lt;p>这是 Bit 的一个强大特性，当源代码保存为 Bit 组件时，Bit 会分析其中包含的所有依赖项（即代码中的 &lt;code>import&lt;/code> 和 &lt;code>require&lt;/code> 语句），然后创建一个组件的依赖关系图，这样，即使组件被抽离，它也是独立的，因为所有的依赖项都会被记录追踪。&lt;/p>
&lt;hr>
&lt;center>**两点说明**&lt;/center>
&lt;p>1⃣️ Bit 可追踪的依赖项只包含使用 NPM 安装的依赖和安装/使用的 Bit 组件，就是说项目中直接通过绝对/相对路径引入的本地组件..不被包含..在依赖项内，如 &lt;code>import Utils from './utils.js'&lt;/code>，那如果组件在另一个项目中使用不就找不到引入的文件了吗🤔️？当然，Bit 从根本上避免了这种情况的产生——需要将引入的本地组件与主组件同步追踪才可发布👍。&lt;/p>
&lt;p>2⃣️ 如果要发布的组件中使用了第三方 UI 框架（如 Vuetify 中特有的标签 &lt;code>v-card&lt;/code> 等），Bit 并不会自动分析出它依赖了这个 UI 框架，而是可以直接发布，但在使用此组件的项目中需要安装对应的 UI 框架才能正确显示 Bit 组件。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>3. 工具与配置&lt;/strong>&lt;/p>
&lt;p>Bit 还会将组件特有的工具和配置保存下来，比如组件使用的编译器和测试工具等。&lt;/p>
&lt;h3 id="发布组件">发布组件&lt;/h3>
&lt;p>&lt;strong>1. 初始化项目&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">init&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>当运行 &lt;code>init&lt;/code> 命令时，Bit 会在项目中创建三个资源：&lt;/p>
&lt;ul>
&lt;li>&lt;code>bit config&lt;/code>：位于 &lt;code>(package.json ? package.json : bit.json)&lt;/code> 中，Bit 工作区配置文件，包括导入组件默认保存的文件夹、包管理工具&lt;a href="https://docs.bit.dev/docs/conf-bit-json">等配置项&lt;/a>。&lt;/li>
&lt;li>&lt;code>.bit&lt;/code> 目录：Bit 的工作区，之后创建和导入的组件信息都位于此目录中。&lt;/li>
&lt;li>&lt;code>.bitmap&lt;/code>：Bit 组件图，描述了组件和组成组件的文件之间的链接关系。&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>导入指使用 &lt;code>bit import&lt;/code> 命令将远程的组件添加到工作区，而使用 NPM/Yarn 安装的 Bit 组件只是常规的包，两者有着很大的区别，详询 &lt;a href="https://docs.bit.dev/docs/workspace#imported-components">Imported Components&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>2. 跟踪组件&lt;/strong>&lt;/p>
&lt;p>跟踪操作将本地项目中的一个或多个文件转换为了 Bit 组件，然后会保存上面提到的三个元素。如：&lt;code>$ bit add src/components/HelloWorld.vue --id my-hello&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">file_path&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">id&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">component_id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>如果一个文件内通过 &lt;code>require&lt;/code> 或 &lt;code>import&lt;/code> 引入了另一个文件，只需将它们一同跟踪，Bit 会自动分析他们之间的依赖关系：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="n">src&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">components&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">father.vue&lt;/span> &lt;span class="n">src&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">components&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">son.vue&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">id&lt;/span> &lt;span class="n">father&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">son&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>也可直接跟踪一个目录下的所有组件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="n">src&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">components&lt;/span>&lt;span class="o">/*&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">namespace&lt;/span> &lt;span class="n">hello&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>添加 &lt;code>--namespace hello&lt;/code> 选项意为使用 hello 作为 &lt;code>src/components/&lt;/code> 目录下所有跟踪组件的父级目录，即生成的组件名为 &lt;code>hello/filename1&lt;/code>、&lt;code>hello/filename2&lt;/code>。&lt;/p>
&lt;p>&lt;strong>3. 设置组件版本&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">tag&lt;/span> &lt;span class="o">--&amp;lt;&lt;/span>&lt;span class="n">component_id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">version&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>执行此操作后 Bit 会锁定该组件的依赖项版本，运行编译并测试该组件（可使用 &lt;code>--verbose&lt;/code> 选项查看组件测试结果），如果编译测试通过则会设置组件的版本并自动标记依赖于该组件的组件。如：&lt;code>$ bit tag --my-hello 1.0.0&lt;/code>&lt;/p>
&lt;p>使用 &lt;code>$ bit untag &amp;lt;component_id&amp;gt; &amp;lt;version&amp;gt;&lt;/code> 命令可取消设置的版本号。&lt;/p>
&lt;p>&lt;strong>4. 导出组件&lt;/strong>&lt;/p>
&lt;p>导出组件即把组件上传到远程仓库中，远程仓库分两种，一种是 Bit 提供的组件中心 &lt;a href="https://bit.dev/">bit.dev&lt;/a>，在组件中心中可以&lt;strong>预览&lt;/strong>、&lt;strong>托管&lt;/strong>、&lt;strong>组织&lt;/strong>发布的组件，功能强大；还有一种是自己搭建的 Bit Server，下文会详细讲述。先来看如何导出到可以白嫖的 bit.dev 中：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>在 &lt;a href="https://bit.dev/">bit.dev&lt;/a> 中申请账号&lt;/p>
&lt;/li>
&lt;li>
&lt;p>创建一个公共的或私有的集合，用以存储组件&lt;/p>
&lt;/li>
&lt;li>
&lt;p>在项目中使用 &lt;code>$ bit login&lt;/code> 登录，Bit 会自动打开浏览器进行身份验证😎&lt;/p>
&lt;/li>
&lt;li>
&lt;p>发布组件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">export&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">user_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="n">.&amp;lt;collection_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;h3 id="查看管理组件">查看、管理组件&lt;/h3>
&lt;p>始言之，「组件化一个很关键的因素在于组件的管理」，Bit 在这点上的优势就在于——&lt;strong>bit.dev&lt;/strong>，在平台上可以浏览、使用所有公开的组件，有点像 &lt;a href="https://codepen.io">Codepen&lt;/a>。也可以管理自己已发布的组件，包括编辑组件文档，查看各个版本组件的代码、参数、依赖关系、安装命令等等，还可以在 Playground 中实时编辑、预览组件效果（这点太棒了，但是中文在缩略图上显示异常😡）：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/bit:preview.png" alt="preview.png" title="预览">&lt;/p>
&lt;p>&lt;strong>但是&lt;/strong>，可惜的是 bit.dev 的目标是成为用来托管组件的 SaaS&lt;sup id="fnref:3">&lt;a href="https://xuezenghui.com/posts/bit/#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>，也就是说它是用来赚钱的。普通用户只可创建三个私有集合，要想一刀999级就要付费了，而且价格还不低——$200/month……&lt;/p>
&lt;h3 id="安装并使用组件">安装并使用组件&lt;/h3>
&lt;p>&lt;strong>1. 安装组件&lt;/strong>&lt;/p>
&lt;p>如果&lt;strong>将组件发布至 bit.dev&lt;/strong>，那么使用 NPM 或者 Yarn 是在其它项目中最方便快捷安装使用 Bit 组件的方式，安装组件的命令位于 bit.dev 中组件页面的右上角。&lt;/p>
&lt;p>第一步需要在 NPM 注册表中配置 &lt;code>@bit&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">config&lt;/span> &lt;span class="n">set&lt;/span> &lt;span class="o">@&lt;/span>&lt;span class="n">bit&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="n">registry&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="o">://&lt;/span>&lt;span class="n">node.bit.dev&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后就可以像 NPM/Yarn 安装第三方包一样安装 Bit 组件了：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">npm&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">@&lt;/span>&lt;span class="n">bit&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">zander.hello.my&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">hello&lt;/span>
&lt;span class="o">$&lt;/span> &lt;span class="n">yarn&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="o">@&lt;/span>&lt;span class="n">bit&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">zander.hello.my&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">hello&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>使用也是同样简单：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-app&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">top-bar&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-content&lt;/span> &lt;span class="na">text&lt;/span>&lt;span class="nt">-center&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-row&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text-center mt-10&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-col&lt;/span> &lt;span class="na">cols&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;12&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">my-hello&lt;/span> &lt;span class="na">msg&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;使用组件😉&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">father-test&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">vuetify&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/v-col&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/v-row&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/v-content&amp;gt;&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/v-app&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">TopBar&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;@bit/zander.hello.top-bar&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">MyHello&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;@bit/zander.hello.my-hello&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">FatherSon&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;@bit/zander.hello.father-son&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="nx">Vuetify&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;@bit/zander.hello.vuetify&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;App&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">components&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">TopBar&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">MyHello&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">FatherSon&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">Vuetify&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/bit:use.png" alt="use.png" title="页面成功显示组件内容">&lt;/p>
&lt;p>&lt;strong>2. 导入组件&lt;/strong>&lt;/p>
&lt;p>导入组件指将远程组件的所有信息下载到本地，包括组件的源代码、依赖项等，导入的文件位于配置文件 &lt;code>package.json&lt;/code> 中 &lt;code>componentsDefaultDirectory&lt;/code> 项定义的目录中。&lt;/p>
&lt;blockquote>
&lt;p>在其它项目中要想使用发布在 Bit Server 中的组件只能..导入组件..，而不能使用 NPM or Yarn 的方式。如果是在新项目中第一次..导入.. Bit 组件需先运行 &lt;code>$ bit init&lt;/code> 命令。&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">import&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">user_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="n">.&amp;lt;collection_name&lt;/span>&lt;span class="o">&amp;gt;/&amp;lt;&lt;/span>&lt;span class="n">component_id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>可加入组件固定版本，如 &lt;code>$ bit import zander.hello/my-hello@1.0.1&lt;/code>，不加则默认导入最新版本。&lt;/p>
&lt;p>同样的，使用组件的方式还是引入再使用，不过不能直接引入组件的路径，引入组件的全名即可（&lt;code>package.json&lt;/code> 中的名字）：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">import&lt;/span> &lt;span class="nx">myHello&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;@bit/zander.hello.my-hello&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="更新组件">更新组件&lt;/h3>
&lt;p>更新组件有两个角度，一种是远程的组件更新了，使用该组件的项目需要同步更新；另一种是在使用组件的项目中需要对导入的组件进行更改并上传至远程。&lt;/p>
&lt;p>&lt;strong>1. 更新本地组件&lt;/strong>&lt;/p>
&lt;p>本地的组件要想同步远程的更新直接使用导入命令即可，组件的最新版本会被自动导入：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">import&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">component_id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 修改组件并更新至远程&lt;/strong>&lt;/p>
&lt;p>项目中如果修改了通过 &lt;code>$ bit import&lt;/code> 导入的组件就需要将组件更新至远程，这样其它使用者才可同步此修改。&lt;/p>
&lt;p>当修改组件后运行 &lt;code>$ bit status&lt;/code> 命令会提示 modified components 的相关信息，此时可为组件设置新的版本：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">tag&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">component_id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">new_version&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后将其重新导出到远程集合中：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">export&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">user_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="n">.&amp;lt;collection_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="弃用或删除组件">弃用或删除组件&lt;/h3>
&lt;p>当不使用一个组件的时候有两种选择：&lt;/p>
&lt;p>&lt;strong>1. 弃用组件&lt;/strong>&lt;/p>
&lt;p>弃用组件意味着 Bit 会将其标记为“已过时”，但不会影响组件正常使用。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">deprecate&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">user_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="n">.&amp;lt;collection&lt;/span>&lt;span class="o">&amp;gt;/&amp;lt;&lt;/span>&lt;span class="n">component_id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">remote&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 删除组件&lt;/strong>&lt;/p>
&lt;p>删除一个组件需要考虑是否有其它组件依赖此组件，所以一定要慎重操作。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-s" data-lang="s">&lt;span class="o">$&lt;/span> &lt;span class="n">bit&lt;/span> &lt;span class="n">remove&lt;/span> &lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">user_name&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="n">.&amp;lt;collection&lt;/span>&lt;span class="o">&amp;gt;/&amp;lt;&lt;/span>&lt;span class="n">component_id&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">remote&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="bit-server">Bit Server&lt;/h3>
&lt;p>&lt;a href="https://docs.bit.dev/docs/bit-server">Bit Server&lt;/a> 就是 bit.dev 的代替工具了，且是超低配版，它能提供的功能只有作为“远程仓库”来..存储..组件，本地通过 SSH 来与 Bit Server 通信，发布和导入组件就相当于 Bit Server 的存和取操作了。相对的，功能不健全但成本也相对较低，要使用 Bit Server 只需要一个服务器就够了。&lt;/p>
&lt;h3 id="常用命令">常用命令&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="center">Command&lt;/th>
&lt;th align="center">Function&lt;/th>
&lt;th align="left">Options&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#init">&lt;code>bit init&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">初始化 Bit 环境&lt;/td>
&lt;td align="left">&lt;code>--package-manager&lt;/code>：设置包管理工具&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#login">&lt;code>bit login&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">登录到远程&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#logout">&lt;code>bit logout&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">注销已登录账号&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#list-1">&lt;code>bit list&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">显示远程和本地所有组件&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#status">&lt;code>bit status&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">查看工作区中被跟踪组件的状态&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#add">&lt;code>bit add &amp;lt;file_path&amp;gt; --id &amp;lt;component_id&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">追踪组件&lt;/td>
&lt;td align="left">&lt;code>--id&lt;/code>：指定组件 ID&lt;br>&lt;code>--namespace&lt;/code>：设置命名空间&lt;br>&lt;code>--exclude&lt;/code>：排除文件&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#untag">&lt;code>bit untrack &amp;lt;component_id&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">取消追踪组件&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#show">&lt;code>bit show &amp;lt;component_id&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">查看组件的依赖关系&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#diff">&lt;code>bit diff &amp;lt;component_id&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">比较更改前后组件的变化&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#log">&lt;code>bit log &amp;lt;component_id&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">查看组件各版本信息&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#tag">&lt;code>bit tag &amp;lt;component_id&amp;gt; &amp;lt;version&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">设置组件版本号&lt;/td>
&lt;td align="left">&lt;code>--message&lt;/code>：版本描述&lt;br>&lt;code>--all&lt;/code>：为所有新增和修改的组件设置&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#untag">&lt;code>bit untag &amp;lt;component_id&amp;gt; &amp;lt;version&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">取消设置的版本号&lt;/td>
&lt;td align="left">&lt;code>--all&lt;/code>：取消设置的所有组件的版本号&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#export">&lt;code>bit export &amp;lt;user_name&amp;gt;.&amp;lt;collection_name&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">导出到远程&lt;/td>
&lt;td align="left">&lt;code>--eject&lt;/code>：导出后将本地代码替换成该组件&lt;br>&lt;code>--all&lt;/code>：导出所有&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#import">&lt;code>bit import &amp;lt;user_name&amp;gt;.&amp;lt;collection_name&amp;gt;/&amp;lt;component_id&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">导入组件&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#deprecate">&lt;code>bit deprecate &amp;lt;component_id&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">弃用组件&lt;/td>
&lt;td align="left">&lt;code>--remote&lt;/code>：同时从远程弃用&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="center">&lt;a href="https://docs.bit.dev/docs/apis/cli-all#remove">&lt;code>bit remove &amp;lt;component_id&amp;gt;&lt;/code>&lt;/a>&lt;/td>
&lt;td align="center">删除组件&lt;/td>
&lt;td align="left">&lt;code>--remote&lt;/code>：同时从远程删除&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>首先，Bit 的确是为前端组件化、代码片段管理提供了一套不错的解决方案，如果组件都能..跨项目..地去使用，而且只需要一行 Bit 命令就可以解决，既保证了开发的高效性，也能保证组件的整体质量及可复用性。&lt;/p>
&lt;p>如果前端的工作方式都切换到 Bit 模式上，那么从发布方式、规范化、标准化等方面来说就没得挑，因为你发布的组件别人要用得上、可以用、用得爽啊！所以，从这里来看 Bit 的确可以&lt;strong>规范前端组件标准，有效解耦前端代码&lt;/strong>。&lt;/p>
&lt;p>但是，以 Bit 目前的知名度和普及度来看，它还有很长的路要走，这也不是没有原因的：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>bit.dev 不开源，自行搭建的 Bit Server 功能实在是太有限了，这点真的是太失望了🙍‍♂️……&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Bit 没有像 GitHub、GitLab 这样的可以以项目为单位管理组织代码的机制，以至于看起来像是 Codepen 这样的交流社区。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>权限管理功能欠缺。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>分布式版本控制系统，Distributed Version Control Systems。 &lt;a href="https://xuezenghui.com/posts/bit/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>图源：&lt;a href="https://docs.bit.dev/docs/quick-start">https://docs.bit.dev/docs/quick-start&lt;/a> &lt;a href="https://xuezenghui.com/posts/bit/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3" role="doc-endnote">
&lt;p>Software-as-a-Service，软件即服务，一种软件应用模式。 &lt;a href="https://xuezenghui.com/posts/bit/#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/bit/">Bit</category><category domain="https://xuezenghui.com/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/">前端工程化</category></item><item><title>MongoDB 中的关联查询</title><link>https://xuezenghui.com/posts/aggregation-population/</link><guid isPermaLink="true">https://xuezenghui.com/posts/aggregation-population/</guid><pubDate>Thu, 14 Nov 2019 13:25:10 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>数据库设计中数据之间的关联关系是极其常见的：一对一、一对多、多对多，作为 NoSQL 领头羊的 MongoDB 中常用做法无非「内嵌」和「引用」两种，因为 Document 有 16MB 的大小限制&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/aggregation-population/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>且「内嵌」不适合复杂的多对多关系，「引用」是用得更广泛的关联方式，所以 MongoDB 官方称其为“Normalized Data Models”——..标准化数据模型..。&lt;/p>
&lt;p>&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/aggregation-population/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;img src="https://xuezenghui.com/images/aggregate-populate:data-model-normalized.svg" alt="bakedsvg.svg" title="引用式关联">
引用式的关联其实很简单，指文档与文档之间通过&lt;code>_id&lt;/code>字段的引用来进行关联。在需要 user 集合中“123xyz”的所有信息时只需要再..多查..两个表就可以得到。而本文要阐述的重点就在于&lt;strong>如何去多查这两个表&lt;/strong>——aggregate 与 populate。&lt;/p>
&lt;h2 id="剖析">剖析&lt;/h2>
&lt;h3 id="aggregate">aggregate&lt;/h3>
&lt;p>先来说说 aggregate 吧，为什么要先说它呢？因为人家是 MongoDB 提供的功能——正儿八经血统纯正官方推荐啊🌚，而且不得不提的是 aggregate 也是我刚开始接触 Node.js + MongoDB 就误打误撞使用到的..业务核心技术..，使用其编写了不少现在正在和公司大佬一起重构的接口🤪……这个问题下文会讲到。&lt;/p>
&lt;p>aggregate 聚合其实是 MongoDB 提供的比较大的功能模块了，而关联多个集合需要用到的是&lt;code>$lookup&lt;/code>，比如有作者集合 authors 和著作集合 books，作者与著作即为「一对多」的关联关系，使用引用式关联：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/aggregate-populate:author-book.png" alt="author-book.png" title="集合 authors 与 books">&lt;/p>
&lt;blockquote>
&lt;p>因为技术栈是 Node.js + Express + Mongoose，以下代码示例也以此为基础，使用 express-generator 生成 demo 目录结构。&lt;/p>
&lt;/blockquote>
&lt;p>使用 aggregate 实现聚合查询作者 Zander 的基本信息及其所有著作信息：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/getAuthorInfo_a&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="nx">Author&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">aggregate&lt;/span>&lt;span class="p">([{&lt;/span> &lt;span class="c1">// 操作的Model为Author
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">$lookup&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">from&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;books&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 数据库中关联的集合名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">localField&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;books&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// author文档中关联的字段
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">foreignField&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// book文档中关联的字段
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">as&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;bookList&amp;#34;&lt;/span> &lt;span class="c1">// 返回数据的字段名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">$match&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 筛选条件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s2">&amp;#34;author&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}]);&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">result&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">result&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>返回数据：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5dccfc3aa3fab06c89020c65&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;author&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;age&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;bookList&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5dccfcb5a3fab06c89020c8d&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;代码的弱点&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5dccfd30a3fab06c89020caa&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;代码与六便士&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5dccfda6a3fab06c89020cc3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;代码失格&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>aggregate 使用方法并不难，抛开结果先不谈，来看看 populate 的实现方式。&lt;/p>
&lt;h3 id="populate">populate&lt;/h3>
&lt;p>populate 是 &lt;a href="http://www.mongoosejs.net/docs/populate.html">Mongoose&lt;/a> 中提供的方法，且 Mongoose单方言之&lt;code>populate()&lt;/code>比 MongoDB 的&lt;code>$lookup&lt;/code>更为强大🧐。那就拉出来溜溜呗🐎&lt;/p>
&lt;p>首先，Mongoose 的一切始于 Schema&lt;sup id="fnref:3">&lt;a href="https://xuezenghui.com/posts/aggregation-population/#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>，使用 populate 的重点也在于 Schema 中的设置：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">mongoose&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;mongoose&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">Schema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Schema&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">authorSchema&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Schema&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s2">&amp;#34;author&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;age&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Number&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;books&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[{&lt;/span>
&lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Schema&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Types&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ObjectId&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">ref&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Book&amp;#39;&lt;/span> &lt;span class="c1">// 关联的Model
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}]&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Author&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">authorSchema&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;authors&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 分别为Model名、Schema、数据库中集合名
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在接口中使用&lt;code>populate()&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/getAuthorInfo_p&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="nx">Author&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s2">&amp;#34;author&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>
&lt;span class="p">}).&lt;/span>&lt;span class="nx">populate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;books&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">result&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">result&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>返回数据：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;books&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5dccfcb5a3fab06c89020c8d&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;代码的弱点&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5dccfd30a3fab06c89020caa&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;代码与六便士&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5dccfda6a3fab06c89020cc3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;代码失格&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5dccfc3aa3fab06c89020c65&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;author&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;age&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">18&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="对比">对比&lt;/h3>
&lt;p>&lt;strong>1. 灵活性&lt;/strong>&lt;/p>
&lt;p>现在可以观察到的就是 aggregate 灵活的点在于可以更改关联查询后返回数据的 key（返回数据中的&lt;code>bookList&lt;/code>），而 populate 返回数据的 key 只能是原来的字段名（返回数据中的&lt;code>books&lt;/code>）。值得一提的是 aggregate 更擅长在聚合管道中..对数据进行二次处理..，比如&lt;code>$unwind&lt;/code>拆分、&lt;code>$group&lt;/code>分组&lt;a href="https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/">等等&lt;/a>。&lt;/p>
&lt;p>&lt;strong>2. 功能性&lt;/strong>&lt;/p>
&lt;p>此外，还有一种情况：依旧是上面的数据，如果要根据著作 name 找到著作信息和作者信息，使用 aggregate 的&lt;code>$lookup&lt;/code>只需要这样就做到了😏：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">$lookup&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">from&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;authors&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">localField&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">foreignField&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;books&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">as&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;author&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然而 populate：“我太难了！”&lt;/p>
&lt;p>是的，它做不到这种使用&lt;code>_id&lt;/code>实现的..反向关联..查询，通俗点讲，Mongoose 不允许你这样写：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">bookSchma&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Schema&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s2">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 不能这样写🙅‍♂️
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Schema&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Types&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ObjectId&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">ref&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Author&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>如果你执意要尝试，那么这是你的下场🙃：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;代码的弱点&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>populate 是将一个集合的&lt;code>_id&lt;/code>和另一个集合的非&lt;code>_id&lt;/code>字段进行关联的，但是 Mongoose 4.5.0版本以后提供了与 aggregate 功能写法都非常类似的&lt;a href="http://www.mongoosejs.net/docs/populate.html#populate-virtuals">&lt;code>virtual()&lt;/code>&lt;/a>方法，这里不做详述了，有这个需求我用 aggregate 它不香么？🤨&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>3. 代码简洁度&lt;/strong>&lt;/p>
&lt;p>大概知道了它们的使用方法和适用场景后再来看看其它方面，比如为什么要重构之前完成的 aggregate 接口🥶。刚入职经验不足拿来别人的代码就依葫芦画瓢，画出来的「瓢」是这样的：
&lt;img src="https://xuezenghui.com/images/aggregate-populate:aggregate-api.png" width=400 title="错误示例">&lt;/p>
&lt;p>一方面是大量的回调函数，一方面是 aggregate 繁杂的写法，导致代码大量冗余，可读性也极差，现在重构后优雅的「葫芦」：
&lt;img src="https://xuezenghui.com/images/aggregate-populate:populate-api.png" width=400 title="正确示例">&lt;/p>
&lt;p>&lt;strong>4. 性能方面&lt;/strong>&lt;/p>
&lt;p>看完了外表再说说内在——查询性能，populate 实际是&lt;code>DBRef&lt;/code>&lt;sup id="fnref:4">&lt;a href="https://xuezenghui.com/posts/aggregation-population/#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>的引用方式，相当于多构造了一层查询。比如有10条数据，在&lt;code>find()&lt;/code>查询到了主集合内的10条数据后会再进行&lt;code>populate()&lt;/code>引用的额外10条数据的查询，性能也相对的大打折扣了。&lt;a href="https://blog.csdn.net/rcjjian/article/details/81512762">这里&lt;/a>有位大佬对&lt;code>aggregate()&lt;/code>和&lt;code>find()&lt;/code>进行了性能上的对比，结论也显而易见——比 find 查询速度都快的 aggregate 比关联查询的 find + populate 定是有过之而无不及了。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>aggregation&lt;/th>
&lt;th>populate&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>灵活性&lt;/td>
&lt;td>⭐️⭐️⭐️⭐️⭐&lt;/td>
&lt;td>⭐️&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>反向关联&lt;/td>
&lt;td>⭐️⭐️⭐️⭐️⭐️&lt;/td>
&lt;td>⭐️⭐️&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>功能性&lt;/td>
&lt;td>⭐️⭐️⭐️⭐️⭐️&lt;/td>
&lt;td>⭐️⭐️⭐️&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>代码简洁度&lt;/td>
&lt;td>⭐️&lt;/td>
&lt;td>⭐️⭐️⭐️⭐️⭐️&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>查询性能&lt;/td>
&lt;td>⭐️⭐️⭐️⭐️&lt;/td>
&lt;td>⭐️⭐️&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;p>综合来看，aggregate 在多集合关联查询和对查询数据的二次处理方面更优，而 populate 更适合简单的正向关联关系且其形成的代码样式较优雅，可读性高而易于维护，性能方面的考究对日常开发中的普通应用来说则大可忽略不计。&lt;/p>
&lt;p>&lt;strong>技术的使用无不建立在需求和场景之上，不抱令守律，不因噎废食，知变通，知择优，毕竟技术只是工具，目的才是关键。&lt;/strong>&lt;/p>
&lt;hr>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>&lt;a href="https://docs.mongodb.com/manual/reference/limits/#bson-documents">BSON Documents¶&lt;/a>。 &lt;a href="https://xuezenghui.com/posts/aggregation-population/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>&lt;a href="https://docs.mongodb.com/manual/core/data-model-design/index.html">https://docs.mongodb.com/manual/core/data-model-design/index.html&lt;/a> &lt;a href="https://xuezenghui.com/posts/aggregation-population/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3" role="doc-endnote">
&lt;p>&lt;a href="https://mongoosejs.com/docs/guide.html#definition">https://mongoosejs.com/docs/guide.html#definition&lt;/a> &lt;a href="https://xuezenghui.com/posts/aggregation-population/#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4" role="doc-endnote">
&lt;p>&lt;a href="https://docs.mongodb.com/manual/reference/database-references/index.html#dbrefs">MongoDB 的一种引用方式&lt;/a> &lt;a href="https://xuezenghui.com/posts/aggregation-population/#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/mongodb/">MongoDB</category><category domain="https://xuezenghui.com/tags/mongoose/">Mongoose</category></item><item><title>macOS 升级 Catalina 后根目录无权限问题</title><link>https://xuezenghui.com/posts/update-catalina-bug/</link><guid isPermaLink="true">https://xuezenghui.com/posts/update-catalina-bug/</guid><pubDate>Wed, 30 Oct 2019 13:31:18 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="bug-复现">Bug 复现&lt;/h2>
&lt;p>&lt;img src="https://xuezenghui.com/images/catelina-bug:catalina.png" alt="Catalina.png" title="Catalina">&lt;/p>
&lt;p>就在昨天..快快乐乐..升级 &lt;a href="https://www.apple.com/macos/catalina/">macOS Cataline&lt;/a> 之后，一切都还是那么舒服，直到刚才需要启动 MongoDB 数据库，就在我自信地在 shell 中输入&lt;code>mongod&lt;/code>之后报了个错，看都没看就&lt;code>sudo mongod&lt;/code>，嗯？？？&lt;/p>
&lt;pre>&lt;code>Data directory /data/db not found., terminating
&lt;/code>&lt;/pre>&lt;p>谁把我 dbpath 目录删了？？？重建呗——&lt;code>sudo mkdir /data&lt;/code>，大问题来了：&lt;/p>
&lt;pre>&lt;code>mkdir: data: Read-only file system
&lt;/code>&lt;/pre>&lt;p>遂尝试各种增加权限方法，无效，直到看到了&lt;a href="https://www.v2ex.com/t/605198?p=1">一篇文章&lt;/a>讲到问题出在&lt;strong>新系统 Catalina 默认不允许往系统分区写文件&lt;/strong>，亲试解决方法有效后在此记录下步骤。&lt;/p>
&lt;h2 id="figure-out">Figure out&lt;/h2>
&lt;h3 id="关闭本机sip系统完整性保护">关闭本机SIP(系统完整性保护)&lt;/h3>
&lt;p>终端中输入&lt;code>csrutil status&lt;/code>后返回&lt;code>System Integrity Protection status: enabled.&lt;/code>说明 SIP 处于开启状态。&lt;/p>
&lt;p>重启电脑，按住&lt;code>command + R&lt;/code>直至进入系统恢复界面，然后点击&lt;strong>实用工具&lt;/strong>选择&lt;strong>终端&lt;/strong>：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/cateline-bug:open-shell.jpeg" alt="open-shell.jpeg" title="打开终端">&lt;/p>
&lt;p>输入&lt;code>csrutil disable&lt;/code>关闭SIP：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/catelina-bug:shut-down-sip.jpeg" alt="shut-down-sip.jpeg" title="关闭 SIP">&lt;/p>
&lt;h3 id="权限获取">权限获取&lt;/h3>
&lt;p>重新启动电脑，shell 中输入&lt;code>sudo mount -uw /&lt;/code>，然后就有权限在根目录创建文件夹了，MongoDB 的启动问题得解。&lt;/p>
&lt;p>如果此时还是报错没有权限，请再尝试以下步骤：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>桌面使用&lt;code>shift + command + C&lt;/code>前往电脑磁盘&lt;/p>
&lt;/li>
&lt;li>
&lt;p>右击 Macintosh HD 磁盘选择&lt;strong>显示简介&lt;/strong>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>在&lt;strong>共享与权限&lt;/strong>中添加自己的用户为管理员并设置&lt;strong>读与写&lt;/strong>权限并应用到包含的项目&lt;/p>
&lt;/li>
&lt;li>
&lt;p>重新创建文件夹&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;blockquote>
&lt;p>因为使用 MongoDB 期间会更改 /data/db 文件，所以不能重新开启 SIP，否则还是会报错权限问题，而 SIP 一直处于关闭状态实际上会导致电脑有一定的安全隐患，详情请查阅&lt;a href="https://support.apple.com/zh-cn/HT204899">关于 Mac 上的系统完整性保护&lt;/a>，希望后续能有更好的解决途径吧。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;p>&lt;del>最后，&lt;em>Catalina&lt;/em> 真香～&lt;/del>&lt;/p>
&lt;p>2019/12/24更：恕我直言，以上解决办法真的是弱爆了，因为每重启一次 Mac 就需要执行一遍权限获取的操作，苦不堪言。更好的解决办法是在执行启动 MongoDB 的命令时指定 /data/db 目录位置，但更一劳永逸的办法是&lt;a href="https://hub.docker.com/_/mongo">使用 Docker 启动 MongoDB&lt;/a>。&lt;/p>
&lt;p>&lt;em>Catalina&lt;/em> 依然挺香的😎～&lt;/p></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/macos/">MacOS</category><category domain="https://xuezenghui.com/tags/debug/">debug</category></item><item><title>踩坑 MongoDB 条件查询</title><link>https://xuezenghui.com/posts/mongodb-query-gt/</link><guid isPermaLink="true">https://xuezenghui.com/posts/mongodb-query-gt/</guid><pubDate>Wed, 16 Oct 2019 00:00:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="bug-复现">Bug 复现&lt;/h2>
&lt;blockquote>
&lt;p>后台使用 Node.js + Express + Mongoose&lt;/p>
&lt;/blockquote>
&lt;p>项目中需要用到 MongoDB 的条件查询&lt;code>$gt&lt;/code>，用法也很简单：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">SinglePrints&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s2">&amp;#34;last_day_click&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">$gt&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">thisPrint&lt;/span> &lt;span class="c1">// 查询大于thisPrint值的数据
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">result&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">data&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>更新于2019/11/9，发誓..再也不写回调..后的我回来更新此处代码了🌚：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">thisPrint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">thisPrint&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="nx">SinglePrints&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">find&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="s2">&amp;#34;last_day_click&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">$gt&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">thisPrint&lt;/span> &lt;span class="c1">// 查询大于thisPrint值的数据
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">result&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>而且强大的地方还在于无论这里的&lt;code>thisPrint&lt;/code>类型是&lt;code>Number&lt;/code>还是&lt;code>String&lt;/code>，MongDB 会自动识别为&lt;code>Number&lt;/code>类型来和数据库中的字段进行比较。&lt;/p>
&lt;p>那么问题出在哪里呢？当&lt;code>thisPrint&lt;/code>的值小于1000时，查询的结果是没有任何问题的，但如果大于1000条件筛选就会失效。&lt;/p>
&lt;h2 id="figure-out">Figure out&lt;/h2>
&lt;p>这种看似和筛选条件大小有关系的 bug 实则非也，而是和&lt;strong>数据库中需要比对的字段的类型&lt;/strong>有关，也就是上述例子中&lt;code>last_day_click&lt;/code>存储类型的原因。当类型为&lt;code>String&lt;/code>时就会发生偶尔失效的状况，这就要求在存储这个字段的时候将它存为&lt;code>Number&lt;/code>类型，即在 Mongoose 的&lt;code>Schema&lt;/code>中将其类型定义为&lt;code>Number&lt;/code>。&lt;/p></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/mongodb/">MongoDB</category></item><item><title>D3.js</title><link>https://xuezenghui.com/posts/d3.js/</link><guid isPermaLink="true">https://xuezenghui.com/posts/d3.js/</guid><pubDate>Sun, 13 Oct 2019 00:00:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="需要了解的几个概念">需要了解的几个概念&lt;/h2>
&lt;h3 id="数据可视化">数据可视化&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="http://globe.cid.harvard.edu/">哈佛大学世界贸易数据可视化&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://www.a4z.cn/pui/ant-admin.html#/">React+D3.js Demo&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>基于Web的数据可视化指的是在网页中显示数据统计报表，从而可以更直观地了解数据的走向和趋势，如图表、图谱、地图、关系图、立体图。&lt;/p>
&lt;p>数据可视化的展现方式从以往的 Flash 技术、IE 的 vml 语言发展至如今规范统一的 HTML5 技术点——&lt;strong>Canvas&lt;/strong> 和 &lt;strong>SVG&lt;/strong>。&lt;/p>
&lt;h3 id="svg">SVG&lt;/h3>
&lt;p>可缩放矢量图形，基于 XML。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/d3:axis.png" alt="axis.png" title="SVG坐标轴">&lt;/p>
&lt;blockquote>
&lt;ol>
&lt;li>SVG 图形练习：&lt;a href="https://www.runoob.com/svg/svg-examples.html">SVG 图形实例&lt;/a>&lt;/li>
&lt;li>要使用 SVG 就要考虑到浏览器的兼容问题，&lt;a href="https://www.caniuse.com">Can I Use&lt;/a> 是一个查看浏览器兼容状态的强力工具&lt;/li>
&lt;/ol>
&lt;/blockquote>
&lt;h2 id="d3js">D3.js&lt;/h2>
&lt;h3 id="简介">简介&lt;/h3>
&lt;p>&lt;a href="https://d3js.org/">&lt;em>Data Driven Documents&lt;/em>&lt;/a>，基于&lt;strong>数据驱动文档&lt;/strong>的 JavaScript 库，用于网页作图、生成互动图形，是一个优秀的可视化库。D3.js 不需要你使用哪个特定的框架，也就是说只要能写 JavaScript 就能使用 D3.js，当然是要在浏览器兼容的前提下了。&lt;/p>
&lt;h3 id="优势">优势&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>堪称 SVG 中的 jQuery，操作 SVG 极为方便，当然 Canvas 也支持&lt;/strong>&lt;/p>
&lt;p>截止目前来说，D3 是操作 SVG 最为方便的一个库，堪比 jQuery 和 JavaScript 的关系，当然这不是说 D3 只能操作 SVG，在 v3 之后的 v4 版本开始 D3 就已经支持 Canvas 了，但是支持不代表擅长，主要的处理目标还是 SVG。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>相比于 &lt;a href="https://www.echartsjs.com/zh/index.html">Echarts&lt;/a> 这种框架式工具来说，D3.js 的自由度更高，但相对的学习成本也会变高&lt;/strong>&lt;/p>
&lt;p>chart 类的工具（Highchart、Echarts）固然简单易上手，但是这类都是“框架式工具”，和“库”区别还是比较大的。举个例子，你需要装修自家房子，一种方式是提供给你装修会用到所有工具并教会你所有装修方法，装修风格就任君发挥了，还有一种方式就是给你出几个样本房，自己从中选择，这就是“库”和“框架式工具”的区别了，所以，玩转 D3 后限制图形样式的就只有你的想象力了。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>操作DOM极其强大&lt;/strong>&lt;/p>
&lt;p>毫不夸张的说，如果你学习过 jQuery，D3 你已经会了⅓。甚至网上有人说 D3 可以替代 jQuery，虽然不现实，但这个说法确实得益于 D3 极强的操作 DOM能力。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="入门实例直方图">入门实例——直方图&lt;/h2>
&lt;p>&lt;strong>1. 新建一个 HTML5 文件&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="cp">&amp;lt;!DOCTYPE html&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">html&lt;/span> &lt;span class="na">lang&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;zh-CN&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">charset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;UTF-8&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>D3.js&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 引入 D3.js&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;http://d3js.org/d3.v3.min.js&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 创建 div 用来展示 SVG&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;svg&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 使用 D3.js 的语法绘制 SVG&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">const&lt;/span> &lt;span class="nx">dataset&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">223&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">34&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">55&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">66&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">99&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">276&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">123&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="c1">// 定义数据
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">400&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义SVG的高
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">width&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">400&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义SVG的宽
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">svg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;#svg&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;svg&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;height&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;width&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">width&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">padding&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">top&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">left&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">right&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">bottom&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">20&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">rectStep&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">35&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义间隔
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">rectWidth&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义矩形宽度
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">svg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rect&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rect&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 操作矩形
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;yellow&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 填充颜色
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">rectStep&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 设置矩形x轴坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// y轴坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;width&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">rectWidth&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 矩形宽度
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;height&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 高度
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">svg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 添加文本
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;pink&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 文本颜色
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;font-size&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;16px&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text-anchor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;middle&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 居中显示
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">rectStep&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 文本内容
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">rectWidth&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// x轴偏移量
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;dy&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-10px&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// y轴偏移量
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/d3:interval&amp;amp;width.png" alt="interval&amp;width.png" title="间距 rectStep 和宽度 rectWidth 说明">&lt;/p>
&lt;img src="https://xuezenghui.com/images/d3:histogram.png" width=400 title="直方图">
&lt;blockquote>
&lt;p>⚠️注意：&lt;/p>
&lt;ol>
&lt;li>矩形的 x、y 坐标以矩形的左上角为中心点&lt;/li>
&lt;li>SVG 填充背景颜色使用的是&lt;code>fill&lt;/code>属性&lt;/li>
&lt;li>矩形之间的间隔为 x 轴的差值&lt;/li>
&lt;/ol>
&lt;/blockquote>
&lt;p>&lt;strong>涉及的API：&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>API&lt;/th>
&lt;th>含义&lt;/th>
&lt;th>示例&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>.select&lt;/td>
&lt;td>选择元素&lt;/td>
&lt;td>d3.select(&amp;quot;#id&amp;quot;)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>.selectAll&lt;/td>
&lt;td>选择多个元素&lt;/td>
&lt;td>d3.selectAll(&amp;quot;#id&amp;quot;)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>.append&lt;/td>
&lt;td>添加元素&lt;/td>
&lt;td>.append(&amp;quot;svg&amp;quot;)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>.attr&lt;/td>
&lt;td>添加属性&lt;/td>
&lt;td>.attr(&amp;quot;width&amp;quot;,&amp;quot;400&amp;quot;)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>.data&lt;/td>
&lt;td>绑定数据&lt;/td>
&lt;td>.data([45,64])&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>.enter&lt;/td>
&lt;td>当DOM元素数量少于data的数量时，自动创建元素&lt;/td>
&lt;td>.enter()&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>.text&lt;/td>
&lt;td>设置文本内容&lt;/td>
&lt;td>.text(d =&amp;gt; d)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="比例尺和坐标轴">比例尺和坐标轴&lt;/h2>
&lt;h3 id="比例尺">比例尺&lt;/h3>
&lt;p>先来回顾一下上次直方图中是怎样设置“柱子”的高度的：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">svg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rect&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rect&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 操作矩形
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;yellow&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 填充颜色
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">rectStep&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 设置矩形x轴坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// y轴坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;width&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">rectWidth&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 矩形宽度
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;height&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 高度
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>对的，这里是直接使用数值的大小来表示“柱子”的高度，你可能会表示：显示的高度肯定是和数值的大小有关系啊！对！问题就在这个..关系..上，如果一定是这样..一对一..的关系，那数值是这样呢：&lt;code>[ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ]&lt;/code>，这样呢：&lt;code>[ 2500, 2100, 1700, 1300, 900 ]&lt;/code>。&lt;/p>
&lt;p>这样的数据可视化是毫无可读性、也是绝不可取的，所以需要一种..计算关系..，&lt;strong>能够将某一区域的值映射到另一区域，其大小关系不变&lt;/strong>，这就是 D3 中的一个重要概念——比例尺。&lt;/p>
&lt;h4 id="scalelinear线性比例尺">&lt;code>scaleLinear()&lt;/code>线性比例尺&lt;/h4>
&lt;p>线性比例尺中，定义域和值域都是连续的一一对应关系。&lt;/p>
&lt;p>&lt;strong>例子&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kd">var&lt;/span> &lt;span class="nx">dataset&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mf">1.2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">2.3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">0.9&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">1.5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">3.3&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>
&lt;p>需求&lt;/p>
&lt;ul>
&lt;li>将最小的值映射为0，将最大的值映射为300。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>定义比例尺：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">linear&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">scaleLinear&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">domain&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">min&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">max&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">)])&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">range&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">300&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;pre>&lt;code>&lt;/code>&lt;/pre>&lt;/li>
&lt;li>
&lt;p>结果&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">linear&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.9&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 输出:0
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">linear&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">2.3&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 输出:175
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">linear&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">3.3&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 输出:300
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;pre>&lt;code>&lt;/code>&lt;/pre>&lt;/li>
&lt;li>
&lt;p>解释&lt;/p>
&lt;ul>
&lt;li>&lt;code>.domain()&lt;/code>为定义域，&lt;code>.range()&lt;/code>为值域&lt;/li>
&lt;li>&lt;code>d3.min()&lt;/code>和&lt;code>d3.max()&lt;/code>返回数组中的最小值和最大值&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://xuezenghui.com/images/d3:scaleLinear.png" alt="scaleLinear.png" title="定义域与值域的映射关系">&lt;/p>
&lt;h4 id="scaleordinal序数比例尺">&lt;code>scaleOrdinal()&lt;/code>序数比例尺&lt;/h4>
&lt;p>输入域和输出域都是离散的数据，不连续，其数据是序数关系。&lt;/p>
&lt;p>&lt;strong>例子&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">dataset&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">45&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">34&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">145&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">11&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">78&lt;/span>&lt;span class="p">];&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">color&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;pink&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;blue&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;yellow&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;black&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;green&amp;#39;&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>
&lt;p>需求&lt;/p>
&lt;ul>
&lt;li>45对应 pink，34对应 blue，以此类推。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>定义比例尺&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">ordinal&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">scaleOrdinal&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">domain&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">color&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;pre>&lt;code>&lt;/code>&lt;/pre>&lt;/li>
&lt;li>
&lt;p>结果&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">ordinal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">45&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 输出:color
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">ordinal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">145&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 输出:yellow
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">ordinal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">78&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 输出:green
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;pre>&lt;code>
&lt;/code>&lt;/pre>&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://xuezenghui.com/images/d3:scaleOrdinal.png" alt="scaleOrdinal.png" title="定义域与值域的映射关系">&lt;/p>
&lt;blockquote>
&lt;p>v3 以下版本和 v3 以上版本比例尺的写法不同，详情请参阅官方文档。&lt;/p>
&lt;/blockquote>
&lt;h3 id="坐标轴">坐标轴&lt;/h3>
&lt;p>基本每种图表都需要坐标轴，但是 SVG 中并没有现成的元素，而是需要由其它图形来组成——..刻度.. 和 ..直线..。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;span class="lnt">64
&lt;/span>&lt;span class="lnt">65
&lt;/span>&lt;span class="lnt">66
&lt;/span>&lt;span class="lnt">67
&lt;/span>&lt;span class="lnt">68
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 引入D3的v5版本：`&amp;lt;script src=&amp;#34;https://d3js.org/d3.v5.min.js&amp;#34;&amp;gt;&amp;lt;/script&amp;gt;`
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">dataset&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">223&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">34&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">55&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">66&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">99&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">276&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">123&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="c1">// 定义数据
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">400&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义SVG的高
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">width&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">400&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义SVG的宽
&lt;/span>&lt;span class="c1">// const
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">svg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;#svg&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;svg&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;height&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;width&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">width&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">padding&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 定义间距
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">top&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">left&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">right&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">bottom&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">50&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">rectStep&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">35&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义间隔
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">rectWidth&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 定义矩形宽度
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">xAxisWidth&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">width&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">right&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// x轴长度
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">yAxisWidth&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">top&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// y轴长度
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">xScale&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span> &lt;span class="c1">// x轴比例尺
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">scaleBand&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">// 序数比例尺
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">domain&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1">// 定义域
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">range&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">xAxisWidth&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="c1">// 值域
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">padding&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.1&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 坐标轴间距
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">yScale&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span> &lt;span class="c1">// y轴比例尺
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">scaleLinear&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">// 线性比例尺
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">domain&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">max&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">)])&lt;/span> &lt;span class="c1">// 定义域
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">rangeRound&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="nx">yAxisWidth&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">]);&lt;/span> &lt;span class="c1">// 值域
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">genRect&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">obj&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 设置矩形的函数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">obj&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;yellow&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 填充颜色
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">xScale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1">// 设置矩形x轴坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">yScale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">yScale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">)))&lt;/span> &lt;span class="c1">// y轴坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;width&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">xScale&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bandwidth&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="c1">// 矩形宽度
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;height&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">yScale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">yScale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">));&lt;/span> &lt;span class="c1">// 高度
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">genText&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">obj&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 设置文本的函数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">obj&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;pink&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 文本颜色
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;number&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;font-size&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;16px&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text-anchor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;middle&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 居中显示
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">xScale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1">// 设置矩形x轴坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">yScale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">yScale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">)))&lt;/span> &lt;span class="c1">// y轴坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 文本内容
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">xScale&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bandwidth&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// x轴偏移量
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;dy&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-10px&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// y轴偏移量
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">init&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">dataset&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 初始化数据函数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">genRect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">svg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rect&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rect&amp;#34;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;span class="nx">genText&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">svg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">init&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">xAxis&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">axisBottom&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">xScale&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// x轴
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">gx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">svg&lt;/span> &lt;span class="c1">// 存放x轴的容器
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;g&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;transform&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sb">`translate(&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">,&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">height&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">)`&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 默认显示在上方显示，要使他偏移到下面
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">gx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">xAxis&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 将x轴放进容器
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">yAxis&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">axisLeft&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">yScale&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// y轴
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">gy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">svg&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;g&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;transform&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sb">`translate(&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">,&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">height&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nx">yAxisWidth&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">)`&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 默认显示在上方显示，要使他偏移到下面
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">gy&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">yAxis&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="用户交互与力导向图">用户交互与力导向图&lt;/h2>
&lt;h3 id="交互式操作">交互式操作&lt;/h3>
&lt;p>用户交互指的是&lt;strong>用户输入了某种指令，程序接收到后做出了某种响应&lt;/strong>，而 D3.js 中的用户交互当然指的是用户与可视化图表的交互操作了。&lt;/p>
&lt;p>例如鼠标移动至图形上时候图形的变色、变形、文字提示，或是点击变大缩小等等，而最经典的运用就是..力导向图..了。&lt;/p>
&lt;blockquote>
&lt;p>&lt;a href="http://www.a4z.cn/pui/ant-admin.html#/vertical-bp-chart">厉害的交互式图表案例&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>添加交互&lt;/strong>&lt;/p>
&lt;p>D3 中使用事件监听函数&lt;code>.on(type, function)&lt;/code>为元素添加交互事件。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>参数 type 为事件类型，下表为常见的事件，参数 function 为事件处理函数&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>事件名&lt;/th>
&lt;th>含义&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>鼠标操作&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>click&lt;/td>
&lt;td>鼠标单击&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>mouseover&lt;/td>
&lt;td>鼠标进入&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>mouseout&lt;/td>
&lt;td>鼠标移出&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>mousemove&lt;/td>
&lt;td>鼠标移动(需节流处理)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>mousedown&lt;/td>
&lt;td>鼠标按下&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>mouseup&lt;/td>
&lt;td>鼠标松开&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>dblclick&lt;/td>
&lt;td>鼠标双击&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>键盘操作&lt;/strong>&lt;/td>
&lt;td>按住不放会持续触发&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>keydown&lt;/td>
&lt;td>键盘按下&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>keyup&lt;/td>
&lt;td>键盘释放&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>keypress&lt;/td>
&lt;td>键盘按下字符键&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>触屏操作&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>touchstart&lt;/td>
&lt;td>触摸屏幕&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>touchmove&lt;/td>
&lt;td>触摸点移动&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>touchend&lt;/td>
&lt;td>离开屏幕&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/li>
&lt;li>
&lt;p>&lt;code>d3.select(this)&lt;/code>可以直接选择触发事件的元素，但前提是事件处理函数不使用箭头函数&lt;/p>
&lt;/li>
&lt;li>
&lt;p>每个&lt;code>select&lt;/code>的元素都可以添加&lt;code>.on()&lt;/code>事件监听函数&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="布局">布局&lt;/h3>
&lt;p>布局的作用是将不适合用于绘图的数据转换为适合用于绘图的数据，简而言之——&lt;strong>数据转换&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>D3（v4 以上版本）提供了以下12种布局：
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>布局( &lt;a href="https://github.com/d3/d3/wiki/API--%E4%B8%AD%E6%96%87%E6%89%8B%E5%86%8C">v3&lt;/a> 与 &lt;a href="https://github.com/tianxuzhang/d3.v4-API-Translation">v4以上&lt;/a>版本写法不同)&lt;/th>
&lt;th>API&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>饼状图(Pie)&lt;/td>
&lt;td>d3.layout.pie&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>力导向图(Force)&lt;/td>
&lt;td>d3.forceSimulation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>弦图(Chord)&lt;/td>
&lt;td>d3.layout.chord&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>树状图(Tree)&lt;/td>
&lt;td>d3.tree&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>集群图(Cluster)&lt;/td>
&lt;td>d3.cluster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>捆图(Bundle)&lt;/td>
&lt;td>d3.layout.bundle&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>打包图(Pack)&lt;/td>
&lt;td>d3.pack&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>直方图(Histogram)&lt;/td>
&lt;td>d3.layout.histogram&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>分区图(Partition)&lt;/td>
&lt;td>d3.partition&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>堆栈图(Stack)&lt;/td>
&lt;td>d3.layout.stack&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>矩阵树图(Treemap)&lt;/td>
&lt;td>d3.treemap&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>层级图(Hierarchy)&lt;/td>
&lt;td>d3.hierarchy&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/li>
&lt;/ul>
&lt;h3 id="力导向图">力导向图&lt;/h3>
&lt;p>建立在力学模型基础上的一种特殊的图表，常用来呈现复杂的关系网络。由节点和连线组成，节点和连线都被施加了力的作用，根据力来计算节点和连线的运动轨迹。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/d3:draw-step.png" alt="draw-step.png" title="不同工具绘制图表步骤">&lt;/p>
&lt;h4 id="实现">实现&lt;/h4>
&lt;p>&lt;strong>整体思路&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>定义数据（节点数据和连线数据）&lt;/p>
&lt;/li>
&lt;li>
&lt;p>定义SVG画布&lt;/p>
&lt;/li>
&lt;li>
&lt;p>设置力导向图布局&lt;/p>
&lt;/li>
&lt;li>
&lt;p>绘制节点（&lt;code>circle&lt;/code>）、连线（&lt;code>line&lt;/code>）、描述文字（&lt;code>text&lt;/code>）和关系文字（&lt;code>text&lt;/code>）&lt;/p>
&lt;/li>
&lt;li>
&lt;p>监听力导向图的&lt;code>tick&lt;/code>事件，每运动一帧重新设置节点坐标、连线起始结尾坐标、描述文字坐标和关系文字坐标&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>核心代码&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt"> 10
&lt;/span>&lt;span class="lnt"> 11
&lt;/span>&lt;span class="lnt"> 12
&lt;/span>&lt;span class="lnt"> 13
&lt;/span>&lt;span class="lnt"> 14
&lt;/span>&lt;span class="lnt"> 15
&lt;/span>&lt;span class="lnt"> 16
&lt;/span>&lt;span class="lnt"> 17
&lt;/span>&lt;span class="lnt"> 18
&lt;/span>&lt;span class="lnt"> 19
&lt;/span>&lt;span class="lnt"> 20
&lt;/span>&lt;span class="lnt"> 21
&lt;/span>&lt;span class="lnt"> 22
&lt;/span>&lt;span class="lnt"> 23
&lt;/span>&lt;span class="lnt"> 24
&lt;/span>&lt;span class="lnt"> 25
&lt;/span>&lt;span class="lnt"> 26
&lt;/span>&lt;span class="lnt"> 27
&lt;/span>&lt;span class="lnt"> 28
&lt;/span>&lt;span class="lnt"> 29
&lt;/span>&lt;span class="lnt"> 30
&lt;/span>&lt;span class="lnt"> 31
&lt;/span>&lt;span class="lnt"> 32
&lt;/span>&lt;span class="lnt"> 33
&lt;/span>&lt;span class="lnt"> 34
&lt;/span>&lt;span class="lnt"> 35
&lt;/span>&lt;span class="lnt"> 36
&lt;/span>&lt;span class="lnt"> 37
&lt;/span>&lt;span class="lnt"> 38
&lt;/span>&lt;span class="lnt"> 39
&lt;/span>&lt;span class="lnt"> 40
&lt;/span>&lt;span class="lnt"> 41
&lt;/span>&lt;span class="lnt"> 42
&lt;/span>&lt;span class="lnt"> 43
&lt;/span>&lt;span class="lnt"> 44
&lt;/span>&lt;span class="lnt"> 45
&lt;/span>&lt;span class="lnt"> 46
&lt;/span>&lt;span class="lnt"> 47
&lt;/span>&lt;span class="lnt"> 48
&lt;/span>&lt;span class="lnt"> 49
&lt;/span>&lt;span class="lnt"> 50
&lt;/span>&lt;span class="lnt"> 51
&lt;/span>&lt;span class="lnt"> 52
&lt;/span>&lt;span class="lnt"> 53
&lt;/span>&lt;span class="lnt"> 54
&lt;/span>&lt;span class="lnt"> 55
&lt;/span>&lt;span class="lnt"> 56
&lt;/span>&lt;span class="lnt"> 57
&lt;/span>&lt;span class="lnt"> 58
&lt;/span>&lt;span class="lnt"> 59
&lt;/span>&lt;span class="lnt"> 60
&lt;/span>&lt;span class="lnt"> 61
&lt;/span>&lt;span class="lnt"> 62
&lt;/span>&lt;span class="lnt"> 63
&lt;/span>&lt;span class="lnt"> 64
&lt;/span>&lt;span class="lnt"> 65
&lt;/span>&lt;span class="lnt"> 66
&lt;/span>&lt;span class="lnt"> 67
&lt;/span>&lt;span class="lnt"> 68
&lt;/span>&lt;span class="lnt"> 69
&lt;/span>&lt;span class="lnt"> 70
&lt;/span>&lt;span class="lnt"> 71
&lt;/span>&lt;span class="lnt"> 72
&lt;/span>&lt;span class="lnt"> 73
&lt;/span>&lt;span class="lnt"> 74
&lt;/span>&lt;span class="lnt"> 75
&lt;/span>&lt;span class="lnt"> 76
&lt;/span>&lt;span class="lnt"> 77
&lt;/span>&lt;span class="lnt"> 78
&lt;/span>&lt;span class="lnt"> 79
&lt;/span>&lt;span class="lnt"> 80
&lt;/span>&lt;span class="lnt"> 81
&lt;/span>&lt;span class="lnt"> 82
&lt;/span>&lt;span class="lnt"> 83
&lt;/span>&lt;span class="lnt"> 84
&lt;/span>&lt;span class="lnt"> 85
&lt;/span>&lt;span class="lnt"> 86
&lt;/span>&lt;span class="lnt"> 87
&lt;/span>&lt;span class="lnt"> 88
&lt;/span>&lt;span class="lnt"> 89
&lt;/span>&lt;span class="lnt"> 90
&lt;/span>&lt;span class="lnt"> 91
&lt;/span>&lt;span class="lnt"> 92
&lt;/span>&lt;span class="lnt"> 93
&lt;/span>&lt;span class="lnt"> 94
&lt;/span>&lt;span class="lnt"> 95
&lt;/span>&lt;span class="lnt"> 96
&lt;/span>&lt;span class="lnt"> 97
&lt;/span>&lt;span class="lnt"> 98
&lt;/span>&lt;span class="lnt"> 99
&lt;/span>&lt;span class="lnt">100
&lt;/span>&lt;span class="lnt">101
&lt;/span>&lt;span class="lnt">102
&lt;/span>&lt;span class="lnt">103
&lt;/span>&lt;span class="lnt">104
&lt;/span>&lt;span class="lnt">105
&lt;/span>&lt;span class="lnt">106
&lt;/span>&lt;span class="lnt">107
&lt;/span>&lt;span class="lnt">108
&lt;/span>&lt;span class="lnt">109
&lt;/span>&lt;span class="lnt">110
&lt;/span>&lt;span class="lnt">111
&lt;/span>&lt;span class="lnt">112
&lt;/span>&lt;span class="lnt">113
&lt;/span>&lt;span class="lnt">114
&lt;/span>&lt;span class="lnt">115
&lt;/span>&lt;span class="lnt">116
&lt;/span>&lt;span class="lnt">117
&lt;/span>&lt;span class="lnt">118
&lt;/span>&lt;span class="lnt">119
&lt;/span>&lt;span class="lnt">120
&lt;/span>&lt;span class="lnt">121
&lt;/span>&lt;span class="lnt">122
&lt;/span>&lt;span class="lnt">123
&lt;/span>&lt;span class="lnt">124
&lt;/span>&lt;span class="lnt">125
&lt;/span>&lt;span class="lnt">126
&lt;/span>&lt;span class="lnt">127
&lt;/span>&lt;span class="lnt">128
&lt;/span>&lt;span class="lnt">129
&lt;/span>&lt;span class="lnt">130
&lt;/span>&lt;span class="lnt">131
&lt;/span>&lt;span class="lnt">132
&lt;/span>&lt;span class="lnt">133
&lt;/span>&lt;span class="lnt">134
&lt;/span>&lt;span class="lnt">135
&lt;/span>&lt;span class="lnt">136
&lt;/span>&lt;span class="lnt">137
&lt;/span>&lt;span class="lnt">138
&lt;/span>&lt;span class="lnt">139
&lt;/span>&lt;span class="lnt">140
&lt;/span>&lt;span class="lnt">141
&lt;/span>&lt;span class="lnt">142
&lt;/span>&lt;span class="lnt">143
&lt;/span>&lt;span class="lnt">144
&lt;/span>&lt;span class="lnt">145
&lt;/span>&lt;span class="lnt">146
&lt;/span>&lt;span class="lnt">147
&lt;/span>&lt;span class="lnt">148
&lt;/span>&lt;span class="lnt">149
&lt;/span>&lt;span class="lnt">150
&lt;/span>&lt;span class="lnt">151
&lt;/span>&lt;span class="lnt">152
&lt;/span>&lt;span class="lnt">153
&lt;/span>&lt;span class="lnt">154
&lt;/span>&lt;span class="lnt">155
&lt;/span>&lt;span class="lnt">156
&lt;/span>&lt;span class="lnt">157
&lt;/span>&lt;span class="lnt">158
&lt;/span>&lt;span class="lnt">159
&lt;/span>&lt;span class="lnt">160
&lt;/span>&lt;span class="lnt">161
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">nodes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Zander&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Annie&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Anna&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Doris&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Linda&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Censek&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Paul&amp;#34;&lt;/span> &lt;span class="p">}];&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">edges&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 数组下标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Cherry组队友&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;上下属&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;Alice组队友&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;上下属&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;上下属&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;两口子&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;上下属&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;上下属&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;上下属&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;相爱相杀&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nx">source&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">relation&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;室友&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">];&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">800&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">width&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">800&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">padding&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">top&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">left&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">bottom&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">right&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">50&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="c1">// svg画布
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">svg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;#svg&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;svg&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;height&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;width&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">width&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 设置力导向图布局和参数
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">force&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">forceSimulation&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">// 指定为力导向图布局
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">nodes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">nodes&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 设置节点
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">force&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;link&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forceLink&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">edges&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">distance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1">// 设置连线和连线的长度
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">force&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;charge&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forceManyBody&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 添加多体力（吸力、斥力等组合起来的高阶函数）
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">force&lt;/span>&lt;span class="p">(&lt;/span>
&lt;span class="s2">&amp;#34;center&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 设置力中心点
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">d3&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">forceCenter&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">// 创建一个力中心
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">width&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">left&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">right&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// x坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">height&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">top&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">padding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bottom&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// y坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">);&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">nodes&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 数据改变
&lt;/span>&lt;span class="c1">// 绘制节点
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">circles&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">svg&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;circle&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">nodes&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;circle&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;r&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;yellow&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 绘制连线
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">lines&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">svg&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">edges&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;line&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;stroke&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;green&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;stroke-width&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 添加描述
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">svg&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">nodes&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;font-size&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;12px&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;#000&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dy&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// 添加关系
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">relations&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">svg&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;.relation&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">edges&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;red&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;font-size&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;11px&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;class&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;relation&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dy&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">relation&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">force&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;tick&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 监听力导向图每运动一帧
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">lines&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">source&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">source&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">circles&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;cx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;cy&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">(&lt;/span> &lt;span class="c1">// 将拖拽行为加入到节点上
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">d3&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">drag&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">// 设置拖拽行为
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;start&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">dragStarted&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 拖拽开始
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;drag&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">dragging&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 拖拽中
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;end&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">dragEnded&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">//拖拽结束
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">);&lt;/span>
&lt;span class="nx">text&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">relations&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">source&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// x坐标为连线中间点x坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">source&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// y坐标为连线中间点y坐标
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">dragStarted&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 拖拽开始事件处理函数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">active&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">force&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">alphaTarget&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.3&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">restart&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 设置力的衰减系数α（范围[0, 1]，值越大移动速度越高，并重新布局该区域
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// d.fx为静止时的坐标，d.x为初始坐标
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">dragging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 拖拽中事件处理函数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">x&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// d3.event.x为拖动时的坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">function&lt;/span> &lt;span class="nx">dragEnded&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 拖拽结束事件处理函数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">active&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">force&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">alphaTarget&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/d3:force.gif" alt="force.gif" title="效果">&lt;/p>
&lt;h2 id="绘制热点图">绘制热点图&lt;/h2>
&lt;p>&lt;strong>没事儿画啥地图？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>比如现有一需求：
&lt;ul>
&lt;li>在吃鸡地图中标示出哪里跳伞的人多及哪里跳伞的人少，然后就可以根据这个图大概分析出跳哪里落地成盒的几率更小🌚。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://xuezenghui.com/images/d3:pubg-hot-map.jpeg" alt="pubg-hot-map.jpeg" title="某手游热力图">&lt;/p>
&lt;ul>
&lt;li>
&lt;p>正经点儿，实际业务需求：&lt;/p>
&lt;ul>
&lt;li>在中国地图上显示出百星23个自有制作中心、16个驻场制作中心、39个外包服务站点所在的地理位置(实力打广告🤨)，并通过点的大小、亮度等体现出不同制作中心的业务量高低。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>没错，&lt;strong>热点图&lt;/strong>(又称地图散点图)是这类需求的标准解决方案，其可通过在地图上不同区域使用不同的标志来呈现不同区域的关注程度，使数据清晰明了地呈现在人眼前。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="准备工作">准备工作&lt;/h3>
&lt;p>&lt;strong>1. 中国地理信息文件——&lt;a href="https://www.oschina.net/translate/geojson-spec">GeoJSON&lt;/a>&lt;/strong>&lt;/p>
&lt;p>GeoJSON是用于描述地理空间信息的数据格式，包括地图上的所有要素，如经纬度、点、线、面、特征等，格式为 JSON 格式。&lt;/p>
&lt;blockquote>
&lt;p>网上搜索 china.json 一大堆😏，但是要注意格式要符合 GeoJSON 标准&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>2. 球形➡️平面投影——&lt;code>d3-geo&lt;/code>&lt;/strong>&lt;/p>
&lt;p>经纬度不能直接用于绘图，需要转换为平面坐标，D3 中提供了丰富的&lt;a href="https://github.com/d3/d3/wiki/API--%E4%B8%AD%E6%96%87%E6%89%8B%E5%86%8C#user-content-d3geo-%E5%9C%B0%E7%90%86">投影函数&lt;/a>来处理球面坐标和经纬度运算。&lt;/p>
&lt;p>安装：&lt;code>npm install d3-geo d3-array --save&lt;/code>&lt;/p>
&lt;p>&lt;strong>3. 配色方案&lt;/strong>&lt;/p>
&lt;p>安装：&lt;code>npm install d3-scale-chromatic --save&lt;/code>&lt;/p>
&lt;h3 id="绘制地图">绘制地图&lt;/h3>
&lt;p>&lt;strong>1. 按需导入类库&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="c">&amp;lt;!-- HTML结构 --&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;svg&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hover&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// .vue文件中
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">as&lt;/span> &lt;span class="nx">geo&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;d3-geo&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="nx">as&lt;/span> &lt;span class="nx">d3Color&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s2">&amp;#34;d3-scale-chromatic&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 定义 SVG 画布和投影函数&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 投影函数
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">projection&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">geo&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">geoMercator&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">// 球形墨卡托投影
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">scale&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">550&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 投影的比例因子，可按比例放大投影
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">center&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="mi">105&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">38&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="c1">// 设置中心点的经纬度为中国地图中心点
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">translate&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="nx">width&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">]);&lt;/span> &lt;span class="c1">// 将投影偏移至设SVG中心
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 创建路径生成器和颜色比例尺&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">geo&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">geoPath&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">projection&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">colors&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">scaleOrdinal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d3Color&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">schemeBrBG&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">11&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>4. 获取 GeoJSON 数据，生成地图&lt;/strong>&lt;/p>
&lt;p>此处为重中之重，要使用 D3.js 提供的&lt;code>d3.json()&lt;/code>方法获取地理信息文件，&lt;a href="https://github.com/d3/d3/wiki/%E8%AF%B7%E6%B1%82#d3_json">官方文档&lt;/a>表明此方法&lt;strong>根据指定的 url 创建一个 JSON 文件请求&lt;/strong>，这就要求不能使用本地的 JSON 文件而要搭建服务器返回数据。本案例使用 Express + MongoDB 搭建了简单的服务器返回地理信息数据。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="nx">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">getJson&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 使用d3.json()方法拿到GeoJSON数据
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/api/allData&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="c1">// const result = await d3.json(&amp;#34;../../static/china.json&amp;#34;); // d3.json()不可访问本地文件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">result&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 拿到数据，Object类型
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="nx">getJson&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">then&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">svg&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">features&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 绑定地理特征数据
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="c1">// .append(&amp;#34;path&amp;#34;)
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">insert&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;g&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;d&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">path&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 加入颜色比例尺
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">colors&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;stroke&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;rgba(255, 255, 255, 1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;stroke-width&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>使用&lt;code>append(&amp;quot;path&amp;quot;)&lt;/code>的话 DOM 结构中 path 元素会处于 g 元素之前，导致 path 覆盖 g 元素，原因是 D3 中元素的层级是由元素创建的先后顺序来决定的。&lt;/p>
&lt;/blockquote>
&lt;h3 id="定位城市坐标">定位城市坐标&lt;/h3>
&lt;p>&lt;strong>1. 定义城市坐标数据&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">places&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;北京制作中心&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">log&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;116.54&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">lat&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;39.82&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="c1">// log为经度， lat为纬度
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;西安制作中心&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">log&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;108.84&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">lat&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;34.21&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;沈阳制作中心&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">log&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;123.39&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">lat&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;41.86&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">];&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>2. 标点并绘制&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">location&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">svg&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">selectAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;g&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">places&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">enter&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;g&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;transform&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">coord&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">projection&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">lat&lt;/span>&lt;span class="p">]);&lt;/span> &lt;span class="c1">// 将经纬度转化为页面坐标
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;coord&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">coord&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="sb">`translate(&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">coord&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">,&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">coord&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">)`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;span class="nx">location&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;circle&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;r&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fill&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;yellow&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>3. 加入动画&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="kr">const&lt;/span> &lt;span class="nx">hover&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;#hover&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">location&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mouseover&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">hover&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">html&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;position&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;fixed&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;color&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;blue&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;left&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">pageX&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">45&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;px&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;top&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">pageY&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">40&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;px&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;opacity&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;circle&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">transition&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">duration&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">500&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;r&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mouseout&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">hover&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;opacity&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">d3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;circle&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">transition&lt;/span>&lt;span class="p">()&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">duration&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">attr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;r&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;img src="https://xuezenghui.com/images/d3:belstar-hot-map.gif" alt="belstar-hot-map.gif" title="百星制作中心热点图">&lt;/p>
&lt;hr>
&lt;p>🎱案例 GitHub 地址：&lt;a href="https://github.com/Xuezenghuigithub/D3.js_china_map">https://github.com/Xuezenghuigithub/D3.js_china_map&lt;/a>&lt;/p>
&lt;h2 id="references--resources">References &amp;amp; Resources&lt;/h2>
&lt;ol>
&lt;li>&lt;a href="https://github.com/d3/d3/wiki/CN-Home">d3/d3/wiki/CN-Home | GitHub&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://segmentfault.com/a/1190000011006780">D3中常用的比例尺 | SegmentFault&lt;/a>&lt;/li>
&lt;/ol></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/d3.js/">D3.js</category></item><item><title>常用 CSS 样式总结</title><link>https://xuezenghui.com/posts/common-css/</link><guid isPermaLink="true">https://xuezenghui.com/posts/common-css/</guid><pubDate>Wed, 09 Oct 2019 00:00:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="标题左右横线">标题左右横线&lt;/h2>
&lt;p>&lt;img src="https://xuezenghui.com/images/common-css:title-h.png" alt="title_h.png" title="效果图">&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="p">.&lt;/span>&lt;span class="nc">title&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">relative&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">font-size&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">line-height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">text-align&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">center&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 标题与顶部距离 */&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nd">before&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">content&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">28&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 横线长度 */&lt;/span>
&lt;span class="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 横线粗细 */&lt;/span>
&lt;span class="k">top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="kt">%&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#ddd&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 横线颜色 */&lt;/span>
&lt;span class="k">left&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">28&lt;/span>&lt;span class="kt">%&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 左横线与文字距离 */&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nd">after&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">content&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">28&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="kt">%&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#ddd&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">right&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">28&lt;/span>&lt;span class="kt">%&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 右横线与文字距离 */&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="微信小程序多行文本展开收起">微信小程序——多行文本展开/收起&lt;/h2>
&lt;p>&lt;img src="https://xuezenghui.com/images/common-css:up&amp;amp;down.gif" alt="up&amp;down.gif" title="效果图">&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="c">&amp;lt;!-- .wxml文件 --&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">text&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text {{ellipsis?&amp;#39;ellipsis&amp;#39;:&amp;#39;unellipsis&amp;#39;}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>北京百星电子系统有限公司（以下简称百星公司）成立于1995年，是全外资的高科技企业，运用“云”技术理念，为客户提供专业的外包服务解决方案，对可变数据的抓取、生成电子文档、存储、传输、调阅、发送、打印等各个阶段实现全面优化管理与监控。截至目前，已在全国主要省会城市建立12个自有打印制作中心，与客户合作建立31个外包服务站点。&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">text&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">view&lt;/span> &lt;span class="na">bindtap&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;ellipsis&amp;#39;&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;ellipsis_arrow&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">image&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;ellipsis_img&amp;#34;&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ellipsis?&amp;#39;/images/down.png&amp;#39;:&amp;#39;/images/up.png&amp;#39;}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">view&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="o">//&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">wxss文件&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">text&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kp">-webkit-&lt;/span>&lt;span class="n">box&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 自适应盒模型 */&lt;/span>
&lt;span class="k">text-overflow&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">ellipsis&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 文字溢出显示省略号 */&lt;/span>
&lt;span class="kp">-webkit-&lt;/span>&lt;span class="n">box-orient&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">vertical&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 内容水平排列 */&lt;/span>
&lt;span class="k">font-size&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">24&lt;/span>&lt;span class="n">rpx&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">line-height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">40&lt;/span>&lt;span class="n">rpx&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">rgb&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">135&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">135&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">135&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">padding-top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">overflow&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">hidden&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 必须设置溢出隐藏 */&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">ellipsis&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kp">-webkit-&lt;/span>&lt;span class="n">line-clamp&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 收起显示的文字行数 */&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">unellipsis&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kp">-webkit-&lt;/span>&lt;span class="n">line-clamp&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 展开后显示全部 */&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">ellipsis_arrow&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c">/* 小箭头样式 */&lt;/span>
&lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">block&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">margin&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">auto&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">margin-top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">-4&lt;/span>&lt;span class="kt">px&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">ellipsis_img&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c">/* 箭头图片大小 */&lt;/span>
&lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// .js文件
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="nx">Page&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">ellipsis&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="c1">// 默认收起
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">ellipsis&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kd">function&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 点击小箭头触发事件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ellipsis&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setData&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">ellipsis&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">value&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">)}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="微信小程序时间轴组件">微信小程序——时间轴组件&lt;/h2>
&lt;img src="https://xuezenghui.com/images/common-css:timeline.png" width="300" title="效果图">
&lt;h3 id="timeline-组件">timeline 组件：&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="c">&amp;lt;!-- timeline.wxml --&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">view&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;time_line&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="c">&amp;lt;!-- 左边时间轴 --&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">view&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;leftView&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">view&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;outRoundView&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">view&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">view&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;leftLine&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">view&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">view&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="c">&amp;lt;!-- 右边文字 --&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">view&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;rightContent&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">slot&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;rightChilren&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">slot&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">text&lt;/span> &lt;span class="na">wx:if&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{axisTitle!=&amp;#39;&amp;#39;}}&amp;#34;&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;rightTitle&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>{{axisTitle}}&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">text&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">block&lt;/span> &lt;span class="na">wx:if&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{textArray.length&amp;gt;0}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">text&lt;/span> &lt;span class="na">wx:for&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{textArray}}&amp;#34;&lt;/span> &lt;span class="na">wx:key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;unique&amp;#34;&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;rightText&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>{{item}}&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">text&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">block&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">text&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;rightText&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>{{axisText}}&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">text&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">view&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">view&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="c">/* timeline.wxss */&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">timeline&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">flex&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">flex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#fff&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">leftView&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">flex&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">flex-direction&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">column&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">outRoundView&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">9&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">9&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">border-radius&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="kt">%&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">background&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#fff&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="kc">solid&lt;/span> &lt;span class="mh">#999&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">leftLine&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">flex&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">flex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">margin-left&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">padding-bottom&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">background&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#dadada&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">rightContent&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">flex&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">flex&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">flex-direction&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">column&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">margin-top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">-6&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">margin-left&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">15&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">padding-bottom&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">24&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">rightTitle&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">font-size&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">32&lt;/span>&lt;span class="n">rpx&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">line-height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">22.5&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">rgba&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mf">.7&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nc">rightText&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">font-size&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">14&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#999&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// timeline.js
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="nx">Component&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="cm">/**
&lt;/span>&lt;span class="cm"> * 组件的属性列表
&lt;/span>&lt;span class="cm"> */&lt;/span>
&lt;span class="nx">properties&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">isShowLeftLine&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 是否显示时间轴
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Boolean&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">axisTitle&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 标题
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">axisText&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 文字
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">textArray&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 多行文字
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Array&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// timeline.json
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;component&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="使用组件的页面">使用组件的页面：&lt;/h3>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="c">&amp;lt;!-- .html文件 --&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">view&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">timeLine&lt;/span> &lt;span class="na">axisTitle&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;1995年&amp;#34;&lt;/span> &lt;span class="na">axisText&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;Bel-Star【北京百星电子系统有限公司】成立于北京&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">timeLine&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">timeLine&lt;/span> &lt;span class="na">axisTitle&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;2000年&amp;#34;&lt;/span> &lt;span class="na">axisText&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;荣获Oce亚太地区打印机销售冠军&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">timeLine&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">timeLine&lt;/span> &lt;span class="na">axisTitle&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;2005年&amp;#34;&lt;/span> &lt;span class="na">textArray&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{textArray2005}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">timeLine&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">timeLine&lt;/span> &lt;span class="na">axisTitle&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;2008年&amp;#34;&lt;/span> &lt;span class="na">axisText&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;发表EOMO保单云端处理生产管理平台&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">timeLine&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">timeLine&lt;/span> &lt;span class="na">axisTitle&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;2015年&amp;#34;&lt;/span> &lt;span class="na">textArray&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{textArray2015}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">timeLine&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">timeLine&lt;/span> &lt;span class="na">axisTitle&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;2017年&amp;#34;&lt;/span> &lt;span class="na">textArray&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{textArray2017}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">timeLine&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">timeLine&lt;/span> &lt;span class="na">axisTitle&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;2019年&amp;#34;&lt;/span> &lt;span class="na">textArray&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{textArray2019}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">timeLine&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">view&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// .json文件
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;usingComponents&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;timeLine&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="s2">&amp;#34;/components/timeLine/timeLine&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/css/">CSS</category></item><item><title>将 Hexo 博客部署至 Netlify</title><link>https://xuezenghui.com/posts/hexo-deploy-to-netlify/</link><guid isPermaLink="true">https://xuezenghui.com/posts/hexo-deploy-to-netlify/</guid><pubDate>Sun, 06 Oct 2019 23:04:22 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>其实这个博客总共也才断断续续捣鼓了 fortnight，被好奇心驱使着到处搜集相关资料，浏览研究了很多“巨人”的文章和令人惊艳的个人博客。最开始是按 &lt;a href="https://hexo.io/zh-cn/docs/">Hexo&lt;/a> 的文档将博客部署在 GitHub Pages 上的，但是这个国庆让我感受到了“墙”深深的恶意——几乎所有的科学上网服务器节点都失效了，访问 GitHub 速度极慢，所以部署于 GitHub Pages 的博客加载速度就非常慢(之前也快不到哪儿去🙄)。然后了解到了 Netlify 的一些&lt;strong>特性&lt;/strong>，决定将博客部署到 Netlify 上。&lt;/p>
&lt;h2 id="netlify简介">Netlify简介&lt;/h2>
&lt;p>&lt;a href="https://www.netlify.com/">Netlify&lt;/a> 是一个静态网站自动化部署系统，可快速将静态网站直接构建部署，不需要你提供静态资源服务器。Netlify 基于 JAMstack 架构，即客户端 JavaScript、可重用 API 和预构建 Markup，这是一种现代化、未来化的构建网站的方法，可以提供更好的性能(&lt;a href="https://www.netlify.com/blog/2016/04/15/make-your-site-faster-with-netlifys-intelligent-cdn/">全球 CDN &lt;/a>)、安全性(HTTPS)和更好的开发体验(持续部署及Git集成)。且 Netlify 提供的功能和服务相比 GitHub Pages 可太强大了😏——绑定域名、页面重定向、静态资源文件压缩、免费的 HTTPS 等等，而且&lt;a href="https://www.netlify.com/docs/">文档&lt;/a>翻译过来也比较清晰流畅…&lt;/p>
&lt;h3 id="优势">优势&lt;/h3>
&lt;ul>
&lt;li>
&lt;p>Simply Push to Deploy&lt;/p>
&lt;p>支持热部署，只需要将代码 push 到 Git 远程仓库即可自动构建及更新。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Free automatic HTTPS&lt;/p>
&lt;p>支持自定义域名，提供免费 HTTPS ，可上传域名证书。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Never have to leave Terminal&lt;/p>
&lt;p>可在终端中操作 Netlify。&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="部署步骤">部署步骤&lt;/h2>
&lt;p>虽然 Netlify 没有中文文档，但也别被它的全英文唬住，部署的步骤还是比较简单的，但是它提供的强大功能我也并没有尝试很多，还在持续摸索总结中。&lt;/p>
&lt;p>Netlify 支持两种部署方式，直接拖拽站点文件至页面部署和&lt;strong>连接 Git 方式的持续部署&lt;/strong>，因为之前博客是将 Hexo 生成的&lt;code>public&lt;/code>文件夹部署至 GitHub Pages 的，为了写博客的便利性我还是选择采用 Git 方式部署，这样部署成功后还是可以本地&lt;code>hexo g&lt;/code>➡️&lt;code>hexo d&lt;/code>实现博客的部署更新。&lt;/p>
&lt;p>首先直接使用 GitHub 账号登录 Netlify，点击界面中的&lt;code>New site from Git&lt;/code>按钮：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:new-site.png" alt="new-site.png" title="New site from Git">&lt;/p>
&lt;p>第一步，选择连接 GitHub：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:contact-github.png" alt="contact-github.png" title="连接 GitHub">&lt;/p>
&lt;blockquote>
&lt;p>需要注意的是进行此步骤时尽量关闭所有的科学上网工具，安安分分使用内网，否则..或将..导致网络请求无法连接 GitHub，从而迟迟不跳出授权的页面。鬼知道我当时费了多少周折寻求解决办法结果花费一秒的时间关掉 Shadowsocks 就成了？？？🙃&lt;/p>
&lt;/blockquote>
&lt;p>授权，直接按照默认的选项点击&lt;code>Install&lt;/code>即可：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:auth.png" alt="authorization.png" title="GitHub 授权">&lt;/p>
&lt;p>第二步，选择博客的仓库，比如我这里的 Xuezenghuigithub.github.io 仓库：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:repository.png" alt="repository.png" title="选择仓库">&lt;/p>
&lt;p>第三步，配置选项、构建部署：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:deploy.png" alt="deploy.png" title="构建部署">&lt;/p>
&lt;p>接着等待片刻就部署成功了🍺，点击笑脸😊选项卡即可访问部署成功的网站：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:success.png" alt="success.png" title="部署成功">&lt;/p>
&lt;h3 id="绑定域名设置-dns">绑定域名，设置 DNS&lt;/h3>
&lt;p>首先肯定得申请域名了，很多前辈推荐在狗爹 &lt;a href="https://sg.godaddy.com/zh">GoDaddy&lt;/a> 上购买域名，言之划算且不需要繁琐的域名备案，但是折腾了一番发现中国内地在 GoDaddy 上购买&lt;code>.com&lt;/code>域名是不能使用支付宝支付的，其它支付方式又很麻烦，故弃之选择在&lt;a href="https://www.aliyun.com/">阿里云&lt;/a>上购买域名，操作方便价格无差——真香！(叮～支付宝到账0.5元！)&lt;/p>
&lt;p>有了域名后点击部署项目的 Domain setting，在 Custom domains 内直接输入域名即可，然后去 DNS 设置面板：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:domains.png" alt="domains.png" title="DNS 设置入口">&lt;/p>
&lt;p>按照面板内步骤在阿里云域名控制台修改域名的 DNS 服务器，修改成功后域名后就有了上图中绿色的&lt;code>Netlify DNS&lt;/code>标志，这时域名就由 Netlify 管理了，DNS 解析也会自动配置，省事ing。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:DNS.png" alt="DNS_server.png" title="域名控制台截图">&lt;/p>
&lt;h3 id="设置https">设置HTTPS&lt;/h3>
&lt;p>上面说到了，Netlify 是支持免费 HTTPS 的，只需要在 Domain setting 的 HTTPS 设置中点击按钮即可为网站添加 Netlify 提供的免费的 &lt;a href="https://letsencrypt.org/">Let's Encrypt&lt;/a> 证书，当然，阿里云也提供了免费的 SSL 证书，不嫌麻烦的话也可以使用自定义的证书，但本质上都是为了域名前的那个🔒，区别不大。&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/hexo-deploy-to-netlify:HTTPS.png" alt="HTTPS.png" title="HTTPS">&lt;/p>
&lt;p>&lt;strong>要说明的是&lt;/strong>，虽然已经有了 HTTPS，但访问博客后域名前是❗️而不是🔒，提示&lt;code>与网站之间建立的连接并非完全安全&lt;/code>，这是因为网站内包含了其它的非 HTTP 请求，比如七牛云的图片外链等，解决办法就是将博客文章内的 HTTP 链接全部替换为 HTTPS，但说回来这种情况也&lt;strong>并不影响正常的浏览访问&lt;/strong>，而且我这个穷鬼不配拥有七牛云的 HTTPS 格式图片外链🌚。&lt;/p>
&lt;h3 id="修改博客相关配置">修改博客相关配置&lt;/h3>
&lt;p>绑定了自己的域名后相关的功能会失效，比如 Valine 评论会不显示、百度统计获取不到新域名的网站报告等，需要对相关配置加以修改。&lt;/p>
&lt;h4 id="博客配置">博客配置&lt;/h4>
&lt;p>修改博客的 URL 配置：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="c1"># 文件位置：blog/_config.yml&lt;/span>
&lt;span class="c1"># URL&lt;/span>
&lt;span class="n">url&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">https&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="o">//&lt;/span>&lt;span class="n">blog&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">xuezenghui&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">com&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="valine-评论系统配置">Valine 评论系统配置&lt;/h4>
&lt;p>登入 &lt;a href="https://leancloud.cn/">LeanCloud&lt;/a>，进入&lt;strong>设置&lt;/strong>➡️&lt;strong>安全中心&lt;/strong>➡️&lt;strong>Web安全域名&lt;/strong>中添加域名：&lt;/p>
&lt;pre>&lt;code>https://xuezenghuigithub.github.io/
https://www.blog.xuezenghui.com/
http://www.blog.xuezenghui.com/
&lt;/code>&lt;/pre>&lt;blockquote>
&lt;p>协议、域名、端口号必须完全一致，自己申请域名的&lt;code>www&lt;/code>不能省略。&lt;/p>
&lt;/blockquote>
&lt;h4 id="百度统计配置">百度统计配置&lt;/h4>
&lt;p>登录&lt;a href="https://tongji.baidu.com/web/10000070711/welcome/login">百度统计&lt;/a>，新增网站，获取代码，将代码中&lt;code>hm.src&lt;/code>行&lt;code>?&lt;/code>后的字符复制，修改配置：&lt;/p>
&lt;pre>&lt;code># 文件位置：blog/_config.yml
# Baidu Analytics ID
baidu_analytics: 123hasdhda8d2u3dhiua
&lt;/code>&lt;/pre></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/netlify/">Netlify</category><category domain="https://xuezenghui.com/tags/hexo/">Hexo</category></item><item><title>小程序中 PDF 文件的上传及下载</title><link>https://xuezenghui.com/posts/upload-pdf/</link><guid isPermaLink="true">https://xuezenghui.com/posts/upload-pdf/</guid><pubDate>Mon, 30 Sep 2019 00:00:00 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;iframe frameborder="no" width=100% height=86 src="https://music.163.com/outchain/player?type=2&amp;id=552594869&amp;auto=1&amp;height=66">&lt;/iframe>
&lt;h4 id="需求">需求&lt;/h4>
&lt;p>微信小程序中上传 PDF 文件（其它格式文件也可）至服务器（Node.js），并可在后台管理系统（Vue.js）中下载存储的 PDF 文件至本地。&lt;/p>
&lt;h4 id="思路">思路&lt;/h4>
&lt;ol>
&lt;li>微信小程序中利用现有 API 实现本地文件的上传&lt;/li>
&lt;li>后台处理上传过来的二进制数据&lt;/li>
&lt;li>存储&lt;/li>
&lt;li>Vue 中发送请求获取后台数据并下载至本地&lt;/li>
&lt;/ol>
&lt;h2 id="开搞">开搞&lt;/h2>
&lt;h4 id="小程序中上传文件">小程序中上传文件&lt;/h4>
&lt;p>首先要明确的是小程序中并没有提供可直接选择手机本地资源文件的 API，原因主要在于iOS系统出于保护用户隐私的文件系统，APP 访问不到系统本地的文件，市值2000亿美金的微信也不例外（辣鸡腾讯???🤨）。&lt;/p>
&lt;p>但是，微信小程序提供了选择会话中文件的功能，即&lt;a href="https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseMessageFile.html">&lt;code>wx.chooseMessageFile()&lt;/code>&lt;/a>接口。什么意思呢？就是比如我从电脑上发了一个文件给&lt;code>文件传输助手&lt;/code>，那这个 API 就能在小程序中获取到这个文件的信息。获取到的文件信息包括：&lt;/p>
&lt;ul>
&lt;li>&lt;code>path&lt;/code> 本地临时文件路径(上传时要用到)&lt;/li>
&lt;li>&lt;code>size&lt;/code> 文件大小，单位为 B&lt;/li>
&lt;li>&lt;code>name&lt;/code> 文件名&lt;/li>
&lt;li>&lt;code>type&lt;/code> 文件类型&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>这就要求用户上传 PDF 文件时必须将文件先发送到任何会话中，本质上改变了&lt;strong>上传本地文件&lt;/strong>的需求，所以我谨慎地在标题中没有加入..本地..二字。&lt;/p>
&lt;/blockquote>
&lt;p>拿到了文件的信息后使用&lt;a href="https://developers.weixin.qq.com/miniprogram/dev/api/network/upload/wx.uploadFile.html">&lt;code>wx.uploadFile()&lt;/code>&lt;/a>接口发送请求上传选择好的 PDF 文件至后台，但是要注意的是编译运行前要勾选微信开发者工具中编辑器面板的详情中的&lt;strong>不校验合法域名、web-view (业务域名)、TLS 版本以及 HTTPS 证书&lt;/strong>，原因请查阅小程序官方文档中的&lt;a href="https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html">域名配置说明&lt;/a>。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 小程序页面js文件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="nx">Page&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">filePath&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 文件路径
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">filename&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span> &lt;span class="c1">// 文件名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">chooseFile&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kd">function&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 点击选择文件按钮触发事件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">_that&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">wx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">chooseMessageFile&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="c1">// 会话中选择文件API
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">count&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 可选文件个数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;file&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 文件类型
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">success&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 选择成功后的回调函数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tempFiles&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">size&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 文件大小
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">filename&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tempFiles&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 文件名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">size&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">4194301&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 判断文件大小不能大于4M
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">wx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">showToast&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="c1">// 弹框提示
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;文件大小不能超过4MB！&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">icon&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">duration&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">2000&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">mask&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">filename&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">indexOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pdf&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 判断文件格式必须为pdf
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">wx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">showToast&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;文件格式必须为PDF！&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">icon&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">duration&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">2000&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">mask&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">_that&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setData&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">filePath&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tempFiles&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">path&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 保存文件地址到data
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">filename&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">filename&lt;/span> &lt;span class="c1">// 保存文件名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">uploadFile&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kd">function&lt;/span>&lt;span class="p">(){&lt;/span> &lt;span class="c1">// 上传文件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">_that&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="nx">wx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">uploadFile&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="c1">// 本地资源上传到服务器API
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">url&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;http://localhost:3000/uploadFile&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 指定服务器接口URL
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">filePath&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">_that&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filePath&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 本地文件路径，即选择文件返回的路径
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;file&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 上传文件的key，后台要用到
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">formData&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 可额外添加字段，存于请求的body对象中
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s1">&amp;#39;filename&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">_that&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filename&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">success&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="后台处理数据并存储">后台处理数据并存储&lt;/h4>
&lt;blockquote>
&lt;p>后台使用 Node.js + Express&lt;/p>
&lt;/blockquote>
&lt;p>既然已经知道小程序中上传的是二进制文件了那就好办了，&lt;a href="https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md">multer&lt;/a> 中间件是 Node.js 中处理 &lt;code>multipart/form-data&lt;/code>类型数据的常用解决方案，中文文档写的也很清晰，建议往下看之前先仔细研读其文档，具体用法这里不再赘述了。&lt;/p>
&lt;p>还有一点需要明确——存储 PDF 文件的方式。我们知道，MongoDB 中的 &lt;a href="https://blog.csdn.net/Xue_zenghui/article/details/100982798">GridFs&lt;/a> 支持文件的存储，但是要考虑项目中具体使用情境的影响如上传的文件数量、文件大小、并发量等，为避免文件数据量过大影响数据库存取效率，建议将 PDF 文件的二进制数据存储于..硬盘文件夹..中而不直接存入数据库中(有文件服务器另说)，下载的时候使用 Node.js 的&lt;code>fs&lt;/code>模块再读取文件的二进制流就可以了，此时 Node.js 充当的角色就是简单的静态资源服务器。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// uploadFile.js文件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">express&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;express&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">router&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">express&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Router&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">mongoose&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;mongoose&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">mongoose&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;useFindAndModify&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">multer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;multer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 引入multer中间件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">storage&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">multer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">diskStorage&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="c1">// multer磁盘存储引擎
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">destination&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">file&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cb&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">cb&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;uploads/&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 指定存储位置，必须手动创建此文件夹
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">filename&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">file&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">cb&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 文件重命名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">filename&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filename&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 读取请求中formData中额外设置的filename
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">cb&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">filename&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 将存储的文件重命名
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">upload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">multer&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">storage&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">storage&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/uploadFile&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">upload&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">single&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;file&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 此处的‘file’即小程序中上传文件时指定的name
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="c1">// req.file为文件信息
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="c1">// req.body为文本域信息
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">status&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">router&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此时，小程序中选择好文件后点击上传按钮服务器会把文件的二进制数据存储在代码中指定的 uploads 文件夹中：&lt;/p>
&lt;p>&lt;img src="https://xuezenghui.com/images/upload-pdf.jpeg" alt="upload-pdf.jpeg" title="存储成功的 PDF">&lt;/p>
&lt;h4 id="下载存储的-pdf-文件至本地">下载存储的 PDF 文件至本地&lt;/h4>
&lt;p>至此，小程序上传 PDF 文件的需求已经完成了。&lt;/p>
&lt;p>既然用户上传了文件，我们就要拿到文件的内容，此处使用 Vue 搭建的的简易后台管理系统(说是个系统显然还不够格儿🤪)完成 PDF 文件的下载需求。&lt;/p>
&lt;ul>
&lt;li>先来完成后台接口：&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// upload.js文件中添加
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">...&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">fs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;fs&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 引入fs模块处理文件
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="p">...&lt;/span>
&lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/downloadFile&amp;#39;&lt;/span>&lt;span class="p">,(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 返回文件二进制流数据接口
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">filename&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">req&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filename&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">file&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;./uploads/&amp;#39;&lt;/span>&lt;span class="o">+&lt;/span> &lt;span class="nx">filename&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 请求文件的实际路径
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">writeHead&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 设置响应头
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s1">&amp;#39;Content-Type&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;application/octet-stream&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="c1">// 告诉浏览器这是一个二进制文件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="s1">&amp;#39;Content-Disposition&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;attachment; filename=&amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nb">encodeURI&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filename&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="c1">// 告诉浏览器这是一个需要下载的文件
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">});&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">readStream&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">fs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createReadStream&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">file&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 得到文件输入流
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">readStream&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">chunk&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">chunk&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;binary&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 文档内容以二进制的格式写到response的输出流
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">});&lt;/span>
&lt;span class="nx">readStream&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">on&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;end&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">end&lt;/span>&lt;span class="p">();&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;span class="p">})&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>Vue 前台发送请求下载文件到本地：&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-vue" data-lang="vue">&lt;span class="c">&amp;lt;!--&lt;/span> &lt;span class="nx">下载文件组件download&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">vue&lt;/span> &lt;span class="o">--&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="c">&amp;lt;!--&lt;/span> &lt;span class="nx">使用了vuetify&lt;/span> &lt;span class="nx">UI框架的组件&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Markdown的代码高亮好像不识别了&lt;/span>&lt;span class="o">=&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">--&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">v-btn&lt;/span> &lt;span class="nt">@click&lt;/span>&lt;span class="s">=&amp;#34;download&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="na">下载pdf到本地&lt;/span>&lt;span class="err">&amp;lt;/&lt;/span>&lt;span class="nt">v-btn&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;download&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">data&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">filename&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;test.pdf&amp;#39;&lt;/span> &lt;span class="c1">// 指定要下载的文件名，与后台uploads文件夹中存储的文件名一致
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">};&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">methods&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">download&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$axios&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">method&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;post&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">url&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;/api/downloadFile&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 请求URL
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">filename&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filename&lt;/span> &lt;span class="c1">// 请求参数
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="nx">responseType&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;blob&amp;#34;&lt;/span> &lt;span class="c1">// 设置返回的数据类型为二进制数据
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">})&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="nx">then&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">response&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">downloadFile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">response&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 将返回结果作为参数调用本地下载文件方法
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">})&lt;/span>
&lt;span class="p">.&lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">error&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{});&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">downloadFile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kd">var&lt;/span> &lt;span class="nx">_that&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="kd">let&lt;/span> &lt;span class="nx">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">URL&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createObjectURL&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">Blob&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">data&lt;/span>&lt;span class="p">]));&lt;/span> &lt;span class="c1">// 后台返回结果data是个对象，其中的data属性才是文件的二进制数据
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kd">let&lt;/span> &lt;span class="nx">link&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;a&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 创建a标签
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">link&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">display&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 设置a标签不可见
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">link&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">href&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">url&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 设置a标签的URL属性
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">link&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;download&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_that&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filename&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 给a标签添加download属性并指定下载的文件名(记得加后缀指定下载的文件格式)
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">link&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 将a标签节点添加在DOM中
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">link&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 触发a标签
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;p>至此，&lt;strong>小程序上传文件&lt;/strong>➡️&lt;strong>后台存储 PDF 文件&lt;/strong>➡️&lt;strong>后台管理系统下载存储的文件&lt;/strong>的整个流程已经走通😏，具体的业务需求就任君发挥了，比如将后台存储的所有文件名渲染在页面中，点击对应文件名将文件下载到本地、文件名多选再下载到本地等。&lt;/p>
&lt;p>🎱案例 Github 地址：&lt;a href="https://github.com/Xuezenghuigithub/miniProgram_upload-download">小程序中 PDF 文件的上传及下载&lt;/a>&lt;/p></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/%E5%B0%8F%E7%A8%8B%E5%BA%8F/">小程序</category><category domain="https://xuezenghui.com/tags/node.js/">Node.js</category><category domain="https://xuezenghui.com/tags/vue.js/">Vue.js</category></item><item><title>大风起兮云飞扬</title><link>https://xuezenghui.com/posts/projects/</link><guid isPermaLink="true">https://xuezenghui.com/posts/projects/</guid><pubDate>Thu, 26 Sep 2019 10:00:31 +0800</pubDate><author>xuezenghui6@gmail.com (Zander Hsueh)</author><copyright>[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh)</copyright><description>&lt;style>
center span {
margin-right: 12px;
}
&lt;/style>
&lt;iframe frameborder="no" width=100% height=86 src="https://xuezenghui.com//music.163.com/outchain/player?type=2&amp;id=492833245&amp;auto=1&amp;height=66">&lt;/iframe>
&lt;hr>
&lt;center>&lt;span>2019.07.01—2021.05.21&lt;/span>&lt;span>北京百星电子系统有限公司&lt;/span>Front-end Developer&lt;/center>
&lt;hr>
&lt;h2 id="百星公司官网">百星公司官网&lt;/h2>
&lt;p>&lt;strong>📅开发周期&lt;/strong>：2019.07.04—2019.07.18&lt;/p>
&lt;p>&lt;strong>📄项目简介&lt;/strong>：公司官方网站。&lt;/p>
&lt;p>&lt;strong>🔗项目链接&lt;/strong>：&lt;a href="https://www.belstar.com.cn/home/">Belstar&lt;/a>（经过了多次迭代）&lt;/p>
&lt;p>&lt;strong>📐技术架构&lt;/strong>：Vue.js、Stylus&lt;/p>
&lt;p>&lt;strong>🔧相关工具&lt;/strong>：蓝湖、Sketch&lt;/p>
&lt;p>&lt;strong>🔒难点攻克&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;a href="https://img-blog.csdnimg.cn/20190710153454163.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1h1ZV96ZW5naHVp,size_16,color_FFFFFF,t_70">页面小三角&lt;/a>问题&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="p">.&lt;/span>&lt;span class="nc">arrow&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">18&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">right&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">-47&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">border-top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="kc">solid&lt;/span> &lt;span class="mh">#e0e0e0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">border-right&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="kc">solid&lt;/span> &lt;span class="mh">#e0e0e0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="k">transform&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">rotate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">225&lt;/span>&lt;span class="kt">deg&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>📝项目总结&lt;/strong>：作为工作后第一个练手的项目，难度较小。完全的静态页面，暂时不需要连接后台获取数据，只是在细节上有几个点需注意：&lt;/p>
&lt;ol>
&lt;li>Vue 组件的拆分要合理&lt;/li>
&lt;li>git 上传代码的步骤要清晰&lt;/li>
&lt;li>vue-router 滚动行为
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="c1">// 文件位置： /router/index.js
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Router&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">scrollBehavior&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">to&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">from&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">savedPosition&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">x&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">y&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">};&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">});&lt;/span>
&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;h2 id="eportal">ePortal&lt;/h2>
&lt;p>&lt;strong>📅开发周期&lt;/strong>：2019.07.22—2020.&lt;/p>
&lt;p>&lt;strong>📄项目简介&lt;/strong>：公司门户网站，采用 MEVN&lt;sup id="fnref:1">&lt;a href="https://xuezenghui.com/posts/projects/#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> 架构，将薪资系统、物流系统、项目管理系统、工单系统、生产管理系统等多个功能分支整合开发，是对公司内部管理的众多具体功能的实现。&lt;/p>
&lt;p>&lt;strong>🔗项目链接&lt;/strong>：&lt;a href="https://eportal.belstar.com.cn">ePortal&lt;/a>&lt;/p>
&lt;p>&lt;strong>📐技术架构&lt;/strong>：Vue.js、Vuetify、Node.js、Express、MongoDB、Mongoose&lt;/p>
&lt;p>&lt;strong>🔧相关工具&lt;/strong>：Sketch、Robo 3T、Postman&lt;/p>
&lt;p>&lt;strong>🔒难点攻克&lt;/strong>：&lt;/p>
&lt;p>&lt;strong>📝项目总结&lt;/strong>：一次真正的锻炼机会。项目采用分组合作、模块分配至个人的开发方式，每个人既负责某模块的前端开发任务，同时也负责另一个模块的服务端开发工作。技术角度上基本是从零开始的——Node.js、Express、MongoDB，高效学习并应用于项目中，遇到问题也几乎都是自主解决。项目开发过程基本遵循 Git Flow 工作流，但开发前期在代码规范和复用方面做的并不好，后期在前辈的带领下对整个项目进行了..重构..，提升了总体的健壮性和规范性。目前仍处于持续的功能迭代阶段，ePortal 项目永不结案😄！&lt;/p>
&lt;h2 id="生产管理微信小程序">生产管理微信小程序&lt;/h2>
&lt;p>&lt;strong>📅开发周期&lt;/strong>：2019.07.22—2019.12.25&lt;/p>
&lt;p>&lt;strong>📄项目简介&lt;/strong>：与 ePortal 项目的生产管理模块搭配使用并依托其进行上线，主要功能是用户根据其权限录入对应制作中心内打印机器的打印量，录入方式分为小程序内录入和扫描小程序码带参进入小程序录入页面录入，且可查看权限内制作中心里所有机器的录入历史记录。&lt;/p>
&lt;p>&lt;strong>🔗项目链接&lt;/strong>：微信内搜索小程序「工作量收集小助手」&lt;/p>
&lt;p>&lt;strong>📐技术架构&lt;/strong>：小程序开发语言、Node.js、Express、MongoDB、Mongoose&lt;/p>
&lt;p>&lt;strong>🔧相关工具&lt;/strong>：微信开发者工具&lt;/p>
&lt;p>&lt;strong>🔒难点攻克&lt;/strong>：&lt;/p>
&lt;p>&lt;strong>📝项目总结&lt;/strong>：起初与 ePortal 同步开发，由组内其它成员负责，后协助进行界面的完善及整体逻辑的修改优化。第一次接触小程序开发，在熟悉 Vue 语法的基础下摸清小程序的目录结构后进行得还是比较顺利的。唯一要提的就是微信小程序的官方文档写的真不咋地🤪，API 用法和参数描述得含糊不清让人走了不少弯路。&lt;/p>
&lt;h2 id="bit-poc">Bit POC&lt;/h2>
&lt;p>&lt;strong>📅开发周期&lt;/strong>：2020.03.23—2020.04.30&lt;/p>
&lt;p>&lt;strong>📄项目简介&lt;/strong>：POC&lt;sup id="fnref:2">&lt;a href="https://xuezenghui.com/posts/projects/#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>类型项目，首要目的是验证使用 &lt;a href="https://docs.bit.dev/docs/quick-start">Bit&lt;/a> 组件化管理前端代码的可行性。&lt;/p>
&lt;p>&lt;strong>🔗项目链接&lt;/strong>：无&lt;/p>
&lt;p>&lt;strong>📐技术架构&lt;/strong>：Bit、Vue.js、Linux&lt;/p>
&lt;p>&lt;strong>🔧相关工具&lt;/strong>：无&lt;/p>
&lt;p>&lt;strong>🔒难点攻克&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>组件文档没有较好的管理方式，已尝试 bit.dev（不开源且费用不菲）、Storybook（不支持生产环境），皆不可行，最后采用的方案是 &lt;a href="http://typecho.org/">Typecho&lt;/a>。&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>📝项目总结&lt;/strong>：初次接触 Bit，进行了系统地学习，撰写了 Bit 组件管理手册并完成相关的技术验证，相关内容见我的这篇&lt;a href="../bit/">博文&lt;/a>。&lt;/p>
&lt;h2 id="委外设备管理系统">委外设备管理系统&lt;/h2>
&lt;p>&lt;strong>📅开发周期&lt;/strong>：2020.05.12—2020.11.12&lt;/p>
&lt;p>&lt;strong>📄项目简介&lt;/strong>：依旧是 ePortal 工程的功能迭代，要问为什么伶出来，其一，该项目与多个第三方平台对接，如钉钉、GitLab、微信小程序；其二，也是项目周期跨度如此大的原因——组织变更，中期由我接手负责该项目的管理工作。当然，最终顺利部署上线。&lt;/p>
&lt;p>&lt;strong>🔗项目链接&lt;/strong>：&lt;a href="https://eportal.belstar.com.cn/prodMgt">ePortal-生产管理&lt;/a>&lt;/p>
&lt;p>&lt;strong>📐技术架构&lt;/strong>：Vue.js、Vuetify、Node.js、Express、MongoDB、Mongoose&lt;/p>
&lt;p>&lt;strong>🔧相关工具&lt;/strong>：Flexmonster、MongoDB Compass、Trello&lt;/p>
&lt;p>&lt;strong>🔒难点攻克&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>aggregate 大数据量大、多管道操作情景下的接口优化&lt;/li>
&lt;/ol>
&lt;p>业务中遇到了聚合管道操作 30w 条数据的情况，导致接口频繁超时，先使用 MongoDB Compass 分析管道操作耗时，从管道操作顺序优化、集合索引优化、业务查询方式优化等多角度入手，配合服务端定时任务完成 API 请求耗时由 &amp;gt;10s 到 &amp;lt;50ms 的转变。&lt;/p>
&lt;p>&lt;strong>📝项目总结&lt;/strong>：事实证明 Team Leader 和 Project Manager 不是多么容易当的，尤其考虑所在环境，项目期间基本是被当开发、设计、产品经理、项目经理、测试、运维都使了一遍。如果要总结这个项目学到了什么，那我一定会忘记繁杂的业务、抛弃使用得驾轻就熟的 aggregate 而选择“管理”和“沟通”，完备合理的项目管理能使团队达到 1+1&amp;gt;2 的效果，而做好协调沟通工作也并非易事，仅此一役，受益匪浅。&lt;/p>
&lt;h2 id="企业报价系统">企业报价系统&lt;/h2>
&lt;p>&lt;strong>📅开发周期&lt;/strong>：2020.11.18—2021.02.20&lt;/p>
&lt;p>&lt;strong>📄项目简介&lt;/strong>：用于企业内部销售部门进行公司产品服务的业务报价，规划灵活易扩展的价格结构及价格维护方式，多种报价模式下导出报价单，审批流程接入钉钉 API。Domain Service 架构，前端集成于 ePortal，后端服务切分、领域驱动，采用 Nest.js + GraphQL 架构。&lt;/p>
&lt;p>&lt;strong>🔗项目链接&lt;/strong>：无&lt;/p>
&lt;p>&lt;strong>📐技术架构&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>客户端：Vue.js、Vuetify、Apollo-Client&lt;/li>
&lt;li>服务端：Node.js、Nest.js、TypeScript、Apollo-Server、Mongoose&lt;/li>
&lt;li>数据库：MongoDB&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>🔧相关工具&lt;/strong>：Trello&lt;/p>
&lt;p>&lt;strong>🔒难点攻克&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>📝项目总结&lt;/strong>：&lt;/p>
&lt;center>&lt;span>2021.05.31—2021.09.21&lt;/span>&lt;span>北京韦尔科技有限公司&lt;/span>Front-end Developer&lt;/center>
&lt;center>&lt;span>2021.10.06—至今&lt;/span>&lt;span>Costco Wholesale China&lt;/span>Software Developer&lt;/center>
&lt;h2 id="architecture-optimization--devops">Architecture Optimization &amp;amp; DevOps&lt;/h2>
&lt;p>&lt;strong>📅开发周期&lt;/strong>：2020.10.15—2021.01.05&lt;/p>
&lt;p>&lt;strong>📄项目简介&lt;/strong>：&lt;/p>
&lt;p>&lt;strong>🔗项目链接&lt;/strong>：无&lt;/p>
&lt;p>&lt;strong>📐技术架构&lt;/strong>：&lt;/p>
&lt;p>&lt;strong>🔧相关工具&lt;/strong>：&lt;/p>
&lt;p>&lt;strong>🔒难点攻克&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>📝项目总结&lt;/strong>：&lt;/p>
&lt;section class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>MongoDB + Express + Vue.js + Node.js 技术架构体系。 &lt;a href="https://xuezenghui.com/posts/projects/#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>Proof of concept，&lt;a href="https://zh.wikipedia.org/wiki/%E6%A6%82%E5%BF%B5%E9%AA%8C%E8%AF%81">概念验证&lt;/a>，对一个想法、一种技术的不完整实现，以证明其可行性。 &lt;a href="https://xuezenghui.com/posts/projects/#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/section></description><category domain="https://xuezenghui.com/categories/tech/">Tech</category><category domain="https://xuezenghui.com/tags/%E9%A1%B9%E7%9B%AE/">项目</category></item></channel></rss>