<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Leet</title>
        <link>https://leetme.netlify.app</link>
        <description>Leet' Blog</description>
        <lastBuildDate>Wed, 22 Oct 2025 03:26:11 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Leet</title>
            <url>https://leetme.netlify.app/avatar.png</url>
            <link>https://leetme.netlify.app</link>
        </image>
        <copyright>CC BY-NC-SA 4.0 2023 © Leet</copyright>
        <atom:link href="https://leetme.netlify.app/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[如何做一个动态心电图]]></title>
            <link>https://leetme.netlify.app/posts/streaming-chart</link>
            <guid>https://leetme.netlify.app/posts/streaming-chart</guid>
            <pubDate>Tue, 29 Apr 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>前言</h2>
<p>我之前写了一个 <a href="./canvas-ecg.md">Canvas 实现动态心电图</a> 文章。它确实能实现需求，但是，面对奇特的需求，它已经不够用了，我能力有限实在做不了更多优化。所以我还是另寻其他方法。</p>
<p>自从收到用来测试的模拟数据后，我就发现不对劲，数据每秒发送几百条，虽然发现了之前的功能面临的问题（画不过来、性能等），但是我就是不想解决，我就是摆。反正公司有时间给我做，我就干脆再找个更加完美的解决方案。</p>
<p>就吐槽这么多，算球。</p>
<h2>寻找方案</h2>
<p>对于这些具体的小众的需求，网上很难找到合适的办法，谷歌都快被我搜烂了，也找不到一个和我的需求相似的例子。</p>
<p>贴几个在搜寻过程中看到的几个例子：</p>
<ul>
<li>
<p><a href="https://juejin.cn/post/7002038619229650958">Canvas如何做个心电图动画</a></p>
</li>
<li>
<p><a href="https://juejin.cn/post/6966136983391305764">如何在react中使用canvas画动态心电图</a></p>
</li>
<li>
<p>一个数据流图形库 <a href="http://smoothiecharts.org/">smoothie charts</a></p>
</li>
<li>
<p>还有两个库密码的要买的</p>
<ul>
<li><a href="https://demo.scichart.com/react/vital-signs-ecg-medical-chart-example">SciChart</a></li>
<li><a href="https://lightningchart.com/js-charts/interactive-examples/edit/lcjs-example-0150-ecg.html">LightningChart</a></li>
</ul>
</li>
</ul>
<p>后来灵感在于我想 <code>echarts</code> 能不能做，封装太高了，不是很合适，我就开始寻找其他类似于 <code>echarts</code> 的图表库。</p>
<p>看到 <a href="https://d3js.org/">D3.js</a> 这个库，感觉它的自由度很高，应该能实现我的需求，但是学习路线挺陡的。看到这是个非常火的库（Github 一百多k star），我觉得只有他能救我命了。于是就开始了我的探索之旅。</p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[前端如何处理流式数据（SSE）]]></title>
            <link>https://leetme.netlify.app/posts/sse-stream-solution</link>
            <guid>https://leetme.netlify.app/posts/sse-stream-solution</guid>
            <pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>开始之前...</h2>
<p>最近又接到一个模块的需求，起初是需要跟后端对接ai对话的api，这是最初版的，当时还是用的其他的返回格式 <code>application/x-ndjson</code>。</p>
<p>但是由于最近的deepseek，后端又将ai换成了deepseek的了，deepseek的返回格式是 <code>text/event-stream</code>，用的协议是 <code>SSE (Server-Sent Events)</code>。</p>
<h2>返回数据格式</h2>
<p>可以参考<a href="https://api-docs.deepseek.com/zh-cn/api/create-chat-completion">deepseek api文档</a>，可知我们需要处理的数据的格式是怎样的：</p>
<pre><code class="language-text">data: {&quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;choices&quot;: [{&quot;index&quot;: 0, &quot;delta&quot;: {&quot;content&quot;: &quot;&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;usage&quot;: null}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot;Hello&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot;!&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot; How&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot; can&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot; I&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot; assist&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot; you&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot; today&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot;?&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;}

data: {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot;&quot;, &quot;role&quot;: null}, &quot;finish_reason&quot;: &quot;stop&quot;, &quot;index&quot;: 0, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;, &quot;usage&quot;: {&quot;completion_tokens&quot;: 9, &quot;prompt_tokens&quot;: 17, &quot;total_tokens&quot;: 26}}

data: [DONE]
</code></pre>
<p>::: tip<br>
要注意的是每一条数据之间都是有<code>\n\n</code>分隔的，标准SSE事件，每条事件就是用'\n\n'分隔的。这个信息在后面处理数据时有用。<br>
:::</p>
<h2>处理方法</h2>
<p>其实这个在网上搜索就知道了，我也看过，很多答案都是用一个前端的API <code>EventSource</code> 来处理的，或者就是用 event-source 的改库 <code>event-source-polyfill</code>。</p>
<p><code>EventSource</code> 限制在于只能使用 'GET' 方法，请求需要的参数都只能明文传输。</p>
<p><code>event-source-polyfill</code> 虽然是基于（XHR）的版本，理论上我们是可以改造xhr来发post请求的。但是为了兼容SSE协议规范，还是会默认使用get方法；另外就算可以模拟post请求，但也已经偏离了标准SSE，相当于自己造轮子了。</p>
<p>我的原则是还是少用点库，有些能自己解决就更好。</p>
<p>所有我还是用常规请求，用fetch和axios都可以，但是fetch是原生支持SSE的，用axios需要指定下适配器 <code>adapter</code>。</p>
<h2>代码实现</h2>
<h3>请求方法</h3>
<p>先找你的后端拿到对应的接口地址先。</p>
<pre><code class="language-js">// const params = {}

const response = await fetch('/v1/chat/completions', {
  method: 'POST',
  body: JSON.stringify(params), // 需要序列化
  headers: {
    'Content-Type': 'application/json',
  },
})
</code></pre>
<p>::: details axios版</p>
<pre><code class="language-js">const response = await axios.post('/v1/chat/completions', params, {
  responseType: 'stream', // axios 需要指定响应格式
  adapter: 'fetch'
})
</code></pre>
<p>:::</p>
<h3>解析流数据</h3>
<p>返回的数据打印你看不到内容，只能知道它是一个 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream"><code>ReadableStream</code></a>。最简单的处理方式就是：</p>
<pre><code class="language-js">const reader = response.body.getReader()

let data = ''

while (true) {
  const { done, value } = await reader.read()
  if (done)
    break

  data += new TextDecoder().decode(value)
}
</code></pre>
<p>这就是一个常用的办法来处理的。但是吧，这应该是针对后端已经帮你处理好了ai api返回的数据，能直接使用的。如果是原始的<a href="#%E8%BF%94%E5%9B%9E%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F">数据格式</a>，那还需要我们自己处理一下。</p>
<p>::: tip<br>
如果是 axios 那么是通过 response.data<br>
:::</p>
<p>接下来我还是用更加完善的另一种方法实现数据处理。</p>
<p>在mdn文档中还看得到另外一种处理流的方法，就是异步的for循环：</p>
<pre><code class="language-js">for await (const chunk of response.body) {
  // ...
}
</code></pre>
<p>现在是需要将 <code>response.body</code> 改造成遍历后能直接使用的数据，而不是这一串字符串。</p>
<blockquote>
<p>data: {&quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;, &quot;choices&quot;: [{&quot;index&quot;: 0, &quot;delta&quot;: {&quot;content&quot;: &quot;&quot;, &quot;role&quot;: &quot;assistant&quot;}, &quot;finish_reason&quot;: null, &quot;logprobs&quot;: null}], &quot;created&quot;: 1718345013, &quot;model&quot;: &quot;deepseek-chat&quot;, &quot;system_fingerprint&quot;: &quot;fp_a49d71b8a1&quot;, &quot;object&quot;: &quot;chat.completion.chunk&quot;, &quot;usage&quot;: null}</p>
</blockquote>
<p>需要先了解几个方法：</p>
<ul>
<li><code>TextDecoderStream</code></li>
</ul>
<p>这个方法跟<code>TextDecoder</code>是类似的，不同就在于<code>TextCoder</code>是一次性拿到完整二进制数据解析成文本；而<code>TextDecoderStream</code>则是实时将数据流转成文本流（边传边解析）。不过<code>TextDecoder</code>也可以实现，只不过需要<code>{ stream: true }</code>手动模拟流。</p>
<ul>
<li><code>pipeThrough(transformStream, option)</code></li>
</ul>
<blockquote>
<p>提供将当前流管道输出到一个转换（transform）流或可写/可读流对的链式方法。</p>
</blockquote>
<p>就是把一个流的数据通过转换流处理一下，输出新的流。通俗讲就是对流的数据边收边改、边流边处理。<br>
跟<code>TextDecoder</code>的read和write类似，只不过更方便更现代化。</p>
<ul>
<li><code>new TransformStream({})</code></li>
</ul>
<p>这个就是<code>pipeThrough</code>需要用到的转换流对象，它包含了<code>transform</code>(每一段流的处理过程)和<code>flush</code>(接收完所有流后的收尾工作)</p>
<p><strong>具体实现</strong></p>
<pre><code class="language-js">function transformStream(readableStream) {
  const decoderStream = new TextDeCoderStream()

  const stream = readableStream
    .pipeThrough(decoderStream)
    .pipeThrough(function () {
      let buffer = ''
      return new TransformStream({
        transform(streamChunk, controller) {
          buffer += streamChunk

          const parts = buffer.split('\n\n')
          parts.slice(0, -1).forEach((part) =&gt; {
            controller.enqueue(part)
          })

          buffer = parts[parts.length - 1]
        },
        flush(controller) {
          if ((buffer ?? '').trim() !== '') {
            controller.enqueue(buffer)
          }
        }
      })
    }())
    .pipeThrough(new TransformStream({
      transform(chunk, controller) {
        const lines = chunk.split('\n')
        const sseEvent = lines.reduce((acc, line) =&gt; {
          const separatorIndex = line.indexOf(':')
          if (separatorIndex === -1) {
            throw new Error('The key-value separator &quot;:&quot; is not found in the sse line chunk!')
          }

          const key = line.slice(0, separatorIndex)
          const value = line.slice(separatorIndex + 1)

          return {
            ...acc,
            [key]: value
          }
        }, {})

        if (Object.keys(sseEvent).length === 0)
          return

        controller.enqueue(sseEvent)
      }
    }))

  return stream
}
</code></pre>
<p><strong>步骤解析</strong></p>
<ol>
<li>第一个 <code>pipeThrough()</code></li>
</ol>
<p>这个步骤目的是将原始的二进制流转成字符串流。这个二进制流你可以通过浏览器F12，在network栏查看流请求的二进制数据。</p>
<p><code>Uint8Array([100, 97, 116, 97, 58, 32, 123, ...])</code> =&gt; <code>&quot;data: {\&quot;id\&quot;: \&quot;1f633...\&quot;}&quot;</code></p>
<ol start="2">
<li>第二个 <code>pipeThrough()</code></li>
</ol>
<p>这个步骤目的是将完整的字符串切成每条独立的SSE消息。<br>
最后一条消息也同样是有<code>\n\n</code>分隔符的，所以最后一项一定是空字符串，所以处理的时候要排除掉最后一项的空字符串。</p>
<ol start="3">
<li>第三个 <code>pipeThrough()</code></li>
</ol>
<p>这个步骤目的是将每条SSE消息字符串解析成一个对象。<br>
因为每条SSE消息可能会包含多个data行的，它们通过'\n'分隔。拆分后，再通过<code>reduce</code>方法或其他办法处理成一个对象 <code>{ data: &quot;{...}&quot; }</code>。</p>
<p>最后在异步for循环中遍历得到的 <code>chunk</code> 就是处理后的数据: <code>{ data: &quot;{...}&quot; }</code>。</p>
<p>::: tip<br>
其实还可以最后一层的transform()处理一下，把data的值解析成一个对象</p>
<pre><code class="language-js">const parsed = JSON.parse(sseEvent.data)

// controller.enqueue(sseEvent) 改成
controller.enqueue(parsed)
</code></pre>
<p>:::</p>
<h3>处理解析后的结果</h3>
<pre><code class="language-js">let fullContent = ''

for await (const chunk of transformStream(response.body)) {
  const { data } = chunk

  // 返回的字符串流，它们可能会包含一个空格符在前面 eg: &quot;id&quot;: &quot;1f633d8bfc032625086f14113c411638&quot;
  if (data.trim() === '[DONE]')
    break

  const parsed = JSON.parse(data)
  const content = parse.choices[0].delta.content ?? '' // 具体格式看你请求具体返回

  fullContent += content
}
</code></pre>
<p>这就可以逐步拿到整个内容了！</p>
<h2>代码优化</h2>
<p>这样看着很冗长，我们可以拆分业务到单独的方法中去（这里我把三个方法拆分开看这方便一点）：</p>
<p>::: code-group</p>
<pre><code class="language-js">async function handleStream() {
  const params = {}

  const response = await fetchStream(params)

  for await (const chunk of transformStream(response.body)) {
    // ...
  }
}

function transformStream(readablesStream) {
  const decoderStream = new TextCoderStream()

  const stream = readableStream
    .pipeThrough(decoderStream)
    .pipeThrough(splitStream())
    .pipeThrough(splitParts())

  return stream
}
</code></pre>
<pre><code class="language-js">function splitStream() {
  let buffer = ''
  return new TransformStream({
    transform(streamChunk, controller) {
      buffer += streamChunk

      const parts = buffer.split('\n\n')
      parts.slice(0, -1).forEach((part) =&gt; {
        controller.enqueue(part)
      })

      buffer = parts[parts.length - 1]
    },
    flush(controller) {
      if ((buffer ?? '').trim() !== '') {
        controller.enqueue(buffer)
      }
    }
  })
}
</code></pre>
<pre><code class="language-js">function splitParts() {
  return new TransformStream({
    transform(chunk, controller) {
      const lines = chunk.split('\n')
      const sseEvent = lines.reduce((acc, line) =&gt; {
        const separatorIndex = line.indexOf(':')
        if (separatorIndex === -1) {
          throw new Error('The key-value separator &quot;:&quot; is not found in the sse line chunk!')
        }

        const key = line.slice(0, separatorIndex)
        const value = line.slice(separatorIndex + 1)

        return {
          ...acc,
          [key]: value
        }
      }, {})

      if (Object.keys(sseEvent).length === 0)
        return

      controller.enqueue(sseEvent)
    }
  })
}
</code></pre>
<pre><code class="language-js">async function fetchStream(params) {
  const response = await fetch('/v1/chat/completions', {
    method: 'POST',
    body: JSON.stringify(params), // 需要序列化
    headers: {
      'Content-Type': 'application/json',
    },
  })

  return response
}
</code></pre>
<p>:::</p>
<p>这样最终版本就完成了。这个版本优势我觉得在于还可以进一步封装来支持其他协议的流，做到真正通用的处理流数据的方法。</p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[Canvas实现动态心电图（ECG）]]></title>
            <link>https://leetme.netlify.app/posts/canvas-ecg</link>
            <guid>https://leetme.netlify.app/posts/canvas-ecg</guid>
            <pubDate>Sun, 05 Jan 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>开始之前...</h2>
<p>最近接到一个需求，用于观测病人数据的心电图功能。当时看到这个功能以为只是一个图形，用 <code>echarts</code> 就能解决了，后来感觉比较难实现（我也不确定一定不能实现，至少会感觉很麻烦我就没有考虑这个方法）。</p>
<p>第一时间问了几个大模型（GPT, Claude, Gemini）给出了一致的答案，都是用 <code>canvas</code> 实现。</p>
<p>那么就开始吧。</p>
<h2>Canvas</h2>
<p>这个东西不熟悉也挺熟悉的，说熟悉也感觉又不太熟悉；就是知道这个东西，但是里面的用法都不太会。</p>
<blockquote>
<p>可以看看 MDN 文档里的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API">Canvas 介绍和教程</a>，完全够用了。</p>
</blockquote>
<p>主要用到的是 <code>HTMLCanvasElement</code> 这个实例，通过实例的 <code>getContext</code> 返回 <code>canvas</code> 的上下文(<code>CanvasRenderingContext2D</code>)，然后通过这个 <code>ctx</code> 来。</p>
<p>在画图方面就主要用到以下几个上下文的属性和方法：</p>
<table>
<thead>
<tr>
<th>名称</th>
<th style="text-align:center">介绍</th>
</tr>
</thead>
<tbody>
<tr>
<td>strokeStyle</td>
<td style="text-align:center">形状描边的颜色</td>
</tr>
<tr>
<td>lineWidth</td>
<td style="text-align:center">线宽</td>
</tr>
<tr>
<td>beginPath()</td>
<td style="text-align:center">创建一个新路径</td>
</tr>
<tr>
<td>moveTo()</td>
<td style="text-align:center">在给定的 (x，y) 坐标处开始一个新的子路径</td>
</tr>
<tr>
<td>lineTo()</td>
<td style="text-align:center">将当前子路径的最后一个点与指定的 (x, y) 坐标用直线段相连</td>
</tr>
<tr>
<td>stroke()</td>
<td style="text-align:center">绘制当前或指定的路径</td>
</tr>
<tr>
<td>getImageData()</td>
<td style="text-align:center">返回一个 ImageData 对象，用于描述 canvas 指定区域的隐含像素数据</td>
</tr>
<tr>
<td>putImageData()</td>
<td style="text-align:center">将数据从已有的 ImageData 对象绘制到画布上</td>
</tr>
<tr>
<td>drawImage()</td>
<td style="text-align:center">在画布（Canvas）上绘制图像的方式</td>
</tr>
</tbody>
</table>
<h2>心电图</h2>
<p>这是个常规的心电图的样子：</p>
<p><img src="https://leetme.netlify.app/images/ecg-basic.webp" alt="心电图示例"></p>
<p>可以看到它的背景是由很多个格子组成的：每个小格代表一个单位，每五个小格组成一个大格。</p>
<p>每个小格代表0.04秒，每个大格代表0.2秒。</p>
<h2>辅助线绘制</h2>
<p>我这里将每个小格按照5px的宽高进行绘制。</p>
<p>通过遍历宽高，将累加值进行 +5 和 +25 绘制大小格：</p>
<pre><code class="language-js">ctx.strokeStyle = '#fecaca'
ctx.lineWidth = 0.5
ctx.beginPath()

// 竖线
for (let x = 0; x &lt;= width; x += 5) {
  ctx.moveTo(x, 0)
  ctx.lineTo(x, height)
}
// 横线
for (let y = 0; y &lt;= height; y += 5) {
  ctx.moveTo(0, y)
  ctx.lineTo(width, y)
}

ctx.stroke()
</code></pre>
<p>就得到了下面的效果：</p>
<p>::: demo<br>
/examples/ecg-grid<br>
:::</p>
<h2>心电图绘制</h2>
<p>::: demo<br>
/examples/ECG<br>
:::</p>
<p>我们先预览下完成之后的效果。</p>
<p>可以看出来这个图的动态效果并不是每次走完长度后又从头（最左边）覆盖重新开始画，而是画到最右边后继续重复画在最右侧。</p>
<p>第一种实现效果我们只需要一个<code>canvas</code>即可：</p>
<ol>
<li>计算初始位置 (0, y / 2)，最左侧中线位置通过<code>moveTo</code>移动到坐标点</li>
<li>根据心电图的特性，每0.2秒画一个大格，我设定每个大格25px</li>
</ol>
<blockquote>
<p>但是我并未完全按照特性去写，因为真正情况下心电图的数据比模拟情况下的数据频率高得多，我是用的数据是模拟1s完成的数据作为一个周期，每0.02s画四条数据，两百条模拟数据刚好1s执行完。</p>
</blockquote>
<ol start="3">
<li>画线，提前算出下一条数据的坐标，通过<code>lineTo</code>画线(算出的坐标只是y轴，x轴是根据格子宽度去固定递增的)，画完之后在下一次执行画之前，<code>moveTo</code>移动到上次画线的坐标</li>
<li>判断x轴递增是否大于<code>canvas</code>宽度，超过就重置x轴位置</li>
</ol>
<p>第二种实现方式则需要两个<code>canvas</code>:</p>
<p>一个bufferCanvas用于绘制心电图，另一个canvas用于绘制背景。主要用到的是绘制心电图的bufferCanvas。</p>
<ol>
<li>每次画都需要先判断x轴是否超出宽度，未超出同样使用上面的方法，超出后需要使用<code>getImageData</code>拿到最后一次画的那段画布，然后通过<code>putImageData</code>将这段画布放到最左侧，同事清除右侧的画布</li>
<li>将bufferCanvas通过<code>drawImage</code>放到canvas上</li>
</ol>
<p>我们主要实现第二种方式：</p>
<h3>绘制逻辑</h3>
<pre><code class="language-vue">&lt;script setup&gt;
const canvasRef = ref(null) // Main canvas
const bufferCanvasRef = ref(null) // Buffer canvas

const beatArray = ref([]) // Buffered data
const indexRef = ref(0) // Data index
const xPos = ref(0) // 起始x轴位置
const endPoint = ref(0) // Last drawn point y-coordinate
const animate = ref(null) // Animation instance
const timer = ref(null) // Timer for generating data

function draw() {
  const canvas = canvasRef.value
  const bufferCanvas = bufferCanvasRef.value
  if (!canvas || !bufferCanvas)
    return

  const ctx = canvas.getContext('2d', { willReadFrequently: true })
  const bufferCtx = bufferCanvas.getContext('2d', { willReadFrequently: true })

  const width = canvas.width
  const height = canvas.height

  const startPoint = height / 2 // 其实y轴位置
  const offset = 10 // 画布最右侧空出的距离用于更好观察

  if (xPos.value &gt;= width - offset) {
    const imageData = bufferCtx.getImageData(1, 0, width - 1 - offset, height)
    bufferCtx.putImageData(imageData, 0, 0)

    bufferCtx.clearRect(width - 1 - offset, 0, 1, height)
  }

  bufferCtx.lineWidth = 2
  bufferCtx.strokeStyle = 'red'

  bufferCtx.beginPath()
  // 超过最大宽度时，只移动offset宽度画在最右侧
  if (xPos.value &gt;= width - offset)
    bufferCtx.moveTo(width - 2 - offset, endPoint.value)
  else bufferCtx.moveTo(xPos.value, endPoint.value)

  // 计算y轴坐标，100是放大倍数，因为数据都是波动很小的小数
  if (beatArray.value.length &gt; 0) {
    endPoint.value = startPoint - beatArray.value[0] * 100
    // 删除已经算出的数据
    beatArray.value.shift()
  }

  if (xPos.value &gt;= width - offset) {
    bufferCtx.lineTo(width - 1 - offset, endPoint.value)
  }
  else {
    bufferCtx.lineTo(xPos.value + 1, endPoint.value)
  }

  bufferCtx.stroke()

  ctx.clearRect(0, 0, width, height)
  ctx.drawImage(bufferCanvas, 0, 0)

  if (xPos.value &lt; width - offset)
    xPos.value += 1 // 每次画移动1px

  // 重复画线
  animate.value = requestAnimationFrame(draw)
}
&lt;/script&gt;
</code></pre>
<p>这样我们的心电图核心逻辑就已经完成了，接下来我们需要放到html中，并且让动动起来。<br>
数据放到文章最底部了。</p>
<pre><code class="language-vue">&lt;script setup&gt;
const canvasRef = ref(null) // Main canvas
const beatArray = ref([]) // Buffered data
const indexRef = ref(0)

function handleResize() {
  const canvas = canvasRef.value
  if (!canvas)
    return

  initBufferCanvas(canvas.width, canvas.height)
}

function generateData() {
  beatArray.value.push(...dataSource.slice(indexRef.value, indexRef.value + 4))
  indexRef.value += 4

  if (indexRef.value &gt;= dataSource.length)
    indexRef.value = 0
}

function initBufferCanvas(width, height) {
  const bufferCanvas = document.createElement('canvas')

  bufferCanvas.width = width
  bufferCanvas.height = height
  bufferCanvasRef.value = bufferCanvas

  endPoint.value = height / 2

  const bufferCtx = bufferCanvas.getContext('2d', { willReadFrequently: true })

  bufferCtx.clearRect(0, 0, width, height)
}

function draw() {}

onMounted(() =&gt; {
  if (!canvasRef.value)
    return

  handleResize()
  window.addEventListener('resize', handleResize)
  excute()
})

onBeforeUnmount(() =&gt; {
  window.removeEventListener('resize', handleResize)
  stopAnimation()
})
&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;div class=&quot;wrapper&quot;&gt;
      &lt;canvas ref=&quot;canvasRef&quot; width=&quot;650&quot; height=&quot;200&quot; /&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;style scoped&gt;
.wrapper {
  position: relative;
}
&lt;/style&gt;
</code></pre>
<p>如果需要添加背景格子的话，只需要在初始化的时候多用一个canvas绘制即可。</p>
<p>::: tip 为什么直接在canvas上画格子<br>
因为这是用两张canvas，在bufferCanvas上是白色的，每次都会把背景格子覆盖掉。<br>
:::</p>
<h3>跟踪的圆点</h3>
<p>我懒得写了哈哈哈哈哈，还是直接看示例里面的完整代码吧。</p>
<pre><code class="language-js">const dataSource = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.01, 0.017, 0.025, 0.032, 0.039, 0.047, 0.059, 0.066, 0.072, 0.079, 0.085, 0.092, 0.1, 0.115, 0.12, 0.115, 0.1, 0.09, 0.072, 0.054, 0.032, 0.014, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -0.015, -0.04, -0.075, -0.1, 0, 0.11, 0.25, 0.36, 0.47, 0.58, 0.69, 0.78, 0.64, 0.48, 0.4, 0.32, 0.26, 0.18, 0.09, 0, -0.05, -0.1, -0.15, -0.2, -0.16, -0.12, -0.08, -0.04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.01, 0.015, 0.02, 0.024, 0.029, 0.033, 0.036, 0.039, 0.042, 0.046, 0.05, 0.055, 0.06, 0.065, 0.07, 0.075, 0.08, 0.085, 0.09, 0.095, 0.1, 0.105, 0.11, 0.115, 0.12, 0.125, 0.13, 0.135, 0.14, 0.145, 0.15, 0.153, 0.158, 0.163, 0.167, 0.161, 0.151, 0.142, 0.133, 0.122, 0.11, 0.09, 0.08, 0.07, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01, 0, 0, 0, 0, 0, 0, 0, 0.012, 0.014, 0.018, 0.023, 0.024, 0.025, 0.024, 0.021, 0.016, 0.013, 0.009, 0.006, 0.003, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
</code></pre>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[CSS 特效]]></title>
            <link>https://leetme.netlify.app/posts/css-effects</link>
            <guid>https://leetme.netlify.app/posts/css-effects</guid>
            <pubDate>Fri, 02 Aug 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>梅花动画特效</h2>
<p>::: demo<br>
/examples/plum<br>
:::</p>
<h2>文字显示动画</h2>
<p>::: demo<br>
/examples/textAnimation<br>
:::</p>
<h2>渐层框线按钮</h2>
<p>::: demo<br>
/examples/gredient-button<br>
:::</p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[实现md自定义块block和代码块组code-group]]></title>
            <link>https://leetme.netlify.app/posts/custom-block-and-code</link>
            <guid>https://leetme.netlify.app/posts/custom-block-and-code</guid>
            <pubDate>Mon, 22 Jul 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<p>先看看block和code-group的效果:</p>
