注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

这些都能成为 Web 语法规范,强迫症看不下去了

JavaScript 一直是饱受诟病,源于网景公司在 1995 年用了 10 天的时间创造。没有什么能用 10 天创造就是完美的,可是某些特性一旦发布,错误或不完善的地方迅速成为必不可少的特色,并且是几乎不可能改变。 Javascript 的发展非常快,根本没...
继续阅读 »

JavaScript 一直是饱受诟病,源于网景公司在 1995 年用了 10 天的时间创造。没有什么能用 10 天创造就是完美的,可是某些特性一旦发布,错误或不完善的地方迅速成为必不可少的特色,并且是几乎不可能改变。


Javascript 的发展非常快,根本没有时间调整设计。在推出一年半之后,国际标准就问世了。设计缺陷还没有充分暴露就成了标准。


历史遗留


比如常见的历史设计缺陷:



  • nullundefined 两者非常容易混淆

  • == 类型转换的问题

  • var 声明创建全局变量

  • 自动插入行尾分号

  • 加号可以表示数字之和,也可以表示字符的连接

  • NaN 奇怪的特性

  • 更多...


Javascript 很多不严谨的特性我们可以添加 eslint 来规避。比如禁用 var== 成了大多数人写代码的必备条件。


现在/未来


如今 CSS、DOM、HTML 规范由 W3C 来制定,JavaScript 规范由 TC39 制定。那些历史缺陷也成为了过去,但是现在也出现了一些不尽人意的规范。


CSS 变量


声明变量的时候,变量名前面要加两根连词线 --


body {
--foo: #7f583f;
--bar: #f7efd2;
}

var() 函数用于读取变量。


