回顾前端系列 - 前瞻

前言

回首自己从2014年入行至今已有了5个年头,对自身的技术实力还是十分不满意,因此想利用这段时间对自己的技术实力进行一个提升。
同时如果没有扎实深厚的技术实力,很可能会演变成写了许多自认为非常好的作品,但实际缺乏设计,只是闭门造车。

冲击目标

1. 基础知识体系

大体划分:

(1) js

(2) 常用框架、类库

(3) html

(4) css

(5) 计算机基础

(6) 算法、设计模式、语言

(7) 网络&安全&浏览器内核

(8) 工程化

(9) 服务端、数据库、node

2. 个人亮点以及解决过的问题

(1) 个人亮点

[1] 工程化
[2] 插件开发经验

(2) 解决过的问题罗列

[1] 技术细节
[2] 设计过的方案 

3.形式

(1) 按大纲更新

(2) 一天一道前端题

(3) 一天一道算法题

目前先罗列了这些部分…

Share

浅聊前端依赖管理及优化(上)

一、引子

在npm、yarn等包管理工具的帮助下,前端工程化模块化的进程日益加快,项目的依赖包也日益增大,特别是若项目采用webpack来构建用到许多webpack的插件、一些辅助开发如(eslint、postcss、dev-server之类的库)以及一些单元测试(jest、mocha、enzyme)可能需要用到的插件,项目中的node_module就会变的十分庞大。

如图:

如果搭配这种情况是不是很绝望:

因此团队开发者每次重新初始化项目进行npm install会十分缓慢,并且若占用大量空间,更进一步来说项目中往往很多前端团队会用到一台构建服务器,对前端项目的代码获取、打包、构建、部署进行管理(例如笔者自己所在的团队就在使用jenkins对前端项目的打包、构建、部署进行自动化托管)。npm这样的项目级依赖管理的特性就会造成大量的时间以及资源的消耗。

同时笔者也想在文章开始之前表明一个态度,npm本身portable的模块化设计,使每个项目互不干系,也是一种它设计的精华所在,本文仅是针对实际使用中遇到的一些小困扰进行解读,希望提供一个新的思路。

此本篇文章就在这样的背景下诞生了。纯粹是笔者在项目中积累的一些经验,若有不足望指出。

二、先来说说示例

github仓库地址:https://github.com/Roxyhuang/npm-module-share

通常现代前端的标准工程往往具备以下功能,为了让这个讨论更贴近实际开发,并且更直观,因此规定示例项目基本具备以下这些能力:

(1) 提供dev-server

(2) 提供语法转译(babel)

(3) 能够解析样式

(4) js代码风格插件以及样式风格检查

(5) 单元测试

(6) 提供样式兼容处理

当然以上部分只体现的依赖层面。因此可能有些依赖的引入可能出现错误或者不合理。

并假设目前团队使用了webpack和parcel两套前端构建方案,团队项目中有使用了React和Vue。

三、问题的总结

若不对项目做任何优化以及设置正常项目的依赖的结构应该是这样的:

如图:

然后我们再看一下node_modules整体所占的空间:

情况一:多个依赖模块完全相同的项目

这种情况相对很常见,实际就是相同依赖的项目有多个

如图:

可以想象这样模块的容量便是162.2mb x 3,同时文件数也会是1072 x 3。

由此可见实际我们对容量的消耗是是十分大的,笔者做了非精确统计实际:

生产中所需的模块容量与文件数(dependencies):

本地开发和测试所需的模块容量与文件数(devDependencies):

实际可以看到devDependencies才是我们整体依赖体积与数量如此庞大的“元凶”。

当然有部分生产中全局所需的库(如:React Vue)可以采用外部化,通过CDN来引入。(即图上CDN模块的复用)

情况二:仅生产中所需的模块依赖依赖重复 (dependencies)

如图:

生产中所需的模块容量与文件数(dependencies):

仅图中dependencies中红色标注的react-redux,redux版本以及使用了mobx react-mobx更换

本地开发和测试所需的模块容量与文件数(devDependencies):

无变化

实际这种情况和情况一类似只有dependencies中部分模块的依赖有差异或者版本不同。当然再进一步,这个部分每个项目确实可能出现较大的差异,同时着部分差异可能会引发devDependencies也出现变化(如:React Vue 可能用到webpack Loader eslint plugins等,之后会提及这块)。

实际依然devDependencies才是我们整体依赖体积与数量如此庞大的“元凶”。

情况三:本地开发和测试所需的包依赖重复且,但不存在版本差异 (devDependencies)

如图:

这种情况我们也囊括了情况二的差异(由于dependencies产生的差异,同时也会影响devDependencies的)

生产中所需的模块容量与文件数(dependencies):

如图所示,除了情况中出现的react-redux,redux版本有差异,由于出现了webpack + vue的项目,项目中dependencies的依赖需要引入vue或vue-router,除此都是一致的。

本地开发和测试所需的模块容量与文件数(devDependencies):

由于我们需要引入vue,导致我们devDependencies需要产生一些变化包括移除一些webpack与react相关的插件,eslint与react的插件等,变更为webpack与vue相关的插件,eslint与vue的插件等。

如图笔者非精确的统计一下这种情况仅devDependencies下的容量与文件数:

还是依然很大,并且实际还是存在大量的依赖重叠(例子中有17个依赖重叠)。

情况四:多个构件工具,存在本地开发和测试所需的包依赖重复,且存在在版本差异, (devDependencies)

这种是目前最复杂的情况,存在下列几个情况

(1) 前端构件方案存在多个, 例子中为webpack 和 parcel

(2) 生产中所需的模块存在差异,甚至技术栈不同

(3) 本地开发和测试所需的包依赖重复,但有略微不同,且存在版本不同

如图:

这种情况我们也囊括了情况二的差异,可以看到同一个构件工具下存在本地开发和测试所需的包依赖重叠的情况依然是较多的。

因此实际如果出现这种情况,我们仍需要考虑以及解决。

小小的总结一下:

结合情况一情况二情况三情况四的例子可以得出几点小小的“规律”:

(1) devDependencies无论从模块所占体积还是文件数量都在整体依赖模块中所占的比较较大

(2) dependencies下的包,可能会因为项目不同差异较大,同时也会导致devDependencies下模块有差异

(3) 即使devDependencies可能项目存在差异,但是仍有可能一部分重叠

(4) 多个模块化方案,可能会导致项目devDependencies差异较大

我们可以发现其中不管前三种情况的哪一种devDependencies都会有一部分的依赖模块是重叠的。那既然存在重复,那我们就有必要考虑“复用”

同时我们发现多个模块化方案(webpack, parcel)devDependencies下的模块差异会较大,这也是我们之后需要解决的。

锁定依赖模块的能力:

非本文重点,仅简述一下:

(1) lock or dont’lock

这个问题社区探讨的老问题,锁不锁都有支持的一方,然而这边笔者不表明态度,但是同时也希望我们之后的依赖管理的优化和“瘦身”能够具备锁定依赖的能力。

(2) npm 锁定依赖以及锁定依赖的内部循环的方式

我们关注的重点是我们需要依赖package-lock.json,来协助锁定依赖的版本以及依赖内部循环的版本。

再总结一下:

(1) devDependencies无论从模块所占体积还是文件数量都在整体依赖模块中所占的比较较大

(2) 项目的dependencies可能会因为项目不同差异较大,同时一部分可以外部化,因此不在本文想要探讨“瘦身管理”的范围内,列出主要是以便更接近现实状况

(3) 即使devDependencies可能项目存在差异,但是仍有大量的依赖重复与重叠,根据实际包的分析他们是目前依赖“瘦身管理”的主要目标

(4) 即使dependencies有差异的项目,devDependencies大部分可能相似(如情况三所示),因此我们仍可以提炼出一部分相同的devDependencies进行“瘦身管理”。

(5) 多个模块化方案,可能会导致项目devDependencie差异较大(如例子中webpack和parcel)可能会有一部分devDependencies可能版本也会不同,不建议两个项目差异极大的node_modules共享。

(6) 同一个模块可能会存在版本差异,因此我们可能会面对同时面对一个库存在多版本的情况

(7) 无论依赖锁定是否必要,但是希望提供的方案能够具备。

(8) 依赖锁定,需要依靠package-lock.json实现(shrinkwrap.json亦可)

四、优化方案以及存在的一些问题

下面的优化方案都依赖了NodeJS模块加载机制,因此先粗略的聊一下,这里不描述完整的过程,感兴趣的朋友可以查询官方文档或可以看一下朴灵大大的深入浅出NodeJS 。

如何利用NodeJS模块加载机制

简单来说 - 在NodeJS中引入模块,需要经历如下3个步骤:

  • 路径分析
  • 扩展名分析
  • 编译执行

我们利用的则是在路径分析中自定义文件模块(第三方npm包)的查找:

自定义文件模块查找顺序为:

  • 当前目录下node_modules目录
  • 父目录下node_modules目录
  • 向上逐级递归直到根目录下下node_modules目录
  • 递归至根目录

有点类似于JS的原型链查找,文件路径越深,模块查找越耗时,同时也是它慢的原因。

当然这是默认情况下,实际通畅我们可以通过配置NODE_PATH的方式,在递归至根目录后,若依然无法找到,给到一个路径或多个路径找到具体模块。

1. 配置NODE_PATH(方案一)

方案一实际就是利用了配置NODE_PATH,通常一般会将其配制成npm i -g所在的全局模块目录(实际可以改,本例暂定这个目录)。

结构变动

情况一、情况二、情况三都可以转化成如下结构:

情况三下为例子 - 如图:

具体实现:

在对应系统的环境变量配置文件中增加环境变量NODE_PATH,例如在MacOS中

1
2
3
vi /ect/profile  
# 或/etc/bashrc或~/.bashrc
# 此处不赘述配置问题、加载顺序以及原理
1
2
3
4
5
-> export PATH=$PATH: 
# 将 /usr/bin 追加到 PATH 变量中

-> export NODE_PATH="/usr/lib/node_modules;/usr/local/lib/node_modules"
# 指定 NODE_PATH 变量

那 NODE_PATH 就是NODE中用来寻找模块所提供的路径注册环境变量。我们可以使用上面的方法指定NODE_PATH环境变量。并且用;分割多个不同的目录。

