注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

字节面试被虐后,是时候搞懂 DNS 了

前几天面了字节 👦🏻:“浏览器从输入URL到显示页面发生了什么?” 👧🏻:%^&@#^&(这我怎么可能没有准备?从网络到渲染说了一通后) 👦🏻:“你刚刚提到了 DNS,那说说 DNS 的查询过程吧” 👧🏻:“DNS 查询是一个递归 + 迭代的...
继续阅读 »

前几天面了字节



👦🏻:“浏览器从输入URL到显示页面发生了什么?”


👧🏻:%^&@#^&(这我怎么可能没有准备?从网络到渲染说了一通后)


👦🏻:“你刚刚提到了 DNS,那说说 DNS 的查询过程吧”


👧🏻:“DNS 查询是一个递归 + 迭代的过程...”


👦🏻:“那具体的递归和迭代过程是怎样的呢?”


👧🏻:“...”



当时我脑子里有个大概的过程,但是细节就记不起来了,所以今天就来梳理一下 DNS 相关的内容,如有不妥之处,还望大家指出。


什么是 DNS


DNS 即域名系统,全称是 Domain Name System。当我们在浏览器输入一个 URL 地址时,浏览器要向这个 URL 的主机名对应的服务器发送请求,就得知道服务器的 IP,对于浏览器来说,DNS 的作用就是将主机名转换成 IP 地址。下面是摘自《计算机网络:自顶向下方法》的概念:



DNS 是:



  1. 一个由分层的 DNS 服务器实现的分布式数据库

  2. 一个使得主机能够查询分布式数据库的应用层协议



也就是,DNS 是一个应用层协议,我们发送一个请求,其中包含我们要查询的主机名,它就会给我们返回这个主机名对应的 IP;


其次,DNS 是一个分布式数据库,整个 DNS 系统由分散在世界各地的很多台 DNS 服务器组成,每台 DNS 服务器上都保存了一些数据,这些数据可以让我们最终查到主机名对应的 IP。


所以 DNS 的查询过程,说白了,就是去向这些 DNS 服务器询问,你知道这个主机名的 IP 是多少吗,不知道?那你知道去哪台 DNS 服务器上可以查到吗?直到查到我想要的 IP 为止。


分布式、层次数据库


什么是分布式?

这个世界上没有一台 DNS 服务器拥有因特网上所有主机的映射,每台 DNS 只负责部分映射。


什么是层次?

DNS 服务器有 3 种类型:根 DNS 服务器、顶级域(Top-Level Domain, TLD)DNS 服务器和权威 DNS 服务器。它们的层次结构如下图所示:



DNS 的层次结构.jpeg



图片来源:《计算机网络:自顶向下方法》



  • 根 DNS 服务器


首先我们要明确根域名是什么,比如 http://www.baidu.com,有些同学可能会误以为 com 就是根域名,其实 com 是顶级域名,http://www.baidu.com 的完整写法是 http://www.baidu.com.,最后的这个 . 就是根域名。


根 DNS 服务器的作用是什么呢?就是管理它的下一级,也就是顶级域 DNS 服务器。通过询问根 DNS 服务器,我们可以知道一个主机名对应的顶级域 DNS 服务器的 IP 是多少,从而继续向顶级域 DNS 服务器发起查询请求。



  • 顶级域 DNS 服务器


除了前面提到的 com 是顶级域名,常见的顶级域名还有 cnorgedu 等。顶级域 DNS 服务器,也就是 TLD,提供了它的下一级,也就是权威 DNS 服务器的 IP 地址。



  • 权威 DNS 服务器


权威 DNS 服务器可以返回主机 - IP 的最终映射。


关于这几个层次的服务器之间是怎么交互的,接下来我们会讲到 DNS 具体的查询过程,结合查询过程,大家就不难理解它们之间的关系了。


本地 DNS 服务器


之前对 DNS 有过了解的同学可能会发现,上一节的 DNS 层次结构,为什么没有提到本地 DNS 服务器?因为严格来说,本地 DNS 服务器并不属于 DNS 的层次结构,但它对 DNS 层次结构是至关重要的。那什么是本地 DNS 服务器呢?


每个 ISP 都有一台本地 DNS 服务器,比如一个居民区的 ISP、一个大学的 ISP、一个机构的 ISP,都有一台或多台本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,本地 DNS 服务器起着代理的作用,并负责将该请求转发到 DNS 服务器层次结构中。


接下来就让我们通过一个简单的例子,看看 DNS 的查询过程是怎样的,看看客户端、本地 DNS 服务器、DNS 服务器层次结构之间是如何交互的。


递归查询、迭代查询


如下图,假设主机 m.n.com 想要获取主机 a.b.com 的 IP 地址,会经过以下几个步骤:



DNS.png





  1. 首先,主机 m.n.com 向它的本地 DNS 服务器发送一个 DNS 查询报文,其中包含期待被转换的主机名 a.b.com




  2. 本地 DNS 服务器将该报文转发到根 DNS 服务器;




  3. 该根 DNS 服务器注意到 com 前缀,便向本地 DNS 服务器返回 com 对应的顶级域 DNS 服务器(TLD)的 IP 地址列表。


    意思就是,我不知道 a.b.com 的 IP,不过这些 TLD 服务器可能知道,你去问他们吧;




  4. 本地 DNS 服务器则向其中一台 TLD 服务器发送查询报文;




  5. 该 TLD 服务器注意到 b.com 前缀,便向本地 DNS 服务器返回权威 DNS 服务器的 IP 地址。


    意思就是,我不知道 a.b.com 的 IP,不过这些权威服务器可能知道,你去问他们吧;




  6. 本地 DNS 服务器又向其中一台权威服务器发送查询报文;




  7. 终于,该权威服务器返回了 a.b.com 的 IP 地址;




  8. 本地 DNS 服务器将 a.b.com 跟 IP 地址的映射返回给主机 m.n.comm.n.com 就可以用该 IP 向 a.b.com 发送请求啦。





bqb4.jpeg



“你说了这么多,递归呢?迭代呢?”


这位同学不要捉急,其实递归和迭代已经包含在上述过程里了。


主机 m.n.com 向本地 DNS 服务器 dns.n.com 发出的查询就是递归查询,这个查询是主机 m.n.com 以自己的名义向本地 DNS 服务器请求想要的 IP 映射,并且本地 DNS 服务器直接返回映射结果给到主机。


而后继的三个查询是迭代查询,包括本地 DNS 服务器向根 DNS 服务器发送查询请求、本地 DNS 服务器向 TLD 服务器发送查询请求、本地 DNS 服务器向权威 DNS 服务器发送查询请求,所有的请求都是由本地 DNS 服务器发出,所有的响应都是直接返回给本地 DNS 服务器


那么问题来了,所有的 DNS 查询都必须遵循这种递归 + 迭代的模式吗?


当然不是。


从理论上讲,任何 DNS 查询既可以是递归的,也可以是迭代的。下图的所有查询就都是递归的,不包含迭代。



DNS2.png



看到这里,大家可能会有个疑问,TLD 一定知道权威 DNS 服务器的 IP 地址吗?


emmm...



bqb7.png



还真不一定,有时 TLD 只是知道中间的某个 DNS 服务器,再由这个中间 DNS 服务器去找到权威 DNS 服务器。这种时候,整个查询过程就需要更多的 DNS 报文。


DNS 缓存


为了让我们更快的拿到想要的 IP,DNS 广泛使用了缓存技术。DNS 缓存的原理非常简单,在一个 DNS 查询的过程中,当某一台 DNS 服务器接收到一个 DNS 应答(例如,包含某主机名到 IP 地址的映射)时,它就能够将映射缓存到本地,下次查询就可以直接用缓存里的内容。当然,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,一旦过了生存时间,这条记录就应该从缓存移出。


事实上,有了缓存,大多数 DNS 查询都绕过了根 DNS 服务器,需要向根 DNS 服务器发起查询的请求很少。


面试感想


这次面试收获还蛮大的,有些东西以为自己懂了,以为自己能说清楚,但到了真的要说的时候,又没有办法完整地梳理出来,描述起来磕磕绊绊,在面试中会很减分。


所以不要偷懒,不要抱有侥幸心理,踏实学。共勉。



作者:我是陆小北
链接:https://juejin.cn/post/6990344840181940261

收起阅读 »

H5页面中调用微信和支付宝支付

最近在工作中,有个H5页面需要实现微信支付和支付宝支付的功能,现在已经完成,抽个时间写出来,分享给有需要的人。 第一步:先判断当前环境 判断用户所属环境,根据环境不同,执行不同的支付程序。 if (/MicroMessenger/.test(window.na...
继续阅读 »

最近在工作中,有个H5页面需要实现微信支付和支付宝支付的功能,现在已经完成,抽个时间写出来,分享给有需要的人。


第一步:先判断当前环境


判断用户所属环境,根据环境不同,执行不同的支付程序。


if (/MicroMessenger/.test(window.navigator.userAgent)) {
// alert('微信');
} else if (/AlipayClient/.test(window.navigator.userAgent)) {
//alert('支付宝');
} else {
//alert('其他浏览器');
}

第二步:如果是微信环境,需要先进行网页授权


网页授权的详细介绍可以查看微信相关文档。这里不做介绍。


第三步:


1、微信支付


微信支付有两种方法:
1:调用微信浏览器提供的内置接口WeixinJSBridge
2:引入微信jssdk,使用wx.chooseWXPay方法,需要先通过config接口注入权限验证配置。
我这里使用的是第一种,在从后台拿到签名、时间戳这些数据后,直接调用微信浏览器提供的内置接口WeixinJSBridge即可完成支付功能。


getRequestPayment(data) {
function onBridgeReady() {
WeixinJSBridge.invoke(
"getBrandWCPayRequest", {
"appId": data.appId, //公众号ID,由商户传入
"timeStamp": data.timeStamp, //时间戳,自1970年以来的秒数
"nonceStr": data.nonceStr, //随机串
"package": data.package,
"signType": data.signType, //微信签名方式:
"paySign": data.paySign //微信签名
},
function(res) {
alert(JSON.stringify(res));
// get_brand_wcpay_request
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
}
);
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener(
"WeixinJSBridgeReady",
onBridgeReady,
false
);
} else if (document.attachEvent) {
document.attachEvent("WeixinJSBridgeReady", onBridgeReady);
document.attachEvent("onWeixinJSBridgeReady", onBridgeReady);
}
} else {
onBridgeReady();
}
},

2、支付宝支付


支付宝支付相对于微信来说,前端这块工作更简单 ,后台会返回给前端一个form表单,我们要做的就是把这个表单进行提交即可。相关代码如下:


this.$api.alipayPay(data).then((res) => {
// console.log('支付宝参数', res.data)
if (res.code == 200) {
var resData =res.data
const div = document.createElement('div')
div.id = 'alipay'
div.innerHTML = resData
document.body.appendChild(div)
document.querySelector('#alipay').children[0].submit() // 执行后会唤起支付宝
}

}).catch((err) => {
})

作者:故友
链接:https://juejin.cn/post/7034033584684204068

收起阅读 »

现代配置指南——YAML 比 JSON 高级在哪?

一直以来,前端工程中的配置大多都是 .js 文件或者 .json 文件,最常见的比如:package.jsonbabel.config.jswebpack.config.js这些配置对前端非常友好,因为都是我们熟悉的 JS 对象结构。一般静态化的配置会选择 j...
继续阅读 »

一直以来,前端工程中的配置大多都是 .js 文件或者 .json 文件,最常见的比如:

  • package.json

  • babel.config.js

  • webpack.config.js

这些配置对前端非常友好,因为都是我们熟悉的 JS 对象结构。一般静态化的配置会选择 json 文件,而动态化的配置,涉及到引入其他模块,因此会选择 js 文件。

还有现在许多新工具同时支持多种配置,比如 Eslint,两种格式的配置任你选择:

  • .eslintrc.json

  • .eslintrc.js

后来不知道什么时候,突然出现了一种以 .yaml.yml 为后缀的配置文件。一开始以为是某个程序的专有配置,后来发现这个后缀的文件出现的频率越来越高,甚至 Eslint 也支持了第三种格式的配置 .eslintrc.yml

既然遇到了,那就探索它!

下面我们从 YAML 的出现背景使用场景具体用法高级操作四个方面,看一下这个流行的现代化配置的神秘之处。

出现背景

一个新工具的出现避免不了有两个原因:

  1. 旧工具在某些场景表现吃力,需要更优的替代方案

  2. 旧工具也没什么不好,只是新工具出现,比较而言显得它不太好

YAML 这种新工具就属于后者。其实在 yaml 出现之前 js+json 用的也不错,也没什么特别难以处理的问题;但是 yaml 出现以后,开始觉得它好乱呀什么东西,后来了解它后,越用越喜欢,一个字就是优雅。

很多文章说选择 yaml 是因为 json 的各种问题,json 不适合做配置文件,这我觉得有些言过其实了。我更愿意将 yaml 看做是 json 的升级,因为 yaml 在格式简化和体验上表现确实不错,这个得承认。

下面我们对比 YAML 和 JSON,从两方面分析:

精简了什么?

JSON 比较繁琐的地方是它严格的格式要求。比如这个对象:

{
 name: 'ruims'
}

在 JSON 中以下写法通通都是错的:

// key 没引号不行
{
 name: 'ruims'
}
// key 不是 "" 号不行
{
 'name': 'ruims'
}
// value 不是 "" 号不行
{
 "name": 'ruims'
}

字符串的值必须 k->v 都是 "" 才行:

// 只能这样
{
 "name": "ruims"
}

虽然是统一格式,但是使用上确实有不便利的地方。比如我在浏览器上测出了接口错误。然后把参数拷贝到 Postman 里调试,这时就我要手动给每个属性和值加 "" 号,非常繁琐。

YAML 则是另辟蹊径,直接把字符串符号干掉了。上面对象的同等 yaml 配置如下:

name: ruims

没错,就这么简单!

除了 "" 号,yaml 觉得 {}[] 这种符号也是多余的,不如一起干掉。

于是呢,以这个对象数组为例:

{
 "names": [{ "name": "ruims" }, { "name": "ruidoc" }]
}

转换成 yaml 是这样的:

names:
- name: ruims
- name: ruidoc

对比一下这个精简程度,有什么理由不爱它?

增加了什么?

说起增加的部分,最值得一提的,是 YAML 支持了 注释

用 JSON 写配置是不能有注释的,这就意味着我们的配置不会有备注,配置多了会非常凌乱,这是最不人性化的地方。

现在 yaml 支持了备注,以后配置可以是这样的:

# 应用名称
name: my_app
# 应用端口
port: 8080

把这种配置丢给新同事,还怕他看不懂配了啥吗?

除注释外,还支持配置复用的相关功能,这个后面说。

使用场景

我接触的第一个 yaml 配置是 Flutter 项目的包管理文件 pubspec.yaml,这个文件的作用和前端项目中的 package.json 一样,用于存放一些全局配置和应用依赖的包和版本。

看一下它的基本结构:

name: flutter_demo
description: A new Flutter project.

publish_to: 'none'
version: 1.0.0

dependencies:
cupertino_icons: ^1.0.2

dev_dependencies:
flutter_lints: ^1.0.0

你看这个结构和 package.json 是不是基本一致?dependencies 下列出应用依赖和版本,dev_dependencies 下的则是开发依赖。

后来在做 CI/CD 自动化部署的时候,我们用到了 GitHub Action。它需要多个 yaml 文件来定义不同的工作流,这个配置可比 flutter 复杂的多。

其实不光 GitHub Action,其他流行的类似的构建工具如 GitLab CI/CDcircleci,全部都是齐刷刷的 yaml 配置,因此如果你的项目要做 CI/CD 持续集成,不懂 yaml 语法肯定是不行的。

还有,接触过 Docker 的同学肯定知道 Docker Compose,它是 Docker 官方的单机编排工具,其配置文件 docker-compose.yml 也是妥妥的 yaml 格式。现在 Docker 正是如日中天的时候,使用 Docker 必然免不了编排,因此 yaml 语法早晚也要攻克。

上面说的这 3 个案例,几乎都是现代最新最流行的框架/工具。从它们身上可以看出来,yaml 必然是下一代配置文件的标准,并且是前端-后端-运维的通用标准。

说了这么多,你跃跃欲试了吗?下面我们详细介绍 yaml 语法。

YAML 语法

介绍 yaml 语法会对比 json 解释,以便我们快速理解。

先看一下 yaml 的几个特点:

  • 大小写敏感

  • 使用缩进表示层级关系

  • 缩进空格数不强制,但相同层级要对齐

  • # 表示注释

相比于 JSON 来说,最大的区别是用 缩进 来表示层级,这个和 Python 非常接近。还有强化的一点是支持了注释,JSON 默认是不支持的(虽然 TS 支持),这也对配置文件非常重要。

YAML 支持以下几种数据结构:

  • 对象:json 中的对象

  • 数组:json 中的数组

  • 纯量:json 中的简单类型(字符串,数值,布尔等)

对象

先看对象,上一个 json 例子:

{
 "id": 1,
 "name": "杨成功",
 "isman": true
}

转换成 yaml:

id: 1
name: 杨成功
isman: true

对象是最核心的结构,key 值的表示方法是 [key]:,注意这里冒号后面有个空格,一定不能少。value 的值就是一个纯量,且默认不需要引号。

数组

数组和对象的结构差不多,区别是在 key 前用一个 - 符号标识这个是数组项。注意这里也有一个空格,同样也不能少。

- hello
- world

转换成 JSON 格式如下:

["hello", "world"]

了解了基本的对象和数组,我们再来看一个复杂的结构。

众所周知,在实际项目配置中很少有简单的对象或数组,大多都是对象和数组相互嵌套而成。在 js 中我们称之为对象数组,而在 yaml 中我们叫 复合结构

比如这样一个稍复杂的 JSON:

{
 "name": "杨成功",
 "isman": true,
 "age": 25,
 "tag": ["阳光", "帅气"],
 "address": [
  { "c": "北京", "a": "海淀区" },
  { "c": "天津", "a": "滨海新区" }
]
}

转换成复合结构的 YAML:

name: 杨成功
isman: true
age: 25
tag:
- 阳光
- 帅气
address:
- c: 北京
  a: 海淀区
- c: 天津
  a: 滨海新区

若你想尝试更复杂结构的转换,可以在 这个 网页中在线实践。

纯量

纯量比较简单,对应的就是 js 的基本数据类型,支持如下:

  • 字符串

  • 布尔

  • 数值

  • null

  • 时间

比较特殊的两个,null 用 ~ 符号表示,时间大多用 2021-12-21 这种格式表示,如:

who: ~
date: 2019-09-10

转换成 JS 后:

{
 who: null,
 date: new Date('2019-09-10')
}

高级操作

在 yaml 实战过程中,遇到过一些特殊场景,可能需要一些特殊的处理。

字符串过长

在 shell 中我们常见到一些参数很多,然后特别长的命令,如果命令都写在一行的话可读性会非常差。

假设下面的是一条长命令:

$ docker run --name my-nginx -d nginx

在 linux 中可以这样处理:

$ docker run \
--name my-nginx \
-d nginx

就是在每行后加 \ 符号标识换行。然而在 YAML 中更简单,不需要加任何符号,直接换行即可:

cmd: docker run
--name my-nginx
-d nginx

YAML 默认会把换行符转换成空格,因此转换后 JSON 如下,正是我们需要的:

{ "cmd": "docker run --name my-nginx -d nginx" }

然而有时候,我们的需求是保留换行符,并不是把它转换成空格,又该怎么办呢?

这个也简单,只需要在首行加一个 | 符号:

cmd: |
docker run
--name my-nginx
-d nginx

转换成 JSON 变成了这样:

{ "cmd": "docker run\n--name my-nginx\n-d nginx" }

获取配置

获取配置是指,在 YAML 文件中定义的某个配置,如何在代码(JS)里获取?

比如前端在 package.json 里有一个 version 的配置项表示应用版本,我们要在代码中获取版本,可以这么写:

import pack from './package.json'
console.log(pack.version)

JSON 是可以直接导入的,YAML 可就不行了,那怎么办呢?我们分环境解析:

在浏览器中

浏览器中代码用 webapck 打包,因此加一个 loader 即可:

$ yarn add -D yaml-loader

然后配置 loader:

// webpack.config.js
module.exports = {
 module: {
   rules: [
    {
       test: /\.ya?ml$/,
       type: 'json', // Required by Webpack v4
       use: 'yaml-loader'
    }
  ]
}
}

在组件中使用:

import pack from './package.yaml'
console.log(pack.version)

在 Node.js 中

Node.js 环境下没有 Webpack,因此读取 yaml 配置的方法也不一样。

首先安装一个 js-yaml 模块:

$ yarn add js-yaml

然后通过模块提供的方法获取:

const yaml = require('js-yaml')
const fs = require('fs')

const doc = yaml.load(fs.readFileSync('./package.yaml', 'utf8'))
console.log(doc.version)

配置项复用

配置项复用的意思是,对于定义过的配置,在后面的配置直接引用,而不是再写一遍,从而达到复用的目的。

YAML 中将定义的复用项称为锚点,用& 标识;引用锚点则用 * 标识。

name: &name my_config
env: &env
version: 1.0

compose:
key1: *name
key2: *env

对应的 JSON 如下:

{
 "name": "my_config",
 "env": { "version": 1 },
 "compose": { "key1": "my_config", "key2": { "version": 1 } }
}

但是锚点有个弊端,就是不能作为 变量 在字符串中使用。比如:

name: &name my_config
compose:
key1: *name
key2: my name is *name

此时 key2 的值就是普通字符串 my name is *name,引用变得无效了。

其实在实际开发中,字符串中使用变量还是很常见的。比如在复杂的命令中多次使用某个路径,这个时候这个路径就应该是一个变量,在多个命令中复用。

GitHub Action 中有这样的支持,定义一个环境变量,然后在其他的地方复用:

env:
NAME: test
describe: This app is called ${NAME}

这种实现方式与 webpack 中使用环境变量类似,在构建的时候将变量替换成对应的字符串。

作者:杨成功
来源:https://segmentfault.com/a/1190000041108051

收起阅读 »

LOOK 直播活动地图生成器方案

在最近的活动开发中,笔者就刚好碰到了这个问题。这次活动开发需要完成一款大富翁游戏,而作为一款大富翁游戏,地图自然是必不可少的。在整个地图中,有很多的不同种类的方格,如果一个个手动去调整位置,工作量是很大的。那么有没有一种方案能够帮助我们快速确定方格的位置和种类...
继续阅读 »

对于前端而言,与视觉稿打交道是必不可少的,因为我们需要对照着视觉稿来确定元素的位置、大小等信息。如果是比较简单的页面,手动调整每个元素所带来的工作量尚且可以接受;然而当视觉稿中素材数量较大时,手动调整每个元素便不再是个可以接受的策略了。

在最近的活动开发中,笔者就刚好碰到了这个问题。这次活动开发需要完成一款大富翁游戏,而作为一款大富翁游戏,地图自然是必不可少的。在整个地图中,有很多的不同种类的方格,如果一个个手动去调整位置,工作量是很大的。那么有没有一种方案能够帮助我们快速确定方格的位置和种类呢?下面便是笔者所采用的方法。

方案简述

位点图

首先,我们需要视觉同学提供一张特殊的图片,称之为位点图。

这张图片要满足以下几个要求:

  1. 在每个方格左上角的位置,放置一个 1px 的像素点,不同类型的方格用不同颜色表示。

  2. 底色为纯色:便于区分背景和方格。

  3. 大小和地图背景图大小一致:便于从图中读出的坐标可以直接使用。

bitmap

上图为一个示例,在每个路径方格左上角的位置都有一个 1px 的像素点。为了看起来明显一点,这里用红色的圆点来表示。在实际情况中,不同的点由于方格种类不同,颜色也是不同的。

bitmap2

上图中用黑色边框标出了素材图的轮廓。可以看到,红色圆点和每个路径方格是一一对应的关系。

读取位点图

在上面的位点图中,所有方格的位置和种类信息都被标注了出来。我们接下来要做的,便是将这些信息读取出来,并生成一份 json 文件来供我们后续使用。

const JImp = require('jimp');
const nodepath = require('path');

function parseImg(filename) {
   JImp.read(filename, (err, image) => {
       const { width, height } = image.bitmap;

       const result = [];

       // 图片左上角像素点的颜色, 也就是背景图的颜色
       const mask = image.getPixelColor(0, 0);

       // 筛选出非 mask 位置点
       for (let y = 0; y < height; ++y) {
           for (let x = 0; x < width; ++x) {
               const color = image.getPixelColor(x, y);
               if (mask !== color) {
                   result.push({
                       // x y 坐标
                       x,
                       y,
                       // 方格种类
                       type: color.toString(16).slice(0, -2),
                  });
              }
          }
      }

       // 输出
       console.log(JSON.stringify({
           // 路径
           path: result,
      }));
  });
}

parseImg('bitmap.png');

在这里我们使用了 jimp 用于图像处理,通过它我们能够去扫描这张图片中每个像素点的颜色和位置。

至此我们得到了包含所有方格位置和种类信息的 json 文件:

{
   "path": [
      {
           "type": "",
           "x": 0,
           "y": 0,
      },
       // ...
  ],
}

其中,x y 为方格左上角的坐标;type 为方格种类,值为颜色值,代表不同种类的地图方格。

通路连通算法

对于我们的项目而言,只确定路径点是不够的,还需要将这些点连接成一个完整的通路。为此,我们需要找到一条由这些点构成的最短连接路径。

代码如下:

function takePath(point, points) {
   const candidate = (() => {
       // 按照距离从小到大排序
       const pp = [...points].filter((i) => i !== point);
       const [one, two] = pp.sort((a, b) => measureLen(point, a) - measureLen(point, b));

       if (!one) {
           return [];
      }

       // 如果两个距离 比较小,则穷举两个路线,选择最短连通图路径。
       if (two && measureLen(one, two) < 20000) {
           return [one, two];
      }
       return [one];
  })();

   let min = Infinity;
   let minPath = [];
   for (let i = 0; i < candidate.length; ++i) {
       // 递归找出最小路径
       const subpath = takePath(candidate[i], removeItem(points, candidate[i]));

       const path = [].concat(point, subpath);
       // 测量路径总长度
       const distance = measurePathDistance(path);

       if (distance < min) {
           min = distance;
           minPath = subpath;
      }
  }

   return [].concat(point, minPath);
}

到这里,我们已经完成了所有的准备工作,可以开始绘制地图了。在绘制地图时,我们只需要先读取 json 文件,再根据 json 文件内的坐标信息和种类信息来放置对应素材即可。

方案优化

上述方案能够解决我们的问题,但仍有一些不太方便的地方:

  1. 只有 1px 的像素点太小了,肉眼无法辨别。不管是视觉同学还是开发同学,如果点错了位置就很难排查。

  2. 位点图中包含的信息还是太少了,颜色仅仅对应种类,我们希望能够包含更多的信息,比如点之间的排列顺序、方格的大小等。

像素点合并

对于第一个问题,我们可以让视觉同学在画图的时候,将 1px 的像素点扩大成一个肉眼足够辨识的区域。需要注意两个区域之间不要有重叠。

bitmap3

这时候就要求我们对代码做一些调整。在之前的代码中,当我们扫描到某个颜色与背景色不同的点时,会直接记录其坐标和颜色信息;现在当我们扫描到某个颜色与背景色不同的点时,还需要进行一次区域合并,将所有相邻且相同颜色的点都纳入进来。

区域合并的思路借鉴了下图像处理的区域生长算法。区域生长算法的思路是以一个像素点为起点,将该点周围符合条件的点纳入进来,之后再以新纳入的点为起点,向新起点相邻的点扩张,直到所有符合条件条件的点都被纳入进来。这样就完成了一次区域合并。不断重复该过程,直到整个图像中所有的点都被扫描完毕。

我们的思路和区域生长算法非常类似:

  1. 依次扫描图像中的像素点,当扫描到颜色与背景色不同的点时,记录下该点的坐标和颜色。

    步骤1.png

  2. 之后扫描与该点相邻的 8 个点,将这些点打上”已扫描“的标记。筛选出其中颜色与背景色不同且尚未被扫描过的点,放入待扫描的队列中。

    步骤2.png

  3. 从待扫描队列中取出下一个需要扫描的点,重复步骤 1 和步骤 2。

  4. 直到待扫描的队列为空时,我们就扫描完了一整个有颜色的区域。区域合并完毕。

    步骤3.png

const JImp = require('jimp');

let image = null;
let maskColor = null;

// 判断两个颜色是否为相同颜色 -> 为了处理图像颜色有误差的情况, 不采用相等来判断
const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;

// 判断是(x,y)是否超出边界
const isWithinImage = ({ x, y }) => x >= 0 && x < image.width && y >= 0 && y < image.height;

// 选择数量最多的颜色
const selectMostColor = (dotColors) => { /* ... */ };

// 选取左上角的坐标
const selectTopLeftDot = (reginDots) => { /* ... */ };

// 区域合并
const reginMerge = ({ x, y }) => {
   const color = image.getPixelColor(x, y);
   // 扫描过的点
   const reginDots = [{ x, y, color }];
   // 所有扫描过的点的颜色 -> 扫描完成后, 选择最多的色值作为这一区域的颜色
   const dotColors = {};
   dotColors[color] = 1;

   for (let i = 0; i < reginDots.length; i++) {
       const { x, y, color } = reginDots[i];

       // 朝临近的八个个方向生长
       const seeds = (() => {
           const candinates = [/* 左、右、上、下、左上、左下、右上、右下 */];

           return candinates
               // 去除超出边界的点
              .filter(isWithinImage)
               // 获取每个点的颜色
              .map(({ x, y }) => ({ x, y, color: image.getPixelColor(x, y) }))
               // 去除和背景色颜色相近的点
              .filter((item) => isDifferentColor(item.color, maskColor));
      })();

       for (const seed of seeds) {
           const { x: seedX, y: seedY, color: seedColor } = seed;

           // 将这些点添加到 reginDots, 作为下次扫描的边界
           reginDots.push(seed);

           // 将该点设置为背景色, 避免重复扫描
           image.setPixelColor(maskColor, seedX, seedY);

           // 该点颜色为没有扫描到的新颜色, 将颜色增加到 dotColors 中
           if (dotColors[seedColor]) {
               dotColors[seedColor] += 1;
          } else {
               // 颜色为旧颜色, 增加颜色的 count 值
               dotColors[seedColor] = 1;
          }
      }
  }

   // 扫描完成后, 选择数量最多的色值作为区域的颜色
   const targetColor = selectMostColor(dotColors);

   // 选择最左上角的坐标作为当前区域的坐标
   const topLeftDot = selectTopLeftDot(reginDots);

   return {
       ...topLeftDot,
       color: targetColor,
  };
};

const parseBitmap = (filename) => {
   JImp.read(filename, (err, img) => {
       const result = [];
       const { width, height } = image.bitmap;
       // 背景颜色
       maskColor = image.getPixelColor(0, 0);
       image = img;

       for (let y = 0; y < height; ++y) {
           for (let x = 0; x < width; ++x) {
               const color = image.getPixelColor(x, y);

               // 颜色不相近
               if (isDifferentColor(color, maskColor)) {
                   // 开启种子生长程序, 依次扫描所有临近的色块
                   result.push(reginMerge({ x, y }));
              }
          }
      }
  });
};

颜色包含额外信息

在之前的方案中,我们都是使用颜色值来表示种类,但实际上颜色值所能包含的信息还有很多。

一个颜色值可以用 rgba 来表示,因此我们可以让 r、g、b、a 分别代表不同的信息,如 r 代表种类、g 代表宽度、b 代表高度、a 代表顺序。虽然 rgba 每个的数量都有限(r、g、b 的范围为 0-255,a 的范围为 0-99),但基本足够我们使用了。

rgba.png

当然,你甚至可以再进一步,让每个数字都表示一种信息,不过这样每种信息的范围就比较小,只有 0-9。

总结

对于素材量较少的场景,前端可以直接从视觉稿中确认素材信息;当素材量很多时,直接从视觉稿中确认素材信息的工作量就变得非常大,因此我们使用了位点图来辅助我们获取素材信息。

无标题-2021-09-28-1450.png

地图就是这样一种典型的场景,在上面的例子中,我们已经通过从位点图中读出的信息成功绘制了地图。我们的步骤如下:

  1. 视觉同学提供位点图,作为承载信息的载体,它需要满足以下三个要求:

    1. 大小和地图背景图大小一致:便于我们从图中读出的坐标可以直接使用。

    2. 底色为纯色:便于区分背景和方格。

    3. 在每个方格左上角的位置,放置一个方格,不同颜色的方格表示不同类型。

  2. 通过 jimp 扫描图片上每个像素点的颜色,从而生成一份包含各个方格位置和种类的 json。

  3. 绘制地图时,先读取 json 文件,再根据 json 文件内的坐标信息和种类信息来放置素材。

上述方案并非完美无缺的,在这里我们主要对于位点图进行了改进,改进方案分为两方面:

  1. 由于 1px 的像素点对肉眼来说过小,视觉同学画图以及我们调试的时候,都十分不方便。因此我们将像素点扩大为一个区域,在扫描时,对相邻的相同颜色的像素点进行合并。

  2. 让颜色的 rgba 分别对应一种信息,扩充位点图中的颜色值能够给我们提供的信息。

我们在这里只着重讲解了获取地图信息的部分,至于如何绘制地图则不在本篇的叙述范围之内。在我的项目中使用了 pixi.js 作为引擎来渲染,完整项目可以参考这里,在此不做赘述。

FAQ

  • 在位点图上,直接使用颜色块的大小作为路径方格的宽高可以不?

    当然可以。但这种情况是有局限性的,当我们的素材很多且彼此重叠的时候,如果依然用方块大小作为宽高,那么在位点图上的方块就会彼此重叠,影响我们读取位置信息。

  • 如何处理有损图的情况?

    有损图中,图形边缘处的颜色和中心的颜色会略微有所差异。因此需要增加一个判断函数,只有扫描到的点的颜色与背景色的差值大于某个数字后,才认为是不同颜色的点,并开始区域合并。同时要注意在位点图中方块的颜色尽量选取与背景色色值相差较大的颜色。

    这个判断函数,就是我们上面代码中的 isDifferentColor 函数。

    const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;
  • 判断两个颜色不相等的 0xf000ff 是怎么来的?

    随便定的。这个和图片里包含颜色有关系,如果你的背景色和图片上点的颜色非常相近的话,这个值就需要小一点;如果背景色和图上点的颜色相差比较大,这个值就可以大一点。

参考资料

作者:李一笑
来源:https://segmentfault.com/a/1190000041115022

收起阅读 »

一个Vue3可使用的JSON转excel组件

JSON to Excel for VUE3在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用重要提示! Microsoft Excel中的额外提示此组件中实现的方法...
继续阅读 »



JSON to Excel for VUE3

在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用

重要提示! Microsoft Excel中的额外提示

此组件中实现的方法使用HTML表绘制。在xls文件中,Microsoft Excel不再将HTML识别为本机内容,因此在打开文件之前会显示警告消息。excel的内容已经完美呈现,但是提示信息无法避免,请不要在意!

Getting started

安装依赖:

npm install vue3-json-excel

在vue3的应用入口处有两种注册组件的方式:

import Vue from "vue"
import {vue3JsonExcel} from "vue3-json-excel"

Vue.component("vue3JsonExcel", vue3JsonExcel)

或者

import Vue from "vue"
import vue3JsonExcel from "vue3-json-excel"

Vue.use(vue3JsonExcel)

在template文件中直接使用即可

<vue3-json-excel :json-data="json_data">
Download Data
</vue3-json-excel>

Props List

NameTypeDescriptionDefaultremark
json-dataArray即将导出的数据

fieldsObject要导出的JSON对象内的字段。如果未提供任何属性,将导出JSON中的所有属性。

export-fields (exportFields)Object用于修复使用变量字段的其他组件的问题,如vee-validate。exportFields的工作原理与fields完全相同

typestringMime 类型 [xls, csv]xls1.0.x版本暂时只支持xls,csv会在下个版本迭代
namestringFile 导出的文件名jsonData.xls
headerstring/Array数据的标题。可以是字符串(一个标题)或字符串数组(多个标题)。

title(deprecated)string/Array与header相同,title是出于追溯兼容性目的而维护的,但由于与HTML5 title属性冲突,不建议使用它。

License

MIT

Status

该项目处于早期开发阶段。欢迎参与共建。
有好的产品建议可以联系我!!!!

npm地址

vue3-json-excel

作者:小章鱼
来源:https://segmentfault.com/a/1190000041117522

收起阅读 »

小程序框架对比(Taro VS uni-app)

前前段时间,要开发一个小程序,需要选一个跨平台的框架,为此做了一些调研,在这里记录一下。 目前的跨平台方案大致是以下三种类型,各有优劣。 结合项目自身情况,我选择了第三种类型的框架,再结合支持多平台的要求,重点锁定在了Taro和uni-app之间。 ...
继续阅读 »

前前段时间,要开发一个小程序,需要选一个跨平台的框架,为此做了一些调研,在这里记录一下。


目前的跨平台方案大致是以下三种类型,各有优劣。


image.png


结合项目自身情况,我选择了第三种类型的框架,再结合支持多平台的要求,重点锁定在了Tarouni-app之间。















































框架技术栈微信小程序H5App支付宝/百度小程序
TaroReact/Vue
uni-appVue
WePYVue
mpvueVue

Taro


开发者:


京东


优缺点:


Taro在App端使用的是React Native的渲染引擎,原生的UI体验较好,但据说在实时交互和高响应要求的操作方面不是很理想。


微信小程序方面,结合度感觉没有那么顺滑,有一些常见功能还是需要自己去封装。


另外就是开发环境难度稍高,需要自己去搭建iOS和Android的环境,对于想要一处开发到处应用的傻瓜式操作来讲,稍显繁琐。


但Taro 3的出现,支持了React 和 Vue两种DSL,适合的人群会更多一点,并且对快应用的支持也更好。


案例:


image.png


学习成本:


React、RN、小程序、XCode、Android Studio


uni-app


开发者:


DCloud


优缺点:


uni-app在App渲染方面,提供了原生渲染引擎和小程序引擎的双选方案,加上自身的一些技术优化(renderjs),对于高性能和响应要求的场景展现得更为流畅。


另外它整体的开发配套流程也做得很容易上手。比如有丰富的插件市场,使用简单,支持大量常用场景。


还比如它的定制IDE——HBuilder,提供了强大的整合能力。在用HBuilder之前,我心想:“还要多装一个编辑器麻烦,再好用能有VS Code好用?”用过之后:“真香!”


虽然用惯了VS Code对比起来还是有一些痛点没有解决,但是对于跨平台开发太友好了,其他缺点都可以忍受。HBuilder里支持直接跳转到微信开发者工具调试,支持真机实时预览,支持直接打包小程序和App,零门槛上手。


image.png


不过,uni-app也还是不够成熟,开发中也存在一些坑,需要不时到论坛社区去寻找答案。


代表产品:


image.png


学习成本:


Vue、小程序


总结


跨平台方案目前来看都不完善,适合以小程序、H5为主,原生APP(RN)为辅,不涉及太过复杂的交互的项目。


uni-app 开发简单,小项目效率高,入门容易debug难,不适合中大型项目。
Taro 3 开发流程稍微复杂一点,但复杂项目的支持度会稍好,未来可以打通React和Vue,已经开始支持RN了。



  1. 不考虑原生RN的话二者差不多,考虑RN目前Taro3不支持,只能选uni-app;

  2. 开发效率uni-app高,有自家的IDE(HBuilderX),编译调试打包一体化,对原生App开发体验友好;

  3. 个人技术栈方面倾向于Taro/React,但项目角度uni-app/Vue比较短平快,社区活跃度也比较高。

作者:sherryhe
链接:https://juejin.cn/post/6974584590841167879

收起阅读 »

前端重新部署后,领导跟我说页面崩溃了..

背景: 每次前端更新,重新部署后,用户还停留在更新之前的页面,当请求页面数据时,会导致页面白屏,报错信息如下: Uncaught ChunkLoadError: Loading chunk {n} failed. 原因 每次更新后,用户端的html文件中的 j...
继续阅读 »

背景:


每次前端更新,重新部署后,用户还停留在更新之前的页面,当请求页面数据时,会导致页面白屏,报错信息如下:


Uncaught ChunkLoadError: Loading chunk {n} failed.


原因


每次更新后,用户端的html文件中的 js 和 css 名称就和服务器上的不一致导致,导致加载失败。


解决方案


1.对error事件进行监听,检测到错误之后重新刷新


      window.addEventListener(
'error',
function (event) {
if (
event.message &&
String(event.message).includes('Loading chunk') &&
String(event.message).includes('failed')
) {
window.location.replace(window.location.href);
}
},
true,
);

2.对window.console.error事件监听,效果同上


      window.console.error = function () {
console.log(JSON.stringify(arguments), 'window.console.error'); // 自定义处理
};

3.其他方案


如:HTTP2.0推送机制 / fis3 构建 /webSocket通知等,未尝试


注:有好的方案可以在下面评论讨论哈


本篇收录在个人工作记录专栏中,专门记录一些比较有意思的场景和问题。


后记


在之后的某一天,该问题再次暴露出来。源于一位同事在使用过程中,会不定页面的出现报错情况,报错如下:


image.png


很明显,还是资源加载问题,按道理讲应该可以走入我们逻辑进行刷新。但是当时用户反馈:刷新也不能解决问题,强制刷新才可以解决。


分析:刷新为什么不能解决问题?其实还是因为当用户第一次因为网络原因未成功加载到资源,后续刷新均走的缓存,因此让用户手动去刷新解决不了问题。


解决方案:


不知道大家发现没有,上面对error事件进行监听代码中,并没有包括css失败的情况,因此匹配上css加载失败的情况即可。


更新代码如下:


      window.addEventListener(
'error',
function (event) {
if (
event.message &&
String(event.message).includes('chunk') &&
String(event.message).includes('failed')
) {
window.location.replace(window.location.href);
}
},
true,
);

可能有人会问,既然刷新解决不了问题,那window.location.replace(window.location.href)可以解决吗?


其实刷新的方法有很多,根据MDN的说法,Location.replace() 方法会以给定的URL替换当前的资源,因此可解决此问题。



作者:纵有疾风起
链接:https://juejin.cn/post/6981718762483679239

收起阅读 »

上一个程序员提桶跑路了!我接手后用这些方法优化了项目

平常我们在开发和维护项目的过程中,如果我们跑的项目有点大啊,或者数据太多,导致项目跑起来弊蜗牛还要慢,然后用户体验还不友好,对于新手程序员来说!老板天天都要你加班改!你还不敢辞职!这种时候,就很让人头痛了,怎么办! 但是!也不是没有办法的!骚年!你当时学vue...
继续阅读 »

平常我们在开发和维护项目的过程中,如果我们跑的项目有点大啊,或者数据太多,导致项目跑起来弊蜗牛还要慢,然后用户体验还不友好,对于新手程序员来说!老板天天都要你加班改!你还不敢辞职!这种时候,就很让人头痛了,怎么办!


但是!也不是没有办法的!骚年!你当时学vue的时候可不是这样说的!


接下来我来给你浓重介绍几个优化性能的小技巧,让你的项目蹭蹭蹭的飞起来!老板看了都直呼内行!


1.v-if和v-show的使用场景要区分


v-if 是条件渲染,当条件满足了,那肯定是渲染哇!如果你需要设置一个元素随时隐藏或者消失,然后用v-if是非常的浪费性能的,因为它不停的创建然后销毁。


但是它也是惰性的,如果你开始一给它条件为 false,它就害羞不出来了,跟你家女朋友一样天天晚上都不跟你回家,甚至你家里都没有你女朋友的衣物!然后结构页面里也不会渲染出来查不到这个元素,不知情的朋友以为你谈了个虚拟女友,直到你让它条件为 true ,它才开始渲染,也就是拿了结婚证才跟你回家。


v-show 就很简单,他的原理就是利用 css display 的属性,让他隐藏或者出现,所以一开始渲染页面哪怕我们看不到这个元素,但是它在文档的话,是确确实实存在的,只是因为 display:none; 隐藏了。




就像是你的打气女朋友,平常有人你肯定不敢打气哇!肯定是等夜深人静的时候,才偷偷打气,然后早上又继续放气藏起来,这样是不是很方便咧!所以这个元素你也就是你打气女朋友每天打气放气,是不是也没有那么费力咧!白天就可以藏起来快乐的上班啦!


好啦划重点啦!不要瞎鸡巴想什么女朋友了,女朋友只会影响我码项目的速度!


所以这样看来,如果是很少改变条件来渲染或者销毁的,建议是用 v-if ,如果是需要不断地切换,不断地隐藏又出现又隐藏这些场景的话, v-show 更适合使用!所以要在这些场景里合适的运用 v-if v-show 会节省很多性能以达到性能优化。


2.v-if和v-for不能同时使用


v-if v-for 一起使用时, v-for **** v-if 更高的优先级。这样就意味着 v-if 将分别重复运行于每一个 v-for 循中,那就是先运行 v-for 的循环,然后在每一个 v-for 的循环中,再进行 v-if 的条件对比,会造成性能浪费,影响项目的速度




如果按照下面的写法(我是用vue3写的),好家伙!直接不工作了,没有报错也没有页面,诡异的很


<template>
<div id="app">
<div v-for="item in list" v-if="list.flag" :key="item">{{item.color}}</div>
</div>
</template>
<script>
export default {
data(){
return{
list:[
{
color:"red",
flag:true
},
{
color:"green",
flag:false
},
{
color:"blue",
flag:true
},
],
}
}}
</script>

当你真的需要根据条件渲染页面的时候,建议是采用计算属性,这样及高效且美观,又不会出问题,如下面的代码展示


<template>
<div id="app">
<div v-for="item in newList" :key="item">{{item.color}}</div>
</div>
</template>
<script>
export default {
data(){
return{
list:[
{
color:"red",
flag:true
},
{
color:"green",
flag:false
},
{
color:"blue",
flag:true
},
],
}
},
computed:{
newList(){
return this.list.filter(list => {
return list.flag
})
}
}
}
</script>

3.computed和watch使用要区分场景


先看一下计算属性computed,它支持缓存,当依赖的数据发生变化的时候,才会重新计算,但是它并不支持异步操作,它无法监听数据变化。而且计算属性是基于响应式依赖进行缓存的。


再看一下侦听属性watch,它不支持缓存,它支持异步操作,当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和computed最大的区别。


所以说,如果你的需求是写像购物车那种的,一个属性受其他属性影响的,用计算属性 computed 就像是你家的二哈,你不带它出去玩,你一回家就发现你家能拆的都被二哈拆掉了,因为你不带它出去跟你女朋友逛街!


如果是像写那种像模糊查询的,可以用侦听属性 watch ,因为可以一条数据影响多条数据。 比如你双12给你女朋友买了很多东西,那双十二之后,是不是很多机会回不去宿舍咧!


用好这两个,可以让你的代码更加高效,看起来也更加简洁优雅,让项目蹭蹭跑起来!这样都是一种优化性能的方式!


4.路由懒加载


当Vue项目是单页面应用的时候,可能会有很多路由引入 ,这样的话,使用 webpcak 打包后的文件会非常的大,当第一次进入首页的时候,加载的资源很多多,页面有时候会出现白屏的情况,对用户很不友好,体验也差。


但是,当我们把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就很高效了。会大大提升首屏加载显示的速度,但是可能其他的页面的速度就会降下来。有利有弊吧,根据自己业务需求来使用,实现效果也非常的简单,在 router index.js 文件下,如下所示


import Home from '../views/Home'

const routes = [
{
path:'/home',
name:"Home", //这里没有用懒加载
component:Home
},
{
path:'/about',
name:'About',
component:()=>import(/*webpackChunkName:"about"*/ '../views/About.vue') //这里用了懒加载
}
]

打开浏览器运行,当我没有点击进入 about 组件的时候包的大小就如蓝色框住的那些,当我点击了 about 组件进入后,就增加了后面红色圈住的包,总的大小是增加了



所以,使用路由懒加载可以降低首次加载的时候的性能消耗,但是后面打开这些组件可能会有所减慢,建议是如果体积不大的又不用马上展示的页面可以使用路由懒加载降低性能消耗,从而做到性能优化!


5.第三方插件按需引入


比如我们做一个项目,如果是全局引入第三方插件,打包构建的时候,会将别人整个插件包也一起打包进去,这样的话文件是非常庞大的,然后我们就需要将第三方插件按需引入,这个就需要自己去根据每个插件的官方文档在项目配置,始终就是一句话,用什么引什么!


6.优化列表的数据


当我们遇到哪些,一开始取的数据非常的庞大,然后还要渲染在页面上的时候,比如一下子给你传回来10w条数据还要渲染在页面上,项目一下子渲染出来是非常的有难度的。


这个时候,我们就 需要采用窗口化的技术来优化性能,只需要渲染少部分的内容(比如一下子拿多少条数据),这样就可以减少重新渲染组件和创建dom节点的时间。可以看看下面代码


<template>
<div>
<h3>列表的懒加载</h3>
<div>
<div v-for="item in list">
<div>{{ item }}</div>
</div>
</div>
<div>
<div v-if="moreShowBoolen">滚动加载更多</div>
<div v-else>已无更多</div>
</div>
</div>
</template>
<script>
export default {
name: 'List',
data() {
return {
list: [],
moreShowBoolen: false,
nowPage: 1,
scrollHeight: 0,
};
},
mounted() {
this.init();
// document.documentElement.scrollTop获取当前页面滚动条的位置,documentElement对应的是html标签,body对应的是body标签
// document.compatMode用来判断当前浏览器采用的渲染方式,CSS1Compat表示标准兼容模式开启
window.addEventListener("scroll", () => {
let scrollY=document.documentElement.scrollTop || document.body.scrollTop; // 滚动条在Y轴上的滚动距离
let vh=document.compatMode === "CSS1Compat"?document.documentElement.clientHeight:document.body.clientHeight; // 页面的可视高度
let allHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
); // 整个页面的高度
if (scrollY + vh >= allHeight) {
// 当滚动条滑到页面底部的时候触发这个函数继续添加数据
this.init2();
}
});
},
methods: {
init() {
//一开始就往list添加数据
for (let i = 0; i < 100; i++) {
this.list.push(i);
}
},
init2() {
for (let i = 0; i < 200; i++) {
// 当滑动到底部的时候,继续触发这个函数
this.list.push(i);
}
},
},
};
</script>

这样的话,就可以做到数据懒加载啦,根据需要,逐步添加数据进去,减少一次性拉取所有数据,因为数据是非常庞大的,这样就可以优化很多性能了!


作者:零零后程序员小三
链接:https://juejin.cn/post/7041471019327946759

收起阅读 »

axios 封装,API接口统一管理,支持动态API!

vue
分享一个自己封装的 axios 网络请求 主要的功能及其优点: 将所有的接口放在一个文件夹中管理(api.js)。并且可以支持动态接口,就是 api.js 文件中定义的接口可以使用 :xx 占位,根据需要动态的改变。动态接口用法模仿的是vue的动态路由,如果你...
继续阅读 »

分享一个自己封装的 axios 网络请求


主要的功能及其优点:


将所有的接口放在一个文件夹中管理(api.js)。并且可以支持动态接口,就是 api.js 文件中定义的接口可以使用 :xx 占位,根据需要动态的改变。动态接口用法模仿的是vue的动态路由,如果你不熟悉动态路由可以看看我的这篇文章:Vue路由传参详解(params 与 query)


1.封装请求:



  1. 首先在 src 目录下创建 http 目录。继续在 http 目录中创建 api.js 文件与 index.js 文件。

  2. 然后再 main.js 文件中导入 http 目录下的 index.js 文件。将请求注册为全局组件。

  3. 将下面封装所需代码代码粘到对应的文件夹


2.基本使用:


//示例:获取用户列表
getUsers() {
 const { data } = await this.$http({
   url: 'users' //这里的 users 就是 api.js 中定义的“属性名”
})
},

3.动态接口的使用:


//示例:删除用户
deleteUser() {
 const { data } = await this.$http({
   method: 'delete',
   //动态接口写法模仿的是vue的动态路由
   //这里 params 携带的是动态参数,其中 “属性名” 需要与 api 接口中的 :id 对应
   //也就是需要保证携带参数的 key 与 api 接口中的 :xx 一致
   url: {
     // 这里的 name 值就是 api.js 接口中的 “属性名”
     name: 'usersEdit',
     params: {
       id: userinfo.id,
    },
  },
})
},

4.不足:


封装的请求只能这样使用 this.$http() 。不能 this.$http.get()this.$http.delete()


由于我感觉使用 this.$http() 这种就够了,所以没做其他的封装处理


如果你有更好的想法可以随时联系我


如下是封装所需代码:



  • api.js 管理所有的接口


// 如下接口地址根据自身项目定义
const API = {
 // base接口
 baseURL: 'http://127.0.0.1:8888/api/private/v1/',
 // 用户
 users: '/users',
 // “修改”与“删除”用户接口(动态接口)
 usersEdit: '/users/:id',
}

export default API


  • index.js 逻辑代码


// 这里请求封装的主要逻辑,你可以分析并将他优化,如果有更好的封装方法欢迎联系我Q:2356924146
import axios from 'axios'
import API from './api.js'

const instance = axios.create({
 baseURL: API.baseURL,
 timeout: '8000',
 method: 'GET'
})

// 请求拦截器
instance.interceptors.request.use(
 config => {
   // 此处编写请求拦截代码,一般用于加载弹窗,或者每个请求都需要携带的token
   console.log('正在请求...')
   // 请求携带的token
   config.headers.Authorization = sessionStorage.getItem('token')
   return config
},
 err => {
   console.log('请求失败', err)
}
)

// 响应拦截器
instance.interceptors.response.use(
 res => {
   console.log('响应成功')
   //该返回对象会绑定到响应对象中
   return res
},
 err => {
   console.log('响应失败', err)
}
)

//options 接收 {method, url, params/data}
export default function(options = {}) {
 return instance({
   method: options.method,
   url: (function() {
     const URL = options.url

     if (typeof URL === 'object') {
       //拿到动态 url
       let DynamicURL = API[URL.name]

       //将 DynamicURL 中对应的 key 进行替换
       for (const key of Object.keys(URL.params)) {
         DynamicURL = DynamicURL.replace(':' + key, URL.params[key])
      }

       return DynamicURL
    } else {
       return API[URL]
    }
  })(),
   //获取查询字符串参数
   params: options.params,
   //获取请求体字符串参数
   data: options.data
})
}



  • main.js 将请求注册为全局组件


import Vue from 'vue'

// 会自动导入 http 目录中的 index.js 文件
import http from './http'

Vue.prototype.$http = http

作者:寸头男生
链接:https://juejin.cn/post/7006508579595223070

收起阅读 »

关于组件文档从编写到生成的那些事

前言说到前端领域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是当前的前端攻城狮们都免不了要实际使用到的基础组件库。而在实际工作中,我们也总免不了要根据自己的工作内容,整理一些适合自己业务风格的一套组...
继续阅读 »



前言

说到前端领域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是当前的前端攻城狮们都免不了要实际使用到的基础组件库。而在实际工作中,我们也总免不了要根据自己的工作内容,整理一些适合自己业务风格的一套组件库,基础组件部分可以基于上面开源的组件库以及 less 框架等多主题样式方案做自己的定制,但更多的是一些基于这些基础组件整理出适合自己业务产品的一套业务组件库。

而说到开发组件库,我们或选择 Monorepo 单仓库多包的形式(参考网文 https://segmentfault.com/a/11... 等)或其他 Git 多仓库单包的形式来维护组件代码,最终都免不了要将组件真正落到一个文档中,提供给其他同事去参考使用。

本篇文章就产出组件文档这件事,聊聊我在产出文档过程中的一系列思考过程,解决组件开发这「最后一公里」中的体验问题。

组件文档的编写

规范与搭建的调研

组件文档是要有一定的规范的,规范是任何软件工程阶段的第一步。对于组件来说,文档透出的内容都应包含哪些内容,决定着组件开发者和使用者双方的所有的体验。确定这些规范并不难,参考开源的组件库的文档,我们会有一些初步的印象。

因为我们团队使用的是 React 技术栈,这里我们参考 Ant Design 组件库。

比如这个最基本的 Button 组件,官方文档从上至下的内容结构是这样:

  1. 显示组件标题,下跟组件的简单描述。

  2. 列出组件的使用场景。

  3. 不同使用场景下的效果演示、代码案例。可外跳 CodeSandbox、CodePen 等在线源码编辑器网站编辑实时查看效果。

  4. 列出组件可配置的属性、接口方法列表。列表中包含属性/方法名、字段类型、默认值、使用描述等。

  5. 常见问题的 FAQ。

  6. 面向设计师的一些 Case 说明链接。

这些文档内容很丰富,作为一个开放的组件库,几乎考虑到的从设计到开发视角的方方面面,使用体验特别好。而在好奇心驱使下,我去查看了官网源码方库,比如 Button 组件:https://github.com/ant-design...。在源码库下,放置了和组件入口文件同名的分别以 .zh-CN.md.en-US.md 后缀命名的 Markdown 文件,而在这些 Markdown 文件中,便是我们看到的官网文档内容...咦?不对,好像缺少了什么,案例演示和示例代码呢?

难道 AntD 官网文档是另外自己手动开发维护的?这么大的一个项目肯定不至于,根据其官网类似 docs/react/introduce-cn 这种访问路径在源码库中有对应的 Markdown 文件来看,官网的文档肯定是官方仓库根据一种规范来生成的。那么是怎么生成的呢?第一次做组件文档规范的我被挑起了兴趣。

而作为一个前端工程老手,我很熟练地打开了其 package.json 文件,通过查看其中的 scripts 命令,轻易便发现了其下的 site 命令(源码仓库 package.json):

npm run site:theme && cross-env NODE_ICU_DATA=node_modules/full-icu ESBUILD=1 bisheng build --ssr -c ./site/bisheng.config.js

原来如此,网站的构建使用了 bisheng。通过查阅了解 bisheng 这个工具库,发现它确实是一个文档系统的自动生成工具,其下有一个插件 bisheng-plugin-react,可以将 Markdown 文档中的 JSX 源码块转换成可以运行演示的示例。而每个组件自身的示例代码文档,则在每个组件路径下的
demo 目录下维护。

Emmm,bisheng 确实是很好很强大,还能支持多语言,结合一定的文档规范约束下,能够快速搭建一个文档的主站。但在深入了解 bisheng 的过程中,发现其文档相对来说比较缺乏,包装的东西又那么多,使用过程中黑盒感严重,而我们团队的组件库其实要求很简单,一是能做到方便流通,而是只在内部流通使用,不会开源。那么,有没有更简单的搭建文档的方式呢?

更多的文档工具库的调研

在谷歌搜索中敲入如 React Components Documentation 等关键字,我们很快便能搜索出很多与 React 组件文档相关的工具库,这里我看到了如下这些:DoczStoryBookReact Styleguidist 、UMI 构建体系下的 dumi 等等。

这些工具库都支持解析 Markdown 文档,其中 DoczStoryBook 还支持使用 mdx 格式(Markdown 和 JSX 的混合写法),且在文档内容格式都能支持到组件属性列表、示例代码演示等功能。

接下来,我们分别简单看下这些工具库对于组件文档的支持情况。

Docz

在了解过程中,发现 Docz 其实是一个比较老牌的文档系统搭建工具了。它本身便主推 MDX 格式的文档,基本不需要什么配置便能跑起来。支持本地调试和构建生成可发布产物,支持多包仓库、TypeScript 环境、CSS 处理器、插件机制等,完全满足功能需要。

只是 Docz 貌似只支持 React 组件(当然对于我们来说够用),且看其 NPM 包最近更新已经是两年之前。另外 MDX 格式的文档虽然理解成本很少但对于使用不多的同事来说还是有一定的接受和熟练上手的成本。暂时备选。

StoryBook

在初次了解到 StoryBook 时便被其 66.7K 的 Star 量惊到了(Docz 是 22K),相对 Docz 来说,StoryBook 相关的社区内容非常丰富,它不依赖组件的技术栈体系,现在已经支持 React、Vue、Angular、Web Components 等数十个技术栈。

StoryBook 搭建文档系统的方式不是去自动解析 Markdown 文件,而是暴露一系列搭建文档的接口,让开发者自己为组件手动编写一个个的 stories 文件,StoryBook 会自动解析这些 stories 文件来生成文档内容。这种方式会带来一定的学习和理解接口的成本,但同时也基于这种方式实现了支持跨组件技术栈的效果,并让社区显得更为丰富。

官方示例:https://github.com/storybookj...

StoryBook 的强大毋庸置疑,但对于我们团队的情况来说还是有些杀鸡用牛刀了。另外,其需要额外理解接口功能并编写组件的 stories 文件在团队内很难推动起来:大家都很忙,组件开发分布在团队几十号人,情况比较复杂,将文档整理约束到一个人身上又不现实。继续调研。

React Styleguidist

React Styleguidist 的 Star 量没有 StoryBook 那么耀眼(10K+),但包体的下载量也比较大,且近期的提交也是相当活跃。由名字可知,它支持的是 React 组件的环境。它是通过自动解析 Mardown 文件的形式来生成文档的,实现方式是自动解析文档中 JSX 声明代码块,按照名称一一对应的规则查找到组件源码,然后将声明的代码块通过 Webpack 打包产生出对应的演示示例。

而在继续试用了 React Styleguidist 的一些基础案例后,它的一个功能让我眼前一亮:它会自动解析组件的属性,并解析出其类型、默认值、注释描述等内容,然后将解析到的内容自动生成属性表格放置在演示示例的上方。这就有点 JSDOC 的意思了,对于一个组件开发者来说,TA 确实需要关心组件属性的透出、注释以及文档案例的编写,但编写完也就够了,不用去考虑怎么适应搭建出一个文档系统。

另外, React Styleguidist 解析组件属性是基于解析 AST 以及配合工具 react-docgen 来实现的,并且还支持配合 react-docgen-typescript 来实现解析 TypeScript 环境下的组件,另外还能很多配置项支持更改文档站点相关的各个部分的展示样式、内容格式等,配置自定义支持相当灵活。

当然,它也有一些缺点,比如内嵌 Webpack,对于已经将编译组件库的构建工具换为 Rollup.js 的情况是一个额外的配置负担。

总的来说,React Styleguidist 在我看来是一个小而美的工具库,很适合我们团队协作参与人多、且大都日常开发工作繁重的情况。暂时备选。

dumi

了解到 dumi 是因为我们团队内已经有部分组件文档站点是基于它来搭建的了。dumi 一样是通过自动解析 Markdown 文档的方式来实现搭建文档系统,同样基本零配置,也有很多灵活的配置支持更改文档站点一些部分的显示内容、(主题)样式等,整体秉承了 UMI 体系的风格:开箱即用,封装极好。它能单独使用,也能结合 UMI 框架一起配置使用。

只是相比于上面已经了解到的 React Styleguidist 来说,并未看到有其他明显的优势,且貌似没有看到有自动解析组件属性部分的功能,对于我来说没有 React Styleguidist 下得一些亮点。可以参考,不再考虑。

组件文档的生成

在多方对比了多个文档搭建的工具库后,我最终还是选用了 React Styleguidist。在我看来,自然是其基于 react-docgen 来实现解析组件属性、类型、注释描述等的功能吸引到了我,这个功能一方面能在较少的额外时间付出下规范团队同事开发组件过程中一系列规范,另一方面其 API 接口的接入形式能够通过统一构建配置而统一产出文档内容格式和样式,方便各业务接入使用。

决定了技术方案后,便是如何具体实现基于其封装一个工具,便于各业务仓库接入了。

我们团队有自己统一的 CLI 构建工具,再多一个 React Styleguidist 的 CLI 配置会在理解上有一定的熟悉成本,但我可以基于 React Styleguidist 的 Node API 接入形式,将 React Styleguidist 的功能分别融入我们自身 CLI 的 devbuild 命令。

首先,基于 React Styleguidist API 的形式,统一一套配置,将生成 React Styleguidist 示例的代码抽象出来:

// 定义一套统一的配置,生成 react-styleguidist 实例
import styleguidist from 'react-styleguidist/lib/scripts/index.esm';
import * as docgen from 'react-docgen';
import * as docgenTS from 'react-docgen-typescript';

import type * as RDocgen from 'react-docgen';

export type DocStyleguideOptions = {
 cwd?: string;
 rootDir: string;
 workDir: string;
 customConfig?: object;
};

const DOC_STYLEGUIDE_DEFAULTS = {
 cwd: process.cwd(),
 rootDir: process.cwd(),
 workDir: process.cwd(),
 customConfig: {},
};

export const createDocStyleguide = (
 env: 'development' | 'production',
 options: DocStyleguideOptions = DOC_STYLEGUIDE_DEFAULTS,
) => {
 // 0. 处理配置项
 const opts = { ...DOC_STYLEGUIDE_DEFAULTS, ...options };
 const {
   cwd: cwdPath = DOC_STYLEGUIDE_DEFAULTS.cwd,
   rootDir,
   workDir,
   customConfig,
} = opts;

 // 标记:是否正在调试所有包
 let isDevAllPackages = true;

 // 解析工程根目录包信息
 const pkgRootJson = Utils.parsePackageSync(rootDir);

 // 1. 解析指定要调试的包下的组件
 let componentsPattern: (() => string[]) | string | string[] = [];
 if (path.relative(rootDir, workDir).length <= 0) {
   // 选择调试所有包时,则读取根路径下 packages 字段定义的所有包下的组件
   const { packages = [] } = pkgRootJson;
   componentsPattern = packages.map(packagePattern => (
     path.relative(cwdPath, path.join(rootDir, packagePattern, 'src/**/[A-Z]*.{js,jsx,ts,tsx}'))
  ));
} else {
   // 选择调试某个包时,则定位至选择的具体包下的组件
   componentsPattern = path.join(workDir, 'src/**/[A-Z]*.{js,jsx,ts,tsx}');
   isDevAllPackages = false;
}

 // 2. 获取默认的 webpack 配置
 const webpackConfig = getWebpackConfig(env);

 // 3. 生成 styleguidist 配置实例
 const styleguide = styleguidist({
   title: `${pkgRootJson.name}`,
   // 要解析的所有组件
   components: componentsPattern,
   // 属性解析设置
   propsParser: (filePath, code, resolver, handlers) => {
     if (/\.tsx?/.test(filePath)) {
       // ts 文件,使用 typescript docgen 解析器
       const pkgRootDir = findPackageRootDir(path.dirname(filePath));
       const tsConfigParser = docgenTS.withCustomConfig(
         path.resolve(pkgRootDir, 'tsconfig.json'),
        {},
      );
       const parseResults = tsConfigParser.parse(filePath);
       const parseResult = parseResults[0];
       return (parseResult as any) as RDocgen.DocumentationObject;
    }
     // 其他使用默认的 react-docgen 解析器
     const parseResults = docgen.parse(code, resolver, handlers);
     if (Array.isArray(parseResults)) {
       return parseResults[0];
    }
     return parseResults;
  },
   // webpack 配置
   webpackConfig: { ...webpackConfig },
   // 初始是否展开代码样例
   // expand: 展开 | collapse: 折叠 | hide: 不显示;
   exampleMode: 'expand',
   // 组件 path 展示内容
   getComponentPathLine: (componentPath) => {
     const pkgRootDir = findPackageRootDir(path.dirname(componentPath));
     try {
       const pkgJson = Utils.parsePackageSync(pkgRootDir);
       const name = path.basename(componentPath, path.extname(componentPath));
       return `import ${name} from '${pkgJson.name}';`;
    } catch (error) {
       return componentPath;
    }
  },
   // 非调试所有包时,不显示 sidebar
   showSidebar: isDevAllPackages,
   // 日志配置
   logger: {
     // One of: info, debug, warn
     info: message => Utils.log('info', message),
     warn: message => Utils.log('warning', message),
     debug: message => console.debug(message),
  },
   // 覆盖自定义配置
   ...customConfig,
});

 return styleguide;
};

这样,在 devbuild 命令下可以分别调用实例的 server 接口方法和 build 接口方法来实现调试和构建产出文档静态资源。

// dev 命令下启动调试
// 0. 初始化配置
const HOST = process.env.HOST || customConfig.serverHost || '0.0.0.0';
const PORT = process.env.PORT || customConfig.serverPort || '6060';

// 1. 生成 styleguide 实例
const styleguide = createDocStyleguide(
 'development',
{
   cwd: cwdPath,
   rootDir: pkgRootPath,
   workDir: workPath,
   customConfig: {
     ...customConfig,
     // dev server host
     serverHost: HOST,
     // dev server port
     serverPort: PORT,
  },
},
);

// 2. 调用 server 接口方法启动调试
const { compiler } = styleguide.server((err, config) => {
 if (err) {
   console.error(err);
} else {
   const url = `http://${config.serverHost}:${config.serverPort}`;
   Utils.log('info', `Listening at ${url}`);
}
});
compiler.hooks.done.tap('done', (stats: any) => {
 const timeStr = stats.toString({
   all: false,
   timings: true,
});

 const statStr = stats.toString({
   all: false,
   warnings: true,
   errors: true,
});

 console.log(timeStr);

 if (stats.hasErrors()) {
   console.log(statStr);
   return;
}
});
// build 命令下执行构建

// 生成 styleguide 实例
const styleguide = MonorepoDev.createDocStyleguide('production', {
 cwd,
 rootDir,
 workDir,
 customConfig: {
   styleguideDir: path.join(pkgDocsDir, 'dist'),
},
});
// 构建文档内容
await new Promise<void>((resolve, reject) => {
 styleguide.build(
  (err, config, stats) => {
     if (err) {
       reject(err);
    } else {
       if (stats != null) {
         const statStr = stats.toString({
           all: false,
           warnings: true,
           errors: true,
        });
         console.log(statStr);
         if (stats.hasErrors()) {
           reject(new Error('Docs build failed!'));
           return;
        }
         console.log('\n');
         Utils.log('success', `Docs published to ${path.relative(workDir, config.styleguideDir)}`);
      }
       resolve();
    }
  },
);

最后,在组件多包仓库的每个包下的 package.json 中,分别配置 devbuild 命令即可。实现了支持无感启动调试和构建产出文档资源。

小结

本文主要介绍了我在调研实现组件文档规范和搭建过程中的一个思考过程,诚如文中介绍其他文档系统搭建工具时所说,有很多优秀的开源工具能够支持实现我们想要的效果,这是前端攻城狮们的幸运,也是不幸:我们可以站在前人的肩膀上,但要在这么多优秀库中选择一个适合自己的,更需要多做一些了解和收益点的权衡。一句老话经久不衰:适合自己的才是最好的。

希望这篇文章对看到这里的你能有所帮助。

作者:ES2049 / 靳志凯
链接:https://segmentfault.com/a/1190000041097170

收起阅读 »

前端 4 种渲染技术的计算机理论基础

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?本文我们就来谈一下网页渲染技术的计算机理论基础。渲染的理论基础人眼的视网膜有视觉暂留机...
继续阅读 »

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?

本文我们就来谈一下网页渲染技术的计算机理论基础。

渲染的理论基础

人眼的视网膜有视觉暂留机制,也就是看到的图像会继续保留 0.1s 左右,图形界面就是根据这个原理来设计的一帧一帧刷新的机制,要保证 1s 至少要渲染 10 帧,这样人眼看到画面才是连续的。

每帧显示的都是图像,它是由像素组成的,是显示的基本单位。不同显示器实现像素的原理不同。

我们要绘制的目标是矩形、圆形、椭圆、曲线等各种图形,绘制完之后要把它们转成图像。图形的绘制有一系列的理论,比如贝塞尔曲线是画曲线的理论。图形转图像的过程叫做光栅化。这些图形的绘制和光栅化的过程,都是图形学研究的内容。

图形可能做缩放、平移、旋转等变化,这些是通过矩阵计算来实现的,也是图形学的内容。

除了 2D 的图形外,还要绘制 3D 的图形。3D 的原理是把一个个三维坐标的顶点连起来,构成一个一个三角形,这是造型的过程。之后再把每一个三角形的面贴上图,叫做纹理。这样组成的就是一个 3D 图形,也叫 3D 模型。

3D 图形也同样需要经历光栅化变成二维的图像,然后显示出来。这种三维图形的光栅化需要找一个角度去观察,就像拍照一样,所以一般把这个概念叫做相机。

同时,为了 3D 图形更真实,还引入了光线的概念,也就是一束光照过来,3D 图形的每个面都会有什么变化,怎么反射等。不同材质的物体反射的方式不同,比如漫反射、镜面反射等,也就有不同的计算公式。一束光会照射到一些物体,到物体的反射,这个过程需要一系列跟踪的计算,叫做光线追踪技术。

我们也能感受出来,3D 图形的计算量比 2D 图形大太多了,用 CPU 计算很可能达不到 1s 大于 10 帧,所以后面出现了专门用于 3D 渲染加速的硬件,叫做 GPU。它是专门用于这种并行计算的,可以批量计算一堆顶点、一堆三角形、一堆像素的光栅化,这个渲染流程叫做渲染管线。

现在的渲染管线都是可编程的,也就是可以控制顶点的位置,每个三角形的着色,这两种分别叫做顶点着色器(shader)、片元着色器。

总之,2D 或 3D 的图形经过绘制和光栅化就变成了一帧帧的图像显示出来。

变成图像之后其实还可以做一些图像处理,比如灰度、反色、高斯模糊等各种滤镜的实现。

所以,前端的渲染技术的理论基础是计算机图形学 + 图像处理。

不同的渲染技术的区别和联系

具体到前前端的渲染技术来说,html+css、svg、canvas、webgl 都是用于图形和图像渲染的技术,但是它们各有侧重:

html + css

html + css 是用于图文布局的,也就是计算文字、图片、视频等的显示位置。它提供了很多计算规则,比如流式布局很适合做图文排版,弹性布局易于做自适应的布局等。但是它不适合做更灵活的图形绘制,这时就要用其他几种技术了。

canvas

canvas 是给定一块画布区域,在不同的位置画图形和图像,它没有布局规则,所以很灵活,常用来做可视化或者游戏的开发。但是 canvas 并不会保留绘制的图形的信息,生成的图像只能显示在固定的区域,当显示区域变大的时候,它不能跟随一起放缩,就会失真,如果有放缩不失真的需求就要用其他渲染技术了。

svg

svg 会在内存中保留绘制的图形的信息,显示区域变化后会重新计算,是一个矢量图,常用于 icon、字体等的绘制。

webgl

上面的 3 种技术都是用于 2D 的图形图像的绘制,如果想绘制 3D 的内容,就要用 webgl 了。它提供了绘制 3D 图形的 api,比如通过顶点构成 3D 的模型,给每一个面贴图,设置光源,然后光栅化成图像等的 api。它常用于通过 3D 内容增强网站的交互效果,3D 的可视化,3D 游戏等,再就是虚拟现实中的 3D 交互。

所以,虽然前端渲染技术的底层原理都是图形学 + 图像处理,但上层提供的 4 种渲染技术各有侧重点。

不过,它们还是有很多相同的地方的:

  • 位置、大小等的变化都是通过矩阵的计算

  • 都要经过图形转图像,也就是光栅化的过程

  • 都支持对图像做进一步处理,比如各种滤镜

  • html + css 渲染会分不同图层分别做计算,canvas 也会根据计算量分成不同的 canvas 来做计算

因为他们底层的图形学原理还是一致的。

除此以外,3D 内容,也就是 webgl 的内容会通过 GPU 来计算,但 css 其实也可以通过 GPU 计算,这叫做 css 的硬件加速,有四个属性可以触发硬件加速:transform、opacity、filter、will-change。(更多的 GPU 和 css 硬件加速的内容可以看这篇文章:这一次,彻底搞懂 GPU 和 css 硬件加速

编译原理的应用

除了图形学和图像技术外,html+css 还用到了编译技术。因为 html、css 是一种 DSL( domin specific language,领域特定语言),也就是专门为界面描述所设计的语言。用 html 来表达 dom 结构,用 css 来给 dom 添加样式都只需要很少的代码,然后运行时解析 html 和 css 来创建 dom、添加样式。

DSL 可以让特定领域的逻辑更容易表达,前端领域还有一些其他技术也用到了 DSL,比如 graphql。

总结

因为人眼的视觉暂留机制,只要每帧绘制不超过 0.1s,人看到的画面就是连续的,这是显示的原理。每帧的绘制要经过图形绘制和图形转图像的光栅化过程,2D 和 3D 图形分别有不同的绘制和光栅化的算法。此外,转成图像之后还可以做进一步的图像处理。

前端领域的四种渲染技术:html+css、canvas、svg、webgl 各有侧重点,分别用于不同内容的渲染:

  • html+ css 用于布局

  • canvas 用于灵活的图形图像渲染

  • svg 用于矢量图渲染

  • webgl 用于 3D 图形的渲染

但他们的理论基础都是计算机图形学 + 图像处理。(而且,html+css 为了方便逻辑的表达,还设计了 DSL,这用到了编译技术)

这四种渲染技术看似差别很大,但在理论基础层面,很多东西都是一样的。这也是为什么我们要去学计算机基础,因为它可以让我们对技术有一个更深入的更本质的理解。


作者:zxg_神说要有光
来源:https://juejin.cn/post/7041157165024804895

收起阅读 »

如何用 docker 打造前端开发环境

用 docker 做开发环境的好处 保持本机清爽 做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了...
继续阅读 »

用 docker 做开发环境的好处


保持本机清爽


做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了 java),久而久之,本机环境会非常乱,对于一些强迫证患者或者有软件洁癖的人来说多少有点不爽。


使用 docker 后,开发环境都配置在容器中,开发时只需要打开 docker,开发完后关闭 docker,本机不用再安装乱七八糟的环境,非常清爽。


隔离环境


不知道大家在开发时有没有遇到这种情况:公司某些项目需要在较新的 node 版本上运行(比如 vite,需要在 node12 或以上),某些老的项目需要在较老的 node 版本上运行,切换起来比较麻烦,虽然可以用 nvm 来解决,但使用 docker 可以更方便的解决此问题。


快速配置环境


买了新电脑,或者重装了系统,又或者换了新的工作环境,第一件事就是配置开发环境。下载 node、git,然后安装一些 npm 的全局包,然后下载 vscode,配置 vscode,下载插件等等……


使用 docker 后,只需从 docker hub 中拉取事先打包好的开发环境镜像,就可以愉快的进行开发了。


安装 docker


到 docker 官网(http://www.docker.com)下载 docker desktop 并安装,此步比较简单,省略。


安装完成,打开 docker,待其完全启动后,打开 shell 输入:


docker images

截屏2021-09-29 22.16.13.png


显示上述信息即成功!


配置开发环境


假设有一个项目,它必须要运行在 8.14.0 版本的 node 中,我们先去 docker hub 中将这个版本的 node 镜像拉取下来:


docker pull node:8.14.0

拉取完成后,列出镜像列表:


docker images

截屏2021-09-29 23.30.16.png


有了镜像后,就可以使用镜像启动一个容器:


docker run -it --name my_container 3b7ecd51 /bin/bash

上面的命令表示以命令行交互的模式启动一个容器,并将容器的名称指定为 my_container。


截屏2021-10-08 23.16.11.png


此时已经新建并进入到容器,容器就是一个 linux 系统,可以使用 linux 命令,我们尝试输入一些命令:


截屏2021-10-08 23.18.11.png


可以看到这个 node 镜像除了预装了 node 8.14.0,还预装了 git 2.11.0。



镜像和容器的的关系:镜像只预装了最基本的环境,比如上面的 node:8.14.0 镜像可以看成是预装了 node 8.14.0 的 linux 系统,而容器是基于镜像克隆出来的另一个 linux 系统,可以在这个系统中安装其它环境比如 java、python 等,一个镜像可以建立多个容器,每个容器环境都是相互隔离的,互不影响(比如在容器 A 中安装了 java,容器 B 是没有的)。



使用命令行操作项目并不方便,所以我们先退出命令行模式,使用 exit 退出:


截屏2021-10-08 23.20.49.png


借助 IDE 可以更方便的玩 docker,这里我们选择 vscode,打开 vscode,安装 Remote - Containers 扩展,这个扩展可以让我们更方便的管理容器:


截屏2021-10-08 23.03.53.png


安装成功后,左下角会多了一个图标,点击:


截屏2021-10-08 23.23.00.png


在展开菜单中选择“Attach to Running Container”:


截屏2021-10-08 23.25.02.png


此时会报一个错“There are no running containers to attach to.”,因为我们刚刚退出了命令行交互模式,所以现在容器是处理停止状态的,我们可以使用以下命令来查看正在运行的容器:


docker ps

# 或者
docker container ls

截屏2021-10-08 23.30.51.png


发现列表中并没有正在运行的容器,我们需要找到刚刚创建的容器并将其运行起来,先显示所有容器列表:


# -a 可以显示所有容器,包括未运行的
docker ps -a

# 或者
docker container ls -a

截屏2021-10-08 23.29.25.png


运行指定容器:


# 使用容器名称
docker start my_container

# 或者使用容器 id,id 只需输入前几位,docker 会自动识别
docker start 8ceb4

再次运行 docker ps 命令后,就可以看到已运行的容器了。然后回到 vscode,再次选择"Attach to Running Container",就会出现正在运行的容器列表:


截屏2021-10-08 23.36.14.png


选择容器进入,添加一个 bash 终端,就可以进入我们刚刚的命令行模式:


截屏2021-10-08 23.40.14.png


我们安装 vue-cli,并在 /home 目录下创建一个项目:


# 安装 vue-cli
npm install -g @vue/cli

# 进入到 home 目录
cd /home

# 创建 vue 项目
vue create demo

在 vscode 中打开目录,发现打开的不再是本机的目录,而是容器中的目录,找到我们刚刚创建的 /home/demo 打开:


截屏2021-10-09 00.01.13.png


输入 npm run serve,就可以愉快的进行开发啦:


截屏2021-10-09 00.03.32.png


上面我们以 node 8.14.0 镜像为例创建了一个开发环境,如果想使用新版的 node 也是一样的,只需要将指定版本的 node 镜像 pull 下来,然后使用这个镜像创建一个容器,并在容器中创建项目或者从 git 仓库中拉取项目进行开发,这样就有了两个不同版本的 node 开发环境,并且可以同时进行开发。


使用 ubuntu 配置开发环境


上面这种方式使用起来其实并不方便,因为 node 镜像只安装了 node 和 git,有时我们希望镜像可以内置更多功能(比如预装 nrm、vue-cli 等 npm 全局包,或者预装好 vscode 的扩展等),这样用镜像新建的容器也包含这些功能,不需要每个容器都要安装一次。


我们可以使用 ubuntu 作为基础自由配置开发环境,首先获取 ubuntu 镜像:


# 不输入版本号,默认获取 latest 即最新版
docker pull ubuntu

新建一个容器:


docker run -itd --name fed 597ce /bin/bash

这里的 -itd 其实是 -i -t -d 的合写,-d 是在后台中运行容器,相当于新建时一并启动容器,这样就不用使用 docker start 命令了。后面我们直接用 vscode 操作容器,所以也不需要使用命令行模式了。


我们将容器命名为 fed(表示 front end development),建议容器的名称简短一些,方便输入。


截屏2021-10-10 00.11.40.png


ubuntu 镜像非常纯净(只有 72m),只具备最基本的能力,为了后续方便使用,我们需要更新一下系统,更新前为了速度快一点,先换到阿里的源,用 vscode 打开 fed 容器,然后打开 /etc/apt/sources.list 文件,将内容改为:


deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

截屏2021-10-10 00.18.57.png


在下面的终端中依次输入以下命令更新系统:


apt-get update
apt-get upgrade

安装 sudo:


apt-get install sudo

安装 git:


apt-get install git

安装 wget(wget 是一个下载工具,我们需要用它来下载软件包,当然也可以选择 axel,看个人喜好):


apt-get install wget

为了方便管理项目与软件包,我们在 /home 目录中创建两个文件夹(projects 与 packages),projects 用于存放项目,packages 用于存放软件包:


cd /home
mkdir projects
mkdir packages

由于 ubuntu 源中的 node 版本比较旧,所以从官网中下载最新版,使用 wget 下载 node 软件包:


# 将 node 放到 /home/packages 中
cd /home/packages

# 需要下载其它版本修改版本号即可
wget https://nodejs.org/dist/v14.18.0/node-v14.18.0-linux-x64.tar

解压文件:


tar -xvf node-v14.18.0-linux-x64.tar

# 删除安装包
rm node-v14.18.0-linux-x64.tar

# 改个名字,方便以后切换 node 版本
mv node-v14.18.0-linux-x64 node

配置 node 环境变量:


# 修改 profile 文件
echo "export PATH=/home/packages/node/bin:$PATH" >> /etc/profile

# 编译 profile 文件,使其生效
source /etc/profile

# 修改 ~.bashrc,系统启动时编译 profile
echo "source /etc/profile" >> ~/.bashrc

# 之后就可以使用 node 和 npm 命令了
node -v
npm -v

安装 nrm,并切换到 taobao 源:


npm install nrm -g
nrm use taobao

安装一些 vscode 扩展,比如 eslint、vetur 等,扩展是安装在容器中的,在容器中会保留一份配置文件,到时打包镜像会一并打包进去。当我们关闭容器后再打开 vscode,可以发现本机的 vscode 中并没有安装这些扩展。


至此一个简单的前端开发环境已经配置完毕,可以根据自己的喜好自行添加一些包,比如 yarn、nginx、vim 等。


打包镜像


上面我们通过 ubuntu 配置了一个简单的开发环境,为了复用这个环境,我们需要将其打包成镜像并推送到 docker hub 中。


第一步:先到 docker 中注册账号。


第二步:打开 shell,登录 docker。


截屏2021-10-10 01.44.26.png


第三步:将容器打包成镜像。


# commit [容器名称] [镜像名称]
docker container commit fed fed

第四步:为镜像打 tag,因为镜像推送到 docker hub 中,要用 tag 来区分版本,这里我们先设置为 latest。tag 名称加上了用户名做命名空间,防止与 docker hub 上的镜像冲突。


docker tag fed huangzhaoping/fed:latest

第五步:将 tag 推送至 docker hub。


docker push huangzhaoping/fed:latest

第六步:将本地所有关于 fed 的镜像和容器删除,然后从 docker hub 中拉取刚刚推送的镜像:


# 拉取
docker pull huangzhaoping/fed

# 创建容器
docker run -itd --name fed huangzhaoping/fed /bin/bash

用 vscode 打开容器,打开命令行,输入:


node -v
npm -v
nrm -V
git --version

然后再看看 vscode 扩展,可以发现扩展都已经安装好了。


如果要切换 node 版本,只需要下载指定版本的 node,解压替换掉 /home/packages/node 即可。


至此一个 docker 开发环境的镜像就配置完毕,可以在不同电脑,不同系统中共享这个镜像,以达到快速配置开发环境的目的。


注意事项



  • 如果要将镜像推送到 docker hub,不要将重要的信息保存到镜像中,因为镜像是共享的,避免重要信息泄露。

  • 千万不要在容器中存任何重要的文件或信息,因为容器一旦误删这些文件也就没了。

  • 如果在容器中开发项目,记得每天提交代码到远程仓库,避免容器误删后代码丢失。

作者:Rise_
链接:https://juejin.cn/post/7017129520649994253

收起阅读 »

【手写代码】面试官:请你手写防抖和节流

一、前言当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节...
继续阅读 »

一、前言

当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率,同时又不影响实际效果。

二、防抖

假设你用手压住一个弹簧,那么弹簧不会弹起来,除非你松手。

函数防抖,就是指触发事件后,函数在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。

简单的说,当一个函数连续触发,只执行最后一次。

函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  1. 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  2. 用户名、手机号、邮箱输入验证;
  3. 浏览器窗口大小改变后,只需窗口调整完后,再执行resize事件中的代码,防止重复渲染。

代码实现

在下面这段代码中,我们实现了最简单的一个防抖函数,我们设置一个定时器,你重复调用一次函数,我们就清除定时器,重新定时,直到在设定的时间段内没有重复调用函数。

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
fn();
}, delay)
}
}

代码优化

仔细一想,上面的代码是不是有什么问题?

问题一: 我们返回的fn函数,如果需要事件参数e怎么办?事件参数被debounce函数保存着,如果不把事件参数给闭包函数,若fn函数需要e我们没给,代码毫无疑问会报错。

问题二: 我们怎么确保调用fn函数的对象是我们想要的对象?你发现了吗,在上面这段代码中fn()函数的调用者是fn所定义的环境,这里涉及this指向问题,想要了解为什么可以去了解下js中的this。

为了解决上述两个问题,我们对代码优化如下

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 保存事件参数,防止fn函数需要事件参数里的数据
let arg = arguments;
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
// 若不改变this指向,则会指向fn定义环境
fn.apply(this, arg);
}, delay)
}
}

三、节流

当水龙头的水一直往下流,这十分的浪费水,所以我们可以把龙头关小一点,让水一滴一滴往下流,每隔一段时间掉下来一滴水。

节流就是限制一个函数在一段时间内只能执行一次,过了这段时间,在下一段时间又可以执行一次。应用场景如:

  1. 输入框的联想,可以限定用户在输入时,只在每两秒钟响应一次联想。
  2. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  3. 表单验证
  4. 按钮提交事件。

代码实现1(时间戳版)

// 方法一:时间戳
function throttle(fn, delay = 1000) {
// 记录第一次的调用时间
var prev = null;
console.log(prev);
// 返回闭包函数
return function () {
// 保存事件参数
var args = arguments;
// 记录现在调用的时间
var now = Date.now();
// console.log(now);
// 如果间隔时间大于等于设置的节流时间
if (now - prev >= delay) {
// 执行函数
fn.apply(this, args);
// 将现在的时间设置为上一次执行时间
prev = now;
}
}
}

触发事件时立即执行,以后每过delay秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行

代码实现2(定时器版)

// 方法二:定时器
function throttle(fn, delay) {
// 重置定时器
let timer = null;
// 返回闭包函数
return function () {
// 记录事件参数
let args = arguments;
// 如果定时器为空
if (!timer) {
// 开启定时器
timer = setTimeout(() => {
// 执行函数
fn.apply(this, args);
// 函数执行完毕后重置定时器
timer = null;
}, delay);
}
}
}

第一次触发时不会执行,而是在delay秒之后才执行,当最后一次停止触发后,还会再执行一次函数。

代码实现3(时间戳 & 定时器)

// 方法三:时间戳 & 定时器
function throttle(fn, delay) {
// 初始化定时器
let timer = null;
// 上一次调用时间
let prev = null;
// 返回闭包函数
return function () {
// 现在触发事件时间
let now = Date.now();
// 触发间隔是否大于delay
let remaining = delay - (now - prev);
// 保存事件参数
const args = arguments;
// 清除定时器
clearTimeout(timer);
// 如果间隔时间满足delay
if (remaining <= 0) {
// 调用fn,并且将现在的时间设置为上一次执行时间
fn.apply(this, args);
prev = Date.now();
} else {
// 否则,过了剩余时间执行最后一次fn
timer = setTimeout(() => {
fn.apply(this, args)
}, delay);
}
}
}


原文链接:https://juejin.cn/post/7040633388625035272


收起阅读 »

vue工程师必须学会封装的埋点指令思路

vue
前言 最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢? 稍加思考... 决定封装个埋点指令,这样使用起来...
继续阅读 »

前言


最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢?


稍加思考...



决定封装个埋点指令,这样使用起来会比较方便,因为指令的颗粒度比较细能够直击要害,挺适合上面所说的业务场景。


指令基础知识


在此之前,先来复习下vue自定义指令吧,这里只介绍常用的基础知识。更全的介绍可以查看官方文档


钩子函数




  • bind:只调用一次,指令第一次绑定到元素时调用。




  • inserted:被绑定元素插入父节点时调用。




  • update:所在组件的 VNode 更新时调用。




钩子函数参数



  • el:指令所绑定的DOM元素。

  • binding:一个对象,包含以下 property:

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。



  • vnode:指令所绑定的当前组件vnode。


在这里分享个小技巧,钩子函数参数中没有可以直接获取当前实例的参数,但可以通过 vnode.context 获取到,这个在我之前的vue技巧文章中也有分享到,有兴趣可以去看看。


正文


进入正题,下面会介绍埋点指令的使用,内部是怎么实现的。


用法与思路


一般我在封装一个东西时,会先确定好它该怎么去用,然后再从用法入手去封装。这样会令整个思路更加清晰,在定义用法时也可以思考下易用性,不至于封装完之后因为用法不理想而返工。


埋点上报的数据会分为公共数据(每个埋点都要上报的数据)和自定义数据(可选的额外数据,和公共数据一起上报)。那么公共数据在内部就进行统一处理,对于自定义数据则需要从外部传入。于是有了以下两种用法:



  • 一般用法


<div v-track:clickBtn></div>


  • 自定义数据


<div v-track:clickBtn="{other:'xxx'}"></div>

可以看到埋点事件是通过 arg 的形式传入,在此之前也看到有些小伙伴封装的埋点事件是在 value 传入。但我个人比较喜欢 arg 的形式,这种更能让人一目了然对应的埋点事件是什么。


另外上报数据结构大致为:


{   
eventName: 'clickBtn'
userId: 1,
userName: 'xxx',
data: {
other: 'xxx'
}
}

eventName 是埋点对应的事件名,与之同级的是公共数据,而自定义数据放在 data 内。


实现


定义一个 track.js 的文件


import SlsWebLogger from 'js-sls-logger'

function getSlsWebLoggerInstance (options = {}) {
return new SlsWebLogger({
host: '***',
project: '***',
logstore: `***`,
time: 10,
count: 10,
...options
})
}

export default {
install (Vue, {baseData = {}, slsOptions = {}) {
const slsWebLogger = getSlsWebLoggerInstance(slsOptions)
// 获取公共数据的方法
let getBaseTrackData = typeof baseData === 'function' ? baseData : () => baseData
let baseTrackData = null
const Track = {
name: 'track',
inserted (el, binding) {
el.addEventListener('click', () => {
if (!binding.arg) {
console.error('Track slsWebLogger 事件名无效')
return
}
if (!baseTrackData) {
baseTrackData = getBaseTrackData()
}
baseTrackData.eventName = binding.arg
// 自定义数据
let trackData = binding.value || {}
const submitData = Object.assign({}, baseTrackData, {data: trackData})
// 上报
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
})
}
}
Vue.directive(Track.name, Track)
}
}

封装比较简单,主要做了两件事,首先是为绑定指令的 DOM 添加 click 事件,其次处理上报数据。在封装埋点指令时,公共数据通过baseData传入,这样可以增加通用性,第二个参数是上报平台的一些配置参数。


在初始化时注册指令:


import store from 'src/store'
import track from 'Lib/directive/track'

function getBaseTrackData () {
let userInfo = store.state.User.user_info
// 公共数据
const baseTrackData = {
userId: userInfo.user_id, // 用户id
userName: userInfo.user_name // 用户名
}
return baseTrackData
}

Vue.use(track, {baseData: getBaseTrackData})

Vue.use 时会自动寻找 install 函数进行调用,最终在全局注册指令。


加点通用性


除了点击埋点之外,如果有停留埋点等场景,上面的指令就不适用了。为此,可以增加手动调用的形式。


export default {
install (Vue, {baseData = {}, slsOptions = {}) {
// ...
Vue.directive(Track.name, Track)
// 手动调用
Vue.prototype.slsWebLogger = {
send (trackData) {
if (!trackData.eventName) {
console.error('Track slsWebLogger 事件名无效')
return
}
const submitData = Object.assign({}, getBaseTrackData(), trackData)
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
}
}
}

这种挂载到原型的方式可以在每个组件实例上通过 this 方便进行调用。


export default {
// ...
created () {
this.slsWebLogger.send({
//...
})
}
}

总结


本文分享了封装埋点指令的过程,封装并不难实现。主要有两种形式,点击埋点通过绑定 DOM click 事件监听点击上报,而其他场景下提供手动调用的方式。主要是想记录下封装的思路,以及使用方式。埋点实现也是根据业务做了一些调整,比如注册埋点指令可以接受上报平台的配置参数。毕竟人是活的,代码是死的。只要能满足业务需求并且能维护,怎么使用舒服怎么来嘛。


作者:出来吧皮卡丘
链接:https://juejin.cn/post/7040649951923142687

收起阅读 »

jsonp的原理是什么?它是怎么实现跨域的?

写在前面一说到javascript的跨域,很多人第一时间想到的就是jsonp(JSON with Padding),那么这种跨域方式的实现原理是什么? 我承认我使用了很长时间,但是还是现在才知道,原来是这样....问题,如果我在 本地 访问 api.com下面...
继续阅读 »



写在前面

一说到javascript的跨域,很多人第一时间想到的就是jsonp(JSON with Padding),那么这种跨域方式的实现原理是什么? 我承认我使用了很长时间,但是还是现在才知道,原来是这样....

问题,如果我在 本地 访问 api.com下面的接口,会出现跨域请求的问题,为什么jsonp能解决这个?

  • 1、script标签是用来加载什么的?

加载js脚本的,src写上一个脚本的地址,然后浏览器就能加载啊!

  • 2、那么本地jsonp.html的script标签可以加载api.com的域名下面的脚本文件吗?

可以啊!要不那些用CDN方式优化网页加载速度的,是不可能成功的如

<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
复制代码
  • 3、那么script能加载别的域名下面的脚本文件,与jsonp何干?

我们都知道,加载api.com的域名下面的js脚本是可以的,此时,api.com下面的js脚本文件为真实存在的静态资源。那么如果这个脚本文件是由后端语言生成的呢?实例使用 php ==>jsonp.php

<?php
echo 'alert("Hello world")';
?>
  • 4、那么问题来了,我们生成js脚本的文件为.php文件啊,怎么加载这个脚本?

答案是:我们的 script标签是能够加载.php文件的,也就是

<script type="text/javascript" src='http://localhost/jsonp.php'></script>

运行结果

以上证明,我们完全可以在服务器端生成一段脚本,然后html页面用script标签去加载然后执行脚本。

那么,我们可以在生成的脚本中执行html中定义的方法吗?我们来试一下

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>jsonp</title>
</head>
<body>
</body>
<script type="text/javascript">
 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,传递过来的参数是==>'+para);
   console.log(para)
}
</script>
<script type="text/javascript" src='http://localhost/jsonp.php'></script>
</html>

jsonp.php

<?php
echo "execWithJsonp({status:'ok'})";
?>

运行结果

是的,我们发现完全没问题,我们平常调用接口就是要的后端返回的数据,上面的例子,后端生成脚本时已然给我们传递了参数,拿到数据之后,我们可以做任何我们想做的事。

问题:如果后端接口这么写,那么前端所有调用这个接口的地方,岂不是都要定义一个 execWithJsonp方法?

如果页面调用两次,处理逻辑还不一样,那么我们岂不是要区分是哪一次?我希望每次访问接口调用不同的处理数据函数,每次我来告诉后端用哪个函数来处理返回的数据。 当然可以,我们可以这么做

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>jsonp</title>
</head>
<body>
</body>
<script type="text/javascript">
 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是execWithJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
 function doExecJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是doExecJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
</script>
<script type="text/javascript" src='http://localhost/jsonp.php?callback=doExecJsonp'></script>
<script type="text/javascript" src='http://localhost/jsonp.php?callback=execWithJsonp'></script>
</html>

jsonp.php

<?php
 $callback=$_GET['callback'];
 echo $callback."({status:'ok'})";
?>

运行结果

说到这儿,我好像还是没说原理是啥,其实你看完上面的也就理解了

jsonp实际上就是

  • 1、前端调用后端时传递给后端数据的处理函数callback

  • 2、后端收到处理函数callback之后,进行数据库查询等操作,将后端要传递给前端的数据(一般为json格式)放入callback函数的()中并返回【实际上就是由后端动态生成一个前端可用的js脚本】,

  • 3、html页面在脚本文件加载后,自动执行脚本

  • 4、完成了整个jsonp请求。

优缺点

优点:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都 可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题,切很明显的需要后端工程师配合才能完成。

后记,发挥自己的想象吧,看这东西该怎么操作好

 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是execWithJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
 function doExecJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是doExecJsonp,传递过来的参数是==>'+para);
   console.log(para)
}

doJsonp('doExecJsonp')

function doJsonp(callbackName){
 var script=document.createElement('script');
 script.src='http://localhost/jsonp.php?callback='+callbackName;
 document.body.appendChild(script);
}


作者:小枫学幽默
来源:https://juejin.cn/post/7040730836156547086

收起阅读 »

Vite为什么快呢?快在哪?说一下我自己的理解吧

前言大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。由于这几个月使用了Vue3 + TS + Vite进行开发,并且是真的被Vite强力吸粉了!!!Vite最大的优点就是:快!!!非常快!!!说实话,使用Vite开发...
继续阅读 »



前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。

由于这几个月使用了Vue3 + TS + Vite进行开发,并且是真的被Vite强力吸粉了!!!Vite最大的优点就是:快!!!非常快!!!

说实话,使用Vite开发之后,我都有点不想回到以前Webpack的项目开发了,因为之前的项目启动项目需要30s以上,修改代码更新也需要2s以上,但是现在使用Vite,差不多启动项目只需要1s,而修改代码更新也是超级快!!!

那到底是为什么Vite可以做到这么快呢?官方给的解释,真的很官方。。所以今天我想用比较通俗易懂的话来讲讲,希望大家能看一遍就懂。

问题现状

ES模块化支持的问题

咱们都知道,以前的浏览器是不支持ES module的,比如:

// index.js

import { add } from './add.js'
import { sub } from './sub.js'
console.log(add(1, 2))
console.log(sub(1, 2))

// add.js
export const add = (a, b) => a + b

// sub.js
export const sub = (a, b) => a - b

你觉得这样的一段代码,放到浏览器能直接运行吗?答案是不行的哦。那怎么解决呢?这时候打包工具出场了,他将index.js、add.js、sub.js这三个文件打包在一个bundle.js文件里,然后在项目index.html中直接引入bundle.js,从而达到代码效果。一些打包工具,都是这么做的,例如webpack、Rollup、Parcel

项目启动与代码更新的问题

这个不用说,大家都懂:

  • 项目启动:随着项目越来越大,启动个项目可能要几分钟

  • 代码更新:随着项目越来越大,修改一小段代码,保存后都要等几秒才更新

解决问题

解决启动项目缓慢

Vite在打包的时候,将模块分成两个区域依赖源码

  • 依赖:一般是那种在开发中不会改变的JavaScript,比如组件库,或者一些较大的依赖(可能有上百个模块的库),这一部分使用esbuild来进行预构建依赖,esbuild使用的是 Go 进行编写,比 JavaScript 编写的打包器预构建依赖快 10-100倍

  • 源码:一般是哪种好修改几率比较大的文件,例如JSX、CSS、vue这些需要转换且时常会被修改编辑的文件。同时,这些文件并不是一股脑全部加载,而是可以按需加载(例如路由懒加载)。Vite会将文件转换后,以es module的方式直接交给浏览器,因为现在的浏览器大多数都直接支持es module,这使性能提高了很多,为什么呢?咱们看下面两张图:

第一张图,是以前的打包模式,就像之前举的index.js、add.js、sub.js的例子,项目启动时,需要先将所有文件打包成一个文件bundle.js,然后在html引入,这个多文件 -> bundle.js的过程是非常耗时间的。

第二张图,是Vite的打包方式,刚刚说了,Vite是直接把转换后的es module的JavaScript代码,扔给支持es module的浏览器,让浏览器自己去加载依赖,也就是把压力丢给了浏览器,从而达到了项目启动速度快的效果。

解决更新缓慢

刚刚说了,项目启动时,将模块分成依赖源码,当你更新代码时,依赖就不需要重新加载,只需要精准地找到是哪个源码的文件更新了,更新相对应的文件就行了。这样做使得更新速度非常快。

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

生产环境

刚刚咱们说的都是开发环境,也说了,Vite在是直接把转化后的es module的JavaScript,扔给浏览器,让浏览器根据依赖关系,自己去加载依赖。

那有人就会说了,那放到生产环境时,是不是可以不打包,直接在开个Vite服务就行,反正浏览器会自己去根据依赖关系去自己加载依赖。答案是不行的,为啥呢:

  • 1、你代码是放在服务器的,过多的浏览器加载依赖肯定会引起更多的网络请求

  • 2、为了在生产环境中获得最佳的加载性能,最好还是将代码进行tree-shaking、懒加载和 chunk 分割、CSS处理,这些优化操作,目前esbuild还不怎么完善

所以Vite最后的打包是使用了Rollup


作者:Sunshine_Lin
来源:https://juejin.cn/post/7040750959764439048

收起阅读 »

优秀的react框架的开源ui库 -- Pile.js

Pile.js是滴滴出行企业级前端组开发的一套基于 react 的移动端组件库,Pile.js组件库在滴滴企业级产品中极大提高了开发效率,也希望我们的产出能给广大前端开发者带来便捷。特性质量可靠 由滴滴企业级业务精简提炼而来,经历了一年多的考验,提供质量保障标...
继续阅读 »

Pile.js是滴滴出行企业级前端组开发的一套基于 react 的移动端组件库,Pile.js组件库在滴滴企业级产品中极大提高了开发效率,也希望我们的产出能给广大前端开发者带来便捷。

特性

质量可靠
由滴滴企业级业务精简提炼而来,经历了一年多的考验,提供质量保障

标准规范
代码规范严格按照eslint Airbnb编码规范,增加代码的可读性

优势

相对于同类型的移动端组件库,Pile.js有哪些优势?

组件数量多、体积小
Pile.js组件库包含52个组件,体积只有236k(未压缩),并且我们支持单个组件引用,除了常用的基础组件(比如:Button、Alert、Toast、Tip、Content等)外,我们还包含更为丰富的日期、时间、城市、车型组件,包括雷达图、环形加载、刻度尺组件等以及canvas动画图表等

样式定制
Pile.js设计规范上支持一定程度的样式定制,以满足业务和品牌上多样化的视觉需求

多语言
组件内文案提供统一的国际化支持,配置LocaleProvider组件,运用React的context特性,只需在应用外围包裹一次即可全局生效。

啰嗦一句,如果你有兴趣,不妨也参与到这个项目中来。

项目地址:https://github.com/didi/pile.js

Pile Issues:https://github.com/didi/pile.js/issues

文档: https://didi.github.io/pile.js/docs/

demo: https://didi.github.io/pile.js/demo/#/?_k=klfvmd

组件分类

作者:闫森
来源: https://www.cnblogs.com/yansen/p/9083173.html


收起阅读 »

又到年会抽奖的时候,这是你要的抽奖程序

原标题:公司年会用了我的抽奖程序,然后我中奖了…… 这是我去年写的代码和文章,眼看又到年底抽奖季了,翻出来洗洗还能再用背景临近年末,又到了各大公司举办年会的时候了。对于年会,大家最关心的应该就是抽奖了吧?虽然中奖概率通常不高,但总归是个机会,期待一下也是好...
继续阅读 »

原标题:公司年会用了我的抽奖程序,然后我中奖了……

这是我去年写的代码和文章,眼看又到年底抽奖季了,翻出来洗洗还能再用

背景

临近年末,又到了各大公司举办年会的时候了。对于年会,大家最关心的应该就是抽奖了吧?虽然中奖概率通常不高,但总归是个机会,期待一下也是好的。

最近,我们部门举办了年会,也有抽奖环节。临近年会的前几天,Boss 突然找到我,说要做一个抽奖程序,部门年会要用。我当时都懵了:就三天时间,万一做的程序有bug,岂不是要被现场百十号人的唾沫给淹死?没办法,Boss 看起来对我很有信心,我也只能硬着头皮上了。

需求

  1. 要一个设置页面,包括设置奖项、参与人员名单等。

  2. 如果单个奖项中奖人数过多,可分批抽取,每批人数可设置。

  3. 默认按奖项顺序抽奖,也可选定某个奖项开始。

  4. 可删除没到场的中奖者,同时可再次抽取以作替补。

  5. 可在任意奖项之间切换,可查中奖记录名单

  6. 支持撤销当前轮次的抽奖结果,重新抽取。

实现

身为Web前端开发,自然想到用Web技术来实现。本着不重复造轮子的原则,首先求助Google,Github。搜了一圈好像没有找到特别好用的程序能直接用的。后来看到一个Github上的一个项目,用 TagCanvas 做的抽奖程序,界面挺好,就是逻辑有问题,点几次就崩溃了。代码是不能拿来用了,标签云这种抽奖形式倒是可以借鉴。于是找来文档看了下基本用法,很快就集成到页面里了。

由于设置页面涉及多种交互,纯手写太费时间了,直接用框架。平时 Element UI 用得比较多,自然就用它了。考虑到年会现场可能没有网络,就把框架相关的JS和CSS都下载到本地,直接引用。为了快速开发,也没搭建webpack构建工具了,直接在浏览器里引入JS。

    <link rel="stylesheet" href="css/reset.css" />
  <link
    rel="stylesheet"
    href="js/element-ui@2.4.11/lib/theme-chalk/index.css"
  />
  <script src="js/polyfill.min.js"></script>
  <script src="js/vue.min.js"></script>
  <script src="js/element-ui@2.4.11/lib/index.js"></script>
  <script src="js/member.js"></script>
1.先设计数据结构。 奖项列表 awards
[{
  "name": "二等奖",
  "count": 25,
  "award": "办公室一日游"
}, {
  "name": "一等奖",
  "count": 10,
  "award": "BMW X5"
}, {
  "name": "特等奖",
  "count": 1,
  "award": "深圳湾一号"
}]
2.参与人列表 members
[{
"id": 1,
"name": "张三"
}, {
"id": 2,
"name": "李四"
}]
3.待抽奖人员列表players,是members 的子集
[{
"id": 1,
"name": "张三"
}]
4.抽奖结果列表result,按奖项顺序索引
[[{
  "id": 1,
  "name": "张三"
}], [{
  "id": 2,
  "name": "李四"
}]]
5.设置页面 包括奖项设置和参与人员列表。

6.抽奖页面

具体代码可以去我的Github项目 查看,方便的话可以点个 star。也可以现在体验一下。由于时间仓促,代码写得比较将就。

年会当天抽中了四等奖:1000元购物卡。我是不是该庆幸自己没中特等奖……

作者:KaysonLi
来源:https://juejin.cn/post/6844904033652572174



收起阅读 »

Hi~ 这将是一个通用的新手引导解决方案

本组件已开源,源码可见:github.com/bytedance/g…组件背景不管是老用户还是新用户,在产品发布新版本、有新功能上线、或是现有功能更新的场景下,都需要一定的指导。功能引导组件就是互联网产品中的指示牌,它旨在带领用户参观产品,帮助用户熟悉新的界面...
继续阅读 »



本组件已开源,源码可见:github.com/bytedance/g…

组件背景

不管是老用户还是新用户,在产品发布新版本、有新功能上线、或是现有功能更新的场景下,都需要一定的指导。功能引导组件就是互联网产品中的指示牌,它旨在带领用户参观产品,帮助用户熟悉新的界面、交互与功能。与 FAQs、产品介绍视频、使用手册、以及 UI 组件帮助信息不同的是,功能引导组件与产品 UI 融合为一体,不会给用户割裂的交互感受,并且不需要用户主动进行触发操作,就会展示在用户眼前。

图片比文字更加具象,以下是两种典型的新手引导组件,你是不是一看就明白功能引导组件是什么了呢?

img

img

功能简介

分步引导

Guide 组件以分步引导为核心,像指路牌一样,一节一节地引导用户从起点到终点。这种引导适用于交互流程较长的新功能,或是界面比较复杂的产品。它带领用户体验了完整的操作链路,并快速地了解各个功能点的位置。

img

img

呈现方式

蒙层模式

顾名思义,蒙层引导是指在产品上用一个半透明的黑色进行遮罩,蒙层上方对界面进行高亮,旁边配以弹窗进行讲解。这种引导方式阻断了用户与界面的交互,让用户的注意力聚焦在所圈注的功能点上,不被其他元素所干扰。

img

弹窗模式

很多场景下,为了不干扰用户,我们并不想使用蒙层。这时,我们可以使用无蒙层模式,即在功能点旁边弹出一个简单的窗口引导。

img

精准定位

初始定位

Guide 提供了 12 种对齐方式,将弹窗引导加载到所选择的元素上。同时,还允许自定义横纵向偏差值,对弹窗的位置进行调整。下图分别展示了定位为 top-left 和 right-bottom 的弹窗:

img

img

并且当用户缩放或者滚动页面时,弹窗的定位依然是准确的。

自动滚动

在很多情境中,我们都需要对距离较远的几个页面元素进行功能说明,串联成一个完整的引导路径。当下一步要圈注的功能点不在用户视野中时,Guide 会自动滚动页面至合适的位置,并弹出引导窗口。

1.gif

键盘操作

当 Guide 引导组件弹出时,我们希望用户的注意力被完全吸引过来。为了让使用辅助阅读器的用户也能够感知到 Guide 的出现,我们将页面焦点移动到弹窗上,并且让弹窗里的每一个可读元素都能够聚焦。同时,用户可以用键盘(tab 或 tab+shift)依次聚焦弹窗里的内容,也可以按 escape 键退出引导。

下图中,用户用 tab 键在弹窗中移动焦点,被聚焦的元素用虚线框标识出来。当聚焦到“下一步”按钮时,敲击 shift 键,便可跳至下一步引导。

2.gif

技术实现

总体流程

在展示组件的步骤前我们会先判断是否过期,判断是否过期的标准有两个:一个是该引导组件在localStorage中存储唯一 key 是否为 true,为 true 则为该组件步骤执行完毕。第二个是组件接收一个props.expireDate,如果当前时间大于expireDate则代表组件已经过期则不会继续展示。

img

当组件没有过期时,会展示传入的props.steps相应的内容,steps 结构如下:

interface Step {
   selector: string;
   title: string;
   content: React.Element | string;
   placement: 'top' | 'bottom' | 'left' | 'right'
       | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right',
   offset: Record<'top' | 'bottom' | 'left' | 'right', number>
}

const steps = Step[]

根据 step.selector 获取高亮元素,再根据 step.placement 将弹窗展示到高亮元素相关的具体位置。点击下一步会按序展示下个 step,当所有步骤展示完毕之后我们会将该引导组件在 localStorage 中存储唯一 key 置为 true,下次进来将不再展示。

下面来看看引导组件的具体细节实现吧。

蒙层模式

当前的引导组件支持有无蒙层两种模式,有蒙层的展示效果如下图所示。

img

蒙层很好实现,就是一个撑满屏幕的 div,但是我们怎么才能让它做到高亮出中间的 selector 元素并且还支持圆角呢?🤔 ,真相只有一个,那就是—— border-width

img

我们拿到了 selector 元素的offsetTop, offsetRight, offsetBottom, offsetLeft,并相应地设置为高亮框的border-width,再把border-color设置为灰色,一个带有高亮框的蒙层就实现啦!在给这个高亮框 div 加个pseudo-element ::after 来赋予它 border-radius,完美!

弹窗的定位

用户使用 Guide 时,传入了步骤信息,每一步都包括了所要进行引导说明的界面元素的 CSS 选择器。我们将所要标注的元素叫做“锚元素”。Guide 需要根据锚元素的位置信息,准确地定位弹窗。

每一个 HTML 元素都有一个只读属性 offsetParent,它指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的 table,td,th,body元素。每个元素都是根据它的 offsetParent 元素进行定位的。比如说,一个 absolute 定位的元素,是根据它最近的、非 static 定位的上级元素进行偏移的,这个上级元素,就是其的 offsetParent。

所以我们想到将弹窗元素放进锚元素的 offsetParent 中,再对其位置进行调整。同时,为了不让锚元素 offsetParent 中的其它元素产生位移,我们设定弹窗元素为 absolute 绝对定位。

定位步骤

弹窗的定位计算流程大致如下:

img

步骤 1. 得到锚元素

通过传给 Guide 的步骤信息中的 selector,即 CSS selector,我们可以由下述代码拿到锚元素:

const anchor = document.querySelector(selector);

如何拿到 anchor 的 offsetParent 呢?这一步其实并没有想象中那么简单。下面我们就来详细地讲一讲这一步吧。

步骤 2. 获取 offsetParent

一般来说,拿到锚元素的 offsetParent,也只需要简单的一行代码:

const parent = anchor.offsetParent;

但是这行代码并不能涵盖所有的场景,我们需要考虑一些特殊的情况。

场景一: 锚元素为 fixed 定位

并不是所有的 HTMLElement 都有 offsetParent 属性。当锚元素为 fixed 定位时,其 offsetParent 返回 null。这时,我们就需要使用其 包含块(containing block) 代替 offsetParent 了。

包含块是什么呢?大多数情况下,包含块就是这个元素最近的祖先块元素的内容区,但也不是总是这样。一个元素的包含块是由其 position 属性决定的。

  • 如果 position 属性是 fixed,包含块通常是 document.documentElement

  • 如果 position 属性是 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

    • transformperspective的值不是none

    • will-change 的值是 transformperspective

    • filter 的值不是 nonewill-change 的值是 filter(只在 Firefox 下生效).

    • contain 的值是 paint (例如: contain: paint;)

因此,我们可以从锚元素开始,递归地向上寻找符合上述条件的父级元素,如果找不到,那么就返回 document.documentElement

下面是 Guide 中用来寻找包含块的代码:

const getContainingBlock = node => {
 let currentNode = getDocument(node).documentElement;

 while (
   isHTMLElement(currentNode) &&
   !['html', 'body'].includes(getNodeName(currentNode))
) {
   const css = getComputedStyle(currentNode);

   if (
     css.transform !== 'none' ||
     css.perspective !== 'none' ||
    (css.willChange && css.willChange !== 'auto')
  ) {
     return currentNode;
  }
   currentNode = currentNode.parentNode;
}

 return currentNode;
};
场景二:在 iframe 中使用 Guide

在 Guide 的代码中,我们常常用到 window 对象。比如说,我们需要在 window 对象上调用 getComputedStyle()获取元素的样式,我们还需要 window 对象作为元素 offsetParent 的兜底。但是我们并不能直接使用 window 对象,为什么呢?这时,我们需要考虑 iframe 的情况。

想象一下,如果我们在一个内嵌了 iframe 的应用中使用 Guide 组件,Guide 组件代码在 iframe 外面,而被引导的功能点在 iframe 里面,那么在使用 Window 对象提供的方法是,我们一定是想在所圈注的功能点所在的 Window 对象上进行调用,而非当前代码运行的 Window。

因此,我们通过下面的 getWindow 方法,确保拿到的是参数 node 所在的 Window。

// Get the window object using this function rather then simply use `window` because
// there are cases where the window object we are seeking to reference is not in
// the same window scope as the code we are running. (https://stackoverflow.com/a/37638629)
const getWindow = node => {
 // if node is not the window object
 if (node.toString() !== '[object Window]') {
   // get the top-level document object of the node, or null if node is a document.
   const { ownerDocument } = node;
   // get the window object associated with the document, or null if none is available.
   return ownerDocument ? ownerDocument.defaultView || window : window;
}

 return node;
};

在 line 8,我们看到一个属性 ownerDocument。如果 node 是一个 DOM Element,那么它具有一个属性 ownerDocument,此属性返回的 document 对象是在实际的 HTML 文档中的所有子节点所属的主对象。如果在文档节点自身上使用此属性,则结果是 null。当 node 为 Window 对象时,我们返回 window;当 node 为 Document 对象时,我们返回了 ownerDocument.defaultView 。这样,getWindow 函数便涵盖了参数 node 的所有可能性。

步骤 3. 挂载弹窗

如下代码所示,我们常常遇到的使用场景是,在组件 A 中渲染 Guide,让其去标注的元素却在组件 B、组件 C 中。

 // 组件A
const A = props => (
   <>
       <Guide
           steps={[
              {
                   ......
                   selector: '#btn1'
              },
              {
                   ......
                   selector: '#btn2'
              },
              {
                   ......
                   selector: '#btn3'
              }
          ]}
       />
       <button id="btn1">Button 1</button>
   </>
)

// 组件B
const B = props => (<button id="btn2">Button 2</button>)

// 组件C
const C = props => (<button id="btn3">Button 3</button>)

上述代码中,Guide 会自然而然地渲染在 A 组件 DOM 结构下,我们怎样将其挂载到组件 B、C 的 offsetParent 中呢?这时候就要给大家介绍一下强大却少为人知的 React Portals 了。

React Portals

当我们需要把一个组件渲染到其父节点所在的 DOM 树结构之外时, 我们首先应该考虑使用 React Portals。Portals 最适用于这种需要将子节点从视觉上渲染到其父节点之外的场景了,在 Antd 的 Modal、Popover、Tooltip 组件实现中,我们也可以看到 Portal 的应用。

我们使用 ReactDOM.createPortal(child, container)创建一个 Portal。child 是我们要挂载的组件,container 则是 child 要挂载到的容器组件。

虽然 Portal 是渲染在其父元素 DOM 结构之外的,但是它并不会创建一个完全独立的 React DOM 树。一个 Portal 与 React 树中其它子节点相同,都可以拿到父组件的传来的 props 和 context,也都可以进行事件冒泡。

另外,与 ReactDOM.render 所创建的 React DOM 树不同,ReactDOM.createPortal 是应用在组件的 render 函数中的,因此不需要手动卸载。

在 Guide 中,每跳一步,上一步的弹窗便会卸载掉,新的弹窗会被加载到这一步要圈注的元素的 offsetParent 里。伪代码如下:

const Modal = props => (
ReactDOM.createPortal(
<div>
......
</div>,
offsetParent);
)

将弹窗渲染进 offsetParent 后,Guide 的下一步工作便是计算弹窗相对于 offsetParent 的偏移量。这一步非常复杂,并且要考虑一些特殊情况。下面就让我们就仔细地讲解这部分计算吧。

步骤 4. 偏移量计算

以一个 placement = left ,即需要在功能点左侧展示的弹窗引导为例。如果我们直接把弹窗通过 React Portal 挂载到锚元素的 offsetParent 中,并赋予其绝对定位,其位置会如下图所示——左上角与 offsetParent 的左上角对齐。

_下图中,用蓝色框表示的考拉图片是 Guide 需要标注的元素,即锚元素;红色框则标识出这个锚元素的 offsetParent 元素。

img

而我们预想的定位结果如下:

img

参考下图,将弹窗从初始位置移动至预期位置,我们需要在 y 轴上向下移动弹窗 offsetTop + h1/2 - h2/2 px。其中,h1 为锚元素的高度,h2 为弹窗的高度。

img

但是,上述计算依然忽略了一种场景,那就是当锚元素定位为 fixed 时。若锚元素定位为 fixed,那么无论锚元素所在的界面怎样滑动,锚元素相对于屏幕视口(viewport)的位置是固定的。自然,用来对 fixed 锚元素进行引导的弹窗也需要具有这些特性,即同样需要为 fixed 定位。

Arrow 实现及定位

arrowmodal 的子元素且相对于 modal 绝对定位,如下图所示有十二种展示位置,我们把十二种定位分为两类情况:

  1. 紫色的四种居中情况;

  2. 黄色的其余八种斜角。

img

对于第一类情况

箭头始终是相对弹窗边缘居中的位置,出对于 top、bottom,箭头的 right 值始终是(modal.width - arrow.diagonalWidth)/2 ,而 top 或 bottom 值始终为-arrow.diagonalWidth/2

对于 left、right,箭头的 top 值是(modal.height - arrow.diagonalWidth)/2 ,而 left 或 right 为-arrow.diagonalWidth/2

img

注:diagonalWidth为对角线宽度,getReversePosition\(placement\)为获取传入参数的 reverse 位置,top 对应 bottom,left 对应 right。

伪代码如下:

const placement = 'top' | 'bottom' | 'left' | 'right';
const diagonalWidth = 10;

const style = {
right: ['bottom', 'top'].includes(placement)
? (modal.width - diagonalWidth) / 2
: '',
top: ['left', 'right'].includes(placement)
? (modal.height - diagonalWidth) / 2
: '',
[getReversePosition(placement)]: -diagonalWidth / 2,
};

对于第二类情况

对于 A-B 的位置,通过下图可以发现,B 的位移总是固定值。比如对于 placement 值为 top-left 的弹窗,箭头 left 值总是固定的,而 bottom 值为-arrow.diagonalWidth/2

img

以下为伪代码:

const [firstPlacement, lastPlacement] = placement.split('-');
const diagonalWidth = 10;
const margin = 24;

const style = {
[lastPlacement]: margin,
[getReversePosition(placement)]: -diagonalWidth / 2,
}

Hotspot 实现及定位

引导组件支持 hotspot 功能,通过给一个 div 元素加上动画改变其 box-shadow 大小实现呼吸灯的效果,效果如下图所示,其中热点的定位是相对箭头的位置计算的,这里便不赘述了。

img

结语

在 Guide 的开发初期,我们并没有想到这样一个小组件需要考虑到以上这些技术点。可见,再小的组件,让其适用于所有场景,做到足够通用都是件难事,需要不断地尝试与反思。

作者:字节前端
来源:https://juejin.cn/post/6960493325061193735

收起阅读 »

领域驱动设计(DDD)能给前端带来什么

为什么需要 DDD在回答这个问题之前,我们先看下大部分软件都会经历的发展过程:频繁的变更带来软件质量的下降而这又是软件发展的规律导致的:软件是对真实世界的模拟,真实世界往往十分复杂人在认识真实世界的时候总有一个从简单到复杂的过程因此需求的变更是一种必然,并且总...
继续阅读 »



为什么需要 DDD

在回答这个问题之前,我们先看下大部分软件都会经历的发展过程:频繁的变更带来软件质量的下降

而这又是软件发展的规律导致的:

  • 软件是对真实世界的模拟,真实世界往往十分复杂

  • 人在认识真实世界的时候总有一个从简单到复杂的过程

  • 因此需求的变更是一种必然,并且总是由简单到复杂演变

  • 软件初期的业务逻辑非常清晰明了,慢慢变得越来越复杂

可以看到需求的不断变更和迭代导致了项目变得越来越复杂,那么问题来了,项目复杂性提高的根本原因是需求变更引起的吗?

根本原因其实是因为在需求变更过程中没有及时的进行解耦和扩展。

那么在需求变更的过程中如何进行解耦和扩展呢? DDD 发挥作用的时候来了。

什么是 DDD

DDD(领域驱动设计)的概念见维基百科:zh.wikipedia.org/wiki/\%E9\%…

可以看到领域驱动设计(domin-driven design)不同于传统的针对数据库表结构的设计,领域模型驱动设计自然是以提炼和转换业务需求中的领域知识为设计的起点。在提炼领域知识时,没有数据库的概念,亦没有服务的概念,一切围绕着业务需求而来,即:

  • 现实世界有什么事物 -> 模型中就有什么对象

  • 现实世界有什么行为 -> 模型中就有什么方法

  • 现实世界有什么关系 -> 模型中就有什么关联

在 DDD 中按照什么样的原则进行领域建模呢?

单一职责原则(Single responsibility principle)即 SRP:软件系统中每个元素只完成自己职责内的事,将其他的事交给别人去做。

上面这句话有没有什么哪里不清晰的?有,那就是“职责”两个字。职责该怎么理解?如何限定该元素的职责范围呢?这就引出了“限界上下文”的概念。

Eric Evans 用细胞来形容限界上下文,因为“细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。”这里,细胞代表上下文,而细胞膜代表了包裹上下文的边界。

我们需要根据业务相关性耦合的强弱程度分离的关注点对这些活动进行归类,找到不同类别之间存在的边界,这就是限界上下文的含义。上下文(Context)是业务目标,限界(Bounded)则是保护和隔离上下文的边界,避免业务目标的不单一而带来的混乱与概念的不一致。

如何 DDD

DDD 的大体流程如下:

  1. 建立统一语言

统一语言是提炼领域知识的产出物,获得统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程。

使用统一语言可以帮助我们将参与讨论的客户、领域专家与开发团队拉到同一个维度空间进行讨论,若没有达成这种一致性,那就是鸡同鸭讲,毫无沟通效率,相反还可能造成误解。因此,在沟通需求时,团队中的每个人都应使用统一语言进行交流。

一旦确定了统一语言,无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,清晰准确地定义领域知识。重要的是,当我们建立了符合整个团队皆认同的一套统一语言后,就可以在此基础上寻找正确的领域概念,为建立领域模型提供重要参考。

举个例子,不同玩家对于英雄联盟(league of legends)的称呼不尽相同;国外玩家一般叫“League”,国内玩家有的称呼“撸啊撸”,有的称呼“LOL”等等。那么如果要开发相关产品,开发人员和客户首先需要统一对“英雄联盟”的语言模型。

  1. 事件风暴(Event Storming)

事件风暴会议是一种基于工作坊的实践方法,它可以快速发现业务领域中正在发生的事件,指导领域建模及程序开发。 它是 Alberto Brandolini 发明的一 种领域驱动设计实践方法,被广泛应用于业务流程建模和需求工程,基本思想是将软件开发人员和领域专家聚集在一起,相互学习,类似头脑风暴。

会议一般以探讨领域事件开始,从前向后梳理,以确保所有的领域事件都能被覆盖。

什么是领域事件呢?

领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可能是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发发送缴费邮件通知操作;或者一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。

  1. 进行领域建模,将各个模型分配到各个限界上下文中,构建上下文地图。

领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。

上面我们大体了解了 DDD 的作用,概念和一般的流程,虽然前端和后端的 DDD 不尽相同,但是我们仍然可以将这种思想应用于我们的项目中。

DDD 能给前端项目带来什么

通过领域模型 (feature)组织项目结构,降低耦合度

很多通过 react 脚手架生成的项目组织结构是这样的:

-components
   component1
   component2
-actions.ts
...allActions
-reducers.ts
...allReducers

这种代码组织方式,比如 actions.ts 中的 actions 其实没有功能逻辑关系;当增加新的功能的时候,只是机械的往每个文件夹中加入对应的 component,action,reducer,而没有关心他们功能上的关系。那么这种项目的演进方向就是:

项目初期:规模小,模块关系清晰 ---> 迭代期:加入新的功能和其他元素 ---> 项目收尾:文件结构,模块依赖错综复杂。

因此我们可以通过领域模型的方式来组织代码,降低耦合度。

  1. 首先从功能角度对项目进行拆分。将业务逻辑拆分成高内聚松耦合的模块。从而对 feature 进行新增,重构,删除,重命名等变得简单 ,不会影响到其他的 feature,使项目可扩展和可维护。

  1. 再从技术角度进行拆分,可以看到 componet, routing,reducer 都来自等多个功能模块

可以看到:

  • 技术上的代码按照功能的方式组织在 feature 下面,而不是单纯通过技术角度进行区分。

  • 通常是由一个文件来管理所有的路由,随着项目的迭代,这个路由文件也会变得复杂。那么可以把路由分散在 feature 中,由每个 feature 来管理自己的路由。

通过 feature 来组织代码结构的好处是:当项目的功能越来越多时,整体复杂度不会指数级上升,而是始终保持在可控的范围之内,保持可扩展,可维护。

如何组织 componet,action,reducer

文件夹结构该如何设计?

  • 按 feature 组织组件,action 和 reducer

  • 组件和样式文件在同一级

  • Redux 放在单独的文件

  1. 每个 feature 下面分为 redux 文件夹 和 组件文件

  1. redux 文件夹下面的 action.js 只是充当 loader 的作用,负责将各个 action 引入,而没有具体的逻辑。 reducer 同理

  1. 项目的根节点还需要一个 root loader 来加载 feature 下的资源

如何组织 router

组织 router 的核心思想是把每个路由配置分发到每个 feature 自己的路由表中,那么需要:

  • 每个 feature 都有自己专属的路由配置

  • 顶层路由(页面级别的路由)通过 JSON 配置 1,然后解析 JSON 到 React Router

  1. 每个 feature 有自己的路由配置

  1. 顶层的 routerConfig 引入各个 feature 的子路由

import { App } from '../features/home';
import { PageNotFound } from '../features/common';
import homeRoute from '../features/home/route';
import commonRoute from '../features/common/route';
import examplesRoute from '../features/examples/route';

const childRoutes = [
 homeRoute,
 commonRoute,
 examplesRoute,
];

const routes = [{
   path: '/',
   componet: App,
   childRoutes: [
       ... childRoutes,
      { path:'*', name: 'Page not found', component: PageNotFound },
  ].filter( r => r.componet || (r.childRoutes && r.childRoutes.length > 0))
}]

export default routes
  1. 解析 JSON 路由到 React Router

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import routeConfig from './common/routeConfig';

function renderRouteConfig(routes, path) {
   const children = []        // children component list
     const renderRoute = (item, routeContextPath) => {
   let newContextPath;
   if (/^\//.test(item.path)) {
     newContextPath = item.path;
  } else {
     newContextPath = `${routeContextPath}/${item.path}`;
  }
   newContextPath = newContextPath.replace(/\/+/g, '/');
   if (item.component && item.childRoutes) {
     const childRoutes = renderRouteConfigV3(item.childRoutes, newContextPath);
     children.push(
       <Route
         key={newContextPath}
         render={props => <item.component {...props}>{childRoutes}</item.component>}
         path={newContextPath}
       />,
    );
  } else if (item.component) {
     children.push(
       <Route key={newContextPath} component={item.component} path={newContextPath} exact />,
    );
  } else if (item.childRoutes) {
     item.childRoutes.forEach(r => renderRoute(r, newContextPath));
  }
};
   routes.forEach(item => renderRoute(item,path))
   return <Switch>children</Switch>
}


function Root() {
 const children = renderRouteConfig(routeConfig, '/');
 return (
     <ConnectedRouter>{children}</ConnectedRouter>
);
}

reference

Rekit:帮助创建遵循一般的最佳实践,可拓展的 Web 应用程序 rekit.js.org/


作者:字节前端
来源:https://juejin.cn/post/7007995442864586766

收起阅读 »

面试官对不起!我终于会了Promise...(一面凉经泪目)

面试题CSS 实现水平垂直居中flex的属性CSS transition的实现效果和有哪些属性CSS 实现沿Y轴旋转360度 (直接自爆了 CSS不行....麻了)好,那来点JS 基本数据类型有哪些 用什么判断数组怎么判断引用类型和基本类型的区别什么是栈?什么...
继续阅读 »

面试题

  • CSS 实现水平垂直居中
  • flex的属性
  • CSS transition的实现效果和有哪些属性
  • CSS 实现沿Y轴旋转360度 (直接自爆了 CSS不行....麻了)
  • 好,那来点JS 基本数据类型有哪些 用什么判断
  • 数组怎么判断
  • 引用类型和基本类型的区别
  • 什么是栈?什么是堆?
  • 手写 翻转字符串
  • 手写 Sum(1,2,3)的累加(argument)(我以为是柯里化,面试官笑了一下,脑筋不要这么死嘛)
  • 箭头函数和普通函数的区别(上题忘记了argument,面试官特意问这个问题提醒我,奈何基础太差救不起来了...泪目)
  • 数组去重的方法
  • 图片懒加载
  • 跨域产生的原因,同源策略是什么
  • 说说你了解的解决办法(只说了JSONP和CORS)
  • Cookie、sessionStorage、localStorage的区别
  • get 和 post 的区别 (只说了传参方式和功能不同,面试官问还有吗 其他的不知道了...)
  • 问了一下项目,react
  • 对ES6的了解 (Promise果真逃不了....)
  • let var const的区别
  • 知道Promise嘛?聊聊对Promise的理解?(说了一下Promise对象代表一个异步操作,有三种状态,状态转变为单向...)
  • 那它是为了解决什么问题的?(emmm当异步返回值又需要等待另一个异步就会嵌套回调,Promise可以解决这个回调地狱问题)
  • 那它是如何解决回调地狱的?(Promise对象内部是同步的,内部得到内部值后进行调用.then的异步操作,可以一直.then .then ...)
  • 好,你说可以一直.then .then ...那它是如何实现一直.then 的?(emmm... 这个.then链式调用就是...额这个...)
  • Promise有哪些方法 all和race区别是什么
  • 具体说一下 .catch() 和 reject (...我人麻了...)


结束环节

  • 问了面试官对CSS的理解(必须但非重要,前端的核心还是尽量一比一还原设计稿,只有写好了页面才能考虑交互)

  • 如何学习(基础是最重要的,CSS和JS要注重实践,盖房子最重要的还是地基,所有的框架源码,组件等都基于CSS和JS)

  • 曾经是如何度过这个过程的(多做项目,在项目中学习理解每个细节,再次告诫我基础的重要性)



Promise概述


Promise是ES6新增的引用类型,可以通过new来进行实例化对象。Promise内部包含着异步的操作。



new Promise(fn)




Promise.resolve(fn)



这两种方式都会返回一个 Promise 对象。



  • Promise 有三种状态: 等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected),且Promise 必须为三种状态之一只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  • 状态只能由 Pending 变为 Fulfilled 或由 Pending 变为 Rejected ,且状态改变之后不会在发生变化,会一直保持这个状态。

  • Pending 变为 Fulfilled 会得到一个私有value,Pending 变为 Rejected会得到一个私有reason,当Promise达到了Fulfilled或Rejected时,执行的异步代码会接收到这个value或reason。


知道了这些,我们可以得到下面的代码:


实现原理


class Promise {
constructor() {
this.state = 'pending' // 初始化 未完成状态
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;
}
}

基本用法


Promise状态只能在内部进行操作,内部操作在Promise执行器函数执行。Promise必须接受一个函数作为参数,我们称该函数为执行器函数,执行器函数又包含resolve和reject两个参数,它们是两个函数。



  • resolve : 将Promise对象的状态从 Pending(进行中) 变为 Fulfilled(已成功)

  • reject : 将Promise对象的状态从 Pending(进行中) 变为 Rejected(已失败),并抛出错误。


使用栗子


let p1 = new Promise((resolve,reject) => {
resolve(value);
})
setTimeout(() => {
console.log((p1)); // Promise {<fulfilled>: undefined}
},1)

let p2 = new Promise((resolve,reject) => {
reject(reason);
})
setTimeout(() => {
console.log((p2)); // Promise {<rejected>: undefined}
},1)

实现原理

  • p1 resolve为成功,接收参数value,状态改变为fulfilled,不可再次改变。
  • p2 reject为失败,接收参数reason,状态改变为rejected,不可再次改变。
  • 如果executor执行器函数执行报错,直接执行reject。


所以得到如下代码:


class Promise{
constructor(executor){
// 初始化state为等待态
this.state = 'pending';
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;
let resolve = value => {
console.log(value);
if (this.state === 'pending') {
// resolve调用后,state转化为成功态
console.log('fulfilled 状态被执行');
this.state = 'fulfilled';
// 储存成功的值
this.value = value;
}
};
let reject = reason => {
console.log(reason);
if (this.state === 'pending') {
// reject调用后,state转化为失败态
console.log('rejected 状态被执行');
this.state = 'rejected';
// 储存失败的原因
this.reason = reason;
}
};
// 如果 执行器函数 执行报错,直接执行reject
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
}

检验一下上述代码咯:


class Promise{...} // 上述代码

new Promise((resolve, reject) => {
console.log(0);
setTimeout(() => {
resolve(10) // 1
// reject('JS我不爱你了') // 2
// 可能有错误
// throw new Error('是你的错') // 3
}, 1000)
})

  • 当执行代码1时输出为 0 后一秒输出 10 和 fulfilled 状态被执行
  • 当执行代码2时输出为 0 后一秒输出 我不爱你了 和 rejected 状态被执行
  • 当执行代码3时 抛出错误 是你的错

.then方法



promise.then(onFulfilled, onRejected)

  • 初始化Promise时,执行器函数已经改变了Promise的状态。且执行器函数是同步执行的。异步操作返回的数据(成功的值和失败的原因)可以交给.then处理,为Promise实例提供处理程序。
  • Promise实例生成以后,可以用then方法分别指定resolved状态rejected状态的回调函数。这两个函数onFulfilled,onRejected都是可选的,不一定要提供。如果提供,则会Promise分别进入resolved状态rejected状态时执行。
  • 而且任何传给then方法的非函数类型参数都会被静默忽略。
  • then 方法必须返回一个新的 promise 对象(实现链式调用的关键)


实现原理

  • Promise只能转换最终状态一次,所以onFulfilledonRejected两个参数的操作是互斥
  • 当状态state为fulfilled,则执行onFulfilled,传入this.value。当状态state为rejected,则执行onRejected,传入this.reason

class Promise {
constructor(executor) {
this.state = 'pending' // 初始化 未完成状态
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;

// .then 立即执行后 state为pengding 把.then保存起来
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];

// 把异步任务 把结果交给 resolve
let resolve = (value) => {
if (this.state === 'pending') {
console.log('fulfilled 状态被执行');
this.value = value
this.state = 'fulfilled'
// onFulfilled 要执行一次
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.state === 'pending') {
console.log('rejected 状态被执行');
this.reason = reason
this.state = 'rejected'
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject)
}
catch (e) {
reject(err)
}
}
// 一个promise解决了后(完成状态转移,把控制权交出来)
then(onFulfilled, onRejected) {
if (this.state == 'pending') {
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason)
})
}
console.log('then');
// 状态为fulfilled 执行成功 传入成功后的回调 把执行权转移
if (this.state == 'fulfiiied') {
onFulfilled(this.value);
}
// 状态为rejected 执行失败 传入失败后的回调 把执行权转移
if (this.state == 'rejected') {
onRejected(this.reason)
}
}
}
let p1 = new Promise((resolve, reject) => {
console.log(0);
setTimeout(() => {
// resolve(10)
reject('JS我不爱你了')
console.log('setTimeout');
}, 1000)
}).then(null,(data) => {
console.log(data, '++++++++++');
})

0
then
rejected 状态被执行
JS我不爱你了 ++++++++++
setTimeout


当resolve在setTomeout内执行,then时state还是pending等待状态 我们就需要在then调用的时候,将成功和失败存到各自的数组,一旦reject或者resolve,就调用它们。



现可以异步实现了,但是还是不能链式调用啊?
为保证 then 函数链式调用,then 需要返回 promise 实例,再把这个promise返回的值传入下一个then中。


链式调用及后续实现源码


这部分我也不会,还没看懂。后续再更。
先贴代码:


class Promise{
constructor(executor){
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
};
let reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
};
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled,onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
let promise2 = new Promise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'rejected') {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'pending') {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0)
});
};
});
return promise2;
}
catch(fn){
return this.then(null,fn);
}
}
function resolvePromise(promise2, x, resolve, reject){
if(x === promise2){
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called;
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if(called)return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if(called)return;
called = true;
reject(err);
})
} else {
resolve(x);
}
} catch (e) {
if(called)return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
//resolve方法
Promise.resolve = function(val){
return new Promise((resolve,reject)=>{
resolve(val)
});
}
//reject方法
Promise.reject = function(val){
return new Promise((resolve,reject)=>{
reject(val)
});
}
//race方法
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(resolve,reject)
};
})
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
let arr = [];
let i = 0;
function processData(index,data){
arr[index] = data;
i++;
if(i == promises.length){
resolve(arr);
};
};
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(data=>{
processData(i,data);
},reject);
};
});
}

Promise的各种方法


Promise.prototype.catch()


catch 异常处理函数,处理前面回调中可能抛出的异常。只接收一个参数onRejected处理程序。它相当于调用Promise.prototype.then(null,onRejected),所以它也会返回一个新的Promise



  • 栗子


let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(10)
}, 1000)
}).then(() => {
throw Error("1123")
}).catch((err) => {
console.log(err);
})
.then(() => {
console.log('异常捕获后可以继续.then');
})
复制代码

当第一个.then的异常被捕获后可以继续执行。


Promise.all()


Promise.all()创建的Promise会在这一组Promise全部解决后在解决。也就是说会等待所有的promise程序都返回结果之后执行后续的程序。返回一个新的Promise。



  • 栗子


let p1 = new Promise((resolve, reject) => {  
resolve('success1')
})

let p2 = new Promise((resolve, reject) => {
resolve('success1')
})
// let p3 = Promise.reject('failed3')
Promise.all([p1, p2]).then((result) => {
console.log(result) // ['success1', 'success2']

}).catch((error) => {
console.log(error)
})
// Promise.all([p1,p3,p2]).then((result) => {
// console.log(result)
// }).catch((error) => {
// console.log(error) // 'failed3'
//
// })
复制代码

有上述栗子得到,all的性质:



  • 如果所有都成功,则合成Promise的返回值就是所有子Promise的返回值数组。

  • 如果有一个失败,那么第一个失败的会把自己的理由作为合成Promise的失败理由。


Promise.race()


Promise.race()是一组集合中最先解决或最先拒绝的Promise,返回一个新的Promise。



  • 栗子


let p1 = new Promise((resolve, reject) => {  
setTimeout(() => {
resolve('success1')
},1000)
})

let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed2')
}, 1500)
})

Promise.race([p1, p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 'success1'
})
复制代码

有上述栗子得到,race的性质:

无论如何,最先执行完成的,就执行相应后面的.then或者.catch。谁先以谁作为回调


总结


上面的Promise就总结到这里,讲的可能不太清楚,有兴趣的小伙伴可以看看链接呀,有什么理解也可以在下方评论区一起交流学习。


面试结束了,面试官人很好,聊的很开心,问题大概都能说上来一点,却总有关键部分忘了hhhhhh,结尾跟面试官聊了一下容易忘这个问题,哈哈哈哈他说我忘就是没学会,以后还是要多总结,多做项目...


面试可以让自己发现更多的知识盲点,从而促进自己学习,大家一起加油冲呀!!


作者:_清水
链接:https://juejin.cn/post/6952083081519955998

收起阅读 »

HashMap原理浅析及相关知识

一、初识Hashmap 作为集合Map的主要实现类;线程不安全的,效率高;存储null的key和value。 二、HashMap在Jdk7中实现原理 1、HashMap map = new HashMap() 实例化之后会在底层创建长度是16的一维数组Ent...
继续阅读 »

一、初识Hashmap


作为集合Map的主要实现类;线程不安全的,效率高;存储null的key和value。


image.png


二、HashMap在Jdk7中实现原理


1、HashMap map = new HashMap()


实例化之后会在底层创建长度是16的一维数组Entry[] table。


2、map.put(key1,value1)


调用Key1所在类的hashCode()计算key1哈希值,得到Entry数组中存放的位置                   ---比较存放位置

如果此位置为空,此时key1-value1添加成功 *情况1,添加成功*

此位置不为空(以为此位置存在一个或多个数据(以链表形式存在)),比较key1和已存在的数据的哈希值: --比较哈希值

如果key1的哈希值与存在数据哈希值都不相同,此时key1-value1添加成功 *情况2,添加成功*

如果key1的哈希值与某一存在数据(key2,value2)相同,继续调用key1类的equals(key2)方法 --equals比较

如果equals()返回false,此时key1-value1添加成功 *情况3,添加成功*

如果equals()返回true,此时value1替换value2 *情况4,更新原有key的值*

情况2和情况3状态下,key1-value1和原来的数据以链表方式存储。

添加过程中会涉及扩容,超出临界值(存放位置非空)时扩容。默认扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。




三、HashMap在Jdk8之后实现原理


1、HashMap map = new HashMap()


底层没创建一个长度为16的数组,而是在首次调用put()方法时,底层创建长度为16的数组。


2、map.put(key1,value1)


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//首次put,创建长度为16的数组
if ((p = tab[i = (n - 1) & hash]) == null)// 需要插入数据位置为空。注:[i = (n - 1) & hash]找到当前key应插入的位置
tab[i] = newNode(hash, key, value, null); //*情况1*
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//*情况4*
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//红黑树情况
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//*情况2、3*
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//*情况4*
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

3、map.entrySet()


返回一个Set集合


public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

4、map.get(ket)


返回key对应的value值。


public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

5、常见参数:


DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16


DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75


threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12


TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8


MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64


四、涉及的基础知识


位运算符用来对二进制位进行操作,Java中提供了如下表所示的位运算符:位运算符中,除 ~ 以外,其余均为二元运算符。


操作数只能为整型和字符型数据。


C语言中六种位运算符:


<<左移


>>右移


| 按位或


& 按位与


~取反


^ 按位异或


左移符号<<:向左移动若干位,高位丢弃,低位补零,对于左移N位,就等于乘以2^n


带符号右移操作>>:向右移动若干位,低位进行丢弃,高位按照符号位进行填补,对于正数做右移操作时,高位补充0;负数进行右移时,高位补充1


不带符号的右移操作>>>:与右移操作类似,高位补零,低位丢弃,正数符合规律,负数不符合规律


键(key)经过hash函数得到的结果作为地址去存放当前的键值对(key-value)(这个是hashmap的存值方式),但是却发现该地址已经有人先来了,一山不容二虎,就会产生冲突。这个冲突就是hash冲突了。


简单来说:两个不同对象的hashCode相同,这种现象称为hash冲突。


HashMap的Put方法在第2、3情况添加前会产生哈希冲突,HashMap采用的链地址法(将所有哈希地址相同的都链接在同一个链表中 ,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况)解决哈希冲突。


五、相关面试问题


1、HashMap原理?


见上


2、HashMap初始化时阈值默认为12(加载因子为0.75),会使HashMap提前进行扩容,那为什么不在HashMap满的时候再进行扩容?


若加载因子越大,填满的元素越多,好处是,空间利用率高了,但冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高. 因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷.
这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。


3、什么是哈希冲突?如何解决?


4、并发集合


以下均为java.util.concurrent - Java并发工具包中的同步集合


4.1、ConcurrentHashMap 支持完全并发的检索和更新,所希望的可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。不过,尽管所有操作都是线程安全的,但检索操作不必锁定,并且不支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable 进行互操作,这取决于其线程安全,而与其同步细节无关。


4.2、ConcurrentSkipListMap 是基于跳表的实现,也是支持key有序排列的一个key-value数据结构,在并发情况下表现很好,是一种空间换时间的实现,ConcurrentSkipListMap是基于一种乐观锁的方式去实现高并发。


4.3、ConCurrentSkipListSet (在JavaSE 6新增的)提供的功能类似于TreeSet,能够并发的访问有序的set。因为ConcurrentSkipListSet是基于“跳跃列表(skip list)”实现的,只要多个线程没有同时修改集合的同一个部分,那么在正常读、写集合的操作中不会出现竞争现象。


4.4、CopyOnWriteArrayList 是ArrayList 的一个线程安全的变形,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内绝不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。自创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。不支持迭代器上更改元素的操作(移除、设置和添加)。这些方法将抛出 UnsupportedOperationException。


4.5、CopyOnWriteArraySet 线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。


4.6、ConcurrentLinkedQueue 是一个基于链接节点的、无界的、线程安全的队列。此队列按照 FIFO(先进先出)原则对元素进行排序,队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列检索操作从队列头部获得元素。当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择,此队列不允许 null 元素。


注:ArrayList和HashMap是非并发集合,迭代时不能进行修改和删除操作

注:CopyOnWriteArrayList和CopyOnWriteArraySet,最适合于读操作通常大大超过写操作的情况


5、线程安全集合及实现原理?


5.1 早期线程安全的集合


Vector:作为Collection->List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储


HashTable:作为Map古老的实现类;线程安全的,效率低;不能存储null的key和value(Properties为其子类:常用来处理配置文件。key和value都是String类型)


5.2 Collections包装方法


Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合


List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

...

5.3 java.util.concurrent包中的集合


ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率


CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行


除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到


6、HashMap和hashTable的区别?


HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value


Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value


7、hashCode的作用?如何重载hashCode方法?


hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;如果两个对象相同,就是适用于equals(Java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object)方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。


总结:再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。


作者:求求了瘦10斤吧
链接:https://juejin.cn/post/7039596855012884510

收起阅读 »

如何优雅地在Vue页面中引入img图片

vue
我们在学习html的时候,图片标签<img>引入图片 <img src="../assets/images/avatar.png" width="100%"> 但是这样会有2个弊端:因为采用绝对路径引入,所以如果后面这张图片移动了目录,...
继续阅读 »

我们在学习html的时候,图片标签<img>引入图片


<img src="../assets/images/avatar.png" width="100%">

但是这样会有2个弊端:

  • 因为采用绝对路径引入,所以如果后面这张图片移动了目录,就需要修改代src里的路径
  • 如果这张图片在同一页面内有多个地方要使用到,就需要引入多次,而且图片移动了目录,这么多地方都要修改src路径

怎么办?使用动态路径import、require



首先讲讲这两个兄弟,在ES6之前,JS一直没有自己的模块语法,为了解决这种尴尬就有了require.js,在ES6发布之后JS又引入了import的概念

  • 使用import引入
  • import之后需要在data中注册一下,否则显示不了


    <script>
    import lf1 from '@/assets/images/lf1.png'
    import lf2 from '@/assets/images/lf2.png'
    import lf3 from '@/assets/images/lf3.png'
    import lf4 from '@/assets/images/lf4.png'
    import lf5 from '@/assets/images/lf5.png'
    import lf6 from '@/assets/images/lf6.png'
    import lf7 from '@/assets/images/lf7.png'
    import top1 from '@/assets/images/icon_top1.png'

    export default {
    name: 'Left',
    data () {
    return {
    lf1,
    lf2,
    lf3,
    lf4,
    lf5,
    lf6,
    lf7,
    top1
    }
    }
    }
    </script>
    • 使用require引入

    <script>
    import top1 from '@/assets/images/cityOfVitality/icon_top1.png'

    export default {
    name: 'Right',
    data () {
    return {
    rt1: require('@/assets/images/crt1.png'),
    rt2: require('@/assets/images/crt2.png'),
    rt3: require('@/assets/images/crt3.png'),
    rt4: require('@/assets/images/crt4.png'),
    rt5: require('@/assets/images/crt5.png'),
    rt6: require('@/assets/images/crt6.png'),
    top1
    }
    }
    }
    </script>

    作者:Jesse90s
    链接:https://juejin.cn/post/7019964864256802829

    收起阅读 »

    原来flex布局还能那么细?

    简介: flex布局(Flexible布局,弹性布局)是在小程序开发经常使用的布局方式 开启了flex布局的元素叫做flex container flex container里面的直接子元素叫做flex items(也就是开启了flex布局的盒子包裹的...
    继续阅读 »

    简介:



    • flex布局(Flexible布局,弹性布局)是在小程序开发经常使用的布局方式

    • 开启了flex布局的元素叫做flex container




    • flex container里面的直接子元素叫做flex items(也就是开启了flex布局的盒子包裹的第一层子元素)

    • 设置display的属性为flex或者inline-flex可以开启flex布局即成为flex container




    属性值设置为flex和inline-flex的区别:



    1. 如果display对应的值是flex的话,那么flex container是以block-level的形式存在的,相当于是一个块级元素

    2. 如果display的值设置为inline-flex的话,那么flex container是以inline-level的形式存在的,相当于是一个行内块元素




    1. 这两个属性值差异的影响在设置了属性值的元素上面,它们在子元素上的效果都是一样的

    2. 如果一个元素的父元素开启了flex布局;那么其子元素的display属性对自身的影响将会失效,但是对其内容的影响依旧存在的;


    举个例子:父元素设置了display: flex,即使子元素设置了display:block或者display:inline的属性,子元素还是会表现的像个行内块元素一样,这就是父元素对其的影响使其display属性对自身的影响失效了;


    但是为什么我们说其对内容的影响还在呢?假如说父子元素都设置了display: flex,那么子元素自身依然是行块级元素,并不会因为其开启了flex布局就变为块级元素,但是该子元素的内容依然会受到它flex布局的影响,各种flex特有的属性就会生效;


    总结:我们如果想让设置flex布局的盒子变成块级元素的话,那就dispaly的属性值就设置为flex;如果想让盒子变为行内块元素的话,就设置为inline-flex;父元素开启了flex布局之后,子元素的display属性对元素本身的影响就会失效,但是依旧可以影响盒子内部的元素;


    应用在flex container上的CSS属性



    1. flex-flow



    • felx-flowflex-direction || flex-wrap的缩写,这个属性很灵活,你可以只写一个属性,也可以两个都写,甚至交换前后顺序都是可以的

    • flex-flow:column wrap === flex-direction:column;flex-wrap:wrap




    • 如果只写了一个属性值的话,那么另一个属性就直接取默认值;flex-flow:row-reverse === flex-direction:row-reverse;flex-wrap:nowrap



    1. flex-direction


    flex items默认都是沿着main axis(主轴)从main start开始往main end方向排布的



    • flex-direction决定了主轴的方向,有四个取值

    • 分别为row(默认值)、row-reversecolumncolumn-reverse




    • 注意:flex-direction并不是直接改变flex items的排列顺序,他只是通过改变了主轴方向间接的改变了顺序


    1. flex-wrap


    flex-wrap能够决定flex items是在单行还是多行显示



    • nowrap(默认):单行


    本例中父盒子宽度为500px,子盒子为100px;当增加了多个子盒子并且给父盒子设置了flex-wrap:nowrap属性后,效果如下图所示:


    我们会惊奇的发现,父盒子的宽度没有变化,子盒子也确实没有换行,但是他们的宽度均缩小至能适应不换行的条件为止了,这也就是flex布局又称为弹性布局的原因


    所以,我们也可以得出一个结论:如果使用了flex布局的话,一个盒子的大小就算是将宽高写死了也是有可能发生改变的




    • wrap:多行


    换行后元素是往哪边排列跟交叉轴的方向有很大的关系,排列方向是顺着交叉轴的方向来的;


    用的还是刚刚的例子,只不过现在将属性flex-wrap的值设置为了wrap,效果如下图所示:


    子盒子的高度在能够正常换行的情况不会发生变化,但因为当前交叉轴的方向是从上往下的,那么要换行的元素就会排列在下方




    • wrap-reverse:多行(对比wrap,cross start与cross end相反),这个方法可以让交叉轴起点和终点相反,这样整体的布局就会翻转过来



    注意:这里就不是单纯的将要换行的元素向上排列,所有的元素都会受到影响,因为交叉轴的起始点和终止点已经反过来了



    1. justify-content


    Tip:下列图像灰色部分均无任何元素,其他颜色的区域为盒子内容区域


    justify-content决定了flex items在主轴上的对齐方式,总共有6个属性值:



    • flex-start(默认值):在主轴方向上与main start对齐




    • flex-end:在主轴方向上与main end对齐




    • center:在主轴方向上居中对齐




    • space-between


    特点:



    1. 与main start、main end两端对齐

    2. flex items之间的距离相等




    • space-evenly


    特点:



    1. flex items之间的距离相等

    2. flex items与main start、main end之间的距离等于flex items的距离




    • space-around


    特点:



    1. flex items之间的距离相等

    2. flex items与main start、main end之间的距离等于flex items的距离的一半




    1. align-items


    align-items决定了单行flex items在cross axis(交叉轴)上的对齐方式


    注意:主轴只要是横向的,无论flex-direction设置的是row还是row-reverse,其交叉轴都是从上指向下的;


    主轴只要是纵向的,无论flex-direction设置的是column还是column-reverse,其交叉轴都是从左指向右的;


    也就是说:主轴可能会有四种,但是交叉轴只有两种



    该属性具有如下几个属性值:



    • stretch(默认值):当flex items在交叉轴方向上的size(指width或者height,由交叉轴方向确定)为auto时,会自动拉伸至填充;但是如果flex items的size并不是auto,那么产生的效果就和设置为flex-start一样


    注意:触发条件为:父元素设置align-items的属性值为stretch,而子元素在交叉轴方向上的size设置为auto




    • flex-start:与cross start对齐




    • flex-end:与cross end对齐




    • center:居中对齐




    • baseline:与基准线对齐



    至于baseline这个属性值,平时用的并不是很多,基准线可以认为是盒子里面文字的底线,基准线对齐就是让每个盒子文字的底线对齐


    注意:align-items的默认值与justify-content的默认值不同,它并不是flex-start,而是stretch



    1. align-content



    • align-content决定了多行flex-items在主轴上的对齐方式,用法与justify-content类似,具有以下属性值

    • stretch(默认值)、flex-startflex-endcenterspace-bewteenspace-aroundspace-evenly




    • 大部分属性值看图应该就能明白,主要说一下stretch,当flex items在交叉轴方向上的size设置为auto之后,多行元素的高度之和会挤满父盒子,并且他们的高度是均分的,这和align-itemsstretch属性有点不一样,后者是每一个元素对应的size会填充父盒子,而前者则是均分



    应用在flex items上的CSS属性



    1. flex



    • flex是flex-grow flex-shrink?|| flex-basis的简写,说明flex属性值可以是一个、两个、或者是三个,剩下的为默认值

    • 默认值为flex: 0 1 auto(不放大但会缩小)

    • none: 0 0 auto(既不放大也不缩小)

    • auto:1 1 auto(放大且缩小)

    • 但是其简写方式是多种多样的,不过我们用到最多的还是flex:n;举个"栗子":如果flex是一个非负整数n,则该数字代表的是flex-grow的值,对应的flex-shrink默认为1,但是要格外注意:这里flex-basis的值并不是默认值auto,而是改成了0%;即flex:n === flex:n 1 0%;所以我们常用的flex:1 --> flex:1 1 0%;下图是flex简写的所有情况:




    1. flex-grow



    • flex-grow决定了flex-items如何扩展

    • 可以设置任何非负数字(正整数、正小数、0),默认值为0




    • 只有当flex container在主轴上有剩余的size时,该属性才会生效

    • 如果所有的flex itemsflex-grow属性值总和sum超过1,每个flex item扩展的size就为flex container剩余size * flex-grow / sum

    • 利用上一条计算公式,我们可以得出:当flex itemsflex-grow属性值总和sum不超过1时,扩展的总长度为剩余 size * sum,但是sum又小于1,所以最终flex items不可能完全填充felx container







    • 如果所有的flex itemsflex-grow属性值总和sum不超过1,每个flex item扩展的size就为flex container剩余size * flex-grow





    注意:不要认为flex item扩展的值都是按照flex-grow/sum的比例来进行分配,也并不是说看到flex-grow是小数,就认为其分配到的空间是剩余size*flex-grow,这些都是不准确的。当看到flex item使用了该属性时,首先判断的应该是sum是否大于1,再来判断通过哪种方法来计算比例



    • flex items扩展后的最终size不能超过max-width/max-height






    1. flex-basis



    • flex-basis用来设置flex items主轴方向上的base size,以后flew-growflex-shrink计算时所需要用的base size就是这个

    • auto(默认值)、content:取决于内容本身的size,这两个属性可以认为效果都是一样的,当然也可以设置具体的值和百分数(根据父盒子的比例计算)




    • 决定flex items最终base size因素的优先级为max-width/max-height/min-width/min-height > flex-basis > width/height > 内容本身的size

    • 可以理解为给flex items设置了flex-basis属性且属性值为具体的值或者百分数的话,主轴上对应的size(width/height)就不管用了



    1. flex-shrink



    • flex-shrink决定了flex items如何收缩

    • 可以设置任意非负数字(正小数、正整数、0),默认值是1




    • flex items在主轴方向上超过了flex container的size之后,flex-shrink属性才会生效

    • 注意:与flex-grow不同,计算每个flex item缩小的大小都是通过同一个公式来的,计算比例的方式也有所不同




    • 收缩比例 = flex-shrink * flex item的base size,base size就是flex item放入flex container之前的size

    • 每个flex item收缩的size为flex items超出flex container的size * 收缩比例 / 所有flex items 的收缩比例之和




    • flex items收缩后的最终size不能小于min-width/min-height

    • 总结:当flex items的flex-shrink属性值的总和小于1时,通过其计算收缩size的公式可知,其总共收缩的距离是超出的size * sum,由于sum是小于1的,那么无论如何子盒子都不会完全收缩至超过的距离,也就是说在不换行的情况下子元素一定会有超出





    不同的盒子缩小的值和其自身的flex-shrink属性有关,而且还与自己的原始宽度有关,这是跟flex-grow最大的区别




    1. order



    • order决定了flex items的排布顺序

    • 可以设置为任意整数(正整数、负整数、0),值越小就排在越前面




    • 默认值为0,当flex itemsorder一致时,则按照渲染的顺序排列






    1. align-self



    • flex items可以通过align-self覆盖flex container设置的align-items

    • 默认值为auto:默认遵从flex containeralign-items设置




    • stretchflex-startflex-endcenterbaseline,效果跟align-items一致,简单来说,就是align-items有什么属性,align-self就有哪些属性,当然auto除外


    .item:nth-child(2) {
    align-self: flex-start;
    background-color: #f8f;
    }


    疑难点解析:


    大家在看到flex-wrap那里换行的图片会不会有疑惑,为什么换行的元素不是紧挨着上一行的元素呢?而是有点像居中了的感觉



    想想多行元素在交叉轴上是上依靠哪一个属性进行排列的,当然是align-content了,那它的默认属性值是什么呢?--->stretch


    对,就是因为默认值是stretch,但是flex item又设置了高度,所以flex item不会被拉伸,但是它们会排列在要被拉伸的位置;我们可以测试一下,将flex-items交叉轴上的size设置为auto之后,stretch属性值才会表现的更加明显,平分flex-container在主轴上的高度,每个元素所在的位置就是上一张图所在的位置



    作者:Running53
    链接:https://juejin.cn/post/7033420158685151262

    收起阅读 »

    微信小程序iOS中JS的Date() 获取到的日期时间显示NaN的解决办法

    首先,js日期格式化函数(通过将日期转化为时间戳,再转化为指定格式):function formatDateTime(timeStamp) { var date = new Date(); date.setTime(timeStamp); var y = d...
    继续阅读 »

    首先,js日期格式化函数(通过将日期转化为时间戳,再转化为指定格式):

    function formatDateTime(timeStamp) { 
    var date = new Date();
    date.setTime(timeStamp);
    var y = date.getFullYear();
    var m = date.getMonth() + 1;
    var d = date.getDate();
    m = m < 10 ? ('0' + m) : m;
    d = d < 10 ? ('0' + d) : d;
    return y + '/' + m + '/' + d;
    };

    然后new Date('2018-08-12 23:00:00').getTime(); 安卓可以,苹果iOS却出现NanNan的问题

    这是因为iOS的日期格式是/不是-

    修改后:

    new Date('2018-08-12 23:00:00'.toString().replace(/\,/g, '/')

    OK。

    同理 new Date().getDay() 获取不到当前时间之前日期的星期几 也需要替换下


    原文链接:https://blog.csdn.net/gdali/article/details/88893549

    收起阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(下)

    接 字节跳动面试官:请你实现一个大文件上传和断点续传(上) 断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失...
    继续阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(上)



    断点续传

    断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能

    • 前端使用 localStorage 记录已上传的切片 hash

    • 服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片

    第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失去了记忆的效果,所以这里选取后者

    生成 hash

    无论是前端还是服务端,都必须要生成文件和切片的 hash,之前我们使用文件名 + 切片下标作为切片 hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 hash 的生成规则

    这里用到另一个库 spark-md5,它可以根据文件内容计算出文件的 hash 值,另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互

    由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了importScripts 函数用于导入外部脚本,通过它导入 spark-md5

    // /public/hash.js
    self.importScripts("/spark-md5.min.js"); // 导入脚本

    // 生成文件 hash
    self.onmessage = e => {
    const { fileChunkList } = e.data;
    const spark = new self.SparkMD5.ArrayBuffer();
    let percentage = 0;
    let count = 0;
    const loadNext = index => {
      const reader = new FileReader();
      reader.readAsArrayBuffer(fileChunkList[index].file);
      reader.onload = e => {
        count++;
        spark.append(e.target.result);
        if (count === fileChunkList.length) {
          self.postMessage({
            percentage: 100,
            hash: spark.end()
          });
          self.close();
        } else {
          percentage += 100 / fileChunkList.length;
          self.postMessage({
            percentage
          });
          // 递归计算下一个切片
          loadNext(count);
        }
      };
    };
    loadNext(0);
    };
    复制代码

    在 worker 线程中,接受文件切片 fileChunkList,利用 FileReader 读取每个切片的 ArrayBuffer 并不断传入 spark-md5 中,每计算完一个切片通过 postMessage 向主线程发送一个进度事件,全部完成后将最终的 hash 发送给主线程

    spark-md5 需要根据所有切片才能算出一个 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash,具体可以看官方文档

    spark-md5

    接着编写主线程与 worker 线程通讯的逻辑

    +      // 生成文件 hash(web-worker)
    +   calculateHash(fileChunkList) {
    +     return new Promise(resolve => {
    +       // 添加 worker 属性
    +       this.container.worker = new Worker("/hash.js");
    +       this.container.worker.postMessage({ fileChunkList });
    +       this.container.worker.onmessage = e => {
    +         const { percentage, hash } = e.data;
    +         this.hashPercentage = percentage;
    +         if (hash) {
    +           resolve(hash);
    +         }
    +       };
    +     });
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
    +     this.container.hash = await this.calculateHash(fileChunkList);
        this.data = fileChunkList.map(({ file },index) => ({
    +       fileHash: this.container.hash,
          chunk: file,
          hash: this.container.file.name + "-" + index, // 文件名 + 数组下标
          percentage:0
        }));
        await this.uploadChunks();
      }  
    复制代码

    主线程使用 postMessage 给 worker 线程传入所有切片 fileChunkList,并监听 worker 线程发出的 postMessage 事件拿到文件 hash

    加上显示计算 hash 的进度条,看起来像这样

    img

    至此前端需要将之前用文件名作为 hash 的地方改写为 workder 返回的这个 hash

    img

    服务端则使用 hash 作为切片文件夹名,hash + 下标作为切片名,hash + 扩展名作为文件名,没有新增的逻辑

    img

    img

    文件秒传

    在实现断点续传前先简单介绍一下文件秒传

    所谓的文件秒传,即在服务端已经存在了上传的资源,所以当用户再次上传时会直接提示上传成功

    文件秒传需要依赖上一步生成的 hash,即在上传前,先计算出文件 hash,并把 hash 发送给服务端进行验证,由于 hash 的唯一性,所以一旦服务端能找到 hash 相同的文件,则直接返回上传成功的信息即可

    +    async verifyUpload(filename, fileHash) {
    +       const { data } = await this.request({
    +         url: "http://localhost:3000/verify",
    +         headers: {
    +           "content-type": "application/json"
    +         },
    +         data: JSON.stringify({
    +           filename,
    +           fileHash
    +         })
    +       });
    +       return JSON.parse(data);
    +     },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);
    +     const { shouldUpload } = await this.verifyUpload(
    +       this.container.file.name,
    +       this.container.hash
    +     );
    +     if (!shouldUpload) {
    +       this.$message.success("秒传:上传成功");
    +       return;
    +   }
        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
          percentage: 0
        }));
        await this.uploadChunks();
      }  
    复制代码

    秒传其实就是给用户看的障眼法,实质上根本没有上传

    image-20200109143511277

    :)

    服务端的逻辑非常简单,新增一个验证接口,验证文件是否存在即可

    + const extractExt = filename =>
    + filename.slice(filename.lastIndexOf("."), filename.length); // 提取后缀名
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    const resolvePost = req =>
    new Promise(resolve => {
      let chunk = "";
      req.on("data", data => {
        chunk += data;
      });
      req.on("end", () => {
        resolve(JSON.parse(chunk));
      });
    });

    server.on("request", async (req, res) => {
    if (req.url === "/verify") {
    +   const data = await resolvePost(req);
    +   const { fileHash, filename } = data;
    +   const ext = extractExt(filename);
    +   const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    +   if (fse.existsSync(filePath)) {
    +     res.end(
    +       JSON.stringify({
    +         shouldUpload: false
    +       })
    +     );
    +   } else {
    +     res.end(
    +       JSON.stringify({
    +         shouldUpload: true
    +       })
    +     );
    +   }
    }
    });
    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    暂停上传

    讲完了生成 hash 和文件秒传,回到断点续传

    断点续传顾名思义即断点 + 续传,所以我们第一步先实现“断点”,也就是暂停上传

    原理是使用 XMLHttpRequest 的 abort 方法,可以取消一个 xhr 请求的发送,为此我们需要将上传每个切片的 xhr 对象保存起来,我们再改造一下 request 方法

       request({
        url,
        method = "post",
        data,
        headers = {},
        onProgress = e => e,
    +     requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
          xhr.upload.onprogress = onProgress;
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
    +         // 将请求成功的 xhr 从列表中删除
    +         if (requestList) {
    +           const xhrIndex = requestList.findIndex(item => item === xhr);
    +           requestList.splice(xhrIndex, 1);
    +         }
            resolve({
              data: e.target.response
            });
          };
    +       // 暴露当前 xhr 给外部
    +       requestList?.push(xhr);
        });
      },
    复制代码

    这样在上传切片时传入 requestList 数组作为参数,request 方法就会将所有的 xhr 保存在数组中了

    img

    每当一个切片上传成功时,将对应的 xhr 从 requestList 中删除,所以 requestList 中只保存正在上传切片的 xhr

    之后新建一个暂停按钮,当点击按钮时,调用保存在 requestList 中 xhr 的 abort 方法,即取消并清空所有正在上传的切片

     handlePause() {
      this.requestList.forEach(xhr => xhr?.abort());
      this.requestList = [];
    }
    复制代码

    image-20200109143737924

    点击暂停按钮可以看到 xhr 都被取消了

    img

    恢复上传

    之前在介绍断点续传的时提到使用第二种服务端存储的方式实现续传

    由于当文件切片上传后,服务端会建立一个文件夹存储所有上传的切片,所以每次前端上传前可以调用一个接口,服务端将已上传的切片的切片名返回,前端再跳过这些已经上传切片,这样就实现了“续传”的效果

    而这个接口可以和之前秒传的验证接口合并,前端每次上传前发送一个验证的请求,返回两种结果

    • 服务端已存在该文件,不需要再次上传

    • 服务端不存在该文件或者已上传部分文件切片,通知前端进行上传,并把已上传的文件切片返回给前端

    所以我们改造一下之前文件秒传的服务端验证接口

    const extractExt = filename =>
    filename.slice(filename.lastIndexOf("."), filename.length); // 提取后缀名
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    const resolvePost = req =>
    new Promise(resolve => {
      let chunk = "";
      req.on("data", data => {
        chunk += data;
      });
      req.on("end", () => {
        resolve(JSON.parse(chunk));
      });
    });
     
    + // 返回已经上传切片名列表
    + const createUploadedList = async fileHash =>
    +   fse.existsSync(path.resolve(UPLOAD_DIR, fileHash))
    +   ? await fse.readdir(path.resolve(UPLOAD_DIR, fileHash))
    +   : [];

    server.on("request", async (req, res) => {
    if (req.url === "/verify") {
      const data = await resolvePost(req);
      const { fileHash, filename } = data;
      const ext = extractExt(filename);
      const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
      if (fse.existsSync(filePath)) {
        res.end(
          JSON.stringify({
            shouldUpload: false
          })
        );
      } else {
        res.end(
          JSON.stringify({
            shouldUpload: true
    +         uploadedList: await createUploadedList(fileHash)
          })
        );
      }
    }
    });
    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    接着回到前端,前端有两个地方需要调用验证的接口

    • 点击上传时,检查是否需要上传和已上传的切片

    • 点击暂停后的恢复上传,返回已上传的切片

    新增恢复按钮并改造原来上传切片的逻辑



    +   async handleResume() {
    +     const { uploadedList } = await this.verifyUpload(
    +       this.container.file.name,
    +       this.container.hash
    +     );
    +     await this.uploadChunks(uploadedList);
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);

    +     const { shouldUpload, uploadedList } = await this.verifyUpload(
          this.container.file.name,
          this.container.hash
        );
        if (!shouldUpload) {
          this.$message.success("秒传:上传成功");
          return;
        }

        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
          percentage: 0
        }));

    +     await this.uploadChunks(uploadedList);
      },
      // 上传切片,同时过滤已上传的切片
    +   async uploadChunks(uploadedList = []) {
        const requestList = this.data
    +       .filter(({ hash }) => !uploadedList.includes(hash))
          .map(({ chunk, hash, index }) => {
            const formData = new FormData();
            formData.append("chunk", chunk);
            formData.append("hash", hash);
            formData.append("filename", this.container.file.name);
            formData.append("fileHash", this.container.hash);
            return { formData, index };
          })
          .map(async ({ formData, index }) =>
            this.request({
              url: "http://localhost:3000",
              data: formData,
              onProgress: this.createProgressHandler(this.data[index]),
              requestList: this.requestList
            })
          );
        await Promise.all(requestList);
        // 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时
        // 合并切片
    +     if (uploadedList.length + requestList.length === this.data.length) {
            await this.mergeRequest();
    +     }
      }
    复制代码

    image-20200109144331326

    这里给原来上传切片的函数新增 uploadedList 参数,即上图中服务端返回的切片名列表,通过 filter 过滤掉已上传的切片,并且由于新增了已上传的部分,所以之前合并接口的触发条件做了一些改动

    到这里断点续传的功能基本完成了

    进度条改进

    虽然实现了断点续传,但还需要修改一下进度条的显示规则,否则在暂停上传/接收到已上传切片时的进度条会出现偏差

    切片进度条

    由于在点击上传/恢复上传时,会调用验证接口返回已上传的切片,所以需要将已上传切片的进度变成 100%

       async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);
        const { shouldUpload, uploadedList } = await this.verifyUpload(
          this.container.file.name,
          this.container.hash
        );
        if (!shouldUpload) {
          this.$message.success("秒传:上传成功");
          return;
        }
        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
    +       percentage: uploadedList.includes(index) ? 100 : 0
        }));
        await this.uploadChunks(uploadedList);
      },
    复制代码

    uploadedList 会返回已上传的切片,在遍历所有切片时判断当前切片是否在已上传列表里即可

    文件进度条

    之前说到文件进度条是一个计算属性,根据所有切片的上传进度计算而来,这就遇到了一个问题

    img

    点击暂停会取消并清空切片的 xhr 请求,此时如果已经上传了一部分,就会发现文件进度条有倒退的现象

    img

    当点击恢复时,由于重新创建了 xhr 导致切片进度清零,所以总进度条就会倒退

    解决方案是创建一个“假”的进度条,这个假进度条基于文件进度条,但只会停止和增加,然后给用户展示这个假的进度条

    这里我们使用 Vue 的监听属性

      data: () => ({
    +   fakeUploadPercentage: 0
    }),
    computed: {
      uploadPercentage() {
        if (!this.container.file || !this.data.length) return 0;
        const loaded = this.data
          .map(item => item.size * item.percentage)
          .reduce((acc, cur) => acc + cur);
        return parseInt((loaded / this.container.file.size).toFixed(2));
      }
    },  
    watch: {
    +   uploadPercentage(now) {
    +     if (now > this.fakeUploadPercentage) {
    +       this.fakeUploadPercentage = now;
    +     }
      }
    },
    复制代码

    当 uploadPercentage 即真的文件进度条增加时,fakeUploadPercentage 也增加,一旦文件进度条后退,假的进度条只需停止即可

    至此一个大文件上传 + 断点续传的解决方案就完成了

    总结

    大文件上传

    • 前端上传大文件时使用 Blob.prototype.slice 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片

    • 服务端接收切片并存储,收到合并请求后使用流将切片合并到最终文件

    • 原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听

    • 使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度

    断点续传

    • 使用 spark-md5 根据文件内容算出文件 hash

    • 通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)

    • 通过 XMLHttpRequest 的 abort 方法暂停切片的上传

    • 上传前服务端返回已经上传的切片名,前端跳过这些切片的上传

    反馈的问题

    部分功能由于不方便测试,这里列出评论区收集到的一些问题,有兴趣的朋友可以提出你的想法/写个 demo 进一步交流

    • 没有做切片上传失败的处理

    • 使用 web socket 由服务端发送进度信息

    • 打开页面没有自动获取上传切片,而需要主动再次上传一次后才显示

    源代码

    源代码增加了一些按钮的状态,交互更加友好,文章表达比较晦涩的地方可以跳转到源代码查看

    file-upload

    谢谢观看 :)

    作者:yeyan1996
    来源:https://juejin.cn/post/6844904046436843527

    收起阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(上)

    前言事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo服务端:nodejs文章有误解的地方,欢迎指出,将在第一时间改正...
    继续阅读 »



    前言

    这段时间面试官都挺忙的,频频出现在博客文章标题,虽然我不是特别想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 :)

    事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对

    结束后花了一段时间整理了下思路,那么究竟该如何实现一个大文件上传,以及在上传中如何实现断点续传的功能呢?

    本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo

    前端:vue element-ui

    服务端:nodejs

    文章有误解的地方,欢迎指出,将在第一时间改正,有更好的实现方式希望留下你的评论

    大文件上传

    整体思路

    前端

    前端大文件上传网上的大部分文章已经给出了解决方案,核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回原文件的某个切片

    这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片,这样从原本传一个大文件,变成了同时传多个小的文件切片,可以大大减少上传时间

    另外由于是并发,传输到服务端的顺序可能会发生变化,所以我们还需要给每个切片记录顺序

    服务端

    服务端需要负责接受这些切片,并在接收到所有切片后合并切片

    这里又引伸出两个问题

    1. 何时合并切片,即切片什么时候传输完成

    2. 如何合并切片

    第一个问题需要前端进行配合,前端在每个切片中都携带切片最大数量的信息,当服务端接受到这个数量的切片时自动合并,也可以额外发一个请求主动通知服务端进行切片的合并

    第二个问题,具体如何合并切片呢?这里可以使用 nodejs 的 读写流(readStream/writeStream),将所有切片的流传输到最终文件的流里

    talk is cheap,show me the code,接着我们用代码实现上面的思路

    前端部分

    前端使用 Vue 作为开发框架,对界面没有太大要求,原生也可以,考虑到美观使用 element-ui 作为 UI 框架

    上传控件

    首先创建选择文件的控件,监听 change 事件以及上传按钮




    复制代码

    请求逻辑

    考虑到通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求

    request({
        url,
        method = "post",
        data,
        headers = {},
        requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
            resolve({
              data: e.target.response
            });
          };
        });
      }
    复制代码

    上传切片

    接着实现比较重要的上传功能,上传需要做两件事

    • 对文件进行切片

    • 将切片传输给服务端




    复制代码

    当点击上传按钮时,调用 createFileChunk 将文件切片,切片数量通过文件大小控制,这里设置 10MB,也就是说 100 MB 的文件会被分成 10 个切片

    createFileChunk 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回

    在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用文件名 + 下标,这样后端可以知道当前切片是第几个切片,用于之后的合并切片

    随后调用 uploadChunks 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 FormData 中,再调用上一步的 request 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片

    发送合并请求

    这里使用整体思路中提到的第二种合并切片的方式,即前端主动通知服务端进行合并,所以前端还需要额外发请求,服务端接受到这个请求时主动合并切片




    复制代码

    服务端部分

    简单使用 http 模块搭建服务端

    const http = require("http");
    const server = http.createServer();

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }
    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    接受切片

    使用 multiparty 包处理前端传来的 FormData

    在 multiparty.parse 的回调中,files 参数保存了 FormData 中文件,fields 参数保存了 FormData 中非文件的字段

    const http = require("http");
    const path = require("path");
    const fse = require("fs-extra");
    const multiparty = require("multiparty");

    const server = http.createServer();
    + const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }

    + const multipart = new multiparty.Form();

    + multipart.parse(req, async (err, fields, files) => {
    +   if (err) {
    +     return;
    +   }
    +   const [chunk] = files.chunk;
    +   const [hash] = fields.hash;
    +   const [filename] = fields.filename;
    +   const chunkDir = path.resolve(UPLOAD_DIR, filename);

    +   // 切片目录不存在,创建切片目录
    +   if (!fse.existsSync(chunkDir)) {
    +     await fse.mkdirs(chunkDir);
    +   }

    +     // fs-extra 专用方法,类似 fs.rename 并且跨平台
    +     // fs-extra 的 rename 方法 windows 平台会有权限问题
    +     // https://github.com/meteor/meteor/issues/7852#issuecomment-255767835
    +     await fse.move(chunk.path, `${chunkDir}/${hash}`);
    +   res.end("received file chunk");
    + });
    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    image-20200110215559194

    查看 multiparty 处理后的 chunk 对象,path 是存储临时文件的路径,size 是临时文件大小,在 multiparty 文档中提到可以使用 fs.rename(由于我用的是 fs-extra,它的 rename 方法 windows 平台权限问题,所以换成了 fse.move) 移动临时文件,即移动文件切片

    在接受文件切片时,需要先创建存储切片的文件夹,由于前端在发送每个切片时额外携带了唯一值 hash,所以以 hash 作为文件名,将切片从临时路径移动切片文件夹中,最后的结果如下

    img

    合并切片

    在接收到前端发送的合并请求后,服务端将文件夹下的所有切片进行合并

    const http = require("http");
    const path = require("path");
    const fse = require("fs-extra");

    const server = http.createServer();
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    + const resolvePost = req =>
    +   new Promise(resolve => {
    +     let chunk = "";
    +     req.on("data", data => {
    +       chunk += data;
    +     });
    +     req.on("end", () => {
    +       resolve(JSON.parse(chunk));
    +     });
    +   });

    + const pipeStream = (path, writeStream) =>
    + new Promise(resolve => {
    +   const readStream = fse.createReadStream(path);
    +   readStream.on("end", () => {
    +     fse.unlinkSync(path);
    +     resolve();
    +   });
    +   readStream.pipe(writeStream);
    + });

    // 合并切片
    + const mergeFileChunk = async (filePath, filename, size) => {
    + const chunkDir = path.resolve(UPLOAD_DIR, filename);
    + const chunkPaths = await fse.readdir(chunkDir);
    + // 根据切片下标进行排序
    + // 否则直接读取目录的获得的顺序可能会错乱
    + chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
    + await Promise.all(
    +   chunkPaths.map((chunkPath, index) =>
    +     pipeStream(
    +       path.resolve(chunkDir, chunkPath),
    +       // 指定位置创建可写流
    +       fse.createWriteStream(filePath, {
    +         start: index * size,
    +         end: (index + 1) * size
    +       })
    +     )
    +   )
    + );
    + fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录
    +};

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }

    +   if (req.url === "/merge") {
    +     const data = await resolvePost(req);
    +     const { filename,size } = data;
    +     const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
    +     await mergeFileChunk(filePath, filename);
    +     res.end(
    +       JSON.stringify({
    +         code: 0,
    +         message: "file merged success"
    +       })
    +     );
    +   }

    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    由于前端在发送合并请求时会携带文件名,服务端根据文件名可以找到上一步创建的切片文件夹

    接着使用 fs.createWriteStream 创建一个可写流,可写流文件名就是切片文件夹名 + 后缀名组合而成

    随后遍历整个切片文件夹,将切片通过 fs.createReadStream 创建可读流,传输合并到目标文件中

    值得注意的是每次可读流都会传输到可写流的指定位置,这是通过 createWriteStream 的第二个参数 start/end 控制的,目的是能够并发合并多个可读流到可写流中,这样即使流的顺序不同也能传输到正确的位置,所以这里还需要让前端在请求的时候多提供一个 size 参数

       async mergeRequest() {
        await this.request({
          url: "http://localhost:3000/merge",
          headers: {
            "content-type": "application/json"
          },
          data: JSON.stringify({
    +         size: SIZE,
            filename: this.container.file.name
          })
        });
      },
    复制代码

    img

    其实也可以等上一个切片合并完后再合并下个切片,这样就不需要指定位置,但传输速度会降低,所以使用了并发合并的手段,接着只要保证每次合并完成后删除这个切片,等所有切片都合并完毕后最后删除切片文件夹即可

    img

    至此一个简单的大文件上传就完成了,接下来我们再此基础上扩展一些额外的功能

    显示上传进度条

    上传进度分两种,一个是每个切片的上传进度,另一个是整个文件的上传进度,而整个文件的上传进度是基于每个切片上传进度计算而来,所以我们先实现切片的上传进度

    切片进度条

    XMLHttpRequest 原生支持上传进度的监听,只需要监听 upload.onprogress 即可,我们在原来的 request 基础上传入 onProgress 参数,给 XMLHttpRequest 注册监听事件

     // xhr
      request({
        url,
        method = "post",
        data,
        headers = {},
    +     onProgress = e => e,
        requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
    +       xhr.upload.onprogress = onProgress;
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
            resolve({
              data: e.target.response
            });
          };
        });
      }
    复制代码

    由于每个切片都需要触发独立的监听事件,所以还需要一个工厂函数,根据传入的切片返回不同的监听函数

    在原先的前端上传逻辑中新增监听函数部分

        // 上传切片,同时过滤已上传的切片
      async uploadChunks(uploadedList = []) {
        const requestList = this.data
    +       .map(({ chunk,hash,index }) => {
            const formData = new FormData();
            formData.append("chunk", chunk);
            formData.append("hash", hash);
            formData.append("filename", this.container.file.name);
    +         return { formData,index };
          })
    +       .map(async ({ formData,index }) =>
            this.request({
              url: "http://localhost:3000",
              data: formData,
    +           onProgress: this.createProgressHandler(this.data[index]),
            })
          );
        await Promise.all(requestList);
          // 合并切片
        await this.mergeRequest();
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.data = fileChunkList.map(({ file },index) => ({
          chunk: file,
    +       index,
          hash: this.container.file.name + "-" + index
    +       percentage:0
        }));
        await this.uploadChunks();
      }    
    +   createProgressHandler(item) {
    +     return e => {
    +       item.percentage = parseInt(String((e.loaded / e.total) * 100));
    +     };
    +   }
    复制代码

    每个切片在上传时都会通过监听函数更新 data 数组对应元素的 percentage 属性,之后把将 data 数组放到视图中展示即可

    文件进度条

    将每个切片已上传的部分累加,除以整个文件的大小,就能得出当前文件的上传进度,所以这里使用 Vue 计算属性

      computed: {
          uploadPercentage() {
            if (!this.container.file || !this.data.length) return 0;
            const loaded = this.data
              .map(item => item.size * item.percentage)
              .reduce((acc, cur) => acc + cur);
            return parseInt((loaded / this.container.file.size).toFixed(2));
          }
    }
    复制代码

    最终视图如下

    img

    字节跳动面试官:请你实现一个大文件上传和断点续传(下)

    作者:yeyan1996
    来源:https://juejin.cn/post/6844904046436843527

    收起阅读 »

    看完这篇文章保你面试稳操胜券——React篇

    ✨欢迎各位小伙伴:\textcolor{blue}{欢迎各位小伙伴:}欢迎各位小伙伴: ✨ 进大厂收藏这一系列就够了,全方位搜集总结,为大家归纳出这篇面试宝典,面试途中祝你一臂之力!,共分为四个系列 ✨包含Vue40道经典面试题\textcolor{g...
    继续阅读 »



    ✨欢迎各位小伙伴:\textcolor{blue}{欢迎各位小伙伴:}欢迎各位小伙伴:
    ✨ 进大厂收藏这一系列就够了,全方位搜集总结,为大家归纳出这篇面试宝典,面试途中祝你一臂之力!,共分为四个系列
    ✨包含Vue40道经典面试题\textcolor{green}{包含Vue40道经典面试题}包含Vue40道经典面试题
    ✨包含react12道高并发面试题\textcolor{green}{包含react12道高并发面试题}包含react12道高并发面试题
    ✨包含微信小程序34道必问面试题\textcolor{green}{包含微信小程序34道必问面试题}包含微信小程序34道必问面试题
    ✨包含javaScript80道扩展面试题\textcolor{green}{包含javaScript80道扩展面试题}包含javaScript80道扩展面试题
    ✨包含APP10道装逼面试题\textcolor{green}{包含APP10道装逼面试题}包含APP10道装逼面试题
    ✨包含HTML/CSS30道基础面试题\textcolor{green}{包含HTML/CSS30道基础面试题}包含HTML/CSS30道基础面试题
    ✨还包含Git、前端优化、ES6、Axios面试题\textcolor{green}{还包含Git、前端优化、ES6、Axios面试题}还包含Git、前端优化、ES6、Axios面试题
    ✨接下来让我们饱享这顿美味吧。一起来学习吧!!!\textcolor{pink}{接下来让我们饱享这顿美味吧。一起来学习吧!!!}接下来让我们饱享这顿美味吧。一起来学习吧!!!
    ✨本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)\textcolor{pink}{本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)}本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)

    react

    React 中 keys 的作用是什么?

    Keys是React用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识 在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。 在 React Diff 算法中React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素, 从而减少不必要的元素重渲染。此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系, 因此我们绝不可忽视转换函数中 Key 的重要性

    传入 setState 函数的第二个参数的作用是什么?

    该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成

    React 中 refs 的作用是什么

    Refs 是 React 提供给我们的安全访问 DOM元素或者某个组件实例的句柄 可以为元素添加ref属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回

    在生命周期中的哪一步你应该发起 AJAX 请求

    我们应当将AJAX 请求放到 componentDidMount 函数中执行,主要原因有下

    React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。 如果我们将AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题

    shouldComponentUpdate 的作用

    shouldComponentUpdate 允许我们手动地判断是否要进行组件更新,根据组件的应用场景设置函数的合理返回值能够帮我们避免不必要的更新

    如何告诉 React 它应该编译生产环境版

    通常情况下我们会使用 Webpack 的 DefinePlugin 方法来将 NODE_ENV 变量值设置为 production。 编译版本中 React会忽略 propType 验证以及其他的告警信息,同时还会降低代码库的大小, React 使用了 Uglify 插件来移除生产环境下不必要的注释等信息

    概述下 React 中的事件处理逻辑

    为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。 这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。 另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。 这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的

    createElement 与 cloneElement 的区别是什么

    createElement 函数是 JSX 编译之后使用的创建 React Element 的函数,而 cloneElement 则是用于复制某个元素并传入新的 Props

    redux中间件

    中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer。 这种机制可以让我们改变数据流,实现如异步action ,action 过滤,日志输出,异常报告等功能 redux-logger:提供日志输出 redux-thunk:处理异步操作 redux-promise:处理异步操作,actionCreator的返回值是promise

    react组件的划分业务组件技术组件?

    根据组件的职责通常把组件分为UI组件和容器组件。 UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。 两者通过React-Redux 提供connect方法联系起来

    react旧版生命周期函数

    初始化阶段

    getDefaultProps:获取实例的默认属性 getInitialState:获取每个实例的初始化状态 componentWillMount:组件即将被装载、渲染到页面上 render:组件在这里生成虚拟的DOM节点 componentDidMount:组件真正在被装载之后 运行中状态

    componentWillReceiveProps:组件将要接收到属性的时候调用 shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render调用,后面的函数不会被继续执行了) componentWillUpdate:组件即将更新不能修改属性和状态 render:组件重新描绘 componentDidUpdate:组件已经更新 销毁阶段

    componentWillUnmount:组件即将销毁

    新版生命周期

    在新版本中,React 官方对生命周期有了新的 变动建议:

    使用getDerivedStateFromProps替换componentWillMount; 使用getSnapshotBeforeUpdate替换componentWillUpdate; 避免使用componentWillReceiveProps; 其实该变动的原因,正是由于上述提到的 Fiber。首先,从上面我们知道 React 可以分成 reconciliation 与 commit两个阶段,对应的生命周期如下:

    reconciliation

    componentWillMount componentWillReceiveProps shouldComponentUpdate componentWillUpdate commit

    componentDidMount componentDidUpdate componentWillUnmount 在 Fiber 中,reconciliation 阶段进行了任务分割,涉及到 暂停 和 重启,因此可能会导致 reconciliation 中的生命周期函数在一次更新渲染循环中被 多次调用 的情况,产生一些意外错误

    Git相关面试题

    git代码冲突处理

    先将本地修改存储起来 git stash 暂存了本地修改之后,就可以pull了。 git pull 还原暂存的内容 git stash pop stash@{0}

    避免重复的合并冲突

    正如每个开发人员都知道的那样,修复合并冲突相当繁琐,但重复解决完全相同的冲突(例如,在长时间运行的功能分支中)更让人心烦。解决方案是:

    git config --global rerere.enabled true 或者你可以通过手动创建目录在每个项目的基础上启用.git/rr-cache。

    使用其他设备从GitHub中导出远程分支项目,无法成功。

    其原因在于本地中根本没有其分支。解决命令如下: git fetch -- 获取所有分支的更新 git branch -a -- 查看本地和远程分支列表,remotes开头的均为远程分支 -- 导出其远程分支,并通过-b设定本地分支跟踪远程分支 git checkout remotes/branch_name -b branch_name

    APP相关面试题

    你平常会看日志吗, 一般会出现哪些异常(Exception)?

    这个主要是面试官考察你会不会看日志,是不是看得懂java里面抛出的异常,Exception

    一般面试中java Exception(runtimeException )是必会被问到的问题 app崩溃的常见原因应该也是这些了。常见的异常列出四五种,是基本要求。

    常见的几种如下:

    NullPointerException - 空指针引用异常 ClassCastException - 类型强制转换异常。 IllegalArgumentException - 传递非法参数异常。 ArithmeticException - 算术运算异常 ArrayStoreException - 向数组中存放与声明类型不兼容对象异常 IndexOutOfBoundsException - 下标越界异常 NegativeArraySizeException - 创建一个大小为负数的数组错误异常 NumberFormatException - 数字格式异常 SecurityException - 安全异常 UnsupportedOperationException - 不支持的操作异常

    app的日志如何抓取?

    app本身的日志,可以用logcat抓取,参考这篇:http://www.cnblogs.com/yoyoketang/…

    adb logcat | find “com.sankuai.meituan” >d:\hello.txt

    也可以用ddms抓取,手机连上电脑,打开ddms工具,或者在Android Studio开发工具中,打开DDMS

    app对于不稳定偶然出现anr和crash时候你是怎么处理的?

    app偶然出现anr和crash是比较头疼的问题,由于偶然出现无法复现步骤,这也是一个测试人员必备的技能,需要抓日志。查看日志主要有3个方法:

    方法一:app开发保存错误日志到本地 一般app开发在debug版本,出现anr和crash的时候会自动把日志保存到本地实际的sd卡上,去对应的app目录取出来就可以了

    方法二:实时抓取 当出现偶然的crash时候,这时候可以把手机拉到你们app开发那,手机连上他的开发代码的环境,有ddms会抓日志,这时候出现crash就会记录下来日志。 尽量重复操作让bug复现就可以了

    也可以自己开着logcat,保存日志到电脑本地,参考这篇:http://www.cnblogs.com/yoyoketang/…

    adb logcat | find “com.sankuai.meituan” >d:\hello.txt

    方法三:第三方sdk统计工具

    一般接入了第三方统计sdk,比如友盟统计,在友盟的后台会抓到报错的日志

    App出现crash原因有哪些?

    为什么App会出现崩溃呢?百度了一下,查到和App崩溃相关的几个因素:内存管理错误,程序逻辑错误,设备兼容,网络因素等,如下: 1.内存管理错误:可能是可用内存过低,app所需的内存超过设备的限制,app跑不起来导致App crash。 或是内存泄露,程序运行的时间越长,所占用的内存越大,最终用尽全部内存,导致整个系统崩溃。 亦或非授权的内存位置的使用也可能会导致App crash。 2.程序逻辑错误:数组越界、堆栈溢出、并发操作、逻辑错误。 e.g. app新添加一个未经测试的新功能,调用了一个已释放的指针,运行的时候就会crash。 3.设备兼容:由于设备多样性,app在不同的设备上可能会有不同的表现。 4.网络因素:可能是网速欠佳,无法达到app所需的快速响应时间,导致app crash。或者是不同网络的切换也可能会影响app的稳定性。

    app出现ANR,是什么原因导致的?

    那么导致ANR的根本原因是什么呢?简单的总结有以下两点:

    1.主线程执行了耗时操作,比如数据库操作或网络编程 2.其他进程(就是其他程序)占用CPU导致本进程得不到CPU时间片,比如其他进程的频繁读写操作可能会导致这个问题。

    细分的话,导致ANR的原因有如下几点: 1.耗时的网络访问 2.大量的数据读写 3.数据库操作 4.硬件操作(比如camera) 5.调用thread的join()方法、sleep()方法、wait()方法或者等待线程锁的时候 6.service binder的数量达到上限 7.system server中发生WatchDog ANR 8.service忙导致超时无响应 9.其他线程持有锁,导致主线程等待超时 10.其它线程终止或崩溃导致主线程一直等待。

    android和ios测试区别?

    App测试中ios和Android有哪些区别呢? 1.Android长按home键呼出应用列表和切换应用,然后右滑则终止应用; 2.多分辨率测试,Android端20多种,ios较少; 3.手机操作系统,Android较多,ios较少且不能降级,只能单向升级;新的ios系统中的资源库不能完全兼容低版本中的ios系统中的应用,低版本ios系统中的应用调用了新的资源库,会直接导致闪退(Crash); 4.操作习惯:Android,Back键是否被重写,测试点击Back键后的反馈是否正确;应用数据从内存移动到SD卡后能否正常运行等; 5.push测试:Android:点击home键,程序后台运行时,此时接收到push,点击后唤醒应用,此时是否可以正确跳转;ios,点击home键关闭程序和屏幕锁屏的情况(红点的显示); 6.安装卸载测试:Android的下载和安装的平台和工具和渠道比较多,ios主要有app store,iTunes和testflight下载; 7.升级测试:可以被升级的必要条件:新旧版本具有相同的签名;新旧版本具有相同的包名;有一个标示符区分新旧版本(如版本号), 对于Android若有内置的应用需检查升级之后内置文件是否匹配(如内置的输入法)

    另外:对于测试还需要注意一下几点: 1.并发(中断)测试:闹铃弹出框提示,另一个应用的启动、视频音频的播放,来电、用户正在输入等,语音、录音等的播放时强制其他正在播放的要暂停; 2.数据来源的测试:输入,选择、复制、语音输入,安装不同输入法输入等; 3.push(推送)测试:在开关机、待机状态下执行推送,消息先死及其推送跳转的正确性; 应用在开发、未打开状态、应用启动且在后台运行的情况下是push显示和跳转否正确; 推送消息阅读前后数字的变化是否正确; 多条推送的合集的显示和跳转是否正确;

    4.分享跳转:分享后的文案是否正确;分享后跳转是否正确,显示的消息来源是否正确;

    5.触屏测试:同时触摸不同的位置或者同时进行不同操作,查看客户端的处理情况,是否会crash等

    app测试和web测试有什么区别?

    WEB测试和App测试从流程上来说,没有区别。 都需要经历测试计划方案,用例设计,测试执行,缺陷管理,测试报告等相关活动。 从技术上来说,WEB测试和APP测试其测试类型也基本相似,都需要进行功能测试、性能测试、安全性测试、GUI测试等测试类型。

    他们的主要区别在于具体测试的细节和方法有区别,比如:性能测试,在WEB测试只需要测试响应时间这个要素,在App测试中还需要考虑流量测试和耗电量测试。

    兼容性测试:在WEB端是兼容浏览器,在App端兼容的是手机设备。而且相对应的兼容性测试工具也不相同,WEB因为是测试兼容浏览器,所以需要使用不同的浏览器进行兼容性测试(常见的是兼容IE6,IE8,chrome,firefox)如果是手机端,那么就需要兼容不同品牌,不同分辨率,不同android版本甚至不同操作系统的兼容。(常见的兼容方式是兼容市场占用率前N位的手机即可),有时候也可以使用到兼容性测试工具,但WEB兼容性工具多用IETester等工具,而App兼容性测试会使用Testin这样的商业工具也可以做测试。

    安装测试:WEB测试基本上没有客户端层面的安装测试,但是App测试是存在客户端层面的安装测试,那么就具备相关的测试点。

    还有,App测试基于手机设备,还有一些手机设备的专项测试。如交叉事件测试,操作类型测试,网络测试(弱网测试,网络切换)

    交叉事件测试:就是在操作某个软件的时候,来电话、来短信,电量不足提示等外部事件。

    操作类型测试:如横屏测试,手势测试

    网络测试:包含弱网和网络切换测试。需要测试弱网所造成的用户体验,重点要考虑回退和刷新是否会造成二次提交。弱网络的模拟,据说可以用360wifi实现设置。

    从系统架构的层面,WEB测试只要更新了服务器端,客户端就会同步会更新。而且客户端是可以保证每一个用户的客户端完全一致的。但是APP端是不能够保证完全一致的,除非用户更新客户端。如果是APP下修改了服务器端,意味着客户端用户所使用的核心版本都需要进行回归测试一遍。

    还有升级测试:升级测试的提醒机制,升级取消是否会影响原有功能的使用,升级后用户数据是否被清除了。

    Android四大组件

    Android四大基本组件:Activity、BroadcastReceiver广播接收器、ContentProvider内容提供者、Service服务。

    Activity:

    应用程序中,一个Activity就相当于手机屏幕,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序可以包含许多活动,比如事件的点击,一般都会触发一个新的Activity。

    BroadcastReceiver广播接收器:

    应用可以使用它对外部事件进行过滤只对感兴趣的外部事件(如当电话呼入时,或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个activity或serice 来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

    ContentProvider内容提供者:

    内容提供者主要用于在不同应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。只有需要在多个应用程序间共享数据时才需要内容提供者。例如:通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中。它的好处:统一数据访问方式。

    Service服务:

    是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要长期运行的任务(一边打电话,后台挂着QQ)。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另一个应用程序,服务扔然能够保持正常运行,不过服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉后,所有依赖于该进程的服务也会停止运行(正在听音乐,然后把音乐程序退出)。

    Activity生命周期?

    周期即活动从开始到结束所经历的各种状态。生命周期即活动从开始到结束所经历的各个状态。从一个状态到另一个状态的转变,从无到有再到无,这样一个过程中所经历的状态就叫做生命周期。

    Activity本质上有四种状态:

    1.运行(Active/Running):Activity处于活动状态,此时Activity处于栈顶,是可见状态,可以与用户进行交互

    2.暂停(Paused):当Activity失去焦点时,或被一个新的非全面屏的Activity,或被一个透明的Activity放置在栈顶时,Activity就转化为Paused状态。此刻并不会被销毁,只是失去了与用户交互的能力,其所有的状态信息及其成员变量都还在,只有在系统内存紧张的情况下,才有可能被系统回收掉

    3.停止(Stopped):当Activity被系统完全覆盖时,被覆盖的Activity就会进入Stopped状态,此时已不在可见,但是资源还是没有被收回

    4.系统回收(Killed):当Activity被系统回收掉,Activity就处于Killed状态

    如果一个活动在处于停止或者暂停的状态下,系统内存缺乏时会将其结束(finish)或者杀死(kill)。这种非正常情况下,系统在杀死或者结束之前会调用onSaveInstance()方法来保存信息,同时,当Activity被移动到前台时,重新启动该Activity并调用onRestoreInstance()方法加载保留的信息,以保持原有的状态。

    在上面的四中常有的状态之间,还有着其他的生命周期来作为不同状态之间的过度,用于在不同的状态之间进行转换,生命周期的具体说明见下。

    什么是activity

    什么是activity,这个前两年出去面试APP测试岗位,估计问的最多了,特别是一些大厂,先问你是不是做过APP测试,那好,你说说什么是activity? 如果没看过android的开发原理,估计这个很难回答,要是第一个问题就被难住了,面试的信心也会失去一半了,士气大减。

    Activity是Android的四大组件之一,也是平时我们用到最多的一个组件,可以用来显示View。 官方的说法是Activity一个应用程序的组件,它提供一个屏幕来与用户交互,以便做一些诸如打电话、发邮件和看地图之类的事情,原话如下: An Activity is an application component that provides a screen with which users can interact in order to do something, such as dial the phone, take a photo, send an email, or view a map.

    Activity是一个Android的应用组件,它提供屏幕进行交互。每个Activity都会获得一个用于绘制其用户界面的窗口,窗口可以充满哦屏幕也可以小于屏幕并浮动在其他窗口之上。 一个应用通常是由多个彼此松散联系的Activity组成,一般会指定应用中的某个Activity为主活动,也就是说首次启动应用时给用户呈现的Activity。将Activity设为主活动的方法 当然Activity之间可以进行互相跳转,以便执行不同的操作。每当新Activity启动时,旧的Activity便会停止,但是系统会在堆栈也就是返回栈中保留该Activity。 当新Activity启动时,系统也会将其推送到返回栈上,并取得用在这里插入图片描述 户的操作焦点。当用户完成当前Activity并按返回按钮是,系统就会从堆栈将其弹出销毁,然后回复前一Activity 当一个Activity因某个新Activity启动而停止时,系统会通过该Activity的生命周期回调方法通知其这一状态的变化。 Activity因状态变化每个变化可能有若干种,每一种回调都会提供执行与该状态相应的特定操作的机会

    语音通话功能

    WebRTC实时通讯的核心 WebRTC 建立连接步骤 1.为连接的两端创建一个 RTCPeerConnection 对象,并且给 RTCPeerConnection 对象添加本地流。

    2.获取本地媒体描述信息(SDP),并与对端进行交换。

    3.获取网络信息(Candidate,IP 地址和端口),并与远端进行交换。

    装逼神器

    一般通过面试的短短一个小时时间,面试官需要对你的技术底子进行磨盘,如果你看完下面这些材料,相信你一定能够让他心里直呼牛逼(下面所有链接文章均是小编自己总结的)

    关于scoped样式穿透问题

    blog.csdn.net/JHXL_/artic…

    Vue2和Vue3的区别

    blog.csdn.net/JHXL_/artic…

    项目中的登录流程

    blog.csdn.net/JHXL_/artic…

    构造函数、原型、继承

    blog.csdn.net/JHXL_/artic…

    项目中遇到的难点

    写在最后

    ✨原创不易,还希望各位大佬支持一下\textcolor{blue}{原创不易,还希望各位大佬支持一下}原创不易,还希望各位大佬支持一下
    👍 点赞,你的认可是我创作的动力!\textcolor{green}{点赞,你的认可是我创作的动力!}点赞,你的认可是我创作的动力!
    ⭐️ 收藏,你的青睐是我努力的方向!\textcolor{green}{收藏,你的青睐是我努力的方向!}收藏,你的青睐是我努力的方向!
    ✏️ 评论,你的意见是我进步的财富!\textcolor{green}{评论,你的意见是我进步的财富!}评论,你的意见是我进步的财富!

    作者:几何心凉
    来源:https://juejin.cn/post/7039640038509903909

    收起阅读 »

    撸一个 webpack 插件,希望对大家有所帮助

    最近,陆陆续续搞 了一个 UniUsingComponentsWebpackPlugin 插件(下面介绍),这是自己第三个开源项目,希望大家一起来维护,一起 star 呀,其它两个:vue-okr-tree基于 Vue 2的组织架构树组件地址:github....
    继续阅读 »

    最近,陆陆续续搞 了一个 UniUsingComponentsWebpackPlugin 插件(下面介绍),这是自己第三个开源项目,希望大家一起来维护,一起 star 呀,其它两个:

    • vue-okr-tree

      基于 Vue 2的组织架构树组件

      地址:github.com/qq449245884…

    • ztjy-cli

      团队的一个简易模板初始化脚手架

      地址:github.com/qq449245884…

    • UniUsingComponentsWebpackPlugin

      地址:github.com/qq449245884…

      配合UniApp,用于集成小程序原生组件

      • 配置第三方库后可以自动引入其下的原生组件,而无需手动配置

      • 生产构建时可以自动剔除没有使用到的原生组件

    背景

    第一个痛点

    用 uniapp开发小程序的小伙伴应该知道,我们在 uniapp 中要使用第三方 UI 库(vant-weappiView-weapp)的时候 ,想要在全局中使用,需要在 src/pages.json 中的 usingComponents 添加对应的组件声明,如:

    // src/pages.json
    "usingComponents": {
       "van-button": "/wxcomponents/@vant/weapp/button/index",
    }

    但在开发过程中,我们不太清楚需要哪些组件,所以我们可能会全部声明一遍(PS:这在做公共库的时候更常见),所以我们得一个个的写,做为程序员,我们绝不允许使用这种笨方法。这是第一个痛点

    第二个痛点

    使用第三方组件,除了在 src/pages.json 还需要在对应的生产目录下建立 wxcomponents,并将第三方的库拷贝至该文件下,这个是 uniapp 自定义的,详细就见:uniapp.dcloud.io/frame?id=%e…

    这是第二个痛点

    第三个痛点

    第二痛点,我们将整个UI库拷贝至 wxcomponents,但最终发布的时候,我们不太可能全都用到了里面的全局组件,所以就将不必要的组件也发布上去,增加代码的体积。

    有的小伙伴就会想到,那你将第三方的库拷贝至 wxcomponents时候,可以只拷使用到的就行啦。是这理没错,但组件里面可能还会使用到其它组件,我们还得一个个去看,然后一个个引入,这又回到了第一个痛点了

    有了这三个痛点,必须得有个插件来做这些傻事,处理这三个痛点。于是就有 UniUsingComponentsWebpackPlugin 插件,这个webpack 插件主要解决下面几个问题:

    • 配置第三方库后可以自动引入其下的原生组件,而无需手动配置

    • 生产构建时可以自动剔除没有使用到的原生组件

    webpack 插件

    webpack 的插件体系是一种基于 Tapable 实现的强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。

    从形态上看,插件通常是一个带有 apply函数的类:

    class SomePlugin {
       apply(compiler) {
      }
    }

    Webpack 会在启动后按照注册的顺序逐次调用插件对象的 apply 函数,同时传入编译器对象 compiler ,插件开发者可以以此为起点触达到 webpack 内部定义的任意钩子,例如:

    class SomePlugin {
       apply(compiler) {
           compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
          })
      }
    }

    注意观察核心语句 compiler.hooks.thisCompilation.tap,其中 thisCompilation 为 tapable 仓库提供的钩子对象;tap 为订阅函数,用于注册回调。

    Webpack 的插件体系基于tapable 提供的各类钩子展开,所以有必要先熟悉一下 tapable 提供的钩子类型及各自的特点。

    到这里,就不做继续介绍了,关于插件的更多 详情可以去官网了解。

    这里推荐 Tecvan 大佬写的 《Webpack 插件架构深度讲解》mp.weixin.qq.com/s/tXkGx6Ckt…

    实现思路

    UniUsingComponentsWebpackPlugin 插件主要用到了三个 compiler 钩子。

    第一个钩子是 environment:

    compiler.hooks.environment.tap(
        'UniUsingComponentsWebpackPlugin',
        async () => {
          // todo someing
        }
      );

    这个钩子主要用来自动引入其下的原生组件,这样就无需手动配置。解决第一个痛点

    第二个钩子 thisCompilation,这个钩子可以获得 compilation,能对最终打包的产物进行操作:

    compiler.hooks.thisCompilation.tap(
        'UniUsingComponentsWebpackPlugin',
        (compilation) => {
          // 添加资源 hooks
          compilation.hooks.additionalAssets.tapAsync(
            'UniUsingComponentsWebpackPlugin',
            async (cb) => {
              await this.copyUsingComponents(compiler, compilation);
              cb();
            }
          );
        }
      );

    所以这个勾子用来将 node_modules 下的第三库拷贝到我们生产 dist 目录里面的 wxcomponents解决第二个痛点

    ps:这里也可直接用现有的 copy-webpack-plugin 插件来实现。

    第三个钩子 done,表示 compilation 执行完成:

        if (process.env.NODE_ENV === 'production') {
        compiler.hooks.done.tapAsync(
          'UniUsingComponentsWebpackPlugin',
          (stats, callback) => {
            this.deleteNoUseComponents();
            callback();
          }
        );
      }

    执行完成后,表示我们已经生成 dist 目录了,可以读取文件内容,分析,获取哪些组件被使用了,然后删除没有使用到组件对应的文件。这样就可以解决我们第三个痛点了

    PS:这里我判断只有在生产环境下才会 剔除,开发环境没有,也没太必要。

    使用

    安装

    npm install uni-using-components-webpack-plugin --save-dev

    然后将插件添加到 WebPack Config 中。例如:

    const UniUsingComponentsWebpackPlugin = require("uni-using-components-webpack-plugin");

    module.exports = {
     plugins: [
    new UniUsingComponentsWebpackPlugin({
      patterns: [
      {
      prefix: 'van',
      module: '@vant/weapp',
      },
      {
      prefix: 'i',
      module: 'iview-weapp',
      },
      ],
      })
    ],
    };

    注意:uni-using-components-webpack-plugin 只适用在 UniApp 开发的小程序。

    参数

    NameTypeDescription
    patterns{Array}为插件指定相关

    Patterns

    moduleprefix
    模块名组件前缀

    module 是指 package.json 里面的 name,如使用是 Vant 对应的 module@vant/weapp,如果使用是 iview,刚对应的 moduleiview-weapp,具体可看它们各自的 package.json

    prefix 是指组件的前缀,如 Vant 使用是 van 开头的前缀,iview 使用是 i 开头的前缀,具体可看它们各自的官方文档。

    PS: 这里得吐曹一下 vant,叫别人使用 van 的前缀,然后自己组件里面声明子组件时,却没有使用 van 前缀,如 picker 组件,它里面的 JSON 文件是这么写的:

    {
    "component": true,
    "usingComponents": {
    "picker-column": "../picker-column/index",
    "loading": "../loading/index"
    }
    }

    picker-columnloading 都没有带 van 前缀,因为这个问题,在做 自动剔除 功能中,我是根据 前缀来判断使用哪些组件的,由于这里的 loadingpicker-column 没有加前缀,所以就被会删除,导致最终的 picker 用不了。为了解决这个问题,增加了不少工作量。

    希望 Vant 官方后面的版本能优化一下。

    总结

    本文通用自定义 Webpack 插件来实现日常一些技术优化需求。主要为大家介绍了 Webpack 插件的基本组成和简单架构,通过三个痛点,引出了 uni-using-components-webpack-plugin 插件,并介绍了使用方式,实现思路。

    最后,关于 Webpack 插件开发,还有更多知识可以学习,建议多看看官方文档《Writing a Plugin》进行学习。

    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    作者:前端小智
    来源:https://juejin.cn/post/7039855875967696904

    收起阅读 »

    膜拜!用最少的代码却实现了最牛逼的滚动动画!

    今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~ 在聊ScrollTrigger插件之前我们先简单了解下GSAP。 GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、Reac...
    继续阅读 »

    今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~



    在聊ScrollTrigger插件之前我们先简单了解下GSAP



    GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、React、画布、通用对象等)动画化,并解决不同浏览器上存在的兼容问题,而且比 jQuery快 20 倍。大约1000万个网站和许多主要品牌都在使用GSAP。



    接下来老鱼带领大家一起学习ScrollTrigger插件的使用。


    插件简介


    ScrollTrigger是基于GSAP实现的一款高性能页面滚动触发HTML元素动画的插件。


    通过ScrollTrigger使用最少的代码创建令人叹为观止的滚动动画。我们需要知道ScrollTrigger是基于GSAP实现的插件,ScrollTrigger是处理滚动事件的,而真正处理动画是GSAP,二者组合使用才能实现滚动动画~


    插件特点



    • 将任何动画链接到特定元素,以便它仅在视图中显示该元素时才执行该动画。

    • 可以在进入/离开定义的区域或将其直接链接到滚动栏时在动画上执行操作(播放、暂停、恢复、重新启动、反转、完成、重置)。

    • 延迟动画和滚动条之间的同步。

    • 根据速度捕捉动画中的进度值。

    • 嵌入滚动直接触发到任何 GSAP 动画(包括时间线)或创建独立实例,并利用丰富的回调系统做任何您想做的事。

    • 高级固定功能可以在某些滚动位置之间锁定一个元素。

    • 灵活定义滚动位置。

    • 支持垂直或水平滚动。

    • 丰富的回调系统。

    • 当窗口调整大小时,自动重新计算位置。

    • 在开发过程中启用视觉标记,以准确查看开始/结束/触发点的位置。

    • 在滚动记录器处于活动状态时,如将active类添加到触发元素中:toggleClass: "active"

    • 使用 matchMedia() 标准媒体查询为各种屏幕尺寸创建不同的设置。

    • 自定义滚动触发器容器,可以定义一个 div 而不一定是浏览器视口。

    • 高度优化以实现最大性能。

    • 插件大约只有6.5kb大小。


    安装/引用


    CDN


    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/ScrollTrigger.min.js"></script>

    ES Modules


    import { gsap } from "gsap";
    import { ScrollTrigger } from "gsap/ScrollTrigger";

    gsap.registerPlugin(ScrollTrigger);

    UMD/CommonJS


    import { gsap } from "gsap/dist/gsap";
    import { ScrollTrigger } from "gsap/dist/ScrollTrigger";

    gsap.registerPlugin(ScrollTrigger);


    简单示例


    gsap.to(".box", {
    scrollTrigger: ".box", // start the animation when ".box" enters the viewport (once)
    x: 500
    });

    高级示例


    let tl = gsap.timeline({
      // 添加到整个时间线
      scrollTrigger: {
        trigger: ".container",
        pin: true,   // 在执行时固定触发器元素
        start: "top top", // 当触发器的顶部碰到视口的顶部时
        end: "+=500", // 在滚动 500 px后结束
        scrub: 1, // 触发器1秒后跟上滚动条
        snap: {
          snapTo: "labels", // 捕捉时间线中最近的标签
          duration: {min: 0.2, max: 3}, // 捕捉动画应至少为 0.2 秒,但不超过 3 秒(由速度决定)
          delay: 0.2, // 从上次滚动事件开始等待 0.2 秒,然后再进行捕捉
          ease: "power1.inOut" // 捕捉动画的过度时间(默认为“power3”)
        }
      }
    });

    // 向时间线添加动画和标签
    tl.addLabel("start")
    .from(".box p", {scale: 0.3, rotation:45, autoAlpha: 0})
    .addLabel("color")
    .from(".box", {backgroundColor: "#28a92b"})
    .addLabel("spin")
    .to(".box", {rotation: 360})
    .addLabel("end");

    自定义示例


    ScrollTrigger.create({
    trigger: "#id",
    start: "top top",
    endTrigger: "#otherID",
    end: "bottom 50%+=100px",
    onToggle: self => console.log("toggled, isActive:", self.isActive),
    onUpdate: self => {
      console.log("progress:", self.progress.toFixed(3), "direction:", self.direction, "velocity", self.getVelocity());
    }
    });

    接下来,我们一起来看使用ScrollTrigger可以实现怎样的效果吧。


    利用ScrollTrigger可以实现很多炫酷的效果,还有更多示例及源代码,快去公众号后台回复aaa滚动获取学习吧!也欢迎同学们和老鱼讨论哦~


    作者:大前端实验室
    链接:https://juejin.cn/post/7038378577028448293

    收起阅读 »

    领导:小伙子,咱们这个页面出来太慢了!赶紧给我优化一下。

    性能优化 这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点? 接下来让我们一起来探索前端...
    继续阅读 »

    性能优化


    这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点?


    接下来让我们一起来探索前端性能优化(emo~


    如何量化网站是否需要做性能优化?


    首先现在工具碎片化的时代,各种工具满天飞,如何找到一个方便又能直击痛点的工具,是重中之重的首要任务。



    下面使用的就是Chrome自带的插件工具进行分析



    可以使用chrome自带的lightHouse工具进行分析。得出的分数会列举出三个档次。然后再根据提出不同建议进行优化。


    例如:打开掘金的页面,然后点开开发者工具中的Lighthouse插件


    1.png


    我们可以看到几项指标:



    • First Contentful Paint 首屏加载时间(FCP)

    • Time to interactive 可互动的时间(TTI) 衡量一个页面多长时间才能完全交互

    • Speed Index 内容明显填充的速度(SI) 分数越低越好

    • Total Blocking Time 总阻塞时间(TBT) 主线程运行超过50ms的任务叫做Long Task,Total Blocking Time (TBT) 是 Long Tasks(所有超过 50ms 的任务)阻塞主线程并影响页面可用性的时间量,比如异步任务过长就会导致阻塞主线程渲染,这时就需要处理这部分任务

    • Largest Contentful Paint 最大视觉元素加载的时间(LCP) 对于SEO来说最重要的指标,用户如果打开页面很久都不能看清楚完整页面,那么SEO就会很低。(对于Google来说)

    • Cumulative Layout Shift 累计布局偏移(CLS) 衡量页面点击某些内容位置发生偏移后对页面对影响 eg:当图片宽高不确定时会时该指标更高,还比如异步或者dom动态加载到现有内容上的情况也会造成CLS升高


    以上的6个指标就能很好的量化我们网页的性能。得出类似以下结论,并采取措施。



    下面的图片是分析自己的项目得出的图表



    2.png


    3.png



    • 比如打包体积 (webpack优化,tree-sharking和按需加载插件,以及css合并)

    • 图片加载大小优化(使用可压缩图片,搭配上懒加载和预加载)

    • http1.0替换为http2.0后可使用二进制标头和多路复用。(某些图片使用cdn请求时使用了http1.0)

    • 图片没有加上width和heigth(或者说没有父容器限制),当页面重绘重排时容易造成页面排版混乱的情况

    • 避免巨大的网络负载,比如图片的同时请求和减少同时请求的数量

    • 静态资源缓存

    • 减少未使用的 JavaScript 并推迟加载脚本(defer和async)



    千遍万遍,不如自己行动一遍。dev your project!然后再对比服用,效果更好哦!



    如何做性能优化


    Vue-cli已经做了的优化:



    • 使用cache-loader默认为Vue/Babel/TypeScript编译开启,文件会缓存在node_modules/.cache里

    • 图片小于4k的会转为base64储存在js文件中

    • 生产环境会将css提取成单独的文件

    • 提取公共代码

    • 代码压缩

    • 给所有的js文件和css文件加上preload


    我们需要做的优化:(下面做出的优化都是根据分析工具得出后,对应自己的项目进行细化而来)

    1. 首先代码层面:

      1. 多图片的页面需要做图片懒加载+预加载+cdn请求以及压缩。后期会推出一篇关于图片优化的文章...
      2. 组件按需加载
      3. 对于迫不得已的dom操作,尽量一次性操作。避免多次操作dom造成页面重绘重排
      4. 公共组件的提取
      5. ajax的请求尽量能够减少多个,如果ajax请求比较慢,但是又必须得请求。那么可以考虑使用 Web Worker
    2. 打包项目。

      1. 使用webpack插件 例如 tree-sharking进行剔除无关的依赖加载。使用terser进行代码压缩,给执行时间长的loader加 cache-loader,可以使得下次打包就会使用 node_modules/.cache 里的
      2. 静态资源使用缓存或者cdn加载,部分动态文件设置缓存过期时间

    作者:Tzyito
    链接:https://juejin.cn/post/7008422231403397134

    收起阅读 »

    知道这个,再也不用写一堆el-table-column了

    前言 最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。 下面就来分享一下! 进入正题 上面就是table...
    继续阅读 »

    前言


    最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。


    下面就来分享一下!


    进入正题


    image.png
    上面就是table中的全部项,去除第一个复选框,最后一个操作的插槽,一共七项,也就是说el-table-column一共要写9对。这简直不能忍!


    image.png



    这个图只作举一个例子用,跟上面不产生对应关系。



    其中就有5个el-form-item,就这么一大堆。


    所以,我当时就想,可不可以用v-for去渲染el-table-column这个标签呢?保留复选框和最后的操作插槽,我们只需要渲染中间的那几项就行。


    经过我的实验,确实是可以实现的。



    这么写之后就开始质疑之前的我为什么没有这个想法? 要不就能少写一堆💩啦



    实现代码如下(标签部分):


    
                v-for="item in columns"
    :key="item.prop"
    :prop="item.prop"
    :label="item.label"
    :formatter="item.formatter"
    :width="item.width">



    思路是这样,把标签需要显示的定义在一个数组中,遍历数组来达到我们想要的效果,formatter是我们完成提交的数据和页面显示数据的一个转换所用到的。具体写法在下面js部分有写。


    定义数组的写法是vue3 composition api的写法,这个思路的话,用Vue2的写法也能实现的,重要的毕竟是思想(啊,我之前还是想不到这种思路)。



    再吐槽一下下,这种写法每写一个函数或者变量就要return回去,也挺麻烦的感觉,hhhhh



    实现代码如下(JS部分):


    const columns = reactive([
    {
    label:'用户ID',
    prop:'userId'
    },
    {
    label:'用户名',
    prop:'userName'
    },
    {
    label:'用户邮箱',
    prop:'userEmail'
    },
    {
    label:'用户角色',
    prop:'role',
    formatter(row,column,value){
    return {
    0:"管理员",
    1:"普通用户"
    }[value]
    }
    },
    {
    label:'用户状态',
    prop:'state',
    formatter(row,column,value){
    return {
    1:"在职",
    2:"离职",
    3:"试用期"
    }[value]
    }
    },
    {
    label:'注册时间',
    prop:'createTime'
    },
    {
    label:'最后登陆时间',
    prop:'lastLoginTime'
    }
    ])

    作者:Ned
    链接:https://juejin.cn/post/7025921628684943396

    收起阅读 »

    浏览器为什么能唤起App的页面

    疑问的开端 大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。 这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面? 说到跨app的页面调用,大家是不是能够想到一个机制:Activity的...
    继续阅读 »

    疑问的开端


    大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。


    image.png


    这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面?


    说到跨app的页面调用,大家是不是能够想到一个机制:Activity的隐式调用?


    一、隐式启动原理


    当我们有需要调起其他app的页面时,使用的API就是隐式调用。


    比如我们有一个app声明了这样的Activity:


    <activity android:name=".OtherActivity"
    android:screenOrientation="portrait">
    <intent-filter>
    <action android:name="mdove"/>
    <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
    </activity>

    其他App想启动上边这个Activity如下的调用就好:


    val intent = Intent()
    intent.action = "mdove"
    startActivity(intent)

    我们没有主动声明Activity的class,那么系统是怎么为我们找到对应的Activity的呢?其实这里和正常的Activity启动流程是一样的,无非是if / else的实现不同而已。


    接下来咱们就回顾一下Activity的启动流程,为了避免陷入细节,这里只展开和大家相对“耳熟能详”的类和调用栈,以串流程为主。


    1.1、跨进程


    首先我们必须明确一点:无论是隐式启动还是显示启动;无论是启动App内Activity还是启动App外的Activity都是跨进程的。比如我们上述的例子,一个App想要启动另一个App的页面。



    注意没有root的手机,是看不到系统孵化出来的进程的。也就是我们常见的为什么有些代码打不上断点。



    image.png


    追过startActivity()的同学,应该很熟悉下边这个调用流程,跟进几个方法之后就发现进到了一个叫做ActivityTread的类里边。



    ActivityTread这个类有什么特点?有main函数,就是我们的主线程。



    很快我们能看到一个比较常见类的调用:Instrumentation


    // Activity.java
    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
    mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);
    // 省略
    }

    注意mInstrumentation#execStartActivity()有一个标黄的入参,它是ActivityThread中的内部类ApplicationThread



    ApplicationThread这个类有什么特点,它实现了IApplicationThread.Stub,也就是aidl的“跨进程调用的客户端回调”。



    此外mInstrumentation#execStartActivity()中又会看到一个大名鼎鼎的调用:


    public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
    // 省略...
    ActivityManager.getService()
    .startActivity(whoThread, who.getBasePackageName(), intent,
    intent.resolveTypeIfNeeded(who.getContentResolver()),
    token, target != null ? target.mEmbeddedID : null,
    requestCode, 0, null, options);
    return null;
    }

    我们点击去getService()会看到一个标红的IActivityManager的类。



    它并不是一个.java文件,而是aidl文件。



    所以ActivityManager.``getService``()本质返回的是“进程的服务端”接口实例,也就是:


    1.2、ActivityManagerService



    public class ActivityManagerService extends IActivityManager.Stub



    所以执行到这就转到了系统进程(system_process进程)。省略一下代码细节,看一下调用栈:


    image.png


    从过上述debug截图,看一看到此时已经拿到了我们的目标Activitiy的相关信息。


    这里简化一些获取目标类的源码,直接引入结论:


    1.3、PackageManagerService


    这里类相当于解析手机内的所有apk,将其信息构造到内存之中,比如下图这样:



    image.png



    小tips:手机目录中/data/system/packages.xml,可以看到所有apk的path、进程名、权限等信息。



    1.4、启动新进程


    打开目标Activity的前提是:目标Activity的进程启动了。所以第一次想要打开目标Activity,就意味着要启动进程。


    启动进程的代码就在启动Activity的方法中:


    resumeTopActivityInnerLocked->startProcessLocked


    image.png


    这里便引入了另一个另一个大名鼎鼎的类:ZygoteInit。这里简单来说会通过ZygoteInit来进行App进程启动的。


    1.5、ApplicationThread


    进程启动后,继续回到目标Activity的启动流程。这里依旧是一系列的system_process进行的转来转去,然后IApplicationThread进入目标进程。



    注意看,在这里再次通过IApplicationThread回调到ActivityThread


    class H extends Handler {
    // 省略
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case EXECUTE_TRANSACTION:
    final ClientTransaction transaction = (ClientTransaction) msg.obj;
    mTransactionExecutor.execute(transaction);
    // 省略
    break;
    case RELAUNCH_ACTIVITY:
    handleRelaunchActivityLocally((IBinder) msg.obj);
    break;
    }
    // 省略...
    }
    }

    // 执行Callback
    public void execute(ClientTransaction transaction) {
    final IBinder token = transaction.getActivityToken();
    executeCallbacks(transaction);
    }

    这里所谓的CallBack的实现是LaunchActivityItem#execute(),对应的实现:


    public void execute(ClientTransactionHandler client, IBinder token,
    PendingTransactionActions pendingActions) {
    ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
    mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
    mPendingResults, mPendingNewIntents, mIsForward,
    mProfilerInfo, client);
    client.handleLaunchActivity(r, pendingActions, null);
    }

    此时就转到了ActivityThread#handleLaunchActivity(),也就转到了咱们日常的生命周期里边,调用栈如下:



    上述截图的调用链中暗含了Activity实例化的过程(反射):


    public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {

    return (Activity) cl.loadClass(className).newInstance();

    }
    复制代码

    二、浏览器启动原理


    Helo站内的回流页就是一个标准的,浏览器唤起另一个App的实例。


    2.1、交互流程


    html标签有一个属性href,比如:<a href="...">


    我们常见的一种用法:<a href="``https://www.baidu.com``">。也就是点击之后跳转到百度。


    因为这个是前端的标签,依托于浏览器及其内核的实现,跳转到一个网页似乎很“顺其自然”(不然叫什么浏览器)。


    当然这里和android交互的流程基本一致:用隐式调用的方式,声明需要启动的Activity;然后<a href="">传入对应的协议(scheme)即可。比如:


    前端页面:


    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
    <a href="mdove1://haha"> 启动OtherActivity </a>
    </body>

    android声明:


    <activity
    android:name=".OtherActivity"
    android:screenOrientation="portrait">
    <intent-filter>
    <data
    android:host="haha"
    android:scheme="mdove1" />
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
    <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    </activity>

    2.2、推理实现


    浏览器能够加载scheme,可以理解为是浏览器内核做了封装。那么想要让android也能支持对scheme的解析,难道是由浏览器内核做处理吗?


    很明显不可能,做了一套移动端的操作系统,然后让浏览器过来实现,是不是有点杀人诛心。


    所以大概率能猜测出来,应该是手机中的浏览器app做的处理。我们就基于这个猜想去看一看浏览器.apk的实现。


    2.3、浏览器实现


    基于上边说的/data/system/packages.xml文件,我们可以pull出来浏览器的.apk。



    然后jadx反编译一下Browser.apk中WebView相关的源码:




    我们可以发现对href的处理来自于隐式跳转,所以一切就和上边的流程串了起来。


    作者:咸鱼正翻身
    链接:https://juejin.cn/post/7033751175551942692

    收起阅读 »

    浅探Google V8引擎

    探析它之前,我们先抛出以下几个疑问:为什么需要 V8 引擎呢?V8 引擎到底是个啥?它可以做些什么呢?了解它能有什么收获呢?接下来就针对以上几个问题进行详细描述。由来我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命...
    继续阅读 »

    探析它之前,我们先抛出以下几个疑问:

    • 为什么需要 V8 引擎呢?

    • V8 引擎到底是个啥?

    • 它可以做些什么呢?

    • 了解它能有什么收获呢?

    接下来就针对以上几个问题进行详细描述。

    由来

    我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命名一个变量时不需要声明变量的类型)、弱类型、基于原型的语言,内置支持类型。而一般 JS 都是在前端执行(直接影响界面),需要能够快速响应用户,那么就要求语言本身可以被快速地解析和执行,JS 引擎就为此而问世。

    这里提到了解释型语言和静态语言(编译型语言),先简单介绍一下二者:

    • 解释型语言(JS)

      • 每次运行时需要解释器按句依次解释源代码执行,将它们翻译成机器认识的机器代码再执行

    • 编译型语言(Java)

      • 运行时可经过编译器翻译成可执行文件,再由机器运行该可执行文件即可

    从上面的描述中可以看到 JS 运行时每次都要根据源文件进行解释然后执行,而编译型的只需要编译一次,下次可直接运行其可执行文件,但是这样就会导致跨平台的兼容性很差,因此各有优劣。

    而众多 JS 引擎(V8、JavaScriptCore、SpiderMonkey、Chakra等)中 V8 是最为出色的,加上它也是应用于当前最流行的谷歌浏览器,所以我们非常有必要去认识和了解一下,这样对于开发者也就更清楚 JS 在浏览器中到底是如何运行的了。

    认识

    定义

    • 使用 C++ 开发

    • 谷歌开源

    • 编译成原生机器码(支持IA-32, x86-64, ARM, or MIPS CPUs)

    • 使用了如内联缓存(inline caching)等方法来提高性能

    • 运行速度快,可媲美二进制程序

    • 支持众多操作系统,如 windows、linux、android 等

    • 支持其他硬件架构,如 IA32,X64,ARM 等

    • 具有很好的可移植和跨平台特性

    运行

    先来一张官方流程图:

    img

    准备

    JS 文件加载(不归 V8 管):可能来自于网络请求、本地的cache或者是也可以是来自service worker,这是 V8 运行的前提(有源文件才有要解释执行的)。 3种加载方式 & V8的优化

    • Cold load: 首次加载脚本文件时,没有任何数据缓存

    • Warm load:V8 分析到如果使用了相同的脚本文件,会将编译后的代码与脚本文件一起缓存到磁盘缓存中

    • Hot load: 当第三次加载相同的脚本文件时,V8 可以从磁盘缓存中载入脚本,并且还能拿到上次加载时编译后的代码,这样可以避免完全从头开始解析和编译脚本

    而在 V8 6.6 版本的时候进一步改进代码缓存策略,简单讲就是从缓存代码依赖编译过程的模式,改变成两个过程解耦,并增加了可缓存的代码量,从而提升了解析和编译的时间,大大提升了性能,具体细节见V8 6.6 进一步改进缓存性能

    分析

    此过程是将上面环节得到的 JS 代码转换为 AST(抽象语法树)。

    词法分析

    从左往右逐个字符地扫描源代码,通过分析,产生一个不同的标记,这里的标记称为 token,代表着源代码的最小单位,通俗讲就是将一段代码拆分成最小的不可再拆分的单元,这个过程称为词法标记,该过程的产物供下面的语法分析环节使用。

    这里罗列一下词法分析器常用的 token 标记种类:

    • 常数(整数、小数、字符、字符串等)

    • 操作符(算术操作符、比较操作符、逻辑操作符)

    • 分隔符(逗号、分号、括号等)

    • 保留字

    • 标识符(变量名、函数名、类名等)

    TOKEN-TYPE TOKEN-VALUE\
    -----------------------------------------------\
    T_IF                 if\
    T_WHILE              while\
    T_ASSIGN             =\
    T_GREATTHAN          >\
    T_GREATEQUAL         >=\
    T_IDENTIFIER name    / numTickets / ...\
    T_INTEGERCONSTANT    100 / 1 / 12 / ....\
    T_STRINGCONSTANT     "This is a string" / "hello" / ...

    上面提到会逐个从左至右扫描代码然后分析,那么很明显就会想到两种方案,扫描完再分析(非流式处理)和边扫描边分析(流式处理),简单画一下他们的时序图就能发现流式处理效率要高得多,同时分析完也会释放分析过程中占用的内存,也能大大提高内存使用效率,可见该优化的细节处理。

    语法分析

    语法分析是指根据某种给定的形式文法对由单词序列构成的输入文本(例如上个阶段的词法分析产物-tokens stream),进行分析并确定其语法结构的过程,最后产出其 AST(抽象语法树)。

    V8 会将语法分析的过程分为两个阶段来执行:

    • Pre-parser

      • 跳过还未使用的代码

      • 不会生成对应的 AST,会产生不带有变量的引用和声明的 scopes 信息

      • 解析速度会是 Full-parser 的 2 倍

      • 根据 JS 的语法规则仅抛出一些特定的错误信息

    • Full-parser

      • 解析那些使用的代码

      • 生成对应的 AST

      • 产生具体的 scopes 信息,带有变量引用和声明等信息

      • 抛出所有的 JS 语法错误

    为什么要做两次解析?

    如果仅有一次,那只能是 Full-parser,但这样的话,大量未使用的代码会消耗非常多的解析时间,结合实例来看下:通过 Coverage 录制的方式可以分析页面哪些代码没有用到,如下图可以看到最高有 75% 的没有被执行。

    img

    但是预解析并不是万能的,得失是并存的,很明显的一个场景:该文件中的代码全都执行了,那其实就是没必要的,当然这种情况其实还是占比远不如上面的例子,所以这里其实也是一种权衡,需要照顾大多数来达到综合性能的提升。

    下面给出一个示例:

    function add(x, y) {
       if (typeof x === "number") {
           return x + y;
      } else {
           return x + 'tadm';
      }
    }

    复制上面的代码到 web1web2 可以很直观的看到他们的 tokens 和 AST 结构(也可自行写一些代码体验)。

    img

    • tokens

    [
      {
           "type": "Keyword",
           "value": "function"
      },
      {
           "type": "Identifier",
           "value": "add"
      },
      {
           "type": "Punctuator",
           "value": "("
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": ","
      },
      {
           "type": "Identifier",
           "value": "y"
      },
      {
           "type": "Punctuator",
           "value": ")"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "if"
      },
      {
           "type": "Punctuator",
           "value": "("
      },
      {
           "type": "Keyword",
           "value": "typeof"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "==="
      },
      {
           "type": "String",
           "value": "\"number\""
      },
      {
           "type": "Punctuator",
           "value": ")"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "return"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "+"
      },
      {
           "type": "Identifier",
           "value": "y"
      },
      {
           "type": "Punctuator",
           "value": ";"
      },
      {
           "type": "Punctuator",
           "value": "}"
      },
      {
           "type": "Keyword",
           "value": "else"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "return"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "+"
      },
      {
           "type": "String",
           "value": "'tadm'"
      },
      {
           "type": "Punctuator",
           "value": ";"
      },
      {
           "type": "Punctuator",
           "value": "}"
      },
      {
           "type": "Punctuator",
           "value": "}"
      }
    ]
    • AST

    {
     "type": "Program",
     "body": [
      {
         "type": "FunctionDeclaration",
         "id": {
           "type": "Identifier",
           "name": "add"
        },
         "params": [
          {
             "type": "Identifier",
             "name": "x"
          },
          {
             "type": "Identifier",
             "name": "y"
          }
        ],
         "body": {
           "type": "BlockStatement",
           "body": [
            {
               "type": "IfStatement",
               "test": {
                 "type": "BinaryExpression",
                 "operator": "===",
                 "left": {
                   "type": "UnaryExpression",
                   "operator": "typeof",
                   "argument": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "prefix": true
                },
                 "right": {
                   "type": "Literal",
                   "value": "number",
                   "raw": "\"number\""
                }
              },
               "consequent": {
                 "type": "BlockStatement",
                 "body": [
                  {
                     "type": "ReturnStatement",
                     "argument": {
                       "type": "BinaryExpression",
                       "operator": "+",
                       "left": {
                         "type": "Identifier",
                         "name": "x"
                      },
                       "right": {
                         "type": "Identifier",
                         "name": "y"
                      }
                    }
                  }
                ]
              },
               "alternate": {
                 "type": "BlockStatement",
                 "body": [
                  {
                     "type": "ReturnStatement",
                     "argument": {
                       "type": "BinaryExpression",
                       "operator": "+",
                       "left": {
                         "type": "Identifier",
                         "name": "x"
                      },
                       "right": {
                         "type": "Literal",
                         "value": "tadm",
                         "raw": "'tadm'"
                      }
                    }
                  }
                ]
              }
            }
          ]
        },
         "generator": false,
         "expression": false,
         "async": false
      }
    ],
     "sourceType": "script"
    }

    解释

    该阶段就是将上面产生的 AST 转换成字节码。

    这里增加字节码(中间产物)的好处是,并不是将 AST 直接翻译成机器码,因为对应的 cpu 系统会不一致,翻译成机器码时要结合每种 cpu 底层的指令集,这样实现起来代码复杂度会非常高;还有个就是内存占用的问题,因为机器码会存储在内存中,而退出进程后又会存储在磁盘上,加上转换后的机器码多出来很多信息,会比源文件大很多,导致了严重的内存占用问题。

    V8 在执行字节码的过程中,使用到了通用寄存器累加寄存器,函数参数和局部变量保存在通用寄存器里面,累加器中保存中间计算结果,在执行指令的过程中,如果直接由 cpu 从内存中读取数据的话,比较影响程序执行的性能,使用寄存器存储中间数据的设计,可以大大提升 cpu 执行的速度。

    编译

    这个过程主要是 V8 的 TurboFan编译器 将字节码翻译成机器码的过程。

    字节码配合解释器和编译器这一技术设计,可以称为JIT(即时编译技术),Java 虚拟机也是类似的技术,解释器在解释执行字节码时,会收集代码信息,标记一些热点代码(就是一段代码被重复执行多次),TurboFan 会将热点代码直接编译成机器码,缓存起来,下次调用直接运行对应的二进制的机器码,加快执行速度。

    在 TurboFan 将字节码编译成机器码的过程中,还进行了简化处理:常量合并、强制折减、代数重新组合。

    比如:3 + 4 --> 7,x + 1 + 2 --> x + 3 ......

    执行

    到这里我们就开始执行上一阶段产出的机器码。

    而在 JS 的执行过程中,经常遇到的就是对象属性的访问。作为一种动态的语言,一个简单的属性访问可能包含着复杂的语义,比如Object.xxx的形式,可能是属性的直接访问,也可能去调用的对象的Getter方法,还有可能是要通过原型链往上层对象中查找。这种不确定性而且动态判断的情况,会浪费很多查找时间,所以 V8 会把第一次分析的结果放在缓存中,当再次访问相同的属性时,会优先从缓存中去取,调用 GetProperty(Object, "xxx", feedback_cache) 的方法获取缓存,如果有缓存结果,就会跳过查找过程,又大大提升了运行性能。

    除了上面针对读取对象属性的结果缓存的优化,V8 还引入了 Object Shapes(隐藏类)的概念,这里面会记录一些对象的基本信息(比如对象拥有的所有属性、每个属性对于这个对象的偏移量等),这样我们去访问属性时就可以直接通过属性名和偏移量直接定位到他的内存地址,读取即可,大大提升访问效率。

    既然 V8 提出了隐藏类(两个形状相同的对象会去复用同一个隐藏类,何为形状相同的对象?两个对象满足有相同个数的相同属性名称和相同的属性顺序),那么我们开发者也可以很好的去利用它:

    • 尽量创建形状相同的对象

    • 创建完对象后尽量不要再去操作属性,即不增加或者删除属性,也就不会破环对象的形状

    完成

    到此 V8 已经完成了一份 JS 代码的读取、分析、解释、编译、执行。

    总结

    以上就是从 JS 代码下载到最终在 V8 引擎执行的过程分析,可以发现 V8 其实有很多实现的技术点,有着很巧妙的设计思想,比如流式处理、缓存中间产物、垃圾回收等,这里面又会涉及到很多细节,很值得继续深入研究。

    作者:Tadm
    来源:https://juejin.cn/post/7032278688192430117

    收起阅读 »

    手写清除console的loader

    前言删除console方式介绍通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅 因此下面需要介绍几种优雅的清除方式该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在...
    继续阅读 »




    前言

    作为一个前端,对于console.log的调试可谓是相当熟悉,话不多说就是好用!帮助我们解决了很多bug^_^
    但是!有因必有果(虽然不知道为什么说这句但是很顺口),如果把console发到生产环境也是很头疼的,尤其是如果打印的信息很私密的话,可能要凉凉TT

    删除console方式介绍

    对于在生产环境必须要清除的console语句,如果手动一个个删除,听上去就很辛苦,因此这篇文章本着看到了就要学,学到了就要用的精神我打算介绍一下手写loader的方式清除代码中的console语句,在此之前也介绍一下其他可以清除console语句的方式吧哈哈

    1. 方式一:暴力清除

    通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅
    因此下面需要介绍几种优雅的清除方式

    2. 方式二 :uglifyjs-webpack-plugin

    该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在webpack的optimization下,即可使用,需要注意的是:此配置只在production环境下生效

    安装
    npm i uglifyjs-webpack-plugin

    其中drop_console和pure_funcs的区别是:

    • drop_console的配置值为boolean,也就是说如果为true,那么代码中所有带console前缀的调试方式都会被清除,包括console.log,console.warn等

    • pure_funcs的配置值是一个数组,也就是可以配置清除那些带console前缀的语句,截图中配的是['console.log'],因此生产环境上只会清除console.log,如果代码中包含其他带console的前缀,如console.warn则保留

    但是需要注意的是,该方法只对ES5语法有效,如果你的代码中涉及ES6就会报错

    3. 方式三:terser-webpack-plugin

    webpack v5 开箱即带有最新版本的 terser-webpack-plugin。如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

    安装
    npm i terser-webpack-plugin@4

    terser-webpack-plugin对于清楚console的配置可谓是跟uglifyjs-webpack-plugin一点没差,但是他们最大的差别就是TerserWebpackPlugin支持ES6的语法

    4. 方式四:手写loader删除console

    终于进入了主题了,朋友们

    1. 什么是loader

    众所周知,webpack只能理解js,json等文件,那么除了js,json之外的文件就需要通过loader去顺利加载,因此loader在其中担任的就是翻译工作。loader可以看作一个node模块,实际上就是一个函数,但他不能是一个箭头函数,因为它需要继承webpack的this,可以在loader中使用webpack的方法。

    • 单一原则,一个loader只做一件事

    • 调用方式,loader是从右向左调用,遵循链式调用

    • 统一原则,输入输出都是字符串或者二进制数据

    根据第三点,下面的代码就会报错,因为输出的是数字而不是字符串或二进制数据

    module.exports = function(source) {
      return 111
    }

    1. 新建清除console语句的loader

    首先新建一个dropConsole.js文件

    // source:表示当前要处理的内容
    const reg = /(console.log\()(.*)(\))/g;
    module.exports = function(source) {
      // 通过正则表达式将当前处理内容中的console替换为空字符串
      source = source.replace(reg, "")
      // 再把处理好的内容return出去,坚守输入输出都是字符串的原则,并可达到链式调用的目的供下一个loader处理
      return source
    }
    1. 在webpack的配置文件中引入

    module: {
      rules:[
          {
              test: /\.js/,
              use: [
                  {
                  loader: path.resolve(__dirname, "./dropConsole.js"),
                  options: {
                    name: "前端"
                  }
                  }
              ]
          },
        {
      ]
    }

    在webpack的配置中,loader的导入需要绝对路径,否则导入失效,如果想要像第三方loader一样引入,就需要配置resolveLoader 中的modules属性,告诉webpack,当node_modules中找不到时,去别的目录下找

    module: {
      rules:[
          {
              test: /\.js/,
              use: [
                  {
                  loader: 'dropConsole',
                  options: {
                    name: "前端"
                  }
                  }
              ]
          },
        {
      ]
    }
    resolveLoader:{
      modules:["./node_modules","./build"] //此时我的loader写在build目录下
    },

    正常运行后,调试台将不会打印console信息

    1. 最后介绍几种在loader中常用的webpack api

    • this.query:返回webpack的参数即options的对象

    • this.callback:同步模式,可以把自定义处理好的数据传递给webpack

    const reg = /(console.log\()(.*)(\))/g;
    module.exports = function(source) {
      source = source.replace(reg, "");
      this.callback(null,source);
      // return的作用是让webpack知道loader返回的结果应该在this.callback当中,而不是return中
      return    
    }
    • this.async():异步模式,可以大致的认为是this.callback的异步版本,因为最终返回的也是this.callback

    const  path = require('path')
    const util = require('util')
    const babel = require('@babel/core')


    const transform = util.promisify(babel.transform)

    module.exports = function(source,map,meta) {
    var callback = this.async();

    transform(source).then(({code,map})=> {
        callback(null, code,map)
    }).catch(err=> {
        callback(err)
    })
    };

    最后的最后,webpack博大精深,值得我们好好学习,深入研究!

    作者:我也想一夜暴富
    来源:https://juejin.cn/post/7038413043084034062

    收起阅读 »

    给团队做个分享,用30张图带你快速了解TypeScript

    正文30张脑图常见的基本类型我们知道TS是JS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:特殊类型除了一些在JS中常见的类型,也还有一些TS所特有的类型类型断言和类型守卫如何在运行时需要保证和检测来自其他地方的数据也符...
    继续阅读 »

    正文

    30张脑图

    常见的基本类型

    我们知道TSJS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:

    1常见的基本类型.png

    特殊类型

    除了一些在JS中常见的类型,也还有一些TS所特有的类型

    2特殊类型.png

    类型断言和类型守卫

    如何在运行时需要保证和检测来自其他地方的数据也符合我们的要求,这就需要用到断言,而断言需要类型守卫

    3类型断言.png

    接口

    接口本身只是一种规范,里头定义了一些必须有的属性或者方法,接口可以用于规范functionclass或者constructor,只是规则有点区别

    4TS中的接口.png

    类和修饰符

    JS一样,类class出现的目的,其实就是把一些相关的东西放在一起,方便管理

    TS主要也是通过class关键字来定义一个类,并且它还提供了3个修饰符

    5类和修饰符.png

    类的继承和抽象类

    TS中的继承ES6中的类的继承极其相识,子类可以通过extends关键字继承一个类

    但是它还有抽象类的概念,而且抽象类作为基类,不能new

    6.0类的继承和抽象类.png

    泛型

    将泛型理解为宽泛的类型,它通常用于类和函数

    但不管是用于类还是用于函数,核心思想都是:把类型当一种特殊的参数传入进去

    7泛型.png

    类型推断

    TS中是有类型推论的,即在有些没有明确指出类型的地方,类型推论会帮助提供类型

    8类型推断.png

    函数类型

    为了让我们更容易使用,TS为函数添加了类型等

    9函数.png

    数字枚举和字符串枚举

    枚举的好处是,我们可以定义一些带名字的常量,而且可以清晰地表达意图或创建一组有区别的用例

    TS支持数字的和基于字符串的枚举

    10枚举.png

    类型兼容性

    TS里的类型兼容性是基于结构子类型的 11类型兼容性.png

    联合类型和交叉类型

    补充两个TS的类型:联合类型和交叉类型

    12联合类型和交叉类型.png

    for..of和for..in

    TS也支持for..offor..in,但你知道他们两个主要的区别吗

    13forin和forof.png

    模块

    TS的模块化沿用了JS模块的概念,模块是在自身的作用域中执行,在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们

    14模块.png

    命名空间的使用

    使用命名空间的方式,其实非常简单,格式如下: namespace X {}

    15命名空间的使用.png

    解决单个命名空间过大的问题

    16解决单个命名空间过大的问题.png

    简化命名空间

    要简化命名空间,核心就是给常用的对象起一个短的名字

    TS中使用import为指定的符号创建一个别名,格式大概是:import q = x.y.z

    17简化命名空间.png

    规避2个TS中命名空间和模块的陷阱

    18陷阱.png

    模块解析流程

    模块解析是指编译器在查找导入模块内容时所遵循的流程

    流程大致如下:

    image.png

    相对和非相对模块导入

    相对和非相对模块导入主要有以下两点不同

    image.png

    Classic模块解析策略

    TS的模块解析策略,其中的一种就叫Classic

    21Classic模块解析策略.png

    Node.js模块解析过程

    为什么要说Node.js模块解析过程,其实是为了讲TS的另一种模块解析策略做铺垫---Node模块解析策略。

    因为Node模块解析策略就是一种试图在运行时模仿Node.js模块解析的策略

    22Node.js的模块解析过程.png

    Node模块解析策略

    Node模块解析策略模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件的模块解析的策略,但是跟Node.js会有点区别

    23Node模块解析策略.png

    声明合并之接口合并

    声明合并指的就是编译器会针对同名的声明合并为一个声明

    声明合并包括接口合并,接口的合并需要区分接口里面的成员有函数成员和非函数成员,两者有差异

    24接口合并.png

    合并命名空间

    命名空间的合并需要分两种情况:一是同名的命名空间之间的合并,二是命名空间和其他类型的合并

    25合并命名空间.png

    JSX模式

    TS具有三种JSX模式:preservereactreact-native

    26JSX.png

    三斜线指令

    三斜线指令其实上面有讲过,像/// <reference>

    它的格式就是三条斜线后面跟一个标签

    27三斜线指令.png


    作者:LBJ
    链接:https://juejin.cn/post/7036266588227502093

    收起阅读 »

    js实现放大镜

    借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。JS // 获取小图和遮罩、大图、大盒子    var small ...
    继续阅读 »



    先看效果图

    实现原理

    借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。

    HTML和CSS

     <div class="wrap">
       
       <div id="small">
         <img src="img/1.jpg" alt="" >
         <div id="mark">div>
       div>
       
       <div id="big">
         <img src="img/2.jpg" alt="" id="bigimg">
       div>
     div>
    * {
        margin: 0;
        padding: 0;
      }
      .wrap {
        width: 1500px;
        margin: 100px auto;
      }

      #small {
        width: 432px;
        height: 768px;
        float: left;
        position: relative;
      }

      #big {
        /* background-color: seagreen; */
        width: 768px;
        height: 768px;
        float: left;
        /* 超出取景框的部分隐藏 */
        overflow: hidden;
        margin-left: 20px;
        position: relative;
        display: none;
      }

      #bigimg {
        /* width: 864px; */
        position: absolute;
        left: 0;
        top: 0;
      }

      #mark {
        width: 220px;
        height: 220px;
        background-color: #fff;
        opacity: .5;
        position: absolute;
        left: 0;
        top: 0;
        /* 鼠标箭头样式 */
        cursor: move;
        display: none;
      }

    JS

     // 获取小图和遮罩、大图、大盒子
       var small = document.getElementById("small")
       var mark = document.getElementById("mark")
       var big = document.getElementById("big")
       var bigimg = document.getElementById("bigimg")
       // 在小图区域内获取鼠标移动事件;遮罩跟随鼠标移动
       small.onmousemove = function (e) {
         // 得到遮罩相对于小图的偏移量(鼠标所在坐标-小图相对于body的偏移-遮罩本身宽度或高度的一半)
         var s_left = e.pageX - mark.offsetWidth / 2 - small.offsetLeft
         var s_top = e.pageY - mark.offsetHeight / 2 - small.offsetTop
         // 遮罩仅可以在小图内移动,所以需要计算遮罩偏移量的临界值(相对于小图的值)
         var max_left = small.offsetWidth - mark.offsetWidth;
         var max_top = small.offsetHeight - mark.offsetHeight;
         // 遮罩移动右侧大图也跟随移动(遮罩每移动1px,图片需要向相反对的方向移动n倍的距离)
         var n = big.offsetWidth / mark.offsetWidth
         // 遮罩跟随鼠标移动前判断:遮罩相对于小图的偏移量不能超出范围,超出范围要重新赋值(临界值在上边已经计算完成:max_left和max_top)
         // 判断水平边界
         if (s_left < 0) {
           s_left = 0
        } else if (s_left > max_left) {
           s_left = max_left
        }
         //判断垂直边界
         if (s_top < 0) {
           s_top = 0
        } else if (s_top > max_top) {
           s_top = max_top
        }
         // 给遮罩left和top赋值(动态的?因为e.pageX和e.pageY为变化的量),动起来!
         mark.style.left = s_left + "px";
         mark.style.top = s_top + "px";
         // 计算大图移动的距离
         var levelx = -n * s_left;
         var verticaly = -n * s_top;
         // 让图片动起来
         bigimg.style.left = levelx + "px";
         bigimg.style.top = verticaly + "px";
      }
       // 鼠标移入小图内才会显示遮罩和跟随移动样式,移出小图后消失
       small.onmouseenter = function () {
         mark.style.display = "block"
         big.style.display= "block"
      }
       small.onmouseleave = function () {
         mark.style.display = "none"
         big.style.display= "none"
      }

    总结

    • 鼠标焦点一旦动起来,它的偏移量就是动态的;父元素和子元素加上定位后,通过动态改变某个元素的lefttop值来实现“动”的效果。

    • 大图/小图=放大镜(遮罩)/取景框

    • 两张图片一定要等比例缩放

    作者:Onion韩
    来源:https://juejin.cn/post/7030963292818374670

    收起阅读 »

    从谷歌一行代码学到的姿势

    网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框。[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()...
    继续阅读 »

    网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框

    [].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})

    运行效果如下图:

    这个代码虽然只有一行,但是包含的知识点不少,网上有很多解析。我也说下自己的理解,然后最后推荐在实务中使用TreeWalker对象进行遍历。

    我的理解其中主要包含如下4个知识点:

    1. [].forEach.call
    2. $$("*")
    3. a.style.outline
    4. (~~(Math.random()*(1<<24))).toString(16)

    1 [].forEach.call

    1.1 [].forEach

    forEach是数组遍历的一个方法,接收一个函数参数用来处理每一个遍历的元素,常规的使用姿势是:

    let arr = [3, 5, 8];
    arr.forEach((item) => {
    console.log(item);
    })
    // 控制台输出:
    // 3
    // 5
    // 8

    那么下面的写法:

    [].forEach

    只是为了得到 forEach 这个方法,这个方法是定义都在Array.prototype上的方法,[] 表示空数组,可以访问到数组原型对象上的方法。

    得到 forEach 这个方法后,就可以通过 call 发起调用。

    1.2 call

    call函数用来调用一个函数,和普通调用不同,call调用可以修改函数内this指向。

    常规调用函数的姿势:

    let object1 = {
    id: 1,
    printId() {
    console.log(this.id)
    }
    }
    object1.printId();
    // 控制台输出:
    // 1

    因为是正常调用,方法内的this指向object1对象,所以上例输出1。

    使用call调用printId方法,并传入另外一个对象object2:

    let object2 = {
    id: 2
    }
    object1.printId.call(object2);
    // 控制台输出:
    // 2

    这里使用call调用object1.printId函数,传入了object2对象,那么printId函数内的this就是指向object2这个对象,所以结果输出2。

    1.3 综合分析

    综合来看:

    [].forEach.call( $$("*"), function(a){} )

    这行代码的意思就是遍历如下对象:

    $$("*") 

    然后用如下方法处理每个元素:

    function(a){}

    其中,a就是遍历的的每一个元素。

    那么

    $$("*") 

    指什么呢?我们接着往后看。

    2 $$("*")

    这个写法用来获取页面所有元素,相当于

    document.querySelectorAll('*')

    只是

    $$("*") 

    只能在浏览器开发控制台内使用,这个是浏览器开发控制台提供出来的预定义API,至于为什么,大家可以参考底部的参考文章。

    3 a.style.outline

    设置元素边框,估计很多人都知道,但是设置外边框就比较少人了解了,外边框的效果和边框类似,唯一不同的点是外边框盒子模型的算式,仅仅做装饰使用。

    <style type="text/css">
    #swiper {
    width: 100px;
    height: 100px;
    outline: 10px solid;
    }
    style>

    <div id="swiper">div>

    运行效果:

    div元素实际的宽高还是100 * 100,如果把outline改成border,那么div元素的实际宽高就是120 * 120,因为要加上border的宽度。

    外边框设置的最大作用就是:

    可以设置元素边框效果,但是不影响页面布局。

    4 (~~(Math.random()*(1<<24))).toString(16)

    这个代码从结果是得到一个16进制的颜色值,但是为什么能得到呢?

    16进制的颜色值:81f262

    4.1 Math.random()

    这个容易理解,就是随机 [0, 1) 的小数。

    4.2 1<<24

    这个表示1左移24位,二进制表示如下所示:

    1 0000 0000 0000 0000 0000 0000  

    十进制就是表示:

    2^24

    那么

    Math.random() * (1<<24)

    就会得到如下范围的一个随机浮点数:

    [0, 2^24) 

    4.3 两次按位取反

    因为Math.random()得到是一个小数,所以两次按位取反就是为了过滤掉小数部分,最后得到整数。

    所以

    (~~(Math.random()*(1<<24)))

    就会得到如下范围的一个随机整数:

    [0, 2^24) 

    4.4 转成字符串toString(16)

    最后就是把上面得到的数字转成16进制,我们知道toString()是用来把相关的对象转成字符串的,它可以接收一个进制参数,转成不同的进制,默认是转成10进制。

    对象.toString(2); // 转成2进制
    对象.toString(8); // 转成8进制
    对象.toString(10); // 转成10进制
    对象.toString(16); // 转成16进制

    上面的得到的随机整数用二进制表示就是:

    0000 0000 0000 0000 0000 0000  

    1111 1111 1111 1111 1111 1111

    那么2进制转成16进制,是不是就是每4位转一个?

    最终是不是就得到一个6个长度的16进制数了?

    这个字符串加上#是不是就是16进制的颜色值了?

    形如:

    #ac83ce
    #b74384
    等等...

    实务应用

    虽然上面的代码简短,并且知识含量也很高,但是在实务中如果要遍历元素,我并不建议使用这样的方式。

    主要原因是两个:

    1. $$("*") 只在开发控制台可以用,正常项目代码中不能用。
    2. 选中所有元素再遍历,性能低。

    如果实务中要遍历元素,建议是用 TreeWalker。querySelectorAll是一次性获取所有元素然后遍历,TreeWalker是迭代器的方式,性能上 TreeWalker 更优,另外 TreeWalker 还支持各种过滤。

    参考如下示例:

    // 实例化 TreeWalker 对象
    let walker = document.createTreeWalker(
    document.documentElement,
    NodeFilter.SHOW_ELEMENT
    );
    // 遍历
    let node = walker.nextNode();
    while (node !== null) {
    node.style.outline = "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
    node = walker.nextNode();
    }

    虽然代码更多,当时性能更好,并且支持各种过滤等,功能也更加强大。

    如果大家有学到新姿势,麻烦帮忙点个赞,谢谢。欢迎大家留言讨论。

    参考资料

    JavaScript中的$$(*)代表什么和$选择器的由来:ourjs.com/detail/54ab…

    querySelectorAll vs NodeIterator vs TreeWalker:stackoverflow.com/questions/6…

    作者:晴空闲云
    来源:https://juejin.cn/post/7034777643014684703

    收起阅读 »

    现在实现倒计时都这么卷了吗?

    但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版 为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时 旧版的功能实现代码 const totalDuration = 10...
    继续阅读 »

    但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版


    为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时


    旧版的功能实现代码


    const totalDuration = 10 * 1000;
    let requestRef = null;
    let startTime;
    let prevEndTime;
    let prevTime;
    let currentCount = totalDuration;
    let endTime;
    let timeDifferance = 0; // 每1s倒计时偏差值,单位ms
    let interval = 1000;
    let nextTime = interval;

    setInterval(() => {
    let n = 0;
    while (n++ < 1000000000);
    }, 0);

    const animate = (timestamp) => {
    if (prevTime !== undefined) {
    const deltaTime = timestamp - prevTime;
    if (deltaTime >= nextTime) {
    prevTime = timestamp;
    prevEndTime = endTime;
    endTime = new Date().getTime();
    currentCount = currentCount - 1000;
    console.log("currentCount: ", currentCount / 1000);
    timeDifferance = endTime - startTime - (totalDuration - currentCount);
    console.log(timeDifferance);
    nextTime = interval - timeDifferance;
    // 慢太多了,就立刻执行下一个循环
    if (nextTime < 0) {
    nextTime = 0;
    }
    console.log(`执行下一次渲染的时间是:${nextTime}ms`);
    if (currentCount <= 0) {
    currentCount = 0;
    cancelAnimationFrame(requestRef);
    console.log(`累计偏差值: ${endTime - startTime - totalDuration}ms`);
    return;
    }
    }
    } else {
    startTime = new Date().getTime();
    prevTime = timestamp;
    endTime = new Date().getTime();
    }
    requestRef = requestAnimationFrame(animate);
    };

    requestRef = requestAnimationFrame(animate);


    然后有个细小的问题在于这段代码


    // 慢太多了,就立刻执行下一个循环
    if (nextTime < 0) {
    nextTime = 0;
    }

    问题在于,假如遇到线程阻塞的情况,出现了倒计时落后情况严重,假设3s,我这里设置下一个循环是0s,然后现在倒计时当前15s,就会看到快速倒计时到12s,产品同学说你这倒计时还怎么加速了呀


    这倒计时加速像极了职业生涯结束在加速倒计时一样,瑟瑟发抖的我立刻赶紧修复一下


    其实很简单,就是把这个临近值0设置接近每次循环的时间数即可,那么其实是看不出来每次是有在稍微加速/减速的,这里每次循环的时间数是1s,那么我们可以将上面这段代码修改下,把以前立刻就追赶描述的操作,放缓一下追赶的脚步,以此优化用户体验


    例如以前追赶2s3s~4s内立刻追赶上,那么波动是很明显的,但是如果把2s的落后秒数,平躺到接下来要倒计时的1min里,每次大概追赶30ms,那是看不出来滴


    // 慢到一定临界点,比正常循环的时间数稍微慢点,再执行下一个循环
    if (nextTime < 900) {
    nextTime = 900;
    }

    这里我设置落后太多时,每秒追赶100ms,假如落后2s20s后就能追赶回来啦,而且看不出明显波动,时间又是被校验准确的,得到了产品同学的好评!


    虽然修改很小,但是也是反复思考得到的~如果对时间要求比较严格,而且倒计时时间范围比较小,来不及把差距平摊到这么大的时间段,可建议让后端同学定时推送最新的倒计时给前端来校验时间准确性,这就万无一失啦


    结语


    以上是我使用requestAnimationFrame实现倒计时功能反复雕琢的心得,希望能对大家有帮助~如果能获得一个小小的赞作为鼓励会十分感激!!


    作者:一只凤梨
    链接:https://juejin.cn/post/7026735190634414087

    收起阅读 »

    中高级前端不一定了解的setTimeout | 网易实践小总结

    setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
    继续阅读 »

    setTimeout的创建和执行


    我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


    首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


    然后我们看下具体例子:


    setTimeout(function showName() { console.log('showName') }, 1000)
    setTimeout(function showName() { console.log('showName1') }, 1000)
    console.log('martincai')

    以上例子执行是这样:



    • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

    • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

    • 3.执行console.log('martincai')代码

    • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


    所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


    循环源码:


    void MainTherad(){
    for(;;){
    // 执行消息队列中的任务
    Task task = task_queue.takeTask();
    ProcessTask(task);

    // 执行延迟队列中的任务
    ProcessDelayTask()

    if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
    break;
    }
    }

    删除延迟任务


    clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


    setTimeout的几个注意点:



    1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


      function showName() {
    setTimeout(function show() {
    console.log('show')
    }, 0)
    for (let i = 0; i <= 5000; i++) {}
    }
    showName()

    这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



    1. setTimeout嵌套下会有4ms的延迟


    Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



    1. 未激活的页面的setTimeout更改为至少1000ms


    当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



    1. 延迟时间有最大值


    目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


      setTimeout(() => {
    console.log(1)
    }, 2 ** 31)

    以上代码会立即执行


    作者:我在曾经眺望彼岸
    链接:https://juejin.cn/post/7032091028609990692

    收起阅读 »

    你可以永远相信debugger,但是不能永远相信console.log

    总结放前面:console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的...
    继续阅读 »

    总结放前面:

    console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的时候会获得最新的值导致展开和不展开的表现不一致。

    不知道各位大佬有没有遇到过这样的情况,我在代码里面console.log()了一个数组,然后打开浏览器控制台,看着是空的就像这样[],结果我点展开它里面又有值了,但是在代码打印的位置使用length或者获取数组里面的值都是不行的,🤯 就像下面这样:

    let arr = [];
    const setFun = () => {
       return new Promise((reslove) => {
           let arr1 = [1, 2, 3];
           setTimeout(() => {
               reslove(arr1);
          }, 2000)
      });
    }
    const getFun = async () => {
       let result = await setFun();
       result.forEach((item) => {
           arr.push(item);
      })
    }
    getFun();
    console.log(arr);


    或者说我在某处代码console.log()一个对象,明明控制台打印对象的某一个key是1,但是我展开这个对象里面的key居然是2,我在代码里面获取的也是2,就像下面这样:
    不知道各位大佬遇到这样的情况是怎么个想法,反正我第一次遇到的时候我还以为是我的谷歌浏览器出问题了,擦💦我甚至都想卸载重装一波。后来动了动🧠,觉得可能是代码执行顺序的原因,所以我就在代码里面打了断点看了一下,在执行console.log()的时候arr的确是一个空的对象,对arr数组的操作是在console.log()执行之后才进行的。

    所以说这到底是为什么呐?
    其实这个还是和js的引用数据类型还有console.log()的设计有关系。我们都知道引用数据类型大体上可以说是由两部分组成:指针和内容,指针保存的内容就是一个内存地址的指向,指针一般都是基本数据类型保存在栈内存,内容就包含着这个引用数据类型的实际值一般保存在堆内存。😍 而console.log呐打印的时候只是打印了这个引用数据类型的一个快照,快照中的指针和内容都是照相的时候的内容,在console.log()之后,修改了这个引用数据类型,或者说在这之前修改的操作在一个异步的内容里面,当我们去看打印的时候,这个引用数据类型的内容可能就被修改了,但是因为快照的原因我们看到的还是以前的值。
    然后当我们展开的时候,浏览器会利用指针去内存重新读取内容,因为快找的指针是没有发生变化的,所以就看到了改变之后内存,这就是为什么我们展开和不展开看到的结果是不一样的原因了。当然造成这样的原因不一定都是因为我们代码在异步里面操作这个引用数据类型。
    还有就是浏览器在进行I/O的时候异步会提升性能,所有这就是为什么有时候我们写的同步代码依然会出现不一致的情况,就像我第二个图一样。
    下面就验证一下我上面的想法,当我把上面的代码修改一下,直接替换:

    let arr = [];
    const setFun = () => {
       return new Promise((reslove) => {
           let arr1 = [1, 2, 3];
           setTimeout(() => {
               reslove(arr1);
          }, 2000)
      });
    }
    const getFun = async () => {
       let result = await setFun();
       arr = result; // 修改部分
    }
    getFun();
    console.log(arr);

    那么我们看到的结果就和上面不一样了,这个展开的表现是和不展开是一样的。
    相信各位大佬也知道是啥原因了,因为这次直接替换,修改的是指针的指向并没有修改之前引用数据饿类型的内存空间,所以当我们展开的时候快照中指针保存的地址还是空的,这样我们看到的和看之前的想法就对应上了。
    注:该问题只存在于打印引用数据类型,基本数据类型不会出现。

    作者:江湖不渡i
    来源:https://juejin.cn/post/7032504319584780325

    收起阅读 »

    12 个救命的 CSS 技巧

    ✨12 个救命的 CSS 技巧✨ 1. 使用 Shape-outside 在浮动图像周围弯曲文本它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:.any-shape {  width: 300px...
    继续阅读 »



    ✨12 个救命的 CSS 技巧✨

    1. 使用 Shape-outside 在浮动图像周围弯曲文本

    它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:

    .any-shape {
     width: 300px;
     float: left;
     shape-outside: circle(50%);
    }

    2. 魔法组合

    这个小组合实际上可以防止你在 HTML 中遇到的大多数布局错误的问题。我们确实不希望水平滑块或绝对定位的项目做他们想做的事情,也不希望到处都是随机的边距和填充。所以这是你们的魔法组合。

    * {
    padding: 0;
    margin: 0;
    max-width: 100%;
    overflow-x: hidden;
    position: relative;
    display: block;
    }

    有时“display:block”没有用,但在大多数情况下,你会将 <a><span> 视为与其他块一样的块。所以,在大多数情况下,它实际上会帮助你!

    3. 拆分 HTML 和 CSS

    这更像是一种“工作流程”类型的技巧。我建议你在开发时创建不同的 CSS 文件,最后才合并它们。例如,一个用于桌面,一个用于移动等。最后,你必须合并它们,因为这将有助于最大限度地减少您网站的 HTTP 请求数量。

    同样的原则也适用于 HTML。如果你不是在 Gatsby 等 SPA 环境中进行开发,那么 PHP 可用于包含 HTML 代码片段。例如,你希望在单独的文件中保留一个“/modules”文件夹,该文件夹将包含导航栏、页脚等。因此,如果需要进行任何更改,你不必在每个页面上都对其进行编辑。模块化越多,结果就越好。

    4. ::首字母

    它将样式应用于块级元素的第一个字母。因此,我们可以从印刷或纸质杂志中引入我们熟悉的效果。如果没有这个伪元素,我们将不得不创建许多跨度来实现这种效果。例如:

    这是如何做到的?代码如下:

    p.intro:first-letter {
     font-size: 100px;
     display: block;
     float: left;
     line-height: .5;
     margin: 15px 15px 10px 0 ;
    }

    5. 四大核心属性

    CSS 动画提供了一种相对简单的方法来在大量属性之间平滑过渡。良好的动画界面依赖于流畅流畅的体验。为了在我们的动画时间线中保持良好的性能,我们必须将我们的动画属性限制为以下四个核心:

    • 缩放 - transform:scale(2)

    • 旋转 - transform:rotate(180deg)

    • 位置 – transform:translateX(50rem)

    • 不透明度 - opacity: 0.5

    边框半径、高度/宽度或边距等动画属性会影响浏览器布局方法,而背景、颜色或框阴影的动画会影响浏览器绘制方法。所有这些都会大大降低您的 FPS (FramesPerSecond)。您可以使用这些属性来产生一些有趣的效果,但应谨慎使用它们以保持良好的性能。

    6. 使用变量保持一致

    保持一致性的一个好方法是使用 CSS 变量或预处理器变量来预定义动画时间。

    :root{ timing-base: 1000;}

    在不定义单元的情况下设置基线动画或过渡持续时间为我们提供了在 calc() 函数中调用此持续时间的灵活性。此持续时间可能与我们的基本 CSS 变量不同,但它始终是对该数字的简单修改,并将始终保持一致的体验。

    7. 圆锥梯度

    有没有想过是否可以只使用 CSS 创建饼图?好消息是,您实际上可以!这可以使用 conic-gradient 函数来完成。此函数创建一个由渐变组成的图像,其中设置的颜色过渡围绕中心点旋转。您可以使用以下代码行执行此操作:

    .piechart {
     background: conic-gradient(rgb(255, 132, 45) 0% 25%, rgb(166, 195, 209) 25% 56%, #ffb50d  56% 100%);
     border-radius: 50%;
     width: 300px;
     height: 300px;
    }

    8. 更改文本选择颜色

    要更改文本选择颜色,我们使用 ::selection。它是一个伪元素,在浏览器级别覆盖以使用您选择的颜色替换文本突出显示颜色。使用光标选择内容后即可看到效果。

    ::selection {
        background-color: #f3b70f;
    }

    9. 悬停效果

    悬停效果通常用于按钮、文本链接、站点的块部分、图标等。如果您想在有人将鼠标悬停在其上时更改颜色,只需使用相同的 CSS,但要添加 :hover到它并更改样式。这是您的方法;

    .m h2{ 
       font-size:36px;
       color:#000;
       font-weight:800;
    }
    .m h2:hover{
       color:#f00;
    }

    当有人将鼠标悬停在 h2 标签上时,这会将您的 h2 标签的颜色从黑色更改为红色。它非常有用,因为如果您不想更改它,则不必再次声明字体大小或粗细。它只会更改您指定的任何属性。

    10.投影

    添加此属性可为透明图像带来更好的阴影效果。您可以使用给定的代码行执行此操作。

    .img-wrapper img{
             width: 100% ;
             height: 100% ;
             object-fit: cover ;
             filter: drop-shadow(30px 10px 4px #757575);
    }

    11. 使用放置项居中 Div

    居中 div 元素是我们必须执行的最可怕的任务之一。但不要害怕我的朋友,你可以用几行 CSS 将任何 div 居中。只是不要忘记设置display:grid; 对于父元素,然后使用如下所示的 place-items 属性。

    main{
    width: 100% ;
    height: 80vh ;
    display: grid ;
    place-items: center center;
    }

    12. 使用 Flexbox 居中 Div

    我们已经使用地点项目将项目居中。但是现在我们解决了一个经典问题,使用 flexbox 将 div 居中。为此,让我们看一下下面的示例:

    <div>
    <div></div>
    </div>
    .center {
    display: flex;
    align-items: center;
    justify-content: center;
    }

    .center div {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: #b8b7cd;
    }

    首先,我们需要确保父容器持有圆,即 flex-container。在它里面,我们有一个简单的 div 来制作我们的圆圈。我们需要使用以下与 flexbox 相关的重要属性:

    • display: flex; 这确保父容器具有 flexbox 布局。

    • align-items: center; 这可确保 flex 子项与横轴的中心对齐。

    • justify-content: center; 这确保 flex 子项与主轴的中心对齐。

    之后,我们就有了常用的圆形 CSS 代码。现在这个圆是垂直和水平居中的,试试吧!

    作者:海拥
    来源:https://juejin.cn/post/7024372412632268813

    收起阅读 »

    vscode调试入门——不要只会console了!什么是launch.json?

    前言 记得我还是一个小菜鸡的时候,就有人问过我,都用什么调试,我红着脸说到,我只会用console调试。羞愧的我想再继续掌握一下vscode调试的方法,可惜当时没有找到很好的教程,加上相关基础较差,只能是一知半解。如今进化为大菜鸡的我,总结一下基础的vscod...
    继续阅读 »

    前言


    记得我还是一个小菜鸡的时候,就有人问过我,都用什么调试,我红着脸说到,我只会用console调试。羞愧的我想再继续掌握一下vscode调试的方法,可惜当时没有找到很好的教程,加上相关基础较差,只能是一知半解。如今进化为大菜鸡的我,总结一下基础的vscode调试


    基本调试


    可以说vscode对js代码的调试非常友好了,它内置了node的debugger插件,如果是要用vscdoe调试python,c++等还是要后续安装插件的


    基本的调试方法很简单,写一段简单的代码


    图片.png


    然后在调试项里找到这个小三角箭头


    图片.png


    然后就可以进入node的调试界面


    图片.png


    怎么样!是不是很简单就达到了我想要的效果,比单纯一个个console出来要更好


    深入一下


    上边的方法虽然很简单,但是只适用比较简单的情况,对于大多数调试场景,去创建launch配置文件是更好的,因为它允许配置和保存调试设置细节


    launch.json


    当你刚开始创建还没有launch.json的时候 vscoed会自动帮你自动检测你的debug环境,开始debug
    但如果失败了,会让你进行选择


    图片.png


    然后他会在你当前工作区下,给你创建个.vscode文件夹,里面有我们要的launch.json文件。简单说我们可以通过这个文件可配置的debug


    假如你的launch.json文件是这样的(搞懂意思就好)



    {
    "version": "0.2.0",
    "configurations": [
    {
    "type": "node",
    "request": "launch",
    "name": "Launch Program",
    "skipFiles": ["<node_internals>/**"],

    }
    ]
    }

    其中


    type属性是指你这次debug的类型 我这里介绍常用的两类 node和chrome 下边都会说到


    request 指的是请求配置类型,氛围launch和attach


    name 指的就是你这一条调试配置的name,会出现在start绿箭头,选择具体方式的时候


    这几个是必用的


    有更多的会在之后涉及,了解更多可以看这里的文档


    attach还是launch


    这是两个核心的debugging模式,用不同方式去处理工作流


    简单来说 launch会在 你调试的工具 也就是我们用的vscode 启动另外的应用,这很适合你习惯于用浏览器的方法


    attach 意为附加 会在你的开发者工具上附加调试程序


    chrome debugger


    除了用上述的type为node的调试方法,我们也可以用调用的chrome的工作台去调试


    这里要安装一个插件 debugger for chrome 我在之前的文章当你买了新的mac 曾经提到过


    当安装好之后


    你就可以在我们的launch.json里添加配置啦!


    图片.png


    假如我们添加一个这样的配置


    {
    "type": "chrome",
    "request": "launch",
    "name": "Launch Chrome",
    "url": "http://localhost:8080",
    "file": "${workspaceFolder}/index.html"
    },

    file 值得就是打开的文件 workspaceFolder是我们当前的工作区


    假如我们的index.html是这样的并打上断点


    图片.png


    可以进入我们的chrome调试页面


    图片.png


    总结


    基本的调试入门方法就是这样啦,其实还有更深层的内容,我会继续学习,完善这篇文章


    作者:douxpang
    链接:https://juejin.cn/post/6956832271236071431

    收起阅读 »

    前端架构师的 git 功力,你有几成火候?

    Git
    分支管理策略 git 分支强大的同时也非常灵活,如果没有一个好的分支管理策略,团队人员随意合并推送,就会造成分支混乱,各种覆盖,冲突,丢失等问题。 目前最流行的分支管理策略,也称工作流(Workflow),主要包含三种: Git Flow GitHub Fl...
    继续阅读 »

    分支管理策略


    git 分支强大的同时也非常灵活,如果没有一个好的分支管理策略,团队人员随意合并推送,就会造成分支混乱,各种覆盖,冲突,丢失等问题。


    目前最流行的分支管理策略,也称工作流(Workflow),主要包含三种:



    • Git Flow

    • GitHub Flow

    • GitLab Flow


    我司前端团队结合实际情况,制定出自己的一套分支管理策略。


    我们将分支分为 4 个大类:



    • dev-*

    • develop

    • staging

    • release


    dev-* 是一组开发分支的统称,包括个人分支,模块分支,修复分支等,团队开发人员在这组分支上进行开发。


    开发前,先通过 merge 合并 develop 分支的最新代码;开发完成后,必须通过 cherry-pick 合并回 develop 分支。


    develop 是一个单独分支,对应开发环境,保留最新的完整的开发代码。它只接受 cherry-pick 的合并,不允许使用 merge。


    staging 分支对应测试环境。当 develop 分支有更新并且准备发布测试时,staging 要通过 rebase 合并 develop 分支,然后将最新代码发布到测试服务器,供测试人员测试。


    测试发现问题后,再走 dev-* -> develop -> staging 的流程,直到测试通过。


    release 则表示生产环境。release 分支的最新提交永远与线上生产环境代码保持同步,也就是说,release 分支是随时可发布的。


    当 staging 测试通过后,release 分支通过 rebase 合并 staging 分支,然后将最新代码发布到生产服务器。


    总结下合并规则:



    • develop -> (merge) -> dev-*

    • dev-* -> (cherry-pick) -> develop

    • develop -> (rebase) -> staging

    • staging -> (rebase) -> release


    为什么合并到 develop 必须用 cherry-pick?


    使用 merge 合并,如果有冲突,会产生分叉;dev-* 分支多而杂,直接 merge 到 develop 会产生错综复杂的分叉,难以理清提交进度。


    而 cherry-pick 只将需要的 commit 合并到 develop 分支上,且不会产生分叉,使 git 提交图谱(git graph)永远保持一条直线。


    再有,模块开发分支完成后,需要将多个 commit 合为一个 commit,再合并到 develop 分支,避免了多余的 commit,这也是不用 merge 的原因之一。


    为什么合并到 staging/release 必须用 rebase?


    rebase 译为变基,合并同样不会产生分叉。当 develop 更新了许多功能,要合并到 staging 测试,不可能用 cherry-pick 一个一个把 commit 合并过去。因此要通过 rebase 一次性合并过去,并且保证了 staging 与 develop 完全同步。


    release 也一样,测试通过后,用 rebase 一次性将 staging 合并过去,同样保证了 staging 与 release 完全同步。


    commit 规范与提交验证


    commit 规范是指 git commit 时填写的描述信息,要符合统一规范。


    试想,如果团队成员的 commit 是随意填写的,在协作开发和 review 代码时,其他人根本不知道这个 commit 是完成了什么功能,或是修复了什么 Bug,很难把控进度。


    为了直观的看出 commit 的更新内容,开发者社区诞生了一种规范,将 commit 按照功能划分,加一些固定前缀,比如 fix:feat:,用来标记这个 commit 主要做了什么事情。


    目前主流的前缀包括以下部分:



    • build:表示构建,发布版本可用这个

    • ci:更新 CI/CD 等自动化配置

    • chore:杂项,其他更改

    • docs:更新文档

    • feat:常用,表示新增功能

    • fix:常用:表示修复 bug

    • perf:性能优化

    • refactor:重构

    • revert:代码回滚

    • style:样式更改

    • test:单元测试更改


    这些前缀每次提交都要写,刚开始很多人还是记不住的。这里推荐一个非常好用的工具,可以自动生成前缀。地址在这里


    首先全局安装:


    npm install -g commitizen cz-conventional-changelog

    创建 ~/.czrc 文件,写入如下内容:


    { "path": "cz-conventional-changelog" }

    现在可以用 git cz 命令来代替 git commit 命令,效果如下:


    WX20210922.png


    然后上下箭选择前缀,根据提示即可方便的创建符合规范的提交。


    有了规范之后,光靠人的自觉遵守是不行的,还要在流程上对提交信息进行校验。


    这个时候,我们要用到一个新东西 —— git hook,也就是 git 钩子。


    git hook 的作用是在 git 动作发生前后触发自定义脚本。这些动作包括提交,合并,推送等,我们可以利用这些钩子在 git 流程的各个环节实现自己的业务逻辑。


    git hook 分为客户端 hook 和服务端 hook。


    客户端 hook 主要有四个:



    • pre-commit:提交信息前运行,可检查暂存区的代码

    • prepare-commit-msg:不常用

    • commit-msg:非常重要,检查提交信息就用这个钩子

    • post-commit:提交完成后运行


    服务端 hook 包括:



    • pre-receive:非常重要,推送前的各种检查都在这

    • post-receive:不常用

    • update:不常用


    大多数团队是在客户端做校验,所以我们用 commit-msg 钩子在客户端对 commit 信息做校验。


    幸运的是,不需要我们手动去写校验逻辑,社区有成熟的方案:husky + commitlint


    husky 是创建 git 客户端钩子的神器,commitlint 是校验 commit 信息是否符合上述规范。两者配合,可以阻止创建不符合 commit 规范的提交,从源头保证提交的规范。


    husky + commitlint 的具体使用方法请看这里


    误操作的撤回方案


    开发中频繁使用 git 拉取推送代码,难免会有误操作。这个时候不要慌,git 支持绝大多数场景的撤回方案,我们来总结一下。


    撤回主要是两个命令:resetrevert


    git reset


    reset 命令的原理是根据 commitId 来恢复版本。因为每次提交都会生成一个 commitId,所以说 reset 可以帮你恢复到历史的任何一个版本。



    这里的版本和提交是一个意思,一个 commitId 就是一个版本



    reset 命令格式如下:


    $ git reset [option] [commitId]

    比如,要撤回到某一次提交,命令是这样:


    $ git reset --hard cc7b5be

    上面的命令,commitId 是如何获取的?很简单,用 git log 命令查看提交记录,可以看到 commitId 值,这个值很长,我们取前 7 位即可。


    这里的 option 用的是 --hard,其实共有 3 个值,具体含义如下:



    • --hard:撤销 commit,撤销 add,删除工作区改动代码

    • --mixed:默认参数。撤销 commit,撤销 add,还原工作区改动代码

    • --soft:撤销 commit,不撤销 add,还原工作区改动代码


    这里要格外注意 --hard,使用这个参数恢复会删除工作区代码。也就是说,如果你的项目中有未提交的代码,使用该参数会直接删除掉,不可恢复,慎重啊!


    除了使用 commitId 恢复,git reset 还提供了恢复到上一次提交的快捷方式:


    $ git reset --soft HEAD^

    HEAD^ 表示上一个提交,可多次使用。


    其实平日开发中最多的误操作是这样:刚刚提交完,突然发现了问题,比如提交信息没写好,或者代码更改有遗漏,这时需要撤回到上次提交,修改代码,然后重新提交。


    这个流程大致是这样的:


    # 1. 回退到上次提交
    $ git reset HEAD^
    # 2. 修改代码...
    ...
    # 3. 加入暂存
    $ git add .
    # 4. 重新提交
    $ git commit -m 'fix: ***'

    针对这个流程,git 还提供了一个更便捷的方法:


    $ git commit --amend

    这个命令会直接修改当前的提交信息。如果代码有更改,先执行 git add,然后再执行这个命令,比上述的流程更快捷更方便。


    reset 还有一个非常重要的特性,就是真正的后退一个版本


    什么意思呢?比如说当前提交,你已经推送到了远程仓库;现在你用 reset 撤回了一次提交,此时本地 git 仓库要落后于远程仓库一个版本。此时你再 push,远程仓库会拒绝,要求你先 pull。


    如果你需要远程仓库也后退版本,就需要 -f 参数,强制推送,这时本地代码会覆盖远程代码。


    注意,-f 参数非常危险!如果你对 git 原理和命令行不是非常熟悉,切记不要用这个参数。


    那撤回上一个版本的代码,怎么同步到远程更安全呢?


    方案就是下面要说的第二个命令:git revert


    git revert


    revert 与 reset 的作用一样,都是恢复版本,但是它们两的实现方式不同。


    简单来说,reset 直接恢复到上一个提交,工作区代码自然也是上一个提交的代码;而 revert 是新增一个提交,但是这个提交是使用上一个提交的代码。


    因此,它们两恢复后的代码是一致的,区别是一个新增提交(revert),一个回退提交(reset)。


    正因为 revert 永远是在新增提交,因此本地仓库版本永远不可能落后于远程仓库,可以直接推送到远程仓库,故而解决了 reset 后推送需要加 -f 参数的问题,提高了安全性。


    说完了原理,我们再看一下使用方法:


    $ git revert -n [commitId]

    掌握了原理使用就很简单,只要一个 commitId 就可以了。


    Tag 与生产环境


    git 支持对于历史的某个提交,打一个 tag 标签,常用于标识重要的版本更新。


    目前普遍的做法是,用 tag 来表示生产环境的版本。当最新的提交通过测试,准备发布之时,我们就可以创建一个 tag,表示要发布的生产环境版本。


    比如我要发一个 v1.2.4 的版本:


    $ git tag -a v1.2.4 -m "my version 1.2.4"

    然后可以查看:


    $ git show v1.2.4

    > tag v1.2.4
    Tagger: ruims <2218466341@qq.com>
    Date: Sun Sep 26 10:24:30 2021 +0800

    my version 1.2.4

    最后用 git push 将 tag 推到远程:


    $ git push origin v1.2.4

    这里注意:tag 和在哪个分支创建是没有关系的,tag 只是提交的别名。因此 commit 的能力 tag 均可使用,比如上面说的 git resetgit revert 命令。


    当生产环境出问题,需要版本回退时,可以这样:


    $ git revert [pre-tag]
    # 若上一个版本是 v1.2.3,则:
    $ git revert v1.2.3

    在频繁更新,commit 数量庞大的仓库里,用 tag 标识版本显然更清爽,可读性更佳。


    再换一个角度思考 tag 的用处。


    上面分支管理策略的部分说过,release 分支与生产环境代码同步。在 CI/CD(下面会讲到)持续部署的流程中,我们是监听 release 分支的推送然后触发自动构建。


    那是不是也可以监听 tag 推送再触发自动构建,这样版本更新的直观性是不是更好?


    诸多用处,还待大家思考。


    永久杜绝 443 Timeout


    我们团队内部的代码仓库是 GitHub,众所周知的原因,GitHub 拉取和推送的速度非常慢,甚至直接报错:443 Timeout。


    我们开始的方案是,全员开启 VPN。虽然大多时候速度不错,但是确实有偶尔的一个小时,甚至一天,代码死活推不上去,严重影响开发进度。


    后来突然想到,速度慢超时是因为被墙,比如 GitHub 首页打不开。再究其根源,被墙的是访问网站时的 http 或 https 协议,那么其他协议是不是就不会有墙的情况?


    想到就做。我们发现 GitHub 除了默认的 https 协议,还支持 ssh 协议。于是准备尝试一下使用 ssh 协议克隆代码。


    用 ssh 协议比较麻烦的一点,是要配置免密登录,否则每次 pull/push 时都要输入账号密码。


    GitHub 配置 SSH 的官方文档在这里


    英文吃力的同学,可以看这里


    总之,生成公钥后,打开 GitHub 首页,点 Account -> Settings -> SSH and GPG keys -> Add SSH key,然后将公钥粘贴进去即可。


    现在,我们用 ssh 协议克隆代码,例子如下:


    $ git clone git@github.com:[organi-name]/[project-name]

    发现瞬间克隆下来了!再测几次 pull/push,速度飞起!


    不管你用哪个代码管理平台,如果遇到 443 Timeout 问题,请试试 ssh 协议!


    hook 实现部署?


    利用 git hook 实现部署,应该是 hook 的高级应用了。


    现在有很多工具,比如 GitHub,GitLab,都提供了持续集成功能,也就是监听某一分支推送,然后触发自动构建,并自动部署。


    其实,不管这些工具有多少花样,核心的功能(监听和构建)还是由 git 提供。只不过在核心功能上做了与自家平台更好的融合。


    我们今天就抛开这些工具,追本溯源,使用纯 git 实现一个 react 项目的自动部署。掌握了这套核心逻辑,其他任何平台的持续部署也就没那么神秘了。


    由于这一部分内容较多,所以单独拆出去一篇文章,地址如下:


    纯 Git 实现前端 CI/CD


    终极应用: CI/CD


    上面的一些地方也提到了持续集成,持续部署这些字眼,现在,千呼万唤始出来,主角正式登场了!


    可以这么说,上面写到的所有规范规则,都是为了更好的设计和实现这个主角 ——— CI/CD。


    首先了解一下,什么是 CI/CD ?


    核心概念,CI(Continuous Integration)译为持续集成,CD 包括两部分,持续交付(Continuous Delivery)和持续部署(Continuous Deployment)


    从全局看,CI/CD 是一种通过自动化流程来频繁向客户交付应用的方法。这个流程贯穿了应用的集成,测试,交付和部署的整个生命周期,统称为 “CI/CD 管道”。


    虽然都是像流水线一样自动化的管道,但是 CI 和 CD 各有分工。


    持续集成是频繁地将代码集成到主干分支。当新代码提交,会自动执行构建、测试,测试通过则自动合并到主干分支,实现了产品快速迭代的同时保持高质量。


    持续交付是频繁地将软件的新版本,交付给质量团队或者用户,以供评审。评审通过则可以发布生产环境。持续交付要求代码(某个分支的最新提交)是随时可发布的状态。


    持续部署是代码通过评审后,自动部署到生产环境。持续部署要求代码(某个分支的最新提交)是随时可部署的。


    持续部署与持续交付的唯一区别,就是部署到生产环境这一步,是否是自动化


    部署自动化,看似是小小的一步,但是在实践过程中你会发现,这反而是 CI/CD 流水线中最难落实的一环。


    为什么?首先,从持续集成到持续交付,这些个环节都是由开发团队实施的。我们通过团队内部协作,产出了新版本的待发布的应用。


    然而将应用部署到服务器,这是运维团队的工作。我们要实现部署,就要与运维团队沟通,然而开发同学不了解服务器,运维同学不了解代码,沟通起来困难重重。


    再有,运维是手动部署,我们要实现自动部署,就要有服务器权限,与服务器交互。这也是个大问题,因为运维团队一定会顾虑安全问题,因而推动起来节节受阻。


    目前社区成熟的 CI/CD 方案有很多,比如老牌的 jenkins,react 使用的 circleci,还有我认为最好用的GitHub Action等,我们可以将这些方案接入到自己的系统当中。


    这篇文章篇幅已经很长了,就到这里结束吧。接下来我会基于 GitHub Action 单独出一篇详细的 react 前端项目 CI/CD 实践,记得关注我的专栏哦。



    作者:杨成功
    链接:https://juejin.cn/post/7024043015794589727

    收起阅读 »

    先睹为快即将到来的HTML6

    HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。 尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心...
    继续阅读 »

    HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。


    尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。


    支持原生模式


    该元素<dialog> 将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。


    这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。


    <dialog>
    <form method="dialog">
    <input type="submit" value="确定" />
    <input type="submit" value="取消" />
    </form>
    </dialog>

    在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。


    可以在 <dialog> 其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>


    总的来说,这个标签在用户交互和改进的界面中变得有益。


    可以通过更改 <dialog> 标签的 open 属性以控制打开和关闭。


    <dialog open>
    <p>组件内容</p>
    </dialog>

    没有 JavaScript 的单页应用程序


    FutureClaw 杂志主编 Bobby Mozumder 建议:



    将锚元素链接到 JSON/XML、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将 DOM 元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。



    据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。


    自由调整图像大小


    HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。


    每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce 标签 img 在处理这个问题时不是很有效。


    这个问题可以通过一个新标签 <srcset> 来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。


    专用库


    将可用库引入 HTML6 绝对是提高开发效率的重要一步。


    微格式


    很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。


    自定义菜单


    尽管标签<ul><ol>非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。


    这就是创建标签 <menu> 的驱动力,它可以处理按钮驱动的列表元素。


    <menu type="toolbar">
    <li><button>个人信息</button></li>
    <li><button>系统设置</button></li>
    <li><button>账号注销</button></li>
    </menu>

    因此 <menu>,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。


    增强身份验证


    虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。


    集成摄像头


    HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。


    总结


    没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。


    作者:天行无忌
    链接:https://juejin.cn/post/7032874253573685261

    收起阅读 »

    先睹为快即将到来的HTML6

    HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。 尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核...
    继续阅读 »
    HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。

    尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。

    支持原生模式

    该元素<dialog> 将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。

    这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。

    <dialog>
    <form method="dialog">
      <input type="submit" value="确定" />
      <input type="submit" value="取消" />
    </form>
    </dialog>

    在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。

    可以在 <dialog> 其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>

    总的来说,这个标签在用户交互和改进的界面中变得有益。

    可以通过更改 <dialog> 标签的 open 属性以控制打开和关闭。

    <dialog open>
    <p>组件内容</p>
    </dialog>

    没有 JavaScript 的单页应用程序

    FutureClaw 杂志主编 Bobby Mozumder 建议:

    将锚元素链接到 JSON/XML、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将 DOM 元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。

    据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。

    自由调整图像大小

    HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。

    每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce 标签 img 在处理这个问题时不是很有效。

    这个问题可以通过一个新标签 <srcset> 来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。

    专用库

    将可用库引入 HTML6 绝对是提高开发效率的重要一步。

    微格式

    很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。

    自定义菜单

    尽管标签<ul><ol>非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。

    这就是创建标签 <menu> 的驱动力,它可以处理按钮驱动的列表元素。

    <menu type="toolbar">
    <li><button>个人信息</button></li>
    <li><button>系统设置</button></li>
    <li><button>账号注销</button></li>
    </menu>

    因此 <menu>,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。

    增强身份验证

    虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。

    集成摄像头

    HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。

    总结

    没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。

    作者:天行无忌
    来源:https://juejin.cn/post/7032874253573685261

    收起阅读 »

    是时候封装一个DOM库了

    增首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步而这里,只需要一步就能完成dom.create('hi') 它可以直接创建多标签的嵌套,如create('你好') 为什么能这样写? 因为我们用inn...
    继续阅读 »

    由于原始的DOM提供的API过长,不方便记忆,
    于是我采用对象风格的形式封装了一个DOM库->源代码链接
    这里对新封装的API进行总结
    同样,用增删改查进行划分,我们先提供一个全局的window.dom对象

    创建节点

    create (string){
           const container = document.createElement("template")//template可以容纳任意元素
           container.innerHTML = string.trim();//去除字符串两边空格
           return container.content.firstChild;//用template,里面的元素必须这样获取  
    }

    首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步

    1. document.createElement('div')

    2. div.innerText = 'hi'

    而这里,只需要一步就能完成dom.create('

    hi
    ')
    它可以直接创建多标签的嵌套,如create('
    你好
    ')

    为什么能这样写?
    因为我们用innerHTML直接把字符串写进了HTML里,字符串直接变成了HTML里面的内容
    为什么使用template?
    因为template可以容纳任意元素,如果使用div,div不能直接容纳标签,但template就可以

    新增哥哥

    before(node,node2){
       node.parentNode.insertBefore(node2,node);
    }

    这个比较简单,找到爸爸节点,然后使用```JavaScript,新增一个node2即可

    新增弟弟

    after(node,node2){
       node.parentNode.insertBefore(node2,node.nextSibling); //把node2插到node下一个节点的前面,即使node的下一个节点为空,也能插入  
    }

    由于原始的DOM只有insertBefore,并没有insertAfter,所以要实现这个功能我们需要一个曲线救国的方法:
    node.nextSibling 表示node节点的下一个节点,
    而想在node的后面插入一个节点,就等于说在node的下一个节点前插入一个新节点node2即可
    如上代码就是实现了这个操作,而即使node的下一个节点为空,也能成功插入

    新增儿子

     append(parent,node){
           parent.appendChild(node)
    }

    找到爸爸节点,用appendChild即可

    新增爸爸

     wrap(node,parent){
           dom.before(node,parent)
           dom.append(parent,node)
    }

    思路如图:

    image.png

    分为两步走:

    1. 先把新增的爸爸节点,放到老节点的前面

    2. 再把老节点放入新增的爸爸节点的里面

    这样就可以使新的爸爸节点包裹住老节点

    使用示例:

    const newDiv = dom.create('
    '
    )

    dom.wrap(test, newDiv)

    删节点

    remove(node){
          node.parentNode.removeChild(node)
          return node
    }

    找到爸爸节点,removeChild即可

    删除所有子节点

    empty(node){
       const array = []
       let x = node.firstChild
       while (x) {
           array.push(dom.remove(node.firstChild))
           x = node.firstChild//x指向下一个节点
      }
       return array
    }

    其实一开始的思路,是用for循环

    for(let i = 0;i<childNodes.length;i++){
       dom.remove(childNodes[i])
    }

    但这样的思路有一个问题:childNodes.length是会随着删除而变化的
    所以我们需要改变思路,用while循环:

    1. 先找到该节点的第一个儿子赋值为x

    2. 当x是存在的,我们就把它移除,并放入数组里面(用于获取删除的节点的引用)

    3. 再把x赋值给它的下一个节点(当第一个儿子被删除后,下一个儿子就变成了第一个儿子)

    4. 反复操作,直到所有子节点被删完

    读写属性

     attr(node,name,value){
           if(arguments.length === 3){
               node.setAttribute(name,value)
          }else if(arguments.length === 2){
               return node.getAttribute(name)
          }
    }

    这里运用重载,实现两种不同的功能:

    1. 当输入的参数是3个时,就写属性

    2. 当如数的参数是2个时,读属性

    使用示例:

    //写:
    //给
    test
    添加属性

    dom.attr(test,'title','Hi,I am Wang')
    //添加之后:
    test

    //读:
    const title = dom.attr(test,'title')
    console.log(`title:${title}`)
    //打印出:title:Hi,Hi,I am Wang

    读写文本内容

    text(node,string){
       if(arguments.length === 2){
           if('innerText' in node){
               node.innerText = string
          }else{
               node.textContent = string
          }
      }else if(arguments.length === 1){
           if('innerText' in node){
               return node.innerText
          }else{
               return node.textContent
          }
      }
    }

    为什么这里需要适配,innerText与textContent?
    因为虽然现在绝大多数浏览器都支持两种,但还是有非常旧的IE只支持innerText,所以这里是为了适配所有浏览器

    同时与读写属性思路相同:

    1. 当输入的参数是2个时,就在节点里写文本

    2. 当输入的参数是1个时,就读文本内容

    读写HTML的内容

    html(node,string){
       if(arguments.length === 2){
           node.innerHTML = string
      }else if(arguments.length === 1){
               return node.innerHTML
      }
    }

    同样,2参数写内容,1参数读内容

    修改Style

    style(node,name,value){
           if(arguments.length === 3){
               //dom.style(div,'color','red')
               node.style[name] = value
          }else if(arguments.length === 2){
               if(typeof name === 'string'){
                //dom.style(div,'color')
               return node.style[name]    
              }else if(name instanceof Object){
                   //dom.style(div,{color:'red'})
                   const Object = name
                   for(let key in Object){
                       //key:border/color
                       //node.style.border = ...
                       //node.style.color = ...
                       node.style[key] = Object[key]
                  }
              }
          }
    }

    思路:

    1. 首先判断输入的参数,如果为3个如:dom.style(div,'color','red')

    2. 就更改它的style

    3. 如果输入参数为2个时,先判断输入name的值的类型

    4. 如果是字符串,如dom.style(div,'color'),就返回style的属性

    5. 如果是对象,如dom.style(div,{border:'1px solid red',color:'blue'}),就更改它的style

    增删查class

    class:{
       add(node,className){
           node.classList.add(className)    
      },
       remove(node,className){
           node.classList.remove(className)
      },
       has(node,className){
           return node.classList.contains(className)
      }
    }

    注:查找一个元素的classList里是否有某一个class, 用的是contains

    添加事件监听

    on(node,eventName,fn){
          node.addEventListener(eventName,fn)
    }

    使用示例:

    const fn = ()=>{
       console.log('点击了')
    }
    dom.on(test,'click',fn)

    这样当点击id为test的div时,就会打印出'点击了'

    删除事件监听

    off(node,eventName,fn){
       node.removeEventListener(eventName,fn)
    }

    获取单个或多个标签

    find(selector,scope){
       return (scope || document).querySelectorAll(selector)
    }

    可以在指定区域或者全局的document里找

    使用示例:
    在document中查询:

    const testDiv = dom.find('#test')[0]
    console.log(testDiv)

    在指定范围内查询:

     <div>
           <div id="test"><span>test1span>
           <p class="red">段落标签p>
           div>
           <div id="test2">
               <p class="red">段落标签p>
           div>
    div>

    我只想找test2里面的red,应该怎么做

    const test2 = dom.find('#test2')[0]
    console.log(dom.find('.red',test2)[0])

    注意:末尾的[0]别忘记写

    获取父元素

    parent(node){
       return node.parentNode
    }

    获取子元素

    children(node){
       return node.children
    }

    获取兄弟姐妹元素

    siblings(node){
       return Array.from(node.parentNode.children).filter(n=>n!==node) //伪数组变数组再过滤本身
    }

    找到爸爸节点,然后过滤掉自己本身

    获取弟弟

    next(node){
       let x = node.nextSibling
       while(x && x.nodeType === 3){
           x = x.nextSibling
      }
       return x
    }

    为什么这里需要while(x && x.nodeType === 3)
    因为我们不想获取文本节点(空格回车等)
    所以当读到文本节点时,自动再去读取下一个节点,直到读到的内容不是文本节点为止

    获取哥哥

    previous(node){
           let x = node.previousSibling
           while(x && x.nodeType === 3){
               x = x.previousSibling
          }
           return x
    }

    与上面思路相同

    遍历所有节点

    each(nodeList,fn){
           for(let i=0;i<nodeList.length;i++){
               fn.call(null,nodeList[i])
          }
    }

    注:null用于填充this的位置
    使用示例:
    利用fn可以更改所有节点的style

    const t = dom.find('#travel')[0]
    dom.each(dom.children(t),(n)=>dom.style(n,'color','red'))

    遍历每个节点,把每个节点的style都更改

    用于获取排行老几

    index(node){
       const list = dom.children(node.parentNode)
       let i;
       for(i=0;i<list.length;i++){
           if(list[i]===node){
                break
          }
      }
       return i
    }

    思路:

    1. 获取爸爸节点的所有儿子

    2. 设置一个变量i

    3. 如果i等于想要查询的node

    4. 退出循环,返回i值

    作者:PrayWang
    来源:https://juejin.cn/post/7038171258617331719

    收起阅读 »

    这一次,彻底搞懂 async...await

    执行 async 函数,返回的都是 Promise 对象Promise.then() 对应 awaitPromise.catch() 对应 try...catch先看下面两个函数:async function test1() {  return 1;...
    继续阅读 »

    先上结论:

    • 执行 async 函数,返回的都是 Promise 对象

    • Promise.then() 对应 await

    • Promise.catch() 对应 try...catch

    执行 async 函数,返回的都是 Promise 对象

    先看下面两个函数:

    async function test1() {
     return 1;
    }
    async function test2() {
     return Promise.resolve(2);
    }
    const res1 = test1();
    const res2 = test2();
    console.log('res1', res1);
    console.log('res1', res2);

    test1 和 test2 两个函数前面都加了 async,说明这两个都是异步函数,并且如果一个函数前加了 async,那么这个函数的返回值就是一个 Promise(不论这个函数返回的是什么,都会被 JS 引擎包装成 Promise 对象)。

    输出结果如下图:

    Promise.then() 对应 await

    1.直接在一个 await 后面加 promise 对象

    看下面的代码:

    async function test3() {
     const p3 = Promise.resolve(3);
     p3.then(data => {
       console.log('data', data);
    });

     const data = await p3;
     console.log('data', data);
    }
    test3();

    输出结果如下图:

    可以看到输出是相同的,这就说明了 Promise 的 then() 方法对应 await。

    2.直接在一个 await 后面加一个基本数据类型的值

    看下面的例子:

    async function test4() {
     const data4 = await 4; // await Promise.resolve(4)
     console.log('data4', data4);
    }
    test4();

    输出结果如下图:

    可以看到输出的是 4,上面的 await 4 就相当于 await Promise.resolve(4),又因为 await 相当于 then(),所以输出的就是 4。

    3.直接在一个 await 后面加一个异步函数

    看下面的例子:

    async function test1() {
     return 1;
    }
    async function test5() {
     const data5 = await test1();
     console.log('data5', data5);
    }
    test5();

    输出结果如下图:

    可以看到输出的是 1,首先 test5() 执行,然后执行 test1(),test1 返回数字 1,相当于返回 Promise.resolve(1),await 又相当于 then(),所以输出 1。

    :::tip 提示 开发中最常用的就是第三种,await 后面跟一个异步函数,所以一定要掌握! :::

    Promise.catch() 对应 try...catch

    看下面的例子:

    async function test6() {
     const p6 = Promise.reject(6);
     const data6 = await p6;
     console.log('data6', data6);
    }
    test6();

    输出结果如下图:

    可以看到没有捕获到错误,那应该怎么做呢?没错,可以使用 try...catch。 看下面的例子:

    async function test6() {
     const p6 = Promise.reject(6);
     try {
       const data6 = await p6;
       console.log('data6', data6);
    } catch (e) {
       console.log('e', e); // 顺利捕获错误
    }
    }
    test6();

    输出结果如下图:

    可以看到已经成功捕获到错误了!

    作者:ShiYan_Chen
    来源:https://juejin.cn/post/7038152028664627230

    收起阅读 »

    会话过期后token刷新,重新请求接口(订阅发布模式)

    需求响应拦截拦截到302后,我们进入到刷新token逻辑我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,...
    继续阅读 »

    前言


    ❝ 最近,我们老大让小白搞一下登录模块,登录模块说简单也简单,复杂也复杂。本章主要讲一下,会话过期后,token 刷新的一系列的事。 ❞

    需求

    在一个页面内,当请求失败并且返回 302 后,判断是接口过期还是登录过期,如果是接口过期,则去请求新的token,然后拿新的token去再次发起请求.

    思路


    • 当初,想了一个黑科技(为了偷懒),就是拿到新的token后,直接强制刷新页面,这样一个页面内的接口就自动刷新啦~(方便是方便,用户体验却不好)

    • 目前,想到了重新请求接口时,可以配合订阅发布模式来提高用户体验

    响应拦截

    首先我们发起一个请求 axios({url:'/test',data:xxx}).then(res=>{})

    拦截到302后,我们进入到刷新token逻辑

    响应拦截代码

    axios.interceptors.response.use(
       function (response) {
           if (response.status == 200) {
               return response;
          }
      },
      (err) => {
           //刷新token
           let res = err.response || {};
           if (res.data.meta?.statusCode == 302) {
               return refeshToken(res);
          } else {  
               return err;
          }
      }
    );

    我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~

    刷新token方法

    //避免其他接口同时请求(只请求一次token接口)
    let isRefreshToken = false;
    const refeshToken = (response) => {
      if (!isRefreshToken) {
               isRefreshToken = true;
               axios({
                   //获取新token接口
                   url: `/api/refreshToken`,
              })
                  .then((res) => {
                       const { data = '', meta = {} } = res.data;
                       if (meta.statusCode === 200) {
                           isRefreshToken = false;
                           //发布 消息
                           retryOldRequest.trigger(data);
                      } else {
                           history.push('/user/login');
                      }
                  })
                  .catch((err) => {
                       history.push('/user/login');
                  });
          }
           //收集订阅者 并把成功后的数据返回原接口
           return retryOldRequest.listen(response);
    };

    看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,这就是我们男二 订阅发布模式队列。

    订阅发布模式


    大家如果还不了解订阅发布模式,可以点击看一下,里面有大神写的通俗易懂的例子(觉得学到的话,可以顺便帮点赞哦~)。

    把失败的接口当订阅者,成功拿到新的token后再发布(重新请求接口)。

    以下便是订阅发布模式代码

    const retryOldRequest = {
       //维护失败请求的response
       requestQuery: [],

       //添加订阅者
       listen(response) {
           return new Promise((resolve) => {
               this.requestQuery.push((newToken) => {
                   let config = response.config || {};
                   //Authorization是传给后台的身份令牌
                   config.headers['Authorization'] = newToken;
                   resolve(axios(config));
              });
          });
      },

       //发布消息
       trigger(newToken) {
           this.requestQuery.forEach((fn) => {
               fn(newToken);
          });
           this.requestQuery = [];
      },
    };

    大家可以先不用关注订阅者的逻辑,只需要知道订阅者是每次请求失败后的接口(reponse)就好了。

    每次进入refeshToken方法,我们失败的接口都会触发retryOldRequest.listen去订阅,而我们的requestQuery则是保存这些订阅者的队列。

    注意:我们订阅者队列requestQuery是保存待发布的方法。而在成功获取新token后,retryOldRequest.trigger就会去发布这些消息(新token)给订阅者(触发订阅队列的方法)。

    而订阅者(response)里面有config配置,我们拿到新的token后(发布后),修改config里面的请求头Autorzation.而借助Promise我们可以更好的拿到新token请求回来的接口数据,一旦请求到数据,我们可以原封不动的返回给原来的接口/test了(因为我们在响应拦截那里返回的是refreshToken,而refreshToken又返回的是订阅者retryOldRequest.listen返回的数据,而Listiner又返回Promise的数据,Promise又在成功请求后resolve出去)。

    看到这,小伙伴们是不是觉得有点绕了~

    而在真实开发中,我们的逻辑还含有登录过期(与请求过期区分开来)。我们是根据当前时间 - 过去时间 < expiresTime(epiresTime:登录后返回的有效时间)来判断是请求过期还是登录过期的。 以下是完整逻辑
    以下是完整代码

    const retryOldRequest = {
       //维护失败请求的response
       requestQuery: [],

       //添加订阅者
       listen(response) {
           return new Promise((resolve) => {
               this.requestQuery.push((newToken) => {
                   let config = response.config || {};
                   config.headers['Authorization'] = newToken;
                   resolve(axios(config));
              });
          });
      },

       //发布消息
       trigger(newToken) {
           this.requestQuery.forEach((fn) => {
               fn(newToken);
          });
           this.requestQuery = [];
      },
    };
    /**
    * sessionExpiredTips
    * 会话过期:
    * 刷新token失败,得重新登录
    * 用户未授权,页面跳转到登录页面
    * 接口过期 => 刷新token
    * 登录过期 => 重新登录
    * expiresTime => 在本业务中返回18000ms == 5h
    * ****/

    //避免其他接口同时请求
    let isRefreshToken = false;
    let timer = null;
    const refeshToken = (response) => {
       //登录后拿到的有效期
       let userExpir = localStorage.getItem('expiresTime');
       //当前时间
       let nowTime = Math.floor(new Date().getTime() / 1000);
       //最后请求的时间
       let lastResTime = localStorage.getItem('lastResponseTime') || nowTime;
       let token = localStorage.getItem('token');

       if (token && nowTime - lastResTime < userExpir) {
           if (!isRefreshToken) {
               isRefreshToken = true;
               axios({
                   url: `/api/refreshToken`,
              })
                  .then((res) => {
                       const { data = '', meta = {} } = res.data;
                       isRefreshToken = false;
                       if (meta.statusCode === 200) {
                           localStorage.getItem('token', data);
                           localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000)
                          );
                           //发布 消息
                           retryOldRequest.trigger(data);
                      } else {
                          //去登录
                      }
                  })
                  .catch((err) => {
                       isRefreshToken = false;
                      //去登录
                  });
          }
           //收集订阅者 并把成功后的数据返回原接口
           return retryOldRequest.listen(response);
      } else {
           //节流:避免重复运行
          //去登录
      }
    };

    // http response 响应拦截
    axios.interceptors.response.use(
       function (response) {
           if (response.status == 200) {
               //记录最后操作时间
              localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000));
               return response;
          }
      },
      (err) => {
           let res = err.response || {};
           if (res.data.meta?.statusCode == 302) {
               return refeshToken(res);
          } else {
               //302 报的错误;
               return err;
          }
      }
    );

    以上便是我们这边的业务,如果写的不好请大佬多担待~~

    如果有好方案的小伙伴也可以在评论区内互相讨论~


    作者:用户3797421129853
    来源:https://juejin.cn/post/7037787299202990093

    收起阅读 »

    通过 Performance 证明,网页的渲染是一个宏任务

    别着急反驳,后面我会给出证据。调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。但是性能分析的调试工具却不能这样做...
    继续阅读 »

    网页的渲染是一个宏任务。 这是我下的一个结论。

    别着急反驳,后面我会给出证据。

    我们先来聊下什么是调试:

    调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。

    我们最常用的调试工具是 JS Debugger,它支持断点,可以在某处断住,查看当前上下文的变量、调用栈等,这对于理清逻辑很有帮助。

    但是性能分析的调试工具却不能这样做,不能用断住的方式实时查看,因为会影响数据的真实性。所以这类工具都是通过录制一段时间的数据,然后作事后的统计和分析的方式,常用的是 Chrome Devtools 里的 Performance 工具。(甚至为了避免浏览器插件的影响,还要用无痕模式来运行网页)点击录制按钮 record 开始录制(如果想录制从页面加载开始的数据,就点击 reload 按钮),Performance 会记录下录制时间内各方面的数据。

    有哪些数据呢?

    网页的运行是有多个线程的,主线程负责通过 Event Loop 的方式来不断的执行 JS 和渲染,也有一些别的线程,比如合成渲染图层的线程,Web Worker 的线程等,渲染的每一帧会绘制到界面上。

    网页是这样运行的,那记录的自然也都是这些数据:Performance 会记录网页的每个线程的数据,其中最重要的是主线程,也就是图中的 Main,这部分记录着 Event Loop 的执行过程,记录着 JS 执行的调用栈和页面渲染的流程。看到图中标出的一个个小灰块了么,那就是一个个 Task,也就是宏任务。Event Loop 就是循环执行宏任务。每个 Task 都有自己的调用栈,可以看到函数的执行路径,耗时等信息。图中宽度代表了耗时,可以直观的通过块的宽窄来分析性能。

    执行完宏任务会执行所有的微任务,在图中也可以清晰的看到:点击每一个块可以看到代码的位置,可以定位到对应代码,这样就可以分析出哪块代码性能不好。这些是 Main 线程的执行逻辑,也就是通过 Event Loop 来不断执行 JS 和渲染。

    当然,还有其他线程,比如光栅化线程,也就是负责把渲染出的图层合并成一帧的线程:总之,就像 Debugger 面前,JS 的执行过程没有秘密一样,在 Performance 面前,网页各线程的执行过程也没有秘密。

    说了这么多,就是为了讲清楚调试工具和 Performance 都是干啥的,它记录了哪些信息。

    我们想知道渲染是不是一个宏任务,自然可以通过 Performance 来轻易的分析出来。

    我们继续看 Main 线程的 Event Loop 执行过程:你会看到一个个很小的灰块,也就是一个个 Task,每隔一段时间都会执行,点击它,就会看到其实他做的就是渲染,包括计算布局,更新渲染树,合并图层、渲染等。

    这说明了什么,不就说明了渲染是一个宏任务么。

    所以,我们得到了结论:渲染是一个宏任务,通过 Event Loop 来做一帧帧的渲染。

    通过 Performance 调试工具,我们可以看到 Main 线程 Event Loop 的细节,看到 JS 执行和渲染的详细过程。

    有时你可能会看到有的 Task 部分被标红了,还警告说这是 Long Task。因为渲染和 JS 执行都是在同一个 Event Loop 内做的,那如果有执行时间过长的 Task,自然会导致渲染被延后,也就是掉帧,用户感受到的就是页面的卡顿。

    避免 Long Task,这是网页性能优化的一个重点。这也是为什么 React 使用了 Fiber 架构的可打断的组件树渲染,替代掉了之前的递归渲染整个组件树的方式,就是为了不产生 Long Task。

    总结

    本文目的为了证明渲染是不是一个宏任务,但其实更重要的是想讲清楚调试工具的意义。

    调试工具可以分析程序运行过程中某一刻或某一段时间的各方面的数据,有两种方式:一种是 Debugger 那种断点的方式,可以看到上下文变量的值、调用栈,可以帮助理清逻辑、定位问题。而性能分析工具则是另一种方式,通过录制一段时间内的各种数据,做事后的分析和统计,这样能保证数据的真实性。

    网页的性能分析工具 Performance 可以记录网页执行过程中的各个线程的执行情况,主要是主线程的 Event Loop 的执行过程,包括 JS 执行、渲染等。

    通过 Performance,我们可以轻易的得出“渲染是一个宏任务”的结论。

    就像在 Debugger 面前,JS 执行过程没有秘密一样。在 Performance 面前,网页的执行过程也同样没有秘密。

    作者:zxg_神说要有光
    来源:https://juejin.cn/post/7037839989018722340

    收起阅读 »