<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh_CN">
  <title>戈小荷</title>
  <subtitle>我永远爱触手猴</subtitle>
  <link href="https://gehongyan.cn/" rel="alternate" type="text/html"/>
  <link href="https://gehongyan.cn/atom.xml" rel="self" type="application/atom+xml"/>
  <id>https://gehongyan.cn/</id>
  <updated>2024-05-09T00:00:00.000Z</updated>
  <entry>
    <title>React 基础</title>
    <link href="https://gehongyan.cn/frontend/react-basics/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/frontend/react-basics/</id>
    <published>2024-05-09T00:00:00.000Z</published>
    <updated>2024-05-09T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<p>::github{repo="gehongyan/ReactTutorials"}</p>
<p>本文档是 React 的基础教程，主要包含安装 React 应用程序的开发环境、创建应用程序、根据业务编写代码、测试与部署等内容。</p>
<h2>开发环境</h2>
<h3>安装 Node.js</h3>
<p>要开发 React 应用程序，首先需要安装 Node.js。Node.js 是一个 JavaScript 运行环境。可以在 <a href="https://nodejs.org/">Node.js 官网</a> 下载到 Node.js 的开发环境。</p>
<h3>开发工具</h3>
<p>主流的 React 开发工具有 <a href="https://code.visualstudio.com/">Visual Studio Code</a>、<a href="https://www.jetbrains.com/webstorm/">JetBrains WebStorm</a> 等。</p>
<p>Visual Studio Code 是一款开源的轻量级的代码编辑器，支持多种语言与扩展，是一款非常适合前端开发的工具。</p>
<h4>Visual Studio Code 插件</h4>
<p>以 Visual Studio Code 为例，为了后续开发的遍历，可以安装一些主流的插件，如：</p>
<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets">ES7+ React/Redux/React-Native snippets</a>：提供了一些便于 React 开发的代码片段。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=burkeholland.simple-react-snippets">Simple React Snippets</a>：提供了一些 React 开发的代码片段。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode">Prettier - Code formatter</a>：提供了代码格式化的功能。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint">ESLint</a>：提供了 JavaScript 代码检查的功能。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint">Stylelint</a>：提供了 CSS 代码检查的功能。</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode">SonarLint</a>：提供了代码质量检查的功能。</li>
</ul>
<h2>创建 React 应用程序</h2>
<p><a href="https://vitejs.dev/">Vite</a> 是一个由原生 ESM 驱动的 Web 开发构建工具，它的目标是快速提供合适的开发环境。 此处以使用 Vite 创建 React 应用程序为例，首先通过 npm 包管理器安装 Vite 全局工具。</p>
<pre><code>npm install --global create-vite
</code></pre>
<p>然后通过 Vite 创建 React 应用程序。</p>
<pre><code>npm create vite@latest
</code></pre>
<p>如果命令中不包含额外的参数，Vite 会进行交互式的操作，根据提示选择创建 React 应用程序。 输入应用程序名称后选择框架和语言，即可在当前工作目录下创建 React 应用程序。 本项目以应用程序名称为 app，框架为 React，语言为 TypeScript 为例。 TypeScript 是 JavaScript 的超集，提供了类型检查的功能，可以提高代码的可维护性。</p>
<h2>模板项目结构</h2>
<p>创建 React 应用程序后，会生成如下的项目结构：</p>
<pre><code>├── public/              # 静态资源
│   └── vite.svg         # Vite 图标
├── src/                 # 应用程序源码
│   ├── assets/          # 资源
│   │   └── react.svg    # React 图标
│   ├── App.css          # 组件 App 的样式
│   ├── App.tsx          # 组件 App 的代码
│   ├── index.css        # 入口文件的样式
│   ├── index.tsx        # 入口文件的模块
│   └── vite-env.d.ts    # Vite 环境变量
├── .eslintrc.cjs        # ESLint 配置
├── .gitignore           # Git 忽略文件
├── index.html           # 入口文件
├── package.json         # 包管理文件
├── README.md            # 项目说明
├── tsconfig.json        # TypeScript 配置
├── tsconfig.node.json   # Node.js TypeScript 配置
└── vite.config.ts       # Vite 配置
</code></pre>
<h2>包管理器</h2>
<p>npm 是 Node.js 的包管理器，用于管理 Node.js 的依赖包。 <code>dependencies</code> 节点下记录的是应用程序的依赖包，<code>devDependencies</code> 节点下记录的是开发环境的依赖包。</p>
<p>版本号前的 <code>^</code> 符号表示允许更新到次要版本，<code>~</code> 符号表示允许更新到补丁版本。 例如，<code>^1.2.3</code> 表示允许更新到 1.x.x 的任意版本，<code>~1.2.3</code> 表示允许更新到 1.2.x 的任意版本， 而 <code>1.2.3</code> 则表示只能使用 1.2.3 版本。</p>
<h2>运行示例程序</h2>
<p>运行 React 应用程序，首先需要安装依赖。</p>
<pre><code>cd app
npm install
</code></pre>
<p><code>npm install</code> 命令会安装 <code>package.json</code> 中的依赖，此过程会创建 <code>node_modules</code> 目录，存放依赖的包。 还会创建 <code>package-lock.json</code> 文件，存放依赖的版本信息。<code>package-lock.json</code> 文件记录的是当前实际安装的依赖版本。</p>
<p>安装依赖后，可以运行 React 应用程序。</p>
<pre><code>npm run dev
</code></pre>
<p><code>npm run</code> 命令会执行 package.json 中 scripts 字段中的命令，<code>dev</code> 是其中一个命令，用于启动开发服务器。 此处的 <code>dev</code> 脚本的值是 <code>vite</code>，即启动 Vite 开发服务器，因此该命令实际和 <code>vite</code> 命令等价。</p>
<p>观察控制台输出，可以看到 React 应用程序的启动信息，如 <code>Local: http://localhost:5173/</code> 便可以在浏览器中打开该地址访问应用程序。</p>
<p>package.json 中的 scripts 字段中还有其他脚本，如 <code>build</code>、<code>lint</code>、<code>preview</code> 等。</p>
<ul>
<li><code>build</code> 脚本调用了 <code>tsc &amp;&amp; vite build</code>，即先对 TypeScript 源码进行编译，然后构建应用程序。</li>
<li><code>lint</code> 脚本调用了 <code>eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0</code>，即对 当前名录下的 <code>.ts</code> 和 <code>.tsx</code> 文件进行 ESLint 检查。<code>--report-unused-disable-directives</code> 用于检查未使用的 ESLint 禁用指令，<code>--max-warnings 0</code> 用于设置最大警告数为 0，即任何警告都会导致脚本失败。</li>
<li><code>preview</code> 脚本调用了 <code>vite preview</code>，即启动 Vite 预览服务器，预览构建后的应用程序。该命令主要用于 预览构建后的应用程序，可以用于测试构建后的应用程序，不应直接用于生产环境。生产环境下应使用一个更完整的服务器应用来托管应用程序。</li>
</ul>
<h2>编写 React 代码</h2>
<p>React 是一个用于构建用户界面的 JavaScript 库。</p>
<h3>入口文件</h3>
<p><code>index.html</code> 是 React 应用程序的入口文件，其中包含一个 <code>div</code> 元素，用于挂载 React 应用程序，以及一个 <code>script</code> 元素，用于加载 React 应用程序的入口模块。</p>
<pre><code>&lt;div id="root"&gt;&lt;/div&gt;
&lt;script type="module" src="/src/main.tsx"&gt;&lt;/script&gt;
</code></pre>
<p><code>main.tsx</code> 是 React 应用程序的入口模块，用于在 React 应用程序中渲染一个 <code>App</code> 组件。</p>
<pre><code>ReactDOM.createRoot(document.getElementById('root')!).render(
  &lt;React.StrictMode&gt;
    &lt;App/&gt;
  &lt;/React.StrictMode&gt;,
)
</code></pre>
<ul>
<li>
<p><code>ReactDOM.createRoot(document.getElementById('root')!)</code>：这是 React 18 中引入的新的渲染 API。<code>createRoot</code> 方法创建一个根 React 节点，并将其挂载到指定的 DOM 元素上，将 React 应用程序挂载到 id 为 <code>root</code> 的 DOM 元素上。</p>
</li>
<li>
<p><code>.render(&lt;React.StrictMode&gt;&lt;App /&gt;&lt;/React.StrictMode&gt;)</code>：这个方法用于在前面创建的根节点上渲染 React 组件。</p>
</li>
<li>
<p><code>&lt;React.StrictMode&gt;</code>：React 严格模式，这是一个用于检查 React 应用程序中潜在问题的工具。它不会影响生产构建， 但在开发模式下，它会激活额外的检查和警告。它包裹了 <code>App</code> 组件，所以这些检查和警告将应用于 <code>App</code> 组件及其所有子组件。</p>
</li>
<li>
<p><code>&lt;App /&gt;</code>：这是要渲染的 React 组件。在这个例子中，<code>App</code> 是应用程序的根组件。</p>
</li>
</ul>
<h3>组件</h3>
<p>React 应用程序是由组件组成的，组件是 React 应用程序的基本单元。 组件是一个函数或一个类，用于描述用户界面的一部分。组件可以嵌套，形成一个组件树。目前主流的 React 开发方式是函数组件。</p>
<p>函数组件是一个函数，接收一个 props 参数，返回一个 React 元素。React 元素是一个描述用户界面的对象，可以是一个 DOM 元素、一个组件等。</p>
<pre><code>import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;a href="https://vitejs.dev" target="_blank"&gt;
          &lt;img src={viteLogo} className="logo" alt="Vite logo"/&gt;
        &lt;/a&gt;
        &lt;a href="https://react.dev" target="_blank"&gt;
          &lt;img src={reactLogo} className="logo react" alt="React logo"/&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;h1&gt;Vite + React&lt;/h1&gt;
      &lt;div className="card"&gt;
        &lt;button onClick={() =&gt; setCount((count) =&gt; count + 1)}&gt;
          count is {count}
        &lt;/button&gt;
        &lt;p&gt;
          Edit &lt;code&gt;src/App.tsx&lt;/code&gt; and save to test HMR
        &lt;/p&gt;
      &lt;/div&gt;
      &lt;p className="read-the-docs"&gt;
        Click on the Vite and React logos to learn more
      &lt;/p&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>
<p><code>function</code> 用于声明一个函数，<code>App</code> 是函数的名称，函数体是函数的实现。<code>return</code> 返回了一个 React 元素。</p>
<p><code>useState</code> 是一个 React Hook，用于在函数组件中添加状态。因为函数组件是无状态的，所以需要使用 Hook 来添加状态。 <code>useState(0)</code> 返回一个数组，第一个元素 <code>count</code> 是状态的值，第二个元素 <code>setCount</code> 是更新状态的函数。 这里的状态是一个数字，初始值为 0。<code>useState(0)</code> 的参数是状态的初始值，这里的初始值是 0。</p>
<p><code>&lt;img src={viteLogo} className="logo" alt="Vite logo" /&gt;</code> 是一个图片元素，<code>src</code> 属性是图片的地址，<code>className</code> 属性是图片的类名，<code>alt</code> 属性是图片的描述。这里的 <code>src</code> 属性使用了一个变量 <code>viteLogo</code>，这个变量是一个图片的地址，通过 <code>import</code> 引入。变量需要使用大括号 <code>{}</code> 包裹。</p>
<p><code>&lt;button onClick={() =&gt; setCount((count) =&gt; count + 1)}&gt;count is {count}&lt;/button&gt;</code> 是一个按钮元素，<code>onClick</code> 属性是点击事件的处理函数，调用了 <code>setCount</code> 函数，更新了 <code>useState</code> 定义的状态。 按钮的文本 <code>count is {count}</code> 使用了一个变量 <code>count</code>，这个变量是一个状态的值，通过大括号 <code>{}</code> 包裹。 状态的更新会自动触发对应的组件的重新渲染。</p>
<p><code>&lt;&gt;&lt;/&gt;</code> 是一个 React 片段，用于包裹多个元素。React 要求一个组件只能返回一个根元素，所以需要使用片段来包裹多个元素。</p>
<p><code>export default App</code> 用于导出组件，这样其他模块就可以引入该组件。</p>
<h3>样式</h3>
<p><code>App.css</code> 是 <code>App</code> 组件的样式文件，用于描述 <code>App</code> 组件的样式。</p>
<p>默认生成的 <code>App.css</code> 包含了以下几个部分。</p>
<pre><code>#root {
    max-width: 1280px;
    margin: 0 auto;
    padding: 2rem;
    text-align: center;
}
</code></pre>
<p><code>#root</code> 是一个 ID 选择器，用于选择 id 为 <code>root</code> 的元素。<code>max-width</code> 属性设置元素的最大宽度为 1280 像素， <code>margin</code> 属性设置元素的外边距为 0，<code>padding</code> 属性设置元素的内边距为 2rem，这里的 rem 单位是相对于根元素的字体大小， <code>text-align</code> 属性设置元素的文本对齐方式为居中。</p>
<pre><code>.logo {
    height: 6em;
    padding: 1.5em;
    will-change: filter;
    transition: filter 300ms;
}

.logo:hover {
    filter: drop-shadow(0 0 2em #646cffaa);
}

.logo.react:hover {
    filter: drop-shadow(0 0 2em #61dafbaa);
}
</code></pre>
<p><code>.logo</code> 是一个类选择器，用于选择类名为 <code>logo</code> 的元素。<code>height</code> 属性设置元素的高度为 6em， <code>padding</code> 属性设置元素的内边距为 1.5em，<code>will-change</code> 属性设置元素之后会发生变化的属性为 <code>filter</code>， <code>transition</code> 属性设置元素的过渡效果为 <code>filter 300ms</code>，即 <code>filter</code> 属性的变化时间为 300 毫秒。</p>
<p><code>.logo:hover</code> 是一个伪类选择器，用于选择鼠标悬停在类名为 <code>logo</code> 的元素上的元素。 <code>filter</code> 属性设置元素的滤镜效果为 <code>drop-shadow(0 0 2em #646cffaa)</code>，</p>
<pre><code>@keyframes logo-spin {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}

@media (prefers-reduced-motion: no-preference) {
    a:nth-of-type(2) .logo {
        animation: logo-spin infinite 20s linear;
    }
}
</code></pre>
<p><code>@keyframes logo-spin</code> 是一个关键帧动画，用于定义一个旋转动画。<code>from</code> 关键字表示动画的开始状态， <code>to</code> 关键字表示动画的结束状态。<code>transform</code> 属性设置元素的变换效果为 <code>rotate(0deg)</code>，即元素的旋转角度为 0 度到 360 度。</p>
<p><code>@media (prefers-reduced-motion: no-preference)</code> 是一个媒体查询，用于根据用户的偏好设置应用样式。 <code>prefers-reduced-motion</code> 是一个用户偏好设置，表示用户所设置的是否偏好减弱动画。 根据文档 <a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/prefers-reduced-motion">prefers-reduced-motion - CSS：层叠样式表 | MDN</a>，<code>no-preference</code> 表示用户未修改系统动画相关特性， <code>reduce</code> 意味着用户修改了系统设置，将动画效果最小化，最好所有的不必要的移动都能被移除。 在 Windows 11 中，影响该值的设置位于「设置 - 辅助功能 - 视觉效果 - 动画效果」。</p>
<h4>模块样式</h4>
<p>但传统的 CSS 有一个问题，就是全局作用域。一个 CSS 文件中的样式会对整个页面生效，这样会导致样式冲突， 不利于组件化开发，类名称的弱关联也不利于样式的维护。</p>
<p>CSS Modules 是一种解决方案，它是一种 CSS 的模块化方案，可以解决全局作用域的问题。CSS Modules 会将 CSS 文件中的类名转换为一个哈希值，这样就可以保证类名的唯一性，避免样式冲突。</p>
<p>要将 <code>App.css</code> 转换为 CSS Module，首先需要将文件重命名为 <code>App.module.css</code>，然后在 <code>App.tsx</code> 中更改样式文件的引入， 最后根据需要修改样式文件中的类名，例如，将 <code>read-the-docs</code> 类名修改为 <code>readTheDocs</code>。</p>
<pre><code>- import './App.css'
+ import styles from './App.module.css'
</code></pre>
<p>这样就可以使用 <code>styles</code> 对象来引用 CSS Module 中的类名。</p>
<pre><code>  &lt;&gt;
    &lt;div&gt;
      &lt;a href="https://vitejs.dev" target="_blank" rel="noopener"&gt;
- &lt;img src={viteLogo} className={styles.logo} alt="Vite logo" /&gt;
+       &lt;img src={viteLogo} className={styles.logo} alt="Vite logo" /&gt;
      &lt;/a&gt;
      &lt;a href="https://react.dev" target="_blank" rel="noopener"&gt;
- &lt;img src={reactLogo} className="logo react" alt="React logo" /&gt;
+       &lt;img src={reactLogo} className={`${styles.logo} ${styles.react}`} alt="React logo" /&gt;
      &lt;/a&gt;
    &lt;/div&gt;
    &lt;h1&gt;Vite + React&lt;/h1&gt;
- &lt;div className="card"&gt;
+   &lt;div className={styles.card}&gt;
      &lt;button onClick={() =&gt; setCount((count) =&gt; count + 1)}&gt;
        count is {count}
      &lt;/button&gt;
      &lt;p&gt;
        Edit &lt;code&gt;src/App.tsx&lt;/code&gt; and save to test HMR
      &lt;/p&gt;
    &lt;/div&gt;
- &lt;p className="read-the-docs"&gt;
+   &lt;p className={styles.readTheDocs}&gt;
      Click on the Vite and React logos to learn more
    &lt;/p&gt;
  &lt;/&gt;
</code></pre>
<p>更改文件后，可以发现 Vite 为我们自动更新了预览视图，这是因为 Vite 支持热模块替换（HMR）。 但修改后的组件并没有像原来一样保持居中，这是因为 CSS Module 会将类名转换为哈希值，不支持现有的 <code>#root</code> ID 选择器。 如果需要使用 ID 选择器，可以使用 <code>:global</code> 关键字，保留样式的原始全局性。</p>
<pre><code>- #root {
+ :global(#root) {
    max-width: 1280px;
    margin: 0 auto;
    padding: 2rem;
    text-align: center;
  }
</code></pre>
<h3>路由</h3>
<h4>安装路由组件</h4>
<p>React 是一个单页面应用程序（SPA）框架，单页面应用程序是指一个页面加载后，不会因为用户的操作而进行页面的重新加载。 当需要切换页面时，只会更新页面的部分内容，而不会重新加载整个页面，这便需要路由来管理页面的切换。</p>
<p>React Router 是一个用于管理 React 应用程序路由的库，它提供了一些组件，用于在 React 应用程序中实现路由功能。</p>
<p>首先需要安装 React Router，这是我们第一次添加依赖。</p>
<pre><code>npm install react-router-dom
</code></pre>
<p>这会在 <code>package.json</code> 中的 <code>dependencies</code> 节点下添加 <code>react-router-dom</code> 依赖。</p>
<pre><code>+ "react-router-dom": "^6.23.0"
</code></pre>
<p>现在，我们可以根据文档的指南为我们的应用程序添加路由能力。</p>
<h4>整理代码结构</h4>
<p>为了让页面结构更清晰，我们先整理以下目录结构。</p>
<p>在 <code>src</code> 目录下创建 <code>features</code> 文件夹，用于存放各个页面组件。</p>
<p>将组件 <code>App</code> 重命名为 <code>Home</code>，并移动到 <code>features/home</code> 目录下，此操作建议使用 IDE 的重构/重命名功能， 这样，相关的引用也会被自动更新。操作完成后，访问浏览器检查所有的引用是否正常。</p>
<p>重新创建 <code>App</code> 组件，用于包裹整个应用程序，这样可以在 <code>App</code> 组件中添加路由。</p>
<pre><code>import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./features/home/Home.tsx";

const router = createBrowserRouter(
  [
    {
      path: `/`,
      element: &lt;Home/&gt;
    }
  ]
);

export default function App() {
  return (
    &lt;RouterProvider router={router}
                    fallbackElement={&lt;p&gt;Loading...&lt;/p&gt;}
    /&gt;
  );
}

if (import.meta.hot) {
  import.meta.hot.dispose(() =&gt; router.dispose());
}
</code></pre>
<p>新的 <code>App</code> 模块道出了一个 <code>RouterProvider</code> 组件，用于提供路由功能。<code>RouterProvider</code> 组件接收一个 <code>router</code> 属性， 这个属性是一个路由对象，用于管理路由。<code>router</code> 对象是通过 <code>createBrowserRouter</code> 函数创建的，这个函数接收一个路由配置数组， 返回一个路由对象。路由配置数组中的每个元素都是一个路由配置对象，包含了路由的路径和对应的元素。</p>
<p><code>createBrowserRouter</code> 函数返回的路由对象有一个 <code>dispose</code> 方法，用于清理路由对象。在开发模式下，当模块被热替换时， 会调用 <code>dispose</code> 方法，清理路由对象。</p>
<p>现在，应用程序的路由已经配置好了，但是还需要在入口文件中引入 <code>App</code> 组件。由于重构操作，<code>main.tsx</code> 中的原来的 <code>&lt;App /&gt;</code> 也被重命名为 <code>&lt;Home /&gt;</code>，所以需要将其改为 <code>&lt;App /&gt;</code>。重新导入模块 <code>App</code> 并使用它。</p>
<pre><code>- import Home from './features/home/Home.tsx'
+ import App from './App.tsx'

  ReactDOM.createRoot(document.getElementById('root')!).render(
    &lt;React.StrictMode&gt;
- &lt;Home /&gt;
+     &lt;App /&gt;
    &lt;/React.StrictMode&gt;,
  )
</code></pre>
<p>现在，重新访问 http://localhost:5173/ ，可以看到页面上仍然正常显示着 <code>Home</code> 组件的内容，但此时已经是通过路由管理的。</p>
<h4>创建新页面</h4>
<p>现在，我们来创建一个新的页面，用于展示一个关于页面。</p>
<p>在 <code>features/about</code> 目录下创建组件 <code>About.tsx</code>。</p>
<pre><code>function About() {
  return (
    &lt;&gt;
      &lt;h1&gt;About&lt;/h1&gt;
      &lt;p&gt;This is a simple app to demonstrate how to create a simple app using Vite and React.&lt;/p&gt;
    &lt;/&gt;
  );
}

export default About;
</code></pre>
<p>然后，我们需要在 <code>App</code> 组件中添加一个新的路由配置，用于展示 <code>About</code> 页面。</p>
<pre><code>import About from "./features/about/About.tsx";

const router = createBrowserRouter(
  [
    {
      path: `/`,
      element: &lt;Home/&gt;
    },
    {
      path: `/about`,
      element: &lt;About/&gt;
    }
  ]
);
</code></pre>
<h3>钩子</h3>
<p>React Hook 是 React 16.8 中引入的新特性，用于在函数组件中添加状态和副作用。 此处对一些常用的钩子进行简单介绍。</p>
<h4><code>useState</code></h4>
<p>用于在函数组件中添加状态，可以视为一个组件内部的变量。 状态是一个组件可以改变的值，它可以影响组件的行为和渲染输出。 <code>useState</code> 接受一个参数，这个参数是状态的初始值，然后返回一个包含两个元素的数组。 第一个元素是当前的状态值，第二个元素是一个函数，用于更新状态值。 调用这个更新函数并传入一个新的值时，React 会重新渲染组件，并使用新的值作为状态。</p>
<p>以 <code>Home.tsx</code> 为例。</p>
<p>我们使用 <code>useState</code> 创建了一个新的状态变量 <code>count</code>，并给它设置了初始值 <code>0</code>。 我们还得到了一个函数 <code>setCount</code>，用于更新 <code>count</code> 的值。 当用户点击按钮时，我们调用 <code>setCount</code> 来增加 <code>count</code> 的值，这会触发组件的重新渲染。</p>
<h4><code>useEffect</code></h4>
<p>用于在函数组件中执行副作用操作。副作用操作包括数据获取、订阅或手动修改 DOM 等等。 这些操作通常在组件渲染后执行，以不影响组件的渲染性能。</p>
<p><code>useEffect</code> 接受两个参数：一个是执行副作用的函数，另一个是依赖数组。 当依赖数组中的值发生变化时，副作用函数就会被执行。如果不提供依赖数组，那么副作用函数会在每次渲染后都执行。</p>
<p>以 <code>Effect.tsx</code> 为例。</p>
<p>在这个例子中，我们使用 <code>useEffect</code> 来更新 <code>document</code> 的 <code>title</code>。 当 <code>count</code> 变化时，副作用函数就会被重新执行，更新 <code>document</code> 的 <code>title</code>。 当组件卸载或者下一次 <code>useEffect</code> 执行前，清理函数会被调用，将 <code>document</code> 的 <code>title</code> 重置为 <code>React App</code>。</p>
<p>需要注意的是，在开发模式下，我们启用了 React 严格模式，这会导致 <code>useEffect</code> 中的副作用函数被调用两次。 严格模式下，React 会在渲染过程中执行两次副作用函数，以确保副作用函数的纯净性，即相同的输入应始终产生相同的输出， 这有助于帮我们发现一些潜在的问题。</p>
<h4><code>useContext</code></h4>
<p>用于在函数组件中访问 React 上下文。React 上下文是一个全局的数据存储区，用于在组件树中传递数据， 而无需通过 props 显式传递。</p>
<p>以 <code>Context.tsx</code> 为例。</p>
<p><code>useContext</code> 接受一个 <code>context</code> 对象（<code>React.createContext</code> 的返回值）作为参数，并返回该 <code>context</code> 的当前值。 当前的 <code>context</code> 值是由上层组件中距离当前组件最近的 <code>&lt;MyContext.Provider&gt;</code> 的 <code>value</code> 决定的。</p>
<h4><code>useReducer</code></h4>
<p>用于在函数组件中管理复杂的状态逻辑。<code>useReducer</code> 类似于 Redux 中的 <code>reducer</code>，它接受一个 <code>reducer</code> 函数和一个初始状态， 并返回一个当前状态和一个 <code>dispatch</code> 函数。</p>
<p><code>useReducer</code> 可以理解为是一种状态机，<code>reducer</code> 函数接收当前的状态和一个 <code>action</code>，返回一个新的状态。</p>
<p>以 <code>Reducer.tsx</code> 为例。</p>
<p>我们首先定义了一个 <code>reducer</code> 函数，它接受当前的 <code>state</code> 和一个 <code>action</code>，根据 <code>action</code> 的类型返回新的 <code>state</code> 。然后我们使用 <code>useReducer</code>，传入 <code>reducer</code> 函数和初始状态，得到当前的 <code>state</code> 和一个 <code>dispatch</code> 函数。在按钮的点击事件中，我们调用 <code>dispatch</code> 函数，传入一个 <code>action</code> 对象，触发 <code>state</code> 的更新。</p>
<h4><code>useMemo</code></h4>
<p>用于在函数组件中缓存计算结果，通过记忆计算结果来避免在每次渲染时都进行重计算，优化性能。 <code>useMemo</code> 接受两个参数：一个是创建记忆值的函数，另一个是依赖数组。当依赖数组中的值发生变化时， <code>useMemo</code> 就会返回一个新的记忆值。如果依赖数组中的值没有变化，那么 <code>useMemo</code> 就会返回上一次的记忆值。</p>
<p>以 <code>Memo.tsx</code> 为例。</p>
<p>我们有一个复杂的计算函数 <code>expensive</code> 和 <code>expensiveWithoutMemo</code>，它们都会根据 <code>count</code> 的值进行计算。<code>expensive</code> 使用了 <code>useMemo</code>，而 <code>expensiveWithoutMemo</code> 没有。当我们改变输入框的值时，组件会重新渲染，但 <code>count</code> 的值并没有改变。对于 <code>expensive</code> 函数，由于 <code>count</code> 没有改变，<code>useMemo</code> 会返回上一次计算的结果，不会重新计算。但对于 <code>expensiveWithoutMemo</code> 函数，每次组件渲染时都会重新计算。因此，通过使用 <code>useMemo</code>，我们可以避免在每次渲染时都进行复杂的计算，从而优化性能。</p>
<h4><code>useCallback</code></h4>
<p>用于在函数组件中缓存回调函数，通过记忆回调函数来避免在每次渲染时都创建新的回调函数，优化性能。 <code>useCallback</code> 接受两个参数：一个是创建函数的函数，另一个是依赖数组。当依赖数组中的值发生变化时，<code>useCallback</code> 就会返回一个新的函数实例。如果依赖数组中的值没有变化，那么 <code>useCallback</code> 就会返回上一次的函数实例。</p>
<p>以 <code>Callback.tsx</code> 为例。</p>
<p>我们使用 <code>useCallback</code> 创建了一个记忆的 <code>increment</code> 函数。只有当 <code>count</code> 变化时，<code>useCallback</code> 才会返回一个新的 <code>increment</code> 函数。如果 <code>count</code> 没有变化，那么 <code>useCallback</code> 就会返回上一次的 <code>increment</code> 函数。这样，即使组件重新渲染，<code>increment</code> 函数的引用也不会变化，除非 <code>count</code> 变化。</p>
<p>这就是 <code>useCallback</code> 的主要用途：它可以帮助我们避免在每次渲染时都创建新的函数，从而优化性能。但是，需要注意的是，<code>useCallback</code> 本身也有一定的开销，因此，只有在必要的时候（例如，当函数作为依赖项传递给其他 Hooks，或者当函数传递给性能敏感的子组件时）才应该使用它。</p>
<h4><code>useRef</code></h4>
<p>用于在函数组件中创建一个可变的引用。<code>useRef</code> 返回一个可变的对象，这个对象的 <code>current</code> 属性可以存储任意值。 这个对象在组件的整个生命周期中保持不变，不会因为组件的重新渲染而改变。</p>
<p>以 <code>Ref.tsx</code> 为例。</p>
<p>我们使用 <code>useRef</code> 创建了一个 <code>ref</code>，并将其赋值给了一个输入框。然后，当按钮被点击时，我们使用 <code>ref</code> 来让输入框获取焦点。</p>
<p>需要注意的是，<code>useRef</code> 返回的 <code>ref</code> 对象有一个 <code>current</code> 属性，用于存储 <code>ref</code> 的值。在这个例子中，<code>current</code> 属性的值就是输入框的 DOM 节点。</p>
<h3>Redux</h3>
<p><a href="https://cn.redux.js.org/">Redux</a> 是一个用于管理 JavaScript 应用程序状态的开源库。它提供了一种在应用程序中以一种可预测的方式来管理和更新状态的方法。Redux 特别适用于那些由于其状态在很多组件之间共享而变得复杂的 JavaScript 应用程序。</p>
<p>Redux 的核心概念包括：</p>
<ul>
<li>
<p><strong>Store</strong>：在 Redux 中，整个应用程序的状态被存储在一个对象树中，这个对象树被称为 Store。</p>
</li>
<li>
<p><strong>Action</strong>：Action 是描述发生了什么的普通对象。它是改变状态的唯一途径。每个 Action 都有一个 <code>type</code> 字段，用来描述 Action 的类型。</p>
</li>
<li>
<p><strong>Reducer</strong>：Reducer 是一个纯函数，它接收当前的状态和一个 Action，然后返回一个新的状态。在 Redux 中，所有的状态更新逻辑都在 Reducer 中完成。</p>
</li>
</ul>
<p>Redux 的工作流程如下：</p>
<ol>
<li>
<p>发起 Action：应用程序中的某个事件触发了一个 Action。</p>
</li>
<li>
<p>Reducer 计算新的状态：Store 调用 Reducer 函数，并传入当前的状态和触发的 Action。Reducer 函数根据 Action 的类型计算出新的状态。</p>
</li>
<li>
<p>Store 更新状态：Store 将新的状态保存下来。然后，Store 会通知所有的监听器，状态已经更新。</p>
</li>
</ol>
<p>Redux 的主要优点是它提供了一种可预测的状态管理方法，使得状态的更新在任何时候都是可追踪的。此外，由于 Redux 的状态存储在一个单一的 Store 中，这使得在调试和测试应用程序时更加方便。</p>
<h4>安装 Redux Toolkit 和 React Redux</h4>
<p>Redux Toolkit 是一个官方的 Redux 工具包，它提供了一些工具和库，用于简化 Redux 的使用。 React Redux 是一个用于在 React 应用程序中使用 Redux 的库。</p>
<pre><code>npm install @reduxjs/toolkit react-redux
</code></pre>
<ul>
<li>定义根 State 和 Dispatch 类型</li>
</ul>
<p>创建 <code>src/app/store.ts</code> 文件</p>
<pre><code>import { Action, ThunkAction, configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})

// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType&lt;typeof store.getState&gt;

// 推断出类型: { counter: CounterState }
export type AppDispatch = typeof store.dispatch

export type AppThunk&lt;ReturnType = void&gt; = ThunkAction&lt;
  ReturnType,
  RootState,
  unknown,
  Action&lt;string&gt;
&gt;
</code></pre>
<h4>定义 Hooks 类型</h4>
<p>创建 <code>src/app/hooks.ts</code> 文件</p>
<pre><code>import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// 在整个应用程序中使用，而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () =&gt; AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook&lt;RootState&gt; = useSelector
</code></pre>
<h4>定义 Slice State 和 Action</h4>
<p>每个 Slice 都包含了一个 Reducer 函数，用于处理 Action，并且可以包含一些额外的 Reducer 函数，用于处理其他的 Action。</p>
<h4>编写模块代码</h4>
<p>可以参考组件 <code>Counter</code>。</p>
<h3>API 调用</h3>
<p><a href="https://axios-http.com/">axios</a> 是一个基于 Promise 的 HTTP 客户端，用于在浏览器和 Node.js 中发送 HTTP 请求。</p>
<pre><code>npm install axios
</code></pre>
<p>可以参考 <code>Timestamp</code> 模块。</p>
<p>在 timestampAPI.ts 文件中，定义了一个异步函数 <code>getTimestamp</code>，用于获取当前时间戳。</p>
<pre><code>import axios from "axios";

export interface WorldTimeApiDto {
  abbreviation: string;
  client_ip: string;
  datetime: string;
  day_of_week: number;
  day_of_year: number;
  dst: boolean;
  dst_from: string | null;
  dst_offset: number;
  dst_until: string | null;
  raw_offset: number;
  timezone: string;
  unixtime: number;
  utc_datetime: string;
  utc_offset: string;
  week_number: number;
}

export async function getTimestamp() {
  const response = await axios.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
  const dto = response.data as WorldTimeApiDto;
  return Date.parse(dto.datetime);
}
</code></pre>
<p>如果需要配置请求头，可以在 axios 实例中配置。</p>
<pre><code>await axios.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai", {
  headers: {
    "Content-Type": "application/json"
  }
});
</code></pre>
<h3>组件库</h3>
<p><a href="https://ant-design.antgroup.com/index-cn">Ant Design</a> 是一个用于构建 React 应用程序的 UI 组件库。</p>
<pre><code>npm install antd
</code></pre>
<p>安装完成后，即可在需要引用 Ant Design 的组件时进行引用，并参考文档进行调用。</p>
<p>可以参考组件 <code>AntDesign</code>。</p>
<h3>图表库</h3>
<p><a href="https://ant-design-charts.antgroup.com/">Ant Design Charts</a> 是一个基于 Ant Design 设计体系的图表库，是 AntV 的 React 版本。</p>
<pre><code>npm install @ant-design/charts
</code></pre>
<p>安装完成后，即可在需要引用 Ant Design Charts 的组件时进行引用，并参考文档进行调用。</p>
<p>可以参考组件 <code>Chart</code>。</p>
<h2>部署</h2>
<p>发布应用程序时，需要将应用程序构建为生产版本，并将构建后的文件部署到服务器上。</p>
<h3>构建</h3>
<p>构建应用程序时，需要使用 <code>build</code> 脚本。</p>
<pre><code>npm run build
</code></pre>
<p>构建完成后，默认情况下，Vite 会在 <code>dist</code> 目录下生成一个生产版本的应用程序，该位置可以在 <code>vite.config.ts</code> 中配置。</p>
<p>此时，便可以通过 <code>preview</code> 脚本来预览构建后的应用程序。</p>
<h3>部署</h3>
<p>部署应用程序时，需要将构建后的文件部署到服务器上。</p>
<p>此处以编写 DockerFile 将应用程序部署到 Docker 容器内的 Nginx 服务器为例。</p>
<pre><code>FROM node:22-alpine as build
WORKDIR /app
COPY /app/package*.json ./
RUN npm install
COPY /app/ ./
RUN npm run build

FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>在 DockerFile 文件中，首先使用 Node 镜像作为构建环境，安装依赖并构建应用程序， 然后使用 Nginx 镜像作为运行环境，将构建后的文件复制到 Nginx 的静态文件目录中，并暴露 80 端口。</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="前端"/>
  </entry>
  <entry>
    <title>Avalonia 中的 Prism</title>
    <link href="https://gehongyan.cn/dotnet/avalonia-prism/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/dotnet/avalonia-prism/</id>
    <published>2024-03-31T00:00:00.000Z</published>
    <updated>2024-03-31T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<p>::github{repo="gehongyan/AvaloniaTutorials"}</p>
<blockquote>
<p>本文源码位于仓库子目录 <a href="https://github.com/gehongyan/AvaloniaTutorials/tree/main/AvaloniaPrism">AvaloniaPrism</a>。</p>
</blockquote>
<h2>添加对 Prism 的引用</h2>
<ol>
<li>
<p>安装 Prism 库</p>
<pre><code>dotnet add package Prism.Avalonia
dotnet add package Prism.DryIoc.Avalonia
</code></pre>
</li>
<li>
<p>在 <code>App.xaml.cs</code> 中添加 Prism 的引用</p>
<pre><code>using Prism.Ioc;
using Prism.DryIoc;
</code></pre>
</li>
<li>
<p>在 <code>App.xaml.cs</code> 中修改基类为 <code>PrismApplication</code></p>
<pre><code>public partial class App : PrismApplication
</code></pre>
</li>
<li>
<p>实现抽象方法 <code>RegisterTypes</code></p>
<pre><code>/// &lt;inheritdoc /&gt;
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}

/// &lt;inheritdoc /&gt;
protected override AvaloniaObject CreateShell()
{
    return Container.Resolve&lt;MainWindow&gt;();
}
</code></pre>
</li>
<li>
<p>删除 <code>OnFrameworkInitializationCompleted</code></p>
</li>
<li>
<p>在 <code>Initialize</code> 中初始化 Prism</p>
<pre><code>public override void Initialize()
{
    AvaloniaXamlLoader.Load(this);
    base.Initialize(); // &lt;-- 添加此行
}
</code></pre>
</li>
</ol>
<h2>视图与视图模型的绑定</h2>
<p>添加视图 <code>MainView</code> 及其视图模型 <code>MainViewModel</code>，在 <code>MainView</code> 中添加 <code>prism:ViewModelLocator.AutoWireViewModel="True"</code>，最后在 <code>MainWindow</code> 中引用该视图。 运行引用程序，可以发现，<code>MainView.DataContext</code> 被自动填充了 <code>MainViewModel</code> 的实例。 这是因为 Prism 会对设置了 <code>AutoWireViewModel</code> 为 <code>True</code> 的视图根据约定自动发现并绑定视图与视图模型。</p>
<p>不妨先看一下 Prism 绑定视图模型的<a href="https://github.com/PrismLibrary/Prism/blob/2a25770c0afc99839c56c0d416b877a3788bd371/src/Prism.Core/Mvvm/ViewModelLocationProvider.cs#L114-L140">源码</a>：</p>
<pre><code>/// &lt;summary&gt;
/// Automatically looks up the viewmodel that corresponds to the current view, using two strategies:
/// It first looks to see if there is a mapping registered for that view, if not it will fallback to the convention based approach.
/// &lt;/summary&gt;
/// &lt;param name="view"&gt;The dependency object, typically a view.&lt;/param&gt;
/// &lt;param name="setDataContextCallback"&gt;The call back to use to create the binding between the View and ViewModel&lt;/param&gt;
public static void AutoWireViewModelChanged(object view, Action&lt;object, object&gt; setDataContextCallback)
{
   // Try mappings first
   object viewModel = GetViewModelForView(view);

   // try to use ViewModel type
   if (viewModel == null)
   {
       // check type mappings
       var viewModelType = GetViewModelTypeForView(view.GetType());

       // check platform View to ViewModel resolver
       if (viewModelType == null)
           viewModelType = _defaultViewToViewModelTypeResolver(view);

       // fallback to convention based
       if (viewModelType == null)
           viewModelType = _defaultViewTypeToViewModelTypeResolver(view.GetType());

       if (viewModelType == null)
           return;

       viewModel = _defaultViewModelFactoryWithViewParameter != null ? _defaultViewModelFactoryWithViewParameter(view, viewModelType) : _defaultViewModelFactory(viewModelType);
   }

   setDataContextCallback(view, viewModel);
}
</code></pre>
<p>它首先查看是否为该视图注册了映射，如果没有，则会回退到基于约定的方法。</p>
<ol>
<li><code>GetViewModelForView</code></li>
</ol>
<p>该方法尝试从 <code>Dictionary&lt;string, Func&lt;object&gt;&gt; _factories</code> 中寻找键为指定视图类型名称的键值对，其值为对应的<strong>视图模型的生成委托</strong>。 这是最短完成解析的路径，因为这需要我们手动注册视图与视图模型生成委托的映射关系。</p>
<p><code>Register</code> 的其中一个重载中访问了 <code>_factories</code> 索引器的 <code>set</code> 方法，将视图类型名称与视图模型生成委托的键值对添加到 <code>_factories</code> 中。 因此，我们可以在 <code>RegisterTypes</code> 中手动指定视图与视图模型生成委托的绑定关系。</p>
<pre><code>ViewModelLocationProvider.Register(typeof(MainView).FullName, () =&gt; new MainViewModel());
</code></pre>
<ol>
<li><code>GetViewModelTypeForView</code></li>
</ol>
<p>该方法尝试从 <code>Dictionary&lt;string, Type&gt; _typeFactories</code> 中寻找键为指定视图类型名称的键值对，其值为对应的<strong>视图模型类型</strong>。</p>
<p><code>Register</code> 的另一个重载中访问了 <code>_typeFactories</code> 索引器的 <code>set</code> 方法，将视图类型名称与视图模型类型的键值对添加到 <code>_typeFactories</code> 中。 因此，我们可以在 <code>RegisterTypes</code> 中手动指定视图与视图模型类型的绑定关系。也存在另一个泛型重载，可以直接指定视图与视图模型类型的绑定关系。</p>
<pre><code>ViewModelLocationProvider.Register&lt;MainView, MainViewModel&gt;();
ViewModelLocationProvider.Register(typeof(MainView).FullName, typeof(MainViewModel));
</code></pre>
<ol>
<li>尝试从 <code>Func&lt;object, Type&gt; _defaultViewToViewModelTypeResolver</code> 生成视图模型类型。</li>
</ol>
<p>这是由各个平台各自实现的默认视图模型类型生成委托，目前仅有 MAUI 设置了该解析器。</p>
<ol>
<li>尝试从 <code>Func&lt;Type, Type&gt; _defaultViewTypeToViewModelTypeResolver</code> 生成视图模型类型。</li>
</ol>
<p>这是最后的回退方案，它会根据视图类型名称生成视图模型类型名称，然后尝试从程序集中加载该视图模型类型。 该生成委托的默认逻辑为：</p>
<pre><code>string viewName = viewType.FullName;
viewName = viewName.Replace(".Views.", ".ViewModels.");
string viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
string suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
string viewModelName = string.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", viewName, suffix, viewAssemblyName);
return Type.GetType(viewModelName);
}
</code></pre>
<p>以 <code>viewType</code> 为 <code>typepf(MainView)</code> 为例：</p>
<ul>
<li><code>viewName</code> 为 <code>AvaloniaPrism.Views.MainView</code></li>
<li><code>viewName</code> 中的 <code>.Views.</code> 被替换为 <code>.ViewModels.</code>，得到 <code>AvaloniaPrism.ViewModels.MainView</code></li>
<li><code>viewAssemblyName</code> 为 <code>AvaloniaPrism, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</code></li>
<li>如果 <code>viewName</code> 以 <code>View</code> 结尾，则 <code>suffix</code> 为 <code>Model</code>，否则为 <code>ViewModel</code></li>
<li><code>viewModelName</code> 为 <code>AvaloniaPrism.ViewModels.MainViewModel, AvaloniaPrism, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</code></li>
<li>通过 <code>Type.GetType(viewModelName)</code> 获取到 <code>MainViewModel</code> 的类型</li>
</ul>
<p>可以发现，要想实现自动转换，视图与视图模型之间需要遵循一定的约定：</p>
<ul>
<li>视图模型位于与视图类型相同的程序集中</li>
<li>视图模型在一个名为 .ViewModels 的子命名空间中</li>
<li>视图位于一个名为 .Views 的子命名空间中</li>
<li>视图模型名称与视图名称相对应，并以 "ViewModel" 结尾</li>
</ul>
<ol>
<li>如果获取到了视图模型的类型，则通过 <code>Activator.CreateInstance</code> 创建视图模型的实例，并通过<code>setDataContextCallback</code> 将其赋值给视图的 <code>DataContext</code> 属性。</li>
</ol>
<h2>依赖注入</h2>
<p>Prism 中的依赖注入主要是由 DryIoc 完成的，Microsoft.Extensions.DependencyInjection 则是在付费商用许可中提供。 因此，此处仅介绍 Avalonia 中 DryIoc 的使用。</p>
<h3>生命周期</h3>
<p>Prism 与大多数依赖注入容器一样，支持以下三种生命周期：</p>
<ul>
<li>Transient：瞬态，每次请求都会创建一个新的实例</li>
<li>Singleton：单例，整个应用程序中只有一个实例</li>
<li>Scoped：作用域，在同一作用域中只有一个实例</li>
</ul>
<pre><code>// 瞬态
containerRegistry.Register&lt;IFooService, FooService&gt;();
// 单例
containerRegistry.RegisterSingleton&lt;IFooService, FooService&gt;();
// 作用域
containerRegistry.RegisterScoped&lt;IFooService, FooService&gt;();
// 通过实例注册
containerRegistry.RegisterInstance&lt;IFooService&gt;(new FooService());
</code></pre>
<h3>惰性解析</h3>
<p>默认情况下，注册服务也会同步注册其相关的 <code>Func&lt;IService&gt;</code> 服务委托及 <code>Lazy&lt;IService&gt;</code> 惰性实例。 可以进一步将服务实例的解析推迟到第一次访问时，而不是在注册时立即解析。 但需要注意的是，如果服务是单例的，那惰性解析不一定会带来性能提升，因为单例服务在第一次解析时就会被创建，相反，惰性解析可能会带来额外的开销。</p>
<h2>绑定</h2>
<p>Prism 中提供了 <code>BindableBase</code> 类，它实现了 <code>INotifyPropertyChanged</code> 接口，可以方便地实现属性绑定。</p>
<pre><code>public class MainViewModel : BindableBase
{
    private string _title = "Hello World";
    public string Title
    {
        get =&gt; _title;
        set =&gt; SetProperty(ref _title, value);
    }
}
</code></pre>
<h2>命令</h2>
<p>Prism 中的命令主要是通过 <code>DelegateCommand</code> 和 <code>CompositeCommand</code> 实现的。</p>
<h3>DelegateCommand</h3>
<p><code>DelegateCommand</code> 是一个泛型类，它接受一个泛型参数，表示命令的参数类型，也可以不传入参数类型，即为无参命令。</p>
<pre><code>public class MainViewModel : BindableBase
{
    public DelegateCommand SayHelloCommand { get; }

    public MainViewModel()
    {
        SayHelloCommand = new DelegateCommand(SayHello);
    }

    private void SayHello()
    {
        Title = "Hello Avalonia!";
    }
}
</code></pre>
<p>如果需要控制命令是否可用，可以通过 <code>CanExecute</code> 方法返回一个布尔值来实现。</p>
<pre><code>public class MainViewModel : BindableBase
{
    private string _title = "Hello World";

    public MainViewModel()
    {
        SayHelloCommand = new DelegateCommand(SayHello, CanSayHello); // &lt;-- 修改此行
    }

    public string Title
    {
        get =&gt; _title;
        set =&gt; SetProperty(ref _title, value);
    }

    public DelegateCommand SayHelloCommand { get; }

    private void SayHello()
    {
        Title = "Hello Avalonia!";
    }

    private bool CanSayHello() // &lt;-- 添加此方法
    {
        return !string.IsNullOrEmpty(Title);
    }
}
</code></pre>
<p>如果 <code>CanExecute</code> 依赖其它可变值，可以通过 <code>RaiseCanExecuteChanged</code> 方法来主动通知命令重新计算是否可用。</p>
<pre><code>public class MainViewModel : BindableBase
{
    private string _title = "Hello World";

    public MainViewModel()
    {
        SayHelloCommand = new DelegateCommand(SayHello, CanSayHello);
    }

    public string Title
    {
        get =&gt; _title;
        set
        {
            SetProperty(ref _title, value);
            SayHelloCommand.RaiseCanExecuteChanged(); // &lt;-- 添加此行
        }
    }

    public DelegateCommand SayHelloCommand { get; }

    private void SayHello()
    {
        Title = "Hello Avalonia!";
    }

    private bool CanSayHello()
    {
        return !string.IsNullOrEmpty(Title);
    }
}
</code></pre>
<p>也可以通过 <code>ObservesCanExecute</code> 来指定命令是否可执行所依赖的属性变更。</p>
<pre><code>public class MainViewModel : BindableBase
{
    private bool _canSayHello = true;

    public MainViewModel()
    {
        SayHelloCommand = new DelegateCommand(SayHello)
            .ObservesCanExecute(() =&gt; CanSayHello); // &lt;-- 修改此行
    }

    public bool CanSayHello // 添加此属性
    {
        get =&gt; _canSayHello;
        set =&gt; SetProperty(ref _canSayHello, value);
    }

    public DelegateCommand SayHelloCommand { get; }

    private void SayHello()
    {
        Title = "Hello Avalonia!";
    }
}
</code></pre>
<h3>CompositeCommand</h3>
<p><code>CompositeCommand</code> 是一个命令集合，可以将多个命令组合成一个命令，当执行该命令时，会依次执行集合中的命令。 当集合中的所有命令的 <code>CanExecute</code> 方法返回 <code>true</code> 时，<code>CompositeCommand</code> 的 <code>CanExecute</code> 方法才会返回 <code>true</code>。</p>
<pre><code>public class MainViewModel : BindableBase
{
    public MainViewModel()
    {
        CompositeCommand = new CompositeCommand();
        CompositeCommand.RegisterCommand(new DelegateCommand(SayHello));
        CompositeCommand.RegisterCommand(new DelegateCommand(SayGoodbye));
    }

    public CompositeCommand CompositeCommand { get; }

    private void SayHello()
    {
        Title = "Hello Avalonia!";
    }

    private void SayGoodbye()
    {
        Title = "Goodbye Avalonia!";
    }
}
</code></pre>
<p>如果视图与视图模型可能会被销毁，则也需要考虑符合命令的取消注册。</p>
<pre><code>SayHelloCompositeCommand.UnregisterCommand(new DelegateCommand(SayHello));
SayHelloCompositeCommand.UnregisterCommand(new DelegateCommand(SayGoodbye));
</code></pre>
<h2>事件聚合器</h2>
<p>Prism 中的事件聚合器是一个全局的事件总线，可以在不同的视图模型之间传递消息。</p>
<p><code>GetEvent</code> 方法的泛型参数需要一个继承自 <code>EventBase</code> 的事件参数，框架提供了 <code>PubSubEvent&lt;TPayload&gt;</code> 可供直接使用。</p>
<p><code>PubSubEvent&lt;TPayload&gt;</code> 是一个发布/订阅事件，可以通过 <code>Publish</code> 方法发布消息，通过 <code>Subscribe</code> 方法订阅消息。</p>
<p>如果需要操作 UI 元素，可以通过 <code>ThreadOption.UIThread</code> 来指定在 UI 线程上执行。</p>
<p>如果需要过滤指定的消息，可以通过 <code>Subscribe</code> 方法的的第四个参数 <code>Predicate&lt;TPayload&gt;</code> 来指定过滤条件。</p>
<pre><code>public class EventsViewModel : BindableBase, IDisposable
{
    private string _receivedMessage;
    private readonly SubscriptionToken _subscriptionToken;

    public EventsViewModel(IEventAggregator eventAggregator)
    {
        PublishEventCommand = new DelegateCommand(() =&gt;
            eventAggregator.GetEvent&lt;PubSubEvent&lt;string&gt;&gt;()
                .Publish($"Hello from Avalonia! It's {DateTime.Now} now."))
        _subscriptionToken = eventAggregator.GetEvent&lt;PubSubEvent&lt;string&gt;&gt;()
            .Subscribe(message =&gt; ReceivedMessage = $"Received message: {message}", ThreadOption.UIThread);
    }

    public DelegateCommand PublishEventCommand { get; private set; }

    public string ReceivedMessage
    {
        get =&gt; _receivedMessage;
        private set =&gt; SetProperty(ref _receivedMessage, value);
    }

    /// &lt;inheritdoc /&gt;
    public void Dispose()
    {
        GC.SuppressFinalize(this);
        _subscriptionToken.Dispose();
    }
}
</code></pre>
<h2>对话框服务</h2>
<p>使用对话框服务前，需要在 <code>App.xaml.cs</code> 中注册对话框服务。</p>
<pre><code>containerRegistry.RegisterDialog&lt;MessageBoxView, MessageBoxViewModel&gt;();
</code></pre>
<p>Prism 中提供了 <code>IDialogService</code> 接口服务，可以用于显示对话框。</p>
<p><code>dialogService.ShowDialog</code> 的其中一个重载可以接收视图名称、对话框参数、回调方法。</p>
<pre><code>DialogParameters parameters = new()
{
    { "title", "Dialog Title" },
    { "content", DialogContent }
};