<p>::: info<br>
123 <code>script</code> <a href="https://www.baidu.com">baidu</a><br>
:::</p>
<p>::: tip<br>
123 <code>script</code> <a href="https://www.baidu.com">baidu</a><br>
:::</p>
<p>::: warning<br>
123 <code>script</code> <a href="https://www.baidu.com">baidu</a><br>
:::</p>
<p>::: danger<br>
123 <code>script</code> <a href="https://www.baidu.com">baidu</a><br>
:::</p>
<p>::: details<br>
123 <code>script</code> <a href="https://www.baidu.com">baidu</a><br>
:::</p>
<p>::: code-group</p>
<pre><code class="language-js">const foo = 'foo'
</code></pre>
<pre><code class="language-ts">const foo: string = 'foo'
</code></pre>
<p>:::</p>
<h2>发现</h2>
<p>在 markdown 中无法做到这种功能，我之前的博客是使用 vitepress 写的，vitepress 扩展了 markdown 的语法，其中就包括了 <code>custom-block</code> 和 <code>code-group</code>.</p>
<p>在现在的博客中，我从之前的博客迁移文章过来才发现使用了大量的 vitepress 扩展的 markdown 语法，这就导致如果我要修改得花大量的时间去查找。这样做既没有效率，之后也无法使用这种语法，那就想着能不能在现在的博客去实现一个相同的功能？</p>
<h2>Markdown-It</h2>
<p>在实现之前还需要了解下 <a href="https://github.com/markdown-it/markdown-it">Markdown-It</a>，这是一个 Markdown 解析器，并且支持扩展和语法插件。</p>
<p>Markdown-It 的原来总的来说可以分为两个步骤: <code>parse</code> 和 <code>render</code>，解析和渲染。要实现 custom-block 和 code-group 主要在于 <code>render</code> 过程，我们不太需要关心 <code>parse</code> 的过程，但是还是会用到一点点 <code>parse</code> 的知识。</p>
<p>看看 <a href="https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.mjs#L114"><code>render</code></a>，解析后的内容是一个 tokens，renderer 函数接收 tokens 和其他参数，在这里我们就可以处理得到最后渲染的 html 了。</p>
<p>::: tip tokens<br>
官方对于 <a href="https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md?plain=1#L41">tokens</a> 的解释大概就是:</p>
<p>我们使用更底层的数据表示 -- tokens，而不是传统的AST( 抽象语法树(Abstract Syntax Tree，AST)，是源代码语法结构的一种抽象表示。 以树状的形式表现编程语言的语法结构，每个节点都表示源代码中的一种结构)</p>
<ul>
<li>tokens是一个简单是序列数组</li>
<li>开始和结束标签是分开的</li>
<li>有一些特殊的标记对象，即内联容器，他们具有嵌套标记。这些带有内联标记的序列，例如粗体、斜体、文本等。</li>
</ul>
<p>总的来说，token流是:</p>
<ul>
<li>在顶层一一成对或单个块标记的数组:
<ul>
<li>开始/结束的标题、列表、块引用、段落等。</li>
<li>code blocks, fenced blocks, 水平线，HTML 块，内联容器</li>
</ul>
</li>
<li>每个内联token都有一个children属性，其中包含用于内联内容的嵌套token流:
<ul>
<li>开始/结束的粗体、斜体、链接、内联代码等。</li>
<li>文本，换行符</li>
</ul>
</li>
</ul>
<p>:::</p>
<p>你可以通过 <a href="https://markdown-it.github.io/">markdown-it demo</a> 中的debug来查看内容转换成 token 后是什么样子。</p>
<h2>custom-block</h2>
<h3>实现之前</h3>
<p>ok，你现在有了一点点了解了，应该也知道我们可以怎么下手了：拿到解析后的tokens，筛选出对应的token并且重新渲染它。</p>
<p>然后在 markdown-it demo 中写了这个语法发现渲染后的 HTML 并非和预想中的一样，它会被渲染成 <code>&lt;p&gt;&lt;/p&gt;</code>, 而我需要的是渲染成 <code>&lt;div&gt;&lt;/div&gt;</code> 并且带上一些属性。</p>
<p>那能不能筛选出 token 的 <code>type=&quot;paragraph_open/close&quot; &amp;&amp; tag=&quot;p&quot;</code>，然后查找在这之中 content 包含了 <code>::: tip :::</code> 的token，然后再处理？想了想后要从非常庞大的tokens中去逐一查找就不太可取，而且从content查找关键字，那我这样写的文字（abcd::: tip :::efg）那也能会匹配上。</p>
<p>其实还需要另外一个库 <a href="https://github.com/markdown-it/markdown-it-container">markdown-it-container</a>，它已经帮我们处理好并筛选出包含关键字的token了。</p>
<pre><code>var md = require('markdown-it')()
            .use(require('markdown-it-container'), name [, options]);
</code></pre>
<p>通过 name 定义 ::: 后的关键字，然后 使用 options 中的 render 去渲染。详细的文档可以去它的仓库查看。</p>
<p>首先下载需要使用到的库:</p>
<pre><code class="language-shell">pnpm install markdown-it markdown-it-container -D
</code></pre>
<h3>createContainer</h3>
<pre><code class="language-ts">import type MarkdownIt from 'markdown-it'
import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
import container from 'markdown-it-container'

type ContainerArgs = [typeof container, string, { render: RenderRule }]

function createContainer(
  klass: string,
  defaultTitle: string,
  md: MarkdownIt
): ContainerArgs {
  return [
    container,
    klass,
    {
      render() {
        return ''
      }
    }
  ]
}
</code></pre>
<p>编写一个通用的 container，之后我们就能在使用插件时直接使用:</p>
<pre><code class="language-ts">md.use(...createContainer('tip', 'TIP', md))
</code></pre>
<h3>render</h3>
<p>在上面我们已经知道了 Markdown-It 的大概原理，在这里我们就是需要通过 render 来渲染。</p>
<p>由于 Markdown-It-container 已经帮我们处理过了 tokens, 所以我们 tokens[idx] 所得到的就只会是 <code>:::</code> 开头结尾的段落。</p>
<p>::: details 被container解析后的token</p>
<pre><code class="language-js">Token = {
  type: 'container_tip_open',
  tag: 'div',
  attrs: null,
  map: [158, 169],
  nesting: 1,
  level: 0,
  children: null,
  content: '',
  markup: ':::',
  info: ' tip',
  meta: null,
  block: true,
  hidden: false
}
Token = {
  type: 'container_tip_close',
  tag: 'div',
  attrs: null,
  map: null,
  nesting: -1,
  level: 0,
  children: null,
  content: '',
  markup: ':::',
  info: '',
  meta: null,
  block: true,
  hidden: false
}
</code></pre>
<p>可以看出被解析后并且赋予了 <code>type</code>，并将 <code>info</code> 中的 <code>:::</code> 解析到 <code>markup</code> 中，而 <code>info</code> 为 <code>:::</code> 后跟的文本。</p>
<p>另外<code>nesting</code>这个字段代表标签的类型，后面会用到:</p>
<ul>
<li>1 代表标签的开始</li>
<li>-1 代表标签的闭合</li>
<li>0 代表自闭合标签</li>
</ul>
<p>:::</p>
<pre><code class="language-ts">function createContainer(
  klass: string,
  defaultTitle: string,
  md: MarkdownIt
): ContainerArgs {
  return [
    container,
    klass,
    {
      /**
       * tokens 为解析的所有的标签
       * idx 为 markdown-it-container 解析后的包含 ::: 的索引
       * _options 为创建新的markdown-it对象时定义的选项
       * env 可以和 tokens 一起使用，将外部变量注入到解析器和渲染器中
       */
      render(tokens, idx, _options, env: { references?: any }) {
        // 拿到 `:::` 的token
        const token = tokens[idx]
        // 解析 token 中 info 的文本，slice的作用是文本后面还可能有其他文本，这里截取掉前面固有的关键字，获得后面的文本，如果没有则是''
        const info = token.info.trim().slice(klass.length).trim()
        // 获取标签的属性
        const attrs = md.renderAttrs(token)
        /**
         * 判断是否标签开始，否则一定为标签结束，不可能为自闭合标签，因为渲染的结果为div。
         * 获取title，就是 ::: 加上关键字后面的部分，还需看是否有链接引用
         * 如果是details则为可展开的标签`&lt;details&gt;&lt;summary&gt;&lt;/summary&gt;&lt;/details&gt;`
         */
        if (token.nesting === 1) {
          const title = md.renderInline(info || defailtTitle, {
            references: env.references,
          })

          if (klass === 'details') {
            return `&lt;details class=&quot;${klass} custom-block&quot;${attrs}&gt;&lt;summary&gt;${title}&lt;/summary&gt;\n`
          }
          return `&lt;div class=&quot;${klass} custom-block&quot;${attrs}&gt;&lt;p class=&quot;custom-block-title&quot;&gt;${title}&lt;/p&gt;\n`
        }
        else {
          return klass === 'details' ? '&lt;/details&gt;\n' : '&lt;/div&gt;\n'
        }
      }
    }
  ]
}
</code></pre>
<p>::: tip<br>
<code>env</code> 用于在分布式规则之间传递数据并返回附加的渲染器所需的 metadata, 例如reference。它也可以用来在特定情况下注入数据。通常，你可以通过 <code>{}</code> 空对象，然后将更新后的对象传递给渲染器。</p>
<p><code>references</code> 在 MarkdownIt 渲染过程中用于存储和查找 Markdown 文档中的引用链接信息。它会使用 <code>references</code> 对象中的数据来生成正确的链接。</p>
<pre><code class="language-md">[link text][ref]

[ref]: http://example.com 'Optional Title'
</code></pre>
<p>:::</p>
<h3>使用</h3>
<p>这时核心的逻辑就已经写好了，接下来就是使用了。如果你是直接使用的 markdown-it 库，那就是直接使用 <code>MarkdownIt.use(...createContainer('tip', 'TIP', md))</code> 即可。</p>
<p>我是使用了 <code>unplugin-vue-markdown</code> 在 vue 中可以使用 markdown 当作页面:</p>
<pre><code class="language-ts">import Markdown from 'unplugin-vue-markdown/vite'

export default defineConfig({
  plugins: [
    Markdown({
      async markdownItSetup(md) {
        md.use(...createContainer('tip', 'TIP', md))
        md.use(...createContainer('warning', 'WARNING', md))
        md.use(...createContainer('danger', 'DANGER', md))
        md.use(...createContainer('info', 'INFO', md))
        md.use(...createContainer('details', 'Details', md))
      }
    })
  ]
})
</code></pre>
<p>这样引入五次比较麻烦，并且写死了title，优化一下会更灵活:</p>
<p>::: code-group</p>
<pre><code class="language-ts">// 定义一个ContainerOptions
export interface ContainerOptions {
  infoLabel?: string
  tipLabel?: string
  warningLabel?: string
  dangerLabel?: string
  detailsLabel?: string
}

// 创建一个plugin方法
export function containerPlugin(
  md: MarkdownIt,
  options: Options,
  containerOptions?: ContainerOptions
) {
  md.use(...createContainer('tip', containerOptions?.tipLabel || 'TIP', md))
  md.use(...createContainer('warning', containerOptions?.warningLabel || 'WARNING', md))
  md.use(...createContainer('danger', containerOptions?.dangerLabel || 'DANGER', md))
  md.use(...createContainer('info', containerOptions?.infoLabel || 'INFO', md))
  md.use(...createContainer('details', containerOptions?.detailsLabel || 'Details', md))
}
</code></pre>
<pre><code class="language-ts">export default defineConfig({
  plugins: [
    Markdown({
      async markdownItSetup(md) {
        md.use(containerPlugin)
      }
    })
  ]
})
</code></pre>
<p>:::</p>
<h3>样式</h3>
<p>这时候算是已经完成了80%了，你能看到没有自定义样式的这些 custom-block 了，接下来就要加上样式。</p>
<blockquote>
<p>[!WARNING]<br>
这是我自己定义的样式，如果你想自定义样式，请自行修改；前提要求你也经给 markdown 设置过样式，否则可能样式会有问题。</p>
</blockquote>
<pre><code class="language-css">:root {
  --c-text: inherit;
  --c-code: rgba(59, 130, 246, 0.72);
  /* bg */
  --c-info-bg: rgba(107, 114, 128, 0.1);
  --c-tip-bg: rgba(34, 197, 94, 0.08);
  --c-warning-bg: rgba(234, 179, 8, 0.1);
  --c-details-bg: rgba(107, 114, 128, 0.1);
  --c-danger-bg: rgba(239, 68, 68, 0.08);
  /* text */
}

html.dark {
  /* bg */
  --c-info-bg: rgba(107, 114, 128, 0.24);
  --c-tip-bg: rgba(34, 197, 94, 0.1);
  --c-warning-bg: rgba(234, 179, 8, 0.12);
  --c-details-bg: rgba(107, 114, 128, 0.24);
  --c-danger-bg: rgba(239, 68, 68, 0.12);
}

.custom-block {
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 16px 16px 8px;
  margin: 24px 0;
  line-height: 24px;
  font-size: 14px;
  color: var(--c-text);
}

.custom-block.info {
  background-color: var(--c-info-bg);
}

.custom-block.info a,
.custom-block.info code {
  color: var(--c-code);
}

.custom-block.info a:hover,
.custom-block.info a:hover &gt; code {
  color: var(--c-text);
}

.custom-block.tip {
  background-color: var(--c-tip-bg);
}

.custom-block.tip a,
.custom-block.tip code {
  color: var(--c-code);
}

.custom-block.tip a:hover,
.custom-block.tip a:hover &gt; code {
  color: var(--c-text);
}

.custom-block.warning {
  background-color: var(--c-warning-bg);
}

.custom-block.warning a,
.custom-block.warning code {
  color: var(--c-code);
}

.custom-block.warning a:hover,
.custom-block.warning a:hover &gt; code {
  color: var(--c-text);
}

.custom-block.danger {
  background-color: var(--c-danger-bg);
}

.custom-block.danger a,
.custom-block.danger code {
  color: var(--c-code);
}

.custom-block.danger a:hover,
.custom-block.danger a:hover &gt; code {
  color: var(--c-text);
}

.custom-block.details {
  background-color: var(--c-details-bg);
}

.custom-block.details a,
.custom-block.details code {
  color: var(--c-code);
}

.custom-block.details a:hover,
.custom-block.details a:hover &gt; code {
  color: var(--c-text);
}

.custom-block-title {
  font-weight: 600;
}

.custom-block p + p {
  margin: 8px 0;
}

.custom-block.details summary {
  margin: 0 0 8px;
  font-weight: 700;
  cursor: pointer;
  user-select: none;
}

.custom-block.details summary + p {
  margin: 8px 0;
}

.custom-block a {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: opacity 0.25s;
}

.custom-block a:hover {
  opacity: 0.75;
}

.custom-block code {
  font-size: 14px;
}

.custom-block.custom-block th,
.custom-block.custom-block blockquote &gt; p {
  font-size: 14px;
  color: inherit;
}
</code></pre>
<h2>code-group</h2>
<h3>实现之前</h3>
<p>首先下载需要使用到的库:</p>
<pre><code class="language-shell">pnpm install nanoid -D
</code></pre>
<p>那这个应该怎么实现呢？看效果我们知道它是一个切换的tabs，使用dom操作实现在这里貌似不太现实，那如何使用纯css去实现tabs呢？</p>
<p>我们可以通过 <code>radio</code> 的特性配合 <code>label</code> 来控制，点击 label 时，对应的代码块显示。你可以自己试试怎么实现tabs。不要忘记css中的伪元素选择器，通过判断<code>input:checked</code>状态去处理。</p>
<p>那么现在就该想想怎样去渲染HTML了，原本的两段代码块渲染后会是这样的:</p>
<pre><code class="language-html">&lt;p&gt;::: code-group&lt;/p&gt;

&lt;pre&gt;
  &lt;code&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;pre&gt;
  &lt;code&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;:::&lt;/p&gt;
</code></pre>
<p>我们现在需要的HTML大概是这样的，还需要识别代码块后面的文本来渲染成label:</p>
<pre><code class="language-html">&lt;div class=&quot;code-group&quot;&gt;
  &lt;div class=&quot;tabs&quot;&gt;
    &lt;input type=&quot;radio&quot; id=&quot;&quot; checked /&gt;
    &lt;label for=&quot;&quot;&gt;&lt;/label&gt;
    &lt;input type=&quot;radio&quot; id=&quot;&quot;/&gt;
    &lt;label for=&quot;&quot;&gt;&lt;/label&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
    &lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>知道需要渲染的内容后，思路就清晰多了。同样的我们需要获取<code>type === 'container_code-group_start'</code> 和 <code>type === container_code-group_close</code> 来处理标签的开始和结束:</p>
<pre><code class="language-html">&lt;!-- 开始标签 --&gt;
&lt;div class=&quot;code-group&quot;&gt;
  &lt;div class=&quot;tabs&quot;&gt;
    &lt;input type=&quot;radio&quot; id=&quot;&quot; checked /&gt;
    &lt;label for=&quot;&quot;&gt;&lt;/label&gt;
    &lt;input type=&quot;radio&quot; id=&quot;&quot;/&gt;
    &lt;label for=&quot;&quot;&gt;&lt;/label&gt;
  &lt;/div&gt;
  &lt;div&gt;

&lt;!-- 代码块部分 --&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;!-- 结束标签 --&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>在上面所了解到的 <code>token</code> 的结构，我们从 <code>markdown-it demo</code>可以知道代码块部分的 <code>type</code> 都是 <code>fence</code>，我们需要拿到代码块定义的标题，就需要知道 <code>type === fence</code>。</p>
<p>那么就开始实现吧。</p>
<h3>createCodeGroup</h3>
<p>code-group 和 custom-block 类似，固定了 name 为 <code>code-group</code>，并且 render 的逻辑也不相同。</p>
<pre><code class="language-ts">function createCodeGroup(): ContainerArgs {
  return [
    container,
    'code-group',
    {
      render() {
        return ''
      }
    }
  ]
}
</code></pre>
<h3>render</h3>
<pre><code class="language-ts">function createCodeGroup(): ContainerArgs {
  return [
    container,
    'code-group',
    {
      render(tokens, idx) {
        // 以防忘记，这里判断的是带有 code-group 的 token 的标签是否是开始标签，而不是判断所有的 tokens
        if (tokens[idx].nesting === 1) {
          const name = nanoid(5) // radio 唯一 name，才能实现单选
          const tabs = '' // tabs html
          const checked = 'checked'

          /**
           * 这里除了要处理渲染开始和结束标签的HTML,还要拿到其中代码片段的 title
           * 这里的循环时查找 code-group 之内的其他标签
           */
          for (
            let i = idx + 1;
            !(
              tokens[i].nesting === -1
              &amp;&amp; tokens[i].type === 'container_code-group_close'
            );
            i++
          ) {
            // 兼容在md中直接使用 &lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt; 编写代码块，并包含属性data-title=&quot;&quot;，那么也可以识别出来
            const isHtml = tokens[i].type === 'html_block'

            if ((tokens[i].type === 'fence' &amp;&amp; tokens[i].tag === 'code') || isHtml) {
              // 获取 title
              const title = extractTitle(isHtml ? tokens[i].content : tokens[i].info, isHtml)

              if (title) {
                const id = nanoid(7) // radio 中 id 和 label 中 for 对应
                tabs += `&lt;input type=&quot;radio&quot; name=&quot;group-${name}&quot; id=&quot;tab-${id}&quot; ${checked}&gt;&lt;label for=&quot;tab-${id}&quot;&gt;${title}&lt;/label&gt;`

                // 给第一个代码块 token.info 加上 active 属性
                if (checked &amp;&amp; !isHtml)
                  tokens[i].info += ' active'
                checked = ''
              }
            }
          }

          return `&lt;div class=&quot;code-group&quot;&gt;&lt;div class=&quot;tabs&quot;&gt;${tabs}&lt;/div&gt;&lt;div class=&quot;blocks&quot;&gt;\n`
        }
        return `&lt;/div&gt;&lt;/div&gt;\n`
      }
    }
  ]
}
</code></pre>
<p>::: details render中使用到的工具</p>
<pre><code class="language-ts">/**
 * 去除块内注释并提取data-title属性值
 */
export function extractTitle(info: string, html = false) {
  if (html) {
    return (
      info.replace(/&lt;!--[\s\S]*?--&gt;/g, '').match(/data-title=&quot;(.*?)&quot;/)?.[1] || ''
    )
  }
  return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt'
}
</code></pre>
<pre><code class="language-ts">/**
 * 提取代码块的语言，```js = js
 */
