<?xml version="1.0" encoding="UTF-8"?>
<rss 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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ 开发者小蓝 - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ 开发者小蓝 - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 26 May 2026 04:21:41 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/bluesram/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 在 catch 语句中进行类型收敛 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你有使用过像 Java、C++、或者 C# 这类编程语言的经验，你通常会透过抛出异常来做错误处理，然后使用一连串的 catch  语句来捕获它们。虽然已经证明有其他更好的方式来处理错误，但因为抛异常这种方式悠久的历史以及对编程习惯的广泛影响，我们依旧会在 JavaScript 当中继续使用它。 当然，这种方式的错误处理在 JavaScript 和 TypeScript 中是行之有效的，但如果你像下面这个例子一样照搬在其他编程语言的使用习惯，在  catch 语句里定义错误类型的话。 try {   // something with Axios, for example } catch(e: AxiosError) { //         ^^^^^^^^^^ Error 1196 💥 } 你将收获一个 TS1196 错误： Catch clause ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/typescript-catch/</link>
                <guid isPermaLink="false">61dda4ee6161280665ed7fe2</guid>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Wed, 12 Jan 2022 05:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/01/danielle-macinnes-IuLgi9PWETU-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>如果你有使用过像 Java、C++、或者 C# 这类编程语言的经验，你通常会透过抛出异常来做错误处理，然后使用一连串的 <code>catch</code> 语句来捕获它们。虽然已经证明有其他更好的方式来处理错误，但因为抛异常这种方式悠久的历史以及对编程习惯的广泛影响，我们依旧会在 JavaScript 当中继续使用它。</p>
<p>当然，这种方式的错误处理在 JavaScript 和 TypeScript 中是行之有效的，但如果你像下面这个例子一样照搬在其他编程语言的使用习惯，在 <code>catch</code> 语句里定义错误类型的话。</p>
<pre><code class="language-typescript">try {
  // something with Axios, for example
} catch(e: AxiosError) {
//         ^^^^^^^^^^ Error 1196 💥
}
</code></pre>
<p>你将收获一个 <code>TS1196</code> 错误：<br>
<code> Catch clause variable type annotation must be ‘any’ or ‘unknown’ if specified.</code></p>
<blockquote>
<p><code>catch</code> 语句变量类型只能声明为 <code>any</code> 或者 <code>unknown</code>.</p>
</blockquote>
<h3 id="">之所以这样，有下面几条理由：</h3>
<h4 id="1">1 . 你可以抛出任何类型</h4>
<p>在 JavaScript 中， 你可以抛出任何的表达式。通常来说，我们会抛出 “异常” （ 在 JavaScript 中我们叫做 错误 Error ），但也不能排除我们会抛出其他任何类型的可能性：</p>
<pre><code class="language-typescript">throw "What a weird error"; // 👍
throw 404; // 👍
throw new Error("What a weird error"); // 👍
</code></pre>
<p>正因为任何合法的类型都能够被抛出，那 <code>catch</code> 语句能捕获到的类型也就不仅限于我们过去理解的错误或其子类型。</p>
<h4 id="2javascriptcatchcatch">2 . JavaScript 中只能使用一个 <code>catch</code> 语句。尽管在很早之前就有了关于<strong>多路捕获</strong>甚至是在 <code>catch</code> 语句中加入条件表达式的提案，但直到今天这些你还不能使用这些新特性。</h4>
<blockquote>
<p>了解更多 <a href="https://www.oreilly.com/library/view/javascript-the-definitive/9781449393854/ch11s06.html"> JavaScript - the definitive guide</a></p>
</blockquote>
<p>作为替代方案，我们可以通过在一个 <code>catch</code> 语句内部使用 <code>instanceof </code> 或者 <code>typeof </code> 类型检查来实现。</p>
<pre><code class="language-typescript">try {
  myroutine(); // There's a couple of errors thrown here
} catch (e) {
  if (e instanceof TypeError) {
    // A TypeError
  } else if (e instanceof RangeError) {
    // Handle the RangeError
  } else if (e instanceof EvalError) {
    // you guessed it: EvalError
  } else if (typeof e === "string") {
    // The error is a string
  } else if (axios.isAxiosError(e)) {
    // axios does an error check for us!
  } else {
    // everything else  
    logMyErrors(e);
  }
}
</code></pre>
<blockquote>
<p>注：上面代码已经是在 typescript 中进行 <strong>收敛异常捕获</strong> 的最佳实践。</p>
</blockquote>
<p>正是由于能够抛出任何可能的值，同时我们对一个 <code>try</code> 语句只能使用有且只有一个 <code>catch</code>，那么这个 <code>e</code> 的类型范围如此的宽泛，就一点都不奇怪了。</p>
<h4 id="3">3 . 无法预测的事情总在发生</h4>
<p>那么你可能又要问了，在上面的例子里面，既然你知道所有可能发生的异常，我们就不能定义一个恰到好处的<strong>联合类型</strong>来表达它吗？<br>
理论上是可以的，不过在实践中我们发现，根本无法穷举所有的异常类型。</p>
<p>除了一些用户自己定义的错误和异常，变量的类型不匹配，或者你的一个函数未定义，都会让系统抛出一个内存错误。更不用说，一个简单的方法超出了调用栈的最大数量，都会引起臭名昭著的 <strong>stack overflow</strong>。</p>
<p>异常的类型范围如此之宽广，而 <code>catch</code> 语句又只有一个，还要应对各种不确定的意外，就造成了这个 <code>e</code> 只能是 <code>any</code> 或 <code>unknown</code> 的局面。</p>
<h3 id="promise">Promise 的拒绝行为有什么不一样呢？</h3>
<p>正确答案是：一毛一样。<br>
typescript 只允许你去定义完成态（fullfilled）的值类型，对于拒绝行为（rejection），要么是你主动抛出了错误，或者系统发生了一场。</p>
<pre><code class="language-typescript">const somePromise = () =&gt; new Promise((fulfil, reject) =&gt; {
  if (someConditionIsValid()) {
    fulfil(42);
  } else {
    reject("Oh no!");
  }
});

somePromise()
  .then(val =&gt; console.log(val)) // val is number
  .catch(e =&gt; {
    console.log(e) // e can be anything, really.
  })