dialogService.ShowDialog(nameof(MessageBoxView), parameters, result =&gt;
{
    DialogContent = result.Result == ButtonResult.OK
        ? "Dialog closed by OK"
        : "Dialog closed by Cancel";
});
</code></pre>
<p>为 <code>MessageBoxViewModel</code> 实现 <code>IDialogAware</code> 接口，可以让感知作为对话框数据模型的参数与状态。</p>
<p><code>OnDialogOpened</code> 方法，<code>IDialogParameters parameters</code> 参数上可以获取 <code>ShowDialog</code> 所传入的 <code>parameters</code>，可用于设置属性。</p>
<p><code>RequestClose</code> 事件可以用于关闭对话框，可以传入一个 <code>DialogResult</code> 枚举值，表示对话框的结果，以供 <code>ShowDialog</code> 的回调方法使用。</p>
<h2>区域导航</h2>
<p>Prism 中的区域导航主要由 <code>IRegionManager</code> 接口提供，可以用于将指定的区域导航到指定的视图。</p>
<p>要使用区域导航，首先需要在 <code>App.xaml.cs</code> 中注册区域导航服务。</p>
<pre><code>containerRegistry.RegisterForNavigation&lt;ServicesView, ServicesViewModel&gt;();
</code></pre>
<p>在视图中的 <code>ContentControl</code> 上添加 <code>prism:RegionManager.RegionName</code> 附加属性，指定区域名称。</p>
<pre><code>&lt;ContentControl prism:RegionManager.RegionName="MainRegion" /&gt;
</code></pre>
<p>在视图模型中，通过 <code>IRegionManager.RequestNavigate</code> 方法导航到指定的视图。</p>
<pre><code>public class MainWindowViewModel : BindableBase
{
    private readonly IRegionManager _regionManager;

    public MainWindowViewModel(IRegionManager regionManager)
    {
        _regionManager = regionManager;
        _regionManager.RequestNavigate("MainRegion", nameof(ServicesView));
    }
}
</code></pre>
<p>即可让指定的区域导航至指定的视图。</p>
<p><code>INavigationAware</code> 接口提供了参与导航的视图模型的相关信息。</p>
<ul>
<li><code>OnNavigatedTo</code> 方法在视图模型导航到时调用，可以获取导航参数。</li>
<li><code>OnNavigatedFrom</code> 方法在视图模型导航离开时调用。</li>
<li><code>IsNavigationTarget</code> 方法用于指示当前视图模型是否可以处理导航请求。</li>
</ul>
<p>在请求导航时可以传入参数：</p>
<pre><code>NavigationParameters parameters = new()
{
    { "at", DateTime.Now }
};
regionManager.RequestNavigate(RegionNames.ContentRegion, nameof(Views.NavigationsView), parameters);
</code></pre>
<p><code>NavigationContext</code> 上便可获取到传入的参数。</p>
<pre><code>DateTime at = navigationContext.Parameters.GetValue&lt;DateTime&gt;("at");
</code></pre>
<p><code>IRegionNavigationService</code> 上提供了 <code>IRegionNavigationJournal Journal</code> 属性，可以用于导航历史记录的管理。 <code>CanGoBack</code> 和 <code>CanGoForward</code> 属性，可以用于判断是否可以回退或前进。 <code>GoBack</code> 和 <code>GoForward</code> 方法，可以用于回退或前进。</p>
<p>如果需要让某些视图不被记录在导航历史记录中，例如启动页、登录页、对话框等中，可以为其实现 <code>IJournalAware</code>接口，并为 <code>PersistInHistory</code> 方法返回 <code>false</code>。</p>
<h2>区域适配器</h2>
<p>RegionManager.RegionName 所能附加的容器必须存在对应的区域适配器，否则 Prism 无法获知如何在容器中添加或删除视图。</p>
<p>内置的区域适配器有：</p>
<ul>
<li><code>ContentControlRegionAdapter</code></li>
<li><code>ItemsControlRegionAdapter</code></li>
</ul>
<p>而要在其它的容器上使用区域导航，需要自定义区域适配器。</p>
<pre><code>public class StackPanelRegionAdapter : RegionAdapterBase&lt;StackPanel&gt;
{
    public StackPanelRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
        : base(regionBehaviorFactory)
    {
    }

    protected override void Adapt(IRegion region, StackPanel regionTarget)
    {
        region.Views.CollectionChanged += (sender, e) =&gt;
        {
            if (e is { Action: NotifyCollectionChangedAction.Add, NewItems: not null })
            {
                foreach (Control item in e.NewItems)
                    regionTarget.Children.Add(item);
            }

            if (e is { Action: NotifyCollectionChangedAction.Remove, OldItems: not null })
            {
                foreach (Control item in e.OldItems)
                    regionTarget.Children.Remove(item);
            }
        };
    }

    protected override IRegion CreateRegion() =&gt; new SingleActiveRegion();
}
</code></pre>
<p>然后在 <code>App.xaml.cs</code> 中注册该区域适配器。</p>
<pre><code>protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
{
    base.ConfigureRegionAdapterMappings(regionAdapterMappings);
    regionAdapterMappings.RegisterMapping(typeof(StackPanel), Container.Resolve&lt;StackPanelRegionAdapter&gt;());
}
</code></pre>
<h2>模块</h2>
<p>模块是 Prism 中的一个概念，用于将应用程序分解为更小的功能单元，以便于管理和维护。</p>
<p>在模块的程序集内需要定义一个模块类，实现 <code>IModule</code> 接口。</p>
<pre><code>public class ModuleAModule : IModule
{
    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
    }

    public void OnInitialized(IContainerProvider containerProvider)
    {
    }
}
</code></pre>
<p>在模块的定义内，可以定于仅属于该模块的服务、视图、视图模型、导航等。这使得服务的定义不再混杂在主程序集中，而是更加清晰地分离开来。</p>
<pre><code>containerRegistry.RegisterForNavigation&lt;ModuleAView, ModuleAViewModel&gt;();
containerRegistry.Register&lt;IDelayService, DelayService&gt;();
</code></pre>
<p>在主程序集中，需要在 <code>App.xaml.cs</code> 中注册模块。</p>
<pre><code>protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    base.ConfigureModuleCatalog(moduleCatalog);
    moduleCatalog.AddModule&lt;ModuleAModule&gt;();
}
</code></pre>
<p>模块相关的视图、视图模型、服务等会在模块初始化时被注册到容器中，以便于在模块内外使用。</p>
<p>如果不想让主程序包含对模块的引用，而是在程序启动时扫描目录加载模块，可以使用 <code>DirectoryModuleCatalog</code>。</p>
<pre><code>protected override IModuleCatalog CreateModuleCatalog()
{
    const string modulePath = @".\Modules";
    if (!Directory.Exists(modulePath))
        Directory.CreateDirectory(modulePath);
    return new DirectoryModuleCatalog { ModulePath = modulePath };
}
</code></pre>
<p>如果需要判断某个模块是否被加载，可以通过 <code>IModuleManager</code> 接口上提供的方法来判断。</p>
<pre><code>if (moduleManager.ModuleExists("ModuleAModule")
    &amp;&amp; moduleManager.IsModuleInitialized("ModuleAModule"))
    regionManager.RequestNavigate(RegionNames.ContentRegion, "ModuleAView");、
</code></pre>
]]></content>
    <author><name>戈小荷</name></author>
    <category term=".NET"/>
  </entry>
  <entry>
    <title>JSON 序列化的自定义转换器</title>
    <link href="https://gehongyan.cn/dotnet/json-custom-converter/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/dotnet/json-custom-converter/</id>
    <published>2023-05-15T00:00:00.000Z</published>
    <updated>2023-05-15T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<p>::github{repo="gehongyan/JsonConverterTutorial"}</p>
<p>转换器是一种将对象或值与 JSON 进行相互转换的类，一般用于实现以下几种目标：</p>
<ol>
<li>重写内置转换器的默认行为。例如，将 <code>DateTimeOffset</code> 值与 Unix 时间戳进行相互转换。</li>
<li>为内置转换器不支持的类型提供 JSON 序列化和反序列化的支持。例如，在 .NET 6 中，内置转换器不支持将 <code>DateOnly</code> 或 <code>TimeOnly</code> 的值与 JSON 进行相互转换。</li>
<li>为自定义类型提供 JSON 序列化和反序列化的支持。例如，将 <code>Person</code> 类型与 JSON 进行相互转换。</li>
<li>其它需要自订序列化与反序列化行为的情况。</li>
</ol>
<h2>JSON 转换器</h2>
<p>以重写 <code>DateTimeOffset</code> 类型的 JSON 序列化默认行为为例。</p>
<p><code>DateTimeOffset</code> 的默认序列化结果与 <code>.ToString(@"'""'yyyy-MM-ddTHH:mm:ss.FFFFFFFzzz'""'")</code> 的结果一致，即将 <code>DateTimeOffset</code> 的值转换为 ISO 8601 格式的字符串。</p>
<p>如果我们希望将 <code>DateTimeOffset</code> 的值转换为 Unix 时间戳，可以通过重写 <code>DateTimeOffset</code> 的默认转换器来实现。</p>
<h3>转换器的应用</h3>
<p>主要有两种常用方法用于注册转换器：</p>
<ul>
<li>通过 <code>JsonSerializerOptions.Converters</code> 属性注册转换器。</li>
</ul>
<pre><code>JsonSerializerOptions options = new();
options.Converters.Add(new DateTimeOffsetTimeStampConverter());
JsonSerializer.Serialize(obj, options);
</code></pre>
<ul>
<li>通过 <code>JsonConverterAttribute</code> 特性注册转换器。</li>
</ul>
<pre><code>class Person
{
    [JsonConverter(typeof(DateTimeOffsetTimeStampConverter))]
    public DateTimeOffset BirthDate { get; set; }
}
</code></pre>
<h3>转换器的工厂方法</h3>
<p>如果转换器需要接受泛型参数，为多种类型提供转换支持，可以通过转换器的工厂方法来实现。</p>
<h3>对于复杂 API 行为的支持</h3>
<p>如有第三方 API 的行为不符合预期，可以通过转换器来实现对其行为的支持。</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term=".NET"/>
  </entry>
  <entry>
    <title>正则表达式及在 .NET 中的应用</title>
    <link href="https://gehongyan.cn/dotnet/regex-in-dotnet/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/dotnet/regex-in-dotnet/</id>
    <published>2023-04-08T00:00:00.000Z</published>
    <updated>2023-04-08T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<p>::github{repo="gehongyan/RegexTutorial"}</p>
<h1>正则表达式</h1>
<p>正则表达式，是一种文本模式，描述了一系列匹配某个句法规则的字符串。使用正则表达式进行的任务一般可以分为两类，即检索和替换。</p>
<h2>正则表达式的语法</h2>
<h3>普通字符</h3>
<p>普通字符，即不具有特殊含义的字符，如字母、数字、汉字等，它们在正则表达式中的含义与在普通文本中的含义相同，如在正则表达式中，<code>a</code> 代表字母 <code>a</code>，<code>1</code> 代表数字 <code>1</code>，<code>你</code> 代表汉字 <code>你</code>。</p>
<h3>元字符</h3>
<p>元字符，即具有特殊含义的字符。</p>
<table>
<thead>
<tr>
<th>元字符</th>
<th>含义</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>^</code></td>
<td>匹配整个字符串的开始位置</td>
<td><code>^a</code> 匹配 <code>at</code>，但不匹配 <code>cat</code></td>
</tr>
<tr>
<td><code>$</code></td>
<td>匹配整个字符串的结束位置</td>
<td><code>i$</code> 匹配 <code>hi</code>，但不匹配 <code>hit</code></td>
</tr>
<tr>
<td><code>.</code></td>
<td>匹配除换行符以外的任意字符</td>
<td><code>a.c</code> 匹配 <code>abc</code>，<code>a c</code>，<code>a1c</code>，<code>a*c</code> 等</td>
</tr>
<tr>
<td><code>?</code></td>
<td>匹配前一个字符 0 次或 1 次</td>
<td><code>hi?</code> 匹配 <code>h</code>，<code>hi</code>，但不匹配 <code>hii</code>，<code>hiii</code> 等</td>
</tr>
<tr>
<td><code>*</code></td>
<td>匹配前一个字符 0 次或多次</td>
<td><code>hi*</code> 匹配 <code>h</code>，<code>hi</code>，<code>hii</code>，<code>hiii</code> 等</td>
</tr>
<tr>
<td><code>+</code></td>
<td>匹配前一个字符 1 次或多次</td>
<td><code>hi+</code> 匹配 <code>hi</code>，<code>hii</code>，<code>hiii</code> 等，但不匹配 <code>h</code></td>
</tr>
<tr>
<td><code>{n}</code></td>
<td>匹配前一个字符 n 次</td>
<td><code>hi{2}</code> 匹配 <code>hii</code>，但不匹配 <code>h</code>，<code>hi</code>，<code>hiii</code> 等</td>
</tr>
<tr>
<td><code>{n,}</code></td>
<td>匹配前一个字符 n 次或更多次</td>
<td><code>hi{2,}</code> 匹配 <code>hii</code>，<code>hiii</code> 等，但不匹配 <code>h</code>，<code>hi</code> 等</td>
</tr>
<tr>
<td><code>{n,m}</code></td>
<td>匹配前一个字符 n 到 m 次</td>
<td><code>hi{1,3}</code> 匹配 <code>hi</code>，<code>hii</code>，<code>hiii</code> 等，但不匹配 <code>h</code></td>
</tr>
<tr>
<td><code>[ ]</code></td>
<td>匹配方括号中的任意字符</td>
<td><code>[abc]</code> 匹配 <code>a</code>，<code>b</code>，<code>c</code>，但不匹配 <code>d</code>，<code>e</code>，<code>f</code></td>
</tr>
<tr>
<td><code>[^ ]</code></td>
<td>匹配除方括号中的任意字符</td>
<td><code>[^abc]</code> 匹配 <code>d</code>，<code>e</code>，<code>f</code> 等，但不匹配 <code>a</code>，<code>b</code>，<code>c</code></td>
</tr>
<tr>
<td><code>( )</code></td>
<td>用于匹配或捕获</td>
<td><code>(abc)</code> 匹配 <code>abc</code>，并将 <code>abc</code> 作为一个整体分组</td>
</tr>
<tr>
<td><code>\|</code></td>
<td>用于分支选择</td>
<td><code>a\|b</code> 匹配 <code>a</code> 或 <code>b</code>，但不匹配 <code>ab</code></td>
</tr>
</tbody>
</table>
<h3>字符转义</h3>
<p>在正则表达式中，如果要匹配元字符本身，需要对元字符进行转义，即在元字符前加上反斜杠 <code>\</code>，使其被解释为普通字符，如 <code>.</code> 匹配除换行符以外的任意字符，如果要匹配 <code>.</code> 本身，则需要使用 <code>\.</code> 进行转义。</p>
<p>如果特殊字符出现在方括号 <code>[]</code> 中，则不需要转义，如 <code>[.]</code> 匹配 <code>.</code>，<code>[*]</code> 匹配 <code>*</code> 等。</p>
<p>如果要匹配反斜杠 <code>\</code>，则需要使用 <code>\\</code> 进行转义。</p>
<p>如果有特殊字符不便于输入与显示，可以使用 <code>\xhh</code> 的形式进行转义，其中 <code>hh</code> 为 ASCII 码的十六进制表示，如 <code>\x2e</code> 转义为 <code>.</code>，<code>\x66</code> 转义为 <code>f</code> 等。也可以使用 <code>\uXXXX</code> 的形式进行转义，其中 <code>XXXX</code> 为 Unicode 码的十六进制表示，如 <code>\u002e</code> 转义为 <code>.</code>，<code>\u0066</code> 转义为 <code>f</code> 等。</p>
<p>还有一些预留的转义字符，以下是常用的转义字符：</p>
<table>
<thead>
<tr>
<th>转义字符</th>
<th>含义</th>
<th>等价于</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>\n</code></td>
<td>换行符</td>
<td><code>\x0a</code> 或 <code>\u000a</code></td>
</tr>
<tr>
<td><code>\r</code></td>
<td>回车符</td>
<td><code>\x0d</code> 或 <code>\u000d</code></td>
</tr>
<tr>
<td><code>\t</code></td>
<td>水平制表符</td>
<td><code>\x09</code> 或 <code>\u0009</code></td>
</tr>
<tr>
<td><code>\v</code></td>
<td>垂直制表符</td>
<td><code>\x0b</code> 或 <code>\u000b</code></td>
</tr>
<tr>
<td><code>\f</code></td>
<td>换页符</td>
<td><code>\x0c</code> 或 <code>\u000c</code></td>
</tr>
</tbody>
</table>
<h3>字符类</h3>
<p>字符类是一组字符的集合，用于匹配某个字符是否属于该字符类中的字符。常见的用法有：</p>
<table>
<thead>
<tr>
<th>用法</th>
<th>含义</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[ ]</code></td>
<td>匹配方括号中的任意字符</td>
<td><code>[abc]</code> 匹配 <code>a</code>，<code>b</code>，<code>c</code>，但不匹配 <code>d</code>，<code>e</code>，<code>f</code></td>
</tr>
<tr>
<td><code>[^ ]</code></td>
<td>匹配除方括号中的任意字符</td>
<td><code>[^abc]</code> 匹配 <code>d</code>，<code>e</code>，<code>f</code> 等，但不匹配 <code>a</code>，<code>b</code>，<code>c</code></td>
</tr>
<tr>
<td><code>.</code></td>
<td>匹配除换行符以外的任意字符</td>
<td><code>a.c</code> 匹配 <code>abc</code>，<code>a c</code>，<code>a1c</code>，<code>a*c</code> 等</td>
</tr>
<tr>
<td><code>\w</code></td>
<td>匹配任意字母、数字或下划线</td>
<td><code>\w</code> 匹配 <code>a</code>，<code>b</code>，<code>c</code>，<code>1</code>，<code>2</code>，<code>3</code>，<code>_</code> 等，但不匹配 <code>.</code>，<code>-</code>，<code>+</code> 等，等价于 <code>[a-zA-Z0-9_]</code></td>
</tr>
<tr>
<td><code>\W</code></td>
<td>匹配任意非字母、数字或下划线</td>
<td><code>\W</code> 匹配 <code>.</code>，<code>-</code>，<code>+</code> 等，但不匹配 <code>a</code>，<code>b</code>，<code>c</code>，<code>1</code>，<code>2</code>，<code>3</code>，<code>_</code> 等，等价于 <code>[^a-zA-Z0-9_]</code></td>
</tr>
<tr>
<td><code>\d</code></td>
<td>匹配任意十进制阿拉伯数字</td>
<td><code>\d</code> 匹配 <code>1</code>，<code>2</code>，<code>3</code>，但不匹配 <code>a</code>，<code>b</code>，<code>c</code>，<code>_</code> 等，等价于 <code>[0-9]</code></td>
</tr>
<tr>
<td><code>\D</code></td>
<td>匹配任意非十进制阿拉伯数字</td>
<td><code>\D</code> 匹配 <code>a</code>，<code>b</code>，<code>c</code>，<code>_</code> 等，但不匹配 <code>1</code>，<code>2</code>，<code>3</code> 等，等价于 <code>[^0-9]</code></td>
</tr>
<tr>
<td><code>\s</code></td>
<td>匹配任意空白符（空格、制表符、换页符等）</td>
<td><code>\s</code> 匹配空格，制表符，换页符等，但不匹配 <code>a</code>，<code>b</code>，<code>c</code>，<code>1</code>，<code>2</code>，<code>3</code>，<code>_</code> 等，等价于 <code>[ \t\n\r\f\v]</code></td>
</tr>
<tr>
<td><code>\S</code></td>
<td>匹配任意非空白符（空格、制表符、换页符等）</td>
<td><code>\S</code> 匹配 <code>a</code>，<code>b</code>，<code>c</code>，<code>1</code>，<code>2</code>，<code>3</code>，<code>_</code> 等，但不匹配空格，制表符，换页符等，等价于 <code>[^ \t\n\r\f\v]</code></td>
</tr>
<tr>
<td><code>\p{}</code></td>
<td>Unicode 类别或命名块中的任意字符</td>
<td>匹配预定义的 <a href="https://learn.microsoft.com/zh-cn/dotnet/standard/base-types/character-classes-in-regular-expressions#supported-unicode-general-categories">Unicode 类别</a>或命名块中的任意字符，如 <code>\p{Lu}</code> 匹配任意大写字母，<code>\p{IsCJKUnifiedIdeographs}</code> 匹配中日韩统一表意文字字符</td>
</tr>
<tr>
<td><code>[base-[excluded]]</code></td>
<td>字符类减法，匹配 <code>base</code> 中的任意字符，但不匹配 <code>excluded</code> 中的任意字符</td>
<td><code>[a-z-[aeiou]]</code> 匹配任意小写字母，但不匹配任意元音字母，等价于 <code>[bcdfghjklmnpqrstvwxyz]</code></td>
</tr>
</tbody>
</table>
<h3>定位点</h3>
<table>
<thead>
<tr>
<th>用法</th>
<th>含义</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>^</code></td>
<td>匹配整个字符串的开始位置</td>
<td><code>^a</code> 匹配 <code>at</code>，但不匹配 <code>cat</code></td>
</tr>
<tr>
<td><code>$</code></td>
<td>匹配整个字符串的结束位置</td>
<td><code>i$</code> 匹配 <code>hi</code>，但不匹配 <code>hit</code></td>
</tr>
<tr>
<td><code>\b</code></td>
<td>匹配单词边界</td>
<td><code>\babc</code> 匹配 <code>abc</code>，<code>abc123</code> 中的 <code>abc</code>，但不匹配 <code>123abc</code>，<code>123abc123</code> 等</td>
</tr>
<tr>
<td><code>\B</code></td>
<td>匹配非单词边界</td>
<td><code>\Babc</code> 匹配 <code>123abc</code>，<code>123abc123</code> 中的 <code>abc</code>，但不匹配 <code>abc</code>，<code>abc123</code> 等</td>
</tr>
</tbody>
</table>
<h3>贪婪与懒惰</h3>
<p>贪婪与懒惰是指正则表达式匹配时的匹配策略，贪婪是指尽可能多的匹配，懒惰是指尽可能少的匹配。 默认情况下，正则表达式是贪婪的，即尽可能多的匹配。如果要使用懒惰匹配，可以在量词后面加上 <code>?</code>。 例如，针对文本 <code>(abc)def(ghi)</code>，<code>\(.*\)</code> 尝试匹配括号及括号内的内容，但是由于是贪婪的，所以会尝试匹配最长的字符串，即 <code>(abc)def(ghi)</code>，而不是 <code>(abc)</code> 或 <code>(ghi)</code>。如果要匹配 <code>(abc)</code> 或 <code>(ghi)</code>，可以使用 <code>\(.*?\)</code>。</p>
<h3>分组捕获</h3>
<p>分组捕获是指将匹配到的字符串分组，以便后续使用。分组捕获的语法是使用 <code>()</code> 包裹要捕获的字符串，例如，针对文本 <code>&lt;p&gt;text&lt;/p&gt;</code>，使用 <code>&lt;p&gt;(.*?)&lt;/p&gt;</code> 可以将匹配结果捕获为一个分组，即 <code>text</code>。</p>
<p>捕获的分组可以进行反向引用，即在后续的正则表达式中使用 <code>\1</code>，<code>\2</code>，<code>\3</code> 等引用之前捕获的分组。例如，针对文本 <code>&lt;p&gt;abc&lt;/p&gt;</code>，使用 <code>&lt;(.*?)&gt;.*?&lt;/\1&gt;</code> 进行匹配，可以保证匹配出来的标签是成对的。</p>
<p>分组可以命名，语法为 <code>(?&lt;name&gt;pattern)</code>，可以使用 <code>\k&lt;name&gt;</code> 进行反向引用。例如，针对文本 <code>&lt;p&gt;abc&lt;/p&gt;</code>，使用 <code>&lt;(?&lt;tag&gt;.*?)&gt;.*?&lt;/\k&lt;tag&gt;&gt;</code> 进行匹配，通过命名分组 <code>tag</code>，可以保证匹配出来的标签是成对的。</p>
<p>分组内的 <code>|</code> 表示或的关系，例如，针对文本 <code>abc</code>，使用 <code>(a|b)c</code> 可以匹配 <code>ac</code> 或 <code>bc</code>。</p>
<h3>断言</h3>
<p>断言是指在匹配时，不消耗字符，只判断是否匹配。 断言分为四种：正向肯定断言、正向否定断言、反向肯定断言、反向否定断言。</p>
<table>
<thead>
<tr>
<th>用法</th>
<th>含义</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>(?=pattern)</code></td>
<td>正向肯定断言，匹配 pattern 之前的位置</td>
<td><code>abc(?=def)</code> 匹配后面是 <code>def</code> 的 <code>abc</code>，匹配结果中不包含 <code>def</code></td>
</tr>
<tr>
<td><code>(?!pattern)</code></td>
<td>正向否定断言，匹配不是 pattern 之前的位置</td>
<td><code>abc(?!def)</code> 匹配后面不是 <code>def</code> 的 <code>abc</code></td>
</tr>
<tr>
<td><code>(?&lt;=pattern)</code></td>
<td>反向肯定断言，匹配 pattern 之后的位置</td>
<td><code>(?&lt;=abc)def</code> 匹配前面是 <code>abc</code> 的 <code>def</code>，匹配结果中不包含 <code>abc</code></td>
</tr>
<tr>
<td><code>(?&lt;!pattern)</code></td>
<td>反向否定断言，匹配不是 pattern 之后的位置</td>
<td><code>(?&lt;!abc)def</code> 匹配前面不是 <code>abc</code> 的 <code>def</code></td>
</tr>
</tbody>
</table>
<p>例如，如要匹配括号及括号内的内容，前文使用的是 <code>\(.*\)</code>，但是这样会匹配到括号。如果要在匹配结果中去掉括号，可以使用 <code>(?&lt;=\().*?(?=\))</code>。</p>
<h3>替换</h3>
<p>替换是使用正则表达式的另一个常见用途，使用普通字符进行替换，则会将匹配的内容替换为指定的字符串。 例如，针对文本 <code>&lt;p&gt;text&lt;/p&gt;</code>，使用 <code>&lt;(?&lt;tag&gt;.*?)&gt;(.*?)&lt;/\k&lt;tag&gt;&gt;</code> 进行匹配，然后使用 <code>result</code> 进行替换，匹配结果为 <code>result</code>。 使用编号组引用语法，则可以对匹配的内容替换为编号捕获组，例如，对上面的文本使用 <code>[$1]</code> 进行替换，匹配结果为 <code>[text]</code>。 使用命名组引用语法，则可以对匹配的内容替换为命名捕获组，例如，对上面的文本使用 <code>[${tag}]</code> 进行替换，匹配结果为 <code>[p]</code>。</p>
<h2>正则表达式选项</h2>
<p>正则表达式选项是指在正则表达式中使用的一些选项，用于控制正则表达式的匹配策略。</p>
<table>
<thead>
<tr>
<th>选项</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>g</code></td>
<td>全局匹配，即匹配所有符合条件的字符串</td>
</tr>
<tr>
<td><code>i</code></td>
<td>忽略大小写</td>
</tr>
<tr>
<td><code>m</code></td>
<td>多行模式，即 <code>^</code> 和 <code>$</code> 匹配每一行的开始和结束位置</td>
</tr>
<tr>
<td><code>s</code></td>
<td>单行模式，即 <code>.</code> 匹配任意字符，包括换行符</td>
</tr>
<tr>
<td><code>x</code></td>
<td>忽略空白字符，即空白字符不参与匹配</td>
</tr>
</tbody>
</table>
<h1>正则表达式在 .NET 中的用法</h1>
<h2>正则表达式选项</h2>
<p>RegexOptions 枚举定义了正则表达式选项，可以按位组合，常用的选项有：</p>
<table>
<thead>
<tr>
<th>选项</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>None</td>
<td>指定不设置任何选项</td>
</tr>
<tr>
<td>IgnoreCase</td>
<td>指定不区分大小写的匹配</td>
</tr>
<tr>
<td>Multiline</td>
<td>多行模式。 更改 ^ 和 $ 的含义，使它们分别在任意一行的行首和行尾匹配，而不仅仅在整个字符串的开头和结尾匹配</td>
</tr>
<tr>
<td>Singleline</td>
<td>指定单行模式。 更改点 (.) 的含义，以使它与每个字符（而不是除 \n 之外的所有字符）匹配。</td>
</tr>
<tr>
<td>IgnorePatternWhitespace</td>
<td>消除模式中的非转义空白并启用由 # 标记的注释</td>
</tr>
<tr>
<td>RightToLeft</td>
<td>指定搜索从右向左而不是从左向右进行</td>
</tr>
<tr>
<td>Compiled</td>
<td>指定正则表达式编译为中间语言代码，而不是解释</td>
</tr>
</tbody>
</table>
<h2>常见方法</h2>
<ul>
<li>IsMatch：指示正则表达式在输入字符串中是否找到匹配项。</li>
<li>Match：在输入字符串中搜索匹配正则表达式模式的子字符串，并将第一个匹配项作为单个 Match 对象返回。</li>
<li>Matches：在输入字符串中搜索正则表达式的所有匹配项并返回所有匹配。</li>
<li>Replace：在指定的输入字符串内，使用指定的替换字符串替换与某个正则表达式模式匹配的字符串。</li>
<li>Split：在由正则表达式匹配定义的位置将输入字符串拆分为一个子字符串数组。</li>
</ul>
<h2>调用方式</h2>
<h3>静态方法</h3>
<pre><code>Regex.IsMatch(Input, Pattern);
Regex.IsMatch(Input, Pattern, RegexOptions.Compiled);
</code></pre>
<h3>实例方法</h3>
<pre><code>var regex = new Regex(Pattern);
var compiledRegex = new Regex(Pattern, RegexOptions.Compiled);
regex.IsMatch(Input);
compiledRegex.IsMatch(Input);
</code></pre>
<h3>源代码生成器</h3>
<p>.NET 7 中新增了正则表达式源代码生成器，可以通过正则表达式生成源代码，然后直接调用源代码中的方法。</p>
<pre><code>[GeneratedRegex(Pattern, RegexOptions.Compiled)]
private static partial Regex MyRegex();