关于 node 的包加载机制我就不在这里赘述了。NODE_PATH中的路径被遍历是发生,从项目的根位置递归搜寻 node_modules 目录,直到文件系统根目录的 node_modules,如果还没有查找到指定模块的话,就会去 NODE_PATH中注册的路径中查找。

存在问题:

(1) 不会生成package.json,因此对依赖管理比较繁琐,实现增量安装比较繁琐

  • 当然其实可以不需要npm i -g去处理,可以手动自己维护一个目录包括package.json,以及package-lock.json,但如果这么只会形成只是一个线性关系,而非一个树状关系。

(2) 不支持同一模块同时存在不同版本,因此如果依赖出现版本差异,没有解决方案

  • 因为形成的是一个线性关系,而非一个树状关系实际项目若存同一依赖版本差异,就会有一个优先级的问题存在。

(3) 全局安装模块,无法生成package-lock.json无法锁定依赖内部循环(依赖模块的依赖)

  • 默认如果使用npm i -g安装,同样不会生成package-lock.json,因此无法锁定依赖内部循环。

2. 提升node_modules目录(方案二)

结合之前问题分析的我们得出的结论,因此实际上我们把项目中devDependencies依赖重叠的模块,在项目的父目录存放node_modules即可将依赖提升。即可以进行项目间的共享。

目前笔者的理想方案应该能够达到下列几个目的:

  • 可以将项目构建相似技术栈的项目统一在一起,共享依赖

  • 可以实现对公共模块的维护和管理,并实现增量安装

  • 对原来npm install流程改动不会太大

(2)结构变化后的依赖结构图

实际需要实现只要将项目目录改为以下结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
----|---wwwroot/ # 工作目录或部署目录
|
|---webpack/--|---webpack-react/---|---package.json
| | |---package-lock.json
| | |---node_modules/
| | |---src/
| | |
| |--- webpack-vue/--- |---package.json
| | |---package-lock.json
| | |---node_modules/
| | |---src/
| |---node_modules/ |
| |---package.json |
| |---package.json-lock|
| | |
|---parcel/---|--- parcel-react/---|---package.json
| | |---package-lock.json
| | |---node_modules/
| | |---src/
| | |
| |--- parcel-vue/ --- |---package.json
| | |---package-lock.json
| | |---node_modules/
| | |---src/

情况一、情况二、情况三、情况四可以转化成如下结构:

情况四下为例子 - 如图:

说说目前笔者探索的两个实践:

(1)手工管理package.json

下一步说起来也十分简单:

实际只要在,维护一个package.json即可,实际常常只需要devDependencies,然后实际需要安装时只需要进行一次npm install即可,并且会生成package-lock.json。

即上面说到的这几个位置

1
2
3
4
5
|---webpack/--|---webpack-react/---|---package.json
| |--- webpack-vue/--- |---package.json

|---parcel/---|--- parcel-react/---|---package.json
| |--- parcel-vue/ --- |---package.json

实际只要在devDependencies按正常package.json中的内容维护即可。

(2)利用 preinstall

“对原来npm install流程改动不会太大”,这一点上面对于手工管理方式实际并并不满足。并且手工进行管理十分繁琐,因此我们是否可以在npm install之前把devDependencies内重叠模块的node_modules进行提升呢?

答案是:可以的,主要利用的是npm script中的preinstall,关于npm script以及preinstall可以通过:https://docs.npmjs.com/misc/scripts来了解。

npm的preinstall这个hook实际会在安装软件包之前运行。实际我们就可以通过preinstall去执行一些node.js的代码让我们的devDependencies内重叠模块的node_modules提升。

那具体来看一下步骤:

a.在项目package.json中增加preinstall
1
2
3
4
"scripts": {
"preinstall": "node build/scripts/preinstall.js",
"start": "webpack-dev-server --mode development --hot --progress --colors --port 3000 --open"
}

这里我会在项目的build目录下的scripts执行preinstall.js这个文件内的代码

b.切换至项目父级目录(即约定的技术栈目录)

这里我仅进行了一个最简单的示范,其原理是在项目的package.json里增加devDependenciesGlobal
项:

实际可以看一下我提供的实现具体代码

实际最为关键的为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const execPath = path.resolve('../');
const preListObj = preList.devDependenciesGlobal; // 项目package.json自己维护的devDependencies那些重叠依赖
const currentMd5 = md5(JSON.stringify(preListObj)); // 简单举例生成一个md5,实际应用中可以搭配其他机制
...

fs.writeFileSync(`${execPath}/package.json`, JSON.stringify({"md5": currentMd5,dependencies: preListObj})); // 在上级目录创建一个package.json文件并,写入md5以及devDependenciesGlobal的内容

let script = `cd ${execPath} && npm i`;

// 切换至父级目录并执行npm install

exec(script, function (err, stdout, stderr) {
console.log(stdout);
if (err) {
console.log('error:' + stderr);
} else {
console.log(stdout);
console.log('package init success');
console.log(`The global install in ${execPath}`);
}
});
c.实际所占空间的变化:

(此处就不贴图片了,大家可以clone实例自己试一下)

1
2
3
4
5
6
7
|---webpack/--|---webpack-react/---|---node_modules/ # 共21.1 MB,369项
| |--- webpack-vue/--- |---node_modules/ # 共17.8 MB, 336项
| |---node_modules/ # 共153.3 MB,994项
| |
|---parcel/---|--- parcel-react/---|---node_modules/ # 89.4 MB,540项
| |--- parcel-vue/ --- |---node_modules/ # 71.5 MB,491项
| |---node_modules/ # 共21.1 MB,118项

实际非常简单,即得执行目录的上级目录,此处为了举例我的例子里会在运行preinstall时根据devDependenciesGlobal生成一个md5,以便下次install时比对,若一致不执行preinstall内的流程。当然这并不是一个最佳实践(之后的各种实践方案在下篇中进一步给出)

当然实际这部分存在“无限”的可能性,可以根据自己的需求来完善(比如服务端获取devDependenciesGlobal以及md5)。

这仅仅是一个简单示范,实际可以通过配合一些服务端以及docker进一步提升。

存在问题:

(1) windows兼容性

笔者所在的团队在使用的过程中,有遇到windows的开发环境下的各种问题,比如:

  • win10下自定义的preinstall流程中的install执行十分缓慢,或无法执行
  • babel或抛出一些错误(这里不具体说明)

(2) 使用者无法做到无感知

也是最大的缺点,即使用的时候使用者会有感知,开发项目的时候要求使用一定是主动必须按特定的目录结构来安排自己的workspace。

(3) 本地npm版本,不一致,也可能对依赖造成的影响较大

(4) npm script相关的必须在devDependencies引入 (当然也可以通过一些方式避开,比如dev-server不直接使用npm script启动)

这仅仅是一个开始…待续

利用node模块加载机制,我们其实已经可以很大程度改进我们的依赖了,但如上所述我们还存在这些问题:

(1) windows兼容性

上文有提到过windows下,笔者实际实践发现会出现一些问题,是不是有办法可以统一环境呢?

(2) 无法做到让使用者到无感知

(3) 本地npm版本,对依赖造成的影响较大

(5) 管理公共依赖没有标准化和自动化

因此下篇笔者可能会主要进一步通过其他方案来配合这个解决方案

(1) 本文举例的完善版本 (通过shell远程下载公共依赖的package.json,也是笔者目前团队在使用的)

(2) 通过搭配docker进一步完善方案 (是笔者想进一步增强的)

(3) 搭配私有npm仓库

(4) 如何更标准化的管理公共依赖,使其可以自动化标准化

这里挖个坑下篇会更新这部分内容

结语以及本文不足之处

经过上述操作,我们便可以将前端项目根据框架进行依赖管理以及划分了,其实如此改进仍并非最完美以及优化,不足之处仍非常之多,期待更加优雅的解决方案,使我们前端项目的架构更佳健壮和灵活。也欢迎大家和我探讨和讨论,谢谢各位大佬能耐心看完。

Share

浅聊HTTP缓存 (HTTP Cache)

1.引子

HTTP缓存一直是一个老生常谈的问题,前端在日常发布、部署工作中,常常要面对。

其中面对的问题有可能会是:部署的代码无法生效

这次本人所在团队也遇到了相关问题,这里简述一下:

  • 项目会在静态资源(如:css,js)使用chunkHash来处理,因此能保证修改后与旧代码文件名字不会重复。以避免无法更新改动。

  • 在该项目中部署后,进行代码进行一次location.reload,改动即可以生效。

最后,本人发现是因为该项目部署的服务器上所有静态资源的response headers的设置如下:

response headers

  • cache-control: public, max-age=31536000

但致命的是,项目的入口: index.html也是如此。因此实际是因为所有的.html文件命中(cache hit)了强缓存,导致了用户无法直接呈现更新后代码的改动。

找到了原因,也想到了如下三个解决方案

  • 跳转时增加时间戳例如:

    具体为什么可以这么做在之后分析查看是否存在缓存步骤时会解析

    1
    location.href = 'https://www.google.com/index.html?t=201811141248001'
  • 修改response headers中的cache-control

    举例:

    1
    cache-control: public, max-age=0
  • 使用HTML Meta 标签

    可以在html代码中增加meta标签:

    1
    2
    3
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Expires" content="0" />

    上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析HTML内容本身。

    最好还是不要指定HTML标签,通过可能会出现混乱(到底以那端为主,实际response header的优先级更高)。此外,在HTML5中,这些标签是无效的。只有HTML5规范中列出的HTTP等效值才被允许。

    可参考:W3 HTML spec chapter 5.2.2

2. HTTP缓存基本概念

既然找到问题了,我觉得那我就顺藤摸瓜的总结一下吧。

  • HTTP 缓存:重用已获取的资源能够有效的提升网站与应用的性能。Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间。借助 HTTP 缓存,Web 站点变得更具有响应性。

  • HTTP 缓存分为:强缓存和协商缓存

(1) 简化流程

