<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>林晓lx的个人Blog</title>
  
  <subtitle>登高现红日，实践出真知！</subtitle>
  <link href="https://blog.matoapp.net/atom.xml" rel="self"/>
  
  <link href="https://blog.matoapp.net/"/>
  <updated>2025-09-24T16:30:02.650Z</updated>
  <id>https://blog.matoapp.net/</id>
  
  <author>
    <name>林晓lx</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>[学习笔记]使用git rebase做分支差异化同步</title>
    <link href="https://blog.matoapp.net/posts/535a3e30/"/>
    <id>https://blog.matoapp.net/posts/535a3e30/</id>
    <published>2025-06-05T10:04:00.000Z</published>
    <updated>2025-09-24T16:30:02.650Z</updated>
    
    <content type="html"><![CDATA[<p>在一个.NET 项目中，使用了Volo.Abp库，但出于某种原因，需要源码调试，因此，使用源码方式集成的项目做了一个分支<code>archive-abp-source</code></p><p>其中引用方式变更操作的提交为：<code>7de53907</code></p><p>后续，在<code>master</code>分支中，又新增了若干个提交，现在的目标是：把 <code>master</code> 分支中后续的所有提交，除了 <code>7de53907...</code> 这个 commit，合并到<code>archive-abp-source</code>分支。</p><p>这在 Git 中不是一个“标准的 merge”操作（Git merge 默认会把所有 commit 都合并）。不过，可以通过以下步骤达成这个目的：</p><h3><span id="基于-master-创建临时分支">基于 master 创建临时分支</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git checkout master</span><br><span class="line">git checkout -b temp-merge</span><br></pre></td></tr></table></figure><h3><span id="使用交互式-rebase-删除指定提交">使用交互式 rebase 删除指定提交</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git rebase -i --rebase-merges &lt;common-base-commit&gt;</span><br></pre></td></tr></table></figure><p><code>common-base-commit</code> 是当前分支与 master 的共同祖先，比如用 <code>git merge-base master archive-abp-source</code> 找到。结果就是 <code>&lt;common-base-commit&gt;</code>，可用于 <code>rebase -i</code> 时指定起点。</p><p>如果你知道数量，也可以简单地 rebase 最近 20 次提交：</p><p><code>git rebase -i HEAD~20</code> </p><p>然后在打开的编辑器中把 <code>7de53907...</code> 那一行改为 <code>drop</code>，保存退出即可。</p><p><img src="/posts/535a3e30/644861-20250605100337923-1080905623.png"></p><h3><span id="切回目标分支执行合并">切回目标分支，执行合并</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git checkout archive-abp-source</span><br><span class="line">git merge temp-merge</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="可选删除临时分支">[可选]删除临时分支</span></h3><pre><code>git branch -d temp-merge</code></pre>]]></content>
    
    
    <summary type="html">文章介绍了如何在Git中将master分支中除特定提交外的其他提交合并到archive-abp-source分支，通过创建临时分支、使用交互式rebase删除指定提交，再合并回目标分支的操作步骤。</summary>
    
    
    
    <category term="DevOps" scheme="https://blog.matoapp.net/categories/DevOps/"/>
    
    
    <category term="git" scheme="https://blog.matoapp.net/tags/git/"/>
    
  </entry>
  
  <entry>
    <title>[学习笔记] 从零开始虚拟化搭建数据库服务器</title>
    <link href="https://blog.matoapp.net/posts/b60d95fe/"/>
    <id>https://blog.matoapp.net/posts/b60d95fe/</id>
    <published>2025-06-05T10:03:00.000Z</published>
    <updated>2025-09-24T16:30:02.634Z</updated>
    
    <content type="html"><![CDATA[<p>[toc]</p><h2><span id="一-创建虚拟机并安装centos-7">一. 创建虚拟机并安装CentOS 7</span></h2><p><img src="/posts/b60d95fe/644861-20250605095825389-1305414928.png"></p><p>硬件参数根据需求进行设置</p><p>CD&#x2F;DVD 源选择CentOS7的 ISO 镜像</p><p><img src="/posts/b60d95fe/644861-20250605095825332-217441518.png"></p><p>安装过程略..</p><h2><span id="二-安装vmware-tools">二. 安装VMware Tools</span></h2><p>等待安装结束，从菜单中选择安装VMware Tools，此时<br>CD&#x2F;DVD 源会自动挂载到虚拟机中。</p><p><img src="/posts/b60d95fe/644861-20250605095825333-574570396.png"></p><p>从虚拟机进入CentOS 7 系统 </p><p>运行，创建挂载目录</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo mkdir -p /mnt/cdrom</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>运行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">lsblk</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>查看CD&#x2F;DVD驱动器设备：</p><p><img src="/posts/b60d95fe/644861-20250605095825380-252923666.png"></p><p>运行命令，挂载CD&#x2F;DVD驱动器</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo mount /dev/sr0 /mnt/cdrom</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>执行结果如下，表明挂载成功</p><p><img src="/posts/b60d95fe/644861-20250605095825348-585812055.png"></p><p>进入挂载目录，解压VMware Tools安装包</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cp /mnt/cdrom/VMwareTools-*.tar.gz /tmp/</span><br><span class="line">cd /tmp</span><br><span class="line">tar -zxvf VMwareTools-*.tar.gz</span><br></pre></td></tr></table></figure><p>如果缺少编译工具和内核头文件，可以运行以下命令安装：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y gcc make perl kernel-devel kernel-headers</span><br></pre></td></tr></table></figure><p>进入解压后的目录，运行以下命令安装VMware Tools：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cd vmware-tools-distrib</span><br><span class="line">sudo ./vmware-install.pl</span><br></pre></td></tr></table></figure><p>执行一路回车即可，安装完成后会提示：</p><p><img src="/posts/b60d95fe/644861-20250605095825427-883297454.png"></p><p><img src="/posts/b60d95fe/644861-20250605095825369-1538718084.png"></p><p>安装成功结果：</p><p><img src="/posts/b60d95fe/644861-20250605095825416-57110023.png"></p><h2><span id="三-安装常用工具">三. 安装常用工具</span></h2><h3><span id="确保联网">确保联网</span></h3><p>当前没有任何网络工具，如何确定系统已经接入互联网？</p><p>以访问百度为例，运行以下命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">echo &gt; /dev/tcp/www.baidu.com/80 &amp;&amp; echo &quot;ok&quot; || echo &quot;not ok&quot;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>显示ok，则表明已经联网。</p><p><img src="/posts/b60d95fe/644861-20250605095825460-449015719.png"></p><p>因为CentOS7已停止维护，所以需要手动添加 repo 源。运行：</p><p><img src="/posts/b60d95fe/644861-20250605095825307-957052514.png"></p><p>安装网络工具</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">yum install -y wget curl net-tools vim epel-release</span><br></pre></td></tr></table></figure><h3><span id="配置网络">配置网络</span></h3><p>将虚拟机自动获取的IP地址改为静态IP地址。</p><p><img src="/posts/b60d95fe/644861-20250605095825406-2061108261.png"></p><p>开放ssh的22端口</p><p><img src="/posts/b60d95fe/644861-20250605095825306-1131144560.png"></p><p>打开sshd</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl start sshd</span><br><span class="line">sudo systemctl <span class="built_in">enable</span> sshd</span><br></pre></td></tr></table></figure><p>打开防火墙，运行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo firewall-cmd --permanent --add-service=ssh</span><br><span class="line">sudo firewall-cmd --reload</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>运行结结果：</p><p><img src="/posts/b60d95fe/644861-20250605095825352-1015731169.png"></p><p>此时可以通过ssh登录了，接下来可以安装SQL-Server、Redis等服务。</p><p><img src="/posts/b60d95fe/644861-20250605095825573-1178251796.png"></p><h2><span id="四-安装服务">四. 安装服务</span></h2><p>配置SELinux，打开码编辑器，并将SELINUX&#x3D;enforcing 改为 SELINUX&#x3D;permissive</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/selinux/config</span><br></pre></td></tr></table></figure><h3><span id="安装-sql-server-2017">安装 SQL Server 2017</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">sudo curl -o /etc/yum.repos.d/mssql-server.repo https://packages.microsoft.com/config/rhel/7/mssql-server-2017.repo</span><br><span class="line">sudo yum install -y mssql-server</span><br><span class="line">sudo /opt/mssql/bin/mssql-conf setup</span><br><span class="line">sudo firewall-cmd --zone=public --add-port=1433/tcp --permanent</span><br><span class="line">sudo firewall-cmd --reload</span><br><span class="line">systemctl status mssql-server</span><br><span class="line">systemctl enable mssql-server</span><br></pre></td></tr></table></figure><h3><span id="安装-openssl">安装 OpenSSL</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">wget https://www.openssl.org/source/openssl-1.1.1.tar.gz</span><br><span class="line">tar -xvf openssl-1.1.1.tar.gz</span><br><span class="line">cd openssl-1.1.1</span><br><span class="line">./config --prefix=/usr/local/openssl shared zlib</span><br><span class="line">make &amp;&amp; make install</span><br><span class="line">sudo ldconfig</span><br><span class="line">ln -s /usr/local/openssl/bin/openssl /usr/bin/openssl</span><br><span class="line">ln -s /usr/local/openssl/lib/libssl.so.1.1 /usr/lib64/libssl.so.1.1</span><br><span class="line">ln -s /usr/local/openssl/lib/libcrypto.so.1.1 /usr/lib64/libcrypto.so.1.1</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="安装-redis">安装 Redis</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf redis-7.4.0.tar.gz</span><br><span class="line">cd redis-7.4.0</span><br><span class="line">make &amp;&amp; make install</span><br><span class="line">ln -s /usr/local/bin/redis-server /usr/bin/redis-server</span><br><span class="line">cp redis.conf /etc/redis/</span><br><span class="line">sudo firewall-cmd --zone=public --add-port=6379/tcp --permanent</span><br><span class="line">sudo firewall-cmd --reload</span><br></pre></td></tr></table></figure><p>新建redis启动脚本</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /usr/lib/systemd/system/redis.service</span><br></pre></td></tr></table></figure><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Redis persistent key-value database</span><br><span class="line"><span class="attr">After</span>=network.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">LimitNOFILE</span>=<span class="number">10032</span></span><br><span class="line"><span class="attr">ExecStart</span>=/usr/bin/redis-server /etc/redis/redis.conf --supervised systemd</span><br><span class="line"><span class="attr">ExecStop</span>=/usr/libexec/redis-shutdown</span><br><span class="line"><span class="attr">Type</span>=simple</span><br><span class="line"><span class="attr">User</span>=redis</span><br><span class="line"><span class="attr">Group</span>=redis</span><br><span class="line"><span class="attr">RuntimeDirectory</span>=redis</span><br><span class="line"><span class="attr">RuntimeDirectoryMode</span>=<span class="number">0755</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>执行命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">systemctl daemon-reload</span><br><span class="line">systemctl start redis</span><br><span class="line">systemctl enable redis</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="安装-rabbitmq">安装 RabbitMQ</span></h3><p>安装Erlang</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">cd /tmp/</span><br><span class="line">curl -sfL -O https://github.com/rabbitmq/erlang-rpm/releases/download/v26.2.5.11/erlang-26.2.5.11-1.el7.x86_64.rpm</span><br><span class="line">yum install -y erlang-26.2.5.11-1.el7.x86_64.rpm</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>安装RabbitMQ</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">curl -sfL -O https://github.com/rabbitmq/rabbitmq-server/releases/download/v4.1.0/rabbitmq-server-4.1.0-1.el8.noarch.rpm</span><br><span class="line">yum install rabbitmq-server-4.1.0-1.el8.noarch.rpm</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>添加防火墙</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">sudo firewall-cmd --zone=public --add-port=5672/tcp --permanent</span><br><span class="line"></span><br><span class="line">sudo firewall-cmd --zone=public --add-port=25672/tcp --permanent</span><br><span class="line"></span><br><span class="line">sudo firewall-cmd --zone=public --add-port=4369/tcp --permanent</span><br></pre></td></tr></table></figure><p>配置外部访问的用户</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"># 1. 添加用户 sa，并设置密码（将 yourpassword 替换成你实际使用的密码）</span><br><span class="line">sudo rabbitmqctl add_user sa yourpassword</span><br><span class="line"></span><br><span class="line"># 2. 给用户 sa 设置为管理员角色</span><br><span class="line">sudo rabbitmqctl set_user_tags sa administrator</span><br><span class="line"></span><br><span class="line"># 3. 设置该用户对默认虚拟主机 &quot;/&quot; 拥有全部权限</span><br><span class="line">sudo rabbitmqctl set_permissions -p / sa &quot;.*&quot; &quot;.*&quot; &quot;.*&quot;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>启动RabbitMQ的Web管理插件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sudo rabbitmq-plugins list | grep management</span><br><span class="line">sudo rabbitmq-plugins enable rabbitmq_management</span><br><span class="line">sudo systemctl restart rabbitmq-server</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>运行结果：</p><p><img src="/posts/b60d95fe/644861-20250605095825493-1895793011.png"></p><p>访问RabbitMQ管理界面，默认地址为 <a href="http://127.0.0.1:15672，使用刚才创建的用户">http://127.0.0.1:15672，使用刚才创建的用户</a> sa 和密码登录。</p><p><img src="/posts/b60d95fe/644861-20250605095825441-1511958927.png"></p><p>..</p>]]></content>
    
    
    <summary type="html">文章介绍了在VMware中创建CentOS 7虚拟机、安装VMware Tools、配置网络、安装常用工具及SQL Server、OpenSSL、Redis和RabbitMQ等服务的详细步骤。</summary>
    
    
    
    <category term="DevOps" scheme="https://blog.matoapp.net/categories/DevOps/"/>
    
    <category term="Linux" scheme="https://blog.matoapp.net/categories/DevOps/Linux/"/>
    
    
    <category term="数据库" scheme="https://blog.matoapp.net/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="Linux" scheme="https://blog.matoapp.net/tags/Linux/"/>
    
    <category term="redis" scheme="https://blog.matoapp.net/tags/redis/"/>
    
  </entry>
  
  <entry>
    <title>开箱即用的.NET MAUI组件库 V-Control 发布了!</title>
    <link href="https://blog.matoapp.net/posts/a4f268b/"/>
    <id>https://blog.matoapp.net/posts/a4f268b/</id>
    <published>2025-02-06T18:04:00.000Z</published>
    <updated>2025-09-24T16:30:02.710Z</updated>
    
    <content type="html"><![CDATA[<p>之前写过挺多的MAUI Sample，其中有很多代码可以打包成组件，当组件完善到一定程度，我会把控件封装起来放到控件库中。</p><p>今天，在这个仓库建立一年零八个月后，我觉得可以考虑将其作为开源库发布。</p><p>有很多网友在观望.NET MAUI，犹豫是否可以拿来作为有生产力的跨平台移动端开发工具，此时我想告诉大家，.NET MAUI是一个非常不错的移动开发平台。配合 V-Control 来构建移动应用程序，V-Control是适用于<a href="https://dotnet.microsoft.com/zh-cn/apps/maui">.NET MAUI</a> 的组件库(Component)，它提供了一组开箱即用的 UI 控件，可快速搭建面向业务的应用程序界面。</p><p><img src="/posts/a4f268b/644861-20250208094035315-1245331087.png"></p><h2><span id="功能">功能</span></h2><ul><li><a href="https://vcontrol.matoapp.net/documents/v-button">VButton</a> - 按钮组件</li><li>VCard - 卡片组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-checkable-collection">VCheckableCollection</a> - 可勾选的集合组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-checkbox">VCheckBox</a> - 复选框组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-checkbox-button">VCheckBoxButton</a> - 复选框按钮组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-checkbox-group">VCheckBoxGroup</a> - 复选框集合组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-collection">VCollection</a> - 集合视图组件</li><li>VDateNativePicker - 原生封装的日期选择器组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-date-picker">VDatePicker</a> - 日期选择器组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-editor">VEditor</a> - 编辑器组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-expander">VExpander</a> - 展开收起组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-form-item">VFormItem</a> - 表单组件</li><li>VIndicator - 进度指示器组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-menu-cell">VMenuCell</a> - 菜单项组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-number-entry">VNumberEntry</a> - 数字输入框组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-picker">VPicker</a> - 选择器组件</li><li>VRadioButton - 单选框组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-radio-button-group">VRadioButtonGroup</a> - 单选框集合组件（切换栏组件）</li><li><a href="https://vcontrol.matoapp.net/documents/v-search-bar">VSearchBar</a> - 搜索栏组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-tag-picker">VTagPicker</a> - 标签选择器组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-time-line">VTimeLine</a> - 时间轴组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-top-app-bar">VTopAppBar</a> - 顶栏组件</li><li>VTouchContentView - 手势监听组件</li><li>VUploader - 文件上传组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-validating-entry">VValidatingEntry</a> - 带验证的输入框组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-validating-picker">VValidatingPicker</a> - 带验证的选择器组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-rich-text-editor">VRichTextEditor</a> - 富文本编辑器组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-entry">VEntry</a> - 输入框组件</li><li><a href="https://vcontrol.matoapp.net/documents/v-placeholder-view">VPlaceholderView</a> - 占位视图组件</li></ul><h2><span id="todo">Todo</span></h2><ul><li>VAutocomplete - 自动完成组件</li><li>VComparisonView - 比较视图组件</li><li>VCalendar - 日历组件</li><li>暗黑模式</li><li>基于 BlazorApp 的组件</li></ul><h2><span id="快速开始">快速开始</span></h2><ol><li>在你的.NET MAUI 项目中执行以下命令来安装 V-Control：</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet add package VControl</span><br></pre></td></tr></table></figure><p>或在NUGET中搜索”V-Control”并安装它。</p><ol start="2"><li>在 <code>MauiProgram</code> 使用<code>.UseVControl()</code>在MauiAppBuilder中添加V-Control的处理程序。</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> MauiApp <span class="title">CreateMauiApp</span>()</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">var</span> builder = MauiApp.CreateBuilder();</span><br><span class="line">    builder</span><br><span class="line">        .UseMauiApp&lt;App&gt;()</span><br><span class="line">        .UseVControl()   <span class="comment">//在这里添加V-Control的处理程序</span></span><br><span class="line">    <span class="keyword">var</span> mauiApp = builder.Build();</span><br><span class="line">    <span class="keyword">return</span> mauiApp;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>打开 <code>App.xaml</code> 文件， 在资源中添加<code>&lt;v:VControlTheme /&gt;</code>。</li></ol><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">&lt;?xml version = <span class="string">&quot;1.0&quot;</span> encoding = <span class="string">&quot;UTF-8&quot;</span> ?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">Application</span> <span class="attr">xmlns</span>=<span class="string">&quot;http://schemas.microsoft.com/dotnet/2021/maui&quot;</span></span></span><br><span class="line"><span class="tag">             <span class="attr">xmlns:x</span>=<span class="string">&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;</span></span></span><br><span class="line"><span class="tag">             <span class="attr">xmlns:v</span>=<span class="string">&quot;clr-namespace:VControl.Styles;assembly=VControl&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Application.Resources</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">v:VControlTheme</span> /&gt;</span></span><br><span class="line">        ...</span><br><span class="line">    <span class="tag">&lt;/<span class="name">Application.Resources</span>&gt;</span>   </span><br><span class="line"><span class="tag">&lt;/<span class="name">Application</span>&gt;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="文档">文档</span></h2><p>前往<a href="https://vcontrol.matoapp.net/documents/starter">V-Control Docs</a></p><h2><span id="源码和示例">源码和示例</span></h2><p>您可以前往<a href="https://github.com/jevonsflash/VControl">GitHub</a>查看V-Control的源码和示例。</p>]]></content>
    
    
    <summary type="html">文章介绍了V-Control组件库，这是一个为.NET MAUI设计的UI组件库，提供多种开箱即用的控件，方便快速开发移动应用。作者在仓库建立一年零八个月后决定将其开源，旨在展示.NET MAUI作为生产力工具的可行性，并提供了安装和使用方法。</summary>
    
    
    
    <category term=".NET MAUI" scheme="https://blog.matoapp.net/categories/NET-MAUI/"/>
    
    <category term="移动开发" scheme="https://blog.matoapp.net/categories/NET-MAUI/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="MAUI" scheme="https://blog.matoapp.net/tags/MAUI/"/>
    
    <category term=".net" scheme="https://blog.matoapp.net/tags/net/"/>
    
  </entry>
  
  <entry>
    <title>在VS Code中使用Snippet Craft扩展提高编码效率</title>
    <link href="https://blog.matoapp.net/posts/10673802/"/>
    <id>https://blog.matoapp.net/posts/10673802/</id>
    <published>2024-08-23T00:18:00.000Z</published>
    <updated>2025-09-24T16:30:02.666Z</updated>
    
    <content type="html"><![CDATA[<h1><span id="snippet-craft">Snippet Craft</span></h1><p>一个VS Code代码片段管理插件</p><h2><span id="功能">功能</span></h2><h3><span id="创建和插入代码片段">创建和插入代码片段</span></h3><p>在编辑器区域右键菜单中点击插入Snippet，或在代码片段视图中点击条目，则会将代码片段插入到当前激活文档的光标位置。</p><p><img src="/posts/10673802/644861-20240823001705716-1562665582.gif"></p><h3><span id="代码片段编辑">代码片段编辑</span></h3><p>代码片段在左侧栏中，根据创建时的文件内容类型，分组显示代码片段，可编辑已有的代码片段。</p><p><img src="/posts/10673802/644861-20240823001705611-1144375649.gif"></p><h3><span id="代码片段预览">代码片段预览</span></h3><p>鼠标移动到代码片段条目上时，显示tooltip预览</p><p><img src="/posts/10673802/644861-20240823001705577-1250694362.png"></p><h3><span id="默认映射">默认映射</span></h3><p>映射是插入代码片段时，自动替换的变量。</p><p>代码片段中通过设置占位符<code>$&#123;VAR_NAME&#125;</code>，在插入代码片段时，将自动替换为全局变量中的值。可用的映射如下表：</p><table><thead><tr><th><strong>类别</strong></th><th><strong>变量</strong></th><th><strong>描述</strong></th></tr></thead><tbody><tr><td><strong>文件和编辑器相关</strong></td><td>TM_SELECTED_TEXT</td><td>当前选定的文本或空字符串</td></tr><tr><td></td><td>TM_CURRENT_LINE</td><td>当前行的内容</td></tr><tr><td></td><td>TM_CURRENT_WORD</td><td>光标下的单词或空字符串的内容</td></tr><tr><td></td><td>TM_LINE_INDEX</td><td>基于零索引的行号</td></tr><tr><td></td><td>TM_LINE_NUMBER</td><td>基于一个索引的行号</td></tr><tr><td></td><td>TM_FILENAME</td><td>当前文档的文件名</td></tr><tr><td></td><td>TM_FILENAME_BASE</td><td>当前文档的文件名（不含扩展名）</td></tr><tr><td></td><td>TM_DIRECTORY</td><td>当前文档的目录</td></tr><tr><td></td><td>TM_FILEPATH</td><td>当前文档的完整文件路径</td></tr><tr><td></td><td>RELATIVE_FILEPATH</td><td>当前文档的相对文件路径（相对于打开的工作区或文件夹）</td></tr><tr><td></td><td>CLIPBOARD</td><td>剪贴板的内容</td></tr><tr><td></td><td>WORKSPACE_NAME</td><td>打开的工作区或文件夹的名称</td></tr><tr><td></td><td>WORKSPACE_FOLDER</td><td>打开的工作区或文件夹的路径</td></tr><tr><td></td><td>CURSOR_INDEX</td><td>基于零索引的游标编号</td></tr><tr><td></td><td>CURSOR_NUMBER</td><td>基于单索引的游标编号</td></tr><tr><td><strong>时间相关</strong></td><td>CURRENT_YEAR</td><td>本年度</td></tr><tr><td></td><td>CURRENT_YEAR_SHORT</td><td>当年的最后两位数字</td></tr><tr><td></td><td>CURRENT_MONTH</td><td>两位数字的月份（例如“02”）</td></tr><tr><td></td><td>CURRENT_MONTH_NAME</td><td>月份的全名（例如“July”）</td></tr><tr><td></td><td>CURRENT_MONTH_NAME_SHORT</td><td>月份的简短名称（例如“Jul”）</td></tr><tr><td></td><td>CURRENT_DATE</td><td>以两位数字表示的月份中的某一天（例如“08”）</td></tr><tr><td></td><td>CURRENT_DAY_NAME</td><td>日期的名称（例如“星期一”）</td></tr><tr><td></td><td>CURRENT_DAY_NAME_SHORT</td><td>当天的简短名称（例如“Mon”）</td></tr><tr><td></td><td>CURRENT_HOUR24</td><td>小时制格式的当前小时</td></tr><tr><td></td><td>CURRENT_MINUTE</td><td>两位数的当前分钟数</td></tr><tr><td></td><td>CURRENT_SECOND</td><td>当前秒数为两位数</td></tr><tr><td></td><td>CURRENT_SECONDS_UNIX</td><td>自 Unix 纪元以来的秒数</td></tr><tr><td></td><td>CURRENT_TIMEZONE_OFFSET</td><td>当前 UTC 时区偏移量为 +HH <br> 或者 -HH <br> (例如“-07:00”)</td></tr><tr><td><strong>其他</strong></td><td>RANDOM6</td><td>6 个随机 Base-10 数字</td></tr><tr><td></td><td>RANDOM_HEX6</td><td>6 个随机 Base-16 数字</td></tr><tr><td></td><td>UUID</td><td>第四版UUID</td></tr></tbody></table><p>注意：当自定义映射值未设置或者不可用时，将直接显示变量占位符</p><h3><span id="自定义映射">自定义映射</span></h3><p>扩展初始化时，插入了三个常用的自定义映射，你可以自由更改或添加自定义映射。</p><p><img src="/posts/10673802/644861-20240823001705563-2086809795.png"></p><p>示例：</p><p>代码片段内容</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">value of &#x27;AUTHOR&#x27; is: $&#123;AUTHOR&#125;</span><br><span class="line">value of &#x27;COMPANY&#x27; is: $&#123;COMPANY&#125;</span><br><span class="line">value of &#x27;MAIL&#x27; is: $&#123;MAIL&#125;</span><br><span class="line">value of &#x27;FOOBAR&#x27; (non-exist) is: $&#123;FOOBAR&#125;</span><br></pre></td></tr></table></figure><p>插入代码片段后，显示如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">value of &#x27;AUTHOR&#x27; is: 林晓lx</span><br><span class="line">value of &#x27;COMPANY&#x27; is: my-company</span><br><span class="line">value of &#x27;MAIL&#x27; is: jevonsflash@qq.com</span><br><span class="line">value of &#x27;FOOBAR&#x27; (non-exist) is: $&#123;FOOBAR&#125;</span><br></pre></td></tr></table></figure><p>与自定义映射一样，当默认映射值未设置或者不可用时，将直接显示变量占位符</p><h3><span id="自动完成">自动完成</span></h3><p>代码片段可在编辑器中显示自动提示和补全内容。</p><p><img src="/posts/10673802/644861-20240823001705706-718130618.gif"></p><h2><span id="快速开始">快速开始</span></h2><p>通过点击上方横幅上的安装按钮安装 Snippet Craft，或在 VS Code 的扩展侧边栏中搜索 Snippet Craft 进行安装。</p><h2><span id="更新内容">更新内容</span></h2><table><thead><tr><th align="center">Date</th><th align="center">Version</th><th align="left">Content</th></tr></thead><tbody><tr><td align="center">V0.1.0</td><td align="center">2024-8-22</td><td align="left">初始版本</td></tr></tbody></table><h2><span id="todo">Todo</span></h2><ul><li><input disabled type="checkbox"> 支持更多语言</li><li><input disabled type="checkbox"> 使用monaco-editor</li><li><input disabled type="checkbox"> 导入导出功能</li></ul><h2><span id="作者信息">作者信息</span></h2><p>作者：林小</p><p>邮箱：<a href="mailto:&#106;&#x65;&#x76;&#111;&#110;&#x73;&#x66;&#108;&#x61;&#115;&#104;&#x40;&#113;&#113;&#x2e;&#x63;&#111;&#109;">&#106;&#x65;&#x76;&#111;&#110;&#x73;&#x66;&#108;&#x61;&#115;&#104;&#x40;&#113;&#113;&#x2e;&#x63;&#111;&#109;</a></p><h2><span id="license">License</span></h2><p>The MIT License (MIT)</p><h2><span id="项目地址">项目地址</span></h2><p><a href="https://github.com/jevonsflash/snippet-craft">Github:snippet-craft</a></p>]]></content>
    
    
    <summary type="html">扩展初始化时，插入了三个常用的自定义映射，你可以自由更改或添加自定义映射。示例：代码片段内容value of &#39;AUTHOR&#39; is: 林晓lx与自定义映射一样，当默认映射值未设置或者不可用时，将直接显示变量占位符。</summary>
    
    
    
    <category term="JavaScript" scheme="https://blog.matoapp.net/categories/JavaScript/"/>
    
    
    <category term="VS Code" scheme="https://blog.matoapp.net/tags/VS-Code/"/>
    
  </entry>
  
  <entry>
    <title>[学习笔记]在不同项目中切换Node.js版本</title>
    <link href="https://blog.matoapp.net/posts/4ba5d0e5/"/>
    <id>https://blog.matoapp.net/posts/4ba5d0e5/</id>
    <published>2024-08-22T19:53:00.000Z</published>
    <updated>2025-09-24T16:30:02.654Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E4%BD%BF%E7%94%A8-node-version-manager-nvm">使用 Node Version Manager (NVM)</a><ul><li><a href="#%E5%AE%89%E8%A3%85-nvm">安装 NVM</a></li><li><a href="#%E4%BD%BF%E7%94%A8-nvm-%E5%AE%89%E8%A3%85%E5%92%8C%E5%88%87%E6%8D%A2-nodejs-%E7%89%88%E6%9C%AC">使用 NVM 安装和切换 Node.js 版本</a></li><li><a href="#%E4%B8%BA%E9%A1%B9%E7%9B%AE%E6%8C%87%E5%AE%9A-nodejs-%E7%89%88%E6%9C%AC">为项目指定 Node.js 版本</a></li></ul></li><li><a href="#%E4%BD%BF%E7%94%A8%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E6%8C%87%E5%AE%9A-nodejs">使用环境变量指定 Node.js</a><ul><li><a href="#%E5%AE%89%E8%A3%85%E5%A4%9A%E4%B8%AA%E7%89%88%E6%9C%AC%E7%9A%84-nodejs">安装多个版本的 Node.js</a></li><li><a href="#%E8%AE%BE%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F">设置环境变量</a></li><li><a href="#%E9%AA%8C%E8%AF%81%E9%85%8D%E7%BD%AE">验证配置</a></li><li><a href="#%E4%BD%BF%E7%94%A8-npm-%E8%84%9A%E6%9C%AC%E5%88%87%E6%8D%A2">使用 npm 脚本切换</a></li></ul></li></ul><!-- tocstop --><p>在开发中，可能会遇到不同的Vue项目需要不同的Node.js，在开发机上如何快速切换Node的版本呢？</p><h2><span id="使用-node-version-manager-nvm">使用 Node Version Manager (NVM)</span></h2><h3><span id="安装-nvm">安装 NVM</span></h3><p>在 Linux 或 macOS 上可以通过以下命令安装 NVM：</p><p><code>curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash</code> </p><p>在 Windows 上，可以下载并安装 <a href="https://github.com/coreybutler/nvm-windows/releases">nvm-windows</a>。</p><h3><span id="使用-nvm-安装和切换-nodejs-版本">使用 NVM 安装和切换 Node.js 版本</span></h3><p>安装一个特定版本的 Node.js：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">nvm install 16</span><br><span class="line"></span><br><span class="line">nvm install 22</span><br></pre></td></tr></table></figure><p>切换到指定的 Node.js 版本：</p><p><code>nvm use 22</code> </p><p>可以通过以下命令检查当前使用的 Node.js 版本：</p><p><code>node -v</code> </p><h3><span id="为项目指定-nodejs-版本">为项目指定 Node.js 版本</span></h3><p>可以在项目根目录中创建一个 <code>.nvmrc</code> 文件，里面指定该项目需要使用的 Node.js 版本：</p><p><code>16</code> </p><p>然后，在进入项目目录时，运行以下命令自动使用 <code>.nvmrc</code> 中指定的版本：</p><p><code>nvm use</code> </p><h2><span id="使用环境变量指定-nodejs">使用环境变量指定 Node.js</span></h2><h3><span id="安装多个版本的-nodejs">安装多个版本的 Node.js</span></h3><p>下载 Node.js，并在不同目录下安装多个版本。如<code>C:\Program Files\nodejs16</code>和<code>C:\Program Files\nodejs22</code></p><p><img src="/posts/4ba5d0e5/644861-20240822195107888-873726239.png"></p><p><img src="/posts/4ba5d0e5/644861-20240822195107993-1055935573.png"></p><h3><span id="设置环境变量">设置环境变量</span></h3><p>可以在系统的环境变量配置中手动设置 <code>NODE_HOME</code>。</p><p><strong>在 Windows 上：</strong></p><ol><li>打开 “系统属性” &gt; “高级系统设置” &gt; “环境变量”。</li><li>在 “系统变量” 或 “用户变量” 中点击 “新建”。</li><li>创建一个新的变量 <code>NODE_HOME</code>，并设置它的值为 Node.js 安装路径。</li></ol><p>   <img src="/posts/4ba5d0e5/644861-20240822195107979-2021687649.png"></p><ol start="4"><li>找到 <code>Path</code> 变量，点击 “编辑”。</li><li>添加一个新条目：<code>%NODE_HOME%</code>。</li></ol><p><strong>在 macOS&#x2F;Linux 上：</strong></p><ol><li><p>打开终端，编辑 <code>.bashrc</code>、<code>.bash_profile</code> 或 <code>.zshrc</code> 文件：</p><p><code>export NODE_HOME=/usr/bin/nodejs22 export PATH=$NODE_HOME:$PATH</code> </p></li><li><p>保存文件并运行 <code>source ~/.bashrc</code> 或 <code>source ~/.zshrc</code> 以使更改生效。</p></li></ol><p>通过更改 <code>NODE_HOME</code> 环境变量的路径，实现切换Node.js 的版本。</p><h3><span id="验证配置">验证配置</span></h3><p>可以通过以下命令验证 Node.js 是否已成功通过 <code>NODE_HOME</code> 变量加载：</p><p><code>node -v</code> </p><p>输出应显示所配置的 Node.js 版本：</p><p><img src="/posts/4ba5d0e5/644861-20240822195107885-1416628331.png"></p><p>cross-env 是一个用于在不同操作系统上设置和使用环境变量的工具，可以用它来切换 <code>NODE_HOME</code> 环境变量，以确保在不同的操作系统上都能正确设置和使用 Node.js 的路径。</p><p>在 <code>package.json</code> 中，可以定义如下的 npm 脚本：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;scripts&quot;: &#123;</span><br><span class="line">    &quot;set-node16&quot;: &quot;cross-env NODE_HOME=C:\\Program Files\\nodejs16&quot;,</span><br><span class="line">    &quot;set-node22&quot;: &quot;cross-env NODE_HOME=C:\\Program Files\\nodejs22&quot;,</span><br><span class="line">    &quot;set-node16-unix&quot;: &quot;cross-env NODE_HOME=/usr/bin/nodejs16&quot;,</span><br><span class="line">    &quot;set-node22-unix&quot;: &quot;cross-env NODE_HOME=/usr/bin/nodejs22&quot;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3><span id="使用-npm-脚本切换">使用 npm 脚本切换</span></h3><p>可以根据操作系统和需要的 Node.js 版本运行相应的脚本：</p><p><strong>在 Windows 上：</strong></p><p><code>npm run set-node16</code> </p><p><strong>在 Unix 系统上（Linux&#x2F;Mac）：</strong></p><p><code>npm run set-node16-unix</code> </p><p>总结：最好是通过第一种方式，因为手动配置环境变量的方式会产生依赖，需要在同事的电脑上手动配置。</p>]]></content>
    
    
    <summary type="html">在开发中，可能会遇到不同的Vue项目需要不同的Node.js，在开发机上如何快速切换Node的版本呢？总结：最好是通过第一种方式，因为手动配置环境变量的方式会产生依赖，需要在同事的电脑上手动配置。cross-env 是一个用于在不同操作系统上设置和使用环境变量的工具，可以用它来切换。环境变量，以确保在不同的操作系统上都能正确设置和使用 Node.js 的路径。下载 Node.js，并在不同目录下安装多个版本。环境变量的路径，实现切换Node.js 的版本。可以在系统的环境变量配置中手动设置。</summary>
    
    
    
    <category term="JavaScript" scheme="https://blog.matoapp.net/categories/JavaScript/"/>
    
    
    <category term="JavaScript" scheme="https://blog.matoapp.net/tags/JavaScript/"/>
    
    <category term="Node.JS" scheme="https://blog.matoapp.net/tags/Node-JS/"/>
    
  </entry>
  
  <entry>
    <title>[VS Code扩展]写一个代码片段管理插件（二）：功能实现</title>
    <link href="https://blog.matoapp.net/posts/9244fcab/"/>
    <id>https://blog.matoapp.net/posts/9244fcab/</id>
    <published>2024-08-22T09:51:00.000Z</published>
    <updated>2025-09-24T16:30:02.618Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E5%88%9B%E5%BB%BA%E5%92%8C%E6%8F%92%E5%85%A5%E4%BB%A3%E7%A0%81%E7%89%87%E6%AE%B5">创建和插入代码片段</a></li><li><a href="#%E4%BB%A3%E7%A0%81%E7%89%87%E6%AE%B5%E5%88%97%E8%A1%A8">代码片段列表</a></li><li><a href="#%E4%BB%A3%E7%A0%81%E7%89%87%E6%AE%B5%E9%A2%84%E8%A7%88">代码片段预览</a></li><li><a href="#%E4%BB%A3%E7%A0%81%E7%89%87%E6%AE%B5%E7%BC%96%E8%BE%91">代码片段编辑</a></li><li><a href="#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%98%A0%E5%B0%84">自定义映射</a></li><li><a href="#%E9%BB%98%E8%AE%A4%E6%98%A0%E5%B0%84">默认映射</a></li><li><a href="#%E8%87%AA%E5%8A%A8%E5%AE%8C%E6%88%90">自动完成</a></li><li><a href="#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80">项目地址</a></li></ul><!-- tocstop --><h2><span id="创建和插入代码片段">创建和插入代码片段</span></h2><p>VS Code扩展提供了数据存储，其中globalState是使用全局存储的Key-Value方式来保存用户状态，支持在不同计算机上保留某些用户状态，详情请参考<a href="https://code.visualstudio.com/api/extension-capabilities/common-capabilities#data-storage">官方文档</a></p><p>若在编辑器区域有选中的文本，点击右键菜单中点击创建Snippet，则调用<code>extension.snippetCraft.createSnipp</code>命令，执行创建代码片段。</p><p><img src="/posts/9244fcab/644861-20240822094918152-773155974.gif"></p><p>创建服务类 <code>SnippService.ts</code>，代码如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">export async function AddSnipp(context: ExtensionContext, state: Partial&lt;ISnipp&gt;) &#123;</span><br><span class="line">  const content = await getSnippText();</span><br><span class="line">  const trimmedName = content?.text?.trim().substring(0, 20) || &#x27;&#x27;;</span><br><span class="line">  await _addOrUpdateSnipp(context, &#123; ...state, name: trimmedName &#125;, content)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在<code>_addOrUpdateSnipp</code>方法中对<code>snipps</code>进行更新操作</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">async function _addOrUpdateSnipp(context: ExtensionContext, state: Partial&lt;ISnipp&gt;, content?: &#123;</span><br><span class="line">  text: string | undefined;</span><br><span class="line">  type: string | undefined;</span><br><span class="line">&#125;, snippIndex?: number) &#123;</span><br><span class="line">   </span><br><span class="line">  ...</span><br><span class="line">  context.globalState.update(&quot;snipps&quot;, updatedSnipps);</span><br></pre></td></tr></table></figure><p>若在编辑器区域右键菜单中点击插入Snippet，或在代码片段视图中点击条目，则调用<code>extension.snippetCraft.insertSnipps</code>命令，它会调用<code>InsertSnipp</code>方法执行插入代码片段操作。</p><p>在服务类 <code>SnippService.ts</code>，插入如下代码</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">export async function InsertSnipp(context: ExtensionContext, snipp: ISnipp) &#123;</span><br><span class="line">  const editor = window.activeTextEditor;</span><br><span class="line">  if (editor &amp;&amp; SnippDataProvider.isSnipp(snipp)) &#123;</span><br><span class="line">    const position = editor?.selection.active;</span><br><span class="line">    editor.edit(async (edit) =&gt; &#123;</span><br><span class="line"></span><br><span class="line">      edit.insert(position, snipp.content || &#x27;&#x27;);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2><span id="代码片段列表">代码片段列表</span></h2><p>代码片段显示为一个树形结构，根据创建时的文件内容类型，分组显示代码片段条目</p><p><img src="/posts/9244fcab/644861-20240822094918107-1250088802.png"></p><p>创建代码片段和分组条目的接口类型</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">import * as vscode from &quot;vscode&quot;;</span><br><span class="line"></span><br><span class="line">export interface ISnipp &#123;</span><br><span class="line">  name: string;</span><br><span class="line">  content: string;</span><br><span class="line">  contentType: string;</span><br><span class="line">  created: Date;</span><br><span class="line">  lastUsed: Date;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">export interface IGroup &#123;</span><br><span class="line">  name: string;</span><br><span class="line">  contentType: string | undefined;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在SnippItem中创建获取所有分组类型的get访问器，和获取分组下的条目getChildren方法</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">export class SnippItem &#123;</span><br><span class="line">  constructor(</span><br><span class="line">    readonly view: string,</span><br><span class="line">    private context: vscode.ExtensionContext</span><br><span class="line">  ) &#123; &#125;</span><br><span class="line"></span><br><span class="line">  public get roots(): Thenable&lt;IGroup[]&gt; &#123;</span><br><span class="line">    const snipps = this.context?.globalState?.get(&quot;snipps&quot;, []);</span><br><span class="line">    const types = snipps</span><br><span class="line">      .map((snipp: ISnipp) =&gt; snipp.contentType)</span><br><span class="line">      .filter((value, index, self) =&gt; self.indexOf(value) === index)</span><br><span class="line">      .map((type) =&gt; (&#123; name: type, contentType: undefined &#125;));</span><br><span class="line">    return Promise.resolve(types);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  public getChildren(node: IGroup): Thenable&lt;ISnipp[]&gt; &#123;</span><br><span class="line">    const snipps = this.context?.globalState</span><br><span class="line">      ?.get(&quot;snipps&quot;, [])</span><br><span class="line">      .filter((snipp: ISnipp) =&gt; &#123;</span><br><span class="line">        return snipp.contentType === node.name;</span><br><span class="line">      &#125;)</span><br><span class="line">      .sort((a: ISnipp, b: ISnipp) =&gt; a.name.localeCompare(b.name));</span><br><span class="line"></span><br><span class="line">    return Promise.resolve(snipps);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">export class GroupItem &#123; &#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>VS Code扩展的侧边栏中显示内容需为树形结构，通过实现<code>TreeDataProvider</code>为内容提供数据，请参考<a href="https://code.visualstudio.com/api/extension-guides/tree-view">官方说明</a></p><p>实现getChildren方法</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">export class SnippDataProvider</span><br><span class="line">  implements</span><br><span class="line">    vscode.TreeDataProvider&lt;ISnipp | IGroup&gt;</span><br><span class="line">&#123;</span><br><span class="line">  </span><br><span class="line">  public getChildren(</span><br><span class="line">    element?: ISnipp | IGroup</span><br><span class="line">  ): ISnipp[] | Thenable&lt;ISnipp[]&gt; | IGroup[] | Thenable&lt;IGroup[]&gt; &#123;</span><br><span class="line">    return element ? this.model.getChildren(element) : this.model.roots;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="代码片段预览">代码片段预览</span></h2><p>实现getTreeItem方法，显示预览</p><p>点击时调用<code>extension.snippetCraft.insertEntry</code>命令实现插入代码片段，command部分在上一章节有介绍。</p><p>鼠标移动到代码片段条目上时，显示tooltip预览</p><p><img src="/posts/9244fcab/644861-20240822094918114-1693642503.png"></p><p>代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">public getTreeItem(element: ISnipp | IGroup): vscode.TreeItem &#123;</span><br><span class="line">    const t = element.name;</span><br><span class="line">    const isSnip = SnippDataProvider.isSnipp(element);</span><br><span class="line">    const snippcomm = &#123;</span><br><span class="line">      command: &quot;extension.snippetCraft.insertEntry&quot;,</span><br><span class="line">      title: &#x27;&#x27;,</span><br><span class="line">      arguments: [element],</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    let snippetInfo: string = `[$&#123;element.contentType&#125;] $&#123;element.name&#125;`;</span><br><span class="line"></span><br><span class="line">    return &#123;</span><br><span class="line">      // @ts-ignore</span><br><span class="line">      label: isSnip ? element.name : element.name,</span><br><span class="line">      command: isSnip ? snippcomm : undefined,</span><br><span class="line">      iconPath:isSnip ? new ThemeIcon(&quot;code&quot;):new ThemeIcon(&quot;folder&quot;),</span><br><span class="line">      tooltip: isSnip</span><br><span class="line">        ? new vscode.MarkdownString(</span><br><span class="line">            // @ts-ignore</span><br><span class="line">            `**标题：**$&#123;snippetInfo&#125;\n\n**修改时间：**$&#123;element.created&#125;\n\n**最近使用：**$&#123;element.lastUsed&#125;\n\n**预览：**\n\`\`\`$&#123;element.contentType&#125;\n$&#123;element.content&#125;\n\`\`\``</span><br><span class="line">          )</span><br><span class="line">        : undefined,</span><br><span class="line">      collapsibleState: !isSnip</span><br><span class="line">        ? vscode.TreeItemCollapsibleState.Collapsed</span><br><span class="line">        : undefined,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><h2><span id="代码片段编辑">代码片段编辑</span></h2><p>编辑器是一个输入框，由于VS Code的输入框不支持多行输入，所以需要使用webview实现多行输入。同时需要提交按钮与取消按钮</p><p><img src="/posts/9244fcab/644861-20240822094918099-849219896.gif"></p><p>首先创建一个多行文本框的WebView，<br>在服务类 <code>SnippService.ts</code>，创建一个函数<code>getWebviewContent</code>，返回一个HTML字符串，用于创建一个多行输入框。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">function getWebviewContent(placeholder: string, initialValue: string): string &#123;</span><br><span class="line">  return `</span><br><span class="line">      &lt;!DOCTYPE html&gt;</span><br><span class="line">      &lt;html lang=&quot;en&quot;&gt;</span><br><span class="line">      &lt;head&gt;</span><br><span class="line">        &lt;meta charset=&quot;UTF-8&quot;&gt;</span><br><span class="line">        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;</span><br><span class="line">        &lt;title&gt;Multiline Input&lt;/title&gt;</span><br><span class="line">      &lt;/head&gt;</span><br><span class="line">      &lt;body&gt;</span><br><span class="line">        &lt;textarea id=&quot;inputBox&quot; rows=&quot;10&quot; cols=&quot;50&quot; placeholder=&quot;$&#123;placeholder&#125;&quot;&gt;$&#123;initialValue&#125;&lt;/textarea&gt;</span><br><span class="line">        &lt;br&gt;</span><br><span class="line">        &lt;button onclick=&quot;submitText()&quot;&gt;提交&lt;/button&gt;</span><br><span class="line">        &lt;button onclick=&quot;cancel()&quot;&gt;取消&lt;/button&gt;</span><br><span class="line">        &lt;script&gt;</span><br><span class="line">          const vscode = acquireVsCodeApi();</span><br><span class="line">          function submitText() &#123;</span><br><span class="line">            const text = document.getElementById(&#x27;inputBox&#x27;).value;</span><br><span class="line">            vscode.postMessage(&#123; command: &#x27;submit&#x27;, text: text &#125;);</span><br><span class="line">          &#125;</span><br><span class="line">          function cancel() &#123;</span><br><span class="line">            vscode.postMessage(&#123; command: &#x27;cancel&#x27; &#125;);</span><br><span class="line">          &#125;</span><br><span class="line">        &lt;/script&gt;</span><br><span class="line">      &lt;/body&gt;</span><br><span class="line">      &lt;/html&gt;</span><br><span class="line">    `;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>添加处理函数，当用户点击“提交“时，将文本输入框中的内容返回，同时关闭输入框窗口。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">async function showInputBoxWithMultiline(context: ExtensionContext, placeholder: string, initialValue: string): Promise&lt;string | undefined&gt; &#123;</span><br><span class="line">  const panel = window.createWebviewPanel(</span><br><span class="line">    &#x27;multilineInput&#x27;,</span><br><span class="line">    &#x27;Multiline Input&#x27;,</span><br><span class="line">    ViewColumn.One,</span><br><span class="line">    &#123;</span><br><span class="line">      enableScripts: true</span><br><span class="line">    &#125;</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  panel.webview.html = getWebviewContent(placeholder, initialValue);</span><br><span class="line"></span><br><span class="line">  return new Promise&lt;string | undefined&gt;((resolve) =&gt; &#123;</span><br><span class="line">    panel.webview.onDidReceiveMessage(</span><br><span class="line">      message =&gt; &#123;</span><br><span class="line">        switch (message.command) &#123;</span><br><span class="line">          case &#x27;submit&#x27;:</span><br><span class="line">            resolve(message.text);</span><br><span class="line">            panel.dispose();</span><br><span class="line">            return;</span><br><span class="line">          case &#x27;cancel&#x27;:</span><br><span class="line">            resolve(undefined);</span><br><span class="line">            panel.dispose();</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      undefined,</span><br><span class="line">      context.subscriptions</span><br><span class="line">    );</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在添加代码片段和编辑代码片段时触发函数</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">export async function AddSnippFromEditor(context: ExtensionContext, state: Partial&lt;ISnipp&gt;) &#123;</span><br><span class="line">  const content = await showInputBoxWithMultiline(context, &#x27;请输入Snippet内容&#x27;, &#x27;&#x27;);</span><br><span class="line">  if (content) &#123;</span><br><span class="line">    _addOrUpdateSnipp(context, state, &#123; text: content, type: &quot;TEXT&quot; &#125;)</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">export async function EditSnipp(context: ExtensionContext, state: Partial&lt;ISnipp&gt;, snippIndex: number) &#123;</span><br><span class="line">  const content = await showInputBoxWithMultiline(context, &#x27;请输入Snippet内容&#x27;, state.content ?? &#x27;&#x27;);</span><br><span class="line">  if (content) &#123;</span><br><span class="line">    _addOrUpdateSnipp(context, state, &#123; text: content, type: state.contentType ?? &quot;TEXT&quot; &#125;, snippIndex)</span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="自定义映射">自定义映射</span></h2><p>映射是插入代码片段时，自动替换的变量，他们通过Key-Value形式存储于globalState中。</p><p>代码片段中通过设置占位符（如<code>$&#123;AUTHOR&#125;</code>），在插入代码片段时，将自动替换为全局变量中的值。</p><p>当自定义映射值未设置或者不可用时，将直接显示变量占位符</p><p>扩展初始化时，插入了三个常用的自定义映射，你可以自由更改或添加自定义映射。</p><ul><li><code>$&#123;AUTHOR&#125;</code>: 作者姓名</li><li><code>$&#123;COMPANY&#125;</code>: 公司名称</li><li><code>$&#123;MAIL&#125;</code>: 邮箱地址</li></ul><p>扩展中所有的自定义映射，呈现于“映射表”树视图中。</p><p><img src="/posts/9244fcab/644861-20240822094918114-453429249.png"></p><p>示例：</p><p>代码片段内容</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">value of &#x27;AUTHOR&#x27; is: $&#123;AUTHOR&#125;</span><br><span class="line">value of &#x27;COMPANY&#x27; is: $&#123;COMPANY&#125;</span><br><span class="line">value of &#x27;MAIL&#x27; is: $&#123;MAIL&#125;</span><br><span class="line">value of &#x27;FOOBAR&#x27; (non-exist) is: $&#123;FOOBAR&#125;</span><br></pre></td></tr></table></figure><p>插入代码片段后，显示如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">value of &#x27;AUTHOR&#x27; is: 林晓lx</span><br><span class="line">value of &#x27;COMPANY&#x27; is: my-company</span><br><span class="line">value of &#x27;MAIL&#x27; is: jevonsflash@qq.com</span><br><span class="line">value of &#x27;FOOBAR&#x27; (non-exist) is: $&#123;FOOBAR&#125;</span><br></pre></td></tr></table></figure><p>首先定义KVItem类：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">export class KVItem extends vscode.TreeItem &#123;</span><br><span class="line">    constructor(</span><br><span class="line">      public readonly key: string,</span><br><span class="line">      public readonly value: string | undefined</span><br><span class="line">    ) &#123;</span><br><span class="line">      super(key, vscode.TreeItemCollapsibleState.None);</span><br><span class="line">      this.tooltip = `$&#123;this.key&#125;: $&#123;this.value&#125;`;</span><br><span class="line">      this.description = this.value;</span><br><span class="line">      this.contextValue = &#x27;kvItem&#x27;;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>“映射表”树视图中显示内容需为树形结构，同样需要定义<code>KVTreeDataProvider</code>，在此实现刷新、添加、删除、获取子节点等方法。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">export class KVTreeDataProvider implements vscode.TreeDataProvider&lt;KVItem&gt; &#123;</span><br><span class="line">  private _onDidChangeTreeData: vscode.EventEmitter&lt;KVItem | undefined&gt; = new vscode.EventEmitter&lt;KVItem | undefined&gt;();</span><br><span class="line">  readonly onDidChangeTreeData: vscode.Event&lt;KVItem | undefined&gt; = this._onDidChangeTreeData.event;</span><br><span class="line"></span><br><span class="line">  constructor(private globalState: vscode.Memento) &#123;&#125;</span><br><span class="line"></span><br><span class="line">  getTreeItem(element: KVItem): vscode.TreeItem &#123;</span><br><span class="line">    return element;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  getChildren(element?: KVItem): Thenable&lt;KVItem[]&gt; &#123;</span><br><span class="line">    if (element) &#123;</span><br><span class="line">      return Promise.resolve([]);</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">      const kvObject = this.globalState.get&lt;&#123; [key: string]: string &#125;&gt;(&#x27;key-value&#x27;, &#123;&#125;);</span><br><span class="line">      const keys = Object.keys(kvObject);</span><br><span class="line">      return Promise.resolve(keys.map(key =&gt; new KVItem(key, kvObject[key])));</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  refresh(): void &#123;</span><br><span class="line">    this._onDidChangeTreeData.fire(undefined);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  addOrUpdateKey(key: string, value: string): void &#123;</span><br><span class="line">    const kvObject = this.globalState.get&lt;&#123; [key: string]: string &#125;&gt;(&#x27;key-value&#x27;, &#123;&#125;);</span><br><span class="line">    kvObject[key] = value;</span><br><span class="line">    this.globalState.update(&#x27;key-value&#x27;, kvObject);</span><br><span class="line">    this.refresh();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  deleteKey(key: string): void &#123;</span><br><span class="line">    const kvObject = this.globalState.get&lt;&#123; [key: string]: string &#125;&gt;(&#x27;key-value&#x27;, &#123;&#125;);</span><br><span class="line">    delete kvObject[key];</span><br><span class="line">    this.globalState.update(&#x27;key-value&#x27;, kvObject);</span><br><span class="line">    this.refresh();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="默认映射">默认映射</span></h2><p>默认映射是扩展内置的映射功能，可用的映射如下</p><p>文件和编辑器相关：</p><ul><li>TM_SELECTED_TEXT: 当前选定的文本或空字符串</li><li>TM_CURRENT_LINE: 当前行的内容</li><li>TM_CURRENT_WORD: 光标下的单词或空字符串的内容</li><li>TM_LINE_INDEX: 基于零索引的行号</li><li>TM_LINE_NUMBER: 基于一个索引的行号</li><li>TM_FILENAME: 当前文档的文件名</li><li>TM_FILENAME_BASE: 当前文档的文件名（不含扩展名）</li><li>TM_DIRECTORY: 当前文档的目录</li><li>TM_FILEPATH: 当前文档的完整文件路径</li><li>RELATIVE_FILEPATH: 当前文档的相对文件路径（相对于打开的工作区或文件夹）</li><li>CLIPBOARD: 剪贴板的内容</li><li>WORKSPACE_NAME: 打开的工作区或文件夹的名称</li><li>WORKSPACE_FOLDER: 打开的工作区或文件夹的路径</li><li>CURSOR_INDEX: 基于零索引的游标编号</li><li>CURSOR_NUMBER: 基于单索引的游标编号</li></ul><p>时间相关：</p><ul><li>CURRENT_YEAR: 本年度</li><li>CURRENT_YEAR_SHORT: 当年的最后两位数字</li><li>CURRENT_MONTH: 两位数字的月份（例如“02”）</li><li>CURRENT_MONTH_NAME: 月份的全名（例如“July”）</li><li>CURRENT_MONTH_NAME_SHORT: 月份的简短名称（例如“Jul”）</li><li>CURRENT_DATE: 以两位数字表示的月份中的某一天（例如“08”）</li><li>CURRENT_DAY_NAME: 日期的名称（例如“星期一”）</li><li>CURRENT_DAY_NAME_SHORT: 当天的简短名称（例如“Mon”）</li><li>CURRENT_HOUR24: 小时制格式的当前小时</li><li>CURRENT_MINUTE: 两位数的当前分钟数</li><li>CURRENT_SECOND: 当前秒数为两位数</li><li>CURRENT_SECONDS_UNIX: 自 Unix 纪元以来的秒数</li><li>CURRENT_TIMEZONE_OFFSET当前 UTC 时区偏移量为 +HH:MM 或者 -HH:MM (例如“-07:00”)。</li></ul><p>其他：</p><ul><li>RANDOM6: 个随机 Base-10 数字</li><li>RANDOM_HEX6: 个随机 Base-16 数字</li><li>UUID: 第四版UUID</li></ul><p>这些项目参考至VS Code 代码片段变量，请查看<a href="https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables">VSCode官方文档</a></p><p>与自定义映射一样，当默认映射值未设置或者不可用时，将直接显示变量占位符</p><p>实现方法如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">export async function ReplacePlaceholders(text: string, context: ExtensionContext): Promise&lt;string&gt; &#123;</span><br><span class="line">  const editor = window.activeTextEditor;</span><br><span class="line">  const clipboard = await env.clipboard.readText();</span><br><span class="line">  const workspaceFolders = workspace.workspaceFolders;</span><br><span class="line">  const currentDate = new Date();</span><br><span class="line">  const kvObject = context.globalState.get&lt;&#123; [key: string]: string &#125;&gt;(&#x27;key-value&#x27;, &#123;&#125;);</span><br><span class="line"></span><br><span class="line">  const replacements: &#123; [key: string]: string &#125; = &#123;</span><br><span class="line">    &#x27;$&#123;TM_SELECTED_TEXT&#125;&#x27;: editor?.document.getText(editor.selection) || &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;TM_CURRENT_LINE&#125;&#x27;: editor?.document.lineAt(editor.selection.active.line).text || &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;TM_CURRENT_WORD&#125;&#x27;: editor?.document.getText(editor.document.getWordRangeAtPosition(editor.selection.active)) || &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;TM_LINE_INDEX&#125;&#x27;: (editor?.selection.active.line ?? 0).toString(),</span><br><span class="line">    &#x27;$&#123;TM_LINE_NUMBER&#125;&#x27;: ((editor?.selection.active.line ?? 0) + 1).toString(),</span><br><span class="line">    &#x27;$&#123;TM_FILENAME&#125;&#x27;: editor ? path.basename(editor.document.fileName) : &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;TM_FILENAME_BASE&#125;&#x27;: editor ? path.basename(editor.document.fileName, path.extname(editor.document.fileName)) : &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;TM_DIRECTORY&#125;&#x27;: editor ? path.dirname(editor.document.fileName) : &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;TM_FILEPATH&#125;&#x27;: editor?.document.fileName || &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;RELATIVE_FILEPATH&#125;&#x27;: editor &amp;&amp; workspaceFolders ? path.relative(workspaceFolders[0].uri.fsPath, editor.document.fileName) : &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;CLIPBOARD&#125;&#x27;: clipboard,</span><br><span class="line">    &#x27;$&#123;WORKSPACE_NAME&#125;&#x27;: workspaceFolders ? workspaceFolders[0].name : &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;WORKSPACE_FOLDER&#125;&#x27;: workspaceFolders ? workspaceFolders[0].uri.fsPath : &#x27;&#x27;,</span><br><span class="line">    &#x27;$&#123;CURSOR_INDEX&#125;&#x27;: (editor?.selections.indexOf(editor.selection) ?? 0).toString(),</span><br><span class="line">    &#x27;$&#123;CURSOR_NUMBER&#125;&#x27;: ((editor?.selections.indexOf(editor.selection) ?? 0) + 1).toString(),</span><br><span class="line">    &#x27;$&#123;CURRENT_YEAR&#125;&#x27;: currentDate.getFullYear().toString(),</span><br><span class="line">    &#x27;$&#123;CURRENT_YEAR_SHORT&#125;&#x27;: currentDate.getFullYear().toString().slice(-2),</span><br><span class="line">    &#x27;$&#123;CURRENT_MONTH&#125;&#x27;: (currentDate.getMonth() + 1).toString().padStart(2, &#x27;0&#x27;),</span><br><span class="line">    &#x27;$&#123;CURRENT_MONTH_NAME&#125;&#x27;: currentDate.toLocaleString(&#x27;default&#x27;, &#123; month: &#x27;long&#x27; &#125;),</span><br><span class="line">    &#x27;$&#123;CURRENT_MONTH_NAME_SHORT&#125;&#x27;: currentDate.toLocaleString(&#x27;default&#x27;, &#123; month: &#x27;short&#x27; &#125;),</span><br><span class="line">    &#x27;$&#123;CURRENT_DATE&#125;&#x27;: currentDate.getDate().toString().padStart(2, &#x27;0&#x27;),</span><br><span class="line">    &#x27;$&#123;CURRENT_DAY_NAME&#125;&#x27;: currentDate.toLocaleString(&#x27;default&#x27;, &#123; weekday: &#x27;long&#x27; &#125;),</span><br><span class="line">    &#x27;$&#123;CURRENT_DAY_NAME_SHORT&#125;&#x27;: currentDate.toLocaleString(&#x27;default&#x27;, &#123; weekday: &#x27;short&#x27; &#125;),</span><br><span class="line">    &#x27;$&#123;CURRENT_HOUR&#125;&#x27;: currentDate.getHours().toString().padStart(2, &#x27;0&#x27;),</span><br><span class="line">    &#x27;$&#123;CURRENT_MINUTE&#125;&#x27;: currentDate.getMinutes().toString().padStart(2, &#x27;0&#x27;),</span><br><span class="line">    &#x27;$&#123;CURRENT_SECOND&#125;&#x27;: currentDate.getSeconds().toString().padStart(2, &#x27;0&#x27;),</span><br><span class="line">    &#x27;$&#123;CURRENT_SECONDS_UNIX&#125;&#x27;: Math.floor(currentDate.getTime() / 1000).toString(),</span><br><span class="line">    &#x27;$&#123;CURRENT_TIMEZONE_OFFSET&#125;&#x27;: formatTimezoneOffset(currentDate.getTimezoneOffset()),</span><br><span class="line">    &#x27;$&#123;RANDOM&#125;&#x27;: Math.random().toString().slice(2, 8),</span><br><span class="line">    &#x27;$&#123;RANDOM_HEX&#125;&#x27;: Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, &#x27;0&#x27;),</span><br><span class="line">    &#x27;$&#123;UUID&#125;&#x27;: generateUUID()</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  Object.keys(kvObject).forEach(key =&gt; &#123;</span><br><span class="line">    replacements[`$\&#123;$&#123;key&#125;\&#125;`] = kvObject[key];</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  return text.replace(/\$\&#123;(\w+)\&#125;/g, (match, key) =&gt; &#123;</span><br><span class="line">    return replacements[match] || match;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="自动完成">自动完成</span></h2><p>自动完成是VS Code编辑器提供的一个功能，用于在编辑器中显示自动提示和补全内容。扩展提供了基于代码片段的自动完成功能。</p><p><img src="/posts/9244fcab/644861-20240822094918143-259248907.gif"></p><p><code>CompletionItemProvider</code>用于注册自动完成的规则，提供者约定了在指定的文档类型下，当输入的字符匹配时，将出现自动完成上下文菜单。</p><p>上下文菜单中列出所有可用的自动完成条目，每个条目由<code>CompletionItem</code>定义，点击对应条目后，将处理后的字符串返回，填写到编辑器当前光标处。</p><p>languages.registerCompletionItemProvider用于注册自动完成的规则提供者。</p><p>在<code>extension.ts</code>中注册初始化时，所有的自动完成条目</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">const providers = contentTypes</span><br><span class="line">  .filter((value, index, self) =&gt; self.indexOf(value) === index)</span><br><span class="line">  .map(type =&gt;</span><br><span class="line">    languages.registerCompletionItemProvider(type, &#123;</span><br><span class="line">      provideCompletionItems(</span><br><span class="line">        document: TextDocument,</span><br><span class="line">        position: Position,</span><br><span class="line">        token: CancellationToken,</span><br><span class="line">        context: CompletionContext</span><br><span class="line">      ) &#123;</span><br><span class="line">        return new Promise&lt;CompletionItem[]&gt;((resolve, reject) =&gt; &#123;</span><br><span class="line"></span><br><span class="line">          var result = snipps</span><br><span class="line">            .filter((snipp: ISnipp) =&gt; &#123;</span><br><span class="line">              return snipp.contentType === type;</span><br><span class="line">            &#125;)</span><br><span class="line">            .map(async (snipp: ISnipp) =&gt; &#123;</span><br><span class="line">              const replacedContentText = await ReplacePlaceholders(snipp.content, extensionContext);</span><br><span class="line"></span><br><span class="line">              const commandCompletion = new CompletionItem(snipp.name);</span><br><span class="line">              commandCompletion.insertText = replacedContentText || &#x27;&#x27;;</span><br><span class="line">              return commandCompletion;</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">          Promise.all(result).then(resolve);</span><br><span class="line">        &#125;);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">context.subscriptions.push(...providers);</span><br></pre></td></tr></table></figure><p><code>SnippService.ts</code>_addOrUpdateSnipp方法中配置修改或新增的自动完成条目</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">if (content?.type &amp;&amp; state.name) &#123;</span><br><span class="line">  languages.registerCompletionItemProvider(content.type, &#123;</span><br><span class="line">    provideCompletionItems(</span><br><span class="line">      document: TextDocument,</span><br><span class="line">      position: Position,</span><br><span class="line">      token: CancellationToken,</span><br><span class="line">      context: CompletionContext</span><br><span class="line">    ) &#123;</span><br><span class="line">      return new Promise&lt;CompletionItem[]&gt;((resolve, reject) =&gt; &#123;</span><br><span class="line">        ReplacePlaceholders(state.content || &#x27;&#x27;, extensionContext).then(res =&gt; &#123;</span><br><span class="line">          const replacedContentText = res;</span><br><span class="line">          const commandCompletion = new CompletionItem(state.name || &#x27;&#x27;);</span><br><span class="line">          commandCompletion.insertText = replacedContentText || &#x27;&#x27;;</span><br><span class="line">          resolve([commandCompletion]);</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="项目地址">项目地址</span></h2><p><a href="https://github.com/jevonsflash/snippet-craft">Github:snippet-craft</a></p>]]></content>
    
    
    <summary type="html">映射是插入代码片段时，自动替换的变量，他们通过Key-Value形式存储于globalState中。代码片段中通过设置占位符（如${AUTHOR}），在插入代码片段时，将自动替换为全局变量中的值。当自定义映射值未设置或者不可用时，将直接显示变量占位符扩展初始化时，插入了三个常用的自定义映射，你可以自由更改或添加自定义映射。${AUTHOR}: 作者姓名${COMPANY}: 公司名称${MAIL}: 邮箱地址扩展中所有的自定义映射，呈现于“映射表”树视图中。示例：代码片段内容。</summary>
    
    
    
    <category term="JavaScript" scheme="https://blog.matoapp.net/categories/JavaScript/"/>
    
    <category term="Web" scheme="https://blog.matoapp.net/categories/JavaScript/Web/"/>
    
    
    <category term="TypeScript" scheme="https://blog.matoapp.net/tags/TypeScript/"/>
    
    <category term="插件" scheme="https://blog.matoapp.net/tags/%E6%8F%92%E4%BB%B6/"/>
    
    <category term="IDE" scheme="https://blog.matoapp.net/tags/IDE/"/>
    
    <category term="VS Code" scheme="https://blog.matoapp.net/tags/VS-Code/"/>
    
    <category term="JavaScript" scheme="https://blog.matoapp.net/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>[VS Code扩展]写一个代码片段管理插件（一）：介绍与界面搭建</title>
    <link href="https://blog.matoapp.net/posts/948f6f1b/"/>
    <id>https://blog.matoapp.net/posts/948f6f1b/</id>
    <published>2024-08-02T23:20:00.000Z</published>
    <updated>2025-09-24T16:30:02.606Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#vs-code%E6%89%A9%E5%B1%95%E6%9C%BA%E5%88%B6">VS Code扩展机制</a></li><li><a href="#%E9%A1%B9%E7%9B%AE%E6%90%AD%E5%BB%BA">项目搭建</a></li><li><a href="#%E5%88%9B%E5%BB%BAui%E5%85%83%E7%B4%A0">创建UI元素</a><ul><li><a href="#%E6%B4%BB%E5%8A%A8%E6%A0%8F%E6%8C%89%E9%92%AE">活动栏按钮</a></li><li><a href="#%E4%B8%BB%E8%BE%B9%E6%A0%8F%E8%A7%86%E5%9B%BE">主边栏视图</a></li><li><a href="#%E4%B8%BB%E8%BE%B9%E6%A0%8F%E5%B7%A5%E5%85%B7%E6%A0%8F%E6%8C%89%E9%92%AE">主边栏工具栏按钮</a></li><li><a href="#%E4%BE%A7%E8%BE%B9%E6%A0%8F%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95">侧边栏右键菜单</a></li><li><a href="#%E7%BC%96%E8%BE%91%E5%99%A8%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95">编辑器右键菜单</a></li></ul></li><li><a href="#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80">项目地址</a></li></ul><!-- tocstop --><ul><li><a href="https://www.cnblogs.com/jevonsflash/p/18339795">[VS Code扩展]写一个代码片段管理插件（一）：介绍与界面搭建</a></li><li><a href="https://www.cnblogs.com/jevonsflash/p/18373141">[VS Code扩展]写一个代码片段管理插件（二）：功能实现</a></li></ul><p>写代码的时候，经常要输入重复的内容，虽然VS Code提供了<a href="https://code.visualstudio.com/docs/editor/userdefinedsnippets">代码片段</a>功能，但是创建自定义代码片段时，需要写JSON格式的配置，这些JSON文件在用户文件夹下，没有统一的界面管理，而且对于我来说，制表符补全这样的高级功能并不是必需的。</p><p>变量映射可以通过内置的映射规则在插入片段时自动生成这些内容。VS Code自带的功能比较单一，我需要一个自定义的变量映射功能，可以自定义Key-Value做为可灵活配置的变量映射。</p><p>VS Code提供了一组<a href="https://code.visualstudio.com/api/">API</a>，用于自定义或增强软件功能，称之为VS Code插件（或扩展）。</p><p>我基于上述的考虑，写一个带有变量映射功能代码片段管理VS Code扩展：<a href="https://github.com/jevonsflash/snippet-craft">SnippetCraft</a>。</p><p><img src="/posts/948f6f1b/644861-20240802231835962-2062088570.gif"></p><h2><span id="vs-code扩展机制">VS Code扩展机制</span></h2><p>首先需要大致了解VS Code的扩展机制，VS Code可以看成一个框架，可以想象是你车上的仪表台，比如时速表仪表台，中控大屏，灯光、空调控制等独立面板所在的占位。</p><p><img src="/posts/948f6f1b/644861-20240802231835228-1759072190.png"></p><p>VS Code这些框架的部分，官方称之为“容器”，整个VS Code由6个容器组成，分别是：活动栏，主边栏，编辑器，辅边栏，面板，状态栏。</p><p><img src="/posts/948f6f1b/644861-20240802231835327-161148948.png"></p><p>每个容器中，包含扩展提供的按钮，或者视图的区域。类似仪表台控制上的控制面板，比如灯光面板上有灯的开关，有些是预留的槽位。通过增加改装件使用这些预留槽位。</p><p><img src="/posts/948f6f1b/644861-20240802231835269-1385732754.png"></p><p>这些区域，官方称之为“项目”。常用的项目有侧边栏，编辑器，状态栏，面板上的工具栏区域。扩展可以将项目添加到各种容器中。</p><p><img src="/posts/948f6f1b/644861-20240802231835880-507690450.png"></p><p>此外，VS Code扩展还提供常用的功能，比如数据持久化，文件选择器，输入框，通知弹窗，网页视图等。</p><p>VS Code扩展是一个基础功能，通过扩展可以满足软件的所有功能增强，包括内置的核心功能，如文件管理，搜索，Git，调试器，这些都是通过扩展实现的。</p><p>VS Code为了体验一致性，禁用扩展中UI元素自定义样式。</p><h2><span id="项目搭建">项目搭建</span></h2><p>请确保已安装 Node.js。使用 Yeoman 和 VS Code 扩展生成器来快速创建扩展项目。首先安装 Yeoman 和生成器：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm install -g yo generator-code</span><br><span class="line">yo code</span><br></pre></td></tr></table></figure><p>项目会自动创建一个HelloWorld扩展。</p><p>如果手动创建项目，可以参考下面的目录结构</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">my-VS Code-extension/</span><br><span class="line">├── .VS Code/</span><br><span class="line">│   └── extensions.json</span><br><span class="line">├── src/</span><br><span class="line">│   └── extension.ts</span><br><span class="line">├── .gitignore</span><br><span class="line">├── package.json</span><br><span class="line">├── tsconfig.json</span><br><span class="line">├── README.md</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>准备图标，扩展需要一个产品展示图标。图标为128x128像素的PNG格式文件<br>准备活动栏按钮图标，图标为24x24像素，居中于50x40像素的块内，填充颜色为’rgb(215, 218, 224)’或’#d7dae0’。建议使用SVG格式的图标。</p><p>VS Code扩展在<code>package.json</code>中声明“贡献点”，“贡献点”用于描述该扩展可以为VS Code增强哪些功能，请参考<a href="https://code.visualstudio.com/api/references/contribution-points">官方说明</a> </p><p>在<code>package.json</code>文件的<code>contributes</code>节点中，我们添加扩展用到的所有命令：</p><table><thead><tr><th>Command</th><th>操作</th></tr></thead><tbody><tr><td>extension.snippetCraft.searchSnipps</td><td>代码片段搜索</td></tr><tr><td>extension.snippetCraft.insertSnipps</td><td>插入代码片段</td></tr><tr><td>extension.snippetCraft.deleteAllSnippets</td><td>删除全部代码片段</td></tr><tr><td>extension.snippetCraft.createSnipp</td><td>创建代码片段</td></tr><tr><td>extension.snippetCraft.refreshEntry</td><td>刷新代码片段列表</td></tr><tr><td>extension.snippetCraft.addEntry</td><td>添加代码片段</td></tr><tr><td>extension.snippetCraft.editEntry</td><td>编辑代码片段</td></tr><tr><td>extension.snippetCraft.editTitle</td><td>编辑代码片段标题</td></tr><tr><td>extension.snippetCraft.deleteEntry</td><td>删除代码片段</td></tr><tr><td>extension.snippetCraft.insertEntry</td><td>插入代码片段</td></tr><tr><td>extension.snippetCraft.addKv</td><td>添加映射</td></tr><tr><td>extension.snippetCraft.refreshKv</td><td>刷新映射列表</td></tr><tr><td>extension.snippetCraft.deleteKv</td><td>删除映射</td></tr><tr><td>extension.snippetCraft.editKv</td><td>编辑映射</td></tr></tbody></table><h2><span id="创建ui元素">创建UI元素</span></h2><h3><span id="活动栏按钮">活动栏按钮</span></h3><p>点击此按钮将打开VS Code扩展的主边栏视图。图标和名称一般为产品的Logo和名称</p><p>在<code>package.json</code>文件的<code>contributes</code>节点中，添加如下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;viewsContainers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;activitybar&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;snippsView&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Snippet Craft&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;icon&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./logo.svg&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>完成活动栏按钮的添加</p><p><img src="/posts/948f6f1b/644861-20240802231835961-1010755575.png"></p><h3><span id="主边栏视图">主边栏视图</span></h3><p>主边栏中用于直观地列出代码片段的列表和映射表</p><p>在<code>package.json</code>文件的<code>contributes</code>节点中，添加如下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;views&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;snippsView&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view.snippetCraft.snippsView&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Snippets列表&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view.snippetCraft.dictionaryView&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;映射表&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>完成主边栏视图的添加<br><img src="/posts/948f6f1b/644861-20240802231835427-280470917.png"></p><h3><span id="主边栏工具栏按钮">主边栏工具栏按钮</span></h3><p>在<code>package.json</code>文件的<code>contributes</code>节点中，添加如下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;view/title&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.addEntry&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;navigation&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.snippsView&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.refreshEntry&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;navigation&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.snippsView&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.searchSnipps&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;navigation&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.snippsView&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.addKv&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.dictionaryView&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;navigation&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.refreshKv&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.dictionaryView&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;navigation&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">    </span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>完成主边栏工具栏按钮的添加</p><p><img src="/posts/948f6f1b/644861-20240802231835857-2134297475.png"><br><img src="/posts/948f6f1b/644861-20240802231835393-1495199995.png"></p><h3><span id="侧边栏右键菜单">侧边栏右键菜单</span></h3><p>在<code>package.json</code>文件的<code>contributes</code>节点中，添加如下内容：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">&quot;view/item/context&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.editTitle&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;snippet&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.snippsView&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.deleteEntry&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;snippet&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.snippsView&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.insertEntry&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;snippet&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.snippsView&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.editEntry&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;snippet&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.snippsView&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.editKv&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.dictionaryView&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;kveditor&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.deleteKv&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;view == view.snippetCraft.dictionaryView&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;kveditor&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><p>完成侧边栏右键菜单的添加</p><p><img src="/posts/948f6f1b/644861-20240802231835868-225961136.png"></p><h3><span id="编辑器右键菜单">编辑器右键菜单</span></h3><p>在编辑器区域右键弹出的上下文菜单中选择“插入Snippet”，可以选择一个已有的片段插入当前光标所在位置</p><p>当编辑器中有文本被选中时，上下文菜单的“创建Snippet”会显示，点击时选中的文本将作为代码片段被存储。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;menus&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;editor/context&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.createSnipp&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;when&quot;</span><span class="punctuation">:</span> <span class="string">&quot;editorHasSelection&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;snippet&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;extension.snippetCraft.insertSnipps&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;group&quot;</span><span class="punctuation">:</span> <span class="string">&quot;snippet&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><p>完成编辑器右键菜单的添加</p><p><img src="/posts/948f6f1b/644861-20240802231835527-869105135.png"></p><h2><span id="项目地址">项目地址</span></h2><p><a href="https://github.com/jevonsflash/snippet-craft">Github:snippet-craft</a></p>]]></content>
    
    
    <summary type="html">功能，但是创建自定义代码片段时，需要写JSON格式的配置，这些JSON文件在用户文件夹下，没有统一的界面管理，而且对于我来说，制表符补全这样的高级功能并不是必需的。VS Code扩展是一个基础功能，通过扩展可以满足软件的所有功能增强，包括内置的核心功能，如文件管理，搜索，Git，调试器，这些都是通过扩展实现的。VS Code这些框架的部分，官方称之为“容器”，整个VS Code由6个容器组成，分别是：活动栏，主边栏，编辑器，辅边栏，面板，状态栏。常用的项目有侧边栏，编辑器，状态栏，面板上的工具栏区域。</summary>
    
    
    
    <category term="JavaScript" scheme="https://blog.matoapp.net/categories/JavaScript/"/>
    
    <category term="Web" scheme="https://blog.matoapp.net/categories/JavaScript/Web/"/>
    
    
    <category term="插件" scheme="https://blog.matoapp.net/tags/%E6%8F%92%E4%BB%B6/"/>
    
    <category term="IDE" scheme="https://blog.matoapp.net/tags/IDE/"/>
    
    <category term="VS Code" scheme="https://blog.matoapp.net/tags/VS-Code/"/>
    
  </entry>
  
  <entry>
    <title>[MAUI 项目实战] 笔记App（二）：数据库设计</title>
    <link href="https://blog.matoapp.net/posts/3b234df5/"/>
    <id>https://blog.matoapp.net/posts/3b234df5/</id>
    <published>2024-07-19T10:52:00.000Z</published>
    <updated>2025-09-24T16:30:02.414Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#sqlite%E9%85%8D%E7%BD%AE">Sqlite配置</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E5%AE%9E%E4%BD%93">创建实体</a><ul><li><a href="#%E7%AC%94%E8%AE%B0%E5%AE%9E%E4%BD%93%E7%B1%BB">笔记实体类</a></li><li><a href="#%E7%AC%94%E8%AE%B0%E5%88%86%E7%BB%84%E5%AE%9E%E4%BD%93">笔记分组实体</a></li><li><a href="#%E7%AC%94%E8%AE%B0%E7%89%87%E6%AE%B5%E5%AE%9E%E4%BD%93">笔记片段实体</a></li><li><a href="#%E7%AC%94%E8%AE%B0%E7%89%87%E6%AE%B5%E8%B4%9F%E8%BD%BD%E5%AE%9E%E4%BD%93">笔记片段负载实体</a></li><li><a href="#%E7%AC%94%E8%AE%B0%E7%89%87%E6%AE%B5%E4%BB%93%E5%BA%93%E5%AE%9E%E4%BD%93">笔记片段仓库实体</a></li><li><a href="#%E7%AC%94%E8%AE%B0%E6%A8%A1%E6%9D%BF%E5%9C%BA%E6%99%AF%E5%AE%9E%E4%BD%93">笔记模板（场景）实体</a></li><li><a href="#%E7%AC%94%E8%AE%B0%E7%89%87%E6%AE%B5%E6%A8%A1%E6%9D%BF%E5%AE%9E%E4%BD%93">笔记片段模板实体</a></li><li><a href="#%E7%AC%94%E8%AE%B0%E7%89%87%E6%AE%B5%E6%A8%A1%E6%9D%BF%E8%B4%9F%E8%BD%BD%E5%AE%9E%E4%BD%93">笔记片段模板负载实体</a></li></ul></li><li><a href="#%E9%85%8D%E7%BD%AEef">配置EF</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E6%98%A0%E5%B0%84">创建映射</a></li><li><a href="#%E8%BF%81%E7%A7%BB%E5%92%8C%E7%A7%8D%E5%AD%90%E6%95%B0%E6%8D%AE">迁移和种子数据</a></li><li><a href="#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80">项目地址</a></li></ul><!-- tocstop --><h2><span id="sqlite配置">Sqlite配置</span></h2><p>应用程序里使用Sqlite作为数据库，使用EntityFramworkCore作为ORM，使用CodeFirst方式用EFCore初始化Sqlite数据库文件：<code>mato.db</code></p><p>在MatoProductivity.Core项目的<code>appsettings.json</code>中添加本地sqlite连接字符串</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&quot;ConnectionStrings&quot;: &#123;</span><br><span class="line">  &quot;Default&quot;: &quot;Data Source=file:&#123;0&#125;;&quot;</span><br><span class="line">&#125;,</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>这里文件是一个占位符，通过代码hardcode到配置文件</p><p>在MatoProductivityCoreModule.cs中，重写PreInitialize并设置Configuration.DefaultNameOrConnectionString：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">public override void PreInitialize()</span><br><span class="line">&#123;</span><br><span class="line">    LocalizationConfigurer.Configure(Configuration.Localization);</span><br><span class="line"></span><br><span class="line">    Configuration.Settings.Providers.Add&lt;CommonSettingProvider&gt;();</span><br><span class="line"></span><br><span class="line">    string documentsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MatoProductivityConsts.LocalizationSourceName);</span><br><span class="line"></span><br><span class="line">    var configuration = AppConfigurations.Get(documentsPath, development);</span><br><span class="line">    var connectionString = configuration.GetConnectionString(MatoProductivityConsts.ConnectionStringName);</span><br><span class="line"></span><br><span class="line">    var dbName = &quot;mato.db&quot;;</span><br><span class="line">    string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MatoProductivityConsts.LocalizationSourceName, dbName);</span><br><span class="line"></span><br><span class="line">    Configuration.DefaultNameOrConnectionString = String.Format(connectionString, dbPath);</span><br><span class="line">    base.PreInitialize();</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="创建实体">创建实体</span></h2><p>接下来定义实体类</p><h3><span id="笔记实体类">笔记实体类</span></h3><p>笔记用于存储实体，在笔记列表中，每个笔记都有标题和内容，创建时间等内容。</p><p>定义于<code>\MatoProductivity.Core\Models\Entities\Note.cs</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">public class Note : FullAuditedEntity&lt;long&gt;</span><br><span class="line">&#123;</span><br><span class="line">    public Note()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    public Note(string name, bool isHidden, bool isRemovable)</span><br><span class="line">    &#123;</span><br><span class="line">        Title = name;</span><br><span class="line">        IsHidden = isHidden;</span><br><span class="line">        IsRemovable = isRemovable;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]</span><br><span class="line">    public override long Id &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public ICollection&lt;NoteSegment&gt; NoteSegments &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string Title &#123; get; set; &#125;</span><br><span class="line">    public string Type &#123; get; set; &#125;</span><br><span class="line">    public string Status &#123; get; set; &#125;</span><br><span class="line">    public string Desc &#123; get; set; &#125;</span><br><span class="line">    public string Icon &#123; get; set; &#125;</span><br><span class="line">    public string Color &#123; get; set; &#125;</span><br><span class="line">    public string BackgroundColor &#123; get; set; &#125;</span><br><span class="line">    public string BackgroundImage &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string PreViewContent &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsEditable &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsHidden &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsRemovable &#123; get; set; &#125;</span><br><span class="line">    public bool CanSimplified &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="笔记分组实体">笔记分组实体</span></h3><p>定义于<code>\MatoProductivity.Core\Models\Entities\NoteGroup.cs</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">public class NoteGroup : FullAuditedEntity&lt;long&gt;</span><br><span class="line">&#123;</span><br><span class="line">    public NoteGroup()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    public NoteGroup(string name, bool isHidden, bool isRemovable)</span><br><span class="line">    &#123;</span><br><span class="line">        Title = name;</span><br><span class="line">        IsHidden = isHidden;</span><br><span class="line">        IsRemovable = isRemovable;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]</span><br><span class="line">    public override long Id &#123; get; set; &#125;</span><br><span class="line">    public string Title &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsHidden &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsRemovable &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public ICollection&lt;Note&gt; Notes &#123; get; set; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="笔记片段实体">笔记片段实体</span></h3><p>定义于<code>\MatoProductivity.Core\Models\Entities\NoteSegment.cs</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br></pre></td><td class="code"><pre><span class="line">public class NoteSegment : FullAuditedEntity&lt;long&gt;, INoteSegment</span><br><span class="line">&#123;</span><br><span class="line">    public NoteSegment()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]</span><br><span class="line">    public override long Id &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    [ForeignKey(nameof(NoteId))]</span><br><span class="line">    public Note Note &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public ICollection&lt;NoteSegmentPayload&gt; NoteSegmentPayloads &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public long NoteId &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string Title &#123; get; set; &#125;</span><br><span class="line">    public string Type &#123; get; set; &#125;</span><br><span class="line">    public string Status &#123; get; set; &#125;</span><br><span class="line">    public string Desc &#123; get; set; &#125;</span><br><span class="line">    public string Icon &#123; get; set; &#125;</span><br><span class="line">    public string Color &#123; get; set; &#125;</span><br><span class="line">    public int Rank &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsHidden &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsRemovable &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    public INoteSegmentPayload GetNoteSegmentPayload(string key)</span><br><span class="line">    &#123;</span><br><span class="line">        if (NoteSegmentPayloads != null)</span><br><span class="line">        &#123;</span><br><span class="line">            return NoteSegmentPayloads.FirstOrDefault(c =&gt; c.Key == key);</span><br><span class="line">        &#125;</span><br><span class="line">        return default;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    public void SetNoteSegmentPayload(INoteSegmentPayload noteSegmentPayload)</span><br><span class="line">    &#123;</span><br><span class="line">        if (NoteSegmentPayloads != null)</span><br><span class="line">        &#123;</span><br><span class="line">            var currentPayload = NoteSegmentPayloads.FirstOrDefault(c =&gt; c.Key == noteSegmentPayload.Key);</span><br><span class="line">            if (currentPayload != null)</span><br><span class="line">            &#123;</span><br><span class="line">                NoteSegmentPayloads.Remove(currentPayload);</span><br><span class="line">            &#125;</span><br><span class="line">            if (!this.IsTransient())</span><br><span class="line">            &#123;</span><br><span class="line">                (noteSegmentPayload as NoteSegmentPayload).NoteSegmentId = this.Id;</span><br><span class="line">            &#125;</span><br><span class="line">            NoteSegmentPayloads.Add((noteSegmentPayload as NoteSegmentPayload));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public INoteSegmentPayload GetOrSetNoteSegmentPayload(string key, INoteSegmentPayload noteSegmentPayload)</span><br><span class="line">    &#123;</span><br><span class="line">        if (NoteSegmentPayloads != null)</span><br><span class="line">        &#123;</span><br><span class="line">            var currentPayload = NoteSegmentPayloads.FirstOrDefault(c =&gt; c.Key == key);</span><br><span class="line">            if (currentPayload != null)</span><br><span class="line">            &#123;</span><br><span class="line">                return currentPayload;</span><br><span class="line">            &#125;</span><br><span class="line">            if (noteSegmentPayload != null)</span><br><span class="line">            &#123;</span><br><span class="line">                if (!this.IsTransient())</span><br><span class="line">                &#123;</span><br><span class="line">                    (noteSegmentPayload as NoteSegmentPayload).NoteSegmentId = this.Id;</span><br><span class="line">                &#125;</span><br><span class="line">                NoteSegmentPayloads.Add((noteSegmentPayload as NoteSegmentPayload));</span><br><span class="line">            &#125;</span><br><span class="line">            return noteSegmentPayload;</span><br><span class="line">        &#125;</span><br><span class="line">        return noteSegmentPayload;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="笔记片段负载实体">笔记片段负载实体</span></h3><p>笔记片段负载与笔记片段实体为一对多的关系，用于存储笔记片段的详细内容。</p><p>定义于<code>\MatoProductivity.Core\Models\Entities\NoteSegmentPayload.cs</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line">public class NoteSegmentPayload : FullAuditedEntity&lt;long&gt;, INoteSegmentPayload</span><br><span class="line">&#123;</span><br><span class="line">    public NoteSegmentPayload()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    public NoteSegmentPayload(string key, object value, string valuetype = null)</span><br><span class="line">    &#123;</span><br><span class="line">        if (value is string)</span><br><span class="line">        &#123;</span><br><span class="line">            this.SetStringValue((value as string).ToString());</span><br><span class="line">        &#125;</span><br><span class="line">        else if (value is byte[])</span><br><span class="line">        &#123;</span><br><span class="line">            this.Value = value as byte[];</span><br><span class="line">        &#125;</span><br><span class="line">        else if (value is DateTime)</span><br><span class="line">        &#123;</span><br><span class="line">            this.SetStringValue(((DateTime)value).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;));</span><br><span class="line">        &#125;</span><br><span class="line">        else</span><br><span class="line">        &#123;</span><br><span class="line">            this.SetStringValue(value.ToString());</span><br><span class="line">        &#125;</span><br><span class="line">        this.Key = key;</span><br><span class="line">        this.ValueType = valuetype;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]</span><br><span class="line">    public override long Id &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    [ForeignKey(nameof(NoteSegmentId))]</span><br><span class="line">    public NoteSegment NoteSegment &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public long NoteSegmentId &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string Key &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public byte[] Value &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string ValueType &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    [NotMapped]</span><br><span class="line">    public string StringValue =&gt; GetStringValue();</span><br><span class="line"></span><br><span class="line">    public T GetConcreteValue&lt;T&gt;() where T : struct</span><br><span class="line">    &#123;</span><br><span class="line">        var value = Encoding.UTF8.GetString(Value);</span><br><span class="line">        T result = value.To&lt;T&gt;();</span><br><span class="line">        return result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public string GetStringValue()</span><br><span class="line">    &#123;</span><br><span class="line">        var value = Encoding.UTF8.GetString(Value);</span><br><span class="line">        return value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public void SetStringValue(string value)</span><br><span class="line">    &#123;</span><br><span class="line">        this.Value = Encoding.UTF8.GetBytes(value);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="笔记片段仓库实体">笔记片段仓库实体</span></h3><p>用于在编辑笔记页面的添加片段菜单中，加载所有可用的片段</p><p>定义于<code>\MatoProductivity.Core\Models\Entities\NoteSegmentStore.cs</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">public class NoteSegmentStore : Entity&lt;long&gt;</span><br><span class="line">&#123;</span><br><span class="line">    </span><br><span class="line">    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]</span><br><span class="line">    public override long Id &#123; get; set; &#125;</span><br><span class="line">    public string Title &#123; get; set; &#125;</span><br><span class="line">    public string Type &#123; get; set; &#125;</span><br><span class="line">    public string Category &#123; get; set; &#125;</span><br><span class="line">    public string Status &#123; get; set; &#125;</span><br><span class="line">    public string Desc &#123; get; set; &#125;</span><br><span class="line">    public string Icon &#123; get; set; &#125;</span><br><span class="line">    public string Color &#123; get; set; &#125;</span><br><span class="line">    public bool IsHidden &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsRemovable &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="笔记模板场景实体">笔记模板（场景）实体</span></h3><p>定义于<code>\MatoProductivity.Core\Models\Entities\NoteTemplate.cs</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">public class NoteTemplate : FullAuditedEntity&lt;long&gt;</span><br><span class="line">&#123;</span><br><span class="line">    public NoteTemplate()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    public NoteTemplate(string name, bool isHidden, bool isRemovable)</span><br><span class="line">    &#123;</span><br><span class="line">        Title = name;</span><br><span class="line">        IsHidden = isHidden;</span><br><span class="line">        IsRemovable = isRemovable;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]</span><br><span class="line">    public override long Id &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public ICollection&lt;NoteSegmentTemplate&gt; NoteSegmentTemplates &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string Title &#123; get; set; &#125;</span><br><span class="line">    public string Type &#123; get; set; &#125;</span><br><span class="line">    public string Status &#123; get; set; &#125;</span><br><span class="line">    public string Desc &#123; get; set; &#125;</span><br><span class="line">    public string Icon &#123; get; set; &#125;</span><br><span class="line">    public string Color &#123; get; set; &#125;</span><br><span class="line">    public string BackgroundColor &#123; get; set; &#125;</span><br><span class="line">    public string BackgroundImage &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string PreViewContent &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsEditable &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsHidden &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsRemovable &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool CanSimplified &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="笔记片段模板实体">笔记片段模板实体</span></h3><p>定义于<code>\MatoProductivity.Core\Models\Entities\NoteSegmentTemplate.cs</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre></td><td class="code"><pre><span class="line">public class NoteSegmentTemplate : FullAuditedEntity&lt;long&gt;, INoteSegment</span><br><span class="line">&#123;</span><br><span class="line">    public NoteSegmentTemplate()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]</span><br><span class="line">    public override long Id &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    [ForeignKey(nameof(NoteTemplateId))]</span><br><span class="line">    public NoteTemplate NoteTemplate &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public ICollection&lt;NoteSegmentTemplatePayload&gt; NoteSegmentTemplatePayloads &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public long NoteTemplateId &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string Title &#123; get; set; &#125;</span><br><span class="line">    public string Type &#123; get; set; &#125;</span><br><span class="line">    public string Status &#123; get; set; &#125;</span><br><span class="line">    public string Desc &#123; get; set; &#125;</span><br><span class="line">    public string Icon &#123; get; set; &#125;</span><br><span class="line">    public string Color &#123; get; set; &#125;</span><br><span class="line">    public int Rank &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsHidden &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public bool IsRemovable &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    public INoteSegmentPayload GetNoteSegmentPayload(string key)</span><br><span class="line">    &#123;</span><br><span class="line">        if (NoteSegmentTemplatePayloads != null)</span><br><span class="line">        &#123;</span><br><span class="line">            return NoteSegmentTemplatePayloads.FirstOrDefault(c =&gt; c.Key == key);</span><br><span class="line">        &#125;</span><br><span class="line">        return default;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public void SetNoteSegmentPayload(INoteSegmentPayload noteSegmentPayload)</span><br><span class="line">    &#123;</span><br><span class="line">        if (NoteSegmentTemplatePayloads != null)</span><br><span class="line">        &#123;</span><br><span class="line">            var currentPayload = NoteSegmentTemplatePayloads.FirstOrDefault(c =&gt; c.Key == noteSegmentPayload.Key);</span><br><span class="line">            if (currentPayload != null)</span><br><span class="line">            &#123;</span><br><span class="line">                NoteSegmentTemplatePayloads.Remove(currentPayload);</span><br><span class="line">            &#125;</span><br><span class="line">            if (!this.IsTransient())</span><br><span class="line">            &#123;</span><br><span class="line">                (noteSegmentPayload as NoteSegmentTemplatePayload).NoteSegmentTemplateId = this.Id;</span><br><span class="line">            &#125;</span><br><span class="line">            NoteSegmentTemplatePayloads.Add((noteSegmentPayload as NoteSegmentTemplatePayload));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public INoteSegmentPayload GetOrSetNoteSegmentPayload(string key, INoteSegmentPayload noteSegmentPayload)</span><br><span class="line">    &#123;</span><br><span class="line">        if (NoteSegmentTemplatePayloads != null)</span><br><span class="line">        &#123;</span><br><span class="line">            var currentPayload = NoteSegmentTemplatePayloads.FirstOrDefault(c =&gt; c.Key == key);</span><br><span class="line">            if (currentPayload != null)</span><br><span class="line">            &#123;</span><br><span class="line">                return currentPayload;</span><br><span class="line">            &#125;</span><br><span class="line">            if (noteSegmentPayload != null)</span><br><span class="line">            &#123;</span><br><span class="line">                if (!this.IsTransient())</span><br><span class="line">                &#123;</span><br><span class="line">                    (noteSegmentPayload as NoteSegmentTemplatePayload).NoteSegmentTemplateId = this.Id;</span><br><span class="line">                &#125;</span><br><span class="line">                NoteSegmentTemplatePayloads.Add((noteSegmentPayload as NoteSegmentTemplatePayload));</span><br><span class="line">            &#125;</span><br><span class="line">            return noteSegmentPayload;</span><br><span class="line">        &#125;</span><br><span class="line">        return noteSegmentPayload;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="笔记片段模板负载实体">笔记片段模板负载实体</span></h3><p>定义于<code>\MatoProductivity.Core\Models\Entities\NoteSegmentTemplatePayload.cs</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line">public class NoteSegmentTemplatePayload : FullAuditedEntity&lt;long&gt;, INoteSegmentPayload</span><br><span class="line">&#123;</span><br><span class="line">    public NoteSegmentTemplatePayload()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    public NoteSegmentTemplatePayload(string key, object value, string valuetype = null)</span><br><span class="line">    &#123;</span><br><span class="line">        if (value is string)</span><br><span class="line">        &#123;</span><br><span class="line">            this.SetStringValue((value as string).ToString());</span><br><span class="line">        &#125;</span><br><span class="line">        else if (value is byte[])</span><br><span class="line">        &#123;</span><br><span class="line">            this.Value = value as byte[];</span><br><span class="line">        &#125;</span><br><span class="line">        else if (value is DateTime)</span><br><span class="line">        &#123;</span><br><span class="line">            this.SetStringValue(((DateTime)value).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;));</span><br><span class="line">        &#125;</span><br><span class="line">        else</span><br><span class="line">        &#123;</span><br><span class="line">            this.SetStringValue(value.ToString());</span><br><span class="line">        &#125;</span><br><span class="line">        this.Key = key;</span><br><span class="line">        this.ValueType = valuetype;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]</span><br><span class="line">    public override long Id &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    [ForeignKey(nameof(NoteSegmentTemplateId))]</span><br><span class="line">    public NoteSegmentTemplate NoteSegmentTemplate &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public long NoteSegmentTemplateId &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string Key &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public byte[] Value &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public string ValueType &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    [NotMapped]</span><br><span class="line">    public string StringValue =&gt; GetStringValue();</span><br><span class="line"></span><br><span class="line">    public T GetConcreteValue&lt;T&gt;() where T : struct</span><br><span class="line">    &#123;</span><br><span class="line">        var value = Encoding.UTF8.GetString(Value);</span><br><span class="line">        T result = value.To&lt;T&gt;();</span><br><span class="line">        return result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public string GetStringValue()</span><br><span class="line">    &#123;</span><br><span class="line">        var value = Encoding.UTF8.GetString(Value);</span><br><span class="line">        return value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public void SetStringValue(string value)</span><br><span class="line">    &#123;</span><br><span class="line">        this.Value = Encoding.UTF8.GetBytes(value);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="配置ef">配置EF</span></h2><p>数据库上下文对象<code>MatoProductivityDbContext</code>定义如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">public class MatoProductivityDbContext : AbpDbContext</span><br><span class="line">&#123;</span><br><span class="line">    //Add DbSet properties for your entities...</span><br><span class="line"></span><br><span class="line">    public DbSet&lt;Note&gt; Note &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;NoteGroup&gt; NoteGroup &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;NoteSegment&gt; NoteSegment &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;NoteSegmentStore&gt; NoteSegmentStore &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;NoteSegmentPayload&gt; NoteSegmentPayload &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;NoteTemplate&gt; NoteTemplate &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;NoteSegmentTemplate&gt; NoteSegmentTemplate &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;NoteSegmentTemplatePayload&gt; NoteSegmentTemplatePayload &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;Theme&gt; Theme &#123; get; set; &#125;</span><br><span class="line">    public DbSet&lt;Setting&gt; Setting &#123; get; set; &#125;</span><br><span class="line">    public MatoProductivityDbContext(DbContextOptions&lt;MatoProductivityDbContext&gt; options) </span><br><span class="line">        : base(options)</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>MatoProductivity.EntityFrameworkCore是应用程序数据库的维护和管理项目，依赖于Abp.EntityFrameworkCore。<br>在MatoProductivity.EntityFrameworkCore项目中csproj文件中，引用下列包</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;PackageReference Include=&quot;Abp.EntityFrameworkCore&quot; Version=&quot;7.4.0&quot; /&gt;</span><br><span class="line">&lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore&quot; Version=&quot;7.0.0&quot; /&gt;</span><br><span class="line">&lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.Sqlite&quot; Version=&quot;7.0.0&quot; /&gt;</span><br><span class="line">&lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.Sqlite.Design&quot; Version=&quot;1.1.6&quot; /&gt;</span><br><span class="line">&lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.Tools&quot; Version=&quot;7.0.0&quot;&gt;</span><br></pre></td></tr></table></figure><p>在该项目MatoProductivityEntityFrameworkCoreModule.cs 中，将注册上下文对象，并在程序初始化运行迁移，此时将在设备上生成<code>mato.db</code>文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">public override void PostInitialize()</span><br><span class="line">&#123;</span><br><span class="line">    Helper.WithDbContextHelper.WithDbContext&lt;MatoProductivityDbContext&gt;(IocManager, RunMigrate);</span><br><span class="line">    if (!SkipDbSeed)</span><br><span class="line">    &#123;</span><br><span class="line">        SeedHelper.SeedHostDb(IocManager);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">public static void RunMigrate(MatoProductivityDbContext dbContext)</span><br><span class="line">&#123;</span><br><span class="line">    dbContext.Database.Migrate();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2><span id="创建映射">创建映射</span></h2><p>从场景到笔记，或者说从模板到实例，我们需要映射，例如从笔记片段菜单中选择一个片段添加，那么需要从笔记片段仓库实体（NoteSegmentStore）映射到笔记片段实体（NoteSegment）或者，在编辑场景中，映射到笔记片段模板实体（NoteSegmentTemplate）。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">[AutoMapTo(typeof(NoteSegment), typeof(NoteSegmentTemplate))]</span><br><span class="line"></span><br><span class="line">public class NoteSegmentStore : Entity&lt;long&gt;</span><br><span class="line">&#123;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用时：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">var note = ObjectMapper.Map&lt;NoteSegment&gt;(noteSegmentStore);</span><br></pre></td></tr></table></figure><p>ABP框架默认使用<code>AutoMapper</code>进行映射，所以需要配置映射关系。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></td><td class="code"><pre><span class="line">Configuration.Modules.AbpAutoMapper().Configurators.Add(config =&gt;</span><br><span class="line">&#123;</span><br><span class="line">    IgnoreAbpProperties(config.CreateMap&lt;NoteTemplate, Note&gt;()</span><br><span class="line">    .ForMember(</span><br><span class="line">        c =&gt; c.NoteSegments,</span><br><span class="line">        options =&gt; options.MapFrom(input =&gt; input.NoteSegmentTemplates))</span><br><span class="line">      .ForMember(</span><br><span class="line">        c =&gt; c.Id,</span><br><span class="line">        options =&gt; options.Ignore()));</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    IgnoreAbpProperties(config.CreateMap&lt;Note, NoteTemplate&gt;()</span><br><span class="line">       .ForMember(</span><br><span class="line">           c =&gt; c.NoteSegmentTemplates,</span><br><span class="line">           options =&gt; options.MapFrom(input =&gt; input.NoteSegments))</span><br><span class="line">      .ForMember(</span><br><span class="line">        c =&gt; c.Id,</span><br><span class="line">        options =&gt; options.Ignore()));</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    IgnoreAbpProperties(config.CreateMap&lt;NoteSegmentTemplate, NoteSegment&gt;()</span><br><span class="line">    .ForMember(</span><br><span class="line">        c =&gt; c.Note,</span><br><span class="line">        options =&gt; options.MapFrom(input =&gt; input.NoteTemplate))</span><br><span class="line">    .ForMember(</span><br><span class="line">        c =&gt; c.NoteSegmentPayloads,</span><br><span class="line">        options =&gt; options.MapFrom(input =&gt; input.NoteSegmentTemplatePayloads))</span><br><span class="line">     .ForMember(</span><br><span class="line">        c =&gt; c.NoteId,</span><br><span class="line">        options =&gt; options.Ignore())</span><br><span class="line">      .ForMember(</span><br><span class="line">        c =&gt; c.Id,</span><br><span class="line">        options =&gt; options.Ignore()));</span><br><span class="line"></span><br><span class="line">    IgnoreAbpProperties(config.CreateMap&lt;NoteSegmentStore, NoteSegment&gt;()</span><br><span class="line">     .ForMember(</span><br><span class="line">       c =&gt; c.Id,</span><br><span class="line">       options =&gt; options.Ignore()));</span><br><span class="line"></span><br><span class="line">    IgnoreAbpProperties(config.CreateMap&lt;NoteSegment, NoteSegmentTemplate&gt;()</span><br><span class="line">       .ForMember(</span><br><span class="line">        c =&gt; c.NoteTemplate,</span><br><span class="line">        options =&gt; options.MapFrom(input =&gt; input.Note))</span><br><span class="line">       .ForMember(</span><br><span class="line">        c =&gt; c.NoteTemplateId,</span><br><span class="line">        options =&gt; options.Ignore())</span><br><span class="line">       .ForMember(</span><br><span class="line">           c =&gt; c.NoteSegmentTemplatePayloads,</span><br><span class="line">           options =&gt; options.MapFrom(input =&gt; input.NoteSegmentPayloads))</span><br><span class="line">      .ForMember(</span><br><span class="line">        c =&gt; c.Id,</span><br><span class="line">        options =&gt; options.Ignore()));</span><br><span class="line"></span><br><span class="line">    IgnoreAbpProperties(config.CreateMap&lt;NoteSegmentTemplatePayload, NoteSegmentPayload&gt;()</span><br><span class="line">       .ForMember(</span><br><span class="line">           c =&gt; c.NoteSegment,</span><br><span class="line">           options =&gt; options.MapFrom(input =&gt; input.NoteSegmentTemplate))</span><br><span class="line">       .ForMember(</span><br><span class="line">        c =&gt; c.NoteSegmentId,</span><br><span class="line">        options =&gt; options.Ignore())</span><br><span class="line"></span><br><span class="line">      .ForMember(</span><br><span class="line">        c =&gt; c.Id,</span><br><span class="line">        options =&gt; options.Ignore()));</span><br><span class="line"></span><br><span class="line">    IgnoreAbpProperties(</span><br><span class="line">    config.CreateMap&lt;NoteSegmentPayload, NoteSegmentTemplatePayload&gt;()</span><br><span class="line">       .ForMember(</span><br><span class="line">           c =&gt; c.NoteSegmentTemplate,</span><br><span class="line">           options =&gt; options.MapFrom(input =&gt; input.NoteSegment))</span><br><span class="line">       .ForMember(</span><br><span class="line">        c =&gt; c.NoteSegmentTemplateId,</span><br><span class="line">        options =&gt; options.Ignore()));</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2><span id="迁移和种子数据">迁移和种子数据</span></h2><p><code>MatoProductivity.EntityFrameworkCore.Seed.SeedHelper</code>可在程序启动时，访问数据库，并初始化种子数据。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">public override void PostInitialize()</span><br><span class="line">&#123;</span><br><span class="line">    Helper.WithDbContextHelper.WithDbContext&lt;MatoProductivityDbContext&gt;(IocManager, RunMigrate);</span><br><span class="line">    if (!SkipDbSeed)</span><br><span class="line">    &#123;</span><br><span class="line">        SeedHelper.SeedHostDb(IocManager);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它通过<code>SkipDbSeed</code>来决定是否跳过执行种子数据初始化。我们需要在安装完成App后第一次运行才执行种子数据初始化。</p><p>MAUI中提供了<code>VersionTracking.Default.IsFirstLaunchEver</code>方式获取是否是第一次在此设备上启动应用，请查看<a href="https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.maui.applicationmodel.iversiontracking.isfirstlaunchever">官方文档</a></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">public override async void Initialize()</span><br><span class="line">&#123;</span><br><span class="line">    IocManager.RegisterAssemblyByConvention(typeof(MatoProductivityModule).GetAssembly());</span><br><span class="line">    if (VersionTracking.Default.IsFirstLaunchEver)</span><br><span class="line">    &#123;</span><br><span class="line">        MatoProductivityEntityFrameworkCoreModule.SkipDbSeed = false;</span><br><span class="line">    &#125;</span><br><span class="line">    else</span><br><span class="line">    &#123;</span><br><span class="line">        MatoProductivityEntityFrameworkCoreModule.SkipDbSeed = true;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在InitialDbBuilder中我们定义了大多数的业务初始数据，具体的实现方式请查阅源码。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">internal void Create()</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">    CreateSetting(&quot;Theme&quot;, &quot;Light&quot;);</span><br><span class="line">    CreateSetting(&quot;DetailPageMode&quot;, &quot;PreviewPage&quot;);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    CreateNoteSegmentStore(&quot;时间戳&quot;, &quot;时间/提醒&quot;, &quot;DateTimeSegment&quot;, &quot;记录一个瞬时时间&quot;, FaIcons.IconClockO, &quot;#D8292B&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;计时器&quot;, &quot;时间/提醒&quot;, &quot;TimerSegment&quot;, &quot;创建一个计时器，当它结束时会通知您&quot;, FaIcons.IconBell, &quot;#D8292B&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;笔记&quot;, &quot;文本&quot;, &quot;TextSegment&quot;, &quot;随时用文本记录您的想法&quot;, FaIcons.IconStickyNoteO, &quot;#E1A08B&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;Todo&quot;, &quot;文本&quot;, &quot;TodoSegment&quot;, &quot;记录一个Todo项目&quot;, FaIcons.IconCheckSquareO, &quot;#E1A08B&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;数值&quot;, &quot;文本&quot;, &quot;KeyValueSegment&quot;, &quot;记录数值，以便统计数据&quot;, FaIcons.IconLineChart, &quot;#E1A08B&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;手绘&quot;, &quot;文件&quot;, &quot;ScriptSegment&quot;, &quot;创建一个手绘&quot;, FaIcons.IconPaintBrush, &quot;#AD9CC2&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;照片/视频&quot;, &quot;文件&quot;, &quot;MediaSegment&quot;, &quot;拍照或摄像&quot;, FaIcons.IconCamera, &quot;#AD9CC2&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;文档&quot;, &quot;文件&quot;, &quot;DocumentSegment&quot;, &quot;从您设备中选取一个文档&quot;, FaIcons.IconFile, &quot;#AD9CC2&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;录音&quot;, &quot;文件&quot;, &quot;VoiceSegment&quot;, &quot;记录一段声音&quot;, FaIcons.IconMicrophone, &quot;#AD9CC2&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;地点&quot;, &quot;其它&quot;, &quot;LocationSegment&quot;, &quot;获取当前地点，或者从地图上选取一个地点&quot;, FaIcons.IconMapMarker, &quot;#6D987C&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;天气&quot;, &quot;其它&quot;, &quot;WeatherSegment&quot;, &quot;获取当前天气信息&quot;, FaIcons.IconCloud, &quot;#6D987C&quot;);</span><br><span class="line">    CreateNoteSegmentStore(&quot;联系人&quot;, &quot;其它&quot;, &quot;ContactSegment&quot;, &quot;从您设备的通讯录中选择一个联系人&quot;, FaIcons.IconUser, &quot;#6D987C&quot;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2><span id="项目地址">项目地址</span></h2><p><a href="https://github.com/jevonsflash/MatoProductivity">GitHub:MatoProductivity</a></p>]]></content>
    
    
    <summary type="html">从场景到笔记，或者说从模板到实例，我们需要映射，例如从笔记片段菜单中选择一个片段添加，那么需要从笔记片段仓库实体（NoteSegmentStore）映射到笔记片段实体（NoteSegment）或者，在编辑场景中，映射到笔记片段模板实体（NoteSegmentTemplate）。在该项目MatoProductivityEntityFrameworkCoreModule.cs 中，将注册上下文对象，并在程序初始化运行迁移，此时将在设备上生成。可在程序启动时，访问数据库，并初始化种子数据。</summary>
    
    
    
    <category term=".NET MAUI" scheme="https://blog.matoapp.net/categories/NET-MAUI/"/>
    
    <category term="产品设计" scheme="https://blog.matoapp.net/categories/NET-MAUI/%E4%BA%A7%E5%93%81%E8%AE%BE%E8%AE%A1/"/>
    
    <category term="移动开发" scheme="https://blog.matoapp.net/categories/NET-MAUI/%E4%BA%A7%E5%93%81%E8%AE%BE%E8%AE%A1/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="数据库" scheme="https://blog.matoapp.net/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="Xamarin" scheme="https://blog.matoapp.net/tags/Xamarin/"/>
    
    <category term="MAUI" scheme="https://blog.matoapp.net/tags/MAUI/"/>
    
  </entry>
  
  <entry>
    <title>[MAUI 项目实战] 笔记App（一）：介绍与程序设计</title>
    <link href="https://blog.matoapp.net/posts/e3d2e51e/"/>
    <id>https://blog.matoapp.net/posts/e3d2e51e/</id>
    <published>2024-07-18T16:37:00.000Z</published>
    <updated>2025-09-24T16:30:02.406Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E5%89%8D%E8%A8%80">前言</a></li><li><a href="#%E8%BD%AF%E4%BB%B6%E8%A7%84%E6%A0%BC%E8%AF%B4%E6%98%8E">软件规格说明</a><ul><li><a href="#%E4%B8%BB%E8%A6%81%E5%8A%9F%E8%83%BD">主要功能</a></li><li><a href="#%E8%BF%90%E8%A1%8C%E7%8E%AF%E5%A2%83">运行环境</a></li><li><a href="#%E5%BC%80%E6%BA%90%E7%BB%84%E4%BB%B6%E5%BA%93">开源组件库</a></li><li><a href="#%E8%AE%BE%E5%A4%87%E6%9D%83%E9%99%90">设备权限</a></li><li><a href="#%E7%AC%AC%E4%B8%89%E6%96%B9%E6%9C%8D%E5%8A%A1">第三方服务</a></li></ul></li><li><a href="#%E4%BD%BF%E7%94%A8%E4%BB%8B%E7%BB%8D">使用介绍</a><ul><li><a href="#%E5%9C%BA%E6%99%AF%E5%88%97%E8%A1%A8%E9%A1%B5">场景列表页</a><ul><li><a href="#%E4%BB%8E%E5%9C%BA%E6%99%AF%E5%88%9B%E5%BB%BA%E7%AC%94%E8%AE%B0">从场景创建笔记</a></li><li><a href="#%E7%BC%96%E8%BE%91%E5%9C%BA%E6%99%AF">编辑场景</a></li><li><a href="#%E6%96%B0%E5%A2%9E%E5%9C%BA%E6%99%AF%E7%89%87%E6%AE%B5">新增场景片段</a></li><li><a href="#%E7%BC%96%E8%BE%91%E5%9C%BA%E6%99%AF%E7%89%87%E6%AE%B5">编辑场景片段</a></li><li><a href="#%E5%88%A0%E9%99%A4%E5%9C%BA%E6%99%AF%E7%89%87%E6%AE%B5">删除场景片段</a></li><li><a href="#%E5%88%A0%E9%99%A4%E5%9C%BA%E6%99%AF">删除场景</a></li></ul></li><li><a href="#%E7%AC%94%E8%AE%B0%E5%88%97%E8%A1%A8%E9%A1%B5">笔记列表页</a><ul><li><a href="#%E5%88%9B%E5%BB%BA%E6%96%B0%E7%AC%94%E8%AE%B0">创建新笔记</a></li><li><a href="#%E7%BC%96%E8%BE%91%E7%AC%94%E8%AE%B0">编辑笔记</a></li><li><a href="#%E6%96%B0%E5%A2%9E%E7%AC%94%E8%AE%B0%E7%89%87%E6%AE%B5">新增笔记片段</a></li><li><a href="#%E7%BC%96%E8%BE%91%E7%AC%94%E8%AE%B0%E7%89%87%E6%AE%B5">编辑笔记片段</a></li><li><a href="#%E5%88%A0%E9%99%A4%E7%AC%94%E8%AE%B0%E7%89%87%E6%AE%B5">删除笔记片段</a></li><li><a href="#%E5%88%A0%E9%99%A4%E7%AC%94%E8%AE%B0">删除笔记</a></li></ul></li><li><a href="#%E7%BB%9F%E8%AE%A1">统计</a></li><li><a href="#%E5%85%B6%E5%AE%83%E8%AE%BE%E7%BD%AE">其它设置</a><ul><li><a href="#pin%E5%88%B0%E5%BF%AB%E6%8D%B7%E6%96%B9%E5%BC%8F">PIN到快捷方式</a></li><li><a href="#%E6%8D%A2%E8%82%A4%E5%8A%9F%E8%83%BD">换肤功能</a></li></ul></li></ul></li><li><a href="#%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1">程序设计</a><ul><li><a href="#%E6%A1%86%E6%9E%B6">框架</a></li><li><a href="#%E5%AE%9A%E4%B9%89">定义</a></li><li><a href="#%E6%A0%B8%E5%BF%83%E7%B1%BB">核心类</a></li></ul></li><li><a href="#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80">项目地址</a></li></ul><!-- tocstop --><p>系列文章：</p><p><a href="https://www.cnblogs.com/jevonsflash/p/18309871">[MAUI 项目实战] 笔记App（一）：介绍与程序设计</a><br><a href="https://www.cnblogs.com/jevonsflash/p/18311048">[MAUI 项目实战] 笔记App（二）：数据库设计</a></p><h1><span id="前言">前言</span></h1><p>有人说现在记事类app这么多，市场这么卷，为什么还想做一个笔记类App？</p><p>一来，去年小孩刚出生，需要一个可以记录喂奶时间的app，发现市面上没有一款app能够在两步内简单记录一个时间，可能iOS可以通过备忘录配合捷径做到快速记录，但是安卓上就没有类似的app。</p><p>二是，自去年做的音乐播放器以来，很长一段时间我在博客上的XF，MAUI都是在介绍局部的功能，[MAUI 项目实战]专题也很长没更新了，这次通过笔记类App做一次完整项目，包括如何上架MAUI应用等内容一并更新了。</p><p>一开始用MAUI简单做了一个功能，就记录喂奶量 + 喂奶时间 + 提醒。后期逐步做成一个可以动态添加摘要片段的功能。取名为《凡事摘要》。</p><p>目前安卓版本已发布打包，并上架腾讯应用商城：<a href="https://sj.qq.com/appdetail/com.mato.matoproductivity">凡事摘要</a>，iOS版本的部分页面还有问题需要调试，最近比较忙，我会抽出时间继续完善。在这个时间点我决定把App所有的代码都放在GitHub上，方便大家学习。也欢迎有兴趣的小伙伴一起参与。</p><h1><span id="软件规格说明">软件规格说明</span></h1><h2><span id="主要功能">主要功能</span></h2><ul><li>笔记管理：新增笔记，编辑笔记标题，增加笔记片段，修改笔记片段内容，删除笔记等功能。</li><li>场景管理：新增场景，编辑场景标题，封面等，增加场景片段，修改场景片段内容，删除场景等功能。</li><li>数据统计：查看统计数据。</li><li>应用快捷入口管理：增加或删除场景快捷方式。</li><li>其它设置：换肤，是否显示预览页面等</li></ul><h2><span id="运行环境">运行环境</span></h2><p> 运行环境为Android 10.0及以上版本，ARM架构CPU，四核2.0GHz， 4GB内存，16GB存储空间，5.5英寸及以上LCD显示屏的Android设备。</p><h2><span id="开源组件库">开源组件库</span></h2><table><thead><tr><th>组件库名称</th></tr></thead><tbody><tr><td>.NET Runtime</td></tr><tr><td>C#&#x2F;WinRT</td></tr><tr><td>Xamarin.Forms</td></tr><tr><td>ASP.NET Core Blazor</td></tr><tr><td>AutoMapper</td></tr><tr><td>ASP.NET Boilerplate (Abp)</td></tr><tr><td>CommunityToolkit.Maui</td></tr><tr><td>Castle.Windsor</td></tr><tr><td>Newtonsoft.Json</td></tr><tr><td>Microsoft.EntityFrameworkCore</td></tr></tbody></table><h2><span id="设备权限">设备权限</span></h2><table><thead><tr><th>权限名称</th><th>使用目的</th><th>使用场景</th></tr></thead><tbody><tr><td>设备GPS传感器信息</td><td>获取当前地理位置用于地址解析</td><td>创建笔记</td></tr><tr><td>设读取设备或存储卡中的内容</td><td>上传图片时获取用户相册图片文件</td><td>APP前台运行时</td></tr><tr><td>相机</td><td>上传图片时通过设备相机获取图片</td><td>创建笔记</td></tr><tr><td>读取联系人</td><td>创建联系人片段时读取设备联系人信息</td><td>创建笔记</td></tr><tr><td>录制音频</td><td>创建声音片段时通过麦克风录制音频</td><td>创建笔记</td></tr></tbody></table><h2><span id="第三方服务">第三方服务</span></h2><table><thead><tr><th>服务名称</th><th>服务资源</th><th>服务内容</th><th>链接</th></tr></thead><tbody><tr><td>百度智能云</td><td>用户创建的音频</td><td>提供语音转文字服务</td><td><a href="https://cloud.baidu.com/">https://cloud.baidu.com/</a></td></tr><tr><td>和风天气</td><td>用户当前地理位置</td><td>提供本地天气信息</td><td><a href="https://www.qweather.com/">https://www.qweather.com</a></td></tr><tr><td>高德开放平台</td><td>用户当前地理位置</td><td>提供当前地理位置的地址解析服务</td><td><a href="https://lbs.amap.com/">https://lbs.amap.com/</a></td></tr></tbody></table><h1><span id="使用介绍">使用介绍</span></h1><h2><span id="场景列表页">场景列表页</span></h2><p>场景列表页用于呈现所有场景，软件默认会有一些场景，用户可以添加新的场景，也可以删除场景。<br><img src="/posts/e3d2e51e/644861-20240721143344194-247406536.png"></p><h3><span id="从场景创建笔记">从场景创建笔记</span></h3><p>用户在“场景”页面点击场景列表中的条目，可创建对应场景的笔记。如在点击“喂奶”场景，软件会创建一条笔记，并跳转到编辑页面，点击“保存”按钮，在笔记列表中会呈现此笔记。</p><p><img src="/posts/e3d2e51e/644861-20240721143344199-973050385.png"></p><h3><span id="编辑场景">编辑场景</span></h3><p>编辑场景可对现有场景进行修改，包括修改场景名称，封面，增加、删除、修改场景片段。<br>点击“…”按钮，打开编辑场景页面，编辑完成后，点击“保存”按钮完成编辑</p><p><img src="/posts/e3d2e51e/644861-20240721143344233-713920861.png"></p><h3><span id="新增场景片段">新增场景片段</span></h3><p>在编辑场景页面，点击“+”按钮，可新增场景片段。如在弹出的片段列表中，点击“笔记”</p><p><img src="/posts/e3d2e51e/644861-20240721143344202-1638628239.png"></p><h3><span id="编辑场景片段">编辑场景片段</span></h3><p>在编辑场景页面，点击片段标题可修改标题内容，按住片段右侧的按钮可拖拽，在其它位置释放手指可完成排序。</p><p><img src="/posts/e3d2e51e/644861-20240721143344079-1853649028.png"></p><h3><span id="删除场景片段">删除场景片段</span></h3><p>在编辑场景页面，向左滑动片段，点击“删除”按钮，即可完成片段删除</p><p><img src="/posts/e3d2e51e/644861-20240721143344052-12576903.png"></p><h3><span id="删除场景">删除场景</span></h3><p>在编辑场景页面，点击“删除”按钮，或在场景列表页面，长按场景条目，点击“-”按钮，即可完成场景删除</p><p><img src="/posts/e3d2e51e/644861-20240721143344183-684913954.png"></p><h2><span id="笔记列表页">笔记列表页</span></h2><p>笔记列表页用于呈现所有笔记，用户可以添加新的笔记，也可以编辑或删除现有的笔记。</p><p><img src="/posts/e3d2e51e/644861-20240721143344148-1834755196.png"></p><h3><span id="创建新笔记">创建新笔记</span></h3><p>在笔记列表页面点击“+”按钮，可创建一条新的笔记。此时，软件会跳转到编辑页面，点击“保存”按钮，在笔记列表中会呈现此笔记。</p><p><img src="/posts/e3d2e51e/644861-20240721143344240-429113863.png"></p><h3><span id="编辑笔记">编辑笔记</span></h3><p>编辑笔记可对现有笔记进行修改，包括修改笔记标题，增加、删除、修改笔记片段。<br>在笔记列表页中，向左滑动笔记条目，点击“编辑”按钮，打开编辑笔记页面，编辑完成后，点击“保存”按钮完成编辑</p><p><img src="/posts/e3d2e51e/644861-20240721143344251-1657340453.png"></p><h3><span id="新增笔记片段">新增笔记片段</span></h3><p>在编辑笔记页面，点击“+”按钮，可新增笔记片段。如在弹出的片段列表中，点击“笔记”</p><p><img src="/posts/e3d2e51e/644861-20240721143344254-1272912372.png"></p><h3><span id="编辑笔记片段">编辑笔记片段</span></h3><p>在编辑笔记页面，点击片段标题可修改标题内容，按住片段右侧的按钮可拖拽，在其它位置释放手指可完成排序。</p><p><img src="/posts/e3d2e51e/644861-20240721143344236-1001648990.png"></p><h3><span id="删除笔记片段">删除笔记片段</span></h3><p>在编辑笔记页面，向左滑动片段，点击“删除”按钮，即可完成片段删除</p><p><img src="/posts/e3d2e51e/644861-20240721143344256-642215871.png"></p><h3><span id="删除笔记">删除笔记</span></h3><p>在笔记列表页向左滑动笔记条目，点击“删除”按钮，或在编辑笔记页面，点击“删除”按钮，即可完成笔记删除</p><p><img src="/posts/e3d2e51e/644861-20240721143344085-612330811.png"></p><h2><span id="统计">统计</span></h2><p>笔记中的数值片段中的数值，可以进行统计。<br>在笔记编辑页面，添加一个数值片段，输入数值并保存，切换到统计页面，可以看到这些数值为纵轴、时间为横轴的趋势图</p><p><img src="/posts/e3d2e51e/644861-20240721143344188-1407053508.png"></p><h2><span id="其它设置">其它设置</span></h2><h3><span id="pin到快捷方式">PIN到快捷方式</span></h3><p>对于符合“快速创建”的场景，用户可以在“场景”页面，点击“PIN到快捷方式”按钮，将场景添加到桌面快捷方式，方便用户快速创建笔记。</p><p><img src="/posts/e3d2e51e/644861-20240721143344284-537709312.png"></p><h3><span id="换肤功能">换肤功能</span></h3><p>软件支持换肤功能，用户可以在“我的”页面，点击“黑暗”、“明亮”或“自动”按钮切换软件主题。</p><p><img src="/posts/e3d2e51e/644861-20240721143344255-1805735805.png"></p><h1><span id="程序设计">程序设计</span></h1><h2><span id="框架">框架</span></h2><p>使用Abp框架，我之前写过如何 <a href="https://www.cnblogs.com/jevonsflash/p/16310387.html">将Abp移植进.NET MAUI项目</a>，本项目也是按照这篇博文完成项目搭建。</p><p>这次的项目，主要通过原型和工厂模式建设基于模板的笔记内容。</p><p>没有使用过多的跨平台特性，如果需要了解更多MAUI跨平台知识，请参考之前<a href="https://www.cnblogs.com/jevonsflash/p/17113139.html">音乐播放器系列文章</a>。</p><h2><span id="定义">定义</span></h2><ul><li>Note - 笔记，可以成整页打开的内容</li><li>NoteTemplate - 笔记模板，或称为场景，是可以快速创建笔记的模板</li><li>NoteSegment - 笔记片段，它是一个笔记（Note）的组成</li><li>NoteSegmentTemplate - 笔记片段模板，对应场景中可快速创建笔记片段的模板</li><li>NoteSegmentPayload - 笔记片段负载，存储具体笔记片段的内容</li><li>NoteSegmentService - 笔记片段服务类，为笔记片段，或笔记片段模板提供增删改等具体的业务逻辑</li><li>NoteSegmentServiceFactory - 笔记片段服务工厂，为笔记片段服务类提供工厂方法</li></ul><h2><span id="核心类">核心类</span></h2><p>INoteSegment：它是笔记片段的抽象类，模板类NoteSegmentTemplate和笔记片段类NoteSegment都实现了INoteSegment</p><p><img src="/posts/e3d2e51e/644861-20240721143344100-1624374133.png"></p><p>它包含了笔记片段的属性，如标题、颜色、图标、是否隐藏、是否可删除、排序、状态、类型等。同时它关联一个笔记片段负载类INoteSegmentPayload</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">public interface INoteSegment</span><br><span class="line">&#123;</span><br><span class="line">    string Color &#123; get; set; &#125;</span><br><span class="line">    string Desc &#123; get; set; &#125;</span><br><span class="line">    string Icon &#123; get; set; &#125;</span><br><span class="line">    bool IsHidden &#123; get; set; &#125;</span><br><span class="line">    bool IsRemovable &#123; get; set; &#125;</span><br><span class="line">    int Rank &#123; get; set; &#125;</span><br><span class="line">    string Status &#123; get; set; &#125;</span><br><span class="line">    string Title &#123; get; set; &#125;</span><br><span class="line">    string Type &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    INoteSegmentPayload GetNoteSegmentPayload(string key);</span><br><span class="line">    INoteSegmentPayload GetOrSetNoteSegmentPayload(string key, INoteSegmentPayload noteSegmentPayload);</span><br><span class="line">    void SetNoteSegmentPayload(INoteSegmentPayload noteSegmentPayload);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>INoteSegementService：凡事摘要拥有不同的笔记类型，如：时间戳片段，文本片段，文件片段等，App中可以通过添加片段按钮查看所有类型。</p><p><img src="/posts/e3d2e51e/644861-20240721143344137-2128060649.png"></p><p>这些片段通过片段服务类（NoteSegementService）来描述该如何存储，使用Payload中的数据。</p><p>不同的片段类型，通过不同的片段服务类来实现。比如，在时间戳片段中，我们要存储当前时间和计算倒计时，而文件片段中，我们要存储文件路径，文件名，文件大小，文件类型等信息。</p><p>这些都是通过片段服务类来实现的。</p><p><img src="/posts/e3d2e51e/644861-20240721143344113-933742018.png"></p><p>具体类型如下：</p><table><thead><tr><th>类型</th><th>描述</th></tr></thead><tbody><tr><td>DateTimeSegmentService</td><td>时间戳片段服务类</td></tr><tr><td>KeyValueSegmentService</td><td>数值片段服务类</td></tr><tr><td>FileSegmentService</td><td>文件片段服务类</td></tr><tr><td>TextSegmentService</td><td>文本片段服务类</td></tr><tr><td>TodoSegmentService</td><td>待办片段服务类</td></tr><tr><td>WeatherSegmentService</td><td>天气片段服务类</td></tr><tr><td>LocationSegmentService</td><td>地点片段服务类</td></tr><tr><td>TimerSegmentService</td><td>闹钟片段服务类</td></tr><tr><td>ContactSegmentService</td><td>联系人片段服务类</td></tr><tr><td>VoiceSegmentService</td><td>录音片段服务类</td></tr><tr><td>MediaSegmentService</td><td>媒体片段服务类</td></tr><tr><td>ScriptSegmentService</td><td>绘制片段服务类</td></tr><tr><td>DocumentSegmentService</td><td>文件片段服务类</td></tr></tbody></table><p>片段服务类包含了一个INoteSegment，它是当前的笔记片段对象</p><p><img src="/posts/e3d2e51e/644861-20240721143344091-1162869154.png"></p><p>INoteSegmentServiceFactory:</p><p>片段服务类的工厂类，除此之外还有一个INoteSegmentTemplateServiceFactory，他们都是根据笔记片段，或者笔记模板中的片段类型创建对应的片段服务类。</p><p><img src="/posts/e3d2e51e/644861-20240721143344098-2011269119.png"></p><p>用于笔记的片段服务类的工厂类:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">public interface INoteSegmentServiceFactory: ISingletonDependency</span><br><span class="line">&#123;</span><br><span class="line">    INoteSegmentService GetNoteSegmentService(NoteSegment noteSegment);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用于笔记模板的片段服务类的工厂类</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">public interface INoteSegmentTemplateServiceFactory: ISingletonDependency</span><br><span class="line">&#123;</span><br><span class="line">    INoteSegmentService GetNoteSegmentService(NoteSegmentTemplate noteSegmentTemplate);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>NoteSegmentService作为笔记片段服务的基类，它继承了<code>ViewModelBase</code>，实际上服务基类是各笔记片段视图层的ViewModel，视图界面元素通过绑定服务类中的属性来显示或更新数据。</p><p><img src="/posts/e3d2e51e/644861-20240721143344075-867092582.png"></p><p>每一种服务类都对应的一个视图。渲染时，Xaml通过NoteSegmentDataTemplateSelector模板选择器来选择对应的视图。有关界面部分将在另一篇文章介绍。</p><h1><span id="项目地址">项目地址</span></h1><p><strong>使用和发布请遵循<a href="https://www.gnu.org/licenses/gpl-3.0.html">GPL-3.0 license</a>许可，请勿用于商业用途。</strong></p><p><a href="https://github.com/jevonsflash/MatoProductivity">GitHub:MatoProductivity</a></p>]]></content>
    
    
    <summary type="html">有人说现在记事类app这么多，市场这么卷，为什么还想做一个笔记类App？一来，去年小孩刚出生，需要一个可以记录喂奶时间的app，发现市面上没有一款app能够在两步内简单记录一个时间，可能iOS可以通过备忘录配合捷径做到快速记录，但是安卓上就没有类似的app。二是，自去年做的音乐播放器以来，很长一段时间我在博客上的XF，MAUI都是在介绍局部的功能，[MAUI 项目实战]专题也很长没更新了，这次通过笔记类App做一次完整项目，包括如何上架MAUI应用等内容一并更新了。</summary>
    
    
    
    <category term=".NET MAUI" scheme="https://blog.matoapp.net/categories/NET-MAUI/"/>
    
    <category term="产品设计" scheme="https://blog.matoapp.net/categories/NET-MAUI/%E4%BA%A7%E5%93%81%E8%AE%BE%E8%AE%A1/"/>
    
    <category term="移动开发" scheme="https://blog.matoapp.net/categories/NET-MAUI/%E4%BA%A7%E5%93%81%E8%AE%BE%E8%AE%A1/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="Xamarin" scheme="https://blog.matoapp.net/tags/Xamarin/"/>
    
    <category term="MAUI" scheme="https://blog.matoapp.net/tags/MAUI/"/>
    
    <category term="App" scheme="https://blog.matoapp.net/tags/App/"/>
    
    <category term=".net blazor" scheme="https://blog.matoapp.net/tags/net-blazor/"/>
    
  </entry>
  
  <entry>
    <title>OAuth 2.0 的四种方式</title>
    <link href="https://blog.matoapp.net/posts/eaf19731/"/>
    <id>https://blog.matoapp.net/posts/eaf19731/</id>
    <published>2024-05-12T22:49:23.000Z</published>
    <updated>2025-09-24T16:30:02.358Z</updated>
    
    <content type="html"><![CDATA[<h2><span id="rfc-6749">RFC 6749</span></h2><p>OAuth 2.0 的标准是 <a href="https://tools.ietf.org/html/rfc6749">RFC 6749</a> 文件。该文件先解释了 OAuth 是什么。</p><blockquote><p>OAuth 引入了一个授权层，用来分离两种不同的角色：客户端和资源所有者。……资源所有者同意以后，资源服务器可以向客户端颁发令牌。客户端通过令牌，去请求数据。</p></blockquote><p>这段话的意思就是，<strong>OAuth 的核心就是向第三方应用颁发令牌。</strong>然后，RFC 6749 接着写道：</p><blockquote><p>（由于互联网有多种场景，）本标准定义了获得令牌的四种授权方式（authorization grant ）。</p></blockquote><p>也就是说，<strong>OAuth 2.0 规定了四种获得令牌的流程。你可以选择最适合自己的那一种，向第三方应用颁发令牌。</strong>下面就是这四种授权方式。</p><blockquote><ul><li>授权码（authorization-code）</li><li>隐藏式（implicit）</li><li>密码式（password）：</li><li>客户端凭证（client credentials）</li></ul></blockquote><p>注意，不管哪一种授权方式，第三方应用申请令牌之前，都必须先到系统备案，说明自己的身份，然后会拿到两个身份识别码：客户端 ID（client ID）和客户端密钥（client secret）。这是为了防止令牌被滥用，没有备案过的第三方应用，是不会拿到令牌的。</p><h2><span id="第一种授权方式授权码">第一种授权方式：授权码</span></h2><p><strong>授权码（authorization code）方式，指的是第三方应用先申请一个授权码，然后再用该码获取令牌。</strong></p><p>这种方式是最常用的流程，安全性也最高，它适用于那些有后端的 Web 应用。授权码通过前端传送，令牌则是储存在后端，而且所有与资源服务器的通信都在后端完成。这样的前后端分离，可以避免令牌泄漏。</p><p>第一步，A 网站提供一个链接，用户点击后就会跳转到 B 网站，授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">https</span>:<span class="comment">//b.com/oauth/authorize?</span></span><br><span class="line">  response_type=code&amp;</span><br><span class="line">  client_id=<span class="variable constant_">CLIENT_ID</span>&amp;</span><br><span class="line">  redirect_uri=<span class="variable constant_">CALLBACK_URL</span>&amp;</span><br><span class="line">  scope=read</span><br></pre></td></tr></table></figure></blockquote><p>上面 URL 中，<code>response_type</code>参数表示要求返回授权码（<code>code</code>），<code>client_id</code>参数让 B 知道是谁在请求，<code>redirect_uri</code>参数是 B 接受或拒绝请求后的跳转网址，<code>scope</code>参数表示要求的授权范围（这里是只读）。</p><p><img src="/posts/eaf19731/bg2019040902.jpg"></p><p>第二步，用户跳转后，B 网站会要求用户登录，然后询问是否同意给予 A 网站授权。用户表示同意，这时 B 网站就会跳回<code>redirect_uri</code>参数指定的网址。跳转时，会传回一个授权码，就像下面这样。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">https</span>:<span class="comment">//a.com/callback?code=AUTHORIZATION_CODE</span></span><br></pre></td></tr></table></figure></blockquote><p>上面 URL 中，<code>code</code>参数就是授权码。</p><p><img src="/posts/eaf19731/bg2019040907.jpg"></p><p>第三步，A 网站拿到授权码以后，就可以在后端，向 B 网站请求令牌。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">https</span>:<span class="comment">//b.com/oauth/token?</span></span><br><span class="line"> client_id=<span class="variable constant_">CLIENT_ID</span>&amp;</span><br><span class="line"> client_secret=<span class="variable constant_">CLIENT_SECRET</span>&amp;</span><br><span class="line"> grant_type=authorization_code&amp;</span><br><span class="line"> code=<span class="variable constant_">AUTHORIZATION_CODE</span>&amp;</span><br><span class="line"> redirect_uri=<span class="variable constant_">CALLBACK_URL</span></span><br></pre></td></tr></table></figure></blockquote><p>上面 URL 中，<code>client_id</code>参数和<code>client_secret</code>参数用来让 B 确认 A 的身份（<code>client_secret</code>参数是保密的，因此只能在后端发请求），<code>grant_type</code>参数的值是<code>AUTHORIZATION_CODE</code>，表示采用的授权方式是授权码，<code>code</code>参数是上一步拿到的授权码，<code>redirect_uri</code>参数是令牌颁发后的回调网址。</p><p><img src="/posts/eaf19731/bg2019040904.jpg"></p><p>第四步，B 网站收到请求以后，就会颁发令牌。具体做法是向<code>redirect_uri</code>指定的网址，发送一段 JSON 数据。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">&#123;    </span><br><span class="line">  <span class="string">&quot;access_token&quot;</span>:<span class="string">&quot;ACCESS_TOKEN&quot;</span>,</span><br><span class="line">  <span class="string">&quot;token_type&quot;</span>:<span class="string">&quot;bearer&quot;</span>,</span><br><span class="line">  <span class="string">&quot;expires_in&quot;</span>:<span class="number">2592000</span>,</span><br><span class="line">  <span class="string">&quot;refresh_token&quot;</span>:<span class="string">&quot;REFRESH_TOKEN&quot;</span>,</span><br><span class="line">  <span class="string">&quot;scope&quot;</span>:<span class="string">&quot;read&quot;</span>,</span><br><span class="line">  <span class="string">&quot;uid&quot;</span>:<span class="number">100101</span>,</span><br><span class="line">  <span class="string">&quot;info&quot;</span>:&#123;...&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></blockquote><p>上面 JSON 数据中，<code>access_token</code>字段就是令牌，A 网站在后端拿到了。</p><p><img src="/posts/eaf19731/bg2019040905.jpg"></p><h2><span id="第二种方式隐藏式">第二种方式：隐藏式</span></h2><p>有些 Web 应用是纯前端应用，没有后端。这时就不能用上面的方式了，必须将令牌储存在前端。<strong>RFC 6749 就规定了第二种方式，允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤，所以称为（授权码）”隐藏式”（implicit）。</strong></p><p>第一步，A 网站提供一个链接，要求用户跳转到 B 网站，授权用户数据给 A 网站使用。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">https</span>:<span class="comment">//b.com/oauth/authorize?</span></span><br><span class="line">  response_type=token&amp;</span><br><span class="line">  client_id=<span class="variable constant_">CLIENT_ID</span>&amp;</span><br><span class="line">  redirect_uri=<span class="variable constant_">CALLBACK_URL</span>&amp;</span><br><span class="line">  scope=read</span><br></pre></td></tr></table></figure></blockquote><p>上面 URL 中，<code>response_type</code>参数为<code>token</code>，表示要求直接返回令牌。</p><p>第二步，用户跳转到 B 网站，登录后同意给予 A 网站授权。这时，B 网站就会跳回<code>redirect_uri</code>参数指定的跳转网址，并且把令牌作为 URL 参数，传给 A 网站。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">https</span>:<span class="comment">//a.com/callback#token=ACCESS_TOKEN</span></span><br></pre></td></tr></table></figure></blockquote><p>上面 URL 中，<code>token</code>参数就是令牌，A 网站因此直接在前端拿到令牌。</p><p>注意，令牌的位置是 URL 锚点（fragment），而不是查询字符串（querystring），这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议，因此存在”中间人攻击”的风险，而浏览器跳转时，锚点不会发到服务器，就减少了泄漏令牌的风险。</p><p><img src="/posts/eaf19731/bg2019040906.jpg"></p><p>这种方式把令牌直接传给前端，是很不安全的。因此，只能用于一些安全要求不高的场景，并且令牌的有效期必须非常短，通常就是会话期间（session）有效，浏览器关掉，令牌就失效了。</p><h2><span id="第三种方式密码式">第三种方式：密码式</span></h2><p><strong>如果你高度信任某个应用，RFC 6749 也允许用户把用户名和密码，直接告诉该应用。该应用就使用你的密码，申请令牌，这种方式称为”密码式”（password）。</strong></p><p>第一步，A 网站要求用户提供 B 网站的用户名和密码。拿到以后，A 就直接向 B 请求令牌。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">https</span>:<span class="comment">//oauth.b.com/token?</span></span><br><span class="line">  grant_type=password&amp;</span><br><span class="line">  username=<span class="variable constant_">USERNAME</span>&amp;</span><br><span class="line">  password=<span class="variable constant_">PASSWORD</span>&amp;</span><br><span class="line">  client_id=<span class="variable constant_">CLIENT_ID</span></span><br></pre></td></tr></table></figure></blockquote><p>上面 URL 中，<code>grant_type</code>参数是授权方式，这里的<code>password</code>表示”密码式”，<code>username</code>和<code>password</code>是 B 的用户名和密码。</p><p>第二步，B 网站验证身份通过后，直接给出令牌。注意，这时不需要跳转，而是把令牌放在 JSON 数据里面，作为 HTTP 回应，A 因此拿到令牌。</p><p>这种方式需要用户给出自己的用户名&#x2F;密码，显然风险很大，因此只适用于其他授权方式都无法采用的情况，而且必须是用户高度信任的应用。</p><h2><span id="第四种方式凭证式">第四种方式：凭证式</span></h2><p><strong>最后一种方式是凭证式（client credentials），适用于没有前端的命令行应用，即在命令行下请求令牌。</strong></p><p>第一步，A 应用在命令行向 B 发出请求。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">https</span>:<span class="comment">//oauth.b.com/token?</span></span><br><span class="line">  grant_type=client_credentials&amp;</span><br><span class="line">  client_id=<span class="variable constant_">CLIENT_ID</span>&amp;</span><br><span class="line">  client_secret=<span class="variable constant_">CLIENT_SECRET</span></span><br></pre></td></tr></table></figure></blockquote><p>上面 URL 中，<code>grant_type</code>参数等于<code>client_credentials</code>表示采用凭证式，<code>client_id</code>和<code>client_secret</code>用来让 B 确认 A 的身份。</p><p>第二步，B 网站验证通过以后，直接返回令牌。</p><p>这种方式给出的令牌，是针对第三方应用的，而不是针对用户的，即有可能多个用户共享同一个令牌。</p><h2><span id="令牌的使用">令牌的使用</span></h2><p>A 网站拿到令牌以后，就可以向 B 网站的 API 请求数据了。</p><p>此时，每个发到 API 的请求，都必须带有令牌。具体做法是在请求的头信息，加上一个<code>Authorization</code>字段，令牌就放在这个字段里面。</p><blockquote><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">curl -H <span class="string">&quot;Authorization: Bearer ACCESS_TOKEN&quot;</span> \</span><br><span class="line"><span class="string">&quot;https://api.b.com&quot;</span></span><br></pre></td></tr></table></figure></blockquote><p>上面命令中，<code>ACCESS_TOKEN</code>就是拿到的令牌。</p><h2><span id="更新令牌">更新令牌</span></h2><p>令牌的有效期到了，如果让用户重新走一遍上面的流程，再申请一个新的令牌，很可能体验不好，而且也没有必要。OAuth 2.0 允许用户自动更新令牌。</p><p>具体方法是，B 网站颁发令牌的时候，一次性颁发两个令牌，一个用于获取数据，另一个用于获取新的令牌（refresh token 字段）。令牌到期前，用户使用 refresh token 发一个请求，去更新令牌。</p><blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">https</span>:<span class="comment">//b.com/oauth/token?</span></span><br><span class="line">  grant_type=refresh_token&amp;</span><br><span class="line">  client_id=<span class="variable constant_">CLIENT_ID</span>&amp;</span><br><span class="line">  client_secret=<span class="variable constant_">CLIENT_SECRET</span>&amp;</span><br><span class="line">  refresh_token=<span class="variable constant_">REFRESH_TOKEN</span></span><br></pre></td></tr></table></figure></blockquote><p>上面 URL 中，<code>grant_type</code>参数为<code>refresh_token</code>表示要求更新令牌，<code>client_id</code>参数和<code>client_secret</code>参数用于确认身份，<code>refresh_token</code>参数就是用于更新令牌的令牌。</p><p>B 网站验证通过以后，就会颁发新的令牌。</p><p>写到这里，颁发令牌的四种方式就介绍完了。<a href="https://www.ruanyifeng.com/blog/2019/04/github-oauth.html">下一篇文章</a>会编写一个真实的 Demo，演示如何通过 OAuth 2.0 向 GitHub 的 API 申请令牌，然后再用令牌获取数据。</p><p>（正文完）</p><h2><span id="前端高级开发工程师免费直播课">前端高级开发工程师免费直播课</span></h2><p>经过多年的发展，前端工程师的地位日益提升，越来越多的人想往前端的方向发展。</p><p>但是，市场对前端工程师的要求也越来越高，深入掌握底层技术的高级前端，才能在市场上找到自己立足之地。</p><p><img src="/posts/eaf19731/bg2019040908.jpg"></p><p>为了帮助大家深入了解高级前端的学习要点，4月22日（周一）到4月29日（下周一），网易云课堂推出了<a href="https://url.163.com/mYh">前端进阶免费直播课</a>。</p><blockquote><ul><li>周一（4月22日）：《前端不会点 node 怎么行，node http 模块详解》</li><li>周二（4月23日）：《进阶面试必备的三大技术题》</li><li>周三（4月24日）：《前端高级工程师核心装备之柯里化》</li><li>周四（4月25日）：《手把手带你实现 Vue 的 MVVM》</li><li>周五（4月26日）：《JQuery 原理分析》</li><li>周六（4月27日）：《用迭代器模式让你的脱离繁重的数据处理 》</li><li>周日（4月28日）：《前端性能–JS的防抖节流实现与案例场景》</li><li>周一（4月29日）：《阿里巴巴 P5 面试题讲解》</li></ul><p>（更多介绍<a href="https://url.163.com/mYh">点击这里</a>）</p></blockquote><p>他们邀请了<a href="https://url.163.com/mYh">网易资深前端工程师</a>，免费直播分享实战中的经验方法。内容涉及网易内部自定组件库工具分享，前端开发相关知识，深度剖析 JavaScript 等。</p><p>这个直播面向前端初、中级开发工程师，每天2个小时，都是一些干货分享并且不收费，建议想进阶的同学坚持学习。大家可以微信扫码，添加助教，获取听课地址。</p><p><img src="/posts/eaf19731/bg2019040911.jpg"></p><p>（完）</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2&gt;&lt;span id=&quot;rfc-6749&quot;&gt;RFC 6749&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;OAuth 2.0 的标准是 &lt;a href=&quot;https://tools.ietf.org/html/rfc6749&quot;&gt;RFC 6749&lt;/a&gt; 文件。该文件先解释了 OAuth 是什</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>CSS命名规范-BEM</title>
    <link href="https://blog.matoapp.net/posts/7b14e662/"/>
    <id>https://blog.matoapp.net/posts/7b14e662/</id>
    <published>2024-05-01T17:52:00.000Z</published>
    <updated>2025-09-24T16:30:02.330Z</updated>
    
    <content type="html"><![CDATA[<h2><span id="bem解决的问题">BEM解决的问题</span></h2><p>css的样式应用是全局性的，没有作用域可言。考虑以下场景</p><p>场景一：开发一个弹窗组件，在现有页面中测试都没问题，一段时间后，新需求新页面，该页面一打开这个弹窗组件，页面中样式都变样了，一查问题，原来是弹窗组件和该页面的样式相互覆盖了，接下来就是修改覆盖样式的选择器…又一段时间，又开发新页面，每次为元素命名都心惊胆战，求神拜佛，没写一条样式，F5都按多几次，每个组件都测试一遍…</p><p>场景二：承接上文，由于页面和弹窗样式冲突了，所以把页面的冲突样式的选择器加上一些结构逻辑，比如子选择器、标签选择器，借此让选择器独一无二。一段时间后，新同事接手跟进需求，对样式进行修改，由于选择器是一连串的结构逻辑，看不过来，嫌麻烦，就干脆在样式文件最后用另一套选择器，加上了覆盖样式…接下来又有新的需求…最后的结果，一个元素对应多套样式，遍布整个样式文件…</p><p>以往开发组件，我们都用“重名概率小”或者干脆起个“当时认为是独一无二的名字”来保证样式不冲突，这是不可靠的。</p><p>理想的状态下，我们开发一套组件的过程中，我们应该可以随意的为其中元素进行命名，而不必担心它是否与组件以外的样式发生冲突。</p><p>BEM解决这一问题的思路在于，由于项目开发中，每个组件都是唯一无二的，其名字也是独一无二的，组件内部元素的名字都加上组件名，并用元素的名字作为选择器，自然组件内的样式就不会与组件外的样式冲突了。</p><p>这是通过组件名的唯一性来保证选择器的唯一性，从而保证样式不会污染到组件外。</p><p>这也可以看作是一种“硬性约束”，因为一般来说，我们的组件会放置在同一目录下，那么操作系统中，同一目录下文件名必须唯一，这一点也就确保了组件之间不会冲突。</p><p>BEM的命名规矩很容易记：block-name__element-name–modifier-name，也就是模块名 + 元素名 + 修饰器名。</p><p>一般来说，根据组件目录名来作为组件名字：</p><p>比如分页组件：</p><p>&#x2F;app&#x2F;components&#x2F;page-btn&#x2F;</p><p>那么该组件模块就名为page-btn，组件内部的元素命名都必须加上模块名，比如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div class=&quot;page-btn&quot;&gt;&lt;/div&gt; &lt;button type=&quot;button&quot; class=&quot;page-btn__prev&quot;&gt;上一页&lt;/button&gt; &lt;!-- ... --&gt; &lt;button type=&quot;button&quot; class=&quot;page-btn__next&quot;&gt;下一页&lt;/button&gt; &lt;/div&gt; </span><br></pre></td></tr></table></figure><p>上面我们用双下划线来明确区分模块名和元素名，当然也可以用单下划线，比如page-btn_prev和page-btn_next。我们只需保留BEM的思想，其命名规范可以任意变通。</p><p>一开始了解BEM的时候，可能会产生误解，出现以下不正确的命名方式：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div class=&quot;page-btn&quot;&gt; &lt;!-- ... --&gt; &lt;ul class=&quot;page-btn__list&quot;&gt; &lt;li class=&quot;page-btn__list__item&quot;&gt; &lt;a href=&quot;#&quot; class=&quot;page-btn__list__item__link&quot;&gt;第一页&lt;/a&gt; &lt;/li&gt; &lt;/ul&gt; &lt;!-- ... --&gt; &lt;/div&gt; </span><br></pre></td></tr></table></figure><p>分页组件有个ul列表名为：page-btn__list，列表里面存放每一页的按钮，名为：page-btn__list__item__link，这是不对的。</p><p>首先，有悖BEM命名规范，BEM的命名中只包含三个部分，元素名只占其中一部分，所以不能出现多个元素名的情况，所以上述每一页的按钮名可以改成：page-btn__btn。</p><p>其次，有悖BEM思想，BEM是不考虑结构的，比如上面的分页按钮，即使它是在ul列表里面，它的命名也不应该考虑其父级元素。当我们遵循了这个规定，无论父元素名发生改变，或是模块构造发生的改变，还是元素之间层级关系互相变动，这些都不会影响元素的名字。</p><p>所以即使需求变动了，分页组件该有按钮还是要有按钮的，DOM构造发生变动，至多也就不同元素的增删减，模块内名称也随之增删减，而不会出现修改名字的情况，也就不会因为名字变动，牵涉到JS文件的修改，或样式文件的修改。</p><h2><span id="bem命名好长">BEM命名好长</span></h2><p>BEM的命名中包含了模块名，长长的命名会让HTML标签会显得臃肿。</p><p>其实每个使用BEM的开发团队多多少少会改变其命名规范，比如Instagram团队使用的驼峰式:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.blockName-elementName--modifierName &#123; /* ... */ &#125; </span><br></pre></td></tr></table></figure><p>还有单下划线：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.block-name_element-name--modifierName &#123; /* ... */ &#125; </span><br></pre></td></tr></table></figure><p>还有修饰器名用单横线连接：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.blockName__elementName-modifierName &#123; /* ... */ &#125; </span><br></pre></td></tr></table></figure><p>其实这些对缩短命名没有多大的帮助，但我们也无需担心文件体积的问题，由于服务端有gzip压缩，BEM命名相同的部分多，压缩下来的体积不会太大。另外现在都用IDE来编写代码了，有自动提示功能，也无须担心重复的输入过长的名字。</p><p>因为命名长，我们是不是可以用子代选择器来代替BEM命名？这样至少在HTML编写时，让HTML标签看起来美观一点。</p><p>下面说说子代选择器带来的问题。</p><h2><span id="子选择器">子选择器</span></h2><p>子代选择器的方式是，通过组件的根节点的名称来选取子代元素。按照这个思路，分页按钮样式可以这么写：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;div class=&quot;page-btn&quot;&gt; &lt;!-- ... --&gt; &lt;ul class=&quot;list&quot;&gt;&lt;/ul&gt; &lt;!-- ... --&gt; &lt;/div&gt; </span><br><span class="line"></span><br><span class="line">.page-btn &#123; /* ... */ &#125; .page-btn .list &#123; /* ... */ &#125; </span><br></pre></td></tr></table></figure><p>HTML看起来美观多了，但这解决了样式冲突问题么？试想下，如果让你来接手这个项目，要增加一个需求，新增一个组件，你命名放心么？</p><p>你面临的问题是：你打开组件目录，里面有个分页组件，叫做page-btn，可是你完全不知道要怎么给新组件命名，因为即使新组件模块名与page-btn不一样，也不能保证新组件与分页组件不冲突。</p><p>比如新的需求是“新增一个列表组件”，如果该组件的名字叫做list，其根节点的名字叫list，那么这个组件下面写的样式，就很可能和.page-btn .list的样式冲突:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.list &#123; /* ... */ &#125; </span><br></pre></td></tr></table></figure><p>这还仅仅只有两个组件而已，实际项目中，十几个或几十个组件，难道我们要每个组件都检查一下来“新组件名是否和以往组件的子元素命名冲突了”么？这不现实。</p><p>BEM禁止使用子代选择器，以上是原因之一。子代选择器不好的地方还在于，如果<strong>层次关系过长</strong>，逻辑不清晰，非常不利于维护。为了懒得命名或者追求所谓的“精简代码”，写出下面这种选择器：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.page-btn button:first-child &#123;&#125; .page-btn ul li a &#123;&#125; /* ... */ /* 维护代码，新增需求 */ .page-btn .prev &#123;&#125; </span><br></pre></td></tr></table></figure><p>用层次关系结构关系来定位元素，可能会因为需求改变而大面积的重写样式文件。试想一下维护这类代码有多么痛苦，我们要一边检查该元素的上下文DOM结构，一边对照着css文件，一一对比，找到该元素对应的样式，也就是说我为了改一个元素的代码，需要不断翻阅HTML文件和CSS文件，可维护性非常之差。更有甚者，来维护这块代码的同事，直接<strong>在样式文件最后</strong>添加覆盖样式，这会造成一个<strong>非常严重</strong>的问题了：同一个元素样式零散的分布在文件的不同地方，而且定位该元素的选择器也可能各不相同。</p><p>这样的样式文件只会越写越糟糕，可以说，当我们用子代选择器来定位元素时，这个样式文件就已经注定是要被翻来覆去的重构的了，甚至，每个来维护这个文件的人都会将其重构一遍。</p><p>子代选择器还会造成权重过大的问题，当我们要做响应式的时候，某个带样式的元素需要适配不同的屏幕，此时，我们还要<strong>不断的</strong>确认该元素之前的选择器写法！为了覆盖前面权重过大的样式，甚至通过添加<strong>额外的类名或标签名</strong>来增加权重。可想而知，此后这个样式文件的维护难度就像雪球一样，越滚越大。</p><p>如果我们用的是BEM，要覆盖样式很简单：找到要覆盖样式的元素，得知它的类名，在媒体查询中，用它的类名作为选择器，写下覆盖样式，样式就覆盖成功了，不需要担心前面样式的权重过大。</p><h2><span id="bem修饰器">BEM修饰器</span></h2><p>根据不同的场景，组件可能会表现出不同的样式。比如分页组件在pc端具有具体的页码以及上下页按钮，但在移动端，因空间有限，可能只保留上下页按钮。我们可以用修饰器来区分这两种情况。默认情况下，分页按钮的类名为page-btn，但在移动端，我们需要加多个类名page-btn–min</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/* 缩小版分页组件中，具体页码按钮隐去 */ .page-btn--min .page-btn__btn &#123; display: none; &#125; .page-btn--min .page-btn__prev &#123; width: 50%; &#125; .page-btn--min .page-btn__prev &#123; width: 50%; &#125; </span><br></pre></td></tr></table></figure><p>上面这种情况用了子代选择器，BEM是不允许这么写的，BEM中修饰器的样式不依赖于任何结构关系，也就是说，元素的状态改变只会影响自身，不对其他元素进行影响，但实际上，这很难做到的。以上的写法不会造成样式冲突的，而且权重的影响也不大。</p><p>BEM修饰器代表着元素的状态，但有时候元素的状态需要js来控制，此时遵循规范没有任何好处，比如激活状态，BEM推荐的写法是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.block__element &#123; display: none; &#125; .block__element--active &#123; display: block; &#125; </span><br></pre></td></tr></table></figure><p>当用js为该元素添加状态时，我们需要知道该元素的名字block__element，这样我们才能推导出它的激活状态为block__element–active，这是不合理的，因为很多时候我们无法得知元素的名称，所以这时候，我们应该统一js控制状态的类名格式，比如is-active、js-active等等，这些类名只用作<strong>标识</strong>，不予许有默认的公共样式：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.block__element &#123; display: none; &#125; .block__element.is-active &#123; display: block; &#125; </span><br></pre></td></tr></table></figure><h2><span id="原子类和bem">原子类和BEM</span></h2><p>BEM可以不需要用到原子类，但是如果已经引入了类似Bootstrap的框架，也没必要强制避免使用原子类，比如“pull-right”、”ellipsis”、“clearfix”等等类，这些类非常实用，和BEM是可以互补的。</p><p>在组件开发中其实不推荐使用原子类，因为这会降低组件的可复用性。可复用性的最理想状态就是组件不仅仅在不同的页面中表现一致，在跨项目的情况下，也能够运行良好。如果组件的样式因为依赖于某<strong>几个</strong>原子类就要依赖整个Bootstrap库，那么组件d 迁移负担就重很多了。</p><p>原子类更适合应用在实际页面中，这是因为页面<strong>变动大而且不可复用</strong>，假设在header中，我们用到了两个组件logo和user-panel（用户操作面板），两个组件分别置于header的左侧和右侧，我们可以这么写：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div class=&quot;header clearfix&quot;&gt; &lt;div class=&quot;logo pull-left&quot;&gt;&lt;!-- ... --&gt;&lt;/div&gt; &lt;div class=&quot;user-panel pull-left&quot;&gt;&lt;!-- ... --&gt;&lt;/div&gt; &lt;/div&gt; </span><br></pre></td></tr></table></figure><p>header可以封装成一个模块，但它复用程度不高，不能算是组件，所以即使使用原子类也没有关系。在项目中，使用原子类之前应该考虑一下，这个场景是否<strong>变动大而且不可复用</strong>，如果是的话，我们可以放心的使用原子类。</p><p>组件应该是“自洽的”，其本身就应该构成了一个“生态圈”，也就是说，他几乎不需要外部供给，自给自足就能够运转下去。</p><h2><span id="实际页面中也应该使用bem">实际页面中也应该使用BEM</span></h2><p>在实际页面中也需要用到BEM命名方法，不然乱起的一个名字很可能就和某一组件冲突了，导致样式相互覆盖。</p><p>假如我们有联系页面，路径是&#x2F;pages&#x2F;contact&#x2F;。那么该页面的模块名可以是page-contact，其名下元素均以page-contact__element-name命名。</p><p>一般来说，实际页面中只是对组件进行调用，对组件的位置进行调整，但不会对组件内部细节进行修改。但实际情况下，同一个组件在不同页面不同模样的情况也是有的，所以会出现在实际页面中对组件样式进行微调的代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/* 联系页面对分页按钮进行微调 */ .page-contact .page-btn &#123;&#125; </span><br></pre></td></tr></table></figure><p>但更推荐的做法是给分页组件添加一个修饰器，将上面的样式放到修饰器名下，再根据实际情况运用到页面中。</p><h2><span id="webpack-css-loader-解决之道">webpack css-loader 解决之道</span></h2><p>BEM主要被诟病的一点在于其命名过长，结合Angular这种带有标签指令的框架时，整个HTML看起来会更混乱：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;!-- 发帖页面 --&gt; &lt;span ng-repeat=&quot;post in postData track by post.id&quot; ng-if=&quot;$index === 0&quot; class=&quot;page-post__post-item&quot; ng-class=&quot;&#123;&#x27;page-post__post-item--even&#x27;: $even&#125;&quot; popover-content=&quot;&quot;&gt; &lt;/span&gt; </span><br></pre></td></tr></table></figure><p>当然，我们可以通过换行来缓解这个问题:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;!-- 发帖页面 --&gt; &lt;span ng-repeat=&quot;post in postData track by post.id&quot; ng-if=&quot;$index === 0&quot; class=&quot;page-post__post-item&quot; ng-class=&quot;&#123;&#x27;page-post__post-item--even&#x27;: $even&#125;&quot; popover-content=&quot;&quot;&gt; &lt;/span&gt; </span><br></pre></td></tr></table></figure><p>但其实说穿了，BEM保证样式不冲突的核心就是：在元素名中加入唯一的标识。这个标识在BEM中对应的是模块名，也可能是一个独一无二的乱序字符串。</p><p>为模块中每个元素名加入标识，这可是重复的工作啊，重复的工作就应该交给机器去做。</p><p><a href="#">webpack</a>加载器css-loader，可在js中读取css样式，自2015年4月份起，该插件加入了placeholder功能，使得该插件可以解决CSS作用域的问题，原理也就是给元素的名称加入唯一的标识。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/* 分页组件 */ :local(.prev) &#123;&#125; </span><br></pre></td></tr></table></figure><p>css-loader加载器自定义的语法：:local(.identifier){}向外暴露出选择器.prev。在JS代码中，我们可以拿到这个选择器：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">import styles from &#x27;./page-btn.css&#x27;; var $prevBtn = $(&#x27;&lt;button class=&quot;&#x27; \+ styles.prev \+ &#x27;&quot;&gt;上一页&lt;/button&gt;&#x27;); // ... </span><br></pre></td></tr></table></figure><p>styles.prev返回的是一串独一无二且随机的字符串，该字符串对应着样式文件中的选择器。这名字有悖语义化，但css-loader支持配置字符串的生成格式</p>]]></content>
    
    
    <summary type="html">CSS命名规范-BEM介绍</summary>
    
    
    
    <category term="转载" scheme="https://blog.matoapp.net/categories/%E8%BD%AC%E8%BD%BD/"/>
    
    <category term="css" scheme="https://blog.matoapp.net/categories/css/"/>
    
    
    <category term="css" scheme="https://blog.matoapp.net/tags/css/"/>
    
    <category term="前端" scheme="https://blog.matoapp.net/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>[MAUI]集成富文本编辑器Editor.js至.NET MAUI Blazor项目</title>
    <link href="https://blog.matoapp.net/posts/33414209/"/>
    <id>https://blog.matoapp.net/posts/33414209/</id>
    <published>2024-04-13T23:52:00.000Z</published>
    <updated>2025-09-24T16:30:02.550Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E8%8E%B7%E5%8F%96%E8%B5%84%E6%BA%90">获取资源</a><ul><li><a href="#%E4%BB%8E%E6%BA%90%E7%A0%81%E6%9E%84%E5%BB%BA">从源码构建</a></li><li><a href="#%E4%BB%8Ecdn%E8%8E%B7%E5%8F%96">从CDN获取</a></li><li><a href="#%E8%8E%B7%E5%8F%96%E6%89%A9%E5%B1%95%E6%8F%92%E4%BB%B6">获取扩展插件</a></li></ul></li><li><a href="#%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE">创建项目</a><ul><li><a href="#%E5%88%9B%E5%BB%BA%E6%8E%A7%E4%BB%B6">创建控件</a></li><li><a href="#%E5%88%9B%E5%BB%BAblazor%E7%BB%84%E4%BB%B6">创建Blazor组件</a></li><li><a href="#%E5%88%9D%E5%A7%8B%E5%8C%96">初始化</a></li><li><a href="#%E4%BF%9D%E5%AD%98">保存</a></li><li><a href="#%E9%94%80%E6%AF%81">销毁</a></li><li><a href="#%E7%BC%96%E5%86%99%E6%B8%B2%E6%9F%93%E9%80%BB%E8%BE%91">编写渲染逻辑</a></li></ul></li><li><a href="#%E5%AE%9E%E7%8E%B0%E5%8F%AA%E8%AF%BB%E7%BC%96%E8%BE%91%E5%8A%9F%E8%83%BD">实现只读&#x2F;编辑功能</a><ul><li><a href="#%E5%88%87%E6%8D%A2%E6%A8%A1%E5%BC%8F">切换模式</a></li><li><a href="#%E8%8E%B7%E5%8F%96%E5%8F%AA%E8%AF%BB%E6%A8%A1%E5%BC%8F%E7%8A%B6%E6%80%81">获取只读模式状态</a></li><li><a href="#%E5%93%8D%E5%BA%94%E5%88%87%E6%8D%A2%E4%BA%8B%E4%BB%B6">响应切换事件</a></li></ul></li><li><a href="#%E5%AE%9E%E7%8E%B0%E6%98%8E%E6%9A%97%E4%B8%BB%E9%A2%98%E5%88%87%E6%8D%A2">实现明&#x2F;暗主题切换</a></li><li><a href="#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80">项目地址</a></li></ul><!-- tocstop --><p><a href="https://github.com/codex-team/editor.js">Editor.js</a> 是一个基于 Web 的所见即所得富文本编辑器，它由CodeX团队开发。之前写过一篇博文专门介绍过这个编辑器，可以回看：<a href="https://www.cnblogs.com/jevonsflash/p/18066803">开源好用的所见即所得(WYSIWYG)编辑器：Editor.js</a>。</p><p>.NET MAUI Blazor允许使用 Web UI 生成跨平台本机应用。 组件在 .NET 进程中以本机方式运行，并使用本地互操作通道将 Web UI 呈现到嵌入式 Web 视图控件（BlazorWebView）。</p><p>这次我们将Editor.js集成到.NET MAUI应用中。并实现只读切换，明&#x2F;暗主题切换等功能。</p><p><img src="/posts/33414209/644861-20240413234916602-1058318088.png"></p><p>使用<a href="https://dotnet.microsoft.com/en-us/apps/maui">.NET MAUI</a>实现跨平台支持，本项目可运行于Android、iOS平台。</p><h2><span id="获取资源">获取资源</span></h2><p>我们先要获取web应用的资源文件（js，css等），以便MAUI的视图呈现标准的Web UI。有两种方式可以获取：</p><ol><li>从源码构建</li><li>从CDN获取</li></ol><h3><span id="从源码构建">从源码构建</span></h3><p>此方法需要首先安装nodejs</p><p>克隆Editorjs项目到本地</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git clone https://github.com/codex-team/editor.js.git</span><br></pre></td></tr></table></figure><p>运行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i</span><br></pre></td></tr></table></figure><p>以及 </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run build</span><br></pre></td></tr></table></figure><p>等待nodejs构建完成，在项目根目录找到<code>dist/editorjs.umd.js</code>这个就是我们需要的js文件</p><p><img src="/posts/33414209/644861-20240413234916736-614390157.png"></p><h3><span id="从cdn获取">从CDN获取</span></h3><p>从官方CDN获取：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest</span><br></pre></td></tr></table></figure><h3><span id="获取扩展插件">获取扩展插件</span></h3><p>Editor.js中的每个块都由插件提供。有简单的外部脚本，有自己的逻辑。默认Editor.js项目中已包含唯一的 Paragraph 块。其它的工具插件可以单独获取。</p><p>同样我们可以找到这些插件的源码编译，或通过CDN获取：</p><ol><li><a href="https://github.com/editor-js/header">Header</a></li><li><a href="https://github.com/editor-js/link">链接</a></li><li><a href="https://github.com/editor-js/raw">HTML块</a></li><li><a href="https://github.com/editor-js/simple-image">简单图片</a>（无后端要求）</li><li><a href="https://github.com/editor-js/image">图片</a></li><li><a href="https://github.com/editor-js/checklist">清单</a></li><li><a href="https://github.com/editor-js/list">列表</a></li><li><a href="https://github.com/editor-js/embed">嵌入</a></li><li><a href="https://github.com/editor-js/quote">引用</a></li></ol><h2><span id="创建项目">创建项目</span></h2><p>新建.NET MAUI Blazor项目，命名<code>Editorjs</code></p><p>将editorjs.umd.js和各插件js文件拷贝至项目根目录下<code>wwwroot</code>文件夹，文件结构如下：</p><p><img src="/posts/33414209/644861-20240413234916407-1091926010.png"></p><p>在wwwroot创建editorjs_index.html文件，并在body中引入editorjs.umd.js和各插件js文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&lt;body&gt;</span><br><span class="line">    ...</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/editorjs.umd.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/checklist@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/code@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/delimiter@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/embed@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/header@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/image@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/inline-code@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/link@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/nested-list@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/marker@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/quote@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/editorjs/tools/table@latest.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">&lt;/body&gt;</span><br></pre></td></tr></table></figure><h3><span id="创建控件">创建控件</span></h3><p>创建 EditNotePage.xaml ，EditNotePage类作为视图控件，继承于ContentView，EditNotePage.xaml的完整代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line">&lt;ContentView xmlns=&quot;http://schemas.microsoft.com/dotnet/2021/maui&quot;</span><br><span class="line">             xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;</span><br><span class="line">             xmlns:mato=&quot;clr-namespace:Editorjs;assembly=Editorjs&quot;</span><br><span class="line">             xmlns:service=&quot;clr-namespace:Editorjs.ViewModels;assembly=Editorjs&quot;</span><br><span class="line">             xmlns:xct=&quot;http://schemas.microsoft.com/dotnet/2022/maui/toolkit&quot;</span><br><span class="line">             x:Name=&quot;MainPage&quot;</span><br><span class="line">             x:Class=&quot;Editorjs.Controls.EditNotePage&quot;&gt;</span><br><span class="line">    &lt;Grid BackgroundColor=&quot;&#123;AppThemeBinding Light=&#123;StaticResource LightPageBackgroundColor&#125;, Dark=&#123;StaticResource DarkPageBackgroundColor&#125;&#125;&quot;</span><br><span class="line">          RowDefinitions=&quot;Auto, *, Auto&quot;</span><br><span class="line">          Padding=&quot;20, 10, 20, 0&quot;&gt;</span><br><span class="line">        &lt;Grid Grid.Row=&quot;0&quot;</span><br><span class="line">              Margin=&quot;0, 0, 0, 10&quot;&gt;</span><br><span class="line">            &lt;Grid.ColumnDefinitions&gt;</span><br><span class="line">                &lt;ColumnDefinition Width=&quot;auto&quot;&gt;&lt;/ColumnDefinition&gt;</span><br><span class="line">                &lt;ColumnDefinition&gt;&lt;/ColumnDefinition&gt;</span><br><span class="line">                &lt;ColumnDefinition&gt;&lt;/ColumnDefinition&gt;</span><br><span class="line">            &lt;/Grid.ColumnDefinitions&gt;</span><br><span class="line"></span><br><span class="line">            &lt;Entry Grid.Column=&quot;1&quot;</span><br><span class="line">                   Placeholder=&quot;请输入标题&quot;</span><br><span class="line">                   Margin=&quot;10, 0, 0, 0&quot;</span><br><span class="line">                   VerticalOptions=&quot;Center&quot;</span><br><span class="line">                   Text=&quot;&#123;Binding Title&#125;&quot;</span><br><span class="line">&gt;</span><br><span class="line">            &lt;/Entry&gt;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            &lt;HorizontalStackLayout Grid.Column=&quot;2&quot;</span><br><span class="line">                                   HeightRequest=&quot;60&quot;</span><br><span class="line">                                   VerticalOptions=&quot;Center&quot;</span><br><span class="line">                                   HorizontalOptions=&quot;End&quot;</span><br><span class="line">                                   Margin=&quot;0, 0, 10, 0&quot;&gt;</span><br><span class="line">                &lt;StackLayout RadioButtonGroup.GroupName=&quot;State&quot;</span><br><span class="line">                             RadioButtonGroup.SelectedValue=&quot;&#123;Binding NoteSegmentState,Mode=TwoWay&#125;&quot;</span><br><span class="line">                             Orientation=&quot;Horizontal&quot;&gt;</span><br><span class="line">                    &lt;RadioButton Value=&quot;&#123;x:Static service:NoteSegmentState.Edit&#125;&quot;</span><br><span class="line">                                 Content=&quot;编辑&quot;&gt;</span><br><span class="line"></span><br><span class="line">                    &lt;/RadioButton&gt;</span><br><span class="line">                    &lt;RadioButton Value=&quot;&#123;x:Static service:NoteSegmentState.PreView&#125;&quot;</span><br><span class="line">                                 Content=&quot;预览&quot;&gt;</span><br><span class="line"></span><br><span class="line">                    &lt;/RadioButton&gt;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">                &lt;/StackLayout&gt;</span><br><span class="line"></span><br><span class="line">            &lt;/HorizontalStackLayout&gt;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        &lt;/Grid&gt;</span><br><span class="line"></span><br><span class="line">        &lt;BlazorWebView Grid.Row=&quot;1&quot;</span><br><span class="line">                       Margin=&quot;-10, 0&quot;</span><br><span class="line">                       x:Name=&quot;mainMapBlazorWebView&quot;</span><br><span class="line">                       HostPage=&quot;wwwroot/editorjs_index.html&quot;&gt;</span><br><span class="line">            &lt;BlazorWebView.RootComponents&gt;</span><br><span class="line">                &lt;RootComponent Selector=&quot;#app&quot;</span><br><span class="line">                               x:Name=&quot;rootComponent&quot;</span><br><span class="line">                               ComponentType=&quot;&#123;x:Type mato:EditorjsPage&#125;&quot; /&gt;</span><br><span class="line">            &lt;/BlazorWebView.RootComponents&gt;</span><br><span class="line">        &lt;/BlazorWebView&gt;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        &lt;ActivityIndicator Grid.RowSpan=&quot;4&quot;</span><br><span class="line">                           IsRunning=&quot;&#123;Binding Loading&#125;&quot;&gt;&lt;/ActivityIndicator&gt;</span><br><span class="line">    &lt;/Grid&gt;</span><br><span class="line">&lt;/ContentView&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>创建一个<code>EditNotePageViewModel</code>的ViewModel类，用于处理页面逻辑。代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br></pre></td><td class="code"><pre><span class="line">public class EditNotePageViewModel : ObservableObject, IEditorViewModel</span><br><span class="line">&#123;</span><br><span class="line">    public Func&lt;Task&lt;string&gt;&gt; OnSubmitting &#123; get; set; &#125;</span><br><span class="line">    public Action&lt;string&gt; OnInited &#123; get; set; &#125;</span><br><span class="line">    public Action OnFocus &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">    public EditNotePageViewModel()</span><br><span class="line">    &#123;</span><br><span class="line">        Submit = new Command(SubmitAction);</span><br><span class="line"></span><br><span class="line">        NoteSegmentState=NoteSegmentState.Edit;</span><br><span class="line">        var content = &quot;&quot;;</span><br><span class="line">        using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(&quot;Editorjs.Assets.sample1.json&quot;))</span><br><span class="line">        &#123;</span><br><span class="line">            if (stream != null)</span><br><span class="line">            &#123;</span><br><span class="line">                using (StreamReader reader = new StreamReader(stream))</span><br><span class="line">                &#123;</span><br><span class="line">                    content = reader.ReadToEnd();                     </span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        Init(new Note()</span><br><span class="line">        &#123;</span><br><span class="line">            Title = &quot;sample&quot;,</span><br><span class="line">            Content=content</span><br><span class="line"></span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private void Init(Note note)</span><br><span class="line">    &#123;</span><br><span class="line">        if (note != null)</span><br><span class="line">        &#123;</span><br><span class="line">            Title = note.Title;</span><br><span class="line">            Content = note.Content;</span><br><span class="line">        &#125;</span><br><span class="line">        OnInited?.Invoke(this.Content);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    private string _title;</span><br><span class="line"></span><br><span class="line">    public string Title</span><br><span class="line">    &#123;</span><br><span class="line">        get &#123; return _title; &#125;</span><br><span class="line">        set</span><br><span class="line">        &#123;</span><br><span class="line">            _title = value;</span><br><span class="line">            OnPropertyChanged();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    </span><br><span class="line">    private string _content;</span><br><span class="line"></span><br><span class="line">    public string Content</span><br><span class="line">    &#123;</span><br><span class="line">        get &#123; return _content; &#125;</span><br><span class="line">        set</span><br><span class="line">        &#123;</span><br><span class="line">            _content = value;</span><br><span class="line">            OnPropertyChanged();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    </span><br><span class="line">    private async void SubmitAction(object obj)</span><br><span class="line">    &#123;</span><br><span class="line">        var savedContent = await OnSubmitting?.Invoke();</span><br><span class="line">        if (string.IsNullOrEmpty(savedContent))</span><br><span class="line">        &#123;</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line">        this.Content=savedContent;</span><br><span class="line"></span><br><span class="line">        var note = new Note();</span><br><span class="line">        note.Title = this.Title;</span><br><span class="line">        note.Content = this.Content;</span><br><span class="line">    &#125;</span><br><span class="line">    public Command Submit &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>注意这里的Init方法，用于初始化内容。这里我们读取<code>Editorjs.Assets.sample1.json</code>资源文件作为初始内容。</p><p><img src="/posts/33414209/644861-20240413234916774-1859480474.png"></p><h3><span id="创建blazor组件">创建Blazor组件</span></h3><p>创建Blazor页面<code>EditorjsPage.razor</code></p><p>在<code>EditorjsPage.razor</code>页面中，我们放置一个<code>div</code>，用于放置编辑器，</p><p>razor页面的 <code>@Code</code> 代码段中，放置EditNotePageViewModel属性，以及一个<code>DotNetObjectReference</code>对象，用于在JS中调用C#方法。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">@code &#123;</span><br><span class="line">    [Parameter]</span><br><span class="line">    public IEditorViewModel EditNotePageViewModel &#123; get; set; &#125;</span><br><span class="line">    private DotNetObjectReference&lt;EditorjsPage&gt; objRef;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    protected override void OnInitialized()</span><br><span class="line">    &#123;</span><br><span class="line">        objRef = DotNetObjectReference.Create(this);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="初始化">初始化</span></h3><p>在script代码段中，创建LoadContent函数，用于加载EditorJs的初始内容。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;div class=&quot;ce-main&quot;&gt;</span><br><span class="line">    &lt;div id=&quot;editorjs&quot;&gt;&lt;/div&gt;</span><br><span class="line">&lt;/div&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>LoadContent中，调用函数<code>window.editor = new window.EditorJS(config)</code>创建一个EditorJS对象，其中config对象包括holder，tools，data等属性，关于EditorJs配置的更多说明请参考<a href="https://editorjs.io/configuration">官方文档</a></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br></pre></td><td class="code"><pre><span class="line">&lt;script type=&quot;text/javascript&quot;&gt;</span><br><span class="line">    window.editor = null;</span><br><span class="line">    window.viewService = &#123;</span><br><span class="line">        LoadContent: function (content) &#123;</span><br><span class="line">            var obj = JSON.parse(content);</span><br><span class="line">            var createEdtor = () =&gt; &#123;</span><br><span class="line">                window.editor = new window.EditorJS(&#123;                 </span><br><span class="line">                    holder: &#x27;editorjs&#x27;,</span><br><span class="line"></span><br><span class="line">                    /**</span><br><span class="line">                     * Tools list</span><br><span class="line">                     */</span><br><span class="line">                    tools: &#123;</span><br><span class="line">                        paragraph: &#123;</span><br><span class="line">                            config: &#123;</span><br><span class="line">                                placeholder: &quot;Enter something&quot;</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        header: &#123;</span><br><span class="line">                            class: Header,</span><br><span class="line">                            inlineToolbar: [&#x27;link&#x27;],</span><br><span class="line">                            config: &#123;</span><br><span class="line">                                placeholder: &#x27;Header&#x27;</span><br><span class="line">                            &#125;,</span><br><span class="line">                            shortcut: &#x27;CMD+SHIFT+H&#x27;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        /**</span><br><span class="line">                         * Or pass class directly without any configuration</span><br><span class="line">                         */</span><br><span class="line">                        image: &#123;</span><br><span class="line">                            class: ImageTool</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        list: &#123;</span><br><span class="line">                            class: NestedList,</span><br><span class="line">                            inlineToolbar: true,</span><br><span class="line">                            shortcut: &#x27;CMD+SHIFT+L&#x27;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        checklist: &#123;</span><br><span class="line">                            class: Checklist,</span><br><span class="line">                            inlineToolbar: true,</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        quote: &#123;</span><br><span class="line">                            class: Quote,</span><br><span class="line">                            inlineToolbar: true,</span><br><span class="line">                            config: &#123;</span><br><span class="line">                                quotePlaceholder: &#x27;输入引用内容&#x27;,</span><br><span class="line">                                captionPlaceholder: &#x27;引用标题&#x27;,</span><br><span class="line">                            &#125;,</span><br><span class="line">                            shortcut: &#x27;CMD+SHIFT+O&#x27;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">                        marker: &#123;</span><br><span class="line">                            class: Marker,</span><br><span class="line">                            shortcut: &#x27;CMD+SHIFT+M&#x27;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        code: &#123;</span><br><span class="line">                            class: CodeTool,</span><br><span class="line">                            shortcut: &#x27;CMD+SHIFT+C&#x27;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        delimiter: Delimiter,</span><br><span class="line"></span><br><span class="line">                        inlineCode: &#123;</span><br><span class="line">                            class: InlineCode,</span><br><span class="line">                            shortcut: &#x27;CMD+SHIFT+C&#x27;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        linkTool: LinkTool,</span><br><span class="line"></span><br><span class="line">                        embed: Embed,</span><br><span class="line"></span><br><span class="line">                        table: &#123;</span><br><span class="line">                            class: Table,</span><br><span class="line">                            inlineToolbar: true,</span><br><span class="line">                            shortcut: &#x27;CMD+ALT+T&#x27;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                    &#125;,</span><br><span class="line">                  </span><br><span class="line">                    i18n: &#123;</span><br><span class="line">                        messages: &#123;</span><br><span class="line">                            &quot;ui&quot;: &#123;</span><br><span class="line">                                &quot;blockTunes&quot;: &#123;</span><br><span class="line">                                    &quot;toggler&quot;: &#123;</span><br><span class="line">                                        &quot;Click to tune&quot;: &quot;点击转换&quot;,</span><br><span class="line">                                        &quot;or drag to move&quot;: &quot;拖动调整&quot;</span><br><span class="line">                                    &#125;,</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;inlineToolbar&quot;: &#123;</span><br><span class="line">                                    &quot;converter&quot;: &#123;</span><br><span class="line">                                        &quot;Convert to&quot;: &quot;转换成&quot;</span><br><span class="line">                                    &#125;</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;toolbar&quot;: &#123;</span><br><span class="line">                                    &quot;toolbox&quot;: &#123;</span><br><span class="line">                                        &quot;Add&quot;: &quot;添加&quot;,</span><br><span class="line">                                        &quot;Filter&quot;: &quot;过滤&quot;,</span><br><span class="line">                                        &quot;Nothing found&quot;: &quot;无内容&quot;</span><br><span class="line">                                    &#125;,</span><br><span class="line">                                    &quot;popover&quot;: &#123;</span><br><span class="line">                                        &quot;Filter&quot;: &quot;过滤&quot;,</span><br><span class="line">                                        &quot;Nothing found&quot;: &quot;无内容&quot;</span><br><span class="line">                                    &#125;</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;,</span><br><span class="line">                            &quot;toolNames&quot;: &#123;</span><br><span class="line">                                &quot;Text&quot;: &quot;段落&quot;,</span><br><span class="line">                                &quot;Heading&quot;: &quot;标题&quot;,</span><br><span class="line">                                &quot;List&quot;: &quot;列表&quot;,</span><br><span class="line">                                &quot;Warning&quot;: &quot;警告&quot;,</span><br><span class="line">                                &quot;Checklist&quot;: &quot;清单&quot;,</span><br><span class="line">                                &quot;Quote&quot;: &quot;引用&quot;,</span><br><span class="line">                                &quot;Code&quot;: &quot;代码&quot;,</span><br><span class="line">                                &quot;Delimiter&quot;: &quot;分割线&quot;,</span><br><span class="line">                                &quot;Raw HTML&quot;: &quot;HTML片段&quot;,</span><br><span class="line">                                &quot;Table&quot;: &quot;表格&quot;,</span><br><span class="line">                                &quot;Link&quot;: &quot;链接&quot;,</span><br><span class="line">                                &quot;Marker&quot;: &quot;突出显示&quot;,</span><br><span class="line">                                &quot;Bold&quot;: &quot;加粗&quot;,</span><br><span class="line">                                &quot;Italic&quot;: &quot;倾斜&quot;,</span><br><span class="line">                                &quot;InlineCode&quot;: &quot;代码片段&quot;,</span><br><span class="line">                                &quot;Image&quot;: &quot;图片&quot;</span><br><span class="line">                            &#125;,</span><br><span class="line">                            &quot;tools&quot;: &#123;</span><br><span class="line">                                &quot;link&quot;: &#123;</span><br><span class="line">                                    &quot;Add a link&quot;: &quot;添加链接&quot;</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;stub&quot;: &#123;</span><br><span class="line">                                    &#x27;The block can not be displayed correctly.&#x27;: &#x27;该模块不能放置在这里&#x27;</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;image&quot;: &#123;</span><br><span class="line">                                    &quot;Caption&quot;: &quot;图片说明&quot;,</span><br><span class="line">                                    &quot;Select an Image&quot;: &quot;选择图片&quot;,</span><br><span class="line">                                    &quot;With border&quot;: &quot;添加边框&quot;,</span><br><span class="line">                                    &quot;Stretch image&quot;: &quot;拉伸图像&quot;,</span><br><span class="line">                                    &quot;With background&quot;: &quot;添加背景&quot;,</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;code&quot;: &#123;</span><br><span class="line">                                    &quot;Enter a code&quot;: &quot;输入代码&quot;,</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;linkTool&quot;: &#123;</span><br><span class="line">                                    &quot;Link&quot;: &quot;请输入链接地址&quot;,</span><br><span class="line">                                    &quot;Couldn&#x27;t fetch the link data&quot;: &quot;获取链接数据失败&quot;,</span><br><span class="line">                                    &quot;Couldn&#x27;t get this link data, try the other one&quot;: &quot;该链接不能访问，请修改&quot;,</span><br><span class="line">                                    &quot;Wrong response format from the server&quot;: &quot;错误响应&quot;,</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;header&quot;: &#123;</span><br><span class="line">                                    &quot;Header&quot;: &quot;标题&quot;,</span><br><span class="line">                                    &quot;Heading 1&quot;: &quot;一级标题&quot;,</span><br><span class="line">                                    &quot;Heading 2&quot;: &quot;二级标题&quot;,</span><br><span class="line">                                    &quot;Heading 3&quot;: &quot;三级标题&quot;,</span><br><span class="line">                                    &quot;Heading 4&quot;: &quot;四级标题&quot;,</span><br><span class="line">                                    &quot;Heading 5&quot;: &quot;五级标题&quot;,</span><br><span class="line">                                    &quot;Heading 6&quot;: &quot;六级标题&quot;,</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;paragraph&quot;: &#123;</span><br><span class="line">                                    &quot;Enter something&quot;: &quot;请输入笔记内容&quot;,</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;list&quot;: &#123;</span><br><span class="line">                                    &quot;Ordered&quot;: &quot;有序列表&quot;,</span><br><span class="line">                                    &quot;Unordered&quot;: &quot;无序列表&quot;,</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;table&quot;: &#123;</span><br><span class="line">                                    &quot;Heading&quot;: &quot;标题&quot;,</span><br><span class="line">                                    &quot;Add column to left&quot;: &quot;在左侧插入列&quot;,</span><br><span class="line">                                    &quot;Add column to right&quot;: &quot;在右侧插入列&quot;,</span><br><span class="line">                                    &quot;Delete column&quot;: &quot;删除列&quot;,</span><br><span class="line">                                    &quot;Add row above&quot;: &quot;在上方插入行&quot;,</span><br><span class="line">                                    &quot;Add row below&quot;: &quot;在下方插入行&quot;,</span><br><span class="line">                                    &quot;Delete row&quot;: &quot;删除行&quot;,</span><br><span class="line">                                    &quot;With headings&quot;: &quot;有标题&quot;,</span><br><span class="line">                                    &quot;Without headings&quot;: &quot;无标题&quot;,</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;quote&quot;: &#123;</span><br><span class="line">                                    &quot;Align Left&quot;: &quot;左对齐&quot;,</span><br><span class="line">                                    &quot;Align Center&quot;: &quot;居中对齐&quot;,</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;,</span><br><span class="line">                            &quot;blockTunes&quot;: &#123;</span><br><span class="line">                                &quot;delete&quot;: &#123;</span><br><span class="line">                                    &quot;Delete&quot;: &quot;删除&quot;,</span><br><span class="line">                                    &#x27;Click to delete&#x27;: &quot;点击删除&quot;</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;moveUp&quot;: &#123;</span><br><span class="line">                                    &quot;Move up&quot;: &quot;向上移&quot;</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;moveDown&quot;: &#123;</span><br><span class="line">                                    &quot;Move down&quot;: &quot;向下移&quot;</span><br><span class="line">                                &#125;,</span><br><span class="line">                                &quot;filter&quot;: &#123;</span><br><span class="line">                                    &quot;Filter&quot;: &quot;过滤&quot;</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;,</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;,</span><br><span class="line"></span><br><span class="line">                    /**</span><br><span class="line">                     * Initial Editor data</span><br><span class="line">                     */</span><br><span class="line">                    data: obj</span><br><span class="line">                &#125;);</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">            if (window.editor) &#123;</span><br><span class="line">                editor.isReady.then(() =&gt; &#123;</span><br><span class="line">                    editor.destroy();</span><br><span class="line">                    createEdtor();</span><br><span class="line">                &#125;);</span><br><span class="line">            &#125;</span><br><span class="line">            else &#123;</span><br><span class="line">                createEdtor();</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">        &#125;,</span><br><span class="line">        DumpContent: async function () &#123;</span><br><span class="line">            outputData = null;</span><br><span class="line">            if (window.editor) &#123;</span><br><span class="line">                if (window.editor.readOnly.isEnabled) &#123;</span><br><span class="line">                    await window.editor.readOnly.toggle();</span><br><span class="line">                &#125;</span><br><span class="line">                var outputObj = await window.editor.save();</span><br><span class="line">                outputData = JSON.stringify(outputObj);</span><br><span class="line">            &#125;</span><br><span class="line">            return outputData;</span><br><span class="line">        &#125;,</span><br><span class="line">        SwitchTheme: function () &#123;</span><br><span class="line">            document.body.classList.toggle(&quot;dark-mode&quot;);</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        SwitchState: async function () &#123;</span><br><span class="line">            state = null;</span><br><span class="line">            if (window.editor &amp;&amp; window.editor.readOnly) &#123;</span><br><span class="line">                var readOnlyState = await window.editor.readOnly.toggle();</span><br><span class="line">                state = readOnlyState;</span><br><span class="line">            &#125;</span><br><span class="line">            return state;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        Focus: async function (atEnd) &#123;</span><br><span class="line">            if (window.editor) &#123;</span><br><span class="line">                await window.editor.focus(atEnd);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        GetState() &#123;</span><br><span class="line">            if (window.editor &amp;&amp; window.editor.readOnly) &#123;</span><br><span class="line">                return window.editor.readOnly.isEnabled;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        Destroy: function () &#123;</span><br><span class="line">            if (window.editor) &#123;</span><br><span class="line">                window.editor.destroy();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    window.initObjRef = function (objRef) &#123;</span><br><span class="line">        window.objRef = objRef;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/posts/33414209/644861-20240413234916929-1626054557.gif"></p><h3><span id="保存">保存</span></h3><p>创建转存函数DumpContent</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">DumpContent: async function () &#123;</span><br><span class="line">    outputData = null;</span><br><span class="line">    if (window.editor) &#123;</span><br><span class="line">        if (window.editor.readOnly.isEnabled) &#123;</span><br><span class="line">            await window.editor.readOnly.toggle();</span><br><span class="line">        &#125;</span><br><span class="line">        var outputObj = await window.editor.save();</span><br><span class="line">        outputData = JSON.stringify(outputObj);</span><br><span class="line">    &#125;</span><br><span class="line">    return outputData;</span><br><span class="line">&#125;,</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="销毁">销毁</span></h3><p>创建销毁函数Destroy</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">Destroy: function () &#123;</span><br><span class="line">    if (window.editor) &#123;</span><br><span class="line">        window.editor.destroy();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><h3><span id="编写渲染逻辑">编写渲染逻辑</span></h3><p>在OnAfterRenderAsync中调用初始化函数，并订阅OnSubmitting和OnInited事件，以便在提交事件触发时保存，以及文本状态变更时重新渲染。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">protected override async Task OnAfterRenderAsync(bool firstRender)</span><br><span class="line">&#123;</span><br><span class="line">    if (!firstRender)</span><br><span class="line">        return;</span><br><span class="line">    if (EditNotePageViewModel != null)</span><br><span class="line">    &#123;</span><br><span class="line">        EditNotePageViewModel.PropertyChanged += EditNotePageViewModel_PropertyChanged;</span><br><span class="line">        this.EditNotePageViewModel.OnSubmitting += OnSubmitting;</span><br><span class="line">        this.EditNotePageViewModel.OnInited += OnInited;</span><br><span class="line">        var currentContent = EditNotePageViewModel.Content;</span><br><span class="line"></span><br><span class="line">        await JSRuntime.InvokeVoidAsync(&quot;viewService.LoadContent&quot;, currentContent);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    await JSRuntime.InvokeVoidAsync(&quot;window.initObjRef&quot;, this.objRef);</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">private async Task&lt;string&gt; OnSubmitting()</span><br><span class="line">&#123;</span><br><span class="line">    var savedContent = await JSRuntime.InvokeAsync&lt;string&gt;(&quot;viewService.DumpContent&quot;);</span><br><span class="line">    return savedContent;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">private async void OnInited(string content)</span><br><span class="line">&#123;</span><br><span class="line">    await JSRuntime.InvokeVoidAsync(&quot;viewService.LoadContent&quot;, content);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="/posts/33414209/644861-20240413234916557-1949357908.gif"></p><h2><span id="实现只读x2f编辑功能">实现只读&#x2F;编辑功能</span></h2><p>在.NET本机中，我们使用枚举来表示编辑状态。 并在控件上设置一个按钮来切换编辑状态。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">public enum NoteSegmentState</span><br><span class="line">&#123;</span><br><span class="line">    Edit,</span><br><span class="line">    PreView</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>EditNotePageViewModel.cs:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">private NoteSegmentState _noteSegmentState;</span><br><span class="line"></span><br><span class="line">    public NoteSegmentState NoteSegmentState</span><br><span class="line">    &#123;</span><br><span class="line">        get &#123; return _noteSegmentState; &#125;</span><br><span class="line">        set</span><br><span class="line">        &#123;</span><br><span class="line">            _noteSegmentState = value;</span><br><span class="line">            OnPropertyChanged();</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>EditNotePage.xaml:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">&lt;StackLayout RadioButtonGroup.GroupName=&quot;State&quot;</span><br><span class="line">             RadioButtonGroup.SelectedValue=&quot;&#123;Binding NoteSegmentState,Mode=TwoWay&#125;&quot;</span><br><span class="line">             Orientation=&quot;Horizontal&quot;&gt;</span><br><span class="line">    &lt;RadioButton Value=&quot;&#123;x:Static service:NoteSegmentState.Edit&#125;&quot;</span><br><span class="line">                 Content=&quot;编辑&quot;&gt;</span><br><span class="line"></span><br><span class="line">    &lt;/RadioButton&gt;</span><br><span class="line">    &lt;RadioButton Value=&quot;&#123;x:Static service:NoteSegmentState.PreView&#125;&quot;</span><br><span class="line">                 Content=&quot;预览&quot;&gt;</span><br><span class="line"></span><br><span class="line">    &lt;/RadioButton&gt;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&lt;/StackLayout&gt;</span><br></pre></td></tr></table></figure><p>Editorjs官方提供了readOnly对象，通过toggle()方法，可以切换编辑模式和只读模式。</p><p>在创建Editorjs实例时，也可以通过设置readOnly属性为true即可实现只读模式。</p><h3><span id="切换模式">切换模式</span></h3><p>在razor页面中创建SwitchState函数，用来切换编辑模式和只读模式。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">SwitchState: async function () &#123;</span><br><span class="line">    state = null;</span><br><span class="line">    if (window.editor &amp;&amp; window.editor.readOnly) &#123;</span><br><span class="line">        var readOnlyState = await window.editor.readOnly.toggle();</span><br><span class="line">        state = readOnlyState;</span><br><span class="line">    &#125;</span><br><span class="line">    return state;</span><br><span class="line">&#125;,</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="获取只读模式状态">获取只读模式状态</span></h3><p>在razor页面中创建GetState函数，用来获取编辑模式和只读模式的状态。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">GetState() &#123;</span><br><span class="line">    if (window.editor &amp;&amp; window.editor.readOnly) &#123;</span><br><span class="line">        return window.editor.readOnly.isEnabled;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="响应切换事件">响应切换事件</span></h3><p>我们监听EditNotePageViewModel 的NoteSegmentState属性变更事件，当状态改变时，调用对应的js方法</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">private async void EditNotePageViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)</span><br><span class="line">&#123;</span><br><span class="line">    if (e.PropertyName == nameof(EditNotePageViewModel.NoteSegmentState))</span><br><span class="line">    &#123;</span><br><span class="line">        if (EditNotePageViewModel.NoteSegmentState==NoteSegmentState.PreView)</span><br><span class="line">        &#123;</span><br><span class="line">            var state = await JSRuntime.InvokeAsync&lt;bool&gt;(&quot;viewService.GetState&quot;);</span><br><span class="line">            if (!state)</span><br><span class="line">            &#123;</span><br><span class="line">                await JSRuntime.InvokeAsync&lt;bool&gt;(&quot;viewService.SwitchState&quot;);</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">        else if (EditNotePageViewModel.NoteSegmentState==NoteSegmentState.Edit)</span><br><span class="line">        &#123;</span><br><span class="line">            var state = await JSRuntime.InvokeAsync&lt;bool&gt;(&quot;viewService.GetState&quot;);</span><br><span class="line">            if (state)</span><br><span class="line">            &#123;</span><br><span class="line">                await JSRuntime.InvokeAsync&lt;bool&gt;(&quot;viewService.SwitchState&quot;);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/posts/33414209/644861-20240413234916807-164856559.gif"></p><h2><span id="实现明x2f暗主题切换">实现明&#x2F;暗主题切换</span></h2><p>lib&#x2F;editorjs&#x2F;css&#x2F;main.css中，定义了<code>.dark-mode</code>类的样式表</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">.dark-mode &#123;</span><br><span class="line">--color-border-light: rgba(255, 255, 255,.08);</span><br><span class="line">--color-bg-main: #212121;</span><br><span class="line">--color-text-main: #F5F5F5;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.dark-mode .ce-popover &#123;</span><br><span class="line">    --color-background: #424242;</span><br><span class="line">    --color-text-primary: #F5F5F5;</span><br><span class="line">    --color-text-secondary: #707684;</span><br><span class="line">    --color-border: #424242;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.dark-mode .ce-toolbar__settings-btn &#123;</span><br><span class="line">    background: #2A2A2A;</span><br><span class="line">    border: 1px solid #424242;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.dark-mode .ce-toolbar__plus &#123;</span><br><span class="line">    background: #2A2A2A;</span><br><span class="line">    border: 1px solid #424242;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.dark-mode .ce-popover-item__icon &#123;</span><br><span class="line">    background: #2A2A2A;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.dark-mode .ce-code__textarea &#123;</span><br><span class="line">    color: #212121;</span><br><span class="line">    background: #2A2A2A;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.dark-mode .tc-popover &#123;</span><br><span class="line">    --color-border: #424242;</span><br><span class="line">    --color-background: #424242;</span><br><span class="line">&#125;</span><br><span class="line">.dark-mode .tc-wrap &#123;</span><br><span class="line">    --color-background: #424242;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在razor页面中添加SwitchTheme函数，用于用于切换<code>dark-mode</code>“的&#96;类名，从而实现暗黑模式和正常模式之间的切换。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">SwitchTheme: function () &#123;</span><br><span class="line">    document.body.classList.toggle(&quot;dark-mode&quot;);</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><p>在<code>OnInitializedAsync</code>中，订阅<code>Application.Current.RequestedThemeChanged</code>事件，用于监听主题切换事件，并调用<code>SwitchTheme</code>函数。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">protected override async Task OnInitializedAsync()</span><br><span class="line">&#123;</span><br><span class="line">    objRef = DotNetObjectReference.Create(this);</span><br><span class="line"></span><br><span class="line">    Application.Current.RequestedThemeChanged += OnRequestedThemeChanged;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line">private async void OnRequestedThemeChanged(object sender, AppThemeChangedEventArgs args)</span><br><span class="line">&#123;</span><br><span class="line">    await JSRuntime.InvokeVoidAsync(&quot;viewService.SwitchTheme&quot;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在渲染页面时，也判断是否需要切换主题</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">protected override async Task OnAfterRenderAsync(bool firstRender)</span><br><span class="line">&#123;</span><br><span class="line">    if (!firstRender)</span><br><span class="line">        return;</span><br><span class="line">    ···</span><br><span class="line">    if (Application.Current.UserAppTheme==AppTheme.Dark)</span><br><span class="line">    &#123;</span><br><span class="line">        await JSRuntime.InvokeVoidAsync(&quot;viewService.SwitchTheme&quot;);</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/posts/33414209/644861-20240413234916933-1949011365.gif"></p><h2><span id="项目地址">项目地址</span></h2><p><a href="https://github.com/jevonsflash/maui-samples">Github:maui-samples</a></p>]]></content>
    
    
    <summary type="html">其它的工具插件可以单独获取。在OnAfterRenderAsync中调用初始化函数，并订阅OnSubmitting和OnInited事件，以便在提交事件触发时保存，以及文本状态变更时重新渲染。在wwwroot创建editorjs_index.html文件，并在body中引入editorjs.umd.js和各插件js文件。我们先要获取web应用的资源文件（js，css等），以便MAUI的视图呈现标准的Web UI。在script代码段中，创建LoadContent函数，用于加载EditorJs的初始内容。</summary>
    
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/categories/NET/"/>
    
    <category term=".NET MAUI" scheme="https://blog.matoapp.net/categories/NET-MAUI/"/>
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/tags/NET/"/>
    
    <category term="Xamarin" scheme="https://blog.matoapp.net/tags/Xamarin/"/>
    
    <category term="MAUI" scheme="https://blog.matoapp.net/tags/MAUI/"/>
    
    <category term=".NET blazor" scheme="https://blog.matoapp.net/tags/NET-blazor/"/>
    
  </entry>
  
  <entry>
    <title>[MAUI]模仿哔哩哔哩的一键三连</title>
    <link href="https://blog.matoapp.net/posts/a6dfa37/"/>
    <id>https://blog.matoapp.net/posts/a6dfa37/</id>
    <published>2024-03-25T19:43:00.000Z</published>
    <updated>2025-09-24T16:30:02.514Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E5%88%9B%E5%BB%BA%E5%BC%A7%E5%BD%A2%E8%BF%9B%E5%BA%A6%E6%9D%A1">创建弧形进度条</a><ul><li><a href="#%E7%BB%98%E5%88%B6%E5%BC%A7">绘制弧</a></li></ul></li><li><a href="#%E5%87%86%E5%A4%87%E7%89%A9%E6%96%99">准备物料</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E6%B0%94%E6%B3%A1">创建气泡</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E6%89%8B%E5%8A%BF">创建手势</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E4%BA%A4%E4%BA%92%E4%B8%8E%E5%8A%A8%E6%95%88">创建交互与动效</a></li><li><a href="#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80">项目地址</a></li></ul><!-- tocstop --><p><a href="https://www.bilibili.com/">哔哩哔哩(Bilibili)</a>中用户可以通过长按点赞键同时完成点赞、投币、收藏对UP主表示支持，后UP主多用“一键三连”向视频浏览者请求对其作品同时进行点赞、投币、收藏。</p><p><img src="/posts/a6dfa37/644861-20240324232316058-1755743616.gif"></p><p>“三连按钮”是一组按钮，轻击时当做普通状态按钮使用，当长按 2 秒钟后，转为三连模式，可以控制并显示进度，并在进度完成时弹出一些泡泡</p><p>一直想实现这个效果，但由于.NET MAUI对<a href="https://github.com/dotnet/maui/issues/15431">图形填充渲染问题</a>直到.NET 8才修复。想要的效果直到最近才能实现。</p><p>两年前Dino老师用UWP实现过“一键三连”:</p><blockquote><p><a href="https://www.cnblogs.com/dino623/p/Three_Actions_With_One_Click.html">[UWP]模仿哔哩哔哩的一键三连</a></p></blockquote><p>今天用MAUI实现。</p><p><img src="/posts/a6dfa37/644861-20240324232316119-1215602199.gif"></p><p>使用<a href="https://dotnet.microsoft.com/en-us/apps/maui">.NET MAU</a>实现跨平台支持，本项目可运行于Android、iOS平台。</p><h2><span id="创建弧形进度条">创建弧形进度条</span></h2><p>新建.NET MAUI项目，命名<code>HoldDownButtonGroup</code></p><p>在项目中添加SkiaSharp绘制功能的引用<code>Microsoft.Maui.Graphics.Skia</code>以及<code>SkiaSharp.Views.Maui.Controls</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;ItemGroup&gt;</span><br><span class="line">    &lt;PackageReference Include=&quot;Microsoft.Maui.Graphics.Skia&quot; Version=&quot;7.0.59&quot; /&gt;</span><br><span class="line">    &lt;PackageReference Include=&quot;SkiaSharp.Views.Maui.Controls&quot; Version=&quot;2.88.3&quot; /&gt;</span><br><span class="line">&lt;/ItemGroup&gt;</span><br></pre></td></tr></table></figure><p>进度条（ProgressBar）用于展示任务的进度，告知用户当前状态和预期，本项目依赖弧形进度条组件（CircleProgressBar），此组件在本项目中用于展示“三连按钮”长按任务的进度</p><p>这里简单介绍弧形进度条组件的原理和实现，更多内容请阅读：<a href="https://www.cnblogs.com/jevonsflash/p/17489161.html">[MAUI]弧形进度条与弧形滑块的交互实现</a>。</p><p>控件将包含以下可绑定属性：</p><ul><li>Maxiumum：最大值</li><li>Minimum：最小值</li><li>Progress：当前进度</li><li>AnimationLength：动画时长</li><li>BorderWidth：描边宽度</li><li>LabelContent：标签内容</li><li>ContainerColor：容器颜色，即进度条的背景色</li><li>ProgressColor：进度条颜色</li></ul><p>创建CircleProgressBar.xaml，代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;</span><br><span class="line">&lt;ContentView xmlns=&quot;http://schemas.microsoft.com/dotnet/2021/maui&quot;</span><br><span class="line">             xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;</span><br><span class="line">             xmlns:forms=&quot;clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls&quot;</span><br><span class="line">             x:Class=&quot;HoldDownButtonGroup.Controls.CircleProgressBar&quot;&gt;</span><br><span class="line">    &lt;ContentView.Content&gt;</span><br><span class="line">        &lt;Grid&gt;</span><br><span class="line">            &lt;forms:SKCanvasView x:Name=&quot;canvasView&quot;</span><br><span class="line">                                PaintSurface=&quot;OnCanvasViewPaintSurface&quot; /&gt;</span><br><span class="line">            &lt;ContentView x:Name=&quot;MainContent&quot;&gt;&lt;/ContentView&gt;</span><br><span class="line">            &lt;Label </span><br><span class="line">                FontSize=&quot;28&quot;</span><br><span class="line">                HorizontalOptions=&quot;Center&quot;</span><br><span class="line">                VerticalOptions=&quot;Center&quot; </span><br><span class="line">                x:Name=&quot;labelView&quot;&gt;&lt;/Label&gt;</span><br><span class="line">        &lt;/Grid&gt;</span><br><span class="line"></span><br><span class="line">    &lt;/ContentView.Content&gt;</span><br><span class="line">&lt;/ContentView&gt;</span><br></pre></td></tr></table></figure><p>SKCanvasView是SkiaSharp.Views.Maui.Controls封装的View控件。</p><h3><span id="绘制弧">绘制弧</span></h3><p>Skia中，通过AddArc方法绘制弧，需要传入一个SKRect对象，其代表一个弧（或椭弧）的外接矩形。startAngle和sweepAngle分别代表顺时针起始角度和扫描角度。</p><p>通过startAngle和sweepAngle可以绘制出一个弧，如下图红色部分所示：</p><p><img src="/posts/a6dfa37/644861-20240324232316080-140356722.png"></p><p>在<code>CircleProgressBar.xaml.cs</code>的CodeBehind中，创建OnCanvasViewPaintSurface，通过给定起始角度为正上方，扫描角度为360对于100%进度，通过插值计算出当前进度对应的扫描角度，绘制出进度条。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">    var SumValue = Maximum - Minimum;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    SKImageInfo info = args.Info;</span><br><span class="line">    SKSurface surface = args.Surface;</span><br><span class="line">    SKCanvas canvas = surface.Canvas;</span><br><span class="line"></span><br><span class="line">    canvas.Clear();</span><br><span class="line"></span><br><span class="line">    SKRect rect = new SKRect(_mainRectPadding, _mainRectPadding, info.Width - _mainRectPadding, info.Height - _mainRectPadding);</span><br><span class="line">    float startAngle = -90;</span><br><span class="line">    float sweepAngle = (float)((_realtimeProgress / SumValue) * 360);</span><br><span class="line"></span><br><span class="line">    canvas.DrawOval(rect, OutlinePaint);</span><br><span class="line"></span><br><span class="line">    using (SKPath path = new SKPath())</span><br><span class="line">    &#123;</span><br><span class="line">        path.AddArc(rect, startAngle, sweepAngle);</span><br><span class="line">        </span><br><span class="line">        canvas.DrawPath(path, ArcPaint);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中SumValue表明进度条的总进度，通过Maximum和Minimum计算得出。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">public double SumValue =&gt; Maximum - Minimum;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>创建进度条轨道背景画刷和进度条画刷，其中进度条画刷的StrokeCap属性设置为SKStrokeCap.Round，使得进度条两端为圆形。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> SKPaint _outlinePaint;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> SKPaint OutlinePaint</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">get</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (_outlinePaint == <span class="literal">null</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            RefreshMainRectPadding();</span><br><span class="line">            SKPaint outlinePaint = <span class="keyword">new</span> SKPaint</span><br><span class="line">            &#123;</span><br><span class="line">                Color = <span class="keyword">this</span>.ContainerColor.ToSKColor(),</span><br><span class="line">                Style = SKPaintStyle.Stroke,</span><br><span class="line">                StrokeWidth = (<span class="built_in">float</span>)BorderWidth,</span><br><span class="line">            &#125;;</span><br><span class="line">            _outlinePaint = outlinePaint;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> _outlinePaint;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">protected</span> SKPaint _arcPaint;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> SKPaint ArcPaint</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">get</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (_arcPaint == <span class="literal">null</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            RefreshMainRectPadding();</span><br><span class="line">            SKPaint arcPaint = <span class="keyword">new</span> SKPaint</span><br><span class="line">            &#123;</span><br><span class="line">                Color = <span class="keyword">this</span>.ProgressColor.ToSKColor(),</span><br><span class="line">                Style = SKPaintStyle.Stroke,</span><br><span class="line">                StrokeWidth = (<span class="built_in">float</span>)BorderWidth,</span><br><span class="line">                StrokeCap = SKStrokeCap.Round,</span><br><span class="line">            &#125;;</span><br><span class="line">            _arcPaint = arcPaint;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> _arcPaint;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在Progress值变更时，重新渲染进度条，并触发ValueChanged事件。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">private void UpdateProgress()</span><br><span class="line">&#123;</span><br><span class="line">    this._realtimeProgress = this.Progress;</span><br><span class="line">    this.labelView.Text = this.Progress.ToString(LABEL_FORMATE);</span><br><span class="line">    this.canvasView?.InvalidateSurface();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>效果如下</p><p><img src="/posts/a6dfa37/644861-20240324232315923-570297066.gif"></p><h2><span id="准备物料">准备物料</span></h2><p>点赞、投币、收藏三个按钮的图片来自于哔哩哔哩（Bilibili）网站。这些按钮用svg格式在html中。</p><p>我们只需前往哔哩哔哩主站，要打开浏览器的开发者工具，用元素检查器，在找到按钮位置后查看其样式，拷贝path中的svg代码，即可得到这些矢量图片。</p><p><img src="https://img2024.cnblogs.com/blog/644861/202403/644861-20240324233409728-684929414.png"></p><p>拷贝右侧红色区域选中的部分，我们只需要这些svg代码。</p><p>在Xaml中我们创建Path元素，并设置Data属性为svg代码。即可得到一个图形</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&lt;Path HeightRequest=&quot;65&quot;</span><br><span class="line">        WidthRequest=&quot;65&quot;</span><br><span class="line">        x:Name=&quot;Icon1&quot;</span><br><span class="line">        Fill=&quot;Transparent&quot;</span><br><span class="line">        Aspect=&quot;Uniform&quot;</span><br><span class="line">        Data=&quot;M 9.77234 30.8573 V 11.7471 H 7.54573 C 5.50932 11.7471 3.85742 13.3931 3.85742 15.425 V 27.1794 C 3.85742 29.2112 5.50932 30.8573 7.54573 30.8573 H 9.77234 Z M 11.9902 30.8573 V 11.7054 C 14.9897 10.627 16.6942 7.8853 17.1055 3.33591 C 17.2666 1.55463 18.9633 0.814421 20.5803 1.59505 C 22.1847 2.36964 23.243 4.32583 23.243 6.93947 C 23.243 8.50265 23.0478 10.1054 22.6582 11.7471 H 29.7324 C 31.7739 11.7471 33.4289 13.402 33.4289 15.4435 C 33.4289 15.7416 33.3928 16.0386 33.3215 16.328 L 30.9883 25.7957 C 30.2558 28.7683 27.5894 30.8573 24.528 30.8573 H 11.9911 H 11.9902 Z&quot;</span><br><span class="line">        VerticalOptions=&quot;Center&quot;</span><br><span class="line">        HorizontalOptions=&quot;Center&quot; /&gt;</span><br></pre></td></tr></table></figure><p><img src="/posts/a6dfa37/644861-20240324232315994-1797478174.png"></p><h2><span id="创建气泡">创建气泡</span></h2><p>气泡实现分为两个步骤：</p><p>Bubble.xaml 创建单一气泡动画<br>Bubbles.xaml 包含气泡集群。随机生成气泡动画路径，创建气泡集群的动画</p><p>Bubbles控件将包含以下可绑定属性：</p><ul><li>Brush：气泡颜色</li><li>BubbleCnt：气泡数量</li><li>BubbleSize：气泡大小</li></ul><p>气泡动画算法参考于火火的 BubbleButton，这里只帖关键代码</p><p>单一气泡的动画：先变大后消失</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">public Animation GetAnimation()</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">    var scaleAnimation = new Animation();</span><br><span class="line"></span><br><span class="line">    var scaleUpAnimation0 = new Animation(v =&gt; MainBox.Scale = v, 0, 1);</span><br><span class="line">    var scaleUpAnimation1 = new Animation(v =&gt; MainBox.Scale = v, 1, 0);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    scaleAnimation.Add(0, 0.2, scaleUpAnimation0);</span><br><span class="line">    scaleAnimation.Add(0.8, 1, scaleUpAnimation1);</span><br><span class="line"></span><br><span class="line">    scaleAnimation.Finished = () =&gt;</span><br><span class="line">    &#123;</span><br><span class="line">        this.MainBox.Scale = 0;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    return scaleAnimation;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>生成气泡</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">public void SpawnBubbles()</span><br><span class="line">&#123;</span><br><span class="line">    this.PitContentLayout.Clear();</span><br><span class="line">    for (int i = 0; i &lt; BubbleCnt; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        var currentBox = new Bubble();</span><br><span class="line">        currentBox.FillColor = i % 2 == 0 ? this.Brush : SolidColorBrush.Transparent;</span><br><span class="line">        currentBox.BorderColor = this.Brush;</span><br><span class="line">        currentBox.HeightRequest = BubbleSize;</span><br><span class="line">        currentBox.WidthRequest = BubbleSize;</span><br><span class="line">        currentBox.HorizontalOptions = LayoutOptions.Start;</span><br><span class="line">        currentBox.VerticalOptions = LayoutOptions.Start;</span><br><span class="line">        this.PitContentLayout.Add(currentBox);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>计算单个气泡的动画路径：随机产生动画运动的随机坐标</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line">private Animation InitAnimation(Bubble element, Size targetSize, bool isOnTop = true)</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    var offsetAnimation = new Animation();</span><br><span class="line"></span><br><span class="line">    if (targetSize == default)</span><br><span class="line">    &#123;</span><br><span class="line">        targetSize = element.DesiredSize;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    var easing = Easing.Linear;</span><br><span class="line"></span><br><span class="line">    var originX = PitContentLayout.Width / 2;</span><br><span class="line">    var originY = PitContentLayout.Height / 2;</span><br><span class="line"></span><br><span class="line">    var targetX = rnd.Next(-(int)targetSize.Width, (int)targetSize.Width) + (int)targetSize.Width / 2 + originX;</span><br><span class="line">    var targetY = isOnTop ? rnd.Next(-(int)(targetSize.Height * 1.5), 0) + (int)targetSize.Height / 2 + originY :</span><br><span class="line">            rnd.Next(0, (int)(targetSize.Height * 1.5)) + (int)targetSize.Height / 2 + originY</span><br><span class="line">        ;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    var offsetX = targetX - originX;</span><br><span class="line">    var offsetY = targetY - originY;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    var offsetAnimation1 = new Animation(v =&gt; element.TranslationX = v, originX - targetSize.Width / 2, targetX - targetSize.Width / 2, easing);</span><br><span class="line">    var offsetAnimation2 = new Animation(v =&gt; element.TranslationY = v, originY - targetSize.Height / 2, targetY - targetSize.Height / 2, easing);</span><br><span class="line"></span><br><span class="line">    offsetAnimation.Add(0.2, 0.8, offsetAnimation1);</span><br><span class="line">    offsetAnimation.Add(0.2, 0.8, offsetAnimation2);</span><br><span class="line">    offsetAnimation.Add(0, 1, element.BoxAnimation);</span><br><span class="line"></span><br><span class="line">    offsetAnimation.Finished = () =&gt;</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">        element.TranslationX = originX;</span><br><span class="line">        element.TranslationY = originY;</span><br><span class="line">        element.Rotation = 0;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    return offsetAnimation;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>开始气泡动画</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">public void StartAnimation()</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">    Content.AbortAnimation(&quot;ReshapeAnimations&quot;);</span><br><span class="line">    var offsetAnimationGroup = new Animation();</span><br><span class="line"></span><br><span class="line">    foreach (var item in this.PitContentLayout.Children)</span><br><span class="line">    &#123;</span><br><span class="line">        if (item is Bubble)</span><br><span class="line">        &#123;</span><br><span class="line">            var isOntop = this.PitContentLayout.Children.IndexOf(item) &gt; this.PitContentLayout.Children.Count / 2;</span><br><span class="line">            var currentAnimation = InitAnimation(item as Bubble, targetSize, isOntop);</span><br><span class="line">            offsetAnimationGroup.Add(0, 1, currentAnimation);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    offsetAnimationGroup.Commit(this, &quot;ReshapeAnimations&quot;, 16, 400);</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="创建手势">创建手势</span></h2><p>可喜可贺，在新发布的.NET 8 中， .NET MAUI 引入了指针手势识别器(PointerGestureRecognizer)，使用方式如下，终于不用自己实现手势监听控件了。</p><p>Xaml:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;Image Source=&quot;dotnet_bot.png&quot;&gt;</span><br><span class="line">    &lt;Image.GestureRecognizers&gt;</span><br><span class="line">        &lt;PointerGestureRecognizer PointerEntered=&quot;OnPointerEntered&quot;</span><br><span class="line">                                  PointerExited=&quot;OnPointerExited&quot;</span><br><span class="line">                                  PointerMoved=&quot;OnPointerMoved&quot; /&gt;</span><br><span class="line">  &lt;/Image.GestureRecognizers&gt;</span><br><span class="line">&lt;/Image&gt;</span><br></pre></td></tr></table></figure><p>C#:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">void OnPointerEntered(object sender, PointerEventArgs e)</span><br><span class="line">&#123;</span><br><span class="line">    // Handle the pointer entered event</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">void OnPointerExited(object sender, PointerEventArgs e)</span><br><span class="line">&#123;</span><br><span class="line">    // Handle the pointer exited event</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">void OnPointerMoved(object sender, PointerEventArgs e)</span><br><span class="line">&#123;</span><br><span class="line">    // Handle the pointer moved event</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>具体请阅读官方文档：<br><a href="https://learn.microsoft.com/zh-cn/dotnet/maui/whats-new/dotnet-8">.NET 8 中 .NET MAUI 的新增功能</a>以及<br><a href="https://learn.microsoft.com/zh-cn/dotnet/maui/fundamentals/gestures/pointer">识别指针手势</a></p><p>在本项目中，需要监听长按动作，当“三连按钮”长按2秒后，转为三连模式，此时需要监听手指释放情况，当时长不足时取消三连。</p><p>由于在之前的文章中实现过监听手势，这里仅简单介绍定义，其余内容不再重复，如需了解请阅读： <a href="https://www.cnblogs.com/jevonsflash/p/17456091.html">[MAUI程序设计] 用Handler实现自定义跨平台控件</a></p><p>定义可以监听的手势类别，分别是按下、移动、抬起、取消、进入、退出</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">public enum TouchActionType</span><br><span class="line">   &#123;</span><br><span class="line">       Entered,</span><br><span class="line">       Pressed,</span><br><span class="line">       Moved,</span><br><span class="line">       Released,</span><br><span class="line">       Exited,</span><br><span class="line">       Cancelled</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><p>添加手势监听器TouchRecognizer，它将提供一个事件OnTouchActionInvoked，用触发手势动作。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">public partial class TouchRecognizer: IDisposable</span><br><span class="line">&#123;</span><br><span class="line">    public event EventHandler&lt;TouchActionEventArgs&gt; OnTouchActionInvoked;</span><br><span class="line">    public partial void Dispose();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>EventArg类TouchActionEventArgs，用于传递手势动作的参数</p><h2><span id="创建交互与动效">创建交互与动效</span></h2><p>在页面创建完三个按钮后，在CodeBehind中编写交互逻辑</p><p>添加更新圆环进度的方法UpdateProgressWithAnimate，此方法根据圆环进度的百分比，计算圆环进度的动画时间，并开始动画</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">private void UpdateProgressWithAnimate(CircleProgressBar progressElement, double progressTarget = 100, double totalLenght = 5*1000, Action&lt;double, bool&gt; finished = null)</span><br><span class="line">&#123;</span><br><span class="line">    Content.AbortAnimation(&quot;ReshapeAnimations&quot;);</span><br><span class="line">    var scaleAnimation = new Animation();</span><br><span class="line"></span><br><span class="line">    double progressOrigin = progressElement.Progress;</span><br><span class="line"></span><br><span class="line">    var animateAction = (double r) =&gt;</span><br><span class="line">    &#123;</span><br><span class="line">        progressElement.Progress = r;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    var scaleUpAnimation0 = new Animation(animateAction, progressOrigin, progressTarget);</span><br><span class="line"></span><br><span class="line">    scaleAnimation.Add(0, 1, scaleUpAnimation0);</span><br><span class="line">    var calcLenght = (double)(Math.Abs((progressOrigin - progressTarget) / 100)) * totalLenght;</span><br><span class="line"></span><br><span class="line">    scaleAnimation.Commit(progressElement, &quot;ReshapeAnimations&quot;, 16, (uint)calcLenght, finished: finished);</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>创建“三连成功”的动画方法StartCelebrationAnimate，在这里通过改变按钮的Scale和Fill属性，实现三连成功时“点亮”图标的动画效果。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">private void StartCelebrationAnimate(Path progressElement, Action&lt;double, bool&gt; finished = null)</span><br><span class="line">&#123;</span><br><span class="line">    var toColor = (Color)this.Resources[&quot;BrandColor&quot;];</span><br><span class="line">    var fromColor = (Color)this.Resources[&quot;DisabledColor&quot;];</span><br><span class="line"></span><br><span class="line">    var scaleAnimation = new Animation();</span><br><span class="line"></span><br><span class="line">    var scaleUpAnimation0 = new Animation(v =&gt; progressElement.Scale=v, 0, 1.5);</span><br><span class="line">    var scaleUpAnimation1 = new Animation(v =&gt; progressElement.Fill=GetColor(v, fromColor, toColor), 0, 1);</span><br><span class="line">    var scaleUpAnimation2 = new Animation(v =&gt; progressElement.Scale=v, 1.5, 1);</span><br><span class="line"></span><br><span class="line">    scaleAnimation.Add(0, 0.5, scaleUpAnimation0);</span><br><span class="line">    scaleAnimation.Add(0, 1, scaleUpAnimation1);</span><br><span class="line">    scaleAnimation.Add(0.5, 1, scaleUpAnimation2);</span><br><span class="line"></span><br><span class="line">    scaleAnimation.Commit(progressElement, &quot;CelebrationAnimate&quot;, 16, 400, finished: finished);</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>按钮触发TouchContentView_OnTouchActionInvoked事件：</p><p>_dispatcherTimer作用是延时1秒，如果按钮被点击，则开始执行后续操作。当按钮被点击时此Timer会开。</p><p>一秒后，开始进入“三连模式”，此时更新圆环进度，当进度完成后开始气泡动画和“三连成功”的动画。</p><p>若中途按钮被释放，则取消此Timer，并重置圆环进度。</p><p>若按钮未进入“三连模式”，则直接播放按钮的点击动画。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br></pre></td><td class="code"><pre><span class="line">private void TouchContentView_OnTouchActionInvoked(object sender, TouchActionEventArgs e)</span><br><span class="line">&#123;</span><br><span class="line">    var layout = ((Microsoft.Maui.Controls.Layout)(sender as TouchContentView).Content).Children;</span><br><span class="line">    var bubbles = layout[0] as Bubbles;</span><br><span class="line">    var circleProgressBar = layout[1] as CircleProgressBar;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    switch (e.Type)</span><br><span class="line">    &#123;</span><br><span class="line">        case TouchActionType.Entered:</span><br><span class="line">            break;</span><br><span class="line">        case TouchActionType.Pressed:</span><br><span class="line">            _dispatcherTimer =Dispatcher.CreateTimer();</span><br><span class="line">            _dispatcherTimer.Interval=new TimeSpan(0, 0, 1);</span><br><span class="line"></span><br><span class="line">            _dispatcherTimer.Tick+=   async (o, e) =&gt;</span><br><span class="line">            &#123;</span><br><span class="line">                _isInProgress=true;</span><br><span class="line">                this.UpdateProgressWithAnimate(ProgressBar1, 100, 2*1000, (d, b) =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    if (circleProgressBar.Progress==100)</span><br><span class="line">                    &#123;</span><br><span class="line">                        this.bubbles1.StartAnimation();</span><br><span class="line">                        StartCelebrationAnimate(this.Icon1);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;);</span><br><span class="line">                this.UpdateProgressWithAnimate(ProgressBar2, 100, 2*1000, (d, b) =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    if (circleProgressBar.Progress==100)</span><br><span class="line">                    &#123;</span><br><span class="line">                        this.bubbles2.StartAnimation();</span><br><span class="line">                        StartCelebrationAnimate(this.Icon2);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;);</span><br><span class="line">                this.UpdateProgressWithAnimate(ProgressBar3, 100, 2*1000, (d, b) =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    if (circleProgressBar.Progress==100)</span><br><span class="line">                    &#123;</span><br><span class="line">                        this.bubbles3.StartAnimation();</span><br><span class="line">                        StartCelebrationAnimate(this.Icon3);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;);</span><br><span class="line"></span><br><span class="line">            &#125;;</span><br><span class="line"></span><br><span class="line">            _dispatcherTimer.Start();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            break;</span><br><span class="line">        case TouchActionType.Moved:</span><br><span class="line">            break;</span><br><span class="line">        case TouchActionType.Released:</span><br><span class="line"></span><br><span class="line">            if (!_isInProgress)</span><br><span class="line">            &#123;</span><br><span class="line">                var brandColor = (Color)this.Resources[&quot;BrandColor&quot;];</span><br><span class="line">                var disabledColor = (Color)this.Resources[&quot;DisabledColor&quot;];</span><br><span class="line"></span><br><span class="line">                if (circleProgressBar.Progress==100)</span><br><span class="line">                &#123;</span><br><span class="line">                    this.UpdateProgressWithAnimate(ProgressBar1, 0, 1000);</span><br><span class="line">                    this.UpdateProgressWithAnimate(ProgressBar2, 0, 1000);</span><br><span class="line">                    this.UpdateProgressWithAnimate(ProgressBar3, 0, 1000);</span><br><span class="line">                    (ProgressBar1.LabelContent as Path).Fill=disabledColor;</span><br><span class="line">                    (ProgressBar2.LabelContent as Path).Fill=disabledColor;</span><br><span class="line">                    (ProgressBar3.LabelContent as Path).Fill=disabledColor;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">                &#125;</span><br><span class="line">                else</span><br><span class="line">                &#123;</span><br><span class="line">                    if (((circleProgressBar.LabelContent as Path).Fill  as SolidColorBrush).Color==disabledColor)</span><br><span class="line">                    &#123;</span><br><span class="line">                        StartCelebrationAnimate(circleProgressBar.LabelContent as Path);</span><br><span class="line"></span><br><span class="line">                    &#125;</span><br><span class="line">                    else</span><br><span class="line">                    &#123;</span><br><span class="line">                        (circleProgressBar.LabelContent as Path).Fill=disabledColor;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">            if (_dispatcherTimer!=null)</span><br><span class="line">            &#123;</span><br><span class="line">                if (_dispatcherTimer.IsRunning)</span><br><span class="line">                &#123;</span><br><span class="line">                    _dispatcherTimer.Stop();</span><br><span class="line">                    _isInProgress=false;</span><br><span class="line"></span><br><span class="line">                &#125;</span><br><span class="line">                _dispatcherTimer=null;</span><br><span class="line"></span><br><span class="line">                if (circleProgressBar.Progress==100)</span><br><span class="line">                &#123;</span><br><span class="line">                    return;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                this.UpdateProgressWithAnimate(ProgressBar1, 0, 1000);</span><br><span class="line">                this.UpdateProgressWithAnimate(ProgressBar2, 0, 1000);</span><br><span class="line">                this.UpdateProgressWithAnimate(ProgressBar3, 0, 1000);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">            break;</span><br><span class="line">        case TouchActionType.Exited:</span><br><span class="line">            break;</span><br><span class="line">        case TouchActionType.Cancelled:</span><br><span class="line">            break;</span><br><span class="line">        default:</span><br><span class="line">            break;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>进入三联模式动画效果如下：<br><img src="/posts/a6dfa37/644861-20240324232315948-1603999285.gif"></p><p>未进入三联模式动画效果如下：</p><p><img src="/posts/a6dfa37/644861-20240324232315881-707492994.gif"></p><p>最终效果如下：</p><p><img src="/posts/a6dfa37/644861-20240324232316119-1215602199.gif"></p><h2><span id="项目地址">项目地址</span></h2><p><a href="https://github.com/jevonsflash/maui-samples">Github:maui-samples</a></p>]]></content>
    
    
    <summary type="html">的CodeBehind中，创建OnCanvasViewPaintSurface，通过给定起始角度为正上方，扫描角度为360对于100%进度，通过插值计算出当前进度对应的扫描角度，绘制出进度条。“三连按钮”是一组按钮，轻击时当做普通状态按钮使用，当长按 2 秒钟后，转为三连模式，可以控制并显示进度，并在进度完成时弹出一些泡泡。在本项目中，需要监听长按动作，当“三连按钮”长按2秒后，转为三连模式，此时需要监听手指释放情况，当时长不足时取消三连。当按钮被点击时此Timer会开。这些按钮用svg格式在html中。</summary>
    
    
    
    <category term=".NET MAUI" scheme="https://blog.matoapp.net/categories/NET-MAUI/"/>
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/tags/NET/"/>
    
    <category term="Xamarin" scheme="https://blog.matoapp.net/tags/Xamarin/"/>
    
    <category term="MAUI" scheme="https://blog.matoapp.net/tags/MAUI/"/>
    
    <category term="XAML" scheme="https://blog.matoapp.net/tags/XAML/"/>
    
  </entry>
  
  <entry>
    <title>[MAUI]集成高德地图组件至.NET MAUI Blazor项目</title>
    <link href="https://blog.matoapp.net/posts/e6d51e9d/"/>
    <id>https://blog.matoapp.net/posts/e6d51e9d/</id>
    <published>2024-03-23T21:58:00.000Z</published>
    <updated>2025-09-24T16:30:02.558Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E5%89%8D%E6%9C%9F%E5%87%86%E5%A4%87%E6%B3%A8%E5%86%8C%E9%AB%98%E5%BE%B7%E5%BC%80%E5%8F%91%E8%80%85%E5%B9%B6%E5%88%9B%E5%BB%BA-key">前期准备：注册高德开发者并创建 key</a><ul><li><a href="#%E7%99%BB%E5%BD%95%E6%8E%A7%E5%88%B6%E5%8F%B0">登录控制台</a></li><li><a href="#%E5%88%9B%E5%BB%BA-key">创建 key</a></li><li><a href="#%E8%8E%B7%E5%8F%96-key-%E5%92%8C%E5%AF%86%E9%92%A5">获取 key 和密钥</a></li></ul></li><li><a href="#%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE">创建项目</a><ul><li><a href="#%E5%88%9B%E5%BB%BAjs-api-loader">创建JS API Loader</a></li><li><a href="#%E9%85%8D%E7%BD%AE%E6%9D%83%E9%99%90">配置权限</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E5%AE%9A%E4%B9%89">创建定义</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E6%A8%A1%E5%9E%8B">创建模型</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E5%9C%B0%E5%9B%BE%E7%BB%84%E4%BB%B6">创建地图组件</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E4%BA%A4%E4%BA%92%E9%80%BB%E8%BE%91">创建交互逻辑</a></li></ul></li><li><a href="#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80">项目地址</a></li></ul><!-- tocstop --><p>地图组件在手机App中常用地理相关业务，如查看线下门店，设置导航，或选取地址等。是一个较为常见的组件。</p><p>在.NET MAUI 中，有两种方案可以集成高德地图，一种是使用原生库绑定。网上也有人实现过：<a href="https://blog.csdn.net/sD7O95O/article/details/125827031">https://blog.csdn.net/sD7O95O/article/details/125827031</a></p><p>但这种方案需要大量平台原生开发的知识，而且需要对每一个平台进行适配。</p><p>在这里我介绍第二种方案：.NET MAUI Blazor + 高德地图JS API 2.0 库的实现。</p><p>JS API 2.0 是高德开放平台基于WebGL的地图组件，可以将高德地图模块集成到.NET MAUI Blazor中的BlazorWebView控件，由于BlazorWebView的跨平台特性，可以达到一次开发全平台通用，无需为每个平台做适配。</p><p>今天用此方法实现一个地图选择器，使用手机的GPS定位初始化当前位置，使用高德地图JS API库实现地点选择功能。混合开发方案涉及本机代码与JS runtime的交互，如果你对这一部分还不太了解，可以先阅读这篇文章：<a href="https://www.cnblogs.com/jevonsflash/p/17772897.html">[MAUI]深入了解.NET MAUI Blazor与Vue的混合开发</a></p><p><img src="/posts/e6d51e9d/644861-20240323215402130-1376893700.png" alt=".NET MAUI Blazor"></p><p>使用<a href="https://dotnet.microsoft.com/en-us/apps/maui">.NET MAU</a>实现跨平台支持，本项目可运行于Android、iOS平台。</p><h2><span id="前期准备注册高德开发者并创建-key">前期准备：注册高德开发者并创建 key</span></h2><h3><span id="登录控制台">登录控制台</span></h3><p>登录 <a href="https://console.amap.com/">高德开放平台控制台</a>，如果没有开发者账号，请 <a href="https://console.amap.com/dev/id">注册开发者</a>。</p><p><img src="/posts/e6d51e9d/644861-20240323215402017-743388851.png"></p><h3><span id="创建-key">创建 key</span></h3><p>进入应用管理，创建新应用，新应用中添加 key，服务平台选择 <code>Web端(JS API)</code>。再创建一个<code>Web服务</code>类型的Key，用于解析初始位置地址。</p><p><img src="/posts/e6d51e9d/644861-20240323215402304-2127445338.png"></p><h3><span id="获取-key-和密钥">获取 key 和密钥</span></h3><p>创建成功后，可获取 key 和安全密钥。</p><p><img src="/posts/e6d51e9d/644861-20240323215401962-857523609.png"></p><h2><span id="创建项目">创建项目</span></h2><p>新建.NET MAUI Blazor项目，命名<code>AMap</code></p><h3><span id="创建js-api-loader">创建JS API Loader</span></h3><p>前往<code>https://webapi.amap.com/loader.js</code>另存js文件至项目wwwroot文件夹</p><p><img src="/posts/e6d51e9d/644861-20240323215401142-1140248833.png"></p><p>在wwwroot创建<code>amap_index.html</code>文件，将loader.js引用到页面中。创建_AMapSecurityConfig对象并设置安全密钥。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">&lt;!DOCTYPE html&gt;</span><br><span class="line">&lt;html lang=&quot;en&quot;&gt;</span><br><span class="line">&lt;head&gt;</span><br><span class="line">    &lt;meta charset=&quot;utf-8&quot; /&gt;</span><br><span class="line">    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover&quot; /&gt;</span><br><span class="line">    &lt;title&gt;AmapApp&lt;/title&gt;</span><br><span class="line">    &lt;base href=&quot;/&quot; /&gt;</span><br><span class="line">    &lt;link href=&quot;css/app2.css&quot; rel=&quot;stylesheet&quot; /&gt;</span><br><span class="line">&lt;/head&gt;</span><br><span class="line"></span><br><span class="line">&lt;body&gt;</span><br><span class="line"></span><br><span class="line">    &lt;div class=&quot;status-bar-safe-area&quot;&gt;&lt;/div&gt;</span><br><span class="line"></span><br><span class="line">    &lt;div id=&quot;app&quot;&gt;Loading...&lt;/div&gt;</span><br><span class="line"></span><br><span class="line">    &lt;div id=&quot;blazor-error-ui&quot;&gt;</span><br><span class="line">        An unhandled error has occurred.</span><br><span class="line">        &lt;a href=&quot;&quot; class=&quot;reload&quot;&gt;Reload&lt;/a&gt;</span><br><span class="line">        &lt;a class=&quot;dismiss&quot;&gt;&lt;/a&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line"></span><br><span class="line">    &lt;script src=&quot;_framework/blazor.webview.js&quot; autostart=&quot;false&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script src=&quot;lib/amap/loader.js&quot;&gt;&lt;/script&gt;</span><br><span class="line">    &lt;script type=&quot;text/javascript&quot;&gt;</span><br><span class="line">        window._AMapSecurityConfig = &#123;</span><br><span class="line">            securityJsCode: &quot;764832459a38e824a0d555b62d8ec1f0&quot;,</span><br><span class="line">        &#125;;</span><br><span class="line">    &lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;/body&gt;</span><br><span class="line"></span><br><span class="line">&lt;/html&gt;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="配置权限">配置权限</span></h3><p>打开Android端AndroidManifest.xml文件</p><p><img src="/posts/e6d51e9d/644861-20240323215401838-1247124808.png"></p><p>添加权限：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;uses-permission android:name=&quot;android.permission.ACCESS_FINE_LOCATION&quot; /&gt;</span><br><span class="line">&lt;uses-permission android:name=&quot;android.permission.ACCESS_COARSE_LOCATION&quot; /&gt;</span><br><span class="line">&lt;uses-permission android:name=&quot;android.permission.ACCESS_BACKGROUND_LOCATION&quot; /&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>打开Info.plist文件，添加权限描述信心</p><p><img src="/posts/e6d51e9d/644861-20240323215401874-61164967.png"></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&lt;key&gt;NSLocationWhenInUseUsageDescription&lt;/key&gt;</span><br><span class="line">&lt;string&gt;允许使用设备的GPS更新您的位置信息。&lt;/string&gt;</span><br></pre></td></tr></table></figure><h3><span id="创建定义">创建定义</span></h3><p>创建Position，Poi，Location等类型，用于描述位置信息。由于篇幅这里不展开介绍。</p><h3><span id="创建模型">创建模型</span></h3><p>创建一个<code>MainPageViewModel</code>类，用于处理页面逻辑。代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br></pre></td><td class="code"><pre><span class="line">public class MainPageViewModel : ObservableObject</span><br><span class="line">&#123;</span><br><span class="line">    public event EventHandler&lt;FinishedChooiseEvenArgs&gt; OnFinishedChooise;</span><br><span class="line">    private static AsyncLock asyncLock = new AsyncLock();</span><br><span class="line">    public static RateLimitedAction throttledAction = Debouncer.Debounce(null, TimeSpan.FromMilliseconds(1500), leading: false, trailing: true);</span><br><span class="line">    public MainPageViewModel()</span><br><span class="line">    &#123;</span><br><span class="line">        Search = new Command(SearchAction);</span><br><span class="line">        Done = new Command(DoneAction);</span><br><span class="line">        Remove = new Command(RemoveAction);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private void RemoveAction(object obj)</span><br><span class="line">    &#123;</span><br><span class="line">        this.Address=null;</span><br><span class="line">        this.CurrentLocation=null;</span><br><span class="line">        OnFinishedChooise?.Invoke(this, new FinishedChooiseEvenArgs(Address, CurrentLocation));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private void DoneAction(object obj)</span><br><span class="line">    &#123;</span><br><span class="line">        OnFinishedChooise?.Invoke(this, new FinishedChooiseEvenArgs(Address, CurrentLocation));</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private void SearchAction(object obj)</span><br><span class="line">    &#123;</span><br><span class="line">        Init();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public async void Init()</span><br><span class="line">    &#123;</span><br><span class="line">        var location = await GeoLocationHelper.GetNativePosition();</span><br><span class="line">        if (location==null)</span><br><span class="line">        &#123;</span><br><span class="line">            return;</span><br><span class="line">        &#125;</span><br><span class="line">        var amapLocation = new Location.Location()</span><br><span class="line">        &#123;</span><br><span class="line">            Latitude=location.Latitude,</span><br><span class="line">            Longitude=location.Longitude</span><br><span class="line">        &#125;;</span><br><span class="line">        CurrentLocation=amapLocation;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private Location.Location _currentLocation;</span><br><span class="line"></span><br><span class="line">    public Location.Location CurrentLocation</span><br><span class="line">    &#123;</span><br><span class="line">        get &#123; return _currentLocation; &#125;</span><br><span class="line">        set</span><br><span class="line">        &#123;</span><br><span class="line"></span><br><span class="line">            if (_currentLocation != value)</span><br><span class="line">            &#123;</span><br><span class="line">                if (value!=null &amp;&amp;_currentLocation!=null&amp;&amp;Location.Location.CalcDistance(value, _currentLocation)&lt;100)</span><br><span class="line">                &#123;</span><br><span class="line">                    return;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                _currentLocation = value;</span><br><span class="line">                OnPropertyChanged();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private string _address;</span><br><span class="line"></span><br><span class="line">    public string Address</span><br><span class="line">    &#123;</span><br><span class="line">        get &#123; return _address; &#125;</span><br><span class="line">        set</span><br><span class="line">        &#123;</span><br><span class="line">            _address = value;</span><br><span class="line">            OnPropertyChanged();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    private ObservableCollection&lt;Poi&gt; _pois;</span><br><span class="line"></span><br><span class="line">    public ObservableCollection&lt;Poi&gt; Pois</span><br><span class="line">    &#123;</span><br><span class="line">        get &#123; return _pois; &#125;</span><br><span class="line">        set</span><br><span class="line">        &#123;</span><br><span class="line">            _pois = value;</span><br><span class="line">            OnPropertyChanged();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private Poi _selectedPoi;</span><br><span class="line"></span><br><span class="line">    public Poi SelectedPoi</span><br><span class="line">    &#123;</span><br><span class="line">        get &#123; return _selectedPoi; &#125;</span><br><span class="line">        set</span><br><span class="line">        &#123;</span><br><span class="line">            _selectedPoi = value;</span><br><span class="line">            OnPropertyChanged();</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    public Command Search &#123; get; set; &#125;</span><br><span class="line">    public Command Done &#123; get; set; &#125;</span><br><span class="line">    public Command Remove &#123; get; set; &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>注意这里的Init方法，用于初始化位置。</p><p><code>GeoLocationHelper.GetNativePosition()</code>方法用于从你设备的GPS模块，获取当前位置。它调用的是<code>Microsoft.Maui.Devices.Sensors</code>提供的设备传感器访问功能<br>，详情可参考官方文档<a href="https://learn.microsoft.com/zh-cn/dotnet/maui/platform-integration/device/geolocation">地理位置 - .NET MAUI</a></p><h3><span id="创建地图组件">创建地图组件</span></h3><p>创建Blazor页面<code>AMapPage.razor</code>以及<code>AMapPage.razor.js</code></p><p>在<code>AMapPage.razor</code>中引入</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">protected override async Task OnAfterRenderAsync(bool firstRender)</span><br><span class="line">&#123;</span><br><span class="line">    if (!firstRender)</span><br><span class="line">        return;</span><br><span class="line">    await JSRuntime.InvokeAsync&lt;IJSObjectReference&gt;(</span><br><span class="line">   &quot;import&quot;, &quot;./AMapPage.razor.js&quot;);</span><br><span class="line">    await Refresh();</span><br><span class="line">    await JSRuntime.InvokeVoidAsync(&quot;window.initObjRef&quot;, this.objRef);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>razor页面的 <code>@Code</code> 代码段中，放置MainPageViewModel属性，以及一个<code>DotNetObjectReference</code>对象，用于在JS中调用C#方法。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">@code &#123;</span><br><span class="line">    [Parameter]</span><br><span class="line">    public MainPageViewModel MainPageViewModel &#123; get; set; &#125;</span><br><span class="line">    private DotNetObjectReference&lt;AMapPage&gt; objRef;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    protected override void OnInitialized()</span><br><span class="line">    &#123;</span><br><span class="line">        objRef = DotNetObjectReference.Create(this);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private async Task Refresh()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">        ...</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>在<code>AMapPage.razor.js</code>我们加载地图，并设置地图的中心点。和一些地图挂件。此外，我们还需要监听地图的中心点变化，更新中心点。 这些代码可以从官方示例中复制。(<a href="https://lbs.amap.com/demo/javascript-api-v2/example/map/map-moving)%E3%80%82">https://lbs.amap.com/demo/javascript-api-v2/example/map/map-moving)。</a></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br></pre></td><td class="code"><pre><span class="line">console.info(&quot;start load&quot;)</span><br><span class="line">window.viewService = &#123;</span><br><span class="line">    map: null,</span><br><span class="line">    zoom: 13,</span><br><span class="line">    amaplocation: [116.397428, 39.90923],</span><br><span class="line">    SetAmapContainerSize: function (width, height) &#123;</span><br><span class="line">        console.info(&quot;setting container size&quot;)</span><br><span class="line"></span><br><span class="line">        var div = document.getElementById(&quot;container&quot;);</span><br><span class="line">        div.style.height = height + &quot;px&quot;;</span><br><span class="line"></span><br><span class="line">    &#125;,</span><br><span class="line">    SetLocation: function (longitude, latitude) &#123;</span><br><span class="line">        console.info(&quot;setting loc&quot;, longitude, latitude)</span><br><span class="line">        window.viewService.amaplocation = [longitude, latitude];</span><br><span class="line">        if (window.viewService.map) &#123;</span><br><span class="line">            window.viewService.map.setZoomAndCenter(window.viewService.zoom, window.viewService.amaplocation);</span><br><span class="line"></span><br><span class="line">            console.info(&quot;set loc&quot;, window.viewService.zoom, window.viewService.map)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    isHotspot: true</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line">AMapLoader.load(&#123; //首次调用 load</span><br><span class="line">    key: &#x27;0896cedc056413f83ca0aee5b029c65d&#x27;,//首次load key为必填</span><br><span class="line">    version: &#x27;2.0&#x27;,</span><br><span class="line">    plugins: [&#x27;AMap.Scale&#x27;, &#x27;AMap.ToolBar&#x27;, &#x27;AMap.InfoWindow&#x27;, &#x27;AMap.PlaceSearch&#x27;]</span><br><span class="line">&#125;).then((AMap) =&gt; &#123;</span><br><span class="line">    console.info(&quot;loading..&quot;)</span><br><span class="line">    var opt = &#123;</span><br><span class="line">        resizeEnable: true,</span><br><span class="line">        center: window.viewService.amaplocation,</span><br><span class="line">        zoom: window.viewService.zoom,</span><br><span class="line">        isHotspot: true</span><br><span class="line">    &#125;</span><br><span class="line">    var map = new AMap.Map(&#x27;container&#x27;, opt);</span><br><span class="line">    console.info(AMap, map, opt)</span><br><span class="line"></span><br><span class="line">    map.addControl(new AMap.Scale())</span><br><span class="line">    map.addControl(new AMap.ToolBar())</span><br><span class="line">    window.viewService.marker = new AMap.Marker(&#123;</span><br><span class="line">        position: map.getCenter()</span><br><span class="line">    &#125;)</span><br><span class="line">    map.add(window.viewService.marker);</span><br><span class="line">    var placeSearch = new AMap.PlaceSearch();  //构造地点查询类</span><br><span class="line">    var infoWindow = new AMap.InfoWindow(&#123;&#125;);</span><br><span class="line">    map.on(&#x27;hotspotover&#x27;, function (result) &#123;</span><br><span class="line">        placeSearch.getDetails(result.id, function (status, result) &#123;</span><br><span class="line">            if (status === &#x27;complete&#x27; &amp;&amp; result.info === &#x27;OK&#x27;) &#123;</span><br><span class="line">                onPlaceSearch(result);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    map.on(&#x27;moveend&#x27;, onMapMoveend);</span><br><span class="line">    // map.on(&#x27;zoomend&#x27;, onMapMoveend);</span><br><span class="line">    //回调函数</span><br><span class="line"></span><br><span class="line">    window.viewService.map = map;</span><br><span class="line"></span><br><span class="line">    function onMapMoveend() &#123;</span><br><span class="line">        var zoom = window.viewService.map.getZoom(); //获取当前地图级别</span><br><span class="line">        var center = window.viewService.map.getCenter(); //获取当前地图中心位置</span><br><span class="line">        if (window.viewService.marker) &#123;</span><br><span class="line">            window.viewService.marker.setPosition(center);</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">        window.objRef.invokeMethodAsync(&#x27;OnMapMoveend&#x27;, center);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    function onPlaceSearch(data) &#123; //infoWindow.open(map, result.lnglat);</span><br><span class="line">        var poiArr = data.poiList.pois;</span><br><span class="line">        if (poiArr[0]) &#123;</span><br><span class="line">            var location = poiArr[0].location;</span><br><span class="line">            infoWindow.setContent(createContent(poiArr[0]));</span><br><span class="line">            infoWindow.open(window.viewService.map, location);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    function createContent(poi) &#123;  //信息窗体内容</span><br><span class="line">        var s = [];</span><br><span class="line">        s.push(&#x27;&lt;div class=&quot;info-title&quot;&gt;&#x27; + poi.name + &#x27;&lt;/div&gt;&lt;div class=&quot;info-content&quot;&gt;&#x27; + &quot;地址：&quot; + poi.address);</span><br><span class="line">        s.push(&quot;电话：&quot; + poi.tel);</span><br><span class="line">        s.push(&quot;类型：&quot; + poi.type);</span><br><span class="line">        s.push(&#x27;&lt;div&gt;&#x27;);</span><br><span class="line">        return s.join(&quot;&lt;br&gt;&quot;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    console.info(&quot;loaded&quot;)</span><br><span class="line"></span><br><span class="line">&#125;).catch((e) =&gt; &#123;</span><br><span class="line">    console.error(e);</span><br><span class="line">&#125;);</span><br><span class="line">window.initObjRef = function (objRef) &#123;</span><br><span class="line">    window.objRef = objRef;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>地图中心点改变时，我们需要使用<code>window.objRef.invokeMethodAsync(&#39;OnMapMoveend&#39;, center);</code>从JS runtime中通知到C#代码。</p><p>同时，在<code>AMapPage.razor</code>中配置一个方法，用于接收从JS runtime发来的回调通知。<br>在此赋值<code>CurrentLocation</code>属性。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">[JSInvokable]</span><br><span class="line">public async Task OnMapMoveend(dynamic location)</span><br><span class="line">&#123;</span><br><span class="line">    await Task.Run(() =&gt;</span><br><span class="line">     &#123;</span><br><span class="line">         var locationArray = JsonConvert.DeserializeObject&lt;double[]&gt;(location.ToString());</span><br><span class="line">         MainPageViewModel.CurrentLocation=new Location.Location()</span><br><span class="line">             &#123;</span><br><span class="line">                 Longitude=locationArray[0],</span><br><span class="line">                 Latitude =locationArray[1]</span><br><span class="line">             &#125;;</span><br><span class="line">     &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同时监听<code>CurrentLocation</code>属性的值，一旦发生变化，则调用JS runtime中的<code>viewService.SetLocation</code>方法，更新地图中心点。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">protected override async Task OnInitializedAsync()</span><br><span class="line">&#123;</span><br><span class="line">    MainPageViewModel.PropertyChanged +=  async (o, e) =&gt;</span><br><span class="line">    &#123;</span><br><span class="line">        if (e.PropertyName==nameof(MainPageViewModel.CurrentLocation))</span><br><span class="line">        &#123;</span><br><span class="line">            if (MainPageViewModel.CurrentLocation!=null)</span><br><span class="line">            &#123;</span><br><span class="line">                var longitude = MainPageViewModel.CurrentLocation.Longitude;</span><br><span class="line">                var latitude = MainPageViewModel.CurrentLocation.Latitude;</span><br><span class="line">                await JSRuntime.InvokeVoidAsync(&quot;viewService.SetLocation&quot;, longitude, latitude);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在<code>MainPageViewModel</code>类中，我们添加一个<code>PropertyChanged</code>事件，用于监听<code>CurrentLocation</code>属性的改变。</p><p>当手指滑动地图触发位置变化，导致<code>CurrentLocation</code>属性改变时，将当前的中心点转换为具体的地址。这里使用了高德逆地理编码API服务（<a href="https://restapi.amap.com/v3/geocode/regeo%EF%BC%89%E8%A7%A3%E6%9E%90CurrentLocation%E7%9A%84%E5%80%BC%EF%BC%8C">https://restapi.amap.com/v3/geocode/regeo）解析CurrentLocation的值，</a> 还需使用了防抖策略，避免接口的频繁调用。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">public MainPageViewModel()</span><br><span class="line">&#123;</span><br><span class="line">    PropertyChanged+=MainPageViewModel_PropertyChanged;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">private async void MainPageViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)</span><br><span class="line">&#123;</span><br><span class="line">    if (e.PropertyName == nameof(CurrentLocation))</span><br><span class="line">    &#123;</span><br><span class="line">        if (CurrentLocation!=null)</span><br><span class="line">        &#123;</span><br><span class="line"></span><br><span class="line">            // 使用防抖</span><br><span class="line">            using (await asyncLock.LockAsync())</span><br><span class="line">            &#123;</span><br><span class="line"></span><br><span class="line">                var amapLocation = new Location.Location()</span><br><span class="line">                &#123;</span><br><span class="line">                    Latitude=CurrentLocation.Latitude,</span><br><span class="line">                    Longitude=CurrentLocation.Longitude</span><br><span class="line">                &#125;;</span><br><span class="line">                var amapInverseHttpRequestParamter = new AmapInverseHttpRequestParamter()</span><br><span class="line">                &#123;</span><br><span class="line">                    Locations= new Location.Location[] &#123; amapLocation &#125;</span><br><span class="line">                &#125;;</span><br><span class="line">                ReGeocodeLocation reGeocodeLocation = null;</span><br><span class="line">                try</span><br><span class="line">                &#123;</span><br><span class="line">                    reGeocodeLocation = await amapHttpRequestClient.InverseAsync(amapInverseHttpRequestParamter);</span><br><span class="line">                &#125;</span><br><span class="line">                catch (Exception ex)</span><br><span class="line">                &#123;</span><br><span class="line"></span><br><span class="line">                    Console.WriteLine(ex.ToString());</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                throttledAction.Update(() =&gt;</span><br><span class="line">                &#123;</span><br><span class="line">                    MainThread.BeginInvokeOnMainThread(() =&gt;</span><br><span class="line">                    &#123;</span><br><span class="line">                        CurrentLocation=amapLocation;</span><br><span class="line">                        if (reGeocodeLocation!=null)</span><br><span class="line">                        &#123;</span><br><span class="line">                            Address = reGeocodeLocation.Address;</span><br><span class="line">                            Pois=new ObservableCollection&lt;Poi&gt;(reGeocodeLocation.Pois);</span><br><span class="line"></span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;);</span><br><span class="line">                &#125;);</span><br><span class="line">                throttledAction.Invoke();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>至此我们完成了地图组件的基本功能。</p><h3><span id="创建交互逻辑">创建交互逻辑</span></h3><p>在MainPage.xaml中，创建一个选择器按钮，以及一个卡片模拟选择器按钮点击后的弹窗。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">&lt;Button Clicked=&quot;Button_Clicked&quot;</span><br><span class="line">        Grid.Row=&quot;1&quot;</span><br><span class="line">        x:Name=&quot;SelectorButton&quot;</span><br><span class="line">        HorizontalOptions=&quot;Center&quot;</span><br><span class="line">        VerticalOptions=&quot;Center&quot;</span><br><span class="line">        Text=&quot;&#123;Binding Address, TargetNullValue=请选择地点&#125;&quot;&gt;&lt;/Button&gt;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&lt;Border StrokeShape=&quot;RoundRectangle 10&quot;</span><br><span class="line">    Grid.RowSpan=&quot;2&quot;</span><br><span class="line">    x:Name=&quot;SelectorPopup&quot;</span><br><span class="line">    IsVisible=&quot;False&quot;</span><br><span class="line">    Margin=&quot;5,50&quot;</span><br><span class="line">    MinimumHeightRequest=&quot;500&quot;&gt;</span><br><span class="line"></span><br><span class="line">    &lt;Grid Padding=&quot;0&quot;&gt;</span><br><span class="line">        &lt;Grid.RowDefinitions&gt;</span><br><span class="line">            &lt;RowDefinition Height=&quot;Auto&quot;&gt;&lt;/RowDefinition&gt;</span><br><span class="line">            &lt;RowDefinition Height=&quot;Auto&quot;&gt;&lt;/RowDefinition&gt;</span><br><span class="line">            &lt;RowDefinition&gt;&lt;/RowDefinition&gt;</span><br><span class="line">        &lt;/Grid.RowDefinitions&gt;</span><br><span class="line"></span><br><span class="line">        &lt;Grid Grid.Row=&quot;0&quot;&gt;</span><br><span class="line">            &lt;Grid.ColumnDefinitions&gt;</span><br><span class="line">                &lt;ColumnDefinition Width=&quot;*&quot;&gt;&lt;/ColumnDefinition&gt;</span><br><span class="line">                &lt;ColumnDefinition&gt;&lt;/ColumnDefinition&gt;</span><br><span class="line">            &lt;/Grid.ColumnDefinitions&gt;</span><br><span class="line">            &lt;Label FontSize=&quot;Large&quot;</span><br><span class="line">                    Margin=&quot;10, 10, 10, 0&quot;</span><br><span class="line">                    FontAttributes=&quot;Bold&quot;</span><br><span class="line">                    Text=&quot;选择地点&quot;&gt;&lt;/Label&gt;</span><br><span class="line">            &lt;HorizontalStackLayout Grid.Column=&quot;1&quot;</span><br><span class="line">                                    HorizontalOptions=&quot;End&quot;&gt;</span><br><span class="line">                &lt;Button Text=&quot;删除&quot;</span><br><span class="line">                        Margin=&quot;5,0&quot;</span><br><span class="line">                        Command=&quot;&#123;Binding Remove&#125;&quot;&gt;&lt;/Button&gt;</span><br><span class="line">                &lt;Button Text=&quot;完成&quot;</span><br><span class="line">                        Margin=&quot;5,0&quot;</span><br><span class="line">                        Command=&quot;&#123;Binding Done&#125;&quot;&gt;&lt;/Button&gt;</span><br><span class="line">            &lt;/HorizontalStackLayout&gt;</span><br><span class="line">        &lt;/Grid&gt;</span><br><span class="line"></span><br><span class="line">        &lt;Grid Grid.Row=&quot;1&quot;</span><br><span class="line">                Margin=&quot;10, 10, 10, 0&quot;&gt;</span><br><span class="line">            &lt;Grid.RowDefinitions&gt;</span><br><span class="line">                &lt;RowDefinition&gt;&lt;/RowDefinition&gt;</span><br><span class="line">                &lt;RowDefinition Height=&quot;Auto&quot;&gt;&lt;/RowDefinition&gt;</span><br><span class="line">            &lt;/Grid.RowDefinitions&gt;</span><br><span class="line">            &lt;Label HorizontalTextAlignment=&quot;Center&quot;</span><br><span class="line">                    VerticalOptions=&quot;Center&quot;</span><br><span class="line">                    x:Name=&quot;ContentLabel&quot;</span><br><span class="line">                    Text=&quot;&#123;Binding Address&#125;&quot;&gt;&lt;/Label&gt;</span><br><span class="line">            &lt;Border IsVisible=&quot;False&quot;</span><br><span class="line">                    Grid.RowSpan=&quot;2&quot;</span><br><span class="line">                    x:Name=&quot;ContentFrame&quot;&gt;</span><br><span class="line">                &lt;Entry Text=&quot;&#123;Binding Address, Mode=TwoWay&#125;&quot;</span><br><span class="line">                        Placeholder=&quot;请输入地址, 按Enter键完成&quot;</span><br><span class="line">                        Completed=&quot;Entry_Completed&quot;</span><br><span class="line">                        Unfocused=&quot;Entry_Unfocused&quot;</span><br><span class="line">                        ClearButtonVisibility=&quot;WhileEditing&quot;&gt;&lt;/Entry&gt;</span><br><span class="line">            &lt;/Border&gt;</span><br><span class="line">            &lt;Border x:Name=&quot;ContentButton&quot;</span><br><span class="line">                    Grid.Row=&quot;1&quot;</span><br><span class="line">                    HorizontalOptions=&quot;Center&quot;</span><br><span class="line">                    VerticalOptions=&quot;Center&quot;&gt;</span><br><span class="line">                &lt;Label&gt;</span><br><span class="line">                    &lt;Label.FormattedText&gt;</span><br><span class="line">                        &lt;FormattedString&gt;</span><br><span class="line">                            &lt;Span FontFamily=&quot;FontAwesome&quot;</span><br><span class="line">                                    Text=&quot;&amp;#xf044;&quot;&gt;&lt;/Span&gt;</span><br><span class="line">                            &lt;Span Text=&quot; 修改&quot;&gt;&lt;/Span&gt;</span><br><span class="line">                        &lt;/FormattedString&gt;</span><br><span class="line">                    &lt;/Label.FormattedText&gt;</span><br><span class="line"></span><br><span class="line">                &lt;/Label&gt;</span><br><span class="line">                &lt;Border.GestureRecognizers&gt;</span><br><span class="line">                    &lt;TapGestureRecognizer Tapped=&quot;TapGestureRecognizer_Tapped&quot;&gt;</span><br><span class="line">                    &lt;/TapGestureRecognizer&gt;</span><br><span class="line">                &lt;/Border.GestureRecognizers&gt;</span><br><span class="line">            &lt;/Border&gt;</span><br><span class="line">        &lt;/Grid&gt;</span><br><span class="line">        &lt;BlazorWebView Grid.Row=&quot;2&quot;</span><br><span class="line">                        Margin=&quot;-10, 0&quot;</span><br><span class="line">                        x:Name=&quot;mainMapBlazorWebView&quot;</span><br><span class="line">                        HostPage=&quot;wwwroot/amap_index.html&quot;&gt;</span><br><span class="line">            &lt;BlazorWebView.RootComponents&gt;</span><br><span class="line">                &lt;RootComponent Selector=&quot;#app&quot;</span><br><span class="line">                                x:Name=&quot;rootComponent&quot;</span><br><span class="line">                                ComponentType=&quot;&#123;x:Type views:AMapPage&#125;&quot; /&gt;</span><br><span class="line">            &lt;/BlazorWebView.RootComponents&gt;</span><br><span class="line">        &lt;/BlazorWebView&gt;</span><br><span class="line">    &lt;/Grid&gt;</span><br><span class="line">&lt;/Border&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="/posts/e6d51e9d/644861-20240323215401235-1955920597.png"></p><p>最终效果如下：</p><p><img src="/posts/e6d51e9d/644861-20240323215401850-804380792.gif"></p><h2><span id="项目地址">项目地址</span></h2><p><a href="https://github.com/jevonsflash/maui-samples">Github:maui-samples</a></p>]]></content>
    
    
    <summary type="html">地图组件在手机App中常用地理相关业务，如查看线下门店，设置导航，或选取地址等。是一个较为常见的组件。在.NET MAUI 中，有两种方案可以集成高德地图，一种是使用原生库绑定。但这种方案需要大量平台原生开发的知识，而且需要对每一个平台进行适配。在这里我介绍第二种方案：.NET MAUI Blazor + 高德地图JS API 2.0 库的实现。JS API 2.0 是高德开放平台基于WebGL的地图组件，可以将高德地图模块集成到.NET MAUI Blazor中的BlazorWebView控件。</summary>
    
    
    
    <category term=".NET MAUI" scheme="https://blog.matoapp.net/categories/NET-MAUI/"/>
    
    <category term="移动开发" scheme="https://blog.matoapp.net/categories/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/"/>
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/tags/NET/"/>
    
    <category term="Xamarin" scheme="https://blog.matoapp.net/tags/Xamarin/"/>
    
    <category term="MAUI" scheme="https://blog.matoapp.net/tags/MAUI/"/>
    
    <category term="C#" scheme="https://blog.matoapp.net/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>[.NET项目实战] Elsa开源工作流组件应用（三）：实战演练</title>
    <link href="https://blog.matoapp.net/posts/6af59036/"/>
    <id>https://blog.matoapp.net/posts/6af59036/</id>
    <published>2024-03-21T15:58:00.000Z</published>
    <updated>2025-09-24T16:30:02.362Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E8%A1%A5%E5%85%85">补充</a></li><li><a href="#%E9%9C%80%E6%B1%82%E6%8F%8F%E8%BF%B0">需求描述</a></li><li><a href="#%E9%9C%80%E6%B1%82%E5%88%86%E6%9E%90">需求分析</a></li><li><a href="#%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0">代码实现</a><ul><li><a href="#%E4%B8%8B%E5%8F%91%E9%97%AE%E5%8D%B7%E6%B4%BB%E5%8A%A8-publishquestionnaireactivity">下发问卷活动 PublishQuestionnaireActivity</a></li><li><a href="#%E9%80%9A%E7%9F%A5%E6%B4%BB%E5%8A%A8notificationactivity">通知活动：NotificationActivity</a></li><li><a href="#%E7%AD%89%E5%BE%85%E9%97%AE%E5%8D%B7%E5%AE%8C%E6%88%90%E6%B4%BB%E5%8A%A8waitfillinsurveyactivity">等待问卷完成活动：WaitFillInSurveyActivity</a></li><li><a href="#%E5%AE%9A%E6%97%B6%E5%92%8C%E5%BB%B6%E6%97%B6%E6%B4%BB%E5%8A%A8">定时和延时活动：</a></li><li><a href="#%E9%97%AE%E5%8D%B7%E6%B4%BB%E5%8A%A8questionnaireactivity">问卷活动：QuestionnaireActivity</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E5%B7%A5%E4%BD%9C%E6%B5%81">创建工作流</a></li><li><a href="#%E5%BC%80%E5%A7%8B%E5%B7%A5%E4%BD%9C%E6%B5%81">开始工作流</a></li></ul></li><li><a href="#troubleshooting">TroubleShooting</a></li></ul><!-- tocstop --><h2><span id="补充">补充</span></h2><p>之前的文章简单介绍了工作流和Elsa工作流库，这里再补充说明两点</p><ol><li><p>工作流的使用场景非常广泛，几乎涵盖了所有需要进行业务流程自动化管理的领域。</p></li><li><p>学习一个开源库，最简单的方法就是看源码，Elsa的工作流引擎源码非常简单易懂，并且提供了非常丰富的示例代码，举一个例子：审批工作流示例<code>.\src\samples\aspnet\Elsa.Samples.AspNet.DocumentApproval</code></p></li></ol><p><img src="/posts/6af59036/644861-20240321161052481-1218143768.png"><br>这个审批流是这样的：<br>作者发来一个文章，有两个审批人需要全部审批通过，文章才算通过，否则退回。</p><p>我们尝试阅读工作流源代码<code>DocumentApprovalWorkflow.cs</code>，并运行此项目，用postman发送请求</p><p>第一步：</p><p>假设这名叫Amanda的作者要发布文章，请求发送后，作者浏览器显示发送成功稍安勿躁之类的提示</p><p>同时后台打印作者信息和4个链接，分别是Jack和Lucy两位审批人“通过”和“退回”的url链接</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">Activities =</span><br><span class="line">&#123;</span><br><span class="line">    new HttpEndpoint</span><br><span class="line">    &#123;</span><br><span class="line">        Path = new(&quot;/documents&quot;),</span><br><span class="line">        SupportedMethods = new(new[] &#123; HttpMethods.Post &#125;),</span><br><span class="line">        ParsedContent = new(documentVariable),</span><br><span class="line">        CanStartWorkflow = true</span><br><span class="line">    &#125;,</span><br><span class="line">    new WriteLine(context =&gt; $&quot;Document received from &#123;documentVariable.Get&lt;dynamic&gt;(context)!.Author.Name&#125;.&quot;),</span><br><span class="line">    new WriteHttpResponse</span><br><span class="line">    &#123;</span><br><span class="line">        Content = new(&quot;&lt;h1&gt;Request for Approval Sent&lt;/h1&gt;&lt;p&gt;Your document has been received and will be reviewed shortly.&lt;/p&gt;&quot;),</span><br><span class="line">        ContentType = new(MediaTypeNames.Text.Html),</span><br><span class="line">        StatusCode = new(HttpStatusCode.OK),</span><br><span class="line">        ResponseHeaders = new(new HttpHeaders &#123; [&quot;X-Powered-By&quot;] = new[] &#123; &quot;Elsa 3.0&quot; &#125; &#125;)</span><br><span class="line">    &#125;,</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>第二步：</p><p>Jack觉得文章不错，通过浏览器请求了“通过”链接，而Lucy觉得文章还不够好，需改进，她在浏览器中请求了“退回”链接。</p><p>两位审批人的审批结果存储于<code>approvedVariable</code>变量中</p><p>同时他们的浏览器返回的响应内容：Thanks for the approval 或 Sorry to hear that</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br></pre></td><td class="code"><pre><span class="line">new Fork</span><br><span class="line">&#123;</span><br><span class="line">    JoinMode = ForkJoinMode.WaitAll,</span><br><span class="line">    Branches =</span><br><span class="line">    &#123;</span><br><span class="line">        // Jack</span><br><span class="line">        new Sequence</span><br><span class="line">        &#123;</span><br><span class="line">            Activities =</span><br><span class="line">            &#123;</span><br><span class="line">                new WriteLine(context =&gt; $&quot;Jack approve url: \n &#123;GenerateSignalUrl(context, &quot;Approve:Jack&quot;)&#125;&quot;),</span><br><span class="line">                new WriteLine(context =&gt; $&quot;Jack reject url: \n &#123;GenerateSignalUrl(context, &quot;Reject:Jack&quot;)&#125;&quot;),</span><br><span class="line">                new Fork</span><br><span class="line">                &#123;</span><br><span class="line">                    JoinMode = ForkJoinMode.WaitAny,</span><br><span class="line">                    Branches =</span><br><span class="line">                    &#123;</span><br><span class="line">                        // Approve</span><br><span class="line">                        new Sequence</span><br><span class="line">                        &#123;</span><br><span class="line">                            Activities =</span><br><span class="line">                            &#123;</span><br><span class="line">                                new Event(&quot;Approve:Jack&quot;),</span><br><span class="line">                                new SetVariable</span><br><span class="line">                                &#123;</span><br><span class="line">                                    Variable = approvedVariable,</span><br><span class="line">                                    Value = new(true)</span><br><span class="line">                                &#125;,</span><br><span class="line">                                new WriteHttpResponse</span><br><span class="line">                                &#123;</span><br><span class="line">                                    Content = new(&quot;Thanks for the approval, Jack!&quot;),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        // Reject</span><br><span class="line">                        new Sequence</span><br><span class="line">                        &#123;</span><br><span class="line">                            Activities =</span><br><span class="line">                            &#123;</span><br><span class="line">                                new Event(&quot;Reject:Jack&quot;),</span><br><span class="line">                                new SetVariable</span><br><span class="line">                                &#123;</span><br><span class="line">                                    Variable = approvedVariable,</span><br><span class="line">                                    Value = new(false)</span><br><span class="line">                                &#125;,</span><br><span class="line">                                new WriteHttpResponse</span><br><span class="line">                                &#123;</span><br><span class="line">                                    Content = new(&quot;Sorry to hear that, Jack!&quot;),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        // Lucy</span><br><span class="line">        new Sequence</span><br><span class="line">        &#123;</span><br><span class="line">            Activities =</span><br><span class="line">            &#123;</span><br><span class="line">                new WriteLine(context =&gt; $&quot;Lucy approve url: \n &#123;GenerateSignalUrl(context, &quot;Approve:Lucy&quot;)&#125;&quot;),</span><br><span class="line">                new WriteLine(context =&gt; $&quot;Lucy reject url: \n &#123;GenerateSignalUrl(context, &quot;Reject:Lucy&quot;)&#125;&quot;),</span><br><span class="line">                new Fork</span><br><span class="line">                &#123;</span><br><span class="line">                    JoinMode = ForkJoinMode.WaitAny,</span><br><span class="line">                    Branches =</span><br><span class="line">                    &#123;</span><br><span class="line">                        // Approve</span><br><span class="line">                        new Sequence</span><br><span class="line">                        &#123;</span><br><span class="line">                            Activities =</span><br><span class="line">                            &#123;</span><br><span class="line">                                new Event(&quot;Approve:Lucy&quot;),</span><br><span class="line">                                new SetVariable</span><br><span class="line">                                &#123;</span><br><span class="line">                                    Variable = approvedVariable,</span><br><span class="line">                                    Value = new(true)</span><br><span class="line">                                &#125;,</span><br><span class="line">                                new WriteHttpResponse</span><br><span class="line">                                &#123;</span><br><span class="line">                                    Content = new(&quot;Thanks for the approval, Lucy!&quot;),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;,</span><br><span class="line"></span><br><span class="line">                        // Reject</span><br><span class="line">                        new Sequence</span><br><span class="line">                        &#123;</span><br><span class="line">                            Activities =</span><br><span class="line">                            &#123;</span><br><span class="line">                                new Event(&quot;Reject:Lucy&quot;),</span><br><span class="line">                                new SetVariable</span><br><span class="line">                                &#123;</span><br><span class="line">                                    Variable = approvedVariable,</span><br><span class="line">                                    Value = new(false)</span><br><span class="line">                                &#125;,</span><br><span class="line">                                new WriteHttpResponse</span><br><span class="line">                                &#123;</span><br><span class="line">                                    Content = new(&quot;Sorry to hear that, Lucy!&quot;),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>第三步：</p><p>根据<code>approvedVariable</code>变量判定文章是否被审核通过。</p><p>如果通过则在控制台打印Document document-1 approved!, 否则打印Document document-1 rejected!</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">    new WriteLine(context =&gt; $&quot;Approved: &#123;approvedVariable.Get&lt;bool&gt;(context)&#125;&quot;),</span><br><span class="line">    new If(context =&gt; approvedVariable.Get&lt;bool&gt;(context))</span><br><span class="line">    &#123;</span><br><span class="line">        Then = new WriteLine(context =&gt; $&quot;Document $&#123;documentVariable.Get&lt;dynamic&gt;(context)!.Id&#125; approved!&quot;),</span><br><span class="line">        Else = new WriteLine(context =&gt; $&quot;Document $&#123;documentVariable.Get&lt;dynamic&gt;(context)!.Id&#125; rejected!&quot;)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Elsa工作流源码还提供了大量的Sample，这里就不一一列举了，</p><h2><span id="需求描述">需求描述</span></h2><p>根据不同的时间规则，发送下发问卷给客户填写。</p><p>下发问卷给用户填写，且填写有超时时间，期间要提醒用户答题，</p><p>如果问卷未在规定的时间内作答则，则作废，并提醒用户。</p><h2><span id="需求分析">需求分析</span></h2><p>我们将需求尽可能分解成为单一职责的功能单元，并定义这些功能单元的输入输出。</p><p>下发问卷任务 PublishQuestionnaireActivity</p><p>下发问卷是将问卷（Questionnaire）实例化成问卷实例（Survey），问卷实例绑定用户Id，用户在问卷实例上作答。明确输入和输出：</p><ul><li>输入：问卷ID</li><li>输出：问卷实例对象SurveyDto</li></ul><p>通知任务 NotificationActivity</p><p>通知在这个需求中需要发送问卷状态，时间等内容给对应的用户，同通至少包含标题和内容。</p><ul><li>输入：标题和内容</li><li>输出：无</li></ul><p>问卷状态跟踪任务 WaitFillInSurveyActivity </p><p>这个任务要追踪问卷实例的状态，当问卷实例状态为已完成时，可以继续执行后续任务。</p><ul><li>输入：问卷实例ID</li><li>输出：无</li></ul><p>定时和延时任务 </p><p>用于延时执行每个下发问卷的时间，等待问卷超时，以及延时发送通知等。</p><ul><li>输入：开始日期，延时日期，间隔时间或cron表达式</li><li>输出：无</li></ul><p>根任务</p><p>根任务包含所有的子任务，完成这个任务后，整个流程结束。在这个需求中根任务只需要知道将什么问卷，发送给哪位用户，以及在何时发送这三个问题。</p><ul><li>输入：问卷ID，用户ID，发送时间</li><li>输出：无</li></ul><p>各子任务参数对于他们的根任务是透明的（Invisible），根任务只需要关心是否完成，而不需要知道任务参数。</p><h2><span id="代码实现">代码实现</span></h2><h3><span id="下发问卷活动-publishquestionnaireactivity">下发问卷活动 PublishQuestionnaireActivity</span></h3><p>下发问卷任务可以抽象成为下发问卷活动 PublishQuestionnaireActivity<br>创建PublishQuestionnaireActivity类并设置输入QuestionnaireId，输出SurveyDto</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">public class PublishQuestionnaireActivity : Activity&lt;SurveyDto&gt;</span><br><span class="line">&#123;</span><br><span class="line">    public PublishQuestionnaireActivity()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    public PublishQuestionnaireActivity(long questionnaireId)</span><br><span class="line">    &#123;</span><br><span class="line">        QuestionnaireId = new Input&lt;long&gt;(questionnaireId);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    </span><br><span class="line">    public Input&lt;long&gt; QuestionnaireId &#123; get; set; &#125; = default!;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>重写ExecuteAsync方法，完成问卷下发逻辑</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)</span><br><span class="line">&#123;</span><br><span class="line">    var _surveyAppService = context.GetRequiredService&lt;ISurveyAppService&gt;();</span><br><span class="line">    if (_surveyAppService != null)</span><br><span class="line">    &#123;</span><br><span class="line">        var currentUserId = await context.GetInputValueAsync&lt;Guid&gt;(&quot;UserId&quot;);</span><br><span class="line">        var survey = await _surveyAppService.PublishAsync(new PublishInput()</span><br><span class="line">        &#123;</span><br><span class="line">            QuestionnaireId = this.QuestionnaireId.Get&lt;long&gt;(context),</span><br><span class="line">            UserId = currentUserId</span><br><span class="line"></span><br><span class="line">        &#125;) ?? throw new Exception(&quot;创建问卷失败&quot;);</span><br><span class="line">        context.SetResult(survey);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    await context.CompleteActivityAsync();</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>如此，其他的任务分别抽象成为相应的活动，这里展示完整代码</p><h3><span id="通知活动notificationactivity">通知活动：NotificationActivity</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">public class NotificationActivity : Activity</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    public NotificationActivity()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    public NotificationActivity(string title, string content)</span><br><span class="line">    &#123;</span><br><span class="line">        Content = new Input&lt;string&gt;(content);</span><br><span class="line">        Title = new Input&lt;string&gt;(title);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)</span><br><span class="line">    &#123;</span><br><span class="line">        var notificationManager = context.GetRequiredService&lt;NotificationManager&gt;();</span><br><span class="line"></span><br><span class="line">        if (notificationManager != null)</span><br><span class="line">        &#123;</span><br><span class="line">            var title = this.Title.Get(context);</span><br><span class="line">            var content = this.Content.Get(context);</span><br><span class="line">            var currentUserId = await context.GetInputValueAsync&lt;Guid&gt;(&quot;UserId&quot;);</span><br><span class="line">            var data = new CreatePrivateMessageNotificationEto(currentUserId, title, content);</span><br><span class="line">            await notificationManager.Send(data);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    await context.CompleteActivityAsync();</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">    public Input&lt;string&gt; Title &#123; get; set; &#125; = default!;</span><br><span class="line">    public Input&lt;string&gt; Content &#123; get; set; &#125; = default!;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="等待问卷完成活动waitfillinsurveyactivity">等待问卷完成活动：WaitFillInSurveyActivity</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line">public class WaitFillInSurveyActivity : Activity</span><br><span class="line">&#123;</span><br><span class="line">    public WaitFillInSurveyActivity()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    public WaitFillInSurveyActivity(Func&lt;ExpressionExecutionContext, long?&gt; surveyId)</span><br><span class="line">: this(Expression.DelegateExpression(surveyId))</span><br><span class="line">    &#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public WaitFillInSurveyActivity(long surveyId) =&gt; SurveyId = new Input&lt;long&gt;(surveyId);</span><br><span class="line"></span><br><span class="line">    public WaitFillInSurveyActivity(Expression expression) =&gt; SurveyId = new Input&lt;long&gt;(expression, new MemoryBlockReference());</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    /// &lt;inheritdoc /&gt;</span><br><span class="line">    protected override ValueTask ExecuteAsync(ActivityExecutionContext context)</span><br><span class="line">    &#123;</span><br><span class="line">        var surveyId = SurveyId.Get(context);</span><br><span class="line">        if (surveyId == default)</span><br><span class="line">        &#123;</span><br><span class="line">            var survey = context.ExpressionExecutionContext.GetLastResult&lt;SurveyDto&gt;();</span><br><span class="line">            surveyId = survey.Id;</span><br><span class="line">        &#125;</span><br><span class="line">        var payload = new WaitFillInSurveyBookmarkPayload(surveyId);</span><br><span class="line">        context.CreateBookmark(new CreateBookmarkArgs</span><br><span class="line">        &#123;</span><br><span class="line">            Payload = payload,</span><br><span class="line">            Callback = Resume,</span><br><span class="line">            BookmarkName = Type,</span><br><span class="line">            IncludeActivityInstanceId = false</span><br><span class="line">        &#125;);</span><br><span class="line">        return ValueTask.CompletedTask;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    private async ValueTask Resume(ActivityExecutionContext context)</span><br><span class="line">    &#123;</span><br><span class="line">        await context.CompleteActivityAsync();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    public Input&lt;long&gt; SurveyId &#123; get; set; &#125; = default!;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>此任务需要等待，我们创建一个Bookmark，注意创建Bookmark时，我们根据问卷实例SurveyId判断是否完成问卷的回答，因此指定<code>IncludeActivityInstanceId</code>为<code>false</code>，创建携带SurveyId的Payload类型：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">public record WaitFillInSurveyBookmarkPayload(long SurveyId);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在回调<code>OnResumeAsync</code>中，我们使用<code>context.CompleteActivityAsync</code>来完成任务。</p><h3><span id="定时和延时活动">定时和延时活动：</span></h3><p>Elsa.Scheduling库提供了用于定时和延时任务的触发器（触发器属于工作流的一种）</p><p><img src="/posts/6af59036/644861-20240321161052603-502103790.png"></p><p>在<a href="https://www.cnblogs.com/jevonsflash/p/18084921">[.NET项目实战] Elsa开源工作流组件应用（二）：内核解读</a> 一文 “构建 - 构建活动 “章节 列出了Elsa所有内建的活动。</p><p>这里使用Elsa内建的三个触发器：</p><p>StartAt 在未来特定的时间戳触发工作流触发器<br>Delay 延迟执行工作流触发器。<br>Timer 定期触发工作流触发器。</p><h3><span id="问卷活动questionnaireactivity">问卷活动：QuestionnaireActivity</span></h3><p>问卷活动是下发问卷，通知，等待填写问卷等活动的父级。</p><p>Elsa定义了容器类型的活动Container类型，其中的Activities可以包含其他活动。</p><p><img src="/posts/6af59036/644861-20240321161052651-2065371486.png"></p><p>Sequence和Parallel都是容器类型，是Activity的子类，它们分别表示并行和顺序执行。</p><p>除此之外我们还需要两个内建活动：</p><p>Fork：分支，用于分支并行执行，与Parallel类似，但比它多了一个等待完成功能。</p><p>通过ForkJoinMode属性，可以指定分支任务的执行方式，<code>ForkJoinMode.WaitAny</code>：等待任意一个任务完成，<code>ForkJoinMode.WaitAll</code>：等待所有任务完成。</p><p>Fault：故障，用于在工作流执行过程中，遇到异常时，触发故障。并结束工作流。</p><p>创建问卷活动类型QuestionnaireActivity，继承自Sequence类型，并设置一些属性，如问卷Id，问卷填写超时时间等。</p><p>[可选]Elsa在注册工作流时，Activity对象是会被序列化并存储到WorflowDefinition表中的， 因此这些属性可以被持久化到数据库中。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">public class QuestionnaireActivity : Sequence</span><br><span class="line">&#123;</span><br><span class="line">    //可选，用于持久化一些属性</span><br><span class="line">    public TimeSpan Delay &#123; get; set; &#125;</span><br><span class="line">    public DateTime StartAt &#123; get; set; &#125;</span><br><span class="line">    public TimeSpan Interval &#123; get; set; &#125;</span><br><span class="line">    public string Cron &#123; get; set; &#125;</span><br><span class="line">    public TimeSpan Duration &#123; get; set; &#125;</span><br><span class="line">    public long QuestionnaireId &#123; get; set; &#125;</span><br><span class="line">    public TimeSpan FillInTimeout &#123; get; set; &#125; = TimeSpan.FromHours(2);</span><br><span class="line"></span><br><span class="line">    public QuestionnaireActivity()</span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;   </span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>重写构造函数，并设置Activities属性</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><span class="line">public QuestionnaireActivity(long questionnaireId, TimeSpan fillInTimeout)</span><br><span class="line">&#123;</span><br><span class="line">    this.QuestionnaireId = questionnaireId;</span><br><span class="line">    this.FillInTimeout = fillInTimeout;</span><br><span class="line">    var currentSurvey = new Variable&lt;SurveyDto&gt;();</span><br><span class="line">    Variables.Add(currentSurvey);</span><br><span class="line">    Activities = new List&lt;IActivity&gt;()</span><br><span class="line">    &#123;</span><br><span class="line">        //流程开始打印</span><br><span class="line">        new WriteLine(&quot;问卷流程开始&quot;),</span><br><span class="line"></span><br><span class="line">        //下发问卷任务</span><br><span class="line">        new PublishQuestionnaireActivity(QuestionnaireId)</span><br><span class="line">        &#123;</span><br><span class="line">            Name=&quot;PublishQuestionnaire&quot;,</span><br><span class="line">            Result=new Output&lt;Questionnaire.Survey.Dto.SurveyDto&gt; (currentSurvey)</span><br><span class="line">        &#125;,</span><br><span class="line">        //问卷到达提醒             </span><br><span class="line">        new NotificationActivity(&quot;新问卷提醒&quot;, &quot;您有新的问卷，请查收&quot;),</span><br><span class="line"></span><br><span class="line">        //问卷处理分支 </span><br><span class="line">        new Fork</span><br><span class="line">        &#123;</span><br><span class="line">            JoinMode = ForkJoinMode.WaitAny,</span><br><span class="line">            Branches =</span><br><span class="line">            &#123;</span><br><span class="line">                //问卷即将过期提醒 </span><br><span class="line">                new Sequence</span><br><span class="line">                &#123;</span><br><span class="line">                    Activities =</span><br><span class="line">                    &#123;</span><br><span class="line">                        //等待</span><br><span class="line">                        new Delay</span><br><span class="line">                        &#123;</span><br><span class="line">                            Name = &quot;RemindDelay&quot;,</span><br><span class="line">                            TimeSpan = new(RemindDelay)</span><br><span class="line">                        &#125;,</span><br><span class="line">                        //通知</span><br><span class="line">                        new NotificationActivity(&quot;问卷即将超时&quot;, &quot;问卷即将超时，请尽快回答&quot;)</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;,</span><br><span class="line">                //问卷过期处理以及提醒 </span><br><span class="line">                new Sequence</span><br><span class="line">                &#123;</span><br><span class="line">                    Activities =</span><br><span class="line">                    &#123;</span><br><span class="line">                        //等待</span><br><span class="line">                        new Delay</span><br><span class="line">                        &#123;</span><br><span class="line">                            Name = &quot;TimeoutDelay&quot;,</span><br><span class="line">                            TimeSpan = new(FillInTimeout)</span><br><span class="line">                        &#125;,</span><br><span class="line">                        //通知</span><br><span class="line">                        new NotificationActivity(&quot;问卷已过期&quot;, &quot;问卷已过期，请等待工作人员处理&quot;),</span><br><span class="line"></span><br><span class="line">                        //处理</span><br><span class="line">                        new Fault()</span><br><span class="line">                        &#123;</span><br><span class="line">                            Message=new (&quot;问卷回答超时&quot;)</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;,</span><br><span class="line">                //问卷状态跟踪 </span><br><span class="line">                new Sequence</span><br><span class="line">                &#123;</span><br><span class="line">                    Activities =</span><br><span class="line">                    &#123;</span><br><span class="line">                        new WriteLine(&quot;开始等待问卷提交信号&quot;),</span><br><span class="line">                        new WaitFillInSurveyActivity(context =&gt; currentSurvey.Get&lt;SurveyDto&gt;(context)?.Id)</span><br><span class="line"></span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        //流程结束打印 </span><br><span class="line">        new WriteLine(&quot;完成流程结束&quot;),</span><br><span class="line">        new Finish(),</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="创建工作流">创建工作流</span></h3><p>现在我们来创建测试工作流，</p><ol><li>添加一个工作流参数UserId，用于各活动中对用户的查询依赖。</li><li>分别实现4个并行任务：延时发送问卷，定时发送问卷，定期间隔发送问卷，根据Cron表达式执行。和一个串行任务</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br></pre></td><td class="code"><pre><span class="line">public class Test1Workflow : WorkflowBase</span><br><span class="line">&#123;</span><br><span class="line">    public Guid UserId &#123; get; set; &#125;</span><br><span class="line">    protected override void Build(IWorkflowBuilder workflow)</span><br><span class="line">    &#123;</span><br><span class="line">        var startTime = new Variable&lt;DateTimeOffset&gt;();</span><br><span class="line">        workflow.Inputs.Add(</span><br><span class="line">                    new InputDefinition() &#123; Name = &quot;UserId&quot;, Type = typeof(Guid), StorageDriverType = typeof(WorkflowStorageDriver) &#125;</span><br><span class="line">            );</span><br><span class="line">        workflow.WithVariable(startTime);</span><br><span class="line"></span><br><span class="line">        workflow.Root = new Sequence</span><br><span class="line">        &#123;</span><br><span class="line">            Activities =</span><br><span class="line">                &#123;</span><br><span class="line">                    new WriteLine(&quot;Start&quot;),</span><br><span class="line">                    new SetVariable&lt;DateTimeOffset&gt;</span><br><span class="line">                    &#123;</span><br><span class="line">                        Variable = startTime,</span><br><span class="line">                        Value = new (DateTime.Now )</span><br><span class="line">                    &#125;,</span><br><span class="line">                    </span><br><span class="line">                    new Parallel()</span><br><span class="line">                    &#123;</span><br><span class="line">                        Activities =</span><br><span class="line">                        &#123;</span><br><span class="line">                            //并行任务1：延时发送问卷</span><br><span class="line">                            new Sequence()</span><br><span class="line">                            &#123;</span><br><span class="line">                                Activities =</span><br><span class="line">                                &#123;</span><br><span class="line">                                    //问卷1 将在工作流启动后1小时执行</span><br><span class="line"></span><br><span class="line">                                    new Delay(TimeSpan.FromHours(1)),</span><br><span class="line">                                    new QuestionnaireActivity(1),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;,</span><br><span class="line">                            //并行任务2：定时发送问卷</span><br><span class="line">                            new Sequence()</span><br><span class="line">                            &#123;</span><br><span class="line">                                Activities =</span><br><span class="line">                                &#123;</span><br><span class="line">                                    //问卷2 将在 2024-4-1 08:30:00 执行</span><br><span class="line">                                    new StartAt(new DateTime(2024,4,1,8,30,0)),</span><br><span class="line">                                    new Delay(TimeSpan.FromHours(2)),</span><br><span class="line">                                    new QuestionnaireActivity(2),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;,</span><br><span class="line">                            //并行任务3：定期间隔发送问卷</span><br><span class="line">                            new Sequence()</span><br><span class="line">                            &#123;</span><br><span class="line">                                Activities =</span><br><span class="line">                                &#123;</span><br><span class="line">                                    //问卷3 每隔两个小时执行</span><br><span class="line">                                    new Timer(new TimeSpan(2,0,0)),</span><br><span class="line"></span><br><span class="line">                                    new Delay(TimeSpan.FromHours(2)),</span><br><span class="line">                                    new QuestionnaireActivity(3),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;,</span><br><span class="line">                            //并行任务4：根据Cron表达式执行</span><br><span class="line">                            new Sequence()</span><br><span class="line">                            &#123;</span><br><span class="line">                                Activities =</span><br><span class="line">                                &#123;</span><br><span class="line">                                    //问卷4 每个月的最后一天上午10点执行任务</span><br><span class="line">                                    new Cron(cronExpression:&quot;0 0 10 L * ?&quot;),</span><br><span class="line"></span><br><span class="line">                                    new Delay(TimeSpan.FromHours(2)),</span><br><span class="line">                                    new QuestionnaireActivity(4),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;,</span><br><span class="line">                            //并行任务5：根据某时间发送问卷</span><br><span class="line">                            new Sequence()</span><br><span class="line">                            &#123;</span><br><span class="line">                                Activities =</span><br><span class="line">                                &#123;</span><br><span class="line">                                    new StartAt(context=&gt; startTime.Get(context).AddMinutes(90)),</span><br><span class="line"></span><br><span class="line">                                    new Delay(TimeSpan.FromHours(2)),</span><br><span class="line">                                    new QuestionnaireActivity(5),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;,</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">                            //串行任务</span><br><span class="line">                            new Sequence()</span><br><span class="line">                            &#123;</span><br><span class="line">                                Activities =</span><br><span class="line">                                &#123;</span><br><span class="line">                                    //问卷3 将在工作流启动后2小时执行</span><br><span class="line"></span><br><span class="line">                                    new Delay(TimeSpan.FromHours(2)),</span><br><span class="line">                                    new QuestionnaireActivity(3),</span><br><span class="line"></span><br><span class="line">                                        //问卷4 将在问卷3完成1天后执行</span><br><span class="line"></span><br><span class="line">                                    new Delay(TimeSpan.FromDays(1)),</span><br><span class="line">                                    new QuestionnaireActivity(4),</span><br><span class="line"></span><br><span class="line">                                    //问卷5 将在问卷4完成3天后执行</span><br><span class="line"></span><br><span class="line">                                    new Delay(TimeSpan.FromDays(3)),</span><br><span class="line">                                    new QuestionnaireActivity(5),</span><br><span class="line">                                &#125;</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;,</span><br><span class="line">                    new Finish(),</span><br><span class="line">            &#125;,</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="开始工作流">开始工作流</span></h3><p>工作流启动参数需设置Input对象</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">var input = new Dictionary&lt;string, object&gt;</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">    &#123;&quot;UserId&quot;, &quot;D1522DBC-5BFC-6173-EB60-3A114454350C&quot;&#125;,</span><br><span class="line"></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">var startWorkflowOptions = new StartWorkflowRuntimeOptions</span><br><span class="line">&#123;</span><br><span class="line">    Input = input,</span><br><span class="line">    VersionOptions = versionOptions,</span><br><span class="line">    InstanceId = instanceId,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">// Start the workflow.</span><br><span class="line">var result = await _workflowRuntime.StartWorkflowAsync(workflowDefinition.DefinitionId, startWorkflowOptions);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>下面进入喜闻乐见的踩坑填坑环节</p><h2><span id="troubleshooting">TroubleShooting</span></h2><ol><li><p>在活动中执行异步操作时，会导致报错：</p><p>如下面的代码，执行Excute方法中的 context.CompleteActivityAsync()方法，时报错</p></li></ol><p> <img src="/posts/6af59036/644861-20240321161052786-1849489643.png"></p><p> <img src="/posts/6af59036/644861-20240321161052704-255701875.png"></p><p> 原因分析：scope资源被提前释放</p><p> 代码先执行到了112行，scope释放</p><p> <img src="/posts/6af59036/644861-20240321161052564-1874368167.png"></p><p> 解决：带有异步的操作一定要使用ExecuteAsync方法</p><p> <img src="/posts/6af59036/644861-20240321161052796-753247424.png"></p><ol start="2"><li>delay之后，Workflow的Input无法访问</li></ol><p>原因分析：</p><p>Delay或其他Schedule类型的Activity，通过创建Bookmark挂起任务，当任务被唤醒时，input被workflowState.Output替换掉，和原先的input不一样了。</p><p><img src="/posts/6af59036/644861-20240321161052805-23612915.png"></p><p>解决：</p><p>虽然input被替换了，但数据库的input还在，可以通过workflowInstanceId先取回workflowInstance对象，再通过<code>instance.WorkflowState.Input.TryGetValue</code>方法获取原始input值。</p><p>可以创建一个一个扩展方法GetInputValueAsync，Delay之后的活动中调用即可。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">public static async Task&lt;TValue&gt; GetInputValueAsync&lt;TValue&gt;(this ActivityExecutionContext context, string name)</span><br><span class="line">&#123;</span><br><span class="line">    TValue value;</span><br><span class="line">    if (!context.TryGetWorkflowInput(name, out value))</span><br><span class="line">    &#123;</span><br><span class="line">        var workflowInstanceStore = context.GetRequiredService&lt;IWorkflowInstanceStore&gt;();</span><br><span class="line"></span><br><span class="line">        var instance = await workflowInstanceStore.FindAsync(new WorkflowInstanceFilter()</span><br><span class="line">        &#123;</span><br><span class="line">            Id = context.WorkflowExecutionContext.Id</span><br><span class="line"></span><br><span class="line">        &#125;);</span><br><span class="line">        if (instance != null)</span><br><span class="line">        &#123;</span><br><span class="line">            instance.WorkflowState.Input.TryGetValue(name, out value);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    return value;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在Activity中调用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">await context.GetInputValueAsync&lt;Guid&gt;(&quot;UserId&quot;);</span><br></pre></td></tr></table></figure><p>持续更新中…</p><p>–完结–</p>]]></content>
    
    
    <summary type="html">之前的文章简单介绍了工作流和Elsa工作流库，这里再补充说明两点工作流的使用场景非常广泛，几乎涵盖了所有需要进行业务流程自动化管理的领域。学习一个开源库，最简单的方法就是看源码，Elsa的工作流引擎源码非常简单易懂，并且提供了非常丰富的示例代码，举一个例子：审批工作流示例这个审批流是这样的：作者发来一个文章，有两个审批人需要全部审批通过，文章才算通过，否则退回。</summary>
    
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/categories/NET/"/>
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/tags/NET/"/>
    
    <category term="工作流" scheme="https://blog.matoapp.net/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    
    <category term="workflow" scheme="https://blog.matoapp.net/tags/workflow/"/>
    
  </entry>
  
  <entry>
    <title>[.NET项目实战] Elsa开源工作流组件应用（二）：内核解读</title>
    <link href="https://blog.matoapp.net/posts/d5b810ff/"/>
    <id>https://blog.matoapp.net/posts/d5b810ff/</id>
    <published>2024-03-20T11:53:00.000Z</published>
    <updated>2025-09-24T16:30:02.366Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E5%AE%9A%E4%B9%89">定义</a><ul><li><a href="#%E5%8F%98%E9%87%8F">变量</a><ul><li><a href="#%E5%86%85%E5%AD%98%E5%AF%84%E5%AD%98%E5%99%A8%E7%B1%BB">内存寄存器类</a></li><li><a href="#%E5%AF%84%E5%AD%98%E5%99%A8%E4%B8%AD%E7%9A%84%E5%AD%98%E5%82%A8%E5%8C%BA%E5%9D%97%E7%B1%BB">寄存器中的存储区块类</a></li><li><a href="#%E5%8F%98%E9%87%8F%E5%88%B0%E5%AD%98%E5%82%A8%E7%9A%84%E6%98%A0%E5%B0%84%E7%B1%BB">变量到存储的映射类</a></li></ul></li><li><a href="#%E4%B8%8A%E4%B8%8B%E6%96%87%E5%AF%B9%E8%B1%A1">上下文对象</a><ul><li><a href="#%E6%B4%BB%E5%8A%A8%E4%B8%8A%E4%B8%8B%E6%96%87activityexecutioncontext">活动上下文（ActivityExecutionContext）</a></li><li><a href="#%E5%B7%A5%E4%BD%9C%E6%B5%81%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87workflowexecutioncontext">工作流执行上下文（WorkflowExecutionContext）</a></li><li><a href="#%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87expressionexecutioncontext">表达式执行上下文（ExpressionExecutionContext）</a></li></ul></li></ul></li><li><a href="#%E6%9E%84%E5%BB%BA">构建</a><ul><li><a href="#%E6%9E%84%E5%BB%BA%E6%B4%BB%E5%8A%A8">构建活动</a></li><li><a href="#%E6%9E%84%E5%BB%BA%E5%B7%A5%E4%BD%9C%E6%B5%81">构建工作流</a></li></ul></li><li><a href="#%E8%BF%90%E8%A1%8C">运行</a><ul><li><a href="#%E6%B3%A8%E5%86%8C">注册</a><ul><li><a href="#%E6%B3%A8%E5%86%8C%E5%B7%A5%E4%BD%9C%E6%B5%81">注册工作流</a></li><li><a href="#%E6%B3%A8%E5%86%8C%E6%B4%BB%E5%8A%A8">注册活动</a></li><li><a href="#%E5%A1%AB%E5%85%85">填充</a></li></ul></li><li><a href="#invoke%E6%B4%BB%E5%8A%A8">Invoke活动</a></li></ul></li><li><a href="#%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7">可观测性</a><ul><li><a href="#%E8%AE%BE%E8%AE%A1%E5%99%A8%E4%B8%8Eapis">设计器与APIs</a></li><li><a href="#%E5%B7%A5%E4%BD%9C%E6%B5%81%E9%85%8D%E7%BD%AE">工作流配置</a></li><li><a href="#%E6%9F%A5%E7%9C%8B%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%8A%B6%E6%80%81">查看工作流状态</a></li></ul></li></ul><!-- tocstop --><p>本篇将带你深入分析Elsa工作流原理，排除干扰展示关键代码段，以加深理解</p><h1><span id="定义">定义</span></h1><h2><span id="变量">变量</span></h2><p>Elsa工作原理可以抽象理解为管道中间件 + 异步模型</p><p>Elsa中，活动的变量的获取和设置都是异步的。Elsa定义了<code>Variable</code>类型作为异步操作的结果或者说是异步操作的占位符，这个变量在运行的时候才会填充数值。这与我们熟悉C#中的Task，或者js里的promise对象作用相同。输入Input，OutPut都属于 Variable。</p><p>Elsa模拟了内存寄存器（MemoryRegister）以及Set和Get访问器实现异步模型。</p><h3><span id="内存寄存器类">内存寄存器类</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">public class MemoryRegister</span><br><span class="line">&#123;</span><br><span class="line">    ...</span><br><span class="line">    public IDictionary&lt;string, MemoryBlock&gt; Blocks &#123; get; &#125;</span><br><span class="line">    </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3><span id="寄存器中的存储区块类">寄存器中的存储区块类</span></h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">public class MemoryBlock</span><br><span class="line">&#123;</span><br><span class="line">    ...</span><br><span class="line">    /// &lt;summary&gt;</span><br><span class="line">    /// The value stored in this block.</span><br><span class="line">    /// &lt;/summary&gt;</span><br><span class="line">    public object? Value &#123; get; set; &#125;</span><br><span class="line">    </span><br><span class="line">    /// &lt;summary&gt;</span><br><span class="line">    /// Optional metadata about this block.</span><br><span class="line">    /// &lt;/summary&gt;</span><br><span class="line">    public object? Metadata &#123; get; set; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3><span id="变量到存储的映射类">变量到存储的映射类</span></h3><p>Id可以代表变量在内存区块中的引用地址</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">public class MemoryBlockReference</span><br><span class="line">&#123;</span><br><span class="line">    </span><br><span class="line">    /// &lt;summary&gt;</span><br><span class="line">    /// The ID of the memory block.</span><br><span class="line">    /// &lt;/summary&gt;</span><br><span class="line">    public string Id &#123; get; set; &#125; = default!;</span><br><span class="line"></span><br><span class="line">    public object? Get(MemoryRegister memoryRegister) =&gt; GetBlock(memoryRegister).Value;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>构建活动时将创建活动中变量到存储区块的映射，分配一个引用给变量</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">public void AssignInputOutputs(IActivity activity)</span><br><span class="line">&#123;</span><br><span class="line">    var activityDescriptor = _activityRegistry.Find(activity.Type, activity.Version) ?? throw new Exception(&quot;Activity descriptor not found&quot;);</span><br><span class="line">    var inputs = activityDescriptor.GetWrappedInputProperties(activity).Values.Cast&lt;Input&gt;().ToList();</span><br><span class="line">    var seed = 0;</span><br><span class="line"></span><br><span class="line">    foreach (var input in inputs)</span><br><span class="line">    &#123;</span><br><span class="line">        var blockReference = input?.MemoryBlockReference();</span><br><span class="line"></span><br><span class="line">        if (blockReference != null!)</span><br><span class="line">            if (string.IsNullOrEmpty(blockReference.Id))</span><br><span class="line">                blockReference.Id = $&quot;&#123;activity.Id&#125;:input-&#123;seed&#125;&quot;;</span><br><span class="line"></span><br><span class="line">        seed++;</span><br><span class="line">    &#125;</span><br><span class="line">   ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>异步变量获取和设置：</p><p>可以通过上下文对象的Set，和Get方法，异步获取和设置异步变量。</p><h2><span id="上下文对象">上下文对象</span></h2><p>查看源码可以看到Elsa定义了如下Context</p><p><img src="/posts/d5b810ff/644861-20240320115236552-1758774440.png"></p><p>其中比较重要的上下文对象：</p><h3><span id="活动上下文activityexecutioncontext">活动上下文（ActivityExecutionContext）</span></h3><p>活动上下文对象由Elsa.Runtime提供，在工作流执行函数中可供访问。通过它可访问包含活动实例、当前输入和输出等。通过它可以访问当前活动所在的工作流执行上下文。</p><h3><span id="工作流执行上下文workflowexecutioncontext">工作流执行上下文（WorkflowExecutionContext）</span></h3><p>工作流上下文对象由Elsa.Runtime提供，可通过活动上下文（ActivityExecutionContext）访问其所属工作流执行上下文。通过它可访问包含工作流实例、当前活动、当前输入和输出等。</p><h3><span id="表达式执行上下文expressionexecutioncontext">表达式执行上下文（ExpressionExecutionContext）</span></h3><p>表达式执行上下文用于在构建活动时传递内存变量（输入，输出），其中包含MemoryRegister对象。</p><p>通过表达式执行上下文（ExpressionExecutionContext）获取到变量的值：<br><img src="/posts/d5b810ff/644861-20240320115236561-328835155.png"></p><h1><span id="构建">构建</span></h1><h2><span id="构建活动">构建活动</span></h2><p>Elsa默认帮我们建立了这些活动：</p><p><img src="/posts/d5b810ff/644861-20240320115236429-1939378738.png"></p><p><img src="/posts/d5b810ff/644861-20240320115236563-593349315.png"></p><p>他们都实现了IActivity接口，Activity和CodeActivity是IActivity的实现类，对应的是一个空的活动，（CodeActivity是带有自动完成功能的空活动）</p><p>我们要做的是继承这个活动，重写Execute方法以实现我们自己的业务。比如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">public class HelloWorld : Activity</span><br><span class="line">&#123;</span><br><span class="line">    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)</span><br><span class="line">    &#123;</span><br><span class="line">        Console.WriteLine(&quot;Hello World!&quot;);</span><br><span class="line">        await CompleteAsync();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以官方默认的WiteLine为例，这个类的Execute代码如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">protected override void Execute(ActivityExecutionContext context)</span><br><span class="line">&#123;</span><br><span class="line">    var text = context.Get(Text);</span><br><span class="line">    var provider = context.GetService&lt;IStandardOutStreamProvider&gt;() ?? new StandardOutStreamProvider(System.Console.Out);</span><br><span class="line">    var textWriter = provider.GetTextWriter();</span><br><span class="line">    textWriter.WriteLine(text);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2><span id="构建工作流">构建工作流</span></h2><p>首要目标是拿到一个工作流对象（Workflow），Elsa启动时会从工作流提供者（IWorkflowProvider）获取所有能用的工作流。并注册到资源池中</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">public interface IWorkflowProvider</span><br><span class="line">&#123;</span><br><span class="line">    string Name &#123; get; &#125;</span><br><span class="line"></span><br><span class="line">    ValueTask&lt;IEnumerable&lt;MaterializedWorkflow&gt;&gt; GetWorkflowsAsync(CancellationToken cancellationToken = default);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Elsa默认的实现类是如下两种，BlobStorageWorkflowProvider将从数据库（BlobStorage）中反序列化来注册。ClrWorkflowProvider使用工作流构建器注册。</p><p><img src="/posts/d5b810ff/644861-20240320115236405-79649478.png"></p><p>我们先定义工作流描述类，它继承自IWorkflow, WorkflowBase是IWorkflow的抽象基类</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">class SequentialWorkflow : WorkflowBase</span><br><span class="line">&#123;</span><br><span class="line">    protected override void Build(IWorkflowBuilder workflow)</span><br><span class="line">    &#123;</span><br><span class="line">        workflow.Root = new Sequence</span><br><span class="line">        &#123;</span><br><span class="line">            Activities =</span><br><span class="line">            &#123;</span><br><span class="line">                new WriteLine(&quot;Line 1&quot;),</span><br><span class="line">                new WriteLine(&quot;Line 2&quot;),</span><br><span class="line">                new WriteLine(&quot;Line 3&quot;)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br></pre></td></tr></table></figure><p>Elsa初始化时，WorkflowBuilder会构建程序集中所有实现IWorkflow的类。</p><p>WorkflowBuilder中的BuildWorkflowAsync方法会将工作流描述类IWorkflow对象构建成Workflow对象。</p><p><img src="/posts/d5b810ff/644861-20240320115236382-1644277513.png"></p><p>这里思考一个问题：终执行的代码是在活动中定义的，但为什么返回的是Workflow对象？通过代码研读，实际上Workflow也是一个IActivity活动，只不过它具有一个Root根节点的复合活动。活动的定义请参考<a href="https://v3.elsaworkflows.io/docs/core-concepts/workflow">官方文档</a></p><p>BuildWorkflowAsync中的具体实现如下：</p><p> <img src="/posts/d5b810ff/644861-20240320115236496-1432091721.png"></p><h1><span id="运行">运行</span></h1><h2><span id="注册">注册</span></h2><p>注册包括注册工作流和注册活动，配置Elsa时需要使用如下两个方法：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">.AddActivitiesFrom&lt;Program&gt;()</span><br><span class="line">.AddWorkflowsFrom&lt;Program&gt;()</span><br></pre></td></tr></table></figure><h3><span id="注册工作流">注册工作流</span></h3><p>工作流可以通过ClrWorkflowProvider，使用工作流构建器注册，也可以从本地存储（BlobStorage）中反序列化来注册。<br>代码构建的工作流是通过实现IWorkflow接口，在Elsa初始化时将工作流注册到工作流定义持久化到数据库的WorkflowDefinition表中</p><p>通过工作流构建器注册：</p><p><img src="/posts/d5b810ff/644861-20240320115236439-1953843656.png"></p><h3><span id="注册活动">注册活动</span></h3><p>Elsa使用描述器（IActivityDescriber）提供一个描述符(ActivityDescriptor)，这里比较绕，阅读源码可以发现，其实是通过各种反射获取活动派生类的特征数据（有的系统喜欢将称之为元数据），封装这些数据的类型称之为描述符，特征数据可以作为在界面上显示，分组，排序的信息。</p><p><img src="/posts/d5b810ff/644861-20240320115236597-216267489.png"></p><p>活动不同于工作流，它在运行中不持久化于数据库，而是以注册表的形式存储于内存中。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IDictionary&lt;(string Type, int Version), ActivityDescriptor&gt; _activityDescriptors</span><br></pre></td></tr></table></figure><p>在构建工作流的时候自动注册活动，也可以通过实现IActivity接口，在Elsa初始化时将所有活动注册到注册表中<br><img src="/posts/d5b810ff/644861-20240320115236434-1803764689.png"></p><p>Elsa启动时将所有实现了IActivity接口的类型注册为活动：</p><p><img src="/posts/d5b810ff/644861-20240320115236433-1874425658.png"></p><h3><span id="填充">填充</span></h3><p>启动时填充活动注册表和工作流定义表。<br>官方也给出了说明，各填充两次确保活动注册表和工作流定义表都是最新的：</p><p><img src="/posts/d5b810ff/644861-20240320115236398-437973109.png"></p><p>阶段一：填充活动注册表<br>因为工作流定义可以用作活动，需要确保在填充工作流定义表之前填充活动注册表。</p><p>阶段二：填充工作流定义表</p><p>阶段三：重新填充活动注册表<br>填充了工作流定义表之后，我们需要重新填充活动注册表，以确保活动描述符是最新的。</p><p>阶段四：用当前的活动集重新更新工作流定义表。<br>最后，需要重新填充工作流定义表，以确保工作流定义是最新的。</p><h2><span id="invoke活动">Invoke活动</span></h2><p>Elsa默认的管道中间件：</p><p> <img src="/posts/d5b810ff/644861-20240320115236481-820532439.png"></p><p>Elsa注册执行活动的中间件（DefaultActivityInvokerMiddleware）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">public static class ActivityInvokerMiddlewareExtensions</span><br><span class="line">&#123;</span><br><span class="line">    /// &lt;summary&gt;</span><br><span class="line">    /// Adds the &lt;see cref=&quot;DefaultActivityInvokerMiddleware&quot;/&gt; component to the pipeline.</span><br><span class="line">    /// &lt;/summary&gt;</span><br><span class="line">    public static IActivityExecutionPipelineBuilder UseDefaultActivityInvoker(this IActivityExecutionPipelineBuilder pipelineBuilder) =&gt; pipelineBuilder.UseMiddleware&lt;DefaultActivityInvokerMiddleware&gt;();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p> <img src="/posts/d5b810ff/644861-20240320115236605-67287203.png"></p><p>在执行活动的中间件（DefaultActivityInvokerMiddleware），最终活动被调用的代码如下：</p><p><img src="/posts/d5b810ff/644861-20240320115236475-210574615.png"></p><p>可以看见，Elsa最终以反射的方式创建一个Activity实例，然后调用它的ExecuteAsync方法。</p><h1><span id="可观测性">可观测性</span></h1><h2><span id="设计器与apis">设计器与APIs</span></h2><p>实际上，Elsa的运行时和设计器是完全分离的。Elsa提供了一个基于Blazor的设计工具，它作为独立的项目发布在Github上： <a href="https://github.com/elsa-workflows/elsa-studio">Elsa-Studio</a></p><p><img src="/posts/d5b810ff/644861-20240320115236543-1296648211.png"></p><p>因为和接口交互是通过REST API实现的，所以你也可以使用任何你想要的客户端来实现。</p><p>接设计器默认的HTTP API实现在<code>Elsa.Workflows.Api</code>库中，用于支持设计器的增删改查业务。</p><p>如果仅要使用工作流引擎，可以使用<code>Elsa.Workflows.Management</code>库，它只包含对于工作流的管理而不涉及HTTP接口。</p><h2><span id="工作流配置">工作流配置</span></h2><p>打开设计器，点击“工作流（Workflow）”菜单，然后单击“定义（Definition）”选项卡。可以看到一个工作流定义的列表。点击右上角新增按钮，</p><p><img src="/posts/d5b810ff/644861-20240320115236548-1560938102.png"></p><p>在打开的页面中，拖拽活动到工作流图上，然后单击“保存（Save）”按钮。</p><p><img src="/posts/d5b810ff/644861-20240320115236565-1355638887.png"></p><p>在浏览器的网络请求中可以看到一个POST请求，请求地址为&#x2F;workflow&#x2F;definitions，请求参数为JSON格式，后端服务中WorkflowDefinitions的Endpoint中将对编辑器的“保存”请求进行处理</p><p>在请求负载中，WorkflowDefinitionModel字段会包含工作流定义和Root活动。</p><p>默认实现会将工作流定义和根活动序列化为JSON，并将其保存到数据库中。其中根活动在数据库WorkflowDefinition表的StringData列中存储。</p><p><img src="/posts/d5b810ff/644861-20240320115236549-350575687.png"></p><p>当工作流执行时，Elsa会实例化（Materialize）Workflow对象</p><p> <img src="/posts/d5b810ff/644861-20240320115236363-582486322.png"><br><img src="/posts/d5b810ff/644861-20240320115236460-343182715.png"></p><p><img src="/posts/d5b810ff/644861-20240320115236557-1759170225.png"></p><p>其中RootActivity会反序列化，可以看到StringData会被反序列化为IActivity对象</p><p> <img src="/posts/d5b810ff/644861-20240320115236340-890689426.png"></p><p><img src="/posts/d5b810ff/644861-20240320115236463-697611256.png"></p><h2><span id="查看工作流状态">查看工作流状态</span></h2><p>Elsa定义了不同的接口和数据库</p><p>主要的接口如下：</p><p>workflowDefinition：工作流定义接口，数据来自WorkflowDefinition表<br>workflowInstance：工作流实例接口，数据来自WorkflowInstance表<br>activity-execution：活动执行接口，查询活动的Id、状态以及结果，输入输出等上下文数据，数据主要通过查询ActivityExecutionRecords表来获取。<br>journal: 活动执行日志，数据来自WorkflowExecutionLogRecords表</p><p>打开设计器，点击“工作流（Workflow）”菜单，然后单击“实例（Instance）”选项卡。可以看到一个工作实例列表</p><p><img src="/posts/d5b810ff/644861-20240320115236615-1469679961.png"></p><p>点击条目即可查看工作流的执行日志和各活动的执行信息。Web页面中各片区的数据来源分布大致如下：</p><p><img src="/posts/d5b810ff/644861-20240320115236548-467455528.png"></p><p>其中页面中央的工作流编辑器显示了工作流的结构，结合工作流的执行日志，可以直观的看到工作流的执行情况。可观测到执行的步骤，以及执行的耗时。</p><p> –完结–</p>]]></content>
    
    
    <summary type="html">Elsa工作原理可以抽象理解为管道中间件 + 异步模型Elsa中，活动的变量的获取和设置都是异步的。Elsa定义了Variable类型作为异步操作的结果或者说是异步操作的占位符，这个变量在运行的时候才会填充数值。这与我们熟悉C#中的Task，或者js里的promise对象作用相同。输入Input，OutPut都属于 Variable。Elsa模拟了内存寄存器（MemoryRegister）以及Set和Get访问器实现异步模型。</summary>
    
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/categories/NET/"/>
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/tags/NET/"/>
    
    <category term="工作流" scheme="https://blog.matoapp.net/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    
    <category term="workflow" scheme="https://blog.matoapp.net/tags/workflow/"/>
    
  </entry>
  
  <entry>
    <title>深度观察2024中国系统架构师大会（SACC）</title>
    <link href="https://blog.matoapp.net/posts/a3da18a9/"/>
    <id>https://blog.matoapp.net/posts/a3da18a9/</id>
    <published>2024-03-19T14:34:00.000Z</published>
    <updated>2025-09-24T16:30:02.738Z</updated>
    
    <content type="html"><![CDATA[<p>今年的中国系统架构师大会（SACC）在我所在的城市广州举办，很荣幸受邀参加。这次能接触到国内最优秀的架构师，学习他们的架构思想和行业经验。对我而言非常有意义。</p><p><img src="/posts/a3da18a9/644861-20240319142514186-21182536.jpg"></p><p>大会分为上下午共4场，我参加了上午的多云多活架构设计专场和下午的AIGC专场。</p><p><img src="/posts/a3da18a9/644861-20240319143126878-924581643.jpg"></p><p>本篇文章就多云多活架构设计专场，我选取几位老师的观点进行分享。（我并不是架构师，只是对架构感兴趣，如有错误，还请指正）</p><h3><span id="张晓辉-flomesh-高级云原生架构师">张晓辉 Flomesh 高级云原生架构师</span></h3><p><img src="/posts/a3da18a9/644861-20240319142514896-1447557337.png"></p><p>张老师分享了混合多云架构的技术方案，这里可以体现一个企业在技术层面发展到什么样的程度，根据这张图可以看看企业架构的复杂程度。可能大部分企业还在容器化，分布式架构和单一集群的复杂程度。</p><p><img src="/posts/a3da18a9/644861-20240319142514014-1441457233.png"></p><p>在资源抽象层面，利用统一的K8s Api，可以实现不同云厂商的资源抽象，实现资源的复用。K8s多集群可以作为防腐层，屏蔽基础设施层差异，避免厂商锁定</p><p><img src="/posts/a3da18a9/644861-20240319142514936-1288726126.png"></p><h3><span id="李中原-平安银行股份有限公司-国产数据库技术负责人">李中原  （平安银行股份有限公司 国产数据库技术负责人  ）</span></h3><p><img src="/posts/a3da18a9/644861-20240319142514172-525297457.png"></p><p>银行系统的技术架构需要满足高吞吐、低延迟要求，业务可靠性的要求，以及数据安全性的要求，同时，基于监管和法规的要求，技术架构需要自主可控。</p><p>李老师分享了银行架构的“去IOE化”的变迁。</p><p><img src="/posts/a3da18a9/644861-20240319142514133-875312736.png"></p><p>对于可靠性要求，从传统的不能容许服务中断，逐渐过渡到允许服务中断但必须迅速恢复。</p><h3><span id="黄奕青-腾讯云-技术专家">黄奕青 （腾讯云 技术专家）</span></h3><p>黄老师对两位讲师做了补充，着重对异地多活架构中的问题，以及解决方案和设计要点等“三阶六要点”进行了分享。</p><ol><li><strong>技术架构的三个阶段</strong>：</li></ol><ul><li>第一阶段是对业务需求的解读：一个是传统的需求分析，一个是企业聘请领域专家对需求建模</li><li>第二阶段是确立架构，目前较为合理的架构是标准的微服务架构，已经被广泛应用（两地三中心架构）。</li><li>第三阶段是在微服务架构之上构建单元化，该阶段需要解决如下异地多活架构中的挑战。</li></ul><ol start="2"><li><strong>异地多活架构的挑战</strong>：</li></ol><ul><li>因为网络基础设施的物理局限性，需要考虑异地资源使用时的延迟问题。</li><li>数据访问效率问题：跨地域的数据访问速度慢会导致核心交易延迟，需要考虑数据切分和流量闭环治理。</li><li>分布式事务成本问题：跨地域的分布式事务会带来高成本和复杂性。</li><li>中间件的单元化考虑：在异地架构中，需要考虑中间件的单元化和调度。</li></ul><ol start="3"><li><strong>六大设计要点</strong>：</li></ol><ul><li>数据切分：设计大型分布式系统时首先要考虑数据如何切分。 数据按什么维度切分最合适，(如按客户号，还是哈希，按range范围？)</li><li>流量闭环与单元化架构：黄老师强调了流量闭环的重要性，特别是在单元化架构中。这意味着流量需要在特定的单元内部循环，而不是跨单元流动，以减少无效的横向流量。为了实现这一目标，网关需要具有状态感知能力，能够识别客户信息并将流量路由到正确的单元。此外，单元和上层计算资源之间的逻辑绑定也是关键。</li><li>中间件单元化：在单元化架构中，中间件（如MQ、Redis等）的链路也需要考虑单元化。特别是在跨单元的场景下，如客户之间的转账，会涉及到分布式事务的问题。这种分布式事务的调度和管理在异地多活架构下会被放大，成为实现的难点。</li><li>数据切分与聚合查询：数据被切分到多个库后，聚合查询变得困难。黄老师提到了从前在一个库下可以轻松进行多表的聚合查询，但现在需要访问多个库进行查询。这需要对现有的查询策略和技术进行重新的设计和优化。</li><li>业务连续性保障：单元之间的互备和业务连续性保障是必要的。</li><li>分布式运维：针对大型核心系统的分布式运维需要特别关注。</li></ul><h3><span id="戴骏贤-网易游戏资深数据库系统工程师">戴骏贤 （网易游戏资深数据库系统工程师 ）</span></h3><p>网易游戏对于RPO的要求相对苛刻（数据要求无损）。但更多是以成本最小化为考量实现双活高可用架构。因此目前仅采用同城双副本。</p><h3><span id="李运华-互联网大厂-资深技术专家">李运华 互联网大厂 资深技术专家</span></h3><p>企业是否需要上云取决于成本和技术实力两个主要因素。</p><ol><li><strong>成本考量</strong>：</li></ol><ul><li><strong>企业规模</strong>：一般来说，如果企业的服务器规模在一千台以下，上云通常是成本效益较高的选择。对于规模在一千到五千台之间的企业，上云可能并不一定省成本，而取决于其在云平台上的使用程度。然而，如果服务器规模超过五千台，可能会发现下云（即从云平台迁移到自建环境）会更经济合算，尤其是对于大型企业。这是因为大型企业可能会拥有足够的资源和资金来自建云或自建机房，并且会节省大量成本。</li><li><strong>全面成本考量</strong>：除了物理服务器成本之外，还需要考虑人员成本、维护成本等方面的费用。这些费用在决定是否上云时也至关重要。</li></ul><ol start="2"><li><strong>技术实力</strong>：</li></ol><ul><li><strong>企业规模与技术实力</strong>：大型企业通常拥有更多的人力资源和技术实力，因此更有能力独立管理和运维自己的服务器环境。相比之下，中小型企业可能缺乏专业技能和资源，难以应对复杂的技术挑战，因此更倾向于使用云服务提供商提供的解决方案。</li><li><strong>云产品的功能和强大程度</strong>：云服务提供商提供了丰富的云产品和服务，包括分布式一致性数据库等高级功能。然而，自行实现这些功能需要大量的时间、资源和技术实力。对于中小型企业而言，使用云平台提供的功能可能更为经济实惠和可行。（ocean base一个几百上千人的团队，从2013年开始，搞了12年）</li></ul><p>同城双活和异地多活有哪些方案？</p><ol><li><p><strong>同城双活方案</strong>：</p><p>由于同城机房之间的网络延迟可以做到非常低，通常可以在几毫秒以内，因此同城双核在逻辑上可以看作是一个统一的机房。可以直接按照集群的方式运作，成本和复杂度会低。</p></li><li><p><strong>近邻的异地多活</strong>：<br>这种方案通常是指在相邻的两个城市之间进行异地多活部署。例如，广州与深圳、杭州与上海等相邻城市之间的部署。由于网络延迟相对较低，大约在十毫秒左右，因此可以接受。近邻的异地多活方案通常可以通过分布式一致性算法来实现投票和选举，确保系统的可用性。</p></li><li><p><strong>远端的异地多活</strong>：<br>在距离较远的城市之间进行异地多活部署，如广州与北京之间的部署。网络延迟可能较高（30ms以上），本质上没有办法实现这种集群的运作。</p></li></ol><p><img src="/posts/a3da18a9/644861-20240319143236095-2129393810.jpg"></p><h3><span id="马洪喜深圳行云创新科技有限公司-ceo">马洪喜（深圳行云创新科技有限公司 CEO）</span></h3><p>马老师分享了三个不同企业的案例：</p><ol><li><strong>区域性银行的高可用解决方案</strong>：</li></ol><ul><li>该银行实施了同城双活的云原生高可用解决方案，这是一个强需求，因为银行业务对高可用性有极高的要求。</li><li>他们的方案是基于一个调度器实现的，使得业务在发生故障时可以切换到单机群模式。</li></ul><ol start="2"><li><strong>某超大型电器公司的混合云方案</strong>：</li></ol><ul><li>该公司实施了混合云，将国内业务部署在私有云上，将海外业务部署在AWS上。</li><li>这是出于业务需求的考虑，例如海外业务需要在AWS上就近访问。</li></ul><ol start="3"><li><strong>某锂电制造企业的工业场景需求</strong>：</li></ol><ul><li>该企业有着复杂的IT&#x2F;OT融合场景，需要在制造业中实现微服务和AI的应用。</li><li>他们的需求包括在本地数据中心或厂区内的小型数据中心处理数据，而不是将数据从生产线传送回总部或云平台。</li><li>制造业趋向于建立一个多级算力体系，包括本地数据中心、云平台和边缘计算，而不仅仅是简单的双活或多活部署。</li></ul><p>另外在与公有云对接的过程中，确实会遇到一些挑战和坑：比如公有云平台通常会限制用户在容器服务中运行某些特定类型的应用程序或容器镜像。以及路由设置、网络安全限制等。此外，有些云服务商在网络层面可能会实施一些特殊的技术手段，如MAC地址劫持等，导致一些不必要的麻烦和延迟。</p><h3><span id="顺炽国-某制造集团-云平台基础服务负责人">顺炽国 （某制造集团 云平台基础服务负责人 ）</span></h3><p>常见的一些互联网应用很多只关注C端的，也就是只需要考虑北线（用户入口）入口流量一个分流跟分发或者流量管控。但是在物联网行业还需要考虑南线（设备入网）的流量管理，这两部分只有配网绑定的阶段需要打通南北两线的数据。</p><ol><li><p><strong>成本压力</strong>：集团的服务器规模正好在一千到五千个主机之间，导致了巨大的成本和运维压力。解决这个问题正在考虑下云。</p></li><li><p><strong>业务复杂性</strong>：智能家电行业有独特的业务特点，需要同时关注北线（用户入口）和南线（设备入网）的流量管理，确保设备与用户之间的关联和通信畅通。</p></li><li><p><strong>多云节点部署</strong>：需要根据业务需求，在各个区域部署多云节点，以提供更快的服务响应时间，降低用户体验的糟糕程度。</p></li><li><p><strong>单元化思路</strong>：采用单元化的思路，尽量减少数据同步带来的问题，通过北线和南线的网关，将业务闭环在单元内，以降低对专线的重度依赖，提高业务稳定性。</p></li></ol><h3><span id="张观石-泰健科技-cto">张观石（ 泰健科技 CTO）</span></h3><p><img src="/posts/a3da18a9/644861-20240319143251857-59896048.jpg"></p><p>张老师讲了个故事： 初始阶段，自建CDN成本高，后接入云CDN，但谈判降价困难。后期通过多云CDN架构，平衡质量和成本，使各厂商主动降价争取份额。</p><p>他从一个比较新颖的角度谈论了多云架构的优势：根据提供商的服务质量和价格，分配负载份额</p><p>另外他谈到了多云架构的优势：</p><ul><li>多云并非只为实现多活，而是为了在架构中灵活利用不同云服务的特点和价格优势（使用特定GPU型号的业务只能在相应云上部署，提高架构灵活性）</li><li>利用不同云的产品优势，如低价存储，以降低整体成本。</li></ul><p>通过多云策略，企业可以增加与云服务商的谈判地位，从而更好地争取资源、降低成本。同时，多云策略也提供了更灵活的资源供给方式，可以根据业务需求在不同云之间调度资源，确保服务的稳定性和容灾能力。</p><p>另外由于多云架构的复杂性，监控告警、排障流程等运维问题复杂度增加，需要考虑跨云沟通和统一运维平台的建设。这就引入了另一个问题：如何建立多云融合统一管理（CMDB）</p><p>CMDB的核心功能是收集和存储关于云环境中各种资源的详细信息，包括虚拟机、存储、网络设备等。管理员可以实现统一的交互界面，方便地查询、分析和修改资源的配置状态</p><p>根据企业对于多云管理的管理要求，可以采用以下几种方式：</p><ul><li><p>十几二十几台服务器，没有必要上CMDB系统，通过管理员登录各云的控制台进行管理</p></li><li><p>资源管理型：创建虚拟机，配置网络等，基于开源的管理工具（比如rental？）二次开发较为容易，各家云厂商的API接口并不复杂。</p></li><li><p>应用管理型，属于高级别应用，可以与应用交付系统结合，从应用的视角出发，自动选择合适的云资源来部署和交付应用，基本要采购商业产品。定制化开发。或寻求混合云厂商，有现有的解决方案可选。</p></li></ul><p>–完结–</p>]]></content>
    
    
    <summary type="html">今年的中国系统架构师大会（SACC）在我所在的城市广州举办，很荣幸受邀参加。这次能接触到国内最优秀的架构师，学习他们的架构思想和行业经验。对我而言非常有意义。大会分为上下午共4场，我参加了上午的多云多活架构设计专场和下午的AIGC专场。本篇文章就多云多活架构设计专场，我选取几位老师的观点进行分享。（我并不是架构师，只是对架构感兴趣，如有错误，还请指正）</summary>
    
    
    
    <category term="架构" scheme="https://blog.matoapp.net/categories/%E6%9E%B6%E6%9E%84/"/>
    
    
    <category term="系统架构" scheme="https://blog.matoapp.net/tags/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/"/>
    
    <category term="大数据" scheme="https://blog.matoapp.net/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    
    <category term="人工智能" scheme="https://blog.matoapp.net/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>[.NET项目实战] Elsa开源工作流组件应用（一）： Elsa工作流简介</title>
    <link href="https://blog.matoapp.net/posts/6233df02/"/>
    <id>https://blog.matoapp.net/posts/6233df02/</id>
    <published>2024-03-15T17:21:00.000Z</published>
    <updated>2025-09-24T16:30:02.362Z</updated>
    
    <content type="html"><![CDATA[<h1><span id="elsa工作流简介">Elsa工作流简介</span></h1><h2><span id="工作流是什么">工作流是什么？</span></h2><p>引用<a href="https://zh.wikipedia.org/zh-cn/%E5%B7%A5%E4%BD%9C%E6%B5%81">维基百科</a>中对工作流的解释：</p><blockquote><p>是对工作流程及其各操作步骤之间业务规则的抽象、概括、描述。工作流建模，即将工作流程中的工作如何前后组织在一起的逻辑和规则在计算机中以恰当的模型进行表示并对其实施计算</p></blockquote><p>Elsa 是一个功能强大的工作流库，支持在任何 .NET 应用程序中执行工作流。Elsa 可以使用C#代码或使用基于Web的可视化设计器生成JSON等方式定义工作流程。</p><p>Elsa 可用于多种场景，包括：</p><ul><li>长时间运行的工作流程，例如订单履行和产品批准。</li><li>运行时间较短的工作流程，例如发送电子邮件和生成 PDF。</li><li>计划的工作流，例如发送每日报告。</li><li>事件驱动的工作流，例如在用户注册时发送欢迎电子邮件。</li></ul><p>Elsa免费且开源，并且有较完善的文档，了解更多请访问<a href="https://v3.elsaworkflows.io/">官网</a>和<a href="https://github.com/elsa-workflows/elsa-core">官方仓库</a></p><h2><span id="工作流的基本概念">工作流的基本概念</span></h2><p>要定义一个工作流，需要包含两个最基本的信息：</p><ol><li>对一系列任务的执行过程的描述；</li><li>各任务之间执行关系的描述。</li></ol><p>在Elsa中，这些被抽象为：</p><ol><li><strong>活动（Activity）</strong>: 执行任务的实体，如：写入文本、发送邮件、执行SQL、执行HTTP请求等；</li><li><strong>工作流（Workflow）</strong>：包含多个活动，以及活动之间的执行关系， 如：并行、串行、分支、循环等；</li></ol><p>有了活动和工作流，就可以描述一个任务的执行过程了。但为了满足业务的复杂性，Elsa还引入了更多的功能和概念。</p><h3><span id="上下文系统">上下文系统</span></h3><p>活动的本质是执行任务，任务的执行需要数据支持，或返回结果。下游的任务也可能依赖于上游任务的返回结果，如：一个发送执行结果邮件任务，需要知道接收信息和前一个任务的执行结果。上下文系统就是用来支持任务执行过程中的数据传递的。</p><p>上下文系统包括：</p><ul><li><strong>变量（Variable）</strong>；</li><li><strong>工作流输入&#x2F;输出（Input&#x2F;Output）</strong>；</li><li><strong>活动输入&#x2F;输出</strong>；</li><li><strong>结果（OutCome）</strong>。</li></ul><h3><span id="阻断x2f恢复功能">阻断&#x2F;恢复功能</span></h3><p>一些场景下，任务不会连续执行，需要暂停任务，任务恢复后，需要继续执行。<br>这些场景包括：</p><ul><li>延时触发，或按计时器规则触发的任务；</li><li>接收到某个消息才执行的任务。</li></ul><p>这些功能在Elsa中使用<strong>书签（Bookmark）</strong> 和 <strong>事件</strong>（Event）等类型实现。</p><h3><span id="设计器designer">设计器（Designer）</span></h3><p>硬编码的方式已经无法满足复杂的业务场景，设计器可以将工作流设计为一个图形化的界面，用户可以拖拽活动，连线，设置活动的属性；活动被打包成一个节点（Node），与活动之间的连线表示活动之间的依赖关系。</p><p><img src="/posts/6233df02/644861-20240315172009032-147356396.png"></p><h3><span id="定义definition与实例化instance">定义（Definition）与实例化（Instance）</span></h3><p>工作流定义描述了一个工作流的结构，比如：名称，变量，包含的活动等，可以理解为一个工作流的模板。</p><p>工作流定义在Elsa初始化时会“注册” 到资源池中，每个工作流定义都有一个唯一的ID。</p><p>使用设计器生成的工作流，通过序列化成JSON字符串，并持久化到数据库。<br>使用硬编码创建的工作流，在Elsa初始化时将工作流注册到工作流定义并持久化到数据库。</p><p>实例化是根据定义创建一个工作流的实例，工作流实例包含工作流状态（WorkflowState）以及活动实例（ActivityState）。与定义一样，它们通过序列化成JSON字符串，并持久化到数据库。当程序重新启动时，会从数据库中恢复工作流实例。</p>]]></content>
    
    
    <summary type="html">工作流定义描述了一个工作流的结构，比如：名称，变量，包含的活动等，可以理解为一个工作流的模板。工作流定义在Elsa初始化时会“注册” 到资源池中，每个工作流定义都有一个唯一的ID。使用设计器生成的工作流，通过序列化成JSON字符串，并持久化到数据库。使用硬编码创建的工作流，在Elsa初始化时将工作流注册到工作流定义并持久化到数据库。实例化是根据定义创建一个工作流的实例，工作流实例包含工作流状态（WorkflowState）以及活动实例（ActivityState）。</summary>
    
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/categories/NET/"/>
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/tags/NET/"/>
    
    <category term="工作流" scheme="https://blog.matoapp.net/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    
    <category term="workflow" scheme="https://blog.matoapp.net/tags/workflow/"/>
    
  </entry>
  
  <entry>
    <title>开源好用的所见即所得(WYSIWYG)编辑器：Editor.js</title>
    <link href="https://blog.matoapp.net/posts/d2c2fe0c/"/>
    <id>https://blog.matoapp.net/posts/d2c2fe0c/</id>
    <published>2024-03-11T18:42:00.000Z</published>
    <updated>2025-09-24T16:30:02.706Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E7%89%B9%E7%82%B9">特点</a><ul><li><a href="#%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97">基于区块</a></li><li><a href="#%E5%B9%B2%E5%87%80%E7%9A%84%E6%95%B0%E6%8D%AE">干净的数据</a></li></ul></li><li><a href="#%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92">界面与交互</a><ul><li><a href="#%E6%8F%92%E4%BB%B6">插件</a></li><li><a href="#%E6%A0%87%E9%A2%98%E5%92%8C%E6%96%87%E6%9C%AC">标题和文本</a></li><li><a href="#%E5%9B%BE%E7%89%87">图片</a></li><li><a href="#%E5%88%97%E8%A1%A8">列表</a></li><li><a href="#todo">Todo</a></li><li><a href="#%E8%A1%A8%E6%A0%BC">表格</a></li></ul></li><li><a href="#%E4%BD%BF%E7%94%A8">使用</a><ul><li><a href="#%E5%AE%89%E8%A3%85">安装</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E7%BC%96%E8%BE%91%E5%99%A8%E5%AE%9E%E4%BE%8B">创建编辑器实例</a></li><li><a href="#%E9%85%8D%E7%BD%AE%E5%B7%A5%E5%85%B7">配置工具</a></li><li><a href="#%E6%9C%AC%E5%9C%B0%E5%8C%96">本地化</a></li><li><a href="#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%B7%E5%BC%8F">自定义样式</a></li></ul></li></ul><!-- tocstop --><p>今天介绍一个开源好用的Web所见即所得(WYSIWYG)编辑器：<a href="https://editorjs.io/">Editor.js</a></p><p>Editor.js 是一个基于 Web 的所见即所得富文本编辑器，它由CodeX团队开发。源代码托管于Github：<a href="https://github.com/codex-team/editor.js">https://github.com/codex-team/editor.js</a></p><h1><span id="特点">特点</span></h1><p>它有两个显著的特点，一个是基于区块（block-styled）的编辑模式，另一个是可以输出干净的数据。</p><h2><span id="基于区块">基于区块</span></h2><p>基于区块官网是这样解释的：</p><p>Editor.js工作区由单独的区块组成：段落、标题、图像、列表、引号等。它们中的每一个都是由 Plugin 提供的独立元素（或更复杂的结构）并由 Editor’s Core 连结。</p><h2><span id="干净的数据">干净的数据</span></h2><p>Editor.js 输出干净的json数据而不是 HTML 标记，虽然对浏览器来说，HTML 是更直观的，但对服务器来说，json更精简更关注内容本身，易于重复使用，存储和传输。</p><p>对于控件本身也更易于实现，比如在文本“加粗”和“常规”来回切换，基于json的更改一个属性，总要比基于HTML反复添加和删除标记<b></b>更简单吧？</p><h1><span id="界面与交互">界面与交互</span></h1><p>在编辑区域，Editor.js提供了区块工具栏(Block Tools)，内联工具栏(Inline Tools)和区块编辑栏(Block Tunes)</p><p><img src="/posts/d2c2fe0c/644861-20240311183835243-1698435099.png"></p><p>他们分别通过 加号 + 按钮，选中区块内容和菜单（六个点和尚按钮）来访问</p><p><img src="/posts/d2c2fe0c/644861-20240311183834849-1679611241.png"></p><h2><span id="插件">插件</span></h2><p>每个区块都通过插件提供支持，官方提供了常用的插件，当然也可以自己写插件。</p><p>官方提供的插件如下图，在sample中，都以cdn方式引入了这些插件，也可以通过npm安装。</p><p><img src="/posts/d2c2fe0c/644861-20240311183834680-1621207662.png"></p><h2><span id="标题和文本">标题和文本</span></h2><p><img src="/posts/d2c2fe0c/644861-20240311183835067-1224582905.png"></p><p>序列化后的数据如下图所示，</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;zcKCF1S7X8&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;header&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;text&quot; : &quot;Editor.js&quot;,</span><br><span class="line">        &quot;level&quot; : 1</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;b6ji-DvaKb&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;paragraph&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;text&quot; : &quot;支持文本，标题，列表，代办，表格，图片，链接，代码片段，引用片段等等&quot;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><p><img src="/posts/d2c2fe0c/644861-20240311183834783-1126835894.png"></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;SSBSguGvP7&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;list&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;style&quot; : &quot;ordered&quot;,</span><br><span class="line">        &quot;items&quot; : [</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;content&quot; : &quot;支持普通文本，&lt;i&gt;斜体文本&lt;/i&gt;，&lt;b&gt;加粗&lt;/b&gt;&quot;,</span><br><span class="line">                &quot;items&quot; : []</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;content&quot; : &quot;支持&lt;mark class=\&quot;cdx-marker\&quot;&gt;文本高亮&lt;/mark&gt;、&lt;a href=\&quot;https://baidu.com\&quot;&gt;文本链接&lt;/a&gt;、&lt;code class=\&quot;inline-code\&quot;&gt;代码片段&lt;/code&gt;&lt;mark class=\&quot;cdx-marker\&quot;&gt;&lt;/mark&gt;&quot;,</span><br><span class="line">                &quot;items&quot; : []</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><h2><span id="图片">图片</span></h2><p>图片支持Base64编码，和url两种方式上传图片</p><p><img src="/posts/d2c2fe0c/644861-20240311183835054-850366715.png"></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;VYsWoLL7yj&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;image&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;url&quot; : &quot;data:image/png;base64, ...&quot;,</span><br><span class="line">        &quot;caption&quot; : &quot;codex2x.png&quot;,</span><br><span class="line">        &quot;withBorder&quot; : false,</span><br><span class="line">        &quot;withBackground&quot; : false,</span><br><span class="line">        &quot;stretched&quot; : false</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2><span id="列表">列表</span></h2><p>支持有序和无序列表，列表支持嵌套</p><p><img src="/posts/d2c2fe0c/644861-20240311183835026-478742518.png"></p><p>无序列表：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">            &quot;id&quot; : &quot;i_cVQxn3Tb&quot;,</span><br><span class="line">            &quot;type&quot; : &quot;list&quot;,</span><br><span class="line">            &quot;data&quot; : &#123;</span><br><span class="line">                &quot;style&quot; : &quot;unordered&quot;,</span><br><span class="line">                &quot;items&quot; : [</span><br><span class="line">                    &#123;</span><br><span class="line">                        &quot;content&quot; : &quot; 香蕉&quot;,</span><br><span class="line">                        &quot;items&quot; : []</span><br><span class="line">                    &#125;,</span><br><span class="line">                    &#123;</span><br><span class="line">                        &quot;content&quot; : &quot; 苹果&quot;,</span><br><span class="line">                        &quot;items&quot; : []</span><br><span class="line">                    &#125;,</span><br><span class="line">                    &#123;</span><br><span class="line">                        &quot;content&quot; : &quot; 葡萄  &quot;,</span><br><span class="line">                        &quot;items&quot; : []</span><br><span class="line">                    &#125;</span><br><span class="line">                ]</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br></pre></td></tr></table></figure><p>有序列表：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;nOTdryosj2&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;list&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;style&quot; : &quot;ordered&quot;,</span><br><span class="line">        &quot;items&quot; : [</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;content&quot; : &quot;洗手心&quot;,</span><br><span class="line">                &quot;items&quot; : []</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;content&quot; : &quot;搓手背&quot;,</span><br><span class="line">                &quot;items&quot; : []</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;content&quot; : &quot;洗指缝&quot;,</span><br><span class="line">                &quot;items&quot; : []</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><p>嵌套列表：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;LJjzlmGa-3&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;list&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;style&quot; : &quot;unordered&quot;,</span><br><span class="line">        &quot;items&quot; : [</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;content&quot; : &quot;序章&quot;,</span><br><span class="line">                &quot;items&quot; : []</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;content&quot; : &quot;第一章&quot;,</span><br><span class="line">                &quot;items&quot; : [</span><br><span class="line">                    &#123;</span><br><span class="line">                        &quot;content&quot; : &quot;第一节&quot;,</span><br><span class="line">                        &quot;items&quot; : [</span><br><span class="line">                            &#123;</span><br><span class="line">                                &quot;content&quot; : &quot;a)&quot;,</span><br><span class="line">                                &quot;items&quot; : []</span><br><span class="line">                            &#125;,</span><br><span class="line">                            &#123;</span><br><span class="line">                                &quot;content&quot; : &quot;b)&quot;,</span><br><span class="line">                                &quot;items&quot; : []</span><br><span class="line">                            &#125;,</span><br><span class="line">                            &#123;</span><br><span class="line">                                &quot;content&quot; : &quot;c)&quot;,</span><br><span class="line">                                &quot;items&quot; : []</span><br><span class="line">                            &#125;</span><br><span class="line">                        ]</span><br><span class="line">                    &#125;,</span><br><span class="line">                    &#123;</span><br><span class="line">                        &quot;content&quot; : &quot;第二节&quot;,</span><br><span class="line">                        &quot;items&quot; : []</span><br><span class="line">                    &#125;</span><br><span class="line">                ]</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><h2><span id="todo">Todo</span></h2><p><img src="/posts/d2c2fe0c/644861-20240311183834606-87249695.png"></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">            &quot;id&quot; : &quot;Hitrs4RqXw&quot;,</span><br><span class="line">            &quot;type&quot; : &quot;checklist&quot;,</span><br><span class="line">            &quot;data&quot; : &#123;</span><br><span class="line">                &quot;items&quot; : [</span><br><span class="line">                    &#123;</span><br><span class="line">                        &quot;text&quot; : &quot;满意&quot;,</span><br><span class="line">                        &quot;checked&quot; : true</span><br><span class="line">                    &#125;,</span><br><span class="line">                    &#123;</span><br><span class="line">                        &quot;text&quot; : &quot;一般&quot;,</span><br><span class="line">                        &quot;checked&quot; : false</span><br><span class="line">                    &#125;,</span><br><span class="line">                    &#123;</span><br><span class="line">                        &quot;text&quot; : &quot;不满意☹️&quot;,</span><br><span class="line">                        &quot;checked&quot; : false</span><br><span class="line">                    &#125;</span><br><span class="line">                ]</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br></pre></td></tr></table></figure><h2><span id="表格">表格</span></h2><p><img src="/posts/d2c2fe0c/644861-20240311183834966-1834621903.png"></p><p>不代表头：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;xPAQ6AkUiK&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;paragraph&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;text&quot; : &quot;&lt;b&gt;不带表头&lt;/b&gt;&quot;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;_MMoOqlgXs&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;table&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;withHeadings&quot; : false,</span><br><span class="line">        &quot;content&quot; : [</span><br><span class="line">            [</span><br><span class="line">                &quot;&lt;b&gt;重要紧急&lt;/b&gt;&quot;,</span><br><span class="line">                &quot;&lt;b&gt;重要不紧急&lt;/b&gt;&quot;</span><br><span class="line">            ],</span><br><span class="line">            [</span><br><span class="line">                &quot;吃饭睡觉&quot;,</span><br><span class="line">                &quot;订生日蛋糕&quot;</span><br><span class="line">            ],</span><br><span class="line">            [</span><br><span class="line">                &quot;&lt;b&gt;不重要但紧急&lt;/b&gt;&quot;,</span><br><span class="line">                &quot;&lt;b&gt;不重要不紧急&lt;/b&gt;&quot;</span><br><span class="line">            ],</span><br><span class="line">            [</span><br><span class="line">                &quot;上班前定好闹钟&quot;,</span><br><span class="line">                &quot;总结这一周的工作&quot;</span><br><span class="line">            ]</span><br><span class="line">        ]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>带表头：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;id&quot; : &quot;fvfQSljMK8&quot;,</span><br><span class="line">    &quot;type&quot; : &quot;table&quot;,</span><br><span class="line">    &quot;data&quot; : &#123;</span><br><span class="line">        &quot;withHeadings&quot; : true,</span><br><span class="line">        &quot;content&quot; : [</span><br><span class="line">            [</span><br><span class="line">                &quot;星期一&quot;,</span><br><span class="line">                &quot;星期二&quot;,</span><br><span class="line">                &quot;星期三&quot;,</span><br><span class="line">                &quot;星期四&quot;,</span><br><span class="line">                &quot;星期五&quot;</span><br><span class="line">            ],</span><br><span class="line">            [</span><br><span class="line">                &quot;a&quot;,</span><br><span class="line">                &quot;b&quot;,</span><br><span class="line">                &quot;c&quot;,</span><br><span class="line">                &quot;d&quot;,</span><br><span class="line">                &quot;e&quot;</span><br><span class="line">            ]</span><br><span class="line">        ]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><h1><span id="使用">使用</span></h1><h2><span id="安装">安装</span></h2><p>页面中引用Editor.js Core库，可通过npm安装。也可以编译<a href="https://github.com/codex-team/editor.js/releases/tag/v2.29.1">项目</a>，然后引入编译后的js文件。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn add @editorjs/editorjs</span><br></pre></td></tr></table></figure><p>或</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&lt;script src=&quot;lib/editorjs/editorjs.umd.js&quot;&gt;&lt;/script&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2><span id="创建编辑器实例">创建编辑器实例</span></h2><p>在页面创建编辑器</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">import EditorJS from &#x27;@editorjs/editorjs&#x27;;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">const editor = new EditorJS(&#123;</span><br><span class="line">  /**</span><br><span class="line">   * Id of Element that should contain Editor instance</span><br><span class="line">   */</span><br><span class="line">  holder: &#x27;editorjs&#x27;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>这是一个最小化的示例。你会发现没有那些默认的工具。因此需要在配置中指定工具。</p><h2><span id="配置工具">配置工具</span></h2><p>可以通过传入配置对象创建编辑器实例。以下是示例</p><p><img src="/posts/d2c2fe0c/644861-20240311183834644-815870536.png"></p><p>holder指定编辑器的容器元素。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">window.editor = new window.EditorJS(&#123;</span><br><span class="line">    /**</span><br><span class="line">        * Wrapper of Editor</span><br><span class="line">        */</span><br><span class="line">    holder: &#x27;editorjs&#x27;,</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>配置工具</p><p>配置完成后，区块工具栏将呈现一个较为完整的工具列表。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br></pre></td><td class="code"><pre><span class="line">    /**</span><br><span class="line">        * Tools list</span><br><span class="line">        */</span><br><span class="line">    tools: &#123;</span><br><span class="line">        paragraph: &#123;</span><br><span class="line">            config: &#123;</span><br><span class="line">                placeholder: &quot;Enter something&quot;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        header: &#123;</span><br><span class="line">            class: Header,</span><br><span class="line">            inlineToolbar: [&#x27;link&#x27;],</span><br><span class="line">            config: &#123;</span><br><span class="line">                placeholder: &#x27;Header&#x27;</span><br><span class="line">            &#125;,</span><br><span class="line">            shortcut: &#x27;CMD+SHIFT+H&#x27;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        /**</span><br><span class="line">            * Or pass class directly without any configuration</span><br><span class="line">            */</span><br><span class="line">        image: ImageTool,</span><br><span class="line"></span><br><span class="line">        list: &#123;</span><br><span class="line">            class: NestedList,</span><br><span class="line">            inlineToolbar: true,</span><br><span class="line">            shortcut: &#x27;CMD+SHIFT+L&#x27;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        checklist: &#123;</span><br><span class="line">            class: Checklist,</span><br><span class="line">            inlineToolbar: true,</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        quote: &#123;</span><br><span class="line">            class: Quote,</span><br><span class="line">            inlineToolbar: true,</span><br><span class="line">            config: &#123;</span><br><span class="line">                quotePlaceholder: &#x27;Enter a quote&#x27;,</span><br><span class="line">                captionPlaceholder: &#x27;Quote\&#x27;s author&#x27;,</span><br><span class="line">            &#125;,</span><br><span class="line">            shortcut: &#x27;CMD+SHIFT+O&#x27;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        marker: &#123;</span><br><span class="line">            class: Marker,</span><br><span class="line">            shortcut: &#x27;CMD+SHIFT+M&#x27;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        code: &#123;</span><br><span class="line">            class: CodeTool,</span><br><span class="line">            shortcut: &#x27;CMD+SHIFT+C&#x27;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        delimiter: Delimiter,</span><br><span class="line"></span><br><span class="line">        inlineCode: &#123;</span><br><span class="line">            class: InlineCode,</span><br><span class="line">            shortcut: &#x27;CMD+SHIFT+C&#x27;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        linkTool: LinkTool,</span><br><span class="line"></span><br><span class="line">        embed: Embed,</span><br><span class="line"></span><br><span class="line">        table: &#123;</span><br><span class="line">            class: Table,</span><br><span class="line">            inlineToolbar: true,</span><br><span class="line">            shortcut: &#x27;CMD+ALT+T&#x27;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">    &#125;,</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">        * Initial Editor data</span><br><span class="line">        */</span><br><span class="line">    data: obj,</span><br><span class="line">    onReady: function () &#123;</span><br><span class="line">        saveButton.click();</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2><span id="本地化">本地化</span></h2><p>可以通过传入<code>i18n</code>配置对象来设置编辑器的本地化。以下是一个较为完整的中文化示例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">i18n: &#123;</span><br><span class="line">    messages: &#123;</span><br><span class="line">        &quot;ui&quot;: &#123;</span><br><span class="line">            &quot;blockTunes&quot;: &#123;</span><br><span class="line">                &quot;toggler&quot;: &#123;</span><br><span class="line">                    &quot;Click to tune&quot;: &quot;点击转换&quot;,</span><br><span class="line">                    &quot;or drag to move&quot;: &quot;拖动调整&quot;</span><br><span class="line">                &#125;,</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;inlineToolbar&quot;: &#123;</span><br><span class="line">                &quot;converter&quot;: &#123;</span><br><span class="line">                    &quot;Convert to&quot;: &quot;转换成&quot;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;toolbar&quot;: &#123;</span><br><span class="line">                &quot;toolbox&quot;: &#123;</span><br><span class="line">                    &quot;Add&quot;: &quot;添加&quot;,</span><br><span class="line">                    &quot;Filter&quot;: &quot;过滤&quot;,</span><br><span class="line">                    &quot;Nothing found&quot;: &quot;无内容&quot;</span><br><span class="line">                &#125;,</span><br><span class="line">                &quot;popover&quot;: &#123;</span><br><span class="line">                    &quot;Filter&quot;: &quot;过滤&quot;,</span><br><span class="line">                    &quot;Nothing found&quot;: &quot;无内容&quot;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;toolNames&quot;: &#123;</span><br><span class="line">            &quot;Text&quot;: &quot;段落&quot;,</span><br><span class="line">            &quot;Heading&quot;: &quot;标题&quot;,</span><br><span class="line">            &quot;List&quot;: &quot;列表&quot;,</span><br><span class="line">            &quot;Warning&quot;: &quot;警告&quot;,</span><br><span class="line">            &quot;Checklist&quot;: &quot;清单&quot;,</span><br><span class="line">            &quot;Quote&quot;: &quot;引用&quot;,</span><br><span class="line">            &quot;Code&quot;: &quot;代码&quot;,</span><br><span class="line">            &quot;Delimiter&quot;: &quot;分割线&quot;,</span><br><span class="line">            &quot;Raw HTML&quot;: &quot;HTML片段&quot;,</span><br><span class="line">            &quot;Table&quot;: &quot;表格&quot;,</span><br><span class="line">            &quot;Link&quot;: &quot;链接&quot;,</span><br><span class="line">            &quot;Marker&quot;: &quot;突出显示&quot;,</span><br><span class="line">            &quot;Bold&quot;: &quot;加粗&quot;,</span><br><span class="line">            &quot;Italic&quot;: &quot;倾斜&quot;,</span><br><span class="line">            &quot;InlineCode&quot;: &quot;代码片段&quot;,</span><br><span class="line">            &quot;Image&quot;: &quot;图片&quot;</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;tools&quot;: &#123;</span><br><span class="line">            &quot;link&quot;: &#123;</span><br><span class="line">                &quot;Add a link&quot;: &quot;添加链接&quot;</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;stub&quot;: &#123;</span><br><span class="line">                &#x27;The block can not be displayed correctly.&#x27;: &#x27;该模块不能放置在这里&#x27;</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;image&quot;: &#123;</span><br><span class="line">                &quot;Caption&quot;: &quot;图片说明&quot;,</span><br><span class="line">                &quot;Select an Image&quot;: &quot;选择图片&quot;,</span><br><span class="line">                &quot;With border&quot;: &quot;添加边框&quot;,</span><br><span class="line">                &quot;Stretch image&quot;: &quot;拉伸图像&quot;,</span><br><span class="line">                &quot;With background&quot;: &quot;添加背景&quot;,</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;code&quot;: &#123;</span><br><span class="line">                &quot;Enter a code&quot;: &quot;输入代码&quot;,</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;linkTool&quot;: &#123;</span><br><span class="line">                &quot;Link&quot;: &quot;请输入链接地址&quot;,</span><br><span class="line">                &quot;Couldn&#x27;t fetch the link data&quot;: &quot;获取链接数据失败&quot;,</span><br><span class="line">                &quot;Couldn&#x27;t get this link data, try the other one&quot;: &quot;该链接不能访问，请修改&quot;,</span><br><span class="line">                &quot;Wrong response format from the server&quot;: &quot;错误响应&quot;,</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;header&quot;: &#123;</span><br><span class="line">                &quot;Header&quot;: &quot;标题&quot;,</span><br><span class="line">                &quot;Heading 2&quot;: &quot;二级标题&quot;,</span><br><span class="line">                &quot;Heading 3&quot;: &quot;三级标题&quot;,</span><br><span class="line">                &quot;Heading 4&quot;: &quot;四级标题&quot;,</span><br><span class="line">                &quot;Heading 5&quot;: &quot;五级标题&quot;,</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;paragraph&quot;: &#123;</span><br><span class="line">                &quot;Enter something&quot;: &quot;请输入笔记内容&quot;,</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;list&quot;: &#123;</span><br><span class="line">                &quot;Ordered&quot;: &quot;有序列表&quot;,</span><br><span class="line">                &quot;Unordered&quot;: &quot;无序列表&quot;,</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;table&quot;: &#123;</span><br><span class="line">                &quot;Heading&quot;: &quot;标题&quot;,</span><br><span class="line">                &quot;Add column to left&quot;: &quot;在左侧插入列&quot;,</span><br><span class="line">                &quot;Add column to right&quot;: &quot;在右侧插入列&quot;,</span><br><span class="line">                &quot;Delete column&quot;: &quot;删除列&quot;,</span><br><span class="line">                &quot;Add row above&quot;: &quot;在上方插入行&quot;,</span><br><span class="line">                &quot;Add row below&quot;: &quot;在下方插入行&quot;,</span><br><span class="line">                &quot;Delete row&quot;: &quot;删除行&quot;,</span><br><span class="line">                &quot;With headings&quot;: &quot;有标题&quot;,</span><br><span class="line">                &quot;Without headings&quot;: &quot;无标题&quot;,</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;quote&quot;: &#123;</span><br><span class="line">                &quot;Align Left&quot;: &quot;左对齐&quot;,</span><br><span class="line">                &quot;Align Center&quot;: &quot;居中对齐&quot;,</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;blockTunes&quot;: &#123;</span><br><span class="line">            &quot;delete&quot;: &#123;</span><br><span class="line">                &quot;Delete&quot;: &quot;删除&quot;,</span><br><span class="line">                &#x27;Click to delete&#x27;: &quot;点击删除&quot;</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;moveUp&quot;: &#123;</span><br><span class="line">                &quot;Move up&quot;: &quot;向上移&quot;</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;moveDown&quot;: &#123;</span><br><span class="line">                &quot;Move down&quot;: &quot;向下移&quot;</span><br><span class="line">            &#125;,</span><br><span class="line">            &quot;filter&quot;: &#123;</span><br><span class="line">                &quot;Filter&quot;: &quot;过滤&quot;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2><span id="自定义样式">自定义样式</span></h2><p>varaiables.css中包含了大部分的样式变量，更改这些变量可以实现自定义样式。</p><p><img src="/posts/d2c2fe0c/644861-20240311183835113-453894703.png"></p><p>如通过重写 .root样式选择器可以实现自定义的背景色, 重写.ce-popover 改变弹出框样式等。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line">:root &#123;</span><br><span class="line">    --color-bg-main: #F0F0F0;</span><br><span class="line">    --color-border-light: #E8E8EB;</span><br><span class="line">    --color-text-main: #000;</span><br><span class="line">    --selectionColor: #e1f2ff;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">.ce-popover &#123;</span><br><span class="line">    --border-radius: 6px;</span><br><span class="line">    --width: 200px;</span><br><span class="line">    --max-height: 270px;</span><br><span class="line">    --padding: 6px;</span><br><span class="line">    --offset-from-target: 8px;</span><br><span class="line">    --color-border: #e8e8eb;</span><br><span class="line">    --color-shadow: rgba(13,20,33,0.13);</span><br><span class="line">    --color-background: white;</span><br><span class="line">    --color-text-primary: black;</span><br><span class="line">    --color-text-secondary: #707684;</span><br><span class="line">    --color-border-icon: rgb(201 201 204 / 48%);</span><br><span class="line">    --color-border-icon-disabled: #EFF0F1;</span><br><span class="line">    --color-text-icon-active: #388AE5;</span><br><span class="line">    --color-background-icon-active: rgba(56, 138, 229, 0.1);</span><br><span class="line">    --color-background-item-focus: rgba(34, 186, 255, 0.08);</span><br><span class="line">    --color-shadow-item-focus: rgba(7, 161, 227, 0.08);</span><br><span class="line">    --color-background-item-hover: #eff2f5;</span><br><span class="line">    --color-background-item-confirm: #E24A4A;</span><br><span class="line">    --color-background-item-confirm-hover: #CE4343;</span><br><span class="line">&#125;</span><br><span class="line">.dark-mode &#123;</span><br><span class="line">  --color-border-light: rgba(255, 255, 255,.08);</span><br><span class="line">  --color-bg-main: #1c1e24;</span><br><span class="line">  --color-text-main: #737886;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="/posts/d2c2fe0c/644861-20240311183834687-232428504.png"></p><p><img src="/posts/d2c2fe0c/644861-20240311183835160-941826674.png"><br>– 完 –</p>]]></content>
    
    
    <summary type="html">varaiables.css中包含了大部分的样式变量，更改这些变量可以实现自定义样式。如通过重写 .root样式选择器可以实现自定义的背景色, 重写.ce-popover 改变弹出框样式等。:root {– 完 –</summary>
    
    
    
    <category term="Web" scheme="https://blog.matoapp.net/categories/Web/"/>
    
    <category term="JavaScript" scheme="https://blog.matoapp.net/categories/JavaScript/"/>
    
    
    <category term="JavaScript" scheme="https://blog.matoapp.net/tags/JavaScript/"/>
    
    <category term="Html" scheme="https://blog.matoapp.net/tags/Html/"/>
    
    <category term="所见即所得" scheme="https://blog.matoapp.net/tags/%E6%89%80%E8%A7%81%E5%8D%B3%E6%89%80%E5%BE%97/"/>
    
    <category term="编辑器" scheme="https://blog.matoapp.net/tags/%E7%BC%96%E8%BE%91%E5%99%A8/"/>
    
  </entry>
  
  <entry>
    <title>在macOS中搭建.NET MAUI开发环境</title>
    <link href="https://blog.matoapp.net/posts/ddf3f205/"/>
    <id>https://blog.matoapp.net/posts/ddf3f205/</id>
    <published>2023-12-31T00:21:00.000Z</published>
    <updated>2025-09-24T16:30:02.670Z</updated>
    
    <content type="html"><![CDATA[<!-- toc --><ul><li><a href="#%E5%87%86%E5%A4%87">准备</a></li><li><a href="#%E5%AE%89%E8%A3%85%E6%89%A9%E5%B1%95">安装扩展</a></li><li><a href="#%E5%AE%89%E8%A3%85-net">安装 .NET</a></li><li><a href="#%E5%AE%89%E8%A3%85%E5%B7%A5%E4%BD%9C%E8%B4%9F%E8%BD%BD">安装工作负载</a></li><li><a href="#%E5%AE%89%E8%A3%85-xcode-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7">安装 Xcode 命令行工具</a></li><li><a href="#%E8%B0%83%E8%AF%95%E5%AE%89%E5%8D%93%E5%BA%94%E7%94%A8">调试安卓应用</a><ul><li><a href="#%E5%AE%89%E8%A3%85-jdk">安装 JDK</a></li><li><a href="#%E5%AE%89%E8%A3%85-android-sdk">安装 Android SDK</a></li></ul></li><li><a href="#%E5%AE%89%E8%A3%85-android-%E6%A8%A1%E6%8B%9F%E5%99%A8">安装 Android 模拟器</a><ul><li><a href="#%E5%AE%89%E8%A3%85%E6%A8%A1%E6%8B%9F%E5%99%A8">安装模拟器</a></li><li><a href="#%E5%AE%89%E8%A3%85%E9%95%9C%E5%83%8F">安装镜像</a></li><li><a href="#%E5%88%9B%E5%BB%BA%E8%99%9A%E6%8B%9F%E6%9C%BA">创建虚拟机</a></li></ul></li><li><a href="#%E5%90%8C%E6%84%8F%E8%AE%B8%E5%8F%AF%E6%9D%A1%E6%AC%BE">同意许可条款</a></li><li><a href="#%E5%88%9B%E5%BB%BA-maui-%E9%A1%B9%E7%9B%AE">创建 MAUI 项目</a></li><li><a href="#%E8%B0%83%E8%AF%95-maui-%E5%BA%94%E7%94%A8">调试 MAUI 应用</a><ul><li><a href="#%E5%88%87%E6%8D%A2%E8%B0%83%E8%AF%95%E7%9B%AE%E6%A0%87">切换调试目标</a></li></ul></li><li><a href="#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li></ul><!-- tocstop --><h1><span id="准备">准备</span></h1><ul><li>一台 macOS Monterey 以上的电脑</li><li>安装 XCode13.3 以上</li><li>安装 Visual Studio Code</li></ul><h1><span id="安装扩展">安装扩展</span></h1><p>打开Visual Studio Code，按下 <code>Command + Shift + P</code>，输入 <code>install</code>，选择 <code>Extensions: Install Extensions</code>，搜索 <code>MAUI</code>，安装 <code>.NET MAUI</code> 扩展。</p><h1><span id="安装-net">安装 .NET</span></h1><p>安装 .NET 8 <a href="https://dotnet.microsoft.com/zh-cn/download/dotnet">https://dotnet.microsoft.com/zh-cn/download/dotnet</a></p><h1><span id="安装工作负载">安装工作负载</span></h1><p>打开终端，输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet workload install maui</span><br></pre></td></tr></table></figure><h1><span id="安装-xcode-命令行工具">安装 Xcode 命令行工具</span></h1><p>打开终端，输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo xcode-select --install</span><br></pre></td></tr></table></figure><p>至此，应该可以调试iOS应用了。</p><h1><span id="调试安卓应用">调试安卓应用</span></h1><p>若要在 Visual Studio Code 中调试 Android 应用，请执行以下操作：</p><h2><span id="安装-jdk">安装 JDK</span></h2><p>下载最新版本的 JDK：</p><p><a href="https://learn.microsoft.com/zh-cn/java/openjdk/download">https://learn.microsoft.com/zh-cn/java/openjdk/download</a></p><p><img src="/posts/ddf3f205/644861-20231231001921584-10989755.png"></p><p>安装 JDK：</p><p><img src="/posts/ddf3f205/644861-20231231001921431-641358887.png"></p><h2><span id="安装-android-sdk">安装 Android SDK</span></h2><p>创建新的 .NET MAUI 项目，在合适的位置打开终端，输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet new maui</span><br></pre></td></tr></table></figure><p>在项目文件夹中，打开终端，输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet build -t:InstallAndroidDependencies -f:net8.0-android -p:AndroidSdkDirectory=<span class="string">&quot;&lt;ANDROID SDK DIRECTORY&gt;&quot;</span> -p:AcceptAndroidSDKLicenses=True</span><br></pre></td></tr></table></figure><p>在 macOS 上，建议的 Android SDK 目录值为 $HOME&#x2F;Library&#x2F;Android&#x2F;sdk</p><p>Android SDK 将自动被创建</p><h1><span id="安装-android-模拟器">安装 Android 模拟器</span></h1><p>此时已可以真机调试 Android 应用，如果真机调试不方便，我们需要安装 Android 模拟器。</p><h2><span id="安装模拟器">安装模拟器</span></h2><p>前往 Android SDK Manager 所在目录，默认为 $HOME&#x2F;Library&#x2F;Android&#x2F;sdk&#x2F;cmdline-tools&#x2F;11.0&#x2F;bin，打开终端，输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./sdkmanager --install emulator</span><br></pre></td></tr></table></figure><h2><span id="安装镜像">安装镜像</span></h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./sdkmanager <span class="string">&quot;emulator&quot;</span> <span class="string">&quot;system-images;android-34;google_apis;x86_64&quot;</span></span><br></pre></td></tr></table></figure><h2><span id="创建虚拟机">创建虚拟机</span></h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./avdmanager create avd -n Pixel5-API34 -k <span class="string">&quot;system-images;android-34;google_apis;x86_64&quot;</span></span><br></pre></td></tr></table></figure><p>另外可以通过切换调试目标中的菜单，查看可用的镜像，选择一个镜像，或创建一个新的镜像。</p><p>CMD+SHIFT+P 或 查看-&gt;命令面板</p><p><img src="/posts/ddf3f205/644861-20231231001921147-705244150.png"><br>选择“创建Android Emulator”<br><img src="/posts/ddf3f205/644861-20231231001921578-637512103.png"><br>根据提示完成创建<br><img src="/posts/ddf3f205/644861-20231231001921521-1334753481.png"></p><h1><span id="同意许可条款">同意许可条款</span></h1><p>打开终端，输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./sdkmanager --licenses --verbose</span><br></pre></td></tr></table></figure><p>按照提示输入 <code>y</code> 同意许可条款。</p><p><img src="/posts/ddf3f205/644861-20231231001921545-688494234.png"></p><h1><span id="创建-maui-项目">创建 MAUI 项目</span></h1><p>若要创建新的 .NET MAUI 应用，请执行以下操作：</p><ol><li>在资源管理器中，单击“创建 .NET 项目”或按 CMD+SHIFT+P&gt; 选择“.NET: New Project…”。</li><li>选择“.NET MAUI 应用”或“.NET MAUI Blazor 应用”。</li><li>选择空文件夹。 如果文件资源管理器弹出窗口再次打开，则文件夹不为空。</li><li>为项目命名。</li><li>确保项目在解决方案资源管理器中成功加载，然后打开 C# 或 XAML 文件。</li><li>也可以通过单击“文件”&gt;“打开…”在 Visual Studio Code 中打开现有的 .NET MAUI 项目。</li></ol><h1><span id="调试-maui-应用">调试 MAUI 应用</span></h1><p>在 Visual Studio Code 中，按 F5 键或单击“运行”&gt;“启动调试”以调试 .NET MAUI 应用。</p><p>调试器选择器将显示可用的调试器。 选择“ .NET MAUI”以启动调试会话。</p><p><img src="/posts/ddf3f205/644861-20231231001921006-1781613748.png"></p><h2><span id="切换调试目标">切换调试目标</span></h2><p>在 Visual Studio Code 的资源管理器中，打开项目sln文件，</p><p><img src="/posts/ddf3f205/644861-20231231001921498-585351228.png"></p><p>此时在任务栏出现大括号 <code>&#123;&#125;</code>，点击选择调试目标。</p><p><img src="/posts/ddf3f205/644861-20231231001921402-920560996.png"></p><h1><span id="参考资料">参考资料</span></h1><p>扩展主页：<br><a href="https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-maui">https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-maui</a></p><p>扩展仓库：<br><a href="https://github.com/microsoft/vscode-dotnettools">https://github.com/microsoft/vscode-dotnettools</a></p><p>官方文档：<br><a href="https://learn.microsoft.com/zh-cn/dotnet/maui/get-started/installation?view=net-maui-8.0&tabs=visual-studio-code">https://learn.microsoft.com/zh-cn/dotnet/maui/get-started/installation?view=net-maui-8.0&amp;tabs=visual-studio-code</a></p>]]></content>
    
    
    <summary type="html">在 Visual Studio Code 中，按 F5 键或单击“运行”&gt;“启动调试”以调试 .NET MAUI 应用。在 macOS 上，建议的 Android SDK 目录值为 $HOME/Library/Android/sdk。此时已可以真机调试 Android 应用，如果真机调试不方便，我们需要安装 Android 模拟器。另外可以通过切换调试目标中的菜单，查看可用的镜像，选择一个镜像，或创建一个新的镜像。在 Visual Studio Code 的资源管理器中，打开项目sln文件，</summary>
    
    
    
    <category term=".NET MAUI" scheme="https://blog.matoapp.net/categories/NET-MAUI/"/>
    
    
    <category term=".NET" scheme="https://blog.matoapp.net/tags/NET/"/>
    
    <category term="Xamarin" scheme="https://blog.matoapp.net/tags/Xamarin/"/>
    
    <category term="MAUI" scheme="https://blog.matoapp.net/tags/MAUI/"/>
    
  </entry>
  
</feed>