// 在方法中调用
bool isMatch = MyRegex().IsMatch(Input);
</code></pre>
<h2>在 IDE 中的用法</h2>
<p>Visual Studio 和 JetBrains Rider 等主流代码编辑工具或集成开发环境都是支持通过正则表达式进行搜索与替换的。 以替换双斜线代码注释为三斜线 XML 注释为例，可以使用以下的正则表达式进行搜索替换：</p>
<ul>
<li>匹配：<code>(?&lt;!/)//(?!/)\s*(?&lt;comment&gt;.+)</code></li>
<li>替换：<code>/// &lt;summary&gt; ${comment} &lt;/summary&gt;</code></li>
</ul>
<p>要替换为三行展开的 XML 注释，则可以使用以下的正则表达式进行搜索替换：</p>
<ul>
<li>
<p>匹配：<code>(?&lt;space&gt; +)(?&lt;!/)//(?!/)\s*(?&lt;comment&gt;.+)</code></p>
</li>
<li>
<p>替换：</p>
<pre><code>${space}/// &lt;summary&gt;
${space}///     ${comment}
${space}/// &lt;/summary&gt;
</code></pre>
</li>
</ul>
<p>再以为所有的属性添加 <code>JsonPropertyName</code> 特性为例，可以使用以下的正则表达式进行搜索替换：</p>
<ul>
<li>匹配：<code>(?&lt;all&gt;public\s(?!class)\S+\s(?&lt;name&gt;\S+))</code></li>
<li>替换：<code>[JsonPropertyName("${name}")] ${all}</code></li>
</ul>
<p><a href="https://regex101.com/r/VG3sYF/1">示例用文本 1</a></p>
<p><a href="https://regex101.com/r/Hu9kmq/1">示例用文本 2</a></p>
<h1>实用工具</h1>
<ul>
<li>
<p>正则表达式在线测试工具</p>
<ul>
<li><a href="https://regex101.com/">Regex101</a></li>
<li><a href="https://regexr.com/">Regexr</a></li>
</ul>
</li>
<li>
<p>正则表达式可视化工具</p>
<ul>
<li><a href="https://regexper.com/">Regexper</a></li>
</ul>
</li>
</ul>
<h1>参考文献</h1>
<ul>
<li><a href="https://learn.microsoft.com/zh-cn/dotnet/standard/base-types/regular-expressions">.NET 正则表达式 | Microsoft Learn</a></li>
<li><a href="https://en.wikipedia.org/wiki/Regular_expression">Regular expression - Wikipedia</a></li>
</ul>
]]></content>
    <author><name>戈小荷</name></author>
    <category term=".NET"/>
  </entry>
  <entry>
    <title>如何在C#中设计并实现流式接口模式</title>
    <link href="https://gehongyan.cn/dotnet/fluent-interface-in-csharp/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/dotnet/fluent-interface-in-csharp/</id>
    <published>2021-07-15T00:00:00.000Z</published>
    <updated>2021-07-15T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<blockquote>
<p>本文翻译自 <a href="https://assist-software.net/blog/how-design-and-implement-fluent-interface-pattern-c">How to Design and Implement the Fluent Interface Pattern in C# | ASSIST Software Romania</a></p>
</blockquote>
<p><img src="https://media.giphy.com/media/uWX4Ae8jvrLKVwKTPj/giphy.gif" alt="" /></p>
<p>什么是<a href="https://zh.wikipedia.org/wiki/%E6%B5%81%E5%BC%8F%E6%8E%A5%E5%8F%A3">流式接口</a> (<a href="https://en.wikipedia.org/wiki/Fluent_interface">Fluent interface</a>)？</p>
<blockquote>
<p>流式接口（Fluent interface）是软件工程中一种面向对象 API 的实现方式，其面向广泛依赖于方法链设计，目的在于通过创建<a href="https://zh.wikipedia.org/wiki/%E9%A2%86%E5%9F%9F%E7%89%B9%E5%AE%9A%E8%AF%AD%E8%A8%80">领域特定语言</a> (<a href="https://en.wikipedia.org/wiki/Domain-specific_language">Domain-specific language, DSL</a>) 来提高代码可读性。</p>
</blockquote>
<h1>前言</h1>
<p>本文包含了多种变成概念，包括但不限于：继承、多态、方法链、<a href="https://zh.wikipedia.org/wiki/%E9%A2%86%E5%9F%9F%E7%89%B9%E5%AE%9A%E8%AF%AD%E8%A8%80">领域特定语言</a>、流式接口等。你可能不熟悉流式接口 API 这个术语，但你非常有可能在以前就已经使用过，因为它在许多面向对象的编程语言的很多框架与库中都有被一直使用过。</p>
<h2>流式接口与扩展方法</h2>
<p>流式接口与扩展方法都不是很简单，或者说是有时候甚至不太可能调试的模式，因为他们的中间结果在调试中都不可达。两者的语法很相似，但还是让我们看看它们之间的区别在哪里：</p>
<p><strong>扩展方法</strong></p>
<ul>
<li>返回类型是不变的，其总会返回一个具有相同类型的对象</li>
<li>循环无法被阻止，开发者可以连接调用相同的方法，每次调用时方法都会被执行</li>
<li>语句会被一行一行执行，其结果取决于之前的操作</li>
</ul>
<p><strong>流式接口</strong></p>
<ul>
<li>每个方法的返回类型可以不同，这保证了灵活性与可扩展性</li>
<li>循环可以被限制，方法可以按照预先决定好的顺序连接</li>
<li>语句会在最后被执行，方法链用来收集数据与建立路径</li>
</ul>
<h2>主题</h2>
<p>假如要实现一个类包含以下功能： * 下载到文件/流与从文件/流上传 * 本机计算机与 Azure Blob 存储之间的文件传输 * 异步传输，这可能不着急，但稍后可能会需要</p>
<p>按标准方式，实现一下接口将满足需求：</p>
<pre><code>interface IBlobTransfer
{
    void BlobDownload(string connectionString, string blobName, string fileName, string filePath);
    void BlobDownload(string connectionString, string blobName, string fileName, Stream stream);
    void BlobUpload(string connectionString, string blobName, string fileName, string filePath);
    void BlobUpload(string connectionString, string blobName, string fileName, Stream stream);
}
</code></pre>
<p>尽管这些代码可以满足需求，但让我们以一种更好的方式来实现他们。</p>
<h1>设计</h1>
<h2>定义自然语言句法</h2>
<p>首先，有必要定义一个包含所有合适且允许的方法组合列表，将此组合列表应用于上述主题将会生成以下结果：</p>
<pre><code>FluentBlobTransfer.Connect(..).OnBlob(..).Download(..).ToFile(..);
FluentBlobTransfer.Connect(..).OnBlob(..).Download(..).ToStream(..);
FluentBlobTransfer.Connect(..).OnBlob(..).Upload(..).FromFile(..);
FluentBlobTransfer.Connect(..).OnBlob(..).Upload(..).FromStream(..);
</code></pre>
<h2>约束</h2>
<p>可以发现<code>Download.FromFile</code>与<code>Upload.ToStream</code>是不合法的组合。为了阻止这样的行为，必须满足以下条件：</p>
<ul>
<li><code>Download</code> 方法应该返回一个带有以下方法签名的接口：</li>
</ul>
<pre><code>void ToFile(..);
void ToStream(..);
</code></pre>
<ul>
<li><code>Upload</code>方法应该返回一个带有以下方法签名的接口：</li>
</ul>
<pre><code>void FromFile(..);
void FromStream(..)
</code></pre>
<h2>类图</h2>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/fluent-csharp/class-diagram.avif" alt="Class Diagram" /></p>
<h2>定义接口</h2>
<p>这是方法签名所在的位置，遵循这些简单的规则来保证成功：</p>
<ul>
<li>第一个和最后一个方法可以是无参的</li>
<li>每个方法最多接受一个参数</li>
<li>使用描述性词汇，如最多五个单词组成的短语</li>
<li>覆盖所有情况，正确分割路径</li>
</ul>
<pre><code>interface IAzureBlob
{
    IAzureAction OnBlob(string blobBlockPath);
}

interface IAzureAction
{
    IAzureWrite Download(string fileName);
    IAzureRead Upload(string fileName);
}

interface IAzureWrite
{
    void ToFile(string filePath);
    void ToStream(Stream stream);
}