a {
color: var(--foo);
text-decoration-color: var(--bar, #7f583f);
}

为什么选择两根连词线(--)表示变量?因为 $Sass 用掉,@Less 用掉。_-,用作为 IEchrome 兼容写法。CSS 中已经找不出来字符可以代替变量声明了。为了不产生冲突,官方的 CSS 变量就改用两根连词线。


作为一个官方的标准规范,时刻影响后面的行业发展。竟然能被第三方的插件所左右,令人大跌眼镜。有开发者吐槽:微软的架构师也是够窝囊。


现在很多应用都放弃了 Sassless,转向了 PostCSS 的怀抱。面向组件编程,根本用不到 Sassless 里面的一些复杂功能。那么 -- 两个字符的繁琐将成为开发者永远的痛。


类私有属性(proposal-class-fields)


JavaScript 中的 class 大家已经不陌生了,简直跟 Javaclass 一模一样。


基本用法:


class BaseClass {
msg = 'hello world';

basePublicMethod() {
return this.msg;
}
}

继承:


class SubClass extends BaseClass {
subPublicMethod() {
return super.basePublicMethod();
}
}

静态属性:


class ClassWithStaticField {
static baseStaticMethod() {
return 'base static method output';
}
}

异步方法


class ClassWithFancyMethods {
*generatorMethod() {}
async asyncMethod() {}
async *asyncGeneratorMethod() {}
}

而类私有属性的提案目前已经进入标准,它用了 # 关键字前缀来修饰一个类的属性。


class ClassWithPrivateField {
#privateField;

constructor() {
this.#privateField = 42;
}
}

你没看错,不是 typescript 中的 private 关键字。


class BaseClass {
readonly msg = 'hello world';

private basePrivateMethod() {
return this.msg;
}
}

然而 # 的语法丑陋本身引起了社区的争议:



「class fields 提案提供了一个极具争议的私有字段访问语法——并成功地做对了唯一一件事情,让社区把全部的争议焦点放在了这个语法上」。




TS 投降主义已经被迫实现了。




No dynamic access, no destructuring is a deal breaker for me




我们制作一个 eslint 插件 no-private-class-fields 并使用下载计数来说明社区反对




'#' 作为名称的一部分会导致混淆,因为 this.#x !== this['#x'] 太奇怪了



前端架构师、TC39 成员贺师俊也在知乎连发好几篇文章吐槽 class fields


不妨大家看看关于 private 的 side: johnhax.net/2017/js-pri…


提案地址:github.com/tc39/propos…


globalThis


在不同的 JavaScript 环境中拿到全局对象是需要不同的语句的。在 Web 中,可以通过 windowself 取到全局对象,但是在 Web Workers 中只有 self 可以。在 Node.js 中,必须使用 global。非严格模式下,可以在函数中返回 this 来获取全局对象,否则会返回 undefined


因此一个叫 global 的提案出现。主要用 global 变量统一上面的行为,但后面绕来绕去改成了 globalThis,引起了激烈讨论。


globalThis 这个名字会让 this 变得更加复杂。



  1. this 一直是困扰程序员的话题,尤其是 JavaScript 新手,关于它的博客文章源源不断

  2. ES6 让事情变得更简单,因为可以告诉人们更喜欢箭头函数并且只使用 this 内部方法定义

  3. 在现代 JS(modules) 中,并没有真正的全局 this,所以 globalThis 甚至不引用现有的概念


现在说这一切都是徒劳的,因为它已经进入 stage 4


提案地址:github.com/tc39/propos…


总结


JavaScript 中遗留的糟粕太多。现在受到这些糟粕的影响,很多新的提案又不得不妥协。在未来,它会变得极其复杂。


也许某一天,会出现一个没有历史包袱的 JavaScript 子集来替换它。



作者:MinJie
链接:https://juejin.cn/post/7043340139049222152

收起阅读 »

13 行 JavaScript 代码让你看起来像是高手

Javascript 可以做许多神奇的事情,也有很多东西需要学习,今天我们介绍几个短小精悍的代码段。 获取随机布尔值(True/False) 使用 Math.random() 会返回 0 到 1 的随机数,之后判断它是否大于 0.5,将会得到一个 50% 概率...
继续阅读 »

Javascript 可以做许多神奇的事情,也有很多东西需要学习,今天我们介绍几个短小精悍的代码段。


获取随机布尔值(True/False)


使用 Math.random() 会返回 0 到 1 的随机数,之后判断它是否大于 0.5,将会得到一个 50% 概率为 TrueFalse 的值


const randomBoolean = () => Math.random() >= 0.5;
console.log(randomBoolean());

判断一个日期是否是工作日


判断给定的日期是否是工作日


const isWeekday = (date) => date.getDay() % 6 !== 0;
console.log(isWeekday(new Date(2021, 0, 11)));
// Result: true (周一)
console.log(isWeekday(new Date(2021, 0, 10)));
// Result: false (周日)

反转字符串


有许多反转字符串的方法,这里使用一种最简单的,使用了 split()reverse()join()


const reverse = str => str.split('').reverse().join('');
reverse('hello world');
// Result: 'dlrow olleh'

判断当前标签页是否为可视状态


浏览器可以打开很多标签页,下面 👇🏻 的代码段就是判断当前标签页是否是激活的标签页


const isBrowserTabInView = () => document.hidden;
isBrowserTabInView();

判断数字为奇数或者偶数


取模运算符 % 可以很好地完成这个任务


const isEven = num => num % 2 === 0;
console.log(isEven(2));
// Result: true
console.log(isEven(3));
// Result: false

从 Date 对象中获取时间


使用 Date 对象的 .toTimeString() 方法转换为时间字符串,之后截取字符串即可


const timeFromDate = date => date.toTimeString().slice(0, 8);
console.log(timeFromDate(new Date(2021, 0, 10, 17, 30, 0)));
// Result: "17:30:00"
console.log(timeFromDate(new Date()));
// Result: 返回当前时间

保留指定的小数位


const toFixed = (n, fixed) => ~~(Math.pow(10, fixed) * n) / Math.pow(10, fixed);
// Examples
toFixed(25.198726354, 1); // 25.1
toFixed(25.198726354, 2); // 25.19
toFixed(25.198726354, 3); // 25.198
toFixed(25.198726354, 4); // 25.1987
toFixed(25.198726354, 5); // 25.19872
toFixed(25.198726354, 6); // 25.198726

检查指定元素是否处于聚焦状态


可以使用 document.activeElement 来判断元素是否处于聚焦状态


const elementIsInFocus = (el) => (el === document.activeElement);
elementIsInFocus(anyElement)
// Result: 如果处于焦点状态会返回 True 否则返回 False

检查当前用户是否支持触摸事件


const touchSupported = () => {
('ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch);
}
console.log(touchSupported());
// Result: 如果支持触摸事件会返回 True 否则返回 False

检查当前用户是否是苹果设备


可以使用 navigator.platform 判断当前用户是否是苹果设备


const isAppleDevice = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
console.log(isAppleDevice);
// Result: 是苹果设备会返回 True

滚动至页面顶部


window.scrollTo() 会滚动至指定的坐标,如果设置坐标为(0,0),就会回到页面顶部


const goToTop = () => window.scrollTo(0, 0);
goToTop();
// Result: 将会滚动至顶部

获取所有参数的平均值


可以使用 reduce() 函数来计算所有参数的平均值


const average = (...args) => args.reduce((a, b) => a + b) / args.length;
average(1, 2, 3, 4);
// Result: 2.5

转换华氏/摄氏


再也不怕处理温度单位了,下面两个函数是两个温度单位的相互转换。


const celsiusToFahrenheit = (celsius) => celsius * 9/5 + 32;
const fahrenheitToCelsius = (fahrenheit) => (fahrenheit - 32) * 5/9;
// Examples
celsiusToFahrenheit(15); // 59
celsiusToFahrenheit(0); // 32
celsiusToFahrenheit(-20); // -4
fahrenheitToCelsius(59); // 15
fahrenheitToCelsius(32); // 0

感谢阅读,希望你会有所收获😄


作者:夜色镇歌
链接:https://juejin.cn/post/7043062481954013197

收起阅读 »

美团跨端一体化富文本管理技术实践

为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。一、引言在互联网圈,开发和产品经理...
继续阅读 »



为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。

一、引言

在互联网圈,开发和产品经理之间相爱相杀的故事,相信大家都有所耳闻。归根结底,往往都是从简单的改需求开始,然后你来我往、互不相让,接着吵架斗嘴,最后导致矛盾不断升级,甚至带来比较严重的后果。

pic_4bf1ae9b.png

图1

在这种背景下,如果把一些功能相对简单的、需求变动比较频繁的页面,直接交给产品或者运营自己去通过平台实现,是不是就可以从一定程度上减少产品和开发人员之间的矛盾呢?

二、背景

当然上述的情况,美团也不例外。近些年,美团到家事业群(包括美团外卖、美团配送、闪购、医药、团好货等)的各个业务稳步发展,业务前端对接的运营团队有近几十个,每个运营团队又有不同的运营规则,这些规则还存在一些细微的样式差别,同时规则内容还会随着运营季节、节日、地理位置等进行变化和更新。这些需求具体来说有以下几个特点:

  1. 需求量大:业务稳步发展,业务需求不断叠加,甚至部分业务呈指数级增长,且业务方向涉及到一些业务规则、消息通知、协议文档、规则介绍等需求。

  2. 变更频繁:面对市场监管和法务的要求,以及新业务调整等因素的影响,会涉及到需求的频繁变更,像一些业务FAQ、产品介绍、协议文档、业务规则、系统更新日志等页面,需要做到快速响应和及时上线。

  3. 复杂度低:这些页面没有复杂的交互逻辑,如果能把这些简单的页面交给运营/产品去实现,开发人员就能有更多的时间去进行复杂功能的研发。

  4. 时效性高:临时性业务需求较多,且生命周期较短,具有定期下线和周期性上线等特点。

基于以上特点,为了提高研发效率,美团医药技术部开始构建了一个跨端一体化富文本管理平台,希望提供解决这一大类问题的产研方案。不过,部门最初的目标是开发一套提效工具,解决大量诸如帮助文档、协议页、消息通知、规则说明等静态页面的生产与发布问题,让产品和运营同学能够以所见即所得的方式自主完成静态页面制作与发布,进而缩短沟通成本和研发成本。

但是,随着越来越多业务部门开始咨询并使用这个平台,我们后续不断完善并扩充了很多的功能。经过多次版本的设计和迭代开发后,将该平台命名为Page-佩奇,并且注册成为美团内部的公共服务,开始为美团内部更多同学提供更好的使用体验。

本文将系统地介绍Page-佩奇平台的定位、设计思路、实现原理及取得成效。我们也希望这些实战经验与总结,能给更多同学带来一些启发和思考。

三、跨端一体化富文本管理解决方案

3.1 平台定位

我们希望将Page-佩奇打造成一款为产品、运营、开发等用户提供快速一站式发布网页的产研工作台,这是对该平台的一个定位。

  • 对产品运营而言,他们能够可视化地去创建或修改一些活动说明、协议类、消息类的文章,无需开发排期,省去向开发二次传递消息等繁琐的流程,也无需等待漫长的发布时间,从而达到灵活快速地进行可视化页面的发布与管理。

  • 对开发同学而言,他们能够在线编写代码,并实现秒级的发布上线,并且支持ES 6、JavaScript 、Less、CSS语法,我们还提供了基础的工具、图表库等,能够生成丰富多样的页面。帮助开发同学快速实现数据图表展示,设计特定样式,完成各种交互逻辑等需求。

  • 对项目管理方而言,他们能够清晰地看到整个需求流转状态和开发日志信息,为运营管理提供强大的“抓手”。

一般来讲,传统开发流程是这样的:首先产品提出需求,然后召集研发评审,最后研发同学开发并且部署上线;当需求上线之后,如果有问题需要反馈,产品再找研发同学进行沟通并修复,这种开发流程也是目前互联网公司比较常见的开发流程。

pic_b81cd143.png

图2 传统开发流程图

而美团Page-佩奇平台的开发流程是:首先产品同学提出需求,然后自己在Page平台进行编辑和发布上线,当需求上线之后有问题需要反馈,直接就能触达到产品同学,他们通常可自行进行修复。如果需求需要定制化,或者需要做一些复杂的逻辑处理,那么再让研发人员配合在平台上进行开发并发布上线。

pic_b3e5d331.png

图3 Page-佩奇平台开发流程图

简单来说,对那些功能相对简单、需求变动比较频繁的页面,如果用传统的开发流程将会增加产研沟通和研发排期成本,因此传统方案主要适用于功能复杂型的需求。而Page-佩奇平台开发流程,并不适合功能复杂型的需求,特别适用于功能相对简单、需求变动比较频繁的页面需求。

综上所述,可以看出这两种开发流程其实起到了一个互补的作用,如果一起使用,既可以减少工作量,又可以达到降本提效的目的。

3.2 设计思路

我们最初设计Page-佩奇平台的初心其实很简单,为了给产品和运营提供一个通过富文本编辑器快速制作并发布网页的工具。但是,在使用的过程中,很多缺陷也就慢慢地开始暴露,大致有下面这些问题:

  1. 简单的富文本编辑器满足不了想要的页面效果,怎么办?

  2. 如果能导入想要的模板,是否会更友好?

  3. 怎么查看这个页面的访问数据?如何能监控这个页面的性能问题?

  4. 发布的页面是否有存在安全风险?

于是,我们针对这些问题进行了一些思考和调研:

  • 当富文本编辑器满足不了想要实现的效果的时候,可以引入了WebIDE编辑器,可以让研发同学再二次编辑进行实现。

  • 一个系统想要让用户用得高效便捷,那么就要完善它的周边生态。就需要配备完善的模板素材和物料供用户灵活选择。

  • 如果用户想要了解页面的运行情况,那么页面运行的性能数据、访问的数据也是必不可少的。

  • 如果发布的内容存在不当言论,就会造成不可控的法律风险,所以内容风险审核也是必不可少的。

实现一个功能很容易,但是想要实现一个相对完善的功能,就必须好好下功夫,多思考和多调研。于是,围绕着这些问题,我们不断挖掘和延伸出了一系列功能:

  1. 富文本编辑:强大而简单的可视化编辑器,让一切操作变得简单、直观。产品同学可以通过编辑器自主创建、编辑网页,即使无程序开发经验也可以通过富文本编辑器随意操作,实现自己想要的效果,最终可以实现一键快速发布上线。

  2. WebIDE:定制化需求,比如,与客户端和后端进行一些通信和请求需求,以及针对产品创建的HTML进行二次加工需求,均可以基于WebIDE通过JavaScript代码实现。具备专业开发经验的同学也可以选择通过前端框架jQuery、Vue,Echarts或者工具库Lodash、Axios实现在线编辑代码。

  3. 页面管理:灵活方便地管理页面。大家可以对有权限的文档进行查看、编辑、授权、下线、版本对比、操作日志、回滚等操作,且提供便捷的文档搜索功能。

  4. 模板市场:丰富多样的网页模板,简易而又具备个性。模板市场提供丰富的页面模板,大家可选择使用自己的模板快速创建网页,且发布的每个页面又可以作为自己的模板,再基于这个模板,可随时添加个性化的操作。

  5. 物料平台:提供基础Utils、Echart、Vue、jQuery等物料,方便开发基于产品的页面进行代码的二次开发。

  6. 多平台跨端接入:高效快捷地接入业务系统。通过通信SDK,其他系统可以快速接入Page-佩奇平台。同时支持以HTTP、Thrift方式的开放API供大家选择,支持客户端、后端调用开放API。

  7. 内容风险审核:严谨高效的审核机制。接入美团内部的风险审核公共服务,针对发布的风险内容将快速审核,防止误操作造成不可控的法律风险。

  8. 数据大盘:提供页面的数据监测,帮助大家时刻掌握流量动向。接入美团内部一站式数据分析平台,帮助大家安全、快速、高效地掌握页面的各种监测数据。

  9. 权限管理:创建的每个页面都有相对独立的权限,只有经过授权的人才能查看和操作该页面。

  10. 业务监控:提供页面级别JavaScript错误和资源加载成功率等数据,方便开发排查和解决线上问题。

功能流程图如下所示:

pic_9d007bab.png

图4 Page-佩奇平台功能流程图

3.3 实现原理

3.3.1 基础服务

Page-佩奇平台的基础服务有四个部分,包括物料服务、编译服务、产品赋能、扩展服务。

pic_f0bf9912.png

图5 整体架构图

3.3.2 核心架构

pic_bbb3bb9a.png

图6 核心架构图

Page-佩奇平台核心架构主要包含页面基础配置层、页面组装层以及页面生成层。我们通过Vuex全局状态对数据进行维护。

  • 页面基础配置层主要提供生成页面的各种能力,包括富文本的各种操作能力、编辑源码(HTML、CSS、JavaScript)的能力、自定义域名配置、适配的容器(PC/H5)、发布环境等。

  • 页面组装层则会基于基础配置层所提供的的能力,实现页面的自由编辑,承载大量的交互逻辑,用户的所有操作都在这一层进行。

    • 业务PV和UV埋点,错误统计,访问成功率上报。

    • 自动适配PC和移动端样式。

    • 内网页面显示外网不可访问标签。

  • 页面生成层则需要根据组装后的配置进行解析和预处理、编译等操作,最终生成HTML、CSS、JavaScript渲染到网页当中。

3.3.3 关键流程

pic_b5adef78.png

图7 关键流程图

如上图7所示,平台的核心流程主要包含页面创建之后的页面预览、编译服务、生成页面。

  • 页面预览:创建、编辑之后的页面,将会根据内容进行页面重组,对样式和JavaScript进行预编译之后,对文本+JavaScript+CSS进行组装,生成HTML代码块,然后将代码块转换成Blob URL,最终以iframe的方式预览页面。

  • 编译服务:文件树状结构和代码发送请求到后端接口,基于Webpack将Less编译成CSS,ES 6语法编译成ES 5。通用物料使用CDN进行引入,不再进行二次编译。

  • 生成页面:当创建、编辑之后的页面进行发布时,服务端将会进行代码质量检测、内容安全审查、代码质量检测、单元测试、上传对象存储平台、同步CDN检测,最终生成页面链接进行访问。

3.3.4 多平台接入

Page-佩奇平台也可以作为一个完善的富文本编辑器供业务系统使用,支持内嵌到其他系统内。作为消息发布等功能承载,减少重复的开发工作,同时我们配备完善的SDK供大家选择使用。通过Page-SDK可以直接触发Page平台发布、管理等操作,具体的流程如下图所示:

pic_e3c8e777.png

图8 Page-SDK流程图

3.3.5 Open API

在使用Page-佩奇平台的时候,美团内部一些业务方提出想要通过Page-佩奇平台进行页面的发布,同时想要拿到发布的内容做一些自定义的处理。于是,我们提供了Open API开放能力,支持以HTTP和Thrift两种方式进行调用。下面主要讲一下Thrift API实现的思路,首先我们先了解下Thrift整体流程:

pic_a6286e54.png

图9 Thrift整体流程图

Thrift的主要使用过程如下:

  1. 服务端预先编写接口定义语言 IDL(Interface Definition Language)文件定义接口。

  2. 使用Thrift提供的编译器,基于IDL编译出服务语言对应的接口文件。

  3. 被调用服务完成服务注册,调用发起服务完成服务发现。

  4. 采用统一传输协议进行服务调用与数据传输。

下面具体讲讲,Node语言是如何实现和其他服务语言实现调用的。由于我们的服务使用的Node语言,因此我们的Node服务就充当了服务端的角色,而其他语言(Java等)调用就充当了客户端的角色。

pic_ca36c11d.png

图10 Thrift使用详细流程图

  • 生成文件:由服务端定义IDL接口描述文件,然后基于IDL文件转换为对应语言的代码文件,由于我们用的是Node语言,所以转换成JavaScript文件。

  • 服务端启动服务:引入生成的JavaScript文件,解析接口、处理接口、启动并监听服务。

  • 服务注册:通过服务器内置的“服务治理代理”,将服务注册到美团内部的服务注册路由中心(也就是命名服务),让服务可被调用方发现。

  • 数据传输:被调用时,根据“服务治理服务”协议序列化和反序列化,与其他服务进行数据传输。

目前,美团内部已经有相对成熟的NPM包服务,已经帮我们实现了服务注册、数据传输、服务发现和获取流程。客户端如果想调用我们所提供的的Open API开放能力,首先申请AppKey,然后选择使用Thrift方式或者HTTP的方式,按照所要求的参数进行请求调用即可。

3.4 方案实践

3.4.1 H5协议

能力:富文本编辑。

描述:提供富文本可视化编辑,产品和运营无需前端就可以发布和二次编辑页面。

场景:文本协议,消息通知,产品FAQ。

具体案例:

pic_f883a4d1.png

图11 H5静态文本协议案例

3.4.2 业务自定义渲染

能力:开放API(Thirft + HTTP)。

描述:提供开放API,支持业务自定义和样式渲染到业务系统,同时解决了iframe体验问题。

场景:客户端、后端、小程序的同学,可根据API渲染文案,实现动态化管理富文本信息。

具体案例:

小程序使用组件、Vue使用v-html指令实现动态化渲染商品选择说明。

{
   "code": 0,
   "data": {
     "tag": "苹果,标准",
     "title": "如何挑选苹果",
     "html": "<h1>如何挑选苹果</h1>><p>以下标准可供消费者参考</p><ul><li>酸甜</li><li>硬度</li></ul>",
     "css": "",
     "js": "",
     "file": {}
  },
   "msg": "success"
}

3.4.3 投放需求

能力:WebIDE代码编辑。

描述:开发基于WebIDE代码开发工作,基于渠道和环境修改下载链接,能够做到分钟级支撑。

场景:根据产品创建静态页面进行逻辑和样式开发。

具体案例:

var ua = window.navigator.userAgent
   var URL_MAP = {
       ios: 'https://apps.apple.com/cn/app/xxx',
       android: 'xxx.apk',
       ios_dpmerchant: 'itms-apps://itunes.apple.com/cn/app/xxx'
  }
   
   if (ua.match(/android/i)) location.href = URL_MAP.android
   if (ua.match(/(ipad|iphone|ipod).*os\s([\d_]+)/i)) {
       if (/xx\/com\.xxx\.xx\.mobile/.test(ua)) {
           location.href = URL_MAP.ios_dpmerchant
      } else {
           location.href = URL_MAP.ios
      }
  }

3.4.4 客户端通信中间页

能力:WebIDE代码编辑 + 物料平台。

描述:通过物料平台,引入公司客户端桥SDK,可以快速完成客户端通信需求。方便前端调试客户端基础桥功能。

场景:客户端跳转,通信中间页。

具体案例:

// 业务伪代码
   XXX.ready(() => {
       XXX.sendMessage({
          sign: true,
           params: {
               id: window.URL
          }
      }, () => {
           console.error('通信成功')
      }, () => {
           console.error('通信失败')
      })
  })

3.4.5 业务系统内嵌Page

能力:提供胶水层Page-SDK,连接业务系统和Page。

描述:业务系统与Page-佩奇平台可进行通信,业务系统可调用Page发布、预览、编辑等功能,Page可返回业务系统页面链接、内容、权限等信息。减少重复前后端工作,提升研发效率。

场景:前端富文本信息渲染,后端富文本信息管理后台。

具体案例:

pic_ec5b5f49.png

图12 业务系统内嵌Page案例

3.5 业务成绩

截止目前数据统计,Page-佩奇平台生成网页5000多个,编辑页面次数16000多次,累计页面访问PV超过8260万。现在,美团已经有十多个部门和三十多条业务线接入并使用了Page-佩奇平台。

pic_becbff8a.png

图13 Page-佩奇平台每日生成页面统计

四、总结与展望

富文本编辑器和WebIDE不仅是复杂的系统,而且还是比较热门的研究方向。特别是在和美团的基建结合之后,能够解决团队内部很多效率和质量问题。这套系统还提供了语法智能提示、Diff对比、前置检测、命令行调试等功能,不仅要关注业务发布出去页面的稳定性和质量,更要有内置的一系列研发插件,主动帮助研发提高代码质量,降低不必要的错误。

经过长期的技术和业务演进,Page-佩奇平台已经能够有效地帮助研发人员大幅提升开发效率,具备初级的Design To Code能力,但是仍有许多业务场景值得去我们探索。我们也期待优秀的你参与进来,一起共同建设。

  • WebIDE融合:完善基础设施建设和功能需求,更好地支持Vue、React、ES 6、TS、Less语法,预览模式采用浏览器编译,能有效地提高预览的速度,发布使用后端编译的模式。

  • 研发流程链路:针对代码进行有效评估,包括ESlint、代码重复率、智能提示是否可以三方库替代。出具开发代码质量、业务上线的质量报告。

  • 综合研发平台:减少团队同学了解整体基建的时间成本,内置了监控、性能、任务管理等功能,提升业务开发效率。建设自动化日报、周报系统,降低非开发工作量占比。

  • 物料开放能力:接入公共组件平台,沉淀更多的物料,快速满足产品更多样化的需求。

五、作者简介

高瞻、宇立、肖莹、浩畅,来自美团医药终端团队。王咏、陈文,来自美团闪购终端团队。
来源:https://blog.csdn.net/MeituanTech/article/details/121551030

收起阅读 »

换一个方式组织你的Axios代码?

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜...
继续阅读 »

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜青菜各有所爱,没有优劣之分。



灵感来源


d4axios的灵感来源于open-figen,现在功能还没有那么丰富,但是足以应付多大多数的场景,比如上传、下载、get、post等等请求,配合上ts,可以解决数据类型前后台一致性,数据类型的转换,保证了请求的便利性。让代码专注于数据处理,而非复制粘贴模版代码



别忘了 在 [ts | js]config.json 文件里开启对装饰器的支持。"experimentalDecorators":true



在项目中使用 d4axios



d4axios (Decorators for Axios) 是一款基于axios请求方式的装饰器方法组,可以快速地对服务进行管理和控制,增强前端下服务请求方式,有效降低服务的认知难度,并且在基于ts的情况下可以保证服务的前后台的数据接口一致性



npm i d4axios

yarn add d4axios

一、 引入配置信息


在这里提供了几种配置方式,可以采用原始的axios的配置方法,也可以采用 d4axios 提供的方法


// 在 vue3下我们建议使用 `createService` 
// 其他情况下使用 `serviceConfig`
import { createApp } from 'vue'
import {createService,serviceConfig} from 'd4axios'


createApp(App).use(createService({ ... /* 一些配置信息 */}))


1.1 提供的axios配置项


createServiceserviceConfig 使用的配置项是一样的,并且完全兼容axios的配置。在现有的项目中改造的话,可以使用:


// 可以直接使用由d4axios提供的服务
createService()

// 可直接传入axios的相关配置,由d4axios自动基于配置相关构建
createService({axios:{ baseURL:"domain.origin" }})

// 可直接传入已经配置好的 `axios` 实例对象
const axios = Axios.create({ /* 你的配置*/ });


createService({axios})

1.2 提供基于请求和相应数据的配置


createService({
beforeRequest(requestData){
// form对象会被转为JSON对象的浅拷贝,但是会在该方法执行完后重新转为form对象
// 你可在请求前追加一些补充的请求参数
// 比如对请求体进行签名等等
return requestData
},
beforeResponse(responseData){
// 默认情况下会返回 axios的response.data值,而不会返回response的完整对象
// 可以修改返回的响应结果
return responseData
}
})

1.3 提供快速的axios interceptors 配置


createService({
interceptors:{
request:{
use(AxiosRequestConfig){},
reject(){}
},
response{
use(AxiosResponse){},
reject(){}
}
}
})

配置完成后,会返回一个axios实例对象,可以继续对axios对象做更多的操作,可以绑定到vue.prototype.$axios下使用


Vue.prototype.$axios = serviceConfig({... /*一些配置*/})

二、创建请求服务


为了更好的组织业务逻辑,d4axios提供了一系列的组织方法,供挑选使用


import {Service,Prefix,Get,Post,Download,Upload,Param,After,Header} from 'd4axios'

@Service("UserService") // 需要提供一个服务名称,该名称将在使用服务的时候被用到
@Prefix("/user") // 可以给当前的服务添加一个请求前缀
export class UserService {

@Get("/:id") // 等同于 /user/:id
@After((respData)=>{
//在输出给最终结果前,可以对结果做一些简单处理
return respData
})
async getUserById(id:string){
// 异步请求需要增加 `async` 属性以便语法识别
// 支持restful的方式
}


@Post("/regist")
@Header({'plantform':'android'}) // 请求前追加一些header参数
async registUser(form:UserForm){
// 可以在请求的时候做一些参数修改
form.nickName = createNickName(form.nickName);

// return的值是最终请求的参数
return {
...form,
plant:"IOS"
};
}

@Download("/user/card") // 支持文件下载
async downloadCard(@Param("id") id:stirng){
// 当我们的参数较少并且不是一个key-value形式的值时
// 可以使用@Param辅助,确定传参名称
}

@Upload("/user/card") // 支持文件上传
async uploadCard(file:File){
return {file}
}

// 可以定义同步函数,直接做服务计算
someSyncFunc(){
return 1+1
}

// 我们还可以直接定义非请求函数
async someFunc(){
// 所有的当前函数都是可以直接调用的
return await this.getUserById(this.someSyncFunc());
}

}


三、使用服务



使用服务分为几种方式,第一种是在一个服务中调用另一个服务。第二种是在react或者vue中调用服务,对于这两种有不同的方法,也可以用相同的方法。



3.1 在 vue或者react中使用useService 导入服务


// 在 vue 或者 react中,可以直接使用 useService 导入一个服务对象
import {useService} from 'd4axios'
import SomeService from './some.service'

const someService = useService(SomeService)

复制代码

3.2 在一个服务中Use调用另一个服务


import {Use} from 'd4axios'
import SomeService from './some.service'
// 也可以直接像上面一样的导入进来是用
const someService = useService(SomeService)

@Service("MyService")
export class MyService {
@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

async someMethod(){
// 就可以使用了,
await this.someService.something();
}
}

四、响应重写


默认情况下,d4axios支持async响应类型值,该值为


 export interface ResponseDataType<T> { }

在项目根路径下定义 d4axios.d.ts文件
然后文件内定义,通过重写该类型,可以得到响应的 response type类型,比如



export interface ResponseDataType<T> {
data : T;
msg:string ;
code:string ;
}

后即可以得到相关内容的提示信息


dataType.png


五、其他一些基于 Decorators 的操作


5.1 在使用装饰器的class上都可以使用 Use 导入服务 比如:


import {Component,Vue,} from 'vue-class-decorator'
import SomeService from './some.service'

@VueServiceBind(MyService,OtherService) // 只能在vue的这种形式下使用,可以绑定多个值
@Component
export default class MyVueComp extends Vue {

@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

myService !: S<MyService>

otherService !: S<OtherService>
}

5.2 在一般的vue的服务下可以使用这种 mapService 形式


// 传统的模式下

import { mapService } from 'd4axios';
import MyService from './MyService.service'

export default {
computed:{
...mapService(MyService,OtherService)
},
created(){
this.myService.getName(10086);
}

作者:非思不可
链接:https://juejin.cn/post/7041930275458285582

收起阅读 »

如何在浏览器 console 控制台中播放视频?

如何在浏览器 console 控制台中播放视频? 要实现这个目标,主要涉及到这几个点: 如何获取和解析视频流? 如何在 console 里播放动态内容? 如何在 console 里播放彩色内容? 如何连接视频流和 console? 事实上最后的代码极其简单...
继续阅读 »

如何在浏览器 console 控制台中播放视频?


要实现这个目标,主要涉及到这几个点:



  1. 如何获取和解析视频流?

  2. 如何在 console 里播放动态内容?

  3. 如何在 console 里播放彩色内容?

  4. 如何连接视频流和 console?


事实上最后的代码极其简单,我们就一步一步简单讲一下


效果



测试地址:yu-tou.github.io/colors-web/…


如何获取和解析视频流?


这里我们用电脑摄像头捕获视频流,然后获取视频流每一帧的图像数据,作为下一步的输入。


// 捕捉电脑摄像头的视频流
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
// 创建一个 video 标签
const video = document.createElement("video");
document.body.appendChild(video);

video.onloadedmetadata = function (e) {
video.play(); // 等摄像头数据加载完成后,开始播放
};
// video 标签播放视频流
video.srcObject = mediaStream;

如何获取每一帧图像的数据?创建一个 canvas 画布,可以将 video 当前的内容绘制到画布上,然后通过 canvas 的方法即可拿到图像的像素数据。


const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;

ctx.drawImage(video, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// imageData 的结构是平铺的,需要自己去学习下

如何在 console 里播放动态内容?


视频每帧的图像内容我们已经可以拿到了,继续下一步,如果需要在 console 中完成播放视频,首先需要能够一帧一帧绘制内容,但是这个好像是不太现实的,console.log 只能输出文本。


回想远古时代,在终端里大家怎么播放视频的?没错,用字符画一帧一帧绘制,连起来不就是动态的视频了。


当然 chrome dev tool 里如果每一帧绘制后都调用 console.clear() 清空重绘,体验不是很好,闪烁会很严重,所以我们采用持续输出的方式绘制,当你停留在 console 的最后的时候,看起来也算是动态内容了。


如何在 console 里播放彩色内容?


console.log 支持部分 css 特性,可以为输出的字符串指定简单的样式,最基本的支持背景色、字体颜色、下划线等,甚至支持 background-image、padding 等特性,利用这些特性,甚至可以插入图片,但是这些特性在不同浏览器的 console 中或多或少有些兼容问题,不过要实现字体着色,或者输出色块(用背景色),问题不大。


我们在此使用 colors-web 来更方便地输出彩色内容到控制台。


这是一个非常方便的库,可以使用链式调用在控制台快速输出彩色内容,并且支持诸多特性,无需自己去了解,直接使用对应的方法即可。


如:


import { logger, colors } from "colors-web";
logger(
colors().red().fontsize(48).fontfamily("SignPainter").log("hello"),
colors().white.redBg("hello").linethrough(),
"world",
colors().white.padding(2, 5).underline().lightgrey("芋头")
);

相信我不解释,大家也基本理解这些用法,非常简单和自由,而且支持 typescript。


我们这里,用 colors-web 输出色块:


for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (i * width + j < data.length) {
const color = `rgb(${data[(i * width + j) * 4 + 0]},${data[(i * width + j) * 4 + 1]},${
data[(i * width + j) * 4 + 2]
})`;
colors()
.bg(color)
.color(color)
.fontfamily(/Chrome/.test(navigator.userAgent) ? "Courier" : "")
.log("╳");
}
}
}

最终逻辑


最终我将每一帧所有的像素值都转换成一个 colors 的实例,记录到数组之后,最终统一调用 logger 即可完成一帧的渲染。


const frameColors = [];
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (i * width + j < data.length) {
const color = `rgb(${data[(i * width + j) * 4 + 0]},${data[(i * width + j) * 4 + 1]},${
data[(i * width + j) * 4 + 2]
})`;
frameColors.push(
colors()
.bg(color)
.color(color)
.fontfamily(/Chrome/.test(navigator.userAgent) ? "Courier" : "")
.log("╳")
);
}
}
}
// 绘制,colors() 只是在准备数据结构,logger 才是真正的输出
logger(...frameColors);

