首页 文章 精选 留言 我的

精选列表

搜索[图像理解],共10000篇文章
优秀的个人博客,低调大师

每日一博 | 三步轻松理解 Kerberos 协议

Kerberos是一种身份验证协议,它作为一种可信任的第三方认证服务,通过使用对称加密技术为客户端/服务器应用程序提供强身份验证。在域环境下,AD域使用Kerberos协议进行验证,熟悉和掌握Kerberos协议是学习域渗透的基础。 Kerberos协议中主要的三个角色: 1.访问服务的Client2.提供服务的Server3.KDC:密钥分发中心,默认安装在域控上 AS:身份验证服务 TGS:票证授予服务 Kerberos协议认证过程: 协议可以分为三个步骤:一是获得票据许可票据,二是获取服务许可票据,三是获得服务。 第一步:获得票据许可票据 KRB_AS_REQ:用于向KDC请求TGT 当用户在客户端输入域用户和密码时,客户端将用户密码转换为hash作为加密密钥,对时间戳进行加密作为请求凭据。 cipher:加密的时间戳,即客户端的当前时间与用户的hash加密后的结果 KRB_AS_REQ 数据包如下: KRB_AS_REP:用于通过KDC传递TGT 收到请求后,KDC从AD域数据库中找到对用用户的hash解密时间戳来验证用户身份。如果时间戳在允许的时间范围内,那么它就会生成一个会话密钥(Session key),以AS_REP 数据包进行响应。 AS_REP 包含信息: ticket:使用krbtgt hash加密,包含用户名/会话密钥和到期时间等信息.enc-part:使用用户hash加密,包含会话密钥/TGT到期时间和随机数(防重放) KRB_AS_REP数据包如下: 第二步:获得服务许可票据 KRB_TGS_REQ:使用TGT向KDC请求TGS 客户端获得TGT和用户密钥加密的enc-part,使用用户hash解密enc-part获得会话密钥(Session key),然后使用会话密钥将用户名/时间戳进行加密,生成authenticator和TGT发送给TGS。 ticket:实质上就是一张TGT,客户端没有 krbtgt hash,故无法解密TGT. KRB_TGS_REQ 数据包如下: KRB_TGS_REP:通过KDC传递TGS TGS 收到KRB_TGS_REQ请求后,使用 krbtgt hash解密ticket 获取会话密钥(Session key),然后使用会话密钥解密 authenticator 获取用户名和时间戳进行身份验证。确认信息后,创建一个服务会话密钥(Service Session key)。 ticket:使用对应的服务密钥进行加密,包含服务会话密钥/用户名/到期时间等信息,本质上就是一张ST(Service Ticket)。enc-part:包含使用会话密钥加密的服务会话密钥(Service Session key) KRB_TGS_REP数据包如下: 第三步:获得服务 KRB_AP_REQ:使用TGS,服务对用户身份验证 客户端已经拥有了有效的TGS可以与服务进行交互,使用会话密钥解密 enc-part,得到 服务会话密钥(Service Session key),将用户名/时间戳等信息使用服务会话密钥(Service Session key)进行加密,得到新的Authentication。 KRB_AP_REP:由服务用来针对用户标识自身 服务端接收到请求,使用自己的hash解密TGS获得服务会话密钥(Service Session key)和授权用户信息,然后使用服务会话密钥解密Authentication,比对用户名和时间戳等信息,如果有相互验证标记,服务端使用服务会话密钥加密时间戳发给客户端,客户端解密时间戳验证服务端,然后开始请求服务。 本文分享自微信公众号 - Bypass(Bypass--)。 如有侵权,请联系 support@oschina.cn 删除。 本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

Pandas之:深入理解Pandas的数据结构

