场景

最近参与的项目和使用的多个开发包都是属于一个大项目的一部分,开发过程中为了在本地正常调用接口需要获取登陆人信息,同时不同的人有不同的身份,在某些业务场景下还需要频繁的切换登陆人以使用不同的身份完成对应身份的操作。 公司为了满足开发需求特地挂了个页面用来获取不同身份的人员信息(cookie,该cookie通过响应头返回),开发者直接在页面上点击不同的身份或输入人员id来发起模拟登录请求,随后在浏览器开发者程序中即可获取到返回的cookie信息。 当然这只是cookie的来源,想要在代理接口请求时带上他则需要我们在项目根目录中(当然是为了方便查找啦)创建cookie.txt文件,随后配置代理在请求前读取该文件中的cookie信息并附加到请求头上即可,这样我们便成功消费了cookie。在整个模拟登录过程中,最繁琐的操作就是需要手动复制响应的cookie然后粘贴到项目的cookie.txt文件中。更别说cookie只有一小时有效期和需要频繁切换身份的场景下。

问题

在上述描述的场景中我们能清晰地发现问题的所在,被身份过期和身份切换打断的开发过程十分恼人,我们不想一遍又一遍的重复复制cookie到各个项目文件中的操作。于是我产生了用node脚本简化cookie获取过程的想法。

这是一个过于简单的需求,你大概已经有了实现的思路-通过node发起请求到模拟登录接口,解析响应头部获取信息,将响应信息写入到项目的cookie.txt文件中,事实上我也正是这么做的。不过模拟登录不只开发人员会用,有些非开发人员也会使用,为了使用的友好和体验的一致性,前端模拟登录的页面直接copy自公司页面(好在该页面十分简单没有任何外部引入)。 现在我们的登录系统有了前端页面。

探索

我们需要挂载该页面以便后续使用,我们尽可能地使该登录系统轻量化,所以尽可能地避免使用外部依赖(比如我第一个想法是使用express来启动服务器)。

让我们npm init -y来初始化一个npm包。

创建src文件夹存放我们的源码,将我们复制的前端页面命名为index.html存放其中。新建index.js,在其中编写代码启动我们的端口服务并在/路径下返回我们的前端页面。

访问我们的端口,我们成功看到了我们'登录系统'的首页。

首页上有几个快捷登录的按钮和一个用户id输入框,每个按钮上的人员对应不同的身份,点击快捷按钮可以不需要输入id实现快速登录,也可以在输入框中输入指定用户的id登录。这两个功能对应的其实是同一个接口,只是按钮操作在请求时会带上对应用户的id,在我们的登录系统中,让我们将该接口路径命名为'/login',下面来实现该接口。

我们的node服务目前只有根'/'路径,也就是返回首页。我们需要在node服务中解析请求url来匹配/login路径,在该路径的处理函数中,我们需要解析登录的用户id参数,并根据该参数生成符合公司模拟登录接口的请求头并发送真实的登录请求。注意模拟登录使用的是https协议,所以我们在发送请求时应当使用node的https库。

调试接口解析响应头我们即可获取到需要的cookie,接下来我们要将cookie写入到对应的项目文件中。我们要写入的文件的路径是未知的,考量再三我决定给我们的系统增加个小小的繁琐的配置项--一个用来存放写入文件的地址的.json文件,这意味着用户在使用前需要将各个项目下的cookie.txt地址粘贴到json数组中。

如此一来我们便实现了整个流程,打开我们的登录系统,点击登录对应的cookie就会写入到对应项目的cookie.txt中,你再也不需要复制粘贴cookie了。

对于身份过期,我们的系统没有任何方法知晓项目中的cookie是否过期,因为过期只发生在代理请求时,那可以在代理请求时判断...,侵入性的增加不必要的代码修改代理配置不是任何人想要的,我们还是使用一个简单的方法,通过设置固定时间后自动重新模拟登录来刷新cookie。

当然上面仍然只是粗略的描述了功能的实现方法,在实际开发过程中还需要更多的关注错误捕获和异步处理。

扩展

上述功能使用了一个月后,我发现有些时候需要获取某一群体或者只知道名字不知道id的人员cookie,但是我们的系统只能通过id登录而不支持搜索功能。于是我萌生了将同事开发的人员选择器挂载到模拟登录页面上的想法。 该选择器基于antd封装,可以搜索人员和分类查询过滤正好满足我的需求。要挂载到页面上需要将该组件打包成独立的js文件库通过script引入挂载(受限于我们的index.html文件)。 不巧的是组件使用的打包程序是公司自研的我不太会配置(更不巧的连文档也没有),所以我决定把源码复制出来然后通过webpack打包。

运行打包组件

我之前写过从头配置一个react开发环境的文章,这个配置好的环境我修改了下让其变得更加通用后推送到的gitee,这里我们直接克隆一份环境用来打包该组件。