其中关键步骤是:

  • 判断是否存在缓存
  • 判断缓存是否有效(即强缓存是否命中)
  • 请求服务端,判断服务端资源是否更新(即协商缓存是否命中
  • 返回资源(若服务端返回的资源,本地保存请求,包括请求头信息)

(2) 查看是否存在缓存

浏览器怎么判定是否存在本地缓存,这个步骤在此可以理解为浏览器去查找本地是否存在该响应请求的文件存在,查找是否是否有该对应的请求,不同浏览器缓存文件的地址也不尽相同。

以firefox举例,可以在地址栏输入:about:cache

P.S: chrome://cache 在 chrome66版本后已废弃。

如图上所示,这里我们可以看到浏览器关于网络请求缓存的一些信息。我们以本地磁盘中的为例子。

如图上所示,在此我们可以查看到一些关于缓存在磁盘内的信息,包括实际本地缓存所在的位置等。

如上图,这就是一次响应请求的文件,并且他会记录完整的url包括:query string。

如上两图所见,我们改变了t参数的值,实际我们在url后打时间戳来规避命中缓存,实际就是在此改变了查询URL,让浏览器无法查询到与之前请求相同的本地缓存。

最后可以看到,我们的本地缓存文件内,会包含response-head的信息,之后的缓存策略和流程都需要依赖此处的信息。

总结一下,查看是否存在缓存的过程实际就是查找响应请求文件是否存在

(3) 强缓存

[1] 强缓存概念

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。

实际就是我们整体流程内的,查看是否存在缓存以及,查看缓存是否有效。

[2] 如何实现强缓存

  • 实现强缓存,主要是根据客户端保留的一个服务器端的response header中的两个字段:expirescache-control

  • cache-control优先级比expires

如图:

图中可知两者的区别

  • HTTP响应报文中expires的时间值,是一个绝对值。

  • HTTP响应报文中Cache-Control为max-age=31536000,是相对值。

在无法确定客户端的时间是否与服务端的时间同步的情况下,Cache-Control相比于expires是更好的选择,所以同时存在时,只有Cache-Control生效。

Expires

  • Expires是HTTP/1.0控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires的值时,直接使用缓存结果。

    1
    Expires: Wed, 21 Oct 2015 07:28:00 GMT

Cache-Control

在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,列几个常见的值:

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存)

  • private:所有内容只有客户端可以缓存,Cache-Control的默认取值

  • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定

  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

  • max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效

    1
    Cache-Control:public, max-age=31536000

判断缓存是否过期的流程的流程:

缓存失效时间计算公式如下:

1
expirationTime = responseTime + freshnessLifetime - currentAge

在上面这个公式里,responseTime 表示浏览器接收到此响应的那个时间点。

[3] 如何判断强缓存是否命中

状态码为灰色的请求则代表使用了强制缓存,请求对应的Size值则代表该缓存存放的位置

至于from memory cache 和 from disk cache相关的之后讲解。

(4) 协商缓存

[1] 协商缓存概念

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。

[2] 如何实现协商缓存

  • 协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-SinceEtag / If-None-Match

  • Etag / If-None-Match 优先级比 Last-Modified / If-Modified-Since 高。

Last-modified:

Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间

1
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT

If-Modified-Since:

1
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

Etag:

Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)

1
2
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
ETag: W/"0815"

If-None-Match:

If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有If-None-Match,则会根据If-None-Match的字段值与该资源在服务器的Etag值做对比,一致则返回304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为200

1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

然后我们看一下具体流程:

[4] 如何判断协商缓存是否命中

如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串。

(5) 两者异同

  • 两者的共同点是:如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据;

  • 两者的区别是:强缓存不发请求到服务器,协商缓存会发请求到服务器。

4.相关浏览操操作

其实浏览器的相关操作,会对开发人员理解浏览器HTTP缓存产生一些影响,因此我们详细来分析一下:

在Alloy Team的Web缓存机制系列中有总结:

Web缓存机制系列2 – Web浏览器的缓存机制 - Alloy Team

浏览器相关操作 Expires/Cache-Control Last-Modified / Etag
地址栏回车 有效 有效
页面链接跳转 有效 有效
新开窗口 有效 有效
前进、后退 有效 有效
刷新 无效 有效
强制刷新 无效 无效

这块我想梳理一下,与大家分享以及验证一下:

测试前提:

  • 服务端设置相应的response header,
  • 相应资源都已经加载完毕第一次(如果测试结果相同,测试结果的图片就复用了):

测试的浏览器为:

  • Chrome 70
  • Firefox 63.0.1
  • Opera 56.0

测试影响的文件:

  • index.html (主页面)
  • index.js (js资源)
  • index.css (样式文件)
  • doge.jpeg (图片文件)
  • favicon.ico (图标文件)
  • temp.html(跳转辅助页面,不设置response header且,不在统计范围内)

测试使用的response header的设置为:

  • Cache-Control: max-age=300 // 缓存5分钟
  • ETag: 33a64df551425fcc55e4d42a148795d9f25f89d4 // 服务端固定返回
  • Expires: Fri Nov 16 2018 09:33:01 GMT+0800 (CST) // 缓存5分钟
  • Last-Modified: Wed, 21 Oct 2018 07:28:00 GMT // 服务端固定返回

(1) 页面链接跳转

Chrome 70测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js: 命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未显示(使用抓包工具抓包,未发出请求)

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:命中强缓存(使用抓包工具抓包,未发出请求)

Opera 56.0 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

(2) 新开窗口

Chrome 70测试(由于默认为google页,采用了隐私模式测试)结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:命中强缓存(使用抓包工具抓包,未发出请求)

Opera 56.0(隐私模式) 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

(4) 前进、后退

Chrome 70测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:命中强缓存(使用抓包工具抓包,未发出请求)

Opera 56.0 测试结果:

如图:

结果:

  • index.html:命中强缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

(5) 刷新

刷新这一块是测试的重点(之前正因为)

Chrome 70测试结果:

如图:

结果:

  • index.html:命中协商缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:没有命中缓存,服务端获取资源

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:命中协商缓存
  • index.js:命中协商缓存
  • index.css:命中协商缓存
  • doge.jpeg:命中协商缓存
  • favicon.ico:命中协商缓存

Opera 56.0 测试结果:

如图:

结果:

  • index.html:命中协商缓存
  • index.js:命中强缓存
  • index.css:命中强缓存
  • doge.jpeg:命中强缓存
  • favicon.ico:未发出请求(使用抓包工具抓包,无请求)

(6) 地址栏回车

这个部分还得分成从当前tab回车和从另一url

  • Chrome 70测试结果:

[1] 从另一url跳转

相当于页面链接跳转

[2] 当前url回车

相当于刷新

Firefox 63.0.1 测试结果:

[1] 从另一url跳转

相当于页面链接跳转

[2] 当前url回车

相当于页面链接跳转

Opera 56.0 测试结果:

[1] 从另一url跳转

相当于页面链接跳转

[2] 当前url回车

相当于刷新

(6) 强制刷新

Chrome 70测试结果:

如图:

结果:

  • index.html:从服务器端获取资源
  • index.js:从服务器端获取资源
  • index.css:从服务器端获取资源
  • doge.jpeg:从服务器端获取资源
  • favicon.ico:从服务器端获取资源

Firefox 63.0.1 测试结果:

如图:

结果:

  • index.html:从服务器端获取资源
  • index.js:从服务器端获取资源
  • index.css:从服务器端获取资源
  • doge.jpeg:从服务器端获取资源
  • favicon.ico:从服务器端获取资源

  • Opera 56.0 测试结果:

如图:

结果:

  • index.html:从服务器端获取资源
  • index.js:从服务器端获取资源
  • index.css:从服务器端获取资源
  • doge.jpeg:从服务器端获取资源
  • favicon.ico:从服务器端获取资源

(7)最终总结:

虽然费了那么大力气测试,最终结论只是稍微调整了一下:

浏览器相关操作 Expires/Cache-Control Last-Modified / Etag
页面链接跳转 有效 有效
新开窗口 有效 有效
前进、后退 有效 有效
刷新 chrome opera html无效 ico文件无效,
ff有效
chrome opera ico文件无效,
ff有效
地址栏回车 当前URL回车 - chrome opera同刷新
当前URL回车 - ff同刷新
其他URL回车 - 同页面链接跳转
当前URL回车 - chrome opera同刷新
当前URL回车 - ff同刷新
其他URL回车 - 同页面链接跳转
强制刷新 无效 无效

6. 相关HTTP相关的头字段总结

图片引用自:Web缓存机制系列2 – Web浏览器的缓存机制

7.完整流程图

总结了一个相对完成的流程图:

8. 本文一些不足之处

(1) 分布式系统相关

这部分没有实际实践过,只是摘选了部分文章的观点:

  • 分布式系统里多台服务器间的文件的Last-Modified必须保持一致,以免负载均衡到不同服务器导致对比结果不一致。

  • 分布式系统尽量关闭掉ETag(每台机器生成的ETag都会不一样,淘宝页面中的静态资源response headers中都没有ETag)。

(2) 缓存的不同来源相关

这个部分目前暂时没有找到十分的标准答案或文档,目前我仅将自己梳理过的部分知识记录在案:

其实webkit缓存机制还有一个叫 pageCache 这里暂不讨论:WebKit Page Cache I – The Basics

Chrome使用两个缓存: disk cachememory cache

以下例子都仅针对Chrome

[1] disk cache

从磁盘中获取缓存资源,等待下次访问时不需要重新下载资源,而直接从磁盘中获取。

[2] memory cache

从内存中获取资源,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。

[3]浏览器如何区分使用两者呢?

测试条件与上文其他测试相同:

a. 当前tabs生命周期未结束

b. 当前tabs生命周期未结束

可以得出一个基本“现象”

memory cache的生命周期于tabs的选项卡大致对应。

The lifetime of an in-memory cache is attached to the lifetime of a render process, which roughly corresponds to a tab.

可以参考:developer.chrome - webRequest

c. 有疑问之处

有见过一种论点:

目前Webkit资源分成两类:

  1. 主资源

    主资源: 通过MainResourceLoader加载,如HTML页面,或者下载项等

  2. 派生资源

    派生资源:,通过 SubresourceLoader加载,比如HTML页面中内嵌的图片或者脚本链接

虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

此图所示:

好像并不适用,完全适用css第一次并没有,从 memory cache 加载,

但是经过几次,后退重新跳转后(不定次数):

到此,以本人的能力可能暂时,无法作出一个比较好的解答了,希望之后有大佬可以给到一个解答。

9. 供实践的Demo

Demo仓库地址

参考文献:

Share

js数组方法分类复习

年纪大了,每过一段时间都记不住数组的基本方法,所以经常要自己做一个review(这篇先占位之后好好再复习)

一、数组

(1) 检测数组

  • value instanceof Array

  • Array.isArray

(2) 转换方法

  • toString

  • toLocalString

  • toValue

(3) 栈方法

  • push (改变原数组)
  • pop (改变原数组)

(4) 队列方法

  • unshift (改变原数组)
  • shift (改变原数组)

(5) 排序方法

  • sort (改变原数组)
  • reverse(改变原数组)

(6) 操作方法

  • concat
  • slice
  • join
  • splice (改变原数组)

(7) 位置方法

  • indexOf
  • lastIndexOf

(8) 迭代方法

  • every (有返回值)
  • filter (有返回值)
  • forEach (没有返回值)
  • map (有返回值)
  • some (有返回值)

(9) 归并方法

  • reduce (有返回值)

  • reduceRight (有返回值)

Share

js复习事件的优先级和事件对象

一、dom0 dom2 dom3事件区分

上篇当中有提到html内联事件、属性事件和事件监听回调事件,那他们和dom0 dom2 dom3事件有什么关系呢,这里简述下:

DOM Level”是描述DOM对象,方法和行为的规范集合。更高级别的DOM规范建立在之前的级别上。变化发生在两个方面:

  • dom0事件模型: html事件、属性事件

  • dom1事件模型: 1级DOM标准中并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型。

  • dom2事件模型: 监听器回调事件

  • dom3事件模型: 与绑定事件无关

二、什么是事件优先级

解释:事件执行的顺序

(1) 几个看实例之前需要知道的概念:
  • html事件会被属性事件覆盖
(2) dom 0 只存在事件冒泡
(3) dom2 支持事件冒泡和事件捕获
  • 事件目标阶段,按绑定顺序事件
  • 冒泡阶段事件执行顺序,属性事件会比监听执行早(哪个时间先绑定哪个先执行)

分析实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<body id="body" onclick="htmlEvent1()">
<div id="container" onclick="htmlEvent2()">
<button id="test" onclick="htmlEvent3">test</button>
</div>

</body>
<script>

var body = document.getElementById('body');
var container = document.getElementById('container');
var test = document.getElementById('test');

body.onclick = propsEvent1;
container.onclick = propsEvent2;
test.onclick = propsEvent3;

body.addEventListener('click',listenerEventC1,true);
container.addEventListener('click',listenerEventC2,true);
test.addEventListener('click',listenerEventC3,true);

body.addEventListener('click',listenerEventB1,false);
container.addEventListener('click',listenerEventB2,false);
test.addEventListener('click',listenerEventB3,false);

function htmlEvent1() {
console.log('htmlEvent1');
}

function htmlEvent2() {
console.log('htmlEvent2');
}

function htmlEvent3() {
console.log('htmlEvent3');
}

function propsEvent1() {
console.log('propsEvent1');
}

function propsEvent2() {
console.log('propsEvent2');
}

function propsEvent3() {
console.log('propsEvent3');
}

function listenerEventB1() {
console.log('listenerEventB1');
}

function listenerEventB2() {
console.log('listenerEventB2');
}

function listenerEventB3() {
console.log('listenerEventB3 ');
}

function listenerEventC1() {
console.log('listenerEventC1');
}

function listenerEventC2() {
console.log('listenerEventC2');
}

function listenerEventC3() {
console.log('listenerEventC3');
}

// 捕获阶段执行开始
// listenerEventC1
// listenerEventC2
// 捕获事件执行完毕
// 事件目标,按绑定顺序执行开始
// propsEvent3
// listenerEventC3
// listenerEventB3
// 事件目标,按书写顺序执行结束
// 冒泡阶段开始,容器,根据书写顺序 (实际会是属性事件先执行,再监听起器)
// propsEvent2
// listenerEventB2
// 冒泡阶段结束,容器,根据书写顺序
// 冒泡阶段开始,body,根据书写顺序 (实际会是属性事件先执行,再监听起器)
// propsEvent1
// listenerEventB1
// 冒泡阶段结束,body,根据书写顺序
</script>
  • 原则一: 牢记事件绑定是浏览器绑定
  • 原则二:事件向传播目标对象(捕获阶段),先出发事件监听
  • 事件在目标阶段,按事件解析或事件被定义的事件执行 (简单理解可理解为js书写顺序)
  • 事件上按事件解析或事件被定义的事件执行,如果包含html事件上执行,即使属性被覆盖了还是会先执行html事件(即被覆盖的属性事件)

(4)事件传递和cs会影响事件传播有关(比如把外层div层级改动)

不会,事件绑定与css显示不相关

特例:
假设绝对定位,住了遮挡元素 不会触发,(以弹窗插件为例): 事件绑定只html结构有关

特例中的特例:

但是有一个例子可能会有事件穿透

移动端中的事件穿透:

touchstart touchend touchmove

  • touch没有 300ms延迟

  • onclick 300ms延迟

处理方式:

(1) 延迟消失弹窗

(2) 叠加层 300毫秒消失

二、事件对象相关:

####(1)定义与兼容写法

event:包含事件相关的所有信息

ie浏览器:绑定在window上

兼容性写法

1
var e = e || window.event

为了更好的兼容IE浏览器和非ie浏览器。
在ie浏览器中,window.event是全局变量,在非ie中,就需要自己传入一个参数来获取event啦,所以就有了var e = e||window.event

####(2)事件对象常用属性

参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Event

  • bubbles(只读): 当前事件是否是冒泡事件 true:是,false:不是

  • cancelable: 代表当前事件是否可以取消默认行为:true: 可取消 (true可以passive优化) false:不可取消

  • currentTarget(只读): 当前事件正在处理哪个元素

  • defaultPrevented(只读) : 一个布尔值,表示了是否已经执行过了event.preventDefault()

  • detail: 事件相关的详细信息(数字表示,和事件api对比)

  • eventPhase(只读): 1捕获 2目标 3冒泡

  • target(只读): 当前的目标元素是什么

  • Event.isTrusted(只读): 指明事件是否是由浏览器(当用户点击实例后)或者由脚本(使用事件的创建方法,例如event.initEvent)启动。 (大部分都是浏览器事件,当然可以开发人员可以自定义生成)

  • Event.type(只读) : 类事件的类型(不区分大小写)。

  • Event.view :关联视图,一般是当前window

(3)事件对象常用方法

  • event.preventDefault

取消事件(如果该事件可取消): canable 为 true ,同时可以用passive优化,取消事件(如果该事件可取消)。

return fasle

  • event.stopPropagation

阻止事件进一步传播 (bubbles为true 可以调用, false不可),只阻止会往下层传播

  • event.stopImmediatePropagation

阻止事件进一步传播 (bubbles为true 可以调用, false不可),阻止传播之后函数不执行

阻止事件传播不会阻止事件默认行为,阻止事件默认行为,不会阻止事件传播

Share

js复习鼠标事件笔记

一、鼠标事件

dom3事件模型中与鼠标事件相关的事件共12种鼠标事件

  • 5个点击相关
  • 5个移动相关
  • 1个滚动
  • 1个选择事件

点击相关事件:

  • click

  • dblclick

  • contextmeun (次键点击)

  • mousedown

  • mouseup

移动相关

  • mousemove

  • mouseenter

  • mouseover

  • mouseleave

  • mouseout

滚动

  • wheel

选择

  • select

click 执行顺序

dblclick 执行顺序

二、relatedTarget

参考:https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget

定义:和事件相关的次要节点,如果存在返回节点,不存在返回null

target与relatedTarget总是相反的

与relatedTarget相关的事件,有以下8种:

  • focusin
  • focusout
  • mouseover
  • mouseout
  • mouseenter
  • mouseleave
  • dragenter
  • dragexit

###(1) mouseover与mouseenter(mouseout和mouseleave同理)的区别

[1]mouseover:

  • 特征:从事件节点进入其子节点也会触发事件,会向父节点冒泡
  • target: 移入进入节点
  • relatedTarget: 移入时离开的节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#container {
width: 200px;
height: 200px;
background-color: blue;
}

#child {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>
<body>
<div id="container" onmouseover="handleMouseover(event)">
<div id="child">sddddd</div>
</div>
<script>
function handleMouseover(e) {
console.log(e);
}
</script>
</body>
</html>

[2]mouseenter:

  • 特征:从事件节点进入其子节点也会不会触发事件,不会向父节点冒泡
  • target: 移入进入节点
  • relatedTarget: 移入时离开的节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#container {
width: 200px;
height: 200px;
background-color: blue;
}

#child {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>
<body>
<div id="container" onmouseenter="handleMouseEnter(event)">
<div id="child">sddddd</div>
</div>
<script>
function handleMouseEnter(e) {
console.log(e); // 进入事件节点的子阶段不会触发
}
</script>
</body>
</html>

[3]通过mouseover模拟mouseenter:

(1) 模拟实现核心思路:

只处理从外层移入的事件

(2) 拆分步骤:

mouseOver伴随以下几种情况

  • 外层节点移入事件节点 (relatedTarget: 外层节点)

  • 事件节点往子节点移入

    • 事件节点往子节点移入 (relatedTarget: 事件节点)
    • 事件节点的子节点往其他子节点移入 (relatedTarget: 事件节点子节点)

(3)因此模拟处理的关键:

判断次要节点不是 本身也不是其子节点

(4)是否可以只判断,非自身节点

不能,若只判断判断次要节点不是本身,绝大多数情况可以解决但是一个前提是需要事件节点与子节点间有空间才可以判断,如果没有空间可能是更上一层的节点

(5)是否可以通过阻止子节点冒泡来解决

不能,只能解决子节点之间移动的问题,不能解决子节点往事件节点上的触发

(6)是否可以通过事件流的方式解决

不能,通过addEventListener解决: 无效(会产生新的mouseover,有一个完整的事件流)、

