注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

【CSS浮动属性】别再纠结布局了!一文带你玩转CSS Float属性

在网页设计的世界里,CSS浮动属性(float)就像一把双刃剑。它能够让元素脱离文档流,实现灵活的布局,但如果处理不当,也可能引发一系列布局问题。今天,我们就来深入探讨这把“剑”的正确使用方法,让你的页面布局既美观又稳定。一、什么是CSS浮动属性浮动属性是CS...
继续阅读 »

在网页设计的世界里,CSS浮动属性(float)就像一把双刃剑。它能够让元素脱离文档流,实现灵活的布局,但如果处理不当,也可能引发一系列布局问题。

今天,我们就来深入探讨这把“剑”的正确使用方法,让你的页面布局既美观又稳定。

一、什么是CSS浮动属性

浮动属性是CSS中的一个定位属性,它允许元素脱离文档流,并向左或向右移动,直到它的外边缘碰到包含框或者另一个浮动元素的边缘。简单来说,它就像是让元素“漂浮”在页面上,不受常规排列规则的限制。

在网站开发中需要一行排列多个元素,使用浮动可以方便实现。下面是使用浮动排列多个元素。

Description

下面我们来了解一下浮动属性的基本使用语法和常用的应用场景都有哪些。

1.1 浮动属性的语法

selector {
float: 值;
}

其中,选择器是你想要应用浮动属性的元素的选择器,值可以是以下之一:

  • none:这是默认值,元素不会浮动,即保持在标准文档流中的位置。

  • left:元素将向左浮动,它会尽量向左移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。

  • right:元素将向右浮动,它会尽量向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。

1.2 浮动的应用场景

CSS浮动属性的应用场景主要包括以下几点:

布局定位:
浮动可以用于创建复杂的页面布局,例如将块级元素放置在一行内,或者创建多列布局。

文本环绕图片:
这是浮动最常见的应用之一,通过将图片设置为浮动,可以使文本自动环绕在图片周围,从而实现类似印刷布局中的文本环绕效果。

清除元素间缝隙:
浮动元素会紧挨着排列,没有间隙,这可以用来清除列表或图片间的空格,使得元素紧密排列。

创建下拉菜单
浮动还常用于创建下拉菜单或弹出式菜单,通过将菜单项设置为浮动,可以实现菜单的显示和隐藏。

实现侧边栏:
在网页设计中,浮动可以用来创建固定在一侧的侧边栏,而主要内容则围绕侧边栏流动。

创建瀑布流布局:
在响应式设计中,浮动可以用来实现瀑布流布局,这种布局可以根据浏览器窗口的大小自动调整列数和列宽。

1.3 盒子的排列规则

在使用浮动属性后盒子是如何排列的呢?

Description

  • 左浮动的盒子向上向左排列
  • 右浮动的盒子向上向右排列
  • 浮动盒子的顶边不得高于上一个盒子的顶边
  • 若剩余空间无法放下浮动的盒子,则该盒子向下移动,直到具备足够的空间能容纳盒子,然后再向左或向右移动

二、浮动的核心特点

下面将通过这个示例来给大家讲解浮动的特点:

<div>
<div>正常元素</div>
<div>
浮动元素
</div>
<div>我是浮动元素后面的第一个同级正常元素</div>
<div>我是浮动元素后面的第二个同级正常元素</div>
</div>
.container{
width: 200px;
height: 200px;
background-color: red;
}
.box1{
background-color: green;
}
.box2{
background-color: brown;
}
.box3{
background-color: pink;
}


.float{
float:left;


background-color: yellow;
}

Description

2.1 包裹性

具有“包裹性”的元素当其未主动设置宽度时,其宽度右内部元素决定。且其宽度最大不会超过其包含块的宽度。

设置了float属性(不为none)的元素都会具有包裹性。

在上面的例子中float元素不设置宽度,其宽度也不会超过container元素的宽度。

2.2 块状化并格式化上下文

设置了float属性(不为none)的元素,无论该元素原本是什么元素类型,其display属性的计算值都会自动变成"block"或’table(针对inline-table元素)'。并且浮动元素会生成一个BFC(块级格式化上下文)。

所以永远不需要对设置了float属性(不为none)的元素再设置"display:block"属性或者vertical-align属性!这都是多余和无效的。

2.3 脱离标准流

设置了float属性(不为none)的元素,都会脱离标准流。标准流一般是针对块级或行级元素(不包括行内块)。

通俗一点解释是,浮动元素A会“漂浮”在标准流上面,此时其原始占用的标准流空间由同级的后续第一个标准流兄弟元素B顶替(但是元素B中的文本内容会记住浮动元素A的位置,并在排布时避开它,由此形成文本环绕效果)。所以会出现B的部分内容会被飘起来的A所遮挡的现象。

有人可能会问,上面的例子中好像没发现类似"A遮挡B"的现象啊?

其实并不是,具体解释如下:
我们将box2元素(即浮动元素后续的第一个同级元素)的文本内容减少一些,使其不换行并不占满一行方便解释效果。

Description

图片这时候发现box2是和container元素宽度200是一致的,而不是自身文本的宽度。由于浮动元素的脱离文档流,.box2会忽略浮动元素的原空间(即当其不存在),由因为普通div不设置宽度默认会是父元素宽度。

所以这里box2和其父元素container宽度一致。但又因为浮动元素会使box2的文本环绕,导致box2的文本重新布局排版,“移动”到了紧跟浮动元素的右边界的地方。所以此时可以看作box2被浮动元素遮挡的那一部分实际是空背景。

2.4 高度坍塌

当你给浮动元素设置了具体宽高度,并增加box2元素的文本内容,也许这种脱离文档流现象更明显,如下示例:

Description

浮动元素脱离标准流的特性很容易带来的一大问题就是——父元素的高度塌陷

我们将html内容精简,只保留浮动元素和box2元素:

<div>
<div>
浮动元素
</div>
<div>我是浮动元素后面的第一个同级元素</div>


</div>

然后设置浮动元素宽高度,并去掉父元素设置的宽高度(核心点)

.container{
/* width: 200px;*/
/*height: 200px;*/
background-color: red;
}
.float{
float:left;
width: 40px;
height: 40px;
background-color: yellow;
}

Description

此时我们发现:没有设置高度的container元素,其实际高度只由标准文档流的box2元素撑起来了21px,而设置了30px高度的浮动元素由于脱离文档流其高度被忽略了。

这就是浮动经典的“高度塌陷”问题了。

2.4 无margin重叠问题

普通的块级元素之间的margin-top和margin-bottom有时会出现margin合并的现象,而浮动元素由于其自身会变成一个BFC(块级格式化上下文),不会影响外部元素,所以不会出现margin重叠问题。

三、清除浮动

清除浮动并不是去掉浮动,而是解决因为浮动带来的副作用的消极影响,也就是我们上面说的父元素高度塌陷问题。

3.1 clear属性

在此之前,我们需要了解另一个CSS属性,就是float的克星——clear

官方对于clear属性的解释是:元素盒子的边不能和前面的浮动元素相邻。其本质在于让当前元素不和前面的float元素在一行显示。

对此我们可以对于clear的属性值形象地理解为:

  • left:元素左边抗浮动

  • right:元素右边抗浮动

  • both:元素两侧抗浮动

注意:由于clear属性只关注当前元素前面的浮动元素,所以使用clear:left/right都是和clear:both等效的。实际上我们只需要用到clear:both即可。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


3.2 清除方法

当今流行的浮动布局已不再是最初的文字环绕了,而是通过给每个子元素添加浮动来实现元素横向排列(一行占不下就换行继续)的布局。

注意:这种横向排列布局建议最好让每个子元素的高度一致,否则可能会出现以下图这种高度不齐引起的布局问题:

Description

即使如此,依然需要解决父元素高度塌陷问题,以下分别对几种常见解决方案简单说明下:

  • 让父元素也浮动

没有从根本解决问题,反而生成了新的浮动问题。

  • 给父元素设置高度

此法太过死板,让父元素高度固定死了,无法自适应高度。

  • 给父元素设置overflow:hidden

此法原理在于让父元素成为一个BFC,唯一缺点容易导致溢出内容被隐藏掉,不过这种场景较少,还是可以用此方法的。

  • 伪元素与clear属性配合(推荐)
/*对浮动元素的父元素设置*/
.clear::after{
clear: both;
content:'';
/*clear属性只对块元素有效,而伪元素::afer默认是行级*/
display: block;
}

CSS浮动属性是网页设计师的重要工具,但也需要谨慎使用。通过今天的介绍,希望你能够更加自信地在你的设计中运用这一属性,创造出既美观又稳定的网页布局。

在CSS的世界里,每一个属性都有其独特的魅力和规则。浮动属性作为布局的强大工具,虽然有时会带来挑战,但只要我们理解它的本质,就能将它变为实现创意设计的利器。

收起阅读 »

【Harmony OS 鸿蒙下载三方依赖 ohpm环境搭建】

1.Harmony 安装 三方依赖 在DevEco Studio 将依赖放到指定的 oh-package-json5 的 dependencies 内2.然后打开 Terminal 执行 :“ohpm install”1.成功会提示失败会提示 2.ohp...
继续阅读 »

1.Harmony 安装 三方依赖 在DevEco Studio 将依赖放到指定的 oh-package-json5 的 dependencies 内

2.然后打开 Terminal 执行 :“ohpm install”
1.成功会提示

失败会提示
2.ohpm not found ! 大概意思就是找不到这个ohpm
3.需要配置环境变量解决该问题
(1)查阅我们的ohpm地址 一定要记住这个地址
Mac端找到该位置路径 点击DevEco Studio ->Preferences

(2)打开终端命令行
输入:echo $SHELL 输入后 单击回车

1.提示/bin/zsh
(1)执行: vi ~/.zshrc
(2)输入 i
(3)export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm
export PATH=${PATH}:${OHPM_HOME}/bin
中间的 xxx 输入 在(1)图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可
(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入“:wq!”,单击回车保存
(5)  输入: source ~/.zshrc;
2.提示/bin/base
(1)执行:vi ~/.bash_profile
(2)输入 i
(3)export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm
export PATH=${PATH}:${OHPM_HOME}/bin
中间的 xxx 输入 在(1)图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可
(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入“:wq!”,单击回车保存
(5)  输入: source ~/.bash_profile
4.检验 ohpm环境是否配置成功:
命令行输入export 查验是否有 ohpm

检验方式第二种 输入 ohpm -v 会显示你的 版本




收起阅读 »

【Java实战项目】SpringBoot + Vue3打造你的在线电子书平台!

今天给大家分享一个基础的Java实战项目,用SpringBoot和Vue3开发一个电子书平台,大家可以尝试做一下这个项目,以此来检验这段时间的学习成果!废话不多说,下面正式进入项目:一、项目介绍1. 项目简介在线电子书微实战项目是一个实践性的基础项目,主要目的...
继续阅读 »

今天给大家分享一个基础的Java实战项目,用SpringBoot和Vue3开发一个电子书平台,大家可以尝试做一下这个项目,以此来检验这段时间的学习成果!废话不多说,下面正式进入项目:

一、项目介绍

1. 项目简介

在线电子书微实战项目是一个实践性的基础项目,主要目的是通过开发一个在线电子书网站来帮助入门学习和实践相关的技术。

预览链接:在线电子书平台项目实战




该项目涵盖了以下主要功能:

  • 电子书管理:主要包括电子书的基本信息、电子书的章节管理、章节信息和章节内容等。

  • 电子书阅读:用户可以浏览在线电子书,并享受连续翻页、目录导航等阅读体验。


通过该项目,可以深入了解电子书网站的设计和开发过程,学习相关的前端和后端技术,并提升实际项目经验。这个项目不仅有助于理论知识的实践运用,还能够培养问题解决的能力。


2. 项目重点

  • 电子书管理:

实现电子书的结构设计,包括电子书信息、章节列表、章节排序和章节编辑等功能,并支持Md格式的内容编写及预览。



  • 电子书阅读界面设计:

重点关注电子书阅读界面的流畅性和易用性,实现灵活的阅读布局和翻页体验,让用户能够快速定位所需内容。


3. 项目目标

旨在通过实践操作,了解和掌握在线电子书平台的基本功能和技术要点。通过完成这些功能,将获得相关的Web前后端技术、数据库管理技术以及用户界面设计等方面的实践经验,并能够将所学知识应用到实际项目中。


4. 项目技术实现

前端技术实现:

主要基于Vite4 + Vue3作为前端框架来进行开发,利用Vue Router进行路由管理,Axios库进行HTTP请求和响应处理等技术和工具。同时还使用Element UI统一页面风格。


为了实现内容的编辑和预览功能,项目还引入了v-md-editor编辑器组件。通过该组件,用户可以方便地编辑和排版电子书的内容,并实时预览效果。这为用户提供了一个直观、便捷的内容管理方式,使其能够快速编辑、修改和发布电子书的内容。


后端技术实现:

项目采用Spring Boot作为后端框架,通过Spring MVC进行请求处理和路由管理,使用MyBatis作为持久层框架进行数据库操作。后端主要实现了API的对接、电子书管理的逻辑处理以及与前端的数据交互。


5. 实现流程


01

规划和设计:

确定项目需求和功能,并进行整体设计和规划,包括前端界面设计、后端API设计以及数据库结构设计等。

02

搭建前端项目框架:

使用Vue3+vite4等前端框架创建项目,并配置相关开发环境和插件,如Vue Router、Axios等。

03

 开发前端页面和功能:

根据设计,开发前端页面组件和功能模块,包括电子书编辑、预览、章节内容管理等功能。

04

设计和创建数据库:

根据需求设计数据库结构,选择MySQL数据库管理系统,创建数据库和相应的表/集合。

05

开发后端API:

使用SpringBoot后端技术,搭建后端服务器,编写API接口,提供与前端交互的数据处理和业务逻辑。

06

处理前后端数据交互:

前端通过Axios等工具发送HTTP请求,后端接收请求,进行数据处理和验证,返回相应的数据结果。

07

数据库操作和持久化:

后端根据接收到的请求,通过数据库操作进行数据的读取、写入、更新和删除等操作,实现数据的持久化存储。

6. 业务流程及页面效果展示

6.1 电子书管理


6.2 电子书的详情(预览)


6.3 电子书基本信息编辑


6.4 电子书章节管理

6.5 电子书章节内容编辑

  • 导入功能:直接导入 Markdown 文件,并将其作为章节内容展示。



  • 导出功能:将章节的内容以 Markdown 文档的形式进行导出


  • 全屏编辑与实时预览:在全屏模式下使用 Markdown 编辑器对章节内容进行编辑,同时能够实时预览编辑后的效果。




7. 总结

在线电子书功能微实战是一项学习实践型的项目,它涉及到多个技术领域,包括前端、后端、数据库和数据交互等。在学习和实践中,我们需要了解项目的需求和功能,并进行规划和设计。


通过选择合适的技术和工具,开发前端页面和后端API实现完整功能,包括电子书编辑、预览、章节内容管理等。这个项目不仅可以帮助我们了解实际的开发流程和技术应用,还可以提升我们的编程能力和实践经验。


二、部署教程


后端部署文档


1、 后端技术栈

  • Java:项目使用Java编程语言进行开发。

  • Spring Boot:项目基于Spring Boot框架搭建。

  • Maven:项目使用Maven作为构建工具和依赖管理工具。

  • MySQL:项目使用MySQL作为关系型数据库。


2、开发环境准备

项目环境所需:

  • 开发工具 IDEA 版本是IntelliJ IDEA 2023.2 (Ultimate Edition)

  • 使用的 Java 环境是 jdk8

  • Maven 使用的 3.8.1

  • Spring Boot 使用的 2.7.10

  • 接口文档 使用 swagger3

  • MySQL 使用的 8.0.31

  • 工具使用 Navicat


3、项目源码下载


源码下载地址:在线电子书平台项目实战源码


4、 构建项目

解压缩下载包得到:electronic_book.sql 数据库SQL文件 和 electronic-book 项目文件夹


4.1. 导入数据库

新建数据库 electronic_book




导入 electronic_book.sql 数据库SQL文件



点击 开始 导入即可

4.2. 编辑器内 导入项目源码

在IDE编辑器内,打开 electronic-book 项目文件夹;


配置项目MAVEN设置,编辑器内 打开 File - Settings,找到MAVEN设置位置;


设置完成后等待MAVEN加载相关依赖,或后续手动在右侧刷新加载。

4.3. 修改数据库配置

在 resources 下 application.yml 文件内,修改合适配置;


4.4. 接口文档查看测试

  • 启动项目


  • 如果端口占用,在 resources 下 application.yml 文件内修改端口再启动


  • 打开 swagger 接口文档查看是否正常启动



5. 源码解析

5.1. 目录结构

电子书业务相关

  • pojo : 电子书相关pojo 实体类及其他

  • mapper : 电子书相关mapper

  • controller - ElectronicBookController: 电子书相关接口

  • controller - ElectronicBookItemController: 电子书内容相关接口

  • service : 电子书相关接口实现


公共类

  • baseClass : 处理统一业务,如参数校验等

  • config - CrosConfig: 处理测试时可能存在的跨域问题

  • config - SwaggerConfig: swagger文档配置

  • enumeration : 业务枚举等

  • exception : 通用异常返回处理

  • result : 统一结果返回

  • utils : 常用工具类


5.2. README

具体源码解析以及电子书管理介绍和流程等,查看源码文件 read.md以及相关文件内注释详情。



前端部署文档

1. 前端技术栈

vite4 + vue3 + scss(1.69.5) + Typescript(5.2.0)+ @kangc/v-md-editor(2.3.18)

  • Vite是一款快速构建现代web应用程序的构建工具,特别适合于Vue.js应用程序。

  • Vue3是一款流行的JavaScript框架。

  • SCSS是一种CSS预处理器,它允许开发者使用类似编程语言的方式来编写CSS,提高代码的可维护性和重用性。

  • TypeScript是一种由微软开发的静态类型检查的JavaScript超集。

  • @kangc/v-md-editor是基于 Vue 开发的 markdown 编辑器组件。


2. 开发环境

  • Node.js 和 npm 的安装
  • 代码编辑器的选择

    推荐:VSCode + Volar(并禁用Vetur) + TypeScript Vue插件(Volar)


3. 准备项目

3.1. 解压项目源码

将解压缩下载包得到的前端项目文件夹(electronic-book-view),放到到指定的文件夹(例如:E:\workspace_dev\project-practice\electronic-book-view)。


3.2. 更改配置文件
  • 代码编辑器中打开项目源码文件夹
  • api地址配置

点击\src\config.ts

// 配置api地址(以实际项目服务器地址为准)
const baseURL ="http://loacalhost:8087/";


4. 启动项目

4.1. 打开命令行界面
4.2. 安装依赖

在命令窗口或者终端窗口输入命令 npm install


4.3. 构建编译

在命令窗口或者终端窗口输入命令 npm run dev


注意:若端口号3000被占用,会自动重新分配一个空闲的端口号,具体以实际为准。

若启动失败,有报错,请具体问题具体分析,如有不懂也可在云端源想【技术咨询】咨询老师进行答疑解惑(地址:https://www.ydcode.cn/)也可点击文末的阅读原文直接跳转。

4.4. 访问测试

上述启动命令脚本,已经配置默认浏览器打开访问模式,若无正常打开先自行尝试解决,若有问题继续咨询老师解答。


默认打开访问地址为:http://localhost:3000/



4.5. 终止项目

若需要终止项目,首先聚焦在命令窗口或者终端窗口,然后按键CTRL+c即可终止项目。


5. 前端项目说明

请参阅项目的 README.md 文档,其中包含了项目的介绍、安装说明和使用方法等关键信息。以下是其中的重点信息展示:


5.1 引入 markdown 编辑器 介绍

本项目中采用 v-md-editor 进阶版编辑器,进阶版编辑器左侧编辑区域使用 CodeMirror (opens new window)实现。


进阶版编辑器可以根据 CodeMirror 提供的 Api 来自定义扩展编辑区域功能,提高编辑体验。但是文件体积远大于轻量版。使用者可根据所在项目的情况进行选择。

5.2 项目文件结构介绍
/
├── public
│ ├── favicon.ico # vue官方自带标识
│ └── logo.svg # 浏览器标签页logo
├── src # 项目源代码
│ ├── api # 用于存放与后端 API 相关的接口定义。
│ ├── assets # 用于存放项目所需的静态资源文件,例如图片、字体等。
│ ├── components # 用于存放可复用的组件。
│ ├── router # 路由的定义和配置
│ ├── styles # 样式文件
│ │ └── main.scss # 全局的 SCSS 变量等
│ ├── views # 页面组件
│ │ ├──components # 页面所需组件
│ │ └──xxx.vue # 页面
│ ├── app.vue # 应用程序根组件
│ ├── config.ts # 应用程序根组件
│ └── main.ts # 应用程序入口
├── .gitinore # git忽略配置文件
├── env.d.ts # 用于声明环境变量的类型
├── index.html # 整个应用的入口HTML文件
├── package-lock.json # 用于锁定安装时的依赖版本
├── package.json # 应用的配置文件,其中包含了项目的依赖、脚本命令等信息。
├── README.md # 项目的说明文档,通常包含了项目的介绍、安装和使用方法等信息。
├── tsconfig.app.json # 用于前端应用程序的TypeScript编译配置
├── tsconfig.json # TypeScript 项目的配置文件
├── tsconfig.node.json # 用于后端(服务器端)应用程序的TypeScript编译配置
└── vite.config.js # Vite 的配置文件

6. 常见问题处理

  • node和npm版本问题

建议:Vite 4 需要 Node.js 版本 ≥ 16.0.0,npm 版本 ≥ 8.0.0。但是,某些模板需要更高的 Node.js 版本才能工作,如果您的包管理器发出警告,请升级。

  • 依赖安装失败

报错:ERESOLVE unable to resolve dependency tree


三、源码下载

到这里这个运用SpringBoot + Vue3打造在线电子书平台项目介绍及部署就讲完了,小伙伴可以自己尝试写一下这个项目来巩固练习你所学的Java知识哦!

完整的源码下载地址:在线电子书平台源码

收起阅读 »

如何利用容器与中间件实现微服务架构下的高可用性和弹性扩展

本文分享自天翼云开发者社区《如何利用容器与中间件实现微服务架构下的高可用性和弹性扩展》,作者:c****w在当今的互联网时代,微服务架构已经成为许多企业选择的架构模式,它能够提高系统的灵活性、可维护性和可扩展性。然而,微服务架构下的高可用性和弹性扩展是一个复杂...
继续阅读 »

本文分享自天翼云开发者社区《如何利用容器与中间件实现微服务架构下的高可用性和弹性扩展作者:c****w

在当今的互联网时代,微服务架构已经成为许多企业选择的架构模式,它能够提高系统的灵活性、可维护性和可扩展性。然而,微服务架构下的高可用性和弹性扩展是一个复杂的挑战。本文将介绍如何利用容器与中间件来实现微服务架构下的高可用性和弹性扩展的解决方案。

1. 理解微服务架构下的高可用性和弹性扩展需求

在微服务架构中,系统由多个微小的服务组成,每个服务都是一个独立的单元,可以独立部署和扩展。因此,要实现高可用性和弹性扩展,需要考虑以下几个方面:

· 服务的自动发现和注册

· 服务的负载均衡和容错处理

· 弹性扩展和自动伸缩

· 故障自愈和自动恢复

2. 利用容器实现微服务的高可用性

容器技术如Docker和Kubernetes可以帮助我们实现微服务的高可用性。首先,我们可以将每个微服务打包成一个独立的容器镜像,然后使用Kubernetes进行容器编排和调度。Kubernetes可以自动监控容器的健康状态,并在发生故障时自动进行容器的重启,从而保证微服务的高可用性。此外,Kubernetes还支持多种负载均衡和服务发现的机制,可以确保请求能够被正确路由到可用的服务实例上。

3. 中间件的应用实现微服务的弹性扩展

在微服务架构中,服务的请求量可能会有很大的波动,因此需要实现弹性扩展来应对高峰时期的流量。这时候,可以利用中间件来实现微服务的弹性扩展。比如,可以使用消息队列来实现异步处理,将请求发送到消息队列中,然后由多个消费者并发处理请求。这样可以有效地应对流量的波动,提高系统的弹性。

4. 实现自动化的监控和故障处理

为了保证微服务架构的高可用性和弹性扩展,需要实现自动化的监控和故障处理机制。可以利用监控系统来实时监控微服务的健康状态和性能指标,一旦发现故障,可以自动触发故障处理流程,比如自动进行容器的重启或者自动进行服务实例的扩展。这样可以大大提高系统的自愈能力,保证系统的高可用性。

结论

通过利用容器和中间件,我们可以很好地实现微服务架构下的高可用性和弹性扩展。容器技术可以帮助我们实现微服务的高可用性,而中间件可以帮助我们实现微服务的弹性扩展。通过自动化的监控和故障处理机制,可以保证系统的高可用性,从而更好地满足业务需求。

希望以上内容能够帮助您更好地理解如何利用容器与中间件实现微服务架构下的高可用性和弹性扩展。

收起阅读 »

Maven下载安装与配置、Idea配置Maven(详细版)

Maven是Apache软件基金会的一个开源项目,是一款优秀的项目构建工具,它主要用于帮助开发者管理项目中jar以及jar之间的依赖关系,最终完成项目编译,测试,打包和发布等工作。前面我们已经简单介绍了Maven的概念、特点及使用,本篇文章就来给大家出一个详细...
继续阅读 »

Maven是Apache软件基金会的一个开源项目,是一款优秀的项目构建工具,它主要用于帮助开发者管理项目中jar以及jar之间的依赖关系,最终完成项目编译,测试,打包和发布等工作。

前面我们已经简单介绍了Maven的概念、特点及使用,本篇文章就来给大家出一个详细的安装和配置教程,还没有安装Maven的小伙伴要赶紧收藏起来哦!

首先给大家解释一下为什么学习Java非要学Maven不可。

为什么要学习Maven?

大家在读这篇文章之前大部分人都已经或多或少的经历过项目,说到项目,在原生代码无框架的时候,最痛苦的一件事情就是要在项目中导入各种各样使用的jar包,jar太多就会导致项目很难管理。需要考虑到jar包之间的版本适配的问题还有去哪找项目中使用的这么多的jar包,等等。这个时候,Maven就出现了,它轻松解决了这些问题。

说的直白一点,maven就相当于个三方安装包平台,可以通过一些特殊的安装方式,把一些三方的jar包,装到咱们自己的项目中。

Maven安装以及配置

Maven下载地址:https://maven.apache.org/

打开欢迎界面点击download下载

Description

Description

Description

在这里我们安装的是Maven的3.6.3版本,下载好之后是一个压缩包

Description

在电脑中找一个地方把压缩包解压,解压后

Description

配置环境变量

Description

在这儿一定要注意配置路径

Description

点击开始菜单 ->搜查框输入:cmd 回车 -> 出现Maven版本号说明安装成功

Description

Maven配置本地仓库

如何将下载的 jar 文件存储到我们指定的仓库中呢?需要在 maven 的服务器解压的文件中找到 conf 文件夹下的 settings.xml 文件进行修改,如下图所示:

Description

进入文件夹打开settings.xml文件

Description

因为默认的远程仓库地址,是国外的地址;在国内为了提高下载速度,可在如图所示位置配置阿里云仓库

Description

在idea工具中配置Maven

打开idea-----点击File-----点击New Projects Settings-----点击Setting for New Projects…

Description

这样,我们的Maven就安装配置完成了。


在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,

点击这里,立即开始你的学习之旅!


最后,附上我们一个配置文件,里面其实很大篇幅都是注释,为我们解释标签的用途,核心是咱们配置的远程中央仓库地址;在有的公司,是有自己的远程私有仓库地址的,配置方式跟中央仓库配置基本雷同,可能有一些账号密码而已。


<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->


<!--
| This is the configuration file for Maven. It can be specified at two levels:
|
| 1. User Level. This settings.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -s /path/to/user/settings.xml
|
| 2. Global Level. This settings.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.conf}/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gs /path/to/global/settings.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation. Where appropriate, the default
| values (values used when the setting is not specified) are provided.
|
|-->

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->


<!-- interactiveMode
| This will determine whether maven prompts you when it needs input. If set to false,
| maven will use a sensible default value, perhaps based on some other setting, for
| the parameter in question.
|
| Default: true
<interactiveMode>true</interactiveMode>
-->


<!-- offline
| Determines whether maven should attempt to connect to the network when executing a build.
| This will have an effect on artifact downloads, artifact deployment, and others.
|
| Default: false
<offline>false</offline>
-->


<!-- pluginGroups
| This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
| when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
| "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
|-->

<pluginGroups>
<!-- pluginGroup
| Specifies a further group identifier to use for plugin lookup.
<pluginGroup>com.your.plugins</pluginGroup>
-->

</pluginGroups>

<!-- proxies
| This is a list of proxies which can be used on this machine to connect to the network.
| Unless otherwise specified (by system property or command-line switch), the first proxy
| specification in this list marked as active will be used.
|-->

<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->

</proxies>

<!-- servers
| This is a list of authentication profiles, keyed by the server-id used within the system.
| Authentication profiles can be used whenever maven must make a connection to a remote server.
|-->

<servers>

<!-- server
| Specifies the authentication information to use when connecting to a particular server, identified by
| a unique name within the system (referred to by the 'id' attribute below).
|
| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are
| used together.
|
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>
-->


<!-- Another sample, using keys to authenticate.
<server>
<id>siteServer</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>optional; leave empty if not used.</passphrase>
</server>
-->

</servers>

<!-- mirrors
| This is a list of mirrors to be used in downloading artifacts from remote repositories.
|
| It works like this: a POM may declare a repository to use in resolving certain artifacts.
| However, this repository may have problems with heavy traffic at times, so people have mirrored
| it to several places.
|
| That repository definition will have a unique id, so we can create a mirror reference for that
| repository, to be used as an alternate download site. The mirror site will be the preferred
| server for that repository.
|-->

<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->

<mirror>
<id>aliyun</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>




</mirrors>

<!-- profiles
| This is a list of profiles which can be activated in a variety of ways, and which can modify
| the build process. Profiles provided in the settings.xml are intended to provide local machine-
| specific paths and repository locations which allow the build to work in the local environment.
|
| For example, if you have an integration testing plugin - like cactus - that needs to know where
| your Tomcat instance is installed, you can provide a variable here such that the variable is
| dereferenced during the build process to configure the cactus plugin.
|
| As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
| section of this document (settings.xml) - will be discussed later. Another way essentially
| relies on the detection of a system property, either matching a particular value for the property,
| or merely testing its existence. Profiles can also be activated by JDK version prefix, where a
| value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
| Finally, the list of active profiles can be specified directly from the command line.
|
| NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
| repositories, plugin repositories, and free-form properties to be used as configuration
| variables for plugins in the POM.
|
|-->

<profiles>
<!-- profile
| Specifies a set of introductions to the build process, to be activated using one or more of the
| mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
| or the command line, profiles have to have an ID that is unique.
|
| An encouraged best practice for profile identification is to use a consistent naming convention
| for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
| This will make it more intuitive to understand what the set of introduced profiles is attempting
| to accomplish, particularly when you only have a list of profile id's for debug.
|
| This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
<profile>
<id>jdk-1.4</id>

<activation>
<jdk>1.4</jdk>
</activation>

<repositories>
<repository>
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<url>http://www.myhost.com/maven/jdk14</url>
<layout>default</layout>
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>
-->


<!--
| Here is another profile, activated by the system property 'target-env' with a value of 'dev',
| which provides a specific path to the Tomcat instance. To use this, your plugin configuration
| might hypothetically look like:
|
| ...
| <plugin>
| <groupId>org.myco.myplugins</groupId>
| <artifactId>myplugin</artifactId>
|
| <configuration>
| <tomcatLocation>${tomcatPath}</tomcatLocation>
| </configuration>
| </plugin>
| ...
|
| NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
| anything, you could just leave off the <value/> inside the activation-property.
|
<profile>
<id>env-dev</id>

<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>

<properties>
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>
-->


</profiles>

<!-- activeProfiles
| List of profiles that are active for all builds.
|
<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>
-->

</settings>


收起阅读 »

Java开发者必备:Maven简介及使用方法详解!

今天我们来介绍一个在Java开发中非常重要的工具——Maven。如果你是一名Java开发者,那么你一定不会对Maven感到陌生。但是,对于一些新手来说,可能还不太了解Maven是什么,它有什么作用,以及如何使用它。接下来,就让我们一起来深入了解一下Maven吧...
继续阅读 »

今天我们来介绍一个在Java开发中非常重要的工具——Maven。如果你是一名Java开发者,那么你一定不会对Maven感到陌生。但是,对于一些新手来说,可能还不太了解Maven是什么,它有什么作用,以及如何使用它。接下来,就让我们一起来深入了解一下Maven吧!

一、maven简介

Maven是什么

Maven是一个项目管理工具,它包含了一个项目对象模型 (Project Object Model),一组标准集合,一个项目生命周期(Project Lifecycle),一个依赖管理系统(Dependency Management System),和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。maven是基于Ant 的构建工具,Ant 有的功能Maven 都有,额外添加了其他功能。

Maven提供了一套标准化的项目结构,所有IDE使用Maven构建的项目结构完全一样,所有IDE创建的Maven项目可以通用。

Maven是专门用于管理和构建Java项目的工具,它的主要功能有:

  • 提供了一套标准化的项目结构

  • 提供了一套标准化的构建流程(编译、测试、打包、发布 …)

  • 提供了一套依赖管理机制

Maven作用

  • 项目构建管理:maven提供一套对项目生命周期管理的标准,开发人员、和测试人员统一使用maven进行项目构建。项目生命周期管理:编译、测试、打包、部署、运行。

  • 管理依赖(jar包):maven能够帮我们统一管理项目开发中需要的jar包。

  • 管理插件:maven能够帮我们统一管理项目开发过程中需要的插件。

二、maven仓库

用过maven的同学,都知道maven可以通过pom.xml中的配置,就能够获取到想要的jar包,但是这些jar是在哪里呢?就是我们从哪里获取到的这些jar包?答案就是仓库。

仓库分为:本地仓库、第三方仓库(私服)和中央仓库。
Description

1、本地仓库

本地仓库:计算机中一个文件夹,自己定义是哪个文件夹。Maven会将工程中依赖的构件(Jar包)从远程下载到本机的该目录下进行管理。

maven默认的仓库是$user.home/.m2/repository目录。

本地仓库的位置可以在$MAVEN_HOME/conf/setting.xml文件中修改。

在文件中找到localRepository目录,修改对应内容即可
<localRepository>D:/maven/r2/myrepository</localRepository>

2、中央仓库

中央仓库:网上地址https://repo1.maven.org/maven2/

这个公共仓库是由Maven自己维护,里面有大量的常用类库,并包含了世界上大部分流行的开源项目构件。工程依赖的jar包如果本地仓库没有,默认从中央仓库下载。

由于maven的中央仓库在国外,所以下载速度比较慢,所以需要配置国内的镜像地址。

在配置文件中找到mirror标签,添加以下内容即可。

<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>

3、第三方仓库(私服)

第三方仓库,又称为内部中心仓库,也称为私服。

私服:一般是由公司自己设立的,只为本公司内部共享使用。它既可以作为公司内部构件协作和存档,也可作为公用类库镜像缓存,减少在外部访问和下载的频率,公司单独开发的私有jar可放置到私服中。(使用私服为了减少对中央仓库的访问)

注意:连接私服,需要单独配置。如果没有配置私服,默认不使用

三、Maven的坐标

什么是坐标?
Maven中的坐标是资源的唯一标识,使用坐标来定义项目或引入项目中需要的依赖。
Description

Maven坐标的主要组成:

  • groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.baidu)

  • artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)

  • version:定义当前项目版本号

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

四、Maven的三套生命周期

什么是生命周期

在Maven出现之前,项目构建的生命周期就已经存在,软件开发人员每天都在对项目进行清理,编译,测试及部署。虽然大家都在不停地做构建工作,但公司和公司间,项目和项目间,往往使用不同的方式做类似的工作。
Description
Maven的生命周期就是为了对所有的构建过程进行抽象和统一。Maven从大量项目和构建工具中学习和反思,然后总结了一套高度完美的,易扩展的生命周期。

这个生命周期包含了项目的清理,初始化,编译,测试,打包,集成测试,验证,部署和站点生成等几乎所有构建步骤。

Maven的生命周期是抽象的,这意味着生命周期本身不做任何实际工作,在Maven的设计中,实际任务(如源代码编译)都交由插件来完成。

Maven的三套生命周期

Maven拥有三套相互独立的生命周期,分别是clean,default和site。

Description

clean生命周期

clean生命周期的目的是清理项目,它包含三个阶段:

  • pre-clean 执行一些清理前需要完成的工作

  • clean 清理上一次构建生成的文件

  • post-clean 执行一些清理后需要完成的工作

default生命周期

default生命周期定义了真正构建项目需要执行的所有步骤,它是所有生命周期中最核心的部分。其中的重要阶段如下:

  • compile :编译项目的源码,一般来说编译的是src/main/java目录下的java文件至项目输出的主classpath目录中

  • test :使用单元测试框架运行测试,测试代码不会被打包或部署

  • package :接收编译好的代码,打包成可以发布的格式,如jar和war

  • install:将包安装到本地仓库,供其他maven项目使用

  • deploy :将最终的包复制到远程仓库,供其他开发人员或maven项目使用

site生命周期

site生命周期的目的是建立和发布项目站点,maven能够基于pom文件所包含的项目信息,自动生成一个友好站点,方便团队交流和发布项目信息。该生命周期中最重要的阶段如下:

  • site :生成项目站点文档

  • Maven生命周期相关命令

  • mvn clean:调用clean生命周期的clean阶段,清理上一次构建项目生成的文件

  • mvn compile :编译src/main/java中的java代码

  • mvn test :编译并运行了test中内容

  • mvn package:将项目打包成可发布的文件,如jar或者war包;

  • mvn install :发布项目到本地仓库

Maven生命周期相关插件

Maven的核心包只有几兆大小,核心包中仅仅定义了抽象的生命周期。生命周期的各个阶段都是由插件完成的,它会在需要的时候下载并使用插件,例如我们在执行mvn compile命令时实际是在调用Maven的compile插件来编译。

我们使用IDEA创建maven项目后,就不需要再手动输入maven的命令来构建maven的生命周期了。IDEA给每个maven构建项目生命周期各个阶段都提供了图形化界面的操作方式。

具体操作如下:

  • 打开Maven视图:依次打开Tool Windows–>Maven Projects

  • 执行命令:双击Lifecycle下的相关命令图标即可执行对应的命令(或者点击运行按钮)

Description

五、maven的版本规范

maven使用如下几个要素来唯一定位某一个jar:

  • Group ID:公司名。公司域名倒着写

  • Artifact ID:项目名

  • Version:版本

发布的项目有一个固定的版本标识来指向该项目的某一个特定的版本。maven在版本管理时候可以使用几个特殊的字符串SNAPSHOT,LATEST ,RELEASE。比如"1.0-SNAPSHOT"。

各个部分的含义和处理逻辑如下说明:

  • SNAPSHOT 正在开发中的项目可以用一个特殊的标识,这种标识给版本加上一个"SNAPSHOT"的标记。

  • LATEST 指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个snapshot版,具体看哪个时间最后。

  • RELEASE 指最后一个发布版。

六、maven项目之间的关系

依赖关系

  • 标签把另一个项目的jar引入到当过前项目

  • 自动下载另一个项目所依赖的其他项目

Description

继承关系

  • 父项目是pom类型,子项目jar 或war,如果子项目还是其他项目的父项目,子项目也是pom 类型。

  • 有继承关系后,子项目中出现标签

  • 如果子项目和和与父项目相同,在子项目中可以不配置和父项目pom.xml 中是看不到有哪些子项目,在逻辑上具有父子项目关系。

父项目
<groupId>cn.zanezz.cn</groupId>
<artifactId>demoparent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
子项目
<parent>
<artifactId>demoparent</artifactId>
<groupId>cn.zanezz.cn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<artifactId>child2</artifactId>

聚合关系

  • 前提是继承关系,父项目会把子项目包含到父项目中。

  • 子项目的类型必须是Maven Module 而不是maven project

  • 新建聚合项目的子项目时,点击父项目右键新建Maven Module

子项目中的pom.xml
<parent>
<artifactId>demoparent</artifactId>
<groupId>cn.zanezz.cn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
父项目中的pom.xml
<groupId>cn.zanezz.cn</groupId>
<artifactId>demoparent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>child1</module>
<module>child2</module>
</modules>

聚合项目和继承项目区别

  • 在语意上聚合项目父项目和子项目关系性较强;

  • 在语意上单纯继承项目父项目和子项目关系性较弱。

Maven是一个非常强大的工具,它可以帮助我们更好地管理和构建Java项目。如果你是Java开发者,那么你一定不能错过这个工具。希望这篇文章能帮助你更好地理解和使用Maven,祝你在Java开发的道路上越走越远!

收起阅读 »

【Java集合】单列集合Set:HashSet与LinkedHashSet详解,为什么它比List接口更严格?

前言 上篇我们介绍了单列集合中常用的list接口,本篇我们来聊聊单列集合中的另外一个重要接口Set集合。1、Set 介绍java.util.Set接口和java.util.List接口一样,同样实现了Collection接口,它与Collection...
继续阅读 »

前言 上篇我们介绍了单列集合中常用的list接口,本篇我们来聊聊单列集合中的另外一个重要接口Set集合。

1、Set 介绍

java.util.Set接口和java.util.List接口一样,同样实现了Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。


与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入的元素不出现重复,这里的某种规则,我们在后面中给大家揭秘,大家不要着急。
1 无序
2 不可重复

它没有索引,所以不能使用普通for 循环进行遍历。

Set 集合 遍历元素的方式  迭代器,增强for

来,我们通过案例练习来看看


//创建集合对象

HashSet hs = new HashSet();

//使用Collection的方法添加元素
hs.add("hello");
hs.add("world");
hs.add("java");
//无法添加,在执行这行代码时,会报错误
hs.add("world");
//遍历
for(String s : hs) {
System.out.println(s);
}

Set接口类型,定义变量,Collection的常用方法 add()没有报错,说明Set 完全实现了Collection中的方法;
在添加代码 hs.add("world");无法加入,验证了 Set的不可重复;
多次运行遍历发现,输入的顺序是改变的,说明Set是无序的。

Set集合有多个实现子类,这里我们介绍其中的java.util.HashSet、java.util.LinkedHashSet这两个集合。

2、HashSet 集合介绍

通过java文档,我们知道java.util.HashSet是Set接口的一个实现类

  • 它所存储的元素是不可重复的

  • 元素都是无序的(即存取顺序不一致)

  • 没有索引,没有带索引的方法,也不能使用普通for循环遍历

  • java.util.HashSet  是由哈希表(实际上是一个 HashMap 实例)支持,换句话说它的底层的实现数据结构是 哈希表结构,而哈希表结构的特点是查询速度非常快。

我们先来使用一下HashSet集合,体验一下,在进行讲解:

    public class Demo1Set {

public static void main(String[] args) {

//创建集合对象
HashSet hs = new HashSet();

//添加元素
hs.add("hello");
hs.add("world");
hs.add("java");

hs.add("world");

//使用增强for遍历
for(String s : hs) {
System.out.println(s);
}

}
}

输出结果如下
world
java
hello

***发现world 单词只存储了一个,集合中没有重复元素***

3、HashSet集合存储数据的结构

3.1 哈希表数据结构

我们在前面的文章中,已经介绍过基本的数据结构,大家可以回顾以下。

哈希表是什么呢?简单的理解,在Java中,哈希表有两个版本,在jdk1.8 之前,哈希表 = 数组+链表 ,而在 jdk1.8 之后,哈希表 = 数组+链表+红黑树(一种特殊的二叉树) 我们在这里对哈希表的讲述,为了便于我们学习,只需要记住它在计算机中的结构图即可。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询…无论你是初学者还是有经验的开发者,这里都有你需要的一切。最重要的是,所有资源完全免费!点击这里,立即开始你的学习之旅!


再一个,现在我们大多使用的jdk版本是1.8之后的,所以我们讲解的哈希表结构是 第二个版本。 废话不多说,来看看图吧:

  1. 我们在之前已经介绍过数组和链表的结构图,所以我们在这里就来简单介绍一下红黑树结构。

我们在生活中树的结构,树根-树枝-叶子。计算机世界的树,刚好与我们现实中的树成镜像相反,树根在上,树枝在下。那每个树枝上只有不超过两个叶子的就叫做  二叉树,而红黑树就是我们一颗特殊的二叉树,结构就是这样: image

  1. 说完红黑二叉树呢,我们来看看我们的哈希表结构图:

    1. 有数组

    2. 有链表

    3. 有红黑树

    image

    3.2 哈希值

    我们刚刚通过哈希表的介绍,知道 元素在结构中的存放位置,是根据hash值来决定的,而 链表的长度达到某些条件就可能触发把链表演化为一个红黑树结构。那么hash值是什么呢?说白了就是个数字,只不过是通过一定算法计算出来的。
    接下来我们介绍一下:

    • 哈希值:是JDK 根据对象地址或者字符串或者数字算出来的 int 类型的数值

    • 如何获取哈希值?

    在java基础中,我们学习过 Object类是所有类的基类,每个类都默认继承Object类。通过API 文档查找,有个方法 public int hashCode():返回对象的哈希码值

    我们看下这个方法的使用

    首先定义一个person类

    具有两个属性,设置getset方法,设置构造函数

       public class Demo2HashCode {

    public static void main(String[] args) {

    String str1 = "hello";
    String str2 = new String("hello");

    System.out.println("str1 hashcode =" + str1.hashCode());
    System.out.println("str2 hashcode =" + str2.hashCode());

    //通过上下两段代码的对比,我们可以知道 String 类重写了HashCode()方法。

    Student student = new Student("玛丽", 20);
    Student student2 = new Student("沈腾", 30);
    System.out.println("student hashcode =" + student.hashCode());
    System.out.println("student2 hashcode =" + student2.hashCode());

    }
    }

    好,我们了解了hash值概念和获取方式,那我们就来看看元素事怎么加入到 hashset中的

    3.3  添加元素过程

    那么向 HashSet集合中,添加一个元素时,到底执行了哪些流程呢? 首先我们在实例化HashSet 对象,同过api文档,在调用空参构造器后,创建了一个长度为16的数组。 其次在调用add方法之后,它的执行流程如下:

    1. 根据对象的哈希值计算存储位置

          如果当前位置没有元素则直接存入
      如果当前位置有元素存在,则进入第二步
    2. 当前元素的元素和已经存在的元素比较哈希值如果哈希值不同,则将当前元素进行存储如果哈希值相同,则进入第三步

    3. 通过equals()方法比较两个元素的内容如果内容不相同,则将当前元素进行存储如果内容相同,则不存储当前元素流程图来表示:

    4. image

    了解了元素添加的原理后,在添加元素的过程中,如果说某一条链表达到一定数量,就会触发条件,去演化为一条红黑树,防止我们学起来太吃力,在这里不做深入探究。 总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。

    4、HashSet 存储自定义类型元素

        //优化后的student
    public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

    @Override
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }

    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Student)) return false;

    Student student = (Student) o;

    if (age != student.age) return false;
    return name.equals(student.name);
    }

    @Override
    public int hashCode() {
    int result = name.hashCode();
    result = 31 * result + age;
    return result;
    }
    }


    public class Demo3HashSet {

    public static void main(String[] args) {

    HashSet students = new HashSet<>();

    Student student = new Student("玛丽", 20);
    Student student2 = new Student("沈腾", 30);
    Student student3 = new Student("沈腾", 30);

    students.add(student);
    students.add(student2);
    students.add(student3);

    for (Student studentObj : students) {
    System.out.println("Hashset 元素=" + studentObj);
    }
    }
    }

    执行结果
    Hashset 元素=Student{name='玛丽', age=20}
    Hashset 元素=Student{name='沈腾', age=30}

    5、LinkedHashSet

    我们知道HashSet 保证元素的唯一,可以元素存放进去是没有顺序的,那么我们有没有办法保证有序呢? 打开API文档,我们查看 HashSet下面有一个子类 java.util.LinkedHashSet,这个名字听起来和我们之前学过的LinedList  有点像呢。通过文档,LinkedHashSet 具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。 简单的理解为:在进行集合添加元素的同时,不仅经过程序的执行形成一个哈希表结构,还维护了一个记录插入前后顺序的双向链表。 我们通过案例来感受一下:

        public class Demo4LinkedHashSet {

    public static void main(String[] args) {

    // Set demo = new HashSet<>();

    Set demo = new LinkedHashSet<>();
    demo.add("hello");
    demo.add("world");
    demo.add("ni");
    demo.add("hao");
    demo.add("hello");

    for (String content : demo) {
    System.out.println("---" + content);
    }
    }
    }

    执行结果:
    第一次:
    ---world
    ---hao
    ---hello
    ---ni

    第二次:
    ---hello
    ---world
    ---ni
    ---hao

    小节

    到这里我们已经讲完了单列集合的两种基本接口,分别是List 和 Set,然后通过一些简单的案例,我们也初步了解了它们的使用,其实大家可能还是有些懵逼的;但是在这里,还是要多练习,多看看别人的代码,看看别的优秀的代码,是在怎样的场景下使用的,是怎么互相转换使用的;希望在大家阅读这个系列文章的时候,一方面使一些刚学习的新人,有所帮助;一方面为一些已经工作了一段时间的朋友,温故而知新;更重要的一点,让大家知道,技术的出现,是为了解决问题而创造出来的,他本来就是为了解决生活的难题,只不过使用了一些好的,快捷的方式而已。
    可能文章中有些地方讲的不恰当,大家可以私信,探讨探讨,互相提高。本篇就到这里,happy ending!

      收起阅读 »

      数据库入门:掌握MySQL数据库的五大基本操作,轻松驾驭数据世界!

      对数据库进行查询和修改操作的语言叫做 SQL(Structured Query Language,结构化查询语言)。SQL 语言是目前广泛使用的关系数据库标准语言,是各种数据库交互方式的基础。在之前的文章中,我们已经掌握了SQL语言的基本概念以及常用的DDL(...
      继续阅读 »

      对数据库进行查询和修改操作的语言叫做 SQL(Structured Query Language,结构化查询语言)。SQL 语言是目前广泛使用的关系数据库标准语言,是各种数据库交互方式的基础。

      在之前的文章中,我们已经掌握了SQL语言的基本概念以及常用的DDL(数据定义)和DML(数据操作)语句。接下来,我们将探讨如何运用这些知识进行MySQL数据库的操作。在本篇文章中,我们将详细介绍基本的增、删、改、查等操作方法。

      Description

      首先我们来回顾一下标识符命名规则:

      • 数据库名、表名不得超过30个字符,变量名限制为29个。

      • 必须只能包含 A–Z, a–z, 0–9, _共63个字符。

      • 数据库名、表名、字段名等对象名中间不要包含空格;

      • 同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名;同一个表中,字段不能重名。

      • 必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使用`(着重号)引起来。

      • 保持字段名和类型的一致性:在命名字段并为其指定数据类型的时候一定要保证一致性,假如数据类型在一个表里是整数,那在另一个表里可就别变成字符型了。

      下面我们来看数据库的基本操作。

      一、创建数据库

      在 MySQL 中,可以使用 CREATE DATABASE 语句创建数据库,下面我们介绍三种创建数据库的方式:
      方式1:创建数据库 ( 数据库名存在时,会报错 )

      CREATE DATABASE 数据库名;

      方式2:创建数据库并指定字符集 ( 数据库名存在时,会报错 )

      CREATE DATABASE 数据库名 CHARACTER SET 字符集;

      ('不指定字符集会选择默认字符集')    

      方式3:判断数据库是否已经存在,不存在则创建数据库 (推荐)

      CREATE DATABASE IF NOT EXISTS 数据库名;

      注意: DATABASE 不能改名。一些可视化工具可以改名,它是建新库,把所有表复制到新库,再删旧库完成的。

      编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

      实例:最简单的创建MySQL数据库的语句
      在 MySQL 中创建一个名为 test_db 的数据库。在 MySQL 命令行客户端输入 SQL 语句CREATE DATABASE test_db;即可创建一个数据库,输入的 SQL 语句与执行结果如下。

      Description

      “Query OK, 1 row affected (0.12 sec);”提示中,“Query OK”表示上面的命令执行成功,“1 row affected”表示操作只影响了数据库中一行的记录,“0.12 sec”则记录了操作执行的时间。

      若再次输入CREATE DATABASE test_db;语句,则系统会给出错误提示信息,如下所示:

      Description

      提示不能创建“test_db”数据库,数据库已存在。MySQL 不允许在同一系统下创建两个相同名称的数据库。

      可以加上IF NOT EXISTS从句,就可以避免类似错误,如下所示:

      Description

      二、查看数据库

      在 MySQL 中,可使用 SHOW DATABASES 语句来查看或显示当前用户权限范围以内的数据库。语法格式如下:

      查看当前所有的数据库

      SHOW DATABASES; #有一个S,代表多个数据库

      查看当前正在使用的数据库

      SELECT DATABASE(); #使用的一个 mysql 中的全局函数

      查看指定库下所有的表

      SHOW TABLES FROM 数据库名;

      查看数据库的创建信息

      SHOW CREATE DATABASE 数据库名;
      或者:
      SHOW CREATE DATABASE 数据库名\G

      注意: 要操作表格和数据之前必须先说明是对哪个数据库进行操作,否则就要对所有对象加上“数据库名.”。

      实例1:查看所有数据库

      列出当前用户可查看的所有数据库:

      Description

      可以发现,在上面的列表中有 6 个数据库,它们都是安装 MySQL 时系统自动创建的,其各自功能如下:

      information_schema: 主要存储了系统中的一些数据库对象信息,比如用户表信息、列信息、权限信息、字符集信息和分区信息等。


      mysql: MySQL 的核心数据库,类似于 SQL Server 中的 master 表,主要负责存储数据库用户、用户访问权限等 MySQL 自己需要使用的控制和管理信息。常用的比如在 mysql 数据库的 user 表中修改 root 用户密码。

      performance_schema: 主要用于收集数据库服务器性能参数。

      sakila: MySQL 提供的样例数据库,该数据库共有 16 张表,这些数据表都是比较常见的,在设计数据库时,可以参照这些样例数据表来快速完成所需的数据表。

      sys: MySQL 5.7 安装完成后会多一个 sys 数据库。sys 数据库主要提供了一些视图,数据都来自于 performation_schema,主要是让开发者和使用者更方便地查看性能问题。

      world: world 数据库是 MySQL 自动创建的数据库,该数据库中只包括 3 张数据表,分别保存城市,国家和国家使用的语言等内容。

      实例2:创建并查看数据库

      先创建一个名为 test_db 的数据库:

      CREATE DATABASE test_db;

      Query OK, 1 row affected (0.12 sec)

      再使用 SHOW DATABASES 语句显示权限范围内的所有数据库名,如下所示:
      Description

      你看,刚才创建的数据库已经被显示出来了。

      三、修改数据库

      在 MySQL 数据库中只能对数据库使用的字符集和校对规则进行修改,数据库的这些特性都储存在 db.opt 文件中。下面我们来介绍一下修改数据库的基本操作。

      在 MySQL 中,可以使用 ALTER DATABASE 来修改已经被创建或者存在的数据库的相关参数。修改数据库的语法格式为:

      语法说明如下:

      • ALTER DATABASE 用于更改数据库的全局特性。

      • 使用 ALTER DATABASE 需要获得数据库 ALTER 权限。

      • 数据库名称可以忽略,此时语句对应于默认数据库。

      • CHARACTER SET 子句用于更改默认的数据库字符集。

      四、删除数据库

      当数据库不再使用时应该将其删除,以确保数据库存储空间中存放的是有效数据。删除数据库是将已经存在的数据库从磁盘空间上清除,清除之后,数据库中的所有数据也将一同被删除。

      在 MySQL 中,需要删除已创建的数据库时,可以使用DROP DATABASE 语句。其语法格式为:

      sql复制代码DROP DATABASE [ IF EXISTS ] <数据库名>

      语法说明如下:

      • <数据库名>:指定要删除的数据库名。

      • IF EXISTS:用于防止当数据库不存在时发生错误。

      • DROP DATABASE:删除数据库中的所有表格并同时删除数据库。
        使用此语句时要非常小心,以免错误删除。如果要使用 DROP DATABASE,需要获得数据库 DROP 权限。

      注意: MySQL 安装后,系统会自动创建名为 information_schema 和 mysql 的两个系统数据库,系统数据库存放一些和数据库相关的信息,如果删除了这两个数据库,MySQL 将不能正常工作。

      还有一点值得注意的是: 在进行删除操作的时候一定要谨慎,在执行DROP DATABASE命令后,MySQL 不会给出任何提示确认信息。并且删除数据库后,数据库中存储的所有数据表和数据也将一同被删除,而且不能恢复,因此最好在删除数据库之前先将数据库进行备份。

      实例 :在 MySQL 中创建一个测试数据库 test_db_del

      Description

      使用命令行工具将数据库 test_db_del 从数据库列表中删除,输入的 SQL 语句与执行结果如下所示:

      Description

      此时数据库 test_db_del 不存在。再次执行相同的命令,直接使用 DROP DATABASE test_db_del,系统会报错,如下所示:

      Description

      如果使用IF EXISTS从句,可以防止系统报此类错误,如下所示:

      Description

      五、选择数据库

      在 MySQL 中就有很多系统自带的数据库,那么在操作数据库之前就必须要确定是哪一个数据库。在 MySQL 中,USE 语句用来完成一个数据库到另一个数据库的跳转。

      当用 CREATE DATABASE 语句创建数据库之后,该数据库不会自动成为当前数据库,需要用 USE 来指定当前数据库。其语法格式为:

      xml复制代码USE <数据库名>

      该语句可以通知 MySQL 把<数据库名>所指示的数据库作为当前数据库。该数据库保持为默认数据库,直到语段的结尾,或者直到遇见一个不同的 USE 语句。 只有使用 USE 语句来指定某个数据库作为当前数据库之后,才能对该数据库及其存储的数据对象执行操作。

      总结:

      本篇文章详细介绍了MySQL数据库的新增、查看、修改、删除和选择等操作,希望对你的数据库入门学习有那么一点点的帮助。

      收起阅读 »

      一文带你玩转SQL中的DML(数据操作)语言:从概念到常见操作大解析!数据操作不再难!

      嗨~ 今天的你过得还好吗?人生就是走自己路看自己的风景🌞- 2023.11.08 -前面我们介绍了SQL语句中数据定义语言(DDL)的概念以及它的常用语句,那么DML又是什么呢?二者有什么区别呢?本篇文章将为你讲述。一DML简介DML是指数据操作语言...
      继续阅读 »


      嗨~ 今天的你过得还好吗?

      人生就是走自己路

      看自己的风景

      🌞

      - 2023.11.08 -

      前面我们介绍了SQL语句中数据定义语言(DDL)的概念以及它的常用语句,那么DML又是什么呢?二者有什么区别呢?本篇文章将为你讲述。




      DML简介

      DML是指数据操作语言,英文全称是Data Manipulation Language,用来对数据库中表的数据记录进行更新。


      它创建的模式(表)使用数据操作语言来填充。DDL填充表的行,每行称为Tuple。使用DML,您可以插入,修改,删除和检索表中的信息。DML命令有助于管理存储在数据库中的数据。但是,DML命令不会自动提交,因此变化不是永久性的。所以,可以回滚操作


      一些DML命令包括insert,update,delete和select。insert命令有助于将新记录或行存储到表中;update命令有助于修改表中的现有记录;delete命令允许从表中删除某个记录或一组记录;select命令允许从一个或多个表中检索特定记录。


      关键字:

      • 插入 insert

      • 删除 delete

      • 更新 update



      DDL 语句与DML 语句的主要区别

      DDL 语句与DML 语句是SQL语言中的两种主要语句类别,它们在数据库操作中起到了不同的作用。


      具体来说,DDL有助于更改数据库的结构。它主要用于定义或修改数据库的架构,包括创建、删除和修改表、数据类型、索引等。常用的DDL语句关键字包括CREATE、DROP和ALTER等。例如,可以使用CREATE TABLE语句来创建一个新表,使用ALTER TABLE语句来修改现有表的结构

      你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!大力推荐!


      相反,DML有助于管理数据库中的数据它涵盖了SQL中处理数据的基本操作,包括查询、插入、更新和删除数据。DML语句通常用于从数据库中提取信息、修改现有数据或添加新数据。一些常用的DML语句关键字包括SELECT、INSERT、UPDATE和DELETE等。通过这些语句,用户可以对数据库中的数据进行各种操作,以满足其需求。


      总之,DDL和DML分别代表了在数据库操作中的结构定义和管理数据的两个关键方面,它们共同构成了SQL语言的核心功能,使用户能够有效地创建和管理数据库。


      DML 常见操作

      前面我们已经了解了DML的概念和与DDL 语句的主要区别,下面我们来看看DML 常用语法有哪些。

      首先,准备一张表:



      1. 插入数据(INSERT)

      1.1 语法:

      INSERT INTO 表名[(字段1,字段2,...)] VALUES(字段的1值,字段2的值,...)

      注:在语法定义上"[]"中的内容表示可写可不写

      例:


      注:

      • 数据库中字符串的字面量是使用单引号’'表达的

      • VALUES中指定的值要与指定的字段名个数,顺序,以及类型完全一致

      • 查看表中数据

      SELECT * FROM person 


      1.2 插入默认值

      当插入数据时不指定某个字段,那么该字段插入默认值。若创建表时字段没有显示的指定默认值时,默认值插入NULL。

      例:


      注意事项:

      • age字段没有指定,因此插入默认值NULL;

      • 数据库中任何字段任何类型默认值都是NULL,当然可以在创建表时使用DEFAULT指定。



      1.3 全列插入

      当我们插入数据是不指定任何字段名时,就是全列插入。此时VALUES子句中需要为每个字段指定值。并且要求(个数,顺序,类型必须与表完全一致)

      例:



      2. 修改数据(UPDATE)

      2.1 语法

      UPDATE 表名 SET 字段1=新值1[,字段2=新值2,...][WHERE 过滤条件]

      例:


      注意:

      UPDATE语句通常都要添加WHERE子句,用于添加要修改记录的条件,否则全表修改!

       


      2.2 修改指定记录(添加WHERE子句)

      例:


      WHERE子句中常用的条件

      =, <, <= ,> ,>= ,<>(不等于使用<>。!=不是所有数据库都支持)




      2.3 将一个计算表达式的结果作为值使用



      2.4 同时修改多个字段


       


      3. 删除语句(DELETE)

      3.1 语法:

      DELETE FROM 表名 [WHERE 过滤条件]

      注:DELETE语句通常都要添加WHERE子句,否则是清空表操作

      例:




      3.2 清空表操作

      DELETE FROM person

      练习:




      总结

      DML是一种用于操作数据库中数据的语言。它提供了对数据的增、删、改、查等基本操作,是进行数据库编程的重要工具之一。

      在DML中,我们可以使用SELECT语句来查询数据库中的数据,使用INSERT语句来插入新的数据,使用UPDATE语句来更新已有的数据,以及使用DELETE语句来删除数据。这些操作可以通过简单的语法来实现,使得开发者能够更方便地对数据库中的数据进行处理。


      除了基本操作外,DML还支持事务处理机制。事务可以确保一系列操作的完整性和一致性,如果其中任何一个步骤失败,则所有步骤都会被回滚,否则它们将被提交到数据库中。这种机制对于需要保证数据一致性的应用程序非常重要。

       


      总的来说,DML是进行数据库编程不可或缺的一部分。通过熟练掌握DML的基本操作和事务处理机制,我们可以更高效地管理和操作数据库中的数据。同时,在实际应用中需要注意一些技巧和安全问题,如避免SQL注入攻击、优化查询语句等。


      收起阅读 »

      【Java集合】数据结构与集合的神秘联系,一文读懂!

      嗨~ 今天的你过得还好吗?变好的过程都不太舒服试试再努力点🌞- 2023.11.06 -上篇文章中我们对单列集合中常用的方法和遍历查询。通过本文章为我们解惑,好好的字符串用起来不就行了,为什么要用集合这些工具类?本篇文章将简要介绍数据结构,让读者了解...
      继续阅读 »



      嗨~ 今天的你过得还好吗?

      变好的过程都不太舒服

      试试再努力点

      🌞

      - 2023.11.06 -

      上篇文章中我们对单列集合中常用的方法和遍历查询。通过本文章为我们解惑,好好的字符串用起来不就行了,为什么要用集合这些工具类?

      本篇文章将简要介绍数据结构,让读者了解它们在计算机中以何种结构方式存在。那么,什么是数据结构呢?下面我们来详细解释。




      数据结构

      1.1 数据结构有什么用?

      数据结构是计算机存储、组织数据的方式。 数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。 通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。

      数据结构往往同高效的检索算法和索引技术有关。 这句话是啥意思呢? 我们举个简单的例子。就像金庸小说中所写的,武功招式就相当于我们的算法,而数据结构就是我们的内功心法;而武功的高低,不仅仅是武功招式,更重要的是 学会的内功心法。就比如张无忌在学会九阳神功之后,就可以大战六大门派。


      而数据结构的学习,也会让我们事半功倍。凭借着“数据结构+算法=程序”这句话,Pascal之父获得了图灵奖。


      总结来说:

      • 数据结构就是一种是将世界上各种数据转化为计算机可以存储和操作的形式,定义了逻辑结构如何在计算机上存储,以及相关的基本操作。

      • 算法是程序猿通过调用不同数据结构的基本操作,从而实现了数据的处理。

      而这两点使我们作为程序开发人员的必备基本功,不是一朝一夕就能成为绝世高手的,我们需要一步步去不断的学习积累,积硅步以致千里。

      编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


      1.2 常见的数据结构

      在计算机学科中,数据结构是一门很重要的基础学科,知识点很多。在这里我们不讲那么多,只讲述我们集合中用到的几种数据结构,同学们可以下去自行学习更多的数据结构的知识。 常用结构三个:数组、链表、红黑树


      我们分别来了解一下:

      1)数组

      数组的定义:

      • 数组是相同类型数据的有序集合;

      • 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成;

      • 其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们。


      存储思路:

      所有数据存储在连续的空间中,数组中的每个元素都是一个具体的数据。




      数组的特点:

      • 使用连续分配的内存空间;

      • 一次申请一大段连续的空间,需要事先声明最大可能要占的固定内存空间。

      如下图:

       image

      • 通过索引,查询快

      • 当给数组插入新元素时,数组中的a2,a3,a4整体后移,代价高。

      • 如果插入元素时,数组长度,还要重新创建一个数组,然后循环赋值,代价高

      优点:

      设计简单,读取与修改表中的任意一个元素的时间都是固定的,速度快 。

      缺点:

      容易造成内存浪费;删除或者插入数据需要移动大量数据,速度慢。


      2)链表

      每个数据单独存在一小块内存中,这个单元叫做节点,每个节点知道下一个节点的地址,叫做单向链表。 每个节点既知道下一个节点地址,又知道上一个节点地址,叫做双向链表。 image 

      链表的特点:

      • 使用不连续的内存空间;

      • 不需要提前声明好指定大小的内存空间,一次申请一小块,按需申请。

      image

      • 查询元素,需要通过节点一次向后查找,直到查找到指定元素

      • 增删元素:只需修改连接节点的地址即可。

      优点: 充分节省内存空间,数据插入和删除方便,不需要移动大量数据。

      缺点: 查询数据必须按顺序找到该数据,操作麻烦。



      3)红黑树

      简单理解,就是一种类似于我们生活中树的结构,只不过每个节点最多只有两个叶子。计算机世界的树,刚好与我们现实中的树成镜像相反,树根在上,树枝在下。二叉树如下图: image

      而我们要说的是二叉树的一种比较有意思的叫做红黑树,红黑树本身就是一颗二叉查找树。我们在这里只需要记住它的特点就可以非常方便的对树中的所有节点进行排序和检索。


      小结

      本文介绍了三种常用的数据结构:数组、链表和红黑树,以及这些数据结构在计算机中的重要意义。通过学习这些内容,我们可以逐步深入了解计算机世界。



      我们下期再见!


      收起阅读 »

      作为一个老程序员,想对新人说什么?

      前言最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。这篇文章根据我多年的工作经验,给新人总结了26条建议,希望对你会有所帮...
      继续阅读 »

      前言

      最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。


      在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。

      这篇文章根据我多年的工作经验,给新人总结了26条建议,希望对你会有所帮助。

      1.写好注释


      很多小伙伴不愿意给代码写注释,主要有以下两个原因:

      1. 开发时间太短了,没时间写注释。
      2.《重构》那本书说代码即注释。


      我在开发的前面几年也不喜欢写注释,觉得这是一件很酷的事情。


      但后来发现,有些两年之前的代码,业务逻辑都忘了,有些代码自己都看不懂。特别是有部分非常复杂的逻辑和算法,需要重新花很多时间才能看明白,可以说自己把自己坑了。


      没有注释的代码,不便于维护。


      因此强烈建议大家给代码写注释。


      但注释也不是越多越好,注释多了增加了代码的复杂度,增加了维护成本,给自己增加工作量。


      我们要写好注释,但不能太啰嗦,要给关键或者核心的代码增加注释。我们可以写某个方法是做什么的,主要步骤是什么,给算法写个demo示例等。


      这样以后过了很长时间,再去看这段代码的时候,也会比较容易上手。


      2.多写单元测试


      我看过身边很多大佬写代码有个好习惯,比如新写了某个Util工具类,他们会同时在test目录下,给该工具类编写一些单元测试代码。


      很多小伙伴觉得写单元测试是浪费时间,没有这个必要。


      假如你想重构某个工具类,但由于这个工具类有很多逻辑,要把这些逻辑重新测试一遍,要花费不少时间。


      于是,你产生了放弃重构的想法。


      但如果你之前给该工具类编写了完整的单元测试,重构完成之后,重新执行一下之前的单元测试,就知道重构的结果是否满足预期,这样能够减少很多的测试时间。


      多写单元测试对开发来说,是一个非常好的习惯,有助于提升代码质量。


      即使因为当初开发时间比较紧,没时间写单元测试,也建议在后面空闲的时间内,把单元测试补上。


      3.主动重构自己的烂代码


      好的代码不是一下子就能写成的,需要不断地重构,修复发现的bug。


      不知道你有没有这种体会,看自己1年之前写的代码,简直不忍直视。


      这说明你对业务或者技术的理解,比之前更深入了,认知水平有一定的提升。


      如果有机会,建议你主动重构一下自己的烂代码。把重复的代码,抽取成公共方法。有些参数名称,或者方法名称当时没有取好的,可以及时修改一下。对于逻辑不清晰的代码,重新梳理一下业务逻辑。看看代码中能不能引入一些设计模式,让代码变得更优雅等等。


      通过代码重构的过程,以自我为驱动,能够不断提升我们编写代码的水平。


      4.代码review很重要


      有些公司在系统上线之前,会组织一次代码评审,一起review一下这个迭代要上线的一些代码。


      通过相互的代码review,可以发现一些代码的漏洞,不好的写法,发现自己写代码的坏毛病,让自己能够快速提升。


      当然如果你们公司没有建立代码的相互review机制,也没关系。


      可以后面可以多自己review自己的代码。


      5.多用explain查看执行计划


      我们在写完查询SQL语句之后,有个好习惯是用explain关键字查看一下该SQL语句有没有走索引。


      对于数据量比较大的表,走了索引和没有走索引,SQL语句的执行时间可能会相差上百倍。


      我之前亲身经历过这种差距。


      因此建议大家多用explain查看SQL语句的执行计划。

      6.多看看优秀的工具


      太空电梯、MOSS、ChatGPT等,都预兆着2023年注定不会是平凡的一年。任何新的技术都值得推敲,我们应要有这种敏感性。


      这几年隐约碰过低代码,目前比较热门,很多大厂都相继加入。



      低代码平台概念:通过自动代码生成和可视化编程,只需要少量代码,即可快速搭建各种应用。



      到底啥是低代码,在我看来就是拖拉拽,呼呼呼,一通操作,搞出一套能跑的系统,前端,后端,数据库,一把完成。当然这可能是最终目标。


      链接:http://www.jnpfsoft.com/?juejin,如果你感兴趣,也体验一下。


      JNPF的优势就在于它能生成前后台代码,提供了极大的灵活性,能够创建更复杂、定制化的应用。它的架构设计也让开发者无需担心底层技术细节,能够专注于应用逻辑和用户体验的开发。


      7.上线前整理checklist


      在系统上线之前,一定要整理上线的清单,即我们说的:checklist。


      系统上线有可能是一件很复杂的事情,涉及的东西可能会比较多。


      假如服务A依赖服务B,服务B又依赖服务C。这样的话,服务发版的顺序是:CBA,如果顺序不对,可能会出现问题。


      有时候新功能上线时,需要提前执行sql脚本初始化数据,否则新功能有问题。


      要先配置定时任务。


      上线之前,要在apollo中增加一些配置。


      上线完成之后,需要增加相应的菜单,给指定用户或者角色分配权限。


      等等。


      系统上线,整个过程中,可能会涉及多方面的事情,我们需要将这些事情记录到checklist当中,避免踩坑。


      8.写好接口文档


      接口文档对接口提供者,和接口调用者来说,都非常重要。


      如果你没有接口文档,别人咋知道你接口的地址是什么,接口参数是什么,请求方式时什么,接口多个参数分别代码什么含义,返回值有哪些字段等等。


      他们不知道,必定会多次问你,无形当中,增加了很多沟通的成本。


      如果你的接口文档写的不好,写得别人看不懂,接口文档有很多错误,比如:输入参数的枚举值,跟实际情况不一样。


      这样不光把自己坑了,也会把别人坑惨。


      因此,写接口文档一定要写好,尽量不要马马虎虎应付差事。


      如果对写接口文档比较感兴趣,可以看看我的另一篇文章《瞧瞧别人家的API接口,那叫一个优雅》,里面有详细的介绍。


      9.接口要提前评估请求量


      我们在设计接口的时候,要跟业务方或者产品经理确认一下请求量。


      假如你的接口只能承受100qps,但实际上产生了1000qps。


      这样你的接口,很有可能会承受不住这么大的压力,而直接挂掉。


      我们需要对接口做压力测试,预估接口的请求量,需要部署多少个服务器节点。


      压力测试的话,可以用jmeter、loadRunner等工具。


      此外,还需要对接口做限流,防止别人恶意调用你的接口,导致服务器压力过大。


      限流的话,可以基于用户id、ip地址、接口地址等多个维度同时做限制。


      可以在nginx层,或者网关层做限流。


      10.接口要做幂等性设计


      我们在设计接口时,一定要考虑并发调用的情况。


      比如:用户在前端页面,非常快的点击了两次保存按钮,这样就会在极短的时间内调用你两次接口。


      如果不做幂等性设计,在数据库中可能会产生两条重复的数据。


      还有一种情况时,业务方调用你这边的接口,该接口发生了超时,它有自动重试机制,也可能会让你这边产生重复的数据。


      因此,在做接口设计时,要做幂等设计。


      当然幂等设计的方案有很多,感兴趣的小伙伴可以看看我的另一篇文章《高并发下如何保证接口的幂等性?》。


      如果接口并发量不太大,推荐大家使用在表中加唯一索引的方案,更加简单。


      11.接口参数有调整一定要慎重


      有时候我们提供的接口,需要调整参数。


      比如:新增加了一个参数,或者参数类型从int改成String,或者参数名称有status改成auditStatus,参数由单个id改成批量的idList等等。


      建议涉及到接口参数修改一定要慎重。


      修改接口参数之前,一定要先评估调用端和影响范围,不要自己偷偷修改。如果出问题了,调用方后面肯定要骂娘。


      我们在做接口参数调整时,要做一些兼容性的考虑。


      其实删除参数和修改参数名称是一个问题,都会导致那个参数接收不到数据。


      因此,尽量避免删除参数和修改参数名。


      对于修改参数名称的情况,我们可以增加一个新参数,来接收数据,老的数据还是保留,代码中做兼容处理。


      12.调用第三方接口要加失败重试


      我们在调用第三方接口时,由于存在远程调用,可能会出现接口超时的问题。


      如果接口超时了,你不知道是执行成功,还是执行失败了。


      这时你可以增加自动重试机制。


      接口超时会抛一个connection_timeout或者read_timeout的异常,你可以捕获这个异常,用一个while循环自动重试3次。


      这样就能尽可能减少调用第三方接口失败的情况。


      当然调用第三方接口还有很多其他的坑,感兴趣的小伙伴可以看看我的另一篇文章《我调用第三方接口遇到的13大坑》,里面有详细的介绍。


      13.处理线上数据前,要先备份数据


      有时候,线上数据出现了问题,我们需要修复数据,但涉及的数据有点多。


      这时建议在处理线上数据前,一定要先备份数据。


      备份数据非常简单,可以执行以下sql:

      create table order_2022121819 like `order`;
      insert into order_2022121819 select * from `order`;

      数据备份之后,万一后面哪天数据处理错了,我们可以直接从备份表中还原数据,防止悲剧的产生。


      14.不要轻易删除线上字段


      不要轻易删除线上字段,至少我们公司是这样规定的。


      如果你删除了某个线上字段,但是该字段引用的代码没有删除干净,可能会导致代码出现异常。


      假设开发人员已经把程序改成不使用删除字段了,接下来如何部署呢?


      如果先把程序部署好了,还没来得及删除数据库相关表字段。


      当有insert请求时,由于数据库中该字段是必填的,会报必填字段不能为空的异常。


      如果先把数据库中相关表字段删了,程序还没来得及发。这时所有涉及该删除字段的增删改查,都会报字段不存在的异常。


      所以,线上环境字段不要轻易删除。


      15.要合理设置字段类型和长度


      我们在设计表的时候,要给相关字段设置合理的字段类型和长度。


      如果字段类型和长度不够,有些数据可能会保存失败。


      如果字段类型和长度太大了,又会浪费存储空间。


      我们在工作中,要根据实际情况而定。


      以下原则可以参考一下:

      1.尽可能选择占用存储空间小的字段类型,在满足正常业务需求的情况下,从小到大,往上选。

      2.如果字符串长度固定,或者差别不大,可以选择char类型。如果字符串长度差别较大,可以选择varchar类型。

      3.是否字段,可以选择bit类型。

      4.枚举字段,可以选择tinyint类型。

      5.主键字段,可以选择bigint类型。

      6.金额字段,可以选择decimal类型。

      7.时间字段,可以选择timestamp或datetime类型。


      16.避免一次性查询太多数据


      我们在设计接口,或者调用别人接口的时候,都要避免一次性查询太多数据。


      一次性查询太多的数据,可能会导致查询耗时很长,更加严重的情况会导致系统出现OOM的问题。


      我们之前调用第三方,查询一天的指标数据,该接口经常出现超时问题。


      在做excel导出时,如果一次性查询出所有的数据,导出到excel文件中,可能会导致系统出现OOM问题。


      因此我们的接口要做分页设计。


      如果是调用第三方的接口批量查询接口,尽量分批调用,不要一次性根据id集合查询所有数据。


      如果调用第三方批量查询接口,对性能有一定的要求,我们可以分批之后,用多线程调用接口,最后汇总返回数据。


      17.多线程不一定比单线程快


      很多小伙伴有一个误解,认为使用了多线程一定比使用单线程快。


      其实要看使用场景。


      如果你的业务逻辑是一个耗时的操作,比如:远程调用接口,或者磁盘IO操作,这种使用多线程比单线程要快一些。


      但如果你的业务逻辑非常简单,在一个循环中打印数据,这时候,使用单线程可能会更快一些。


      因为使用多线程,会引入额外的消耗,比如:创建新线程的耗时,抢占CPU资源时线程上下文需要不断切换,这个切换过程是有一定的时间损耗的。


      因此,多线程不一定比单线程快。我们要根据实际业务场景,决定是使用单线程,还是使用多线程。


      18.注意事务问题


      很多时候,我们的代码为了保证数据库多张表保存数据的完整性和一致性,需要使用@Transactional注解的声明式事务,或者使用TransactionTemplate的编程式事务。


      加入事务之后,如果A,B,C三张表同时保存数据,要么一起成功,要么一起失败。


      不会出现数据保存一半的情况,比如:表A保存成功了,但表B和C保存失败了。


      这种情况数据会直接回滚,A,B,C三张表的数据都会同时保存失败。


      如果使用@Transactional注解的声明式事务,可能会出现事务失效的问题,感兴趣的小伙伴可以看看我的另一篇文章《聊聊spring事务失效的12种场景,太坑了》。


      建议优先使用TransactionTemplate的编程式事务的方式创建事务。


      此外,引入事务还会带来大事务问题,可能会导致接口超时,或者出现数据库死锁的问题。


      因此,我们需要优化代码,尽量避免大事务的问题,因为它有许多危害。关于大事务问题,感兴趣的小伙伴,可以看看我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,里面有详情介绍。


      19.小数容易丢失精度


      不知道你在使用小数时,有没有踩过坑,一些运算导致小数丢失了精度。


      如果你在项目中使用了float或者double类型的数据,用他们参与计算,极可能会出现精度丢失问题。


      使用Double时可能会有这种场景:

      double amount1 = 0.02;
      double amount2 = 0.03;
      System.out.println(amount2 - amount1);

      正常情况下预计amount2 - amount1应该等于0.01

      但是执行结果,却为:

      0.009999999999999998

      实际结果小于预计结果。


      Double类型的两个参数相减会转换成二进制,因为Double有效位数为16位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。


      因此,在做小数运算时,更推荐大家使用BigDecimal,避免精度的丢失。


      但如果在使用BigDecimal时,使用不当,也会丢失精度。

      BigDecimal amount1 = new BigDecimal(0.02);
      BigDecimal amount2 = new BigDecimal(0.03);
      System.out.println(amount2.subtract(amount1));

      这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。

      结果:

      0.0099999999999999984734433411404097569175064563751220703125

      使用BigDecimal的构造函数创建BigDecimal,也会导致精度丢失。

      如果如何避免精度丢失呢?

      BigDecimal amount1 = BigDecimal.valueOf(0.02);
      BigDecimal amount2 = BigDecimal.valueOf(0.03);
      System.out.println(amount2.subtract(amount1));

      使用BigDecimal.valueOf方法初始化BigDecimal类型参数,能保证精度不丢失。


      20.优先使用批量操作


      有些小伙伴可能写过这样的代码,在一个for循环中,一个个调用远程接口,或者执行数据库的update操作。


      其实,这样是比较消耗性能的。


      我们尽可能将在一个循环中多次的单个操作,改成一次的批量操作,这样会将代码的性能提升不少。

      例如:

      for(User user : userList) {
      userMapper.update(user);
      }

      改成:

      userMapper.updateForBatch(userList);

      21.synchronized其实用的不多


      我们在面试中当中,经常会被面试官问到synchronized加锁的考题。


      说实话,synchronized的锁升级过程,还是有点复杂的。


      但在实际工作中,使用synchronized加锁的机会不多。


      synchronized更适合于单机环境,可以保证一个服务器节点上,多个线程访问公共资源时,只有一个线程能够拿到那把锁,其他的线程都需要等待。


      但实际上我们的系统,大部分是处于分布式环境当中的。


      为了保证服务的稳定性,我们一般会把系统部署到两个以上的服务器节点上。


      后面哪一天有个服务器节点挂了,系统也能在另外一个服务器节点上正常运行。


      当然也能会出现,一个服务器节点扛不住用户请求压力,也挂掉的情况。


      这种情况,应该提前部署3个服务节点。


      此外,即使只有一个服务器节点,但如果你有api和job两个服务,都会修改某张表的数据。


      这时使用synchronized加锁也会有问题。


      因此,在工作中更多的是使用分布式锁。


      目前比较主流的分布式锁有:

      1.数据库悲观锁。

      2.基于时间戳或者版本号的乐观锁。

      3.使用redis的分布式锁。

      4.使用zookeeper的分布式锁。


      其实这些方案都有一些使用场景。


      目前使用更多的是redis分布式锁。


      当然使用redis分布式锁也很容易踩坑,感兴趣的小伙伴可以看看我的另一篇文章《聊聊redis分布式锁的8大坑》,里面有详细介绍。


      22.异步思想很重要


      不知道你有没有做过接口的性能优化,其中有一个非常重要的优化手段是:异步。


      如果我们的某个保存数据的API接口中的业务逻辑非常复杂,经常出现超时问题。


      现在让你优化该怎么优化呢?


      先从索引,sql语句优化。


      这些优化之后,效果不太明显。


      这时该怎么办呢?


      这就可以使用异步思想来优化了。


      如果该接口的实时性要求不高,我们可以用一张表保存用户数据,然后使用job或者mq,这种异步的方式,读取该表的数据,做业务逻辑处理。


      如果该接口对实效性要求有点高,我们可以梳理一下接口的业务逻辑,看看哪些是核心逻辑,哪些是非核心逻辑。


      对于核心逻辑,可以在接口中同步执行。


      对于非核心逻辑,可以使用job或者mq这种异步的方式处理。


      23.Git提交代码要有好习惯


      有些小伙伴,不太习惯在Git上提交代码。


      非常勤劳的使用idea,写了一天的代码,最后下班前,准备提交代码的时候,电脑突然死机了。


      会让你欲哭无泪。


      用Git提交代码有个好习惯是:多次提交。


      避免一次性提交太多代码的情况。


      这样可以减少代码丢失的风险。


      更重要的是,如果多个人协同开发,别人能够尽早获取你最新的代码,可以尽可能减少代码的冲突。


      假如你开发一天的代码准备去提交的时候,发现你的部分代码,别人也改过了,产生了大量的冲突。


      解决冲突这个过程是很痛苦的。


      如果你能够多次提交代码,可能会及时获取别人最新的代码,减少代码冲突的发生。因为每次push代码之前,Git会先检查一下,代码有没有更新,如果有更新,需要你先pull一下最新的代码。


      此外,使用Git提交代码的时候,一定要写好注释,提交的代码实现了什么功能,或者修复了什么bug。


      如果有条件的话,每次提交时在注释中可以带上jira任务的id,这样后面方便统计工作量。


      24.善用开源的工具类


      我们一定要多熟悉一下开源的工具类,真的可以帮我们提升开发效率,避免在工作中重复造轮子。


      目前业界使用比较多的工具包有:apache的common,google的guava和国内几个大佬些hutool。


      比如将一个大集合的数据,按每500条数据,分成多个小集合。


      这个需求如果要你自己实现,需要巴拉巴拉写一堆代码。


      但如果使用google的guava包,可以非常轻松的使用:

      List list = Lists.newArrayList(1, 2, 3, 4, 5);
      List> partitionList = Lists.partition(list, 2);
      System.out.println(partitionList);

      如果你对更多的第三方工具类比较感兴趣,可以看看我的另一篇文章《吐血推荐17个提升开发效率的“轮子”》。


      25.培养写技术博客的好习惯


      我们在学习新知识点的时候,学完了之后,非常容易忘记。


      往往学到后面,把前面的忘记了。


      回头温习前面的,又把后面的忘记了。


      因此,建议大家培养做笔记的习惯。


      我们可以通过写技术博客的方式,来记笔记,不仅可以给学到的知识点加深印象,还能锻炼自己的表达能力。


      此外,工作中遇到的一些问题,以及解决方案,都可以沉淀到技术博客中。


      一方面是为了避免下次犯相同的错误。


      另一方面也可以帮助别人少走弯路。


      而且,在面试中如果你的简历中写了技术博客地址,是有一定的加分的。


      因此建议大家培养些技术博客的习惯。


      26.多阅读优秀源码


      建议大家利用空闲时间,多阅读JDK、Spring、Mybatis的源码。


      通过阅读源码,可以真正的了解某个技术的底层原理是什么,这些开源项目有哪些好的设计思想,有哪些巧妙的编码技巧,使用了哪些优秀的设计模式,可能会出现什么问题等等。


      当然阅读源码是一个很枯燥的过程。


      有时候我们会发现,有些源码代码量很多,继承关系很复杂,使用了很多设计模式,一眼根本看不明白。


      对于这类不太容易读懂的源码,我们不要一口吃一个胖子。


      要先找一个切入点,不断深入,由点及面的阅读。


      我们可以通过debug的方式阅读源码。


      在阅读的过程中,可以通过idea工具,自动生成类的继承关系,辅助我们更好的理解代码逻辑。


      我们可以一边读源码,一边画流程图,可以更好的加深印象。

      作者:高端章鱼哥
      链接:https://juejin.cn/post/7257735219435765820
      来源:稀土掘金
      收起阅读 »

      喂!不用这些网站,你哪来的时间摸鱼?

      一些我常用且好用的在线工具Postcat - 在线API 开发测试工具postcat.com/ API 开发测试工具Postcat 是一个强大的开源、免费的、跨平台(Windows、Mac、Linux、Browsers...)的 ...
      继续阅读 »

      一些我常用且好用的在线工具

      Postcat - 在线API 开发测试工具
      postcat.com/ API 开发测试工具


      Postcat 是一个强大的开源、免费的、跨平台(Windows、Mac、Linux、Browsers...)的 API 开发测试工具,支持 REST、Websocket 等协议(即将支持 GraphQL、gRPC、TCP、UDP),帮助你加速完成 API 开发和测试工作。它非常适合中小团队及个人使用。

      在保证 Postcat 轻巧灵活的同时,它设计了一个强大的插件系统,让您可以一键使用插件来增强它的功能。


      因此 Postcat 理论上是一个拥有无限可能的 API 产品,可以从Logo 中看到,我们也形象地为它加上了一件披风,代表它的无限可能。

      Excalidraw - 在线白板画图

      ajietextd.github.io/ 一个开源的虚拟手绘风格的白板。创建任何漂亮的手绘图。



      EmojiXD - Emoji表情

      emojixd.com/ EmojiXD 是一本线上Emoji百科全书📚,收录了所有emoji


      Carbon - 在线生成代码图片

      carbon.now.sh/Carbon 能够轻松地将你的源码生成漂亮的图片并分享。


      Pixso - 产品设计协作一体化工具

      pixso.cn/ Pixso,一站式完成原型、设计、交互与交付,为数字化团队协作提效
      原来不用注册 现在需要注册了


      作者:前端小蜗
      链接:https://juejin.cn/post/7243680457815261221
      来源:稀土掘金

      收起阅读 »

      金三银四好像消失了,IT行业何时复苏!

      疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过自我10连问我的心情自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。...
      继续阅读 »

      疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过


      自我10连问

      我的心情

      自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。然而,站在这个应该是光明的时刻,举世瞩目的景象却显得毫无生气。令人失望的是,我们盼望已久的春天似乎仍未到来。
      我的工作生涯
      我已经从业近十年,然而最近两年一直在小公司中工作,

      我的技术和经历并不出色。随着年龄的增长,是否我的技能也在快速提高呢?我们该如何前进呢 ,转产品,产品到达极限,转管理,可是不会人情事故,

      我们该如何继续前进呢?目前还没有人给出答案。

      第一家公司

      我记得那是很早的时候了,那个时候简历投递出去,就马上会收到很多回复,不像现在 ,
      失联招聘, 前程堪忧,boss直坑,
      你辛苦的写完简历,满怀期待投递了各大招聘平台,可等来的 却是已读未回,等的心也凉透了。
      好怀念之前的高光时刻 神仙打架日子
      前面我面试 几乎一周都安排满了,都面试不过来,我记得那会最多时候一天可以跑三家面试哈哈哈,也是很拼命的,有面试机会谁不想多试试呢
      我第一家进入的是一个外包公司,叫xxx东软集团, 那个时候也不不懂,什么是外包给公司,只看工资给的所有offer中最高的,然后就去了哈哈哈哈。
      入职第一天,我背着我自己的电脑满怀着激动就去了,然后被眼前一幕吸引了,办公的人真多啊,办公室都是拿透明玻璃隔开那种,人挺多,我一想公司还挺大的,
      随后我就被带到也是一个玻璃格子办公室,里面就三个人,加我一个4个。
      我害怕极了,这个时候一个稍微有一些秃顶的 大叔过来了 哈哈哈(内心台词,早就知道这一行干就了,会秃头这不会就是下一个我把)
      他把我安排在了靠近玻璃门的也就是大门位置,这是知道我准备随时跑路节奏吗。然后就去忙他自己的了。整个上午我就像是一个被遗忘在角落里的人一样。根本没人管我,就这样第一天结束了,我尴尬了做了一整天。
      这工作和我想象的有点不太一样啊!
      后面第三天还是如此,办公室里依旧是那么几个人,直到第四天,大叔来了,问我直到多线程吗,让我用多线程写一个抽奖的活动项目。(内心我想终于有事情干了,可是也高兴不起来谁知道怎么写)
      不过好在,他也没有说什么时候交,只是说写完了给他看一下,经过我几天的,复制粘贴工程师 一顿谷歌,百度,终于是勉强写出来了。。。。。
      后面,就又陆陆续续来了几个小伙伴,才开始新项目和开会,第一份工作大致就是这样开始了我的职业生涯。怎么说呢和我想象的有所不一样,但又有一些失望。
      后面干了1年多,我就离职了原因是太累了没时间休息,一个项目接着一个项目的

      第二家公司

      在离开第一家公司时候,我休息了好长一段时间,调整了我自己的状态
      也了解了什么是外包公司,什么是工作外派,也是我这一次就在投递简历,和面试时候刻意去避免进那种外包,和外派公司。
      面试什么也还算顺利,不到半个月就拿到了offer。 但是工资总体来说比上一家是要少一点,但是我也接受了,是一家做本地生鲜电商公司,,本来生活科技有公司, 我觉得公司氛围,和公司都挺不错的,就入职了。
      入职了我们那个项目经理还算很热情的,让同事帮我开git账号,开了邮箱,我自己拉取了公司项目,然后同事帮我运行调试环境第一天,项目什么都跑了起来,
      你知道的,每次去一家新公司,开始新项目难的是项目复杂配置文件,和各种mave包依赖,接口,环境冲突,所以跑起来项目自己一个人摸索还是需要一些时间的。
      在这家公司前期也还不错,公司维护自己项目,工作时间也比较自由和灵活,
      大体流程是,每次有新的pm时候 产品经理就会组织各个部门开会
      h5端-移动端-接口端开会讨论需求细节和实现,如果有问题头就会pass掉
      然后产品经理就会把需求指派到每一个头,头把需求指派给组员,然后组员按照
      redmine 上截止时间开发需求,
      开发过程中自己去找对应接口负责方,其他业务负责方 去对接数据,没有问题了就可以提交给指定测试组测试了。
      然后测试组头会把,测试分配给他们组员,进行测试。
      有问题了就会在指派回来给对应负责各个开发同学去解决bug,直到测试完成。测试会让你提交堡垒环境 然后等待通知发布上线,
      我们一般是晚上8点时候发布,发布时候,一般开发人员都要留守,直到发布上线没有问题了,才可以回家。如果弄得很晚的话,第二天可以晚点上班。
      这一点是我觉得比较好的地方,工作时间弹性比较自由。
      记得有一次生产事故。
      我影响很深刻,东西上线了,然后产品经理说这个和他设计的预期的不符合要求,需要重新写,那一晚我们整组弄得很晚,就是为了等我弄好去吃饭。
      你知道人在心急如焚情况下,是写不好代码的最后还是同事帮我一起完成了产品经理变态需求修改。。。。。。(也就在那时候我才知道产品经理和开发为什么不和了)
      因为五行相克
      因为经常这样发版,然后一起吃饭公司报销。我们组员和领导关系还算不错氛围还挺好。在这一家公司我待了挺久的。
      离职原因
      后期因为说公司项目战略升级。空降了一位携程cto,还带来了他的手下人,我们组头,职权被削弱了,我不在由原来头管理了。再加上后面一些其他原因。老同事一个一个走了。
      最后这个组只剩下我,和一个进来不久新同事。 不久我也离职了。

      第三家公司


      这次离职后,我调整休息了差不多有一年,中间离开上海去了江苏,因为家里,女朋友等各种事情。后面我才又从新去了上海,开始找工作。
      找工作期间投奔的同事,合同事住一起。
      这次面试我明显感觉,有一些慌张了,可能是太久没上班原因,有一些底气不足。好在也是找到了工作虽然不太理想。
      这个过程太曲折了,后面公司终究没有扛过疫情,可以说在疫情边缘倒闭了,钱赔偿也没拿到,。。。这里就不赘述了。
      IT行业如何破局 大家有什么想法和故事吗。可以关注 程序员三时公众号 进行技术交流讨论
      嗯~还想往下面写一点什么,,,下一篇分享一下我现在工作和未来思考


      作者:程序员三时
      链接:https://juejin.cn/post/7231608749588693048
      来源:稀土掘金
      收起阅读 »

      2023年35大龄程序员最后的挣扎

      一、自身情况 我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。 其实30岁的时候已经开始焦虑了,并且努力想找出路。 提升技术,努力争增加自己的能力。 努力争取进入管理层,可是卷无处不在,没有人离开这...
      继续阅读 »

      一、自身情况


      我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。



      1. 其实30岁的时候已经开始焦虑了,并且努力想找出路。

      2. 提升技术,努力争增加自己的能力。

      3. 努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。

      4. 大环境我们普通人根本改变不了。

      5. 自己大龄性价比不高,中年危机就是客观情况。

      6. 无非就是在本赛道继续卷,还是换赛道卷的选择了。


      啊Q精神:我还不是最惨的那一批,最惨的是19年借钱买了恒大的烂尾楼,并且在2021年就失业的那拨人。简直不敢想象,那真是绝望啊。心里不够坚强的,想不开轻生的念头都会有。我至少拿了点赔偿,手里还有些余粮,暂时饿不死。


      二、大环境情况




      1. 大环境不好已经不是秘密了,整个经济走弱。大家不敢消费,对未来信心不足已经是板上钉钉的事了。




      2. 这剧本就是30年前日本的剧本,不敢说一摸一样。可以说大差不差了,互联网行业的薪资会慢慢的回归平均水平,或者技术要求在提升一个等级。




      3. 大部分普通人,还是做应用层拧螺丝,少部分框架师能造轮子也就是2:8理论。




      4. 能卷进这20%里,就能在上一层楼。也不是说这行就不行了,只不过变成了存量市场,而且坑位变少,人并没有变少还增加了。




      5. 不要怀疑自己的能力,这也不是你的问题了,是外部环境导致的市场萎缩。我们能做的就是,脱下孔乙己的长衫,先保证生活。努力干活,不违法乱纪做什么都是光荣了,不要带有色眼镜看待任何人。




      三、未来出路


      未来的出路在哪里?


      这个我也很迷惑,因为大佬走的路,并不是我们这些普通的不能在普通的人能够走的通的。当然也有例外的情况,这些就是幸存者偏差了。


      我先把chartGPT给的答应贴出来:


      可以看到chartGPT还是给出,相对可行有效的方案。当然这些并不是每个人都适用。


      我提几个普通人能做的建议(普通人还是围绕生存在做决策):



      1. 有存款的,并且了解一些行业的可以开店,比如餐饮店,花店,水果店等。

      2. 摆摊,国家也都改变政策了。

      3. 超市,配送员,外卖员。

      4. 开滴滴网约车。

      5. 有能力的,可以润出G。可以吸一吸GW“free的air”,反正都是要被ZBJ榨取的。


      以上都是个人不成熟的观点,jym多多包涵。


      每个行业都卷,没有很好的建议都是走一步算一步,保持学习,减少精神内耗


      作者:可乐泡枸杞
      链接:https://juejin.cn/post/7230656455808335930
      来源:稀土掘金
      收起阅读 »

      【环信十周年】千呼万唤始出来,环信礼物真实在!

      当看到公布的信息有自己中奖的时候真的非常开心,然后就是在等待中..等待自己的大礼包。今天终于是拿到了自己,急不可耐开箱~🙃看到这包装...居然被丢成这样,但是还好里面没啥问题~拆开包装,一打开入眼就是环信十周年~😁打开包装就是大礼包全家桶了,包含了手提袋、手机...
      继续阅读 »

      当看到公布的信息有自己中奖的时候真的非常开心,然后就是在等待中..等待自己的大礼包。

      今天终于是拿到了自己,急不可耐开箱~


      🙃看到这包装...居然被丢成这样,但是还好里面没啥问题~


      [舔屏]拆开包装,一打开入眼就是环信十周年~


      😁打开包装就是大礼包全家桶了,包含了手提袋、手机支架、徽章、杯子、T恤、贴画,非常nice~


      (全家桶)


      这个手提袋外面的材质有点像纸,但是又不是纸,整体非常不错,就是有点皱巴巴的~😓


      手提袋的内层是布艺的,蓝色的布也是非常的清爽~手提带上面还印有文字,非常的大气~


      这个贴纸的“又不是不能用”非常的贴切,一眼就相中了~不就是说当下的工作心态嘛,又不是不能干,将就干吧~[污]


      [坏笑]接下来就是这个杯子了,非常的实用,可以泡咖啡~提神醒脑~


      但同时不得不吐槽一下这个杯口,有点锋利~不注意可能会导致划手~(扣厂家货款


      接下来是这个徽章,质感真的不错,直接当成摆件用也非常nice~


      这个手机支架实用性太高了,看电影~摸鱼都是一等一的利器~还有LOGO点缀~


      最后就是这个T恤了,好看~😁


      衣服背后也还有个小LOGO,这点缀炒鸡棒~


      衣服洗了,看下材质~94%棉,弹性非常不错,夏天穿着凉快~

      最后,感谢环信~

      收起阅读 »

      环信十周年趴——我的从业之路

              我的从业经历,对大家来说就是一个避坑史,感觉自己啥坑都遇到过。       2015年,我大学毕业了。毕业即失业,校招压根没公司来看一眼我们。你肯定会说,这怪谁,谁...
      继续阅读 »

              我的从业经历,对大家来说就是一个避坑史,感觉自己啥坑都遇到过。

             2015年,我大学毕业了。毕业即失业,校招压根没公司来看一眼我们。你肯定会说,这怪谁,谁让你学校垃圾。是的,不可否认,学校确实不行,这也导致我们无人问津。毕业之后回到了所在的城市,整天往人才市场跑,奈何这个城市没啥网络企业以及科技公司,全是招聘销售人员。药品销售,保险销售,地产销售,我看着人来人往的人才市场,偌大的城市好像容不下我一人。迎面走来一个招聘的小姐姐,婀娜多姿,我的目光在她身上移不开。在当时我感觉她的声音就像乡间的轻铃,清脆悦耳,字字敲击耳膜。她看向我,目光流转,翘眉生盼,然后诚恳的让我加入她们,一起为保险事业做贡献。我欣喜若狂,使劲的点头,但是我又还想找互联网的公司,我不甘的又摇头。她说你不想跟着我一起为众人的健康事业而努力奋斗?我点头又摇头,她拽着我就要去办理手续,最终我还是坚守住了本心,我知道我们俩的相遇只是命运的一个玩笑,因为她结婚了。

             后来,我在这个城市找了份网管的工作,日常任务就是跟着一个老师傅去这个城市的各个地方机房里去维护。都是些没人想干的脏活累活扔给外包公司,然后我们去干,去加油站里面给机房走线,整理机房,布置服务器机架,没有丝毫的技术含量。自己也在慢慢沉沦,可能也就要这样,匆匆忙忙,无所事事的度过往后,但是我又好不甘心...

             就这样在一次次的纠结,煎熬中,来到了2016年5月份。我厌倦了这样的工作内容,转身投奔了在北京的朋友。来到北京一切都是那么的新奇,心情是那么的兴奋,感觉就连空气都是甜的,四周都是自由的气息。朋友把我带到了他在城中村租的一个屋子,虽简陋但却很整洁,他说:有wifi有空调就够了。是啊,还奢求什么呢,北京本来就令人向往。我们坐在一起吃着肉,喝着啤酒,高谈阔论。唯一一点让我感觉不爽的是空调,因为它不仅制冷,还喷水,向内喷,喷的身上到处都是,也不知道房东通过什么神通手段安装了一个这么个奇葩空调。后面我在朋友这,边住边找工作,最终找到了一个愿意收留我的公司,虽然我会的不多,但是公司看我还算本分,一问三不知,那是确实一点都不知道。我在公司跟了一个老员工开启了Android之路,他会分我点特别简单的工作,然后把自己珍藏多年的种子,哦,不是,是搜集的Android项目,让我学习,尽快能承担更多的工作。在这个公司我一直在成长,学的也很快,公司看我本分,所以工资也很本分,后面也有调整,但是还是调整的很本分。我感觉我是有野心的,所以在两年之后,我选择了离开,之后便开启了找工作之旅。

             没多久我就进入了一家做线上游戏陪玩的公司,因为工资足够的低。我进去之后才知道公司之前融资过2000w,但不肯在我身上多花一分钱。钱呢大部分被老板挥霍了,挥霍到哪了呢,我猜大部分是挥霍到自己兜里了,因为他又给自己买了辆50来万的车。随着资金越来越少,我们的办公场地,办公环境以肉眼可见的速度在迅速变差,不仅越来越小,最后只能跟其他公司在一个屋里面拼凑,要不是同事早都认识了,我估计大家都会认为,这个屋里的都是自己同事。2019年开始,公司颓势越发严重,工资也开始断断续续,但是好在老板还有点良心,每个月给发,只是日期不固定了。在4月的时候,终于还是元气耗尽,
      公司不行了。我再一次的开始了找工作,没几天就去了一家搞平行进口车的公司,这家公司有3个前端维护网站,然后新招一个Android跟一个ios。移动端项目接口用网页端的,进来按着原型图搞就行了。上班第一天就开始匆匆忙忙开始写项目。晚上加班更是家常便饭,时不时还有人一直催。功夫不负有心人,在2个多月的时候,项目搞完了。这个公司的转正需要提前半个月申请,我们申请之后发现,批准就没动过,也找人事主管咨询过,她说不清楚是怎么回事,然后在某一天,人事找到我跟ios,告诉我们转正不批了,让我们走人吧。费半天劲,项目搞完,卸磨杀驴,遇到这样的也是恶心至极。


             干了三个月,又要匆匆忙忙的找工作,随后来到了一家搞广告传媒的公司。本以为可以安安稳稳的待一段时间,但是谁料屋漏偏逢连夜雨。在经过四个月的试用期80%工资之后,疫情突起,铺天盖地而至,开启了在家办公、值班的方式,公司业务也大受打击,随即开始降薪,只发一半工资。又在当前公司挣扎了5个月之后,看着日渐空扁的钱包,实在是无以为继,只能重新开始找工作。后来呢入职了一家稍正常的公司,正常呢,也只是说可以正常发工资,日期也是充满随机。随着年龄的增长,钱包却还是依然空扁如故,看到别人都是锦衣玉食,衣冠华丽,而我却还在原地兜兜转转。最终在2022年,离开了北京,一个待了6年的地方,一个充满回忆的地方,去的时候充满了向往,回来的时候带走了满眼的沧桑...


      本文参与环信十周年活动,活动链接:https://www.imgeek.net/question/474026

      收起阅读 »

      《环信十周年趴——我的程序人生之iOS- Flutter》

          我是一名iOS开发工程师,已经在这个行业里摸爬滚打了14年。在这14年中,我经历了从Objective-C到Swift再到Flutter的演进,也经历了从少年时期的满怀抱负到中年危机的现实冲击。现在,我想分享一下...
      继续阅读 »


          我是一名iOS开发工程师,已经在这个行业里摸爬滚打了14年。在这14年中,我经历了从Objective-C到Swift再到Flutter的演进,也经历了从少年时期的满怀抱负到中年危机的现实冲击。现在,我想分享一下我的程序人生,希望能给那些像我曾经一样,心中有梦的年轻人一些启示。

          在我刚开始从事iOS开发的时候, Objective-C还是主流语言。那时候,我用C语言的基本语法,学习Objective-C的面向对象编程,一步步攻克了这个语言的第一道难关。但是,随着时间的推移,Objective-C的局限性越来越明显。它语法笨重、效率不高,越来越难以满足我们开发的需求。于是,Swift语言应运而生。
      Swift语言的出现,让我看到了iOS开发的新希望。它快速、安全、简洁,有着现代编程语言的特性,如可选类型、函数式编程等。在Swift语言的基础上,我能够编写出更加高效、更加优美的代码,也能够更加深入地理解iOS框架的本质。同时,Swift语言也为我后续学习其他语言提供了很大的帮助。

          随着移动开发的不断发展,跨平台开发成为了一个越来越重要的话题。Flutter逐渐成为了时下移动开发领域的一个热门话题。我开始探索这个新的开发框架,并迅速被它的快速高效、易于学习所吸引。Flutter允许开发者使用Dart语言编写应用程序,并能够在多个平台上运行,包括iOS和Android。这对于需要跨平台开发的人来说,是一个巨大的优势。通过学习Flutter,我不仅拓宽了自己的技能树,还为未来的发展奠定了更加坚实的基础。

          在多年的开发工作中,我不仅积累了丰富的开发经验,还面临了各种挑战。在这个过程中,我从一个满怀梦想和追求的少年成长为一个中年大叔,在职业道路上走过了漫长的历程。我曾在北京这样的城市追逐梦想,也曾为了安逸的生活而选择了哈尔滨这样的城市。我的职业生涯也因此经历了起起伏伏,有过了许多成就和贡献,也面临过中年危机和职业瓶颈。

      然而,尽管我的职业生涯充满了挑战和不确定性,我始终坚信技术发展带来的机遇和变革。我相信,只要我们保持学习、适应和发展的态度,不断面对挑战并克服困难,我们终将在程序人生的道路上找到自己的位置,实现自己的价值。

          未来,我希望继续探索新的技术和框架,不断拓宽自己的技能树和学习领域。我相信,只有不断尝试新事物、不断挑战自己,才能在程序人生的道路上走得更远、更高。同时,我也希望把自己的经验和所学所悟传递给更多有志于从事程序员这个行业的年轻人,帮助他们少走弯路,实现自己的梦想。

          场面话说完了,说一下这几年要感谢的人吧。 QQ好友洪江Objective-C学习道路上的伙伴,白嫖了他一套iOS开发的课程,经常一起半夜互相改代码的兄弟,QQ好友阿林同学97年的开发者,白嫖一套游戏源码。最最需要感谢是我死皮赖脸认的师傅,凌晨三点起来给我改代码被媳妇大骂的声音至今还在我的脑海中。还有就是自己了。没想到上课坐不住板凳的自己,遇到代码后我可以变得这样的安静。

          我的程序人生,充满了挑战和机遇,也充满了不确定性和可能性。我期待着在这个道路上继续前行,不断探索、学习和成长,迎接未来的挑战和机遇。

      本文参与环信十周年活动,活动链接:https://www.imgeek.net/question/474026”

      收起阅读 »

      恶意爬虫?能让恶意爬虫遁于无形的小Tips

      前言验证码是阻挡机器人攻击的有效实践,网络爬虫,又被称为网络机器人,是按照一定的规则,自动地抓取网络信息和数据的程序或者脚本。如何防控,这里简单提供几个小Tips。使用nginx的自带功能通过对httpuseragent阻塞来实现,包括GET/POST方式的请...
      继续阅读 »

      前言

      验证码是阻挡机器人攻击的有效实践,网络爬虫,又被称为网络机器人,是按照一定的规则,自动地抓取网络信息和数据的程序或者脚本。如何防控,这里简单提供几个小Tips。


      使用nginx的自带功能

      通过对httpuseragent阻塞来实现,包括GET/POST方式的请求,以nginx为例。

      拒绝以wget方式的httpuseragent,增加如下内容:

      Block http user agent - wget
      if ($http_user_agent ~* (Wget) ) {
      return 403;
      }

      如何拒绝多种httpuseragent,内容如下:

      if ($http_user_agent ~ (agent1|agent2|Foo|Wget|Catall Spider|AcoiRobot) ) {
      return 403;
      }

      限制User-Agent字段

      User-Agent字段能识别用户所使用的操作系统、版本、CPU、浏览器等信息,如果请求来自非浏览器,就能识别其为爬虫,阻止爬虫抓取网站信息。

      限制IP或账号

      根据业务需求,要求用户通过验证码后才能使用某些功能或权限。当同一IP、同一设备在一定时间内访问网站的次数,系统自动限制其访问浏览。只有在输入正确的验证码之后才能继续访问。

      验证码拦截

      在登录页等页面,添加验证码,以识别是正常流量还是恶意爬虫,也是一种基本的操作。


      HTML代码:

      <script src="captcha.js?appid=xxx"></script>
      <script>
      kg.captcha({
      // 绑定元素,验证框显示区域
      bind: "#captchaBox3",
      // 验证成功事务处理
      success: function(e) {
      console.log(e);
      document.getElementById('kgCaptchaToken').value = e['token']
      },
      // 验证失败事务处理
      failure: function(e) {
      console.log(e);
      },
      // 点击刷新按钮时触发
      refresh: function(e) {
      console.log(e);
      }
      });
      </script>

      <div id="captchaBox3">载入中 ...</div>
      <input type="hidden" name="kgCaptchaToken" value="" />

      Python代码:

      from wsgiref.simple_server import make_server
      from KgCaptchaSDK import KgCaptcha
      def start(environ, response):
      # 填写你的 AppId,在应用管理中获取
      AppID = "xxx"
      # 填写你的 AppSecret,在应用管理中获取
      AppSecret = "xxx"
      request = KgCaptcha(AppID, AppSecret)
      # 填写应用服务域名,在应用管理中获取
      request.appCdn = "https://cdn.kgcaptcha.com"
      # 请求超时时间,秒
      request.connectTimeout = 10
      # 用户id/登录名/手机号等信息,当安全策略中的防控等级为3时必须填写
      request.userId = "kgCaptchaDemo"
      # 使用其它 WEB 框架时请删除 request.parse,使用框架提供的方法获取以下相关参数
      parseEnviron = request.parse(environ)
      # 前端验证成功后颁发的 token,有效期为两分钟
      request.token = parseEnviron["post"].get("kgCaptchaToken", "") # 前端 _POST["kgCaptchaToken"]
      # 客户端IP地址
      request.clientIp = parseEnviron["ip"]
      # 客户端浏览器信息
      request.clientBrowser = parseEnviron["browser"]
      # 来路域名
      request.domain = parseEnviron["domain"]
      # 发送请求
      requestResult = request.sendRequest()
      if requestResult.code == 0:
      # 验证通过逻辑处理
      html = "验证通过"
      else:
      # 验证失败逻辑处理
      html = f"{requestResult.msg} - {requestResult.code}"
      response("200 OK", [("Content-type", "text/html; charset=utf-8")])
      return [bytes(str(html), encoding="utf-8")]
      httpd = make_server("0.0.0.0", 8088, start) # 设置调试端口 http://localhost:8088/
      httpd.serve_forever()

      最后

      SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

      收起阅读 »

      公司没钱了,工资发不出来,作为员工怎么办?

      现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发 80%、50% 工资这样的操作。 员工遇到这种情况,无非以下几种选择。1认同公司的决策,愿意跟公司共同进退。2不认同...
      继续阅读 »

      现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发 80%、50% 工资这样的操作。


      员工遇到这种情况,无非以下几种选择。

      1认同公司的决策,愿意跟公司共同进退。

      2不认同公司的决策,我要离职。

      3不认同公司的决策,但感觉自己反对也没用。所以嘴上先答应,事后会准备去找新的工作机会。

      4不认同公司的决策,我也不主动离职。准备跟公司 battle,” 你们这么做是不合法滴 “


      你可以代入这个场景看看自己是哪一类。首先由于每个人遇到的真实情况不一样,所以所有的选择只有适合自己的,并没有对错之分。


      我自己的应对思路是,抛开存量,看增量。存量就是我在公司多少多少年了,公司开除我要给我 N+1 的补偿。公司之前对我特别特别好,老板对我有知遇之恩等等。你就当做自己已经不在公司了,现在公司给你发了 offer,现在的你是否愿意接受公司开给你的条件?如果愿意,那么你可以选择方案一。如果不愿意,那么你就可以选择方案三。现在这环境,骑驴找马,否则离职后还要自己交社保。还不如先苟着。


      为什么不选择方案四?因为不值得,如果公司属于违法操作,你先苟着,后面离职后还是可以找劳动局仲裁的。这样既不耽误你换工作,也不耽误你要赔偿。如果公司是正规操作,那么闹腾也没用,白浪费自己的时间。


      离职赔偿还是比较清晰明确的,如果是散伙那可能会牵扯到更多利益。我自己的经验是,不能什么都想着要。当最优解挺难获得的时候,拿个次优解也可以。当然,不管你选择的哪个,我都有一个建议。那就是当一天和尚,敲一天钟。在职期间,还是要把事情干好的,用心并不全是为了公司,更多是为了自己。人生最大的投资是投资自己的工作和事业,浪费时间就是浪费生命。哪怕公司没有事情安排给你做,也要学会自己找事情做。


      如果公司后面没钱了,欠的工资还拿得到吗?



      我们作为员工是很难知道公司财务状况的,所以出了这样的事就直接去仲裁,最好是跟同事凑齐十个人去,据说会优先处理。公司如果还要做生意,一般会在仲裁前选择和解,大概是分几个月归还欠款。如果公司不管,那么仲裁后,会冻结公司公账。但有没有钱就看情况了。


      如果公司账上没钱且股东已经实缴了股本金,那么公司是可以直接破产清算的。公司破产得话,基本上欠员工的钱就没有了。如果没有实缴,那么股东还需要按照股份比例偿还债务。


      链接:https://juejin.cn/post/7156242740034928671

      收起阅读 »

      项目开发过程中,成员提离职,怎么办?

      环境从问题发生的环境看,如果我们有一个好的氛围,好的企业文化。员工会不会突然突出离职?或者哪怕提出离职,会不会给我们更多一点时间,在离职期间仍然把事情做好?如果答案是肯定的,那么管理者可以尝试从问题发生的上游解决问题。提前安排更多的资源来做项目,预防资源不足的...
      继续阅读 »

      环境

      从问题发生的环境看,如果我们有一个好的氛围,好的企业文化。员工会不会突然突出离职?或者哪怕提出离职,会不会给我们更多一点时间,在离职期间仍然把事情做好?如果答案是肯定的,那么管理者可以尝试从问题发生的上游解决问题。

      提前安排更多的资源来做项目,预防资源不足的情况发生。比如整体预留了20%的开发时间做缓冲,或者整体安排的工作量比规划的多20%。


      问题本身


      从问题本身思考,员工离职导致的问题是资源不够用。

      新增资源,能不能快速找到替代离职员工的人?或者我们能不能使用外包方式完成需求?跟团队商量增加一些工作时间或提高工作效率?

      减少需求,少做一些不是很重要的需求,把离职员工的需求分给其他人。


      这2个解决方案其实都有一个前提,那就是离职人员的代码是遵循编码规范的,这样接手的人才看得懂。否则,需要增加的资源会比原来规划的多很多。这种问题不能靠员工自觉,而应该要有一套制度来规范编码。


      问题的主体


      我们不一定能解决问题,但可以解决让问题发生的人。这样问题就不存在了。比如,既然问题出现在张三面前,那就想办法搞定张三,让他愿意按计划把项目完成。如果公司里没人能搞定这个事,这里还有另一个思路,就是想想谁能解决这个问题,找那个能解决问题的人。


      从环境、问题本身、问题的主体三个维度来分析,我们得到了好几个解决方案。我们接着分析哪种方案更靠谱。


      解决方案分析


      方案一,从环境角度分析,让问题不发生。这种成本是最小的。但如果问题已经发生,那这个方案就没用了。


      方案二,在项目规划的时候,提前安排更多资源。这招好是好,但前提是你公司有那么多资源。大部分公司都是资源不足。


      方案三,新增资源,这个招人不会那么快,就算招进来了,一时半会还发挥不出多大的价值。请外包的话,其实跟招人一样,一时半会还发挥不出多大的价值,成本还更高,也不适合。至于跟团队成员商量提高工作效率或者大家加个班赶上进度,这也是一个解决方案。不过前提是团队还有精力承担这些工作。


      方案四,减少需求。这个成本最小,对大部分公司其实也适用。关键是需求管理要做好,对需求的优先级有共识。


      方案五,解决让问题发生的人。这个如果不是有大的积怨,也是一个比较好的方案。对整个项目来说,成本也不会很大,项目时间和质量都有保证。


      项目管理里有一个生命周期概念,越是在早期发生问题,成本越小。越到后期成本越大。所以,如果让我选,我会选择方案一。但如果已经发生,那只能在四和五里选一个。


      实战经验


      离职是一场危机管理


      让问题不发生,那么解决之道就是不让员工离职。尤其是不让核心骨干员工提离职。离职就是一场危机管理。


      这里的本质的是人才是资产,我们在市场上看到很多案例,很多企业的倒闭并不是因为经营问题,而是管理层的大批量流失,资本市场也不看好管理层流失的企业。了解这点,你就能理解为什么人才是资产了。所以对企业来说,核心员工离职不亚于一场危机。


      下面分享一个危机管理矩阵,这样有助于我们对危机进行分类。


      横轴是一件事情发生之后,危害性有多大,我们分为大、中、小。纵轴就是这件事发生的概率,也可以分为大、中、小。然后就形成了九种不同的类型。



      我自己的理解是,有精力的话,上图红色区域是需要重点关注的。如果精力有限,就关注最右边那三种离职后,危害性特别大的员工(不管概率发生的大小)。要知道给企业造成大影响的往往是那些发生概率小的,因为概率大的,你肯定有预防动作,而那些你认为不会离职的员工,突然一天找到你提离职,你连什么准备都没,这种伤害是最大的。


      理论上所有岗位都应该准备好”接班人“计划,但实际上很多公司没办法做到。在一些小公司是一个萝卜一个坑,这个岗位人员离职,还得现招。这不合理,但这就是现状。


      公司如何管理危机?


      好,回到公司身上,公司如何管理危机?


      第一,稳住关键性员工,让员工利益和公司利益进行深入绑定。


      那些创造利润最大的前10~20%的员工,就应该获得50%甚至更高的收益。当然除了金钱上的激励外,还要有精神上的激励,给他目标,让他有成就感等等。


      第二,有意识地培养关键岗位的接班人或者助理。


      比如通过激励鼓励他们带新人、轮岗等等


      第三,人员的危机管理是动态变化的,要时不时地明确团队各成员的位置。


      比如大公司每年都会做人才盘点。


      第四,当危机真的出现后,要有应对方案。


      也就是把危机控制在可承受的范围内。比如,项目管理中的playB方案,真遇到资源不够,时间不够的情况下,我们能不能放弃一些不重要的需求?亦或者能不能先用相对简单但可用的方案?


      离职管理的核心是:降低离职发生的概率和降低离职造成危害的大小。


      离职沟通


      如果事情已经发生了,管理者应该先通过离职沟通,释放自己的善意。我会按照如下情况跟离职员工沟通


      第一,先做离职沟通,了解对方为什么离职?还有没有留下来的可能,作为管理者有什么能帮他做的?


      第二,确定走的话,确认下对方期望的离职时间,然后根据公司情况,协商一个双方都能接受的离职时间点。不要因为没有交接人,就不给明确时间。


      第三,征求对方意见,是否需要公布离职。然后一起商量这段时间的工作安排。比如,你会坦诚告知会减少工作量,但哪些工作是需要他继续支持的。希望他能一如既往地高效完成工作。


      第四,如果还没有交接人到岗,最好在一周内安排人员到岗,可以考虑内部换岗,内招、猎聘等手段尽快让人员到岗。


      第五,如果已经到离职时间,但还没有交接人,作为公司管理者,你就是最好的交接人。在正式交接工作之前,要理清楚需要哪些相关的资料,做好文档分类。如果实在对离职员工的工作不了解,可以让离职人员写一封日常工作的总结。


      如果做完这些,离职员工还是消极怠工。作为管理者能做得就比较有限,可以尝试以下几个方法


      1、再进行一次沟通。表明现在公司的情况,希望他给予支持。


      2、看看自己能给予对方哪些帮助,先把这些落实好。比如写推荐信。另外有些公司入职的时候会做背景调查,这也是你能够帮助到他的。


      3、如果你有权利,可以跟离职员工商量是否可以以兼职的方式来完成后续工作。这种方式对大家都好,他可以早点离职,你也不用担心因为时间仓促招错人。


      如果做完以上这些还不行,那么就考虑减少一些需求,用更简单的方案先用着,后期做迭代。至于说让团队加班加点赶进度,这个要根据项目实际情况来定。


      总结:今天给大家分享了一个简单分析问题的方法。然后重点聊了一下项目成员突然要离职,项目负责人有哪些应对方案。如果你看完有收获,欢迎留言讨论。

      作者:石云升
      链接:https://juejin.cn/post/7147319129542770702
      收起阅读 »

      python+selenium自动化测试(入门向干货)

      今天带来的是python+selenium自动化测试的入门向教程,也是做个小总结。 我实现的大致流程为: 1 - 准备自动化测试环境 2 - 发起网页请求 3 - 定位元素 4 - 行为链 使用工具:python,selenium,chromedriver...
      继续阅读 »

      今天带来的是python+selenium自动化测试的入门向教程,也是做个小总结。




      我实现的大致流程为:


      1 - 准备自动化测试环境

      2 - 发起网页请求

      3 - 定位元素

      4 - 行为链



      使用工具:python,selenium,chromedriver,chrom浏览器





      操作步骤讲解环节




      下面就是喜闻乐见的操作步骤讲解环节了(´◔౪◔)



      1、准备自动化测试环境


      本次的环境准备较为复杂,但是只要跟着方法走,问题应该也不是很多。

      另外,软件包我都整理好了,评论区可见。



      • 准备python环境,这里网上的教程都挺多的,我也就不赘述了。

      • 导入python的第三方扩展包 - selenium,urllib3,jdcal,et_xmlfile(后三个为selenium的依赖包)
      安装方法如下:
      1)解压后,进入扩展包,shift+右键,在此处打开PowerShell窗口,执行命令
      2)python setup.exe install
      • 安装对应版本的chrom浏览器,获取对应版本的chromedriver
      这里说的对应版本,是说浏览器的版本需要与chromedriver相对应
      我资源里给到的是81版本的chrom浏览器和chromedriver

      2、发起网页请求

      环境准备好后,就可以发起网页请求验证了。
      代码如下:

      from selenium import webdriver
      from selenium.webdriver.chrome.options import Options
      # 设定目的url
      url = "https://www.baidu.com/"
      # 创建一个参数对象,用来控制chrome以无界面模式打开
      chrome_options = Options()
      # chrome_options.add_argument('--headless')
      # chrome_options.add_argument('--disable-gpu')

      # 跳过https安全认证页面
      chrome_options.add_argument('--ignore-certificate-errors')
      # 创建自己的一个浏览器对象
      driver = webdriver.Chrome(chrome_options=chrome_options)
      # 访问网页
      driver.get(url)
      # 等待防止网络不稳定引起的报错
      driver.implicitly_wait(5)
      # 浏览器全屏显示
      driver.maximize_window()

      3、定位元素

      参考文档:https://python-selenium-zh.readthedocs.io/zh_CN/latest/
      代码如下:

      1) 根据Id定位,driver.find_element_by_id()
      2) 根据 Name 定位,driver.find_element_by_name()
      3) XPath定位,driver.find_element_by_xpath()
      4) 用链接文本定位超链接,driver.find_element_by_link_text()
      5) 标签名定位,driver.find_element_by_tag_name()
      6) class定位,driver.find_element_by_class_name()
      7) css选择器定位,driver.find_element_by_css_selector()

      4、行为链
      这里说的操作是指,定位元素后,针对元素进行的鼠标移动,鼠标点击事件,键盘输入,以及内容菜单交互等操作。
      参考文档:
      https://python-selenium-zh.readthedocs.io/zh_CN/latest/
      https://www.cnblogs.com/GouQ/p/13093339.html
      代码如下:

      1) 鼠标单击事件,find_element_by_id().click()
      2) 键盘输入事件,find_element_by_id().send_keys()
      3) 文本清空事件,find_element_by_id().clear()
      4) 右键点击事件,find_element_by_id().context_click()
      5) 鼠标双击事件,find_element_by_id().double_click()


      收起阅读 »

      转行的程序猿都去做什么了?这些个案羡煞我也

      程序猿是口青春饭,30有点“老”,35非常“老”。当年龄越来越大,体力精力学习能力越来越竞争不过年轻人时,除了技术和管理,码农还有没有别的选择吗?以下这些切实又不切实的选择仅供参考。 1.转往临近岗位,比如你讨厌的产品经理 程序猿和产品经理可谓是最像夫妻的两个...
      继续阅读 »

      程序猿是口青春饭,30有点“老”,35非常“老”。当年龄越来越大,体力精力学习能力越来越竞争不过年轻人时,除了技术和管理,码农还有没有别的选择吗?以下这些切实又不切实的选择仅供参考。


      1.转往临近岗位,比如你讨厌的产品经理


      程序猿和产品经理可谓是最像夫妻的两个职位,相爱相杀,知根知底。


      程序员转产品经理有很大优势,因为了解产品的实现过程,所以对项目的时间把握有相当的话语权,能保证了项目的进度,对产品将来的扩展和升级都有帮助,所以程序员转过来的产品经理是很抢手的。


      只要多学习一些产品营销和运营方面的知识,多一点沟通能力,程序员出生的产品经理就自然而然在市场上占据很大优势。



      国内目前最牛逼的产品经理非微信之父张小龙莫属,他就是程序员转产品经理的最佳案例。如果你拥有绝佳的洞察力,能够了解人性需求,相信自己可以创造出人人都愿意的产品,你也可以像张小龙一样,升职加薪、当上总经理、出任CEO、迎娶白富美、走上人生巅峰。


      2.任何行业的教育行业都可以作为备胎


      随着猿在劳动力市场的走红,IT讲师也成为了一个热门职位。大龄码农在工作中年限越长上升越慢,到大龄阶段,就不得不面对自己的停滞,IT讲师对于他们会是一个很好的选择。


      开发一线需要越年轻越好,这样薪水低能加班,不过IT讲师可谓是越老越有“味道”,资历越丰厚,越能吸引学员。因此相当一部分“退役程序猿”都转去了IT教育行业。


      3.个体户,谁自由谁知道


      除了做IT讲师和产品经理,有的程序猿纯粹是写腻了代码,想完全摆脱代码,摆脱程序猿的工作。他们有的就利用自己多年攒下的钱,做起了个体户。


      不知是因为新闻性还是真有那么大基数,据说程序猿转行做餐饮的不少,并且都还做得不错。


      无论是卖肠粉的,卖凉皮的,卖热干面,卖火锅的都有!






      程序猿卖烧饼,卖凉皮,听上去有些屈才。但如今的个体户收入并不低,即使只是卖个热干面,卖个烧饼,好好经营,收入不比一个高级码农差。


      4.一些高薪产业,比如做明星


      此条建议,是认真的,毕竟明星中有不少的程序猿转行成功的案例。


      潘玮柏


      潘玮柏,曾设计一款游戏——熊猫屁王,他是第一个设计 iPhone app 的艺人, 还创造过 App Store 上下载量第一。


      这也就算了,别的明星送礼都是送花送大牌,潘玮柏给周杰伦的发片礼竟然是以周杰伦为游戏主角的手机游戏。


      李健


      男神李健,原来也是码农。他是清华高材生,大学里学习的就是电子工程专业。李健的第一份工作就是在国家广电总局当一名网络工程师。


      马东


      奇葩说的主持人马东也曾经是一名计算机专业的人才。在他还未满18岁的时候,马东就只身前往澳洲学习计算机专业,在澳洲有着10年的IT工程师经历。而这使得他对数据有着灵敏的认知和有序的编程思维,能够在传媒行业风格别具一致,脱颖而出。


      除了这些明星是程序猿转行过来的,还有猿混成了总理。




      李显龙


      新加坡总理李显龙也曾是位真正入行的程序员。他在剑桥大学三一学院修习数学和计算机科学,曾把自己写过的解熟读程序放了出来,让网民们一起帮忙找BUG。李显龙在一次科技创业论坛上透露,自己曾“非常享受编程”。


      5.追求自己的爱好,找寻真正的自我


      有人说,最好的转行是为了兴趣爱好而转。毕竟换一个行业需要投入大量时间精力,还要面对失败。做一个程序猿,奋斗几年,攒够一定资金后,就该找寻自我,追求自己的爱好了。以下两个案例中的程序猿,都曾有稳定的程序猿工作,但都放弃了选择了追求自己的爱好,最终闯出了一片天。


      闫鹤祥


      著名德云社相声演员——闫鹤祥原是一名程序猿。闫鹤祥曾在社交软件上晒出他当年任中国移动数字大厦无线局域网管理工程师职位。从一个程序员到相声演员,闫鹤祥从幕后转到台前,由原先的默默无闻变成了现在为德云社少帮主郭麒麟捧哏的相声大腕儿。


      王小波


      大多数人都知道王小波是小说家,却很少人知道王小波算得上是中国早期的程序员,在90年代初国内应用软件缺乏的时候,王小波学会了汇编和C语言,编了中文编辑器和输入法,相比同期的雷军、求伯君,王小波的编程能力毫不逊色。


      王小波曾在自己的小说里骄傲地写到,我写书的软件都是自己编写的。当时的他还通过卖软件还赚了不少钱呢,很多中关村的老板想要拉他入伙,这对于当时的屌丝王小波还是有致命吸引力的,王小波也认真地考虑过,只不过后来觉得写东西更有意思,便一一回绝了。


      以上为不完全程序猿转行方向手册,无论如何,转行需谨慎。



      链接:https://juejin.cn/post/6844903829507407879

      收起阅读 »

      vue hash和history路由的区别

      vue
      在了解路由模式前,我们先看下 什么是单页面应用,vue-router  的实现原理是怎样的,这样更容易理解路由。 SPA与前端路由 SPA(单页面应用,全程为:Single-page Web applications)指的是只有一张Web页...
      继续阅读 »


      在了解路由模式前,我们先看下 什么是单页面应用,vue-router  的实现原理是怎样的,这样更容易理解路由。


      SPA与前端路由


      • SPA(单页面应用,全程为:Single-page Web applications)指的是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序,简单通俗点就是在一个项目中只有一个html页面,它在第一次加载页面时,将唯一完成的html页面和所有其余页面组件一起下载下来,所有的组件的展示与切换都在这唯一的页面中完成,这样切换页面时,不会重新加载整个页面,而是通过路由来实现不同组件之间的切换。
      • 单页面应用(SPA)的核心之一是:更新视图而不重新请求页面。

      优点:


      • 具有桌面应用的即时性、网站的可移植性和可访问性
      • 用户体验好、快,内容的改变不需要重新加载整个页面
      • 良好的前后端分离,分工更明确

      缺点:


      • 不利于搜索引擎的抓取
      • 首次渲染速度相对较慢

      vue Router实现原理


      vue-router  在实现单页面路由时,提供了两种方式:Hash  模式和  History  模式;vue2是 根据  mode  参数来决定采用哪种方式,默认是  Hash  模式,手动设置为  History  模式。更新视图但不重新请求页面”是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有以下两种方式:


      image.png


      Hash


      简述


      • vue-router   默认为 hash 模式,使用 URL 的  hash  来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载;#  就是  hash符号,中文名为哈希符或者锚点,在  hash  符号后的值称为  hash  值。
      • 路由的  hash  模式是利用了  window 可以监听 onhashchange 事件来实现的,也就是说  hash  值是用来指导浏览器动作的,对服务器没有影响,HTTP 请求中也不会包括  hash  值,同时每一次改变  hash  值,都会在浏览器的访问历史中增加一个记录,使用“后退”按钮,就可以回到上一个位置。所以,hash 模式 是根据  hash 值来发生改变,根据不同的值,渲染指定DOM位置的不同数据。

      参考:Vue 前端路由工作原理,hash与history之间的区别


      image.png


       特点


      • url中带一个   #   号
      • 可以改变URL,但不会触发页面重新加载(hash的改变会记录在  window.hisotry  中)因此并不算是一次 HTTP 请求,所以这种模式不利于 SEO 优化
      • 只能修改  #  后面的部分,因此只能跳转与当前 URL 同文档的 URL
      • 只能通过字符串改变 URL
      • 通过  window.onhashchange  监听  hash  的改变,借此实现无刷新跳转的功能。
      • 每改变一次  hash ( window.location.hash),都会在浏览器的访问历史中增加一个记录。
      • 路径中从  #  开始,后面的所有路径都叫做路由的  哈希值 并且哈希值它不会作为路径的一部分随着 http 请求,发给服务器

      参考:在SPA项目的路由中,注意hash与history的区别


       History


      简述


      • history  是路由的另一种模式,在相应的  router  配置时将  mode  设置为  history  即可。
      • history  模式是通过调用  window.history  对象上的一系列方法来实现页面的无刷新跳转。
      • 利用了 HTML5 History Interface  中新增的   pushState()  和  replaceState()  方法。
      • 这两个方法应用于浏览器的历史记录栈,在当前已有的  back、forward、go  的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会向后端发送请求。

       参考:深入了解前端路由 hash 与 history 差异



      特点


      • 新的URL可以是与当前URL同源的任意 URL,也可以与当前URL一样,但是这样会把重复的一次操作记录到栈中。
      • 通过参数stateObject可以添加任意类型的数据到记录中。
      • 可额外设置title属性供后续使用。
      • 通过pushState、replaceState实现无刷新跳转的功能。
      • 路径直接拼接在端口号后面,后面的路径也会随着http请求发给服务器,因此前端的URL必须和向发送请求后端URL保持一致,否则会报404错误。
      • 由于History API的缘故,低版本浏览器有兼容行问题。

      参考:在SPA项目的路由中,注意hash与history的区别前端框架路由实现的Hash和History两种模式的区别


       生产环境存在问题


             因为  history  模式的时候路径会随着  http 请求发送给服务器,项目打包部署时,需要后端配置 nginx,当应用通过  vue-router  跳转到某个页面后,因为此时是前端路由控制页面跳转,虽然url改变,但是页面只是内容改变,并没有重新请求,所以这套流程没有任何问题。但是,如果在当前的页面刷新一下,此时会重新发起请求,如果  nginx  没有匹配到当前url,就会出现404的页面。


      那为什么hash模式不会出现这个问题呢?


           上文已讲,hash 虽然可以改变URL,但不会被包括在  HTTP  请求中。它被用来指导浏览器动作,并不影响服务器端,因此,改变  hash  并没有改变URL,所以页面路径还是之前的路径,nginx  不会拦截。 因此,切记在使用  history  模式时,需要服务端允许地址可访问,否则就会出现404的尴尬场景。


      那为什么开发环境时就不会出现404呢?


      因为在 vue-cli  中  webpack  帮我们做了处理


       


       解决问题


      生产环境 刷新 404 的解决办法可以在 nginx  做代理转发,在  nginx 中配置按顺序检查参数中的资源是否存在,如果都没有找到,让   nginx  内部重定向到项目首页。



      作者:前端吃货媛
      链接:https://juejin.cn/post/7116336664540086286
      来源:稀土掘金
      著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
      收起阅读 »

      如何让 x == 1 && x == 2 && x == 3 等式成立

      如何让 x == 1 && x == 2 && x == 3 等式成立 某次面试,面试官突然问道:“如何让 x 等于 1 且让 x 等于 2 且让 x 等于 3 的等式成立?” 话音刚落,笔者立马失去意识,双眼一黑,两腿一蹬,心...
      继续阅读 »

      如何让 x == 1 && x == 2 && x == 3 等式成立


      某次面试,面试官突然问道:“如何让 x 等于 1 且让 x 等于 2 且让 x 等于 3 的等式成立?


      话音刚落,笔者立马失去意识,双眼一黑,两腿一蹬,心里暗骂:什么玩意儿!



      虽然当时没回答上来,但觉得这题非常有意思,便在这为大家分享下后续的解题思路:


      宽松相等 == 和严格相等 === 都能用来判断两个值是否“相等”,首先,我们要明确上文提到的等于指的是哪一种,我们先看下二者的区别:


      (1) 对于基础类型之间的比较,== 和 === 是有区别的:


      (1.1) 不同类型间比较,== 比较“转化成同一类型后的值”看“值”是否相等,=== 如果类型不同,其结果就是不等

      (1.2) 同类型比较,直接进行“值”比较,两者结果一样

      (2) 对于引用类型之间的比较,== 和 === 是没有区别的,都进行“指针地址”比较 


      (3) 基础类型与引用类型之间的比较,== 和 === 是有区别的:


      (3.1) 对于 ==,将引用类型转化为基础类型,进行“值”比较

      (3.2) 因为类型不同,=== 结果为 false

      “== 允许在相等比较中进行强制类型转换,而 === 不允许。”


      由此可见,上文提到的等于指的宽松相等 ==,题目变为 “x == 1 && x == 2 && x == 3”。


      那多种数据类型之间的相等比较又有哪些呢?笔者查阅了相关资料,如下所示:


      同类型数据之间的相等比较


      如果 Type(x) 等于 Type(y) ES5 规范 11.9.3.1 这样定义:



      1. 如果 Type(x)Undefined,返回 true



      2. 如果 Type(x)Null,返回 true



      3. 如果 Type(x)Number ,则


        • 如果 xNaN,返回 false
        • 如果 yNaN,返回 false
        • 如果 xy 的数字值相同,返回 true
        • 如果 x+0y-0,返回 true
        • 如果 x-0y+0,返回 true


      4. 如果 Type(x)String,则如果 xy 是字符的序列完全相同(相同的长度和相同位置相同的字符),则返回 true。否则,返回 false



      5. 如果 Type(x)Boolean,则如果 xy 都为 true 或都为 false,则返回 true。否则,返回 false



      6. 如果 xy 指向同一对象,则返回 true。否则,返回 false



      null 和 undefined 之间的相等比较


      nullundefined 之间的 == 也涉及隐式强制类型转换。ES5 规范 11.9.3.2-3 这样定义:


      1. 如果 xnullyundefined,则结果为 true
      2. 如果 xundefinedynull,则结果为 true

      在 == 中,nullundefined 相等(它们也与其自身相等),除此之外其他值都不和它们两个相等。


      这也就是说, 在 == 中nullundefined 是一回事。


      var a = null;
      var b;
      a == b; // true
      a == null; // true
      b == null; // true
      a == false; // false
      b == false; // false
      a == ""; // false
      b == ""; // false
      a == 0; // false
      b == 0; // false

      字符串和数字之间的相等比较


      ES5 规范 11.9.3.4-5 这样定义:


      1. 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
      2. 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

      var a = 42;

      var b = "42";

      a === b; // false

      a == b; // true

      因为没有强制类型转换,所以 a === bfalse,42 和 "42" 不相等。


      根据规范,"42" 应该被强制类型转换为数字以便进行相等比较。


      其他类型和布尔类型之间的相等比较


      ES5 规范 11.9.3.6-7 这样定义:


      1. 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
      2. 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

      仔细分析例子,首先:


      var x = true;

      var y = "42";

      x == y; // false

      Type(x) 是布尔值,所以 ToNumber(x)true 强制类型转换为 1,变成 1 == "42",二者的类型仍然不同,"42" 根据规则被强制类型转换为 42,最后变成 1 == 42,结果为 false


      对象和非对象之间的相等比较


      关于对象(对象 / 函数 / 数组)和标量基本类型(字符串 / 数字 / 布尔值)之间的相等比较,ES5 规范 11.9.3.8-9 做如下规定:


      1. 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
      2. 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。

      什么是 toPrimitive() 函数?


      **应用场景:**在 JavaScript 中,如果想要将对象转换成基本类型时,再从基本类型转换为对应的 String 或者 Number,实质就是调用 valueOftoString 方法,也就是所谓的拆箱转换。


      **函数结构:**toPrimitive(input, preferedType?)


      参数解释:


      input 是输入的值,即要转换的对象,必选;


      preferedType 是期望转换的基本类型,他可以是字符串,也可以是数字。选填,默认为 number


      执行过程:


      如果转换的类型是 number,会执行以下步骤:


      1. 如果 input 是原始值,直接返回这个值;
      2. 否则,如果 input 是对象,调用 input.valueOf(),如果结果是原始值,返回结果;
      3. 否则,调用input.toString()。如果结果是原始值,返回结果;
      4. 否则,抛出错误。

      如果转换的类型是 string,2和3会交换执行,即先执行 toString() 方法。


      valueOf 和 toString 的优先级:


      1. 进行对象转换时 (alert(对象)),优先调用 toString 方法,如没有重写 toString 将调用 valueOf 方法,如果两方法都不没有重写,但按 ObjecttoString 输出。
      2. 进行强转字符串类型时将优先调用 toString 方法,强转为数字时优先调用 valueOf
      3. 在有运算操作符的情况下,valueOf 的优先级高于 toString

      由此可知,若 x 为对象时,我们改写 x 的 valueOf 或 toString 方法可以让标题的等式成立:


      const x = {
      val: 0,
      valueOf: () => {
      x.val++
      return x.val
      },
      }

      或者:


      const x = {
      val: 0,
      toString: () => {
      x.val++
      return x.val
      },
      }

      给对象 x 设置一个属性 val 并赋值为 0,并修改其 valueOf、toString 方法,在 “x == 1 && x == 2 && x == 3”判断执行时,每次等式比较都会触发 valueOf、toString 方法,都会执行 val++ ,同时把最新的 val 值用于等式比较,三次等式判断时 val 值分别为 1、2、3 与等式右侧的 1、2、3 相同,从而使等式成立。



      作者:政采云前端团队
      链接:https://juejin.cn/post/7111848825232293918
      来源:稀土掘金
      著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
      收起阅读 »

      Android架构之路--热更新Tinker

      当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题,这也是正是最后使用 Tinker 的原因。先看一张图对比:Tinker热补丁方案不仅支持类、So 以及资源的替...
      继续阅读 »

      一、简介

      当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题,这也是正是最后使用 Tinker 的原因。先看一张图对比:

      1-1:热更新对比图

      Tinker热补丁方案不仅支持类、So 以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做 bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上。

      TinkerPatch 平台在 Github 为大家提供了各种各样的 Sample,大家可点击前往 [TinkerPatch Github].

      Tinker原理图

      1-2:原理图

      Tinker流程图

      1-3:Tinker 流程图

      二、Tinker相关网站

      微信Tinker Patch官网:Tinker Patch
      Github地址:tinker

      三、接入Tinker步骤

      基础步骤

      • 注册Tinker账户、添加APP、记录AppKey,添加 APP 版本、 发布补丁。详细步骤请移步Tinker平台使用文档

      主要来说下配置Gradle和代码

      1. 配置Tinker版本信息

      我们使用配置文件去配置Tinker版本信息,易于统一版本和后面更换版本,如图:

      2-1 gradle.properties文件

      代码如下:

      TINKER_VERSION=1.9.6
      TINKERPATCH_VERSION=1.2.6

      2. 使用Tinker插件

      在根目录下的build.gradle文件下配置,如图:

      2-2 添加Tinker插件



      代码如下:

      classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:${TINKERPATCH_VERSION}"

      3. 配置Tinker的gradle脚本

      在项目app目录下新建tinkerparch.gradle文件,如图:

      2-3 tinkerpatch.gradle

      代码如下:

      apply plugin: 'tinkerpatch-support'
      /**
      * TODO: 请按自己的需求修改为适应自己工程的参数
      */

      def bakPath = file("${buildDir}/bakApk/")
      def baseInfo = "app-1.0.0-0529-14-38-02"
      def variantName = "release"
      /**
      * 对于插件各参数的详细解析请参考
      * http://tinkerpatch.com/Docs/SDK
      */

      tinkerpatchSupport {
      /** 可以在debug的时候关闭 tinkerPatch **/
      /** 当disable tinker的时候需要添加multiDexKeepProguard和proguardFiles,
      * 这些配置文件本身由tinkerPatch的插件自动添加,当你disable后需要手动添加
      * 你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro,
      * 需要你手动修改'tinker.sample.android.app'本示例的包名为你自己的包名,
      * com.xxx前缀的包名不用修改
      **/

      tinkerEnable = true
      reflectApplication = true
      /**
      * 是否开启加固模式,只能在APK将要进行加固时使用,否则会patch失败。
      * 如果只在某个渠道使用了加固,可使用多flavors配置
      **/

      protectedApp = false
      /**
      * 实验功能
      * 补丁是否支持新增 Activity (新增Activity的exported属性必须为false)
      **/

      supportComponent = true

      autoBackupApkPath = "${bakPath}"
      /** 注意:换成自己在Tinker平台上申请的appKey**/
      appKey = "521db2518e0ca16d"

      /** 注意: 若发布新的全量包, appVersion一定要更新 **/
      appVersion = "1.0.0"

      def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
      def name = "${project.name}-${variantName}"

      baseApkFile = "${pathPrefix}/${name}.apk"
      baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
      baseResourceRFile = "${pathPrefix}/${name}-R.txt"

      /**
      * 若有编译多flavors需求, 可以参照:
      * https://github.com/TinkerPatch/tinkerpatch-flavors-sample
      * 注意: 除非你不同的flavor代码是不一样的,
      * 不然建议采用zip comment或者文件方式生成渠道信息
      * (相关工具:walle 或者 packer-ng)
      **/

      }

      /**
      * 用于用户在代码中判断tinkerPatch是否被使用
      */

      android {
      defaultConfig {
      buildConfigField "boolean", "TINKER_ENABLE",
      "${tinkerpatchSupport.tinkerEnable}"
      }
      }

      /**
      * 一般来说,我们无需对下面的参数做任何的修改
      * 对于各参数的详细介绍请参考:
      * https://github.com/Tencent/tinker/wiki/Tinker-
      * %E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
      */

      tinkerPatch {
      ignoreWarning = false
      useSign = true
      dex {
      dexMode = "jar"
      pattern = ["classes*.dex"]
      loader = []
      }
      lib {
      pattern = ["lib/*/*.so"]
      }

      res {
      pattern = ["res/*", "r/*", "assets/*", "resources.arsc",
      "AndroidManifest.xml"]
      ignoreChange = []
      largeModSize = 100
      }

      packageConfig {
      }
      sevenZip {
      zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
      // path = "/usr/local/bin/7za"
      }
      buildConfig {
      keepDexApply = false
      }
      }

      注意

      • AppKey:换成自己在Tinker平台上申请的。
      • baseInfo:基准包名称,使用Tinker脚本编译在模块的build/bakApk生成编译副本。
      • variantName: 这个一般对应buildTypes里面你基准包生成的类型,release、debug或其他
      • appVersion:配置和Tinker后台新建补丁包的一致。

      其他地方可以暂时不用改

      4. 配置模块下的build.gradle

      配置签名

      如果有不会的同学可以看这篇 Android Studio的两种模式及签名配置

      2-4:配置签名

      在配置混淆代码的时候,想要提醒下大家,当设置 minifyEnabled 为false时代表不混淆代码,shrinkResources也应设置为false ,它们通常是彼此关联。
      要是你设置minifyEnabled 为false,shrinkResources为true,将会报异常,信息如下:

      Error:A problem was found with the configuration of task':watch:packageOfficialDebug'.
      File '...\build\intermediates\res\resources-official-debug-stripped.ap_' specified for property 'resourceFile' does not exist.

      2-4-1:混淆配置

      配置依赖

      2-5:配置Tinker依赖

      使用插件

      2-6:使用Tinker插件

      具体代码如下:

      apply plugin: 'com.android.application'
      apply from: 'tinkerpatch.gradle'

      android {
      compileSdkVersion 25
      buildToolsVersion "25.0.3"
      defaultConfig {
      applicationId "qqt.com.tinkerdemo"
      minSdkVersion 17
      targetSdkVersion 25
      versionCode 1
      versionName "1.0.0"
      multiDexEnabled true
      testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
      }
      signingConfigs {
      release {
      storeFile file("./jks/tinker.jks")
      storePassword "123456"
      keyAlias "tinker"
      keyPassword "123456"

      }
      debug {
      storeFile file("./jks/debug.keystore")
      }
      }
      buildTypes {
      release {
      minifyEnabled false
      signingConfig signingConfigs.release
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
      }
      debug {
      signingConfig signingConfigs.debug
      }
      }
      }

      dependencies {
      compile fileTree(dir: 'libs', include: ['*.jar'])
      androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
      exclude group: 'com.android.support', module: 'support-annotations'
      })
      compile 'com.android.support:appcompat-v7:25.3.1'
      compile 'com.android.support.constraint:constraint-layout:1.0.2'
      testCompile 'junit:junit:4.12'

      annotationProcessor("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") {
      changing = true
      }
      provided("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") {
      changing = true
      }
      compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:${TINKERPATCH_VERSION}") {
      changing = true
      }

      compile 'com.android.support:multidex:1.0.1'
      }

      5. 代码集成

      最后一步,在自己的代码新建一个Application,把代码集成在App中,别忘了在AndroidManifest里面配置APP。。。
      如图:

      2-7:集成代码



      我是继承MultiDexApplication主要是防止64k异常。有关这块知识,请看 Android 方法数超过64k、编译OOM、编译过慢解决方案

      具体代码如下:

      package qqt.com.tinkerdemo;

      import android.support.multidex.MultiDexApplication;

      import com.tencent.tinker.loader.app.ApplicationLike;
      import com.tinkerpatch.sdk.TinkerPatch;
      import com.tinkerpatch.sdk.loader.TinkerPatchApplicationLike;

      /**
      * 邮箱:ljh12520jy@163.com
      *
      *
      @author Ljh on 2018/5/28
      */


      public class App extends MultiDexApplication {

      private ApplicationLike mApplicationLike;

      @Override
      public void onCreate() {
      super.onCreate();
      mApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
      // 初始化TinkerPatch SDK, 更多配置可参照API章节中的,初始化SDK
      TinkerPatch.init(mApplicationLike)
      .reflectPatchLibrary()
      .fetchPatchUpdate(true)
      // 强制更新
      .setPatchRollbackOnScreenOff(true)
      .setPatchRestartOnSrceenOff(true)
      .setFetchPatchIntervalByHours(3);

      // 每隔3个小时(通过setFetchPatchIntervalByHours设置)去访问后台时候有更新,
      //通过handler实现轮训的效果
      TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
      }
      }

      四、生成基准包

      在生成基准包的时候,要注意一个问题,就是关闭 instant run(当tinkerEnable = true时,false的时候,就不需要),如图:

      3-1:关闭InstantRun

      在Android Studio的右上角,点击Gradle,如图:

      3-2:准备生成基准包

      双击assembleRelease生成成功后安装模块/build/outputs/apk/release/app-release.apk就OK了,这时候进去模块/build/bakApk里面记录一下类似app-1.0.0-0530-18-01-59的文件名称,只生成一次基准包,那么就会生成一个。这里需要注意一下,如果点太多生成太多的话确定不了刚刚生成的是哪个,那么就选最新那个或者删掉重新生成基准包。
      生成后的基准包如图:

      3-3:生成基准包

      五、修改bug

      在自己的代码中随便修改点代码(Tinker1.9.6 里面支持新增Activity代码)

      六、生成补丁包

      在生成补丁包前,我们需要去tinkerpatch.gradle文件下修改一些信息。

      • baseInfo :把前面app-1.0.0-0529-14-38-02换成我们刚生成记录下的基准包(app-1.0.0-0530-18-01-59)就可以。
      • variantName : 因为刚刚我们使用assembleRelease生成的补丁,所以我们只需要使用release

      双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包

      生成的补丁包如图:

      3-4:生成补丁包

      3-5:tinkerPatch下的一些文件说明

      七、发布补丁包

      回到Tinker后台,选中我们开始新建的项目,补丁下发->添加APP版本。然后上传刚刚的patch_signed_7zip.apk。

      APP开启强制更新的话那么重启应用就会更新,否则会通过轮询去更新。应用重启才生效。

      3-6:发布补丁包

      注:在Tinker后台发布的差分包(补丁包)是根据app-1.0.0-0530-18-01-59为基准包下,修复bug生成的补丁包,只对于app-1.0.0-0530-18-01-59版本的apk生效。

      3-7:差分包


      一、多渠道打包

      tinker官方文档推荐用walle或者packer-ng-plugin来辅助打渠道包。估计有不少同学用过,今天我想推荐另外一款多渠道打包的插件ApkMultiChannelPlugin,它作为Android Studio插件进行多渠道打包。
      安装步骤:打开 Android Studio: 打开 Setting/Preferences -> Plugins -> Browse repositories 然后搜索 ApkMultiChannel 安装重启。
      有不了解的同学,可以直接看它的文档。

      我是采用add channel file to META-INF方式进行多渠道打包,在这里提供一个读取渠道的工具类ChannelHelper。

      二、多渠道打包步骤

      1. 选择一个基准包

      选择基准包的一个apk,然后右键,点击Build MultiChannel

      1-1:选择基准包

      2. 配置

      配置签名信息,打包方式和渠道等。

      1-2:配置多渠道

      配置说明:

      Key Store Path: 签名文件的路径
      Key Store Password: 签名文件的密码
      Key Alias: 密钥别名
      Key Password: 密钥密码

      Zipalign Path: zipalign 文件的路径(用于优化 apk;zipalign 可以确保所有未压缩的数据均是以相对于文件开始部分的特定字节对齐开始,这样可减少应用消耗的 RAM 量。)
      Signer Version: 选择签名版本:apksigner 和 jarsigner
      Build Type: 打包方式

      Channels: 渠道列表,每行一个,最前面可加 > 或不加(保存信息的时候,程序会自行加上)

      我们刚才刚才配置的东西会保存在根目录的 channels.properties里

      1-3:channel配置文件

      3. 开始打包

      配置完成后,选择基准包的一个apk,然后右键,点击Build MultiChannel,就会开始进行多渠道打包,文件会输出在选中的apk的当前目录下的channels是目录下,如图:

      1-4:多渠道打包

      4. 发布APK

      将刚才打包完成的包,分别发布到对应的应用市场。

      5. 修改bug

      随便修改部分代码

      6. 生成补丁包

      在生成补丁包前,我们需要去tinkerpatch.gradle文件下修改一些信息。

      • baseInfo :改成我们刚才选择基准包的目录app-1.0.1-0601-14-30-42就可以。
        双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包
      • variantName : 因为刚刚我们使用assembleRelease生成的补丁,所以我们只需要使用release

      双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包

      1-5:生成补丁包

      7. 发布补丁包

      回到Tinker后台,选中我们开始新建的项目,补丁下发->添加APP版本。然后上传刚刚的patch_signed_7zip.apk。

      APP开启强制更新的话那么重启应用就会更新,否则会通过轮询去更新。应用重启才生效。

      1-6: 发布补丁包.png

      注:

      1. 这个补丁包对于以app-1.0.1-0601-14-30-42为基准宝,进行多渠道打包的apk都能生效(亲测成功),如果你把该渠道包进行360加固(protectedApp = true),也生效。
      2. 当我们在正式环境需要混淆代码:设置 minifyEnabled true,添加混淆:
      -keep public class * implements com.tencent.tinker.loader.app.ApplicationLike

      如图:

      1-7: 混淆代码

      本文转载自: https://cloud.tencent.com/developer/article/1872556

      收起阅读 »

      Object.defineProperty也能监听数组变化?

      首先,解答一下标题:Object.defineProperty 不能监听原生数组的变化。如需监听数组,要将数组转成对象。 在 Vue2 时是使用了 Object.defineProperty 监听数据变化,但我查了下 文档,发现 Object.define...
      继续阅读 »


      首先,解答一下标题:Object.defineProperty 不能监听原生数组的变化。如需监听数组,要将数组转成对象。




      Vue2 时是使用了 Object.defineProperty 监听数据变化,但我查了下 文档,发现 Object.defineProperty 是用来监听对象指定属性的变化。没有看到可以监听个数组变化的。


      Vue2 有的确能监听到数组某些方法改变了数组的值。本文的目标就是解开这个结。






      基础用法


      Object.defineProperty() 文档


      关于 Object.defineProperty() 的用法,可以看官方文档。


      基础部分本文只做简单的讲解。




      语法


      Object.defineProperty(obj, prop, descriptor)

      参数


      • obj 要定义属性的对象。
      • prop 要定义或修改的属性的名称或 Symbol
      • descriptor 要定义或修改的属性描述符。

      const data = {}
      let name = '雷猴'

      Object.defineProperty(data, 'name', {
      get() {
      console.log('get')
      return name
      },
      set(newVal) {
      console.log('set')
      name = newVal
      }
      })

      console.log(data.name)
      data.name = '鲨鱼辣椒'

      console.log(data.name)
      console.log(name)

      上面的代码会输出


      get
      雷猴
      set
      鲨鱼辣椒
      鲨鱼辣椒



      上面的意思是,如果你需要访问 data.name ,那就返回 name 的值。


      如果你想设置 data.name ,那就会将你传进来的值放到变量 name 里。


      此时再访问 data.name 或者 name ,都会返回新赋予的值。




      还有另一个基础用法:“冻结”指定属性


      const data = {}

      Object.defineProperty(data, 'name', {
      value: '雷猴',
      writable: false
      })

      data.name = '鲨鱼辣椒'
      delete data.name
      console.log(data.name)

      这个例子,把 data.name 冻结住了,不管你要修改还是要删除都不生效了,一旦访问 data.name 都一律返回 雷猴


      以上就是 Object.defineProperty 的基础用法。






      深度监听


      上面的例子是监听基础的对象。但如果对象里还包含对象,这种情况就可以使用递归的方式。


      递归需要创建一个方法,然后判断是否需要重复调用自身。


      // 触发更新视图
      function updateView() {
      console.log('视图更新')
      }

      // 重新定义属性,监听起来(核心)
      function defineReactive(target, key, value) {

      // 深度监听
      observer(value)

      // 核心 API
      Object.defineProperty(target, key, {
      get() {
      return value
      },
      set(newValue) {
      if (newValue != value) {
      // 深度监听
      observer(newValue)

      // 设置新值
      // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
      value = newValue

      // 触发视图更新
      updateView()
      }
      }
      })
      }

      // 深度监听
      function observer(target) {
      if (typeof target !== 'object' || target === null) {
      // 不是对象或数组
      return target
      }

      // 重新定义各个属性(for in 也可以遍历数组)
      for (let key in target) {
      defineReactive(target, key, target[key])
      }
      }

      // 准备数据
      const data = {
      name: '雷猴'
      }

      // 开始监听
      observer(data)

      // 测试1
      data.name = {
      lastName: '鲨鱼辣椒'
      }

      // 测试2
      data.name.lastName = '蟑螂恶霸'

      上面这个例子会输出2次“视图更新”。




      我创建了一个 updateView 方法,该方法模拟更新 DOM (类似 Vue的操作),但我这里简化成只是输出 “视图更新” 。因为这不是本文的重点。




      测试1 会触发一次 “视图更新” ;测试2 也会触发一次。


      因为在 Object.definePropertyset 里面我有调用了一次 observer(newValue)observer 会判断传入的值是不是对象,如果是对象就再次调用 defineReactive 方法。


      这样可以模拟一个递归的状态。




      以上就是 深度监听 的原理,其实就是递归。


      但递归有个不好的地方,就是如果对象层次很深,需要计算的量就很大,因为需要一次计算到底。






      监听数组


      数组没有 key ,只有 下标。所以如果需要监听数组的内容变化,就需要将数组转换成对象,并且还要模拟数组的方法。


      大概的思路和编码流程顺序如下:


      1. 判断要监听的数据是否为数组
      2. 是数组的情况,就将数组模拟成一个对象
      3. 将数组的方法名绑定到新创建的对象中
      4. 将对应数组原型的方法赋给自定义方法



      代码如下所示


      // 触发更新视图
      function updateView() {
      console.log('视图更新')
      }

      // 重新定义数组原型
      const oldArrayProperty = Array.prototype
      // 创建新对象,原形指向 oldArrayProperty,再扩展新的方法不会影响原型
      const arrProto = Object.create(oldArrayProperty);

      ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName = 收起阅读 »

      JS 将伪数组转换成数组

      在 JS 中,伪数组 是非常常见的,它也叫 类数组。伪数组可能会给 JS 初学者带来一点困扰。 本文将详细讲解 什么是伪数组,以及分别在 ES5 和 ES6 中将伪数组转换成真正的数组 。 什么是伪数组? 伪数组的主要特征:它是一个对象,并且该对象有 le...
      继续阅读 »




      JS 中,伪数组 是非常常见的,它也叫 类数组。伪数组可能会给 JS 初学者带来一点困扰。


      本文将详细讲解 什么是伪数组,以及分别在 ES5ES6 中将伪数组转换成真正的数组






      什么是伪数组?


      伪数组的主要特征:它是一个对象,并且该对象有 length 属性


      比如


      let arrayLike = {
      "0": "a",
      "1": "b",
      "2": "c",
      "length": 3
      }

      像上面的 arrayLike 对象,有 length 属性,key 也是有序序列。可以遍历,也可以查询长度。但却不能调用数组的方法。比如 push、pop 等方法。


      ES6 之前,还有一个常见的伪数组:arguments


      arguments 看上去也很像一个数组,但它没有数组的方法。


      比如 arguments.push(1) ,这样做一定会报错。




      除了 arguments 之外,NodeList 对象表示节点的集合也是伪数组,比如通过 document.querySelectorAll 获取的节点集合等。






      转换


      将伪数组转换成真正的数组的方法不止一个,我们先从 ES5 讲起。




      ES5 的做法


      在 ES6 问世之前,开发者通常需要用以下的方法把伪数组转换成数组。




      方法1


      // 通过 makeArray 方法,把数组转成伪数组
      function makeArray(arrayLike) {
      let result = [];
      for (let i = 0, len = arrayLike.length; i < len; i++) {
      result.push(arrayLike[i]);
      }
      return result;
      }

      function doSomething () {
      let args = makeArray(arguments);
      console.log(args);
      }

      doSomething(1, 2, 3);

      // 输出: [1, 2, 3]

      这个方法虽然有效,但要多写很多代码。




      方法2


      function doSomething () {
      let args = Array.prototype.slice.call(arguments);
      console.log(args);
      }
      doSomething(1, 2, 3);

      // 输出: [1, 2, 3]

      这个方法的功能和 方法1 是一样的,虽然代码量减少了,但不能很直观的让其他开发者觉得这是在转换。




      ES6的做法


      直到 ES6 提供了 Array.from 方法完美解决以上问题。


      function doSomething () {
      let args = Array.from(arguments);
      console.log(args);
      }

      doSomething('一', '二', '三');

      // 输出: ['一', '二', '三']

      Array.from 的主要作用就是把伪数组和可遍历对象转换成数组的。




      说“主要作用”的原因是因为 Array.from 还提供了2个参数可传。这样可以延伸很多种小玩法。


      Array.from 的第二个参数是一个函数,类似 map遍历 方法。用来遍历的。


      Array.from 的第三个参数接受一个 this 对象,用来改变 this 指向。




      第三个参数的用法(不常用)


      let helper = {
      diff: 1,
      add (value) {
      return value + this.diff; // 注意这里有个 this
      }
      };

      function translate () {
      return Array.from(arguments, helper.add, helper);
      }

      let numbers = translate(1, 2, 3);

      console.log(numbers); // 2, 3, 4



      Array.from 其他玩法


      创建长度为5的数组,且初始化数组每个元素都是1


      let array = Array.from({length: 5}, () => 1)
      console.log(array)

      // 输出: [1, 1, 1, 1, 1]

      第二个参数的作用和 map遍历 差不多的,所以 map遍历 有什么玩法,这里也可以做相同的功能。就不多赘述了。




      把字符串转换成数组


      let msg = 'hello';
      let msgArr = Array.from(msg);
      console.log(msgArr);

      // 输出: ["h", "e", "l", "l", "o"]

      如果传一个真正的数组给 Array.from 会返回一个一模一样的数组。

       
      收起阅读 »

      我写了一个将 excel 文件转化成 本地json文件的插件

      插件介绍 excel-2b-json 插件用于将 google excel 文件转化成 本地json文件。 适用场景: 项目国际化,配置多语言 使用方法 1. 安装excel-2b-json npm install excel-2b-json 2. 引入使用...
      继续阅读 »


      插件介绍


      excel-2b-json 插件用于将 google excel 文件转化成 本地json文件。


      适用场景: 项目国际化,配置多语言


      使用方法


      1. 安装excel-2b-json


      npm install excel-2b-json

      2. 引入使用


      const excelToJson = require('excel-2b-json');
      // path 生成的json文件目录

      excelToJson('https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0', path)


      转化得到



      下面是插件的实现



      源码已放到github:github.com/Sunny-lucki…



      一、涉及的算法


      1. 26字母转换成数字,26进制,a为1,aa为27,ab为28


        function colToInt(col) {
      const letters = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
      col = col.trim().split('')
      let n = 0

      for (let i = 0; i < col.length; i++) {
      n *= 26
      n += letters.indexOf(col[i])
      }

      return n
      }

      2. 生成几行几列的二维空数组


      function getEmpty2DArr(rows, cols) {
      let arrs = new Array(rows);
      for (var i = 0; i < arrs.length; i++) {
      arrs[i] = new Array(cols).fill(''); //每行有cols列
      }
      return arrs;
      }


      3. 清除二维数组中空的数组


      [
      [1,2,3],
      ['','',''],
      [7,8,9]
      ]

      转化为
      [
      [1,4,7],
      [3,6,9]
      ]

        clearEmptyArrItem(matrix) {
      return matrix.filter(function (val) {
      return val.some(function (val1) {
      return val1.replace(/\s/g, '') !== ''
      })
      })
      }


      4. 矩阵的翻转


      [
      [1,2,3],
      [4,5,6],
      [7,8,9]
      ]

      转化为
      [
      [1,4,7],
      [2,5,8],
      [3,6,9]
      ]

      算法实现


        /**
      *
      * @param {array*2} matrix 一个二维数组,返回旋转后的二维数组。
      */

      rotateExcelDate(matrix) {
      if (!matrix[0]) return []
      var results = [],
      result = [],
      i,
      j,
      lens,
      len
      for (i = 0, lens = matrix[0].length; i < lens; i++) {
      result = []
      for (j = 0, len = matrix.length; j < len; j++) {
      result[j] = matrix[j][i]
      }
      results.push(result)
      }
      return results
      }

      二、插件的实现


      1. 下载google Excel文档到本地


      我们先看看google Excel文档的url的组成


      https://docs.google.com/spreadsheets/d/文档ID/edit#哈希值

      例如下面这条,你可以尝试打开,下面这条链接是可以打开的。


      https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0


      下载google文档的步骤非常简单,只要获取原始的链接,然后拼接成下面的url,向这个Url发起请求,然后以流的方式写入生成文件就可以了。


      https://docs.google.com/spreadsheets/d/ + "文档ID" + '/export?format=xlsx&id=' + id + '&' + hash

      因此实现下载的方法非常简单,可以直接看代码


      downLoadExcel.js



      const fs = require('fs')
      const request = require('superagent')
      const rmobj = require('./remove')

      /**
      * 下载google excel 文档到本地
      * @param {*} url // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0
      * @returns
      */

      function downLoadExcel(url) {

      // 记录当前下载文件的目录,方便删除
      rmobj.push({
      path: __dirname,
      ext: 'xlsx'
      })
      return new Promise((resolve, reject) => {
      var down1 = url.split('/')
      var down2 = down1.pop() // edit#gid=0
      var url2 = down1.join('/') // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM
      var id = down1.pop() // 12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM
      var hash = down2.split('#').pop() // gid=0
      var downurl = url2 + '/export?format=xlsx&id=' + id + '&' + hash // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/export?format=xlsx&id=12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM&gid=0
      var loadedpath = __dirname + '/' + id + '.xlsx'
      const stream = fs.createWriteStream(loadedpath)
      const req = request.get(downurl)
      req.pipe(stream).on('finish', function () {
      resolve(loadedpath)
      // 已经成功下载下来了,接下来将本地excel转化成json的工作就交给Excel对象来完成
      })
      })

      }

      module.exports = downLoadExcel

      入口文件可以这样写


      async function excelToJson(excelPathName, outputPath) {
      if (Util.checkAddress(excelPathName) === 'google') {
      // 1.判断是谷歌excel文档,需要交给Google对象去处理,主要是下载线上的,生成本地excel文件
      const filePath = await downLoadExcel(excelPathName)

      // 2.解析本地excel成二维数组
      const data = await parseXlsx(filePath)

      // 3.生成json文件
      generateJsonFile(data, outputPath)
      }

      }
      module.exports = excelToJson


      之所以写if判断,是为了后面扩展,也许就不止是解析google文档了,或许也要解析腾讯等其他文档呢


      第一步已经实现了,接下来就看第二步怎么实现


      2. 解析本地excel成二维数组


      解析本地excel文件,获取excel的sheet信息和strings信息


      excel 文件其实本质上是多份xml文件的压缩文件。



      xml是存储数据的,而html是显示数据的



      而在这里我们只需要获取两份xml 文件,一份是strings,就是excel里的内容,一份是sheet,概括整个excel文件的信息。


      async function parseXlsx(path) {

      // 1. 解析本地excel文件,获取excel的sheet信息和content信息
      const files = await extractFiles(path);

      // 2. 根据strings和sheet解析成二维数组
      const data = await extractData(files)

      // 3. 处理二维数组的内容,
      const fixData = handleData(data)
      return fixData;
      }

      所以第一步我们看看怎么获取excel的sheet信息和strings信息


      function extractFiles(path) {

      // excel的本质是多份xml组成的压缩文件,这里我们只需要xl/sharedStrings.xml和xl/worksheets/sheet1.xml
      const files = {
      strings: {}, // strings内容
      sheet: {},
      'xl/sharedStrings.xml': 'strings',
      'xl/worksheets/sheet1.xml': 'sheet'
      }

      const stream = path instanceof Stream ? path : fs.createReadStream(path)

      return new Promise((resolve, reject) => {
      const filePromises = [] // 由于一份excel文档,会被解析成好多分xml文档,但是我们只需要两份xml文档,分别是(xl/sharedStrings.xml和xl/worksheets/sheet1.xml),所以用数组接受

      stream
      .pipe(unzip.Parse())
      .on('error', reject)
      .on('close', () => {
      Promise.all(filePromises).then(() => {
      return resolve(files)
      })
      })
      .on('entry', entry => {

      // 每解析某个xml文件都会进来这里,但是我们只需要xl/sharedStrings.xml和xl/worksheets/sheet1.xml,并将内容保存在strings和sheet中
      const file = files[entry.path]
      if (file) {
      let contents = ''
      let chunks = []
      let totalLength = 0
      filePromises.push(
      new Promise(resolve => {
      entry
      .on('data', chunk => {
      chunks.push(chunk)
      totalLength += chunk.length
      })
      .on('end', () => {
      contents = Buffer.concat(chunks, totalLength).toString()
      files[file].contents = contents
      if (/�/g.test(contents)) {
      throw TypeError('本次转化出现乱码�')
      } else {
      resolve()
      }
      })
      })
      )
      } else {
      entry.autodrain()
      }
      })
      })
      }

      可以断点看看entry.path,你就会看到分别进来了好几次,然后我们会分别看到我们想要的那两个文件



      两份xml文件解析之后就会到close方法里了,这时就可以看到strings和sheet都有内容了,而且内容都是xml



      我们分别看看strings和sheet的内容


      stream
      .pipe(unzip.Parse())
      .on('error', reject)
      .on('close', () => {
      Promise.all(filePromises).then(() => {
      console.log(files.strings.contents);
      console.log(files.sheet.contents);
      return resolve(files)
      })
      })


      格式化一下


      strings



      sheet


      可以发现strings的内容非常简单,现在我们借助xmldom将内容解析为节点对象,然后用xpath插件来获取内容


      xpath的用法:github.com/goto100/xpa…


        const XMLDOM = require('xmldom')
      const xpath = require('xpath')
      const ns = { a: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' }
      const select = xpath.useNamespaces(ns)

      const valuesDoc = new XMLDOM.DOMParser().parseFromString(
      files.strings.contents
      )

      // 把所有每个格子的内容都放进了values数组里。
      values = select('//a:si', valuesDoc).map(string =>

      select('.//a:t', string)
      .map(t => t.textContent)
      .join('')
      )


      '//a:si' 是xpath语法,//表示选择当前节点下的所有子孙节点,a是schemas.openxmlformats.org/spreadsheet…




      可以看到,xpath的用法很简单,就是找到si节点下的子节点t的内容,然后放进数组里



      最终生成的values数组是[ 'lang', 'cn','en', 'lang001','我是阳光', 'i am sunny','lang002', '前端阳光','FE Sunny', 'lang003','带带我', 'ddw']


      现在我们要获取sheet的内容了,我们先分析一下xml结构



      可以看到sheetData节点其实就是记录strings的内容的信息的,strings的内容是我们真正输入的,而sheet则是类似一种批注。


      我们分析看看


      row就是表示表格中的行,c则表示的是列,属性t="s"表示的是当前这个格子有内容,r="A1"表示的是在第一行中的A列



      而节点v则表示该格子是该表格的第几个有值的格子,不信?我们可以试试看




      可以看到这打印出来的xml内容,strings中已经没有了那两个值,而sheet中的那两个格子的c节点的t属性没了,而且v节点也没有了。


      现在我们可以知道,string只保存有值的格子里的值,而sheet则是一个网格,不管格子有没有值都会记录,有值的会有个序号存在v节点中。


      现在就要收集c节点


        const na = {
      textContent: ''
      }

      class CellCoords {
      constructor(cell) {
      cell = cell.split(/([0-9]+)/)
      this.row = parseInt(cell[1])
      this.column = colToInt(cell[0])
      }
      }

      class Cell {
      constructor(cellNode) {
      const r = cellNode.getAttribute('r')
      const type = cellNode.getAttribute('t') || ''
      const value = (select('a:v', cellNode, 1) || na).textContent
      const coords = new CellCoords(r)

      this.column = coords.column // 该格子所在列数
      this.row = coords.row // 该格子所在行数
      this.value = value // 该格子的顺序
      this.type = type // 该格子是否为空
      }
      }

      const cells = select('/a:worksheet/a:sheetData/a:row/a:c', sheet).map(
      node => new Cell(node)
      )

      每个c节点用cell对象来表示


      可以看到cell节点有四个属性。


      你现在知道它为什么要保存顺序了吗?


      因为这样才可以直接从strings生成的values数组中拿出对应顺序的值填充到网格中。


      接下来要获取总共有多少列数和行数。这就需要获取最大最小行数列数,然后求差得到


      // 计算该表格的最大最小列数行数
      d = calculateDimensions(cells)

      const cols = d[1].column - d[0].column + 1
      const rows = d[1].row - d[0].row + 1

      function calculateDimensions(cells) {
      const comparator = (a, b) => a - b
      const allRows = cells.map(cell => cell.row).sort(comparator)
      const allCols = cells.map(cell => cell.column).sort(comparator)
      const minRow = allRows[0]
      const maxRow = allRows[allRows.length - 1]
      const minCol = allCols[0]
      const maxCol = allCols[allCols.length - 1]

      return [{ row: minRow, column: minCol }, { row: maxRow, column: maxCol }]
      }

      接下来就根据列数和行数造空二维数组,然后再根据cells和values填充内容


        // 计算该表格的最大最小列数行数
      d = calculateDimensions(cells)

      const cols = d[1].column - d[0].column + 1
      const rows = d[1].row - d[0].row + 1

      // 生成二维空数组
      data = getEmpty2DArr(rows, cols)

      // 填充二维空数组
      for (const cell of cells) {
      let value = cell.value

      // s表示该格子有内容
      if (cell.type == 's') {
      value = values[parseInt(value)]
      }

      // 填充该格子
      if (data[cell.row - d[0].row]) {
      data[cell.row - d[0].row][cell.column - d[0].column] = value
      }
      }
      return data

      我们看看最终生成的data,可以发现,excel的网格已经被二维数组模拟出来了



      所以我们看看extractData的完整实现


      function extractData(files) {
      let sheet
      let values
      let data = []

      try {
      sheet = new XMLDOM.DOMParser().parseFromString(files.sheet.contents)
      const valuesDoc = new XMLDOM.DOMParser().parseFromString(
      files.strings.contents
      )

      // 把所有每个格子的内容都放进了values数组里。
      values = select('//a:si', valuesDoc).map(string =>
      select('.//a:t', string)
      .map(t => t.textContent)
      .join('')
      )

      console.log(values);
      } catch (parseError) {
      return []
      }



      const na = {
      textContent: ''
      }

      class CellCoords {
      constructor(cell) {
      cell = cell.split(/([0-9]+)/)
      this.row = parseInt(cell[1])
      this.column = colToInt(cell[0])
      }
      }

      class Cell {
      constructor(cellNode) {
      const r = cellNode.getAttribute('r')
      const type = cellNode.getAttribute('t') || ''
      const value = (select('a:v', cellNode, 1) || na).textContent
      const coords = new CellCoords(r)

      this.column = coords.column // 该格子所在列数
      this.row = coords.row // 该格子所在行数
      this.value = value // 该格子的顺序
      this.type = type // 该格子是否为空
      }
      }

      const cells = select('/a:worksheet/a:sheetData/a:row/a:c', sheet).map(
      node => new Cell(node)
      )

      // 计算该表格的最大最小列数行数
      d = calculateDimensions(cells)

      const cols = d[1].column - d[0].column + 1
      const rows = d[1].row - d[0].row + 1

      // 生成二维空数组
      data = getEmpty2DArr(rows, cols)

      // 填充二维空数组
      for (const cell of cells) {
      let value = cell.value

      // s表示该格子有内容
      if (cell.type == 's') {
      value = values[parseInt(value)]
      }

      // 填充该格子
      if (data[cell.row - d[0].row]) {
      data[cell.row - d[0].row][cell.column - d[0].column] = value
      }
      }
      return data
      }


      接下来就是要去除空行和空列,并将二维数组翻转成我们需要的格式


      function handleData(data) {
      if (data) {
      data = clearEmptyArrItem(data)
      data = rotateExcelDate(data)
      data = clearEmptyArrItem(data)
      }
      return data
      }


      可以看到,现在数组的第一项子数组则是key列表了。


      接下来就可以根据key来生成对应的json文件了。


      3. 生成json数据


      这一步非常简单


      function generateJsonFile(excelDatas, outputPath) {

      // 获得转化成json格式
      const jsons = convertProcess(excelDatas)

      // 生成写入文件
      writeFile(jsons, outputPath)
      }

      首先就是获取json数据


      先获取data数组的第一项数组,第一项数组是key,然后生成每种语言的json对象


        /**
      *
      * @param {array*2} data
      * 返回处理完后的多语言数组,每一项都是一个json对象。
      */

      function convertProcess(data) {
      var keys_arr = [],
      data_arr = [],
      result_arr = [],
      i,
      j,
      data_arr_len,
      col_data_json,
      col_data_arr,
      data_arr_col_len
      // 表格合并处理,这是json属性列。
      keys_arr = data[0]
      // 第一例是json描述,后续是语言包
      data_arr = data.slice(1)

      for (i = 0, data_arr_len = data_arr.length; i < data_arr_len; i++) {
      // 取出第一个列语言包
      col_data_arr = data_arr[i]
      // 该列对应的临时对象
      col_data_json = {}
      for (
      j = 0, data_arr_col_len = col_data_arr.length;
      j < data_arr_col_len;
      j++
      ) {

      col_data_json[keys_arr[j]] = col_data_arr[j]
      }
      result_arr.push(col_data_json)
      }

      return result_arr
      }


      我们可以看看生成的result_arr



      可见已经成功生成每一种语言的json对象了。


      接下来只需要生成json文件就可以了,注意把之前生成的excel文件删除


        //得到的数据写入文件
      function writeFile(datas, outputPath) {
      for (let i = 0, len = datas.length; i < len; i++) {
      fs.writeFileSync(outputPath +
      (datas[i].filename || datas[i].lang) +
      '.json',
      JSON.stringify(datas[i], null, 4)
      )
      }
      rmobj.flush();
      }

      到此,一个稍微完美的插件就此完成了。 撒花撒花!!!!

      收起阅读 »

      一盏茶的功夫,拿捏作用域&作用域链

      前言 我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢? 一、作...
      继续阅读 »


      前言


      我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢?


      一、作用域(scope)


      作用域的定义:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。


      1、作用域的分类

      1. 全局作用域

      var name="global";
      function foo(){
      console.log(name);
      }
      foo();//global

      这里函数foo()内部并没有声明name变量,但是依然打印了name的值,说明函数内部可以访问到全局作用域,读取name变量。再来一个例子:


      hobby='music';
      function foo(){
      hobby='book';
      console.log(hobby);
      }
      foo();//book

      这里全局作用域和函数foo()内部都没有声明hobby这个变量,为什么不会报错呢?这是因为hobby='music';写在了全局作用域,就算没有var,let,const的声明,也会被挂在window对象上,所以函数foo()不仅可以读取,还可以修改值。也就是说hobby='music';等价于window.hobby='music';


      1. 函数体作用域

      函数体的作用域是通过隐藏内部实现的。换句话说,就是我们常说的,内层作用域可以访问外层作用域,但是外层作用域不能访问内层。原因,说到作用域链的时候就迎刃而解了。


      function foo(){
      var age=19;
      console.log(age);
      }
      console.log(age);//ReferenceError:age is not defined

      很明显,全局作用域下并没有age变量,但是函数foo()内部有,但是外部访问不到,自然而然就会报错了,而函数foo()没有调用,也就不会执行。


      1. 块级作用域

      块级作用域更是见怪不怪,像我们接触的let作用域,代码块{},for循环用let时的作用域,if,while,switch等等。然而,更深刻理解块级作用域的前提是,我们需要先认识认识这几个名词:


      --标识符:能在作用域生效的变量。函数的参数,变量,函数名。需要格外注意的是:函数体内部的标识符外部访问不到


      --函数声明:function 函数名(){}


      --函数表达式: var 函数名=function(){}


      --自执行函数: (function 函数名(){})();自执行函数前面的语句必须有分号,通常用于隐藏作用域。


      接下来我们就用一个例子,一口气展示完吧


      function foo(sex){
      console.log(sex);
      }
      var f=function(){
      console.log('hello');
      }
      var height=180;
      (
      function fn(){
      console.log(height);
      }
      )();
      foo('female');
      //依次打印:
      //180
      //female
      //hello

      分析一下:标识符:foo,sex,height,fn;函数声明:function foo(sex){};函数表达式:var f=function(){};自执行函数:(function fn(){})();需要注意,自执行函数fn()前面的var height=180;语句,分号不能抛弃。否则,你可以试一下。


      二、预编译


      说好只是作用域和作用域链的,但是考虑到理解作用域链的必要性,这里还是先聊聊预编译吧。先讨论预编译在不同环境发生的情况下,是如何进行预编译的。


      1. 发生在代码执行之前

      (1)声明提升


      console.log(b);
      var b=123;//undefined

      这里打印undefined,这不是报错,与Refference:b is not defined不同。这是代码执行之前,预编译的结果,等同于以下代码:


      var b;//声明提升
      console.log(b);//undefined
      b=123;

      (2)函数声明整体提升


      test();//hello123  调用函数前并没有声明,但是任然打印,是因为函数声明整体提升了
      function test(){
      var a=123;
      console.log('hello'+a);
      }

      2.发生在函数执行之前


      理解这个只需要掌握四部曲


      (1)创建一个AO(Activation Object)


      (2)找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined


      (3)将实参和形参统一


      (4)在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
      那么接下来就放大招了:


      var global='window';
      function foo(name,sex){
      console.log(name);
      function name(){};
      console.log(name);
      var nums=123;
      function nums(){};
      console.log(nums);
      var fn=function(){};
      console.log(fn);
      }
      foo('html');

      这里的结果是什么呢?分析如下:


      //从上到下
      //1、创建一个AO(Activation Object)
      AO:{
      //2、找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
      name:undefined,
      sex:undefined,
      nums=undefined,
      fn:undefined,
      //3、将实参和形参统一
      name:html,
      sex:undefined,
      nums=123,
      fn:function(){},
      //4、在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
      name:function(){},
      sex:undefined,
      fn:function(){},
      nums:123//这里不仅存在nums变量声明,也存在nums函数声明,但是取前者的值

      以上步骤得到的值,会按照后面步骤得到的值覆盖前面步骤得到的值
      }
      //依次打印
      //[Function: name]
      //[Function: name]
      //123
      //[Function: fn]

      3.发生在全局(内层作用域可以访问外层作用域)


      同发生在函数执行前一样,发生在全局的预编译也有自己的三部曲:


      (1)创建GO(Global Object)对象
      (2)找全局变量声明,将变量声明作为GO的属性名,属性值为undefined
      (3)在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体
      举个栗子:


      var global='window';
      function foo(a){
      console.log(a);
      console.log(global);
      var b;
      }
      var fn=function(){};
      console.log(fn);
      foo(123);
      console.log(b);

      这个例子比较简单,一样的步骤和思路,就不在赘述分析了,相信你已经会了。打印结果依次是:


      [Function: fn]
      123
      window
      ReferenceError: b is not defined

      好啦,进入正轨,我们接着说作用域链。


      三、作用域链


      作用域链就可以帮我们找到,为什么内层可以访问到外层,而外层访问不到内层?但是同样的,在认识作用域链之前,我们需要见识见识一些更加晦涩抽象的名词。


      1. 执行期上下文:当函数执行的时候,会创建一个称为执行期上下文的对象(AO对象),一个执行期上下文定义了一个函数执行时的环境。 函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁。
      2. 查找变量:从作用域链的顶端依次往下查找。

      3. [[scope]]:作用域属性,也称为隐式属性,仅支持引擎自己访问。函数作用域,是不可访问的,其中存储了运行期上下文的结合。


      我们先看一眼函数的自带属性:


      function test(){//函数被创建的那一刻,就携带name,prototype属性
      console.log(123);
      }
      console.log(test.name);//test
      console.log(test.prototype);//{} 原型
      // console.log(test[[scope]]);访问不到,作用域属性,也称为隐式属性

      // test() --->AO:{}执行完毕会回收
      // test() --->AO:{}执行完毕会回收

      接下来看看作用域链怎么实现的:


      var global='window';
      function foo(){
      function fn(){
      var fn=222;
      }
      var foo=111;
      console.log(foo);
      }
      foo();

      分析:


      GO:{
      foo:function(){}
      }
      fooAO:{
      foo:111,
      fn:function(){}
      }
      fnAO:{
      fn:222
      }
      // foo定义时 foo.[[scope]]---->0:GO{}
      // foo执行时 foo.[[scope]]---->0:AO{} 1:GO{} 后访问的在前面
      //fn定义时 fn.[[scope]]---->0:fnAO{} 1:fooAO{} 2:GO{}
      fnAO:fn的AO对象;fooAO:foo的AO对象

       


      综上而言:作用域链就是[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。



      收起阅读 »

      iOS block与__block、weak、__weak、__strong

      iOS

      首先需要知道:

      block,本质是OC对象,对象的内容,是代码块。
      封装了函数调用以及函数调用环境。

      block也有自己的isa指针,依据block的类别不同,分别指向
      __NSGlobalBlock __ ( _NSConcreteGlobalBlock )
      __NSStackBlock __ ( _NSConcreteStackBlock )
      __NSMallocBlock __ ( _NSConcreteMallocBlock )
      需要注意是,ARC下只存在__NSGlobalBlock和__NSMallocBlock。
      通常作为参数时,才可能是栈区block,但是由于ARC的copy作用,会将栈区block拷贝到堆上。
      通常不管作为属性、参数、局部变量的block,都是__NSGlobalBlock,即使block内部出现了常量、静态变量、全局变量,也是__NSGlobalBlock,
      除非block内部出现其他变量,auto变量或者对象属性变量等,就是__NSMallocBlock

      为什么block要被拷贝到堆区,变成__NSMallocBlock,可以看如下链接解释:Ios开发-block为什么要用copy修饰

      对于基础数据类型,是值传递,修改变量的值,修改的是a所指向的内存空间的值,不会改变a指向的地址。

      对于指针(对象)数据类型,修改变量的值,是修改指针变量所指向的对象内存空间的地址,不会改变指针变量本身的地址
      简单来说,基础数据类型,只需要考虑值的地址,而指针类型,则需要考虑有指针变量的地址和指针变量指向的对象的地址

      以变量a为例

      1、基础数据类型,都是指值的地址

      1.1无__block修饰,

      a=12,地址为A
      block内部,a地址变B,不能修改a的值
      block外部,a的地址依旧是A,可以修改a的值,与block内部的a互不影响
      内外a的地址不一致

      1.2有__block修饰

      a=12,地址为A
      block内部,地址变为B,可以修改a的值,修改后a的地址依旧是B
      block外部,地址保持为B,可以修改a的值,修改后a的地址依旧是B

      2、指针数据类型

      2.1无__block修饰

      a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
      block内部,a指针变量的地址为C,指向的对象地址为B,不能修改a指向的对象地址
      block外部,a指针变量的地址为A,指向的对象地址为B,可以修改a指向的对象地址,
      block外部修改后,
      外部a指针变量的地址依旧是A,指向的对象地址变为D
      内部a指针变量的地址依旧是C,指向的对象地址依旧是B

      2.1有__block修饰

      a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
      block内部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
      block外部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
      block内外,或者另一个block中,无论哪里修改,a指针变量地址都保持为C,指向的对象地址保持为修改后的一致

      block内修改变量的实质(有__block修饰):

      block内部能够修改的值,必须都是存放在堆区的。
      1、基础数据类型,__block修饰后,调用block时,会在堆区开辟新的值的存储空间,
      指针数据类型,__block修饰后,调用block时,会在堆区开辟新的指针变量地址的存储空间

      2、并且无论是基础数据类型还是指针类型,block内和使用block之后,变量的地址所有地址(包括基础数据类型的值的地址,指针类型的指针变量地址,指针指向的对象的地址),都是保持一致的
      当然,只有block进行了真实的调用,才会在调用后发生这些地址的变化

      另外需要注意的是,如果对一个已存在的对象(变量a),进行__block声明另一个变量b去指向它,
      a的指针变量地址为A,b的指针变量会是B,而不是A,
      原因很简单,不管有没__block修饰,不同变量名指向即使指向同一个对象,他们的指针变量地址都是不同的。

      __weak,__strong

      两者本身也都会增加引用计数。
      区别在于,__strong声明,会在作用域区间范围增加引用计数1,超过其作用域然后引用计数-1
      而__weak声明的变量,只会在其使用的时候(这里使用的时候,指的是一句代码里最终并行使用的次数),临时生成一个__strong引用,引用+次数,一旦使用使用完毕,马上-次数,而不是超出其作用域再-次数

          NSObject *obj = [NSObject new];
      NSLog(@"声明时obj:%p, %@, 引用计数:%ld",&obj, obj, CFGetRetainCount((__bridge CFTypeRef)(obj)));
      __weak NSObject *weakObj = obj;
      NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));
      NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

      声明时obj:0x16daa3968, , 引用计数:1
      声明时weakObj:0x16daa3960, ,, , 引用计数:5
      声明后weakObj引用计数:2

      这个5,是因为obj本来计数是1,

          NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

      这句代码打印5,是因为除去&weakObj(&这个不是使用weakObj指向的对象,而只是取weakObj的指针变量地址,所以不会引起计数+1),另外还使用了4次weakObj,导致引用计数+4

         NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

      这句打印2,说明上一句使用完毕后,weakObj引用增加的次数会马上清楚,重新变回1,而这句使用了一次weakObj,加上obj的一次引用,就是2了

      __weak 与 weak

      通常,__weak是单独为某个对象,添加一条弱引用变量的。
      weak则是property属性里修饰符。

      LGTestBlockObj *testObj = [LGTestBlockObj new];
      self.prpertyObj = testObj;
      __weak LGTestBlockObj *weakTestObj = testObj;
      NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
      NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), self.prpertyObj,self.prpertyObj,self.prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(self.prpertyObj)));
      NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), _prpertyObj,_prpertyObj,_prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
      NSLog(@"prpertyObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
      NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
      NSLog(@"weakTestObj:%p, %@,%@, %@, 引用计数:%ld",&weakTestObj, weakTestObj,weakTestObj,weakTestObj, CFGetRetainCount((__bridge CFTypeRef)(weakTestObj)));

      prpertyObj:0x1088017b0, ,, , 引用计数:2
      prpertyObj:, 引用计数:2
      testObj:, 引用计数:2
      weakTestObj:0x16b387958, ,, , 引用计数:6

      待补充...

      Block常见疑问收录

      1、block循环引用

      通常,block作为属性,并且block内部直接引用了self,就会出现循环引用,这时就需要__weak来打破循环。

      2、__weak为什么能打破循环引用?

      一个变量一旦被__weak声明后,这个变量本身就是一个弱引用,只有在使用的那行代码里,才会临时增加引用结束,一旦那句代码执行完毕,引用计数马上-1,所以看起来的效果是,不会增加引用计数,block中也就不会真正持有这个变量了

      3、为什么有时候又需要使用__strong来修饰__weak声明的变量?

      在block中使用__weak声明的变量,由于block没有对该变量的强引用,block执行的过程中,一旦对象被销毁,该变量就是nil了,会导致block无法继续正常向后执行。
      使用__strong,会使得block作用区间,保存一份对该对象的强引用,引用计数+1,一旦block执行完毕,__strong变量就会销毁,引用计数-1
      比如block中,代码执行分7步,在执行第二步时,weak变量销毁了,而第五步要用到weak变量。
      而在block第一步,可先判断weak变量是否存在,如果存在,加一个__strong引用,这样block执行过程中,就始终存在对weak变量的强引用了,直到block执行完毕

      4、看以下代码,obj对象最后打印的引用计数是多少,为什么?

          NSObject *obj = [NSObject new];
      void (^testBlock)(void) = ^{
      NSLog(@"%@",obj);
      };
      NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

      最后的打印的是3
      作为一个局部变量的block,由于引用了外部变量(非静态、常量、全局),定义的时候其实是栈区block,但由于ARC机制,使其拷贝到堆上,变成堆block,所以整个函数执行的过程中,实际上该block,存在两份,一个栈区,一个堆区,这就是使得obj引用计数+2了,加上创建obj的引用,就是3了

      5、为什么栈区block要copy到堆上

      block:我们称代码块,他类似一个方法。而每一个方法都是在被调用的时候从硬盘到内存,然后去执行,执行完就消失,所以,方法的内存不需要我们管理,也就是说,方法是在内存的栈区。所以,block不像OC中的类对象(在堆区),他也是在栈区的。如果我们使用block作为一个对象的属性,我们会使用关键字copy修饰他,因为他在栈区,我们没办法控制他的消亡,当我们用copy修饰的时候,系统会把该 block的实现拷贝一份到堆区,这样我们对应的属性,就拥有的该block的所有权。就可以保证block代码块不会提前消亡。

      转载自:https://cloud.tencent.com/developer/article/1894410

      iOS皮肤适配

      iOS
      1、皮肤颜色资源和图片路径配置皮肤配置文件light.json 配置示例dark.json 配置示例2、设置全局的colorKey 来对应颜色路径 imageKey 来对应图片路径,利于维护颜色key配置图片key配置1、获取皮肤资源协议方法2、皮肤使用3、...
      继续阅读 »
      皮肤配置文件创建

      1、皮肤颜色资源和图片路径配置


      皮肤配置文件

      如图所示,创建 light.json 和 dark.json ( light 和 dark 配置路径key 一样,对应的value 不同)

      light.json 配置示例

      {
      "statusBarStyle": "black",
      "colors":{
      "mainFunction":"#E92424",
      "gradientStockUp":[
      "#0FFFFFFF",
      "#0FE92424"
      ]
      },
      "images": {
      "selfStock_info_icon": "appres/skinImage/light/selfStock_info_icon.png",
      "selfStock_money_icon": "appres/skinImage/light/selfStock_money_icon.png",
      }

      // appres/skinImage/light/selfStock_info_icon.png 对应的图片文件夹路径
      }

      dark.json 配置示例

      {
      "statusBarStyle": "red",
      "colors":{
      "mainFunction":"#BC935C",
      "gradientStockUp":[
      "#26171717",
      "#26E92424"
      ]
      },
      "images": {
      "selfStock_info_icon": "appres/skinImage/dark/selfStock_info_icon.png",
      "selfStock_money_icon": "appres/skinImage/dark/selfStock_money_icon.png",
      }
      }

      2、设置全局的colorKey 来对应颜色路径 imageKey 来对应图片路径,利于维护


      颜色key配置


      图片key配置

      皮肤使用

      1、获取皮肤资源协议方法

      // 获取皮肤资源协议方法
      - (HJThemeDataModel *)getThemeModelWithName:(NSString *)name {
      NSString *path = [NSString stringWithFormat:@"appres/theme/%@",name];
      NSDictionary *colorDic = [self getDictFromJsonName:path][@"colors"];
      NSDictionary *imageDic = [self getDictFromJsonName:path][@"images"];
      HJThemeDataModel *model = [HJThemeDataModel new];
      model.colorDic = colorDic;
      model.imageDic = imageDic;
      return model;
      }

      /// 设置默认主题(使用皮肤,至少有一个默认皮肤)
      - (HJThemeDataModel *)getDefaultThemeModel {
      return [self getThemeModelWithName:@"light"];
      }

      2、皮肤使用

      // 导入头文件
      #import "HJThemeManager.h"

      // 设置当前皮肤 或切换 皮肤为 @"light"
      [[HJThemeManager sharedInstance] switchThemeWithName:@"light"];

      // 设置当前view 的背景色
      //1、适配皮肤
      self.view.themeBackgroundColor = backgroundColorKey;
      //2、不适配皮肤,必须带#号
      self.view.themeBackgroundColor = @“#333333;
      //3、适配皮肤,随皮肤变化
      self.view.themeBackgroundColor = [HJThemeManager getThemeColor:backgroundColorKey];
      //4、指定皮肤,不会随皮肤变化
      self.view.themeBackgroundColor = [HJThemeManager getThemeColor:backgroundColorKey themeName:@"light"];

      /**
      * [HJThemeManager getThemeColor:backgroundColorKey];
      * 实质上是
      theme://
      "backgroundColorKey"?
      *
      * [HJThemeManager getThemeColor:backgroundColorKey themeName:@"light"];
      * 实质上是
      theme://"backgroundColorKey"?themeName=light
      */

      //所以可以直接写URL 例如:
      self.view.themeBackgroundColor = theme://backgroundColorKey?themeName=light;

      // 设置当前imageView 的image
      //1、适配皮肤
      imageView.themeImage = imageKey;
      //2、适配皮肤,随皮肤变化
      imageView.themeImage = [HJThemeManager getThemeImage:imageKey];
      //3、指定皮肤,不会随皮肤变化
      imageView.themeImage = [HJThemeManager getThemeImage:imageKey themeName:@"light"];

      /**
      * [HJThemeManager getThemeImage:imageKey];
      * 实质上是
      theme://
      "imageKey"?
      *
      * [HJThemeManager getThemeImage:imageKey themeName:@"light"];
      * 实质上是
      theme://"imageKey"?themeName=light
      */


      //完整写法,指定皮肤
      imageView.themeImage = theme://"imageKey"?themeName=light;

      // 兼容不适配皮肤写法
      // imageNamed 加载图片
      imageView.themeImage = bundle://"imageKey";
      // sdwebimage 解析 http/https 加载图片
      imageView.themeImage = http://imagePath;
      // 使用serverManager getimage 的协议方法获取图片
      imageView.themeImage = imagePath;

      3、皮肤的实现原理
      1、创建一个NSObject分类(category),然后关联一个字典属性(themes),用于进行缓存UI控件调用的颜色方法和参数或者是图片方法和参数。再关联属性的时候添加一个通知监听,用于切换皮肤时,发送通知,然后再次调用缓存的方法和参数,进行颜色和图片的更换。

      2、创建UI控件的分类(category),然后每个分类都有themes字典,然后设置新的方法来设置颜色或图片。在该方法内,需要做的处理有:

      颜色举例说明:themeBackgroundColor = colorKey

      a、在 themeBackgroundColor 的set方法中,判断是否是皮肤设置,皮肤的设置都是带有 theme:// 的字符串。这个(theme://)字符串是约定的。
      b、皮肤适配模式,即带有 theme:// 字符串,就会用 themes 字典保存 系统的方法setBackgroundColor: 方法和参数colorKeythemeName,当切换皮肤时,再次调用 setBackgroundColor: 方法和参数colorKeythemeName
      c、@"#333333", 直接是色值方法的 不需要 themes 字典保存,只需要直接调用系统方法 setBackgroundColor:[UIColor colorFromHexString:@"#333333"];

      图片举例说明:imageView.themeImage = imageKey

      a、在 themeImage 的set方法中,判断是否是皮肤设置,皮肤的设置都是带有 theme:// 的字符串。这个(theme://)字符串是约定的。
      b、皮肤适配模式,即带有 theme:// 字符串,就会用 themes 字典保存 系统的方法setImage: 方法和参数imageKeythemeName,当切换皮肤时,再次调用 setImage: 方法和参数imageKeythemeName
      c、bundle://, 直接是调用系统方法setImage:[UIImage imageNamed:@"imageNamed"] 进行赋值,不需要进行 themes 字典保存处理;
      d、http:// 或 https:// , 采用SD框架加载图片,不需要进行 themes 字典保存处理;

      3、主要的UI控件的分类

      #import <UIKit/UIKit.h>
      #import <Foundation/Foundation.h>

      @interface UIView (CMSThemeView)

      /// 设置皮肤文件名称 默认为空值,取当前皮肤
      /// 可以设置指定皮肤 例如: @"Dark" / @"Light" ;
      /// defaultThemeKey 为默认皮肤
      /// 如何设置 Color 或 Image 有 themeName ,优先使用 themeName
      指定皮肤
      @property (nonatomic, copy) NSString *themeStyle;
      @property (nonatomic, copy) NSString *themeBackgroundColor;
      @property (nonatomic, copy) NSString *themeTintColor;
      /// 根据路径获取color 并缓存方法和参数 ()
      - (void)setThemeColorWithIvarName:(NSString *)ivarName colorPath:(NSString *)path;
      @end

      @interface UILabel (ThemeLabel)
      @property (nonatomic, copy) NSString *themeTextColor;
      @property (nonatomic, copy) NSString *themeHighlightedTextColor;
      @property (nonatomic, copy) NSString *themeShadowColor;
      /// 主要是颜色
      @property (nonatomic, strong) NSAttributedString *themeAttributedText;
      @end

      @interface UITextField (ThemeTextField)
      @property (nonatomic, copy) NSString *themeTextColor;
      @end

      @interface UIImageView (CMSThemeImageView)
      @property (nonatomic, copy) NSString *themeImage;

      // 带有 UIImageRenderingMode 的处理,image 修改渲染色的,即tintColor
      - (void)themeSetImageKey:(NSString *)imageKey
      renderingMode:(UIImageRenderingMode)mode;

      @end

      @interface UIButton (ThemeButton)

      - (void)themeSetImage:(NSString *)path forState:(UIControlState)state;
      - (void)themeSetImage:(NSString *)path forState:(UIControlState)state renderingMode:(UIImageRenderingMode)mode;
      - (void)themeSetBackgroundImage:(NSString *)path forState:(UIControlState)state;
      - (void)themeSetTitleColor:(NSString *)path forState:(UIControlState)state;
      @end

      @interface UITableView (ThemeTableView)
      @property (nonatomic, copy) NSString *themeSeparatorColor;
      @end

      @interface CALayer (ThemeLayer)
      /// 设置皮肤文件名称 默认为空值,取当前皮肤 eg: @"Dark" / @"Light" ; defaultThemeKey 为默认皮肤
      @property (nonatomic, copy) NSString *themeStyle;
      @property (nonatomic, copy) NSString *themeBackgroundColor;
      @property (nonatomic, copy) NSString *themeBorderColor;
      @property (nonatomic, copy) NSString *themeShadowColor;
      /// 根据路径获取cgcolor 并缓存方法和参数 ()
      - (void)setThemeCGColorWithIvarName:(NSString *)ivarName colorPath:(NSString *)path;
      @end

      以上是简单列举了几个,其他UIKIt 控件一样分类处理即可

      皮肤颜色流程图



      皮肤颜色流程图

      皮肤图片流程图


      皮肤图片流程图

      存在的缺陷

      1、不能全局统一处理,需要一处一处的设置,比较麻烦。
      2、目前还不支持网络下载皮肤功能,需要其他位置处理下载解压过程。
      3、XIB的使用还需要其他的处理,这个比较重要

      转载自:https://cloud.tencent.com/developer/article/1894412

      收起阅读 »

      Python爬虫 | 一条高效的学习路径

      数据是创造和决策的原材料,高质量的数据都价值不菲。而利用爬虫,我们可以获取大量的价值数据,经分析可以发挥巨大的价值,比如:豆瓣、知乎:爬取优质答案,筛选出各话题下热门内容,探索用户的舆论导向。淘宝、京东:抓取商品、评论及销量数据,对各种商品及用户的消费场景进行...
      继续阅读 »

      数据是创造和决策的原材料,高质量的数据都价值不菲。而利用爬虫,我们可以获取大量的价值数据,经分析可以发挥巨大的价值,比如:

      豆瓣、知乎:爬取优质答案,筛选出各话题下热门内容,探索用户的舆论导向。

      淘宝、京东:抓取商品、评论及销量数据,对各种商品及用户的消费场景进行分析。

      搜房、链家:抓取房产买卖及租售信息,分析房价变化趋势、做不同区域的房价分析。

      拉勾、智联:爬取各类职位信息,分析各行业人才需求情况及薪资水平。

      雪球网:抓取雪球高回报用户的行为,对股票市场进行分析和预测。

      爬虫是入门Python最好的方式,没有之一。Python有很多应用的方向,比如后台开发、web开发、科学计算等等,但爬虫对于初学者而言更友好,原理简单,几行代码就能实现基本的爬虫,学习的过程更加平滑,你能体会更大的成就感。

      掌握基本的爬虫后,你再去学习Python数据分析、web开发甚至机器学习,都会更得心应手。因为这个过程中,Python基本语法、库的使用,以及如何查找文档你都非常熟悉了。

      对于小白来说,爬虫可能是一件非常复杂、技术门槛很高的事情。比如有的人则认为先要掌握网页的知识,遂开始 HTMLCSS,结果入了前端的坑,瘁……

      但掌握正确的方法,在短时间内做到能够爬取主流网站的数据,其实非常容易实现,但建议你从一开始就要有一个具体的目标。

      在目标的驱动下,你的学习才会更加精准和高效。那些所有你认为必须的前置知识,都是可以在完成目标的过程中学到的。这里给你一条平滑的、零基础快速入门的学习路径。

      - ❶ -

      学习 Python 包并实现基本的爬虫过程

      大部分爬虫都是按“发送请求——获得页面——解析页面——抽取并储存内容”这样的流程来进行,这其实也是模拟了我们使用浏览器获取网页信息的过程。

      Python中爬虫相关的包很多:urllib、requests、bs4、scrapy、pyspider 等,建议从requests+Xpath 开始,requests 负责连接网站,返回网页,Xpath 用于解析网页,便于抽取数据。

      如果你用过 BeautifulSoup,会发现 Xpath 要省事不少,一层一层检查元素代码的工作,全都省略了。这样下来基本套路都差不多,一般的静态网站根本不在话下,豆瓣、糗事百科、腾讯新闻等基本上都可以上手了

      -❷-

      掌握各种技巧,应对特殊网站的反爬措施

      当然,爬虫过程中也会经历一些绝望啊,比如被网站封IP、比如各种奇怪的验证码、userAgent访问限制、各种动态加载等等。

      遇到这些反爬虫的手段,当然还需要一些高级的技巧来应对,常规的比如访问频率控制、使用代理IP池、抓包、验证码的OCR处理等等

      往往网站在高效开发和反爬虫之间会偏向前者,这也为爬虫提供了空间,掌握这些应对反爬虫的技巧,绝大部分的网站已经难不到你了。

      -❸-

      学习 scrapy,搭建工程化的爬虫

      掌握前面的技术一般量级的数据和代码基本没有问题了,但是在遇到非常复杂的情况,可能仍然会力不从心,这个时候,强大的 scrapy 框架就非常有用了。

      scrapy 是一个功能非常强大的爬虫框架,它不仅能便捷地构建request,还有强大的 selector 能够方便地解析 response,然而它最让人惊喜的还是它超高的性能,让你可以将爬虫工程化、模块化。

      学会 scrapy,你可以自己去搭建一些爬虫框架,你就基本具备爬虫工程师的思维了。

      - ❹ -

      学习数据库基础,应对大规模数据存储

      爬回来的数据量小的时候,你可以用文档的形式来存储,一旦数据量大了,这就有点行不通了。所以掌握一种数据库是必须的,学习目前比较主流的 MongoDB 就OK。

      MongoDB 可以方便你去存储一些非结构化的数据,比如各种评论的文本,图片的链接等等。你也可以利用PyMongo,更方便地在Python中操作MongoDB。

      因为这里要用到的数据库知识其实非常简单,主要是数据如何入库、如何进行提取,在需要的时候再学习就行。

      -❺-

      分布式爬虫,实现大规模并发采集

      爬取基本数据已经不是问题了,你的瓶颈会集中到爬取海量数据的效率。这个时候,相信你会很自然地接触到一个很厉害的名字:分布式爬虫

      分布式这个东西,听起来很恐怖,但其实就是利用多线程的原理让多个爬虫同时工作,需要你掌握 Scrapy + MongoDB + Redis 这三种工具

      Scrapy 前面我们说过了,用于做基本的页面爬取,MongoDB 用于存储爬取的数据,Redis 则用来存储要爬取的网页队列,也就是任务队列。

      所以有些东西看起来很吓人,但其实分解开来,也不过如此。当你能够写分布式的爬虫的时候,那么你可以去尝试打造一些基本的爬虫架构了,实现一些更加自动化的数据获取。

      你看,这一条学习路径下来,你已然可以成为老司机了,非常的顺畅。所以在一开始的时候,尽量不要系统地去啃一些东西,找一个实际的项目(开始可以从豆瓣、小猪这种简单的入手),直接开始就好

      在这里有一套非常系统的爬虫课程,除了为你提供一条清晰的学习路径,我们甄选了最实用的学习资源以及庞大的主流爬虫案例库。短时间的学习,你就能够很好地掌握 Python 爬虫,获取你想得到的数据,同时具备数据分析、机器学习的Python基础。

      如果你希望在短时间内学会Python爬虫,少走弯路

      - 高效的学习路径 -

      一上来就讲理论、语法、编程语言是非常不合理的,我们会直接从具体的案例入手,通过实际的操作,学习具体的知识点。我们为你规划了一条系统的学习路径,让你不再面对零散的知识点。

      说点具体的,比如我们会直接用 lxml+Xpath取代 BeautifulSoup 来进行网页解析,减少你不必要的检查网页元素的操作,多种工具都能完成的,我们会给你最简单的方法,这些看似细节,但可能是很多人都会踩的坑。

      《Python爬虫:入门+进阶》大纲

      第一章:Python 爬虫入门

      1、什么是爬虫

      网址构成和翻页机制

      网页源码结构及网页请求过程

      爬虫的应用及基本原理

      2、初识Python爬虫

      Python爬虫环境搭建

      创建第一个爬虫:爬取百度首页

      爬虫三步骤:获取数据、解析数据、保存数据

      3、使用Requests爬取豆瓣短评

      Requests的安装和基本用法

      用Requests爬取豆瓣短评信息

      一定要知道的爬虫协议

      4、使用Xpath解析豆瓣短评

      解析神器Xpath的安装及介绍

      Xpath的使用:浏览器复制和手写

      实战:用Xpath解析豆瓣短评信息

      5、使用pandas保存豆瓣短评数据

      pandas的基本用法介绍

      pandas文件保存、数据处理

      实战:使用pandas保存豆瓣短评数据

      6、浏览器抓包及headers设置(案例一:爬取知乎)

      爬虫的一般思路:抓取、解析、存储

      浏览器抓包获取Ajax加载的数据

      设置headers突破反爬虫限制

      实战:爬取知乎用户数据

      7、数据入库之MongoDB(案例二:爬取拉勾)

      MongoDB及RoboMongo的安装和使用

      设置等待时间和修改信息头

      实战:爬取拉勾职位数据

      将数据存储在MongoDB中

      补充实战:爬取微博移动端数据

      8、Selenium爬取动态网页(案例三:爬取淘宝)

      动态网页爬取神器Selenium搭建与使用

      分析淘宝商品页面动态信息

      实战:用Selenium爬取淘宝网页信息

      第二章:Python爬虫之Scrapy框架

      1、爬虫工程化及Scrapy框架初窥

      html、css、js、数据库、http协议、前后台联动

      爬虫进阶的工作流程

      Scrapy组件:引擎、调度器、下载中间件、项目管道等

      常用的爬虫工具:各种数据库、抓包工具等

      2、Scrapy安装及基本使用

      Scrapy安装

      Scrapy的基本方法和属性

      开始第一个Scrapy项目

      3、Scrapy选择器的用法

      常用选择器:css、xpath、re、pyquery

      css的使用方法

      xpath的使用方法

      re的使用方法

      pyquery的使用方法

      4、Scrapy的项目管道

      Item Pipeline的介绍和作用

      Item Pipeline的主要函数

      实战举例:将数据写入文件

      实战举例:在管道里过滤数据

      5、Scrapy的中间件

      下载中间件和蜘蛛中间件

      下载中间件的三大函数

      系统默认提供的中间件

      6、Scrapy的Request和Response详解

      Request对象基础参数和高级参数

      Request对象方法

      Response对象参数和方法

      Response对象方法的综合利用详解

      第三章:Python爬虫进阶操作

      1、网络进阶之谷歌浏览器抓包分析

      http请求详细分析

      网络面板结构

      过滤请求的关键字方法

      复制、保存和清除网络信息

      查看资源发起者和依赖关系

      2、数据入库之去重与数据库

      数据去重

      数据入库MongoDB

      第四章:分布式爬虫及实训项目

      1、大规模并发采集——分布式爬虫的编写

      分布式爬虫介绍

      Scrapy分布式爬取原理

      Scrapy-Redis的使用

      Scrapy分布式部署详解


      转载自: https://cloud.tencent.com/developer/article/1895399

      收起阅读 »

      什么是爬虫?怎么样玩爬虫

      看到上面的那只蜘蛛没?别误会,今天要教你如何玩上面的蜘蛛。我们正式从0到1轻松学会Python爬虫.......知识碎片化学习难度学习特点爬虫的概念网络爬虫(又被称为网页蜘蛛、网页机器人)就是模拟客户端(主要是指浏览器)发送请求,接收请求响应,按照一定规则、自...
      继续阅读 »

      Python爬虫入门:什么是爬虫?

      看到上面的那只蜘蛛没?别误会,今天要教你如何玩上面的蜘蛛。我们正式从0到1轻松学会Python爬虫.......

      爬虫特点概要

      • 知识碎片化

      爬虫方向的知识是十分碎片化的,因为我们写爬虫的时候会面对各种各样的网站,每个网站实现的技术都是相似的,但是大多数时候还是有差别的,这就要求我们对不同的网站使用不同的技术手段。爬虫并不像在学习web的时候要实现某一功能只要按照一定的套路就能做出来。

      • 学习难度

      爬虫的入门相对而言还是要比web简单,但是在后期,爬虫的难度要大于web。难点在于爬虫工程师与运维人员进行对抗,可能你写一个网站的爬虫,结果该网站的运维人员加了反爬的措施,那么作为爬虫工程师就要解决这个反爬。

      • 学习特点

      学习爬虫并不像学习web,学习web有一个完整的项目可以练手,因为爬虫的特点,也导致学习爬虫是以某网站为对象的,可以理解为一个技术点一个案例。

      爬虫的概念

      模拟浏览器,发送请求,获取响应

      网络爬虫(又被称为网页蜘蛛、网页机器人)就是模拟客户端(主要是指浏览器)发送请求,接收请求响应,按照一定规则、自动抓取互联网信息的程序。

      • 原则上,只要是浏览器能做的事情,爬虫都能做
      • 爬虫也只能获取浏览器所展示出来的数据

      在浏览器中输入百度网址,打开开发者工具,点击network,点击刷新,即可进行抓包。


      了解爬虫概念

      爬虫的作用

      爬虫在互联网中的作用

      • 数据采集
      • 软件测试
      • 网站投票
      • 网络安全

      爬虫的分类

      根据被爬网闸的数量不同,可以分为:

      • 通用爬虫,如搜索引擎
      • 聚焦爬虫,如12306抢票,或者专门抓取某一网站的某一类数据

      根据是否以获取数据为目的,可以分为:

      • 功能性爬虫,给你喜欢的明星,投票点赞
      • 数据增量式爬虫,比如招聘信息

      根据URL地址和对应页面内容是否改变,数据增量爬虫可以分为:

      • 基于URL地址变化,内容变化的增量式爬虫
      • URL地址不变,内容变化的数据增量式爬虫

      爬虫分类


      了解爬虫分类


      爬虫流程

      1、获取一个URL

      2、向URL发送请求,并获取响应(http协议)

      3、如果从响应中提取URL,则继续发送请求获取响应

      4、如果从响应中获取数据,则数据进行保存


      掌握爬虫流程


      http以及https的概念和区别

      在爬虫流程的第二步,向URL发送请求,那么就要依赖于HTTP/HTTPS协议。

      HTTPS比HTTP更安全,但是性能更低

      • HTTP:超文本传输协议,默认端口为80
        。超文本:是指超过文本,不限于文本,可以传输图片、视频、音频等数据
        。传输协议:是指使用公共约定的固定格式来传递转换成字符串的超文本内容
      • HTTPS:HTTP+SSL(安全套接字),即带有安全套接字层的超文本传输协议,默认端口443
        。SSL对传输内容(超文本,也就是请求头和响应体)进行加密
      • 可以打开一个浏览器访问URL,右键检查,点击network,选择一个URL,查看HTTP协议的形式。

      掌握http及https的概念和默认端口


      爬虫特别注意的请求头

      请求头与响应头

      http请求形式如上图所示,爬虫要特别关注以下几个请求头字段

      • Content-Type
      • Host
      • Connection
      • Upgrade-Insecure-Requests(升级为https请求)
      • User-Agent(用户代理)
      • Referer
      • Cookie(保持用户状态)
      • Authorization(认证信息)


      例如,使用浏览器访问百度进行抓包

      当我点击view source的时候,就会出现另外一种格式的请求头,这个是原始的版本,如果没有点击view source的请求头格式是经过浏览器优化的。

      爬虫特别注意的响应头

      • set-cookie

      cookie是基于服务端生成的,在客户端头信息中,在第一次把请求发送到服务端,服务端生成cookie,存放到客户端,下次发送请求时会带上cookie。

      常见的响应状态码

      • 200:成功
      • 302:跳转,新的URL在响应中的Location头中给出
      • 303:浏览器对于post响应进行重定向至新的URL
      • 307:浏览器对于get响应进行重定向至新的URL
      • 403:资源不可用,服务器理解客户端的请求,但拒绝处理它(没有权限)
      • 404:找不到页面
      • 500:服务器内部错误
      • 503:服务器由于维护或者负载过重未能应答。在响应中可能会携带Retry-After响应头,有可能是因为爬虫频繁访问URL,使服务器忽视爬虫的请求,最终返回503状态码

      所有的状态码都不可信,一切要以抓包得到的响应中获取的数据为准

      network中抓包得到的源码才是判断依据。element中的源码是渲染之后的源码,不能作为判断标准。


      了解常见的响应状态码


      http请求的过程

      1、浏览器在拿到域名对应的IP之后,先向地址栏中的URL发起请求,并获取响应。

      2、在返回响应内容(HTML)中,会带有CSS、JS、图片等URL地址,以及Ajax代码,浏览器按照响应内容中的顺序依次发送其他请求,并获取响应。

      3、浏览器每获取一个响应就对展示出的结果进行添加(加载),JS、CSS等内容会修改页面内容,JS也可以重新发送请求,获取响应。

      4、从获取第一个响应并在浏览器中展示,直到最终获取全部响应,并在展示结果中添加内容或修改,这个过程叫做浏览器的渲染

      注意

      在爬虫中,爬虫只会请求URL地址,对应的拿到URL地址对应的响应(该响应可以是HTML、CSS 、JS或是是图片、视频等等)。

      浏览器渲染出来的页面和爬虫请求抓取的页面很多时候是不一样的,原因是爬虫不具有渲染功能。

      • 浏览器最终展示的结果是由多次请求响应共同渲染的结果
      • 爬虫只对一个URL地址发起请求并得到响应

      理解浏览器展示的结果可以是多次请求响应共同渲染的结果,而爬虫是一次请求对应一个响应。

      转载自: https://cloud.tencent.com/developer/article/1895395

      收起阅读 »

      实时监控股市公告的Python爬虫

      精力有限的我们,如何更加有效率地监控信息? 很多时候特别是交易时,我们需要想办法监控一些信息,比如股市的公告。如果现有的软件没有办法实现我们的需求,那么就要靠我们自己动手,才能丰衣足食。你在交易看盘时,如果有一个小窗口,平时默默的不声不响,但是如果有公告发布...
      继续阅读 »

      精力有限的我们,如何更加有效率地监控信息?

      很多时候特别是交易时,我们需要想办法监控一些信息,比如股市的公告。如果现有的软件没有办法实现我们的需求,那么就要靠我们自己动手,才能丰衣足食。

      你在交易看盘时,如果有一个小窗口,平时默默的不声不响,但是如果有公告发布,就会显示公告的信息:这是什么公告,然后给我们公告的链接。这样,既不会像弹窗那样用信息轰炸我们,又能够定制我们自己想要的内容,做到想看就看,想不看就不看,那就很方便了。

      爬虫抓取的是东方财富上的上市公司公告,上市公司公告有些会在盘中公布。实时监控的原理,其实就是程序代替人工,定期地去刷新网页,然后用刷新前后得到的数据进行比对,如果一样,那么等待下一个周期继续刷新,如果不一样,那么就把增量信息提取出来,供我们查阅。

      利用python爬虫实时监控公告信息四部曲

      第一步,导入随机请求头和需要的包

      我们使用json来解析获取的信息,使用什么方法解析数据取决于我们请求数据的返回形式,这里使用json最方便,我们就导入json包。

      第二步,获取初始的公告数据

      我们发现,每一个公告都有一个独有的文章号码:art_code,因此我们以这个号码作为新旧比较的基准,如果新页面的头一个公告的art_code和已有的一致,那么就进入下一个刷新周期,如果不一致,那么说明页面已经更新过了,我们提取最新的报告,同时更新这个art_code,用于下一次比对。

      原始url的获取。获取之后,通过json解析其中的内容,得到art_code,覆盖写入在tmp.txt文件中,用于比对。

      读取了tmp.txt文件中的art_code,跟页面解析的art_code比对。

      第三步,获取公告标题和文章链接

      通过json我们基本上已经能够解析出大部分的数据内容。

      通过观察网站的公告链接的特点,我们发现主要的差别就是在art_code,因此通过网址链接的拼接,我们就能够得到公告的pdf链接。

      第四步,运行我们的程序

      程序运行的结果会打印到窗口当中,每当有新的公告发布,程序上就会出现一串新的信息。

      总结

      自此,我们通过程序把我们要的信息打印到了程序的运行窗口,同时,我们的程序也可以根据我们需求进行强化和扩充。首先,这些信息也可以非常方便的通过接口发送到邮箱、钉钉等平台,起到实时提醒的作用,其次,我们也可以从不同的地方抓取信息,完成所需信息的自定义整合,这些将在我们后续的文章中提到。

      收起阅读 »

      这可能是掘金讲「原型链」,讲的最好最通俗易懂的了,附练习题!

      前言 大家好,我是林三心,相信大家都听过前端的三座大山:闭包,原型链,作用域,这三个其实都只是算基础。而我一直觉得基础是进阶的前提,所以不能因为是基础就忽视他们。今天我就以我的方式讲讲原型链吧,希望大家能牢固地掌握原型链知识 很多文章一上来就扔这个图,但是我不...
      继续阅读 »


      前言


      大家好,我是林三心,相信大家都听过前端的三座大山:闭包,原型链,作用域,这三个其实都只是算基础。而我一直觉得基础是进阶的前提,所以不能因为是基础就忽视他们。今天我就以我的方式讲讲原型链吧,希望大家能牢固地掌握原型链知识


      很多文章一上来就扔这个图,但是我不喜欢这样,我觉得这样对基础不好的同学很不好,我喜欢带领大家去从零实现这个图,在实现的过程中,不断地掌握原型链的所有知识!!!来吧!!!跟着我从零实现吧!!!跟着我驯服原型链吧!!!


      截屏2021-09-13 下午9.58.41.png


      prototype和__proto__


      是啥


      这两个东西到底是啥呢?


      • prototype: 显式原型
      • __ proto__: 隐式原型

      有什么关系


      那么这两个都叫原型,那他们两到底啥关系呢?


      一般,构造函数的prototype和其实例的__proto__是指向同一个地方的,这个地方就叫做原型对象


      那什么是构造函数呢?俗话说就是,可以用来new的函数就叫构造函数,箭头函数不能用来当做构造函数哦


      function Person(name, age) { // 这个就是构造函数
      this.name = name
      this.age = age
      }

      const person1 = new Person('小明', 20) // 这个是Person构造函数的实例
      const person2 = new Person('小红', 30) // 这个也是Person构造函数的实例

      构造函数的prototype和其实例的__proto__是指向同一个地方的,咱们可以来验证一下


      function Person(name, age) {
      this.name = name
      this.age = age
      }
      Person.prototype.sayName = function() {
      console.log(this.name)
      }
      console.log(Person.prototype) // { sayName: [Function] }

      const person1 = new Person('小明', 20)
      console.log(person1.__proto__) // { sayName: [Function] }

      const person2 = new Person('小红', 30)
      console.log(person2.__proto__) // { sayName: [Function] }

      console.log(Person.prototype === person1.__proto__) // true
      console.log(Person.prototype === person2.__proto__) // true

      截屏2021-09-12 下午9.23.35.png


      函数


      咱们上面提到了构造函数,其实他说到底也是个函数,其实咱们平时定义函数,无非有以下几种


      function fn1(name, age) {
      console.log(`我是${name}, 我今年${age}岁`)
      }
      fn1('林三心', 10) // 我是林三心, 我今年10岁

      const fn2 = function(name, age){
      console.log(`我是${name}, 我今年${age}岁`)
      }
      fn2('林三心', 10) // 我是林三心, 我今年10岁

      const arrowFn = (name, age) => {
      console.log(`我是${name}, 我今年${age}岁`)
      }
      arrowFn('林三心', 10) // 我是林三心, 我今年10岁

      其实这几种的本质都是一样的(只考虑函数的声明),都可以使用new Function来声明,是的没错Function也是一个构造函数。上面的写法等同于下面的写法


      const fn1 = new Function('name', 'age', 'console.log(`我是${name}, 我今年${age}岁`)')
      fn1('林三心', 10) // 我是林三心, 我今年10岁

      const fn2 = new Function('name', 'age', 'console.log(`我是${name}, 我今年${age}岁`)')
      fn2('林三心', 10) // 我是林三心, 我今年10岁

      const arrowFn = new Function('name', 'age', 'console.log(`我是${name}, 我今年${age}岁`)')
      arrowFn('林三心', 10) // 我是林三心, 我今年10岁

      截屏2021-09-12 下午9.17.42.png


      我们之前说过,构造函数prototype和其实例__proto__是指向同一个地方的,这里的fn1,fn2,arrowFn其实也都是Function构造函数的实例,那我们来验证一下吧


      function fn1(name, age) {
      console.log(`我是${name}, 我今年${age}岁`)
      }

      const fn2 = function(name, age){
      console.log(`我是${name}, 我今年${age}岁`)
      }

      const arrowFn = (name, age) => {
      console.log(`我是${name}, 我今年${age}岁`)
      }

      console.log(Function.prototype === fn1.__proto__) // true
      console.log(Function.prototype === fn2.__proto__) // true
      console.log(Function.prototype === arrowFn.__proto__) // true

      截屏2021-09-12 下午9.29.00.png


      对象


      咱们平常开发中,创建一个对象,通常会用以下几种方法。


      • 构造函数创建对象,他创建出来的对象都是此Function构造函数的实例,所以这里不讨论它
      • 字面量创建对象
      • new Object创建对象
      • Object.create创建对象,创建出来的是一个空原型的对象,这里不讨论它

      // 第一种:构造函数创建对象
      function Person(name, age) {
      this.name = name
      this.age = age
      }
      const person1 = new Person('林三心', 10)
      console.log(person1) // Person { name: '林三心', age: 10 }

      // 第二种:字面量创建对象
      const person2 = {name: '林三心', age: 10}
      console.log(person2) // { name: '林三心', age: 10 }

      // 第三种:new Object创建对象
      const person3 = new Object()
      person3.name = '林三心'
      person3.age = 10
      console.log(person3) // { name: '林三心', age: 10 }

      // 第四种:Object.create创建对象
      const person4 = Object.create({})
      person4.name = '林三心'
      person4.age = 10
      console.log(person4) // { name: '林三心', age: 10 }

      咱们来看看字面量创建对象new Object创建对象两种方式,其实字面量创建对象的本质就是new Object创建对象


      // 字面量创建对象
      const person2 = {name: '林三心', age: 10}
      console.log(person2) // { name: '林三心', age: 10 }

      本质是

      // new Object创建对象
      const person2 = new Object()
      person2.name = '林三心'
      person2.age = 10
      console.log(person2) // { name: '林三心', age: 10 }

      截屏2021-09-12 下午9.52.47.png


      我们之前说过,构造函数prototype和其实例__proto__是指向同一个地方的,这里的person2,person3其实也都是Object构造函数的实例,那我们来验证一下吧


      const person2 = {name: '林三心', age: 10}

      const person3 = new Object()
      person3.name = '林三心'
      person3.age = 10

      console.log(Object.prototype === person2.__proto__) // true
      console.log(Object.prototype === person3.__proto__) // true

      截屏2021-09-12 下午9.58.31.png


      Function和Object


      上面咱们常说


      • 函数Function构造函数的实例
      • 对象Object构造函数的实例

      Function构造函数Object构造函数他们两个又是谁的实例呢?


      • function Object()其实也是个函数,所以他是Function构造函数的实例
      • function Function()其实也是个函数,所以他也是Function构造函数的实例,没错,他是他自己本身的实例

      咱们可以试验一下就知道了


      console.log(Function.prototype === Object.__proto__) // true
      console.log(Function.prototype === Function.__proto__) // true

      截屏2021-09-12 下午10.12.40.png


      constructor


      constructor和prototype是成对的,你指向我,我指向你。举个例子,如果你是我老婆,那我肯定是你的老公。


      function fn() {}

      console.log(fn.prototype) // {constructor: fn}
      console.log(fn.prototype.constructor === fn) // true

      截屏2021-09-12 下午10.35.40.png


      原型链


      Person.prototype 和 Function.prototype


      讨论原型链之前,咱们先来聊聊这两个东西


      • Person.prototype,它是构造函数Person的原型对象
      • Function.prototype,他是构造函数Function的原型对象

      都说了原型对象,原型对象,可以知道其实这两个本质都是对象


      那既然是对象,本质肯定都是通过new Object()来创建的。既然是通过new Object()创建的,那就说明Person.prototype 和 Function.prototype都是构造函数Object的实例。也就说明了Person.prototype 和 Function.prototype他们两的__proto__都指向Object.prototype


      咱们可以验证一下


      function Person(){}

      console.log(Person.prototype.__proto__ === Object.prototype) // true
      console.log(Function.prototype.__proto__ === Object.prototype) // true

      截屏2021-09-12 下午10.46.41.png


      什么是原型链?


      什么是原型链呢?其实俗话说就是:__proto__的路径就叫原型链


      截屏2021-09-12 下午10.55.48.png


      原型链终点


      上面咱们看到,三条原型链结尾都是Object.prototype,那是不是说明了Object.prototype就是原型链的终点呢?其实不是的,Object.prototype其实也有__proto__,指向null,那才是原型链的终点


      至此,整个原型示意图就画完啦!!!


      截屏2021-09-13 下午9.56.10.png


      原型继承


      说到原型,就不得不说补充一下原型继承这个知识点了,原型继承就是,实例可以使用构造函数上的prototype中的方法


      function Person(name) { // 构造函数
      this.name = name
      }
      Person.prototype.sayName = function() { // 往原型对象添加方法
      console.log(this.name)
      }


      const person = new Person('林三心') // 实例
      // 使用构造函数的prototype中的方法
      person.sayName() // 林三心

      截屏2021-09-12 下午11.10.41.png


      instanceof


      使用方法


      A instanceof B

      作用:判断B的prototype是否在A的原型链上


      例子


      function Person(name) { // 构造函数
      this.name = name
      }

      const person = new Person('林三心') // 实例

      console.log(Person instanceof Function) // true
      console.log(Person instanceof Object) // true
      console.log(person instanceof Person) // true
      console.log(person instanceof Object) // true

      练习题


      练习题只为了大家能巩固本文章的知识


      第一题


      var F = function() {};

      Object.prototype.a = function() {
      console.log('a');
      };

      Function.prototype.b = function() {
      console.log('b');
      }

      var f = new F();

      f.a();
      f.b();

      F.a();
      F.b();

      答案


      f.a(); // a
      f.b(); // f.b is not a function

      F.a(); // a
      F.b(); // b

      第二题


      var A = function() {};
      A.prototype.n = 1;
      var b = new A();
      A.prototype = {
      n: 2,
      m: 3
      }
      var c = new A();

      console.log(b.n);
      console.log(b.m);

      console.log(c.n);
      console.log(c.m);

      答案


      console.log(b.n); // 1
      console.log(b.m); // undefined

      console.log(c.n); // 2
      console.log(c.m); // 3

      第三题


      var foo = {},
      F = function(){};
      Object.prototype.a = 'value a';
      Function.prototype.b = 'value b';

      console.log(foo.a);
      console.log(foo.b);

      console.log(F.a);
      console.log(F.b);

      答案


      console.log(foo.a); // value a
      console.log(foo.b); // undefined

      console.log(F.a); // value a
      console.log(F.b); // value b

      第四题


      function A() {}
      function B(a) {
      this.a = a;
      }
      function C(a) {
      if (a) {
      this.a = a;
      }
      }
      A.prototype.a = 1;
      B.prototype.a = 1;
      C.prototype.a = 1;

      console.log(new A().a);
      console.log(new B().a);
      console.log(new C(2).a);

      答案


      console.log(new A().a); // 1
      console.log(new B().a); // undefined
      console.log(new C(2).a); // 2

      第五题


      console.log(123['toString'].length + 123)

      答案:123是数字,数字本质是new Number(),数字本身没有toString方法,则沿着__proto__function Number()prototype上找,找到toString方法,toString方法的length是1,1 + 123 = 124,至于为什么length是1,可以看95%的人都回答不上来的问题:函数的length是多少?


      console.log(123['toString'].length + 123) // 124

      结语



      如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下

      收起阅读 »

      7张图,20分钟就能搞定的async/await原理!为什么要拖那么久?

      前言 大家好,我是林三心,以最通俗的话,讲最难的知识点是我写文章的宗旨 之前我发过一篇手写Promise原理,最通俗易懂的版本!!!,带大家基本了解了Promise内部的实现原理,而提到Promise,就不得不提一个东西,那就是async/await,asyn...
      继续阅读 »


      前言


      大家好,我是林三心,以最通俗的话,讲最难的知识点是我写文章的宗旨


      之前我发过一篇手写Promise原理,最通俗易懂的版本!!!,带大家基本了解了Promise内部的实现原理,而提到Promise,就不得不提一个东西,那就是async/awaitasync/await是一个很重要的语法糖,他的作用是用同步的方式,执行异步操作。那么今天我就带大家一起实现一下async/await吧!!!


      async/await用法


      其实你要实现一个东西之前,最好是先搞清楚这两样东西


      • 这个东西有什么用?
      • 这个东西是怎么用的?

      有什么用?


      async/await的用处就是:用同步方式,执行异步操作,怎么说呢?举个例子


      比如我现在有一个需求:先请求完接口1,再去请求接口2,我们通常会这么做


      function request(num) { // 模拟接口请求
      return new Promise(resolve => {
      setTimeout(() => {
      resolve(num * 2)
      }, 1000)
      })
      }

      request(1).then(res1 => {
      console.log(res1) // 1秒后 输出 2

      request(2).then(res2 => {
      console.log(res2) // 2秒后 输出 4
      })
      })

      或者我现在又有一个需求:先请求完接口1,再拿接口1返回的数据,去当做接口2的请求参数,那我们也可以这么做


      request(5).then(res1 => {
      console.log(res1) // 1秒后 输出 10

      request(res1).then(res2 => {
      console.log(res2) // 2秒后 输出 20
      })
      })

      其实这么做是没问题的,但是如果嵌套的多了,不免有点不雅观,这个时候就可以用async/await来解决了


      async function fn () {
      const res1 = await request(5)
      const res2 = await request(res1)
      console.log(res2) // 2秒后输出 20
      }
      fn()

      是怎么用?


      还是用刚刚的例子


      需求一:


      async function fn () {
      await request(1)
      await request(2)
      // 2秒后执行完
      }
      fn()

      需求二:


      async function fn () {
      const res1 = await request(5)
      const res2 = await request(res1)
      console.log(res2) // 2秒后输出 20
      }
      fn()

      截屏2021-09-11 下午9.57.58.png


      其实就类似于生活中的排队,咱们生活中排队买东西,肯定是要上一个人买完,才轮到下一个人。而上面也一样,在async函数中,await规定了异步操作只能一个一个排队执行,从而达到用同步方式,执行异步操作的效果,这里注意了:await只能在async函数中使用,不然会报错哦


      刚刚上面的例子await后面都是跟着异步操作Promise,那如果不接Promise会怎么样呢?


      function request(num) { // 去掉Promise
      setTimeout(() => {
      console.log(num * 2)
      }, 1000)
      }

      async function fn() {
      await request(1) // 2
      await request(2) // 4
      // 1秒后执行完 同时输出
      }
      fn()

      可以看出,如果await后面接的不是Promise的话,有可能其实是达不到排队的效果的


      说完await,咱们聊聊async吧,async是一个位于function之前的前缀,只有async函数中,才能使用await。那async执行完是返回一个什么东西呢?


      async function fn () {}
      console.log(fn) // [AsyncFunction: fn]
      console.log(fn()) // Promise {<fulfilled>: undefined}

      可以看出,async函数执行完会自动返回一个状态为fulfilled的Promise,也就是成功状态,但是值却是undefined,那要怎么才能使值不是undefined呢?很简单,函数有return返回值就行了


      async function fn (num) {
      return num
      }
      console.log(fn) // [AsyncFunction: fn]
      console.log(fn(10)) // Promise {<fulfilled>: 10}
      fn(10).then(res => console.log(res)) // 10

      可以看出,此时就有值了,并且还能使用then方法进行输出


      总结


      总结一下async/await的知识点


      • await只能在async函数中使用,不然会报错
      • async函数返回的是一个Promise对象,有无值看有无return值
      • await后面最好是接Promise,虽然接其他值也能达到排队效果
      • async/await作用是用同步方式,执行异步操作

      什么是语法糖?


      前面说了,async/await是一种语法糖,诶!好多同学就会问,啥是语法糖呢?我个人理解就是,语法糖就是一个东西,这个东西你就算不用他,你用其他手段也能达到这个东西同样的效果,但是可能就没有这个东西这么方便了。


      • 举个生活中的例子吧:你走路也能走到北京,但是你坐飞机会更快到北京。
      • 举个代码中的例子吧:ES6的class也是语法糖,因为其实用普通function也能实现同样效果

      回归正题,async/await是一种语法糖,那就说明用其他方式其实也可以实现他的效果,我们今天就是讲一讲怎么去实现async/await,用到的是ES6里的迭代函数——generator函数


      generator函数


      基本用法


      generator函数跟普通函数在写法上的区别就是,多了一个星号*,并且只有在generator函数中才能使用yield,什么是yield呢,他相当于generator函数执行的中途暂停点,比如下方有3个暂停点。而怎么才能暂停后继续走呢?那就得使用到next方法next方法执行后会返回一个对象,对象中有value 和 done两个属性


      • value:暂停点后面接的值,也就是yield后面接的值
      • done:是否generator函数已走完,没走完为false,走完为true

      function* gen() {
      yield 1
      yield 2
      yield 3
      }
      const g = gen()
      console.log(g.next()) // { value: 1, done: false }
      console.log(g.next()) // { value: 2, done: false }
      console.log(g.next()) // { value: 3, done: false }
      console.log(g.next()) // { value: undefined, done: true }

      可以看到最后一个是undefined,这取决于你generator函数有无返回值


      function* gen() {
      yield 1
      yield 2
      yield 3
      return 4
      }
      const g = gen()
      console.log(g.next()) // { value: 1, done: false }
      console.log(g.next()) // { value: 2, done: false }
      console.log(g.next()) // { value: 3, done: false }
      console.log(g.next()) // { value: 4, done: true }

      截屏2021-09-11 下午9.46.17.png


      yield后面接函数


      yield后面接函数的话,到了对应暂停点yield,会马上执行此函数,并且该函数的执行返回值,会被当做此暂停点对象的value


      function fn(num) {
      console.log(num)
      return num
      }
      function* gen() {
      yield fn(1)
      yield fn(2)
      return 3
      }
      const g = gen()
      console.log(g.next())
      // 1
      // { value: 1, done: false }
      console.log(g.next())
      // 2
      // { value: 2, done: false }
      console.log(g.next())
      // { value: 3, done: true }

      yield后面接Promise


      前面说了,函数执行返回值会当做暂停点对象的value值,那么下面例子就可以理解了,前两个的value都是pending状态的Promise对象


      function fn(num) {
      return new Promise(resolve => {
      setTimeout(() => {
      resolve(num)
      }, 1000)
      })
      }
      function* gen() {
      yield fn(1)
      yield fn(2)
      return 3
      }
      const g = gen()
      console.log(g.next()) // { value: Promise { <pending> }, done: false }
      console.log(g.next()) // { value: Promise { <pending> }, done: false }
      console.log(g.next()) // { value: 3, done: true }

      截屏2021-09-11 下午10.51.38.png


      其实我们想要的结果是,两个Promise的结果1 和 2,那怎么做呢?很简单,使用Promise的then方法就行了


      const g = gen()
      const next1 = g.next()
      next1.value.then(res1 => {
      console.log(next1) // 1秒后输出 { value: Promise { 1 }, done: false }
      console.log(res1) // 1秒后输出 1

      const next2 = g.next()
      next2.value.then(res2 => {
      console.log(next2) // 2秒后输出 { value: Promise { 2 }, done: false }
      console.log(res2) // 2秒后输出 2
      console.log(g.next()) // 2秒后输出 { value: 3, done: true }
      })
      })

      截屏2021-09-11 下午10.38.37.png


      next函数传参


      generator函数可以用next方法来传参,并且可以通过yield来接收这个参数,注意两点


      • 第一次next传参是没用的,只有从第二次开始next传参才有用
      • next传值时,要记住顺序是,先右边yield,后左边接收参数

      function* gen() {
      const num1 = yield 1
      console.log(num1)
      const num2 = yield 2
      console.log(num2)
      return 3
      }
      const g = gen()
      console.log(g.next()) // { value: 1, done: false }
      console.log(g.next(11111))
      // 11111
      // { value: 2, done: false }
      console.log(g.next(22222))
      // 22222
      // { value: 3, done: true }

      截屏2021-09-11 下午10.53.02.png


      Promise+next传参


      前面讲了


      • yield后面接Promise
      • next函数传参

      那这两个组合起来会是什么样呢?


      function fn(nums) {
      return new Promise(resolve => {
      setTimeout(() => {
      resolve(nums * 2)
      }, 1000)
      })
      }
      function* gen() {
      const num1 = yield fn(1)
      const num2 = yield fn(num1)
      const num3 = yield fn(num2)
      return num3
      }
      const g = gen()
      const next1 = g.next()
      next1.value.then(res1 => {
      console.log(next1) // 1秒后同时输出 { value: Promise { 2 }, done: false }
      console.log(res1) // 1秒后同时输出 2

      const next2 = g.next(res1) // 传入上次的res1
      next2.value.then(res2 => {
      console.log(next2) // 2秒后同时输出 { value: Promise { 4 }, done: false }
      console.log(res2) // 2秒后同时输出 4

      const next3 = g.next(res2) // 传入上次的res2
      next3.value.then(res3 => {
      console.log(next3) // 3秒后同时输出 { value: Promise { 8 }, done: false }
      console.log(res3) // 3秒后同时输出 8

      // 传入上次的res3
      console.log(g.next(res3)) // 3秒后同时输出 { value: 8, done: true }
      })
      })
      })

      截屏2021-09-11 下午11.05.44.png


      实现async/await


      其实上方的generator函数Promise+next传参,就很像async/await了,区别在于


      • gen函数执行返回值不是Promise,asyncFn执行返回值是Promise
      • gen函数需要执行相应的操作,才能等同于asyncFn的排队效果
      • gen函数执行的操作是不完善的,因为并不确定有几个yield,不确定会嵌套几次

      截屏2021-09-11 下午11.53.41.png


      那我们怎么办呢?我们可以封装一个高阶函数。什么是高阶函数呢?高阶函数的特点是:参数是函数,返回值也可以是函数。下方的highorderFn就是一个高阶函数


      function highorderFn(函数) {
      // 一系列处理

      return 函数
      }

      我们可以封装一个高阶函数,接收一个generator函数,并经过一系列处理,返回一个具有async函数功能的函数


      function generatorToAsync(generatorFn) {
      // 经过一系列处理

      return 具有async函数功能的函数
      }

      返回值Promise


      之前我们说到,async函数的执行返回值是一个Promise,那我们要怎么实现相同的结果呢


      function* gen() {

      }

      const asyncFn = generatorToAsync(gen)

      console.log(asyncFn()) // 期望这里输出 Promise

      其实很简单,generatorToAsync函数里做一下处理就行了


      function* gen() {

      }
      function generatorToAsync (generatorFn) {
      return function () {
      return new Promise((resolve, reject) => {

      })
      }
      }

      const asyncFn = generatorToAsync(gen)

      console.log(asyncFn()) // Promise

      加入一系列操作


      咱们把之前的处理代码,加入generatorToAsync函数


      function fn(nums) {
      return new Promise(resolve => {
      setTimeout(() => {
      resolve(nums * 2)
      }, 1000)
      })
      }
      function* gen() {
      const num1 = yield fn(1)
      const num2 = yield fn(num1)
      const num3 = yield fn(num2)
      return num3
      }
      function generatorToAsync(generatorFn) {
      return function () {
      return new Promise((resolve, reject) => {
      const g = generatorFn()
      const next1 = g.next()
      next1.value.then(res1 => {

      const next2 = g.next(res1) // 传入上次的res1
      next2.value.then(res2 => {

      const next3 = g.next(res2) // 传入上次的res2
      next3.value.then(res3 => {

      // 传入上次的res3
      resolve(g.next(res3).value)
      })
      })
      })
      })
      }
      }

      const asyncFn = generatorToAsync(gen)

      asyncFn().then(res => console.log(res)) // 3秒后输出 8

      可以发现,咱们其实已经实现了以下的async/await的结果了


      async function asyncFn() {
      const num1 = await fn(1)
      const num2 = await fn(num1)
      const num3 = await fn(num2)
      return num3
      }
      asyncFn().then(res => console.log(res)) // 3秒后输出 8

      完善代码


      上面的代码其实都是死代码,因为一个async函数中可能有2个await,3个await,5个await
      ,其实await的个数是不确定的。同样类比,generator函数中,也可能有2个yield,3个yield,5个yield,所以咱们得把代码写成活的才行


      function generatorToAsync(generatorFn) {
      return function() {
      const gen = generatorFn.apply(this, arguments) // gen有可能传参

      // 返回一个Promise
      return new Promise((resolve, reject) => {

      function go(key, arg) {
      let res
      try {
      res = gen[key](arg) // 这里有可能会执行返回reject状态的Promise
      } catch (error) {
      return reject(error) // 报错的话会走catch,直接reject
      }

      // 解构获得value和done
      const { value, done } = res
      if (done) {
      // 如果done为true,说明走完了,进行resolve(value)
      return resolve(value)
      } else {
      // 如果done为false,说明没走完,还得继续走

      // value有可能是:常量,Promise,Promise有可能是成功或者失败
      return Promise.resolve(value).then(val => go('next', val), err => go('throw', err))
      }
      }

      go("next") // 第一次执行
      })
      }
      }

      const asyncFn = generatorToAsync(gen)

      asyncFn().then(res => console.log(res))

      这样的话,无论是多少个yield都会排队执行了,咱们把代码写成活的了


      示例


      async/await版本


      async function asyncFn() {
      const num1 = await fn(1)
      console.log(num1) // 2
      const num2 = await fn(num1)
      console.log(num2) // 4
      const num3 = await fn(num2)
      console.log(num3) // 8
      return num3
      }
      const asyncRes = asyncFn()
      console.log(asyncRes) // Promise
      asyncRes.then(res => console.log(res)) // 8

      使用generatorToAsync函数的版本


      function* gen() {
      const num1 = yield fn(1)
      console.log(num1) // 2
      const num2 = yield fn(num1)
      console.log(num2) // 4
      const num3 = yield fn(num2)
      console.log(num3) // 8
      return num3
      }

      const genToAsync = generatorToAsync(gen)
      const asyncRes = genToAsync()
      console.log(asyncRes) // Promise
      asyncRes.then(res => console.log(res)) // 8

      结语


      如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。


      如果你想一起学习前端或者摸鱼,那你可以加我,加入我的摸鱼学习群,点击这里 ---> 摸鱼沸点


      如果你是有其他目的的,别加我,我不想跟你交朋友,我只想简简单单学习前端,不想搞一些有的没的!!

       
      收起阅读 »

      知其然,而知其所以然,JS 对象创建与继承【汇总梳理】

        这些文章是: 蓦然回首,“工厂、构造、原型”设计模式,正在灯火阑珊处JS精粹,原型链继承和构造函数继承的 “毛病”“工厂、构造、原型” 设计模式与 JS 继承JS 高级程序设计 4:class 继承的重点JS class 并不只是简单的语法糖! ...
      继续阅读 »
       

      这些文章是:



      本篇作为汇总篇,来一探究竟!!冲冲冲


      image.png


      对象创建


      不难发现,每一篇都离不开工厂、构造、原型这 3 种设计模式中的至少其一!


      让人不禁想问:JS 为什么非要用到这种 3 种设计模式了呢??


      正本溯源,先从对象创建讲起:


      我们本来习惯这样声明对象(不用任何设计模式)


      let car= {
      price:100,
      color:"white",
      run:()=>{console.log("run fast")}
      }

      当有两个或多个这样的对象需要声明时,是不可能一直复制写下去的:


      let car1 = {
      price:100,
      color:"white",
      run:()=>{console.log("run fast")}
      }

      let car2 = {
      price:200,
      color:"balck",
      run:()=>{console.log("run slow")}
      }

      let car3 = {
      price:300,
      color:"red",
      run:()=>{console.log("broken")}
      }

      这样写:


      1. 写起来麻烦,重复的代码量大;
      2. 不利于修改,比如当 car 对象要增删改一个属性,需要多处进行增删改;

      工厂函数


      肯定是要封装啦,第一个反应,可以 借助函数 来帮助我们批量创建对象~


      于是乎:


      function makeCar(price,color,performance){
      let obj = {}
      obj.price = price
      obj.color= color
      obj.run = ()=>{console.log(performance)}
      return obj
      }

      let car1= makeCar("100","white","run fast")
      let car2= makeCar("200","black","run slow")
      let car3= makeCar("300","red","broken")

      这就是工厂设计模式在 JS 创建对象时应用的由来~


      到这里,对于【对象创建】来说,应该够用了吧?是,在不考虑扩展的情况下,基本够用了。


      但这个时候来个新需求,需要创建 car4、car5、car6 对象,它们要在原有基础上再新增一个 brand 属性,会怎么写?


      第一反应,直接修改 makeCar


      function makeCar(price,color,performance,brand){
      let obj = {}
      obj.price = price
      obj.color= color
      obj.run = ()=>{console.log(performance)}
      obj.brand = brand
      return obj
      }

      let car4= makeCar("400","white","run fast","benz")
      let car5= makeCar("500","black","run slow","audi")
      let car6= makeCar("600","red","broken","tsl")

      这样写,不行,会影响原有的 car1、car2、car3 对象;


      那再重新写一个 makeCarChild 工厂函数行不行?


      function makeCarChild (price,color,performance,brand){
      let obj = {}
      obj.price = price
      obj.color= color
      obj.run = ()=>{console.log(performance)}
      obj.brand = brand
      return obj
      }

      let car4= makeCarChild("400","white","run fast","benz")
      let car5= makeCarChild("500","black","run slow","audi")
      let car6= makeCarChild("600","red","broken","tsl")

      行是行,就是太麻烦,全量复制之前的属性,建立 N 个相像的工厂,显得太蠢了。。。


      image.png


      构造函数


      于是乎,在工厂设计模式上,发展出了:构造函数设计模式,来解决以上复用(也就是继承)的问题。


      function MakeCar(price,color,performance){
      this.price = price
      this.color= color
      this.run = ()=>{console.log(performance)}
      }

      function MakeCarChild(brand,...args){
      MakeCar.call(this,...args)
      this.brand = brand
      }

      let car4= new MakeCarChild("benz","400","white","run fast")
      let car5= new MakeCarChild("audi","500","black","run slow")
      let car6= new MakeCarChild("tsl","600","red","broken")

      构造函数区别于工厂函数:


      • 函数名首字母通常大写;
      • 创建对象的时候要用到 new 关键字(new 的过程这里不再赘述了,之前文章有);
      • 函数没有 return,而是通过 this 绑定来实现寻找属性的;

      到此为止,工厂函数的复用也解决了。


      构造+原型


      新的问题在于,我们不能通过查找原型链从 MakeCarChild 找到 MakeCar


      car4.__proto__===MakeCarChild.prototype // true

      MakeCarChild.prototype.__proto__ === MakeCar.prototype // false
      MakeCarChild.__proto__ === MakeCar.prototype // false

      无论在原型链上怎么找,都无法从 MakeCarChild 找到 MakeCar


      这就意味着:子类不能继承父类原型上的属性



      这里提个思考问题:为什么“要从原型链查找到”很重要?为什么“子类要继承父类原型上的属性”?就靠 this 绑定来找不行吗?



      image.png


      于是乎,构造函数设计模式 + 原型设计模式 的 【组合继承】应运而生


      function MakeCar(price,color,performance){
      this.price = price
      this.color= color
      this.run = ()=>{console.log(performance)}
      }

      function MakeCarChild(brand,...args){
      MakeCar.call(this,...args)
      this.brand = brand
      }

      MakeCarChild.prototype = new MakeCar() // 原型继承父类的构造器

      MakeCarChild.prototype.constructor = MakeCarChild // 重置 constructor

      let car4= new MakeCarChild("benz","400","white","run fast")

      现在再找原型,就找的到啦:


      car4.__proto__ === MakeCarChild.prototype // true
      MakeCarChild.prototype.__proto__ === MakeCar.prototype // true

      其实,能到这里,就已经很很优秀了,该有的都有了,写法也不算是很复杂。


      工厂+构造+原型


      但,总有人在追求极致。


      image.png


      上述的组合继承,父类构造函数被调用了两次,一次是 call 的过程,一次是原型继承 new 的过程,如果每次实例化,都重复调用,肯定是不可取的,怎样避免?


      工厂 + 构造 + 原型 = 寄生组合继承 应运而生


      核心是,通过工厂函数新建一个中间商 F( ),复制了一份父类的原型对象,再赋给子类的原型;


      function object(o) { // 工厂函数
      function F() {}
      F.prototype = o;
      return new F(); // new 一个空的函数,所占内存很小
      }

      function inherit(child, parent) { // 原型继承
      var prototype = object(parent.prototype)
      prototype.constructor = child
      child.prototype = prototype
      }


      function MakeCar(price,color,performance){
      this.price = price
      this.color= color
      this.run = ()=>{console.log(performance)}
      }

      function MakeCarChild(brand,...args){ // 构造函数
      MakeCar.call(this,...args)
      this.brand = brand
      }

      inherit(MakeCarChild,MakeCar)

      let car4= new MakeCarChild("benz","400","white","run fast")

      car4.__proto__ === MakeCarChild.prototype // true

      MakeCarChild.prototype.__proto__ === MakeCar.prototype // true

      ES6 class


      再到后来,ES6 的 class 作为寄生组合继承的语法糖:


      class MakeCar {
      constructor(price,color,performance){
      this.price = price
      this.color= color
      this.performance=performance
      }
      run(){
      console.log(console.log(this.performance))
      }
      }

      class MakeCarChild extends MakeCar{
      constructor(brand,...args){
      super(brand,...args);
      this.brand= brand;
      }
      }

      let car4= new MakeCarChild("benz","400","white","run fast")

      car4.__proto__ === MakeCarChild.prototype // true

      MakeCarChild.prototype.__proto__ === MakeCar.prototype // true

      有兴趣的工友,可以看下 ES6 解析成 ES5 的代码:原型与原型链 - ES6 Class的底层实现原理 #22


      对象与函数


      最后本瓜想再谈谈关于 JS 对象和函数的关系:


      image.png


      即使是这样声明一个对象,let obj = {} ,它一样是由构造函数 Object 构造而来的:


      let obj = {} 

      obj.__proto__ === Object.prototype // true


      在 JS 中,万物皆对象,对象都是有函数构造而来,函数本身也是对象。



      对应代码中的意思:


      1. 所有的构造函数的隐式原型都等于 Function 的显示原型,函数都是由 Function 构造而来,Object 构造函数也不例外;
      2. 所有构造函数的显示原型的隐式原型,都等于 Object 的显示原型,Function 也不例外;

      // 1.
      Object.__proto__ === Function.prototype // true

      // 2.
      Function.prototype.__proto__ === Object.prototype // true

      这个设计真的就一个大无语,大纠结,大麻烦。。。


      image.png


      只能先按之前提过的歪理解记着先:Function 就是上帝,上帝创造了万物;Object 就是万物。万物由上帝创造(对象由函数构造而来),上帝本身也属于一种物质(函数本身却也是对象);


      对于本篇来说,继承,其实都是父子构造函数在继承,然后再由构造函数实例化对象,以此来实现对象的继承。


      到底是谁在继承?函数?对象?都是吧~~




      小结


      本篇由创建对象说起,讲了工厂函数,它可以做一层最基本的封装;


      再到,对工厂的拓展,演进为构造函数;


      再基于原型特点,构造+原型,得出组合继承;


      再追求极致,讲到寄生组合;


      再讲到简化书写的 Es6 class ;


      以及最后对对象与函数的思考。


      就先到这吧~~

       
      收起阅读 »

      CSS揭秘之性能优化技巧篇

      CSS揭秘之性能优化技巧篇 一、写在前面 我们说的性能优化与降低开销,那必然都是在都能实现需求的条件下,选取其中的“最优解”,而不是避开需求,泛泛地谈性能和开销。 “沉迷”于寻求最优解,在各行各业都存在,哪怕做一顿晚餐,人们也总在摸索如何能在更短的时间更少的资...
      继续阅读 »



      CSS揭秘之性能优化技巧篇


      一、写在前面


      我们说的性能优化与降低开销,那必然都是在都能实现需求的条件下,选取其中的“最优解”,而不是避开需求,泛泛地谈性能和开销。


      “沉迷”于寻求最优解,在各行各业都存在,哪怕做一顿晚餐,人们也总在摸索如何能在更短的时间更少的资源,做更多的“美味”。例如要考虑先把米放到电饭煲,
      然后把需要解冻的拿出来解冻,把蘑菇黄豆这种需要浸泡的先“预处理”,青菜要放在后面炒,汤要先炖,
      洗菜的水要用来浇花...需要切的菜原料排序要靠近,有些菜可以一起洗省时节水,要提前准备好装菜的器皿否则你可能要洗好几次手


      瞧做一顿晚餐其实也可以很讲究,归纳一下这些行为,可以统称为“优化行为”,也可以引用一些术语表示,例如寻找“最优解“和”关键路径“,
      在 CSS 的使用中,同样也需要”关键路径“、”最优解“和”优化“,下面将从这几个方面解释 CSS 性能优化:



      ①渲染方向的优化


      ②加载方向的优化



      二、CSS性能优化技巧


      2.1 渲染方向的优化


      • ①减少重排(redraw)重绘(repaint)


      例如符合条件的vue中应尽可能使用 v-show 代替 v-if。v-show 是通过改变 css display 属性值实现切换效果,
      v-if 则是通过直接销毁或创建 dom 元素来达到显示和隐藏的效果。 v-if是真正的条件渲染,当一开始的值为true时才会编译渲染,
      而v-show不管怎样都会编译,只是简单地css属性切换。v-if适合条件不经常改变的场景,因为它的切换会重新编译渲染,
      会创建或销毁 dom 节点,开销较大。 v-show 适合切换较为频繁的场景,开销较小。




      • ②减少使用性能开销大的属性:例如动画、浮动、特殊定位。



      • ③减少css计算属性的使用,有时它们不是必须使用的:例如 calc(100% - 20px),如果这 20px 是边距,



      那么或许可以考虑 border-size:border-box。



      • ④脚本行为的防抖节流,减少不必要的的重复渲染开销。



      • ⑤属性值为 0 时,不必添加单位(无论是相对单位还是绝对单位),那可能会增加计算开销,



      且也没有规范提倡0值加单位,那是没有意义的,0rem和0em在结果上是没有区别的,但加上单位可能会带来不必要的计算开销。
      关于0不必加单位,想必你也收到过编辑器的优化提示。


      • ⑥css 简写属性的使用,有时开销会更大得不偿失,例如 padding: 0 2px 0 0;和 padding-right:2px;

      后者的写法对机器和人的阅读理解和计算的开销都是更小的。常见的 css 可简写属性还有 background,border,
      font 和 margin。


      • ⑦尽可能减少 CSS 规则的数量,并删除未使用到的 CSS 规则。一些默认就有的 CSS 规则,就不必写了,具有继承性的样式,

      也不必每级节点都写。


      -⑧避免使用不必要且复杂的 CSS 选择器(尤其是后代选择器),因为此类选择器需要耗用更多的 CPU 处理能力来执行选择器匹配。
      总之不必要的深度,不管是 css 还是 dom 都不是好的选择,这对人和机器都是同样的道理,因为读和理解起来都同样的“费力”。


      -⑨关键选择器(key selector)。


      览器会从最右边的样式选择器开始,依次向左匹配。最右边的选择器相当于关键选择器(key selector),
      浏览器会根据关键选择器从 dom 中筛选出对应的元素,然后再向上遍历相关的父元素,判断是否匹配。


      所以组合嵌套选择器时,匹配语句越短越简单,浏览器消耗的时间越短,
      同时也应该减少像标签选择器,这样的大范围命中的通配选择器出现在组合嵌套选择器链中,
      因为那样会让浏览器做大量的筛选,从而去判断选出匹配的元素。


      -⑩少用*{}通配规则,那样的计算开销是巨大的。


      2.2 加载方向的优化


      • ①减少 @import 的使用

      合理规划 css 的加载引入方式,减少 @import 的使用,页面被加载时,link 会同时被加载,
      而 @import 引用的 CSS 会等到页面被加载完再加载。



      • ②css 尽量放在 head 中,会先加载,减少首次渲染时间。



      • ③按需加载,不必一次就加载完全部资源,在单页应用中应尤其注意。



      • ④考虑样式和结构行为分离,抽放到独立css文件,减少重复加载和渲染。



      • ⑤css压缩技术的应用,减少体积。



      三、写在后面


      有一个好的家务机器人,我们可以省很多事,少操心很多,同样的,
      有一个好的 css 预处理工具和打包工具或许也可以帮助程序员节省很多精力。


      网速的提升和设备性能提升,也让程序员拥有许多资源可以“挥霍”,例如现在的很多“国民级”的
      应用在3g网络下和早期的手机中都无法正常工作,但那似乎不影响它们的“优秀”。诚然,那又是复杂的问题。



      正如开头所言,程序员寻求“最优解”和“关键路径”,应当在有可替代方案和能满足需求的前提下进行。



      仅是理论空谈优化,无异于是”耍流氓“。矛盾无处无时不在,重要的是衡量取舍和你能承受。


      作者:灵扁扁
      链接:https://juejin.cn/post/7106825740754092069
      来源:稀土掘金
      著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
      收起阅读 »

      系统介绍浏览器缓存机制及前端优化方案

      背景 缓存是用来做性能优化的好东西,但是,如果用不好缓存,就会产生一系列问题: 为什么我的页面显示的还是老版本为什么我的网页白屏请刷新下网页... 以上问题大家或多或少都遇到过,归根结底是使用缓存的姿势不对,今天,我们就来一起了解下浏览器是如何进行缓存的,以...
      继续阅读 »


      背景


      image-20220610170115175


      缓存是用来做性能优化的好东西,但是,如果用不好缓存,就会产生一系列问题:


      • 为什么我的页面显示的还是老版本
      • 为什么我的网页白屏
      • 请刷新下网页
      • ...

      以上问题大家或多或少都遇到过,归根结底是使用缓存的姿势不对,今天,我们就来一起了解下浏览器是如何进行缓存的,以及我们要怎样科学的使用缓存


      浏览器的缓存机制


      1. 什么是浏览器缓存?


      image-20220609105103551


      简单说,浏览器把 http 请求的资源保存到本地,供下次使用的行为,就是浏览器缓存


      这里先记一个点:http 响应头,决定了浏览器会对资源采取什么样的缓存策略


      2. 浏览器是读取缓存还是请求数据?


      • 用户第一次请求资源

      image-20220609173401737


      • 整个完整流程

      image-20220609171118083


      3. 缓存过程分类——强缓存 / 协商缓存



      根据是否请求服务,我们把缓存过程分为强缓存和协商缓存,也可以理解为必然经过的过程称为强缓存,如果强缓存没有,那在和服务器协商一下



      强缓存


      强缓存看的是响应头的 Expires 和 Cache-Control 字段


      • Expires 是老规范,它表示的是一个绝对有效时间,在该时间之前则命中缓存,如果超过则缓存失效,并且,由于它是跟本地时间(可以随意修改)做比较,会导致缓存混乱
      • Cache-Control 是新规范,优先级要高于Expires,也是目前主要使用的缓存策略,字段是max-age,表示的是一个相对时间,例如 Cache-Control: max-age=3600,代表着资源的有效期是 3600 秒。


      其他配置


      no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。


      no-store:禁止使用缓存,每一次都要重新请求数据。


      public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。


      private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。



      协商缓存


      当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据 header 中的部分信息来判断是否命中缓存。如果命中,则返回 304 ,告诉浏览器资源未更新,可使用本地的缓存。


      协商缓存看的是 header 中的 Last-Modified / If-Modified-Since 和 Etag / If-None-Match



      缓存生效,返回304,缓存失效,返回200和请求结果


      Etag 优先级 Last-Modified 高



      • Last-Modified / If-Modified-Since

      浏览器第一次请求一个资源的时候,服务器返回的 header 中会加上 Last-Modify,Last-modify 是一个时间标识该资源的最后修改时间。


      当浏览器再次请求该资源时,request 的请求头中会包含 If-Modify-Since,该值为缓存之前返回的
      Last-Modify。服务器收到 If-Modify-Since
      后,根据资源的最后修改时间判断是否命中缓存,命中返回304使用本缓存,否则返回200和请求最新资源。


      • Etag / If-None-Match

      etag 是更为严谨的校验,一般情况下使用时间检验已经足够,但我们想象一个场景,如果我们在短暂时间内修改了服务端资源,然后又迅速的改回去,理论上这种情况本地缓存还是可以继续使用的,这就是 etag 诞生的场景。


      使用 etag 时服务端会对资源进行一次类似 hash 的操作获得一个标识(内容不变标识不变),并返回给客户端。


      再次请求时客户端会在 If-None-Match 带上 etag 的值给服务端进行对比验证,如果命中返回304使用缓存,否则重新请求资源。



      注:由于 e-atg 服务端计算会有额外开销,所以性能较差



      扩展:DNS缓存与CDN缓存


      DNS 缓存


      我们在网上所有的通信行为都需要IP来进行连接,DNS解析是指通过域名获取相应IP地址的过程。


      基本上有DNS的地方就有缓存,查询顺序如下:


      image-20220610104424261


      一般我们日常会接触到的就是有时内网域名访问需要修改本地host映射关系,或者某些科学上网的情况,可以通过修改本地host来正常访问网址


      CDN 缓存


      CDN 缓存从减轻根服务的分发压力和缩短物理的传输距离(跨地域访问)上2个层面对资源访问进行优化。



      CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低。


      大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源服务器的负载。



      一般CDN服务都由运营商提供,我们只需要了解如何验证CDN是否生效即可



      • 查看域名是否配置了CDN缓存


        ping {{ 域名 }} 会看到转向了其他地址(alikunlun)


        例如: ping customer.kukahome.com


        image-20220610110014867



      • 查看我们的页面资源是否命中CDN缓存



      通过查看相应头有 X-cache:HIT 字段,则命中CDN缓存,注意这里名称并不固定,但一般都会有HIT标识,如果是MISS 或None之类的,则没有命中缓存


      image-20220610110324860


      前端针对缓存部署优化方案


      构建演进



      构建方面优化的核心思想是如何更优,更快速的加载资源,以及如何保证资源的新鲜度



      这个优化过程也分为几个阶段,有些其实已经不适用现在的场景,但也可以了解下



      • 早期的图标合并雪碧图(sprite),多脚本文件整理到一个文件:目的是通过减少碎片化的请求数量来加速资源加载(相关知识点是浏览器一般最多只支持6个并发请求,同一时间请求数量需要控制在合理范围)


        • 现在雪碧图已基本被 iconfont 代替,js 加载更多采用分模块异步加载,而不是一味合并


      • 随着 web 应用的推广和浏览器缓存技术的普及,前端缓存问题也随着而来,最常见的就是服务端资源变了,但是客户端资源始终无法更新,这个阶段工程师们想了很多方案。


        • 打包时在静态资源路径上加上 “?v=version” 或者使用时间戳来给资源文件命名


        • 跟 modified 缓存有点像,由于时间戳并不能识别出文件内容是否变动,所以有了后来的 hash 方案,理论上 hash 出来的文件只要内容不变,文件名就不变,大大提高了缓存的使用寿命,也是现代常用打包工具的默认配置

        image-20220610141324528



      • 然后,重点来了,以上我们对 html 文件里链接的资源做了一系列优化,但是 html
        本身也是一种静态资源,并且,客户在访问页面时是不会带上所谓的时间戳或者版本号的,导致了很多时候虽然服务端资源更新了,但是客户端还是用老的
        html 文本发起请求,这个时候就会导致各种各样的问题了,包括但不限于白屏,展现的旧版本页面等等


        image-20220610150956617


        • 为了解决这个问题,目前主流的解决方案是不对 html 进行缓存(一般单页应用html文件较小,大的是 js),只对 js,css 等静态文件进行本地缓存

        image-20220610151923311



        • 那么,如何让浏览器不缓存 html 呢,目前都是通过设置 Cache-Control实现, 有前端方案和后端方案,风险提示,前端方案很不靠谱,后端很多默认配置会覆盖前端方案,可以做了解,生产中请使用后端配置。


          通过 html 标签设置 cache-control


            <meta http-equiv="Pragma" content="no-cache" />  // 旧协议
          <meta http-equiv="Expires" content="0" /> // 旧协议
          <meta http-equiv="Cache-Control" content="no-cache" /> // 目前主流



      部署配置



      目前主流的前端部署方式都是使用 nginx,我们来看看 nginx 如何禁用 html 的缓存



      location / {
        root **;
        # 配置页面不缓存html和htm结尾的文件
        if ($request_filename ~* .*.(?:htm|html)$)
        {
            add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
        }
        index index.html index.htm;
      }

      • Private 会影响到CDN缓存命中率,但本身CDN缓存也会存在版本问题,量不大的情况下也可以禁掉
      • No-cache 可以使用缓存,但是使用前必须到服务端验证,可以是 no-cache,也可以是 max-age=0
      • No-store 完全禁用缓存
      • Must-revalidate 与 no-cache 类似,强制到服务端验证,作用于一些特殊场景,例如发送了校验请求,但发送失败了之类
      • Proxy-revalidate 与上面类似,要求代理缓存服务验证有效性

      以上配置可以跟据项目需要灵活配置,考虑到浏览器对缓存协议支持会有些许差异,只是想简单粗暴禁用 html 缓存全上也没有关系,并不会有特别大影响,除非特殊场景需要调优时需要关注。


      资源压缩


      都讲到这了,前端构建优化还有一个常用的就是 Gzip 资源压缩,可以极大减小资源包体积,前端一般构建工具都有插件支持,需要注意的是也需要 nginx 做配置才能生效


      http {
        gzip_static on;
        gzip_proxied any;
      }

      如果生效了,响应头里可以看到 content-encoding: gzip


      image-20220610162843430

      收起阅读 »

      🦊【低代码相关】表单联动新思路 摆脱if-else的地狱🦄

      在低代码解决方案中,表单是一大类低代码搭建解决的问题。表单作为用户信息采集的手段存在于各类应用中,无论是面向C端的手机页面,还是面向B端的运营平台,甚至低代码平台本身都会存在表单这种形式的交互。 表单本身并不复杂,各个组件库,如antd,element ui等...
      继续阅读 »


      在低代码解决方案中,表单是一大类低代码搭建解决的问题。表单作为用户信息采集的手段存在于各类应用中,无论是面向C端的手机页面,还是面向B端的运营平台,甚至低代码平台本身都会存在表单这种形式的交互。


      表单本身并不复杂,各个组件库,如antd,element ui等都提供了表单组件,能够将一组输入控件组织成一个表单,并且都提供了简单的校验功能,能够检查单控件类似非空、输入长度、正则匹配之类的问题,也可以针对类似多字段情形自己定制复杂校验逻辑。然而,对于表单项之间存在联动的情形,比如一些字段的出现/消失依赖于其他字段的情形,或者一些字段填写以后其他字段的选项应当变更,这些情形通用的组件库就没有提供解决方案,而是由开发各显神通了。


      表单联动最简单的方式自然是if-else了,对于联动项较少的情形,简单一个if-else就能够实现我们所需要的功能。然而,在复杂的表单上if-else层层嵌套下来代码的可读性会变差,下次开发的时候看着长长的一串if-else,每个人都会超级头痛。更重要的是,采用if-else的维护方式,在表单渲染部分需要一组对应的逻辑,在表单提交校验的时候又需要一组对应的逻辑,两边的逻辑大量都是重复的,但一组是嵌套在视图里,一组是针对表单数据。


      在程序语言中,解决if-else的方法是采用模式匹配,在表单联动这个主题上,这个方式也是可行的嘛?让我们就着手试试吧!


      模式定义


      我们的目标是尽可能多地去掉if-else。表单联动主要是基于表单的值,那模式自然是基于值来定义的。


      举个🌰:假设我们需要开发一个会议预订系统,支持单次和循环会议,那么表单的模式有那几种呢?


      系统最后的效果就类似Outlook:


      image.png



      1. 单次会议,需要会议日期(event_date)、开始时间(event_start)、结束时间(event_end)、主题(subject)、参与者(attenders)、地址(location)



      2. 循环会议,一样需要开始时间(event_start),结束时间(event_end),主题(subject)、参与者(attenders),地址(location),还需要循环的间隔(recurrence_interval)和循环的起始(recurrence_start)、结束日期(recurrence_end)。而循环又可以分为以下几种子模式:


        1. 按日循环
        2. 按周循环,额外需要周几举行会议(recurrence_weekdays)
        3. 按月循环,额外需要几号举行会议(recurrence_date)
        4. 按年循环,额外需要几月几号举行会议(recurrence_month,recurrence_date)


      这里除了地址和循环结束日期以外的所有字段都是必选的,循环的间隔需要是一个正整数。


      可以看到,这里一共是5种模式。区分模式主要是两个字段——是否循环(is_recurrence)和循环单位(recurrence_unit),并且都是值的唯一匹配,因此我们可以用简单用JSON的方式定义模式:


      // 单次会议
      {
      "is_recurrence": false
      }
      // 按日循环
      {
      "is_recurrence": true,
      "recurrence_unit": "day"
      }
      // 按周循环
      {
      "is_recurrence": true,
      "recurrence_unit": "week"
      }
      // 按月循环
      {
      "is_recurrence": true,
      "recurrence_unit": "month"
      }
      // 按年循环
      {
      "is_recurrence": true,
      "recurrence_unit": "year"
      }

      对于更复杂的情况来说,模式的区分可能就不是单一值匹配了。例如我们需要做一个医院急诊管理系统,需要根据用户输入的体温来获取更多信息,体温在38.5度上下需要有不同的反馈,这样的情况就没法简单用JSON来表达,而是需要使用function,但整体的逻辑是一致的,都是将可能的情况定义为模式,并将表单状态与模式相关联。


      表单定义


      定义完模式后我们需要定义对应的表单。


      在我们的会议预订应用中,总共有以下几个字段:


      • event_date
      • event_start
      • event_end
      • subject
      • attenders
      • location
      • is_recurrence
      • recurrence_interval
      • recurrence_unit
      • recurrence_start
      • recurrence_end
      • recurrence_weekdays
      • recurrence_month
      • recurrence_date

      在这个场景下,每个字段展示的内容和校验逻辑在5种模式下都是一致的,需要根据模式联动的点只在于每个字段是否展示,整个表单数据的校验逻辑其实所有展示字段的单字段校验逻辑。因此,我们将每个字段通过以下类型表示:


      type FormField<T> = {
      /**
      * 表单展示
      */

      render: (value: T | undefined) => ReactNode;
      /**
      * 校验规则
      */

      rules: {
      validates: (value: T | undefined) => boolean;
      errorMessage: boolean;
      }[];
      };

      所有字段根据字段key通过一个map进行存储与索引。同时,将每个模式下应该展示的字段以字段key的数组的方式进行存储:


      /** 所有字段的存储,这里省略实现 */
      declare const formFields: Record<keyof Schedule, FormField<any>>;
      type Pattern = {
      pattern_indicator: Partial<Schedule>;
      fields: (keyof Schedule)[];
      };
      /** 每个模式下应该展示的字段映射 */
      const patterns: Pattern[] = [
      {
      pattern_indicator: { is_recurrence: false },
      fields: [
      "event_date",
      "event_start",
      "event_end",
      "subject",
      "attenders",
      "location",
      "is_recurrence",
      ],
      },
      {
      pattern_indicator: { is_recurrence: true, recurrence_unit: "day" },
      fields: [
      "event_start",
      "event_end",
      "subject",
      "attenders",
      "location",
      "is_recurrence",
      "recurrence_interval",
      "recurrence_unit",
      "recurrence_start",
      "recurrence_end",
      ],
      },
      {
      pattern_indicator: { is_recurrence: true, recurrence_unit: "week" },
      fields: [
      "event_start",
      "event_end",
      "subject",
      "attenders",
      "location",
      "is_recurrence",
      "recurrence_interval",
      "recurrence_unit",
      "recurrence_start",
      "recurrence_end",
      "recurrence_weekdays",
      ],
      },
      {
      pattern_indicator: { is_recurrence: true, recurrence_unit: "month" },
      fields: [
      "event_start",
      "event_end",
      "subject",
      "attenders",
      "location",
      "is_recurrence",
      "recurrence_interval",
      "recurrence_unit",
      "recurrence_start",
      "recurrence_end",
      "recurrence_date",
      ],
      },
      {
      pattern_indicator: { is_recurrence: true, recurrence_unit: "year" },
      fields: [
      "event_start",
      "event_end",
      "subject",
      "attenders",
      "location",
      "is_recurrence",
      "recurrence_interval",
      "recurrence_unit",
      "recurrence_start",
      "recurrence_end",
      "recurrence_month",
      "recurrence_date",
      ],
      },
      ];

      展示逻辑


      表单定义好后,具体应该如何展示呢?


      对于刚好匹配上一个模式的情况,显而易见地,我们应当展示该模式应当展示的字段。


      然而,也存在匹配不上任何模式的情况。比如初始状态下,所有字段都还没有值,自然就不可能匹配上任何模式;又比如is_recurrence选择了true,但其他字段都还没有填写的情况。这种情况下我们该展示哪些字段呢?


      我们可以从初始状态这种情况开始考虑,初始情况是是所有情况的起始点,那么只要所有情况下都会展示的字段,那么初始情况也应该展示。然后,当用户将is_recurrence选择了true,那么单次会议这种可能性已经被排除了,还剩下4种循环的情况,这时就应该展示这四种剩余情况都展示的字段。


      这样,整套展示逻辑就出来了:


      const matchedPattern: Pattern = getMatchedPattern(patterns, answer);
      if (matchedPattern) {
      return matchedPattern.fields;
      }
      const possiblePatterns: Pattern[] = removeUnmatchedPatterns(patterns, answer);
      return getIntersectionFields(possiblePatterns);

      本文用一个简单的例子来阐释了我们通过模式匹配的方式定义表单的思路。其实,像类似决策树、有限状态机等的模型都可以用来帮助我们通过更灵活的方式来定义我们的表单联动逻辑,像formily之类的专业的表单库更是有完整的解决方案,欢迎各位读者一起提供思路哈哈。

       
      收起阅读 »

      如何编写复杂拖拽组件🐣

      阅读本文🦀 1.您将了解到如何让echart做到响应式 2.您将到如何编写复杂的拖拽组件 3.和我一起实现可拖拽组件的增删改查、可编辑、可以拖拽、可排序、可持久化 4.和我一起实现可拖拽组件的删除抖动动画 前言🌵 在业务中得到一个很复杂的需求,需要实现组件中...
      继续阅读 »





      阅读本文🦀


      1.您将了解到如何让echart做到响应式


      2.您将到如何编写复杂的拖拽组件


      3.和我一起实现可拖拽组件的增删改查、可编辑、可以拖拽、可排序、可持久化


      4.和我一起实现可拖拽组件的删除抖动动画


      前言🌵



      在业务中得到一个很复杂的需求,需要实现组件中展示ecahrts图表,并且图表可编辑,可排序,大小可调整,还要可持续化,下面就是解决方案啦



      正文 🦁


      先看效果再一步步实现



      技术调研



      如何做到可拖拽?自己造轮子?显然不是,当然是站在巨人的肩膀上😁



      1. react-dnd
      2. react-beautiful-dnd
      3. dnd-kit
      4. react-sortable-hoc
      5. react-grid-layout

      • react-dnd

        • 文档齐全
        • github star星数16.4k
        • 维护更新良好,最近一月内有更新维护
        • 学习成本较高
        • 功能中等
        • 移动端兼容情况,良好
        • 示例数量中等
        • 概念较多,使用复杂
        • 组件间能解耦

      • react-beautiful-dnd

        • 文档齐全
        • github star星数24.8k
        • 维护更新良好,最近三月内有更新维护
        • 学习成本较高
        • 使用易度中等
        • 功能丰富
        • 移动端兼容情况,优秀
        • 示例数量丰富
        • 是为垂直和水平列表专门构建的更高级别的抽象,没有提供 react-dnd 提供的广泛功能
        • 外观漂亮,可访问性好,物理感知让人感觉更真实的在移动物体
        • 开发理念上是拖拽,不支持copy/clone

      • dnd-kit

        • 文档齐全
        • github star星数2.8k
        • 维护更新良好,最近一月内有更新维护
        • 学习成本中等
        • 使用易度中等
        • 功能中等
        • 移动端兼容情况,中等
        • 示例数量丰富
        • 未看到copy/clone

      • react-sortable-hoc

        • 文档较少
        • github star星数9.5k
        • 维护更新良好,最近三月内有更新维护
        • 学习成本较低
        • 使用易度较低
        • 功能简单
        • 移动端兼容情况,中等
        • 示例数量中等
        • 不支持拖拽到另一个容器中
        • 未看到copy/clone
        • 主要集中于排序功能,其余拖拽功能不丰富

      • react-grid-layout
        • 文档较少
        • github star 星星15.8k
        • 维护更新比较好,近三个月有更新维护
        • 学习成本比较高
        • 功能复杂
        • 支持拖拽、放大缩小


      总结:为了实现我们想要的功能,最终选择react-grid-layout,应为我们想要的就是在网格中实现拖拽、放大缩小、排序等功能


      Coding🔥



      由于代码量比较大,只讲述一些核心的code



      1.先创建基础布局


      • isDraggable 控制是否可拖拽
      • isResizable 控制是否可放大缩小
      • rowHeight控制基础行高
      • layout控制当前gird画布中每个元素的排列顺序
      • onLayoutChange 当布局发生改变后的回调函数

        <ReactGridLayout
      isDraggable={edit}
      isResizable={edit}
      rowHeight={250}
      layout={transformLayouts}
      onLayoutChange={onLayoutChange}
      cols={COLS}
      >
      {layouts && layouts.map((layout, i) => {
      if (!chartList?.some(chartId => chartId === layout.i))
      return null

      return (<div
      key={layout.i}
      data-grid={layout}
      css={css`width: 100%;
      height: 100%`}
      >

      <Chart
      setSpinning={setSpinning}
      updateChartList={updateChartList}
      edit={edit}
      key={layout.i}
      chartList={chartList}
      chartId={Number(layout.i)}
      scenarioId={scenarioId}/>

      </div>

      )
      })}
      </ReactGridLayout>


      2.如何让grid中的每个echarts图表随着外层item的放大缩小而改变


          const resizeObserver = new ResizeObserver((entries) => {
      myChart?.resize()//当dom发生大小改变就重置echart大小
      })
      resizeObserver.observe(chartContainer.current)//通过resizeObserver观察echart对应的item实例对象

      3.如何实现排序的持久化


      //通过一下代码可以实现记录edit变量的前后状态
      const [edit, setEdit] = useState(false)
      const prevEdit = useRef(false)
      useEffect(() => {
      prevEdit.current = edit
      })

       //通过将grid中的每个item的排序位置记录为对象,然后对每个属性进行前后的对比,如果没有改变就不进行任何操作,如果发生了改变就可以
      //通过网络IO更新grid中item的位置
      useEffect(() => {
      if (prevEdit && !edit) {
      // 对比前后的layout做diff 判断是否需要更新位置
      const diffResult = layouts?.every((layout) => {
      const changedLayout = changedLayouts.find((changedLayout) => {
      // eslint-disable-next-line eqeqeq
      return changedLayout.i == layout.i
      })
      return changedLayout?.w === layout.w
      && changedLayout?.h === layout.h
      && changedLayout?.x === layout.x
      && changedLayout?.y === layout.y
      })
      // diffResult为false 证明发生了改变
      if (!diffResult) {
      //这里就可以做图表发生改变后的操作
      //xxxxx
      }
      }, [edit])

      4.如何实现编辑时的抖动动画


      .wobble-hor-bottom{
      animation:wobble-hor-bottom infinite 1.5s ;
      }

      @-webkit-keyframes wobble-hor-bottom {
      0%,
      100% {
      -webkit-transform: translateX(0%);
      transform: translateX(0%);
      -webkit-transform-origin: 50% 50%;
      transform-origin: 50% 50%;
      }
      15% {
      -webkit-transform: translateX(-10px) rotate(-1deg);
      transform: translateX(-10px) rotate(-1deg);
      }
      30% {
      -webkit-transform: translateX(5px) rotate(1deg);
      transform: translateX(5px) rotate(1deg);
      }
      45% {
      -webkit-transform: translateX(-5px) rotate(-0.6deg);
      transform: translateX(-5px) rotate(-0.6deg);
      }
      60% {
      -webkit-transform: translateX(3px) rotate(0.4deg);
      transform: translateX(3px) rotate(0.4deg);
      }
      75% {
      -webkit-transform: translateX(-2px) rotate(-0.2deg);
      transform: translateX(-2px) rotate(-0.2deg);
      }
      }

      总结 🍁


      本文大致讲解了下如何使用react-grid-layout如何与echart图表结合使用,来完成复杂的拖拽、排序、等功能,但是这个组件实现细节还有很多,本文只能提供一个大值的思路,还是希望能够帮助到大家,给大家提供一个思路,欢迎留言和我讨论,如果你有什么更好的办法实现类似的功能


      结束语 🌞



      那么我的如何编写复杂拖拽组件🐣就结束了,文章的目的其实很简单,就是对日常工作的总结和输出,输出一些觉得对大家有用的东西,菜不菜不重要,但是热爱🔥,希望大家能够喜欢我的文章,我真的很用心在写,也希望通过文章认识更多志同道合的朋友,如果你也喜欢折腾,欢迎加我好友,一起沙雕,一起进步

      收起阅读 »

      一日正则一日神,一直正则一直神

      本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!! 千分位格式化 在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。 123456789 => ...
      继续阅读 »


      本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!!


      千分位格式化


      在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。


      1. 123456789 => 123,456,789
      2. 123456789.123 => 123,456,789.123


      const formatMoney = (money) => {
      return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
      }

      formatMoney('123456789') // '123,456,789'
      formatMoney('123456789.123') // '123,456,789.123'
      formatMoney('123') // '123'


      想想如果不是用正则,还可以用什么更优雅的方法实现它?


      解析链接参数


      你一定常常遇到这样的需求,要拿到 url 的参数的值,像这样:





      // url <https://qianlongo.github.io/vue-demos/dist/index.html?name=fatfish&age=100#/home>

      const name = getQueryByName('name') // fatfish
      const age = getQueryByName('age') // 100
       



      通过正则,简单就能实现 getQueryByName 函数:


      const getQueryByName = (name) => {
      const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`)
      const queryNameMatch = window.location.search.match(queryNameRegex)
      // Generally, it will be decoded by decodeURIComponent
      return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ''
      }

      const name = getQueryByName('name')
      const age = getQueryByName('age')

      console.log(name, age) // fatfish, 100
       



      驼峰字符串




      JS 变量最佳是驼峰风格的写法,怎样将类似以下的其它声明风格写法转化为驼峰写法?


      1. foo Bar => fooBar
      2. foo-bar---- => fooBar
      3. foo_bar__ => fooBar

      正则表达式分分钟教做人:


      const camelCase = (string) => {
      const camelCaseRegex = /[-_\s]+(.)?/g
      return string.replace(camelCaseRegex, (match, char) => {
      return char ? char.toUpperCase() : ''
      })
      }

      console.log(camelCase('foo Bar')) // fooBar
      console.log(camelCase('foo-bar--')) // fooBar
      console.log(camelCase('foo_bar__')) // fooBar
       

      小写转大写


      这个需求常见,无需多言,用就完事儿啦:


      const capitalize = (string) => {
      const capitalizeRegex = /(?:^|\s+)\w/g
      return string.toLowerCase().replace(capitalizeRegex, (match) => match.toUpperCase())
      }

      console.log(capitalize('hello world')) // Hello World
      console.log(capitalize('hello WORLD')) // Hello World

      实现 trim()


      trim() 方法用于删除字符串的头尾空白符,用正则可以模拟实现 trim:


      const trim1 = (str) => {
      return str.replace(/^\s*|\s*$/g, '') // 或者 str.replace(/^\s*(.*?)\s*$/g, '$1')
      }

      const string = ' hello medium '
      const noSpaceString = 'hello medium'
      const trimString = trim1(string)

      console.log(string)
      console.log(trimString, trimString === noSpaceString) // hello medium true
      console.log(string)

      trim() 方法不会改变原始字符串,同样,自定义实现的 trim1 也不会改变原始字符串;

      HTML 转义


      防止 XSS 攻击的方法之一是进行 HTML 转义,符号对应的转义字符:


      正则处理如下:


      const escape = (string) => {
      const escapeMaps = {
      '&': 'amp',
      '<': 'lt',
      '>': 'gt',
      '"': 'quot',
      "'": '#39'
      }
      // The effect here is the same as that of /[&amp;<> "']/g
      const escapeRegexp = new RegExp(`[${Object.keys(escapeMaps).join('')}]`, 'g')
      return string.replace(escapeRegexp, (match) => `&${escapeMaps[match]};`)
      }

      console.log(escape(`
      <div>
      <p>hello world</p>
      </div>
      `
      ))
      /*
      &lt;div&gt;
      &lt;p&gt;hello world&lt;/p&gt;
      &lt;/div&gt;
      */


      HTML 反转义


      有了正向的转义,就有反向的逆转义,操作如下:


      const unescape = (string) => {
      const unescapeMaps = {
      'amp': '&',
      'lt': '<',
      'gt': '>',
      'quot': '"',
      '#39': "'"
      }
      const unescapeRegexp = /&([^;]+);/g
      return string.replace(unescapeRegexp, (match, unescapeKey) => {
      return unescapeMaps[ unescapeKey ] || match
      })
      }

      console.log(unescape(`
      &lt;div&gt;
      &lt;p&gt;hello world&lt;/p&gt;
      &lt;/div&gt;
      `
      ))
      /*
      <div>
      <p>hello world</p>
      </div>
      */


      校验 24 小时制


      处理时间,经常要用到正则,比如常见的:校验时间格式是否是合法的 24 小时制:


      const check24TimeRegexp = /^(?:(?:0?|1)\d|2[0-3]):(?:0?|[1-5])\d$/
      console.log(check24TimeRegexp.test('01:14')) // true
      console.log(check24TimeRegexp.test('23:59')) // true
      console.log(check24TimeRegexp.test('23:60')) // false
      console.log(check24TimeRegexp.test('1:14')) // true
      console.log(check24TimeRegexp.test('1:1')) // true

      校验日期格式


      常见的日期格式有:yyyy-mm-dd, yyyy.mm.dd, yyyy/mm/dd 这 3 种,如果有符号乱用的情况,比如2021.08/22,这样就不是合法的日期格式,我们可以通过正则来校验判断:


      const checkDateRegexp = /^\d{4}([-\.\/])(?:0[1-9]|1[0-2])\1(?:0[1-9]|[12]\d|3[01])$/

      console.log(checkDateRegexp.test('2021-08-22')) // true
      console.log(checkDateRegexp.test('2021/08/22')) // true
      console.log(checkDateRegexp.test('2021.08.22')) // true
      console.log(checkDateRegexp.test('2021.08/22')) // false
      console.log(checkDateRegexp.test('2021/08-22')) // false

      匹配颜色值


      在字符串内匹配出 16 进制的颜色值:


      const matchColorRegex = /#(?:[\da-fA-F]{6}|[\da-fA-F]{3})/g
      const colorString = '#12f3a1 #ffBabd #FFF #123 #586'

      console.log(colorString.match(matchColorRegex))
      // [ '#12f3a1', '#ffBabd', '#FFF', '#123', '#586' ]

      判断 HTTPS/HTTP


      这个需求也是很常见的,判断请求协议是否是 HTTPS/HTTP


      const checkProtocol = /^https?:/

      console.log(checkProtocol.test('https://medium.com/')) // true
      console.log(checkProtocol.test('http://medium.com/')) // true
      console.log(checkProtocol.test('//medium.com/')) // false

      校验版本号


      版本号必须采用 x.y.z 格式,其中 XYZ 至少为一位,我们可以用正则来校验:


      // x.y.z
      const versionRegexp = /^(?:\d+\.){2}\d+$/

      console.log(versionRegexp.test('1.1.1'))
      console.log(versionRegexp.test('1.000.1'))
      console.log(versionRegexp.test('1.000.1.1'))

      获取网页 img 地址


      这个需求可能爬虫用的比较多,用正则获取当前网页所有图片的地址。在控制台打印试试,太好用了~~


      const matchImgs = (sHtml) => {
      const imgUrlRegex = /<img[^>]+src="((?:https?:)?\/\/[^"]+)"[^>]*?>/gi
      let matchImgUrls = []

      sHtml.replace(imgUrlRegex, (match, $1) => {
      $1 && matchImgUrls.push($1)
      })
      return matchImgUrls
      }

      console.log(matchImgs(document.body.innerHTML))

      格式化电话号码


      这个需求也是常见的一匹,用就完事了:


      let mobile = '18379836654' 
      let mobileReg = /(?=(\d{4})+$)/g

      console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654
       











      收起阅读 »

      4 个 JavaScript 的心得体会

      按需所取,冲冲冲ヾ(◍°∇°◍)ノ゙ 一、你能说出 JavaScript 的编程范式吗?   首先要说出:JavaScript 是一门多范式语言!支持面向过程(命令式)、面向对象(OOP)和函数式编程(声明式)。 其次,最重要的是说出:JavaScr...
      继续阅读 »




      按需所取,冲冲冲ヾ(◍°∇°◍)ノ゙


      一、你能说出 JavaScript 的编程范式吗?


       


      首先要说出:JavaScript 是一门多范式语言!支持面向过程(命令式)、面向对象(OOP)和函数式编程(声明式)。


      其次,最重要的是说出:JavaScript 是通过原型继承(OLOO-对象委托)来实现面向对象(OOP)的;


      如果还能说出以下,就更棒了:JavaScript 通过闭包、函数是一等公民、lambda 运算来实现函数式编程的。


      如果再进一步,回答出 JavaScript 演进历史,就直接称绝叫好了:JavaScript的语言设计主要受到了Self(一种基于原型的编程语言)和Scheme(一门函数式编程语言)的影响。在语法结构上它又与C语言有很多相似。

       

      • Self 语言 => 基于原型 => JavaScript 用原型实现面向对象编程;
      • Scheme 语言 => 函数式编程语言 => JavaScript 函数式编程;
      • C 语言 => 面向过程 => JavaScript 面向过程编程;




      推荐 Eric Elliott 的另外两篇文章,JavaScript 的两大支柱:


      1. 基于原型的继承
      2. 函数式编程



      二、什么是函数式编程?




      函数式编程是最早出现的编程范式,通过组合运算函数来生成程序。有一些重要的概念:


      • 纯函数
      • 避免副作用
      • 函数组合
      • 高阶函数(闭包)
      • 函数组合
      • 其它函数式编程语言,比如 Lisp、Haskell

      本瓜觉得这里最 nb 就是能提到 monad 和延迟执行了~




      三、类继承和原型继承有什么区别?





      类继承,通过构造函数实现( new 关键字);tips:即使不用 ES6 class,也能实现类继承;


      原型继承,实例直接从其他对象继承,工厂函数或 Object.create();


      本瓜这里觉得能答出以下就很棒了:


      类继承:基于对象复制;


      原型继承:基于对象委托;


      推荐阅读:


       

      四、面向对象和函数式的优缺点




      面向对象优点:对象的概念容易理解,方法调用灵活;


      面向对象缺点:对象可在多个函数中共享状态、被修改,极有可能会产生“竞争”的情况(多处修改同一对象);


      函数式优点:避免变量的共享、修改,纯函数不产生副作用;声明式代码风格更易阅读,更易代码重组、复用;


      函数式缺点:过度抽象,可读性降低;学习难度更大,比如 Monad;

       

      OK,以上便是本篇分享。点赞关注评论,为好文助力👍 🌏









      收起阅读 »

      十分详细的diff算法原理解析

      diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom。 虚拟Dom 上面的概念我们提到了虚拟Dom,相信大家对...
      继续阅读 »


      diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom



      虚拟Dom


      上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟Dom,而不是直接去操作真实Dom。

      虚拟Dom,其实很简单,就是一个用来描述真实Dom的对象


      它有六个属性,sel表示当前节点标签名,data内是节点的属性,children表示当前节点的其他子标签节点,elm表示当前虚拟节点对应的真实节点(这里暂时没有),key即为当前节点的key,text表示当前节点下的文本,结构类似这样。

       
      let vnode = {
      sel: 'ul',
         data: {},
      children: [
      {
      sel: 'li', data: { class: 'item' }, text: 'son1'
      },
      {
      sel: 'li', data: { class: 'item' }, text: 'son2'
      },    
        ],
         elm: undefined,
         key: undefined,
         text: undefined
      }



      那么虚拟Dom有什么用呢。我们其实可以把虚拟Dom理解成对应真实Dom的一种状态。当真实Dom发生变化后,虚拟Dom可以为我们提供这个真实Dom变化之前和变化之后的状态,我们通过对比这两个状态,即可得出真实Dom真正需要更新的部分,即可实现最小量更新。在一些比较复杂的Dom变化场景中,通过对比虚拟Dom后更新真实Dom会比直接更新真实Dom的效率高,这也就是虚拟Dom和diff算法真正存在的意义。


      h函数


      在介绍diff算法原理之前还需要简单让大家了解一下h函数,因为我们要靠它为我们生成虚拟Dom。这个h函数大家应该也比较熟悉,就是render函数里面传入的那个h函数


      h函数可以接受多种类型的参数,但其实它内部只干了一件事,就是执行vnode函数。根据传入h函数的参数来决定执行vnode函数时传入的参数。那么vnode函数又是干什么的呢?vnode函数其实也只干了一件事,就是把传入h函数的参数转化为一个对象,即虚拟Dom。

       
      // vnode.js
      export default function (sel, data, children, text, elm) {
      const key = data.key
      return {sel, data, children, text, elm, key}
      }



      执行h函数后,内部会通过vnode函数生成虚拟Dom,h函数把这个虚拟在return出去。


      diff对比规则


      明确了h函数是干什么的,我们可以简单用h函数生成两个不同的虚拟节点,我们将通过一个简易版的diff算法代码介绍diff对比的具体流程。



      // 第一个参数是sel 第二个参数是data 第三个参数是children
      const myVnode1 = h("h1", {}, [
       h("p", {key: "a"}, "a"),
       h("p", {key: "b"}, "b"),
      ]);

      const myVnode2 = h("h1", {}, [
       h("p", {key: "c"}, "c"),
       h("p", {key: "d"}, "d"),
      ]);



      patch


      比较的第一步就是执行patch,它相当于对比的入口。既然是对比两个虚拟Dom,那么就将两个虚拟Dom作为参数传入patch中。patch的主要作用是对比两个虚拟Dom的根节点,并根据对比结果操作真实Dom。


      patch函数的核心代码如下,注意注释。

       
      // patch.js

      import vnode from "./vnode"
      import patchDetails from "./patchVnode"
      import createEle from "./createEle"

      /**
      * @description 用来对比两个虚拟dom的根节点,并根据对比结果操作真实Dom
      * @param {*} oldVnode
      * @param {*} newVnode
      */
      export function patch(oldVnode, newVnode) {
       // 1.判断oldVnode是否为虚拟节点,不是的话转化为虚拟节点
       if(!oldVnode.sel) {
         // 转化为虚拟节点
         oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
      }

       // 2.判断oldVnode和newVnode是否为同一个节点
       if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
         console.log('是同一个节点')
         // 比较子节点
         patchDetails(oldVnode, newVnode)
      }else {
         console.log('不是同一个节点')
         // 插入newVnode
         const newNode = createEle(newVnode) // 插入之前需要先将newVnode转化为dom
         oldVnode.elm.parentNode.insertBefore(newNode, oldVnode.elm) // 插入操作
         // 删除oldVnode
         oldVnode.elm.parentNode.removeChild(oldVnode.elm)
      }
      }

      // createEle.js

      /**
      * @description 根据传入的虚拟Dom生成真实Dom
      * @param {*} vnode
      * @returns real node
      */
      export default function createEle (vnode) {
       const realNode = document.createElement(vnode.sel)

       // 子节点转换
       if(vnode.text && (vnode.children == undefined || (vnode.children && vnode.children.length == 0)) ) {
         // 子节点只含有文本
         realNode.innerText = vnode.text  
      }else if(Array.isArray(vnode.children) && vnode.children.length > 0) {
         // 子节点为其他虚拟节点 递归添加node
         for(let i = 0; i < vnode.children.length; i++) {
           const childNode = createEle(vnode.children[i])
           realNode.appendChild(childNode)
        }
      }

       // 补充vnode的elm属性
       vnode.elm = realNode

       return vnode.elm
      }



      patchVnode


      patchVnode用来比较两个虚拟节点的子节点并更新其子节点对应的真实Dom节点



      // patchVnode.js

      import updateChildren from "./updateChildren"
      import createEle from "./createEle"

      /**
      * @description 比较两个虚拟节点的子节点(children or text) 并更新其子节点对应的真实dom节点
      * @param {*} oldVnode
      * @param {*} newVnode
      * @returns
      */
      export function patchDetails(oldVnode, newVnode) {
       // 判断oldVnode和newVnode是否为同一个对象, 是的话直接不用比了
       if(oldVnode == newVnode) return

       // 默认newVnode和oldVnode只有text和children其中之一,真实的源码这里的情况会更多一些,不过大同小异。

       if(hasText(newVnode)) {
         // newVnode有text但没有children

         /**
          * newVnode.text !== oldVnode.text 直接囊括了两种情况
          * 1.oldVnode有text无children 但是text和newVnode的text内容不同
          * 2.oldVnode无text有children 此时oldVnode.text为undefined
          * 两种情况都可以通过innerText属性直接完成dom更新
          * 情况1直接更新text 情况2相当于去掉了children后加了新的text
          */
         if(newVnode.text !== oldVnode.text) {
           oldVnode.elm.innerText = newVnode.text
        }

      }else if(hasChildren(newVnode)) {
         // newVnode有children但是没有text
         
         if(hasText(oldVnode)) {
           // oldVnode有text但是没有children
           
           oldVnode.elm.innerText = '' // 删除oldVnode的text
           // 添加newVnode的children
           for(let i = 0; i < newVnode.children.length; i++) {
             oldVnode.elm.appendChild(createEle(newVnode.children[i]))
          }

        }else if(hasChildren(oldVnode)) {
           // oldVnode有children但是没有text

           // 对比两个节点的children 并更新对应的真实dom节点
           updateChildren(oldVnode.children, newVnode.children, oldVnode.elm)
        }
      }
      }

      // 有children没有text
      function hasChildren(node) {
       return !node.text && (node.children && node.children.length > 0)
      }

      // 有text没有children
      function hasText(node) {
       return node.text && (node.children == undefined || (node.children && node.children.length == 0))
      }



      updateChildren


      该方法是diff算法中最复杂的方法(大的要来了)。对应上面patchVnodeoldVnodenewVnode都有children的情况。


      首先我们需要介绍一下这里的对比规则。


      对比过程中会引入四个指针,分别指向oldVnode子节点列表中的第一个节点和最后一个节点(后面我们简称为旧前旧后)以及指向newVnode子节点列表中的第一个节点和最后一个节点(后面我们简称为新前新后


      对比时,每一次对比按照以下顺序进行命中查找


      • 旧前与新前节点对比(1)
      • 旧后与新后节点对比(2)
      • 旧前与新后节点对比(3)
      • 旧后与新前节点对比(4)

      上述四种情况,如果某一种情况两个指针对应的虚拟Dom相同,那么我们称之为命中。命中后就不会接着查找了,指针会移动,(还有可能会操作真实Dom,3或者4命中时会操作真实Dom移动节点)之后开始下一次对比。如果都没有命中,则去oldVnode子节点列表循环查找当前新前指针所指向的节点,如果查到了,那么操作真实Dom移动节点,没查到则新增真实Dom节点插入。


      这种模式的对比会一直进行,直到满足了终止条件。即旧前指针移动到了旧后指针的后面或者新前指针移动到了新后指针的后面,我们可以理解为旧子节点先处理完毕新子节点处理完毕。那么我们可以预想到新旧子节点中总会有其一先处理完,对比结束后,我们会根据没有处理完子节点的那一对前后指针决定是要插入真实Dom还是删除真实Dom。


      • 如果旧子节点先处理完了,新子节点有剩余,说明有要新增的节点。将根据最终新前新后之间的虚拟节点执行插入操作
      • 如果新子节点先处理完了,旧子节点有剩余,说明有要删除的节点。将根据最终旧前旧后之间的虚拟节点执行删除操作

      下面将呈现代码,注意注释

       
      // updateChildren.js

      import patchDetails from "./patchVnode"
      import createEle from "./createEle";

      /**
      * @description 对比子节点列表并更新真实Dom
      * @param {*} oldCh 旧虚拟Dom子节点列表
      * @param {*} newCh 新虚拟Dom子节点列表
      * @param {*} parent 新旧虚拟节点对应的真实Dom
      * @returns
      */

      export default function updateChildren(oldCh, newCh, parent) {
       // 定义四个指针 旧前 旧后 新前 新后 (四个指针两两一对,每一对前后指针所指向的节点以及其之间的节点为未处理的子节点)
       let oldStartIndex = 0;
       let oldEndIndex = oldCh.length - 1;
       let newStartIndex = 0;
       let newEndIndex = newCh.length - 1;

       // 四个指针对应的节点
       let oldStartNode = oldCh[oldStartIndex];
       let oldEndNode = oldCh[oldEndIndex];
       let newStartNode = newCh[newStartIndex];
       let newEndNode = newCh[newEndIndex];

       // oldCh中每个子节点 key 与 index的哈希表 用于四种对比规则都不匹配的情况下在oldCh中寻找节点
       const keyMap = new Map();

       /**
        * 开始遍历两个children数组进行细节对比
        * 对比规则:旧前-新前 旧后-新后 旧前-新后 旧后-新前
        * 对比之后指针进行移动
        * 直到指针不满足以下条件 意味着有一对前后指针之间再无未处理的子节点 则停止对比 直接操作DOM
        */

       while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
         // 这四种情况是为了让指针在移动的过程中跳过空节点
         if (oldStartNode == undefined) {
           oldStartNode = oldCh[++oldStartIndex];
        } else if (oldEndNode == undefined) {
           oldEndNode = oldCh[--oldEndIndex];
        } else if (newStartNode == undefined) {
           newStartNode = newCh[++newStartIndex];
        } else if (newEndNode == undefined) {
           newEndNode = newCh[--newEndIndex];
        } else if (isSame(oldStartNode, newStartNode)) {
           console.log("method1");
           // 旧前-新前是同一个虚拟节点

           // 两个子节点再对比他们的子节点并更新dom (递归切入点)
           patchDetails(oldStartNode, newStartNode);
           // 指针移动
           oldStartNode = oldCh[++oldStartIndex];
           newStartNode = newCh[++newStartIndex];
        } else if (isSame(oldEndNode, newEndNode)) {
           console.log("method2");
           // 旧后-新后是同一个虚拟节点

           // 两个子节点再对比他们的子节点并更新dom (递归切入点)
           patchDetails(oldEndNode, newEndNode);
           // 指针移动
           oldEndNode = oldCh[--oldEndIndex];
           newEndNode = newCh[--newEndIndex];
        } else if (isSame(oldStartNode, newEndNode)) {
           console.log("method3");
           // 旧前-新后是同一个虚拟节点

           // 两个子节点再对比他们的子节点并更新dom (递归切入点)
           patchDetails(oldStartNode, newEndNode);

           /**
            * 这一步多一个移动(真实)节点的操作
            * 需要把当前指针所指向的子节点 移动到 oldEndIndex所对应真实节点之后(也就是未处理真实节点的尾部)
            * 注意:这一步是在操作真实节点
            */
           parent.insertBefore(oldStartNode.elm, oldEndNode.elm.nextSibling);

           // 指针移动
           oldStartNode = oldCh[++oldStartIndex];
           newEndNode = newCh[--newEndIndex];
        } else if (isSame(oldEndNode, newStartNode)) {
           console.log("method4");
           // 旧后-新前 是同一个虚拟节点

           // 两个子节点再对比他们的子节点并更新dom (递归切入点)
           patchDetails(oldEndNode, newStartNode);
           /**
            * 这一步多一个移动(真实)节点的操作
            * 与method3不同在移动位置
            * 需要把当前指针所指向的子节点 移动到 oldStartIndex所对应真实节点之前(也就是未处理真实节点的顶部)
            * 注意:这一步是在操作真实节点
            */
           parent.insertBefore(oldEndNode.elm, oldCh[oldStartIndex].elm);

           // 指针移动
           oldEndNode = oldCh[--oldEndIndex];
           newStartNode = newCh[++newStartIndex];
        } else {
           console.log("does not match");
           // 四种规则都不匹配

           // 生成keyMap
           if (keyMap.size == 0) {
             for (let i = oldStartIndex; i <= oldEndIndex; i++) {
               if (oldCh[i].key) keyMap.set(oldCh[i].key, i);
            }
          }

           // 在oldCh中搜索当前newStartIndex所指向的节点
           if (keyMap.has(newStartNode.key)) {
             // 搜索到了

             // 先获取oldCh中该虚拟节点
             const oldMoveNode = oldCh[keyMap.get(newStartNode.key)];
             // 两个子节点再对比他们的子节点并更新dom (递归切入点)
             patchDetails(oldMoveNode, newStartNode);

             // 移动这个节点(移动的是真实节点)
             parent.insertBefore(oldMoveNode.elm, oldStartNode.elm);

             // 该虚拟节点设置为undefined(还记得最开始的四个条件吗,因为这里会将子节点制空,所以加了那四个条件)
             oldCh[keyMap.get(newStartNode.key)] = undefined;
               
          } else {
             // 没搜索到 直接插入
             parent.insertBefore(createEle(newStartNode), oldStartNode.elm);
          }

           // 指针移动
           newStartNode = newCh[++newStartIndex];
        }
      }

       /**
        * 插入和删除节点
        * while结束后 有一对前后指针之间仍然有未处理的子节点,那么就会进行插入或者删除操作
        * oldCh的双指针中有未处理的子节点,进行删除操作
        * newCh的双指针中有未处理的子节点,进行插入操作
        */
       if (oldStartIndex <= oldEndIndex) {
         // 删除
         for (let i = oldStartIndex; i <= oldEndIndex; i++) {
           // 加判断是因为oldCh[i]有可能为undefined
           if(oldCh[i]) parent.removeChild(oldCh[i].elm);
        }
      } else if (newStartIndex <= newEndIndex) {
         /**
          * 插入
          * 这里需要注意的点是从哪里插入,也就是appendChild的第二个参数
          * 应该从oldStartIndex对应的位置插入
          */
         for (let i = newStartIndex; i <= newEndIndex; i++) {
           // oldCh[oldStartIndex]存在是从头部插入
           parent.insertBefore(createEle(newCh[i]), oldCh[oldStartIndex] ? oldCh[oldStartIndex].elm : undefined);
        }
      }
      }

      // 判断两个虚拟节点是否为同一个虚拟节点
      function isSame(a, b) {
       return a.sel == b.sel && a.key == b.key;
      }



      这里的逻辑稍微比较复杂,需要大家多理几遍,必要的话,自己手画一张图自己移动一下指针。着重需要注意的地方是操作真实Dom时,插入、移动节点应该将节点从哪里插入或者移动到哪里,其实基本插入到oldStartIndex对应的真实Dom的前面,除了第三种命中后的移动节点操作,是移动到oldEndIndex所对应真实节点之后


      总结


      由于diff算法对比的是虚拟Dom,而虚拟Dom是呈树状的,所以我们可以发现,diff算法中充满了递归。总结起来,其实diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。


      这里再提一嘴key,我们面试中经常会被问到vue中key的作用。根据上面我们分析的,key的主要作用其实就是对比两个虚拟节点时,判断其是否为相同节点。加了key以后,我们可以更为明确的判断两个节点是否为同一个虚拟节点,是的话判断子节点是否有变更(有变更更新真实Dom),不是的话继续比。如果不加key的话,如果两个不同节点的标签名恰好相同,那么就会被判定为同一个节点(key都为undefined),结果一对比这两个节点的子节点发现不一样,这样会凭空增加很多对真实Dom的操作,从而导致页面更频繁地重绘和回流。


      所以我认为合理利用key可以有效减少真实Dom的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。

       








      收起阅读 »

      JS堆栈内存的运行机制也需时常回顾咀嚼

      在js引擎中对变量的存储主要有两个位置,堆内存和栈内存。栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null, 以及对象变量的指针(地址值)。栈内存中的变量一般都是已知大小或者有范围上限的,算作...
      继续阅读 »



      在js引擎中对变量的存储主要有两个位置,堆内存和栈内存。栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,


      以及对象变量的指针(地址值)。栈内存中的变量一般都是已知大小或者有范围上限的,算作一种简单存储。而堆内存主要负责像对象Object这种变量类型的存储,对于大小这方面,一般都是未知的。

      栈内存 ECStack


      栈内存ECStack(Execution Context Stack)(作用域)




      栈内存ECStack(Execution Context Stack)(作用域)



      JS之所以能够在浏览器中运行,是因为浏览器给JS提供了执行的环境栈内存





      浏览器会在计算机内存中分配一块内存,专门用来供代码执行=》栈内存ECStack(Execution Context Stack)执行环境栈,每打开一个网页都会生成一个全新的ECS


      ECS的作用




      • 提供一个供JS代码自上而下执行的环境(代码都在栈中执行)
      • 由于基本数据类型值比较简单,他们都是直接在栈内存中开辟一个位置,把值直接存储进去的,当栈内存被销毁,存储的那些基本值也都跟着销毁



      堆内存


      堆内存:引用值对应的空间,堆内存是区别于栈区、全局数据区和代码区的另一个内存区域。堆允许程序在运行时动态地申请某个大小的内存空间。


      存储引用类型值(对象:键值对, 函数:代码字符串),当内存释放销毁,那么这个引用值彻底没了
      堆内存释放


      当堆内存没有被任何得变量或者其他东西所占用,浏览器会在空闲的时候,自主进行内存回收,把所有不被占用得内存销毁掉


      谷歌浏览器(webkit),每隔一定时间查找对象有没有被占用
      引用计数器:当对象引用为0时释放它

      收起阅读 »

      React-Native热更新 - 3分钟教你实现

      此文使用当前最新版本的`RN`与`Code-Push`进行演示,其中的参数不会过多进行详细解释,更多参数解释可参考其它文章,这里只保证APP能正常进行热更新操作,方便快速入门,跟着大猪一起来快活吧。操作指南以下操作在Mac系统上完成的,毕竟 大猪 工作多年之后...
      继续阅读 »

      此文使用当前最新版本的`RN`与`Code-Push`进行演示,其中的参数不会过多进行详细解释,更多参数解释可参考其它文章,这里只保证APP能正常进行热更新操作,方便快速入门,跟着大猪一起来快活吧。

      操作指南

      以下操作在Mac系统上完成的,毕竟 大猪 工作多年之后终于买得起一个Mac了。

      1. 创建`React-Native`项目

      ```
      react-native init dounineApp
      `
      ``

      2. 安装`code-push-cli`

      ```
      npm install -g code-push-cli
      `
      ``

      3. 注册`code-push`帐号

      ```
      code-push register
      Please login to Mobile Center in the browser window we've just opened.
      Enter your token from the browser:
      #会弹出一个浏览器,让你注册,可以使用github帐号对其进行授权,授权成功会给一串Token,点击复制,在控制进行粘贴回车(或者使用code-push login命令)。
      `
      ``
      ```
      Enter your token from the browser: b0c9ba1f91dd232xxxxxxxxxxxxxxxxx
      #成功提示如下方
      Successfully logged-in. Your session file was written to /Users/huanghuanlai/.code-push.config. You can run the code-push logout command at any time to delete this file and terminate your session.
      `
      ``
      ![](http://upload-images.jianshu.io/upload_images/9028759-7736182c03cea82a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

      4. 在`code-push`添加一个ios的app

      ```
      code-push app add dounineApp-ios ios react-native
      #成功提示如下方
      Successfully added the "dounineApp-ios" app, along with the following default deployments:
      ┌────────────┬──────────────────────────────────────────────────────────────────┐
      │ Name │ Deployment Key │
      ├────────────┼──────────────────────────────────────────────────────────────────┤
      │ Production │ yMAPMAjXpfXoTfxCd0Su9c4-U4lU6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
      ├────────────┼──────────────────────────────────────────────────────────────────┤
      │ Staging │ IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
      └────────────┴──────────────────────────────────────────────────────────────────┘
      `
      ``

      5. 继续在`code-push`添加一个android的app

      ```
      code-push app add dounineApp-android android react-native
      #成功提示如下方
      Successfully added the "dounineApp-android" app, along with the following default deployments:
      ┌────────────┬──────────────────────────────────────────────────────────────────┐
      │ Name │ Deployment Key │
      ├────────────┼──────────────────────────────────────────────────────────────────┤
      │ Production │ PZVCGLlVW-0FtdoCF-3ZDWLcX58L6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
      ├────────────┼──────────────────────────────────────────────────────────────────┤
      │ Staging │ T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
      └────────────┴──────────────────────────────────────────────────────────────────┘
      `
      ``

      6. 在项目根目录添加`react-native-code-push`

      ```
      npm install react-native-code-push --save
      #或者
      yarn add react-native-code-push
      `
      ``

      7. link react-native-code-push

      ```
      react-native link
      Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (8ms)
      ? What is your CodePush deployment key for Android (hit to ignore) T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d
      #将刚才添加的Android App的Deployment Key复制粘贴到这里,复制名为Staging测试Deployment Key。
      rnpm-install info Linking react-native-code-push android dependency
      rnpm-install info Android module react-native-code-push has been successfully linked
      rnpm-install info Linking react-native-code-push ios dependency
      rnpm-install WARN ERRGROUP Group 'Frameworks' does not exist in your Xcode project. We have created it automatically for you.
      rnpm-install info iOS module react-native-code-push has been successfully linked
      Running ios postlink script
      ? What is your CodePush deployment key for iOS (hit to ignore) IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d
      #继续复制Ios的Deployment Key
      Running android postlink script
      `
      ``

      8. 在`react-native`的`App.js`文件添加自动更新代码

      ```
      import codePush from "react-native-code-push";
      const codePushOptions = { checkFrequency: codePush.CheckFrequency.MANUAL };
      export default class App extends Component<{}> {
      componentDidMount(){
      codePush.sync({
      updateDialog: true,
      installMode: codePush.InstallMode.IMMEDIATE,
      mandatoryInstallMode:codePush.InstallMode.IMMEDIATE,
      //deploymentKey为刚才生成的,打包哪个平台的App就使用哪个Key,这里用IOS的打包测试
      deploymentKey: 'IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d',
      });
      }
      ...
      `
      ``

      9. 运行项目在ios模拟器上

      ```
      react-native run-ios
      `
      ``

      如图下所显

      1:开启debug调试

      2:`CodePush`已经成功运行

      目前App已经是最新版本

      ![](http://upload-images.jianshu.io/upload_images/9028759-41607a87f412b06a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

      10. 发布一个ios新版本

      ```
      code-push release-react dounineApp-ios ios
      `
      ``
      发布成功如图下
      ```
      Detecting ios app version:
      Using the target binary version value "1.0" from "ios/dounineApp/Info.plist".
      Running "react-native bundle" command:
      node node_modules/react-native/local-cli/cli.js bundle --assets-dest /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush --bundle-output /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush/main.jsbundle --dev false --entry-file index.js --platform ios
      Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (10ms)
      Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (10ms)
      Loading dependency graph, done.
      bundle: start
      bundle: finish
      bundle: Writing bundle output to: /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush/main.jsbundle
      bundle: Done writing bundle output
      Releasing update contents to CodePush:
      Upload progress:[==================================================] 100% 0.0s
      Successfully released an update containing the "/var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush" directory to the "Staging" deployment of the "dounineApp-ios" app.
      `
      ``

      11. 重新Load刷新应用

      ![](http://upload-images.jianshu.io/upload_images/9028759-30c17d2f5db173cd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

      12. 安卓发布

      与上面9~11步骤是一样的,命令改成Android对应的,以下命令结果简化

      1.修改App.js的deploymentKey为安卓的

      ```
      deploymentKey:'T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d'
      `
      ``

      2.运行

      ```
      react-native run-android
      `
      ``

      3.发布

      ```
      code-push release-react dounineApp-android android
      `
      ``
      收起阅读 »

      应急响应 WEB 分析日志攻击,后门木马(手动分析 和 自动化分析.)

      💛不登上悬崖💚,💛又怎么领略一览众山的绝顶风光💚🍪目录:🌲应急响应的概括:🌲应急响应阶段:🌲应急响应准备工作:🌲从入侵面及权限面进行排查:🌲工具下载🌲应急响应的日志分析:🌷手动化分析日志:🌷自动化化分析日志: (1)360星图.(支持 iis /...
      继续阅读 »


      💛不登上悬崖💚,💛又怎么领略一览众山的绝顶风光💚
      🍪目录:

      🌲应急响应的概括:

      🌲应急响应阶段:

      🌲应急响应准备工作:

      🌲从入侵面及权限面进行排查:

      🌲工具下载

      🌲应急响应的日志分析:

      🌷手动化分析日志:

      🌷自动化化分析日志:

      (1)360星图.(支持 iis / apache / nginx日志)

      (2)方便大量日志查看工具.

      🌷后门木马检测.

      (1)D盾_Web查杀. 

      🌲应急响应的概括:
      🌾🌾🌾应急响应”对应的英文是“Incident Response”或“Emergency Response”等,通常是指一个组织为了应对各种意外事件的发生所做的准备以及在事件发生后所采取的措.
      🌾🌾🌾网络安全应急响应:针对已经发生的或可能发生的安全事件进行监控、分析、协调、处理、保护资产安全.

      🌲应急响应阶段:

      保护阶段:断网,备份重要文件(防止攻击者,这些期间删除文件重要文件.)

      分析阶段:分析攻击行为,找出相应的漏洞.

      复现阶段:复现攻击者攻击的过程,有利于了解当前环境的安全问题和安全检测.

      修复阶段:对相应的漏洞提出修复.

      建议阶段:对漏洞和安全问题提出合理解决方案.
      目的:分析出攻击时间,攻击操作,攻击后果,安全修复等并给出合理解决方案

      🌲应急响应准备工作:
      (1)收集目标服务器各类信息.

      (2)部署相关分析软件及平台等.

      (3)整理相关安全渗透工具指纹库.

      (4)针对异常表现第一时间触发思路. 

      🌲从入侵面及权限面进行排查:
      有明确信息网站被入侵: 1.基于时间 2.基于操作 3.基于指纹 4.基于其他.


      无明确信息网站被入侵:(1)WEB 漏洞-检查源码类别及漏洞情况.
      (2)中间件漏洞-检查对应版本及漏洞情况.
      (3)第三方应用漏洞-检查是否存在漏洞应用.
      (4)操作系统层面漏洞-检查是否存在系统漏洞.
      (5)其他安全问题(口令,后门等)- 检查相关应用口令及后门扫描.


      🌲工具下载   链接:https://pan.baidu.com/s/14njkNfj3HisIKN26IYOZXQ 
                          提取码:tian 




      🌲应急响应的日志分析:


      🌷手动化分析日志:


      (1)弱口令的爆破日志.(可以看到是一个IP在同一个时间,使用多个账号和密码不停测试)





      (2)SQL注入的日志.(搜索 select 语句.)


      (3)有使用SQLmap工具的注入.(搜索SQLmap)


      我的靶场日志没有记录SQLmap.(这里就不加图了)


           


      (4)目录扫描日志.(看的时候会发现,前面的目录都是一样的.)




           


      (5)XSS攻击日志.(搜索:script,javascript,onclick,%3Cimg对这些关键字进行查看)
      (7)目录遍历攻击日志.


      (8)后门木马日志.(搜索连接工具:anTSword,菜刀,冰蝎等工具 排查后门.)


             


             


      🌷自动化化分析日志:


      (1)360星图.(支持 iis / apache / nginx日志


      1.设置日志分析路径.
      2.点击进行日志分析.


          


      3.点击查看日志.




          


      安全分析报告.




      常规分析报告.


           


      (2)方便大量日志查看工具.


      1.工具的设置.




      2.SQL注入攻击日志.


      3.目录遍历攻击.


      4.XSS攻击日志.


            


      🌷后门木马检测.


      (1)D盾_Web查杀.


      1.选择扫描的目录.




      2.扫描到的后门木马.












      收起阅读 »

      堆(优先级队列)

       目录 🥬堆的性质 🥬堆的分类  🥬堆的向下调整 🥬堆的建立 🥬堆得向上调整 🥬堆的常用操作 🍌入队列 🍌出队列 🍌获取队首元素 🥬TopK 问题 🥬堆的性质 堆逻辑上是一棵完全二叉树,堆物理上是保存在数组中 。总...
      继续阅读 »


       目录


      🥬堆的性质


      🥬堆的分类


       🥬堆的向下调整


      🥬堆的建立


      🥬堆得向上调整


      🥬堆的常用操作


      🍌入队列


      🍌出队列


      🍌获取队首元素


      🥬TopK 问题



      🥬堆的性质




      堆逻辑上是一棵完全二叉树,堆物理上是保存在数组中 。

      总结:一颗完全二叉树以层序遍历方式放入数组中存储,这种方式的主要用法就是堆的表示。
      并且 如果已知父亲(parent) 的下标,则:
      左孩子(left) 下标 = 2 * parent + 1;
      右孩子(right) 下标 = 2 * parent + 2;
      已知孩子(不区分左右)(child)下标,则:
      双亲(parent) 下标 = (child - 1) / 2;
      🥬堆的分类
      大堆:根节点大于左右两个子节点的完全二叉树 (父亲节点大于其子节点),叫做大堆,或者大根堆,或者最大堆 。 


      小堆:根节点小于左右两个子节点的完全二叉树叫
      小堆(父亲节点小于其子节点),或者小根堆,或者最小堆。

      🥬堆的向下调整

      现在有一个数组,逻辑上是完全二叉树,我们通过从根节点开始的向下调整算法可以把它调整成一个小堆或者大堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

      以小堆为例:

      1、先让左右孩子结点比较,取最小值。

      2、用较小的那个孩子结点与父亲节点比较,如果孩子结点<父亲节点,交换,反之,不交换。

      3、循环往复,如果孩子结点的下标越界,则说明已经到了最后,就结束。

      //parent: 每棵树的根节点
      //len: 每棵树的调整的结束位置

      public void shiftDown(int parent,int len){
      int child=parent*2+1; //因为堆是完全二叉树,没有左孩子就一定没有右孩子,所以最起码是有左孩子的,至少有1个孩子
      while(child<len){
      if(child+1<len && elem[child]<elem[child+1]){
      child++;//两孩子结点比较取较小值
      }
      if(elem[child]<elem[parent]){
      int tmp=elem[parent];
      elem[parent]=elem[child];
      elem[child]=tmp;
      parent=child;
      child=parent*2+1;
      }else{
      break;
      }
      }
      }


      🥬堆的建立
      给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆(左右子树不满足都是大堆或者小堆),现在我们通过算法,把它构建成一个堆(大堆或者小堆)。该怎么做呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。 这里我们就要用到刚才写的向下调整。

      public void creatHeap(int[] arr){
      for(int i=0;i<arr.length;i++){
      elem[i]=arr[i];
      useSize++;
      }
      for(int parent=(useSize-1-1)/2;parent>=0;parent--){//数组下标从0开始
      shiftDown(parent,useSize);
      }
      }



      建堆的空间复杂度为O(N),因为堆为一棵完全二叉树,满二叉树也是一种完全二叉树,我们用满二叉树(最坏情况下)来证明。


      🥬堆得向上调整


      现在有一个堆,我们需要在堆的末尾插入数据,再对其进行调整,使其仍然保持堆的结构,这就是向上调整。


      以大堆为例:




      代码示例:

      public void shiftup(int child){
      int parent=(child-1)/2;
      while(child>0){
      if(elem[child]>elem[parent]){
      int tmp=elem[parent];
      elem[parent]=elem[child];
      elem[child]=tmp;
      child=parent;
      parent=(child-1)/2;
      }else{
      break;
      }
      }
      }



      🥬堆的常用操作


      🍌入队列


      往堆里面加入元素,就是往最后一个位置加入,然后在进行向上调整

      public boolean isFull(){
      return elem.length==useSize;
      }

      public void offer(int val){
      if(isFull()){
      elem= Arrays.copyOf(elem,2*elem.length);//扩容
      }
      elem[useSize++]=val;
      shiftup(useSize-1);
      }



      🍌获取队首元素



      public int peek() {
      if (isEmpty()) {
      throw new RuntimeException("优先级队列为空");
      }
      return elem[0];
      }


      🥬TopK 问题

      给你6个数据,求前3个最大数据。这时候我们用堆怎么做的?

      解题思路:

      1、如果求前K个最大的元素,要建一个小根堆。
      2、如果求前K个最小的元素,要建一个大根堆。
      3、第K大的元素。建一个小堆,堆顶元素就是第K大的元素。
      4、第K小的元素。建一个大堆,堆顶元素就是第K大的元素。

      🍌举个例子:求前n个最大数据

       
      代码示例:

      public static int[] topK(int[] array,int k){
      //创建一个大小为K的小根堆
      PriorityQueue<Integer> minHeap=new PriorityQueue<>(k, new Comparator<Integer>() {
      @Override
      public int compare(Integer o1, Integer o2) {
      return o1-o2;
      }
      });
      //遍历数组中元素,将前k个元素放入队列中
      for(int i=0;i<array.length;i++){
      if(minHeap.size()<k){
      minHeap.offer(array[i]);
      }else{
      //从k+1个元素开始,分别和堆顶元素比较
      int top=minHeap.peek();
      if(array[i]>top){
      //先弹出后存入
      minHeap.poll();
      minHeap.offer(array[i]);
      }
      }
      }
      //将堆中元素放入数组中
      int[] tmp=new int[k];
      for(int i=0;i< tmp.length;i++){
      int top=minHeap.poll();
      tmp[i]=top;
      }
      return tmp;
      }

      public static void main(String[] args) {
      int[] array={12,8,23,6,35,22};
      int[] tmp=topK(array,3);
      System.out.println(Arrays.toString(tmp));
      }



      结果:


      🍌数组排序


       再者说如果要对一个数组进行从小到大排序,要借助大根堆还是小根堆呢?


      ---->大根堆


        代码示例:

      public void heapSort(){
      int end=useSize-1;
      while(end>0){
      int tmp=elem[0];
      elem[0]=elem[end];
      elem[end]=tmp;
      shiftDown(0,end);//假设这里向下调整为大根堆
      end--;
      }
      }



      🥬小结


      以上就是今天的内容了,有什么问题可以在评论区留言✌✌✌



      收起阅读 »

      直播APP开发搭建,直播APP源码涉及方方面面

      最近直播APP源码的热度持续上升,究其原因还是因为直播市场的繁荣。直播APP开发搭建到底能做哪些事呢?1. 主播用户开启手机直播 可以设置只有签约主播才能开启手机直播,也可以设置所有注册用户都有权限 开启手机直播的时候可以选择手机前后手机摄像头设备,也可以在直...
      继续阅读 »

      最近直播APP源码的热度持续上升,究其原因还是因为直播市场的繁荣。

      直播APP开发搭建到底能做哪些事呢?

      1. 主播用户开启手机直播 可以设置只有签约主播才能开启手机直播,也可以设置所有注册用户都有权限 开启手机直播的时候可以选择手机前后手机摄像头设备,也可以在直播过程中进行切换。

      2. 玩家用户观看手机直播 玩家可以通过手机APP观看主播的直播。

      3. 用户之间聊天互动 用户可以通过聊天窗口进行文字和表情的聊天,聊天部分使用node.js实现。

      4. 用户之间礼物赠送 玩家可以充值后购买赠送礼物给主播,用户和主播分别可以在用户中心可以看到自己赠送和获得礼物的详细列表。

      播放列表

      直播APP开发的媒体播放器提供下面的方法来访问播放列表中的剪辑:

      Next 方法,跳到节目(播放列表)中的下一个剪辑;

      Previous 方法,跳回到节目中的上一个剪辑;

      媒体播放器的一个特性是能够预览节目中的每一个剪辑,使用如下属性:

      PreviewMode 属性,决定媒体播放器当前是否处于预览模式;

      直播APP开发搭建,直播APP源码涉及方方面面

      了解流媒体(直播APP开发搭建需要用到流媒体)

      流媒体开发:网络层(socket或st)负责传输,协议层(rtmp或hls)负责网络打包,封装层(flv、ts)负责编解码数据的封装,编码层(h.264和aac)负责图像,音频压缩。

      帧:每帧代表一幅静止的图像。

      GOP:(Group of Pictures)画面组,一个GOP就是一组连续的画面,每个画面都是一帧,一个GOP就是很多帧的集合。

      直播的数据,其实是一组图片,包括I帧、P帧、B帧,当用户第一次观看的时候,会寻找I帧,而播放器会到服务器寻找到最近的I帧反馈给用户。因此,GOP Cache增加了端到端延迟,因为它必须要拿到最近的I帧。

      直播APP的开发说难也难,说容易也相当容易。

      难,是因为搞个直播要考虑的东西太多了,根本不是一个人能够搞定的。因为直播中运用到的技术难点非常之多,视频/音频处理,图形处理,视频/音频压缩,CDN分发,即时通讯等技术,每一个技术都够你学几年的。

      从采集编码传输到解码播放这个过程要处理的东西太多了:单就视频的采集来说就涉及两方面数据的采集:音频采集和图像采集,涉及到各种算法,这是一个很庞大的工作量。


      收起阅读 »

      短视频源码开发,短视频源码应该从哪些方面进行优化?

      短视频作为更加符合移动互联网时代用户触媒习惯的视频内容形式,在内容上和功能上本身就具有很大的想象空间。通过“短视频+”的方式现在有不少平台上搭建和嵌入短视频源码,是一个不错的入局途径。短视频压缩短视频的压缩问题是短视频源码的难点之一。视频拍摄、上传完成后,要在...
      继续阅读 »

      短视频作为更加符合移动互联网时代用户触媒习惯的视频内容形式,在内容上和功能上本身就具有很大的想象空间。通过“短视频+”的方式现在有不少平台上搭建和嵌入短视频源码,是一个不错的入局途径。

      短视频压缩

      短视频的压缩问题是短视频源码的难点之一。视频拍摄、上传完成后,要在不影响用户体验的情况下完成短视频帧率的统一、格式统一、分辨率处理、短视频压缩等处理。

      短视频不进行压缩处理的情况下上传,会造成资源的浪费,对运营商来说,长期以往的带宽浪费会非常费钱。

      实现秒播

      短视频app源码中的短视频列表在打开时,就会主动扣留列表中的最后一个视频到内存中,然后再对其他视频进行预加载,当进行下拉刷新操作时,会将上次扣留的短视频作为刷新后的第一个视频进行展示,再去预加载其他视频内容,通过这样的方式,优化小视频app源码中短视频秒播的问题。

      短视频源码开发,短视频源码应该从哪些方面进行优化?

      大数据分析

      短视频源码的数据分析功能非常重要,在推荐上、获取用户喜好上都能发挥很大的作用。百万级的用户,想要做到精准的推荐,就离不开对用户数据的分析,通过平时观看的喜好,充分了解用户的观看习惯。

      短视频的录制

      短视频录制功能通常能够设置视频的录制时长,可以录制的时间长一点或者短一点,配置各类的视频参数,像视频分辨率、码率等等。此外,短视频之所以这么火爆,还在于短视频中加了很多特效和滤镜,像有一个可爱的、搞怪的、清新的等等,还有很多动态的贴纸,面具,动态的在线染发,更关键的话还有很多小游戏,让人沉迷在短视频中。

      除了以上短视频源码开发功能之外,管理后台也在全套方案中扮演着重要的角色。除了能够配置APP客户端相对应的功能外,还需要有良好的操作体验,方便短视频平台运营商的各项实际操作。

      消息也是短视频源码当中重要的一环,是内容互动的前提条件。除了系统及时推送消息之外,粉丝和主播之间、粉丝和粉丝之间都可以进行私信的互动,也可以针对自己喜欢的视频进行评论。

      收起阅读 »