222022.4

用外观模式实现跨端开发

外观模式即对外提供统一接口,底层实现随意。对于使用者而言,使用基于外观模式的库时,在任何情况下,想要调用某个功能时,都使用相同的api。在日常中,常见的外观模式就是不同浏览器,都基于HTML标准提供相同接口,但是实际上浏览器内部怎么实现这些接口的,完全不一样(当然,由于不一样的实现,在实际运行时也会导致运行效果差异)。

那么在跨端开发时,怎么做呢?我们要创建一个H5页面和一个RN界面,我们使用相同的接口调用某库,然后希望被调用的接口在web和原生上都能用。例如:

import SomeLib from 'some-lib'

const pdf = SomeLib.print('xxx')

对于some-lib这个库而言,它提供 print 接口。在web和原生平台上都这样去使用,使用层面没有差异。但是,我们知道,web上和原生上调用系统的打印功能是不一样的,所以我们又要分开去实现不同平台上的具体能力。设计上怎么做呢?如下:

SomeLib.implement((text) => {
  console.log(text)
})

这是在web上的实现,在原生上的实现可能需要借助原生提供的接口来做。对于some-lib这个库而言,它本身实际上没有实现打印这个过程,这个过程要开发者自己去实现。但是这个库却提供一系列用于打印的功能。

通过这种设计,我们就可以在应用的开发上做到比较方便的跨端开发。

10:11:24 已有1条回复
  1. 和react有点像呢
    #1185 1188 2022-04-28 09:55 回复

23种设计模式

212022.4

JS如何判断用户是否点击浏览器“退回”按钮返回上一个界面?

通过window的popstate事件可以监听history的变化,但是,“popstate会在浏览器前进后退操作、history.go/back/forward调用、hashchange的时候触发”,它是一个复合事件,你根本判断不了到底是哪种情况引起的popstate。更难理解的是,我不知道为什么要把forward也设计为popstate,pop这个词的意思不就是从stack顶把最上面的一个从栈中移除么?forward明显是push的行为,怎么也放到popstate里面。这个事件明显是有设计缺陷的。回到题目,我们怎么去判断用户是点击了“退回”按钮?

我们要用到一些取巧的办法。具体我总结为3步:

  • 充实history stack,以提供更多信息让我们可以在用户刷新浏览器的情况下,仍然获得上下页信息关联
  • 为history创建一个私有的记录值,用以区分history当前的state和上一个state(我们在事件回调中只能拿到当前state)
  • 通过第一步和第二步铺垫的内容,在回调函数中进行判断,从而判断是否是用户点击了“退回”按钮

接下来我们进行实施。在此之前,我们需要了解一些简单的知识。history是浏览器用户记录用户浏览器历史的全局对象,既然是“浏览历史”,那么就是一组单一数据的列表(有顺序),这个所谓单一数据,就是其内部的state概念,一个state表示用户在浏览一个界面(对应一个url)时留下的痕迹,不过,这个state必须由开发者定义,如果开发者不定义,那么state就是null,而且为null的时候,就没哟意义。注意,浏览器不会主动帮你记录当前的url,虽然它自己记录在了浏览器内部,你可以通过浏览器的“浏览历史”功能查看,但是,你无法在代码层面直接读取这些历史记录,你只能读取history当前的state,即通过 window.history.state 来读取,当然,你也可以通过 window.location 来读取当前的url信息。“只能读取当前的”,也就意味着这是一种栈结构的数据管理,而且,这个栈在用户刷新当前浏览器tab时,仍然是维持的。

基于这一特性,我们可以自己在state栈中,构建一个链表结构,从而可以读取整个栈内的state链。具体怎么做呢?

