講評世界そして、次の曲が始まるのですhttps://blog.moeyua.com/5515617275574272555155533279089664- My App Defaults 2023https://blog.moeyua.com/posts/my-defaults-2023/https://blog.moeyua.com/posts/my-defaults-2023/Thu, 28 Dec 2023 17:16:37 GMT<p>最近在 <a href="https://anotherdayu.com/2023/5452/">Another Dayu</a> 这里发现了 <a href="https://defaults.rknight.me/">App Defaults</a> 这个有趣的项目,而我从来没法抗拒这种类型的分享,所以这就是我的 App Defaults:</p>
<ul>
<li>Mail Client: Apple Mail</li>
<li>Mail Server: iCloud Mail</li>
<li>Notes: Apple Notes</li>
<li>To-Do: Apple Reminders</li>
<li>iPhone Photo Shooting: Apple Camera</li>
<li>Photo Management: Apple Photos</li>
<li>Calendar: Apple Calendar</li>
<li>Cloud File Storage: iCloud</li>
<li>RSS: <a href="https://reederapp.com/">Reeder</a></li>
<li>Contacts: Apple Contacts</li>
<li>Browser: <a href="https://arc.net/">Arc</a></li>
<li>Chat: <a href="https://telegram.org/">Telegram</a></li>
<li>Bookmarks: <a href="https://gist.github.com/moeyua/5265cc7b43de8ddf2ea7e7808c771ef2">GitHub Gist</a></li>
<li>Read It Later: <a href="https://reederapp.com/">Reeder</a> + <a href="https://www.instapaper.com/">Instapaper</a></li>
<li>Word Processing: <a href="https://code.visualstudio.com/">Visual Studio Code</a></li>
<li>Spreadsheets: <a href="https://www.apple.com/numbers/">Numbers</a></li>
<li>Shopping Lists: <a href="https://www.notion.so/">Notion</a></li>
<li>Budgeting and Personal Finance: <a href="https://percento.app/">Percento</a></li>
<li>News: <a href="https://reederapp.com/">Reeder</a></li>
<li>Music: <a href="https://www.spotify.com/">Spotify</a></li>
<li>Podcasts: Apple Podcasts</li>
<li>Password Management: <a href="https://1password.com/">1Password</a></li>
<li>Code Editor: <a href="https://code.visualstudio.com/">Visual Studio Code</a></li>
<li>Terminal: <a href="https://hyper.is/">Hyper</a></li>
<li>Network Tool: <a href="https://nssurge.com/">Surge</a></li>
<li>Launcher: <a href="https://raycast.com/">Raycast</a></li>
<li>Blog Platform: <a href="https://astro.build/">Astro</a> + <a href="https://vercel.com/">Vercel</a></li>
<li>Code Assistant: <a href="https://copilot.github.com/">Copilot</a></li>
</ul>
<h2>一些补充和吐槽</h2>
<h3>RSS</h3>
<p>我依然希望每一个人都能使用 RSS 来订阅自己喜欢的内容,而不是被推送算法所控制。</p>
<h3>Browser</h3>
<p>在这之前我一直在使用 Chrome,但是 Arc 的体验让我不得不改变这个选择。我很欣赏 Arc 对于侧边栏的尝试,而且优秀的 UI 也让人爱不释手。基于 Chromium 的 Arc 也让我不用担心开发过程中的问题。我很期待 Arc 的未来。</p>
<h3>Bookmarks</h3>
<p>我一直在使用 GitHub Gist 来存储我的书签,原因有以下几点:</p>
<ul>
<li>能在链接后增加一些必要的描述,防止自己想不起来这是做什么的/为什么收藏;</li>
<li>能够精简浏览器收藏夹,快速找到常用的内容;</li>
<li>可以通过插件的形式对本页面内容进行快速搜索;</li>
<li>经过简单拓展能够转变为「个人知识库」。</li>
<li>可以向其他人共享。</li>
</ul>
<h3>Budgeting and Personal Finance</h3>
<p>我很赞同 Percento “「记帐」的根本目的是「管账」” 的理念,只更新重要的财务变动将「记账」这件事的负担降到了最低,而且 Percento 的设计也非常好看。</p>
<h3>Surge</h3>
<p>从去年开始我就陆续将自己的科学上网工具转向了 Surge。Surge 在 iOS 和 macOS 的协同体验非常棒,而且由于 Surge 定位是 Network Toolbox,这使得 Surge 也能完成一些简单的网络管理功能。今年 tvOS 开放了网络拓展功能后 Surge 也能够在 Apple TV 上使用,让我的看剧体验更加完美。</p>
<h3>Code Assistant</h3>
<p><s>我不敢想没了 Github Copilot 我还怎么写代码。</s></p>
<h2>最后</h2>
<p>我很期待看到更多人的 App Defaults,如果你也有兴趣,可以在 <a href="https://defaults.rknight.me/">App Defaults</a> 这里找到更多信息。</p>
随便写点Moeyua
- 读书的意义https://blog.moeyua.com/posts/%E8%AF%BB%E4%B9%A6%E7%9A%84%E6%84%8F%E4%B9%89/https://blog.moeyua.com/posts/%E8%AF%BB%E4%B9%A6%E7%9A%84%E6%84%8F%E4%B9%89/Wed, 26 Apr 2023 22:07:50 GMT<p>朋友在世界读书日看到一个名为《对你影响最大的书是什么》的话题的时候:</p>
<blockquote>
<p>感觉虽然这些年读的比前些年多
但是得到的东西都是很碎片化的东西
看到这种话题我需要回想一下</p>
</blockquote>
<p>看书的影响很大程度上是潜移默化的,短时间根本看不出来。一本书看完大概只有其中的几个片段能留在脑海里,大部分内容在看的时候就已经改变了你的思考方式。</p>
<p>跟何况「影响最大」本身就难讲,是指态度?思想方式?还是现实生活呢。昨晚在书店和女朋友正好聊到漫画,我说“我最喜欢的漫画应该是知音漫客上的《暴走邻家》,如果没看这个漫画,我现在八成还不知道是哪里的混混呢”。那这么看对我影响最大的书就是一本不太知名的二流漫画,或者也可能是《ES6标准入门》,但从现实角度考虑肯定不会是陈嘉映。</p>
<p>书嘛,看就看了,也不用非要追求什么意义。非要说有什么意义的话,大概是能让我滑手机的时间更少一点。</p>
随便写点Moeyua
- そして、次の曲が始まるのですhttps://blog.moeyua.com/posts/%E3%81%9D%E3%81%97%E3%81%A6%E6%AC%A1%E3%81%AE%E6%9B%B2%E3%81%8C%E5%A7%8B%E3%81%BE%E3%82%8B%E3%81%AE%E3%81%A7%E3%81%99/https://blog.moeyua.com/posts/%E3%81%9D%E3%81%97%E3%81%A6%E6%AC%A1%E3%81%AE%E6%9B%B2%E3%81%8C%E5%A7%8B%E3%81%BE%E3%82%8B%E3%81%AE%E3%81%A7%E3%81%99/Sat, 31 Dec 2022 20:31:39 GMT<p>我已经不记得我是怎么度过上一个新年假期的了。新年,除了代表新的一年开始,也就是平常的一天,这些平常的一天拼凑成了过去的一整年。过去一年,日常变得格外重要。日常是什么?大概是那些维持着我们生活的一件件小事。它们往往琐碎而反复,却构成了生活最基本的底色。</p>
<p>听说今年是我的本命年。妈妈讲以前给我上户口的时候为了方便把出生日期提前了一年,导致我身份证上的年龄比我实际的年龄小一岁,再加上老家有「实岁」「虚岁」的说法,这让我从 12 岁以后就没有搞明白过自己的年纪,每次到了要填写年龄的时候就要在脑海里推演一番。</p>
<p>第一次听到 Taylor Swift 的《 22 》的时候在想自己 22 岁会是什么样的,会在做什么呢?一转眼就 24 岁了,现在的我在做什么呢?</p>
<blockquote>
<p>听着 Taylor's Version 的《 Fearless 》,坐在桌子前写着那时自己最讨厌的小作文。</p>
</blockquote>
<p>不知道那个小孩会满意现在的生活吗,我不知道。但现在的这个小孩还算满意。</p>
<p>生日的时候给自己买了红白配色的 Air Jordan 1 Mid Chicago 2020",女朋友送了一支红色的 LAMY safari。本命年嘛,就该是红色,而我也一直很喜欢红色。是因为《 Red 》这张专辑吗?是那辆「红得像是火焰的法拉利 599 GTB Fiorano」吗?或者干脆是开着法拉利的红发巫女诺诺呢?我不知道,但总之我不是路明非,以前不是,现在也不是。</p>
<hr />
<p>今年我时常会想,如果我选择了不同的道路,又会有怎样的际遇呢。会像是在无限延展的四叠半世界里一般,无论怎么选,都会是差不多的结果吗?</p>
<p>年初时负责带我的姐姐怀了孕,组里就只剩我一个前端。刚毕业半年的我突然就被通知要负责组里大部分的前端业务,大量的陈旧项目让我感到十分焦虑。为了对抗这种焦虑,我选择了大量的阅读,从以前一年不到 5 本书到今年的接近 20 本书,我不知道这种方式是不是真的有用,我也不确定我是不是真的从书中学到了什么,但这可以让我暂时从焦虑中解脱出来,告诉自己没有浪费时间,我没有停下来。</p>
<p>后半年学了一点国际象棋,顺势给自己买了一套胡桃木的棋子。我很享受那种用指尖提起一枚棋子,思考片刻后,棋子落回棋盘发出一声沉闷但又有点清脆的响声,这让我几乎忘掉任何的不愉快。</p>
<blockquote>
<p>Twenty years from now you will be more disappointed by the things that you didn't do than by the ones you did do. So throw off the bowlines. Sail away from the safe harbor. Catch the trade winds in your sails. Explore. Dream. Discover.</p>
</blockquote>
<hr />
<p>夏天。又是一个闷热的夏天,又一次被迫异地。</p>
<p>道别之后回过身的我,突然有了一种不知所措的感觉,不知道要去哪里,索性放空大脑,任自己在迷宫一样的地铁站内游荡。<br />
回到家里,原先很多塞满的地方空了出来,像是忽然从你的生活中抽走,留下一个巨大的空隙。要接受这件事并不容易,似乎也有一点能够体会到我母亲的感受。</p>
<p>我开始在房间里大声讲话,大声放音乐,大声听播客,给自己制造噪音。在经历了一周后,我去 Apple Store 提了一个 Homepod mini 回来,让这个小小的,圆滚滚的东西的声音充满整个房间,充满所有被抽走后留下的空隙。</p>
<hr />
<p>十月份的国庆假期,我准备回老家度过这七天的假期。起因是九月份的时候,母亲在电话那头带着一点哭腔的说已经快要想不起来我的样子了。想了想我的确是自从春节离开以后就再没有回去过了,慌乱中安抚好母亲,挂掉电话我便预定了九月底的机票,还有今年的年假。</p>
<blockquote>
<p>人生路上,步履不停。总有那么一点来不及。</p>
</blockquote>
<p>我不知道当那天到来之时我会不会悔恨,但我知道,失去的终究还是失去了,四叠半不会给你答案,即便你再怎么追问也只会迷失在四叠半的世界里。我能把握的也就只有现在,还有未来的每一个「现在」。</p>
<p>意料之外的是,国庆假期期间遭遇了封控。这是这座小城第一次遭遇到如此规模的封控,也没有人想到会长达两个月之久,直到十一月底的一系列事件发生才逐渐的解除。</p>
<p>多出来的这一个多月时间让我有了足够时间去陪伴家人,同时也体会到了居家办公的感受。居家办公的感受很独特,抛开一些沟通上的不便,它能够让工作回归到工作本身——我只需要完成我的工作,而不必关心我是在什么时候完成的,是早上九点还是晚上九点,这不重要,重要的是能够保质保量的完成工作,能够及时处理问题,这就够了。而我也不必担心早上要起床,计算路上花费的时间,以及价格不菲的房租,任何地方都可以成为我的办公室。</p>
<p>在家办公的我实际上并没有感受到疫情和封控带来的影响,在家呆着几乎是每个阿宅必备的技能,抱着 Switch 把火焰纹章反反复复玩了四个周目。开始的时候可以到小区外的超市买一些东西,为了去买东西我还会偶尔做一下核酸检测,后来小区大门被锁后我干脆家门都不出,并延伸出了「只要我不做核酸我就不会被感染」的封控哲学。</p>
<hr />
<p>12 月中旬,返回南京。从飞机上下来,一直到离开机场,一路畅通无阻。沿途看到几位旅客,因为没有用到提前打开的健康码而感到困惑。巨大的机器突然停下,曾经占据了生活的许多限制消失不见,可人们的身体和心灵显然未做足准备,依然保持着惯性。</p>
<p>在经历了 10 天左右的停滞后,明显能够感受到许多东西正在回归,外卖、超市、行人、车流。社会不会停下来等待什么,因为社会不能停滞下来,既要自然延续,在过去的惯性中发展,也需要不断扩容,探索出全新的模式。社会重启是一种机会,对所有人都是个不小的挑战。</p>
<p>就在我写下这篇文章的时候,京都动画公开了《吹响!上低音号》系列时隔 4 年的续作,在这个时间点上,京都动画与京吹也迎来了它们的重启,美好的事情正在发生,下一曲终将奏响。</p>
<p>そして、次の曲が始まるのです!</p>
随便写点Moeyua
- 从零开始的 RSSHub Docker 私有化部署指南https://blog.moeyua.com/posts/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%9A%84-rsshub-docker-%E7%A7%81%E6%9C%89%E5%8C%96%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97/https://blog.moeyua.com/posts/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%9A%84-rsshub-docker-%E7%A7%81%E6%9C%89%E5%8C%96%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97/Mon, 27 Jun 2022 19:16:52 GMT<p>使用 RSSHub 也已经很长一段时间了,慢慢萌生了尝试一下自己部署 RSSHub 的想法,能够进行一些自定义的内容,也可以解决许多国内网站反爬严格的问题。部署过程还算顺利,这里做一个记录。</p>
<h4>安装 Docker</h4>
<ol>
<li>
<p>安装 <code>yum-utils</code>:</p>
<pre><code>sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
</code></pre>
</li>
<li>
<p>安装 <code>Docker Engine</code>:</p>
<pre><code>sudo yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin
</code></pre>
</li>
<li>
<p>启动 Docker:</p>
<pre><code>sudo systemctl start docker
</code></pre>
</li>
<li>
<p>确认 Docker 安装正确:</p>
<pre><code>sudo docker run hello-world
</code></pre>
<p>这个命令下载了一个测试镜像,并在一个容器中运行它。当容器运行时,它会打印一条信息并退出。</p>
</li>
<li>
<p>设置 Docker 开机启动</p>
<pre><code>sudo systemctl enable docker
</code></pre>
</li>
</ol>
<h5>安装 Docker Compose</h5>
<ol>
<li>
<p>下载 Docker Compose 的当前稳定版本:</p>
<pre><code></code></pre>
</li>
</ol>
<p>sudo curl -L "https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose</p>
<pre><code>
2. 将可执行权限应用于二进制文件:
```bash
sudo chmod +x /usr/local/bin/docker-compose
</code></pre>
<ol>
<li>
<p>检查版本:</p>
<pre><code>docker-compose --version
</code></pre>
</li>
</ol>
<h4>部署 RSSHub</h4>
<ol>
<li>
<p>下载 <a href="https://github.com/DIYgod/RSSHub/blob/master/docker-compose.yml">docker-compose.yml</a></p>
<pre><code>wget https://raw.githubusercontent.com/DIYgod/RSSHub/master/docker-compose.yml
</code></pre>
</li>
<li>
<p>检查有无需要修改的配置</p>
<pre><code>vi docker-compose.yml
</code></pre>
</li>
<li>
<p>创建 volume 持久化 Redis 缓存</p>
<pre><code>docker volume create redis-data
</code></pre>
</li>
<li>
<p>启动</p>
<pre><code>docker-compose up -d
</code></pre>
</li>
</ol>
<h4>开放服务器端口</h4>
<p><code>docker-compose.yml</code> 文件中配置的端口号默认为 1200,需要在服务器上开启对应的端口号,使用时只需要将 https://rsshub.app/twitter/user/evodmoeyua 中的 <code>https://rsshub.app</code> 替换为 <code>http://yourip:1200</code>。</p>
技术文档Moeyua
- 使用 Homebrew 安装 Typora 的 0.11.18 版本https://blog.moeyua.com/posts/%E4%BD%BF%E7%94%A8-homebrew-%E5%AE%89%E8%A3%85-typora-%E7%9A%84-0-11-18-%E7%89%88%E6%9C%AC/https://blog.moeyua.com/posts/%E4%BD%BF%E7%94%A8-homebrew-%E5%AE%89%E8%A3%85-typora-%E7%9A%84-0-11-18-%E7%89%88%E6%9C%AC/Mon, 27 Jun 2022 18:45:39 GMT<p><a href="https://typora.io/">Typora</a> 虽然现在已经变成付费制软件,但是官方还是保持 <code>1.0</code> 版本之前的版本为免费应用,<s>而且还把下载链接藏了起来</s>。
虽然我们依然能够从官网下载到最后一个免费版本 <code>0.11.18</code>,但是<s>程序员</s>我们可能还不仅限于此,习惯使用 <a href="https://brew.sh/">Homebrew</a> 的人更倾向于使用 Homebrew 来管理自己的大部分软件。手快的同学可能已经发现 Homebrew 上根本找不到旧版本的 Typora 包。
这里我就记录一下如何使用 Homebrew 下载 Typora,改方法理论上同样适用于其他 Homebrew 不提供的旧版本软件。</p>
<blockquote>
<p>被官方藏了起来的下载链接:<a href="https://download.typora.io/mac/Typora-0.11.18.dmg">Typora-0.11.18.dmg</a>,懒得折腾的同学可以直接下载安装。</p>
</blockquote>
<ol>
<li>查看一下 Typora 的信息 <code>brew info typora</code>:</li>
</ol>
<pre><code>➜ brew info typora
typora: 1.3.7 (auto_updates)
https://typora.io/
Not installed
From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/typora.rb
==> Name
Typora
==> Description
Configurable document editor that supports Markdown
==> Artifacts
Typora.app (App)
==> Analytics
install: 715 (30 days), 1,947 (90 days), 14,205 (365 days)
</code></pre>
<p><code>From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/typora.rb</code>
这里给我们提供了 Typora 的下载脚本地址,我们直接访问这个地址。</p>
<ol>
<li>
<p>查看这个脚本的历史提交记录:
<img src="https://s2.loli.net/2022/06/24/AyiVRpknjxhstme.png" alt="image-20220624125205567" /></p>
</li>
<li>
<p>找到我们需要的历史提交版本
<img src="https://s2.loli.net/2022/06/24/BbFqy1Ym9ZAHGjk.png" alt="image-20220624125303673" /></p>
</li>
<li>
<p>下载这个文件
<img src="https://s2.loli.net/2022/06/24/aUF2j4tGSpPhH7x.png" alt="image-20220624125408151" />
<img src="https://s2.loli.net/2022/06/24/qhBYVtW3RK96pQN.png" alt="image-20220624125452425" /></p>
</li>
</ol>
<p>网页右键选择「另存为/储存为」,将这个文件保存到本地</p>
<ol>
<li>
<p>用任意的文本编辑器,例如「记事本」/「文本编辑」或者 IDE 将 URL 修改为官网提供的地址:<code>https://download.typora.io/mac/Typora-0.11.18.dmg</code>
<img src="https://s2.loli.net/2022/06/24/qVvchCR5eSZAEKN.png" alt="image-20220624125819271" /></p>
</li>
<li>
<p>执行 <code>brew install {下载的文件路径} --cask</code></p>
<pre><code>➜ brew install ./Downloads/typora.rb --cask
==> Downloading https://download.typora.io/mac/Typora-0.11.18.dmg
######################################################################## 100.0%
==> Installing Cask typora
==> Moving App 'Typora.app' to '/Applications/Typora.app'
🍺 typora was successfully installed!
</code></pre>
</li>
</ol>
技术文档Moeyua
- 使用 TypeScript 为 Vue 组件的 prop 标注类型https://blog.moeyua.com/posts/%E4%BD%BF%E7%94%A8-typescript-%E4%B8%BA-vue-%E7%BB%84%E4%BB%B6%E7%9A%84-prop-%E6%A0%87%E6%B3%A8%E7%B1%BB%E5%9E%8B/https://blog.moeyua.com/posts/%E4%BD%BF%E7%94%A8-typescript-%E4%B8%BA-vue-%E7%BB%84%E4%BB%B6%E7%9A%84-prop-%E6%A0%87%E6%B3%A8%E7%B1%BB%E5%9E%8B/Thu, 02 Jun 2022 15:27:41 GMT<p>在选项式 API 或者不使用 <code><script setup></code> 时我们可以使用 <code>PropType</code> 这个工具类型来标记更复杂的 prop 类型:</p>
<pre><code>import { defineComponent } from 'vue'
// 引入 Proptype
import type { PropType } from 'vue'
interface Book {
title: string
author: string
year: number
}
export default defineComponent({
props: {
book: {
// 提供相对 `Object` 更确定的类型
type: Object as PropType<Book>,
required: true
},
// 也可以标记函数
callback: Function as PropType<(id: number) => void>,
itemList: {
// 定义数组类型
type: Array as PropType<Array<SingleItem>>,
required: false,
},
},
})
</code></pre>
<p>如果不使用 <code>PropType</code> 而直接对类型进行断言:</p>
<pre><code>itemList: {
// 项目列表
type: Array as Array<SingleItem>,
required: false,
},
</code></pre>
<p>会得到 <code>类型 "ArrayConstructor" 到类型 "SingleItem[]" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"。</code> 这样的错误。</p>
<p>当使用 <code><script setup></code> 时,我们直接通过泛型参数来定义 prop 的类型:</p>
<pre><code><script setup lang="ts">
const props = defineProps<{
foo: string
bar?: number
}>()
</script>
</code></pre>
<p><code>defineProps()</code> 宏函数支持从它的参数中推导类型,所以我们也可以在它的参数中定义:</p>
<pre><code><script setup lang="ts">
const props = defineProps({
foo: { type: String, required: true },
bar: Number
})
props.foo // string
props.bar // number | undefined
</script>
</code></pre>
技术文档Moeyua
- 【译文】Grid 用于布局, Flexbox 用于组件https://blog.moeyua.com/posts/grid-%E7%94%A8%E4%BA%8E%E5%B8%83%E5%B1%80-flexbox-%E7%94%A8%E4%BA%8E%E7%BB%84%E4%BB%B6/https://blog.moeyua.com/posts/grid-%E7%94%A8%E4%BA%8E%E5%B8%83%E5%B1%80-flexbox-%E7%94%A8%E4%BA%8E%E7%BB%84%E4%BB%B6/Sat, 05 Mar 2022 14:27:05 GMT<p>本文是 <a href="https://ishadeed.com/">Ahmad Shadeed</a> 的博客文章 <a href="https://ishadeed.com/article/grid-layout-flexbox-components/">Grid for layout, Flexbox for components</a> 的翻译。</p>
<p>3 月 5 号开始翻译,摸了 3 个月终于翻译完了,下次还敢(不是</p>
<hr />
<p>我的弟弟是一名刚毕业的软件工程师,现在他正在做前端开发相关的实习岗位。他以前学过 Grid 和 flexbox,但是我发现和我经常网上看到的情况一样,他在布局的时候使用 Grid 还是 flexbox 之间摇摆不定。举个例子,他尝试使用 Grid 布局去开发一个网站的 header,但是当他使用了 <code>grid-column</code> 属性的时候他发现过程好像并不像想象中那么顺利,所以他只能不停地调整来让页面看起来和设计稿一致。</p>
<p>说句实话,我不太喜欢这样子,所以我试着去找一些关于这方面的资料来让他了解 grid 和 flexbox 之间的区别,最好还能带上几个例子,但可惜的是我一无所获。所以我尝试写了一篇涵盖了这个主题所有内容的文章,希望大家能够从中获益。</p>
<h4>介绍</h4>
<p>在深入探讨概念和实例之前,首先要确保你了解 CSS grid 和 flexbox 的主要区别。CSS Grid是一个拥有「行」和「列」概念的多维布局模块。Flexbox 可以将其子项布局成列或行,但不能同时进行(译者注:可以理解为一维)。</p>
<p>如果你还不了解关于 CSS grid 和 flexbox 相关的知识,我非常建议你去阅读这篇 <a href="https://ishadeed.com/article/learn-box-alignment/">可视化文章</a>。如果你已经了解了这些内容那就太棒了,接下来我们就将深入了解它们直接的区别,以及如何使用它们。</p>
<h4>Grid 和 Flexbox 之间的不同点</h4>
<p>首先我需要澄清一点,关于「何时使用 CSS Grid 或 flexbox」这里没有一个非常明确的界线。还有一点就是,没有「使用这种方法 <strong>正确</strong> 或者 <strong>错误</strong>」这种说法。这篇文章是推荐在特定的使用情况下使用某种技术的指南,我将会讲述一些基本概念,然后通过一些例子说明这些概念,剩余的就需要靠你自己去探索和实验了。</p>
<pre><code>/* Flexbox 容器 */
.wrapper {
display: flex;
}
/* Grid 容器 */
.wrapper {
display: grid;
grid-template-columns: 2fr 1fr;
grid-gap: 16px;
}
</code></pre>
<p><img src="https://s2.loli.net/2022/03/05/TR6MeZLhgxrW7ED.png" alt="grid-vs-flexbox" /></p>
<p>发现了什么吗?Flexbox 是在一行内布局自己的元素,CSS grid 使其转化为拥有行和列的表格。Flexbox 是在行内进行对齐的,当然如果我们愿意也可以在列内。</p>
<pre><code>/* Flexbox 容器 */
.wrapper {
display: flex;
flex-direction: column;
}
</code></pre>
<p><img src="https://s2.loli.net/2022/03/05/Eo3KZHtYLcURTwv.png" alt="grid-vs-flexbox-1" /></p>
<h4>如何决定使用哪种布局呢</h4>
<p>在 CSS grid 和 flexbox 之间做决定有时会很困难,特别是刚入门 CSS 的新手,我非常理解这种心情,我在开始选择之前会问自己下面这几个问题:</p>
<ul>
<li>
<p>组件内的元素是如何展示的?行内还是行和列?</p>
</li>
<li>
<p>组件在不同种类的屏幕大小下期望的展示方式是什么?</p>
</li>
</ul>
<p>如果组件的子元素的排列方式是行内,那大多数情况最好的方案应该就是 flexbox 了。看看下面这个例子:</p>
<p><img src="https://s2.loli.net/2022/03/05/msqhfZrGS3toQ9Y.png" alt="decide-1" /></p>
<p>如果是行和列的话,CSS grid 应该是这种情况下的方案。</p>
<p><img src="https://s2.loli.net/2022/03/05/APLnzX29OUEB5iu.png" alt="decide-2" /></p>
<p>现在我已经说明了它们之间主要的不同点,现在让我们看一下更加特殊的例子来学习如何决定这两种布局。</p>
<h4>一些案例和实例</h4>
<p>在下面的章节中,我将详细讨论 flexbox 和 grid 的不同使用情况。</p>
<h5>CSS Grid</h5>
<h6>两栏式布局(Main And Sidebar)</h6>
<p>当你需要一个两栏式布局(一个侧边栏和主要内容)时,CSS grid 就是最好的选择,请看下面这个例子:</p>
<p><img src="https://s2.loli.net/2022/03/07/7RwjJ92avFyO5VX.png" alt="grid-use-1" /></p>
<p>这里是实现代码:</p>
<pre><code><div class="wrapper">
<aside>Sidebar</aside>
<main>Main</main>
</div>
</code></pre>
<pre><code>@media (min-width: 800px) {
.wrapper {
display: grid;
grid-template-columns: 200px 1fr;
grid-gap: 16px;
}
aside {
align-self: start;
}
}
</code></pre>
<p>如果 <code><aside></code> 元素没有使用 <code>align-self</code> 属性,那么它的高度就会无视内容,始终等于 <code><main></code> 元素。</p>
<h6>Cards Grid(网格卡片)</h6>
<p>正如我们在文章开头所说的,布局 cards grid 的最佳方式从 CSS grid 的名字就不言而喻了。</p>
<p><img src="https://s2.loli.net/2022/03/07/wPGfl194NcrRhFj.png" alt="grid-use-2" /></p>
<p>这里是我实现这个布局的代码:</p>
<pre><code>.wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-gap: 16px;
}
</code></pre>
<p>每一列的宽度最小应该是 <code>200px</code>,如果没有足够的空间则会自动换到下一行。需要注意的是,如果视口(viewport)的宽度小于 <code>200px</code> ,上述写法会导致在水平方向上发生滚动。</p>
<p>一个比较简单的解决方式是只有在 <code>viewport</code> 的宽度足够时上述代码才会生效,像下面这样:</p>
<pre><code>@media (min-width: 800px) {
.wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-gap: 16px;
}
}
</code></pre>
<h6>Section Layout</h6>
<p>在下面的这个例子中,我们可以使用两次 grid 布局来实现。首先我们使用 grid 布局将整个区域分成两个部分(侧边栏部分和表单部分),接着我们就可以使用 grid 将表单进行布局。</p>
<p><img src="https://s2.loli.net/2022/03/09/lZfCT9dbjVUqLex.png" alt="grid-use-3" /></p>
<p>I can’t emphasis how CSS grid is perfect for that. 下面是 CSS 实现代码:</p>
<pre><code>@media (min-width: 800px) {
.wrapper {
display: grid;
grid-template-columns: 200px 1fr;
}
.form-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 16px;
}
.form-message,
.form-button {
grid-column: 1 /3; /* 让它们充满整个宽度 */
}
}
</code></pre>
<p>这个例子引用自我在 Envato 的 <a href="https://webdesign.tutsplus.com/tutorials/how-to-build-web-form-layouts-with-css-grid--cms-28776">这一篇文章</a>,这是一篇关于在 web 开发时如何使用 CSS grid 进行布局的文章,非常值得一读。</p>
<h5>CSS Flexbox</h5>
<h6>网站导航</h6>
<p>在 90% 的情况下,网站的导航栏都应该使用 CSS flexbox 进行开发。它们最大的共同之处就是在左边有一个 logo,导航的部分都在右边。这种情况非常适用于 flexbox。</p>
<p><img src="https://s2.loli.net/2022/03/09/mhKH4MjieyZ3uc5.png" alt="flexbox-use-1" /></p>
<p>要想实现上面这个例子,你只需要按照下面的方式进行设置:</p>
<pre><code>.site-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
</code></pre>
<p>同样的,上述代码也可以用在下面的这种设计中。</p>
<p><img src="https://s2.loli.net/2022/03/09/dS1gIjmuVKABCFN.png" alt="flexbox-use-2" /></p>
<p>你应该注意到导航栏的结构发生了一点变化,但是子元素之间的间距仍由 <code>ustify-content</code> 属性决定。</p>
<h6>操作列表</h6>
<p>当你听到列表的时候第一反应一定是垂直排列的列表。这里特别说明一下,一个列表也可以在一行内排列,所以各位不要搞错。</p>
<p>关于操作列表的例子我们可以参考 Facebook 或者 Twitter。操作列表由几个操作按钮组成。我们看看下面的这张截图吧:</p>
<p><img src="https://s2.loli.net/2022/03/09/7jLgqSUGeJiTKuZ.png" alt="flexbox-use-3" /></p>
<p>就像你看到的那样,所有元素之间彼此相邻,而且它们水平分布。Flexbox 简直是最完美的选择,这也是它的核心用途之一。</p>
<pre><code>.action-list {
display: flex;
}
.action-list_item {
flex: 1; /* 让所有子元素扩大,使得它们能够平分所有空间 */
}
</code></pre>
<p>另一个类似的用法就是用作弹窗的标题和操作按钮。
不管弹窗页脚 <code>footer</code> 还是页眉 <code>header</code> 的子元素全都是在行内排列,就像你看到的,他们之间的空隙如下所示:
<img src="https://s2.loli.net/2022/03/14/NfznIZOB9DGk1b5.png" alt="flexbox-use-4" />
对于弹窗的页眉来说,下面这种写法就能够满足需要:</p>
<pre><code>.modal-header {
display: flex;
justify-content: space-between;
}
</code></pre>
<p>页脚对于我们来说可能有一些不同的地方。「取消」按钮需要将 <code>margin</code> 设置为 <code>auto</code> 来将它放置到右边。关于这一点我写了一篇详细介绍这方面内容的 [文章](https://ishadeed.com/article/auto-css/ 。</p>
<pre><code>.cancel_action {
margin-left: auto;
}
</code></pre>
<p><code>.cancel_action</code> 可能在这里不是一个好的命名方式,但是在这篇文章里我并不打算详细说明有关 CSS 的命名规则。</p>
<h6>表单元素</h6>
<p>一个输入框旁边紧挨着一个按钮的组合可以说是 Flexbox 的完美案例了。
请看下面这个例子:</p>
<p><img src="https://s2.loli.net/2022/03/14/A9v8msD6bk3WzTU.png" alt="flexbox-use-5" /></p>
<p>在第一个表单当中,输入框占据了所有的剩余空间,所以我们需要给它设置一个动态的宽度。同样的,第二个表单(Facebook 的 Messenger)也是一样,文字输入框占据了所有的剩余位置。让我们试着模仿一下。</p>
<p><img src="https://s2.loli.net/2022/03/14/rmgPZoAE6tlJ3Mb.png" alt="flexbox-use-6" /></p>
<pre><code>.input {
flex: 1 1 auto;
}
</code></pre>
<p>主要注意的是,如果我们没有在文字输入框上设置 <code>flex: 1 1 auto</code>,那么输入框就不会自动填充满整个剩余空间。</p>
<h6>跟帖回复</h6>
<p>Flexbox 的另一个比较通用的例子就是「跟帖回复」,比如下面这个例子:</p>
<p><img src="https://s2.loli.net/2022/03/16/O24yCjo6KvsnUfc.png" alt="flexbox-use-7" /></p>
<p>这里有用户的头像和评论本身,评论模块占据了容器的所有剩余空间,用 flexbox 可以完美的实现这种布局。</p>
<h6>卡片组件</h6>
<p>卡片组件的种类有非常多,但是一般来说最常见的还是下面例子中的这种设计:</p>
<p><img src="https://s2.loli.net/2022/03/16/fXLsipQD7mkBV52.png" alt="flexbox-use-8" /></p>
<p>左边的例子中,我们将 flex 的方向设置为了 <code>column</code>,所以卡片的子元素是叠在一起的。相反,右边的方向是 <code>row</code> ,而且 flexbox 默认的方向就是 row,这一点千万不要忘记。</p>
<pre><code>.card {
display: flex;
flex-direction: column;
}
@media (min-width: 800px) {
.card {
flex-direction: row;
}
}
</code></pre>
<p>另外一个比较常见的变种是在卡片中有一个图标,而且会有一个文字标签紧挨在下面。它可能是一个按钮、一个链接、或者就仅仅是装饰而已。让我们看看下面这个例子:</p>
<p><img src="https://s2.loli.net/2022/03/16/3GD6qXxuSekcLBd.png" alt="flexbox-use-9" /></p>
<p>值得注意的是图标和文本标签在水平和垂直方向是居中的。感谢 flexbox,让我们能够很简单的实现这种布局。</p>
<pre><code>.card {
display: flex;
flex-direction: column;
align-items: center;
}
</code></pre>
<p>行内的样式是默认的,我们只需要删除 <code>flex-direction: column</code> 让它恢复到默认的值 <row> 就可以了。</p>
<h6>标签页 / 底部菜单</h6>
<p>当一个元素的宽度需要始终等于屏幕宽度,而且它的子元素需要填满所有的可用空间,这时 flexbox 就是我们的最佳选择。</p>
<p><img src="https://s2.loli.net/2022/03/17/BvNJwm58njPo1db.png" alt="flexbox-use-10" /></p>
<p>在上面这个例子中,所有的子元素都应该充满可用空间,而且所有子元素的宽度相同。我们只需要将容器的 display 设置为 <code>flex</code> 就可以很轻松的实现这一布局了。</p>
<pre><code>.tabs_item {
flex-grow: 1;
}
</code></pre>
<p>这种方式被 React Native 框架用来在移动端创建 Tab 菜单。这里有一段 React Native 的示例代码展示了和上面一样的内容。这里的代码是从 <a href="https://reactnative.dev/docs/flexbox">这里</a> 借鉴来的。</p>
<pre><code>import React from "react";
import { View } from "react-native";
export default FlexDirectionBasics = () => {
return (
<View style=>
<View
style=
/>
<View
style=
/>
<View
style=
/>
</View>
);
};
</code></pre>
<h6>功能列表</h6>
<p>关于 flexbox 最喜欢的一点就是它能够随意翻转元素的方向。flexbox 默认的方向是 <code>row</code> ,但是我们可以像下面这样转换一下:</p>
<pre><code>.item {
flex-direction: row-reverse;
}
</code></pre>
<p>在下面这个例子中我们能看到偶数项被翻转,这就是通过上面的方式实现的,非常实用。</p>
<p><img src="https://s2.loli.net/2022/04/21/YoIa5zwRxBjuXry.png" alt="flexbox-use-11" /></p>
<h6>居中内容</h6>
<p>然后我们设想这样一种情况:我们有一个很重要的部分,这个部分的内容需要被垂直且水平居中。水平居中我们能够使用文本对齐简单的实现。</p>
<p><img src="https://s2.loli.net/2022/04/21/lCbLxvm29Bq67wT.png" alt="flexbox-use-12" /></p>
<pre><code>.hero {
text-align: center;
}
</code></pre>
<p>但是如何使用 flexbox 将元素垂直居中呢?这正是我们需要实现的:</p>
<pre><code>.hero {
display: flex;
flex-direction: column;
align-items: center; /* 水平居中元素 */
justify-content: center; /* 垂直居中元素 */
text-align: center;
}
</code></pre>
<h4>CSS Grid 和 Flexbox 的结合</h4>
<p>不仅每个布局模块有自己的使用例,我们甚至可以将他们结合起来使用。当我考虑如何将这两种布局方式结合在一起的时候,我脑海中的第一个想法就是卡片列表。使用 Grid 来布局卡片,使用 flexbox 来布局卡片组件自身。</p>
<p><img src="https://s2.loli.net/2022/04/28/yWjkASTfL8zbaVC.png" alt="grid-and-flex" /></p>
<p>上面的这个布局有以下几点需求:</p>
<ul>
<li>
<p>每一行的卡片高度应该保持相等;</p>
</li>
<li>
<p>不管卡片高度如何,Read more 按钮应该始终在卡片的底部;</p>
</li>
<li>
<p>Grid 应当使用 <code>minimal()</code> 函数。</p>
</li>
</ul>
<pre><code><div class="wrapper">
<article class="card">
<img src="sunrise.jpg" alt="" />
<div class="card__content">
<h2><!-- Title --></h2>
<p><!-- Desc --></p>
<p class="card_link"><a href="#">Read more</a></p>
</div>
</article>
</div>
</code></pre>
<pre><code>@media (min-width: 500px) {
.wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-gap: 16px;
}
}
.card {
display: flex; /* [1] */
flex-direction: column; /* [2] */
}
.card__content {
flex-grow: 1; /* [3] */
display: flex; /* [4] */
flex-direction: column;
}
.card__link {
margin-top: auto; /* [5] */
}
</code></pre>
<p>现在让我来解释一下上面的 CSS。</p>
<ol>
<li>将卡片设置成 flexbox 容器</li>
<li>排列方向为纵向,也就是说卡片的元素是通过栈的方式进入的</li>
<li>让卡片的内容扩大并充满所有剩余空间</li>
<li>将卡片内容设置为 flexbox 容器</li>
<li>最后,使用 <code>margin-top: auto</code> 将 链接 推入栈中。这样不管卡片高度是多少它都会保持在最底部。</li>
</ol>
<p>正如你所看到的,结合 CSS grid 和 flexbox 并不是很困难。这两种方式可以给我们带来许多种实现 web 布局的方式。我们应该正确的使用它们,而且<strong>只在</strong>像上述需要的情况去结合它们。</p>
<h4>支持旧版本的浏览器的回退方案</h4>
<h5>通过 CSS <code>@supports</code></h5>
<p>大概在几个月之前,我得到了一条推特回复说我的网站在 IE11 上崩溃了。在检查过后我发现了一个奇怪的现象。所有的网站内容都被压缩都了左上角的区域。我的网站无法使用了!</p>
<p><img src="https://ishadeed.com/assets/grid-flex/ishadeed-ie11.png" alt="ishadeed-ie11" /></p>
<p>是的,这就是我的网站——一个前端开发工程师的网站,在 IE11 上。首先让我感到困惑的是,这是如何发生的?我记得 CSS grid 是支持 IE11 的,但是这是微软发布的旧版本。解决方法非常简单,那就是使用 <code>@supports</code> 只在新的浏览器中使用 CSS grid。</p>
<pre><code>@supports (grid-area: auto) {
body {
display: grid;
}
}
</code></pre>
<p>让我解释一下。我使用 <code>grid-area</code> 是因为它只在新的 CSS grid 规范中被支持,从2017年3月到今天。由于IE不支持 <code>@supports</code> 查询,整个规则将被忽略。因此,新的CSS网格将只用于支持的浏览器。</p>
<h5>使用 Flexbox 作为 CSS Grid 的回退方案</h5>
<p>flexbox 不适合用于展示网格布局中的项目,但是这并不意味着它不能当作备用方案。你可以使用 flexbox 来作为 不支持 CSS grid 浏览器的备用方案。我曾经开发了一个<a href="https://shadeed.github.io/grid-to-flex/">工具l</a>用来解决这个问题。</p>
<pre><code>@mixin grid() {
display: flex;
flex-wrap: wrap;
@supports (grid-area: auto) {
display: grid;
grid-gap: 16px 16px;
}
}
@mixin gridAuto() {
margin-left: -16px;
> * {
margin-bottom: 16px;
margin-left: 16px;
}
@media (min-width: 320px) {
> * {
width: calc((99% / #{2}) - 16px);
flex: 0 0 calc((99% / #{2}) - 16px);
}
}
@media (min-width: 768px) {
> * {
width: calc((99% / #{3}) - 16px);
flex: 0 0 calc((99% / #{3}) - 16px);
}
}
@supports (grid-area: auto) {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
margin-left: 0;
> * {
width: auto;
margin-left: 0;
margin-bottom: 0;
}
}
}
</code></pre>
<p>上面的回退代码通过下面所述的方式运行:</p>
<ol>
<li>添加 <code>display: flex</code> 和 <code>flex-wrap: wrap</code> 使元素换行;</li>
<li>检查 CSS grid 是否被支持,如果支持,那么就会使用 <code>display: grid</code>;</li>
<li>通过使用 <code>> *</code>选择器,我们可以选择容器的直接子后代。在选择完成后,我们就可以给它们每一个都添加一个特殊的宽度或者大小了。</li>
<li>当然,它们之间的间隙是必须的,如果浏览器支持 CSS grid,我们将会用 <code>grid-gap</code> 来充当间隙。</li>
</ol>
<p>这里是一个使用 Sass mixin 的例子:</p>
<pre><code>.wrapper {
@include grid();
@include gridAuto();
}
</code></pre>
<p><a href="https://codepen.io/shadeed/pen/XWrLmYe">Demo</a></p>
<h4>如果 Grid 和 Flexbox 都无法正常使用</h4>
<p>当我和我弟弟在进行代码评审的时候,我注意到了几个 CSS grid 和 flexbox 都会错误的使用方式,我认为将它们作为重点写出来很有必要。</p>
<h5>使用 CSS Grid 来布局网站头部区域</h5>
<p>这个问题是让我写下这篇文章的动机之一。我注意到我的弟弟使用 CSS grid 来实现网站的头部区域。</p>
<p>他辩解道“CSS grid 实在是太复杂,太难了“等等。由于使用了不正确的布局方法,他得到了一个想法,认为这是 CSS grid 太复杂造成的。其实不然,他所有的困惑都来自于把它用在不合适的地方的事实。</p>
<p>看一下我注意到的这个例子:</p>
<p><img src="https://s2.loli.net/2022/05/07/sCj3tBUTWmLZSrv.png" alt="incorrect-use-1" /></p>
<pre><code>.site-header {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
}
.site-nav {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
}
</code></pre>
<p>CSS grid 被使用了两次,第一次是用于整个标题,第二次是用于导航。他用 <code>grid-column</code> 来微调元素之间的间距,还有其他一些奇怪的东西,我记不起来了,但是最重要的你应该明白了吧!</p>
<h5>使用 CSS Grid 在标签上</h5>
<p>CSS grid 的另一个不正确的用法是将其应用于标签组件。请看下面的模拟图。</p>
<p><img src="https://s2.loli.net/2022/05/07/QGpurMsVOt8RUwN.png" alt="incorrect-use-2" /></p>
<p>下面是错误的 CSS 代码:</p>
<pre><code>.tabs-wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
</code></pre>
<p>从上面的代码中,我可以看到,开发者假设标签数只有三个。因此,他用 <code>1fr 1fr 1fr</code> 来布置列。如果列数发生变化,布局就失效了。</p>
<h5>过度使用 Flexbox 或者 Grid</h5>
<p>记住,旧的布局方法可能是完美的方式。过度使用 flexbox 或 grid 会使你的 CSS 的复杂程度随着时间而增加。我并不是说它们很复杂,而是像本文中的例子所解释的那样,在正确的情况下<strong>正确地</strong>使用它们会好很多。</p>
<p>例如,你有如下的主要部分,要求将其所有内容水平居中。</p>
<p><img src="https://s2.loli.net/2022/05/07/yB3ocaDu6G5RvmY.png" alt="img" /></p>
<p>我们可以通过 <code>text-align: center</code> 来实现这种布局,那么这个时候我们为什么要去使用 felxbox 这种更复杂的方式?</p>
<h4>总结</h4>
<p>关于使用 CSS Grid 和 Flexbox 之间的区别,我们已经说了很多了。这个话题我想了很久,我很高兴有机会写这个话题。如果有任何问题请不要犹豫,通过电子邮件或twitter <a href="https://twitter.com/shadeed9">@shadeed9</a> 提供反馈!</p>
<p>感谢你的阅读!</p>
译文Moeyua
- 【译文】IndexedDB 为什么这么慢?如何更好的使用呢?https://blog.moeyua.com/posts/indexeddb-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%99%E4%B9%88%E6%85%A2%E5%A6%82%E4%BD%95%E6%9B%B4%E5%A5%BD%E7%9A%84%E4%BD%BF%E7%94%A8%E5%91%A2/https://blog.moeyua.com/posts/indexeddb-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%99%E4%B9%88%E6%85%A2%E5%A6%82%E4%BD%95%E6%9B%B4%E5%A5%BD%E7%9A%84%E4%BD%BF%E7%94%A8%E5%91%A2/Fri, 14 Jan 2022 22:48:32 GMT<p>本文是 <a href="https://github.com/pubkey/rxdb">RxDB</a> 文档 Opinions 部分的文章 <a href="https://rxdb.info/slow-indexeddb.html">Why IndexedDB is slow and what to use instead</a> 的翻译,原作者为 <a href="https://github.com/pubkey">pubkey</a>。</p>
<hr />
<p>我们可能出于离线使用的需求,也可能是出于缓存等等的其他目的,需要将 JavaScript Web Application 的数据保存在客户端本地也就是浏览器里。而在浏览器内存储数据一般来说有以下几个选项:</p>
<ul>
<li><strong>Cookies</strong> 会随着每次 HTTP 请求被发送出去,所以它不能存储太多的数据。</li>
<li><strong>WebSQL</strong> 已经被 <a href="https://hacks.mozilla.org/2010/06/beyond-html5-database-apis-and-the-road-to-indexeddb/">弃用</a>,因为它从来都不是一个标准,而将它变成标准又十分困难。</li>
<li><strong>LocalStorage</strong> 是一个基于异步 IO-access 的同步 API,存储和读取都会使得 JavaScript 进程被完全阻塞,所以不应该在有许多键值对的情况下使用 LocalStorage 。</li>
<li><strong>FileSystem API</strong> 可以用来存储简单的二进制文件,但可惜现在 <a href="https://caniuse.com/filesystem">只有 Chrome 支持</a> 这一特性。</li>
<li><strong>IndexedDB</strong> 是一种「键-值数据库」,可以用来存储 json 类型的数据,而且能够遍历所有的索引。IndexedDB 不仅稳定,而且得到 <a href="https://caniuse.com/indexeddb">广泛的支持</a>。</li>
</ul>
<p>不难看出,最好的选择就是 IndexedDB。选定了要使用的方法后就可以着手进行开发了。在刚开始的时候似乎一切都很不错,但随着进度的推进你的应用程序也越来越大,你需要处理更多或者更复杂的数据,这时你发现事情并没有那么简单—— <strong>IndexedDB 太慢了</strong>,甚至比运行在廉价服务器上的数据库<strong>还要慢</strong>!插入几百个文档就要花费好几秒的时间。对于一个需要快速加载的页面来说时间是至关重要的,有时候直接向后端发送请求来传输数据都要比 IndexedDB 要快。</p>
<blockquote>
<p>事务处理 vs 吞吐量</p>
</blockquote>
<p>在抱怨之前我们可以先分析一下这么慢的原因。当你在 Nolans 的 <a href="http://nolanlawson.github.io/database-comparison/">浏览器数据库比较</a> 中测试时就能发现:插入 1k 条文档到 IndexedDB 大概会花费 80ms,平均 0.08ms 一条,不仅不算慢,甚至可以称得上很快了,而且我们也不太可能同时在客户端存储如此大量的数据。但问题的关键在于这些文档是在 <code>single transaction</code> 上写入的。</p>
<p>所以我 fork 了一个 <a href="https://pubkey.github.io/client-side-databases/database-comparison/index.html">对比工具</a> 并将每次写入文档的方式修改为 <code>single transaction</code>,我们可以看到 <code>single transaction</code> 插入 1k 条文档大概花费 2 秒钟。但有趣的是,当我们把文档的大小增加到原来的 100 倍以后,存储这些数据的时间差不多和原来是一样的!这下我们大概就清楚了原来限制 IndexedDB 性能的是 <code>transaction</code> 而不是数据吞吐量。</p>
<p><img src="https://s2.loli.net/2022/01/06/E5ewCK6vYoMWfxP.png" alt="IndexedDB transaction throughput" /></p>
<p>要想解决 IndexedDB 的性能问题你可以尽可能使用更少的 <code>transactions</code> 。有的时候很容易就能解决这个问题:使用 RxDB 的 <a href="https://rxdb.info/rx-collection.html#bulkinsert"><code>bulk</code> 方法</a> 你就可以一次性将许多数据压缩并存储。但是大多数情况下事情没有这么简单:你的用户不停的在页面上点击,重复的数据不停的从后端发送过来,另外一个页面同时还在写入数据。所有的这些事情都会在你不知道的什么时候发生,你也不可能将这些数据全都在单个 <code>transactions</code> 处理完成。</p>
<p>另一个解决的办法就是不要再去关心它的性能问题。一些浏览器厂商将会对 IndexedDB 进行优化,它的速度将会有所改观。当然,IndexedDB 缓慢的问题在 <a href="https://www.researchgate.net/publication/281065948_Performance_Testing_and_Comparison_of_Client_Side_Databases_Versus_Server_Side">2013 年</a> 就已经有了,按照这种趋势,我们有理由相信在未来几年它也还是会缓慢下去,所以我们不应该等待下去。chromium 的开发者们也发布了一个 <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1025456#c15">声明</a> 来呼吁大家更多的关注读取性能而不是写入性能。</p>
<p>使用 WebSQL (即便它已经被弃用)也不是一个很好的选择,因为就像 <a href="https://pubkey.github.io/client-side-databases/database-comparison/index.html">对比工具显示的结果</a> 一样,它的 <code>transactions</code> 甚至更慢。</p>
<h2>不要将 IndexedDB 当作数据库来使用</h2>
<p>为了处理性能问题和防止 <code>transaction</code> ,我们应当停止将 IndexedDB 当作数据库来使用。相反的,我们应当在初始页面加载时就将所有数据载入到内存(<code>memory</code>)当中。这样一来所有的读写操作都在内存当中进行,而内存的读写速度是原来的 100 倍。只有在数据写入以后,通过单事务写入的方式将内存状态持久化到 IndexedDB 中。这种情况下 Indexed 是作为一个文件系统来使用的,而不是一个数据库。</p>
<p>这里有一些已经采用了这种方式的库:</p>
<ul>
<li>LokiJS with the <a href="https://techfort.github.io/LokiJS/LokiIndexedAdapter.html">IndexedDB Adapter</a></li>
<li><a href="https://github.com/jlongster/absurd-sql">Absurd-SQL</a></li>
<li>SQL.js with the <a href="https://emscripten.org/docs/api_reference/Filesystem-API.html#filesystem-api-idbfs">empscripten Filesystem API</a></li>
<li><a href="https://duckdb.org/2021/10/29/duckdb-wasm.html">DuckDB Wasm</a></li>
</ul>
<h2>持久化</h2>
<p>不直接使用 IndexedDB 的一个缺点就是你的数据不会一直持久化。当 JavaScript 进程在你还没有持久化到 IndexedDB 之前就退出的话,你的数据很有可能就丢失了。为了防止这种情况发生,我们必须要确保内存中的数据已经被写入到了硬盘。一个很重要的点就是尽可能快的将数据进行持久化。例如 LokiJS 就提供了 <code>incremental-indexeddb-adapter</code> 用来持久化最新的数据到硬盘当中而不是每次都去持久化所有的数据。而另外一点就是要在正确的时间去持久化你的数据。例如 RxDB <a href="https://rxdb.info/rx-storage-lokijs.html">LokiJS storage</a> 只会在以下几种情况持久化数据:</p>
<ul>
<li>当有新的数据写入出现,而此时数据库处于空闲状态,既没有读取也没有写入在进行。此时会对数据进行持久化。</li>
<li>当 <code>window</code> 触发了 <a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload">beforeunload event</a> 的时候,说明此时的 JavaScript 进程随时都可能退出,这时一定要进行数据持久化。在 <code>beforeunload</code> 结束之后会有大概几秒的时间让我们足够保存所有新的改动,这足以证明我们的工作是可靠的。</li>
</ul>
<p>唯一遗漏的地方就是如果浏览器突然崩溃或者电脑电源被关闭,这会导致浏览器意外退出。</p>
<h2>多标签支持</h2>
<p>Web application 和 「普通」应用程序的最大不同就在于用户可以同时在多个浏览器标签当中使用你的应用。但是假如你的所有内存中的数据库状态只会定期写入硬盘,多个浏览器标签可能会发生冲突造成数据丢失。但是这可能对于依赖于服务端响应的应用程序不是什么问题,因为丢失的数据可能早已经上传到了后端,其他标签的数据也一样。但是如果你的客户端是离线使用的话这样就行不通了。</p>
<p>解决这个问题最理想的方法就是使用 <a href="https://developer.mozilla.org/en/docs/Web/API/SharedWorker">SharedWorker</a>。SharedWorker 和 <a href="https://developer.mozilla.org/en/docs/Web/API/Web_Workers_API">WebWorker</a> 一样都运行在单独的 JavaScript 进程中,唯一不同的是 SharedWorker 会在多个上下文见进行共享。你可以在 SharedWorker 中创建数据库,这样所有的浏览器标签就会向 Worker 请求数据而不是去单独创建一个数据库。但很遗憾的是 SharedWorker API 并<a href="https://caniuse.com/sharedworkers">不支持所有浏览器</a>。Safari <a href="https://bugs.webkit.org/show_bug.cgi?id=140344">放弃了对它的支持</a>,而 IE 和 安卓平台的 Chrome 甚至从来都没有适配过。</p>
<p>此外,我们可以通过 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API">BroadcastChannel API</a> 在标签页之间进行通信,然后在它们之间采用 <a href="https://github.com/pubkey/broadcast-channel#using-the-leaderelection">leader election</a>。Leader election 能够确保无论有多少个标签页被打开永远都会有一个标签是 <code>Leader</code>。</p>
<p><img src="https://s2.loli.net/2022/01/10/YP8U3OXiMhHtFuT.gif" alt="Leader Election" /></p>
<p>Leader election 的缺点就是它的进程会在首页面加载时消耗一定时间(大概 150 毫秒)。此外,当 JavaScript 进程阻塞的时候 Leader election 可能会被中断。当这种情况发生时,一个好的解决办法就是重新加载浏览器的标签使 election 进程重新启动。</p>
<p><a href="https://rxdb.info/rx-storage-lokijs.html">RxDB LokiJS Storage</a> 已经实现了 Leader election 这种方法来支持多标签。</p>
<h2>延伸阅读</h2>
<ul>
<li><a href="https://github.com/pubkey/client-side-databases">Offline First Database Comparison</a></li>
<li><a href="https://nolanlawson.com/2021/08/22/speeding-up-indexeddb-reads-and-writes/">Speeding up IndexedDB reads and writes</a></li>
<li><a href="https://hackaday.com/2021/08/24/sqlite-on-the-web-absurd-sql/">SQLITE ON THE WEB: ABSURD-SQL</a></li>
<li><a href="https://anita-app.com/blog/articles/sqlite-in-a-pwa-with-file-system-access-api.html">SQLite in a PWA with FileSystemAccessAPI</a></li>
</ul>
译文Moeyua
- Hello 2022https://blog.moeyua.com/posts/hello-2022/https://blog.moeyua.com/posts/hello-2022/Fri, 31 Dec 2021 14:28:09 GMT<p>2021 对我来说是前所未有的一年,太多事情影响了我,能够很明显感觉到自己的改变,在很多事情上有了更加清晰的认识。希望在即将到来的 2022 年,不管是自己的文字也好,写的代码也好,还是我的一些想法也好,都能给他人带来一点帮助和启发,让这个似乎已经很糟糕的世界变得更好,这应该就是每一个理想主义者的宿命罢。</p>
随便写点Moeyua
- 「他山之石」零贰https://blog.moeyua.com/posts/%E4%BB%96%E5%B1%B1%E4%B9%8B%E7%9F%B3%E9%9B%B6%E8%B4%B0/https://blog.moeyua.com/posts/%E4%BB%96%E5%B1%B1%E4%B9%8B%E7%9F%B3%E9%9B%B6%E8%B4%B0/Mon, 29 Nov 2021 15:50:53 GMT<blockquote>
<p>艰深的语言有时出自学术内容的要求,有时则用来骗自己吓唬别人,浅显的语言,有时是大师的炉火纯青,有时是流于表面不肯深思。<br />
—— <a href="https://pdc.capub.cn/search.html#/detail?id=2k6hfft25xobqsuotpzkf6jzwme2fq5gsrxbuhqfbkwsz5n3ersa&from=1&type=marc">《走出唯一真理观》</a></p>
</blockquote>
<blockquote>
<p>现在,好多学术文章难读,跟内容深奥曲折没什么关系。很多时候无非显示他是个学术家,是个身份标志。我们要识别一个人的身份,可以看他穿什么品牌进什么餐厅,但最保险的是听他开口说话,萧伯纳在《卖花女》一剧中把这一点写得淋漓尽致。派个中学语文老师去和卖毒品的接头,一开口人家就识破你不是同行。一个行当有一个行当的行话,主要的功能是设置门槛,不让这个行当外面的人混进来。你要搞学术得有个“会员证”,证件上的戳子就是学术语言——你可以不会德文、英文、希腊文、古文,但你不能不会学术语言。你说“天冷,水都结冰了”,他说“在外因的作用下量变导致了质变”,一听就听出谁有学问谁没学问。你没啥悟性,没啥才华,只要你会说学术语言就是学者,所以你埋头苦练,四年大学外加三年研究院,毕业后再实习三五年。费这么大劲儿学到的东西,谁挡得住他玩命用?<br />
—— <a href="https://pdc.capub.cn/search.html#/detail?id=2k6hfft25xobqsuotpzkf6jzwme2fq5gsrxbuhqfbkwsz5n3ersa&from=1&type=marc">《走出唯一真理观》</a></p>
</blockquote>
<blockquote>
<p>我不把 Facebook、Instagram、TikTok、Snapchat 这种软件,看成互联网的一部分。它们只是一个应用程序,只向注册会员开放,不与外部分享数据。它们虽然有网站,但是难于使用,而且有很多限制,只作为补充的访问方式。<br />
——<a href="https://blog.archive.today/post/665401109290074112/why-do-you-view-fb-ig-tiktok-snapchat-as">Archive.is blog</a></p>
</blockquote>
<blockquote>
<p>我们接触了很多好设计、差设计,但通常都有一个的印象:设计是为了更好的体验。设计师拼命把产品的体验设计得更好,消费者才会买单。但如果买单的人是政府、地产开发商这些公共空间的运营者,设计就是为他们服务的。他们需要保护自己的资产,来为他们的受众服务。这中间就会有一些人被排除了。这些人通常会是流浪汉、残障人士,但很多情况下,普通人也会受影响。<br />
———— <a href="https://mp.weixin.qq.com/s/v_gQVw1gdYRkO1fUi5HTsQ">强行在尖刺上休息的法科尔</a></p>
</blockquote>
<blockquote>
<p>瞎说几句,只说防止过度反思,不说反思不足。一、我们循着道理反思,反思时,时不时停一下,跟自己的经验对勘。别只被道理领着走,因为我们认之为道理的,不一定是真道理、实实在在的道理。更不要事事“上纲上线”。二、跟不那么好反思的人交谈,跟未经反思的想法对勘。三、体会一下自己的生性有多厚,反思以不压垮生机为限。四、参加足球、篮球之类的团队体育活动,要求你即时反应,即时与他人互动。归结为一点,用厚实的生存托起反思。<br />
—— <a href="https://pdc.capub.cn/search.html#/detail?id=2k6hfft25xobqsuotpzkf6jzwme2fq5gsrxbuhqfbkwsz5n3ersa&from=1&type=marc">《走出唯一真理观》</a></p>
</blockquote>
<blockquote>
<p>像“成为你自己”这样箴言,可以有无数不同的理解,深者得其深,浅者得其浅。很多人好道,但没有时间或心力去阅读、去理解系统的哲理,记一些箴言在心里,随自己的实践成长,时不时回味对照。这跟系统研习有坚实论证的哲学不是一回事,但从实践领悟来说,两者也会有殊途同归之妙。箴言也分好多种,像“己所不欲勿施于人”这样的箴言,内容相当确定,可以视作一种系统伦理观的概括。<br />
—— <a href="https://pdc.capub.cn/search.html#/detail?id=2k6hfft25xobqsuotpzkf6jzwme2fq5gsrxbuhqfbkwsz5n3ersa&from=1&type=marc">《走出唯一真理观》</a></p>
</blockquote>
<blockquote>
<p>现在说创新,大家都去创新没问题,但创新成功的毕竟是少数。本来是一个个小的community,有点小本事一点小创新就行,有点儿小成就就怪激动,别人也跟着激动。现在不是全世界最新的就不算创新。要做就得做到世界上最好的。那我们哪有这个机会?我们现在见多识广,你没做到世界级,就算失败了。从前的藩篱打破了,有一种解放感。我出生在一个小山村子,第一次来到海边,站在大海边上,会有解放的感觉。可是你永远回不到你的村庄,永远站在浩瀚无际的大海边上,甚至漂流在大海中央,那就可能不是解放感而是无力感了。今天的人容易产生失败感和无足轻重感,原因非常多,我想这跟人人都面对漫无边界的整个世界有关。一个人直接面对太大的世界会带来一种无力感。<br />
我的想法很老套———新的时代来了,有些东西会失去,有些人怀旧,我的朋友里甚至有人设想建立儒家保护区。在我看,退回去是不可能的,我们能做的是设法把我们所珍爱的东西融合到互联网时代之中去。每个时代都有它自己的好,自己的坏。我们争取把自己的好东西传下去,不管时代有多艰难,只要你挺过来了,就可以把一些美好的东西传下去。你们的时代已经大大不同了,但还是可能把这些美好的东西融化在你们自己的生活之中。<br />
—— <a href="https://pdc.capub.cn/search.html#/detail?id=2k6hfft25xobqsuotpzkf6jzwme2fq5gsrxbuhqfbkwsz5n3ersa&from=1&type=marc">《走出唯一真理观》</a></p>
</blockquote>
他山之石Moeyua
- 「他山之石」零壹https://blog.moeyua.com/posts/%E4%BB%96%E5%B1%B1%E4%B9%8B%E7%9F%B3%E9%9B%B6%E5%A3%B9/https://blog.moeyua.com/posts/%E4%BB%96%E5%B1%B1%E4%B9%8B%E7%9F%B3%E9%9B%B6%E5%A3%B9/Sat, 20 Nov 2021 13:01:32 GMT<blockquote>
<p>关于人生社会问题的思考,跟科学的思考有根本的不同。科学的思考在一个很简单的意义上是有真理性的。一道数学题,最简单地说,我们承认有一个标淮答案或者类似标淮的答案,关于人生问题,社会的问题,对我来说很显然,没有一套标准答案。另一方面,并不因为没有一套标淮答案,这里就完全没有真理性,而无非是我喜欢这样你喜欢那样,各是其所是非其所非就完了。这里仍然有实质性的讨论、对话、争论,我们可能实质性地被说服,获得更富真理性的见地。要把这里的真理性说清楚,殊非易事。一条恩考路径是,去弄清科学如何成其为科学的,它为什么会得到它所得到的那类真理,弄清了这个,你岂不就明白了人生问题的思考为什么不能够达到那种真理性,以及为什么不应该达到?岂不就对怎样去思考人生社会问题有个更牢靠的自我意识?
-- <a href="https://pdc.capub.cn/search.html#/detail?id=2k6hfft25xobqsuotpzkf6jzwme2fq5gsrxbuhqfbkwsz5n3ersa&from=1&type=marc">《走出唯一真理观》</a></p>
</blockquote>
<blockquote>
<p>任何科研讨论的开端,并不是基于什么真实、完整、完美的事物,而是基于假定、猜想、假说。虽然我们不能完整描述一个事物,但我们能逼近这个事物。
巴别塔故事的核心在于,上帝为了阻止人类交流,而创造了不同的语言。但在我看来,不可交流性与生俱来,与语种无关。同一语言内部也存在不可交流性,而其不可交流性正是源于语言的延异,与对元概念理解的偏差。<br />
-- <a href="https://sspai.com/post/60478">《当我们谈论 1+1=2 时,我们在谈论什么?》</a></p>
</blockquote>
<blockquote>
<p>贝多芬不高雅——这个我们都知道——听贝多芬的高雅。我们属于生产力。很多人都会有这种想象,把艺术家的生活和艺术混在一起,就是把做工作的人和那个工作本身连在一起。生产者其实就是个劳动者。<br />
-- <a href="https://pdc.capub.cn/search.html#/detail?id=2k6hfft25xobqsuotpzkf6jzwme2fq5gsrxbuhqfbkwsz5n3ersa&from=1&type=marc">《走出唯一真理观》</a></p>
</blockquote>
他山之石Moeyua
- 「言論」 零壹https://blog.moeyua.com/posts/%E8%A8%80%E8%AB%96-%E9%9B%B6%E5%A3%B9/https://blog.moeyua.com/posts/%E8%A8%80%E8%AB%96-%E9%9B%B6%E5%A3%B9/Sat, 20 Nov 2021 12:51:08 GMT<p>分类的时候一定会出现的两个问题,一个是会出现不属于任何一个分类的文件,另一个就是会出现同属于多个类别的文件,所以我认为分类的最好方式就是不分类,再好的分类方案都比不上顺手且快速的搜索方式。</p>
<hr />
<p>所谓的「知识管理」只是一种手段,而不是目的。如果你本身没有需要解决的问题(或者说专注研究的领域),那么知识管理只是个伪命题。</p>
<hr />
<p>输入:尽量多的捕捉下来自己的想法和知识盲区,但避免无脑摘录。
输出:重要的不是文采,而是让自己内化知识,并获得高质量的反馈。</p>
<hr />
<p>任何一个好的系统都不应该耗费你大量的时间去维护,一旦你需要不断地「定期」维护一个系统,那么就违背了系统的初衷 —— 整理东西本身并不能产生太大的价值,除了耗费时间。</p>
言論Moeyua
- 给 icarus 主题增加所有文章的字数统计https://blog.moeyua.com/posts/%E7%BB%99-icarus-%E4%B8%BB%E9%A2%98%E5%A2%9E%E5%8A%A0%E6%89%80%E6%9C%89%E6%96%87%E7%AB%A0%E7%9A%84%E5%AD%97%E6%95%B0%E7%BB%9F%E8%AE%A1/https://blog.moeyua.com/posts/%E7%BB%99-icarus-%E4%B8%BB%E9%A2%98%E5%A2%9E%E5%8A%A0%E6%89%80%E6%9C%89%E6%96%87%E7%AB%A0%E7%9A%84%E5%AD%97%E6%95%B0%E7%BB%9F%E8%AE%A1/Thu, 18 Nov 2021 18:45:17 GMT<p>看到苏卡卡大佬的 profile 上有一个统计所有文章的字数功能,感觉很有意思,于是本菜鸡也决定给自己搞一个。
<!--more-->
虽然说菜,但是菜有菜的办法,先到 <code>profile.jsx</code> 这个文件看一下组件的源码:</p>
<pre><code>const { Component } = require('inferno');
const gravatrHelper = require('hexo-util').gravatar;
const { cacheComponent } = require('hexo-component-inferno/lib/util/cache');
class Profile extends Component {
renderSocialLinks(links) {
if (!links.length) {
return null;
}
return <div class="level is-mobile is-multiline">
{links.filter(link => typeof link === 'object').map(link => {
return <a class="level-item button is-transparent is-marginless"
target="_blank" rel="noopener" title={link.name} href={link.url}>
{'icon' in link ? <i class={link.icon}></i> : link.name}
</a>;
})}
</div>;
}
render() {
const {
avatar,
avatarRounded,
author,
authorTitle,
location,
counter,
followLink,
followTitle,
socialLinks
} = this.props;
return <div class="card widget" data-type="profile">
<div class="card-content">
<nav class="level">
<div class="level-item has-text-centered flex-shrink-1">
<div>
<figure class="image is-128x128 mx-auto mb-2">
<img class={'avatar' + (avatarRounded ? ' is-rounded' : '')} src={avatar} alt={author} />
</figure>
{author ? <p class="title is-size-4 is-block" style={{'line-height': 'inherit'}}>{author}</p> : null}
{authorTitle ? <p class="is-size-6 is-block">{authorTitle}</p> : null}
{location ? <p class="is-size-6 is-flex justify-content-center">
<i class="fas fa-map-marker-alt mr-1"></i>
<span>{location}</span>
</p> : null}
</div>
</div>
</nav>
<nav class="level is-mobile">
<div class="level-item has-text-centered is-marginless">
<div>
<p class="heading">{counter.post.title}</p>
<a href={counter.post.url}>
<p class="title">{counter.post.count}</p>
</a>
</div>
</div>
<div class="level-item has-text-centered is-marginless">
<div>
<p class="heading">{counter.category.title}</p>
<a href={counter.category.url}>
<p class="title">{counter.category.count}</p>
</a>
</div>
</div>
<div class="level-item has-text-centered is-marginless">
<div>
<p class="heading">{counter.tag.title}</p>
<a href={counter.tag.url}>
<p class="title">{counter.tag.count}</p>
</a>
</div>
</div>
</nav>
{followLink ? <div class="level">
<a class="level-item button is-primary is-rounded" href={followLink} target="_blank" rel="noopener">{followTitle}</a>
</div> : null}
{socialLinks ? this.renderSocialLinks(socialLinks) : null}
</div>
</div>;
}
}
Profile.Cacheable = cacheComponent(Profile, 'widget.profile', props => {
const { site, helper, widget } = props;
const {
avatar,
gravatar,
avatar_rounded = false,
author = props.config.author,
author_title,
location,
follow_link,
social_links
} = widget;
const { url_for, _p, __ } = helper;
function getAvatar() {
if (gravatar) {
return gravatrHelper(gravatar, 128);
}
if (avatar) {
return url_for(avatar);
}
return url_for('/img/avatar.png');
}
const postCount = site.posts.length;
const categoryCount = site.categories.filter(category => category.length).length;
const tagCount = site.tags.filter(tag => tag.length).length;
const socialLinks = social_links ? Object.keys(social_links).map(name => {
const link = social_links[name];
if (typeof link === 'string') {
return {
name,
url: url_for(link)
};
}
return {
name,
url: url_for(link.url),
icon: link.icon
};
}) : null;
return {
avatar: getAvatar(),
avatarRounded: avatar_rounded,
author,
authorTitle: author_title,
location,
counter: {
post: {
count: postCount,
title: _p('common.post', postCount),
url: url_for('/archives')
},
category: {
count: categoryCount,
title: _p('common.category', categoryCount),
url: url_for('/categories')
},
tag: {
count: tagCount,
title: _p('common.tag', tagCount),
url: url_for('/tags')
}
},
followLink: follow_link ? url_for(follow_link) : undefined,
followTitle: __('widget.follow'),
socialLinks
};
});
module.exports = Profile;
</code></pre>
<p>很长,虽然没什么头猪但是关键的部分还是能看懂的,首先我们需要添加 html,改个名字,直接复制过来就好了:</p>
<pre><code><div class="level-item has-text-centered is-marginless">
<div>
<p class="heading">{counter.word.title}</p>
<a href={counter.word.url}>
<p class="title">{counter.word.count}</p>
</a>
</div>
<div>
</code></pre>
<p>很简单,但是这时我们需要看一下这个 <code>counter</code> 是什么:</p>
<pre><code>counter: {
post: {
count: postCount,
title: _p('common.post', postCount),
url: url_for('/archives')
},
category: {
count: categoryCount,
title: _p('common.category', categoryCount),
url: url_for('/categories')
},
tag: {
count: tagCount,
title: _p('common.tag', tagCount),
url: url_for('/tags')
}
}
</code></pre>
<p>这个不就是定义三个计数器的属性的嘛,我们也来一个就好了:</p>
<pre><code>word: {
count: wordCount,
title: _p('字数', wordCount),
}
</code></pre>
<p>这里需要注意的是这里我们 title 直接写死,要不然还得去其他地方配置。同时我们不需要点击跳转,所以也就不需要给它 url。 ~<s>想写也没有</s>~</p>
<p>这样一来样式就没问题了,我们开始计算字数,这是个麻烦事,先看看其他三个 counter 是怎么写的吧:</p>
<pre><code>const postCount = site.posts.length;
const categoryCount = site.categories.filter(category => category.length).length;
const tagCount = site.tags.filter(tag => tag.length).length;
</code></pre>
<p>看起来是在 <code>site</code> 这个对象中储存了一些网站的信息,我们打印一下它的 <code>post</code> 属性看看是什么。</p>
<p>......</p>
<p>看起来还挺多,terminal 都放不下了,都是文章的各种信息,而且因为无法显示全部信息,我们不知道这个对象的全部属性是什么,也就没有办法拿到文章内容了。这个时候就需要用到 <code>Object.getOwnPropertyNames</code> 这个方法了。</p>
<blockquote>
<p><code>Object.getOwnPropertyNames</code> 方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。</p>
</blockquote>
<p>这样以来我们就能够摸清楚这个对象的结构了,大概是这样子:</p>
<pre><code>site: {
posts: {
data: [
{
_content: 文章内容,
...,
...
},
{
...
}
],
length: num
},
categories: { ... },
tags: { ... }
}
</code></pre>
<p>文章内容其实有好多个属性都有保存,但是这个格式相对较少,我们就选择这个来统计文章字数。</p>
<pre><code>site.posts.data[0]._content.length
</code></pre>
<p>试一下,好像计算出来的结果有点不对,看起来正好是一倍,那这好办,也懒得管为什么,直接给它砍一半</p>
<pre><code>site.posts.data[0]._content.length / 2
</code></pre>
<p>OK,这样以来就没有问题了,之后只需要遍历 <code>data</code> 对象,计算出所有文章的字数总和就好了,这里是我写的函数:</p>
<pre><code>function getWords(site) {
let posts = site.posts.data;
let words = 0;
for (const post of posts) {
words = words + post._content.length / 2;
}
words = (words / 10000).toFixed(2);
return words;
}
</code></pre>
<p>这里我选择的单位是 <code>万</code>,保留了两位小数。最后我们只需要调用这个函数就 ok 了。</p>
<pre><code> const postCount = site.posts.length;
const categoryCount = site.categories.filter(category => category.length).length;
const tagCount = site.tags.filter(tag => tag.length).length;
const wordCount = getWords(site);
</code></pre>
<p>最后放一张完成后的截图:
<img src="https://i.loli.net/2021/11/18/lKHOfbvDzoE3Tj5.png" alt="" /></p>
技术文档Moeyua
- hexo 无法在本地实时预览https://blog.moeyua.com/posts/hexo-%E6%97%A0%E6%B3%95%E5%9C%A8%E6%9C%AC%E5%9C%B0%E5%AE%9E%E6%97%B6%E9%A2%84%E8%A7%88/https://blog.moeyua.com/posts/hexo-%E6%97%A0%E6%B3%95%E5%9C%A8%E6%9C%AC%E5%9C%B0%E5%AE%9E%E6%97%B6%E9%A2%84%E8%A7%88/Tue, 16 Nov 2021 17:24:09 GMT<p>自从更换了 icarus 主题以后,之前一直在使用的 <code>hexo s --debug</code> 以及 <code>hexo s</code> 都没有办法在本地实时对页面进行更新,只能通过 <code>hexo g</code> 和 <code>hexo s</code> 的方式重新启动服务器才能够更新。虽然这样也能看到预览,但是像我这种写一下看一眼的选手来说这可是要命的,属实是困扰了我好久。</p>
<p><!-- more --></p>
<p>今天摸鱼时候寻思不如赶紧解决掉,查了一圈,虽然不知道是什么原因导致的,但解决办法找到了:</p>
<pre><code>hexo g --watch
</code></pre>
<blockquote>
<p>Hexo 能够监视文件变动并立即重新生成静态文件,在生成时会比对文件的 SHA1 checksum,只有变动的文件才会写入。</p>
</blockquote>
<p>上面是 hexo 官方给出的命令解释,也就是说虽然 <code>hexo s</code> 不能够帮我们监视文件变化,那么我们就自己来监视。只需要在 <code>hexo s</code> 之前启动一个 <code>hexo g --watch</code> 就能解决这个问题了。</p>
<p><a class="tag is-dark is-medium" href="https://www.pixiv.net/artworks/94130143" target="_blank">
<span class="icon"><i class="fas fa-camera"></i></span>
Cover by あんよ@お仕事募集中
</a></p>
疑难杂症Moeyua
- JavaScript 立即调用的函数表达式(IIFE)https://blog.moeyua.com/posts/javascript-%E7%AB%8B%E5%8D%B3%E8%B0%83%E7%94%A8%E7%9A%84%E5%87%BD%E6%95%B0%E8%A1%A8%E8%BE%BE%E5%BC%8Fiife/https://blog.moeyua.com/posts/javascript-%E7%AB%8B%E5%8D%B3%E8%B0%83%E7%94%A8%E7%9A%84%E5%87%BD%E6%95%B0%E8%A1%A8%E8%BE%BE%E5%BC%8Fiife/Wed, 10 Nov 2021 15:25:10 GMT<p>最近工作一直很闲,导师姐姐看我没事做就安排我看一下公司的项目,顺便让我画一份登陆的流程图来(摸鱼不好吗,流泪了)。<br />
<!-- more -->
在项目里发现了一段没见过的函数写法,看着很奇怪:</p>
<pre><code>(function (win, doc, c) {
function login(options) {
// JavaScript code
}
win.cpdailyLogin = login
})(window, document);
</code></pre>
<p>查了一下发现原来是立即调用的函数表达式(<s>学完就忘</s>),学的时候觉得这东西真的有人用吗,结果工作了发现还真的有人用,借此机会查阅了一些资料,顺便记录一下。</p>
<p>立即调用的函数表达式(IIFE) 其实也算是 JavaScript 的特色之一了,这么写的好处就在于不需要设置变量名,不用污染全局变量,而且在 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。</p>
<p>根据 JavaScript 的语法,在函数名称后面跟一对圆括号<code>()</code>表示直接调用该函数,这个是学过 JS 的人都知道的,但是有时候我们需要在定义一个函数后立即对这个函数进行调用,例如:</p>
<pre><code>function () {
// some code
} ()
// SyntaxError: Unexpected token (
</code></pre>
<p>但是这种写法会报错,因为<code>function</code>这个关键字可以当作表达式和语句,上面这种写法就是语句,JavaScript 认为这是函数的定义,不应该在后面对函数进行调用,所以会报错。<br />
为了避免这种歧义,JavaScript 规定:如果<code>function</code>关键字出现在一行的开头,一律都解释为语句。那么这样事情就简单了很多,既然出现在行首会被认为是函数的定义,那么只要<code>function</code>关键字不出现在行首不就行了。于是乎就有了我所看到的写法:将函数定义用括号包起来,或者是将他们一起包起来,这样行首就不是<code>function</code>关键字了,而是括号,这样一来就解决了问题。</p>
<pre><code>// 方法 1
(function () {
// some code
})();
// 方法 2
(function () {
// some code
} () );
</code></pre>
<blockquote>
<p>这里的分号是一定需要的,否则第二行会被解析为第一行的参数,会产生错误。</p>
</blockquote>
<p>顺着这种思路,有很多种方法都可以实现,但原理和效果都是一样的。</p>
技术文档Moeyua
- 解决 nvm 无法在 arm 架构下安装 V15 以下的 node 版本 的问题https://blog.moeyua.com/posts/%E8%A7%A3%E5%86%B3-nvm-%E6%97%A0%E6%B3%95%E5%9C%A8-arm-%E6%9E%B6%E6%9E%84%E4%B8%8B%E5%AE%89%E8%A3%85-v15-%E4%BB%A5%E4%B8%8B%E7%9A%84-node-%E7%89%88%E6%9C%AC-%E7%9A%84%E9%97%AE%E9%A2%98/https://blog.moeyua.com/posts/%E8%A7%A3%E5%86%B3-nvm-%E6%97%A0%E6%B3%95%E5%9C%A8-arm-%E6%9E%B6%E6%9E%84%E4%B8%8B%E5%AE%89%E8%A3%85-v15-%E4%BB%A5%E4%B8%8B%E7%9A%84-node-%E7%89%88%E6%9C%AC-%E7%9A%84%E9%97%AE%E9%A2%98/Mon, 08 Nov 2021 18:08:02 GMT<p>迫于需要维护公司一个比较老的项目,所以在配置 macOS 环境的时候选择了使用 <code>nvm</code> 来管理多个 <code>node</code>,但是遇到了一些问题。
<!-- more -->
根据 nvm 官方文档的说法:</p>
<blockquote>
<p>January 2021: there are no pre-compiled NodeJS binaries for versions prior to 15.x for Apple's new M1 chip (arm64 architecture).</p>
</blockquote>
<p>也就是说 M1 芯片( arm64 )现在并没有对应的预编译版本,所以安装之后需要进行编译。而在编译过程中会遇到一些问题:</p>
<ul>
<li>编译成功,但是因为内存不足而崩溃( crashes ),增加足够的 node 内存后再次尝试但依然提示内存不足;</li>
<li>直接编译失败。</li>
</ul>
<p>这里我遇到的是第二种情况,也就是直接编译失败。那么如何解决这个问题呢, nvm 其实在文档里给出了一个方案,这个方案有两个前提:</p>
<ul>
<li>使用 <code>zsh</code></li>
<li>已经安装好 Rosetta 2
macOS 应该在 macOS X 上的默认终端就已经是 <code>zsh</code> 了,而 Rosetta 2 如果在你第一次打开因特尔架构的软件时就已经安装过了,如果没有安装过也可以手动进行安装:</li>
</ul>
<pre><code>softwareupdate --install-rosetta
</code></pre>
<p>以上两个条件都满足之后我们就可以处理这个问题了。</p>
<ol>
<li>首先检查自己的 <code>node</code> 架构,返回的结果应该是 <code>arm64</code>,这个是 M1 芯片的架构,也就是我们问题的<s>元凶</s>;</li>
</ol>
<pre><code>node -p process.arch
# arm64
</code></pre>
<ol>
<li>在 64 位 x86 架构下启动一个新的 zsh 进程;</li>
</ol>
<pre><code>arch -x86_64 zsh
</code></pre>
<ol>
<li>下载你需要的 node 版本,这个 node 将会是 x86 架构的;</li>
</ol>
<pre><code>nvm install node
</code></pre>
<ol>
<li>现在检查一下架构是否正确;</li>
</ol>
<pre><code>node -p process.arch
# x64
</code></pre>
<ol>
<li>退出这个进程。</li>
</ol>
<pre><code>exit
</code></pre>
<p>到这里我们就成功的安装好了一个低版本的 node。</p>
疑难杂症Moeyua
- m1 芯片安装 nvm 提示 command not foundhttps://blog.moeyua.com/posts/m1-%E8%8A%AF%E7%89%87%E5%AE%89%E8%A3%85-nvm-%E6%8F%90%E7%A4%BA-command-not-found/https://blog.moeyua.com/posts/m1-%E8%8A%AF%E7%89%87%E5%AE%89%E8%A3%85-nvm-%E6%8F%90%E7%A4%BA-command-not-found/Fri, 05 Nov 2021 23:13:53 GMT<p>最近新购入了一台 M1 的 MacBook Air,作为一个合格的程序员自然是先配置环境,但是没想到第一个安装的 nvm 上来就给了我当头一棒。</p>
<p><!--more--></p>
<p>首先根据 <a href="https://github.com/nvm-sh/nvm#manual-install">nvm</a>给出的文档下载:</p>
<pre><code>curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
</code></pre>
<p>下载完成,键入 <code>nvm</code> 测试一下是否成功,果不其然,报错:</p>
<pre><code>command not found: nvm
</code></pre>
<p>那就看一下是哪里的问题吧,查了一下,似乎是没有 <code>.bash_profile</code> 这个文件造成的。</p>
<p>那好办,创建一个就完了:</p>
<pre><code>touch ~/.bash_profile
</code></pre>
<p>创建完根据文档的说法是需要再运行一次安装命令,之后 <code>source ~./bash_profile</code> 让配置生效即可。</p>
<blockquote>
<p>If you use bash, the previous default shell, your system may not have a .bash_profile file where the command is set up. Create one with <code>touch ~/.bash_profile</code> and run the install script again. Then, run <code>source ~/.bash_profile</code> to pick up the nvm command.</p>
</blockquote>
<p>执行完上述操作后好像没什么问题了,那么试一下 <code>nvm</code>,ok 成功了。</p>
<hr />
<p>似乎问题到这里就结束了,但很遗憾并没有。</p>
<p>在几分钟之后,我再次打开 Terminal 准备安装 node 的时候,习惯性先输入 <code>nvm</code>,这时候怪事发生了:</p>
<pre><code>zsh: command not found: nvm
</code></pre>
<p>这是怎么回事呢,按照前面的再来一遍,正常了;重启 Terminal 输入 <code>nvm</code>, 报错。</p>
<p>试了好几次,都是一样的结果。这个时候就有点急了,这是怎么会事呢?翻来覆去查了好多资料也没结果,这个时候,文档上的一句话给了我提示:</p>
<blockquote>
<p>Since macOS 10.15, the default shell is zsh and nvm will look for <code>.zshrc</code> to update, none is installed by default. Create one with <code>touch ~/.zshrc</code> and run the install script again.
也就是说我现在使用的是 zsh 而不是 bash,所以 nvm 会寻找 <code>.zshrc</code>。那么这就好解决了:</p>
</blockquote>
<pre><code>touch ~/.zshrc
</code></pre>
<p>运行安装命令,<code>vim .zshrc</code> 查看一下文件确实有写入,输入 <code>nvm</code> 也正常,再次重启 Terminal,一切 ok。</p>
<hr />
<p>还是要仔细看文档,google 了一圈啥用没有,文档啥都写清楚了😅</p>
疑难杂症Moeyua
- 如何在 JavaScript 完美的确定一个数据的类型https://blog.moeyua.com/posts/%E5%A6%82%E4%BD%95%E5%9C%A8-javascript-%E5%AE%8C%E7%BE%8E%E7%9A%84%E7%A1%AE%E5%AE%9A%E4%B8%80%E4%B8%AA%E6%95%B0%E6%8D%AE%E7%9A%84%E7%B1%BB%E5%9E%8B/https://blog.moeyua.com/posts/%E5%A6%82%E4%BD%95%E5%9C%A8-javascript-%E5%AE%8C%E7%BE%8E%E7%9A%84%E7%A1%AE%E5%AE%9A%E4%B8%80%E4%B8%AA%E6%95%B0%E6%8D%AE%E7%9A%84%E7%B1%BB%E5%9E%8B/Wed, 03 Nov 2021 19:02:53 GMT<p>JavaScript 中有三种方式来确定一个数据的类型:</p>
<ul>
<li><code>typeof</code> 运算符</li>
<li><code>instanceof</code> 运算符</li>
<li><code>Object.prototype.toString()</code> 方法<br />
这里就来简单梳理一下这三种方式的优劣,同时得出一个能够完美判断数据类型的方法。
<!--more--></li>
</ul>
<h1>typeof</h1>
<p><code>typeof</code> 很简单,下面是一个简单的例子:</p>
<pre><code>let a = 'foo';
let b = 1;
let c = true;
typeof a // "string"
typeof b // "number"
typeof c // "boolean"
</code></pre>
<p>使用 <code>typeof</code> 判断『原始类型』(数值、字符串、布尔值)时分别返回 <code>number</code>、<code>string</code>、<code>boolean</code>。『合成类型』(对象、数组、函数)分别返回 <code>object</code>、<code>object</code>、<code>function</code></p>
<pre><code>let a = {foo = 'bar'};
let b = ['foo', 'bar'];
let c = function(foo) {
return foo + 'bar'
}
typeof a // "object"
typeof b // "object"
typeof c // "function"
</code></pre>
<p>而对于 <code>undefined</code> 和 <code>null</code> 这两种类型,<code>typeof</code> 能够正常判断 <code>undefined</code>,但是 <code>typeof null</code> 会返回 <code>object</code>。</p>
<pre><code>let a = undefined;
let b = null;
typeof a // "undefined"
typeof b // "object"
</code></pre>
<p>出现这种情况的原因时最初版的 JavaScript 并没有将 <code>null</code> 单独拿出来作为一个数据类型,只是作为 <code>object</code> 类型的一种,后来将 <code>null</code> 单独拿了出来,但是为了兼容以前的旧的代码,就没有改变 <code>typeof null</code> 的返回值。</p>
<p>可见 <code>typeof</code> 大部分情况下确实能够准确判断出数据类型,但是对于 <code>Array</code> 和 <code>null</code> 就会失效。</p>
<h1>instanceof</h1>
<p><code>instanceof</code> 运算符返回一个布尔值,表示对象是否为某个构造函数的实例。根据这一特点我们就能够使用该运算符判断数据类型。</p>
<pre><code>let a = {foo = 'bar'};
let b = ['foo', 'bar'];
let c = function(){};
a instanceof Object // true
b instanceof Array // true
c instanceof Function // true
</code></pre>
<p>如上面的代码所示,<code>instanceof</code> 能够区分对象的各种类型,但是需要注意的是,<code>instanceof</code> 只能判断对象类型,对于 <code>number</code>、<code>string</code>、<code>boolean</code>、<code>undefined</code>、<code>null</code> 这几种类型是无法判断的。</p>
<h1>Object.prototype.toString()</h1>
<p><code>Object.prototype.toString()</code> 方法的作用是返回一个对象的字符串形式,默认情况下返回类型字符串。不同数据类型的 <code>toString</code> 方法返回如下</p>
<table>
<thead>
<tr>
<th>数据类型</th>
<th>返回值</th>
</tr>
</thead>
<tbody>
<tr>
<td>数字</td>
<td><code>[object Number]</code></td>
</tr>
<tr>
<td>字符串</td>
<td><code>[object String]</code></td>
</tr>
<tr>
<td>布尔值</td>
<td><code>[object Boolean]</code></td>
</tr>
<tr>
<td>数组</td>
<td><code>[object Array]</code></td>
</tr>
<tr>
<td>函数</td>
<td><code>[object Function]</code></td>
</tr>
<tr>
<td>undefined</td>
<td><code>[object Undefined]</code></td>
</tr>
<tr>
<td>null</td>
<td><code>[object Null]</code></td>
</tr>
<tr>
<td>arguments 对象</td>
<td><code>[object Arguments]</code></td>
</tr>
<tr>
<td>Error 对象</td>
<td><code>[object Error]</code></td>
</tr>
<tr>
<td>Date 对象</td>
<td><code>[object Date]</code></td>
</tr>
<tr>
<td>RegExp 对象</td>
<td><code>[object RegExp]</code></td>
</tr>
<tr>
<td>其他对象</td>
<td><code>[object Object]</code></td>
</tr>
</tbody>
</table>
<p><strong>那么基于这一特性,我们可以近乎完美的确定一个常见数据的类型:</strong></p>
<pre><code>var type = function (o){
var s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
type({});// "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"
</code></pre>
<blockquote>
<p>需要注意的是,由于实例对象可能会改写 Object.prototype.toSring,所以我们直接使用 Object.prototype.toSring,使用函数的 Call 方法在任意值上调用这个方法。</p>
</blockquote>
<p>在上面这个type函数的基础上,还可以加上专门判断某种类型数据的方法。</p>
<pre><code>var type = function (o){
var s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
['Null',
'Undefined',
'Object',
'Array',
'String',
'Number',
'Boolean',
'Function',
'RegExp'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase();
};
});
type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true
</code></pre>
技术文档Moeyua
- Cookie?小饼干!https://blog.moeyua.com/posts/cookie-%E5%B0%8F%E9%A5%BC%E5%B9%B2/https://blog.moeyua.com/posts/cookie-%E5%B0%8F%E9%A5%BC%E5%B9%B2/Tue, 02 Nov 2021 15:09:53 GMT<p>虽然在浏览网页的时候经常看到 Cookie 这个词汇,但是好像几乎没有人知道是做什么的呢。
<!--more--></p>
<h2>概述</h2>
<p>Cookie,又称“小甜饼”,指某些网站为了辨别用户身份而储存在用户本地终端(通常为用户的浏览器)上的数据(通常经过加密)。</p>
<p>简单来讲,Cookie 是由服务器保存在用户浏览器上的一小块数据,而且每次都会和 HTTP 请求一起发送给服务器。通常 Cookie 的作用有大概三种:</p>
<ul>
<li>会话状态管理(用户登陆状态、购物车数据)</li>
<li>个性化设置(颜色、字体、字号等其他自定义设置)</li>
<li><strong>浏览器行为跟踪(跟踪并分析用户行为)</strong></li>
</ul>
<p>Cookie 这个名字应该源自一种叫 Fortune Cookie 的饼干,这种饼干里面包有写着一些有趣的句子的纸条。它这种内里包含有隐藏的信息的寓意被用在了计算机上。用户发送给服务器的每一次请求都携带有用户的一些信息,所以就用 Cookie 来指代这些很小的信息碎片。</p>
<p>由于 HTTP 请求是没有状态的,服务器无法知道用户在上一次请求时做了什么,甚至也不知道这个用户是谁,这个特性给用户带来了很糟糕的体验,也其实严重阻碍了 <a href="https://zh.wikipedia.org/wiki/%E4%BA%A4%E4%BA%92%E5%BC%8FWeb%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F">交互式Web应用程序</a> 的实现。Cookie 的作用就是在每次请求时携带一些必要的信息告诉服务器,这次请求的发送者是谁;同时因为 Cookie 保存在本地,有些数据直接从本地读取就可以,例如一些个性化设置。</p>
<p>Cookie 不是一种理想的客户端存储机制。它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端存储建议使用 Web storage API 和 IndexedDB。只有那些每次请求都需要让服务器知道的信息,才应该放在 Cookie 里面。</p>
<p>每个 Cookie 都有以下几方面的元数据。</p>
<ul>
<li>Cookie 的名字</li>
<li>Cookie 的值(真正的数据写在这里面)</li>
<li>到期时间(超过这个时间会失效)</li>
<li>所属域名(默认为当前域名)</li>
<li>生效的路径(默认为当前网址)</li>
</ul>
<p>我们以简单的用户登陆为例,用户登陆网站,浏览器向服务器发送请求,服务器回应的头信息告诉浏览器设置一个 Cookie,这个 Cookie 保存了服务端识别用户所需要的信息。之后用户的每一次请求都会将 Cookie 一同发送给服务器,然后服务器根据不同的用户返回相应的数据。但是 Cookie 并不会一直存在,在最开始设置的时候它的生效时间就以及定好了,所以在 Cookie 过期后,用户发现网站提示登陆失效,需要重新登陆,这时用户就需要重新登陆,而服务器会再次告诉浏览器设置一个 Cookie 以识别用户,直到它再次失效。以下是这个流程的一个示意图:<br />
<img src="https://i.loli.net/2021/11/01/ldUm56WesqynIVg.png" alt="" /></p>
<h2>创建和发送 Cookie</h2>
<p>服务器如果希望在浏览器保存 Cookie,就要在 HTTP 回应的头信息里面,放置一个或者 <code>Set-Cookie</code> 字段用来生成一个或多个 Cookie。<br />
除了 Cookie 的值,<code>Set-Cookie</code> 字段还可以附加多个 Cookie ,没有次序的要求。</p>
<pre><code>HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>;Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>;Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
</code></pre>
<h3>Cookie 的生命周期</h3>
<p>上文我们说过 Cookie 不会一直保存,所以在设置时需要定义 Cookie 的生命周期。Cookie 的生命周期分为两种:</p>
<ul>
<li>会话期 Cookie 不需要定义 <code>Expires</code> 和 <code>Max-Age</code> 在浏览器关闭时就会结束生命周期,所以会话期 Cookie 仅在会话期间有效。需要注意的是有些浏览器提供了会话恢复的功能,这将会导致 Cookie 在浏览器重新启动后依然存在,使得 Cookie 一直存在,这可能会导致一些问题。</li>
<li>持久性 Cookie 的生命周期取决于 <code>Expires</code> 或 <code>Max-Age</code> 所设定的时间。</li>
</ul>
<blockquote>
<p>如果您的站点对用户进行身份验证,则每当用户进行身份验证时,它都应重新生成并重新发送会话 Cookie,甚至是已经存在的会话 Cookie。此技术有助于防止会话固定攻击(session fixation attacks) (en-US),在该攻击中第三方可以重用用户的会话。</p>
</blockquote>
<p>如果服务器想改变一个早先设置的 Cookie,必须同时满足四个条件:Cookie 的 <code>key</code>、<code>domain</code>、<code>path</code> 和 <code>secure</code> 都匹配,否则都会修改失败,浏览器就会生成一个全新的 Cookie,而不是替换掉原来那个 Cookie。</p>
<h3>发送 Cookie</h3>
<p>浏览器在发送请求时候也会带上相应的 Cookie,下面是一个例子:</p>
<pre><code>GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
</code></pre>
<h2>Cookie 的属性</h2>
<h3>Expires, Max-Age</h3>
<p><code>Expires</code> 属性指定一个 UTC 格式的时间作为 Cookie 的过期时间,当时间超过以后 Cookie 就会被删除。但是需要注意的是该时间以本地时间为准,所以并不能保证一定会在指定的时间被删除。<br />
<code>Max-Age</code> 指定 Cookie 会在创建后的多少秒后被删除,倒计时结束后该 Cookie 就会被删除。</p>
<blockquote>
<p>需要注意的是 <code>Expires</code> 和 <code>Max-Age</code> 如果都没有设置或者有效的值的时候该 Cookie 就成为了 Session Cookie,在结束会话的时候就会被删除。<br />
如果 <code>Expires</code> 和 <code>Max-Age</code> 同时都被设置时以后者为准,也就是说 <code>Max-Age</code> 的优先级更高。</p>
</blockquote>
<h3>Domain, Path, SameSite</h3>
<p><code>Domain</code> 和 <code>Path</code> 定义了 Cookie 的作用域,也就是 Cookie 在哪些网站有效。
<code>Domain</code> 设置一个域名作为 Cookie 的作用域,如果没有设置则浏览器默认为当前所在的域名。</p>
<blockquote>
<p>需要特别注意的是,通过 <code>Domain</code> 设置的作用域是允许 Cookie 在子域名当中生效的,而通过浏览器默认设置是不允许子域名的。</p>
</blockquote>
<p>设置 <code>Domain</code> 时也需要遵守一些规则,假设当前为 <code>foo.bar.com</code>,只能设置当前域名以及上级域名,但不能直接设置为顶级域名(如 <code>.net</code>,<code>.com</code>)、子域名(如 <code>child.foo.bar.com</code>)、或者其他公共域名(如 <code>github.io</code>),正确的设置方法为 <code>foo.bar.com</code> 或者 <code>bar.com</code>。如果没有正确设置 <code>Domain</code> 浏览器会拒绝该 Cookie。</p>
<p><code>Path</code> 指定了 <code>Domain</code> 匹配到的域名下的哪些路径可以接受该 Cookie</p>
<p><code>SameSite</code> 要求 Cookie 在跨站请求的时候不会被发送,从而阻止跨站请求伪造攻击 <a href="https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0">CSRF</a>。</p>
<p>恶意网站会在诱导用户发送带有攻击目标网站请求的链接或者表单,如果用户浏览器中有目标网站的 Cookie,目标网站就会收到带有正确 Cookie 的请求。这种第三方网站引导而附带发送的 Cookie,就称为第三方 Cookie。<br />
除此之外就是网站通过 Cookie 跟踪用户,通过在第三方网站嵌入一些自己网站的请求,浏览器在加载页面时候就会发出带有用户 Cookie 的请求,这样就能够得知用户访问了哪些网站。</p>
<p><code>SameSite</code> 有三个值:</p>
<ul>
<li><code>None</code> 不做任何限制,第三方网站也可以向服务器发出带有 Cookie 的请求</li>
<li><code>Strict</code> 严格限制,任何第三方网站的请求都不可以携带该 Cookie,只在 <code>Domain</code> 规定的域名下的请求才可以携带该 Cookie</li>
<li><code>Lax</code> 和 <code>Strict</code> 相似,只有在用户是通过第三方网站的链接跳转过来的时候才会携带该 Cookie,为一些跨站子请求保留。具体规则可以参考下表:</li>
</ul>
<p><img src="https://i.loli.net/2021/11/01/tQzUX752Wln6akh.png" alt="" /></p>
<blockquote>
<p>以前,如果 <code>SameSite</code> 属性没有设置,或者没有得到运行浏览器的支持,那么它的行为等同于 <code>None</code>,Cookies 会被包含在任何请求中——包括跨站请求。</p>
<p>大多数主流浏览器正在将 <code>SameSite</code> 的<a href="https://www.chromestatus.com/feature/5088147346030592">默认值迁移至 Lax</a>。如果想要指定 Cookies 在同站、跨站请求都被发送,现在需要明确指定 <code>SameSite</code> 为 <code>None</code>。</p>
</blockquote>
<h3>Secure, HttpOnly</h3>
<p>有两种方法可以确保 Cookie 被安全发送,并且不会被意外的参与者或脚本访问:<code>Secure</code> 属性和 <code>HttpOnly</code> 属性。<br />
<code>Scure</code> 属性只允许 Cookie 通过 HTTPS 加密过后的请求,而不支持 HTTP 协议。但需要注意的是,即便如此也不应当通过 Cookie 发送任何敏感信息。</p>
<blockquote>
<p>从 Chrome 52 和 Firefox 52 开始,不安全的站点(http:)无法使用Cookie的 Secure 标记。</p>
</blockquote>
<p><code>HttpOnly</code> 规定 Cookie 不能通过 Javascript 的 <code>Document.cookie</code> API 等其他方式直接获取。这样可以防止恶意脚本的攻击。</p>
<h2>通过 JavaScript 访问和创建 Cookie</h2>
<p>JavaScript 提供了 <code>Document.cookie</code> 来访问当前 Cookie,当然前提是该 Cookie 没有设置 <code>HttpOnly</code> 属性。</p>
<p>该方法会返回一个字符串包含所有的Cookie,每条cookie以"分号和空格(; )"分隔(即 key=value 键值对),可以尝试使用 <code>String.Prototype.split</code> 手动将它们分离开来。</p>
<pre><code>const cookies = Document.cookie.split(';');
// type of cookies is Array;
</code></pre>
<p>该方法也可以用来创建一个 Cookie。<br />
和访问 Cookie 不同,需要注意的是,创建 Cookie 时一次只能创建一条,而且需要以 <strong>键值对</strong> 的形式创建,例如:</p>
<pre><code>document.cookie = "foo=bar;domain=example.com;path=/home;";
</code></pre>
<ul>
<li>path属性必须为绝对路径,默认为当前路径。</li>
<li>domain属性值必须是当前发送 Cookie 的域名的一部分。比如,当前域名是 <code>example.com</code>,就不能将其设为 <code>foo.com</code>。该属性默认为当前的一级域名(不含二级域名)。</li>
<li><code>max-age</code> 属性的值为秒数。</li>
<li><code>expires</code> 属性的值为 UTC 格式,可以使用 <code>Date.prototype.toUTCString()</code> 进行日期格式转换。</li>
<li>Cookie 的值字符串可以用 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent">encodeURIComponent()</a> 来保证它不包含任何逗号、分号或空格( Cookie 值中禁止使用这些值).</li>
</ul>
<h2>参考资料:</h2>
<ul>
<li><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies">HTTP Cookies - MDN</a></li>
<li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie">Document.cookie - MDN</a></li>
<li><a href="https://zh.wikipedia.org/wiki/Cookie">Cookie - Wikipedia</a></li>
<li><a href="https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0">跨站点攻击 - Wikipedia</a></li>
<li><a href="https://wangdoc.com/javascript/bom/Cookie.html">Cookie - 阮一峰</a></li>
<li><a href="http://fornote.blogspot.com/2009/02/CookiesCookies.html">Cookies 为什么叫 Cookies</a></li>
</ul>
技术文档Moeyua
- 使用 RSS 在推荐算法中获取主动权https://blog.moeyua.com/posts/%E4%BD%BF%E7%94%A8rss%E5%9C%A8%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E4%B8%AD%E8%8E%B7%E5%8F%96%E4%B8%BB%E5%8A%A8%E6%9D%83/https://blog.moeyua.com/posts/%E4%BD%BF%E7%94%A8rss%E5%9C%A8%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95%E4%B8%AD%E8%8E%B7%E5%8F%96%E4%B8%BB%E5%8A%A8%E6%9D%83/Sat, 30 Jan 2021 21:25:53 GMT<h2>前言</h2>
<p>当学习或者工作感到疲惫时,我们通常会放下手里的事情,拿起手机休息一下,这倒也无可厚非,但是今天我们似乎很难自己掌控休息的时间,往往拿起手机就被各种内容吸引,各类软件依靠着自己的推荐算法,总是能带来新鲜的信息,但是这些信息并不能带给我太多东西,比起零碎的信息还是系统性的的知识比较有用。</p>
<p><!--more--></p>
<p>为了让我们回归工作和学习,解决信息过载,获取接受信息的主动权,RSS 或许是一个很好的选择,我们可以通过 RSS 主动选择和调整订阅源,摆脱推荐算法,让获取信息变的简单。</p>
<p>鉴于熟练RSS需要一定的学习,这篇文章就想详细讲讲什么是 RSS ,如何使用 RSS 在网络中获取主动权,争取让小白也可以看懂。</p>
<h2>什么是RSS</h2>
<p>RSS 其实并不是什么新鲜的技术,相反 RSS 是在被成为 WEB1.0 时代就已经出现的技术,我们来看一下维基百科上面对 RSS 的定义:</p>
<blockquote>
<p>RSS(全称:RDF Site Summary;Really Simple Syndication),中文译作「简易信息聚合」,也称「聚合内容」,是一种消息来源 格式规范,<strong>用以聚合经常发布更新资料的网站</strong>,例如 博客 文章、新闻、音频 或 视频 的网摘。RSS 文件(或称做摘要、网络摘要、或频更新,提供到频道)包含全文或是节录的文字,再加上发布者所订阅之网摘资料和授权的元数据。简单来说,RSS 能够让用户订阅个人网站个人博客,当订阅的网站有新文章是能够获得通知。</p>
</blockquote>
<p>Really Simple Syndication「简易信息聚合」,听名字就能知道这是一个十分简单的技术,简单来说 RSS最主要的目的就是给个人网站和博客提供信息聚合,并通知所有订阅的阅读者,使信息能够更高效的传播。</p>
<h2>RSS 能做什么</h2>
<ul>
<li>
<p>可以看到没有广告和图片的标题或文章的摘要,这样你不必阅读全文即可知文章讲的一个意思是什么,方便确定是否要阅读本文。</p>
</li>
<li>
<p>RSS 阅读器会自动更新你定制的网站内容,保持新闻的及时性。这样每天你就可以在固定时间打开 RSS 阅读最新文章,而不必打开各个软件和网站,也不会被其他消息所干扰。</p>
</li>
<li>
<p>使用 RSS 可以根据你自已的喜好定制多个 RSS 订阅源,这样做的好处是从多个网站来源搜集,然后整合到单个数据流当中。</p>
</li>
<li>
<p>可以在 RSS 中的路由参数中选择订阅网站的某个栏目,比如知乎热榜,微博热搜,而不需要订阅整个网站。</p>
</li>
<li>
<p>某些阅读器甚至提供了过滤功能,用户只需要提供关键词,阅读器就会自动过滤掉相应内容,极大的简化了我们的信息流</p>
</li>
</ul>
<h2>RSS怎么使用</h2>
<p>RSS的使用方式实际上很简单,找到一个订阅源,然后添加到RSS阅读器当中刷新就能完成订阅,但实际情况却比这个麻烦很多————因为你并没有订阅源。</p>
<p>要想知道一个网站是否支持RSS,最直接的方法就是看网站的底部或侧边栏是否有 RSS 图标。一般来说,图标所指向的地址就是该网站的订阅链接,你可以直接点击 RSS 订阅链接跳转到 RSS 客户端内进行订阅,也可以复制粘贴按钮中的地址到自己在用的 RSS 服务中订阅这些网站中的内容。</p>
<p>以我的 <a href="https://moeyua.github.io/">博客</a> 为例,上方导航栏的 RSS 即为订阅链接,复制或点击即可进行订阅,移动端可以长按图标复制订阅源。</p>
<p><img src="https://i.loli.net/2021/01/30/MtbJOLgoknjVC7T.png" alt="" /><br />
<img src="https://i.loli.net/2021/01/30/vyTlD32WhuE5Gr7.png" alt="" /></p>
<p><strong>是复制链接而不是链接里的内容!!</strong></p>
<h2>RSSHub</h2>
<p>现在我们已经知道如何添加订阅源了,但是你会发现很多主流网站像微博,哔哩哔哩,知乎等并没有支持 RSS,那么这时候就需要介绍一个开源项目————<a href="https://docs.rsshub.app/"><strong>RSSHub</strong></a></p>
<p><img src="https://i.loli.net/2021/01/30/LoV4s7jxdw2IGmk.png" alt="" /></p>
<blockquote>
<p>RSSHub 是一个开源、简单易用、易于扩展的 RSS 生成器,可以给任何奇奇怪怪的内容生成 RSS 订阅源。RSSHub 借助于开源社区的力量快速发展中,目前已适配数百家网站的上千项内容</p>
</blockquote>
<p>以上内容来自RSSHub的官网介绍,就像官网所说的一样,通过RSSHub确实能够做到 <strong>万物皆可 RSS</strong> ,你可以通过 <a href="https://docs.rsshub.app/">https://docs.rsshub.app/</a> 来访问他们的官网,里面由很详细的文档以及许多RSS路由,这些RSS几乎涵盖了生活中涉及到的所有方面,所以我们就不需要自己去各个网站寻找RSS图标了</p>
<hr />
<p>接下来我就以微博为例展示如何使用RSSHub</p>
<p><img src="https://i.loli.net/2021/01/30/tuD6k5K8GIhsz9n.png" alt="" /></p>
<p>注意路由部分<br />
这里的路由中没有带”:” 表示为固定内容,不需要更改,而带有冒号的表示为参数,需要使用者自行配置。这里有 uid 和 routeParams 两个参数,其中第一个参数为必选参数,第二个参数为可选择参数,后者可以根据个人需求进行配置,这里只展示第一个参数 uid:</p>
<ol>
<li>首先我们打开博主主页,按下F12启动控制台</li>
<li>切换到控制台( console )选项卡</li>
<li>在一堆提示最下面执行 <code>$CONFIG.oid</code></li>
<li>得到的数字就是我们需要的参数 uid</li>
<li>用我们得到的 id 替换路由中的 <code>:uid</code></li>
</ol>
<p>这样我们就能够得到博主“游点艺术”微博的RSS<br />
<code>https://rsshub.app/weibo/user/2040839563</code></p>
<p><img src="https://i.loli.net/2021/01/30/lIYBDeEFxcAm6Ny.png" alt="" /></p>
<p>接下来只需要复制这个订阅源,到阅读器中订阅,我们就可以在阅读器中接收到博主的微博了。</p>
<p>RSSHub 还支持自建路由和浏览器插件,感兴趣的可以参考官方文档。</p>
<h2>RSS 阅读器</h2>
<p>RSS 阅读器我个人使用过 Inoreader 以及 ios 端的 Reeder4,目前在使用的是 Reeder4。</p>
<h3>Reeder4</h3>
<p>官网:<a href="https://reederapp.com/">https://reederapp.com/</a></p>
<p><strong>不支持中文界面</strong></p>
<p>Reeder 是 iOS 和 Mac 的老牌阅读器了,说是最好的阅读器也不过分,它除了可以让你手动加入 RSS 频道外,也可以从 Feedbin,Feedly,Inoreader,The Old Reader 等 RSS 阅读平台导入数据,程序本身支持 iCloud Reader 稍后,Pocket,Instapaper 等稍后阅读服务。在 iOS 平台手势操作也是一大亮点,标题左划 Star 文章,右划标记为已读,文章页左划查看源网站,几乎所有操作都可以用手势完成。</p>
<h3>Inoreader</h3>
<p>官网:https://www.inoreader.com/</p>
<p><strong>支持中文界面</strong></p>
<p>就功能而言 Inoreader 更加丰富,直观的界面设计,高级模式甚至支持去重功能,避免当热点新闻发生时避免被大量内容重复的文章刷屏,也支持关键词过滤,增强订阅源等等。免费版的功能也足够非重度用户使用,且支持 Win,Android,iOS,Mac 以及浏览器使用。推荐新手入门使用</p>
<h2>推荐订阅源</h2>
<p>https://moeyua.notion.site/RSS-1eda6578a8cf474eb9d0634821119334</p>
<p>这里是我个人使用的部分订阅源,是当作笔记写在Notion上的,因为并没有什么内容,暂时不打算搬到博客上来,有需要可以自行查看。</p>
<p>参考资料</p>
<blockquote>
<ol>
<li><a href="https://sspai.com/post/43998">论 RSS 的「复兴」</a></li>
<li><a href="https://zh.wikipedia.org/zh/RSS">RSS-Wikipedia</a></li>
<li><a href="https://www.cnki.net/KCMS/detail/detail.aspx?QueryID=4&CurRec=3&recid=&filename=QBKX200906015&dbname=CJFD2009&dbcode=CJFQ&pr=&urlid=&yx=&uid=WEEvREcwSlJHSldTTGJhYlRqaHdoRU9XSkZ2UlNsYkZkWnlDM3AzTGEzbWdvYkcyQi82Q0pSVFFoUVN1N1crUHBTRT0=A4hF_YAuvQ5obgVAqNKPCYcEjKensW4IQMovwHtwkF4VYPoHbKxJw!!&v=MDU3MDVyRzRIdGpNcVk5RVlZUjhlWDFMdXhZUzdEaDFUM3FUcldNMUZyQ1VSTDZlWmVackZDdm1VYnJCTkMvQWQ=">Web 源与内容聚合: RSS/Atom 的扩展、生成、发布、发现与共享</a></li>
<li><a href="https://docs.rsshub.app/">RSSHub</a></li>
<li><a href="https://sspai.com/post/56391">高效获取信息,你需要这份 RSS 入门指南</a></li>
</ol>
</blockquote>
随便写点Moeyua