大公告成啦!


作者:芋头君
链接:https://juejin.cn/post/7013620775143866376

收起阅读 »

某科技公司前端三面面经

okay, it's me again. 哈哈哈我怎么也没想到,我又会经历多一次三面,这次可以说是被狠狠的按在地上摩擦了,没办法,只能奉行一贯的“技术不够,吹牛来凑”原则 btw 应该看多点别人的面经,而不是自己写面经,当然自己写也可以当作一个很好的复盘 这次...
继续阅读 »

okay, it's me again.


哈哈哈我怎么也没想到,我又会经历多一次三面,这次可以说是被狠狠的按在地上摩擦了,没办法,只能奉行一贯的“技术不够,吹牛来凑”原则


btw 应该看多点别人的面经,而不是自己写面经,当然自己写也可以当作一个很好的复盘


这次是某家准备上市的公司,公司的技术部门也是挺强大的,所以也才会有三面吧可能


其实复盘过程中记的也不太清楚,只能说想起来一点写一点,这里建议将整个面试过程录音,以便做一次彻底的复盘,当然最好还是取得面试官同意才这么做


面试流程:boss直聘聊 -> 发邮件邀约面试 -> 一面技术面 -> 二面技术面 -> 三面HR面 -> 电话沟通薪资和入职事宜 -> offer