interface IAzureRead
{
    void FromFile(string filePath);
    void FromStream(Stream stream);
}
</code></pre>
<h1>实现</h1>
<h2>要点</h2>
<ul>
<li>构造函数必须是私有的，不允许类的实例化</li>
<li>不允许继承，使用**<code>sealed</code>**密闭修饰符</li>
<li>为方法和变量使用释义正确的名称，不使用含糊歧义的名称</li>
<li>每个方法最多接受一个参数，别忘了我们要以_流畅的方式_实现！</li>
</ul>
<h2>入口函数</h2>
<p>入口函数需标记为<code>static</code>静态，建议名称使用动词来表达意图。</p>
<h2>实现接口与保证合适的方法链</h2>
<p>方法链中的每一步都应该用来收集数据后指向下一个方法。通常，方法链的最后一个方法返回**<code>void</code>**未知类型指针，来表明方法链链尾。</p>
<p>请注意，<code>FluentBlobTransfer</code>类需要实现所有先前准备好的接口。</p>
<pre><code>public sealed class FluentBlobTransfer : IAzureBlob, IAzureAction, IAzureWrite, IAzureRead
{
    private readonly string connectionString;
    private string blobBlockPath;
    private string fileName;

    private FluentBlobTransfer(string connectionString) =&gt; this.connectionString = connectionString;

    public static IAzureBlob Connect(string connectionString) =&gt; new FluentBlobTransfer(connectionString);

    public IAzureAction OnBlob(string blobBlockPath)
    {
        this.blobBlockPath = blobBlockPath;

        return this;
    }

    public IAzureWrite Download(string fileName)
    {
        this.fileName = fileName;

        return this;
    }

    public IAzureRead Upload(string fileName)
    {
        this.fileName = fileName;

        return this;
    }

    public void ToFile(string filePath)
    {
        // Code to download from Azure Blob Storage to file
    }

    public void ToStream(Stream stream)
    {
        // Code to download from Azure Blob Storage to stream
    }

    public void FromFile(string filePath)
    {
        // Code to upload from file to Azure Blob Storage
    }

    public void FromStream(Stream stream)
    {
        // Code to upload from stream to Azure Blob Storage
    }
}
</code></pre>
<p>请注意，传输过程的具体实现不在本文的讨论范围内。</p>
<p>现在你可以流畅地调用方法传输文件：</p>
<pre><code>FluentBlobTransfer
    .Connect("storageAccountConnectionString")
    .OnBlob("blobName")
    .Download("fileName")
    .ToFile(@"D:\Azure\Downloads\");
</code></pre>
<h1>探索</h1>
<p>我最喜欢的流式接口的例子是**<code>Microsoft.Azure.*.Fluent</code>**库，这些库的基础指出在于它们通过不言自明的流式接口取代了过往需要记忆命令和参数的需要（不像 PowerShell 脚本）。</p>
<p>下一个代码片段在一个新的资源组中创建了一个新的 SQL Server 数据库，并包含了两个数据库及一些防火墙规则。</p>
<pre><code>await Azure
    .Configure()
    .WithLogLevel(Level.Basic)
    .Authenticate(credentials)
    .WithSubscription("subscriptionId")
    .SqlServers
    .Define("sqlServerName")
    .WithRegion(Region.EuropeWest)
    .WithNewResourceGroup()
    .WithAdministratorLogin("sqlAdmin")
    .WithAdministratorPassword("pass")
    .WithNewDatabase("databaseOne")
    .WithNewDatabase("databaseTwo")
    .WithNewFirewallRule("0.0.0.0")
    .WithNewFirewallRule("1.1.1.1")
    .WithNewFirewallRule("2.2.2.2")
    .CreateAsync();
</code></pre>
<p>EntityFramework 的<a href="https://www.learnentityframeworkcore.com/configuration/fluent-api">流式接口配置</a>提供了一个使用优秀句法配置数据库表与字段的方式。</p>
<pre><code>modelBuilder
    .Entity&lt;Foo&gt;()
    .HasMany(foo =&gt; foo.Bars)
    .WithOne(bar =&gt; bar.Foo)
    .HasForeignKey(bar =&gt; bar.FooId)
    .OnDelete(DeleteBehavior.Cascade);

modelBuilder
    .Entity&lt;Foo&gt;()
    .Property(foo =&gt; foo.Value)
    .HasDefaultValue("0123456789")
    .HasMaxLength(10)
    .IsFixedLength()
    .IsRequired();
</code></pre>
<p>在本文原作者的 <a href="https://github.com/dimitrietataru/fluentinterface">GitHub 账户</a>中可以浏览其他<strong>流式接口样例</strong>、探索流式方法链、访问服务本文的源代码。</p>
<h1>结论</h1>
<p>设计与实现一个易于解释与使用的流式接口会花费很多时间，但这样的努力非常值得。你可能会觉得它让你无法抗拒，但也要记住不是所有的都有必要以流式实现。</p>
<p>为可重用代码提供<strong>流式接口</strong>很方便，开发者可以不用担心“它是怎么工作的？”“我需要多少参数？”“多有少个选项？”就可以直接调用。</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term=".NET"/>
  </entry>
  <entry>
    <title>【字幕组】曲目表列表</title>
    <link href="https://gehongyan.cn/subtitle/subtitle-group-tracklist/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/subtitle/subtitle-group-tracklist/</id>
    <published>2021-04-29T00:00:00.000Z</published>
    <updated>2021-04-29T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<ul>
<li>♪ ANIMA -『ソードアート・オンライン アリシゼーション War of Underworld/刀剑神域 Aliciazation 异界战争』终章OP - 曲：毛蟹(LIVE LAB.) 歌：ReoNa</li>
<li>♪ Astronomia - Vicetone &amp; Tony Igy</li>
<li>♪ Bad Apple!! feat. nomico - Alstroemeria Records</li>
<li>♪ Cat's dance - まらしぃ feat. 初音ミク/初音未来</li>
<li>♪ DAYBREAK FRONTLINE / 黎明前线 - Orangestar feat. IA</li>
<li>♪ from Y to Y - ジミーサムP(OneRoom) feat. 初音ミク/初音未来</li>
<li>♪ God knows... - TV动画『涼宮ハルヒの憂鬱/凉宫春日的忧郁』12话插曲 - 曲：神前暁/神前晓 歌：平野绫</li>
<li>♪ Happy Birthday/生日快乐歌 - 11月26日</li>
<li>♪ Help me, ERINNNNNN!! - COOL&amp;CREATE</li>
<li>♪ KARIN - パズドラ/智龙迷城 - まらしぃ</li>
<li>♪ KING - Kanaria feat. GUMI</li>
<li>♪ Lemon - TBS 日剧『アンナチュラル/非自然死亡』主题曲 - 米津玄師 / 米津玄师</li>
<li>♪ LOSER -「Honda Jade/本田杰德」电视广告- 米津玄師</li>
<li>♪ LovePiano - YAMAHA主办『LovePiano presents みんなのピアノ発表会/大家的钢琴发表会』印象曲 - まらしぃ［💗🎹］</li>
<li>♪ MEGALOVANIA -『Undertale/传说之下』Sans战斗曲 - Toby Fox</li>
<li>♪ Messire - まらフェス2018印象曲 - まらしぃ</li>
<li>♪ ODDS&amp;ENDS/梦想碎片 - ryo(supercell) feat. 初音ミク/初音未来</li>
<li>♪ One Last Kiss -『シン・エヴァンゲリオン劇場版𝄇/新·福音战士剧场版𝄇』主題歌 - 宇多田ヒカル/宇多田光</li>
<li>♪ only my railgun - 电视动画『とある科学の超電磁砲/某科学的超电磁炮』前期OP - fripSide</li>
<li>♪ Sogna - まらしぃ</li>
<li>♪ stella=steLLa - SEGA音游『CHUNITHM』收录曲目 - まらしぃ</li>
<li>♪ tadu - 电视广告『FUNAI 4K有機ELテレビ/船井电机4K有机EL电视』用曲 - まらしぃ</li>
<li>♪ This game -『ノーゲーム・ノーライフ/NO GAME NO LIFE 游戏人生』OP - 曲：若林充 歌：鈴木このみ/铃木木乃美</li>
<li>♪ U.N.オーエンは彼女なのか？/U.N.OWEN就是她吗？ -『東方紅魔郷/东方红魔乡』Extra面Boss：フランドール・スカーレット/芙兰朵露·斯卡蕾特 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ unravel -『東京喰種トーキョーグール/东京食尸鬼』1期OP - 曲：TK(北岛彻) 歌：TK from 凛として時雨/Tk from 凛冽时雨</li>
<li>♪ アイネクライネ/Eine Kleine - 东京地下铁广告活动「Color your days.」广告用曲 - 米津玄師/米津玄师</li>
<li>♪ アカシア/相思树 -『Pokémon Special Music Video/宝可梦系列宣传影片「GOTCHA！」』主题曲 - BUMP OF CHICKEN</li>
<li>♪ アマツキツネ/天狐 - 初投稿四周年纪念作 - まらしぃ feat. 鏡音リンAppend/镜音铃Append</li>
<li>♪ アンノウン・マザーグース/Unknown Mother-Goose - wowaka(現実逃避P) feat.初音ミク/初音未来</li>
<li>♪ インフェルノ/Inferno - 电视动画『炎炎ノ消防隊/炎炎消防队』1期1-14话OP - Mrs. GREEN APPLE</li>
<li>♪ うまぴょい伝説/马儿蹦跳传说 -『ウマ娘 プリティーダービー/赛马娘 Pretty Derby』13话ED - 曲：Cygames(本田晃弘)</li>
<li>♪ おてんば恋娘/活泼的纯情小姑娘 -『東方紅魔郷/东方红魔乡』2面Boss：チルノ/琪露诺 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ グッバイ宣言/Goodbye宣言 - Chinozo feat. v flower</li>
<li>♪ コネクト/Connect - 电视动画『魔法少女まどか☆マギカ/魔法少女小圆』OP - ClariS</li>
<li>♪ ザナルカンドにて/To Zanarkand -『FINAL FANTASY X/最终幻想X』OP - 植松伸夫</li>
<li>♪ シノノメ/东云 - まらしぃ feat. 初音ミク/初音未来</li>
<li>♪ シャルル/Charles - バルーン(须田景凪) feat. v flower</li>
<li>♪ テオ/将手 - Omoi feat. 初音ミク/初音未来</li>
<li>♪ とある霖雨 - marasy×粗品 feat. 初音ミク/初音未来</li>
<li>♪ ドラマツルギー/拟剧论 - Eve</li>
<li>♪ ナイト・オブ・ナイツ/Night of Knights - COOL&amp;CREATE by ビートまりお</li>
<li>♪ ネイティブフェイス/Native Faith -『東方風神録/东方风神录』Extra面Boss：洩矢諏訪子/洩矢诹访子 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ ハルトマンの妖怪少女/哈德曼的妖怪少女 -『東方地霊殿/东方地灵殿』Extra面BOSS：古明地こいし/古明地恋 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ ピースサイン/Peace Sign - 电视动画『僕のヒーローアカデミア/我的英雄学院』2期OP - 米津玄師/米津玄师</li>
<li>♪ ヒバナ/火花 - DECO*27 feat. 初音ミク/初音未来</li>
<li>♪ ビバハピ/Viva Happy - Mitchie M feat. 初音ミク/初音未来 (MORE MORE JUMP!)</li>
<li>♪ ピュアヒューリーズ ～ 心の在処/Pure Furies ～ 心之所在 -『東方紺珠伝/东方绀珠传』6面BOSS：純狐/纯狐 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ フラワリングナイト/Flowering Night -『東方花映塚/东方花映塚』十六夜咲夜 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ ブリキノダンス/马口铁之舞 - 日向電工/日向电工 feat. 初音ミク/初音未来</li>
<li>♪ ベノム/猛毒 - かいりきベア/怪力熊 feat.flower</li>
<li>♪ メルト/melt - ryo(supercell) feat. 初音ミク/初音未来</li>
<li>♪ ようこそジャパリパークへ/欢迎来到加帕里公园 - けものフレンズ/兽娘动物园 OP - どうぶつビスケッツ/曲：大石昌良 歌：动物饼干 × PPP</li>
<li>♪ ラーメン†デュエル/拉面†决斗 - Logical Emotion/まらしぃ</li>
<li>♪ ラストリモート/Last Remote -『東方地霊殿/东方地灵殿』Extra面道中曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ ロキ/ROKI - みきとP/MikitoP feat. 镜音リン</li>
<li>♪ ワールズエンド・ダンスホール/世末舞厅 - wowaka feat. 初音ミク/初音未来 &amp; 巡音ルカ/巡音流歌</li>
<li>♪ 残酷な天使のテーゼ/残酷天使的行动纲领 - 动画『新世紀エヴァンゲリオン/新世纪福音战士』OP - 高橋洋子/高桥洋子</li>
<li>♪ 打打打打打打打打打打 - ヒゲドライバー/Hige Driver</li>
<li>♪ 地球最後の告白を/地球最后的告白 - kemu feat. GUMI</li>
<li>♪ 東方妖怪小町/东方妖怪小町 -『東方永夜抄/东方永夜抄』ラストワード/Last Word 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ 対決のテーマ/对决主题 - 名探偵コナン/名侦探柯南 - 大野克夫</li>
<li>♪ 風神少女/风神少女 -『東方文花帖/东方文花帖』審判「浄頗梨審判 ‐射命丸文‐」/审判「净颇梨审判 -射命丸文-」摄影用曲5 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ 感電/感电 -『MIU404』主題歌 - 米津玄師/米津玄师</li>
<li>♪ 怪物 -『BEASTARS/动物狂想曲』第2期OP - YOASOBI</li>
<li>♪ 紅蓮華/红莲华 -动画『鬼滅の刃/鬼灭之刃』OP - 曲：草野華余子 歌：LiSA</li>
<li>♪ 輝く針の小人族 ～ Little Princess/辉光之针的小人族 ～ Little Princess -『東方輝針城/东方辉针城』6面BOSS：少名針妙丸/少名针妙丸 主题其 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ 回レ！雪月花/旋转吧！雪月花 -『機巧少女は傷つかない/机巧少女不会受伤』ED - 歌組雪月花(茅野愛衣/原田ひとみ/小倉唯)/歌组雪月花(茅野爱衣/原田瞳/小仓唯) - ヒゲドライバー/Hige Driver</li>
<li>♪ 魂のルフラン/魂之轮回 - 剧场版动画『新世紀エヴァンゲリオンシト新生/新世纪福音战士剧场版 死与新生』主题曲 - 高橋洋子/高桥洋子</li>
<li>♪ 君の知らない物語/你不知道的故事 -『化物語/化物语』ED / 曲：ryo 歌：supercell</li>
<li>♪ 霖と五線譜 - まらしぃ feat. 初音ミク/初音未来</li>
<li>♪ 六兆年と一夜物語/六兆年零一夜的故事 - kemu feat. IA</li>
<li>♪ 炉心融解 / iroha feat. 鏡音リン/镜音铃</li>
<li>♪ 夢、時々.../梦、时时... - まらしぃ feat. 初音ミクAppend/初音未来Append</li>
<li>♪ 偶像に世界を委ねて ～ Idoratrize World/寄世界于偶像 ～ Idoratrize World -『東方鬼形獣/东方鬼形兽』6面BOSS：埴安神袿姫/埴安神袿姬 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ 僕のために泣いてくれ - まらしぃ feat. 初音ミク/初音未来</li>
<li>♪ 千本桜/千本樱 - 黒うさP feat. 初音ミク/初音未来</li>
<li>♪ 千本桜/千本樱 - 黒うさP/黑兔P feat. 初音ミク/初音未来</li>
<li>♪ 青く駆けろ！/奔跑吧！蓝色！- 宝矿力水特网络视频『バーチャル東京サプライ少女2020 青く駆けろ！』篇 - まらしぃ feat. 初音ミク/初音未来</li>
<li>♪ 青空のラプソディ/青空狂想曲 - 电视动画『小林さんちのメイドラゴン/小林家的龙女仆』OP - fhána</li>
<li>♪ 人生のメリーゴーランド/人生的旋转木马 - 动画电影『ハウルの動く城/哈尔的移动城堡』OP - 久石譲/久石让</li>
<li>♪ 砂の惑星/砂之行星 - ハチ/米津玄师 feat. 初音ミク/初音未来</li>
<li>♪ 上海紅茶館 ～ Chinese Tea/上海红茶馆 ～ Chinese Tea -『東方紅魔郷/东方红魔乡』3面道中 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ 少女綺想曲 ～ Capriccio/少女绮想曲 ～ Capriccio -『東方永夜抄/东方永夜抄』4面BOSS：博麗靈夢/博丽灵梦 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ 十年越しのラストピース/跨越十年最后的碎片 - 触手猴与kemu十年友谊纪念曲 - まらしぃ x kemu feat. 初音ミク/初音未来</li>
<li>♪ 太陽曰く燃えよカオス/太阳说燃烧吧混沌 -『這いよれ！ニャル子さん/潜行吧!奈亚子』1期&amp;OVA OP - 後ろから這いよれ隊G(ニャルラトホテプ/クトゥグア/暮井珠緒)/从后匍匐而来队G(奈亚子/克子/暮井珠绪)</li>
<li>♪ 天ノ弱/天之弱 - 164 feat. GUMI</li>
<li>♪ 崖っぷちのオーマイガール - まらしぃ feat. 初音ミク/初音未来</li>
<li>♪ 炎 - 剧场版动画『「鬼滅の刃」無限列車編/鬼灭之刃 无限列车篇』主题曲 - 曲：梶浦由记 歌：LiSA</li>
<li>♪ 夜に駆ける/向夜晚飞奔 - 小说『タナトスの誘惑/塔纳托斯的诱惑』及『夜に溶ける/溶于夜中』印象曲 - YOASOBI</li>
<li>♪ 幽雅に咲かせ、墨染の桜　～ Border of Life/幽雅地绽放吧，墨染的樱花　～ Border of Life -『東方妖々夢/东方妖妖梦』6面BOSS：西行寺幽々子/西行寺幽幽子 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
<li>♪ 月まで届け、不死の煙/飘上月球，不死之烟 -『東方永夜抄/东方永夜抄』Extra面BOSS：藤原妹紅/藤原妹红 主题曲</li>
<li>♪ 竹取飛翔 ～ Lunatic Princess/竹取飞翔 ～ Lunatic Princess -『東方永夜抄/东方永夜抄』6面BOSS：蓬莱山輝夜/蓬莱山辉夜 主题曲 - 上海アリス幻樂団/上海爱丽丝幻乐团/ZUN</li>
</ul>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="字幕组"/>
  </entry>
  <entry>
    <title>【ASP.NET】ASP.NET MVC 5 入门概述</title>
    <link href="https://gehongyan.cn/dotnet/get-started-with-asp-net-core-mvc/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/dotnet/get-started-with-asp-net-core-mvc/</id>
    <published>2020-10-14T00:00:00.000Z</published>
    <updated>2020-10-14T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h2>分离关注点的通用设计原则</h2>
<p>分离关注点是开发时的指导原则<a href="#refer-anchor-1">1</a>。此原则主张应根据软件执行的工作类型将软件分离。例如，假设应用程序中包含两个逻辑，其中一个逻辑标识要显示给用户的注意事项，另一个以特定方式设置这些注意事项的格式，使其更加显眼。负责选择为哪些事项设置格式的行为应与负责设置格式的行为区分开，因为这两种行为只是碰巧彼此相关联的独立关注点。</p>
<p>从体系结构上来说，按此原则有逻辑地构建应用程序应将核心业务行为与基础结构及用户界面逻辑区分开。理想情况下，业务规则和逻辑应单独位于一个项目中，且该项目不依赖于应用程序中的其他项目。此区分操作可帮助确保该业务模型易于测试，且可在不与低级别实现详细信息紧密耦合的情况下逐步改进。在应用程序体系结构的使用层背后，关注点分离是核心设计思想。</p>
<hr />
<h2>ASP.NET MVC 概述</h2>
<p>ASP.NET MVC 是使用 MVC 设计模式构建 Web 应用和 API 的框架<a href="#refer-anchor-2">2</a>。其中：</p>
<ul>
<li><strong>M</strong>odel：模型</li>
<li><strong>V</strong>iew：视图</li>
<li><strong>C</strong>ontroller：控制器</li>
</ul>
<h3>MVC 模式</h3>
<p>模型-视图-控制器 (MVC) 设计模式将应用程序分成 3 个主要组成部分：模型、视图和控制器。此模式有助于实现<a href="#refer-anchor-1">关注点分离</a>。使用此模式，用户请求被路由至控制器，控制器负责使用模型来执行用户操作且/或检索查询结果，控制器选择要显示给用户的视图，并为其提供所需的模型数据。</p>
<p>下图显示 3 个主要组件及其相互引用关系：</p>
<p><img src="https://docs.microsoft.com/zh-cn/aspnet/core/mvc/overview/_static/mvc.png?view=aspnetcore-3.1" alt="MVC 模式" /></p>
<h3>模型责任</h3>
<p>模型 (<strong>M</strong>odel) 表示应用程序和任何应由其执行的业务逻辑或操作的状态。业务逻辑应与保持应用程序状态的任何实现逻辑共同封装在模型中。强类型视图通常使用 ViewModel 类型，旨在包含要在该视图上显示的数据。控制器从模型创建并填充 ViewModel 实例。</p>
<h3>视图责任</h3>
<p>视图 (<strong>V</strong>iew) 负责通过用户界面展示内容。它们使用 Razor 视图引擎在 HTML 标记中嵌入 .NET 代码。视图页面中应尽可能少得包含逻辑处理，且其中的任何逻辑都必须与展示内容相关。如果发现需要在视图文件中执行大量逻辑以显示复杂模型中的数据，请考虑使用 View Component、ViewModel 或视图模板来简化视图。</p>
<h3>控制器责任</h3>
<p>控制器 (<strong>C</strong>ontroller) 是处理用户交互、使用模型并最终选择呈现视图的组件。在 MVC 应用程序中，视图仅显示信息；控制器处理并响应用户输入和交互。在 MVC 模式中，控制器是初始入口点，负责选择要使用的模型类型和要呈现的视图，控制应用如何响应给定请求，并因此得名。</p>
<blockquote>
<p>控制器不应由于责任过多而变得过于复杂。要阻止控制器逻辑变得过于复杂，请将业务逻辑推出控制器并推入<a href="#refer-anchor-3">域模型</a>。</p>
</blockquote>
<hr />
<h2>创建 ASP.NET MVC Web 应用程序（.NET Framework）</h2>
<p>有关如何创建并运行 ASP.NET MVC Web 应用程序的指南可参考：</p>
<blockquote>
<p><a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/getting-started/introduction/getting-started#create-your-first-app">入门与 ASP.NET MVC 5 | Microsoft Docs</a></p>
</blockquote>
<hr />
<h2>ASP.NET MVC Web 应用程序的目录结构</h2>
<p>Visual Studio 会在创建 ASP.NET MVC Web 应用程序时自动添加一些文件与目录结构，但这并不是固定的目录结构。事实上，那些处理大型应用程序的开发人员通常跨多个项目来分割应用程序，以便使应用程序更易于管理，默认的项目结构哦确实提供了一个很好的默认目录约定，是的应用程序的关注点更加清晰。</p>
<p>本站点介绍涉及 ASP.NET Code MVC Web 应用程序会采用在默认目录结构基础上再添加一些其它实用文件夹的以下目录结构：</p>
<pre><code>{解决方案名称}
└── {项目名称}                     # 命名空间
    ├── App_Code/                  # 公共业务类
    ├── App_Data/                  # 本地数据文件
    ├── App_Start                  # 部分配置代码，如：路由、捆绑、Web API
    │   ├── BundleConfig.cs        # 打包器配置
    │   ├── FilterConfig.cs        # 过滤器配置
    │   ├── RouteConfig.cs         # 路由配置
    │   └── WebApiConfig.cs        # Web API 配置
    ├── Areas                      # 区域
    │   └── {区域名称}             # 定义区域
    │       ├── Controllers/       # 区域控制器
    │       ├── Data/              # 区域本地数据
    │       ├── Scripts/           # 区域脚本
    │       ├── Views/             # 区域视图
    │       └── {区域名称}AreaRegistration.cs  # 区域注册配置
    ├── Asserts/                   # 自定义资源，如本地图片等非代码内容
    ├── Content/                   # 用于存储 CSS 样式等非脚本内容
    ├── Controllers                # 控制器
    │   └── {名称}Controllers.cs   # 控制器名称
    ├── fonts/                     # 自定义字体，主要用于 Bootstrap 模板系统引用
    ├── Migrations/                # Entity Framework Code First 所生成与使用的迁移代码
    ├── Models/                    # 模型
    ├── Scripts/                   # Javascript 库与脚本
    ├── Views                      # 视图
    │   ├── {名称}                 # 视图子文件夹
    │   │   └── {视图名称}         # 单视图
    │   ├── Shared                 # 共享视图文件夹
    │   │   ├── _Layout.cshtml     # 共享母版视图
    │   │   └── Error.cshtml       # 共享错误页视图
    │   └── _ViewStart.cshtml      # 呈现视图前运行的代码
    ├── favicon.ico                # 站点图标
    ├── Global.asax                # ASP.NET 应用程序的入口点 [4]
    ├── packages.config            # NuGet 包引用列表
    ├── Startup.cs                 # ASP.NET Core 应用程序的入口点，由 OWIN 提供 [4]
    └── Web.config                 # ASP.NET Web 应用程序配置信息
</code></pre>
<hr />
<h2>关键文件解析</h2>
<h3>~/Global.asax.cs</h3>
<p><code>Global.asax.cs</code>文件是一个特殊文件，其中包含 ASP.NET 应用程序生命周期事件的事件处理程序<a href="#refer-anchor-5">5</a>。默认地，<code>Global.asax.cs</code>包含了<code>Application_Start()</code>方法，该方法在 MVC 应用程序启动时会被调用。<code>Global.asax.cs</code>默认代码如下：</p>
<pre><code>using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace {命名空间}
{
    public class MvcApplication : HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();                        // 注册所有区域
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);  // 注册所有全局过滤器
            RouteConfig.RegisterRoutes(RouteTable.Routes);              // 注册所有传统路由信息
            BundleConfig.RegisterBundles(BundleTable.Bundles);          // 注册所有捆绑器配置
        }
    }
}

</code></pre>
<h3>~/App_Start/RouteConfig.cs</h3>
<h4>路由概述</h4>
<p><code>RouteConfig.cs</code>文件配置了 ASP.NET 应用程序的传统路由信息<a href="#refer-anchor-5">5</a>。</p>
<p>ASP.NET MVC 框架中的路由主要有两种用途：</p>
<ul>
<li>匹配传入的请求（该请求不匹配服务器文件系统中的文件），并把这些请求映射到控制器操作</li>
<li>构造传出的 URL，用来响应控制器操作</li>
</ul>
<h4>代码功能分析</h4>
<p>以下是此文件默认代码：</p>
<pre><code>using System.Web.Mvc;
using System.Web.Routing;

namespace {命名空间}
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            // {resource}.axd 表示扩展名为 .axd 所有资源，如 WebResource.axd
            // {*pathInfo} 表示匹配任何路径
            // IgnoreRoute("{resource}.axd/{*pathInfo}") 方法通知路由系统忽略处理 ASP.NET 的 Web 资源文件
            // 如：WebResource.axd 或 ScriptResource.axd
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",                        // 路由名称
                url: "{controller}/{action}/{id}",      // 路由模板
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }     // 默认路由参数值
            );
        }
    }
}

</code></pre>
<p>这样的默认配置代码制定了一个名为 <code>Default</code> 的路由模板，它接受形如<code>URL/{controller}/{action}/{id}</code>的请求，其中：</p>
<ul>
<li><code>{controller}</code>为控制器名称，未提供时的默认值为<code>Home</code></li>
<li><code>{action}</code>为动作名称，未提供时的默认值未<code>Index</code></li>
<li><code>{id}</code>为一URL参数，<code>UrlParameter.Optional</code>表示包含可选参数的只读值</li>
</ul>
<p>例如：</p>
<ul>
<li>当 URL 请求为<code>URL</code>本身时，路由会将请求传递至 <code>HomeController.Index()</code> 方法</li>
<li>当 URL 请求为<code>URL/Contact/Find/35</code>时，路由会将请求传递至 <code>ContactController.Find(35)</code> 方法</li>
</ul>
<h4>更多</h4>
<p>有关 ASP.NET MVC 路由的更多帮助可参考：</p>
<ul>
<li><a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs">ASP.NET MVC 路由概述（C#） | Microsoft Docs</a></li>
<li><a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/older-versions-1/controllers-and-routing/creating-custom-routes-cs">创建自定义路由 （C#） | Microsoft Docs</a></li>
<li><a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/older-versions-1/controllers-and-routing/creating-a-route-constraint-cs">创建路由约束（C#） | Microsoft Docs</a></li>
<li><a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/older-versions-1/controllers-and-routing/creating-a-custom-route-constraint-cs">创建自定义路由约束（C#） | Microsoft Docs</a></li>
</ul>
<h3>~/App_Start/BundleConfig.cs</h3>
<h4>捆绑与缩减概述</h4>
<p>捆绑和缩减是可以在 ASP.NET 4.5 中使用的两种技术来提高请求加载时间。捆绑和缩减减少了对服务器的请求数并减小了请求资产（如 CSS 和 JavaScript）的大小，从而缩短了加载时间<a href="#refer-anchor-6">6</a>。</p>
<p>捆绑是 ASP.NET 4.5 中引入的一项新功能，可轻松地将多个文件组合或捆绑到一个文件中。捆绑器可以创建 CSS、JavaScript 和其他捆绑包。较少的文件意味着 HTTP 请求更少，从而可以提高首页加载性能。</p>
<p>缩减对 JavaScript 或 CSS 执行各种不同的代码优化，如删除不必要的空白和注释并缩短变量名称为一个字符。例如考虑下面的 JavaScript 函数</p>
<pre><code>AddAltToImg = function (imageTagAndImageID, imageContext) {
    ///&lt;signature&gt;
    ///&lt;summary&gt; Adds an alt tab to the image
    // &lt;/summary&gt;
    //&lt;param name="imgElement" type="String"&gt;The image selector.&lt;/param&gt;
    //&lt;param name="ContextForImage" type="String"&gt;The image context.&lt;/param&gt;
    ///&lt;/signature&gt;
    var imageElement = $(imageTagAndImageID, imageContext);
    imageElement.attr('alt', imageElement.attr('id').replace(/ID/, ''));
}
</code></pre>
<p>缩减之后，该函数将缩减为以下内容：</p>
<pre><code>AddAltToImg=function(n,t){var i=$(n,t);i.attr("alt",i.attr("id").replace(/ID/, ""))}
</code></pre>
<h4>控制捆绑与缩减</h4>
<p><code>web.config</code>中的<code>configuration/system.web/compilation</code>的<code>debug</code>值会影响捆绑与缩减功能的启用或禁用，将<code>debug</code>值设置为<code>false</code>可以启用捆绑与缩减。</p>
<pre><code>&lt;system.web&gt;
    &lt;compilation debug="true" /&gt;
    &lt;!-- 为了清晰起见，其它与本节介绍的内容无关的行被省略，但不表示不应该包含其它行 --&gt;
&lt;/system.web&gt;
</code></pre>
<p>另外，<code>BundleTable</code>类中的的<code>EnableOptimizations</code>可以覆盖<code>web.config</code>中的<code>debug</code>配置。</p>
<pre><code>public static void RegisterBundles(BundleCollection bundles)
{
    bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                 "~/Scripts/jquery-{version}.js"));

    // 为了清晰起见，其它与本节介绍的内容无关的代码被省略，但不表示不应该包含其它代码
    BundleTable.EnableOptimizations = true;
}
</code></pre>
<h4>代码功能分析</h4>
<p><code>BundleConfig.cs</code>文件的默认代码如下：</p>
<pre><code>using System.Web.Optimization;

namespace IoTJissen
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                        "~/Scripts/jquery.validate*"));

            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                        "~/Scripts/modernizr-*"));

            bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                      "~/Scripts/bootstrap.js"));

            bundles.Add(new StyleBundle("~/Content/css").Include(
                      "~/Content/bootstrap.css",
                      "~/Content/site.css"));
        }
    }
}
</code></pre>
<p>此代码对<code>bundles</code>使用<code>Add()</code>方法创建了 5 个捆绑包，其中：</p>
<ul>
<li>参数为<code>ScriptBundle</code>的为 JavaScript 捆绑包</li>
<li>参数为<code>StyleBundle</code>的为 CSS 捆绑包</li>
</ul>
<p><code>ScriptBundle</code>与<code>StyleBundle</code>的参数皆为捆绑包的虚拟路径。</p>
<p><code>Bundle</code>类的<code>Include</code>方法接收若干个带有路径的文件匹配字符串，捆绑器在捆绑时会遵循以下约定：</p>
<ul>
<li>如果<code>FileX.min.js</code>和<code>FileX.js</code>同时存在，则在发布时选择<code>FileX.js</code>进行捆绑，在调试时选择<code>FileX.min.js</code>进行捆绑</li>
<li>忽略仅由 IntelliSense 使用的 "-vsdoc" 文件（如 jquery-1.7.1-vsdoc.js）</li>
</ul>
<p>使用<code>{version}</code>通配符可以通配版本号，这将允许开发人员在使用 NuGet 更新脚本版本时无需更改捆绑器或视图页面中的引用代码。</p>
<p>要使用已经创建好的捆绑包，可以在视图页使用如下代码：</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    @* 为了清晰起见，其它与本节介绍的内容无关的代码被省略，但不表示不应该包含其它代码 *@
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
&lt;/head&gt;
&lt;body&gt;
    @* 为了清晰起见，其它与本节介绍的内容无关的代码被省略，但不表示不应该包含其它代码 *@
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>例如，<code>@Scripts.Render("~/bundles/jquery")</code>引用了虚拟路径为<code>~/bundles/jquery</code>的 JavaScript 捆绑包，进而引用了<code>~/Scripts/jquery-{version}.js</code>，在调试时会进一步匹配<code>~/Scripts/jquery-3.4.1.js</code>，且在更新 jQuery 时无需对上述代码做出任何改动。</p>
<h4>更多</h4>
<p>有关捆绑与缩减的更多帮助可参考：</p>
<blockquote>
<p><a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/performance/bundling-and-minification">绑定和缩减 | Microsoft Docs</a></p>
</blockquote>
<hr />
<h2>~/Views/Shared/_Layout.cshtml</h2>
<h3>使用母版页为不同的页面创建一致的布局</h3>
<p>大多数站点都具有一致的外观和布局，大多数网站都由含页眉、导航、动态内容、页脚等几个部分，且每个站点内的各个页面也都遵循基本固定的布局，具有大量相似的元素或结构。建此布局的低效方法是在每个页面上分别定义标题、导航栏和页脚，但每次都要复制相同的标记。如果要更改某些内容（例如，更新页脚），则必须单独更改每个页面。</p>
<p>在 ASP.NET 网页中，可以定义为网站上的页面提供总体容器的母版页面。例如，母版页可以包含页眉、导航区和页脚，并包含一个占位符，其中包含动态内容。然后，可以定义单独的内容页，其中只包含该页的标记和代码。内容页面不必是完整的 HTML 页面；它们甚至不必有<code>&lt;body&gt;</code>元素。它们会在页面中或其它位置设置一行代码，告诉 ASP.NET 要在其中显示内容时使用的母版页。下面的图片显示了此关系的具体工作原理：</p>
<p><img src="https://docs.microsoft.com/zh-cn/aspnet/web-pages/overview/getting-started/introducing-aspnet-web-pages-2/layouts/_static/image2.png" alt="HowLayoutWorks" /></p>
<p>母版页名称需以前导下划线（<code>_</code>）开始，如果页的名称以下划线开头，则 ASP.NET 不会直接将该页发送到浏览器。此约定允许您定义站点所需的页面，但用户不能直接请求该页面。</p>
<p>有关如何在 ASP.NET 中使用母版页的更多帮助可参考：</p>
<blockquote>
<p><a href="https://docs.microsoft.com/zh-cn/aspnet/web-pages/overview/getting-started/introducing-aspnet-web-pages-2/layouts">介绍 ASP.NET 网页-创建一致的布局 | Microsoft Docs</a></p>
</blockquote>
<hr />
<h2>参考</h2>
<ul>
<li>[1] <a href="https://docs.microsoft.com/zh-cn/dotnet/architecture/modern-web-apps-azure/architectural-principles#separation-of-concerns">体系结构原则 | Microsoft Docs</a></li>
<li>[2] <a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/">用 ASP.NET 开发 Web 应用程序 | Microsoft Docs</a></li>
<li>[3] <a href="https://zh.wikipedia.org/wiki/%E9%A2%86%E5%9F%9F%E6%A8%A1%E5%9E%8B">领域模型 - 维基百科，自由的百科全书</a></li>
<li>[4] <a href="https://docs.microsoft.com/zh-cn/aspnet/core/migration/proper-to-2x/?view=aspnetcore-3.1#globalasax-file-replacement">从 ASP.NET 迁移到 ASP.NET Core | Microsoft Docs</a></li>
<li>[5] <a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs">ASP.NET MVC 路由概述（C#） | Microsoft Docs</a></li>
<li>[6] <a href="https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/performance/bundling-and-minification">绑定和缩减 | Microsoft Docs</a></li>
</ul>
]]></content>
    <author><name>戈小荷</name></author>
    <category term=".NET"/>
  </entry>
  <entry>
    <title>【ASP.NET】安装 ASP.NET 网页应用程序所需的开发环境</title>
    <link href="https://gehongyan.cn/dotnet/install-environment-for-asp-net-mvc/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/dotnet/install-environment-for-asp-net-mvc/</id>
    <published>2020-10-14T00:00:00.000Z</published>
    <updated>2020-10-14T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h3>安装 Visual Studio 并配置工作负载</h3>