(2)鼠标事件对应的事件处理程序

参考:https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent

[1] 鼠标事件相关属性

可以大致分为三类:

  • 位置相关

    • screenX (相对屏幕位置):他提供了鼠标相对于屏幕坐标系的水平偏移量
    • clientX (相对可是区域位置,浏览器窗口)
    • pageX (相对于html文档) (非标准属性)兼容:pageX = clientX + scrollLeft + clientLeft
    • offesetX: 鼠标在的位置与触发元素相距的距离,以content为参考点(ie属性非标准,但浏览器打多实现,基本通用)
    • movementX : 与mousemove搭配,两个连续的mousemove相隔的距离 (ie不支持,现代浏览器支持)
  • 键盘相关

    • cotrrolKey:
      点鼠标时,有没有按住cotrrol
    • shiftKey:
      点击点鼠标时,有没有按住shift
    • altKey:
      点击点鼠标时,有没有按住alt
    • metaKey: (win键 command键)
    • button:-1(没有按键)鼠标移动 0 (主键) 1 (辅助键) 2 (次键)
    • buttons: 同时按下两个键,数字可能会变,3位2进制值 1 左 2 右 4 中
      举例: 左中:5
[2]位置相关以及键盘相关注意事项

(1)强调 click只是主键点击

(2)其他键可以通过 mousedown mouseup

