<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
            <title>我是框框</title>
            <link>https://kxq.io</link>
        <generator>Halo 1.6.0</generator>
        <lastBuildDate>Tue, 12 Aug 2025 17:01:34 CST</lastBuildDate>
                <item>
                    <title>
                        <![CDATA[普通老百姓如何维护自己的消费者权益？]]>
                    </title>
                    <link>https://kxq.io/archives/how-everyday-consumers-can-protect-their-rights-and-interests</link>
                    <description>
                            <![CDATA[<p>最近家里出了一件事情，有个电器刚出保修期，就出现故障，而且是能伤人的故障，商家提出维修方案，但并不能彻底解决问题，那个故障深埋于产品设计之内，就算现在解决了，我也难以相信它在未来不会再次出现。虽然经过了一番波折，但也很快就解决了问题。有点心得和体会，就此记录下来：</p><h1 id="%E7%BB%B4%E6%9D%83%E7%9A%84%E6%A0%B8%E5%BF%83%E6%98%AF%EF%BC%9A%E4%BA%8C%E4%B8%8D%E8%A6%81%E3%80%81%E4%B8%80%E8%A6%81" tabindex="-1">维权的核心是：二不要、一要</h1><ol><li>不要为难客服，他们的工作是安抚消费者情绪，他们可能会同情你的遭遇，但他们没有权利做出任何决策；</li><li>不要胡言乱语、撒泼耍赖、得理不饶人；</li><li>要相信党和政府，他们是站在群众的一边，而且他们有能力抗衡企业。</li></ol><h1 id="%E5%AE%8C%E6%95%B4%E4%BA%8B%E4%BB%B6" tabindex="-1">完整事件</h1><h2 id="%E8%B5%B7%E5%9B%A0" tabindex="-1">起因</h2><p>本人于2022年6月于京东自营平台购买了一款家用电器，并按照要求安装平稳使用三年，于 2025 年 8 月，刚出三年保修期，突然因固定装置发生故障差点导致家中老人和孩子受伤，所幸并无大碍。</p><p>第一次交锋：和店铺客服反映情况，店铺客服给到免费邮寄固定装置，并发放故障配件折扣券。<br />第二次交锋：和平台客服反映情况，后平台客服给到免费邮寄固定装置和故障配件，并由平台承担安装费用。</p><p>在这个过程中平台其实已经帮了大忙，我对他们心存感激，然而我忧心的并不是电器能否修好，而是他未来如果又发生故障，但如果导致家人受到伤害怎么办。所以我开始了自己的维权路程。</p><h2 id="%E7%BB%B4%E6%9D%83%E8%B7%AF%E7%A8%8B" tabindex="-1">维权路程</h2><ol><li>通过 <a href="https://www.gsxt.gov.cn/index.html" target="_blank">国家企业信用信息公示系统</a> 该京东自营网店属于企业直营，确定了投诉目标。</li><li>通过 <a href="https://www.12315.cn/" target="_blank">12315</a> 平台向该企业所属的市场监督局，投诉并举报：<ol><li>投诉：伸张自己所求 - 根据消费者权益保护法中 <code>第十八条“经营者应当保证其提供的 商品或者服务符合保障人身、财产安全的要求。</code> 要求企业退一赔三。</li><li>举报：举报企业违法行为 - 因该企业所生产电器不符合保障人身安全要求的基本原则，要求市场监督局责令该企业对该批次电器进行召回，并检查市面现有电器的安全性。</li></ol></li></ol><p>到此实际已经完成了所有维权，在和市场监督局工作人员的三次电话沟通中，他对我的遭遇表示同情，并表示一定会给到结果。</p><h1 id="%E7%BB%93%E5%B1%80" tabindex="-1">结局</h1><p>经过市场监督局的努力，企业客服来电答应退货退款，我要求三倍赔款其实并不抱有能够实现的希望，我只是希望我家人不再受到伤害，于是答应了。</p><p>同时市场监督局会继续监督该企业的产品，杜绝此类伤人事故的可能。</p><h1 id="%E6%8F%92%E6%9B%B21---%E8%B5%B7%E8%AF%89%E4%B9%A6" tabindex="-1">插曲1 - 起诉书</h1><p>你们也知道，12315 只能协商，所有我还是使用 AI 准备了一份起诉书，如果企业负隅顽抗调解不成，我会不客气直接在当地法院起诉。必须说 AI 现在完成此类稿件真的好用，整个起诉书由四个章节构成，是标准的起诉书模板，我仅引用一下第四章节<code>原告的诉讼请求依据</code>：</p><blockquote><p>四、原告的诉讼请求依据<br />1.退一赔三的合理性：根据《消费者权益保护法》第五十五条第一款“经营者提供商品或者服务有欺诈行为的，应当按照消费者的要求增加赔偿其受到的损失，增加赔偿的金额为消费者购买商品的价款或者接受服务的费用的三倍；增加赔偿的金额不足五百元的，为五百元”，虽本案被告可能不存在主观欺诈，但涉案电器因设计缺陷导致安全隐患，属于“不符合保障人身安全要求”的根本性质量问题，且被告在销售时未对潜在风险作出警示，原告基于对品牌及产品质量的合理信赖购买使用，现因电器故障面临持续的人身安全威胁，有权要求被告退还货款并支付惩罚性赔偿（三倍价款）。<br />2.召回产品的必要性：涉案电器的同批次产品可能存在相同设计或工艺缺陷（社交媒体反馈已佐证），若不及时召回并改进，将导致更多消费者面临类似风险。根据《消费者权益保护法》第十九条“经营者发现其提供的商品或者服务存在缺陷，有危及人身、财产安全危险的，应当立即向有关行政部门报告和告知消费者，并采取停止销售、警示、召回、无害化处理、销毁、停止生产或者服务等措施”，被告作为生产者和销售者，负有主动召回缺陷产品的法定义务。</p></blockquote><p>我觉得这才是使用 AI 的正路，让普通人也可以攥写出专业的文稿。</p><h2 id="%E6%80%BB%E7%BB%93---%E6%99%AE%E9%80%9A%E4%BA%BA%E7%BB%B4%E6%9D%83%E6%84%9F%E6%83%B3%E5%BF%83%E8%B7%AF" tabindex="-1">总结 - 普通人维权感想心路</h2><p>社会上，每个人都有自己的立场，从电商和平台的角度来说，如何将损失降到最低是他们的立场，从消费者角度上来说，买到一个商品，并且在合理的使用期限内安全地使用自己买到的产品是自己的立场。</p><p>这个电器如果只是普通的故障，不会伤害到人，我可能就自己修了，我在小红书上也确实是个装修翻车博主，各种奇怪的故障都遇见过，出事后我也迷茫了一阵不知道自己该要什么，之前的故障都不像这次这样让我害怕，这也是我一定要退货的原因，尽管它已经过了三年的保修期，但它还没达到它的合理使用期限。</p><p>立场的冲突是很常见的，你不能因为自己占理就要求放弃对方的立场，那样只会沾一脸土回家。我认为正确的方式是找一个权限更高的调停者，也就是国家的各种机构，通过向他们反映情况来完成自己的合理诉求。</p><p>有的人可能认为国家也未必和自己站在一边的，我觉得国家只和合理的诉求是站在一边的，如果你的诉求是合理合法的，尤其是可能影响到大多数人切身利益的，国家为什么不支持你呢。</p><p>社会始终是一个个普通人构成的，你的遭遇，说不定有一天就落在别人身上。</p>]]>
                    </description>
                    <pubDate>Tue, 12 Aug 2025 16:07:52 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Apple 以太网默认开启 AVB/EAV 模式导致网络连接丢失的故障]]>
                    </title>
                    <link>https://kxq.io/archives/ethernet-lost-connection-leds-by-new-avb-eav-mode</link>
                    <description>
                            <![CDATA[<blockquote><p>趁着国补入手了 Mac mini M4，因为家里内网接入了 2.5G 交换机，所以顺手升级购入了万兆以太网卡的版本。不过没想到问题才刚刚开始。</p></blockquote><p>入手了 Mac mini M4 非常开心，迫不及待地把开发环境配置上去，这次打算把 Git 仓库放在 SMB 共享文件夹里，这样多个设备都能对同一个代码仓库进行修改。不过几次 clone 仓库时发现有问题，会出 SIGBUS 错误，搜索了一下这个问题普遍就在把 Git 仓库放在 SMB 共享文件夹，然后就没有然后了。</p><p>再往下查，先是发现从 NAS 复制大文件时会突然中断，继而发现网卡有时会自动丢失连接，表现是 System Settings 里的 Ethernet 会从绿灯变成红灯。我心想不妙，不会碰到新网卡和交换机不兼容了吧，都要准备退货了。。。</p><p>当时开着 ping 路由器，隔一段时间会丢失连接。</p><pre><code class="language-">64 bytes from 192.168.88.1: icmp_seq=4225 ttl=64 time=0.966 ms64 bytes from 192.168.88.1: icmp_seq=4226 ttl=64 time=0.979 ms64 bytes from 192.168.88.1: icmp_seq=4227 ttl=64 time=0.955 msping: sendto: No route to hostRequest timeout for icmp_seq 4228ping: sendto: No route to hostRequest timeout for icmp_seq 4229ping: sendto: No route to hostRequest timeout for icmp_seq 4230ping: sendto: No route to hostRequest timeout for icmp_seq 4231ping: sendto: No route to hostRequest timeout for icmp_seq 4232Request timeout for icmp_seq 423364 bytes from 192.168.88.1: icmp_seq=4234 ttl=64 time=0.905 ms64 bytes from 192.168.88.1: icmp_seq=4235 ttl=64 time=0.821 ms</code></pre><p>但我也想苹果应该不至于连个万兆网卡的 2.5G 兼容都做不好吧，如果标准没问题，那只有私货有问题了，苹果一向喜欢搞私有协议的。</p><p>看了一下 System Settings 的 Ethernet 里有个 AVB/EAV Mode，默认情况下开启，搜索了一下它是什么：</p><blockquote><p>Audio Video Bridging (AVB) mode is a set of technical standards that improve the reliability, synchronization, and low latency of switched Ethernet networks. AVB defines the features, options, and configurations needed to build networks that can transport time-sensitive audio and video data streams.</p></blockquote><blockquote><p>EAV may refer to Ethernet Audio/Video Bridging, a standard that allows audio and video signals to be sent over an Ethernet line. The IEEE is developing the EAV standard, which includes features like bandwidth reservation and time-synchronized low latency streaming services. When deployed, EAV will allow audio and video hardware to be connected via Ethernet, enabling the simultaneous transmission of network, audio, and video signals.</p></blockquote><p>我想这可能就是私货了，因为如果使用 USB 网卡是没有这个选项的。将网卡 Hardware 切到 Manually 模式，取消掉它的复选框。运行了一阵，搞定～</p><p>Apple Community 里也找到了类似的问题 - <a href="https://discussions.apple.com/thread/255876126?sortBy=rank" target="_blank">https://discussions.apple.com/thread/255876126?sortBy=rank</a></p><p>说实话我希望苹果不要默认开启一些大多数人用不到的东西，而是默认关闭它，如果真的有人有需要，再自己手工开启。</p>]]>
                    </description>
                    <pubDate>Wed, 18 Dec 2024 16:20:00 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Brew 不支持某些版本的 macOS，如何使用它安装软件包]]>
                    </title>
                    <link>https://kxq.io/archives/install-package-via-brew-on-unsupported-macos</link>
                    <description>
                            <![CDATA[<p>我最近重新启用了我的 Macbook Air 2015，它最后官方支持的 macOS 版本是 Monterey 12.7.6。而我最近需要在上面安装 Docker、Qemu 和 Colima，在安装 Qemu 时碰到了一些困难。</p><p>安装这些命令行工具的常规做法是使用 Brew，但在今年9月份的一次更新中，Brew 放弃了对 Monterey 的支持，如果强行安装使用它，软件包不会直接安装 bottle 二进制而是从源代码开始安装，同时会报出下面几行字：</p><blockquote><p>Warning: You are using macOS 12.<br />We (and Apple) do not provide support for this old version.<br />It is expected behaviour that some formulae will fail to build in this old version.<br />It is expected behaviour that Homebrew will be buggy and slow.<br />Do not create any issues about this on Homebrew’s GitHub repositories.<br />Do not create any issues even if you think this message is unrelated.<br />Any opened issues will be immediately closed without response.<br />Do not ask for help from Homebrew or its maintainers on social media.<br />You may ask for help in Homebrew’s discussions but are unlikely to receive a response.<br />Try to figure out the problem yourself and submit a fix as a pull request.<br />We will review it but may or may not accept it.</p></blockquote><p>非常不幸的是，通过 Brew 走源代码安装的 Qemu 截稿最新的 9.1.1 在 Monterey 上可以通过编译，但通不过单元检测，于是一直无法顺利安装。期间我尝试使用 macPorts，但它安装 Qemu 通过 Colima 启动时有一个参数不对，导致串行日志没有内核启动内容，这意味着启动失败了；也想尝试另一个 Fink，但它居然连 Catalina 的支持都未完成。所以我开始了对 Brew 的研究，这里记录一下结果。</p><p>资料来源于 Github 上 “macOS 12 unsupported? #5603” 的这条回复 - <a href="https://github.com/orgs/Homebrew/discussions/5603#discussioncomment-11253847" target="_blank">https://github.com/orgs/Homebrew/discussions/5603#discussioncomment-11253847</a></p><p>核心点有：</p><ol><li>Brew 虽然停止了对 Monterey 的支持，但是针对老系统的 bottle 并未删除，只是不再在老系统上构建新版本的软件包。</li><li>这里依然使用 Bottle 安装，原因是我们只需要使用到软件包，而不想安装编译所需的依赖。</li><li>Bottle 都是老版本的，我们无法在老系统上使用新版本软件包的 Bottle。</li></ol><h1 id="%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95" tabindex="-1">操作方法</h1><p>这里以 Qemu 为例。</p><h2 id="%E6%89%BE%E5%88%B0%E5%AF%B9%E5%BA%94%E8%BD%AF%E4%BB%B6%E5%8C%85%E7%9A%84-formula-%E5%92%8C%E5%AF%B9%E5%BA%94%E6%8F%90%E4%BA%A4%E5%93%88%E5%B8%8C" tabindex="-1">找到对应软件包的 Formula 和对应提交哈希</h2><blockquote><p>Qemu - <a href="https://github.com/Homebrew/homebrew-core/blob/master/Formula/q/qemu.rb" target="_blank">https://github.com/Homebrew/homebrew-core/blob/master/Formula/q/qemu.rb</a></p></blockquote><p>点击右上角 History，翻找它的提交历史，找到 Monterey 的 bottle 依然被保留时的提交哈希。</p><blockquote><p>Qemu - <a href="https://github.com/Homebrew/homebrew-core/commit/a30892887bd5ee16db1ecf3094951734e5e4dfe4" target="_blank">https://github.com/Homebrew/homebrew-core/commit/a30892887bd5ee16db1ecf3094951734e5e4dfe4</a></p></blockquote><p>这时可见它的提交哈希是 a308928</p><h2 id="%E8%BF%9B%E5%85%A5-homebrew-%E5%88%87%E6%8D%A2%E5%88%B0%E8%AF%A5%E6%8F%90%E4%BA%A4%E5%93%88%E5%B8%8C" tabindex="-1">进入 Homebrew 切换到该提交哈希</h2><pre><code class="language-bash">cd $(brew --repository)/Library/Taps/homebrew/homebrew-core/git checkout -b qemu-freeze a308928</code></pre><blockquote><p>这里没有使用原文的 macOS-monterey-freeze 分支名称，是因为原文后面一段使用了相同的分支名，会出冲突。同时因为不同的软件包的哈希值可能是不一样的，其实根据需要安装的软件包起分支名称会更加合适一点。</p></blockquote><blockquote><p>不过 Qemu 对应的 a308928 提交于2024年9月10日，刚好在 Brew 放弃对 Monterey 支持之前，我自己测试它已经足够应对在 Monterey 上安装老版本软件包的需要了 – 比如切到 Colima 的提交哈希时，它还会从源代码构建部分依赖，但用 Qemu 那个分支就不会。我自己打算锁定这个提交的时间了。</p></blockquote><p>然后使用 brew install 安装需要的软件包，就会发现它不会再安装最新版本，而是安装老版本的 Bottle 了。</p><pre><code class="language-">brew install qemu # It will install 9.1.0 from bottle, not the 9.1.1 from source</code></pre><h2 id="%E9%94%81%E5%AE%9A-brew-%E8%AE%A9%E5%AE%83%E4%B8%8D%E5%86%8D%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0-formula" tabindex="-1">锁定 Brew 让它不再自动更新 Formula</h2><p>在设置环境变量的启动脚本中，增加一个：</p><pre><code class="language-bash">    export HOMEBREW_NO_AUTO_UPDATE=1</code></pre><p>例如我使用的 zsh 就修改 ~/.zprofile 增加上面这一行。<br />Homebrew 的安装脚本就永远不会更新了。</p><h1 id="%E4%B8%80%E7%82%B9%E5%BF%83%E5%BE%97" tabindex="-1">一点心得</h1><p>Brew 作为 macOS 上最受欢迎的软件包安装工具，还是有它的设计想法在的，虽然它不直接支持安装旧版本的软件包，但可以通过切换 Formula 所在的 Git 仓库的 commit 安装旧版本的软件包。</p><p>我不太能说这是个好主意，但也给了个可以回滚的办法。</p>]]>
                    </description>
                    <pubDate>Wed, 20 Nov 2024 09:39:18 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[我的命令行现代化历程 - 持续更新中]]>
                    </title>
                    <link>https://kxq.io/archives/my-journey-to-modernize-cli</link>
                    <description>
                            <![CDATA[<p>基于 POSIX 的 Shell，包含 bash、zsh 的历史都太悠久了，有点不向前兼容的 fish 也不敢做颠覆性的革新。一提到命令行的编辑器必谈 vi/vim/emacs，但它们的整个快捷键、操作方式也和现代 GUI 模式下的编辑器差异太大。都什么年代了，干嘛还要死守着这些老古董不放呢。</p><p>前一阵看了一下 powershell 给我了我一些启发，原来管道之间还可以传对象，让后续的数据处理更加简单方便。</p><p>所以我打算将我的命令行系统现代化一下，抛弃一些历史悠久的东西，诚然这会有额外的学习成本，不过持续学习是程序员的基本素质了。</p><h1 id="shell---nushell">Shell - <a href="https://www.nushell.sh/">nushell</a></h1><p>Powershell 虽好，但是它启动得太慢了，无意间我发现了 <a href="https://www.nushell.sh/">nushell</a>，这熟悉的面向对象的管道、结构化的数据存储和传输、二十毫秒左右的启动速度，简直不要太棒了。</p><p>具体安装流程什么的看官网吧，我只贴一下我首次安装后的配置流程。安装后执行 <code>nu</code> 命令就可以进入 nushell。</p><p>我只使用 nushell 自己的配置能力，所以看着会有点绕。但好处是不受配置文件路径配置不同的影响，可以照抄。</p><h2 id="1-配置默认编辑器">1. 配置默认编辑器</h2><p>nushell 自己的配置能力需要 <code>$env.config.buffer_editor</code> 获取编辑器，它的默认值是 <code>null</code> 意味着当你直接执行 <code>config env</code> 的时候它会报错：</p><pre><code class="language-plaintext">Error:   × No editor configured   ╭─[entry #18:1:1] 1 │ config env   · ─────┬────   ·      ╰── Please specify one via `$env.config.buffer_editor` or `$env.EDITOR`/`$env.VISUAL`   ╰────  help: Nushell's config file can be found with the command: $nu.config-path. For more help: (https://nushell.sh/book/configuration.html#configurations-with-built-        in-commands)</code></pre><p>所以这里需要手工改一下，$env 的具体说明请参考官方文档。</p><pre><code class="language-ini">$env.config.buffer_editor = vim  # 这里暂时修改为 vim，也可以选择自己称手的编辑器</code></pre><h3 id="将默认编辑器配置持久化">将默认编辑器配置持久化</h3><p>执行 <code>config nu</code> 打开 nushell 的配置文件，并找到 <code>buffer_editor</code> 这行，将原来的 null 改为自己的编辑器</p><pre><code class="language-ini">    buffer_editor: vim # command that will be used to edit the current line buffer with ctrl+o, if unset fallback to $env.EDITOR and $env.VISUAL</code></pre><h2 id="2-还原环境变量">2. 还原环境变量</h2><p>需要特别说明的是 nushell 的环境变量和 POSIX Shell 差异巨大，它是结构化的存储，如果使用 <code>echo $PATH</code> 会报错告诉你根本不支持：</p><pre><code class="language-plaintext">Error: nu::parser::env_var_not_var  × Use $env.PATH instead of $PATH.   ╭─[entry #6:1:6] 1 │ echo $PATH   ·      ──┬──   ·        ╰── use $env.PATH instead of $PATH   ╰────</code></pre><p>要访问环境变量直接使用 <code>$env.[ENVIRONMENT_VARIABLE]</code> 即可。上一章节中的 <code>config</code> 也是以环境变量存储，但是是一个嵌套的对象。 -- 这才是我想要的命令行环境。</p><h3 id="path">$PATH</h3><p>nushell 的 <code>$PATH</code> 默认只有 <code>/usr/bin</code> 和 <code>/bin</code> 两个，对于装了 brew 的我来说是不可接受的。</p><p>首先要在原来的 Shell 中通过 <code>echo $PATH</code> 获取原来的环境变量，我这里是：</p><pre><code class="language-shell">/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/sbin:/usr/local/sbin:/usr/sbin:/sbin</code></pre><p>而在 nushell 中 <code>$env.PATH</code> 是这样的：</p><pre><code class="language-table">╭───┬────────────────────╮│ 0 │ /usr/bin           ││ 1 │ /bin               │╰───┴────────────────────╯</code></pre><p>其实我没有找到 nushell 的 <code>PATH</code> 在哪里初始化，但是按照<a href="https://www.nushell.sh/book/configuration.html#path-configuration">官方文档</a> 的说明，目前 nushell 中的路径在整个路径列表中是在中间的，这也就意味着需要在前面添几个，后面添几个才可以。</p><p>执行 <code>config env</code>，进入环境变量编辑界面，找到  $env.PATH 段，改成下面这样，prepend 表示在数组前面添加路径，append 表示在后面添加路径：</p><pre><code class="language-nushell"># To add entries to PATH (on Windows you might use Path), you can use the following pattern:# $env.PATH = ($env.PATH | split row (char esep) | prepend '/some/path')# An alternate way to add entries to $env.PATH is to use the custom command `path add`# which is built into the nushell stdlib:# use std &quot;path add&quot;# $env.PATH = ($env.PATH | split row (char esep))# path add /some/path# path add ($env.CARGO_HOME | path join &quot;bin&quot;)# path add ($env.HOME | path join &quot;.local&quot; &quot;bin&quot;)# $env.PATH = ($env.PATH | uniq)    $env.PATH = (   $env.PATH |  prepend /usr/local/bin |  prepend /opt/homebrew/bin |  append /opt/homebrew/sbin |  append /usr/local/sbin |  append /usr/sbin |  append /sbin) </code></pre><p>保存后，重启 nushell，再执行 $env.PATH 就会输出成这样，即为正常 ，通过 brew 安装的所有命令行工具就回来了：</p><pre><code class="language-table">╭───┬────────────────────╮│ 0 │ /opt/homebrew/bin  ││ 1 │ /usr/local/bin     ││ 2 │ /usr/bin           ││ 3 │ /bin               ││ 4 │ /opt/homebrew/sbin ││ 5 │ /usr/local/sbin    ││ 6 │ /usr/sbin          ││ 7 │ /sbin              │╰───┴────────────────────╯</code></pre><h2 id="其他资料">其他资料</h2><ol><li>推荐从别的 Bash 迁移过来的参考一下，方便理解它的概念和差异 -<a href="https://www.nushell.sh/book/coming_from_bash.html#coming-from-bash">Coming from Bash</a></li><li>nushell 的第三方扩展 - <a href="https://github.com/nushell/nu_scripts">nu_scripts</a>，有一个 <a href="https://github.com/nushell/nupm">nupm</a> 项目貌似可以用来下载安装，我还在测试中。</li></ol><h1 id="编辑器---micro">编辑器 - <a href="https://micro-editor.github.io/">Micro</a></h1><p>其实绝大多数情况下我们在命令行下只需要一个简单的文本编辑工具，传统的编辑器学习成本太高了，vim、emacs 只留如果不看文档进入后都不知道该如何退出来，即使是足够简化的 nano 也需要重新学习。所以我换成 micro 了，快捷键和 GUI 下的编辑器基本相同：</p><ol><li>关闭 Ctrl + Q</li><li>保存 Ctrl + S</li><li>查找 Ctrl + F</li><li>撤销 Ctrl + Z</li></ol><p>更多请参考官方的 <a href="https://github.com/zyedidia/micro/blob/master/runtime/help/keybindings.md">keybindings.md</a>。另外，它还有<a href="https://micro-editor.github.io/plugins.html">一些不错的插件</a>。</p><h1 id="编辑器---helix">编辑器 - <a href="https://helix-editor.com/">Helix</a></h1><p>vim 的最大问题是在于它本身过于简单，但如果要让它能用起来还得加上一堆插件。Helix 就比较简单了，基本开箱即用。</p><p><em>TODO: 这个我还得多用用再来写。</em></p>]]>
                    </description>
                    <pubDate>Sat, 03 Aug 2024 12:40:29 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[使用 Ollama 和 VS Code 的 Continue 插件在本地实现 AI 辅助代码编写]]>
                    </title>
                    <link>https://kxq.io/archives/localai-assistedcodewritingwithollama</link>
                    <description>
                            <![CDATA[<blockquote><p>Github Copilot 确实好用，不过作为程序员能自己动手，就尽量不使用商业软件。</p></blockquote><p><a href="Ollama">Ollama</a> 作为一个在本地运行各类 AI 模型的简单工具，将门槛拉到了一个人人都能在电脑上运行 AI 模型的程度，不过运行它最好有 Nvidia 的显卡或者苹果 M 系列处理器的笔记本。</p><p>我自己是在 Macbook Pro M1 Pro 32G 统一内存的笔记本上运行的，目前运行 <a href="https://ollama.com/library/codellama:7b-code-q4_K_M">codellama:7b-code-q4_K_M</a> 小模型效果都不错，但是运行模型体积稍大一点就容易出现反应迟钝（例如 <a href="dolphin-mixtral:8x7b">dolphin-mixtral:8x7b</a>），或者输出出错的情况（例如 <a href="https://ollama.com/library/codellama:34b-code-q4_K_M">codellama:34b-code-q4_K_M</a>)。我的意思是，本地运行 AI 还是要量力而行，即使对自己的硬件有信心，也建议从小慢慢往大了测试。</p><h1 id="本地运行-ollama">本地运行 Ollama</h1><h2 id="下载安装-ollama">下载安装 Ollama</h2><p>直接从 <a href="https://ollama.com/download">https://ollama.com/download</a> 官网下载应用即可，目前平台覆盖比较齐全。</p><p>下载后安装，命令行中会增加 ollama 命令，启动后会在系统托盘中出现羊驼图标。</p><h2 id="下载模型">下载模型</h2><p>我这里依然以 <a href="https://ollama.com/library/codellama:7b-code-q4_K_M">codellama:7b-code-q4_K_M</a> 它是针对编码训练的 Lama 模型，对大部分代码有比较不错的兼容性。</p><p>直接在命令行中运行：</p><pre><code class="language-bash">ollama pull codellama:7b-code-q4_K_M</code></pre><p>然后就会开始下载，在 4G 多。下载完成后可以先启动试试：</p><pre><code class="language-bash">ollama run codellama:7b-code-q4_K_M</code></pre><p>它会跟你一个 <code>&gt;&gt;&gt;</code> 的命令提示符，然后就可以和它沟通了，一定要描述清晰你的需求，否则就会输出一堆没什么用的东西。:-(</p><h1 id="使用-continue-与-ide-集成">使用 Continue 与 IDE 集成</h1><p>VSCode 的 Llama 插件目前我觉得 <a href="https://marketplace.visualstudio.com/items?itemName=Continue.continue">Continue</a> 还算不错，它也提供了 Jetbran 的插件。</p><p>截稿时它的自动补齐功能还在 pre-prelease 版本中，所以我这里以它正式版本为主。</p><h2 id="安装-continue-插件">安装 Continue 插件</h2><p>直接从 Extension Marketplace 安装即可 <a href="https://marketplace.visualstudio.com/items?itemName=Continue.continue">https://marketplace.visualstudio.com/items?itemName=Continue.continue</a></p><p>安装完成后在 VSCode 左侧侧栏中会增加一个 <code>&gt; CD_</code> 的图标，这就是它的主界面了。</p><h2 id="配置本地-ai-模型">配置本地 AI 模型</h2><p>Continue 默认只提供了几种线上 AI 模型的试用，如果需要更改，需要在 Continue 主界面中点击右下角的齿轮图标，进入配置界面，会打开 <code>$HOME/.continue/config.json</code>，需要在该文件的字段中增加以下字段：</p><pre><code class="language-json">{  &quot;models: {    {      &quot;title&quot;: &quot;Local Ollama&quot;,      &quot;provider&quot;: &quot;ollama&quot;,      &quot;model&quot;: &quot;codellama:7b-code-q4_K_M&quot;    },    // ...  }  // ...}</code></pre><p>这样就启用了本地 Ollama，如果未来需要更换别的模型，直接修改 model 字段即可。</p><h1 id="使用-continue-进行-ai-辅助代码编写">使用 Continue 进行 AI 辅助代码编写。</h1><p>Continue 主要有两种使用方式：</p><h2 id="1---l-选中代码">1. ⌘ + L 选中代码</h2><p>选中需要的代码，按下 ⌘+L 快捷键后，Continue 界面就会弹出来，并将选中的代码插入到聊天框中，这时候你可以让它帮你编写单元测试，或者检查可能存在的 bug。</p><h2 id="2---i-插入代码">2. ⌘ + I 插入代码</h2><p>在代码的任意位置按下 ⌘ + I，会弹出一个小的 prompt 输入框，你可以输入需求让它快速生成。</p><h1 id="总结">总结</h1><p>相对线上的大模型，本地运行的小模型在精准度上还是有些欠缺，不过已经可以代替部分工作，最主要是它依托开源的优势，降低了成本。</p><h2 id="copilot-的免费替代">Copilot 的免费替代</h2><p>如果觉得本地 Codellama 模型不够使用，其实还有个 Copilot 的免费替代 - <a href="https://codeium.com/">https://codeium.com/</a> 这个功能我自己试用下来也很不错，目前跟本地 Ollama 交替使用。</p>]]>
                    </description>
                    <pubDate>Thu, 07 Mar 2024 17:50:09 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[在远程 Linux 服务器上使用微软 RDP 远程桌面服务]]>
                    </title>
                    <link>https://kxq.io/archives/connect-remote-linux-desktop-with-rdp</link>
                    <description>
                            <![CDATA[<blockquote><p>现在很流行将本地开发环境放到远端，我也这么做很久了，因为服务器的强大硬件效率比本地开发要高得多，但调试工作放在本地电脑上，而且每次开启 IDE 都有个重新加载的过程，所以在想能不能把整个开发调试环境全都放在服务器上呢，这样不管去哪里，打开远程桌面就可以直接进行工作。</p></blockquote><blockquote><p>这里使用远程桌面服务的目标是进行远程开发或者测试，不推荐在提供对外服务的服务器上安装，可能对性能和安全性造成影响。</p></blockquote><p>远程服务器如果有了图形界面，就可以安装顺手的 VSCode 编辑器进行远程开发，本地电脑就作为一个瘦客户端，对配置要求可以非常低。我之前曾经考虑过换成 Windows 的远程开发机，但是 NTFS 羸弱的小文件处理性能让我打消了这个想法，做开发而已，要求不高。</p><p>先来看一下总体效果，macOS 使用微软的远程桌面连接到了 Linux 服务器上。</p><p><img src="https://blog-1251620540.cos-website.ap-guangzhou.myqcloud.com/1640014093-6647-61c0a10da24f4-126959_1640314816999.png" alt="Screenshot" /></p><p>那么下面就开始吧。</p><h1 id="背景介绍">背景介绍：</h1><p>远程桌面协议有很多种，例如常见的 VNC、TeamView 之类的，XWindow 本身也支持将本地的显示通过 DISPLAY 环境变量重定向到另外台电脑上显示。</p><p>不过我这里用的是微软的 RDP 协议，它的虚拟虚拟显示器分辨率可以由客户端指定，体验相对较好。</p><p>而它在 Linux 上通过 <a href="http://xrdp.org">xrdp</a> 服务将 XWindow 协议转换为 RDP 协议供客户端调用。</p><h1 id="准备工作">准备工作</h1><p>在开始之前建议先使用 <code>adduser</code> 命令建立一个跟自己 git 同名的普通账号，这样在操作代码资源时更加方便，普通账号权限受限也相对更加安全，如需进行越权操作，可以给他添加 <code>sudo</code> 操作权限。</p><p>本文假设你已经具备基本的 Linux 知识水平，所以所有内容如果碰到问题请先自行查阅资料。</p><h1 id="安装">安装</h1><p>以 CentOS 8 版本为例，按照下面命令一次输入即可：</p><pre><code class="language-bash">sudo dnf install -y xrdp # 安装 xrdp 服务sudo systemctl enable xrdp # 启用 xrdp 服务sudo systemctl start xrdp # 启动 xrdp 服务</code></pre><h1 id="安装-xfce-桌面环境">安装 XFce 桌面环境</h1><p>桌面环境有很多种，红帽系默认是 <a href="https://www.gnome.org">GNOME</a>，但它的半透明和阴影较多不适合在远程桌面上使用，所以我用了 <a href="https://www.xfce.org">XFce</a> 一个轻量化的桌面环境</p><pre><code class="language-bash">sudo dnf install -y @xfce-desktop # 安装 xfce4-desktop 分组echo &quot;xfce4-session&quot; &gt; ~/.Xclients # 将 xfce4 作为默认桌面环境chmod a+x ~/.Xclients</code></pre><p>其他分组可以使用下面命令查看：</p><pre><code class="language-bash">sudo dnf grouplist -v</code></pre><p>如果有别的桌面环境喜好，也可以手工安装，修改 <code>~/.Xclients</code> 即可启动。</p><h1 id="连接远程桌面环境">连接远程桌面环境</h1><h2 id="安装远程桌面客户端-app">安装远程桌面客户端 App</h2><ul><li>Windows: <a href="https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-allow-access">https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-allow-access</a></li><li>macOS: <a href="https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-mac">https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-mac</a> （其中提到的 <a href="https://apps.apple.com/us/app/microsoft-remote-desktop/id1295203466?mt=12">下载链接</a> 需要外服 AppStore 账号）</li></ul><h2 id="连接">连接</h2><p>启动客户端，点击 + 号新增 PC：</p><p><img src="https://blog-1251620540.cos-website.ap-guangzhou.myqcloud.com/connection_1640314917097.jpg" alt="Connect" /></p><p>填入用户名和密码，点击 Continue 正常情况即可连接：</p><p><img src="https://blog-1251620540.cos-website.ap-guangzhou.myqcloud.com/password_1640314936441.png" alt="Password" /></p><h1 id="声音转发">声音转发</h1><p>一般情况下，没有声音的远程桌面已经足够使用了，不过如果有更高要求，可以考虑把远程开发机上的声音也同步过来，这就要提到 xrdp 相对别的远程桌面协议强大的地方了，它是能通过<a href="https://github.com/neutrinolabs/pulseaudio-module-xrdp">一个插件</a>支持声音重定向的。</p><h2 id="安装依赖">安装依赖</h2><p>正常情况下开发着工具应该是都已经安装好了，可以再次执行一下确保环境正常</p><pre><code class="language-bash">sudo dnf groupinstall &quot;Development Tools&quot; -ysudo dnf install rpmdevtools yum-utils -ysudo rpmdev-setuptree</code></pre><p>安装开发所使用到的库和头文件，这里其实用了 <a href="https://www.freedesktop.org/wiki/Software/PulseAudio/">PulseAudio</a> 它也是目前 Linux 上的混音器的事实标准，我们这里需要通过它的一个插件将声音通过远程桌面重定向过来。</p><pre><code class="language-bash">sudo yum install pulseaudio pulseaudio-libs pulseaudio-libs-devel -ysudo yum-builddep pulseaudio -y </code></pre><h2 id="可选建立独立的编译账号">【可选】建立独立的编译账号</h2><p>主账号安装 brew 后可能一些共享目标（so）和头文件等开发运行时可能会乱，所以这里可以考虑建一个纯粹的 Linux 编译账号，只使用原始的环境进行编译</p><blockquote><p>推荐建立这么一个账号，这样以后有别的编译任务的时候也可以使用它。</p></blockquote><pre><code class="language-bash">sudo useradd mockbuildsudo usermod -G mockbuild mockbuild</code></pre><p>然后切换过去</p><pre><code class="language-bash">sudo su - mockbuild</code></pre><h2 id="准备编译所依赖的-pulseaudio">准备编译所依赖的 PulseAudio</h2><pre><code class="language-bash">yumdownloader --source pulseaudiorpm --install pulseaudio*.src.rpm</code></pre><p>然后就会看见你的用户目录下多了 <code>~/rpmbuild/BUILD/</code> 目录，刚刚装的 rpm 源代码就在里面，可以直接用下面命令进行编译。</p><pre><code class="language-bash">rpmbuild -bb --noclean ~/rpmbuild/SPECS/pulseaudio.spec</code></pre><h2 id="主角编译和安装-pulseaudio-的-xrdp-插件">【主角】编译和安装 PulseAudio 的 XRDP 插件</h2><pre><code class="language-bash">git clone https://github.com/neutrinolabs/pulseaudio-module-xrdp.gitcd pulseaudio-module-xrdp./bootstrap &amp;&amp; ./configure PULSE_DIR=~/rpmbuild/BUILD/pulseaudio-*makesudo make install</code></pre><h2 id="验证安装">验证安装</h2><p>使用 <code>pulseaudio -D</code> 启动音频服务后，如果之前已经启动过，那可以用 <code>pulseaudio -k</code> 重启服务，应该就能从音频控制面板中调节音量了，如下图：</p><p><img src="https://blog-1251620540.cos-website.ap-guangzhou.myqcloud.com/Screen%20Shot%202022-02-18%20at%205.37.18%20PM_1645183473701.png" alt="Screen Shot 20220218 at 5.37.18 PM.png" /></p><h1 id="trouble-shooting">Trouble Shooting</h1><h2 id="服务无法连接">服务无法连接</h2><p>首先使用下面命令检查 xrdp 服务是否正常运行中<br /><code>sudo systemctl status xrdp</code></p><p><img src="https://blog-1251620540.cos-website.ap-guangzhou.myqcloud.com/terminal_1640314952428.png" alt="Service Status" /></p><p>其次检查防火墙（一般是关掉的，但不保证部分人可能开着）</p><pre><code>firewall-cmd --permanent --add-port=3389/tcpfirewall-cmd --reload</code></pre><h2 id="连接后闪退">连接后闪退</h2><p>一般是桌面环境没有安装成功，可以先用下面命令看一下服务日志确定一下：</p><pre><code class="language-bash">sudo cat /var/log/xrdp-sesman.log</code></pre><h2 id="报告用户名或者密码错">报告用户名或者密码错</h2><p>xrdp 使用的是 DevCloud 上的本地用户名和密码，需要使用能直接登录的用户名和密码登录。</p>]]>
                    </description>
                    <pubDate>Tue, 21 Dec 2021 23:29:53 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[程序员应该具备的数学抽象能力]]>
                    </title>
                    <link>https://kxq.io/archives/developer-math-abstraction</link>
                    <description>
                            <![CDATA[<h1 id="案例一-css-的值展开">案例一: CSS 的值展开</h1><p>以之前碰到过的 CSS 值展开代码为例，它的输入是个不定长，以空格作为分割的字符串，输出的是一个长度为 4 的字符串数组：如字符串只有一个元素（没有空格），输出数组4个元素都是那个字符串；有两个元素（一个空格），则输出数组则是两个元素的顺序重复；有三个元素（两个空格），则输出数字前三是三个元素，第四则是第二个元素；有四个元素（三个空格），则按顺序平铺。</p><blockquote><p>这通常在 CSS 的 padding、margin 上使用，可以想一下是不是实现相同？</p></blockquote><p>原代码是：</p><pre><code class="language-javascript">function expandValueToFourValues(value) {    var trimValue = value.replace(/\s+/, &quot; &quot;);    var attribValues = trimValue.split(&quot; &quot;);    var valueExpanded = [];    if(attribValues.length == 4) {        valueExpanded = attribValues;    } else if (attribValues.length == 3) {        valueExpanded[0] = attribValues[0];        valueExpanded[1] = attribValues[1];        valueExpanded[2] = attribValues[2];        valueExpanded[3] = attribValues[1];    } else if (attribValues.length == 2) {        valueExpanded[0] = attribValues[0];        valueExpanded[1] = attribValues[1];        valueExpanded[2] = attribValues[0];        valueExpanded[3] = attribValues[1];    } else if (attribValues.length == 1) {        valueExpanded[0] = attribValues[0];        valueExpanded[1] = attribValues[0];        valueExpanded[2] = attribValues[0];        valueExpanded[3] = attribValues[0];    }    return valueExpanded;}</code></pre><p>如果要简化，应该怎么去做？这里直接给出答案，包含函数声明和注释只有 6 行：</p><pre><code class="language-javascript">function expandValueToFourValues(value) {  // split 方法可以接受一个正则表达式或者函数作为拆分条件，没必要先替换字符串，可以直接切，/\s+/ 表示适配任意白字符  const attribValues = value.split(/\s+/);    // 把输入转换为矩阵，就可以看出单行宽度和索引值是有规律的，直接通过 Array.from() 生成长度为4的数组即  return Array.from({length: 4}, (n, i) =&gt; attribValues[i] || attribValues[i-2] || attribValues[0]);}</code></pre><p>这是怎么做到的？注释上写着，把字符串拆分后当成一个矩阵来找规律，就是：</p><table><thead><tr><th align="center">输入字符串</th><th align="center">输出数组第0元素</th><th align="center">输出数组第1元素</th><th align="center">输出数组第2元素</th><th align="center">输出数组第3元素</th></tr></thead><tbody><tr><td align="center"><em>a</em></td><td align="center">a</td><td align="center">a</td><td align="center">a</td><td align="center">a</td></tr><tr><td align="center"><em>a b</em></td><td align="center">a</td><td align="center">b</td><td align="center">a</td><td align="center">b</td></tr><tr><td align="center"><em>a b c</em></td><td align="center">a</td><td align="center">b</td><td align="center">c</td><td align="center">b</td></tr><tr><td align="center"><em>a b c d</em></td><td align="center">a</td><td align="center">b</td><td align="center">c</td><td align="center">d</td></tr></tbody></table><p>这时再来看那个表达式 <code>(n, i) =&gt; attribValues[i] || attribValues[i-2] || attribValues[0]</code>，将几个值代入：</p><p>当只有一个元素时，假设 i 只有 0 有值：</p><ul><li>[0] attributes[i] 有值，正常加入</li><li>[1..3] attributes[i] 和 attributes[i-2] 都没有值，走到最后 attributes[0]</li></ul><p>当有两个元素时，i 只有 0 和 1 有值，按照下面就做到了两个元素重复添加：</p><ul><li>[0,1] attributes[i] 有值，将前两个元素添加进入返回数组</li><li>[2] attributes[i] 没有值，但是 attributes[i-2 = 0] 返回第一个元素</li><li>[3] attributes[i] 没有值，但是 attributes[3-2 = 1] 返回第二个元素</li></ul><p>当有三个元素时，i 只有 0、1、2 有值：</p><ul><li>[0,1,2] attributes[i] 有值，将前两个元素添加进入返回数组</li><li>[3] attributes[i] 没有值，则取 attributes[3-2] 刚好取第 index 1 的元素，这就需求惊人的巧合了。</li></ul><p>当有四个元素时：</p><ul><li>[0..3] 都有值，依次加入即可</li></ul><p>其实这是 CSS 里颜色展开的算法，我怀疑浏览器里可能也是这么简洁实现的，相对原来的代码，这种写法明显简单干净，少了很多判断。</p><h1 id="案例二canvas-的-textdecoration-实现">案例二：Canvas 的 textDecoration 实现</h1><p>Canvas 的 textDecoration 实现并不复杂，只需要处理三条线的起终点即可，但是它有一个问题是它跟 textBaseline 相交，因为 Canvas 的文本画线是从 textBaseline 开始画起的，所以它的装饰线也要考虑到起点：</p><blockquote><p>出于简化说明，这里不考虑 textBaseline 为  <code>alphabetic</code> 的情况，它是从英文的四线格第三线开始画，公式会不太一样。</p></blockquote><p><img src="https://blog-1251620540.cos-website.ap-guangzhou.myqcloud.com/text-decorations_1638984294610.png" alt="textdecorations.png" /></p><p>初看这个图，是不是又要写一堆 if else 构成？不，我们把它展开成一个二维矩阵：</p><table><thead><tr><th align="center">textBaseline\textDecoration</th><th align="center">overline</th><th align="center">lineThrough</th><th align="center">underline</th></tr></thead><tbody><tr><td align="center"><strong>top</strong></td><td align="center">0</td><td align="center">fontSize/2</td><td align="center">fontSize</td></tr><tr><td align="center"><strong>middle</strong></td><td align="center">-fontSize/2</td><td align="center">0</td><td align="center">fontSize/2</td></tr><tr><td align="center"><strong>bottom</strong></td><td align="center">-fontSize</td><td align="center">-fontSize/2</td><td align="center">0</td></tr></tbody></table><p>最简单的办法是把 key 合并，成为一维哈希直接取值：</p><pre><code class="language-javascript">const decorationBaseLineMap = {  'overline top': 0,  'lineThrough top': fontSize / 2,  'underline top': fontSize,  'overline middle': -fontSize / 2,  // ...}// 获取 Y 轴起点const y = decorationBaseLineMap[`${textDecoration} ${textBaseline}`];</code></pre><p>但这样还是很啰嗦，而且我从来不觉得拼接字符串是个很快的事情。我们尝试从这个二维数组中发现几何规律，其实它是个很完美的以 middle 和 lineThrough 为中心的镜像：</p><ul><li>fontSize = unerline/top</li><li>fontSize / 2 = lineThrough/top = underline/middle</li><li>0 = overline/top = lineThrough/middle = underline/bottom</li><li>-fontSIze / 2 = overline/middle = lineThrough/bottom</li><li>-fontSize = overline/bottom =</li></ul><p>此时第一反应是给二维数组代数进去，使之前后相等（代什么数字进去，我是凭感觉[狗头]），然后 X 轴和 Y 轴相加结果如下：</p><table><thead><tr><th align="center">textBaseline\textDecoration</th><th align="center"><em>overline</em>  [0]</th><th align="center"><em>lineThrough</em>  [-1]</th><th align="center"><em>underline</em>  [-2]</th></tr></thead><tbody><tr><td align="center"><strong>top</strong>  [0]</td><td align="center">0</td><td align="center">fontSize/2 [-1]</td><td align="center">fontSize [-2]</td></tr><tr><td align="center"><strong>middle</strong> [1]</td><td align="center">-fontSize/2 [1]</td><td align="center">0</td><td align="center">fontSize/2 [-1]</td></tr><tr><td align="center"><strong>bottom</strong> [2]</td><td align="center">-fontSize [2]</td><td align="center">-fontSize/2 [1]</td><td align="center">0</td></tr></tbody></table><p>此时已经呼之欲出了，有一个隐藏的系数存在于这个公式里，通过它可以做到镜像取反，假设 <code>fontSize = 1</code>，代入到多元一次方程很容易算出来：<code>x = -.5</code>（0.5 去掉整数部分），代入到其它几个公式里，继续验证一下正确性：</p><ul><li>underline/top: <code>x(underline+top)fontSize = -.5*(-1+0)*1 = .5(正数)</code></li><li>overline/middle: <code>x(overline+middle)fontSize = -.5*(0+1)*1 = -.5(负数)</code>.</li><li>lineThrough/middle: <code>x(lineThrough+middle)fontSize = -.5*(-1+1)*1 = 0</code></li><li>underline/middle: <code>x(underline+middle)fontSize = -.5*(-2+1)*1 = .5(正数)</code></li><li>overline/bottom: <code>x(overline+bottom)fontSize = -.5*(0+2)*1 = -1(负数)</code></li><li>lineThrough/bottom: <code>x(lineThrough+bottom)fontSize = 0.5*(-1+2)*1=-.5(负数)</code></li><li>underline/bottom: <code>x(underline+bottom)fontSize = -.5*(-2+2)*1 = 0</code></li></ul><p>写成实际的代码：</p><pre><code class="language-typescript">const decorationDim = ({  overline: 0,  lineThrough: -1,  underline: -2,})[textDecoration];const baselineDim = ({  top: 0,  middle: 1,  bottom: 2,})[textBaseline];// 获取 Y 轴值, -.5 是 xAxis 和 yAxis 相加后与字体大小的系数const y = fontSize * (decorationDim + baselineDim) * -.5;</code></pre><p>如果写了一堆 if else，会发现这个结果也是相符的，我写了个可公开的范例：<a href="https://codepen.io/xuqingkuang/pen/dyVXPgO">Canvas textDecoration (codepen.io)</a>。</p><h1 id="总结">总结</h1><p>希望大家能在开发过程中多进行思考，不要按照需求翻译成代码，而是具备更高的抽象能力，通过一些数学方法，提高代码的执行效率。</p>]]>
                    </description>
                    <pubDate>Thu, 09 Dec 2021 01:26:45 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[编写可 Review 的代码]]>
                    </title>
                    <link>https://kxq.io/archives/编写可review的代码</link>
                    <description>
                            <![CDATA[<h1 id="总览">总览</h1><p>本文档介绍了一种结构化的代码管理策略，该策略在各类开源项目中都有实践（例如 <a href="https://github.com/torvalds/linux/commits/master">Linux</a>、<a href="https://github.com/facebook/react/commits/master">React</a>、<a href="https://github.com/vuejs/vue/commits/dev">Vue</a>)，简而言之的话有以下几个原则：</p><ol><li>每个提交（commit）应尽可能小；</li><li>每个提交的提交都要是一个内聚的实现；</li><li>通过将大问题分解为较小的问题并一次解决一个小问题，最终将大提交变成小提交；</li><li>编写清晰的提交消息（commit message）；</li><li>代码中在关键位置要保留足够的上下文；</li><li>使用无副作用且简洁的实现；</li><li>编写足够的测试用例。</li></ol><h1 id="提交还是小的好">提交还是小的好</h1><p>小型、简单的提交通常比大型、复杂的提交要好。它们更容易理解，更易于测试和更易于查看。理解、测试和审查变更的复杂性通常比变更的大小要快：每次做一件事情的十个200行的变更通常比做十件事的一个2000的行变更更容易理解。</p><p>将一个可以做很多事情的更改拆分为每个只能做一个事情的较小更改，可以减少实现同一目标的总复杂度。</p><p>而且，即使出现问题，如果只有一个提交，也会很容易进行回滚（Revert）。</p><h1 id="每个提交的提交都要是一个内聚的实现">每个提交的提交都要是一个内聚的实现</h1><p>每次提交都应该只实现一个功能或者修复一个问题。通常，这意味着在开发时应将不同的更改分为不同的提交。例如，如果您正在开发功能并遇到先前存在的错误，请清理出干净的 HEAD，修复该问题，然后在此基础上进行新功能开发，这样您就可以进行两项更改，每项更改都有一个单独的提交（<code>修复Y中的错误</code>、<code>添加功能X</code>），而不是一项更改有两个提交（<code>添加功能X并修复Y中的错误</code>）。</p><h1 id="将大的问题分解为小的解决方案">将大的问题分解为小的解决方案</h1><p>同样，将复杂的更改分解为更小、更简单的更改会更好。举个栗子：如果要建设一栋新房子，请不要一次性把整个房子全部完工后直接交付。将其拆分为每个易于理解的较小步骤：从地基开始、然后构建支撑、楼层、外装、室内设计等等，修依次慢慢来，做完一点、检查一点、交付一点。</p><p>如果您决定用铲子挖地基或用硬纸板构建框架，则既容易出错，也难以纠正可能埋在地底的误差。每次执行一点，提供足够的上下文，可以让整个架构更加容易理解，每个步骤仅完成所需的步骤即可，并需要使它具备足够的独立性，别的施工跟它无需耦合即可运行。</p><p>更改的最小体积应该是最简单的子问题的完整实现，该子问题可以独立工作并表达整个设计，而不仅仅是设计的一部分。您任意将1000行的更改分为十个100行的更改，但是这10个100行的更改可能没有任何意义，并且会增加总体复杂性。真正的目标是使每次更改具有最小的复杂度，行大小通常与问题和设计的复杂度紧密相关。</p><h1 id="编写清晰的提交信息commit-message">编写清晰的提交信息（commit message）</h1><p>最重要的一点是：提交消息应说明您进行更改的原因。</p><p>自动化检查能够检查提交消息，但只能强制执行结构，而不能强制执行内容。从结构上讲，提交消息可能应该：</p><ol><li>标题，在一行中简要描述更改。</li><li>总结，以更详细地描述更改。</li><li>也许还有其它信息。</li></ol><p>内容远比结构重要。特别是，摘要应说明为什么要进行更改以及为什么要选择要实现的实现。变更本身通常可以很好地解释变更的内容。举个栗子，下面是个完全没有意义的提交消息：</p><pre><code>修复了一个 bug</code></pre><p>下面这个也好不到哪儿去：</p><pre><code>增加了一个正则表达式限制用户名只接受 ASCII 和数字</code></pre><p>其实比较好的提交信息是能告诉 Reviewer 这个提交解决了什么样的问题，跟之前代码的区别在哪里，并且提供一下为什么要这么做的上下文，以及这是正确的做法。</p><pre><code>限制用户名只接受 ASCII 和数字在之前的实现中并未对用户名加以限制，任意字符都可以通过申请用户，这导致跟其它系统进行交互时发生问题，例如创建用户文件目录时因为受文件系统限制创建失败。这个补丁仅修改了前后端校验逻辑，通过在前端进行输入校验，对输入内容进行限制，避免了此类问题的发生。</code></pre><h1 id="具备上下文的注释信息">具备上下文的注释信息</h1><p>对于代码评审者而言，其实不怎么关心代码的作用是什么，对于绝大多数代码而言其实是可以读出来的，他们会更关心代码的上下文关系， 这段代码通过什么方式，解决了什么问题：</p><p>比如同样一段代码，等待 30 秒：</p><pre><code class="language-typescript">  // 推送镜像  exec('docker push tag');   // 等待 30 秒  await sleep(3e4);</code></pre><p>但是为什么要等待呢，却没有给出原因。但如果按照下面这样写，就很清晰了。</p><pre><code class="language-typescript">  // 推送代码后，因为仓库存在缓存，直接更新版本会出错，所以等待一下，稳妥起见等待 30 秒确保缓存刷新完成  await sleep(3e4);</code></pre><h1 id="确保实现无副作用且简洁">确保实现无副作用且简洁</h1><p>副作用（Side effect）简而言之就是一段代码的实现有两种不同的返回值，阅读体验上会非常糟糕，很难一眼就看出这段代码的作用是什么，为什么不同条件还会有不同的返回值？这些不同的返回值又有何用？</p><p>从编译器角度而言，如果不同返回值的类型不同，也会造成优化困难，甚至可能进入 slow path。</p><p>比如下面有一段代码，用来找一段数组中，比输入值大的数就返回：</p><pre><code class="language-typescript">/** * 在数组里面找出第一个比目标数字索引值大的数，如果没有则返回 null */function getLargerNumIndex(arr: number[], target: number): null | number {  let index = 0;  do {    let current = arr[index];    if (current &gt; target) return index;    index += 1  } while (true);  return null;}</code></pre><p>这个函数有什么问题？粗看其实没什么问题，但是不同的返回值类型在使用它的函数里，就需要进行额外的判断。而且获取索引值的函数里会拿到 null 本身就是个反直觉的设定，一般都是拿到 -1 才是。</p><p>从执行层面来说，现代 JS 引擎会对这种情况进行优化，但是保持相同的返回值类型也能提升 JS 引擎的识别和执行效率，更少的类型判断代码也能提升执行性能。</p><p>所以第一步，就是将结尾的 <code>return null</code> 改为 <code>return -1</code>，这样就保持了返回类型的统一。</p><p>如果对 JavaScript 很熟悉的人，还有个更简单的办法是直接使用数组的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex">findIndex()</a> 方法，直接一行就写完了，这就要求开发同学对数据结构的处理有扎实的基础，一行的代码所能表达的内容却比之前的多行内容一样，但是阅读负担就要少很多了。</p><pre><code class="language-typescript">function getLargerNumIndex(arr: number[], target: number): number {  return arr.findIndex(n =&gt; n &gt; target);}</code></pre><h1 id="足够的测试代码">足够的测试代码</h1><p>测试代码除了可以保证自身质量的保证意外，对于代码评审者而言，还是代码的使用说明书，很多情况下对于代码的审视都是从单元测试开始的，通过在不同场景内观察逻辑的不同行为和返回值就能知道逻辑的具体工作目标，所以测试代码写得好的话对代码评审者是种减负。</p>]]>
                    </description>
                    <pubDate>Sun, 04 Jul 2021 12:09:56 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[提问的艺术]]>
                    </title>
                    <link>https://kxq.io/archives/提问的艺术</link>
                    <description>
                            <![CDATA[<p>我们每天都会咨询别人很多问题，也可能会被别人问到很多问题，在回答、处理、解决这些问题的时候，我们常常会发现有些问题特别好，可以带来提问者和被问者的深刻思考和非常有建设性的讨论，最后提问者和被问者都得到了学习和提高。</p><p>而有些问题质量很差，通常提问者和被提问者讨论了半天都不知所云。那么怎么才能提一个好问题呢？怎么才能让自己的问题很快的得到回答？怎么才能让自己的问题对自己和对别人都有用处呢？</p><h1 id="提问前">提问前</h1><ol><li>首先应该尝试用在网上问答社区搜索一下，起码在问别人之前应该问一下 <a href="https://www.google.com">Google</a>，至少也应该使用 <a href="https://bing.com">Bing</a> 的国际版本，如果能够使用英文表达应该能收获更多回答。如果在一个论坛上提问，搜一下这个问题之前有没有人问过，不要让被提问者去做人肉搜索引擎。</li><li>阅读一下帮助文档、常见问题FAQ和源代码（如果有的话），别让别人回答你文档地址。</li><li>如果可以的话，自己应该试一试。比如*”x和y哪个快？“<em>就是一个烂问题。一个好一点的问题是：</em>“基于zzzz，我认为x应该比y快，但是我在tttt环境下的测试结果是x比y慢，为什么呢？”*。</li></ol><h1 id="如何提问">如何提问</h1><ol><li>明确描述标题，比如可以使用目标-差异形式的标题。使用准确的分类，标签（tag）等。</li><li>把你的问题写下来，站在一个被问者的角度看看自己能不能看懂自己的问题。我经常发现当我花时间组织自己的语言来描述清楚自己问题的时候，我已经知道我的答案是什么了，或者至少对如何找到答案有了新的想法。</li><li>如果是书面提问，斟酌一下字句，最好别有错别字、语法错误。更重要的是，如果粘贴代码，确保你的代码能<strong>编译通过</strong>！！！（除非你标明是伪代码）如果要写输入输出，确保他们是对的！！！</li><li>注意格式，特别是代码格式。</li><li>明确标明哪些是症状，哪些是你的猜测。</li><li>简短，别说废话！！！</li><li>除非非常肯定，别动辄就说找到了bug，什么东西设计的很烂等等，更可能的情况是你漏掉了什么东西。</li><li>找到合适的人或者论坛发问。项目相关请去 <a href="https://github.com">Github</a> 上发起 Issue；常规使用可以去 <a href="http://stackoverflow.com/">Stack Overflow</a> 或者国内的 <a href="https://segmentfault.com/">SegmentFault</a>；电脑问题可以去<a href="http://superuser.com/">superuser</a>；如果是服务器和网管，可以去<a href="http://serverfault.com/">serverfault</a>。如果针对这个具体问题有相关的论坛、邮件列表的，也可以去那里。</li><li>要有礼貌，没有人有义务回答你的问题。</li></ol><h1 id="问题应该包含什么">问题应该包含什么</h1><ol><li>包含问题的背景知识，帮助别人理解你的真正问题。比如你要解决X问题，发现它的一个子问题Y问题你不知道，那么在问Y问题说一下X问题。比如你要解决X问题，你想到了一个解决方案Y，但是你不知道Y怎么做，那么在问Y时说一下你的X问题。</li><li>包含问题发生的环境（机器配置、操作系统（版本）、编程语言（版本）、IDE（版本）、调试器等和问题相关的一切信息）。</li><li>包含一个最小的可以重现你的问题的步骤。</li><li>包含自己尝试了哪些诊断方法，得到了什么输出。尝试过哪些解决方案，遇到的问题分别是什么。自己学到了哪些东西，有哪些是可供被问者参考的。</li><li>如果自己已经排除了一些解决方案，说明这些解决方案为什么会被排除，这会有利于别人更好的理解你的需求。</li></ol><h1 id="怎么对待别的人回答">怎么对待别的人回答</h1><ol><li>问题解决后，给出一个总结，这样是对所有帮过你的人的表示尊重，同时也会帮到以后遇到同样问题的人。</li><li>如果别人的回答你没看懂，在继续追问前先做上面提到的<strong>提问前应该做什么</strong>。</li></ol>]]>
                    </description>
                    <pubDate>Fri, 05 Jun 2020 12:56:02 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Hippy 常用调试方法和常见问题案例]]>
                    </title>
                    <link>https://kxq.io/archives/hippy常用调试方法和常见问题案例</link>
                    <description>
                            <![CDATA[<p>近日，<a href="https://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247490089&amp;idx=1&amp;sn=64b8c26b95c17d2f6603637a770193f6&amp;chksm=eaab0079dddc896f47e97b4de98d1e139d18cd5d9aedaafbf4eead656d46d915815dba125a84&amp;token=1007227535&amp;lang=zh_CN#rd">腾讯开源跨端框架 Hippy</a>，一周即吸引3000+star。在腾讯内部，Hippy 已运行3年之久，跨 BG 共有 18 款线上业务正在使用 Hippy，日均 PV 过亿，且已建立一套完整生态。相较于其他跨端框架，Hippy 对前端开发者更友好：紧贴 W3C 标准，遵从网页开发各项规则，使用 JavaScript 为开发语言，同时支持 React 和 Vue 两种前端主流框架。本文为大家介绍了Hippy 常用调试方法和常见问题案例，希望能够帮助开发者快速上手。</p><h1 id="调试服务">调试服务</h1><p>前端调试在<a href="https://tencent.github.io/Hippy/#/guide/debug">官网</a>已经有专门章节进行描述，就不多说，这里具体说一下调试常见问题、案例和一些基本原理。</p><p>Hippy 已经在 <a href="https://www.npmjs.com/package/hippy-debug-server">hippy-debug-server</a> 中集成了一套基于 <a href="https://developer.chrome.com/devtools/docs/integrating">Chrome DevTools Protocol</a> 的调试服务器，启动后在终端进入本地调试界面，便可以进入远程调试模式。</p><p>目前 iOS 和 Android 都已经支持了真机调试，Android 通过 <code>adb reverse</code> 命令直接实现了本地调试端口的转发，就是指在手机上访问 <code>localhost:38989</code> 的调试端口时，访问的实际是开发机上的 <code>38989</code> 端口，但是 iOS 需要终端和前端的双方面配合修改端口才可以做到真机调试，所以建议先通过 iOS 模拟器进行调试工作。</p><p>启动调试服务、进入终端的本地调试环境后，JavaScript 代码将会通过调试服务加载到真机中运行，如果代码没问题应该能正常运行，但有时候会碰到启动就 Crash 的情况，可以参考常见案例最后一条“iOS 版本低于 9 时模拟器报告 SyntaxError”。</p><p>同样的，iOS 上有的特性有的能用 Polyfill 解决，但有的不行（例如 Proxy、正则表达式的 Sticky Flag 等就需要 iOS 10 以上才可以使用，而且无法 Polyfill），所以如果要兼容低版本 iOS，要注意不能使用到太新的 JS 特性。</p><h2 id="秘技整合到终端内的前端-jsbundle-包调试">秘技：整合到终端内的前端 jsbundle 包调试</h2><blockquote><p>该方案暂时只适用于 iOS</p></blockquote><p>有的 App 调试模式下运行很正常，但是打完包集成进去以后就挂了，这时候我们需要用到整合后的 jsbundle 包调试大法了。</p><p>其实非常简单，Hippy 在 iOS 中时通过自带的 JavaScriptCore 运行的，所以可以通过自带的 Safar 进行调试，在 Safari 的设置 -&gt; 高级打开开发者菜单后 ，启动 Hippy 就能看到多出了一个模拟器设备。</p><p><img src="https://ask.qcloudimg.com/draft/1928917/3a4t2zo8wj.png" alt="Safari 调试菜单位置" /></p><p>然后就可以用 Safari 开始调试了，唯一要注意的时，断点需要在启动后才生效，启动时是断不下来的，启动问题可以在关键点加上日志，日志能够正常输出。如果是其它启动后问题，可以直接打断点，跟 Chrome 调试服务的使用方法基本一致。</p><p><img src="https://ask.qcloudimg.com/draft/1928917/tyl7git2qx.png" alt="整合后包打断点" /></p><h1 id="内存占用情况">内存占用情况</h1><p>前端开发普遍对内存占用缺乏概念，直到终端同学过来说 JS 内存占用太多把 App 搞崩溃了才回过神来。</p><p>JavaScript 目前主要以<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#Mark-and-sweep_algorithm">标记清除算法</a>的方案来进行内存回收，它的核心是定期从全局对象中遍历所有对象，并且对不可到达的对象进行标记，并进而清除。</p><p>在绝大多数情况下作为前端开发确实不需要关心内存占用，但是 Hippy 中不太一样，Hippy 是前端的开发方式去开发终端 App，有几个类在组件卸载时一定要记得销毁，包含了 React 中负责事件监听的 EventEmitter 实例、Animation/AnimationSet 动画组件，Vue 中的 $app.on() 终端事件监听等等，不释放掉它们，它们就会一直占用着内存，随着界面越来越多，App 最终将会崩溃。</p><p>其实调试方法也非常简单，直接在调试器的 Memory 观察内存占用情况，打快照看一下当时各类对象对内存的占用情况，它是 Hippy 在浏览器里运行的容器，可以代表 App 的整体内存占用情况。</p><p><img src="https://ask.qcloudimg.com/draft/1928917/8ck982m7bo.png" alt="内存调试方案" /></p><p>这部分内容 Google 官方也有<a href="https://developers.google.com/web/tools/chrome-devtools/memory-problems/">文档</a>。</p><h1 id="常见案例">常见案例</h1><h2 id="1-数据已经更新但是界面内容或者样式不变">1. 数据已经更新，但是界面内容或者样式不变</h2><p>这是经常碰到的，最直接的方式是对 React 和 Vue 进行界面绘画的模块 - <a href="https://github.com/Tencent/Hippy/blob/master/core/js/global/UIManagerModule.js">UIManagerModule</a> 的三个方法 - <code>createNode</code>、<code>updateNode</code>、<code>deleteNode</code> 打断点，其实不管 MVVM 怎么做，最终都会通过这三个方法把界面通知终端画上去，这其实也带来了无限的扩展性，任何框架只要对接了这三个方法就可以进行 Hippy 绘制，如果掌握了 UIManagerModule 的语法，甚至不需要 React 或者 Vue 也可以直接通过它画界面。</p><p>但从一定程度上来讲，Hippy 画界面的方式其实跟浏览器是不一样的，它是异步的，MVVM 里组件创建完毕，componentDidMount 或者 mounted 后，其实并不意味着界面真的画上去了（但是这个耗时极少，mounted 后基本可以认为真的画上去了），如果要对界面进行操作，需要确定终端确实画上去了才行，这可以通过 onLayout 事件获得；其次可以看到画界面和普通的 Native Module 调用没有本质区别，最终都要通过 JSBridge 进行通讯。-- 这部分正在通过 C++ 方式重写。</p><p>通过观察它，我们可以了解到最终通过 React、Vue 解析后的组件是什么样的，可以观察到为什么界面没有更新，或者样式不如预期。</p><p><a href="https://tencent.github.io/Hippy/">Hippy</a> 的前端框架在开发初期就考虑到了调试的便利性，调试模式下会将前端框架与终端之间的通讯都打印到 Console 里，当觉得自己的业务 App 或者框架显示存在问题时，直接观察它就能很方便获得所有信息。</p><p>以 Hippy-Vue 为例：</p><p><img src="https://ask.qcloudimg.com/draft/1928917/2pnubv5z2l.png" alt="Hippy-Vue 的终端通讯日志" /></p><p>Hippy-Vue 要关闭该功能只要将入口文件中的 <a href="https://github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/src/main-native.js#L9">Vue.config.silent</a> 改为 true 即可；Hippy-React 要关闭该功能需要在启动参数里增加一个 <code>silent: true</code>。不过一般不建议关闭，它在打包后会自动停止输出。</p><h2 id="2-scrollviewvue-的-div--overflow-xy-scroll或者-listviewvue-的-ulli无法滚动">2. ScrollView（Vue 的 div + overflow-x/y: scroll）或者 ListView（Vue 的 ul/li）无法滚动</h2><p>在 Hippy 中只有这两种 View 是可以滚动的，剩下的都不可以滚动，但是要让它们能滚起来也不是那么简单，需要有样式进行配合，简单说就是：</p><ul><li>ScrollView 以上所有父节点都必须有一个固定的高度，ScrollView 中只能嵌套一个内容子节点，它可以随意变高。</li><li>ListView 以上所有父节点都必须有一个固定的高度，里面所有的 renderRow 出来的 ListItemView（Vue 中的 li）可以随意变高。</li></ul><p>这里的固定高度可以是直接指定高度，也可以是通过 flex 进行界面动态分割的高度，但是一定要是固定的，因为滚动实际是终端去实现的，它需要能够区分可以滚动和不可以滚动的区域，如果容器高度和内容高度一样，那就变成不可以滚动了。</p><p>另外 Vue 里的 <code>ul</code> 默认已经加上了 flex: 1 样式会把整个 View 撑满屏幕，一般情况下不用做特别处理，但是 div + overflow-x/y: scroll 依然需要手工指定高度。</p><p>当滚动出现异常的时候，可以通过 XCode 调试一下终端代码，它有个 <code>Debug View Hierarchy</code> 功能，可以非常直观地看到界面层级和尺寸，对调试样式问题有很大帮助。</p><p><img src="https://ask.qcloudimg.com/draft/1928917/dyi4oszubo.png" alt="XCode 的界面层级调试" /></p><h2 id="3-listviewvue-里的-ulli性能很差卡顿闪烁">3. ListView（Vue 里的 ul/li）性能很差、卡顿、闪烁</h2><p>这里需要提到前端三点非常需要注意的地方：</p><ol><li>如果界面发生异常闪烁，首先需要通过第一个小章节里的 UIManagerModule 观察法，看一下那那三个方法是否有异常的执行，例如 updateNode 执行过于频繁，或者 deleteNode/createNode 异常执行，这通常发生在数据有变化导致界面重绘，可以通过调用栈看一下是哪里的数据更新导致界面重绘，并针对性地进行前端优化。</li><li>ListView 决定界面是否重绘，有个很关键的参数是 key（<a href="https://reactjs.org/docs/lists-and-keys.html#keys">React 官文</a>、<a href="https://vuejs.org/v2/guide/list.html#key">Vue 官文</a>），Hippy-React 也通过 getRowKey() 的方法实现了 key 在 ListView 中的应用。key 其实是数据的唯一标示符，数据不发生改变，key 就不应该发生改变，而 key 一旦发生改变 ListView 就会重绘。目前很多业务在开发时 key 不指定，或者把 index 作为 key，前者会导致 ListView 每次有数据更新都做一次完整的 Array diff，开销非常大，后者会导致删除中间一个节点时将后面所有的节点全部删除再重新插入一次，开销也非常大。处于性能考虑，key 是必须要加的，一般跟数据的主键保持一致即可。<br />**但是：**如果 ListView 中的数据需要进行排序，那就不要指定 key 了，目前 Hippy 的 <code>moveNode 功能</code>，已经计划但仍未完成，指定 key 后在重新排序时会因为对应索引的 key 值不同，先删除全部节点内容，再全部重建，可能会造成轻度闪烁。如果此时不指定 key，就只有一个更新节点的请求，两次请求合并为一次，终端层会对数据进行对比并更新节点内容。</li><li>如果到这一步终端渲染依然很慢、帧率低，我们就要提到另外一个参数 type 了，对应到 Hippy-React 里是 getRowType() 方法，它是用来表示组件样式的，样式不变，type 就不变。这里需要先说一下 Hippy ListView 的复用机制，当不指定 type 时，每次有新的 ListItemView 被渲染（HippyReact 里 renderRow() 将返回 ListItemView，Hippy-Vue 里的 li），终端都会重新构建所有终端组件节点，加了 type 之后，会将将之前渲染过的终端组件节点放到缓存池中，下次碰到相同 type 类型的 ListItemView，就不会重新渲染，而是从缓存池中把缓存的节点拿出来做次拷贝并更新数据，再上屏，即使只有一个样式的 ListItemView，通过 type 也能做到性能优化。</li></ol><p>经过上面三步，能解决 90% 的 ListView 性能问题。</p><p><a href="https://github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/src/components/demos/demo-list.vue#L28">Hippy-Vue 官方范例</a>中也对这三个参数加了注释。</p><h2 id="4-ios-上-listview-不渲染但-android-没问题">4. iOS 上 ListView 不渲染，但 Android 没问题</h2><p>首先需要检查 <a href="https://tencent.github.io/Hippy/#/hippy-react/components?id=%25252525252525e5%252525252525258f%2525252525252582%25252525252525e6%2525252525252595%25252525252525b0-1">numberOfRows</a> 参数是否真的是 ListView 中 ListItemView 的数量，这个除了在业务代码中打断点查看数据数量是否和 numberOfRows 一致以外，也可以通过第一个 UIManagerModule 的调试方法查出来。</p><p>这个问题牵扯到 iOS 上一个 ListView 的上屏性能优化，iOS 上并不是发一个 ListItemView 就上屏一个的，而是需要先改变 ListView 的 numberOfRows 再去创建节点，当节点数量与 numberOfRows 一致时再上屏。</p><p>目前碰到的所有不渲染的问题都是因为这个原因造成的。</p><p>另外在 Hippy-Vue 中，对于静态的 li（就是终端的 ListItemView），可以不需要手工指定 numberOfRows，Hippy-Vue 会在 DOM 层计算子节点数量。但是对于动态获取的数据，也必须要加上该参数，因为 Hippy-Vue 位于 Vue 的渲染层，跟业务还隔了一个 Vue，无法知道业务到底有多少数据准备要渲染。</p><h2 id="5-iphone-中红屏报告-modulenotregist">5. iPhone 中红屏报告 ModuleNotRegist</h2><p>这里需要提到 Hippy App 的启动方式：当终端 JS 引擎加载完 JavaScript 后，会从 <a href="https://github.com/Tencent/Hippy/blob/master/core/js/bridge/ios/native2js.js#L48"><strong>GLOBAL</strong>.appRegister</a> 对象里去寻找终端指定的 moduleName，而 <code>__GLOBAL__.appRegister</code> 是在 Hippy 启动时通过 <a href="https://github.com/Tencent/Hippy/blob/master/core/js/global/Others.js#L18">HippyRegister.regist()</a> 方法注册上的，在 <a href="https://github.com/Tencent/Hippy/blob/master/packages/hippy-react/src/hippy.ts#L76">Hippy-React 入口文件</a> 或者 <a href="https://github.com/Tencent/Hippy/blob/master/packages/hippy-vue/src/runtime/index.js#L98">Hippy-Vue 入口文件</a> 定义的 appName 最终都会执行到 regist() 方法上进行  <code>__GLOBAL__.appRegister</code> 的注册，首先，我们要检查终端的 moduleName 是否和 appName 一致。</p><p>如果一致依然出错的话，很大几率是之前 JS 执行失败，也不排除 SDK 更新后存在 bug，也有可能其它问题，导致 <code>__GLOBAL__.appRegister</code> 未注册成功，但我们有个办法可以在<a href="https://github.com/Tencent/Hippy/blob/master/core/js/bridge/ios/native2js.js#L67">该错误抛出时</a>二次确认一下终端所寻找到 moduleName 是否和前端定义的 appName 一致，可以在那一行打上日志，然后使用上文的 Release 包调试方案检查终端过来查的到底是什么 appName。</p><h2 id="6-ios-版本低于-9-时模拟器报告-syntaxerror">6. iOS 版本低于 9 时模拟器报告 SyntaxError</h2><p>这是因为 Hippy 自带的 Webpack 默认调试模式配置文件，最低仅开启了 iOS 9 的输出，因为输出到 iOS 8 会多出很多 polyfill，语法上也会转换，导致体积大很多。</p><p>Hippy 本身最低支持的 iOS 8，我们建议在高版本的 iOS 上进行调试，然后打包后在低版本 iOS 走一遍测试流程，没什么问题即可。</p><p>如果非要在低版本的 iOS 上进行调试，修改一下 <a href="https://github.com/Tencent/Hippy/blob/master/examples/hippy-react-demo/scripts/hippy-webpack.dev.js#L44">webpack 配置文件</a> iOS 将 preset-env 中的 ios 版本改成更低即可，但目前经过测试 core-js 对 iOS 8 那样对低版本可能存在问题，这就需要你自己手工调整了。</p>]]>
                    </description>
                    <pubDate>Tue, 07 Jan 2020 15:55:24 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[浏览器原生能力系列 - 错误和异常]]>
                    </title>
                    <link>https://kxq.io/archives/liu-lan-qi-yuan-sheng-neng-li-xi-lie--cuo-wu-he-yi-chang</link>
                    <description>
                            <![CDATA[<blockquote><p>Error 对象在 JS 中貌似是一个长期被忽略的对象，很多人宁愿用别的方法来描述错误，例如一个特别类型的返回值，或者通过返回码，但其实这个对象从 ES1 里引入开始就带来了无限的可能性。</p></blockquote><p>笔者开发代码的时候，一直偏好将函数的正常输出和异常分开，类似这样：</p><pre><code>function mustBeEqual(a, b) {  if (a !== b) {throw new Error(`${a} is not equal with ${b}`);  }  return true;}try {  const equal = mustBeEqual(1, 1);  console.log('1 is equal with 1');  const equal2 = mustBeEqual(1, 2);  console.log('1 is equal with 2');} catch (err) {  console.error(err.message);  console.error(err.stack);}</code></pre><p>可以看到正确的输出和错误的输出泾渭分明：<br /><img src="https://i.loli.net/2021/07/08/3LWNydA15vej9qQ.png" alt="1506302611_100_w491_h104.png" /><br />我一直觉得这样的写法有几个好处：</p><ol><li>可以将异常逻辑和主流程逻辑分开，从而使可读性更加清晰。</li><li>Erlang 中有一句话叫做：“Let it crash”。 - 这不是说让程序真的崩溃了，而是提醒开发者小心处理每一个错误，有的时候崩溃了会更加容易发现问题所在。</li><li>Error 对象的一些属性，例如 stack 对于发现问题所在位置其实非常有帮助，它对于还原问题帮助非常大。</li></ol><p>Error 对象的具体参数请参考一下 MDN，就此不再多述: <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error">https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error</a></p><p>我只想说 Error 的主要用法。</p><h2 id="继承出业务错误类型">继承出业务错误类型。</h2><p>在项目开发中，会碰到各种各样的网络、数据库、外部 RPC 调用，各种问题出现之后难以以一种统一的方案去解决。</p><p>此时其实可以通过继承几个业务错误，把底层错误转换为自己项目中所使用的，二次抛出后进行处理。</p><p>例如下面代码，摘取自 <a href="http://git.code.oa.com/weplus/serverplus/blob/master/src/errors.ts">http://git.code.oa.com/weplus/serverplus/blob/master/src/errors.ts</a> 这里是 Typescript 的语法。</p><pre><code>/** * Base Error */class BaseError extends Error {  public status: number;  public code?: number;}/** * Request not passed validation will trigger */class BadRequestError extends BaseError {  public status = 400;}/** * The user was not login */class UnauthorizedError extends BaseError {  public status = 401;}export {  BadRequestError,  UnauthorizedError,}</code></pre><p>这样做有几个好处：</p><h3 id="上层可以对输出进行统一处理">上层可以对输出进行统一处理</h3><p>底层输出只有两种：正常返回和异常，所有的异常都是一个错误的对象，这样就可以简化处理逻辑，对正常输出走业务逻辑，而错误会全部进入 catch 段进行异常处理。</p><p>在上面的例子中，HTTP 的状态码就是依靠错误的 status 属性进行确定，当某个业务流程需要返回一个错误时，直接 throw 即可。</p><p>摘自 <a href="http://git.code.oa.com/weplus/serverplus/blob/master/src/base-model-controller.ts">http://git.code.oa.com/weplus/serverplus/blob/master/src/base-model-controller.ts</a></p><pre><code>/**  * Get single record method  */public async get(req, res) {  let authenticated;  try {authenticated = await this.apiAuthenticate(req);  } catch (err) {throw err;  }  if (typeof authenticated === 'boolean' &amp;&amp; !authenticated) {throw new this.errors.ForbiddenError('Permission denied', {  requestId: req.headers['X-Request-Id'],});  }  const id = req.params.id;  const result = await this.__get(req, id, req.query, req.params);  return result;}</code></pre><p>上面的路由层接收到一个错误，而不是一个正常的返回值时，就会将它作为错误进行输出。</p><h3 id="上层也可以通过继承链找到具体错误的具体类型和原因">上层也可以通过继承链，找到具体错误的具体类型和原因。</h3><p>通过 instanceof 去找错误，效率比通过字符串高出数倍不止，可以将程序内的错误，和给用户的提示分开，可以根据不同的错误类型，进行不同的处理。</p><pre><code>const err = new TypeError('Something went wrong');err instanceof TypeError// trueerr instanceof Error// trueerr instanceof RangeError// falseerr.message// &quot;Something went wrong&quot;</code></pre><h3 id="因为异常本身是可被触发的它可以干更多事情例如将错误自己上报给错误监测系统">因为异常本身是可被触发的，它可以干更多事情，例如将错误自己上报给错误监测系统。</h3><p>在 <a href="http://git.code.oa.com/weplus/serverplus/blob/master/src/errors.ts">http://git.code.oa.com/weplus/serverplus/blob/master/src/errors.ts</a> 还可以看到，在基类错误的构建函数中，还有一段 <code>Raven.captureException()</code> 的上报方法，这样我们在这里利用了错误的构建时，将自身连带附加信息一起上报给了 Sentry（Sentry 是什么情参考<a href="http://km.oa.com/group/502/articles/show/312784">《Web 前后端错误上报及问题跟踪》</a>）。</p><pre><code>/** * Base Error */class BaseError extends Error {  public status: number;  public code?: number;  constructor (message, context: any = {}) {super(message);Object.setPrototypeOf(this, BaseError.prototype);if (!config.sentry) {  return this;}const extra = {  status: this.status,  code: this.code,  ...context,};Raven.captureException(this, {  extra,});  }}</code></pre><p>这样一来，当服务器发生异常时候，不用改动主逻辑代码，自动实现了错误上报，这样可以作为一个单独的错误上报层在项目中使用，而且它在上报的时候，还能带上上下文，这样对于错误还原帮助巨大。</p><p>还是上面那个例子，这里在非法请求时在返回 Permission Denied 的同时，把前端请求过来的 requestId 一起附带上报，前端整合 Sentry 后可以做全链路的错误还原。</p><pre><code>  if (typeof authenticated === 'boolean' &amp;&amp; !authenticated) {throw new this.errors.ForbiddenError('Permission denied', {  requestId: req.headers['X-Request-Id'],});  }</code></pre><h2 id="面向错误进行应用开发">面向错误进行应用开发</h2><p>这种开发模式可以根据场景使用，需要将之前的思维进行小幅度调整，之前 <code>res.code === 0 &amp;&amp; doSomethingRight() || doSomethingWrong()</code> 的方式得用 try catch 重新进行梳理，但这样做可以让主线流程代码变得很干净，减少大量的 if else。</p><p>面向错误进行开发，需要控制好 try catch 的颗粒度，理论上都是越细越好的，如果一个大的 try 都裹在一起，任何一处发生问题后都会走入 catch 环节这会加大判断错误问题发生位置的难度，尤其是在某些未对底层错误进行二次捕获抛出的架构中会更加严重。</p><h2 id="过去和未来">过去和未来</h2><p>在早期的浏览器引擎中， try catch 方式是比较低效无法被优化的，不过现在新版的 V8 引擎 TurboFan 已经对 try catch 进行了大幅度调整，之前无法被优化的代码也可以以最优方式运行，而服务器端截止本文完稿，搭载 TurboFan 的 Node 8 已经进入 Stable，预期 10 月份进入 LTS，这会让 try catch 的使用更加放心。</p><p>面向错误进行开发这种开发模式其实在 Java、Python 或其它语言中已经非常普遍，但在 Javascript 领域目前感觉比较好的是 NodeJS 上的 ORM 库 <a href="https://github.com/sequelize/sequelize/blob/master/lib/errors/index.js">Sequelize</a>，它里面对错误都进行了良好封装。</p><p>我希望未来这样比较小，但是有用的开发模式能更加普及。</p>]]>
                    </description>
                    <pubDate>Mon, 25 Sep 2017 15:03:28 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Git 全功能介绍]]>
                    </title>
                    <link>https://kxq.io/archives/gitquan-gong-neng-jie-shao</link>
                    <description>
                            <![CDATA[<p>目前组内正在从 SVN 逐步迁移到 Git 上来，趁机介绍一下觉得挺好的，之前做过一次分享，但因为最近项目一直很忙，这篇总结才刚出来。</p><h1 id="git-历史和现状">Git 历史和现状</h1><p>Linux 的作者的另一个作品，2002年时他还在使用 Bitkeeper 作为 Linux 内核的版本管理，但因为它是 Copyright 有版权的软件备受质疑，然后 Andrew Tridgell 对 Bitkeeper 进行逆向工程，导致 BitMover 要回收 Linux 开发者的 Bitkeeper 的免费使用权，Linus 一怒之下花了10天写出了 Git。</p><p>名字的意思是：混蛋 - egotistical bastard</p><p>如今 Git 已经成为绝大多数开发者的选择， Tom Preston-Werner、Chris Wanstrath 和 PJ Hyett 在 2007 年 10 月推出的 Github 已经成为了全球最大的开发者网站，我们厂在上面也是贡献颇多。</p><p><img src="https://i.loli.net/2021/07/08/pSdzn6KM5bger1o.png" alt="Tencent at Github" /></p><p>更有甚者，一向自己造轮子的的微软，也打算把巨达 300G 的 Windows 源代码迁移到 Git 上进行管理，他们为 Git 提供了新的 GVFS 实现，有效地改善了 Git 对巨大代码仓库的性能。</p><p><img src="https://i.loli.net/2021/07/08/EO8bmtjGAraXU1v.png" alt="Microsoft will migrate windows source code to git" /></p><p>另外说一句：Docker 的二进制 image 管理，也是基于 git 实现的。</p><h1 id="集中式版本管理和分布式版本管理">集中式版本管理和分布式版本管理</h1><p>Git 和 SVN 是从设计理念上就不一样的版本工具，SVN 将代码进行中心化管理，拥有更好的稳定性和安全性，但是去中心化的 Git 却是从 Linux 操作系统的开发需求而来，更加适合多人协作的开源项目，可以以任何一个点为 remote 将他的代码与本地代码合并，随着时间发展，还衍生出了更多强大功能和一整套操纵流程，让它也可以适应了商业软件的开发。</p><p><img src="https://i.loli.net/2021/07/08/u2Kh5beGsQ9lRDc.png" alt="Central and distribution" /></p><h2 id="git-和-svn-代码历史的不同">Git 和 SVN 代码历史的不同</h2><p>SVN 的代码历史相对比较简单，因为它是中心化的，所有人的代码都直接提交到某个 repository 上，所以它的 Reversion ID 号是一个按顺序增加的数字类型，一般情况下不能在两个数字之间插入别的 reversion。</p><p><img src="https://i.loli.net/2021/07/08/hoQ9TVzrKfeI3Ct.png" alt="SVN History" /></p><p>Git 的看起来就是杂乱多了，它的 Reversion ID 号是一个 40 位长度的 hash 值，通常也可以缩写为 7 位，这样做的原因是因为 Git 的最小单位是代码修改的历史，即为补丁 Patch，而分支、 Tag、Remote（一会儿会说到这些概念）等都只是分支的集合，互相之间可以随意拆分、合并。</p><p>我更愿意把分支、Tag、Remote 想象成不同的平行宇宙，因为某些机缘导致产生了分裂，走向了不同的历史，也可能因为某些机缘又合并到了一起，变得更加强大。</p><p><img src="https://i.loli.net/2021/07/08/ycTtaHkr6hYzXfx.png" alt="Git history" /></p><h1 id="git-基础命令">Git 基础命令</h1><p><img src="https://i.loli.net/2021/07/08/vk96VdXHJGW3zEL.png" alt="Git sences" /></p><p>Git 按照场景可以分为以下场景（Scence）：</p><ol><li>Workspace：当前工作区，修改的的最初状态。</li><li>Staging：修改后，添加到准备提交的缓存状态。</li><li>Local repository：本地的代码仓库，只对自己的代码生效。这也是和 svn 区别之一，svn commit  之后就直接提交到远程服务器了，git commit 之后只是到本地代码库。</li><li>Remote repository：远程代码库，将自己的本地代码库同步到远程代码库上，这样可以供别的开发者分享自己的成果。</li></ol><p>具体流程看图即可，下面对几个常用命令进行简单介绍</p><p>*PS: 图中没有提到 rebase 和 cherry-pick 命令，这两个命令也非常强大，后面有提到，有时间可以关注一下 *</p><h2 id="补丁-diff">补丁 diff</h2><p>之前有提到过，补丁是 Git/SVN 代码版本管理的基础概念，它其实是以行为单位的文件修改历史，增加行以 + 号开头 ，删除行以 - 号开头，而修改一行，就是先 - 后 +。</p><p>在 Git 里可以通过 <code>git diff</code> 或者 Linux/Mac/Conemu 中，也可以通过 <code>diff -Naur</code> 来生成文件对比结果，有点类似下图。</p><p>这是整个代码管理的基础概念，所有的分支、Tag、Remote 都是在此基础上衍生的。</p><p><img src="https://i.loli.net/2021/07/08/dlg1coHXP7L4nVG.png" alt="Git diff" /></p><h2 id="基本流程">基本流程</h2><h3 id="1--克隆代码到本地开发环境---clone">1.  克隆代码到本地开发环境 - Clone</h3><pre><code>$ git clone [REPOSITORY_URL]</code></pre><p>对应到了 <code>svn checkout</code> 命令，用于把远程代码克隆到本地，跟 svn 一样，REPOSITORY_URL 的协议非常灵活，之前流行用 ssh/scp 协议，但现在 https/http 协议渐渐流行起来了。</p><h3 id="2-更新代码---statuscommitlog">2. 更新代码 - Status/Commit/Log</h3><p>刚有提到过 Git 的四种场景，其中前两种场景需要通过</p><pre><code>$ git status</code></pre><p>命令查看，代码刚刚新建可以看到是 <code>Untracked files</code>（Workspace） 状态，执行 <code>add</code> 之后变成 <code>Changes to be commited</code>（Stage） 状态，修改了 Stage 中的文件，又会变成 <code>Changes not staged for commit</code> 状态。<br />执行 <code>commit</code> 之后就从 Stage 中转移到了 Local repository 中，可以通过</p><pre><code>$ git log</code></pre><p>查看到代码提交。</p><h3 id="3-branch-和-tag">3. Branch 和 Tag</h3><p>如刚从所说，Branch 和 Tag 都可以看成是补丁的时序化集合，branch 可以互相合并，在 clone 完 repository 后有一个主线分支叫做 master。而 Tag 用于发布后标记版本，这两个只是从名字上不一样，功能（我感觉实现上）并没有太大区别。</p><p>和 SVN 不同， SVN 的 Branch 和 Tag 都是把 Trunk 整个代码库拷贝出来，Git 只是将补丁引用重新对当前代码应用一下，所以 Git 的 Branch/Tag 都非常轻量，切换起来非常轻松，使用 Git 要尽量多使用它的分支来提高开发效率，一会儿提到 Git flow 时会描述一下如果用分支进行代码功能开发管理。</p><h4 id="31-新建分支的两种办法">3.1 新建分支的两种办法</h4><pre><code>$ git checkout -b [BRANCH_NAME] # 在当前版本切换并新建分支$ git branch [BRANCH_NAME] # 直接新建分支</code></pre><h4 id="32-切换分支">3.2 切换分支</h4><pre><code>$ git checkout [BRANCH_NAME] </code></pre><h4 id="33-合并分支的两种办法">3.3 合并分支的两种办法</h4><pre><code>$ git merge [BRANCH_NAME] # 将另外一个分支的代码，打到当前分支之后。$ git rebase [BRANCH_NAME] # 不推荐，对代码进行比较，将本分支修改后的代码打到另外一个分支之后</code></pre><p><code>rebase</code> 通常情况下不推荐使用，因为 rebase 完下游分支，再从上游分支 merge 的时候会丢失分支合并的 commit，但是对于部分有 history mysophobia 的人来说，它是保持代码提交历史记录干净的神器，那个 Merge branch 'xxx' of <a href="http://github.com/xxx">http://github.com/xxx</a> into yyy 的 commit 看起来也挺讨厌的。</p><p>对于已经推到 remote repository 的 commit，是不建议 rebase 的，因为一旦 rebase 了，别人再 pull 就会出一大堆的冲突 conflict，而且基本没法修，通常情况下还是建议用 merge 稳妥一些。</p><p><em>PS: rebase 还有一个强大的功能是配合 --interactive 参数修改之前的补丁，具体自己查一下，但是改完了 push  --force 上去，别人再 pull 回来出 conflict 是必然的，但是这招是修改 Github 上的 Pull request（后面有提到）必备技能。</em></p><h4 id="34-删除分支">3.4 删除分支</h4><pre><code>$ git branch -d [BRANCH_NAME] # 已经合并到 master$ git branch -D [BRANCH_NAME] # 该分支未合并到 master，强制删除</code></pre><p><em>PS: 即使删除了分支等，也可以用 git reflogs 找回来喔</em></p><h4 id="35-取消修改">3.5 取消修改</h4><pre><code>git stash # 取消全部修改，很强大的是它可以恢复过来，具体自己查一下git reset —soft [REV] # 保留修改内容，从 Local repository 中撤销，也可以用于回退历史记录git reset —hard [REV] # 丢掉修改内容，从 Local repository 中撤销，也可以用于回退历史记录</code></pre><h3 id="推送本地代码到远程仓库">推送本地代码到远程仓库</h3><p>推送代码是为了跟别人一起合作，命令行非常简单</p><pre><code>$ git push [REMOTE] [BRANCH]</code></pre><p>remote 默认为 origin，如果不填的话就推送到它上面，branch 默认为当前分支，其实可以不加，加了就把指定的分支推送到远程了。<br />如果要将推送本地功能分支，建议 push 后面加上 —set-upstream 参数。</p><p>郑重警告：**永远不要对主线 master 分支执行 —force **</p><h3 id="获取远程分支更新">获取远程分支更新</h3><pre><code>$ git pull # 把代码更新到  workspace$ git fetch # 把代码更新到 Local repository，可能需要通过 merge 再合并到 worksapce 一次。</code></pre><h4 id="远程仓库">远程仓库</h4><p>Clone 之后会有一个默认的远程仓库为 origin，但如果还要增加别的远程仓库，就需要用到下面命令了：</p><pre><code>$ git remote add [REMOTE_NAME] [URL] # 添加原创仓库$ git fetch [REMOTE_NAME] # 获取远程仓库更新$ git branch -a # 查看包括远程仓库以内的所有分支$ git push [REMOTE_NAME] [BRANCH_NAME] # 推送到远程仓库</code></pre><h1 id="github-pull-request--gitlab-merge-request">Github Pull Request &amp; Gitlab Merge Request</h1><p>Github 在 Git Remote 的基础上为了方便大家参与开源项目，衍生出的一套机制，目前常规开源项目的参与流程是，先注册一个 Github 账号，然后将感兴趣的开源项目 Fork 一份到自己的 namespace 下，然后拆分分支进行修改，然后提交到自己的 Github repository 下，再发起一个 Pull Request，让项目维护者来合并你的代码（Pull request 名副其实），在这个过程中，项目维护者会对你的代码进行 Review 和点评，你得按照维护者要求进行修改（这里 rebase 会用得很勤），修改通过，维护者同意后，就有他将代码合并进项目中。</p><p>具体流程自己走一圈就明白了，Gitlab 的 Merge Request 原理是一模一样的。</p><p><em>PS: 范例图片在 PPT 的第 22 页起</em></p><h1 id="git-flow">Git flow</h1><p>Git flow 本来应该是本文的重点内容的，它是在 Git branch 的基础上实现了一套简单的功能模块化开发流程，主要思想是把分支分成了上下游几个层级，然后通过一套命令行工具进行维护。</p><ol><li>master 分支 - 与线上版本保持一致，当发生线上问题后可以很轻松地修复。</li><li>develop 分支 - 功能开发基线分支，功能开发完之后合并到上面，所有功能开发完毕后经过测试上线，然后合并回 master。</li><li>feature/* 功能开发分支 - 从 develop 上拆分，也需要随时把 develop 的更新 merge 回来，跟上游的分支保持一致，等功能开发完毕之后，即可合并到 develop。通过和上游分支保持一致，这样可以避免对误删别人的代码，所有代码冲突必须在下游分支修好，测试完毕后才可合并到上游分支。</li><li>hotfix/* 热修复分支 - 主要用于线上 bug 修复，但是修复后应该同时合并到 master 和 develop 两个分支上。</li></ol><p><img src="https://i.loli.net/2021/07/08/wlHAn3xTPCyK2g1.png" alt="Git flow" /></p><p>然后 git flow 提供了套命令行工具来更加轻松地去做这些代码合并的事情。</p><pre><code>$ git flow init # 初始化 git flow 分支模型$ git flow feature start [NAME] # 开始一个功能分支$ git flow feature finish [NAME] # 将功能分支合并进 develop$ git flow hotfix start [NAME] # 开始一个热修复分支$ git flow hotfix finish [NAME] # 将补丁合并进 develop 和 master$ git flow release [NAME] # 发布一个新版本，打 tag </code></pre><p>感觉 Git flow 得有个篇幅，下次有机会再来详述。</p><h1 id="其它内容">其它内容</h1><p>有兴趣可以继续看一下别的相关内容，非常有意思：</p><ol><li>git svn -  Git 可以以 svn 为代码后端，通过 Giit 来对 SVN 里的代码进行版本管理。</li><li>git reflogs - 引用记录，在 git 中误删除某提交是不用害怕的，只要 commit 了，就可以通过 reflogs 找到，用 reset 或者 cherry pick 恢复。</li><li>git cherry pick - 摘樱桃（commit），从另一个分支中单独将某个 patch 摘回来。</li><li>git bare repository - 建立 Git 服务器</li><li>git submodule - 子模块，一个大项目可以通过 submodule 进行拆分，可以随时进行子模块的版本更新和回溯。</li><li>git hooks - 钩子，当 git 在 repository 上发生某种行为的时候，可以通过钩子触发一些行为，像目前 Github 上的一些第三方持续集成服务，就是在此基础上实现的。</li><li>git signature - 签名，通过 gpg 在 commit 时对 patch 进行签名，证明那个补丁确实是自己提交的，可以参考一下 <a href="https://help.github.com/articles/signing-commits-with-gpg/">Github 的文档</a>。</li><li>Source Tree - <a href="https://www.sourcetreeapp.com">Source Tree</a> 是一套跨平台的 Git 图形界面，简单方便，我目前主要用它进行基本的 Patch 阅读，比命令行更加舒服。</li></ol><p>多谢阅读，下次有机会再写。</p>]]>
                    </description>
                    <pubDate>Mon, 10 Jul 2017 19:52:00 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[浏览器原生能力系列 - 时间、日期和时区]]>
                    </title>
                    <link>https://kxq.io/archives/浏览器原生能力系列-时间日期和时区</link>
                    <description>
                            <![CDATA[<blockquote><p>在做国际化项目中，服务器端时间转换为用户本地时间一直是个问题，其实浏览器本身已经提供了这样的能力，而且它可能比想象中更加容易。</p></blockquote><blockquote><p>时区（Time Zone)是地球上的区域使用同一个时间定义。1884年在华盛顿召开国际经度会议时，为了克服时间上的混乱，规定将全球划分为24个时区。<br />在中国采用首都北京所在地东八区的时间为全国统一使用时间。 - <a href="http://baike.baidu.com/item/%E6%97%B6%E5%8C%BA">百度百科</a></p></blockquote><p>时区，做国际化项目中肯定会遇到，因为不同人所在地区不同，时间也是不同的，从这点来说，时间和地区也是相对的。传统处理时间的方式是将本地时间序列化成类似“2014-04-14 13:10:28”这样的字符串，这样的缺点显而易见，一是将时间转换为这样的字符串，需要额外的代码，因为日期格式不一定，这样的需求作为 Javascript 的语言根本不会去处理，换而言之，这样的字符串虽然方便人类阅读，但是却不利于计算机处理；二是这样只记录了本地时间，当一个项目走向国际化就需要一个单独的时区字段来进行转换，同样的另外一个时区的人提交数据的时间也需要额外的转换工作，这样既增加了冗余，同时增加了整体系统复杂度。</p><h1 id="操作系统的解决方案">操作系统的解决方案</h1><p>无论是 Windows、Linux 还是 macOS，他们也面临着相同的问题，所以操作系统的设置中都会有一项“日期和时区”配置，这样的配置也为所有应用程序所共用，包含浏览器，所以浏览器是自带时区支持的。</p><p>以 macOS 为例：</p><p><img src="https://i.loli.net/2021/07/08/sLWVEl4B5afiRIb.png" alt="Time zone" /></p><pre><code>sudo systemsetup -gettimezone&gt; Password:&gt; Time Zone: Asia/Shanghai</code></pre><h1 id="浏览器中的时区">浏览器中的时区</h1><p>浏览器通过 Date instance 的 <code>getTimezoneOffset()</code> 方法提供了获取当前环境中时区的能力，它返回了当前时区和 UTC 时区按分钟的偏移量，即当前时间加上偏移量，就是 0 时区的格林尼治时间，如果返回的偏移量是负数，则是东区时间，正数则是西区时间。</p><pre><code>const now = new Date();                // 获取当前时间now.getTimezoneOffset() / 60;          // 获取时区偏移量&gt; -8                                   // -8 即为东8区</code></pre><h1 id="时间序列化---date-serialization">时间序列化 - Date serialization</h1><p>序列化也叫对象字符串化，其实浏览器已经提供了一个简单方便的方法进行时间序列化，无需拼凑字符串，序列化后的数据可以除了在浏览器环境中，还可以在各类数据库中直接使用，它的名字叫做 <code>JSON.stringify()</code>。 ;-)</p><pre><code>JSON.stringify(now)&gt; &quot;&quot;2017-04-14T05:11:42.247Z&quot;&quot;</code></pre><p>其实序列化后的时间，会被转换成格林尼治时间。</p><p>这样的字符串可以直接以 Date 类型保存到数据库中，取出来的时候，使用 <code>JSON.stringify()</code> 进行序列化，自动就会转换成这样的日期格式，而不用再手工处理。</p><p>以 Sequelize 为例：</p><pre><code>const aModel = require('./models/im-a-model');aModel.default.findOne().then(model =&gt; console.log(JSON.stringify(model.dataValues, null, 2)));&gt; {&gt;   &quot;id&quot;: 1,&gt;   &quot;name&quot;: &quot;某某某&quot;,&gt;   &quot;createdAt&quot;: &quot;2017-02-14T19:32:15.000Z&quot;,&gt;   &quot;updatedAt&quot;: &quot;2017-04-06T18:08:53.000Z&quot;&gt; }</code></pre><h1 id="时间反序列化---date-deserialization">时间反序列化 - Date deserialization</h1><p>将字符串恢复为对象，即为反序列化，常规我们在将 JSON 对象反序列化时，最常用的还是 <code>JSON.parse()</code> 方法，同样的 Date 也可以这么做。</p><pre><code>var dateStr = JSON.stringify(now);JSON.parse(dateStr);&gt; &quot;2017-04-14T05:11:42.247Z&quot;</code></pre><p>不过此时它还是个字符串，因为 JSON 序列化成字符串后已经丢失了对象的原始信息，要将该日期重新赋值到 Date 对象中才可以更加方便地使用。</p><pre><code>new Date(JSON.parse(dateStr))&gt; Fri Apr 14 2017 13:11:42 GMT+0800 (CST)</code></pre><p>我们可以看到时间已经从格林尼治时间恢复为东8区的北京时间了。</p><h1 id="结论">结论</h1><p>以上代码已经简单地说明了时间对象的序列化、保存、反序列化的步骤，希望能对应用开发带来一些帮助。</p><p>在做开发过程中，通过原始的对象进行操作带来的便利性比拼凑字符串要强很多，原始的数据或者对象也很方便进行二次加工，这是浏览器自身就具备的能力。</p><h1 id="题外话---时间对象的操作">题外话 - 时间对象的操作</h1><p>说到时间对象，其实 Javascript 本身的能力是很弱的，即使是将日期对象转换为一个人类可阅读的、指定格式的字符串都很不容易，遇到这样问题的开发者并不只有我们，所以有人开发了一个很强大的时间日期处理库 - <a href="http://momentjs.com/">moment.js</a>，同样是将日期对象转换为字符串，它的 <code>format()</code> 方法异常强大，例如：</p><pre><code>moment(now).format('YYYY-MM-DD HH:mm:ss');&gt; &quot;2017-04-14 13:11:42&quot;</code></pre><p>它同样提供了增减时间、两个时间判断大小等等一系列功能，moment 对象在使用时比 Date 更加方便。</p><p>目前它已经和 lodash 一起成为了前端项目开发的基础库之一了，更多功能等待你的发掘。</p>]]>
                    </description>
                    <pubDate>Fri, 14 Apr 2017 14:04:50 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[浏览器原生能力系列 - 上传文件类型检测]]>
                    </title>
                    <link>https://kxq.io/archives/浏览器原生能力系列-上传文件类型检测</link>
                    <description>
                            <![CDATA[<p><em>突然想写一些关于浏览器原生能力的文章，这些能力不但能减少代码冗余，从可靠性、扩展性、功能性上都要比自己定义的方法更加强大。</em></p><p>前端在上传文件时，往往会要求指定上传的文件类型，可能是图片、可能是视频、可能是一个 Word 文件，依靠文件扩展名只能进行简单校验，但是如果碰上文件没有扩展名，或者扩展名与文件的实际类型不一致的情况，后果可能是后期服务器端处理时发生问题。</p><p>所以需要一个更加靠谱的方案，不是依靠文件扩展名，而是实际的文件内容进行类型判断。</p><h1 id="linuxmacunix-中检查文件类型的方法---没兴趣的同学可以略过此章节">Linux/Mac/Unix 中检查文件类型的方法 - 没兴趣的同学可以略过此章节。</h1><p>通过扩展名进行类型判断准确而言应该是从 Windows 操作系统的传统，但类 Unix 操作系统不依赖文件扩展名的特性（很多文件根本没有扩展名，最典型的就是 /bin 下面的可执行程序），所以它们最需要文件类型推断功能。</p><p>提供此功能的命令叫做 <a href="http://www.darwinsys.com/file/">file</a>，它已经被包含在大多数 Unix 系统中，基本的使用方法是直接使用 <code>file [文件名]</code> 即可显示出它的文件类型，例如：</p><pre><code>$ file logo.pnglogo.png: PNG image data, 556 x 36, 8-bit/color RGBA, non-interlaced$ cp logo.png logo-real-png.gif # 复制一下改一下扩展名$ file logo-real-png.gif # 继续探测，可以发现检测出的文件类型不变，logo-2.png: PNG image data, 556 x 36, 8-bit/color RGBA, non-interlaced$ file test.txttest.txt: UTF-8 Unicode (with BOM) text</code></pre><p><code>file</code> 就是传说中不依靠扩展名直接对文件内容进行推断的命令，基本原理是文件内容中的特征码进行检测，具体方法后面再说。</p><h1 id="文件类型的标准---mime-types">文件类型的标准 - Mime Types</h1><p>在前端开发中应该能接触到 Mime Types 这个概念，它是文件类型的一个标准，通过将文件进行分组归类的方式，可以很轻松地识别该文件属于什么类型、具体是什么格式，很常见的有 <code>text/html</code> - HTML 文本、<code>text/plain</code> - 纯文本、<code>image/jpeg</code> - JPEG 图片、<code>image/png</code> - PNG 图片等等，text 就是文本类型分类，而 image 是图片类型分类，后面的就是具体格式。</p><p>具体的请参考 <a href="https://en.wikipedia.org/wiki/Media_type#mime.types">mime.types - Wikipedia</a> 在此对它的概念不多说。</p><p>同样的，<code>file</code> 命令也能够输出 Mime Type，参数是 <code>--mime</code>，依然不依靠扩展名，而直接检查文件内容，在输出 Mime Type 时还会把字符集显示出来。</p><pre><code>$ file --mime logo.png logo.png: image/png; charset=binary$ file --mime logo-real-png.giflogo.gif: image/png; charset=binary$ file test.txttest.txt: text/plain; charset=utf-8</code></pre><p>完整的 Mime Types 类型如果是 Linux 系统可以去 /etc/mime.types 下找到。</p><h1 id="通过-javascript-简单地获取-mime-type">通过 Javascript 简单地获取 Mime Type</h1><p>做前端开发时，很有可能会通过限制扩展名来进行可上传文件约束，这可以通过 <code>input</code> tag 的 <code>accept</code> attribute 进行限制，但在某些情况下可能无需限制，但需要知道上传文件类型，常规做法是做一个扩展名的类型分类，根据扩展名进行分组，但其实绕了弯路。</p><p>Web 引擎早已经提供了基于 Mime Types 的类型了，范例代码非常简单。</p><pre><code>var input = document.createElement('input');input.type = 'file';input.multiple = true;input.addEventListener('change', evt =&gt; [...evt.currentTarget.files].forEach(f =&gt; console.log(`${f.name} type is ${f.type}`)));</code></pre><p>关键就在于 input.files 这个 FileList 对象中的每个 file，它是一个 File 对象，会有一个 type property，记录了该文件的 Mime Type，这个范例代码可以一次选中多个文件，多个文件的 Mime Type 都能直接输出，这是几个测试文件后的输出结果，依然有刚才被重命名的 gif 实际为 png 的图片：</p><pre><code>&gt; input.click()logo.gif type is image/giflogo-real-png.gif type is image/giflogo.png type is image/png</code></pre><p>** 但是，这里有个问题 - 浏览器的文件类型推断依然是基于扩展名的，并不能做到重命名扩展名后，对文件内容的推断，logo-real-png.gif 本来应该是 png 但是却识别成了 gif。**</p><p>我查了一下，未来浏览器会实现这个功能的，但是这个标准名为  <a href="https://mimesniff.spec.whatwg.org/">《MIME Sniffing》</a>，今年3月才刚刚更新，各大浏览器还不能支持。</p><p>所以需要手工来进行检测了。</p><h1 id="文件特征码">文件特征码</h1><p>以前了解过杀毒软件的，可能会知道计算机病毒都有特征编码，这些编码标志着该段代码可能会干点奇奇怪怪的事情，让你的电脑不正常。</p><p>同样的，每一种文件类型也都有自己的编码，比较好识别的是文本类型的文件，只要所有字符集都在 Unicode/Ascii 编码范围之内，肯定就是文本类型了，如果有兴趣打开一个 zip 包，或者 Java 编译后的 jar 文件，可以看到开头一定是一个 <code>PK</code>，PK 是 <a href="https://en.wikipedia.org/wiki/Phil_Katz">Phil Katz</a> 的名字缩写，那是另一个有趣的故事了。</p><p><img src="http://km.oa.com/files/photos/pictures/201704/1491373750_89_w728_h65.png" alt="A zip" /></p><p>但是，并不是每个 PK 开头的都是 zip 包，后面还会有一些其它二进制编码来告诉计算机，它是某种类型的文件。</p><p>只需要知道，每种文件类型，都会有不同的特征编码，那就够了。</p><h1 id="通过-javascript-来实现基于文件内容的类型检测">通过 Javascript 来实现基于文件内容的类型检测</h1><p>这里需要用到 <code>FileReader</code> 方法，它可以直接通过浏览器中将文件以 ArrayBuffer 形式读入内存，通过 Javascript 进行处理。</p><p>先贴完整代码：</p><pre><code>var input = document.createElement('input');input.type = 'file';input.multiple = true;function detectFileType(file) {  // Fallback when browser was not supported FileReader.  if (window.FileReader === void 0) {    return console.log(`${file.name} type is: ${type}`);  }  // Start detecting  var fileReader = new FileReader();  fileReader.onload = function(e) {    var arr = (new Uint8Array(e.target.result)).subarray(0, 4);    var header = &quot;&quot;;    for(var i = 0; i &lt; arr.length; i++) {      header += arr[i].toString(16);    }    // Check the file signature against known types    switch (header) {      case &quot;89504e47&quot;: {        type = &quot;image/png&quot;;        break;      }      case &quot;47494638&quot;: {        type = &quot;image/gif&quot;;        break;      }      case &quot;ffd8ffe0&quot;:      case &quot;ffd8ffe1&quot;:      case &quot;ffd8ffe2&quot;: {        type = &quot;image/jpeg&quot;;        break;      }      default: {        type = &quot;unknown&quot;; // Or you can use the blob.type as fallback        return console.error(`Unkonwn ${file.name} type header: ${header}`);      }    }    console.log(`${file.name} type is: ${type}`);  };  fileReader.readAsArrayBuffer(file);}input.addEventListener('change', evt =&gt; [...evt.currentTarget.files].forEach(detectFileType));</code></pre><p>执行结果如下，这一下文件类型都被正确检测出来了：</p><pre><code>&gt; input.click()logo.gif type is: image/giflogo-real-png.gif type is: image/pnglogo.png type is: image/png</code></pre><p>解释一下：</p><p>FileReader 的 onload 方法可以在文件加载完成之后进行一些处理，同样的它是异步的，可能随着文件大小不一致执行顺序有所差异。</p><p>文件加载，首先将文件通过 Uint8Array 转换一些数组类型，然后通过 subarray 截取前 4 个 item，即为文件头，然后通过 toString(16) 转换为 16 进制的字符串。</p><p>这段代码只能检测 jpeg、gif 和 png 三种图片类型，除此以外的都进入 unkown 类型分支，因为图片类型的特征码都在文件头部前几个字节就能获取具体文件类型，相对比较好打比方，对于更加复杂的情况或者实现 <code>file</code> 的能力，这是远远不够的。</p><h1 id="万能的-npm">万能的 npm</h1><p>npm 上有一个比较复杂的文件类型检测实现 <a href="https://github.com/sindresorhus/file-type">file-type</a> 可以参考一下，它通过文件的前 4100 个字节来进行类型判断，可以判断出的文件类型种类也更加丰富。</p>]]>
                    </description>
                    <pubDate>Wed, 05 Apr 2017 16:37:27 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[一个简单的对数据有效性进行验证的办法。]]>
                    </title>
                    <link>https://kxq.io/archives/一个简单的对数据有效性进行验证的办法</link>
                    <description>
                            <![CDATA[<p>最近在一个项目中跟某金融行业的公司有些合作，他们除了要求通讯使用 HTTPS  之外，还对数据的绝对安全性有一定要求，安全性要求有两方面：</p><ol><li>数据本身不要求特别的加密措施，只是必须确认提交数据的真实性。</li><li>数据来往的双方服务器身份必须能够得到有效识别（https 依然有中间人攻击风险）。</li><li>单个请求的数据在特定时间（目前暂定10秒）之后，就必须失效。</li></ol><h1 id="背景知识">背景知识</h1><ul><li>非对称加密</li></ul><p>对称加密指的是双方知道密码就能够解开密文，得到真实信息。而与之对应的非对称加密，即使知道密码也未必能解开密文，它由两部分组成，一部分是用于加密数据的公钥，另外一部分是用于解密数据的私钥，加密数据的公钥可以随意传播给任何一个人，但是解密数据的私钥只能由所有者持有，别人使用公钥加密的数据，只能由所有者通过私钥解密。</p><ul><li>数据散列（hash）</li></ul><p>将一段任意长度的内容，经过某种算法，转换成一个固定长度的字符串，但是这种转换是有损的，转换后的字符串没有办法再转回原始内容，这种算法有一个特征是内容中发生任意的细微改变，都将导致结果的巨大不同，通常用于验证数据的真实性。</p><h1 id="实现方案">实现方案</h1><p>现有业务可以完全不动，最终决定在数据上增加一个签名字段，该签名整合了对方的名称、当前时间戳、和使用 SHA1 256 算法生成的消息内容散列数据，还有一个增加破译复杂度的盐，然后用 RSA 非对称算法进行签名。</p><p>具体代码如下，只使用了 openssl 生成公私钥，以及 node 的 crypto 模块：</p><pre><code class="language-javascript">#!/bin/env nodeconst crypto = require('crypto');// 私钥内容，用于解密签名// 使用 openssl genrsa -out rsa.private 1024 生成const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----MIICXAIBAAKBgQDdAwx2Srt4i9363PL+RK6o/LF58cbAwX9+IVoQGmBvf8jDJZqACkOBwdRo0LSeTQRBhe5HuuFS3VpatFIoghq069RfeQfOCxH+qzNqYQ+nI5Er3+FK9rZFqfbWjZt7GxSfCjZkcikC1z2mgetleUMow0Pi7SuV0PadbqYt4cC16QIDAQABAoGBAKWGEgBKKiu3PRIUBp0uXU1Mq7Lzw/I7OTwCyIwE5TK8lmSpNhQtG7ADtgymOo/QiJ52KyZnrTe9dl02bc3O2yY+lG1gFeP52zYq0dcAucJqHwyIMPhPsxANgZHXw+8Y3F5L124MWnZ1YOL9ckNDa7sni8OqZNKgQtu8fAmdTPQBAkEA8xtK571bZ5XO3HTqOcbGFy3xe+MHndG24QgNdTeedX1zNOnfVtSOEQa53AK9ImKjMZBd3VlXq+2oGMNa2gWYaQJBAOi7xZg560iDPk/qPscJRgDTyfibAFaV5YNXnPgV4iPWlmn8/KvrvLmd79fyTYaps8SZ65Rz7DjQwSQBMyXSgYECQDDB0IwZ1jM4QHzGlhNwYlpTxJLsPaLRZLRNQSW5Ofamamy6Wyi3CKcxiiUuB3DWB5TxN2IlgQfiakxNIfOIG8ECQHVTzD583Hd26qABGFrg+vCJ1KVHBvmfodAACDstVQ76LGQMTRkiw8bTr0kvdxPvU5hGfHQfqLPP0b6j+DQWFoECQCKLSQIGoJIJ+BQPcWpsO7T4zJxSd2gbvMIsKydtn52qc44/dX59oXkDt2gvDSrGY4Lj/RswNK652ymBIfgKfkI=-----END RSA PRIVATE KEY-----`;// 公钥内容，用于加密签名// 使用 openssl rsa -in rsa.private -pubout -out rsa.pub 生成const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdAwx2Srt4i9363PL+RK6o/LF58cbAwX9+IVoQGmBvf8jDJZqACkOBwdRo0LSeTQRBhe5HuuFS3VpatFIoghq069RfeQfOCxH+qzNqYQ+nI5Er3+FK9rZFqfbWjZt7GxSfCjZkcikC1z2mgetleUMow0Pi7SuV0PadbqYt4cC16QIDAQAB-----END PUBLIC KEY-----`;// 加密的信息const MESSAGE = { message: 'Hello World' };// 信息散列方式const MESSAGE_HASH_TYPE = 'sha256';// 应用名称，长度不定，双方知道即可const APP_NAME = 'testApp';// Salt，盐，双方约定增加破解难度的随机数，为 32 字节长度const SALT = 'd41d8cd98f00b204e9800998ecf8427e';// 签名超时时间，按秒计算，const TIMEOUT = 10;// 签名类型const SIGNATURE_TYPE = 'RSA-SHA256';// 签名输出类型// see https://nodejs.org/dist/latest-v7.x/docs/api/crypto.html#crypto_sign_sign_private_key_output_formatconst SIGNATURE_OUTPUT_TYPE = 'base64';/** * 签名函数 *  * @param {privateKey} string 加密用私钥 * @param {appName} string 加密内容 * @param {salt} string 盐 * @param {message} string | object | array 消息内容 * @returns string 加密后的签名字符串 */function sign(privateKey, appName, salt, message = '') {  // 当前时间的 unix 时间戳  const timestamp = parseInt(new Date().getTime()/1000);  // 生成信息散列  const json = JSON.stringify(message);  const hash = crypto.createHash(MESSAGE_HASH_TYPE);  hash.update(json);  const messageHash = hash.digest('hex');  // 生成加密的字符串  const signStr = appName + timestamp + salt + messageHash;  console.log(`String will sign is - ${signStr}`);  // 加密内容  const signer = crypto.createSign(SIGNATURE_TYPE); // 默认通过 RSA-SHA256 对称算法进行加密  signer.update(signStr);  return signer.sign(privateKey, SIGNATURE_OUTPUT_TYPE)}/** * 签名验证函数 * 原理是根据加密后的签名，倒推10秒钟，其中一次的签名如果对上了，就算验证成功。 * 中途最多可能需要生成 10 次加密后的签名，进行对比。 *  * @param {publicKey} string 解密用公钥 * @param {privateKey} string 生成加密用私钥 * @param {appName} string 应用名称 * @param {salt} string 盐 * @param {signature} string 用于验证的签名 * @param {message} string | object | array 消息内容 * @returns boolean 成功为真，失败为假 */function validate(publicKey, privateKey, appName, salt, signature, message) {  // 获取当前时间戳  const now = parseInt(new Date().getTime()/1000);  // 生成信息散列  const json = JSON.stringify(message);  const hash = crypto.createHash(MESSAGE_HASH_TYPE);  hash.update(json);  const messageHash = hash.digest('hex');  // 从当前时间戳往前推 10 秒，检查签名字符串是否合法  let count = 0;  let timestamp = now;  while(count &lt; TIMEOUT) {    const signStr = appName + timestamp + salt + messageHash;    // 生成签名    const signer = crypto.createSign(SIGNATURE_TYPE);    signer.update(signStr);    const newSignature = signer.sign(privateKey, SIGNATURE_OUTPUT_TYPE);    // 首先检查签名是否正确    const verifier = crypto.createVerify(SIGNATURE_TYPE);    verifier.update(signStr);    const result = verifier.verify(publicKey, newSignature, SIGNATURE_OUTPUT_TYPE)    // 签名正确后，检查加密后内容是否一致    if (result) {      // Signature correct      if (newSignature === signature) {        // Content correct        return true      }    }    count += 1;    timestamp -=1;  }  return false}const signature = sign(PRIVATE_KEY, APP_NAME, SALT, MESSAGE);console.log(`Signature is - ${signature}`);// 2 秒内签名可用，应该返回 truesetTimeout(() =&gt; {  const result = validate(PUBLIC_KEY, PRIVATE_KEY, APP_NAME, SALT, signature, MESSAGE);  console.log(`Validate should be correct in 2 seconds - ${result}`);}, 2000);// 10 秒后签名失效，应该返回 falsesetTimeout(() =&gt; {  const result = validate(PUBLIC_KEY, PRIVATE_KEY, APP_NAME, SALT, signature, MESSAGE);  console.log(`Validate should be incorrect after 10 seconds - ${result}`);}, 10000);</code></pre><p>执行结果如下：</p><pre><code class="language-shell">$ node ./sign.js String will sign is - testApp1484759320d41d8cd98f00b204e9800998ecf8427ea5759911f3c834348667ca417f6c8bb4484b58f9af27e91e7c95b208e43761faSignature is - uDQgfC2oy+4vndp5/PZA+6vnp4q6iicTFIHDVh6gJZ5Ufkd0/c75Oj3xK/jcKthAOkbNnd3A+CcVzo6aFdKmtPAmsR71ZgoVpJ+5g+kAzz5Gruzt499i9IYxhTQKK5Vo2/swF02n6ShOwWvwVeERUfzzYT9AIuUNhmQJD3+egm8=Validate should be correct in 2 seconds - trueValidate should be incorrect after 10 seconds - false</code></pre><p>可以看到该文本被序列化成 <code>testApp1484759320d41d8cd98f00b...</code> 这样的字符串，进而加密成了一串不可识别的代码，接收方拿到这串代码之后即可验证数据的真实性，当超时时则验证失效，这样做还有个好处是将数据失效的时间交由接收方决定，消息发送方只需要增加签名即可。</p><p>这段代码只是阐述一个思考过程，并未充分优化，其实可以异步化将每次验证过程放入消息队列中执行。</p><p>欢迎交流。;-)</p>]]>
                    </description>
                    <pubDate>Thu, 19 Jan 2017 01:19:43 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Typescript 是如何保证前端质量的]]>
                    </title>
                    <link>https://kxq.io/archives/typescript是如何保证前端质量的</link>
                    <description>
                            <![CDATA[<p>Typescript 是微软于 2014 年发布的基于 Javascript 的超集，和 Babel 将 ES6 语法编译成 ES5 一样，Typescript 也会把 TS 的语法编译成从各种目标代码（一会儿继续说这事儿）。</p><h2 id="开发目标">开发目标</h2><p>我们很清楚 ES6 只是 ES5 的扩展，尽管 Chrome 等浏览器已经率先实现了部分 ES6 功能，但依然需要通过 Babel 进行编译，才能对旧版的浏览器提供支持，其实我个人觉得它除了解决部分开发效率，对于 Javascript 弱类型的实质没有任何改进，从产品质量保证而言，Babel 提供了编译时的语法检查，但是能力仅限于检查未定义变量，而浏览器中直接运行的 ES6 语法，和 Javascript 一样是纯粹的动态语言，最基本的检查能力都不具备。</p><p>回到 2014 年，那是个 ES6 语法还未成型的年代，当时有句话叫做“<strong>动态语言一时爽，重构时候火葬场</strong>”，各大厂商已经认识到了 Javascript 的动态特性无法支撑大型项目的开发，纷纷提出了自己的解决方案，例如 <a href="https://www.dartlang.org">Google Dart</a>、<a href="https://flowtype.org">Facebook flow.js</a> 以及本文要介绍的 <a href="http://www.typescriptlang.org">Microsoft Typescript</a>。</p><p>笔者认为，Typescript 是最合适的解决方案，它很简单地为 Javascript 赋予了单个对象赋予了类型、对象赋予了 interface、为目前现有的 Javascript 库赋予了 Declaration File 使他们全部都获得了静态的类型系统，与 ES6 语法基本兼容，比重新设计整个语言的 Dart 更轻，但比 flow.js 更重，配合官方免费的、跨平台的 <a href="https://code.visualstudio.com">VisualStudio Code</a> 更是将整个开发生态打造得无可挑剔（另外说一句这个编辑器也是 Typescript 开发，基于 Electron 的 Web 应用）。</p><h2 id="一个简单的范例">一个简单的范例</h2><p><img src="http://km.oa.com/files/photos/pictures/201612/1481077373_88_w814_h544.png" alt="ts-node" /></p><p>我们可以通过 <code>tnpm install -g ts-node</code> 来体验 typescript，范例代码是一个很常见的场景，做数据运算的时候，经常会有数据类型不对的情况，Typescript 对于直接的数据操作并没有类型检查，但当生成一个函数，并且对参数赋予类型时，便会在编译时进行类型检查，对于不符合类型要求的地方，会直接抛出错误，中止编译过程，同时我们还可以看到，它对 Javascript 内置的函数都已经做了基本的类型声明，<code>parseInt(value)</code> 后会是一个 number，符合了函数的入参类型要求，便正确输出返回值。</p><p>是否有一种 Java 的既视感？通过静态类型声明，就具备了和 Java 一样的开发大型应用的能力，</p><h2 id="基本配置">基本配置</h2><p>Typescript 比较好的地方是，编译器本身只有 <code>typescript</code> 一个包，通过 <code>tnpm install -g typscript</code> 将会安装 v2.0.10 稳定版（截止发稿时），安装之后，系统中将会多出一个 <code>tsc</code> 命令，它是 Typescript 的编译器。</p><p>可以写一个很简单的代码，进行编译测试。</p><pre><code>let value: string;value = 'Hello world';const printStr = (str: string) =&gt; {  console.log(str);}printStr(value);</code></pre><p>保存为 <code>helloworld.ts</code>，然后直接执行 <code>tsc helloworld.ts</code>，将会输出成默认的 ES3 javascript</p><pre><code>var value;value = 'Hello world';var printStr = function (str) {  console.log(str);};printStr(value);</code></pre><p>Typescript 是具备直接输出 ES6 能力的，只需要在编译时加上 <code>-t es6</code> 参数，便可以输出 ES6 的目标文件，从输出的 js 文件和 ts 文件对比，就会发现 ts 只是比 js 多了个参数类型定义。</p><pre><code>let value;value = 'Hello world';const printStr = (str) =&gt; {  console.log(str);};printStr(value);</code></pre><p>编译参数可以直接在命令行后面加上，更多参数可以参考<a href="http://www.typescriptlang.org/docs/handbook/compiler-options.html">编译选项</a>，也可以通过 <code>tsconfig.json</code> 直接定义，首先可以使用 <code>tsc --init</code> 生成初始化的配置文件，我这里加了 <code>files</code> 用于定义输入的源代码。</p><pre><code>{  &quot;compilerOptions&quot;: {    &quot;module&quot;: &quot;commonjs&quot;,    &quot;target&quot;: &quot;es5&quot;,    &quot;noImplicitAny&quot;: false,    &quot;sourceMap&quot;: false  },  &quot;files&quot;: [    &quot;helloworld.ts&quot;  ]}</code></pre><p>然后直接使用 <code>tsc</code> 就可以进行编译了，更多编译参数，请参考 <a href="http://www.typescriptlang.org/docs/handbook/tsconfig-json.html">tsconfig.json 文档</a></p><p>需要特别说明的是以下几个参数</p><table><thead><tr><th align="left">参数名称</th><th align="left">可选值</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">target</td><td align="left">ES3、ES5、ES2015</td><td align="left">输出的 Javascript 代码类型，默认是 ES3，如果只兼容到 IE9 以上，用 ES5 即可，Typescript 生成的代码质量已经很高了，以前还需要用 Babel 做后端再二次编译一次，现在完全无需  Babel。</td></tr><tr><td align="left">module</td><td align="left">none、commonjs 、amd 、system、umd 、es2015</td><td align="left">输出的模块系统，可以看到支持得很全面，最新式的 system.js 也支持到了，需要说明的是，有一个 <code>--outFile</code> 参数可以讲所有代码打包成一个文件（可以代替 webpack），但是当使用它打包时，只能使用 <code>amd</code> 或者 <code>system</code> 这种支持异步加载的模块类型。</td></tr><tr><td align="left">jsx</td><td align="left">React、Preserve</td><td align="left">处理 jsx 的方案、<code>React</code> 可以讲所有 jsx 编译成 React.createElement 的形式，<code>Preserve</code> 则保留原有格式</td></tr></tbody></table><h3 id="配合-webpack">配合 webpack</h3><p>Typescript + Webpack 使用非常简单，和 Babel 非常类似，只需要加上 <a href="https://github.com/TypeStrong/ts-loader">ts-loader</a> 或者 <a href="https://github.com/s-panferov/awesome-typescript-loader">awesome-typescript-loader</a> 这两个 loader 各有千秋，其实目前 Typescript 直出 ES5 已经非常成熟，用 ts-loader 即可，如果有需要使用 Babel 进行 ES6 到 ES3 编译的可以使用 awesome-typescript-loader 据说有更好的性能和特性。</p><p>这里有一份 <a href="https://github.com/xuqingkuang/react-redux-boilerplate/blob/master/webpack.config.js">webpack 范例配置文件</a>。</p><h3 id="语法-linter">语法 Linter</h3><p>Linter 的作用是保证多人开发时的语法的一致性，它可以在编译前进行语法检查，找出不合规的地方，并给出 Warning，这些不合规的地方未必会影响代码运行结果，但是当多人开发时，保持一致的代码风格还是很有必要的。</p><p>和 Javascript 一样，Typescript 也有 linter，叫做 <a href="https://github.com/palantir/tslint">tslint</a>，它提供了语法检查和开发指导。</p><p>使用 <code>tnpm install -g tslint</code> 之后，会增加 <code>tslint</code> 命令，可以使用 <code>tslint --init</code> 生成 tslint 的默认配置文件，我们用它来检查一下刚出的 <code>helloworld.ts</code>。</p><pre><code>» tslint helloworld.ts helloworld.ts[2, 9]: ' should be &quot;helloworld.ts[5, 2]: Missing semicolon</code></pre><p>很明显，它提示了第二行的单引号需要改为双引号，同时第五行少了一个分号。</p><p>实际开发之中是不会使用默认的宽松配置的，tslint 已经提供了大量参考配置，我们一般使用“推荐”配置，可以参考 <a href="https://github.com/xuqingkuang/react-redux-boilerplate/blob/master/tslint.json">tslint.json</a> 它从代码的考虑已经做了大量优化，可以作为项目中的推荐方案。</p><h2 id="语法简介">语法简介</h2><p>Typescript 语法与 ES6 语法基本一致，const、let 箭头函数可以直接使用，比较出色的地方是它不需要增加插件便可以实现一些高级语法编译，例如 async 和 await，相对于 Babel 我感觉 Typescript 编译出的代码更佳简单干净，可读性高。</p><p>和 ES6 不一样的地方，是它增加了类型系统，这又主要分以下几种类型定义方式。</p><h3 id="变量类型系统">变量类型系统</h3><p>在 Typescript 中，声明变量时如果直接赋值，则会使用自动类型判断固定该变量的类型，例如：</p><pre><code>const foo = 123; // numberconst bar = 'abc'; // string</code></pre><p>如果需要声明一个变量，但不赋值，就必须给它声明一个类型，当后期使用类型不符合时会抛出错误。</p><pre><code>let foo: number;foo = 123; // Okfoo = 'abc'; // Type 'string' is not assignable to type 'number'. (2322)</code></pre><p>Typescript 的基本类型主要有：</p><table><thead><tr><th align="left">类型</th><th align="left">范例</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">boolean</td><td align="left">let isDone: boolean = false;</td><td align="left">布尔值，只允许 true、false</td></tr><tr><td align="left">number</td><td align="left">let decimal: number = 6;</td><td align="left">数字类型，包含正数、浮点数和16进制数</td></tr><tr><td align="left">string</td><td align="left">let color: string = &quot;blue&quot;;</td><td align="left">字符串</td></tr><tr><td align="left">Array</td><td align="left">let list: number[] = [1, 2, 3];</td><td align="left">数组，需要说明的是数据也需要声明内容的类型，写法就是内容类型加上 <code>[]</code></td></tr><tr><td align="left">Tuple</td><td align="left">let x: [string, number];</td><td align="left">强制数组内的元素类型</td></tr><tr><td align="left">Enum</td><td align="left">enum Color {Red, Green, Blue};</td><td align="left">枚举类型</td></tr><tr><td align="left">Any</td><td align="left">let notSure: any = 4;</td><td align="left">不限制变量类型，如果使用它就是普通的 JS，应该去声明每一个变量的类型，避免使用 any。</td></tr><tr><td align="left">Void</td><td align="left">let unusable: void = undefined;</td><td align="left">空值，只允许赋予 null 和 undefined</td></tr><tr><td align="left">Null and Undefined</td><td align="left">let u: undefined = undefined;</td><td align="left">比 void 更佳详细，null 或者 undefined 只可选其一</td></tr><tr><td align="left">Never</td><td align="left"> </td><td align="left">主要用于声明一个无返回值的函数</td></tr></tbody></table><p>更多范例，请参考官方<a href="http://www.typescriptlang.org/docs/handbook/basic-types.html">基础类型</a>文档。</p><h3 id="对象--object">对象  Object</h3><p>对于 Object 的类型定义，主要通过 <code>interface</code> 关键字进行定义，和基本变量定义类似，<code>interface</code> 也非常简单。</p><pre><code>interface IHelloWorld {  hello: string;  world: number;  foobar?: void; // Optional property}const helloWorld: IHelloWorld {  hello: 'Hello',  world: 123,}console.log(JSON.stringify(helloWorld));helloWorld.foobar = null;console.log(JSON.stringify(helloWorld));helloWorld.test = 123;console.log(JSON.stringify(helloWorld));</code></pre><p>直接使用 ts-node 运行会发现编译不过，抛出了错误</p><pre><code>test.ts(17,12): error TS2339: Property 'test' does not exist on type 'IHelloWorld'.</code></pre><p>是因为在最后我们给 helloWorld 赋予了一个 interface <code>IHelloWorld</code> 中不存在的 <code>test</code> property，把它删掉就可以正常编译运行了，由此可见 Typescript 的严谨。</p><h3 id="类-property-类型声明方法私有性声明">类 property 类型声明、方法私有性声明</h3><p>和 ES6 一样，Typescript 也提供了 <code>class</code> 关键字用于声明累，而 property 类型声明借鉴了初始化值的语法，直接在 <code>constructor()</code> 之上，像初始化变量一样进行类型赋予即可。</p><pre><code>class Foobar {  private str: string; // this.str type  public constructor(str) {    this.str = str;  }  public printStr() {    console.log(this.str);  }}const foo = new Foobar('Hello world');foo.printStr();</code></pre><p>这里还能对方法的私有性进行定义，当不慎掉用到 <code>private</code> 方法时，编译器就会报出错误阻止编译过程，有效保护私有方法。</p><h3 id="第三方库接口类型定义-declaration-file">第三方库接口类型定义 Declaration File</h3><p>Typescript 因为其特点，所以对第三方库提供的接口也有强类型的需求，但是老的第三方库往往都是使用 Javascript 进行开发，并没有声明接口类型，微软采用了一个取巧的办法，给第三方库增加了一个  <code>.d.ts</code> 的类型声明文件。</p><p>社区里已经有了绝大多数常用库的类型声明文件，保存在 <a href="https://github.com/DefinitelyTyped/DefinitelyTyped">DefinitelyTyped</a> 仓库里，可以直接使用 <code>tnpm</code> 的 <code>@types</code> private repo 进行安装，例如 <code>tnpm install @types/react-bootstrap</code> 安装 react-bootstrap</p><p>类型声明文件还有一个好处是它在声明类型的同时，还可以对函数的用法进行说明，这样开发起来不用查看源代码或者官方文档，在 IDE 里就能了解方法的功能。</p><p><img src="http://km.oa.com/files/photos/pictures/201612/1481091813_15_w598_h238.png" alt="" /></p><p>但遇到比较冷门的第三方库，没有 <code>d.ts</code> 文件提供时，直接 <code>import</code> 它会提示找不到 module，对于比较小的第三方库，建议自己用 Typescript 重写，也可以自己开发 <code>d.ts</code> 文件进行类型定义，Typescript 2.0 对 <code>d.ts</code> 文件进行了大量简化，具体开发内容有点大，可以参考 <a href="http://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html">官方文档</a>，未来或许会单独写一篇《Typescript Declaration File 开发指南》。</p><h2 id="成功案例">成功案例</h2><p>因为 Typescript 静态类型的特性，各大公司都在积极使用 Typescript 进行项目开发。</p><ol><li>Google 的 <a href="https://angular.io">Angular 2</a></li><li>蚂蚁金服的 <a href="https://github.com/ant-design/ant-design">Ant.design</a></li><li><a href="https://github.com/teambition/teambition-sdk">Teambition</a></li></ol><p>目前我们组已经在内部使用 Typescript 进行项目开发，目前主要成果有：</p><ol><li>vincenzheng 的<a href="https://github.com/vincent-zheng/wechat-small-app-todo-demo">微信小程序脚手架</a></li><li>xqkuang 的 <a href="https://github.com/xuqingkuang/react-redux-boilerplate">react-redux 脚手架</a></li><li>xqkuang 的 <a href="http://git.code.oa.com/xqkuang/mig-server">NodeJS 服务器框架</a>（进行中）</li><li>xqkuang 的<a href="http://git.code.oa.com/xqkuang/react-router-redux-mta/tree/master">腾讯指数统计埋点</a></li></ol>]]>
                    </description>
                    <pubDate>Thu, 19 Jan 2017 01:16:00 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[React、Redux 的单页应用在初始化时恢复状态]]>
                    </title>
                    <link>https://kxq.io/archives/reactredux的单页应用在初始化时恢复状态</link>
                    <description>
                            <![CDATA[<p>这是一个我思考了很久的问题 - 如何通过 URL 恢复 React 网页保存在 Redux 中的所有状态，目前正在开发的项目网页上有很多选项框，需要把 URL 给别人打开该 URL 时，所有选项框都必须恢复成之前的状态。</p><p>这个用 React + Redux 因为没有先例，而它又有自己内部的，不对外暴露的 Store，导致整个中秋节都在思考这个问题</p><p>先来看一下最终实现的效果（请忽略还没有开始写样式。。。）。</p><p><img src="https://i.loli.net/2021/07/08/9pglKAEPsQi3UdZ.gif" alt="1474192360_7_w885_h534.gif" /></p><h1 id="url-是什么">URL 是什么？</h1><p>一个典型的 URL: <a href="http://example.com/user/xqkuang/articles/add?kmref=km_header&amp;editor=md">http://example.com/user/xqkuang/articles/add?kmref=km_header&amp;editor=md</a> ，其中包含了：</p><ol><li>协议 Protocol- <code>http://</code> 协议</li><li>服务器域名 Host - <code>km.oa.com</code></li><li>访问网页路径，学名 <code>路由</code> Router - <code>/user/xqkuang/articles/add</code> -这是网页路由，还有一种单页应用专有的通过 hash <code>#</code> 符号开头确定的路由，因为单页应用只有一个 HTML 文件，当没办法做 nginx tryfile 的时候 hashHistory 也是个 workaround。</li><li>参数 Query，问号后面的字符串，用于给路由的网页确定展示的方式 - <code>?kmref=km_header&amp;editor=md</code></li></ol><p>其中记录状态，主要依靠 <code>路由</code> 确定当前显示哪个网页，<code>参数</code> 确定那个网页怎么进行展示。</p><h2 id="路由和参数如何设计">路由和参数如何设计？</h2><p>设计原则其实非常简单，一个网页从一开始就必须确定的内容，就放到路由里面，例如上面的例子，意思非常明显，就是 <code>/用户/xqkuang/文章/添加</code> 把主谓宾都准备妥当了，谁在哪儿要干什么都很清楚，这些参数都不能少（这个例子其实不是很好，<code>/user/xqkuang</code>可以通过 session 确定，而无需写到 URL 里），如果把这个网页拷贝给另外一个人打开，也会是相同的页面。</p><p>而可变的内容，就放到参数里，例如这里的 <code>&amp;编辑器=md</code> 就表明了要用 markdown 而不是 html 编辑器去写文章，如果这里发生变化，那网页的内容也会发生变化，它并不用去描述具体做什么，而是锦上添花似地说明怎么去做，如果没有这些参数，并不影响网页的具体行为和它要做的事情，网页也会以一个默认的参数去达到目标。</p><h1 id="服务器端网页和单页应用在处理路由和参数时的异同">服务器端网页和单页应用在处理路由和参数时的异同</h1><p>服务器端渲染处理路由和参数其实非常简单，因为服务器端直接承接来自浏览器完整的 HTTP 请求，整个 URL 都 直接暴露与 Web service 可见范围之内，服务器端网页拿到请求后，可以根据路由选择合适的 HTML 模板文件，根据参数去判断各种条件，读取数据库并且生成页面，是个很平滑的过程。</p><p>而单页应用不同，它从服务器那里获取获取的实际只有一个 Javascript 文件具备处理能力（该文件其实通常放在没有任何处理能力的 CDN 上），路由和参数都是由该 Javascript 进行处理，路由和参数虽然也都传递给了后端服务器，但其实后端根本不处理，对于单页应用的后端服务器而言，除了数据接口服务，它的功能只是生成一个引用了 CDN 上 Javascript 文件的 HTML 字符串给浏览器，Javascript 需要自行根据路由选择合适的前端模板，根据参数去生成 Ajax 数据请求，获取到数据后再在浏览器中显示。</p><p>还有一个很重要的不同点，和服务器渲染的网页不同，单页应用通常是通过 window.history.pushState() 方法做到无刷新切换页面的，虽然在页面中点击链接跳转，其实并没有发起新的网页请求到服务器，而全部被 Javascript 捕获，并进行内部根据链接地址进行页面重新渲染，所以每次 URL 的状态也需要随时进行更新。</p><h1 id="react--redux-方案处理路由需要的两个库">React + Redux 方案处理路由需要的两个库</h1><p>从这里开始就需要你有 React + Redux 开发经验了。。。</p><p>React 本身只是个 View，所以需要其它的库进行扩展，以方便在应用中使用路由、参数处理等功能。</p><h2 id="1-react-router---路由库">1. react-router - 路由库</h2><p><a href="https://github.com/ReactTraining/react-router">https://github.com/ReactTraining/react-router</a></p><p>这已经是 React 应用标准组件之一，截稿时最新稳定版本是 2.8.x，最新开发版本的 React router 4.0 alpha 将会有很大改变（事实上这个库每次升级看起来都像新的路由库），具体写法请参考官方文档，特别需要提到的是，它在 v2.4 之后提供了 <a href="https://github.com/ReactTraining/react-router/blob/caf468563112c34ffa213bd0620cbc6276e30277/docs/API.md#withroutercomponent-options">withRouter()</a> 的高阶组件，为 Component 增加了 props.router 方法， 可以通过它进行页面路由、参数的跳转切换，后面会继续提到它。</p><h2 id="2-react-router-redux---将-react-router-的状态映射到-redux-store-中">2. react-router-redux - 将 react-router 的状态映射到 redux store 中</h2><p><a href="https://github.com/reactjs/react-router-redux">https://github.com/reactjs/react-router-redux</a></p><p>这个库本来只是一个个人开发的小作品，但现在也被 Facebook 收入到 reactjs git 库之下，它的作用是将当前 URL 的路由和状态信息，跟 Store 进行同步，放到 props.routing 里，里面有类似 path、query 那样的属性，这样 Component 可以很方便的查询到当前的 URL 信息。</p><p><em>PS: React router 4.0 后将会提供一个单独的 props.location 来支持相同功能，这个库到时可能会被废弃。</em></p><h1 id="思考路程和-redux-的限制">思考路程和 Redux 的限制</h1><p>在 React Redux 应用中，所有的数据初始化、保存都是在 Redux 中的，经过特别细致的 Component 拆分，目前我所开发应用已经不再使用 React state，而全部使用 mapStateToProps() 方法把所有的 Redux state 转换为 React props 在 Component 间传递数据，这样整个结构非常简单，业务逻辑全部放到 Redux 中，通过 dispatch action 获取需要的数据后，触发 reducer 整理成 Component 所需要的数据，Component 唯一要做的事情，就是根据数据进行重新渲染，类似表格 Component 也是由 Reducer 直出表格行数据进行绘制，数据层与视图层完全分离，这样的优势无疑是很大的，Component 不但不再需要传统网页那样的操作 DOM，甚至不需要和未拆分完全 React 应用那样进行各种是否需要重新渲染的判断，而且特别方便进行单元测试。</p><p>Redux 的数据初始化，一般放在 reducer 中，它有两个参数：</p><ol><li><code>state</code> - 指向当前 Store 里的 state 信息，第一次 Store 为空时初始化 initialState 数据也在这里声明。</li><li><code>action</code> - dispatch 过来的 action 信息，或者是异步 action 返回来的数据。</li></ol><p>实际的业务流程是这样的，分两部分：</p><p>一、当网页打开时：</p><p><code>从 URL 获取参数</code> -&gt; <code>生成 Action 的初始化参数</code> -&gt; <code>根据初始化参数生成 ajax 参数</code> -&gt; <code>发起数据请求</code> -&gt; <code>异步回调将数据传递给 reducer</code> -&gt; <code>reducer 生成 Component 所需要的数据进行渲染</code></p><p>二、当在页面跳转或者选项框 Component 发生变化时:</p><p><code>选项框发生变化</code>-&gt; <code>生成新的参数 dispatch action</code> -&gt; <code>根据初始化参数生成 ajax 参数</code> -&gt; <code>发起数据请求</code> -&gt; <code>异步回调将数据传递给 reducer</code> -&gt; <code>reducer 生成 Component 所需要的数据进行渲染</code> -&gt; <code>更新 URL 中的参数字段</code></p><p>依然使用 Redux 去同步数据相对会让视图更新更加容易，因为需要通过它对数据进行修改触发重新渲染机制，但是这样的方案会出现有 Redux Store 和 URL Store 两个存储数据的地方。</p><p>最初的想法，是希望能找到一个办法，把 URL 参数 Query 和 Redux Store 里同步起来，但是 <a href="http://stackoverflow.com/questions/36596996/how-to-sync-redux-state-and-url-query-params">Dan 大神认为通过 React Router 进行跳转已经足够</a>。而且在实际的探索中，因为那时 LOCATION_CHANGE action 尚未触发，发现 react-router-redux 的 props.routing 并不能在页面加载完第一次初始化时使用，整个 props 都是空值。同时 reducer 的第一个 state 参数在应用初始化之后不可修改，这给最初的想法带来了很深的困扰。</p><h1 id="最终的解决方案">最终的解决方案</h1><p>既然不能在应用运行时修改 reducer 的 state 初始化参数，那就在应用初始化时获取一下 URL 里的参数，进行 state 参数初始化好了，虽然没有用上 react-router-redux 的 props.routing 进行参数获取，多余做了一步，但是起码能先解决问题。</p><h2 id="监听-history-变动">监听 history 变动</h2><p>首先，我这儿先做了一步，在入口代码中，监听 react-router 的 history 改变事件，将路由状态信息放到了 window 全局中（这个确实有点脏，不过路由信息我这儿别的地方也要用，放在这里方便一些）。</p><pre><code class="language-javascript">/* Initial history */export const history = syncHistoryWithStore(  reactRouter[config.historyBackend], store)// Binding location to window for reducer initialStatehistory.listen(location =&gt; {  window.reactLocation = location;});</code></pre><h2 id="应用初始化时从-url-获取参数">应用初始化时从 URL 获取参数</h2><p>然后在工具类中加入 getUrlQuery() 方法，用于获取指定的 URL 参数，还有一个 getUrlQueryTarget() 方法，用于从数据数组中提取指定的对象，我这儿的 action 都是传递对象，这样当 URL 参数不对时（比如被用户篡改了），也可以恢复成默认参数。</p><p>可以看到 getUrlQuery() 方法首先从刚才定义的 window.reactLocation 中获取参数，第一次初始化时它不存在，才去直接判断 window.location 中获取实际的参数值的，这里还用到了 qs 库进行 URL 参数解析。</p><pre><code class="language-javascript">import qs from 'qs';/* * Get url params */export const getUrlQuery = key =&gt; {  if (window.reactLocation) {    return window.reactLocation.query[key];  }  let queryStr = ''  if (config.historyBackend === 'hashHistory') {    queryStr = window.location.hash.split('?')[1];  } else if (config.historyBackend === 'browserHistory') {    queryStr = window.location.search.substring(1);  }  const query = qs.parse(queryStr);  return query[key]}/* * Get url query specific object */export const getUrlQueryTarget = (key, collection) =&gt; {  const query = getUrlQuery(key);  const targets = collection.filter(x =&gt; x.key === query);  if (!targets.length) {    console.error(`Target was not found for query key; ${key}`);    return null  }  return targets[0]}</code></pre><p>Reducer 初始化时需要通过 getUrlQueryTarget() 方法，从 URL 里获取初始化参数，并且监听 LOCATION_CHANGE action，当路由发生改变时，处理好 Component 需要的数据并且返回给 Component。</p><pre><code class="language-javascript">import { LOCATION_CHANGE } from 'react-router-redux';import { INTERNET_PLUS_KINDS } from '../constants';const kindInitialState = {  kinds: INTERNET_PLUS_KINDS,  kind: getUrlQueryTarget('kind', INTERNET_PLUS_KINDS) || INTERNET_PLUS_KINDS[0]}export const internetPlusKindReducer = (state = kindInitialState, action) =&gt; {  switch (action.type) {    case LOCATION_CHANGE: {      const kind = INTERNET_PLUS_KINDS.filter(        x =&gt; x.key === action.payload.query.kind      )[0];      return {        kinds: INTERNET_PLUS_KINDS,        kind: kind || INTERNET_PLUS_KINDS[0]      }    }    default:      return state  }}</code></pre><p>同样的，在另外一个负责 Ajax 请求的容器 Component 里，也需要 connect 到 internetPlusKindReducer，当接收到新的 kind 参数时，通过 componentWillReceiveProps(nextProps) 方法 dispatch action.</p><p>之所以把请求放在容器 Component 里，是因为这些组件都是共享一个来自 fetchInternetPlusProvincesData action 的数据，但是会经过 reducer 处理成各自不同需要的数据，所以触发放在容器里。所有子 Component 都无需从父 Container 的 props 里获取参数，而是对接各自的 Reducer 直接获取数据，这样其实就解耦了数据层和视图层。</p><pre><code class="language-javascript">import React, { Component, PropTypes } from 'react';import { connect } from 'react-redux'import Kind from './kind';import Content from './content';import { fetchInternetPlusProvincesData } from '../../actions/internet-plus';export class Container extends Component {  static propTypes = {    fetchInternetPlusProvincesData: PropTypes.func,    kind: PropTypes.object,  }  componentDidMount () {    const { fetchInternetPlusProvincesData, kind } = this.props;    fetchInternetPlusProvincesData({ kind });  }  componentWillReceiveProps (nextProps) {    const { fetchInternetPlusProvincesData } = this.props;const { kind } = nextProps;    fetchInternetPlusProvincesData({ kind });  }  render () {    return (  &lt;div&gt;        &lt;Kind className=&quot;internet-plus-kind&quot; /&gt;&lt;Content /&gt;  &lt;/div&gt;    );  }}// State to props for connect argumentexport const mapStateToProps = (state) =&gt; {  return {    kind: state.internetPlusKindlReducer.kind  };};// Dispatch to props for connect argumentconst mapDispatchToProps = {  fetchInternetPlusProvincesData};export default connect(mapStateToProps, mapDispatchToProps)(Container);</code></pre><h2 id="当选项框发生改变时直接改变路由参数">当选项框发生改变时，直接改变路由参数。</h2><p>流程和之前设计时发生了一点点变化，变成了直接改变 URL 参数，然后触发 LOCATION_CAHNGE，reducer 接收到之后整理数据，到容器 Component 触发 Ajax call，整理的数据再返回给容器和选项框组件。</p><p><code>选项框发生变化</code>-&gt; <code>直接改变路由参数触发 LOCATION_CHANGE</code> -&gt; <code>Reducer 接受到之后根据参数整理新的 Ajax 参数</code> -&gt; <code>发起数据请求</code> -&gt; <code>异步回调将数据传递给 reducer</code> -&gt; <code>reducer 生成 Component 所需要的数据进行渲染</code></p><pre><code class="language-javascript">import React, { Component, PropTypes } from 'react';import { connect } from 'react-redux'import { withRouter } from 'react-router';import { DropdownButton, MenuItem } from 'react-bootstrap';import { changeQuery } from '../../utils';export class InternetPlusChartKind extends Component {  static propTypes = {    router: PropTypes.object,    className: PropTypes.string,    kinds: PropTypes.array,    kind: PropTypes.object  }  changeKindHandle (eventKey) {    const { router } = this.props;    changeQuery(router, 'kind', eventKey);  }  render () {    const { className, kinds, kind } = this.props;    return (      &lt;div className={ className }&gt;        &lt;DropdownButton          id=&quot;internet-plus-kind&quot;          onSelect={ eventKey =&gt; { this.changeKindHandle(eventKey); } }          bsStyle={ 'primary' }          title={ kind.name }&gt;          {            kinds.map((d) =&gt; {              return (                &lt;MenuItem key={ d.key } eventKey={ d.key }&gt;  { d.name }&lt;/MenuItem&gt;              );            })          }        &lt;/DropdownButton&gt;      &lt;/div&gt;    );  }}// State to props for connect argumentexport const mapStateToProps = (state) =&gt; {  return {    kinds: state.internetPlusKindReducer.kinds,    kind: state.internetPlusKindReducer.kind  };};export default connect(mapStateToProps)(withRouter(InternetPlusChartKind));</code></pre><p>其实我有尝试过保留选项框组件的单独的 change kind action dispatching，不在 Reducer 中监听 LOCATION_CHANGE，但是发现那样会导致浏览器的前进后退按钮失效，最终改成了这样的方案。</p><p>这个方案简单、干脆、明了，以上都是核心代码，思路仅供参考，希望能给大家提供帮助。</p>]]>
                    </description>
                    <pubDate>Mon, 19 Sep 2016 13:57:00 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[现代前端开发背后的设计模式]]>
                    </title>
                    <link>https://kxq.io/archives/现代前端开发背后的设计模式</link>
                    <description>
                            <![CDATA[<p>本文内容来自于之前的一次技术分享，阐述了一些新的前端开发的设计模式，希望和前端开发同学交流一下，开阔思路，共同提高。作为初级文并没有牵扯太高深的东西，更多是一种解决问题的方法。</p><h1 id="现代前端开发的特点">现代前端开发的特点</h1><p>现代前端应用越来越复杂，越来越像桌面和移动端的原生应用，这就要求现代前端需要具备一些原生应用的能力，如以下：</p><ol><li>具备复杂交互的处理能力 - 什么是复杂的交互？当用户发生一次点击行为时，可能需要同时刷新多个界面元素的状态，在这种情况下，就要加强界面元素之间的通讯能力。</li><li>良好的工程化和团队协作开发能力 - 随着项目越来越大，代码复杂度增加，传统的前端开发三驾马车（HTML、CSS、Javascript）难以为继，如何组织架构好前端工程，促进团队协作就成了重点。</li><li>可被自动化测试 - 传统的前端开发是非常难以测试的，一是因为从开发模型上就没为自动化测试考虑过，但是项目复杂度的增加，导致版本迭代时回归性 Bug 越来越多，就需要有更好的方案来解决质量问题。</li></ol><h1 id="现代前端设计模式">现代前端设计模式</h1><h2 id="数据视图双向绑定-mvvm">数据视图双向绑定 MVVM</h2><h3 id="前因">前因</h3><p>传统的前端开发都是直接通过 Javascript 操作数据与 DOM，例如以下的代码：</p><pre><code class="language-javascript">$('#button').click(function() {  $.getJSON('http://url.com', function(res) {     $('#label').val(res.value);  });}</code></pre><p>短短五行覆盖了点击事件处理、线上数据抓取、界面更新三步，这样的开发适合小型或者简单的页面，但是在开发复杂页面时，这样混杂在一起的代码很容易碰到数据与视图代码难以复用的情况，因为都在一起，也无法进行有效的单元测试。</p><p>经过了之前传统开发模式的洗涤之后，有一些开发者开始思考，如何将视图与数据分开，于是借鉴后端的 MVC 开发模型，将前端也进行了拆分，其中比较典型的框架是 <a href="http://backbonejs.org/" title="Backbone">Backbone</a>。</p><p>但和后端应用的数据库数据不同，这里的数据是通过接口从后端获取的数据；View 视图层，只负责响应页面内的交互事件；Controller 控制器层，负责数据与视图层、视图与视图的通讯。</p><p>类似如下：</p><pre><code class="language-javascript">class Worker extends Controller {  route () {    const view = new View();    view.render().appendTo('#root');    const model = new Model();    model.fetch(function(res) {      view.renderData(res);    }  }}</code></pre><p>但是从这里我们可以看到，实际上 Controller 的工作依然是同步视图层和数据层，在适当的时候重绘页面，但这样有两个问题：</p><ol><li>数据发生改变时，视图层肯定要发生改变，这一步其实可以让它自动化，就能减少开发者的重复代码。</li><li>数据发生改变时，整个页面都需要全部重新绘制，而我们知道 DOM 重绘的开销是非常大的，而且它是一个同步的过程，会造成浏览器线程阻塞，导致用户无法操作。</li></ol><h3 id="后果数据视图双向绑定的-mvvm">后果：数据视图双向绑定的 MVVM</h3><p>开发这在经过了数据和视图之间大量的同步工作后，Reactive 式开发模式在移动端开发先流行起来，它的实现就是 MVVM，它通过 MV（Model-View）层替换了之前的 Controller 层，在 Model 和 View 之间搭建了一个桥梁，当数据发生改变时自动将数据的改变同步到界面上，而无需再有人工操作。</p><p>这样大大提高了整体开发效率，同时有效的拆分让可测试性大大提高，只需要测试界面组件对用户行为的响应是否正常，以及数据输入参数后的输出是否符合预期即可。</p><h3 id="第一个问题javascript-如何知道数据发生改变">第一个问题：Javascript 如何知道数据发生改变？</h3><p>在 ES5 规范中，提供了一个 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" title="Object.defineProperty()">Object.defineProperty()</a> 方法，它为监听对象属性（Property）改变提供了可能。</p><p>它给 Javascript Object 提供了设置 getter 和 setter 的可能，setter 和 getter 是指当给对象属性添加一个获取和改变的监听回调函数，当对对象属性进行操作时，回调函数会自动执行。</p><p>例如（代码来自 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" title="Object.defineProperty()">Object.defineProperty()</a>）：</p><pre><code class="language-javascript">function Archiver() {  var temperature = null;  var archive = [];  Object.defineProperty(this, 'temperature', {    get: function() {      console.log('get!');      return temperature;    },    set: function(value) {      temperature = value;      archive.push({ val: temperature });    }  });  this.getArchive = function() { return archive; };}var arc = new Archiver();arc.temperature; // 'get!'arc.temperature = 11;arc.temperature = 13;arc.getArchive(); // [{ val: 11 }, { val: 13 }]</code></pre><h3 id="第二个问题部分数据改变时如何提高视图更新速度">第二个问题：部分数据改变时，如何提高视图更新速度？</h3><p>再次强调，DOM 更新是非常耗时的，Javascript 本身的运算速度非常快，但是一旦开始更新 DOM 就全部慢下来了。所以 MVVM 都采用了更加先进的方法来更新视图。</p><ol><li>进行和原始数据的数据对比，缩小更新范围。</li><li>普遍采用虚拟 DOM（Virtual DOM）来处理数据更新。</li></ol><p>以 React JSX 为例，来描述一下它的虚拟 DOM 如何生成的，首先，JSX 的写法与 HTML 并无二致：</p><pre><code class="language-javascript">import React from 'react';const HelloComponent = ({ name }) =&gt; {  return (    &lt;div className=&quot;hello&quot;&gt;      Hello, { name }.    &lt;/div&gt;  )}React.render(&lt;HelloComponent name=&quot;XQ&quot; /&gt;, '#root');</code></pre><p>经过 Babel 编译之后，原有的 XML 结构会改成 React.createElement() 方法。</p><pre><code class="language-javascript">'use strict';var _react = require('react');var _react2 = _interopRequireDefault(_react);function _interopRequireDefault(obj) { return obj &amp;&amp; obj.__esModule ? obj : { default: obj }; }var HelloComponent = function HelloComponent(_ref) {  var name = _ref.name;  return _react2.default.createElement(    'div',    { className: &quot;hello&quot; },    'Hello, ',    name,    '.'  );};_react2.default.render(_react2.default.createElement(HelloComponent, { name: 'XQ' }), '#root');</code></pre><p>React.createElement() 做了如下几件事情，首先把 JSX 改成类似如下的数据结构，其次，对监听该数据中 name 参数的改变。</p><pre><code class="language-javascript">{tagName: &quot;div&quot;,properties: {className: &quot;hello&quot;}children: [{text: &quot;Hello, &quot;},{text: name},{text: &quot;.&quot;}]}</code></pre><p>然后根据该数据结构中的每一个 Object 都生成实际的 DOM，并且缓存起来，当数据发生改变时就可以直接找到对应的 DOM 更新界面。</p><p>当然 React 还做了大量优化，有更好的数据对比算法，而且当数据改变时不是立即更新界面，而是积累到一起，等主线程空闲下来时，一起批量更新等等。</p><h2 id="前端资源组件化-web-component">前端资源组件化 Web Component</h2><p><em>这里说的不是 W3C 基于 Shadow DOM 的 Web Components 标准，那个标准目前在浏览器支持不完善下暂时不能投入实际使用中。</em></p><p>如前所说，HTML 已经被 Javascript 化，从写页面改为写组件了，但是前端三驾马车里，还有一个 CSS，还有一堆静态资源（例如图片、字体、声音），它们的一些特点让它们难以被组件化。</p><ol><li>全局性 - 这些资源直接影响页面全局，不像 Javascript 那样具备命名空间的特性，CSS 的样式一写就会直接影响到整个网页。</li><li>难以维护 - 大量散乱的 CSS 和静态资源经过长时间版本迭代后，难以区分哪些资源归属于哪些页面，通常采取的办法是置之不理，但是这样时间一长会导致整个工程很大，冗余代码过多。</li></ol><h3 id="解决方案webpack">解决方案：Webpack</h3><p>Webpack 是于 2012 年出现的一套前端打包方案，虽然之前出现过其它的组件加载方案，例如 require.js、sea.js、browserify 等等，但是它是第一个能够把静态资源也打包进来的解决方案。</p><p>它的神器之处在于它有一堆 Loader，通过指定扩展名和 Loader 的对应关系，便能将它们打包整合进入前端组件中。</p><p>Loader 的配置范例如下：</p><pre><code class="language-javascript">    loaders: [      {        test: /.*\.json$/,        loader: 'json' // json loader 可以将 JSON 转换为 Javascript      },      {        test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.eot$|\.woff2?$|\.ttf$|\.wav$|\.mp3$/,        loader: &quot;file&quot; // file loader 将静态资源转换为 base64 整合进入 Javascript      },      {        test: /\.js$/,        loader: 'babel',        exclude: /node_modules/,        include: __dirname // babel loader 将 ES6 代码编译成 ES5，提前使用新特性。      },      {        test: /\.less?$/,        loaders: ['style', 'css', 'less'] // 这三个 loader 将 less 转换为 HTML inline style。      }    ]</code></pre><p>如果需要引用图片，不再需要手写 <img src="" />，而是：</p><pre><code class="language-javacript">import logoImage from './logo.png'; // Import logo imageimport 'style.less'; // Import styleconst img = document.createElement('img');img.src = logoImage;document.body.appendChild(img);</code></pre><p>以<a href="https://maimai.cn/feed_list" title="脉脉网">脉脉网</a>为例，样式被引用进界面后，会生成一个单独的 <style> 来描述样式，这样不同组件就可以引用不同的样式文件，做到基本的隔离。</p><pre><code class="language-html">&lt;div data-reactid=&quot;.v6852rafi8&quot; data-react-checksum=&quot;1364425829&quot;&gt;  &lt;!-- 内联的 Style --&gt;  &lt;style data-reactid=&quot;.v6852rafi8.0&quot;&gt;body{background: #f9f9f9;}&lt;/style&gt;  &lt;div class=&quot;headerBox&quot; data-reactid=&quot;.v6852rafi8.2&quot;&gt;...&lt;/div&gt;  &lt;div class=&quot;PCcontainer clearfix&quot; data-reactid=&quot;.v6852rafi8.3&quot;&gt;...&lt;/div&gt;&lt;/div&gt;</code></pre><p>在前端界面组件化上，另外一个框架 <a href="https://vuejs.org/" title="Vue">Vue</a> 走得更远，它通过 vue-loader 可以将 JSX 模版、ES6 代码、样式都写在一个文件中。</p><p>做完这一切之后：</p><ol><li>组件间完全独立，真正做到低耦合、高内聚。</li><li>组件增删改不再影响到别的组件。</li><li>多人协作时，可以同时并行开发多个组件，互不影响。</li><li>组件只有输入和输出，可以很方便地进行自动化测试。</li><li>MVVM 后，只需要对数据进行测试，界面更新被框架的单元测试覆盖。</li></ol><h2 id="数据流控制">数据流控制</h2><p>前端界面组件化后，组件之间的通讯和数据共享依然存在问题。</p><p>在早期的前端开发中，数据和视图不分，需要同步两个视图组件时，需要在数据层或者控制器层同时操作，导致耦合性很高，用户的行为操作和数据更新都会影响到视图层，有时发生问题会非常难以调试。</p><h3 id="单项数据流-unidirectional-data-flow">单项数据流 Unidirectional data flow</h3><p>那种方案叫做“双向数据流”，界面影响数据，数据也直接影响界面，为了让数据更好地在组件间共享，Facebook 在推出 React 的同时也推出了 Flux，提供了一种简单的“单项数据流”方案，后期又被简化为 Redux，Redux 的架构图如下：</p><p><img src="http://odoudgo1i.bkt.clouddn.com/design-pattern-of-modern-web-development/redux-arch.png" alt="" /></p><p>当视图组件需要数据时，不再时直接调动数据组件去获取数据，而是触发一个行为（Action），行为在获取数据后，放到数据仓库（Store）中，这里的数据是可以被所有地方共享的，但是它也不是直接对接到视图组件，而是通过 Reducer 再次处理成视图组件需要的数据后，通过 MVVM 的数据视图双向绑定，来更新视图。</p><p>Flux 和 Redux 二者的区别在于 Flux 的每一个界面组件都可以监听一个数据仓库（Store），允许多个数据仓库存在，而 Redux 只有一个 Store，随着事件发展，逐渐发现单个 Store 的优势更多（后面会说到），因此代替了 Flux 成了前端社区的标准配置，优势如：</p><ol><li>Store 对于数据共享有设计上的优势，一个 Store 保持了简洁。</li><li>Reducer 将共享数据处理成 View/Component 所需要的数据，将数据处理与视图层分开。</li><li>单向数据流，经过 Store 中转，数据流将变成得更加可见、可控。</li><li>可以跟踪每一个 Action 的执行时间。</li><li>Time travel</li></ol><p>单项数据流提供了开发上的优势，数据可以做到一览无余。</p><p><img src="http://odoudgo1i.bkt.clouddn.com/design-pattern-of-modern-web-development/redux-chart.png" alt="" /></p><p>数据更新可以被跟踪，同时还能看到两个 Action 之间所花费的时间：</p><p><img src="http://odoudgo1i.bkt.clouddn.com/design-pattern-of-modern-web-development/redux-inspector.png" alt="" /></p><p>因为数据更新前后的状态都被保存下来，还可以做到时间旅行（Time travel），可以很方便地实现 Undo 和 Redo，可以随时回到之前任何一个时间操作的状态，或者重现之前操作的步骤，这里有两个 Redux 的范例：</p><ol><li>Todo App - <a href="https://github.com/reactjs/redux/tree/master/examples/todomvc">https://github.com/reactjs/redux/tree/master/examples/todomvc</a></li><li>Flappy bird - <a href="https://github.com/Lucifier129/flappy-bird">https://github.com/Lucifier129/flappy-bird</a></li></ol><h2 id="不可变数据结构-immutable">不可变数据结构 Immutable</h2><p>React 的数据对比算法在数据量巨大或者层级过多时性能依然不够，迫切需要一种更方便进行对比的数据结构。</p><p>不可变数据结构最早应该是出现在 Scala 语言中，该语言是为多线程设计的，因此不可变数据结构一开始就是线程安全的，它有这么几个特性：</p><ol><li>创建的时候便已经被固定，并且在整个生命周期中都不可改变。</li><li>当对象属性发生改变时，会生成一个新的对象，和旧的对象不再相同。</li><li>绝大多数基于链表实现，每次对象属性生成新对象时，所有旧的对象属性只是个引用，而不会完整拷贝。</li><li>因为基于链表，所以两个不可变数据结构对比时，只需要检查二者每个对象属性引用的内存地址是否一致，对比性能超强。<br />5、因为每次修改都生成新的对象，所以特别适合需要在多个函数中进行对象引用传值时，保持原始对象的完整性。</li></ol><p>它更新数据时是这样操作的：</p><p><img src="http://odoudgo1i.bkt.clouddn.com/design-pattern-of-modern-web-development/immutable.gif" alt="" /></p><p>因为是基于链表，所以它更新数据的开销非常小，首先去掉旧的对象属性，然后生成新的对象属性，做好关联后，生成一个新的根节点，变成新的对象。</p><h3 id="它和常量的不同">它和常量的不同</h3><p>常量是不可以改变的单位，发生改变时，编译器会直接抛出错误。</p><pre><code class="language-javascript">const foo = 123;foo = 234; // throw: &quot;foo&quot; is read-only</code></pre><p>不可变数据结构，是当对象属性发生改变时，整个对象指针都发生变化，不再指向旧的对象，而是一个全新的对象，这样在函数间进行引用传值时，原始值不变。</p><pre><code class="language-javascript">let foo = Immutable.fromJS({number: 123});function printNumber(obj) {  let bar = obj.set('number', 234);  return bar.get('number');}printNumber(foo); // 234foo.get('number'); //123</code></pre><h1 id="前端开发的未来">前端开发的未来</h1><p>现在社区的异常活跃带动了本属于浏览器的 Javascript 语言走上了其它平台的开发之路，现在在应用层 Javascript 已经没什么不能去做了，尤其因为 WebView 的兼容性，使 Javascript 成为了跨平台开发的万金油。</p><p><img src="http://odoudgo1i.bkt.clouddn.com/design-pattern-of-modern-web-development/javascript-env.png" alt="" /></p><p>在这样的背景下，Javascript 本身的一些缺陷和历史遗留问题也被暴露出来，各大厂商都在积极推出自己的解决方案，让 Javascipt 变得更好，除了 Google 一直推动前端技术发展外，微软推出了 TypeScript 加强了 Javascript 动态类型的缺陷，变成了静态类型的语言，这样在开发过程中就能检查出以前需要运行时才能发现的错误，Facebook 也推出了自己的静态类型方案 flow.js，社区内的活跃也让 ES6 到 ES5 的转换程序 Babel 成为主流，让前端开发提前使用了新的语法标准所带来的便利。</p><p>Javascript 虽然之前只是运行在浏览器引擎上的语言，但是未来的路会越来越宽。</p>]]>
                    </description>
                    <pubDate>Sun, 18 Sep 2016 14:36:50 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[在 Windows 上部署基于 Docker 的 Web 服务器环境进阶 - Dockerfile 和 Docker Compose]]>
                    </title>
                    <link>https://kxq.io/archives/在windows上部署基于docker的web服务器环境进阶-dockerfile和dockercompose</link>
                    <description>
                            <![CDATA[<h1 id="概述">概述</h1><p>在上一篇<a href="http://www.atatech.org/articles/49956">《在 Windows 上部署基于 Docker 的 Web 服务器环境 - 零基础入门》</a>中，已经对如何安装、使用 Docker 进行了基本的介绍，而在本进阶篇中，将通过一个实际的例子，介绍两个工具 - Dockerfile 和 Docker Compose，将环境的部署、迁移更进一步简化。读者按照教程一步一步走，就能够将一个 NodeJS 服务在  Docker 中运行起来，本文依然非常入门浅显，更多资料还请参考官方文档。</p><p>首先需要说明的，是为什么需要这两个工具。</p><p>##Dockerfile</p><p>如果用虚拟机镜像的方式，将自己的服务器环境移交给别人，需要告诉别人自己的镜像地址，让别人自行下载，但是这样有两个缺点：</p><ol><li>镜像经过多次修改，可能非常大，下载会很慢；</li><li>别人如果需要使用自己的镜像，可能还得再增加一点原来镜像中缺少的个性化配置（例如不同开发机可能绑定了不同的域名，或者有不同的 Client ID、Secret 之类的），而原来的镜像中可能已经写死了。</li></ol><p>为了解决这两个问题，Docker 还提供了将镜像打包的过程脚本化的办法，就是 Dockerfile，这样，别人只要拿到一份体积很小的 Dockerfile，稍加需要自定义配置的编辑，即可在本地编译出一份属于自己的镜像。</p><h2 id="docker-compose">Docker Compose</h2><p>现代的 Web 开发越来越复杂，可能依赖着好几个别的服务，如果手工给每个镜像配置目录映射、端口映射将是非常复杂的工作，为了解决这个问题，提出了 Docker Compose 的概念。</p><p>一个 Compose 即是一个应用与服务的集合，通过一个文件配置好之后，一行命令即可启动应用和所有依赖服务，在这里可以理解为 Compose 也是一个独立的服务集合容器，在该容器中，应用对服务可以无限制访问，但这些服务对外又是被隔离的，只有自己应用所指定的端口是被开放给 Host，可供开发者访问。</p><h1 id="场景">场景</h1><p>为了简化说明，让读者您更佳好地理解其中的理念和用法，我想通过一个很简单，但很实际的例子来描述如何通过这两个工具，来进行服务器部署。</p><p>一个 NodeJS 应用，使用 ES6 语法编写，在 Node 0.12.10 上运行，因为 Node 0.12.10 没有 ES6 支持**（最新版本的 Node 已经有 ES6 语法支持了）**，因此需要在 node 的服务镜像中加入  Babel 6 以提供 ES6 脚本运行支持（Dockerfile 自定义镜像），然后随着业务发展，会增加 MySQL 支持，因此需要该应用加入 MySQL 服务，在启动应用时，MySQL 也需要一起启动（Docker Compose 应用服务集合）。</p><h1 id="dockerfile-自定义镜像">Dockerfile 自定义镜像</h1><p>把以下的代码，都保存到同一个目录下即可。</p><h2 id="应用代码">应用代码</h2><p><em>server.js - 应用</em></p><pre><code class="language-javascript">/* 加载模块以创建 HTTP 服务 */const http = require('http');/* 定义服务器将监听的地址 */const host = '0.0.0.0';        // 因为需要将 Docker 服务对外，因此需要放开 IP 限制。const port = 8000;/* 配置 HTTP 模块来响应请求 */const server = http.createServer((req, res) =&gt; {  res.writeHead(200, {'Content-Type': 'text/plain'});  res.end('Hello Docker\n');});/* 监听主机和端口 */server.listen(port, host);/* 在终端上输出启动信息 */console.log(`Server running at http://${host}:${port}/`);/* 处理 Ctrl + C 键，当按下时退出 */process.on('SIGINT', () =&gt; {  console.log(&quot;Exiting...&quot;);  process.exit();});</code></pre><p><em>package.json - 应用配置文件</em></p><pre><code class="language-json">{  &quot;name&quot;: &quot;docker-example&quot;,  &quot;version&quot;: &quot;0.0.1&quot;,  &quot;author&quot;: &quot;xuqing.kxq@alibaba-inc.com&quot;,  &quot;dependencies&quot;: {    &quot;babel-core&quot;: &quot;^6.5.2&quot;,    &quot;babel-preset-es2015&quot;: &quot;^6.5.0&quot;,    &quot;mysql&quot;: &quot;^2.10.2&quot;  }}</code></pre><p><em>.babelrc - Babael 配置文件</em></p><pre><code class="language-json">{  &quot;presets&quot;: [    &quot;es2015&quot;  ]}</code></pre><h2 id="安装应用运行镜像-node-01210">安装应用运行镜像 node 0.12.10</h2><p>因为 node 最新版本已经到  5.7.0 了，我们需要通过 <code>:0.12.10</code> tag 来指定 node 版本。</p><p><code>$ docker pull node:0.12.10</code></p><p>与上篇提过的操作系统镜像不同，该镜像不提供 Shell，直接 run 后无法进入 Linux 环境的，只能通过 <code>run</code> 来执行一些该镜像里包含的命令，例如可以看看 node 的版本：</p><pre><code>$ docker run -it node:0.12.10 node --versionv0.12.10</code></pre><p>同样的，我们可以直接通过 run 安装 Babel6，但是这样安装的仅可供自己的 Container 使用，当然，也可以将安装后的 Babel6 的 Container push  到自己的镜像里，但是小的服务可以这样做，复杂的运行环境可没那么简单，Babel 体积很小，自己从基础的 node 镜像上安装也很简单，因此需要 Dockerfile。</p><h2 id="安装应用依赖">安装应用依赖</h2><p>这里需要通过 node 镜像中的 <code>npm</code> 命令，为 Node 应用安装上依赖的包。</p><p>需要说明的是，Dockerfile 只适合公用的部分，例如可公用的类库；私有的或者应用的部分不能集成到镜像中，比如 package.json 中的 dependencies 都是安装到应用里的本地模块，那些模块如果通过 <code>npm install -g</code> 安装的的话，启动应用时会找不到，所以这里为了应用能运行起来，依然有安装依赖这一步。（使用 <code>npm link</code> 那是另外一个话题了）</p><p>依赖的包已经定义在 <code>package.json</code> 中了，而 <code>--no-bin-links</code> 参数是给 Windows 系统使用的，因为文件系统限制，Linux 的 ln  命令不能再 NTFS 文件系统上建立软链接，如果不加的话，会报 Protocol Error 错误。</p><p>这里还为 <code>run</code> 增加了一个 <code>-w</code> 参数，意思是切换到该工作目录下，再执行 <code>npm</code> 命令。</p><pre><code>$ docker run -v $(pwd):/code -w /code -it node npm install --no-bin-links</code></pre><p><em>此处有八阿哥</em></p><p>node-0.12.10 带的 npm 2.14.9 貌似有 bug，在执行这一步时可能会因为文件名问题出现 EPERM 错误，此时 pull 一下最新版本的 node 镜像，并且重新执行上面命令即可安装成功。</p><h2 id="打包自定义镜像">打包自定义镜像</h2><p>打包镜像需要首先编写 Dockerfile，它定义了新的镜像将从哪儿来，将往哪儿去。</p><p><em>Dockerfile</em></p><pre><code># 下载镜像FROM node:0.12.10# 维护人信息MAINTAINER XQ Kuang &lt;xuqing.kxq@alibaba-inc.com&gt;# 安装 Babel，这里其实可以执行多次 RUN，以行分隔RUN npm install -g babel-cli# 增加一个默认的 .babelrc 配置文件ADD .babelrc /root/# 暴露端口以供映射EXPOSE 8000</code></pre><p>然后使用 <code>build</code> 命令进行打包，其中 <code>-t</code> 参数是指定一个 Repository ，这样方便启动 Container，暂定为 xuqingkuang/docker-example，因为该名称可以推送到 Docker Hub，最好起 Docker Hub 上的名字，最后一个参数是 Dockerfile 的存放路径。</p><p><code>$ docker build -t xuqingkuang/docker-example .</code></p><p><img src="http://img4.tbcdn.cn/L1/461/1/5891ca021b81c3f163c7864e83524d01b67615cb.png" alt="docker_build" /></p><h2 id="测试镜像">测试镜像</h2><p>经过一段时间脚本的执行，新的镜像已经产生了，可以通过 <code>images</code>  检查一下，然后用 babel-node 来执行之前的服务器脚本 <code>server.js</code>，如果可以正常运行，那我们的镜像就打包成功了。</p><p><code>$ docker run -p 80:8000 -v $(pwd):/www -it xuqingkuang/docker-example babel-node /www/server.js</code></p><p><img src="http://img4.tbcdn.cn/L1/461/1/39c1139c8a58d4716b94201d967cf4503bb66f11.png" alt="3_docker_run" /></p><h1 id="docker-compose-连接服务容器">Docker Compose 连接服务容器</h1><p>通过 Dockerfile 定制的镜像，我们已经有了个 NodeJS 的应用容器了，但是有一天随着时间发展，该应用容器有了扩展的需求，新的需求是连接  MySQL，并且通过 MySQL 将当前日期打印出来，于是乎代码被进一步扩充。</p><h2 id="应用代码-1">应用代码</h2><p><em>server-mysql.js</em></p><pre><code class="language-javascript">/*  加载模块以创建 HTTP 服务和 MySQL 连接 */const http  = require('http');const mysql = require('mysql');/* 定义服务器将监听的地址，以及 MySQL 连接的参数 */const host = '0.0.0.0';        // 因为需要将 Docker 服务对外，因此需要放开 IP 限制。const port = 8000;const mysqlOptions = {  host     : 'mysql',          // 这里和 docker-compose.yaml 中定义的 mysql 配置一致  user     : 'nodejsapp',  password : 'ILoveDocker'}/* 创建 MySQL 连接 */const mysqlConnection = mysql.createConnection(mysqlOptions);mysqlConnection.connect();/* 配置 HTTP 模块来响应请求 */const server = http.createServer((req, res) =&gt; {  res.writeHead(200, {'Content-Type': 'text/plain'});  // 查询数据库，取当前时间，并且返回给浏览器  mysqlConnection.query('SELECT NOW() AS now;', (err, rows, fields) =&gt; {    res.write(`Now is ${rows[0].now}\n`);    res.end('Hello Docker\n');  });});/* 监听主机和端口 */server.listen(port, host);/* 在终端上输出启动信息 */console.log(`Server running at http://${host}:${port}/`);/* 处理 Ctrl + C 键，当按下时退出 */process.on('SIGINT', () =&gt; {  console.log('Closing MySQL Connection');  mysqlConnection.close();  console.log(&quot;Exiting...&quot;);  process.exit();});</code></pre><h2 id="编写-docker-composeyaml">编写 docker-compose.yaml</h2><p>Docker compose 的配置文件是个 YAML，其实就是个简单的没有花括号的 Object 结构。</p><p>需要特别说明的说明的是 <code>links</code> 段，里面的内容必须和下面的服务名称保持一致，对于服务 Container 而言，可以理解为一个独立的虚拟机，所有端口都开放，应用通过 links 连接它相当于连接一台独立的虚拟机，所以没有端口映射一说。</p><p>这里只 link 了一个服务，现实开发中，还可以将更多的服务连接起来。</p><p><em>docker-compose.yaml</em></p><pre><code class="language-yaml">version: &quot;2&quot;                                   # 版本services:                                      # 服务  web:                                         # NodeJS 应用    image: xuqingkuang/docker-example          # 源自镜像    ports:                                     # 端口映射     - &quot;80:8000&quot;    volumes:                                   # 目录映射     - .:/code    links:                                     # 关联服务映射 - 重点     - mysql    command: babel-node /code/server-mysql.js  # 启动后执行命令  mysql:                                       # 服务名称    image: mysql                               # 服务镜像    environment:                               # 服务环境变量     - MYSQL_ALLOW_EMPTY_PASSWORD=yes     - MYSQL_USER=nodejsapp     - MYSQL_PASSWORD=ILoveDocker</code></pre><h2 id="启动-docker-compose">启动 Docker Compose</h2><p>全部准备完成之后， 就可以启动 Docker Compose 了，可以看到它先启动了服务，然后才启动了应用。</p><p>同时这里可以看到 Compose 给应用和每个服务都设置了一个单独的名字，事实上服务连接就是依靠这些独一无二的名字连接起来的。</p><p><code>$ docker-compose up</code></p><p><img src="http://img3.tbcdn.cn/L1/461/1/2785e7c69aee358e361d75a35c0a1a1600d74579.png" alt="docker_compose_up" /></p><h2 id="测试-dock-compose">测试 Dock Compose</h2><p>还是连接 Docker  Host 的端口，应用本身除了增加了 mysql 读取当前时间，没有任何变化。</p><p>我们可以看到，现在的时间，已经被加入到网页了。</p><p><img src="http://img3.tbcdn.cn/L1/461/1/b7a3b5b21521d1a684b37c3018d49a9d76a726ef.png" alt="docker_compose_test" /></p><h2 id="停止-docker-compose">停止 Docker Compose</h2><p>按下 Ctrl + C 键即可停止 Docker Compose，可以看出这里与启动时相反，是先停止应用，再停止服务的。</p><p>这里有个小技巧，按下一次 Ctrl + C 是“优雅地”等待所有服务完成再退出，再按下一次就“强制地“关闭 Container 了。</p><p><code>Ctrl + C</code></p><p><img src="http://img1.tbcdn.cn/L1/461/1/818488f8ec6521d9aceb8ff19b7e4e4bcaf98853.png" alt="docker_compose_stop" /></p><h1 id="结尾">结尾</h1><p>Docker 的基本使用两篇就到此结束了，它的应用服务隔离、多版本共存其实非常适合在开发环境的服务器架设中使用，我希望这样的技术能在集团内更广泛地被使用。</p><p>本文之所以能完成，要感谢以下几位：</p><ul><li>感谢 @大知 指导我使用了 Docker Compose，让我理解了它的理念，如果没有他，那我只能产出上个入门篇了；</li><li>还要感谢 @劲叔 和 @零一 率先在组内率先试用 Docker 部署服务，准备开发环境，希望你们能用得开心；</li><li>最后要感谢 @澄苍 和整个网站工程部门，也只有这样灵活而且敏感的团队，让我可以在工作之余，去尝试这样的新技术。</li></ul><p>欢迎讨论，有任何问题，也可以钉钉我 @释我。</p><h1 id="one-more-八阿哥"><em>One More 八阿哥</em></h1><p>在使用 Docker Toolbox 的过程中，无意中在 @劲叔 那里发现了一个八阿哥，他的代码放在 D 盘上，但在 Docker 中使用 <code>run -v</code> 参数无法映射目录，Docker Compose 中也无法通过 volumes 映射目录。</p><p>经过仔细排查，才发现是因为 VirtualBox 中默认只是共享了用户的目录，而其它目录并未共享，导致在 Docker Quickstart Terminal 中映射的目录，在 Docker Host 中根本无法访问。</p><p>所以建议大家，请将自己的项目代码放到 %HOME% 自己的用户目录里（OS X 里是 $HOME），这样方便 Docker 使用。</p><p><img src="http://img3.tbcdn.cn/L1/461/1/ecf4bd884e811469100ba7b4122e2e8429ea2039.png" alt="virtualbox_share_folders" /></p>]]>
                    </description>
                    <pubDate>Tue, 12 Jul 2016 14:06:13 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[在 Windows 上部署基于 Docker 的 Web 服务器环境 - 零基础入门]]>
                    </title>
                    <link>https://kxq.io/archives/在windows上部署基于docker的web服务器环境-零基础入门</link>
                    <description>
                            <![CDATA[<h1 id="概述">概述</h1><p>本文将以尽可能通俗易懂、详尽粗浅的语言，主要面向零基础的研发工程师同学，介绍一下如何在 Windows 上搭建基于 Docker 的 Web 服务器环境，Docker 是目前非常热门的技术，它降低了大规模部署服务的难度，提升了集群部署升级的效率。其实它不仅能提升运维的效率，研发工程师使用 Docker 来进行本地服务器环境部署，进而进行 Web 开发也非常合适，用 Docker 来进行服务器环境部署有几个优势：</p><ol><li>可以避免在开发用电脑中安装大量服务器软件 - 这些正常安装的服务器软件很有可能在开机时即启动，占用了大量系统资源，导致日常使用时内存变少，系统性能变差，而使用  Docker 来管理这些服务可以做到按需启动；</li><li>可以在一台电脑上，管理相同服务的多个不同版本 - 同一台电脑可能要进行多个不同应用的开发，而这些不同的应用可能会依赖同一个服务的不同版本，这通过常规的安装途径难以解决，而 Docker 的服务镜像能有效地解决这点，绝大多数  Docker 的服务镜像也提供了多个版本；</li><li>重复部署会非常简单 - 在团队协作中，同一个应用所依赖的服务器环境肯定是一样的，但是却难以避免团队中多个成员的重复部署和配置，而 Docker 可以将部署好的整个服务器环境打包成镜像，成员直接将镜像下载回来，即可将整个服务器环境运行起来，非常方便快捷。同样的，如果自己更换电脑，或者重装系统，原来的服务器环境也可以以最快速度恢复运行；</li><li>可以在 Windows 上运行一个轻量的 Linux 环境 - Windows 上的命令行工具 cmd.exe/PowerShell 和 Linux 有较大差异，而绝大多数开源开发工具都是基于Linux 的，尽管已经有了 Cygwin 和 MinGW 依然难以保证自己需要的工具已经被编译好可用，一套完整的 Linux 环境就能解决这个问题，而 Docker 本身就是基于 Linux，在 Windows 只需要一个 Linux 虚拟机就能解决问题，而且 Docker 的资源隔离策略也可以最大限度减少资源占用。</li></ol><p>关于 Docker 原理的文章上 ATA 上已经很多，这篇 <a href="http://www.atatech.org/articles/18261">《Docker简介》</a> 可以在阅读本文实战之前先了解一下基础概念，本文仅介绍 Docker 的安装和使用。</p><h2 id="与-linux-上运行-docker-的区别">与 Linux 上运行 Docker 的区别</h2><p>Docker 本是在 Linux  上使用的应用，后才移植到 Windows 和 Mac OS X 上，在 Windows  上运行 Docker 和在 Linux  唯一不同的问题在于，Docker 所依赖的 cgroups 进程和资源隔离策略在 Windows 并未实现，无法作为 Host 主机，所以依然需要一套在虚拟机中运行的 Linux 系统作为 Host 主机，这样才能运行别的  Container。</p><p>这样，Docker 的架构就有点变化了。</p><p><em>Docker 架构图</em></p><p><img src="http://img4.tbcdn.cn/L1/461/1/3b5f7e36d4ce571ccb8e606e7bd0cb40a6633c24.png" alt="Docker架构" /></p><h1 id="运行需求">运行需求</h1><p>在安装 Docker 之前，我们需要先检查电脑是否符合 Docker 的运行需求。</p><h2 id="cpu-和操作系统必须都是-64-位的">CPU 和操作系统必须都是 64 位的。</h2><p>可以通过在“我的电脑”或者“此电脑”上点击鼠标右键，在点击“属性”，打开系统属性进行查看。</p><p><em>系统属性中查看是否支持 64 位</em></p><p><img src="http://img1.tbcdn.cn/L1/461/1/43b81d2771c0d4de0bf1ef848de463f745f94d90.png" alt="Windows系统" /></p><h2 id="操作系统必须支持-vt-虚拟化技术">操作系统必须支持 VT 虚拟化技术。</h2><p>Windows 8 以上系统可以通过“任务管理器”检查，直接在底部任务栏上点击鼠标右键即可打开。</p><p>Windows 7 系统需要去 <a href="https://www.microsoft.com/en-us/download/details.aspx?id=592">微软官方</a> 下载一份虚拟化检查工具，来检查自己的电脑是否支持虚拟化。</p><p>需要说明的是现在新的电脑绝大多数都已经支持了 VT 技术，如果检查该特性未启用，可能是在 BIOS 中尚未开启，在 BIOS 中找一下 Virtualization Technology 将其开启即可。</p><p><em>任务管理器中查看是否支持  VT 技术</em></p><p><img src="http://img2.tbcdn.cn/L1/461/1/5f3c1fdca24919a762491633cafba89248dc85f5.png" alt="任务管理器" /></p><h1 id="安装">安装</h1><p>Docker 必须在 Linux 上运行，其实自己找一个虚拟机跑一个 Linux 把 Docker 套件下载下来之后都可以运行，而 Docker 官方已经为我们提供了一个非常方便的工具，叫做 <a href="https://www.docker.com/products/docker-toolbox">Docker Toolbox</a> 包含了 VirtualBox 虚拟机、一个 Docker Host Linux、以及 Docker Compose，直接下载后安装即可，截止本文发布 Docker Toolbox 的最新版本为 1.10.2。**补充：**因为 Amazon S3 国内访问补偿，因此我在淘云盘上也将当前最新版本分享了出来，请点击链接：<a href="http://yunpan.alibaba-inc.com/share/link/TH78GoG9v9">http://yunpan.alibaba-inc.com/share/link/TH78GoG9v9</a></p><p>在安装过程中，因为 Kitematic 还不成熟，可以取消它的选项，作为服务器环境，只需要 Docker 的命令行工具就够了。</p><p><em>Docker 安装</em></p><p><img src="http://img4.tbcdn.cn/L1/461/1/6a146a086b28460e9fca39869a0e5c7dc23a9cad.png" alt="Docker安装" /></p><p>#使用</p><h2 id="启动">启动</h2><p>安装之后，开始菜单中就会多出三个菜单项，分别是 Git、VirtualBox、以及 Docker，我们需要使用这里的 Docker Quickstart Terminal，这是 Docker 使用的入口。</p><p><em>Docker 菜单</em></p><p><img src="http://img2.tbcdn.cn/L1/461/1/96e6b7262e91e8eb38219d0ab3e52a4d78705ba4.png" alt="docker_menu" /></p><p>第一次启动 Docker，经过简单的初始化，Docker Client 便启动起来了，它在 Windows 上是使用 MinGW（全称是“Minimalist GNU for Windows”) 运行的一个小的环境，类似 Linux 系统，根据上面的架构图可知，我们对 Docker 的操作皆由它传递给 Docker Daemon，进而操作 Docker Container 的。</p><p><em>Docker 初始化</em></p><p><img src="http://img1.tbcdn.cn/L1/461/1/191a61168fe486c03123d02f0eb0fbed8cb282a9.png" alt="docker_init" /></p><p><em>进入 Docker Client Shell</em></p><p><img src="http://img2.tbcdn.cn/L1/461/1/adc79f2a18516b3e8dd5469c47f3e43cd42348c0.png" alt="docker_shell" /></p><h2 id="进入-docker-host-本章可略过不读">进入 Docker Host [本章可略过不读]</h2><p>本章与 Docker 使用无关，只是对架构图的实现进行一下说明，普通用户可略过不看。</p><p>刚才的 Docker Quickstart Terminal 启动后，其实依然在 Windows  中，但在架构图中我们可以看到有一层 Linux VM，通过下面的命令，就可以直接进入进去，在 Docker Quickstart Terminal 里执行的命令，在 Linux VM 中也同样可以执行，事实上，Docker Quickstart Terminal 对 Container 的操作，经过了一层 Wrapper 的网络通信，再交由 Linux VM 的 Docker Daemond 进行的。</p><p><code>$ docker-machine ssh default</code></p><p><em>Docker Machine</em></p><p><img src="http://img2.tbcdn.cn/L1/461/1/0e64cec13f05454143582871b0857885c0d3bef9.png" alt="docker_machine" /></p><p>default 是虚拟机的名字，由 Docker Toolbox 创建，如果打开 Virtualbox，就可以看见它安静地躺在机器列表里。</p><p><em>Virtualbox</em></p><p><img src="http://img1.tbcdn.cn/L1/461/1/6ebf8bd09e22eeb92b39fd0ec69561e6c084dce5.png" alt="Virtualbox" /></p><h2 id="下载镜像">下载镜像</h2><p>镜像 Image 是 Docker 的第一个概念，Docker 不是虚拟机，而是 Application Container，镜像简单说就是 Container 的内容，Container 都是从镜像中启动，如果是服务器镜像，那可以用它运行起服务，如果是虚拟机镜像，那可以进入其中，进行普通 Linux 的操作，镜像是 Docker 最强大的地方。</p><p>所有的镜像，都存放在 <a href="https://hub.docker.com/explore/">Docker Hub</a> 上，进入后可以看见有操作系统的 ubuntu，也有 redis、mysql 一类的服务，甚至还有 node 一类的应用执行环境，各种服务镜像包罗万象，各个服务官方也都在为 Docker 打包，在准备运行一个服务前先去 <a href="https://hub.docker.com/explore/">Docker Hub</a> 上看看，说不定有惊喜。</p><p>为了说明方便，我们先使用操作系统包 <a href="https://hub.docker.com/_/fedora/">fedora</a> 来举例。首先使用 pull 命令，经过少许等待后，镜像就能下载下来了。</p><p><code>$ docker pull fedora</code></p><p><img src="http://img2.tbcdn.cn/L1/461/1/15dbb6164185e453b49a32bea201614347ac5996.png" alt="docker_pull" /></p><p>其实从 pull 命令上可以看出，Docker 的操作其实和 Git 非常类似，事实上 Docker 镜像就是建立在 Git 上的。</p><h3 id="下载指定版本号的镜像">下载指定版本号的镜像</h3><p>在实际使用过程中，我们经常对服务的版本号有严格要求，Docker 也提供了很简单的下载指定版本号的办法，在每个 <a href="https://hub.docker.com/explore/">Docker Hub</a> 都可以看到不同版本的镜像都提供了很多个版本，而指定版本号其实非常简单，就是在 Image 名称后面，加上冒号，和版本号即可。</p><p><code>$ docker pull fedora:20</code></p><h2 id="检查已经下载好的镜像">检查已经下载好的镜像</h2><p>我们可以通过 images 命令，来检查镜像是否下载成功，从这里可以看到 fedora 的镜像已经下载成功了。</p><p><code>$ docker images</code></p><p><img src="http://img1.tbcdn.cn/L1/461/1/e75cd2cab0e2381c37d40566b318b3482571707d.png" alt="docker_images" /></p><h2 id="从镜像启动-container">从镜像启动 Container</h2><p>启动 Fedora 的 Container 其实非常简单，下面的命令即可：</p><p><code>$ docker run -it fedora</code></p><p>这里参数的意思是：</p><ul><li><p><code>-i</code> 即使没有对接（Attach）成功，也保持标准输入接口开启；</p></li><li><p><code>-t</code> 分配一个终端，没有它的话，就没有 shell 了。</p></li></ul><p>除了这两个参数之外，还有两个映射参数非常常用，用于把 Host 里的资源，映射到 Container 里。</p><ul><li><code>-v</code> 目录映射，参数是 &quot;[Host 目录]:[Container 目录]&quot;，映射后即可在 Container 里访问 Host 的某个目录，常用于自己的文件、或者代码映射到 Container 中，由 Container 中的服务执行；</li><li><code>-p</code> 端口映射，参数是 &quot;[Host 端口]:[Container 端口]&quot;，Container 因为是被完全隔离的，如果其中某个服务开启了某个端口，需要从 Client 访问到，则需要通过映射到 Host 上才能访问。</li></ul><p>举个例子，我们有个静态的文件，需要被 Container 中的 node-static（一个 Node 编写的静态文件服务器）提供 Web service，则命令变成了（因为 fedora 中没有 node，所以这里换成了我编辑过的镜像  xuqingkuang/nodejs）：</p><p><code>$ docker run /p 80:8000 -v /project:/www -it xuqingkuang/nodejs</code></p><p>这个例子，就是把 Container 里的 8000  端口，映射到 Host 的 80 端口，把当前目录下的 /Projects 目录映射到 Container 的 /www，然后 Chrome 透过 Docker 访问  static 提供的 Web 服务。</p><p><img src="http://img4.tbcdn.cn/L1/461/1/47ed3cca5d6056def972cd5422de4991c09a7408.png" alt="docker_service" /></p><h2 id="从-container-中退出">从 Container 中退出</h2><p>在 Container 的 Shell 中如果直接使用 exit 退出，或者从服务镜像启动的 Container 上按下 Ctrl + C，则会把 Container 停止掉，之前运行的服务也都会随之停止，对于绝大多数服务类型的这样做可能是比较合适的，但是对操作系统镜像，可能依然需要它在后台运行，在合适的时候再切换回去，这里有一个小技巧，是按下 Ctrl + P 键，这时看起来没有任何反应，再按下 Ctrl + Q 键即可 detach 当前的 Container。</p><p>这里又需要提到一个用于查看所有 Container 的小命令 - <code>ps</code>，<code>-n</code> 参数是为了限制 Container 显示数量，可以看出，第一个 9c1e80 那个 Container 通过 exit 命令退出，它的状态已经变成了 Exited，而 832c07，通过 detach，它依然在后台运行着，状态是 Up。</p><p><code>$ docker ps -n=3</code></p><p><img src="http://img3.tbcdn.cn/L1/461/1/7563c74239378262399bd3e7fb46abb5acc0ae34.png" alt="docker_attach" /></p><h2 id="重启已经停止的-container">重启已经停止的 Container</h2><p>每个 Container 变成 Exit 状态后，我们是否有办法重新运行它呢？答案是肯定的，通过 <code>start</code> 命令就可以重新启动它，但是重启后的 Container 是 detach 状态，我们需要通过 <code>attach</code> 命令重新连入 Container。</p><p><code>$ docker start [Container ID]</code></p><p><img src="http://img3.tbcdn.cn/L1/461/1/c1474f5118eec171e47812b6d26446a1fc3d3a10.png" alt="docker_start" /></p><h2 id="提交对-container-的修改到镜像中">提交对 Container 的修改到镜像中</h2><p>从刚才的操作我们可以看出，每次通过镜像生成一个  Container 后，都会给它赋予一个独立的 Container ID，所有对 Container 的操作（增加文件、删除文件，安装程序等），都会应用到 Container 里，<strong>但不会对镜像产生任何影响</strong>，也就是说，如果下次再次从镜像重新 <code>run</code> 一个 Container，那对之前 Container 所做的改动，在新的 Container 中不会生效。</p><p>之前有提过 Docker 的高度可定制化，就是本章将阐述的内容，即将 Container 的改动应用到镜像中，这样下次重新运行  Container，或者换了一台电脑将镜像重新 <code>pull</code> 回来，之前的运行环境都能够直接复原。</p><p>大致流程上如下：</p><ol><li>注册一个  <a href="https://hub.docker.com/">Docker Hub</a> 账号；</li><li>在自己的 Profile 页面中，<a href="https://hub.docker.com/add/repository/?namespace=xuqingkuang">Create Repository</a> 建立一个新的镜像仓库（和  Git  很像）；</li><li>在  Docker Quickstart Terminal 中通过 <code>login</code> 命令登录 <a href="https://hub.docker.com/">Docker Hub</a>；</li><li>然后使用 <code>commit</code> 命令，将对 Container 的修改应用到刚才新建的 Repository，其实该 Repository 就是存放镜像的地方；</li><li>将更改后的镜像 <code>push</code> 到 <a href="https://hub.docker.com/">Docker Hub</a>  上。</li></ol><p>我这儿已经有账号了，就直接从第三步起了，<code>commit</code> 之后可以注意一下返回的 digest，然后通过 <code>images</code> 就会发现，最后一次修改已经变成了新的 digest 了。</p><p><code>$ docker push</code></p><p><img src="http://img2.tbcdn.cn/L1/461/1/bf66cb8322e466fedaabd8527f0c042d044a1844.png" alt="docker_push" /></p><h1 id="结尾">结尾</h1><p>看完本文后，运行起了自己的第一个 Docker 容器是否感到一阵激动，是否被 Docker 的便利性所折服？</p><p>但是这只是 Docker 的虚拟机用法，这样已经可以将您的服务器环境和操作系统隔离，但这样做会造成镜像越来越大，部署起来越来越慢，Docker 既然被称为“应用容器”，<a href="https://hub.docker.com/explore/">Docker Hub</a> 上提供了大量的服务镜像，其实没打算让用户进入 Container 的 Shell 然后手工装服务器软件，<a href="https://hub.docker.com/explore/">Docker Hub</a> 上的 Linux 发行版镜像，更多地是让用户体验一下该发行版，</p><p>那么如何使用才更加正确的呢？这里需要交代一个 Docker 理念，即“服务最小化、细分化”，最小化便于服务的更新升级，细分化降低服务间的耦合性，不会因为升级一个服务而重新更新整个镜像。</p><p>下一篇中，将介绍贯彻这两个理念所提供的两个工具：</p><ol><li>负责自定义编译镜像的 Dockerfile；</li><li>能够将应用和多个服务整合起来，为应用提供服务的 Docker Compose。</li></ol><p>如欲知更多内容，请参考下一篇<a href="http://www.atatech.org/articles/50139">《在 Windows 上部署基于 Docker 的 Web 服务器环境进阶 - Dockerfile 和 Docker Compose》</a>。</p>]]>
                    </description>
                    <pubDate>Tue, 12 Jul 2016 14:00:45 CST</pubDate>
                </item>
    </channel>
</rss>