Skip to content

Latest commit

 

History

History
1120 lines (739 loc) · 58.2 KB

升级改造过程中发现的问题.md

File metadata and controls

1120 lines (739 loc) · 58.2 KB

TypeScript到JavaScript的转换,很多方法不太兼容,具有代表性的就是Promise.resolve();

有的函数明明没有参数,却给了参数,没有给参数,却需要参数,点进去一看,参数已经不用了。

断言的重要性,现有断言帮助我认识代码,我加断言,后面第一时间出问题

for await of的使用,与由于TypeScript和JavaScript语法检查,环境变化导致。

这个问题的根源在于 JavaScript 的动态类型检查 和 TypeScript 的静态类型检查 之间的区别。

1. JavaScript 运行时的行为:
在 JavaScript 中,for await...of 可以用于任何实现了异步迭代器协议的对象。而 ReadableStream 在某些上下文中(比如 Node.js 的 Readable)是可以被异步迭代的。这使得代码在运行时不会抛出错误。

2. TypeScript 静态类型检查的行为:
TypeScript 是静态类型语言,它会在编译时检查 readable 是否符合异步迭代器的要求。

在浏览器环境中的 ReadableStream
在浏览器中,ReadableStream 并不是原生异步可迭代对象,它没有实现异步迭代器协议(Symbol.asyncIterator)。所以 TypeScript 会报错:

在 Node.js 环境中的 Readable
Node.js 的 Readable 流实现了异步迭代器协议,可以直接用于 for await...of。如果你在 Node.js 环境使用的是 Readable,TypeScript 通常不会报错。

switch对象还可以是一个参数:getStyleToAppend

引入外部的JavaScript:OPENJPEG

变量的重复使用,在JavaScript中和TypeScript中不兼容。

数组的声明也比较麻烦,不能直接声明

有趣的吐槽,别的PDF生成器生成的错误文件,PDF作者也要把它纠正了

代码之间的循环依赖,display层和webviewer层之间的互相调用

诡异的事,undefined && boolean的结果居然是undefined

没有接口导致的类与类之间的不兼容,比如Font和TranslateFont

父类子类的顺序颠倒,父类直接调用子类的方法和函数,父类没有这些属性和函数

元祖比数组更加精确,且使用面也比较广,在Java中我也用到过这个,但是不是Java内置的,好奇为什么Java不增加元组类型?

奇怪的报错: #zIndex = AnnotationEditor._zIndex++; 局部属性必须在静态属性下面声明,比如就会报没有初始化的错

还是要多思考一下面向对象,接口、继承、多态这些事。尤其是怎么规范好现有的类和接口

为什么我在TypeScript的接口中已经定义了一个方法,当我使用一个抽象类去实现它的时候,仍然要去再次声明它为抽象方法?

能出现never这样的返回值也是有原因的,不然无法识别一些特殊的函数

字面量这东西,我在Java中没见过,但是在TypeScript中见了不少,感觉TypeScript中还是有必要的,这或许是为了兼容历史的JavaScript?

推断类型的两种方法,一种是从调用方推断,看看传了哪些参数进去,一种是从使用方推断,看看用到哪些属性,最后拼凑和推测一下数据类型。

TypeScript中似乎没有能缩小类型范围的东西: TypeScript中,我定义了一个变量,一开始他的类型是不确定的,但是到一定程度的代码之后,类型就确定了,我怎么做才能让代码检查器知道,我的变量已经一定是某个类型的了?

setTimeout的返回值,在浏览器下和node下不一样。 或许我应该把node干掉。

离谱报错,不能将A类型的值分配给A类型: 不能将类型“HighlightOutline”分配给类型“HighlightOutliner”。

viewer代码写的质量不如core和display层高,里面出现了一些循环依赖倒置,viewer调display层可以理解,display层调viewer就不太对了,这放在后端不就像是service调controller,或者放在前端就是修改vue代码来调业务代码。

如果一个属性,在父类中是属性,在子类中却以get访问器的形式暴露,为什么会报错?

102个symbol,XFA相关的代码里大量使用了symbol,非常不好改,或许当初是为了防止core层的代码的滥用而这么做的?

获取到了新的信息,生成器函数,函数前面加个*号。

使用symbols的原因 // We use these symbols to avoid name conflict between tags // and properties/methods names.

静态方法不兼容,如xfa地方使用静态方法进行实现多态,在TypeScript里比较麻烦。

有的代码必须要先梳理,先改写,毕竟JavaScript不是面向对象的,一旦涉及到多态,就会变得很麻烦。

像是拼图一样,一点一点的,通过面向对象的配置,将所有的东西都串联起来。点开一个方法,就能知道是谁调用的,调用方一点开,立马就能知道都传入了哪些参数。

不太清楚Dict类型的key和value属不属于魔数,但是直接使用key确实导致不太容易确认类型,msgHandler也是这个问题,eventBus也是这个问题,调用者不知道被调用者需要的类型。

在TypeScript中,我有这样一种场景,有一个Map,它的键类型是一个枚举,枚举大约有300多个值,它的值类型跟键强关联,比如key如果是A,那么值类型就是number,如果是B,可能就是一个数组。我想要通过一种方式,来将键和值的类型做一个强关联,使得开发者只需要简单的点进键就能知道值是什么类型,但是我又不希望采用文档的形式,因为文档并不严谨。有什么好的方法能实现这个目标吗?

实现的方式:查看DictValueTypeMapping。

尽可能的消灭原始的json对象,用Map来替代会更好。

IdFactory的写法很诡异,采用了静态方法+匿名类的形式来生成id。

// Workaround for bad PDF generators that reference fonts incorrectly, 又一次为了防止糟糕的PDF生成器生成的有问题的文档,这里需要做一下兼容

字符串居然可以使用大于小于号来进行比较,,真的是无语死了。

类型推断似乎是有问题的,不是特别准确: operation.args = null; if (!preprocessor.read(operation)) { break; } let args: any[] | null = operation.args as unknown as any[]; 难道是我的bug?只传递了值?

参数瘦身:去掉不必要的参数,一个对象可能有很多很多变量,但是确实并不是全部都是必要的,因此在修改函数签名的时候,考虑一下最小知道原则(迪米特法则)。 如果我不对参数进行瘦身,那么对于JavaScript/TypeScript这样鸭子类型的语言,没有严格的对象定义要求,传递的参数将会乱七八糟,类型五花八门。

构造器为什么还有返回值?

createDict(Type, dict, strings) { const cffDict = new Type(strings); for (const [key, value] of dict) { cffDict.setByKey(key, value); } return cffDict; }

type类型可以直接做参数,直接初始化。

不太喜欢函数的参数是解构类型的,它虽然有不少优势,比如说动态的传入参数,可以不考虑参数顺序,但是也会导致很多蛋疼的问题:比如要定义多很多类型,比如参数的类型不够简单明了。所以一般情况下我觉得也不要使用的好,对于一些特殊的情况,可以利用好这种方式的优势。