export function extractLang(info: string) {
  return info
    .trim()
    .replace(/=(\d*)/, '')
    // eslint-disable-next-line regexp/optimal-quantifier-concatenation
    .replace(/:(no-)?line-numbers(\{| |$|=\d*).*/, '')
    .replace(/(-vue|\{| ).*$/, '')
    .replace(/^vue-html$/, 'template')
    .replace(/^ansi$/, '')
}
</code></pre>
<p>:::</p>
<h3>使用</h3>
<p>在containerPlugin中直接加上 <code>md.use(...createCodeGroup())</code> 即可。</p>
<p>然后我们测试一下，会发现虽然HTML结果没问题，但是功能有问题。虽然没有样式，但是我们也能发现问题，看看HTML你会发现，我们在代码中加上的 <code>active</code> 只是在 token 中加上而已，并没有使用到。我们需要 <code>active</code> 来控制代码块的显隐。</p>
<p>那能不能不通过 <code>active</code> 来控制呢。实现tabs确实可以，通过给 <code>tab panel</code> 也设置特定的属性值，然后通过属性选择器去控制。但是怎么给它加上属性值呢？好像没有办法，<code>markdown-it-container</code> 只返回开始和结束的标签。</p>
<p>在 <code>markdown-it</code> 代码中的 <a href="https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.mjs#L4">renderer.mjs描述</a>, 可以知道：从解析的 token 流 生成HTML。每个实例都有独立的 rules 副本。这些可以轻松重写。此外，如果您创建插件并添加新的令牌类型，则可以添加新的 rules。</p>
<p>现在我们知道我们可以编写一个新的插件来处理这个情况。因为都是代码块，所以它的 token 流的 <code>type === 'fence'</code>。</p>
<h3>处理代码块的 active</h3>
<p>可以直接把 <code>active</code> 加在代码块 <code>&lt;pre class=&quot;active&quot;&gt;&lt;/pre&gt;</code> 上，但是我选择用一层 <code>div</code> 包裹，显得层次清晰一点，更能看清修改痕迹。并且需要加上其他属性也不会混乱。</p>
<p>我们新建一个插件:</p>
<pre><code class="language-ts">export function preWrapperPlugin(md: MarkdownIt) {
  // fence本身就是render，但是我们需要重写它
  const fence = md.renderer.rules.fence!

  md.renderer.rules.fence = (...args) =&gt; {
    const [tokens, idx] = args
    // 拿到所有 fence 的 token
    const token = tokens[idx]

    // 移除代码块定义的 title，eg: [index.js]会被整个移除
    token.info = token.info.replace(/\[.*\]/, '')

    // 判断 info 中是否有 `active`
    // eslint-disable-next-line regexp/no-unused-capturing-group
    const active = / active( |$)/.test(token.info) ? ' active' : ''

    // 移除 active
    token.info = token.info.replace(/ active$/, '').replace(/ active /, ' ')

    // 获取定义代码块的语言, 这个方法在上面有定义过 `render中使用到的工具`
    const lang = extractLang(token.info)

    // 自定义包裹后渲染原来的 fence即可
    return (
      `&lt;div class=&quot;language-${lang}${active}&quot;&gt;${fence(...args)}&lt;/div&gt;`
    )
  }
}
</code></pre>
<p>在 <code>vite.config.ts</code> 中使用这个插件:</p>
<pre><code class="language-ts">export default defineConfig({
  plugins: [
    Markdown({
      async markdownItSetup(md) {
        md.use(preWrapperPlugin)

        // container
        md.use(containerPlugin)
      }
    })
  ]
})
</code></pre>
<p>现在再看看HTML，它已经正确显示了，接下来我们只需要加上样式就可以了。</p>
<h3>样式</h3>
<blockquote>
<p>[!WARNING]<br>
这是我自己定义的样式，如果你想自定义样式，请自行修改；前提要求你也经给 markdown 设置过样式，否则可能样式会有问题。</p>
</blockquote>
<pre><code class="language-css">.code-group {
  margin-top: 16px;
}

.code-group .tabs {
  position: relative;
  display: flex;
  padding: 0 12px;
  background-color: #fafafa;
  overflow-x: auto;
  overflow-y: hidden;
  box-shadow: inset 0 -1px #ffffff;
  border-radius: 6px 6px 0 0;
}

html.dark .code-group .tabs {
  background-color: #0e0e0e;
  box-shadow: inset 0 -1px #000000;
}

.code-group .tabs input {
  position: fixed;
  opacity: 0;
  pointer-events: none;
}

.code-group .tabs label {
  position: relative;
  display: inline-block;
  border-bottom: 1px solid transparent;
  padding: 0 12px;
  line-height: 48px;
  font-size: 14px;
  font-weight: 500;
  color: inherit;
  opacity: 0.6;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.25s;
}

.code-group .tabs label::after {
  position: absolute;
  right: 8px;
  bottom: -1px;
  left: 8px;
  z-index: 1;
  height: 2px;
  border-radius: 2px;
  content: '';
  background-color: transparent;
  transition: background-color 0.25s;
}

.code-group label:hover {
  opacity: 1;
}

.code-group input:checked + label {
  opacity: 1;
}

.code-group input:checked + label::after {
  background: #000000;
  opacity: 0.6;
}

html.dark .code-group input:checked + label::after {
  background: #ffffff;
}

.code-group div[class*='language-'] {
  display: none;
  margin-top: 0 !important;
  border-top-left-radius: 0 !important;
  border-top-right-radius: 0 !important;
}

.code-group div[class*='language-'].active {
  display: block;
}
</code></pre>
<h3>切换 tab 失效</h3>
<p>这时候加上样式，会发现点击 tab 还是无法切换，因为之前只是默认给第一个 tab 加上了 active，但是没有处理切换时 active 也切换的逻辑。</p>
<p>我们需要处理监听点击 tab 的逻辑:</p>
<pre><code class="language-ts">export function useCodeGroups() {
  const initializeCodeGroups = () =&gt; {
    document.querySelectorAll('.code-group &gt; .blocks').forEach((el) =&gt; {
      Array.from(el.children).forEach((child) =&gt; {
        child.classList.remove('active')
      })
      el.children[0]?.classList.add('active')
    })
  }

  onMounted(() =&gt; {
    if (import.meta.env.DEV) {
      initializeCodeGroups()
    }

    if (typeof window !== 'undefined') {
      window.addEventListener('click', (e) =&gt; {
        const el = e.target as HTMLInputElement

        if (el.matches('.code-group input')) {
          const group = el.parentElement?.parentElement
          if (!group)
            return

          // 获取点击的 tab 索引
          const i = Array.from(group.querySelectorAll('input')).indexOf(el)
          if (i &lt; 0)
            return

          const blocks = group.querySelector('.blocks')
          if (!blocks)
            return

          const current = Array.from(blocks.children).find(child =&gt;
            child.classList.contains('active'),
          )
          if (!current)
            return

          // 获取点击的 tab 对应的代码块
          const next = blocks.children[i]
          if (!next || current === next)
            return

          current.classList.remove('active')
          next.classList.add('active')

          const label = group.querySelector(`label[for=&quot;${el.id}&quot;]`)
          label?.scrollIntoView({ block: 'nearest' })
        }
      })
    }
  })

  onUpdated(() =&gt; {
    if (import.meta.env.DEV) {
      initializeCodeGroups()
    }
  })
}
</code></pre>
<p>最后在 App 中使用即可：</p>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
useCodeGroups()
&lt;/script&gt;
</code></pre>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[安装sharp v0.32.6导致的问题]]></title>
            <link>https://leetme.netlify.app/posts/sharp-install-failed</link>
            <guid>https://leetme.netlify.app/posts/sharp-install-failed</guid>
            <pubDate>Mon, 15 Jul 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>问题发现</h2>
<p>之前看到了<a href="https://antfu.me/">antfu.me</a>这个博客，很符合我对博客心目中的样子。看了下仓库，发现代码是MIT协议后，我决定要弄一个这样的博客，并改成对自己来说更好的样子。前两天心血来潮果断拉取了项目代码就开始了一顿操作。</p>
<p>然而我今天在公司也想 pull 来看看，却发现 <code>pnpm install</code> 后有报错:</p>
<img src="https://leetme.netlify.app/images/sharp-install-failed.png" />
<p>图中显示 sharp 包所需要的 <code>sharp-libvips</code> 安装不了。sharp版本为0.32.6</p>
<h2>解决问题</h2>
<p>我来到 github 查看了<code>sharp</code>仓库，这是一个高性能 Node.js 图像处理，调整 JPEG、PNG、WebP、AVIF 和 TIFF 图像大小的最快模块。使用 libvips 库。它是基于<code>libvips</code>的，所有我又找到了<code>sharp-libvips</code>这个库。有了一点点了解后我开始利用搜索引擎查找<code>安装sharp失败</code>，得到的结果有：</p>
<ul>
<li>npm，单独给sharp和sharp-libvips设置源</li>
<li>npm，手动下载sharp-libvips包，然后放到npm cache中</li>
<li>npm，让我<code>npm i -g windows-build-tools</code>，没发现有用</li>
</ul>
<p>得到的其他结果都是使用npm，我使用的是pnpm，也试过了这些方法都没有解决。</p>
<p>于是回到我自己电脑上看能跑起来的博客，在node_modules看了sharp文件夹，和现在下载失败的sharp文件对比了一下，刚好就少了<code>sharp-libvips</code>包和通过<code>sharp-libvips</code>打包后的产物<code>build</code>。</p>
<pre><code>
├── noed_modules
│ └── sharp
│ ├── build
│ │ └── Release
│ ├── vendor
│ │ └── 8.14.5
│ │ └── win32-x64

</code></pre>
<p>我就想着能不能下载这个包，我从上面错误信息中下载了这个包 <a href="https://github.com/lovell/sharp-libvips/releases/download/v8.14.5/libvips-8.14.5-win32-x64.tar.br">sharp-libvips</a>，我却发现解压不了，顺便怀疑了是不是下载依赖无法解压这个tar.br包，我来到其github仓库，找到release找到对应版本下载了tar.gz的压缩包，解压后得到的产物和自己电脑上的相同，于是我就粘贴到<code>win320x64</code>文件夹下了。</p>
<p>再次<code>pnpm install</code>, 上次的错误消失了，但是有了新的报错:</p>
<pre><code>
PS E:\personal\leet.me&gt; pnpm install
Lockfile is up to date, resolution step is skipped
Packages: +763
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 763, reused 763, downloaded 0, added 0, done
node_modules/.pnpm/sharp@0.32.6/node_modules/sharp: Running install script, failed in 11.6s
.../sharp@0.32.6/node_modules/sharp install$ (node install/libvips &amp;&amp; node install/dll-copy &amp;&amp; prebuild-install) || (node install/can-compile &amp;&amp; node-gyp rebuild &amp;&amp; node install/dll-copy)
│ sharp: Using existing vendored libvips v8.14.5
│ sharp: Creating E:\personal\leet.me\node_modules\.pnpm\sharp@0.32.6\node_modules\sharp\build\Release
│ sharp: Copying DLLs from E:\personal\leet.me\node_modules\.pnpm\sharp@0.32.6\node_modules\sharp\vendor\8.14.5\win32-x64\lib to E:\personal\leet.me\node_modules\.pnpm\sharp@0.32.6\node_modules\sharp\build\Rel…
│ prebuild-install warn install aborted
│ E:\personal\leet.me\node_modules\.pnpm\sharp@0.32.6\node_modules\sharp&gt;if not defined npm_config_node_gyp (node &quot;C:\Users\Admin\AppData\Roaming\npm\node_modules\pnpm\dist\node-gyp-bin\\..\node_modules\node-g…
│ gyp info it worked if it ends with ok
│ gyp info using node-gyp@9.4.1
│ gyp info using node@20.10.0 | win32 | x64
│ gyp ERR! find Python
│ gyp ERR! find Python Python is not set from command line or npm configuration
│ gyp ERR! find Python Python is not set from environment variable PYTHON
│ gyp ERR! find Python checking if &quot;python3&quot; can be used
│ gyp ERR! find Python - &quot;python3&quot; is not in PATH or produced an error
│ gyp ERR! find Python checking if &quot;python&quot; can be used
│ gyp ERR! find Python - &quot;python&quot; is not in PATH or produced an error
│ gyp ERR! find Python checking if Python is

...

</code></pre>
<p>它要进行打包生成build中的产物，在这里出错了，应该是需要python环境才能进行打包。我看了自己电脑上确实有python，所以我就安装了一个python 3.12。再次<code>pnpm install</code>，还是同样的错误，原来是忘了设置pnpm的python路径，可以在系统搜索到后打开文件所在位置就能得到路径。</p>
<pre><code>
pnpm config set python &lt;python路径&gt;

</code></pre>
<p>再次<code>pnpm install</code>，这次发现依赖已经下载成功了。我以为已经解决问题了，于是<code>pnpm dev</code>启动项目时又有报错:</p>
<img src="/images/sharp-start-failed.png" />
<p>我用了提示中的命令都不行。</p>
<pre><code>
npm install --ignore-scripts=false --foreground-scripts --verbose sharp

npm install --platform=win32 --arch=x64 sharp

</code></pre>
<p>后面我又去对比了下我的电脑和现在电脑的build的文件，发现少了三个文件，只有最后两个文件。</p>
<pre><code>
├── build
│ └── Release
│ ├── libglib-2.0-0.dll
│ │── libgobject-2.0-0.dll
│ ├── libvips-42.dll
│ │── libvips-cpp.dll
│ │── sharp-win32-x64.node

</code></pre>
<p>于是我就把另外三个文件粘贴过来，再次<code>pnpm dev</code>，启动成功！</p>
<h2>最后解决办法</h2>
<p>但是这种方法使用起来太局限了。</p>
<p>最终还是看到这两条issue：</p>
<p><a href="https://github.com/lovell/sharp/issues/3921">https://github.com/lovell/sharp/issues/3921</a></p>
<p><a href="https://github.com/lovell/sharp/issues/3922">https://github.com/lovell/sharp/issues/3922</a></p>
<p>作者是建议升级到最新版本的0.33.1。确实安装最新版本没有问题了。</p>
<p>查了这么久的问题，没想到升个级就可以了，在你不知道这个库以及升级之后会有什么影响时尽量不要升级版本。</p>
<p>sharp主要就是利用sharp-libvips打包出来的module给sharp使用，刚刚碰到的问题基本上来源于这个根本的。</p>
<pre><code>
</code></pre>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[统一代码风格和规范项目代码 - 新]]></title>
            <link>https://leetme.netlify.app/posts/code-style-standard-new</link>
            <guid>https://leetme.netlify.app/posts/code-style-standard-new</guid>
            <pubDate>Sun, 28 Apr 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<p>上一篇<a href="/workflow/code-style-standard.md">规范代码的文章</a>，由于安装需要用到的库版本比较新，可能会导致旧版的库的一些功能的废弃等等问题，比如在规范 stylelint 时因为 v15 版本原因废弃了<code>stylelint-config-prettier</code>插件；还有在 <code>eslint: ^9.0.0</code> 之前的版本，使用的文件格式一般都是 <code>.xxxrc.{js,cjs,mjs}</code>，在 eslint v9 中官方已经明确废弃了这种写法，改成使用<code>eslint.config.js</code>。所以还是更建议阅读这篇文章。</p>
<blockquote>
<p>[!TIP]<br>
如果你是修改以前项目的提交规范和代码规范，请注意安装的库版本！</p>
</blockquote>
<p>所以请注意你安装的三方插件的版本，没注意可能会导致一些难以察觉的 bug。</p>
<p>另外最近我比较关注前端开源人<a href="https://antfu.me/"><code>Anthony Fu</code></a>，他不仅是<code>Vue</code>, <code>Nuxt</code>, <code>Vite</code>团队核心成员，而且我们使用的很多在工作中用到的工具都是出自他的手中，例如:<br>
<code>Vitese</code>, <code>Slidev</code>, <code>VueUse</code>, <code>UnoCSS</code>。而且他还有很多用于我们便捷开发使用的工具: 通用模板<code>Vitesse</code>系列, <code>@antfu/eslint-config</code>, 开发调试工具等等。</p>
<p>这期我们就要使用便于我们规范代码的工具<a href="https://github.com/antfu/eslint-config"><code>@antfu/eslint-config</code></a>，并且我并不打算使用<code>prettier</code>，我很赞同他这篇文章所说的<a href="https://antfu.me/posts/why-not-prettier-zh">为什么我不使用 Prettier</a>。</p>
<h2>介绍</h2>
<p>其实想重复使用一个通用的模板完全可以使用<a href="https://github.com/antfu-collective/vitesse"><code>Vitesse</code></a>系列产品的。但是还是会有人更喜欢自己从零开始、不使用现成的、完全自定义的创建一个模板。</p>
<h2>创建项目</h2>
<p>::: code-group</p>
<pre><code class="language-bash">pnpm create vite my-app --template vue-ts
</code></pre>
<pre><code class="language-bash">npm create vite my-app -- --template vue-ts
</code></pre>
<pre><code class="language-bash">yarn create vite my-app -- --template vue-ts
</code></pre>
<p>:::</p>
<img src="https://leetme.netlify.app/images/init-app.png" />
<pre><code class="language-bash"># 进入文件夹
cd my-app

# 打开编辑器
code .

# 安装依赖
pnpm install

# 启动项目
pnpm dev
</code></pre>
<h2>ESLint</h2>
<blockquote>
<p>[!TIP]<br>
建议使用 <code>v9.0.0</code> 及以上版本。</p>
</blockquote>
<pre><code class="language-bash">pnpm i -D eslint @antfu/eslint-config
</code></pre>
<p>在你的项目中新建<code>eslint.config.{js,mjs,cjs}</code>:</p>
<pre><code class="language-js">// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu({
  // 如果项目使用了unocss，添加以下配置能格式化class
  // {
  //   unocss: true,
  //   formatters: true,
  // },
})
</code></pre>
<p>::: tip<br>
集成旧配置: 如果您仍然使用旧版 eslintrc 格式中的一些配置，则可以使用 @eslint/eslintrc 包将它们转换为平面配置。</p>
<pre><code class="language-js">import antfu from '@antfu/eslint-config'
import { FlatCompat } from '@eslint/eslintrc'

const compat = new FlatCompat()

