基于构建的无侵入类方法实现:用webpack loader来改写原始类
在项目中我们需要针对不同的平台(Web和Native)做不同实现,在抽象中实现大部分功能,把细节实现留到具体平台的代码中,但是一直没有找到好的方法,如果直接在入口文件中引入实现文件,就会导致原始代码被打包的启动文件中,文件体积变大,但如果使用import()又无法确保每次代码加载都是ok的,总不可能自己实现顶层的await import,另外这些实现常常是侵入式的,需要对原始类进行方法覆盖。今天想到一种基于构建工具的无侵入式实现,这里无侵入是指把实现作为旁路代码,而不是主体入口代码。这种旁路代码有点像依赖注入,但是是从构建工具的角度来做,实际上也很简单,通过webpack的loader,把原始代码进行改写,把实现代码合并到原始代码中去。
/* eslint-disable @typescript-eslint/no-require-imports */ const fs = require('fs'); const path = require('path'); module.exports = function (contents) { const { resourcePath } = this; const options = this.getOptions(); const { abstractDir, implementDir } = options; if (resourcePath.indexOf(abstractDir) === 0) { const implementFilePath = path.resolve(implementDir, resourcePath.replace(abstractDir, '.')); if (fs.existsSync(implementFilePath)) { const implementContents = fs.readFileSync(implementFilePath).toString(); const newContents = composeFileContents(contents, implementContents); return newContents; } } return contents; }; function composeFileContents(sourceContents, implementContents) { const sourceLines = sourceContents.split('\n'); const implementLines = implementContents.split('\n'); implementLines.shift(); // 去掉第一行,第一行是对原始文件(要被实现的文件)的引入 const { imports: sourceImports, codes: sourceCodes } = splitCodes(sourceLines); const { imports: implementImports, codes: implementCodes } = splitCodes(implementLines); const { contents: imports } = composeImports(sourceImports, implementImports); const { contents: codes } = composeCodes(sourceCodes, implementCodes); return `${imports}\n${codes}`; } function splitCodes(lines) { const imports = []; const codes = []; let reach = false; let incomment = false; lines.forEach((line) => { const text = line.trim(); const push = () => { if (reach) { codes.push(line); } else { imports.push(line); } }; // 忽略注释 if (text.indexOf('/*') === 0) { incomment = true; push(); return; } if (text.substring(text.length - 2) === '*/') { incomment = false; push(); return; } if (incomment) { push(); return; } if (text.indexOf('//') === 0) { push(); return; } if (text.indexOf('import ') === 0) { imports.push(line); } else { codes.push(line); reach = true; } }); return { imports, codes }; } function composeImports(sourceImports, implementImports) { // TODO: 需要考虑如果import了相同的变量名的问题 const importMapping = {}; const importVars = {}; sourceImports.forEach((line) => { if (line.indexOf('import ') !== 0) { return; } const { vars, src, def } = parseImport(line); importMapping[src] = { vars, src, def }; if (vars) { vars.forEach((v) => { importVars[v] = true; }); } if (def) { importVars[def] = true; } }); // TODO: 暂时未考虑default的冲突问题 implementImports.forEach((line) => { const { vars, src, def } = parseImport(line); if (importMapping[src]) { const importVars = importMapping[src].vars; // TODO: 暂时未考虑import as后的变量名冲突问题 if (vars && importVars) { importVars.push(...vars); } } else { importMapping[src] = { vars, src, def }; } }); // 先处理原始的 const results = []; sourceImports.forEach((line) => { if (line.indexOf('import ') !== 0) { results.push(line); return; } const { src } = parseImport(line); const importText = createImport(importMapping[src]); results.push(importText); delete importMapping[src]; }); // 再处理多出来的 const srcs = Object.keys(importMapping); srcs.forEach((src) => { const importText = createImport(importMapping[src]); results.push(importText); }); return { contents: results.join('\n') }; } function composeCodes(sourceCodes, implementCodes) { const source = sourceCodes.join('\n'); const imports = `(function() { ${implementCodes.join('\n')} } ())`; return { contents: `${source}\n${imports}` }; } function parseImport(importLine) { const [, exp, src] = importLine.match(/import ([\w\W]+) from ['"](.*?)['"]/m); const parseVar = (txt) => { // if (txt.indexOf(' as ') > -1) { // const [, v] = item.split(' as '); // return v.trim(); // } const t = txt.trim(); return t; }; const parseVars = (txt) => { const t = txt.substring(1, txt.length - 1); const items = t.split(','); const list = items.map(parseVar); return list; }; const txt = exp.trim(); if (/^\{.*?\}$/.test(txt)) { const vars = parseVars(txt); return { vars, src }; } if (/^\w+,.*?\}$/.test(txt)) { const [d, i] = txt.split(',').map((item) => item.trim()); const vars = parseVars(i); const def = parseVar(d); return { src, vars, def }; } const def = parseVar(txt); return { src, def }; } function createImport(mapItem) { const { vars, src, def } = mapItem; if (def && vars) { return `import ${def}, { ${vars.join(', ')} } from '${src}';`; } if (def) { return `import ${def} from '${src}';`; } if (vars) { return `import { ${vars.join(', ')} } from '${src}';`; } return ''; }
这里的实现方式比较暴力,就是通过替换的形式,把实现代码合并进原始代码。这些代码一般被放在一个叫 @implements
的目录中,没有任何其他文件引用它们,构建工具根据其给定的路径进行匹配,如果路径匹配上了,就执行合并逻辑,因此称它们为旁路代码。不过,如果原始文件中缺少这部分实现,则无法运行。
虽然这种方式割裂了代码本身的逻辑,无法通过编辑器的源文件链接找到,但是,这种方式借助构建工具,使得代码层面更加清晰,最终的产物更加合理(主体代码中不存在与之无关的代码)。
如何终止fetch发出的请求?
我们知道xhr可以调用abort来终止请求,但是fetch如何终止却鲜有人知,其实非常简单,只是我们不够了解,即使用AbortController,具体如下:
const controller = new AbortController(); const signal = controller.signal; const url = "video.mp4"; const downloadBtn = document.querySelector(".download"); const abortBtn = document.querySelector(".abort"); downloadBtn.addEventListener("click", fetchVideo); abortBtn.addEventListener("click", () => { controller.abort(); console.log("Download aborted"); }); function fetchVideo() { fetch(url, { signal }) .then((response) => { console.log("Download complete", response); }) .catch((err) => { console.error(`Download error: ${err.message}`); }); }
即在fetch时传入signal作为信令,当调用controller.abort()时,该信令就会发出终止请求的信号,fetch发出的请求就会被终止,其Promise会抛出一个AbortError。
前端代码中,哪些可以抽象,哪些不可以?
抽象的目的绝对不是复用,而是为了清晰的设计。从某种意义上讲,抽象分为两种,一种是绝对抽象,只有神态,没有实体,另一种是轮廓抽象,或者半抽象,有了基本的架子,留下空间去具体化。前一种我们常称之为接口interface,后一种我们常称之为抽象类abstract class。对于前端代码而言,在传统前端开发模式中,不存在这两种中的任何一种,对于abstract class则可能存在一些雏形,例如:
class Some {
stay() {
throw new Error('stay should be overrided')
}
}
此类处理虽然可行,但是在最终代码中会多出来许多没有用的代码,增加代码量。只有在我们引入ts之后,才有了真正的抽象代码。例如:
abstract class Some {
abstract stay(): void
}
这段代码定义了一个抽象类,它不能被new实例化,只能被extends扩展。扩展时,带有abstract前缀的成员必须被实现,否则编译阶段会报错,而在最终生成的代码中,不存在这些abstract成员,这样就可以使代码量最小化。
在具体业务中,哪些内容可以抽象,哪些不可以呢?
其实对于前端而言,基于interface的抽象(业务层面)几乎没有,我们很少会去写一个用于描述业务的interface,而且这完全没有必要。我们大部分情况下使用abstract class进行抽象,甚至在大部分情况下,不需要abstract,直接使用class进行抽象。这里抽象脱离了技术层面的抽象,而是对业务进行抽象,同时由于大部分前端场景中,同一业务是固定不变的,因此,这类抽象可以被具体实现。例如对于同一对象实体,我们直接对其进行建模。再例如,我们直接对数据请求进行建模。对于数据的操作,我们进行建模。总之,你会发现,抛开界面和交互的一切,都是可以进行建模处理的,这个部分全部可以抽象出来,在代码中形成一块封闭的可扩展的代码块,需要时被取出来使用,不需要时不import,对当前毫无影响。
最麻烦的是界面和交互的抽象。
先看下交互的抽象,由于交互动作往往会引起界面的变化,所以我们在对交互进行建模时,就必须预留下可产生界面变化的abstract成员,从而可以使界面产生变化。例如:
abstract class SomeView extends View {
@inject(SomeController)
controller: SomeController
abstract confirm(message: string, onOk: Function): void
deleteItem(id) {
this.confirm('确定删除吗?', () => {
// 执行删除
})
}
}
上面代码中,我们预留了一个confirm方法,对于具体的某个view而言,必须扩展这个confirm方法来,为什么要留呢?因为confirm往往需要弹出一个对话框,同时,这个对话框一定是一个中间态,用户点击它的按钮之后,一定还会有后续界面变化。这种界面的流动过程,无法通过简单的处理来实现。当然,如果你想具体化,还可以借助react的state,例如:
class SomeView extends View {
state = {
showConfirm: false,
deleteItemId: null,
}
deleteItem(id) {
this.setState({ showConfirm: true, deleteItemId: id })
}
handleDeleteItem() {
// 执行删除动作
}
}
通过以上方式确实可以做到提供完整的动作,但是这就意味着必须按照react的状态管理模式进行编程,而且对于使用方来说,一个动作的方法太多了,在实现时,不仅要调用deleteItem还要再调用handleDeleteItem。不过,从另外一个角度看,这似乎又是正确的一种做法。
最后看下界面的抽象。这个时最难的,因为不同端端界面呈现是不一样的。比如PC和APP上。但是我们也不是不能做,其前提是开发者,或者项目的架构师,在前期规划了非常细腻的业务组件,我们所有的业务开发,基于已有的业务组件进行,我们写代码,更想写配置或DSL,比如下面:
<Page>
<ProjectBasicInfo />
<ProjectMembers />
<ProjectDeals />
<Tabs>
<CompanyInfo />
<FinancingInfo />
</Tabs>
</Page>
这样一段代码,更像是一个页面的结构描述,至于每一个部分都具体展示什么内容,怎么展示,界面交互怎样,PC和APP上的差异,全靠业务组件内自己去实现,通过这种方式来进行界面的抽象,可以最大程度的抹平不同端的差异,但是对团队和架构师的要求会比较高,当然收益也是显而易见的,就是效率很高。
随着年龄的增长,学习成为一件越来越奢侈的事情
“活到老,学到老”听上去是一件非常容易的是,它告诉你,只要想学习,什么时候都不晚,但是在现实生活中,这种自以为是的哲学,本质上是钳住人思维的一种毒药。人的主要学习时间是在我们普通人常见的读书阶段,大学毕业之后,很难再持续学习。但当我们意识到这一点的时候,已经回不去了。“人最重要的投资,就是学习。”我觉得这句话及其重要,而且不仅适用于“投资自己学习”,也适用于“投资家人、下一代学习”。当然,投资是有风险的。我以前觉得,小孩子嘛,能否学的好不一定靠成绩体现,具有一种浪漫主义色彩的自由学习论。但随着年龄的增长,我们的很多思想都发生了变化。你可以说随波逐流,但我称之为逐渐意识到了前人的智慧。
学习本质上就是一种能力。在我们青年时期,我们的首要任务是学习“如何学习”这项能力。当我们习得这项能力之后,我们便可以用它“学习”其他的知识。因此,我们可以看到以前很多学渣,现在混的很好,因为我们在学校的时候,我们自以为是的认为考试成绩是以自己掌握的多少知识来进行打分,而实际上,我现在发现,考试成绩是以掌握知识与掌握知识的时间比来进行打分,所以哪些拥有100分的同学,应该思考一个问题,如果自己的100分是建立在100天的基础上,那么那些只用了3天临时抱佛脚得到70分的学渣,真的是学渣吗?本质上,我们的考试结果,是“学习效率比”,而学习效率比的本质,就是“学习的能力”。掌握了“如何学习”的能力与掌握了“通过学习获得的知识”相比,前者不仅简单的多,而且有利的多。任何的知识都会忘记,但是一旦掌握了“如何学习”,那么任何忘记的知识都可以快速重新获得,而如果我们把大量的时间用来掌握“通过学习获得的知识”,那么最后我们只能获得一堆很快就会忘记的知识,而不是获得一项终身受用的能力。
如果我们的工作不是做文学研究,就没有必要研读小说的每一个字,而是知道故事的梗概和作者的核心思想即可;如果我们的工作不是建筑设计师,那么就没有必要挖掘每一页照片和梁柱结构的细节,而是只要像浏览杂志一样从各种精美的构图中找到灵感即可;如果我们的工作不是系统工程师,那么就没有必要对每个自然段的来龙去脉理解的那么透彻,而是能够对章节大标题的主要内容和涉及问题有轮廓的理解即可。书尽读不如不读。我们只需要对自己所在专业,或者自己正在从事的工作所需要的知识,进行深读深专,而不应该纠缠于那些用于扩展思路的书籍中的细节。同样,面对开源项目也是这样。
不过,现在最关键的是,我们没有时间来进行如上的操作。除了平时工作时间占据了过长,生活琐事无休无止外,还有一个不得不面对的问题,就是社交媒体和短视频让我不“吸上两口”没法过好这一天。以上这些问题阻碍了我们在随着年龄增长过程中的持续学习过程。“持续学习”和“学习”本质是不同的,“学习”的本质是“学习如何学习”,而“持续学习”的本质是“通过发挥掌握的如何学习的能力,快速掌握目前自己需要的某些既能或想法,以开阔自己在工作或某项事务中的思路,帮助我解决问题”。如果人生没有“学习”,那么就只会停留在离开学校的那一刻,那一刻你什么样,就什么样;但如果人生没有“持续学习”,那么就没法解决问题,生活问题积累越多,人就会崩溃。我小时候经常听到“学习型社会”,意思是人们通过持续学习解决自己的生活问题,从而让社会和谐。但随着经济发展,这种提法被抹掉了,似乎对于某些人而言,矛盾越多,利益越大。
我前不久买了几本书,觉得应该很快可以看完,可今天发现,半本都没读完。人啊,有的时候对自己太自信,是对自己的侮辱。说来说去,还是因为懒,社交媒体短视频“吸”的不亦乐乎,哪还有心思读书。等到疫情来了,阳了,嗯,没时间刷短视频了,时间都用来照顾家人了,至于学习,早丢到九霄云外。人呐,有的时候有时间,觉得自己很富有,直到事情一来,才发现,自己不是富有,自己是蜉蝣。
有什么是vue能做到而react做不到的?
我在知乎讲react是全量更新脏检查,很多人听不懂,说我在乱说,我也懒得理会。我有一个例子,这个例子里面,我们可以看到react是全量更新,而vue可以做到定点更新(当然,vue是不是实现了另说)。我们来看下结构:
<a>
<b>
<c>
<d>
<e>
在这个结构里,我希望以直观的形式,去监督某个值变化,来实现<d>的更新,注意,这里<d>是一个node。在react里面,我们的写法可能是这样:
<SomeComponent attr={this.props.tableTips} />
如果想要更新SomeComponent的渲染结果,要么SomeComponent内部做了什么处理,让其自身可以根据某些条件重新渲染,要么必需是当前这个组件重新渲染,让SomeComponent被动重新渲染。但是为了重新渲染<d>而把a, b, c, e都重新渲染一遍,代价太大。有没有什么办法,在不重新渲染当前组件的情况下,重新渲染<d>这个片段?
一种方法是把d再封装一层,作为一个高阶组件,把需要监听的内容传入该封装好的组件,由组件来监听和重新渲染,这样就可以做到局部重新渲染,而不需要整个重新渲染。但是,这种方法也很笨,因为不可能存在一种万能的高阶组件来达到这个目的,所以到处去写这样的组件也不好。另外,高阶组件的使用,割裂了阅读组件结构的顺序,让我们在后期的维护中,不得不跳来跳去看代码。
而vue就可以做到基于模板来优化这一需求。例如:
<some-component attr="tableTips" />
就可以在模板编译时把tableTips作为可动态读取的片段,这样,每次在渲染some-component时,读取到的attr都可能是新的,从而带来重新渲染。
通过这个例子,我们就可以发现,虽然jsx具有非常强的表达力,但它始终基于原生语法,而作为模板语法,当然具有更强的能力。
-
首先React肯定能局部渲染,不然官方文档就是笑话(https://reactjs.org/docs/rendering-elements.html#react-only-updates-whats-necessary)。
Vue、React的渲染都是要通过vnode diff做更新,Vue也没细粒到标签级别的vnode,只能说Vue的tempalte编译期间做个太多的aot优化,Jsx因为要使用js/ts的全部能力,注定做不了很好的优化。#1253 断崖上的风 2022-11-22 13:33 -
你说的那个局部渲染叫commit,不是一回事,麻烦看懂再来讨论#1256 回复给#1253 否子戈 2022-11-22 22:27
-
如果你说的是前面的render过程,你说的对,默认情况下调用setState都会导致组件及其子孙的render方法的调用。
这就是React跟Vue的不同之处。Vue基于订阅发布模型的数据响应可以在数据变化时准确知道应该通知谁要做更新;React推崇函数式,没有依赖收集过程,调用setState后无法知道依赖的数据是否更新,默认只能进行render,也因为粗暴的渲染机制,所以也才有了shouldComponentUpdate这样的api。
以上是否就是你想说的?
至于你说的脏检查,应该说的是渲染组件及其子孙吧,毕竟对应state,React没做任何检查。#1259 回复给#1256 断崖上的风 2022-11-26 10:32
在前端,你聊建模和分层,总会被人烦
过去两年,我遇到比较尴尬的情况,就是无法很好的解释建模和分层的总要性和便利性,因为前端领域关于这块的东西共同语言比较少,大家都是在专注于视图层的新技术研究,而在更广泛的编程范式、原则上,比较少去探讨,因此,每当我聊到这个东西的时候,无法快速的聊核心思想,必须在外围给听者建立场景,举例子,但是这些例子在日常前端开发中,又不是非得按你这套才能实现,所以听者往往带着自己的想象和经验先对例子进行脑海复盘,然后来评价所谓建模、分层都太复杂,这就是非常尴尬的场面,你还没有开始聊建模和分层本身,就已经在前期预热阶段被听者否定,那么,后续所讲的任何话都没有任何意义。
前端建模和分层是非常重要的,它本身并不复杂,也不会增加工作量,问题在于它所要求开发者具备的思维方式、理念是比较庞大的,因此往往被抵触。如果你在完成一个需求时,提交代码进行review,直接被对方打回来,理由是不符合我们的开发理念,你一定也会很生气。但是这种东西往往需要有一个强有力的人去推动,他会去review每个人写的代码,是否按照分层理念去写,是否符合开放封闭原则,是否遵循整洁设计,是否考虑周期等等。而要确保这些,review人总需要花比较长的时间,除了那些对软件质量要求高的企业,很少有团队这样去做,包括现在国内的一线大厂,都不舍得这样去做,“我花钱来就是请你看别人代码的吗?”对他们来说,快速写出功能上线,才是最重要的,至于代码质量早被抛诸脑后,等到出了事,就“杀一个程序员祭天”。
在这样的环境下,把前端应用当作软件,追求其稳定性健壮性,就非常不合群,因为稳定健壮的软件,会淡化开发者本身的成就,只会把最终的成就摊平到日积月累的时间长河中去,除非是某个强有力的tech leader从古老的开始就把它当孩子般培养,否则没有人愿意做一些短期内看上去根本无法凸显自己价值的事,而且更可悲的是,软件健壮稳定了,用户本身根本没有任何感知,而没有感知就会直接忽略你的劳动成果,相反,那种到处漏洞,你天天救火的,在甲方看来,还觉得你态度好,能力也够。
所以,我已经在考虑避开聊建模分层这个话题了,我想接下来我会去研究一个新的领域,这个领域得有一定的需求,同时又有深度。
-
整体上前端处于鄙视链底层也是有从业人员自身的原因。#1221 断崖上的风 2022-09-08 09:58
-
太赞同你说的这些了,然后我又想起了这篇文章:http://www.lowcoder.cn/best-practice/detail?fid=BZxGdCrUX2。不知道你怎么看待目前的应用技术发展,比如近几年涌现的微前端,低码和跨端技术。#1222 戡玉 2022-09-08 11:09
-
这块确实全在细节里,不容易被察觉。 但一个需要天天救火的项目和一个平稳运行的项目,甲方应该也是能看出差别的。#1225 1188 2022-09-16 18:29
typescript class static静态方法内this类型约束
typescript中只能对class成员进行约束,无法对static方法进行约束,我们通过一些特殊方法来实现。
/** * 用于得到某个class的构造函数,例如: * class Some {} * Constructor<Some> -> Some类型的构造函数,也就是class Some本身 * 用处: * class Some { * static fn<T>(this: Constructor<T>): void; // -> this: Constructor<T> 规定了该静态方法内的this类型,由于类型推导,此处的this被推导为Some本身 * } */ export type Constructor<T> = new (...args: any[]) => T;
通过Constructor这个util,我们可以获得一个class的type,也就是class构造函数本身。所以,当我们在静态方法中进行this约束,或返回this时,可以这样:
class Some {
static create<T>(this: Constructor<T>): T & { a: number } {
// @ts-ignore extends 要求特定结构
return class extends this {
a: number = 1;
}
}
}
通过这样对static方法进行签名,就可以在使用时很好的约束类型。
Javascript如何区分function和class?
JS里面,普通的function也可以通过new进行实例化,成为一个对象。而ES6引入的class是个阉割版本,导致class缺失了作为class的特征,class可以理解为封装的function。所以,在JS里面区分一个值是function还是class是比较麻烦的,目前TC39已经在考虑加入[[FunctionKind]]来进行区分。目前,我们可以通过一些办法来区分,如果是在纯浏览器环境下,我们有如下的一些办法:
- 通过字符串
把function或class与空字符串连接,得到字符串就可以看出来。
- 通过prototype的discriptor
对于class A而言Object.getOwnPropertyDescriptor(A, 'prototype')的writable为false,而对于function a而言,Object.getOwnPropertyDescriptor(a, 'prototype')的writable为true。
- 通过arguments
对于class A而言,A.arguments会报错,而对于function a而言,a.arguments为null。
这些方法都抵挡不住现在很多编译工具会把class编译为ES5的function,这导致这些特性都失效,所以能不能用还要看你项目里面的编译是怎么做的。