</code></pre>
<p>如果你使用 <code>asnyc/await</code> 模式的话，它的表现会更符合上面的表达：</p>
<pre><code class="language-typescript">try {
  const z = await somePromise(); // z is number
} catch(e) {
  // same thing, e can be anything!
}
</code></pre>
<h3 id="">写在最后</h3>
<p>很显然，对于从其他编程语言转移过来的开发者来讲，JavaScript 和 typescript 的错误处理并不十分友好。</p>
<p>我们能够做的，就是充分了解此间的差异，并坚信在不久的将来， typescript 团队给我们的类型检查能力，会让我们的错误处理足够的好。</p>
<!--kg-card-end: markdown--><p>原文链接：<a href="https://fettblog.eu/typescript-typing-catch-clauses/">https://fettblog.eu/typescript-typing-catch-clauses/</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 通关 Node.js 的 Event Loop 执行顺序，一篇就够！ ]]>
                </title>
                <description>
                    <![CDATA[ 先说明，本文针对的是node.js运行时，由uv实现的event loop。 所有理论依据来源于 node.js源码。（版本略） 0x00 总有面试官要刁难朕 我们不妨看一下这样的题目 console.log(1) setTimeout(() => {   console.log(2) }, 0) Promise.resolve().then(() => { 	console.log(3) }).then(() => { 	console.log(4) }) console.log(5) 请问上面代码的打印结果？ ▇▇▇▇▇▇▇▇▇▇  <--- 刮开查看答案 对吧，无数次被这种装X面试题恶心。 > 小声哔哔：谁项目里会这样写代码？ 不过恶心归恶心，不管有没有实用性，透过这些题目来弄清楚技术的真相，是没有坏处的。 我们的目标是：以后还有类似的题目，不管千变万化，直接通关。 0x01 没有银弹，还是要拿源码说话 为了证明不是胡说八道，先贴出关键源码。 // 来自 deps/uv/src/unix/core.c while (r != 0 && loop->st ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/node-js-event-loop/</link>
                <guid isPermaLink="false">5e6e4fbbca1efa04e196bbac</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Wed, 24 Nov 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1584320427708-7931cfa241ec.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>先说明，本文针对的是<code>node.js</code>运行时，由<code>uv</code>实现的<code>event loop</code>。</p><p>所有理论依据来源于 <code>node.js</code>源码。（版本略）</p><h3 id="0x00-">0x00 总有面试官要刁难朕</h3><p>我们不妨看一下这样的题目</p><pre><code>console.log(1)

setTimeout(() =&gt; {
  console.log(2)
}, 0)

Promise.resolve().then(() =&gt; {
	console.log(3)
}).then(() =&gt; {
	console.log(4)
})

console.log(5)

请问上面代码的打印结果？
▇▇▇▇▇▇▇▇▇▇  &lt;--- 刮开查看答案
</code></pre><p>对吧，无数次被这种装X面试题恶心。</p><blockquote>小声哔哔：谁项目里会这样写代码？</blockquote><h2 id="-">不过恶心归恶心，不管有没有实用性，透过这些题目来弄清楚技术的真相，是没有坏处的。</h2><p></p><p><strong>我们的目标是：以后还有类似的题目，不管千变万化，直接通关。</strong></p><figure class="kg-card kg-image-card"><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/WechatIMG1730.jpeg" class="kg-image" alt="WechatIMG1730" width="600" height="400" loading="lazy"></figure><h3 id="0x01-">0x01 没有银弹，还是要拿源码说话</h3><p>为了证明不是胡说八道，先贴出关键源码。</p><pre><code>// 来自 deps/uv/src/unix/core.c
while (r != 0 &amp;&amp; loop-&gt;stop_flag == 0) {
    uv__update_time(loop); 
    uv__run_timers(loop); // ⭐️ timer
    ran_pending = uv__run_pending(loop); // ⭐️ 上一个循环一些没来得及做完的事
    uv__run_idle(loop); // ⭐️ 底层用，暂时不懂
    uv__run_prepare(loop); // ⭐️ 底层用，暂时不懂

	/*
	* 忽略几行不重要的
    */
    
    uv__io_poll(loop, timeout); // ⭐️io, network or file system 等等
    uv__run_check(loop); // ⭐️ setImmediate
    uv__run_closing_handles(loop); // ⭐️ event on('close')

    if (mode == UV_RUN_ONCE) {
     // 这里不重要
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
     // 这里不重要
  }
</code></pre><p>然后我们开始逐个去了解。</p><h2 id="timer">timer</h2><p>这部分主要是检查有没有可以执行的定时器，包括但不限于<code>setTimeout``setInterval</code>。</p><p>这里的的具体实现在<code>deps/uv/src/unix/timer.c</code>，简单说就是使用一个最小堆(小顶堆), 把时间最接近的一个取出来，判断当前时间是否可以执行。</p><h2 id="pending">pending</h2><p>这个阶段是执行 上一个循环<code>poll阶段</code>还没来得及处理的callback。</p><p>这句话，在下面介绍<code>poll阶段</code>的时候才回过头来理解。</p><h2 id="idle-prepare">idle + prepare</h2><p>按文档说是底层预留的，暂时我还没研究清楚。请忽略。</p><h2 id="poll">poll</h2><p><strong>关键！</strong>这个阶段处理的，就是我们比较熟悉的<code>network , fs</code>之类的异步操作回调。就是说你去请求一个远程的接口，那么回调函数会在<code>poll</code>阶段执行。</p><p>然后就是跟上面<code>pending</code>的关联。</p><p>由于<code>uv__io_poll</code>代码有点长就不贴了，有兴趣自己去看。</p><p>一般来说，我们的每一个阶段，都会处理完已经就绪的所有callback，如果<code>poll</code>阶段触发大量的 callback，就会占用很多的时间。</p><p>我们的<code>uv</code>当然是不会设计成这样的，所以，它会从<code>timer</code>里拿到最小的(未来最快到达的)一个定时器的时间，作为<code>poll</code>阶段的 <code>timeout</code>。</p><p>如果<code>timeout</code>到了，还有callback没开始执行的，对不起，请到<code>pending</code>队列里。</p><p>可能是<code>uv</code>认为，<code>poll</code>阶段的callback，相对来说对“准时”不太敏感，所以通过这样尽量确保<code>timer</code>的执行不会误差太多。</p><h2 id="check">check</h2><p>为什么叫做<code>check</code>我也不清楚。</p><p>但是这个阶段将会运行我们 <code>setImmediate</code>注册的回调。</p><p>很震惊吧，<code>setImmediate</code>完全就不是<code>timer</code>那一族的~~~~</p><h2 id="closing_handles">closing_handles</h2><p>执行<code>close</code>事件注册的回调，放在循环的最后一个阶段，也是合情合理。</p><h2 id="0x03-">0x03 那么我们练习一下</h2><blockquote>关于process.nextTick</blockquote><blockquote>nextTick 是个复杂的实现，需要另外开一篇来讲解。</blockquote><blockquote>为了方便下面的练习，我暂时先把结论放出来。</blockquote><blockquote>nextTick会直接追加在每一个阶段末尾，就是说，如果<code>timer</code>阶段的回调里有<code>process.nextTick</code>，通过这个来注册的回调，会在紧接着的<code>pending</code>之前就执行。</blockquote><p>✏️ 题目一</p><pre><code>setTimeout(() =&gt; {
  console.log('A')
}, 0)

setImmediate(() =&gt; {
  console.log('B')
})
</code></pre><p>答案：</p><pre><code>AB 或 BA
</code></pre><p>解释：</p><blockquote>首先这里的第一个知识点，是timer的第二个参数，取值范围是 [1, 2^31 - 1]。也就是说，这个 0 会被当成 1 处理。</blockquote><blockquote>然后根据运行环境的差异，如果进入到当前循环前，已经过去了 1ms ,那就打印 AB。</blockquote><blockquote>否则，如果在 1ms 内就开始了本次循环，那<code>timer</code>还没准备后，就会在下一次循环触发，自然就打印 BA。</blockquote><p>✏️ 题目二</p><pre><code>const fs = require('fs')

fs.readFile(__filename, () =&gt; {
  setTimeout(() =&gt; {
    console.log('A')
  }, 0)

  setImmediate(() =&gt; {
    console.log('B')
  })
})
</code></pre><p>答案：</p><pre><code>BA 
</code></pre><p>解释</p><blockquote>知识点在于<code>fs.readFile</code>，这个是 io操作，它的整个回调会在<code>poll</code>阶段执行。<br>而<code>poll</code>之后马上进入<code>check</code>，所以正好先执行了刚注册的<code>setImmediate</code>。</blockquote><blockquote><code>setTimeout</code>自然就要等到下一个循环的<code>timer</code>阶段。</blockquote><p>✏️ 题目三，这个划重点</p><pre><code>setImmediate(() =&gt; {
  console.log('1')
  setImmediate(() =&gt; {
    console.log('2')
  })
  process.nextTick(() =&gt; {
    console.log('nextTick')
  })
})

setImmediate(() =&gt; {
  console.log('3')
})
</code></pre><p>答案：</p><pre><code>1 3 nextTick 2
</code></pre><p>解释：</p><blockquote>首先，最外层的两个<code>setImmediate</code>会顺序注册到同一个<code>check</code>阶段，而上面提到<code>nextTick</code>会直接追加到当前阶段末尾，所以是<code>1 3 nextTick</code>而不是<code>1 nextTick 3</code> 。</blockquote><blockquote>而内层的<code>setImmediate</code>会注册到下一次循环的<code>check</code>阶段，所以 <code>2</code>最后打印。</blockquote><blockquote>请细品。</blockquote><h2 id="0x04-promise"><strong>0x04 继续练习之前，讲讲 promise</strong></h2><p>和 <code>process.nextTick</code>类似，<code>promise</code>的回调也是在当前阶段的末尾追加。</p><p>不过有意思的是，<code>process.nextTick</code>拥有更高的优先级。</p><p>这个实现细节，也是需要另外一篇文章来讲解（挖坑+1）。。。。</p><h3 id="0x05-">0x05 继续练习吧</h3><p>✏️ 题目四</p><pre><code>const promise = Promise.resolve()

promise.then(() =&gt; {
  console.log('A')
})

process.nextTick(() =&gt; {
  console.log('B')
})
</code></pre><p>答案：</p><pre><code>BA
</code></pre><p>解释</p><blockquote>无需解释，先记住二者的优先级。</blockquote><p>✏️ 题目五</p><pre><code>setTimeout(() =&gt; {
  console.log(1)
}, 0)

new Promise((resolve, reject) =&gt; {
  console.log(2)
  for (let i = 0; i &lt; 10000; i++) {
    i === 9999 &amp;&amp; resolve()
  }
  console.log(3)
}).then(() =&gt; {
  console.log(4)
})
console.log(5)
</code></pre><p>答案：</p><pre><code>2 3 5 4 1
</code></pre><p>解释</p><blockquote>这里有个知识点，<code>new Promise</code>的参数是同步执行的。</blockquote><blockquote>所以 <code>2 3 5</code>都是同步顺序输出的。</blockquote><blockquote>然后 <code>then</code> 在一个同步的for循环后触发，会追加到本阶段末尾，所以<code>4</code>紧接着输出。</blockquote><blockquote>最后是<code>setTimeout</code>，会在下一个循环的<code>timer</code>阶段执行，输出 <code>1</code></blockquote><p><strong>🐸 BOSS戦</strong></p><pre><code>setImmediate(() =&gt; {
  console.log(1)
  setTimeout(() =&gt; {
    console.log(2)
  }, 100)
  setImmediate(() =&gt; {
    console.log(3)
  })
  process.nextTick(() =&gt; {
    console.log(4)
  })
})
process.nextTick(() =&gt; {
  console.log(5)
  setTimeout(() =&gt; {
    console.log(6)
  }, 100)
  setImmediate(() =&gt; {
    console.log(7)
  })
  process.nextTick(() =&gt; {
    console.log(8)
  })
})
console.log(9)
</code></pre><p>答案：</p><pre><code>9 5 8 1 7 4 3 6 2
</code></pre><p>解释：</p><blockquote>你已经是一个成熟的程序员了，试着用上面的知识自己来解释吧。</blockquote><blockquote>Tips 可以尝试画出来，一共经过了多少个<strong>循环</strong>， 每个循环的每个<strong>阶段</strong>执行了什么。</blockquote> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 微前端实践的两个模型 ]]>
                </title>
                <description>
                    <![CDATA[ > 这里主要是提供基于 Qiankun 微前端落地的两种基本模(Tao)型(Lu)实践。 一般技术文章开头，为了凑字数，都会介绍一下什么是 Qiankun, 什么是 微前端, 他们有什么优点和解决了什么问题之类。 其实大可不必，我们直接展开。 术语前置 首先要定义一些术语，不然话都说不清楚了。  * 主应用 > 指的是微前端架构下，作为入口的应用。 一般还要承担加载子应用、识别前端路由、分发路由信息到子应用等职责。个别场景下还要管理状态，应用间通信等。  * 子应用 > 指的是具有业务特殊性的应用，可以理解为你当前主要开发的东西。在 Qiankun 下，一个系统有一个主应用，可以根据路由信息，分发到 N 个子应用 。 1.基座模型 基座模型是指，通过把共性封装到一个完好的主应用作为系统基座，然后把特性部分作为 子应用  集成进去，从而减轻开发任务。比较适用于一些管理后台项目。 图片很好地描述了基座模型下应用间的关系，同时清除地看到，子应用的功能开发，可以只关注自己的业务需求，而不需要重复实现 “登录注册”、“权限管理”  等等其他共性化需求。 🚀🚀� ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/qiankun-in-practice/</link>
                <guid isPermaLink="false">6077fe0b7a8acf0586ec8ddb</guid>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Thu, 15 Apr 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/christopher-gower-m_HRfLhgABo-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <blockquote>
<p>这里主要是提供基于 Qiankun 微前端落地的两种基本模(Tao)型(Lu)实践。</p>
</blockquote>
<p>一般技术文章开头，为了凑字数，都会介绍一下什么是 <strong>Qiankun</strong>, 什么是 <strong>微前端</strong>, 他们有什么优点和解决了什么问题之类。</p>
<p><strong>其实大可不必</strong>，我们直接展开。</p>
<h3 id="">术语前置</h3>
<p>首先要定义一些术语，不然话都说不清楚了。</p>
<ul>
<li>主应用</li>
</ul>
<blockquote>
<p>指的是微前端架构下，作为入口的应用。</p>
<p>一般还要承担加载子应用、识别前端路由、分发路由信息到子应用等职责。个别场景下还要管理状态，应用间通信等。</p>
</blockquote>
<ul>
<li>子应用</li>
</ul>
<blockquote>
<p>指的是具有业务特殊性的应用，可以理解为你当前主要开发的东西。在 Qiankun 下，一个系统有一个主应用，可以根据路由信息，分发到 N 个子应用 。</p>
</blockquote>
<h3 id="1">1.基座模型</h3>
<p>基座模型是指，通过把共性封装到一个完好的<code>主应用</code>作为系统基座，然后把特性部分作为  <code>子应用</code> 集成进去，从而减轻开发任务。比较适用于一些管理后台项目。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/332/1.png" alt="1" width="974" height="708" loading="lazy"></p>
<p>图片很好地描述了基座模型下应用间的关系，同时清除地看到，子应用的功能开发，可以只关注自己的业务需求，而不需要重复实现 <strong>“登录注册”</strong>、<strong>“权限管理”</strong> 等等其他共性化需求。</p>
<p>🚀🚀🚀</p>
<p>对于常见的管理后台项目，基座模型有天然的契合。因为管理后台几乎都具有很多公共的部分，只是具体“管理”的东西有差异。</p>
<p>所以，如果你有大量管理后台开发的需要，就可以尽可能地把更多的共性开发到一个主应用下，当一个新的管理后台项目发起时，只需要少了的开发（子应用），通过简单的集成，就能呈现一个完整的系统。</p>
<p><strong>注意事项</strong></p>
<p>因为大部分基础功能都在基座里实现了，所以要充分考虑从基座到业务应用的信息传递。</p>
<blockquote>
<p>比如说，用户信息、权限信息之类，可以通过 <code>props</code> 的方式传递给子应用，当然，你还可以传递函数，实现反向传递数据。</p>
</blockquote>
<p><strong>基座模型也有其缺陷，主要表现在 UI 定制化能力弱。</strong></p>
<p>因为基座都是现成的，而基座又决定了最终系统的大体颜值。（可能对于大部分管理后台来说，这个不太敏感）</p>
<p>于是乎，对于界面操控欲望更强的一些系统，我们就需要另一种实践模型。</p>
<h3 id="2">2.镶嵌模型</h3>
<p>在游戏中，一件优秀的装备除了基础属性牛X，它还可以通过镶嵌各种宝石来提供更多的加持。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/332/2.png" alt="2" width="908" height="652" loading="lazy"></p>
<p><strong>镶嵌模型的思路正好和基座模型想法，我们把一些独立的功能实现为子应用，把业务系统作为主应用来实现。</strong></p>
<p>业务团队除了开发自己的系统外，如果有复用已有能力的需要，只需要简单的接入需要的子应用即可。</p>
<p>这个模型下，业务开发对系统的颜值有完全的掌控。</p>
<p>而在能效方面，通过组合子应用，也在很大程度上节约了不少的开发投入。</p>
<p>这么模型的特点，比较适合那种 <strong>OEM 定制系统</strong>：</p>
<ul>
<li>客户对界面的独特性有要求；</li>
<li>同时对于乙方能提供的能力进行“按需”整合。</li>
</ul>
<p><strong>当然了，权力越大，事情越多</strong></p>
<p>界面定制能力强，决定了你需要操心 <strong>路由</strong>、<strong>菜单</strong>之类的实现，毕竟在镶嵌模型下，子应用只会提供 <strong>子页面</strong> 级别的能力复用，怎么把他们整合到当前项目下，不同的框架都有不同的要求。</p>
<blockquote>
<p>我这边只实践过 Antd-pro 还不算太麻烦。</p>
</blockquote>
<h3 id="">总结</h3>
<p>不管是那种模型，都是对 Qiankun 微前端架构的抽象理解。</p>
<p>目的都是能力复用、效能提升，患者可以根据自己团队和销售策略，灵活选择。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 做轮子·实现路由的两种思路 ]]>
                </title>
                <description>
                    <![CDATA[ > 本文指的路由，不是路由器，也不是PS4（5？），是特指后端API框架里的router，前端路由也有可以有借鉴的部分，但本文不承诺正确性。 今天不妨讨论个不那么大的话题，restful路由的实现。 我们眼中的路由是什么样子的？ 我们常见的路由是这个样子的 router.get('/what/i/want', ... ); router.get('/what/i/hate', ... ); router.get('/what/u/want', ... ); 我们今天就来讨论一下，当我们想访问 /what/i/want时，都有些什么玄机。 路由是怎么工作的 首先明确路由的目的，就是根据接收到的路径字符串，找到对应的业务处理逻辑。 为了弄明白业界常用框架对这一块，我专门查阅了若干开发框架的源码（express, koa, Gin, kratos, Lumen），最终概括成两种实现手段。  * Key - Value 型路由  * Tree 型路由 现在来分析一下两种路由的特性和适用场景。 Key - Value 型路由 这个模式，形如我们熟悉的hash tab ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-backend-router-work/</link>
                <guid isPermaLink="false">5f5103dccd07b005bfb5aeb6</guid>
                
                    <category>
                        <![CDATA[ 路由 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ RESTful API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 正则表达式 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Sat, 13 Feb 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/09/318-02.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <blockquote>
<p>本文指的路由，不是路由器，也不是PS4（5？），是特指后端API框架里的router，前端路由也有可以有借鉴的部分，但本文不承诺正确性。</p>
</blockquote>
<p>今天不妨讨论个不那么大的话题，<strong>restful路由</strong>的实现。</p>
<h4 id="">我们眼中的路由是什么样子的？</h4>
<p>我们常见的路由是这个样子的</p>
<pre><code>router.get('/what/i/want', ... );

router.get('/what/i/hate', ... );

router.get('/what/u/want', ... );
</code></pre>
<p>我们今天就来讨论一下，当我们想访问 <code>/what/i/want</code>时，都有些什么玄机。</p>
<h4 id="">路由是怎么工作的</h4>
<p>首先明确路由的目的，就是根据接收到的路径字符串，找到对应的业务处理逻辑。</p>
<p>为了弄明白业界常用框架对这一块，我专门查阅了若干开发框架的源码（express, koa, Gin, kratos, Lumen），最终概括成两种实现手段。</p>
<ul>
<li>Key - Value 型路由</li>
<li>Tree 型路由</li>
</ul>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/328/318-03.jpg" alt="318-03" width="1066" height="427" loading="lazy"></p>
<p>现在来分析一下两种路由的特性和适用场景。</p>
<h4 id="keyvalue">Key - Value 型路由</h4>
<p>这个模式，形如我们熟悉的<code>hash table</code>、字典，通过键值对维护这<code>uri</code>与函数的对应关系。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/328/318-04.jpg" alt="318-04" width="882" height="373" loading="lazy"></p>
<p>我们理所当然地认为，只要确定的访问地址， 如<code>/what/i/want</code>, 就能立即找到对应的业务方法<code>functionA</code>，是个铁骨铮铮的 <code>O(1)</code>操作。</p>
<blockquote>
<p>当一个操作的复杂度与规模无关，就是一个O(1)的操作。比如无论我的 uri 地址有多少个，只要确定 uri 的值，总是能立即确定与之对应的方法。</p>
</blockquote>
<p><strong>然而遗憾的是，我就没见到有这么实现路由的。</strong></p>
<p>真实的情况是，</p>
<p><strong>Key - Value 型路由会从前到后一个一个比较，直到找到一个能匹配当前 uri 的key，再确定它对应的 function</strong></p>
<p>所以实际上它是一个 <code>O(n)</code> 操作，平均情况下需要比较 <code>N/2</code> 次才能定位。</p>
<blockquote>
<p>一个 <code>O(n)</code> 操作，其复杂度会随着数据规模的增大而线性增长，而我们普遍认为，在经过多次测量观察后，平均情况下会在 N/2 的位置上找到答案。</p>
</blockquote>
<p>疑惑归疑惑，正如本文的作者一样，做轮子人并不是疯了。</p>
<p><strong>之所以采用这种逐个匹配的模式，是因为我们的uri通过是不能确定的。</strong></p>
<p>比如， 一些<code>uri</code>里甚至包含了变量</p>
<pre><code>GET /staff/9527

GET /staff/709394
</code></pre>
<p>所以，我们通常是吧 <strong>Key - Value</strong>里面的<strong>Key</strong>，设计成<strong>正则表达式</strong>。</p>
<p>相应的，在理论研究中的<strong>Hash Table</strong>也会被<strong>顺序表</strong>替代。</p>
<p>总结起来就是，</p>
<hr>
<p>当我们访问一个确定的<code>uri</code>时，路由会尝试按顺序逐个匹配注册好的<strong>正则表达式</strong>，直到<code>match</code>到一个结果。</p>
<p>否则，返回 404</p>
<hr>
<p><strong>升华一下</strong></p>
<blockquote>
<p>如何压榨这种路由的性能？ 既然是按顺序逐个匹配，那我们就根据自己的业务特点，把可能访问量最大的接口注册到前面去。</p>
<p>也有一些框架是会动态调整顺序的，这就是题外话。</p>
</blockquote>
<p>--</p>
<h4 id="tree">Tree 型路由</h4>
<p>这个模式相对来说思路要骚一些。</p>
<p>它把<code>uri</code>按照<code>/</code>分割一个，在之前构建好的路由树里，一个节点一个几点的检索，直到成功走到一个叶子节点。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/328/318-05.jpg" alt="318-05" width="744" height="644" loading="lazy"></p>
<p>与上面提到的<code>N/2</code>次比较相比，这个模式的比较次数比较稳定，就是<code>uri</code>的层数。</p>
<blockquote>
<p>对于 <code>/what/i/want</code>，我们认为它是一个 3 层的地址，如果这个地址是存在的，那么在比较3次以后，就能找到对应的答案。</p>
</blockquote>
<p><strong>真是 “遇事不决，数据结构” 啊</strong></p>
<p>这种树形的路由特别适合<code>restful</code>风格的API，因为我们设计<code>restful</code>接口的时候，通常每一层都有它自己的<strong>类聚含义</strong>，所以它会构建出一棵非常标致的树。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/328/318-06.jpg" alt="318-06" width="766" height="252" loading="lazy"></p>
<p><strong>问(gang)题(jing)来了</strong></p>
<p>Q1: 请问 Tree 型路由 怎么解决<code>uri</code>里有变量的情况呢？</p>
<blockquote>
<p>很巧妙。当你注册的路由包含了变量，比如 <code>/staff/:id</code>这样的，它会在这个树<code>staff</code>的子节点里，也就第二层，创建一个通配节点（可能是<code>*</code>）。</p>
<p>它认为，不管<code>staff</code>后面跟什么内容，都能匹配这个<code>*</code></p>
</blockquote>
<p>Q2: 如果我同时注册了<code>/staff/:id</code>和<code>/staff/list</code>呢？会匹配错吗？</p>
<blockquote>
<p>考虑到这种情况，Tree 型路由的一般实现是，优先匹配确定值<code>list</code>，次要匹配<code>*</code>。</p>
<p>有这种情况是要复杂一些，不过并不影响它的整体复杂度。</p>
</blockquote>
<h4 id="">最后，作者喜欢哪一种？</h4>
<p>尽管我在最近开发的一个框架里使用了<strong>Tree 型路由</strong>，这并不代表我的倾向。</p>
<p>实际上应该根据上文提到两种模式各自的特点，选择最好的实现方式。</p>
<p><strong>当然</strong></p>
<p>我期待读者自己做轮子的时候，能做出一个根据开发者注册的路由的特点，自动选择路由模型的<strong>智能路由</strong>。</p>
<hr>
<p>好了，今天先到这里，下次再会。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 一个不能再普通的函数，如何提供 http 服务？ ]]>
                </title>
                <description>
                    <![CDATA[ 前调：一些吐槽 因为开发过大大大大大大量的 Restful API ，越来越厌烦那种一成不变的代码组织方式。 以 egg为例，要增加一个接口，需要经历繁琐的操作 [路由里注册] ---> [编写 controller] ---> [编写 service]  哪怕我只是想实现一个 a+b => c！ 除此以外，我还调研过某几个比较大的 函数计算 服务商，看看有没有什么好的途径，可以更简便地进行简单接口开发。 让人很失望，天下技术一大抄，它们无一例外都是这样的开发模型： function(event, context, callback) {} 或者 function(event, context) {} 于是你不得不这样写你的代码 // 举例 function(event, context, callback) {   const { a, b } = event.arguments;    ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/way-to-build-restful-api-via-pure-function/</link>
                <guid isPermaLink="false">600a8ecf5f61e30501b5c037</guid>
                
                    <category>
                        <![CDATA[ 函数 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HTTP ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Fri, 22 Jan 2021 09:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/goran-ivos-TorAcb4AQRc-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h3 id="">前调：一些吐槽</h3>
<p>因为开发过大大大大大大量的 <code>Restful API</code> ，越来越厌烦那种一成不变的代码组织方式。</p>
<p>以 <code>egg</code>为例，要增加一个接口，需要经历繁琐的操作</p>
<pre><code>[路由里注册] ---&gt; [编写 controller] ---&gt; [编写 service] 
</code></pre>
<h4 id="abc">哪怕我只是想实现一个 <code>a+b =&gt; c</code>！</h4>
<p>除此以外，我还调研过某几个比较大的 <strong>函数计算</strong> 服务商，看看有没有什么好的途径，可以更简便地进行<strong>简单接口开发</strong>。</p>
<p>让人很失望，天下技术一大抄，它们无一例外都是这样的开发模型：</p>
<pre><code>function(event, context, callback) {}
或者
function(event, context) {}
</code></pre>
<p>于是你不得不这样写你的代码</p>
<pre><code>// 举例
function(event, context, callback) {
  const { a, b } = event.arguments; 
  callback(a+b);
}
</code></pre>
<h4 id="">这算个什么鬼函数？！</h4>
<p>你必须改变第一直觉，严格按照厂商的要求来写你的代码，并且寄希望于<strong>有人站出来要求它们统一<code>event</code>、<code>context</code>这些旁系知识的实现细节</strong>。</p>
<h3 id="">并且！</h3>
<p>你很难进行本地测试！</p>
<p>你要构造奇奇怪怪的对象作为参数。哪怕只是测试<strong>两数相加</strong>.</p>
<h5 id="ok">OK! 如果你说，稍微学习一下，其实还是可以用的吧。</h5>
<h4 id="">可以用就是我们的追求吗？要 <strong>应然</strong> 还是 <strong>实然</strong> ？</h4>
<h4 id="">难倒我们期望的样子，不应该是这样吗？</h4>
<pre><code>// 🔥🔥
function (a, b) {
	return a+b;
}
</code></pre>
<h3 id="">正文开始：</h3>
<p>所以，如果我需要实现一个 <code>a+b =&gt; c</code> 的接口，</p>
<p>我期望我的代码是这样的：</p>
<pre><code>export default (a: number, b: number): number =&gt; {
  return a + b;
}
</code></pre>
<p>我期望我的测试是这样的：</p>
<pre><code>describe('sum', () =&gt; {
  it('1+1 is 2', () =&gt; {
    assert.equal(sum(1,1), 2);
  });
});
</code></pre>
<h4 id="">十分明显好吗！</h4>
<p>它并不受制于具体平台实现；</p>
<p>它并不需要你学习其他旁系知识，只关注你的功能本身；</p>
<p>它方便测试，因为它就是一个不能再普通的函数；</p>
<p>它可复用！</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/331/1.png" alt="1" width="606" height="604" loading="lazy"></p>
<h3 id="">多说无益，直接动手</h3>
<pre><code>export default (a: number, b: number): number =&gt; {
  return a + b;
}
</code></pre>
<p>有过<a href="https://chinese.freecodecamp.org/news/practice-in-ast-and-code-gen/">上一篇</a>的基础，我们直奔抽象语法树：</p>
<pre><code>// 由于只有一行代码，结构就简单多了
{
	type: 'ExportDefaultDeclaration',
	declaration: {
		type: 'ArrowFunctionExpression',
		params: [ 下文展开 ],
		body: { 不重要 },
		returnType: { 下文展开 }
	}
}

</code></pre>
<p>我们还是从整体上先看一下，上面这个结构充分表达了源代码的意图：</p>
<ul>
<li>这是一个 <code>export default</code>声明 (<code>ExportDefaultDeclaration </code>)</li>
<li>声明的内容是一个箭头函数 (<code>ArrowFunctionExpression </code>)</li>
</ul>
<p>考虑到，我们希望从这一行代码里面，生成<strong>提供HTTP服务</strong>的基础代码，</p>
<p>那么我们的主要任务就是，从以上有限的信息当中，提取出<strong>Restful API</strong>需要的基础信息：</p>
<ul>
<li>Method</li>
<li>Path</li>
<li>请求参数</li>
</ul>
<h4 id="">🤖 我们一个一个来解决：</h4>
<ol>
<li>
<p>HTTP Method</p>
<p>首选是要确定这个接口，最终通过什么 <code>Method</code>对外服务，在这里源代码并不能提供任何有效信息。</p>
<p><strong>那么我们就默认用 GET 好了</strong></p>
<blockquote>
<p>🔥其他的 Method，会在文末提及</p>
</blockquote>
</li>
<li>
<p>Path， 或者叫 URL</p>
<p>对于一个接口来说，这也是十分关键的信息，这里我们有两种解决方案</p>
<ul>
<li>使用函数名，如果有的话</li>
<li>使用当前文件名</li>
</ul>
<p><strong>我们姑且把 URL 定为  <code>/sum</code> 好了</strong></p>
<blockquote>
<p>🔥 笔者的一个实验性项目里，使用的是文件名</p>
</blockquote>
</li>
<li>
<p>请求参数</p>
<p>如果说前面两点都是基于约定，或者一些简单的手段，</p>
<p>那么在请求参数这个环节，我们必须上价值了，要让 <code>ast</code> 发挥作用！</p>
<p>我们先展开一下上面 <code>ast</code> 里关于函数参数的部分：</p>
<pre><code>// params 部分
[
	{
		name: 'a',
		typeAnnotation: { type: 'TSNumberKeyword' }
	},
	{
		name: 'b',
		typeAnnotation: { type: 'TSNumberKeyword' }
	},
]
</code></pre>
<h4 id="ab">好家伙，可以拿到参数名 <code>a</code> 和 <code>b</code> 了</h4>
<p>结合 <code>TSNumberKeyword</code> 信息，我们甚至能笃定这两个参数是数字类型。</p>
<h4 id="">知道类型，就可以做参数校验！</h4>
<blockquote>
<p>🔥通过原始函数定义的参数类型生成 http 参数校验逻辑，比起手工编写 <code>Joi</code> 配置要可靠得多。</p>
</blockquote>
<h4 id="">等等，还有一个问题没解决</h4>
<p>参数名和参数类型都有了，缺的就是参数位置以及 <code>content-type</code> 。</p>
<p>约定大法好，根据多年开发总结得出：</p>
<p><strong>总所周知， GET 请求的参数就放在 queryString 里吧。</strong></p>
<p><strong>至于 <code>content-type</code> 统一使用 <code>application/json</code> 不接受反驳</strong></p>
</li>
</ol>
<h3 id="">齐活了</h3>
<p>结合上面收集到的信息，我们可以想象最终的场景是：</p>
<ul>
<li>step 1 : 按上面的方式，编写一个不能再普通的函数</li>
<li>step 2: 使用我们编写好的工具，运行这个函数</li>
<li>step 3: 可以通过 <code>http://127.0.0.1:3000/sum?a=11&amp;b=22</code> 来访问这个接口，并且得到结果 33</li>
</ul>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/331/2.png" alt="2" width="1700" height="1270" loading="lazy"></p>
<p>通过这个截图可知，虽然实现细节比较多，但是<strong>可行性</strong>是没问题的。</p>
<p>并且笔者已经做出了一个实验性项目。</p>
<p>如果对这种模式比较感兴趣，欢迎前来讨论，在这里就不打广告了。</p>
<h3 id="faq">FAQ 环节</h3>
<ul>
<li>
<p>更多的 HTTP_METHOD 怎么办？总不能都用 GET 吧</p>
<blockquote>
<p>目前我选能用的方案是，如果没有声明，就用一个默认的 <code>GET</code>，</p>
</blockquote>
<blockquote>
<p>因为要尽量选一个能覆盖 90% 情况的作为默认值。</p>
</blockquote>
<blockquote>
<p>当我需要使用其他 Method ，我的方案是显式指定，比如</p>
</blockquote>
<pre><code>export const method = 'POST';

export default (a: number, b: number): number =&gt; {
  return a + b;
}
</code></pre>
<p>这个方案可以使信息更紧凑，同时不影响函数逻辑跑本地单元测试。</p>
</li>
<li>
<p>上面提到的入参参数位置，什么时候在 <code>queryString</code>，什么时候在其他？</p>
<blockquote>
<p>遵从大多数的案例，GET 的情况下使用 <code>queryString</code>， 其他情况下在 <code>body</code></p>
</blockquote>
<blockquote>
<p>当然还有完全自定义的方案，为避广告嫌疑就不展开了，基本原则还是不影响函数核心逻辑和单元测试。</p>
</blockquote>
</li>
<li>
<p>上面提到的 入参参数类型 ，有什么应用场景？</p>
<blockquote>
<p>有了这个信息，如果你喜欢 <code>Joi</code>， 应该能很容易生成参数校验的代码了。</p>
</blockquote>
<blockquote>
<p>或者有自己想法的话，和我一样，自己实现一套 <code>validator</code> 也不是什么难事。</p>
</blockquote>
</li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 左手抽象语法树，右手自动化编程 ]]>
                </title>
                <description>
                    <![CDATA[ > 随着软件开发技术的不断进步，处理偶发复杂度的行为都有可能被自动化、被机器取代；只有处理本质复杂度的建模行为，无法被机器取代。 —— 熊大 引用上面一段话，是因为它很好地点出了这几年我一直专注的方向，如何让技术工作者专注于无法被机器取代的部分，而不是拼体力去和自动化机器抢生意。 再不努力你就要被机器取代！ 为什么会有这种焦虑？并不是源于现在网络上一直吹嘘的人工智能，而是来自自我反省。 最近在研究一些代码质量分析系统的时候，其中有一项就是代码的重复率。意思就是你一直以来提交的代码里，在做多少重复的劳动？ 且不论这个系统的科学性和准确性，但它的确给了我一个声音，我从直觉是感到自己平时也写很多重复代码。 好了，感慨完进入正题。首先，怎么找到重复劳动？ 我们看一段代码： import * as mongoose from 'mongoose'; enum Lang { js, java, c } export interface IUserModel extends mongoose.Document {   name: string;   age: number;   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/practice-in-ast-and-code-gen/</link>
                <guid isPermaLink="false">5ffd5b195f61e30501b5b796</guid>
                
                    <category>
                        <![CDATA[ 自动化 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Tue, 12 Jan 2021 09:18:33 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/martin-shreder-5Xwaj9gaR0g-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <blockquote>
<p>随着软件开发技术的不断进步，处理偶发复杂度的行为都有可能被自动化、被机器取代；只有处理本质复杂度的建模行为，无法被机器取代。 —— 熊大</p>
</blockquote>
<p>引用上面一段话，是因为它很好地点出了这几年我一直专注的方向，如何让技术工作者专注于无法被机器取代的部分，而不是拼体力去和自动化机器抢生意。</p>
<h4 id="">再不努力你就要被机器取代！</h4>
<p>为什么会有这种焦虑？并不是源于现在网络上一直吹嘘的人工智能，而是来自自我反省。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/329/1.png" alt="1" width="600" height="400" loading="lazy"></p>
<p>最近在研究一些代码质量分析系统的时候，其中有一项就是代码的重复率。意思就是你一直以来提交的代码里，在做多少重复的劳动？</p>
<p>且不论这个系统的科学性和准确性，但它的确给了我一个声音，我从直觉是感到自己平时也写很多重复代码。</p>
<h4 id="">好了，感慨完进入正题。首先，怎么找到重复劳动？</h4>
<p>我们看一段代码：</p>
<pre><code>import * as mongoose from 'mongoose';

enum Lang { js, java, c }

export interface IUserModel extends mongoose.Document {
  name: string;
  age: number;
  language: Lang;
}

const schema = new mongoose.Schema(
  {
    name: {
      type: String,
      require: true, 
    },
    age: {
      type: Number,
      require: true, 
    },
    language: {
      type: Lang,
      require: true, 
      enum: [ Lang.js, Lang.java, Lang.c ],
    },
  },
  {
    usePushEach: true,
    timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
  },
);

export default mongoose.model('user', schema);

</code></pre>
<p>如何识别上面的重复代码？表面看来，真正在运行中有用的部分，是 <code>new mongoose/Schema(...)</code> 这部分，但我们判断的依据并不在此。</p>
<blockquote>
<p>代码编写实际上是体现了我们的主观意图。</p>
</blockquote>
<p>抛开其他庞杂的知识体系，在这段代码里，我们的主观意图是<strong>定义一个用户类型的模型</strong>。再简化一下，就是<strong>定义用户类型</strong>。</p>
<p>所以我思考上面这几十行代码，哪些是代表我自己的主观意图，哪些是机器没法代替的部分呢？</p>
<p>其实仅仅是用户接口类型的定义：</p>
<pre><code>enum Lang { js, java, c }

export interface IUserModel extends mongoose.Document {
  name: string;
  age: number;
  language: Lang;
}
</code></pre>
<p>至于如何使用 <code>mongoose</code>去定义一个 <code>Schema</code>，再导出一个<code>Model</code>实例，这些每次都要我翻看在线文档的操作，我恨不得交给计算机去做。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/329/2.jpg" alt="2" width="600" height="400" loading="lazy"></p>
<p><strong>而接下来我就是要让计算机来完成这个任务。</strong></p>
<blockquote>
<p>🤡 这个真的可行吗？</p>
<p>🤖 理论上是可以的，只要你确定某些操作，完全没有注入自己的脑力创作元素，那它一定可以被计算机替代。</p>
</blockquote>
<p>如果这真的可以做到，那么理想情况下，我们需要自己编写的代码，只剩下这些。</p>
<pre><code>import * as mongoose from 'mongoose';

enum Lang { js, java, c }

export interface IUserModel extends mongoose.Document {
  name: string;
  age: number;
  language: Lang;
}
</code></pre>
<p>很好，只定义了一个用户的接口类型，符合我们的主观意图。</p>
<p>然后使用程序<strong>分析这段代码</strong>：</p>
<pre><code>//  因为讲解需要，对原始 AST 输出做了简化，突出重点

[
  {
    type: 'ImportDeclaration',
    source: {
      value: 'mongoose'
    },
  },
  {
    type: 'TSEnumDeclaration',
    id: { type: 'Identifier', name: 'Lang' },
    members: [ 'js', 'java', 'c' ]
  },
  {
    type: 'ExportNamedDeclaration',
    declaration: {
      type: 'TSInterfaceDeclaration',
      body: [{...},{...},{...}],
      id: { name: 'IUserModel' },
      extends: ['Document']
    },
  }
]

</code></pre>
<p>可见，我们只写了三行代码：</p>
<ol>
<li>引入一个依赖 mongoose</li>
<li>定义了 Lang 枚举类型</li>
<li>导出了 用户接口，继承自 Document</li>
</ol>
<p>从以上信息里，我需要机器人捕捉到的关键是第三行代码🔍。</p>
<p>我们可以根据<code>AST</code>结构的特征，让机器人只处理 <code>TSInterfaceDeclaration </code>并且 <code>extends</code> 自 <code>Document</code>的，这个逻辑简单，自然是不需要贴代码的，大家都能理解。</p>
<h4 id="tsinterfacedeclaration">然后我们需要让机器人明白我们的  <code>TSInterfaceDeclaration </code> 语句，定义了一个怎样的（包含哪些字段类型）的接口类型。</h4>
<p>我们再展开一下上面的 <code>body: [{...},{...},{...}],</code></p>
<pre><code>// 因为讲解需要，对原始 AST 输出做了简化，突出重点

[
  {
    key: { name: 'name' },
    typeAnnotation: {
      type: 'TSStringKeyword',
    }
  },
  {
    key: { name: 'age' },
    typeAnnotation: {
      type: 'TSNumberKeyword',
    }
  },
  {
    key: { name: 'language' },
    typeAnnotation: {
      type: 'TSTypeReference',
      typeName: { name: 'Lang' }
    }
  }
]
</code></pre>
<p>这部分如果比较晦涩，可以对照着 <code>IUserModel</code>的定义，就很好读懂了。</p>
<p>主要就是表达了<code>IUserModel</code>里三个字段的类型定义，已经能清楚看出<code>name</code>是一个字符串类型，<code>age</code>是一个数字类型，而<code>language</code>则是一个<code>Lang</code>的引用类型。</p>
<h4 id="">分析完毕，重复工作一次编写</h4>
<p>代码分析环节到这里几乎就到了柳暗花明枯木逢春的境地了。</p>
<p>我们的主观意图很好地传递给了机器人，然后，我们让机器人根据 <strong>mongoose文档</strong> 的要求，书写后续的 <code>Schema</code>和<code>Model</code>。</p>
<p>因为 <code>TS</code>的类型定义，比如<code>TSStringKeyword </code>， 也就是我们平时书写的 <code>:string</code>，总能找到一个对应的<code>mongoose SchemaType</code>，比如 <code>mongoose.Schema.Types.String</code>。</p>
<p><strong>我们平时就是花了比较多的时间在写这种重复代码。</strong></p>
<p>最后，因为本文并不是工具开发介绍，就不贴具体实现了。如果有人感兴趣，后续我视情况再写一篇《实践》。</p>
<p>送一个动图，证明我没说谎。</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/3.gif" class="kg-image" alt="3" width="600" height="400" loading="lazy"></figure> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 你知道在 Nginx.conf 里可以写 JavaScript 吗？ ]]>
                </title>
                <description>
                    <![CDATA[ 老夫万万没想到，几年前无意间学了 NJS ，最近竟然用上了。 是的你没看错，如果你会写javascript，你甚至已经是一名Nginx配置管理员。 今天我们就通过一个例子，带大家初步浅入门 NJS。 > 什么是 NJS [https://nginx.org/en/docs/njs/] 前置知识：  1. 一点点 nginx.conf 的认识  2. javascript基本语法  3. 其他作为程序员的基本素质 0x00 场景介绍（纯杜撰） 案件的经过是这样的，最近有一个新的后端项目，希望接入到我们已有的微服务集群里。 然后我好像明白了一些事情： > Python 开发效率第一条：不做复杂的事情。 不过既然阿刚硬刚，恰好我有解决的方案，就不妨接受挑战吧。 首先梳理一下原始需求：  1. 真实的业务接口部署在我们的集群里  2. 真实的业务接口需要做用户鉴权，如：判断用户是否登录  3. 真实的业务接口不希望请求外部接口，希望网关放进来的都是已经登录的 如果做到了以上三点，他们确实可以很轻松，这理由很“充分”。 所以我指定了这个方案： 好了交代完背景 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/using-javascript-in-nginx/</link>
                <guid isPermaLink="false">5ffdbaf55f61e30501b5b892</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NGINX ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Tue, 12 Jan 2021 02:39:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/dean-pugh-C8NDn4xk9zs-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>老夫万万没想到，几年前无意间学了 NJS ，最近竟然用上了。</p>
<p>是的你没看错，如果你会写<code>javascript</code>，你甚至已经是一名<code>Nginx</code>配置管理员。</p>
<p>今天我们就通过一个例子，带大家初步浅入门 <code>NJS</code>。</p>
<blockquote>
<p><a href="https://nginx.org/en/docs/njs/">什么是 NJS</a></p>
</blockquote>
<p>前置知识：</p>
<ol>
<li>一点点 nginx.conf 的认识</li>
<li>javascript基本语法</li>
<li>其他作为程序员的基本素质</li>
</ol>
<p><img src="https://nginx.org/nginx.png" alt="nginx" width="600" height="400" loading="lazy"></p>
<h3 id="0x00">0x00 场景介绍（纯杜撰）</h3>
<p>案件的经过是这样的，最近有一个新的后端项目，希望接入到我们已有的微服务集群里。</p>
<img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/330/330-1.png" style="width:300px;height:auto;margin: 0 auto; display:block" width="600" height="400" alt="330-1" loading="lazy">
<p>然后我好像明白了一些事情：</p>
<blockquote>
<p>Python 开发效率第一条：不做复杂的事情。</p>
</blockquote>
<h4 id="">不过既然阿刚硬刚，恰好我有解决的方案，就不妨接受挑战吧。</h4>
<p>首先梳理一下原始需求：</p>
<ol>
<li>真实的业务接口部署在我们的集群里</li>
<li>真实的业务接口需要做用户鉴权，如：判断用户是否登录</li>
<li>真实的业务接口不希望请求外部接口，希望网关放进来的都是已经登录的</li>
</ol>
<p>如果做到了以上三点，他们确实可以很轻松，这理由很“充分”。</p>
<p>所以我指定了这个方案：</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/330/2.png" alt="2" width="600" height="400" loading="lazy"></p>
<h3 id="">好了交代完背景我们进入很干的正题</h3>
<p>NJS 真是一个好东西，我们平时看到的 <code>Nginx.conf</code>，最多就做做反向代理、负载均衡。</p>
<p>现在告诉你，通过 <code>javascript</code> 还能给<code>Nginx</code>赋能一些逻辑，一众 <code>JSer</code>自然是很兴奋。</p>
<blockquote>
<p>🤡 Nginx 高手竟在我身边。</p>
</blockquote>
<p>那么我们看看怎么做：</p>
<pre><code>// auth.js
function auth(r) {
	r.subrequest('/auth') // 假设我们原本需要调用这个接口来检查登录
	  .then((reply) =&gt; {
	      if (reply.status !== 200) {
	        throw '未登录';
	      }
	  }).then(() =&gt; {
	    	r.subrequest(r.uri, {})
	    	// 隐去了一些不必要的细节
	  }).catch(e =&gt; {
	  	r.return(401, e);
	  });
}

export default { auth };
</code></pre>
<p>直接贴代码，我觉得我也很刚。</p>
<p>上面这段简单的 <code>JS</code> 代码，相信大家也能看明白的大概。</p>
<p>意思就是，当请求进来时，首先去内部请求一下 <code>/auth</code> 接口；</p>
<p>如果返回的不是200，就告诉前端 <code>401 未登录</code>；</p>
<p>否则，再发起内部请求到真实业务接口 <code>r.uri</code>，得到真正的业务接口返回数据。</p>
<h3 id="jsnginxconf">编写完JS逻辑，这事还没完，需要在 Nginx.conf 里增加点东西</h3>
<pre><code>// nginx.conf (只贴关键内容)
# 配置文件开头：载入 NJS 模块
load_module modules/ngx_http_js_module.so;  
...
...
http {
  # 通过 js_import 引入上面的JS代码
  js_import main from auth.js;
  ...
  ...
  server {
    ...
    ...
  location ~ ^/ {
    # 所有请求都通过上面的JS代码来处理了
    js_content main.auth; 
  }
</code></pre>
<p><strong>🔥🔥🔥因为上面的分解动作，都是在 Nginx 内部转发完成的，所以在前端的感受上就只是发生了一次简单的请求。</strong></p>
<p>而对于躲在背后的业务接口来说，</p>
<p><strong>开发接口的时候不需要关心用户鉴权，直接裸奔上阵。</strong></p>
<h3 id="">🤡当然是皆大欢喜的事情，很简单是不是？</h3>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 一篇就够·两种分布式ID服务的实现 ]]>
                </title>
                <description>
                    <![CDATA[ 🦆分布式ID的需求背景 我试图把这个内容说得直白一些。 分布式ID服务，是指在我们的分布式服务群里，有那么一个(组)服务，啥也不干只管给其他系统发放ID。 当然这只是目前来说的最终状态，我们不妨回到过去，看看分布式ID是怎么一步一步走到今天这个样子的。  * 上古时代，ID由数据库自增提供 一开始，我们的ID根本不会考虑分布式的问题，对于每个实体类型，完全依赖数据库的自增ID安排就好了。 不过随着业务量增大，一个问题出现了：自增ID是服务于单库的，对于大并发写入，就捉襟见肘了。  * 分库自增ID的方案        当单点不满足我们的写入性能要求，我们就分库好了，通过一些特定的哈希规则，把数据分别写到不同的数据库实例里。        那怎么保证几个库的自增ID不重叠呢？原来，数据库自增ID还能设置起始值和步长。                不过，这个方案需要你前期就对业务规模发展有一个准确的估计，因为后续继续扩容节点，是一件十分麻烦的事。       ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/distributed-id/</link>
                <guid isPermaLink="false">5f3b71abc8da7105cbc14b99</guid>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Tue, 18 Aug 2020 06:17:13 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/08/001-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <h3 id="id">🦆分布式ID的需求背景</h3>
<p>我试图把这个内容说得直白一些。</p>
<p>分布式ID服务，是指在我们的分布式服务群里，有那么一个(组)服务，啥也不干只管给其他系统发放ID。</p>
<p>当然这只是目前来说的最终状态，我们不妨回到过去，看看分布式ID是怎么一步一步走到今天这个样子的。</p>
<ul>
<li>上古时代，ID由数据库自增提供</li>
</ul>
<p>一开始，我们的ID根本不会考虑分布式的问题，对于每个实体类型，完全依赖数据库的自增ID安排就好了。</p>
<p>不过随着业务量增大，一个问题出现了：自增ID是服务于单库的，对于大并发写入，就捉襟见肘了。</p>
<ul>
<li>
<p>分库自增ID的方案</p>
<p>当单点不满足我们的写入性能要求，我们就分库好了，通过一些特定的哈希规则，把数据分别写到不同的数据库实例里。</p>
<p>那怎么保证几个库的自增ID不重叠呢？原来，数据库自增ID还能设置起始值和步长。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/327/002.png" alt="002" width="1126" height="842" loading="lazy"></p>
<p>不过，这个方案需要你前期就对业务规模发展有一个准确的估计，因为后续继续扩容节点，是一件十分麻烦的事。</p>
</li>
</ul>
<blockquote>
<p>有多麻烦？有人可以自己想想吗？</p>
</blockquote>
<ul>
<li>
<p>再后来，干脆把自增ID这个能力独立出来</p>
<p>上面提到的方案，不管是单点还是分库，都是每种实体类型管理自己的ID序列。比如订单、商品、售后记录这些，分别有自己的ID序列，它们分别在不同的平行世界。</p>
<p>后来人们发现，我们产生了一种新的需求：不同的实体类型公用一个ID序列。</p>
<p>这个时候，就回到了本文顶部的那张图。业务流程会变成:</p>
<ol>
<li>服务A需要新增一条记录</li>
<li>服务A向IDs申请一个id</li>
<li>服务A把业务数据和id写到数据库</li>
</ol>
<blockquote>
<p>想想这样做的好处是什么？</p>
</blockquote>
</li>
</ul>
<h3 id="">🦆业界最常用的两种实现</h3>
<p>上面交代完需求背景，我们接下来实现一个分布式ID服务。</p>
<p>业界这类开源的项目比较多，我们今天并不会讲解开源项目，而是介绍一下两种常见的分布式ID生成实现。</p>
<h3 id="">🦆号段模式</h3>
<h4 id="">是什么</h4>
<p><strong>号段模式</strong>和前面说到的历史方案，都是依赖数据库来管理ID生成与分配。区别在于，以往的实现里，没请求一个ID，都要产生一次数据库写入操作，注定性能上不去。</p>
<p><strong>号段模式</strong>对此的改良，是一次性申请一个区间的ID，比如 <code>[1,100)</code>。如此一来，通过一次数据库写入，提供100次的ID请求，大大提升了性能指标。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/327/004.png" alt="004" width="1130" height="478" loading="lazy"></p>
<h4 id="">怎么做</h4>
<p>上面说到的原理，大家都能理解。</p>
<p>一般来说，我们需要提前在数据库里初始化一些信息。如</p>
<table>
<thead>
<tr>
<th>name</th>
<th>current</th>
<th>step</th>
</tr>
</thead>
<tbody>
<tr>
<td>order</td>
<td>100</td>
<td>100</td>
</tr>
<tr>
<td>product</td>
<td>200</td>
<td>100</td>
</tr>
</tbody>
</table>
<p>这个表格记录了每一种类型(order / product)序列，当前分配出去的最大ID(<code>current</code>), 以及当前序列每次分配的ID数量(<code>step</code>)。</p>
<p>那么，当<code>order</code>序列分配 <strong>100</strong> 时，ID池用完的情况下，再次向数据库申请一批(step个)ID。</p>
<p>此时，数据库记录会更新为</p>
<table>
<thead>
<tr>
<th>name</th>
<th>current</th>
<th>step</th>
</tr>
</thead>
<tbody>
<tr>
<td>order</td>
<td>200</td>
<td>100</td>
</tr>
</tbody>
</table>
<p>通过这个例子，我们很容易发现，一次数据库写入(更新)操作，可以应付 100 次的服务请求。<br>
服务的性能指标理论上提升了100倍。</p>
<h3 id="snowflake">🦆snowflake</h3>
<p>另一个值得介绍的分布式ID方案，就是大名鼎鼎的 <code>snowflake</code>算法。</p>
<h4 id="">背景&amp;解决什么问题</h4>
<p>snowflake是Twitter开源的分布式ID生成算法，它给一个long类型的每个bit都分别赋予了特定的含义。</p>
<p>具体到一个64位整型：</p>
<ul>
<li>第0位固定为0，表示非负数</li>
<li>接下来41位来表示时间，单位是毫秒，2^41 可以存69年多一点</li>
<li>接下来的10位，原理上说是5位表示机房，5位表示机器，在实际应用在，你可以赋予它不一样的含义，总的来说是用来区分机器的(或者说服务的实例)。这里共可以区分1024个节点，或者1024个容器实例。</li>
<li>最后12位，是服务运行时，自己维护的一个序号，每次生成一个新的ID之后，就自己加一。这里的容量是4096。</li>
</ul>
<p>综上，如果你采取上面的编排方案，意味着你在一个有1024个节点的分布式ID服务集群里，每毫秒能产生4096个不同的ID。</p>
<blockquote>
<p>实际上不会有谁弄个1024个节点的ID服务，这个方案的并发上限非常高。</p>
</blockquote>
<h4 id="">技术实现</h4>
<p><code>snowflake</code>有太多开源的实现，一般不需要我们自己手写。</p>
<p>不过我们还可以根据自己的需要，参考这个算法实现一套规模更小的 <code>snowflake</code>。</p>
<p>比如我只需要32位的整形（有些语言就可怜的没法表达64位），</p>
<p>或者根据自己的需要调整每一段的位数，有的实现是时间容量更大，有的实现是节点数容纳更多，反正比较自由。</p>
<h3 id="">🦆自己手写两种实现时，碰到的一些细节问题记录</h3>
<p>然后我试着按照自己对以上两种模式的理解，分别实现的<strong>号段模式</strong>和<strong>snowflake</strong>。</p>
<p>代码在这里  <a href="https://github.com/mohism-framework/id">github地址</a></p>
<p>并不是要star，而是总结了整个过程中的一些感悟：</p>
<ol>
<li>
<p>号段模式注意边界切换</p>
<p>实现号段模式时，每次会提前申请一批ID。当系统分配完一批ID，而这时又有新的请求进来时，我们称之为边界。</p>
<p>为了避免这个时候去操作数据库而产生“卡顿”，我采取了提前申请下一组ID的措施。就是当存量ID使用超过80%时，异步提前申请下一批ID做后备，当到达边界时，可以做到无缝切换。<br>
<img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/327/005.png" alt="005" width="1192" height="178" loading="lazy"></p>
</li>
<li>
<p>javascript专属问题</p>
<p>因为我是使用ts开发，不可避免要面对js一些根源性的问题，比如没有没有64位整形。</p>
<ul>
<li>
<p>位运算左移的坑</p>
<p>在实现<code>snowflake</code>时，就会用到左移操作 <code>&lt;&lt;</code>, 而在js的实现里，左移最多支持int32的范围，再大就溢出了。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/327/006.png" alt="006" width="448" height="144" loading="lazy"></p>
<p>但是众所周知 js 的数字类型是遵从 <a href="https://en.wikipedia.org/wiki/Floating-point_arithmetic">IEEE 754</a> ，理论上可以支持很大的数字。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/327/007.png" alt="007" width="700" height="216" loading="lazy"></p>
<p>所以这个是无解的，最后我使用了 <strong>Bigint</strong> 类型。</p>
<p>关于 <strong>BigInt</strong> 的更多信息，请参考 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/BigInt">BigInt[MDN]</a></p>
</li>
<li>
<p>传输时，还是要变成字符串</p>
<p>考虑到这个服务需要提供<code>restful</code>接口，在实际传输中，我还是把ID变成字符串来传输。</p>
<pre><code class="language-javascript">main.get(name).then((id: bigint) =&gt; {
    res.end(JSON.stringify({
      id: id.toString(),
    }));
  })
</code></pre>
<p>因为<strong>BigInt</strong>不能直接放到json里去返回（这个希望有人可以给出解决方案。）</p>
</li>
</ul>
</li>
</ol>
<!--kg-card-end: markdown--><p><a href="https://lanhao.name/327">原文地址</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ JavaScript 隐式类型转换，一篇就够了！ ]]>
                </title>
                <description>
                    <![CDATA[ JavaScript 隐式类型转换，一篇就够了 ！ 一图胜千言 数学算符中的类型转换  * 减、乘、除  * 特殊的加法 逻辑语句中的类型转换  * 单个变量  * 使用 == 比较中的5条规则  * 通过几个特别的题目来练习一下吧 附录1：类型转换表 一图胜千言 JavaScript发生隐式类型转换时的各种猫腻，相信各位开发者已经饱受折磨。 即便是多年的 JS 老兵，也不一定能很好地理顺这背后的规律。 接下来，本文通过几个产生隐式类型转换的场景，务必帮助你彻底掌握这个知识点。 > 提示 1：请充分注意到行文中出现的 ⭐️， 意味着这是重点句子。 > 提示 2：阅读过程，可以随时翻看【附录】 数学运算符中的类型转换 因为 JS 并没有类型声明，所以任意两个变量或字面量，都可以做加减乘除。 1. 减、乘、除 ⭐️我们在对各种非Number类型运用数学运算符(- * /)时，会先将非Number类型转换为Number类型。 1 - true ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/javascript-implicit-type-conversion/</link>
                <guid isPermaLink="false">5e0b3c28ca1efa04e196ac94</guid>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Thu, 16 Jul 2020 07:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/07/web-design-2906159_960_720.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>JavaScript 隐式类型转换，<strong>一篇就够了</strong> ！</p><p><a href="#-">一图胜千言</a></p><p><a href="#--1">数学算符中的类型转换</a></p><ul><li><a href="#1-">减、乘、除</a></li><li><a href="#2-">特殊的加法</a></li></ul><p><a href="#--2">逻辑语句中的类型转换</a></p><ul><li><a href="#1--1">单个变量</a></li><li><a href="#2-5-">使用 == 比较中的5条规则</a></li><li><a href="#--3">通过几个特别的题目来练习一下吧</a></li></ul><p><a href="#-1-">附录1：类型转换表</a></p><h2 id="-">一图胜千言</h2><p><code>JavaScript</code>发生隐式类型转换时的各种猫腻，相信各位开发者已经饱受折磨。</p><p>即便是多年的 JS 老兵，也不一定能很好地理顺这背后的规律。</p><p>接下来，本文通过几个产生<strong>隐式类型转换</strong>的场景，务必帮助你彻底掌握这个知识点。</p><blockquote>提示 1：请充分注意到行文中出现的 ⭐️， 意味着这是重点句子。</blockquote><blockquote>提示 2：阅读过程，可以随时翻看【附录】</blockquote><figure class="kg-card kg-image-card"><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/how-it-fucking-work.png" class="kg-image" alt="how-it-fucking-work" width="1158" height="326" loading="lazy"></figure><h2 id="--1">数学运算符中的类型转换</h2><p>因为 JS 并没有类型声明，所以任意两个变量或字面量，都可以做加减乘除。</p><h3 id="1-">1. 减、乘、除</h3><p><strong>⭐️我们在对各种非<code>Number</code>类型运用数学运算符(<code>- * /</code>)时，会先将非<code>Number</code>类型转换为<code>Number</code>类型。</strong></p><pre><code class="language-javascript">1 - true // 0， 首先把 true 转换为数字 1， 然后执行 1 - 1
1 - null // 1,  首先把 null 转换为数字 0， 然后执行 1 - 0
1 * undefined //  NaN, undefined 转换为数字是 NaN
2 * ['5'] //  10， ['5']首先会变成 '5', 然后再变成数字 5
</code></pre><blockquote>上面例子中的 ['5']的转换，涉及到<strong>拆箱操作</strong>，将来有机会再出一篇文章说明。</blockquote><h3 id="2-">2. 加法的特殊性</h3><p>⭐️为什么加法要区别对待？因为JS里 <code>+</code>还可以用来拼接字符串。谨记以下3条：</p><ul><li>当一侧为<code>String</code>类型，被识别为字符串拼接，并会优先将另一侧转换为字符串类型。</li><li>当一侧为<code>Number</code>类型，另一侧为原始类型，则将原始类型转换为<code>Number</code>类型。</li><li>当一侧为<code>Number</code>类型，另一侧为引用类型，将引用类型和<code>Number</code>类型转换成字符串后拼接。</li></ul><p>⭐️以上 3 点，优先级从高到低，即 <code>3+'abc'</code> 会应用规则 1，而 <code>3+true</code>会应用规则2。</p><pre><code class="language-javascript">123 + '123' // 123123   （规则1）
123 + null  // 123    （规则2）
123 + true // 124    （规则2）
123 + {}  // 123[object Object]    （规则3）
</code></pre><h2 id="--2">逻辑语句中的类型转换</h2><p>当我们使用 <code>if</code> <code>while</code> <code>for</code> 语句时，我们期望表达式是一个<code>Boolean</code>，所以一定伴随着隐式类型转换。而这里面又分为两种情况：</p><h3 id="1--1">1.单个变量</h3><p>⭐️如果只有单个变量，会先将变量转换为Boolean值。</p><p>我们可以参考<strong>附录</strong>的转换表来判断各种类型转变为<code>Boolean</code>后的值。</p><p>不过这里有个小技巧：</p><p>只有 <code>null</code> <code>undefined</code> <code>''</code> <code>NaN</code> <code>0</code> <code>false</code> 这几个是 <code>false</code>，其他的情况都是 <code>true</code>，比如 <code>{}</code> , <code>[]</code>。</p><h3 id="2-5-">2.使用 == 比较中的5条规则</h3><p>虽然我们可以严格使用 <code>===</code>，不过了解<code>==</code>的习性还是很有必要的。</p><p>⭐️根据 <code>==</code> 两侧的数据类型，我们总结出 5 条规则：</p><ul><li>规则 1：<code>NaN</code>和其他任何类型比较永远返回<code>false</code>（包括和他自己）。</li></ul><pre><code class="language-javascript">NaN == NaN // false
</code></pre><ul><li>规则 2：Boolean 和其他任何类型比较，Boolean 首先被转换为 Number 类型。</li></ul><pre><code class="language-javascript">true == 1  // true 
true == '2'  // false, 先把 true 变成 1，而不是把 '2' 变成 true
true == ['1']  // true, 先把 true 变成 1， ['1']拆箱成 '1', 再参考规则3
true == ['2']  // false, 同上
undefined == false // false ，首先 false 变成 0，然后参考规则4
null == false // false，同上
</code></pre><ul><li>规则 3：<code>String</code>和<code>Number</code>比较，先将<code>String</code>转换为<code>Number</code>类型。</li></ul><pre><code class="language-javascript">123 == '123' // true, '123' 会先变成 123
'' == 0 // true, '' 会首先变成 0
</code></pre><p>规则 4：<code>null == undefined</code>比较结果是<code>true</code>，除此之外，<code>null</code>、<code>undefined</code>和其他任何结果的比较值都为<code>false</code>。</p><pre><code class="language-javascript">null == undefined // true
null == '' // false
null == 0 // false
null == false // false
undefined == '' // false
undefined == 0 // false
undefined == false // false
</code></pre><p>规则 5：<code>原始类型</code>和<code>引用类型</code>做比较时，引用类型会依照<code>ToPrimitive</code>规则转换为原始类型。</p><blockquote>⭐️<code>ToPrimitive</code>规则，是引用类型向原始类型转变的规则，它遵循先<code>valueOf</code>后<code>toString</code>的模式期望得到一个原始类型。</blockquote><p>如果还是没法得到一个原始类型，就会抛出 <code>TypeError</code>。</p><pre><code class="language-javascript">'[object Object]' == {} 
// true, 对象和字符串比较，对象通过 toString 得到一个基本类型值
'1,2,3' == [1, 2, 3] 
// true, 同上  [1, 2, 3]通过 toString 得到一个基本类型值
</code></pre><h3 id="--3">通过几个特别的题目来练习一下吧！</h3><p><strong>1. </strong> <code>[] == ![]</code></p><pre><code>	- 第一步，![] 会变成 false
	- 第二步，应用 规则2 ，题目变成： [] == 0
	- 第三步，应用 规则5 ，[]的valueOf是0，题目变成： 0 == 0
	- 所以， 答案是 true ！//
</code></pre><h4 id="2-undefined-false"><strong>2.</strong> <code>[undefined] == false</code></h4><pre><code>	- 第一步，应用 规则5 ，[undefined]通过toString变成 '',
	  题目变成  '' == false
	- 第二步，应用 规则2 ，题目变成  '' == 0
	- 第三步，应用 规则3 ，题目变成  0 == 0
	- 所以， 答案是 true ！
	// 但是 if([undefined]) 又是个true！
</code></pre><h4 id="3-"><strong>3.</strong> 更多的题目</h4><p>更多的练习，大家去生活中去发现吧。（悲惨的生活）</p><blockquote>⭐️强烈建议大家去找各种奇奇怪怪的题目，反复练习上面 5 条规则，直到烂熟于心。</blockquote><h2 id="-1-">附录1：类型转换表</h2><p>这个表老实用了，在执行上面提到的转换规则时，可以参考这个对照表。</p><figure class="kg-card kg-image-card"><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/convert-table.png" class="kg-image" alt="convert-table" width="1080" height="1362" loading="lazy"></figure> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