export default antfu(
  {
    ignores: []
  },
  // Legacy config
  ...compat.config({
    extends: [
      'eslint:recommended'
      // Other extends...
    ]
  })

  // other flat configs
)
</code></pre>
<p>:::</p>
<p>添加脚本配置:</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;lint&quot;: &quot;eslint .&quot;,
    &quot;lint:fix&quot;: &quot;eslint . --fix&quot;
  }
}
</code></pre>
<p>::: details 推荐使用vscode配置<br>
VS Code 配置，在<code>.vscode/settings.json</code>中添加</p>
<pre><code class="language-json">{
  // Enable the ESlint flat config support
  &quot;eslint.experimental.useFlatConfig&quot;: true,

  // Disable the default formatter, use eslint instead
  &quot;prettier.enable&quot;: false,
  &quot;editor.formatOnSave&quot;: false,

  // Auto fix
  &quot;editor.codeActionsOnSave&quot;: {
    &quot;source.fixAll.eslint&quot;: &quot;explicit&quot;,
    &quot;source.organizeImports&quot;: &quot;never&quot;
  },

  // Silent the stylistic rules in you IDE, but still auto fix them
  &quot;eslint.rules.customizations&quot;: [
    { &quot;rule&quot;: &quot;style/*&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;format/*&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;*-indent&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;*-spacing&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;*-spaces&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;*-order&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;*-dangle&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;*-newline&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;*quotes&quot;, &quot;severity&quot;: &quot;off&quot; },
    { &quot;rule&quot;: &quot;*semi&quot;, &quot;severity&quot;: &quot;off&quot; }
  ],

  // Enable eslint for all supported languages
  &quot;eslint.validate&quot;: [
    &quot;javascript&quot;,
    &quot;javascriptreact&quot;,
    &quot;typescript&quot;,
    &quot;typescriptreact&quot;,
    &quot;vue&quot;,
    &quot;html&quot;,
    &quot;markdown&quot;,
    &quot;json&quot;,
    &quot;jsonc&quot;,
    &quot;yaml&quot;,
    &quot;toml&quot;,
    &quot;gql&quot;,
    &quot;graphql&quot;
  ]
}
</code></pre>
<p>:::</p>
<h2>husky</h2>
<ol>
<li>安装</li>
</ol>
<pre><code class="language-bash">pnpm i -D husky
</code></pre>
<ol start="2">
<li>添加脚本命令并运行</li>
</ol>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;prepare&quot;: &quot;husky&quot;
  }
}
</code></pre>
<pre><code class="language-shell">pnpm prepare
</code></pre>
<ol start="3">
<li>配置文件</li>
</ol>
<pre><code class="language-bash">echo &quot;pnpm lint:fix&quot; &gt; .husky/pre-commit
</code></pre>
<h2>lint-staged</h2>
<ol>
<li>安装</li>
</ol>
<pre><code class="language-bash">pnpm i -D lint-staged
</code></pre>
<ol start="2">
<li>在<code>package.json</code>中添加</li>
</ol>
<pre><code class="language-json">{
  &quot;lint-staged&quot;: {
    &quot;*&quot;: &quot;eslint --fix&quot;
  }
}
</code></pre>
<ol start="3">
<li>添加脚本</li>
</ol>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;lint-staged&quot;: &quot;lint-staged&quot;
  }
}
</code></pre>
<ol start="4">
<li>修改 husky 配置文件</li>
</ol>
<pre><code class="language-bash">echo &quot;pnpm lint-staged&quot; &gt; .husky/pre-commit
</code></pre>
<h2>提交规范</h2>
<ol>
<li>安装</li>
</ol>
<pre><code class="language-bash">pnpm i -D commitizen cz-git
</code></pre>
<ol start="2">
<li>在<code>package.json</code>中添加</li>
</ol>
<pre><code class="language-json">{
  &quot;config&quot;: {
    &quot;commitizen&quot;: {
      &quot;path&quot;: &quot;node_modules/cz-git&quot;
    }
  }
}
</code></pre>
<ol start="3">
<li>添加提交命令</li>
</ol>
<p>在<code>package.json</code>中<code>script</code>添加命令:</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;commit&quot;: &quot;git-cz&quot;
  }
}
</code></pre>
<p>使用了这两个插件后，你可以通过 <code>pnpm commit</code> 进行规范化的提交，但是还是可能会出现会有通过 <code>git bash</code> 去提交代码的情况，这种情况下如果没有规范化提交那也能提交成功，那么就没有完全实现规范提交，这时就需要用到其他插件: <code>pnpm install @commitlint/cli @commitlint/config-conventional -D</code>.</p>
<ol start="4">
<li>添加 <code>commitlint.config.js</code></li>
</ol>
<pre><code class="language-shell">echo &quot;export default { extends: ['@commitlint/config-conventional'] };&quot; &gt; commitlint.config.js
</code></pre>
<ol start="5">
<li>添加 husky githook</li>
</ol>
<pre><code class="language-shell">echo &quot;npx --no -- commitlint --edit \$1&quot; &gt; .husky/commit-msg
</code></pre>
<h2>editorconfig</h2>
<p>为了统一每个人编辑器不同的编写格式推荐添加以下配置</p>
<p>新建<code>.editorconfig</code>:</p>
<pre><code>root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
</code></pre>
<h2>提交错误记录</h2>
<pre><code class="language-shell">pnpm commit
</code></pre>
<p>之后会有一系列选项和input需要填写，提交时有可能会报错 <code>Cannot execute binary file: Exec format error</code>，这时需要来到 <code>.husky/pre-commit</code> 脚本文件中，查看编辑器右下角该文件的编码格式，如果是 <code>UTF-16</code> 之类的需要改成 <code>UTF-8</code> 即可。</p>
<p>同样的错误也可能出现在 <code>.husky/commit-msg</code> 和 <code>commitlint.config.js</code> 文件中，只需跟上面一样修改即可</p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[Vue 深入组件]]></title>
            <link>https://leetme.netlify.app/posts/vue-diving-component</link>
            <guid>https://leetme.netlify.app/posts/vue-diving-component</guid>
            <pubDate>Mon, 18 Mar 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h1>深入组件</h1>
<h2>Props</h2>
<h3>传递 prop 的细节</h3>
<p><strong>使用一个对象绑定多个 prop</strong></p>
<p>如果你想要将一个对象的所有属性都当作 props 传入，你可以使用没有参数的 <code>v-bind</code>，即只使用 <code>v-bind</code> 而非 <code>:prop-name</code>。</p>
<pre><code class="language-vue">&lt;script setup&gt;
const post = {
  id: 1,
  title: 'My Journey with Vue'
}
&lt;/script&gt;

&lt;BlogPost v-bind=&quot;post&quot; /&gt;

&lt;!-- 等价于 --&gt;
&lt;BlogPost :id=&quot;post.id&quot; :title=&quot;post.title&quot; /&gt;
</code></pre>
<h3>单向数据流</h3>
<p><strong><em>所有的 props 都遵循着单向绑定原则，props 因父组件的更新而变化，自然地将新的状态向下流往子组件，而不会逆向传递。这避免了子组件意外修改父组件的状态的情况，不然应用的数据流将很容易变得混乱而难以理解。</em></strong></p>
<p>如果要更改一个 prop 通常是下面两种场景：</p>
<ol>
<li>prop 被用于传入初始值；而子组件想在之后将其作为一个局部数据属性。</li>
</ol>
<pre><code class="language-ts">const props = defineProps(['initialCounter'])

// 计数器只是将 props.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
const counter = ref(props.initialCounter)
</code></pre>
<ol start="2">
<li>需要对传入的 prop 值做进一步的转换。</li>
</ol>
<pre><code class="language-ts">const props = defineProps(['size'])

// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() =&gt; props.size.trim().toLowerCase())
</code></pre>
<p><strong>更改对象 / 数组类型的 props</strong></p>
<p>当对象或数组作为 props 被传入时，虽然子组件无法更改 props 绑定，但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递，而对 Vue 来说，禁止这样的改动，虽然可能生效，但有很大的性能损耗，比较得不偿失。</p>
<p>这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态，可能会使数据流在将来变得更难以理解。在最佳实践中，你应该尽可能避免这样的更改，除非父子组件在设计上本来就需要紧密耦合。<em>在大多数场景下，子组件应该抛出一个事件来通知父组件做出改变。</em></p>
<h3>Prop 校验</h3>
<p>常用的几种校验应该不用我多说了，下面介绍几种不常见的:</p>
<pre><code class="language-vue">&lt;script setup&gt;
defineProps({
  // 对象类型的默认值
  propA: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propB: {
    validator(value, props) {
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认，这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})
&lt;/script&gt;
</code></pre>
<p>::: tip<br>
<code>defineProps()</code> 宏中的参数不可以访问 <code>&lt;script setup&gt;</code> 中定义的其他变量，因为在编译时整个表达式都会被移到外部的函数中。<br>
:::</p>
<p>一些细节:</p>
<ul>
<li>
<p>所有 prop 都是可选的，除非定义了<code>required: true</code></p>
</li>
<li>
<p>除去<code>Boolean</code>外的其他未传递的可选 prop 将会有一个默认值<code>undefined</code></p>
</li>
<li>
<p><code>Boolean</code>类型的未传递 prop 将转换为<code>false</code>。也可以通过设置 default 来改变。设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致</p>
</li>
<li>
<p>当 prop 被声明多个类型时，且只有当<code>String</code>在<code>Boolean</code>前面时，<code>Boolean</code>的转换规则才会生效。否则:<code>disabled: [String, Boolean]</code>会转换成<code>disabled=&quot;&quot;</code></p>
</li>
</ul>
<p>当 prop 的校验失败后，Vue 会抛出一个控制台警告 (在开发模式下)</p>
<p>如果使用了基于类型的 prop 声明 ，Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举例来说，<code>defineProps&lt;{ msg: string }&gt;</code> 会被编译为 <code>{ msg: { type: String, required: true }}</code>。</p>
<h2>事件</h2>
<p>通常父子组件通信都是用 emit，在这里有些细节:</p>
<ul>
<li>
<p>在 template 中可以直接用<code>$emit('eventName', params)</code></p>
</li>
<li>
<p>在<code>script setup</code>中定义:</p>
</li>
</ul>
<pre><code class="language-vue">&lt;script setup&gt;
// or
const emit = defineEmits&lt;{
  (e: 'submit', { username: string, password: string }): void
}&gt;()

// 最常见的定义
// const emit = defineEmits(['submit'])

// 约束参数类型
// const emit = defineEmits({
//   submit(payload: { username: string, password: string }) {}
// })
&lt;/script&gt;
</code></pre>
<p>::: tip<br>
尽管事件声明是可选的，官方文档更推荐完整声明所有触发的事件，以此在代码中作为文档记录组件的用法。同时，事件声明能让 Vue 更好地将事件和透传 attribute 作出区分，从而避免一些由第三方代码触发的自定义 DOM 事件所导致的边界情况。</p>
<p>如果定义的事件名和原生事件名冲突(比如<code>click</code>)，那么监听器只会监听组件触发的而不是原生事件<br>
:::</p>
<h3>事件校验</h3>
<p>要为事件添加校验，那么事件可以被赋值为一个函数，接受的参数就是抛出事件时传入 emit 的内容，返回一个布尔值来表明事件是否合法。</p>
<pre><code class="language-vue">&lt;script setup&gt;
const emit = defineEmits({
  submit: ({ username, password }) =&gt; {
    if (username &amp;&amp; password) {
      return true
    }
    else {
      // 提示未通过事件校验
      return false
    }
  }
})
&lt;/script&gt;
</code></pre>
<h2>组件 v-model</h2>
<h3>基本使用</h3>
<p>先来实现一下自定义组件 v-model：</p>
<ol>
<li>通过子组件的 prop 的<code>modelValue</code>和 emit 的<code>update:modelValue</code>实现（两个名称必须是这个）</li>
</ol>
<p>::: code-group</p>
<pre><code class="language-vue">&lt;script setup&gt;
const username = ref('')
&lt;/script&gt;

&lt;template&gt;
  &lt;Child v-model=&quot;username&quot; /&gt;
&lt;/template&gt;
</code></pre>
<pre><code class="language-vue">&lt;script setup&gt;
defineProps({
  modelValue: required
})

const emit = defineEmits(['update:modelValue'])
&lt;/script&gt;

&lt;template&gt;
  &lt;input
    type=&quot;text&quot;
    :value=&quot;modelValue&quot;
    @input=&quot;emit('update:modelValue', $event.target.value)&quot;
  &gt;
&lt;/template&gt;
</code></pre>
<p>:::</p>
<ol start="2">
<li>vue3.4+新增的 API: <code>defineModel()</code>, 更推荐这种方式</li>
</ol>
<p>::: code-group</p>
<pre><code class="language-vue">&lt;Child v-model=&quot;count&quot; /&gt;
</code></pre>
<pre><code class="language-vue">&lt;script setup&gt;
const model = defineModel()

function update() {
  model.value++
}
&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;parent bound v-model is: {{ model }}&lt;/div&gt;
&lt;/template&gt;
</code></pre>
<p>:::</p>
<p><code>defineModel()</code>返回的是 ref，所以：</p>
<ul>
<li>它的 <code>.value</code> 和父组件的 <code>v-model</code> 的值同步</li>
<li>当它被子组件变更了，会触发父组件绑定的值一起更新</li>
</ul>
<p>这意味的你可以用 v-model 将这个 ref 绑定到原生 input 上</p>
<pre><code class="language-vue">&lt;script setup&gt;
const model = defineModel()
&lt;/script&gt;

&lt;template&gt;
  &lt;input v-model=&quot;model&quot;&gt;
&lt;/template&gt;
</code></pre>
<p><code>defineModel()</code>是声明了一个 prop，所以你可以传递选项来约束 prop。</p>
<p>::: warning<br>
如果<code>defineModel</code>prop 设置了一个 default 值，但是父组件并没有为该 prop 提供任何值，那将会导致父子组件不同步</p>
<pre><code class="language-js">// 子组件：
const model = defineModel({ default: 1 })

// 父组件
const myRef = ref()
</code></pre>
<br/>
<pre><code class="language-vue">&lt;Child v-model=&quot;myRef&quot;&gt;&lt;/Child&gt;
</code></pre>
<p>:::</p>
<h3>v-model 的参数</h3>
<p>如果想要 v-model 有不一样的名称可以在<code>defineModel</code>第一个参数定义:</p>
<pre><code class="language-vue">&lt;MyComponent v-model:title=&quot;title&quot; /&gt;
</code></pre>
<pre><code class="language-vue">&lt;script setup&gt;
const title = defineModel('title')
&lt;/script&gt;

&lt;template&gt;
  &lt;input v-model=&quot;title&quot; type=&quot;text&quot;&gt;
&lt;/template&gt;
</code></pre>
<p>::: details 3.4 之前的写法</p>
<pre><code class="language-vue">&lt;script setup&gt;
defineProps({
  title: {
    required: true
  }
})
defineEmits(['update:title'])
&lt;/script&gt;

&lt;template&gt;
  &lt;input
    type=&quot;text&quot;
    :value=&quot;title&quot;
    @input=&quot;$emit('update:title', $event.target.value)&quot;
  &gt;
&lt;/template&gt;
</code></pre>
<p>:::</p>
<p>如果需要多个 v-model，只需要使用上面的方式创建多个 prop 即可。</p>
<h3>v-model 修饰符</h3>
<p>除了系统自带了<code>.trim</code>, <code>.number</code>, <code>.lazy</code>等。还有可能需要自定义修饰符。</p>
<p>比如现在自定义一个修饰符用于将输入的字符串首位字母转换成大写:</p>
<p>::: code-group</p>
<pre><code class="language-vue">&lt;MyComponent v-model.capitalize=&quot;text&quot; /&gt;
</code></pre>
<pre><code class="language-vue">&lt;script setup&gt;
// 这里证明已经传入了修饰符capitalize
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }

// 下面是处理修饰符功能:
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
&lt;/script&gt;

&lt;template&gt;
  &lt;input v-model=&quot;model&quot; type=&quot;text&quot;&gt;
&lt;/template&gt;
</code></pre>
<p>:::</p>
<p>::: details 3.4 之前的写法</p>
<pre><code class="language-vue">&lt;script setup&gt;
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () =&gt; ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
&lt;/script&gt;

&lt;template&gt;
  &lt;input type=&quot;text&quot; :value=&quot;modelValue&quot; @input=&quot;emitValue&quot;&gt;
&lt;/template&gt;
</code></pre>
<p>:::</p>
<h2>透传 Attributes</h2>
<h3>Attributes 继承</h3>
<p>“透传 Attributes”指的是传递给一个组件，但是没有被该组件声明为<code>props</code>, <code>emits</code>的 attribute 或者是<code>v-on</code>事件监听器，比如 class，style 和 id。</p>
<p>当一个组件以单个元素作为跟渲染时，那么透传的 attribute 会自动添加到根元素上。</p>
<pre><code class="language-vue">&lt;LeButton class=&quot;btn&quot; /&gt;

&lt;!-- 那么他是这样的 --&gt;
&lt;button class=&quot;btn&quot;&gt;
Click
&lt;/button&gt;
</code></pre>
<p><strong>对 class 或 style 的合并</strong></p>
<p>如果根元素上已经存在 class 或者 style attribute，他会和从父元素上继承的值合并。</p>
<p><strong><code>v-on</code>监听器继承</strong></p>
<p>如果将监听器添加到组件时会被添加到根元素上，即 button 上会绑定一个监听器，当 button 点击时会触发 LeButton 上的 click 方法。</p>
<p><strong>深层组件继承</strong></p>
<p>有些情况下一个组件会在根节点渲染另一个组件，例如<code>&lt;BaseButton /&gt;</code>，此时<code>&lt;LeButton /&gt;</code>接受的透传 attribute 会继续传给<code>&lt;BaseButton /&gt;</code></p>
<p>::: tip</p>
<ol>
<li>
<p>透传的 <code>attribute</code> 不会包含 <code>&lt;LeButton&gt;</code> 上声明过的 props 或是针对 <code>emits</code> 声明事件的 <code>v-on</code> 侦听函数，换句话说，声明过的 props 和侦听函数被 <code>&lt;LeButton&gt;</code>“消费”了</p>
</li>
<li>
<p>透传的 <code>attribute</code> 若符合声明，也可以作为 props 传入 <code>&lt;BaseButton&gt;</code><br>
:::</p>
</li>
</ol>
<h3>禁用 Attributes 继承</h3>
<p>如果你不想要组件自动的继承，你可以在组件选项中配置<code>inheritAttrs: false</code>。</p>
<p>在 3.3 中你也可以直接使用<code>defineOptions({ inheritAttrs: false })</code>。</p>
<p>最常见的需要禁用 Attribute 继承的场景就是需要应用在根节点以外的其他元素上。通过 inheritAttrs 禁用继承来主动控制透传的 Attributes 如何使用。</p>
<p>在模板中能直接使用<code>$attrs</code>，这个对象包含了除组件声明的 props 和 emits 之外的所有 attribute。</p>
<ul>
<li>和 <code>props</code> 有所不同，透传 attributes 在 JavaScript 中保留了它们原始的大小写，所以像 <code>foo-bar</code> 这样的一个 attribute 需要通过 <code>$attrs['foo-bar']</code> 来访问。</li>
<li>像 <code>@click</code> 这样的一个 <code>v-on</code> 事件监听器将在此对象下被暴露为一个函数 <code>$attrs.onClick</code>。</li>
</ul>
<h3>多根节点的 Attributes 继承</h3>
<p>多根节点的情况下，不会自动 attribute 透传行为。如果<code>$attrs</code>没有被显式绑定会抛出运行时警告。</p>
<h3>在 JavaScript 中访问透传 Attributes</h3>
<p>如果需要，可以在<code>&lt;script setup&gt;</code>中使用<code>useAttrs()</code></p>
<pre><code class="language-vue">&lt;script setup&gt;
import { useAttrs } from 'vue'

const attrs = useAttrs()
&lt;/script&gt;
</code></pre>
<p>如果没使用 setup 语法:</p>
<pre><code class="language-js">export default {
  setup(props, ctx) {
    // 透传 attribute 被暴露为 ctx.attrs
    console.log(ctx.attrs)
  }
}
</code></pre>
<p>::: tip<br>
需要注意的是，虽然这里的 attrs 对象总是反映为最新的透传 attribute，但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。如果你需要响应性，可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。<br>
:::</p>
<h2>插槽</h2>
<p>用三张图代替:</p>
<!-- <ZoomImg src="https://cn.vuejs.org/assets/slots.inBPF2Hb.png" desc="默认插槽" />

<ZoomImg src="https://cn.vuejs.org/assets/named-slots.giG_TKP2.png" desc="具名插槽" />

<ZoomImg src="https://cn.vuejs.org/assets/scoped-slots.eu7SD3OQ.svg" desc="作用域插槽" /> -->
<p><strong>无渲染组件</strong></p>
<p>一些组件可能只包括了逻辑而不需要自己渲染内容，视图输出通过作用域插槽全权交给了消费者组件。我们将这种类型的组件称为无渲染组件</p>
<pre><code class="language-vue">&lt;MouseTracker v-slot=&quot;{ x, y }&quot;&gt;
  Mouse is at: {{ x }}, {{ y }}
&lt;/MouseTracker&gt;
</code></pre>
<h2>依赖注入</h2>
<h3>Prop 逐级透传</h3>
<p>业务中，我们会碰到这种情况：有一三层级的组件树，c 组件要使用 a 组件的数据，但是需要 a 组件传递给 b 再传递给 c。这就是 prop 逐级透传。如果 b 组件根本不关心传给 c 组件的数据，那这整个过程 a 传给 b 这个过程是完全没必要的。</p>
<!-- <ZoomImg src="https://cn.vuejs.org/assets/prop-drilling.FyV2vFBP.png" /> -->
<p>所以就有了<code>provide</code>和<code>inject</code>。一个父组件相对所有的后代组件，会作为依赖提供者。任何后代的组件树，都可以注入由父组件提供给整条链路的依赖。</p>
<!-- <ZoomImg src="https://cn.vuejs.org/assets/provide-inject.tIACH1Z-.png" /> -->
<h3><code>provide</code></h3>
<p>为组件后代提供数据要用到<code>provide()</code>（必须是与 setup 同步调用）:</p>
<pre><code class="language-vue">&lt;script setup&gt;
import { provide } from 'vue'

// (name, value)
provide('message', 'hello')
&lt;/script&gt;
</code></pre>
<p>还可以直接在最顶层 App 上提供依赖，这样整个应用下都能使用依赖。直接在最顶层<code>App.vue</code>使用<code>provide()</code>即可。</p>
<h3><code>inject</code></h3>
<pre><code class="language-vue">&lt;script setup&gt;
import { inject } from 'vue'

const message = inject('message')
&lt;/script&gt;
</code></pre>
<p>如果提供的值是 ref，那么注入进来的会是该 ref 对象，而不会自动解包。这使得注入放组件能够通过 ref 对象保持了和供给放的响应式链接</p>
<p>如果在注入一个值不要求必须有提供者，那么可以声明一个默认值</p>
<pre><code class="language-js">const value = inject('message', '默认值')

// 在一些场景中，默认值可能需要通过调用一个函数或者初始化一个类来取得
// 为了避免在用不到默认值的情况下进行不必要的计算或产生副作用，可以用工厂函数创建
// 第三个参数表示默认值应该被当作一个工厂函数
// const value = inject('message', () =&gt; new ExpensiveClass(), true)
</code></pre>
<h3>和响应式数据配合使用</h3>
<p><strong>建议尽可能将任何对响应式状态的变更都保持在供给方组件中</strong>，这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内，使其更容易维护</p>
<p>如果需要在注入方组件中更改数据，可以在提供方声明一个更改该数据的函数:</p>
<p>::: code-group</p>
<pre><code class="language-vue">&lt;script setup&gt;
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
&lt;/script&gt;
</code></pre>
<pre><code class="language-vue">&lt;script setup&gt;
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
&lt;/script&gt;

&lt;template&gt;
  &lt;button @click=&quot;updateLocation&quot;&gt;
    {{ location }}
  &lt;/button&gt;
&lt;/template&gt;
</code></pre>
<p>:::</p>
<p>如果你想确保注入方不能修改数据你可以这样做:</p>
<pre><code class="language-js">const count = ref(0)
provide('read-only-count', readonly(count))
</code></pre>
<h2>异步组件</h2>
<h3>基本用法</h3>
<p>在大型项目中，我们可能需要拆分应用为更小的块，并仅在需要时再从服务器加载相关组件。Vue 提供了 <code>defineAsyncComponent</code> 方法来实现此功能：</p>
<pre><code class="language-js">import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =&gt; {
  return new Promise((resolve, reject) =&gt; {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
</code></pre>
<p>如你所见，<code>defineAsyncComponent</code> 方法接收一个返回 Promise 的加载函数。这个 Promise 的 <code>resolve</code> 回调方法应该在从服务器获得组件定义时调用。你也可以调用 <code>reject(reason)</code> 表明加载失败。</p>
<p>ES 模块动态导入也会返回一个 Promise。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点)，因此我们也可以用它来导入 Vue 单文件组件：</p>
<pre><code class="language-js">import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =&gt;
  import('./components/MyComponent.vue')
)
</code></pre>
<p>最后得到的 <code>AsyncComp</code> 是一个外层包装过的组件，仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件，所以你可以使用这个异步的包装组件无缝地替换原始组件，同时实现延迟加载。</p>
<h3>加载与错误状态</h3>
<p>异步操作避免不了加载和错误状态，因此该 API 也支持在高级选项中处理这些状态：</p>
<pre><code class="language-js">const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () =&gt; import('./Foo.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间，默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制，并超时了
  // 也会显示这里配置的报错组件，默认值是：Infinity
  timeout: 3000
})
</code></pre>
<h3>搭配 Suspense 使用</h3>
<p>异步组件可以搭配内置的 <code>&lt;Suspense&gt;</code> 组件一起使用</p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[Vue 基础]]></title>
            <link>https://leetme.netlify.app/posts/vue-base</link>
            <guid>https://leetme.netlify.app/posts/vue-base</guid>
            <pubDate>Fri, 15 Mar 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<p>最好的教程就是官方文档，自己打算多过即便文档，来查缺补漏，并记录一些不常见或者重要的知识。</p>
<h2><code>v-html</code></h2>
<p>可以利用 v-html 指令来渲染 html 内容；<strong><em>但注意，你不能使用 v-html 来拼接组合模板，因为 Vue 不是一个基于字符串的模板引擎</em></strong></p>
<blockquote>
<p>在网站上动态渲染任意 HTML 是非常危险的，因为这样非常容易造成 XSS 漏洞。请仅在内容完全可信时再使用 v-html，并且永远不要使用用户提供的 HTML 内容。</p>
</blockquote>
<h2>v-bind</h2>
<p>如果绑定的值是 null 或 undefined，那么该 attribute 将会从渲染的元素上移除</p>
<p><em>同名简写</em>，在 Vue3.4+中，当属性名和值的名称相同时，可以使用类似于 ES6 对象属性的写法:</p>
<pre><code class="language-vue">&lt;div :id&gt;&lt;/div&gt;

&lt;!-- 等同于 --&gt;

&lt;div :id=&quot;id&quot;&gt;&lt;/div&gt;

&lt;div v-bind:id&gt;&lt;/div&gt;
</code></pre>
<p><code>v-bind</code>在表单的<code>:disabled</code>下的行为略有不同:<br>
当值为真值或是空字符串时，元素会包含这个属性，而当为其他假值时属性才会被忽略。</p>
<p>没有显式包含在列表中的全局对象将不能在模板内表达式中访问，例如用户附加在 window 上的属性。然而，你也可以自行在 app.config.globalProperties 上显式地添加它们，供所有的 Vue 表达式使用。</p>
<h2>指令</h2>
<h3>动态参数</h3>
<p>在指令参数上也可以使用 js 表达式，需要包含在一对方括号内:</p>
<pre><code class="language-vue">&lt;!--
注意，参数表达式有一些约束

动态参数值的限制：动态参数中表达式的值应当是一个字符串，或是null。
特殊值null意为显示一处该绑定，其他非字符串的值会触发警告

动态参数语法的限制：动态参数表达式因为某些字符的缘故有一些语法限制，
比如空格和引号，在HTML attribute名称中都是不合法的；
如果需要传入复杂动态参数，最好使用计算属性；
避免在名称中使用大写字母，浏览器会强制转换成小写字母
--&gt;
&lt;a v-bind:[attributeName]=&quot;url&quot;&gt;
 ...
&lt;/a&gt;

&lt;!-- 简写 --&gt;
&lt;a :[attributeName]=&quot;url&quot;&gt;
 ...
&lt;/a&gt;
</code></pre>
<h2>响应式</h2>
<h3><code>ref</code></h3>
<p>可以通过<code>shallowRef</code>来放弃 ref 的深层响应性，可以避免大型数据的响应性开销。</p>
<h3>DOM 更新时机</h3>
<p>当你修改了响应式状态时，DOM 会被自动更新。但是需要注意的是，DOM 更新不是同步的。Vue 会在<code>next tick</code>更新周期中缓冲所有状态的修改，以确保不管你进行了多少次状态修改，每个组件都只会被更新一次。</p>
<p>要等待 DOM 更新完成后再执行额外的代码可以使用全局 API: <code>nextTick()</code></p>
<h3><code>reactive</code></h3>
<p>也可以通过<code>shallowReactive</code>来退出深层响应性。</p>
<p><strong>局限性</strong></p>
<ul>
<li>有限的值类型: 只能用于对象类型(对象、数组和<code>Map</code>、<code>Set</code>这样的集合类型)</li>
<li>不能替换整个对象: 由于 Vue 的响应式跟踪是通过属性访问实现的，因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象，因为这样的话与第一个引用的响应性连接将丢失</li>
</ul>
<pre><code class="language-js">let state = reactive({ count: 0 })

// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失！)
state = reactive({ count: 1 })
</code></pre>
<ul>
<li>对解构操作不友好: 当我们将响应式对象的原始类型属性解构为本地变量时，或者将该属性传递给函数时，我们将丢失响应性连接</li>
</ul>
<pre><code class="language-js">const state = reactive({ count: 0 })

// 当解构时，count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
</code></pre>
<p>由于这些限制，更建议使用 ref。</p>
<h3>额外的 ref 解包细节</h3>
<p><strong>作为 reactive 对象的属性</strong></p>
<p>一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。换句话说，它的行为就像一个普通的属性</p>
<pre><code class="language-js">const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1
</code></pre>
<p>如果将一个新的 ref 赋值给一个关联了已有 ref 的属性，那么它会替换掉旧的 ref</p>
<pre><code class="language-js">const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
</code></pre>
<p>只有当嵌套在一个深层响应式对象内时，才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。</p>
<h2>计算属性</h2>
<p>若我们将同样的函数定义为一个方法而不是计算属性，两种方式在结果上确实是完全相同的，然而，不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变，无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果，而不用重复执行 getter 函数。</p>
<p>相比之下，方法调用总是会在重渲染发生时再次执行函数。</p>
<p>为什么需要缓存呢？想象一下我们有一个非常耗性能的计算属性 list，需要循环一个巨大的数组并做许多计算逻辑，并且可能也有其他计算属性依赖于 list。没有缓存的话，我们会重复执行非常多次 list 的 getter，然而这实际上没有必要！如果你确定不需要缓存，那么也可以使用方法调用。</p>
<h3>可写计算属性</h3>
<pre><code class="language-vue">&lt;script setup&gt;
import { computed, ref } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // getter
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  // setter
  set(newValue) {
    // 注意：我们这里使用的是解构赋值语法
    ;[firstName.value, lastName.value] = newValue.split(' ')
  }
})
&lt;/script&gt;
</code></pre>
<h3>可传参的计算属性</h3>
<pre><code class="language-vue">&lt;script setup&gt;
import { computed, ref } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed(() =&gt; (value) =&gt; {
  return firstName.value + value + lastName.value
})
&lt;/script&gt;
</code></pre>
<h3>最佳实践</h3>
<p><strong>Getter 不应有副作用</strong>:<br>
计算属性的 getter 应只做计算而没有任何其他的副作用，这一点非常重要，请务必牢记。举例来说， <strong>不要改变其他状态、在 getter 中做异步请求或者更改 DOM！</strong> 一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。</p>
<p><strong>避免直接修改计算属性值</strong>:<br>
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”，每当源状态发生变化时，就会创建一个新的快照。更改快照是没有意义的，因此计算属性的返回值应该被视为只读的，并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。</p>
<h2>样式绑定</h2>
<p>当在自定义组件上使用样式，且组件有多个根元素，你将需要指定哪个根元素来接收这个 class。你可以通过组件的<code>$attrs</code>属性来实现指定:</p>
<pre><code class="language-vue">&lt;!-- MyComponent 模板使用 $attrs 时 --&gt;
&lt;p :class=&quot;$attrs.class&quot;&gt;
Hi!
&lt;/p&gt;

&lt;span&gt;
This is a child component
&lt;/span&gt;
</code></pre>
<pre><code class="language-vue">&lt;MyComponent class=&quot;baz&quot; /&gt;
</code></pre>
<p>这将被渲染为:</p>
<pre><code class="language-vue">&lt;p class=&quot;baz&quot;&gt;
Hi!
&lt;/p&gt;

&lt;span&gt;
This is a child component
&lt;/span&gt;
</code></pre>
<h3>样式多值</h3>
<pre><code class="language-vue">&lt;div :style=&quot;{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }&quot;&gt;&lt;/div&gt;
</code></pre>
<p>数组仅会渲染浏览器支持的最后一个值。在这个示例中，在支持不需要特别前缀的浏览器中都会渲染为 display: flex。</p>
<h2>条件渲染</h2>
<p>v-if 是“真实的”按条件渲染，因为它确保了在切换时，条件区块内的事件监听器和子组件都会被销毁与重建。</p>
<p>v-if 也是惰性的：如果在初次渲染时条件值为 false，则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。</p>
<p>相比之下，v-show 简单许多，元素无论初始条件如何，始终会被渲染，只有 CSS display 属性会被切换。</p>
<p>总的来说，v-if 有更高的切换开销，而 v-show 有更高的初始渲染开销。因此，如果需要频繁切换，则使用 v-show 较好；如果在运行时绑定条件很少改变，则 v-if 会更合适。</p>
<h2>列表渲染</h2>
<p>你也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys() 的返回值来决定。</p>
<pre><code class="language-vue">&lt;li v-for=&quot;(value, key, index) in myObject&quot;&gt;
  {{ index }}. {{ key }}: {{ value }}
&lt;/li&gt;
</code></pre>
<blockquote>
<p>同时使用 v-if 和 v-for 是不推荐的，因为这样二者的优先级不明显。当它们同时存在于一个节点上时，v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名</p>
</blockquote>
<h3>数组变化侦测</h3>
<p>Vue 能够侦听响应式数组的变更方法，并在它们被调用时触发相关的更新。这些变更方法包括(能改变原数组)：</p>
<p>push()<br>
pop()<br>
shift()<br>
unshift()<br>
splice()<br>
sort()<br>
reverse()</p>
<p>在计算属性中使用 reverse() 和 sort() 等其他能改变原数组的方法的时候务必小心！这两个方法将变更原始数组，计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本: <code>return [...array].reverse()</code></p>
<h2>事件处理</h2>
<p>内联事件处理器和方法事件处理器的区别:</p>
<p>模板编译器会通过检查 v-on 的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说，foo、foo.bar 和 foo['bar'] 会被视为方法事件处理器，而 foo() 和 count++ 会被视为内联事件处理器。</p>
<p>有时候看到<code>&lt;div @click=&quot;foo()&quot;&gt;&lt;/div&gt;</code>和<code>&lt;div @click=&quot;foo&quot;&gt;&lt;/div&gt;</code>都是一样的效果，到底有哪里不同。</p>
<pre><code class="language-vue">&lt;div @click=&quot;foo()&quot;&gt;&lt;/div&gt;

&lt;!-- 等价于 --&gt;
&lt;div @click=&quot;() =&gt; foo()&quot;&gt;&lt;/div&gt;

&lt;!-- 且获取不到事件参数，需要主动传入 --&gt;
&lt;div @click=&quot;(event) =&gt; foo(event)&quot;&gt;&lt;/div&gt;

&lt;div @click=&quot;foo($event)&quot;&gt;&lt;/div&gt;
</code></pre>
<p>而一下这种是自带事件参数， 在方法中能直接访问</p>
<pre><code class="language-vue">&lt;div @click=&quot;foo&quot; /&gt;

&lt;script setup&gt;
function foo(event) {
  console.log(event)
}
&lt;/script&gt;
</code></pre>
<h2>表单输入绑定</h2>
<p>::: tip<br>
<code>v-model</code> 会忽略任何表单元素上初始的 <code>value</code>、<code>checked</code> 或 <code>selected</code> attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的 API 来声明该初始值。<br>
:::</p>
<p>::: tip<br>
对于需要使用 IME 的语言 (中文，日文和韩文等)，你会发现 <code>v-model</code> 不会在 IME 输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新，请直接使用自己的 <code>input</code> 事件监听器和 <code>value</code> 绑定而不要使用 <code>v-model</code>。<br>
:::</p>
<p>::: tip<br>
如果 <code>v-model</code> 表达式的初始值不匹配任何一个选择项，<code>&lt;select&gt;</code> 元素会渲染成一个“未选择”的状态。在 iOS 上，这将导致用户无法选择第一项，因为 iOS 在这种情况下不会触发一个 change 事件。因此，我们建议提供一个空值的禁用选项，如上面的例子所示。<br>
:::</p>
<h3>复选框</h3>
<pre><code class="language-vue">&lt;input type=&quot;checkbox&quot; v-model=&quot;toggle&quot; true-value=&quot;yes&quot; false-value=&quot;no&quot; /&gt;
</code></pre>
<p><code>true-value</code> 和 <code>false-value</code> 是 Vue 特有的 attributes，仅支持和 <code>v-model</code> 配套使用。这里 <code>toggle</code> 属性的值会在选中时被设为 <code>'yes'</code>，取消选择时设为 <code>'no'</code>。你同样可以通过 <code>v-bind</code> 将其绑定为其他动态值</p>
<p>::: tip<br>
<code>true-value</code> 和 <code>false-value</code> attributes 不会影响 <code>value</code> attribute，因为浏览器在表单提交时，并不会包含未选择的复选框。为了保证这两个值 (例如：“yes”和“no”) 的其中之一被表单提交，请使用单选按钮作为替代。<br>
:::</p>
<h2>生命周期</h2>
<!-- <ZoomImg src="https://cn.vuejs.org/assets/lifecycle_zh-CN.FtDDVyNA.png" desc="生命周期流程图" /> -->
<h2>侦听器</h2>
<h3>一次性侦听器(vue3.4+)</h3>
<p>每当被侦听源发生变化时，侦听器的回调就会执行。如果希望回调只在源变化时触发一次，请使用 <code>once: true</code> 选项。</p>
<h3><code>watchEffect</code></h3>
<p>::: tip<br>
<code>watchEffect</code> 仅会在其同步执行期间，才追踪依赖。在使用异步回调时，只有在第一个 await 正常工作前访问到的属性才会被追踪。<br>
:::</p>
<h3><code>watch</code> vs. <code>watchEffect</code></h3>
<p>watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式：</p>
<ul>
<li>
<p><code>watch</code> 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外，仅在数据源确实改变时才会触发回调。<code>watch</code> 会避免在发生副作用时追踪依赖，因此，我们能更加精确地控制回调函数的触发时机。</p>
</li>
<li>
<p><code>watchEffect</code>，则会在副作用发生期间追踪依赖。它会在同步执行过程中，自动追踪所有能访问到的响应式属性。这更方便，而且代码往往更简洁，但有时其响应性依赖关系会不那么明确。</p>
</li>
</ul>
<h3>回调的触发机制</h3>
<p>默认情况下，侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM，那么 DOM 将处于更新前的状态。</p>
<p>如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM:</p>
<pre><code class="language-js">watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})
// or
watchPostEffect(() =&gt; {
  /* 在 Vue 更新后执行 */
})
</code></pre>
<p>还可以创建一个同步触发的侦听器，它会在 Vue 进行任何更新之前触发:</p>
<pre><code class="language-js">watch(source, callback, {
  flush: 'sync'
})

watchEffect(callback, {
  flush: 'sync'
})
// or
watchSyncEffect(() =&gt; {
  /* 在响应式数据变化时同步执行 */
})
</code></pre>
<p>::: warning 谨慎使用<br>
同步侦听器不会进行批处理，每当检测到响应式数据发生变化时就会触发。可以使用它来监视简单的布尔值，但应避免在可能多次同步修改的数据源 (如数组) 上使用。<br>
:::</p>
<h3>停止侦听器</h3>
<p>侦听器必须用同步语句创建：如果用异步回调创建一个侦听器，那么它不会绑定到当前组件上，你必须手动停止它，以防内存泄漏。</p>
<pre><code class="language-vue">&lt;script setup&gt;
import { watchEffect } from 'vue'

// 它会自动停止
watchEffect(() =&gt; {})

// ...这个则不会！
setTimeout(() =&gt; {
  watchEffect(() =&gt; {})
}, 100)

const unwatch = watchEffect(() =&gt; {})
// ...当该侦听器不再需要时
unwatch()
&lt;/script&gt;
</code></pre>
<h2>模板引用</h2>
<h3><code>v-for</code>中的模板引用</h3>
<p>当在 v-for 中使用模板引用时，对应的 ref 中包含的值是一个数组，它将在元素被挂载后包含对应整个列表的所有元素，但是 <strong>ref 数组并不保证与源数组相同的顺序</strong>。</p>
<h3>函数模板引用</h3>
<p>除了使用字符串值作名字，ref attribute 还可以绑定为一个函数，会在每次组件更新时都被调用。</p>
<pre><code class="language-vue">&lt;input
  :ref=&quot;
    (el) =&gt; {
      /* 将 el 赋值给一个数据属性或 ref 变量 */
    }
  &quot;
/&gt;
</code></pre>
<p>注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时，函数也会被调用一次，此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。</p>
<h3>组件上的 ref</h3>
<p><code>option API</code>或者非<code>&lt;script setup&gt;</code>，被引用的组件实例和该子组件的 this 完全一致，这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易，当然也因此，应该只在绝对需要时才使用组件引用。大多数情况下，你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。</p>
<p>使用了 <code>&lt;script setup&gt;</code> 的组件是默认私有的：一个父组件无法访问到一个使用了 <code>&lt;script setup&gt;</code> 的子组件中的任何东西，除非子组件在其中通过 defineExpose 宏显式暴露:</p>
<pre><code class="language-vue">&lt;script setup&gt;
import { ref } from 'vue'

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
&lt;/script&gt;
</code></pre>
<h2>组件基础</h2>
<h3>动态组件</h3>
<p>有些场景会需要在两个组件间来回切换</p>
<pre><code class="language-vue">&lt;component :is=&quot;tabs[currentTab]&quot;&gt;&lt;/component&gt;
</code></pre>
<p>在上面的例子中，被传给 <code>:is</code> 的值可以是以下几种：</p>
<ul>
<li>被注册的组件名</li>
<li>导入的组件对象<br>
你也可以使用 <code>is</code> attribute 来创建一般的 HTML 元素。</li>
</ul>
<p>当使用 <code>&lt;component :is=&quot;...&quot;&gt;</code> 来在多个组件间作切换时，被切换掉的组件会被卸载。我们可以通过 <code>&lt;KeepAlive&gt;</code> 组件强制被切换掉的组件仍然保持“存活”的状态。</p>
<h3>DOM 内模板解析注意事项</h3>
<p>如果你想在 DOM 中直接书写 Vue 模板，Vue 则必须从 DOM 中获取模板字符串。由于浏览器的原生 HTML 解析行为限制，有一些需要注意的事项。</p>
<p>::: tip<br>
请注意下面讨论只适用于直接在 DOM 中编写模板的情况。如果你使用来自以下来源的字符串模板，就不需要顾虑这些限制了：</p>
<ul>
<li>单文件组件</li>
<li>内联模板字符串 (例如 template: '...')</li>
<li><code>&lt;script type=&quot;text/x-template&quot;&gt;</code></li>
</ul>
<p>:::</p>
<p><strong>闭合标签</strong></p>
<p>在 DOM 内模板中，我们必须显式地写出关闭标签<code>&lt;my-component&gt;&lt;/my-component&gt;</code>。</p>
<p>这是由于 HTML 只允许一小部分特殊的元素省略其关闭标签，最常见的就是 <input> 和 <img>。对于其他的元素来说，如果你省略了关闭标签，原生的 HTML 解析器会认为开启的标签永远没有结束，用下面这个代码片段举例来说：</p>
<pre><code class="language-vue">&lt;my-component /&gt;

&lt;!-- 我们想要在这里关闭标签... --&gt;
&lt;span&gt;
hello
&lt;/span&gt;

&lt;!-- 将被解析为 --&gt;
&lt;my-component&gt;
  &lt;span&gt;hello&lt;/span&gt;
&lt;/my-component&gt;
&lt;!-- 但浏览器会在这里关闭标签 --&gt;
</code></pre>
<p><strong>元素位置限制</strong></p>
<p>某些 HTML 元素对于放在其中的元素类型有限制，例如 <code>&lt;ul&gt;</code>，<code>&lt;ol&gt;</code>，<code>&lt;table&gt;</code> 和 <code>&lt;select&gt;</code>，相应的，某些元素仅在放置于特定元素中时才会显示，例如 <code>&lt;li&gt;</code>，<code>&lt;tr&gt;</code> 和 <code>&lt;option&gt;</code>。</p>
<pre><code class="language-vue">&lt;table&gt;
  &lt;blog-post-row&gt;&lt;/blog-post-row&gt;
&lt;/table&gt;
</code></pre>
<p>自定义的组件 <code>&lt;blog-post-row&gt;</code> 将作为无效的内容被忽略，因而在最终呈现的输出中造成错误。我们可以使用特殊的 is attribute 作为一种解决方案：</p>
<pre><code class="language-vue">&lt;table&gt;
  &lt;tr is=&quot;vue:blog-post-row&quot;&gt;&lt;/tr&gt;
&lt;/table&gt;
</code></pre>
<p>::: tip<br>
当使用在原生 HTML 元素上时，is 的值必须加上前缀 vue: 才可以被解析为一个 Vue 组件。这一点是必要的，为了避免和原生的自定义内置元素相混淆。<br>
:::</p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[内存泄漏和垃圾回收]]></title>
            <link>https://leetme.netlify.app/posts/garbage-collection</link>
            <guid>https://leetme.netlify.app/posts/garbage-collection</guid>
            <pubDate>Thu, 11 Jan 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<p>像 C 这样的底层语言一般都有底层的内存管理接口，比如<code>malloc()</code>和<code>free()</code>。相反，<strong>JavaScript 是在创建变量时自动进行内存分配，并且在不使用他们时&quot;自动&quot;释放</strong>。释放的过程称为垃圾回收。这个&quot;自动&quot;是混乱的根源，并让 JavaScript 开发者错误的感觉他们可以不关心内存管理。</p>