简介 本文将会讲解Pandas中基本的数据类型Series和DataFrame,并详细讲解这两种类型的创建,索引等基本行为。 使用Pandas需要引用下面的lib: In [1]: import numpy as np In [2]: import pandas as pd Series Series是一维带label和index的数组。我们使用下面的方法来创建一个Series: >>> s = pd.Series(data, index=index) 这里的data可以是Python的字典,np的ndarray,或者一个标量。 index是一个横轴label的list。接下来我们分别来看下怎么创建Series。 从ndarray创建 s = pd.Series(np.random.randn(5), index=['a', 'b', 'c', 'd', 'e']) s Out[67]: a -1.300797 b -2.044172 c -1.170739 d -0.445290 e 1.208784 dtype: float64 使用index获取index: s.index Out[68]: Index(['a', 'b', 'c', 'd', 'e'], dtype='object') 从dict创建 d = {'b': 1, 'a': 0, 'c': 2} pd.Series(d) Out[70]: a 0 b 1 c 2 dtype: int64 从标量创建 pd.Series(5., index=['a', 'b', 'c', 'd', 'e']) Out[71]: a 5.0 b 5.0 c 5.0 d 5.0 e 5.0 dtype: float64 Series 和 ndarray Series和ndarray是很类似的,在Series中使用index数值表现的就像ndarray: s[0] Out[72]: -1.3007972194268396 s[:3] Out[73]: a -1.300797 b -2.044172 c -1.170739 dtype: float64 s[s > s.median()] Out[74]: d -0.445290 e 1.208784 dtype: float64 s[[4, 3, 1]] Out[75]: e 1.208784 d -0.445290 b -2.044172 dtype: float64 Series和dict 如果使用label来访问Series,那么它的表现就和dict很像: s['a'] Out[80]: -1.3007972194268396 s['e'] = 12. s Out[82]: a -1.300797 b -2.044172 c -1.170739 d -0.445290 e 12.000000 dtype: float64 矢量化操作和标签对齐 Series可以使用更加简单的矢量化操作: s + s Out[83]: a -2.601594 b -4.088344 c -2.341477 d -0.890581 e 24.000000 dtype: float64 s * 2 Out[84]: a -2.601594 b -4.088344 c -2.341477 d -0.890581 e 24.000000 dtype: float64 np.exp(s) Out[85]: a 0.272315 b 0.129487 c 0.310138 d 0.640638 e 162754.791419 dtype: float64 Name属性 Series还有一个name属性,我们可以在创建的时候进行设置: s = pd.Series(np.random.randn(5), name='something') s Out[88]: 0 0.192272 1 0.110410 2 1.442358 3 -0.375792 4 1.228111 Name: something, dtype: float64 s还有一个rename方法,可以重命名s: s2 = s.rename("different") DataFrame DataFrame是一个二维的带label的数据结构,它是由Series组成的,你可以把DataFrame看成是一个excel表格。DataFrame可以由下面几种数据来创建: 一维的ndarrays, lists, dicts, 或者 Series 结构化数组创建 2维的numpy.ndarray 其他的DataFrame 从Series创建 可以从Series构成的字典中来创建DataFrame: d = {'one': pd.Series([1., 2., 3.], index=['a', 'b', 'c']),'two': pd.Series([1., 2., 3., 4.], index=['a', 'b', 'c', 'd'])} df = pd.DataFrame(d) df Out[92]: one two a 1.0 1.0 b 2.0 2.0 c 3.0 3.0 d NaN 4.0 进行index重排: pd.DataFrame(d, index=['d', 'b', 'a']) Out[93]: one two d NaN 4.0 b 2.0 2.0 a 1.0 1.0 进行列重排: pd.DataFrame(d, index=['d', 'b', 'a'], columns=['two', 'three']) Out[94]: two three d 4.0 NaN b 2.0 NaN a 1.0 NaN 从ndarrays 和 lists创建 d = {'one': [1., 2., 3., 4.],'two': [4., 3., 2., 1.]} pd.DataFrame(d) Out[96]: one two 0 1.0 4.0 1 2.0 3.0 2 3.0 2.0 3 4.0 1.0 pd.DataFrame(d, index=['a', 'b', 'c', 'd']) Out[97]: one two a 1.0 4.0 b 2.0 3.0 c 3.0 2.0 d 4.0 1.0 从结构化数组创建 可以从结构化数组中创建DF: In [47]: data = np.zeros((2, ), dtype=[('A', 'i4'), ('B', 'f4'), ('C', 'a10')]) In [48]: data[:] = [(1, 2., 'Hello'), (2, 3., "World")] In [49]: pd.DataFrame(data) Out[49]: A B C 0 1 2.0 b'Hello' 1 2 3.0 b'World' In [50]: pd.DataFrame(data, index=['first', 'second']) Out[50]: A B C first 1 2.0 b'Hello' second 2 3.0 b'World' In [51]: pd.DataFrame(data, columns=['C', 'A', 'B']) Out[51]: C A B 0 b'Hello' 1 2.0 1 b'World' 2 3.0 从字典list创建 In [52]: data2 = [{'a': 1, 'b': 2}, {'a': 5, 'b': 10, 'c': 20}] In [53]: pd.DataFrame(data2) Out[53]: a b c 0 1 2 NaN 1 5 10 20.0 In [54]: pd.DataFrame(data2, index=['first', 'second']) Out[54]: a b c first 1 2 NaN second 5 10 20.0 In [55]: pd.DataFrame(data2, columns=['a', 'b']) Out[55]: a b 0 1 2 1 5 10 从元组中创建 可以从元组中创建更加复杂的DF: In [56]: pd.DataFrame({('a', 'b'): {('A', 'B'): 1, ('A', 'C'): 2}, ....: ('a', 'a'): {('A', 'C'): 3, ('A', 'B'): 4}, ....: ('a', 'c'): {('A', 'B'): 5, ('A', 'C'): 6}, ....: ('b', 'a'): {('A', 'C'): 7, ('A', 'B'): 8}, ....: ('b', 'b'): {('A', 'D'): 9, ('A', 'B'): 10}}) ....: Out[56]: a b b a c a b A B 1.0 4.0 5.0 8.0 10.0 C 2.0 3.0 6.0 7.0 NaN D NaN NaN NaN NaN 9.0 列选择,添加和删除 可以像操作Series一样操作DF: In [64]: df['one'] Out[64]: a 1.0 b 2.0 c 3.0 d NaN Name: one, dtype: float64 In [65]: df['three'] = df['one'] * df['two'] In [66]: df['flag'] = df['one'] > 2 In [67]: df Out[67]: one two three flag a 1.0 1.0 1.0 False b 2.0 2.0 4.0 False c 3.0 3.0 9.0 True d NaN 4.0 NaN False 可以删除特定的列,或者pop操作: In [68]: del df['two'] In [69]: three = df.pop('three') In [70]: df Out[70]: one flag a 1.0 False b 2.0 False c 3.0 True d NaN False 如果插入常量,那么会填满整个列: In [71]: df['foo'] = 'bar' In [72]: df Out[72]: one flag foo a 1.0 False bar b 2.0 False bar c 3.0 True bar d NaN False bar 默认会插入到DF中最后一列,可以使用insert来指定插入到特定的列: In [75]: df.insert(1, 'bar', df['one']) In [76]: df Out[76]: one bar flag foo one_trunc a 1.0 1.0 False bar 1.0 b 2.0 2.0 False bar 2.0 c 3.0 3.0 True bar NaN d NaN NaN False bar NaN 使用assign 可以从现有的列中衍生出新的列: In [77]: iris = pd.read_csv('data/iris.data') In [78]: iris.head() Out[78]: SepalLength SepalWidth PetalLength PetalWidth Name 0 5.1 3.5 1.4 0.2 Iris-setosa 1 4.9 3.0 1.4 0.2 Iris-setosa 2 4.7 3.2 1.3 0.2 Iris-setosa 3 4.6 3.1 1.5 0.2 Iris-setosa 4 5.0 3.6 1.4 0.2 Iris-setosa In [79]: (iris.assign(sepal_ratio=iris['SepalWidth'] / iris['SepalLength']) ....: .head()) ....: Out[79]: SepalLength SepalWidth PetalLength PetalWidth Name sepal_ratio 0 5.1 3.5 1.4 0.2 Iris-setosa 0.686275 1 4.9 3.0 1.4 0.2 Iris-setosa 0.612245 2 4.7 3.2 1.3 0.2 Iris-setosa 0.680851 3 4.6 3.1 1.5 0.2 Iris-setosa 0.673913 4 5.0 3.6 1.4 0.2 Iris-setosa 0.720000 注意, assign 会创建一个新的DF,原DF保持不变。 下面用一张表来表示DF中的index和选择: 操作 语法 返回结果 选择列 df[col] Series 通过label选择行 df.loc[label] Series 通过数组选择行 df.iloc[loc] Series 行的切片 df[5:10] DataFrame 使用boolean向量选择行 df[bool_vec] DataFrame 本文已收录于http://www.flydean.com/03-python-pandas-data-structures/ 最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现! 欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