在JavaScript中,解构赋值好处还是很多的,但是到了TypeScript中,解构赋值有很多麻烦的地方。比如有的参数要加类型,有的参数要处理null和undefined的情况。需要加上一堆冗长的类型说明。通常使用interface的形式。

对于参数特别多特别复杂的情况,在Java中通常采用Builder来实现。既优雅、美观、易读、易使用。

我在JavaScript中,整理对象的时候发现,一个对象有很多属性,但是往往在定义的时候只给了一小部分属性,然后在后续的代码中补全了其中的很多属性。但是这种代码我要转换成TypeScript,需要将每一个属性都明确起来,这种时候,是使用接口好呢,还是直接定义class类好呢?

类型的复杂,让人感觉有点累觉不爱,同一个属性,类型会变来变去,比如为了读取一段pdf信息,一开始toUnicode属性是url,然后过一会儿变成了stream,最后才变成了toUnicode的具体信息。如果这样搞的话,toUnicode不仅会变得复杂,还会污染一些函数,让函数的参数以及返回值变得不明确,最后影响函数的复用。

变量重用,类型变换太复杂。

里面很多隐式,动态的类型,是比较麻烦的。比如一个属性可能是MapA或MapB,如果是MapA的时候,data属性是string[],如果是MapB,data属性是number[],这种不太容易标注属性,这种代码最好也是重写了。

由于要处理大量和null相关的东西,空对象模式和Optional更加能引发思考。

一个函数明明只用到了一两个参数,但是却把一整个对象传进去了,导致整个函数难以复用。

chatgpt又一次帮我解决了一个蛋疼的问题:

createDict(Type, dict, strings): T { const cffDict = new Type(strings); for (const [key, value] of dict) { cffDict.setByKey(key, value); } return cffDict; }

函数直接把类传进来,搞得我还一时间有点手足无措。 不过通过定义new函数,最后做了一个比较好的兼容。