<p>不管什么语言，内存的生命周期基本是一致的:</p>
<ol>
<li>分配你所需要的内存</li>
<li>使用分配到的内存（读、写）</li>
<li>不需要时将其释放/归还</li>
</ol>
<blockquote>
<p>所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的，但在像 JavaScript 这些高级语言中，大部分都是隐含的。</p>
</blockquote>
<p>在 Chorme 浏览器中，V8 被限制了内存的使用<code>(x64约1.4G/1464MB)/(x86约0.7G/732MB)</code>，限制的主要原因是 V8 最初为浏览器而设计，不太可能遇到用大量内存的场景，更深层原因是 V8 垃圾回收机制的限制: <em>清理大量的内存垃圾很耗时间，这样会引起 JavaScript 线程暂停执行，导致性能和应用直线下降。</em></p>
<h2>内存泄漏</h2>
<p>内存泄漏(Memory leak)是在计算机科学中，由于疏忽或错误造成程序未能释放已经不再使用的内存。</p>
<p>并非指内存在物理上的消失，而是应用程序分配某段内存后，由于设计错误，导致在释放改段内存之前就失去了对该段内存的控制，从而造成内存的浪费。</p>
<p>程序的运行需要内存。只要程序提出要求，操作系统或运行时(runtime)就必须供给内存。</p>
<p>对于持续运行的服务进程，必须及时释放不再用到的内存。否则，内存占用越来越高，轻则影响系统性能，重则导致进程崩溃。</p>
<h2>常见的内存泄漏</h2>
<p>写得不好的 JavaScript 可能出现难以察觉且有害的内存泄漏问题。</p>
<p>在内存有限的设备上，或者在函数会被调用很多次的情况下，内存泄漏可能是个大问题。JavaScript 中的内存泄露大部分是由不合理的引用导致的。</p>
<h3>意外声明的全局变量</h3>
<p>JavaScript 对未声明变量的处理方式: 在全局对象上创建该变量的引用(即全局对象上的属性，不是变量，因为它能通过<code>delete</code>删除)。如果在浏览器中，全局对象就是 window 对象。</p>
<p>如果未声明的变量缓存大量的数据，会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。</p>
<pre><code class="language-js">function foo(arg) {
  bar = 'Leet'
}

// 等同于
function foo2(arg) {
  window.bar = 'Leet'
}
</code></pre>
<p>通过 this 创建意外的全局变量:</p>
<pre><code class="language-js">function foo(arg) {
  this.val = 'Leet'
}

// 当在全局作用域下调用foo函数，此时this指向的是全局对象window，而不是'undefined'
foo()
</code></pre>
<p>此时，解释器会把变量<code>name</code>当作<code>window</code>的属性来创建（<code>window.name = 'Leet'</code>）。只要在<code>window</code>对象上创建的属性，只要<code>window</code>本身不被清理就不会消失。这个问题很容易解决:</p>
<ul>
<li>只要在变量声明前加上<code>let</code>或<code>const</code>关键字即可，这样变量就会在函数执行完毕后离开作用域。</li>
<li>在文件中添加<code>use strict</code>开启严格模式可以有效避免</li>
</ul>
<pre><code class="language-js">function foo(arg) {
  'use strict'
  bar = 'Leet' // 报错bar未声明
}
</code></pre>
<p>如果需要在一个函数内使用全局变量，建议:</p>
<pre><code class="language-js">function foo(arg) {
  window.bar = 'Leet'
}
</code></pre>
<p>这样不仅可读性高，而且易维护。</p>
<blockquote>
<p>谈到全局变量，需要注意那些用来临时存储大量数据的全局变量，确保在处理玩这些数据后将其设置为 null 或重新赋值。全局变量也常用来做 cache，一般 cache 都是为了性能优化才能用到的，为了性能，最好对 cache 的大小做个上限限制。因为 cache 是不能被回收的，越高的 cache 会导致越高的内存消耗。</p>
</blockquote>
<h3><code>console.log</code></h3>
<p><code>console.log</code>: 向 web 开发控制台打印一条消息，常用来开发时调试分析。有时在开发时，需要打印一些对象信息，但发布时却忘记去掉<code>console.log</code>，这可能造成内存泄漏。</p>
<p>在传递给<code>console.log</code>的对象是不能被垃圾回收的，因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中<code>console.log</code>任何对象，另外还有<code>console.dir</code>、<code>console.err</code>、<code>console.warn</code>等都存在类似问题，这些细节需要特别关注。</p>
<h3>闭包(Closures)</h3>
<p>当一个函数 A 返回一个内联函数 B，即使函数 A 执行完，函数 B 也能访问函数 A 作用域内的变量，这就是一个闭包————本质上闭包是将函数内部和外部连接起来的一座桥梁。</p>
<pre><code class="language-js">function foo(msg) {
  function closure() {
    console.log(msg)
  }

  return closure
}

const bar = foo('leet')
bar() // 'leet'
</code></pre>
<p>在函数 foo 内创建的函数 closure 对象是不能被回收掉的，因为它被全局变量 bar 引用，处于一直可访问状态。通过执行<code>bar()</code>可以打印出<code>leet</code>。如果想释放掉可以将<code>bar = null</code>即可。</p>
<blockquote>
<p>由于闭包会携带包含它的函数的作用域，因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。</p>
</blockquote>
<h3>DOM 泄露</h3>
<p>DOM 元素的生命周期正常情况下取决于是否挂载在 DOM 树上，当元素从 DOM 树上移除时，就可以被销毁回收了。</p>
<p>但如果某个 DOM 元素在 JS 中也持有它的引用，想要彻底删除这个元素，就需要把两个引用都清楚，这样才能正常回收它。</p>
<pre><code class="language-js">// 在对象中引用 DOM
const elements = {
  btn: document.getElementById('btn')
}
function doSomeThing() {
  elements.btn.click()
}

function removeBtn() {
  // 移除 DOM 树中的 btn
  document.body.removeChild(document.getElementById('button'))
  // 但是此时全局变量 elements 还是保留了对 btn 的引用, btn 还是存在于内存中,不能被 GC 回收
}
</code></pre>
<p>虽然别的地方删除了，但是对象中还存在对 dom 的引用。</p>
<p>解决方法是删除 DOM 节点时，也要释放 JS 对节点的引用：<code>elements.btn = null</code></p>
<h3>timers</h3>
<p>在 JavaScript 常用<code>setInterval()</code>来实现一些动画效果。当然也可以使用链式<code>setTimeout()</code>调用模式来实现:</p>
<pre><code class="language-js">// setTimeout(function () {
//   setTimeout(arg.callee, interval)
// }, interval)
</code></pre>
<p>如果在不需要<code>setInterval()</code>时，没有通过<code>clearInterval()</code>方法移除，那么<code>setInterval()</code>会不停的调用函数，直到调用<code>clearInterval()</code>或窗口关闭。如果链式<code>setTimeout()</code>调用模式没有给出终止逻辑，也会一直运行下去。</p>
<p><strong><em>因此在不需要重复定时器时，确保对定时器进行清除</em></strong>，避免占用系统资源。另外，在使用<code>setInterval()</code>和<code>setTimeout()</code>来实现动画时，无法确保定时器按照指定的时间间隔来执行动画。</p>
<p>为了能在 JavaScript 中创建出平滑流畅的动画，浏览器为 JavaScript 动画添加了一个新 API<code>requestAnimationFrame()</code>。</p>
<h3>EventListener</h3>
<p>做移动开发时，需要对不同设备尺寸做适配。如在开发组件时，有时候需要考虑处理横竖屏适配问题。一般做法，在横竖屏发生变化时，需要将组件销毁后再重新生成。而在组件中会对其进行相关事件绑定，如果在销毁组件时，没有将组件的事件解绑，在横竖屏发生变化时，就会不断地对组件进行事件绑定。这样会导致一些异常，甚至可能会导致页面崩掉。</p>
<p>在开发中，开发者很少关注事件解绑，因为浏览器已经为我们处理得很好了。不过在使用第三方库时，需要特别注意，因为一般第三方库都实现了自己的事件绑定，如果在使用过程中，在需要销毁事件绑定时，没有调用所解绑方法，就可能造成事件绑定数量的不断增加。</p>
<h2>垃圾回收</h2>
<p>JavaScript 是使用垃圾回收的语言，也就是说执行环境负责在代码执行时管理内存。在 C 和 C++等底层语言中，跟踪内存使用对开发者来说是个很大的负担，也是很多问题的来源。</p>
<p>JavaScript 为开发者卸下了这个负担，通过自动内存管理实现内存分配和闲置资源回收。基本思路就是: 确定哪个变量不会再使用，然后释放它占用的内存。</p>
<p>这个过程时周期性的，即垃圾回收程序每隔一定时间（或者说在代码执行过程中某个预定的收集时间）就会自动运行。垃圾回收过程时一个近似且不完美的方案，因为某块内存是否还有用，属于&quot;不可判定的&quot;问题，意味着靠算法是解决不了的。</p>
<p>过去这些年 V8 的垃圾回收器（GC）发生了很多的变化，Orinoco 项目采用了 stop-the-world 垃圾回收器，以使其变成了一个更加并行，并发和增量的垃圾回收器。</p>
<p>附上原视频(youtube)</p>
<YouTubeEmbed id="Scxz6jVS4Ls" noScale />
<h3>主垃圾回收器</h3>
<p>主垃圾回收器从整个堆（heap）中收集垃圾。</p>
<p>主垃圾回收器主要有三个阶段：标记（marking），清除（sweeping）和整理（compacting）：</p>
<img src="https://v8.js.cn/_img/trash-talk/01.svg" />
<h4>标记阶段</h4>
<p>确定哪些对象可以被回收是垃圾回收中重要的一步。垃圾回收器通过可访问性(reachability)来确定对象的“活跃度”(liveness)。这意味着任何对象如果在运行时是可访问的(reachable)，那么必须保证这些对象应该在内存中保留，如果对象是不可访问的(unreachable)，那么这些对象就可能被回收。</p>
<p>标记阶段就是找到可访问对象的一个过程；垃圾回收是从一组对象的指针(objects pointers)开始的，我们将其称之为根集(root set)，这其中包括了执行栈和全局对象；然后垃圾回收器会跟踪每一个指向 JavaScript 对象的指针，并将对象标记为可访问的，同时跟踪对象中每一个属性的指针并标记为可访问的，这个过程会递归地进行，直到标记到运行时每一个可访问的对象。</p>
<h4>清理阶段</h4>
<p>清理阶段就是将非活动对象占用的内存空间添加到一个叫空闲列表(free-list)的数据结构中。一旦标记完成，垃圾回收器会找到不可访问对象的内存对象，并将内存空间添加到相应的空闲列表中。空闲列表中的内存块由大小来区分，为什么这样做？为了方便以后需要分配内存，就可以快速的找到大小合适的内存空间并分配个新的对象。</p>
<h4>整理阶段</h4>
<p>主垃圾回收器会通过一种叫碎片启发式(fragmentation heuristic)的算法来整理内存页，你也可以将整理阶段理解为老式 PC 上的磁盘整理。那么碎片启发式算法是怎么做的呢？我们将活动对象复制到当前没有被整理的其他内存页中(即被添加到空闲列表的内存页); 通过这种做法，我们就可以利用内存中高度小而分散的内存空间。</p>
<p>垃圾回收器复制活动对象到当前没有被整理的其他内存页中有一个潜在的缺点，我们要分配内存空间给很多常驻内存(long-living)的对象时，复制这些对象会带来很高的成本。这就是为什么我们只选择整理内存中高度分散的内存页，并且对其他内存页我们只进行清理而不是也同样复制活动对象的原因。</p>
<h3>分代堆布局</h3>
<p>堆在 V8 中会分为两个不同的区域，我们将其称之为代(generations)；这两块区域分别称之为老生代(old generation)和新生代(young generation)，新生代又进一步分为'nursery'子代和'intermediate'子代两块区域；一个对象第一次分配内存时会被分配到新生代中'nursery'子代；如果进过下一次垃圾回收这个对象还存在新生代中，这时候我们移动到'intermediate'子代，再经过下一次垃圾回收这个对象还在新生代，这时候我们就会把这个对象移动到老生代。</p>
<p>V8中堆分成两代，如果经过垃圾回收对象还存活的话会从新生代移动到老生代:</p>
<img src="https://v8.js.cn/_img/trash-talk/02.svg" />
<p>在垃圾回收中有一个重要的术语：“代际假说”（The Generational Hypothesis）；<strong>代际假说表明很多对象在内存中存在的时间很短（die young）。换句话说，从垃圾回收的角度来看，很多对象一经分配内存空间随即就变成了不可访问的</strong>。这个假说不仅仅适用于 V8 和 JavaScript，同样适用于大多数的动态语言</p>
<p>V8 分代堆布局的设计主要是为了利用对象存在生命周期的这个事实；垃圾回收实质上就是整理内存和移动内存中的对象，那这就意味着我们应该多移动对象到空闲列表中的内存中去；这个看上去似乎有点违反直觉，因为在垃圾回收的时候复制对象的成本很高。但是根据代际假说在垃圾回收中，在内存中存活下来的对象其实并不是很多。所以重新分配内存给新创建的对象，这反而变成了隐式的垃圾；这就意味着我们只需花费复制存活对象的成本，并不需要耗费成本去分配新的内存。</p>
<h3>副垃圾回收器</h3>
<p>V8 有两个垃圾回收器，<a href="/articles/garbage-collection#%E4%B8%BB%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8">主垃圾回收器（Full Mark-Compact）</a>从整个堆中回收垃圾，<strong>副垃圾回收器</strong>（Scavenger）从新生代中回收垃圾。主垃圾回收器可以很有效的从整个堆中回收垃圾，但是代际假说告诉我们新分配内存的对象也极有可能需要垃圾回收。</p>
<p>副垃圾回收器只从新生代中回收垃圾，幸存的对象总是会被分配到内存页中去。V8 为新生代内存采用了‘半空间’（semi-space）的设计，这意味着为了做疏散（译者注：移动对象）这一步骤（evacuation step），有一半的内存空间是空闲的。在清理时，初始的空闲区域称之为“To-Space”，复制对象过来的区域称之为“From-Space”；在最坏的情况下，如果每一个对象在清理的时候存活了下来，那我们就要复制每一个对象。</p>
<p>对于清理，我们会维护一个额外的根集（root set），这个根集里会存放一些从旧到新的引用。这些引用是在旧空间（old-space）中指向新生代中对象的指针。我们使用“写屏障（write barriers）”来维护从旧到新的引用列表，而不是跟踪整个堆中的每一个对象变更。当堆和全局对象结合使用时，我们知道每一个在新生代中对象的引用，而无需追踪整个老生代。</p>
<p>疏散步骤将所有的活动对象移动到连续的一块内存中，这样做的好处就是完全移除内存碎片（清理非活动对象时留下的内存碎片）；然后我们把两块内存空间互换，即把 ‘To-Space’ 变成 ‘From-Space’，反之亦然。一旦垃圾回收完成，新分配的内存空间将从 ‘From-Space’ 下一个空闲内存地址开始。</p>
<p>副垃圾回收器移动活动对象到一个新的内存页:</p>
<img src="https://v8.js.cn/_img/trash-talk/03.svg" />
<p>如果仅仅是凭借这一策略，我们就会很快的耗尽新生代的内存空间；为了新生代的内存空间不被耗尽，在下一次垃圾回收的时候，我们会把活动对象移动（evacuate）到老生代，而不是 ‘To-Space’。</p>
<p>清理的最后一步是把移动后的对象的指针地址更新，每一个被复制对象都会留下一个转发地址（forwarding-address），用于更新指针以指向新的地址。</p>
<p>副垃圾回收器移动 ‘intermediate’ 子代的活动对象到老生代:</p>
<img src="https://v8.js.cn/_img/trash-talk/04.svg" />
<p>副垃圾回收器在清理时，实际上执行三个步骤：标记，移动活动对象，和更新对象的指针；这些都是交错进行，而不是在不同阶段。</p>
<h3>Orinoco</h3>
<p>这些算法和优化在很多垃圾回收相关的文献或着具有垃圾回收机制的编程语言中都是非常常见的，但是这些先进的垃圾回收机制已经经过了漫长发展。测量垃圾回收所花费时间的一个重要指标就是执行垃圾回收时主线程挂起的时间。对于传统的 ‘stop-the-world’ 垃圾回收器来说，垃圾回收所花费的时间可以直接简单相加。而这种垃圾回收的方式直接影响了用户体验，会直接导致页面卡顿，渲染延迟等一系列问题。</p>
<p>V8 垃圾回收器 Orinoco 的 LOGO:</p>
<img src="https://v8.js.cn/_img/trash-talk/05.svg" />
<p>Orinoco 是 V8 垃圾回收器项目的代号，它利用最新的和最好的垃圾回收技术来降低主线程挂起的时间， 比如：并行（parallel）垃圾回收，增量（incremental）垃圾回收和并发（concurrent）垃圾回收。这里有一些术语在垃圾回收的上下文中有特定的含义，所以这是值得去详细的探讨的。</p>
<h4>并行垃圾回收</h4>
<p>并行是主线程和协助线程同时执行同样的工作，但是这任然是一种‘stop-the-world’的垃圾回收方式，但是垃圾回收所消耗的时间等于总时间除以参与的线程数量(加上一些同步开销)。这是这三种技术中最简单的 JavaScript 垃圾回收方式；因为没有 JavaScript 的执行，因此只需要确保同时只有一个协助线程在访问对象就好了。</p>
<p>主线程和协助线程同在一时间做同样的任务:</p>
<img src="https://v8.js.cn/_img/trash-talk/06.svg" />
<h4>增量垃圾回收</h4>
<p>增量式垃圾回收是主线程间歇性的去做少量的垃圾回收的方式。我们不会在增量式垃圾回收的时候执行整个垃圾回收的过程，只要整个垃圾回收过程的一小部分工作。做这样的工作是极其困难的，因为 JavaScript 也在做增量式垃圾回收的时候同时执行，这意味着堆的状态已经发生了变化，这可能会导导致之前的增量回收工作完全无效。从图中可以看出并没有减少主线程暂停的时间（事实上，通常会略微增加），只会随着时间的推移而增长。但这仍然是解决问题的好办法，通过 JavaScript 间歇性的执行，同时也间歇性的去做垃圾回收工作，JavaScript 的执行仍然可以在用户输入或者执行动画的时候得到及时的响应。</p>
<p>垃圾回收任务交错的进入主线程执行:</p>
<img src="https://v8.js.cn/_img/trash-talk/06.svg" />
<h4>并发垃圾回收</h4>
<p>并发是主线程一直执行 JavaScript，而辅助线程在后台完全的执行垃圾回收。这种方式是三种技术中最难的一种，JavaScript 堆里面的内容随时都可能发生变化，从而使之前做的工作完全无效。最重要的是，现在有读/写竞争（read/write races），主线程和辅助线程极有可能在同一时间去更改同一个对象。这种方式的优势也非常明显，主线程不会被挂起，JavaScript 可以自由的执行，尽量为了保证同一对象同一时间只在一个辅助线程在修改而带来的一些同步开销。</p>
<p>垃圾回收任务完全发生在后台，主线程可以自由的执行 JavaScript:</p>
<img src="https://v8.js.cn/_img/trash-talk/07.svg" />
<h3>V8 当前使用的机制</h3>
<br />
<h4>Scavenging</h4>
<p>现今，V8 在新生代垃圾回收中使用并行清理，每个协助线程会将所有的活动对象都移动到 ‘To-Space’。在每一次尝试将活动对象移动到 ‘To-Space’ 的时候必须通确保原子化的读和写以及比较和交换操作。不同的协助线程都有可能通过不同的路径找到相同的对象，并尝试将这个对象移动到 ‘To-Space’；无论哪个协助线程成功移动对象到 ‘To-Space’，都必须更新这个对象的指针，并且去维护移动这个活动对象所留下的转发地址。以便于其他协助线程可以找到该活动对象更新后的指针。为了快速的给幸存下来的活动对象分配内存，清理任务会使用线程局部分配缓冲区。</p>
<p>并行清理在主线程和多个协助线程之间分配清理任务:</p>
<img src="https://v8.js.cn/_img/trash-talk/08.svg" />
<h4>Major GC</h4>
<p>V8 中的主垃圾回收器主要使用并发标记，一旦堆的动态分配接近极限的时候，将启动并发标记任务。每个辅助线程都会去追踪每个标记到的对象的指针以及对这个对象的引用。在 JavaScript 执行的时候，并发标记在后台进行。写入屏障（write barriers）技术在辅助线程在进行并发标记的时候会一直追踪每一个 JavaScript 对象的新引用。</p>
<p>主垃圾回收器并发的去标记和清除对象，并行的去整理内存和更新活动对象的指针:</p>
<img src="https://v8.js.cn/_img/trash-talk/09.svg" />
<p>当并发标记完成或者动态分配到达极限的时候，主线程会执行最终的快速标记步骤；在这个阶段主线程会被暂停，这段时间也就是主垃圾回收器执行的所有时间。在这个阶段主线程会再一次的扫描根集以确保所有的对象都完成了标记；然后辅助线程就会去做更新指针和整理内存的工作。并非所有的内存页都会被整理，之前提到的加入到空闲列表的内存页就不会被整理。在暂停的时候主线程会启动并发清理的任务，这些任务都是并发执行的，并不会影响并行内存页的整理工作和 JavaScript 的执行。</p>
<h4>空闲时垃圾回收器</h4>
<p>JavaScript 是无法去直接访问垃圾回收器的，这些都是在 V8 的实现中已经定义好的。但是 V8 确实提供了一种机制让 Embedders（嵌入 V8 的环境）去触发垃圾回收，即便 JavaScript 本身不能直接去触发垃圾回收。垃圾回收器会发布一些 “空闲时任务（Idle Tasks）”，虽然这些任务都是可选的，但最终这些任务会被触发。像 Chrome 这些嵌入了 V8 的环境会有一些空闲时间的概念。比如：在 Chrome 中，以每秒 60 帧的速度去执行一些动画，浏览器大约有 16.6 毫秒的时间去渲染动画的每一帧，如果动画提前完成，那么 Chrome 在下一帧之前的空闲时间去触发垃圾回收器发布的空闲时任务。</p>
<p>空闲时垃圾回收器，利用主线程上的空闲时间主动的去执行垃圾回收工作:</p>
<img src="https://v8.js.cn/_img/trash-talk/10.svg" />
<h2>参考</h2>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_management">JS 内存管理</a><br /><br>
<a href="https://juejin.cn/post/7083477868508315684">JS 内存泄漏和垃圾回收机制</a><br /><br>
<a href="https://github.com/zhansingsong/js-leakage-patterns/blob/master/%E5%B8%B8%E8%A7%81%E7%9A%84JavaScript%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2/%E5%B8%B8%E8%A7%81%E7%9A%84JavaScript%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2.md">常见的 JavaScript 内存泄漏</a><br /><br>
<a href="https://v8.js.cn/blog/trash-talk/">谈谈 GC：新的 Orinoco 垃圾收集器</a></p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[包管理器]]></title>
            <link>https://leetme.netlify.app/posts/package-manager</link>
            <guid>https://leetme.netlify.app/posts/package-manager</guid>
            <pubDate>Wed, 20 Dec 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<p>现如今创建的项目基本上都是基于<code>React</code>，<code>Vue</code>。所以必然离不开<code>npm</code>这个包管理器，其优秀的 包版本管理机制承载了整个繁荣发展的Node.js社区，理解其内部机制非常有利于加深我们对模块开发的理解、各项前端工程化的配置以加快我们排查问题（相信不少同学收到过各种依赖问题的困扰）的速度。</p>
<h2>package.json</h2>
<p>在 node.js 中，模块是一个库或者框架，也是一个 node.js 项目。node.js 项目遵循模块化的架构，当我们创建了一个项目，意味着创建了一个模块，而这个模块必须有一个描述文件，即<code>package.json</code>。一个合理的<code>package.json</code>配置文件能直接决定我们项目的质量。</p>
<h3>必备属性</h3>
<p><code>package.json</code>中有多个属性，其中<code>name</code>, <code>version</code>是必填的，这两个属性组成一个<code>npm</code>模块的唯一标识。</p>
<h4>npm 包命名规范</h4>
<p><code>name</code>在命名时需要遵循官方的规范和建议:</p>
<ul>
<li>包名会成为模块 url、命令行中的参数或者一个文件夹名称，所以非 url 安全的字符都不能使用，我们可以使用<code>validate-npm-package-name</code>包来检测名称是否合法</li>
<li>语义化，能帮助开发者更快找到包</li>
<li>若包名存在一些符号，将符号去除后不得与现有包名重复
<blockquote>
<p><code>react-native</code>存在即不能创建<code>reactnative</code></p>
</blockquote>
</li>
<li>如果包名和现有的包名太相近导致不能发布，你可以发布到自己的作用域下
<blockquote>
<p>比如<code>leet</code>, 那么发布的包可以说<code>@leet/hooks</code></p>
</blockquote>
</li>
</ul>
<h4>查看包是否被占用</h4>
<p>控制台执行<code>npm view [packageName]</code>可查看包是否被占用，若不存在被占用则会显示<code>404</code>。</p>
<h3>描述信息</h3>
<br/>
<h4>基本描述</h4>
<pre><code class="language-json">{
  &quot;description&quot;: &quot;Some descriptions of the module&quot;,
  &quot;keywords&quot;: [&quot;component&quot;, &quot;design&quot;, &quot;framework&quot;, &quot;frontend&quot;, &quot;react&quot;, &quot;ui&quot;]
}
</code></pre>
<p>基本描述有利于其他人了解你的模块，并且有助于检索模块。当你使用<code>npm search</code>时会和 <code>description</code> 和 <code>keywords</code> 进行匹配。有点类似于<code>html</code>的<code>&lt;meta name=&quot;description&quot;&gt;</code>和<code>&lt;meta name=&quot;keyword&quot;&gt;</code>，有利于搜索引擎的检索。</p>
<h4>开发人员</h4>
<p>包含<code>author</code>和<code>contributors</code>，字面意思就是作者和贡献者，作者只有一个人，贡献者包含多个人。</p>
<pre><code class="language-json">// 每个人员信息包含下面三个字段或者时一个字符串描述
{
  &quot;author&quot;: {
    &quot;name&quot;: &quot;Leet&quot;,
    &quot;email&quot;: &quot;1414395519@qq.com&quot;,
    &quot;url&quot;: &quot;https://github.com/skyline523&quot;
  },
  &quot;contributors&quot;: [
    &quot;a contributor ......&quot;,
    {
      &quot;name&quot;: &quot;Leet&quot;,
      &quot;email&quot;: &quot;1414395519@qq.com&quot;,
      &quot;url&quot;: &quot;https://github.com/skyline523&quot;
    }
  ]
}
</code></pre>
<h4>地址</h4>
<pre><code class="language-json">{
  // 指定模块的主页
  &quot;homepage&quot;: &quot;https://react.dev/&quot;,
  // 指定一个地址或邮箱，其他开发者可以向你提问
  &quot;bugs&quot;: {
    &quot;url&quot;: &quot;https://github.com/facebook/react/issues&quot;
  },
  // 指定模块的代码仓库
  &quot;repository&quot;: {
    &quot;type&quot;: &quot;git&quot;,
    &quot;url&quot;: &quot;https://github.com/facebook/react&quot;
  }
}
</code></pre>
<h3>依赖配置</h3>
<p>我们的项目可能会依赖其他多个外部依赖包，根据依赖包的不同途径可分为以下几个:</p>
<ul>
<li>dependencies</li>
<li>devDependencies</li>
<li>peerDependencies</li>
<li>bundleDependencies</li>
<li>optionalDependencies</li>
</ul>
<h4>配置规则</h4>
<p>你看到的依赖包配置可能有以下几种情况：</p>
<pre><code class="language-json">{
  &quot;axios&quot;: &quot;^1.2.0&quot;,
  &quot;antd&quot;: &quot;ant-design/ant-design#4.0.0-alpha.8&quot;,
  &quot;test-js&quot;: &quot;file:../test&quot;,
  &quot;test2-js&quot;: &quot;http://cdn.com/test2-js.tar.gz&quot;,
  &quot;core-js&quot;: &quot;^1.1.5&quot;
}
</code></pre>
<ul>
<li><code>NAME:VERSION</code> 是一个遵循 <a href="https://semver.org/lang/zh-CN/">SemVer 规范</a> 的版本号配置，<code>npm install</code> 时将到 npm 服务器下载符合指定版本范围的包</li>
<li><code>NAME:DOWNLOAD_URL</code> 是一个可下载的 <code>tarball</code> 压缩包地址，模块安装时会把这个 <code>.tar</code> 安装到本地</li>
<li><code>NAME:LOCAL_PATH</code> 是一个本地的依赖包路径，适用于在本地测试一个 npm 包，但不适用于线上</li>
<li><code>NAME:GITHUB_URL</code> 即 github 的 <code>username/module_name</code> 的写法，比如 <code>ant-design/ant-design</code>, 你还可以在后面指定 tag 和 commit_id</li>
<li><code>NAME:GIT_URL</code> 即通过<code>git clone [url]</code>克隆代码的url</li>
</ul>
<p>::: tip<br>
<code>tarball</code> 是一组打包成单个文件的文件，然后使用 <code>gzip</code> 压缩程序进行压缩。</p>
<p><code>git clone [url]</code> 中的 url 遵循以下形式:</p>
<pre><code>&lt;protocol&gt;://[&lt;user&gt;[:&lt;password&gt;]@]&lt;hostname&gt;[:&lt;port&gt;][:][/]&lt;path&gt;[#&lt;commit-ish&gt; | #semver:&lt;semver&gt;]