(3)preventDefalt可以注销contextmenu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.context {
background-color: pink;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div class="context" oncontextmenu="handleOnContextmenu(event)">
21321
</div>
<script>
function handleOnContextmenu(e) {
e.preventDefault(); //阻止默认右键菜单
console.log('弹出自定义菜单');
}
</script>
</body>
</html>

当滚动鼠标滚轮或操作其它类似输入设备时会触发滚轮事件。滚轮事件替换了已被弃用的非标准mousewheel事件。

(1)mousewheel事件

  • 可以冒泡冒泡到document
  • wheeldelta: 是一个整值120的倍数 上滚120倍数 下滚是-120倍数 是滚动调的方向,一般只关心正数还是负数
  • mousewheel,documentmouse
    存在detail wheelDelta 1的倍数 新版正3 -3,和标准相反
  • 标准版本weel事件(html5):大部分浏览器都支持
    deltaX: 变量增量 X 浏览器数值不同
    deltaY:变量增量 Y 浏览器数值不同
    deltaZ: 变量增量 Z 浏览器数值不同
[4]滚动鼠标事件注意点:
  • 要根据实际客户需求调研,客户版本(mdn文档)
  • wheeldelta 不一定是整数倍,线性滚轮鼠标不一定正数,mac触控板也模拟鼠标滚轮,就有可能是3的倍数(可能会触发几十次,并且可能有动画过度,wheelDealte可能还会继续改变)
  • 不要相信wheeldelta的值
  • 极限情况:wheeldelta(通常是抛物线曲线)会出现震动情况(一般是动画过程中旋转设备,切换屏幕)
  • 鼠标滚动事件和页面滚动事件无关 (触摸设备手指滑,pc拖拽滚动条也不会触发滚动事件,mac放大不会触发,mac和安卓 2手指手势会触发)
    只能通过滚轮或触控板触发
  • 滚动到页面顶部和底部后也会继续触发

(3)防抖函数与节流函数

只叙述概念,之后文章具体分析源码与原理

根本原理是通过时间戳控制

[1] 防抖函数

同一个时间处理,当超过设定时间后才触发,

核心实现思路:2个事件发生间隔小于某个时间点,推迟执行

[2] 节流函数

同一个事件处理,设定时间内只触发一次

核心实现思路:2个事件发生间隔大于某个时间点,就执行

Share

js复习事件的基本操作

一、什么是事件

1. 定义

在js中事件通常是指发生某种于Dom元素、document对象、window对象 有关的预定义或自定义的时刻或契机,这些契机通常是预定义的并且程序上依赖于它们,以关联这些契机发生时候的的功能发生代码。

2. 相关基本概念

(1)事件类型

字符串,用来表示事件类型

1
2
3
4
5
6
<div onclick="">来点我</div>
<script>
window.addEventListener('onclick',function () {
console.log('我是点击事件');
});
</script>

(2)事件目标

指事件发生在哪个对象上(dom元素、document对象、window对象)。

(3)事件处理程序

当事件发生时,执行的函数

1
2
3
4
5
6
<div onclick="console.log('来点我')">来点我</div>
<script>
window.addEventListener('onclick',function () {
console.log('我是点击事件');
})
</script>

(4)事件对象

事件发生时,事件本身包含的信息的对象(event对象)

如:位置信息,具体操作,事件对象等

1
2
3
4
5
6
7
8
<body>
<button onclick="getEvent(event)">来点我</button>
</body>
<script>
function getEvent(e) {
console.log(e); // MouseEvent {clientX:27, clientY: 16, toElement: button ...}
}
</script>

(5)事件流

浏览器中事件传播的机制:

[1]事件冒泡:

从具体节点到不具体节点,接受事件

[2]事件捕获:

从不具体节点到具体节点,接受事件

在事件达到节点之前,捕获事件,性能更好,移动设备上touch事件用捕获实现

ps: 规范中定义事件从document开始传播,但实际浏览器是从windows开始捕获(为了性能)

[3]IE事件流 (基于事件冒泡,并且他们不基于W3C规范)
  • EventTarget.attachEvent(type, listener)

    • this指向window
    • 执行顺序相反
    • detachEventEvent删除必须是同一引用
  • EventTarget.detachEventEvent(type, listener)

分两个阶段

(1)事件目标

(2)事件冒泡

[4]dom2级别事件流(现代浏览器):

添加事件处理程序

分三个阶段

(1)事件捕获 (规范不设计涉及事件目标,但是实现里会触发事件对象)

(2)事件目标

(3)事件冒泡 (为了IE增加了事件冒泡)

3.事件触发

(1) html事件

称为html内联属性或html事件

[1] 如何触发

在dom元素上on + 事件名称 = “需要执行的函数” (需要括号,因为是执行函数)

1
<button onclick="getEvent(event)">html事件</button>

例子:

1
2
3
4
5
6
7
8
<body>
<button onclick="getEvent(event)">html事件</button>
</body>
<script>
function getEvent(e) {
console.log(e);
}
</script>
[2] Html事件使用注意事项
  • Html不支持字符 需要转义
  • 可以访问全局作用域下所有函数
  • 有event 事件对象
  • this的指针指向当前是触发事件的dom元素
1
2
3
4
5
6
7
8
9
<body>
<button onclick="htmlEvent(this)">HTML事件</button>
</body>
<script>
function htmlEvent(_this) {
console.log(_this); // 不能直接打印this
console.log(this); // 指向window
}
</script>
  • 可以通过setattribute 动态修改、移除事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<button id="button" onclick="htmlEvent(this)">HTML事件</button>
<button onclick="removeEvent()">移除HTML事件</button>
</body>
<script>
var btn = document.getElementById('button');
function htmlEvent(_this) {
console.log(_this); // 不能直接打印this
console.log(this); // 指向window
btn.setAttribute('onclick',console.log('替换咯...'));
}

function removeEvent() {
btn.setAttribute('onclick',''); // 删除事件
}
</script>
[3] Html事件的缺点
  • 使用会存在时间差 写在body最后 try catch避免
  • html事件作用域,全局作用域访问限定对象会出问题
  • 代码耦合问题(需要修改两个地方)

(2) 属性事件(dom 0事件)

[1] 添加事件

获取dom元素,将on + 事件名称的属性赋予相关函数

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<button id="test">属性事件</button>
</body>
<script>
var test = document.getElementById('test');

test.onclick = getEvent;

function getEvent(e) {
console.log(e);
}
</script>
[2] 删除事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<button id="button">HTML事件</button>
<button id="button2">移除HTML事件</button>
</body>
<script>
var btn = document.getElementById('button');
var btn2 = document.getElementById('button2');
btn.onclick = htmlEvent;
btn2.onclick = removeEvent;

function htmlEvent() {
console.log(this);
}

function removeEvent() {
btn.onclick = null;
}
</script>
[3] 节点复制

事件会被复制,复制出来的事件不是指向同一个引用

[4]属性事件使用注意事项
  • 增加事件 事件处理程序(小写)

    1
    El.onclick
  • 支持Event对象

  • This指向当前dom

    1
    2
    3
    function htmlEvent() {
    console.log(this); // 指向当前dom
    }

解决问题

  • 解决了时间差问题
  • 降低了代码耦合性

缺点

  • 一次只能指定一个事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <body>
    <button id="test">属性事件</button>
    </body>
    <script>
    var test = document.getElementById('test');
    test.onclick = hello;

    test.onclick = world;

    test.onclick = helloWolrd;

    function hello() {
    console.log('hello');
    }
    // world 会覆盖 world
    function world() {
    console.log('world');
    }
    // 必须放一个fp
    function helloWolrd() {
    console.log('hello');
    console.log('world');
    }
    </script>

(3) 事件监听回调 (dom2)

注意事件类型没有on

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<button id="test">属性事件</button>
</body>
<script>
var test = document.getElementById('test');

function getEvent(e) {
console.log(e);
}

test.addEventListener('click',getEvent);
</script>

p.s: 没有dom1没有做事件相关规定

[1]绑定事件与删除事件

1
2
3
4
5
6
7
target.addEventListener(type, listener, options);

target.addEventListener(type, listener ,{capture: Boolean, passive: Boolean, once: Boolean});

target.addEventListener(type, listener, useCapture);

target.addEventListener(type, listener[, useCapture, wantsUntrusted ]); // Gecko/Mozilla only
  • type 表示监听事件类型的字符串。

  • listener

    当所监听的事件类型触发时,会接收到一个事件通知(实现了 Event 接口的对象)对象。listener 必须是一个实现了 EventListener 接口的对象,或者是一个函数

    1
    2
    3
    4
    5
    6
    7
    var btn = document.getElementById('button');

    btn.addEventListener('click',handleEvent);

    function handleEvent() {
    console.log(this); // 指向当前dom
    }
  • options 可选
    一个指定有关 listener 属性的可选参数对象。可用的选项如下:

    • capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。

      效果同useCapture可参考

      1
      2
      3
      4
      5
      6
      7
      8
      9
      var btn = document.getElementById('button');
      var container = document.getElementById('container');
      btn.addEventListener('click',function() {
      console.log('button'); // 后触发
      });

      container.addEventListener('click',function() {
      console.log('container'); // 先触发
      },{ capture: true });
- once:  Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。

1
2
3
4
var btn = document.getElementById('button');
btn.addEventListener('click',function() {
console.log('button'); // 只执行一次
},{once: true});
- passive: Boolean,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。 mozSystemGroup: 只能在 XBL 或者是 Firefox' chrome 使用,这是个 Boolean,表示 listener 被添加到 system group。 关于passive可以参考:[https://zhuanlan.zhihu.com/p/24555031](****) 具体之后文章会具体聊聊这个问题 - true的话浏览器会开2个线程为了提高性能, - 永远不会调用 preventDefault() - 同一个dom对象,只要同一事件一个设置了passivefasle,浏览器都不会做性能优化, - cancelable 为true都可以支持

option支持的安全检测例子:

1
2
3
4
5
6
7
8
9
10
11
var passiveSupported = false;

try {
var options = Object.defineProperty({}, "passive", {
get: function() {
passiveSupported = true;
}
});

window.addEventListener("test", null, options);
} catch(err) {}
  • useCapture 可选
    Boolean,是指在DOM树中,注册了该listener的元素,是否会先于它下方的任何事件目标,接收到该事件。沿着DOM树向上冒泡的事件不会触发被指定为use capture(也就是设为true)的listener。当一个元素嵌套了另一个元素,两个元素都对同一个事件注册了一个处理函数时,所发生的事件冒泡和事件捕获是两种不同的事件传播方式。事件传播模式决定了元素以哪个顺序接收事件。进一步的解释可以查看 事件流 及 JavaScript Event order 文档。 如果没有指定, useCapture 默认为 false 。

例子:父容器元素与子容器元素都为false:

1
2
3
4
5
6
7
8
9
var btn = document.getElementById('button');
var container = document.getElementById('container');
btn.addEventListener('click',function() {
console.log('button'); // 先触发
},false);

container.addEventListener('click',function() {
console.log('container'); // 后触发
}, false);

例子:父容器元素与子容器元素都为true:

1
2
3
4
5
6
7
8
9
var btn = document.getElementById('button');
var container = document.getElementById('container');
btn.addEventListener('click',function() {
console.log('button'); // 后触发
},true);

container.addEventListener('click',function() {
console.log('container'); // 先触发
},true);

例子:父容器元素为true与子容器元素false:

1
2
3
4
5
6
7
8
9
var btn = document.getElementById('button');
var container = document.getElementById('container');
btn.addEventListener('click',function() {
console.log('button'); // 后触发
},false);

container.addEventListener('click',function() {
console.log('container'); // 先触发
},true);

例子:父容器元素为false与子容器元素true:

1
2
3
4
5
6
7
8
9
var btn = document.getElementById('button');
var container = document.getElementById('container');
btn.addEventListener('click',function() {
console.log('button'); // 先触发
},true);

container.addEventListener('click',function() {
console.log('container'); // 后触发
},false);

注意事项:

  • 事件类别没有on 如click keydown
  • this指向window
  • useCapture默认是fasle表示冒泡,true表示捕获
  • wantsUntrusted (only Gecko)
    如果为 true , 则事件处理程序会接收网页自定义的事件。此参数只适用于 Gecko,主要用于附加组件的代码和浏览器本身。请见 Interaction between privileged and non-privileged pages.
    在使用 options 对象中的特定值之前,最好确保用户的浏览器支持它,因为他并没有被所有浏览器所支持。查看更多细节关于option支持的安全检测。

  • 删除事件处理程序

1
target.removeEventListener(type, listener[, useCapture])
  • type:一个字符串,表示需要移除的事件类型,如 “click”。
  • listener: 需要移除的 EventListener 函数(先前使用 addEventListener 方法定义的)
  • useCapture 可选
    指定需要移除的 EventListener 函数是否为事件捕获。如果无此参数,默认值为 false。

注意事项

  • 用EventTarget.addEventListener指定,只能用EventTarget.removeEventListener删除
  • 如果同一个监听事件分别为“事件捕获”和“事件冒泡”注册了一次,一共两次,这两次事件需要分别移除。两者不会互相干扰。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<body>
<div id="container">
<button id="button">事件</button>
<button onclick="removeBubbleEvent()">删除冒泡事件</button>
<button onclick="removeCaptureEvent()">删除捕获事件</button>
</div>

</body>
<script>
var btn = document.getElementById('button');

btn.addEventListener('click',clickEvent,false);
btn.addEventListener('click',clickEvent,true);


function removeCaptureEvent(){
btn.removeEventListener('click',clickEvent,true);
console.log('removeCaptureEvent ')
}

function removeBubbleEvent(){
btn.removeEventListener('click',clickEvent,false);
console.log('removeBubbleEvent ')
}

function clickEvent(){
console.log('clickEvent');
}
</script>
  • 删除的函数,必须与添加时函数同一引用
1
2
3
4
5
6
var div = document.getElementById('div');
var listener = function (event) {
/* do something here */
};
div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<div id="container">
<button id="button">事件</button>
<button onclick="delEvent()">删除事件</button>
</div>

</body>
<script>
var btn = document.getElementById('button');

btn.addEventListener('click',clickEvent,false);


function delEvent(){
btn.removeEventListener('click',clickEvent,false);
console.log('removeEvent')
}

function clickEvent(){
console.log('clickEvent');
}
</script>

(4) 事件目标支持情况

[1] element:

[x] html事件

[x] 属性事件 (dom0 事件)

[x] 事件监听回调 (dom2事件)

[2] document:

[x] html事件

[x] 属性事件 (dom0 事件)

[] 事件监听回调 (dom2事件)

[3] window:

实际body标签通过frameset等来支持html事件

[x] html事件

[x] 属性事件 (dom0 事件)

[x] 事件监听回调 (dom2事件)

Share

前端项目共享node_modules

1. 前言

在npm、yarn等包管理工具的帮助下,前端工程化模块化的进程日益加快,项目的依赖包也日益增大,特别是若项目采用webpack来构建用到许多webpack的插、一些辅助开发如(eslint、Browsersync之类的插件)以及一些单元测试可能需要用到的插件,项目中的node_module就会变的十分庞大。

因此团队开发者每次重新初始化项目进行npm install会十分缓慢,并且若占用大量空间,更进一步来说项目中往往很多前端团队会用到一台构建服务器,对前端项目的代码获取、打包、构建、部署进行管理(例如我自己所在的团队就在使用jenkins对前端项目的打包、构建、部署进行自动化托管)。npm这样的项目级依赖管理的特性就会造成大量的时间以及资源的消耗。

当然笔者也想在文章开始之前表明一个态度,npm本身portable的模块化设计,使每个项目互不干系,也是一种它设计的精华所在,本文仅是针对实际使用中遇到的一些小困扰进行解读,希望提供一个新的思路。

此本篇文章就在这样的背景下诞生了。纯粹是笔者在项目中积累的一些经验,若有不足望指出。

2. 问题的总结

接前言,若不对项目做任何优化以及设置正常一般前端项目的依赖应该是这样的:

如图:

可以看到图中 webpackA 与 webpackB项目在devDependencies层面其实只有less和sass,以及他们两者的webpackloader的差异,但是其项目中的node_modules剩余的依赖都会被重复获取。parcel项目A项目A 与 parcel项目B项目B同理,项目中的devDependencies只有parcel-plugin-vue 1.5.0和babel-preset-react有区别。

p.s:

1. dependencies一般项目都会有较大差异,因此不在本文讨论范围,该图还是列出以便更接近现实状况

2. 使用webpack和parcel技术栈也可能会有一部分devDependencies,但本文暂不考虑两个项目差异极大的node_modules共享

这样的情况,总结如下来会带来如下问题:

  • 项目devDependencies以及dependencies过多造成项目初始化过慢。
  • 各个项目devDependencies大量重复,打包时项目占用空间过大。

这样的问题在项目数不大时并不会明显体现,并且这些重复很大一部分是因为构建是devDependencies中的依赖所导致的。

3. 现有的一些解决方案以及存在的一些问题

那难道没有方法让几个项目的依赖指向同一路径吗?答案当然是有

(1)解决办法

可以通过配置NODE_PATH,具体来说就是:

在对应系统的环境变量配置文件中增加环境变量NODE_PATH,例如在MacOS中

1
vi /ect/profile # 或/etc/bashrc或~/.bashrc此处不赘述配置问价的加载顺序以及原理
1
2
-> export PATH=$PATH: # 将 /usr/bin 追加到 PATH 变量中
-> export NODE_PATH="/usr/lib/node_modules;/usr/local/lib/node_modules" #指定 NODE_PATH 变量

那 NODE_PATH 就是NODE中用来寻找模块所提供的路径注册环境变量。我们可以使用上面的方法指定NODE_PATH环境变量。并且用;分割多个不同的目录。

关于 node 的包加载机制我就不在这里赘述了。NODE_PATH中的路径被遍历是发生在
从项目的根位置递归搜寻 node_modules 目录,直到文件系统根目录的 node_modules,如果还没有查找到指定模块的话,就会去 NODE_PATH中注册的路径中查找。

(2)存在问题

经过调整后,依赖的结构目前变为了:

如图:

看似不错让多个项目指向的依赖指向了全局的node_modules,但这种方式存在很明显的缺点:

a. 不会生成package.json,无法增量安装

在使用 –global 参数的时候 –save 或 –save-dev参数是无效的。
此时 package.json 中的 dependencies, devDependencies 将无法享受到npm增量更新带来的便利,不使用 dependencies, devDependencies 字段对整个依赖库的管理是非常不利的。

b.若项目模块出现版本差异,很难维护以及解决

差异性的解决方法
如果 项目A 使用了,express的3.x版本,项目B 使用了 express的4.x版本,那这种情况该怎么办呢?

如图:

可以将 NODE_PATH 指定的位置中存放 webpack的3.x版本,项目B需要webpack 目录中放置 2.x 版本呢?

可以利用node_module依赖包加载的机制,将webpack的3.x版本放在项目A自己的node_modules中

,这样确实可以解决了模块版本差异性问题,并不能做到根据让项目按技术框架去划分并且因为若项目数量多,依然无法做到统一管理,当然这种灵活使用node_module依赖包加载的机制仍十分有借鉴意义,也启发了我们可以使用一个更灵活的方式去解决问题。

4.进一步增强以及改进的方案

总而言之上述两种方式,在实际使用中都存在一定的问题。

目前笔者的理想方案应该能够达到下列几个目的:

  • 可以将项目构建相似技术栈的项目统一在一起,共享依赖

  • 可以实现对公共模块的维护和管理,并实现增量安装

  • 对原来npm install流程改动不会太大

因此笔者我经过了一系列的实践,采用了这种结构去维护模块,以下是主要思路:

  • 由于node的包加载机制是从项目的根位置递归搜寻 node_modules 目录,因此我们只需要向当前项目的上层的node_modules安装依赖,既可以使项目正常引入。

  • 对于相同构建技术栈的项目,建立一个框架目录,并将相同技术栈项目放在该目录中,在此目录下的node_modules和package.json中管理以及共享依赖包,项目目录如: webpack、gulp

  • 在每个项目框架下维护一个自己的package.json以及一个框架级的node_modules,为了更好的区分哪些是我们框架需要的依赖包,并仍可以在项目中直接操作,故我们在项目级增加一个devDependenciesGlobal字段以从项目层面更好的维护公共框架的依赖

  • 使用nodejs、shell脚本,结合npm script中的preinstall hook npm install的流程进行一些补充

  • 在框架目录中建立带有md5的package.json来维护、管理公共的依赖

  • 检查框架目录下是否存在package.json,若不存在视为需要初始化

  • 若被视为初始化,会生成一个含有devDependenciesGlobal指纹的package.json,并在这个package.json的devDependencies写入,devDependenciesGlobal中所有描述的依赖

  • 若已存在,跳过此步骤对项目自己的devdependencies以及devDependencies进行安装

  • 若md5改变,则进行npm install,此时npm install会对借助npm本身的机制,对包进行分析,进行增量安装或更新

故按这种结构来管理,假设团队项目中有2套技术框架 A: webpack B: gulp

调整下的依赖关系图:

如图:

示例webpack项目的github地址:https://github.com/Roxyhuang/npm-module-share

下面是本人的一些具体实践的核心代码以及,希望能够对大家有所启发:

(1)按技术框架划分项目目录,并增加devDependenciesGlobal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"dependencies": {
"react": "^16.2.0",
"react-dom": "^16.2.0"
},
"devDependencies": {
"babel-cli": "6.26.0",
"babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-plugin-transform-runtime": "6.23.0",
"babel-polyfill": "6.23.0",
"babel-preset-env": "1.6.0",
"babel-preset-react": "6.24.1",
"babel-preset-stage-0": "6.24.1"
},
"devDependenciesGlobal": {
"babel-core": "6.26.0",
"babel-loader": "7.1.2",
"chalk": "2.1.0",
"css-loader": "0.28.4",
"connect-history-api-fallback": "1.3.0",
"express": "4.16.1",
"webpack": "3.6.0",
"style-loader": "0.18.2",
"less": "2.7.3",
"rimraf": "2.6.1",
"html-webpack-plugin": "2.30.1",
"less-loader": "4.0.5",
"webpack-dev-middleware": "1.12.0",
"webpack-hot-middleware": "2.19.1"
}

(2)利用npm script的hook机制,增加preinstall脚本

接下来为了更好和本身npm依赖管理机制结合的更好,可以利用npm script的hook,在script中增加preinstall

1
2
3
"scripts": {
"preinstall": "node build/script/preinstall.js"
},

下面是具体的node.js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const path = require('path');

const fs = require('fs');

const exec = require('child_process').exec;

const preList = require('../../package.json');

const crypto = require('crypto');

const execPath = path.resolve('../');

const preListObj = preList.devDependenciesGlobal;

const preListObjArray = Object.keys(preListObj);

const isFirst = !fs.existsSync(`${execPath}/package.json`);

const currentMd5 = md5(JSON.stringify(preListObj));

let script = `cd ${execPath} && npm i`;

let isUpdate = false;

console.log(`The md5 for this project: ${currentMd5}`);

const md5 = function (str) {
const md5sum = crypto.createHash('md5');
md5sum.update(str);
str = md5sum.digest('hex');
return str;
};

if (isFirst){
console.log('init global modules...');
fs.writeFileSync(`${execPath}/package.json`, JSON.stringify({"md5": currentMd5,dependencies: preListObj}));
isUpdate = true;
} else {
console.log('will check update');
const lastVersion = require(`${execPath}/package.json`).md5;
if (lastVersion !== currentMd5) {
fs.writeFileSync(`${execPath}/package.json`, JSON.stringify({"md5": currentMd5,dependencies: preListObj}));
isUpdate = true;
}
}

if (isUpdate) {
console.log(`will install in ${execPath}`);
console.log('start install public package for neo-antd');
console.log('please wait some minutes.....');

exec(script, function (err, stdout, stderr) {
console.log(stdout);
if (err) {
console.log('error:' + stderr);
} else {
console.log(stdout);
console.log('package init success');
console.log(`The global install in ${execPath}`);
}
});

} else {
console.log('not any update');
console.log('will be run next');
}

(3) 通过node.js生成package.json,并维护md5安装框架级依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"md5": "4c4ee67828b3e09f07b17774cb1633c7",
"dependencies": {
"babel-core": "6.26.0",
"babel-loader": "7.1.2",
"chalk": "2.1.0",
"css-loader": "0.28.4",
"connect-history-api-fallback": "1.3.0",
"express": "4.16.1",
"webpack": "3.6.0",
"style-loader": "0.18.2",
"less": "2.7.3",
"rimraf": "2.6.1",
"html-webpack-plugin": "2.30.1",
"less-loader": "4.0.5",
"webpack-dev-middleware": "1.12.0",
"webpack-hot-middleware": "2.19.1"
}
}