一面技术面




  1. 自我介绍




  2. 一个业务场景,PC端用vue做后台管理系统的时候,一般路由是动态生成的,前端的文件与路由是一一对应的,假如不小心删了一个文件,这个时候就会跳404页面,会有不好的用户体验,怎么做才能比较好的防止跳去404页面?




  3. 有一个页面,一个绝对够长的背景图,我们知道不给盒子设定高度的情况下默认是100%的高度,盒子高度会被内容所撑开。那么怎么做到第一屏完全显示背景图,第二屏也能继续显示呢?


    好,来看我的第一个错误回答🤣


    <style>
    * {
    margin: 0;
    padding: 0;
    }
    .container {
    width: 100%;
    height: 100vh;
    background-image: url('./assets/images/long.jpeg');
    }
    </style>

    <body>
    <div class="container">
    <p>1</p>
    这里复制出足够多的<p>1</p>就好,我就不贴出来重复代码占据太大篇幅了
    </div>
    </body>


    这是第一屏的效果,嗯很好完全没有问题! 但是当我们鼠标来到第二屏就哦豁了🙈




WechatIMG83.jpeg


然后我的第二个回答是:将图片绝对定位,这样图片就能适应不管多少屏了,但是图片绝对定位的话,没有内容撑开,那么第一屏根本都不会出现背景,所以这样也是不行的😅