非空判断不够智能,假如我现在写的代码是这样的: if(map!.has('key')){ map.set('key', value) // 这里map已经必然不为空了,但是还是要再强调一遍 }

如果一个项目要做一个更长远的规划的话,比如当前是JavaScript写的,因为这样比较快,但是将来可能发展大了,不追求快而追求规范的时候,会升级为TypeScript,那最好还是提前做准备,避开一些难以处理的写法。

直接通过某个字段来判断的存不存在来判断具体的对象类型, 比如 CFFTopDict和CFFPrivateDict类,代码通过判断有没有privateDict来确定他们的类型,在TypeScript中就不太好。 因为CFFTopDict没有这个属性,会直接报错。

同样一个变量,一会儿是number[] 一会儿是UInt8Array,造成很多问题。处理的时候要考虑,不一样的代码,到底怎么弄呢?

父类直接调用子类的方法和属性,在JavaScript中可以,在TypeScript中就不行了,这种也是全要改的对象。

在 JavaScript 中,code | 0 是一种常见的位运算表达式,它的作用是将 code 转换为一个32 位整数。这是通过按位或(|)运算实现的。

有的函数连调用的地方都没有,准确的说,不会直接通过函数名来进行调用,这种函数的参数,只能通过跟踪值,来进行确认了。

不直接调用,而是通过名称拼凑出来调用,有点类似于Java里面的反射,这种有点蛋疼。因为分析代码过程,分析分析着,就跟丢了,一旦跟丢了,既不知道参数是什么类型,又不知道哪些地方在调用。

CanvasGraphics#showText,我都不知道哪里可以调用这个函数,也就无从说参数了。

通过下面这行代码反射调用的

if (fnId !== OPS.dependency) { // eslint-disable-next-line prefer-spread this[fnId].apply(this, argsArray[i]); }

反射调用虽然灵活,但是也不可避免的带来了参数丢失,参数类型与个数不确定,进而无法通过检查,带来隐患,参数调用链路丢失,不知道从哪里开始调的。

formcalc_parser.js只在测试环境中被调用,或许是准备在将来的版本中发布,亦或是已经将调用的代码移除了,但是却没有删除原始的代码?

数组和元组要搞清楚,好的元组非常的灵活。Java中则是缺失了这种东西。

抽象真的太重要了,如果不搞抽象,要多谢很多行代码。

有些代码中似乎存在着不太合理的问题,或许是已经抛弃不用了?单元测试里也没有相关代码。

return [ "Mesh", this.shadingType, this.coords, this.colors, this.figures, bounds, this.bbox, this.background, null, // 返回值有8个,使用的时候却用了9个,最后一个补null,这或许会出问题? ];

返回值有8个,在使用的时候却用到了第九个

小插曲,Map不叫Map,叫MapElement,boolean不叫boolean,叫booleanElement,Date不叫Date,叫DateElement。

因为JavaScript中属性的类型不明,所以很多时候,会必须要再加一个parseFloat之类的转换一下。

相同的文件和类名,比如Fonts.ts,通过export as 可以让引用文件和外部文件不用保持一致。这个简化了开发。不然随着系统里面的类名越来越多,重复的名称,超长的名称,对开发来说还挺受困扰的。

代理,在JavaScript中有ProxyHandler,在Java中也是有这个。Java的ProxyHandler的广泛使用,比如负责通信的OpenFeign,还是负责数据库调用的MyBatis,还是Spring,通过代理创建接口对象也是一件很常见的事。由于对Java中的Proxy代理的熟悉,也让我看到JavaScript中的代理的时候倍感亲切。

TypeScript能力边界,有些代码必须要重写,重写更符合后续的发展。

Sandbox限制pdf中的JavaScript代码执行,防止pdf中的代码执行JavaScript,执行一些恶意代码,访问window对象。

干掉node吧,如果后面需要添加对node的支持,再重新组织代码的层级吧,提取出环境无关的功能和环境相关的功能。

作为一个Java程序员,自动处理包和类名使用惯了,以至于几乎不用考虑对象的导入导出。但是在JavaScript/TypeScript中,这一点却是无法避免的,相较于Java,它更复杂。

JavaScript存在要考虑多种环境的情况,浏览器、WebWorker、NodeJS,但是在Java中却不怎么要考虑这些。

xfa相关的代码,改起来确实缺少头绪,因为太面向JavaScript了,比如大量的使用Symbol作为属性名来确保外部无法调用,比如父类使用子类的属性,比如同样的属性,在父类上是一个函数,在子类中却变成了一个属性,而且二者完全不搭。

对XFA的代码进行一定规模的重构,看来是一件不可避免的事了。那就只能重构了,XFA部分的代码,不是很面向对象,将来要维护的话,还是要考虑面向对象这一点的。

说实话,不能把json当map使用,json就是json,它可以声明一个具体对象,但是作为map使用,它的使用方式过于面向过程而非面向对象,它缺乏约束,缺少具体的泛型类型,缺少清晰的方法。 json对于key中包含冒号、破折号之类的情况,需要做特殊的处理,处理不好还可能会报错,这加重了负担。

纠结了很长时间,最后决定还是直接移除对XFA的支持,因为XFA的规范不明确,资料非常匮乏,就连www.xfa.org都无法打开。况且xfa也已经被视为是过时的技术了,因此最后决定不再支持xfa了。移除xfa相关代码。越占总代码行数的10%左右。

// globalThis上的event被篡改了,导致四处报错。

读代码和读书很像,第一遍过的时候,阅读越厚,这也不懂,那也不会,但是读到后面,却越读越薄。

一开始谨小慎微,到后面就放开了,放大了胆子就是干。

isGeneric这种,也要考虑一并删去,pdfjs可以通过打包方式,将各种各样的打包方式,这个也要考虑移除。

用枚举代替常量,const xxx = { xxx : 1, xxx: 2},不如枚举来的约束更强。

子类没有构造函数就直接调父类构造函数,感觉这个也是怪怪的。

耦合和混乱的依赖,才是重构最大的挑战。

静态方法该如何处理,因为静态即是全局的,这一点既需要好好利用,防止资源的重复加载,又需要谨慎提防,防止不同实例之间的互相影响。

可以考虑使用get方法的延迟处理。

get xxx(){ if(xxx == null){ // init } return xxx; }

// Ideally we'd directly search for "endstream", however there are corrupt
// PDF documents where the command is incomplete; hence we search for:
//  1. The normal case.
//  2. The misspelled case (fixes issue18122.pdf).
//  3. The truncated case (fixes issue10004.pdf).

还可以更搞笑吗?终结符应该是stream,但是有的pdf生成程序会生成steam和strea。

尽可能的使用null去替代undefined。

接触掉undefined的方式: let value = undefined; let newValue = value || null; console.log(newValue); // null

map.get(xxx) || null

换一种思路,一切代码都是从getDocument开始的,那我就从getDocument开始支持研究和重写吧。

不少代码是为了方便单元测试而写,这是一种通用的做法吗?我觉得似乎不太妥当,所以还是移除吧。 应该是用单元测试去驱动出更好的代码,而非说代码为了单元测试而强行做出不必要的扭曲。

缺省值也是一个操蛋的事,假如我一个参数是null,但是参数类型是number,如果没传就直接用默认值,那如果我传了个null进去,到底是缺省值还是参数呢?

通过由MessageHandler连结而成的复杂的调用关系,必须清晰和明确起来。不然后续根本无法维护。

静态变量和私有变量没有通过名称来进行区分,这一点也优化掉吧。装饰器看来是必须的了,没有装饰器,不好把不同的函数串联起来。如果不能串联起来,那么我就无法通过MessageAction找到对应的发送点和响应点。

异步调用不清晰,随处可见的send("GetDocRequest"),on("GetDocRequest"),这样会让我无法确定调用方式,参数和返回值更是非常不明确,想做改动的时候,心里没有底,测试完其实心里也没有把握;

反射啊反射,或者说直接通过属性来调用函数或者获取属性名,我是真不喜欢这种写法,要么就是把引用关系丢了,要么就是在做改动的时候,根本就不会发现,还有这地方要改,还有就是获取不到具体的类型。 ensureDoc('abc'),如果我把abc改成_abc了,那这个地方有问题我都发现不了。 还有就是参数不明的问题,传入了a/b/c三个参数,但是我后面多加了一个参数,反射调用也发现不了。

在查询某一个字段被哪些地方调用的时候,也会因为反射而丢失一些相关信息。在面向对象的改造中,该干掉还是都要干掉啊。

let x = [1, 2, 3, 4] as const; 这种方法是一个好方法,避免x被推段为number[],元组与数组之间的推断,防止编写大量的代码指明元组,而不是数组。

泛型啊泛型,凡是基础的类,必定一大堆泛型,我说怎么很多源码里面,类的参数那么多,合着都是要做泛型的缘故。

第一阶段,到处加as,第二阶段就是一段一段的删除这些as了。

在TypeScript中,我有一个数组,前两个元素使string,其余元素都是number,这种数组怎么表示比较好?

["black","XYZ", 0,0,0]

type MixedArray = [string, string, ...number[]];

chatGPT还是给力的。

核心的类写的不够强大,都是比较简单,这样很多关键信息都会丢失。

硬编码,万恶的硬编码,等到系统大到一定地步,就会发现,一个一个的梳理硬编码,到底是多么痛苦的一件事。

动态生成的对象,通过parse根据PDF文档生成的对象,没有具体的类型,真的是没有什么好的办法。

[...groupRefCache] 普通的类也可以使用这种方式展开。

递归函数的分析比较麻烦,尤其是间接的递归,得找到正确的出口,才能分析出函数的意义。

解构赋值写一大堆参数类型,或者非常别扭的参数写法,很难看。

日志是否要统一处理?日志占的行数太多,影响观感。

function assert(cond: boolean, msg: string): asserts cond { if (!cond) { unreachable(msg); } }

通过返回值的断言,实现了一个比较好的assert功能

_startRenderPage 明明声明了是私有函数,外部照样可以调

lib里面的代码居然也可以随便改动,应该要做个限制的吧?

什么属性都往参数里塞,然后塞完再在同一个方法里面写大量的if和in做判断,真是糟糕透了

request(args) { .... if (this.isHttp && "begin" in args && "end" in args) { .... } .... }

进程间通信,该考虑什么呢?参数的丢失?这好像也是必然??我该怎么做才能让进程间通信的参数明确起来呢?

不太喜欢这种写法,不如4个const来得整齐: const streamId = data.streamId, sourceName = this.sourceName, targetName = data.sourceName, comObj = this.comObj;

下面的这种写法更加整齐,看起来更一目了然。 const streamId = data.streamId; const sourceName = this.sourceName; const targetName = data.sourceName; const comObj = this.comObj;

new Promise(function (resolve) { resolve(streamSink.onPull?.()); }).then( function () { comObj.postMessage({ sourceName, targetName, stream: StreamKind.PULL_COMPLETE, streamId, success: true, }); }, function (reason) { comObj.postMessage({ sourceName, targetName, stream: StreamKind.PULL_COMPLETE, streamId, reason: wrapReason(reason), }); });

改造完成后: new Promise(resolve => resolve(streamSink.onPull?.())).then(() => { const msg = { sourceName, targetName, stream: StreamKind.PULL_COMPLETE, streamId, success: true, } comObj.postMessage(msg); }, reason => { const msg = { sourceName, targetName, stream: StreamKind.PULL_COMPLETE, streamId, reason: wrapReason(reason), } comObj.postMessage(msg); });

函数的顺序和类名这一点,如果能够让IDE来完成,我们就不要自己去管理它,我们应该把精力聚焦在问题上。

__originalSetTransform?: (a?: number | DOMMatrix2DInit, b?: number, c?: number, d?: number, e?: number, f?: number) => void;

竟然还有这种兼容写法,也太太太蛋疼了吧,不过仔细想想,能理解,但是感觉还是不值当。

const iterate = (0, match.iterateFn)(context, i);

居然还有这种写法,这是我几乎没有见过的

借助工具非常重要,我就通过ctrl+p和ctrl+q,能够快速知道函数的参数是什么。

this.#tryCleanup(/* delayed = */ false);

通过工具可以将 /* delayed */ 这个注释删掉。

超长的switch case 约400-800行,里面变量重复定义,break,变量赋值,很不容易推断。

似乎这种比较复杂的算法代码,使用面向过程的开发方式可能会更好

热点代码,尤其是解析部分的代码,还是要做特殊的处理啊。如果过于注重可读性和可扩展性,可能会造成性能的下降。 写这种高性能代码,非常考验技术功底。

代码最好写的通用一点,毕竟TypeScript和Java还挺想的,或许后面写好了,能直接将一部分代码拷贝过去?尤其是和UI以及API无关的。

转递归为循环,这样的代码可能不好写,但是应该能提高效率,降低时间。

写好注释也非常重要,尤其是注释可以通过@link之类的东西,link到其它代码上去,避免重复声明的代码。

我遇到一个参数类型,它既要可以是number[],又要可以是Uin8Array、Uint32Array,但是在实际使用过程中发生,他只要是xxxx[i] = j 就可以了,i是数字,j也是数字,最后它是什么类型的呢?ArrayLike!

#toRgb(src: ArrayLike, srcOffset: number, maxVal: number | null, dest: MutableArray, destOffset: number) { 最大值是maxVal是false,外面传了个false进去,进了里面之后,判断值是不是false,根据maxVal的值是false还是数字执行两套逻辑。这个写法真是不太好。

ImageDecoder直接定义全局变量,然后直接使用。以至于我找不到这个对象的定义。

declare var,这是一个很重要的写法,它要解决的问题就是:如果一个变量在web浏览器中已经存在了(可能某些浏览器存在,某些浏览器不存在),那么我想以一种规范的方式使用它,那么我就必须要使用decalre var。

getLookupTableFactory 重复生成对象,浪费CPU,但是这个或许浪费的很少? 或者是希望这些对象用后就丢弃,不要常驻在内存里?这个可能是一个比较合理的回答。

imgData = await imageObj.createImageData( /* forceRGBA = / true, / isOffscreenCanvasSupported = */ false );

无需在写代码,ctrl+p就可以查看每个值对应的参数。

JavaScript中的很多函数,参数都太多了,这也许是必然?

变量巨多,使用私有变量必须要用this,这个其实加大了开发的复杂性,因为有些东西能够通过IDE来实现,我们就不要画蛇添足了,我们应当把所有的精力都聚焦在代码本身上。写出纯净、易读、易修改的代码。

const emptyXObjectCache = new LocalImageCache();

因为管控不严格,所以可以“挪用”其它类型,这个类型明明是用来存图片的,但是它也刚好来做一个Map,就当Map来引用了,这个会造成混乱。

/**

  • 该函数主要实现懒加载功能,一般为某个getter方法服务
  • 针对某个getter方法,第一次使用的时候,会直接调原来的getter方法,并且生成相应的属性
  • 生成完相应属性之后,然后用shadow创建新的属性替代掉原来的getter方法,从而实现功能的缓存
  • 原来的getter在被调用一次之后,就会被shadow方法替换掉
  • shadow方法实现的其实并不完美,因为它的prop是用的字符串形式,它会将代码与代码之间的关联断开掉
  • 不小心误触或者大小写错误或者字母顺序出错了,都会导致问题。 */ function shadow(obj: object, prop: string, value: T, nonSerializable = false): T { if (PlatformHelper.isTesting()) { assert( prop in obj, shadow: Property "${prop && prop.toString()}" not found in object. ); } Object.defineProperty(obj, prop, { value, enumerable: !nonSerializable, configurable: true, writable: false, }); return value; }

这个函数我一开始还没看懂,后来仔细研究后,才恍然大悟。

构建的过程中,缺少一个全局的Context,以至于构建的代码零零碎碎的,以至于大量的参数都需要通过各种各样的方式传递来传递去。

考虑一个问题,是不是JavaScript中,只要定义了全局变量,这些全局变量都会常驻在内存当中?如果是这样的话,那是要考虑变量不重复定义的必要性。

关于内存泄漏:JavaScript似乎在这一点上更加危险,因为如果不能够处理好JavaScript的全局变量,是会导致内存泄漏的。这一点比Java更容易。

是不是要考虑,把async拿掉。在使用语法这个方面,其实我觉得过多的语法糖其实是不太好的,因为它会加大代码的混乱程度。你使用async,我使用Promise。当我想要对整个项目的代码做一个大的整改之后,就会发现,其实是很困难。因为各种各样的东西都不统一,对一个不统一的东西做一个统一的处理,那势必是要先对齐所有的写法。对齐所有写法意味着改动,改动意味着可能会出bug,改动意味着可能需要测试,改动意味着工作量,很多时候,这样那样的问题加起来,就导致我们无法对项目做大的改动。因为成本太高、工作量太大、不确定性风险太大。

const作为一个常量,居然不能够被用来做实值使用。

我定义了SKIP=1,OVER=2,我希望返回值返回1或者2,但是1或者2是魔法值,没有实际含义,但是我发现直接写SKIP和OVER也会报错。

泛型,泛型,泛型,要把所有的泛型全部都提取出来。泛型能够高效的让开发者明白,自己需要的是什么,而不是到处的any和unknown。

真的不喜欢到处强转,转的太难受,也不优雅。使用面向对象的抽象和设计模式,解决起问题来,会更好。 现在之所以强转,是因为之前的代码主要是以面向过程的方式写出来的,不是很好维护和扩展。

class PredictorStream extends DecodeStream {

protected pixBytes: number = 0;

protected rowBytes: number = 0;

constructor(str: Stream, maybeLength: number, params: Dict) { super(maybeLength);

if (!(params instanceof Dict)) {
  return str; // no prediction
}
const predictor = (this.predictor = params.getValue(DictKey.Predictor) || 1);

if (predictor <= 1) {
  return str; // no prediction
}

}

constructor居然会返回string,这个还是挺操蛋的。

因为TypeScript毕竟不是一个运行时强类型保护的语言,有时候可能会出现类型与实际不符的情况。是否可以考虑严格strict模式,如果打开严格模式,则该检查的地方都要检查。

function ChunkedStreamSubstream() { } ChunkedStreamSubstream.prototype = Object.create(this); ChunkedStreamSubstream.prototype.getMissingChunks = function () { const chunkSize = this.chunkSize; const beginChunk = Math.floor(this.start / chunkSize); const endChunk = Math.floor((this.end - 1) / chunkSize) + 1; const missingChunks = []; for (let chunk = beginChunk; chunk < endChunk; ++chunk) { if (!this._loadedChunks.has(chunk)) { missingChunks.push(chunk); } } return missingChunks; }; Object.defineProperty(ChunkedStreamSubstream.prototype, "isDataLoaded", { get() { if (this.numChunksLoaded === this.numChunks) { return true; } return this.getMissingChunks().length === 0; }, configurable: true, });

通过function+prototype的形式,实现了一个子类,这种形式在TypeScript我觉得还是要移除。 这种写法完全没必要,使用类+继承的方式会更好。

本来只对原型链有一个基础的认识,现在发现要还必须要深入了解,才能够解决某些问题。

constructor(pdfNetworkStream: PDFStream, args) { this.length = args.length; this.chunkSize = args.rangeChunkSize; this.stream = new ChunkedStream(this.length, this.chunkSize, this); this.pdfNetworkStream = pdfNetworkStream; this.disableAutoFetch = args.disableAutoFetch; this.msgHandler = args.msgHandler; }

变量类型明明叫pdfNetworkStream,实际上给的确实pdfWorkerStream。而系统中又有PDFWorkerStream这个类,因此这里产生了误导。

父类可以随意使用子类的变量,这也蛮头疼的。

LZWStream.lastCode,似乎是一个从全局角度来看,没有意义的变量。

onImmediateLosslessHalftoneRegion() { const [region, referredSegments, data, start, end] = arguments as unknown as [ SegmentHalftoneRegion, number[], Uint8TypedArray, number, number ]; this.onImmediateHalftoneRegion(region, referredSegments, data, start, end); }

...arguments的改法。

硬编码,真是一个毒瘤一般的存在。写起来爽,维护起来爆炸。 // 7.3 Segment types const SegmentTypes = [ "SymbolDictionary", null, null, null, "IntermediateTextRegion", null, "ImmediateTextRegion", "ImmediateLosslessTextRegion", null, null, ....

代码在线编辑器其实还是很有用的,我在很多不确定问题的处理过程中,比如构造函数有返回值这件事上, 我都要去验证一下:PredictorStream,还有枚举什么的,使用在线编辑器验证一下自己的想法。

export class PredictorStream extends DecodeStream {

protected pixBytes: number = 0;

protected rowBytes: number = 0;

protected predictor: number | null = null;

constructor(stream: BaseStream, maybeLength: number, params: Dict){ super(maybeLength);

if (!(params instanceof Dict)) {
  return stream; // no prediction
}

}

私有变量的声明是会先于构造器的,return会不会带来一些影响?

在PDFImage创建过程中,会生成一系列的BaseStream,但是我也不知道这些BaseStream对应的类型是什么。 只能把所有相关类型的测试用例都跑一遍。

这个参数真的别扭。

parse(data: Uint8Array, { dnlScanLines }: { dnlScanLines: number | null = null}): undefined {

get、set的存在,威胁了面向对象,因为他们可能无法被重写。准确的说是,调用者不应该访问父类的属性。除非这个属性不可变,但是有些属性可以直接被访问,而子类无法利用好这一点,因为子类可能需要延迟加载这个类。

干掉delete!面向对象中不应该需要这种东西!

有些特性,在改造的过程中,起了很大的作用。比如多个类型连接在一起,但是从长久的角度来看,我还是倾向于移除他们。

或许使用类似于DictKeyMapping这种形式的方式来控制返回值类型不够优雅,但是也是当前比较好的一个解法了。这也许和TypeScript中的枚举太弱有关系。

getInheritableProperty这个方法既能返回数组,又能返回单个元素,这会对调用这个函数的人产生困扰。并且还要做以一些强转工作,我们知道的,做的工作越多,错的概率越大。如果把它拆分成两个函数,那么效果就一目了然了。

我大部分时间都在做静态代码分析,但是静态代码分析的所得,似乎是有限的。

as unknown as xxx这种强制转换,实际上是非常糟糕的,只能临时用用,或者特殊情况下使用。

// The pdflib PDF generator can generate a nested trailer dictionary if (!(dict instanceof Dict) && (<{ dict?: Dict }>dict)!.dict) { dict = (<{ dict?: Dict }>dict)!.dict!; }

这样的代码不好搞,通过有无某个属性来判断是否是某一类型的数据,似乎很蛋疼。用 instanceof 或许更好一点。

改掉一个错误,蹦出来四五个新的错误,这也是常有的事。

onMessage不仅处理所有的send相关的请求,如果是其它地方也是用了postMessage,这里也是可以接到的。

解决一个报错又蹦出来三四个报错,一个参数没被表明类型的时候,它只会报一个错,就是缺少类型,并且默认是any。一旦标注了,所有引用的地方可能都会报错。

引入外部的js,像OpenJPEG这样的,可能是一个挑战,不过弄清楚它是怎么引入的。我查过了,这玩意还不能直接通过npm来进行管理,有一股传统的jar包无法通过maven来进行管理的感觉。

看来还是引用了wsam相关的方法。

Emscripten 工具链,将openJPEG的代码编译成JavaScript。

emcc 是 Emscripten 工具链的核心命令,用于将 C 或 C++ 源代码编译成可以在浏览器中运行的 JavaScript 或 WebAssembly。它使得将原生代码引入 Web 环境变得可行,并且在性能要求高的场景(如图像解码、视频编解码等)中非常有用。

底层的代码需要一些更高级的写法,但是在静态分析的时候,那边的代码不太好分析,因此很难直接做出调整。

annotation这一块,要做大的改造。一来他本来就写的比较乱,二来我要实现一些更高级的批注,甚至还要实现一些插件的功能。

static async createNewPrintAnnotation( annotationGlobals: AnnotationGlobals, xref: XRef, annotation: Record<string, any>, params: { evaluator?: PartialEvaluator, image?: CreateStampImageResult | null, evaluatorOptions: DocumentEvaluatorOptions } ) { const ap = await this.createNewAppearanceStream(annotation, xref, params); const annotationDict = this.createNewDict( annotation, xref, ap ? { ap } : {} );

const newAnnotation = new this.prototype.constructor({
  dict: annotationDict, xref, annotationGlobals, evaluatorOptions: params.evaluatorOptions,
});

if (annotation.ref) {
  newAnnotation.ref = newAnnotation.refToReplace = annotation.ref;
}

return newAnnotation;

}

静态方法+抽象方法。

批注是一个很考验编程技巧的地方。因为涉及到泛型、涉及到多态,涉及到可扩展性。后面还可能涉及到插件。因此没有那么容易就能改好。

批注这块要做的改动也是最大的。

复杂的代码,要先改结构,然后才能谈改细节。annotation.ts就是这样的,如果不能把结构做出明显的调整,是无法有效的改代码的细节的。

if (storageEntry) {
  value = storageEntry.formattedValue || storageEntry.value;
  rotation = storageEntry.rotation;

这种通过有没有属性来进行对象类型的判断,还是很蛋疼的,不过我通过MaybeType巧妙的解决了这个问题。

后面可以问一问chatGPT,看看他是不是有什么比较好的想法。

// 这显然是一段废弃的代码,明明函数只有四个参数,但是却有五个参数, // 而且是一个明显的错位 return super.getOperatorList( evaluator, task, intent, false, // we use normalAppearance to render the button annotationStorage );

JavaScript对内存的考虑,STOP THE WORLD的考虑和Java不太一样。Chrome的内存就那么多,如何精细化的利用好每一点内存、缓存、本地存储,是非常关键的。

JavaScript和Java虽然面对的是不同场景,但是有很多优点还是可以互相学习的。

单元测试,这样的项目我觉得单元测试更加有用。以前做开发,后端的单元测试怎么都写不好。它会影响迭代,而且mock的代码量太大。并没有说就很好用。

if (data.fillAlpha && data.fillAlpha < 1) { // 这边的代码有bug,怎么把值赋给了不该赋值的对象了? const styler = trigger; (<{ style: string }>styler).style = filter: opacity(${Math.round(data.fillAlpha * 100)} %); ;

if (PlatformHelper.isTesting()) { this.container!.classList.add("hasFillAlpha"); } }

addEventListener(type: K, listener: (this: Element, ev: ElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;

不同的addEventListener,给的参数不同,对应的event类型也就不同了,有的是keyof类型有的是string类型。

处理监听需要EventTarget等一众泛型参数。这个还是挺麻烦的。

混乱,没有按照一定层次来组织代码。我的的理解是,代码组织结构应该是这样的: 代码应该分为三层,第一层是pdf的核心处理,解析、网络,第二层是在pdf上组装的基本层,比如说是editor,或者一些更通用的API,第三层是基于这些API开发的应用,但是第二层的代码却会调第三层的代码,这不是很奇怪吗?比如TextLayerBuilder。

静态方法穿插在对象属性和对象方法之间,这不太好。要么统一放上面,要么统一放下面会更好。这个在Display的代码里尤甚。

解构赋值的一个问题就是,如果结构出来的值,从值的角度来看可能为空 比如 x: string | null,但是到了代码运行的地方,实际上它已经不可能为空 x的值必定是某个string,那么 const {x } = obj; x在后面就必须要处理null了。

(TextItem | TextMarkedContent)[] 通过有误str来判断某个元素使TextItem还是TextMarkedContent。

其实我也不太喜欢多种类型的组合这种写法,这种写法放弃了抽象,把难处丢给调用方去处理了。

断言真的非常非常重要,告诉代码,到了某一个地方,它的属性值或者类型一定是xxx,这样避免了向上再深究别的代码。也将bug及时遏制住了。

#removePointerdownListener() { this.pointerdownAC?.abort(); this.pointerdownAC = null; }

这么明显的错误,变量名叫做#pointerdownAC,但是调用的时候用了pointerdownAC。这妥妥的因为类型不明确导致的bug啊。

/**

  • The annotation element with the given id has been deleted.

  • @param {AnnotationEditor} editor */ addDeletedAnnotationElement(editor) { this.#deletedAnnotationsElementIds.add(editor.annotationElementId); this.addChangedExistingAnnotation(editor); editor.deleted = true; }

    this.#uiManager.addDeletedAnnotationElement(editor.annotationElementId);

参数明明传要的是editor,传递的却是editorId,在JS中不报错?

function isValidScrollMode(mode: unknown) { return ( Number.isInteger(mode) && Object.values(ScrollMode).includes(mode) && mode !== ScrollMode.UNKNOWN ); }

如何判断一个值是否是某种类型的枚举?

CanvasGraphics和OPS关联的写法是真操蛋,它是这样做的,对于解析出来的PDF指令,转换成一个个操作,然后把这个操作放进队列里去。在队列的另一头,拿到指令,一条一条分析怎么处理,但是最后生成指令的代码和处理指令的代码关联不上,自然而然也就没办法正确处理好参数和返回值的问题。我现在就是要解决这个问题。

对于 xxx as xxxType和我只保留一种,就是后者,两者都使用,会导致混乱。

注意拼写错误,好些地方,我都把visible拼成了visiable,并导致了问题。

主要部分的报错都修复后,后面就开始盯着小bug,一个一个的修了。

static #editorTypes = new Map( [FreeTextEditor, InkEditor, StampEditor, HighlightEditor].map(type => [ type._editorType, type, ]) );

这种代码,改起来真的很操蛋,我也不知道该怎么改。

乱借用代码,本来给AnnotationEditor准备的代码,后面写着写着,发现ColorPick也能调用,然后就把ColorPick也传进来了,这不是给升级为TypeScript带来很多操蛋的的工作吗?

我想开发一个扩展性比较强,能够写插件的组件库,因此使用枚举不是一个好选择,因为枚举本身难以被扩展。而批注本身,恰恰就是深度依赖枚举。

展开特定FloatArray64的方法:...Array.from(outline.slice(i, i + 6)) 试图直接展开outline.slice(i, i+6)会报错。

KeyboardManager.exec真是一个操蛋的写法。

警惕:将很多Record类型改变成为Map类型后,Object.values(xxxx)都要同步进行更改。 Record和Map的优劣之比??我更倾向于使用Map。

null和undefined,我想尽可能的使用null,undefined,在全等这方面,我担心还是有一些bug,毕竟是改的别人的代码。

到后期好了一点,因为可以通过调试原版的代码,来动态确定某些对象的类型了。

有些代码写法完全不适合于面向对象,你要是想做出牛逼的改动,那你只能改。但是代码都是环环相扣的,很容易牵一发而动全身,这时候就要对代码有一个全局的认识,然后做出一个在全局层面来看,比较合适的改动。

思考一个问题:在定义一个接口的时候,这个接口可能是 interface Tester{
propA: xxx | null; propB: xxx | null; propC: xxx | null; } propA、propB、propC的值在接下来的代码中会完成初始化,初始化结束后这些值都不为空。 但是声明的时候却是可以是空的,在后面对这些属性进行引用的时候,又因为声明为空,所以必须要对null值进行处理,这个处理其实是不必要的,在TypeScript中,如何解决这个问题?

Partial和Required ==> 或许可以解决上面的问题!

const popup = (this._popupElement = new PopupAnnotationElement({ data: { .... borderStyle: 0, ... }, parent: this.parent, elements: [this], })); 代码写的很随意,导致无法兼容,borderStyle明明在所有地方都是BorderStyle类型,这里却给了个0,直接导致无法兼容。要想兼容,必须要把代码改掉。

多种写法之间的较量:onclick和addEventListener,一套项目除了非必要的部分,应该只保留一种写法,否则会带来混乱,不方便同一个管理,增加维护者的困惑。

我也不太喜欢!这种写法,尽量通过各类语法来避开null,!可能会导致不准确。

递归嵌套的类型: 假设我有一个类型是X,X的具体类型是[number,number,number, X],那么应该如何声明X的类型? #getOutlines(outlineVerticalEdges: [number, number, number][]) { 就遇到这个问题了。

this.prototype.constructor通过这种方式来初始化类,应该被禁止,和Java的反射一样,这种初始化会导致很多信息的丢失,以及代码无法跟踪的问题。

const editor = new this.prototype.constructor({ parent, id: parent.getNextId(), uiManager, });

reporttelemetry:原来PDFjs在使用过程中,还会向firefox发送要测数据? 这个看来是没有太大的必要保留了。

对于editor的序列化和反序列化,我看直接移除比较好,后面进行重新设计,因为批注这个功能不仅要强大,而且要全面。 后面也很有可能开放插件功能。

在面对TypeScript中的每一个特性的时候,我都要思考,哪些应当保留,哪些应当移除。起码一些兼容旧代码的语法,未来可能会被移除的东西,不应该再继续使用下去了。

文件名重复:highlight.ts有两个,这个可不太好。

构造函数不能重载?如何一个类的构造函数可以很少,只有寥寥几个,也可以很多,那么这个时候该怎么办?

静态方法对我来说,也是要少用,因为我要创建多个实例,所以要避免和static这个全局的东西进行耦合。

AnnotationData.getFieldObject(),不同基类采用返回不同的值,我觉得应该改造成基于基类的方法。 对于JavaScript中那些不太面向对象的写法,日后不好更改。应该尽量避免。实在不行的该改动要改动。

其实单个点拿出来看,感觉都没有那么难,但是所有的问题杂糅在一起的时候,且数量庞大,那就有点不好解了。

潜藏的错误: editAltText(editor: AnnotationEditor) { this.#altTextManager?.editAltText(this, editor); }

明明只要一个参数,却给了两个参数。这个在JavaScript里是不容易被发现的。

核心的部分改完之后,外部的就好改了,毕竟外部的可以直接调试,类型什么的一下子就出来了。 为什么不直接才能够外部开始改? -- 写博客的时候,可以好好想想。

那些专属于Firefox浏览器的代码,要不要保留?--这是一个问题。

在关注单元测试的问题之后,面向接口编程显得更加重要。因为单元测试的代码,没有初始化一大堆对象的必要。很多时候,其实也初始化不了那么多对象。

Fluent:一个要考虑外部依赖的问题。

使用Barrel文件来进行模块的导出。

不要轻易使用全局对象,使用全局对象之后,想要初始化多个实例的时候,就会渐渐变得困难起来。

blockUnblockOnload非标准API,需要做一些特殊的处理。

既然是要做库,那么具体的操作按钮和功能就要解耦。库只提供渲染的操作和API,将这些API绑定到对应的按钮上,是开发者自己的事。

class PDFSinglePageViewer extends PDFViewer 为什么单页View查看器会继承全区PDFView查看器。不应该是全局PDFView继承单页PageView?

现在必须要将HTML元素和对应的事件以及回调全部都剥离开来。 原来是:操作DOM=>调用功能, 功能触发=>修改dom 现在要改成:操作DOM=>调用功能, 功能触发=>回调=>修改dom。

altTextLearnMoreUrl: { /** @type {string} */ value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL") ? "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/pdf-alt-text" : "", kind: OptionKind.VIEWER + OptionKind.PREFERENCE, },

移除为pdf调试而写的代码。

解耦很重要,代码不要和document耦合,毕竟以后我们也许会把里面的代码移植到别处呢?

突然感觉到lombok里面有些东西还是好用的。毕竟作为开发者,有些代码需要重复写,但是实际上自己并不想重复写,而且重复写也容易导致修改的时候遗漏,从而产生bug。

Option写不好,最关键原因,感觉还是缺少了lombok一样的插件,需要写重复代码。——deepseek告诉我有,但是我不太确定,是不是跟我想要的是一样的。

解耦应该要充分和彻底,不要调用全局对象。这会导致测试的困难、代码移植的困难!

批量修改代码的时候,正则表达式真的很好用。当我想要使用xxx=yyy,想改造成 protected _xxx: { type: xx, value: yyy, descriptor: xx, };

get xxx(){ return this._xxx.value; }

set xxx(value: yyy){ return this.xxx.value = yyy } 使用正则表达式即可。 get $1() : $2{ return this.$1.value!; } set $1(value: $2){ this._$1 = value; }

findController和findService要解耦,怎么find,让用户自己去决定吧。 我提供高亮文字的功能。并且写一个功能模块,实现find获取高亮的信息,然后调用高亮的功能来实现文字。

有些值可以有默认值,但是不应当在非入口的代码中读取环境变量。这会导致很多问题,比如环境发生变化时会产生bug、不易测试、代码移植性变差。

ARIA网页无障碍阅读,先放一放。

每一段注释我都会重新审视一遍,将它由英文的注释改变为中文的注释。

有DocumentProxy和PageProxy的存在,说明Viewer层和PDF的处理层,这两层应该是做了隔绝的,但是事实上有些代码写的很乱,PDF的处理层跑去调用viewer层的代码了。

改到最后的时候,对代码结构、流程、细节已经了然于心的时候,可以自由自在的按照自己的想法进行改动了。

啊啊啊啊啊啊啊啊啊 ======================= 代码编译不通过的时候,可能无法自动有效的清理import。这可能是因为清理import是必须要在编辑器能够有效认识TypeScript文件的情况下。

渲染顺序不明了,一个接一个的渲染是通过回调实现的,而非用一个队列实现,这让渲染的改动就比较麻烦了。 这边的逻辑应该直观一点。不直观,意味着不好控制、不好改动。

对web层面的代码,实际上是有比较大的改动的。

为了兼容多个平台,像PDFFindService、PDFPageViewManager、PDFLinkService,这些都应该高度抽象,因为他们在不同的平台下,跳转的具体实现细节都是不同的。

要留两样东西,一样东西是API,这个其实直接给到对象或者对象代理就行了,另一个则是Callback。 我想把 “通过发消息来调用某些函数” 这样的形式干掉,因为这样写苔草淡了。调用者和被调用者互相不知道,调试起来麻烦。 参数改了也不会报错。

如果一个属性不能设置为null,那么就无法使用xxx.prop = null 这种方式来释放内存。

很多仅用来表示对象参数的注释,也删了很多。

在很长一段时间里面,其实我做的是静态代码分析的活儿。没有动态运行,或者动态运行的是原代码,然后对比来修改这边的代码。 对PDF自身的结构也没有深入的了解。

pdfjs还经常被做成插件,放到VSCode扩展之类的。因此它还有一些这方面的兼容代码。 // In the Chrome extension, the URL is rewritten using the history API // in viewer.js, so an absolute URL must be generated. viewerUrl = // eslint-disable-next-line no-undef chrome.runtime.getURL("/content/web/viewer.html") + "?file=" + encodeURIComponent(blobUrl + "#" + filename);

有的代码在JS里能调,到TS里就不行了,因为在依赖的TypeScript直接写成了私有属性,这导致了需要变通。 const messages = await this.#l10n!.formatMessages(ids);

写了一段shell,粗暴的将所有的js均改造为ts,结果有了超过2万个报错。现在终于调试完了,下一步是让整个项目变得更合理起来。 一是要方便调试,而是要方便打包。

打包出来的库太大了,超过1Mb了都。

要考虑一个多模块的问题,但是还不需要现在进行考虑。

禁用全局变量,不要通过全局变量来实现某些类的耦合。即便是用默认值也不行。

constructor(ownerDocument = globalThis.document, enableHWA = false) { super(enableHWA); this._document = ownerDocument; }

这种构造器的写法,就是一种耦合,这种耦合会加大代码移植的难度,还有iframe等各式各样情况的存在,会导致即使你在源头修改了某些代码,结果发现代码依旧存在问题。 比如你在初始化的时候传入了一个值,你修改了这个值,发现没有起作用,然后一直一直向下定位,发现是因为某行代码使用了全局变量,导致你的修改没有传导到此处来。

type和interface不能混用,type只能做起别名用,尽管可以在一些程度上代替interface,但是显然不能这么做,因为这么做会导致语义混乱、后面要升级改进重构时会带来困惑。

在给属性定义是否要用?的时候,要慎重。可以用,但是不能滥用。要理解每一种语法的具体含义,而不是说这种语法能用就用。

export type CanvasAndContext = { savedCtx?: CanvasRenderingContext2D; canvas: HTMLCanvasElement | null; context: CanvasRenderingContext2D | null; };

循环依赖问题:

A.ts import { B } from './B';

export class A { constructor() { console.log('A is initialized'); } }

// 文件作用域下使用 B const b = new B();

B.ts import { A } from './A';

export class B { constructor() { console.log('B is initialized'); } }

// 文件作用域下使用 A const a = new A(); 这种情况下会出问题,终于感受到了Spring的好了,当初阅读Spring的时候,就提到过一句,Spring主要解决的是对象间依赖的问题。

大文件中很容易产生循环依赖。 editor依赖tool中的工具,tool又依赖ink,因为tool要做一些全局性的操作,ink又依赖editor。

多模块看来是无法避免的了,要想代码跑起来,必须要先解决worker的问题,worker不动起来,代码无法正常运作。 要是想要把worker跑起来,那需要先解决多模块的问题。

import { xxx } from xxx 这原来也是一种解构赋值

需要使用到Monorepo来进行多包管理。这就体现解耦的重要性了,项目之间的依赖应该是单向的。viewer依赖于core,但是core不能依赖viewer。 monorepo的一些功能类似于maven。

参考了vue的源码,还查阅了一些资料,发现可以用monorepo来实现多模块化。

monorepo原来不是一个库,而是一种风格,我搞错了,实际上依赖的是pnpm。这一点我从vue的源码中学到了。

如果把所有的代码都分开了,那么这种双向的耦合该怎么办呢? 比如双方要通过MessageHandler来进行通信?

JS所面临的环境和Java有所不同,JavaScript写个库,调试还是依赖于浏览器。不像java跑个main方法就好了。

即便是分包这一点,可以参考Java中的做法,分两个包,声明是一个包,实现是一个包。不然互相调用很容易耦合上。产生循环依赖。

WebWorker中的坑也不少,WebWorker和主线程是隔离的。因此即使在主线程中加载的类,也没办法在WebWorker中使用。

面向接口开发还是很重要的,接口的存在,把类的耦合解开来了。Dict、XRef是在全局到处都能用到的对象,因此要把它们的声明写在common目录下,而具体的实现,则要写在core目录下,因为它的处理逻辑依赖很多其它东西,通过接口和实现的独立变化,实现了解耦。

TypeScript中的接口在运行过程中会被擦除,这个有点像Java的泛型,因此无法使用instanceof来判断一个对象是否是某个接口。这一点有点蛋疼。

PDFjs源代码的文件目录没有细分,如果文件目录细分做得好的话,每个文件夹下的代码都是干什么用的,看过去一目了然,不至于要一个一个的猜。

需要先对WebWorker有一定程度的了解: const webWorker = new WebWorker('worker.js') // 创建一个WebWorker线程,并开始在worker.js里运行代码。 worker.js中,使用self.onMessage来接受消息,使用self.postMessage来向主线程发送消息。 在主线程中,使用worker对象的postMessage来向worker发送消息,通过onMessage来回应worker的消息。

TypeScript提供的具体类型,虽然在编译的过程中会被擦除,但是已经好多了。

多模块还是好用的,通过多模块,巧妙的将pdfjs依赖的js文件隔离了开来。OPENJPEG就是一个代表。

worker这一点还不太一样,因为worker是要打成文件的,然后通过路径来加载,不像其它的代码,都是一些类和对象,只要正确声明了,依赖关系会被自动处理好,也不需要单独加载。worker是不一样的,所以这一点要注意。

worker和core应该分开放,worker是处理pdf的形式,而core是处理pdf的代码。二者不可耦合。不然会引起迁移问题。

seren-worker的js代码应该究竟是应该直接放在seren-worker里进行调用,还是应该由seren-worker暴露出来,然后由viewer来进行调用。那这样的话,seren-worker还有存在的必要吗?

seren-viwer直接加载seren-worker的代码,这合适吗?似乎这是不合适的,因为这违背了封装性,或者seren-worker直接打包出来一个js文件,给seren-viewer去加载,这样应该是合适的。如果seren-worker,只提供API,而非js文件,那么相当于seren-viewer需要去处理,那不是相当于seren-viewer需要研究worker的细节了?那么写seren-worker的意义何在呢?所以最终结果还是:seren-viewer要依赖于seren-worker最终生成的worker.js文件,并且直接在seren-viewer中引入加载就完事了。

问题:seren-viewer需不需要依赖seren-core?感觉是不需要的,甚至也是不需要依赖seren-worker的,因为seren-worker提供了一个独立的运行场景。

viewer就是viewer,只需要加载seren-core,而不应该依赖于seren-core。workerSrc应该单独处理,至于有些数据可以通过PostMessage来进行传递,这些数据只要声明好就行了,函数式不能被传递的,因此viewer无需依赖seren-core。只需要依赖一个公共的seren-common就行了,seren-viewer中还有一些代码可能要依赖seren-core里的代码,因此seren-core可能需要剥离一部分代码出来。况且渲染和解析,也本就有一些中间地带。