比如：

git://github.com/user/project.git#commit-ish
git+ssh://user@hostname:project.git#commit-ish
git+ssh://user@hostname/project.git#commit-ish
git+http://user@hostname/project/blah.git#commit-ish
git+https://user@hostname/project/blah.git#commit-ish
</code></pre>
<p>:::</p>
<h4><code>dependencies</code></h4>
<p><code>dependencies</code>字段指定了项目运行所需依赖的模块。安装依赖是使用<code>--save</code>参数可将该模块写入到 dependencies 属性，安装依赖是默认安装到 dependencies，故可不用添加该参数。</p>
<pre><code>&quot;dependencies&quot;: {
  &quot;vite&quot;: &quot;^3.3.2&quot;
}
</code></pre>
<p>::: tip 版本说明<br>
上面有说过版本号是遵循 <a href="https://semver.org/lang/zh-CN/">SemVer 规范</a> 的，这里还是提出几个常用写法的意思：</p>
<ul>
<li><code>3.3.2</code>: 只安装指定版本</li>
<li><code>~3.3.2</code>: 表示安装 3.3.x 的最新版本</li>
<li><code>^3.3.2</code>: 表示安装 3.x.x 的最新版本</li>
<li><code>latest</code>: 表示安装最新版本</li>
</ul>
<p>:::</p>
<h4><code>devDependencies</code></h4>
<p><code>devDependencies</code> 字段指定了项目开发所需依赖的模块。有一些包有可能你只是在开发环境中用到，例如拟用于检测代码规范的 <code>eslint</code>, 用于进行测试的 <code>vitest</code>。 用户不安装这些依赖也能正常运行项目。安装依赖是使用 <code>--save-dev</code> 或 <code>-D</code> 参数可将该模块写入到 <code>devDependencies</code>属性</p>
<pre><code>&quot;devDependencies&quot;: {
  &quot;eslint&quot;: &quot;^8.56.0&quot;,
  &quot;vitest&quot;: &quot;latest&quot;
}
</code></pre>
<h4><code>peerDependencies</code></h4>
<p>当我们开发一个模块的时候，如果当前模块与所依赖的模块同时依赖一个第三方模块，并且依赖的是两个不兼容的版本时就会出现问题。<code>peerDependencies</code>用于指定你正在开发的模块所依赖的版本以及用户安装的依赖包版本的兼容性。这部分虽然平时开发项目不常用，但开发需要发布的 npm 包可能需要用上，而且并不太好理解。</p>
<p>它的几个作用点：</p>
<ul>
<li>插件正确运行的前提是，核心依赖库必须先下载安装，不能脱离核心依赖库而被单独依赖并引用</li>
<li>插件入口 api 的设计必须要符合核心依赖库的规范</li>
<li>插件的核心逻辑运行在依赖库的调用中</li>
<li>在项目实践中，同一插件体系下，核心依赖库版本最好是相同的</li>
</ul>
<p>假设现在有一个<code>工程A</code>，已经在其<code>package.json</code>的<code>dependencies</code>中声明了<code>packageA</code>， 有两个插件<code>插件1</code>和<code>插件2</code>他们也依赖<code>packageA</code>，如果在插件中使用<code>dependencies</code>而不是<code>peerDependencies</code>来声明<code>packageA</code>，那么在安装完依赖后的依赖图是这样的：</p>
<pre><code>├── 工程A
│   └── node_modules
│       ├── packageA
│       ├── 插件1
│       │   └── nodule_modules
│       │       └── packageA
│       └── 插件2
│       │   └── nodule_modules
│       │       └── packageA
</code></pre>
<p>显而易见，<code>packageA</code>安装了三次，有两次安装是冗余的。</p>
<p>如果使用<code>peerDependencies</code>来声明<code>插件1</code>和<code>插件2</code>的依赖:</p>
<pre><code class="language-json">{
  &quot;peerDependencies&quot;: {
    &quot;packageA&quot;: &quot;1.0.1&quot;
  }
}
</code></pre>
<p>那么在安装完依赖后的依赖图是这样的：</p>
<pre><code>├── 工程A
│   └── node_modules
│       ├── packageA
│       ├── 插件1
│       └── 插件2
</code></pre>
<ul>
<li>如果用户显式依赖了核心库，则可以忽略各插件的 <code>peerDependency</code> 声明</li>
<li>如果用户没有显式依赖核心库，则按照插件 <code>peerDependencies</code> 中声明的版本将库安装到项目根目录中</li>
<li>当用户依赖的版本、各插件依赖的版本之间不相互兼容，会报错让用户自行修复</li>
</ul>
<h4><code>optionalDependencies</code></h4>
<p>在某些场景下，依赖包可能不是强依赖，这个依赖包可有可无，当这个依赖包无法被获取时，你希望<code>npm install</code>继续运行，而不会导致失败，你可以将依赖放到<code>optionalDependencies</code>中。</p>
<p>::: tip<br>
<code>optionalDependencies</code>中的配置将会覆盖掉<code>dependencies</code>，所以只需在一个地方进行配置</p>
<p>引用了<code>optionalDependencies</code>中安装的依赖时，需要做好异常处理，否者在模块获取不到时会报错<br>
:::</p>
<h4><code>bundledDependencies</code></h4>
<p><code>bundledDependencies</code> 的值是一个数组，数组里可以指定一些模块，这些模块将在这个包发布时被一起打包。</p>
<pre><code>&quot;bundledDependencies&quot;: [&quot;package1&quot; , &quot;package2&quot;]
</code></pre>
<h3>协议</h3>
<pre><code class="language-json">{
  &quot;license&quot;: &quot;MIT&quot;
}
</code></pre>
<p><code>license</code> 字段用于指定软件的开源协议，开源协议里面详尽表述了其他人获得你代码后拥有的权利，可以对你的的代码进行何种操作，何种操作又是被禁止的。同一款协议有很多变种，协议太宽松会导致作者丧失对作品的很多权利，太严格又不便于使用者使用及作品的传播，所以开源作者要考虑自己对作品想保留哪些权利，放开哪些限制。</p>
<blockquote>
<p>软件协议可分为开源和商业两类，对于商业协议，或者叫法律声明、许可协议，每个软件会有自己的一套行文，由软件作者或专门律师撰写，对于大多数人来说不必自己花时间和精力去写繁长的许可协议，选择一份广为流传的开源协议就是个不错的选择。</p>
</blockquote>
<ul>
<li><code>MIT</code> 只要用户在项目副本中包含了版权声明和许可声明，他们就可以拿你的代码做任何想做的事情，你也无需承担任何责任</li>
<li><code>Apache</code> 类似于 <code>MIT</code>，同时还包含了贡献者向用户提供专利授权相关的条款</li>
<li><code>GPL</code> 修改项目代码的用户再次分发源码或二进制代码时，必须公布他的相关修改</li>
</ul>
<h3>目录、文件相关</h3>
<h4><code>main</code></h4>
<pre><code class="language-json">{
  &quot;main&quot;: &quot;lib/index.js&quot;
}
</code></pre>
<p><code>main</code>可以指定程序的主入口文件，例如<code>element-plus</code>的模块入口是<code>lib/index.js</code>，当我们引入<code>import { el-button } from 'element-plus</code>，实际上就是<code>lib/index.js</code>中暴露出去的模块。</p>
<h4><code>bin</code></h4>
<p>许多<code>package</code>都有一个或多个可执行文件，它们希望将其安装到 <code>PATH</code> 中。npm 使这变得非常简单（事实上，它使用此功能来安装“npm”可执行文件。）</p>
<p>要使用它，请在 <code>package.json</code> 中提供一个 <code>bin</code> 字段，它是命令名到本地文件名的映射。当此软件包全局安装时，该文件将链接到全局 bins 目录内，或者将创建一个 cmd（Windows 命令文件）来执行 <code>bin</code> 字段中的指定文件，因此可以按<code>name</code>或<code>name.cmd</code>（在 Windows PowerShell 上）运行。当此包作为另一个包中的依赖项安装时，该文件将被链接到该包可以直接通过 <code>npm exec</code> 或通过 <code>npm run-script</code> 调用其他脚本时通过名称来访问该文件。</p>
<pre><code class="language-json">{
  &quot;bin&quot;: {
    &quot;myapp&quot;: &quot;./cli.js&quot;
  }
}
</code></pre>
<p>因此，当您安装 myapp 时，如果是类 Unix 操作系统，它将创建一个从 cli.js 脚本到 <code>/usr/local/bin/myapp</code> 的符号链接，如果是 Windows，它将创建一个 cmd 文件，通常位于 <code>C:\Users\{username}\AppData\Roaming\npm\myapp.cmd</code> 运行 cli.js 脚本。</p>
<p>如果您有一个可执行文件，并且其名称应该是包的名称，那么您可以将其作为字符串提供。例如：</p>
<pre><code>{
  &quot;name&quot;: &quot;my-program&quot;,
  &quot;version&quot;: &quot;1.2.5&quot;,
  &quot;bin&quot;: &quot;./path/to/program&quot;
}

// 等同于
{
  &quot;name&quot;: &quot;my-program&quot;,
  &quot;version&quot;: &quot;1.2.5&quot;,
  &quot;bin&quot;: {
    &quot;my-program&quot;: &quot;./path/to/program&quot;
  }
}
</code></pre>
<p>请确保 <code>bin</code> 中引用的文件以 <code>#!/usr/bin/env</code> 节点开头，否则脚本将在没有节点可执行文件的情况下启动！</p>
<h4><code>files</code></h4>
<pre><code class="language-json">{
  &quot;files&quot;: [&quot;dist&quot;, &quot;lib&quot;, &quot;es&quot;]
}
</code></pre>
<p>可选<code>files</code>字段是一个数组，描述当您的包作为依赖项 <code>npm publish</code> 推送到<code>npm</code>服务器文件列表。<code>files</code>遵循与 .gitignore 类似的语法，但相反：包含文件、目录或glob模式（<em>、**/</em> 等）将使文件在打包时包含在tarball中。省略该字段将使其默认为 [&quot;*&quot;]，这意味着它将包含所有文件。</p>
<p>一些特殊的文件和目录也会被包含或排除，无论它们是否存在于文件数组中（见下文）。</p>
<p>您还可以在包的根目录或子目录中提供 .npmignore 文件，这将防止包含文件。在包的根目录中，它不会覆盖“文件”字段，但在子目录中它会覆盖。 .npmignore 文件的工作方式与 .gitignore 类似。如果存在 .gitignore 文件，并且缺少 .npmignore，则将使用 .gitignore 的内容。</p>
<p>始终包含的文件:</p>
<ul>
<li><code>package.json</code></li>
<li><code>README</code></li>
<li><code>LICENSE</code> / <code>LICENCE</code></li>
<li>The file in the &quot;main&quot; field</li>
<li>The file(s) in the &quot;bin&quot; field</li>
</ul>
<p>始终不包含的文件:</p>
<ul>
<li>*.orig</li>
<li>.*.swp</li>
<li>.DS_Store</li>
<li>._*</li>
<li>.git</li>
<li>.npmrc</li>
<li>.hg</li>
<li>.lock-wscript</li>
<li>.npmrc</li>
<li>.svn</li>
<li>.wafpickle-N</li>
<li>CVS</li>
<li>config.gypi</li>
<li>node_modules</li>
<li>npm-debug.log</li>
<li>package-lock.json (如果你希望发布，请使用<code>npm-shrinkwrap.json</code>)</li>
<li>pnpm-lock.yaml</li>
<li>yarn.lock</li>
</ul>
<h4><code>man</code></h4>
<p><code>man</code> 命令是 Linux 下的帮助指令, 通过 <code>man</code> 指令可以查看 Linux 中的指令帮助、配置文件帮助和编程帮助等信息。</p>
<blockquote>
<p>这个配置在通常开发下使用较少，不过多赘述，可以前往 <a href="https://docs.npmjs.com/cli/v10/configuring-npm/package-json#man">npm pacakge.json</a> 查看文档</p>
</blockquote>
<h4><code>directories</code></h4>
<p>CommonJS 规范详细介绍了几种使用目录对象指示包结构的方法。如果你查看 npm 的 package.json，你会发现它有 doc、lib 和 man 目录。</p>
<p>将来，这些信息可能会以其他创造性的方式使用。</p>
<blockquote>
<p>目前作用不大也不过多赘述</p>
</blockquote>
<h3>脚本配置</h3>
<h4><code>scripts</code></h4>
<p>package.json 文件的<code>scripts</code>属性支持许多内置脚本及其预设生命周期事件以及任意脚本。这些都可以通过运行 <code>npm run-script &lt;stage&gt;</code> 或简称 <code>npm run &lt;stage&gt;</code> 来执行。具有匹配名称的前置和后置命令也将为这些命令运行（例如 premyscript、myscript、postmyscript）。依赖项中的脚本可以使用 <code>npm explore &lt;pkg&gt; -- npm run &lt;stage&gt;</code> 运行。</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;jest --config .jest.js --no-cache&quot;,
    &quot;dist&quot;: &quot;antd-tools run dist&quot;,
    &quot;compile&quot;: &quot;antd-tools run compile&quot;,
    &quot;build&quot;: &quot;npm run compile &amp;&amp; npm run dist&quot;
  }
}
</code></pre>
<h4><code>config</code></h4>
<p><code>config</code> 字段用于配置脚本中使用的环境变量，例如下面的配置，可以在脚本中使用 <code>process.env.npm_package_config_port</code> 进行获取。</p>
<pre><code class="language-json">{
  &quot;config&quot;: {
    &quot;port&quot;: &quot;8080&quot;
  }
}
</code></pre>
<h3>发布配置</h3>
<h4><code>preferGlobal</code></h4>
<p>如果你的 node.js 模块主要用于安装到全局的命令行工具，那么该值设置为<code>true</code>，当用户将该模块安装到本地时，将得到一个警告。这个配置并不会阻止用户安装，而是会提示用户防止错误使用而引发一些问题。</p>
<h4><code>private</code></h4>
<p>如果将 private 属性设置为 <code>true</code>，npm将拒绝发布它，这是为了防止一个私有模块被无意间发布出去。</p>
<h4><code>publishConfig</code></h4>
<p>发布模块时更详细的配置，例如你可以配置只发布某个<code>tag</code>、配置发布到的私有<code>npm</code>源。更详细的配置可以参考<a href="https://docs.npmjs.com/cli/v10/using-npm/config">npm-config</a></p>
<h4><code>os</code></h4>
<p>假如你开发了一个模块，只能跑在<code>darwin</code>系统下，你需要保证<code>windows</code>用户不会安装到你的模块，从而避免发生不必要的错误。</p>
<p>使用<code>os</code>属性可以帮助你完成以上的需求，你可以指定你的模块只能被安装在某些系统下，或者指定一个不能安装的系统黑名单：</p>
<pre><code>&quot;os&quot; : [ &quot;darwin&quot;, &quot;linux&quot; ]
&quot;os&quot; : [ &quot;!win32&quot; ]
</code></pre>
<blockquote>
<p>在 node 环境下可以使用 process.platform 来判断操作系统</p>
</blockquote>
<h4><code>cpu</code></h4>
<p>和上面的<code>os</code>类似，我们可以用<code>cpu</code>属性更精准的限制用户安装环境：</p>
<pre><code>&quot;cpu&quot; : [ &quot;x64&quot;, &quot;ia32&quot; ]
&quot;cpu&quot; : [ &quot;!arm&quot;, &quot;!mips&quot; ]
</code></pre>
<blockquote>
<p>在 node 环境下可以使用 process.arch 来判断 cpu 架构</p>
</blockquote>
<h2>包版本管理机制</h2>
<p><code>nodejs</code>离不开<code>npm</code>优秀的依赖管理系统。在介绍整个依赖系统之前，必须要了解 npm 如何管理依赖包的版本。</p>
<h3>查看 npm 包版本</h3>
<ul>
<li><code>npm view &lt;pkg&gt; version</code>查看<code>&lt;pkg&gt;</code>的最新版本</li>
<li><code>npm view &lt;pkg&gt; versions</code>查看<code>&lt;pkg&gt;</code>在 npm 服务器上所有发布过的版本</li>
<li><code>npm ls</code>查看当前仓库依赖树上所有包的版本信息</li>
</ul>
<h3>SemVer 规范</h3>
<p>npm 包中的模块版本都需要遵循<code>SemVer</code>规范，是由<code>Github</code>起草的一个具有指定意义的，统一的版本号表示规则。（Sem[antic] Ver[sion]）语义化版本的缩写。</p>
<blockquote>
<p>详细规范请查看<a href="https://semver.org/">SemVer 规范官网</a></p>
</blockquote>
<h4>标准版本</h4>
<p><code>SemVer</code>规范的标准版本号采用<code>x.y.z</code>的格式，每位都是非负的整数，且禁止在数字前方补零。</p>
<ul>
<li><code>x</code>: 主版本号(major)，当你做了不兼容的 API 修改</li>
<li><code>y</code>: 次版本号(minor)，当你做了向下兼容的功能新新增</li>
<li><code>z</code>: 修订号(patch)，当你做了向下兼容的问题修正</li>
</ul>
<h4>先行版本</h4>
<p>当某个版本改动比较大，并非稳定且可能无法满足预期的兼容性需求时，你可能要先发布一个现行版本。</p>
<p>现行版本号可以加到标准版本号的后面，先加上一个连接号再加上一连串以句点分割的标识符和版本编译信息。</p>
<p>比如<code>vitepress</code>的版本号：</p>
<!-- <ZoomImg
  src="https://leetme.netlify.app/assets/articles/engineering/vitepress_versions.png"
  desc="vitepress部分版本号"
/> -->
<p>能看出常用的关键字:</p>
<ul>
<li><code>alpha</code>内部版本</li>
<li><code>draft</code>草稿版本</li>
<li><code>beta</code>公测版本</li>
<li><code>rc(release candidate)</code>正式版本的候选版本</li>
</ul>
<h4>发布版本</h4>
<p>在修改<code>npm</code>包某些功能后通常需要发布一个新的版本，我们通常的做法就是直接修改<code>package.json</code>到指定版本。如果操作失误，很容易造成版本号混乱，我们可以借助符合<code>SemVer</code>规范的命令来完成这一操作:</p>
<ul>
<li><code>npm version patch</code>: 升级修订版本号</li>
<li><code>npm version minor</code>: 升级次版本号</li>
<li><code>npm version major</code>: 升级主版本号</li>
</ul>
<h3>版本工具使用</h3>
<p>如果需要对一些版本号的操作，如果这些版本号符合<code>SemVer</code>规范，我们可以借助<code>semver</code>包来帮助我们进行一些操作</p>
<blockquote>
<p>npm 也使用了该工具来处理版本相关的工作</p>
</blockquote>
<pre><code class="language-shell">npm install semver
</code></pre>
<p>其用法可以查看文档 <a href="https://github.com/npm/node-semver">node-semver</a></p>
<h3>依赖版本管理</h3>
<p>你能看懂下面这些依赖版本的关系吗？</p>
<pre><code>&quot;dependencies&quot;: {
  &quot;@bassist/utils&quot;: &quot;^0.14.0&quot;
},
&quot;devDependencies&quot;: {
  &quot;@mdit-vue/shared&quot;: &quot;^1.0.0&quot;,
  &quot;@types/node&quot;: &quot;^20.9.0&quot;,
  &quot;feed&quot;: &quot;^4.2.2&quot;,
  &quot;medium-zoom&quot;: &quot;^1.0.8&quot;,
  &quot;sass&quot;: &quot;^1.69.5&quot;,
  &quot;vitepress&quot;: &quot;1.0.0-rc.25&quot;,
  &quot;vue&quot;: &quot;^3.3.8&quot;
}
</code></pre>
<p>除了上面三种版本的写法还包含另外一种写法: <code>*</code>。</p>
<ul>
<li>固定版本号: <code>只有版本号</code></li>
<li>任意版本: <code>*</code></li>
<li>匹配主要版本: <code>^</code>
<blockquote>
<p><code>x.y.z</code>，只保持主版本号<code>x</code>不变，<code>y</code>和<code>z</code>保持最新版本</p>
</blockquote>
</li>
<li>匹配主要版本和次要版本: <code>~</code>
<blockquote>
<p><code>x.y.z</code>，只保持修订版本号<code>z</code>为最新版本，<code>y</code>和<code>z</code>保持不变</p>
</blockquote>
</li>
</ul>
<p>::: tip<br>
当主版本号为<code>0</code>的情况，会被认定为不稳定版本，情况有所不同:</p>
<ul>
<li>主版本号和次版本号都为<code>0</code>: <code>~0.0.z</code>, <code>^0.0.z</code>都会被当作固定版本，安装依赖时均不会发生改变</li>
<li>主版本号为<code>0</code>: <code>^0.y.z</code>表现和<code>~0.y.z</code>相同，只保持修订号为最新版本</li>
</ul>
<p>:::</p>
<h3>锁定依赖版本</h3>
<br />
<h4><code>package-lock.json</code></h4>
<blockquote>
<p>项目中使用其他包管理器，<code>lock</code>文件和文件后缀会有所不同，但都是以<code>lock</code>结尾的文件。</p>
</blockquote>
<p>实际开发中，经常会因为各种依赖不一致而产生奇怪的问题，或在某些场景下，不希望依赖被更新，建议在开发中使用<code>package-lock.json</code>。</p>
<p>锁定依赖版本意味着在我们不动手执行更新的情况下，每次安装依赖都会安装固定版本。保证整个团队使用版本号一致的依赖。</p>
<p>每次安装固定版本，无需计算依赖版本范围，大部分场景下能大大加速依赖安装时间。</p>
<blockquote>
<p>使用 package-lock.json 要确保 npm 的版本在 5.6 以上，因为在 5.0 - 5.6 中间，对 package-lock.json 的处理逻辑进行过几次更新，5.6 版本后处理逻辑逐渐稳定。</p>
</blockquote>
<blockquote>
<p>详细结束跳转到<a href="/articles/engineering/package-manager#lock-%E6%96%87%E4%BB%B6">Lock 文件</a></p>
</blockquote>
<h4>定期更新依赖</h4>
<p>我们的目的是保证团队中使用的依赖一致或者稳定，而不是永远不去更新这些依赖。实际开发场景中，我们虽然不需要每次都去安装新版本，但仍然需要定时去升级依赖版本，来让我们享受依赖包升级带来的问题修复、性能提升、新特新更新等。</p>
<!-- <ZoomImg
  src="/assets/articles/engineering/npm-outdated.png"
  desc="npm outdated查看依赖需要更新的列表"
