SegmentFault 程序开发最新的文章
2024-03-20T15:55:49+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
理解RBAC授权
https://segmentfault.com/a/1190000044731414
2024-03-20T15:55:49+08:00
2024-03-20T15:55:49+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
0
<blockquote><a href="https://link.segmentfault.com/?enc=r4ArIymJ0%2Fqd2G0%2FH7Dfng%3D%3D.PL9sm3ueGymJK51A3vLl747Fa1UMDEznx2zujJwF9aXpeiVOQY1PtAD%2F5MFHDbuQj%2F%2B3uHKGfqyCin3E7AxC2g%3D%3D" rel="nofollow">基于角色的访问控制(RBAC)</a>是围绕<strong>角色</strong>和<strong>权限</strong>定义的策略中立的访问控制机制。RBAC 的组件(例如<strong>角色权限</strong>、<strong>用户角色</strong>和角色角色关系)使执行用户分配变得简单。</blockquote><p>在本文中,我将分享一些有关 RBAC 和 ABAC 的信息。<br><!--more--><br>RBAC的基本思想是将权限管理与用户分离,降低管理复杂度,提供更高的灵活性和安全性。通过使用角色作为中间层,管理员可以更轻松地管理用户权限,而不必关注每个用户的权限设置。</p><h2>术语</h2><p>RBAC中有3个术语:</p><ul><li>角色</li><li>权限</li><li>用户</li></ul><p><strong>角色</strong></p><ul><li>角色是一个抽象概念,定义一组相关的权限。例如,一个系统中有一些角色:管理员、普通用户、访客,每个角色都有相关的权限。</li><li>角色通过给用户分配权限,赋予用户相应的访问权限。</li><li>在某些实现中,角色可以形成层次结构以提供更高的灵活性。</li></ul><p><strong>权限</strong></p><p>权限是执行特定操作或访问特定资源的能力。</p><ul><li>权限描述了访问系统中的资源或操作的要求。</li><li>有两种定义权限的方法:允许和拒绝。允许权限允许用户访问资源或执行操作,就像白名单一样;拒绝权限拒绝用户访问资源或执行操作,就像黑名单一样。</li></ul><p><strong>用户</strong></p><p>用户是系统中的一个身份,可以为其分配不同的角色以授予不同的权限。</p><h2>实现</h2><p>在系统中实现 RBAC 很简单。在接下来的内容中,我将提供一个简单的实现。</p><h3>数据表定义</h3><p><strong>roles</strong></p><p>表示系统存在的角色列表。</p><table><thead><tr><th>列名</th><th>柱型</th><th>描述</th></tr></thead><tbody><tr><td>ID</td><td>int</td><td>角色ID</td></tr><tr><td>name</td><td>varchar(40)</td><td>角色名称</td></tr><tr><td>desc</td><td>varchar(128)</td><td>角色简要描述</td></tr><tr><td>enabled</td><td>boolean</td><td>角色是否启用</td></tr><tr><td>created_at</td><td>Datetime</td><td>创建时间</td></tr></tbody></table><p><strong>permissions</strong></p><p>表示系统存在的权限列表。</p><table><thead><tr><th>列名</th><th>柱型</th><th>描述</th></tr></thead><tbody><tr><td>ID</td><td>int</td><td>权限ID</td></tr><tr><td>name</td><td>varchar(40)</td><td>权限名称</td></tr><tr><td>desc</td><td>varchar(128)</td><td>权限的简要说明</td></tr><tr><td>created_at</td><td>datetime</td><td>创建时间</td></tr></tbody></table><p><strong>role_permissions</strong></p><p>表示特定角色拥有哪些权限。</p><table><thead><tr><th>列名</th><th>柱型</th><th>描述</th></tr></thead><tbody><tr><td>role_id</td><td>整数</td><td>角色ID</td></tr><tr><td>permission_id</td><td>整数</td><td>权限ID</td></tr></tbody></table><p><strong>user</strong></p><p>用户代表有权登录本系统的身份。</p><table><thead><tr><th>列名</th><th>柱型</th><th>描述</th></tr></thead><tbody><tr><td>ID</td><td>Int</td><td>用户ID</td></tr><tr><td>username</td><td>varchar(20)</td><td>-</td></tr><tr><td>password</td><td>varchar(255)</td><td>哈希密码</td></tr><tr><td>name</td><td>varchar(20)</td><td>-</td></tr><tr><td>created_at</td><td>datetime</td><td>创建时间</td></tr></tbody></table><p><strong>user_roles</strong></p><p>表格表示特定用户具有的角色。</p><table><thead><tr><th>列名</th><th>柱型</th><th>描述</th></tr></thead><tbody><tr><td>user_id</td><td>int</td><td>用户ID</td></tr><tr><td>role_id</td><td>Int</td><td>角色ID</td></tr></tbody></table><h3>示例数据</h3><p>如下是一个CRM的RBAC数据示例。</p><p><strong>用户表</strong></p><table><thead><tr><th>id</th><th>username</th><th>password</th><th>name</th><th>created_at</th></tr></thead><tbody><tr><td>1</td><td>staff1</td><td>-</td><td>staff1</td><td>2023-01-01 10:00:00</td></tr><tr><td>2</td><td>manager1</td><td>-</td><td>manager1</td><td>2023-01-01 10:00:00</td></tr></tbody></table><p><strong>角色表</strong></p><table><thead><tr><th>id</th><th>name</th><th>description</th><th>enabled</th><th>created_at</th></tr></thead><tbody><tr><td>1</td><td>salesman</td><td>-</td><td>true</td><td>2023-01-01 10:00:00</td></tr><tr><td>2</td><td>manager</td><td>-</td><td>true</td><td>2023-01-01 10:00:00</td></tr></tbody></table><p><strong>权限表</strong></p><table><thead><tr><th>id</th><th>name</th><th>description</th><th>created_at</th></tr></thead><tbody><tr><td>1</td><td>customer:create</td><td>-</td><td>2023-01-01 10:00:00</td></tr><tr><td>2</td><td>customer:update</td><td>-</td><td>2023-01-01 10:00:00</td></tr><tr><td>3</td><td>customer:delete</td><td>-</td><td>2023-01-01 10:00:00</td></tr><tr><td>4</td><td>customer:view</td><td>-</td><td>2023-01-01 10:00:00</td></tr></tbody></table><p><strong>角色-权限关联表</strong></p><p>The following data shows the salesman allows create/update/view a customer and the manager has the full access.</p><table><thead><tr><th>role_id</th><th>permission_id</th></tr></thead><tbody><tr><td>1</td><td>1</td></tr><tr><td>1</td><td>2</td></tr><tr><td>1</td><td>4</td></tr><tr><td>2</td><td>1</td></tr><tr><td>2</td><td>2</td></tr><tr><td>2</td><td>3</td></tr><tr><td>2</td><td>4</td></tr></tbody></table><p><strong>用户角色表</strong></p><table><thead><tr><th>user_id</th><th>role_id</th></tr></thead><tbody><tr><td>1</td><td>1</td></tr><tr><td>2</td><td>2</td></tr></tbody></table><h3>时序图</h3><p>下图显示了用户登录并创建客户的示例。</p><pre><code class="mermaid">sequenceDiagram
User->>+LoginService: Logged in
LoginService->>+Database: Query user by username and password
Database-->>-LoginService: An user or empty
LoginService->>+Database: Query roles and permissions of current user
Database-->>-LoginService: Roles and permissions list
LoginService-->>+LoginService: add roles and permissions to session.
LoginService-->>-LoginService: add completed
LoginService-->>-User: Logged in
User-->>+CustomerService: create customer
CustomerService-->>+CustomerService: check whether there is a 'customer:create' permission in session
CustomerService-->>-CustomerService: found permission, allow operation
CustomerService-->>+Database: create a customer
Database-->>-CustomerService: create completed
CustomerService-->>-User: create succeed</code></pre><h2>结论</h2><p>基于角色的访问控制(RBAC)在访问控制领域具有重要的应用价值。RBAC通过将权限与角色关联起来,实现了结构化的访问控制管理方法。RBAC的实施可以提高管理效率,增强系统安全性。</p><h2>参考</h2><ul><li><a href="https://link.segmentfault.com/?enc=H1SzahhkDrMx2YlS2r2T6w%3D%3D.PKukp8G6Lp1rdzSHd5vL08yBJafvroG9l4gN%2BuTHaDwCc2eu4lTmfP1kl%2BNk929%2FItcj7uZXuP0HLf4uzN4j1A%3D%3D" rel="nofollow">基于角色的访问控制</a></li></ul><p><img width="723" height="211" src="/img/bVdbQQu" alt="扫码_搜索联合传播样式-标准色版 (1).png" title="扫码_搜索联合传播样式-标准色版 (1).png"></p>
理解MySQL InnoDB 中的 MVCC机制
https://segmentfault.com/a/1190000044698026
2024-03-10T12:37:11+08:00
2024-03-10T12:37:11+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
1
<p>在MySQL中,MVCC(多版本并发控制)是指InnoDB存储引擎使用的并发控制机制。 它提供对数据的并发访问,并确保多用户环境中数据的一致性和隔离性。</p><p>InnoDB通过“Undo log”存储每条记录的多个版本,提供历史记录供读取,并允许不同的事务访问不同的数据版本。 在事务期间,客户端只能看到当前事务开始之前提交的记录以及当前事务内所做的修改。</p><h2>隔离级别</h2><p>MySQL中有4种隔离级别:</p><p><strong>READ UNCOMMITED</strong></p><p>当前事务可以读取未提交的数据。 这些数据可能会回滚,所以我们将未提交的数据称为脏数据,这种问题称为<strong>脏读</strong>。</p><p><strong>READ COMMITED</strong></p><p>当前事务确实可以读取已提交的数据,因此不存在脏读问题。 但是,如果当前事务多次读取同一条记录,则可能检索到不同的数据。 这是因为,在当前事务期间,可能有其他事务修改并提交了该记录。 此问题称为<strong>不可重复读取</strong>。</p><p><strong>REPETABLE READ</strong></p><p>事务多次读取特定的记录集,即使其他事务在事务过程中修改或提交对这些记录的更改,它也始终会获得这些记录的相同值。 但是,如果我们在当前事务中多次执行“SELECT COUNT(*) FROM {table_name}”,我们可能会看到不同的结果,这个问题称为<strong>幻读</strong>。</p><p><em>REPEATABLE READ 是 MySQL InnoDB 中的默认隔离级别。</em></p><p><strong>SERIALIZABLE</strong></p><p>所有事务都强制排序,解决了脏读、不可重复读、幻读等问题。 但Serialized隔离级别性能较差,因此在实际中很少使用。</p><p><em>MVCC 仅适用于 <strong>READ COMMITTED</strong> 和 <strong>REPEATABLE READ</strong> 隔离级别,因为 <strong>READ UNCOMMITTED</strong> 始终读取最新记录,而 <strong>SERIALIZABLE</strong> 会为其读取的所有记录添加锁。</em></p><h2>概念</h2><p>让我们探讨一下 MVCC 中的一些概念。 之后,我们将了解 MVCC 的工作原理。</p><p><strong>TRANSACTION ID</strong></p><p>当一个新的事务开始时,它会得到一个自增的事务ID,通过它InnoDB可以知道每个事务的执行顺序。</p><p><strong>隐藏列</strong></p><p>InnoDB中的每条记录都有两个隐藏列“db_trx_id”和“db_roll_pointer”,如果表中没有主键或非空唯一键,InnoDB将生成一个隐藏的自增列“db_row_id”。</p><table><thead><tr><th>字段名称</th><th>必填</th><th>描述</th></tr></thead><tbody><tr><td>db_trx_id</td><td>Y</td><td>记录操作该行的事务的事务id</td></tr><tr><td>db_roll_pointer</td><td>Y</td><td>undo指针,指向该行的undo日志</td></tr></tbody></table><p><em>InnoDB 在撤消日志中记录“插入”、“更新”和“删除”操作。 然而,对于“删除”操作,InnoDB实际上将其记录为“更新”操作,即通常所说的“软删除”。 InnoDB 不是物理删除该行,而是更新“已删除标志”来指示该行已被逻辑删除。 此方法允许检索该行的先前版本,如果该行被永久删除,则无法检索先前版本。</em></p><p><strong>当前读</strong></p><p>一些 SQL 语句,例如“SELECT <em> ... LOCK IN SHARE MODE(共享锁)”、“SELECT </em> ... FOR UPDATE(独占锁)”、“UPDATE”、“DELETE”和“INSERT”, 考虑当前的读取操作。 这些操作读取该行的最新版本。 在读取过程中,InnoDB通过对当前记录加锁来确保没有其他事务可以修改当前记录。</p><p><strong>快照读</strong></p><p>没有加锁的 SELECT 语句被视为快照读取操作,通过 MVCC 读取所需的版本。 快照读取无锁,有效提升事务性能。</p><p>本质上,快照是一种以空间换取时间的方式。</p><p><strong>Undo log</strong></p><p>Undo log存储修改行的先前版本。 在行被修改之前,InnoDB会将当前版本复制到Undo log中,Undo log具有以下功能:</p><ul><li>如果事务回滚,InnoDB可以找到以前的版本来恢复。</li><li>如果当前版本对于当前事务不可见,则会通过undo log查找可见版本。</li></ul><p>前面提到,InnoDB将删除操作记录为更新操作,因此Undo Log中只有两种操作:</p><ul><li>Insert undo log:由insert操作产生,仅用于事务回滚,事务提交后可立即丢弃。</li><li>更新undo log:由更新操作生成,不仅用于事务回滚,还用于快照读取。</li></ul><p><strong>版本链</strong></p><p>当多个事务同时操作同一条记录时,每个事务都会生成一个新的版本,这些版本通过 db_roll_pointer 形成一个链表,称为版本链。</p><p><img width="570" height="359" src="/img/bVdbH91" alt="image.png" title="image.png"></p><p><strong>Read View</strong></p><p>ReadView是事务执行快照读取时生成的记录快照。</p><p>读取视图存储当前事务开始之前的所有活动事务。 有 4 个重要属性:</p><ul><li>trx_ids:活动事务ID(不包括当前事务和已提交事务)。</li><li>low_limit_id:分配的下一个交易ID。</li><li>up_limit_id:trx_ids中的最小交易id,如果trx_ids为空,则up_limit_id等于low_limit_id。</li><li>Creator_trx_id:生成读取视图的事务ID。</li></ul><p>以下规则用于检查记录是否对当前事务可见:</p><ul><li>如果访问版本的事务ID=creator_trx_id,则表示当前事务访问了自己修改过的记录,则该版本对当前事务可见。</li><li>如果访问版本的事务ID < up_limit_id,则说明生成该版本的事务在当前事务生成ReadView之前已经提交,因此该版本可以被当前事务访问。</li><li>如果访问版本的事务ID > low_limit_id 值,则说明生成该版本的事务是在当前事务生成ReadView之后打开的,因此当前事务无法访问该版本。</li><li>如果访问版本的事务ID在up_limit_id和m_low_limit_id之间,则需要判断该版本的事务ID是否在trx_ids列表中。 如果是,说明该版本生成的事务在ReadView创建时仍然处于活动状态,该版本无法访问。</li><li>如果没有,说明创建ReadView时生成该版本的事务已经提交,可以访问该版本。</li></ul><h2>MVCC实现原理</h2><p>当我们理解了关键概念后,MVCC的实现就非常简单了。</p><h3>查询流程</h3><ol><li>获取交易自己的交易ID,称为trx_id。 (这不是在 SELECT 语句期间获得的,而是在事务开始时获得的,即执行 BEGIN 时获得的。)</li><li>检索 ReadView(仅在 SELECT 语句期间生成)。</li><li>在数据库表中,如果找到数据,则将其与ReadView中的事务版本号进行比较。<br>4、如果不符合ReadView的可见性规则,则需要Undo Log中的历史快照,直到返回符合规则的数据。</li></ol><p>InnoDB通过ReadView和Undo Log的结合来实现MVCC。 Undo Log 存储历史快照,而 ReadView 的可见性规则有助于确定当前版本数据的可见性。</p><h3>已提交读和可重复读之间的区别</h3><p>提交读 (RC) 和可重复读 (RR) 隔离级别之间的唯一区别是,<strong>在 RC 中,为每个 SELECT 语句创建一个新的 ReadView</strong>,而在 RR 中,仅为第一个 SELECT 创建 ReadView 事务中的语句。</p><h2>结论</h2><p>综上所述,MySQL的MVCC机制,由InnoDB存储引擎实现,提供了多用户环境下的并发控制和数据一致性。 它利用事务 ID、隐藏列、Read View、Undo log和版本链来管理对数据的并发访问。 已提交读隔离级别和可重复读隔离级别的不同之处在于创建 ReadView 的方式不同,已提交读隔离级别为每个 SELECT 语句创建一个新的 ReadView,而可重复读隔离级别仅为事务中的第一个 SELECT 语句创建新的 ReadView。 MVCC 确保事务隔离并允许在 MySQL 中进行一致且高效的读取操作。</p>
PlanUML指南
https://segmentfault.com/a/1190000040122348
2021-06-04T16:32:43+08:00
2021-06-04T16:32:43+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
3
<h2>简介</h2><blockquote><strong>统一建模语言</strong>(英语:Unified Modeling Language,缩写 UML)是非专利的第三代<a href="https://link.segmentfault.com/?enc=lsS84MdL9NFltEVU14Xlsg%3D%3D.V%2FlG7IfLcrfPHHRYQlWGQlurxK%2FdGBWuaDHya77RNqZSeSQZ6zUaYSk0f8m5oa1HYfY22zYacKLobB2gMfeAL57GLCI2e7Obk6%2Bb69%2FsaQMGj8Elvqn3gzmNvHx90N%2Fm75koodifFaETgp1Ud0bY%2Ff4DoiMyzsRuE1m3%2F4W9p%2BM%3D" rel="nofollow">建模</a>和<a href="https://link.segmentfault.com/?enc=7Nwi5WEtaLloKM6UWwDuXA%3D%3D.X%2BNzpnAM4q6w4pdAW82Py%2Bx1TLkcq9%2BsnpOinUiIIvu5qVA3eprl5oSjNtauuIhFf%2FUcAf8Q85iRftaFdIfV5i0Ql%2FVJhl8iAbgaw67nt%2B4%3D" rel="nofollow">规约语言</a>。UML是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开放方法</blockquote><p>编写UML的软件很多,但是基本是可视化的,需要手动编写,本文主要介绍基于文本的UML编写工具——PlantUML。</p><h2>安装</h2><p>PlantUML有以下依赖:</p><ol><li>graphviz</li><li>jdk</li><li>Jetbrains IDE插件(可选,本文推荐)</li></ol><h3>安装graphviz</h3><p>本文使用<code>Homebrew</code>安装<code>graphviz</code>,终端执行以下命令安装<code>graphviz</code>。</p><pre><code class="bash">brew install graphviz</code></pre><p>安装完毕后查看版本信息。</p><pre><code class="bash">dot -v</code></pre><p>输出如下:</p><pre><code>dot - graphviz version 2.47.0 (20210316.0004)
libdir = "/usr/local/Cellar/graphviz/2.47.0/lib/graphviz"
Activated plugin library: libgvplugin_dot_layout.6.dylib
Using layout: dot:dot_layout
Activated plugin library: libgvplugin_core.6.dylib
Using render: dot:core
Using device: dot:dot:core
The plugin configuration file:
/usr/local/Cellar/graphviz/2.47.0/lib/graphviz/config6
was successfully loaded.
render : cairo dot dot_json fig gd json json0 map mp pic pov ps quartz svg tk visio vml vrml xdot xdot_json
layout : circo dot fdp neato nop nop1 nop2 osage patchwork sfdp twopi
textlayout : textlayout
device : bmp canon cgimage cmap cmapx cmapx_np dot dot_json eps exr fig gd gd2 gif gv icns ico imap imap_np ismap jp2 jpe jpeg jpg json json0 mp pct pdf pic pict plain plain-ext png pov ps ps2 psd sgi svg svgz tga tif tiff tk vdx vml vmlz vrml wbmp webp xdot xdot1.2 xdot1.4 xdot_json
loadimage : (lib) bmp eps gd gd2 gif jpe jpeg jpg pdf png ps svg webp xbm</code></pre><h3>jdk</h3><p>本文使用<code>Homebrew</code>安装<code>openjdk</code>即可,终端执行以下命令安装<code>openjdk</code>。</p><pre><code class="bash">brew install openjdk</code></pre><p>安装完毕后查看版本信息。</p><pre><code class="bash">java -version</code></pre><p>输出如下:</p><pre><code>openjdk version "11.0.10" 2021-01-19
OpenJDK Runtime Environment (build 11.0.10+9)
OpenJDK 64-Bit Server VM (build 11.0.10+9, mixed mode)</code></pre><h3>Jetbrains IDE插件安装</h3><p>本文以<code>Goland</code>为例。</p><ol><li>打开IDE设置,打开<code>Plugins</code>窗口,搜索<code>PlantUML integration</code></li><li>安装完毕后重启IDE</li><li><p>打开IDE设置,搜索<code>plantuml</code>,确保<code>Remote rendering</code>已关闭</p><p><img src="/img/remote/1460000040122350" alt="image-20210604154007098" title="image-20210604154007098"></p></li></ol><h2>PlantUML语法</h2><p>以最常用的时序图、类图、流程图、组件图举例。</p><h3>时序图</h3><h4>Get Started</h4><ol><li><p>IDE新建一个空项目,打开项目之后,右键新建文件</p><p><img src="/img/remote/1460000040122351" alt="image-20210604152912197" title="image-20210604152912197"></p></li><li><p>选择<code>Sequence</code></p><p><img src="/img/remote/1460000040122352" alt="image-20210604152935751" title="image-20210604152935751"></p></li><li><p>PlantUML菜单项说明</p><p><img src="/img/remote/1460000040122353" alt="image-20210604155009752" title="image-20210604155009752"></p></li><li><p>以微信网页授权为例编写时序图。</p><pre><code>@startuml
'https://plantuml.com/sequence-diagram
'启用自动编号
autonumber
'生命线自动激活
autoactivate on
actor 用户
用户 -> 应用服务器: 获取用户信息
应用服务器 -> 微信服务器: 跳转授权链接:(appid,scope,callback)
微信服务器 -> 用户: 请求用户授权
return 允许授权
return 返回授权code
应用服务器 -> 微信服务器: 获取用户access_token(appid,secret,code)
return 返回access_token+openid
应用服务器 -> 微信服务器: 获取用户信息(openid,access_token)
return 用户信息
return 用户信息
@enduml</code></pre></li><li><p>渲染效果</p><p><img src="/img/remote/1460000040122354" alt="image-20210604153910674" title="image-20210604153910674"></p></li></ol><h4>语法说明</h4><h5>标记声明</h5><pre><code>@startuml和@enduml是PlantUML的开始结束标记,无需更改。
autonumber 打开启动编号,也就是每个步骤之前都有数字编号,打开之后整个流程更加清晰
autoactivate on 打开生命线自动激活,需要配合`return`使用
actor 用户 声明`用户`的类型是actor(行为人)</code></pre><h5>时序声明</h5><ul><li>使用<code>-></code>来声明一个时序操作,<code>:</code>后面可以附加消息</li><li>使用<code>return</code>来返回消息给调用者</li></ul><h5>声明参与者</h5><p>默认情况下参与者为矩形,无法看出实际类型。实际应用中,会有数据库、消息队列等等参与者,使用以下关键字来改变参与者的图例。</p><pre><code>actor 用户
database 数据库
queue 消息队列</code></pre><p><img src="/img/remote/1460000040122355" alt="image-20210604154840162" title="image-20210604154840162"></p><h3>类图</h3><p>类图是UML中非常重要的一种类型,能够在实际编码之前为我们提供OOP的详细设计。</p><h4>Get Started</h4><ol><li><p>IDE新建一个空项目,打开项目之后,右键新建文件</p><p><img src="/img/remote/1460000040122351" alt="image-20210604152912197" title="image-20210604152912197"></p></li><li><p>选择<code>Class</code></p><p><img src="/img/remote/1460000040122356" alt="image-20210604155301322" title="image-20210604155301322"></p></li><li><p>以一个上传类为例编写类图</p><pre><code>@startuml
'https://plantuml.com/class-diagram
namespace com.ddhigh.uploader {
interface Uploader {
+ String Upload(String filename) Throws IOException
}
namespace qiniu {
class QiniuUploader implements Uploader {
- client: qiniu.Client
--
+ String Upload(String filename) Throws IOException
}
QiniuUploader *-- qiniu.Client
}
package aliyun {
class AliyunUploader implements Uploader {
- client: aliyun.Client
--
+ String Upload(String filename) Throws IOException
}
AliyunUploader *-- aliyun.Client
}
class UploaderFacade {
- uploaders: List<Uploader>
--
+ List<String> Upload(String filename) Throws IOException
}
UploaderFacade o-- Uploader
}
@enduml</code></pre></li><li><p>渲染效果</p><p><img src="/img/remote/1460000040122357" alt="image-20210604155821009" title="image-20210604155821009"></p></li></ol><h5>语法说明</h5><h5>包</h5><p>建议使用<code>namespace</code>关键字声明包,<code>package</code>声明的包内的类名必须全局唯一(无视package),而<code>namespace</code>只要求该<code>namespace</code>内唯一即可。</p><h5>class/interface</h5><p>与实际编程语言几乎无差别,比如上面例子中采用的是java语法。</p><h5>可见性</h5><p>PlantUML支持3种可见性:</p><ul><li><code>-</code> 私有级别 <code>private</code></li><li><code>#</code> 保护级别 <code>protected</code></li><li><code>+</code> 公有级别 <code>public</code></li></ul><h5>元素关系</h5><p>PlantUML主要有以下3种关系:</p><ol><li>扩展: 包含<code>implements</code>和<code>extends</code></li><li>聚合: 使用<code>o--</code>,<code>左边</code>的包含<code>右边</code>的</li><li>组合: 使用<code>*--</code>,<code>左边</code>的依赖<code>右边</code>的</li></ol><blockquote><p>组合和聚合的区别:(以上面的图为例)</p><ol><li>组合:QiniuUploader必须依赖Client才能提供上传功能,组合一般是1对1的。</li><li>聚合:UploadFacade可以依赖多个Uploader实例,也可以依赖0个实例(只是这时候不会有文件上传了),聚合一般是1对多的。</li></ol></blockquote><h3>流程图</h3><p>在梳理复杂业务逻辑时,善用流程图能帮我们更加清晰地梳理清楚,也方便我们和其他人员进行沟通(非开发人员基本看不懂代码)。</p><h4>Get Started</h4><ol><li><p>IDE新建一个空项目,打开项目之后,右键新建文件</p><p><img src="/img/remote/1460000040122351" alt="image-20210604152912197" title="image-20210604152912197"></p></li><li><p>新建<code>Activity</code>类型文件</p><p><img src="/img/remote/1460000040122358" alt="image-20210604160929588" title="image-20210604160929588"></p></li><li><p>下面以一个<code>授权获取用户openid并插入数据库,然后查询用户好友进行消息推送</code>的场景编写流程图</p><pre><code>@startuml
'https://plantuml.com/activity-diagram-beta
start
:使用code,appid,secret请求微信服务器获取access_token和openid;
:使用access_token和openid请求微信服务器获取用户信息;
:查询数据库openid是否存在;
if (数据库查询失败?) then (是)
stop
elseif (用户已存在?) then (是)
:更新用户信息;
else (否)
:新建用户并绑定openid;
endif
:获取用户好友列表;
while(好友列表遍历完成?) is (否)
:推送消息给好友;
endwhile(是)
stop
@enduml</code></pre></li><li><p>渲染效果</p><p><img src="/img/remote/1460000040122359" alt="image-20210604161522881" title="image-20210604161522881"></p></li></ol><h4>语法说明</h4><ul><li>开始和结束: 使用<code>start</code>和<code>stop</code></li><li>处理语句: 使用<code>:</code>和<code>;</code>包裹该流程</li><li>条件判断: 使用<code>if</code>,<code>elseif</code>,<code>else</code>,<code>endif</code>,<code>then</code></li><li>循环语句: 使用<code>while</code>,<code>is</code>,<code>endwhile</code>编写</li></ul><h3>组件图</h3><p>现阶段组件化MVVM框架大行其道,具有代表性的有<code>Vue</code>,<code>React</code>和<code>Angular</code>。我们可以使用组件图来绘制组件关系,简单易懂。</p><h5>Get Started</h5><ol><li><p>IDE新建一个空项目,打开项目之后,右键新建文件</p><p><img src="/img/remote/1460000040122351" alt="image-20210604152912197" title="image-20210604152912197"></p></li><li><p>选择<code>Component</code></p><p><img src="/img/remote/1460000040122360" alt="image-20210604162020830" title="image-20210604162020830"></p></li><li><p>以微信首页聊天列表为例绘制组件关系图</p><pre><code>@startuml
'https://plantuml.com/component-diagram
package widgets {
[SearchBar] --> [Text]
[SearchBar] --> [Icon]
[NavigationBar] --> [Text]
[NavigationBar] --> [Icon]
[ListView] --> [ListItem]
[ListItem] --> [Image]
[ListItem] --> [Text]
}
package routes {
[Home] --> [NavigationBar]
[Home] --> [SearchBar]
[Home] --> [ListView]
}
@enduml</code></pre></li><li><p>渲染效果</p><p><img src="/img/remote/1460000040122361" alt="image-20210604162521451" title="image-20210604162521451"></p><blockquote><p>依赖关系如下:</p><ul><li>首页: 导航栏, 搜索框,列表</li><li>导航栏: 文本,图标</li><li>搜索框: 文本,图标</li><li>列表: 列表项</li><li>列表项: 文本,图片</li></ul></blockquote></li></ol><h5>语法说明</h5><ul><li>package 声明包,同一个包内的组件是类似地位</li><li><code>[组件名]</code>声明组件,<code>组件名</code>在单个文件内是唯一的</li><li><code>--></code> 声明依赖关系,<code>左边</code>依赖<code>右边</code></li></ul><p><img src="/img/remote/1460000039375682" alt="img" title="img"></p><p>(完)</p>
Golang程序设计——函数
https://segmentfault.com/a/1190000039651394
2021-03-17T10:33:54+08:00
2021-03-17T10:33:54+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
3
<p>本文学习Go语言函数知识。函数是基本的代码块,用于执行一个任务。在Go语言中,函数可以接收数量不固定的参数,也可以返回多个结果。</p><h2>函数结构</h2><p>在编程领域,函数向编译器和开发者提供了有关的信息,这些信息指明了函数该接收什么样的输入以及会产生什么样的输出。这些信息是通过函数第一行提供的,第一行称为函数签名。</p><p>Go语言声明函数语法如下:</p><pre><code class="go">func 函数名称(参数名 参数类型) (返回值名称 返回值类型) {
// 函数体
return语句
}</code></pre><ol><li>参数名在参数类型前面,如<code>a int</code>,这一点和其他语言是不同的</li><li>函数参数数量可以不固定,但是只允许最后一个参数数量不固定,而且必须是同种类型</li><li>返回值名称不是必须的,但是参数名是必须写的</li><li>有返回值的函数,函数体内必须包含return语句</li></ol><p>示例:函数定义与调用</p><pre><code class="go">package main
import "fmt"
func sum(a, b int) int {
return a + b
}
func main() {
fmt.Printf("1+2=%d\n", sum(1, 2))
}</code></pre><p>在Go语言中,如果多个参数或多个返回值类型相同,只需要在最后一个参数或返回值声明类型。</p><p>例如下面的函数签名在Go语言中是合法的。</p><pre><code class="go">func sum2(a, b int) (c, d int) </code></pre><h2>不定参数函数</h2><p>不定参数也就是数量不固定的参数。例如C语言中的printf函数就是一个典型的不定参数函数。Go语言支持不定参数函数,但是不定参数的类型必须相同。要声明不定参数,需要使用3个点(...)。</p><p>示例:不定参数的加法函数</p><pre><code class="go">package main
import "fmt"
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Printf("1+2+3+4=%d\n", sum(1, 2, 3, 4))
}</code></pre><p>在sum函数中,nums是一个包含所有参数的切片。</p><h2>函数返回值</h2><h3>多返回值</h3><p>在Go语言中,函数能声明多个返回值,在这种情况下,return可以返回多个结果。函数调用者可通过多变量声明接收多个返回值。</p><p>示例:多返回值函数</p><pre><code class="go">package main
import (
"errors"
"fmt"
)
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("b is zero")
}
return a / b, nil
}
func main() {
ret, err := div(2, 1)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("2/1=%d\n", ret)
}</code></pre><h3>命名返回值</h3><p>命名返回值让函数能够在返回前将返回值赋给命名变量,这种设计有利于提高程序可读性。要指定命名返回值,可在函数签名的返回值类型前面添加变量名。</p><p>示例:命名返回值函数</p><pre><code class="go">package main
import (
"fmt"
)
func sum(a, b int) (total int) {
total = a + b
return
}
func main() {
fmt.Printf("1+2=%d\n", sum(1, 2))
}</code></pre><p>使用命名返回值后,return关键字可以单独出现,当然,return关键字继续返回结果值也是合法的。</p><pre><code class="go">func sum(a, b int) (total int) {
total = a + b
return total
}</code></pre><h2>函数类型</h2><p>在Go语言中,函数是一种数据类型,可以将函数赋值给变量、或者作为参数传递,也可以作为返回值返回。</p><p>示例:将函数作为变量、参数、返回值。</p><pre><code class="go">package main
import "fmt"
func main() {
// 函数作为变量
sum := func(a, b int) int {
return a + b
}
fmt.Printf("1+1=%d\n", sum(1, 1))
// 函数作为参数
sum2(1, 1, func(total int) {
fmt.Printf("1+1=%d\n", total)
})
// 函数作为返回值
totalFn := sum3(1, 1)
fmt.Printf("1+1=%d\n", totalFn())
}
func sum2(a, b int, callback func(int)) {
total := a + b
callback(total)
}
func sum3(a, b int) func() int {
return func() int {
return a + b
}
}</code></pre><h2>匿名函数、闭包、延迟执行函数</h2><h3>匿名函数</h3><p>匿名函数指没有名称的函数,只有函数签名(参数和返回值声明)和函数体,匿名函数经常用于回调、闭包、临时函数等。</p><p>示例:利用匿名函数实现事件总线。</p><pre><code class="go">package main
import "fmt"
func main() {
emitter := make(map[string]func())
addEventListener(emitter, "event1", func() {
fmt.Println("event1 called")
})
emit(emitter, "event2")
}
// 添加事件监听器
// emitter 事件总线
// event 事件名
// callback 回调函数
func addEventListener(emitter map[string]func(), event string, callback func()) {
emitter[event] = callback
}
// 触发事件
// emitter 事件总线
// event 事件名
func emit(emitter map[string]func(), event string) {
callback, ok := emitter[event]
if ok {
callback()
}
}</code></pre><p>main函数调用addEventListener时传入的第三个函数即为匿名函数。</p><h3>闭包</h3><p>闭包可以理解为定义在一个函数内部的函数。在本质上,闭包是函数和其引用环境的组合体。引用环境即使在外部函数执行结束也不会被回收,因此可以利用闭包保存保存执行环境。</p><p>示例:利用闭包提供唯一ID生成器。</p><pre><code class="go">package main
import "fmt"
func main() {
s1 := sequenceId()
s2 := sequenceId()
fmt.Printf("s1 -> %v\n", s1())
fmt.Printf("s1 -> %v\n", s1())
fmt.Printf("s2 -> %v\n", s2())
fmt.Printf("s2 -> %v\n", s2())
}
func sequenceId() func() int {
var id int
return func() int {
id++
return id
}
}</code></pre><p>输出如下</p><pre><code class="go">s1 -> 1
s1 -> 2
s2 -> 1
s2 -> 2</code></pre><p>函数sequenceId定义了一个局部变量id,并返回了一个子函数,子函数内部访问了外部的id,因此这构成一个典型的闭包。在前面的内容中我们学习过变量作用域,内部总是可以访问外部的变量或常量,而外部无法访问内部的变量或常量。此外,由于变量id被子函数使用,因此在sequenceId函数返回后,id也不会被销毁。</p><p>每调用一次sequenceId函数都会返回一个新的子函数以及对应的id,因此s1和s2之间的输出互不影响。</p><blockquote>注意:由于闭包会导致被引用的变量无法销毁,因此需要注意使用,避免产生内存泄漏。</blockquote><h3>延迟执行函数</h3><p>在实际编程中往往会打开一些资源,例如文件、网络连接等等,这些资源在使用完毕时(无论是正常关闭或者函数异常)需要主动关闭,当函数的结束分支太多或者逻辑比较复杂时容易发生忘记关闭的情况导致资源泄漏。</p><p>Go语言提供了defer关键字用来延迟执行一个函数,一般使用该函数延迟关闭资源。多个defer语句会按照先进后出的方式执行,也就是最后声明的最先执行,典型的栈结构。</p><p>示例:defer执行顺序。</p><pre><code class="go">package main
import "fmt"
func main() {
defer f1()
defer f2()
fmt.Println("call main")
}
func f1() {
fmt.Println("call f1")
}
func f2() {
defer fmt.Println("defer call f2")
fmt.Println("call f2")
}</code></pre><p>输出如下</p><pre><code>call main
call f2
defer call f2
call f1</code></pre><ol><li>第一行输出call main是因为main函数中只有一个非defer语句,因此call main最先执行</li><li>第二行输出call f2是因为f2函数内部有一个非defer语句</li><li>第三行输出defer call f2是因为f2函数的fmt.Println("call f2")执行完毕后才能执行defer</li><li>第四行输出call f1是因为defer f1()最先声明因此最后执行</li></ol><p>示例:基于defer和闭包构造一个函数执行耗时记录器。</p><pre><code class="go">package main
import (
"fmt"
"time"
)
type Person struct {
Name string
Age int
Sex string
}
func main() {
defer spendTime()()
time.Sleep(time.Second)
fmt.Println("call main")
}
func spendTime() func() {
startAt := time.Now()
return func() {
fmt.Println(time.Since(startAt))
}
}</code></pre><p>输出如下</p><pre><code>call main
1.002345498s</code></pre><p>spendTime()会返回一个闭包,因此定义defer时会初始化startAt为当前时间,defer执行时会执行闭包函数得到函数耗时。main函数为了测试方便休眠了一秒钟,因此可以看到输出是超过1秒的。</p><h2>小结</h2><p>本文介绍了如何在Go语言中使用函数。包括不定参数函数、多返回值和命名返回值函数以及将函数作为类型使用的方法,最后介绍了匿名函数、闭包和延迟执行函数。接下来的内容中将介绍Go语言中的结构体。</p><p><img src="/img/remote/1460000039375682" alt="img" title="img"></p>
Golang程序设计——数据容器
https://segmentfault.com/a/1190000039375680
2021-03-09T16:36:06+08:00
2021-03-09T16:36:06+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
2
<p>本文学习Go语言数据容器、包括数组、切片和映射。</p><h2>数组</h2><p>数组是一个数据集合,常用于存储用数字索引的同类型数据。Go语言的数组调用函数时使用的是值传递,因此形参会拷贝一份实参的值。</p><p>在Go语言中,声明数组需要同时指定长度和数据类型,数组长度是其类型的一部分,因此<code>[5]int</code>和<code>[1]int</code>是两种类型。</p><p>Go语言可以对数组进行写入、读取、删除、遍历等操作。</p><pre><code class="go">package main
import "fmt"
func main() {
// 声明数组并指明长度,不初始化,因此a的5个元素为int类型的零值(0)
var a [5]int
// 声明数组并指明长度,并初始化4个元素,因此b的最后1个元素为int类型零值(0)
var b = [5]int{1, 2, 3, 4}
// 声明数组,不指明长度,编译器会根据值数量推导长度为4
var c = [...]int{1, 2, 3, 4}
// 数组写入
a[0] = 0
a[1] = 1
// 数组读取
fmt.Printf("a[0]=%d\n", a[0])
// 数组删除(赋零值)
a[0] = 0
// 数组的遍历
for index, value := range c {
fmt.Printf("c[%d]=%d\n", index, value)
}
// 输出b
fmt.Printf("b=%v\n", b)
}</code></pre><h2>切片</h2><h3>使用切片</h3><p>在Go语言中,数组是一个重要的类型,但是使用切片的情况更多。切片是底层数组中的一个连续片段,因此数组支持的特性切片也全部支持,必须顺序遍历、通过索引访问元素等等。</p><p>为何使用切片的情况更多呢?主要是因为Go语言的数组不支持自动扩容,而且不支持删除元素,更重要的是Go语言数组是值类型,切片是引用类型,在向函数传参时切片拥有更好的性能。</p><pre><code class="go">package main
import "fmt"
func main() {
// 声明一个大小为0的int类型切片
var a = make([]int, 0)
// 添加三个元素
a = append(a, 1, 2, 3)
fmt.Println(a)
// 遍历元素
for index, value := range a {
fmt.Printf("a[%d]=%d\n", index, value)
}
// 声明一个大小为4的切片
var b = make([]int, 4)
// 将a的元素复制到b
copy(b, a)
// 删除指定下标的元素
a = append(a[:1], a[2:]...)
fmt.Printf("a=%v\n", a)
fmt.Printf("b=%v\n", b)
// 使用值初始化切片
var c = []int{1, 2, 3, 4}
fmt.Printf("c=%v\n", c)
// 只定义,不初始化切片
var d []int
d = append(d, 1, 2, 3)
fmt.Printf("d=%v\n", d)
}</code></pre><p>声明切片可以不使用make初始化,append也不会报错。</p><h3>运行时结构</h3><p>切片运行时结构如下:</p><pre><code class="go">type slice struct {
array unsafe.Pointer
len int
cap int
}</code></pre><ol><li>array是底层数组</li><li>len是数组大小,可以通过len函数获取</li><li>cap是数组容量,可以通过cap函数获取</li></ol><p>make函数创建切片有两种写法:</p><pre><code class="go">make([]int, 0) // 1
make([]int, 0, 8) // 2</code></pre><ol><li>声明了一个长度为0的切片,此时len为0,cap也为0</li><li>声明一个长度为0,容量为8的切片,此时len为0,cap为8</li></ol><h3>追加元素</h3><p>Go语言提供append函数追加元素到切片中,append会在必要时扩容底层数组。扩容规则如下:</p><ol><li>新容量小于1024时,每次扩容2倍。例如现有容量为2,扩容后为4</li><li>新容量大于1024时,每次扩容1.25倍。例如现有容量为1024,扩容后为1280</li></ol><pre><code class="go">package main
import "fmt"
func main() {
// 直接使用值初始化切片
var a = []int{1, 2, 3, 4, 5}
a = append(a, 6, 7)
var b = []int{9, 10}
// 追加b的全部元素到a
a = append(a, b...)
fmt.Printf("a=%v\n", a)
fmt.Printf("b=%v\n", b)
}</code></pre><h3>范围操作符</h3><p>切片支持取范围操作,新切片和原切片共享底层数组,因此对切片的修改会同时影响两个切片。</p><p>范围操作符语法如下:a[begin:end],左闭右开区间。因此a[1:10]包含a切片索引为1~9的元素。</p><pre><code class="go">package main
import "fmt"
func main() {
// 直接使用值初始化切片
var a = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var b = a[1:10]
fmt.Println(b)
// 修改新切片元素
b[0] = 11
fmt.Println(a)
fmt.Println(b)
}</code></pre><p>可以看到修改b索引为0的元素为11之后,a切片也同时受到影响。</p><p>范围操作符的切片这一点在编程中要特别注意!</p><h3>删除元素</h3><p>利用范围操作符和append函数可以删除指定的切片元素。</p><pre><code class="go">package main
import "fmt"
func main() {
// 直接使用值初始化切片
var a = []int{1, 2, 3, 4, 5}
// 删除第2个元素
a = append(a[:1], a[2:]...)
fmt.Println(a)
// 删除第2、3个元素
a = []int{1, 2, 3, 4, 5}
a = append(a[:1], a[3:]...)
fmt.Println(a)
}</code></pre><h3>复制元素</h3><p>通过copy函数可以复制切片的全部或部分元素。在复制切片之前,需要声明好目标切片并设置len。</p><p><em>len<strong>必须大于</strong>0</em><em>,否则将不会复制任何元素。</em></p><pre><code class="go">package main
import "fmt"
func main() {
// 直接使用值初始化切片
var a = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var b = make([]int, 0, 8)
copy(b, a)
fmt.Println(b)
var c = make([]int, 8)
copy(c, a[9:10])
fmt.Println(c)
}</code></pre><p>程序输出如下:</p><pre><code>[]
[10 0 0 0 0 0 0 0]</code></pre><p>可以看到切片b没有任何值,切片c成功复制了a的最后一个元素。</p><h2>映射</h2><p>映射也叫字典、哈希表,数组和切片是通过数字索引访问的顺序集合,而映射是通过键来访问的无序集合。映射在查找方面非常高效,有着O(1)的时间复杂度,是非常常用的数据结构。</p><h3>使用映射</h3><p>映射必须初始化之后才能使用,这一点和切片不同。</p><pre><code class="go">package main
import "fmt"
func main() {
// 使用make初始化映射
var a = make(map[string]int)
a["zhangsan"] = 18
a["lisi"] = 28
fmt.Printf("a=%v\n", a)
// 使用值初始化映射
var b = map[string]int{
"zhangsan": 18,
"lisi": 28,
}
fmt.Printf("b=%v\n", b)
// 遍历映射
for key, value := range b {
fmt.Printf("%s=%d\n", key, value)
}
}</code></pre><p>下面是未初始化映射的使用</p><pre><code class="go">package main
import "fmt"
func main() {
var a map[string]int
a["zhangsan"] = 1
for k, v := range a {
fmt.Printf("%s=%d\n", k, v)
}
}
</code></pre><p>该程序会产生运行时错误:</p><pre><code>panic: assignment to entry in nil map
goroutine 1 [running]:
main.main()
/Users/example/go/src/go-microservice-inaction/src/2.1/main.go:7 +0x5d</code></pre><h3>运行时结构</h3><p>映射的运行时结构如下:</p><pre><code class="go">type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}</code></pre><p>部分字段说明如下:</p><ol><li>count是目前映射的键值对数量</li><li>B是映射的容量,对数。例如B为8,则映射容量为28=256</li><li>buckets中存储具体的键值对</li><li>oldbuckets在扩容中会使用到</li><li>nevacuate 扩容进度指示器</li></ol><p>当装载因子超过6.5时,映射将发生扩容操作。装载因子计算公式:count/2B。例如当前为为166,此时装载因子为166/28=0.6484375,继续插入元素时,装载因子变为167/28= 0.65234375,此时会触发自动扩容。</p><p>每次扩容会增加1倍的空间,同时会对已存在的键值对进行渐进式迁移(一次迁移一小部分)。</p><h3>添加元素</h3><p>Go语言映射添加元素和其他语言类似,使用[]语法即可。</p><pre><code class="go">var m = make(map[string]int)
m["name"] = 18</code></pre><p>添加元素时运行时会自动处理扩容和键值对迁移,无需用户程序关心。</p><h3>删除元素</h3><p>要从映射中删除元素,需要使用delete函数。</p><pre><code class="go">var m = map[string]int{
"zhangsan":18,
}
delete(m, "zhangsan") </code></pre><h2>小结</h2><p>本章介绍了Go语言常用的数据容器,其中对切片和映射的底层原理进行了简单介绍。Go语言通过内置切片和映射解决了C语言需要手动实现这两种常用数据结构的问题,提高了开发效率。在下一章将介绍Go语言的函数。</p><p><img src="/img/remote/1460000039375682" alt="img" title="img"></p>
Golang程序设计——基本语法
https://segmentfault.com/a/1190000039292146
2021-02-26T16:21:14+08:00
2021-02-26T16:21:14+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
5
<p>本文学习Go语言基本语法,例如变量和常量、数据类型、运算符、条件语句、循环语句。</p><h2>变量和常量</h2><p>变量和常量是计算机程序不可或缺的部分。本节将介绍如何在Go程序中声明、使用变量和常量、还将介绍声明方式和作用域。</p><h3>变量声明</h3><p>在Go语言中,声明变量的方式有多种。在前面的文章介绍过,Go语言是一种静态类型语言,因此声明变量时必须指明其类型。</p><p>例:声明string类型的变量。</p><pre><code class="go">package main
import "fmt"
func main() {
var s1 string = "Hello World"
var s2 = "Hello World"
var s3 string
s3 = "Hello World"
fmt.Println(s1, s2, s3)
}</code></pre><ul><li>使用关键字var声明变量。</li><li>如果变量类型可以通过值推导则不用声明类型。s2通过值可以推导类型为string类型。</li><li>变量可以在声明后赋值,未赋值的变量值为该类型的零值。</li></ul><blockquote>变量的类型很重要,因为这决定了可将什么值赋给该变量。例如,对于类型为string的变量,不能将整数赋值给它。将不匹配的值赋值给变量时,将导致编译错误。</blockquote><p>例:将string类型的值赋值给int类型的变量。</p><pre><code class="go">package main
import "fmt"
func main() {
var i int
i = "Hello World"
fmt.Println(i)
}</code></pre><p>编译该文件将导致编译错误。</p><pre><code>go build main.go
# command-line-arguments
./main.go:7:4: cannot use "Hello World" (type untyped string) as type int in assignment</code></pre><h3>多变量声明</h3><p>例:声明多个<strong>类型相同</strong>的变量并进行赋值(显式指定类型)。</p><pre><code class="go">package main
import "fmt"
func main() {
var s1, s2 string = "S1", "S2"
fmt.Println(s1, s2)
}</code></pre><p>例:声明多个<strong>类型不同</strong>的变量并进行赋值(不能显式指定类型)。</p><pre><code class="go">package main
import "fmt"
func main() {
var s1, i1= "S1", 1
fmt.Println(s1, i1)
}</code></pre><p>例:声明多个<strong>类型不同</strong>的变量(显式指定类型)。</p><pre><code class="go">package main
import "fmt"
func main() {
var (
s1 string
i1 int
)
s1 = "Hello"
i1 = 10
fmt.Println(s1, i1)
}</code></pre><blockquote>声明变量后可以再次赋值,但是同一个变量只允许声明一次,否则将导致编译错误。</blockquote><h3>简短变量声明</h3><p>在<strong>函数</strong>中声明变量时,可以用更简洁的方式。</p><pre><code class="go">package main
import "fmt"
func main() {
s1 := "Hello World"
fmt.Println(s1)
}</code></pre><ul><li>:=表示简短变量声明,可以不使用var,不指定类型,但是必须进行赋值。</li><li>只能在函数中使用简短变量声明。</li></ul><h3>变量声明最佳实践</h3><p>Go语言提供了多种变量声明方式,下面的声明方式都是合法的。</p><pre><code class="go">var s string = "Hello"
var s1 = "Hello"
var s2 string
s2 = "Hello"
s3 := "Hello"</code></pre><p>该使用哪种方式呢?</p><p>Go语言对此有一个限制——只能在函数内部使用简短变量声明,在函数外部必须使用var进行声明。</p><blockquote>在标准库中遵循的约定如下:有初始值的情况下,在函数内使用简短变量声明,在函数外使用var并省略类型;无初始值的情况下使用var并指定类型。</blockquote><pre><code class="go">package main
import "fmt"
var s = "Hello World"
func main() {
s1 := "Hello World"
fmt.Println(s, s1)
}</code></pre><h3>变量和零值</h3><p>在Go语言中,声明变量时如果未初始化,则变量为默认值,该默认值也称为零值。在其他语言中未初始化的值为null或undefined。</p><pre><code class="go">package main
import "fmt"
func main() {
var s string
var i int
var b bool
var f float32
fmt.Printf("%v %v %v %v\n", s, i, b, f)
}</code></pre><p>在Go语言中,检查变量是否为空,必须与该类型的零值比较。例如检测string类型的变量是否为空,可以与""判定。</p><pre><code class="go">package main
import "fmt"
func main() {
var s string
if s == "" {
fmt.Println("s为空")
}
}</code></pre><h3>变量作用域</h3><p>作用域指变量可以在什么地方使用,而不是说变量在哪里声明的。Go语言使用基于块的词法作用域,简单来说就是{}会产生一个作用域。</p><p>Go语言作用域规则如下:</p><ol><li>一对大括号({})表示一个块,块是可以嵌套的</li><li>对于在块内声明的变量,可以在本块以及子块中访问</li><li>子块可以访问父块的变量,父块不能访问子块的变量</li></ol><p>例:Go语言的作用域。</p><pre><code class="go">package main
import "fmt"
func main() {
var s1 = "s1"
{
var s2 = "s2"
// 可以访问s1,s2
fmt.Println(s1, s2)
{
var s3 = "s3"
// 可以访问s1,s2,s3
fmt.Println(s1, s2, s3)
}
}
// 只能访问s1
fmt.Println(s1)
}</code></pre><blockquote>简单来说,就是块内可以访问块外的变量,块外不能访问块内变量。</blockquote><h3>声明常量</h3><p>常量只在整个程序运行过程中都不变的值,常量必须在声明时赋值,声明后不可以更改。</p><p>Go语言使用const关键字声明常量。</p><pre><code class="go">package main
import "fmt"
const s = "Hello"
func main() {
const s2 = "World"
const s3,s4 = "Hello","World"
fmt.Println(s, s2)
}</code></pre><blockquote>常量也支持一次声明多个,此外常量的作用域和变量作用域一致。</blockquote><h2>数据类型</h2><p>Go语言提供了丰富的数据类型,按类别分为布尔型、数值型(整数、浮点数、复数)、字符串型 、派生型。其中派声型包括指针类型、数组类型、结构体类型、接口类型、Channel类型、函数类型、切片类型和Map类型。</p><p>派生类型我们将在后面的内容中进行介绍。</p><h3>布尔类型</h3><p>布尔类型值只能为true或false。某些语言允许使用1和0来表示true和false,但Go语言不允许。</p><p>布尔类型的零值为false。</p><pre><code class="go">package main
import "fmt"
func main() {
var b bool
if b {
fmt.Println("b是true")
} else {
fmt.Println("b是false")
}
} </code></pre><h3>数值型</h3><p>Go语言中数值型包含整数、浮点数以及复数。</p><p><strong>整数型</strong></p><table><thead><tr><th><strong>类型</strong></th><th><strong>字节数</strong></th><th><strong>范围</strong></th></tr></thead><tbody><tr><td>byte</td><td>1</td><td>0 ~ 28</td></tr><tr><td>uint8</td><td>1</td><td>0 ~ 28</td></tr><tr><td>int8</td><td>1</td><td>-27 ~ 27-1</td></tr><tr><td>uint16</td><td>2</td><td>0 ~ 216</td></tr><tr><td>int16</td><td>2</td><td>-215 ~ 215-1</td></tr><tr><td>uint32</td><td>4</td><td>0 ~ 232</td></tr><tr><td>int32</td><td>4</td><td>-231 ~ 231-1</td></tr><tr><td>uint64</td><td>8</td><td>0 ~ 264</td></tr><tr><td>int64</td><td>8</td><td>263 ~ 263-1</td></tr><tr><td>int</td><td>平台相关(32位或64位)</td><td> </td></tr><tr><td>uint</td><td>平台相关(32位或64位)</td><td> </td></tr></tbody></table><p><strong>浮点数</strong></p><table><thead><tr><th><strong>类型</strong></th><th><strong>字节数</strong></th><th><strong>范围</strong></th></tr></thead><tbody><tr><td>float32</td><td>4</td><td>-3.403E38 ~ 3.403E38</td></tr><tr><td>float64</td><td>8</td><td>-1.798E308 ~ 1.798E308</td></tr></tbody></table><p><strong>复数</strong></p><p>略</p><h3>字符串类型</h3><p>字符串可以是任何字符序列,包括数字、字母和符号。Go语言使用Unicode来存储字符串,因此可以支持世界上所有的语言。</p><p>下面是一些字符串示例:</p><pre><code class="go">var s = "$%^&*"
var s2 = "1234"
var s3 = "你好"</code></pre><h2>运算符</h2><p>运算符用于在程序运行时执行数据运算和逻辑运算。Go语言支持的运算符有:</p><ul><li>算术运算符</li><li>逻辑运算符</li><li>关系运算符</li><li>位运算符</li></ul><h3>算术运算符</h3><p>算术运算符是用来对数值类型进行算术运算的。下表列出了Go语言支持的算术运算符。</p><table><thead><tr><th>运算符</th><th>说明</th></tr></thead><tbody><tr><td>+</td><td>相加</td></tr><tr><td>-</td><td>相减</td></tr><tr><td>*</td><td>相乘</td></tr><tr><td>/</td><td>相除</td></tr><tr><td>%</td><td>取余</td></tr><tr><td>++</td><td>自增</td></tr><tr><td>--</td><td>自减</td></tr></tbody></table><pre><code class="go">package main
import "fmt"
func main() {
var (
a = 10
b = 20
)
fmt.Printf("a+b=%d\n", a+b)
fmt.Printf("a-b=%d\n", a-b)
fmt.Printf("a*b=%d\n", a*b)
fmt.Printf("a/b=%d\n", a/b)
fmt.Printf("a%%b=%d\n", a%b)
a++
fmt.Printf("a++=%d\n", a)
a--
fmt.Printf("a--=%d\n", a)
}</code></pre><blockquote>和其他语言不同的是,Go语言不提供++a,--a运算符,只提供a++,a--。</blockquote><h3>关系运算符</h3><p>关系运算符用来判断两个值的关系。下表列出了Go语言支持的关系运算符。</p><table><thead><tr><th>运算符</th><th>说明</th></tr></thead><tbody><tr><td>==</td><td>判断两个值是否相等</td></tr><tr><td>!=</td><td>判断两个值是否不相等</td></tr><tr><td>></td><td>判断运算符左边的值是否大于右边的值</td></tr><tr><td><</td><td>判断运算符左边的值是否小于右边的值</td></tr><tr><td>>=</td><td>判断运算符左边的值是否大于等于右边的值</td></tr><tr><td><=</td><td>判断运算符左边的值是否小于等于右边的值</td></tr></tbody></table><pre><code class="go">package main
import "fmt"
func main() {
var (
a = 10
b = 20
)
if a == b {
fmt.Println("a==b")
} else {
fmt.Println("a!=b")
}
if a < b {
fmt.Println("a<b")
} else {
fmt.Println("a>=b")
}
if a <= b {
fmt.Println("a<=b")
} else {
fmt.Println("a>b")
}
}</code></pre><h3>逻辑运算符</h3><p>逻辑运算符用来对操作数进行逻辑判断。下表列出了Go语言支持的逻辑运算符。</p><table><thead><tr><th>运算符</th><th>说明</th></tr></thead><tbody><tr><td>&&</td><td>逻辑与。两边操作数都为true则结果为true,否则为false</td></tr><tr><td>\</td><td>\</td><td> </td><td>逻辑或。两边操作数只要有一个为true则结果为true,否则为false</td></tr><tr><td>!</td><td>逻辑非。如果操作数为true则结果为false,否则为true</td></tr></tbody></table><pre><code class="go">package main
import "fmt"
func main() {
var a, b = true, false
if a && b {
fmt.Println("a和b同时为true")
} else {
fmt.Println("a和b至少一个为false")
}
if a || b {
fmt.Println("a和b至少一个为true")
} else {
fmt.Println("a和b都为false")
}
if !a {
fmt.Println("a是false")
} else {
fmt.Println("a是true")
}
}</code></pre><h3>位运算符</h3><p>位运算符用来对整数进行二进制位操作。下表列出了Go语言支持的位运算符。</p><table><thead><tr><th>运算符</th><th>说明</th></tr></thead><tbody><tr><td>&</td><td>按位与</td></tr><tr><td>\</td><td> </td><td>按位或</td></tr><tr><td>^</td><td>按位异或</td></tr><tr><td>>></td><td>右移</td></tr><tr><td><<</td><td>左移</td></tr></tbody></table><pre><code class="go">package main
import "fmt"
func main() {
var (
a = 1
b = 2
)
fmt.Printf("a&b=%d\n", a&b)
fmt.Printf("a|b=%d\n", a|b)
fmt.Printf("a^b=%d\n", a^b)
fmt.Printf("a>>1=%d\n", a>>1)
fmt.Printf("a<<1=%d\n", a<<1)
}</code></pre><h2>条件语句</h2><p>条件语句是计算机程序的重要组成部分,几乎所有编程语言都支持。简单地说,条件语句检查指定的条件是否满足,并在满足时执行指定的操作。</p><p>下表列出了Go语言支持的条件语句。</p><table><thead><tr><th>if</th><th>由一个布尔表达式后紧跟一个或多个语句组成。</th></tr></thead><tbody><tr><td>if...else if...else</td><td>由多个布尔表达式分支组成,并提供例外分支</td></tr><tr><td>switch</td><td>基于不同条件执行不同操作,并提供默认操作</td></tr></tbody></table><p>例:if的使用。</p><pre><code class="go">package main
import "fmt"
func main() {
var a = 10
if a > 10 {
fmt.Println("a大于10")
} else if a == 10 {
fmt.Println("a等于10")
} else {
fmt.Println("a小于10")
}
}</code></pre><p>例:switch的使用。</p><pre><code class="go">package main
import "fmt"
func main() {
var a = 10
switch a {
case 1:
fmt.Println("a等于1")
case 2:
fmt.Println("a等于2")
case 10:
fmt.Println("a等于3")
default:
fmt.Println("默认分支")
}
}</code></pre><blockquote>和其他语言不同,Go语言的case分支不需要添加break。</blockquote><h2>循环语句</h2><p>在其他语言中一般会提供for、while、foreach等关键字实现循环,而在Go语言中只提供for关键字,但是也实现了类似的效果。</p><h3>for</h3><p>for循环有着经典的三段式结构:</p><ol><li>循环初始化</li><li>循环终止条件</li><li>循环步进条件</li></ol><pre><code class="go">package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}</code></pre><h3>while</h3><p>while循环指定循环终止条件,不满足条件时循环一直执行并向终止条件靠拢,满足条件后终止循环。(无终止条件的循环称为死循环)</p><pre><code class="go">package main
import "fmt"
func main() {
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}</code></pre><p>死循环不需要终止条件。</p><pre><code class="go">package main
import (
"fmt"
"time"
)
func main() {
i := 0
for {
fmt.Println(i)
i++
time.Sleep(time.Second)
}
}</code></pre><h3>foreach</h3><p>foreach循环多用来遍历列表、字典等数据结构。</p><pre><code class="go">package main
import "fmt"
func main() {
list := []int{1, 2, 3, 4, 5}
for index, value := range list {
fmt.Println(index, value)
}
}</code></pre><h3>continue</h3><p>continue用来跳过本次循环继续执行下次循环。</p><pre><code class="go">package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
if i == 1 {
continue
}
fmt.Println(i)
}
}</code></pre><p>该程序判断i为1时跳过并执行下次循环,该程序输出如下。</p><pre><code>0
2
3
4</code></pre><h3>3.1.5 break</h3><p>break用来跳出循环,后续循环将不执行。</p><pre><code class="go">package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
if i == 1 {
break
}
fmt.Println(i)
}
}</code></pre><p>该程序判断i为1时跳出循环,该程序输出如下。</p><pre><code>0</code></pre><h2>小结</h2><p>本文介绍了Go语言的基本语法,包括变量和常量的使用、基础数据类型、流程控制等知识。下一章将介绍Go语言的数据容器类型,包括数组、切片和映射。</p><p><img src="/img/remote/1460000039292149" alt="img" title="img"></p>
修复GitTalk出现Forbidden问题
https://segmentfault.com/a/1190000039280316
2021-02-25T12:14:47+08:00
2021-02-25T12:14:47+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
1
<h2>GitTalk失效原因</h2><p>对于所有自建博客的博主来书,GitTalk应该不陌生。GitTalk通过Github的OpenAPI以及issues功能实现社区评论,确实是一大亮点。</p><p>今天在查看文章的时候发现评论区出现了Forbidden错误,通过检查网络请求发现获取Github Token时请求了以下链接</p><pre><code>https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token</code></pre><p>通过查询GitTalk官方文档发现github.com的oauth是不允许跨域请求的,cors-anywhere.herokuapp.com是一个第三方提供的CORS代理服务,会默认放行所有CORS请求。目前由于该CORS代理服务遭到滥用,因此做了限制,导致GitTalk失效。</p><h2>解决方案</h2><blockquote>通过自己的nginx进行反向代理转发即可。</blockquote><h3>修改gitalk初始化参数</h3><p>笔者使用的是hexo+icarus主题,其他主题或者博客系统也是类似做法。</p><p>编辑themes/icarus/layout/comment/gitalk.ejs</p><pre><code class="javascript"><script>
var gitalk = new Gitalk({
clientID: '<%= get_config('comment.client_id') %>',
clientSecret: '<%= get_config('comment.client_secret') %>',
id: '<%= md5(page.path) %>',
repo: '<%= get_config('comment.repo') %>',
owner: '<%= get_config('comment.owner') %>',
admin: <%- JSON.stringify(get_config('comment.admin'))%>,
createIssueManually: <%= get_config('comment.create_issue_manually', false) %>,
distractionFreeMode: <%= get_config('comment.distraction_free_mode', false) %>,
proxy: '/github/login/oauth/access_token' // 新添加的
})
gitalk.render('comment-container')
</script></code></pre><h3>nginx配置</h3><p>编辑nginx配置,笔者的博客域名为www.ddhigh.com,因此需要限制CORS来源域名,否则将有盗用风险!</p><pre><code class="nginx">location /github {
add_header Access-Control-Allow-Origin www.ddhigh.com;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass https://github.com/; # 尾部斜杠不能少
}</code></pre><p>执行nginx -s reload配置。</p><h3>访问测试</h3><p>访问新写的文章<a href="https://link.segmentfault.com/?enc=%2BKv80k2452SGEuYDLdkA5Q%3D%3D.xIr5yNnHkOuYN8h1SyFQ6FoWwKHuApVR8OSBFUEgOkeut5%2BUzcrrobpC1u%2BBh0%2FUnDfPW6kx6End8yEkeZ4uqg%3D%3D" rel="nofollow">https://www.ddhigh.com/2021/0...</a>,可以看到界面上已经正常了。</p><p><img src="/img/remote/1460000039280318" alt="image-20210225121050348" title="image-20210225121050348"></p><p>查看Chrome网络状况,可以看到已经走了自己配置的CORS跨域了。</p><pre><code>Request URL: https://www.ddhigh.com/github/login/oauth/access_token
Request Method: POST
Status Code: 200
Remote Address: 106.52.24.199:443
Referrer Policy: strict-origin-when-cross-origin</code></pre>
Go语言程序设计
https://segmentfault.com/a/1190000039279720
2021-02-25T11:22:41+08:00
2021-02-25T11:22:41+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
1
<h3>Go语言概述</h3><h4>语言历史</h4><p>Go语言也称为Golang,是由Google公司开发的一种静态强类型、编译型、语言原生支持并发、具有垃圾回收功能的编程语言。起源于2007年,并在2009年正式对外发布。Go语言是非常年轻的一门语言,它的主要目标是“兼具 Python 等动态语言的开发速度和 C/C++等编译型语言的性能与安全性”。</p><p>Go语言是编程语言设计的又一次尝试,是对类C语言的重大改进,它不但能让你访问底层操作系统,还提供了强大的网络编程和并发编程支持。Go语言的用途众多,可以进行网络编程、系统编程、并发编程等等。</p><p>Go语言的推出,旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、语言设计良好、执行性能好”等优势。</p><p>Go语言有时候被描述为“21世纪的C语言”。Go 从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言编译后的运行效率。</p><p>Go语言没有类和继承的概念,通过组合来实现代码复用,同时它通过接口(interface)的概念来实现多态性。所以Go语言的面向对象编程和传统面向对象语言(如C++和Java)并不相同。</p><p>Go语言有一个吉祥物,在会议、文档页面和博文中,大多会包含下图所示的 Go Gopher,这是才华横溢的插画家 Renee French 设计的,她也是 Go 设计者之一 Rob Pike 的妻子。</p><p><img src="https://static.ddhigh.com/blog/2021-02-25-111457-2.jpg" alt="img" title="img"></p><h4>语言特性</h4><p><strong>语法简单</strong></p><p>Go语言的设计思想类似Unix的“少即是多”。Go语言的语法规则严谨,没有歧义,这使得Go语言简单易学。Go语言保留了指针,但通常情况下禁止指针运算(保留unsafe包操作指针的能力)。此外,Go语言还内置切片和字典,在保留运行性能的同时也提高了开发效率。</p><p><strong>语言级别支持并发</strong></p><p>主流的并发模型有多进程模型、多线程模型。和主流多并发模型不同,Go语言采用了基于CSP的协程实现,并且在运行时做了更深度的优化处理。这使得语言级别上并发编程变得极为容易,无须处理回调、也无需关注线程切换,只需要添加一个go关键字即可。</p><p>“通过通信去共享内存,而不是通过共享内存去通信”,go语言内置的channel数据结构配合go关键字实现并发通信及控制,这对于需要考虑内存可见性等问题的多线程模型来说,是一个良好的解决方案。</p><p><strong>高效的垃圾回收</strong></p><p>Go语言的每次升级,垃圾回收器必然是核心组件里修改最多的部分。从并发清理,到降低STW时间,直到Go的1.5版本实现并发标记,逐步引入三色标记和写屏障等等,都是为了能让垃圾回收在不影响用户逻辑的情况下更好地工作。从最开始的秒级别STW到目前的微秒级STW,Go语言开发团队一直在垃圾回收方面进行努力。</p><p><strong>静态链接</strong></p><p>静态编译的好处显而易见。将运行时、依赖库直接打包到可执行文件内部,简化了部署和发布操作,无须事先安装运行环境和下载诸多第三方库。虽然相比动态编译增加了可执行文件的大小,但是省去了依赖库的管理。随着微服务和容器化的发展,这也成为了Go语言的杀手锏之一,一个二进制文件即可运行服务。</p><p><strong>标准库</strong></p><p>功能完善、质量可靠的标准库为编程语言提供了有力的支持。在不借助第三方扩展的情况下,就可完成大部分基础功能开发,这大大降低了学习和使用成本。</p><p>Go语言标准库可以说极为丰富。其中值得称道的是net/http,仅须简单几条语句就能实现一个高性能 Web Server。</p><p><strong>工具链</strong></p><p>完整的工具链对于项目开发极为重要。Go语言在此做得相当不错,无论是编译、格式化、错误检查、帮助文档,还是第三方包下载、更新都有对应的工具。</p><p>值得一提的gofmt工具,为了解决开发者经常遇到的“代码风格不统一”的难题,官方直接通过gofmt指定一套标准,可以看出go语言在工程方面确实解决了许多实际问题。</p><p>此外Go语言内置完整测试框架,其中包括单元测试、性能测试、代码覆盖率、数据竞争,以及用来调优的pprof,这些都是保障代码能正确而稳定运行的必备利器。</p><h4>Go语言应用场景</h4><p>Go 语言从发布1.0版本以来备受众多开发者关注并得到广泛使用,Go 语言的简单、高效、并发特性吸引了众多传统语言开发者的加入,而且人数越来越多。</p><p>鉴于Go语言的特点和设计的初衷,Go语言作为服务器编程语言,很适合处理日志、数据打包、虚拟机处理、文件系统、分布式系统、数据库代理等;网络编程方面,Go语言广泛应用于Web应用、API应用、下载应用等;除此之外,Go语言还适用于内存数据库和云平台领域,目前国外很多云平台都是采用Go开发。</p><ul><li>服务器编程。例如处理日志、数据打包、虚拟机处理、文件系统等。</li><li>分布式系统、数据库代理器、中间件等。例如Etcd。</li><li>网络编程。这一块目前应用最广,包括Web应用、API应用、下载应用等等。</li><li>开发云平台。目前国内外很多云平台在采用Go开发。</li></ul><h4>Go语言知名项目</h4><p>Go发布之后,很多公司特别是云计算公司开始用Go重构他们的基础架构,很多基础设施都是直接采用Go进行了开发,诞生了许多热门项目。</p><p><strong>基础设施</strong></p><p>代表项目:docker、kubernetes、etcd、consul等。</p><p><strong>数据库</strong></p><p>代表项目:influxdb、cockroachdb等。</p><p><strong>微服务</strong></p><p>代表项目:go-kit、micro、kratos等。</p><h3>安装Go语言</h3><p>Go语言可用于FreeBSD、Linux、Windows和macOS等操作系统。有关对这些平台的要求,请参与Go语言网站列出的系统需求。</p><p>Go语言的官方网站为<a href="https://link.segmentfault.com/?enc=%2BP7olVujKNQVL7eHHOXKfg%3D%3D.4oByQPL1fAu4nnKmHITciotd98kfGYrZKamEL0vjVQw%3D" rel="nofollow">https://golang.org/</a>,国内的用户可以访问<a href="https://link.segmentfault.com/?enc=wupx2dJptvrrSu57WEfL9Q%3D%3D.%2FbLobyTkvwZBfbG3GhOIUuVGhtJQaiLsxY1kIZsM8mA%3D" rel="nofollow">https://golang.google.cn/dl/</a>。通常情况下,按照本文的步骤进行安装不会出现问题,遇到安装问题的读者,请通过公众号与我联系。</p><h4>Windows系统</h4><p><strong>下载链接</strong></p><ul><li>32位下载地址:<a href="https://link.segmentfault.com/?enc=7hPoNSwHca8NWIT6sno0XA%3D%3D.xv94e6GCG0rwnRp%2B363vu40XphLazWebbU58j%2BHbUd8omz9MPs11eSVJgVohx5h9g3Tjm8q5mtg7BBceDE2DPQ%3D%3D" rel="nofollow">https://golang.google.cn/dl/go1.15.8.windows-386.msi</a></li><li>64位下载地址:<a href="https://link.segmentfault.com/?enc=gQR%2BU%2B8g9QCerjZKeV9dtw%3D%3D.PrY3uqL8tx45EluBKnEJf0dhFCzIS1LmmmciBO%2F4K0yi1lRYdRMnWpEd6zRF924FKmHoopgbXUTAHCrf6dSdIw%3D%3D" rel="nofollow">https://golang.google.cn/dl/go1.15.8.windows-amd64.msi</a></li></ul><p>默认安装到C:go目录下,建议不要更改安装目录。</p><p><strong>GOPATH配置</strong></p><p>安装完毕后需要配置GOPATH,GOPATH是Go语言用来存放第三方源码、二进制文件、类库等文件的路径。</p><ol><li>例如系统用户名为demo,则需要新建以下三个目录:</li></ol><ul><li>C:Usersdemogosrc 存放源码</li><li>C:Usersdemogopkg 存放类库</li><li>C:Usersdemogobin 存在二进制文件</li></ul><ol><li>环境变量设置:</li></ol><ul><li>新增GOPATH,值为C:Usersdemogo</li><li>新增PATH(已存在则编辑),值为C:Usersdemogobin</li></ul><h4>Linux系统</h4><p>Linux具有众多发行版,如Ubuntu、CentOS、RedHat、Debian等等,所有发行版的安装步骤是一致的,区别是根据CPU架构选择不同的发布包。</p><p>常见的个人计算机CPU架构为amd64,下载amd64架构的发布包即可。</p><p><strong>Linux配置命令</strong></p><h2>下载压缩包</h2><p>wget <a href="https://link.segmentfault.com/?enc=zgMapC70DaEIQoBqw3LS1w%3D%3D.shNtIifjYtO41lkguiVse%2FkUPAEOsFyNJ6QC5e9u5bReSLSsLFkawVBjTvK7W4Ltxo8oEMYzjryMsMDgBIo18Q%3D%3D" rel="nofollow">https://golang.google.cn/dl/go1.15.8.linux-amd64.tar.gz</a></p><h2>移动到opt目录</h2><p>mv go1.15.8.linux-amd64.tar.gz /opt</p><h2>解压</h2><p>tar xf go1.15.8.linux-amd64.tar.gz</p><h2>新建GOPATH目录</h2><p>cd ~</p><p>mkdir go</p><p>cd go</p><p>mkdir pkg src bin</p><h2>编辑 ~/.bashrc文件, 添加bin路径到PATH环境变量中</h2><p>echo 'GOPATH=用户主目录/go' >> ~/.bashrc</p><p>echo 'PATH=/opt/go/bin:$GOPATH/bin:$PATH' >> ~/.bashrc</p><h2>更新环境变量</h2><p>source ~/.bashrc</p><h2>测试安装结果</h2><p>go version</p><h4>macOS系统</h4><p>Apple公司于2020年发布了采用M1芯片(arm64架构)的硬件产品,支持M1芯片的Go语言版本为1.16,根据CPU架构选择对应的pkg包安装即可。</p><ul><li>amd64: <a href="https://link.segmentfault.com/?enc=C0xLfVPbmmgXmQB3zmAoyw%3D%3D.yvqEaEL4hPk7yADUSNWSmSA7i46C2KYMxxyLRVJmiKX%2BbYSRv13v6Kj59yLAEp%2FGETl%2FH2UICa4P7Tbxqv5A4w%3D%3D" rel="nofollow">https://golang.google.cn/dl/go1.15.8.darwin-amd64.pkg</a></li><li>arm64: <a href="https://link.segmentfault.com/?enc=8lihUkdx3m75R24m7ugJXQ%3D%3D.DgsxhTWdO5Rfe32urXvLkFA3E0NYHa1bzAjPnR5nlLdNxkqOwc7odO5HwbcJvFIAYr2FVGd35%2FLlz3eRp0C7yQ%3D%3D" rel="nofollow">https://golang.google.cn/dl/go1.16.darwin-arm64.pkg</a></li></ul><p><strong>macOS配置命令</strong></p><h2>新建GOPATH目录</h2><p>cd ~</p><p>mkdir go</p><p>cd go</p><p>mkdir pkg src bin</p><h2>编辑 ~/.bashrc文件, 添加bin路径到PATH环境变量中</h2><p>echo 'GOPATH=用户主目录/go' >> ~/.bashrc</p><p>echo 'PATH=$GOPATH/bin:$PATH' >> ~/.bashrc</p><h2>更新环境变量</h2><p>source ~/.bashrc</p><h2>测试安装结果</h2><p>go version</p><h3>配置集成开发环境</h3><p>本节将介绍如何在本地计算机上配置集成开发环境,以下步骤使用macOS版本作为示例,其他操作系统类似。</p><p>Visual Studio Code(简称VSCode)是由微软开发的、同时支持Windows、Linux和macOS操作系统的开源编辑器,它支持测试,并且内置了git功能,提供了丰富的语言支持与常用编程工具。</p><ol><li>打开官方网站 <a href="https://link.segmentfault.com/?enc=d4S5nWMfOQCYS9tV78MIJA%3D%3D.em8iooyqrt%2FmIqpGcCDXwRY03kC9A3Y%2F95oiOrucwSI%3D" rel="nofollow">https://code.visualstudio.com/</a>,点击蓝色按钮下载即可。</li><li>新版本的VSCode不再内置中文语言包,需要安装语言包扩展。安装VSCode后打开VSCode编辑器,在扩展窗口中搜索“Chinese”,安装第一个即可。</li></ol><p><img src="https://static.ddhigh.com/blog/2021-02-25-111457-2.png" alt="image-20191022102923920" title="image-20191022102923920"></p><ol><li>用VSCode新建一个空项目,打开项目之后新建main.go,此时VSCode右下角会弹出Go工具链安装的提示,选择”Install All“即可。</li></ol><h3>编写HTTP服务器</h3><h4>代码</h4><pre><code class="go">package main
import (
"io"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "hello world")
})
http.ListenAndServe(":8080", nil)
}</code></pre><h4>程序结构说明</h4><ul><li>package 关键字声明文件所在的包,每个go文件都必须声明。每个可执行程序都必须包含main包,程序的入口点为main包的func main函数</li><li>import 关键字声明需要导入的包,代码中需要使用http服务器相关方法,因此导入了http包</li><li>func main程序的入口点</li></ul><h4>编译并运行程序</h4><p>编译并运行文件是开发过程中的一个常见步骤,Go提供了完成这个步骤的快捷途径。</p><p>Go语言提供了build和run两个命令来编译运行Go程序:</p><ul><li>go build 会编译可执行文件,并不执行</li><li>go run 不会创建可执行文件,直接执行</li></ul><p>使用go run运行HTTP服务器,之后通过浏览器打开即可。</p><h3>小结</h3><p>本文介绍了Go语言的安装以及集成开发环境的配置。通过HTTP服务器演示了Go程序的开发过程。</p><p>下一章将学习Go语言的基本语法:</p><ul><li>变量和常量</li><li>数据类型</li><li>运算符</li><li>条件语句</li><li>循环语句</li></ul><p><img src="https://static.ddhigh.com/blog/2021-02-25-112206-2.png" alt="0.jpeg" title="0.jpeg"></p>
golang依赖注入工具wire指南
https://segmentfault.com/a/1190000039185137
2021-02-06T21:39:50+08:00
2021-02-06T21:39:50+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
9
<h2>wire与依赖注入</h2><p><a href="https://link.segmentfault.com/?enc=lwoJG61elnpXT1CR%2FQnNvg%3D%3D.29bFS5glM%2Bt5y%2BqgjDSDhk1JLJ%2BOtXCcV6tzGu%2BfwxE%3D" rel="nofollow">Wire</a> 是一个的Golang依赖注入工具,通过自动生成代码的方式在<strong>编译期</strong>完成依赖注入,Java体系中最出名的<strong>Spring</strong>框架采用<strong>运行时</strong>注入,个人认为这是wire和其他依赖注入最大的不同之处。</p><p>依赖注入(Dependency Injection)也称作控制反转(Inversion of Control),个人给控制反转下的定义如下:</p><blockquote>当前对象需要的依赖对象由外部提供(通常是IoC容器),外部负责依赖对象的构造等操作,当前对象只负责调用,而不关心依赖对象的构造。即依赖对象的控制权交给了IoC容器。</blockquote><p>下面给出一个控制反转的示例,比如我们通过配置去创建一个数据库连接:</p><pre><code class="go">// 连接配置
type DatabaseConfig struct {
Dsn string
}
func NewDB(config *DatabaseConfig)(*sql.DB, error) {
db,err := sql.Open("mysql", config.Dsn)
if err != nil {
return nil, err
}
// ...
}
fun NewConfig()(*DatabaseConfig,error) {
// 读取配置文件
fp, err := os.Open("config.json")
if err != nil {
return nil,err
}
defer fp.Close()
// 解析为Json
var config DatabaseConfig
if err:=json.NewDecoder(fp).Decode(&config);err!=nil {
return nil,err
}
return &config, nil
}
func InitDatabase() {
cfg, err:=NewConfig()
if err!=nil {
log.Fatal(err)
}
db,err:=NewDB(cfg)
if err!=nil {
log.Fatail(err)
}
// db对象构造完毕
}</code></pre><p>数据库配置怎么来的,<code>NewDB</code>方法并不关心(示例代码采用的是<code>NewConfig</code>提供的JSON配置对象),<code>NewDB</code>只负责创建DB对象并返回,和配置方式并没有耦合,所以即使换成配置中心或者其他方式来提供配置,<code>NewDB</code>代码也无需更改,这就是控制反转的魔力!</p><p>来看一个反面例子,也就是控制正转:</p><blockquote>当前对象需要的依赖由自己创建,即依赖对象的控制权在当前对象自己手里。</blockquote><pre><code class="go">type DatabaseConfig struct {
Dsn string
}
func NewDB()(*sql.DB, error) {
// 读取配置文件
fp, err := os.Open("config.json")
if err != nil {
return nil,err
}
defer fp.Close()
// 解析为Json
var config DatabaseConfig
if err:=json.NewDecoder(fp).Decode(&config);err!=nil {
return nil,err
}
// 初始化数据库连接
db,err = sql.Open("mysql", config.Dsn)
if err != nil {
return
}
// ...
}</code></pre><p>在控制正转模式下,<code>NewDB</code>方法需要自己实现配置对象的创建工作,在示例中需要读取Json配置文件,这是<strong>强耦合</strong>的代码,一旦配置文件的格式不是Json,<code>NewDB</code>方法将返回错误。</p><p>依赖注入固然好用,但是像刚才的例子中去手动管理依赖关系是相当复杂也是相当痛苦的一件事,因此在接下来的内容中会重点介绍golang的依赖注入工具——wire。</p><h2>上手使用</h2><p>通过<code>go get github.com/google/wire/cmd/wire</code>安装好<code>wire</code>命令行工具即可。</p><p>在正式开始之前需要介绍一下wire中的两个概念:<code>Provider</code>和<code>Injector</code>:</p><ul><li><code>Provider</code>:负责创建对象的方法,比如上文中<code>控制反转示例</code>的<code>NewDB</code>(提供DB对象)和<code>NewConfig</code>(提供DatabaseConfig对象)方法。</li><li><code>Injector</code>:负责根据对象的依赖,依次构造依赖对象,最终构造目的对象的方法,比如上文中<code>控制反转示例</code>的<code>InitDatabase</code>方法。</li></ul><p>现在我们通过<code>wire</code>来实现一个简单的项目。项目结构如下:</p><pre><code>|--cmd
|--main.go
|--wire.go
|--config
|--app.json
|--internal
|--config
|--config.go
|--db
|--db.go</code></pre><p>config/app.json</p><pre><code class="json">{
"database": {
"dsn": "root:root@tcp(localhost:3306)/test"
}
}</code></pre><p>internal/config/config.go</p><pre><code class="go">package config
import (
"encoding/json"
"github.com/google/wire"
"os"
)
var Provider = wire.NewSet(New) // 将New方法声明为Provider,表示New方法可以创建一个被别人依赖的对象,也就是Config对象
type Config struct {
Database database `json:"database"`
}
type database struct {
Dsn string `json:"dsn"`
}
func New() (*Config, error) {
fp, err := os.Open("config/app.json")
if err != nil {
return nil, err
}
defer fp.Close()
var cfg Config
if err := json.NewDecoder(fp).Decode(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
</code></pre><p>internal/db/db.go</p><pre><code class="go">package db
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"github.com/google/wire"
"wire-example2/internal/config"
)
var Provider = wire.NewSet(New) // 同理
func New(cfg *config.Config) (db *sql.DB, err error) {
db, err = sql.Open("mysql", cfg.Database.Dsn)
if err != nil {
return
}
if err = db.Ping(); err != nil {
return
}
return db, nil
}</code></pre><p>cmd/main.go</p><pre><code class="go">package main
import (
"database/sql"
"log"
)
type App struct { // 最终需要的对象
db *sql.DB
}
func NewApp(db *sql.DB) *App {
return &App{db: db}
}
func main() {
app, err := InitApp() // 使用wire生成的injector方法获取app对象
if err != nil {
log.Fatal(err)
}
var version string
row := app.db.QueryRow("SELECT VERSION()")
if err := row.Scan(&version); err != nil {
log.Fatal(err)
}
log.Println(version)
}</code></pre><p>cmd/wire.go</p><p>重点文件,也就是实现Injector的核心所在:</p><pre><code class="go">// +build wireinject
package main
import (
"github.com/google/wire"
"wire-example2/internal/config"
"wire-example2/internal/db"
)
func InitApp() (*App, error) {
panic(wire.Build(config.Provider, db.Provider, NewApp)) // 调用wire.Build方法传入所有的依赖对象以及构建最终对象的函数得到目标对象
}</code></pre><p>文件编写完毕,进入<code>cmd</code>目录执行<code>wire</code>命令会得到以下输出:</p><pre><code>C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
wire: wire-example2/cmd: wrote C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire_gen.go</code></pre><p>表明成功生成<code>wire_gen.go</code>文件,文件内容如下:</p><pre><code class="go">// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject
package main
import (
"wire-example2/internal/config"
"wire-example2/internal/db"
)
// Injectors from wire.go:
func InitApp() (*App, error) {
configConfig, err := config.New()
if err != nil {
return nil, err
}
sqlDB, err := db.New(configConfig)
if err != nil {
return nil, err
}
app := NewApp(sqlDB)
return app, nil
}</code></pre><p>可以看到生成App对象的代码已经自动生成了。</p><h3>Provider说明</h3><p>通过<code>NewSet</code>方法将本包内创建对象的方法声明为<code>Provider</code>以供其他对象使用。<code>NewSet</code>可以接收多个参数,比如我们<code>db</code>包内可以创建Mysql和Redis连接对象,则可以如下声明:</p><pre><code class="go">var Provider = wire.NewSet(NewDB, NewRedis)
func NewDB(config *Config)(*sql.DB,error) { // 创建数据库对象
}
func NewRedis(config *Config)(*redis.Client,error) { // 创建Redis对象
}</code></pre><h3>wire.go文件说明</h3><p><code>wire.go</code>文件需要放在创建目标对象的地方,比如我们<code>Config</code>和<code>DB</code>对象最终是为<code>App</code>服务的,因此<code>wire.go</code>文件需要放在<code>App</code>所在的包内。</p><blockquote>wire.go文件名不是固定的,不过大家习惯叫这个文件名。</blockquote><p><code>wire.go</code>的第一行<code>// +build wireinject</code>是必须的,含义如下:</p><blockquote>只有添加了名称为"wireinject"的build tag,本文件才会编译,而我们go build main.go的时候通常不会加。因此,该文件不会参与最终编译。</blockquote><p><code>wire.Build(config.Provider, db.Provider, NewApp)</code>通过传入<code>config</code>以及<code>db</code>对象来创建最终需要的<code>App</code>对象</p><h3>wire_gen.go文件说明</h3><p>该文件由<code>wire</code>自动生成,无需手工编辑!!!</p><p><code>//+build !wireinject</code>标签和<code>wire.go</code>文件的标签相对应,含义如下:</p><blockquote>编译时只有<strong>未添加</strong>"wireinject"的build tag,本文件才参与编译。</blockquote><p>因此,任意时刻下,<code>wire.go</code>和<code>wire_gen.go</code>只会有一个参与编译。</p><h2>高级玩法</h2><h3>cleanup函数</h3><p>在创建依赖资源时,如果由某个资源创建失败,那么其他资源需要关闭的情况下,可以使用cleanup函数来关闭资源。比如咱们给<code>db.New</code>方法返回一个<code>cleanup</code>函数来关闭数据库连接,相关代码修改如下(未列出的代码不修改):</p><p>internal/db/db.go</p><pre><code class="go">func New(cfg *config.Config) (db *sql.DB, cleanup func(), err error) { // 声明第二个返回值
db, err = sql.Open("mysql", cfg.Database.Dsn)
if err != nil {
return
}
if err = db.Ping(); err != nil {
return
}
cleanup = func() { // cleanup函数中关闭数据库连接
db.Close()
}
return db, cleanup, nil
}</code></pre><p>cmd/wire.go</p><pre><code class="go">func InitApp() (*App, func(), error) { // 声明第二个返回值
panic(wire.Build(config.Provider, db.Provider, NewApp))
}</code></pre><p>cmd/main.go</p><pre><code class="go">func main() {
app, cleanup, err := InitApp() // 添加第二个参数
if err != nil {
log.Fatal(err)
}
defer cleanup() // 延迟调用cleanup关闭资源
var version string
row := app.db.QueryRow("SELECT VERSION()")
if err := row.Scan(&version); err != nil {
log.Fatal(err)
}
log.Println(version)
}</code></pre><p>重新在cmd目录执行<code>wire</code>命令,生成的<code>wire_gen.go</code>如下:</p><pre><code class="go">func InitApp() (*App, func(), error) {
configConfig, err := config.New()
if err != nil {
return nil, nil, err
}
sqlDB, cleanup, err := db.New(configConfig)
if err != nil {
return nil, nil, err
}
app := NewApp(sqlDB)
return app, func() { // 返回了清理函数
cleanup()
}, nil
}</code></pre><h3>接口绑定</h3><p>在面向接口编程中,代码依赖的往往是接口,而不是具体的struct,此时依赖注入相关代码需要做一点小小的修改,继续刚才的例子,示例修改如下:</p><p>新增<code>internal/db/dao.go</code></p><pre><code class="go">package db
import "database/sql"
type Dao interface { // 接口声明
Version() (string, error)
}
type dao struct { // 默认实现
db *sql.DB
}
func (d dao) Version() (string, error) {
var version string
row := d.db.QueryRow("SELECT VERSION()")
if err := row.Scan(&version); err != nil {
return "", err
}
return version, nil
}
func NewDao(db *sql.DB) *dao { // 生成dao对象的方法
return &dao{db: db}
}</code></pre><p>internal/db/db.go也需要修改Provider,增加<code>NewDao</code>声明:</p><pre><code class="go">var Provider = wire.NewSet(New, NewDao)</code></pre><p>cmd/main.go文件修改:</p><pre><code class="go">package main
import (
"log"
"wire-example2/internal/db"
)
type App struct {
dao db.Dao // 依赖Dao接口
}
func NewApp(dao db.Dao) *App { // 依赖Dao接口
return &App{dao: dao}
}
func main() {
app, cleanup, err := InitApp()
if err != nil {
log.Fatal(err)
}
defer cleanup()
version, err := app.dao.Version() // 调用Dao接口方法
if err != nil {
log.Fatal(err)
}
log.Println(version)
}</code></pre><p>进入cmd目录执行<code>wire</code>命令,此时会出现报错:</p><pre><code>C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
wire: C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire.go:11:1: inject InitApp: no provider found for wire-example2/internal/db.Dao
needed by *wire-example2/cmd.App in provider "NewApp" (C:\Users\Administrator\GolandProjects\wire-example2\cmd\main.go:12:6)
wire: wire-example2/cmd: generate failed
wire: at least one generate failure</code></pre><p><code>wire</code>提示<code>inject InitApp: no provider found for wire-example2/internal/db.Dao</code>,也就是没找到能提供<code>db.Dao</code>对象的<code>Provider</code>,咱们不是提供了默认的<code>db.dao</code>实现也注册了<code>Provider</code>吗?这也是go的OOP设计奇特之处。</p><p>咱们修改一下<code>internal/db/db.go</code>的<code>Provider</code>声明,增加<code>db.*dao</code>和<code>db.Dao</code>的接口绑定关系:</p><pre><code class="go">var Provider = wire.NewSet(New, NewDao, wire.Bind(new(Dao), new(*dao)))</code></pre><p><code>wire.Bind()</code>方法第一个参数为<code>interface{}</code>,第二个参数为<code>实现</code>。</p><p>此时再执行<code>wire</code>命令就可以成功了!</p><h2>结尾</h2><p><code>wire</code>工具还有很多玩法,但是就笔者个人工作经验而言,掌握本文介绍到的知识已经能够胜任绝大部分场景了!</p>
Golang组件化网络服务器框架Halia指南
https://segmentfault.com/a/1190000038946586
2021-01-12T12:09:34+08:00
2021-01-12T12:09:34+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
2
<h2>写在前面</h2><p>在<strong>netty</strong>框架面世之前,几乎没有一个成熟的OOP/组件化规范指导网络服务器开发,一些常用的<code>FrameDecoder</code>,<code>BusinessHandler</code>等等组件紧密耦合在了项目当中,整个项目可以说扩展性比较差。</p><p>netty的出现可以说是划时代的,基于OOP/组件化屏蔽了底层 <strong>BlockingIO</strong>/<strong>NonBlockingIO</strong>/<strong>AsynchrousIO</strong>之间的差异,各种组件可以无缝切换,网络服务器开发效率有了非常大的提高。</p><p>通过阅读netty源码,以及核心组件的架构,基于Golang进行了实现,至此,Golang的Halia框架面世了!</p><h2>Halia特性</h2><h3>组件化/可扩展</h3><p>Halia框架面向接口编程,并提供默认实现,同时内置常用的解码器,真正做到开箱即用。</p><h3>高性能</h3><p>基于Golang原生网络库进行开发,无第三方依赖,性能有保障。</p><h3>易用性</h3><p>Halia框架采用极简设计,没有冗余代码,并附带3个常用解码器示例,助您基于Halia快速开始开发。</p><h3>开源免费</h3><p>Halia框架基于MIT开源协议发布,无论是商用以及非商用都可以免费使用。</p><h3>社区驱动</h3><p>Halia框架托管于Github,任何人都可以贡献一臂之力。</p><h2>快速开始</h2><p>接下来将演示如何开发一个时间回显服务器。</p><p>客户端每隔1秒发送时间字符串给服务器,服务器回显该数据。</p><h3>公用代码</h3><h4>encoder.go</h4><p>字符串编码器,将字符串转换为<code>[]byte</code>传输到下一个出站处理器</p><pre><code class="go">package main
import (
"halia/channel"
)
type StringToByteEncoder struct{}
// 编码器不处理处理,交由下一个处理器(也就是业务处理器)处理
func (e *StringToByteEncoder) OnError(c channel.HandlerContext, err error) {
c.FireOnError(err)
}
func (e *StringToByteEncoder) Write(c channel.HandlerContext, msg interface{}) error {
if str, ok := msg.(string); ok { // string才转换
return c.Write([]byte(str))
}
return c.Write(msg)
}
func (e *StringToByteEncoder) Flush(c channel.HandlerContext) error {
return c.Flush()
}</code></pre><h3>客户端代码</h3><h4>handler.go</h4><p>客户端业务处理代码。</p><pre><code class="go">package main
import (
"fmt"
log "github.com/sirupsen/logrus"
"halia/channel"
"strings"
"time"
)
type EchoClientHandler struct {
log *log.Entry
}
func NewEchoClientHandler() *EchoClientHandler {
return &EchoClientHandler{
log: log.WithField("component", "EchoClientHandler"),
}
}
// 发送错误回调
func (p *EchoClientHandler) OnError(c channel.HandlerContext, err error) {
p.log.WithField("peer", c.Channel().RemoteAddr()).Warnln("error caught", err)
}
// 连接已建立
func (p *EchoClientHandler) ChannelActive(c channel.HandlerContext) {
p.log.WithField("peer", c.Channel().RemoteAddr()).Infoln("connected")
if err := c.WriteAndFlush("Hello World\r\n"); err != nil {
p.log.WithError(err).Warnln("write error")
}
p.log.Infof("pipeline in: %v", strings.Join(c.Pipeline().InboundNames(), "->"))
p.log.Infof("pipeline out: %v", strings.Join(c.Pipeline().OutboundNames(), "->"))
}
// 连接已断开
func (p *EchoClientHandler) ChannelInActive(c channel.HandlerContext) {
p.log.WithField("peer", c.Channel().RemoteAddr()).Infoln("disconnected")
}
// 读取到完整的消息回调
func (p *EchoClientHandler) ChannelRead(c channel.HandlerContext, msg interface{}) {
data, ok := msg.([]byte)
if !ok {
p.log.WithField("peer", c.Channel().RemoteAddr()).Warnf("unknown msg type: %+v", msg)
return
}
str := string(data)
p.log.WithField("peer", c.Channel().RemoteAddr()).Infoln("receive ", str)
// 1秒后发送数据给服务器
time.AfterFunc(time.Second, func() {
if err := c.WriteAndFlush(fmt.Sprintf("client say:%s\r\n", time.Now().String())); err != nil {
p.log.WithError(err).Warnln("write error")
}
})
}</code></pre><h4>main.go</h4><pre><code class="go">package main
import (
log "github.com/sirupsen/logrus"
"halia/bootstrap"
"halia/channel"
"halia/handler/codec"
"net"
"os"
)
func init() {
log.SetOutput(os.Stdout)
log.SetLevel(log.DebugLevel)
}
func main() {
client := bootstrap.NewClient(&bootstrap.ClientOptions{
// 将原始net.Conn包装为Channel实现,一般情况下用DefaultChannel即可
ChannelFactory: func(conn net.Conn) channel.Channel {
c := channel.NewDefaultChannel(conn)
// 添加解码器,换行符分割报文解码器
c.Pipeline().AddInbound("decoder", codec.NewLineBasedFrameDecoder())
// 添加业务处理器
c.Pipeline().AddInbound("handler", NewEchoClientHandler())
// 添加编码器
c.Pipeline().AddOutbound("encoder", &StringToByteEncoder{})
return c
},
})
// 连接服务器
log.WithField("component", "client").Fatal(client.Dial("tcp", "127.0.0.1:8080"))
}</code></pre><h3>服务端代码</h3><h4>handler.go</h4><pre><code class="go">package main
import (
log "github.com/sirupsen/logrus"
"halia/channel"
"strings"
)
type EchoServerHandler struct {
log *log.Entry
}
func NewEchoServerHandler() *EchoServerHandler {
return &EchoServerHandler{
log: log.WithField("component", "EchoServerHandler"),
}
}
func (p *EchoServerHandler) OnError(c channel.HandlerContext, err error) {
p.log.WithField("peer", c.Channel().RemoteAddr()).Warnln("error caught", err)
}
func (p *EchoServerHandler) ChannelActive(c channel.HandlerContext) {
p.log.WithField("peer", c.Channel().RemoteAddr()).Infoln("connected")
p.log.Infof("pipeline in: %v", strings.Join(c.Pipeline().InboundNames(), "->"))
p.log.Infof("pipeline out: %v", strings.Join(c.Pipeline().OutboundNames(), "->"))
}
func (p *EchoServerHandler) ChannelInActive(c channel.HandlerContext) {
p.log.WithField("peer", c.Channel().RemoteAddr()).Infoln("disconnected")
p.log.Infof("pipeline in: %v", strings.Join(c.Pipeline().InboundNames(), "->"))
p.log.Infof("pipeline out: %v", strings.Join(c.Pipeline().OutboundNames(), "->"))
}
func (p *EchoServerHandler) ChannelRead(c channel.HandlerContext, msg interface{}) {
data, ok := msg.([]byte)
if !ok {
p.log.WithField("peer", c.Channel().RemoteAddr()).Warnf("unknown msg type: %+v", msg)
return
}
str := string(data)
p.log.WithField("peer", c.Channel().RemoteAddr()).Infoln("receive ", str)
if err := c.Write("server:" + str + "\r\n"); err != nil {
p.log.WithField("peer", c.Channel().RemoteAddr()).WithError(err).Warnln("write error")
}
}</code></pre><h4>main.go</h4><pre><code class="go">package main
import (
log "github.com/sirupsen/logrus"
"halia/bootstrap"
"halia/channel"
"halia/handler/codec"
"net"
"os"
)
func init() {
log.SetOutput(os.Stdout)
log.SetLevel(log.DebugLevel)
}
func main() {
s := bootstrap.NewServer(&bootstrap.ServerOptions{
ChannelFactory: func(conn net.Conn) channel.Channel {
c := channel.NewDefaultChannel(conn)
c.Pipeline().AddInbound("decoder", codec.NewLineBasedFrameDecoder())
c.Pipeline().AddInbound("handler", NewEchoServerHandler())
c.Pipeline().AddOutbound("encoder", &StringToByteEncoder{})
return c
},
})
log.WithField("component", "server").Fatal(s.Listen("tcp", "0.0.0.0:8080"))
}</code></pre><h3>运行</h3><p>先运行服务端,再运行客户端。</p><p>服务端输出</p><pre><code class="text">time="2021-01-12T11:30:13+08:00" level=info msg=started addr="0.0.0.0:8080" component=server network=tcp pid=7584
time="2021-01-12T11:30:13+08:00" level=info msg=initialized component=channelId machineId=a0c5895a25a3 pid=7584
time="2021-01-12T11:30:18+08:00" level=info msg=connected component=EchoServerHandler peer="127.0.0.1:57641"
time="2021-01-12T11:30:18+08:00" level=info msg="pipeline in: InHeadContext->decoder->handler" component=EchoServerHandler
time="2021-01-12T11:30:18+08:00" level=info msg="pipeline out: OutHeadContext->encoder->OutTailContext" component=EchoServerHandler
time="2021-01-12T11:30:18+08:00" level=info msg="receive Hello World" component=EchoServerHandler peer="127.0.0.1:57641"
time="2021-01-12T11:30:19+08:00" level=info msg="receive client say:2021-01-12 11:30:19.5192868 +0800 CST m=+1.046443501" component=EchoServerHandler peer="127.0.0.1:57641"
time="2021-01-12T11:30:20+08:00" level=info msg="receive client say:2021-01-12 11:30:20.5193884 +0800 CST m=+2.046545101" component=EchoServerHandler peer="127.0.0.1:57641"
time="2021-01-12T11:30:21+08:00" level=info msg="receive client say:2021-01-12 11:30:21.5345887 +0800 CST m=+3.061745401" component=EchoServerHandler peer="127.0.0.1:57641"
time="2021-01-12T11:30:22+08:00" level=info msg="receive client say:2021-01-12 11:30:22.5459978 +0800 CST m=+4.073154501" component=EchoServerHandler peer="127.0.0.1:57641"</code></pre><p>客户端输出</p><pre><code>time="2021-01-12T11:30:18+08:00" level=info msg=connected component=EchoClientHandler peer="127.0.0.1:8080"
time="2021-01-12T11:30:18+08:00" level=info msg="pipeline in: InHeadContext->decoder->handler" component=EchoClientHandler
time="2021-01-12T11:30:18+08:00" level=info msg="pipeline out: OutHeadContext->encoder->OutTailContext" component=EchoClientHandler
time="2021-01-12T11:30:18+08:00" level=info msg="receive server:Hello World" component=EchoClientHandler peer="127.0.0.1:8080"
time="2021-01-12T11:30:18+08:00" level=info msg=initialized component=channelId machineId=a0c5895a25a3 pid=960
time="2021-01-12T11:30:19+08:00" level=info msg="receive server:client say:2021-01-12 11:30:19.5192868 +0800 CST m=+1.046443501" component=EchoClientHandler peer="127.0.0.1:8080"
time="2021-01-12T11:30:20+08:00" level=info msg="receive server:client say:2021-01-12 11:30:20.5193884 +0800 CST m=+2.046545101" component=EchoClientHandler peer="127.0.0.1:8080"
time="2021-01-12T11:30:21+08:00" level=info msg="receive server:client say:2021-01-12 11:30:21.5345887 +0800 CST m=+3.061745401" component=EchoClientHandler peer="127.0.0.1:8080"
time="2021-01-12T11:30:22+08:00" level=info msg="receive server:client say:2021-01-12 11:30:22.5459978 +0800 CST m=+4.073154501" component=EchoClientHandler peer="127.0.0.1:8080"</code></pre><h2>写在后面</h2><p>Halia期待您的贡献!</p><ul><li><a href="https://link.segmentfault.com/?enc=2PpUmLN9ZiYWj4e5jxf2Zg%3D%3D.KN%2BhLUDyQb6nr2Uaagsf1Iy9Uq%2FlVJnoZBU0i%2FgEHjEZbgJSgYZuNWIJGo3nFaTt" rel="nofollow">文档地址</a></li><li><a href="https://link.segmentfault.com/?enc=WdBbUIWXx7Iv14%2Fj%2B%2BAYdw%3D%3D.NWsaqlt2LGNaKDtr14CRa9Ij%2F3kyG%2BiDTVywuvJlhcKWmKbtuGc0yWVcRLdv%2Fpog" rel="nofollow">仓库地址</a></li></ul><p><img src="/img/remote/1460000038946589" alt="" title=""></p>
Webpack4不求人(5)——编写自定义插件
https://segmentfault.com/a/1190000022056204
2020-03-18T11:46:27+08:00
2020-03-18T11:46:27+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
9
<p><img src="/img/remote/1460000022056207" alt="" title=""></p>
<p>Webpack通过Loader完成模块的转换工作,让“一切皆模块”成为可能。Plugin机制则让其更加灵活,可以在Webpack生命周期中调用钩子完成各种任务,包括修改输出资源、输出目录等等。</p>
<p>今天我们一起来学习如何编写Webpack插件。</p>
<h2>构建流程</h2>
<p>在编写插件之前,还需要了解一下Webpack的构建流程,以便在合适的时机插入合适的插件逻辑。Webpack的基本构建流程如下:</p>
<ol>
<li>校验配置文件</li>
<li>生成Compiler对象</li>
<li>初始化默认插件</li>
<li>run/watch:如果运行在watch模式则执行watch方法,否则执行run方法</li>
<li>compilation:创建Compilation对象回调compilation相关钩子</li>
<li>emit:文件内容准备完成,准备生成文件,这是最后一次修改最终文件的机会</li>
<li>afterEmit:文件已经写入磁盘完成</li>
<li>done:完成编译</li>
</ol>
<h2>插件示例</h2>
<p>一个典型的Webpack插件代码如下:</p>
<pre><code class="javascript">// 插件代码
class MyWebpackPlugin {
constructor(options) {
}
apply(compiler) {
// 在emit阶段插入钩子函数
compiler.hooks.emit.tap('MyWebpackPlugin', (compilation) => {});
}
}
module.exports = MyWebpackPlugin;</code></pre>
<p>接下来需要在webpack.config.js中引入这个插件。</p>
<pre><code class="javascript">module.exports = {
plugins:[
// 传入插件实例
new MyWebpackPlugin({
param:'paramValue'
}),
]
};</code></pre>
<p>Webpack在启动时会实例化插件对象,在初始化compiler对象之后会调用插件实例的apply方法,传入compiler对象,插件实例在apply方法中会注册感兴趣的钩子,Webpack在执行过程中会根据构建阶段回调相应的钩子。</p>
<h2>Compiler && Compilation对象</h2>
<p>在编写Webpack插件过程中,最常用也是最主要的两个对象就是Webpack提供的Compiler和Compilation,Plugin通过访问Compiler和Compilation对象来完成工作。</p>
<ul>
<li>Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该对象获取到Webpack的配置信息进行处理。</li>
<li>Compilation对象可以理解编译对象,包含了模块、依赖、文件等信息。在开发模式下运行Webpack时,每修改一次文件都会产生一个新的Compilation对象,Plugin可以访问到本次编译过程中的模块、依赖、文件内容等信息。</li>
</ul>
<h3>常见钩子</h3>
<p>Webpack会根据执行流程来回调对应的钩子,下面我们来看看都有哪些常见钩子,这些钩子支持的tap操作是什么。</p>
<table>
<thead><tr>
<th>钩子</th>
<th>说明</th>
<th>参数</th>
<th>类型</th>
</tr></thead>
<tbody>
<tr>
<td>afterPlugins</td>
<td>启动一次新的编译</td>
<td>compiler</td>
<td>同步</td>
</tr>
<tr>
<td>compile</td>
<td>创建compilation对象之前</td>
<td>compilationParams</td>
<td>同步</td>
</tr>
<tr>
<td>compilation</td>
<td>compilation对象创建完成</td>
<td>compilation</td>
<td>同步</td>
</tr>
<tr>
<td>emit</td>
<td>资源生成完成,输出之前</td>
<td>compilation</td>
<td>异步</td>
</tr>
<tr>
<td>afterEmit</td>
<td>资源输出到目录完成</td>
<td>compilation</td>
<td>异步</td>
</tr>
<tr>
<td>done</td>
<td>完成编译</td>
<td>stats</td>
<td>同步</td>
</tr>
</tbody>
</table>
<h2>Tapable</h2>
<p>Tapable是Webpack的一个核心工具,Webpack中许多对象扩展自Tapable类。Tapable类暴露了tap、tapAsync和tapPromise方法,可以根据钩子的同步/异步方式来选择一个函数注入逻辑。</p>
<ul>
<li>tap 同步钩子</li>
<li>tapAsync 异步钩子,通过callback回调告诉Webpack异步执行完毕</li>
<li>tapPromise 异步钩子,返回一个Promise告诉Webpack异步执行完毕</li>
</ul>
<h3>tap</h3>
<p>tap是一个同步钩子,同步钩子在使用时不可以包含异步调用,因为函数返回时异步逻辑有可能未执行完毕导致问题。</p>
<p>下面一个在compile阶段插入同步钩子的示例。</p>
<pre><code class="javascript">compiler.hooks.compile.tap('MyWebpackPlugin', params => {
console.log('我是同步钩子')
});</code></pre>
<h3>tapAsync</h3>
<p>tapAsync是一个异步钩子,我们可以通过callback告知Webpack异步逻辑执行完毕。</p>
<p>下面是一个在emit阶段的示例,在1秒后打印文件列表。</p>
<pre><code class="javascript">compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
setTimeout(()=>{
console.log('文件列表', Object.keys(compilation.assets).join(','));
callback();
}, 1000);
});</code></pre>
<h3>tapPromise</h3>
<p>tapPromise也是也是异步钩子,和tapAsync的区别在于tapPromise是通过返回Promise来告知Webpack异步逻辑执行完毕。</p>
<p>下面是一个将生成结果上传到CDN的示例。</p>
<pre><code class="javascript">compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => {
return new Promise((resolve, reject) => {
const filelist = Object.keys(compilation.assets);
uploadToCDN(filelist, (err) => {
if(err) {
reject(err);
return;
}
resolve();
});
});
});</code></pre>
<p>apply方法中插入钩子的一般形式如下:</p>
<pre><code class="javascript">compileer.hooks.阶段.tap函数('插件名称', (阶段回调参数) => {
});</code></pre>
<h2>常用API</h2>
<h3>读取输出资源、模块及依赖</h3>
<p>在emit阶段,我们可以读取最终需要输出的资源、chunk、模块和对应的依赖,如果有需要还可以更改输出资源。</p>
<pre><code class="javascript">apply(compiler) {
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
// compilation.chunks存放了代码块列表
compilation.chunks.forEach(chunk => {
// chunk包含多个模块,通过chunk.modulesIterable可以遍历模块列表
for(const module of chunk.modulesIterable) {
// module包含多个依赖,通过module.dependencies进行遍历
module.dependencies.forEach(dependency => {
console.log(dependency);
});
}
});
callback();
});
}</code></pre>
<h3>修改输出资源</h3>
<p>通过操作compilation.assets对象,我们可以添加、删除、更改最终输出的资源。</p>
<pre><code class="javascript">apply(compiler) {
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation) => {
// 修改或添加资源
compilation.assets['main.js'] = {
source() {
return 'modified content';
},
size() {
return this.source().length;
}
};
// 删除资源
delete compilation.assets['main.js'];
});
}</code></pre>
<p>assets对象需要定义source和size方法,source方法返回资源的内容,支持字符串和Node.js的Buffer,size返回文件的大小字节数。</p>
<h2>插件编写实例</h2>
<p>接下来我们开始编写自定义插件,所有插件使用的示例项目如下(需要安装webpack和webpack-cli):</p>
<pre><code>|----src
|----main.js
|----plugins
|----my-webpack-plugin.js
|----package.json
|----webpack.config.js</code></pre>
<p>相关文件的内容如下:</p>
<pre><code class="javascript">// src/main.js
console.log('Hello World');</code></pre>
<pre><code class="json">// package.json
{
"scripts":{
"build":"webpack"
}
}</code></pre>
<pre><code class="javascript">const path = require('path');
const MyWebpackPlugin = require('my-webpack-plugin');
// webpack.config.js
module.exports = {
entry:'./src/main',
output:{
path: path.resolve(__dirname, 'build'),
filename:'[name].js',
},
plugins:[
new MyWebpackPlugin()
]
};</code></pre>
<h3>生成清单文件</h3>
<p>通过在emit阶段操作compilation.assets实现。</p>
<pre><code class="javascript">class MyWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
const manifest = {};
for (const name of Object.keys(compilation.assets)) {
manifest[name] = compilation.assets[name].size();
// 将生成文件的文件名和大小写入manifest对象
}
compilation.assets['manifest.json'] = {
source() {
return JSON.stringify(manifest);
},
size() {
return this.source().length;
}
};
callback();
});
}
}
module.exports = MyWebpackPlugin;</code></pre>
<p>构建完成后会在build目录添加manifest.json,内容如下:</p>
<pre><code class="json">{"main.js":956}</code></pre>
<h3>构建结果上传到七牛</h3>
<p>在实际开发中,资源文件构建完成后一般会同步到CDN,最终前端界面使用的是CDN服务器上的静态资源。</p>
<p>下面我们编写一个Webpack插件,文件构建完成后上传的七牛CDN。</p>
<p>我们的插件依赖qiniu,因此需要额外安装qiniu模块</p>
<pre><code class="bash">npm install qiniu --save-dev</code></pre>
<p>七牛的Node.js SDK文档地址如下:</p>
<pre><code>https://developer.qiniu.com/kodo/sdk/1289/nodejs</code></pre>
<p>开始编写插件代码:</p>
<pre><code class="javascript">const qiniu = require('qiniu');
const path = require('path');
class MyWebpackPlugin {
// 七牛SDK mac对象
mac = null;
constructor(options) {
// 读取传入选项
this.options = options || {};
// 检查选项中的参数
this.checkQiniuConfig();
// 初始化七牛mac对象
this.mac = new qiniu.auth.digest.Mac(
this.options.qiniu.accessKey,
this.options.qiniu.secretKey
);
}
checkQiniuConfig() {
// 配置未传qiniu,读取环境变量中的配置
if (!this.options.qiniu) {
this.options.qiniu = {
accessKey: process.env.QINIU_ACCESS_KEY,
secretKey: process.env.QINIU_SECRET_KEY,
bucket: process.env.QINIU_BUCKET,
keyPrefix: process.env.QINIU_KEY_PREFIX || ''
};
}
const qiniu = this.options.qiniu;
if (!qiniu.accessKey || !qiniu.secretKey || !qiniu.bucket) {
throw new Error('invalid qiniu config');
}
}
apply(compiler) {
compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => {
return new Promise((resolve, reject) => {
// 总上传数量
const uploadCount = Object.keys(compilation.assets).length;
// 已上传数量
let currentUploadedCount = 0;
// 七牛SDK相关参数
const putPolicy = new qiniu.rs.PutPolicy({ scope: this.options.qiniu.bucket });
const uploadToken = putPolicy.uploadToken(this.mac);
const config = new qiniu.conf.Config();
config.zone = qiniu.zone.Zone_z1;
const formUploader = new qiniu.form_up.FormUploader()
const putExtra = new qiniu.form_up.PutExtra();
// 因为是批量上传,需要在最后将错误对象回调
let globalError = null;
// 遍历编译资源文件
for (const filename of Object.keys(compilation.assets)) {
// 开始上传
formUploader.putFile(
uploadToken,
this.options.qiniu.keyPrefix + filename,
path.resolve(compilation.outputOptions.path, filename),
putExtra,
(err) => {
console.log(`uploade ${filename} result: ${err ? `Error:${err.message}` : 'Success'}`)
currentUploadedCount++;
if (err) {
globalError = err;
}
if (currentUploadedCount === uploadCount) {
globalError ? reject(globalError) : resolve();
}
});
}
})
});
}
}
module.exports = MyWebpackPlugin;</code></pre>
<p>Webpack中需要传递给该插件传递相关配置:</p>
<pre><code class="javascript">module.exports = {
entry: './src/index',
target: 'node',
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
publicPath: 'CDN域名'
},
plugins: [
new CleanWebpackPlugin(),
new QiniuWebpackPlugin({
qiniu: {
accessKey: '七牛AccessKey',
secretKey: '七牛SecretKey',
bucket: 'static',
keyPrefix: 'webpack-inaction/demo1/'
}
})
]
};</code></pre>
<p>编译完成后资源会自动上传到七牛CDN,这样前端只用交付index.html即可。</p>
<h2>小结</h2>
<p>至此,Webpack相关常用知识和进阶知识都介绍完毕,需要各位读者在工作中去多加探索,Webpack配合Node.js生态,一定会涌现出更多优秀的新语言和新工具!</p>
<p><img src="/img/remote/1460000021980127" alt="" title=""></p>
Webpack4不求人系列(4)——自定义Loader
https://segmentfault.com/a/1190000021980124
2020-03-11T14:16:26+08:00
2020-03-11T14:16:26+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
11
<p>在前面的内容中,我们学习了Webpack的基本知识、常用脚手架和性能优化,虽然说大部分的开发场景社区已经又成熟的模块给我们使用,但是遇到特殊情况还是需要自己有独立开发的能力,因此今天我们一起来学习如何编写自定义Loader。</p>
<h2>基本Loader</h2>
<p>Webpack中loader是一个CommonJs风格的函数,接收输入的源码,通过同步或异步的方式替换源码后进行输出。</p>
<pre><code class="javascript">module.exports = function(source, sourceMap, meta) {
}</code></pre>
<ul>
<li>source是输入的内容</li>
<li>sourceMap是可选的</li>
<li>meta是模块的元数据,也是可选的</li>
</ul>
<p>需要注意的是,该导出函数必须使用function,不能使用箭头函数,因为loader编写过程中会经常使用到<code>this</code>访问选项和其他方法。</p>
<p>我们先编写一个基本的Loader,完成的工作很简单,那就是把输出的字符串进行替换。</p>
<p>1.新建loader-example目录,执行npm初始化,并安装webpack</p>
<pre><code class="bash">mkdir loader-example
cd loadeer-example
npm init -y
npm install webpack webpack-cli</code></pre>
<p>2.构建项目目录</p>
<pre><code>|----loader # loader目录
|----replace-loader.js # 替换字符串的Loader
|----src # 应用源码
|----index.js # 首页
|----package.json
|----webpack.config.js</code></pre>
<p>3.编写loader/replace-loader.js</p>
<pre><code class="javascript">module.exports = function(source) {
return source.replace(/World/g, 'Loader');
};</code></pre>
<p>本例中我们Loader只是简单的将源码中的”World“替换成了”Loader“。</p>
<p>4.编写src/index.js</p>
<pre><code class="javascript">console.log('Hello World');</code></pre>
<p>5.编写webpack.config.js</p>
<pre><code class="javascript">const path = require('path');
module.exports = {
entry: './src/index',
target: 'node', // 我们编译为Node.js环境下的JS,等下直接使用Node.js执行编译完成的文件
output:{
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
},
module:{
rules:[
{
test:/\.js$/,
use: 'replace-loader'
}
]
},
resolveLoader: {
modules: ['./node_modules', './loader'] // 配置loader的查找目录
}
};</code></pre>
<p>6.编写package.json</p>
<pre><code class="json">{
"scripts":{
"build":"webpack"
}
}</code></pre>
<p>7.执行构建</p>
<pre><code class="bash">npm run build</code></pre>
<p>8.构建完成后,执行build/main.js</p>
<pre><code class="bash">node build/main.js</code></pre>
<p>此时终端输出如下,我们编写的Loader工作正常。</p>
<pre><code>Hello Loader</code></pre>
<h2>Loader选项</h2>
<p>我们使用第三方loader时经常可以看到传递选项的情况:</p>
<pre><code class="javascript">{
test:/\.js$/,
use:[
{
loader:'babel-loader',
options:{
plugins:['@babel/transform-runtime'],
presets:['@babel/env']
}
}
]
}</code></pre>
<p>在Loader编写时,Webpack中官方推荐通过loader-utils来读取配置选项,我们需要先安装。</p>
<pre><code class="bash">npm install loader-utils</code></pre>
<p>我们给刚才编写的replace-loader传递一个选项,允许自定义替换结果。</p>
<pre><code class="javascript">const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
return source.replace(/World/g, options.text);
};</code></pre>
<p>接下来编辑webpack.config.js,给replace-loader传递选项。</p>
<pre><code class="javascript">module.exports = {
module:{
rules:[
{
test:/\.js$/,
use:[
{
loader:'replace-loader',
options:{
text: 'Webpack4'
}
}
]
}
]
},
resolveLoader:{
modules: ['./node_modules', './loader']
}
};</code></pre>
<p>执行构建之后用Node.js执行build/main.js,可以看到输出的内容已经发生变化了。</p>
<pre><code>Hello Webpack4</code></pre>
<h2>异步Loader</h2>
<p>在Loader中,如果存在异步调用,那么就无法直接通过return返回构建后的结果了,此时需要使用到Webpack提供的回调函数将数据进行回调。</p>
<p>Webpack4给Loader提供了<code>this.async()</code>函数,调用之后返回一个callback,callback的签名如下:</p>
<pre><code class="javascript">function callback(
err: Error|null,
content: string|Buffer,
sourceMap?:SourceMap,
meta?: any
)</code></pre>
<p>例如我们需要在loader中调用setTimeout进行等待,则相应的代码如下:</p>
<pre><code class="javascript">module.exports = function(source) {
const callback = this.async();
setTimeout(() => {
const output = source.replace(/World/g, 'Webpack4');
callback(null, output);
}, 1000);
}</code></pre>
<p>执行构建,Webpack会等待一秒,然后再输出构建内容,通过Node.js执行构建后的文件,输出如下</p>
<pre><code>Hello Webpack4</code></pre>
<h2>"Raw" Loader</h2>
<p>默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 <code>raw</code>,loader 可以接收原始的 <code>Buffer</code>。比如处理非文本文件时(如图片等等)。</p>
<pre><code class="javascript">module.exports = function(source) {
assert(source instanceof Buffer);
return someSyncOperation(source);
};
module.exports.raw = true; // 设置当前Loader为raw loader, webpack会将原始的Buffer对象传入</code></pre>
<h2>读取loader配置文件</h2>
<p>babel-loader在使用时可以加载.babelrc配置文件来配置plugins和presets,减少了webpack.config.js的代码量,便于维护。接下来我们编写一个i18n-loader,通过读取语言配置文件完成语言转换。</p>
<h3>项目结构</h3>
<pre><code>|----loader
|----i18n-loader.js # loader
|----i18n
|----zh.json # 中文语言包
|----src
|----index.js # 入口文件
|----webpack.config.js</code></pre>
<p>i18n/zh.json</p>
<pre><code class="json">{
"hello": "你好",
"today": "今天"
}</code></pre>
<p>loader/i18n-loader.js</p>
<pre><code class="javascript">const loaderUtils = require('loader-utils');
const path = require('path');
module.exports = function (source) {
const options = loaderUtils.getOptions(this);
const locale = options ? options.locale : null;
// 读取语言配置文件
let json = null;
if (locale) {
const filename = path.resolve(__dirname, '..', 'i18n', `${locale}.json`);
json = require(filename);
}
// 读取语言标记 {{}}
const matches = source.match(/\{\{\w+\}\}/g);
for (const match of matches) {
const name = match.match(/\{\{(\w+)\}\}/)[1].toLowerCase();
if (json !== null && json[name] !== undefined) {
source = source.replace(match, json[name]);
} else {
source = source.replace(match, name);
}
}
return source;
}</code></pre>
<p>src/index.js</p>
<pre><code class="javascript">console.log('{{Hello}}, {{Today}} is a good day.');</code></pre>
<p>webpack.config.js</p>
<pre><code class="javascript">const path = require('path');
module.exports = {
entry: './src/index',
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
},
target: 'node',
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'i18n-loader',
options: { // 传递选项
locale: 'zh'
}
}
]
}
]
},
resolveLoader: {
modules: ['./node_modules', './loader']
}
};</code></pre>
<p>package.json</p>
<pre><code class="json">{
"scripts":{
"build":"webpack"
}
}</code></pre>
<h3>执行构建</h3>
<pre><code>npm run build</code></pre>
<p>构建完毕后使用Node.js执行build/main.js输出如下:</p>
<pre><code>你好, 今天 is a good day.</code></pre>
<p>可以看到i18n-loader成功读取了配置文件。</p>
<h2>小结</h2>
<p>本文简要介绍了Webpack中如何编写一个自定义的loader,权当抛砖引玉,更多的用法等待读者在实际工作中去挖掘,要想掌握Webpack的高级知识,Loader是必不可少的技能,有时候如果社区找不到合适的Loader,大家可以根据需要自己进行开发。</p>
<p><img src="/img/remote/1460000021980127" alt="0" title="0"></p>
Webpack4不求人(3) ——性能优化
https://segmentfault.com/a/1190000021933146
2020-03-06T15:02:56+08:00
2020-03-06T15:02:56+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
6
<h2>限定Webpack处理文件范围</h2>
<p>项目比较小的情况下Webpack的性能问题几乎可以忽略,但是一旦项目复杂度上升,Webpack会有额外的性能损失需要我们进行优化。</p>
<p>通过前面内容的学习我们可以知道Webpack主要干下面这些事情:</p>
<ol>
<li>通过entry指定的入口脚本进行依赖解析。</li>
<li>找到文件后通过配置的loader对其进行处理。</li>
</ol>
<p>因此,我们可以从这方面入手进行优化,减少Webpack搜索文件的范围,减少不必要的处理。</p>
<h3>loader配置</h3>
<p>在之前的内容中介绍过loader可以使用test、include、exclude配置项来匹配需要Loader处理的文件,因此推荐给每个loader定义test之后还定义include或exclude。</p>
<pre><code class="javascript">module.exports = {
module:{
rules:[
{
test:/\.js$/,
use:'babel-loader',
include: path.resolve(__dirname, 'src'), // 只处理src目录下的js文件
}
]
}
};</code></pre>
<h3>resolve.extensions配置</h3>
<p>导入未添加扩展名的模块时,Webpack会通过resolve.extensions后缀去检查文件是否存在。由于resolve.extensions是一个数组,如果数组项比较多,正确的后缀放置得越靠后,Webpack尝试次数就会越多,影响到性能。</p>
<p>因此配置resolve.extensions时需要遵守以下规则:</p>
<ul>
<li>尽量减少后缀列表,不要将不可能存在的文件后缀配置进来</li>
<li>出现频率越高的后缀尽量写到前面,比如可以将.js写在第一个</li>
<li>业务代码中导入模块时,可以手动加上后缀导入,省去Webpack查找过程</li>
</ul>
<h3>module.noParse配置</h3>
<p>module.noParse可以告诉Webpack忽略未采用模块系统文件的处理,可以有效地提高性能。比如常见的jQuery非常大,又没有采用模块系统,让Webpack解析这类型文件完全是浪费性能。</p>
<p>因此我们可以配置如下的module.noParse:</p>
<pre><code class="javascript">module.exports = {
module:{
noParse:[/jQuery/]
}
};</code></pre>
<h2>IgnorePlugin</h2>
<p>在导入模块时,IgnorePlugin可以忽略指定模块的生成。比如moment.js在导入时会自动导入本地化文件,一般情况下几乎不使用而且又比较大,此时可以通过IgnorePlugin忽略对本地化文件的生成,减小文件大小。</p>
<pre><code class="javascript">module.exports = {
plugins:[
new webpack.IgnorePlugin(/\.\/local/, /moment/)
]
};</code></pre>
<h2>DllPlugin</h2>
<p>使用过Windows操作系统的读者应该会经常看到以.dll扩展名的文件,这些文件叫做动态链接库,包含了其他程序或动态链接库的函数和数据。</p>
<p>Webpack的DllPlugin的思想是类似的,先将公共模块打包为独立的Dll模块,然后在业务代码中直接引用这些模块。采用DllPlugin之后会大大提升Webpack构建速度,原因在于,包含大量复用模块的动态链接库只需要编译一次,之后的构建中会直接引用这些构建好的模块。</p>
<p>在Webpack中使用动态链接库有以下两个步骤:</p>
<ol>
<li>通过webpack.DllPlugin插件打包出Dll库</li>
<li>通过webpack.DllReferencePlugin引用打包好的Dll库</li>
</ol>
<p>下面以React项目为例进行说明。</p>
<p>Dll库需要单独构建,因此我们需要一份单独的配置Webpack文件。</p>
<p>1.新建webpack.dll.config.js</p>
<pre><code class="javascript">const webpack = require('webpack');
module.exports = {
entry:{
react: ['react', 'react-dom']
},
output:{
filename: '_dll_[name].js', // 输出的文件名
path: path.resolve(__dirname, 'dist'), // 输出到dist目录
library: '_dll_[name]'
},
plugins: [
// name要等于output.library里的name
new webpack.DllPlugin({
name: "_dll_[name]",
path: path.resolve(__dirname, "dist", "manifest.json") // 清单文件路径
})
]
};</code></pre>
<p>2.编辑webpack.config.js</p>
<pre><code class="javascript">const webpack = require('webpack');
module.exports = {
entry: './src/main',
output:{
filename: '[name].js', // 输出的文件名
path: path.resolve(__dirname, 'dist'), // 输出到dist目录
},
plugins: [
// 传入manifest.json
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, "dist", "manifest.json") // 清单文件路径
})
]
};</code></pre>
<p>3.添加构建命令</p>
<pre><code class="json">{
"scripts":{
"build-dll":"webpack --config webpack.dll.config.js",
"build":"webpack"
}
}</code></pre>
<p>4.构建Dll</p>
<pre><code class="bash">npm run build-dll</code></pre>
<p>5.构建应用</p>
<pre><code class="bash">npm run build</code></pre>
<blockquote>Dll需要先构建,否则应用将构建失败</blockquote>
<h2>HappyPack</h2>
<p>Webpack默认情况下是单进程执行的,因此无法利用多核优势,通过HappyPack可以变成多进程构建,从而提升构建速度。下面我们一起来看看如何使用happypack来加速构建。</p>
<p>1.安装happypack</p>
<pre><code class="bash">npm isntall happypack</code></pre>
<p>2.编辑配置文件,需要将Loader配置到HappyPack插件中,由HappyPack对Loader进行调用。</p>
<pre><code class="javascript">const HappyPackPlugin = require('happypack');
const path = require('path');
module.exports = {
entry: './src/main',
output:{
path: path.resolve(__dirname, 'build'),
filename:'[name].js'
},
module:{
rules:[
{
test:/\.js$/,
use:'happypack/loader?id=js', // 配置id为js
include:[
path.resolve(__dirname,'src')
]
},
{
test:/\.scss$/,
use:'happypack/loader?id=scss', // 配置id为scss
include:[
path.resolve(__dirname,'src')
]
},
{
test:/\.css$/,
use:'happypack/loader?id=css', // 配置id为css
include:[
path.resolve(__dirname,'src')
]
}
]
},
plugins:[
new HappyPackPlugin({
id:'js', // id为js的loader配置
use:[
{
loader:'babel-loader',
options:{
plugins:['@babel/transform-runtime'],
presets:['@babel/env']
}
}
]
}),
new HappyPackPlugin({
id:'scss', // id为scss的loader配置
use:['style-loader','css-loader','sass-loader']
}),
new HappyPackPlugin({
id:'css', // id为css的loader配置
use:['style-loader','css-loader']
}),
]
};</code></pre>
<h2>Tree-Shaking</h2>
<p>Tree-Shaking原始的本意是”摇动树“,这样就会将一些分支”摇掉“,从而减少主干大小。而Webpack中的Tree-Shaking是类似的,在Webpack项目中,有一个入口文件,相当于树的主干,入口文件又依赖了许多模块。实际开发中,虽然依赖了某个模块,但其实只使用了其中的部分代码,通过Tree-Shaking,可以将模块中未使用的代码剔除掉,从而减少构建结果的大小。</p>
<blockquote>注意:只有使用ES6模块系统的代码,在mode为production时,Tree-Shaking才会生效。因此,在编写代码时尽量使用import/export的方式。</blockquote>
<h2>按需加载</h2>
<p>在开发中,我们一般会将业务代码打包为app.js,其他第三方依赖打包为vendor.js。这样会有一个比较大的问题,如果依赖的第三方模块过多,vendor.js会越来越大,而在浏览器加载时需要完全加载完vendor.js才可以,这样就会造成无谓的等待,因为我们当前页面可能只使用了一部分代码。此时可以使用Webpack来实现按需加载,只有在真正用到这个模块时才会加载相应的js。</p>
<p>比如基于echarts开发了一个数据可视化页面,可以在这个路由组件下面使用异步的方式加载echarts的代码:</p>
<pre><code class="javascript">import('echarts').then(modules => {
const echarts = modules.default;
const chart = echarts.init(document.querySelector('#chart'));
});</code></pre>
<p>不过使用按需加载时,构建代码中会包含Promise调用,因此低版本浏览器需要注入Promise的polyfill实现。</p>
<h2>提取公共代码</h2>
<p>Webpack4中可以将多个公共模块打包一份,减少代码冗余,Webpack4之前的版本是使用webpack内置的CommonsChunkPlugin实现的,Webpack4直接配置<code>optimization</code>即可。</p>
<pre><code class="javascript">module.exports = {
optimization:{
splitChunks:{
cacheGroups:{
common:{ // 应用代码中公共模块
chunks: 'all',
// 最小公共模块引用次数
minChunks: 2
},
vendor:{ // node_modules中第三方模块
test: /node_modules/,
chunks: 'all',
minChunks: 1
}
}
}
}
};</code></pre>
<p>第三方库代码的变更一般比较少(通过package.json的版本可以指定依赖版本),因此构建出来的vendor.js基本不会变就可以利用浏览器的缓存机制进行缓存。</p>
<p>而应用代码的变更是比较频繁的,因此单独打包为common.js,浏览器可以单独缓存,如果应用代码发生变更,浏览器只用重新下载common.js文件,而不用重新下载vendor.js。</p>
<h2>热更新</h2>
<p>HMR(Hot Module Replacement)是Webpack提供的常用功能之一,它允许在运行时对模块进行修改,而无需刷新整个页面(LiveReload需要刷新页面才能加载),这样有以下优势:</p>
<ul>
<li>保留应用状态,比如使用Vue/React时如果使用LiveReload,组件状态全部丢失,而HMR不会</li>
<li>只更新变更的内容,节省开发时间</li>
</ul>
<p>使用以下配置即可打开内置的HMR功能:</p>
<pre><code class="javascript">const webpack = require('webpack');
module.exports = {
devServer: {
hot: true, // 启用热加载
contentBase: './dist',
},
plugins:[
new webpack.NamedModulesPlugin(), // 打印更新的模块路径
new webpack.HotModuleReplacementPlugin() // 热更新插件
]
};</code></pre>
<h2>小结</h2>
<p>本文我们对Webpack4最常用的性能优化技术进行了学习,这些优化方法对业务代码的侵入性非常小(只有按需加载优化会要求使用import()函数进行加载),在实际的开发中,可以结合这些技术进行针对性的优化,比如开发时编译慢,可能就需要使用HappyPack插件进行多进程编译以加快编译速度等等。</p>
<p><img src="/img/remote/1460000020849746" alt="0.jpeg" title="0.jpeg"></p>
Webpack4不求人(2) ——手把手搭建TypeScript+React16+ReactRouter5同构应用脚手架
https://segmentfault.com/a/1190000021892364
2020-03-02T17:45:10+08:00
2020-03-02T17:45:10+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
6
<h2>同构应用</h2>
<p>使用同一份应用代码,同时提供浏览器环境和服务器环境下的应用,解决传统浏览器单页应用的两个顽固问题:</p>
<ul>
<li>不利于SEO,浏览器环境代码是在客户端渲染,大部分爬虫都只能爬到一个空白的入口文件</li>
<li>代码在浏览器渲染,低端机可能会卡顿</li>
</ul>
<p>接下来我们一起从零开始搭建基于Webpack的React同构应用脚手架。</p>
<h2>SSR流程</h2>
<ol>
<li>Web应用构建完成后输出CSS、JS和HTML</li>
<li>SSR应用构建完成后输出一个CommonJs模块文件,可以将虚拟DOM在服务端渲染为HTML字符串</li>
<li>Node.js新建HTTP服务器,收到请求后调用SSR模块导出的render函数输出HTML到客户端</li>
</ol>
<h2>初始化项目</h2>
<pre><code class="bash">mkdir react-ssr-example
cd react-ssr-example
yarn init -y
yarn add webpack webpack-cli webpack-dev-server -D # 安装Webpack
yarn add react react-dom react-router-dom # 安装React
yarn add @types/react @types/react-dom @types/react-router-dom -D # 安装React声明文件
yarn add express # 安装express
yarn add css-loader sass-loader node-sass mini-css-extract-plugin # 安装CSS相关模块
yarn add ts-loader typescript # 安装TypeScript
yarn add html-webpack-plugin # 安装HTML处理插件</code></pre>
<h2>目录结构</h2>
<p>脚手架的完整目录如下:(这些文件一步步都会有)</p>
<pre><code>|----build # 构建结果目录
|----styles # 样式
|----main.css
|----bundle.ssr.js # SSR应用文件
|----bundle.web.js # Web应用文件
|----index.html # Web应用入口HTML
|----src # 应用源码
|----home # 首页组件
|----index.scss # 首页SCSS
|----index.tsx # 首页组件
|----signin # 登录页组件
|----index.scss # 登录页SCSS
|----index.tsx # 登录页组件
|----App.tsx # 应用路由设置
|----index.html # Web应用入口HTML
|----main.ssr.tsx # SSR入口文件
|----main.web.tsx # Web入口文件
|----index.js # express服务器入口
|----package.json
|----tsconfig.json # TypeScript配置文件
|----webpack.config.js # Web应用webpack配置
|----webconfig.ssr.config.js # SSR应用Webpack配置</code></pre>
<h2>工具配置</h2>
<p>1.TypeScript配置,新建tsconfig.json</p>
<pre><code class="json">{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"strict": true,
"lib": [
"DOM"
],
"esModuleInterop": true
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
],
"exclude": [
"node_modules"
]
}</code></pre>
<p>主要是添加了jsx设置和include设置</p>
<p>2.Web环境webpack配置,新建webpack.config.js</p>
<pre><code class="javascript">const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/main.web', // 入口文件
output: {
path: path.resolve(__dirname, 'build'), // 输出目录
filename: 'bundle.web.js' // 输出文件
},
module: {
rules: [
{
test: /\.tsx?$/, // ts文件处理
use: 'ts-loader'
},
{
test: /\.scss$/, // scss文件处理
use: [MiniCssPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.css$/, // css文件处理
use: [MiniCssPlugin.loader, 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
chunks: ['main'], // chunk名称,entry是字符串类型,因此chunk为main
filename: 'index.html', // 输出到build目录的文件名
template: 'src/index.html' // 模板路径
}),
new MiniCssPlugin({
filename: 'styles/[name].[contenthash:8].css', // 输出的CSS文件名
chunkFilename: 'styles/[name].[contenthash:8].css'
})
],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'] // 添加ts和tsx后缀
}
};</code></pre>
<p>3.SSR环境Webpack配置,新建webpack.ssr.config.js</p>
<pre><code class="javascript">const path = require('path');
const MiniCssPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/main.ssr',
target: 'node', // 必须指定为Node.js,否则会打包Node.js内置模块
output: {
path: path.resolve(__dirname, 'build'),
filename: 'bundle.ssr.js',
libraryTarget: 'commonjs2' // 打包为CommonJs模块才能被Node.js加载
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader'
},
{
test: /\.scss$/,
use: [MiniCssPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.css$/,
use: [MiniCssPlugin.loader, 'css-loader']
}
]
},
plugins: [
new MiniCssPlugin({
filename: 'styles/[name].[contenthash:8].css',
chunkFilename: 'styles/[name].[contenthash:8].css'
})
],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json']
}
};</code></pre>
<p>4.package.json添加npm命令</p>
<pre><code class="json">{
"scripts": {
"build": "webpack",
"start": "webpack-dev-server",
"build-ssr": "webpack --config webpack.ssr.config.js"
}
}</code></pre>
<h2>应用编码</h2>
<p>src/home/index.tsx</p>
<pre><code class="tsx">import React from 'react';
import './index.scss';
export default class Home extends React.Component {
render() {
return (
<div className="main">首页</div>
)
}
}</code></pre>
<p>src/home/index.scss</p>
<pre><code class="scss">.main {
color: red;
}</code></pre>
<p>src/signin/index.tsx</p>
<pre><code class="tsx">import React from 'react';
import { withRouter } from 'react-router-dom';
function SignIn(props: any) {
return (
<button onClick={() => props.history.replace('/')}>登录</button>
)
}
export default withRouter(SignIn);</code></pre>
<p>src/App.tsx</p>
<pre><code class="tsx">import React from 'react';
import { Switch, Route, Link } from 'react-router-dom'; // router
// 导入页面组件
import Home from './home';
import SignIn from './signin';
// 导出路由组件配置
export default function App() {
return (
<Switch>
<Route path="/signin" component={SignIn} />
<Route path="/" component={Home} />
</Switch>
)
}</code></pre>
<p>index.html</p>
<pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
</head>
<body>
<div id="app"></div>
</body>
</html></code></pre>
<p>src/main.ssr.tsx</p>
<pre><code class="tsx">import React from 'react';
import { StaticRouter, Link } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import App from './App'; // 将路由组件导入进来
export function render(req: any) { // 导出一个渲染函数,根据请求链接进行分发
const context = {};
const html = renderToString(
<StaticRouter location={req.url} context={context}>
<header>
<nav>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/signin">登录</Link></li>
</ul>
</nav>
</header>
<App />
</StaticRouter>
);
return [html, context]; // 导出context和html渲染结果
}</code></pre>
<p>src/main.web.tsx</p>
<pre><code class="tsx">import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Link } from 'react-router-dom';
import App from './App';
ReactDOM.render( // 渲染路由
<BrowserRouter>
<header>
<nav>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/signin">登录</Link></li>
</ul>
</nav>
</header>
<App />
</BrowserRouter>, document.querySelector('#app'))</code></pre>
<p>index.js</p>
<pre><code class="javascript">const express = require('express'); // 加载express
const { render } = require('./build/bundle.ssr'); // 加载ssr
const app = express();
app.use(express.static('.')) // 静态资源配置
app.get('/*', (req, res) => { // 所有请求都走这里处理,必须加*
const [html, context] = render(req)
console.log(context) // context目前没发现啥用处
res.send(`
<html>
<head>
<meta charset="UTF-8">
<title>SSR</title>
<link href="build/styles/main.8f173ff5.css" rel="stylesheet">
</head>
<body>
<div id="app">${html}</div>
<script src="build/bundle.web.js"></script>
</body>
</html>
`);
console.log(context)
});
app.listen(8080)</code></pre>
<p>注意:</p>
<ul>
<li>静态资源配置必须在最上面</li>
<li>app.get('/<em>')必须有</em>号</li>
<li>HTML字符串必须手动引入CSS和Web构建结果</li>
</ul>
<h2>执行构建</h2>
<pre><code class="bash">npm run build # 构建Web
npm run build-ssr # 构建SSR
node index.js # 启动Express服务器</code></pre>
<h2>查看结果</h2>
<p>首页样式</p>
<p><img src="/img/remote/1460000021892368" alt="image-20200302173258430" title="image-20200302173258430"></p>
<p>首页代码</p>
<p><img src="/img/remote/1460000021892367" alt="image-20200302173322601" title="image-20200302173322601"></p>
<p>登录页样式</p>
<p><img src="/img/remote/1460000021892370" alt="image-20200302173344488" title="image-20200302173344488"></p>
<p>登录页代码</p>
<p><img src="/img/remote/1460000021892369" alt="image-20200302173405907" title="image-20200302173405907"></p>
<h2>源码地址</h2>
<p><a>Https://github.com/xialeistud...</a></p>
<p><img src="/img/remote/1460000020849746" alt="0.jpeg" title="0.jpeg"></p>
Webpack4不求人系列(1)
https://segmentfault.com/a/1190000021338375
2019-12-19T15:17:19+08:00
2019-12-19T15:17:19+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
36
<blockquote>Webpack是一个现在Javascript应用程序的模块化打包器,在Webpack中JS/CSS/图片等资源都被视为JS模块,简化了编程。当Webpack构建时,会递归形成一个模块依赖关系图,然后将所有的模块打包为一个或多个bundle。</blockquote>
<p><img src="/img/remote/1460000021338378" alt="img" title="img"></p>
<p>本文内容</p>
<ol>
<li>简介</li>
<li>常用loader && plugin</li>
<li>传统网站的webpack配置</li>
</ol>
<h2>简介</h2>
<p>要系统地学习Webpack,需要先了解Webpack的四个<strong>核心概念</strong>:</p>
<ul>
<li>入口(entry)</li>
<li>输出(output)</li>
<li>loader</li>
<li>plugin</li>
</ul>
<p>webpack使用Node.js运行,因此所有的Node.js模块都可以使用,比如文件系统、路径等模块。</p>
<p>对Node.js基础不太了解的读者,可以参考我的<a href="https://link.segmentfault.com/?enc=xpGj6and1soB2iWiAzsvBA%3D%3D.pKSCPY2neHGFineD3SWOpTvJBtqoDg9ftjqSyfHHPMh7LX1GjeTXJgfh7%2FQH%2BG7NahQAaPR8Hvp5dKjlP3FTHw%3D%3D" rel="nofollow">Node.js系列</a></p>
<p>配置文件<code>webpack.config.js</code>的一般格式为:</p>
<pre><code class="javascript">const path = require('path'); // 导入Node.js的path模块
module.exports = {
mode: 'development', // 工作模式
entry: './src/index', // 入口点
output: { // 输出配置
path: path.resolve(__dirname, 'dist'), // 输出文件的目录
filename: 'scripts/[name].[hash:8].js', // 输出JS模块的配置
chunkFilename:'scripts/[name].[chunkhash:8].js', // 公共JS配置
publicPath:'/' // 资源路径前缀,一般会使用CDN地址,这样图片和CSS就会使用CDN的绝对URL
},
module:{
rules: [
{
test:/\.(png|gif|jpg)$/, // 图片文件
use:[
{
loader:'file-loader', // 使用file-loader加载
options:{ // file-loader使用的加载选项
name:'images/[name].[hash:8].[ext]' // 图片文件打包后的输出路径配置
}
}
]
}
]
},
plugins:[ // 插件配置
new CleanWebpackPlugin()
]
};</code></pre>
<blockquote>Webpack自己只管JS模块的输出,也就是output.filename是JS的配置,CSS、图片这些是通过loader来处理输出的</blockquote>
<h3>入口</h3>
<p>入口指明了Webpack从哪个模块开始进行构建,Webpack会分析入口模块依赖到的模块(直接或间接),最终输出到一个被称为<em>bundle</em>的文件中。</p>
<blockquote>使用<strong>entry</strong>来配置项目入口。</blockquote>
<p><strong>单一入口</strong></p>
<p>最终只会生成1个js文件</p>
<pre><code class="javascript">module.exports = {
entry: './src/index',
};</code></pre>
<p><strong>多个入口</strong></p>
<p>最终会根据入口数量生成对应的js文件</p>
<pre><code class="javascript">module.exports = {
entry:{
home:'./src/home/index', // 首页JS
about:'./src/about/index' // 关于页JS
}
};</code></pre>
<p>多个入口一般会在多页面应用中使用,比如传统的新闻网站。</p>
<h3>输出</h3>
<p>输出指明了Webpack将bundle输出到哪个目录,以及这些bundle如何命名等,默认的目录为<code>./dist</code>。</p>
<pre><code class="javascript">module.exports = {
output:{
path:path.resolve(__dirname, 'dist'), // 输出路径
filename:'scripts/[name].[hash:8].js', // 输出JS模块的文件名规范
chunkFilename:'scripts/[name].[chunkhash:8].js', // 公共JS的配置
publicPath:'/', // 资源路径前缀,一般会使用CDN地址,这样图片和CSS就会使用CDN的绝对URL
}
};</code></pre>
<p><strong>path</strong></p>
<p>path是打包后bundle的输出目录,<strong>必须使用绝对路径</strong>。所有类型的模块(js/css/图片等)都会输出到该目录中,当然,我们可以通过配置输出模块的名称规则来输出到path下的子目录。比如上例中最终输出的JS目录如下:</p>
<pre><code>|----dist
|---- scripts
|---- home.aaaaaaaa.js</code></pre>
<p><strong>filename</strong></p>
<p><strong>入口模块</strong>输出的命名规则,在Webpack中,只有js是亲儿子,可以直接被Webpack处理,其他类型的文件(css/images等)需要通过loader来进行转换。</p>
<p>filename的常用的命名如下:</p>
<pre><code>[name].[hash].js</code></pre>
<ul>
<li>[name] 为定义入口模块的名称,比如定义了home的入口点,这里的name最终就是home</li>
<li>[hash] 是模块内容的MD5值,一次打包过程中所有模块的hash值是相同的,由于浏览器会按照文件名缓存,因此每次打包都需要指定hash来改变文件名,从而清除缓存。</li>
</ul>
<p><strong>chunkFilename</strong></p>
<p><strong>非入口模块</strong>输出的命名规则,一般是代码中引入其他依赖,同时使用了optimization.splitChunks配置会抽取该类型的chunk</p>
<p><strong>hash</strong></p>
<p>Webpack中常见的hash有<code>hash</code>,<code>contenthash</code>,<code>chunkhash</code>,很容易弄混淆,这里说明一下。</p>
<ul>
<li>hash 整个项目公用的hash值,不管修改项目的什么文件,该值都会变化</li>
<li>chunkhash 公共代码模块的hash值,只要不改该chunk下的代码,该值不会变化</li>
<li>contenthash 基于文件内容生成的hash,只要改了文件,对应的hash都会变化</li>
</ul>
<p><strong>publicPath</strong></p>
<p>资源的路径前缀,打包之后的资源默认情况下都是相对路径,当更改了部署路径或者需要使用CDN地址时,该选项比较常用。</p>
<p>比如我们把本地编译过程中产生的所有资源都放到一个CDN路径中,可以这么定义:</p>
<pre><code>publicPath: 'https://static.ddhigh.com/blog/'</code></pre>
<p>那么最终编译的js,css,image等路径都是绝对链接。</p>
<h3>loader</h3>
<p>loader用来在import时预处理文件,一般用来将非JS模块转换为JS能支持的模块,比如我们直接import一个css文件会提示错误,此时就需要loader做转换了。</p>
<p>比如我们使用loader来加载css文件。</p>
<pre><code class="javascript">module.exports = {
module:{
rules:[
{
test: /\.(css)$/,
use: ['css-loader']
}
]
}
};</code></pre>
<h4>配置方式</h4>
<p>Webpack中有<strong>3</strong>种使用loader的方式:</p>
<ol>
<li>配置式:在webpack.config.js根据文件类型进行配置,这是推荐的配置</li>
<li>内联:在代码中import时指明loader</li>
<li>命令行:通过cli命令行配置</li>
</ol>
<h4>配置式</h4>
<p><strong>module.rules</strong>用来配置loader。<strong>test</strong>用来对加载的<strong>文件名(包括目录)</strong>进行正则匹配,只有当匹配时才会应用对应loader。</p>
<blockquote>多个loader配置时从右向左进行应用</blockquote>
<p>配置式Webpack的loader也有好几种形式,有些是为了兼容而添加的,主要使用的方式有以下3种。</p>
<pre><code class="javascript">module.exports = {
module:{
rules:[
{
test: /\.less$/,
loader:'css-loader!less-loader', // 多个loader中用感叹号分隔
},
{
test:/\.css/,
use:['css-loader'],//数组形式
},
{
test:/\.(png|gif|jpg)$/,
use:[ // loader传递参数时建议该方法
{
loader: 'file-loader',
options:{ // file-loader自己的参数,跟webpack无关
name: 'images/[name].[hash:8].js'
}
}
]
}
]
}
};</code></pre>
<blockquote>每个loader的options参数不一定相同,这个需要查看对应loader的官方文档。</blockquote>
<h3>Plugin</h3>
<p>loader一般用来做模块转换,而插件可以执行更多的任务,包括打包优化、压缩、文件拷贝等等。插件的功能非常强大,可以进行各种各样的任务。</p>
<p>下面是打包之前清空dist目录的插件配置示例。</p>
<pre><code class="javascript">const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin(),
]
};</code></pre>
<p>插件也可以传入选项,一般在实例化时进行传入。</p>
<pre><code class="javascript">new MiniCssPlugin({
filename: 'styles/[name].[contenthash:8].css',
chunkFilename: 'styles/[name].[contenthash:8].css'
})</code></pre>
<h2>提取公共代码</h2>
<p>Webpack4中提取公共代码只需要配置optimization.splitChunks即可。</p>
<pre><code class="javascript">optimization: {
splitChunks: {
cacheGroups: {
vendor: { // 名为vendor的chunk
name: "vendor",
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
priority: 10
},
styles: { // 名为styles的chunk
name: 'styles',
test: /\.css$/,
chunks: 'all'
}
}
}
},</code></pre>
<ul>
<li>cacheGroups 缓存组</li>
<li>name chunk的名称</li>
<li>test 加载的模块符合该正则时被打包到该chunk</li>
<li>chunks 模块的范围,有initial(初始模块),async(按需加载模块),all(全部模块)</li>
</ul>
<p>上面的例子中将node_modules中的js打包为vendor,以css结尾的打包为styles</p>
<h2>常用的loader和plugin</h2>
<h3>css-loader</h3>
<blockquote>加载css文件</blockquote>
<pre><code class="javascript">{
test:/\.css$/
loader:['css-loader']
}</code></pre>
<h3>less-loader</h3>
<blockquote>加载less文件,一般需要配合css-loader</blockquote>
<pre><code class="javascript">{
test:/\.less$/,
loader:['css-loader','less-loader']
}</code></pre>
<h3>file-loader</h3>
<blockquote>将文件拷贝到输出文件夹,并返回相对路径。一般常用在加载图片</blockquote>
<pre><code class="javascript">{
test:/\.(png|gif|jpg)/,
use:[
{
loader:'file-loader',
options:{
name:'images/[name].[hash:8].[ext]'
}
}
]
}</code></pre>
<h3>babel-loader</h3>
<blockquote>转换ES2015+代码到ES5</blockquote>
<pre><code class="javascript">{
test:/\.js$/,
exclude: /(node_modules|bower_components)/, // 排除指定的模块
use:[
{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
}
]
}</code></pre>
<h3>ts-loader</h3>
<blockquote>转换Typescript到Javascript</blockquote>
<pre><code class="javascript">{
test:/\.ts/,
loader:'ts-loader'
}</code></pre>
<h3>html-webpack-plugin</h3>
<blockquote>简化HTML的创建,该插件会自动将当前打包的资源(如JS、CSS)自动引用到HTML文件</blockquote>
<pre><code class="javascript">const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins:[
new HtmlWebpackPlugin()
]
};</code></pre>
<h3>clean-webpack-plugin</h3>
<blockquote>打包之前清理dist目录</blockquote>
<pre><code class="javascript">const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins:[
new CleanWebpackPlugin()
]
};</code></pre>
<h3>mini-css-extract-plugin</h3>
<blockquote>提取、压缩CSS,需要同时配置loader和plugin</blockquote>
<pre><code class="javascript">const MiniCssPlugin = require('mini-css-extract-plugin');
module.exports = {
module:{
rules:[
{
test: /\.less$/,
use: [MiniCssPlugin.loader, 'css-loader', 'less-loader']
},
{
test: /\.css$/,
use: [MiniCssPlugin.loader, 'css-loader']
},
]
},
plugins:[
new MiniCssPlugin({
filename: 'styles/[name].[contenthash:8].css',
chunkFilename: 'styles/[name].[contenthash:8].css'
}),
]
};</code></pre>
<h2>实战</h2>
<p>下面使用Webpack来配置一个传统多页面网站开发的示例。</p>
<h3>目录结构</h3>
<pre><code>├── package.json
├── src
│ ├── about 关于页
│ │ ├── index.html
│ │ ├── index.js
│ │ └── style.less
│ ├── common
│ │ └── style.less
│ └── home 首页
│ ├── images
│ │ └── logo.png
│ ├── index.html
│ ├── index.js
│ └── style.less
├── webpack.config.js</code></pre>
<h3>使用到的npm包</h3>
<pre><code>"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.2.1",
"exports-loader": "^0.7.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^5.0.2",
"html-webpack-plugin": "^3.2.0",
"html-withimg-loader": "^0.1.16",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"mini-css-extract-plugin": "^0.8.0",
"normalize.css": "^8.0.1",
"script-loader": "^0.7.2",
"style-loader": "^1.0.1",
"url-loader": "^3.0.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"zepto": "^1.2.0"</code></pre>
<h3>配置入口点</h3>
<p>由于是传统多页网站,每个页面都需要单独打包一份JS,因此<strong>每个页面需要一个入口</strong>。</p>
<pre><code class="javascript">entry: { // 入口配置,每个页面一个入口JS
home: './src/home/index', // 首页
about: './src/about/index' // 关于页
}</code></pre>
<h3>配置输出</h3>
<p>本例我们不进行CDN部署,因此输出点配置比较简单。</p>
<pre><code class="javascript">output: { // 输出配置
path: path.resolve(__dirname, 'dist'), // 输出资源目录
filename: 'scripts/[name].[hash:8].js', // 入口点JS命名规则
chunkFilename: 'scripts/[name]:[chunkhash:8].js', // 公共模块命名规则
publicPath: '/' // 资源路径前缀
}</code></pre>
<h3>配置开发服务器</h3>
<p>本地开发时不需要每次都编译完Webpack再访问,通过webpack-dev-server,我们可以边开发变查看效果,文件会实时编译。</p>
<pre><code class="javascript">devServer: {
contentBase: './dist', // 开发服务器配置
hot: true // 热加载
},</code></pre>
<h3>配置loader</h3>
<p>本例中没有使用ES6进行编程,但是引用了一个非CommonJS的js模块<code>Zepto</code>,传统用法中在HTML页面引入Zepto就会在window下挂载全局对象Zepto。但是在Webpack开发中不建议使用全局变量,否则模块化的优势将受到影响。</p>
<p>通过使用exports-loader和script-loader,我们可以将Zepto包装为CommonJS模块进入导入。</p>
<pre><code class="javascript">module: {
rules: [
{
test: require.resolve('zepto'),
loader: 'exports-loader?window.Zepto!script-loader' // 将window.Zepto包装为CommonJS模块
},
{
test: /\.less$/,
use: [MiniCssPlugin.loader, 'css-loader', 'less-loader']
},
{
test: /\.css$/,
use: [MiniCssPlugin.loader, 'css-loader']
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: 'images/[name].[hash:8].[ext]'
}
}
]
},
{
test: /\.(htm|html)$/i,
loader: 'html-withimg-loader'
}
]
},</code></pre>
<h3>配置optimization</h3>
<p>主要进行公共模块的打包配置。</p>
<pre><code class="javascript">optimization: {
splitChunks: {
cacheGroups: {
vendor: {
name: "vendor",
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
priority: 10, // 优先级
},
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all'
}
}
}
},</code></pre>
<h3>配置plugin</h3>
<pre><code class="javascript">plugins: [
new CleanWebpackPlugin(), // 清理发布目录
new HtmlWebpackPlugin({
chunks: ['home', 'vendor', 'styles'], // 声明本页面使用到的模块,有主页,公共JS以及公共CSS
filename: 'index.html', // 输出路径,这里直接输出到dist的根目录,也就是dist/index.html
template: './src/home/index.html', // HTML模板文件路径
minify: {
removeComments: true, // 移除注释
collapseWhitespace: true // 合并空格
}
}),
new HtmlWebpackPlugin({
chunks: ['about', 'vendor', 'styles'],
filename: 'about/index.html', // 输出到dist/about/index.html
template: './src/about/index.html',
minify: {
removeComments: true,
collapseWhitespace: true
}
}),
new MiniCssPlugin({
filename: 'styles/[name].[contenthash:8].css',
chunkFilename: 'styles/[name].[contenthash:8].css'
}),
new webpack.NamedModulesPlugin(), // 热加载使用
new webpack.HotModuleReplacementPlugin() // 热加载使用
]</code></pre>
<h3>示例代码</h3>
<p>部分示例代码如下:</p>
<pre><code class="javascript">// src/about/index.js
const $ = require('zepto');
require('normalize.css');
require('../common/style.less');
require('./style.less');
$('#about').on('click', function () {
alert('点击了about按钮');
});</code></pre>
<p>和传统的JS有点不太一样,多了一些css的require,前面说过,webpack把所有资源当做JS模块,因此这是推荐的做法。</p>
<pre><code class="html"><!--首页-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>首页</title>
</head>
<body>
<ul>
<li><a href="/">首页</a> </li>
<li><a href="/about">关于</a></li>
</ul>
<div class="logo"></div>
<button id="home">首页按钮</button>
</body>
</html></code></pre>
<p>页面中不再需要编写JS。</p>
<blockquote>注意:html中使用<img />标签导入图片的编译,目前还没有好的解决办法,可以通过css background的形式进行处理</blockquote>
<h3>开发模式</h3>
<p>开发模式下直接启用webpack-dev-server即可,会自动加载工作目录下的webpack.config.js</p>
<pre><code>// package.json
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server"
}</code></pre>
<pre><code class="bash">npm run dev</code></pre>
<h3>生产模式</h3>
<p>生产模式下使用webpack编译,编译完成后输出最终文件。</p>
<pre><code class="bash">npm run build</code></pre>
<h3>输出效果</h3>
<pre><code>├── about
│ └── index.html
├── images
│ └── logo.b15c113a.png
├── index.html
├── scripts
│ ├── about.3fb4aa0f.js
│ ├── home.3fb4aa0f.js
│ └── vendor:ed5b7d31.js
└── styles
├── about.71eb65e9.css
├── home.cd2738e6.css
└── vendor.9df34e21.css</code></pre>
<h3>项目地址</h3>
<p>项目已经托管到github,有需要的读者可以自取。</p>
<p><a href="https://link.segmentfault.com/?enc=Fgx95kXEAcpb9Z%2Bd8ElStA%3D%3D.GfOjJjLhXGapHVfQFfMXIHZkKI6g2UPGkJhw6TWChawKuaaWKg8g0DdS5MX7UfrKOgnJR%2BUworBwkBPl1aAgiA%3D%3D" rel="nofollow">https://github.com/xialeistudio/webpack-multipage-example</a></p>
<p><img src="/img/remote/1460000020849746" alt="0.jpeg" title="0.jpeg"></p>
我是如何发现我的文章被侵权以及如何得到侵权网站的联系方式的?
https://segmentfault.com/a/1190000020856988
2019-10-30T11:42:19+08:00
2019-10-30T11:42:19+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
16
<p>本文内容</p>
<ul>
<li>如何发现自己的文章被侵权</li>
<li>如何结合whois信息查询侵权网站的联系方式</li>
</ul>
<blockquote>声明:本文只做技术研究,请勿用于非法目的,如果恶意使用造成任何法律责任本人概不负责!</blockquote>
<h2>发现侵权</h2>
<p>我的文章除了发布在博客之外,还会同步到思否平台。自己没事的时候会去看看百度统计,比较关注来源网站(也就是referer),一般来说通过搜索引擎过来的流量我不太关注,私人网站过来的就比较关注了,昨天查看来源的时候看到了一个新网站。</p>
<p><img src="/img/remote/1460000020856991" alt="image-20191030111724873" title="image-20191030111724873"></p>
<p>可以看到这个 www.twblogs.net是一个普通网站,点进去发现这样一篇文章。</p>
<p><img src="/img/remote/1460000020856992" alt="image-20191030111833685" title="image-20191030111833685"></p>
<p>可以看到这篇文章的作者是xialeistudio(也就是我常用的网络用户名),可我压根就没听说过这个网站,我意识到可能被爬虫爬取了。</p>
<p>点击作者进入到作者的文章页,发现我昨天下午刚发布没多久的文章就被爬了。</p>
<p><img src="/img/remote/1460000020856993" alt="image-20191030112018569" title="image-20191030112018569"></p>
<p>然后我去查看nginx的访问日志,没有发现异常访问,有个IP虽然是美国的,但是是Google的爬虫。这意味着文章不是直接爬取我的博客来的。</p>
<blockquote>nginx的访问日志过滤使用shell命令即可做到,不过这不是本文的重点,此处略过</blockquote>
<p>那就只剩下思否和掘金,通过文章中的公众号图片二维码,我发现了爬取的文章来源。</p>
<p><img src="/img/remote/1460000020856994" alt="image-20191030112207253" title="image-20191030112207253"></p>
<p>可以看到是通过思否爬取到的。</p>
<h3>侵权结论</h3>
<ul>
<li>伪造用户名爬取了思否大量的文章,截止发文时大概爬了三四十篇文章</li>
<li>思否应该并不知道这件事,应该早期的文章都爬过来了(包括我在思否已经删除的文章)</li>
</ul>
<h2>获取侵权网站联系方式</h2>
<p>目前手头只有一个域名<a href="https://link.segmentfault.com/?enc=ze8n7cl8QFI4Yt9%2B%2Fjrk3Q%3D%3D.Mpq6LE8IIsWMAe9l8MNhD5f4WE9UU5yKreDvEXgW%2Fkw%3D" rel="nofollow">https://www.twblogs.net</a>,先去站长工具的whois查询工具<a href="https://link.segmentfault.com/?enc=A5aKUHAJaLWfstw3UsIHbw%3D%3D.Td7%2FcqIwpLKbSkIm7OdHRtSeIPUxk1pGVukyTkWHGeI%3D" rel="nofollow">http://whois.chinaz.com/</a>查询一下。</p>
<blockquote>whois:用来查询域名注册信息的一种技术</blockquote>
<p>通过查询并没有得到什么有效的信息。看来是whois做了保护处理。</p>
<p><img src="/img/remote/1460000020856995" alt="image-20191030112649319" title="image-20191030112649319"></p>
<p>不过没关系,目前得到了twblogs.net的域名提供商是Goddy,我们可以到域名提供商的网站看一下Whois信息</p>
<p><img src="/img/remote/1460000020856996" alt="image-20191030112807217" title="image-20191030112807217"></p>
<p>浏览器打开whois.godaddy.com输入www.twblogs.net就可以了,查询到的Whois信息如下:</p>
<pre><code>WHOIS 搜索结果
Domain Name: twblogs.net
Registry Domain ID: 2330628228_DOMAIN_NET-VRSN
Registrar WHOIS Server: whois.godaddy.com
Registrar URL: http://www.godaddy.com
Updated Date: 2019-10-09T11:09:43Z
Creation Date: 2018-11-08T16:30:46Z
Registrar Registration Expiration Date: 2021-11-08T16:30:46Z
Registrar: GoDaddy.com, LLC
Registrar IANA ID: 146
Registrar Abuse Contact Email: abuse@godaddy.com
Registrar Abuse Contact Phone: +1.4806242505
Domain Status: clientTransferProhibited http://www.icann.org/epp#clientTransferProhibited
Domain Status: clientUpdateProhibited http://www.icann.org/epp#clientUpdateProhibited
Domain Status: clientRenewProhibited http://www.icann.org/epp#clientRenewProhibited
Domain Status: clientDeleteProhibited http://www.icann.org/epp#clientDeleteProhibited
Registry Registrant ID: Not Available From Registry
Registrant Name: kiwi robert
Registrant Organization:
Registrant Street: american
Registrant City: austin
Registrant State/Province: Anhui
Registrant Postal Code: 73344
Registrant Country: CN
Registrant Phone: +86.15220288017
Registrant Phone Ext:
Registrant Fax:
Registrant Fax Ext:
Registrant Email: 898611548@qq.com
Registry Admin ID: Not Available From Registry
Admin Name: kiwi robert
Admin Organization:
Admin Street: american
Admin City: austin
Admin State/Province: Anhui
Admin Postal Code: 73344
Admin Country: CN
Admin Phone: +86.15220288017
Admin Phone Ext:
Admin Fax:
Admin Fax Ext:
Admin Email: 898611548@qq.com
Registry Tech ID: Not Available From Registry
Tech Name: kiwi robert
Tech Organization:
Tech Street: american
Tech City: austin
Tech State/Province: Anhui
Tech Postal Code: 73344
Tech Country: CN
Tech Phone: +86.15220288017
Tech Phone Ext:
Tech Fax:
Tech Fax Ext:
Tech Email: 898611548@qq.com
Name Server: JOBS.NS.CLOUDFLARE.COM
Name Server: LANA.NS.CLOUDFLARE.COM
DNSSEC: unsigned
URL of the ICANN WHOIS Data Problem Reporting System: http://wdprs.internic.net/
>>> Last update of WHOIS database: 2019-10-30T03:00:00Z <<<</code></pre>
<p>可以看到域名注册者有点意思,<code>手机号是深圳的</code>,<code>省份是安徽的</code>,估计是<code>安徽人到深圳上班</code>。</p>
<p>拿到联系信息之后就通知思否小姐姐去发律师函了</p>
<h2>总结</h2>
<ol>
<li>通过百度统计发现来源网站</li>
<li>通过访问来源网站发现内容侵权</li>
<li>check博客服务器的nginx访问日志,看看有无明显异常的访问</li>
<li>通过文章中的图片发现文章是通过思否爬取的</li>
<li>检测侵权网站的whois信息,如果做了保护就去域名提供商获取whois</li>
<li>获取到联系方式之后可以联系思否小姐姐帮你处理,给思否点个赞,内容这块的响应速度很快</li>
</ol>
<p>大家发现侵权不要不作为,可以处理的方法如下:</p>
<ol>
<li>向国家新闻出版广电总局举报<a href="https://link.segmentfault.com/?enc=cTq4NfrIekPW8IuQaT7yRA%3D%3D.pCbtEPCIpd7NjGfJnHXcRidgd68zddQ8Yx7Yea47J89OsxUj25VymiJng32ZWuCyiIWepTgtDm%2Fi%2F5%2B6rSpKdw%3D%3D" rel="nofollow">http://www.sapprft.gov.cn/sapprft/channels/6979.shtml</a>
</li>
<li>联系平台(思否,知乎,掘金等等),平台会帮你发律师函啥的,毕竟内容使他们的根基</li>
</ol>
<blockquote>《中华人民共和国刑法》节选如下:<p>第二百一十七条 【侵犯著作权罪】以营利为目的,有下列侵犯著作权情形之一,违法所得数额较大或者有其他严重情节的,处三年以下有期徒刑或者拘役,并处或者单处罚金;违法所得数额巨大或者有其他特别严重情节的,<code>处三年以上七年以下有期徒刑,并处罚金</code>:</p>
<p>(一)未经著作权人许可,复制发行其文字作品、音乐、电影、电视、录像作品、计算机软件及其他作品的;</p>
<p>(二)出版他人享有专有出版权的图书的;</p>
<p>(三)未经录音录像制作者许可,复制发行其制作的录音录像的;</p>
<p>(四)制作、出售假冒他人署名的美术作品的。</p>
</blockquote>
<p><img src="/img/remote/1460000020849746" alt="0.jpeg" title="0.jpeg"></p>
Redis优化高并发下的秒杀性能
https://segmentfault.com/a/1190000020849743
2019-10-29T17:16:11+08:00
2019-10-29T17:16:11+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
19
<p>本文内容</p>
<ul>
<li>使用Redis优化高并发场景下的接口性能</li>
<li>数据库乐观锁</li>
</ul>
<p>随着双11的临近,各种促销活动开始变得热门起来,比较主流的有秒杀、抢优惠券、拼团等等。</p>
<p>涉及到高并发争抢同一个资源的主要场景有秒杀和抢优惠券。</p>
<h2>前提</h2>
<p>活动规则</p>
<ul>
<li>奖品数量有限,比如100个</li>
<li>不限制参与用户数</li>
<li>每个用户只能参与1次秒杀</li>
</ul>
<p>活动要求</p>
<ul>
<li>不能多发,也不能少发,100个奖品要全部发出去</li>
<li>1个用户最多抢1个奖品</li>
<li>遵循先到先得原则,先来的用户有奖品</li>
</ul>
<h2>数据库实现</h2>
<p>悲观锁性能太差,本文不予讨论,讨论一下使用乐观锁解决高并发问题的优缺点。</p>
<h3>数据库结构</h3>
<table>
<thead><tr>
<th>ID</th>
<th>Code</th>
<th>UserId</th>
<th>CreatedAt</th>
<th>RewardAt</th>
</tr></thead>
<tbody><tr>
<td>奖品ID</td>
<td>奖品码</td>
<td>用户ID</td>
<td>创建时间</td>
<td>中奖时间</td>
</tr></tbody>
</table>
<ul>
<li>未中奖时UserId为0,RewardAt为NULL</li>
<li>中奖时UserId为中奖用户ID,RewardAt为中奖时间</li>
</ul>
<h3>乐观锁实现</h3>
<p>乐观锁实际上并不存在真正的锁,乐观锁是利用数据的某个字段来做的,比如本文的例子就是以UserId来实现的。</p>
<p>实现流程如下:</p>
<ol>
<li>
<p>查询UserId为0的奖品,如果未找到则提示无奖品</p>
<pre><code class="sql">SELECT * FROM envelope WHERE user_id=0 LIMIT 1</code></pre>
</li>
<li>
<p>更新奖品的用户ID和中奖时间(假设奖品ID为1,中奖用户ID为100,当前时间为2019-10-29 12:00:00),这里的user_id=0就是我们的乐观锁了。</p>
<pre><code class="sql">UPDATE envelope SET user_id=100, reward_at='2019-10-29 12:00:00' WHERE user_id=0 AND id=1</code></pre>
</li>
<li>检测UPDATE语句的执行返回值,如果返回1证明中奖成功,否则证明该奖品被其他人抢了</li>
</ol>
<h4>为什么要添加乐观锁</h4>
<p>正常情况下获取奖品、然后把奖品更新给指定用户是没问题的。如果不添加user_id=0时,高并发场景下会出现下面的问题:</p>
<ol>
<li>两个用户同时查询到了1个未中奖的奖品(发生并发问题)</li>
<li>将奖品的中奖用户更新为用户1,更新条件只有ID=奖品ID</li>
<li>上述SQL执行是成功的,影响行数也是1,此时接口会返回用户1中奖</li>
<li>接下来将中奖用户更新为用户2,更新条件也只有ID=奖品ID</li>
<li>由于是同一个奖品,已经发给用户1的奖品会重新发放给用户2,此时影响行数为1,接口返回用户2也中奖</li>
<li>所以该奖品的最终结果是发放给用户2</li>
<li><code>用户1就会过来投诉活动方了,因为抽奖接口返回用户1中奖,但他的奖品被抢了,此时活动方只能赔钱了</code></li>
</ol>
<h4>添加乐观锁之后的抽奖流程</h4>
<ol>
<li>更新用户1时的条件为<code>id=红包ID AND user_id=0</code> ,由于此时红包未分配给任何人,用户1更新成功,接口返回用户1中奖</li>
<li>当更新用户2时更新条件为<code>id=红包ID AND user_id=0</code>,由于此时该红包已经分配给用户1了,所以该条件不会更新任何记录,接口返回用户2中奖</li>
</ol>
<h4>乐观锁优缺点</h4>
<p>优点</p>
<ul>
<li>性能尚可,因为无锁</li>
<li>不会超发</li>
</ul>
<p>缺点</p>
<ul><li>通常不满足“先到先得”的活动规则,一旦发生并发,就会发生未中奖的情况,此时奖品库还有奖品</li></ul>
<h3>压测</h3>
<p>在MacBook Pro 2018上的压测表现如下(Golang实现的HTTP服务器,MySQL连接池大小100,Jmeter压测):</p>
<ul><li>500并发 500总请求数 平均响应时间331ms 发放成功数为31 吞吐量458.7/s</li></ul>
<h2>Redis实现</h2>
<p>可以看到乐观锁的实现下争抢比太高,不是推荐的实现方法,下面通过Redis来优化这个秒杀业务。</p>
<h3>Redis高性能的原因</h3>
<ul>
<li>单线程 省去了线程切换开销</li>
<li>基于内存的操作 虽然持久化操作涉及到硬盘访问,但是那是异步的,不会影响Redis的业务</li>
<li>使用了IO多路复用</li>
</ul>
<h3>实现流程</h3>
<ol>
<li>活动开始前将数据库中奖品的code写入Redis队列中</li>
<li>活动进行时使用lpop弹出队列中的元素</li>
<li>
<p>如果获取成功,则使用UPDATE语法发放奖品</p>
<pre><code class="sql">UPDATE reward SET user_id=用户ID,reward_at=当前时间 WHERE code='奖品码'</code></pre>
</li>
<li>如果获取失败,则当前无可用奖品,提示未中奖即可</li>
</ol>
<p>使用Redis的情况下并发访问是通过Redis的<code>lpop()</code>来保证的,该方法是原子方法,可以保证并发情况下也是一个个弹出的。</p>
<h3>压测</h3>
<p>在MacBook Pro 2018上的压测表现如下(Golang实现的HTTP服务器,MySQL连接池大小100,Redis连接池代销100,Jmeter压测):</p>
<ul><li>500并发 500总请求数 平均响应时间48ms 发放成功数100 吞吐量497.0/s</li></ul>
<h2>结论</h2>
<p>可以看到Redis的表现是稳定的,不会出现超发,且访问延迟少了8倍左右,吞吐量还没达到瓶颈,可以看出Redis对于高并发系统的性能提升是非常大的!接入成本也不算高,值得学习!</p>
<p><img src="/img/remote/1460000020849746" alt="0.jpeg" title="0.jpeg"></p>
<h2>实验代码</h2>
<pre><code class="go">// main.go
package main
import (
"fmt"
"github.com/go-redis/redis"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"log"
"net/http"
"strconv"
"time"
)
type Envelope struct {
Id int `gorm:"primary_key"`
Code string
UserId int
CreatedAt time.Time
RewardAt *time.Time
}
func (Envelope) TableName() string {
return "envelope"
}
func (p *Envelope) BeforeCreate() error {
p.CreatedAt = time.Now()
return nil
}
const (
QueueEnvelope = "envelope"
QueueUser = "user"
)
var (
db *gorm.DB
redisClient *redis.Client
)
func init() {
var err error
db, err = gorm.Open("mysql", "root:root@tcp(localhost:3306)/test?charset=utf8&parseTime=True&loc=Local")
if err != nil {
log.Fatal(err)
}
if err = db.DB().Ping(); err != nil {
log.Fatal(err)
}
db.DB().SetMaxOpenConns(100)
fmt.Println("database connected. pool size 10")
}
func init() {
redisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
DB: 0,
PoolSize: 100,
})
if _, err := redisClient.Ping().Result(); err != nil {
log.Fatal(err)
}
fmt.Println("redis connected. pool size 100")
}
// 读取Code写入Queue
func init() {
envelopes := make([]Envelope, 0, 100)
if err := db.Debug().Where("user_id=0").Limit(100).Find(&envelopes).Error; err != nil {
log.Fatal(err)
}
if len(envelopes) != 100 {
log.Fatal("不足100个奖品")
}
for i := range envelopes {
if err := redisClient.LPush(QueueEnvelope, envelopes[i].Code).Err(); err != nil {
log.Fatal(err)
}
}
fmt.Println("load 100 envelopes")
}
func main() {
http.HandleFunc("/envelope", func(w http.ResponseWriter, r *http.Request) {
uid := r.Header.Get("x-user-id")
if uid == "" {
w.WriteHeader(401)
_, _ = fmt.Fprint(w, "UnAuthorized")
return
}
uidValue, err := strconv.Atoi(uid)
if err != nil {
w.WriteHeader(400)
_, _ = fmt.Fprint(w, "Bad Request")
return
}
// 检测用户是否抢过了
if result, err := redisClient.HIncrBy(QueueUser, uid, 1).Result(); err != nil || result != 1 {
w.WriteHeader(429)
_, _ = fmt.Fprint(w, "Too Many Request")
return
}
// 检测是否在队列中
code, err := redisClient.LPop(QueueEnvelope).Result()
if err != nil {
w.WriteHeader(200)
_, _ = fmt.Fprint(w, "No Envelope")
return
}
// 发放红包
envelope := &Envelope{}
err = db.Where("code=?", code).Take(&envelope).Error
if err == gorm.ErrRecordNotFound {
w.WriteHeader(200)
_, _ = fmt.Fprint(w, "No Envelope")
return
}
if err != nil {
w.WriteHeader(500)
_, _ = fmt.Fprint(w, err)
return
}
now := time.Now()
envelope.UserId = uidValue
envelope.RewardAt = &now
rowsAffected := db.Where("user_id=0").Save(&envelope).RowsAffected // 添加user_id=0来验证Redis是否真的解决争抢问题
if rowsAffected == 0 {
fmt.Printf("发生争抢. id=%d\n", envelope.Id)
w.WriteHeader(500)
_, _ = fmt.Fprintf(w, "发生争抢. id=%d\n", envelope.Id)
return
}
_, _ = fmt.Fprint(w, envelope.Code)
})
fmt.Println("listen on 8080")
fmt.Println(http.ListenAndServe(":8080", nil))
}</code></pre>
深入浅出ES6的Symbol类型
https://segmentfault.com/a/1190000020836423
2019-10-28T14:54:51+08:00
2019-10-28T14:54:51+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
2
<h2>本文内容</h2>
<ul>
<li>JS基本数据类型种类</li>
<li>Symbol的主要用法, 全局Symbol的使用与检测</li>
<li>Symbol与其他基本类型转换时的规则</li>
</ul>
<p>ES6引入了一种新的原始数据类型,表示独一无二的值,最大的用处是作为对象属性的唯一标识符。</p>
<p>至此,Javascript拥有6种基本数据类型和一种复杂数据类型。</p>
<h2>数据类型</h2>
<p>基本类型</p>
<ul>
<li>string</li>
<li>number</li>
<li>boolean</li>
<li>undefined</li>
<li>null</li>
<li>symbol</li>
</ul>
<p>复杂类型</p>
<ul><li>object</li></ul>
<h2>用法</h2>
<h3>基本语法</h3>
<pre><code class="js">Symbol([description])</code></pre>
<ul>
<li>description 可选的描述,一般用在调试的时候作为区分,但是 <strong>不能用来访问Symbol</strong>。</li>
<li>该方法返回一个symbol值</li>
</ul>
<pre><code class="js">let s = Symbol('test');
let s2 = Symbol('test');
let s3 = new Symbol('test'); // TypeError
console.log(s === s2); // false
console.log(typeof s); // symbol
console.log(s.description); // test</code></pre>
<ul>
<li>每次调用Symbol()返回的值都是独一无二的,不管描述是否一致。</li>
<li>
<code>Symbol</code>不支持<code>new</code>调用</li>
<li>通过description属性可以获取到传入Symbol的描述性字符串</li>
</ul>
<h3>全局单例的Symbol</h3>
<p>使用Symbol.for()可以创建全局单例的symbol值,语法如下:</p>
<pre><code class="js">Symbol.for([name])</code></pre>
<ul><li>name 可选的描述,建议传入,否则name会作为undefined传入</li></ul>
<ol>
<li>类似于单例模式,执行环境(一般是浏览器)内部维护了一个全局Symbol注册表,记录name和Symbol(name)关系</li>
<li>尝试通过name在该注册表查找对应symbol值,如果找到,则返回这个symbol值</li>
<li>如果没找到,则使用Symbol(name)创建一个symbol值,并记录该symbol值与name的关联关系,之后返回该symbol</li>
</ol>
<pre><code class="js">const name = Symbol('name');
const name2 = Symbol.for('name');
const name3 = Symbol.for('name');
console.log(name === name2); // false
console.log(name2 === name3); // true</code></pre>
<ul>
<li>
<p>全局Symbol映射关系中name是作为字符串来使用的,结构类似下面的代码:</p>
<pre><code class="js">const globalSymbols = {
name: Symbol('name')
};</code></pre>
</li>
<li>使用同样的字符串描述调用Symbol()和Symbol.for()获取到的symbol值不相等</li>
</ul>
<h3>查找是否为全局的单例Symbol</h3>
<p>使用Symbol.keyFor()可以检测给定的symbol值是否是全局单例的symbol(简单来说就是检测是否是Symbol.for()创建的),语法如下:</p>
<pre><code class="js">Symbol.keyFor(symbol)</code></pre>
<ul>
<li>symbol 必传, 待检测的symbol值</li>
<li>如果给定的symbol值是通过Symbol.for()得到的,该方法返回传入symbol的字符串描述</li>
<li>如果给定的symbol值不是通过Symbol.for()得到的,该方法返回undefined</li>
</ul>
<pre><code class="js">const s = Symbol('s1');
const s2 = Symbol.for('s2');
console.log(Symbol.keyFor(s)); // undefined
console.log(Symbol.keyFor(s2)); // s2</code></pre>
<h3>Symbol与JSON.stringify</h3>
<blockquote>Symbol类型的属性不会参与JSON的序列化</blockquote>
<pre><code class="js">const name = Symbol('name');
const obj = {
[name]: 'xialei',
data: 1
};
console.log(JSON.stringify(obj)); // {"data": 1}</code></pre>
<h3>Symbol与for ... in和for ... of</h3>
<blockquote>Symbol类型的属性不会出现在for ... in和for ... of循环中</blockquote>
<pre><code class="js">const name = Symbol('name');
const user = {
[name]: 'xialei',
data: 1
};
for(let i in user) {
console.log(i, user[i]);
}
// 上述循环输出 data 1
for(let i of user) {
console.log(i, user[i]);
}
// TypeError: user不是可迭代对象</code></pre>
<h3>Symbol与Object.keys()和Object.getOwnPropertyNames()</h3>
<blockquote>Symbol类型的属性不会出现在Object.keys()和Object.getOwnPropertyNames()返回结果中</blockquote>
<pre><code class="js">const name = Symbol('name');
const user = {
[name]: 'xialei',
data: 1
};
console.log(Object.keys(user)); // ["data"]
console.log(Object.getOwnPropertyNames(user)); // ["data"]</code></pre>
<h3>Symbol与Object.getOwnPropertySymbols()</h3>
<blockquote>Symbol类型的属性会出现在Object.Object.getOwnPropertySymbols()</blockquote>
<pre><code class="js">const name = Symbol('name');
const user = {
[name]: 'xialei',
data: 1
};
console.log(Object.Object.getOwnPropertySymbols(user)); // [Symbol(name)]</code></pre>
<h3>Symbol数据类型转换</h3>
<pre><code class="js">const name = Symbol('1');
console.log(name + 1); // TypeError
console.log(Number(name)); // 创建包装对象
console.log(name + '1'); // TypeError
console.log(String(name)); // Symbol(1)
console.log(!!name); // true
console.log(Boolean(name)); // true</code></pre>
<ul>
<li>symbol值不能转换为数字</li>
<li>symbol不能直接转换为字符串,需要通过String包装才能转化</li>
<li>symbol可以直接转换为boolean,转化后为true</li>
</ul>
<h2>结尾</h2>
<ul>
<li>使用Symbol最大的注意事项应该是使用方括号语法去访问对应的属性,而不是字符串。</li>
<li>Symbol数据类型转换规范比较简单,大部分场景下也没用拿Symbol去做数据转换</li>
</ul>
<p><img src="/img/bVbziYd?w=638&h=283" alt="2019-10-22-102654.jpg" title="2019-10-22-102654.jpg"></p>
不只是块级作用域,你不知道的let和const
https://segmentfault.com/a/1190000020768392
2019-10-22T18:28:49+08:00
2019-10-22T18:28:49+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
12
<p>ES6新增了两个重要的关键字<code>let</code>和<code>const</code>,相信大家都不陌生,但是包括我在内,在系统学习ES6之前也只使用到了【不存在变量提升】这个特性。</p>
<ul>
<li>
<strong>let</strong>声明一个块级作用域的本地变量</li>
<li>
<strong>const</strong>语句声明一个块级作用域的本地常量,不可以重新赋值</li>
</ul>
<h2>支持块级作用域</h2>
<p><code>var</code>定义的变量会提升到整个函数作用域内,<code>let/const</code>则支持块级作用域。</p>
<blockquote>块级作用域: 由<code>{}</code>包裹的作用域(函数那种{}不算)</blockquote>
<p>来看一个<code>var</code>的例子:</p>
<pre><code class="javascript">{
var a = 1;
}
console.log(a);</code></pre>
<p>此时输出1,因为<code>var</code>没有块级作用域。</p>
<p>来看一个<code>let</code>的例子(<code>const</code>效果一样):</p>
<pre><code class="javascript">{
let a = 1;
}
console.log(a);</code></pre>
<p>此时会报错<code>ReferenceError</code>,因为<code>let/const</code>支持块级作用域,所以<code>let</code>定义的<code>a</code>只在<code>{}</code>可以访问</p>
<h2>不存在变量提升</h2>
<p>与<code>var</code>不同的是,<code>let/const</code>声明的变量不存在变量提升,也就是说<code>{}</code>对于<code>let/const</code>是有效的。</p>
<p>来看一个<code>var</code>的例子:</p>
<pre><code class="javascript">console.log(a);
var a = 1;</code></pre>
<p>此时会输出undefined,因为var声明的变量会提升到作用域顶部(只提升声明,不提升赋值)</p>
<p>来看一个<code>let</code>的例子(<code>const</code>效果也一样):</p>
<pre><code class="javascript">console.log(a);
let a = 1;
</code></pre>
<p>此时会报错<code>ReferenceError</code>,因为<code>let</code>不存在变量提升</p>
<h2>同一作用域内不可以重复声明</h2>
<p>同一作用域内<code>let/const</code>不可以重复声明,<code>var</code>可以。</p>
<p>来看一个<code>var</code>的例子:</p>
<pre><code class="javascript">var a = 1;
var a = 2;
console.log(a);</code></pre>
<p>此时会输出2,var是支持重复声明的,后面声明的值会覆盖前面声明的值。</p>
<p>来看一个<code>let</code>的例子(<code>const</code>效果也一样):</p>
<pre><code class="javascript">let a = 1;
let a = 2;
console.log(a);</code></pre>
<p>此时会报错<code>SyntaxError</code>,因为同一作用域内<code>let/const</code>不可以重复声明。</p>
<p>再来看一个不同作用域的例子:</p>
<pre><code class="javascript">let a = 1;
{
let a = 2;
}
console.log(a);</code></pre>
<p>此时输出1,因为两者作用域不同</p>
<h2>暂存死区</h2>
<p><strong>暂存死区TDZ(Temporal Dead Zone)</strong>是ES6中对作用域新的语义。</p>
<p>通过<code>let/const</code>定义的变量直到执行他们的初始化代码时才被初始化。在初始化之前访问<code>该变量</code>会导致<code>ReferenceError</code>。该变量处于<code>一个自作用域顶部到初始化代码</code>之间的“暂存死区”中。</p>
<p>来看以下例子:</p>
<pre><code class="javascript">function do_something() {
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2;
}
do_something();</code></pre>
<p><code>var</code>定义的变量声明会提升到作用域顶部,所以<code>bar</code>是undefined,而<code>let</code>定义的变量<code>从作用域开始到let foo=2</code>这中间都无法访问,访问会报错<code>ReferenceError</code></p>
<h2>暂存死区与typeof</h2>
<p>typeof检测var定义的变量或者检测不存在的变量时会返回undefined,如果检测暂存死区内的变量,会报错<code>ReferenceError</code>.</p>
<pre><code class="javascript">console.log(typeof foo); // undefined
console.log(typeof bar); // ReferenceError
console.log(typeof bar2); // undefined
let bar = 1;
var bar2 = 2;</code></pre>
<p>也就是说typeof去检测未初始化的<code>let</code>变量时会报错,<code>var</code>或者未声明的变量不会报错</p>
<h2>面试题</h2>
<pre><code class="javascript">function test(){
var foo = 33;
{
let foo = (foo + 55);
}
}
test();</code></pre>
<p>以上函数执行结果是什么?为什么?</p>
<blockquote>报错<p><code>{}</code>内有<code>let</code>定义的<code>foo</code>,所以存在暂存死区,<code>(foo + 55)</code>这个表达式是在<code>let foo</code>之前执行的(赋值时先执行等号右边的,执行完毕把结果赋给等号左边),表达式执行的时候还没有初始化foo,所以报错<code>ReferenceError</code></p>
</blockquote>
<h2>总结</h2>
<ol>
<li>let/const支持函数作用域和块级作用域,var只有函数作用域</li>
<li>let/const不存在变量提升,var存在变量提升</li>
<li>let/const同一作用域内不可以重复声明,var可以重复声明</li>
<li>let/const存在暂存死区,var不存在</li>
</ol>
<h2>面试题</h2>
<pre><code class="javascript">let b = 1;
function test4() {
console.log(b);
let b = 2;
}
test4()</code></pre>
<p><img src="/img/bVbziYd?w=638&h=283" alt="2019-10-22-102654.png" title="2019-10-22-102654.png"></p>
聊一聊valueOf和toString
https://segmentfault.com/a/1190000020696134
2019-10-15T17:37:44+08:00
2019-10-15T17:37:44+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
9
<p>valueOf和toString是Object.prototype的方法。一般很少直接调用,但是在使用对象参与运算的时候就会调用这两个方法了。我想大部分人都存在以下疑问:</p>
<ul>
<li>valueOf和toString哪个优先级较高?</li>
<li>是不是所有场景都会调用valueOf和toString</li>
</ul>
<h2>概念解释</h2>
<ul>
<li>valueOf: 返回对象的原始值表示</li>
<li>toString: 返回对象的字符串表示</li>
</ul>
<p>在介绍下面的内容之前先了解一下转换规则,下面的内容解释都是基于这个规则表来的:</p>
<h3>valueOf转换规则</h3>
<p>valueOf是Object.prototype的方法,由Object来的对象都会有该方法,但是很多内置对象会重写这个方法,以适合实际需要。</p>
<p>说到原始值就必须说到原始类型,JS规范中 <strong>原始类型</strong> 如下:</p>
<ul>
<li>Boolean</li>
<li>Null</li>
<li>Undefined</li>
<li>Number</li>
<li>String</li>
</ul>
<p>非原始值(也就是对象)重写规则如下:</p>
<table>
<thead><tr>
<th>对象</th>
<th>valueOf返回值</th>
</tr></thead>
<tbody>
<tr>
<td>Array</td>
<td>数组本身</td>
</tr>
<tr>
<td>Boolean</td>
<td>布尔值</td>
</tr>
<tr>
<td>Date</td>
<td>返回毫秒形式的时间戳</td>
</tr>
<tr>
<td>Function</td>
<td>函数本身</td>
</tr>
<tr>
<td>Number</td>
<td>数字值</td>
</tr>
<tr>
<td>Object</td>
<td>对象本身</td>
</tr>
<tr>
<td>String</td>
<td>字符串值</td>
</tr>
</tbody>
</table>
<blockquote>以下规则是经过验证的,如果对验证过程不关心,可以只看转换规则。<p>建议看一下验证过程,这样可以加深理解</p>
</blockquote>
<h4>对象转换为布尔值</h4>
<ol><li>直接转换为true(包装类型也一样),不调用valueOf和toString</li></ol>
<h4>对象转换为数字</h4>
<p>在预期会将对象用作数字使用时,比如参与算术运算等等操作,对象转换为数字会依次调用valueOf和toString方法,具体规则如下:</p>
<ol>
<li>如果对象具有<code>valueOf</code>方法且返回原始值(string、number、boolean、undefined、null),则将该原始值转换为数字(转换失败会返回NaN),并返回这个数字</li>
<li>如果对象具有<code>toString</code>方法且返回原始值(string、number、boolean、undefined、null),则将该原始值转换为数字(转换失败会返回NaN),并返回这个数字</li>
<li>转换失败,抛出TypeError</li>
</ol>
<h4>对象转换为字符串</h4>
<ol>
<li>如果对象具有<code>toString</code>方法且返回原始值(string、number、boolean、undefined、null),则将该原始值转换为字符串,并返回该字符串</li>
<li>如果对象具有<code>valueOf</code>方法且返回原始值(string、number、boolean、undefined、null),则将该原始值转换为字符串,并返回该字符串</li>
<li>转换失败,抛出TypeError</li>
</ol>
<h3>toString转换规则</h3>
<table>
<thead><tr>
<th>对象</th>
<th>toString返回值</th>
</tr></thead>
<tbody>
<tr>
<td>Array</td>
<td>以逗号分割的字符串,如[1,2]的toString返回值为"1,2"</td>
</tr>
<tr>
<td>Boolean</td>
<td>"True"</td>
</tr>
<tr>
<td>Date</td>
<td>可读的时间字符串,如"Tue Oct 15 2019 12:20:56 GMT+0800 (中国标准时间)"</td>
</tr>
<tr>
<td>Function</td>
<td>声明函数的JS源代码字符串</td>
</tr>
<tr>
<td>Number</td>
<td>"数字值"</td>
</tr>
<tr>
<td>Object</td>
<td>"[object Object]"</td>
</tr>
<tr>
<td>String</td>
<td>"字符串"</td>
</tr>
</tbody>
</table>
<h2>验证对象到原始值的转换</h2>
<p>光看valueOf和toString没啥东西可说,日常开发中也很少直接调用,但是当我们将对象当做原始值来使用时会发生转换,而且转换过程还略微有点迷糊。</p>
<h3>对象转换为Boolean</h3>
<p>为了能够直观的看到JS内部的转换过程,我把valueOf和toString都简单重写了,加了日志。</p>
<pre><code class="javascript">// 保存原始的valueOf
var valueOf = Object.prototype.valueOf;
var toString = Object.prototype.toString;
// 添加valueOf日志
Object.prototype.valueOf = function () {
console.log('valueOf');
return valueOf.call(this);
};
// 添加toString日志
Object.prototype.toString = function () {
console.log('toString');
return toString.call(this);
};
var a = {};
var b = new Boolean(false);
if (a) {
console.log(1);
}
if(b) {
console.log(2);
}</code></pre>
<p>以上例子的输出如下:</p>
<pre><code>1
2</code></pre>
<blockquote>未调用valueOf和toString,符合[对象到布尔值]的转换规则</blockquote>
<h3>对象转换为Number</h3>
<h4>例子1</h4>
<pre><code class="javascript">// 保存原始的valueOf
var valueOf = Object.prototype.valueOf;
var toString = Object.prototype.toString;
// 添加valueOf日志
Object.prototype.valueOf = function() {
console.log('valueOf');
return valueOf.call(this);
};
// 添加toString日志
Object.prototype.toString = function() {
console.log('toString');
return toString.call(this);
};
var a = {};
console.log(++a);</code></pre>
<p>输出如下:</p>
<pre><code>valueOf
toString
NaN</code></pre>
<h4>分析</h4>
<ol>
<li>valueOf方法返回的是对象本身,不是原始值,继续执行</li>
<li>toString方法返回的是"[object Object]",是原始值(字符串),将字符串转换为数字NaN</li>
</ol>
<h4>例子2</h4>
<pre><code class="javascript">// 保存原始的valueOf
var valueOf = Object.prototype.valueOf;
var toString = Object.prototype.toString;
// 添加valueOf日志
Object.prototype.valueOf = function () {
console.log('valueOf');
return "1"; // 强制返回原始值
};
// 添加toString日志
Object.prototype.toString = function () {
console.log('toString');
return toString.call(this);
};
var a = {};
console.log(++a);</code></pre>
<p>输出如下:</p>
<pre><code>valueOf
2</code></pre>
<h4>分析</h4>
<ol><li>valueOf返回原始值(字符串),直接将该字符串转换为数字,得到1</li></ol>
<h3>对象转换为字符串</h3>
<p>在预期会将对象用作字符串时,比如一个字符串拼接了字符串,传入了一个对象,此时会发生转换。</p>
<pre><code class="javascript">// 保存原始的valueOf
var valueOf = Object.prototype.valueOf;
var toString = Object.prototype.toString;
// 添加valueOf日志
Object.prototype.valueOf = function () {
console.log('valueOf');
return valueOf.call(this);
};
// 添加toString日志
Object.prototype.toString = function () {
console.log('toString');
return toString.call(this);
};
var a = {};
alert(a);</code></pre>
<p>输出如下:</p>
<pre><code>toString
// 弹出[object Object]</code></pre>
<h4>分析</h4>
<ol><li>调用toString方法,返回了字符串"[object Object]",对象最终转换为该字符串</li></ol>
<h4>例子2</h4>
<pre><code class="javascript">// 保存原始的valueOf
var valueOf = Object.prototype.valueOf;
var toString = Object.prototype.toString;
// 添加valueOf日志
Object.prototype.valueOf = function () {
console.log('valueOf');
return valueOf.call(this);
};
// 添加toString日志
Object.prototype.toString = function () {
console.log('toString');
return this;
};
var a = {};
alert(a);</code></pre>
<p>输出如下:</p>
<pre><code>toString
valueOf
Uncaught TypeError: Cannot convert object to primitive value
at 1.js:16</code></pre>
<h4>分析</h4>
<ol>
<li>调用toString方法,返回的不是原始值,继续执行</li>
<li>调用valueOf方法,返回的不是原始值,继续执行</li>
<li>抛出TypeError</li>
</ol>
<h2>【番外】使用加号运算符连接字符串和对象时的处理</h2>
<p>在测试对象到字符串转换时发现如下代码的表现和预期并不一致:</p>
<pre><code class="javascript">// 保存原始的valueOf
var valueOf = Object.prototype.valueOf;
var toString = Object.prototype.toString;
// 添加valueOf日志
Object.prototype.valueOf = function () {
console.log('valueOf');
return valueOf.call(this);
};
// 添加toString日志
Object.prototype.toString = function () {
console.log('toString');
return toString.call(this);
};
console.log("a" + {});</code></pre>
<p>输出如下:</p>
<pre><code>valueOf
toString
a[object Object]</code></pre>
<h3>疑问</h3>
<p><code>"a"+ {}</code> 应该是预期把<code>{}</code>当做字符串使用,应该先调用toString方法的,实际情况却不是这样。</p>
<h3>结论</h3>
<p>通过查找资料得到的结论如下:</p>
<ol>
<li>如果有一个是对象,则遵循对象对原始值的转换过程(Date对象直接调用toString完成转换,其他对象通过valueOf转化,如果转换不成功则调用toString)</li>
<li>如果两个都是对象,两个对象都遵循步骤1转换到字符串</li>
<li>两个数字,进行算数运算</li>
<li>两个字符串,直接拼接</li>
<li>一个字符串一个数字,直接拼接为字符串</li>
</ol>
<h2>面试题</h2>
<pre><code class="javascript">var a = {};
var b = {};
var c = {};
c[a] = 1;
c[b] = 2;
console.log(c[a]);
console.log(c[b]);</code></pre>
<h3>题解</h3>
<p>由于对象的key是字符串,所以<code>c[a]</code>和<code>c[b]</code>中的<code>a</code>和<code>b</code>会执行[对象到字符串]的转换。</p>
<p>根据转换规则, <code>a</code>和<code>b</code>都转换为了<code>[object Object]</code>,所以<code>c[a]</code>和<code>c[b]</code>操作的是同一个键。</p>
<p>答案是<code>输出两个2</code>,c对象的最终结构如下:</p>
<pre><code class="javascript">{
'[object Object]':2
}</code></pre>
<p><img src="/img/bVbyWOv?w=638&h=283" alt="0.jpeg" title="0.jpeg"></p>
搞懂JS闭包
https://segmentfault.com/a/1190000020683223
2019-10-14T18:22:23+08:00
2019-10-14T18:22:23+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
12
<p>闭包(Closure)是JS比较难懂的一个东西,或者说别人说的难以理解, 本文将以简洁的语言+面试题来深入浅出地介绍一下。</p>
<h2>作用域和作用域链</h2>
<p>在将闭包之前,需要先讲一下作用域。</p>
<p>JS中有全局作用域和局部作用域两种。</p>
<p>全局作用域任何地方都能访问,而局部作用于只有内部能访问。</p>
<pre><code class="javascript">function a() {
var num = 1;
}
console.log(num);</code></pre>
<p>在上面的例子中会报错,num不存在。</p>
<blockquote>总结:函数外部无法访问函数内部的值</blockquote>
<p>当代码在一个作用域中执行时,JS引擎会默认创建一个作用域链(从当前作用域一直链接到全局作用域)。</p>
<p>在访问变量或者函数时,如果当前作用域查找不到,则向上级作用域查找,找到就返回,如果查找到全局作用域还没找到的话就报错。</p>
<pre><code class="javascript">function a() {
var num = 1;
function b() {
console.log(num);
}
}</code></pre>
<p>在上面的例子中,num是在<code>a</code>函数作用域下的局部变量,我们在<code>b</code>函数访问num时会有以下过程:</p>
<ol>
<li>在<code>b</code>的作用域查找num,发现找不到</li>
<li>往上一级作用域查找,发现num在<code>a</code>作用域,查找成功</li>
</ol>
<blockquote>总结:函数可以访问同级或上级作用域的值</blockquote>
<h2>闭包</h2>
<p>当我们需要在函数外部访问函数内部的值时,闭包就产生了。</p>
<pre><code class="javascript">function a() {
var num = 1;
function b () {
console.log(num);
}
return b;
}
var bb = a();
bb(); // 1</code></pre>
<blockquote>在函数<code>a</code>的内部声明一个函数<code>b</code>,然后把<code>return b</code>,这个时候的<code>b()</code>函数就可以在外部访问,最终能够访问到num。</blockquote>
<p>简单来说:</p>
<blockquote>闭包就是函数内部的函数,上面的那个b就是闭包,可以在外面访问到内部的num</blockquote>
<h2>面试题</h2>
<pre><code class="javascript">// 每隔1秒输出0-10的数字
for(var i = 0;i<10;i++) {
setTimeout(function() {
console.log(i);
},1000);
}
// 上面这段代码输出什么?如果需要修改为正确的情况,怎么修改?</code></pre>
<blockquote>1秒后输出10,因为setTimeout是到下一轮tick中执行,而for循环在当前这轮循环完毕后i的值已经是最后一个值了。需要使用闭包来保留现场</blockquote>
<pre><code class="javascript">for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000);
}(i))
}</code></pre>
<p>这样就是正常的输出了。</p>
<pre><code class="javascript">var name = "Window";
var object = {
name: "Object",
f: function () {
return function () {
return this.name;
};
}
};
alert(object.f()());</code></pre>
<p>答案是Window。</p>
<blockquote>
<p><code>object.getNameFunc()()</code>可以分开两部分来看。</p>
<ol><li>object.f()得到了一个这样的函数</li></ol>
<pre><code class="javascript">function() {
return this.name
}</code></pre>
<ol>
<li>
<code>object.f()()</code>相当于执行上面那个函数,也就是<code>普通函数调用方式</code>,this指向<code>全局环境</code>,this指向搞不清楚的同学可以看看我之前发的<a href="https://link.segmentfault.com/?enc=guiKbL%2BUvO2PVLZFmzCkkw%3D%3D.IdchWw1xN%2FSF7vVqD2RF78VnnAOjQWdJwc9G9X75e6D3oyF%2FP%2FPsY%2B1wn6XtjJu5Mvk9u9vCiKeesFmX11F79w%3D%3D" rel="nofollow">Javavscript基础——this指向</a>
</li>
<li>所以this.name也就是<code>var name = "Window"</code>
</li>
</ol>
</blockquote>
<p>还是那道题,不过我们把this保存一下,变成下面这种形式</p>
<pre><code class="javascript">var name = "Window";
var object = {
name: "Object",
f: function () {
var that = this;
return function () {
return that.name;
};
}
};
alert(object.f()());</code></pre>
<p>答案是Object。</p>
<blockquote><ol>
<li>object.f()中调用时这个this指向object,当用变量保存时,这个that相当于object</li>
<li>object.f()()调用时,由于that相当于object,所以that.name就是object中的Object</li>
</ol></blockquote>
<h2>总结</h2>
<ol>
<li>闭包可以在外部访问函数内部的变量</li>
<li>闭包可以保留现场</li>
</ol>
<h2>结尾</h2>
<p>来道面试题给大家看看吧</p>
<pre><code class="javascript">const buttons = document.querySelectorAll('.btn');
for(var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
console.log(i);
}
}</code></pre>
<p>以上例子显示什么?如果需要正常显示需要怎么修改?</p>
<p><img src="/img/bVbyWOv?w=638&h=283" alt="0.jpeg" title="0.jpeg"></p>
HTTPS协议是如何保证安全的?
https://segmentfault.com/a/1190000020653571
2019-10-11T17:28:39+08:00
2019-10-11T17:28:39+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
9
<p>相信大家对于HTTPS协议都不陌生,但是应该存在以下疑问:</p>
<ol>
<li>HTTPS协议到底是如何运作的?</li>
<li>HTTPS是如何解决HTTP协议的不安全特性的?</li>
<li>HTTPS网站抓包为什么要信任证书?</li>
</ol>
<h2>HTTP协议</h2>
<p>HTTP协议是一个<strong>应用层</strong>协议,通常运行在TCP协议之上。它是一个<code>明文协议</code>,客户端发起请求,服务端给出响应的响应。</p>
<p>由于网络并不是可信任的,HTTP协议的明文特性会存在以下风险:</p>
<ul>
<li>通信数据有被窃听和被篡改的风险</li>
<li>目标网站有被冒充的风险</li>
</ul>
<p>一般的网站可能没什么影响,但是如果是银行这种网站呢?</p>
<p>好在国内的银行在HTTP协议时代针对IE开发了ActiveX插件来保证安全性,这一点算是值得点赞了。</p>
<h2>解决方案</h2>
<p>既然HTTP协议是明文协议,如果对数据进行加密之后是否就能保证安全性了呢?</p>
<p>在回答这个问题之前,我们先看看比较常见的两种加密算法。</p>
<h3>加密算法</h3>
<p>常见的有对称加密算法和非对称加密算法。</p>
<p><strong>对称加密</strong></p>
<p>加密和解密使用同一个密钥。加解密效率比非对称加密高。但是密钥一旦泄露,通信就不安全了</p>
<p><strong>非对称加密</strong></p>
<p>存在密钥对,公钥加密私钥解密或者私钥加密公钥解密,无法通过公钥反推私钥,也无法通过私钥反推公钥。</p>
<blockquote>一般情况下,使用非对称加密来传输通信所用的密钥,通信过程中采用对称加密,可以解决对称加密的安全问题以及非对称加密的性能问题。</blockquote>
<h3>HTTP加密通信过程</h3>
<ol>
<li>浏览器生成随机串A作为通信密钥</li>
<li>浏览器使用公钥将随机串A加密后得到密文B发送给服务器,这一步是安全的,因为黑客没有服务端私钥无法解密</li>
<li>服务端利用私钥解密出随机串A得到通信密钥</li>
<li>服务端和客户端用随机串A以及对称加密算法进行通信</li>
</ol>
<p>这么一看似乎没有问题,毕竟黑客无法破解非对称加密的的内容,但是浏览器是如何得到公钥的?</p>
<p>有以下两种办法:</p>
<ol>
<li>浏览器内置(不太可能,网站域名这么多,浏览器内置这么多公钥不现实)</li>
<li>服务器给浏览器下发(由于是明文下发,存在被窃听和篡改风险,也就是著名的中间人攻击)</li>
</ol>
<h3>中间人攻击</h3>
<ol>
<li>浏览器请求服务器获取公钥</li>
<li>中间人劫持了服务器的公钥,保存在自己手里</li>
<li>中间人生成一对密钥对,把伪造的公钥下发给浏览器</li>
<li>浏览器使用伪造的公钥和中间人通信</li>
<li>中间人和服务器进行通信</li>
</ol>
<blockquote>由于浏览器使用了伪造的公钥进行通信,所以通信过程是不可靠的</blockquote>
<h3>需要解决的问题</h3>
<p>只要保证浏览器得到的公钥是目标网站的公钥即可保证通信安全,那么问题来了,如何在不可靠的网络上安全的传输公钥呢?</p>
<p>这就是HTTPS协议需要解决的问题</p>
<h2>HTTPS协议</h2>
<p>HTTPS协议涉及到的知识很多,本文只关注<strong>密钥安全交换部分</strong>,这也是HTTPS协议的精华。</p>
<p>HTTPS协议引入了<strong>CA</strong>和<strong>数字证书</strong>的概念。</p>
<h3>数字证书</h3>
<p>包含签发机构、有效期、申请人公钥、证书所有者、证书签名算法、证书指纹以及指纹算法等信息。</p>
<h3>CA</h3>
<p>数字证书签发机构,权威CA是受操作系统信任的,安装操作系统就会内置。</p>
<h3>数字签名</h3>
<p>用Hash算法对数据进行计算得到Hash值,利用私钥对该Hash加密得到签名。</p>
<p>只有匹配的公钥才能解密出签名,来保证签名是本人私钥签发的</p>
<h3>证书签发过程</h3>
<ol>
<li>网站生成密钥对,将私钥自己保存,公钥和网站域名等信息提交给CA</li>
<li>CA把证书签发机构(也就是自己)、证书有效期、网站的公钥、网站域名等信息以<strong>明文</strong>形式写入到一个文本文件</li>
<li>CA选择一个<strong>指纹算法</strong>(一般为hash算法)计算文本文件的内容得到<strong>指纹</strong>,用CA的<strong>私钥</strong>对<strong>指纹</strong>和<strong>指纹算法</strong>进行加密得到<strong>数字签名</strong>,签名算法包含在证书的<strong>明文</strong>部分</li>
<li>CA把明文证书、指纹、指纹算法、数字签名等信息打包在一起得到证书下发给服务器</li>
<li>此时服务器拥有了权威CA颁发的数字证书以及自己的私钥</li>
</ol>
<h3>证书验证过程</h3>
<p>浏览器是如何验证网站的有效性的呢?</p>
<ol>
<li>浏览器以HTTPS协议请求服务器的443端口</li>
<li>服务器下发自己的数字证书给浏览器(明文)</li>
<li>浏览器先校验CA、有效期、域名是否有效,如果无效,则终止连接(服务器此时不可信任)</li>
<li>如果有效,则从<strong>操作系统</strong>取出证书颁发机构的公钥,根据<strong>签名算法</strong>对<strong>数字签名</strong>解密得到<strong>证书指纹</strong>和<strong>指纹算法</strong>
</li>
<li>浏览器用解密得到的指纹算法计算证书的指纹,与解密得到的指纹进行比对,如果一致,证书有效,公钥也安全拿到了</li>
<li>浏览器此时已经和真实的服务器进行通信了,中间人无法得知通信内容,因为中间人没有网站私钥</li>
</ol>
<h3>问题是如何解决的</h3>
<ol>
<li>
<p>黑客冒充CA给了一个假证书给浏览器</p>
<blockquote>浏览器通过CA名称从操作系统取出CA公钥时对数字签名进行解密,发现解密失败,证明这个CA签名用的私钥和操作系统内置的不是一对,就发现了伪造</blockquote>
</li>
<li>
<p>黑客篡改了证书中的网站公钥</p>
<blockquote>证书中的网站公钥可以被篡改,但是数字签名是CA私钥计算出来的,黑客无法计算数字签名,浏览器用内置的CA公钥对数字签名解密时就会发现指纹不匹配了,这也发现了伪造</blockquote>
</li>
<li>
<p>黑客也能正常获取网站公钥</p>
<blockquote>的确,黑客自己通过浏览器访问网站时也能得到公钥,这个公钥跟正常用户的公钥是一致的。<p>但是每个客户端和服务器通信使用的对称密钥都是临时生成且随机的,黑客只能知道自己的随机密钥,但是不知道其他的随机密钥</p>
</blockquote>
</li>
</ol>
<p>综上,浏览器通过操作系统内置权威CA公钥的方式解决了网站公钥下发问题。</p>
<h2>HTTPS中间人攻击</h2>
<p>HTTPS从协议上解决了HTTP时代的中间人攻击问题,但是HTTPS在用户主动信任了伪造证书的时候也会发生中间人攻击(比如早期的12306需要手动信任证书),HTTPS中间人攻击流程如下:</p>
<ol>
<li>客户端用HTTPS连接服务器的443端口</li>
<li>服务器下发自己的数字证书给客户端</li>
<li>黑客劫持了服务器的真实证书,并伪造了一个假的证书给浏览器</li>
<li>浏览器可以发现得到的网站证书是假的,但是浏览器选择信任</li>
<li>浏览器生成随机对称密钥A,用伪造的证书中的公钥加密发往服务器</li>
<li>黑客同样可以劫持这个请求,得到浏览器的对称密钥A,从而能够窃听或者篡改通信数据</li>
<li>黑客利用服务器的真实公钥将客户端的对称密钥A加密发往服务器</li>
<li>服务器利用私钥解密这个对称密钥A之后与黑客通信</li>
<li>黑客利用对称密钥A解密服务器的数据,篡改之后利用对称密钥A加密发给客户端</li>
<li>客户端收到的数据已经是不安全的了</li>
</ol>
<blockquote>以上就是HTTPS中间人攻击的原理,这也就是HTTPS抓包为什么要信任证书的原因。</blockquote>
<h2>总结</h2>
<ol>
<li>操作系统内置权威CA公钥来保证数字签名以及数字证书的安全性</li>
<li>实施HTTPS中间人攻击需要手动信任攻击者的假证书</li>
</ol>
Javascript基础——this指向
https://segmentfault.com/a/1190000020513696
2019-09-27T14:45:55+08:00
2019-09-27T14:45:55+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
26
<p>前几天发布的<a href="https://segmentfault.com/a/1190000020470749">Javavscript基础——原型和原型链</a> 收藏转化率还挺高,看来大家对于JS基础知识还是很看重的,由于JS语言设计的关系,很多语言特性不是那么清晰。比如经典的this在哪的问题。</p>
<p>本文研究一下Javascript的this指向。相信学完之后应该没啥问题。</p>
<p>Javascript的this指向问题,有些人可能觉得很简单,有些人却觉得扑朔迷离,看完本文之后相应会对this的掌握有一个直观的判断,而不是"开局全靠猜"。</p>
<h2>敲黑板</h2>
<ol>
<li>function函数this指向由<code>调用方式</code>确定,跟定义环境无关。</li>
<li>箭头函数this指向由<code>定义环境</code>决定,与<code>调用方式无关</code>,也不可以<code>bind(this)</code>。</li>
</ol>
<h2>严格模式</h2>
<ol>
<li>非严格模式下,全局作用域下的this指向<code>window</code>
</li>
<li>严格模式下,全局作用域下的this指向<code>undefined</code>
</li>
</ol>
<p>以下讨论均为<code>非严格模式</code>,这个不影响今天的讨论。</p>
<h2>实践</h2>
<p>说结论往往是让人难以理解的,下面通过不同的调用场景对this做一个说明。</p>
<h3>1. 直接调用</h3>
<pre><code class="javascript">function test() {
console.log(this);
}
test(); // 输出undefined</code></pre>
<p>直接调用是最简单的, 大部分人在这里都能回答的很好。</p>
<h4>总结</h4>
<blockquote>
<p>直接调用时this指向<code>全局作用域</code>。</p>
<ul>
<li>非严格模式this指向window</li>
<li>严格模式this指向undefined</li>
</ul>
</blockquote>
<h3>2. 对象调用</h3>
<pre><code class="javascript">'use strict'
var n = 1;
var a = {
n: 2,
b: function() {
console.log(this.n);
}
};
a.b(); // 输出2
var b = a.b;
b(); // 输出1</code></pre>
<h4>面试题:请问上述例子输出什么?</h4>
<blockquote>非严格模式下,输出2和1,严格模式下输出2和一个报错(this指向undefined,访问undefined的n属性肯定报错)</blockquote>
<p>那如果你这么回答,<code>满分</code>!</p>
<h4>分析</h4>
<p>知其然还要知其所以然,我们分析一下:</p>
<p>为什么输出2?</p>
<blockquote>因为<code>a.b()</code>是对象调用方式,所以b()中的this指向a</blockquote>
<p>为什么输出1?</p>
<p>这个非常有意思,而且也很有迷惑性,面试的时候经常问到,也经常有人被问倒。</p>
<pre><code class="javascript">var b = a.b</code></pre>
<p>把<code>a.b</code>赋值给<code>变量b</code>,b就是一个函数,<code>请注意: 这里只是赋值,没有调用,所以b中的this指向还不确定</code>。</p>
<pre><code class="java">b();</code></pre>
<p>调用函数<code>b</code>,这是什么调用方式? <strong><code>普通调用</code></strong>,所以this指向全局作用域。</p>
<h4>总结</h4>
<p>对象调用方式下this指向调用对象。</p>
<p>是否GET? 如果没有GET,请关注公众号<code>NodeJs之路</code>,我在线给你答疑。</p>
<p>开胃菜已经吃了,下面来点"有难度的(实际上也没啥难度)"。</p>
<h3>3. 嵌套对象调用</h3>
<pre><code class="javascript">var n = 1;
var a = {
n: 2,
b: {
n: 3,
c: function() {
console.log(this.n)
}
}
};</code></pre>
<h4>面试题:请问上述例子中function中的this指向哪里?</h4>
<blockquote>正确答案:<code>未确定调用环境</code>的情况下,this的指向<code>不确定</code>。<p>错误答案:指向a.b对象,Too young too simple!</p>
</blockquote>
<pre><code class="javascript">var n = 1;
var a = {
n: 2,
b: {
n: 3,
c: function() {
console.log(this.n)
}
}
};
a.b.c(); // 输出3
a.c = a.b.c;
a.c(); // 输出2
var c = a.b.c;
c(); // 输出1</code></pre>
<p>这道题跟之前那道<code>对象调用</code>很像。</p>
<p>为什么输出3?</p>
<blockquote>对象调用方式下指向调用对象,a.b.c()中c()是通过<code>a.b</code>对象调用,指向对象<code>a.b</code>
</blockquote>
<p>为什么输出?</p>
<blockquote>a.c = a.b.c 给a对象定义一个函数c,注意,此时没有调用!this指向不确定<p>a.c() 通过a对象来调用c(),所以this指向对象<code>a</code></p>
</blockquote>
<p>为什么输出1?</p>
<blockquote>var c = a.b.c 函数赋值给普通变量,注意,此时没有调用!<p>c(); 普通方式调用,指向window</p>
</blockquote>
<h4>总结</h4>
<p>嵌套对象调用方式下,this指向<code>最终调用</code>函数的对象。</p>
<p><code>a.b.c.d.e.f.g.h()</code> h函数中的this指向<code>a.b.c.d.e.f.g</code></p>
<h3>4. 构造函数方式调用</h3>
<pre><code class="javascript">var name = 1;
function Person() {
this.name = 2;
}
var p1= Person(); // p1为undefined
console.log(p1.name); // 报错
var p2 = new Person();
console.log(p2.name); // 输出2
</code></pre>
<p>p1为什么是undefined?</p>
<blockquote>这道题比较坑,跟调用方式和this指向无关,因为Person函数没有返回值,js中,默认会返回undefined.</blockquote>
<p>p2.name为什么是2?</p>
<blockquote>使用new操作符时,构造函数的返回值<code>默认</code>指向对象实例,所以p2.name就是Person()中的this.name</blockquote>
<p>如果在<code>Person()</code>函数中加上<code>return this</code>的话,<code>Person()</code>返回值还是<code>this</code>,因为这是普通调用。</p>
<h3>5. 构造函数中指明返回值</h3>
<p>原则上构造函数不应该有返回值,但是如果真的写了会怎么样?我们来探讨一下。</p>
<h4>返回复杂对象</h4>
<pre><code class="javascript">function Person() {
this.name = 2;
return {};
}
var p1 = new Person();
console.log(p1.name)// 输出undefined</code></pre>
<h4>返回简单对象</h4>
<p>虽然Js只有对象,但是有一些如string,number这种一般叫做简单对象,date,regex,array,object等等叫复杂对象。</p>
<pre><code class="javascript">function Person() {
this.name = 2;
return 1;
}
var p1 = new Person();
console.log(p1.name)// 输出2</code></pre>
<h4>返回null</h4>
<p>使用<code>typeof null</code>返回的是<code>[object]</code>,证明null是个对象,不过咱们来看看构造函数返回null的表现。</p>
<pre><code class="javascript">function Person() {
this.name = 2;
return null;
}
var p1 = new Person();
console.log(p1.name)// 输出2</code></pre>
<h4>总结</h4>
<p>构造函数中this指向对象实例本身,如果构造函数指明了返回值,那么表现如下:</p>
<ul>
<li>返回普通值,this指向不变,还是对象实例本身</li>
<li>返回复杂对象,this指向新对象,也就是你new Person()返回的是那个新对象</li>
</ul>
<h3>6. bind</h3>
<pre><code class="javascript"> var a = {
n: 1
};
var b = {
n: 2
}
function f() {
console.log(this.n);
}
var fa = f.bind(a);
var fb = fa.bind(b);
fa(); // 输出1
fb(); // 输出1</code></pre>
<p>第1个输出1应该不难理解,bind可以更改function内部的this指向。多次bind已经bind过的函数,this指向不变。</p>
<p>bind的实现原理有点复杂,将在下一篇文章进行详细解读。</p>
<h4>总结</h4>
<p>bind可以手动绑定function的this,<code>this</code>指向<code>第1次</code>bind时的this。</p>
<h3>7. apply/call</h3>
<p>这两个函数在this指向上表现一致,放到一起讲</p>
<pre><code class="javascript"> var a = {
n: 1
};
var b = {
n: 2
}
function f() {
console.log(this.n);
}
f.call(a); // 输出1
f.apply(b); // 输出2</code></pre>
<blockquote>call和apply的第1个参数为function执行时的this,这个this是确定的,对未使用过bind的函数进行多次apply/call,this指向都会改变。</blockquote>
<h3>8. 箭头函数</h3>
<pre><code class="javascript">var n = 1;
var b = {
n: 2,
a: () => {
console.log(this.n);
}
}
b.a(); // 输出1
b.a.call({n:3}); // 输出1</code></pre>
<p>b.a定义时的this和<code>n</code>,<code>b</code>所在的<code>this一致</code>,默认情况下为全局作用域</p>
<blockquote>箭头函数的this指向定义时所在的this,这个是明确的,但是如果定义时所在的是1个function,那么this指向同上面7点。</blockquote>
<p><em>说下我之前学JS遇到过的问题: ES5下function才会有作用域隔离, {}这种玩意不会隔离作用域。</em></p>
<h2>结尾</h2>
<ol>
<li>直接调用this指向全局作用域window,严格模式指向undefined</li>
<li>对象调用方式指向调用对象</li>
<li>嵌套对象调用方式指向最终调用对象(离function最近的那个)</li>
<li>
<p>构造函数方式调用指向对象实例</p>
<ol>
<li>构造函数返回String/Number等简单类型时this指向不变,返回null指向也不变</li>
<li>构造函数返回Object/Array等复杂对象时,new Person()的返回值为return的对象</li>
</ol>
</li>
<li>bind可以更改function的this,一经绑定,永不改变。<code>但是并不执行函数</code>
</li>
<li>apply/call可以更改没有被bind过的this</li>
<li>箭头函数的<code>this</code>指向为<code>定义</code>箭头函数的<code>this</code>
</li>
</ol>
Javavscript基础——原型和原型链
https://segmentfault.com/a/1190000020470749
2019-09-23T18:07:09+08:00
2019-09-23T18:07:09+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
33
<p>本文研究一下Javascript的核心基础——原型链和继承。</p>
<p>对于使用过基于类的语言(如Java或C#)的人来说,Javascript的继承有点难以搞懂,因为它本身没有<code>class</code>这种东西。(ES6中引入了<code>class</code>关键字,看上去也像传统的OOP语言,但是那只是语法糖,底层还是基于原型)。</p>
<h2>原型链</h2>
<p>MDN上对于原型链的解释:</p>
<blockquote>当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 <code>__proto__</code> )指向它的构造函数的原型对象(<code>prototype</code> )。该原型对象也有一个自己的原型对象( <code>__proto__</code> ) ,层层向上直到一个对象的原型对象为 <code>null</code>。根据定义,<code>null</code> 没有原型,并作为这个<strong>原型链</strong>中的最后一个环节。<p>几乎所有 JavaScript 中的对象都是位于原型链顶端的 <code>Object</code> 的实例。</p>
</blockquote>
<p>这段话可能难以理解,我们来举个例子:</p>
<pre><code class="javascript">const list = []; // 定义数组
list.__proto__ === Array.prototype; // true
list.__proto__.__proto__ === Object.prototype; // true
list.__proto__.__proto__.__proto__===null; // true
// 继承关系为
// list -> Array.prototype -> Object.prototype -> null</code></pre>
<p>结合MDN的解释,我们来解释一下上述例子:</p>
<p>list是<code>Array</code>的实例对象,使用了<code>字面量</code>的方式创建了<code>对象实例</code>。</p>
<blockquote>每个实例对象( object )都有一个私有属性(称之为 <code>__proto__</code> )指向它的构造函数的原型对象(<code>prototype</code> )。</blockquote>
<pre><code class="javascript">// list的构造函数是Array,所以list.__proto__指向构造函数Array的原型对象。
list.__proto__ === Array.prototype; // true</code></pre>
<blockquote>该原型对象也有一个自己的原型对象( <code>__proto__</code> )</blockquote>
<pre><code class="javascript">// Array.prototype也是对象,也有自己的原型对象,原型是Object.prototype
// 下面是数学运算(等量代换)
// list.__proto__ = Array.prototype
// Array.prototype.__proto__ = Object.prototype
list.__proto__.__proto__ === Object.prototype; // true</code></pre>
<blockquote>层层向上直到一个对象的原型对象为 <code>null</code>。根据定义,<code>null</code> 没有原型,并作为这个<strong>原型链</strong>中的最后一个环节。</blockquote>
<pre><code class="javascript">// 目前我们来到了Object.prototype,根据规范,Object.prototype的原型对象为null
// list.__proto__ = Array.prototype
// Array.prototype.__proto__ = Object.prototype
// Object.prototype.__proto__ = null;
list.__proto__.__proto__.__proto__ === null; // true</code></pre>
<h3>原型链查找</h3>
<blockquote>当我们访问对象的属性或者方法时,会先从对象本身开始查找,如果查找不到,则查找对象的<code>__proto__</code>,层层向上查找,直到查找到属性,否则抛出错误。</blockquote>
<pre><code class="javascript">const list = [];
list.toString();</code></pre>
<p>属性查找过程如下:</p>
<ol>
<li>查找list.toString()方法,没找到</li>
<li>继续查找list.<code>__proto__</code>,也就是<code>Array.prototype</code>,找到了</li>
<li>调用<code>Array.prototype.toString</code>
</li>
</ol>
<h3>原型链结论</h3>
<ol>
<li>对象实例.<code>__proto__</code> = 对象构造函数.<code>prototype</code>
</li>
<li>几乎所有对象的原型都是<code>Object.prototype</code>
</li>
<li>null是对象,但是null没有原型</li>
<li>属性/方法查找采用<code>优先返回</code>机制。</li>
</ol>
<h2>函数</h2>
<p>经过原型链的简单介绍,相信大家对原型和原型链有了一个比较直观的了解了,现在要说到的是函数。</p>
<blockquote>我们知道,Javascript中函数也是对象,所以<code>Function.__proto__</code>指向<code>Object.prototype</code>。</blockquote>
<p>上面的结论在Javascript中是<code>有问题</code>的。我们来聊一聊函数。</p>
<p>先看看简单一点的例子,大家知道,<code>Object</code>是对象的<code>构造函数</code>,<code>构造函数</code>也是<code>函数</code>,所有的<code>函数</code>的原型都是<code>Function.prototype</code>,所以<code>Object.__proto__</code>是等于<code>Function.prototype</code>的。</p>
<p>事实证明,也是如此。</p>
<p><img src="/img/remote/1460000020470752" alt="image-20190923170248951" title="image-20190923170248951"></p>
<p>那么<code>Function.__proto__</code>为什么不等于<code>Object.prototype</code>呢?<code>Function</code>不是对象吗?</p>
<blockquote>Function确实是对象,同时还是构造函数,可以通过new Function()来得到函数实例。</blockquote>
<p>上面我们说到所有<code>函数</code>的原型是<code>Function.prototype</code>,所以<code>Function这个构造函数</code>的原型<code>__proto__</code>等于<code>Function.prototype</code>。</p>
<p>基于以上原理,还有以下相等关系:</p>
<ul>
<li><code>Object.__proto__ === Function.prototype</code></li>
<li><code>Array.__proto__ === Function.prototype</code></li>
</ul>
<h3>引申的问题</h3>
<p>我们知道<code>Function.__proto__</code>是指向<code>Function.prototype</code>,那个<code>Function.prototype</code>这个<code>Function</code>哪里来的?<code>Function</code>自己创造自己?那不是会死循环吗?</p>
<blockquote>
<p>这个问题不是纯JS层面能解决的,牵涉到底层实现,下面是网络上别人整理的结论,有需要的可以研究一下V8的源码,这样可以彻底解决这个问题。</p>
<ol>
<li>用C/C++ 构造内部数据结构创建一个 OP 即(Object.prototype)以及初始化其内部属性但不包括行为。</li>
<li>用 C/C++ 构造内部数据结构创建一个 FP 即(Function.prototype)以及初始化其内部属性但不包括行为。</li>
<li>将 FP 的[[Prototype]]指向 OP。</li>
<li>用 C/C++ 构造内部数据结构创建各种内置引用类型。</li>
<li>将各内置引用类型的[[Prototype]]指向 FP。</li>
<li>将 Function 的 prototype 指向 FP。</li>
<li>将 Object 的 prototype 指向 OP。</li>
<li>用 Function 实例化出 OP,FP,以及 Object 的行为并挂载。</li>
<li>用 Object 实例化出除 Object 以及 Function 的其他内置引用类型的 prototype 属性对象。</li>
<li>用 Function 实例化出除Object 以及 Function 的其他内置引用类型的 prototype 属性对象的行为并挂载。</li>
<li>实例化内置对象 Math 以及 Grobal</li>
<li>至此,所有 内置类型构建完成。</li>
</ol>
</blockquote>
<h3>函数结论</h3>
<ol><li>函数的原型都是<code>Function.protype</code>,构造函数也是函数,所以构造函数的原型也是<code>Function.prototype</code>
</li></ol>
<h2>来自灵魂的拷问1</h2>
<p>下面是一道有点难度的JS基础题,可以感受一下:</p>
<pre><code class="javascript">function A() {
}
function B(a) {
this.a = a;
}
function C(a) {
if(a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a);
console.log(new B().a);
console.log(new C().a);</code></pre>
<p>输出是</p>
<pre><code class="javascript">1
undefined
1</code></pre>
<h3>解释</h3>
<ol>
<li>
<p>为什么输出<code>1</code>?</p>
<blockquote>因为new A()这个对象上没有属性a,所以去查找原型链,查到了F.prototype.a</blockquote>
</li>
<li>
<p>为什么输出<code>undefined</code>?</p>
<blockquote>因为new B时没有传递a,所以a是undefined,new B()这个对象是有a属性的,只不过值是undefined,所以不查原型链</blockquote>
</li>
<li>
<p>为什么输出<code>1</code>?</p>
<blockquote>因为new C()未传递a,所以a是undefined,由于if(a)的判断,new C()这个对象内部没有a属性,所以去查原型链</blockquote>
</li>
</ol>
<h2>来自灵魂的拷问2</h2>
<pre><code class="javascript">function F() {
this.a = 1;
}
F.prototype.b = 2;
var f = new F();
console.log(f.hasOwnProperty('a'));
console.log(f.hasOwnProperty('b'));</code></pre>
<p>输出是</p>
<pre><code class="text">true
false</code></pre>
<h2>解释</h2>
<ol><li>为什么输出true`?</li></ol>
<blockquote>输出true比较好理解,因为构造函数<code>F</code>声明了属性<code>a</code>,所以<code>F</code>的实例有<code>a</code>属性</blockquote>
<ol><li>为什么输出<code>false</code>?</li></ol>
<blockquote>b是<code>f</code>的原型对象<code>F.prototype</code>的属性,不是<code>b</code>自己的,不能拿别人的说成自己的。</blockquote>
<h2>结尾</h2>
<p>本文研究了原型和原型链之间的关系以及常见对象的原型和原型链,对于特殊对象Function也研究了一下,如果能搞懂后面两个问题,那本文对你来说没什么问题了。</p>
搞懂JS变量提升
https://segmentfault.com/a/1190000020454881
2019-09-21T18:29:55+08:00
2019-09-21T18:29:55+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
11
<p>本文讲解Javascript变量提升引起的问题以及如何规避。</p>
<h2>问题</h2>
<p>今天看到一道有意思的面试题,考察的还真是JS的基本功,题目如下:</p>
<pre><code class="javascript">var name = "world";
(function(){
if(typeof name === "undefined") {
var name = "Jack";
console.log("Hello " + name);
} else {
console.log("Hello " + name);
}
}());</code></pre>
<p>根据if条件可以得出可能的答案:</p>
<ul>
<li>Hello world</li>
<li>Hello Jack</li>
</ul>
<h2>正确答案</h2>
<p>答案是<code>Hello Jack</code>,但是答案怎么来的,回答不好可能还是只能打50分,有以下两种理解:</p>
<p>理解1:</p>
<blockquote>立即执行函数有独立的作用域,访问不到外部name,所以if判断成立,输出 <code>Hello Jack</code>
</blockquote>
<p>这个理解是不正确的。虽然函数隔离了作用域,但是由于作用域链的关系,JS会从当前作用域一直往上级查找,直到顶级作用域(浏览器环境为window)。</p>
<p>如下代码输出<code>Hello world</code></p>
<pre><code class="javascript">var name = "world";
(function(){
console.log("Hello " + name);
}());</code></pre>
<p>理解2:</p>
<blockquote>var存在变量提升,所以if在判断的时候name确实为undefined,走了if分支,输出 <code>Hello Jack</code>
</blockquote>
<h2>变量提升</h2>
<p>MDN对变量提升的解释:</p>
<blockquote>
<p>“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中。</p>
<ul>
<li><strong><code>敲黑板: JavaScript 仅提升声明,而不提升初始化</code></strong></li>
<li><strong>函数和变量相比,会被优先提升</strong></li>
</ul>
</blockquote>
<p>根据变量提升理论我们可以“模拟”JS实际执行代码的过程:</p>
<pre><code class="javascript">var name = "world";
(function(){
var name; // 变量提升,仅提升声明,不提升初始化
if(typeof name === "undefined") {
name = "Jack";
console.log("Hello " + name);
} else {
console.log("Hello " + name);
}
}());</code></pre>
<p>函数内部作用域顶级的name初始化时为undefined,所以会走if分支,输出<code>Hello Jack</code>。这才是100分答案!</p>
<h2>规避变量提升问题</h2>
<blockquote><ol>
<li>在作用域的顶部定义变量</li>
<li>使用ES6新语法let或const定义变量</li>
</ol></blockquote>
<h2>技术参考</h2>
<ul><li><a href="https://link.segmentfault.com/?enc=jx0LiVmiIg3wUXlKtmDIOg%3D%3D.edc9nICp89DYI4XAWp0JDBSBaUfC7a%2BWbe%2FdQrrMHoaFI4xFVhwugUig9PSVzLgbmngVlabBk6Vu06NTnNMMmg%3D%3D" rel="nofollow">变量提升 - 术语表 | MDN</a></li></ul>
leetcode(3)——无重复字符的最长子串
https://segmentfault.com/a/1190000020398402
2019-09-16T18:33:29+08:00
2019-09-16T18:33:29+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
0
<p>欢迎跟着夏老师一起学习算法,这方面我自己的基础很薄弱,所以解题方法和思路也会讲的很”小白“,不需要什么基础就能看懂。</p>
<p>关注公众号可以进行交流和加入微信群,群内定期有系列文章分享噢!</p>
<p><img src="/img/remote/1460000020213550?w=400&h=177" alt="img" title="img"></p>
<h2>Question</h2>
<blockquote>给定一个字符串,请你找出其中不含有重复字符的 <strong>最长子串</strong> 的长度。</blockquote>
<p><strong>示例1:</strong></p>
<pre><code class="text">输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。</code></pre>
<p><strong>示例2:</strong></p>
<pre><code class="text">输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。</code></pre>
<p><strong>示例3:</strong></p>
<pre><code class="text">输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。</code></pre>
<h2>遍历法</h2>
<blockquote>
<p>最容易想到的一种算法,也是效率最低的一种算法</p>
<ol>
<li>通过两次遍历得到所有可能的 <strong>子字符串</strong> 列表</li>
<li>将每个字符串传入一个函数检测是否包含重复字符,如果不包含则更新最长子串的长度</li>
</ol>
</blockquote>
<pre><code class="javascript">// 判断给定的子串是否包含重复字符
function isUnique(str, start, end) {
const chars = [];
for(let i = start; i < end; i++) {
const char = str[i];
if(chars.indexOf(char) !== -1) { // 字符已存在,本字符串不符合条件
return false;
}
chars.push(char); // 添加字符
}
return true;
}
// 获取字符串最长子串长度
function lengthOfLongestSubstring(s) {
let max = 0;
for(let i = 0; i < s.length; i++) {
for(let j = i+1; j <= s.length; j++) {
if(isUnique(s, i, j)) { // 判断子串是否唯一
max = Math.max(max, j - i); // j - i 为当前子串长度
}
}
}
return max;
}</code></pre>
<p>时间复杂度$O(n^3)$</p>
<blockquote>i循环,j循环,isUnquie中的循环,3次循还嵌套</blockquote>
<p>空间复杂度$O(min(n,m))$</p>
<blockquote>isUnique函数中定义了一个数组来存储不重复的子串字符,长度为$k$,$k$的长度取决于字符串$s$的大小$n$以及 字符串$s$包含的不重复字符数大小$m$</blockquote>
<h2>滑动窗口法</h2>
<blockquote>暴力法中我们会重复检查一个子串是否包含重复的字符,如果从$i$ ~ $j-1$ 之间的子串已经被检查过没有重复字符了,那么只需要检查$s[j]$是否在这个子串就行了。<p>子串使用js自带的数据结构Set存储</p>
<p>如果不在该子串,那么子串长度+1,$j+1$,继续往后走</p>
<p>如果在这个子串,证明出现了重复,我们需要将$s[i]$移出来之后$i+1$,继续往后走</p>
</blockquote>
<pre><code class="javascript">function lengthOfLongestSubstring(s) {
const set = new Set();
const max = 0;
let i = 0;
let j = 0;
while(i < s.length && j < s.length) {
if(!set.has(s[j])) { // j 不在set中,set中添加s[j],j后移,同时更新最大子串长度
set.add(s[j]);
j++;
max = Math.max(max, j - i);
} else {
set.delete(s[i]); // 移除set左边的数据,i后移一位
i++;
}
}
return max;
}</code></pre>
<p>时间复杂度 $O(2n) \approx O(n)$</p>
<blockquote>最好的情况是j一次走完没有出现重复,最坏的情况是i和j都走到了末尾</blockquote>
<p>空间复杂度 $O(min(n,m))$</p>
<blockquote>与暴力法相似,也需要一个Set存储不重复字符,$n$ 是字符串$s$长度,$m$是字符串$s$中不重复的字母个数</blockquote>
<h2>优化的滑动窗口</h2>
<blockquote>在滑动窗口解法中,$i$的后移可以优化一下,如果 s$[j]$ 在 s[$i$] ~ s[$j$] 内与字符 $c$ (随便取的名字)重复,$i$ 不需要一步一步$i$++,直接把 $i$ 定位到 $c$ + 1的位置即可。这样可以将算法时间复杂度稳定在 $O(n)$</blockquote>
<pre><code class="javascript">function lengthOfLongestSubstring(s) {
const map = {}; // 保存 字符和下标的映射关系,如果字符重复,从map拿到位置,i直接跳到这个位置
let max = 0;
for(let i = 0, j = 0;j < s.length;j++) {
const char = s[j];
if(map[char] !== undefined) { // 当前字符存在重复,需要将i更新
i = Math.max(i, map[char]); // 如果i的当前位置大于map[char],不能更新为map[char]
}
max = Math.max(max, j - i + 1); // 由于j最大是s.length-1,所以最大子串长度需要+1
map[char] = j + 1; // 保存映射关系
}
return max;
}</code></pre>
<p>时间复杂度 $O(n)$</p>
<blockquote>只遍历了j</blockquote>
<p>空间复杂度 $O(min(n,m))$</p>
<blockquote>与之前的方法相同</blockquote>
<p>Q: 为什么第8行的 <code>i = Math.max(i, map[char])</code> 不能直接是 <code>i = map[char]</code>?</p>
<p>A: $i$ 的位置比<code>map[char]</code>大的情况下如果直接赋值会导致 $i$ 往前面走,会导致返回的子串长度大于实际的子串长度</p>
<p>错误例子 <code>abba</code></p>
<table>
<thead><tr>
<th>i</th>
<th>j</th>
<th>s[j]</th>
<th>s[i] ~ s[j]</th>
<th>Max</th>
</tr></thead>
<tbody>
<tr>
<td>0</td>
<td>0</td>
<td>a</td>
<td>a</td>
<td>1</td>
</tr>
<tr>
<td>0</td>
<td>1</td>
<td>b</td>
<td>ab</td>
<td>2</td>
</tr>
<tr>
<td>2(map中没有s[j],所以这里的位置直接是当前j的值)</td>
<td>2</td>
<td>b</td>
<td>b</td>
<td>2</td>
</tr>
<tr>
<td>1(map中有s[j],第1个字符就是a,直接拿来用)</td>
<td>3</td>
<td>a</td>
<td>bba</td>
<td>3</td>
</tr>
</tbody>
</table>
<p>可以看到第4次循环中 i 的位置已经出现了问题,把位置1的a拿过来进行计算了,窗口的起始左边也从2变成了1,往回走了。</p>
leetcode(2) —— 两数相加
https://segmentfault.com/a/1190000020376897
2019-09-12T23:24:14+08:00
2019-09-12T23:24:14+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
2
<p>欢迎跟着夏老师一起学习算法,这方面我自己的基础很薄弱,所以解题方法和思路也会讲的很”小白“,不需要什么基础就能看懂。</p>
<p>关注公众号可以进行交流和加入微信群,群内定期有系列文章分享噢!</p>
<p><img src="/img/remote/1460000020213550?w=400&h=177" alt="img" title="img"></p>
<h2>Question</h2>
<blockquote>给出两个 <strong>非空</strong> 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 <strong>逆序</strong> 的方式存储的,并且它们的每个节点只能存储 <strong>一位</strong> 数字。<p>如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。</p>
<p>您可以假设除了数字 0 之外,这两个数都不会以 0 开头。</p>
</blockquote>
<p><strong>示例:</strong></p>
<pre><code class="text">输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807</code></pre>
<h2>分析</h2>
<p>遍历两个链表把值加起来好之后插入链表,如果有进位的话需要把进位的值保存到后面的节点上,如果遍历完毕之后还剩下需要进位的值,那么需要插入末尾新节点。</p>
<h3>边界情况</h3>
<p>遇到链表相关的题目时一定要处理好边界情况,因为有些为空的链表或者只有1个节点的链表没有处理的必要,及时返回可以降低算法复杂度。</p>
<ol>
<li>链表1和链表2同时为空,直接返回undefined即可</li>
<li>链表1为空,返回链表2</li>
<li>链表2为空,返回链表1</li>
</ol>
<h2>解题方法</h2>
<pre><code class="javascript">// 链表节点定义
function ListNode(val) {
this.val = val;
this.next = null;
}
function addTwoNumbers(l1, l2) {
if(!l1 && !l2) { // 链表1和链表2同时为空,无需任何处理
return;
}
if(!l1) { // 链表1为空,直接返回链表2
return l2;
}
if(!l2) { // 链表2为空,直接返回链表1
return l1;
}
let carry = 0; // 进位值
let head = new ListNode(0); // 链表头节点
let p = head; // 链表移动指针
while(l1 || l2 || carry > 0) { // l1和l2虽然不会同时为空,但是存在l1和l2长度不一致的情况, 这种也需要处理
let sum = carry; // sum为本节点的值,需要加上前一个节点的进位值
if(l1) {
sum += l1.val; // 把链表1当前节点的值加上
l1 = l1.next; // 移动链表1指针
}
if(l2) {
sum += l2.val;
l2 = l2.next;
}
if(sum >= 10) { // 两个个位数相加最大值为18,所以到下一个节点进位的最大值为1
carry = 1;
sum -= 10; // 去掉十位,保留个位为节点最终值
} else {
carry = 0; // 相加之后和小于10,不需要进位,清除进位数据,否则死循环
}
p.next = new ListNode(sum); // 插入新节点
p = p.next; // 新链表指针后移
}
return head.next; // 头结点的值不是相加得到的,所以需要后移一个节点返回由两个链表加起来的结果
}</code></pre>
<p>进位的处理搞清楚之后这道题就清楚了。</p>
<p>时间复杂度O(max(l1.length, l2.length))</p>
<blockquote> 循环次数的根据链表1和链表2中长的那个链表来的,因为要保证两个链表的所有节点都被便利到</blockquote>
<p>空间复杂度O(max(l1,l2))</p>
<blockquote>最终链表的节点数也是根据链表1和链表2中长的那个链表来的,因为要保证两个链表的所有节点都被便利到,如果最后有进位的话,结果链表的长度会比链表1和链表2中长的链表大小+1。</blockquote>
<h2>结尾</h2>
<p>这道题的难度是中等,但是摸清楚链表的基本操作之后,应该没什么问题就能解决。</p>
leetcode(1) —— 两数之和
https://segmentfault.com/a/1190000020376888
2019-09-12T23:21:01+08:00
2019-09-12T23:21:01+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
1
<p>欢迎跟着夏老师一起学习算法,这方面我自己的基础很薄弱,所以解题方法和思路也会讲的很”小白“,不需要什么基础就能看懂。</p>
<p>关注公众号可以进行交流和加入微信群,群内定期有系列文章分享噢!</p>
<p><img src="/img/remote/1460000020213550?w=400&h=177" alt="img" title="img"></p>
<h2>问题</h2>
<blockquote>给定一个整数数组 <em><code>nums</code></em> 和一个目标值 <em><code>target</code></em>,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。<p>你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。</p>
</blockquote>
<p><strong>示例:</strong></p>
<pre><code class="text">给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]</code></pre>
<h2>嵌套循环解题法</h2>
<blockquote>通过第1遍循环可以拿到当前值和剩余值,然后嵌套循环一次,检查剩余值是不是在数组中。</blockquote>
<pre><code class="javascript">function twoSum(nums, target) {
for(let i = 0;i<nums.length;i++) {
const current = nums[i]; // 拿到当前值
const remain = target - current; // 拿到剩余值
for(let j = 1;j<nums.length;j++) {
if(nums[j] === remain) {
return [i, j];
}
}
}
}</code></pre>
<p>时间复杂度是O(n^2)</p>
<blockquote>nums的长度为n,嵌套循环的总执行次数是 n*(n-1),当n趋向于无穷大时n-1和n没什么区别,忽略</blockquote>
<p>空间复杂度为O(1)</p>
<blockquote>增加的临时变量有 current, remain, i, j,不会随着nums的长度而增加,所以是常量O(1)</blockquote>
<p>嵌套循环的效率是最低的, 面试的时候就算回答出来被送走的几率也是很大的。</p>
<h2>两遍HashTable解题法</h2>
<blockquote>核心思想是使用一个HashTable保存每个值和每个值的位置。<p>第1次循环时构造出HashTable,键为nums数组的元素,值为元素对应的下标</p>
<p>第2次循环时获取当前循环的值以及剩余值,如果剩余值的索引不等于当前值的索引,且剩余值也在HashTable中,直接从HashTable读取出当前值和剩余值的index返回。</p>
</blockquote>
<pre><code class="javascript">function twoSum(nums, target) {
const hashTable = {};
// 第1次循环
for(let i = 0;i<nums.length;i++) {
hashTable[nums[i]] = i;
}
// 第2次循环
for(let i = 0;i<nums.length;i++) {
const current = nums[i];
const remain = target - current;
if(map[remain] !== undefined && map[remain] !== i) {
return [i, map[remain]];
}
}
}</code></pre>
<p>时间复杂度为O(2n) = O(n)</p>
<blockquote>进行了两次循环,理论上是2*n的时间复杂度,但是当n趋向于无穷大时,n和2n的差距可以忽略,所以结果是O(n)</blockquote>
<p>空间复杂度为O(n)</p>
<blockquote>增加了HashTable,大小是nums的长度n,所以空间复杂度是O(n)</blockquote>
<p>该算法利用了HashTable的O(1)的时间复杂度巧妙地减少了嵌套循环,算法效率提升很大!</p>
<p>一般回答到这里基本就没啥问题了,但是还有一种基于HashTable一次循环就能解决问题的方案。</p>
<h2>一遍HashTable解题法</h2>
<blockquote>循环nums数组,得到当前值和剩余值,判断剩余值在不在HashTable,如果在的话,直接返回剩余值的位置和当前值的位置。如果不在则把剩余值插入HashTable,继续循环。<p>Q: 为什么先返回的是剩余值的位置而不是当前值的位置?</p>
<p>A: 因为当前值的位置是确定的,所以当前值的位置不在HashTable中,但是剩余值可能在前面的循环中插入了HashTable,是老值,所以先返回。</p>
</blockquote>
<pre><code class="javascript">function twoSum(nums, target) {
const hashTable = {};
for(let i = 0;i<nums.length;i++) {
const current = nums[i];
const remain = target - remain;
if(hashTable[remain] !== undefined) { // 为什么不需要判断位置?因为当前值的位置根本没插入HashTable中,索引不可能重复
return [hashTable[remain], i];
}
hashTable[current] = i; // 插入当前值到HashTable,下一次循环时这里就成了"老值"
}
}</code></pre>
<p>时间复杂度O(n)</p>
<blockquote>正宗的O(n),一次循环解决问题</blockquote>
<p>空间复杂度O(n)</p>
<blockquote>增加了HashTable,大小随着nums的增大而增大</blockquote>
<h2>结尾</h2>
<p>两数之和是leetcode的第1个问题,也是比较简单的一个问题,对算法有畏难情绪的读者可以把心收到肚子里了,跟着夏老师一起学算法!<br>有疑问的读者可以扫描上方二维码和我沟通。</p>
NestJs学习之旅(9)——拦截器
https://segmentfault.com/a/1190000020347579
2019-09-10T15:14:00+08:00
2019-09-10T15:14:00+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
3
<p>欢迎持续关注<em><code>NestJs之旅</code></em>系列文章</p>
<p><img src="/img/remote/1460000020213550?w=400&h=177" alt="img" title="img"></p>
<p>拦截器是一个实现了<strong>NestInterceptor</strong>接口且被<strong>@Injectable</strong>装饰器修饰的类。</p>
<p><img src="/img/remote/1460000020347582" alt="img" title="img"></p>
<p>拦截器是基于AOP编程思想的一种应用,以下是常用的功能:</p>
<ul>
<li>在方法执行之前或之后执行<strong>额外的逻辑</strong>,这些逻辑一般不属于业务的一部分</li>
<li>
<strong>转换</strong>函数执行结果</li>
<li>
<strong>转换</strong>函数执行时抛出的异常</li>
<li>扩展函数基本行为</li>
<li>特定场景下完全重写函数的行为(比如缓存拦截器,一旦有可用的缓存则直接返回,不执行真正的业务逻辑,即业务逻辑处理函数行为已经被重写)</li>
</ul>
<h2>拦截器接口</h2>
<p>每个拦截器都需要实现<strong>NestInterceptor</strong>接口的<strong>intercept()</strong>方法,该方法接收两个参数。方法原型如下:</p>
<pre><code class="typescript">function intercept(context: ExecutionContext, next: CallHandler): Observable<any></code></pre>
<ul>
<li>ExecutionContext 执行上下文,与<a href="https://link.segmentfault.com/?enc=jVG5ayjLwKsELTRiAibWIg%3D%3D.XFaazNxXI8IhUcqdBvZuYEfojtywl16xy%2FYLOEq%2Bcd%2FqOypC%2F5s9OxeDiupwPCtV5jQ3fRFSGiA5cAGIB1Eibw%3D%3D" rel="nofollow">NestJs学习之旅(7)——路由守卫</a>中的<strong>执行上下文</strong>相同</li>
<li>CallHandler 路由处理函数</li>
</ul>
<h2>CallHandler</h2>
<p>该接口是对路由处理函数的抽象,接口定义如下:</p>
<pre><code class="typescript">export interface CallHandler<T = any> {
handle(): Observable<T>;
}</code></pre>
<p>handle()函数的返回值也就是对应路由函数的返回值。</p>
<p>以获取用户列表为例:</p>
<pre><code class="typescript">// user.controller.ts
@Controller('user')
export class UserController {
@Get()
list() {
return [];
}
}</code></pre>
<p>当访问 /user/list 时,路由处理函数返回<strong>[]</strong>,如果在应用拦截器的情况下,调用CallHandler接口的handle()方法得到的也是Observable<[]>(RxJs包装对象)。</p>
<p><strong>所以,如果在拦截器中调用了next.handle()方法就会执行对应的路由处理函数,如果不调用的话就不会执行。</strong></p>
<h2>一个请求链路日志记录拦截器</h2>
<p>随着微服务的兴起,原来的单一项目被拆分成多个比较小的子模块,这些子模块可以独立开发、独立部署、独立运行,大大提高了开发、执行效率,但是带来的问题也比较多,一个经常遇到的问题是接口调用出错不好查找日志。</p>
<p>如果在业务逻辑中硬编码这种链路调用日志是非常不可取的,严重违反了单一职责的原则,这在微服务开发中是相当不好的一种行为,会让微服务变得臃肿,这些逻辑完全可以通过拦截器来实现。</p>
<pre><code class="typescript">// app.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Request } from 'express';
import { format } from 'util';
@Injectable()
export class AppInterceptor implements NestInterceptor {
private readonly logger = new Logger(); // 实例化日志记录器
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now(); // 请求开始时间
return next.handle().pipe(tap((response) => {
// 调用完handle()后得到RxJs响应对象,使用tap可以得到路由函数的返回值
const host = context.switchToHttp();
const request = host.getRequest<Request>();
// 打印请求方法,请求链接,处理时间和响应数据
this.logger.log(format(
'%s %s %dms %s',
request.method,
request.url,
Date.now() - start,
JSON.stringify(response),
));
}));
}
}</code></pre>
<pre><code class="typescript">// user.controller.ts
@UseInterceptors(AppInterceptor)
export class UserController {
@Get()
list() {
return [];
}
}</code></pre>
<p>当访问 /user时控制台想输出</p>
<pre><code class="text">[Nest] 96310 - 09/10/2019, 2:44 PM GET /user 1ms []</code></pre>
<h2>拦截器作用域</h2>
<p>拦截器可以在以下作用域进行绑定:</p>
<ul>
<li>全局拦截器</li>
<li>控制器拦截器</li>
<li>路由方法拦截器</li>
</ul>
<h3>全局拦截器</h3>
<p>在main.ts中使用以下代码即可:</p>
<pre><code class="typescript">const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new AppInterceptor());</code></pre>
<h3>控制器拦截器</h3>
<p>将对该控制器所有<strong>路由</strong>方法生效:</p>
<pre><code class="typescript">@Controller('user')
@UseInterceptors(AppInterceptor)
export class UserController {
}</code></pre>
<h3>路由方法拦截器</h3>
<p>只对当前被装饰的路由方法进行拦截:</p>
<pre><code class="typescript">@Controller('user')
export class UserController {
@UseInterceptors(AppInterceptor)
@Get()
list() {
return [];
}
}</code></pre>
<h2>响应处理</h2>
<p>CallHandler接口的handle()返回值实际上是RxJs的Observable对象,利用RxJs操作符可以对该对象进行操作,比如有一个API接口,之前返回的数据结构如下,如果正常响应,响应体就是数据,没有包装结构:</p>
<pre><code class="json">{
"id":1,
"name":"xialei"
}</code></pre>
<p>新的需求是要把之前的纯数据响应包装为一个data属性,结构如下:</p>
<pre><code class="json">{
"data": {
"id": 1,
"name":"xialei"
}
}</code></pre>
<p>接到这个需求时有的小伙伴可能已经在梳理响应接口的数量然后评估工时准备进行开发了,而使用NestJs的拦截器,不到一炷香的时间即可实现该需求。</p>
<pre><code class="typescript">import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class AppInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().
pipe(map(data => ({ data }))); // map操作符与Array.prototype.map类似
}
}</code></pre>
<p>应用上述拦截器后响应数据就会被包上一层data属性。</p>
<h2>异常映射</h2>
<p>另外一个有趣的例子是利用RxJs的catchError来覆盖路由处理函数抛出的异常。</p>
<pre><code class="typescript">import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(new BadGatewayException())) // catchError用来捕获异常
);
}
}</code></pre>
<h2>重写路由函数逻辑</h2>
<p>在文章开始部分提到了拦截器可以重写路由处理函数逻辑。如下是一个缓存拦截器的例子</p>
<pre><code class="typescript">import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private readonly cacheService: CacheService) {}
async intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const host = context.switchToHttp();
const request = host.getRequest();
if(request.method !== 'GET') {
// 非GET请求放行
return next.handle();
}
const cachedData = await this.cacheService.get(request.url);
if(cachedData) {
// 命中缓存,直接放行
return of(cachedData);
}
return next.handle().pipe(tap(response) => {
// 响应数据写入缓存,此处可以等待缓存写入完成,也可以不等待
this.cacheService.set(request.method, response);
});
}
}</code></pre>
<h2>结尾</h2>
<p>本文是NestJs基础知识的最后一篇,接下将针对特定模块进行更新,比如数据库、上传、鉴权等等。</p>
<p>由于直接放出群二维码导致加群门槛极低,近期有微商之类的人员扫码入群发送广告/恶意信息,严重骚扰群成员,二维码入群通道已关闭。有需要的伙伴可以关注公众号来获得加群资格。</p>
NestJs学习之旅(8)——管道
https://segmentfault.com/a/1190000020213547
2019-08-28T14:46:46+08:00
2019-08-28T14:46:46+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
7
<p>欢迎持续关注<strong>NestJs学习之旅</strong>系列文章</p>
<p><img src="/img/remote/1460000020213550" alt="img" title="img"></p>
<h2>管道</h2>
<p>熟悉Linux命令的伙伴应该对“管道运算符”不陌生。</p>
<pre><code class="bash">ls -la | grep demo</code></pre>
<p>"|" 就是管道运算符,它把左边命令的输出作为输入传递给右边的命令,支持级联,如此一来,便可以通过管道运算符进行复杂命令的交替运算。</p>
<p><img src="/img/remote/1460000020213551" alt="img" title="img"></p>
<p>NestJs中的管道有着类似的功能,也可以级联处理数据。NestJs管道通过<strong>@Injectable()</strong>装饰器装饰,需要实现<strong>PipeTransform</strong>接口。</p>
<p>NestJs中管道的主要职责如下:</p>
<ul>
<li>
<strong>数据转换</strong> 将输入数据转换为所需的输出</li>
<li>
<strong>数据验证</strong> 接收客户端提交的参数,如果通过验证则继续传递,如果验证未通过则提示错误</li>
</ul>
<h2>执行顺序</h2>
<p>在前面的文章中我们讨论了<strong>中间件</strong>、<strong>控制器</strong>、<strong>路由守卫</strong>,结合本问讨论的<strong>管道</strong>,可能有些读者会对这些组件的执行顺序提出疑问:这些东西执行的顺序到底是怎样的?</p>
<p>执行顺序也不用找资料,自己在这些组件执行时加上日志即可,我得出的结论如下:</p>
<blockquote>客户端请求 -> 中间件 -> 路由守卫 -> 管道 -> 控制器方法</blockquote>
<h2>开发管道</h2>
<p>数据转换类的管道就不详细解释了:</p>
<blockquote>给你一个value和元数据,你的return值就是转换后的值。</blockquote>
<p>NestJs内置了ValidationPipe、ParseIntPipe和ParseUUIDPipe。为了更好地理解它们的工作原理,我们以ValidationPipe(验证器管道)为例来演示管道的使用。</p>
<h3>PipeTransform</h3>
<p>这是管道必须实现的接口,该接口定义如下:</p>
<pre><code class="typescript">export interface PipeTransform<T = any, R = any> {
transform(value: T, metadata: ArgumentMetadata): R;
}</code></pre>
<ul>
<li>value <T> 输入参数,T为输入参数类型</li>
<li>metadata <ArgumentMetadata> value的元数据,包括参数来源,参数类型等等</li>
<li><R> 输出参数,R为输出参数类型</li>
</ul>
<h3>ArgumentMetadata</h3>
<p>用来描述当前处理value的元数据接口,接口定义如下:</p>
<pre><code class="typescript">export interface ArgumentMetadata {
readonly type: 'body' | 'query' | 'param' | 'custom';
readonly metatype?: Type<any>;
readonly data?: string;
}</code></pre>
<p>这个接口大家可能看不明白,没关系,等下会有具体示例来进行解读。</p>
<ul>
<li>type <string> 输入数据的来源</li>
<li>metatype <Type<any>> 注入数据的类型</li>
<li>data <string|undefined>传递给装饰器的数据类型</li>
</ul>
<p>例如如下控制器方法:</p>
<pre><code class="typescript">@Post()
login(@Query('type') type: number) { // type 为登录类型参数,类似手机号登录为1,账号登录为2的例子
}</code></pre>
<p>上述例子的元数据如下:</p>
<ul>
<li>type query @Query装饰器是读取GET参数</li>
<li>metatype Number type的类型符号</li>
<li>data type 传递给@Query装饰器的参数为“type”</li>
</ul>
<h2>验证器示例</h2>
<p>下面以用户登录时校验账号密码来说明验证器管道的使用,规则如下:</p>
<ul>
<li>账号必须是字符串,长度6-20</li>
<li>密码不能为空</li>
</ul>
<h3>DTO定义</h3>
<p>DTO在Java中是Data Transfer Object,简单来说就是对数据的一层包装。咱们NestJs中用这个东西一般是为了防止非法字段的提交和IDE自动提示(偷笑)。</p>
<p>使用规则装饰器需要安装class-validator和class-transformer:</p>
<pre><code class="bash">npm i --save class-validator class-transformer</code></pre>
<p>登录表单定义如下:</p>
<pre><code class="typescript">// userLogin.dto.ts
export class UserLoginDto {
@IsString()
@Length(6, 20, { message: '长度不合法' })
readonly username: string;
@Length(1)
readonly password: string;
}</code></pre>
<h3>管道定义</h3>
<p>由于咱们的管道是通用的,也就是验证什么内容是由外部决定的,管道只负责“你给我数据和规则,我来校验”。所以咱们需要使用到装饰器元数据。</p>
<pre><code class="typescript">// validate.pipe.ts
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
@Injectable()
export class ValidatePipe implements PipeTransform {
async transform(value: any, { metatype }: ArgumentMetadata): Promise<any> {
if (!metatype || !this.toValidate(metatype)) { // 如果不是注入的数据且不需要验证,直接跳过处理
return value;
}
// 数据格式转换
const object = plainToClass(metatype, value);
// 调用验证
const errors = await validate(object);
// 如果错误长度大于0,证明出错,需要抛出400错误
if (errors.length > 0) {
throw new BadRequestException(errors);
}
return value;
}
/**
* 需要验证的数据类型
* @param metatype
*/
private toValidate(metatype: any): boolean {
const types = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}</code></pre>
<h3>控制器定义</h3>
<p>今天的主角是管道,所以控制器层就不写逻辑了</p>
<pre><code class="typescript">// user.controller.ts
@Post('login')
@UsePipes(ValidatePipe)
login(@Body() userLoginDto: UserLoginDTO) {
return {errcode:0, errmsg: 'ok'};
}</code></pre>
<h3>运行项目</h3>
<p>项目根目录执行以下命令即可运行NestJs项目:</p>
<pre><code class="bash">npm run start</code></pre>
<p>项目运行后可以使用Postman来验证一下:</p>
<p>请求数据1</p>
<pre><code class="json">{
}</code></pre>
<p>响应数据1</p>
<pre><code class="json">{
"statusCode": 400,
"error": "Bad Request",
"message": [
{
"target": {},
"property": "username",
"children": [],
"constraints": {
"length": "长度不合法",
"isString": "username must be a string"
}
},
{
"target": {},
"property": "password",
"children": [],
"constraints": {
"length": "password must be longer than or equal to 1 characters"
}
}
]
}</code></pre>
<p>请求数据2</p>
<pre><code class="json">{
"username":"xialeistudio"
}</code></pre>
<p>响应数据2</p>
<pre><code class="json">{
"statusCode": 400,
"error": "Bad Request",
"message": [
{
"target": {
"username": "xialeistudio"
},
"property": "password",
"children": [],
"constraints": {
"length": "password must be longer than or equal to 1 characters"
}
}
]
}</code></pre>
<p>请求数据3</p>
<pre><code class="json">{
"username":"xialeistudio",
"password":"111111"
}</code></pre>
<p>响应数据3</p>
<pre><code class="json">[]</code></pre>
<h3>注意事项</h3>
<p>上文演示了ValidatePipe的实现,生产环境直接使用NestJs提供的ValidationPipe即可。我们可以在main.ts中使用全局管道。</p>
<pre><code class="typescript">async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();</code></pre>
<h2>结尾</h2>
<p>和笔者使用的SpringBoot中验证框架对比一下之后发现,NestJs验证管道所实现的功能还真不比SpringBoot差,看来官方说的“下一代Node.js全栈开发框架”确实不是盖的!</p>
<p>如果您觉得有所收获,分享给更多需要的朋友,谢谢!</p>
<p>如果您想交流关于NestJs更多的知识,欢迎加群讨论!</p>
<p><img src="/img/remote/1460000020213552" alt="20190827145318" title="20190827145318"></p>
NestJs学习之旅(7)——路由守卫
https://segmentfault.com/a/1190000020201409
2019-08-27T15:07:18+08:00
2019-08-27T15:07:18+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
9
<p>欢迎持续关注<strong>NestJs学习之旅</strong>系列文章</p>
<p><img src="/img/bVbwSlz?w=400&h=177" alt="图片描述" title="图片描述"></p>
<p>传统的Web应用中去检测用户登录、权限判断等等都是在控制器层或者中间件层做的,而在目前比较推荐的模块化与组件化架构中,不同职责的功能建议拆分到不同的类文件中去。</p>
<p>通过前几篇的学习可以发现NestJs在这方面做的很好,传统的express/koa应用中,需要开发者去思考项目结构以及代码组织,而NestJs不需要你这样做,降低了开发成本,另外也统一了开发风格。</p>
<h2>路由守卫</h2>
<p>熟悉Vue,React的伙伴应该比较熟悉这个概念,通俗的说就是在访问指定的路由之前回调一个处理函数,如果该函数<strong>返回true</strong>或者<strong>调用了next()</strong>就会放行当前访问,否则阻断当前访问。</p>
<p>NestJs中路由守卫也是如此,通过继承<strong>CanActive</strong>接口即可定义一个路由守卫。</p>
<p><img src="/img/bVbwVqV?w=906&h=269" alt="图片描述" title="图片描述"></p>
<pre><code class="typescript">import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
class AppGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}</code></pre>
<h2>路由守卫与中间件</h2>
<h3>区别</h3>
<p>路由守卫本质上也是中间件的一种,koa或者express开发中接口鉴权就是基于中间件开发的,如果当前请求是不被允许的,当前中间件将不会调用后续中间件,达到阻断请求的目的。</p>
<p>但是中间件的职责是不明确的,中间件可以干任何事(数据校验,格式转化,响应体压缩等等),这导致只能通过名称来识别中间件,项目迭代比较久以后,有比较高的维护成本。</p>
<h3>联系</h3>
<p>由于单一职责的关系,路由守卫只能返回true和false来决定放行/阻断当前请求,不可以修改request/response对象,因为一旦破坏单一职责的原则,排查问题比较麻烦。</p>
<p>如果需要修改request对象,可以结合中间件一起使用。</p>
<blockquote>路由守卫在所有中间件执行完毕之后开始执行。</blockquote>
<p>以下是一个结合路由守卫和中间件的例子。</p>
<pre><code class="typescript">// auth.middleware.ts
// 中间件职责:读取请求头Authorization,如果存在且有效的话,设置user对象到request中
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class AuthMiddleware implements NestMiddleware<Request|any, Response> {
constructor(private readonly userService: UserService) {}
async use(req: Request|any, res: Response, next: Function) {
const token = req.header('authorization');
if(!token) {
next();
return;
}
const user = await this.userService.getUserByToken(token);
if(!user) {
next();
return;
}
request.user = user;
next();
}
}
</code></pre>
<pre><code class="typescript">// user.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request } from 'express';
@Injectable()
export class UserGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request | any>();
// 直接检测是否有user对象,因为无user对象证明无token或者token无效
return !!request.user;
}
}</code></pre>
<p>以上例子是笔者常用的一种方法,这样职责比较清晰,而且user对象可以在其他中间件中读取。</p>
<h2>使用路由守卫来保护我们的应用</h2>
<p>NestJs使用<strong>@UseGuards()</strong>装饰器来注入路由守卫。支持全局守卫、控制器级别守卫、方法级别守卫。</p>
<p>下面以一个实际的例子来演示路由守卫的工作过程。</p>
<h3>登录流程</h3>
<ol>
<li>用户输入账号密码后进行登录,如果登录成功下发Token</li>
<li>客户端在请求头Authorization中加入第1步下发的Token进行请求</li>
<li>路由守卫读取当前请求的Authorization信息并与数据库的进行比对,如果成功则放行,否则阻断请求</li>
</ol>
<h3>定义token校验业务类</h3>
<pre><code class="typescript">// user.service.ts
@Injetable()
export class UserService {
// 模拟校验,这里直接返回true,实际开发中自行实现即可
validateToken(token: string) {
return true;
}
}</code></pre>
<h3>定义路由守卫</h3>
<pre><code class="typescript">// user.guard.ts
@Injetable()
export class UserGuard implements CanActive {
constructor(private readonly userService: UserService) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
// 读取token
const authorization = request.header('authorization');
if (!authorization) {
return false;
}
return this.userService.validateToken(authorization);
}
}</code></pre>
<h3>定义控制器</h3>
<pre><code class="typescript">@Controller('user')
export class UserController {
// 请求登录
@Post('login')
login() {
return {token:'fake_token'}; // 直接下发token,真实场景下需要验证账号密码
}
// 查看当前用户信息
@Get('info')
@UseGuards(UserGuard) // 方法级路由守卫
info() {
return {username: 'fake_user'};
}
}</code></pre>
<p>一个完整的路由守卫应用实例就已经出来了,虽然咱们的路由守卫没啥逻辑都是直接放行的,但是实际开发中也是基于这种思路去开发的,只不过校验的逻辑不一样罢了。</p>
<h2>路由守卫级别</h2>
<h3>控制器级别</h3>
<p>该级别会对被装饰控制器的所有路由方法生效。</p>
<pre><code class="typescript">@Controller('user')
@UseGuards(UserGuard)
export class UserController {
// 查看当前用户信息
@Get('info')
info() {
return {username: 'fake_user'};
}
}</code></pre>
<h3>方法级别</h3>
<p>该级别只对被装饰的方法生效。</p>
<pre><code class="typescript">@Get('info')
@UseGuards(UserGuard)
info() {
return {username: 'fake_user'};
}</code></pre>
<h3>全局级别</h3>
<p>与全局异常过滤器类似,该级别对所有控制器的所有路由方法生效。该方法与全局异常过滤器一样不会对WebSocket和GRPC生效。</p>
<pre><code class="typescript">async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 由于main.ts启动时并未初始化依赖注入容器,所以依赖必须手动传入,一般情况下不建议使用全局守卫,因为依赖注入得自己解决。
app.useGlobalGuards(new UserGuard(new UserService()));
await app.listen(3000);
}
bootstrap();
</code></pre>
<h2>执行上下文</h2>
<p><strong>CanActive</strong>接口的方法中有一个<strong>ExecutionContext</strong>对象,该对象为请求上下文对象,该对象定义如下:</p>
<pre><code class="typescript">export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}
</code></pre>
<p>可以看到继承了ArgumentHost,ArgumentHost在之前的异常处理文章中已经提到过了,这里不再赘述。</p>
<ul>
<li>getClass<T>() 获取当前访问的Controller对象(不是实例),T为调用时传入的具体控制器对象泛型参数</li>
<li>getHandler() 获取当前访问路由的方法</li>
</ul>
<p>例如访问 /user/info 时,getClass()将返回UserController对象(不是实例),getHandler()将返回info()函数的引用。</p>
<p>这个特性有什么作用呢?</p>
<blockquote>NestJs中可以使用反射来获取定义在方法、属性、类等等上面的自定义属性,这一点和Java的注解有点类似。</blockquote>
<h2>反射示例——基于角色的权限验证(RBAC)</h2>
<h3>定义角色装饰器</h3>
<p>被角色装饰器装饰的控制器或者方法在访问时,路由守卫会读取当前用户的角色,与装饰器传入的角色相匹配,如果匹配失败,将阻断请求,否则将放行请求。</p>
<pre><code class="typescript">// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
</code></pre>
<h3>定义控制器</h3>
<p>假设我们有一个只允许管理员访问的创建用户的接口:</p>
<pre><code class="typescript">@Post('create')
@Roles('admin')
async create(@Body() createUserDTO: CreateUserDTO) {
this.userService.create(createUserDTO);
}
</code></pre>
<h3>定义路由守卫</h3>
<pre><code class="typescript">// role.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 获取roles元数据,roles与roles.decorator.ts中SetMetadata()第一个参数一致
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) { // 未被装饰器装饰,直接放行
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user; // 读取请求对象的user,该user对象可以通过中间件来设置(本文前面有例子)
const hasRole = () => user.roles.some((role) => roles.includes(role));
return user && user.roles && hasRole();
}
}
</code></pre>
<p>以上就是读取自定义装饰器数据开发RBAC的例子,写的比较简陋,但是原理是一样的,代码量少的话便于理解核心。</p>
<h2>异常处理</h2>
<p>路由守卫返回false时框架会抛出ForbiddenException,客户端收到的默认响应如下:</p>
<pre><code class="json">{
"statusCode": 403,
"message": "Forbidden resource"
}
</code></pre>
<p>如果需要抛出其他异常,比如UnauthorizedException,可以直接在路由守卫的canActive()方法中抛出。</p>
<p>另外,在这里抛出的异常时可以被异常过滤器捕获并且处理的,所以我们可以自定义异常类型以及输出自定义响应数据。</p>
<h2>结尾</h2>
<p>本文除了路由守卫之外另一个重要的知识是【自定义元数据装饰器】的使用,基于该装饰器可以开发很多令人惊艳的功能,这个就看各位看官的实现了。</p>
<p>如果您觉得有所收获,分享给更多需要的朋友,谢谢!</p>
<p>如果您想交流关于NestJs更多的知识,欢迎加群讨论!<br><img src="/img/bVbwVq4?w=204&h=200" alt="图片描述" title="图片描述"></p>
NestJs学习之旅(6)——异常处理
https://segmentfault.com/a/1190000020189399
2019-08-26T15:35:13+08:00
2019-08-26T15:35:13+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
5
<p>欢迎持续关注<em><code>NestJs之旅</code></em>系列文章,关注公众号可以获得最新的教程!</p>
<p><img src="/img/bVbwSlz?w=400&h=177" alt="2019-08-26-060638.jpg" title="2019-08-26-060638.jpg"></p>
<h2>传统的异常处理</h2>
<p>在前面的内容中我们介绍了NestJs的几大常用组件,但是有一点没有做出说明,当我们的应用需要中断此次请求且输出错误信息时,我们需要怎么做?</p>
<p>这个问题有两种解决办法:</p>
<ol>
<li>
<p>services层直接返回中断请求的响应对象,controller直接输出该对象即可</p>
<pre><code class="typescript">if(!this.allowLogin()) {
return {errcode: 403, errmsg: '不允许登录'};
}</code></pre>
</li>
<li>services层抛出异常,controller捕获该异常,然后输出响应对象</li>
</ol>
<p>以上两种方法都有一定的缺点:</p>
<ol>
<li>controller调用多个services时,需要依据services层的返回值来进行错误判断,要是漏了判断的话会导致原本需要中断的请求处理继续运行,导致不可预料的后果</li>
<li>如果每个controller都需要try/catch掉services层抛出的异常的话,会多了很多“重复”代码</li>
</ol>
<p>那有没有一个像SpringBoot的<code>ExceptionHandler</code>相似的解决办法呢?</p>
<h2>NestJs的异常处理</h2>
<p>NestJs提供了统一的异常处理器,来集中处理运行过程中<strong>未捕获的异常</strong>,可以自定义响应参数,非常灵活。</p>
<p><img src="/img/remote/1460000020189403" alt="img" title="img"></p>
<h2>默认响应</h2>
<p>NestJs内置了默认的<strong>全局异常过滤器</strong>,该过滤器处理<strong>HttpException</strong>(及其子类)的异常。如果抛出的异常不是上述异常,则会响应以下默认JSON:</p>
<pre><code class="json">{
"statusCode": 500,
"message": "Interval server error"
}</code></pre>
<h2>内置异常过滤器</h2>
<p>由于NestJs内置了默认的异常过滤器,如果在应用内抛出HttpException,是可以被NestJs自动捕获的。</p>
<p>比如在services层抛出一个HttpException:</p>
<pre><code class="typescript">@Injectable()
export class UserService {
login(username: string, password: string) {
if(!this.allowLogin()) {
throw new HttpException('您无权登录', HttpStatus.FORBIDDEN);
}
return {user_id:1, token: 'fake token'}
}
}</code></pre>
<p>controller正常调用该services即可:</p>
<pre><code class="typescript">@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('login')
login(@Body('username') username: string, @Body('password') password: string) {
return this.userService.login(username, password);
}
}</code></pre>
<p>客户端访问/user/login时,如果不允许登录,会收到以下响应:</p>
<pre><code class="json">{
"statusCode": 403,
"message": "您无权登录"
}</code></pre>
<p>一般情况下,上述JSON的返回的信息是不够的,比如有些业务自定义的错误码没地方可以自定义。</p>
<p>如果你有这种需求,可以传递object给HttpException的第一个参数来实现:</p>
<pre><code class="typescript">throw new HttpException({errcode: 40010, errmsg: '您无权登录'}, HttpStatus.FORBIDDEN);</code></pre>
<p>客户端访问时,如果不允许登录,会收到以下响应:</p>
<pre><code class="json">{
"errcode": 40010,
"errmsg": "您无权登录"
}</code></pre>
<h2>自定义异常</h2>
<p>企业级应用开发过程中,使用HttpException进行处理对开发是不太友好的,一个比较常用的做法是自定义一个UserException来承载业务异常(系统运行正常,只不过当前请求不满足业务上的要求而中断,比如注册的时候用户名重复的时候打回去,此时数据库查询是正常的,这就是业务异常和系统异常的区别)。</p>
<pre><code class="typescript">export class UserException extends HttpException {
constructor(errcode: number, errmsg: string, statusCode: number) {
super({ errcode, errmsg }, statusCode);
}
}</code></pre>
<p>业务层在使用该异常时直接使用以下代码即可,将原来传递对象的代码扁平化了:</p>
<pre><code class="typescript">throw new UserException(40010, '您无权登录', HttpStatus.FORBIDDEN);
</code></pre>
<h3>语义化业务异常</h3>
<p>使用自定义异常时HTTP协议层是正常的,抛出403错误有点不符合语义化的需求。对上例改造一下:</p>
<pre><code class="typescript">export class UserException extends HttpException {
constructor(errcode: number, errmsg: string) {
super({ errcode, errmsg }, HttpStatus.OK);
}
}
</code></pre>
<pre><code class="typescript">throw new UserException(40010, '您无权登录');
</code></pre>
<p>此时客户端收到的HttpStatus为200,意味着此次请求在协议层面是成功的,只不过业务层返回了错误。前端在处理响应时可以直接对errcode是否为0来确定此次请求是否成功。</p>
<h2>自定义异常过滤器</h2>
<p>虽然内置的异常过滤器可以自动处理很多情况,但是不是“可编程”的,也就是说我们无法完全控制异常处理过程,如果我们需要记录日志的话,使用内置的异常过滤器办不到,这时候可以使用<strong>@Catch</strong>注解来自定义异常处理器,添加日志记录什么的。</p>
<pre><code class="typescript">import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// @todo 记录日志
console.log('%s %s error: %s', request.method, request.url, exception.message);
// 发送响应
response
.status(status)
.json({
statusCode: status,
message: exception.message
path: request.url,
});
}
}
</code></pre>
<h3>ArgumentHost</h3>
<p>ArgumentHost是原始请求的包装器,由于NestJs支持HTTP/GRPC/WebSocket,这三种请求的原始请求对象是有差异的,为了异常过滤器能够统一处理这三种异常,NestJs做了包装。最终在使用时处理那种异常由开发者来决定。</p>
<p>ArgumentHost接口定义如下:</p>
<pre><code class="typescript">export interface ArgumentsHost {
getArgs<T extends Array<any> = any[]>(): T;
getArgByIndex<T = any>(index: number): T;
switchToRpc(): RpcArgumentsHost;
switchToHttp(): HttpArgumentsHost;
switchToWs(): WsArgumentsHost;
}
</code></pre>
<p>如果需要处理的是WebSocket异常,就使用<strong>host.switchToWs()</strong>,其他异常以此类推。</p>
<h3>使用自定义异常过滤器</h3>
<p>如果定义完自定义异常过滤器之后,直接去访问会抛出异常的接口,此时可以发现并没有走自定义异常过滤器。</p>
<p>因为我们<strong>只是定义,并没有注册</strong>。</p>
<p>使用<strong>@UseFilters</strong>注册自定义异常过滤器。</p>
<p>异常过滤器有以下三种作用范围:</p>
<ul>
<li>方法级别</li>
<li>控制器级别</li>
<li>全局级别</li>
</ul>
<h3>方法级别</h3>
<p>只会处理该方法上抛出的异常,其他方法抛出的异常不会处理。</p>
<pre><code class="typescript">@Post('login')
@UseFilters(UserExceptionFilter)
login(@Body('username') username:string, password: string) {
throw new UserException(40010, '您无权登录');
}
</code></pre>
<h3>控制器级别</h3>
<p>只会处理该控制器方法上抛出的异常,其他控制器抛出的异常不处理。</p>
<pre><code class="typescript">@Controller('user')
@UseFilters(UserExceptionFilter)
export class UserController {
}
</code></pre>
<h3>全局级别</h3>
<p>在应用入口注册,不会对Websocket或者混合应用(同时支持两种应用,如HTTP/GRPC或者HTTP/WebSocket)生效。一般Web开发中全局异常过滤器已经够用了。</p>
<p>在main.ts中注册全局异常过滤器</p>
<pre><code class="typescript">async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new UserExceptionFilter());
await app.listen(3000);
}
bootstrap();
</code></pre>
<h2>依赖注入</h2>
<p>由于异常过滤器并不是任何模块上下文的一部分,所以NestJs无法对其进行依赖注入管理,如果有此种需求,比如在异常过滤器中注入service,需要定义服务提供者。服务提供者名称为NestJs规定的常量APP_FILTER</p>
<pre><code class="typescript">import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: UserExceptionFilter,
},
],
})
export class AppModule {}
</code></pre>
<h2>捕获多种异常或者所有异常</h2>
<p>上例中提到的自定义异常处理器只会捕获UserException异常,如果有系统异常,会使用内置的异常处理器。通过传入异常类型给<strong>@Catch</strong>装饰器来捕获多种异常。如果不传任何异常类型的话,NestJs会捕获所有异常(也就是Error及其子类)。</p>
<pre><code class="typescript">import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch() // 捕获所有异常
export class HttpExceptionFilter implements ExceptionFilter<Error> {
catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// @todo 记录日志
console.log('%s %s error: %s', request.method, request.url, exception.message);
// 发送响应
response
.status(status)
.json({
statusCode: status,
message: exception.message
path: request.url,
});
}
}
</code></pre>
<h2>结尾</h2>
<p>异常过滤器让应用异常有了统一的处理渠道,同时也解决文章开头提出的两个问题。通过自定义异常过滤器,开发者可以进行统一响应格式,统一记录日志等等操作。</p>
<p>如果您觉得有所收获,分享给更多需要的朋友,谢谢!</p>
<p>如果您想交流关于NestJs更多的知识,欢迎加群讨论!</p>
<p><img src="/img/remote/1460000020138769?w=200&h=200" alt="img" title="img"></p>
Socks5代理协议
https://segmentfault.com/a/1190000020174099
2019-08-24T16:54:24+08:00
2019-08-24T16:54:24+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
3
<h2>Socks5代理协议</h2>
<p>或许你没听说过socks5,但你一定听说过SS,SS内部使用的正是socks5协议。</p>
<p>socks5是一种网络传输协议,主要用于客户端与目标服务器之间通讯的透明传递。</p>
<p>该协议设计之初是为了让有权限的用户可以穿过防火墙的限制,访问外部资源。</p>
<h3>1. RFC地址</h3>
<ol>
<li><a href="https://link.segmentfault.com/?enc=UQtG0sd5IxVJ3OXVpOQX9g%3D%3D.y%2BEO7ITwSZ4s0mOgpvW%2FWFvB%2F8%2B59StTK3fgDrHrrH1%2FqOQvbwiveIgx4jMYPCgO" rel="nofollow">socks5协议规范rfc1928</a></li>
<li><a href="https://link.segmentfault.com/?enc=tJT5eUitr8uowV8UXJ0%2BEw%3D%3D.ABCp%2B6ah2fOXrb1rZHlfj0RKlsxhc3mBzAy4h7%2FqH2QsfAYEJS45I01sw8tKslzW" rel="nofollow">socks5账号密码鉴权规范rfc1929</a></li>
</ol>
<h3>2. 协议过程</h3>
<p><img src="/img/bVbwOmL?w=1452&h=520" alt="1" title="1"></p>
<ol>
<li>客户端连接上代理服务器之后需要发送请求告知服务器目前的socks协议版本以及支持的认证方式</li>
<li>代理服务器收到请求后根据其设定的认证方式返回给客户端</li>
<li>如果代理服务器不需要认证,客户端将直接向代理服务器发起真实请求</li>
<li>代理服务器收到该请求之后连接客户端请求的目标服务器</li>
<li>代理服务器开始转发客户端与目标服务器之间的流量</li>
</ol>
<h3>3. 认证过程</h3>
<h4>3.1 客户端发出请求</h4>
<blockquote>客户端连接服务器之后将直接发出该数据包给代理服务器</blockquote>
<table>
<thead><tr>
<th>VERSION</th>
<th>METHODS_COUNT</th>
<th align="left">METHODS...</th>
</tr></thead>
<tbody>
<tr>
<td>1字节</td>
<td>1字节</td>
<td align="left">1到255字节,长度由METHODS_COUNT值决定</td>
</tr>
<tr>
<td>0x05</td>
<td>0x03</td>
<td align="left">0x00 0x01 0x02</td>
</tr>
</tbody>
</table>
<ul>
<li>VERSION SOCKS协议版本,目前固定0x05</li>
<li>METHODS_COUNT 客户端支持的认证方法数量</li>
<li>METHODS... 客户端支持的认证方法,每个方法占用1个字节</li>
</ul>
<p>METHOD定义</p>
<ul>
<li>0x00 不需要认证(常用)</li>
<li>0x01 GSSAPI认证</li>
<li>0x02 账号密码认证(常用)</li>
<li>0x03 - 0x7F IANA分配</li>
<li>0x80 - 0xFE 私有方法保留</li>
<li>0xFF 无支持的认证方法</li>
</ul>
<h4>3.2 服务端返回选择的认证方法</h4>
<blockquote>接收完客户端支持的认证方法列表后,代理服务器从中选择一个受支持的方法返回给客户端</blockquote>
<h5>3.2.1 无需认证</h5>
<table>
<thead><tr>
<th>VERSION</th>
<th>METHOD</th>
</tr></thead>
<tbody>
<tr>
<td>1字节</td>
<td>1字节</td>
</tr>
<tr>
<td>0x05</td>
<td>0x00</td>
</tr>
</tbody>
</table>
<ul>
<li>VERSION SOCKS协议版本,目前固定0x05</li>
<li>METHOD 本次连接所用的认证方法,上例中为无需认证</li>
</ul>
<h5>3.2.2 账号密码认证</h5>
<table>
<thead><tr>
<th>VERSION</th>
<th>METHOD</th>
</tr></thead>
<tbody>
<tr>
<td>1字节</td>
<td>1字节</td>
</tr>
<tr>
<td>0x05</td>
<td>0x02</td>
</tr>
</tbody>
</table>
<h5>3.2.3 客户端发送账号密码</h5>
<blockquote>服务端返回的认证方法为0x02(账号密码认证)时,客户端会发送账号密码数据给代理服务器</blockquote>
<table>
<thead><tr>
<th>VERSION</th>
<th>USERNAME_LENGTH</th>
<th>USERNAME</th>
<th>PASSWORD_LENGTH</th>
<th>PASSWORD</th>
</tr></thead>
<tbody>
<tr>
<td>1字节</td>
<td>1字节</td>
<td>1-255字节</td>
<td>1字节</td>
<td>1-255字节</td>
</tr>
<tr>
<td>0x01</td>
<td>0x01</td>
<td>0x0a</td>
<td>0x01</td>
<td>0x0a</td>
</tr>
</tbody>
</table>
<ul>
<li>VERSION 认证子协商版本(与SOCKS协议版本的0x05无关系)</li>
<li>USERNAME_LENGTH 用户名长度</li>
<li>USERNAME 用户名字节数组,长度为USERNAME_LENGTH</li>
<li>PASSWORD_LENGTH 密码长度</li>
<li>PASSWORD 密码字节数组,长度为PASSWORD_LENGTH</li>
</ul>
<h5>3.2.4 服务端响应账号密码认证结果</h5>
<blockquote>收到客户端发来的账号密码后,代理服务器加以校验,并返回校验结果</blockquote>
<table>
<thead><tr>
<th>VERSION</th>
<th>STATUS</th>
</tr></thead>
<tbody><tr>
<td>1字节</td>
<td>1字节</td>
</tr></tbody>
</table>
<ul>
<li>VERSION 认证子协商版本,与客户端VERSION字段一致</li>
<li>
<p>STATUS 认证结果</p>
<ul>
<li>0x00 认证成功</li>
<li>大于0x00 认证失败</li>
</ul>
</li>
</ul>
<h3>4. 命令过程</h3>
<blockquote>认证成功后,客户端会发送连接命令给代理服务器,代理服务器会连接目标服务器,并返回连接结果</blockquote>
<h5>4.1 客户端请求</h5>
<table>
<thead><tr>
<th>VERSION</th>
<th>COMMAND</th>
<th>RSV</th>
<th>ADDRESS_TYPE</th>
<th>DST.ADDR</th>
<th>DST.PORT</th>
</tr></thead>
<tbody><tr>
<td>1字节</td>
<td>1字节</td>
<td>1字节</td>
<td>1字节</td>
<td>1-255字节</td>
<td>2字节</td>
</tr></tbody>
</table>
<ul>
<li>VERSION SOCKS协议版本,固定0x05</li>
<li>
<p>COMMAND 命令</p>
<ul>
<li>0x01 CONNECT 连接上游服务器</li>
<li>0x02 BIND 绑定,客户端会接收来自代理服务器的链接,著名的FTP被动模式</li>
<li>0x03 UDP ASSOCIATE UDP中继</li>
</ul>
</li>
<li>RSV 保留字段</li>
<li>
<p>ADDRESS_TYPE 目标服务器地址类型</p>
<ul>
<li>0x01 IP V4地址</li>
<li>0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组</li>
<li>0x04 IP V6地址</li>
</ul>
</li>
<li>DST.ADDR 目标服务器地址</li>
<li>DST.PORT 目标服务器端口</li>
</ul>
<h5>4.2 代理服务器响应</h5>
<table>
<thead><tr>
<th>VERSION</th>
<th>RESPONSE</th>
<th>RSV</th>
<th>ADDRESS_TYPE</th>
<th>BND.ADDR</th>
<th>BND.PORT</th>
</tr></thead>
<tbody><tr>
<td>1字节</td>
<td>1字节</td>
<td>1字节</td>
<td>1字节</td>
<td>1-255字节</td>
<td>2字节</td>
</tr></tbody>
</table>
<ul>
<li>VERSION SOCKS协议版本,固定0x05</li>
<li>
<p>RESPONSE 响应命令</p>
<ul>
<li>0x00 代理服务器连接目标服务器成功</li>
<li>0x01 代理服务器故障</li>
<li>0x02 代理服务器规则集不允许连接</li>
<li>0x03 网络无法访问</li>
<li>0x04 目标服务器无法访问(主机名无效)</li>
<li>0x05 连接目标服务器被拒绝</li>
<li>0x06 TTL已过期</li>
<li>0x07 不支持的命令</li>
<li>0x08 不支持的目标服务器地址类型</li>
<li>0x09 - 0xFF 未分配</li>
</ul>
</li>
<li>RSV 保留字段</li>
<li>BND.ADDR 代理服务器连接目标服务器成功后的代理服务器IP</li>
<li>BND.PORT 代理服务器连接目标服务器成功后的代理服务器端口</li>
</ul>
<h3>5. 通信过程</h3>
<blockquote> 经过认证与命令过程后,客户端与代理服务器进入正常通信,客户端发送需要请求到目标服务器的数据给代理服务器,代理服务器转发这些数据,并把目标服务器的响应转发给客户端,起到一个“透明代理”的功能。</blockquote>
<h3>6. 实际例子</h3>
<p>上文详细讲解了协议规范,下面来一个实例的通信过程范例。</p>
<p><em>6.2中无需认证和需要账号密码认证是互斥的</em>,同一请求只会采取一种,本文都列在下面。</p>
<h4>6.1 客户端发送受支持的认证方法</h4>
<pre><code class="text">0x05 0x02 0x00 0x02</code></pre>
<ul>
<li>0x05 SOCKS5协议版本</li>
<li>0x02 支持的认证方法数量</li>
<li>0x00 免认证</li>
<li>0x02 账号密码认证</li>
</ul>
<h4>6.2 服务端响应选择的认证方法</h4>
<h5>6.2.1 无需认证</h5>
<blockquote>以下是无需认证,客户端收到该响应后直接发送需要发送给目标服务器的数据给到代理服务器,此时进入通信错过程</blockquote>
<pre><code class="text">0x05 0x00</code></pre>
<ul>
<li>0x05 SOCKS5协议版本</li>
<li>0x00 免认证</li>
</ul>
<h5>6.2.2 需要账号密码认证</h5>
<pre><code class="text">0x05 0x02</code></pre>
<ul>
<li>0x05 SOCKS5协议版本</li>
<li>0x02 账号密码认证</li>
</ul>
<h5>6.2.3 客户端发送账号密码</h5>
<pre><code class="text">0x01 0x04 0x61 0x61 0x61 0x61 0x04 0x61 0x61 0x61 0x61</code></pre>
<ul>
<li>0x01 子协商版本</li>
<li>0x04 用户名长度</li>
<li>0x61 0x61 0x61 0x61 转换为ascii字符之后为"aaaa"</li>
<li>0x04 密码长度</li>
<li>0x61 0x61 0x61 0x61 转换为ascii字符之后"aaaa"</li>
</ul>
<h5>6.2.4 代理服务器响应认证结果</h5>
<pre><code class="text">0x01 0x00</code></pre>
<ul>
<li>0x01 子协商版本</li>
<li>0x00 认证成功(也就是代理服务器允许aaaa账号以aaaa密码登录)</li>
</ul>
<h4>6.3 客户端请求代理服务器连接目标服务器</h4>
<p>以127.0.0.1和80端口为例</p>
<pre><code class="text">0x05 0x01 0x01 0x01 0x7f 0x00 0x00 0x01 0x00 0x50</code></pre>
<ul>
<li>0x05 SOCKS协议版本</li>
<li>0x01 CONNECT命令</li>
<li>0x01 RSV保留字段</li>
<li>0x01 地址类型为IPV4</li>
<li>0x7f 0x00 0x00 0x01 目标服务器IP为127.0.0.1</li>
<li>0x00 0x50 目标服务器端口为80</li>
</ul>
<h4>6.4 代理服务器连接目标主机,并返回结果给客户端</h4>
<pre><code class="text">0x05 0x00 0x01 0x01 0x7f 0x00 0x00 0x01 0x00 0xaa 0xaa</code></pre>
<ul>
<li>0x05 SOCKS5协议版本</li>
<li>0x00 连接成功</li>
<li>0x01 RSV保留字段</li>
<li>0x01 地址类型为IPV4</li>
<li>0x7f 0x00 0x00 0x01 代理服务器连接目标服务器成功后的代理服务器IP, 127.0.0.1</li>
<li>0xaa 0xaa 代理服务器连接目标服务器成功后的代理服务器端口(代理服务器使用该端口与目标服务器通信),本例端口号为43690</li>
</ul>
<h4>6.5 客户端发送请求数据给代理服务器</h4>
<p>如果客户端需要请求目标服务器的HTTP服务,就会发送HTTP协议报文给代理服务器,代理服务器将这些报文原样转发给目标服务器,并将目标服务器的响应发送给客户端,代理服务器不会对客户端或者目标服务器的报文做任何解析。</p>
<h3>7. 结尾</h3>
<p>SOCKS5协议的讲解到此结束,后续会使用GOLANG实现一个SOCKS5服务器来讲述TCP协议服务器的开发。</p>
<p><img src="/img/bVbwOmM?w=638&h=283" alt="2019-08-24-085213.jpg" title="2019-08-24-085213.jpg"></p>
NestJs学习之旅(5)——中间件
https://segmentfault.com/a/1190000020161858
2019-08-23T11:06:08+08:00
2019-08-23T11:06:08+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
4
<p>欢迎持续关注<code>NestJs之旅</code>系列文章<br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<h2>中间件</h2>
<p>中间件是在路由处理程序<strong>之前</strong>调用的函数。中间件函数可以访问<strong>请求</strong>和<strong>响应</strong>对象。</p>
<p>使用过koa和express的朋友应该知道,中间件是一个很核心的功能,尤其是koa,核心就是中间件,连路由功能都是由中间件提供的。</p>
<p>中间件可以提供以下功能:</p>
<ul>
<li>运行过程中执行任意代码</li>
<li>对请求和响应进行更改</li>
<li>结束本次请求的响应</li>
<li>继续调用下一个中间件</li>
</ul>
<h2>示例</h2>
<p>NestJs使用<code>@Injectable()</code>来装饰中间件,被装饰的对象应该实现<code>NestMiddleware</code>接口。</p>
<p>以下是一个日志中间件的实现:</p>
<pre><code class="ts">// log.middleware.ts
import {Injectable, NestMiddleware} from '@nestjs/common';
import {Request, Response} from 'express';
@Injectable()
export class LogMiddleware implements NestMiddleware {
use(req: Request, resp: Response, next: Function) {
console.log(`${req.method} ${req.path}`)
next();
}
}</code></pre>
<pre><code class="ts">// app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LogMiddleware } from './common/middleware/log.middleware';
import { UserModule } from './user/user.module';
@Module({
imports: [UserModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LogMiddleware)
.forRoutes('users');
}
}</code></pre>
<h2>针对请求方法应用中间件</h2>
<p>上面的简单示例中会对所有的<code>users</code>路由应用中间件,如果需要只对特定的请求方法,比如GET请求才应用中间件,可以使用以下方式:</p>
<pre><code class="ts">import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LogMiddleware } from './common/middleware/log.middleware';
import { UserModule } from './user/user.module';
@Module({
imports: [UserModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LogMiddleware)
.forRoutes({ path: 'users', method: RequestMethod.GET });
}
}</code></pre>
<h2>应用多个中间件</h2>
<pre><code class="ts">import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LogMiddleware } from './common/middleware/log.middleware';
import { UserModule } from './user/user.module';
@Module({
imports: [UserModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LogMiddleware, OtherMiddleware)
.forRoutes({ path: 'users', method: RequestMethod.GET });
}
}</code></pre>
<h2>基于控制器名称应用中间件</h2>
<p>上述代码都是针对固定的路由地址应用中间件,在NestJs中路由地址是通过装饰器定义的,如果控制器的路由地址有变化,而中间件这里没有跟着改掉,就会导致问题。</p>
<p>NestJs在使用中间件的时候提供了基于控制器来注册的方式:</p>
<pre><code class="ts">import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LogMiddleware } from './common/middleware/log.middleware';
import { UserModule } from './user/user.module';
@Module({
imports: [UserModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LogMiddleware)
.forRoutes(UserController);
}
}</code></pre>
<h2>排除指定路由</h2>
<p>有些场景下对控制器应用了中间件之后需要绕过其中几个方法,比如登录验证中间件应该放行登录路由,否则没有人能够登录成功。</p>
<pre><code class="ts">import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LogMiddleware } from './common/middleware/log.middleware';
import { UserModule } from './user/user.module';
@Module({
imports: [UserModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LogMiddleware)
.exclude(
{path:'users/login',method:RequestMethod.GET}
)
.forRoutes(UserController);
}
}</code></pre>
<h2>全局中间件</h2>
<p>类似于全局模块,中间件也可以全局注册,对每一个路由都生效。</p>
<pre><code class="ts">// main.ts
const app = await NestFactory.create(AppModule);
app.use(LogMiddleware);
await app.listen(3000);</code></pre>
<h2>结尾</h2>
<p>中间件给框架赋予了极大的灵活性,可以根据功能抽象为中间件,达到”可插拔“的目的。</p>
<p>如果您觉得有所收获,分享给更多需要的朋友,谢谢!</p>
<p>如果您想交流关于NestJs更多的知识,欢迎加群讨论!</p>
<p><img src="/img/remote/1460000020138769?w=200&h=200" alt="image" title="image"></p>
NestJs学习之旅(4)——模块系统
https://segmentfault.com/a/1190000020150791
2019-08-22T12:06:45+08:00
2019-08-22T12:06:45+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
6
<p>欢迎持续关注<code>NestJs之旅</code>系列文章<br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<h2>模块</h2>
<p>NestJs中模块是构建和组织业务单元的基本元素。使用<code>@Module()</code>装饰模块来声明该模块的元信息:</p>
<ul>
<li>本模块导出哪些服务提供者</li>
<li>本模块导入了哪些依赖模块</li>
<li>本模块提供了哪些控制器</li>
</ul>
<p>每个NestJs至少有一个跟模块,这个就是<code>app.module.ts</code>定义的。根模块一般不放具体的业务逻辑,具体业务逻辑应该下沉到各个子业务模块去做。</p>
<p>比如我们开发一个商城系统,该系统有以下业务模块:</p>
<ul>
<li>订单中心</li>
<li>用户中心</li>
<li>支付中心</li>
<li>商品中心</li>
<li>物流中心</li>
</ul>
<p>那我们可以定义以下的模块结构:</p>
<pre><code>|-- app.module.ts
|-- order
|-- order.module.ts
|-- services
|-- order.service.ts
|-- controllers
|-- order.controller.ts
|-- user
|-- user.module.ts
|-- services
|-- user.service.ts
|-- controllers
|-- user.controller.ts
|-- pay
|-- pay.module.ts
|-- services
|-- wepay.service.ts
|-- alipay.service.ts
|-- pay.service.ts
|-- controller
|-- pay.controller.ts
...</code></pre>
<p>模块化有以下优点:</p>
<ul>
<li>业务低耦合</li>
<li>边界清晰</li>
<li>便于排查错误</li>
<li>便于维护</li>
</ul>
<h2>模块声明与配置</h2>
<p><code>@Module()</code>装饰的类为<code>模块类</code>,该装饰器的典型用法如下:</p>
<pre><code class="ts">@Module({
providers: [UserService],
controllers: [UserController],
imports: [OrderModule],
exports: [UserService]
})
export class UserModule {
}</code></pre>
<table>
<thead><tr>
<th align="left">参数名称</th>
<th align="left">说明</th>
</tr></thead>
<tbody>
<tr>
<td align="left">proviers</td>
<td align="left">服务提供者列表,本模块可用,可以自动注入</td>
</tr>
<tr>
<td align="left">controllers</td>
<td align="left">控制器列表,本模块可用,用来绑定路由访问</td>
</tr>
<tr>
<td align="left">imports</td>
<td align="left">本模块导入的模块,如果需要使用到其他模块的服务提供者,此处必须导入其他模块</td>
</tr>
<tr>
<td align="left">exports</td>
<td align="left">本模块导出的服务提供者,只有在此处定义的服务提供者才能在其他模块使用</td>
</tr>
</tbody>
</table>
<h2>模块重导出</h2>
<p>ts中有以下用法:</p>
<pre><code class="ts">// a.ts
export interface A {
}</code></pre>
<pre><code class="ts">// index.ts
export * from './a';</code></pre>
<p>我们在使用的时候直接使用以下代码即可,方面封装</p>
<pre><code class="ts">import {A} from './index'</code></pre>
<p>NestJs中的模块也有类似用法,比如我们定义了两个基本模块,这两个基本模块用的时候基本都是一起导入的,此时我们通过模块重导出将其封装到一个叫<code>CoreModule</code>,其他地方直接导入<code>CoreModule</code>即可。</p>
<pre><code class="ts">@Module({
providers: [CommonService],
exports: [CommonService]
})
export class CommonModule {}</code></pre>
<pre><code class="ts">@Module({
providers: [Util],
exports: [Util]
})
export class UtilModule {}</code></pre>
<pre><code class="ts">@Module({
imports: [CommonModule, UtilModule],
exports: [CommonModule, UtilModule]
})
export class CoreModule {}</code></pre>
<h2>模块初始化与依赖注入</h2>
<p>如果需要在模块实例化的时候运行一些逻辑,而且该逻辑有外部依赖的时候,可以通过以下方式处理</p>
<pre><code class="ts">import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
controllers: [UserController],
providers: [UserService],
})
export class catsModule {
constructor(private readonly userService: UserService) { // 没有@Inject
// 调用userService
}
}</code></pre>
<h2>全局模块</h2>
<p>上面定义的模块都是需要手动<code>imports</code>进来的,如果有些模块是使用率很高的,比如工具模块,此时可以声明为全局模块。</p>
<p>使用<code>@Global()</code>即可声明全局模块。</p>
<pre><code class="ts">import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Global()
@Module({
controllers: [UserController],
providers: [UserService],
})
export class catsModule {
}</code></pre>
<h2>动态模块</h2>
<p>上面定义的都是静态模块,如果我们需要动态声明我们的模块,比如数据库模块,连接成功我才返回模块,此时需要使用动态模块来处理。</p>
<p>使用<code>模块名.forRoot()</code>方法来返回模块定义,通过该方式定义的即为动态模块。</p>
<pre><code class="ts">@Module({
providers: [DatabaseProvider]
})
export class DatabaseModule {
static async forRoot(env: string) {
const provider = createDatabaseProvider(env); // 根据环境变量连接不同的数据库
return {
module: DatabaseModule,
providers: [provider],
exports: [provider]
}
}
}</code></pre>
<pre><code class="ts">// user.module.ts
@Module({
imports: [DatabaseModule.forRoot('production')]
})
export class UserModule {}</code></pre>
<h2>生产环境下的姿势</h2>
<p>上面有一个商城系统的模块例子,当我们的业务模块开发完毕之后,需要将其注册到AppModule,这样才能生效,这个也有个好处,有点像插拔的例子,当需要下掉一个业务时,业务代码不动,在AppModule取消注册即可。</p>
<pre><code class="ts">@Module({
imports:[UserModule,GoodsModule,OrderModule,PayModule]
})
export class AppModule {}</code></pre>
<h2>结尾</h2>
<p>模块系统是NestJs另一个重要的特性,个人认为是基于DDD思想的,每个模块就是一个单独的领域业务,可以由一个小组去独立开发。多个模块时可以同时开发,如果有依赖问题的话,可以先把模块和响应的interface公开出去,别人正常调用你的interface,当实现类开发完毕之后NestJs会自动注入该实现类,调用方的代码不用更改。</p>
<p>如果您觉得有所收获,分享给更多需要的朋友,谢谢!</p>
<p>如果您想交流关于NestJs更多的知识,欢迎加群讨论!</p>
<p><img src="/img/remote/1460000020138769?w=200&h=200" alt="image" title="image"></p>
NestJs学习之旅(3)——服务提供者
https://segmentfault.com/a/1190000020138766
2019-08-21T11:58:57+08:00
2019-08-21T11:58:57+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
5
<p>欢迎持续关注<code>NestJs之旅</code>系列文章<br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<h2>简介</h2>
<p>服务提供者是NestJs一个非常重要的概念,一般来说,被装饰器<code>@Injectable()</code>修饰的类都可以视为服务提供者。服务提供者一般包含以下几种:</p>
<ul>
<li>Services(业务逻辑)</li>
<li>Factory(用来创建提供者)</li>
<li>Repository(数据库访问使用)</li>
<li>Utils(工具函数)</li>
</ul>
<h2>使用</h2>
<p>下文中将以Services来说明服务提供者的具体使用。</p>
<p>典型的MVC架构中其实有一个问题,业务逻辑到底放哪里?</p>
<ul>
<li>放在控制器,代码复用成了问题,不可能去New一个控制器然后调用方法,控制器方法都是根据路由地址绑定的</li>
<li>放在Model,导致Model层臃肿,Model应该是直接和数据库打交道的,业务逻辑跟数据库的关系并不是强制绑定的,只有业务逻辑涉及到数据查询/存储才会使用到Model层</li>
</ul>
<p>现阶段比较流行的架构是多添加一个Services层来写业务逻辑,分离Model层不应该做的事情。</p>
<pre><code class="typescript">// 业务类 user.service.ts
@Injectable()
export class UserServices {
private readonly users: User[] = [];
create(user: User) {
this.users.push(user);
}
findAll(): User[] {
return this.users;
}
}</code></pre>
<pre><code class="ts">// 用户控制器
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {} // 注入UserService
@Post()
async create(@Body() createUserDTO:CreateUserDTO) {
this.userService.create(createUserDTO);
}
@Get()
async findAll() {
return this.userService.findAll();
}
}</code></pre>
<h2>服务提供者的Scope</h2>
<p>SpringBoot中提供了Scope注解来指明Bean的作用域,NestJs也提供了类似的<code>@Scope()</code>装饰器:</p>
<table>
<thead><tr>
<th align="left">scope名称</th>
<th align="left">说明</th>
</tr></thead>
<tbody>
<tr>
<td align="left">SINGLETON</td>
<td align="left">单例模式,整个应用内只存在一份实例</td>
</tr>
<tr>
<td align="left">REQUEST</td>
<td align="left">每个请求初始化一次</td>
</tr>
<tr>
<td align="left">TRANSIENT</td>
<td align="left">每次注入都会实例化</td>
</tr>
</tbody>
</table>
<pre><code class="ts">@Injectable({scope: Scope.REQUEST})
export class UserService {
}</code></pre>
<h2>可选的依赖项</h2>
<p>默认情况下,如果依赖注入的对象不存在会提示错误,中断应用运行,此时可以使用<code>@Optional()</code>来指明选择性注入,但依赖注入的对象不存在时不会发生错误。</p>
<pre><code class="ts">@Controller('users')
export class UserController {
constructor(@Optional() private readonly userService:UserService){}
}</code></pre>
<h2>基于属性的注入</h2>
<p>上文中的注入都是基于构造函数的,这样做有一个缺陷,如果涉及到继承的话,子类必须显示调用<code>super</code>来实例化父类。如果父类的构造函数参数过多的话反而成了子类的负担。</p>
<p>针对这个问题,NestJs建议的方式是<code>基于属性</code>进行注入。</p>
<pre><code class="ts">@Controller('users')
export class UserController {
@Inject()
private readonly userService:UserService;
}</code></pre>
<h2>服务提供者注册</h2>
<p>只有被注册过的服务提供者才能被NestJs进行自动注入。</p>
<pre><code class="ts">@Module({
controllers:[UserController], // 注册控制器
providers: [UserServices], // 注册服务提供者,可以是services,factory等等
})
export class UserModule {
}</code></pre>
<h2>自定义服务提供者</h2>
<h3>使用值</h3>
<p>上文中提供的Services一般用在编写业务逻辑,结构基本是固定的,如果需要集成其他库作为注入对象的话,需要使用的自定义的服务提供者。</p>
<p>比如我们使用sequelize创建了数据库连接,想把他注入到我们的Services中进行数据库操作。可以使用以下方式进行处理:</p>
<pre><code class="ts">// sequelize.ts 数据库访问
export const sequelize = new Sequelize({
///
});</code></pre>
<pre><code class="ts">// sequelize.provider.ts
import {sequelize} from './sequelize';
export const sequelizeProvider = {
provide: 'SEQUELIZE', // 服务提供者标识
useValue: sequelize, // 直接使用值
}</code></pre>
<pre><code class="ts">// user.module.ts
@Module({
providers:[UserService, sequelizeProvider]
})
export class UserModule {}</code></pre>
<pre><code class="ts">// user.service.ts
@Injectable()
export class UserService {
constructor(@Inject('SEQUELIZE') private readonly sequelize: Sequelize) {}
}</code></pre>
<h3>使用类</h3>
<p>OOP的一个重要思想就是<code>面向接口化</code>设计,比如我们开发了一个日志接口,有写入本地文件的实现,也有写入syslog的实现。依赖注入到时候我们希望使用接口进行注入,而不是具体的实现。</p>
<pre><code class="ts">// logger.ts
export interface Logger {
log(log:string);
}</code></pre>
<pre><code class="ts">// file.logger.ts
export class FileLogger implements Logger {
log(log:string) {
// 写入本地文件
}
}</code></pre>
<pre><code class="ts">// syslog.logger.ts
export class SyslogLogger implements Logger {
log(log:string) {
// 写入Syslog
}
}</code></pre>
<pre><code class="ts">// logger.provider.ts
export const loggerProvider = {
provide: Logger, // 使用接口标识
useClass: process.env.NODE_ENV==='development'?FileLogger:SyslogLogger, // 开发日志写入本地,生产日志写入syslog
}</code></pre>
<pre><code class="ts">// user.module.ts
@Module({
providers:[UserService,loggerProvider]
})
export class UserModule {
}</code></pre>
<pre><code class="ts">// user.service.ts
@Injectable()
export class UserService {
constructor(@Inject(Logger) private readonly logger: Logger) {}
}</code></pre>
<h3>使用工厂</h3>
<p>工厂模式相信大家都不陌生,工厂模式本质上是一个函数或者方法,返回我们需要的产品。</p>
<p>传统的第三方库都是提供callback形式或者事件形式的进行连接,比如redis,如果需要使用该类型的注入对象,工厂模式是最佳方式。</p>
<p>以下是使用工厂模式创建数据库连接的例子:</p>
<pre><code class="ts">// database.provider.ts
export const databaseProvider = {
provide:'DATABASE',
useFactory: async(optionsProvider: OptionsProvider) { // 使用依赖,注入顺序和下面定义的顺序一致
return new Promise((resolve, reject) => {
const connection = createConnection(optionsProvider.getDatabaseConfig())
connection.on('ready',()=>resolve(connection));
connection.on('error',(e)=>reject(e));
});
},
inject:[OptionsProvider], // 注入依赖
}</code></pre>
<pre><code class="ts">// user.module.ts
@Module({
providers:[OptionsProvider, databaseProvider]
})
export class UserModule {
}</code></pre>
<pre><code class="ts">// user.service.ts
@Injectable()
export class UserService {
constructor(@Inject('DATABASE') private readonly connection: Connection) {}
}</code></pre>
<h3>别名方式</h3>
<p>别名方式可以基于现有的提供者进行创建。</p>
<pre><code class="ts">const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: Logger,
};</code></pre>
<h2>导出服务提供者到其他模块</h2>
<p>模块的详细知识将在后文提到,但是有一点需要提前知道,<code>只有被模块导出的服务提供者才能被其他模块导入</code></p>
<h3>基于类型的导出</h3>
<p>上文中的<code>UserService</code>是基于类型而不是进入名称进行注入的。</p>
<pre><code class="ts">@Module({
providers: [UserService],
exports: [UserService], // 重要
})
export class UserModule {}</code></pre>
<h3>基于名称的导出</h3>
<p>上文中<code>DATABASE</code>和<code>SEQUELIZE</code>这种服务提供者都是自定义的,而且指定的标识符。</p>
<pre><code class="ts">@Module({
providers: [sequelizeProvider],
exports: ['SEQUELIZE'], // 其他模块的组件直接使用@Inject('SEQUELIZE')即可
})</code></pre>
<h2>结尾</h2>
<p>服务提供者是NestJs的精华之一,提供了几种方式方便我们在各种环境下的服务提供者创建。</p>
<p>如果您觉得有所收获,分享给更多需要的朋友,谢谢!</p>
<p>如果您想交流关于NestJs更多的知识,欢迎加群讨论!</p>
<p><img src="/img/remote/1460000020138769" alt="image" title="image"></p>
NestJs学习之旅(2)——控制器
https://segmentfault.com/a/1190000020127817
2019-08-20T14:33:18+08:00
2019-08-20T14:33:18+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
4
<p>欢迎持续关注<code>NestJs之旅</code>系列文章<br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<h2>MVC</h2>
<p>说到控制器就不得不说经典的MVC架构。</p>
<blockquote>
<p>MVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。</p>
<ul>
<li>控制器(Controller)- 负责转发请求,对请求进行处理,处理完毕后输出响应。</li>
<li>视图 (View) - 界面设计人员进行图形界面设计</li>
<li>模型 (Model)- 数据库查询和业务逻辑</li>
</ul>
</blockquote>
<p>可以看到控制器起着承上启下的作用,是Web开发中必备的一环,视图和模型倒不是必须的,理由如下:</p>
<ol>
<li>API项目直接输出JSON数据,无需渲染页面</li>
<li>无数据库或者复杂业务逻辑的项目时可以把请求处理直接在控制器完成</li>
</ol>
<h2>路由</h2>
<p>控制器的目的是接收应用程序的特定请求。基于路由机制来实现请求的分发。通常,每个控制器具有多个路由,并且不同的路由可以执行不同的动作。</p>
<p>为了创建一个基本的控制器,我们使用类和装饰器。装饰器将类与所需的元数据相关联,并使Nest能够创建路由映射(将请求绑定到相应的控制器)。</p>
<h2>控制器定义</h2>
<p>使用<code>@Controller</code>装饰器来定义控制器,传入一个可选的路由前缀可以将该控制器绑定到该前缀。</p>
<pre><code class="typescript">import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get('list')
findAll(): string {
return 'This action returns all cats';
}
@Get('show')
fineOne(): string {
return 'one cat';
}
@Get()
index():string {
return 'index';
}
}</code></pre>
<p>以上例程会生成以下路由:</p>
<ol>
<li>GET /cats/list CatsController::findAll方法处理</li>
<li>GET /cats/show CatsController::show方法处理</li>
<li>GET /cats CatsController::index方法处理</li>
</ol>
<p>上述例程使用的是<code>@Get</code>装饰器,所以只能处理<code>GET</code>请求,以下是支持的请求方法与对应的装饰器</p>
<table>
<thead><tr>
<th align="left">请求方法</th>
<th align="left">装饰器名称</th>
<th align="left">说明</th>
</tr></thead>
<tbody>
<tr>
<td align="left">GET</td>
<td align="left">@Get</td>
<td align="left">匹配GET请求</td>
</tr>
<tr>
<td align="left">POST</td>
<td align="left">@Post</td>
<td align="left">匹配POST请求</td>
</tr>
<tr>
<td align="left">PUT</td>
<td align="left">@Put</td>
<td align="left">匹配PUT请求</td>
</tr>
<tr>
<td align="left">HEAD</td>
<td align="left">@Head</td>
<td align="left">匹配HEAD请求</td>
</tr>
<tr>
<td align="left">DELETE</td>
<td align="left">@Delete</td>
<td align="left">匹配DELETE请求</td>
</tr>
<tr>
<td align="left">OPTIONS</td>
<td align="left">@Options</td>
<td align="left">匹配OPTIONS请求</td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">@All</td>
<td align="left">匹配所有请求方法</td>
</tr>
</tbody>
</table>
<h2>动态路由</h2>
<p>上文中的路由方法接收的参数是固定的,所以只能匹配固定的请求,如果路由地址是动态变化的(<code>路由地址指请求的path,不包括QueryString</code>),则上述路由定义方式无法正常工作。</p>
<p>NestJs支持基于路径的路由定义,使用如下:</p>
<pre><code class="typescript">import {@Controller, Get} from '@nestjs/common';
@Controller('cats')
class CatsController {
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
}</code></pre>
<p>当请求<code>/cats/猫ID</code>这种动态路由时(<code>因为猫ID是path的一部分,所以path是变化的</code>),<code>params.id</code>就是<code>猫ID</code>,做过Vue或者React开发的读者应该熟悉以下写法:</p>
<p>路由定义 <code>/user/:userId/orders/:orderId</code><br>页面地址 <code>/user/1/orders/2</code></p>
<p>访问以上页面将产生以下参数:</p>
<ul>
<li>userId => 1</li>
<li>orderId => 2</li>
</ul>
<p>NestJs在这方面是一致的。</p>
<h2>请求参数</h2>
<p>上述例子中,我们使用<code>@Params</code>读取了请求路径上的动态参数。NestJs还支持以下的装饰器来获取不同的请求参数</p>
<table>
<thead><tr>
<th align="left">装饰器名称</th>
<th align="left">底层对象</th>
<th align="left">说明</th>
</tr></thead>
<tbody>
<tr>
<td align="left">@Request()</td>
<td align="left">req</td>
<td align="left">原始请求对象</td>
</tr>
<tr>
<td align="left">@Response()</td>
<td align="left">res</td>
<td align="left">原始响应对象</td>
</tr>
<tr>
<td align="left">@Param(key?:string)</td>
<td align="left">req.params或req.params[key]</td>
<td align="left">路径参数</td>
</tr>
<tr>
<td align="left">@Body(key?:string)</td>
<td align="left">req.body或req.body[key]</td>
<td align="left">请求体,支持表单或JSON</td>
</tr>
<tr>
<td align="left">@Query(key?:string)</td>
<td align="left">req.query或req.query[key]</td>
<td align="left">请求链接的查询字符串</td>
</tr>
<tr>
<td align="left">@Headers(name?:string)</td>
<td align="left">req.headers或req.headers[key]</td>
<td align="left">请求头</td>
</tr>
</tbody>
</table>
<h2>请求体</h2>
<p>在POST/PUT/PATCH请求中,会包含请求体,NestJs通过<code>@Body</code>装饰器可以自动获取该数据。比如如下代码:</p>
<pre><code class="ts">@Controller('user')
export class AppController {
constructor(private readonly appService: AppService) {}
@Post()
findAll(@Body() data: any) {
return data;
}
}</code></pre>
<p>以上例程会原样输出请求内容。</p>
<h3>请求体绑定</h3>
<p>SpringBoot中<code>@RequestBody</code>注解可以直接绑定到给定的POJO对象实现请求参数自动注入,在NestJs中,该特性也得到了支持。</p>
<p>定义DTO对象</p>
<pre><code class="ts">export class UserLoginDTO {
readonly username: string;
readonly password: string;
}</code></pre>
<p>定义控制器</p>
<pre><code class="ts">@Controller('users')
class UserController() {
@Post('login')
login(@Body() userLoginDTO: UserLoginDTO) {
console.log(userLoginDTO.username, userLoginDTO.password);
}
}</code></pre>
<p>可以看到与SpringBoot的开发体验几乎一致。</p>
<h2>响应头</h2>
<p>如果需要输出响应头,可以使用<code>@Header(name:string,value:string)</code>装饰器来进行处理。</p>
<p><code>请注意:响应头使用@Header()装饰器,请求头使用@Headers()装饰器,末尾有个s的区别!</code></p>
<pre><code class="ts">@Controller('users')
class UserController {
@Head(':id')
@Header('x-version', '1.0.0')
function head(@Param('id') id:number) {
}</code></pre>
<h2>响应状态码</h2>
<p>从响应体的设计可以发现一个问题,由于不推荐直接操纵<code>response</code>对象,如果需要输出响应状态码怎么办?NestJs也为我们提供了解决方案。</p>
<p>使用<code>@HttpCode(statusCode:number)</code>装饰器可以设定响应状态码。</p>
<p>在Restful API设计中,DELETE请求应当返回<code>204 No Content</code>状态码,如下代码所示:</p>
<pre><code class="ts">@Controller('users')
class UserController {
@DELETE(":id")
@HttpCode(204)
delete(@Param('id') id:number) {
// 删除成功不需要返回数据
}
}</code></pre>
<h2>响应体</h2>
<p>在express或者开发中,响应内容都是我们手动赋值或者输出的,但是在NestJs,可以直接根据路由函数的返回值<code>类型</code>自动识别响应体类型。NestJs支持以下格式的响应:</p>
<table>
<thead><tr>
<th align="left">TS类型</th>
<th align="left">响应类型</th>
<th align="left">响应格式</th>
</tr></thead>
<tbody>
<tr>
<td align="left">string</td>
<td align="left">字符串</td>
<td align="left">text/html</td>
</tr>
<tr>
<td align="left">object</td>
<td align="left">JSON</td>
<td align="left">application/json</td>
</tr>
<tr>
<td align="left">array</td>
<td align="left">JSON</td>
<td align="left">application/json</td>
</tr>
<tr>
<td align="left">null</td>
<td align="left">无(响应体长度为0)</td>
<td align="left">无</td>
</tr>
<tr>
<td align="left">undefined</td>
<td align="left">无(响应体长度为0)</td>
<td align="left">无</td>
</tr>
<tr>
<td align="left">Promise<*></td>
<td align="left">根据Promise返回的结果类型确定(规则如上)</td>
<td align="left">-</td>
</tr>
</tbody>
</table>
<h2>异步路由函数</h2>
<p>在前面的例子中,我们所有的路由处理函数都是同步的,但是在实际开发中基本不可能,一旦涉及到数据库访问、缓存访问就会存在IO,有IO就会有异步。</p>
<p>NestJs天生完美支持异步,有以下两种方法进行异步编程:</p>
<h3>Promise</h3>
<pre><code class="ts">@Get()
async findAll(): Promise<any[]> {
return Promise.resolve([]);
}</code></pre>
<h3>RxJs</h3>
<p>RxJs中提供了<code>Observable</code>对象,NestJs可以自动订阅并获取最后一次产生的值。</p>
<pre><code class="ts">@Get()
findAll: Observable<any[]> {
return of([]); // of为RxJs操作符
}</code></pre>
<h2>实现一个Restful API</h2>
<p>以下是基于Restful API规范开发的API,本文的主要内容为控制器,所以DTO对象的创建省略。</p>
<pre><code class="ts">import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';
@Controller('users')
export class UsersController {
// 创建用户,POST请求会自动返回201状态码,响应体为空
@Post()
create(@Body() dto: CreateUserDto) {
}
// 用户列表
@Get()
findAll() {
return [
{id:1,username:'a',password:'a'},
{id:2,username:'b',password:'b'}
];
}
// 查看用户
@Get(':id')
findOne(@Param('id') id: number) {
return {id,username:'mock username', password: 'mock password'};
}
// 更新用户,需要返回编辑后的用户资源
@Put(':id')
update(@Param('id') id: number, @Body() updateUserDto: UpdateUserDto) {
return {id,username:'updated username',password: 'updated password'};
}
// 删除用户,返回204状态码
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id: number) {
}
}</code></pre>
<h2>结尾</h2>
<p>如果您觉得有所收获,请点击右下角在看或者转发朋友圈,分享给更多需要的朋友,谢谢!<br>如果您想交流关于NestJs更多的知识,欢迎加群讨论!<br><img src="/img/bVbwCkg?w=200&h=200" alt="图片描述" title="图片描述"></p>
NestJs学习之旅(1)——快速开始
https://segmentfault.com/a/1190000020114989
2019-08-19T11:46:33+08:00
2019-08-19T11:46:33+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
6
<p>经过<a href="https://link.segmentfault.com/?enc=o9yqEL5r7yv%2FRbhSfGT9FQ%3D%3D.HH3J7HJXstBAeklcz3bW4xHqAn%2F949Aco9zQfdqrQlP4VzzexN6Rk0gBNUvgUpnfEP46kiXCgv0j8Qp9AAzmGg%3D%3D" rel="nofollow">NodeJs系列课程</a>和<a href="https://link.segmentfault.com/?enc=kcLY5FFjII9NahPXvLvzpg%3D%3D.2U98bi%2FWcEunLedk2gA7OB9Os1fFpyiljmSRpJihoyahq8NJrV0yRyS53L4ev9lGkxow3sJukBF8HEeFNFQ92Q%3D%3D" rel="nofollow">Typescript系列课程</a>,终于开始了激动人心的NestJs学习之旅。</p>
<p>欢迎持续关注<code>NestJs之旅</code>系列文章<br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<h2>介绍</h2>
<p>Nest(或NestJS)是一个用于构建高效,可扩展的Node.js服务器端应用程序的框架。它使用渐进式JavaScript,内置并完全支持TypeScript(但仍然允许开发人员使用纯JavaScript编写代码)并结合了OOP(面向对象编程),FP(功能编程)和FRP(功能反应编程)的元素。</p>
<pre><code class="javascript">import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}</code></pre>
<p>熟悉Java的同学应该有似曾相识的感觉,SpringBoot中大量使用注解来简化开发。现在,使用基于ES6装饰器构建的NestJs框架,你也可以做到!</p>
<h2>优缺点</h2>
<p>先说说优点吧:</p>
<ul>
<li>完美支持Typescript,因此可以使用日益繁荣的TS生态资源</li>
<li>兼容express中间件,降低造轮子成本</li>
<li>完美支持响应式编程框架rxjs</li>
<li>完美支持依赖注入</li>
<li>模块化思想,方便开发以及后期维护</li>
<li>使用装饰器简化开发,减少样板代码</li>
<li>组件化设计,解决Node.js无全栈框架约束的现存问题</li>
</ul>
<p>当然,"缺点"也是有点的,不过熟练之后这些都不是缺点:</p>
<ul>
<li>基于TS导致的语言门槛</li>
<li>代码设计上对模块化/组件化思想有一定要求</li>
</ul>
<h2>第一个NestJs应用</h2>
<p>使用NestJs的命令行工具,可以简化项目的创建以及项目文件的创建。</p>
<ol>
<li>
<code>npm install -g @nestjs/cli</code>安装命令行工具</li>
<li>
<code>nest new 项目名称</code>初始化项目</li>
</ol>
<p>初始化完毕后可以看到一个完整的项目结果,目录如下(忽略node_modules):</p>
<pre><code class="text">├── README.md 自述文件
├── nest-cli.json NestJs项目配置
├── package.json npm文件
├── src 项目源码
│ ├── app.controller.spec.ts 控制器测试文件
│ ├── app.controller.ts 控制器类
│ ├── app.module.ts 模块类
│ ├── app.service.ts 服务类
│ └── main.ts 项目入口文件
├── test 测试目录
│ ├── app.e2e-spec.ts 应用e2e测试
│ └── jest-e2e.json jest e2e测试配置
├── tsconfig.build.json 生产环境Typescript所用
├── tsconfig.json 开发环境Typescript配置
├── tslint.json tslint配置
└── yarn.lock yarn锁文件</code></pre>
<p>NestJs有几大类文件是主要的是下面几种,其他类型的文件在后续课程会讲解;</p>
<ul>
<li>module 模块声明(这是NestJs的一个亮点,有点DDD的思想)</li>
<li>controller 控制器(负责接收数据,返回响应)</li>
<li>service 服务(主要业务逻辑)</li>
</ul>
<p>使用<code>npm run start</code>来运行项目。终端输出如下:</p>
<pre><code class="text">[Nest] 2986 - 08/19/2019, 10:29 AM [NestFactory] Starting Nest application...
[Nest] 2986 - 08/19/2019, 10:29 AM [InstanceLoader] AppModule dependencies initialized +22ms
[Nest] 2986 - 08/19/2019, 10:29 AM [RoutesResolver] AppController {/}: +12ms
[Nest] 2986 - 08/19/2019, 10:29 AM [RouterExplorer] Mapped {/, GET} route +9ms
[Nest] 2986 - 08/19/2019, 10:29 AM [NestApplication] Nest application successfully started +6ms</code></pre>
<p>一般来说,看到<code>successfully</code>就可以认为启动成功了。启动失败的话可以根据错误提示进行处理,比较多的情况可能是端口占用导致的错误。</p>
<p>打开浏览器访问<code>http://localhost:3000</code>即可看到输出<code>Hello World!</code>。</p>
<h2>To Be Continued</h2>
<p>下一期将介绍Controller,欢迎持续关注!</p>
TS简明教程(4)——装饰器
https://segmentfault.com/a/1190000020006268
2019-08-08T11:04:35+08:00
2019-08-08T11:04:35+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
5
<p>为了后续内容(如<code>nestjs</code>等框架)的开展,本文更新TS相关的基础知识。</p>
<p>关注获取更多<code>TS精品文章</code><br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<p>本文讲解装饰器</p>
<h2>装饰器</h2>
<p>装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。 装饰器使用<code>@expression</code>这种形式,expression必须是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。</p>
<blockquote>Typescript中的装饰器是一项实验性功能,需要在tsconfig.json中开启该特性</blockquote>
<pre><code class="json">{
"compilerOptions": {
"experimentalDecorators": true
}
}</code></pre>
<p>例如,有一个<code>@sealed</code>装饰器,我们这样定义<code>sealed</code>:</p>
<pre><code class="typescript">function sealed(target: any) {
// 操作被装饰对象
}</code></pre>
<h2>装饰器工厂</h2>
<p>如果需要给装饰器添加一些动态行为,比如开发一个监控统计的装饰器,需要传入当前统计的事件名称,有多个事件名称时只需要变更传入的事件名而不用重复定义装饰器。</p>
<p>这时候需要使用到装饰器工厂。装饰器工厂也是一个函数,只不过它的返回值是一个装饰器。例如如下的事件监控装饰器:</p>
<pre><code class="typescript">function event(eventName: string) {
return function(target: any) {
// 获取到当前eventName和被装饰对象进行操作
}
}</code></pre>
<h2>装饰器组合</h2>
<p>多个装饰器可以同时应用到被装饰对象上,例如下面的例子:</p>
<pre><code class="typescript">@sealed
@test('test')
class Demo {
}</code></pre>
<p>装饰器执行顺序:</p>
<ol>
<li>装饰器工厂需要先求值,再装饰,求值顺序是由上到下</li>
<li>装饰器可以直接求值,装饰顺序是由下到上</li>
</ol>
<p>上面的说明可以难以理解,下面举一个实际的例子:</p>
<pre><code class="typescript">function f() {
console.log('f求值');
return function(target: any) {
console.log('f装饰');
}
}
function g() {
console.log('g求值');
return function(target: any) {
console.log('g装饰');
}
}
@f()
@g()
class Demo {
}</code></pre>
<p>上例的执行顺序为</p>
<pre><code class="text">f求值
g求值
g装饰
f装饰</code></pre>
<p>因为先求值,所以在上面的f会比g先求值。因为装饰器是由下到上装饰,所以求值后的g比f先执行。</p>
<h2>装饰器类型</h2>
<p>根据被装饰的对象不同,装饰器分为以下几类:</p>
<ol>
<li>类装饰器</li>
<li>方法装饰器</li>
<li>属性装饰器</li>
<li>函数参数装饰器</li>
</ol>
<h2>类装饰器</h2>
<p>类装饰器在定义类的地方。类装饰器可以监视、修改或替换类定义。类的构造函数将作为唯一参数传递给装饰器。如果类装饰器返回一个值,它会使用返回的构造函数替换原来的类声明。</p>
<pre><code class="typescript">function sealed(target: Function) {
Object.seal(target);
Object.seal(target.prototype);
}
@sealed
class Demo {}
</code></pre>
<p>下面来一个替换构造函数的示例:</p>
<pre><code class="typescript">function replace<T extends {new(...args: any[]):{}}>(target: T) {
return class extends target {
newname = "newName";
age = 18
}
}
@replace
class Demo {
oldname = "oldname";
constructor(oldname: string) {
this.oldname = oldname;
}
}
console.log(new Demo("oldname"));</code></pre>
<p>以上例程会输出</p>
<pre><code class="text">class_1 { oldname: 'oldname', newname: 'newName', age: 18 }</code></pre>
<p>可以看到通过装饰器新增的newname和age属性已经成功注入了。</p>
<h2>方法装饰器</h2>
<p>方法装饰器用来装饰类的方法(静态方法和实例方法都可以)。方法装饰器可以监视、修改或替换方法定义。<br>方法装饰器接收3个参数:</p>
<ol>
<li>类的原型对象,如果是静态方法则为类的构造函数</li>
<li>方法名称</li>
<li>方法的属性描述符</li>
</ol>
<p>下面是一个<code>修改</code>方法行为的装饰器:</p>
<pre><code class="typescript">function hack(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const oldFunction = target[propertyKey]; // 获取方法引用
const newFunction = function(...args: any[]) {
console.log('call function ', propertyKey);
oldFunction.call(target, ...args);
}
descriptor.value = newFunction; // 替换原声明
}
class Demo {
@hack
demo() {
console.log('call demo');
}
}
const demo = new Demo();
demo.demo();</code></pre>
<p>以上例程输出如下:</p>
<pre><code class="text">call function demo
call demo</code></pre>
<h2>属性装饰器</h2>
<p>属性装饰器用来装饰类的成员属性。属性装饰器接收两个参数:</p>
<ol>
<li>类的原型对象,如果是静态方法则为类的构造函数</li>
<li>属性名</li>
</ol>
<pre><code class="typescript">function demo(value: string) {
return function(target: any, propertyKey: string) {
target[propertyKey] = value;
}
}
class Demo {
@demo('haha') name?: string;
}
const d = new Demo();
console.log(d.name);</code></pre>
<p>属性装饰器多用在属性依赖注入上面</p>
<h2>函数参数装饰器</h2>
<p>参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:</p>
<ol>
<li>对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。</li>
<li>参数的名字。</li>
<li>参数在函数参数列表中的索引。</li>
</ol>
<pre><code class="typescript">function PathParam(paramDesc: string) {
return function (target: any, paramName: string, paramIndex: number) {
!target.$meta && (target.$meta = {});
target.$meta[paramIndex] = paramDesc;
}
}
class Demo {
constructor() { }
getUser( @PathParam("userId") userId: string) { }
}
console.log((<any>Demo).prototype.$meta);</code></pre>
<p>以上例程输出</p>
<pre><code class="text">{ '0': 'userId' }</code></pre>
<p>函数参数装饰器可以用在开发Web框架时自动注入请求参数。</p>
<h2>结语</h2>
<p>装饰器的介绍到这里就暂时结束了,装饰器的存在让Typescript有了与Java和C#等语言的注解相同的功能。当然,基于装饰器能做的工作是相当多的,注明的Angular2就大量使用了装饰器来分离业务逻辑。<br>对装饰器有想法的小伙伴可以扫码加我进行交流<br><img src="/img/remote/1460000019883199" alt="微信" title="微信"></p>
TS简明教程(3)
https://segmentfault.com/a/1190000019917223
2019-07-30T12:56:05+08:00
2019-07-30T12:56:05+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
0
<p>为了后续内容(如<code>nestjs</code>等框架)的开展,本文更新TS相关的基础知识。</p>
<p>关注获取更多<code>TS精品文章</code><br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<p>本文讲解泛型</p>
<h2>泛型</h2>
<blockquote>泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。</blockquote>
<p>泛型的出现有效的降低了代码重复率,同时也能很好的保留类型信息,降低运行期崩溃的概率。</p>
<h2>HelloWorld</h2>
<p>假设有个函数,你给他啥类型,他就返回啥类型,代码如下:</p>
<pre><code class="typescript">function getValue(arg: number):number {
return arg;
}</code></pre>
<p>如果需要支持字符串的话,有以下做法:</p>
<ol>
<li>复制一份代码,然后更改<code>number</code>为<code>string</code>
</li>
<li>把<code>number</code>改为<code>any</code>
</li>
</ol>
<p>但是以上做法有弊端,方法1会导致代码重复比较多,而且难以扩展(只能通过复制代码来扩展);方法2的话会丢失变量类型信息,运行期可能会抛出异常。</p>
<p>因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。</p>
<pre><code class="typescript">function getValue<T>(arg: T): T {
return arg;
}</code></pre>
<p>调用</p>
<pre><code class="typescript">const n = getValue<number>(2);
const s = getValue<string>('s');</code></pre>
<p>说明,如果<code>arg:T</code>中<code>arg</code>是<code>可自动推导类型(一般不是any就能推导)</code>,那么<code><></code>之间的类型可以省略,如果<code><></code>指定了类型,但是<code>arg</code>类型不匹配的话,编译失败。</p>
<p><code>T</code>是随便取的,你叫ABCD都没人管你</p>
<h2>泛型函数</h2>
<p>原型如下:</p>
<pre><code class="typescript">function 函数名<泛型类型,有几个写几个,逗号分隔>(参数名: 参数类型,参数名:参数类型):返回值类型</code></pre>
<p>传统风格</p>
<pre><code class="typescript">function makeMap<K,V>(key: K, value: V):map<K,V> {
return map<K,V>(key,value);
}</code></pre>
<p>箭头函数风格</p>
<pre><code class="typescript">const makeMap: <K,V>(key:K,value:V) => map<K,V> = { // <K,V>(key:K,value:V) => map<K,V> 类型声明
return map<K,V>(key,value);
}</code></pre>
<h2>泛型接口</h2>
<p>原型如下:</p>
<pre><code class="typescript">interface 接口名称<泛型类型,有几个写几个,逗号分隔> {
// 使用泛型约束
}</code></pre>
<p>例子</p>
<pre><code class="typescript">interface GenericFunction<T> {
getValue(arg:T):T;
}
// 字符串类型
class Test implements GenericFunction<string> {
getValue(arg:string):string {
returna arg;
}
}
// 数字类型
class Test2 implements GenericFunction<number> {
getValue(arg:number):number {
returna arg;
}
}
const test = new Test();
console.log(test.getValue('111'));
const test2 = new Test2();
console.log(test.getValue(111));
</code></pre>
<h2>泛型类</h2>
<p>泛型类的使用和泛型接口差不多</p>
<pre><code class="typescript">class GenericClass<T> {
add(a: T, b: T):T;
}
const n = new GenericClass<number>();
console.log(n.add(1,1));
const s = new GenericClass<string>();
console.log(s.add('1','2'));</code></pre>
<h2>使用继承约束</h2>
<p>Java中经常看到如下代码</p>
<pre><code class="java">public class Generic<T extends Number>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}</code></pre>
<p>上例中,T只能为<code>Number</code>子类。避免过大范围的泛型导致问题</p>
<p>TS也可以使用以上方法:</p>
<pre><code class="typescript">class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // 编译OK
createInstance(Bee).keeper.hasMask; // 编辑OK</code></pre>
<p>以下代码可能难以理解</p>
<pre><code class="typescript">function createInstance<A extends Animal>(c: new () => A): A</code></pre>
<p>拆开来看:</p>
<ol>
<li><a> 泛型约束,A必须是Animal子类</a></li>
<li>
<code>new () => A</code> 箭头函数,约束了传入的值必须是构造方法</li>
<li>
<code>:A</code> <code>createInstance</code>必须返回传入的构造函数的实例</li>
</ol>
<h2>结语</h2>
<p>泛型有效减少了重复代码,同时也解决了类型强制转换的问题,在开发中要尽量使用泛型而不是<code>any</code>。<br>TS的泛型用法大部分都比这复杂,但是原理是一样的,不足之处,敬请包涵。<br>对TS有兴趣的小伙伴可以扫码加我进行交流<br><img src="/img/remote/1460000019883199" alt="微信" title="微信"></p>
TS简明教程(2)——类与接口
https://segmentfault.com/a/1190000019903347
2019-07-29T11:33:08+08:00
2019-07-29T11:33:08+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
0
<p>为了后续内容(如<code>nestjs</code>等框架)的开展,本文更新TS相关的基础知识。</p>
<p>关注获取更多<code>TS精品文章</code><br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<h2>类</h2>
<p>传统JS使用<code>函数</code>和<code>原型链</code>进行集成,在<code>ES6</code>出现了<code>class</code>关键,JS也能使用传统OOP的方式进行继承,但是还是存在一定的局限性,在TS中,OOP已经和传统语言差不多。</p>
<pre><code class="typescript">class Parent {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
say() {
return `name: ${this.name}, age: ${this.age}`;
}
}
const parent = new Parent();
parent.say();</code></pre>
<p>可以看到TS的OOP写法和Java还是有点类似的。但是他两的构造方法名不同,TS构造方法名为<code>constructor</code>,Java是<code>类名</code>。</p>
<h2>继承</h2>
<p>继承用来扩展现有的类,TS中这一点和传统语言一样使用<code>extends</code>语法。</p>
<pre><code class="typescript">class Parent {
name: string;
constructor(name: string) {
this.name = name;
}
say() {
console.log(`Parent say: ${this.name}`);
}
}
class Child extends Parent {
age: number;
constructor(name: string, age: number) { // 覆盖父类构造方法
super(name); // 调用父类构造方法
this.age = age;
}
say() {
console.log(`Child say: ${this.name} ${this.age}`);
}
}
const child: Parent = new Child("haha" ,1);
child.say(); // 输出 Child say haha 1</code></pre>
<ol>
<li>子类存在构造方法时,必须<code>显示调用</code>父类构造方法<code>先有父亲,后有儿子</code>
</li>
<li>TS方法调用是基于<code>值</code>而不是基于<code>类型声明</code>,比如<code>child</code>声明为<code>Parent</code>类型,但是值是子类型,所以调用方法时会调用<code>子类</code>的<code>say</code>
</li>
</ol>
<h2>访问限定符</h2>
<h3>public</h3>
<p>TS中方法和属性默认的访问限定符为<code>public</code>,所有外部或内部成员都可访问。</p>
<pre><code class="typescript">class Parent {
public name: string; // public可以不加
say() {
console.log(`say ${this.name}`);
}
}
const p = new Parent();
p.name = 'hello';
p.say(); // 输出 say hello</code></pre>
<h3>private</h3>
<p>私有访问,只能在<code>本类</code>访问,<code>子类和其他类都不行</code>。</p>
<pre><code class="typescript">class Parent {
private name: string;
private say() {
console.log(`say ${this.name}`);
}
}
const p = new Parent();
p.name = 'hello'; // 错误,private限定的属性不能被外部访问
p.say(); // 错误,private限定的访问不能被外部访问</code></pre>
<h3>protected</h3>
<p>保护性访问,只能<code>被本类或本类的子类(子类的子类也可以访问)</code>。</p>
<pre><code class="typescript">class Parent {
protected name: string;
constructor(name: string) {
this.name = name;
}
protected say() {
console.log(`say ${this.name}`);
}
}
class Child extends Parent {
public say() { // 提升访问性
console.log(`say ${this.name}`); // 访问父类属性
}
}
const c = new Child('hello');
c.say(); // 输出 say hello</code></pre>
<p>访问限定符只能提升,不能降低,如下例子是<code>无法通过编译的</code>:</p>
<pre><code class="typescript">class Parent {
protected name: string;
}
class Child extends Parent {
private name: string; // 错误,子类访问性必须>=父类的访问性
}</code></pre>
<h2>只读限定</h2>
<p>TS使用<code>readonly</code>声明只读<code>属性(方法不能使用)</code>,必须在<code>声明时</code>或者<code>构造时</code>进行赋值,<code>其他地方不能赋值</code></p>
<pre><code class="typescript">class Parent {
private readonly name = 'hello';
private readonly age: number;
constructor(age: number) {
this.age = age;
}
}</code></pre>
<h2>参数属性</h2>
<p>在上例中我们在构造方法中使用<code>this.age = age</code>对已存在的<code>私有只读属性age</code>进行了赋值。由于该操作时常用操作,所以TS有了更加便捷的写法:</p>
<pre><code class="typescript">class Parent {
constructor(readonly name: string, private readonly age: number) {
}
say() {
console.log(`say ${this.name} ${this.age}`);
}
}</code></pre>
<p>上例中声明了<code>公有只读的name属性,私有只读的age属性</code></p>
<h2>getter && setter</h2>
<p>在传统语言中,几乎不会直接声明公有属性,然后对其进行操作,都会先定义私有属性,然后提供<code>getter</code>和<code>setter</code>方法对其操作(<code>Java中很多类都是这种情况</code>)</p>
<pre><code class="typescript">class Parent {
private _name: string;
get name(): string {
return this._name;
}
set name(name: string) {
console.log(`name设置前: ${this._name} 设置后: ${name}`);
this._name = name;
}
}
const parent = new Parent();
parent.name = 'ok'; // 可以直接使用赋值语句,但是会自动调用set name(name: string)方法</code></pre>
<p>getter和setter方法提高了开发者对属性的控制,一起对属性的访问都是可控的,为以后的扩展性打下了基础(比如如果需要加缓存,我们可以在set时设置缓存,get时读取缓存,如果是直接操作属性的话,该功能实现起来很麻烦</p>
<h2>静态属性 && 静态方法</h2>
<p>以上讨论的都是<code>实例属性和梳理方法</code>,需要有实例才能调用,如果有些属性或方法并不是存在于实例上时可以使用静态方法或静态属性</p>
<pre><code class="typescript">class Parent {
static name: string;
static say() {
console.log(`name ${this.name}`); // 方法是静态,属性是静态时可以使用this
}
}
Parent.say();// 使用类名调用静态方法</code></pre>
<p>需要注意的是<code>实例可以直接调用静态,静态不能直接调用实例</code>,因为<code>实例需要实例化后调用</code></p>
<h2>抽象类</h2>
<p>传统语言中接口只包含实现,不包含细节。而抽象类可以包含细节。一般来说,有些公有方法可以放到抽象类做,不同的子类完成不同功能的代码可以放到抽象类做。</p>
<pre><code class="typescript">abstract class Animal {
abstract say(): void; // 声明抽象方法,子类必须实现
eat() {
console.log(`animal eat`);
}
}
class Human extends Animal { // 使用extends关键字
say() {
console.log('human say words');
}
}
class Dog extends Animal {
say() {
console.log('dog say wangwang');
}
}</code></pre>
<h2>接口</h2>
<p>接口用来限定子类的行为,不关心具体实现。与传统语言不同的是,TS接口还可以限定变量或常量的属性</p>
<p>限定子类行文:</p>
<pre><code class="typescript">interface Animal {
say(): void;
eat(): void;
}
class Human implements Animal {
say() {
console.log('human say');
}
eat() {
console.log('human eat');
}
}</code></pre>
<p>限定变量属性:</p>
<pre><code class="typescript">interface A {
name?: string;
age: number;
}
const obj: A = {
age: 10,
// name是可选的
};</code></pre>
<h3>可索引类型</h3>
<p>使用<code>可索引类型</code>来<code>描述</code>可以通过<code>索引访问得到</code>的类型。如<code>person["name"]</code>,<code>list[0]</code></p>
<pre><code class="typescript">interface HashMap {
[key: string]: any; // 冒号左边为属性名类型,右边为值类型
}
const map: HashMap = {};
map["name"] = "1";
map.a = "2";</code></pre>
<h3>接口继承</h3>
<p>与类继承类似,接口也可以通过继承来扩展现有的功能:</p>
<pre><code class="typescript">interface Animal {
eat(): void; // 动物会吃,但是怎么吃的不管
}
interface Human extends Animal {
say(): void; // 人会说话,但是怎么说,说什么不管
}</code></pre>
<h3>混合类型</h3>
<p>JS中,函数可以直接调用也可以通过对象方式调用,TS中可以通过接口声明被修饰的函数支持的调用方式:</p>
<pre><code class="typescript">interface Counter {
(start: number): string;
step: number;
reset(): void;
}
function getCounter(): Counter {
const counter = <Counter> function(start: number) {};
counter.step = 1;
counter.reset = function() {};
}
const c = getCounter();
c(1);
c.reset();
c.step = 2;</code></pre>
<h2>结语</h2>
<p>面向对象中的类和接口内容实在是太多了,本文只选择了开发中常用到的用法进行说明,不足之处,敬请包涵。<br>对TS有兴趣的小伙伴可以扫码加我进行交流<br><img src="/img/remote/1460000019883199" alt="微信" title="微信"></p>
手把手从零开始小程序单元测试(附避坑指南以及源码跟踪)
https://segmentfault.com/a/1190000019894437
2019-07-27T20:43:11+08:00
2019-07-27T20:43:11+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
2
<p>单元测试是一个老生常谈的话题,基于Web/NodeJs环境的测试框架、测试教程数不胜数,也趋于成熟了。但是对于微信小程序的单元测试,目前还是处于起步状态,这两天在研究微信小程序的测试,也遇到了一些坑,在这里记录一下,希望给看到本文的小伙伴带来一点帮助,少走一些弯路。</p>
<p>本文内容有点多,但是干货满满,不明白的小伙伴可以关注公众号给我留言<br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<h2>demo地址</h2>
<p><a href="https://link.segmentfault.com/?enc=Obnw4W5xiSci4oTRoSivLg%3D%3D.tFahDc51XefiTNauBq9Zph2lV1wpcZbH7KlvHo10LlMYv9Q%2B5KmP%2F9qStf6hI6Rd9iVo8rTyyepj2pt5%2BFG8pA%3D%3D" rel="nofollow">https://github.com/xialeistudio/miniprogram-unit-test-demo</a></p>
<h2>关键依赖版本</h2>
<p>本文写作时相关依赖版本如下(版本不同,源码行数可能不同):</p>
<ol>
<li>miniprogram-simulate: 1.0.7</li>
<li>j-component: 1.1.6</li>
<li>miniprogram-exparser: 0.0.6</li>
</ol>
<h2>测试流程</h2>
<ol>
<li>初始化小程序项目,编写待测试组件</li>
<li>安装jest,miniprogram-simulate测试环境</li>
<li>编写测试用例</li>
<li>执行测试</li>
</ol>
<h2>初始化小程序项目</h2>
<ol>
<li>使用小程序开发者工具初始化新项目,APPID选择<code>测试号</code>即可,语言选择<code>Javascript</code>。</li>
<li>使用小程序开发者工具新建<code>/components/user</code>组件</li>
<li>
<p><code>components/user.js</code></p>
<pre><code class="js">// components/user.js
Component({
data: {
nickname: ''
},
methods: {
handleUserInfo: function(e) {
this.setData({ nickname: e.detail.userInfo.nickName })
}
}
})</code></pre>
</li>
<li>
<p><code>components/user.wxml</code></p>
<pre><code class="xml"> <text class="nickname">{{nickname}}</text>
<button class="button" open-type="getUserInfo" bindgetuserinfo="handleUserInfo">Oauth</button></code></pre>
</li>
<li>
<p><code>pages/index/index.js</code></p>
<pre><code class="js">Page({
data:{}
})</code></pre>
</li>
<li>
<p><code>pages/index/index.wxml</code></p>
<pre><code class="xml"><view class="container">
<user></user>
</view></code></pre>
</li>
<li>打开小程序开发者工具,可以看到有一个<code>Oauth</code>按钮,点击之后会在上面显示昵称。</li>
<li>由此可以得到测试用例<code>点击授权按钮时上方显示为授权用户的昵称</code>
</li>
</ol>
<h2>安装jest/miniprogram-simulate测试环境</h2>
<ol>
<li>由于JS项目的小程序根目录没有<code>package.json</code>,需要手动生成一下</li>
<li>打开终端,在项目根目录执行<code>npm init -y</code>生成<code>package.json</code>
</li>
<li>安装测试工具集<code>npm install jest miniprogram-simulate --save-dev</code>
</li>
<li>
<p>编辑<code>package.json</code>,在<code>scripts</code>新建<code>test</code>命令</p>
<pre><code class="json">{
"name": "unit-test-demo",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^24.8.0",
"miniprogram-simulate": "^1.0.7"
}
}</code></pre>
</li>
</ol>
<h2>编写测试用例</h2>
<ol>
<li>在项目根目录新建<code>tests/components/user.spec.js</code>文件(目录需要手动创建)</li>
<li>
<p>代码如下(参考微信官方单元测试文档编写):</p>
<pre><code class="js">const simulate = require('miniprogram-simulate');
const path = require('path');
test('components/user', (done) => { // 定义测试名称,传入done表示当前测试是异步测试,需要回调函数来告诉jest,我测试执行完毕
const id = simulate.load(path.join(__dirname, '../../components/user')); // 加载组件
const component = simulate.render(id); // 渲染组件
const text = component.querySelector('.nickname'); // 获取nickname节点
const button = component.querySelector('.button'); // 获取button节点
button.dispatchEvent('getuserinfo', { // 模拟触发事件
detail: { // 传递事件参数
userInfo: {
nickName: 'hello',
},
},
});
setTimeout(() => { // 异步断言
expect(text.dom.innerHTML).toBe('hello'); // 检测text节点的innerHTML等于模拟授权获取的昵称
done();
}, 1000);
});</code></pre>
</li>
</ol>
<h2>执行测试</h2>
<ol>
<li>
<code>npm run test</code>,等待一秒后发现,<code>不出意外的话,测试肯定过不去</code>
</li>
<li>
<p>部分出错日志:</p>
<pre><code class="text">Expected: "hello"
Received: ""
at toBe (/Users/xialeistudio/WeChatProjects/unit-test-demo/tests/components/user.spec.js:18:32)
at Timeout.callback [as _onTimeout] (/Users/xialeistudio/WeChatProjects/unit-test-demo/node_modules/jsdom/lib/jsdom/browser/Window.js:678:19)
at listOnTimeout (internal/timers.js:535:17)
at processTimers (internal/timers.js:479:7)</code></pre>
</li>
<li>
<p>可以推测一下原因:</p>
<ol>
<li>dispatchEvent的事件触发有问题,导致handleUserInfo未触发[1]</li>
<li>dispatchEvent的事件触发成功,但是触发参数有问题[2]</li>
</ol>
</li>
</ol>
<h2>错误分析(源码跟踪过程)</h2>
<ol>
<li>
<p>针对第1点原因,可以写一下测试代码(<code>components/user.js</code>)</p>
<pre><code class="js"> Component({
data: {
nickname: ''
},
methods: {
handleUserInfo: function(e) {
console.log(e);
}
}
})</code></pre>
</li>
<li>
<p><code>npm run test</code>,可以看到事件还是成功触发了,不过<code>detail</code>是<code>{}</code></p>
<pre><code class="text"> console.log components/user.js:21
{ type: 'getuserinfo',
timeStamp: 948,
target: { id: '', offsetLeft: 0, offsetTop: 0, dataset: {} },
currentTarget: { id: '', offsetLeft: 0, offsetTop: 0, dataset: {} },
detail: {},
touches: {},
changedTouches: {} }</code></pre>
</li>
<li>原因1排除,查原因2</li>
<li>
<code>dispatchEvent</code>方法是<code>被测试组件的子组件</code>,<code>被测试组件</code>由<code>simulate.render</code>函数返回</li>
<li>浏览<code>node_modules/miniprogram-simulate/src/index.js</code>,看到<code>render函数(152行)</code>,可以看到返回的组件由<code>jComponent.create</code>提供</li>
<li>浏览<code>node_modules/j-component/src/index.js</code>的<code>create</code>函数,可以看到其返回了<code>RootComponent</code>实例,而<code>RootComponent</code>是由<code>./render/component.js</code>提供</li>
<li>浏览<code>node_modules/j-component/src/render/component.js</code>的<code>dispatchEvent</code>函数,在这里可以打下日志测试(本文就不打了,结果是这里的options就是<code>user.spec.js</code> <code>dispatchEvent</code>函数的<code>第二个参数</code>,<code>detail</code>是有值的)</li>
<li>
<p>继续跟踪源码,由于咱们的是<code>自定义事件</code>,所以会走到<code>91行</code>的代码,该代码块如下:</p>
<pre><code class="js">// 自定义事件
const customEvent = new CustomEvent(eventName, options);
// 模拟异步情况
setTimeout(() => {
dom.dispatchEvent(customEvent);
exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, {}, {
originalEvent: customEvent,
bubbles: true,
capturePhase: true,
composed: true,
extraFields: {
touches: options.touches || {},
changedTouches: options.changedTouches || {},
},
}));
}, 0);</code></pre>
</li>
<li>可以看到调用了<code>exparser.Event.dispatchEvent</code>函数,该函数的<code>第二个参数</code>调用了<code>exparser.Event.create</code>对自定义事件进行了包装,这里还没到最底层,需要继续跟踪</li>
<li>
<code>exparser</code>对象是<code>miniprogram-exparser模块</code>提供的,浏览<code>node_modules/miniprogram-exparser/exparser.min.js</code>,发现该文件被混淆了,不过没关系<code>混淆后的代码逻辑是不变的,只不过变量名变得无意义,可读性变差</code>
</li>
<li>使用webstorm格式化该文件,这里我传了一份格式化好的到github <a href="https://link.segmentfault.com/?enc=V6LtRsxoklfln1oeAJHJ4g%3D%3D.BOUIrAF6JDj4P3Au%2FhqSHWZzS3jjY0IAFb0%2BBlBLE6ERFU9RvPTPq7ek2AUT70bLBGvbN4JvMH8LsnctPXwy%2F82RteGemxaAg03D9eSnqDXv3dGLx%2FWw%2BWEHg8%2BC9JI9" rel="nofollow">wxparser.js,可在线观看</a>
</li>
<li>
<p>需要在源码中搜索<code>三个参数</code>的<code>create</code>函数(<code>Object.create不算</code>),需要有耐心,经过排查后发现<a href="https://link.segmentfault.com/?enc=OpSV6mwvpgYYlRITlzU%2BGw%3D%3D.VmR5v%2FZXiMr9WLxjBJ%2BmK0MS8PXzIYKip6BJzjKqSQlcffiiZbq5o5stangVbKJ2y8JYiDsK8DynCmcWB1d9xs02nQedJDAoNZtVXjGQAqmAMxXSpN3l8xY%2BQ%2B7ahv35" rel="nofollow">168行</a>代码应该是目标代码</p>
<pre><code class="js">i.create = function(e, t, r) {
r = r || {};
var n = r.originalEvent, o = r.extraFields || {}, a = Date.now() - l, s = new i;
s.currentTarget = null, s.type = e, s.timeStamp = a, s.mark = null, s.detail = t, s.bubbles = !!r.bubbles, s.composed = !!r.composed, s.__originalEvent = n, s.__hasCapture = !!r.capturePhase, s.__stopped = !1, s.__dispatched = !1;
for (var u in o) s[u] = o[u];
return s;
}</code></pre>
</li>
<li>
<p>可以看到<code>s.detail = t</code>这个赋值,<code>t</code>是<code>create</code>的<code>第二个参数</code>,由<code>node_modules/j-component/render/component.js</code>的<code>wxparser.Event.create</code>传入,但是传入的<code>第二个参数写死了{}</code>,所以咱们的组件获取<code>detail</code>的时候<code>永远为{}</code>,将其修改为<code>options.detail||{}</code>即可,修改后代码如下:</p>
<pre><code class="js">exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, options.detail||{}, xxxxxx</code></pre>
</li>
<li>
<p>重新测试</p>
<pre><code class="text"> PASS tests/components/user.spec.js
✓ components/user (1099ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.622s
Ran all test suites.</code></pre>
</li>
</ol>
<h2>避坑指南</h2>
<ol>
<li>
<code>querySelector</code>用法同HTML,但是需要在<code>组件</code>执行,而不是<code>组件.dom</code>,HTML中实在<code>DOMNode</code>执行的</li>
<li>
<code>dispatchEvent</code>是触发事件,需要在<code>组件</code>执行,上述代码中是触发<code>button组件</code>的<code>自定义事件</code>
</li>
<li>
<code>dispatchEvent</code>事件名规范: <code>去掉前导bind剩余的字符串为事件名</code>,示例代码中<code>bindgetuserinfo</code>,触发时就是<code>getuserinfo</code>,如果是<code>bindtap</code>,那触发时就是<code>tap</code>
</li>
<li>
<p><code>dispatchEvent</code>底层是<code>j-component</code>这个<code>npm模块实现</code>的,跟踪源码发现执行是异步的(代码文件<code>node_modules/j-component/src/render/component.js</code>,函数名<code>dispatchEvent</code>)</p>
<pre><code class="js">// 自定义事件
const customEvent = new CustomEvent(eventName, options);
// 模拟异步情况
setTimeout(() => {
dom.dispatchEvent(customEvent);
exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, {}, {
originalEvent: customEvent,
bubbles: true,
capturePhase: true,
composed: true,
extraFields: {
touches: options.touches || {},
changedTouches: options.changedTouches || {},
},
}));
}, 0);</code></pre>
</li>
<li>由于<code>setTimeout</code>的存在,触发事件为异步,所以写断言时需要加定时器</li>
</ol>
<h2>结语</h2>
<p>小程序单元测试基本是没什么经验扩借鉴,但是基于官网提供的工具,以及<code>开源</code>,咱们遇到问题时细心排查然后修改一下,还是可以解决问题的。对单元测试有疑问的小伙伴可以扫码加我进行交流<br><img src="/img/remote/1460000019883199" alt="微信" title="微信"></p>
使用Typescript装饰器来劫持React组件
https://segmentfault.com/a/1190000019888547
2019-07-26T19:29:16+08:00
2019-07-26T19:29:16+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
4
<p>最近在捣鼓Typescript的装饰器,NodeJs项目的装饰器比较好理解,但是React项目的装饰器由于有JSX,走了一点弯路,但是总之来说是<code>新技能get</code></p>
<h2>typescript对装饰器的说明</h2>
<blockquote>装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。</blockquote>
<p>装饰器为我们提供了<code>运行时修改数据</code>的能力。</p>
<h2>React例子</h2>
<p>Parent.tsx</p>
<pre><code class="typescript">@Component
export default class App extends PureComponent {
handleClick() {
console.log('parent click');
}
render() {
return (
<div className="App" onClick={this.handleClick}>parent</div>
);
}
}</code></pre>
<p>Component装饰器</p>
<pre><code class="typescript">function Component<T extends { new(...args: any[]): any }>(component: T) { // 泛型限定
return class extends component {
handleClick() { // 劫持onClick
super.handleClick()
console.log('child clicked');
}
render() {
const parent = super.render()
// 劫持onClick
return React.cloneElement(parent, { onClick: this.handleClick })
}
}
}</code></pre>
<p>点击渲染之后的<code>parent</code>字符,可以看到劫持成功</p>
<p><img src="/img/bVbvB4V?w=920&h=974" alt="clipboard.png" title="clipboard.png"></p>
<h2>项目地址</h2>
<p><a href="https://link.segmentfault.com/?enc=R3einB8EL8ifr07ZeTYWTQ%3D%3D.0eGVA50%2FeeEnaeULkzu6aHEejPSPTajnkEvHG0Fbt2p1Z8DJmqZbTKMSsKkUiXFWdWfIUusB2gY2T9zCVGyO%2FQ%3D%3D" rel="nofollow">react-decorator-example</a></p>
<h2>文后</h2>
<p>本文写的只是比较简单的装饰器用法,但是可以基于此文的原来来开发如<code>登录后才能访问的组件</code>之类的装饰器,将业务逻辑更好的组织起来。</p>
<p>对TS有兴趣的伙伴可以加我微信交流~</p>
<p><img src="/img/bVbvB5a?w=200&h=200" alt="clipboard.png" title="clipboard.png"></p>
TS简明教程(1)
https://segmentfault.com/a/1190000019883180
2019-07-26T12:42:40+08:00
2019-07-26T12:42:40+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
1
<p>为了后续内容(如<code>nestjs</code>等框架)的开展,本文更新TS相关的基础知识。</p>
<p>关注获取更多<code>TS精品文章</code><br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<blockquote>TypeScript是JavaScript的一个超集,支持 ECMAScript 6 标准。<br>TypeScript可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。<br>Typescript由微软开发,与C#出自同一人之手!</blockquote>
<h2>TS与JS的区别</h2>
<blockquote>TS是JS的超集,扩展了TS的语法,因此现有的JS代码可<code>直接与TS一起工作无需任何修改</code>,TS通过类型注解提供编译时的静态类型检查。</blockquote>
<p>由于TS与JS语法大部分一致,本文只对有差异的部分进行讲解。</p>
<h2>目录</h2>
<p>有些知识点可能是交叉的建议通读完本文再开始真正的开发,这样疑惑会比较少一点</p>
<ol>
<li>数据类型与类型断言</li>
<li>函数</li>
<li>接口和类</li>
<li>泛型</li>
<li>枚举</li>
<li>命名空间和模块</li>
<li>装饰器(注解)</li>
<li>高级类型</li>
<li>声明文件</li>
<li>tsconfig.json</li>
<li>
<p>示例</p>
<ol>
<li>React示例(前端)</li>
<li>Koa示例(后端)</li>
</ol>
</li>
</ol>
<h2>数据类型与类型声明</h2>
<p>TS使用<code>:</code>语法对类型进行声明。基础类型如下:</p>
<h3>布尔类型</h3>
<p>TS使用<code>boolean</code>来声明布尔类型。</p>
<pre><code class="typescript">let succeed: boolean = false; // 声明succeed为boolean类型</code></pre>
<h3>数字</h3>
<p>TS对数字的支持与JS一致,所有数字都是浮点数,所以TS并不存在<code>int</code>,<code>float</code>之类的数字类型声明,只有<code>number</code>。<br>除了支持十进制和十六进制,TS还支持ES6的二进制和八进制数字。</p>
<pre><code class="typescript">const age: number = 16; // 声明年龄为数字类型
const price: number = 99.99; // 声明价格为数字类型</code></pre>
<h3>字符串</h3>
<p>TS使用<code>string</code>声明字符串,和JS一样,支持<code>单引号</code>和<code>双引号</code>。</p>
<pre><code class="typescript">let name: string = "demo";
name = "demo1";
const description = `我是${name}`; // ES6语法</code></pre>
<h3>数组</h3>
<p>TS使用<code>类型[]</code>声明数组的元素类型,与JS不一样的地方在于,<code>TS</code>中一旦指明一个类型,所有元素必须是该类型。<code>JS</code>则可以往数组放任意类型的元素。</p>
<pre><code class="typescript">const numbers: number[] = [];
numbers.push(1);
numbers.push(2);
numbers.push('3'); // 错误,'3'不是数字类型</code></pre>
<h3>对象</h3>
<p>与JS一样,TS的对象也是由<code>键值对</code>构成,类型声明可以分别作用与<code>键类型</code>以及<code>值类型</code>。</p>
<p>声明语法:<code>{[key名称: key类型]: 值类型}</code><br>key名称可以<code>自定义</code>,如<code>key</code>,<code>index</code>都是合法的。</p>
<pre><code class="typescript">const config: {[key: string]: string} = {}; // 声明键和值都只能是字符串类型
config.version = '1.0.0';
const ages: {[key: number]: number} = {}; // 声明键值对都是数字类型
ages[10] = '1.0.0'; // 赋值</code></pre>
<p>上例中赋值语法虽然和数组一致,但是ages对象的长度为1,如果ages是数组的话,长度为11。(0-9个元素为undefined)</p>
<h3>任意类型</h3>
<p>TS用<code>any</code>用来声明<code>任意类型</code>,被<code>any</code>修饰的变量(或常量以及返回值等等)在编译阶段会<code>直接通过</code>,但是运行阶段可能会<code>抛出undefined或null相关错误</code>。</p>
<p><code>any</code>的出现使得现有的JS代码能够很快速的切换到TS。</p>
<pre><code class="typescript">let age:any = 10;
age = 'name'; // 编译通过</code></pre>
<h3>空类型</h3>
<p>TS使用<code>void</code>声明空类型。与<code>any</code>相反,表示没有任何类型,常用在函数返回值中。<br><code>void</code>类型只能被赋值为<code>null</code>和<code>undefined</code>。</p>
<pre><code class="typescript">function test(name: string): void { // 声明函数无返回值,编译成JS之后取返回值会取到undefined,与JS一致
console.log(name);
}
let v: void = null;</code></pre>
<h3>null和undefined</h3>
<p>TS中<code>默认情况</code>下,<code>null</code>和<code>undefined</code>是所有类型的子类型,换句话说,你可以把<code>null</code>和<code>undefined</code>直接赋值给<code>number</code>/<code>string</code>/<code>boolean</code>等类型。<br>但是不能反过来干,你不能把<code>number</code>/<code>string</code>/<code>boolean</code>类型赋值给<code>null</code>或者<code>undefined</code></p>
<pre><code>let u: undefined = undefined;
let n: null = null;</code></pre>
<h3>never</h3>
<p><code>never</code>是<code>100%不存在的值</code>的类型。比如函数中直接抛出异常或者有死循环。</p>
<pre><code class="typescript">function error(message: string): never {
throw new Error(message);
}
function fail() { // TS自动类型推断返回值类型为never,类型推断在下文中会提到
return error('failed');
}
function loop(): never { // 死循环,肯定不会返回
while(true) {}
}</code></pre>
<p><code>never</code>和<code>void</code>区别</p>
<ol>
<li>被<code>void</code>修饰的函数<code>能正常终止,只不过没有返回值</code>
</li>
<li>被<code>never</code>修饰的函数<code>不能正常终止,如抛出异常或死循环</code>
</li>
</ol>
<h3>枚举</h3>
<p>枚举是对JS的一个扩展。TS使用<code>enum</code>关键字定义枚举类型。</p>
<pre><code class="typescript">enum Color {
Red,
Green,
Yellow
}
let c: Color = Color.Red;</code></pre>
<h3>Object</h3>
<p>TS使用<code>object</code>类修饰对象类型,TS中表示<code>非原始类型</code>。原始类型如下:</p>
<ol>
<li>number</li>
<li>string</li>
<li>boolean</li>
<li>null</li>
<li>undefined</li>
<li>symbol(ES6新出类型)</li>
</ol>
<pre><code class="typescript">let a: object = {}; // ok
let a: object = 1; // error
let a: object = Symbol(); / error</code></pre>
<p>虽然<code>Symbol</code>长得像<code>对象类型</code>,不过在<code>ES6</code>规范中,人家就是<code>原始类型</code>。</p>
<h3>函数声明</h3>
<p>TS中可以对函数的<code>形参</code>以及<code>返回值</code>进行类型声明。</p>
<pre><code class="typescript">function a(name: string, age: number): string {
return `name:${name},age:${age}`;
}</code></pre>
<h3>类型断言</h3>
<p>类型断言说白了就是<code>告诉编译器,你按照我指定的类型进行处理</code>。</p>
<pre><code class="typescript">let value: any = 'a string';
const length: number = (<string>value).length;</code></pre>
<p>编译结果(正常编译且正常运行)</p>
<pre><code class="javascript">let value = 'a string';
const length = value.length;</code></pre>
<h3>类型推断</h3>
<p>当没有手动指定类型时,TS编译器利用类型推断来推断类型。<br>如果由于缺乏声明而不能推断出类型,那么它的类型被视作默认的动态 any 类型。</p>
<pre><code class="typescript">let num = 2; // 推断为number类型</code></pre>
<h2>函数</h2>
<p>TS函数与JS函数没有大的区别,多了一个类型系统。</p>
<pre><code class="typescript">function test(name: string) { // 自动推断返回类型为string
return name;
}</code></pre>
<h3>可选参数</h3>
<p>TS中函数每个形参都是<code>必须</code>的,当然你可以传递<code>null</code>和<code>undefined</code>,因为<code>他们是值</code>。但是在JS中,每个形参都是可选的,没传的情况下取值会得到<code>undefined</code>。<br>TS中<code>在参数名后面使用?号指明该参数为可选参数</code></p>
<pre><code class="typescript">function test(name: string, age?: number) {
console.log(`${name}:${age}`);
}
test('a'); // 输出 a:undefined</code></pre>
<h3>默认参数</h3>
<p>与ES6一致,TS也的函数也支持默认参数。需要注意的是<code>可选参数</code>和<code>默认参数</code>是<code>互斥</code>的。因为如果使用了默认参数,不管外部传不传值,取值的时候都是有值的,和可选参数矛盾。</p>
<pre><code class="typescript">function test(name: string, age: number = 10) {
console.log(`${name}:${age}`)
}
test('a'); // 输出 a:10</code></pre>
<h3>剩余参数</h3>
<p>剩余参数和ES6表现一致,但是多了类型声明:</p>
<pre><code class="typescript">function test(name1: string, ...names: string[]) {
console.log(name1, names);
}
test('1','2','3');// 输出 1 ['2', '3']</code></pre>
<h3>this执行</h3>
<p>TS中this指向和JS一致,这里不做赘述。</p>
<h2>结语</h2>
<p>未完待续~欢迎加我交流TS相关的知识~</p>
<p><img src="/img/remote/1460000019883230" alt="" title=""></p>
NodeJs简明教程(11) - 完结篇
https://segmentfault.com/a/1190000019870214
2019-07-25T11:47:33+08:00
2019-07-25T11:47:33+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
23
<blockquote>NodeJs简明教程将从零开始学习NodeJs相关知识,助力JS开发者构建全栈开发技术栈!</blockquote>
<p>关注获取更多<code>NodeJs精品文章</code><br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<p>本文是NodeJs简明教程的完结篇,将对以往文章进行归档。</p>
<h2>系列文章</h2>
<ol>
<li><a href="https://link.segmentfault.com/?enc=3w3%2BTKtR33D7ox838Qr6lw%3D%3D.Dyhi5mGsNgx9%2FpF19QRT7y6yJQ6UGZIUXPN5QcOSiv9%2F%2B3tnKGl%2B2raLhEkIhJn1i8PG9789oX5abbu6lO%2BRtA%3D%3D" rel="nofollow">NodeJs简明教程(1)——简介</a></li>
<li><a href="https://link.segmentfault.com/?enc=FyG8HX4u82vcq9oo2gv3PA%3D%3D.wedegacmUWpr1cbQQhOGlR34Aah7UcIaPCCVVxYqc0XyY6M53UFBqsdZYha4GJxzSH2k900vcD86bwWL3ItPS93F6ZF6Gy6kiuaujoTlv3s%3D" rel="nofollow">NodeJs简明教程(2)——安装</a></li>
<li><a href="https://link.segmentfault.com/?enc=KZMMA%2FmHfqrWGkjsJOR02Q%3D%3D.KrgBtcjCtcTyWa2ZnXIcg2sGmsaG98LXhV7O3oiF4EbM2j8MopNGEEP%2BMRNnAu7zlwYoGnow4LYXAr2eug2qSQ%3D%3D" rel="nofollow">NodeJs简明教程(3)——HTTP服务器</a></li>
<li><a href="https://link.segmentfault.com/?enc=2D6cluMrnNp2hHoQYw%2F3DA%3D%3D.KKXH4GBK5Vitq%2FWfsuDFsBWlgXD9Wg4mU5WSOPEVdPBprrld0BN8R5Lnppn%2F0T4JYKnsINjRZ9xT9FZzAKOu8Q%3D%3D" rel="nofollow">NodeJs简明教程(4)——文件系统</a></li>
<li><a href="https://link.segmentfault.com/?enc=M3QGOe17xv2nw%2Fqhoh719A%3D%3D.0tGW5Uv5BLqi8ZWGZA%2F3em%2F6mF2mlmFpNE%2F%2B0BNSnQRJGDnpRr%2FUXHjdDGI05mzPwquGF8Wokd6jkTPgZSWpWQ%3D%3D" rel="nofollow">NodeJs简明教程(5)——路径</a></li>
<li><a href="https://link.segmentfault.com/?enc=v4EEx4qt77CARxSEtwthnA%3D%3D.e1NjZQfEofpLt2W%2BaVUYLhrQf8kYFYKwYccCz%2FiyMXMXOF%2BD813AGtZjdZJvZ4pMGcLi5qU98LZFSxV3%2BX%2FiMw%3D%3D" rel="nofollow">NodeJs简明教程(6)——加解密</a></li>
<li><a href="https://link.segmentfault.com/?enc=ZiX2viqF%2BSw72lQ%2B5FIDgg%3D%3D.xC0HXKa7RQG6lxY5GegJPAxSVXC9UjHTV41TxZ2zfiZvS7UAJSDHAGuf2PAIuG2T4bEct%2FCEtcK1hmwt1MusAQ%3D%3D" rel="nofollow">NodeJs简明教程(7)——事件</a></li>
<li><a href="https://link.segmentfault.com/?enc=jsshZqO5dV0l16ISZNZjrg%3D%3D.NiGEDo5%2BBdO6sZcwH%2BAyg8yxvZL2FOHkMWgb4CbeGH3pR%2FdOnDiRm7nKtH103jtGzDMoKcjx0DvPamCEPdVIscDhyyXMw9iievY8sfkUnck%3D" rel="nofollow">NodeJs简明教程(8)——子进程</a></li>
<li><a href="https://link.segmentfault.com/?enc=pREQQv7MWYTfhibgwpiIiA%3D%3D.0QXuMHbPMeSpMBmnn7gkYtrnrmyLCxrWMsalCrZ0AdF2li1SDp%2FgPVQARBA8CK6JjT8y4b%2FagoSKmNuRuXGHdA%3D%3D" rel="nofollow">NodeJs简明教程(9)——TCP开发</a></li>
<li><a href="https://link.segmentfault.com/?enc=YgIe%2BIdYy3XWRZ6minr45A%3D%3D.AeijvilIMATuisYVLUYHnyQDB4l%2Faeqvik2Qtua8Prh2AlCkA%2BObaxyl1Zjd1wpwh4wyMaQIlu9b34lEn6OcVw%3D%3D" rel="nofollow">NodeJs简明教程(10)——UDP开发</a></li>
</ol>
<h2>接下来的内容</h2>
<ol>
<li>Typescript语言基础</li>
<li>NodeJs常用第三方模块(缓存、数据库、通用连接池、消息队列等等)</li>
<li>koa 专题</li>
<li>express 专题</li>
<li>nestjs 专题</li>
</ol>
<p>欢迎持续关注~</p>
<p>欢迎加群交流NodeJs相关的开发~</p>
<p><img src="/img/remote/1460000019825626" alt="微信群" title="微信群"></p>
NodeJs简明教程(10)
https://segmentfault.com/a/1190000019870200
2019-07-25T11:45:58+08:00
2019-07-25T11:45:58+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
1
<blockquote>NodeJs简明教程将从零开始学习NodeJs相关知识,助力JS开发者构建全栈开发技术栈!</blockquote>
<p>关注获取更多<code>NodeJs精品文章</code><br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<p>本文是NodeJs简明教程的第十篇,将介绍NodeJs <strong>dgram</strong> 模块(<code>UDP服务端/客户端</code>)相关的基本操作。</p>
<h2>啥是UDP</h2>
<blockquote>Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据报的方法。RFC 768描述了 UDP。</blockquote>
<p>NodeJs使用<code>dgram模块</code>实现<code>UDP服务端/客户端</code>相关功能。</p>
<p><code>dgram.createSocket</code>用来创建一个Socket对象,可以基于该套接口<code>接收</code>或<code>发送</code>数据。该方法原型如下:</p>
<p><code>dgram.createSocket(type[, callback])</code></p>
<ul>
<li>type <code><string></code> socket类型。<code>udp4</code>或<code>udp6</code>,对应<code>ipv4</code>和<code>ipv6</code>
</li>
<li>callback <code><Function></code> 接收到消息时的回调函数</li>
</ul>
<h2>Echo服务端开发</h2>
<p>server.js</p>
<pre><code class="js">const dgram = require('dgram');
const socket = dgram.createSocket('udp4');
socket.on('error', function(err) { // 监听socket错误
console.log('服务器错误', err);
socket.close();
});
socket.on('message',function(msg,sender) { // 监听收到数据
console.log('%s:%d => %s', sender.address,sender.port,msg.toString()); // 打印该数据包详情
socket.send('socket: '+msg.toString(),sender.port,sender.address,function(err) { // 发送数据给来源地址
if(err) {
console.log('回复%s:%d失败: %s',sender.address,sender.port,err.message);
return;
}
});
});
socket.bind(10000, function() { // 监听UDP端口
console.log('服务器正在监听 %s:%d', socket.address().address, socket.address().port);
});</code></pre>
<h2>Echo客户端开发</h2>
<p>由于<code>telnet</code>连接服务器使用的是<code>TCP协议</code>,所以本文对应的客户端需要使用NodeJs开发。</p>
<p>client.js</p>
<pre><code class="js">const dgram = require('dgram');
const socket = dgram.createSocket('udp4'); // 创建socket实例
socket.on('message', function(msg,sender) { // 监听收到数据
console.log('接收到%s:%d的消息:%s',sender.address,sender.port,msg.toString());
socket.close();
});
socket.send('hello',10000,function(err) { // 向目标端口发送数据
if(err) {
console.log('发送错误', err);
return;
}
console.log('发送成功');
});</code></pre>
<h2>执行</h2>
<ol>
<li>
<p>终端执行<code>node server.js</code>,输出</p>
<pre><code class="text">服务器正在监听 0.0.0.0:10000</code></pre>
</li>
<li>
<p>终端执行<code>node client.js</code>,输出</p>
<pre><code class="text">发送成功
接收到127.0.0.1:10000的消息:server: hello</code></pre>
</li>
<li>
<p>服务端输出:</p>
<pre><code class="text">127.0.0.1:50577 => hello</code></pre>
</li>
</ol>
<h2>结语</h2>
<p>NodeJs UDP服务端与客户端开发到此结束,但是使用UDP的情况下,数据包确认、流量控制等等操作都需要程序员手动完成,这一方面确实挺复杂的,没有什么特殊的要求的话使用TCP即可。</p>
<p><img src="/img/remote/1460000019825626" alt="微信群" title="微信群"></p>
NodeJs简明教程(9)
https://segmentfault.com/a/1190000019857595
2019-07-24T11:50:10+08:00
2019-07-24T11:50:10+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
4
<blockquote>NodeJs简明教程将从零开始学习NodeJs相关知识,助力JS开发者构建全栈开发技术栈!</blockquote>
<p>关注获取更多<code>NodeJs精品文章</code><br><img src="/img/remote/1460000019812012" alt="二维码" title="二维码"></p>
<p>本文是NodeJs简明教程的第九篇,将介绍NodeJs <strong>net</strong> 模块(<code>TCP服务端/客户端</code>)相关的基本操作。</p>
<h2>啥是TCP</h2>
<blockquote>传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793 定义。</blockquote>
<p>NodeJs使用<code>net模块</code>实现<code>TCP服务端/客户端</code>相关功能。</p>
<h2>Echo服务器开发</h2>
<blockquote>Echo服务器就是客户端发送什么,服务端就显示什么的一种服务端程序。主要为了调试网络和协议是否正常工作。</blockquote>
<p><code>net.createServer</code>用来创建一个服务端,该方法原型如下:</p>
<p><code>net.createServer([options][, connectionlistener]): net.Server</code></p>
<ul>
<li>
<p>options <code><Object></code></p>
<ul>
<li>allowHalfOpen <code><boolean></code> 表明是否允许半开的 TCP 连接。默认值: <code>false</code>。</li>
<li>pauseOnConnect <code><boolean></code> 表明是否应在传入连接上暂停套接字。默认值: false。</li>
</ul>
</li>
<li>connectionListener <code><Function></code> 客户端连接事件监听器。回调参数为<code>Socket(可以视为一个客户端连接)</code>
</li>
</ul>
<p>返回值为<code>net.Server</code>,<code>net.Server</code>主要方法如下:</p>
<p><code>server.listen([port[, host[, backlog]]][, callback])</code></p>
<ul>
<li>port <code><number></code> 监听端口</li>
<li>host <code><string></code> 监听主机</li>
<li>backlog <code><number></code> 待连接队列的最大长度</li>
<li>callback <code><Function></code> 监听成功回调函数</li>
</ul>
<p>server.js</p>
<pre><code class="js">const net = require('net');
const server = net.createServer(function (client) { // 创建服务端
console.log(client.address().address, '连接成功'); // 客户端连接成功时打印客户端地址
client.on('error', function (e) {
console.log(client.address().address, ' error >> ', e.message); // 连接错误时(如客户端异常断开)
});
client.on('data', function (data) { // 收到客户端数据
console.log(client.address().address, ' >> ', data.toString());
client.write(data); // 往客户端写数据
});
client.on('end', function () { // 客户端正常断开
console.log(client.address().address, '断开连接');
});
});
server.on('error', function (e) { // 服务器错误(如启动失败,端口占用)
console.log('服务器启动失败', e);
});
server.listen(10000, function () {
console.log('启动成功,地址', server.address().address);
});</code></pre>
<ol>
<li>执行<code>node server.js</code>可以看到输出<code>启动成功,地址xxx</code>
</li>
<li>
<p>打开终端,执行<code>telnet localhost 10000</code>,可以看到输出如下(如果不一样,请加群讨论):</p>
<pre><code class="text">Trying ::1...
Connected to localhost.
Escape character is '^]'.</code></pre>
</li>
<li>
<p>终端继续输入以下字符:</p>
<pre><code class="text">helloworld</code></pre>
</li>
<li>
<p>服务端会回复</p>
<pre><code class="text">hello world</code></pre>
</li>
</ol>
<p>该Echo服务器就开发已经测试通过了。虽然代码量不多,但是演示了从零开始开发一个TCP服务器的流程,相比于C语言开发TCP服务器还是方便很多的。</p>
<h2>TCP客户端</h2>
<p><code>net.connect</code>可以连接目标TCP服务器,该方法原型如下:</p>
<p><code>net.connect(port[,host][,connectionListener])</code></p>
<ul>
<li>port <code><number></code> 连接端口</li>
<li>host <code><string></code> 连接主机</li>
<li>connectionListener <code><Function></code> 连接成功的回调</li>
</ul>
<p>还是以刚才监听<code>10000</code>端口的服务端为例来开发客户端</p>
<p>client.js</p>
<pre><code class="js">const net = require('net');
const client = net.connect(10000, 'localhost', function () { // 连接服务器
console.log('连接服务器成功');
client.write('我是客户端'); // 往服务端发送数据
client.on('data', function (data) { // 接收到服务端数据
console.log('服务端消息', data.toString());
client.end(); // 断开连接
});
client.on('end', function () { // 连接断开事件
console.log('服务端连接断开');
});
});</code></pre>
<p>保证服务端开启的情况下,执行该js,输出如下:</p>
<pre><code class="text">连接服务器成功
服务端消息 我是客户端
服务端连接断开</code></pre>
<h2>结语</h2>
<p>NodeJs TCP服务端与客户端开发到此结束,但是TCP协议的学习远远不止于此,包括<code>自定义协议开发</code>、<code>TCP粘包问题</code>等等。这一块有问题的可以扫码加群交流:</p>
<p><img src="/img/remote/1460000019825626" alt="微信群" title="微信群"></p>
NodeJs简明教程(8)
https://segmentfault.com/a/1190000019846710
2019-07-23T14:16:33+08:00
2019-07-23T14:16:33+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
1
<blockquote>NodeJs简明教程将从零开始学习NodeJs相关知识,助力JS开发者构建全栈开发技术栈!</blockquote>
<p>关注获取更多<code>NodeJs精品文章</code><br><img src="/img/remote/1460000019846903?w=200&h=200" alt="二维码" title="二维码"></p>
<p>本文是NodeJs简明教程的第八篇,将介绍NodeJs <strong>子进程</strong> 模块相关的基本操作。</p>
<blockquote>child_process 模块提供了衍生子进程的能力(以一种与 popen(3) 类似但不相同的方式)。</blockquote>
<p>NodeJs的JS线程虽然是单线程,不能利用多核CPU,也不能执行CPU密集型的任务,但是通过派生子进程的形式加上<strong>IPC(进程间通信)</strong>,可以充分利用多核CPU。</p>
<h2>spawn</h2>
<p><code>spawn</code>可以执行<code>指定的命令</code>,<code>spawn</code>的函数原型如下:</p>
<pre><code class="js">child_process.spawn(command[,args][,options])</code></pre>
<ul>
<li>command <code><string></code> 要执行的命令</li>
<li>args <code><string[]></code> 传给命令的参数列表</li>
<li>
<p>options <code><Object></code> 额外选项</p>
<ul>
<li>cwd <code><string></code> 子进程<code>workdir</code>
</li>
<li>env <code><Object></code> 子进程环境变量</li>
</ul>
</li>
</ul>
<pre><code class="js">const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']); // 命令配置
ls.stdout.on('data', (data) => { // 监听命令执行的标准输出
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => { // 监听命令执行的标准错误输出
console.log(`stderr: ${data}`);
});
ls.on('close', (code) => { // 监听子进程退出
console.log(`子进程退出,使用退出码 ${code}`);
});</code></pre>
<p>以上例程输出(不同机器输出可能不一样)</p>
<pre><code class="text">stdout: total 0
drwxr-xr-x 970 root wheel 30K 7 19 23:00 bin
drwxr-xr-x 306 root wheel 9.6K 7 12 22:35 lib
drwxr-xr-x 249 root wheel 7.8K 7 19 23:00 libexec
drwxr-xr-x 15 root wheel 480B 4 1 14:15 local
drwxr-xr-x 239 root wheel 7.5K 7 12 22:35 sbin
drwxr-xr-x 46 root wheel 1.4K 9 21 2018 share
drwxr-xr-x 5 root wheel 160B 9 21 2018 standalone
子进程退出,使用退出码 0</code></pre>
<h2>exec</h2>
<p><code>exec</code>也可以执行<code>指定的命令</code>,与<code>spawn</code>区别是执行结果通过回调通知,<code>spawn</code>是通过事件,<code>exec</code>函数原型如下:</p>
<pre><code class="js">exec(command[,options][,callback])</code></pre>
<ul>
<li>command <code><string></code> 要执行的命令,命令参数使用空格分隔</li>
<li>
<p>options <code><Object></code> 额外选项</p>
<ul>
<li>cwd <code><string></code> 子进程<code>workdir</code>
</li>
<li>env <code><Object></code> 子进程环境变量</li>
<li>timeout <code><number></code> 子进程执行超时</li>
</ul>
</li>
<li>
<p>callback <code><Function></code> 执行结果回调</p>
<ul>
<li>error <code><Error></code> 执行错误(不是子进程的错误输出)</li>
<li>stdout <code><string|Buffer></code> 子进程标准输出</li>
<li>stderr <code><string|Buffer></code> 子进程标准错误输出</li>
</ul>
</li>
</ul>
<pre><code class="js">const exec = require('child_process').exec;
exec('ls -lh /usr',function(err,stdout,stderr) {
if(err) {
console.log('执行错误', err);
}
console.log('stdout', stdout);
console.log('stderr', stderr);
});</code></pre>
<p>以上例程输出</p>
<pre><code class="text">stdout: total 0
drwxr-xr-x 970 root wheel 30K 7 19 23:00 bin
drwxr-xr-x 306 root wheel 9.6K 7 12 22:35 lib
drwxr-xr-x 249 root wheel 7.8K 7 19 23:00 libexec
drwxr-xr-x 15 root wheel 480B 4 1 14:15 local
drwxr-xr-x 239 root wheel 7.5K 7 12 22:35 sbin
drwxr-xr-x 46 root wheel 1.4K 9 21 2018 share
drwxr-xr-x 5 root wheel 160B 9 21 2018 standalone
子进程退出,使用退出码 0</code></pre>
<h2>execFile</h2>
<p><code>execFile</code>类似于<code>exec</code>,但默认情况下不会派生shell, 相反,指定的可执行文件 file 会作为新进程直接地衍生,使其比 <code>exec</code>稍微更高效。</p>
<p>支持与<code>exec</code>相同的选项。 由于没有衍生 shell,因此<code>不支持 I/O 重定向和文件通配等行为</code>。<code>execFile</code>原型:</p>
<pre><code class="js">execFile(file[,args][,options][,callback])</code></pre>
<ul>
<li>file <code><string></code> 要执行的命令或可执行文件路径</li>
<li>args <code><string[]></code> 字符串数组形式的参数列表</li>
<li>
<p>options <code><Object></code> 额外选项</p>
<ul>
<li>cwd <code><string></code> 子进程<code>workdir</code>
</li>
<li>env <code><Object></code> 子进程环境变量</li>
<li>timeout <code><number></code> 子进程执行超时</li>
</ul>
</li>
<li>
<p>callback <code><Function></code> 执行结果回调</p>
<ul>
<li>error <code><Error></code> 执行错误(不是子进程的错误输出)</li>
<li>stdout <code><string|Buffer></code> 子进程标准输出</li>
<li>stderr <code><string|Buffer></code> 子进程标准错误输出</li>
</ul>
</li>
</ul>
<pre><code class="js">const execFile = require('child_process').execFile;
execFile('ls', ['--version'], function(error, stdout, stderr) {
if(err) {
console.log('执行错误', err);
}
console.log('stdout', stdout);
console.log('stderr', stderr);
});</code></pre>
<p>以上例程输出同<code>exec</code></p>
<h2>fork</h2>
<p><code>fork</code>是<code>spawn</code>的一个特例,专门用于派生新的<code>NodeJs进程</code>。<code>spawn</code>可以派生<code>任何进程</code>。<code>fork</code>方法原型如下:</p>
<pre><code class="js">fork(modulePath[,args][,options])</code></pre>
<ul>
<li>modulePath <code><string></code> 要执行的JS路径</li>
<li>args <code><string[]></code> 字符串数组形式的参数列表</li>
<li>
<p>options <code><Object></code> 额外选项</p>
<ul>
<li>cwd <code><string></code> 子进程的<code>workdir</code>
</li>
<li>env <code><Object></code> 环境变量</li>
<li>silent <code><boolean></code> 如果为 true,则子进程的 stdin、stdout 和 stderr 将会被输送到父进程,否则它们将会继承自父进程。默认<code>false</code>
</li>
</ul>
</li>
</ul>
<p>b.js</p>
<pre><code class="js">const fork = require('child_process').fork;
const child = fork('./a.js',{silent:true}); // silent为true时可以监听子进程标准输出和标准错误输出
child.stdout.on('data',function(data){ // 监听子进程标准输出
console.log('child stdout', data.toString('utf8'));
});
child.stderr.on('data', function(data){ // 监听子进程标准错误输出
console.log('child stderr', data.toString('utf8'));
});
child.on('close', function(){
console.log('child exit');
});</code></pre>
<p>a.js</p>
<pre><code class="js">console.log('我是子进程`);</code></pre>
<p>终端执行<code>node b.js</code>,以上例程输出:</p>
<pre><code class="text">child stdout 我是子进程
child exit</code></pre>
<h2>结语</h2>
<p>子进程模块的介绍到此就告一段落了,一般情况下使用<code>spawn</code>和<code>execFile</code>即可。有任何疑问请扫码加群交流:</p>
<p><img src="/img/remote/1460000019846904" alt="微信群" title="微信群"></p>
NodeJs简明教程(7)
https://segmentfault.com/a/1190000019833156
2019-07-22T11:37:14+08:00
2019-07-22T11:37:14+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
1
<blockquote>NodeJs简明教程将从零开始学习NodeJs相关知识,助力JS开发者构建全栈开发技术栈!</blockquote>
<p>本文是NodeJs简明教程的第七篇,将介绍NodeJs events模块相关的基本操作。</p>
<blockquote>大多数 Node.js 核心 API 构建于惯用的异步事件驱动架构,其中某些类型的对象(又称触发器,Emitter)会触发命名事件来调用函数(又称监听器,Listener)。</blockquote>
<h2>快速开始</h2>
<p>使用事件监听器一般包含以下操作:</p>
<ol>
<li>新建事件监听器实例</li>
<li>设置监听函数</li>
<li>触发事件</li>
</ol>
<pre><code class="js">const EventEmitter = require('events'); // 引用模块
class MyEmitter extends EventEmitter {} // 初始化监听器
const myEmitter = new MyEmitter();
myEmitter.on('event', () => { // 设置监听函数
console.log('an event occurred!');
});
myEmitter.emit('event'); // 触发事件</code></pre>
<p>以上例程会输出<code>an event occurred!</code></p>
<h2>一次性事件监听</h2>
<p>上文中的监听方式<code>事件触发几次</code>就会<code>输出几次an event occurred!</code>,有些事件可能是一次性的。这时候可以使用<code>once</code>监听。</p>
<pre><code class="js">const EventEmitter = require('events'); // 引用模块
class MyEmitter extends EventEmitter {} // 初始化监听器
const myEmitter = new MyEmitter();
myEmitter.once('event', () => { // 设置监听函数
console.log('an event occurred!');
});
myEmitter.emit('event'); // 触发事件
myEmitter.emit('event'); // 触发事件</code></pre>
<p>以上例程会输出<code>1次</code> <code>an event occurred!</code>;</p>
<h2>同一事件多次监听</h2>
<p>上文中的监听方式都是只有<code>1个</code>监听函数,通过<code>多次调用on</code>可以设置多个监听函数。</p>
<pre><code class="js">const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.once('event', () => { // 监听器1
console.log('监听器1收到事件');
});
myEmitter.on('event', () => { // 监听器2
console.log('监听器2收到事件');
})
myEmitter.emit('event'); // 触发事件</code></pre>
<p>以上例程会输出</p>
<pre><code class="text">监听器1收到事件
监听器2收到事件</code></pre>
<h2>接收事件参数</h2>
<ol>
<li>
<code>emit</code>函数的第一个值为<code>事件名</code>,<code>后续参数为事件值</code>
</li>
<li>
<code>on</code>和<code>once</code>等监听器设置函数的回调函数收到的值<code>为emit传入的事件参数</code>
</li>
</ol>
<pre><code class="js">const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.once('event', (param1,param2,param3) => { // 接收事件参数
console.log('收到事件',param1,param2,param3);
});
myEmitter.emit('event','参数1','参数2',{name:'参数3'}); // 发送事件参数</code></pre>
<p>以上例程会输出</p>
<pre><code class="text">收到事件 参数1 参数2 { name: '参数3' }</code></pre>
<h2>获取事件监听器上所有事件</h2>
<p>使用<code>eventNames()实例方法</code>获取监听器上所有事件</p>
<pre><code class="js">const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.once('event', (param1,param2,param3) => {
console.log('收到事件',param1,param2,param3);
});
myEmitter.once('event2',() => {
console.log('收到事件2');
});
console.log(myEmitter.eventNames());</code></pre>
<p>以上例程输出<code>[ 'event', 'event2' ]</code></p>
<h2>移除事件监听器</h2>
<p>使用<code>off实例方法</code>移除单个监听器</p>
<pre><code class="js">const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
const callback = (param1) => {
console.log(param1);
};
myEmitter.on('event', callback); // 添加监听器
myEmitter.off('event', callback); // 移除监听器
myEmitter.emit('event'); // 触发事件</code></pre>
<p>以上例程<code>没有输出</code>,因为<code>先添加监听器,随后移除,触发事件时已经没有可用的监听器了</code></p>
<h2>移除所有监听器</h2>
<p>使用<code>removeAllListeners([eventName])实例方法移除所有监听器</code>。</p>
<ol>
<li>removeAllListener<code>不传参数</code>时移除该<code>emitter实例</code>上<code>所有</code>事件监听器</li>
<li>removeAllListener传入<code>字符串</code>参数时移除该<code>emitter实例</code>上<code>所有该事件</code>的监听器</li>
</ol>
<h2>结语</h2>
<p>事件系统是NodeJs的灵魂,在几乎所有的I/O模块都有使用,希望各位读者好好掌握。事件模块读后有疑问请加微信群讨论。</p>
<p><img src="/img/remote/1460000019825626" alt="微信群" title="微信群"></p>
NodeJs简明教程(6)
https://segmentfault.com/a/1190000019825623
2019-07-21T11:42:05+08:00
2019-07-21T11:42:05+08:00
xialeistudio
https://segmentfault.com/u/xialeistudio
2
<blockquote>NodeJs简明教程将从零开始学习NodeJs相关知识,助力JS开发者构建全栈开发技术栈!</blockquote>
<p>本文是NodeJs简明教程的第六篇,将介绍NodeJs crypto模块相关的基本操作。</p>
<blockquote>crypto 模块提供了加密功能,包括对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装。</blockquote>
<h2>Hash</h2>
<blockquote>Hash类是用于创建数据哈希值的工具类。</blockquote>
<p>哈希算法严格来说并不属于加密算法,传统意义上的 <strong>加密</strong> 是与 <strong>解密</strong> 相配对的。哈希算法能够保证被哈希的内容不被篡改。针对任意长度的输入数据都可以产生固定位数的哈希值。</p>
<p>crypto模块对hash的操作是一致的,除了算法名不一致之外,本文以 <strong>md5</strong> 和 <strong>sha1</strong> 作为示例。</p>
<h3>MD5</h3>
<pre><code class="js">const crypto = require('crypto');
const hash = crypto.createHash('md5'); // 创建MD5 hash示例
hash.update('111111'); // 待计算hash的数据
console.log(hash.digest('hex'));</code></pre>
<p>以上例程输出 <code>96e79218965eb72c92a549dd5a330112</code></p>
<h3>SHA1</h3>
<pre><code class="js">const crypto = require('crypto');
const hash = crypto.createHash('sha1'); // 创建MD5 hash示例
hash.update('111111'); // 待计算hash的数据
console.log(hash.digest('hex'));</code></pre>
<p>以上例程输出 <code>3d4f2bf07dc1be38b20cd6e46949a1071f9d0e3d</code></p>
<h2>Base64</h2>
<p><code>Base64</code>并不是<code>crypto</code>模块的成员,但是跟本节内容比较相近,所以放过来了。Base64是一套编码算法,常用在二进制数据编码上。</p>
<h3>Base64编码</h3>
<pre><code class="js">const data = '111111';
const encodedData = Buffer.from(data, 'utf8').toString('base64'); // 输入编码为utf8,输出为base64
console.log(encodedData);</code></pre>
<p>以上例程输出 <code>MTExMTEx</code></p>
<h3>Base64解码</h3>
<pre><code class="js">const data = 'MTExMTEx';
const decodedData = Buffer.from(data, 'base64').toString('utf8'); // 输入编码为base64,输出编码为utf8
console.log(decodedData);</code></pre>
<p>以上例程输出<code>111111</code></p>
<h2>Hmac</h2>
<blockquote>Hmac类是用于创建加密Hmac摘要的工具。</blockquote>
<p>Hmac算法也是一种hash算法,但是它需要一个密钥,针对同样的输入,传统的hash算法输出是固定的。<br>但是Hmac的输出会随着密钥的不同而不同。</p>
<pre><code class="js">const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'secret-key');
hmac.update('Hello, world!');
console.log(hmac.digest('hex'));</code></pre>
<p>以上例程输出 <code>f4d850b1017eb4e20e0c58443919033c90cc9f4fe889b4d6b4572a4a0ec2d08a</code></p>
<h2>AES</h2>
<blockquote>AES是一种常用的对称加密算法,加解密都用同一个密钥。</blockquote>
<h3>AES加密</h3>
<pre><code class="js">const crypto = require('crypto');
const cipher = crypto.createCipheriv('aes192', '111111111111111111111111', '1111111111111111')
var crypted = cipher.update('1', 'utf8', 'hex');
crypted += cipher.final('hex');
console.log(crypted);</code></pre>
<p>以上例程输出 <code>5bb3e6eb39e502b5fa74d93796087efa</code></p>
<p><strong>说明:</strong></p>
<p><code>createCipheriv</code>原型如下:</p>
<p><code>crypto.createCipheriv(algorithm,key,iv [,options])</code></p>
<ol>
<li>
<code>iv</code>是初始化向量,可以 <strong>为空</strong> 或者 <strong>16</strong> 字节的字符串</li>
<li>
<p><code>key</code>是加密密钥,根据选用的算法不同,密钥长度也不同,对应关系如下:</p>
<ol>
<li>
<code>aes128</code>对应<code>16位</code>长度密钥</li>
<li>
<code>aes192</code>对应<code>24位</code>长度秘钥</li>
<li>
<code>aes256</code>对应<code>32位</code>长度密钥</li>
</ol>
</li>
</ol>
<h3>AES解密</h3>
<pre><code class="js">const crypto = require('crypto');
const cipher = crypto.createDecipheriv('aes192', '111111111111111111111111', '1111111111111111')
var data = cipher.update('5bb3e6eb39e502b5fa74d93796087efa', 'hex', 'utf8'); // 输入数据编码为hex(16进制),输出为utf8
data += cipher.final('utf8');
console.log(data);</code></pre>
<p>以上例程输出<code>1</code></p>
<p><code>crypto.createDecipheriv</code>方法原型与<code>crypto.createCipher</code>一致,这里不在赘述。</p>
<h2>RSA</h2>
<blockquote>RSA算法是一种非对称加密算法,即由一个私钥和一个公钥构成的密钥对,通过私钥加密,公钥解密,或者通过公钥加密,私钥解密。其中,公钥可以公开,私钥必须保密。</blockquote>
<h3>生成密钥对</h3>
<p>使用RSA算法前必须提供密钥对,本文使用<code>openssl</code>命令进行生成。</p>
<ol>
<li>
<code>openssl genrsa -out private.pem 2048 </code> 生成<code>2048位</code>长度的<code>私钥</code>
</li>
<li>
<code>openssl rsa -in private.pem -pubout -out public.pem</code> 导出公钥</li>
</ol>
<p>这样在当前目录我们就得到了<code>private.pem</code>和<code>public.pem</code></p>
<h3>RSA加密</h3>
<pre><code class="js">const crypto = require('crypto');
const fs = require('fs');
const privateKey = fs.readFileSync('./private.pem', { encoding: 'utf8' });
const encodedData = crypto.privateEncrypt(privateKey, Buffer.from('111111','utf8')); // 传入utf8编码的数据
console.log(encodedData.toString('hex'));</code></pre>
<p>以上例程输出</p>
<pre><code class="text">44a1b50b9639e4cbe17d55ca57dcb041387acadae3d3721fd9803a3a33091a36d59977feaa6caad990e58b9542c26297de6014e20819f0a71eadd0793bfe0fac834f30d2a05f8b329a3b2409e9f8b7fbd7de3734ada00228b84027568be58a2a34ccf0c4a8b2d02c58eef510931423ed5f40c696361b606df11609248b271aebcd17f9a113f98a8fa86c9c45bd609256f4779ce01ea3027171fffb35e695f1c38553aecafb72a2f46a9012246fde0f2934eacba8932bca38e228f4f4294873ed75d9acf79ab854897ebaab2375384b2da682c1b2e2b49b0592929067b3d5a11971d912629a178691345f7f88137343588b5c51d60643e5c00998484727b8c4a8</code></pre>
<h3>RSA解密</h3>
<pre><code class="js">const crypto = require('crypto');
const fs = require('fs');
const publicKey = fs.readFileSync('./public.pem', { encoding: 'utf8' });
const encodedData = '44a1b50b9639e4cbe17d55ca57dcb041387acadae3d3721fd9803a3a33091a36d59977feaa6caad990e58b9542c26297de6014e20819f0a71eadd0793bfe0fac834f30d2a05f8b329a3b2409e9f8b7fbd7de3734ada00228b84027568be58a2a34ccf0c4a8b2d02c58eef510931423ed5f40c696361b606df11609248b271aebcd17f9a113f98a8fa86c9c45bd609256f4779ce01ea3027171fffb35e695f1c38553aecafb72a2f46a9012246fde0f2934eacba8932bca38e228f4f4294873ed75d9acf79ab854897ebaab2375384b2da682c1b2e2b49b0592929067b3d5a11971d912629a178691345f7f88137343588b5c51d60643e5c00998484727b8c4a8';
const rawData = crypto.publicDecrypt(publicKey, Buffer.from(encodedData, 'hex')); // 传入hex(16进制)数据
console.log(rawData.toString('utf8'));</code></pre>
<p>以上例程输出</p>
<pre><code class="text">111111</code></pre>
<h2>结语</h2>
<p>常用的加解密、哈希、编解码用法已经介绍完毕,读后有疑问请加微信群讨论。<br><img src="/img/remote/1460000019825626" alt="微信群" title="微信群"><br>也可以关注公众号获取本系列持续更新:<br><img src="/img/remote/1460000019812012" alt="公众号" title="公众号"></p>