<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>chrome-extension on Reorx’s Forge</title><link>https://reorx.com/tags/chrome-extension/</link><description>Recent content in chrome-extension on Reorx’s Forge</description><image><url>https://reorx.com/images/forge-v2-compat.svg</url><link>https://reorx.com/images/forge-v2-compat.svg</link></image><generator>Hugo -- gohugo.io</generator><lastBuildDate>Tue, 04 Apr 2023 11:46:57 +0800</lastBuildDate><atom:link href="https://reorx.com/tags/chrome-extension/feed.xml" rel="self" type="application/rss+xml"/><item><title>Window Opener for Chrome</title><link>https://reorx.com/makers-daily/007-window-opener-for-chrome/</link><pubDate>Tue, 04 Apr 2023 11:46:57 +0800</pubDate><guid>https://reorx.com/makers-daily/007-window-opener-for-chrome/</guid><description>做了一个以特定大小和位置的窗口打开网页的扩展，记录一些想法和开发过程</description><content:encoded><![CDATA[<p>最近又做了一个新的扩展——<a href="https://github.com/reorx/window-opener">Window Opener</a>，这篇文章介绍它的动机、开发过程和用法说明。</p>
<p class="standout">
  <a href="https://chrome.google.com/webstore/detail/window-opener/iiliegedaegmcdoflhojnpgagjekkhbl">Window Opener - Chrome Web Store</a>
</p>

<h2 id="动机">动机</h2>
<p>我平时主屏的窗口布局一般是 Chrome 占 3/5 靠左，VSCode 占 1/2 靠右，交叠的部分一般不会影响两边的浏览。
最近关于 AI 的新闻几乎都从 Twitter 上获取，我很希望它以一个单独的窗口出现在主窗口的右侧，这样我在打开来自 Twitter 的链接时仍然可以继续向下滚动，得到更好的浏览体验。
我在 Moom <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> 上添加了让窗口以 1/5 的屏幕大小靠 Chrome 右侧的布局，但仍然觉得很麻烦，因为我在专注工作时会关闭 Twitter，而每次打开时，都要走一遍 <code>[打开新窗口] → [输入 twit 回车] → [快捷键唤出 Moom] → [快捷键应用布局]</code> 的流程。于是我便想，要是能够一键把 Twitter 在当前窗口的侧边以特定大小打开就好了，既然没有这样的工具，何不自己做一个呢？</p>
<h2 id="开发过程">开发过程</h2>
<blockquote>
<p>如果你对此不感兴趣，可以直接跳到下一个章节查看插件的功能和用法介绍</p>
</blockquote>
<p>说干就干，我从自己的 <a href="https://github.com/reorx/webpack-chrome-boilerplate">webpack-chrome-boilerplate</a> 脚手架中复制了 <code>vanilla-ext</code> 到新的项目，为它取了一个简单直接的名字 <code>window-opener</code>. 我的脚手架的 tech stack 为 TypeScript + Webpack，其中内置了一些常用的库比如用于 DOM 操作的 cash-dom 和用于记录日志的 loglevel，不过最重要的一个包是 <a href="https://www.npmjs.com/package/@reorx/webpack-ext-reloader">@reorx/webpack-ext-reloader</a>, 这是我维护的用于自动重载扩展的工具，能够减少开发时每次保存就要手动点击 reload extension 的心智负担。</p>
<p>为了快速实现一个 demo，我首先想到的是让扩展的图标点击就可以打开 Twitter。我在 manifest.json 里添加了下面的配置</p>
<pre tabindex="0"><code>  &#34;action&#34;: {
    &#34;default_title&#34;: &#34;Open a window&#34;
  },