优秀的个人博客,低调大师

一文带你理解最简消息队列实现

最近在看公司的 redis queue 时,发现底层使用的是 go-zero 的 queue 。本篇文章来看看 queue 的设计,也希望可以从里面了解到 mq 的最小型设计实践。 使用 结合其他 mq 的使用经历,基本的使用流程: 创建 producer 或 consumer 启动 mq 生产消息/消费消息 对应到 queue 中,大致也是这个: 创建 queue // 生产者创建工厂 producer := newMockedProducer() // 消费者创建工厂 consumer := newMockedConsumer() // 将生产者以及消费者的创建工厂函数传递给 NewQueue() q := queue.NewQueue(func() (Producer, error) { return producer, nil }, func() (Consumer, error) { return consumer, nil }) 我们看看 NewQueue 需要什么构建条件: producer constructor consumer constructor 将双方的工厂函数传递给 queue ,由它去执行以及重试。 这两个需要的目的是将生产者/消费者的构建和消息生产/消费都封装在 mq 中,而且将生产者/消费者的整套逻辑交给开发者处理: type ( // 开发者需要实现此接口 Producer interface { AddListener(listener ProduceListener) Produce() (string, bool) } ... // ProducerFactory定义了生成Producer的方法 ProducerFactory func() (Producer, error) ) 其实也就是将生产者的逻辑交个开发者自己完成,mq 只负责生产者/消费者的消息传递和之间的调度。 工厂方法的设计,是将生产者本身和生产消息,这两个任务都交给 queue 自己来做调度或者重试。 生产msg 生产消息当然要回到生产者本身: type mockedProducer struct { total int32 count int32 // 使用waitgroup来模拟任务的完成 wait sync.WaitGroup } // 实现 Producer interface 的方法:Produce() func (p *mockedProducer) Produce() (string, bool) { if atomic.AddInt32(&p.count, 1) <= p.total { p.wait.Done() return "item", true } time.Sleep(time.Second) return "", false } queue 中的生产者编写都必须实现: Produce():由开发者编写生产消息的逻辑 AddListener():生产者 消费msg 和生产者类似: type mockedConsumer struct { count int32 } func (c *mockedConsumer) Consume(string) error { atomic.AddInt32(&c.count, 1) return nil } 启动 queue 启动,然后验证我们上述的生产者和消费者之间的数据是否传输成功: func TestQueue(t *testing.T) { producer := newMockedProducer(rounds) consumer := newMockedConsumer() // 创建 queue q := NewQueue(func() (Producer, error) { return producer, nil }, func() (Consumer, error) { return consumer, nil }) // 当生产者生产完毕,执行 Stop() 关闭生产端生产 go func() { producer.wait.Wait() // mq生产端停止生产,不是mq本身 Stop 运行 q.Stop() }() // 启动 q.Start() // 验证生产消费端是否消息消费完成 assert.Equal(t, int32(rounds), atomic.LoadInt32(&consumer.count)) } 以上就是 queue 最简易的入门使用代码。开发者可以根据自己的业务实际情况:自由定义生产者/消费者已经生产/消费逻辑。 整体设计 ![image-20210506224102836](/Users/dyhxl/Library/Application Support/typora-user-images/image-20210506224102836.png) 整体流程如上图: 全体的通信都由 channel 进行 通过加入监听器 listener ,以及事件触发 event ,相当于将触发器逻辑分离出来 生产者有 produceone ,这个是生产消息的逻辑,但是其中的 Produce() 是由开发者编写【上面的 interface 中正是这个函数】 同理消费者,Consume() 基本的消息流动就入上图以及上述描写的,具体的代码分析我们就留到下一篇,我们😁分析里面,尤其是如何控制 channel 是整个设计的核心。 总结 本篇文章从使用以及整个架构分析上简略介绍了 queue 的设计。下篇我们将深入源码,分析内部消息流转以及 channel 控制。 关于 go-zero 更多的设计和实现文章,可以持续关注我们。欢迎大家去关注和使用。 项目地址 https://github.com/tal-tech/go-zero https://gitee.com/kevwan/go-zero 欢迎使用 go-zero 并 star 支持我们! 微信交流群 关注『微服务实践』公众号并回复 进群 获取社区群二维码。

