<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>肖陈伟的博客</title>
  
  <subtitle>人工智能 / 全栈 / 开发运维</subtitle>
  <link href="/cn/blog/atom.xml" rel="self"/>
  
  <link href="https://chanvinxiao.com/cn/blog/"/>
  <updated>2025-04-20T22:16:48.298Z</updated>
  <id>https://chanvinxiao.com/cn/blog/</id>
  
  <author>
    <name>chanvinxiao.com</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>数学公式中常用的 LaTeX 符号</title>
    <link href="https://chanvinxiao.com/cn/blog/common-latex-symbols-used-in-mathematics-formulas/"/>
    <id>https://chanvinxiao.com/cn/blog/common-latex-symbols-used-in-mathematics-formulas/</id>
    <published>2025-04-20T00:50:37.000Z</published>
    <updated>2025-04-20T22:16:48.298Z</updated>
    
    <content type="html"><![CDATA[<style>#post-common-latex-symbols-used-in-mathematics-formulas {  pre {    display: inline-block;    background: #eee;    color: #555;    margin: 2px 0;    padding: 5px;    vertical-align: middle;  }  span {    padding: 0 3px;  }  dd {    margin-top: 2px;  }}</style><p>LaTeX 符号常用于数学公式，以下分别从高等数学、线性代数和离散数学进行罗列并举例。</p><a id="more"></a><h3 id="高等数学"><a href="#高等数学" class="headerlink" title="高等数学"></a>高等数学</h3><dl>  <dt>加减（正负） / 乘 / 除</dt>  <dd><span>\(\pm\)</span> / <span>\(\times\)</span> / <span>\(\div\)</span>: <code>\pm</code> / <code>\time</code> / <code>\div</code></dd>  <dd><span>\(\pm\infty\)</span> / <span>\(n!=n(n-1)\cdots2\times1\)</span> / <span>\(n\div d\)</span>: <code>\pm\infty</code> / <code>n!=n(n-1)\cdots2\times1</code> / <code>n\div d</code></dd></dl><dl>  <dt>复合</dt>  <dd><span>\(\circ\)</span>: <code>\circ</code></dd>  <dd><span>\((f\circ g)(x)=f[g(x)]\)</span>: <code>(f\circ g)(x)=f[g(x)]</code></dd></dl><dl>  <dt>去心邻域</dt>  <dd><span>\(\mathring{}\)</span>: <code>\mathring{}</code></dd>  <dd><span>\(\mathring{U}(x_0,\delta)\)</span>: <code>\mathring{U}(x_0,\delta)</code></dd></dl><dl>  <dt>分式</dt>  <dd><span>\(\frac{}{}\)</span>: <code>\frac{}{}</code></dd>  <dd><span>\(\frac{dy}{dx}=-\frac{F_x}{F_y}\)</span>: <code>\frac{dy}{dx}=-\frac{F_x}{F_y}</code></dd></dl><dl>  <dt>极限</dt>  <dd><span>\(\lim\)</span>: <code>\lim</code></dd>  <dd><span>\(\lim\limits_{x \to \infty}f(x)=A\)</span>: <code>\lim\limits_{x\to\infty}f(x)=A</code></dd></dl><dl>  <dt>开平方 / 开 n 次方</dt>  <dd><span>\(\sqrt{}\)</span> / <span>\(\sqrt[n]{}\)</span>: <code>\sqrt{}</code> / <code>\sqrt[n]{}</code></dd>  <dd><span>\(ds=\sqrt{1+y'^2}dx\)</span> / <span>\(\lim_{n\to\infty}\sqrt[n]{n}=1\)</span>: <code>ds=\sqrt{1+y'^2}dx</code> / <code>\lim_{n\to\infty}\sqrt[n]{n}=1</code></dd></dl><dl>  <dt>不等于 / 约等于</dt>  <dd><span>\(\neq\)</span> / <span>\(\approx\)</span>: <code>\neq</code> / <code>\approx</code></dd>  <dd><span>\(f'(x_0)\neq0\)</span> / <span>\(\Delta y\approx f'(x_0)\Delta x\)</span>: <code>f'(x_0)\neq0</code> / <code>\Delta y\approx f'(x_0)\Delta x</code></dd></dl><dl>  <dt>小于等于 / 大于等于 / 远小于 / 远大于</dt>  <dd><span>\(\leq\)</span> / <span>\(\geq\)</span> / <span>\(\ll\)</span> / <span>\(\gg\)</span>: <code>\leq</code> / <code>\geq</code> / <code>\ll</code> / <code>\gg</code></dd>  <dd><span>\(\ln x\leq x-1\)</span> / <span>\(e^x\geq 1+x\)</span> / <span>\(|y'|\ll1\)</span> / <span>\(1\ll|y'|\)</span>: <code>\ln x\leq x-1</code> / <code>e^x\geq 1+x</code> / <code>|y'|\ll1</code> / <code>1\ll|y'|</code></dd></dl><dl>  <dt>偏导</dt>  <dd><span>\(\partial\)</span>: <code>\partial</code></dd>  <dd><span>\(\frac{\partial^2z}{\partial x\partial y}=f_{xy}(x,y)\)</span>: <code>\frac{\partial^2z}{\partial x\partial y}=f_{xy}(x,y)</code></dd></dl><dl>  <dt>积分</dt>  <dd><span>\(\int\)</span>: <code>\int</code></dd>  <dd><span>\(\int f(x)dx=F(x)+C\)</span>: <code>\int f(x)dx=F(x)+C</code></dd></dl><dl>  <dt>二重积分</dt>  <dd><span>\(\iint\)</span>: <code>\iint</code></dd>  <dd><span>\(\iint\limits_Df(x,y)d\sigma=\iint\limits_Df(x,y)dxdy\)</span>: <code>\iint\limits_Df(x,y)d\sigma=\iint\limits_Df(x,y)dxdy</code></dd></dl><dl>  <dt>平均</dt>  <dd><span>\(\overline{}\)</span>: <code>\overline{}</code></dd>  <dd><span>\(\overline{y}=\frac{1}{b-a}\int_a^by(x)dx\)</span>: <code>\overline{y}=\frac{1}{b-a}\int_a^by(x)dx</code></dd></dl><h3 id="线性代数"><a href="#线性代数" class="headerlink" title="线性代数"></a>线性代数</h3><dl>  <dt>行列式</dt>  <dd><span>\(\begin{vmatrix}\end{vmatrix}\)</span>: <code>\begin{vmatrix}\end{vmatrix}</code></dd>  <dd>    <span>\(D=\begin{vmatrix}a_{11}&a_{12}&\cdots&a_{1n}\\a_{21}&a_{22}&\cdots&a_{2n}\\\vdots&\vdots&\vdots&\vdots\\a_{n1}&a_{n2}&\cdots&a_{nn}\end{vmatrix}\)</span>:    <pre>D=\begin{vmatrix}a_{11}&a_{12}&\cdots&a_{1n}\\a_{21}&a_{22}&\cdots&a_{2n}\\\vdots&\vdots&\vdots&\vdots\\a_{n1}&a_{n2}&\cdots&a_{nn}\end{vmatrix}</pre>  </dd></dl><dl>  <dt>等号上说明</dt>  <dd><span>\(\xlongequal{}\)</span>: <code>\xlongequal{}</code></dd>  <dd>    <span>\(\begin{vmatrix}a_{11}&a_{12}&\cdots&a_{1n}\\\vdots&\vdots&\vdots&\vdots\\ka_{i1}&ka_{i2}&\cdots&ka_{in}\\\vdots&\vdots&\vdots&\vdots\\a_{n1}&a_{n2}&\cdots&a_{nn}\end{vmatrix}\xlongequal{r_i\div k}k\begin{vmatrix}a_{11}&a_{12}&\cdots&a_{1n}\\\vdots&\vdots&\vdots&\vdots\\a_{i1}&a_{i2}&\cdots&a_{in}\\\vdots&\vdots&\vdots&\vdots\\a_{n1}&a_{n2}&\cdots&a_{nn}\end{vmatrix}\)</span>:    <pre>\begin{vmatrix}a_{11}&a_{12}&\cdots&a_{1n}\\\vdots&\vdots&\vdots&\vdots\\ka_{i1}&ka_{i2}&\cdots&ka_{in}\\\vdots&\vdots&\vdots&\vdots\\a_{n1}&a_{n2}&\cdots&a_{nn}\end{vmatrix}\xlongequal{r_i\div k}k\begin{vmatrix}a_{11}&a_{12}&\cdots&a_{1n}\\\vdots&\vdots&\vdots&\vdots\\a_{i1}&a_{i2}&\cdots&a_{in}\\\vdots&\vdots&\vdots&\vdots\\a_{n1}&a_{n2}&\cdots&a_{nn}\end{vmatrix}</pre>  </dd></dl><dl>  <dt>连乘</dt>  <dd><span>\(\prod\)</span>: <code>\prod</code></dd>  <dd>    <span>\(\begin{vmatrix}1&1&\cdots&1\\x_1&x_2&\cdots&x_n\\x_1^2&x_2^2&\cdots&x_n^2\\\vdots&\vdots&\vdots&\vdots\\x_1^{n-1}&x_2^{n-1}&\cdots&x_n^{n-1}\\\end{vmatrix}=\prod_{1\leq i&ltj\leq n}(x_j-x_i)\)</span>:    <pre>\begin{vmatrix}1&1&\cdots&1\\x_1&x_2&\cdots&x_n\\x_1^2&x_2^2&\cdots&x_n^2\\\vdots&\vdots&\vdots&\vdots\\x_1^{n-1}&x_2^{n-1}&\cdots&x_n^{n-1}\\\end{vmatrix}=\prod_{1\leq i&ltj\leq n}(x_j-x_i)</pre>  </dd></dl><dl>  <dt>矩阵</dt>  <dd><span>\(\begin{pmatrix}\end{pmatrix}\)</span>: <code>\begin{pmatrix}\end{pmatrix}</code></dd>  <dd>    <span>\(A=\begin{pmatrix}a_{11}&a_{12}&\cdots&a_{1n}\\a_{21}&a_{22}&\cdots&a_{2n}\\\vdots&\vdots&\vdots&\vdots\\a_{m1}&a_{m2}&\cdots&a_{mn}\end{pmatrix}\)</span>:    <pre>A=\begin{pmatrix}a_{11}&a_{12}&\cdots&a_{1n}\\a_{21}&a_{22}&\cdots&a_{2n}\\\vdots&\vdots&\vdots&\vdots\\a_{m1}&a_{m2}&\cdots&a_{mn}\end{pmatrix}</pre>  </dd></dl><dl>  <dt>左大括号</dt>  <dd><span>\(\begin{cases}\end{cases}\)</span>: <code>\begin{cases}\end{cases}</code></dd>  <dd>    <span>\(\begin{cases}a_{11}x_1+a_{12}x_2+\cdots+a_{1n}x_n=b_1,\\a_{21}x_1+a_{22}x_2+\cdots+a_{2n}x_n=b_2,\\\cdots\ \cdots\ \cdots\ \cdots\\a_{m1}x_1+a_{m2}x_2+\cdots+a_{mn}x_n=b_m,\end{cases}\)</span>:    <pre>\begin{cases}a_{11}x_1+a_{12}x_2+\cdots+a_{1n}x_n=b_1,\\a_{21}x_1+a_{22}x_2+\cdots+a_{2n}x_n=b_2,\\\cdots\ \cdots\ \cdots\ \cdots\\a_{m1}x_1+a_{m2}x_2+\cdots+a_{mn}x_n=b_m,\end{cases}</pre>  </dd></dl><dl>  <dt>等价 / 合同</dt>  <dd><span>\(\cong\)</span> / <span>\(\simeq\)</span>: <code>\cong</code> / <code>\simeq</code></dd>  <dd><span>\(A\cong B\)</span> / <span>\(A\simeq B\)</span>: <code>A\cong B</code> / <code>A\simeq B</code></dd></dl><h3 id="离散数学"><a href="#离散数学" class="headerlink" title="离散数学"></a>离散数学</h3><dl>  <dt>合取 / 析取 / 异或</dt>  <dd><span>\(\land\)</span> / <span>\(\lor\)</span> / <span>\(\oplus\)</span>: <code>\land</code> / <code>\lor</code> / <code>\oplus</code></dd>  <dd><span>\(p\land q\)</span> / <span>\(p\lor q\)</span> / <span>\(p\oplus q\)</span>: <code>p\land q</code> / <code>p\lor q</code> / <code>p\oplus q</code></dd></dl><dl>  <dt>连续合取 / 连续析取</dt>  <dd><span>\(\bigwedge\)</span> / <span>\(\bigvee\)</span>: <code>\bigwedge</code> / <code>\bigvee</code></dd>  <dd><span>\(\neg(\underset{j=1}{\overset{n}\bigwedge}p_j)\equiv\underset{j=1}{\overset{n}\bigvee}\neg p_j\)</span>: <pre>\neg(\underset{j=1}{\overset{n}\bigwedge}p_j)\equiv\underset{j=1}{\overset{n}\bigvee}\neg p_j</pre></dd></dl><dl>  <dt>蕴含 / 双向蕴含</dt>  <dd><span>\(\to\)</span> / <span>\(\leftrightarrow\)</span>: <code>\to</code> / <code>\leftrightarrow</code></dd>  <dd><span>\(p\to q\)</span> / <span>\(p\leftrightarrow q\)</span>: <code>p\to q</code> / <code>p\leftrightarrow q</code></dd></dl><dl>  <dt>全称量词 / 存在量词</dt>  <dd><span>\(\forall\)</span> / <span>\(\exists\)</span>: <code>\forall</code> / <code>\exists</code></dd>  <dd><span>\(\forall xp(x)\)</span> / <span>\(\exists xp(x)\)</span>: <code>\forall xp(x)</code> / <code>\exists xp(x)</code></dd></dl><dl>  <dt>因此</dt>  <dd><span>\(\therefore\)</span>: <code>\therefore</code></dd>  <dd>    <span>\(\begin{align}&p\\&\underline{p\to q}\\\therefore\ &q\end{align}\)</span>:    <pre>\begin{align}&p\\&\underline{p\to q}\\\therefore\ &q\end{align}</pre>  </dd></dl><dl>  <dt>属于 / 不属于</dt>  <dd><span>\(\in\)</span> / <span>\(\notin\)</span>: <code>\in</code> / <code>\notin</code></dd>  <dd><span>\(a\in A\)</span> / <span>\(a\notin A\)</span>: <code>a\in A</code> / <code>a\notin A</code></dd></dl><dl>  <dt>子集 / 父集 / 真子集 / 真父集</dt>  <dd><span>\(\subseteq\)</span> / <span>\(\supseteq\)</span> / <span>\(\subset\)</span> / <span>\(\supset\)</span>: <code>\subseteq</code> / <code>\supseteq</code> / <code>\subset</code> / <code>\supset</code></dd>  <dd><span>\(A\subseteq B\)</span> / <span>\(B\supseteq A\)</span> / <span>\(A\subset B\)</span> / <span>\(B\supset A\)</span>: <code>A\subseteq B</code> / <code>B\supseteq A</code> / <code>A\subset B</code> / <code>B\supset A</code></dd></dl><dl>  <dt>空集 / 阿列夫零</dt>  <dd><span>\(\varnothing\)</span> / <span>\(\aleph_0\)</span>: <code>\varnothing</code> / <code>\aleph_0</code></dd>  <dd><span>\(\varnothing\subseteq S\)</span> / <span>\(|S|=\aleph_0\)</span>: <code>\varnothing\subseteq S</code> / <code>|S|=\aleph_0</code></dd></dl><dl>  <dt>并集 / 交集</dt>  <dd><span>\(\cup\)</span> / <span>\(\cap\)</span>: <code>\cup</code> / <code>\cap</code></dd>  <dd><span>\(A\cup B\)</span> / <span>\(A\cap B\)</span>: <code>A\cup B</code> / <code>A\cap B</code></dd></dl><dl>  <dt>连续并集 / 连续交集</dt>  <dd><span>\(\bigcup\)</span> / <span>\(\bigcap\)</span>: <code>\bigcup</code> / <code>\bigcap</code></dd>  <dd><span>\(\underset{i=1}{\overset{n}\bigcup}A_i\)</span> / <span>\(\underset{i=1}{\overset{n}\bigcap}A_i\)</span>: <code>\underset{i=1}{\overset{n}\bigcup}A_i</code> / <code>\underset{i=1}{\overset{n}\bigcap}A_i</code></dd></dl><dl>  <dt>布尔积</dt>  <dd><span>\(\odot\)</span>: <code>\odot</code></dd>  <dd><span>\(A\odot B\)</span>: <code>A\odot B</code></dd></dl><dl>  <dt>求和</dt>  <dd><span>\(\sum\)</span>: <code>\sum</code></dd>  <dd><span>\(\sum_{k=1}^{n}k=\frac{n(n+1)}{2}\)</span>: <code>\sum_{k=1}^{n}k=\frac{n(n+1)}{2}</code></dd></dl><dl>  <dt>下大括号</dt>  <dd><span>\(\underbrace{}\)</span>: <code>\underbrace{}</code></dd>  <dd><span>\(A^R=\underbrace{AAA\cdots A}_{(r个A相乘)}\)</span>: <code>A^R=\underbrace{AAA\cdots A}_{(r个A相乘)}</code></dd></dl><dl>  <dt>不大于 x 的最大整数 / 不小于 x 的最小整数</dt>  <dd><span>\(\lfloor x\rfloor\)</span> / <span>\(\lceil x\rceil\)</span>: <code>\lfloor x\rfloor</code> / <code>\lceil x\rceil</code></dd>  <dd><span>\(\lfloor \log_2n\rfloor+1\)</span> / <span>\(\lceil \log_2(n+1)\rceil\)</span>: <code>\lfloor \log_2n\rfloor+1</code> / <code>\lceil \log_2(n+1)\rceil</code></dd></dl><script async src="/cn/blog/js/tex-mml-chtml.js"></script>]]></content>
    
    <summary type="html">
    
      &lt;style&gt;
#post-common-latex-symbols-used-in-mathematics-formulas {
  pre {
    display: inline-block;
    background: #eee;
    color: #555;
    margin: 2px 0;
    padding: 5px;
    vertical-align: middle;
  }
  span {
    padding: 0 3px;
  }
  dd {
    margin-top: 2px;
  }
}
&lt;/style&gt;

&lt;p&gt;LaTeX 符号常用于数学公式，以下分别从高等数学、线性代数和离散数学进行罗列并举例。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="LaTeX" scheme="https://chanvinxiao.com/cn/blog/tags/LaTeX/"/>
    
      <category term="Mathematics" scheme="https://chanvinxiao.com/cn/blog/tags/Mathematics/"/>
    
  </entry>
  
  <entry>
    <title>GitHub Workflow 和 Action 的一些注意事项</title>
    <link href="https://chanvinxiao.com/cn/blog/github-workflow-action-caveats/"/>
    <id>https://chanvinxiao.com/cn/blog/github-workflow-action-caveats/</id>
    <published>2024-12-29T08:55:22.000Z</published>
    <updated>2024-12-29T12:58:55.985Z</updated>
    
    <content type="html"><![CDATA[<p>GitHub 的 workflow 和 action 存在着一些注意事项，包括 workflow 的 yaml 配置，action 的脚本编写，以及对应的 branch 的保护设置，总结如下，以供参考</p><a id="more"></a><h2 id="Workflow"><a href="#Workflow" class="headerlink" title="Workflow"></a>Workflow</h2><h3 id="on-issues-types"><a href="#on-issues-types" class="headerlink" title="on.issues.types"></a>on.issues.types</h3><p>如果需要判断 label，不需要指定 <code>opened</code>，只需要指定 <code>labeled</code>，因为即使 label 是新建时设置的，也会触发 <code>labeled</code></p><h3 id="permissions"><a href="#permissions" class="headerlink" title="permissions"></a>permissions</h3><p>如果需要 checkout 当前 repo，需要添加 <code>contents: write</code>，否则会有权限问题</p><h3 id="jobs-check-steps-env"><a href="#jobs-check-steps-env" class="headerlink" title="jobs.check.steps.env"></a>jobs.check.steps.env</h3><p>如果需要使用 GitHub API，需要添加环境变量</p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">GITHUB_TOKEN</span>: <span class="variable">$&#123;&#123; secrets.GITHUB_TOKEN &#125;</span>&#125;</span><br></pre></td></tr></table></figure><h2 id="Action"><a href="#Action" class="headerlink" title="Action"></a>Action</h2><h3 id="List-repository-issues"><a href="#List-repository-issues" class="headerlink" title="List repository issues"></a>List repository issues</h3><p><a href="https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues" target="_blank" rel="noopener">API</a> 不仅返回 issues，也会返回 prs，默认 30 条每页，可以指定 <code>labels</code> 来过滤</p><h3 id="List-pull-requests"><a href="#List-pull-requests" class="headerlink" title="List pull requests"></a>List pull requests</h3><p><a href="https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests" target="_blank" rel="noopener">API</a> 返回所有 pull requests，默认 30 条每页，可以通过 <code>per_page</code> 和 <code>page</code> 参数做分页处理，例如：</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">const prs = [];</span><br><span class="line"><span class="keyword">for</span> (let<span class="built_in"> page </span>= 1; ; page++) &#123;</span><br><span class="line">  const &#123; data &#125; = await octokit.rest.pulls.list(&#123;</span><br><span class="line">    <span class="built_in">..</span>.context.repo,</span><br><span class="line">    base: `refs/heads/main`,</span><br><span class="line">    state: <span class="string">'open'</span>,</span><br><span class="line">    per_page: PER_PAGE,</span><br><span class="line">    page,</span><br><span class="line">  &#125;);</span><br><span class="line">  prs.push(<span class="built_in">..</span>.data);</span><br><span class="line">  <span class="keyword">if</span> (data.length &lt; PER_PAGE) &#123;</span><br><span class="line">    break;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Get-commit-status"><a href="#Get-commit-status" class="headerlink" title="Get commit status"></a>Get commit status</h3><p><a href="https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference" target="_blank" rel="noopener">API</a> 列出所有 contexts 对应的 state，可以过滤当前的 context</p><h3 id="Create-commit-status"><a href="#Create-commit-status" class="headerlink" title="Create commit status"></a>Create commit status</h3><p><a href="https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#create-a-commit-status" target="_blank" rel="noopener">API</a> 用于创建 commit 的 status，可指定 <code>state</code>，<code>context</code> 和 <code>description</code>（作为运行结果显示）</p><h2 id="Setting"><a href="#Setting" class="headerlink" title="Setting"></a>Setting</h2><h3 id="Require-status-checks"><a href="#Require-status-checks" class="headerlink" title="Require status checks"></a>Require status checks</h3><p>需要在 Branch protection rule 中的 Protect matching branches 下，勾选 Require status checks to pass before merging</p><h3 id="Add-required-checks"><a href="#Add-required-checks" class="headerlink" title="Add required checks"></a>Add required checks</h3><p>需要将当前的 GitHub action 添加到 Status checks that are required</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;GitHub 的 workflow 和 action 存在着一些注意事项，包括 workflow 的 yaml 配置，action 的脚本编写，以及对应的 branch 的保护设置，总结如下，以供参考&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="GitHub" scheme="https://chanvinxiao.com/cn/blog/tags/GitHub/"/>
    
      <category term="Workflow" scheme="https://chanvinxiao.com/cn/blog/tags/Workflow/"/>
    
      <category term="Action" scheme="https://chanvinxiao.com/cn/blog/tags/Action/"/>
    
  </entry>
  
  <entry>
    <title>React 类组件转换为函数式</title>
    <link href="https://chanvinxiao.com/cn/blog/transform-class-react-components-to-functional/"/>
    <id>https://chanvinxiao.com/cn/blog/transform-class-react-components-to-functional/</id>
    <published>2023-12-29T13:05:16.000Z</published>
    <updated>2023-12-30T09:46:04.195Z</updated>
    
    <content type="html"><![CDATA[<p>函数式的 React 组件更加现代，并支持有用的 hooks，现在流行把旧式的类组件转换为函数式组件。这篇文章总结了转换的一些通用的步骤和陷阱。</p><a id="more"></a><h2 id="通用替换"><a href="#通用替换" class="headerlink" title="通用替换"></a>通用替换</h2><h3 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h3><p>从</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> (<span class="params">\w+</span>) <span class="keyword">extends</span> <span class="title">Component</span> <span class="title">\</span></span>&#123;</span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight coffeescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">const $1: FC = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br></pre></td></tr></table></figure><ul><li>这是没有 <code>export</code> 和 <code>props</code> 的场景</li></ul><p>从</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(export) <span class="keyword">default</span> <span class="class"><span class="keyword">class</span> (<span class="params">\w+</span>) <span class="keyword">extends</span> <span class="title">Component</span> <span class="title">\</span></span>&#123;</span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$<span class="number">1</span> <span class="keyword">const</span> $<span class="number">2</span>: FC&lt;$<span class="number">2</span>Props&gt; = () =&gt; &#123;</span><br></pre></td></tr></table></figure><ul><li>作为第二个捕捉的单词，<code>$2</code> 就是组件名。</li><li><code>$2Props</code> 应该定义为 <code>props</code> 的接口名。</li></ul><h3 id="Attributes-前缀"><a href="#Attributes-前缀" class="headerlink" title="Attributes 前缀"></a>Attributes 前缀</h3><p>从</p><figure class="highlight pf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">this\.(<span class="keyword">state</span>\.|props\.)?</span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"> </span><br></pre></td></tr></table></figure><ul><li>假设 <code>props</code> 被统一解构。  </li></ul><h3 id="生命周期函数"><a href="#生命周期函数" class="headerlink" title="生命周期函数"></a>生命周期函数</h3><p>从</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="title">componentDidMount</span><span class="params">()</span></span> &#123;</span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight coffeescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">useEffect(<span class="function"><span class="params">()</span> =&gt;</span> &#123;&#125;, []);</span><br></pre></td></tr></table></figure><ul><li><code>componentDidUpdate</code> 也可以被转换为 <code>useEffect</code>，并设置合适的依赖。</li><li><code>componentWillUnmount</code> 可以转换为对应 <code>useEffect</code> 处理函数的返回函数。</li></ul><h3 id="State-相关语句"><a href="#State-相关语句" class="headerlink" title="State 相关语句"></a>State 相关语句</h3><p>从</p><figure class="highlight pf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">state</span> = &#123;</span><br><span class="line">  data: null,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight haskell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title">const</span> [<span class="class"><span class="keyword">data</span>, setData] = useState();</span></span><br></pre></td></tr></table></figure><p>从</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.setState(&#123;</span><br><span class="line">  <span class="keyword">data</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="title">setData</span><span class="params">(data)</span></span></span><br></pre></td></tr></table></figure><h3 id="类方法"><a href="#类方法" class="headerlink" title="类方法"></a>类方法</h3><p>从</p><figure class="highlight taggerscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">^(<span class="symbol">\s</span>*)(<span class="symbol">\w</span>+)<span class="symbol">\(</span>(<span class="symbol">\w</span>*)<span class="symbol">\)</span> <span class="symbol">\&#123;</span></span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$<span class="number">1</span><span class="keyword">const</span> $<span class="number">2</span> = ($<span class="number">3</span>) =&gt; &#123;</span><br></pre></td></tr></table></figure><ul><li>这属于常规函数定义。</li><li><code>$1</code> 是空格, <code>$2</code> 是方法名, <code>$3</code> 是参数.</li></ul><p>从</p><figure class="highlight coffeescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">^(\s*)(<span class="function"><span class="params">(\w+)</span> = <span class="params">(async )</span>?\(<span class="params">(\w+(, )?)</span>*\) =&gt;</span>)</span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$<span class="number">1</span><span class="keyword">const</span> $<span class="number">2</span></span><br></pre></td></tr></table></figure><ul><li>这属于箭头函数定义。</li><li><code>$1</code> 是空格, <code>$2</code> 方法名之后的所有内容</li></ul><h3 id="类-Getter"><a href="#类-Getter" class="headerlink" title="类 Getter"></a>类 Getter</h3><p>从</p><figure class="highlight gcode"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">^<span class="comment">(\s*)</span><span class="comment">(get)</span> <span class="comment">(\w+)</span>\<span class="comment">(\)</span></span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$<span class="number">1</span><span class="keyword">const</span> $<span class="number">2</span>\u$<span class="number">3</span> = () =&gt;</span><br></pre></td></tr></table></figure><ul><li><code>\u</code> 表示对后面捕获的单词首字母大写。</li><li>对 getter 的调用应该在方法名后加上 <code>()</code>。</li><li>如果 getter 很简单，可以直接赋值而不用使用函数。</li></ul><h3 id="渲染函数"><a href="#渲染函数" class="headerlink" title="渲染函数"></a>渲染函数</h3><p>从</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">render() &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    &lt;&gt;&lt;<span class="regexp">/&gt;</span></span><br><span class="line"><span class="regexp">  );</span></span><br><span class="line"><span class="regexp">&#125;</span></span><br></pre></td></tr></table></figure><p>改为</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> (</span><br><span class="line">  &lt;&gt;&lt;<span class="regexp">/&gt;</span></span><br><span class="line"><span class="regexp">);</span></span><br></pre></td></tr></table></figure><h2 id="值得关注的陷阱"><a href="#值得关注的陷阱" class="headerlink" title="值得关注的陷阱"></a>值得关注的陷阱</h2><h3 id="命名冲突"><a href="#命名冲突" class="headerlink" title="命名冲突"></a>命名冲突</h3><p>类组件可以具有同名的 <code>attributes</code> 和 <code>props</code>，例如 <code>this.data</code> 和 <code>this.props.data</code>。<br>当 <code>this.data</code> 变为 <code>data</code>，另外 <code>props</code> 经常被解构为 <code>const {data} = props</code>，命名冲突 就产生了。</p><h3 id="State-回调"><a href="#State-回调" class="headerlink" title="State 回调"></a>State 回调</h3><p>通过 <code>this.setState</code>，我们可以设置一个回调，在 state 确实改变时进行调用，但我们需要把这种方式更新为使用更新的 state 作为依赖的 <code>useEffect</code>。</p><h3 id="函数-State"><a href="#函数-State" class="headerlink" title="函数 State"></a>函数 State</h3><p>如果 state 的值是函数，你需要把这个函数包裹在另一个匿名函数中，否则 hook 版本的 <code>setState</code> 会把这个函数视为回调。<br>实际上，在大多情况下，这种 state 是和渲染无关的，所以也许使用 <code>useRef</code> 更加合适。</p><p>这个文章展示了一些使用 RegExp 的替换，可以使类组件到函数式组件的替换简单点，另外指出了一些在这个过程中你可能会遇到的陷阱，可以特别留意下，不过当然，不同的场景会存在更多的工作要处理。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;函数式的 React 组件更加现代，并支持有用的 hooks，现在流行把旧式的类组件转换为函数式组件。这篇文章总结了转换的一些通用的步骤和陷阱。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="Regex" scheme="https://chanvinxiao.com/cn/blog/tags/Regex/"/>
    
      <category term="React" scheme="https://chanvinxiao.com/cn/blog/tags/React/"/>
    
      <category term="functional" scheme="https://chanvinxiao.com/cn/blog/tags/functional/"/>
    
  </entry>
  
  <entry>
    <title>常用 Git 命令行操作</title>
    <link href="https://chanvinxiao.com/cn/blog/common-git-command-line-operation/"/>
    <id>https://chanvinxiao.com/cn/blog/common-git-command-line-operation/</id>
    <published>2022-12-31T01:03:10.000Z</published>
    <updated>2022-12-31T10:57:31.343Z</updated>
    
    <content type="html"><![CDATA[<p>本文记录了一些常用 Git 命令行操作的具体使用方式</p><a id="more"></a><h3 id="git-clone"><a href="#git-clone" class="headerlink" title="git clone"></a><a href="https://git-scm.com/docs/git-clone" target="_blank" rel="noopener">git clone</a></h3><ul><li><p><code>git clone REPOSITORY_URL</code><br>拉取仓库，并使用仓库名作为本地文件名</p></li><li><p><code>git clone REPOSITORY_URL FOLDER</code><br>拉取仓库，并使用 FOLDER 作为本地文件名</p></li></ul><h3 id="git-fetch"><a href="#git-fetch" class="headerlink" title="git fetch"></a><a href="http://git-scm.com/docs/git-fetch" target="_blank" rel="noopener">git fetch</a></h3><ul><li><p><code>git fetch origin</code><br>更新所有远程分支</p></li><li><p><code>git fetch origin BRACH</code><br>更新指定远程分支</p></li></ul><h3 id="git-pull"><a href="#git-pull" class="headerlink" title="git pull"></a><a href="http://git-scm.com/docs/git-pull" target="_blank" rel="noopener">git pull</a></h3><ul><li><p><code>git pull origin</code><br>相当于 <code>fetch</code> + <code>merge</code> 对应的上游分支</p></li><li><p><code>git pull origin BRACH</code><br>拉取指定分支到当前分支</p></li><li><p><code>git pull origin --rebase master</code><br>让本地分支重新基于远端的 master 分支</p></li></ul><h3 id="git-push"><a href="#git-push" class="headerlink" title="git push"></a><a href="http://git-scm.com/docs/git-push" target="_blank" rel="noopener">git push</a></h3><ul><li><p><code>git push origin</code><br>把分支推到远端对应的上游分支</p></li><li><p><code>git push origin BRANCH</code><br>把分支推到远端对应的分支</p></li><li><p><code>git push --set-upstream origin BRANCH</code><br>把分支推到远端对应的分支，并将其设为上游分支（一般第一次提交自己的开发分支需要用到）</p></li><li><p><code>git push -f origin</code><br>把分支强推到远端对应的上游分支（会覆盖远端分支，需要慎用）</p></li><li><p><code>git push origin -d BRANCH</code><br>删除远程分支</p></li></ul><h3 id="git-branch"><a href="#git-branch" class="headerlink" title="git branch"></a><a href="http://git-scm.com/docs/git-branch" target="_blank" rel="noopener">git branch</a></h3><ul><li><p><code>git branch</code><br>列出本地所有分支</p></li><li><p><code>git branch -a</code><br>列出本地和远程分支</p></li><li><p><code>git branch -m NEW_BRANCH</code><br>更新当前分支名</p></li><li><p><code>git branch -d BRANCH</code><br>删除已合并的分支</p></li><li><p><code>git branch -D BRANCH</code><br>强制删除分支（即使未合并）</p></li></ul><h3 id="git-checkout"><a href="#git-checkout" class="headerlink" title="git checkout"></a><a href="http://git-scm.com/docs/git-checkout" target="_blank" rel="noopener">git checkout</a></h3><ul><li><p><code>git checkout BRANCH</code><br>切到对应分支</p></li><li><p><code>git checkout -b NEW_BRANCH</code><br>创建新分支</p></li><li><p><code>git checkout -b NEW_BRANCH BRANCH</code><br>基于 BRANCH 创建新分支</p></li><li><p><code>git checkout SHA-1</code><br>切换到某个提交，也可以用 HEAD~N（N 为 1, 2, 3…）切到上 N 个提交</p></li><li><p><code>git checkout SHA-1 /PATH/TO/FILE</code><br>把文件还原到相应的提交版本</p></li><li><p><code>git checkout --theirs /PATH/TO/FILE</code><br>有冲突时使用对方的文件版本</p></li><li><p><code>git checkout --ours /PATH/TO/FILE</code><br>有冲突时使用自己的文件版本</p></li><li><p><code>git checkout -</code><br>切换到之前的分支，适合在两个分支频繁切换时使用</p></li></ul><h3 id="git-add"><a href="#git-add" class="headerlink" title="git add"></a><a href="http://git-scm.com/docs/git-add" target="_blank" rel="noopener">git add</a></h3><ul><li><p><code>git add .</code><br>把所有增加/修改/删除的文件标识为要提交</p></li><li><p><code>git add /PATH/TO/FILE</code><br>只把单一文件标识为要提交，当有其他不需要提交的文件被修改时可使用</p></li></ul><h3 id="git-commit"><a href="#git-commit" class="headerlink" title="git commit"></a><a href="http://git-scm.com/docs/git-commit" target="_blank" rel="noopener">git commit</a></h3><ul><li><p><code>git commit</code><br>把 <code>git add</code> 标识的文件进行提交</p></li><li><p><code>git commit -a</code><br>把修改/删除的文件进行提交（如果有新增的文件，需要使用 <code>git add</code> 添加）</p></li><li><p><code>git commit -am &quot;MESSAGE&quot;</code><br>把修改/删除的文件进行提交并指定注释（适用于临时或简单注释内容）</p></li><li><p><code>git commit --amend</code><br>更新上一次提交，可以加上 <code>-a</code> 或在之前运行 <code>git add</code> 追加更新文件</p></li><li><p><code>git commit --amend --reset-author</code><br>默认的更新提交是不改变作者的，如果需要改变可以明确配置</p></li></ul><h3 id="git-cherry-pick"><a href="#git-cherry-pick" class="headerlink" title="git cherry-pick"></a><a href="http://git-scm.com/docs/git-cherry-pick" target="_blank" rel="noopener">git cherry-pick</a></h3><ul><li><code>git cherry-pick SHA-1</code><br>把某个提交应用到当前分支</li></ul><h3 id="git-status"><a href="#git-status" class="headerlink" title="git status"></a><a href="http://git-scm.com/docs/git-status" target="_blank" rel="noopener">git status</a></h3><ul><li><code>git status</code><br>查看目前状态</li></ul><h3 id="git-diff"><a href="#git-diff" class="headerlink" title="git diff"></a><a href="http://git-scm.com/docs/git-diff" target="_blank" rel="noopener">git diff</a></h3><ul><li><p><code>git diff</code><br>当前所有修改到的，没被标识为要提交的文件的更新内容</p></li><li><p><code>git diff --cache</code><br>当前所有修改到的，并被标识为要提交的文件的更新内容</p></li><li><p><code>git diff /PATH/TO/FILE</code><br>指定文件的更新内容，同样可以用 <code>--cache</code> 区分</p></li></ul><h3 id="git-log"><a href="#git-log" class="headerlink" title="git log"></a><a href="http://git-scm.com/docs/git-log" target="_blank" rel="noopener">git log</a></h3><ul><li><p><code>git log</code><br>详细显示所有记录</p></li><li><p><code>git log -n 10</code><br>显示最近 10 条记录</p></li><li><p><code>git log --oneline</code><br>简要显示所有记录</p></li><li><p><code>git log --oneline master ^BRANCH | wc -l</code><br>可以计算 BRANCH 和 master 分支相差多少个提交</p></li></ul><h3 id="git-stash"><a href="#git-stash" class="headerlink" title="git stash"></a><a href="http://git-scm.com/docs/git-stash" target="_blank" rel="noopener">git stash</a></h3><ul><li><p><code>git stash</code><br>暂存修改/删除，或已标识为要 commit 的新增的文件</p></li><li><p><code>git stash -u</code><br>暂存修改/删除/新增的文件，即新增文件可以不用 <code>git add</code></p></li><li><p><code>git stash pop</code><br>把暂存的文件重新放出来</p></li></ul><h3 id="git-revert"><a href="#git-revert" class="headerlink" title="git revert"></a><a href="http://git-scm.com/docs/git-revert" target="_blank" rel="noopener">git revert</a></h3><ul><li><p><code>git revert SHA-1</code><br>通过形成一个新提交取消某个提交</p></li><li><p><code>git revert SHA-1 -m 1</code><br>如果是合并节点，需要指定要取消提交对应的父节点<br>例如合并是把 BRANCH_2 合并到 BRANCH_1，那么要在 BRANCH_1 取消这次合并，就应该指定 m 为 1（大多数情况都是这样）</p></li></ul><h3 id="git-reset"><a href="#git-reset" class="headerlink" title="git reset"></a><a href="http://git-scm.com/docs/git-reset" target="_blank" rel="noopener">git reset</a></h3><ul><li><p><code>git reset</code><br>取消对要 commit 的文件的标识（相当于 <code>git add</code> 的撤销）</p></li><li><p><code>git reset --hard</code><br>取消修改/删除或已标识为要 commit 的新增的文件的更新</p></li><li><p><code>git reset SHA-1</code><br>取消从 SHA-1 之后的所有提交，但是保留提交文件的更新<br>如果只想取消上一次提交，SHA-1 可以设为 <code>HEAD^</code></p></li><li><p><code>git reset --hard SHA-1</code><br>取消从 SHA-1 之后的所有提交，而且不保留提交文件的更新</p></li></ul><h3 id="git-rebase"><a href="#git-rebase" class="headerlink" title="git rebase"></a><a href="http://git-scm.com/docs/git-rebase" target="_blank" rel="noopener">git rebase</a></h3><ul><li><p><code>git rebase BRANCH</code><br>让当前分支重新基于 BRANCH</p></li><li><p><code>git rebase -i SHA-1</code><br>更新 SHA-1 以后的提交，可以 <code>pick/p</code>，<code>edit/e</code>，<code>drop/d</code>，<code>squash/s</code> 相应提交<br>如果第一个提交使用 <code>p</code>，后面的提交使用 <code>s</code>，可以把多个提交合并成一个提交   </p></li></ul><h3 id="git-merge"><a href="#git-merge" class="headerlink" title="git merge"></a><a href="http://git-scm.com/docs/git-merge" target="_blank" rel="noopener">git merge</a></h3><ul><li><p><code>git merge BRANCH</code><br>把 BRANCH 合并到当前分支，尽量不形成合并节点</p></li><li><p><code>git merge --no-ff BRANCH</code><br>把 BRANCH 合并到当前分支，并确保形成合并节点</p></li><li><p><code>git merge --squash BRANCH</code><br>把 BRANCH 和当前分支的变更作为标识为要提交的内容，需要运行 <code>git commit</code> 完成只有一个提交的合并</p></li></ul><h3 id="git-update-index"><a href="#git-update-index" class="headerlink" title="git update-index"></a><a href="http://git-scm.com/docs/git-update-index" target="_blank" rel="noopener">git update-index</a></h3><ul><li><p><code>git update-index --assume-unchanged /PATH/TO/FILE</code><br>当某个文件被临时修改，但不想提交，也不适合放到 <code>.gitignore</code>，可以用此命令让 <code>git</code> 不将其识别为已修改<br>如果这个文件是新增的，就不能用这个命令了，不过可以把文件路径加到 <code>.git/info/exclude</code></p></li><li><p><code>git update-index --no-assume-unchanged /PATH/TO/FILE</code><br>恢复以上文件的修改识别</p></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文记录了一些常用 Git 命令行操作的具体使用方式&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="Git" scheme="https://chanvinxiao.com/cn/blog/tags/Git/"/>
    
      <category term="Shell" scheme="https://chanvinxiao.com/cn/blog/tags/Shell/"/>
    
  </entry>
  
  <entry>
    <title>不同环境的性能测试计划</title>
    <link href="https://chanvinxiao.com/cn/blog/performance-test-plan-for-different-environment/"/>
    <id>https://chanvinxiao.com/cn/blog/performance-test-plan-for-different-environment/</id>
    <published>2021-06-19T23:07:15.000Z</published>
    <updated>2021-06-20T11:19:22.490Z</updated>
    
    <content type="html"><![CDATA[<p>不同环境下的 JMeter 性能测试，设置基本一致，只是地址，账户等信息不同，本文利用 Shell 脚本和 JMeter 变量进行统一处理</p><a id="more"></a><h2 id="安装-JMeter"><a href="#安装-JMeter" class="headerlink" title="安装 JMeter"></a>安装 JMeter</h2><blockquote><p>wget <a href="https://mirror.cogentco.com/pub/apache//jmeter/binaries/apache-jmeter-5.4.1.tgz" target="_blank" rel="noopener">https://mirror.cogentco.com/pub/apache//jmeter/binaries/apache-jmeter-5.4.1.tgz</a><br>tar -zxvf apache-jmeter-5.4.1.tgz -C /opt</p></blockquote><p>把执行目录加入 PATH</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">PATH</span>=<span class="string">"<span class="variable">$PATH</span>:/opt/apache-jmeter-5.4.1/bin"</span></span><br></pre></td></tr></table></figure><ul><li>这样就可以从任何地方执行 jmeter 命令了</li></ul><h2 id="设置测试计划"><a href="#设置测试计划" class="headerlink" title="设置测试计划"></a>设置测试计划</h2><p>运行 JMeter GUI</p><blockquote><p>jmeter</p></blockquote><h3 id="常规设置"><a href="#常规设置" class="headerlink" title="常规设置"></a>常规设置</h3><ol><li>右键单击 Test Plan，把鼠标移至 Add，再移至 Threads(User)，点击 Thread Group，设置合适的 Number of Threads (users) 值<br><img src="threads.png?40" alt=""></li><li>右键单击 Test Plan 下新增的 Thread Group，把鼠标移至 Add，再移至 Config Element，点击 HTTP Request Default，设置 Server Name or IP 为 <code>${__P(HOST)}</code>，Port Number 为 <code>${__P(PORT)}</code><br><img src="defaults.png" alt=""></li><li>右键单击 Thread Group，把鼠标移至 Add，再移至 Sample，点击 HTTP Request，设置 Name 为 Frontend，Path 为 /<br><img src="frontend.png?40" alt=""></li><li>右键单击 Thread Group，把鼠标移至 Add，再移至 Sample，点击 HTTP Request，设置 Name 为 Backend，Path 为 /api/currentUser<br><img src="backend.png?40" alt=""></li><li>右键单击 Thread Group，把鼠标移至 Add，再移至 Assertion，点击 Response Assertion，设置 Field to Test 为 Response Code，Patten Matching Rules 为 Equals，点击下方的 Add，输入 200<br><img src="assertion.png?40" alt=""></li></ol><ul><li><a href="https://jmeter.apache.org/usermanual/functions.html#__P" target="_blank" rel="noopener"><code>__P</code></a> 为 JMeter 获取命令行参数的函数</li></ul><h3 id="权限处理"><a href="#权限处理" class="headerlink" title="权限处理"></a>权限处理</h3><p>以上的后端接口是需要先登录才能有权限访问的，权限认证方式为 cookies 或 Authorization Header 中的 token，所以需要先获取这个 token， 并在每次访问时带上</p><ol><li>右键单击 Test Plan，把鼠标移至 Add，再移至 Threads(User)，点击 setUp Thread Group<br><img src="setup.png?40" alt=""></li><li>右键单击 Test Plan 下新增的 setUp Thread Group，把鼠标移至 Add，再移至 Sample，点击 HTTP Request，设置 Name 为 Login，Server Name or IP 为 <code>${__P(HOST)}</code>，Port Number 为 <code>${__P(PORT)}</code>，HTTP Request 的方法为 POST，Path 为 /api/login/account，点击 下方的 Add，分别增加 Name 为 username 和 password，Value 为 <code>${__P(USERNAME)}</code> 和 <code>${__P(PASSWORD)}</code><br><img src="login.png?40" alt=""></li><li>右键单击 setUp Thread Group，把鼠标移至 Add，再移至 Post Processors，点击 Regular Expression Extractor，设置 Name of created variable 为 token，Regular Expression 为 <code>&quot;token&quot;:&quot;(.+?)&quot;</code>，Template 为 <code>$1$</code><br><img src="regular.png?40" alt=""></li><li>右键单击 setUp Thread Group，把鼠标移至 Add，再移至 Sample，点击 BeanShellSampler，设置 Name 为 Token，Script 为 <code>${__setProperty(token, ${token})}</code><br><img src="token.png?40" alt=""></li><li>右键单击 Thread Group，把鼠标移至 Add，再移至 Config Element，点击 HTTP Cookie Manager，点击下方的 Add，设置 Name 为 token，Value 为 <code>${__property(token)}</code>，Domain 为 <code>${__P(HOST)}:${__P(PORT)}</code>，Path 为 /<br><img src="cookies.png" alt=""></li></ol><ul><li><code>&quot;token&quot;:&quot;(.+?)&quot;</code> 将返回结果中的 token 的值捕捉为 <code>$1$</code> </li><li><a href="https://jmeter.apache.org/usermanual/functions.html#__setProperty" target="_blank" rel="noopener"><code>__setProperty</code></a> 为 JMeter 设置全局变量的函数</li><li><a href="https://jmeter.apache.org/usermanual/functions.html#__property" target="_blank" rel="noopener"><code>__property</code></a> 为 JMeter 获取置全局变量的函数</li></ul><h2 id="运行测试计划"><a href="#运行测试计划" class="headerlink" title="运行测试计划"></a>运行测试计划</h2><h3 id="直接运行"><a href="#直接运行" class="headerlink" title="直接运行"></a>直接运行</h3><p>以上的计划设置，把地址和账户信息都作为变量，可以在命令行传递，形如：</p><blockquote><p>mkdir report<br>HEAP=”-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m”<br>jmeter -n -t test.jmx \<br>  -J HOST=127.0.0.1 \<br>  -J PORT=80 \<br>  -J USERNAME=USERNAME \<br>  -J PASSWORD=PASSWORD \<br>  -l log/local-20210620095700.txt \<br>  -e -o report/local-20210620095700  </p></blockquote><ul><li><code>report</code> 文件夹必须存在，才能写入报告目录，所以首次需用 <code>mkdir</code> 新建</li><li>以上命令使用了 <a href="https://jmeter.apache.org/usermanual/get-started.html#options" target="_blank" rel="noopener">jmeter 的命令选项</a> <code>-n</code>，<code>-t</code>，<code>-J</code>，<code>-l</code>，<code>-e</code> 和 <code>-o</code></li><li>local-20210620095700 里面的 html 格式的测试报告，包括表格数据和可视化图表，如下图所示<br><img src="dash.png" alt=""><br><img src="time.png" alt=""></li></ul><h3 id="脚本处理"><a href="#脚本处理" class="headerlink" title="脚本处理"></a>脚本处理</h3><p>以上命令相对繁琐，而且不同环境需要重新设置每个参数，所以通过 Shell 脚本简化</p><h4 id="设置变量"><a href="#设置变量" class="headerlink" title="设置变量"></a>设置变量</h4><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">LOCAL_HOST</span>=127.0.0.1</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">LOCAL_PORT</span>=80</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">LOCAL_USERNAME</span>=USERNAME</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">LOCAL_PASSWORD</span>=PASSWORD</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">TESTING_HOST</span>=192.168.1.10</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">TESTING_PORT</span>=80</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">TESTING_USERNAME</span>=USERNAME</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">TESTING_PASSWORD</span>=PASSWORD</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">PRODUCT_HOST</span>=192.168.1.100</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">PRODUCT_PORT</span>=80</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">PRODUCT_USERNAME</span>=USERNAME</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">PRODUCT_PASSWORD</span>=PASSWORD</span><br></pre></td></tr></table></figure><h4 id="调用变量"><a href="#调用变量" class="headerlink" title="调用变量"></a>调用变量</h4><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/bin/sh</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">time</span>=$(date <span class="string">"+%Y%m%d%H%M%S"</span>)</span><br><span class="line"><span class="attribute">server</span>=<span class="variable">$&#123;1:-"local"&#125;</span></span><br><span class="line"><span class="attribute">prefix</span>=$(echo <span class="variable">$server</span> | tr <span class="string">'a-z'</span> <span class="string">'A-Z'</span>)_</span><br><span class="line"><span class="attribute">HOST</span>=<span class="variable">$&#123;prefix&#125;</span>HOST</span><br><span class="line"><span class="attribute">PORT</span>=<span class="variable">$&#123;prefix&#125;</span>PORT</span><br><span class="line"><span class="attribute">USERNAME</span>=<span class="variable">$&#123;prefix&#125;</span>USERNAME</span><br><span class="line"><span class="attribute">PASSWORD</span>=<span class="variable">$&#123;prefix&#125;</span>PASSWORD</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> ! [ -d <span class="string">"report"</span> ]</span><br><span class="line">then</span><br><span class="line">mkdir report</span><br><span class="line">fi</span><br><span class="line"></span><br><span class="line"><span class="attribute">HEAP</span>=<span class="string">"-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m"</span></span><br><span class="line">jmeter -n -t test.jmx \</span><br><span class="line">  -J <span class="attribute">HOST</span>=<span class="variable">$&#123;!HOST&#125;</span> \</span><br><span class="line">  -J <span class="attribute">PORT</span>=<span class="variable">$&#123;!PORT&#125;</span> \</span><br><span class="line">  -J <span class="attribute">USERNAME</span>=<span class="variable">$&#123;!USERNAME&#125;</span> \</span><br><span class="line">  -J <span class="attribute">PASSWORD</span>=<span class="variable">$&#123;!PASSWORD&#125;</span> \</span><br><span class="line">  -l log/<span class="variable">$server</span>-<span class="variable">$time</span>.txt \</span><br><span class="line">  -e -o report/<span class="variable">$server</span>-<span class="variable">$time</span></span><br></pre></td></tr></table></figure><ul><li><code>$(echo $VAR | tr &#39;a-z&#39; &#39;A-Z&#39;)</code> 表示全部大写，等效于版本 4.0 以上的 Bash 的 <code>${VAR^^}</code></li><li>如果 <code>report</code> 文件夹存在就不用再重新创建，所以先通过 <code>-d</code> 判断</li><li><code>${!HOST}</code> 表示 <code>$HOST</code> 的值为另一变量的名称，最后解析出来的是这另一个变量的值</li></ul><h3 id="运行脚本"><a href="#运行脚本" class="headerlink" title="运行脚本"></a>运行脚本</h3><h4 id="本地环境"><a href="#本地环境" class="headerlink" title="本地环境"></a>本地环境</h4><blockquote><p>./test.sh  </p></blockquote><h4 id="测试环境"><a href="#测试环境" class="headerlink" title="测试环境"></a>测试环境</h4><blockquote><p>./test.sh testing  </p></blockquote><h4 id="正式环境"><a href="#正式环境" class="headerlink" title="正式环境"></a>正式环境</h4><blockquote><p>./test.sh product  </p></blockquote><ul><li>在不设环境的时候，<code>${1:-&quot;local&quot;}</code> 把默认环境设置为 local</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>以上计划和脚本可以在此 <a href="https://github.com/vinzid/perf-test-plan" target="_blank" rel="noopener">GitHub 仓库</a> 下载，主要实现了：</p><ul><li>JMeter 通过接收命令行输入的参数进行动态设置测试计划</li><li>通过启动线程组进行登录处理，并将 token 设置为全局变量</li><li>通过 Shell 脚本简化命令并进行不同环境的统一处理</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;不同环境下的 JMeter 性能测试，设置基本一致，只是地址，账户等信息不同，本文利用 Shell 脚本和 JMeter 变量进行统一处理&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="Shell" scheme="https://chanvinxiao.com/cn/blog/tags/Shell/"/>
    
      <category term="Performance" scheme="https://chanvinxiao.com/cn/blog/tags/Performance/"/>
    
      <category term="JMeter" scheme="https://chanvinxiao.com/cn/blog/tags/JMeter/"/>
    
  </entry>
  
  <entry>
    <title>React 应用的 Nginx 缓存控制</title>
    <link href="https://chanvinxiao.com/cn/blog/cache-control-for-react-app-with-nginx/"/>
    <id>https://chanvinxiao.com/cn/blog/cache-control-for-react-app-with-nginx/</id>
    <published>2021-05-23T01:54:46.000Z</published>
    <updated>2021-05-29T14:43:45.980Z</updated>
    
    <content type="html"><![CDATA[<p>典型 React 应用面临的缓存问题，可通过 Nginx 配置进行解决</p><a id="more"></a><h2 id="通用部署"><a href="#通用部署" class="headerlink" title="通用部署"></a>通用部署</h2><p>构建应用后，只需使用 Nginx 指向静态文件即可</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">  <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">  <span class="attribute">root</span> /PATH/TO/APP/build;</span><br><span class="line">  <span class="attribute">try_files</span> <span class="variable">$uri</span> <span class="variable">$uri</span>/ /index.html;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="缓存问题"><a href="#缓存问题" class="headerlink" title="缓存问题"></a>缓存问题</h2><p>第一次请求页面的时候，所有页面和资源都是从服务器返回的，如下图所示：</p><p><img src="fresh.png" alt=""></p><p>关闭浏览器，重新打开，输入网址，按下Enter，浏览器会从本地缓存获取文件，如下图所示：</p><p><img src="cache.png" alt=""></p><p>即使在以上两次请求之间，页面已更新，浏览器也不会从服务器重新获取更新，因为 <code>disk cache</code> 并没有和服务器进行任何通信</p><h2 id="解决思路"><a href="#解决思路" class="headerlink" title="解决思路"></a>解决思路</h2><p>如果资源文件有更新，其文件名就会更新，所以资源的缓存不是问题，只需禁用页面的缓存即可</p><p>即把以上的</p><figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">try_files <span class="variable">$uri</span> <span class="variable">$uri</span><span class="regexp">/ /i</span>ndex.html;</span><br></pre></td></tr></table></figure><p>替换为</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">location</span> / &#123;</span><br><span class="line">  <span class="attribute">if</span> ( <span class="variable">$uri</span> = <span class="string">'/index.html'</span> ) &#123;</span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="literal">no</span>-store always;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="attribute">try_files</span> <span class="variable">$uri</span> <span class="variable">$uri</span>/ /index.html;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>因为所有的页面最终都指向入口文件，所以其实际的 <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#var_uri" target="_blank" rel="noopener"><code>$uri</code></a> 都是 <code>/index.html</code></li><li><code>no-store</code> 是最严格的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control" target="_blank" rel="noopener"><code>Cache-Control</code></a> 禁用缓存的值，确保浏览器不使用任何缓存</li><li>因为 <a href="http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header" target="_blank" rel="noopener"><code>add_header</code></a> 连带 <code>if</code> 不能直接在 <code>server</code> 下，所以加了一层 location</li></ul><h2 id="解决效果"><a href="#解决效果" class="headerlink" title="解决效果"></a>解决效果</h2><p>这样第二次请求页面时，页面本身不会缓存，但是资源如果没有改变就会缓存，如下图所示：</p><p><img src="index.png" alt=""></p><p>可以访问以下地址，尝试操作，并查看对应的网络请求：</p><blockquote><p><a href="http://react.chanvinxiao.com" target="_blank" rel="noopener">http://react.chanvinxiao.com</a></p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li>浏览器输入地址按 Enter 会出现奇怪的缓存问题</li><li>通过 Nginx 的 <code>$uri</code> 可以判断请求是否为页面</li><li>通过 Nginx 的 <code>add_header</code> 可以设置缓存控制头</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;典型 React 应用面临的缓存问题，可通过 Nginx 配置进行解决&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="Nginx" scheme="https://chanvinxiao.com/cn/blog/tags/Nginx/"/>
    
      <category term="React" scheme="https://chanvinxiao.com/cn/blog/tags/React/"/>
    
      <category term="Cache" scheme="https://chanvinxiao.com/cn/blog/tags/Cache/"/>
    
  </entry>
  
  <entry>
    <title>Hexo 博客利用 Nginx 实现中英文切换</title>
    <link href="https://chanvinxiao.com/cn/blog/hexo-blog-bilingualism-with-nginx/"/>
    <id>https://chanvinxiao.com/cn/blog/hexo-blog-bilingualism-with-nginx/</id>
    <published>2020-05-10T10:36:32.000Z</published>
    <updated>2020-06-21T12:17:11.113Z</updated>
    
    <content type="html"><![CDATA[<p>本文记录了对 <a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a> 博客进行中英文切换的配置过程，实现同一应用共用模版，任何页面可以切换到另一语言的对应页面，并对未明确语言的访问地址，根据浏览器语言进行自动跳转</p><a id="more"></a><h2 id="实现细则"><a href="#实现细则" class="headerlink" title="实现细则"></a>实现细则</h2><h3 id="中英文地址区分"><a href="#中英文地址区分" class="headerlink" title="中英文地址区分"></a>中英文地址区分</h3><p>博客中文首页：</p><blockquote><p><a href="https://chanvinxiao.com/cn/blog/">https://chanvinxiao.com/cn/blog/</a></p></blockquote><p>博客英文首页：</p><blockquote><p><a href="https://chanvinxiao.com/en/blog/">https://chanvinxiao.com/en/blog/</a></p></blockquote><h3 id="中英文切换"><a href="#中英文切换" class="headerlink" title="中英文切换"></a>中英文切换</h3><p>例如以下博客中文页面</p><blockquote><p><a href="https://chanvinxiao.com/cn/blog/archives/2020/04/">https://chanvinxiao.com/cn/blog/archives/2020/04/</a></p></blockquote><p>点击右上角的 English，则切换到以下地址</p><blockquote><p><a href="https://chanvinxiao.com/en/blog/archives/2020/04/">https://chanvinxiao.com/en/blog/archives/2020/04/</a></p></blockquote><p>在这个页面点击右上角的中文，则会切换回来</p><h3 id="自动跳转"><a href="#自动跳转" class="headerlink" title="自动跳转"></a>自动跳转</h3><p>例如以下博客地址</p><blockquote><p><a href="https://chanvinxiao.com/blog/vuejs-tic-tac-toe/">https://chanvinxiao.com/blog/vuejs-tic-tac-toe/</a></p></blockquote><p>当浏览器语言设置的首选语言为英文时，会跳转到其对应的英文版本</p><blockquote><p><a href="https://chanvinxiao.com/en/blog/vuejs-tic-tac-toe/">https://chanvinxiao.com/en/blog/vuejs-tic-tac-toe/</a></p></blockquote><p>当浏览器语言设置的首选语言为中文时，则跳转到其对应的中文版本</p><blockquote><p><a href="https://chanvinxiao.com/cn/blog/vuejs-tic-tac-toe/">https://chanvinxiao.com/cn/blog/vuejs-tic-tac-toe/</a></p></blockquote><h2 id="Hexo-配置"><a href="#Hexo-配置" class="headerlink" title="Hexo 配置"></a>Hexo 配置</h2><h3 id="增加英文配置"><a href="#增加英文配置" class="headerlink" title="增加英文配置"></a>增加英文配置</h3><p>在项目根目录下增加 <code>_config-en.yml</code></p><figure class="highlight dts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"># Site</span></span><br><span class="line"><span class="symbol">title:</span> TITLE</span><br><span class="line"><span class="symbol">subtitle:</span> SUBTITLE</span><br><span class="line"><span class="symbol">description:</span> DESCRIPTION</span><br><span class="line"><span class="symbol">keywords:</span> KEYWORDS</span><br><span class="line"><span class="symbol">language:</span> en</span><br><span class="line"></span><br><span class="line"><span class="meta"># URL</span></span><br><span class="line"><span class="symbol">url:</span> https:<span class="comment">//chanvinxiao.com/en/blog</span></span><br><span class="line"><span class="symbol">root:</span> <span class="meta-keyword">/en/</span>blog/</span><br><span class="line"></span><br><span class="line"><span class="meta"># Directory</span></span><br><span class="line"><span class="symbol">source_dir:</span> source-en</span><br><span class="line"><span class="symbol">public_dir:</span> public-en</span><br></pre></td></tr></table></figure><ul><li><code>#Site</code> 相关的配置，主要是把中文的内容改为英文的，关键是将 <code>language</code> 设为 <code>en</code>，这样模版就会使用英文的语言项</li><li><code>URL</code> 和 <code>root</code> 要设置为独立于中文对应的地址和目录</li><li>将英文的 <code>source</code> 和 <code>public</code> 目录和中文区分开，就可以确保中、英文版分别只出现中、英文博客文章</li></ul><h3 id="增加相关脚本"><a href="#增加相关脚本" class="headerlink" title="增加相关脚本"></a>增加相关脚本</h3><p>在 <code>package.json</code> 中增加以下脚本</p><figure class="highlight jboss-cli"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">"scripts"</span>: &#123;</span><br><span class="line">  <span class="string">...</span></span><br><span class="line">  <span class="string">"build:en"</span>: <span class="string">"hexo generate --config _config.yml,_config-en.yml"</span>,</span><br><span class="line">  <span class="string">"clean:en"</span>: <span class="string">"hexo clean --config _config.yml,_config-en.yml"</span>,</span><br><span class="line">  <span class="string">"server:en"</span>: <span class="string">"hexo server --config _config.yml,_config-en.yml"</span></span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><ul><li>增加了英文对应的构建、清除和服务器的脚本，中英文相对独立，互不影响</li><li>使用<a href="https://hexo.io/docs/configuration#Using-an-Alternate-Config" target="_blank" rel="noopener">自定义配置</a>, 将相应脚本的配置设为 <code>_config.yml</code> 与 <code>_config-en.yml</code> 的叠加配置</li><li>系统会自动生成叠加配置文件 <code>_multiconfig.yml</code>，应将此文件添加至 .gitignore 中</li></ul><h2 id="Nginx-配置"><a href="#Nginx-配置" class="headerlink" title="Nginx 配置"></a>Nginx 配置</h2><p>在 Nginx 对应的 <code>server</code> 中增加以下配置</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">if</span> ( <span class="variable">$http_accept_language</span> <span class="regexp">~* ^en</span> ) &#123;</span><br><span class="line">  <span class="attribute">rewrite</span><span class="regexp"> ^(/blog.*)</span> /en<span class="variable">$1</span> <span class="literal">redirect</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="attribute">rewrite</span><span class="regexp"> ^(/blog.*)</span> /cn<span class="variable">$1</span> <span class="literal">redirect</span>;</span><br><span class="line"></span><br><span class="line"><span class="attribute">location</span> /cn/blog &#123;</span><br><span class="line">  <span class="attribute">alias</span> /PATH/TO/BLOG/public;</span><br><span class="line">  <span class="attribute">error_page</span> <span class="number">404</span> <span class="variable">$scheme</span>://<span class="variable">$host</span>/cn/blog;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="attribute">location</span> /en/blog &#123;</span><br><span class="line">  <span class="attribute">alias</span> /PATH/TO/BLOG/public-en;</span><br><span class="line">  <span class="attribute">error_page</span> <span class="number">404</span> <span class="variable">$scheme</span>://<span class="variable">$host</span>/en/blog;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><code>$http_accept_language</code> 为 Nginx 的 <code>http</code> 模块为请求首部 <code>Accept-Language</code> 设置的<a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#variables" target="_blank" rel="noopener">内嵌变量</a>，如果浏览器的默认语言为英文，其值将以 en 开头，例如 <code>en-US,en;q=0.9</code></li><li><code>rewrite ^(/blog.*) /en$1 redirect;</code> 相当于把 /blog 开头的地址前面增加 /en，<a href="http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite" target="_blank" rel="noopener"><code>rewrite</code></a> 的标记设置为 <code>redirect</code> 表示 302 跳转，下面默认的 cn 跳转也是一致</li><li>以上设置对以 <code>/blog</code> 开头的地址（即未明确语言的地址）进行了判断跳转，如果浏览器默认语言为英文，则跳转到以 <code>/en/blog</code> 开头的英文站，否则默认跳转到以 <code>/cn/blog</code> 开头的中文站</li><li>因为 /cn/blog 对应的是 public 目录下的 index.html，而不是 cn/blog/index.html，所以需要使用 <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#alias" target="_blank" rel="noopener"><code>alias</code></a>，而不是 <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#root" target="_blank" rel="noopener"><code>root</code></a></li><li><a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page" target="_blank" rel="noopener"><code>error_page</code></a> 设置了 404 处理，<code>$scheme</code> 为 <code>http</code> 或 <code>https</code>，标示为页面跳转，分别跳转到对应博客中、英文首页</li></ul><h2 id="页面对应切换"><a href="#页面对应切换" class="headerlink" title="页面对应切换"></a>页面对应切换</h2><p>以模版 landscape 为例，在 <code>themes/landscape/source/js/script.js</code> 中的 <code>})(jQuery);</code>前，增加以下内容</p><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">let <span class="keyword">language</span> = &#123;&#125;;</span><br><span class="line"><span class="keyword">language</span>.now = <span class="keyword">location</span>.pathname.match(/^\/en/) ? <span class="string">'en'</span> : <span class="string">'cn'</span>;</span><br><span class="line"><span class="keyword">if</span>(<span class="string">'en'</span> === <span class="keyword">language</span>.now)&#123;</span><br><span class="line">  <span class="keyword">language</span>.label = <span class="string">'中文'</span>;</span><br><span class="line">  <span class="keyword">language</span>.href = <span class="keyword">location</span>.pathname.replace(/^\/en/, <span class="string">'/cn'</span>);</span><br><span class="line">&#125;<span class="keyword">else</span>&#123;</span><br><span class="line">  <span class="keyword">language</span>.label = <span class="string">'English'</span>;</span><br><span class="line">  <span class="keyword">language</span>.href = <span class="keyword">location</span>.pathname.replace(/^\/cn/, <span class="string">'/en'</span>);</span><br><span class="line">&#125;</span><br><span class="line">$(<span class="string">'#sub-nav'</span>).prepend(`&lt;a <span class="keyword">class</span>="main-nav-link" href="$&#123;language.href&#125;"&gt;$&#123;<span class="keyword">language</span>.label&#125;&lt;/a&gt;`)</span><br></pre></td></tr></table></figure><ul><li>根据页面路径前面是否为 <code>/en</code>，确认是博客中文页面还是英文页面</li><li>英文页面增加到对应中文页面的链接菜单，中文则增加英文的链接</li><li>直接将地址中的 <code>cn</code> 改为 <code>en</code> 或 <code>en</code> 改为 <code>cn</code> 则为对应页面，如果没有对应页面，根据以上的 Nginx 配置，将跳转到对应首页</li><li>利用 <code>jQuery</code> 的 <code>prepend</code> 将链接增加到子菜单中，共用类 <code>main-nav-link</code> 的样式</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在实现博客中英文过程中，主要使用了以下技术：</p><ul><li>Hexo 的 自定义配置和 package.json 的 <code>scripts</code></li><li>Nginx 的 <code>http</code> 模块的请求首部内嵌变量</li><li>Nginx 的指令 <code>rewrite</code>, <code>alias</code> 和 <code>error_page</code></li><li>location 的 <code>pathname</code> 和 jQuery 的 <code>prepend</code></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本文记录了对 &lt;a href=&quot;https://hexo.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hexo&lt;/a&gt; 博客进行中英文切换的配置过程，实现同一应用共用模版，任何页面可以切换到另一语言的对应页面，并对未明确语言的访问地址，根据浏览器语言进行自动跳转&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="Nginx" scheme="https://chanvinxiao.com/cn/blog/tags/Nginx/"/>
    
      <category term="Hexo" scheme="https://chanvinxiao.com/cn/blog/tags/Hexo/"/>
    
      <category term="jQuery" scheme="https://chanvinxiao.com/cn/blog/tags/jQuery/"/>
    
  </entry>
  
  <entry>
    <title>prefetch 和 preload 及 webpack 的相关处理</title>
    <link href="https://chanvinxiao.com/cn/blog/prefetch-and-preload-with-webpack/"/>
    <id>https://chanvinxiao.com/cn/blog/prefetch-and-preload-with-webpack/</id>
    <published>2020-04-26T03:43:29.000Z</published>
    <updated>2020-06-07T11:32:07.500Z</updated>
    
    <content type="html"><![CDATA[<p>使用预取和预加载是网站性能和用户体验提升的一个很好的途径，本文介绍了使用 prefetch 和 prefetch 进行预取和预加载的方法，并使用 webpack 进行实现</p><a id="more"></a><h2 id="Link-的链接类型"><a href="#Link-的链接类型" class="headerlink" title="Link 的链接类型"></a>Link 的链接类型</h2><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/link" target="_blank" rel="noopener"><code>&lt;link&gt;</code></a> 标签的 <code>rel</code> 属性可以定义链接类型，与 <code>href</code> 配合使用可以预取或预加载对应资源，<code>prefetch</code> 是其中的一种链接类型</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;link <span class="attribute">rel</span>=<span class="string">"prefetch"</span> <span class="attribute">herf</span>=<span class="string">"URL"</span>&gt;</span><br></pre></td></tr></table></figure><p><code>preload</code> 是另外一种类型，同样用 href 定义资源地址，但其处理预取外，还会对资源进行解析，所以还要增加属性 as，说明资源的类型</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;link <span class="attribute">rel</span>=<span class="string">"preload"</span> <span class="attribute">href</span>=<span class="string">"URL"</span> <span class="attribute">as</span>=<span class="string">"MIME_TYPE"</span>&gt;</span><br></pre></td></tr></table></figure><h2 id="预取资源"><a href="#预取资源" class="headerlink" title="预取资源"></a>预取资源</h2><p><code>prefetch</code> 表示用户在接下来的浏览中（例如在下一个页面），有可能用到对应资源，提示浏览器要在闲时获取对应资源</p><p>先新建文件夹 prefetch-preload-demo（本文所有代码将在此创建），安装相关依赖，并新建文件夹 static</p><blockquote><p>mkdir prefetch-preload-demo<br>cd prefetch-preload-demo<br>npm init -y<br>npm i -D http-server<br>mkdir static</p></blockquote><p>在 static 中创建 <code>prefetch.html</code>， <code>main.js</code> 和 <code>script.js</code></p><p><code>prefetch.html</code> 定义了一个 <code>rel</code> 为 <code>prefetch</code> 的链接</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">title</span>&gt;</span>Prefetch<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">"utf-8"</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">"viewport"</span> <span class="attr">content</span>=<span class="string">"width=device-width, initial-scale=1, maximum-scale=1"</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">"prefetch"</span> <span class="attr">href</span>=<span class="string">"script.js"</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">"main.js"</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><p><code>main.js</code> 创建了一个按钮，并绑定了点击事件</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">let button = document.createElement(<span class="string">'button'</span>);</span><br><span class="line">button.innerHTML = <span class="string">'Add Script'</span>;</span><br><span class="line">button.addEventListener(<span class="string">'click'</span>, e =&gt; &#123;</span><br><span class="line">  let<span class="built_in"> script </span>= document.createElement(<span class="string">"script"</span>);</span><br><span class="line">  script.src = <span class="string">"script.js"</span>;</span><br><span class="line">  document.head.appendChild(script);</span><br><span class="line">&#125;);</span><br><span class="line">document.body.appendChild(button);</span><br></pre></td></tr></table></figure><p><code>script.js</code> 只是简单的打印了一下</p><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">console.<span class="built_in">log</span>('<span class="keyword">script</span> <span class="built_in">run</span>');</span><br></pre></td></tr></table></figure><p>运行服务器（也可在 <code>package.json</code> 中增加 <code>server</code> 脚本）</p><blockquote><p>npx http-server</p></blockquote><p>访问 http://localhost:8080 并导航至 <code>static</code> 中，点击 prefetch.html，或者直接访问线上<a href="https://chanvinxiao.com/demo/html/prefetch.html">页面</a>，初始状态下，查看控制台的网络选项卡下的内容如下（不要勾选 <code>Disable Cache</code>，点击右侧齿轮，勾选 <code>Use large request rows</code>）</p><p><img src="prefetch-init.png" alt=""></p><ul><li><code>script.js</code> 被 fetch 下来，size 列的两个数字，275 B 表示下载的字节大小，0 B 表示解析的字节大小（即目前并没有解析）</li><li>控制台是空的，即脚本没有运行</li></ul><p>点击页面上的 <code>Add Script</code>，会在页面增加地址为 <code>script.js</code> 的 <code>&lt;script&gt;</code> 标签，此时网络选项卡会增加以下内容</p><p><img src="prefetch-add.png" alt=""></p><ul><li>下载字节量为 <code>(prefetch cache)</code> ，即直接从预取缓存获取资源，下面的解析后的字节不再为 0</li><li>控制台打印出脚本中的调试内容，即这时脚本才被解析并运行</li></ul><h2 id="预加载资源"><a href="#预加载资源" class="headerlink" title="预加载资源"></a>预加载资源</h2><p><code>preload</code> 表示用户在当前的浏览中（往往是在当前页面），极有可以可能用到对应资源，提示浏览器要优先获取对应资源</p><p>将 prefetch.html 的 link 标签的 prefetch 改为 <code>preload</code>，并增加资源类型 <code>as</code> 为 <code>script</code>，即得 preload.html</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;link <span class="attribute">rel</span>=<span class="string">"preload"</span> <span class="attribute">href</span>=<span class="string">"script.js"</span> <span class="attribute">as</span>=<span class="string">"script"</span>&gt;</span><br></pre></td></tr></table></figure><p>访问本地服务器对应的 <code>prefetch.html</code>，或者直接访问线上<a href="https://chanvinxiao.com/demo/html/preload.html">页面</a>，初始状态下，查看控制台的网络选项卡下的内容如下</p><p><img src="preload.png" alt=""></p><ul><li><code>script.js</code> 被优先下载， size 列的解压字节不再为 0，即 <code>preload</code> 除了把脚本下载了下来，还进行了解析</li><li>控制台目前仍为空，即脚本虽然被解析，但并没有运行</li></ul><p>点击 <code>Add Script</code>，网络选项卡并没有增加任何记录，但是控制台输出了脚本的打印内容</p><ul><li>因为脚本已经解析完成，所以连从缓存获取都不需要了，直接运行即可</li><li>如果没有在 3 秒内点击 <code>Add Script</code>，控制台会进行警告，因为没有及时使用应该优先加载的资源<blockquote><p>The resource <a href="https://chanvinxiao.com/demo/html/script.js">https://chanvinxiao.com/demo/html/script.js</a> was preloaded using link preload but not used within a few seconds from the window’s load event. Please make sure it has an appropriate <code>as</code> value and it is preloaded intentionally.</p></blockquote></li></ul><h2 id="webpack-的相关处理"><a href="#webpack-的相关处理" class="headerlink" title="webpack 的相关处理"></a>webpack 的相关处理</h2><p>运行以下命令安装相关依赖，并新建文件夹 src</p><blockquote><p>npm i -D webpack webpack-cli html-webpack-plugin preload-webpack-plugin@3.0.0-beta.4<br>mkdir src</p></blockquote><ul><li>PreloadWebpackPlugin 的当前版本 2.x 与 webpack 当前版本 4.x 不兼容，所以需要指定版本号为最新的 3.x beta</li></ul><p>将 <code>main.js</code> 与 <code>script.js</code> 复制到 src 中，并将 <code>main.js</code> 的点击事件处理更新为</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">button.addEventListener(<span class="string">'click'</span>, <span class="function"><span class="params">e</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">import</span>(<span class="comment">/* webpackChunkName: "script" */</span> <span class="string">'./script.js'</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ul><li><a href="https://webpack.js.org/api/module-methods/#import-1" target="_blank" rel="noopener">import()</a> 为动态加载脚本，webpack 会生成类似以上动态创建 <code>script</code> 标签的代码</li><li>import 里的注释为特殊含义的魔法注释，如果不设置 webpackChunkName，加载的脚本将被按数字次序命名</li></ul><p>增加 <code>webpack.config.js</code> 如下</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> HtmlWebpackPlugin = <span class="built_in">require</span>(<span class="string">'html-webpack-plugin'</span>);</span><br><span class="line"><span class="keyword">const</span> PreloadWebpackPlugin = <span class="built_in">require</span>(<span class="string">'preload-webpack-plugin'</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  entry: <span class="string">'./src/main.js'</span>,</span><br><span class="line">  plugins: [</span><br><span class="line">    <span class="keyword">new</span> HtmlWebpackPlugin(&#123;</span><br><span class="line">      filename: <span class="string">'preload.html'</span></span><br><span class="line">    &#125;),</span><br><span class="line">    <span class="keyword">new</span> PreloadWebpackPlugin()</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><a href="https://webpack.js.org/plugins/html-webpack-plugin/" target="_blank" rel="noopener">HtmlWebpackPlugin</a> 将自动生成相应的 html 文件，默认为 index.html，这里通过设置 <code>filename</code> 选项更改</li><li><a href="https://github.com/GoogleChrome/preload-webpack-plugin" target="_blank" rel="noopener">PreloadWebpackPlugin</a> 为 HtmlWebpackPlugin 的插件，默认为其动态加载资源增加链接类型为 <code>preload</code> 的 <code>link</code> 标签，其 <code>as</code> 的值可根据后缀自动判断</li></ul><p>PreloadWebpackPlugin 也支持 prefetch，需要增加 <code>rel</code> 选项为 <code>prefetch</code></p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">new</span> <span class="selector-tag">HtmlWebpackPlugin</span>(&#123;</span><br><span class="line">  <span class="attribute">filename</span>: <span class="string">'prefetch.html'</span></span><br><span class="line">&#125;),</span><br><span class="line"><span class="selector-tag">new</span> <span class="selector-tag">PreloadWebpackPlugin</span>(&#123;</span><br><span class="line">  <span class="attribute">rel</span>: <span class="string">'prefetch'</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>不过要同时生成 preload.html 和 prefetch.html，需要在对应的 PreloadWebpackPlugin 中设置 <code>excludeHtmlNames</code> 排除对方，否则会同时产生 preload 和 prefetch 的 link 标签</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">new</span> <span class="selector-tag">HtmlWebpackPlugin</span>(&#123;</span><br><span class="line">  <span class="attribute">filename</span>: <span class="string">'preload.html'</span></span><br><span class="line">&#125;),</span><br><span class="line"><span class="selector-tag">new</span> <span class="selector-tag">HtmlWebpackPlugin</span>(&#123;</span><br><span class="line">  <span class="attribute">filename</span>: <span class="string">'prefetch.html'</span></span><br><span class="line">&#125;),</span><br><span class="line"><span class="selector-tag">new</span> <span class="selector-tag">PreloadWebpackPlugin</span>(&#123;</span><br><span class="line">  <span class="attribute">excludeHtmlNames</span>: [<span class="string">'prefetch.html'</span>]</span><br><span class="line">&#125;),</span><br><span class="line"><span class="selector-tag">new</span> <span class="selector-tag">PreloadWebpackPlugin</span>(&#123;</span><br><span class="line">  <span class="attribute">rel</span>: <span class="string">'prefetch'</span>,</span><br><span class="line">  excludeHtmlNames: [<span class="string">'preload.html'</span>]</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>构建文件（也可在 <code>package.json</code> 中增加 <code>build</code> 脚本）</p><blockquote><p>npx webpack</p></blockquote><p><code>dist</code> 文件夹中将生成 prefetch.html 和 preload.html，访问本地服务器对应地址，即可得到与以上静态页面同样的效果</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>此文使用静态页面和 webpack 打包两种方式演示了预取和预加载的实现，完整代码见 <a href="https://github.com/vinzid/prefetch-preload-demo" target="_blank" rel="noopener">GitHub</a>，主要技术点如下：</p><ul><li>ELEMENT.appendChild 动态创建脚本</li><li>import() 动态加载脚本并设置魔法注释</li><li>html-webpack-plugin 及其插件的配置</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;使用预取和预加载是网站性能和用户体验提升的一个很好的途径，本文介绍了使用 prefetch 和 prefetch 进行预取和预加载的方法，并使用 webpack 进行实现&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="Performance" scheme="https://chanvinxiao.com/cn/blog/tags/Performance/"/>
    
      <category term="HTML" scheme="https://chanvinxiao.com/cn/blog/tags/HTML/"/>
    
      <category term="webpack" scheme="https://chanvinxiao.com/cn/blog/tags/webpack/"/>
    
  </entry>
  
  <entry>
    <title>利用 Github 网络钩子实现自动化部署</title>
    <link href="https://chanvinxiao.com/cn/blog/automatically-deploy-by-github-webhook/"/>
    <id>https://chanvinxiao.com/cn/blog/automatically-deploy-by-github-webhook/</id>
    <published>2020-04-18T14:05:52.000Z</published>
    <updated>2020-05-06T13:34:00.932Z</updated>
    
    <content type="html"><![CDATA[<p>GitHub 的网络钩子（<a href="https://developer.github.com/webhooks/" target="_blank" rel="noopener">webhook</a>）功能，可以很方便的实现自动化部署。本文记录了使用 Node.js 的开发部署过程，当项目的 master 分支被推时，将在服务器进行自动部署，完整代码见 <a href="https://github.com/vinzid/github-webhook/blob/master/app.js" target="_blank" rel="noopener">GitHub</a></p><a id="more"></a><h2 id="添加网络钩子"><a href="#添加网络钩子" class="headerlink" title="添加网络钩子"></a>添加网络钩子</h2><ol><li><p>在 GitHub 的相应项目首页，点击右上角菜单 <code>Setting</code>， 点击左侧菜单 <code>Webhooks</code>，点击右上角按钮 <code>Add webhook</code></p></li><li><p>设置 <code>Payload URL</code> 为接收事件的地址，<code>Content type</code> 建议选择 <code>applicaiton/json</code>，<code>Secret</code> 可选填任意字符串，<code>Which events would you like to trigger this webhook?</code> 设为 <code>Just the push event.</code>，勾选 <code>Active</code>，点击下方的 <code>Add webhook</code> 按钮</p></li></ol><h2 id="开发处理请求"><a href="#开发处理请求" class="headerlink" title="开发处理请求"></a>开发处理请求</h2><h3 id="接收请求"><a href="#接收请求" class="headerlink" title="接收请求"></a>接收请求</h3><p>使用 Node.js 建立一个 http 服务器，接收 POST 请求并处理其提交数据</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">const &#123; createServer &#125; = require(<span class="string">'http'</span>);</span><br><span class="line">const<span class="built_in"> port </span>= process.env.GITHUB_WEBHOOK_PORT || <span class="string">'3000'</span>;</span><br><span class="line"></span><br><span class="line">const<span class="built_in"> server </span>= createServer((req, res) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span>(<span class="string">'POST'</span> === req.method)&#123;</span><br><span class="line">    let body = <span class="string">''</span>;</span><br><span class="line">    req.on(<span class="string">'data'</span>, chunk =&gt; &#123;</span><br><span class="line">      body += chunk.toString();</span><br><span class="line">    &#125;);</span><br><span class="line">    req.on(<span class="string">'end'</span>, () =&gt; &#123;</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">server.listen(port, () =&gt; &#123;</span><br><span class="line">  console.log(`Listening on <span class="variable">$&#123;port&#125;</span>`);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>如果需要更改默认端口 3000，可以先运行以下命令添加环境变量（NUMBER 为任意端口）</p><blockquote><p>export GITHUB_WEBHOOK_PORT=NUMBER</p></blockquote><h3 id="解析-Body"><a href="#解析-Body" class="headerlink" title="解析 Body"></a>解析 Body</h3><p>在 req 的 end 事件处理器中，把字符串 body 解析成对象</p><figure class="highlight coffeescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">req.<span class="literal">on</span>(<span class="string">'end'</span>, <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">try</span>&#123;</span><br><span class="line">    body = JSON.parse(decodeURIComponent(body).replace(<span class="regexp">/^payload=/</span>, <span class="string">''</span>));</span><br><span class="line">  &#125;<span class="keyword">catch</span>(e)&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(e)</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>如果 <code>Content type</code> 设置为 <code>applicaiton/json</code>，只需要 <code>body = JSON.parse(body)</code> 即可，以上代码兼容了 <code>Content type</code> 设置为 <code>application/x-www-form-urlencoded</code> 的情况</p><h3 id="拉取更新"><a href="#拉取更新" class="headerlink" title="拉取更新"></a>拉取更新</h3><p>根据 body 的 push <a href="https://developer.github.com/v3/activity/events/types/#pushevent" target="_blank" rel="noopener">负载</a>，提取项目和分支信息，如果是 master 分支，则执行进入对应项目，拉取分支的命令</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(<span class="string">'object'</span> === <span class="keyword">typeof</span> body)&#123;</span><br><span class="line">  <span class="keyword">if</span>(<span class="string">'refs/heads/master'</span> === body.ref)&#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; exec &#125; = <span class="built_in">require</span>(<span class="string">'child_process'</span>);</span><br><span class="line">    <span class="keyword">const</span> command = <span class="string">`cd ../<span class="subst">$&#123;body.repository.name&#125;</span> &amp;&amp; git pull origin master`</span>;</span><br><span class="line">    exec(command, <span class="function">(<span class="params">error, stdout, stderr</span>) =&gt;</span> &#123;</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure><p>注意这里的项目所在的目录，与此应用所在的目录，是在同一个父目录下的，如果不是可以相应调整命令的进入路径</p><h3 id="验证密钥"><a href="#验证密钥" class="headerlink" title="验证密钥"></a>验证密钥</h3><p>以上步骤已经实现了自动拉取更新，不过存在安全性的问题，因为不仅仅 GitHub 可以发送这样的请求，所以最好设置 Secret 以进行安全验证</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">const<span class="built_in"> secret </span>= process.env.GITHUB_WEBHOOK_SECRET || <span class="string">''</span>;</span><br><span class="line"><span class="built_in">..</span>.</span><br><span class="line">    req.on(<span class="string">'end'</span>, () =&gt; &#123;</span><br><span class="line">      <span class="keyword">if</span>(<span class="string">''</span> !== secret)&#123;</span><br><span class="line">        const &#123; createHmac &#125; = require(<span class="string">'crypto'</span>);</span><br><span class="line">        let signature = createHmac(<span class="string">'sha1'</span>, secret).update(body).digest(<span class="string">'hex'</span>);</span><br><span class="line">        <span class="keyword">if</span>(req.headers[<span class="string">'x-hub-signature'</span>] !== `<span class="attribute">sha1</span>=<span class="variable">$&#123;signature&#125;</span>`)&#123;</span><br><span class="line">          console.log(<span class="string">'Signature Error'</span>);</span><br><span class="line">          res.statusCode = 403;</span><br><span class="line">          res.end();</span><br><span class="line">          return;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br></pre></td></tr></table></figure><p>运行应用前，先运行以下命令增加密钥变量（STRING 为任意字符串）</p><blockquote><p>export GITHUB_WEBHOOK_SECRET=STRING</p></blockquote><ul><li>设置了 Secret 后，GitHub 在发送请求时，会在<a href="https://developer.github.com/webhooks/#delivery-headers" target="_blank" rel="noopener">请求头</a>增加 x-hub-signature 为 sha1=SIGNATURE， 其中 SIGNATURE 为 body 的 密钥为 Secret，算法为 sha1 的 HMAC 16 进制值</li><li>通过对 Secret 的检验，可以确保只有知道了 Secret，才能发送正确的带 x-hub-signature 头的请求，否则将拒绝请求</li><li>以上代码兼容了不设置 Secret 的情况，即如果没有增加变量 GITHUB_WEBHOOK_SECRET，则按原有逻辑处理，不会进行检验</li></ul><h2 id="本地钩子构建"><a href="#本地钩子构建" class="headerlink" title="本地钩子构建"></a>本地钩子构建</h2><p>如果项目在拉取更新后需要构建，那么可以 command 变量后面加上构建命令，例如 <code>&amp;&amp; npm run build</code>，但是不同项目的构建命令有可能是不一样的，而且有的项目的构建命令可能还比较复杂，这些情况下可以通过设置 git 的本地<a href="https://git-scm.com/docs/githooks" target="_blank" rel="noopener">钩子</a>进行处理</p><blockquote><p>cd /PATH/TO/PROJECT/.git/hooks<br>nano post-merge</p></blockquote><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line">SHELL_SCRIPT</span><br></pre></td></tr></table></figure><blockquote><p>chmod +x post-merge</p></blockquote><ul><li>其中 /PATH/TO/PROJECT/ 为项目的目录位置，SHELL_SCRIPT 可以为任意 Shell 脚本</li><li>因为 <a href="https://git-scm.com/docs/git-pull" target="_blank" rel="noopener">git pull</a> 是 git fetch 和 git merge 的组合，所以拉取更新会触发 <a href="https://git-scm.com/docs/githooks#_post_merge" target="_blank" rel="noopener">post-merge</a> 钩子</li><li>默认新增的文件是没有执行权限的，所以需要通过 <code>chmod</code> 增加 <code>x</code> 位</li></ul><h2 id="部署应用上线"><a href="#部署应用上线" class="headerlink" title="部署应用上线"></a>部署应用上线</h2><p>应用部署上线需要实现持久化和自动化，即项目应该一直在运行，如果服务器重启，项目应该自动启动</p><h3 id="变量自动创建"><a href="#变量自动创建" class="headerlink" title="变量自动创建"></a>变量自动创建</h3><p>/etc/profile.d/ 里的变量创建脚本会在服务器重启时自动运行，所以添加一个设置脚本进去</p><blockquote><p>nono /etc/profile.d/github-webhook.sh</p></blockquote><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">GITHUB_WEBHOOK_PORT</span>=NUMBER</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">GITHUB_WEBHOOK_SECRET</span>=STRING</span><br></pre></td></tr></table></figure><p>运行以下命令可以使变量创建马上生效</p><blockquote><p>source /etc/profile</p></blockquote><h3 id="pm2-运行应用"><a href="#pm2-运行应用" class="headerlink" title="pm2 运行应用"></a>pm2 运行应用</h3><p><a href="https://pm2.keymetrics.io/" target="_blank" rel="noopener">pm2</a> 可以确保 Node 应用的持续运行，并可通过配置实现监控和热更新等功能</p><blockquote><p>npm install pm2 -g<br>pm2 start app.js –name github-webhook </p></blockquote><h3 id="重启自动运行"><a href="#重启自动运行" class="headerlink" title="重启自动运行"></a>重启自动运行</h3><p>pm2 还内置支持配置<a href="https://pm2.keymetrics.io/docs/usage/startup/" target="_blank" rel="noopener">自启动</a>原有应用，通过以下命令实现</p><blockquote><p>pm2 startup<br>pm2 save</p></blockquote><p><code>pm2 startup</code> 会创建并开启开机自动运行的服务， <code>pm2 save</code> 会保存当前的 pm2 运行应用，作为重启后的恢复内容</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在基于 GitHub webhook 的自动化部署中，主要使用了以下技术：</p><ul><li>Node.js 的 http，child_process 和 crypto 模块</li><li>Git 的 post-merge Shell 钩子</li><li>profile 的自动变量设置和 pm2 工具</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;GitHub 的网络钩子（&lt;a href=&quot;https://developer.github.com/webhooks/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;webhook&lt;/a&gt;）功能，可以很方便的实现自动化部署。本文记录了使用 Node.js 的开发部署过程，当项目的 master 分支被推时，将在服务器进行自动部署，完整代码见 &lt;a href=&quot;https://github.com/vinzid/github-webhook/blob/master/app.js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="Git" scheme="https://chanvinxiao.com/cn/blog/tags/Git/"/>
    
      <category term="Node.js" scheme="https://chanvinxiao.com/cn/blog/tags/Node-js/"/>
    
      <category term="Shell" scheme="https://chanvinxiao.com/cn/blog/tags/Shell/"/>
    
  </entry>
  
  <entry>
    <title>使用 Nginx 实现 301 跳转至 https 的根域名</title>
    <link href="https://chanvinxiao.com/cn/blog/301-to-root-with-https-by-nginx/"/>
    <id>https://chanvinxiao.com/cn/blog/301-to-root-with-https-by-nginx/</id>
    <published>2020-04-12T01:41:31.000Z</published>
    <updated>2020-05-05T13:35:15.438Z</updated>
    
    <content type="html"><![CDATA[<p>基于 SEO 和安全性的考量，需要进行 301 跳转，以下使用 Nginx 作通用处理</p><a id="more"></a><h2 id="实现结果"><a href="#实现结果" class="headerlink" title="实现结果"></a>实现结果</h2><p>需要将以下地址都统一跳转到 https 的根域名 <a href="https://chanvinxiao.com">https://chanvinxiao.com</a></p><ul><li><a href="http://chanvinxiao.com">http://chanvinxiao.com</a> （不带 www 的 http）</li><li><a href="http://www.chanvinxiao.com" target="_blank" rel="noopener">http://www.chanvinxiao.com</a> （带 www 的 http）</li><li><a href="https://www.chanvinxiao.com" target="_blank" rel="noopener">https://www.chanvinxiao.com</a> （带 www 的 https）</li></ul><h2 id="301-与-302-的区别"><a href="#301-与-302-的区别" class="headerlink" title="301 与 302 的区别"></a>301 与 302 的区别</h2><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/301" target="_blank" rel="noopener">301</a> 是永久重定向，<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/302" target="_blank" rel="noopener">302</a> 是临时跳转，主要的区别在于搜索引擎对此的对待方式</p><ul><li>301：搜索引擎会将权重和 PR 值进行转移</li><li>302：搜索引擎不会进行额外处理</li></ul><p>现在是希望搜索引擎认为原地址已经不存在了，完全转移到新地址，所以使用 301</p><h2 id="http-跳转到-https"><a href="#http-跳转到-https" class="headerlink" title="http 跳转到 https"></a>http 跳转到 https</h2><p>最简单的方法是直接在 sever 中返回一个重定向的地址，中间再加上 301 状态码（否则默认为 302）</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">  <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">  <span class="attribute">return</span> <span class="number">301</span> https://<span class="variable">$host</span><span class="variable">$request_uri</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><a href="http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#return" target="_blank" rel="noopener">return</a> 和 <a href="http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite" target="_blank" rel="noopener">rewrite</a> 都属于 Nginx 的重写模块的指令，因为这里不需要对路径进行修改，所以用 return 会比较方便</li><li><a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#var_host" target="_blank" rel="noopener">$host</a> 和 <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_uri" target="_blank" rel="noopener">$request_uri</a> 都是 Nginx http 模块的嵌入变量，两个变量合并一起就相当于把请求地址的 http:// 去掉的结果</li></ul><h2 id="www-跳转到根域名"><a href="#www-跳转到根域名" class="headerlink" title="www 跳转到根域名"></a>www 跳转到根域名</h2><p>这个只需在 https 中作处理，因为所有 http 都跳转到 https 了</p><figure class="highlight perl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">  <span class="keyword">listen</span> <span class="number">443</span> ssl;</span><br><span class="line">  server_name ~^(?&lt;www&gt;www\.)?(.+)$;</span><br><span class="line">  <span class="keyword">if</span> ( $www ) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">301</span> https:<span class="regexp">//</span>$2$request_uri;</span><br><span class="line">  &#125;</span><br><span class="line">...</span><br></pre></td></tr></table></figure><ul><li>这里利用了 <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name" target="_blank" rel="noopener">server_name</a> 的正则匹配功能，在其值前增加 ～ 即可启用，支持 <a href="http://www.pcre.org" target="_blank" rel="noopener">PCRE</a> 语法</li><li>使用正则是为了确认是否有前缀 www. 以及捕获根域名，生成两个变量，一个是<a href="http://www.pcre.org/current/doc/html/pcre2pattern.html#SEC16" target="_blank" rel="noopener">具名捕获</a>变量 $www ，另一个是数值捕获变量 $2</li><li>if 里面不支持使用数值捕获变量，否则会报错（<code>unknown &quot;1&quot; variable</code>），所以增加了 <code>?&lt;www&gt;</code> 将 $1 的值赋给了 $www</li></ul><h2 id="减少跳转次数"><a href="#减少跳转次数" class="headerlink" title="减少跳转次数"></a>减少跳转次数</h2><p>以上设置已经满足了实现结果，但是有一点瑕疵，就是 <a href="http://www.chanvinxiao.com" target="_blank" rel="noopener">http://www.chanvinxiao.com</a> 会先跳转到 <a href="https://www.chanvinxiao.com" target="_blank" rel="noopener">https://www.chanvinxiao.com</a> ， 再跳转到 <a href="https://chanvinxiao.com">https://chanvinxiao.com</a> ，进行二次跳转肯定是不如只需跳转一次的，所以最好让其直接一步到位，修改 http 的配置如下：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">  <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">  <span class="attribute">server_name</span> ~^(?:www\.)?(.+)$;</span><br><span class="line">  <span class="attribute">return</span> <span class="number">301</span> https://<span class="variable">$1</span><span class="variable">$request_uri</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>在 http 对应的 sever 中，把 server_name 也改为正则模式，并将 $host 用捕获的根域名 $1 取代</li><li>www 在这里会直接弃掉，所以不需要捕获，使用 ?: 标示实现只<a href="http://www.pcre.org/current/doc/html/pcre2pattern.html#SEC14" target="_blank" rel="noopener">分组</a>不捕获，于是后面的根域名就成了 $1</li><li>这样的结果是不管原来是否带 www，都统一跳转到不带 www 的 https 根域名</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>以上配置中不需指定特定域名，可方便兼容和移植，使用了 Nginx 的以下特性：</p><ul><li>server_name 的正则匹配</li><li>return 指令接收状态码和地址</li><li>$host 和 $request_uri 嵌入变量</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;基于 SEO 和安全性的考量，需要进行 301 跳转，以下使用 Nginx 作通用处理&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="Nginx" scheme="https://chanvinxiao.com/cn/blog/tags/Nginx/"/>
    
      <category term="HTTP" scheme="https://chanvinxiao.com/cn/blog/tags/HTTP/"/>
    
      <category term="Regex" scheme="https://chanvinxiao.com/cn/blog/tags/Regex/"/>
    
  </entry>
  
  <entry>
    <title>作为 attribute 和 property 的 value 及 Vue.js 的相关处理</title>
    <link href="https://chanvinxiao.com/cn/blog/value-as-attribute-and-property-with-vuejs/"/>
    <id>https://chanvinxiao.com/cn/blog/value-as-attribute-and-property-with-vuejs/</id>
    <published>2020-04-06T12:30:00.000Z</published>
    <updated>2020-05-17T01:45:16.850Z</updated>
    
    <content type="html"><![CDATA[<p>attribute 和 property 是 web 开发中，比较容易混淆的概念，而对于 value，因其特殊性，更易困惑，本文尝试做一下梳理和例证</p><a id="more"></a><h2 id="Attribute-和-Property-的概念"><a href="#Attribute-和-Property-的概念" class="headerlink" title="Attribute 和 Property 的概念"></a>Attribute 和 Property 的概念</h2><p>简单的说，attribute 是元素标签的属性，property 是元素对象的属性，例如：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">input</span> <span class="attr">id</span>=<span class="string">"input"</span> <span class="attr">value</span>=<span class="string">"test value"</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="javascript"><span class="keyword">let</span> input = <span class="built_in">document</span>.getElementById(<span class="string">'input'</span>);</span></span><br><span class="line"><span class="javascript"><span class="built_in">console</span>.log(input.getAttribute(<span class="string">'value'</span>)); <span class="comment">// test value</span></span></span><br><span class="line"><span class="javascript"><span class="built_in">console</span>.log(input.value); <span class="comment">// test value</span></span></span><br><span class="line"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><ul><li>input 的 value attribute 是通过标签里的 value=”test value” 定义的，可以通过 input.getAttribute(‘value’) 获取，可以通过 input.setAttribute(‘value’, ‘New Value’) 更新</li><li>input 的 value property 可通过 input.value 获取和更新，初始值是与 attribute 中的赋值一致的</li></ul><h2 id="Attribute-和-Property-的绑定"><a href="#Attribute-和-Property-的绑定" class="headerlink" title="Attribute 和 Property 的绑定"></a>Attribute 和 Property 的绑定</h2><p>如果在最开始的时候，更新 attribute value 的值，property 的值也会随之改变</p><p>但是更新 property value 的值（在文本框输入或给 input.value 赋新值），attribute 的值不会随之改变，而且此时再更新 attribute 的值，property 的值也不再随之改变，如此<a href="value.gif">动画</a>所示，也可访问此<a href="https://codepen.io/chanvin/pen/RwPzdMd?editors=1010" target="_blank" rel="noopener">页面</a>尝试进行操作</p><p>这其实是脏值标记（<a href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-dirty" target="_blank" rel="noopener">dirty value flag</a>）在起作用，dirty value flag 的初始值为 false，即 attribute value 的更新默认会改变对应的 property value，但是一旦用户交互修改了 property value，dirty value flag 的值就变为 true，即 attribute value 的更新就不会改变对应的 property value 了</p><p>所以在实际项目中，我们一般都是在处理作为 property 的 value</p><h2 id="Vue-js-对-Value-的处理"><a href="#Vue-js-对-Value-的处理" class="headerlink" title="Vue.js 对 Value 的处理"></a>Vue.js 对 Value 的处理</h2><h3 id="一般情况使用-value"><a href="#一般情况使用-value" class="headerlink" title="一般情况使用 :value"></a>一般情况使用 :value</h3><p>Vue.js 的 v-bind，一般情况下是在处理 attribute，如果要作为 property 处理的话，需要加上 .prop</p><p>不过 v-bind:value 却大都默认为处理 property 值，因为被强制转化了，例如：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">input</span> <span class="attr">id</span>=<span class="string">"input"</span> <span class="attr">:value</span>=<span class="string">"'test value'"</span> &gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">"https://cdn.jsdelivr.net/npm/vue/dist/vue.js"</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="javascript"><span class="keyword">let</span> input = <span class="keyword">new</span> Vue(&#123;</span></span><br><span class="line"><span class="actionscript">  el: <span class="string">'#input'</span>,</span></span><br><span class="line">  mounted () &#123;</span><br><span class="line"><span class="javascript">    <span class="built_in">console</span>.log(<span class="keyword">this</span>.$el.getAttribute(<span class="string">'value'</span>)); <span class="comment">// null</span></span></span><br><span class="line"><span class="javascript">    <span class="built_in">console</span>.log(<span class="keyword">this</span>.$el.value); <span class="comment">// test value</span></span></span><br><span class="line"><span class="javascript">    <span class="built_in">console</span>.log(<span class="keyword">this</span>._vnode.data) <span class="comment">// &#123;attrs: &#123;id: "input"&#125;, domProps: &#123;value: "test value"&#125;&#125;</span></span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>可见，Vue.js 将 value 作为 VNode 的 data 中的 domProps 的属性，而不是 attrs 的属性，所以挂载后会成为作为 property 的 value</p><p>在 Vue.js 源码中，强制转化 property 的处理如下：</p><figure class="highlight reasonml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/compiler/parser/index.js</span></span><br><span class="line"><span class="keyword">function</span> processAttrs (el) &#123;</span><br><span class="line">...</span><br><span class="line">        <span class="keyword">if</span> ((modifiers<span class="operator"> &amp;&amp; </span>modifiers.prop)<span class="operator"> || </span>(</span><br><span class="line">          !el.component<span class="operator"> &amp;&amp; </span>platform<span class="constructor">MustUseProp(<span class="params">el</span>.<span class="params">tag</span>, <span class="params">el</span>.<span class="params">attrsMap</span>.<span class="params">type</span>, <span class="params">name</span>)</span></span><br><span class="line">        )) &#123;</span><br><span class="line">          add<span class="constructor">Prop(<span class="params">el</span>, <span class="params">name</span>, <span class="params">value</span>, <span class="params">list</span>[<span class="params">i</span>], <span class="params">isDynamic</span>)</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          add<span class="constructor">Attr(<span class="params">el</span>, <span class="params">name</span>, <span class="params">value</span>, <span class="params">list</span>[<span class="params">i</span>], <span class="params">isDynamic</span>)</span></span><br><span class="line">        &#125;</span><br></pre></td></tr></table></figure><p>其中 platformMustUseProp 在 web 平台的定义如下：</p><figure class="highlight lasso"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/platforms/web/util/attrs.js</span></span><br><span class="line">const acceptValue = makeMap(<span class="string">'input,textarea,option,select,progress'</span>)</span><br><span class="line">export const mustUseProp = (<span class="built_in">tag</span>: <span class="built_in">string</span>, <span class="keyword">type</span>: ?<span class="built_in">string</span>, attr: <span class="built_in">string</span>): <span class="built_in">boolean</span> =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    (attr === <span class="string">'value'</span> &amp;&amp; acceptValue(<span class="built_in">tag</span>)) &amp;&amp; <span class="keyword">type</span> !== <span class="string">'button'</span> ||</span><br><span class="line">    (attr === <span class="string">'selected'</span> &amp;&amp; <span class="built_in">tag</span> === <span class="string">'option'</span>) ||</span><br><span class="line">    (attr === <span class="string">'checked'</span> &amp;&amp; <span class="built_in">tag</span> === <span class="string">'input'</span>) ||</span><br><span class="line">    (attr === <span class="string">'muted'</span> &amp;&amp; <span class="built_in">tag</span> === <span class="string">'video'</span>)</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由上可知，类型不为 button 的 input, textarea, option, select 和 progress 的 value 会强制作为 property，而不需要设置为 :value.prop</p><p>例如 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/textarea" target="_blank" rel="noopener">textarea</a> 标签，其本身其实并不支持 value attribute，所以以下代码中的 value 的值并不会显示在多行文本框中</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">textarea</span> <span class="attr">value</span>=<span class="string">"test value"</span>&gt;</span><span class="tag">&lt;/<span class="name">textarea</span>&gt;</span></span><br></pre></td></tr></table></figure><p>但是在 Vue.js 中， 以下代码能成功绑定到 value property 并显示在多行文本框中</p><figure class="highlight elixir"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;textarea <span class="symbol">:value=<span class="string">"'test value'"</span>&gt;&lt;/textarea&gt;</span></span><br></pre></td></tr></table></figure><h3 id="特殊情况使用-value-prop"><a href="#特殊情况使用-value-prop" class="headerlink" title="特殊情况使用 :value.prop"></a>特殊情况使用 :value.prop</h3><p>以上 Vue.js 源码需要注意的还有，强制作为 property， 还要满足 !el.component，即不为动态组件，因为动态组件的 el.component 的值为其 is attribute 的值</p><p>即动态组件的的 v-bind 默认都是作为 attribute 的，如果要作为 property，就要使用 .prop，例如：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">"app"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">component</span> <span class="attr">:is</span>=<span class="string">"element"</span> <span class="attr">:value.prop</span>=<span class="string">"'test value'"</span>&gt;</span><span class="tag">&lt;/<span class="name">component</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> @<span class="attr">click</span>=<span class="string">"change"</span>&gt;</span>Change<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">"https://cdn.jsdelivr.net/npm/vue/dist/vue.js"</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="javascript"><span class="keyword">let</span> app = <span class="keyword">new</span> Vue(&#123;</span></span><br><span class="line"><span class="actionscript">  el: <span class="string">'#app'</span>,</span></span><br><span class="line">  data: &#123;</span><br><span class="line"><span class="actionscript">    element: <span class="string">'input'</span></span></span><br><span class="line">  &#125;,</span><br><span class="line">  methods: &#123;</span><br><span class="line">    change () &#123;</span><br><span class="line"><span class="actionscript">      <span class="keyword">this</span>.element = <span class="string">'input'</span> === <span class="keyword">this</span>.element ? <span class="string">'textarea'</span> : <span class="string">'input'</span>;</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>如果以上 component 中，删除 :value.prop 的 .prop，切换到 textarea 时，其值就不会显示在多行文本框中，可以在此<a href="https://codepen.io/chanvin/pen/KKpjYmy?editors=1010" target="_blank" rel="noopener">页面</a>点击切换标签查看</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li>作为 attribute 和 property 的 value 的绑定关系会在用户交互更新 property 值后失效</li><li>Vue.js 一般使用 :value 即可让 value 作为 property</li><li>Vue.js 动态模版需要使用 :value.prop 才可让 value 作为 property</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;attribute 和 property 是 web 开发中，比较容易混淆的概念，而对于 value，因其特殊性，更易困惑，本文尝试做一下梳理和例证&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="HTML" scheme="https://chanvinxiao.com/cn/blog/tags/HTML/"/>
    
      <category term="JavaScript" scheme="https://chanvinxiao.com/cn/blog/tags/JavaScript/"/>
    
      <category term="Vue.js" scheme="https://chanvinxiao.com/cn/blog/tags/Vue-js/"/>
    
  </entry>
  
  <entry>
    <title>使用 Vue.js 改写 React 的官方教程井字棋</title>
    <link href="https://chanvinxiao.com/cn/blog/vuejs-tic-tac-toe/"/>
    <id>https://chanvinxiao.com/cn/blog/vuejs-tic-tac-toe/</id>
    <published>2020-03-29T01:54:00.000Z</published>
    <updated>2020-05-04T11:29:34.799Z</updated>
    
    <content type="html"><![CDATA[<p>React 的官方<a href="https://zh-hans.reactjs.org/tutorial/tutorial.html" target="_blank" rel="noopener">教程</a>井字棋很好的引导初学者一步步走进 React 的世界，我想类似的教程对 Vue.js 的初学者应该也会有启发，于是使用 Vue.js 进行了改写</p><a id="more"></a><p>可以先查看最终的<a href="https://codepen.io/chanvin/pen/rNVZwJy?editors=0010" target="_blank" rel="noopener">结果</a>，尝试点击体验，我们将逐步地实现这个效果</p><h2 id="初始状态代码"><a href="#初始状态代码" class="headerlink" title="初始状态代码"></a>初始状态代码</h2><h3 id="初始状态查看"><a href="#初始状态查看" class="headerlink" title="初始状态查看"></a>初始状态查看</h3><p>打开<a href="https://codepen.io/chanvin/pen/yLNxVdL" target="_blank" rel="noopener">初始状态</a>直接编辑，或者将对应的文件复制下来放置在同一文件夹中<br>此时只是一个简单的井字棋格子，以及写死的下一个选手</p><h3 id="初始代码分析"><a href="#初始代码分析" class="headerlink" title="初始代码分析"></a>初始代码分析</h3><p>目前定义了三个组件，分别为 Square，Board 和 Game</p><p>Square 目前只是一个普通的按钮</p><figure class="highlight coffeescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Vue.component(<span class="string">'Square'</span>, &#123;</span><br><span class="line">  template: `</span><br><span class="line"><span class="javascript">    &lt;button <span class="class"><span class="keyword">class</span></span>=<span class="string">"square"</span>&gt;</span></span><br><span class="line"><span class="javascript">      &#123;&#123; <span class="comment">/* TODO */</span> &#125;&#125;</span></span><br><span class="line"><span class="javascript">    &lt;<span class="regexp">/button&gt;</span></span></span><br><span class="line"><span class="javascript">  </span>`</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><ul><li>这样定义了组件后，别的组件就可以直接以 &lt;Square /&gt; 的方式引用该组件</li></ul><p>Board 模版由当前状态和 9 个 Square 组成</p><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">Vue.component('Board', &#123;</span><br><span class="line">  data() &#123;</span><br><span class="line"><span class="built_in">    return</span> &#123;</span><br><span class="line">      status: `$&#123;nextLabel&#125;X`,</span><br><span class="line">      board: [</span><br><span class="line">        [<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>],</span><br><span class="line">        [<span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>],</span><br><span class="line">        [<span class="number">6</span>, <span class="number">7</span>, <span class="number">8</span>]</span><br><span class="line">      ]</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  template: `</span><br><span class="line">    &lt;<span class="keyword">div</span>&gt;</span><br><span class="line">      &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"status"</span>&gt;&#123;&#123; status &#125;&#125;&lt;/<span class="keyword">div</span>&gt;</span><br><span class="line">      &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"board-row"</span> v-<span class="keyword">for</span>=<span class="string">"(row, index) in board"</span> :key=<span class="string">"index"</span>&gt;</span><br><span class="line">        &lt;Square v-<span class="keyword">for</span>=<span class="string">"square in row"</span> :key=<span class="string">"square"</span> /&gt;</span><br><span class="line">      &lt;/<span class="keyword">div</span>&gt;</span><br><span class="line">    &lt;/<span class="keyword">div</span>&gt;</span><br><span class="line">  `</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ul><li>data 定义了当前状态 status，和 board 的值，这样在模版中就可以用 &lbrace;{ status }} 的方式引用状态值，使用 v-for 将 board 二维数组里的值两次循环组装成井字格  </li><li>在组件中的 data 必须是返回对象的函数而非对象字面值</li><li>v-for 需要有 key 确保性能以及不报警告</li></ul><p>Game 模版由 Board 与 后面需要增加的状态和历史组成</p><figure class="highlight django"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="xml">Vue.component('Game', &#123;</span></span><br><span class="line"><span class="xml">  template: `</span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"game"</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"game-board"</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">Board</span> /&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"game-info"</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><span class="template-variable">&#123;&#123; /* status */ &#125;&#125;</span><span class="xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">        <span class="tag">&lt;<span class="name">ol</span>&gt;</span></span><span class="template-variable">&#123;&#123; /* TODO */ &#125;&#125;</span><span class="xml"><span class="tag">&lt;/<span class="name">ol</span>&gt;</span></span></span><br><span class="line"><span class="xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="xml">  `</span></span><br><span class="line"><span class="xml">&#125;);</span></span><br></pre></td></tr></table></figure><h2 id="增加数据处理"><a href="#增加数据处理" class="headerlink" title="增加数据处理"></a>增加数据处理</h2><h3 id="增加-Props"><a href="#增加-Props" class="headerlink" title="增加 Props"></a>增加 Props</h3><p>在 Board 中传递一个名为 value 的 prop 到 Square</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;Square <span class="attribute">v-for</span>=<span class="string">"square in row"</span> :<span class="attribute">key</span>=<span class="string">"square"</span> :<span class="attribute">value</span>=<span class="string">"square"</span> /&gt;</span><br></pre></td></tr></table></figure><ul><li>:value 是 v-bind:value 的缩写，表示其值是一个表达式</li></ul><p>在 Square 的组件定义和模版中增加 value prop</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Vue.component(<span class="string">'Square'</span>, &#123;</span><br><span class="line">  props: [<span class="string">'value'</span>],</span><br><span class="line">  template: `</span><br><span class="line">    &lt;button <span class="keyword">class</span>=<span class="string">"square"</span>&gt;</span><br><span class="line">      &#123;&#123; <span class="keyword">value</span> &#125;&#125;</span><br><span class="line">    &lt;/button&gt;</span><br><span class="line">  `</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><ul><li>props 为父组件可传递给子组件的变量，在父组件调用子组件时在标签中设置对应属性，在子组件中使用方法与 data 一致</li></ul><p>目前的<a href="https://codepen.io/chanvin/pen/wvaEgPV?editors=0010" target="_blank" rel="noopener">代码和效果</a>：0 - 8 的数字分别填充进井字棋格中</p><h3 id="增加交互"><a href="#增加交互" class="headerlink" title="增加交互"></a>增加交互</h3><p>增加点击事件至按钮元素以更新值</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">Vue.component(<span class="string">'Square'</span>, &#123;</span><br><span class="line">  <span class="comment">//props: ['value'],</span></span><br><span class="line">  data() &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="keyword">value</span>: <span class="literal">null</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  methods: &#123;</span><br><span class="line">    setValue() &#123;</span><br><span class="line">      <span class="keyword">this</span>.<span class="keyword">value</span> = <span class="string">'X'</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  template: `</span><br><span class="line">    &lt;button <span class="keyword">class</span>=<span class="string">"square"</span> @click=<span class="string">"setValue"</span>&gt;</span><br><span class="line">      &#123;&#123; <span class="keyword">value</span> &#125;&#125;</span><br><span class="line">    &lt;/button&gt;</span><br><span class="line">  `</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><ul><li>@click 为 v-on:click 的缩写，其值为点击需要运行的函数，这里为组件定义的方法 methods 中的 setValue  </li><li>子组件不能直接更新父组件的值，所以将 value 从 props 改为 data</li><li>data 的值更新，对应模版就会自动更新展示内容</li></ul><p>目前的<a href="https://codepen.io/chanvin/pen/jOPvyxW?editors=0010" target="_blank" rel="noopener">代码和效果</a>：点击井字棋格，对应填充 X</p><h2 id="完善游戏"><a href="#完善游戏" class="headerlink" title="完善游戏"></a>完善游戏</h2><h3 id="数值提升"><a href="#数值提升" class="headerlink" title="数值提升"></a>数值提升</h3><p>为交替落子和确认输赢，需要统一判断各格状态，所以将 value 提升至 Board</p><p>Board 增加数据 squares 和方法 handleClick</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">Vue.component(<span class="string">'Board'</span>, &#123;</span><br><span class="line">  data() &#123;</span><br><span class="line">    return &#123;</span><br><span class="line">      <span class="built_in">..</span>.</span><br><span class="line">      squares: Array(9).fill(<span class="literal">null</span>),</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  methods: &#123;</span><br><span class="line">    handleClick(i) &#123;</span><br><span class="line">      const squares = this.squares.slice();</span><br><span class="line">      <span class="keyword">if</span> (squares[i])&#123;</span><br><span class="line">        alert(<span class="string">'此位置已被占!'</span>);</span><br><span class="line">        return</span><br><span class="line">      &#125;</span><br><span class="line">      squares[i] = <span class="string">'X'</span>;</span><br><span class="line">      this.squares = squares;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  template: `</span><br><span class="line">    <span class="built_in">..</span>.</span><br><span class="line">      &lt;div <span class="attribute">class</span>=<span class="string">"board-row"</span> <span class="attribute">v-for</span>=<span class="string">"(row, index) in board"</span> :<span class="attribute">key</span>=<span class="string">"index"</span>&gt;</span><br><span class="line">        &lt;Square <span class="attribute">v-for</span>=<span class="string">"square in row"</span> :<span class="attribute">key</span>=<span class="string">"square"</span> :<span class="attribute">value</span>=<span class="string">"squares[square]"</span> @<span class="attribute">click</span>=<span class="string">"handleClick(square)"</span> /&gt;</span><br></pre></td></tr></table></figure><ul><li>squares 初始为 9 个 null 组成的数组，井字棋盘为空的状态</li><li>handleClick 接收对应格子序号的参数，并更新对应的 square 元素</li><li>事件处理器不是 handleClick(square) 的返回值，而是 handleClick，只是在触发时会带上参数值 square</li></ul><p>在 Square 的点击事件处理器中触发 Board 的点击事件</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Vue.component(<span class="string">'Square'</span>, &#123;</span><br><span class="line">  props: [<span class="string">'value'</span>],</span><br><span class="line">  methods: &#123;</span><br><span class="line">    setValue() &#123;</span><br><span class="line">      <span class="keyword">this</span>.$emit(<span class="string">'click'</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br></pre></td></tr></table></figure><ul><li>value 要从 data 改回到 props</li><li>$emit 可以调用父组件传递的事件处理器</li><li>prop 里的值在父组件更新，子组件模版也会对应更新展示内容</li></ul><p>目前的<a href="https://codepen.io/chanvin/pen/mdJGWxV?editors=0010" target="_blank" rel="noopener">代码和效果</a>：点击井字棋格，如果未被占，则填充 X</p><h3 id="轮流落子"><a href="#轮流落子" class="headerlink" title="轮流落子"></a>轮流落子</h3><p>增加数据 xIsNext，并在点击时切换</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">data</span>() &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    ...</span><br><span class="line">    xIsNext: <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;,</span><br><span class="line">methods: &#123;</span><br><span class="line">    handleClick(i) &#123;</span><br><span class="line">      ...</span><br><span class="line">      squares[i] = <span class="keyword">this</span>.xIsNext ? <span class="string">'X'</span> : <span class="string">'O'</span>;</span><br><span class="line">      <span class="keyword">this</span>.squares = squares;</span><br><span class="line">      <span class="keyword">this</span>.xIsNext = !<span class="keyword">this</span>.xIsNext;</span><br><span class="line">      <span class="keyword">this</span>.status = `$&#123;nextLabel&#125;$&#123;<span class="keyword">this</span>.xIsNext ? <span class="string">'X'</span> : <span class="string">'O'</span>&#125;`;</span><br></pre></td></tr></table></figure><ul><li>xIsNext 初始值为 true，即 X 先落子</li><li>点击后，通过取反交替 xIsNext</li><li>更新状态值 status 为下一个落子者</li></ul><p>目前的<a href="https://codepen.io/chanvin/pen/ZEGMKBZ?editors=0010" target="_blank" rel="noopener">代码和效果</a>：点击井字棋格，X 和 O 交替落子</p><h3 id="判断胜者"><a href="#判断胜者" class="headerlink" title="判断胜者"></a>判断胜者</h3><p>增加计算胜者的函数</p><figure class="highlight lsl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">function calculateWinner(squares) &#123;</span><br><span class="line">  const lines = [</span><br><span class="line">    [<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>],</span><br><span class="line">    [<span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>],</span><br><span class="line">    [<span class="number">6</span>, <span class="number">7</span>, <span class="number">8</span>],</span><br><span class="line">    [<span class="number">0</span>, <span class="number">3</span>, <span class="number">6</span>],</span><br><span class="line">    [<span class="number">1</span>, <span class="number">4</span>, <span class="number">7</span>],</span><br><span class="line">    [<span class="number">2</span>, <span class="number">5</span>, <span class="number">8</span>],</span><br><span class="line">    [<span class="number">0</span>, <span class="number">4</span>, <span class="number">8</span>],</span><br><span class="line">    [<span class="number">2</span>, <span class="number">4</span>, <span class="number">6</span>],</span><br><span class="line">  ];</span><br><span class="line">  for (let i = <span class="number">0</span>; i &lt; lines.length; i++) &#123;</span><br><span class="line">    const [a, b, c] = lines[i];</span><br><span class="line">    if (squares[a] &amp;&amp; squares[a] === squares[b] &amp;&amp; squares[a] === squares[c]) &#123;</span><br><span class="line">      return squares[a];</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  return null;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>列举可能获胜的组合，与 squares 数组的值进行比对</li></ul><p>增加点击处理函数的胜者逻辑</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (calculateWinner(squares)) &#123;</span><br><span class="line">  alert(<span class="string">'胜负已定！'</span>);</span><br><span class="line">  <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line">...</span><br><span class="line"><span class="keyword">const</span> winner = calculateWinner(squares);</span><br><span class="line"><span class="keyword">if</span> (winner) &#123;</span><br><span class="line">  <span class="keyword">this</span>.status = <span class="string">'获胜者: '</span> + winner;</span><br><span class="line">  <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>点击后，如果之前已有取胜，则点击无效</li><li>处理落子后，再次判断是否取胜，更新状态</li></ul><p>目前的<a href="https://codepen.io/chanvin/pen/PoqdmEQ?editors=0010" target="_blank" rel="noopener">代码和效果</a>：有一方获胜后， 状态和点击处理更新</p><h2 id="增加时间旅行"><a href="#增加时间旅行" class="headerlink" title="增加时间旅行"></a>增加时间旅行</h2><h3 id="保存历史记录"><a href="#保存历史记录" class="headerlink" title="保存历史记录"></a>保存历史记录</h3><p>为实现“悔棋”功能，需要记录每一次落子的整体状态，相当于棋盘的快照，作为一个历史记录，提升至 Game 组件中</p><p>在 Game 增加数据 history，将 xIsNext，status 和 handleClick 方法 从 Board 中转移到 Game 中</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">Vue.component(<span class="string">'Game'</span>, &#123;</span><br><span class="line">  <span class="keyword">data</span>() &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      history: [&#123;</span><br><span class="line">        squares: Array(<span class="number">9</span>).fill(<span class="literal">null</span>),</span><br><span class="line">      &#125;],</span><br><span class="line">      xIsNext: <span class="literal">true</span>,</span><br><span class="line">      status: `$&#123;nextLabel&#125;X`</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  methods: &#123;</span><br><span class="line">    handleClick(i) &#123;</span><br><span class="line">      <span class="keyword">const</span> history = <span class="keyword">this</span>.history;</span><br><span class="line">      <span class="keyword">const</span> current = history[history.length - <span class="number">1</span>]</span><br><span class="line">      <span class="keyword">const</span> squares = current.squares.slice();</span><br><span class="line">      ...</span><br><span class="line">      squares[i] = <span class="keyword">this</span>.xIsNext ? <span class="string">'X'</span> : <span class="string">'O'</span>;</span><br><span class="line">      history.push(&#123;</span><br><span class="line">        squares: squares</span><br><span class="line">      &#125;);</span><br><span class="line">      ...</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  template: `</span><br><span class="line">    &lt;div <span class="class"><span class="keyword">class</span>="<span class="title">game</span>"&gt;</span></span><br><span class="line">      &lt;div <span class="class"><span class="keyword">class</span>="<span class="title">game</span>-<span class="title">board</span>"&gt;</span></span><br><span class="line">        &lt;Board :squares=<span class="string">"history[history.length - 1].squares"</span> <span class="meta">@click</span>=<span class="string">"handleClick"</span> /&gt;</span><br><span class="line">  `</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><ul><li>squares 从 history 的最后一个记录取值（目前只有一个记录）</li><li>落子后，squares 把落子记录进去后，history 再增加一个记录</li></ul><p>Board 增加 prop squares，handleClick 更新为调用父组件的事件处理器</p><figure class="highlight reasonml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="module-access"><span class="module"><span class="identifier">Vue</span>.</span></span>component('Board', &#123;</span><br><span class="line">  props: <span class="literal">['<span class="identifier">squares</span>']</span>,</span><br><span class="line">  methods: &#123;</span><br><span class="line">    handle<span class="constructor">Click(<span class="params">i</span>)</span> &#123;</span><br><span class="line">      this.<span class="constructor">$emit('<span class="params">click</span>', <span class="params">i</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br></pre></td></tr></table></figure><p>目前的<a href="https://codepen.io/chanvin/pen/poJOwbv?editors=0010" target="_blank" rel="noopener">代码和效果</a>：状态位置更新，历史记录已存储</p><h3 id="展示历史步骤记录"><a href="#展示历史步骤记录" class="headerlink" title="展示历史步骤记录"></a>展示历史步骤记录</h3><p>把历史记录循环展示出来，并绑定点击事件，通过 stepNumber 的更新显示对应步骤的记录</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">Vue.component(<span class="string">'Game'</span>, &#123;</span><br><span class="line">  data() &#123;</span><br><span class="line">    ...</span><br><span class="line">      stepNumber: <span class="number">0</span>,</span><br><span class="line">    ...</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  methods: &#123;</span><br><span class="line">    handleClick(i) &#123;</span><br><span class="line">      <span class="keyword">const</span> history = <span class="keyword">this</span>.history.slice(<span class="number">0</span>, <span class="keyword">this</span>.stepNumber + <span class="number">1</span>);</span><br><span class="line">      ...</span><br><span class="line">      <span class="keyword">this</span>.history = history.concat([&#123;</span><br><span class="line">        squares: squares</span><br><span class="line">      &#125;]);</span><br><span class="line">      <span class="keyword">this</span>.stepNumber = history.length;</span><br><span class="line">      ...</span><br><span class="line">    &#125;,</span><br><span class="line">    jumpTo(step) &#123;</span><br><span class="line">      <span class="keyword">if</span>(step === <span class="keyword">this</span>.stepNumber)&#123;</span><br><span class="line">        alert(<span class="string">'已在'</span> + (<span class="number">0</span> === step ? <span class="string">'最开始'</span> : `步骤<span class="meta">#$&#123;step&#125;！`));</span></span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">this</span>.stepNumber = step;</span><br><span class="line">      <span class="keyword">this</span>.xIsNext = (step % <span class="number">2</span>) === <span class="number">0</span>;</span><br><span class="line">      <span class="keyword">this</span>.status = `$&#123;nextLabel&#125;$&#123;<span class="keyword">this</span>.xIsNext ? <span class="string">'X'</span> : <span class="string">'O'</span>&#125;`;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  template: `</span><br><span class="line">    &lt;div <span class="keyword">class</span>=<span class="string">"game"</span>&gt;</span><br><span class="line">      &lt;div <span class="keyword">class</span>=<span class="string">"game-board"</span>&gt;</span><br><span class="line">        &lt;Board :squares=<span class="string">"history[this.stepNumber].squares"</span> @click=<span class="string">"handleClick"</span> /&gt;</span><br><span class="line">      &lt;/div&gt;</span><br><span class="line">      &lt;div <span class="keyword">class</span>=<span class="string">"game-info"</span>&gt;</span><br><span class="line">        &lt;div&gt;&#123;&#123; status &#125;&#125;&lt;/div&gt;</span><br><span class="line">        &lt;ol&gt;</span><br><span class="line">          &lt;li v-<span class="keyword">for</span>=<span class="string">"(squares, index) in history"</span> :key=<span class="string">"index"</span> :<span class="keyword">class</span>=<span class="string">"&#123;'move-on': index === stepNumber&#125;"</span>&gt;</span><br><span class="line">            &lt;button @click=<span class="string">"jumpTo(index)"</span>&gt;&#123;&#123; <span class="number">0</span> === index ? <span class="string">'回到开始'</span> : <span class="string">'回到步骤#'</span> + index &#125;&#125;&lt;/button&gt;</span><br><span class="line">   ...</span><br><span class="line">  `</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><ul><li>在 Game 中增加 stepNumber，初始为 0，记录当前展示的步骤</li><li>将 Board 的 prop squares 的取值更新为 this.stepNumber 对应的步骤</li><li>handleClick 中以已当前步骤为基础处理 history，并更新 stepNumber</li><li>增加方法 jumpTo 处理回到历史的展示，更新 stepNumber，xIsNext 和 status</li></ul><p>最终的<a href="https://codepen.io/chanvin/pen/rNVZwJy?editors=0010" target="_blank" rel="noopener">代码和效果</a>：每落一子，都会增加一个历史步骤，点击步骤可回到该步</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><h3 id="游戏实现内容"><a href="#游戏实现内容" class="headerlink" title="游戏实现内容"></a>游戏实现内容</h3><ul><li>交替落子</li><li>判断输赢</li><li>悔棋重来</li></ul><h3 id="展示技术内容"><a href="#展示技术内容" class="headerlink" title="展示技术内容"></a>展示技术内容</h3><ul><li>v-bind：在模版中进行数据绑定</li><li>v-for：在模版中进行数组循环</li><li>v-on, $emit：在组件间进行事件传递和触发 </li><li>data：在组件的定义和模版自动更新</li><li>prop：在组件间的传递和模版自动更新</li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;React 的官方&lt;a href=&quot;https://zh-hans.reactjs.org/tutorial/tutorial.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;教程&lt;/a&gt;井字棋很好的引导初学者一步步走进 React 的世界，我想类似的教程对 Vue.js 的初学者应该也会有启发，于是使用 Vue.js 进行了改写&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="JavaScript" scheme="https://chanvinxiao.com/cn/blog/tags/JavaScript/"/>
    
      <category term="Vue.js" scheme="https://chanvinxiao.com/cn/blog/tags/Vue-js/"/>
    
  </entry>
  
</feed>