</code></pre><p>这使得当扩展的图标被点击时可以触发一个事件，从而执行打开新窗口的操作。以下是 <code>background.ts</code> 的代码:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">chrome</span><span class="p">.</span><span class="nx">action</span><span class="p">.</span><span class="nx">onClicked</span><span class="p">.</span><span class="nx">addListener</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nb">window</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">windows</span><span class="p">.</span><span class="nx">getCurrent</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">windowWidth</span>: <span class="kt">window.width</span> <span class="o">??</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">windowHeight</span>: <span class="kt">window.height</span> <span class="o">??</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">screenWidth</span>: <span class="kt">2560</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">screenHeight</span>: <span class="kt">1440</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">xOffset</span>: <span class="kt">58</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">windowArgs</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">left</span>: <span class="kt">context.windowWidth</span> <span class="o">+</span> <span class="nx">context</span><span class="p">.</span><span class="nx">xOffset</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">top</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">width</span>: <span class="kt">context.screenWidth</span> <span class="o">-</span> <span class="nx">context</span><span class="p">.</span><span class="nx">windowWidth</span> <span class="o">-</span> <span class="nx">context</span><span class="p">.</span><span class="nx">xOffset</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">height</span>: <span class="kt">context.screenHeight</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">chrome</span><span class="p">.</span><span class="nx">windows</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">url</span><span class="o">:</span> <span class="s1">&#39;https://twitter.com&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">focused</span>: <span class="kt">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">...</span><span class="nx">windowArgs</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></div><p>一个简单的 Proof-of-Concept 便完成了，点击扩展，便会在当前窗口右侧打开高度和屏幕一致、宽度占满剩余空间的 Twitter 窗口。这里用到的最核心的 API 是 chrome.windows <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>，实现了当前窗口大小的获取，和新窗口的大小、位置的控制。为了计算出相对于屏幕的空间，我将自己所用屏幕的大小赋值给了 <code>screenWidth</code> 和 <code>screenHeight</code>, 但这样做不具备通用性，我希望动态获取当前窗口所在屏幕的大小。讽刺的是，Chrome 扩展的 API 竟然无法实现<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>，经过各种尝试，最终我通过在扩展的设置页获取 <code>window.screen</code> 对象的方式得到了这些数值。(注意这里的 <code>window</code> 并非 <code>chrome.windows.Window</code>, 而是 DOM 的 <code>window</code>。)</p>
<p>核心功能完成后，我又为扩展增加了易于使用的界面。如果是非常简单的扩展 (比如 <a href="https://github.com/reorx/refgen">refgen</a>，未来会写篇单独的文章介绍), 我会直接使用原生的 DOM 接口来实现页面交互，但这次我感觉到编辑界面有一定的复杂度，于是将脚手架换为 <a href="https://github.com/reorx/webpack-chrome-boilerplate">webpack-chrome-boilerplate</a> 中的 <code>react-ext</code>，用 React 来增加代码的模块化和可维护性。</p>
<p>一直以来我一直都习惯用 Vanilla JS 来调用 Chrome 扩展接口，但引入 React 后不得不考虑状态管理，于是我找到了 <a href="https://github.com/onikienko/use-chrome-storage">use-chrome-storage</a>，它能够以 hooks 的方式获取和保存扩展数据，使我免于用 <code>useEffect</code> 重新实现。下面是代码示例:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="cm">/* define settings store hook */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">interface</span> <span class="nx">Settings</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">iconAction</span>: <span class="kt">IconAction</span>
</span></span><span class="line"><span class="cl">  <span class="nx">windows</span>: <span class="kt">WindowData</span><span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">INITIAL_SETTINGS</span>: <span class="kt">Settings</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">iconAction</span>: <span class="kt">IconAction.defaultWindow</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">windows</span><span class="o">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">useSettingsStore</span> <span class="o">=</span> <span class="nx">createChromeStorageStateHookSync</span><span class="p">(</span><span class="nx">STORAGE_KEY</span><span class="p">,</span> <span class="nx">INITIAL_SETTINGS</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* use settings store hook */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">Popup</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">[</span><span class="nx">settings</span><span class="p">,</span> <span class="nx">setSettings</span><span class="p">,</span> <span class="nx">isPersistent</span><span class="p">,</span> <span class="nx">error</span><span class="p">,</span> <span class="nx">isInitialStateResolved</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useSettingsStore</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isInitialStateResolved</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span><span class="nx">loading</span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">...</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>在 Options 页面中，我实现了一个窗口管理器组件 <code>WindowsManager</code>，它会循环渲染所有窗口的编辑界面，而每个窗口都需要用到 <code>chrome.windows.Window</code> 来计算。我不希望每个窗口都调用一次 <code>chrome.windows.getCurrent</code>，便想在整个页面初始化时获取 Window 对象，向下传递给子组件来使用。如果传递的层级很深，React 推荐的方式是使用 <a href="https://react.dev/reference/react/useContext">useContext</a><sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>，但我觉得比较麻烦，而且不够灵活，于是引入了 <a href="https://github.com/pmndrs/zustand">zustand</a> 来做全局状态的同步。下面是代码示例:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="cm">/* define app store hook */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">interface</span> <span class="nx">AppStore</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">chromeWindow</span>: <span class="kt">chrome.windows.Window</span><span class="o">|</span><span class="kc">null</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">useStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">&lt;</span><span class="nt">AppStore</span><span class="p">&gt;()((</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">chromeWindow</span>: <span class="kt">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* use app store hook */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// options.tsx: update chromeWindow
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">chrome</span><span class="p">.</span><span class="nx">windows</span><span class="p">.</span><span class="nx">getCurrent</span><span class="p">().</span><span class="nx">then</span><span class="p">(</span><span class="nb">window</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">useStore</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">chromeWindow</span>: <span class="kt">window</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// WindowManager.tsx: get chromeWindow
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">WindowItem</span> <span class="o">=</span> <span class="p">({</span><span class="nx">data</span><span class="p">,</span> <span class="nx">defaultId</span><span class="p">,</span> <span class="nx">onDataChanged</span><span class="p">,</span> <span class="nx">onDelete</span><span class="p">}</span><span class="o">:</span> <span class="nx">WindowItemProps</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">chromeWindow</span> <span class="o">=</span> <span class="nx">useStore</span><span class="p">(</span><span class="nx">state</span> <span class="o">=&gt;</span> <span class="nx">state</span><span class="p">.</span><span class="nx">chromeWindow</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">getContext</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">staticContext</span><span class="p">,</span> <span class="nx">chromeWindow</span><span class="o">!</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>以上是一些开发中的心得和收获，如果你有更多兴趣，可以直接阅读<a href="https://github.com/reorx/window-opener">源码</a>。还有一些技巧不再赘述，以下是一个简单的列举:</p>
<ul>
<li>通过 <code>chrome.action.setPopup</code> 实现切换点击扩展按钮的行为（显示 popup 或触发 action click 事件) → <a href="https://github.com/reorx/window-opener/blob/14e2cc3bbc880d27f3f0fa36e1dc6d11da5819fc/src/utils/action.ts#L4-L16">code-0</a>, <a href="https://github.com/reorx/window-opener/blob/14e2cc3bbc880d27f3f0fa36e1dc6d11da5819fc/src/background.ts#L20-L23">code-1</a></li>
<li>通过 <code>chrome.windows.onBoundsChanged</code> 监听窗口大小的改变，并控制事件的发生间隔 → <a href="https://github.com/reorx/window-opener/blob/14e2cc3bbc880d27f3f0fa36e1dc6d11da5819fc/src/options.tsx#L23-L36">code</a></li>
<li>使用 <a href="https://github.com/silentmatt/expr-eval">expr-eval</a> 进行数学表达式计算，避免使用 Chrome 扩展所不支持的 <code>eval</code> → <a href="https://github.com/reorx/window-opener/blob/14e2cc3bbc880d27f3f0fa36e1dc6d11da5819fc/src/window.ts#L154">code</a></li>
<li>使用 data url 创建一个临时的窗口来显示错误信息 → <a href="https://github.com/reorx/window-opener/blob/14e2cc3bbc880d27f3f0fa36e1dc6d11da5819fc/src/window.ts#L37-L45">code</a></li>
<li>通过 <code>key</code> 属性值的变化使得设置了 <code>defaultValue</code> 的 input 元素在 rerender 时仍可以改变数值 → <a href="https://github.com/reorx/window-opener/blob/14e2cc3bbc880d27f3f0fa36e1dc6d11da5819fc/src/WindowsManager.tsx#L289-L291">code</a></li>
</ul>
<h2 id="用法说明">用法说明</h2>
<h3 id="options">Options</h3>
<p>在安装了 Window Opener 之后，首次点击扩展按钮，会打开设置页面:</p>
<p><img loading="lazy" src="/makers-daily/007-window-opener-for-chrome/images/init-settings.png" type="" alt=""  /></p>
<ul>
<li><strong>Icon action</strong>: 点击扩展按钮的行为，有两种模式，Open Default Window 会直接打开默认的窗口，Open Windows List 会打开窗口列表供选择。</li>
<li><strong>Windows</strong>: 用户自定义的窗口，在这里进行添加、修改和删除</li>
<li><strong>Backup and restore</strong>: 导出和导入扩展配置。点击 Export 会直接下载 JSON 格式的配置文件。若要导入，请先点击 Choose File 选择文件，再点击 Import 完成导入。</li>
</ul>
<p>点击 Create 按钮，开始创建第一个窗口。下面的截图是我定义的用于满足最初需求的 Twitter 窗口。</p>
<p><img loading="lazy" src="/makers-daily/007-window-opener-for-chrome/images/window-twit.png" type="" alt=""  /></p>
<p>参数说明如下:</p>
<ul>
<li>Name: 窗口名称</li>
<li>URL: 窗口链接</li>
<li>Type: 窗口类型，normal 是平时使用的正常窗口，popup 仅有边框，没有地址栏和扩展按钮</li>
<li>Focused: 是否在打开后聚焦到该窗口</li>
<li>Default: 是否为默认窗口。仅能设置一个，需要先取消勾选才能更改为其他窗口。</li>
<li>left: 窗口到屏幕左边的像素距离</li>
<li>top: 窗口到屏幕顶部的像素距离</li>
<li>width: 窗口的宽度</li>
<li>height: 窗口的高度</li>
<li>Context: 用于参与 left, top, width, height 表达式计算的变量
<ul>
<li>windowWidth: 当前窗口宽度，动态数值</li>
<li>windowHeight: 当前窗口高度，动态数值</li>
<li>screenWidth: 屏幕宽度。静态数值，与窗口绑定（以下3个变量与此相同）</li>
<li>screenHeight: 屏幕高度</li>
<li>xOffset: 屏幕X轴的不可用宽度，比如 MacOS 的 Dock 放在屏幕左侧就会使得一部分空间对于窗口来说是不可用的。</li>
<li>yOffset: 屏幕Y周的不可用高度，比如 MacOS 的 menubar。</li>
</ul>
</li>
</ul>
<p>这里我希望 Twitter 在当前窗口的右侧，而我的屏幕将 Dock 放在左侧，因此新窗口距离屏幕左侧的距离 <code>left</code> 应该是 xOffset + windowWidth；与屏幕顶部的距离 <code>top</code> 可以简单设置为 0，Chrome 会考虑 menubar 所占用的空间，自动将窗口下移，也可以像我这样精确设置为 yOffset。宽度 <code>width</code> 要填满右侧可用空间，因此是 screenWidth - (windowWidth + xOffset)；高度 <code>height</code> 则可以直接使用 screenHeight，与 <code>top</code> 同理，超出可用长度的部分会被自动处理，也可以填为精确计算的数值 screenHeight - yOffset。</p>
<blockquote>
<p>Tips: 要实现一个宽 600px, 高 500px 的居中窗口，请参考以下参数</p>
<p><code>width=600, height=500, left=(screenWidth - 600) / 2, top=(screenHeight - 500) / 2</code></p>
</blockquote>
<h3 id="popup">Popup</h3>
<p>当 Icon action 设置为 Open Windows List 时，就可以打开 popup，界面如下:</p>
<p><img loading="lazy" src="/makers-daily/007-window-opener-for-chrome/images/popup.png" type="" alt=""  /></p>
<p>蓝色 ★ 表示默认窗口，鼠标点击窗口条目即可打开。</p>
<p>下方 Settings 是设置页的链接。点击 Create from current window 会基于当前窗口的 URL, left, top, width height 创建新的窗口。</p>
<blockquote>
<p>目前 Popup 存在 accessibility 上的问题，应该使所有按钮可以通过 tab 键切换 focus，实现仅用键盘导航和打开窗口。</p>
</blockquote>
<h3 id="keyboard-shortcut">Keyboard shortcut</h3>
<p>Window Opener 默认的快捷键是 <kbd>⌘ ⌃ T</kbd>, 也可以在 <code>chrome://extensions/shortcuts</code> 进行自定义。</p>
<h2 id="结语">结语</h2>
<p>开发 Chrome 扩展越来越成为我的一大爱好。浏览器是我们在赛博世界赖以生存的基本工具，能让它变得更好用，意味着我可以用更短的时间做更多的事，并享受更好的体验。开一个新的 side project，可以让我短暂离开主线任务和生活中的琐事，专注在具体明确的目标上，不仅是精神上的放松，也是对开发技术的淬炼。</p>
<p>希望你能喜欢 Window Opener :)</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Moom 是我使用多年的窗口管理工具 <a href="https://manytricks.com/moom/">https://manytricks.com/moom/</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://developer.chrome.com/docs/extensions/reference/windows/">https://developer.chrome.com/docs/extensions/reference/windows/</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://developer.chrome.com/docs/extensions/reference/system_display/#type-DisplayUnitInfo">chrome.system.display</a> 可以获得所有屏幕的数据，但无法知道当前窗口所在的是哪一个屏幕，而 Window Opener 需要在 background 中运行，此时是无法使用 DOM 的 <code>window.screen</code> 对象的，因此最终将 <code>screenWidth</code>, <code>screenHeight</code> 设计成了绑定在每个用户添加的 window 上的静态数值，但可以在编辑界面动态更新。&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://react.dev/learn/passing-data-deeply-with-context">Passing Data Deeply with Context</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item></channel></rss>