我们知道history有pushState和replaceState两个接口,对于SPA应用而言,整个应用中只会使用pushState和replaceState两个接口进行url的跳转(还有一种是在a标签href中使用#触发,例如 <a href="#/base/xxx"> 这种也可以做到无刷新的界面跳转,但是由于它走另外一个体系,也就是hashchange的体系,是比较老的技术,现在大部分框架都是基于state的体系来做,因此,我们本文不考虑hashchange这种方案),因此,我们可以对这两个接口进行改造,从而在跳转时,对state进行信息充实。

const originalPushState = history.pushState.bind(history);

// 要求在调用pushState时state必须是一个对象
history.pushState = function(state, title, url) {
  const { state: currentState } = history; // 获取未跳转之前的state,也就是当前的state
  const nextState = state || {};
  nextState.prevState = currentState;
  originalPushState(nextState);
};

通过这一改造,我们重写了history.pushState,这样我们就让每一个被push到state栈中的state拥有了prevState属性,通过该属性就可以形成一条反向链表,用以追踪state之间的关系。

接下来,我们创建一个私有的变量,用来记录history.state发生变化前的state.

let latestState = history.state;

window.addEventListener('popstate', () => {
  const prevState = latestState;
  const { state } = history;
  latestState = state;
  ....
})

在每一次popstate被触发时,我们去修改latestState,这样,我们就可以记录在state发生变化前的state是哪一个。

最后,我们通过latestState和当前的state进行对比,来猜测用户是否点击了“退回”按钮。

window.addEventListener('popstate', () => {
  const currentState = latestState;
  const { state: nextState } = history;
  latestState = nextState;

  if (currentState?.prev === nextState) {
    // 用户点击了“退回”按钮
  }
})

当变化后的state正好是变化前state.prev时,我们就认为用户点击了“退回”按钮。当然,这里有一个点需要提前预设,因为在不同的SPA框架中,触发popstate的可能包含在代码中调用history.back()和history.go(-1)的情况,所以,在代码层面,需要继续去区分到底是程序里调用.back还是用户点击“退回”按钮。区分方法也很简单,你可以重写back和go方法,在里面做一个标记,从而在判断时增加对该标记的判断。

10:31:45 已有0条回复
022022.4

css-loader和file-loader/url-loader冲突无法显示图片

最近在调试的时候,同事反馈说图片没有展示出来,去看了以后简直莫名奇妙,因为我自认为对css-loader和file-loader的配置没有问题,难道css-loader又挖了什么坑?于是开始漫长的调试,测试过程。最终发现,问题是真的存在,而且我google了半天,恁是在国内外都没有找到靠谱的解决方法。调试过程中可以肯定的是,css-loader竟然自己把url()的图片生成到目录下了,然后file-loader又做了一遍:

这也就意味着一张图片被处理了两次,具体造成的问题是css-loader给的路径不对还是file-loader给的不对,就不想去深究,总之就是两个loader冲突了。按照我以前的理解,css-loader只负责把url()转化为require()的形式,并不负责生成真实的图片,然后file-loader可以识别require(image)的形式,并生成图片,两个loader分工明确。那么这里是为什么呢?于是,我把file-loader从rules中去掉,然后……图片竟然展示出来了!!

所以说,无论是css-loader的问题,还是结合webpack5一起的问题,现在webpack5+新版css-loader就可以自己把图片处理到目标目录下了。然而,问题在于,如果去掉file-loader,那么我们在js中是没有办法直接import一张图片作为url来使用的。这就矛盾了。而且检查了file-loader的配置,也没有接口可以让我们忽略对css文件中的图片进行排除。似乎就这样卡住了。

于是重新回去阅读文档。

果然,不独不知道,webpack5现在已经牛x到自己可以把非js的文件资源作为模块读取了。什么?不懂?意思就是,不需要file-loader了,webpack内置了工具!具体做法就是把我们配置文件中的 loader: 'file-loader' 一句改为 type: 'asset/resource' 就可以了。

webpack的rule.type在v4中就已经加入了,只是很少被用到,它的文档也说的不清不楚,我也不知道它有这个能力,而 asset/resource 也是到v5才悄悄加进去的,你可以查看v4的文档v5的文档的差异。

新增的asset这个选项就是对非js相关的资源的处理,它文档单独通过一节对这些项目进行解释,而且里面就明确提到可以代替以前的file-loader, url-loader, raw-loader,然而,大哥你虽然可以代替,但是直接破坏原来的效果,是不是有点不厚道呀!

00:28:48 已有4条回复
  1. 来 vite 保平安(逃
    #1178 rxliuli 2022-04-03 17:15 回复
  2. Cool!
    #1183 ayu 2022-04-16 16:09 回复
  3. Cool
    #1300 HduSy 2024-01-27 12:17 回复
  4. 确实,如果升级后想用的话,可将`type: javascript/auto`,同时`esModule: false`
    #1301 HduSy 2024-01-29 19:06 回复