答案:将 height: 100vh; 换成 min-height: 100vh;就可以了😂




  1. 我们都知道在谷歌浏览器里面字体的最低像素是 12px ,就算设置font-size: 8px;也会变成 12px ,我现在有一个需求需要 8px 的字体,怎么才能突破 12px 的限制?


    基本原理是使用css3的 transform: scale(); 属性


    需求是 8px 的字体,那我们就 font-size: 16px; transform: scale(0.5); 即可




  2. 讲一下 ES6 的新特性




  3. 说一些你经常用到的数组的方法




  4. 前端性能优化


    传送门:聊一聊前端性能优化




  5. 原型链


    传送门:继承与原型链


    传送门:JavaScript原型系列(三)Function、Object、null等等的关系和鸡蛋问题




  6. 假设在一个盒子里,里面所有小盒子的宽高都是相等的(PS技术不好,画的不相等),大盒子刚好放得下7个小盒子,使用css实现下面的布局




WechatIMG84.png




  1. 讲一下微信登录流程




  2. 怎么给每个请求加上 Authorization token ? (考察封装请求,axios 拦截器)




  3. 讲一下 vue 的双向数据绑定原理




  4. 移动端防止重复点击,防抖节流




  5. 怎么触发BFC,有什么应用场景?




  6. Promise有哪几种状态?




  7. 如果现在有一个任务,让你来做主力开发,架构已经搭好了,UI设计图也已经出完了,那你第一步会做什么?




  8. 后台管理系统怎么做权限分配?




  9. 怎么判断一个对象是否为空对象?




  10. 数字1-50的累加,不用 for 循环,用递归写


    因为我很抗拒当场写代码,然后满脑子都是1-50的累加为什么不用 for 循环,用 for 循环不是更快吗?为什么要用递归?但是面试官都把纸笔递过来了,没办法也是只能硬着头皮上了,但是这也是很简单的一道题,下面贴出当时手写的代码(是错的)


        // 这是错的这是错的这是错的
    function add(n) {
    let sum = 0;

    if (n > 0) {
    sum += add(n - 1);
    } else {
    return sum;
    }
    }

    // 这是根据上面改进之后的写法
    function add(n, sum) {
    if (n > 0) {
    return add(n - 1, (sum += n));
    } else {
    return sum;
    }
    }

    // 当然还有一种更为优雅与简便的写法
    function add(n) {
    return n === 0 ? 0 : n + add(n - 1);
    }

    // 想一行代码搞定的话就是
    const add = (n) => (n === 0 ? 0 : n + add(n - 1));



  11. 怎么解决 vuex 里的数据在页面刷新后丢失的问题?




  12. 说一下 vue 组件通信有几种方式(老生常谈的问题)




  13. 说一下 vue 和微信小程序各自的生命周期




  14. 看一下这个 ts 问题


        let num: string = '1';
    转一下数据类型转成 number



  15. 说一下 ts 总共有多少种数据类型




二面技术面




  1. 封装一个级联组件,讲一下思路




  2. 封装 v-model




  3. POST请求的 Content-Type 有多少种?




  4. css flex: 1; 是哪几个属性的组合写法




  5. vue provide/inject 的数据不会及时回流到父组件的问题(我记得没错的话好像是这么问的)




  6. 不用Promise的情况下,怎么实现一个Promise.all()方法




  7. [1, 2, 3].map((item, index) => parseInt(item, index))的结果


    这里考察了两点,1是parseInt()方法的第二个参数有什么作用,2是进制转换的相关知识




  8. cookie,sessionStorage,localStorage 3者之间有什么区别?




  9. http://www.xxx.com (a网站) 和 http://www.api.xxx.com (b网站) 两个网站,在b网站里登录授权拿到了 cookie ,怎么在a网站里拿到这个 cookie ?




  10. 说一下 forEach, map, for...in, for...of 的区别




  11. git fetch和git pull的区别(最后一道题)


    git pull:相当于是从远程获取最新版本并 merge 到本地


    git fetch:相当于是从远程获取最新版本到本地,不会自动 merge


    区别就是会不会自动 merge




三面HR面


这里就不展开了,HR面差不多都是那些东西


以上


其实一面二面还有很多问题都没有写出来,但是碍于当时也没有录音,只记得这么多


严格来讲,这并不太算是一篇面经,在上面很多都只是抛出了问题,因为技术的原因并没有做出相应的解答,还是有些遗憾的



作者:Lieo
链接:https://juejin.cn/post/7021394272519716872

收起阅读 »

亲身经历,大龄程序员找工作,为什么这么难!

背景 临近年底,公司还在招人,可筛选的人才真是越来越少,这可能是因为大家都在等年终奖吧。于是在简历筛选时,将学历和年龄都适当的放松了。正因为如此,面试了不少大龄的程序员。 网络上一直有讨论大龄程序员找工作困境的话题,对于我个人来说,是将信将疑的,但作为程序员对...
继续阅读 »

背景


临近年底,公司还在招人,可筛选的人才真是越来越少,这可能是因为大家都在等年终奖吧。于是在简历筛选时,将学历和年龄都适当的放松了。正因为如此,面试了不少大龄的程序员。


网络上一直有讨论大龄程序员找工作困境的话题,对于我个人来说,是将信将疑的,但作为程序员对自己职业生涯和未来的危机感还是有的。同时,作为技术部门领导,我是不介意年龄比我大,能力比我强的人加入的,只要能把事做好,这都不是事。


随着互联网的发展,大量程序员必然增多,都找不到工作是不可能的。而且中国的未来必然也会像发达国家一样,几十岁甚至一辈子都在写代码,也不是有可能的。


那么,我们担忧的是什么呢?又是什么影响了找工作呢?本文就通过自己亲身面试的几个典型案例来说说,可能样本有些小,仅供参考,不喜勿喷。


大厂与小厂招人的区别


前两天在朋友圈发了一条招人的感慨,关于大厂招人和小公司招人的区别。


大厂:有影响力,有钱,能够吸引了大量的应聘者。因此,也就有了筛选的资格,比如必须985名校毕业,必须35岁以下,不能5年3跳,必须这个……不能那个……当员工不合适时,绩效分给的低点或直接赔钱让其出局。


