<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>梦翼坊</title>
  
  <subtitle>给梦想一双翅膀</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://xiangfa.org/"/>
  <updated>2023-11-21T05:42:32.553Z</updated>
  <id>https://xiangfa.org/</id>
  
  <author>
    <name>子丶言</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Mac 下配置 Aria2</title>
    <link href="https://xiangfa.org/2021/12/configure-aria2-under-mac/"/>
    <id>https://xiangfa.org/2021/12/configure-aria2-under-mac/</id>
    <published>2021-12-22T13:00:49.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>Aria2 是一款自由、跨平台命令行界面的下载管理器，该软件根据 GPLv2 许可证进行分发。支持的下载协议有：HTTP、HTTPS、FTP、Bittorrent 和 Metalink。</p><a id="more"></a><h2 id="安装和设置-Aria2"><a href="#安装和设置-Aria2" class="headerlink" title="安装和设置 Aria2"></a>安装和设置 Aria2</h2><pre><code class="line-numbers language-bash"># 使用 Homebrew 安装 aria2brew install aria2# 创建配置文件aria2.conf和空对话文件aria2.sessionmkdir ~/.aria2 &amp;&amp; cd ~/.aria2touch aria2.conftouch aria2.session</code></pre><p>编辑配置文件<code>aria2.conf</code></p><pre><code class="line-numbers language-bash">## &#39;#&#39;开头为注释内容, 选项都有相应的注释说明, 根据需要修改 #### 被注释的选项填写的是默认值, 建议在需要修改时再取消注释  #### 文件保存相关 ### 文件的保存路径(可使用绝对路径或相对路径), 默认: 当前启动位置dir=${HOME}/Downloads# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16Mdisk-cache=32M# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc# 预分配所需时间: none &lt; falloc ? trunc &lt; prealloc# falloc和trunc则需要文件系统和内核支持# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项#file-allocation=none# 断点续传continue=true## 下载连接相关 ### 最大同时下载任务数, 运行时可修改, 默认:5#max-concurrent-downloads=5# 同一服务器连接数, 添加时可指定, 默认:1, 最大值16max-connection-per-server=5# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载min-split-size=10M# 单个任务最大线程数, 添加时可指定, 默认:5#split=5# 分片选择算法,有助于视频的边下边播同时兼顾减少建立连接的次数stream-piece-selector=geom# 整体下载速度限制, 运行时可修改, 默认:0#max-overall-download-limit=0# 单个任务下载速度限制, 默认:0#max-download-limit=0# 整体上传速度限制, 运行时可修改, 默认:0#max-overall-upload-limit=0# 单个任务上传速度限制, 默认:0#max-upload-limit=0# 禁用IPv6, 默认:false#disable-ipv6=true# 连接超时时间, 默认:60timeout=60# 最大重试次数, 设置为0表示不限制重试次数, 默认:5max-tries=5# 设置重试等待的秒数, 默认:0#retry-wait=0## 进度保存相关 ### 日志文件log-level=noticelog=${HOME}/.aria2/aria2.log# 从会话文件中读取下载任务# 需提前创建一个空文件否则会报错input-file=${HOME}/.aria2/aria2.session# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件save-session=${HOME}/.aria2/aria2.session# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0save-session-interval=60# 强制保存会话, 即使任务已经完成, 默认:false# 较新的版本开启后会在任务完成后依然保留.aria2文件#force-save=true## RPC相关设置 ### 启用RPC, 默认:falseenable-rpc=true# 允许所有来源, 默认:falserpc-allow-origin-all=true# 允许非外部访问, 默认:falserpc-listen-all=true# RPC监听端口, 端口被占用时可以修改, 默认:6800rpc-listen-port=6800# 设置的RPC授权令牌# 此处使用`openssl rand -base64 32`命令生成&lt;TOKEN&gt;rpc-secret=&lt;TOKEN&gt;# 是否启用 RPC 服务的 SSL/TLS 加密,# 启用加密后 RPC 服务需要使用 https 或者 wss 协议连接#rpc-secure=true# 在 RPC 服务中启用 SSL/TLS 加密时的证书文件,# 使用 PEM 格式时，您必须通过 --rpc-private-key 指定私钥#rpc-certificate=/path/to/certificate.pem# 在 RPC 服务中启用 SSL/TLS 加密时的私钥文件#rpc-private-key=/path/to/certificate.key## HTTP 设置 ### 自定义 User Agentuser-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.85 Safari/537.36## BT/PT下载相关 ### 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:truefollow-torrent=true# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999listen-port=6881-6999# 单个种子最大连接数, 默认:55#bt-max-peers=55### DHT 功能, 仅对 BT 生效, PT 无效#### 打开 DHT (IPv4) 功能enable-dht=true# 打开 DHT (IPv6) 功能enable-dht6=true# DHT网络监听端口, 默认:6881-6999dht-listen-port=6881-6999# 本地节点查找bt-enable-lpd=true# 种子交换enable-peer-exchange=true# DHT (IPv4) 路由表文件路径dht-file-path=${HOME}/.aria2/dht.dat# DHT (IPv6) 路由表文件路径dht-file-path6=${HOME}/.aria2/dht6.dat# 客户端伪装, PT需要peer-id-prefix=-UT341-peer-agent=uTorrent/341(109279400)(30888)# 同一服务器连接数# 每个种子限速, 对少种的PT很有用, 默认:50K#bt-request-peer-speed-limit=50K# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0seed-ratio=0# BT校验相关, 默认:true#bt-hash-check-seed=true# 继续之前的BT任务时, 无需再次校验, 默认:falsebt-seed-unverified=true# 保存磁力链接元数据为种子文件(.torrent文件), 默认:falsebt-save-metadata=true# BT 服务器地址# 逗号分隔的 BT 服务器地址. 如果服务器地址在 --bt-exclude-tracker 选项中, 其将不会生效.bt-tracker=udp://tracker.coppersurfer.tk:6969/announce,udp://tracker.leechers-paradise.org:6969/announce,udp://tracker.opentrackr.org:1337/announce,udp://p4p.arenabg.com:1337/announce,udp://9.rarbg.to:2710/announce,udp://9.rarbg.me:2710/announce,udp://tracker.internetwarriors.net:1337/announce,udp://exodus.desync.com:6969/announce,udp://tracker.tiny-vps.com:6969/announce,udp://tracker.moeking.me:6969/announce,udp://retracker.lanta-net.ru:2710/announce,udp://open.stealth.si:80/announce,udp://open.demonii.si:1337/announce,udp://tracker.torrent.eu.org:451/announce,udp://tracker.cyberia.is:6969/announce,udp://denis.stalker.upeer.me:6969/announce,udp://tracker3.itzmx.com:6961/announce,udp://ipv4.tracker.harry.lu:80/announce,udp://valakas.rollo.dnsabr.com:2710/announce,udp://tracker.nyaa.uk:6969/announce,udp://retracker.netbynet.ru:2710/announce,udp://opentor.org:2710/announce,udp://explodie.org:6969/announce,http://explodie.org:6969/announce,udp://zephir.monocul.us:6969/announce,udp://xxxtor.com:2710/announce,udp://tracker.zum.bi:6969/announce,udp://tracker.yoshi210.com:6969/announce,udp://tracker.uw0.xyz:6969/announce,udp://tracker.sbsub.com:2710/announce,udp://tracker.lelux.fi:6969/announce,udp://tracker.iamhansen.xyz:2000/announce,udp://tracker.filemail.com:6969/announce,udp://tracker.dler.org:6969/announce,udp://retracker.sevstar.net:2710/announce,udp://retracker.akado-ural.ru:80/announce,udp://open.nyap2p.com:6969/announce,udp://chihaya.toss.li:9696/announce,udp://bt2.archive.org:6969/announce,udp://bt1.archive.org:6969/announce,udp://bt.okmp3.ru:2710/announce,https://tracker.nanoha.org:443/announce,http://tracker.torrentyorg.pl:80/announce,http://tracker.opentrackr.org:1337/announce,http://tracker.internetwarriors.net:1337/announce,http://tracker.bt4g.com:2095/announce,http://t.nyaatracker.com:80/announce,http://retracker.sevstar.net:2710/announce,http://pow7.com:80/announce,http://mail2.zelenaya.net:80/announce,http://h4.trakx.nibba.trade:80/announce,udp://tracker4.itzmx.com:2710/announce,udp://tracker2.itzmx.com:6961/announce,udp://tracker.zerobytes.xyz:1337/announce,udp://tracker.swateam.org.uk:2710/announce,udp://tr.bangumi.moe:6969/announce,udp://qg.lorzl.gq:2710/announce,udp://opentracker.i2p.rocks:6969/announce,udp://bt2.54new.com:8080/announce,https://tracker.opentracker.se:443/announce,https://tracker.lelux.fi:443/announce,http://www.loushao.net:8080/announce,http://vps02.net.orel.ru:80/announce,http://tracker4.itzmx.com:2710/announce,http://tracker3.itzmx.com:6961/announce,http://tracker2.itzmx.com:6961/announce,http://tracker1.itzmx.com:8080/announce,http://tracker01.loveapp.com:6789/announce,http://tracker.zerobytes.xyz:1337/announce,http://tracker.yoshi210.com:6969/announce,http://tracker.nyap2p.com:8080/announce,http://tracker.lelux.fi:80/announce,http://tracker.bz:80/announce,http://opentracker.i2p.rocks:6969/announce,http://open.acgnxtracker.com:80/announce# BT 排除服务器地址bt-exclude-tracker=# 启用后台进程daemon=false# 部分事件hook, 调用第三方命令:/path/to/command# BT下载完成(如有做种将包含做种，如需调用请务必确定设定完成做种条件)on-bt-download-complete=${HOME}/.aria2/download-complete-hook.sh# 下载完成on-download-complete=${HOME}/.aria2/download-complete-hook.sh# 下载错误on-download-error=# 代理 仅支持 HTTP 协议#all-proxy=http://127.0.0.1:1087</code></pre><ul><li><a href="#file-aria2-template-conf">设置文件详解</a></li><li><a href="#file-aria2-conf">自用设置文件</a></li></ul><p>本人设置文件:</p><ul><li>默认开启 RPC 模式</li><li>已设置RPC授权令牌, 详见设置文件注释</li><li>已经添加 BT tracker，更多详见 <a href="https://trackerslist.com/#/zh" target="_blank" rel="noopener">XIU2/TrackersListCollection</a></li></ul><h2 id="设置为macOS的开机启动"><a href="#设置为macOS的开机启动" class="headerlink" title="设置为macOS的开机启动"></a>设置为macOS的开机启动</h2><blockquote><p>参考: <a href="https://www.jianshu.com/p/eee8a7de179c" target="_blank" rel="noopener">控制macOS的开机启动</a></p></blockquote><h3 id="创建用户启动文件"><a href="#创建用户启动文件" class="headerlink" title="创建用户启动文件"></a>创建用户启动文件</h3><pre><code class="line-numbers language-bash">touch ~/Library/LaunchAgents/aria2.plist</code></pre><p>写入如下</p><pre><code class="line-numbers language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;&lt;plist version=&quot;1.0&quot;&gt;&lt;dict&gt;    &lt;key&gt;KeepAlive&lt;/key&gt;    &lt;true/&gt;    &lt;key&gt;Label&lt;/key&gt;    &lt;string&gt;aria2&lt;/string&gt;    &lt;key&gt;ProgramArguments&lt;/key&gt;    &lt;array&gt;        &lt;string&gt;/usr/local/bin/aria2c&lt;/string&gt;    &lt;/array&gt;    &lt;key&gt;RunAtLoad&lt;/key&gt;    &lt;true/&gt;    &lt;key&gt;WorkingDirectory&lt;/key&gt;    &lt;string&gt;${HOME}/Downloads&lt;/string&gt;&lt;/dict&gt;&lt;/plist&gt;</code></pre><blockquote><p>注意: 修改<code>WorkingDirectory</code>目录</p></blockquote><pre><code class="line-numbers language-bash"># 检查plist语法是否正确plutil ~/Library/LaunchAgents/aria2.plist# 修改文件权限chmod 644 ~/Library/LaunchAgents/aria2.plist</code></pre><h3 id="添加并启用自启动项"><a href="#添加并启用自启动项" class="headerlink" title="添加并启用自启动项"></a>添加并启用自启动项</h3><pre><code class="line-numbers language-bash"># 添加自启动项: aria2launchctl load ~/Library/LaunchAgents/aria2.plist# 删除自启动项: aria2launchctl unload ~/Library/LaunchAgents/aria2.plist# 启动服务: aria2launchctl start aria2# 停止服务: aria2launchctl stop aria2</code></pre><blockquote><p>更多<code>launchctl</code>使用方法, 详见命令手册<br><br>可使用<code>killall aria2c</code> 结束进程, 并会自动重启进程</p></blockquote><h2 id="添加自动更新BT-tracker功能"><a href="#添加自动更新BT-tracker功能" class="headerlink" title="添加自动更新BT tracker功能"></a>添加自动更新<code>BT tracker</code>功能</h2><h3 id="创建trackers-list-aria2-sh脚本"><a href="#创建trackers-list-aria2-sh脚本" class="headerlink" title="创建trackers-list-aria2.sh脚本"></a>创建<code>trackers-list-aria2.sh</code>脚本</h3><blockquote><p>参考: <a href="https://www.feng.ee/aria2-trackers-auto-update.html" target="_blank" rel="noopener">Aria2 bt-tracker跟踪服务器列表自动更新</a></p></blockquote><p>脚本内容如下: </p><pre><code class="line-numbers language-bash">#!/bin/bash#trackers-list-aria2.sh# aria2 设置文件路径CONF=${HOME}/.aria2/aria2.conf#设置选择的 trackerlist （可选 all_aria2.txt, best_aria2.txt, http_aria2.txt）trackerfile=all_aria2.txt#downloadfile=https://raw.githubusercontent.com/ngosang/trackerslist/master/${trackerfile}downloadfile=https://trackerslist.com/${trackerfile}list=$(curl -fsSL ${downloadfile})if ! grep -q &quot;bt-tracker&quot; &quot;${CONF}&quot; ; then    echo -e &quot;\033[34m==&gt; 添加 bt-tracker 服务器信息......\033[0m&quot;    echo -e &quot;\nbt-tracker=${list}&quot; &gt;&gt; &quot;${CONF}&quot;else    echo -e &quot;\033[34m==&gt; 更新 bt-tracker 服务器信息.....\033[0m&quot;    sed -i &#39;&#39; &quot;s@bt-tracker.*@bt-tracker=${list}@g&quot; &quot;${CONF}&quot;fi## 重启 aria2 服务echo -e &quot;\033[34m==&gt; 停止 aria2 服务......\033[0m&quot;launchctl stop aria2echo -e &quot;\033[34m==&gt; 启动 aria2 服务......\033[0m&quot;launchctl start aria2</code></pre><p>脚本放置到 <code>~/.aria2/</code>，并设置运行权限:</p><pre><code class="line-numbers language-bash">chmod +x ~/.aria2/trackers-list-aria2.sh</code></pre><h3 id="设置任务计划程序-实现自动更新"><a href="#设置任务计划程序-实现自动更新" class="headerlink" title="设置任务计划程序 实现自动更新"></a>设置任务计划程序 实现自动更新</h3><blockquote><p>参考:</p><ul><li><a href="https://www.feng.ee/aria2-trackers-auto-update.html" target="_blank" rel="noopener">Aria2 bt-tracker跟踪服务器列表自动更新</a></li><li><a href="https://blog.csdn.net/ty_hf/article/details/72354230" target="_blank" rel="noopener">mac下crontab执行定时脚本</a></li></ul></blockquote><p>编译当前用户任务计划</p><pre><code class="line-numbers language-bash">crontab -e</code></pre><p>在打开的<code>vi</code>中 键入如下, 并使用<code>:wq</code>命令保存退出, 可用<code>crontab -l</code>查看当前用户任务计划</p><pre><code>0 18 * * * ~/.aria2/trackers-list-aria2.sh</code></pre><p>或者 直接</p><pre><code class="line-numbers language-bash">(crontab -l 2&amp;&gt; /dev/null; echo &quot;0 18 * * * ~/.aria2/trackers-list-aria2.sh&quot;) | crontab</code></pre><blockquote><p>以上表示: 每天下午 6 点自动更新<code>BT tracker</code>(并重启<code>aria2</code>服务)<br><br>更多<code>crontab</code>时间的设定详见: <a href="https://user-images.githubusercontent.com/7850715/87239248-94674100-c43f-11ea-8445-1d084be61436.png" target="_blank" rel="noopener">这里</a></p></blockquote><blockquote><p>取消计划任务</p><pre><code class="line-numbers language-bash">crontab -e</code></pre><p>然后手动删除, 或者</p><pre><code class="line-numbers language-bash">crontab -l 2&amp;&gt; /dev/null| sed &quot;/trackers-list-aria2.sh/d&quot; | crontab</code></pre></blockquote><h2 id="添加下载通知"><a href="#添加下载通知" class="headerlink" title="添加下载通知"></a>添加下载通知</h2><blockquote><p>参考: <a href="https://github.com/maboloshi/Blog/blob/hexo/source/_posts/05.%20macOS%E4%B8%8B%20%E7%BB%99aria2%20RPC%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA%E4%B8%8B%E8%BD%BD%E9%80%9A%E7%9F%A5.md" target="_blank" rel="noopener">macOS下 给aria2 RPC添加一个下载通知</a></p></blockquote><p>最终效果：当下载完成会在屏幕右上角弹出一个提示框显示具体下载完成的文件名，同时中文语音播报：“有个文件下载完成，请查收！”</p><p><img src="https://user-images.githubusercontent.com/7850715/74525348-cc159f80-4f18-11ea-84bd-56be79bf3b0a.png" alt="macOS 下aria2 提示框实例"></p><h3 id="创建download-complete-hook-sh脚本"><a href="#创建download-complete-hook-sh脚本" class="headerlink" title="创建download-complete-hook.sh脚本"></a>创建<code>download-complete-hook.sh</code>脚本</h3><blockquote><p>参考:</p><ul><li><a href="https://aria2.github.io/manual/en/html/aria2c.html#event-hook" target="_blank" rel="noopener">aria2 event-hook</a></li><li><a href="https://code-maven.com/display-notification-from-the-mac-command-line" target="_blank" rel="noopener">Display notification from the Mac command line</a></li><li><a href="https://www.zixi.org/archives/notification_on_macos.html" target="_blank" rel="noopener">在mac命令行执行显示通知</a></li><li><a href="https://stackoverflow.com/a/17243326/7488424" target="_blank" rel="noopener">Pass in variable from shell script to applescript</a></li></ul></blockquote><p>脚本内容如下: </p><pre><code class="line-numbers language-bash">#!/bin/sh# 给aria2 RPC添加一个下载完成通知 for macOS# 最终效果：当下载完成会在屏幕右上角弹出一个提示框显示具体下载完成的文件名，# 同时中文语音播报：“有个文件下载完成，请查收！”# 变量 3 表示下载完成文件的路径# 具体提示框设置可参考`https://code-maven.com/display-notification-from-the-mac-command-line`。# 不支持设置自定义图标fname=`basename $3`osascript &lt;&lt;EOFdisplay notification &quot;$fname 已经下载完成！&quot; with title &quot;【下载完成】&quot;say &quot;有个文件下载完成，请查收！&quot;EOF</code></pre><p>将脚本放置到 <code>~/.aria2/</code>，并设置运行权限:</p><pre><code class="line-numbers language-bash">chmod +x ~/.aria2/download-complete-hook.sh</code></pre><h3 id="添加-Hook-设置"><a href="#添加-Hook-设置" class="headerlink" title="添加 Hook 设置"></a>添加 Hook 设置</h3><blockquote><p>参考:</p><ul><li><a href="https://aria2.github.io/manual/en/html/aria2c.html#event-hook" target="_blank" rel="noopener">https://aria2.github.io/manual/en/html/aria2c.html#event-hook</a></li></ul></blockquote><p>在 aria2 设置文件<code>.aria2.conf</code>加入如下：</p><pre><code class="line-numbers language-bash"># BT下载完成(如有做种将包含做种，如需调用请务必确定设定完成做种条件)on-bt-download-complete=${HOME}/.aria2/download-complete-hook.sh# 下载完成on-download-complete=${HOME}/.aria2/download-complete-hook.sh</code></pre><h2 id="Aria2-web-UI"><a href="#Aria2-web-UI" class="headerlink" title="Aria2 web UI"></a>Aria2 web UI</h2><p>无需安装，直接使用浏览器打开: <a href="http://ariang.mayswind.net/latest/" target="_blank" rel="noopener">AriaNg版 UI</a></p><h3 id="PRC-设置"><a href="#PRC-设置" class="headerlink" title="PRC 设置"></a>PRC 设置</h3><blockquote><p>根据 aria2 配置文件中的 PRC 相关设置项进行设置</p></blockquote><img width="839" alt="AriaNg_PRC_设置" src="https://user-images.githubusercontent.com/7850715/73242932-3353f580-419e-11ea-8059-dc9490a5f595.png"><h2 id="安装浏览器下载插件"><a href="#安装浏览器下载插件" class="headerlink" title="安装浏览器下载插件"></a>安装浏览器下载插件</h2><p><a href="https://chrome.google.com/webstore/detail/aria2-for-chrome/mpkodccbngfoacfalldjimigbofkhgjn" target="_blank" rel="noopener">Aria2 for Chrome插件</a></p><ul><li>内置一个离线 AriaNg版 UI</li><li>整合右键下载菜单</li></ul><blockquote><p>内置的离线 AriaNg版也需要设置PRC，否则无法“导出到 ARIA2 RPC”。</p></blockquote><hr><h2 id="以下内容仅供参考-现阶段实用性不大"><a href="#以下内容仅供参考-现阶段实用性不大" class="headerlink" title="以下内容仅供参考 现阶段实用性不大"></a><strong>以下内容仅供参考 现阶段实用性不大</strong></h2><h2 id="百度云下载"><a href="#百度云下载" class="headerlink" title="百度云下载"></a>百度云下载</h2><blockquote><p>方案来自: <a href="https://www.runningcheese.com/baiduyun" target="_blank" rel="noopener">https://www.runningcheese.com/baiduyun</a></p></blockquote><h3 id="安装-网盘助手"><a href="#安装-网盘助手" class="headerlink" title="安装 网盘助手"></a>安装 <a href="http://pan.newday.me/" target="_blank" rel="noopener"><code>网盘助手</code></a></h3><h4 id="浏览器插件版"><a href="#浏览器插件版" class="headerlink" title="浏览器插件版"></a>浏览器插件版</h4><p>直接进入<a href="http://pan.newday.me/" target="_blank" rel="noopener">网盘助手</a>主页,按浏览器不同下载并安装对应的插件</p><h4 id="脚本版"><a href="#脚本版" class="headerlink" title="脚本版"></a>脚本版</h4><p><a href="https://greasyfork.org/zh-CN/scripts/378301" target="_blank" rel="noopener">网盘助手脚本</a>，需要通过拓展 <a href="https://violentmonkey.github.io/get-it/" target="_blank" rel="noopener">Violentmonkey</a> 或者 <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener">Tampermonkey</a> 等脚本管理器来启用</p><h3 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h3><blockquote><p>配合<a href="https://chrome.google.com/webstore/detail/aria2-for-chrome/mpkodccbngfoacfalldjimigbofkhgjn" target="_blank" rel="noopener">Aria2 for Chrome插件</a>使用</p></blockquote><ol><li>选择要下载的文件，点击页面里的 “生成链接” 来获取加速下载地址。</li><li>使用鼠标右键点击链接，选择“导出到 ARIA2 RPC”，然后确定下载。</li></ol><p><img src="https://user-images.githubusercontent.com/7850715/73240648-81192f80-4197-11ea-8154-653c7b95adc1.gif" alt="网盘助手配合Aria2_for_Chrome"></p><blockquote><p>图片出处:<a href="https://www.runningcheese.com/baiduyun" target="_blank" rel="noopener">https://www.runningcheese.com/baiduyun</a></p></blockquote><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://www.damocles.me/2019/06/16/mac-aria2-configure/" target="_blank" rel="noopener">Mac下配置Aria2来代替迅雷</a></li><li><a href="http://www.senra.me/aria2-conf-file-parameters-translation-and-explanation/" target="_blank" rel="noopener">Aria2配置文件参数翻译详解</a></li><li><a href="http://ivo-wang.github.io/2019/04/18/%E5%85%B3%E4%BA%8Earia2%E6%9C%80%E5%AE%8C%E6%95%B4%E7%9A%84%E4%B8%80%E7%AF%87/" target="_blank" rel="noopener">关于aria2最完整的一篇</a></li><li><a href="https://aria2.github.io/manual/en/html/aria2c.html#aria2-conf" target="_blank" rel="noopener">aria2c手册</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Aria2 是一款自由、跨平台命令行界面的下载管理器，该软件根据 GPLv2 许可证进行分发。支持的下载协议有：HTTP、HTTPS、FTP、Bittorrent 和 Metalink。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Aria2" scheme="https://xiangfa.org/categories/Aria2/"/>
    
    
      <category term="Aria2" scheme="https://xiangfa.org/tags/Aria2/"/>
    
      <category term="Mac" scheme="https://xiangfa.org/tags/Mac/"/>
    
  </entry>
  
  <entry>
    <title>qiankun 微前端方案适配 HashRouter 模式及 TypeScript 项目</title>
    <link href="https://xiangfa.org/2021/01/qiankun-micro-front-end-solution-adapts-to-hash-router/"/>
    <id>https://xiangfa.org/2021/01/qiankun-micro-front-end-solution-adapts-to-hash-router/</id>
    <published>2021-01-25T09:43:08.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>微前端的概念在近几年不断“升温”，越来越多的公司尝试通过微前端方案来解决“巨石”应用(<a href="https://www.youtube.com/watch?v=pU1gXA0rfwc" target="_blank" rel="noopener">Frontend Monolith</a>)的问题。微前端的六种常见实现方案不是本文的讨论点，感兴趣的朋友可以阅读<a href="https://xiaomi-info.github.io/2020/04/14/fe-microfrontends-practice/" target="_blank" rel="noopener">微前端在小米 CRM 系统的实践</a>这篇文章。在比较文中的各类实现方案后，我确定了使用 <a href="https://qiankun.umijs.org/zh/guide" target="_blank" rel="noopener">Qiankun</a> 来实现前端微服务化，但在实现过程中遇到了一些困难，本文主要描述问题及解决方案。</p><a id="more"></a><h3 id="问题一：支持-HashRouter-模式"><a href="#问题一：支持-HashRouter-模式" class="headerlink" title="问题一：支持 HashRouter 模式"></a>问题一：支持 HashRouter 模式</h3><p>Qiankun 官网的适配方案使用了 BrowserRouter 模式，但对于如何适配 HashRouter 模式并没有展开描述，不管我如何调整，路由似乎并没有被正确的激活，这曾让我数次决定放弃 qiankun 微前端方案，但 Single-Spa 的适配调整又把我再次劝退…<br>我一开始一直以为是在 HashRouter 的配置上出了问题，尝试将 basename 设为 <code>window.__POWERED_BY_QIANKUN__ ? &#39;#/demo&#39; : &#39;#/&#39;</code>，依然无法解决项目加载的问题。从项目的错误提示中，我摸到了些许蛛丝马迹，将问题从子应用回归到主应用的配置上，原来 <code>activeRule</code> 是支持 Hash 前缀的，比如 <code>activeRule: &#39;#/demo&#39;</code>，而这一点官网并没有任何提示！至此，HashRouter 的适配问题迎刃而解，正所谓“山穷水复疑无路，柳暗花明又一村”。</p><pre><code class="line-numbers language-ts">import { registerMicroApps, start } from &#39;qiankun&#39;registerMicroApps([  {    name: &#39;reactApp&#39;,    entry: &#39;//localhost:3000&#39;,    container: &#39;#container&#39;,    activeRule: &#39;#/demo&#39;,  },])// 启动 qiankunstart()</code></pre><h3 id="问题二：适配-TypeScript-项目"><a href="#问题二：适配-TypeScript-项目" class="headerlink" title="问题二：适配 TypeScript 项目"></a>问题二：适配 TypeScript 项目</h3><p>官方的文档是基于 JavaScript 写的，如果直接将 JavaScript 代码复制到 TypeScript 项目时，你将会收获一堆的错误提示，比如以下适配代码简直是 TypeScript 项目的“灾难”！</p><pre><code class="line-numbers language-js">if (window.__POWERED_BY_QIANKUN__) {  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}</code></pre><p>以上代码的问题在于，一个全局变量未定义，两个 <code>window</code> 上的变量未定义，对于这类 “Dirty Code”，你可以通过补充定义进行修复，具体代码如下：</p><pre><code class="line-numbers language-ts">interface Window {  __POWERED_BY_QIANKUN__?: string  __INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string}declare let __webpack_public_path__: string | undefined</code></pre><p><strong>注意</strong>：以上代码最好放到全局引入的 TypeScript 定义文件中，比如 <code>types.d.ts</code> 或 <code>react-app-env.d.ts</code> 这类文件中。</p><h3 id="问题三：如何适配-customize-cra-插件"><a href="#问题三：如何适配-customize-cra-插件" class="headerlink" title="问题三：如何适配 customize-cra 插件"></a>问题三：如何适配 customize-cra 插件</h3><p>Qiankun 官网对 React 项目的适配方案中对 webpack 配置调整提到了使用 <code>@rescripts/cli</code> 这个插件，但对于老项目而言调整 webpack 插件显然存在很大的项目风险。我司的项目使用了 <code>react-app-rewired + customize-cra</code> 的方案，但网上似乎并没有针对这个问题的具体解决方案。因为我之前写过些许自定义的 customize-cra plugin，因此这个问题相对容易解决，具体实现代码如下：</p><pre><code class="line-numbers language-js">const pkg = require(&#39;./package.json&#39;)const { override, overrideDevServer } = require(&#39;customize-cra&#39;)// 适配 qiankun 微前端逻辑const adaptMicroApp = () =&gt; {  return (config) =&gt; {    // 也可以不使用 pkg.name，手动定义    config.output.library = `${pkg.name}_[name]`    config.output.libraryTarget = &#39;umd&#39;    config.output.jsonpFunction = `webpackJsonp_${pkg.name}_[name]`    config.output.globalObject = &#39;window&#39;    return config  }}// 开启项目跨域模式const adaptCors = () =&gt; {  return (config) =&gt; {    // 关闭主机检查，使微应用可以被 fetch    config.disableHostCheck = true    // 配置跨域请求头，解决开发环境的跨域问题    config.headers = {      &#39;Access-Control-Allow-Origin&#39;: &#39;*&#39;,    }    // 如果你的路由方案是 BrowserRouter，需要配置 history 模式    // config.historyApiFallback = true    return config  }}module.exports = {  webpack: override(    // 适配 qiankun 微前端逻辑    adaptMicroApp()  ),  devServer: overrideDevServer(    // 开启项目跨域模式    adaptCors()  ),}</code></pre><p>以上几个就是我目前遇到的比较难解的几个问题及我的解决方案，希望能给你带来些许帮助！</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;微前端的概念在近几年不断“升温”，越来越多的公司尝试通过微前端方案来解决“巨石”应用(&lt;a href=&quot;https://www.youtube.com/watch?v=pU1gXA0rfwc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Frontend Monolith&lt;/a&gt;)的问题。微前端的六种常见实现方案不是本文的讨论点，感兴趣的朋友可以阅读&lt;a href=&quot;https://xiaomi-info.github.io/2020/04/14/fe-microfrontends-practice/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;微前端在小米 CRM 系统的实践&lt;/a&gt;这篇文章。在比较文中的各类实现方案后，我确定了使用 &lt;a href=&quot;https://qiankun.umijs.org/zh/guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Qiankun&lt;/a&gt; 来实现前端微服务化，但在实现过程中遇到了一些困难，本文主要描述问题及解决方案。&lt;/p&gt;
    
    </summary>
    
    
      <category term="qiankun" scheme="https://xiangfa.org/categories/qiankun/"/>
    
    
      <category term="qiankun" scheme="https://xiangfa.org/tags/qiankun/"/>
    
  </entry>
  
  <entry>
    <title>Vue 中的 AST 与虚拟 Dom</title>
    <link href="https://xiangfa.org/2021/01/AST-and-virtual-dom-in-vue/"/>
    <id>https://xiangfa.org/2021/01/AST-and-virtual-dom-in-vue/</id>
    <published>2021-01-14T06:07:52.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>在 Vue 的概念中有两个最基础也最难理解的概念 AST 与虚拟 Dom。总有很多新手把这两个概念搞混，以为 AST 就是虚拟 Dom，或认为虚拟 Dom 的结构才叫 AST，这其实都是错的。网上有不少半壶水的专家，张口闭口，三句不离这两个词汇，但他们真正能说明白的少之又少，说错的更不在少数…我不敢保证我的解答都是正确的，但我能保证在讲述过程中有查阅过相关资料，本文仅供参考。</p><a id="more"></a><h3 id="抽象语法树-Abstract-syntax-tree"><a href="#抽象语法树-Abstract-syntax-tree" class="headerlink" title="抽象语法树(Abstract syntax tree)"></a>抽象语法树(Abstract syntax tree)</h3><blockquote><p>在计算机科学中，称之为抽象语法树，或简称语法树，是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构，树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的，是因为这里的语法并不会表示出真实语法中出现的每个细节。<a href="https://zh.wikipedia.org/zh-cn/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9" target="_blank" rel="noopener">维基百科</a></p></blockquote><p>从维基百科的描述可知，AST(Abstract syntax tree) 是一种语法结构的抽象表示。<a href="https://astexplorer.net/" target="_blank" rel="noopener">AST Explorer</a> 这个网站可以帮你更加直观的展示 AST 的抽象结构。比如以下代码：</p><pre><code class="line-numbers language-html">&lt;div class=&quot;demo&quot;&gt;  &lt;span class=&quot;text&quot;&gt;hello world&lt;/span&gt;&lt;/div&gt;</code></pre><p>在 HTML 解析引擎下会生成负载的抽象结构：</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/426787a8c8fa353ea63efaf0abacc9f3.png" alt="AST"></p><p>看上去就想是一个大的结构化对象，非常接近于 JSON 的形式。你不需要去理解这个结构的意义，因为 AST 不是给开发人员阅读的，而是提供给编译器做语义分析用的。AST 的具体概念我这里不做展开，感兴趣的朋友可以阅读 <a href="https://segmentfault.com/a/1190000016231512" target="_blank" rel="noopener">AST抽象语法树——最基础的javascript重点知识，99%的人根本不了解</a> 这篇文章。</p><h3 id="虚拟-Dom-Virtual-Dom"><a href="#虚拟-Dom-Virtual-Dom" class="headerlink" title="虚拟 Dom(Virtual Dom)"></a>虚拟 Dom(Virtual Dom)</h3><p>虚拟 Dom 的概念并不是 Vue 项目最先提出的，但我们可以在 Vue 的官方文档中找到对应的解释：</p><pre><code>Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码：```jsreturn createElement(&#39;h1&#39;, this.blogTitle)````createElement` 到底会返回什么呢？其实不是一个实际的 DOM 元素。它更准确的名字可能是 `createNodeDescription`，因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点，包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”，也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。</code></pre><p>由此可知，Vue 项目的虚拟 Dom 是 Vue 整个 <code>VNode</code> 树的称呼。</p><h4 id="虚拟-Dom-的优势"><a href="#虚拟-Dom-的优势" class="headerlink" title="虚拟 Dom 的优势"></a>虚拟 Dom 的优势</h4><p>如果你经历过 JQuery 的时代，你应该知道 Dom 也是一个对象，那虚拟 Dom 与真实 Dom 相比有什么优势么？</p><p>在回答这个问题之前，我们先来看一下原生的 Dom 长啥样吧！我们可以使用以下代码打印一个纯粹的 div 的 Dom 结构:</p><pre><code class="line-numbers language-js">console.dir(document.createElement(&#39;div&#39;))</code></pre><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/b7cca713ac0c458635b463d74b81a574.png" alt="div 的 Dom 对象内容"></p><p>以上只截取了一小部分属性，你会发现这是一个相当复杂的对象，那虚拟 Dom 又长啥样呢？</p><pre><code class="line-numbers language-vue">&lt;template&gt;  &lt;div class=&quot;demo&quot;&gt;    &lt;span class=&quot;text&quot;&gt;hello world&lt;/span&gt;  &lt;/div&gt;&lt;/template&gt;</code></pre><p>以上代码的虚拟 Dom 的结构大概是这样的：</p><pre><code class="line-numbers language-js">{  tag: &#39;div&#39;  data: {    class: &#39;demo&#39;  },  children: [    {      tag: &#39;span&#39;,      data: {        class: &#39;text&#39;      }      text: &#39;hello world&#39;    }  ]}</code></pre><p>w(ﾟДﾟ)w，两者差距太大了，简直是柚子和橘子的区别。</p><p>虚拟 Dom 主要是用于渲染前的状态比较的，也就是我们经常提到的 diff 过程，不参与 UI 的展示，因此结构上异常精简。</p><p>回到之前的问题，虚拟 Dom 的主要优势其实就是结构简单！</p><h3 id="AST-与-虚拟-Dom-的关系"><a href="#AST-与-虚拟-Dom-的关系" class="headerlink" title="AST 与 虚拟 Dom 的关系"></a>AST 与 虚拟 Dom 的关系</h3><p>上面的内容分别介绍了 AST 与 虚拟 Dom 这两个概念，那这两个又有什么关系呢？</p><p><strong>这两者其实并没有关系</strong>╮(╯▽╰)╭。因为他们是在不同的阶段的产物。</p><p>AST 大多是在编译过程中出现，常规的开发框架并不涉及 AST 的概念。Vue 中 <code>.vue</code> 后缀的文件使用了 ”All in one file“ 的形式，不再是传统的 js 文件，其中 <code>&lt;template&gt;&lt;/template&gt;</code> 标签中的内容更包含了一些自定义的 vue 语法，比如 <code>v-if</code>、<code>v-for</code>，这些语法即不属于 js 规范，更不是 html 的语法，常规的 js 引擎无法处理，因此在交给 js 引擎运行之前，需要转换成标准的 js 语法，这时候就轮到 AST 出场了。vue-cli 会将模板代码解析成 AST 语法结构，再转换成 js 代码，即我们常说的 <code>build</code> 过程。</p><p>虚拟 Dom 是在运行时（vue-runtime）出现的一种数据中间态。Vue 项目在每个 render 周期内都会产生一次虚拟 Dom，如果每次都使用虚拟 Dom 去重新渲染整个 UI，那么页面不可避免地会出现”闪屏“的现象。因此在 Vue 项目内部会先对新旧虚拟 Dom 进行一次 <code>diff</code> 操作，获取发生变化的节点内容，然后获取最小的改变内容进行 UI 渲染，这也是虚拟 Dom 比真实 Dom 高效的具体原因。所以那种虚拟 Dom 比真实 Dom 操作快的说法是错误的，因为虚拟 Dom 最终还是会渲染成真实 Dom，只是一个过程的先后罢了。</p><p>AST 是在编译过程中出现的，但项目运行时就不再有 AST 的概念了，而虚拟 Dom 是在 render 过程中出现的，主要用于比较 render 前后的数据结构变化。</p><p>在前端技术日新月异的时代，各类新技术、新名词不断涌现，你在使用过程中，需要知其然，而非人云亦云。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在 Vue 的概念中有两个最基础也最难理解的概念 AST 与虚拟 Dom。总有很多新手把这两个概念搞混，以为 AST 就是虚拟 Dom，或认为虚拟 Dom 的结构才叫 AST，这其实都是错的。网上有不少半壶水的专家，张口闭口，三句不离这两个词汇，但他们真正能说明白的少之又少，说错的更不在少数…我不敢保证我的解答都是正确的，但我能保证在讲述过程中有查阅过相关资料，本文仅供参考。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Vue" scheme="https://xiangfa.org/categories/Vue/"/>
    
    
      <category term="vue" scheme="https://xiangfa.org/tags/vue/"/>
    
      <category term="AST" scheme="https://xiangfa.org/tags/AST/"/>
    
      <category term="virtual dom" scheme="https://xiangfa.org/tags/virtual-dom/"/>
    
      <category term="虚拟 Dom" scheme="https://xiangfa.org/tags/%E8%99%9A%E6%8B%9F-Dom/"/>
    
      <category term="抽象语法树" scheme="https://xiangfa.org/tags/%E6%8A%BD%E8%B1%A1%E8%AF%AD%E6%B3%95%E6%A0%91/"/>
    
  </entry>
  
  <entry>
    <title>vue 项目极限优化，提升首屏访问体验</title>
    <link href="https://xiangfa.org/2020/12/improve-the-first-screen-access-experience-of-vue-project/"/>
    <id>https://xiangfa.org/2020/12/improve-the-first-screen-access-experience-of-vue-project/</id>
    <published>2020-12-28T09:53:47.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>自从带队开发公司的运营类项目的开发，这半年来似乎一直在关注 vue 项目的性能优化。作为公司的用户引流项目，活动的首屏加载时间长短会对用户转换率产生一些”蝴蝶效应“。该影响对于弱网络的用户群体的影响尤为明显。如果一个页面白屏时间超过 2.5s，那么用户很可能会认为页面存在问题而关闭页面，从而导致用户流失。</p><a id="more"></a><blockquote><p>项目的优化是一条无休止的路。</p></blockquote><p>常规的项目优化方式我在 <a href="/2020/06/optimize-package-size-of-vue-project/">优化 vue 项目包大小，提升首屏加载速度</a> 这篇文章中已经有所提及，可以优先阅读。</p><h2 id="首屏加载视觉提速"><a href="#首屏加载视觉提速" class="headerlink" title="首屏加载视觉提速"></a>首屏加载视觉提速</h2><p>何谓“视觉提速”？其实这是我自己造的的一个名词。我们经常会听到白屏时间这个名词，主要是只用户从打开网页到看到网页内容的时间。如果一个页面白屏时长超过 2.5s，那么用户很大概率会认为页面打不开而关闭页面，你再精彩的页面内容在用户看不到的前提下，都是“白费功夫”。所以你可以从网站找到很多有关首屏加载优化的方案。其中最常见的就是 SSR 的方案，但这个方案需要后端服务器配合，在公司没有强大运维能力支持的情况下，贸然尝试 SSR，你的项目很可能会受到“致命打击”。</p><p>那么除去 SSR 之外就没办法减少白屏时间么？</p><p>在思考这个问题之前，我们需要思考另外一个问题：<em>如果让你给 Vue 项目加 loading，你会怎么做？</em></p><p>是在 <code>App.vue</code> 这类入口文件上加一个 loading 组件么，如果你这么做，那你已经超过了全国 50% 的用户了。但你可知，Vue 项目在库函数载入完并初始化完成之前都是白屏状态，是不是很意外？</p><p>再问另一个问题：<em>你觉得从用户角度而言白屏时间是否就等于 vue 项目初始化完成之前的时间？</em></p><p>对于这个问题，如果你深入过群众你就会知道，我们跟用户的理解还是存在很大隔阂的。对于用户而言，只要看到屏幕上有内容，即便是最基本的 loading，也能让用户感到“安心”，至少不会认为页面打不开而关闭页面。</p><p>如果你能看懂以上论述，那么视觉提速就会比较好理解，我们可以在 vue 初始化之前就在 Html 页面埋入一个 loading，这类 loading 可以使用 css 实现，也可以考虑使用 svg、gif、静态占位图片甚至是一个背景色！总之，你需要让用户第一时间看到页面有内容。我个人比较推荐使用 svg，因为其他几种方式跟 svg 比都存在一些劣势。</p><pre><code class="line-numbers language-html">&lt;div id=&quot;app&quot;&gt;  &lt;div style=&quot;padding-top: 30vh; text-align: center&quot;&gt;    &lt;div style=&quot;margin: auto&quot;&gt;      &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; style=&quot;margin: auto; display: block;&quot;      width=&quot;200px&quot; height=&quot;200px&quot; viewBox=&quot;0 0 100 100&quot; preserveAspectRatio=&quot;xMidYMid&quot;&gt;        &lt;g transform=&quot;translate(20 50)&quot;&gt;          &lt;circle cx=&quot;0&quot; cy=&quot;0&quot; r=&quot;6&quot; fill=&quot;#31ca89&quot; transform=&quot;scale(0.00244898 0.00244898)&quot;&gt;            &lt;animateTransform attributeName=&quot;transform&quot; type=&quot;scale&quot; begin=&quot;-0.375s&quot; calcMode=&quot;spline&quot;              keySplines=&quot;0.3 0 0.7 1;0.3 0 0.7 1&quot; values=&quot;0;1;0&quot; keyTimes=&quot;0;0.5;1&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot;&gt;            &lt;/animateTransform&gt;          &lt;/circle&gt;        &lt;/g&gt;        &lt;g transform=&quot;translate(40 50)&quot;&gt;          &lt;circle cx=&quot;0&quot; cy=&quot;0&quot; r=&quot;6&quot; fill=&quot;#ef465d&quot; transform=&quot;scale(0.197344 0.197344)&quot;&gt;            &lt;animateTransform attributeName=&quot;transform&quot; type=&quot;scale&quot; begin=&quot;-0.25s&quot; calcMode=&quot;spline&quot;              keySplines=&quot;0.3 0 0.7 1;0.3 0 0.7 1&quot; values=&quot;0;1;0&quot; keyTimes=&quot;0;0.5;1&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot;&gt;            &lt;/animateTransform&gt;          &lt;/circle&gt;        &lt;/g&gt;        &lt;g transform=&quot;translate(60 50)&quot;&gt;          &lt;circle cx=&quot;0&quot; cy=&quot;0&quot; r=&quot;6&quot; fill=&quot;#ff9936&quot; transform=&quot;scale(0.537416 0.537416)&quot;&gt;            &lt;animateTransform attributeName=&quot;transform&quot; type=&quot;scale&quot; begin=&quot;-0.125s&quot; calcMode=&quot;spline&quot;              keySplines=&quot;0.3 0 0.7 1;0.3 0 0.7 1&quot; values=&quot;0;1;0&quot; keyTimes=&quot;0;0.5;1&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot;&gt;            &lt;/animateTransform&gt;          &lt;/circle&gt;        &lt;/g&gt;        &lt;g transform=&quot;translate(80 50)&quot;&gt;          &lt;circle cx=&quot;0&quot; cy=&quot;0&quot; r=&quot;6&quot; fill=&quot;#ffe700&quot; transform=&quot;scale(0.862077 0.862077)&quot;&gt;            &lt;animateTransform attributeName=&quot;transform&quot; type=&quot;scale&quot; begin=&quot;0s&quot; calcMode=&quot;spline&quot;              keySplines=&quot;0.3 0 0.7 1;0.3 0 0.7 1&quot; values=&quot;0;1;0&quot; keyTimes=&quot;0;0.5;1&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot;&gt;            &lt;/animateTransform&gt;          &lt;/circle&gt;        &lt;/g&gt;      &lt;/svg&gt;    &lt;/div&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><p><strong>注意：此处有一个细节，<code>id=&quot;app&quot;</code> 一般是 vue 项目的挂载 dom，vue 在 dom 挂载之后会清空该 dom 的内容，因此我们可以不用再额外考虑 loading 加载完成后的移除问题了~</strong></p><h2 id="资源加载可视化"><a href="#资源加载可视化" class="headerlink" title="资源加载可视化"></a>资源加载可视化</h2><p>我的项目采用了大量的 <a href="https://airbnb.design/lottie/" target="_blank" rel="noopener">lottie</a> 动画，而 <a href="https://github.com/airbnb/lottie-web" target="_blank" rel="noopener">lottie-web</a> 动画引擎库，对于网页来说简直是巨无霸般的存在！而且 lottie 脚本文件的体积也不可小觑。光 lottie-web 一个库可能就比你的 vue 项目都要大，在这种前提下，即便你使用了按需载入（用户很可能会遭遇页面“假死”），也很难在用户体验上做到很好的平衡。</p><p>既然不能“硬”来，也不能“胡”来，我们只能考虑另辟蹊径了。我们是否可以考虑将资源加载可视化呢？</p><p>如果你接触过游戏，你可能就已经见过这种模式了…什么，我都没听过这个词，你说我见过？</p><p>你玩过王者农药、吃鸡这类手游么？这类大型手游包含大量的资源文件，资源初始化需要比较长的一段时间，因此会采用以下的方式进行资源加载进度的显示。</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/b2970b994763578b330268087c8c4c08.jpg" alt="某游戏启动画面截图"></p><p>这就是资源加载可视化，你可以通过加载进度告诉用户需要等待的预期时间。这种方式在游戏或者软件领域经常出现，但在前端领域出现频率非常低，毕竟网页从来都不是大型应用的首选载体。虽然出现频率低，不代表我们不能使用。</p><pre><code class="line-numbers language-vue">&lt;template&gt;  &lt;section class=&quot;loader&quot; v-show=&quot;visible&quot;&gt;    &lt;div class=&quot;progress-bar&quot;&gt;      &lt;div class=&quot;progress&quot; :style=&quot;{ width: `${currentProgress}%` }&quot;&gt;&lt;/div&gt;    &lt;/div&gt;    &lt;slot v-if=&quot;currentProgress === 100&quot;&gt;&lt;/slot&gt;  &lt;/section&gt;&lt;/template&gt;</code></pre><pre><code class="line-numbers language-javascript">const lottiejs = () =&gt; {  return import(/* webpackChunkName: &quot;lottie&quot; */ &#39;lottie-web&#39;)}const animate = () =&gt; {  return import(/* webpackChunkName: &quot;animate&quot; */ &#39;@/assets/animate.json&#39;)}const loader = [lottiejs(), animate()].map((item) =&gt; {  return item    .then((data) =&gt; {      this.loaded += 1      return data    })    .catch((err) =&gt; {      console.error(err.message)    })})Promise.all(loader).then((resp) =&gt; {  this.$emit(&#39;loaded&#39;, resp)})const timer = setInterval(() =&gt; {  if (this.currentProgress &lt; 100) {    this.currentProgress = Math.floor((this.loaded / loader.length) * 100)    if (this.currentProgress &gt; 100) this.currentProgress = 100  } else {    clearInterval(timer)  }}, 300)</code></pre><p>以上代码截取了组件的核心实现逻辑。实现效果如下：</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/42ac76a28964fd8bea0b2cceda297c20.png" alt="前端界面效果"></p><p>细心的人应该已经发现了，这里使用了 webpack 的按需载入方案。按需载入默认返回一个 Promise 对象，而 Promise 对象是可以进行统计的！我们利用这一点可以做到依赖的预加载且独立页面按需载入并减少了主依赖包的大小，简直是一举多得呐~</p><p>可为什么不直接使用 <code>const lottiejs = import(/* webpackChunkName: &quot;lottie&quot; */ &#39;lottie-web&#39;)</code>，而要多此一举的包一个函数呢？这个主要是为了避免项目在 webpack 打包过程中的资源加载策略，如果直接引用资源，那么 webpack 会认为这个资源时内联的，会提前进行加载，这对于既希望预加载一些必要依赖，又希望不加载全部依赖的场景下，这会非常有用！</p><h2 id="图片最优化和预加载"><a href="#图片最优化和预加载" class="headerlink" title="图片最优化和预加载"></a>图片最优化和预加载</h2><p>新项目涉及了大量的图片素材，如果按照常规的加载方式，会极大的降低用户体验。你可以试想一下如下场景：你打开一个页面，页面上的图片还在加载，但在加载过程中，半张图片还在执行动画效果…这个场景犹如，你在逛街突然看到半个身影，在街上飘…着实有些诡异！</p><h3 id="图片预加载"><a href="#图片预加载" class="headerlink" title="图片预加载"></a>图片预加载</h3><p>解决当前页面图片加载过慢的情况一般可以考虑预加载的方式。但这里提到的预加载与传统方式的预加载还有些许不同，主要在于当前项目的图片分散在不同的页面间，因此传统的针对屏外图片预加载并不适合当下的场景。我们希望图片在页面出现之前就已经加载完成，进入到页面后，图片资源应该从浏览器文件缓存中直接读取。其实要做到这一点很简单，你只需要在进入页面前在后台请求一次相同资源路径的图片，浏览器在第二次访问时就会直接使用文件缓存中图片，而不再从服务器（或CDN）上下载图片。<br>既然知道了原理，那么就很容易实现对应的加载逻辑：</p><pre><code class="line-numbers language-javascript">function loadImage(url) {  return new Promise((resolve, reject) =&gt; {    // 利用新建一个图片实例而不渲染实现图片预加载    const image = new Image()    image.onload = () =&gt; resolve(url)    image.onerror = () =&gt; reject(new Error(`Image preload error: ${url}`))    image.src = url  })}</code></pre><p>预加载实际上是在后台提前偷跑图片加载流程，如果你需要预加载的图片资源非常多，势必会影响正常的网络数据，我们坚决不做“偷鸡不成蚀把米”的事！在 Http2 时代，你可能知道浏览器已经支持了资源的并行加载，我们可以既要竟可能地减少对项目的正常加载的影响，又要进行资源“偷跑”，我们可以考虑把图片加载任务进行分配加载，一批图片加载完了，再加载下一批图片。w(ﾟДﾟ)w，原来还能这么玩？</p><pre><code class="line-numbers language-javascript">// thread，表示每一批的资源数量function preloadImage(images, thread = 3) {  const tasks = []  images.splice(0, thread).forEach((url) =&gt; {    tasks.push(loadImage(url))  })  return Promise.all(tasks).then(() =&gt; {    if (images.length &gt; 0) preloadImage(images, thread)  })}</code></pre><h3 id="图片最优化"><a href="#图片最优化" class="headerlink" title="图片最优化"></a>图片最优化</h3><p>图片资源大小对加载速度有着最直接的影响。这个道理几乎所有人都知道，但很少有人去尝试最优解。我目前的项目使用 CDN 的方式对图片进行加载优化，但这种方式只能说是“治标不治本”。如果一张图片有几 M 大小，即便使用最快的 CDN，也会让绝大多数用户感觉到图片加载慢。所以我们一般在图片上传到 CDN 之前，对图片进行一次压缩，对图片的压缩实现有很多种方式，我推荐使用 <a href="https://tinypng.com/" target="_blank" rel="noopener">Tinypng</a> 提供的在线图片压缩服务。<br>可现实总是残酷的，在设计师提供的 3倍图的场景下，有些图片即便是压缩也很可能会非常大，但我们真的对此无能为力了么？也并不是，你可以使用 CSS3 的 <code>media query</code> 或 <code>image-set</code>属性，对不同屏幕分辨率的图片进行设置，这就需要设计师把所有场景下的图片都提供给你，然后你需要在所有用到该图片的地方都需要设置！这不仅增加了你的工作量，连设计师都需要花时间额外给你提供对应的图片素材。这对于小公司而言，是一种相当“耗费”人力的做法，性价比极低…正因为性价比低，所以国内很少有公司采用这种方式来做图片加载优化。<br>如果“人工”的方式不好走，那何不试试“智能”的方式呢？很多 CDN 服务商都有提供图片瘦身的方案，这里以七牛 CDN 为例：</p><pre><code class="line-numbers language-javascript">let checkedWebp = falsefunction slimImages(images) {  if (checkedWebp) return images  // checkWebpFeature 函数的实现，你可以参考网上的方案，这里由于篇幅原因不再展开  return checkWebpFeature(&#39;alpha&#39;)    .then((supportWebp) =&gt; {      if (supportWebp) {        _.entries(images).forEach(([key, val]) =&gt; {          images[key] = `${val}?imageMogr2/thumbnail/!${Math.ceil((window.devicePixelRatio / 3) * 100) || 100}p/format/webp`        })      }      return images    })    .finally(() =&gt; {      checkedWebp = true    })}</code></pre><p>上述代码主要做了两处优化：</p><p>1、不同分辨率的设备使用对应尺寸的图片 <code>Math.ceil((window.devicePixelRatio / 3) * 100)</code>，七牛云的 <code>thumbnail/!100p</code> 指令可以生成对应比例的图片，其中 100p 代表原始大小。<br>2、使用了 webp 格式，webp 是 Google 早期推出的一种高压缩的图片格式。这种图片格式据说曾帮助淘宝每年节省好几亿的流量费，Amazing！</p><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p>通过以上优化，你可以看到图片在 1倍屏下得到了极大的优化：</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/a4493e1b4402864ad0c8a334e1612d3e.png" alt="原始图片"><br>原始图片</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/eb10b6a816482825c80e07d2b4bf7272.png" alt="使用 Tinypng 压缩后的图片"><br>使用 Tinypng 压缩后的图片</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/ed0d5cfd2ec16075695bf6d110757c07.png" alt="借助 CDN 瘦身后的 2倍屏图片"><br>借助 CDN 瘦身后的 2倍屏图片</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/93ef51d5872dae2fab5787fac0a7b6f6.png" alt="借助 CDN 瘦身后的 1倍屏图片"><br>借助 CDN 瘦身后的 1倍屏图片</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;自从带队开发公司的运营类项目的开发，这半年来似乎一直在关注 vue 项目的性能优化。作为公司的用户引流项目，活动的首屏加载时间长短会对用户转换率产生一些”蝴蝶效应“。该影响对于弱网络的用户群体的影响尤为明显。如果一个页面白屏时间超过 2.5s，那么用户很可能会认为页面存在问题而关闭页面，从而导致用户流失。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Vue" scheme="https://xiangfa.org/categories/Vue/"/>
    
    
      <category term="vue" scheme="https://xiangfa.org/tags/vue/"/>
    
      <category term="项目优化" scheme="https://xiangfa.org/tags/%E9%A1%B9%E7%9B%AE%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端开发人员学习路线</title>
    <link href="https://xiangfa.org/2020/10/frontend-learning-roadmap/"/>
    <id>https://xiangfa.org/2020/10/frontend-learning-roadmap/</id>
    <published>2020-10-29T08:53:02.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>最近 Github 上有一个非常热门的有关学习路线的仓库，这仓库的名字叫做 <a href="https://github.com/kamranahmedse/developer-roadmap" target="_blank" rel="noopener">developer-roadmap</a>，其包括的学习路线涵盖：前端、后端、DevOps、Android、React、PostgraSQL DBA。另外，测试的学习路线还在路上。目前已经在 Github收获了 131 k+ star，Star 数量在 Github 所有仓库中排名第 9。虽然这个火爆程度有点让人匪夷所思…但文章内容还算比较靠谱，大家还算可以了解一下的。</p><a id="more"></a><h3 id="前端学习路线图"><a href="#前端学习路线图" class="headerlink" title="前端学习路线图"></a>前端学习路线图</h3><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/amery/120bfbab31eeb56613ed5ccee989dbf3.png" alt="前端学习路线"></p><p>这份学习路线图已经非常地完善了，但我确发现整份路线图并没有包括 Node.js 的系统性学习，本人猜测可能是作者把 Node.js 归属到了后端开发的领域吧？当然也可能是由于 Node.js 学习内容过多，他们打算单独作为一个内容来讲解。按照我个人的学习经验，Node.js 在前端学习过程中有着举足轻重的地位！为什么这么说呢，你可以想一想前端领域最重要的一个概念<strong>项目工程化</strong>。这个在学习路线图里被分拆为构建工具和前端框架两个篇幅，特别是在构建工具中，你写的脚本语言几乎都是 Node.js 代码。如果你想更好的深入前端领域，你最好也学习一部分 Node.js 的知识，这对后期的发展非常有帮助。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近 Github 上有一个非常热门的有关学习路线的仓库，这仓库的名字叫做 &lt;a href=&quot;https://github.com/kamranahmedse/developer-roadmap&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;developer-roadmap&lt;/a&gt;，其包括的学习路线涵盖：前端、后端、DevOps、Android、React、PostgraSQL DBA。另外，测试的学习路线还在路上。目前已经在 Github收获了 131 k+ star，Star 数量在 Github 所有仓库中排名第 9。虽然这个火爆程度有点让人匪夷所思…但文章内容还算比较靠谱，大家还算可以了解一下的。&lt;/p&gt;
    
    </summary>
    
    
      <category term="学习" scheme="https://xiangfa.org/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
      <category term="学习" scheme="https://xiangfa.org/tags/%E5%AD%A6%E4%B9%A0/"/>
    
      <category term="前端开发" scheme="https://xiangfa.org/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>算法学习笔记 — 数学领域</title>
    <link href="https://xiangfa.org/2020/10/algorithm-for-mathematics/"/>
    <id>https://xiangfa.org/2020/10/algorithm-for-mathematics/</id>
    <published>2020-10-22T09:29:32.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>在做算法时，可能需要运用一些数学知识。数学也是计算机的基础，但大部分算法只涉及到高中以下的知识，因此不需要过于担心能否看懂题目。</p><a id="more"></a><h2 id="罗马数字转整数"><a href="#罗马数字转整数" class="headerlink" title="罗马数字转整数"></a>罗马数字转整数</h2><blockquote><p>罗马数字包含以下七种字符：I， V， X， L，C，D 和 M。<br>分别对应的数值为：1 ，5，10，50，100，500，1000 。<br>例如， 罗马数字 3 写做 III，即为三个并列的 1。12 写做 XII，即为 X+II。 26 写做 XXVII, 即为 XX+V+I。<br>通常情况下，不能出现超过连续三个相同的罗马数字并且罗马数字中小的数字在大的数字的右边。但也存在特例，例如 4 不写做 IIII，而是 IV。数字 1 在数字 5 的左边，所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地，数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况：</p></blockquote><pre><code>I 可以放在 V(5) 和 X(10) 的左边，来表示 4 和 9。X 可以放在 L(50) 和 C(100) 的左边，来表示 40 和90。C 可以放在 D(500) 和 M(1000) 的左边，来表示 400 和 900。</code></pre><blockquote><p>给定一个罗马数字，将其转换成整数。输入确保在 1 到 3999 的范围内。</p></blockquote><pre><code>输入:&quot;III&quot;输出:3输入:&quot;IV&quot;输出:4输入:&quot;LVIII&quot;输出:58输入:&quot;MCMXCIV&quot;输出:1994</code></pre><p>详解：<a href="https://leetcode-cn.com/problems/roman-to-integer/solution/yong-shi-9993nei-cun-9873jian-dan-jie-fa-by-donesp/" target="_blank" rel="noopener">LeetCode 罗马数字转整数</a></p><h3 id="解法一：模拟法"><a href="#解法一：模拟法" class="headerlink" title="解法一：模拟法"></a>解法一：模拟法</h3><p>思路：罗马数字的特殊值组合主要有 IV、VI、IX、XI 等，对于 IV，如果用右边的值减去左边的值可以得到 <code>5 - 1 = 4</code>，而对于 VI，如果使用右边的值加上左边的值可以得到 <code>5 + 1 = 6</code>。因此我们在遍历过程中每次取前后两个值，就可以得到如果左侧的值小于右侧的值，则使用大值减小值，如果左边的值大于右边的值，则大值加小值的方案。</p><pre><code class="line-numbers language-js">/** * 罗马数字转整数 * @param {string} str 需要转换的罗马数字 * @return {number} 转换后的整数 */function romanToInteger(str) {  // 异常值处理  if (typeof str !== &#39;string&#39;) return NaN  const getValue = char =&gt; {    switch (char) {      case &#39;I&#39;: return 1      case &#39;V&#39;: return 5      case &#39;X&#39;: return 10      case &#39;L&#39;: return 50      case &#39;C&#39;: return 100      case &#39;D&#39;: return 500      case &#39;M&#39;: return 1000      default: return 0    }  }  let sum = 0  // 获取起始值  let preValue = 0  for (let i = 0; i &lt; str.length; i++) {    // 当前值    const curValue = getValue(str.charAt(i))    if (preValue &lt; curValue) {      sum -= preValue    } else {      sum += preValue    }    preValue = curValue  }  sum += preValue  return sum}</code></pre><h4 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法只使用了一次遍历，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法只使用了一些常规值，因此空间复杂度为 $ O(1) $。</p></li></ul><h3 id="解法二：特殊值合并运算"><a href="#解法二：特殊值合并运算" class="headerlink" title="解法二：特殊值合并运算"></a>解法二：特殊值合并运算</h3><p>思路：罗马数字的特殊逻辑中，除了 IV、IX、XL、XC、CD 和 CM 不符合累加原则以外，其他位数的数组都可以使用累加的形式进行取值。如果我们将特殊值作为一个“整体”来看待，这些特殊值也可以等于具体的值，即依然可以采用累加的原则。</p><pre><code class="line-numbers language-js">/** * 罗马数字转整数 * @param {string} str 需要转换的罗马数字 * @return {number} 转换后的整数 */function romanToInteger(str) {  // 异常值处理  if (typeof str !== &#39;string&#39;) return NaN  const getValue = char =&gt; {    switch (char) {      case &#39;I&#39;: return 1      case &#39;V&#39;: return 5      case &#39;X&#39;: return 10      case &#39;L&#39;: return 50      case &#39;C&#39;: return 100      case &#39;D&#39;: return 500      case &#39;M&#39;: return 1000      case &#39;IV&#39;: return 4      case &#39;IX&#39;: return 9      case &#39;XL&#39;: return 40      case &#39;XC&#39;: return 90      case &#39;CD&#39;: return 400      case &#39;CM&#39;: return 900      default: return 0    }  }  const specChars = [&#39;IV&#39;, &#39;IX&#39;, &#39;XL&#39;, &#39;XC&#39;, &#39;CD&#39;, &#39;CM&#39;]  let sum = 0  let i = 0  while (i &lt; str.length) {    if (str.charAt(i + 1) !== &#39;&#39;      &amp;&amp; specChars.indexOf(str.charAt(i) + str.charAt(i + 1)) !== -1) {      sum += getValue(str.charAt(i) + str.charAt(i + 1))      i += 2    } else {      sum += getValue(str.charAt(i))      i += 1    }  }  return sum}</code></pre><h4 id="复杂度分析-1"><a href="#复杂度分析-1" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法只使用了一次遍历，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法只使用了一些常规值，因此空间复杂度为 $ O(1) $。</p></li></ul><h2 id="Fizz-Buzz"><a href="#Fizz-Buzz" class="headerlink" title="Fizz Buzz"></a>Fizz Buzz</h2><blockquote><p>写一个程序，输出从 1 到 n 数字的字符串表示。<br>1.如果 n 是 3 的倍数，输出“Fizz”；<br>2.如果 n 是 5 的倍数，输出“Buzz”；<br>3.如果 n 同时是 3 和 5 的倍数，输出 “FizzBuzz”。</p></blockquote><pre><code>示例n = 15返回:12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz</code></pre><p>详解：<a href="https://leetcode-cn.com/problems/fizz-buzz/solution/fizz-buzz-by-leetcode/" target="_blank" rel="noopener">LeetCode 罗马数字转整数</a></p><h3 id="解法一：模拟法-1"><a href="#解法一：模拟法-1" class="headerlink" title="解法一：模拟法"></a>解法一：模拟法</h3><p>思路：只需要判断 1 - n 的每个数字是否能被 3、5、15 整除，输出对应的字符串即可。</p><pre><code class="line-numbers language-js">/** * Fizz Buzz * @param {number} n 数字 * @return {string} 结果字符串 */function fizzBuzz(n) {  // 异常值处理  if (typeof n !== &#39;number&#39;) return &#39;&#39;  let result = &#39;&#39;  for (let i = 1; i &lt;= n; i++) {    if (i % 15 === 0) { // 被15整除      result += &#39;FizzBuzz&#39;    } else if (i % 3 === 0) { // 被3整除      result += &#39;Fizz&#39;    } else if (i % 5 === 0) { // 被5整除      result += &#39;Buzz&#39;    } else {      result += i.toString()    }  }  return result}</code></pre><h4 id="复杂度分析-2"><a href="#复杂度分析-2" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法使用了一次数据遍历，循环次数依赖于数字 n，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法只使用了常规变量，因此时间复杂度为 $ O(1) $。</p></li></ul><h3 id="解法二：数组转对象"><a href="#解法二：数组转对象" class="headerlink" title="解法二：数组转对象"></a>解法二：数组转对象</h3><p>思路：将数据存储进数组，然后利用数组的 <code>Array.join()</code> 方法转换成字符串。</p><pre><code class="line-numbers language-js">/** * Fizz Buzz * @param {number} n 数字 * @return {string} 结果字符串 */function fizzBuzz(n) {  // 异常值处理  if (typeof n !== &#39;number&#39;) return &#39;&#39;  return Array.from(    new Array(n),    (t, i) =&gt; (t = (++i % 3 ? &#39;&#39; : &#39;Fizz&#39;) + (i % 5 ? &#39;&#39; : &#39;Buzz&#39;)) ? t : &#39;&#39; + i  ).join(&#39;&#39;)}</code></pre><h4 id="复杂度分析-3"><a href="#复杂度分析-3" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法使用了 Array.from 的数组变量函数，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(n) $<br>该算法中临时创建了长度为 <code>n</code> 的数组，因此空间复杂度为 $ O(n) $。</p></li></ul><h2 id="计数质数"><a href="#计数质数" class="headerlink" title="计数质数"></a>计数质数</h2><blockquote><p>统计所有小于非负整数 n 的质数的数量。其中 $ 0 &lt;= n &lt;= 5 * 10^6 $</p></blockquote><pre><code>示例输入: 10输出: 4解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。</code></pre><p>详解：<a href="https://leetcode-cn.com/problems/count-primes/solution/ji-shu-zhi-shu-bao-li-fa-ji-you-hua-shai-fa-ji-you/" target="_blank" rel="noopener">LeetCode 计数质数</a></p><h3 id="解法一：暴力求解"><a href="#解法一：暴力求解" class="headerlink" title="解法一：暴力求解"></a>解法一：暴力求解</h3><p>思路：求质数最简单的方案就是暴力求解，即穷举遍历。</p><pre><code class="line-numbers language-js">/** * 计数质数 * @param {number} n 数字 * @return {number} 质数数量 */function countPrimes(n) {  // 异常值处理  if (typeof n !== &#39;number&#39;) return 0  let count = 0  for (let i = 2; i &lt; n; i++) {    let sign = true    for (let j = 2; j &lt; i; j++) {      if (i % j === 0) {        sign = false        break      }    }    if (sign) count++  }  return count}</code></pre><h4 id="复杂度分析-4"><a href="#复杂度分析-4" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n^2) $<br>该算法使用了两次遍历，时间复杂度为 $ O(n^2) $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法只使用了常规的变量，因此空间复杂度为 $ O(1) $。</p></li></ul><h3 id="解法二：埃拉托斯特尼筛法"><a href="#解法二：埃拉托斯特尼筛法" class="headerlink" title="解法二：埃拉托斯特尼筛法"></a>解法二：埃拉托斯特尼筛法</h3><p>思路：从 2 开始，将每个质数的各个倍数，标记成合数。一个质数的各个倍数，是一个差为此质数本身的等差数列。此为这个筛法和试除法不同的关键之处，后者是以质数来测试每个待测数能否被整除。</p><pre><code class="line-numbers language-js">/** * 计数质数 * @param {number} n 数字 * @return {number} 质数数量 */function countPrimes(n) {  // 异常值处理  if (typeof n !== &#39;number&#39;) return 0  const arr = new Array(n)  let count = 0  for (let i = 2; i &lt; n; i++) {    if (!arr[i - 1]) {      count++      for (let j = i * i; j &lt;= n; j += i) {        arr[j - 1] = true      }    }  }  return count}</code></pre><h4 id="复杂度分析-5"><a href="#复杂度分析-5" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(nlog(log(n))) $<br>对每一个 $ i $，要划掉 $ n/i $ 个数, 要进行 $ n/i $ 次运算，全部加起来，就是 $ n $ (从 1 到 $ \sqrt[]{n} $ 之间的 $ 1/i $ 之和)，简单讲就是 (从 1 到 $ n $ 之间的 $ 1/i $ 之和) 约等于 $ log $ (对所有 $ k $ 从 1 到 $ n $ 之间的 $ 1/k $ 之和)，后者是 $ log(n) $，所以前者就是 $ log(log(n)) $；最外层需要判断 $ n $ 次 ；所以最终时间复杂度为 $ O(nlog(log(n))) $。</p></li><li><p>空间复杂度：$ O(n) $<br>该算法申请了一个长度为 n 的数组，因此空间复杂度为 $ O(n) $。</p></li></ul><h2 id="3-的幂"><a href="#3-的幂" class="headerlink" title="3 的幂"></a>3 的幂</h2><blockquote><p>给定一个整数，写一个函数来判断它是否是 3 的幂次方。</p></blockquote><pre><code>示例 1：输入: 27输出: true示例 2：输入: 0输出: false示例 3：输入: 9输出: true示例 4：输入: 45输出: false</code></pre><p><strong>进阶</strong>：你能不使用循环或者递归来完成本题吗？</p><p>详解：<a href="https://leetcode-cn.com/problems/power-of-three/solution/3de-mi-by-leetcode/" target="_blank" rel="noopener">LeetCode 3 的幂</a></p><h3 id="解法一：循环迭代"><a href="#解法一：循环迭代" class="headerlink" title="解法一：循环迭代"></a>解法一：循环迭代</h3><p>思路：只要让数字 <code>n</code>，循环除以 3，看能否被整除就可以了。</p><pre><code class="line-numbers language-js">/** * 求 3 的幂 * @param {number} n 数字 * @return {boolean} 是否为 3 的幂 */function isPowerOfThree(n) {  // 异常值处理  if (typeof n !== &#39;number&#39;) return false  if (n &lt; 1) return false  while (n &gt; 1) {    // 如果该数字不能被 3 整除，则直接输出 false    if (n % 3 !== 0) {      return false    } else {      n = n / 3    }  }  return true}</code></pre><h4 id="复杂度分析-6"><a href="#复杂度分析-6" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法只使用了一次遍历，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法未使用额外的内存空间，因此空间复杂度为 $ O(1) $。</p></li></ul><h3 id="解法二：整数限制"><a href="#解法二：整数限制" class="headerlink" title="解法二：整数限制"></a>解法二：整数限制</h3><p>思路：常规的整数定义为 [-2147483648, 2147483647]，而在此范围内 3 的最大幂是 19，即 Math.pow(3, 19)。只要判断 $ 3^19 $ 除以 <code>n</code> 余数是否为 0 就可以判断是否为 3 的幂。</p><pre><code class="line-numbers language-js">/** * 求 3 的幂 * @param {number} n 数字 * @return {boolean} 是否为 3 的幂 */function isPowerOfThree(n) {  // 异常值处理  if (typeof n !== &#39;number&#39;) return false  return n &gt; 0 &amp;&amp; Math.pow(3, 19) % n == 0}</code></pre><h4 id="复杂度分析-7"><a href="#复杂度分析-7" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(1) $<br>该算法中，<code>Math.pow</code> 的时间复杂度为 $ O(1) $，加减乘除的时间复杂度也为 $ O(1) $，因此整体时间复杂度为 $ O(1) $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法未使用额外的内存空间，因此空间复杂度为 $ O(1) $。</p></li></ul><h2 id="Excel-表列序号"><a href="#Excel-表列序号" class="headerlink" title="Excel 表列序号"></a>Excel 表列序号</h2><blockquote><p>给定一个 Excel 表格中的列名称，返回其相应的列序号。</p></blockquote><pre><code>示例A -&gt; 1B -&gt; 2C -&gt; 3...Z -&gt; 26AA -&gt; 27AB -&gt; 28输入: &quot;A&quot;,输出: 1输入: &quot;AB&quot;,输出: 28</code></pre><p>详解：<a href="https://leetcode-cn.com/problems/excel-sheet-column-number/solution/hua-jie-suan-fa-171-excelbiao-lie-xu-hao-by-guanpe/" target="_blank" rel="noopener">LeetCode Excel 表列序号</a></p><h3 id="解法一：26进制计算法"><a href="#解法一：26进制计算法" class="headerlink" title="解法一：26进制计算法"></a>解法一：26进制计算法</h3><p>思路：由于 Excel 表序列号只包含 [A-Z] 26个字符，且 A = 1，B = 2…我们可以将其看成是一种特殊的 26进制数，因此这道题也就变成了让你将 26进制数转 10进制数。</p><pre><code class="line-numbers language-js">/** * Excel 表列序号 * @param {string} str 表序列号 * @return {number} 当前为第几列 */function titleToNumber(str) {  // 异常值处理  if (typeof str !== &#39;string&#39;) return -1  let sum = 0  let i = str.length - 1  let carry = 1  while (i &gt;= 0) {    // A 的 charCode 等于 64    sum += (str[i].charCodeAt() - 64) * carry    carry *= 26    i--  }  return sum}</code></pre><h4 id="复杂度分析-8"><a href="#复杂度分析-8" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法只使用了一次遍历操作，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法中临时变量的个数与循环次数无关，因此空间复杂度为 $ O(1) $。</p></li></ul><h3 id="解法二：利用-Hash-表快速转换"><a href="#解法二：利用-Hash-表快速转换" class="headerlink" title="解法二：利用 Hash 表快速转换"></a>解法二：利用 Hash 表快速转换</h3><p>思路：直接利用 Hash 表的方式，快速获取字母对应的值，并按照位数进行累加，即 26进制转 10进制。</p><pre><code class="line-numbers language-js">/** * Excel 表列序号 * @param {string} str 表序列号 * @return {number} 当前为第几列 */function titleToNumber(str) {  // 异常值处理  if (typeof str !== &#39;string&#39;) return -1  const arr = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;D&#39;, &#39;E&#39;, &#39;F&#39;, &#39;G&#39;, &#39;H&#39;, &#39;I&#39;, &#39;J&#39;, &#39;K&#39;, &#39;L&#39;, &#39;M&#39;,              &#39;N&#39;, &#39;O&#39;, &#39;P&#39;, &#39;Q&#39;, &#39;R&#39;, &#39;S&#39;, &#39;T&#39;, &#39;U&#39;, &#39;V&#39;, &#39;W&#39;, &#39;X&#39;, &#39;Y&#39;, &#39;Z&#39;]  let sum = 0  const length = str.length  for (let i = 0; i &lt; length; i++) {    sum = (arr.indexOf(str[i]) + 1) * Math.pow(26, length - 1 - i) + sum  }  return sum}</code></pre><h4 id="复杂度分析-9"><a href="#复杂度分析-9" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法只使用了一次遍历操作，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法中临时变量的个数与循环次数无关，因此空间复杂度为 $ O(1) $。</p></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在做算法时，可能需要运用一些数学知识。数学也是计算机的基础，但大部分算法只涉及到高中以下的知识，因此不需要过于担心能否看懂题目。&lt;/p&gt;
    
    </summary>
    
    
      <category term="算法学习笔记" scheme="https://xiangfa.org/categories/%E7%AE%97%E6%B3%95%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="算法" scheme="https://xiangfa.org/tags/%E7%AE%97%E6%B3%95/"/>
    
      <category term="数学" scheme="https://xiangfa.org/tags/%E6%95%B0%E5%AD%A6/"/>
    
      <category term="学习笔记" scheme="https://xiangfa.org/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
  </entry>
  
  <entry>
    <title>Markdown 语法简明指南</title>
    <link href="https://xiangfa.org/2020/09/markdown-grammar-guide/"/>
    <id>https://xiangfa.org/2020/09/markdown-grammar-guide/</id>
    <published>2020-09-14T11:06:24.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>Markdown 是一种轻量级标记语言，它以纯文本形式（易读、易写、易更改）编写文档，并最终以 HTML 格式发布。通过简单的标记语法，它可以使普通文本内容具有一定的格式。相比所见即所得（WYSIWYG）编辑器，Markdown 可以让使用者摆脱排版的困扰，专心于写作。当前许多网站都广泛使用 Markdown 来撰写帮助文档或是用于论坛上发表消息。例如：GitHub、简书、掘金等。</p><a id="more"></a><h2 id="标题"><a href="#标题" class="headerlink" title="标题"></a>标题</h2><p>1、你可以使用 <code>=</code> 和 <code>-</code> 语法来展示一级和二级标题</p><p><code>=</code> 和 <code>-</code> 语法格式如下：</p><pre><code class="line-numbers language-md">这是一级标题=================这是二级标题-----------------</code></pre><p>展示效果如下：</p><h1 id="这是一级标题"><a href="#这是一级标题" class="headerlink" title="这是一级标题"></a>这是一级标题</h1><h2 id="这是二级标题"><a href="#这是二级标题" class="headerlink" title="这是二级标题"></a>这是二级标题</h2><p>2、使用 <code>#</code> 号语法</p><p>使用 <code>#</code> 号可表示 1-6 级标题，一级标题对应一个 <code>#</code> 号，二级标题对应两个 <code>#</code> 号，以此类推。</p><pre><code class="line-numbers language-md"># 一级标题## 二级标题### 三级标题#### 四级标题##### 五级标题###### 六级标题</code></pre><p>效果如下：</p><h1 id="一级标题"><a href="#一级标题" class="headerlink" title="一级标题"></a>一级标题</h1><h2 id="二级标题"><a href="#二级标题" class="headerlink" title="二级标题"></a>二级标题</h2><h3 id="三级标题"><a href="#三级标题" class="headerlink" title="三级标题"></a>三级标题</h3><h4 id="四级标题"><a href="#四级标题" class="headerlink" title="四级标题"></a>四级标题</h4><h5 id="五级标题"><a href="#五级标题" class="headerlink" title="五级标题"></a>五级标题</h5><h6 id="六级标题"><a href="#六级标题" class="headerlink" title="六级标题"></a>六级标题</h6><h2 id="段落"><a href="#段落" class="headerlink" title="段落"></a>段落</h2><p>段落的前后要有空行，所谓的空行是指没有文字内容。<br>若想在段内强制换行的方式是<strong>使用两个以上空格加上回车</strong>（引用中换行不需要回车）。<br>当然也可以在段落后面使用一个空行来表示重新开始一个段落。</p><h3 id="空行模式"><a href="#空行模式" class="headerlink" title="空行模式"></a>空行模式</h3><pre><code class="line-numbers language-md">这是一个段落。这是另一个段落。</code></pre><p>效果如下：</p><p>这是一个段落。</p><p>这是另一个段落。</p><h3 id="空格换行模式"><a href="#空格换行模式" class="headerlink" title="空格换行模式"></a>空格换行模式</h3><pre><code class="line-numbers language-md">这是一个段落，后面带了两个空格。  这是另一个段落。</code></pre><p>效果如下：</p><p>这是一个段落，后面带了两个空格。<br>这是另一个段落。</p><h2 id="字体"><a href="#字体" class="headerlink" title="字体"></a>字体</h2><h3 id="斜体"><a href="#斜体" class="headerlink" title="斜体"></a><em>斜体</em></h3><p>你只要给需要倾斜的文字左右分别用一个 <code>*</code> 或 <code>_</code> 号包起来就可以了。</p><pre><code class="line-numbers language-md">*斜体文本*_斜体文本_</code></pre><p>效果如下：</p><p><em>斜体文本</em><br><em>斜体文本</em></p><h3 id="粗体"><a href="#粗体" class="headerlink" title="粗体"></a><strong>粗体</strong></h3><p>你只要给需要倾斜的文字左右分别用两个 <code>*</code> 或 <code>_</code> 号包起来就可以了。</p><pre><code class="line-numbers language-md">**粗体文本**__粗体文本__</code></pre><p>效果如下：</p><p><strong>粗体文本</strong><br><strong>粗体文本</strong></p><h3 id="粗斜体"><a href="#粗斜体" class="headerlink" title="粗斜体"></a><strong><em>粗斜体</em></strong></h3><p>你只要给需要倾斜的文字左右分别用三个 <code>*</code> 或 <code>_</code> 号包起来就可以了。</p><pre><code class="line-numbers language-md">***粗斜体文本***___粗斜体文本___</code></pre><p>效果如下：</p><p><strong><em>粗斜体文本</em></strong><br><strong><em>粗斜体文本</em></strong></p><h3 id="删除线"><a href="#删除线" class="headerlink" title="删除线"></a><del>删除线</del></h3><p>你只要给需要倾斜的文字左右分别用两个 <code>~</code> 号包起来就可以了。</p><pre><code class="line-numbers language-md">~~这段文本使用了删除线~~</code></pre><p>效果如下：</p><p><del>这段文本使用了删除线</del></p><h3 id="在文本段内使用"><a href="#在文本段内使用" class="headerlink" title="在文本段内使用"></a>在文本段内使用</h3><p>Markdown 的文本格式虽然不多但已经满足绝大部分的写作需求。虽然示例都是一段文本演示一个文本语法，但 Markdown 的文本语法是可以在一段文本内混合。</p><pre><code class="line-numbers language-md">**月光如流水一般，静静地泻在这一片叶子和花上**。薄薄的青雾浮起在荷塘里。叶子和花*仿佛在牛乳中洗过一样*；又像笼着轻纱的梦。虽然是满月，天上却有一层淡淡的云，所以不能朗照；但我以为这恰是到了好处—— ___酣眠固不可少，小睡也别有风味的___ 。月光是隔了树照过来的，高处丛生的灌木，落下参差的斑驳的黑影，峭楞楞如鬼一般；弯弯的杨柳的稀疏的倩影，却又像是画在荷叶上。塘中的月色并不均匀；但光与影有着和谐的旋律，如~~梵婀玲~~上奏着的名曲。</code></pre><p>效果如下：</p><p><strong>月光如流水一般，静静地泻在这一片叶子和花上</strong>。薄薄的青雾浮起在荷塘里。叶子和花<em>仿佛在牛乳中洗过一样</em>；又像笼着轻纱的梦。虽然是满月，天上却有一层淡淡的云，所以不能朗照；但我以为这恰是到了好处—— <strong><em>酣眠固不可少，小睡也别有风味的</em></strong> 。月光是隔了树照过来的，高处丛生的灌木，落下参差的斑驳的黑影，峭楞楞如鬼一般；弯弯的杨柳的稀疏的倩影，却又像是画在荷叶上。塘中的月色并不均匀；但光与影有着和谐的旋律，如<del>梵婀玲</del>上奏着的名曲。</p><h2 id="列表"><a href="#列表" class="headerlink" title="列表"></a>列表</h2><p>Markdown 支持有序列表和无序列表。</p><h3 id="无序列表"><a href="#无序列表" class="headerlink" title="无序列表"></a>无序列表</h3><p>无序列表使用 <code>*</code>、<code>+</code> 或 <code>-</code> 作为列表标记。<br><strong>注意</strong>：标记后面最少有一个<strong>空格</strong>或<strong>制表符</strong>。若不在引用区块中，必须和前方段落之间存在空行。</p><pre><code class="line-numbers language-md">* 星号标记第一项* 星号标记第二项* 星号标记第三项+ 加号标记第一项+ 加号标记第二项+ 加号标记第三项- 减号标记第一项- 减号标记第二项- 减号标记第三项</code></pre><p>效果如下：</p><ul><li>星号标记第一项</li><li>星号标记第二项</li><li>星号标记第三项</li></ul><ul><li>加号标记第一项</li><li>加号标记第二项</li><li>加号标记第三项</li></ul><ul><li>减号标记第一项</li><li>减号标记第二项</li><li>减号标记第三项</li></ul><h3 id="有序列表"><a href="#有序列表" class="headerlink" title="有序列表"></a>有序列表</h3><p>有序列表使用数字并加上 <code>.</code> 来表示。<br><strong>注意</strong>：标记后面最少有一个<strong>空格</strong>或<strong>制表符</strong>。若不在引用区块中，必须和前方段落之间存在空行。</p><pre><code class="line-numbers language-md">1. 第一项2. 第二项3. 第三项</code></pre><p>效果如下：</p><ol><li>第一项</li><li>第二项</li><li>第三项</li></ol><h3 id="列表嵌套"><a href="#列表嵌套" class="headerlink" title="列表嵌套"></a>列表嵌套</h3><p>列表嵌套只需在子列表中的选项前面添加<strong>两个以上</strong>空格即可：</p><pre><code class="line-numbers language-md">- 一级无序列表内容  + 二级无序列表内容  + 二级无序列表内容  + 二级无序列表内容- 一级无序列表内容  1. 二级有序列表内容  2. 二级有序列表内容  3. 二级有序列表内容1. 一级有序列表内容  * 二级无序列表内容  * 二级无序列表内容  * 二级无序列表内容2. 一级有序列表内容  1. 二级有序列表内容  2. 二级有序列表内容  3. 二级有序列表内容</code></pre><p>效果如下：</p><ul><li><p>一级无序列表内容</p><ul><li>二级无序列表内容</li><li>二级无序列表内容</li><li>二级无序列表内容</li></ul></li><li><p>一级无序列表内容</p><ol><li>二级有序列表内容</li><li>二级有序列表内容</li><li>二级有序列表内容</li></ol></li></ul><ol><li><p>一级有序列表内容</p><ul><li>二级无序列表内容</li><li>二级无序列表内容</li><li>二级无序列表内容</li></ul></li><li><p>一级有序列表内容</p><ol><li>二级有序列表内容</li><li>二级有序列表内容</li><li>二级有序列表内容</li></ol></li></ol><h2 id="待办事项"><a href="#待办事项" class="headerlink" title="待办事项"></a>待办事项</h2><p>待办事项是列表的进阶用法，是在列表的基础上结合未完成 <code>[ ]</code> 与已完成 <code>[x]</code> 实现。<br><strong>注意</strong>：待办事项是一个进阶语法，不同的 Markdown 编辑器对其支持程度不同。</p><pre><code class="line-numbers language-md">- [x] 吃饭- [x] 睡觉- [ ] 打豆豆</code></pre><p>效果如下：</p><ul><li><input checked="" disabled="" type="checkbox"> 吃饭</li><li><input checked="" disabled="" type="checkbox"> 睡觉</li><li><input disabled="" type="checkbox"> 打豆豆</li></ul><h3 id="待办事项嵌套"><a href="#待办事项嵌套" class="headerlink" title="待办事项嵌套"></a>待办事项嵌套</h3><pre><code class="line-numbers language-md">- [ ] 日常  1. [x] 吃饭  2. [x] 睡觉  3. [ ] 打豆豆- [ ] 工作  - [ ] Coding  - [ ] 摸鱼- [x] 思考人生  - [x] 思考人生    - [x] 思考人生      - [x] 思考人生</code></pre><ul><li><input disabled="" type="checkbox"> 日常<ol><li><input checked="" disabled="" type="checkbox"> 吃饭</li><li><input checked="" disabled="" type="checkbox"> 睡觉</li><li><input disabled="" type="checkbox"> 打豆豆</li></ol></li><li><input disabled="" type="checkbox"> 工作<ul><li><input disabled="" type="checkbox"> Coding</li><li><input disabled="" type="checkbox"> 摸鱼</li></ul></li><li><input checked="" disabled="" type="checkbox"> 思考人生<ul><li><input checked="" disabled="" type="checkbox"> 思考人生<ul><li><input checked="" disabled="" type="checkbox"> 思考人生<ul><li><input checked="" disabled="" type="checkbox"> 思考人生</li></ul></li></ul></li></ul></li></ul><h2 id="区块引用"><a href="#区块引用" class="headerlink" title="区块引用"></a>区块引用</h2><p>Markdown 区块引用是在段落开头使用 <code>&gt;</code> 符号 ，加上一个<strong>空格</strong>来表示。</p><pre><code class="line-numbers language-md">&gt; 区块引用&gt; 其他的引用内容</code></pre><p>效果如下：</p><blockquote><p>区块引用<br>其他的引用内容</p></blockquote><h3 id="引用嵌套"><a href="#引用嵌套" class="headerlink" title="引用嵌套"></a>引用嵌套</h3><p>区块引用是可以嵌套的，你可以加两个 <code>&gt;&gt;</code>、三个 <code>&gt;&gt;&gt;</code>，甚至 <code>n</code> 个 <code>&gt;</code>，这有点类似于引用回复的“盖塔”效果，一般情况下用不到这种特性。</p><pre><code class="line-numbers language-md">&gt; 第一层&gt;&gt; 第一层嵌套&gt;&gt;&gt; 第二层嵌套&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; 第九层~~妖塔~~嵌套</code></pre><p>效果如下：</p><blockquote><p>第一层</p><blockquote><p>第一层嵌套</p><blockquote><p>第二层嵌套</p><blockquote><blockquote><blockquote><blockquote><blockquote><blockquote><p>第九层<del>妖塔</del>嵌套</p></blockquote></blockquote></blockquote></blockquote></blockquote></blockquote></blockquote></blockquote></blockquote><h3 id="区块引用中使用其他标记"><a href="#区块引用中使用其他标记" class="headerlink" title="区块引用中使用其他标记"></a>区块引用中使用其他标记</h3><p>区块引用中可以混用其他的 Markdown 标记。</p><pre><code class="line-numbers language-md">&gt; 区块引用中使用其他的 **Markdown** 标记&gt; 1. **第一项**&gt; 2. ___第二项___&gt; + _第一项_&gt; + ~~第二项~~</code></pre><p>效果如下：</p><blockquote><p>区块引用中使用其他的 <strong>Markdown</strong> 标记</p><ol><li><strong>第一项</strong></li><li><strong><em>第二项</em></strong></li></ol><ul><li><em>第一项</em></li><li><del>第二项</del></li></ul></blockquote><h2 id="分割线"><a href="#分割线" class="headerlink" title="分割线"></a>分割线</h2><p>Markdown 中的分割线使用<strong>三个以上</strong>的 <code>-</code> 或 <code>*</code> 表示。<br><strong>注意</strong>：<code>-</code> 或 <code>*</code> 的多少并不影响分割线的样式。</p><pre><code class="line-numbers language-md">--------****</code></pre><p>效果如下：</p><hr><hr><h2 id="链接"><a href="#链接" class="headerlink" title="链接"></a>链接</h2><p>Markdown 的链接主要有两种形式：行内式和参考式。</p><h3 id="行内式"><a href="#行内式" class="headerlink" title="行内式"></a>行内式</h3><p>行内式主要是以下两种写法，其中链接描述是可选的。</p><pre><code>[链接名称](链接地址 &quot;可选标题&quot;)&lt;链接地址 &quot;可选标题&quot;&gt;</code></pre><p>实例如下：</p><pre><code class="line-numbers language-md">[梦翼坊](https://xiangfa.org &quot;点击访问梦翼坊&quot;)这是一个网址&lt;https://xiangfa.org&gt;</code></pre><p>效果如下：</p><p><a href="https://xiangfa.org/" title="点击访问梦翼坊">梦翼坊</a><br>这是一个网址<a href="https://xiangfa.org/">https://xiangfa.org</a></p><h3 id="参考式"><a href="#参考式" class="headerlink" title="参考式"></a>参考式</h3><p>当一篇文章中多次涉及到相同的网址链接时，如果每处都使用完整的长链接，会使文章变得”臃肿“，甚至导致难以阅读。<br>此外，当你后期需要修改链接时，你需要找到页面内所有需要修改的链接并一一调整。<br>为了解决这类问题，你可以考虑使用参考式链接。</p><pre><code>[链接名称][引用变量]</code></pre><p>实例如下：</p><pre><code class="line-numbers language-md">这篇使用指南在编写过程中参考了很多文章，比如[Github][github]、[菜鸟教程][1]以及[简书][2]。[github]: https://github.com/younghz/Markdown[1]: https://www.runoob.com/markdown/md-tutorial.html[2]: https://www.jianshu.com/p/191d1e21f7ed</code></pre><p>效果如下：</p><p>这篇使用指南在编写过程中参考了很多文章，比如<a href="https://github.com/younghz/Markdown" target="_blank" rel="noopener">Github</a>、<a href="https://www.runoob.com/markdown/md-tutorial.html" target="_blank" rel="noopener">菜鸟教程</a>以及<a href="https://www.jianshu.com/p/191d1e21f7ed" target="_blank" rel="noopener">简书</a>。</p><h2 id="图片"><a href="#图片" class="headerlink" title="图片"></a>图片</h2><p>图片的使用方式跟链接很相似，或者说是在链接语法前多加了一个 <code>!</code>，具体语法如下：</p><pre><code>![alt 属性文本](图片地址  &quot;可选标题&quot;)</code></pre><p>具体实例如下：</p><pre><code class="line-numbers language-md">![梦翼坊 Logo](https://xiangfa.org/images/logo.svg &quot;这是梦翼坊的 Logo&quot;)</code></pre><p>效果如下：</p><p><img src="https://xiangfa.org/images/logo.svg" alt="梦翼坊 Logo" title="这是梦翼坊的 Logo"></p><p>当然，你也可以像链接那样对图片网址使用变量。</p><pre><code class="line-numbers language-md">这是梦翼坊的 Logo：![梦翼坊 Logo][logo][logo]: https://xiangfa.org/images/logo.svg</code></pre><p>效果如下：</p><p>这是梦翼坊的 Logo：<img src="https://xiangfa.org/images/logo.svg" alt="梦翼坊 Logo"></p><h2 id="表格"><a href="#表格" class="headerlink" title="表格"></a>表格</h2><p>Markdown 制作表格使用 <code>|</code> 来分隔不同的单元格，使用<strong>一个以上</strong> <code>-</code> 来分隔表头和其他行。</p><pre><code class="line-numbers language-md">|  表头   | 表头  ||  ----  | ----  || 单元格  | 单元格 || 单元格  | 单元格 |</code></pre><p>效果如下：</p><table><thead><tr><th>表头</th><th>表头</th></tr></thead><tbody><tr><td>单元格</td><td>单元格</td></tr><tr><td>单元格</td><td>单元格</td></tr></tbody></table><p>我们可以通过 <code>-</code> 与 <code>:</code> 的组合设置表格的对齐方式：</p><ul><li><code>-:</code> 设置内容和标题栏居右对齐</li><li><code>:-</code> 设置内容和标题栏居左对齐</li><li><code>:-:</code> 设置内容和标题栏居中对齐</li></ul><p><strong>注意</strong>：<code>-</code> 的个数不会影响显示效果。</p><p>实例如下：</p><pre><code class="line-numbers language-md">| 左对齐 | 居中对齐 | 右对齐 || :-----| :----: | ----: || 单元格 | 单元格 | 单元格 || 单元格 | 单元格 | 单元格 |</code></pre><p>效果如下：</p><table><thead><tr><th align="left">左对齐</th><th align="center">居中对齐</th><th align="right">右对齐</th></tr></thead><tbody><tr><td align="left">单元格</td><td align="center">单元格</td><td align="right">单元格</td></tr><tr><td align="left">单元格</td><td align="center">单元格</td><td align="right">单元格</td></tr></tbody></table><h2 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h2><p>代码可以分为行内区块、代码区块以及代码片段三种形式。</p><h3 id="行内区块"><a href="#行内区块" class="headerlink" title="行内区块"></a>行内区块</h3><p>如果是段落上的一个函数或片段的代码可以用反引号(`)把它包起来，比如：</p><pre><code class="line-numbers language-md">在网页上可以使用 `alert(&#39;Hello World&#39;)` 函数进行页面提示</code></pre><p>效果如下：</p><p>在网页上可以使用 <code>alert(&#39;Hello World&#39;)</code> 函数进行页面提示</p><h3 id="代码区块"><a href="#代码区块" class="headerlink" title="代码区块"></a>代码区块</h3><p>代码区块使用四个空格或者一个制表符（Tab 键）来表示。</p><pre><code class="line-numbers language-md">    function say(text) {      alert(text)    }    say(&#39;Hello World&#39;)</code></pre><p>效果如下：</p><pre><code>function say(text) {  alert(text)}say(&#39;Hello World&#39;)</code></pre><h3 id="代码片段"><a href="#代码片段" class="headerlink" title="代码片段"></a>代码片段</h3><p>目前很多 Markdown 编辑器都支持代码高亮，但常规的代码区块方式无法明确代码语言导致无法精确的代码高亮，因此代码片段的方式更适合用于代码高亮。代码片段的语法格式如下：</p><pre><code>```语言类型这里是具体的代码片段```</code></pre><p>实例如下：</p><pre><code>```JavaScriptfunction say(text) {  alert(text)}say(&#39;Hello World&#39;)```</code></pre><p>效果如下：</p><pre><code class="line-numbers language-JavaScript">function say(text) {  alert(text)}say(&#39;Hello World&#39;)</code></pre><h2 id="高级技巧"><a href="#高级技巧" class="headerlink" title="高级技巧"></a>高级技巧</h2><h3 id="支持混合使用-HTML-标签"><a href="#支持混合使用-HTML-标签" class="headerlink" title="支持混合使用 HTML 标签"></a>支持混合使用 HTML 标签</h3><p>本文一开始就提到 Markdown 最终以 HTML 格式发布，因此你完全可以在 Markdown 里直接混用标准的 HTML 标签。</p><pre><code class="line-numbers language-md">&lt;u&gt;在 Windows 操作系统中&lt;/u&gt;，你可以使用 &lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;Alt&lt;/code&gt;+&lt;code&gt;Del&lt;/code&gt; 重启电脑</code></pre><p>效果如下：</p><p><u>在 Windows 操作系统中</u>，你可以使用 <code>Ctrl</code>+<code>Alt</code>+<code>Del</code> 重启电脑</p><h3 id="转义"><a href="#转义" class="headerlink" title="转义"></a>转义</h3><p>Markdown 使用了很多特殊符号来表示特定的意义，如果需要显示特定的符号则需要使用转义字符，Markdown 使用反斜杠转义特殊字符：</p><pre><code class="line-numbers language-md">**文本加粗**  \*\* 正常显示星号 \*\*</code></pre><p>效果如下：</p><p><strong>文本加粗</strong><br>** 正常显示星号 **</p><p>Markdown 支持以下这些符号前面加上反斜杠来帮助插入普通的符号：</p><pre><code>\   反斜线`   反引号*   星号_   下划线{}  花括号[]  方括号()  小括号#   井字号+   加号-   减号.   英文句点!   感叹号</code></pre><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>不同的 Markdown 编辑器对 Markdown 的语法支持程度也不同，部分 Markdown 编辑器通过插件的方式可以支持<strong>公式</strong>、<strong>流程图</strong>、<strong>时序图</strong>、<strong>甘特图</strong>等进阶语法。这些语法并不属于 Markdown 的基础语法，因此这里也不再展开叙述，你可以根据实际情况选择性的学习。</p><h2 id="文章源码"><a href="#文章源码" class="headerlink" title="文章源码"></a>文章源码</h2><p>本文介绍了 Markdown 的基础用法，当然你看到的这篇文章也完全是用 Markdown 编写，你可以在<a href="https://raw.githubusercontent.com/Amery2010/xiangfa.org/master/source/_posts/markdown-grammar-guide.md" target="_blank" rel="noopener">这里</a> 查看当前文章的 Markdown 源码。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://www.runoob.com/markdown/md-tutorial.html" target="_blank" rel="noopener">Markdown 教程</a></li><li><a href="https://www.jianshu.com/p/191d1e21f7ed" target="_blank" rel="noopener">Markdown基本语法</a></li><li><a href="https://github.com/younghz/Markdown" target="_blank" rel="noopener">Markdown 基本语法</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Markdown 是一种轻量级标记语言，它以纯文本形式（易读、易写、易更改）编写文档，并最终以 HTML 格式发布。通过简单的标记语法，它可以使普通文本内容具有一定的格式。相比所见即所得（WYSIWYG）编辑器，Markdown 可以让使用者摆脱排版的困扰，专心于写作。当前许多网站都广泛使用 Markdown 来撰写帮助文档或是用于论坛上发表消息。例如：GitHub、简书、掘金等。&lt;/p&gt;
    
    </summary>
    
    
      <category term="教程" scheme="https://xiangfa.org/categories/%E6%95%99%E7%A8%8B/"/>
    
    
      <category term="Markdown" scheme="https://xiangfa.org/tags/Markdown/"/>
    
      <category term="md" scheme="https://xiangfa.org/tags/md/"/>
    
      <category term="语法指南" scheme="https://xiangfa.org/tags/%E8%AF%AD%E6%B3%95%E6%8C%87%E5%8D%97/"/>
    
  </entry>
  
  <entry>
    <title>算法学习笔记 — 数字与字符串处理</title>
    <link href="https://xiangfa.org/2020/09/algorithm-for-string/"/>
    <id>https://xiangfa.org/2020/09/algorithm-for-string/</id>
    <published>2020-09-07T09:15:46.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>算法并不是什么高深的领域内容，我们平时在项目开发中几乎都有涉及，比如翻转字符串、字符串截取，这些熟悉的函数方法你一定有所接触。</p><a id="more"></a><h2 id="翻转整数"><a href="#翻转整数" class="headerlink" title="翻转整数"></a>翻转整数</h2><blockquote><p>给出一个 32 位的有符号整数，你需要将这个整数中每位上的数字进行反转。</p></blockquote><pre><code>示例 1:输入: 123输出: 321示例 2:输入: -123输出: -321示例 3:输入: 120输出: 21</code></pre><p>假设我们的环境只能存储得下 32 位的有符号整数，则其数值范围为 $ \begin{bmatrix} -2^{31}, 2^{31} - 1 \end{bmatrix} $。请根据这个假设，如果反转后整数溢出那么就返回 0。</p><p>详解：<a href="https://leetcode-cn.com/problems/reverse-integer/solution/zheng-shu-fan-zhuan-by-leetcode/" target="_blank" rel="noopener">LeetCode 整数反转</a></p><h3 id="解法一：利用数组的-reverse-方法"><a href="#解法一：利用数组的-reverse-方法" class="headerlink" title="解法一：利用数组的 reverse 方法"></a>解法一：利用数组的 reverse 方法</h3><p>思路：在 JavaScript 中数组有一个 reverse 方法，该方法是将数组元素进行翻转。既然数组有翻转的方法，那么我们能不能想办法将数字转换成数组呢？目前虽然没有直接将数字转换成数组的方法，但我们可以考虑先将数字变成字符串，然后将字符串数值转换数组，利用数组的 reverse 方法翻转数组后，将转换回数字。</p><pre><code class="line-numbers language-js">/** * 翻转整数 * @param {number} num 需要翻转的整数 * @return {number} 翻转后的整数 */function reverseInteger(num) {  // 异常值处理  if (typeof num !== &#39;number&#39;) return NaN  // 获取绝对值状态下的翻转整数  let result = parseInt(Math.abs(num).toString().split(&#39;&#39;).reverse().join(&#39;&#39;), 10)  // 补充符号  result = num &lt; 0 ? 0 - result : result  // 判断溢出  if (result &gt;= Math.pow(2, 31) - 1 || result &lt;= Math.pow(-2, 31) + 1) return 0  return result}</code></pre><h4 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>当前算法将数值转换为字符串进行字符串转数组，数组翻转以及数组转字符串操作，该过程时间消耗与数值的位数 $ n $ 有关，可以认为是 $ 3n $ 的时间消耗，忽略系数，因此时间复杂度可以认为是 $ O(n) $。<br>考虑到该题限制了数值范围为 32 位的有符号整数，其最大整数位长度为 <code>11</code>，也可以认为是常数时间复杂度 $ O(1) $。</p></li><li><p>空间复杂度：$ O(n) $<br>当前算法在转换过程中临时创建了字符串和数组对象，临时空间的大小与数值的位数 $ n $ 有关，因此空间复杂度为 $ O(n) $。<br>考虑到该题限制了数值范围为 32 位的有符号整数，其最大整数位长度为 <code>11</code>，也可以认为是常数空间复杂度 $ O(1) $。</p></li></ul><h3 id="解法二：借助欧几里得算法求解"><a href="#解法二：借助欧几里得算法求解" class="headerlink" title="解法二：借助欧几里得算法求解"></a>解法二：借助欧几里得算法求解</h3><p>思路：我们借鉴欧几里得求最大公约数的方法来解题。符号的处理逻辑不变，对于整数部分我们通过模 <code>10</code> 取到最低位，然后又通过乘 <code>10</code> 将最低位迭代到最高位，完成数值翻转。</p><pre><code class="line-numbers language-js">/** * 翻转整数 * @param {number} num 需要翻转的整数 * @return {number} 翻转后的整数 */function reverseInteger(num) {  // 异常值处理  if (typeof num !== &#39;number&#39;) return NaN  // 获取相应数的绝对值  let int = Math.abs(num)  let result = 0  // 遍历循环生成每一位数字  while (int !== 0) {    // 借鉴欧几里得算法，从 num 的最后一位开始取值拼成新的数    result = int % 10 + result * 10    // 剔除掉被消费的部分    int = Math.floor(int / 10)  }  // 补充符号  result = num &lt; 0 ? 0 - result : result  // 判断溢出  if (result &gt;= Math.pow(2, 31) - 1 || result &lt;= Math.pow(-2, 31) + 1) return 0  return result}</code></pre><h4 id="复杂度分析-1"><a href="#复杂度分析-1" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>由于当前算法只使用了 <code>while</code> 循环，循环次数为 $ n $ 次，即数值的整数长度，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(1) $<br>当前算法只用到了常数个变量，因此空间复杂度为 $ O(1) $。</p></li></ul><h2 id="有效的字母异位词"><a href="#有效的字母异位词" class="headerlink" title="有效的字母异位词"></a>有效的字母异位词</h2><blockquote><p>给定两个字符串 s 和 t ，编写一个函数来判断 t 是否是 s 的字母异位词。</p></blockquote><pre><code>示例 1:输入: s = &quot;anagram&quot;, t = &quot;nagaram&quot;输出: true示例 2:输入: s = &quot;rat&quot;, t = &quot;car&quot;输出: false</code></pre><p>详解：<a href="https://leetcode-cn.com/problems/valid-anagram/solution/you-xiao-de-zi-mu-yi-wei-ci-by-leetcode/" target="_blank" rel="noopener">LeetCode 有效的字母异位词</a></p><h3 id="解法一：利用数组的-sort-方法"><a href="#解法一：利用数组的-sort-方法" class="headerlink" title="解法一：利用数组的 sort 方法"></a>解法一：利用数组的 sort 方法</h3><p>思路：先将字符串转为数组，利用数组的 sort 方法进行默认排序，将排序后的数组转会字符串，比较字符串是否相等。</p><pre><code class="line-numbers language-js">/** * 判断是否为有效的字母异位词 * @param {string} source 当前字符串 * @param {string} target 目标字符串 * @return {boolean} 排序后的字符串 */function isAnagram(source, target) {  if (typeof source !== &#39;string&#39; || typeof target !== &#39;string&#39;) {    return false  }  /**   * 字符串排序   * @param {string} str 需要排序的字符串   * @return {string} 排序后的字符串   */  const sortString = str =&gt; {    return str.split(&#39;&#39;).sort().join(&#39;&#39;)  }  return sortString(source) === sortString(target)}</code></pre><h4 id="复杂度分析-2"><a href="#复杂度分析-2" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(nlog(n)) $<br>当前算法主要借助了数组的 <code>sort</code> 方法，但 JavaScript 中 <code>sort</code> 方法的实现原理，当数组长度小于等于 10 的时候，采用插入排序 $ O(n^2) $，大于 10 的时候，采用快速排序，快速排序的平均时间复杂度是 $ O(nlog(n)) $。</p></li><li><p>空间复杂度：$ O(n) $<br>算法中申请了 2 个数组变量用于存放字符串分割后的字符串数组，所以数组空间长度跟字符串长度线性相关，所以为 $ O(n) $。</p></li></ul><h3 id="解法二：计数累加法"><a href="#解法二：计数累加法" class="headerlink" title="解法二：计数累加法"></a>解法二：计数累加法</h3><p>思路：先统计第一个字符串的字符类型和出现次数，然后对另一个字符串进行对比统计，如果字符类型不一致或者出现次数不同，则表示两个字符串不相等。</p><pre><code class="line-numbers language-js">/** * 判断是否为有效的字母异位词 * @param {string} source 当前字符串 * @param {string} target 目标字符串 * @return {boolean} 排序后的字符串 */function isAnagram(source, target) {  if (typeof source !== &#39;string&#39; || typeof target !== &#39;string&#39;) {    return false  }  if (source.length !== target.length) {    return false  }  const hash = Object.create(null)  for (const k of source) {    hash[k] = k in hash ? hash[k] + 1 : 1  }  for (const k of target) {    if (!(k in hash)) return false    hash[k] -= 1  }  return true}</code></pre><h4 id="复杂度分析-3"><a href="#复杂度分析-3" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>当前算法只使用了两次单循环，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(1) $<br>当前算法只使用 <code>hash</code> 和 <code>k</code> 两个变量，空间大小不随字符串的变量而变化。</p></li></ul><h2 id="字符串中的第一个唯一字符"><a href="#字符串中的第一个唯一字符" class="headerlink" title="字符串中的第一个唯一字符"></a>字符串中的第一个唯一字符</h2><blockquote><p>给定一个字符串，假定该字符串只包含小写字母，找到它的第一个不重复的字符，并返回它的索引。如果不存在，则返回 -1。</p></blockquote><pre><code>s = &quot;leetcode&quot;返回 0.s = &quot;loveleetcode&quot;,返回 2.</code></pre><p>详解：<a href="https://leetcode-cn.com/problems/first-unique-character-in-a-string/solution/zi-fu-chuan-zhong-de-di-yi-ge-wei-yi-zi-fu-by-leet/" target="_blank" rel="noopener">LeetCode 字符串中的第一个唯一字符</a></p><h3 id="解法一：利用-js-自带方法求解"><a href="#解法一：利用-js-自带方法求解" class="headerlink" title="解法一：利用 js 自带方法求解"></a>解法一：利用 js 自带方法求解</h3><p>思路：如果字符串某个字符的正向索引值和反向索引值相同，则表示该字符只出现了一次。</p><pre><code class="line-numbers language-js">/** * 获取字符串中的第一个唯一字符 * @param {string} str 目标字符串 * @return {number} 唯一字符的索引值，如果不存在，则返回 -1。 */function firstUniqChar(str) {  if (typeof str !== &#39;string&#39;) return -1  for (let i = 0; i &lt; str.length; i += 1) {    if (str.indexOf(str[i]) === str.lastIndexOf(str[i])) {      return i    }  }  return -1}</code></pre><h4 id="复杂度分析-4"><a href="#复杂度分析-4" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n^2) $<br>循环的时间复杂度为 $ O(n) $，<code>indexOf</code> 与 <code>lastIndexOf</code> 的时间复杂度均为 $ O(n) $，所以总时间复杂度应该为 $ O(n^2) $。</p></li><li><p>空间复杂度：$ O(1) $<br>除了临时变量 <code>i</code>，没有开辟额外的存储空间。</p></li></ul><h3 id="解法二：利用哈希"><a href="#解法二：利用哈希" class="headerlink" title="解法二：利用哈希"></a>解法二：利用哈希</h3><p>思路：先使用一个对象存储所有的字符出现次数，再找出对象中字符只出现一次的下标。</p><pre><code class="line-numbers language-js">/** * 获取字符串中的第一个唯一字符 * @param {string} str 目标字符串 * @return {number} 唯一字符的索引值，如果不存在，则返回 -1。 */function firstUniqChar(str) {  if (typeof str !== &#39;string&#39;) return -1  const hash = Object.create(null)  for (const k of str) {    hash[k] = k in hash ? hash[k] + 1 : 1  }  for (let i = 0; i &lt; str.length; i += 1) {    if (hash[str[i]] === 1) {      return i    }  }  return -1}</code></pre><h4 id="复杂度分析-5"><a href="#复杂度分析-5" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法存在两次遍历，每次遍历的时间复杂度为 $ O(n) $，因为不存在嵌套遍历，因此时间复杂度只与变量 <code>str</code> 有关。</p></li><li><p>空间复杂度：$ O(1) $<br>当前算法只使用 <code>hash</code>、<code>k</code> 和 <code>i</code> 三个变量，空间大小不随字符串的变量而变化。</p></li></ul><h2 id="验证回文串"><a href="#验证回文串" class="headerlink" title="验证回文串"></a>验证回文串</h2><blockquote><p>给定一个字符串，验证它是否是回文串，只考虑字母和数字字符，可以忽略字母的大小写。本题中，我们将空字符串定义为有效的回文串。</p></blockquote><pre><code>示例 1:输入: &quot;A man, a plan, a canal: Panama&quot;输出: true示例 2:输入: &quot;race a car&quot;输出: false</code></pre><p>详解：<a href="https://leetcode-cn.com/problems/valid-palindrome/solution/yan-zheng-hui-wen-chuan-by-leetcode-solution/" target="_blank" rel="noopener">LeetCode 验证回文串</a></p><h3 id="解法一：字符串遍历"><a href="#解法一：字符串遍历" class="headerlink" title="解法一：字符串遍历"></a>解法一：字符串遍历</h3><p>思路：先移除字符串中的非字母和数字，再将字符串转换为数组，再对数组首尾一一比较，即可得出结果。</p><pre><code class="line-numbers language-js">/** * 验证回文串 * @param {string} str 需要判断的字符串 * @return {boolean} 是否为回文串 */function isPalindrome(str) {  if (typeof str !== &#39;string&#39;) return false  // 将传入的字符串,统一转化为小写,同时去除非字母和数字,在转换为数组  const strArr = str.toLowerCase().replace(/[^a-z0-9]/g, &#39;&#39;).split(&#39;&#39;)  let i = 0  let j = strArr.length - 1  // 循环比较元素  while (i &lt; j) {    // 从首尾开始, 一一比较元素是否相等    if (strArr[i] === strArr[j]) {      // 若相等,即第二个元素和倒数第二个元素继续比较,依次类推      i += 1      j -= 1    } else {      // 只要有一个相对位置上不相等,既不是回文串      return false    }  }  return true}</code></pre><h4 id="复杂度分析-6"><a href="#复杂度分析-6" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法中 while 循环最多执行 $ n/2 $ 次，因此时间复杂度为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(n) $<br>该算法使用了长度为 $ n $ 的数组，因此空间复杂度为 $ O(n) $。</p></li></ul><h3 id="解法二：利用数组的-reverse-方法"><a href="#解法二：利用数组的-reverse-方法" class="headerlink" title="解法二：利用数组的 reverse 方法"></a>解法二：利用数组的 reverse 方法</h3><p>思路：先移除字符串中的非字母和数字，然后利用数组的 reverse 方法将字符串翻转，再和原字符串进行比较，即可得到结果。</p><pre><code class="line-numbers language-js">/** * 验证回文串 * @param {string} str 需要判断的字符串 * @return {boolean} 是否为回文串 */function isPalindrome(str) {  if (typeof str !== &#39;string&#39;) return false  // 将传入的字符串,统一转化为小写,同时去除非字母和数字,在转换为数组  const strArr = str.toLowerCase().replace(/[^a-z0-9]/g, &#39;&#39;).split(&#39;&#39;)  // 将2个字符进行比较得出结果  return strArr.join(&#39;&#39;) === strArr.reverse().join(&#39;&#39;)}</code></pre><h4 id="复杂度分析-7"><a href="#复杂度分析-7" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n) $<br>该算法中所用的 js 方法的时间复杂度都为 $ O(n) $，且都在独立的循环中执行，因此，总的时间复杂度依然为 $ O(n) $。</p></li><li><p>空间复杂度：$ O(n) $<br>该算法使用了长度为 $ n $ 的数组，因此空间复杂度为 $ O(n) $。</p></li></ul><h2 id="最长回文子串"><a href="#最长回文子串" class="headerlink" title="最长回文子串"></a>最长回文子串</h2><blockquote><p>给定一个字符串 str，找到 str 中最长的回文子串。你可以假设 str 的最大长度为 1000。</p></blockquote><pre><code>示例输入: &quot;babad&quot;输出: &quot;bab&quot;注意: &quot;aba&quot; 也是一个有效答案。</code></pre><p>详解：<a href="https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/" target="_blank" rel="noopener">LeetCode 最长回文子串</a></p><h3 id="解法一：动态规划法"><a href="#解法一：动态规划法" class="headerlink" title="解法一：动态规划法"></a>解法一：动态规划法</h3><p>动态规划的思想，是希望把问题划分成相关联的子问题；然后从最基本的子问题出发来推导较大的子问题，直到所有的子问题都解决。<br>根据字符串的长度，建立一个矩阵 dp, 通过不同情况的判断条件，通过 <code>dp[i][j]</code> 表示 <code>s[i]</code> 至 <code>s[j]</code> 所代表的子串是否是回文子串。</p><pre><code class="line-numbers language-js">/** * 获得最长回文子串 * @param {string} str 需要判断的字符串 * @return {string} 最长回文串 */function longestPalindrome(str) {  if (typeof str !== &#39;string&#39;) return &#39;&#39;  const dp = []  for (let i = 0; i &lt; str.length; i++) {    dp[i] = []  }  let max = -1  let result = &#39;&#39;  for (let l = 0; l &lt; str.length; l++) {    // l为所遍历的子串长度 -1，即左下标到右下标的长度    for (let i = 0; i + l &lt; str.length; i++) {      const j = i + l      // i为子串开始的左下标，j为子串开始的右下标      if (l === 0) {        // 当子串长度为1时，必定是回文子串        dp[i][j] = true      } else if (l &lt;= 2) {        // 长度为2或3时，首尾字符相同则是回文子串        if (str[i] === str[j]) {          dp[i][j] = true        } else {          dp[i][j] = false        }      } else {        // 长度大于3时，若首尾字符相同且去掉首尾之后的子串仍为回文，则为回文子串        if ((str[i] === str[j]) &amp;&amp; dp[i + 1][j - 1]) {          dp[i][j] = true        } else {          dp[i][j] = false        }      }      if (dp[i][j] &amp;&amp; l &gt; max) {        max = l        result = str.substring(i, j + 1)      }    }  }  return result}</code></pre><h4 id="复杂度分析-8"><a href="#复杂度分析-8" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n^2) $<br>该算法存在两层遍历，因此时间复杂度为 $ O(n^2) $。</p></li><li><p>空间复杂度：$ O(n) $<br>该算法申请了一个长度为 <code>n</code> 的数组用于结果存储，因此空间复杂度为 $ O(n) $。</p></li></ul><h3 id="解法二：中心扩展"><a href="#解法二：中心扩展" class="headerlink" title="解法二：中心扩展"></a>解法二：中心扩展</h3><p>思路：回文子串一定是对称的，所以我们可以每次选择一个中心，然后从中心向两边扩展判断左右字符是否相等。<br>中心点的选取有两种情况：  </p><ul><li>当长度为奇数时，以单个字符为中心。</li><li>当长度为偶数时，以两个字符之间的空隙为中心。</li></ul><pre><code class="line-numbers language-js">/** * 获得最长回文子串 * @param {string} str 需要判断的字符串 * @return {string} 最长回文串 */function longestPalindrome(str) {  if (typeof str !== &#39;string&#39;) return &#39;&#39;  if (str.length &lt; 1) return &#39;&#39;  const expandFromCenter = (str, left, right) =&gt; {    while (left &gt;= 0 &amp;&amp; right &lt; str.length &amp;&amp; str[left] === str[right]) {      left -= 1      right += 1    }    return right - left - 1  }  let start = 0  let end = 0  for (let i = 0; i &lt; str.length; i++) {    // 中心的两种选取（奇对称和偶对称）    const len1 = expandFromCenter(str, i, i)    const len2 = expandFromCenter(str, i, i + 1)    // 两种组合取最大的回文子串长度    const len = Math.max(len1, len2)    // 如果此位置为中心的回文数长度大于之前的长度，则进行处理    if (len &gt; end - start) {      start = i - Math.floor((len - 1) / 2)      end = i + Math.floor(len / 2)    }  }  return str.substring(start, end + 1)}</code></pre><h4 id="复杂度分析-9"><a href="#复杂度分析-9" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><ul><li><p>时间复杂度：$ O(n^2) $<br>该算法存在两层循环嵌套，因此遍历的最大次数为 $ n^2 $。</p></li><li><p>空间复杂度：$ O(1) $<br>该算法只使用了常量，因此空间复杂度为 $ O(1) $。</p></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;算法并不是什么高深的领域内容，我们平时在项目开发中几乎都有涉及，比如翻转字符串、字符串截取，这些熟悉的函数方法你一定有所接触。&lt;/p&gt;
    
    </summary>
    
    
      <category term="算法学习笔记" scheme="https://xiangfa.org/categories/%E7%AE%97%E6%B3%95%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="算法" scheme="https://xiangfa.org/tags/%E7%AE%97%E6%B3%95/"/>
    
      <category term="学习笔记" scheme="https://xiangfa.org/tags/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
      <category term="数字与字符串处理" scheme="https://xiangfa.org/tags/%E6%95%B0%E5%AD%97%E4%B8%8E%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%A4%84%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>算法复杂度</title>
    <link href="https://xiangfa.org/2020/09/algorithm-complexity/"/>
    <id>https://xiangfa.org/2020/09/algorithm-complexity/</id>
    <published>2020-09-03T09:23:43.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>算法是指用来操作数据、解决程序问题的一组方法，而算法复杂度是考评算法执行效率和资源消耗的一个重要指标。</p><p>算法优劣主要从「时间」和「空间」两个维度去考量。</p><p>时间维度：是指执行当前算法所消耗的时间，我们通常用「时间复杂度」来描述。<br>空间维度：是指执行当前算法需要占用多少内存空间，我们通常用「空间复杂度」来描述。</p><p>在符合算法本身的要求的基础上，编写的程序运行时间越短，运行过程中占用的内存空间越少，意味着这个算法越“好”。</p><a id="more"></a><h2 id="时间复杂度"><a href="#时间复杂度" class="headerlink" title="时间复杂度"></a>时间复杂度</h2><p><strong>时间复杂度是描述算法运行的时间。我们把算法需要运算的次数用输入大小为 $ n $ 的函数来表示，计作 $ T(n) $。时间复杂度通常用 $ O(n) $ 来表示，公式为 $ T(n)=O(f(n)) $，其中 $ f(n) $ 表示每行代码的执行次数之和，注意是执行次数。</strong></p><h3 id="常见的时间复杂度"><a href="#常见的时间复杂度" class="headerlink" title="常见的时间复杂度"></a>常见的时间复杂度</h3><table><thead><tr><th>名称</th><th>运行时间 $ T(n) $</th><th>时间举例</th><th>算法举例</th></tr></thead><tbody><tr><td>常数</td><td>$ O(1) $</td><td>$ 1 $</td><td>加法运算</td></tr><tr><td>线性</td><td>$ O(n) $</td><td>$ n $</td><td>数组遍历</td></tr><tr><td>平方</td><td>$ O(n^2) $</td><td>$ n^2 $</td><td>冒泡排序</td></tr><tr><td>立方</td><td>$ O(n^3) $</td><td>$ n^3 $</td><td>矩阵乘法</td></tr><tr><td>对数</td><td>$ O(log(n)) $</td><td>$ log(n) $</td><td>二分搜索</td></tr><tr><td>线性对数</td><td>$ O(nlog(n)) $</td><td>$ nlog(n) $</td><td>比较排序</td></tr></tbody></table><h4 id="常数的时间复杂度-O-1"><a href="#常数的时间复杂度-O-1" class="headerlink" title="常数的时间复杂度 $ O(1) $"></a>常数的时间复杂度 $ O(1) $</h4><p><strong>算法执行所需要的时间不随着某个变量 $ n $ 的变化而变化，即此算法时间复杂度为一个常量，可表示为 $ O(1) $。</strong></p><p>比如，最常见的加法运算：</p><pre><code class="line-numbers language-js">const sum = 1 + 2</code></pre><p>上述代码在执行的时候，它执行所需的时间并不随着某个变量 $ n $ 的变化而变化，这类代码即便有几十万行，都可以用 $ O(1) $ 来表示它的时间复杂度。</p><h4 id="线性的时间复杂度-O-n"><a href="#线性的时间复杂度-O-n" class="headerlink" title="线性的时间复杂度 $ O(n) $"></a>线性的时间复杂度 $ O(n) $</h4><pre><code class="line-numbers language-js">for (let i = 0; i &lt; n; i++) {  console.log(i)}</code></pre><p>for 循环里面的代码会执行 $ n $ 次，因此它执行所需时间是随着 $ n $ 的变化而变化，因此得到复杂度为 $ O(n) $。</p><h4 id="平方的时间复杂度-O-n-2"><a href="#平方的时间复杂度-O-n-2" class="headerlink" title="平方的时间复杂度 $ O(n^2) $"></a>平方的时间复杂度 $ O(n^2) $</h4><pre><code class="line-numbers language-js">for (let i = 0; i &lt; n; i++) {  for (let j = 0; j &lt; n; i++) {    console.log(i, j)  }}</code></pre><p>上述代码存在两个循环，外层需要执行 $ n $ 次，内层也需要执行 $ n $ 次，它的时间复杂度就是 $ O(n * n) $，即 $ O(n^2) $。</p><h4 id="对数的时间复杂度-O-log-n"><a href="#对数的时间复杂度-O-log-n" class="headerlink" title="对数的时间复杂度 $ O(log(n)) $"></a>对数的时间复杂度 $ O(log(n)) $</h4><p>在数学中，<a href="https://zh.wikipedia.org/wiki/%E5%AF%B9%E6%95%B0" target="_blank" rel="noopener">对数</a>是幂运算的逆运算。换句话说，假如 $ x = \beta^y $，则有 $ y = log_\beta x $，其中 $ \beta $ 是对数的底（也称为基数），而 $ y $ 就是 $ x $（对于底数 $ \beta $ ）的对数。</p><pre><code class="line-numbers language-js">let i = 1while(i &lt;= y) {  i *= 2}</code></pre><p>对数复杂度 $ O(log(n)) $ 是比较常见的一种复杂度，也是比较难分析的一种复杂度。观察上面的代码， $ i $ 从 1 开始，每循环一次就乘以 2，直到 $ i $ 大于 $ n $ 时结束循环。</p><p>$$ 2^1 -&gt; 2^2 -&gt; 2^3 -&gt; … -&gt; 2^x $$</p><p>观察上面列出 $ i $ 的取值发现，是一个等比数列，要知道循环了多少次，求出 $ x $ 的值即可。由 $ 2^x = n $ 得到 $ x = log_2 n $，所以这段代码的时间复杂度为 $ log_2 n $。</p><p>如果把上面的 i *= 2 改为 i *= 3，那么这段代码的时间复杂度就是 $ log_3 n $。</p><p>根据<a href="https://zh.wikipedia.org/wiki/%E5%AF%B9%E6%95%B0%E6%81%92%E7%AD%89%E5%BC%8F" target="_blank" rel="noopener">换底公式</a>：</p><p>$$ log_c a * log_a b = log_c b $$</p><p>因此 $ log_3 n = log_3 2 * log_2 n $，其中 $ log_3 2 $ 是一个常量，得到 $ O(log_3 n) = O(log_2 n) $。当我们忽略对数的“底”之后，我们可以获得 $ O(log(n)) $ 的复杂度。</p><h4 id="递归的时间复杂度"><a href="#递归的时间复杂度" class="headerlink" title="递归的时间复杂度"></a>递归的时间复杂度</h4><p>我们在面试的时候，经常会被问及递归的概念，那么递归的时间复杂度如何计算呢？</p><p>递归算法中，每个递归函数的的时间复杂度为 $ O(s) $，递归的调用次数为 $ n $，则该递归算法的时间复杂度为 $ O(n) = n * O(s) $，其中 $ O(s) $ 可能是任意的时间复杂度。因此，我们可以认为递归的时间复杂是一个复合型的时间复杂度。</p><p>我们以经典的<a href="https://zh.wikipedia.org/zh-hans/%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97" target="_blank" rel="noopener">斐波那契数列</a>（<code>Fibonacci sequence</code>）为例：</p><p>$$ F(0) = 1, F(1) = 1, F(n) = F(n−1) + F(n−2)(n ≥ 2, n \in N ∗) $$</p><pre><code class="line-numbers language-js">function fibonacci(n) {  if (n === 0 || n === 1) return 1  return fibonacci(n - 1) + fibonacci(n - 2)}</code></pre><p>我们很容易写出上面这样一段递归的代码，往往会忽略了时间复杂度是多少，换句话说调用多少次。可以代一个数进去，例如 n = 5，完了之后大概就能理解递归的时间复杂度是怎么来的。</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/ee51a0178eb6fd03281a77beeec928a3.png" alt="fibonacci"></p><p>上图把 n = 5 的情况都列举出来。可以看出，虽然代码非常简单，在实际运算的时候会有大量的重复计算。</p><p>在 $ n $ 层的完全二叉树中，节点的总数为 $ 2^n - 1 $，所以得到 $ F(n) $ 中递归数目的上限为 $ 2^n - 1 $。因此我们可以毛估出 $ F(n) $ 的时间复杂度为 $ O(2^n) $。</p><p>时间复杂度为 $ O(2^n) $，指数级的时间复杂度，显然不是最优的解法。</p><pre><code class="line-numbers language-js">// 黄金分割率const goldenRatio = (1 + Math.sqrt(5)) / 2function fibonacci(n) {  return Math.round((Math.pow(goldenRatio, n - 1) + Math.pow(goldenRatio, n)) / Math.sqrt(5))}</code></pre><p>以上写法是借助了斐波那契数列和黄金分割的关系，开普勒发现数列前、后两项之比 1/2, 2/3, 3/5, 5/8, 8/13, 13/21, 21/34 …，也组成了一个数列，会趋近黄金分割：</p><p>$$ \frac{f_{n+5}}{f_n} \approx a = \frac{1}{2}({1 + \sqrt{5}}) = \varphi \approx 1.6180339887…  $$</p><p>因此，我们可以认为黄金分割率 $ \varphi $ 是一个常数，因此该算法的时间复杂度为 $ O(1) $。</p><h2 id="空间复杂度"><a href="#空间复杂度" class="headerlink" title="空间复杂度"></a>空间复杂度</h2><p>空间复杂度是对算法运行过程中临时占用空间大小的度量，也是一种趋势。一个算法所需的存储空间用 $ f(n) $ 表示，可得出 $ S(n) = O(f(n)) $，其中 $ n $ 为问题的规模，$ S(n) $ 表示空间复杂度。</p><h3 id="O-1-复杂度"><a href="#O-1-复杂度" class="headerlink" title="$ O(1) $ 复杂度"></a>$ O(1) $ 复杂度</h3><p>如果算法执行所需要的临时空间不随着某个变量 $ n $ 的大小而变化，即此算法空间复杂度为一个常量，可表示为 $ O(1) $。</p><pre><code class="line-numbers language-js">const i = 1const j = 2</code></pre><p>上述代码，执行所需要的临时空间不会随着处理数据量的变化而变化，因此得到空间复杂度为 $ O(1) $。</p><h3 id="O-n-复杂度"><a href="#O-n-复杂度" class="headerlink" title="$ O(n) $ 复杂度"></a>$ O(n) $ 复杂度</h3><pre><code class="line-numbers language-js">const arr = new Array(n)for (let i = 0; i &lt; n; i++) {  console.log(arr[i])}</code></pre><p>上述代码一开始申请了长度为 $ n $ 的数组空间，但之后的 for 循环中没有分配新的空间，可以得出这段代码的时间复杂度为 $ O(n) $，即 $ S(n) = O(n) $。</p><h2 id="时间空间相互转换"><a href="#时间空间相互转换" class="headerlink" title="时间空间相互转换"></a>时间空间相互转换</h2><p>对于一个算法来说，它的时间复杂度和空间复杂度往往是相互影响的。</p><p>那我们熟悉的 Chrome 来说，流畅性方面比其他厂商好了多人，但是占用的内存空间略大。</p><p>当追求一个较好的时间复杂度时，可能需要消耗更多的储存空间。 反之，如果追求较好的空间复杂度，算法执行的时间可能就会变长。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>常见的复杂度不多，从低到高排列就这么几个：$ O(1) $、$ O(log(n)) $、$ O(n) $、$ O(n^2) $，很多算法题涉及的算法复杂度也基本上只有这几种。</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/98b80545be56b4774d29b58f8946284b.png" alt="Big-O"></p><h3 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h3><ul><li><a href="https://101.zoo.team/kai-pian-fu-za-du" target="_blank" rel="noopener">算法 101</a></li><li><a href="https://zhuanlan.zhihu.com/p/50479555" target="_blank" rel="noopener">算法的时间与空间复杂度</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;算法是指用来操作数据、解决程序问题的一组方法，而算法复杂度是考评算法执行效率和资源消耗的一个重要指标。&lt;/p&gt;
&lt;p&gt;算法优劣主要从「时间」和「空间」两个维度去考量。&lt;/p&gt;
&lt;p&gt;时间维度：是指执行当前算法所消耗的时间，我们通常用「时间复杂度」来描述。&lt;br&gt;空间维度：是指执行当前算法需要占用多少内存空间，我们通常用「空间复杂度」来描述。&lt;/p&gt;
&lt;p&gt;在符合算法本身的要求的基础上，编写的程序运行时间越短，运行过程中占用的内存空间越少，意味着这个算法越“好”。&lt;/p&gt;
    
    </summary>
    
    
      <category term="算法学习笔记" scheme="https://xiangfa.org/categories/%E7%AE%97%E6%B3%95%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="算法" scheme="https://xiangfa.org/tags/%E7%AE%97%E6%B3%95/"/>
    
      <category term="复杂度" scheme="https://xiangfa.org/tags/%E5%A4%8D%E6%9D%82%E5%BA%A6/"/>
    
      <category term="时间复杂度" scheme="https://xiangfa.org/tags/%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6/"/>
    
      <category term="空间复杂度" scheme="https://xiangfa.org/tags/%E7%A9%BA%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6/"/>
    
  </entry>
  
  <entry>
    <title>让 Hexo 支持 LaTeX 公式</title>
    <link href="https://xiangfa.org/2020/09/let-hexo-support-latex-formulas/"/>
    <id>https://xiangfa.org/2020/09/let-hexo-support-latex-formulas/</id>
    <published>2020-09-02T12:11:42.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>通常情况下，你并不会去使用 LaTex 公式，但当你文章涉及数学公式时，你往往会在第一时间想到 $ LaTex $ 公式。网上有比较多的文章告诉你如何在 Hexo 里使用 $ LaTex $ 公式，但大部分都是推荐使用 <code>hexo-math</code> 插件来实现，但 <code>hexo-math</code> 的兼容性并不好，并且和 Hexo 内置的 MarkDown 渲染引擎存在冲突。因此我推荐使用另一种实现方案：通过增加 Hexo 主题脚本片段的方式实现 $ LaTex $ 语法高亮。</p><a id="more"></a><h2 id="让-Hexo-主题使用-Mathjax"><a href="#让-Hexo-主题使用-Mathjax" class="headerlink" title="让 Hexo 主题使用 Mathjax"></a>让 Hexo 主题使用 Mathjax</h2><p>你可以从主题文件的 <code>README.md</code> 确认你的 Hexo 主题是否已经引入了 <code>Mathjax</code>，如果已经引入，那你只要按照说明来开启 <code>Mathjax</code> 支持即可。如果你的主题并不支持 <code>Mathjax</code>，那你可以按照以下步骤使你的 Hexo 支持 <code>Mathjax</code>。</p><p>1、你需要在 <code>themes/YourThemeName/layout/</code> 下新建文件 <code>mathjax.ejs</code> 文件。</p><pre><code class="line-numbers language-html">&lt;% if (theme.mathjax.enable){ %&gt;&lt;script type=&quot;text/x-mathjax-config&quot;&gt;  MathJax.Hub.Config({    tex2jax: {      inlineMath: [ [&#39;$&#39;,&#39;$&#39;], [&quot;\\(&quot;,&quot;\\)&quot;] ],      processEscapes: true,      skipTags: [&#39;script&#39;, &#39;noscript&#39;, &#39;style&#39;, &#39;textarea&#39;, &#39;pre&#39;, &#39;code&#39;]    }  });  MathJax.Hub.Queue(function() {    var all = MathJax.Hub.getAllJax(), i;    for(i=0; i &lt; all.length; i += 1) {        all[i].SourceElement().parentNode.className += &#39; has-jax&#39;;    }  });&lt;/script&gt;&lt;script type=&quot;text/javascript&quot; src=&quot;&lt;%- theme.mathjax.cdn %&gt;&quot;&gt;&lt;/script&gt;&lt;% } %&gt;</code></pre><p>2、你需要在 <code>themes/YourThemeName/_config.yml</code> 末尾追加：</p><pre><code class="line-numbers language-yml"># MathJax Supportmathjax:  enable: true  cdn: https://cdn.jsdelivr.net/npm/mathjax@2.7.8/MathJax.js?config=TeX-AMS-MML_HTMLorMML</code></pre><p>3、修改 <code>themes/YourThemeName/layout/post.ejs</code> 文件，在中间添加：</p><pre><code class="line-numbers language-html">&lt;% if (theme.mathjax){ %&gt;  &lt;%- partial(&#39;mathjax&#39;) %&gt;&lt;% } %&gt;</code></pre><p>4、最好你需要在你用到 LaTex 公式的文章顶部配置里追加 Mathjax 配置：</p><pre><code class="line-numbers language-yml">---title: 文章标题date: 2020-09-02 20:11:42mathjax: true---</code></pre><p>至此，大功告成~<br>你最后只需要重新生成一下文章就可以看到你的文章现在已经支持 LaTex 公式了。</p><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>测试一下你的文章是否已经支持 LaTex 公式：</p><pre><code class="line-numbers language-bash">$$ J_\alpha(x) = \sum_{m=0}^\infty \frac{(-1)^m}{m! \Gamma (m + \alpha + 1)} {\left({ \frac{x}{2} }\right)}^{2m + \alpha} $$</code></pre><p>如果你的文章已经支持那么上面的 LaTex 语法将会有如下显示：</p><p>$$ J_\alpha(x) = \sum_{m=0}^\infty \frac{(-1)^m}{m! \Gamma (m + \alpha + 1)} {\left({ \frac{x}{2} }\right)}^{2m + \alpha} $$</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;通常情况下，你并不会去使用 LaTex 公式，但当你文章涉及数学公式时，你往往会在第一时间想到 $ LaTex $ 公式。网上有比较多的文章告诉你如何在 Hexo 里使用 $ LaTex $ 公式，但大部分都是推荐使用 &lt;code&gt;hexo-math&lt;/code&gt; 插件来实现，但 &lt;code&gt;hexo-math&lt;/code&gt; 的兼容性并不好，并且和 Hexo 内置的 MarkDown 渲染引擎存在冲突。因此我推荐使用另一种实现方案：通过增加 Hexo 主题脚本片段的方式实现 $ LaTex $ 语法高亮。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Hexo" scheme="https://xiangfa.org/categories/Hexo/"/>
    
    
      <category term="Hexo" scheme="https://xiangfa.org/tags/Hexo/"/>
    
      <category term="LaTex" scheme="https://xiangfa.org/tags/LaTex/"/>
    
  </entry>
  
  <entry>
    <title>利用 Node.js 中间层进行高效的敏捷开发思考</title>
    <link href="https://xiangfa.org/2020/08/use-nodejs-middleware-for-development/"/>
    <id>https://xiangfa.org/2020/08/use-nodejs-middleware-for-development/</id>
    <published>2020-08-05T11:31:18.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>在项目敏捷开发的过程中，经常会遭遇 Api 联调导致项目开发周期紧张的情况。目前国外很多大型互联网公司流行全栈开发概念，所谓全栈开发即一个项目的前后端逻辑都由一个人来完成。这种模式是的好处是基本上在开发环境没有联调的概念只有 Api 调试的概念。但在目前国内大环境背景下，全栈开发无疑是动了前端或后端工程师的“蛋糕”，所以全栈开发在国内依然是以概念的形式存在。如果前端开发人员利用中间层来处理这个问题，那么是否是一种新的解决思路呢？</p><a id="more"></a><h2 id="常规的业务开发流程"><a href="#常规的业务开发流程" class="headerlink" title="常规的业务开发流程"></a>常规的业务开发流程</h2><p>后端根据不同的业务需求给到前端特定的数据内容，不同的需求会产生不同的 api，但对于类似功能的需求，后端因为需求的不一致，都需要进行“定制”，即不同的 api 返回不同的内容，这种开发模式下，每次有类似的需求都需要开发不同的 api 来承载需求，久而久之，后端需要维护一大堆相似功能的 api。</p><p><strong>根据业务要求查询数据库 -&gt; 生成(过滤)数据 -&gt; 注册前端 api</strong></p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/2a25d0225d418b2ec769f1d813cf8684.png" alt="流程图"></p><h3 id="优势："><a href="#优势：" class="headerlink" title="优势："></a>优势：</h3><ul><li>根据需求查询数据，能直接满足业务的数据需求</li><li>后端人员对 api 开发经验丰富</li></ul><h3 id="劣势："><a href="#劣势：" class="headerlink" title="劣势："></a>劣势：</h3><ul><li>相似业务的数量越多，冗余的 api 也会越来越多</li><li>与前端需要进行业务 api 的联调</li><li>每次改动都需要进行后端发版，发版过程会影响少部分的线上用户数据</li></ul><h2 id="传统的微服务开发模式"><a href="#传统的微服务开发模式" class="headerlink" title="传统的微服务开发模式"></a>传统的微服务开发模式</h2><p>后端通过将 api 逻辑解耦成独立的微服务，后端维护服务 api 的功能可用性，可以最大程度的减少 api 的冗余。对于特殊业务数据需求依然需要开发独立的 api 来满足业务的需求。</p><p><strong>微服务 api 获取特定数据 -&gt; 注册服务 api</strong><br>**根据业务要求查询数据库 -&gt; 生成(过滤)数据 -&gt; 注册业务 api **</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/d4a948c1ba8e6e74cf306f96acbe90a7.png" alt="流程图"></p><h3 id="优势：-1"><a href="#优势：-1" class="headerlink" title="优势："></a>优势：</h3><ul><li>根据需求查询数据，能直接满足业务的数据需求</li><li>微服务的模式可以解决一部分 api 冗余的问题，且可以进行独立的 api 发布</li></ul><h3 id="劣势：-1"><a href="#劣势：-1" class="headerlink" title="劣势："></a>劣势：</h3><ul><li>与前端需要进行业务 api 的联调</li><li>某些情况下，需要从多个微服务 api 或业务 api 中获取数据并进行数据处理，在访问多个微服务 api 的过程中可能会暴露一些敏感信息</li></ul><h2 id="中间层开发模式"><a href="#中间层开发模式" class="headerlink" title="中间层开发模式"></a>中间层开发模式</h2><p>后端只需要提供最基本的功能实现，比如用户登录、某个数据库表单的数据查询等，中间层服务器通过对内部 api 的数据获取处理，将业务相关的数据提取出来后，以特定 api 的形式返回给前端。</p><p><strong>根据逻辑需求查询数据库 -&gt; 注册底层 api -&gt; 中间层从一（多）个底层 api 获取数据并进行处理 -&gt; 注册前端 api</strong></p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/8920486ef229a92f26c65a0b48cb293f.png" alt="流程图"></p><h3 id="优势：-2"><a href="#优势：-2" class="headerlink" title="优势："></a>优势：</h3><ul><li>由于中间层的存在，不同来源的数据内容，可以在中间层进行处理，比如隐藏敏感信息、组合不同数据数据。</li><li>前端人员可以使用 Node.js 参与中间层的开发，前后端一起开发可以极大的减少联调的时间，开发过程即联调。</li><li>中间层 api 有问题可以及时回滚和修复不影响后端底层 api。</li></ul><h3 id="劣势：-2"><a href="#劣势：-2" class="headerlink" title="劣势："></a>劣势：</h3><ul><li>前端同事需要一定的 Node.js 开发经验</li><li>前端开发工作量少量增加</li></ul><h2 id="更深入的思考🤔"><a href="#更深入的思考🤔" class="headerlink" title="更深入的思考🤔"></a>更深入的思考🤔</h2><h3 id="微服务-中间层-微接口"><a href="#微服务-中间层-微接口" class="headerlink" title="微服务 + 中间层 ?= 微接口"></a>微服务 + 中间层 ?= 微接口</h3><p>我在 Gaea 项目里使用了一种新的 api 模式，暂时称之为微接口模式。Gaea 通过管理底层 api，并通过业务注册，对不同的业务予以不同的 api 访问权限。在 Gaea 里注册应用之后，可以申请各类 api 的访问权限，前端项目通过应用 id 和 token 获取 api 的访问权限，前端开发人员可以根据需求自助式地使用服务 api。<br>这种 api 模式非常适合内部项目的敏捷开发，前端人员在开发一些没有特殊需求的页面时，甚至不需要后端开发人员的参与也可以进行项目开发。</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/3029e4214175310e9790c596b47ef62e.png" alt="结构图"></p><h4 id="微接口与微服务的关联与差异"><a href="#微接口与微服务的关联与差异" class="headerlink" title="微接口与微服务的关联与差异"></a>微接口与微服务的关联与差异</h4><p>微接口是在微服务的基础进行的概念拓展。前端页面对于微服务的开放 api 直接拥有访问权限，对于权限 api 的访问，你需在在对应的微服务 api 里增加访问权限逻辑。微接口的模式则将访问权限解耦成独立的服务控制模式，以应用作为控制载体，在微服务注册中心的基础上增加了应用注册中心，而应用注册中心用于微服务 api 的权限控制。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在项目敏捷开发的过程中，经常会遭遇 Api 联调导致项目开发周期紧张的情况。目前国外很多大型互联网公司流行全栈开发概念，所谓全栈开发即一个项目的前后端逻辑都由一个人来完成。这种模式是的好处是基本上在开发环境没有联调的概念只有 Api 调试的概念。但在目前国内大环境背景下，全栈开发无疑是动了前端或后端工程师的“蛋糕”，所以全栈开发在国内依然是以概念的形式存在。如果前端开发人员利用中间层来处理这个问题，那么是否是一种新的解决思路呢？&lt;/p&gt;
    
    </summary>
    
    
      <category term="Node.js" scheme="https://xiangfa.org/categories/Node-js/"/>
    
      <category term="微服务" scheme="https://xiangfa.org/categories/Node-js/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Node.js" scheme="https://xiangfa.org/tags/Node-js/"/>
    
      <category term="middleware" scheme="https://xiangfa.org/tags/middleware/"/>
    
      <category term="微服务" scheme="https://xiangfa.org/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>优化 vue 项目包大小对比实验结果</title>
    <link href="https://xiangfa.org/2020/07/optimize-package-size-of-vue-project-result/"/>
    <id>https://xiangfa.org/2020/07/optimize-package-size-of-vue-project-result/</id>
    <published>2020-07-15T03:48:06.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>之前我有写过一篇<a href="/2020/06/optimize-package-size-of-vue-project/">优化 vue 项目包大小，提升首屏加载速度</a>的文章，文章中写了一些有关 vue 项目包大小优化的方案，这篇文章则是优化效果的对比实验结果。总的来说，在资源数量提升的情况下，页面加载速度反而有了 10~17% 的提升，这种结果既意外又欣喜，有一种“不明觉厉”的感觉…</p><a id="more"></a><h2 id="新用户初次进入页面的场景"><a href="#新用户初次进入页面的场景" class="headerlink" title="新用户初次进入页面的场景"></a>新用户初次进入页面的场景</h2><p>在 chrome 里模拟新用户初次进入页面的场景：<br>控制变量：Chrome 无痕模式、禁用页面缓存、模拟弱网络环境（Fast 3G 模式）</p><h3 id="优化之前"><a href="#优化之前" class="headerlink" title="优化之前"></a>优化之前</h3><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/0899adb2aa81dc6cc394ce98037474e6.png" alt="优化前"></p><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>资源数量</td><td>28 个请求</td></tr><tr><td>gzip 资源</td><td>294Kb</td></tr><tr><td>资源总大小</td><td>524kb</td></tr><tr><td>Dom加载完成时间</td><td>3.35s</td></tr><tr><td>页面加载时间</td><td>4.06s</td></tr></tbody></table><h3 id="优化之后"><a href="#优化之后" class="headerlink" title="优化之后"></a>优化之后</h3><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/c1f9477227e7044d7b06558eb5ce19fa.png" alt="优化后"></p><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>资源数量</td><td>35 个请求</td></tr><tr><td>gzip 资源</td><td>257Kb</td></tr><tr><td>资源总大小</td><td>432kb</td></tr><tr><td>Dom加载完成时间</td><td>2.78s</td></tr><tr><td>页面加载时间</td><td>3.55s</td></tr></tbody></table><h3 id="实验结论"><a href="#实验结论" class="headerlink" title="实验结论"></a>实验结论</h3><p>由于分包原因，资源请求数量从 28 个提升到了 35 个，gizp 传输大小从 294kb 降到了 257kb，资源总大小从 524kb变成了 432kb，Dom 加载完成时间也就是页面加载完之后出现 loading 的时间从 3.35s 降到了 2.78s，提升 17%，页面完全载入时间从 4.06s 降到了 3.55s，提升 12.56%。</p><h2 id="用户二次进入页面的场景"><a href="#用户二次进入页面的场景" class="headerlink" title="用户二次进入页面的场景"></a>用户二次进入页面的场景</h2><p>在 chrome 里模拟用户二次进入页面的场景：<br>控制变量：Chrome 无痕模式、启用页面缓存、模拟弱网络环境（Fast 3G 模式）</p><h3 id="优化之前-1"><a href="#优化之前-1" class="headerlink" title="优化之前"></a>优化之前</h3><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/96442806834347f5480d50412aad79c9.png" alt="优化前"></p><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>资源数量</td><td>28 个请求</td></tr><tr><td>gzip 资源</td><td>991b</td></tr><tr><td>资源总大小</td><td>524kb</td></tr><tr><td>Dom加载完成时间</td><td>1.81s</td></tr><tr><td>页面加载时间</td><td>2.07s</td></tr></tbody></table><h3 id="优化之后-1"><a href="#优化之后-1" class="headerlink" title="优化之后"></a>优化之后</h3><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/649c56194940d11247811f1e23ea6050.png" alt="优化后"></p><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>资源数量</td><td>41 个请求</td></tr><tr><td>gzip 资源</td><td>205Kb</td></tr><tr><td>资源总大小</td><td>432kb</td></tr><tr><td>Dom加载完成时间</td><td>1.62s</td></tr><tr><td>页面加载时间</td><td>1.88s</td></tr></tbody></table><h3 id="实验结论-1"><a href="#实验结论-1" class="headerlink" title="实验结论"></a>实验结论</h3><p>用户二次加载时，资源从缓存中加载，资源总数量从 28 个变为了 41 个，gizp 传输大小可以忽略不计，资源总大小从 524kb 变成了 432kb，与初次进入页面的资源大小完全一致，Dom 加载完成时间也就是页面加载完之后出现 loading 的时间从 1.81s 降到了 1.62s，提升 10.5%，页面完全载入时间从 2.07s 降到了 1.88s，提升 10%。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;之前我有写过一篇&lt;a href=&quot;/2020/06/optimize-package-size-of-vue-project/&quot;&gt;优化 vue 项目包大小，提升首屏加载速度&lt;/a&gt;的文章，文章中写了一些有关 vue 项目包大小优化的方案，这篇文章则是优化效果的对比实验结果。总的来说，在资源数量提升的情况下，页面加载速度反而有了 10~17% 的提升，这种结果既意外又欣喜，有一种“不明觉厉”的感觉…&lt;/p&gt;
    
    </summary>
    
    
      <category term="Vue" scheme="https://xiangfa.org/categories/Vue/"/>
    
    
      <category term="vue" scheme="https://xiangfa.org/tags/vue/"/>
    
      <category term="项目优化" scheme="https://xiangfa.org/tags/%E9%A1%B9%E7%9B%AE%E4%BC%98%E5%8C%96/"/>
    
      <category term="vue-cli" scheme="https://xiangfa.org/tags/vue-cli/"/>
    
      <category term="webpack" scheme="https://xiangfa.org/tags/webpack/"/>
    
  </entry>
  
  <entry>
    <title>给你的 npm 提提速 — 优化国内 npm 下载速度</title>
    <link href="https://xiangfa.org/2020/07/optimize-npm-download-speed/"/>
    <id>https://xiangfa.org/2020/07/optimize-npm-download-speed/</id>
    <published>2020-07-10T03:10:18.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>在国内大环境的背景下，一些外国网站的访问速度经常会遇到加载慢的情况。这对于前端开发人员而言，最难接受的就是 npm 包下载过慢的问题。你想想呐，一个程序员很开心的写着代码，喝着快乐水，却在下载 npm 依赖时频繁遭遇 <code>Network Error</code> 或 <code>Network Timeout</code>。你那时还有心情写代码么？为了解决广大同僚的工作问题，我有一法可解。</p><a id="more"></a><h2 id="更换-npm-registry-源"><a href="#更换-npm-registry-源" class="headerlink" title="更换 npm registry 源"></a>更换 npm registry 源</h2><p>Node.js 的 npm cli 默认的 registry 源是 <code>https://npmjs.com/</code>。由于某些原因，<a href="https://npmjs.com/" target="_blank" rel="noopener">npmjs.com</a> 在国内的访问速度很不稳定。所以我们可以考虑用国内的镜像网站地址替换默认的地址。</p><h3 id="查看当前-registry-源"><a href="#查看当前-registry-源" class="headerlink" title="查看当前 registry 源"></a>查看当前 registry 源</h3><pre><code class="line-numbers language-shell">yarn config get registry# 或npm config get registry</code></pre><p>这条命令会输出你当前系统的 npm registry 源的网址。如果你显示的是 <code>https://registry.npmjs.com/</code> 那么你可以考虑更换为更快的国内 registry 源。</p><p>目前国内比较稳定的镜像源：</p><pre><code>taobao --- https://registry.npm.taobao.org/cnpm --- https://r.cnpmjs.org/nj --- https://registry.nodejitsu.com/rednpm --- https://registry.mirror.cqupt.edu.cn/npmMirror --- https://skimdb.npmjs.com/registry/deunpm --- http://registry.enpmjs.org/</code></pre><h3 id="如何设置"><a href="#如何设置" class="headerlink" title="如何设置"></a>如何设置</h3><p>个人推荐使用淘宝的镜像源 <code>https://registry.npm.taobao.org/</code></p><h4 id="临时设置"><a href="#临时设置" class="headerlink" title="临时设置"></a>临时设置</h4><pre><code class="line-numbers language-shell">yarn add node_module --registry https://registry.npm.taobao.org/# 或npm install node_module --registry https://registry.npm.taobao.org/</code></pre><h4 id="全局设置"><a href="#全局设置" class="headerlink" title="全局设置"></a>全局设置</h4><pre><code class="line-numbers language-shell">yarn config set registry https://registry.npm.taobao.org/# 或npm config set registry https://registry.npm.taobao.org/</code></pre><h4 id="使用第三方脚本快速修改、切换-npm-镜像源"><a href="#使用第三方脚本快速修改、切换-npm-镜像源" class="headerlink" title="使用第三方脚本快速修改、切换 npm 镜像源"></a>使用第三方脚本快速修改、切换 npm 镜像源</h4><p>你可以考虑使用 <a href="https://www.npmjs.com/package/nrm" target="_blank" rel="noopener">nrm</a>。</p><pre><code class="line-numbers language-shell">yarn global add nrm# 或npm install -g nrm</code></pre><pre><code class="line-numbers language-shell"># 查看当前可用的所有镜像源nrm ls# 切换淘宝源nrm use taobao# 测试速度nrm test taobao</code></pre><h3 id="更快的-npm-资源"><a href="#更快的-npm-资源" class="headerlink" title="更快的 npm 资源"></a>更快的 npm 资源</h3><p>你以为切换成了国内源，npm 就没有优化空间了么？你想变得更快更强么？那么请你继续往下看。</p><pre><code class="line-numbers language-shell">npm set disturl https://npm.taobao.org/dist # node-gyp 编译依赖的 node 源码镜像## 以下选择添加npm set sass_binary_site https://npm.taobao.org/mirrors/node-sass # node-sass 二进制包镜像npm set electron_mirror https://npm.taobao.org/mirrors/electron/ # electron 二进制包镜像npm set puppeteer_download_host https://npm.taobao.org/mirrors # puppeteer 二进制包镜像npm set chromedriver_cdnurl https://npm.taobao.org/mirrors/chromedriver # chromedriver 二进制包镜像npm set operadriver_cdnurl https://npm.taobao.org/mirrors/operadriver # operadriver 二进制包镜像npm set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs # phantomjs 二进制包镜像npm set selenium_cdnurl https://npm.taobao.org/mirrors/selenium # selenium 二进制包镜像npm set node_inspector_cdnurl https://npm.taobao.org/mirrors/node-inspector # node-inspector 二进制包镜像npm set selenium_cdnurl=http://npm.taobao.org/mirrors/seleniumnpm set node_inspector_cdnurl=https://npm.taobao.org/mirrors/node-inspector</code></pre><p>npm 和 yarn 都会对 npm 进行本地缓存，如果你的资源依然被解析为了 <code>https://registry.npmjs.com/</code>，那么可以考虑使用以下命令进行缓存清理：</p><pre><code class="line-numbers language-shell">yarn cache clean# 或npm cache clean --force</code></pre>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在国内大环境的背景下，一些外国网站的访问速度经常会遇到加载慢的情况。这对于前端开发人员而言，最难接受的就是 npm 包下载过慢的问题。你想想呐，一个程序员很开心的写着代码，喝着快乐水，却在下载 npm 依赖时频繁遭遇 &lt;code&gt;Network Error&lt;/code&gt; 或 &lt;code&gt;Network Timeout&lt;/code&gt;。你那时还有心情写代码么？为了解决广大同僚的工作问题，我有一法可解。&lt;/p&gt;
    
    </summary>
    
    
      <category term="npm" scheme="https://xiangfa.org/categories/npm/"/>
    
    
      <category term="npm" scheme="https://xiangfa.org/tags/npm/"/>
    
      <category term="yarn" scheme="https://xiangfa.org/tags/yarn/"/>
    
      <category term="Node.js" scheme="https://xiangfa.org/tags/Node-js/"/>
    
  </entry>
  
  <entry>
    <title>优化 vue 项目包大小，提升首屏加载速度</title>
    <link href="https://xiangfa.org/2020/06/optimize-package-size-of-vue-project/"/>
    <id>https://xiangfa.org/2020/06/optimize-package-size-of-vue-project/</id>
    <published>2020-06-29T10:13:40.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>在前端性能优化中有一个很重要的概念-白屏时间。所谓白屏时间，即用户点击一个链接或打开浏览器输入URL地址后，从屏幕空白到显示第一个画面的时间。<strong>白屏时间的长短将直接影响用户对网站的第一印象。</strong>白屏时间过长还会导致用户流失，降低页面的留存率。</p><p>公司的 To C 项目目前采用 vue 开发，使用 vue-cli 构建的项目已经做了不少性能优化，但随着项目的迭代，依然避免不了项目白屏时间过长的问题。为了缩短首页白屏时间，我开始尝试首屏分包加载实验，以下是我的首屏加载优化心得，希望对你有所帮助。</p><a id="more"></a><h2 id="页面按需加载"><a href="#页面按需加载" class="headerlink" title="页面按需加载"></a>页面按需加载</h2><p>目前公司内部不少 vue 项目都使用 vue-router 这个库，vue-router 可以借助 webpack 的 <code>import()</code> 动态导入实现页面分包，从而实现页面的按需加载。</p><pre><code class="line-numbers language-js">import Vue from &#39;vue&#39;import VueRouter from &#39;vue-router&#39;import Home from &#39;./views/Home&#39;Vue.use(VueRouter)const router = new VueRouter({  routes: [    {      path: &#39;/home&#39;,      name: &#39;home&#39;,      component: Home    },    // 使用 import() 可以实现页面的动态载入，也就是不会和其他页面内容打包在一起。    // 其中 webpackChunkName 是 webpack 用于命名分包的名称的。    {      path: &#39;/&#39;,      name: &#39;landing&#39;,      component: () =&gt; import(/* webpackChunkName: &quot;landing&quot; */ &#39;./views/Landing&#39;)    }  ]})export default router</code></pre><p>如果你的项目页面较多时，你会看到 vue-cli 构建后会多出很多已命名的分包，而首屏页面的分包体积也会明显的变小。这样首屏页面初始化只需要下载比之前小一些的 js 文件就可以马上显示了，从而减少首屏的白屏时间。</p><p>此时，你可能会有另一个担忧：如果其他页面都是按需加载的情况，那么页面切换会不会变慢？这显然是一个好问题！那么到底会不会出现这个情况呢？答案是可能会，但你不需要担心，因为 webpack 在打包时已经考虑到了这个问题，分包并非在页面载入时才去加载，而是通过 preload 或则 prefetch 的方式进行了预加载。所以在页面初始化之后的一段时间，页面的其他分包 js 会被陆续加载进来，在页面切换时一般不会出现需要等待加载的情况。</p><h2 id="分离项目中的基础依赖"><a href="#分离项目中的基础依赖" class="headerlink" title="分离项目中的基础依赖"></a>分离项目中的基础依赖</h2><p>你在开发 vue 项目过程中，可能会使用到 vue、vuex、vue-router 以及 axios 等常用依赖。这些依赖在项目开发过程中以基础依赖的形式存在。而每次项目打包都会把这些基础依赖重复打包进项目，以至于即便是最简单的页面也会生成不小的 js 包文件。如果我们将这些基础依赖抽离出来，这样每次项目更新，用户只需更新业务相关的 js 文件就可以正常显示页面了。</p><pre><code class="line-numbers language-html">&lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.runtime.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;https://cdn.jsdelivr.net/npm/vuex@3.5.1/dist/vuex.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;https://cdn.jsdelivr.net/npm/vue-router@3.3.4/dist/vue-router.min.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js&quot;&gt;&lt;/script&gt;</code></pre><pre><code class="line-numbers language-js">// vue.config.jsmodule.exports = {  configureWebpack: config =&gt; {    config.externals = {      &#39;vue&#39;: &#39;Vue&#39;,      &#39;vuex&#39;: &#39;Vuex&#39;,      &#39;vue-router&#39;: &#39;VueRouter&#39;,      &#39;axios&#39;: &#39;axios&#39;    }  }}</code></pre><p>当你将项目中的基础依赖进行分离之后你会发现 vue-cli 生成的项目体积明显变小了，首屏加载速度在第一次加载时并没有明显变化，但在之后的加载过程中会得到显著的提升。</p><h3 id="处理小众依赖问题"><a href="#处理小众依赖问题" class="headerlink" title="处理小众依赖问题"></a>处理小众依赖问题</h3><p>对于私有依赖或者小众的依赖，你如果也想将其分包处理，但你却无法从网上找到对应的 cdn 链接的时候，你可以考虑自己动手打包。</p><p>只有 <code>amd</code> 或者 <code>umd</code> 格式的 js 文件才可以通过 <code>script</code> 标签的方式进行引入，很多 npm 依赖官方并不会构建 umd 格式的依赖包，这种情况下，你需要自己想办法生成。你可以考虑通过 rollup 来生成 umd 格式的依赖包，具体做法如下：</p><h4 id="引入-rollup-相关依赖"><a href="#引入-rollup-相关依赖" class="headerlink" title="引入 rollup 相关依赖"></a>引入 rollup 相关依赖</h4><pre><code class="line-numbers language-shell">yarn add rollup rollup-plugin-uglify @rollup/plugin-node-resolve @rollup/plugin-commonjs -D// 或者使用 npmnpm install rollup rollup-plugin-uglify @rollup/plugin-node-resolve @rollup/plugin-commonjs --save-dev</code></pre><h4 id="引入依赖并导出文件"><a href="#引入依赖并导出文件" class="headerlink" title="引入依赖并导出文件"></a>引入依赖并导出文件</h4><pre><code class="line-numbers language-js">// src/demo.jsimport { default as demo } from &#39;demo&#39;export default demo</code></pre><h4 id="创建-rollup-config-js-文件"><a href="#创建-rollup-config-js-文件" class="headerlink" title="创建 rollup.config.js 文件"></a>创建 rollup.config.js 文件</h4><pre><code class="line-numbers language-js">// rollup.config.jsimport resolve from &#39;@rollup/plugin-node-resolve&#39;import commonjs from &#39;@rollup/plugin-commonjs&#39;import { uglify } from &#39;rollup-plugin-uglify&#39;export default [  {    // 源文件路径    input: &#39;src/demo.js&#39;,    context: &#39;window&#39;,    output: {      // 生成的目标文件路径及文件名称      file: &#39;libs/demo.min.js&#39;,      // umd 导出的全局对象名称      name: &#39;demo&#39;,      format: &#39;umd&#39;,      // 是否生成 sourceMap      sourcemap: true,    },    plugins: [      // 用于解析 node_modules 依赖关系      resolve(),      // 用于处理 commonjs 导入导出格式      commonjs(),      // 压缩 js 文件      uglify(),    ],  }]</code></pre><p>最后你只需要在 package.json 中添加 script <code>build-lib: &quot;rollup -c&quot;</code>，运行 <code>npm run build-lib</code> 就可以生成 umd 格式的依赖包了。</p><p>自己构建的 umd 格式的依赖包可以放进项目中进行引入，但比较推荐的方案是上传到 cdn 服务上，然后引入 cdn 的链接。</p><h3 id="独立依赖按需载入"><a href="#独立依赖按需载入" class="headerlink" title="独立依赖按需载入"></a>独立依赖按需载入</h3><p>在有一些子页面中，你可以会引入一些独立的依赖文件，所谓独立依赖，是指那些只在特定页面或组件中使用的依赖，比如 qrcode。qrcode 依赖默认会被打包进项目依赖中，因此会增大项目依赖包的体积，对于普通用户来说，很可能并不会用到。如果可以想办法将这个依赖分离出去，然后按需载入的话，可以进一步减少基础依赖包的体积。</p><p>独立依赖的按需载入依然是借助了 webpack 的动态导入来实现。</p><p>常规的依赖使用方式：</p><pre><code class="line-numbers language-js">import QRcode from &#39;qrcode&#39;QRcode.toDataURL(&#39;xxxx&#39;, {  margin: 2,  width: 105})</code></pre><p>改为依赖按需载入的使用方式：</p><pre><code class="line-numbers language-js">import(&#39;qrcode&#39;).then(QRcode =&gt; {  QRcode.toDataURL(&#39;xxxx&#39;, {    margin: 2,    width: 105  })})</code></pre><p>这时候你再通过 vue-cli 进行项目构建时，你会惊喜的发现，又多了一（几）个独立依赖包，基础依赖包的体积也降了下来~</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过以上三种方式，我们可以很大程度的减少首屏需要加载的核心文件大小，从而提高页面的加载速度，减少白屏时间。当然白屏时间优化，不止这一种方案，还有很多其他的方式，比如 SSR、Landing Page、静态资源上传 CDN 等，这里就不再一一展开。你可以根据实际的业务场景进一步做优化。</p><p>需要注意一点，页面的分包数量并不是越多越好，分包越多，网络请求就越多，在不同平台上网络请求的系统带来的开销可能会比文件体积大小导致的白屏问题更严重。webpack 的动态导入特性的使用需要自己权衡。</p><hr><h3 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h3><ul><li><a href="https://juejin.im/post/5d80bd58e51d4561c94b1076" target="_blank" rel="noopener">前端性能优化之白屏时间</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在前端性能优化中有一个很重要的概念-白屏时间。所谓白屏时间，即用户点击一个链接或打开浏览器输入URL地址后，从屏幕空白到显示第一个画面的时间。&lt;strong&gt;白屏时间的长短将直接影响用户对网站的第一印象。&lt;/strong&gt;白屏时间过长还会导致用户流失，降低页面的留存率。&lt;/p&gt;
&lt;p&gt;公司的 To C 项目目前采用 vue 开发，使用 vue-cli 构建的项目已经做了不少性能优化，但随着项目的迭代，依然避免不了项目白屏时间过长的问题。为了缩短首页白屏时间，我开始尝试首屏分包加载实验，以下是我的首屏加载优化心得，希望对你有所帮助。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Vue" scheme="https://xiangfa.org/categories/Vue/"/>
    
    
      <category term="vue" scheme="https://xiangfa.org/tags/vue/"/>
    
      <category term="项目优化" scheme="https://xiangfa.org/tags/%E9%A1%B9%E7%9B%AE%E4%BC%98%E5%8C%96/"/>
    
      <category term="vue-cli" scheme="https://xiangfa.org/tags/vue-cli/"/>
    
      <category term="webpack" scheme="https://xiangfa.org/tags/webpack/"/>
    
  </entry>
  
  <entry>
    <title>使用代理映射解决微信页面调试难题</title>
    <link href="https://xiangfa.org/2020/06/use-proxy-tools-to-debug-wechat-page/"/>
    <id>https://xiangfa.org/2020/06/use-proxy-tools-to-debug-wechat-page/</id>
    <published>2020-06-09T07:03:57.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>在开发微信页面过程中，有些人可能会遇到跟我相同的麻烦——如何快速调试页面。在微信页面中获取数据（比如，用户昵称和头像）都需要走微信的授权流程。在微信授权之后你可以拿到一个 code，用这个 code 你可以经由后端获取到用户数据。但微信 code 的使用是存在限制的，比如每个 code 只能使用一次，微信 code 获取的域名有白名单限制等。这篇文章介绍了一种在不调整后端逻辑的情况下，解决获取微信 code 过程中的域名白名单验证的问题。</p><a id="more"></a><h2 id="解决微信-code-域名白名单验证的问题"><a href="#解决微信-code-域名白名单验证的问题" class="headerlink" title="解决微信 code 域名白名单验证的问题"></a>解决微信 code 域名白名单验证的问题</h2><p>目前微信页面授权采用的是 OAuth2 的方案。微信会对授权的域名做白名单校验，这是一种安全的做法，但也是一种让开发人员<del>抓狂</del>头疼的做法。但这只是一种简单的安全校验规则，你可以通过域名代理映射来解决这个问题。</p><h3 id="通过-Hosts-Nginx-实现代理映射"><a href="#通过-Hosts-Nginx-实现代理映射" class="headerlink" title="通过 Hosts + Nginx 实现代理映射"></a>通过 Hosts + Nginx 实现代理映射</h3><p>如果你项目的端口号是 80，你可以通过简单的修改 hosts 进行本地域名的映射。但 hosts 只能解决 <code>http://xxxx.com</code> 这种形式的域名映射，对于非 80 端口的本地服务，则无能为力。</p><p>我们在本地调试过程中，可能会用到 IP 或本地域名，而项目开发框架为了避免项目间的冲突，因此很少会直接使用 80 端口。因此你经常可以看到类似于 8080 这样的端口号。</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/7640f6dd39389c0e500248fe1aaf4220.png" alt="项目启动"></p><p>这类本地服务你无法直接通过修改 hosts 解决微信 code 域名白名单验证的问题。这时候你就需要通过代理来解决问题。</p><p>最简单的代理方案就是通过 Nginx 来做代理转发。</p><p>在设置 Nginx 配置之前，你需要先通过增加 hosts 映射来“捕获”微信开发者工具的域名请求。</p><pre><code># 假定你的 Nginx 服务使用 127.0.0.1，且端口号为 80127.0.0.1     www.yourdomain.com</code></pre><p>之后你可以在 Nginx 配置中添加以下代码，用以注册服务。</p><pre><code class="line-numbers language-nginx">server {  listen          80;  server_name     www.yourdomain.com; #修改为你需要调试的域名  location / {    index  index.html index.htm index.php;    index  proxy_set_header Host $host;    index  proxy_set_header X-Real-IP $remote_addr;    index  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_pass http://localhost:8080; #你本地的项目域名  }}</code></pre><p>然后重启 Nginx 服务，这样你就可以在微信开发者工具里调试本地代码了。</p><p>虽然解决了目前遇到的问题，但这种方案操作比较复杂，重置过程相对繁琐，可以用来解决一时需求，无法作为长期解决方案。除此之外，在 Mac 操作系统上，由于系统安全限制，Nginx 无法直接使用 80 端口服务。</p><h3 id="通过代理软件实现代理映射"><a href="#通过代理软件实现代理映射" class="headerlink" title="通过代理软件实现代理映射"></a>通过代理软件实现代理映射</h3><p>市面上的代理软件众多，此处不展开讨论，此文以 Mac 平台的 Charles 为例，为大家提供一种使用思路。</p><h4 id="配置-Charles-代理"><a href="#配置-Charles-代理" class="headerlink" title="配置 Charles 代理"></a>配置 Charles 代理</h4><p>首先你需要勾选 Charles 的自动代理 <code>Proxy -&gt; macOS Proxy</code>，并设置远程映射 <code>Tools -&gt; Map Remote</code></p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/f285555e5e02ec872ffd4f838594340f.png" alt="远程映射配置"></p><p>你点击 <code>Add</code> 可以增加新的规则，如果 <code>Add</code> 按钮灰色不可点击，你需要勾选 <code>Enable Map Remote</code>，之后你就可以编写你的映射规则了。</p><p><img src="https://gaeacdn.jiliguala.com/devjlgl/tmp/1e34f34bc6d6031d48cd6361e24a73e7.png" alt="配置映射规则"></p><h4 id="配置微信开发者工具代理"><a href="#配置微信开发者工具代理" class="headerlink" title="配置微信开发者工具代理"></a>配置微信开发者工具代理</h4><p>Charles 默认不会代理系统的网络请求，只是启动了一项全局代理服务，所以你还需要通过配置微信开发者工具的代理设置才可以真正的使用 Charles 代理。</p><p>在微信开发者工具中找到 <code>设置 -&gt; 代理设置 -&gt; 代理</code>，然后选择”手动代理”，我这里设置为 <code>locolhost 8888</code>，Charles 默认的代理端口号为 <code>8888</code>，你可以在 Charles 修改默认代理端口号。</p><p>这样你就可以在微信开发者工具里用本地服务模式进行开发了。</p><p>最新的 Chales 版本默认执行白名单机制，如果你未在白名单设置内进行访问域名配置，可能会遇到微信开发者工具页面打开异常的情况，你可以通过 <code>Tools -&gt; Allow List</code> 进行关闭。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在开发微信页面过程中，有些人可能会遇到跟我相同的麻烦——如何快速调试页面。在微信页面中获取数据（比如，用户昵称和头像）都需要走微信的授权流程。在微信授权之后你可以拿到一个 code，用这个 code 你可以经由后端获取到用户数据。但微信 code 的使用是存在限制的，比如每个 code 只能使用一次，微信 code 获取的域名有白名单限制等。这篇文章介绍了一种在不调整后端逻辑的情况下，解决获取微信 code 过程中的域名白名单验证的问题。&lt;/p&gt;
    
    </summary>
    
    
      <category term="微信页面开发" scheme="https://xiangfa.org/categories/%E5%BE%AE%E4%BF%A1%E9%A1%B5%E9%9D%A2%E5%BC%80%E5%8F%91/"/>
    
    
      <category term="微信页面开发" scheme="https://xiangfa.org/tags/%E5%BE%AE%E4%BF%A1%E9%A1%B5%E9%9D%A2%E5%BC%80%E5%8F%91/"/>
    
      <category term="微信页面调试" scheme="https://xiangfa.org/tags/%E5%BE%AE%E4%BF%A1%E9%A1%B5%E9%9D%A2%E8%B0%83%E8%AF%95/"/>
    
      <category term="微信开发者工具" scheme="https://xiangfa.org/tags/%E5%BE%AE%E4%BF%A1%E5%BC%80%E5%8F%91%E8%80%85%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>Electron 学习笔记 - 使用 Bridge 通信模式解耦 Electron 逻辑</title>
    <link href="https://xiangfa.org/2020/06/electron-study-notes-use-jsBridge/"/>
    <id>https://xiangfa.org/2020/06/electron-study-notes-use-jsBridge/</id>
    <published>2020-06-04T10:26:22.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>虽然官方允许你直接在前端业务代码中直接使用 Electron 甚至直接引用 Node.js 依赖，但这种方式却对业务无意间侵入了前端业务代码。当项目加载远端业务页面以及业务代码由其他项目打包工具生成的情况下，你可能无法对前端业务代码做修改。如果引入 Native 与 HTML 页面的 Bridge 通信模式，这两类问题将迎刃而解。</p><a id="more"></a> <h2 id="Electron-业务通信逻辑"><a href="#Electron-业务通信逻辑" class="headerlink" title="Electron 业务通信逻辑"></a>Electron 业务通信逻辑</h2><p>Electron 提供了 <a href="https://www.electronjs.org/docs/api/ipc-main" target="_blank" rel="noopener">ipcMain</a> 与 ipcRenderer 模块用以主进程与渲染进程之间的通信。</p><pre><code class="line-numbers language-javascript">// 在主进程中.const { ipcMain } = require(&#39;electron&#39;)ipcMain.on(&#39;asynchronous-message&#39;, (event, arg) =&gt; {  console.log(arg) // 输出 &quot;ping&quot;  event.reply(&#39;asynchronous-reply&#39;, &#39;pong&#39;)})ipcMain.on(&#39;synchronous-message&#39;, (event, arg) =&gt; {  console.log(arg) // 输出 &quot;ping&quot;  event.returnValue = &#39;pong&#39;})</code></pre><pre><code class="line-numbers language-javascript">// 在渲染器进程 (网页) 中。const { ipcRenderer } = require(&#39;electron&#39;)console.log(ipcRenderer.sendSync(&#39;synchronous-message&#39;, &#39;ping&#39;)) // 输出 &quot;pong&quot;ipcRenderer.on(&#39;asynchronous-reply&#39;, (event, arg) =&gt; {  console.log(arg) // 输出 &quot;pong&quot;})ipcRenderer.send(&#39;asynchronous-message&#39;, &#39;ping&#39;)</code></pre><p>如果你想在你的前端页面使用这种通信方式，那么你需要在 webview 中配置 <code>&lt;webview src=&quot;http://www.google.com/&quot; nodeintegration&gt;&lt;/webview&gt;</code> 或者在 BrowserWindow 实例中的 webPreferences 属性里配置 <code>nodeIntegration: true</code>。这虽然能够解决进程间的通信问题，但也将 Node.js 环境引入了前端业务页面。</p><h2 id="使用-Bridge-通信模式解决-Electron-逻辑耦合问题"><a href="#使用-Bridge-通信模式解决-Electron-逻辑耦合问题" class="headerlink" title="使用 Bridge 通信模式解决 Electron 逻辑耦合问题"></a>使用 Bridge 通信模式解决 Electron 逻辑耦合问题</h2><p>当你加载的页面是一个外部页面，那么你可能需要在其他的项目里引入 Electron 作为项目依赖，那么 Electron 在无意间入侵了该项目的业务逻辑。倘若我们将 Electron 的进程通信方法封装为 Bridge 的模式，那么我们可以将业务代码与 Electron 解耦为 Bridge 间的通信。在 Bridge 通信模式下，你可以更好的处理业务降级工作。</p><p>以下我将提供我目前在业务使用的一种 jsBridge 通信方式。</p><h3 id="Electron-使用-contextBridge-注册-Native-Bridge"><a href="#Electron-使用-contextBridge-注册-Native-Bridge" class="headerlink" title="Electron 使用 contextBridge 注册 Native Bridge"></a>Electron 使用 contextBridge 注册 Native Bridge</h3><p>Electron 提供了 <a href="https://www.electronjs.org/docs/api/context-bridge" target="_blank" rel="noopener">contextBridge</a> 用以注册 Native Bridge。在这之前你需要在 BrowserWindow 实例中的 webPreferences 属性里配置 <code>contextIsolation: true</code> 以及 <code>preload: path.join(__dirname, &#39;preload.js&#39;)</code> 引入 preload.js。</p><p>你需要在 preload.js 页面中引入以下逻辑：</p><pre><code class="line-numbers language-javascript">// Preload (Isolated World)const { contextBridge, ipcRenderer } = require(&#39;electron&#39;)contextBridge.exposeInMainWorld(  &#39;electron&#39;,  {    doThing: () =&gt; ipcRenderer.send(&#39;do-a-thing&#39;)  })</code></pre><p>注册完 Native Bridge 之后，你就可以直接在页面中调用你注册过的方法。</p><pre><code class="line-numbers language-javascript">// Renderer (Main World)window.electron.doThing()</code></pre><p>即便载入的页面是一个远端页面，并且该页面在不引入 Electron 的情况下，你也完全可以使用以上方法直接与 Electron 进行交互。</p><h4 id="使用-jsBridge-替代直接调用-Native-Bridge"><a href="#使用-jsBridge-替代直接调用-Native-Bridge" class="headerlink" title="使用 jsBridge 替代直接调用 Native Bridge"></a>使用 jsBridge 替代直接调用 Native Bridge</h4><p>你可以对 Native Bridge 进行二次封装，这样你的页面代码就可以优雅降级了。</p><pre><code class="line-numbers language-javascript">window.jsBridge = {  doThing: () =&gt; {    if (typeof window.electron === &#39;object&#39;) {      window.electron.doThing()    } else {      // 你可以在这里使用更完善的降级处理方案。      console.log(&#39;Non-electron environment!&#39;)    }  }}</code></pre><p>这样你就可以在前端业务代码中大胆的使用 Bridge 通信模式了。</p><pre><code class="line-numbers language-javascript">window.jsBridge.doThing()</code></pre><h3 id="更完善的-Native-Bridge-逻辑"><a href="#更完善的-Native-Bridge-逻辑" class="headerlink" title="更完善的 Native Bridge 逻辑"></a>更完善的 Native Bridge 逻辑</h3><p>之前已经说明了如何借助 <a href="https://www.electronjs.org/docs/api/context-bridge" target="_blank" rel="noopener">contextBridge</a> 来实现 Native Bridge。但之前的实现无法做到异步的数据回调通信。比如，你需要通过借助 Electron 的 Node.js 能力处理大量的数据，而这些数据的处理相当耗时，你不应当采用同步回调的模式与 Electron 进行通信，因为这样做会导致你的页面进入”假死“的状态。但如果采用异步的方式，你可能需要在前端业务代码中引入 Electron 的 ipcRenderer 方法用以进程间的通信。哎，怎么又绕回原地了呢？但不要这么早放弃希望，因为你可以通过实现一套完整的 jsBridge 的方式来解决这个问题，以下是提供我实现的一种支持异步回调的简易通信方式，你可以作为参考。</p><pre><code class="line-numbers language-javascript">// MainipcMain.on(&#39;postMessage&#39;, async (event, message) =&gt; {  switch (message.bridgeName) {    case &#39;doThing&#39;:      try {        const result = await doThing(message.data)        event.reply(&#39;receiveMessage&#39;, {          bridgeName: &#39;doThing&#39;,          cid: message.cid,          data: result,        },)      } catch (err) {        event.reply(&#39;receiveMessage&#39;, {          bridgeName: &#39;doThing&#39;,          error: { code: 500, message: err.message },        })      }      break    default:      break  }})</code></pre><pre><code class="line-numbers language-javascript">// Preload (Isolated World)const { contextBridge, ipcRenderer } = require(&#39;electron&#39;)let cid = 0const callbacks = {}// 注册 nativeBridgecontextBridge.exposeInMainWorld(  &#39;electron&#39;,  {    invoke (bridgeName, data, callback) {      // 如果不存在方法名或不为字符串，则提示调用失败      if (typeof bridgeName !== &#39;string&#39;) {        throw new Error(&#39;Invoke failed!&#39;)      }      // 与 Native 的通信信息      const message = { bridgeName }      if (typeof data !== &#39;undefined&#39; || data !== null) {        message.data = data      }      if (typeof callback !== &#39;function&#39;) {        callback = () =&gt; null      }      cid = cid + 1      // 存储回调函数      callbacks[cid] = callback      message.cid = cid      ipcRenderer.send(&#39;postMessage&#39;, message)      ipcRenderer.once(&#39;receiveMessage&#39;, (_, message): void =&gt; {        const { data, cid, error } = message        // 如果存在方法名，则调用对应函数        if (typeof cid === &#39;number&#39; &amp;&amp; cid &gt;= 1) {          if (typeof error !== &#39;undefined&#39;) {            callbacks[cid](error)            delete callbacks[cid]          } else if (callbacks[cid]) {            callbacks[cid](null, data)            delete callbacks[cid]          } else {            throw new Error(&#39;Invalid callback id&#39;)          }        } else {          throw new Error(&#39;message format error&#39;)        }      })    }  })</code></pre><pre><code class="line-numbers language-javascript">window.jsBridge = {  doThing: (data, callback) =&gt; {    if (typeof window.electron === &#39;object&#39;) {      window.electron.invoke(&#39;doThing&#39;, data, callback)    } else {      // 你可以在这里使用更完善的降级处理方案。      console.log(&#39;Non-electron environment!&#39;)    }  }}</code></pre><pre><code class="line-numbers language-javascript">window.jsBridge.doThing({}, () =&gt; {  console.log(&#39;finished&#39;)})</code></pre>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;虽然官方允许你直接在前端业务代码中直接使用 Electron 甚至直接引用 Node.js 依赖，但这种方式却对业务无意间侵入了前端业务代码。当项目加载远端业务页面以及业务代码由其他项目打包工具生成的情况下，你可能无法对前端业务代码做修改。如果引入 Native 与 HTML 页面的 Bridge 通信模式，这两类问题将迎刃而解。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Electron 学习笔记" scheme="https://xiangfa.org/categories/Electron-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="Electron" scheme="https://xiangfa.org/tags/Electron/"/>
    
      <category term="Electron 学习笔记" scheme="https://xiangfa.org/tags/Electron-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
      <category term="跨平台" scheme="https://xiangfa.org/tags/%E8%B7%A8%E5%B9%B3%E5%8F%B0/"/>
    
      <category term="桌面应用" scheme="https://xiangfa.org/tags/%E6%A1%8C%E9%9D%A2%E5%BA%94%E7%94%A8/"/>
    
      <category term="Bridge 通信" scheme="https://xiangfa.org/tags/Bridge-%E9%80%9A%E4%BF%A1/"/>
    
  </entry>
  
  <entry>
    <title>Electron 学习笔记 - TypeScript 的使用</title>
    <link href="https://xiangfa.org/2020/06/electron-study-notes-use-typescript/"/>
    <id>https://xiangfa.org/2020/06/electron-study-notes-use-typescript/</id>
    <published>2020-06-01T09:21:00.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>Electron（原名为Atom Shell）是 GitHub 开发的一个开源框架。它允许使用 Node.js（作为后端）和 Chromium（作为前端）完成桌面GUI应用程序的开发。Electron 现已被多个开源 Web 应用程序用于前端与后端的开发，著名项目包括 GitHub 的 Atom 和微软的 Visual Studio Code。</p><p>Electron 使用 JavaScript 作为基础编程语言，你实际上是在 Chromium 中编写前端代码，在 Node.js 中编写后端代码。随着时代的发展，TypeScript 越来越受到前端开发人员的欢迎，源于 TypeScript 是 JavaScript 的严格超集，不仅包含 JavaScript 的语法，而且还提供了静态类型检查。</p><a id="more"></a> <h2 id="在项目中使用-TypeScript"><a href="#在项目中使用-TypeScript" class="headerlink" title="在项目中使用 TypeScript"></a>在项目中使用 TypeScript</h2><h3 id="针对新项目"><a href="#针对新项目" class="headerlink" title="针对新项目"></a>针对新项目</h3><p>目前 Electron 并不支持直接使用 TypeScript 进行开发，但官方提供了 <a href="https://github.com/electron/electron-quick-start-typescript" target="_blank" rel="noopener">electron-quick-start-typescript</a>，我们将在此基础上进行开发。</p><p>在运行以下代码之前，你需要确保已经安装了 <a href="https://git-scm.com/" target="_blank" rel="noopener">Git</a> 和 <a href="https://nodejs.org/zh-cn/" target="_blank" rel="noopener">Node.js</a>。</p><pre><code class="line-numbers language-shell"># 拉取项目git clone https://github.com/electron/electron-quick-start-typescript# 进行项目目录cd electron-quick-start-typescript# 安装依赖npm install# 运行项目npm start</code></pre><p>这样你就可以在项目中愉快的使用 TypeScript 进行项目开发了。</p><h3 id="针对老项目"><a href="#针对老项目" class="headerlink" title="针对老项目"></a>针对老项目</h3><p>当你在某一天需要把一个现有 TypeScript 项目转成 Electron 应用时，你可能会遇到一些麻烦。你的项目可能使用了 webpack 进行代码编译，但 webpack 并没有对 Electron 做优化，因此编译后的代码往往无法直接使用 electron 运行。这时候你可能会想到重构项目，但往往重构项目代码的成本会远超你的预期，并且可能会带来更多的不可预知的问题。</p><p>如果你退一步，将 Electron 作为项目插件来运行的话，那么问题就可以轻松解决了。</p><pre><code class="line-numbers language-shell">npm install electron --save-dev</code></pre><p>你可以在原来项目的下创建一个 <code>electron</code> 目录（你可以使用其他目录名称），并创建 main.ts、preload.ts 文件。</p><pre><code class="line-numbers language-typescript">// main.tsimport { app, BrowserWindow } from &#39;electron&#39;import * as path from &#39;path&#39;let mainWindow: Electron.BrowserWindowfunction createWindow() {  // 创建浏览器窗口  mainWindow = new BrowserWindow({    height: 600,    width: 800,  })  // 加载应用入口页面，此处假设你的应用编译后的应用入口为 build/index.html  mainWindow.loadFile(path.join(__dirname, &#39;../build/index.html&#39;))  // 打开开发者工具  mainWindow.webContents.openDevTools()  // 在窗口关闭时触发  mainWindow.on(&#39;closed&#39;, () =&gt; {    mainWindow = null  })}// Electron会在初始化完成并且准备好创建浏览器窗口时调用这个方法，部分 API 在 ready 事件触发后才能使用。app.on(&#39;ready&#39;, createWindow)// 当所有窗口都被关闭后退出app.on(&#39;window-all-closed&#39;, () =&gt; {  // 在 macOS 上，除非用户用 Cmd + Q 确定地退出，否则绝大部分应用及其菜单栏会保持激活。  if (process.platform !== &#39;darwin&#39;) {    app.quit()  }})app.on(&#39;activate&#39;, () =&gt; {  // 在macOS上，当单击dock图标并且没有其他窗口打开时，通常在应用程序中重新创建一个窗口。  if (mainWindow === null) {    createWindow()  }})</code></pre><p>最后你需要在你的 <code>package.json</code> 文件中的 <code>script</code> 处添加额外的 electron 脚本：</p><pre><code class="line-numbers language-json">{  // 此处省略其他的配置内容  &quot;script&quot;: {    &quot;electron&quot;: &quot;npm run build &amp;&amp; tsc ./electron/*.ts -t ES5 -m commonjs &amp;&amp; electron ./electron/main.js&quot;  }}</code></pre><p>如果你遇到一些 TypeScript 的编译错误提示，你可以尝试使用 <code>npm install @types/node --save-dev</code> 来解决。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Electron（原名为Atom Shell）是 GitHub 开发的一个开源框架。它允许使用 Node.js（作为后端）和 Chromium（作为前端）完成桌面GUI应用程序的开发。Electron 现已被多个开源 Web 应用程序用于前端与后端的开发，著名项目包括 GitHub 的 Atom 和微软的 Visual Studio Code。&lt;/p&gt;
&lt;p&gt;Electron 使用 JavaScript 作为基础编程语言，你实际上是在 Chromium 中编写前端代码，在 Node.js 中编写后端代码。随着时代的发展，TypeScript 越来越受到前端开发人员的欢迎，源于 TypeScript 是 JavaScript 的严格超集，不仅包含 JavaScript 的语法，而且还提供了静态类型检查。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Electron 学习笔记" scheme="https://xiangfa.org/categories/Electron-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
    
      <category term="Electron" scheme="https://xiangfa.org/tags/Electron/"/>
    
      <category term="Electron 学习笔记" scheme="https://xiangfa.org/tags/Electron-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    
      <category term="跨平台" scheme="https://xiangfa.org/tags/%E8%B7%A8%E5%B9%B3%E5%8F%B0/"/>
    
      <category term="桌面应用" scheme="https://xiangfa.org/tags/%E6%A1%8C%E9%9D%A2%E5%BA%94%E7%94%A8/"/>
    
  </entry>
  
  <entry>
    <title>React Hooks 时代的状态管理库的选择</title>
    <link href="https://xiangfa.org/2020/05/react-hooks-vs-mobx-vs-redux/"/>
    <id>https://xiangfa.org/2020/05/react-hooks-vs-mobx-vs-redux/</id>
    <published>2020-05-26T10:06:00.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<p>React 的数据流是自上而下的，从组件外到组件内，从父组件到子组件，且传递下来的 props 是只读的，如果你想更改 props，只能父组件传入一个封装好的 setState 方法。虽然你可以通过一些方案来解决 React 组件间的通信问题，但随着项目业务的增长，组件通信的成本会越来越高！这时候你可能希望有一处专门负责数据状态管理的地方，而这就是我们今天要提到的数据状态管理库的概念。</p><p>在 React 项目中常用的数据状态管理主要有 Redux 和 Mobx。而在早期，React 引入 Redux 需要使用大量的“胶水代码”，且遵循 setState 原则。而 Mobx 主张干掉 setState 的机制，它简化了使用成本，但确增加了“依赖收集”的新概念。这两个状态管理库各有优劣，多年相争不下。</p><a id="more"></a> <h2 id="React-Hooks-时代"><a href="#React-Hooks-时代" class="headerlink" title="React Hooks 时代"></a>React Hooks 时代</h2><p>在进入到 React Hooks 时代，Mobx 率先推出了 <a href="https://mobx-react.js.org/" target="_blank" rel="noopener">Mobx-react-lite</a>，让隔壁的 Redux，完全没来得及反应。随着 React Hooks 日渐增长，Redux 也在后来推出了 <a href="https://react-redux.js.org/api/hooks" target="_blank" rel="noopener">React Redux Hooks</a>。<del>开启了大航海时代！</del> React Hooks 时代的状态管理之争,由此进入白热化状态。</p><p>我将使用 React Hooks 的 useContext 和 useReducer 实现简易的 TodoList，并使用 React Redux Hooks 和 Mobx-react-lite 实现相同的功能。</p><h3 id="React-Hooks-原生实现"><a href="#React-Hooks-原生实现" class="headerlink" title="React Hooks 原生实现"></a>React Hooks 原生实现</h3><h4 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h4><pre><code class="line-numbers language-js">// reducer.jsexport const initState = {  todoList: {},}export const reducer = (state, action) =&gt; {  const { payload } = action  switch (action.type) {    case &#39;ADD_TODOLIST&#39;: {      state.todoList[payload.todo] = false      return {        todoList: { ...state.todoList },      }    }    case &#39;TOGGLE_TODOLIST&#39;: {      state.todoList[payload.todo] = !state.todoList[payload.todo]      return {        todoList: { ...state.todoList },      }    }    default:      return state  }}export default {  reducer,  initState,}</code></pre><pre><code class="line-numbers language-jsx">// context.jsximport React, { useReducer } from &#39;react&#39;import { reducer, initState } from &#39;./reducer&#39;const StoreContext = React.createContext(null)export const useStore = () =&gt; {  const store = React.useContext(StoreContext)  if (!store) {    // this is especially useful in TypeScript so you don&#39;t need to be checking for null all the time    throw new Error(&#39;You have forgot to use StoreProvider, shame on you.&#39;)  }  return store}export function Provider({ children }) {  const [state, dispatch] = useReducer(reducer, initState)  return (    &lt;StoreContext.Provider value={{ state, dispatch }}&gt;      {children}    &lt;/StoreContext.Provider&gt;  )}</code></pre><pre><code class="line-numbers language-jsx">// main.jsximport React, { useState, useMemo } from &#39;react&#39;import { useStore } from &#39;./context&#39;function Main() {  const { state, dispatch } = useStore()  const [todoText, setTodoText] = useState(&#39;&#39;)  const paddingTodos = useMemo(() =&gt; {    return Object.keys(state.todoList).filter(      todo =&gt; state.todoList[todo] === false    )  }, [state.todoList])  const doneTodos = useMemo(() =&gt; {    return Object.keys(state.todoList).filter(      todo =&gt; state.todoList[todo] === true    )  }, [state.todoList])  const addTodoList = () =&gt; {    dispatch({      type: &#39;ADD_TODOLIST&#39;,      payload: { todo: todoText },    })    setTodoText(&#39;&#39;)  }  const toggleTodoList = (todo) =&gt; {    dispatch({      type: &#39;TOGGLE_TODOLIST&#39;,      payload: { todo },    })  }  return (    &lt;div&gt;      &lt;input value={todoText} onChange={ev =&gt; setTodoText(ev.target.value)} /&gt;      &lt;button type=&quot;button&quot; onClick={() =&gt; addTodoList(todoText)}&gt;增加待办事项&lt;/button&gt;      &lt;ul&gt;        {paddingTodos.map(todo =&gt; {          return &lt;li key={todo} onClick={() =&gt; toggleTodoList(todo)}&gt;{todo}&lt;/li&gt;        })}        {doneTodos.map(todo =&gt; {          return &lt;li key={todo} style={{ textDecoration: 'line-through' }} onClick={() =&gt; toggleTodoList(todo)}&gt;{todo}&lt;/li&gt;        })}      &lt;/ul&gt;    &lt;/div&gt;  )}export default Main</code></pre><pre><code class="line-numbers language-jsx">// index.jsximport React from &#39;react&#39;import { Provider } from &#39;./context&#39;import Main from &#39;./main&#39;function TodoList () {  return (    &lt;Provider&gt;      &lt;Main /&gt;    &lt;/Provider&gt;  )}export default TodoList</code></pre><h4 id="优势"><a href="#优势" class="headerlink" title="优势"></a>优势</h4><p>原生的 React Hooks 可以实现简单的数据状态管理，你需要引入额外的依赖。这在小型的项目中非常有优势。</p><h4 id="劣势"><a href="#劣势" class="headerlink" title="劣势"></a>劣势</h4><p>虽然借助原生 React Hooks 可以实现简易的数据状态管理，但官方确只是把这种实现当成一种新的组件间通信的解决方案。主要原因在于数据在不同页面间如果需要同步的话，你需要在不同的页面里引入相同的 reducer.js，当你使用 React Router 之后，你会发现页面切换很容易造成数据的丢失，因此你不得不将 Context 移到组件的最上层，才能解决这类组件注销造成数据丢失的问题。</p><hr><h3 id="React-Redux-Hooks-实现"><a href="#React-Redux-Hooks-实现" class="headerlink" title="React Redux Hooks 实现"></a>React Redux Hooks 实现</h3><h4 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h4><pre><code class="line-numbers language-shell">npm install redux react-redux// oryarn add redux react-redux</code></pre><h4 id="代码实现-1"><a href="#代码实现-1" class="headerlink" title="代码实现"></a>代码实现</h4><pre><code class="line-numbers language-js">// reducer.jsconst initState = {  todoList: {},}const reducer = (state = initState, action) =&gt; {  const { payload } = action  switch (action.type) {    case &#39;ADD_TODOLIST&#39;: {      state.todoList[payload.todo] = false      return {        todoList: { ...state.todoList },      }    }    case &#39;TOGGLE_TODOLIST&#39;: {      state.todoList[payload.todo] = !state.todoList[payload.todo]      return {        todoList: { ...state.todoList },      }    }    default:      return state  }}export default reducer</code></pre><pre><code class="line-numbers language-jsx">// main.jsximport React, { useState } from &#39;react&#39;import { useSelector, useDispatch } from &#39;react-redux&#39;function Main () {  const [todoText, setTodoText] = useState(&#39;&#39;)  const paddingTodos = useSelector(state =&gt; {    return Object.keys(state.todoList).filter(      todo =&gt; state.todoList[todo] === false    )  })  const doneTodos = useSelector(state =&gt; {    return Object.keys(state.todoList).filter(      todo =&gt; state.todoList[todo] === true    )  })  const dispatch = useDispatch()  const addTodoList = () =&gt; {    dispatch({      type: &#39;ADD_TODOLIST&#39;,      payload: { todo: todoText },    })    setTodoText(&#39;&#39;)  }  const toggleTodoList = (todo) =&gt; {    dispatch({      type: &#39;TOGGLE_TODOLIST&#39;,      payload: { todo },    })  }  return (    &lt;div&gt;      &lt;input value={todoText} onChange={ev =&gt; setTodoText(ev.target.value)} /&gt;      &lt;button type=&quot;button&quot; onClick={() =&gt; addTodoList(todoText)}&gt;增加待办事项&lt;/button&gt;      &lt;ul&gt;        {paddingTodos.map(todo =&gt; {          return &lt;li key={todo} onClick={() =&gt; toggleTodoList(todo)}&gt;{todo}&lt;/li&gt;        })}        {doneTodos.map(todo =&gt; {          return &lt;li key={todo} style={{ textDecoration: 'line-through' }} onClick={() =&gt; toggleTodoList(todo)}&gt;{todo}&lt;/li&gt;        })}      &lt;/ul&gt;    &lt;/div&gt;  );}export default Main</code></pre><pre><code class="line-numbers language-jsx">// index.jsximport React from &#39;react&#39;import { createStore } from &#39;redux&#39;import { Provider } from &#39;react-redux&#39;import Main from &#39;./main&#39;import reducer from &#39;./reducer&#39;const store = createStore(reducer)function TodoList () {  return (    &lt;Provider store={store}&gt;      &lt;Main /&gt;    &lt;/Provider&gt;  )}export default TodoList</code></pre><h4 id="优势-1"><a href="#优势-1" class="headerlink" title="优势"></a>优势</h4><p>引入 React Redux Hooks 之后，你会发现页面代码变得更加简洁了。相对于之前的 Redux 实现，React Redux Hooks 更接近于 React Hooks 的原生实现。</p><h4 id="劣势-1"><a href="#劣势-1" class="headerlink" title="劣势"></a>劣势</h4><p>React Redux Hooks 虽然精简了大部分的代码，但依然采用 React Hooks reducer 的实现，你在修改数据状态时需要注意返回全新的 state，不然数据状态可能会不变。 </p><hr><h3 id="Mobx-react-lite-实现"><a href="#Mobx-react-lite-实现" class="headerlink" title="Mobx-react-lite 实现"></a>Mobx-react-lite 实现</h3><h4 id="安装依赖-1"><a href="#安装依赖-1" class="headerlink" title="安装依赖"></a>安装依赖</h4><pre><code class="line-numbers language-shell">npm install mobx mobx-react-lite// oryarn add mobx mobx-react-lite</code></pre><h4 id="代码实现-2"><a href="#代码实现-2" class="headerlink" title="代码实现"></a>代码实现</h4><pre><code class="line-numbers language-js">// store.jsimport { observable } from &#39;mobx&#39;const store = observable({})export function createStore() {  return {    todoList: store,    get pendingTodos() {      return Object.keys(this.todoList).filter(        todo =&gt; this.todoList[todo] === false,      )    },    get doneTodos() {      return Object.keys(this.todoList).filter(        todo =&gt; this.todoList[todo] === true,      )    },    ADD_TODOLIST(todo) {      this.todoList[todo] = false    },    TOGGLE_TODOLIST(todo) {      this.todoList[todo] = !this.todoList[todo]    }  }}</code></pre><pre><code class="line-numbers language-jsx">// context.jsximport React from &#39;react&#39;import { useLocalStore } from &#39;mobx-react-lite&#39;import { createStore } from &#39;./store&#39;const StoreContext = React.createContext(null)export const useStore = () =&gt; {  const store = React.useContext(StoreContext)  if (!store) {    // this is especially useful in TypeScript so you don&#39;t need to be checking for null all the time    throw new Error(&#39;You have forgot to use StoreProvider, shame on you.&#39;)  }  return store}export function Provider({ children }) {  const store = useLocalStore(createStore)  return (    &lt;StoreContext.Provider value={store}&gt;      {children}    &lt;/StoreContext.Provider&gt;  )}</code></pre><pre><code class="line-numbers language-jsx">// main.jsximport React, { useState } from &#39;react&#39;import { observer } from &#39;mobx-react-lite&#39;import { useStore } from &#39;./context&#39;const Main = observer(() =&gt; {  const store = useStore()  const [todoText, setTodoText] = useState(&#39;&#39;)  const addTodo = (todo) =&gt; {    store.ADD_TODOLIST(todo)    setTodoText(&#39;&#39;)  }  const toggleTodo = (todo) =&gt; {    store.TOGGLE_TODOLIST(todo)  }  return (    &lt;div&gt;      &lt;input value={todoText} onChange={ev =&gt; setTodoText(ev.target.value)} /&gt;      &lt;button type=&quot;button&quot; onClick={() =&gt; addTodo(todoText)}&gt;增加待办事项&lt;/button&gt;      &lt;ul&gt;        {store.pendingTodos.map(todo =&gt; {          return &lt;li key={todo} onClick={() =&gt; toggleTodo(todo)}&gt;{todo}&lt;/li&gt;        })}        {store.doneTodos.map(todo =&gt; {          return &lt;li key={todo} style={{ textDecoration: 'line-through' }} onClick={() =&gt; toggleTodo(todo)}&gt;{todo}&lt;/li&gt;        })}      &lt;/ul&gt;    &lt;/div&gt;  )})export default Main</code></pre><pre><code class="line-numbers language-jsx">// index.jsximport React from &#39;react&#39;import { Provider } from &#39;./context&#39;import Main from &#39;./main&#39;function TodoList () {  return (    &lt;Provider&gt;      &lt;Main /&gt;    &lt;/Provider&gt;  )}export default TodoList</code></pre><h4 id="优势-2"><a href="#优势-2" class="headerlink" title="优势"></a>优势</h4><p>Mobx 中 observer 的实现非常优雅，你可以观察一个对象，然后在需要使用使用到 store 的组件中监听变化（observer）就可以了。这样的写法和 Vue 比较接近。</p><h4 id="劣势-2"><a href="#劣势-2" class="headerlink" title="劣势"></a>劣势</h4><p>Mobx 的实现与 React 的数据不可变思想有些出入，以至于部分只用过 React 开发项目的人无法理解 Mobx 的实现机制。</p><hr><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>不管是 React Hooks 原生实现，还是借助 Redux 或 Mobx 来实现数据状态管理都是完全可行的。Redux 和 Mobx 的数据状态管理库一哥位置的争夺仍会持续很长一段时间，React Hooks 也可能会继续推出更强大的官方实现方案。但你需要根据实际的业务需求以及你个人对这类框架实现机制的理解来选择最合适的实现。</p><h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul><li><a href="https://github.com/sunyongjian/blog/issues/36" target="_blank" rel="noopener">对 React 状态管理的理解及方案对比</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;React 的数据流是自上而下的，从组件外到组件内，从父组件到子组件，且传递下来的 props 是只读的，如果你想更改 props，只能父组件传入一个封装好的 setState 方法。虽然你可以通过一些方案来解决 React 组件间的通信问题，但随着项目业务的增长，组件通信的成本会越来越高！这时候你可能希望有一处专门负责数据状态管理的地方，而这就是我们今天要提到的数据状态管理库的概念。&lt;/p&gt;
&lt;p&gt;在 React 项目中常用的数据状态管理主要有 Redux 和 Mobx。而在早期，React 引入 Redux 需要使用大量的“胶水代码”，且遵循 setState 原则。而 Mobx 主张干掉 setState 的机制，它简化了使用成本，但确增加了“依赖收集”的新概念。这两个状态管理库各有优劣，多年相争不下。&lt;/p&gt;
    
    </summary>
    
    
      <category term="React" scheme="https://xiangfa.org/categories/React/"/>
    
    
      <category term="React" scheme="https://xiangfa.org/tags/React/"/>
    
      <category term="React Hooks" scheme="https://xiangfa.org/tags/React-Hooks/"/>
    
      <category term="状态管理" scheme="https://xiangfa.org/tags/%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86/"/>
    
      <category term="Mobx" scheme="https://xiangfa.org/tags/Mobx/"/>
    
      <category term="Redux" scheme="https://xiangfa.org/tags/Redux/"/>
    
  </entry>
  
  <entry>
    <title>Vue 组件库构建流程</title>
    <link href="https://xiangfa.org/2020/05/vue-components-lib/"/>
    <id>https://xiangfa.org/2020/05/vue-components-lib/</id>
    <published>2020-05-25T10:45:15.000Z</published>
    <updated>2023-11-21T05:42:32.553Z</updated>
    
    <content type="html"><![CDATA[<pre><code>Leader：我们最近好几个项目都有用到相似的业务逻辑，比如，手机验证码注册功能最好能有一个组件库来支持，这样就不需要要多次编写相似的代码了。Me：我..Leader：做一个 vue 的组件库应该没什么问题吧。Me：额..Leader：我记得你 OKR 里有写到这个。Me：没问题！</code></pre><p><del>以上内容纯属虚构</del></p><a id="more"></a> <h2 id="查找组件库"><a href="#查找组件库" class="headerlink" title="查找组件库"></a>查找组件库</h2><p>既然要开始构建一个 vue 的组件库，特别是针对移动端 App 的组件库，首先需要的做到就是去了解是否有成熟的组件可以直接用。虽然搜出来的 vue 的组件库并不少，但像 iView、element UI 和 Ant Design Vue 这类组件库都是 focus 在后台开发领域的，并不适用于移动端，而移动端目前比较完善的只有 <a href="https://github.com/youzan/vant" target="_blank" rel="noopener">Vant</a> 这个有赞开发的 vue 组件库了。<del>有赞，打钱！</del></p><p>Vant 这个库组件种类还是很丰富的，但..我接到的需求却是开发一个功能型的组件库，主要是实现一些公司内部常用业务的组件开发。<del>做程序员实在是太南了。</del></p><p>既然没办法<del>直接拿 vant 交差</del>解决问题，那是不是可以借鉴 Vant 的方案来做一个 vue 组件库呢？</p><p><img src="https://h5.jiliguala.com/activity/bf6a3b5f7598acea22631a56026e4236.png" alt="打包脚本"></p><p>当看到这么多打包脚本就直接把我劝退了。既然<del>Fork</del>借鉴无望，那就只能撸起袖子自己干了。</p><h2 id="如何开始构建工作？"><a href="#如何开始构建工作？" class="headerlink" title="如何开始构建工作？"></a>如何开始构建工作？</h2><p>如果之前没有相关经验，当然是 Google 一下咯！</p><p>不错<del>有人已经实践过了，那就不需要自己再</del>造轮子~~开发了，那就点开前两个链接看一下别人是怎么去实践的。<em>掘金，打钱！</em></p><ul><li><a href="https://juejin.im/post/5afcd516f265da0b9e65414b" target="_blank" rel="noopener">手把手带你撸一个vue组件库！</a></li><li><a href="https://juejin.im/post/5bbab9de5188255c8c0cb0e3" target="_blank" rel="noopener">详解：Vue cli3 库模式搭建组件库并发布到npm</a></li></ul><p>第一个链接中的文章写得很细致，是一个完整的从0到1的构建流程，不过，我并不打算完全采用该方案。主要理由是…该文章发布于 2018年 5月 17日，啊，一年半了呀，vue-cli 都从 2 进化到 3 了！文章的前半部分目前依然适用，但打包发布部分已经过时。文章采用的打包发布方案用的依然是裸写 webpack 配置的方案，该方案虽然可行，但过于繁琐，而且需要开发者了解 webpack 的配置写法，其次 vue-cli 3 默认隐藏了 webpack 的相关配置，官方都不建议用户直接接触 webpack 配置了，我们为什么还要去简就繁呢？<del>用好webpack太南了</del>其实 vue-cli 3 已经有官方的方案了…</p><p>那我们点开第二个链接再来看一下另一个方案。图文并茂，而且是非常完整的从构建到发布的整一个流程！而且文章里采用的打包方案是基于 vue-cli 3 的，更重要的是文章告诉我一个信息：vue-cli 3 已经自带了组件的打包脚本。</p><pre><code>&quot;scripts&quot;: {  // ...  &quot;lib&quot;: &quot;vue-cli-service build --target lib --name vcolorpicker --dest lib packages/index.js&quot;}</code></pre><p>我根据文章的开发步骤，成功生成了一个组件，并通过编写多个 packages 可以生成一个组件库。<br>等等..好像有些不对劲？</p><p>为什么我编写了多个 packages 打包出来的依然是一个文件…虽然打包成单文件还是多文件对于组件库来说影响不大，但对使用者而言，如果为了使用组件库里一个组件，而需要引入一整个组件库…<del>这还玩个球呀</del></p><h2 id="如何实现多文件打包？"><a href="#如何实现多文件打包？" class="headerlink" title="如何实现多文件打包？"></a>如何实现多文件打包？</h2><p>既然遇到了问题，那就想办法去解决一下吧！既然 vue-cli 3 自带了打包脚本，那就去官方文档上看一下，没准就有打包成多文件的方案呢。</p><p><a href="https://cli.vuejs.org/zh/guide/" target="_blank" rel="noopener">Vue Cli 指南</a></p><p>很快我就找到了<strong>构建目标</strong>章节。里面有构建库的方案，也有构建应用和 Web Components 的方案。其中，构建库的方案<del>看来之前那位作者是完全信仰了尤大大呀</del>跟上文提到的完全一致。</p><p><img src="https://h5.jiliguala.com/activity/3aaceb16520d5c54e01cdc2310e95b2d.png" alt="官网构建方案"></p><p>通过 <code>--target lib</code> 默认生成 umd、umd.min 和 common 格式的文件以及 css。通过 Web Components 可以生成单个或多个 Web Components 组件…哎？我怎么是多个 Web Components 组件的打包方案，我要到多文件 lib 方案呢？我又看了一遍文档，依然没有发现我希望的多文件组件库方案，难道是尤大大忘了写进文档了？感觉又走进了死胡同了…</p><p>不行？咱就再 Google 一下吧！</p><ul><li><a href="https://juejin.im/post/5d2c248151882556d1683363" target="_blank" rel="noopener">组件库之按需加载</a></li><li><a href="https://juejin.im/post/5d3e4a66f265da1b7c6161ee" target="_blank" rel="noopener">教你搭建按需加载的Vue组件库</a></li></ul><p>第一篇文章给你科普的是“什么叫按需加载”，完全不是我想要的内容。<br>第二篇文章虽然有提到组件库的打包，但实际上是教你如何使用 <code>babel-plugin-component</code> 实现组件库的按需加载。</p><p>那我换一个关键词 Google 一下试试？”vue组件库 构建 按需加载”</p><p><a href="https://github.com/vuejs/vue-cli/issues/2851" target="_blank" rel="noopener">vue-cli 3，构建lib模式的时候，怎么才能做到把包按组件进行 …</a></p><p>搜索出来的文章大多跟我想要解决的问题无关，倒是其中一个 github/vue-cli 的 issue 引起了我的注意。原来也有人再苦恼我遇到的问题，而且还给官方提了 issue，心喜，难道我的问题有解了？可事实总是残酷的，这个 issue 里满满的负能量…总之，三个字“不支持”。</p><p>兜兜转转，还是回到了原地。既然 vue-cli 3 支持打包单文件组件，那我是不是可以<del>魔改</del>修改 vue-cli 的打包脚本实现一个打包多文件的方案呢？</p><p>那我就从 vue-cli 的源码里开始找吧…</p><p>经过漫长的查找定位…终于找到了我需要的打包 lib 的<a href="https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-service/lib/commands/build/index.js" target="_blank" rel="noopener">源码</a>：</p><pre><code>// https://github.com/vuejs/vue-cli/blob/master/packages/@vue/cli-service/lib/commands/build/index.jsvue-cli/packages/@vue/cli-service/lib/commands/build/index.js</code></pre><p><del>鸡汁如我，藏得这么深都能被我找到。</del></p><p>不看源码还不知道，原来 vue-cli-service 有着这么多的可选参数：</p><pre><code>options: {  &#39;--mode&#39;: `specify env mode (default: production)`,  &#39;--dest&#39;: `specify output directory (default: ${options.outputDir})`,  &#39;--modern&#39;: `build app targeting modern browsers with auto fallback`,  &#39;--no-unsafe-inline&#39;: `build app without introducing inline scripts`,  &#39;--target&#39;: `app | lib | wc | wc-async (default: ${defaults.target})`,  &#39;--inline-vue&#39;: &#39;include the Vue module in the final bundle of library or web component target&#39;,  &#39;--formats&#39;: `list of output formats for library builds (default: ${defaults.formats})`,  &#39;--name&#39;: `name for lib or web-component mode (default: &quot;name&quot; in package.json or entry filename)`,  &#39;--filename&#39;: `file name for output, only usable for &#39;lib&#39; target (default: value of --name)`,  &#39;--no-clean&#39;: `do not remove the dist directory before building the project`,  &#39;--report&#39;: `generate report.html to help analyze bundle content`,  &#39;--report-json&#39;: &#39;generate report.json to help analyze bundle content&#39;,  &#39;--skip-plugins&#39;: `comma-separated list of plugin names to skip for this run`,  &#39;--watch&#39;: `watch for changes`}</code></pre><p>可…似乎并没有设置多文件的打包方案。原以为“柳暗花明”，结果依然“山穷水尽”。都走了这么远的路了，怎么可以就此放弃！</p><h2 id="从头开始思考"><a href="#从头开始思考" class="headerlink" title="从头开始思考"></a>从头开始思考</h2><p>既然没办法走阳关大道，那只要有路还是可以走一走的。</p><p>通过之前的思考，目前我可以掌握以下三个关键信息：</p><ul><li>vue-cli 3 支持打包组件</li><li>可以通过 npm 依赖实现组件的按需加载（使用时）</li><li>可以通过自己编写脚本实现能够支持按需加载的组件库</li></ul><p>通过以上信息，可以证明实现支持按需加载的组件库还是有可能的。<del>啊，呸，我早就知道了，可惜臣妾还是做不到呀！</del></p><p>那么我们不妨来综合一下以上三点：通过脚本多次执行 vue-cli 的打包命令，生成可以直接按需加载的组件库结构。</p><p>看来有戏！</p><p>vue-cli 的脚本是通过命令行执行的，那么可以借助 <a href="https://www.npmjs.com/package/shelljs" target="_blank" rel="noopener">shelljs</a> 可以实现。但生成的 umd、umd.min 以及 demo.html 文件都不是我们需要的，当然我们可以借助 shelljs 在构建后将他们删除。那有没有更好的方式让他们一开始就不生成呢？</p><p>我们回过头去 vue-cli-service 的脚本配置，其中的 <code>--formats</code> 选项虽然在官方文档里没有提及，但实际上是可用的，通过设置 <code>--formats commonjs</code> 可以很容易的实现在构建时只生成 <code>index.common.js</code> 和 <code>index.css</code> 这两个文件。</p><p>那有没有办法再把 <code>index.common.js</code> 变成 <code>index.js</code> 呢？关是猜，当时是没有用的，既然文档里都没提及 <code>--formats</code>，自然也不会有相应的使用说明，我们还得去看 vue-cli 的源码！</p><pre><code>// https://github.com/vuejs/vue-cli/blob/master/packages/@vue/cli-service/lib/commands/build/resolveLibConfig.jsvue-cli/packages/@vue/cli-service/lib/commands/build/resolveLibConfig.js</code></pre><p>通过 vue-cli-service 的依赖引用，我很快定位到了这个文件，但也很快确定了一点，没有参数可以移除 common 后缀，因为 common 文件的导出是固定带 common 后缀的！</p><p>既然此路不同..那我就绕道使用 shelljs 在构建后移除对应的 common 后缀吧。为什么不早这么做呢，因为我<del>比较懒</del>相信尤大大啊！</p><p>默认导出的目录结构显然不能满足按需加载插件的需求，所以很多成熟的组件库都会自己写脚本去生成对应目录结构。但我既然已经上了 vue-cli 的车了，总不该半路“跳车”吧。<del>我真的懒得写那么复杂的构建脚本…</del>解决方案便是再次借助万能的 shelljs 来实现~</p><h2 id="打包脚本代码"><a href="#打包脚本代码" class="headerlink" title="打包脚本代码"></a>打包脚本代码</h2><p>完整的打包代码如下：</p><pre><code class="line-numbers language-js">const shell = require(&#39;shelljs&#39;)const version = require(&#39;../package.json&#39;).versionlet scripts = []shell.ls(&#39;packages&#39;).forEach(file =&gt; {  /**   * 由于 vue-cli-service 在 build 过程中会先删除导出的父级目录，   * 因此需要先执行主入口文件的打包命令   */  if (file === &#39;index.js&#39;) {    scripts.unshift({      type: &#39;index&#39;,      script: &#39;vue-cli-service build --target lib --name index --formats commonjs --dest lib packages/index.js&#39;,    })  } else {    scripts.push({      type: &#39;package&#39;,      filename: file,      script: `vue-cli-service build --target lib --name index --formats commonjs --dest lib/${file} packages/${file}/index.js`,    })  }})scripts.forEach(config =&gt; {  if (config.type === &#39;index&#39;) {    shell.exec(config.script)    /**     * 目前构建项目利用的是 vue-cli 原生的 lib 构建方案，     * 但目前并不支持生成不带 .common 后缀的文件，     * 此处脚本就是用于 hack 该问题的代码     */    shell.mv(&#39;lib/index.common.js&#39;, &#39;lib/vcomp.js&#39;)    shell.mv(&#39;lib/index.css&#39;, &#39;lib/vcomp.css&#39;)    // 更新组件库版本号    shell.sed(&#39;-i&#39;, &#39;0.0.0&#39;, version, &#39;lib/vcomp.js&#39;)  } else if (config.type === &#39;package&#39;) {    shell.exec(config.script)    shell.mv(`lib/${config.filename}/index.common.js`, `lib/${config.filename}/index.js`)    shell.mkdir(`lib/${config.filename}/style`)    shell.echo(`require(&#39;../index.css&#39;)`).to(`lib/${config.filename}/style/index.js`)  }})</code></pre><p>至此，通过“曲线救国”的方式，我顺利的完成了 vue 组件库开发的任务。</p><p>注意，编译后的项目最好避免使用有副作用的 <code>postcss</code> 插件，有些 <code>postcss</code> 插件，比如 <code>postcss-px2rem</code> 会影响项目的构建，属于有副作用类型的插件。</p>]]></content>
    
    <summary type="html">
    
      &lt;pre&gt;&lt;code&gt;Leader：我们最近好几个项目都有用到相似的业务逻辑，比如，手机验证码注册功能最好能有一个组件库来支持，这样就不需要要多次编写相似的代码了。
Me：我..
Leader：做一个 vue 的组件库应该没什么问题吧。
Me：额..
Leader：我记得你 OKR 里有写到这个。
Me：没问题！&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;del&gt;以上内容纯属虚构&lt;/del&gt;&lt;/p&gt;
    
    </summary>
    
    
      <category term="Vue" scheme="https://xiangfa.org/categories/Vue/"/>
    
    
      <category term="Vue 组件库" scheme="https://xiangfa.org/tags/Vue-%E7%BB%84%E4%BB%B6%E5%BA%93/"/>
    
      <category term="Vue" scheme="https://xiangfa.org/tags/Vue/"/>
    
      <category term="组件库" scheme="https://xiangfa.org/tags/%E7%BB%84%E4%BB%B6%E5%BA%93/"/>
    
  </entry>
  
</feed>