/> -->
<p><code>npm outdated</code> 可以帮助列出有哪些没有升级到最新版本的依赖:</p>
<ul>
<li>黄色表示不符合我们指定的语意化版本范围 - 不需要升级</li>
<li>红色表示符合指定的语意化版本范围 - 需要升级</li>
</ul>
<p><code>npm update</code>会升级所有红色依赖</p>
<p>::: tip<br>
当你的项目选择了其他包管理器时，对应的命令也可能会改变，比如<code>pnpm</code>使用的升级依赖为<code>pnpm up</code>。不知道其他包管理器对应的命令时，请上对应官方文档查看。<br>
:::</p>
<h3>依赖版本选择的最佳实践</h3>
<br />
<h4>版本发布</h4>
<ul>
<li>对外发布一个正式版本的 npm 包时，把它的版本标为<code>1.0.0</code></li>
<li>某个包版本发行后，任何修改都必须以新版本发行</li>
<li>版本号严格按照<code>主版本.此版本.修订号</code>格式</li>
<li>版本号发布必须时严格递增的</li>
<li>发布重大版本或版本改动较大时，先发布<code>alpha\beta\rc</code>等先行版本</li>
</ul>
<br />
<h4>依赖范围选择</h4>
<ul>
<li>主工程依赖了很多子模块，都是团队成员开发的<code>npm</code>包，此时建议把版本前缀改为<code>~</code>，如果锁定的话每次子依赖更新都要对主工程的依赖进行升级，非常繁琐，如果对子依赖完全信任，直接开启<code>^</code>每次升级到最新版本</li>
<li>主工程跑在<code>docker</code>线上，本地还在进行子依赖开发和升级，在<code>docker</code>版本发布前要锁定所有依赖版本，确保本地子依赖发布后线上不会出问题</li>
</ul>
<br />
<h4>保持依赖一致</h4>
<ul>
<li>确保<code>npm</code>的版本在<code>5.6</code>以上，确保默认开启 <code>package-lock.json</code> 文件</li>
<li>由初始化成员执行 <code>npm inatall</code> 后，将 <code>package-lock.json</code> 提交到远程仓库。不要直接提交 <code>node_modules</code> 到远程仓库</li>
<li>定期执行 <code>npm update</code> 升级依赖，并提交 <code>lock</code> 文件确保其他成员同步更新依赖，不要手动更改 <code>lock</code> 文件</li>
</ul>
<h4>依赖变更</h4>
<ul>
<li>升级依赖: 修改 <code>package.json</code> 文件的依赖版本，执行 <code>npm install</code></li>
<li>降级依赖: 直接执行 <code>npm install package@version</code>(改动<code>package.json</code>不会对依赖进行降级)</li>
<li>注意改动依赖后提交<code>lock</code>文件</li>
</ul>
<h2><code>npm install</code> 原理 ~new</h2>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef327ccaba5~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="npm install流程"
/> -->
<h3>嵌套结构</h3>
<p>执行<code>npm install</code>后，依赖包被安装到<code>node_modules</code>。在<code>npm</code>的早期版本，<code>npm</code>处理依赖的方式简单粗暴，以递归的形式，严格按照<code>package.json</code>结构以及子依赖包的<code>package.json</code>结构将依赖安装到它们各自的<code>node_modules</code>中。直到有子依赖包不再依赖其他模块。</p>
<p>比如现在有个模块<code>my-app</code>依赖了两个模块: <code>buffer</code>和<code>ignore</code>，其中<code>ignore</code>不依赖其他模块，<code>buffer</code>依赖<code>base64-js</code>和<code>ieee754</code>:</p>
<p>::: code-group</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;my-app&quot;,
  &quot;dependencies&quot;: {
    &quot;buffer&quot;: &quot;^5.4.3&quot;,
    &quot;ignore&quot;: &quot;^5.1.4&quot;
  }
}
</code></pre>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;buffer&quot;,
  &quot;dependencies&quot;: {
    &quot;base64-js&quot;: &quot;^1.0.2&quot;,
    &quot;ieee754&quot;: &quot;^1.1.4&quot;
  }
}
</code></pre>
<p>:::</p>
<p>那么执行<code>npm install</code>后得到的<code>node_modules</code>中模块目录结构是这样的:</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef33997d7f2~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="嵌套的模块目录结构"
/> -->
<p>如果依赖的模块非常多就会出现这种情况:</p>
<ul>
<li>在不同层级的依赖中，可能引用同一个模块，导致大量冗余</li>
<li>在 <code>Windows</code> 系统中，文件路径最大长度为 260 个字符，嵌套层级过深可能导致不可预知的问题</li>
</ul>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef33d822969~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="多层嵌套的模块目录结构"
/> -->
<h3>扁平结构</h3>
<p>为了解决上面的问题，<code>NPM</code>在<code>3.x</code>版本做了一次较大更新，将嵌套结构改为扁平结构: 不管是直接依赖还是子依赖都优先安装在<code>node_modules</code>根目录。</p>
<p>现在扁平结构下的目录是这样的:</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef3518941f2~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
/> -->
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef3519475d1~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="扁平的模块目录结构"
/> -->
<p>如果我们在<code>my-app</code>中又依赖了<code>base64-js@1.0.1</code>版本:</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;my-app&quot;,
  &quot;dependencies&quot;: {
    &quot;buffer&quot;: &quot;^5.4.3&quot;,
    &quot;ignore&quot;: &quot;^5.1.4&quot;,
    &quot;base64-js&quot;: &quot;1.0.1&quot;
  }
}
</code></pre>
<p><strong><em>当安装到相同模块时，判断已安装的模块版本是否符合新模块的版本范围，如果符合则跳过，不符合则在当前模块的<code>node_modules</code>下安装该模块</em></strong></p>
<p>此时的目录结构为:</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef355ae3b37~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
/> -->
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef35ae17872~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="根模块和子模块都包含同一模块的扁平模块目录结构"
/> -->
<p>如果我们在项目中引用了一个模块，模块的查找流程从最里层找到最外层:</p>
<ul>
<li>在当前模块路径下搜索</li>
<li>在当前模块<code>node_modules</code>路径下搜索</li>
<li>在上级模块的<code>node_modules</code>路径下搜索</li>
<li>...</li>
<li>直到搜索到全局路径中的<code>node_modules</code></li>
</ul>
<p>假设我们又依赖了一个包 buffer2@^5.4.3，而它依赖了包 base64-js@1.0.3，则此时的安装结构是下面这样的：</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef377260d67~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
/> -->
<p>所以<code>npm 3.x</code>并未解决模块冗余问题，甚至会带来新的问题。</p>
<p>假设<code>my-app</code>没有依赖 <code>base64-js@1.0.1</code> 版本，而你同时依赖了依赖不同 <code>base64-js</code> 版本的 <code>buffer</code> 和 <code>buffer2</code>。由于在执行 <code>npm install</code> 的时候，按照 <code>package.json</code> 里依赖的顺序依次解析，则 <code>buffer</code> 和 <code>buffer2</code> 在 <code>package.json</code> 的放置顺序则决定了 <code>node_modules</code> 的依赖结构。</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef3824eba10~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="先安装buffer2的模块目录结构"
/> -->
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef38a55f11e~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="先安装buffer的模块目录结构"
/> -->
<blockquote>
<p>如果还没理解这两个结构是怎么形成的，仔细阅读: <strong><em>当安装到相同模块时，判断已安装的模块版本是否符合新模块的版本范围，如果符合则跳过，不符合则在当前模块的<code>node_modules</code>下安装该模块</em></strong></p>
</blockquote>
<p>另外，为了让开发者在安全的前提下使用最新的依赖包，我们在<code>pacakge.json</code>通常只会锁定大版本即<code>^x.y.z</code>，这意味着在某些依赖包小版本更新后，同样可能造成依赖结构的改动，依赖结构的不确定性可能会给程序带来不可预知的问题。</p>
<h3>Lock 文件</h3>
<p>为了解决<code>npm install</code>的不确定性，在<code>5.x</code>版本新增了<code>package-lock.json</code>文件，而安装方式继续沿用扁平化的方式。</p>
<p><code>package-lock.json</code>的作用时锁定依赖结构，即只要目录下有<code>package-lock.json</code>文件，那么每次执行<code>npm install</code>后生成的<code>node_modules</code>目录结构一定是完全相同的。</p>
<p>以下的依赖结构通过安装依赖后生成的<code>package-lock.json</code>如下:</p>
<p>::: code-group</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;my-app&quot;,
  &quot;dependencies&quot;: {
    &quot;buffer&quot;: &quot;^5.4.3&quot;,
    &quot;ignore&quot;: &quot;^5.1.4&quot;,
    &quot;base64-js&quot;: &quot;1.0.1&quot;
  }
}
</code></pre>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;my-app&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;dependencies&quot;: {
    &quot;base64-js&quot;: {
      &quot;version&quot;: &quot;1.0.1&quot;,
      &quot;resolved&quot;: &quot;https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz&quot;,
      &quot;integrity&quot;: &quot;sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=&quot;
    },
    &quot;buffer&quot;: {
      &quot;version&quot;: &quot;5.4.3&quot;,
      &quot;resolved&quot;: &quot;https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz&quot;,
      &quot;integrity&quot;: &quot;sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==&quot;,
      &quot;requires&quot;: {
        &quot;base64-js&quot;: &quot;^1.0.2&quot;,
        &quot;ieee754&quot;: &quot;^1.1.4&quot;
      },
      &quot;dependencies&quot;: {
        &quot;base64-js&quot;: {
          &quot;version&quot;: &quot;1.3.1&quot;,
          &quot;resolved&quot;: &quot;https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz&quot;,
          &quot;integrity&quot;: &quot;sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==&quot;
        }
      }
    },
    &quot;ieee754&quot;: {
      &quot;version&quot;: &quot;1.1.13&quot;,
      &quot;resolved&quot;: &quot;https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz&quot;,
      &quot;integrity&quot;: &quot;sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==&quot;
    },
    &quot;ignore&quot;: {
      &quot;version&quot;: &quot;5.1.4&quot;,
      &quot;resolved&quot;: &quot;https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz&quot;,
      &quot;integrity&quot;: &quot;sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==&quot;
    }
  }
}
</code></pre>
<p>:::</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef3a81eb51f~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="package-lock.json结构"
/> -->
<p>最外面的两个属性<code>name</code>、<code>version</code>同<code>package.json</code>中的，用以描述当前包名和版本。</p>
<p><code>dependencies</code>是一个对象，对象和<code>node_modules</code>中的包结构一一对应，对象的<code>key</code>为包名，值为包的一些描述信息:</p>
<ul>
<li><code>version</code>: 包版本，当前安装在 node_modules 中的版本</li>
<li><code>resolved</code>: 包具体的安装来源</li>
<li><code>integrity</code>: 包<code>hash</code>值，基于<code>Subresource Integrity</code>来验证已安装的软件包是否被改动过、是否已失效</li>
<li><code>requires</code>: 对应子依赖的依赖，与子依赖的<code>package.json</code>中<code>dependencies</code>的依赖项相同</li>
<li><code>dependencies</code>: 结构和外层的<code>dependencies</code>结构相同，安装在子依赖 node_modules 中的依赖包</li>
</ul>
<blockquote>
<p>并不是所有的子依赖都有<code>dependencies</code>属性，只有子依赖的依赖和当前安装在根目录的<code>node_modules</code>中的依赖冲突之后，才会有这个属性。</p>
</blockquote>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef35ae17872~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="根模块和子模块都包含同一模块的扁平模块目录结构"
/> -->
<p>我们在<code>my-app</code>中依赖的<code>base64-js@1.0.1</code>与<code>buffer</code>中依赖的<code>base64-js@1.0.2</code>发生冲突，所以<code>base64-js@1.0.1</code>需要安装在<code>buffer</code>包的<code>node_modules</code>中，对应了<code>package-lock.json</code>中<code>buffer</code>的<code>dependencies</code>属性。这也对应了<code>npm</code>对依赖的扁平化处理方式</p>
<p>所以，根据上面的分析， <code>package-lock.json</code> 文件 和 <code>node_modules</code> 目录结构是一一对应的，即项目目录下存在 <code>package-lock.json</code> 可以让每次安装生成的依赖目录结构保持相同。</p>
<p>另外，项目中使用了 <code>package-lock.json</code> 可以显著加速依赖安装时间。</p>
<p>我们使用<code> npm i --timing=true --loglevel=verbose</code> 命令可以看到 <code>npm install</code> 的完整过程，下面我们来对比下使用 <code>lock</code> 文件和不使用 <code>lock</code> 文件的差别。在对比前先清理下<code>npm</code>缓存。</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef39713273a~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="不使用lock文件"
/> -->
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef3b5e532e0~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="使用lock文件"
/> -->
<p>可见， <code>package-lock.json</code> 中已经缓存了每个包的具体版本和下载链接，不需要再去远程仓库进行查询，然后直接进入文件完整性校验环节，减少了大量网络请求。</p>
<p>::: info 使用建议</p>
<p>在开发系统应用时，建议把<code>package-lock.json</code>文件提交到代码仓库，从而保证团队开发者以及<code>CI</code>环节可以执行<code>npm install</code>时安装的依赖版本都是一致的。</p>
<p>在开发一个<code>npm</code>包 时，你的<code>npm</code>包 是需要被其他仓库依赖的，由于上面我们讲到的扁平安装机制，如果你锁定了依赖包版本，你的依赖包就不能和其他依赖包共享同一 <code>semver</code> 范围内的依赖包，这样会造成不必要的冗余。所以我们不应该把<code>package-lock.json</code>文件发布出去（<code>npm</code>默认也不会把<code>package-lock.json</code>文件发布出去）。</p>
<p>:::</p>
<h3>缓存</h3>
<p>在执行<code>npm install</code>或<code>npm update</code>下载依赖后，除了将安装包安装在<code>node_modules</code>目录下，还会在本地缓存一份。</p>
<p>通过 <code>npm config get cache</code> 命令可以查询到：在 Linux 或 Mac 默认是用户主目录下的 <code>.npm/_cacache</code> 目录。</p>
<p>在这个目录下又存在两个目录：<code>content-v2</code>、<code>index-v5</code>，<code>content-v2</code>目录用于存储<code>tar</code>包的缓存，而<code>index-v5</code>目录用于存储<code>tar</code>包的<code>hash</code>。</p>
<p>npm 在执行安装时，可以根据 <code>package-lock.json</code> 中存储的 <code>integrity</code>、<code>version</code>、<code>name</code> 生成一个唯一的 <code>key</code> 对应到 <code>index-v5</code> 目录下的缓存记录，从而找到<code>tar</code>包的 <code>hash</code>，然后根据 <code>hash</code> 再去找缓存的 <code>tar</code> 包直接使用。</p>
<pre><code class="language-shell">grep &quot;https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz&quot; -r index-v5
</code></pre>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef3b8fb68f5~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
/> -->
<pre><code class="language-json">{
  &quot;key&quot;: &quot;pacote:version-manifest:https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz:sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=&quot;,
  &quot;integrity&quot;: &quot;sha512-C2EkHXwXvLsbrucJTRS3xFHv7Mf/y9klmKDxPTE8yevCoH5h8Ae69Y+/lP+ahpW91crnzgO78elOk2E6APJfIQ==&quot;,
  &quot;time&quot;: 1575554308857,
  &quot;size&quot;: 1,
  &quot;metadata&quot;: {
    &quot;id&quot;: &quot;base64-js@1.0.1&quot;,
    &quot;manifest&quot;: {
      &quot;name&quot;: &quot;base64-js&quot;,
      &quot;version&quot;: &quot;1.0.1&quot;,
      &quot;engines&quot;: {
        &quot;node&quot;: &quot;&gt;= 0.4&quot;
      },
      &quot;dependencies&quot;: {},
      &quot;optionalDependencies&quot;: {},
      &quot;devDependencies&quot;: {
        &quot;standard&quot;: &quot;^5.2.2&quot;,
        &quot;tape&quot;: &quot;4.x&quot;
      },
      &quot;bundleDependencies&quot;: false,
      &quot;peerDependencies&quot;: {},
      &quot;deprecated&quot;: false,
      &quot;_resolved&quot;: &quot;https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz&quot;,
      &quot;_integrity&quot;: &quot;sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=&quot;,
      &quot;_shasum&quot;: &quot;6926d1b194fbc737b8eed513756de2fcda7ea408&quot;,
      &quot;_shrinkwrap&quot;: null,
      &quot;bin&quot;: null,
      &quot;_id&quot;: &quot;base64-js@1.0.1&quot;
    },
    &quot;type&quot;: &quot;finalized-manifest&quot;
  }
}
</code></pre>
<p>上面的 <code>_shasum</code> 属性 <code>6926d1b194fbc737b8eed513756de2fcda7ea408</code> 即为 <code>tar</code> 包的 <code>hash</code>， <code>hash</code>的前几位 <code>6926</code> 即为缓存的前两层目录，我们进去这个目录果然找到的压缩后的依赖包：</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef3bc635b03~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
/> -->
<blockquote>
<p>以上的缓存策略是从 npm v5 版本开始的，在 npm v5 版本之前，每个缓存的模块在 ~/.npm 文件夹中以模块名的形式直接存储，储存结构是{cache}/{name}/{version}。</p>
</blockquote>
<p><code>npm</code> 提供了几个命令来管理缓存数据：</p>
<ul>
<li><code>npm cache add</code>：官方解释说这个命令主要是 npm 内部使用，但是也可以用来手动给一个指定的 <code>package</code> 添加缓存。</li>
<li><code>npm cache clean</code>：删除缓存目录下的所有数据，为了保证缓存数据的完整性，需要加上 <code>--force</code> 参数。</li>
<li><code>npm cache verify</code>：验证缓存数据的有效性和完整性，清理垃圾数据。</li>
</ul>
<p>基于缓存数据，<code>npm</code> 提供了离线安装模式，分别有以下几种：</p>
<ul>
<li><code>--prefer-offline</code>： 优先使用缓存数据，如果没有匹配的缓存数据，则从远程仓库下载。</li>
<li><code>--prefer-online</code>： 优先使用网络数据，如果网络数据请求失败，再去请求缓存数据，这种模式可以及时获取最新的模块。</li>
<li><code>--offline</code>： 不请求网络，直接使用缓存数据，一旦缓存数据不存在，则安装失败。</li>
</ul>
<h3>文件完整性</h3>
<p>在下载依赖包之前，我们一般就能拿到 npm 对该依赖包计算的<code>hash</code>值，执行<code>npm info &lt;pkg&gt;</code>，<code>shasum</code>就是<code>hash</code>。</p>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef3c2a2dac0~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="npm info express"
/> -->
<p>用户下载依赖包到本地后，需要确定在下载过程中没有出现错误，所以在下载完成之后需要在本地再计算一次文件的 <code>hash</code> 值，如果两个 <code>hash</code> 值是相同的，则确保下载的依赖是完整的，如果不同，则进行重新下载。</p>
<h3>整体流程</h3>
<ul>
<li>检查<code>.npmrc</code>文件: 优先级为: 项目级的<code>.npmrc</code>文件 &gt; 用户级的<code>.npmrc</code>文件 &gt; 全局级的<code>.npmrc</code>文件 &gt; npm 内置的<code>.npmrc</code>文件</li>
<li>检查项目中有无<code>lock</code>文件</li>
<li>无<code>lock</code>文件
<ul>
<li>从 npm 远程仓库获取包信息</li>
<li>根据<code>package.json</code>构建依赖书，构建过程:
<ol>
<li>构建依赖树时，不管其是直接依赖还是子依赖的依赖，优先将其放置在<code>node_modules</code>根目录。</li>
<li>当遇到相同模块时，判断已放置在依赖树的模块版本是否符合新模块的版本范围，如果符合则跳过，不符合则在当前模块的<code>node_modules</code>下放置该模块。</li>
<li>注意这一步只是确定逻辑上的依赖树，并非真正的安装，后面会根据这个以来结构去下载或者拿到缓存中的依赖包</li>
</ol>
</li>
<li>在缓存中依次查找依赖树中的每个包
<ul>
<li>不存在缓存:
<ol>
<li>从 npm 远程仓库下载包</li>
<li>校验包的完整性</li>
<li>校验不通过:
<ul>
<li>重新下载</li>
</ul>
</li>
<li>校验通过:
<ul>
<li>将下载的包复制到 npm 缓存目录</li>
<li>将下载的包按照依赖结构解压到<code>node_modules</code></li>
</ul>
</li>
</ol>
</li>
<li>存在缓存: 将缓存按照依赖结构解压到<code>node_modules</code></li>
</ul>
</li>
<li>将包解压到<code>node_modules</code></li>
<li>生成<code>lock</code>文件</li>
</ul>
</li>
<li>有<code>lock</code>文件
<ul>
<li>检查<code>package.json</code>中的依赖版本是否和<code>package-lock.json</code>中的依赖有冲突</li>
<li>如果没有冲突，直接跳过获取包信息、构建依赖树过程，开始在缓存中查找包信息，后续过程相同从检查缓存开始</li>
</ul>
</li>
</ul>
<!-- <ZoomImg
  src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/16/16f0eef327ccaba5~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png"
  desc="npm install流程"
/> -->
<p>这个过程还包含了一些其他的操作，例如执行你定义的一些生命周期函数，你可以执行 <code>npm install package --timing=true --loglevel=verbose</code> 来查看某个包具体的安装流程和细节。</p>
<h2>参考</h2>
<p><a href="https://docs.npmjs.com/cli/v10/configuring-npm/package-json">package.json</a><br>
<a href="https://juejin.cn/post/6844904022080667661">前端工程化 - 剖析 npm 的包管理机制</a><br>
<a href="https://segmentfault.com/a/1190000022435060">一文搞懂 peerDependencies</a></p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[前端模块化]]></title>
            <link>https://leetme.netlify.app/posts/module</link>
            <guid>https://leetme.netlify.app/posts/module</guid>
            <pubDate>Mon, 11 Dec 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>什么是模块化</h2>
<ul>
<li>将复杂的程序根据规则或规范拆分成为若干模块，一个模块包括输入和输出</li>
<li>模块化的内部数据和实现是私有的，对外暴露一些接口与其他模块进行通讯</li>
</ul>
<h2>模块化的背景</h2>
<p>JavaScript 程序本来很小——在早期，它们大多被用来执行独立的脚本任务，在你的 web 页面需要的地方提供一定交互，所以一般不需要多大的脚本。过了几年，我们现在有了运行大量 JavaScript 脚本的复杂程序，还有一些被用在其他环境（例如 Node.js）。</p>
<p>因此，近年来，有必要开始考虑提供<code>一种将 JavaScript 程序拆分为可按需导入的单独模块</code>的机制。Node.js 已经提供这个能力很长时间了，还有很多的 JavaScript 库和框架已经开始了模块的使用（例如，CommonJS 和基于 AMD 的其他模块系统如 RequireJS，以及最新的 Webpack 和 Babel）。</p>
<ul>
<li>模块化是一种标准，不是实现</li>
<li>理解模块化是理解前端工程化的前提</li>
<li>前端模块化是前端项目规范化的必然结果</li>
</ul>
<h2>模块和脚本的区别</h2>
<p>首先，JavaScript 有两种源文件，一种叫脚本(script)，一种叫模块(module)。这个区分是从 ES6 引入了模块机制后开始的，在 ES5 和之前的版本中，只有一种源文件类型：脚本。</p>
<p>脚本可以是浏览器或者 node 环境引入执行的，而模块只能由 JavaScript 代码用 import 引入执行。</p>
<blockquote>
<p>这里只说了<code>import</code>一种引入方式，后面会在介绍<a href="/articles/engineering/module#%E6%A8%A1%E5%9D%97%E5%8C%96%E8%A7%84%E8%8C%83">模块化规范</a>时讲解。</p>
</blockquote>
<p>从概念上说，可以认为脚本具有主动的 JavaScript 代码段，是控制宿主完成一定任务的代码，而模块是被动性的代码段，是等待被调用的库。</p>
<p>现代浏览器支持用 script 标签引入模块或者脚本，若引入模块须加上属性 type。</p>
<pre><code class="language-html">&lt;script type=&quot;module&quot; src=&quot;xxx.js&quot;&gt;&lt;/script&gt;
</code></pre>
<h2>模块化的进化过程</h2>
<h3>全局 function 模式</h3>
<p>将不同的功能封装成不同的全局函数。</p>
<p>缺点：污染全局命名空间, 容易引起命名冲突或数据不安全，而且模块成员之间看不出直接关系。</p>
<pre><code class="language-js">function m1() {
  // ...
}

function m2() {
  // ...
}
</code></pre>
<h3>namespace 模式</h3>
<p>优点：减少了全局变量，解决命名冲突</p>
<p>缺点：数据不安全（外部可以直接修改模块内部的数据）</p>
<pre><code class="language-js">const __module = {
  data: 'xxx',
  foo() {
    // ...
  },
  bar() {
    // ...
  }
}

__module.data = '123' // 可直接修改
</code></pre>
<h3>IIFE 模式</h3>
<p>该模式又称匿名函数自调用(闭包)，将数据和行为封装到一个函数内部，通过给 window 添加属性来向外暴露接口。</p>
<p>优点：通过自执行函数创建闭包，解决私有化的问题，外部只能通过暴露的方法操作</p>
<p>缺点：无法解决模块间相互依赖的问题</p>
<pre><code>&lt;!-- index.html --&gt;
&lt;script type=&quot;text/javascript&quot; src=&quot;module.js&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
  __module.foo()
  __module.bar()
  console.log(__module.data) //undefined 不能访问模块内部数据
  __module.data = 'xxxx' //不是修改的模块内部的data
  __module.foo() //没有改变
&lt;/script&gt;
</code></pre>
<pre><code class="language-js">// module.js
;(function (window) {
  const data = 'xxx'
  // 操作数据的函数
  function foo() {
    // 用于暴露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {
    // 用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() // 内部调用
  }
  function otherFun() {
    // 内部私有的函数
    console.log('otherFun()')
  }
  // 暴露行为
  window.__module = { foo, bar } // ES6写法
})(window)
</code></pre>
<h3>IIFE 模式增强</h3>
<p>引入依赖。这就是现代模块实现的基石。</p>
<pre><code class="language-js">// module.js
;(function (window, $) {
  const data = 'www.baidu.com'
  // 操作数据的函数
  function foo() {
    // 用于暴露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    // 用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() // 内部调用
  }
  function otherFun() {
    // 内部私有的函数
    console.log('otherFun()')
  }
  // 暴露行为
  window.__module = { foo, bar }
})(window, jQuery)
</code></pre>
<pre><code>&lt;!-- index.html --&gt;
&lt;!-- 引入的js必须有一定顺序 --&gt;
&lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.10.1.js&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot; src=&quot;module.js&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
  __module.foo()
&lt;/script&gt;
</code></pre>
<p>上例子通过 jquery 方法将页面的背景颜色改成红色，所以必须先引入 jQuery 库，就把这个库当作参数传入。<code>这样做除了保证模块的独立性，还使得模块之间的依赖关系变得明显。</code></p>
<h2>模块化的好处</h2>
<ul>
<li>避免命名冲突（减少命名空间污染）</li>
<li>更好的分离，按需加载</li>
<li>更高复用性</li>
<li>高可维护性</li>
</ul>
<h2>引入多个<code>script</code>后出现的问题</h2>
<ul>
<li>
<p>请求过多<br>
首先我们需要依赖多个模块，那就会发送多个请求导致请求过多</p>
</li>
<li>
<p>依赖模糊<br>
我们不知道模块之间具体依赖关系，无法确定引入模块的先后顺序导致出错</p>
</li>
<li>
<p>难以维护<br>
由于上面两个问题导致很难维护，引发一系列问题导致项目出现严重问题</p>
</li>
</ul>
<p>而之后的模块化规范得以解决以上的问题。</p>
<h2>模块化规范</h2>
<p>模块化规范包括：</p>
<ul>
<li>CommonJS</li>
<li>ESModule</li>
<li>AMD</li>
<li>CMD</li>
<li>UMD</li>
</ul>
<p>文章只重点介绍 CommonJS 和 ESModule</p>
<h3>CommonJS</h3>
<p>是 Node 应用采用的模块化规范。每个文件就是一个模块，有自己的作用域。在一个文件中定义的变量、函数、类都是私有的，对其他文件不可见。</p>
<blockquote>
<p>在服务器端，模块的加载时运行时同步加载的</p>
<p>在浏览器端，模块需要通过提前编译打包处理；首先，既然同步的，很容易引起阻塞；其次，浏览器不认识 require 语法，因此，需要提前编译打包。</p>
</blockquote>
<h4>特点</h4>
<ul>
<li>所有代码都运行在模块作用域内，不会污染全局作用域</li>
<li>只在第一次加载时运行一次，运行结果会被缓存；想让模块再次运行需要先清除缓存</li>
<li>模块的加载顺序按照其在代码中的引入顺序</li>
</ul>
<h4>模块的暴露和引入</h4>
<p>Node.js 中，每个模块都有一个 exports 接口对象，我们需要把公告的变量、函数等挂在到 exports 对象上，其他模块才可以使用。</p>
<h5>暴露: <code>exports</code></h5>
<p><code>exports</code>对象用来导出当前模块的公共方法或属性。别的模块通过 require 函数调用当前模块时，得到的就是当前模块的 exports 对象。</p>
<pre><code class="language-js">function foo() {}
const bar = ''

exports.foo = foo
exports.bar = bar
</code></pre>
<p>::: tip<br>
暴露的关键词是 exports，不是 export。其实，这里的 exports 类似于 ES6 中的 export 的用法，都是用来导出一个指定名字的对象。<br>
:::</p>
<h5>暴露: <code>module.exports</code></h5>
<p><code>module.exports</code>用来导出一个默认对象，没有指定对象名</p>
<pre><code class="language-js">module.exports = {}

// or
const name = 'leet'
module.exports.name = name

// 重复使用module.exports整个赋值会覆盖上一次的赋值
</code></pre>
<p>::: tip <code>exports</code>和<code>module.exports</code>的区别<br>
主要：</p>
<ul>
<li>使用 exports 时，只能单个设置属性 <code>exports.a = a</code></li>
<li>使用 module.exports 时，即单个设置属性<code>module.exports.a</code>，也可整个赋值<code>module.exports = obj</code></li>
</ul>
<p>其他：</p>
<ul>
<li>Node 中每个模块的最后，都会执行<code>return: module.exports</code></li>
<li>Node 中每个模块都会把<code>module.exports</code>指向的对象赋值给一个变量<code>exports</code>，也就是说<code>exports = module.exports</code></li>
<li><code>module.exports = xxx</code>，表示当前模块导出一个单一成员，结果就是 xxx</li>
<li>如果需要导出多个成员，则必须使用<code>exports.foo = xxx; exports.bar = xxx</code>。或者<code>module.exports.foo = xxx; module.exports.bar = xxx</code></li>
</ul>
<p><strong>暴露的模块到底是谁</strong></p>
<p>暴露的本质就是<code>exports</code>对象。</p>
<p>方式一的 exports.a = a 可以理解成是，给 exports 对象添加属性。方式二的 module.exports = a 可以理解成是给整个 exports 对象赋值。方式二的 module.exports.c = c 可以理解成是给 exports 对象添加属性。<br>
:::</p>
<h5>引入: <code>require</code></h5>
<p>require 函数用来在一个模块中引入另外一个模块。传入模块名，返回模块导出对象。</p>
<ul>
<li>内置模块：require 的是包名。</li>
<li>下载的第三方模块：require 的是包名。</li>
<li>自定义模块：require 的是文件路径。文件路径既可以用绝对路径，也可以用相对路径。后缀名.js 可以省略。</li>
</ul>
<p><strong>作用</strong></p>
<ul>
<li>执行导入的模块中的代码。</li>
<li>返回导入模块中的接口对象。</li>
</ul>
<h4>模块的加载机制</h4>
<p><strong><em>输入的是被输出的值的拷贝</em></strong>。一旦输出这个值，模块内部的变化就影响不到这个值。</p>
<p>::: code-group</p>
<pre><code class="language-js">let counter = 1
function incrementCounter() {
  ++counter
}

module.exports = {
  counter,
  incrementCounter
}
</code></pre>
<pre><code class="language-js">const counter = require('./lib.js').counter
const incrementCounter = require('./lib.js').incrementCounter