小公司:没有品牌,资金有限,每一分钱都要精打细算。招聘的人选有限,在这有限的选择范围内,还要考虑成本、能不能用、能不能留住等问题。能力太强,给不起钱,留不住;能力太弱,只会让项目越来越糟糕;所以,最好的选择只能是稍微高于现有团队能力,又不至于轻易跳槽的人。


有了上面的基本前提,再来看看大厂与小厂对待大龄程序员的差别。


对于35岁以后的程序员,有的大厂已经直接卡死,也就别死磕了。另外一些大厂还是开放的,但肯定是有一定的要求的,比如职位必须达到什么等级,能力必须达到什么要求。换句话说,如果你是牛人,其实35岁并不是什么问题,如果不是,那么这个选项几乎不存在。


所以,大厂的选择基本上等于没什么选择。再来看看小公司,小公司追求的核心是性价比,或者直白点说就是能干事且节省成本。另外就是能不能一职多能,能不能带新人,能不能加班……


个人看来,相对于大厂的要求,小公司的要求稍微努力一下还是可以满足的。对于加班这一项,不是所有的公司都有加班文化,也不是所有的公司常年需要加班。


招聘案例


挑选面试中几个比较典型的案例来聊聊,看看对你有什么启发。


案例一


84年的应聘者,自己在简历上填写的是应聘“中高级Java开发”。面试中,各项技能都平平,属于有功能开发经验,但没有深钻技术,没有考虑更好解决方案的状态。明确加班不能超过9点。也有写博客和公众号。9月份离职,目前暂未未找到工作。


就这位应聘者而言:第一,能接受员工比自己年龄大的领导不多,因为担心管不住;第二,技能没有亮点,就像他自己定位的那样,十多年工作经验,只是中高级开发;第三,加班这一项卡的太死,哪家公司上个线不到10点以后了,有突发需求不加个班?


案例二


86年的应聘者,别家公司裁员,推荐过来的简历。十来年工作经验,一直负责一个简单彩会不会是敏感词票业务的开发,中间有几年还没有项目履历。简历上写的功能还是:用户管理、权限管理、XX接口对接。推荐他的老板,给他的定位是中级开发。


这位应聘者,真的是将一年的代码写了十年。上家老板裁员选择了他,定位也只是中级,然后帮忙推荐了他到其他公司。这背后的故事,你仔细品,再仔细品。


案例三


87年的应聘者,学历一般,这两年的工作履历有点糟糕,跳槽的时机选择的也不好。长期从事支付行业,十来年的工作履历中,有七八年在做支付及相关的行业,其中在一家支付公司工作了四年。面试中,特意问了行业中的一些解决方案、技术实现,都没问题。基础知识稍微有点弱,但影响不大。面试过后,发了Offer,其中我还在老板面前帮忙争取了一把。


这位应聘者,虽然在学历,近两年的经历,基础知识上都略有不足。但他的行业经验丰富,给人一种踏实做事的感觉。整体能力恰好符合上面提到的小公司选择标准:比现有团队人能力强,有行业经验,薪资适中,稳定性较好。他的长板完全弥补了短板。


案例四


91年的应聘者,一家小有名气二线互联网公司出来。最近半年未工作,给出的原因是:家中有事,处理了半年,现在决定找工作了。聊半年前做过的项目,基本上记不起逻辑了;聊技术知识,也只能说个大概,但能感觉还是做过一些功能的,但没有深入思考过或没有做过复杂逻辑。


这位应聘者,不确定已经面试多久了,但应该不那么容易找工作。第一,半年未工作,即使有原因,也让人多少有些顾虑;第二,面试前完全没做功课,这不是能力问题,而是态度的问题了。


上面的案例,有成功的也有失败的。总结一下失败的原因,基本上有几点:能力与年龄不匹配、不能加班、家庭影响、没有特长……当然,你如果能看到其他的失败原因,那就更好了。


小结


上面只是最近一段时间面试的几个典型案例,至于你能从中获得什么,能不能提前行动,做好准备。那就是大家自己的事了。当然,还是那句话,样本有些小,但也能说明一些问题。仅个人观点,不抬杠,不喜勿喷。


我也曾为职场的未来担忧,也曾为年龄担忧,但始终未放弃的就是持续学习和思考。多位朋友都曾说过:无论你是否当上领导,是否还在写代码,技术能力都不能丢,你必须是团队中技术最牛的那一个。我一直在努力做到,你呢


作者:程序新视界
链接:https://juejin.cn/post/7043589223345029133

收起阅读 »

JavaScript函数封装随机颜色验证码