复制组件的源码到项目中,复制组件的package.json下的相关依赖信息替换项目的依赖信息,安装依赖启动项目。然后理所当然的出现了一堆报错。

我这里的报错分为两类,ts语法报错和依赖版本报错。项目使用的react-dom版本较新和组件的不同,所以替换依赖安装后需要更换调用方法;ts的语法报错全部来自antd组件的声明文件,鉴于打包只是开发的中转站并且解决依赖报错后项目能正常运行所以我最终决定忽略这些报错,在webpack的配置文件中为ts-loader增加如下配置来忽略ts类型检查:

options: {
          // 忽略TypeScript的类型检查错误
          transpileOnly: true
        }

组件挂载与渲染

直接导出组件是无法运行的,我们还得考虑如何渲染并挂载组件的问题。我们可以通过cdn的方式导入react相关依赖,这样组件在浏览器中加载完毕后在全局环境下执行react的渲染挂载。这是比较合适的方案。

不过这里我是将组件挂载代码放置在在项目内部,这样导出的函数直接运行即可,不需要在组件在页面上加载后执行额外的挂载代码。为此我们需要额外引入react-dom包。

打包

组件调试没问题后(当然由于配置了相对路径,接口是肯定调不通的)即可打包,webpack配置output.libraryTarget="umd",即可run build。

信息

我们这里来选择将所有依赖项打包,包括react和react-dom等,所以包会较大,这是一种偷懒的做法,正常情况下react及其相关依赖会被作为外部依赖externals

模拟内容分发

复制打包文件到我们的登录系统中并在index.html中导入。我们的资源包其实是存放在我们本地的,所以我们需要在我们的小服务器上实现一个简单的静态内容分发,这里我们新增一个/assets路径用来处理所有的静态文件请求, 在处理中我们需要解析请求的路径并加载找到,读取对应的本地文件随后响应到前端。只不过当前我们的静态资源只有一个打包后的组件。

现在重新启动我们的登录系统,组件包加载成功,我们的组件成功渲染到了页面上。

组件请求代理

组件内部的请求是相对地址,故而我们需要将这些请求代理到正确的地址以获取数据。具体的实现和最开始获取cookie的方式大同小异,拦截到对应前缀的接口后请求正确的地址并返回数据。

重新启动项目,测试组件接口是否正常,完工。

浏览器插件开发

我们上边的开发只适用于本地项目服务(当然对于本地开发人员来说完全能满足需求了),不过一般测试人员的测试工作或者项目演示都会在公司的开发环境下进行,我们的本地模拟服务无法引用于这些环境。 碰巧某天听到同事提到浏览器插件,我便有了用插件来重新构建模拟登录的想法。本人之前并没有开发浏览器开发的经验,这里基于MDN的文档来进行开发.

初始化插件包

得益于ChatGpt,某些概念的理解和api的调用没有太过困难,我们参照文档创建好Manifest.json并进行必要的配置,我们就已经完成了插件的初始化。 接下来我们理下插件功能和功能点。

插件设计

模拟登录作为一个插件提供的功能应该小巧易用,所以当插件安装运行后,我们通过url来匹配公司环境的域名,并在这些域名的页面中注入一个悬浮按钮,hover该按钮会弹出人员登录按钮和刷新按钮。前者点击后弹出modal可以搜索人员并登录到对应的身份,刷新按钮用于登录过期后(身份有效期一小时)重新登录到上一次的登陆人。

在页面上注入我们的组件

开发组件我们任然选择react和antd,打包工具是我上面用到的webpack工具。首先要明确的是如何挂载该组件,我们并不知道每个插件页面的具体结构,所以我们直接选择将该组件挂载到document.body上。在我们上面的webpack项目中,Select组件同级下新建FloatButton文件夹用来存放我们最终封装好的组件。 导入我们需要的antd组件(Modal,FloatButton,Button,Message,Select等)。在我们的Modal中放入Select组件和Button登录按钮,当用户打开该Modal时,可以在Select中输入想要登录的人名或者工号。并在搜索结果中选中想要登录的对象后即可点击登录按钮登录。搜索的接口公司有现成的,我这里直接解析输入参数调用并解析回填到Select中即可。当登陆时,通过fetch调用公司的模拟登录地址,响应头中的set-cookie响应头会让浏览器帮我们设置好cookie。

要想实现刷新功能,我们需要记录下用户上次登录的人的工号,这里我们存在浏览器本地存储中,当用户登录成功时写入到本地存储,当用户刷新时读取该信息并请求登录接口。

完成业务逻辑后,我们在组件入口处使用react-dom将该组件渲染到body上并导出。打包并在Manifest.json的"content_scripts"字段下导入bundle后即可。

最后登录成功时我们可以帮助用户刷新页面,完工。