(4) 初始化项目,并在框架目录生成存有md的package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
npm install #

> npm-module-share@1.0.0 preinstall /Users/neo/home/webpack/npm-module-share
> node build/script/preinstall.js

...

The md5 for this project: 4c4ee67828b3e09f07b17774cb1633c7
init global modules...
will install in /Users/neo/home/webpack
start install public package for neo-antd
please wait some minutes....

....


added 704 packages in 7.777s

package init success
The global install in /Users/neo/home/webpack

....


> fsevents@1.1.3 install /Users/neo/home/webpack/npm-module-share/node_modules/fsevents
> node install

[fsevents] Success: "/Users/neo/home/webpack/npm-module-share/node_modules/fsevents/lib/binding/Release/node-v57-darwin-x64/fse.node" already installed
Pass --update-binary to reinstall or --build-from-source to recompile
npm WARN npm-module-share@1.0.0 No description

added 341 packages in 13.46s

(5) 检查md5是否存在变化,判断是否需要进行增量安装

1
2
3
4
5
6
7
8
9
npm install # 若没有任何更新,md5不会有任何变化,也不会再对会直接跳过preinstall的步骤

The md5 for this project: 4c4ee67828b3e09f07b17774cb1633c7
will check update
not any update
will be run next
npm WARN npm-module-share@1.0.0 No description

update in 1.291s

(6) 几点需要注意的地方

a. npm script中直接需要调用的模块,需要放置到该项目的dependencies和devDependencies中

关于这点阮一峰前辈的博客中: http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html

在对npm script的原理中有提到,在此因为篇幅有限不赘述了,简单来说就是npm script当中用到的模块需要存在当前项目的devdependencies或devDependencies中。

b. 经过测试在windows下需要将babel以及babelrc相关的依赖

目前这点笔者暂时还没有找到原因,在mac中并不会报错,但是在windows中会包找不到这些模块。

希望知道的同行能够与我联系告知我具体原因

c. 若有eslint需要增加 .eslintignore
d. 需要尽量保证一套技术框架下的devDependenciesGlobal尽量一致

结语:

经过上述操作,我们便可以将前端项目根据框架进行依赖管理以及划分了,其实如此改进仍并非最完美以及优化,不足之处仍非常之多,如:同一技术框架下,框架依赖包的升级策略并不完善,期待更加优雅的解决方案,使我们前端项目的架构更佳健壮和灵活。

Share

监听浏览器回退

一、前言

近来遇到一个需求需要监听浏览器回退事件,恰好目前项目中运用的路由为hash-router,因此对这方面做了一个总结。因此本文更多其实只能算对mdn相关文档的一个汇总笔记。

二、相关api

1.window.onpopstate

参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onpopstate

每当活动的历史记录项发生变化时, popstate 事件都会被传递给window对象。如果当前活动的历史记录项是被 pushState 创建的,或者是由 replaceState 改变的,那么 popstate 事件的状态属性 state 会包含一个当前历史记录状态对象的拷贝。

(1) 概述:

根据mdn文档上的解释,我还想来画一下重点:

[1] window.onpopstate是popstate事件在window对象上的事件处理程序,每当处于激活状态的历史记录条目发生变化时,popstate事件就会在对应window对象上触发。

[2] 调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法)。

[3] 如果当前处于激活状态的历史记录条目是由history.pushState()方法创建,或者由history.replaceState()方法修改过的, 则popstate事件对象的state属性包含了这个历史记录条目的state对象的一个拷贝。

[4] 当网页加载时,各浏览器对popstate事件是否触发有不同的表现,Chrome 和 Safari会触发popstate事件, 而Firefox不会。

语法:

1
window.onpopstate = funcRef;

例子:

1
2
3
4
5
6
7
8
9
10
window.onpopstate = function(event) {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
//绑定事件处理函数.
history.pushState({page: 1}, "title 1", "?page=1"); //添加并激活一个历史记录条目 http://example.com/example.html?page=1,条目索引为1
history.pushState({page: 2}, "title 2", "?page=2"); //添加并激活一个历史记录条目 http://example.com/example.html?page=2,条目索引为2
history.replaceState({page: 3}, "title 3", "?page=3"); //修改当前激活的历史记录条目 http://ex..?page=2 变为 http://ex..?page=3,条目索引为3
history.back(); // 弹出 "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // 弹出 "location: http://example.com/example.html, state: null
history.go(2); // 弹出 "location: http://example.com/example.html?page=3, state: {"page":3}

2.history.pushState

参考:https://developer.mozilla.org/zh-CN/docs/Web/API/History_API

(1) 概念:添加历史记录中的条目

(2) pushState()使用

pushState() 需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个URL. 让我们来解释下这三个参数详细内容:

1
pushState({ state1: 1, state2: 2 }, title, URL);
  • 状态对象:

    状态对象state是一个JavaScript对象,通过pushState () 创建新的历史记录条目。无论什么时候用户导航到新的状态,popstate事件就会被触发,且该事件的state属性包含该历史记录条目状态对象的副本。

    状态对象可以是能被序列化的任何东西。原因在于Firefox将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有640k的大小限制。如果你给 pushState() 方法传了一个序列化后大于640k的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用 sessionStorage 以及 localStorage.

    1
    这一句可以理解为,pushState()后不会立即跳转,此时会对该历史记录条目状态保存一个副本,无	论什么时候用户导航到新的状态,popstate事件就会被触发。
  • 标题

    Firefox 目前忽略这个参数,但未来可能会用到。传递一个空字符串在这里是安全的,而在将来这是不安全的。二选一的话,你可以为跳转的state传递一个短标题

  • URL

    该参数定义了新的历史URL记录。注意,调用 pushState() 后浏览器并不会立即加载这个URL,但可能会在稍后某些情况下加载这个URL,比如在用户重新打开浏览器时。新URL不必须为绝对路径。如果新URL是相对路径,那么它将被作为相对于当前URL处理。新URL必须与当前URL同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前URL。

(3) 例子

1
2
var stateObj = { foo: "bar" };
history.pushState(stateObj, "page 2", "bar.html");

使用 history.pushState() 可以改变referrer,它在用户发送 XMLHttpRequest 请求时在HTTP头部使用,改变state后创建的 XMLHttpRequest 对象的referrer都会被改变。因为referrer是标识创建 XMLHttpRequest 对象时 this 所代表的window对象中document的URL。

在某种意义上,调用 pushState() 与 设置 window.location = “#foo” 类似,二者都会在当前页面创建并激活新的历史记录。但 pushState() 具有如下几条优点:

  • 新的 URL 可以是与当前URL同源的任意URL 。而设置 window.location 仅当你只修改了哈希值时才保持同一个 document。

  • 如果需要,你可以不必改变URL。而设置 window.location = “#foo”;在当前哈希不是 #foo 的情况下, 仅仅是新建了一个新的历史记录项。

  • 你可以为新的历史记录项关联任意数据。而基于哈希值的方式,则必须将所有相关数据编码到一个短字符串里。

  • 假如 标题 在之后会被浏览器用到,那么这个数据是可以被使用的(哈希则不然)。

注意 pushState() 绝对不会触发 hashchange 事件,即使新的URL与旧的URL仅哈希不同也是如此。

3.history.replaceState

参考:https://developer.mozilla.org/zh-CN/docs/Web/API/History_API

(1) 概念:修改历史记录中的条目

history.replaceState() 的使用与 history.pushState() 非常相似,区别在于 replaceState() 是修改了当前的历史记录项而不是新建一个。 注意这并不会阻止其在全局浏览器历史记录中创建一个新的历史记录项。

replaceState() 的使用场景在于为了响应用户操作,你想要更新状态对象state或者当前历史记录的URL。

(2) replaceState()使用

1
2
var stateObj = { foo: "bar" };
history.replaceState(stateObj, "page 3", "bar2.html");

导致地址栏显示http://mozilla.org/bar2.html,,但是浏览器并不会去加载bar2.html 甚至都不需要检查 bar2.html 是否存在。

假设现在用户重新导向到了 http://www.microsoft.com,然后点击了回退按钮。这里,地址栏会显示http://mozilla.org/bar2.html。假如用户再次点击回退按钮,地址栏会显示http://mozilla.org/foo.html,完全跳过了bar.html。

4.HashChangeEvent

当URL中的片段标识符发生改变时触发(URL中紧跟’#’号的部分,包括‘#’号)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(function(window) {

// exit if the browser implements that event
if ( "onhashchange" in window.document.body ) { return; }

var location = window.location,
oldURL = location.href,
oldHash = location.hash;

// check the location hash on a 100ms interval
setInterval(function() {
var newURL = location.href,
newHash = location.hash;

// if the hash has changed and a handler has been bound...
if ( newHash != oldHash && typeof window.onhashchange === "function" ) {
// execute the handler
window.onhashchange({
type: "hashchange",
oldURL: oldURL,
newURL: newURL
});

oldURL = newURL;
oldHash = newHash;
}
}, 100);

})(window);

5.history.length

History.length是一个只读属性,返回当前session中的history个数,包含当前页面在内。举个例子,对于新开一个tab加载的页面当前属性返回值1。

1
2
3
length = history.length; // 返回当前session中的history个数

var result = window.history.length; // 返回当前session中的history个数

mdn文档中,对这块的翻译基本以上,实际在使用中有一点是非常需要注意的,就是history.length并不是整个当前会话的历史条目的当前状态(即currentLength),而实际是当前会话历史条目的存在记录的整体长度(即maxlength)

三、常用遇到的情况

1. 监听回退

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var detectBack = {

initialize: function() {
//监听hashchange事件
window.addEventListener('hashchange', function() {

//为当前导航页附加一个tag
this.history.replaceState('hasHash', '', '');

}, false);

window.addEventListener('popstate', function(e) {

if (e.state) {
//侦测是用户触发的后退操作, dosomething
//这里刷新当前url
this.location.reload();
}
}, false);
}
}

detectBack.initialize();

2. 阻止回退

1
2
3
4
5
6
7
8
9
10
function pushHash() {
window.history.pushState('home',null,'#/home'); // 跳转后压入一次历史,并表示home状态
window.history.pushState(null,null,'#/home'); // 跳转后压入一次历史,以便回退拦截
}

window.addEventListener('popstate',function (e) {
if(e.state === 'home') {
拦截回退
}
}
Share

白话聊聊position sticky

一、前言

项目中有一个同事遇到了一个吸顶效果的需求,于是帮助他完成的同时顺便分析下position:sticky。

二、position:sticky是什么

粘性定位元素(stickily positioned element):粘性定位可以被认为是相对定位和固定定位的混合。元素在跨越特定阈值前为相对定位,之后为固定定位。

最白话来说,其实这句话想表达的是: 粘性定位(position:sticky) = 相对定位(position:relative) + 固定定位 (position:fixed)

三、position:sticky作用以及兼容

1. 作用:

其作用为了在iOS下实现吸顶效果,原先吸顶效果的基本的开发思路如下:

利用scroll事件进行监听scrollTop的值,当scrollTop达到一定的值得时候设置吸顶元素的position : fixed;属性。

但是问题是:安卓支持scroll事件和fixed属性,但是ios8.0的scroll事件不是连续触发的,只会在scroll事件结束后触发一次scroll事件,同时ios下fixed属性的支持一直是个问题。

2. 兼容性探究:

(1) MDN

(2) caniuse

####( 3) 因为mdn以及caniuse给到的兼容性并不一致,同时如上面所述主要在iOS使用,因此实际使用仍需要对iOS机型进行实际测试,并考虑polyfill方案

之后在实际找到设备测试后,会持续更新

(4) polyfill方案:

https://github.com/wilddeer/stickyfill

三、position:sticky触发条件

1、具有sticky属性的元素,其父级高度必须大于sticky元素的高度。

2、sticky元素的底部,不能和父级底部重叠。(这条不好表述,文后详细说明)

3、sticky元素的父级不能含有overflow:hidden 和 overflow:auto 属性 (不包括祖先,仅直接包含的父容器)

4、必须具有top,或 bottom 属性。

四、移动端吸顶效果实战

1.position:sticky

使用position:sticky 一定要记得处理浏览器兼容,并且指定top或bottom

1
2
3
4
5
.sticky {
position: -webkit-sticky;
position: sticky;
top: 0;
}

2.实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!--适配移动端js-->
<script>
(function (doc, win) {
var docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
recalc = function () {
var clientWidth = docEl.clientWidth;
if (!clientWidth) return;
docEl.style.fontSize = 75 * (clientWidth / 750) + 'px';
};
if (doc.addEventListener) {
win.addEventListener(resizeEvt, recalc, false);
doc.addEventListener('DOMContentLoaded', recalc, false);
}
})(document, window);
</script>
<title>Document</title>
</head>
<style>
* {
margin: 0;
padding: 0;
}
.container {
position: fixed;
overflow: auto;
width: 100%;
height: 100%;
}
.banner {
height: 2rem;
background-color: #009EE0;
}
.sticky {
position: -webkit-sticky;
position: sticky;
top: 0;
}
.fixed {
position: fixed;
width: 100%;
top: 0;
left: 0;
background-color: white;
}
.relative {
background-color: white;
}
.sticky-title {
height: 1.4rem;
background-color: pink;
}
.sticky-content {
height: 20.3rem;
overflow: auto;
}
</style>
<body>
<div class="container" id="container">
<div class="banner">
Banner部分
</div>
<div id="sticky">
<div class="sticky-title">吸顶结构标题。。。</div>
<div class="sticky-content">
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
<div>内容块</div>
</div>
</div>
</div>
</body>
<script>
// 安卓吸顶方案
var scrollWrapper = document.getElementById('container'); // 假设windows 为滚动容器

var stickyDom = document.getElementById('sticky');

const u = navigator.userAgent;
const isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;
const isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);

scrollWrapper.addEventListener('scroll',function() {
if (isAndroid) {
var bannerHeight = document.getElementsByClassName('banner')[0].clientHeight;
if (this.scrollTop > 72) {
stickyDom.className = 'fixed';
}

} else if(isiOS && (CSS.supports("position", "sticky") || CSS.supports("position", "-webkit-sticky"))){
stickyDom.className = 'stickay';
}
});
</script>
</html>
Share