数字或者字母或者数字字母混合的n位验证码带随机的颜色。下面是完整的代码,需要的自取哈!function verify(a = 6,b = "num"){ //定义三个随机验证码验证码库 var num ="0123456789" var str ="ab...
继续阅读 »

数字或者字母或者数字字母混合的n位验证码带随机的颜色。下面是完整的代码,需要的自取哈!

function verify(a = 6,b = "num"){
//定义三个随机验证码验证码库
var num ="0123456789"
var str ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNIPQRSTUVWXYZ"
var mixin = num +str;
 
//定义一个空字符串用来存放验证码
var verify=""
if(a == undefined || b == undefined){
  //验证输入是否合法 不通过就抛出一个异常
  throw new Error("参数异常");
}else{
    if(a ==""||b==""){
      //判断用户是否没有输入
      throw new Error("参数非法.");
    }else{
      //检测输入的类型来判断是否进入
      var typea = typeof(a);
      var typeb = typeof(b);
      if(typea =="number" && typeb =="string"){
          if(b == "num"){
                 
              //定义一个循环来接收验证码   纯数字验证码
              for(var i=0;i<a;i++){
                    //定义一个变量来存储颜色的随机值
                    var r1 = Math.random()*255;
                    var g1 = Math.random()*255;
                    var b1 = Math.random()*255;

                    //确定随机索引
                    var index = Math.floor(Math.random()*(num.length-1))
                    //确定随机的验证码
                    var char = num[index];
                    //给随机的验证码加颜色
                    verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
                }
                //返回到数组本身
              return verify;
          }else if(b =="str"){
                for(var i=0;i<a;i++){
                  //纯字母的验证码
                  var r1 = Math.random()*255;
                  var g1 = Math.random()*255;
                  var b1 = Math.random()*255;

                  var index = Math.floor(Math.random()*(str.length-1));
                  var char = str[index];

                  verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
                }
                return verify;  
          }else if(b == "mixin"){
                // 混合型的验证码
              for(var i=0;i<a;i++){
                  var r1 = Math.random()*255;
                  var g1 = Math.random()*255;
                  var b1 = Math.random()*255;

                  var index = Math.floor(Math.random()*(mixin.length-1));
                  var char = mixin[index];

                  verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
              }
              return verify;
          }else{
              //验证没通过抛出一个异常
              throw new Error("输入类型非法.")
          }
       
      }else{
          //验证没通过抛出一个异常
          throw new Error("输入类型非法.")
      }
    }
}
}

下面我们来调用函数试试看

  //第一个值为用户输入的长度,第二个为类型! 
var arr = verify(8,"mixin");
    document.write(arr)

上面就是结果啦!

这个记录下来为了方便以后使用的方便,也希望大佬们多多交流,多多留言,指出我的不足的之处啦!

有需要的小伙伴可以研究研究啦!!
————————————————
作者:土豆切成丝
来源:https://blog.csdn.net/A20201130/article/details/122030872

收起阅读 »

localhost、127.0.0.1和0.0.0.0和本机IP的区别

localhostlocalhost其实是域名,一般windows系统默认将localhost指向127.0.0.1,但是localhost并不等于127.0.0.1,localhost指向的IP地址是可以配置的 127.0.0.1首先我们要先知道一...
继续阅读 »
localhost
localhost其实是域名,一般windows系统默认将localhost指向127.0.0.1,但是localhost并不等于127.0.0.1,localhost指向的IP地址是可以配置的
 
127.0.0.1
首先我们要先知道一个概念,凡是以127开头的IP地址,都是回环地址(Loop back address),其所在的回环接口一般被理解为虚拟网卡,并不是真正的路由器接口。
所谓的回环地址,通俗的讲,就是我们在主机上发送给127开头的IP地址的数据包会被发送的主机自己接收,根本传不出去,外部设备也无法通过回环地址访问到本机。
 
小说明:正常的数据包会从IP层进入链路层,然后发送到网络上;而给回环地址发送数据包,数据包会直接被发送主机的IP层获取,后面就没有链路层他们啥事了。
而127.0.0.1作为{127}集合中的一员,当然也是个回环地址。只不过127.0.0.1经常被默认配置为localhost的IP地址。
一般会通过ping 127.0.0.1来测试某台机器上的网络设备是否工作正常。
 
0.0.0.0
首先,0.0.0.0是不能被ping通的。在服务器中,0.0.0.0并不是一个真实的的IP地址,它表示本机中所有的IPV4地址。监听0.0.0.0的端口,就是监听本机中所有IP的端口。
 
本机IP
本机IP通常仅指在同一个局域网内,能同时被外部设备访问和本机访问的那些IP地址(可能不止一个)。像127.0.0.1这种一般是不被当作本机IP的。本机IP是与具体的网络接口绑定的,比如以太网卡、无线网卡或者PPP/PPPoE拨号网络的虚拟网卡,想要正常工作都要绑定一个地址,否则其他设备就不知道如何访问它。
 
 

localhost
首先 localhost 是一个域名,在过去它指向 127.0.0.1 这个IP地址。在操作系统支持 ipv6 后,它同时还指向ipv6 的地址 [::1] 
在 Windows 中,这个域名是预定义的,从 hosts 文件中可以看出:
# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
 
而在 Linux 中,其定义位于 /etc/hosts 中:
127.0.0.1 localhost
 
注意这个值是可修改的,比如把它改成
192.068.206.1 localhost
 
然后再去 ping localhost,提示就变成了
PING localhost (192.168.206.1) 56(84) bytes of data.
 
127.0.0.1
127.0.0.1 这个地址通常分配给 loopback 接口。loopback 是一个特殊的网络接口(可理解成虚拟网卡),用于本机中各个应用之间的网络交互。只要操作系统的网络组件是正常的,loopback 就能工作。Windows 中看不到这个接口,Linux中这个接口叫 lo:
#ifconfig
eth0 Link encap:Ethernet hwaddr 00:00:00:00:00:00
  inet addr :192.168.0.1 Bcase:192.168.0.255 Mask:255.255.255.0
  ......
lo     Link encap:Local Loopback
  inetaddr: 127.0.0.1 Mask: 255.0.0.0
       ......
 
可以看出 lo 接口的地址是 127.0.0.1。事实上整个 127.* 网段都算能够使用,比如你 ping 127.0.0.2 也是通的。 
但是使用127.0.0.1作为loopback接口的默认地址只是一个惯例,比如下面这样:
#ifconfig lo 192.168.128.1
#ping localhost  #糟糕,ping不通了
#ping 192.128.128.1 # 可以通
#ifconfig lo
lo  Link encap:Local Loopback
  inetaddr: 192.168.128.1 Mask: 255.255.255.0
     ......
 
如果随便改这些配置,可能导致很多只认 127.0.0.1 的软件挂掉。
 
本机IP
确切地说,“本机地址”并不是一个规范的名词。通常情况下,指的是“本机物理网卡所绑定的网络协议地址”。由于目前常用网络协议只剩下了IPV4,IPX/Apple Tak消失了,IPV6还没普及,所以通常仅指IP地址甚至ipv4地址。一般情况下,并不会把 127.0.0.1当作本机地址——因为没必要特别说明,大家都知道。 
本机地址是与具体的网络接口绑定的。比如以太网卡、无线网卡或者PPP/PPPoE拨号网络的虚拟网卡,想要正常工作都要绑定一个地址,否则其他设备就不知道如何访问它。
● localhost 是个域名,不是地址,它可以被配置为任意的 IP 地址,不过通常情况下都指向 127.0.0.1(ipv4)和 ::1 
● 整个127.* 网段通常被用作 loopback 网络接口的默认地址,按惯例通常设置为 127.0.0.1。这个地址在其他计算机上不能访问,就算你想访问,访问的也是自己,因为每台带有TCP/IP协议栈的设备基本上都有 localhost/127.0.0.1。 
● 本机地址通常指的是绑定在物理或虚拟网络接口上的IP地址,可供其他设备访问到。 
● 最后,从开发度来看 
○ localhost是个域名,性质跟 “www.baidu.com” 差不多。不能直接绑定套接字,必须先gethostbyname转成IP才能绑定。 
○ 127.0.0.1 是绑定在 loopback 接口上的地址,如果服务端套接字绑定在它上面,你的客户端程序就只能在本机访问

原文链接:https://www.cnblogs.com/absoluteli/p/13958072.html
收起阅读 »

如何用JavaScript实现双向映射?

本文翻译自 《How to create a Bidirectional Map in JavaScript》双向映射是指在键值对中建立双向一一对应关系的一种模式。它既可以通过键名(key)去获取值(value),也可以通过值去获取键名。让我们看下如何在Jav...
继续阅读 »



本文翻译自 《How to create a Bidirectional Map in JavaScript》

双向映射是指在键值对中建立双向一一对应关系的一种模式。它既可以通过键名(key)去获取值(value),也可以通过值去获取键名。让我们看下如何在JavaScript中实现一个双向映射,以及 TypeScript 中的应用。

双向映射背后的计算机科学与数学

首先看一下双向映射的基本定义:

在计算机科学中,双向映射是由一一对应的键值对组成的数据结构,因此在每个方向都可以建立二元关系:每个值也可以对应唯一的键。

百科指路双向映射

计算机科学中的双向映射,源于数学上的双射函数。双射函数是指两个集合中的每个元素,都可以在另一个集合中找到与之匹配的另一个元素,反之也可以通过后者找到匹配的前者,因此也被叫做可逆函数。

百科指路: 双射函数

扩展:

  • 单射(injection):每一个x都有唯一的y与之对应;

  • 满射(surjection):每一个y都必有至少一个x与之对应;

  • 双射(又叫一一对应,bijection):每一个x都有y与之对应,每一个y都有x与之对应。

根据上面的说明,一个简单的双射函数就像这样:

f(1) = 'D';
f(C) = 3;

另外,双射函数需要两个集合的长度相等,否则会失败。

初始化双向映射

我们可以在JavaScript 中创建一个类来初始化键值对:

const bimap = new BidirectionalMap({
 a: 'A',
 b: 'B',
 c: 'C',
})

在类里面,我们将会创建两个列表,一个用来处理正向映射,存放初始化对象的副本;另一个用来处理逆向映射,存放的内容是「键」「值」翻转后的初始化对象。

class BidirectionalMap {
 fwdMap = {}
 revMap = {}

 constructor(map) {
     this.fwdMap = { ...map }
     this.revMap = Object.keys(map).reduce(
        (acc, cur) => ({
             ...acc,
            [map[cur]]: cur,
        }),
        {}
    )
}
}

注意,由于初始对象本身的性质,你不能用数字当 key,但可以作为值来使用。

const bimap = new BidirectionalMap({
 a: 42,
 b: 'B',
 c: 'C',
})

如果不满足于此,也有更强大健壮的实现方式,按照 JavaScript 映射数据类型 中允许使用数字、函数甚至NaN来作为 key 的规范来实现,当然这会更加复杂。

通过双向映射获取元素

现在,我们有了一个包含两个对象的数据结构,它们互为键值对的镜像。我们现在需要一个方法来取出元素,让我们来实现一个 get() 函数:

 get( key ) {
   return this.fwdMap[key] || this.revMap[key]
}

这个方法非常简单: 如果正向映射里存在就返回,否则返回逆向映射,都没有就返回 undefined

试一下获取元素:

console.log(bimap.get('a')) // displays A
console.log(bimap.get('A')  // displays a

给双向映射添加元素

目前映射还无法添加元素,我们创建一个添加方法:

add(pair) {
   this.fwdMap[pair[0]] = pair[1]
   this.revMap[pair[1]] = pair[0]
}

add 函数接收一个双元素数组(在TypeScript 中叫做元组),按不同键值顺序加入到相应对象中。

现在我们可以添加和读取映射中的元素了:

bimap.add(['d', 'D'])
console.log( bimap.get('D') ) // displays d

在TypeScript中安全使用双向映射

为了确保数据类型安全,我们可以在 TypeScript 中进行改写,对输入类型进行检查,例如初始化的映射必须为一个通用对象,添加的元素必须为一个 元组

class BidirectionalMap {
 fwdMap = {}
 revMap = {}

 constructor(map: { [key: string]: string }) {
     this.fwdMap = { ...map }
     this.revMap = Object.keys(map).reduce(
        (acc, cur) => ({
             ...acc,
            [map[cur]]: cur,
        }),
        {}
    )
}

 get(key: string): string | undefined {
     return this.fwdMap[key] || this.revMap[key]
}

 add(pair: [string, string]) {
   this.fwdMap[pair[0]] = pair[1]
   this.revMap[pair[1]] = pair[0]
}
}

这样我们的映射就更加安全和完美了。在这里,我们的 key 和 value 都必须使用字符串。


翻译:sherryhe
来源:https://juejin.cn/post/6976797991277428750

收起阅读 »

Vue图片懒加载

1、问题在vue项目中,如果图片是从服务器端加载到页面上,图片较大的时候,就会存在一部分一部分加载的情况,会显示非常卡顿,影响体验。2、实现(1)、图片懒加载首先将图片的src链接设为一张我们已经准备好的图片(比如类似加载中的图片),并将其真正的图片地址存储在...
继续阅读 »

1、问题

在vue项目中,如果图片是从服务器端加载到页面上,图片较大的时候,就会存在一部分一部分加载的情况,会显示非常卡顿,影响体验。

2、实现

(1)、图片懒加载

首先将图片的src链接设为一张我们已经准备好的图片(比如类似加载中的图片),并将其真正的图片地址存储在img标签的自定义属性中。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。这样就可以缓解服务器压力,并且提高用户体验。

(2)、安装vue-lazyload
npm i vue-lazyload -S
(3)、在main.js中引入
import VueLazyload from "vue-lazyload";
Vue.use(VueLazyload,{
  preLoad: 1.3,
  loading: require('../src/assets/loading.gif'),
  attempt: 1
})

其中../src/assets/loading.gif是我本地的正在加载图片gif路径。

3、查看效果

在LazyLoad.vue中引入一张网络图片,在浏览器中限制网速,模拟图片加载缓慢的情况。

LazyLoad.vue

<template>
<div>
<img v-lazy=url1 alt="">
</div>
</template>
<script>
export default {
data (){
return{
url1: 'https://w.wallhaven.cc/full/pk/wallhaven-pkgkkp.png'
}
}
}
</script>

效果:

图片加载中:

图片加载完成:


常用参数:


作者:小绵杨Yancy
来源:https://blog.csdn.net/ZHANGYANG_1109/article/details/121868420

收起阅读 »

大白话讲解JavaScript 执行机制,一看就懂

JavaScript的运行机制所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是...
继续阅读 »



JavaScript的运行机制

1.JavaScript为什么是单线程?
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2、执行机制相关知识点

  • 同步任务

  • 异步任务

举个栗子
有一天,张三要去做饭 这时候他要做两件事 分别是蒸米饭 和 炒菜 ,现在有两种方式去完成这个任务

A. 先去蒸米饭 然后等蒸米饭好了 再去抄菜 ---同步任务
B. 先去蒸米饭 然后等蒸米饭的过程中 再去抄菜 ---异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

具体来说,异步执行的运行机制如下:(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

总结: 同步任务在主线程执行,形成一个执行栈,执行栈中的所有同步任务执行完毕,就会去读取任务队列,就是对应的异步任务。

JavaScript的宏任务与微任务
除了广义上的定义,我们可以将任务进行更精细的定义,分为宏任务微任务

宏任务(macro-task):包括整体代码script脚本的执行,setTimeout,setInterval,ajax,dom操作,还有如 I/O 操作、UI 渲染等。

微任务(micro-task):Promise回调 node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。

主线程都从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)

我们解释一下这张图:

1、同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
2、当指定的事情完成时,Event Table会将这个函数移入Event Queue。
3、主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。(Event Loop是javascript的执行机制)

优先级
setTimeout()、setInterval()
setTimeout() 和 setInterval() 产生的任务是 异步任务,也属于 宏任务。
setTimeout() 接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
如果将第二个参数设置为0或者不设置,意思 并不是立即执行,而是指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。(画重点)
所以说,setTimeout() 和 setInterval() 第二个参数设置的时间并不是绝对的,它需要根据当前代码最终执行的时间来确定的

Promise
Promise 相对来说就比较特殊了,在 new Promise() 中传入的回调函数是会 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,它属于 微任务。

process.nextTick
process.nextTick 是 Node.js 提供的一个与"任务队列"有关的方法,它产生的任务是放在 执行栈的尾部,并不属于 宏任务 和 微任务,因此它的任务 总是发生在所有异步任务之前。

setImmediate
setImmediate 是 Node.js 提供的另一个与"任务队列"有关的方法,它产生的任务追加到"任务队列"的尾部,它和 setTimeout(fn, 0) 很像,但优先级都是 setTimeout 优先于 setImmediate。
有时候,setTimeout 的执行顺序会在 setImmediate 的前面,有时候会在 setImmediate 的后面,这并不是 node.js 的 bug,这是因为虽然 setTimeout 第二个参数设置为0或者不设置,但是 setTimeout 源码中,会指定一个具体的毫秒数(node为1ms,浏览器为4ms),而由于当前代码执行时间受到执行环境的影响,执行时间有所起伏,如果当前执行的代码小于这个指定的值时,setTimeout 还没到推迟执行的时间,自然就先执行 setImmediate 了,如果当前执行的代码超过这个指定的值时,setTimeout 就会先于 setImmediate 执行。

通过上面的介绍,我们就可以得出一个代码执行的优先级:
同步代码(宏任务) > process.nextTick > Promise(微任务)> setTimeout(fn)、setInterval(fn)(宏任务)> setImmediate(宏任务)> setTimeout(fn, time)、setInterval(fn, time),其中time>0

面试回答
面试中该如何回答呢? 下面是我个人推荐的回答:
首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。

面试遇到的问题总结
1、同步和异步的区别是什么?分别举一个同步和异步的例子
同步会阻塞代码执行,而异步不会。alert是同步,setTimeout是异步

2、为何需要异步呢?
如果第一个示例中间步骤是一个 ajax 请求,现在网络比较慢,请求需要5秒钟。如果是同步,这5秒钟页面就卡死在这里啥也干不了了。

最后,前端 JS 脚本用到异步的场景主要有两个:

  • 定时 setTimeout setInverval

  • 网络请求,如 ajax 加载

  • 事件绑定

3、写出下图执行顺序

执行顺序是2431
在 new Promise() 中传入的回调函数是 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,
.then是回调函数,链式回调,会被放在挂起,等待执行栈的内容执行完后(输出4)再回调(输出3),最后执行异步的1

作者:我写的代码绝对没有问题
来源:https://www.jianshu.com/p/22641c97e351

收起阅读 »

12个有用的JavaScript数组技巧

数组是Javascript最常见的概念之一,它为我们提供了处理数据的许多可能性,熟悉数组的一些常用操作是很有必要的。1、数组去重1、from()叠加new Set()方法字符串或数值型数组的去重可以直接使用from方法。var plants = ['Satur...
继续阅读 »

数组是Javascript最常见的概念之一,它为我们提供了处理数据的许多可能性,熟悉数组的一些常用操作是很有必要的。

1、数组去重

1、from()叠加new Set()方法

字符串或数值型数组的去重可以直接使用from方法。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var uniquePlants = Array.from(new Set(plants));
console.log(uniquePlants); // [ 'Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Mars', 'Jupiter' ]

2、spread操作符(…)

扩展运算符是ES6的一大创新,还有很多强大的功能。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var uniquePlants = [...new Set(plants)];
console.log(uniquePlants); // [ 'Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Mars', 'Jupiter' ]

2、替换数组中的特定值

splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。该方法会改变原始数组。特别需要注意插入值的位置!

// arrayObject.splice(index,howmany,item1,.....,itemX)

var plants = ['Saturn', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var result = plants.splice(2, 1, 'www.shanzhonglei.com')
console.log(plants); // ['Saturn','Uranus','www.shanzhonglei.com','Mercury','Venus','Earth','Mars','Jupiter']
console.log(result); // ['Mercury']

3、没有map()的映射数组

我们先介绍一下map方法。map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值,它会按照原始数组元素顺序依次处理元素。注意: map()不会改变原始数组,也不会对空数组进行检测。
下面我们来实现一个没有map的数组映射:

// array.map(function(currentValue,index,arr), thisValue)

var plants = [
  { name: "Saturn" },
  { name: "Uranus" },
  { name: "Mercury" },
  { name: "Venus" },
]
var plantsName = Array.from(plants, ({ name }) => name);
console.log(plantsName); // [ 'Saturn', 'Uranus', 'Mercury', 'Venus' ]

4、空数组

如果要清空一个数组,将数组的长度设置为0即可,额,这个有点简单。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
plants.length = 0;
console.log(plants); // []

5、将数组转换为对象

如果要将数组转换为对象,最快的方法莫过于spread运算符(…)。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var plantsObj = {...plants }
console.log(plantsObj); // {'0': 'Saturn','1': 'Earth', '2': 'Uranus','3': 'Mercury','4': 'Venus','5': 'Earth','6': 'Mars','7': 'Jupiter'}

6、用数据填充数组

如果我们需要用一些数据来填充数组,或者需要一个具有相同值的数据,我们可以用fill()方法。

var plants = new Array(8).fill('8');
console.log(plants); // ['8', '8', '8','8', '8', '8','8', '8']

7、合并数组

当然你会想到concat()方法,但是哦,spread操作符(…)也很香的,这也是扩展运算符的另一个应用。

var plants1 = ['Saturn', 'Earth', 'Uranus', 'Mercury'];
var plants2 = ['Venus', 'Earth', 'Mars', 'Jupiter'];
console.log([...plants1, ...plants2]); // ['Saturn', 'Earth','Uranus', 'Mercury','Venus', 'Earth','Mars', 'Jupiter']

8、两个数组的交集

要求两个数组的交集,首先确保数组不重复,然后使用filter()方法和includes()方法。

var plants1 = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var plants2 = ['Saturn', 'Earth', 'Uranus'];
var alonePlants = [...new Set(plants1)].filter(item => plants2.includes(item));
console.log(alonePlants); // [ 'Saturn', 'Earth', 'Uranus' ]

9、删除数组中的假值

我们时常需要在处理数据的时候要去掉假值。在Javascript中,假值是false, 0, " ", null, NaN, undefined。

var plants = ['Saturn', 'Earth', null, undefined, false, "", NaN, 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var trueArr = plants.filter(Boolean);
console.log(trueArr); // ['Saturn', 'Earth','Uranus', 'Mercury','Venus', 'Earth','Mars', 'Jupiter']

10、获取数组中的随机值

我们可以根据数组长度获得一个随机索引号。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
console.log(plants[Math.floor(Math.random() * (plants.length + 1))])

11、lastIndexOf()方法

lastIndexOf()可以帮助我们查找元素最后一次出现的索引。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
console.log(plants.lastIndexOf('Earth')) // 5

12、将数组中的所有值相加

reduce()方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。

// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

var nums = [1, 2, 3, 4, 5];
var sum = nums.reduce((x, y) => x + y);
console.log(sum); // 15

作者:前端技术驿站
来源:https://www.jianshu.com/p/651338c88bb4

收起阅读 »

字节面试被虐后,是时候搞懂 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

    收起阅读 »