<p><strong>1.</strong> 下载并启动 Visual Studio Installer</p>
<blockquote>
<p><a href="https://visualstudio.microsoft.com/zh-hans/">Visual Studio IDE、代码编辑器、Azure DevOps 和 App Center - Visual Studio</a></p>
</blockquote>
<p>其中，Community 2019 是针对开发人员个人可免费下载用于学术用途，并且是开放源代码的版本。</p>
<p><strong>2.</strong> 选择以下工作负载安装 Visual Studio Community 2019（注意调整安装目标位置）</p>
<ul>
<li>
<p>Web 和云</p>
<ul>
<li>ASP.NET 和 Web 开发（使用 ASP.NET Core、ASP.NET、HTML/Javascript 和包括 Docker 支持的容器生成 Web 应用程序）</li>
</ul>
</li>
<li>
<p>其它工具集</p>
<ul>
<li>数据存储和处理（使用 SQL Server，Azure Data Lake 或 Hadoop 连接、开发和测试数据解决方案）</li>
</ul>
</li>
</ul>
<p>注意确认右侧安装详细信息中<strong>数据存储和处理</strong>下的 <strong>SQL Server Data Tools</strong> 为已勾选。</p>
<p><strong>3.</strong> 启动 Visual Studio Community 2019 并登录个人微软账户，免费激活面向个人用于学术用途的 Visual Studio Community 许可证</p>
<h3>安装 SQL Server 2019 Express 与 SSMS</h3>
<p><strong>1.</strong> 下载并依向导安装 SQL Server 2019 Express</p>
<blockquote>
<p><a href="https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads">SQL Server 下载 | Microsoft</a></p>
</blockquote>
<p>其中：</p>
<ul>
<li><strong>SQL Server 2019 Developer</strong> 是一个<strong>全功能免费版本</strong>，许可在<strong>非生产环境下</strong>用作开发和测试数据库。</li>
<li><strong>SQL Server 2019 Express</strong> 是 SQL Server 的一个<strong>免费版本</strong>，非常适合用于<strong>桌面、Web 和小型服务器应用程序的开发和生产</strong>。</li>
</ul>
<p>有关使用安装向导进行数据库服务安装的指南可参考：</p>
<blockquote>
<p><a href="https://docs.microsoft.com/zh-cn/sql/database-engine/install-windows/install-sql-server-from-the-installation-wizard-setup?view=sql-server-ver15">使用图形用户界面安装 - SQL Server | Microsoft Docs</a></p>
</blockquote>
<p><strong>2.</strong> 下载并依向导安装 SQL Server Management Studio (SSMS)</p>
<blockquote>
<p><a href="https://docs.microsoft.com/zh-cn/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver15">下载 SQL Server Management Studio (SSMS) - SQL Server Management Studio (SSMS) | Microsoft Docs</a></p>
</blockquote>
<h3>启动 SSMS 连接本地数据库并配置 sa 账户</h3>
<p><strong>1.</strong> 启动 SSMS，使用 Windows 身份验证连接本地数据库实例</p>
<p>有关如何使用 SQL Server Management Studio (SSMS) 连接和查询 SQL Server 实例的指南可参考：</p>
<blockquote>
<p><a href="https://docs.microsoft.com/zh-cn/sql/ssms/quickstarts/connect-query-sql-server?view=sql-server-ver15">连接和查询 SQL Server 实例 - SQL Server Management Studio (SSMS) | Microsoft Docs</a></p>
</blockquote>
<p><strong>2.</strong> 启用数据库实例的 SQL Server 和 Windows 身份验证模式</p>
<p>有关如何使用 SQL Server 和 Windows 身份验证模式 的指南可参考：</p>
<blockquote>
<p><a href="https://docs.microsoft.com/zh-cn/sql/database-engine/configure-windows/change-server-authentication-mode?view=sql-server-ver15">更改服务器身份验证模式 - SQL Server | Microsoft Docs</a></p>
</blockquote>
]]></content>
    <author><name>戈小荷</name></author>
    <category term=".NET"/>
  </entry>
  <entry>
    <title>【曲谱】霊知の太陽信仰 ～ Nuclear Fusion</title>
    <link href="https://gehongyan.cn/gakufu/reichi-no-taiyou-shinkou/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/gakufu/reichi-no-taiyou-shinkou/</id>
    <published>2020-10-08T00:00:00.000Z</published>
    <updated>2020-10-08T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h3>灵知的太阳信仰 ～ Nuclear Fusion</h3>
<p>东方地灵殿 6面BOSS 灵乌路空 主题曲 まらしぃさん在2020/9/11的ニコ生演奏的版本</p>
<hr />
<p><strong>来源：</strong> 東方地霊殿　～ Subterranean Animism</p>
<p><strong>曲名：</strong> 霊知の太陽信仰 ～ Nuclear Fusion</p>
<p><strong>译名：</strong> 灵知的太阳信仰 ～ Nuclear Fusion</p>
<p><strong>作曲：</strong> ZUN</p>
<p><strong>编排：</strong> まらしぃ</p>
<hr />
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/%20albums/TH17_Cover-300x296.jpg" alt="TH11Cover" /></p>
<hr />
<p><a href="https://oss-piano.gehongyan.cn/%E9%9C%8A%E7%9F%A5%E3%81%AE%E5%A4%AA%E9%99%BD%E4%BF%A1%E4%BB%B0%20%EF%BD%9E%20Nuclear%20Fusion.pdf">曲谱下载</a> | <a href="https://musescore.com/user/23409316/scores/6382348">Musescore</a> | <a href="https://www.bilibili.com/video/BV1bK411N7SG/">视频预览</a> | <a href="https://live2.nicovideo.jp/watch/lv327990943">视频参考</a> | <a href="https://www.youtube.com/watch?v=jqGfz19p5l0">本家</a></p>
<hr />
<p>::: collapse</p>
<ul>
<li>
<p>浏览曲谱图像</p>
<p>:::grid{columns="1" aspect="70/99" fit="contain"}
<img src="https://oss-piano.gehongyan.cn/pic/%E9%9C%8A%E7%9F%A5%E3%81%AE%E5%A4%AA%E9%99%BD%E4%BF%A1%E4%BB%B0%20%EF%BD%9E%20Nuclear%20Fusion/TaiyouShinKou_Pic1.jpg" alt="ShinKou01" />
<img src="https://oss-piano.gehongyan.cn/pic/%E9%9C%8A%E7%9F%A5%E3%81%AE%E5%A4%AA%E9%99%BD%E4%BF%A1%E4%BB%B0%20%EF%BD%9E%20Nuclear%20Fusion/TaiyouShinKou_Pic2.jpg" alt="ShinKou02" />
<img src="https://oss-piano.gehongyan.cn/pic/%E9%9C%8A%E7%9F%A5%E3%81%AE%E5%A4%AA%E9%99%BD%E4%BF%A1%E4%BB%B0%20%EF%BD%9E%20Nuclear%20Fusion/TaiyouShinKou_Pic3.jpg" alt="ShinKou03" />
<img src="https://oss-piano.gehongyan.cn/pic/%E9%9C%8A%E7%9F%A5%E3%81%AE%E5%A4%AA%E9%99%BD%E4%BF%A1%E4%BB%B0%20%EF%BD%9E%20Nuclear%20Fusion/TaiyouShinKou_Pic4.jpg" alt="ShinKou04" />
<img src="https://oss-piano.gehongyan.cn/pic/%E9%9C%8A%E7%9F%A5%E3%81%AE%E5%A4%AA%E9%99%BD%E4%BF%A1%E4%BB%B0%20%EF%BD%9E%20Nuclear%20Fusion/TaiyouShinKou_Pic5.jpg" alt="ShinKou05" />
<img src="https://oss-piano.gehongyan.cn/pic/%E9%9C%8A%E7%9F%A5%E3%81%AE%E5%A4%AA%E9%99%BD%E4%BF%A1%E4%BB%B0%20%EF%BD%9E%20Nuclear%20Fusion/TaiyouShinKou_Pic6.jpg" alt="ShinKou06" />
:::</p>
</li>
</ul>
<hr />
<p><strong>哔哩哔哩视频</strong></p>
<p>::bilibili{bvid="BV1bK411N7SG" title="霊知の太陽信仰 ～ Nuclear Fusion" p="1"}</p>
<p><strong>YouTube</strong></p>
<p>::youtube{id="MxPB9P17DdU" title="霊知の太陽信仰 ～ Nuclear Fusion"}</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="曲谱"/>
  </entry>
  <entry>
    <title>【ASP.NET】限制程序在同一时刻只有一个运行的实例</title>
    <link href="https://gehongyan.cn/dotnet/asp-net-mvc-lock/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/dotnet/asp-net-mvc-lock/</id>
    <published>2020-09-26T00:00:00.000Z</published>
    <updated>2020-09-26T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<ol>
<li>创建私有静态对象 <code>_lockobject</code> 作为互斥锁</li>
</ol>
<pre><code>// 互斥锁
private static object _lockobject = new object();
</code></pre>
<ol>
<li>使用 <code>lock</code> 执行互斥程序，参数为 <code>_lockobject</code>，可以确保 <code>lock</code> 方法内程序在同一时刻只有一个运行的实例</li>
</ol>
<pre><code>public ActionResult Index() {
    String str;
    lock (_lockobject)
    {
        str = dosomething();
    }
    return View();
}
</code></pre>
<ol>
<li>如果需要对互斥锁设置超时，可以使用 <code>Monitor.TryEnter</code> 方法</li>
</ol>
<pre><code>public ActionResult Index() {
    String str;
    if (Monitor.TryEnter(_lockobject, TimeSpan.FromSeconds(5)))
    {
        str = dosomething();
        Monitor.Exit(_lockobject);
    }
    else str = "Took too long!";
    return View();
}
</code></pre>
<h4>参考文献</h4>
<ul>
<li><a href="https://stackoverflow.com/questions/22154306/how-to-implement-an-mvc-4-web-app-server-side-mutex/22154881#22154881">How to implement an MVC 4 web App server side mutex - Answered by McGarnagle - Stack Overflow</a></li>
</ul>
]]></content>
    <author><name>戈小荷</name></author>
    <category term=".NET"/>
  </entry>
  <entry>
    <title>条码概述</title>
    <link href="https://gehongyan.cn/iot/barcode/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/iot/barcode/</id>
    <published>2020-09-23T00:00:00.000Z</published>
    <updated>2020-09-23T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h2>一维码</h2>
