历时百日,我用TypeScript重构了5W Star的JavaScript开源项目,从零基础到精通,收藏这篇就够了!

.markdown-body pre,.markdown-body pre>code.hljs{color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

在Web上展示PDF,这是一个很普遍的需求。但是长久以来,社区中一直缺乏一个好用的库来帮助开发者做好这件事。pdf.js能够实现展示PDF的需求,但是在集成进业务系统这件事上,却显得有些力不从心。它其实还是有一些问题的:

  • 接入pdf.js很容易,但是想和后端整合,却不太十分方便。
  • PDF阅读器的基本功能都具备,但是开发者却难通过代码控制这些功能。
  • 源代码较为复杂,不易阅读、修改和扩展,更不要谈进行定制了。
  • 缺乏强有力的文档的支撑,官方文档不够清晰,社区文档深度广度都不够。

随着时代不断地向前发展,一些问题正变得越来越棘手。我曾帮助过好几位客户解决他们对pdf.js较为一些深入的需求。比如PDF分页加载、批注的持久化、定制批注的开发、疑难BUG的定位等。

在和他们沟通了解的过程中,我意识到他们所面临的大部分问题,都是具备一定共性的通用问题。从技术上来看,是可以通过代码的复用来实现的。至于剩下一小部分的个性问题,则是可以通过插件和定制代码的方式来解决。

于是我决定基于pdf.js开发一个进阶的版本。并设定了两个基础的目标,第一个目标是加强API和文档这两个方面,让用户能够丝滑的将pdf.js接入到自己的系统。前端开发者能够使用API来操作PDF阅读器,后端开发者能够持久化PDF阅读器中各式各样的信息。第二个目标是针对性的开发一些更实用的功能,比如更丰富的批注系统、PDF权限管理、图片文本的提取、定制化的水印等。

但是当我试图去做这件事的时候,我发现这件事不仅难度非常大,而且效果也很差。我使用工具扫描了pdf.js的源码,发现它的代码量其实比我想象的要更多。如果空行和注释计算在内,它的总代码量大约在20万行这个级别。即使去除掉空行和注释,总代码量也有十多万行。起初,我改了两三个礼拜,遇到的问题远比我一开始估计的要多很多。在现有代码的基础上,即使只是实现一些最基本的目标,都十分的困难。比如我试图将PDF阅读器由上下滑动的风格,改造成翻页的风格时,我发现不仅需要改动大量的代码,而且这样的改动还会影响到其它的功能。并且即使我开发出了这个功能,我也不太好提供自由定制的接口和API,甚至源代码中函数参数这些信息,都难以以一种方便的方式进行告知愿意阅读代码的人。

初步尝试过后,我发现我无法用简单的方法解决上面提到的那些看似简单的问题——事实上,如果小修小补式的改进就能将这些问题通通化解掉,我相信这个活儿,早就被人做掉了。即便不是pdf.js的团队本身,也会有其他活跃在社区的开发者来完成这件事。

于是我准备干一票大的。我打算直接用TypeScript重构pdf.js,保留其PDF处理的核心逻辑,改造其展示PDF相关的代码。相关的工具链与开发风格,也通通都要跟上时代的脚步。我决定要放弃npm,移除gulp,用pnpm和vite取而代之。传统的单模块写法也不再使用,monorepo这种多模块的方式是更好的选择。

我之前主攻后端,只是偶尔带着写一写前端,所以对前端的开发理解的不是很深,对前端的生态也知之甚少。但是在重构中大型项目这件事上,我还是有不少经验的。我重构过如仓储物流之类的业务系统,也重构过如知识图谱之类的大数据系统。总的来说,重构的挑战是有的,阵痛期是有的,而且这个阵痛期甚至还可以说并不短。阵痛期期间,新功能的产出是几乎没有的,bug可能是稀奇古怪的,困难是一环接一环的。但是当这阵痛期结束之后,整个项目就都开始柳暗花明了。原来无法定位的bug,现在可以被定位了。原来无法开发的功能,现在可以开发了。原来低下的开发效率,现在也高了起来。原来无法接入的各种主流组件,现在也轻轻松松能接进来了。所以,想要充分发挥pdf.js的潜力,让它能够完美的融入到我们的日常开发当中去,一个彻底的重构,是不可避免的。

pdf.js在接入业务系统这件事上表现的不太好,根源就在于它的定位并不是一个供开发者调用的库,而是一个供使用者查看PDF的工具。pdf.js在做PDF查看工具这件事上,做的是非常的好的。它也被集成到不少的工具当中去了,比如VSCode、IDEA、chrome等。使得这些工具的使用者,能够很轻松的就能够在这些工具上查看PDF。对于普通的计算机用户,它们也可以直接使用chrome来查看pdf,而无须安装任何别的查看工具。相对的,尺有所短寸有所长,pdf.js无法完美的兼顾开发者和使用者。对于开发者来说,只有稍微委屈一点了。能够接入pdf.js,但是接入之后效果却没有那么的好。

我在重构pdf.js这件事上,首先要做的就是重新确立它的定位和目标——它不直接为使用者服务,它为开发者服务。开发者应该可以像下面一样,通过简单的代码打开一个PDF,并通过一系列的配置来对自己的PDF阅读器进行一定的定制,而不是必须通过iframe这样的方式来嵌入。

const viewer = WebSerenViewer.init('app', {
  viewerScale: 0.7
});

viewer.open({
  url: 'compressed.tracemonkey-pldi-09.pdf',
  verbosity: VerbosityLevel.WARNINGS
}).then(() => {
  const controller = viewer.getViewController();
  bindEvents(controller);
})

function bindEvents(controller: WebViewerController) {
  document.getElementById("pdf-page-up")?.addEventListener("click", () => {
    controller.pageUp();
  })
  document.getElementById("pdf-page-down")?.addEventListener("click", () => {
    controller.pageDown();
  })
}


对于单个的功能而言,我要提供的不是具体的按钮或图标,而是开发者能够操作的API、参数、回调和各类事件等。 对于想要调试、定制、贡献源代码的开发者,我要提供的主流的工具链、清晰的参数、明确的调用关系等,确保他们能够一个并不高成本来了解、调试和改动相关代码。

在花了一百多天进行攻坚之后,我在这些事上迈出了关键的几大步。代码所使用到的主要语言、工具已经改造成了TypeScript、pnpm、vite。代码开发风格已经改造成多模块的monorepo:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所有的函数的参数类型都清晰下来了,硬编码都相继被我消灭掉了。整个代码质量已经有了一个相当程度的提升。并且最基础的demo也已经被我调通了:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 在这个示例中,我就是用的上前文代码中的初始化方法,用init创建了一个PDF阅读器,用open打开了一个pdf文件,并将上一页、下一页两个按钮绑定到了相关的接口上。

这个过程充满着挑战性,当我写了一个脚本简单粗暴的将所有js结尾的后缀,都改造成ts结尾后。光是直接报错的error,就有多达2万多个。随后接近两个月的时间,我一直如愚公移山一般,一个一个的修复这些error。当我修补好一个error之后,可能又会蹦出几个新的error。比如我将某个函数的参数明确下来之后,原来隐藏的“该对象没有xx属性”“该对象可能为空”之类的报错也显现了出来。因此实际修复的报错,也是不止2万多个的。

在修补过程中,我还发现了不少写法不规范、耦合等问题。比如某个方法明明参数只有数字,但是某段特定的代码为了图方便,将参数写成false;反射用起来很随意导致代码之间的调用链断掉;大量使用record而不是map;硬编码;调用关系混乱等。我都将他们通通改过来了。至少是从面向过程改的面向对象了。下面是一段改造前后的代码对比,改造前:

handler.on("GetPage", function (data) {
  return pdfManager.getPage(data.pageIndex).then(function (page) {
    return Promise.all([
      pdfManager.ensure(page, "rotate"),
      pdfManager.ensure(page, "ref"),
      pdfManager.ensure(page, "userUnit"),
      pdfManager.ensure(page, "view"),
    ]).then(function ([rotate, ref, userUnit, view]) {
      return {
        rotate,
        ref,
        refStr: ref?.toString() ?? null,
        userUnit,
        view,
      };
    });
  });
});


这段代码监听了"GetPage"事件,但是这个事件的参数类型却是不知道的(即function(data)中的data类型),想要分析需要花很长时间。监听到这个代码之后,pdfManager要调用page中的rotate的get方法,就直接使用这样一种硬编码+反射的方式来做。这样的代码就不太好改,也容易出问题,比如当有人不小心修改了page中的rotate的名称之后,这里可能就会成为遗漏的点,并导致错误。 在经过TypeScript的改造之后,所有的参数都清晰了,调用也明显了,page和rotate之间的关联也成为强关联了:

handler.onGetPage(async data => {
  return pdfManager!.getPage(data.pageIndex).then(async page => {
    return Promise.all([
      pdfManager!.ensure(page, page => page.rotate),
      pdfManager!.ensure(page, page => page.ref),
      pdfManager!.ensure(page, page => page.userUnit),
      pdfManager!.ensure(page, page => page.view),
    ]).then(([rotate, ref, userUnit, view]) => {
      return { rotate, ref, refStr: ref?.toString() ?? null, userUnit, view };
    });
  });
});


诸如此类的改动还有很多,我会在后续的代码中详细的讨论这些问题,并分享我的经验。 改造完成之后,实际上并不能直接运行,一运行,发现bug还是一大堆。只不过这个时候代码库已经通过编译了。重构其中的一些大文件、大方法,大批量修改record为map,带来了不少的bug,甚至有几个bug都花了我一整天才定位出来。修复了这些bug之后,我重构后的代码,终于是跑通了。但是这还不够,为了能够给读者展示一个最基本的案例,即前面的翻页案例。我又做了不少工作,将参数和API都抽取出来,确保如果有人要使用本库,那么它可以直接通过配置和API来掌控创建的PDF阅读器。

尽管代码已经跑通了,但是距离真正生产可用仍旧相当一段距离。我还会继续花上一些时间,对新代码进行一个丰富的测试,确保pdf.js中所有的pdf在新旧代码上的表现都是一致的。API、回调事件,这些东西的改造目前也是尚未完成,这些关系到开发者对用本库创建的阅读器的控制能力,也是很重要的。也许我不会一次性把所有的扩展点都开发完。但是把那些关键的扩展点开发好,仍旧是必要的。等这些事情都完成后,我将会发布第一个版本,并配上配套的案例、代码、文档。确保这个库是高效且易用的。

这个项目的代码我已经放在Github上了:

github.com/xingshen24/…

调试这个库相对来说也是比较简单的,只要使用pnpm recursive install安装好依赖,再进入到 packages-private/seren-viewer-develop 目录下,使用pnpm run dev运行,即可。

开发好这样一个库需要大量的时间精力,如果有愿意赞助和合作或以其它方式给于支持的,欢迎私信我。

题外话

黑客&网络安全如何学习

今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。

1.学习路线图

在这里插入图片描述

攻击和防守要学的东西也不少,具体要学的东西我都写在了上面的路线图,如果你能学完它们,你去就业和接私活完全没有问题。

2.视频教程
网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我们和网安大厂360共同研发的网安视频教程,之前都是内部资源,专业方面绝对可以秒杀国内99%的机构和个人教学!全网独一份,你不可能在网上找到这么专业的教程。

内容涵盖了入门必备的操作系统、计算机网络和编程语言等初级知识,而且包含了中级的各种渗透技术,并且还有后期的CTF对抗、区块链安全等高阶技术。总共200多节视频,200多G的资源,不用担心学不全。
在这里插入图片描述
因篇幅有限,仅展示部分资料,需要见下图即可前往获取
在这里插入图片描述

🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源

3.技术文档和电子书
技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本,由于内容的敏感性,我就不一一展示了。

在这里插入图片描述

因篇幅有限,仅展示部分资料,需要见下图即可前往获取
在这里插入图片描述

🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源

4.工具包、面试题和源码
“工欲善其事必先利其器”我为大家总结出了最受欢迎的几十款款黑客工具。涉及范围主要集中在 信息收集、Android黑客工具、自动化工具、网络钓鱼等,感兴趣的同学不容错过。

还有我视频里讲的案例源码和对应的工具包,需要的话见下图即可前往获取
在这里插入图片描述

🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源

最后就是我这几年整理的网安方面的面试题,如果你是要找网安方面的工作,它们绝对能帮你大忙。

这些题目都是大家在面试深信服、奇安信、腾讯或者其它大厂面试时经常遇到的,如果大家有好的题目或者好的见解欢迎分享。

参考解析:深信服官网、奇安信官网、Freebuf、csdn等

内容特点:条理清晰,含图像化表示更加易懂。

内容概要:包括 内网、操作系统、协议、渗透测试、安服、漏洞、注入、XSS、CSRF、SSRF、文件上传、文件下载、文件包含、XXE、逻辑漏洞、工具、SQLmap、NMAP、BP、MSF…

在这里插入图片描述

因篇幅有限,仅展示部分资料,需要见下图即可前往获取
在这里插入图片描述

🐵这些东西我都可以免费分享给大家,需要的可以点这里自取👉:网安入门到进阶资源
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值