优秀的个人博客,低调大师

手写React-Router源码,深入理解其原理

上一篇文章我们讲了React-Router的基本用法,并实现了常见的前端路由鉴权。本文会继续深入React-Router讲讲他的源码,套路还是一样的,我们先用官方的API实现一个简单的例子,然后自己手写这些API来替换官方的并且保持功能不变。 本文全部代码已经上传GitHub,大家可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-router-code 简单示例 本文用的例子是上篇文章开始那个不带鉴权的简单路由跳转例子,跑起来是这样子的: 我们再来回顾下代码,在app.js里面我们用Route组件渲染了几个路由: import React from 'react'; import { BrowserRouter as Router, Switch, Route, } from "react-router-dom"; import Home from './pages/Home'; import Login from './pages/Login'; import Backend from './pages/Backend'; import Admin from './pages/Admin'; function App() { return ( <router> <switch> <route path="/login" component="{Login}/"> <route path="/backend" component="{Backend}/"> <route path="/admin" component="{Admin}/"> <route path="/" component="{Home}/"> </route></route></route></route></switch> </router> ); } export default App; 每个页面的代码都很简单,只有一个标题和回首页的链接,比如登录页长这样,其他几个页面类似: import React from 'react'; import { Link } from 'react-router-dom'; function Login() { return ( <> <h1>登录页</h1> <Link to="/">回首页</Link> </> ); } export default Login; 这样我们就完成了一个最简单的React-Router的应用示例,我们来分析下我们用到了他的哪些API,这些API就是我们今天要手写的目标,仔细一看,我们好像只用到了几个组件,这几个组件都是从react-router-dom导出来的: BrowserRouter: 被我们重命名为了Router,他包裹了整个React-Router应用,感觉跟以前写过的react-redux的Provider类似,我猜是用来注入context之类的。 Route: 这个组件是用来定义具体的路由的,接收路由地址path和对应渲染的组件作为参数。 Switch:这个组件是用来设置匹配模式的,不加这个的话,如果浏览器地址匹配到了多个路由,这几个路由都会渲染出来,加了这个只会渲染匹配的第一个路由组件。 Link:这个是用来添加跳转链接的,功能类似于原生的a标签,我猜他里面也是封装了一个a标签。 BrowserRouter源码 我们代码里面最外层的就是BrowserRouter,我们先去看看他的源码干了啥,地址传送门:https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/BrowserRouter.js 看了他的源码,我们发现BrowserRouter代码很简单,只是一个壳: import React from "react"; import { Router } from "react-router"; import { createBrowserHistory as createHistory } from "history"; class BrowserRouter extends React.Component { history = createHistory(this.props); render() { return <router history="{this.history}" children="{this.props.children}" />; } } 在这个壳里面还引用了两个库react-router和history,BrowserRouter仅仅是调用history的createHistory得到一个history对象,然后用这个对象渲染了react-router的Router组件。看起来我们要搞懂react-router-dom的源码还必须得去看react-router和history的源码,现在我们手上有好几个需要搞懂的库了,为了看懂他们的源码,我们得先理清楚他们的结构关系。 React-Router的项目结构 React-Router的结构是一个典型的monorepo,monorepo这两年开始流行了,是一种比较新的多项目管理方式,与之相对的是传统的multi-repo。比如React-Router的项目结构是这样的: 注意这里的packages文件夹下面有四个文件夹,这四个文件夹每个都可以作为一个单独的项目发布。之所以把他们放在一起,是因为他们之前有很强的依赖关系: react-router:是React-Router的核心库,处理一些共用的逻辑 react-router-config:是React-Router的配置处理,我们一般不需要使用 react-router-dom:浏览器上使用的库,会引用react-router核心库 react-router-native:支持React-Native的路由库,也会引用react-router核心库 像这样多个仓库,发布多个包的情况,传统模式是给每个库都建一个git repo,这种方式被称为multi-repo。像React-Router这样将多个库放在同一个git repo里面的就是monorepo。这样做的好处是如果出了一个BUG或者加一个新功能,需要同时改react-router和react-router-dom,monorepo只需要一个commit一次性就改好了,发布也可以一起发布。如果是multi-repo则需要修改两个repo,然后分别发布两个repo,发布的时候还要协调两个repo之间的依赖关系。所以现在很多开源库都使用monorepo来将依赖很强的模块放在一个repo里面,比如React源码也是一个典型的monorepo。 yarn有一个workspaces可以支持monorepo,使用这个功能需要在package.json里面配置workspaces,比如这样: "workspaces": { "packages": [ "packages/*" ] } 扯远了,monorepo可以后面单独开一篇文章来讲,这里讲这个主要是为了说明React-Router分拆成了多个包,这些包之间是有比较强的依赖的。 前面我们还用了一个库是history,这个库没在React-Router的monorepo里面,而是单独的一个库,因为官方把他写的功能很独立了,不一定非要结合React-Router使用,在其他地方也可以使用。 React-Router架构思路 我之前另一篇文章讲Vue-Router的原理提到过,前端路由实现无非这几个关键点: 监听URL的改变 改变vue-router里面的current变量 监视current变量 获取对应的组件 render新组件 其实React-Router的思路也是类似的,只是React-Router将这些功能拆分得更散,监听URL变化独立成了history库,vue-router里面的current变量在React里面是用Context API实现的,而且放到了核心库react-router里面,一些跟平台相关的组件则放到了对应的平台库react-router-dom或者react-router-native里面。按照这个思路,我们自己写的React-Router文件夹下面也建几个对应的文件夹: 手写自己的React-Router 然后我们顺着这个思路一步一步的将我们代码里面用到的API替换成自己的。 BrowserRouter组件 BrowserRouter这个代码前面看过,直接抄过来就行: import React from "react"; import { Router } from "react-router"; import { createBrowserHistory as createHistory } from "history"; class BrowserRouter extends React.Component { history = createHistory(this.props); render() { return <router history="{this.history}" children="{this.props.children}" />; } } export default BrowserRouter; react-router的Router组件 上面的BrowserRouter用到了react-router的Router组件,这个组件在浏览器和React-Native端都有使用,主要获取当前路由并通过Context API将它传递下去: import React from "react"; import HistoryContext from "./HistoryContext.js"; import RouterContext from "./RouterContext.js"; /** * The public API for putting history on context. */ class Router extends React.Component { // 静态方法,检测当前路由是否匹配 static computeRootMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; } constructor(props) { super(props); this.state = { location: props.history.location // 将history的location挂载到state上 }; // 下面两个变量是防御性代码,防止根组件还没渲染location就变了 // 如果location变化时,当前根组件还没渲染出来,就先记下他,等当前组件mount了再设置到state上 this._isMounted = false; this._pendingLocation = null; // 通过history监听路由变化,变化的时候,改变state上的location this.unlisten = props.history.listen(location => { if (this._isMounted) { this.setState({ location }); } else { this._pendingLocation = location; } }); } componentDidMount() { this._isMounted = true; if (this._pendingLocation) { this.setState({ location: this._pendingLocation }); } } componentWillUnmount() { if (this.unlisten) { this.unlisten(); this._isMounted = false; this._pendingLocation = null; } } render() { // render的内容很简单,就是两个context // 一个是路由的相关属性,包括history和location等 // 一个只包含history信息,同时将子组件通过children渲染出来 return ( <routercontext.provider value="{{" history: this.props.history, location: this.state.location, match: router.computerootmatch(this.state.location.pathname), }}> <historycontext.provider children="{this.props.children" || null} value="{this.props.history}" /> </routercontext.provider> ); } } export default Router; 上述代码是我精简过的代码,原版代码可以看这里。这段代码主要是创建了两个context,将路由信息和history信息放到了这两个context上,其他也没干啥了。关于React的Context API我在另外一篇文章详细讲过,这里不再赘述了。 history 前面我们其实用到了history的三个API: createBrowserHistory: 这个是用在BrowserRouter里面的,用来创建一个history对象,后面的listen和unlisten都是挂载在这个API的返回对象上面的。 history.listen:这个是用在Router组件里面的,用来监听路由变化。 history.unlisten:这个也是在Router组件里面用的,是listen方法的返回值,用来在清理的时候取消监听的。 下面我们来实现这个history: // 创建和管理listeners的方法 function createEvents() { let handlers = []; return { push(fn) { handlers.push(fn); return function () { handlers = handlers.filter(handler => handler !== fn); }; }, call(arg) { handlers.forEach(fn => fn &amp;&amp; fn(arg)); } } } function createBrowserHistory() { const listeners = createEvents(); let location = { pathname: '/', }; // 路由变化时的回调 const handlePop = function () { const currentLocation = { pathname: window.location.pathname } listeners.call(currentLocation); // 路由变化时执行回调 } // 监听popstate事件 // 注意pushState和replaceState并不会触发popstate // 但是浏览器的前进后退会触发popstate // 我们这里监听这个事件是为了处理浏览器的前进后退 window.addEventListener('popstate', handlePop); // 返回的history上有个listen方法 const history = { listen(listener) { return listeners.push(listener); }, location } return history; } export default createBrowserHistory; 上述history代码是超级精简版的代码,官方源码很多,还支持其他功能,我们这里只拎出来核心功能,对官方源码感兴趣的看这里:https://github.com/ReactTraining/history/blob/28c89f4091ae9e1b0001341ea60c629674e83627/packages/history/index.ts#L397 Route组件 我们前面的应用里面还有个很重要的组件是Route组件,这个组件是用来匹配路由和具体的组件的。这个组件看似是从react-router-dom里面导出来的,其实他只是相当于做了一个转发,原封不动的返回了react-router的Route组件: 这个组件其实只有一个作用,就是将参数上的path拿来跟当前的location做对比,如果匹配上了就渲染参数上的component就行。为了匹配path和location,还需要一个辅助方法matchPath,我直接从源码抄这个方法了。大致思路是将我们传入的参数path转成一个正则,然后用这个正则去匹配当前的pathname: import pathToRegexp from "path-to-regexp"; const cache = {}; const cacheLimit = 10000; let cacheCount = 0; function compilePath(path, options) { const cacheKey = `${options.end}${options.strict}${options.sensitive}`; const pathCache = cache[cacheKey] || (cache[cacheKey] = {}); if (pathCache[path]) return pathCache[path]; const keys = []; const regexp = pathToRegexp(path, keys, options); const result = { regexp, keys }; if (cacheCount &lt; cacheLimit) { pathCache[path] = result; cacheCount++; } return result; } /** * Public API for matching a URL pathname to a path. */ function matchPath(pathname, options = {}) { if (typeof options === "string" || Array.isArray(options)) { options = { path: options }; } const { path, exact = false, strict = false, sensitive = false } = options; const paths = [].concat(path); return paths.reduce((matched, path) => { if (!path &amp;&amp; path !== "") return null; if (matched) return matched; const { regexp, keys } = compilePath(path, { end: exact, strict, sensitive }); const match = regexp.exec(pathname); if (!match) return null; const [url, ...values] = match; const isExact = pathname === url; if (exact &amp;&amp; !isExact) return null; return { path, // the path used to match url: path === "/" &amp;&amp; url === "" ? "/" : url, // the matched portion of the URL isExact, // whether or not we matched exactly params: keys.reduce((memo, key, index) => { memo[key.name] = values[index]; return memo; }, {}) }; }, null); } export default matchPath; 然后是Route组件,调用下matchPath来看下当前路由是否匹配就行了,当前路由记得从RouterContext里面拿: import React from "react"; import RouterContext from "./RouterContext.js"; import matchPath from "./matchPath.js"; /** * The public API for matching a single path and rendering. */ class Route extends React.Component { render() { return ( <routercontext.consumer> {context => { // 从RouterContext获取location const location = context.location; const match = matchPath(location.pathname, this.props); // 调用matchPath检测当前路由是否匹配 const props = { ...context, location, match }; let { component } = this.props; // render对应的component之前先用最新的参数match更新下RouterContext // 这样下层嵌套的Route可以拿到对的值 return ( <routercontext.provider value="{props}"> {props.match ? React.createElement(component, props) : null} </routercontext.provider> ); }} </routercontext.consumer> ); } } export default Route; 上述代码也是精简过的,官方源码还支持函数组件和render方法等,具体代码可以看这里:https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Route.js 其实到这里,React-Router的核心功能已经实现了,但是我们开始的例子中还用到了Switch和Link组件,我们也一起来把它实现了吧。 Switch组件 我们上面的Route组件的功能是只要path匹配上当前路由就渲染组件,也就意味着如果多个Route的path都匹配上了当前路由,这几个组件都会渲染。所以Switch组件的功能只有一个,就是即使多个Route的path都匹配上了当前路由,也只渲染第一个匹配上的组件。要实现这个功能其实也不难,把Switch的children拿出来循环,找出第一个匹配的child,给它添加一个标记属性computedMatch,顺便把其他的child全部干掉,然后修改下Route的渲染逻辑,先检测computedMatch,如果没有这个再使用matchPath自己去匹配: import React from "react"; import RouterContext from "./RouterContext.js"; import matchPath from "./matchPath.js"; class Switch extends React.Component { render() { return ( <routercontext.consumer> {context => { const location = context.location; // 从RouterContext获取location let element, match; // 两个变量记录第一次匹配上的子元素和match属性 // 使用React.Children.forEach来遍历子元素,而不能使用React.Children.toArray().find() // 因为toArray会给每个子元素添加一个key,这会导致两个有同样component,但是不同URL的<route>重复渲染 React.Children.forEach(this.props.children, child => { // 先检测下match是否已经匹配到了 // 如果已经匹配过了,直接跳过 if (!match &amp;&amp; React.isValidElement(child)) { element = child; const path = child.props.path; match = matchPath(location.pathname, { ...child.props, path }); } }); // 最终<switch>组件的返回值只是匹配子元素的一个拷贝,其他子元素被忽略了 // match属性会被塞给拷贝元素的computedMatch // 如果一个都没匹配上,返回null return match ? React.cloneElement(element, { location, computedMatch: match }) : null; }} </switch></route></routercontext.consumer> ); } } export default Switch; 然后修改下Route组件,让他先检查computedMatch: // ... 省略其他代码 ... const match = this.props.computedMatch ? this.props.computedMatch : matchPath(location.pathname, this.props); // 调用matchPath检测当前路由是否匹配 Switch组件其实也是在react-router里面,源码跟我们上面写的差不多:https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Switch.js Link组件 Link组件功能也很简单,就是一个跳转,浏览器上要实现一个跳转,可以用a标签,但是如果直接使用a标签可能会导致页面刷新,所以不能直接使用它,而应该使用history API,history API具体文档可以看这里。我们这里要跳转URL可以直接使用history.pushState。使用history.pushState需要注意一下几点: history.pushState只会改变history状态,不会刷新页面。换句话说就是你用了这个API,你会看到浏览器地址栏的地址变化了,但是页面并没有变化。 当你使用history.pushState或者history.replaceState改变history状态的时候,popstate事件并不会触发,所以history里面的回调不会自动调用,当用户使用history.push的时候我们需要手动调用回调函数。 history.pushState(state, title[, url])接收三个参数,第一个参数state是往新路由传递的信息,可以为空,官方React-Router会往里面加一个随机的key和其他信息,我们这里直接为空吧,第二个参数title目前大多数浏览器都不支持,可以直接给个空字符串,第三个参数url是可选的,是我们这里的关键,这个参数是要跳往的目标地址。 由于history已经成为了一个独立的库,所以我们应该将history.pushState相关处理加到history库里面。 我们先在history里面新加一个APIpush,这个API会调用history.pushState并手动执行回调: // ... 省略其他代码 ... push(url) { const history = window.history; // 这里pushState并不会触发popstate // 但是我们仍然要这样做,是为了保持state栈的一致性 history.pushState(null, '', url); // 由于push并不触发popstate,我们需要手动调用回调函数 location = { pathname: url }; listeners.call(location); } 上面说了我们直接使用a标签会导致页面刷新,但是如果不使用a标签,Link组件应该渲染个什么标签在页面上呢?可以随便渲染个span,div什么的都行,但是可能会跟大家平时的习惯不一样,还可能导致一些样式失效,所以官方还是选择了渲染一个a标签在这里,只是使用event.preventDefault禁止了默认行为,然后用history api自己实现了跳转,当然你可以自己传component参数进去改变默认的a标签。因为是a标签,不能兼容native,所以Link组件其实是在react-router-dom这个包里面: import React from "react"; import RouterContext from "../react-router/RouterContext"; // LinkAnchor只是渲染了一个没有默认行为的a标签 // 跳转行为由传进来的navigate实现 function LinkAnchor({navigate, ...rest}) { let props = { ...rest, onClick: event => { event.preventDefault(); navigate(); } } return <a {...props}></a>; } function Link({ component = LinkAnchor, // component默认是LinkAnchor to, ...rest }) { return ( <routercontext.consumer> {context => { const { history } = context; // 从RouterContext获取history对象 const props = { ...rest, href: to, navigate() { history.push(to); } }; return React.createElement(component, props); }} </routercontext.consumer> ); } export default Link; 上述代码是精简版的Link,基本逻辑跟官方源码一样:https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js 到这里开头示例用到的全部API都换成了我们自己的,其实也实现了React-Router的核心功能。但是我们只实现了H5 history模式,hash模式并没有实现,其实有了这个架子,添加hash模式也比较简单了,基本架子不变,在react-router-dom里面添加一个HashRouter,他的基本结构跟BrowserRouter是一样的,只是他会调用history的createHashHistory,createHashHistory里面不仅仅会去监听popstate,某些浏览器在hash变化的时候不会触发popstate,所以还需要监听hashchange事件。对应的源码如下,大家可以自行阅读: HashRouter: https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/HashRouter.js createHashHistory: https://github.com/ReactTraining/history/blob/28c89f4091ae9e1b0001341ea60c629674e83627/packages/history/index.ts#L616 总结 React-Router的核心源码我们已经读完了,下面我们来总结下: React-Router因为有跨平台的需求,所以分拆了好几个包,这几个包采用monorepo的方式管理: react-router是核心包,包含了大部分逻辑和组件,处理context和路由匹配都在这里。 react-router-dom是浏览器使用的包,像Link这样需要渲染具体的a标签的组件就在这里。 react-router-native是react-native使用的包,里面包含了android和ios具体的项目。 浏览器事件监听也单独独立成了一个包history,跟history相关的处理都放在了这里,比如push,replace什么的。 React-Router实现时核心逻辑如下: 使用不刷新的路由API,比如history或者hash 提供一个事件处理机制,让React组件可以监听路由变化。 提供操作路由的接口,当路由变化时,通过事件回调通知React。 当路由事件触发时,将变化的路由写入到React的响应式数据上,也就是将这个值写到根router的state上,然后通过context传给子组件。 具体渲染时将路由配置的path和当前浏览器地址做一个对比,匹配上就渲染对应的组件。 在使用popstate时需要注意: 原生history.pushState和history.replaceState并不会触发popstate,要通知React需要我们手动调用回调函数。 浏览器的前进后退按钮会触发popstate事件,所以我们还是要监听popstate,目的是兼容前进后退按钮。 本文全部代码已经上传GitHub,大家可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-router-code 参考资料 官方文档:https://reactrouter.com/web/guides/quick-start GitHub源码地址:https://github.com/ReactTraining/react-router/tree/master/packages 文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。 “前端进阶知识”系列文章及示例源码: https://github.com/dennis-jiang/Front-End-Knowledges 欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

优秀的个人博客,低调大师

这个 bug 让我更加理解 Spring 单例了

我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农! 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。 谁还没在 Spring 里栽过跟头呢,从哪儿跌倒,就从哪儿睡一会儿,然后再爬起来。 讲点儿武德 这是由一个真实的 bug 引起的,bug 产生的原因就是忽略了 Spring Bean 的单例模式。来,先看一段简单的代码。 public class TestService { private String callback = "https://ip.com/token={token}"; public String getCallback() { Random random = new Random(); int number = random.nextInt(100); System.out.println("本次随机数为:" + number); callback = callback.replace("{token}", String.valueOf(number)); return callback; } public static void main(String[] args) { TestService testService = new TestService(); while (true) { Scanner reader = new Scanner(System.in); int number = reader.nextInt(); if (number > 0) { String url = testService.getCallback(); System.out.println(url); } } } } callback是一个带有一个回调地址,参数 token是不确定的。 getCallback方法每次调用,会随机生成一个100以内的数字,然后将 callback中的{token}替换为这个随机数字,最后的格式就像这样的: https://ip.com/token=88 然后在 main方法中接收控制台输入,每次输入的数字大于0,调用 getCallback方法,然后输出 url。 相信各位都能轻易的看出这段程序的输出。 执行程序之后,不管你输入多少次数字,最后输出的 callback都是第一次的那个。 虽然每次生成的随机数都变了,但是 callback没变。 其实就是单例 有同学说,你过分了啊,这我能不知道为啥吗? main方法只创建了一个TestService实例,在第一次调用 getCallback方法的时候,callback这个字符串就被修改成 https://ip.com/token=89了,所以,之后不管你再调用多少次,都不会执行 replace动作了,因为 callback中已经没有 {token}这一段了。 TestService 在整个程序执行过程中就是一个单例,所以,在 callback第一次被修改后,后面再执行 callback.replace("{token}", String.valueOf(number)); 的动作,拿到的 callback中就已经没有 {token}了,所以说,不会有替换的动作。 当然,这只是用最简单的程序说明单例中的这个问题,真正的项目中想用单例的话,还要借助于单例设计模式实现。 回到那个 bug 有个弟弟在做微信服务号的开发,微信服务号或者订阅号中有个 access_token的概念,这是所有请求的凭证,有效期 2 个小时,到期之前要进行刷新。 他是这样设计的,在项目启动的时候立即调用微信接口获取 access_token,然后写了一个定时任务每1个小时刷新一次,获取来的 access_token放到 redis 和 数据库中,当调用微信服务号其他接口的时候,在 redis 中获取 access_token并拼接到接口地址中。 开发调试的时候一起顺利,看上去非常完美。 问题出现了 当项目部署到测试环境测试的时候,问题出现了。项目刚发版的时候,测试都正常,但是过一段时间,就会出现错误,查看日志的时候,发现是微信服务号的接口返回了错误码,意思就是 access_token已过期,需要重新获取。 弟弟第一时间怀疑是定时任务出现了问题,但是通过日志和数据库中的更新时间,发现定时任务是完全没有问题的,刷新 access_token的时间和定时任务是完全吻合的,说明已经及时刷新了。 我让他用 redis 或数据库中的access_token去调一下服务号接口,看看是不是也有同样的过期问题。 结果一试,redis 中存的是没问题的,可以正常使用。 那彻底排除是定时任务的问题了,问题的症结应该就出在两个地方: 1、在获取 redis 中的access_token的过程; 2、将获取到的 access_token拼接到请求接口 URL 上发生了错误; 到这里就很好判断了,他把从 redis 拿到的access_token和最后拼接好的 URL 都输出到日志中一看,果然,两个是不一致的。 从 redis 取出的确实是最新可用的 access_token ,但是拼接到接口 URL 上之后,发现是另外一个。那就确定是拿到的 access_token 是没问题的,但是最后拼接到 URL 却有问题。这时,弟弟仔细检查了代码,然后彻底蒙了。 讲点武德 既然问题出在哪儿已经确定了,那就分析那段代码就好了。 项目整体采用的是 Spring Boot,代码很简单,就是在一个 Controller 中调用 Service 中的一个方法。大致 demo 是这样的。 @RestController @RequestMapping(value = "test") public class TestController { @Autowired private TestService testService; @GetMapping(value = "call") public Object getCallback() { return testService.getCallback(); } } @Service public class TestService { private String callback = "https://ip.com/token={token}"; public String getCallback() { Random random = new Random(); int number = random.nextInt(100); System.out.println("本次随机数为:" + number); callback = callback.replace("{token}", String.valueOf(number)); return callback; } } 看到这里,各位肯定已经发现问题原因了。虽然有多次请求,但因为 Spring Bean 默认是单例模式,所以实际上和前面演示的那个控制台程序是类似的,从头到尾都只有一个 TestService 实例,所以只有第一次能将{token}替换成真正的access_token。 对应到实际的服务号场景中,在第一次调用这个接口时,从 redis 拿到 access_token拼接到具体的 URL中是没问题的,但是一旦这个access_token过期(1小时后),再次请求这个接口就会出现 access_token过期的问题。 这里违反了 Spring 单例模式的一个点,那就是 Spring 单例模式,不适合存储有状态的值,比如这里的 callback就是个有状态的值,它应该随着定时任务的进行,获取到不同的值。 关于 Spring 或 Spring Boot 工作流程的介绍可以阅读文末的两篇文章,其中包括 Bean 实例化过程。 修改建议 如何解决这个问题呢? 其实很简单,不让callback每次调用发生变化就可以了,每次拼接 URL 的时候,先将 callback赋给一个局部变量,然后在这个变量上操作就好了。 public String getCallback() { Random random = new Random(); int number = random.nextInt(100); System.out.println("本次随机数为:" + number); String tempCallback = callback; tempCallback = tempCallback.replace("{token}", String.valueOf(number)); return tempCallback; } 另外,说到 Spring 单例模式,Spring 本身还支持其他几种模式,与单例模式对应的就是 prototype模式,这种模式是每个请求都重新生成实例。所以,如果你确定这个 Controller 和 Service 可以不用单例模式,可以加上 @Scope(value = "prototype")注解。 @RestController @RequestMapping(value = "test") @Scope(value = "prototype") public class TestController { @Autowired private TestService testService; @GetMapping(value = "call") public Object getCallback() { return testService.getCallback(); } } @Service @Scope(value = "prototype") public class TestService { private String callback = "https://ip.com/token={token}"; public String getCallback() { Random random = new Random(); int number = random.nextInt(100); System.out.println("本次随机数为:" + number); callback = callback.replace("{token}", String.valueOf(number)); return callback; } } 这样一来,每次都是新的实例,自然就不存在那个问题了。 看完就懂的 Spring IoC 实现过程 从 Spring Boot 出发,分析 Spring IoC 过程 公众号「古时的风筝」,Java 开发者,全栈工程师,bug 杀手,擅长解决问题。 一个兼具深度与广度的程序员鼓励师,本打算写诗却写起了代码的田园码农!坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一起变优秀!

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册