console.log(counter) // 3
incrementCounter()
console.log(counter) // 3
</code></pre>
<p>:::</p>
<p>counter 输出后，lib.js 模块内部的变化就影响不到 counter 了。因为 counter 是一个原始类型的值，会被缓存，除非写成一个函数，才能得到内部变动的值。</p>
<h4>服务器和浏览器端的实现</h4>
<br />
<h5>服务器端实现</h5>
<ol>
<li>下载 node.js</li>
<li>创建项目结构(根目录执行命令 npm init)</li>
<li>下载第三方模块(可选)</li>
<li>定义模块代码</li>
<li>在主模块引入其他模块</li>
<li>执行主模块(执行命令 node 主模块.js)</li>
</ol>
<h5>浏览器端实现</h5>
<ol>
<li>创建项目结构</li>
<li>下载<code>broswerify</code>(<code>npm install broswerify -g</code> <code>npm install broswerify -D</code>)</li>
<li>定义模块代码</li>
<li>打包处理 js(根目录执行命令<code>browserify 主模块.js -o 打包生成文件.js</code>)</li>
<li>inedx.html 引入<code>打包生成文件.js</code></li>
</ol>
<h3>ESModule</h3>
<ul>
<li>自动采用严格模式，例如 this 直接打印出来是 undefined</li>
<li>模块的内容<code>只有在第一次被import的时候会被执行</code></li>
<li>通过 CORS 的方式请求外部 JS 模块，所以只能请求支持 CORS 的方式的外部地址</li>
<li>如果在浏览器中使用 ESModule，则每个脚本都会以与<code>defer</code>相同的方式执行，即延迟执行脚本，会等待网页渲染完成之后再执行</li>
</ul>
<h4>模块的暴露和引入</h4>
<br />
<h5>暴露: export</h5>
<p>暴露模块包含两部分</p>
<ul>
<li>具名：export name</li>
<li>默认：export default</li>
</ul>
<pre><code class="language-js">export const name = 'Leet'

// or
// const name = 'Leet'
// export { name }

// or 别名
// export { name as firstName }

// or 默认导出
// export default name

// or 默认导出匿名
// export default function() {}
// export default {}
</code></pre>
<h5>引入: import</h5>
<pre><code class="language-js">import { name } from 'xxx.js'

// or 别名
// import { name as firstName } from 'xxx.js'

// or 引入模块所有
// import * as __module from 'xxx.js'
// console.log(__module.name)

// or 引入默认导出 名称可以自定义
// import xxx from 'xxx.js'

// or 动态import
// import('xxx.js').then((module) =&gt; {
//   const { default: foo, aaa } = module
// })
</code></pre>
<p>::: tip<br>
当<code>import</code>是，如果引入的是<code>export</code>具名导出的数据，则需要知道变量名或函数名，否则无法加载。如果是<code>export default</code>则可以自定义名称。<br>
:::</p>
<h4>与 CommonJS 模块的差异</h4>
<ol>
<li>CommonJS 模块输出的是一个值的拷贝，ES6 模块输出的是值的引用</li>
<li>CommonJS 模块是运行时加载，ES6 模块是编译时输出接口</li>
<li>ESModule 在编译期间会将所有 import 提升到顶部，CommonJs 不会提升 require</li>
<li>CommonJs 中顶层的 this 指向这个模块本身，而 ESModule 中顶层 this 指向 undefined</li>
<li>CommonJS 和 ES Module 都对循环引入做了处理，不会进入死循环，但方式不同：</li>
</ol>
<ul>
<li>CommonJS 借助模块缓存，遇到 require 函数会先检查是否有缓存，已经有的则不会进入执行，在模块缓存中还记录着导出的变量的拷贝值。</li>
<li>ES Module 借助模块地图，已经进入过的模块标注为获取中，遇到 import 语句会去检查这个地图，已经标注为获取中的则不会进入，地图中的每一个节点是一个模块记录，上面有导出变量的内存地址，导入时会做一个连接——即指向同一块内存。</li>
</ul>
<p>第二个差异是因为 CommonJS 模块加载的是一个对象(exports)，该对象只有在脚本完全加载完成时才会生成；而 ESModule 模块不是对象，他的对外接口只是一种静态定义，在代码解析阶段就会生成。</p>
<p>::: tip<br>
我们在搭建框架后，有些配置文件又是后会报红，是因为文件没有遵循对应的模块化规范。</p>
<ul>
<li><code>.mjs</code>遵循 ESModule 规范，可以使用 import、export</li>
<li><code>.cjs</code>遵循 CommonJS 规范，可以使用 exports、module.exports、require</li>
</ul>
<p>也可以通过<code>package.json</code>来指定遵循哪个规范，<code>type: module</code>，<code>type: commonjs</code>。<br>
:::</p>
<h3>AMD、CMD 和 UMD</h3>
<br />
<h4>AMD</h4>
<p>CommonJS 规范加载模块是同步的，也就是说，只有加载完成，才能执行后面的操作。AMD 规范则是非同步加载模块，允许指定回调函数。由于 Node.js 主要用于服务器编程，模块文件一般都已经存在于本地硬盘，所以加载起来比较快，不用考虑非同步加载的方式，所以 CommonJS 规范比较适用。但是，如果是浏览器环境，要从服务器端加载模块，这时就必须采用非同步模式，因此浏览器端一般采用 AMD 规范。此外 AMD 规范比 CommonJS 规范在浏览器端实现要来的早。</p>
<pre><code class="language-js">// 定义没有依赖的模块
define(() =&gt; {
  return 模块
})

// 定义有依赖的模块
define(['module1', 'module2'], (m1, m2) =&gt; {
  // 模块
})

require(['module1', 'module2'], (m1, m2) =&gt; {
  // ...
})
</code></pre>
<h4>CMD</h4>
<p>CMD 规范专门用于浏览器端，模块的加载是异步的，模块使用时才会加载执行。CMD 规范整合了 CommonJS 和 AMD 规范的特点。在 Sea.js 中，所有 JavaScript 模块都遵循 CMD 模块定义规范。</p>
<pre><code class="language-js">// 定义没有依赖的模块
define((require, exports, module) =&gt; {
  exports.xxx = value
  module.exports = value
})

// 定义有依赖的模块
define((require, exports, module) =&gt; {
  // 引入依赖模块(同步)
  const module2 = require('./module2')
  // 引入依赖模块(异步)
  require.async('./module3', (m3) =&gt; {})
  // 暴露模块
  exports.xxx = value
})

define((require) =&gt; {
  const m1 = require('./module1')
  const m4 = require('./module4')
  m1.show()
  m4.show()
})
</code></pre>
<h4>UMD</h4>
<p>通过对 CommonJs、CMD、AMD 进一步处理，它没有自己专有的规范，是集结了 CommonJs、CMD、AMD 的规范于一身。</p>
<p>它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。<br>
未来同一个 JavaScript 包运行在浏览器端、服务区端都只需要遵守同一个写法就行了。</p>
<pre><code class="language-js">;((global, factory) =&gt; {
  // 如果 当前的上下文有define函数，并且AMD  说明处于AMD 环境下
  if (typeof define === 'function' &amp;&amp; define.amd) {
    define(['moduleA'], factory)
  }
  else if (typeof exports === 'object') {
    // commonjs
    const moduleA = require('moduleA')
    modules.exports = factory(moduleA)
  }
  else {
    global.moduleA = factory(global.moduleA) // 直接挂载成 windows 全局变量
  }
})(this, (moduleA) =&gt; {
  // 本模块的定义
  return {}
})
</code></pre>
<h2>参考</h2>
<p><a href="https://jelly.jd.com/article/639be9a0abf18f005786c57f#">前端工程化三部曲之基础篇--模块化技术</a></p>
<p><a href="https://github.com/ljianshu/Blog/issues/48">前端模块化详解(完整版)</a></p>
<p><a href="https://zhuanlan.zhihu.com/p/139895805">从底层看前端（十一）—— JavaScript 语法：脚本，模块和函数体。</a></p>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
        <item>
            <title><![CDATA[统一代码风格和规范项目代码]]></title>
            <link>https://leetme.netlify.app/posts/code-style-standard</link>
            <guid>https://leetme.netlify.app/posts/code-style-standard</guid>
            <pubDate>Mon, 27 Nov 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<blockquote>
<p>[!WARNING]<br>
建议阅读新的文章 <a href="/workflow/code-style-standard-new">统一代码风格和规范项目代码 - 新</a></p>
</blockquote>
<p>本来是想着搭个完整的项目框架的，记录完规范这节后，代码那些配置就已经懒得更新了。心想着那些东西新建项目的时候，脚手架都会弄好。索性就把之前的第二节停掉删了，只留下这一节。因为我当初新建一个项目时，这些规范我也不知道怎么搞，记录以下以后也能参考参考。</p>
<p>::: info<br>
在公司每次有新项目时，我都得重新搭建项目框架。并且后端的一般也是复用在新项目中，例如登录授权以及返回数据格式基本上每个项目都是一样的。所以决定还是搭建一个项目框架(其实是两个，一个 Admin、一个 Mobile)，一劳永逸！</p>
<p>由于文章太长所以分为两篇文章，第一篇是配置规范化，第二篇是配置对应必要的库和初始化代码<br>
:::</p>
<h2>介绍</h2>
<p>本文将记录新建完一个项目后的代码规范配置等，包括 eslint, prettier, stylelint, husky, lint-stage, commitlint, cz-git, editorconfig</p>
<h2>创建项目</h2>
<p><img src="https://leetme.netlify.app/images/init-vite.png" alt="创建项目"></p>
<h2>ESLint</h2>
<h3>安装和初始化</h3>
<pre><code class="language-shell"># 创建项目后先安装依赖
pnpm install

# 安装eslint
pnpm install eslint -D

# 初始化eslint
pnpm eslint --init
</code></pre>
<br />
<p><img src="/images/init-eslint.png" alt="初始化ESLint"></p>
<h3>添加脚本命令</h3>
<p>在<code>package.json</code>中<code>script</code>添加命令：</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;lint&quot;: &quot;eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix&quot;
  }
}
</code></pre>
<p>添加完脚本命令后<code>pnpm lint</code>执行一次</p>
<p>::: tip<br>
eslint fix 时可能会对不相关的文件进行修复，所以需要在根目录新建<code>.eslintignore</code>来排除不相关的文件</p>
<pre><code class="language-text">dist
node_modules
public
.husky
.vscode
.idea
*.sh
*.md

src/assets

.eslintrc.cjs
.prettierrc.cjs
.stylelintrc.cjs
</code></pre>
<p>:::</p>
<h2>Prettier</h2>
<h3>安装</h3>
<pre><code class="language-shell"># 安装eslint
pnpm install prettier -D
</code></pre>
<h3>.prettierrc.cjs</h3>
<p>在根目录创建<code>.prettierrc.cjs</code>，并使用以下配置:</p>
<pre><code class="language-js">module.exports = {
  // 一行的字符数，如果超过会进行换行，默认为80
  printWidth: 80,
  // 一个tab代表几个空格数，默认为2
  tabWidth: 2,
  // 是否使用tab进行缩进，默认为false，表示用空格进行缩减
  useTabs: false,
  // 字符串是否使用单引号，默认为false，使用双引号
  singleQuote: true,
  // 行位是否使用分号，默认为true
  semi: false,
  // 是否使用尾逗号，有三个可选值&quot;&lt;none|es5|all&gt;&quot;
  trailingComma: 'none',
  // 对象大括号直接是否有空格，默认为true，效果：{ foo: bar }
  bracketSpacing: true,
  // 是否只格式化在文件顶部包含特定注释(@prettier| @format)的文件，默认false
  requirePragma: false,
  // 是否格式化一些文件中被嵌入的代码片段的风格(auto|off;默认auto)
  embeddedLanguageFormatting: 'auto',
  // 指定 HTML 文件的空格敏感度 (css|strict|ignore;默认css)
  htmlWhitespaceSensitivity: 'css'
}
</code></pre>
<p>::: tip<br>
eslint fix 时可能会对不相关的文件进行修复，所以需要在根目录新建<code>.prettierignore</code>来排除不相关的文件</p>
<pre><code class="language-text">dist
node_modules
public
.husky
.vscode
.idea
*.sh
*.md
src/assets
</code></pre>
<p>:::</p>
<h3>添加脚本命令</h3>
<p>在<code>package.json</code>中<code>script</code>添加命令：</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;format&quot;: &quot;prettier --write \&quot;./**/*.{html,vue,ts,js,json,md}\&quot;&quot;
  }
}
</code></pre>
<h2>ESLint 和 Prettier 的冲突</h2>
<p>在使用的过程中会发现，由于我们开启了自动化的 eslint 修复与自动化的根据 prettier 来格式化代码。所以我们已保存代码，会出现屏幕闪一起后又恢复到了报错的状态。</p>
<p>这其中的根本原因就是 eslint 有部分规则与 prettier 冲突了，所以保存的时候显示运行了 eslint 的修复命令，然后再运行 prettier 格式化，所以就会出现屏幕闪一下然后又恢复到报错的现象。这时候你可以检查一下是否存在冲突的规则。</p>
<h3>安装依赖</h3>
<pre><code class="language-shell">pnpm install eslint-config-prettier eslint-plugin-prettier -D
</code></pre>
<h3>解决冲突</h3>
<pre><code class="language-json">{
  &quot;extends&quot;: [
    &quot;eslint:recommended&quot;,
    &quot;plugin:@typescript-eslint/recommended&quot;,
    &quot;plugin:vue/vue3-essential&quot;,
    &quot;plugin:prettier/recommended&quot; // [!code ++]
  ]
}
</code></pre>
<p>最后重启一遍 VSCode。</p>
<h2>Stylelint</h2>
<h3>安装依赖</h3>
<pre><code class="language-shell">pnpm install stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order -D
</code></pre>
<br />
<p>::: info 依赖说明</p>
<p>安装的依赖是以 scss 为基础安装的，若不需要可去掉相关 scss 的依赖</p>
<ul>
<li><code>stylelint</code>: css 样式 lint 工具</li>
<li><code>postcss</code>: 转换 css 代码工具</li>
<li><code>postcss-scss</code>: 识别 scss 语法</li>
<li><code>postcss-html</code>: 识别 html/vue 中的<code>&lt;style&gt;&lt;/style&gt;</code>标签中的样式</li>
<li><code>stylelint-config-standard</code>: Stylelint 的标准可共享配置规则，详细可查看官方文档</li>
<li><code>stylelint-config-prettier</code>: 关闭所有不必要或可能与 Prettier 冲突的规则</li>
<li><code>stylelint-config-recommended-less</code>: scss 的推荐可共享配置规则，详细可查看官方文档</li>
<li><code>stylelint-config-standard-vue</code>: lint.vue 文件的样式配置</li>
<li><code>stylelint-scss</code>: stylelint-config-recommended-scss 的依赖，scss 的 stylelint 规则集合</li>
<li><code>stylelint-order</code>: 指定样式书写的顺序，在.stylelintrc.js 中 order/properties-order 指定顺序</li>
</ul>
<p>:::</p>
<h3>.stylelintrc.cjs</h3>
<p>在根目录创建<code>.stylelintrc.cjs</code>，并使用以下配置:</p>
<pre><code class="language-js">module.exports = {
  extends: [
    'stylelint-config-standard',
    'stylelint-config-prettier',
    'stylelint-config-recommended-scss',
    'stylelint-config-standard-vue'
  ],
  plugins: ['stylelint-order'],
  // 不同格式的文件指定自定义语法
  overrides: [
    {
      files: ['**/*.(scss|css|vue|html)'],
      customSyntax: 'postcss-scss'
    },
    {
      files: ['**/*.(html|vue)'],
      customSyntax: 'postcss-html'
    }
  ],
  ignoreFiles: [
    '**/*.js',
    '**/*.jsx',
    '**/*.tsx',
    '**/*.ts',
    '**/*.json',
    '**/*.md',
    '**/*.yaml'
  ],
  rules: {
    'no-descending-specificity': null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器
    'selector-class-pattern': null, // 选择器类名命名规则
    'selector-pseudo-element-no-unknown': [
      true,
      {
        ignorePseudoElements: ['v-deep']
      }
    ],
    'selector-pseudo-class-no-unknown': [
      true,
      {
        ignorePseudoClasses: ['deep']
      }
    ],
    // 指定样式的排序
    'order/properties-order': [
      'position',
      'top',
      'right',
      'bottom',
      'left',
      'z-index',
      'display',
      'justify-content',
      'align-items',
      'float',
      'clear',
      'overflow',
      'overflow-x',
      'overflow-y',
      'padding',
      'padding-top',
      'padding-right',
      'padding-bottom',
      'padding-left',
      'margin',
      'margin-top',
      'margin-right',
      'margin-bottom',
      'margin-left',
      'width',
      'min-width',
      'max-width',
      'height',
      'min-height',
      'max-height',
      'font-size',
      'font-family',
      'text-align',
      'text-justify',
      'text-indent',
      'text-overflow',
      'text-decoration',
      'white-space',
      'color',
      'background',
      'background-position',
      'background-repeat',
      'background-size',
      'background-color',
      'background-clip',
      'border',
      'border-style',
      'border-width',
      'border-color',
      'border-top-style',
      'border-top-width',
      'border-top-color',
      'border-right-style',
      'border-right-width',
      'border-right-color',
      'border-bottom-style',
      'border-bottom-width',
      'border-bottom-color',
      'border-left-style',
      'border-left-width',
      'border-left-color',
      'border-radius',
      'opacity',
      'filter',
      'list-style',
      'outline',
      'visibility',
      'box-shadow',
      'text-shadow',
      'resize',
      'transition'
    ]
  }
}
</code></pre>
<p>::: tip .stylelintignore<br>
eslint fix 时可能会对不相关的文件进行修复，所以需要在根目录新建<code>.stylelintignore</code>来排除不相关的文件</p>
<pre><code class="language-text">dist
node_modules
public
.husky
.vscode
.idea
*.sh
*.md

src/assets
</code></pre>
<p>:::</p>
<h3>添加脚本命令</h3>
<p>在<code>package.json</code>中<code>script</code>添加命令：</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;lint:style&quot;: &quot;stylelint \&quot;./**/*.{css,less,vue,html}\&quot; --fix&quot;
  }
}
</code></pre>
<p>::: tip<br>
如果安装的 stylelint 版本时&gt;=15.0，使用脚本命令时会出现 bug，请查看文章<a href="/notes/pit/others#stylelint-v15">stylelint v15 导致的报错</a><br>
:::</p>
<h2>husky</h2>
<p>虽然我们在上面配置了<code>eslint prettier stylelint</code>，但是对于有些不适用 vscode，或者没有安装对应插件，且没有配置自动保存时，就不能实现修复和格式化代码。</p>
<p>未修复和格式化的代码提交到<code>git</code>是不符合要求的。因此需要<code>husky</code>来强制验证提交的代码是否通过验证。</p>
<h3>安装依赖</h3>
<pre><code class="language-shell">pnpm install husky -D
</code></pre>
<h3>添加脚本命令</h3>
<p>在<code>package.json</code>中<code>script</code>添加命令：</p>
<pre><code>&quot;scripts&quot;: {
  &quot;prepare&quot;: &quot;husky install&quot;
}
</code></pre>
<p>该命令会在 pnpm install 之后运行，这样其他克隆该项目的同学就在安装依赖的时候就会自动执行该命令来安装 husky。这里我们就不重新执行 pnpm install 了，直接执行 pnpm prepare，这个时候你会发现多了一个.husky 目录。</p>
<p>运行<code>husky</code>生成<code>pre-commit</code>钩子</p>
<pre><code class="language-shell">pnpm husky add .husky/pre-commit &quot;pnpm lint &amp;&amp; pnpm format &amp;&amp; pnpm lint:style&quot;
</code></pre>
<p>当我们执行 git commit 的时候就会执行 pnpm lint 与 pnpm format，当这两条命令出现报错，就不会提交成功。</p>
<p>::: tip<br>
如果你也是跟我一样一步一步搭建框架，那你会碰到以下问题：</p>
<ul>
<li>在执行运行<code>husky</code>时会出现
<blockquote>
<p>husky - can't create hook, .husky directory doesn't exist (try running husky install)<br>
因为一步一步搭建是已经安装过依赖，所以并不会执行<code>pnpm prepare</code>，我们需要手动执行这个命令来生成<code>.husky</code>文件夹</p>
</blockquote>
</li>
<li>执行<code>husky</code>时也会出现另一个问题
<blockquote>
<p>husky - git command not found, skipping install<br>
原因是没有初始化项目的<code>git</code>仓库，执行<code>git init</code>后再执行命令即可</p>
</blockquote>
</li>
</ul>
<p>:::</p>
<h2>lint-staged</h2>
<p>lint-staged 是什么？</p>
<ul>
<li>一个仅仅过滤出 Git 代码暂存区文件(被 git add 的文件)的工具</li>
<li>对个人要提交的代码的一个规范和约束</li>
<li>是一个在 git 暂存文件上（也就是被 git add 的文件）运行已配置的 linter（或其他）任务。lint-staged 总是将所有暂存文件的列表传递给任务。</li>
</ul>
<h3>安装依赖</h3>
<pre><code class="language-shell">pnpm install lint-staged -D
</code></pre>
<h3>添加 lint-staged 配置</h3>
<p>在<code>package.json</code>中新建<code>lint-staged</code>：</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {

  },
  &quot;lint-staged&quot;: {
    &quot;*.{js,ts}&quot;: [
      &quot;eslint --fix&quot;,
      &quot;prettier --write&quot;
    ],
    &quot;*.{cjs,json}&quot;: [
      &quot;prettier --write&quot;
    ],
    &quot;*.{vue,html}&quot;: [
      &quot;eslint --fix&quot;,
      &quot;prettier --write&quot;,
      &quot;stylelint --fix&quot;
    ],
    &quot;*.{scss,css}&quot;: [
      &quot;stylelint --fix&quot;,
      &quot;prettier --write&quot;
    ],
    &quot;*.md&quot;: [
      &quot;prettier --write&quot;
    ]
  }
}
</code></pre>
<h3>添加脚本命令</h3>
<p>在<code>package.json</code>中<code>script</code>添加命令：</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;lint-staged&quot;: &quot;lint-staged&quot;
  }
}
</code></pre>
<h3>修改 <code>.husky/pre-commit</code></h3>
<pre><code class="language-sh">#!/usr/bin/env sh
. &quot;$(dirname -- &quot;$0&quot;)/_/husky.sh&quot;

pnpm lint &amp;&amp; pnpm format &amp;&amp; pnpm lint:style // [!code --]
pnpm lint-staged // [!code ++]
</code></pre>
<h2>commitlint</h2>
<h3>安装依赖</h3>
<pre><code class="language-shell">pnpm install @commitlint/cli @commitlint/config-conventional -D
</code></pre>
<h3>commitlint.config.js</h3>
<pre><code class="language-bash">echo &quot;module.exports = {extends: ['@commitlint/config-conventional']}&quot; &gt; .commitlintrc.cjs
</code></pre>
<h3>添加 githook</h3>
<pre><code class="language-shell">pnpm husky add .husky/commit-msg 'npx --no --commitint --edit &quot;${1}&quot;'
</code></pre>
<h2>标准化规范化 commit message</h2>
<p><code>commitizen</code>和<code>cz-git</code>来实现标准和规范化的 commit message</p>
<blockquote>
<p>什么是 commitizen：基于 Node.js 的 git commit 命令行工具，辅助生成标准化规范化的 commit message。<br>
什么是适配器（cz-git）：更换 commitizen 命令行工具的 交互方式 插件。</p>
</blockquote>
<h3>安装依赖</h3>
<pre><code class="language-shell">pnpm install commitizen cz-git -D
</code></pre>
<h3>添加 config 指定使用的适配器</h3>
<p>在<code>package.json</code>中添加<code>config</code>配置：</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {

  },
  &quot;config&quot;: {
    &quot;commitizen&quot;: {
      &quot;path&quot;: &quot;node_modules/cz-git&quot;
    }
  }
}
</code></pre>
<h3>更改.commitlintrc.cjs 配置</h3>
<pre><code class="language-js">module.exports = {
  // 继承的规则
  extends: ['@commitlint/config-conventional'],
  // 自定义规则
  rules: {
    // @see https://commitlint.js.org/#/reference-rules

    // 提交类型枚举，git提交type必须是以下类型
    'type-enum': [
      2,
      'always',
      [
        'feat', // 新增功能
        'fix', // 修复缺陷
        'docs', // 文档变更
        'style', // 代码格式（不影响功能，例如空格、分号等格式修正）
        'refactor', // 代码重构（不包括 bug 修复、功能新增）
        'perf', // 性能优化
        'test', // 添加疏漏测试或已有测试改动
        'build', // 构建流程、外部依赖变更（如升级 npm 包、修改 webpack 配置等）
        'ci', // 修改 CI 配置、脚本
        'revert', // 回滚 commit
        'chore' // 对构建过程或辅助工具和库的更改（不影响源文件、测试用例）
      ]
    ],
    'subject-case': [0] // subject大小写不做校验
  },

  prompt: {
    messages: {
      type: '选择你要提交的类型 :',
      scope: '选择一个提交范围（可选）:',
      customScope: '请输入自定义的提交范围 :',
      subject: '填写简短精炼的变更描述 :\n',
      body: '填写更加详细的变更描述（可选）。使用 &quot;|&quot; 换行 :\n',
      breaking: '列举非兼容性重大的变更（可选）。使用 &quot;|&quot; 换行 :\n',
      footerPrefixesSelect: '选择关联issue前缀（可选）:',
      customFooterPrefix: '输入自定义issue前缀 :',
      footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
      generatingByAI: '正在通过 AI 生成你的提交简短描述...',
      generatedSelectByAI: '选择一个 AI 生成的简短描述:',
      confirmCommit: '是否提交或修改commit ?'
    },
    // prettier-ignore
    types: [
      { value: 'feat', name: 'feat:     ✨  A new feature', emoji: ':sparkles:' },
      { value: 'fix', name: 'fix:      🐛  A bug fix', emoji: ':bug:' },
      { value: 'docs', name: 'docs:     📝  Documentation only changes', emoji: ':memo:' },
      { value: 'style', name: 'style:    💄  Markup, white-space, formatting, missing semi-colons...', emoji: ':lipstick:' },
      { value: 'refactor', name: 'refactor: ♻️  A code change that neither fixes a bug or adds a feature', emoji: ':recycle:' },
      { value: 'perf', name: 'pref:     ⚡️  A code change that improves performance', emoji: ':zap:' },
      { value: 'test', name: 'test:     ✅  Adding missing tests or correcting existing tests', emoji: ':white_check_mark:' },
      { value: 'build', name: 'build:    📦️  Changes that affect the build system or external dependencies', emoji: ':package:' },
      { value: 'ci', name: 'ci:       🎡  Changes to our CI configuration files and scripts', emoji: ':ferris_wheel:' },
      { value: 'revert', name: 'revert:   ⏪️  Reverts a previous commit', emoji: ':rewind:' },
      { value: 'chore', name: 'chore:    🔨  Other changes that don\'t modify src or test files', emoji: ':hammer:' },
    ],
    useEmoji: true,
    emojiAlign: 'center',
    useAI: false,
    aiNumber: 1,
    themeColorCode: '',
    scopes: [],
    allowCustomScopes: true,
    allowEmptyScopes: true,
    customScopesAlign: 'bottom',
    customScopesAlias: 'custom',
    emptyScopesAlias: 'empty',
    upperCaseSubject: false,
    markBreakingChangeMode: false,
    allowBreakingChanges: ['feat', 'fix'],
    breaklineNumber: 100,
    breaklineChar: '|',
    skipQuestions: [],
    issuePrefixes: [
      { value: 'closed', name: 'closed:   ISSUES has been processed' }
    ],
    customIssuePrefixAlign: 'top',
    emptyIssuePrefixAlias: 'skip',
    customIssuePrefixAlias: 'custom',
    allowCustomIssuePrefix: true,
    allowEmptyIssuePrefix: true,
    confirmColorize: true,
    maxHeaderLength: Infinity,
    maxSubjectLength: Infinity,
    minSubjectLength: 0,
    scopeOverrides: undefined,
    defaultBody: '',
    defaultIssues: '',
    defaultScope: '',
    defaultSubject: ''
  }
}
</code></pre>
<h3>添加脚本命令</h3>
<p>在<code>package.json</code>中<code>script</code>添加命令：</p>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;commit&quot;: &quot;git-cz&quot;
  }
}
</code></pre>
<h2>editorconfig</h2>
<p>完成上面的配置后，有可能会出现莫名其妙的报错，如:</p>
<p><code>Delete </code>␍<code>eslint(prettier/prettier) </code></p>
<p>新建<code>.editorconfig</code>:</p>
<pre><code># http://editorconfig.org
root = true

# 表示所有文件适用
[*]
charset = utf-8 # 设置文件字符集为 utf-8
end_of_line = lf # 控制换行类型(lf | cr | crlf)
indent_style = space # 缩进风格（tab | space）
insert_final_newline = true # 始终在文件末尾插入一个新行

# 表示仅 md 文件适用以下规则
[*.md]
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪

</code></pre>
]]></content:encoded>
            <author>1414395519@qq.com (Leet)</author>
        </item>
    </channel>
</rss>