<h3>简介</h3>
<p><strong>条形码</strong>或称<strong>条码</strong>（barcode）是将宽度不等的多个<strong>黑条</strong>和<strong>空白</strong>，按照一定的编码规则排列，用以表达一组信息的图形标识符。常见的条形码是由反射率相差很大的黑条（简称条）和白条（简称空）排成的平行线图案。条形码可以标出物品的生产国、制造厂家、商品名称、生产日期、图书分类号、邮件起止地点、类别、日期等信息，因而在商品流通、图书管理、邮政管理、银行系统等许多领域都得到了广泛的应用。</p>
<h3>条形码种类</h3>
<blockquote>
<p><a href="https://en.wikipedia.org/wiki/Barcode#Linear_barcodes">Barcode - Linear Barcodes</a></p>
</blockquote>
<h3>UPC/EAN/JAN码</h3>
<p><strong>通用产品代码</strong>（<strong>U</strong>niversal <strong>P</strong>roduct <strong>C</strong>ode，<strong>UPC</strong>），通常简称 <strong>UPC 码</strong>，是<strong>美国均匀码理事会</strong>（<strong>U</strong>niform <strong>C</strong>ode <strong>C</strong>ouncil, <strong>UCC</strong>）制定的一种商品条码，主要在美国及加拿大使用。在其基础之上发展起来的 <strong>EAN 码</strong>则已发展成为适用范围最广的通用条码。</p>
<p><strong>国际商品编码</strong>（<strong>I</strong>nternational <strong>A</strong>rticle <strong>N</strong>umber，<strong>IAN</strong>），即<strong>欧洲商品编码</strong>（<strong>E</strong>uropean <strong>A</strong>rticle <strong>N</strong>umber, <strong>EAN</strong>），原来只是欧洲范围内商品，而现在已是全球范围内产品交易的商品代码。为了适应读码器辨认的需要，这些代码又被做成大家熟知的条形码。</p>
<p>另外，日本于 1978 年在 <strong>EAN</strong> 的基础上开发出“<strong>JAN</strong> 码”（<strong>JAN</strong> コード，<strong>J</strong>apanese <strong>A</strong>rticle <strong>N</strong>umber）。</p>
<p>本文以 UPC 码为例。</p>
<h3>UPC 码简介</h3>
<p>UPC 码只能用来表示 0-9 的数字。每 7 个模组表达一个字符，每个模组有空（白色）与条（黑色）两种状态。UPC码又分为 UPC-A、B、C、D、E 五种版本。此处以 <strong>UPC-A</strong> 码为例。</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/UPC_EANUCC-12_barcode.png" alt="UPCEANUCC-12barcode" /></p>
</blockquote>
<p><strong>UPC-A</strong> 用于通用商品，是适用范围最广的一种UPC码。一共有 113 个模组。左右两个各由 9 个模组组成的空白。UPC-A码是定长码，只能表示 12 位数字。从左至右，依次是 3 个模组（101）的起始码、1 位的系统码、5 位的左侧数据码、5 个模组（01010）的中间码、5 位的右侧数据码、检查码、3个模组（101）的终止码。其中，起始码、中间码、终止码的模组长度都要长于数据码。</p>
<p>左侧数据码与右侧数据码的数值对应规则并不相同，左侧数据码含有<strong>奇数</strong>个模组，右侧数据码含有<strong>偶数</strong>个。黑色模组对应逻辑值为 1，白色则为 0。</p>
<ul>
<li><strong>对应关系表</strong></li>
</ul>
<blockquote>
<table>
<thead>
<tr>
<th>数值</th>
<th>左侧逻辑值</th>
<th>右侧逻辑值</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>000<strong>11</strong>0<strong>1</strong></td>
<td><strong>111</strong>00<strong>1</strong>0</td>
</tr>
<tr>
<td>1</td>
<td>00<strong>11</strong>00<strong>1</strong></td>
<td><strong>11</strong>00<strong>11</strong>0</td>
</tr>
<tr>
<td>2</td>
<td>00<strong>1</strong>00<strong>11</strong></td>
<td><strong>11</strong>0<strong>11</strong>00</td>
</tr>
<tr>
<td>3</td>
<td>0<strong>1111</strong>0<strong>1</strong></td>
<td><strong>1</strong>0000<strong>1</strong>0</td>
</tr>
<tr>
<td>4</td>
<td>0<strong>1</strong>000<strong>11</strong></td>
<td><strong>1</strong>0<strong>111</strong>00</td>
</tr>
<tr>
<td>5</td>
<td>0<strong>11</strong>000<strong>1</strong></td>
<td><strong>1</strong>00<strong>111</strong>0</td>
</tr>
<tr>
<td>6</td>
<td>0<strong>1</strong>0<strong>1111</strong></td>
<td><strong>1</strong>0<strong>1</strong>0000</td>
</tr>
<tr>
<td>7</td>
<td>0<strong>111</strong>0<strong>11</strong></td>
<td><strong>1</strong>000<strong>1</strong>00</td>
</tr>
<tr>
<td>8</td>
<td>0<strong>11</strong>0<strong>111</strong></td>
<td><strong>1</strong>00<strong>1</strong>000</td>
</tr>
<tr>
<td>9</td>
<td>000<strong>1</strong>0<strong>11</strong></td>
<td><strong>111</strong>0<strong>1</strong>00</td>
</tr>
</tbody>
</table>
</blockquote>
<p>校验码为全部 12 位数据码最后一位。如果从左至右依次将数据码前十一位命名为 N1-N11，校验码命名为 C，则校验码 C 的计算方式如下：</p>
<blockquote>
<p><strong>CC</strong> =（N1 + N3 + N5 + N7 + N9 + N11）× 3 + (N2 + N4 + N6 + N8 + N10）然后取个位。 <strong>C</strong> = 10 - CC (若 C 值为 10，则取 0)。</p>
</blockquote>
<p>以图中条码为例，CC = (0 + 6 + 0 + 2 + 1 + 5）× 3 + (3 + 0 + 0 + 9 + 4 ) = 58，C = 2</p>
<h3>条形码生成</h3>
<blockquote>
<p><a href="https://cn.online-qrcode-generator.com/Barcode-Generator">在线条码发生器</a></p>
</blockquote>
<h2>二维码</h2>
<h3>简介</h3>
<p><strong>二维条码/二维码</strong> （2-dimensional bar code） 是用某种特定的几何图形按一定规律在<strong>平面（二维方向上）<strong>分布的黑白相间的图形记录数据符号信息的；在代码编制上巧妙地利用构成计算机内部逻辑基础的</strong>“0”、“1”比特流</strong>的概念，使用若干个与<strong>二进制</strong>相对应的几何形体来表示文字数值信息，通过<strong>图象输入设备或光电扫描设备</strong>自动识读以实现信息自动处理。它具有条码技术的一些共性：每种码制有其特定的字符集；每个字符占有一定的宽度；具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化等特点。<strong>二维条码/二维码能够在横向和纵向两个方位同时表达信息，因此能在很小的面积内表达大量的信息。</strong></p>
<h3>二维码种类</h3>
<blockquote>
<p><a href="https://en.wikipedia.org/wiki/Barcode#Matrix_(2D)_barcodes">Barcode - Matrix_(2D) / Barcodes</a></p>
</blockquote>
<h4>堆叠式二维码</h4>
<ul>
<li><strong>PDF417</strong> 条码，<strong>P</strong>ortable <strong>D</strong>ata <strong>F</strong>ile，是一种高密度、高信息含量的便携式数据文件，是实现证件及卡片等大容量、高可靠性信息自动存储、携带并可用机器自动识读的理想手段。可以用传统的线性扫码器识别，而矩阵式二维码需要图像传感器成像才能识别。</li>
</ul>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/PDF417Sample.png" alt="PDF417Sample" /></p>
</blockquote>
<h4>矩阵式二维码</h4>
<ul>
<li><strong>QR Code</strong>，<strong>QR 码</strong>（<strong>Q</strong>uick <strong>R</strong>esponse <strong>C</strong>ode，<strong>快速响应矩阵图码</strong>）是，于 1994 年由<strong>日本 DENSO WAVE 公司</strong>发明，广泛应用于生活与工业生产中。</li>
</ul>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRcode_image.svg.png?x-oss-process=style/ResizeTo320px" alt="QRCode" /></p>
</blockquote>
<ul>
<li><strong>Data Matrix</strong>（<strong>数据矩阵</strong>），是一种由黑色、白色的色块（单元格）以正方形或长方形组成的二维码（也可称矩阵），于 1994 年 8 月由<strong>美国国际资料公司</strong>所研发出，主要用于零件、印制电路板等等。</li>
</ul>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/Datamatrix.png" alt="DataMatrix" /></p>
</blockquote>
<ul>
<li><strong>MaxiCode</strong>，MaxiCode 是最初由 United Parcel Service 创建和使用的公共领域的机器可读符号系统，应用于跟踪和管理包裹的运输，使用以六角形网格排列的点表示数据。</li>
</ul>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/1280px-MaxiCode_Regions.svg.png?x-oss-process=style/ResizeTo320px" alt="MaxiCodeRegions" /></p>
</blockquote>
<ul>
<li>
<p><strong>汉信码</strong>，是一种矩阵式二维条码，我国自主研发，具有高度的汉字表示能力和汉字压缩效率，形状上呈正方向、有深色和浅色数据模块分布其间。2007 年 8 月 23 日《汉信码》国家标准正式颁布，并于 2008 年 2 月 1 日正式实施。常见于发票、追溯、筛查等。</p>
<ul>
<li>国家国税总局汉信码增值税发票</li>
<li>特种行业应用发票</li>
<li>广东省水产品质量追溯体系</li>
<li>医疗卫生领域中新生儿疾病筛查</li>
</ul>
</li>
</ul>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/HanXinCode.png?x-oss-process=style/ResizeTo320px" alt="HanXinCode" /></p>
</blockquote>
<ul>
<li><strong>网格矩阵码</strong>，<strong>G</strong>rid <strong>M</strong>atrix Code，我国自主研发，常用于食品安全质量追溯。<a href="#refer-anchor-1">1</a></li>
</ul>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/GMMatrix.png" alt="GMCode" /></p>
</blockquote>
<ul>
<li><strong>龙贝码</strong>，拥有完全自主知识产权，具有更高的信息密度、更强的加密功能、可以对所有汉字进行编码、适用于各种类型的识读器、最多可使用多达32种语言系统、具有多向编码/译码功能、极强的抗畸变性能、可对任意大小及长宽比的二维条码进行编码和译码。<a href="#refer-anchor-2">2</a></li>
</ul>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/LPCode.gif" alt="LPCode" /></p>
</blockquote>
<h4>私有码制</h4>
<ul>
<li><strong>微信小程序码</strong>，由腾讯公司发布，专用于微信小程序，信息轴向排布，具有较高的辨识度。</li>
</ul>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/WeChatMiniProgramCode.jpg?x-oss-process=style/ResizeTo320px" alt="WeChatMiniProgramCode" /></p>
</blockquote>
<h3>QR码<a href="#refer-anchor-3">3</a></h3>
<h4>版本</h4>
<p>QR 码一共提供 40 种不同版本存储密度的结构，版本 1 为 21 × 21 模块（模块为 QR 码中的最小单元），每增加一个版本，长宽各增加 4 个模块，最大的版本 40 为 177 × 177 模块。</p>
<ul>
<li>
<p>版本 1 &amp; 2</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCode_Version1and2.png?x-oss-process=style/ResizeTo320px" alt="QRCodeVersion1and2" /></p>
</blockquote>
</li>
<li>
<p>版本 6 &amp; 7</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCode_Version6and7.png?x-oss-process=style/ResizeTo320px" alt="QRCodeVersion1and2" /></p>
</blockquote>
</li>
<li>
<p>版本 14</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCode_Version14.png?x-oss-process=style/ResizeTo320px" alt="QRCodeVersion1and2" /></p>
</blockquote>
</li>
<li>
<p>版本 40</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCode_Version40.png?x-oss-process=style/ResizeTo320px" alt="QRCodeVersion1and2" /></p>
</blockquote>
</li>
</ul>
<h4>容量</h4>
<p>针对模块数量最多的版本 40 来说，QR 码的最大数据容量如下表：</p>
<blockquote>
<table>
<thead>
<tr>
<th>存储类型</th>
<th>最大容量</th>
</tr>
</thead>
<tbody>
<tr>
<td>数字</td>
<td>7089 字符</td>
</tr>
<tr>
<td>字母</td>
<td>4296 字符</td>
</tr>
<tr>
<td>二进制数(8bit)</td>
<td>2953 字节</td>
</tr>
<tr>
<td>日文汉字/假名（Shift JIS 编码）</td>
<td>1817 字符</td>
</tr>
<tr>
<td>中文汉字（UTF-8 编码）</td>
<td>984 字符</td>
</tr>
<tr>
<td>中文汉字（BIG5/GB2312 编码）</td>
<td>1800 字符</td>
</tr>
</tbody>
</table>
</blockquote>
<h4>容错</h4>
<p>QR 码有容错能力，QR 码图形如果有破损，仍然可以被机器读取内容，最高可以到 30% 面积破损仍可被读取。所以 QR 码可以被广泛使用在运输外箱上。</p>
<p>QR 码所使用的纠错码编码算法为<a href="https://zh.wikipedia.org/wiki/%E9%87%8C%E5%BE%B7-%E6%89%80%E7%BD%97%E9%97%A8%E7%A0%81"><strong>里德-所罗门码</strong>（<strong>R</strong>eed-<strong>S</strong>olomon codes）</a>，是一种前向错误更正的信道编码，对由校正过采样数据所产生的有效多项式。</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/BrokenQRCode.png" alt="BrokenQRCode" /></p>
</blockquote>
<p>QR 码的容错共有 4 个等级，对应的错误修正容量如下表：</p>
<blockquote>
<table>
<thead>
<tr>
<th>容错等级</th>
<th>最大的错误修正容量</th>
</tr>
</thead>
<tbody>
<tr>
<td>L 等级</td>
<td>7%</td>
</tr>
<tr>
<td>M 等级</td>
<td>15%</td>
</tr>
<tr>
<td>Q 等级</td>
<td>25%</td>
</tr>
<tr>
<td>H 等级</td>
<td>30%</td>
</tr>
</tbody>
</table>
</blockquote>
<p>不同容错等级的对比如下：</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeBrokenSampleBefore.png" alt="QRCodeBrokenSampleBefore" /> <img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeBrokenSampleAfter.png" alt="QRCodeBrokenSampleAfter" /></p>
</blockquote>
<h4>结构</h4>
<p>QR 码最大特征为其左上，右上，左下三个大型的如同“回”字的黑白间同心方图案，为 QR 码识别定位标记，失去其中一个会影响识别。而呈棋盘般分布的有别与大定位标记的较小的同心方则为其校正标记，用于校正识别，版本 1 没有校正标记，版本 2 在右下方，其中心点在左下和右上定位标记的外边框的相交点，版本10开始以每个等距的方式出现在右下校正点至左下和右上定位标记的外边框的连线、左上与左下定位标记的外边框的连线、左上与右上定位标记的外边框的连线之间、这四边线上等距点对边相连线，版本 10 等距有 1 个，版本 25 为 3 个，版本 40 为 5 个。</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeStructureENG.png" alt="QRCodeStructureENG" /> <img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeStructureCHN.png" alt="QRCodeStructureCHN" /></p>
</blockquote>
<h4>数据构造</h4>
<p>在规格确定的条件下，数据按照规定的计算方式生成序列按次序放入分块中，按规定把数据分块，对每一块进行计算，得出相应的纠错码字区块，把纠错码字区块 按顺序构成一个序列，添加到原先的数据码字序列后面。如：D1, D12, D23, D35, D2, D13, D24, D36, … D11, D22, D33, D45, D34, D46, E1, E23,E45, E67, E2, E24, E46, E68，……</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeBlockArrangeSequence.png?x-oss-process=style/ResizeTo320px" alt="QRCodeBlockArrangeSequence" /> <img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeBlockArrange.png?x-oss-process=style/ResizeTo320px" alt="QRCodeBlockArrangement" /> <img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeBlockArrangeSample.png" alt="QRCodeBlockArrangeSample" /></p>
</blockquote>
<h4>掩膜</h4>
<p>将掩膜图形应用于符号的编码区域，使得二维码图形中的深色和浅色（黑色和白色）区域能够比率最优的分布。</p>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeMaskStructure.png" alt="QRCodeMaskStructure" /> <img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeMaskPatterns.png" alt="QRCodeMaskPatterns" /> <img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeMaskSample.png" alt="QRCodeMaskSample" /></p>
</blockquote>
<h4>解码</h4>
<p>光学处理 - 几何校正 - 码位读取 - 反掩码 - 确定类别 - 确定长度 - 破译 - 检错纠错 - 字符转换 - 协议执行</p>
<h3>应用类型<a href="#refer-anchor-4">4</a></h3>
<h4>文本</h4>
<pre><code>物联网（英语：Internet of Things，简称IoT）是一种计算设备、机械、数字机器相互关系的系统，具备通用唯一识别码（UID），并具有通过网络传输数据的能力，无需人与人、或是人与设备的交互。
</code></pre>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeTextSample.png?x-oss-process=style/ResizeTo320px" alt="QRCodeTextSample" /></p>
</blockquote>
<h4>网址</h4>
<pre><code>https://www.gehongyan.cn/2020/09/23/iotjissen/198
</code></pre>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeURLSample.png?x-oss-process=style/ResizeTo320px" alt="QRCodeURLSample" /></p>
</blockquote>
<h4>名片</h4>
<pre><code>MECARD:
N:戈小荷,;
TEL:13800138000;
EMAIL:ge@example.com;
ADR:北京市海淀区;
</code></pre>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeNamecardSample.png?x-oss-process=style/ResizeTo320px" alt="QRCodeNamecardSample" /></p>
</blockquote>
<h4>位置</h4>
<pre><code>geo:39.9931885,116.3520637?q=北京科技大学
</code></pre>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeLocationSample.png?x-oss-process=style/ResizeTo320px" alt="QRCodeLocationSample" /></p>
</blockquote>
<h4>Wi-Fi</h4>
<pre><code>WIFI:
S:CMCC-asdfgh;
P:;
T:;
H:;
</code></pre>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeWiFiSample.png?x-oss-process=style/ResizeTo320px" alt="QRCodeWiFiSample" /></p>
</blockquote>
<h4>短信</h4>
<pre><code>sms:10086
</code></pre>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeSMSSample.png?x-oss-process=style/ResizeTo320px" alt="QRCodeSMSSample" /></p>
</blockquote>
<h4>电话</h4>
<pre><code>tel:10086
</code></pre>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeTelSample.png?x-oss-process=style/ResizeTo320px" alt="QRCodeTelSample" /></p>
</blockquote>
<h4>日历事件</h4>
<pre><code>BEGIN:VEVENT
SUMMARY:国庆节假期！
DTSTART:20200930T160000Z
DTEND:20201004T155959Z
END:VEVENT
</code></pre>
<blockquote>
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/barcode/QRCodeEventSample.png?x-oss-process=style/ResizeTo320px" alt="QRCodeEventSample" /></p>
</blockquote>
<h3>二维码工具</h3>
<h4>生成</h4>
<ul>
<li><a href="https://www.beaconstac.com/qr-code-generator"><strong>Beaconstac</strong></a></li>
<li><a href="https://cli.im/"><strong>草料二维码</strong></a></li>
<li><a href="https://www.erweicaihong.cn/"><strong>二维彩虹</strong></a></li>
</ul>
<h4>解码</h4>
<ul>
<li><a href="https://www.onlinebarcodereader.com/"><strong>Online Barcode Reader</strong></a></li>
<li><a href="https://zxing.org/w/decode.jspx"><strong>ZXing Decoder Online</strong></a></li>
</ul>
<h2>参考</h2>
<ul>
<li>
<p>[1] <a href="http://read.pudn.com/downloads341/sourcecode/compiler/1493267/syscan%20GM%E7%BC%96%E7%A0%81/GM%E7%A0%81%E6%8A%80%E6%9C%AF%E7%89%B9%E7%82%B9%E4%BB%8B%E7%BB%8D-1.pdf">GM 码技术特点及优势 - 深圳矽感科技有限公司（PDF）</a></p>
</li>
<li>
<p>[2] <a href="http://www.gs1cn.org/Knowledge/article22.aspx?id=159">龙贝码 - 中国物品编码中心网站</a></p>
</li>
<li>
<p>[3] <a href="https://oss-gexiaohe.gehongyan.cn/reference-docs/barcode/GBT-18284-2000-%E5%BF%AB%E9%80%9F%E5%93%8D%E5%BA%94%E7%9F%A9%E9%98%B5%E7%A0%81.pdf">《快速响应矩阵码》- GBT-18284-2000（PDF）</a></p>
</li>
<li>
<p>[4] <a href="https://github.com/zxing/zxing/wiki/Barcode-Contents">Barcode Contents · zxing/zxing Wiki</a></p>
</li>
</ul>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="物联网"/>
  </entry>
  <entry>
    <title>RFID 读写器程序</title>
    <link href="https://gehongyan.cn/iot/rfid-coding/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/iot/rfid-coding/</id>
    <published>2020-09-22T00:00:00.000Z</published>
    <updated>2020-09-22T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h2>工程文件结构</h2>
<ul>
<li><strong>Devices</strong> <code>硬件驱动</code>
<ul>
<li><strong>display</strong> <code>数码管驱动</code></li>
<li><strong>fm17xx</strong> <code>读写器芯片驱动</code></li>
<li><strong>spi</strong> <code>SPI接口驱动</code></li>
<li><strong>uart</strong> <code>串口驱动</code></li>
</ul>
</li>
<li><strong>List</strong>
<ul>
<li><strong>RFID.m51</strong> <code>自动生成的文件，记录了单片机存储器的使用情况，以及每个变量包括局部变量的位置</code><a href="#refer-anchor-1">1</a></li>
</ul>
</li>
<li><strong>Output</strong> <code>输出文件</code>
<ul>
<li><strong>RFID</strong> <code>用于软件仿真的编译后文件</code></li>
<li><strong>RFID.hex</strong> <code>用于下载程序的十六进制文件</code></li>
<li><strong>RFID.plg</strong> <code>编译日志文件</code></li>
</ul>
</li>
<li><strong>Prject</strong> <code>项目文件夹</code>
<ul>
<li><strong>RFID.uvgui</strong> <code>Keil用户界面</code></li>
<li><strong>RFID.uvopt</strong> <code>工程配置文件</code></li>
<li><strong>RFID.uvproj</strong> <code>工程主题文件</code></li>
<li><strong>STARTUP.A51</strong> <code>项目初始化文件</code><a href="#refer-anchor-2">2</a></li>
</ul>
</li>
<li><strong>User</strong> <code>用户代码</code>
<ul>
<li><strong>INCLUDE.H</strong> <code>头文件</code></li>
<li><strong>main.c</strong> <code>主函数</code></li>
<li><strong>stc_it.c</strong> <code>中断处理函数</code></li>
<li><strong>STC11.H</strong> <code>STC11芯片驱动</code></li>
</ul>
</li>
</ul>
<h3>/Devices/display/display.c</h3>
<pre><code>// 共阳数码管编码
uchar code table[12] =
{
    0xc0, 0xf9, 0xa4, 0xb0,     // 0, 1, 2, 3
    0x99, 0x92, 0x82, 0xf8,     // 4, 5, 6, 7
    0x80, 0x90, 0x7f, 0xbf      // 8, 9, ., -
};
</code></pre>
<pre><code>// 显示当前值到数码管上
void disp(void)
{
    // 动态扫描显示
}
</code></pre>
<h3>/User/fm17xx.c</h3>
<pre><code>// FM17xx 初始化
void init_FM17xx(void)
{
    // 可用于初始化 FM1702SL FM1715SL FM1722SL RC522 RC531 等
}

</code></pre>
<pre><code>// 卡处理函数，参数 cmd 为指令字段
int rfid_card(uchar cmd)
{
    uchar status;
    switch (cmd)
    {
    case _FM17XX_Halt_:         // 卡片停止工作
        break;
    case _FM17XX_Request_:      // 获取当前卡片类型
        break;
    case _FM17XX_Anticoll_:     // 防冲突
        break;
    case _FM17XX_Select_:       // 选择卡
        break;
    case _FM17XX_Cmp_Key2_:     // 密钥校验与密钥解析
        break;
    case _FM17XX_Read_:         // 读取某一地址内容
        break;
    case _FM17XX_Write_:        // 写某一地址内容
        break;
    }
    return 0;
}
</code></pre>
<pre><code>// 伪代码
// 根据串口命令解析数据
void rfid_deal(uchar cmd, uchar *buf)
{
    uchar status, i;    //临时变量
    uchar sector;       //扇区
    uchar block;        //块
    uchar key[6];       //密钥
    uchar rev_Data[16]; //块内容
    static uchar UID1[4] = {0, 0, 0, 0};
    static uchar beep_timer_flag;

    switch (cmd)
    {
    case 0x08:      // 读取卡号 显示当前金额依据第二块内容
        status = rfid_card(_FM17XX_Request_);
        status = rfid_card(_FM17XX_Anticoll_);
        break;
    case 0x09:      // 读取卡信息
        // 从 buf 中读区块信息
        status = rfid_card(_FM17XX_Request_);       // 识别卡类型
        status = rfid_card(_FM17XX_Anticoll_);      // 防碰撞
        if (!enable_Rfid)
        {
            // 使用上位机的处理函数
            // 调用 LED 与蜂鸣器
            // 通过串口发送卡号到上位机
            status = rfid_card(_FM17XX_Select_);    // 选卡
            status = rfid_card(_FM17XX_Cmp_Key2_);  // 密钥校验与密钥解析
            fm1702sl.buffer[0] = _FM17XX_Read_;     // 读卡
            fm1702sl.buffer[1] = block;
            if (!status)
            {
                status = rfid_card(_FM17XX_Read_);  // 读卡
                // 协议缓冲与其它处理
            }
        }
        else
        {
            // 不使用上位机的处理函数
            // 调用 LED 与蜂鸣器
            status = rfid_card(_FM17XX_Select_);    // 选卡
            status = rfid_card(_FM17XX_Cmp_Key2_);  //密钥校验与密钥解析
            fm1702sl.buffer[0] = _FM17XX_Read_;     // 读卡
            fm1702sl.buffer[1] = block;
            // 其它处理
        }
        break;
    case 0x10:      //写入块地址内容 上位机无法修改 4*SEN+3 地址内容
        status = rfid_card(_FM17XX_Request_);       // 识别卡类型
        status = rfid_card(_FM17XX_Anticoll_);      // 防碰撞
        status = rfid_card(_FM17XX_Select_);        // 选卡
        status = rfid_card(_FM17XX_Cmp_Key2_);      //密钥校验与密钥解析
        if (!status)
        {
            status = rfid_card(_FM17XX_Write_);     // 写卡
        }
        break;
    case 0x11:      //修改密钥
        status = rfid_card(_FM17XX_Request_);       // 识别卡类型
        status = rfid_card(_FM17XX_Anticoll_);      // 防碰撞
        status = rfid_card(_FM17XX_Select_);        // 选卡
        status = rfid_card(_FM17XX_Cmp_Key2_);      //密钥校验与密钥解析
        if (!status)
        {
            status = rfid_card(_FM17XX_Write_);     // 写卡
        }
        break;
    case 0x12:  //已注册卡
        while (1)
        {
            status = rfid_card(_FM17XX_Request_);   // 识别卡类型
            status = rfid_card(_FM17XX_Anticoll_);  // 防碰撞
            // 调用 LED
        }
        break;
    }
}
</code></pre>
<h3>/User/main.c</h3>
<pre><code>// 系统初始化
void sys_init(void)
{
    // 初始化 MCU-port UART-int fm17xx-spi
}
</code></pre>
<pre><code>// 延迟函数
void delay1(uint dlength)
{
    uint i;
    for (i = 0; i &lt; dlength; i++)
    {
        _nop_();
        _nop_();
        _nop_();
    }
}
</code></pre>
<pre><code>// 主函数
void main(void)
{
    uchar data Aread[8] = {0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
    uchar status;
    sys_init();                 // 系统初始化：MCU-port UART-int fm17xx-spi

    while (1)
    {
        if (enable_Rfid)        // 按照上位机发送的命令和数据处理卡片
        {
            P1 = H5;
            P2 = table[0];      // 点亮数码管最低位，显示数字'0'
            rfid_deal(fm1702sl.cmd, fm1702sl.rev_Data);
            enable_Rfid = FALSE;
            fm1702sl.read_one = 1;
            sys_init();         // 系统重新初始化
        }
        else
        {
            // 只通过读写器读卡，没有使用上位机，只读一次卡的内容
            if (fm1702sl.read_one == 1)
            {
                disp();         // 如果读过一次，则显示卡余额
                if (fm1702sl.reading &gt; 20)
                {
                    status = rfid_card(_FM17XX_Request_);
                    if (status != 0)
                    {
                        fm1702sl.read_one = 0;
                    }
                    fm1702sl.reading = 0;
                }
            }
            //如果还没读卡，则发送 0x09 命令读取卡上信息
            else
            {
                ET0 = 0;
                rfid_deal(0x09, Aread);
                P1 = H5;
                P2 = table[0];
            }
        }
    }
}
</code></pre>
<h2>编译程序</h2>
<ol>
<li>在 Keil C51 中打开工程<code>RFID.uvproj</code></li>
</ol>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/BuildGuidePic/BuildC51Project_01.png" alt="" /></p>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/BuildGuidePic/BuildC51Project_02.png" alt="" /></p>
<ol>
<li>打开 <code>Target Options...</code>，转到<code>Output</code>，确认勾选<code>Create HEX File</code>并确认，以在编译时输出<code>hex 文件</code></li>
</ol>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/BuildGuidePic/BuildC51Project_03.png" alt="" /></p>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/BuildGuidePic/BuildC51Project_04.png" alt="" /></p>
<ol>
<li>点击编译按钮，或使用快捷键<code>F7</code>开始编译，编译输出窗口中显示<code>Creating hex file from "..OutputRFID"...</code>及<code>0 Error(s)</code>，即表示编译输出成功</li>
</ol>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/BuildGuidePic/BuildC51Project_05.png" alt="" /></p>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/BuildGuidePic/BuildC51Project_06.png" alt="" /></p>
<h2>下载程序</h2>
<ol>
<li>
<p>使用<code>Prolific PL2303 RS232 - USB 接口转换器</code>连接主板与计算机，打开<code>设备管理器</code>检查端口号并记录备用</p>
<ul>
<li><strong>注意</strong>：如显示设备未正确识别，则需先下载并安装驱动程序：<a href="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/PL2303_Prolific_DriverInstaller_v1160.exe"><code>PL2303_Prolific_DriverInstaller_v1160.exe</code></a></li>
</ul>
</li>
<li>
<p>启动<a href="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/stc-isp-15xx-v6.87F.zip"><code>STC-ISP</code></a>，单片机型号选择<code>STC11F60XE系列 - STC11F32XE</code>，端口号选择步骤 1 中记录的端口号，点击打开程序文件，选择到工程根目录下的<code>OutputRFID.hex</code>，点击<code>下载/编程</code>，此时关闭并开启单片机电源，即可开始下载程序。</p>
</li>
</ol>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/BuildGuidePic/BuildC51Project_07.png" alt="" /></p>
<h2>功能测试</h2>
<p>启动专用调试工具，选择上一节步骤 1 中记录的端口号，打开串口，即可测试充值、消费、余额查询等功能。执行各项操作时，也可以观察实际收发的指令信息。</p>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/BuildGuidePic/BuildC51Project_08.png" alt="" /></p>
<h2>参考</h2>
<ul>
<li>
<p>[1] <a href="https://blog.csdn.net/liucheng5037/article/details/46934147">Keil 编译后生成的 M51 文件解析 - 追梦的魂影 - CSDN</a></p>
</li>
<li>
<p>[2] <a href="https://www.silabs.com/community/mcu/8-bit/knowledge-base.entry.html/2015/12/24/startup_a51_-oGxz">STARTUP.A51是什么文件，如何使用？ - Silicon Labs</a></p>
</li>
</ul>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="物联网"/>
  </entry>
  <entry>
    <title>RFID 与 RFID 系统概述</title>
    <link href="https://gehongyan.cn/iot/rfid-summary/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/iot/rfid-summary/</id>
    <published>2020-09-20T00:00:00.000Z</published>
    <updated>2020-09-20T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h2>射频识别技术</h2>
<p><strong>射频识别</strong>（英语：<strong>R</strong>adio <strong>F</strong>requency <strong>ID</strong>entification，缩写：<strong>RFID</strong>）是一种无线通信技术，可以通过无线电信号识别特定目标并读写相关数据，而无需识别系统与特定目标之间建立机械或者光学接触。</p>
<hr />
<h2>RFID 系统的基本构成</h2>
<h3>RFID 系统概述</h3>
<h4>RFID 系统的基本组成</h4>
<p><strong>RFID 系统</strong>是由电子标签、读写器和系统高层构成。RFID系统是一种非接触式的自动识别系统，它通过射频无线信号自动识别目标对象，并获取相关数据。RFID系统一般由以下三部分组成：</p>
<ul>
<li><strong>电子标签</strong>：附着在物体上，用来标识物体，由芯片和天线组成，可以存储数据与收发射频无线信号，具有唯一的电子编码，存储物体的相关信息。</li>
<li><strong>读写器</strong>：用来读写物体的信息，电子标签与读写提通过射频无线信号传递信息。工作时一般先由读写器发射特定的询问信号；电子标签接收到该信号后作出应答，应答信号中含有电子标签携带的数据信息；读写器接收到应答信号后将其传输给系统高层。</li>
<li><strong>系统高层</strong>：管理多个电子标签与多个读写器之间的数据处理、交换、传输、存储过程。</li>
</ul>
<p>RFID 系统的结构框图如下图所示：</p>
<blockquote>
<p><img src="https://timgsa.baidu.com/timg?image&amp;quality=80&amp;size=b9999_10000&amp;sec=1600599806462&amp;di=1caa0d3df64c60c7d2a1170478819d4a&amp;imgtype=0&amp;src=http%3A%2F%2Fyzhtml01.book118.com%2F2016%2F11%2F24%2F08%2F44749300%2F4.files%2Ffile0001.png" alt="RFIDSystemStructure" /></p>
</blockquote>
<h4>RFID 系统的分类</h4>
<h5>按频率</h5>
<ol>
<li><strong>低频系统</strong>：30kHz ~ 300kHz，常见的有 125kHz 和 134.2kHz，用于短距传输、低数据量的场合。</li>
<li><strong>高频系统</strong>：3MHz ~ 30MHz，常见的有 6.75MHz 和 13.56MHz，用于短距传输、较大数据量的场合。</li>
<li><strong>微波系统</strong>：大于 300MHz，常见的有 433MHz、860/960MHz、2.45GHz 和 5.8GHz，用于多标签同时操作、较长距传输、高读写速度的场合。</li>
</ol>
<h5>按供电方式</h5>
<ol>
<li><strong>无源供电系统</strong>：标签内无电池，利用读写器发出的波束供电，用于短距传输、长寿命、工作环境要求低的场合。</li>
<li><strong>有源供电系统</strong>：标签内有电池，电池为电子标签提供全部能量，用于要求高可靠性、远距传输的场合，但寿命不长、体积较大、成本较高。</li>
<li><strong>半有源供电系统</strong>：标签内有电池，但电池仅对维持数据的电路及维持芯片工作电压的电路提供支持，电子标签不工作时休眠，当进度读写器范围内受射频信号激励时，标签进入工作状态，此时其能量主要来源于读写器的射频能量，内部电池主要用于弥补射频场强的不足。</li>
</ol>
<h5>按耦合方式</h5>
<ol>
<li><strong>电感耦合方式</strong>：读写器与标签之间的射频信号传递采用变压器模型，电磁能量通过空间高频交变磁场实现耦合，理论依据是法拉第电磁感应定律。分为密耦合系统和遥耦合系统。密耦合系统典型工作范围为 0 ~ 1cm，工作于 0 ~ 30MHz；遥耦合系统典型工作范围为 15cm ~ 1m，可工作于 135kHz、6.75MHz、13.56MHz 或 27.125MHz。</li>
<li><strong>电磁反向散射方式</strong>：读写器与电子标签之前的射频信号传递采用雷达模型，读写器发射出去的电磁波碰到电子标签后，电磁波被反射，同时携带回电子标签的额信息，理论依据是电磁波空间辐射原理。适用于微博系统，典型工作频率为 433MHz、860/960MHz、2.45GHz 和 5.8GHz，典型作用距离为 1m ~ 10m 甚至更远。</li>
</ol>
<h5>按技术方式</h5>
<ol>
<li><strong>主动广播式</strong>：电子标签主动向外发射信息，读写器相当于只收不发的接收机，电子标签采用有源工作</li>
<li><strong>被动倍频式</strong>：电子标签返回读写器的频率是读写器发射频率的 2 倍，读写器发射与接受占用两个不同的频点。</li>
<li><strong>被动反射调制式</strong>：读写器发射查询信号，电子标签被动接收，但电子标签返回读写器的频率于读写器发射的频率相同。</li>
</ol>
<h5>按保存信息方式分类</h5>
<ol>
<li><strong>只读电子标签</strong>：电子标签内部只有只读存储器（Read Only Memory，ROM），其信息在集成电路生产时以只读内存工艺注入，此后信息不能更改。</li>
<li><strong>一次写入电子标签</strong>：电子标签内部有 ROM 和随机存储器（Random Access Memory，RAM），这种电子标签可以写入一次数据，其标识信息可在标签制造过程中写入，也可由用户自己写入，但一旦写入就不能再更改。</li>
<li><strong>现场有线可改写电子标签</strong>：用户可通过访问电子标签的存储器进行读写操作。</li>
<li><strong>现场无线可改写电子标签</strong>：电子标签一般为有源类型，用户可通过特定的改写指令用无线方式改写信息。</li>
</ol>
<h5>但系统档次分类</h5>
<ol>
<li><strong>低档系统</strong>：一般为只读标签存储少量数据，电子标签功能简单、芯片体积小、功耗小、成本低、系统不能同时读取多个电子标签。</li>
<li><strong>中档系统</strong>：中档系统的电子标签数据存储量较大，数据可读可写，系统可提供更多功能。</li>
<li><strong>高档系统</strong>：一般带有密码功能，电子标签带有微处理器，可实现密码的复杂验证，系统功能完善。</li>
</ol>
<h5>按照工作方式分类</h5>
<ol>
<li><strong>全双工和半双工工作方式</strong>：全双工表示电子标签和读写器之间可以在同一时刻互相传送信息；半双工表示电子标签与读写器之间可以双向传送信息，但在同一时刻只能向同一个方向传送信息。</li>
<li><strong>时序工作方式</strong>：读写器辐射出的电磁场短时间周期性地断开，这些间隔倍电子标签识别出来，并被用于从电子标签到读写器的数据传输，这是一种典型的雷达工作方式。</li>
</ol>
<h3>电子标签</h3>
<h4>基本组成</h4>
<ol>
<li><strong>芯片</strong>：具有一定的存储容量，可以存储被识别物体的相关信息，对标签接收的信号进行解调、解码等各种处理，并把标签需要返回的信号进行编码、调制等各种处理。</li>
<li><strong>天线</strong>：收集读写器发射到空间的电磁波，并把标签本身的数据信号以电磁波的形式发射出去。</li>
</ol>
<h4>结构形式</h4>
<ol>
<li>
<p><strong>卡片型电子标签</strong> / <strong>射频卡</strong></p>
<ul>
<li>我国第二代身份证</li>
<li>城市一卡通</li>
<li>门禁卡</li>
<li>银行卡</li>
</ul>
</li>
<li>
<p><strong>标签类电子标签</strong></p>
<ul>
<li>具有粘贴功能的电子标签</li>
<li>车辆自动收费电子标签</li>
<li>钥匙扣型电子标签</li>
<li>扎带型电子标签</li>
<li>手表型 / 腕带型电子标签</li>
</ul>
</li>
<li>
<p><strong>植入式电子标签</strong> / <strong>皮下芯片植入</strong></p>
<ul>
<li>动物跟踪标签</li>
</ul>
</li>
</ol>
<h4>技术参数</h4>
<ul>
<li>
<p>标签机获得能量要求</p>
</li>
<li>
<p>标签信息的读写速度</p>
</li>
<li>
<p>标签信息的传输速率</p>
</li>
<li>
<p>标签信息的容量</p>
<ul>
<li>后台式电子标签</li>
<li>前台式电子标签</li>
</ul>
</li>
<li>
<p>标签的封装尺寸</p>
</li>
<li>
<p>标签的读写距离</p>
</li>
<li>
<p>标签的可靠性</p>
</li>
<li>
<p>标签的工作频率</p>
</li>
<li>
<p>标签的价格</p>
</li>
</ul>
<h4>功能模块</h4>
<ul>
<li>
<p>电源电路</p>
</li>
<li>
<p>时钟电路</p>
</li>
<li>
<p>数据输入和输出模块</p>
</li>
<li>
<p>存储器</p>
<ul>
<li>
<p>只读标签存储器（Read Only Memory，ROM）</p>
</li>
<li>
<p>一次写入标签存储器（Random Access Memory，RAM）</p>
</li>
<li>
<p>可读写标签存储器</p>
<ul>
<li>电可擦可编程只读存储器（Electrically Erasable Programmable Read-Only Memory，EEPROM）</li>
<li>具有静止存取功能的存储器（Static RAM，SRAM）</li>
<li>铁电存储器（Ferroelectric RAM，FRAM）</li>
</ul>
</li>
</ul>
</li>
<li>
<p>负载调制模块</p>
</li>
<li>
<p>天线</p>
<ul>
<li>低频天线</li>
<li>高频天线</li>
<li>微波天线</li>
</ul>
</li>
</ul>
<h4>封装</h4>
<ul>
<li>纸标签</li>
<li>塑料标签</li>
<li>玻璃标签</li>
</ul>
<h4>发展趋势</h4>
<ul>
<li>体积更小</li>
<li>成本更低</li>
<li>作用距离更远</li>
<li>无源可读写性能更加完善</li>
<li>适合高速移动物体的识别</li>
<li>多标签的读写功能</li>
<li>电磁场下自我保护功能更完善</li>
<li>智能性更强</li>
<li>加密特性更完善</li>
<li>带有其他附属功能的标签</li>
<li>具有杀死功能的标签</li>
<li>新的生产工艺</li>
<li>带有传感器功能</li>
</ul>
<h3>读写器</h3>
<h4>基本组成</h4>
<ol>
<li><strong>天线</strong>：将电磁波发射到空间，并收集电子标签的无线数据信号。</li>
<li><strong>射频模块</strong>：用于基带信号与射频信号的相互转换，并与天线相连。既可将频率很低的基带信号转换为射频信号，然后传输至天线；又可将天线接受地射频信号转换为频率很低的基带信号。</li>
<li><strong>控制模块</strong>：对发射信号进行编码、调制等各种处理，对接收信号进行解调、解码等各种处理；执行防碰撞算法；实现与后端应用程序的规范接口。</li>
</ol>
<h4>结构形式</h4>
<ul>
<li>固定式读写器</li>
<li>OEM（Original Equipment Manufacturer，原始设备制造商）模块式读写器</li>
<li>手持便携式读写器</li>
<li>身份证阅读器和银行卡读卡器</li>
<li>工业读写器</li>
<li>发卡器</li>
</ul>
<h4>工作特点</h4>
<ul>
<li>
<p>电子标签与读写器之间的通信</p>
</li>
<li>
<p>读写器与系统高层之间的通信</p>
</li>
<li>
<p>读写器的识别能力</p>
<ul>
<li>防碰撞识别能力</li>
<li>对移动物体的识别能力</li>
</ul>
</li>
<li>
<p>读写器对有源电子标签的管理</p>
</li>
<li>
<p>读写器的适应性</p>
</li>
<li>
<p>应用软件的控制作用</p>
</li>
</ul>
<h4>技术参数</h4>
<ul>
<li>工作频率</li>
<li>输出功率</li>
<li>输出接口</li>
<li>读写器类型</li>
<li>工作方式</li>
<li>读写器优先或电子标签优先</li>
</ul>
<h4>功能模块</h4>
<ul>
<li>
<p>射频模块</p>
</li>
<li>
<p>基带信号处理模块</p>
</li>
<li>
<p>智能模块</p>
</li>
<li>
<p>天线</p>
<ul>
<li>对称阵子天线</li>
<li>微带贴片天线</li>
<li>线圈天线</li>
<li>阵列天线</li>
<li>螺旋天线</li>
<li>八木天线</li>
</ul>
</li>
</ul>
<h4>发展趋势</h4>
<ul>
<li>
<p>兼容性</p>
</li>
<li>
<p>接口多样化</p>
</li>
<li>
<p>采用新技术</p>
<ul>
<li>智能天线</li>
<li>新的防碰撞算法 读写器管理技术</li>
</ul>
</li>
<li>
<p>模块化和标准化</p>
</li>
</ul>
<h3>系统高层</h3>
<p>将许多读写器获取的数据信息有效地整合起来，完成查询、管理与数据交换等功能。</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="物联网"/>
  </entry>
  <entry>
    <title>Keil 激活指南</title>
    <link href="https://gehongyan.cn/iot/keilcracking/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/iot/keilcracking/</id>
    <published>2020-09-17T00:00:00.000Z</published>
    <updated>2020-09-17T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h3>下载链接</h3>
<ul>
<li>
<p><a href="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/C51V901.exe">Keil 4 C51V901</a></p>
</li>
<li>
<p><a href="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/Keil%20V4%20Crack%20to%202032.7z">Keil 激活工具</a></p>
<ul>
<li>解压密码 <code>gehongyan.cn</code></li>
</ul>
</li>
</ul>
<hr />
<h3>如何激活 Keil</h3>
<ol>
<li>
<p>以管理员身份运行 <strong><code>Keil</code></strong></p>
</li>
<li>
<p>菜单栏依次点击 <strong><code>File</code></strong> - <strong><code>License Management...</code></strong></p>
</li>
</ol>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/guidePic/KeilCrackGuidePic01.png" alt="" /></p>
<ol>
<li>复制 <strong><code>License Management</code></strong> 窗口内右侧的 **<code>CID 值</code>**备用</li>
</ol>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/guidePic/KeilCrackGuidePic02.png" alt="" /></p>
<ol>
<li>
<p>解压并运行 Keil 激活工具</p>
<ul>
<li>
<p>**注意：**激活工具会被包括 Windows 安全中心在内的安全软件认定为危险程序，请对解压文件或解压路径添加白名单/排除项后再进行解压，也可临时关闭安全软件待解压并完成后续步骤后再重新开启</p>
</li>
<li>
<p><strong>指南：</strong><a href="https://support.microsoft.com/zh-cn/help/4028485/windows-10-add-an-exclusion-to-windows-security">将排除项添加到 Windows 安全中心</a></p>
<ul>
<li><a><strong>打开 Windows 安全中心</strong></a></li>
</ul>
</li>
</ul>
</li>
<li>
<p>将步骤 3 中复制的 **<code>CID 值</code>**粘贴在激活工具 **<code>CID 输入框</code>**中，点击 <strong><code>Generate</code></strong>，复制下方新生成的 30 位 <strong><code>Key</code></strong> 备用</p>
</li>
</ol>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/guidePic/KeilCrackGuidePic03.png" alt="" /></p>
<ol>
<li>将步骤 5 中复制的 <strong><code>Key</code></strong> 粘贴在 <strong><code>License Management</code></strong> 中的 <strong><code>New License ID Code (LIC)</code></strong> 输入框中，单击输入框右侧的 <strong><code>Add LIC</code></strong>，下方显示 <strong><code>*** LIC Added Successfully ***</code></strong>，<strong><code>Support Period</code></strong> 列中有 Expires 时间为 2032 年的信息即激活成功</li>
</ol>
<p><img src="https://oss-gexiaohe.gehongyan.cn/programs/Keil/V4/guidePic/KeilCrackGuidePic04.png" alt="" /></p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="物联网"/>
  </entry>
  <entry>
    <title>【曲谱】偶像に世界を委ねて ～ Idoratrize World</title>
    <link href="https://gehongyan.cn/gakufu/idoratrize-world/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/gakufu/idoratrize-world/</id>
    <published>2020-09-13T00:00:00.000Z</published>
    <updated>2020-09-13T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h3>偶像に世界を委ねて ～ Idoratrize World</h3>
<p>东方鬼形兽 埴安神袿姬 角色曲 まらしぃさん在2020/9/7的ニコ生演奏的版本</p>
<hr />
<p><strong>来源：</strong> 東方鬼形獣 ～ Wily Beast and Weakest Creature</p>
<p><strong>曲名：</strong> 偶像に世界を委ねて ～ Idoratrize World</p>
<p><strong>译名：</strong> 寄世界于偶像 ～ Idoratrize World</p>
<p><strong>作曲：</strong> ZUN</p>
<p><strong>编排：</strong> まらしぃ</p>
<hr />
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/%20albums/TH17_Cover-300x296.jpg" alt="TH17Cover" /></p>
<hr />
<p><a href="https://oss-piano.gehongyan.cn/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6%20%EF%BD%9E%20Idoratrize%20World.pdf">曲谱下载</a> | <a href="https://musescore.com/user/23409316/scores/6339830">Musescore</a> | <a href="https://www.bilibili.com/video/BV1oa4y1L7R1">视频预览</a> | <a href="https://live2.nicovideo.jp/watch/lv327953965">视频参考</a> | <a href="https://www.youtube.com/watch?v=4AYAw_L_R1U">本家</a></p>
<hr />
<p>::: collapse</p>
<ul>
<li>
<p>点击浏览曲谱图像</p>
<p>:::grid{columns="1" aspect="70/99" fit="contain"}
<img src="https://oss-piano.gehongyan.cn/pic/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6%20%EF%BD%9E%20Idoratrize%20World/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6_pic_1.jpg" alt="GuuZouPic01" />
<img src="https://oss-piano.gehongyan.cn/pic/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6%20%EF%BD%9E%20Idoratrize%20World/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6_pic_2.jpg" alt="GuuZouPic02" />
<img src="https://oss-piano.gehongyan.cn/pic/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6%20%EF%BD%9E%20Idoratrize%20World/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6_pic_3.jpg" alt="GuuZouPic03" />
<img src="https://oss-piano.gehongyan.cn/pic/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6%20%EF%BD%9E%20Idoratrize%20World/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6_pic_4.jpg" alt="GuuZouPic04" />
<img src="https://oss-piano.gehongyan.cn/pic/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6%20%EF%BD%9E%20Idoratrize%20World/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6_pic_5.jpg" alt="GuuZouPic05" />
<img src="https://oss-piano.gehongyan.cn/pic/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6%20%EF%BD%9E%20Idoratrize%20World/%E5%81%B6%E5%83%8F%E3%81%AB%E4%B8%96%E7%95%8C%E3%82%92%E5%A7%94%E3%81%AD%E3%81%A6_pic_6.jpg" alt="GuuZouPic06" />
:::</p>
</li>
</ul>
<hr />
<p><strong>哔哩哔哩视频</strong></p>
<p>::bilibili{bvid="BV1oa4y1L7R1" title="偶像に世界を委ねて ～ Idoratrize World" p="1"}</p>
<p><strong>YouTube</strong></p>
<p>::youtube{id="rtwXz8KaGoE" title="偶像に世界を委ねて ～ Idoratrize World"}</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="曲谱"/>
  </entry>
  <entry>
    <title>【曲谱】ドラマツルギー / 拟剧论</title>
    <link href="https://gehongyan.cn/gakufu/doramatsurugi/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/gakufu/doramatsurugi/</id>
    <published>2020-09-09T00:00:00.000Z</published>
    <updated>2020-09-09T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h3><strong>ドラマツルギー - Eve</strong></h3>
<p>拟剧论</p>
<hr />
<p><strong>曲名：</strong> ドラマツルギー
<strong>译名：</strong> 拟剧论
<strong>作曲：</strong> Eve
<strong>编排：</strong> まらしぃ</p>
<hr />
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/%20albums/Dramaturgy_Cover-300x300.jpg" alt="DramaturgyCover" /></p>
<hr />
<p><a href="https://oss-piano.gehongyan.cn/%E3%83%89%E3%83%A9%E3%83%9E%E3%83%84%E3%83%AB%E3%82%AE%E3%83%BC.pdf">曲谱下载</a> | <a href="https://musescore.com/user/23409316/scores/6333642">Musescore</a> | <a href="https://www.bilibili.com/video/BV1Y54y1C7Pa">视频预览</a> | <a href="https://www.bilibili.com/video/BV11i4y1u7Q9">视频参考</a> | <a href="https://www.youtube.com/watch?v=jJzw1h5CR-I">本家</a></p>
<hr />
<p>::: collapse</p>
<ul>
<li>
<p>点击浏览曲谱图像</p>
<p>:::grid{columns="1" aspect="70/99" fit="contain"}
<img src="https://oss-piano.gehongyan.cn/pic/%E3%83%89%E3%83%A9%E3%83%9E%E3%83%84%E3%83%AB%E3%82%AE%E3%83%BC/Dramaturgy_pic_01.jpg" alt="Dramaturgy_Pic_01" />
<img src="https://oss-piano.gehongyan.cn/pic/%E3%83%89%E3%83%A9%E3%83%9E%E3%83%84%E3%83%AB%E3%82%AE%E3%83%BC/Dramaturgy_pic_02.jpg" alt="Dramaturgy_Pic_02" />
<img src="https://oss-piano.gehongyan.cn/pic/%E3%83%89%E3%83%A9%E3%83%9E%E3%83%84%E3%83%AB%E3%82%AE%E3%83%BC/Dramaturgy_pic_03.jpg" alt="Dramaturgy_Pic_03" />
<img src="https://oss-piano.gehongyan.cn/pic/%E3%83%89%E3%83%A9%E3%83%9E%E3%83%84%E3%83%AB%E3%82%AE%E3%83%BC/Dramaturgy_pic_04.jpg" alt="Dramaturgy_Pic_04" />
:::</p>
</li>
</ul>
<hr />
<p><strong>哔哩哔哩视频</strong></p>
<p>::bilibili{bvid="BV1Y54y1C7Pa" title="ドラマツルギー / 拟剧论" p="1"}</p>
<p><strong>YouTube</strong></p>
<p>::youtube{id="s2wJl2D6uHU" title="ドラマツルギー / 拟剧论"}</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="曲谱"/>
  </entry>
  <entry>
    <title>【曲谱】ナイト・オブ・ナイツ 10周年 ver.</title>
    <link href="https://gehongyan.cn/gakufu/night-of-knights-10th/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/gakufu/night-of-knights-10th/</id>
    <published>2019-09-13T00:00:00.000Z</published>
    <updated>2019-09-13T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h3>ナイト・オブ・ナイツ 10周年 ver.</h3>
<p>Night of Knight 10 周年版本</p>
<hr />
<p><strong>作曲：</strong> ZUN</p>
<p><strong>编排：</strong> まらしぃ</p>
<hr />
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/%20albums/NightOfKnights10Anniversary.png" alt="NightOfKnights10AnniversaryCover" /></p>
<hr />
<p><a href="https://oss-piano.gehongyan.cn/%E3%83%8A%E3%82%A4%E3%83%88%E3%83%BB%E3%82%AA%E3%83%96%E3%83%BB%E3%83%8A%E3%82%A4%E3%83%84_10%E5%91%A8%E5%B9%B4_ver..pdf">曲谱下载</a> | <a href="https://www.bilibili.com/video/BV1pE411R7oh">视频预览</a> | <a href="https://youtu.be/axvjr6rSZa8">视频参考/本家</a></p>
<hr />
<p>::: collapse</p>
<ul>
<li>
<p>点击浏览曲谱图像</p>
<p>:::grid{columns="1" aspect="70/99" fit="contain"}
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_01.jpg" alt="NightOfKnights10Anniversary_Pic_01" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_02.jpg" alt="NightOfKnights10Anniversary_Pic_02" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_03.jpg" alt="NightOfKnights10Anniversary_Pic_03" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_04.jpg" alt="NightOfKnights10Anniversary_Pic_04" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_05.jpg" alt="NightOfKnights10Anniversary_Pic_05" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_06.jpg" alt="NightOfKnights10Anniversary_Pic_06" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_07.jpg" alt="NightOfKnights10Anniversary_Pic_07" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_08.jpg" alt="NightOfKnights10Anniversary_Pic_08" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_09.jpg" alt="NightOfKnights10Anniversary_Pic_09" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_10.jpg" alt="NightOfKnights10Anniversary_Pic_10" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_11.jpg" alt="NightOfKnights10Anniversary_Pic_11" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_12.jpg" alt="NightOfKnights10Anniversary_Pic_12" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_13.jpg" alt="NightOfKnights10Anniversary_Pic_13" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_14.jpg" alt="NightOfKnights10Anniversary_Pic_14" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_15.jpg" alt="NightOfKnights10Anniversary_Pic_15" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_16.jpg" alt="NightOfKnights10Anniversary_Pic_16" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_17.jpg" alt="NightOfKnights10Anniversary_Pic_17" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_18.jpg" alt="NightOfKnights10Anniversary_Pic_18" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_19.jpg" alt="NightOfKnights10Anniversary_Pic_19" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_20.jpg" alt="NightOfKnights10Anniversary_Pic_20" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_21.jpg" alt="NightOfKnights10Anniversary_Pic_21" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_22.jpg" alt="NightOfKnights10Anniversary_Pic_22" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_23.jpg" alt="NightOfKnights10Anniversary_Pic_23" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_24.jpg" alt="NightOfKnights10Anniversary_Pic_24" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_25.jpg" alt="NightOfKnights10Anniversary_Pic_25" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_26.jpg" alt="NightOfKnights10Anniversary_Pic_26" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_27.jpg" alt="NightOfKnights10Anniversary_Pic_27" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_28.jpg" alt="NightOfKnights10Anniversary_Pic_28" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_29.jpg" alt="NightOfKnights10Anniversary_Pic_29" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_30.jpg" alt="NightOfKnights10Anniversary_Pic_30" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_31.jpg" alt="NightOfKnights10Anniversary_Pic_31" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_32.jpg" alt="NightOfKnights10Anniversary_Pic_32" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_33.jpg" alt="NightOfKnights10Anniversary_Pic_33" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_34.jpg" alt="NightOfKnights10Anniversary_Pic_34" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_35.jpg" alt="NightOfKnights10Anniversary_Pic_35" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_36.jpg" alt="NightOfKnights10Anniversary_Pic_36" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_37.jpg" alt="NightOfKnights10Anniversary_Pic_37" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_38.jpg" alt="NightOfKnights10Anniversary_Pic_38" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_39.jpg" alt="NightOfKnights10Anniversary_Pic_39" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_40.jpg" alt="NightOfKnights10Anniversary_Pic_40" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_41.jpg" alt="NightOfKnights10Anniversary_Pic_41" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_42.jpg" alt="NightOfKnights10Anniversary_Pic_42" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_43.jpg" alt="NightOfKnights10Anniversary_Pic_43" />
<img src="https://oss-piano.gehongyan.cn/pic/NightOfKnights10Anniversary/NightOfKnights10Anniversary_Pic_44.jpg" alt="NightOfKnights10Anniversary_Pic_44" />
:::</p>
</li>
</ul>
<hr />
<p><strong>哔哩哔哩视频</strong></p>
<p>::bilibili{bvid="BV1pE411R7oh" title="ナイト・オブ・ナイツ 10周年 ver." p="1"}</p>
<p><strong>YouTube</strong></p>
<p>::youtube{id="_2Z1yNKN798" title="ナイト・オブ・ナイツ 10周年 ver."}</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="曲谱"/>
  </entry>
  <entry>
    <title>【曲谱】グランドピアノで東方の曲をメドレーにして弾いてみた2019</title>
    <link href="https://gehongyan.cn/gakufu/touhou-medley-2019/" rel="alternate" type="text/html"/>
    <id>https://gehongyan.cn/gakufu/touhou-medley-2019/</id>
    <published>2019-08-13T00:00:00.000Z</published>
    <updated>2019-08-13T00:00:00.000Z</updated>
    <summary></summary>
    <content type="html"><![CDATA[<h3>グランドピアノで東方の曲をメドレーにして弾いてみた2019（Toho medley）</h3>
<p>幻想遊戯＜雅＞3</p>
<hr />
<p><strong>作曲：</strong> ZUN</p>
<p><strong>编排：</strong> まらしぃ</p>
<hr />
<p><img src="https://oss-gexiaohe.gehongyan.cn/reference-pics/%20albums/TouhouMedley2019_Cover-300x169.png" alt="TouhouMedley2019Cover" /></p>
<hr />
<p><a href="https://piano-sheet.oss-cn-beijing.aliyuncs.com/%E6%9D%B1%E6%96%B9%E3%81%AE%E6%9B%B2%E3%82%92%E3%83%A1%E3%83%89%E3%83%AC%E3%83%BC%E3%81%AB%E3%81%97%E3%81%A6%E5%BC%BE%E3%81%84%E3%81%A6%E3%81%BF%E3%81%9F_2019.pdf">曲谱下载</a> | <a href="https://oss-piano.gehongyan.cn/midi/TouhouMedley2019.mid">MIDI 下载</a> | <a href="https://musescore.com/user/23409316/scores/5697014">Musescore</a> | <a href="https://www.bilibili.com/video/BV124411S78J">视频预览</a> | <a href="https://www.bilibili.com/video/av60881404">视频参考/本家</a></p>
<hr />
<p>::: collapse</p>
<ul>
<li>
<p>点击浏览曲谱图像</p>
<p>:::grid{columns="1" aspect="70/99" fit="contain"}
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_01.jpg" alt="TouhouMedley2019_Pic_01" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_02.jpg" alt="TouhouMedley2019_Pic_02" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_03.jpg" alt="TouhouMedley2019_Pic_03" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_04.jpg" alt="TouhouMedley2019_Pic_04" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_05.jpg" alt="TouhouMedley2019_Pic_05" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_06.jpg" alt="TouhouMedley2019_Pic_06" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_07.jpg" alt="TouhouMedley2019_Pic_07" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_08.jpg" alt="TouhouMedley2019_Pic_08" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_09.jpg" alt="TouhouMedley2019_Pic_09" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_10.jpg" alt="TouhouMedley2019_Pic_10" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_11.jpg" alt="TouhouMedley2019_Pic_11" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_12.jpg" alt="TouhouMedley2019_Pic_12" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_13.jpg" alt="TouhouMedley2019_Pic_13" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_14.jpg" alt="TouhouMedley2019_Pic_14" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_15.jpg" alt="TouhouMedley2019_Pic_15" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_16.jpg" alt="TouhouMedley2019_Pic_16" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_17.jpg" alt="TouhouMedley2019_Pic_17" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_18.jpg" alt="TouhouMedley2019_Pic_18" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_19.jpg" alt="TouhouMedley2019_Pic_19" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_20.jpg" alt="TouhouMedley2019_Pic_20" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_21.jpg" alt="TouhouMedley2019_Pic_21" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_22.jpg" alt="TouhouMedley2019_Pic_22" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_23.jpg" alt="TouhouMedley2019_Pic_23" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_24.jpg" alt="TouhouMedley2019_Pic_24" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_25.jpg" alt="TouhouMedley2019_Pic_25" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_26.jpg" alt="TouhouMedley2019_Pic_26" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_27.jpg" alt="TouhouMedley2019_Pic_27" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_28.jpg" alt="TouhouMedley2019_Pic_28" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_29.jpg" alt="TouhouMedley2019_Pic_29" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_30.jpg" alt="TouhouMedley2019_Pic_30" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_31.jpg" alt="TouhouMedley2019_Pic_31" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_32.jpg" alt="TouhouMedley2019_Pic_32" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_33.jpg" alt="TouhouMedley2019_Pic_33" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_34.jpg" alt="TouhouMedley2019_Pic_34" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_35.jpg" alt="TouhouMedley2019_Pic_35" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_36.jpg" alt="TouhouMedley2019_Pic_36" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_37.jpg" alt="TouhouMedley2019_Pic_37" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_38.jpg" alt="TouhouMedley2019_Pic_38" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_39.jpg" alt="TouhouMedley2019_Pic_39" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_40.jpg" alt="TouhouMedley2019_Pic_40" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_41.jpg" alt="TouhouMedley2019_Pic_41" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_42.jpg" alt="TouhouMedley2019_Pic_42" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_43.jpg" alt="TouhouMedley2019_Pic_43" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_44.jpg" alt="TouhouMedley2019_Pic_44" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_45.jpg" alt="TouhouMedley2019_Pic_45" />
<img src="https://oss-piano.gehongyan.cn/pic/TohoMedley2019/TouhouMedley2019_Pic_46.jpg" alt="TouhouMedley2019_Pic_46" />
:::</p>
</li>
</ul>
<hr />
<p><strong>哔哩哔哩视频</strong></p>
<p>::bilibili{bvid="BV124411S78J" title="グランドピアノで東方の曲をメドレーにして弾いてみた2019" p="1"}</p>
<p><strong>YouTube</strong></p>
<p>::youtube{id="o7SsOBV5MaM" title="グランドピアノで東方の曲をメドレーにして弾いてみた2019"}</p>
]]></content>
    <author><name>戈小荷</name></author>
    <category term="曲谱"/>
  </entry>
</feed>
