之前学习的时候,都会看到网络上一些在线工具的网站,比如获取时间戳,编码转化,正则表达式等等,这些对于一些初学者在电脑上没有安装相对应的工具,往往就会百度搜索在线工具,然后就能实现自己所要完成的目的。
这些工具很是方便,或多或少都会用到。但一些在线工具并不能满足我日常开发中的个别需求,于是乎我也准备自己写一个这样的在线工具网站。不过这里肯定不会像其他网站那种搞得花里胡哨的,同时也不会去重复的造轮子,这样很没有意义。
顺便也记录下这类工具的编写以及用途
技术栈
有一段时间没怎么写 React 了,同时对 mui 组件库感兴趣,于是这次的使用 React 和 material 设计风格的组件库来进行编写,使用到 create-react-app 脚手架进行开发,同时使用 TypeScript 进行编写。
该纯前端,无任何后端交互部分。
不做代码分析与演示,可自行在网站上测试与 clone 源码进行查看。
功能
查询字符串与 json 转化
如果是在协议复现的话,有些包的请求体是通过查询字符串来拼接的,也就是协议头Content-Type:application/x-www-form-urlencoded
,请求体如 username=kuizuo&password=a123456
,一般情况下都不会采取字符串来进行替换,而是转成 json 格式,如 { "username":"kuizuo","password","a123456"}
然后使用一些库(querystring)将 json 转成查询字符串的形式。
但是编写代码的时候,需要把抓包得到的查询字符串转成 json,而这也就是这部分的主要功能。
不过 js 本身是不支持 gbk 编码的,遇到使用 gbk 进行 url 编码的网站就需要自定义先将原文本进行 gbk 编码或解码然后才进行转化操作。
Cookie 与 json 转化
和查询字符串与 json 转化功能类似,只不过是将 cookie 文本与 json 互转。
URL UTF8 与 GBK 编码
有些国内网站上可能会使用 GBK 编码,但大多数语言默认都是 UTF8 编码,如果编码格式错误,后端接收到的请求必定失败。在这里我是使用到gbk-nice这个库,可以达到 gbk 版的encodeURIComponent
与decodeURIComponent
,与 js 自带的encodeURIComponent
和decodeURIComponent
也就是 gbk 与 utf-8 编码的区别。
网站实现
上面所介绍的都是作为一个工具库的功能,我只是将其封装成一个在线工具使用,并非主要重点。而主要是对一些网站的功能实现,例如复制与下载等等。
react-codemirror
在网页上展示代码,并有代码高亮的功能,首选的组件就是 codemirror 了,也是很多在线工具都使用的,我这里也不例外。
复制
复制的话其实是可以使用 npm 包的,但是之前在写其他项目的时候,看到封装过一个 copyToClipboard 的功能,这里也就是将其拷贝置 utils 下供外部使用。
interface Options {
target?: HTMLElement
}
export function copyTextToClipboard(input: string, { target = document.body }: Options = {}) {
const element = document.createElement('textarea')
const previouslyFocusedElement = document.activeElement
element.value = input
element.setAttribute('readonly', '')
;(element.style as any).contain = 'strict'
element.style.position = 'absolute'
element.style.left = '-9999px'
element.style.fontSize = '12pt'
const selection = document.getSelection()
let originalRange
if (selection && selection.rangeCount > 0) {
originalRange = selection.getRangeAt(0)
}
target.append(element)
element.select()
element.selectionStart = 0
element.selectionEnd = input.length
let isSuccess = false
try {
isSuccess = document.execCommand('copy')
} catch (e: any) {
throw new Error(e)
}
element.remove()
if (originalRange && selection) {
selection.removeAllRanges()
selection.addRange(originalRange)
}
if (previouslyFocusedElement) {
;(previouslyFocusedElement as HTMLElement).focus()
}
return isSuccess
}
主要使用浏览器中的document.execCommand('copy')
下载
在需要这个需求的时候,我一开始是懵的,因为之前我是没有写过原生的浏览器下载事件,都是使用外有已经封装好的的接口直接调用即可,于是这次我也是毫不意外的通过搜索引擎找到了个复制的代码
type BlobPart = BufferSource | Blob | string
export function downloadByData(data: BlobPart, filename: string, mime?: string, bom?: BlobPart) {
const blobData = typeof bom !== 'undefined' ? [bom, data] : [data]
const blob = new Blob(blobData, { type: mime || 'application/octet-stream' })
const blobURL = window.URL.createObjectURL(blob)
const tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}
原理的话是创建Blob对象(类文件对象),将数据写入,然后创建一个 a 标签(隐藏任何样式),然后并点击创建后的 a 标签,最终移除 a 标签,已达到类似点击下载按钮来下载文件的目的。
主题切换
我一开始实现这个功能是想使用自定义 hooks 的,但是在我编写的过程中,发现切换主题的组件与 codemirror 展示的组件,并不属于在一个组件内。也就是说,我如果写了个 useTheme(实际上我也真写了),我相当于在这两个组件内都使用了独立的状态,互不影响,也就是我点击了切换主题的按钮,但影响不到展示组件的代码。也算是加深了我对 hooks 的理解。
然后我就在想 Vue 的话是如何实现主题切换的,然后翻看了一些 vue 相关的代码,不出所料,使用到全局状态管理,也就是 Store。react 状态管理有 redux,还有官方提供的 useReducer,但我感觉都太繁琐了,于是我另寻其路。
我博客不是就是用 React 写的吗,我直接看源码是如何实现的,发现使用到了 React 的 useContext,也就是接下来我所要写的。
useContext
首先要明确的是,theme 的状态应该是放在全局配置或者说最顶层的组件(当做父组件),然后子组件接收父组件的相关数据进行重新渲染组件。如果只是父子两层之间的关系还相对简单些,直接通过 props 传参即可,但是对于大部分组件关系,基本都是祖孙级别的关系,所以也就有了 Context。
并且 官方文档 中也是用主题切换作为 context 作为演示例子。而对于应用程序中许多组件都需要的属性,Context 无法是一个很好的选择。
首先我创建了一个 Context
import { createContext } from 'react'
export const ThemeContext = createContext<any>({})
然后在顶层组件中使用(Demo 仅作为演示),其中 value 需要是需要传入给子孙组件所要使用的状态或函数,比方这里 theme 和 toggleTheme。
import { ThemeContext } from '../contexts/theme-context'
;<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Demo></Demo>
</ThemeContext.Provider>
在子孙组件的话就可以使用 useContext(ThemeContext)
来获取父级传入的数据,在这里也就是 { theme, toggleTheme }
import { ThemeContext } from '../contexts/theme-context'
export default function SwitchTheme() {
const { theme, toggleTheme } = useContext(ThemeContext)
// ...
}
接着就是切换主题的按钮点击,然后更改 theme,就会渲染对应的组件,至此切换主题的功能也就完成了
keep-alive
在使用 mui 的 Tabs 组件库时,来回切换 Tab 会导致上一个页面的组件重新渲染,状态返回初始状态。这肯定不是我所希望的,由于我之前又有接触过 Vue,所以自然而然就联想到 keep-alive,然后我就去搜索 react 的 keep-alive 解决方案,找到了个react-activation,并解决了的需求。
想做到的目的就是能把 Demo 组件缓存起来,像下面这样的写法
<KeepAlive>
<Demo></Demo>
</KeepAlive>
由于我使用的是 React17+,所以需要使用放置<AliveScope>
外层,也就是上面的代码要改写成
<AliveScope>
<KeepAlive>
<Demo></Demo>
</KeepAlive>
</AliveScope>
不过要注意: 当与react-router
或react-redux
一起使用时,你需要将<AliveScope>
放在<Router>
或<Provider>
内
这些在官方文档中也有介绍,这里就不细说了。
Webpack5 配置 Node 相关库
由于使用的 create-react-app,就使用到了 Webpack5,但是 Webpack5 是不支持 Node 自带的一些库,例如我所需要使用到的 Buffer,所以就需要 npm run eject 把 webpack 配置暴露出来。
如果是要配置 Node 相关库是有一个插件 NodePolyfillPlugin,将会把 Node 的系统库的函数注册到 webpack 中供前端使用,相对简单,而且方便。但相对打包体积肯定有所提升,这里只是提提。
在 webpack5 官方有个测试 兼容性 的,就有提到 Buffer: false
module.exports = {
// ...
node: {
Buffer: false,
process: false,
},
}
要解决 Buffer 也比较简单,直接通过如下代码即可
module.exports = {
// ...
plugins: {
...new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
},
}
也有可能需要在 resolve 中设置下 fallback
resolve: {
fallback: {
buffer: require.resolve('buffer')
}
}
总结
实际编写过程其实并不轻松,但是好在很多坑与开发难点都有前人的遇到,所以搜索就已经帮我解决了很多问题。不过这期间也是不断的尝试,编译,最终实现结果是符合我一开始的期望的。
不过目前所编写的几个功能主要为我目前写协议复现所需要的,而对于大部分像时间戳,正则,加解密相关这些外面都有现成的,何必又要花费时间去造轮子呢。
后续的话应该还是会添加一些额外的功能,例如搞个代码框的配置页面,可供选择语言,代码框的高度,可视化表格的增删改。同时对代码进行一定的重构,对一些组件复用,已经完善持久化的配置。
最主要还是希望能成为我日常开发中常用的在线工具类,而不是简简单单 Demo 学习。