注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

清除 useEffect 副作用

web
在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。import { useState, useEffect } f...
继续阅读 »

在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   const id = setInterval(async () => {
     const data = await fetchData();
     setList(list => list.concat(data));
  }, 2000);
   return () => clearInterval(id);
}, [fetchData]);

 return list;
}

🐚 问题

该方法的问题在于没有考虑到 fetchData() 方法的执行时间,如果它的执行时间超过 2s 的话,那就会造成轮询任务的堆积。而且后续也有需求把这个定时时间动态化,由服务端下发间隔时间,降低服务端压力。

所以这里我们可以考虑使用 setTimeout 来替换 setInterval。由于每次都是上一次请求完成之后再设置延迟时间,确保了他们不会堆积。以下是修改后的代码。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   async function getList() {
     const data = await fetchData();
     setList(list => list.concat(data));
     id = setTimeout(getList, 2000);
  }
   getList();
   return () => clearTimeout(id);
}, [fetchData]);

 return list;
}

不过改成 setTimeout 之后会引来新的问题。由于下一次的 setTimeout 执行需要等待 fetchData() 完成之后才会执行。如果在 fetchData() 还没有结束的时候我们就卸载组件的话,此时 clearTimeout() 只能无意义的清除当前执行时的回调,fetchData() 后调用 getList() 创建的新的延迟回调还是会继续执行。

在线示例:CodeSandbox


可以看到在点击按钮隐藏组件之后,接口请求次数还是在继续增加着。那么要如何解决这个问题?以下提供了几种解决方案。

🌟如何解决

🐋 Promise Effect

该问题的原因是 Promise 执行过程中,无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录,而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout,保证我们每次都能确切的清除掉任务。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let getListPromise;
   async function getList() {
     const data = await fetchData();
     setList((list) => list.concat(data));
     return setTimeout(() => {
       getListPromise = getList();
    }, 2000);
  }

   getListPromise = getList();
   return () => {
     getListPromise.then((id) => clearTimeout(id));
  };
}, [fetchData]);
 return list;
}

🐳 AbortController

上面的方案能比较好的解决问题,但是在组件卸载的时候 Promise 任务还在执行,会造成资源的浪费。其实我们换个思路想一下,Promise 异步请求对于组件来说应该也是副作用,也是需要”清除“的。只要清除了 Promise 任务,后续的流程自然不会执行,就不会有这个问题了。

清除 Promise 目前可以利用 AbortController 来实现,我们通过在卸载回调中执行 controller.abort() 方法,最终让代码走到 Reject 逻辑中,阻止了后续的代码执行。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

function fetchDataWithAbort({ fetchData, signal }) {
 if (signal.aborted) {
   return Promise.reject("aborted");
}
 return new Promise((resolve, reject) => {
   fetchData().then(resolve, reject);
   signal.addEventListener("aborted", () => {
     reject("aborted");
  });
});
}
function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   const controller = new AbortController();
   async function getList() {
     try {
       const data = await fetchDataWithAbort({ fetchData, signal: controller.signal });
       setList(list => list.concat(data));
       id = setTimeout(getList, 2000);
    } catch(e) {
       console.error(e);
    }
  }
   getList();
   return () => {
     clearTimeout(id);
     controller.abort();
  };
}, [fetchData]);

 return list;
}

🐬 状态标记

上面一种方案,我们的本质是让异步请求抛错,中断了后续代码的执行。那是不是我设置一个标记变量,标记是非卸载状态才执行后续的逻辑也可以呢?所以该方案应运而生。

定义了一个 unmounted 变量,如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   let unmounted;
   async function getList() {
     const data = await fetchData();
     if(unmounted) {
       return;
    }

     setList(list => list.concat(data));
     id = setTimeout(getList, 2000);
  }
   getList();
   return () => {
     unmounted = true;
     clearTimeout(id);
  }
}, [fetchData]);

 return list;
}

🎃 后记

问题的本质是一个长时间的异步任务在过程中的时候组件卸载后如何清除后续的副作用。

这个其实不仅仅局限在本文的 Case 中,我们大家平常经常写的在 useEffect 中请求接口,返回后更新 State 的逻辑也会存在类似的问题。

只是由于在一个已卸载组件中 setState 并没有什么效果,在用户层面无感知。而且 React 会帮助我们识别该场景,如果已卸载组件再做 setState 操作的话,会有 Warning 提示。


再加上一般异步请求都比较快,所以大家也不会注意到这个问题。

所以大家还有什么其他的解决方法解决这个问题吗?欢迎评论留言~


作者:lizheming
链接:juejin.cn/post/7057897311187238919

收起阅读 »

如何用一个插件解决 Serverless 灰度发布难题?

web
Serverless 灰度发布什么是 Serverless ?Serverless 顾名思义就是无服务器,它是一种“来了就用,功能齐全,用完即走”的全新计算提供方式,用户无需预制或管理服务器即可运行代码,只需将代码从部署在服务器上,转换到部署到各厂商 Serv...
继续阅读 »

Serverless 灰度发布

什么是 Serverless ?

Serverless 顾名思义就是无服务器,它是一种“来了就用,功能齐全,用完即走”的全新计算提供方式,用户无需预制或管理服务器即可运行代码,只需将代码从部署在服务器上,转换到部署到各厂商 Serverless 平台上;同时享受 Serverless 按需付费,高弹性,低运维成本,事件驱动,降本提效等优势。

什么是 Serverless 灰度发布?

灰度发布又称为金丝雀发布( Canary Deployment )。过去,矿工们下矿井前,会先放一只金丝雀到井内,如果金丝雀在矿井内没有因缺氧、气体中毒而死亡后,矿工们才会下井工作,可以说金丝雀保护了工人们的生命。

与此类似,在软件开发过程中,也有一只金丝雀,也就是灰度发布(Gray release):开发者会先将新开发的功能对部分用户开放,当新功能在这部分用户中能够平稳运行并且反馈正面后,才会把新功能开放给所有用户。金丝雀发布就是从不发布,然后逐渐过渡到正式发布的一个过程。

那么对于部署在 Serverless 平台上的函数应该怎么进行灰度发布呢?

下文将以阿里云函数计算 FC 为例,为各位展开介绍。

灰度发布有一个流程,两种方式。

一个流程

Serverless 灰度发布是通过配置别名来实现的,别名可以配置灰度版本和主版本的流量比例,在调用函数时使用配置好的别名即可将流量按比例发送到相应版本。


配置灰度发布的流程如下:

  1. Service 中发布一个新版本。

  2. 创建或更新别名,配置别名关联新版本和稳定版本,新版本即为灰度版本。

  3. 将触发器 ( Trigger ) 关联到别名。

  4. 将自定义域名 ( Custom Domain ) 关联到别名。

  5. 在调用函数中使用别名,流量会按配置比例发送到新版本和稳定版本。

传统做法的两种方式

1、阿里云控制台 web 界面:

a.发布版本


b.创建别名


c.关联触发器


d.关联自定义域名


2、使用 Serverless Devs cli

a.发布版本

s cli fc version publish --region cn-hangzhou --service-name fc-deploy-service --description "test publish version"

b.创建别名并设置灰度

s cli fc alias publish --region cn-hangzhou --service-name fc-deploy-service --alias-name pre --version-id 1 --gversion 3 --weight 20

c.关联触发器

需要到控制台配置

d.关联自定义域名

需要到控制台配置可以看到,使用控制台或 Serverless Devs 进行灰度发布流程中的每一步,都需要用户亲自操作。并且由于配置繁多,极易出错。除了这些弊端以外,客户困扰的另一个问题是使用灰度发布策略非常不方便。

常见的灰度发布策略有 5 种:

    1. CanaryStep: 灰度发布,先灰度指定流量,间隔指定时间后再灰度剩余流量。

    2. LinearStep:分批发布,每批固定流量,间隔指定时间后再开始下一个批次。

    3. CanaryPlans:自定义灰度批次,每批次设定灰度流量和间隔时间,间隔指定时间后按照设定的流量进行灰度。

    4. CanaryWeight:手动灰度,直接对灰度版本设置对应的权重。

    5. FullWeight: 全量发布,全量发布到某一版本。

这些灰度策略中,前三项都需要配置间隔时间,而用户在控制台或者使用 Serverless Devs 工具去配置灰度都没有办法通过自动程序来配置间隔时间,不得不通过闹钟等方式提醒用户手动进行下一步灰度流程,这个体验是非常不友好的。下面我们介绍个能够帮您一键灰度发布函数的插件:FC-Canary 。

基于 Serverless Devs 插件 FC-Canary 的灰度发布

为了应对以上问题,基于 Serverless Devs 的插件 FC-Canary 应运而生,该插件可以帮助您通过 Serverless-Devs 工具和 FC 组件实现函数的灰度发布能力,有效解决灰度发布时参数配置繁杂、需要开发人员亲自操作以及可用策略少等问题。


(内容配置及注意事项-部分截图)

详细流程请见:github.com/devsapp/fc-…

FC-Canary 的优势

1、FC-Canary 支持超简配置

用户最短只需在 s.yaml 中增加 5 行代码代码即可开启灰度发布功能。


2、FC-Canary 配置指引简单清晰:


3、FC-Canary 支持多种灰度策略

  • 灰度发布,先灰度指定流量,间隔指定时间后再灰度剩余流量。

此时流量变化为:20%流量到新版本,10 分钟后 100%流量到新版本


  • 手动灰度,指定时直接将灰度版本设置对应的权重。

此时为 10%流量到新版本,90%到稳定版本


  • 自定义灰度,以数组的方式配置灰度变化。

此时流量变化为:10%到新版本 -> (5 分钟后) 30% 流量到新版本 -> (10 分钟后) 100% 流量到新版本


  • 分批发布,不断累加灰度比例直到 100%流量到新版本。

流量变化:40%到新版本 -> (10 分钟后) 80%流量到新版本 -> (再 10 分钟后) 100%流量到新版本


  • 全量发布,100%流量发到新版本


FC-Canary 插件支持上述 5 种灰度策略,用户选择所需策略并进行简单配置,即可体验一键灰度发布。

4、FC-Canary 灰度阶段提示清晰


插件对每一个里程碑都会以 log 的方式展现出来,给开发者足够的信心。

5、FC-Canary 支持钉钉群组机器人提醒


配置钉钉机器人即可在群中收到相关提醒,例如:


FC-Canary 最佳实践

使用 FC-Canary 插件灰度发布 nodejs 12 的函数。

代码仓库:

github.com/devsapp/fc-…

初始化配置

  • 代码配置


  • yaml 配置


我们采用 canaryWeight 的灰度策略:灰度发布后,50%的流量到新版本,50%的流量到旧版本。

进行第一次发布

  1. 执行发布

在 terminal 中输入: s deploy --use-local

  1. 查看结果

命令行输出的 log 中可以看到:


由于是第一次发布,项目中不存在历史版本,所以即使配置了灰度发布策略 ,FC-Canary 插件也会进行全量发布,即流量都发送到版本 1。

修改代码,第二次发布

  1. 在第二次发布前,我们修改一下代码,让代码抛出错误。


  1. 执行发布

在terminal中输入: s deploy --use-local

  1. 结果

命令行输出 log 中可以看到:


第二次发布,应用了灰度发布策略,即 50%流量发送到版本 1, 50%的流量发送到版本 2。

测试

获取 log 中输出的 domain,访问 domain 100 次后查看控制台监控大盘。


可以看到调用了函数 100 次,错误的函数有 49 次,正确的函数有 100 - 49 = 51 次,正确和错误的函数都约占总调用数的 50%。

分析:函数版本 1 为正确函数,函数版本 2 为错误函数,我们的灰度配置为流量 50% 到版本 1,50% 到版本 2,所以调用过程中,错误函数和正确函数应该各占 50%,图中结果符合我们的假设,这证明我们的灰度策略是成功的。

总结

我们可以发现相比使用控制台进行灰度发布,使用 FC-Canary 插件免去了用户手动创建版本、发布别名、关联触发器和管理自定义域名的麻烦,使用起来非常方便。

引申阅读

Serverless Devs 组件和插件的关系

  • 组件是什么?

根据 Serverless Devs Model v0.0.1 中说明, 组件 Component: 是由 Package developer 开发并发布的符合 Serverless Package Model 规范的一段代码,通常这段代码会在应用中被引用,并在 Serverless Devs 开发者工具中被加载,并按照预定的规则进行执行某些动作。例如,将用户的代码部署到 Serverless 平台;将 Serverless 应用进行构建和打包;对 Serverless 应用进行调试等。

举个例子:

如果想要使用 Serverless Devs 管理阿里云函数计算的函数计算资源,则需要在 yaml 配置文件中声明阿里云 FC 组件,之后便可以使用阿里云 FC 组件的能力。

FC 组件可以提供管理阿里云函数计算资源的能力,包括:管理服务、函数、版本、别名 等功能。组件地址:github.com/devsapp/fc

  • 插件是什么?

插件作为组件的补充,提供组件的原子性功能。

举个例子:

  1. 使用 FC 组件 deploy 的功能部署函数,可以在部署结束后采用 FC-Canary 插件对部署的函数进行灰度发布。

  2. 使用 FC 组件 deploy 的功能部署函数,可以在部署开始前采用 layer-fc 插件来降低部署过程中上传代码的耗时:即 layer-fc 可以让函数直接使用公共依赖库(远程)中的依赖,从而在部署时不再需要上传这些远程存在的依赖。

  • 组件和插件的关系?


  • 在 Serverless Devs Model 中,组件是占据核心地位,插件是辅助地位,也就是说,插件的目的是提升组件能力,提供给组件一些可选的原子性功能。

  • Serverless Devs 管理组件和插件的生命周期,如果是 pre 插件,则会让其在组件执行前执行,反之,post 插件则会在组件后完成一些收尾工作。

  • 一个组件可以同时使用多个插件, 其中组件插件的执行顺序是:

    1. 插件按照 yaml 顺序执行, 前一个插件的执行结果为后一个插件的入参

    2. 最后一个 pre 插件的输出作为组件的入参

    3. 组件的输出作为第一个 post 插件的入参

相关概念

  • FC 函数 (Function) 是系统调度和运行的单位,由函数代码和函数配置构成。FC 函数必须从属于服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。函数的相关操作,请参见 管理函数。函数计算支持事件函数和 HTTP 函数两种函数类型,关于二者的区别,请参见 函数类型。

  • 服务 (Service) 可以和微服务对标 ( 有版本和别名 ),多个函数可以共同组成服务单元。创建函数前必须先创建服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。

  • 触发器 (Trigger) 的作用是触发函数执行的。函数计算提供了一种事件驱动的计算模型。函数的执行可以通过函数计算控制台或 SDK 触发,也可以由其他一些事件源来触发。您可以在指定函数中创建触发器,该触发器描述了一组规则,当某个事件满足这些规则,事件源就会触发关联的函数。

  • 自定义域名(Custom Domain) 是函数计算提供为 Web 应用绑定域名的能力。

  • 版本 (Version) 是服务的快照,包括服务的配置、服务内的函数代码及函数配置,不包括触发器,当发布版本时,函数计算会为服务生成快照,并自动分配一个版本号与其关联,以供后续使用。

  • 别名 (Alias) 结合版本,帮助函数计算实现软件开发生命周期中的持续集成和发布。

最后,欢迎大家一起来贡献更多的开源插件!

参考链接:

Serverless Devs:

github.com/Serverless-…

FC 组件地址:

github.com/devsapp/fc

FC-Canary 插件具体信息及其使用请参考:

github.com/devsapp/fc-…

FC 函数管理:

help.aliyun.com/document_de…

FC 函数类型:

help.aliyun.com/document_de…


作者:长淇
来源:https://juejin.cn/post/7116556273662820382

收起阅读 »

Java VS .NET:Java与.NET的特点对比

为什么要写Java跟.NET对比?二、项目构建工欲善其事必先利其器。开发环境配置+工具使用当然要先讲了。平台工具ken.io的解释JavaIdea/EclipseIDE,负责管理项目以及代码的运行调试等,依赖于JDKJavaJDKJRE(Java项目运行环境)...
继续阅读 »

一、前言

为什么要写Java跟.NET对比?

.NET出生之后就带着Java的影子。从模仿到创新,.NET平台也越来越成熟。他们不同的支持者也经常因为孰弱孰强的问题争论不休。但是本文并不是为了一分高下。而是针对Java平台跟.NET平台做一些对比。主要围绕项目构建、Web框架、项目部署展开讨论。相信经过这些讨论可以让Java/.NET工程师对Java平台、.NET平台有更好的了解。

二、项目构建

项目构建工具

工欲善其事必先利其器。开发环境配置+工具使用当然要先讲了。

1、表面上的工具

平台工具ken.io的解释

.NETVisual Studio微软官方IDE,它具备了开发.NET应用程序的几乎所有工具

JavaIdea/EclipseIDE,负责管理项目以及代码的运行调试等,依赖于JDK

JavaMaven负责管理项目模板、打包(jar包等),依赖于JDK

JavaJDKJRE(Java项目运行环境),Java工具(编译器等)

.NET工程师要开展工作,安装Visual Studio(后面简称:VS)就可以进行开发了。但是Java开发,只安装IDE是不行的,就算某些IDE会自动安装JDK,甚至是Maven,但是这些还是需要自己配置,不然还可能会踩坑。从开发环境的配置来说,.NET工程师操作上确实简单一些,一直下一步,等待安装完成即可。Java工程师就先要了解下工具,以及各个工具的职责。然后逐一配置。

从这个点上来说,Java的入门曲线会稍陡一些,但是Java工程师也会比.NET工程师更早关注到项目构建的重要环节。

2、实际上的工具

职责.NET平台Java平台ken.io的解释

项目管理VSIDEA/Eclips.NET只有微软官方IDE,Java没有官方的IDE,没有VS好用,但是有多个选择

项目模板VS+MSBuildIDE+Maven.NET项目的模板是VS自带的,是直接符合MSBuild(编译器)标准的,项目由sln+csproj文件组织,Java平台编译器的标准是公开的,目前主流项目都是基于Maven模板来创建,项目由pom.xml文件组织。

编译&调试VS+MSBuild+SDKIDE+Maven+SDK.NET平台的编译器是独立的,Java平台的编译器是集成在JDK中,Maven模板的项目是由pom.xml文件组织,但是编译器并不是认识pom.xml,所以编译需要Maven的参与

Package管理NuGetMavenNuget是微软官方开源的VS插件,Maven是Apache下的开源项目。ken.io觉得Maven更灵活、强大。NuGet容易上手。

打包/发布VS+MSBuild+SDKIDE+Maven+SDK.NET平台的编译器是独立的,Java平台的编译器是集成在JDK中,Maven模板的项目是由pom.xml文件组织的,但是编译器并不是认识pom.xml,所以打包需要Maven的参与。IDE主要是提供图形化界面替代命令操作

从项目管理上说。VS这个IDE更好用一些,项目模板上,.NET项目模板由于有Visual Studio的存在,可以说简单易用而且丰富,Java平台的Maven模板灵活。

其实大部分差异都是编译器跟模板带来的差异。.NET平台的编译器是独立的,编译器MSBuild有一套标准, 而且Visual Studio提供了丰富好用的项目模板。

Java平台的编译器的编译配置是xml文档,由于Java官方没有项目模板,IDE只负责帮你组织项目,但是并没有模板,你可以将任意目录指定为SourceRoot(代码根目录),ResourceRoot(资源文件根目录:比如配置文件)也可以任意指定,编译的时候,IDE会将你的项目代码,以及编译器所需要的编译描述/配置xml文档告诉编译器该如何编译你的项目。确实非常灵活,但是也增加了项目管理的成本。包的管理也非常麻烦,还好有Maven结束了这个混沌的Java世界。

编码特点

—.NETJava

类的组织namespace:命名空间,name跟目录无关Package:name跟目录名一致

类.cs文件:类名跟文件名无关.java文件,类名跟文件名无关,但一个类文件只能定义一个public类

编译产出.dll,.exe文件.jar,.war文件

三、框架

.NET的Web框架基本上都是微软官方的,官方的框架也最为流行,而Java平台,除了官方提供的Servlet API(相当于.NET的System.Web)其他的基本都由Spring大家族统治了。本次我们主要对比目前Web开发最常用的MVC框架以及持久层框架

功能.NETJavaken.io的说明

Web核心ASP.NETServlet—

Web框架ASP.NET MVCSpring MVCASP.NET MVC是微软官方框架,Srping MVC框架隶属于Spring大家族,依赖于Spring

视图引擎RazorThymeleaf/FreeMarkerRazor是微软官方的视图引擎,非常好用,Spring MVC并没有视图引擎,但是有Thymeleaf,FreeMarker。ken.io更喜欢Razor的风格

持久层Entity FrameworkMyBatisEF是微软官方的持久层框架,易上手、开发效率高、但侵入性强。MyBatis配置灵活,无侵入性。各有利弊。

.NET平台的框架由于都是微软官方的,比较好组织,上手容易。Java平台的框架,灵活可配置。这也是Java平台一贯的风格。但是ken.io不得不吐槽的是,Spring MVC作为一个MVC框架,竟然没有自己的视图引擎,那MVC种的View去哪了?

可能是因为Java作为Web后端的主力平台,确实很少关注视图层,但是Spring MVC没有View层引擎,还是感觉不合适。Thymeleaf跟FreeMarker,ken.io更推荐FreeMarker。因为ken.io更喜欢FreeMaker的语法。可能是用惯了Razor的缘故。

四、项目部署

对于项目部署。.NET平台貌似没得选,只能选Windows+IIS,虽然有Mono,但毕竟不是支持所有的类库。而Java平台既可以选择Windows+Tomcat,也可以选择Linux+Tomcat。但是通常会选择Linux+Tomcat毕竟成本低。

职责.NETJava

操作系统Windows ServerWindows Server、Linux Server

Web服务器IISTomcat(Tomcat是目前最主流的,也有其他的Servlet容易例如:JBoss)

不过Java平台的特性,Java项目的部署会比.NET项目部署偏麻烦一些。

IIS图形化界面一直下一步,再调整下应用程序池的版本就行了。而Tomcat不论是在Windows,还是在Linux,都通过修改配置文件完成站点配置


转载自:https://cloud.tencent.com/developer/article/1926747

收起阅读 »

ASP.NET MVC 与 ASP.NET Web Form 的介绍与区别

是微软提供的以MVC模式为基础的ASP.NET Web应用程序开发框架。Model:领域模型 处理应用程序数据逻辑部分,获取数据,处理数据Controller:控制器 通过Model 读取处理数据,通过View 将结果返回。在 ASP.NET 框架下的一种基于...
继续阅读 »

1 ASP.NET MVC

是微软提供的以MVC模式为基础的ASP.NET Web应用程序开发框架。

MVC 模式分别为:

Model:领域模型 处理应用程序数据逻辑部分,获取数据,处理数据

View:视图 用于处理实际返回给用户的页面

Controller:控制器 通过Model 读取处理数据,通过View 将结果返回。

2 ASP.NET Webform

在 ASP.NET 框架下的一种基于事件模型的开发模式,有开发速度快,容易上手等特点。

3 两者的区别和各自优缺点

ASP.NET 作为微软的Web程序开发框架,MVC与Webform 是不同时期的开发模式,

在ASP.NET 运行处理原理 基本一致.

Webform 优点:可以基于事件模型开发,类似Winform中,所有请求使用ViewState和页面生命周期来维持控件状态,同时控件的开发,加快了开发速度,整体Webform的内部封装比较高。

Webform 缺点: 正是由于封装程度高,Webform非常难扩展,开发人员便利了解内部运行原理,不容易被测试。同时控件的ViewState 增加了网站服务器的传输量,一定程度上影响程序的效率。

MVC 优点 :易于扩展,易于单元测试,易于测试驱动开发。MVC中的一个路由的存在,可以做一些链接伪静态的处理。

总结: MVC 不是取代了Webform,两者适用于不同的开发环境下,都是简单三层中的表示层的开发框架,都是ASP.NET 框架下的开发模式。

1 页面处理流程:
MCV的页面处理流程依旧在ASP.NET原有上有扩展,MVC通过特定的IHttpModule和IHttpHandler 来处理请求,与Webform不同的,Webform中每个aspx页面都会有是一个IHttphandler实例。MVC中 Controller都比是IHttpHandler的子类实例,Action是在MvcHandler中通过MVC的工厂反射执行的,MvcHandler可以自定义。

2 上下文 请求对象: Context Session Request Response Cookie 基本一致

3 配置文件基本一致,但不通用

4 部分服务器控件并不是不可以在MVC中使用

5 在ASP.NET MVC中,包括Membership,healthMonitoring,httpModule,trace在内的内置和自定义的组件模块仍然是继续可用。

附图 :MVC 原理图和介绍

123123123.jpg

1 客户端发出请求给IIS(mvc中为集成模式),执行HttpRunTime的ProcessRequest方法

2 创建了一批MvcApplication对象,存放在应用程序池中,执行第一个MvcApplication对象实例中的 Application的Application_Start()方法、

RouteConfig.RegisterRountes(RouteTable.Routes)-->向路由规则集合注册一条默认的路由规则

3 调用Application对象实例的ProcessRequest方法 ,传入上下文对象HttpContext,开始执行19个管道事件

4 第七个管道事件:

UrlRouting过滤器:

1 获取当前Reuqest对象中的RawUrl:此时 /Home/Index

2 去扫描当前路由规则集合中的所,从上而下开始匹配,匹配成功了--{controller}/{action}/{id}这条路由规则,MVC底层就会根据路由规则解析出控制器名称

HomeController action:Index

3 调用DefaultControllerFactory反射创建控制器类的对象实例,存入RemapHandler中

4 将 控制器和action名称以字符串的形式存入RouteData中

5 第八个管道事件

1 判断当前RemapHandler是否为null, 不为null直接跳过执行后面的管道事件

2null继续创建页面类对象

6 第十一,十二个管道事件

1 获取上下文的RemapHandler中的控制器类的对象实例

2从RoutData中取出当前请求action名称

3 以反射的方式动态执行action方法

4 action返回类型分为:

4.1 如果是一个视图类型:调用具体的视图(.cshtml)编译成页面类,在调用页面类的Excute()方法,将所有的代码执行后写入到Response中

4.2如果是一个非视图类型,直接将结果写入到Response中即可
收起阅读 »

NodeJS 入门了解

1 NodeJS 是什么NodeJS 是 javascript 的一种运行环境,是对 Google V8 引擎进行的封装。是一个服务器端的 javascript 解释器;NodeJS 使用事件驱动,非阻塞 I/O 模型;什么是非阻塞 I/O 模型:阻塞:I/O...
继续阅读 »

1 NodeJS 是什么

  • NodeJS 是 javascript 的一种运行环境,是对 Google V8 引擎进行的封装。是一个服务器端的 javascript 解释器;
  • NodeJS 使用事件驱动,非阻塞 I/O 模型;

什么是非阻塞 I/O 模型:

  • 阻塞:I/O 时进程休眠等待 I/O 完成后再进行下一步;
  • 非阻塞 I/O :I/O 时函数立即返回,进程不等待 I/O 完成;

什么是事件驱动:
I/O 等异步操作结束后的通知。

2 NodeJS 和 npm 的关系

包含关系,NodeJS 中含有 npm,比如说你安装好 NodeJS,你打开 cmd 输入 npm -v 会发现出 npm 的版本号,说明 npm 已经安装好。

引用大神的总结:
其实 npm 是 NodeJS 的包管理器(package manager)。我们在 NodeJS 上开发时,会用到很多别人已经写好的 javascript 代码,如果每当我们需要别人的代码时,都根据名字搜索一下,下载源码,解压,再使用,会非常麻烦。

于是就出现了包管理器 npm。大家把自己写好的源码上传到 npm 官网上,如果要用某个或某些个,直接通过 npm 安装就可以了,不用管那个源码在哪里。并且如果我们要使用模块 A,而模块 A 又依赖模块 B,模块 B 又依赖模块 C 和 D,此时 npm 会根据依赖关系,把所有依赖的包都下载下来并且管理起来。试想如果这些工作全靠我们自己去完成会多么麻烦!

3 NodeJS 的安装

直接网上下载安装就可以了。环境配置,其实就是在 path,加入 NodeJS 的安装目录,这样就可以在控制台使用 NodeJS 的命令。验证,可以在控制台输入:node -vnpm -v


4 初始化 npm 环境

首先保证有 node 和 npm 环境,运行 node -vnpm -v 查看

进入项目目录,运行 npm init 按照步骤填写最终生成 package.json 文件,所有使用 npm 做依赖管理的项目,根目录下都会有一个这个文件,该文件描述了项目的基本信息以及一些第三方依赖项(插件)。详细的使用说明可查阅官网文档

5 安装插件

已知我们将使用 webpack 作为构建工具,那么就需要安装相应插件,运行 npm install webpack webpack-dev-server --save-dev 来安装两个插件。

又已知我们将使用 React ,也需要安装相应插件,运行 npm i react react-dom --save 来安装两个插件。其中 iinstall 的简写形式。

安装完成之后,查看 package.json 可看到多了 devDependenciesdependencies 两项,根目录也多了一个 node_modules 文件夹。

6 --save--save-dev 的区别

npm i 时使用 --save--save-dev,可分别将依赖(插件)记录到 package.json 中的 dependenciesdevDependencies 下面。

dependencies 下记录的是项目在运行时必须依赖的插件,常见的例如 reactjquery 等,即及时项目打包好了、上线了,这些也是需要用的,否则程序无法正常执行。

devDependencies 下记录的是项目在开发过程中使用的插件,例如这里我们开发过程中需要使用 webpack 打包,而我在工作中使用 fis3 打包,但是一旦项目打包发布、上线了之后,webpackfis3 就都没有用了,可卸磨杀驴。

延伸一下,我们的项目有 package.json,其他我们用的项目如 webpack 也有 package.json,见 ./node_modules/webpack/package.json,其中也有 devDependenciesdependencies。当我们使用 npm i webpack 时,./node_modules/webpack/package.json 中的dependencies 会被 npm 安装上,而 devDependencies 也没必要安装。

参考:http://www.imooc.com/article/14499

7 CommonJS

CommonJS 是 node 的模块管理规范

  • 每个文件都是一个模块,有自己的作用域;
  • 在模块内部 module 变量代表模块本身;
  • module.exports 属性代表模块对外接口;

require 规则

  • / 表示绝对路径,./ 表示相对路径;
  • 支持 js、json、node 扩展名,不写依次尝试;
  • 不写路径则认为是 build-in 模块或者各级 node_modules 内的第三方模块

require 特性

  • module 被加载的时候执行,加载后缓存;
  • 一旦出现某个模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出

8 global

node 全局对象 global,相当于 web 的 window 对象。

  • CommonJS
  • Buffer、process、console
  • timer
收起阅读 »

nodeJS操纵数据库

下载nodeJS,安装另外一种安装我们node的方式 使用nvm这个软件来安装 node version manger,如果你想同时安装多个node版本 教程:http://www.jianshu.com/p/07c3456e875a2、使用上面装好的n...
继续阅读 »

Node.exe的安装

下载nodeJS,安装

检测是否安装成功 node -v

另外一种安装我们node的方式
使用nvm这个软件来安装
node version manger,如果你想同时安装多个node版本
教程:http://www.jianshu.com/p/07c3456e875a

步骤:
1、安装nvm这个软件: https://github.com/coreybutler/nvm-windows/releases

2、使用上面装好的nvm软件,安装我们需要的node版本了
指令:
nvm install 具体的版本号就行了
nvm uninstall 具体的版本号
nvm list 查看当前安装了哪些版本
nvm use 具体版本号,切换到某个版本

建议:
安装一个高一点的稳定的版本即可,因为软件都是向下兼容

系统环境变量及其作用

系统环境变量

每个系统都会提供一种叫做环境变量的东西,用来简化我们去
访问某一个应用程序可执行文件(.exe)的操作

我们配置了环境变量能做到什么事呢?
在我们终端的任何一个目录下,都可以访问,配置在系统
环境变量里面的可执行文件

如何将一个软件的可执行文件配置在我们的系统环境变量中?
步骤:
1、拷贝一个可执行文件所在的目录,比如:
node.exe所在的目录 C:\Program Files\nodejs

2、系统 > 高级系统设置 > 高级 > 环境变量 >
系统变量 > Path > 填写上你的目录

注意事项:
如果更改了系统的环境变量,就必须把终端重新启动

启动node.exe执行js代码

启动(相当于启动Apache服务器)

1、在我们的node的安装目录下,去双击我们node.exe

2、在终端输入 node即可 node.exe

退出我们的node.exe

1、在终端中输入.exit
2、连续按住两次 CTRL + C

怎么去执行js代码

1、直接在我们启动的node.exe中写代码(在开启的REPL环境中写代码执行)
缺点:
书写不方便,阅读起来也不方便
因为在我们的cmd中写的代码,是放在内存中的,
一旦我们退出了node.exe,原先写的代码都没有了

2、把我们写好的代码放在一个单独的js文件中去执行

在终端中输入 node.exe +执行的文件名称

注意:
1、我们js代码不是在终端中运行的,只是借助终端
去启动我们node.exe,并且最终将结果展现在终端里面而已

2、在运行时候,首先你的终端的目录得切换到你要
执行的文件的目录下面去,然后使用node 文件名称执行即可
我们nodejs的代码是在一个叫做REPL环境中,执行的

REPL

JS的执行

执行js在浏览器端,我是是要依靠浏览器(js的解析引擎)

在服务器端 nodejs开启的REPL环境

官网的解释:
参考:http://shouce.qdfuns.com/nodejs/repl.html

REPL就是当通过node.exe启动之后开辟的一块内存空间,
在这块内容空间里面就可以解释执行我们的js代码

例如:
在终端中输入了 node abc.js 做的事情就是,将abc.js中
写好的js的逻辑代码扔在启动好的node的内容空间中去运行,
我们把启动好的node的这块内存空间称之为REPL环境

模块化思想

为什么前端需要有模块化

1、解决全局变量名污染的问题
2、把相同功能的代码放在一个模块(一个js文件中)方便后期维护
3、便于复用

NodeJS中如何体现模块化

1、Node本身是基于CommonJS规范,
参考:http://javascript.ruanyifeng.com/nodejs/module.html#toc0

2、Node作者在设计这门语言的时候,就严格按照CommonJS
的规范,将它的API设计成模块化了,比如它将开启Web服务这
个功能所有代码都放入一个http模块中

3、Node本质来说就是将相同功能的代码放入到一个.js文件中管理

常用NodeJS中的模块

模块              作用
http 开启一个Web服务,给浏览器提供服务
url 给浏览器发送请求用,还可以传递参数(GET)
querystring 处理浏览器通过GET/POST发送过来的参数
path 查找文件的路径
fs 在服务器端读取文件用的

上面五大核心模块加上其它一些第三方的模块,就可以完成基本的数据库操作了

nodeJS核心模块及其操作

http

使用http模块开启web服务
步骤:
//1、导入我们需要的核心模块(NodeJS提供的模块我们称之为核心模块)
var http = require('http');

//2、利用获取到的核心模块的对象,创建一个server对象
var server = http.createServer();

//3、利用server对象监听浏览器的请求,并且处理(请求-处理-响应)
server.on('request',function(req,res){
res.end("welcome");
});

//4、开启web服务开始监听
server.listen(8080,'127.0.0.1',function(){
console.log('开启服务器成功');
});

url

1、导入url这个核心模块

2、调用url.parse(url字符串,true),如果是true的话代表把我们
的username=zhangsan&pwd=123 字符串解析成js对象

 // 使用url模块获取url中的一些相关信息
const url = require('url')
var testURL = http://127.0.0.1:8899/login?username=zhangsan&pwd=123
console.log(url.parse(testURL,true))//{username:zhangsan,pwd:123}

QueryString

作用:
将GET/POST传递过来的参数,进行解析
GET : ?username=zhangsan&pwd=123
POST : username=zhangsan&pwd=123

使用:
const querystring = require('querystring')

const paramsObj = querystring.parse(键值对的字符串)

GET&POST

相同点:
都是HTTP协议的方法
都能传递参数给服务器

不同点:
1、传参的方式不一样
GET 放在路径后面 ?开始,后面键值对
POST 放在请求体 键值对的方式

2、传参的限制不一样
GET 2048B
POST 2M

3、GET有缓存,POST没有

4、GET传参不安全,POST相对安全

建议:
如果只是单纯的获取数据,就用GET,因为GET有缓存效率高

如果是要向服务器提交数据,就用POST

fs&path

path

作用:获取路径

path.join(__dirname,'你要读取的文件夹下面的文件名称即可')

__dirname全局属性,代表当前文件所在的文件夹路径

path.join会自动判断文件的路径,并且给他加上`/`

fs

作用:读取服务器硬盘上面的某一个文件(操作文件)

fs.readFile : 异步读取服务器硬盘上面的某一个文件
fs:node去读取服务器硬盘中的文件(操作文件)

path:获取文件的路径

上面两个基本上配合起来用

自定义模块

CommonJS规范认为,一个.js文件就可以看成一个模块,如果我们想把模块中定义的变量,方法,对象给外面的js使用,就必须使用CommonJS提供module将我们需要给外面用的东西,导出去

注意点

在commonjs中导入模块用 require
在commonjs中在模块中导出 使用module.exports
如果是自定义模块,在导入自定义模块的时候,得把路径写完整
require导入的东西,就是别的文件modulu.exports导出的东西

Express 框架

基本概念

它是对HTTP封装,用来简化我们网络功能那一块

官网:http://www.expressjs.com.cn/ 官方解释:
基于 Node.js 平台,快速、开放、极简的 web 开发框架。

重点

1、如何去接收GET/POST传递过来的参数
2、如何通过Express进行分门别类的处理路由
3、静态资源的处理

使用

1、Hello World 案例

步骤:
1、导入包
2、创建一个app
3、请求处理响应
4、开启web服务,开始监听

2、获取GET/POST参数
GET参数:登录 http://127.0.0.1:3000/login?username=zhangsan&pwd=123

可以直接在我们的req.query中就可以获取了

POST参数:因为express没有直接提供获取POST参数的方法,需要借助一个第三方包 body-parser
参考: https://www.npmjs.com/package/body-parser

步骤:
1、npm install body-parser --save
2、导包
3、实现某些方法

最后通过req.body即可以获取到post提交过来的参数

路由处理

前端路由:
作用:当触发了某个超链接之后,根据路由的配置,决定
跳转到哪个页面,最终将这个页面呈现出来

后台的路由
作用:就是用来分门别类的出路用户发送过来的请求

    http://127.0.0.1:3000/login
http://127.0.0.1:3000/register

http://127.0.0.1:3000/getGoodsList
http://127.0.0.1:3000/getGoodsInfo

jd购物
男士:(专门创建一个man.js文件来实现男士区域商品的请求)
http://www.jd.com/man/xz
http://www.jd.com/man/ld
http://www.jd.com/man/px

女士:(专门创建一个girl.js文件来实现女士区域商品的请求)
http://www.jd.com/girl/xs
http://www.jd.com/girl/bag
http://www.jd.com/girl/kh

express中代码实现?

步骤:
1、先要创建一个单独的路由(js文件),来处理某一类
请求下面的所有用户请求,并且需要导出去
1.1 导入包 express
1.2 创建一个路由对象
const manRouter = express.Router()
1.3 在具体的路由js中处理属于我们该文件的路由
manRouter.get(xxx)
manRouter.post(xxx)
1.4 将上面创建的路由对象导出去,在入口文件中使用

2、在入口文件中,导入我们的路由文件,并且使用就可以了

//导入路由文件
const manRouter = require(path.join(__dirname,"man/manRouter.js"))
//在入口文件中使用
app.use('/man',manRouter)
```

## Express中静态资源的处理
Express希望对我们后台静态资源处理,达到简单的目的,
然后只希望我们程序员写一句话就能搞定

步骤:
1、在我们入口文件中设置静态资源的根目录
注意点:一定要在路由处理之前设置

app.use(express.static(path.join(__dirname,'statics')))
```

2、在我们的页面中,按照我们Express的规则来请求后台
静态资源数据
写link的href,script的src写的时候,除开静态资源根
路径之外,按照他在服务器上面的路径规则写

mongodb数据库

数据库

保存数据的仓库,数据库本质也是一个文件,只是说和普通的
文件不太一样,他有自己的存储规则,让我们保存数据和查询
数据更加方便

存储文件的介质

localStorage 文本文件
大型数据或是海量数据的时候必须要用到数据库

数据库的分类

客户端:
iOS/Android/前端
iOS/Android SQLite 在iOS/Android存储App的数据

服务端:
关系型数据库
部门---员工
mysql
sqlserver
oracle

非关系型数据库
JSON对象的形式来存储

MongoDB : 简单,你会js、JSON就能操作 Redis Memcached

数据库的作用

1、保存应用程序产生的数据(用户注册数据,用户的个人信息等等)
2、当应用程序需要数据的时候,提供给应用程序去展示

安装mongodb服务端

步骤:
1、安装mongodb服务端软件
2、设置mongodb的环境变量,重启终端验证 mongo -version
3、建立一个文件夹,用来存储mongodb数据库产生的数
据(建议放在C盘根目录 mongodb_datas)
4、启动
mongod --dbpath c:/mongodb_datas

启动服务端有几种方式

1、方式一,直接在cmd中输入 mongod --dbpath c:/mongodb_datas
32位: mongod --dbpath c:/mongodb_datas --journal --storageEngine=mmapv1

2、方式二,可以把mongod --dbpath c:/mongodb_datas做成一个批处理文件
32位: mongod --dbpath c:/mongodb_datas --journal --storageEngine=mmapv1

使用robomongo这个小机器人来操作我们的数据库中的数据

步骤:
1、连接到我们mongodb数据库服务端,并且连接成功之
后,服务端会给我们返回一个操作数据库的db对象

2、拿着上一步返回的db对象,对mongodb数据库中的数据进行操作了

连接成功之后,我们要来操作数据的话
1、创建一个数据库 (相当于在excel中创建空白工作簿)
2、创建集合 (相当于在excel创建工作表单)
数据的一个集合,把相关联的数据放在一个集合中
3、确立表头,插入数据、删除数据、修改数据、查询数据

MongoDB数据库中的概念

数据库 : 一个App中对应一个数据库

集合:相当于Excel中表单,一堆数据的集合,相关联的数据,
会放在一个集合中

文档:相当于excel中的每一行数据

一个数据中可以有多个集合(学生集合、食品集合)
一个集合可以有多条文档(多条数据)

在NodeJS中使用mongodb这个第三方包来操作我们mongodb数据库中的数据

参考: https://www.npmjs.com/package/mongodb

前提准备:
1、使用npm i mongodb --save来安装

正式集成:
1、导入包
2、拿到我们mongoClient对象
3、使用mongoClient连接到mongodb的服务端,返回操作数据库的db对象
4、通过db对象,拿到数据集合

db.collection('集合的名称')
5、调用集合的增,删,改,查的方法,来操作数据库中的数据
收起阅读 »

nodejs中的fs模块

对于文件处理的四个操作 增删改查 简称 curd(create-update-read-del)需要使用到的模块叫File System 简称fs 是nodejs 自带的一个库const fs=require('fs');1、使用 fs.mkdir...
继续阅读 »

对于文件处理的四个操作 增删改查 简称 curd(create-update-read-del)

需要使用到的模块叫File System 简称fs 是nodejs 自带的一个库

const fs=require('fs');

1、使用 fs.mkdir 创建目录css


2、fs.readdir 读取当前目录下的文件node02

同步读取 异步读取 同步读取时候 用try catch 处理报错 异步 直接用回调函数中的参数处理。


3、fs.rename 重命名html 下的index为base


4、 fs.unlink 删除文件t.txt


收起阅读 »

这可能是掘金讲「原型链」,讲的最好最通俗易懂的了,附练习题!

前言 大家好,我是林三心,相信大家都听过前端的三座大山:闭包,原型链,作用域,这三个其实都只是算基础。而我一直觉得基础是进阶的前提,所以不能因为是基础就忽视他们。今天我就以我的方式讲讲原型链吧,希望大家能牢固地掌握原型链知识 很多文章一上来就扔这个图,但是我不...
继续阅读 »


前言


大家好,我是林三心,相信大家都听过前端的三座大山:闭包,原型链,作用域,这三个其实都只是算基础。而我一直觉得基础是进阶的前提,所以不能因为是基础就忽视他们。今天我就以我的方式讲讲原型链吧,希望大家能牢固地掌握原型链知识


很多文章一上来就扔这个图,但是我不喜欢这样,我觉得这样对基础不好的同学很不好,我喜欢带领大家去从零实现这个图,在实现的过程中,不断地掌握原型链的所有知识!!!来吧!!!跟着我从零实现吧!!!跟着我驯服原型链吧!!!


截屏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 ;


以及最后对对象与函数的思考。


就先到这吧~~

 
收起阅读 »

程序员版本的八荣八耻~

大家好,最近整理了一个关于程序员日常开发版本的八荣八耻,还挺有意思的。给大家分享一下,哈哈~以接口兼容为荣,怎么理解呢?我们还要以接口裸奔为耻。为了保证接口报文的安全性,拒绝接口报文裸奔。因此,我们可以使用https协议,还建议对接口加签验签处理,数据加密等。...
继续阅读 »

前言

大家好,最近整理了一个关于程序员日常开发版本的八荣八耻,还挺有意思的。给大家分享一下,哈哈~

1. 以接口兼容为荣,以接口裸奔为耻

接口兼容为荣,怎么理解呢?

很多bug都是因为修改了对外旧接口,但是却不做兼容导致的。关键这个问题多数是比较严重的,可能直接导致系统发版失败的。新手程序员很容易犯这个错误。所以我们修改老接口的时候,一般要做好兼容


如果你的需求是在原来接口上修改,尤其这个接口是对外提供服务的话,一定要考虑接口兼容。举个例子吧,比如dubbo接口,原本是只接收A,B参数,现在你加了一个参数C,就可以考虑这样处理:

//老接口
void oldService(A,B){
//兼容新接口,传个null代替C
newService(A,B,null);
}

//新接口,暂时不能删掉老接口,需要做兼容。
void newService(A,B,C){
...
}

我们还要以接口裸奔为耻。为了保证接口报文的安全性,拒绝接口报文裸奔。因此,我们可以使用https协议,还建议对接口加签验签处理,数据加密等。

接口签名很简单,就是把接口请求相关信息(请求报文,包括请求时间戳、版本号、appid等),客户端私钥加签,然后服务端用公钥验签,验证通过才认为是合法的、没有被中间人篡改过的请求。

2. 以规范日志为荣,以乱打日志为耻

我们的业务逻辑代码需要日志保驾护航。比如:你实现转账业务,转个几百万,然后转失败了,接着客户投诉,然后你还没有打印到日志,想想那种水深火热的困境下,你却毫无办法。。。

因此大家要打好日志,比如日志级别使用恰当,日志格式,在哪些地方打日志,参数打印哪个等等。不能乱打日志,要以规范日志为荣,乱打日志为耻。


3. 以代码自测为荣,以过度自信为耻

修改完代码,要自测一下,这个是每个程序必备的素养,即使你只是修改了一个变量或者一个字段。

要杜绝过度自信,尤其不要抱有这种侥幸心理:我只是改了一个变量或者我只改了一行配置的代码,不用自测了,怎么可能有问题


因此,我们要以代码自测为荣,以过度自信为耻

4. 以参数校验为荣,以运行异常为耻

数校验是每个程序员必备的基本素养。你的方法处理,必须先校验参数。比如入参是否允许为空,入参长度是否符合你的预期长度。因此,我们要以参数校验为荣

比如你的数据库表字段设置为varchar(16),对方传了一个32位的字符串过来,如果你不校验参数,插入数据库直接异常了。


我们要以运行时异常为耻

比如你没有做好一些非空校验,数组边界校验等等,导致的空指针异常、数组边界异常,尤其这些运行时异常还发生在生产环境的话,在有经验的程序员看来,这些错误行为会显得特别低级。

所以,我们要以参数校验为荣,以运行异常为耻

5. 以设计模式为荣,以代码重复为耻

日常工作中,我们要以设计模式为荣。

比如策略模式、工厂模式、模板方法模式、观察者模式、单例模式、责任链模式等等,都是很常用的。在恰当的业务场景,我们还是把设计模式用上吧。设计模式可以让我们的代码更优雅、更具有扩展性。但是不要过度设计哈,不要硬套设计模式。

我们还要以重复代码为耻。重复代码,我相信每个程序员都讨厌的,尤其有时候你的开发工具还会给你提示出来。我们可以抽取公共方法,抽取公用变量、扩展继承类等方式去消除重复代码。


6. 以优化代码为荣,以复制粘贴为耻

日常开发中,很多程序员在实现某个功能时,如果看到老代码有类似的功能,他们很喜欢复制粘贴过来。这样很容易产生重复代码,所以我们要以复制粘贴为耻。一般建议加自己的思考,怎么优化这部分代码,怎么抽取公用方法,用什么设计模式等等。


个人觉得,优化代码的过程,可以让自己取得更大的进步。因此我们要以优化代码为荣,以复制粘贴为耻。

7. 以定义常量为荣,以魔法数字为耻

大家平时工作中,是不是经常看到魔法数字。魔法数字(Magic Number)是指拥有特殊意义,却又不能明确表现出这种意义的数字。程序里面存在魔法数字,易读性很差,且非常难以维护。

如下:

if(type==1){
  System.out.println("关注公众号:捡田螺的小男孩");
}else if(type==2){
  System.out.println("关注公众号:程序员田螺");
}else{
  System.out.println("关注其他公众号");
}

代码中的1、2就表示魔法数字,我们可以用常量取代魔法数,或者定义枚举去代替魔法数字哈。

8. 以总结思考为荣,以混水摸鱼为耻。

我们要以总结思考为荣。

比如你看完田螺哥的文章,可以总结思考一下,或者做做笔记,或者放到收藏夹,茶余饭后再看看。再比如你日常工作中,看到一段不错的代码,也可以思考一下亮点在哪里,如果是你自己来写的话,怎么写出更好的代码。反正就是要多总结,多思考,多复习,温故而知新嘛。

我们要以混水摸鱼为耻。比如工作中,一些小伙伴喜欢混水摸鱼,当一天和尚敲一天钟,代码多是复制粘贴,做完需求就摸鱼。实际上这个不可取的。


我们要在工作中成长,通过认真工作,使自己会得更多,将来换工作也可以拿到更高的薪水,对吧,加油吧,小伙子,以总结思考为荣,以混水摸鱼为耻

来源:捡田螺的小男孩 ,作者捡田螺的小男孩

收起阅读 »

生成二维码或条形码JavaScript脚本库

web
二维码或条形码在日常生活中现在应用已经非常普遍了,文章分享生成条形码和二维码的JavaScript库。条形码条形码是日常生活中比较常见的,主要用于商品。通俗的理解就是一串字符串的集合(含字母、数字及其它ASCII字符的集合应用),用来常用来标识一个货品的唯一性...
继续阅读 »


二维码或条形码在日常生活中现在应用已经非常普遍了,文章分享生成条形码和二维码的JavaScript库。

条形码

条形码是日常生活中比较常见的,主要用于商品。通俗的理解就是一串字符串的集合(含字母、数字及其它ASCII字符的集合应用),用来常用来标识一个货品的唯一性,当然还有更多更深入与广泛的应用,像超市的商品、衣服、微信、支付宝、小程序等到处都有条形码的广泛应用;

安装依赖:

npm install jsbarcode --save-dev

在 HTML 页面上加入以下代码:

<svg id="barcode"
jsbarcode-value="123456789012"
jsbarcode-format="code128"></svg>

接下来看下 JavaScript 代码,如下:

import jsbarcode from 'jsbarcode';
const createBarcode = (value, elemTarget) => {
  jsbarcode(elemTarget, "value");
};
createBarcode("#barcode", "devpoint");

运行成功的效果如下:


二维码

相比条形码,二维码的使用场景也越来也多,支付码、场所码、小程序等等。二维码的长相经常是在一个正方形的框中填充各种点点或无规则小图形块而构成的图形,这种称之为二维码,他与一维码最大的区别就是存储容量大很多,而且保密性好。二维码本质上表现给大家的就是一个静态图片,其实是包含特字加密算法的图形,里面存储的是一串字符串(即字母、数字、ASCII码等),这说明二维码不仅存储量大,而且存储的内容很广泛,数字、字母、汉字等都可以被存储。

安装依赖:

npm install qrcode --save-dev

HTML:

<canvas id="qrcode"></canvas>

JavaScript:

import QRCode from "qrcode";
const createQrcode = (value, elemTarget) => {
  QRCode.toCanvas(document.querySelector(elemTarget), value);
};
createQrcode("#qrcode", "devpoint");

效果如下:


来源:juejin.cn/post/7116156434605146126

收起阅读 »

Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套

这次的 Flutter 小技巧是 ListView 和 PageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListView 和 PageView 的三种嵌套模式带大家收获一些不一样的小技巧。正常嵌套最常见的...
继续阅读 »

这次的 Flutter 小技巧是 ListViewPageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListViewPageView 的三种嵌套模式带大家收获一些不一样的小技巧。

正常嵌套

最常见的嵌套应该就是横向 PageView 加纵向 ListView 的组合,一般情况下这个组合不会有什么问题,除非你硬是要斜着滑

最近刚好遇到好几个人同时在问:“斜滑 ListView 容易切换到 PageView 滑动” 的问题,如下 GIF 所示,当用户在滑动 ListView 时,滑动角度带上倾斜之后,可能就会导致滑动的是 PageView 而不是 ListView


虽然从我个人体验上并不觉得这是个问题,但是如果产品硬是要你修改,难道要自己重写 PageView 的手势响应吗?

我们简单看一下,不管是 PageView 还是 ListView 它们的滑动效果都来自于 Scrollable ,而 Scrollable 内部针对不同方向的响应,是通过 RawGestureDetector 完成:

  • VerticalDragGestureRecognizer 处理垂直方向的手势

  • HorizontalDragGestureRecognizer 处理水平方向的手势

所以简单看它们响应的判断逻辑,可以看到一个很有趣的方法 computeHitSlop根据 pointer 的类型确定当然命中需要的最小像素,触摸默认是 kTouchSlop (18.0)

image-20220613103745974

看到这你有没有灵光一闪:如果我们把 PageView 的 touchSlop 修改了,是不是就可以调整它响应的灵敏度? 恰好在 computeHitSlop 方法里,它可以通过 DeviceGestureSettings 来配置,而 DeviceGestureSettings 来自于 MediaQuery ,所以如下代码所示:

body: MediaQuery(
 ///调高 touchSlop 到 50 ,这样 pageview 滑动可能有点点影响,
 ///但是大概率处理了斜着滑动触发的问题
 data: MediaQuery.of(context).copyWith(
     gestureSettings: DeviceGestureSettings(
   touchSlop: 50,
)),
 child: PageView(
   scrollDirection: Axis.horizontal,
   pageSnapping: true,
   children: [
     HandlerListView(),
     HandlerListView(),
  ],
),
),

小技巧一:通过嵌套一个 MediaQuery ,然后调整 gestureSettingstouchSlop 从而修改 PageView 的灵明度 ,另外不要忘记,还需要把 ListViewtouchSlop 切换会默认 的 kTouchSlop

class HandlerListView extends StatefulWidget {
 @override
 _MyListViewState createState() => _MyListViewState();
}
class _MyListViewState extends State<HandlerListView> {
 @override
 Widget build(BuildContext context) {
   return MediaQuery(
     ///这里 touchSlop 需要调回默认
     data: MediaQuery.of(context).copyWith(
         gestureSettings: DeviceGestureSettings(
       touchSlop: kTouchSlop,
    )),
     child: ListView.separated(
       itemCount: 15,
       itemBuilder: (context, index) {
         return ListTile(
           title: Text('Item $index'),
        );
      },
       separatorBuilder: (context, index) {
         return const Divider(
           thickness: 3,
        );
      },
    ),
  );
}
}

最后我们看一下效果,如下 GIF 所示,现在就算你斜着滑动,也很触发 PageView 的水平滑动,只有横向移动时才会触发 PageView 的手势,当然, 如果要说这个粗暴的写法有什么问题的话,大概就是降低了 PageView 响应的灵敏度

xiehuabudong

同方向 PageView 嵌套 ListView

介绍完常规使用,接着来点不一样的,在垂直切换的 PageView 里嵌套垂直滚动的 ListView , 你第一感觉是不是觉得不靠谱,为什么会有这样的场景?

对于产品来说,他们不会考虑你如何实现的问题,他们只会拍着脑袋说淘宝可以,为什么你不行,所以如果是你,你会怎么做?

而关于这个需求,社区目前讨论的结果是:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理

如果对实现逻辑分析没兴趣,可以直接看本小节末尾的 源码链接

看到自己管理先不要慌,虽然要自己实现 PageViewListView 的手势分发,但是其实并不需要重写 PageViewListView ,我们可以复用它们的 Darg 响应逻辑,如下代码所示:

  • 通过 NeverScrollableScrollPhysics 禁止了 PageViewListView 的滚动效果

  • 通过顶部 RawGestureDetectorVerticalDragGestureRecognizer 自己管理手势事件

  • 配置 PageControllerScrollController 用于获取状态

body: RawGestureDetector(
 gestures: <Type, GestureRecognizerFactory>{
   VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
           VerticalDragGestureRecognizer>(
      () => VerticalDragGestureRecognizer(),
      (VerticalDragGestureRecognizer instance) {
     instance
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
      ..onEnd = _handleDragEnd
      ..onCancel = _handleDragCancel;
  })
},
 behavior: HitTestBehavior.opaque,
 child: PageView(
   controller: _pageController,
   scrollDirection: Axis.vertical,
   ///屏蔽默认的滑动响应
   physics: const NeverScrollableScrollPhysics(),
   children: [
     ListView.builder(
       controller: _listScrollController,
       ///屏蔽默认的滑动响应
       physics: const NeverScrollableScrollPhysics(),
       itemBuilder: (context, index) {
         return ListTile(title: Text('List Item $index'));
      },
       itemCount: 30,
    ),
     Container(
       color: Colors.green,
       child: Center(
         child: Text(
           'Page View',
           style: TextStyle(fontSize: 50),
        ),
      ),
    )
  ],
),
),

接着我们看 _handleDragStart 实现,如下代码所示,在产生手势 details 时,我们主要判断:

  • 通过 ScrollController 判断 ListView 是否可见

  • 判断触摸位置是否在 ListIView 范围内

  • 根据状态判断通过哪个 Controller 去生产 Drag 对象,用于响应后续的滑动事件

  void _handleDragStart(DragStartDetails details) {

   if (_listScrollController?.hasClients == true &&
       _listScrollController?.position.context.storageContext != null) {
     ///获取 ListView 的 renderBox
     final RenderBox? renderBox = _listScrollController
         ?.position.context.storageContext
        .findRenderObject() as RenderBox;

     if (renderBox?.paintBounds
            .shift(renderBox.localToGlobal(Offset.zero))
            .contains(details.globalPosition) ==
         true) {
       _activeScrollController = _listScrollController;
       _drag = _activeScrollController?.position.drag(details, _disposeDrag);
       return;
    }
  }

   ///这时候就可以认为是 PageView 需要滑动
   _activeScrollController = _pageController;
   _drag = _pageController?.position.drag(details, _disposeDrag);
}

前面我们主要在触摸开始时,判断需要响应的对象时ListView 还是 PageView ,然后通过 _activeScrollController 保存当然响应对象,并且通过 Controller 生成用于响应手势信息的 Drag 对象。

简单说:滑动事件发生时,默认会建立一个 Drag 用于处理后续的滑动事件,Drag 会对原始事件进行加工之后再给到 ScrollPosition 去触发后续滑动效果。

接着在 _handleDragUpdate 方法里,主要是判断响应是不是需要切换到 PageView:

  • 如果不需要就继续用前面得到的 _drag?.update(details)响应 ListView 滚动

  • 如果需要就通过 _pageController 切换新的 _drag 对象用于响应

void _handleDragUpdate(DragUpdateDetails details) {
 if (_activeScrollController == _listScrollController &&

     ///手指向上移动,也就是快要显示出底部 PageView
     details.primaryDelta! < 0 &&

     ///到了底部,切换到 PageView
     _activeScrollController?.position.pixels ==
         _activeScrollController?.position.maxScrollExtent) {
   ///切换相应的控制器
   _activeScrollController = _pageController;
   _drag?.cancel();

   ///参考 Scrollable 里
   ///因为是切换控制器,也就是要更新 Drag
   ///拖拽流程要切换到 PageView 里,所以需要 DragStartDetails
   ///所以需要把 DragUpdateDetails 变成 DragStartDetails
   ///提取出 PageView 里的 Drag 相应 details
   _drag = _pageController?.position.drag(
       DragStartDetails(
           globalPosition: details.globalPosition,
           localPosition: details.localPosition),
       _disposeDrag);
}
 _drag?.update(details);
}

这里有个小知识点:如上代码所示,我们可以简单通过 details.primaryDelta 判断滑动方向和移动的是否是主轴

最后如下 GIF 所示,可以看到 PageView 嵌套 ListView 同方向滑动可以正常运行了,但是目前还有个两个小问题,从图示可以看到:

  • 在切换之后 ListView 的位置没有保存下来

  • 产品要求去除 ListView 的边缘溢出效果

7777777777777

所以我们需要对 ListView 做一个 KeepAlive ,然后用简单的方法去除 Android 边缘滑动的 Material 效果:

  • 通过 with AutomaticKeepAliveClientMixinListView 在切换之后也保持滑动位置

  • 通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Scrollable 的边缘 Material 效果

child: PageView(
 controller: _pageController,
 scrollDirection: Axis.vertical,
 ///去掉 Android 上默认的边缘拖拽效果
 scrollBehavior:
     ScrollConfiguration.of(context).copyWith(overscroll: false),


///对 PageView 里的 ListView 做 KeepAlive 记住位置
class KeepAliveListView extends StatefulWidget {
 final ScrollController? listScrollController;
 final int itemCount;

 KeepAliveListView({
   required this.listScrollController,
   required this.itemCount,
});

 @override
 KeepAliveListViewState createState() => KeepAliveListViewState();
}

class KeepAliveListViewState extends State<KeepAliveListView>
   with AutomaticKeepAliveClientMixin {
 @override
 Widget build(BuildContext context) {
   super.build(context);
   return ListView.builder(
     controller: widget.listScrollController,

     ///屏蔽默认的滑动响应
     physics: const NeverScrollableScrollPhysics(),
     itemBuilder: (context, index) {
       return ListTile(title: Text('List Item $index'));
    },
     itemCount: widget.itemCount,
  );
}

 @override
 bool get wantKeepAlive => true;
}

所以这里我们有解锁了另外一个小技巧:通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Android 滑动到边缘的 Material 2效果,为什么说 Material2, 因为 Material3 上变了,具体可见: Flutter 3 下的 ThemeExtensions 和 Material3

000000000

本小节源码可见: github.com/CarGuo/gsy_…

同方向 ListView 嵌套 PageView

那还有没有更非常规的?答案是肯定的,毕竟产品的小脑袋,怎么会想不到在垂直滑动的 ListView 里嵌套垂直切换的 PageView 这种需求。

有了前面的思路,其实实现这个逻辑也是异曲同工:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理,不同的就是手势方法分发的差异。

RawGestureDetector(
         gestures: <Type, GestureRecognizerFactory>{
           VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
                   VerticalDragGestureRecognizer>(
              () => VerticalDragGestureRecognizer(),
              (VerticalDragGestureRecognizer instance) {
             instance
              ..onStart = _handleDragStart
              ..onUpdate = _handleDragUpdate
              ..onEnd = _handleDragEnd
              ..onCancel = _handleDragCancel;
          })
        },
         behavior: HitTestBehavior.opaque,
         child: ListView.builder(
               ///屏蔽默认的滑动响应
               physics: NeverScrollableScrollPhysics(),
               controller: _listScrollController,
               itemCount: 5,
               itemBuilder: (context, index) {
                 if (index == 0) {
                   return Container(
                     height: 300,
                     child: KeepAlivePageView(
                       pageController: _pageController,
                       itemCount: itemCount,
                    ),
                  );
                }
                 return Container(
                     height: 300,
                     color: Colors.greenAccent,
                     child: Center(
                       child: Text(
                         "Item $index",
                         style: TextStyle(fontSize: 40, color: Colors.blue),
                      ),
                    ));
              }),
      )

同样是在 _handleDragStart 方法里,这里首先需要判断:

  • ListView 如果已经滑动过,就不响应顶部 PageView 的事件

  • 如果此时 ListView 处于顶部未滑动,判断手势位置是否在 PageView 里,如果是响应 PageView 的事件

  void _handleDragStart(DragStartDetails details) {
   if (_listScrollController.offset > 0) {
     _activeScrollController = _listScrollController;
     _drag = _listScrollController.position.drag(details, _disposeDrag);
     return;
  }
   if (_pageController.hasClients) {
     ///获取 PageView
     final RenderBox renderBox =
         _pageController.position.context.storageContext.findRenderObject()
             as RenderBox;

     ///判断触摸范围是不是在 PageView
     final isDragPageView = renderBox.paintBounds
        .shift(renderBox.localToGlobal(Offset.zero))
        .contains(details.globalPosition);

     ///如果在 PageView 里就切换到 PageView
     if (isDragPageView) {
       _activeScrollController = _pageController;
       _drag = _activeScrollController.position.drag(details, _disposeDrag);
       return;
    }
  }

   ///不在 PageView 里就继续响应 ListView
   _activeScrollController = _listScrollController;
   _drag = _listScrollController.position.drag(details, _disposeDrag);
}

接着在 _handleDragUpdate 方法里,判断如果 PageView 已经滑动到最后一页,也将滑动事件切换到 ListView


当然,同样还有 KeepAlive 和去除列表 Material 边缘效果,最后运行效果如下 GIF 所示。

22222222222

本小节源码可见:github.com/CarGuo/gsy_…

最后再补充一个小技巧:如果你需要 Flutter 打印手势竞技的过程,可以配置 debugPrintGestureArenaDiagnostics = true;来让 Flutter 输出手势竞技的处理过程

import 'package:flutter/gestures.dart';
void main() {
 debugPrintGestureArenaDiagnostics = true;
 runApp(MyApp());
}

image-20220613115808538

最后

最后总结一下,本篇介绍了如何通过 Darg 解决各种因为嵌套而导致的手势冲突,相信大家也知道了如何利用 ControllerDarg 来快速自定义一些滑动需求,例如 ListView 联动 ListView 的差量滑动效果:




44444444444444


作者:恋猫de小郭
来源:juejin.cn/post/7116267156655833102

收起阅读 »

跟我学企业级flutter项目:简化框架demo参考

前言最近很多人在问我,没有一个不错的demo,不会如何做单工程模式,如何封装网络请求,如何去做网络持久化。那么今天我将demo分享出来。现阶段还无法把我构建的flutter快速开发框架开源出来。暂时用简化demo来展示。 相关文章: 跟我学企业级fl...
继续阅读 »

前言

最近很多人在问我,没有一个不错的demo,不会如何做单工程模式,如何封装网络请求,如何去做网络持久化。那么今天我将demo分享出来。现阶段还无法把我构建的flutter快速开发框架开源出来。暂时用简化demo来展示。 相关文章: 跟我学企业级flutter项目:flutter模块化,单工程架构模式构思与实践

跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层

跟我学企业级flutter项目:dio网络框架增加公共请求参数&header

demo地址:

github.com/smartbackme…

为了大家更清楚的使用,我将对目录结构进行说明:

目录结构

在这里插入图片描述

以模块一来说明

在这里插入图片描述 模块一启动配置:

class MyConfiger extends ICommentConfiger{

@override
Widget getRouter(RouteSettings settings) {
var router = RouterPage.getRouter(settings);
if(router!=null){
return router;
}
return const NullRouter();
}

}
void main() {
Application.init(AppInit(MyConfiger()));
runApp(const MyApp());
}

公共模块说明

在这里插入图片描述

主工程启动说明

import 'package:commonmodule/commonmodule.dart';
import 'package:commonmodule/config.dart';
import 'package:flutter/material.dart';
import 'package:commonmodule/router_name.dart' as common;
import 'package:kg_density/kg_density.dart';
import 'package:myflutter/page/home.dart';
import 'package:onemodule/router_page.dart' as onemodule;
import 'package:twomodule/router_page.dart' as twomodule;

// 路由分配管理
class MyCommentConfiger extends ICommentConfiger{
@override
Widget getRouter(RouteSettings settings) {
if(settings.name == common.RouterName.home){
return const HomePage();
}
var teachertRouter = onemodule.RouterPage.getRouter(settings);
if(teachertRouter!=null){
return teachertRouter;
}
var clientRouter = twomodule.RouterPage.getRouter(settings);
if(clientRouter!=null){
return clientRouter;
}
return const NullRouter();

}


}

//启动初始化
void main() async {
MyFlutterBinding.ensureInitialized();
KgDensity.initKgDensity(designWidth : 375);
await SpSotre.instance.init();
ULogManager.init();
Application.init(AppInit(MyCommentConfiger()));
runApp(const MyApp());
}

//WidgetsFlutterBinding 配置
class MyFlutterBinding extends WidgetsFlutterBinding with KgFlutterBinding {

static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null) MyFlutterBinding();
return WidgetsBinding.instance!;
}
}


作者:王二蛋与他的张大花
链接:https://juejin.cn/post/7115236177136844808/
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

React Native ART

Android自带ART,不用导入。iOS要使用需要使用xcode打开react native 的ios目录,

1、使用xcode中打开react-native中的ios项目,选中‘Libraries’目录 ——> 右键选择‘Add Files to 项目名称’ ——> 'node_modules/react-native/Libraries/ART/ART.xcodeproj' 添加;


2、选中项目根目录 ——> 点击’Build Phases‘ ——> 点击‘Link Binary With Libraries’ ——> 点击左下方‘+’ ——> 选中‘libART.a’添加。


感谢奋斗的orange 提供,转载原文http://blog.csdn.net/u010940770/article/details/71126700

如果要使用svg作为渲染,使用react-native-art-svg

以下是个人记录:

1. svg的设计要使用局中描边;

2. 画扇形

import React from 'react'

import {

View,

ART

} from 'react-native'

const {Surface} = ART;

import Wedge from './Wedge'

export default class Fan extends React.Component{

render(){

return(

outerRadius={50}

startAngle={0}

endAngle={60}

originX={50}

originY={50}

fill="blue"/>

)

}

}



解析Python爬虫赚钱方式

Python爬虫怎么挣钱?解析Python爬虫赚钱方式,想过自己学到的专业技能赚钱,首先需要你能够数量掌握Python爬虫技术,专业能力强才能解决开发过程中出现的问题,Python爬虫可以通过Python爬虫外包项目、整合信息数据做产品、独立的自媒体三种方式挣钱。

Python爬虫怎么挣钱?

  一、Python爬虫外包项目

  网络爬虫最通常的的挣钱方式通过外包网站,做中小规模的爬虫项目,向甲方提供数据抓取,数据结构化,数据清洗等服务。新入行的程序员大多都会先尝试这个方向,直接靠技术手段挣钱,这是我们技术人最擅长的方式,因项目竞价的人太多,外包接单网站上的爬虫项目被砍到了白菜价也是常有的事。

  二、整合信息数据做产品

  利用Python爬虫简单说就是抓取分散的信息,整合后用网站或微信或APP呈现出来,以通过网盟广告,电商佣金,直接售卖电商产品或知识付费来变现。

三、最典型的就是找爬虫外包活儿

网络爬虫最通常的的挣钱方式通过外包网站,做中小规模的爬虫项目,向甲方提供数据抓取,数据结构化,数据清洗等服务。新入行的程序员大多都会先尝试这个方向,直接靠技术手段挣钱,这是我们技术人最擅长的方式,因项目竞价的人太多,外包接单网站上的爬虫项目被砍到了白菜价也是常有的事。

接着又去琢磨了其他的挣钱方法

四、爬数据做网站

那会儿开始接触运营,了解到一些做流量,做网盟挣钱的一些方法。挺佩服做运营的热,觉得鬼点子挺多的(褒义),总是会想到一些做流量的方法,但是他们就是需要靠技术去帮忙实现,去帮忙抓数据,那会我就在思考我懂做网站,抓数据都没问题,只要我能融汇运营技巧,就可以靠个人来挣钱钱了,于是就学习了一些SEO,和做社群的运营方法。

开始抓数据,来做网站挣钱,每个月有小几千块钱,虽然挣得不多,但做成之后不需要怎么维护,也算是有被动收入了。当然如果你技术学的还不够好,暂时就不要做了,可以先去小编的专栏简介的学习小天地,里面很多新教程项目多练习

五、去股市里浪一下【股市有风险,谨慎入市】

年龄越来越大了,有点余钱了就想投资一下,就去研究了下美股,买了一阵美股,挣了点钱,就想挣得更多,就在想有没有方法通过IT技术手段来辅助一下,那时喜欢买shopitify (类似国内的有赞)这类高成长,财报季股价波动大的股票。因为他是依附于facebook这类社交网站的,就是那些facebook上的网红可以用shopitify开店,来给他们的粉丝卖商品。

所以shopitify有个特点就是在社交媒体上的讨论量和相关话题度能反应一些这家公司这个季度的销售近况,这会影响它这个季度的财报,所以就想方设法就facebook上抓数据,来跟往期,历史上的热度来对比,看当季的财报是否OK,就用这种方法来辅助我买卖(是辅助,不是完全依靠)。

当初战绩还是可以,收益基本2-3倍于本金,心里挺喜滋滋的,后面由于我的风险控制意识不够,大亏了2次,亏到吐血。所以印证了那句话,股市有风险,谨慎入市。

六、在校大学生

最好是数学或计算机相关专业,编程能力还可以的话,稍微看一下爬虫知识,主要涉及一门语言的爬虫库、html解析、内容存储等,复杂的还需要了解URL排重、模拟登录、验证码识别、多线程、代理、移动端抓取等。由于在校学生的工程经验比较少,建议找一些少量数据抓取的项目,而不要去接一些监控类的项目、或大规模抓取的项目。慢慢来,步子不要迈太大。

七、在职人员

如果你本身就是爬虫工程师,挣钱很简单。如果你不是,也不要紧。只要是做IT的,稍微学习一下爬虫应该不难。

在职人员的优势是熟悉项目开发流程,工程经验丰富,能对一个任务的难度、时间、花费进行合理评估。可以尝试去找一些大规模抓取任务、监控任务、移动端模拟登录并抓取任务等,收益想对可观一些。

八、独立的自媒体号

  做公众号、自媒体、独立博客,学Python写爬虫的人越来越多,很多是非计算机科班出身。所以把用Python写爬虫的需求增大了,工作上的实践经验多一点,可以多写一些教程和学习经验总结。

以上就是关于Python爬虫赚钱的方式介绍,掌握专业技能除本职工作外还可以兼职接单哦。

  掌握python爬虫、Web前端、人工智能与机器学习、自动化开发、金融分析、网络编程等技能,零基础python找到工作也就不难了的哦。

本文转自: https://cloud.tencent.com/developer/article/1895384

两个textinput 切换不用点两下

原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。如有侵权,请联系 cloudcommunity@tencent.com 删除。

核心代码

//添加手势监听

componentWillMount(){ 

this._panResponder = PanResponder.create({

onStartShouldSetPanResponder: () => true,

onPanResponderRelease: (evt,gs)=>{

console.log(gs);

{/*处理事件*/}

if (gs.y0<46+38 && gs.y0>46) {

this.refs.textInputs.focus()

};

}

})

}


将手势监听给一个组件

{...this._panResponder.panHandlers}

将组建和事件写出来

ref='textInputs'

onFocus={() => {this.refs.textInputs.focus()}}

即可

🌰

/**

 * Sample React Native App

 * https://github.com/facebook/react-native

 * @flow

 */

import React, { Component } from 'react'; 

import {

AppRegistry,

StyleSheet,

View,

ScrollView,

PanResponder,

TextInput,

Text

} from 'react-native';

export default class button extends Component {

constructor(props) {

//加载父类方法,不可省略

super(props);

//设置初始的状态

this.state = {

top:0,

left:0,

};

}

componentWillMount(){

this._panResponder = PanResponder.create({

onStartShouldSetPanResponder: () => true,

onPanResponderRelease: (evt,gs)=>{

console.log(gs);

if (gs.y0<46+38 && gs.y0>46) {

this.refs.textInputs.focus()

};

}

})

}

render(){

return (



{...this._panResponder.panHandlers}

keyboardShouldPersistTaps={false}>



联系方式







style={styles.telTextInput}

autoCapitalize = "none"

autoCorrect={false}

multiline = {true}

keyboardType = "default"

ref='textInputs'

placeholder = "请输入手机号或邮箱"

placeholderTextColor = "#999"

onFocus={() => {this.refs.textInputs.focus()}}

>





style={styles.telTextInput}

autoCapitalize = "none"

autoCorrect={false}

multiline = {true}

keyboardType = "default"

ref='textInput'

placeholder = "请输入手机号或邮箱"

placeholderTextColor = "#999"

onFocus={() => {this.refs.textInput.focus()}}

>







);

}

}

const styles = StyleSheet.create({

container: {

flex:1,

flexDirection: 'column',

//marginTop:64,

backgroundColor:'white'

},

line3:{

height:46,

paddingHorizontal:15,

paddingVertical:15,

borderBottomColor:'#E0E0E0',

borderBottomWidth:1

},

fdcontext:{

color:'#aaa',

fontSize:14

},

line5:{

flexDirection: 'column',

flex:1,

height: 38*2,

borderBottomColor:'#E0E0E0',

borderBottomWidth:1,

},

telTextInput:{

height:37,

fontSize: 12,

color:'#aaa',

paddingHorizontal:15,

paddingVertical:6,

}

});

AppRegistry.registerComponent('button', () => button);


原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

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代码块不会提前消亡。

10行代码实现一个爬虫

跟我学习Python爬虫系列开始啦。带你简单快速高效学习Python爬虫。

一、快速体验一个简单爬虫

以抓取简书首页文章标题和链接为例

简书首页

就是以上红色框内文章的标签,和这个标题对应的url链接。当然首页还包括其他数据,如文章作者,文章评论数,点赞数。这些在一起,称为结构化数据。我们先从简单的做起,先体验一下Python之简单,之快捷。

1)环境准备
当然前提是你在机器上装好了Python环境,初步掌握和了解Python语法。如果还没有装好Python环境,对Python语言法不了解,可以先看《然学科技 Python基础系列》文章:https://www.jianshu.com/nb/20496406

2)安装相应包
快速入门我们使用的是requests包和BeautifulSoup包。简单解释一下,requests功能强大,代码少,封装了网络请求request(发起请求)和response(网络响应),request就像打开浏览器地址栏输入你想要访问的网站,浏览器中马上就可以看到内容一样(response)。

爬虫可以抓到大量数据(结构化的数据),存到数据库中(或excel, csv文件),再进行清洗整理,进行其他工作,如数据分析等。数据抓取也是数据分析前提和基础。

  1. 安装requests
pip install requests
  1. 安装beautifulsoup4
pip install beautifulsoup4 
  1. 可以查看一下你安装了哪些包
pip list

3)代码:

# _*_ coding:utf-8 _*_

import requests
from bs4 import BeautifulSoup

URL='http://www.jianshu.com'


def simple_crawel():
html = requests.get(URL).content
soup = BeautifulSoup(html, 'lxml')
titles = soup.find_all('a',class_="title")
for t in titles:
print t.text+' -->>> '+'https://www.jianshu.com'+t['href']


if __name__ == '__main__':
simple_crawel()

运行这个文件:

python demo.py

结果:

抓取到的数据

代码解析:

html = requests.get(URL).content

发起一个请求,获到到页面的内容(文本),对的就是一行代码就抓取到网页的全部内容。下一步就是要对页面进行解析。

titles = soup.find_all('a',class_="title")

这行代码表示,寻找页面上所有class属性为titlea标签,就是文章标题所对应的标签。怎样才能找到文章标题对就是的哪个标题呢?很简单,在chrome浏览器中右键“检查”中查看就知道。看下图:

文章标题所对应的标签

然后再循环遍历,就得到每一个文章标题的a标签对象,在循环中取出文章标题的内容'text'和链接href就行了。

就这么简单,10行代码就抓取到首页热门文章的标题和URL打印在屏幕上。

二、学习爬虫需要的相关知识

代码很少,涉及到的知识点却很多。如果想要入门系统学习Python爬虫需要哪些知识呢?

  • Python语言基础
  • Python爬虫相关库
  • HTTP请求响应模式
  • HTML基础(HTML+CSS)
  • 数据库基础

以上内容,都会在《跟我学Python爬虫》逐步讲。也可以把Python爬虫作为Python语言学习的起点,来了解和学习这门“人工智能的第一语言”,进而打开爬虫、数据分析、数据可视化、深度学习、人工智能的大门。

几个对程序员的误解,害人不浅!

在很多人眼里,程序员是一类高薪、“高危” 的职业。他们穿着格子衫、顶着一碗超帅的光头,能修电脑、能黑网站、简直无所不能。。。但直到我自己当上程序员,才发现其实很多都是对程序员的误解。除了外行的误解外,还有很多来自于程序员同行的误解。今天这篇文章呢,我想结合自己...
继续阅读 »

在很多人眼里,程序员是一类高薪、“高危” 的职业。他们穿着格子衫、顶着一碗超帅的光头,能修电脑、能黑网站、简直无所不能。。。

但直到我自己当上程序员,才发现其实很多都是对程序员的误解。除了外行的误解外,还有很多来自于程序员同行的误解。今天这篇文章呢,我想结合自己的学习 / 工作经历和感悟,分享下我对这些误解的看法,当然也希望给程序员朋友们一些实质性的建议和启发。

外行的误解

1. 程序员为啥工资那么高?人均年薪百万?

作为达不到平均的一方,我觉得这句话伤害不大,侮辱性极强。

程序员平均薪资可能的确稍微高了一点点,但是年薪百万真的是幸存者偏差了,真的极少数程序员(尤其是只凭技术的程序员)能做到这个地步。如果你拿我和小马哥去平均,那我还人均千万、人均上亿呢对吧?

2. 发量代表水平?

之前很多同学看我视频都吐槽说:“你为什么还有头发,你个菜鸡!”

我觉得这句话伤害不大,侮辱性极强,如果发量代表水平的话我应该比在座的大多数同学都要浓密才对。

所以有没有种可能,是因为太菜,需求做不出来、Bug 改不完,所以才经常熬夜加班、精神压力极大,导致头发熬没了呢?

咳咳,别骂了别骂了,是我本人了。


3. 感觉程序员的手速都很快?

我觉得这个要分情况。拿我自己来说,我一般在 2 种情况下敲键盘比较快:

  1. 要么是在写贼简单的、不用动脑的重复代码(比如增删改查)

  2. 要么就是在回消息聊天

所以有没有种可能,程序员的手速是通过摸鱼、怼产品、重复劳动、或者是平时打游戏打得多而提升的呢?

不过毕竟要经常敲代码,所以程序员的手速通常都不慢。

4. 程序员都是 996 吗?

我记得我之前不怎么加班的时候,就有同学在我视频下评论:你为什么不加班?

这个问题直接把我问懵了,好像我真的觉得自己应该加班,不加班是罪过。


我想说其实程序员也是有个人时间的。至于为什么程序员经常会加班呢?我觉得主要是以下几点:

  1. 首先是我们的程序代码是越写越多的,写的越多,系统越复杂,Bug 就越多。就拿我自己来说,刚做项目一周的时候,就那几行代码,Bug 多好查。但现在项目做了一年多了、用户也多了,很多陈年老 Bug 慢慢被发现了,而且经常牵一发而动全身。

  2. 第 2 点是程序员对排期的错误估算。我发现一个有趣的事情,需求是做不完的,你需求做的越快,新需求来的就越快;而且我们很多时候只考虑了做需求的时间,没有考虑改 Bug 的时间。但现实却有可能是改 Bug 的时间比开发的时间还要长。所以可能的话,还是别把需求排太满,预留一部分时间改 Bug。

  3. 当然还有很多其他因素,比如不会拒绝需求、不会跟产品 battle;缺乏经验、写的系统不利于维护、或者身边的人都很卷你不好意思走等等。总之加班是由很多方面决定的。

5. 重启可以解决 Bug?!

对不起,我觉得这个并不是误解。。这是真的!

以前我遇到过一些莫名其妙的 Bug 就死扣到底,但后来我就学聪明了,先重启一下编辑器、重启下软件,说不定就好了。因为 Bug 不一定是你造成的,可能真的是编辑器的 Bug。

大家就理解为电脑死机后,重启一下就又能开机了。原理应该是差不多的(将程序置于初始化状态)?

同行的误解

1. 算法和数据结构不重要?

有很多程序员是这么认为的,觉得工作中也用不到自己写算法,用个现成的函数、类库,或者上网抄一段就能搞定对吧?

但事实上,有些时候并不是你用不到算法,而是你缺了一些知识,根本想不到可以用算法去更好地解决问题。比如同样是存储和查找 20 万 个单词,没学过算法,用数组也能存、也能顺序查找,但是时空间都存在浪费;那如果你知道前缀树或者其他数据结构,就可以大幅节省存储空间、提升查找效率。

我觉得自己学的知识越多,反而会越觉得基础才是最重要的。因为上层的技术不断发展、不断迭代和淘汰,但是底层原理、编程思想、基本功一般是不会变的。

当然也有同学问是不是前端就不用学数据结构和算法了呢?只能这么说,这一块在前端面试的比重的确不大,时间紧大家可以优先以技术框架学习为主,但是有空了还是要好好补一下基础。

2. 写程序应当追求完美?

我觉得这句话对一半,应该是追求 特定条件下 的最优解。

没有工作经验的同学会觉得程序就要完美、看见你程序有 bug 了、写的不好看了、前人留屎山代码了,多少都会嫌弃。


其实真实工作下,我们没办法把程序写到完美,往往是空间和时间的权衡,比如 HashMap ,用内存换查找效率;或者人力成本和资源的权衡,比如花钱买现成的服务、节省开发时间;再或者是需求和实现的权衡,比如天天都让你做紧急需求,你还有空去优化架构、有空去追求极致的性能么?对不对,代码屎山就是这么来的。

所以这里就要求我们在写代码之前先做调研设计,多思考几种方案、权衡利弊,然后从中选择相对的最优解。同时也希望对别人写的代码多一些包容,把你放在别人的场景下,你未必能做得更好。

3. 代码量等于水平?

在学校的时候,我的确是这么认为的,当时经常跟舍友吹牛逼说我今天又写了多少行代码。但现在仔细回想一下,绝大多数可能都是复制粘贴、增删改查。

进了公司后我才发现,真正写代码的时间很少,像前期的需求评审、跟产品 Battle 啊、方案设计、技术选型啊、沟通、资源协调更重要,也往往更花时间。在你想清楚要不要写代码、怎么写代码后,再去写代码,那时你会发现只不过是一种翻译工具而已。

而且就我观察下来,一般职级越高、工资越高、能力越强的人,写的代码反而越少。并不是他们写不出来,而是他们已经写的太多了、已经有了很多经验、更懂得去利用工具来脱离重复的工作,比如写个自动化脚本、重复代码生成工具之类的。

还有一方面原因是公司需要他们去做更重要的事情,从底层的执行慢慢转变为上层的决策,比如刚刚说的方案设计、或者系统架构。大佬定了个框架,写个 Demo,剩下的就交给我们小码农对吧。

所以代码量是无法真正权衡水平的,在做需求时多去思考更合理的解决方案、写代码时尽量避免重复劳动,才是我们要追求的,

4. 技术决定程序员的水平?

我觉得这个误解和上一个很像啊,如果你觉得程序员的工作就是写代码,写的多、写得快就是强者,那你就真的把自己当成码农了。

我觉得衡量优秀程序员的标准绝不止有技术,比如问题的解决能力,同一件事,你完成的比别人快比别人好;比如业务理解能力,给你一个需求,很快就能判断它是否合理、梳理清楚流程;比如沟通能力,你能够很好地维护用户、组员、同事、跨部门合作者的关系,从杂乱的消息中提取出有效信息;比如产品思维,你能给出更好的建议来推动产品发展;比如管理能力,善于组织成员、推动团队发展。再比如分享表达能力,能把自己学会的东西清晰地讲出来、让别人也能理解,我觉得很酷的事情,也是我一直做分享的原因。

来源:程序员鱼皮

收起阅读 »

React-Native 20分钟入门指南

背景为什么需要React-Native?在React-Native出现之前移动端主流的开发模式是原生开发和Hybrid开发(H5混合原生开发),Hybrid app相较于native app的优势是开发成本低开发速度快(H5页面开发跨平台,无需重新写web、a...
继续阅读 »

背景

为什么需要React-Native?

在React-Native出现之前移动端主流的开发模式是原生开发和Hybrid开发(H5混合原生开发),Hybrid app相较于native app的优势是开发成本低开发速度快(H5页面开发跨平台,无需重新写web、android、ios代码),尽管native app在开发上需要更多时间,但却带来了更好的用户体验(页面渲染、手势操作的流畅性),也正是基于这两点Facebook在2015年推出了React-Native

What we really want is the user experience of the native mobile platforms, combined with the developer experience we have when building with React on the web.

上文摘自React-Native发布稿,React-Native的开发既保留了React的开发效率又拥有媲美原生的用户体验,其运行原理并非使用webview所以不属于Hybrid开发,想了解的可以查看React Native运行原理解析这篇文章。React-Native提出的理念是‘learn once,write every where’,之所以不是‘learn once, run every where’,是因为不同平台的用户体验有所不同,因此要运行全平台仍需要一些额外的适配,这里是Occhino对React-Native的介绍。

React-Native在Github的Star数

React-Native的npm下载数

上面两张图展示了React-Native的对于开发者的热门程度,且官方对其的开发状态一直更新,这也是其能抢占原生开发市场的重要因素。

搭建开发环境

在创建项目前我们需要先搭建React-Native所需的开发环境。
第一步需要先安装nodejs、python2、jdk8(windows有所不同,推荐使用macos开发,轻松省事)

brew install node //macos自带python和jdk

第二步安装React Native CLI

npm install -g react-native-cli

第三步安装Android Studio,参考官方的开发文档

创建第一个应用

使用react-native命令创建一个名为HelloReactNative的项目

react-native init HelloReactNative

等待其下载完相关依赖后,运行项目

react-native run-ios
or
react-native run-android

成功运行后的出现的界面是这样的

react-native-helloworld.png

基本的JSX和ES6语法

先看一下运行成功后的界面代码

/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/


import React, {Component} from 'react';
import {
Platform,
StyleSheet,
Text,
View
} from 'react-native';

const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' +
'Cmd+D or shake for dev menu',
android: 'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});

//noinspection BadExpressionStatementJS
type
Props = {};
//noinspection JSAnnotator
export default class App extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit App.js
</Text>
<Text style={styles.instructions}>
{instructions}
</Text>
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});

代码中出现的importexportextendsclass以及未出现的() =>箭头函数均为ES6需要了解的基础语法,import表示引入需要的模块,export表示导出模块,extends表示继承自某个父类,class表示定义一个类,()=>为箭头函数,用此语法定义的函数带有上下文信息,因此不必再处理this引用的问题。
<Text style={styles.welcome}>Welcome to React Native!</Text>这段代码是JSX语法使用方式,和html标记语言一样,只不过这里引用的是React-Native的组件,Text是一个显示文本的组件,可以看到style={styles.welcome}这是JSX的另一个语法可以将有效的js表示式放入大括号内,Welcome to React Native!为其内容文本,可以尝试修改他的内容为Hello React Native!,刷新界面后

react-native-text.png



熟悉更多的ES6语法有助于更有效率的开发。

组件的属性和状态

在了解了一些基本的JSX和ES6语法后,我们还需要了解两个比较重要的概念即propsstateprops为组件的属性,state为组件的状态,两者间的区别在于,props会在组件被实例化时通过构造参数传入,所以props的传递为单向传递,且只能由父组件控制,state为组件的内部状态由组件自己管理,不受外界影响。propsstate都能修改组件的状态,两者的改变会导致相关引用的组件状态改变,也就是说在组件的内部存在子组件引用了propsstate,那么当发生改变时相应子组件会重新渲染,其实这里也可以看出propsstate的使用联系,父组件可以通过setState修改state,并将其传递到子组件的props中使子组件重新渲染从而使父组件重新渲染。

组件生命周期

image

组件的生命周期会经历三个阶段

Mounting:挂载
Updating:更新
Unmounting:移除

对应的生命周期回调方法为

componentWillMount()//组件将要挂载时调用
render()//组件渲染时调用
componentDidMount()//组件挂载完成时调用
componentWillReceiveProps(object nextProps)//组件props和state改变时调用
shouldComponentUpdate(object nextProps,object nextState)//返回false不更新组件,一下两个方法不执行
componentWillUpdate(object nextProps,object nextState)//组件将要更新时调用
componentDidUpdate(object nextProps,object nextState)//组件完成更新时调用
componentWillUnmount()//组件销毁时调用

这里我们需要重点关注的地方在于组件运行的阶段,组件每一次状态收到更新都会调用render()方法,除非shouldComponentUpdate方法返回false,可以通过此方法对组件做一些优化避免重复渲染带来的性能消耗。

样式

React-Native样式实现了CSS的一个子集,样式的属性与CSS稍有不同,其命名采用驼峰命名,对前端开发者来说基本没差。使用方式也很简单,首先使用StyleSheet创建一个styles

const styles = StyleSheet.create({ 
container:{
flex:1
}
})

然后将对应的style传给组件的style属性,例如<View style={styles.container}/>

常用组件

在日常开发中最常使用的组件莫过于View,Text,Image,TextInput的组件。

View基本上作为容器布局,在里面可以放置各种各样的控件,一般只需要为其设置一个style属性即可,常用的样式属性有flex,width,height,backgroundColor,flexDirector,margin,padding更多可以查看Layout Props

Text是一个显示文本的控件,只需要在组件的内容区填写文字内容即可,例如<Text>Hello world</Text>,可以为设置字体大小和颜色<Text style={{fontSize:14,color:'red'}}>Hello world</Text>,同时也支持嵌套Text,例如

<Text style={{fontWeight: 'bold'}}>
I am bold
<Text style={{color: 'red'}}>
and red
</Text>
</Text>

TextInput是文本输入框控件,其使用方式也很简单

<TextInput
style={{width:200,height:50}}
onChangeText={(text)=>console.log(text)}
/>

style设置了他的样式,onChangeText传入一个方法,该方法会在输入框文字发生变化时调用,这里我们使用console.log(text)打印输入框的文字。

Image是一个图片控件,几乎所有的app都会使用图片作为他们的个性化展示,Image可以加载本地和网络上的图片,当加载网络图片时必须设定控件的大小,否则图片将无法展示

加载本地图片,图片地址为相对地址
<Image style={{width:100,height:100}} source={require('../images/img001.png')}/>
加载网络图片
<Image style={{width:100,height:100}} source={{uri:'https://facebook.github.io/react-native/docs/assets/favicon.png'}}/>
收起阅读 »

新建一个简单的React-Native工程

一、环境配置(1)需要一台Mac(OSX)(2)在Mac上安装Xcode(3)安装node.js:https://nodejs.org/download/(4)建议安装watchman,终端命令:brew install watchman(5)安装flow:b...
继续阅读 »

一、环境配置

(1)需要一台Mac(OSX)

(2)在Mac上安装Xcode

(3)安装node.js:https://nodejs.org/download/

(4)建议安装watchman,终端命令:brew install watchman

(5)安装flow:brew install flow

ok,按照以上步骤,你应该已经配置好了环境。

二、Hello, React-Native

现在我们需要创建一个React-Native的项目,因此可以按照下面的步骤:

打开终端,开始React-Native开发的旅程吧。

(1)安装命令行工具:sudo npm install -g react-native-cli

(2)创建一个空项目:react-native init HelloWorld

(3)找到创建的HelloWorld项目,双击HelloWorld.xcodeproj即可在xcode中打开项目。xcodeproj是xcode的项目文件。

(4)在xcode中,使用快捷键cmd + R即可启动项目。基本的Xcode功能可以熟悉,比如模拟器的选择等。

启动完成后,你会看到React-Packger和iOS模拟器,具体的效果如下,说明你创建项目成功了。

Xcode10 上创建RN工程报错:error: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src:

解决办法:

不要直接使用 react-native init HelloWorld 创建项目,

后面加个 --version 0.45.0 之前的版本就好了,

比如:

react-native init HelloWorld --version 0.44.0

收起阅读 »

react-native自定义原生组件

使用react-native的时候能够看到不少函数调用式的组件,像LinkIOS用来呼起url请求 LinkIOS.openUrl('http://www.163.com');复制actionSheetIOS用来实现ios客户端底部弹起的选择对话框Action...
继续阅读 »

使用react-native的时候能够看到不少函数调用式的组件,像LinkIOS用来呼起url请求

 LinkIOS.openUrl('http://www.163.com');

actionSheetIOS用来实现ios客户端底部弹起的选择对话框

ActionSheetIOS.showActionSheetWithOptions({
options: BUTTONS,
cancelButtonIndex: CANCEL_INDEX,
destructiveButtonIndex: DESTRUCTIVE_INDEX,
},
(buttonIndex) => { this.setState({ clicked: BUTTONS[buttonIndex] });
});

这些组件的使用方式都大同小异,通过声明一个native module,然后在这个组件内部通过底层实现方法的具体内容

像ActionSheetIOS在使用的时候,首先需要在工程的pod库中添加ActionSheetIOS对应的RCTActionSheet

pod 'React', :path => 'node_modules/react-native', :subspecs => ['Core','RCTActionSheet'# Add any other subspecs you want to use in your project]

我们可以看到RCTActionSheet相关的实现的代码是放在react-native/Libraries/ActionSheetIOS下的


整个工程包含3个代码文件,ActionSheetIOS.js、RCTActionSheetManager.h、RCTActionSheetManager.m

ActionSheetIOS.js内容很简单,先是定义了引用oc代码的方式

var RCTActionSheetManager = require('NativeModules').ActionSheetManager;

然后定义了ActionSheetIOS组件,并export

var ActionSheetIOS = {
showActionSheetWithOptions(options: Object, callback: Function) {
invariant( typeof options === 'object' && options !== null, 'Options must a valid object'
);
invariant( typeof callback === 'function', 'Must provide a valid callback'
);
RCTActionSheetManager.showActionSheetWithOptions(
{...options, tintColor: processColor(options.tintColor)},
callback
);
},
.....,

};module.exports = ActionSheetIOS;

我们看到关键是引入底层oc的方式,其他的跟写前端没啥差别

然后再看RCTActionSheetManager的实现

#import "RCTBridge.h"@interface RCTActionSheetManager : NSObject@end

主要是实现了RCTBridgeModule这个协议,这个协议是实现前端js-》oc的主要中间件,感兴趣的可以看看实现,

然后就是对RCTActionSheetManager的实现的代码,关键几句

@implementation RCTActionSheetManager
{
// Use NSMapTable, as UIAlertViews do not implement // which is required for NSDictionary keys
NSMapTable *_callbacks;}

RCT_EXPORT_MODULE()
...
RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options
callback:(RCTResponseSenderBlock)callback
)
{
...
}

主要是RCT_EXPORT_MODULE用来注册react-native module ,然后具体的实现方法放在RCT_EXPORT_METHOD开头的函数内

RCT开头的宏用来区分react-native函数与原声的函数,jspatch的bang有过具体分析,感兴趣的可以看看

http://blog.cnbang.net/tech/2698/

所以我们自己实现一个原生的react-native组件的时候,完全可以照着actionSheetIOS来做

在前端自定义一个js,通过require('NativeModules').XXX 引入

然后在底层实现RCTBridgeModule的类,在类里把RCT_EXPORT_MODULE、RCT_EXPORT_METHOD加上即可


转载自 https://cloud.tencent.com/developer/article/1896500

收起阅读 »

iOS10-iOS15主要适配回顾

iOS
ios15适配1、UITabar、NaBar新增scrollEdgeAppearance,来描述滚动视图滚动到bar边缘时的外观,即使没有滚动视图也需要去指定scrollEdgeAppearance,否则可能导致bar的背景设置无效。具体可以参考UIBarAp...
继续阅读 »

ios15适配

  • 1、UITabar、NaBar新增scrollEdgeAppearance,来描述滚动视图滚动到bar边缘时的外观,即使没有滚动视图也需要去指定scrollEdgeAppearance,否则可能导致bar的背景设置无效。具体可以参考UIBarAppearance
  • 2、tableView 增加sectionHeaderTopPadding属性,默认值是UITableViewAutomaticDimension,可能会使tableView sectionHeader多处一段距离,需要设置 为
  • 3、IDFA 请求权限不弹框问题,解决参考iOS15 ATTrackingManager请求权限不弹框
  • 4、iOS15终于迎来了UIButton的这个改动

ios14适配

  • 1、更改了cell布局视图,之前将视图加载在cell上,将会出现contentView遮罩,导致事件无法响应,必须将customView 放在 contentView 上
  • 2、UIDatePicker默认样式不再是以前的,需要设置preferredDatePickerStyle为 UIDatePickerStyleWheels。
  • 3、IDFA必须要用户用户授权处理,否则获取不到IDFA
  • 4、 UIPageControl的变化 具体参考iOS 14 UIPageControl对比、升级与适配

ios13适配

-1、 iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整

  • 2、支持第三方登录必须,就必须Sign In with Apple
  • 3、MPMoviePlayerController 废弃
  • 4、iOS 13 DeviceToken有变化
  • 5、模态弹出默认不再是全屏。
  • 6、私有方法 KVC 不允许使用
  • 7、蓝牙权限需要申请
  • 8、LaunchImage 被弃用
  • 9、新出UIBarAppearance统一配置navigation bars、tab bars、 toolbars等bars的外观。之前设置na bar和tab bar外观的方法可能会无效

ios12适配

  • 1、C++ 标准库libstdc++相关的3个库(libstdc++、libstdc++.6、libstdc++6.0.9 )废弃,使用libc++代替
  • 2、短信 验证码自动填充api
if (@available(iOS 12.0, *)) {
codeTextFiled.textContentType = UITextContentTypeOneTimeCode;
}

ios11适配

  • 1、ViewController的automaticallyAdjustsScrollViewInsets属性被废弃,用scrollView的contentInsetAdjustmentBehavior代替。
  • 2、safeAreaLayoutGuide的引入
  • 3、tableView默认开启了Size-self
  • 4、新增的prefersLargeTitles属性
  • 5、改善圆角,layer新增了maskedCorners属性
  • 6、tableView右滑删除新增api
  • 7、导航条的层级发生了变化。
    ios11适配相关

ios10适配

  • 1、通知统一使用UserNotifications.framework框架
  • 2、UICollectionViewCell的的优化,新增加Pre-Fetching预加载机制
  • 3、苹果加强了对隐私数据的保护,要对隐私数据权限做一个适配,iOS10调用相机,访问通讯录,访问相册等都要在info.plist中加入权限访问描述,不然之前你们的项目涉及到这些权限的地方就会直接crash掉。
  • 4、AVPlayer增加了多个属性,timeControlStatus、
    automaticallyWaitsToMinimizeStalling
  • 5、tabar未选中颜色设置 用 unselectedItemTintColor代替
收起阅读 »

iOS安全–浅谈关于iOS加固的几种方法

iOS
关于IOS安全这方面呢,能做的安全保护确实要比Android平台下面能做的少很多。 只要你的手机没越狱,基本上来说是比较安全的,当然如果你的手机越狱了,可能也会相应的产生一些安全方面的问题。就比如我在前面几篇博客里面所介绍的一些IOS逆向分析,动态分析以及破...
继续阅读 »

关于IOS安全这方面呢,能做的安全保护确实要比Android平台下面能做的少很多。
只要你的手机没越狱,基本上来说是比较安全的,当然如果你的手机越狱了,可能也会相应的产生一些安全方面的问题。就比如我在前面几篇博客里面所介绍的一些IOS逆向分析,动态分析以及破解方法。
但是尽管这样,对IOS保护这方面来说,需求还不是很乏,所有基于IOS平台的加固产品也不是很多,目前看到几种关于IOS加固的产品也有做的比较好的。
最开始关于爱加密首创的IOS加密,http://www.ijiami.cn/ios 个人感觉这只是一个噱头而已,因为没有看到具体的工具以及加固应用,所以也不知道它的效果怎么样了。
后来在看雪上面看到一个http://www.safengine.com/mobile/ 有关于IOS加密的工具,但是感觉用起来太麻烦了,而且让产品方也不是很放心,要替换xcode默认的编译器。
不久前看到偶然看到一个白盒加密的应用http://kiwisec.com/ 也下下来试用了一下,感觉要比上面两个从使用上方面了许多,而且考虑的东西也是比较多的。
好了,看了别人做的一些工具,这里大概说下都有哪些加固方法以及大概的实现吧,本人也是刚接触这个方面不就,可能分析的深度没有那么深入,大家就随便听听吧。
现在的加固工具总的来说都是从以下几个方面来做的:
一、字符串加密:
现状:对于字符串来说,程序里面的明文字符串给静态分析提供了极大的帮助,比如说根据界面特殊字符串提示信息,从而定义到程序代码块,或者获取程序使用的一些网络接口等等。
加固:对程序中使用到字符串的地方,首先获取到使用到的字符串,当然要注意哪些是能加密,哪些不能加密的,然后对字符串进行加密,并保存加密后的数据,再在使用字符串的地方插入解密算法,这样就很好的保护了明文字符串。
二、类名方法名混淆
现状:目前市面上的IOS应用基本上是没有使用类名方法名混淆的,所以只要我们使用class-dump把应用的类和方法定义dump下来,然后根据方法名就能够判断很多程序的处理函数是在哪。从而进行hook等操作。
加固:对于程序中的类名方法名,自己产生一个随机的字符串来替换这些定义的类名和方法名,但是不是所有类名,方法名都能替换的,要过滤到系统有关的函数以及类,可以参考下开源项目:https://github.com/Polidea/ios-class-guard
三、程序代码混淆
现状:目前的IOS应用找到可执行文件然后拖到Hopper Disassembler或者IDA里面程序的逻辑基本一目了然。
加固:可以基于Xcode使用的编译器clang,然后在中间层也就是IR实现自己的一些混淆处理,比如加入一些无用的逻辑块啊,代码块啊,以及加入各种跳转但是又不影响程序原有的逻辑。可以参考下开源项目:https://github.com/obfuscator-llvm/obfuscator/ 当然开源项目中也是存在一些问题的,还需自己再去做一些优化工作。
四、加入安全SDK
现状:目前大多数IOS应用对于简单的反调试功能都没有,更别说注入检测,以及其它的一些检测了。
加固:加入SDK,包括多处调试检测,注入检测,越狱检测,关键代码加密,防篡改等等功能。并提供接口给开发者处理检测结果。

当然除了这些外,还有很多方面可以做加固保护的,相信大家会慢慢增加对IOS应用安全的意识,保护好自己的APP。

收起阅读 »

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图表结合使用,来完成复杂的拖拽、排序、等功能,但是这个组件实现细节还有很多,本文只能提供一个大值的思路,还是希望能够帮助到大家,给大家提供一个思路,欢迎留言和我讨论,如果你有什么更好的办法实现类似的功能


结束语 🌞



那么我的如何编写复杂拖拽组件🐣就结束了,文章的目的其实很简单,就是对日常工作的总结和输出,输出一些觉得对大家有用的东西,菜不菜不重要,但是热爱🔥,希望大家能够喜欢我的文章,我真的很用心在写,也希望通过文章认识更多志同道合的朋友,如果你也喜欢折腾,欢迎加我好友,一起沙雕,一起进步

收起阅读 »

前端取消请求与取消重复请求

一、前言 大家好,我是大斌,一名野生的前端工程师,今天,我想跟大家分享几种前端取消请求的几种方式。相信大家在平时的开发中,肯定或多或少的会遇到需要取消重复请求的场景,比如最常见的,我们在使用tab栏时,我们都会使用一个盒子去存放内容,然后在切换tab栏时,会清...
继续阅读 »





一、前言


大家好,我是大斌,一名野生的前端工程师,今天,我想跟大家分享几种前端取消请求的几种方式。相信大家在平时的开发中,肯定或多或少的会遇到需要取消重复请求的场景,比如最常见的,我们在使用tab栏时,我们都会使用一个盒子去存放内容,然后在切换tab栏时,会清除掉原来的内容,然后替换上新的内容,这个时候,如果我们的数据是通过服务从后端获取的,就会存在一个问题,由于获取数据是需要一定的时间的,就会存在当我们切换tab栏到新的tab页时,原来的tab页的服务还在响应中,这时新的tab页的数据服务已经响应完成了,且页面已经显示了新的tab页的内容,但是,这个时候旧的tab页的数据也成功了并返回了数据,并将新的tab页的内容覆盖了。。。所以为了避免这种情况的发生,我们就需要在切换tab栏发送新的请求之前,将原来的的请求取消掉,至于如何取消请求,这便是今天我要讲的内容。


二、项目准备


在正式学习之前,我们先搭建一个项目,并还原刚刚所说的场景,为了节省时间,我们使用脚手架搭建了一个前端vue+TS+vite项目,简单的做了几个Demo,页面如下,上面是我们现实内容的区域,点击tab1按钮时获取并展示tab1的内容,点击tab2按钮时获取并展示tab2的内容,以此类推,内容比较简单,这里就不放具体代码了。


image.png


然后我们需要搭建一个本地服务器,这里我们新建一个app.ts文件,使用express以及cors解决跨域问题去搭建一个简单的服务器,具体代码如下:

 
// app.ts
const express = require('express')
const app = express()

const cors = require('cors')
app.use(cors())

app.get('/tab1', (req, res) => {
res.send('这是tab1的内容...')
})

app.get('/tab2', (req, res) => {
setTimeout(() => {
res.send('这是tab2的内容...')
}, 3000)
})

app.get('/tab3', (req, res) => {
res.send('这是tab3的内容...')
})

app.listen('3000', () => {
console.log('server running at 3000 port...')
})



上面代码,我们新建了一个服务器并让他运行在本地的3000端口,同时在获取tab2的内容时,我们设置了3秒的延迟,以便实现我们想要的场景,然后我们使用node app.ts启动服务器,当终端打印了server running at 3000 port...就说明服务器启动成功了。


然后我们使用axios去发送请求,安装axios,然后我们在项目中src下面新建utils文件夹,然后新建request.ts文件,具体代码如下:


作者:还是那个大斌啊
链接:https://juejin.cn/post/7108359238598000671
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


import axios, { AxiosRequestConfig } from 'axios'

// 新建一个axios实例
const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

export function request(Args: AxiosRequestConfig) {
return ins.request(Args)
}



这里我们新建了一个axios实例,并配置了baseURL和超时时间,并做了一个简单的封装然后导出,需要注意的是,axios请求方法的别名有很多种,如下图这里就不做过多介绍了,大家想了解的可以去看官网,我们这里使用request方法。


image.png


最后,我们在页面上引入并绑定请求:

// bar.vue
<script setup lang="ts">
import { ref } from 'vue'
import { request } from '@/utils/request'

const context = ref('tab1的内容...')

const getTab1Context = async () => {
const { data } = await request({
url: '/tab1',
})

context.value = data
}
const getTab2Context = async () => {
const { data } = await request({
url: '/tab2',
})

context.value = data
}
const getTab3Context = async () => {
const { data } = await request({
url: '/tab3',
})

context.value = data
}
</script>



为了方便理解,将template部分代码也附上:

// bar.vue
<template>
<div class="container">
<div class="context">{{ context }}</div>
<div class="btns">
<el-button type="primary" @click="getTab1Context">tab1</el-button>
<el-button type="primary" @click="getTab2Context">tab2</el-button>
<el-button type="primary" @click="getTab3Context">tab3</el-button>
</div>
</div>
</template>



到这里,我们的项目准备工作就好了,看下效果图


取消请求1.gif


然后看下我们前面提到的问题:


取消请求2.gif
注意看,在我点击了tab2之后立马点击tab3,盒子中会先显示tab3的内容,然后又被tab2的内容覆盖了。




三、原生方法


项目准备好之后,我们就可以进入正题了,其实,关于取消请求的方法,axios官方就已经有了,所以我们先来了解下使用axios原生的方法如何取消请求:
先看下官方的代码:


可以使用 CancelToken.source 工厂方法创建 cancel token 像这样:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else { /* 处理错误 */ }
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
});

// 取消请求 (message 参数是可选的)
source.cancel('Operation canceled by the user.');



同时还可以通过传递一个executor函数到CancelToken的构造函数来创建 cancel token :

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});

// 取消请求
cancel();



这是官方提供的两种方法,我们将他们用到我们的项目上,因为都差不多,所以我们这里就只演示一种,选择通过传递函数的方式来取消请求;


进入项目utils文件夹下的request.ts文件,修改代码如下:

 
// request.ts

import axios, { AxiosRequestConfig } from 'axios'
const CancelToken = axios.CancelToken

const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

// 新建一个取消请求函数并导出
export let cancelFn = (cancel: string) => {
console.log(cancel)
}
export function request(Args: AxiosRequestConfig) {
// 在请求配置中增加取消请求的Token
Args.cancelToken = new CancelToken(function (cancel) {
cancelFn = cancel
})
return ins.request(Args)
}



然后我们就可以在想要取消请求的地方调用cancelFn函数就可以了,我们给tab1tab3按钮都加上取消请求功能:

// bar.vue

<script setup lang="ts">
import { ref } from 'vue'
import { request, cancelFn } from '@/utils/request'

const context = ref('tab1的内容...')

const getTab1Context = async () => {
cancelFn('取消了tab2的请求')
const { data } = await request({
url: '/tab1',
})

context.value = data
}
const getTab2Context = async () => {
const { data } = await request({
url: '/tab2',
})

context.value = data
}
const getTab3Context = async () => {
cancelFn('取消了tab2的请求')
const { data } = await request({
url: '/tab3',
})

context.value = data
}
</script>



这样取消请求的功能就完成了,看下效果:


取消请求3.gif


四、promise


除了官网的方式之外,其实我们也可以借助Promise对象,我们都知道,Promise对象的状态一旦确定就不能再改变的,基于这个原理,我们可以使用Promise封装下我们的请求,然后通过手动改变Promise的状态去阻止请求的响应,看下面代码:

 
// request.ts

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'

const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

// 新建一个取消请求函数并导出
export let cancelFn = (cancel: string) => {
console.log(cancel)
}

export function request(Args: AxiosRequestConfig): Promise<AxiosResponse> {
return new Promise((resolve, reject) => {
ins.request(Args).then((res: AxiosResponse) => {
resolve(res)
})
cancelFn = (msg) => {
reject(msg)
}
})
}



效果也是一样的


取消请求4.gif


需要注意的是,虽然效果是一样的,但是使用Promise的方式,我们只是手动修改了Promise的状态为reject,但是请求还是一样发送并响应了,没有取消,这个是和使用Axios原生方法的不同之处。


五、借助Promise.race


讲完了取消请求,其实还有一种场景也很常见,那就是取消重复请求,如果是要取消重复请求,我们又该怎么实现呢?其实我们可以借助Promise.racePromise.race的作用就是将多个Promise对象包装成一个,即它接受一个数组,每一个数组成员都是一个Promise对象,只要这些成员中有一个状态改变,Promise.race的状态就随之改变,基于这个原理,我们可以实现取消重复请求请求的目的。


基本思路就是,我们给每一个请求身边都放一个Promise对象,这个对象就是一颗炸弹,将他们一起放到Promise.race里面,当我们需要取消请求的时候就可以点燃这颗炸药。


还是上面的例子,我们针对按钮tab2做一个取消重复请求的功能,我们先声明一个类,在里面做取消重复请求的功能,在utils下新建cancelClass.ts文件:

 
// cancelClass.ts

import { AxiosResponse } from 'axios'
export class CancelablePromise {
pendingPromise: any
reject: any
constructor() {
this.pendingPromise = null
this.reject = null
}

handleRequest(requestFn: any): Promise<AxiosResponse> {
if (this.pendingPromise) {
this.cancel('取消了上一个请求。。。')
}
const promise = new Promise((resolve, reject) => (this.reject = reject))
this.pendingPromise = Promise.race([requestFn(), promise])
return this.pendingPromise
}

cancel(reason: string) {
this.reject(reason)
this.pendingPromise = null
}
}

上面代码中,我们声明了一个类,然后在类中声明了两个属性pendingPromisereject,一个request请求方法用来封装请求并判断上一个请求是否还在响应中,如果还未响应则手动取消上一次的请求,同时声明了一个promise对象,并将他的reject方法保存在类的reject属性中,然后用promise.race包装了请求函数和刚刚声明的promise对象。最后声明了一个cancel方法,在cancel方法中触发reject函数,来触发promise对象的状态改变,这样就无法获取到reuestFn的响应数据了。从而达到了取消请求的目的;


因为requestFn必须是一个函数,所以我们需要改装下Axiosrequest函数,让他返回一个函数;

 

因为requestFn必须是一个函数,所以我们需要改装下Axiosrequest函数,让他返回一个函数;


// request.ts

export function request(Args: AxiosRequestConfig) {
return () => ins.request(Args)
}

最后在页面中引入并使用:


// bar.vue

<script setup lang="ts">
import { ref } from 'vue'
import { request, cancelFn } from '@/utils/request'
import { CancelablePromise } from '@/utils/cancelClass'

...
const cancelablePromise = new CancelablePromise()
...
const getTab2Context = async () => {
const { data } = await cancelablePromise.handleRequest(
request({
url: '/tab2',
})
)

context.value = data
}
</script>

最后看下效果


取消请求5.gif


六、总结


到这里,我们前端取消请求和取消重复请求的方法就学习完了,需要注意的是,即使是使用官方的方法,也仅仅是取消服务器还没接收到的请求,如果请求已经发送到了服务端是取消不了的,只能让后端同时去处理了,使用promise的方法,仅仅只是通过改变promise的状态来阻止响应结果的接收,服务还是照常发送的。今天的分享就到这里了,如果对你有帮助的,请给我一个赞吧!

 










 


收起阅读 »

100w的数据表比1000w的数据表查询更快吗?

当我们对一张表发起查询的时候,是不是这张表的数据越少,查询的就越快?答案是不一定,这和mysql B+数索引结构有一定的关系。innodb逻辑存储结构从Innodb存储引擎的逻辑存储结构来看,所有数据都被逻辑的放在一个表空间(tablespace)中,默认情况...
继续阅读 »

当我们对一张表发起查询的时候,是不是这张表的数据越少,查询的就越快?

答案是不一定,这和mysql B+数索引结构有一定的关系。

innodb逻辑存储结构

从Innodb存储引擎的逻辑存储结构来看,所有数据都被逻辑的放在一个表空间(tablespace)中,默认情况下,所有的数据都放在一个表空间中,当然也可以设置每张表单独占用一个表空间,通过innodb_file_per_table来开启。

mysql> show variables like 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON   |
+-----------------------+-------+
1 row in set (0.00 sec)

表空间又是由各个段组成的,常见的有数据段,索引段,回滚段等。因为innodb的索引类型是b+树,那么数据段就是叶子结点,索引段为b+的非叶子结点。

段空间又是由区组成的,在任何情况下,每个区的大小都为1M,innodb引擎一般默认页的大小为16k,一般一个区中有64个连续的页(64*16k=1M)。

通过段我们知道,还存在一个最小的存储单元页。它是innodb管理的最小的单位,默认是16K,当然也可以通过innodb_page_size来设置为4K、8K...,我们的数据都是存在页中的

mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name   | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.00 sec)

所以innodb的数据结构应该大致如下:


B+ 树

b+树索引的特点就是数据存在叶子结点上,并且叶子结点之间是通过双向链表方式组织起来的。

假设存在这样一张表:

CREATE TABLE `user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '',
`age` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

聚集索引

对于主键索引id,假设它的b+树结构可能如下:


  • 此时树的高度是2

  • 叶子节点之间双向链表连接

  • 叶子结点除了id外,还存了name、age字段(叶子结点包含整行数据)

我们来看看 select * from user where id=30 是如何定位到的。

  • 首先根据id=30,判断在第一层的25-50之间

  • 通过指针找到在第二层的p2中

  • 把p2再加载到内存中

  • 通过二分法找到id=30的数据

总结:可以发现一共发起两次io,最后加载到内存检索的时间忽略不计。总耗时就是两次io的时间。

非聚集索引

通过表结构我们知道,除了id,我们还有name这个非聚集索引。所以对于name索引,它的结构可能如下:


  • 此时树的高度是2

  • 叶子节点之间双向链表连接

  • 叶子结点除了name外,还有对应的主键id

我们来看看 select * from user where name=jack 是如何定位到的。

  • 首先根据 name=jack,判断在第一层的mary-tom之间

  • 通过指针找到在第二层的p2中

  • 把p2再加载到内存中

  • 通过二分法找到name=jack的数据(只有name和id)

  • 因为是select *,所以通过id再去主键索引查找

  • 同样的原理最终在主键索引中找到所有的数据

总结:name查询两次io,然后通过id再次回表查询两次io,加载到内存的时间忽略不计,总耗时是4次io。另外,搜索公众号GitHub猿后台回复“天猫”,获取一份惊喜礼包。

一棵树能存多少数据

以上面的user表为例,我们先看看一行数据大概需要多大的空间:通过show table status like 'user'\G

mysql> show table status like 'user'\G
*************************** 1. row ***************************
          Name: user
        Engine: InnoDB
      Version: 10
    Row_format: Dynamic
          Rows: 10143
Avg_row_length: 45
  Data_length: 458752
Max_data_length: 0
  Index_length: 311296
    Data_free: 0
Auto_increment: 10005
  Create_time: 2021-07-11 17:22:56
  Update_time: 2021-07-11 17:31:52
    Check_time: NULL
    Collation: utf8mb4_general_ci
      Checksum: NULL
Create_options:
      Comment:
1 row in set (0.00 sec)

我们可以看到Avg_row_length=45,那么一行数据大概占45字节,因为一页的大小是16k,那么一页可以存储的数据是16k/45b = 364行数据,这是叶子结点的单page存储量。

以主键索引id为例,int占用4个字节,指针大小在InnoDB中占6字节,这样一共10字节,从root结点出来多少个指针,就可以知道root的下一层有多少个页。因为root结点只有一页,所以此时就是16k/10b = 1638个指针。

  • 如果树的高度是2,那么能存储的数据量就是1638 * 364 = 596232

  • 如果树的高度是3,那么能存储的数据量就是1638 * 1638 * 364 = 976628016


如何知道一个索引树的高度

innodb引擎中,每个页都包含一个PAGE_LEVEL的信息,用于表示当前页所在索引中的高度。默认叶子节点的高度为0,那么root页的PAGE_LEVEL + 1就是这棵索引的高度。


那么我们只要找到root页的PAGE_LEVEL就行了。

通过以下sql可以定位user表的索引的page_no:

mysql> SELECT b.name, a.name, index_id, type, a.space, a.PAGE_NO FROM information _schema.INNODB_SYS_INDEXES a, information _schema.INNODB_SYS_TABLES b WHERE a.table_id = b.table_id AND a.space <> 0 and b.name='test/user';
+-----------+---------+----------+------+-------+---------+
| name     | name   | index_id | type | space | PAGE_NO |
+-----------+---------+----------+------+-------+---------+
| test/user | PRIMARY |     105 |   3 |   67 |       3 |
| test/user | name   |     106 |   0 |   67 |       4 |
+-----------+---------+----------+------+-------+---------+
2 rows in set (0.00 sec)

可以看到主键索引的page_no=3,因为PAGE_LEVEL在每个页的偏移量64位置开始,占用两个字节。所以算出它在文件中的偏移量:16384*3 + 64 = 49152 + 64 =49216,再取前两个字节就是root的PAGE_LEVEL了。

通过以下命令找到ibd文件目录

show global variables like "%datadir%" ;
+---------------+-----------------------+
| Variable_name | Value                 |
+---------------+-----------------------+
| datadir       | /usr/local/var/mysql/ |
+---------------+-----------------------+
1 row in set (0.01 sec)

user.ibd/usr/local/var/mysql/test/下。

通过hexdump来分析data文件。

hexdump -s 49216 -n 10 user.ibd
000c040 00 01 00 00 00 00 00 00 00 69
000c04a
000c040 00 01 00 00 00 00 00 00 00 69

00 01就是说明PAGE_LEVEL=1,那么树的高度就是1+1=2

回到题目

100w的数据表比1000w的数据表查询更快吗?通过查询的过程我们知道,查询耗时和树的高度有很大关系。如果100w的数据如果和1000w的数据的树的高度是一样的,那其实它们的耗时没什么区别。

来源:juejin.cn/post/6984034503362609165

收起阅读 »

PHP语法和PHP变量

PHP
一.PHP语言标记在一个后缀为.php的文件立马,以<?php ?>开始和结束的文件,就是php标记文件,具体格式如下:1.xml风格,是PHP的标准风格,推荐使用 2.简短风格,遵循SGML处理。需要在php.ini中将指令short_open...
继续阅读 »

一.PHP语言标记

在一个后缀为.php的文件立马,以<?php ?>开始和结束的文件,就是php标记文件,具体格式如下:

1.xml风格,是PHP的标准风格,推荐使用

2.简短风格,遵循SGML处理。需要在php.ini中将指令short_open_tag打开,或者在php编译时加入–enable-short-tags.如果你想你的程序移植性好,就抛弃这种风格,它就比1.1少了个php

3.ASP 风格(已移除)

种标记风格与 ASP 或 ASP.NET 的标记风格相同,默认情况下这种风格是禁用的。如果想要使用它需要在配置设定中启用了 asp_tags 选项。
不过该标记风格在 PHP7 中已经不再支持,了解即可。

4.SCRIPT 风格(已移除)

种标记风格是最长的,如果读者使用过 JavaScript 或 VBScript,就会熟悉这种风格。该标记风格在 PHP7 中已经不再支持,了解即可。
注意:如果文件内容是纯 PHP 代码,最好将文件末尾的 PHP 结束标记省略。这样可以避免在 PHP 结束标记之后,意外插入了空格或者换行符之类的误操作,而导致输出结果中意外出现空格和换行

位置

可以将PHP语言放在后缀名为.php的HTML文件的任何地方。注意了,是以.php结尾的HTML文件。比如

PHP 注释规范

单行注释 每行必须单独使用注释标记,称为单行注释。它用于进行简短说明,形如 //php

多行注释

多行注释用于注释多行内容,经常用于多行文本的注释。注释的内容需要包含在(/* 和 */)中,以“/*”开头,以“*/结尾

php里面常见的几种注释方式

1.文件头的注释,介绍文件名,功能以及作者版本号等信息

2.函数的注释,函数作用,参数介绍及返回类型

3.类的注释


二.PHP变量

什么是变量呢?

程序中的变量源于数学,在程序语言中能够储存结果或者表示抽象概念。简单理解变量就是临时存储值的容器,它可以储存数字、文本、或者一些复杂的数据等。变量在 PHP 中居于核心地位,是使用 PHP 的关键所在,变量的值在程序运行中会随时发生变化,能够为程序中准备使用的一段数据起一个简短容易记的名字,另外它还可以保存用户输入的数据或运算的结果。

声明(创建)变量

因为 PHP 是一种弱类型的语言,所以使用变量前不用提前声明,变量在第一次赋值时会被自动创建,这个原因使得 PHP 的语法和C语言、Java 等强类型语言有很大的不同。声明 PHP 变量必须使用一个美元符号“$”后面跟变量名来表示,然后再使用“=”给这个变量赋值。如下所示


变量命名规则

变量名并不是可以随意定义的,一个有效的变量名应该满足以下几点要求:
1. 变量必须以 $ 符号开头,其后是变量的名称,$ 并不是变量名的一部分;
2. 变量名必须以字母或下划线开头;
3. 变量名不能以数字开头;
4.变量名只能包含字母(A~z)、数字(0~9)和下划线(_);
5.与其它语言不通的是,PHP 中的一些关键字也可以作为变量名(例如 $true、$for)。
注意:PHP 中的变量名是区分大小写的,因此 $var 和 $Var 表示的是两个不同的变量

错误的变量命名示范


当使用多个单词构成变量名时,可以使用下面的命名规范:
下划线命名法:将构成变量名的单词以下划线分割,例如 $get_user_name、$set_user_name;
驼峰式命名法(推荐使用):第一个单词全小写,后面的单词首字母小写,例如 $getUserName、$getDbInstance;
帕斯卡命名法:将构成变量名的所有单词首字母大写,例如 $Name、$MyName、$GetName。
收起阅读 »

PHP 基本语法2

PHP
一、PHP 标记PHP 也是通过标记来识别的,像 JSP 的 <% %> 的一样,PHP 的最常用的标记是:<?php php 代码 ?> 。 以 “<?” 开始,“?>”结束。 该风格是最简单的标记风格,默认是禁止的,可...
继续阅读 »

一、PHP 标记

PHP 也是通过标记来识别的,像 JSP 的 <% %> 的一样,PHP 的最常用的标记是:<?php php 代码 ?>

以 “<?” 开始,“?>”结束。
该风格是最简单的标记风格,默认是禁止的,可以通过修改 short_open_tag 选项来允许使用这种风格。

[捂脸哭] 我们其实目前不需要去配置这个风格哈,老老实实用 <?php php 代码 ?> 就够了~

二、基础语法

1. PHP 语句都以英文分号【;】结束。

2. PHP 注释

大体上有三种:

<?php
/*
多行注释
*/

echo "string";// 单行注释
echo "string";# 单行注释
?>

sublime text 3 神奇快捷键:ctrl shift d => 复制当前行到下一行

3. 输出语句:echo

<?php
echo "string";
echo("string");
?>

PHP 可以嵌套在 HTML 里面写,所以也可以输出 HTML、CSS、JavaScript 语句等。

 <font id="testPhpJs"></font>
<?php
echo "<style type='text/css'>#testPhpJs {color: red}</style>";
echo "<h1>一级标题</h1>";
echo "<script>var font = document.getElementById('testPhpJs');font.innerText='php输出js填充的文字';</script>";
?>
<input type="text" name="test" value="<?php echo "123"; ?>">


网页输出结果:

4. 变量及变量类型

PHP 的类型有六种,整型、浮点型、字符串、布尔型、数组、对象。

但是定义的方式只有一种:$ 变量名。PHP 变量的类型会随着赋值的改变而改变(动态类型)

<?php
$variable = 1; //整型
$variable = 1.23; //浮点型
$variable = "字符串"; //字符串 ""
$variable = '字符串'; //字符串 ''
$variable = false; //布尔型
?>

特殊的变量(见附录)。

5. 字符串

关于字符串,我们还有几点需要说的:

a. 双引号和单引号

这两者包起来的都是字符串:'阿'"阿"。注意单引号里不能再加单引号,双引号里不能再加双引号,实在要加的话记得用转义符 “ \

b. 定界符

如果想输出很大一段字符串,那么就需要定界符来帮忙。定界符就是由头和尾两部分。

<?php
echo <<<EOT
hello world!
lalala~
EOT;
// 这个定界符的尾巴和前面<<<后面的字符应该一样
// !定界符的尾巴必须靠在最左边
?>

定界符的名字是自己起的,乐意叫啥就叫啥,但是它的尾巴必须靠在最左边,不能有任何其他的字符!空格也不行:

<?php
//定界符的名字随便起
echo <<<ERROR
ERROR;
//但是尾巴必须靠左,前面不能有任何东西。比如这样就是错的 ↑
?>

看!上面这个注释都变成绿色了~ 它都报错了,大家写的时候可不能这么写哦~O(∩_∩)O哈哈~

6. 字符串连接

不同于 Java 的 “+” 号连接符,PHP 用的是点【.】。在做数据库查询语句的时候,常会遇到要与变量拼接的情况。这里给个小技巧:

在数据库相关软件中先用一个数据例子写好查询语句,并测试直到执行成功:

然后将数据换成变量:

  1. 将 sql 语句用字符串变量存储。
  2. 将写死的数据换成两个双引号
  3. 在双引号中间加两个连接符 点【.】
  4. 在连接符中间将变量放入
<?php
$isbn = "9787508353937";//存储isbn的变量
$sql = "SELECT * FROM bookinfo WHERE isbn = '9787508353937'";
// $sql = "SELECT * FROM bookinfo WHERE isbn = '""'";
// $sql = "SELECT * FROM bookinfo WHERE isbn = '".."'";
$sql = "SELECT * FROM bookinfo WHERE isbn = '".$isbn."'";
//修改完成
?>

保证不会出错哈哈(这个多用于数据库的增删改查,避免 sql 语句的错误)

7. 表单数据

表单在提交数据的时候,method 有两种方式:post & get。所以 PHP 有几种不同的方式来获取表单数据:

<?php
$_POST['表单控件名称'] //对应POST方式提交的数据
$_GET['表单控件名称'] //对应GET方式提交的数据
$_REQUEST['表单控件名称'] //同时适用于两种方式
?>

8. 运算符

运算符和其他语言基本一致,如果不了解的可以去看看我的 java 运算符(https://blog.csdn.net/ahanwhite/article/details/89461167)。

但这里还是有一个比较特殊的:

字符串连接赋值:【.=】

<?php
$str = "这是连接";
$str .= "字符串的运算符";
// 那么现在的$str = "这是连接字符串的运算符";
?>

9. 分支与选择

同样和其他语言差别不大,有兴趣可以看我的 java 控制语句(https://blog.csdn.net/ahanwhite/article/details/89461652

10. PHP 函数

PHP 的函数和 Java 还是有点儿区别,定义的格式:

<?php
function 函数名($参数) {
函数体;
}
?>

a. 函数参数可以为空

b. 如果需要修改函数的值,可以使用引用参数传递,但是需要在参数前面加上【&】

c. 函数的参数可以使用默认值,在定义函数是参数写成: $ 参数 =“默认值”; 即可。(默认值又叫缺省值)。

<?php
//改变参数变量的值
function myName(&$name) {
$name = "baibai";
echo $name;
}
$name = "huanhuan";
myName($name);
//设置默认参数值
function myName2($name="baibai") {
echo "<br>".$name;
}
//不传参测试默认值
myName2();
?>


输出结果:

d. PHP 也有一些自己的系统函数(比如 echo),这里再列几个常用的字符串函数:

  • 字符串长度计算
$a = mb_strlen("abdsd");
$b = mb_strlen("lalalal",'UTF-8')

我一般用后面这个,按 utf-8 编码计算长度。

  • 在一个字符串中查找另一个字符串
strstr(字符串1,字符串2)

补充一个函数 var_dump() 【实名感谢石老师】
用来判断一个变量的类型与长度, 并输出变量的数值, 如果变量有值输的是变量的值并回返数据类型. 此函数显示关于一个或多个表达式的结构信息,包括表达式的类型与值。数组将递归展开值,通过缩进显示其结构。

<?php
$a = strstr("asgduiashufai","dui");
$b = strstr("asgduiashufai","?");

echo var_dump($a);
echo "<br>";
echo var_dump($b);
?>

如果存在前面的字符串里存在后面的字符串,那么会返回字符串 2 以及在字符串 1 里后面的所有字符。如果不存在,就会返回 false(但是不能直接输出,直接输出好像是空值,判断一下再输出提示信息会比较好)

  • 按照 ASCII 码比较两个字符串大小
strcmp("字符串1","字符串2")

//1比2打,返回大于0,2比1打,返回小于0,一样大的话返回等于0
  • 将 html 标记作为字符串输出
htmlspecialchars("字符串")
  • 改变字符串大小写
strtolower("字符串");//将字符串全变成小写

strtoupper("字符串");//将字符串全变成大写
  • 加密函数
    md5() 将一个字符串进行 MD5 加密计算。(没有解密的函数,用于密码,检验时将用户提交的密码加密之后进行对比)
$a = md5("字符串");

附录

特殊的变量


收起阅读 »

PHP-Beast 加密你的PHP源代码

PHP
前言首先说说为什么要用PHP-Beast? 有时候我们的代码会放到代理商上, 所以很有可能代码被盗取,或者我们写了一个商业系统而且不希望代码开源,所以这时候就需要加密我们的代码。 另外PHP-Beast是完全免费和开源的, 当其不能完成满足你的需求时, 可...
继续阅读 »

前言

首先说说为什么要用PHP-Beast?
有时候我们的代码会放到代理商上, 所以很有可能代码被盗取,或者我们写了一个商业系统而且不希望代码开源,所以这时候就需要加密我们的代码。
另外PHP-Beast是完全免费和开源的, 当其不能完成满足你的需求时, 可以修改其代码而满足你的要。

编译安装如下

注意:如果你需要使用,首先修改key。可以参考下文

Linux编译安装:
$ wget https://github.com/liexusong/php-beast/archive/master.zip
$ unzip master.zip
$ cd php-beast-master
$ phpize
$ ./configure
$ sudo make && make install

编译好之后修改php.ini配置文件, 加入配置项: extension=beast.so, 重启php-fpm 。

配置项:
 beast.cache_size = size
beast.log_file = "path_to_log"
beast.log_user = "user"
beast.enable = On
beast.log_level支持参数:
 1. DEBUG
2. NOTICE
3. ERROR
支持的模块有:
 1. AES
2. DES
3. Base64
通过测试环境:
Nginx + Fastcgi + (PHP-5.2.x ~ PHP-7.1.x)

怎么加密你的项目

加密方案1:

安装完 php-beast 后可以使用 tools 目录下的 encode_files.php 来加密你的项目。使用 encode_files.php 之前先修改 tools 目录下的 configure.ini 文件,如下:

; source path
src_path = ""
; destination path
dst_path = ""
; expire time
expire = ""
; encrypt type (selection: DES, AES, BASE64)
encrypt_type = "DES"

src_path 是要加密项目的路径,dst_path 是保存加密后项目的路径,expire 是设置项目可使用的时间 (expire 的格式是:YYYY-mm-dd HH:ii:ss)。encrypt_type是加密的方式,选择项有:DES、AES、BASE64。 修改完 configure.ini 文件后就可以使用命令 php encode_files.php 开始加密项目。

加密方案2:

使用beast_encode_file()函数加密文件,函数原型如下:

beast_encode_file(string $input_file, string $output_file, int expire_timestamp, int encrypt_type)
  1. $input_file: 要加密的文件
  2. $output_file: 输出的加密文件路径
  3. $expire_timestamp: 文件过期时间戳
  4. $encrypt_type: 加密使用的算法(支持:BEAST_ENCRYPT_TYPE_DES、BEAST_ENCRYPT_TYPE_AES)

制定自己的php-beast

php-beast 有多个地方可以定制的,以下一一列出:

  1. 使用 header.c 文件可以修改 php-beast 加密后的文件头结构,这样网上的解密软件就不能认识我们的加密文件,就不能进行解密,增加加密的安全性。
  2. php-beast 提供只能在指定的机器上运行的功能。要使用此功能可以在 networkcards.c 文件添加能够运行机器的网卡号,例如:
char *allow_networkcards[] = {
"fa:16:3e:08:88:01",
NULL,
};

这样设置之后,php-beast 扩展就只能在 fa:16:3e:08:88:01 这台机器上运行。另外要注意的是,由于有些机器网卡名可能不一样,所以如果你的网卡名不是 eth0 的话,可以在 php.ini 中添加配置项: beast.networkcard = "xxx" 其中 xxx 就是你的网卡名,也可以配置多张网卡,如:beast.networkcard = "eth0,eth1,eth2"。

  1. 使用 php-beast 时最好不要使用默认的加密key,因为扩展是开源的,如果使用默认加密key的话,很容易被人发现。所以最好编译的时候修改加密的key,aes模块 可以在 aes_algo_handler.c 文件修改,而 des模块 可以在 des_algo_handler.c 文件修改。

函数列表 & Debug

开启debug模式:

可以在configure时加入 --enable-beast-debug 选项来开启debug模式。开启debug模式后需要在php.ini配置文件中加入配置项:beast.debug_path 和 beast.debug_mode。beast.debug_mode 用于指定是否使用debug模式,而 beast.debug_path 用于输出解密后的php脚本源码。这样就可以在 beast.debug_path 目录中看到php-beast解密后的源代码,可以方便知道扩展解密是否正确。

函数列表:
  1. beast_encode_file(): 用于加密一个文件
  2. beast_avail_cache(): 获取可以缓存大小
  3. beast_support_filesize(): 获取beast支持的最大可加密文件大小
  4. beast_file_expire(): 获取一个文件的过期时间
  5. beast_clean_cache(): 清空beast的所有缓存(如果有文件更新, 可以使用此函数清空缓存)

修改默认加密的key

1,修改加密后的文件头结构:打开header.c文件,找到以下代码:

char encrypt_file_header_sign[] = {
0xe8, 0x16, 0xa4, 0x0c,
0xf2, 0xb2, 0x60, 0xee
};

int encrypt_file_header_length = sizeof(encrypt_file_header_sign);
自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0xe8, 0x16, 0xa4, 0x0c,
0xf2, 0xb2, 0x60, 0xee

2,修改aes模块加密key:
打开php-beast-master/aes_algo_handler.c文件,找到以下代码:

static uint8_t key[] = {
0x2b, 0x7e, 0x61, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xi7, 0x10, 0x88, 0x09, 0xcf, 0xef, 0xxc,
};

自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0x2b, 0x7e, 0x61, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xi7, 0x10, 0x88, 0x09, 0xcf, 0xef, 0xxc,

3,修改des模块加密key:
打开php-beast-master/des_algo_handler.c文件,找到以下代码:

static char key[8] = {
0x21, 0x1f, 0xe1, 0x1f,
0xy1, 0x9e, 0x01, 0x0e,
};

自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0x21, 0x1f, 0xe1, 0x1f,
0xy1, 0x9e, 0x01, 0x0e,

4,修改base64模块加密key:
打开php-beast-master/base64_algo_handler.c文件,自定义修改以下代码:

static const short base64_reverse_table[256] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
};

php-beast自定义加密模块

一,首先创建一个.c的文件。例如我们要编写一个使用base64加密的模块,可以创建一个名叫base64_algo_handler.c的文件。然后在文件添加如下代码:
#include "beast_module.h"
int base64_encrypt_handler(char *inbuf, int len, char **outbuf, int *outlen)
{
...
}
int base64_decrypt_handler(char *inbuf, int len, char **outbuf, int *outlen)
{
...
}
void base64_free_handler(void *ptr)
{
...
}
struct beast_ops base64_handler_ops = {
.name = "base64-algo",
.encrypt = base64_encrypt_handler,
.decrypt = base64_decrypt_handler,
.free = base64_free_handler,
};

模块必须实现3个方法,分别是:encrypt、decrypt、free方法。
encrypt方法负责把inbuf字符串加密,然后通过outbuf输出给beast。
decrypt方法负责把加密数据inbuf解密,然后通过outbuf输出给beast。
free方法负责释放encrypt和decrypt方法生成的数据

二,写好我们的加密模块后,需要在global_algo_modules.c添加我们模块的信息。代码如下:
#include <stdlib.h>
#include "beast_module.h"
extern struct beast_ops des_handler_ops;
extern struct beast_ops base64_handler_ops;
struct beast_ops *ops_handler_list[] = {
&des_handler_ops,
&base64_handler_ops, /* 这里是我们的模块信息 */
NULL,
};
三,修改config.m4文件,修改倒数第二行,如下代码:

PHP_NEW_EXTENSION(beast, beast.c des_algo_handler.c beast_mm.c spinlock.c cache.c beast_log.c global_algo_modules.c * base64_algo_handler.c *, $ext_shared)

base64_algo_handler.c的代码是我们添加的,这里加入的是我们模块的文件名。
现在大功告成了,可以编译试下。如果要使用我们刚编写的加密算法来加密php文件,可以修改php.ini文件的配置项,如下:
``
beast.encrypt_handler = "base64-algo"`

名字就是我们模块的name。


转载自:https://cloud.tencent.com/developer/article/1911039

收起阅读 »

Java中的数据类型

Java是强类型语言什么是强类型语言? 就是一个变量只能对应一种类型。而不是模棱两可的类型符号。 下面我通过一个例子来解释一下这个现象.javascript中可以用var表示许多数据类型// 此时a为number var a = 1; // 此时a为字符串...
继续阅读 »

Java是强类型语言

什么是强类型语言?
就是一个变量只能对应一种类型。而不是模棱两可的类型符号。
下面我通过一个例子来解释一下这个现象.

// 此时a为number
var a = 1;
// 此时a为字符串形式的'1'
var a = '1';

可以看到,javascript里面,可以用var来承载各种数据类型,但是在Java,你必须对变量声明具体的数据类型(Java10中也开放了var,目前我们讨论的版本为Java8) 。

8大数据类型

基本类型

存储所需大小

取值范围

int

4字节

-2147483648~2147483647

short

2字节

-32768~32767

long

8字节

-9223372036854775808~9223372036854775807

byte

1字节

-128~127

float

4字节

1.4e-45f~ 3.4028235e+38f

double

8字节

4.9e-324~1.7976931348623157e+308

char

2字节

\u0000~\uFFFF

boolean

根据JVM的编译行为会有不同的结果(1/4)

布尔(boolean)类型的大小没有明确的规定,通常定义为取字面值 “true” 或 “false”

NaN与无穷大

  • NaN

在浮点数值计算中,存在一个NaN来表示该值不是一个数字

/**
* @author jaymin<br>
* 如何表示一个值不是数字
* 2021/3/21 14:54
*/

public class NaNDemo {
public static void main(String[] args) {
Double doubleNaN = new Double(0.0/0.0);
// 一个常数,其值为double类型的非数字(NaN)值
Double nan = Double.NaN;
System.out.println(doubleNaN.isNaN());
System.out.println(nan.isNaN());
}
}
  • 正负无穷大
    private static void isPositiveInfinityAndNegativeInfinity(){
double positiveInfinity = Double.POSITIVE_INFINITY;
double negativeInfinity = Double.NEGATIVE_INFINITY;
System.out.println(positiveInfinity);
System.out.println(negativeInfinity);
}

Result:

Infinity
-Infinity

浮点数存在精度问题

Java中无法用浮点数值来表示分数,因为浮点数值最终采用二进制系统表示。

/**
* @author jaymin<br>
* 浮点数无法表示分数
* @since 2021/3/21 15:07
*/

public class PrecisionDemo {
public static void main(String[] args) {
System.out.println(2.0 - 1.1);
// 如何解决?使用BigDecimal
BigDecimal a = BigDecimal.valueOf(2.0);
BigDecimal b = BigDecimal.valueOf(1.1);
System.out.println(a.subtract(b));
}
}

精度

向上转型和向下强转

  • 向上转型
/**
*
*
* @author jaymin
* @since 2021/3/21 15:40
*/

public class ForcedTransfer {

public static void main(String[] args) {
int n = 123456789;
// 整型向上转换丢失了精度
float f = n;
System.out.println(f);
int n1 = 1;
float f1 = 2.2f;
// 不同类型的数值进行运算,将向上转型
System.out.println(n1 + f1);
}
}

这里我们看到两个现象:

  1. 整型可以赋值给浮点型,但是可能会丢失精度.
  2. 整形和浮点数进行相加,先将整型向上转型为float,再进行float的运算.

层级关系:double>float>long>int

  • 面试官经常问的一个细节

此处能否通过编译?

short s1= 1;
s1 = s1 + 1;

答案是不能的,如果我们对小于 int 的基本数据类型(即 char、byte 或 short)执行任何算术或按位操作,这些值会在执行操作之前类型提升为 int,并且结果值的类型为 int。若想重新使用较小的类型,必须使用强制转换(由于重新分配回一个较小的类型,结果可能会丢失精度).
可以简单理解为: 比int类型数值范围小的数做运算,最终都会提升为int,当然,使用final可以帮助你解决这种问题.

  • 正确示例
short s1= 1;
// 1. 第一个种解决办法
s1 = (short) (s1 + 1);
// 2. 第二种解决办法
s1+=1;
        final short a1 = 1;
final short a2 = 2;
short result = a1 + a2;
  • 向下转型(强制转换)

场景: 在程序中得到了一个浮点数,此时将其转成整形,那么你就可以使用强转.

/**
* 数值之间的强转
*
* @author jaymin
* @since 2021/3/21 15:40
*/

public class ForcedTransfer {

public static void main(String[] args) {
double x = 2021.0321;
// 强转为整型
int integerX = (int) x;
System.out.println(integerX);
x = 2021.8888;
// 四舍五入
int round = (int) Math.round(x);
System.out.println(round);
}
}

Result:

2021  
2022

如果强转的过程中,上层的数据类型范围超出了下层的数据类型范围,那么会进行截断.
可以执行以下程序来验证这个问题.

        long l = Long.MAX_VALUE;
int l1 = (int) l;
System.out.println(l1);
int i = 300;
byte b = (byte) i;
// 128*2 = 256,300-256=44
System.out.println(b);

Reuslt:

-1
44

初始值

基本数据类型都会有默认的初始值.

基本类型

初始值

boolean

false

char

\u0000 (null)

byte

(byte) 0

short

(short) 0

int

0

long

0L

float

0.0f

double

0.0d

在定义对象的时候,如果你使用了基本类型,那么类在初始化后,如果你没有显性地赋值,那么就会为默认值。这在某些场景下是不对的(比如你需要在http中传输id,当对方没有传输id时,你应该报错,但是由于使用了基本的数据类型,id拥有了默认值0,那么此时程序就会发生异常)

定义对象的成员,最好使用包装类型,而不是基础类型.

Integer对象的缓存区

在程序中有些值是需要经常使用的,比如定义枚举时,经常会使用1,2,3作为映射值.Java的语言规范JLS中要求将-128到127的值进行缓存。(高速缓存的大小可以由-XX:AutoBoxCacheMax = <size>选项控制。在VM初始化期间,可以在sun.misc.VM类的私有系统属性中设置并保存java.lang.Integer.IntegerCache.high属性。)

  • 使用==比较Integer可能会出现意想不到的结果
    public static void main(String[] args) {
Integer a1 = Integer.valueOf(127);
Integer a2 = Integer.valueOf(127);
System.out.println(a1==a2);
Integer a3 = Integer.valueOf(128);
Integer a4 = Integer.valueOf(128);
System.out.println(a3==a4);
}

Result:

true
false

解决的办法很简单,使用equals来进行比较即可,Integer内部重写了equals和hashcode.

常用的一些转义字符

在字符串中,如果你想让输出的字符串换行,你就需要用到转义字符

转义字符

Unicode

含义

\b

\u0008

退格

\t

\u0009

制表

\n

\u000a

换行

\r

\u000d

回车

\"

\u0022

双引号

\'

\u0027

单引号

\\

\u005c

反斜杠

\\.

-

.

  • 换行输出字符串
    System.out.println("我马上要换行了\n我是下一行");
收起阅读 »

Java文字转图片防爬虫

最近部分页面数据被爬虫疯狂的使用,主要就是采用动态代理IP爬取数据,主要是不控制频率,这个最恶心。因为对方是采用动态代理的方式,所以没什么特别好的防止方式。具体防止抓取数据方案大全,下篇博客我会做一些讲解。本篇也是防爬虫的一个方案。就是部分核心文字采用图片输出...
继续阅读 »

最近部分页面数据被爬虫疯狂的使用,主要就是采用动态代理IP爬取数据,主要是不控制频率,这个最恶心。因为对方是采用动态代理的方式,所以没什么特别好的防止方式。

具体防止抓取数据方案大全,下篇博客我会做一些讲解。本篇也是防爬虫的一个方案。就是部分核心文字采用图片输出。加大数据抓取方的成本。

图片输出需求

上图红色圈起来的数据为图片输出了备案号,就是要达到这个效果,如果数据抓取方要继续使用,必须做图片解析,成本和难度都加到了。也就是我们达到的效果了。

Java代码实现

import javax.imageio.ImageIO;

import java.awt.*;

import java.awt.font.FontRenderContext;

import java.awt.geom.AffineTransform;

import java.awt.geom.Rectangle2D;

import java.awt.image.BufferedImage;

import java.io.File;

import java.nio.file.Paths;

public class ImageDemo {

public static void main(String[] args) throws Exception {

System.out.println(System.currentTimeMillis());

//输出目录

String rootPath = "/Users/sojson/Downloads/";

//这里文字的size,建议设置大一点,其实就是像素会高一点,然后缩放后,效果会好点,最好是你实际输出的倍数,然后缩放的时候,直接按倍数缩放即可。

Font font = new Font("微软雅黑", Font.PLAIN, 130);

createImage("https://www.sojson.com", font, Paths.get(rootPath, "sojson-image.png").toFile());

}

private static int[] getWidthAndHeight(String text, Font font) {

Rectangle2D r = font.getStringBounds(text, new FontRenderContext(

AffineTransform.getScaleInstance(1, 1), false, false));

int unitHeight = (int) Math.floor(r.getHeight());//

// 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度

int width = (int) Math.round(r.getWidth()) + 1;

// 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度

int height = unitHeight + 3;

return new int[]{width, height};

}

// 根据str,font的样式以及输出文件目录

public static void createImage(String text, Font font, File outFile)

throws Exception {

// 获取font的样式应用在输出内容上整个的宽高

int[] arr = getWidthAndHeight(text, font);

int width = arr[0];

int height = arr[1];

// 创建图片

BufferedImage image = new BufferedImage(width, height,

BufferedImage.TYPE_INT_BGR);//创建图片画布

//透明背景 the begin

Graphics2D g = image.createGraphics();

image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);

g=image.createGraphics();

//透明背景 the end

/**

如果你需要白色背景或者其他颜色背景可以直接这么设置,其实就是满屏输出的颜色

我这里上面设置了透明颜色,这里就不用了

*/

//g.setColor(Color.WHITE);

//画出矩形区域,以便于在矩形区域内写入文字

g.fillRect(0, 0, width, height);

/**

* 文字颜色,这里支持RGB。new Color("red", "green", "blue", "alpha");

* alpha 我没用好,有用好的同学可以在下面留言,我开始想用这个直接输出透明背景色,

* 然后输出文字,达到透明背景效果,最后选择了,createCompatibleImage Transparency.TRANSLUCENT来创建。

* android 用户有直接的背景色设置,Color.TRANSPARENT 可以看下源码参数。对alpha的设置

*/

g.setColor(Color.gray);

// 设置画笔字体

g.setFont(font);

// 画出一行字符串

g.drawString(text, 0, font.getSize());

// 画出第二行字符串,注意y轴坐标需要变动

g.drawString(text, 0, 2 * font.getSize());

//执行处理

g.dispose();

// 输出png图片,formatName 对应图片的格式

ImageIO.write(image, "png", outFile);

}

}

输出图片效果:


当然我这里是做了放缩,要不然效果没那么好。

注意点:

其实代码里注释说的已经比较清楚了。主要设置透明色这里。

//透明背景 the begin
Graphics2D g = image.createGraphics();
image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
g=image.createGraphics();
//透明背景 the end

Android 参考的颜色值

android.graphics.Color 包含颜色值
Color.BLACK 黑色
Color.BLUE 蓝色
Color.CYAN 青绿色
Color.DKGRAY 灰黑色
Color.GRAY 灰色
Color.GREEN 绿色
Color.LTGRAY 浅灰色
Color.MAGENTA 红紫色
Color.RED 红色
Color.TRANSPARENT 透明
Color.WHITE 白色
Color.YELLOW 黄色




收起阅读 »

一日正则一日神,一直正则一直神

本篇带来 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的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。

 








收起阅读 »

关于 Axios 的再再再封装,总是会有所不一样

特性 class 封装 可以多次实例化默认全局可以共用一个实例对象可以实例化多个对象,实例化时可以配置该实例特有的 headers根据各个接口的要求不同,也可以针对该接口进行配置设置请求拦截和响应拦截,这个都是标配了拦截处理系统响应状态码对应的提示语 拦截器 ...
继续阅读 »


特性


  • class 封装 可以多次实例化
  • 默认全局可以共用一个实例对象
  • 可以实例化多个对象,实例化时可以配置该实例特有的 headers
  • 根据各个接口的要求不同,也可以针对该接口进行配置
  • 设置请求拦截和响应拦截,这个都是标配了
  • 拦截处理系统响应状态码对应的提示语

拦截器


首先为防止多次执行响应拦截,这里我们将拦截器设置在类外部,如下:

import axios from "axios";

// 添加请求拦截器
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么 添加 token 等鉴权功能
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use((res) => {
const {status} = res;
// 对错误状态提示进行处理
let message = '';
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
message = showResState(status)
if (typeof res.data === 'string') {
res.data = {code: status, message: message}
} else {
res.data.code = status
res.data.message = message
}
}
return res.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

function showResState(state) {
let message = '';
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}



封装主体


这里为了方便起见,实例化对象处理的其实就是传入的配置文件,而封装的方法还是按照 axios 原生的方法处理的。为了方便做校验在接口上都统一增加了客户端发起请求的时间,以方便服务端做校验。配置参数可参照文档 axios 配置文档

// 构造函数
constructor(config) {
// 公共的 header
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
'Authorization': null
}

let defaultConfig = {
headers: defaultHeaders
}

// 合并配置文件
if (config) {
for (let i in config) {
if (i === 'headers' && config.headers) {
for (let i in config.headers) {
defaultHeaders[i] = config.headers[i];
}
defaultConfig.headers = defaultHeaders;
} else {
defaultConfig[i] = config[i];
}
}
}
// 全局使用
this.init = axios;
this.config = defaultConfig;
}



get 方法的配置

// Get 请求
get(url, params = {}, headers = {}) {

params.time = Date.now();

// 合并 headers
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.get(url[, config])
this.init.get(url, {
...this.config,
...{params: params}
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



post 请求

// POST 请求
post(url, params = {}, headers = {}) {

url = url + '?time=' + Date.now();

if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.post(url[, data[, config]])
this.init.post(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



PUT 请求

// PUT 请求
put(url, params = {}, headers = {}) {

url = url + '?time=' + Date.now();

if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.put(url[, data[, config]])
this.init.put(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



Delete 请求

// Delete 请求
delete(url, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.delete(url[, config])
this.init.delete(url, {
...this.config,
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



>>使用


完整的代码的代码在文末会贴出来,这里简单说下如何使用

// @/api/index.js
import Http,{Axios} from '@/api/http'; // Axios 数据请求方法

// ① 可以使用文件中实例化的公共对象 Axios


// ②也可以单独实例化使用
const XHttp = new Http({
headers: {
'x-token': 'xxx'
}
});


export const getArticles = (params={}) => {
return XHttp.get('https://api.ycsnews.com/api/v1/blog/getArticles', params);
}

export const getArticle = (params={}) => {
return Axios.get('https://api.ycsnews.com/api/v1/blog/getArticles', params);
}



在页面中使用

// @/views/home.vue
import { getArticles,getArticle } from '@/api/index.js'

// 两个方法名差一个字母 's'
getArticle({id:1234444}).then((res) => {
console.log(res)
})
.catch(err => {
console.log(err)
})

getArticles({id:1234444}).then((res) => {
console.log(res)
})
.catch(err => {
console.log(err)
})



完整代码

// @/api/http.js
/**
* 说明:
* 1.多实例化,可以根据不同的配置进行实例化,满足不同场景的需求
* 2.多实例化情况下,可共用公共配置
* 3.请求拦截,响应拦截 对http错误提示进行二次处理
* 4.接口可单独配置 header 满足单一接口的特殊需求
* body 直传字符串参数,需要设置 headers: {"Content-Type": "text/plain"}, 传参:System.licenseImport('"'+this.code+'"');
* import Http,{Axios} from '../http'; // Http 类 和 Axios 数据请求方法 如无特殊需求 就使用实例化的 Axios 方法进行配置 有特殊需求再进行单独实例化
*
*
*/
import axios from "axios";

// 添加请求拦截器
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么 添加 token 等鉴权功能
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use((res) => {
const {status} = res;
// 对错误状态提示进行处理
let message = '';
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
message = showResState(status)
if (typeof res.data === 'string') {
res.data = {code: status, message: message}
} else {
res.data.code = status
res.data.message = message
}
}
return res.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

function showResState(state) {
let message = '';
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}

class Http {
constructor(config) {
// 公共的 header
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
'Authorization': null
}

let defaultConfig = {
headers: defaultHeaders
}

// 合并配置文件
if (config) {
for (let i in config) {
if (i === 'headers' && config.headers) {
for (let i in config.headers) {
defaultHeaders[i] = config.headers[i];
}
defaultConfig.headers = defaultHeaders;
} else {
defaultConfig[i] = config[i];
}
}
}
this.init = axios;
this.config = defaultConfig;
}

// Get 请求
get(url, params = {}, headers = {}) {
// 合并 headers
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.get(url[, config])
this.init.get(url, {
...this.config,
...{params: params}
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}

// POST 请求
post(url, params = {}, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.post(url[, data[, config]])
this.init.post(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}

// PUT 请求
put(url, params = {}, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.put(url[, data[, config]])
this.init.put(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}


// Delete 请求
delete(url, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.delete(url[, config])
this.init.delete(url, {
...this.config,
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}
}

export default Http;

// 无特殊需求的只需使用这个一个对象即可 公共 header 可在此配置, 如需多个实例 可按照此方式创建多个进行导出
export const Axios = new Http({
baseURL:'https://docs.ycsnews.com',
headers: {
'x-http-token': 'xxx'
}
});











收起阅读 »

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时释放它

收起阅读 »

大家好啊,新手一枚,请多关照哈

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

使用环信提供的uni-app Demo,快速实现一对一单聊

如何利用环信提供的uni-app Demo,快速实现一对一单聊?真真保姆级别教程! 写在前面: 1)因为初期直接下载环信的uni-app的demo源码直接看可能一头雾水,因此写下这篇文档帮助项目周期较急,想要快速集成环信uni-app端IM开发者小伙伴。2)这...
继续阅读 »

如何利用环信提供的uni-app Demo,快速实现一对一单聊?真真保姆级别教程!


写在前面:


1)因为初期直接下载环信的uni-app的demo源码直接看可能一头雾水,因此写下这篇文档帮助项目周期较急,想要快速集成环信uni-app端IM开发者小伙伴。

2)这篇文档只帮助实现单聊功能,群组功能其实与单聊基本相仿,可以在参考单聊后的流程,自行看看源码实现群聊。

3)尽管已经从原项目中剥离了很多无关核心逻辑的代码,但仍然可能还有一些小伙伴本身项目中用不到的代码,因此化繁去简这一步就不再本文档中展示,请在按照这篇文档,完成核心逻辑后自行进行优化。

然后就不多啰嗦了,下面开搞~


1、 下载环信uni-app demo 源码 源码地址:

https://github.com/easemob/webim-uniapp-demo


2、在编辑器中打开项目,建议进行一次试运行确保demo源码可以正常跑起来,大概率是可以正常跑起来的。
运行没有问题之后,强烈建议先在README.md中了解一下demo中的目录结构,做个初期的了解。 参考实际目录结构如图:



3、由于是作为演示,所以我只是简单的新建一个示例项目,写一个简易的聊天界面界面作为即时通讯功能的入口。
仿咸鱼在线一对一沟通界面入口:


这个就是默认的项目目录(该示例项目为Vue)


4、这一步就正式开始从环信的Uni-App demo中CV代码到自有的项目中:

setp1:先把最核心的SDKcopy进来,复制demo源码的 newSDK 这个文件到项目中(demo中的SDK其实有很多个,建议选择版本号最新的一个即可)。

自己的项目目录如图:


setp2:复制demo中的 utils 文件到项目中。

utils目录结构如图:


其中 WebIMConfig.js 是作为SDK的Config配置使用,WebIM.js 是针对于SDK进行初始化,并挂载一些常用方法,Dispatcher.js broadcast.js Observer.js 是用作发布订阅的使用,因为源码中有所使用,所以这几个文件都是必须引入。


setp3:copy static 静态资源包到自己的项目当中,因为组件的聊天界面里面的emoji是图片所以要用到。

此时的目录结构如图:



setp4:copy uview-ui进来,因为组件中有用到这个包的UI组件,使用过UI组件的朋友应该都知道,除了这个还要引入相应的样式,这个组件的README.md中,说明了要进行什么样的配置,这里就不再一一赘述。




setp5:在示例项目中新建components文件夹,分别copy demo当中的 components 文件夹下的整个 chat 组件,pages 文件夹下的chatroom组件,由于示例项目中的App.vue组件没有自己的其他逻辑,所以我直接将demo中的Appp.vue中的所有代码全部copy到示例项目中。



PS:特别说明demo的App.vue尽管不是每一行代码都是必要的,但是如果要做优化或者copy,确保import的引入部分先全部粘贴上,conn.listen 监听回调也一定要先copy上。确保先跑起来的原则,优化放在之后。


此时的目录结构如图:


以上步骤执行完成之后便可以跑一把试试了,运行起来查看一下是否有什么引入类型或者其他类型的报错在集中解决一下。


下图是运行到小程序的界面:




VM22 WAService.js:2 TypeError: Cannot read property 'forceUpdate' of undefined

这个报错原因是没有在HbuilderX配置微信小程序的AppId。

5、开始登陆环信,执行跳转至chat聊天界面进行单聊消息的发送测试


step1:确保先登陆环信(能到这一步相信也都已经注册了环信的账号创建了应用,或者利用环信官方demo注册了测试id)

我在示例项目中是在index.vue写的入口页面,因此登陆也写在了这个页面,示例代码截图可以看下图:




step2:运行项目看 App.vue中的监听回调--onOpened回调是否触发(这一步很重要,因为所有功能性接口调用都必须保证环信的连接成功)






看到代码中的打印输出之后证明已成功的建立websocket连接,正式可以开始下一步跳转至chat页面。


step3: 给引入的chatroom组件在pages.json中配置对应的路由映射,并在pages/index.vue组件再给"我想要"按钮添加事件执行路由跳转至chatroom组件。

index.vue中的示例代码如图:




chatroom组件不需要执行其他操作,onload直接将路由传递的参数进行了接收:


step:4 跑起来看看吧!

这个时候顺利的话你会跳转至这样的一个页面,有可能出现这样一个报错:



这是因为demo重写了一个setData并放在了main.jsmixin里面,手动加上去即可,代码所在位置如图:



6、重新编译启动,点击进入chat页面测试聊天,就没问题了!




不排除列位遇到一些其他阻力导致没有成功跑起来,如果还遇到有其他问题,可以在评论区友好交流,我看到会帮忙解决的。


源码下载: uni-app-singleChat-demo.zip

收起阅读 »

Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密

今天这篇文章的目的是补全大家对于 MediaQuery 和对应 rebuild 机制的基础认知,相信本篇内容对你优化性能和调试 bug 会很有帮助。 Flutter 里大家应该都离不开 MediaQuery ,比如通过 MediaQuery.of(cont...
继续阅读 »

今天这篇文章的目的是补全大家对于 MediaQuery 和对应 rebuild 机制的基础认知,相信本篇内容对你优化性能和调试 bug 会很有帮助


Flutter 里大家应该都离不开 MediaQuery ,比如通过 MediaQuery.of(context).size 获取屏幕大小 ,或者通过 MediaQuery.of(context).padding.top 获取状态栏高度,那随便使用 MediaQuery.of(context) 会有什么问题吗?


首先我们需要简单解释一下,通过 MediaQuery.of 获取到的 MediaQueryData 里有几个很类似的参数:



  • viewInsets被系统用户界面完全遮挡的部分大小,简单来说就是键盘高度

  • padding简单来说就是状态栏和底部安全区域,但是 bottom 会因为键盘弹出变成 0

  • viewPadding padding 一样,但是 bottom 部分不会发生改变


举个例子,在 iOS 上,如下图所示,在弹出键盘和未弹出键盘的情况下,可以看到 MediaQueryData 里一些参数的变化:



  • viewInsets 在没有弹出键盘时是 0,弹出键盘之后 bottom 变成 336

  • padding 在弹出键盘的前后区别, bottom 从 34 变成了 0

  • viewPadding 在键盘弹出前后数据没有发生变化


image-20220624115935998



可以看到 MediaQueryData 里的数据是会根据键盘状态发生变化,又因为 MediaQuery 是一个 InheritedWidget ,所以我们可以通过 MediaQuery.of(context) 获取到顶层共享的 MediaQueryData



那么问题来了,InheritedWidget 的更新逻辑,是通过登记的 context 来绑定的,也就是 MediaQuery.of(context) 本身就是一个绑定行为,然后 MediaQueryData 又和键盘状态有关系,所以:键盘的弹出可能会导致使用 MediaQuery.of(context) 的地方触发 rebuild,举个例子:


如下代码所示,我们在 MyHomePage 里使用了 MediaQuery.of(context).size 并打印输出,然后跳转到 EditPage 页面,弹出键盘 ,这时候会发生什么情况?



class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("######### MyHomePage ${MediaQuery.of(context).size}");
return Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: new Text(
"Click",
style: TextStyle(fontSize: 50),
),
),
),
);
}
}

class EditPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
new Container(
margin: EdgeInsets.all(10),
child: new Center(
child: new TextField(),
),
),
new Spacer(),
],
),
);
}
}

如下图 log 所示 , 可以看到在键盘弹起来的过程,因为 bottom 发生改变,所以 MediaQueryData 发生了改变,从而导致上一级的 MyHomePage 虽然不可见,但是在键盘弹起的过程里也被不断 build 。


image-20220624121917686



试想一下,如果你在每个页面开始的位置都是用了 MediaQuery.of(context) ,然后打开了 5 个页面,这时候你在第 5 个页面弹出键盘时,也触发了前面 4 个页面 rebuild,自然而然可能就会出现卡顿。



那么如果我不在 MyHomePage 的 build 方法直接使用 MediaQuery.of(context) ,那在 EditPage 里弹出键盘是不是就不会导致上一级的 MyHomePage 触发 build



答案是肯定的,没有了 MediaQuery.of(context).size 之后, MyHomePage 就不会因为 EditPage 里的键盘弹出而导致 rebuild。



所以小技巧一:要慎重在 Scaffold 之外使用 MediaQuery.of(context) ,可能你现在会觉得奇怪什么是 Scaffold 之外,没事后面继续解释。


那到这里有人可能就要说了:我们通过 MediaQuery.of(context) 获取到的 MediaQueryData ,不就是对应在 MaterialApp 里的 MediaQuery 吗?那它发生改变,不应该都会触发下面的 child 都 rebuild 吗?



这其实和页面路由有关系,也就是我们常说的 PageRoute 的实现



如下图所示,因为嵌套结构的原因,事实上弹出键盘确实会导致 MaterialApp 下的 child 都触发 rebuild ,因为设计上 MediaQuery 就是在 Navigator 上面,所以弹出键盘自然也就触发 Navigator 的 rebuild


image-20220624141749056


那正常情况下 Navigator 都触发 rebuild 了,为什么页面不会都被 rebuild 呢


这就和路由对象的基类 ModalRoute 有关系,因为在它的内部会通过一个 _modalScopeCache 参数把 Widget 缓存起来,正如注释所说:



缓存区域不随帧变化,以便得到最小化的构建




举个例子,如下代码所示:



  • 首先定义了一个 TextGlobal ,在 build 方法里输出 "######## TextGlobal"

  • 然后在 MyHomePage 里定义一个全局的 TextGlobal globalText = TextGlobal();

  • 接着在 MyHomePage 里添加 3 个 globalText

  • 最后点击 FloatingActionButton 触发 setState(() {});


class TextGlobal extends StatelessWidget {
const TextGlobal({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
print("######## TextGlobal");
return Container(
child: new Text(
"测试",
style: new TextStyle(fontSize: 40, color: Colors.redAccent),
textAlign: TextAlign.center,
),
);
}
}
class MyHomePage extends StatefulWidget {
final String? title;
MyHomePage({Key? key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
TextGlobal globalText = TextGlobal();
@override
Widget build(BuildContext context) {
print("######## MyHomePage");
return Scaffold(
appBar: AppBar(),
body: new Container(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
globalText,
globalText,
globalText,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {});
},
),
);
}
}

那么有趣的来了,如下图 log 所示,"######## TextGlobal" 除了在一开始构建时有输出之外,剩下 setState(() {}); 的时候都没有在触发,也就是没有 rebuild ,这其实就是上面 ModalRoute 的类似行为:弹出键盘导致了 MediaQuery 触发 Navigator 执行 rebuild,但是 rebuild 到了 ModalRoute 就不往下影响



其实这个行为也体现在了 Scaffold 里,如果你去看 Scaffold 的源码,你就会发现 Scaffold 里大量使用了 MediaQuery.of(context)


比如上面的代码,如果你给 MyHomePageScaffold 配置一个 3333 的 ValueKey ,那么在 EditPage 弹出键盘时,其实 MyHomePageScaffold 是会触发 rebuild ,但是因为其使用的是 widget.body ,所以并不会导致 body 内对象重构。




如果是 MyHomePage 如果 rebuild ,就会对 build 方法里所有的配置的 new 对象进行 rebuild;但是如果只是 MyHomePage 里的 Scaffold 内部触发了 rebuild ,是不会导致 MyHomePage 里的 body 参数对应的 child 执行 rebuild 。



是不是太抽象?举个简单的例子,如下代码所示:



  • 我们定义了一个 LikeScaffold 控件,在控件内通过 widget.body 传递对象

  • LikeScaffold 内部我们使用了 MediaQuery.of(context).viewInsets.bottom ,模仿 Scaffold 里使用 MediaQuery

  • MyHomePage 里使用 LikeScaffold ,并给 LikeScaffold 的 body 配置一个 Builder ,输出 "############ HomePage Builder Text " 用于观察

  • 跳到 EditPage 页面打开键盘


class LikeScaffold extends StatefulWidget {
final Widget body;

const LikeScaffold({Key? key, required this.body}) : super(key: key);

@override
State<LikeScaffold> createState() => _LikeScaffoldState();
}

class _LikeScaffoldState extends State<LikeScaffold> {
@override
Widget build(BuildContext context) {
print("####### LikeScaffold build ${MediaQuery.of(context).viewInsets.bottom}");
return Material(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [widget.body],
),
);
}
}
····
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
return new LikeScaffold(
body: Builder(
builder: (_) {
print("############ HomePage Builder Text ");
return InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: Text(
"FFFFFFF",
style: TextStyle(fontSize: 50),
),
);
},
),
);
}
}

可以看到,最开始 "####### LikeScaffold build 0.0############ HomePage Builder Text 都正常执行,然后在键盘弹出之后,"####### LikeScaffold build 跟随键盘动画不断输出 bottom 的 大小,但是 "############ HomePage Builder Text ") 没有输出,因为它是 widget.body 实例。



所以通过这个最小例子,可以看到虽然 Scaffold 里大量使用 MediaQuery.of(context) ,但是影响范围是约束在 Scaffold 内部


接着我们继续看修改这个例子,如果在 LikeScaffold 上嵌套多一个 Scaffold ,那输出结果会是怎么样?



class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
///多加了个 Scaffold
return Scaffold(
body: new LikeScaffold(
body: Builder(
·····
),
),
);
}

答案是 LikeScaffold 内的 "####### LikeScaffold build 也不会因为键盘的弹起而输出,也就是: LikeScaffold 虽然使用了 MediaQuery.of(context) ,但是它不再因为键盘的弹起而导致 rebuild


因为此时 LikeScaffoldScaffold 的 child ,所以在 LikeScaffold 内通过 MediaQuery.of(context) 指向的,其实是 Scaffold 内部经过处理的 MediaQueryData


image-20220624150712453



Scaffold 内部有很多类似的处理,例如 body 里会根据是否有 AppbarBottomNavigationBar 来决定是否移除该区域内的 paddingTop 和 paddingBottom 。



所以,看到这里有没有想到什么?为什么时不时通过 MediaQuery.of(context) 获取的 padding ,有的 top 为 0 ,有的不为 0 ,原因就在于你获取的 context 来自哪里


举个例子,如下代码所示, ScaffoldChildPage 作为 Scaffold 的 child ,我们分别在 MyHomePageScaffoldChildPage 里打印 MediaQuery.of(context).padding


class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("MyHomePage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Scaffold(
appBar: AppBar(
title: new Text(""),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
ScaffoldChildPage(),
new Spacer(),
],
),
);
}
}
class ScaffoldChildPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("ScaffoldChildPage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Container();
}
}

如下图所示,可以看到,因为此时 MyHomePageAppbar ,所以 ScaffoldChildPage 里获取到 paddingTop 是 0 ,因为此时 ScaffoldChildPage 获取到的 MediaQueryData 已经被 MyHomePage 里的 Scaffold 改写了。


image-20220624151522429


如果此时你给 MyHomePage 增加了 BottomNavigationBar ,可以看到 ScaffoldChildPage 的 bottom 会从原本的 34 变成 90 。


image-20220624152008795


到这里可以看到 MediaQuery.of 里的 context 对象很重要:



  • 如果页面 MediaQuery.of 用的是 Scaffold 外的 context ,获取到的是顶层的 MediaQueryData ,那么弹出键盘时就会导致页面 rebuild

  • MediaQuery.of 用的是 Scaffold 内的 context ,那么获取到的是 Scaffold 对于区域内的 MediaQueryData ,比如前面介绍过的 body ,同时获取到的 MediaQueryData 也会因为 Scaffold 的配置不同而发生改变


所以,如下动图所示,其实部分人会在 push 对应路由地方,通过嵌套 MediaQuery 来做一些拦截处理,比如设置文本不可缩放,但是其实这样会导致键盘在弹出和收起时,触发各个页面不停 rebuild ,比如在 Page 2 弹出键盘的过程,Page 1 也在不停 rebuild。


1111333


所以,如果需要做一些全局拦截,推荐通过 useInheritedMediaQuery 这种方式来做全局处理。


return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
child: MaterialApp(
useInheritedMediaQuery: true,
),
);

所以最后做个总结,本篇主要理清了:



  • MediaQueryDataviewInsets \ padding \ viewPadding 的区别

  • MediaQuery 和键盘状态的关系

  • MediaQuery.of 使用不同 context 对性能的影响

  • 通过 Scaffold 内的 context 获取到的 MediaQueryData 受到 Scaffold 的影响

作者:恋猫de小郭
链接:https://juejin.cn/post/7114098725600903175
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter 实现背景图片毛玻璃效果

前言 继续我们绘图相关篇章,这次我们来看看如何使用 CustomPaint 实现毛玻璃背景图效果。毛玻璃背景图其实就是将图片进行一定程度的模糊,背景图经过模糊后更加虚幻,使得前景和后景就会有层次感。相比直接加蒙层的效果来说,毛玻璃看起来更加好看一些。下面是背景...
继续阅读 »

前言


继续我们绘图相关篇章,这次我们来看看如何使用 CustomPaint 实现毛玻璃背景图效果。毛玻璃背景图其实就是将图片进行一定程度的模糊,背景图经过模糊后更加虚幻,使得前景和后景就会有层次感。相比直接加蒙层的效果来说,毛玻璃看起来更加好看一些。下面是背景图处理前后的对比,我们的前景图片的透明度并没有改变,但是背景图模糊虚化后,感觉前景更加显眼了一样。
模糊前后对比.jpg
本篇涉及如下内容:



  • 使用 canvas 绘制图片。

  • 绘制图片时如何更改图片的填充范围。

  • 使用 ImageFilter 模糊图片,实现毛玻璃效果。


使用 canvas 绘制图片


Flutter 为 canvas 提供了drawImage 方法用于绘制图片,方法定义如下:


void drawImage(Image image, Offset offset, Paint paint)

其中各个参数说明如下:



  • imagedart:ui 中的 Image 对象,注意不是Widget 中的 Image,因此绘制的时候需要将图片资源转换为 ui.Image 对象。下面是转换的示例代码,fillImage 即最终得到的 ui.Image 对象。注意转换需要一定的时间,因此需要使用异步 async / await 操作。


Future<void> init() async {
final ByteData data = await rootBundle.load('images/island-coder.png');
fillImage = await loadImage(Uint8List.view(data.buffer));
}

Future<ui.Image> loadImage(Uint8List img) async {
final Completer<ui.Image> completer = Completer();
ui.decodeImageFromList(img, (ui.Image img) {
setState(() {
isImageLoaded = true;
});
return completer.complete(img);
});
return completer.future;
}


  • offset:绘制图片的起始位置。

  • paint:绘图画笔对象,在 paint 上可以应用各种处理效果,比如本篇要用到的图片模糊效果。


注意,drawImage 方法无法更改图片绘制的区域大小,默认就是按图片的实际尺寸绘制的,所以如果要想保证全屏的背景图,我们就需要使用另一个绘制图片的方法。


更改绘制图片的绘制范围


Flutter 的 canvas 为绘制图片提供了一个尺寸转换方法,即可以通过指定原绘制区域的矩形和目标区域的矩形,将图片某个区域映射到新的矩形框中绘制。也就是我们甚至可以实现绘制图片的局部区域。该方法名为 drawImageRect,定义如下:


void drawImageRect(Image image, Rect src, Rect dst, Paint paint)

方法的参数比较容易懂,我们来看看 Flutter 的文档说明。



Draws the subset of the given image described by the src argument into the canvas in the axis-aligned rectangle given by the dst argument.
翻译:通过 src 参数将给定图片的局部(subset)绘制到坐标轴对齐的目标矩形区域内。



下面是我们将源矩形框设置为实际图片的尺寸和一半宽高的对比图,可以看到取一半宽高的只绘制了左上角的1/4区域。实际我们可以定位起始位置来截取部分区域绘制。
截取原图的一半宽高.jpg


毛玻璃效果实现


毛玻璃效果实现和我们上两篇使用 paintshader属性有点类似,Paint 类提供了一个imageFilter属性专门用于图片处理,其中dart:ui 中就提供了ui.ImageFilter.blur方法构建模糊效果处理的 ImageFilter对象。方法定义如下:


factory ImageFilter.blur({ 
double sigmaX = 0.0,
double sigmaY = 0.0,
TileMode tileMode = TileMode.clamp
})

这个方法实际调用的是一个高斯模糊处理器,高斯模糊其实就是应用一个方法将像素点周边指定范围的值进行处理,进而实现模糊效果,有兴趣的可以自行百度一下。下面的 sigmaXsigmaY 分布代表横轴方向和纵轴方向的模糊程度,数值越大,模糊程度越厉害。因此我们可以通过这两个参数控制模糊程度。


return _GaussianBlurImageFilter(
sigmaX: sigmaX,
sigmaY: sigmaY,
tileMode: tileMode
);

**注意,这里 sigmaX 和 sigmaY 不能同时为0,否则会报错!**这里应该是如果同时为0会导致除0操作。
下面来看整体的绘制实现代码,如下所示:


class BlurImagePainter extends CustomPainter {
final ui.Image bgImage;
final double blur;

BlurImagePainter({
required this.bgImage,
required this.blur,
});
@override
void paint(Canvas canvas, Size size) {
var paint = Paint();
// 模糊的取值不能为0,为0会抛异常
if (blur > 0) {
paint.imageFilter = ui.ImageFilter.blur(
sigmaX: blur,
sigmaY: blur,
tileMode: TileMode.mirror,
);
}

canvas.drawImageRect(
bgImage,
Rect.fromLTRB(0, 0, bgImage.width.toDouble(), bgImage.height.toDouble()),
Offset.zero & size,
paint,
);
}

代码其实很短,就是在模糊值不为0的时候,应用 imageFilter 进行模糊处理,然后使用 drawImageRect 方法确保图片填充满整个背景。完整代码已经提交至:绘图相关代码,文件名为:blur_image_demo.dart。变换模糊值的效果如下动图所示。
背景图模糊过程.gif


总结


本篇介绍了使用 CustomPaint 实现背景图模糊,毛玻璃的效果。关键点在于 使用 Paint 对象的 imageFilter属性,使用高斯模糊应用到图片上。以后碰到需要模糊背景图的地方就可以直接上手用啦!


作者:岛上码农
链接:https://juejin.cn/post/7113131636195065892
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Native 如何快速集成 Flutter

如何 Android 项目中集成 Flutter 概述 目前flutter越来越受欢迎,但对于一些成熟的产品来说,完全摒弃原有App全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通...
继续阅读 »

如何 Android 项目中集成 Flutter


概述


目前flutter越来越受欢迎,但对于一些成熟的产品来说,完全摒弃原有App全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通过有序推进来提升移动终端的开发效率。 目前,想要在已有的原生App里嵌入一些Flutter页面主要有两种方案。一种是将原生工程作为Flutter工程的子工程,由Flutter进行统一管理,这种模式称为统一管理模式。另一种是将Flutter工程作为原生工程的子模块,维持原有的原生工程管理方式不变,这种模式被称为三端分离模式,如下图所示。
1.png
三端代码分离模式的原理是把Flutter模块作为原生工程的子模块,从而快速地接入Flutter模块,降低原生工程的改造成本。


如何在Native项目中接入flutter 模块


在原生项目中集成flutter模块有两种方式,第一种是直接在项目中新建一个flutter module,第二种将flutter项目模块打包成aar或so包集成到Native项目中。一下将详细介绍这两种方式 (以Android为例)


采用module引用的方式


直接通过Android stuido



File->New ->New Module 选择 Flutter Module 来生成一个Flutter Module.



image.png


image.png



如下图:Android studio为原生项目创建了一个module



image.png


手动创建Flutter module


假设你在 some/path/MyApp 路径下已有一个 Android 应用,并且你希望 Flutter 项目作为同级项目:


 cd some/path/
$ flutter create -t module --org com.example my_flutter

image.png


注意:



  1. 这会创建一个 some/path/my_flutter/ 的 Flutter 模块项目,其中包含一些 Dart 代码来帮助你入门以及一个隐藏的子文件夹 .android/。 .android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。

  2. 为了避免 Dex 合并出现问题,flutter.androidPackage 不应与应用的包名相同


引入 Java 8


Flutter Android 引擎需要使用到 Java 8 中的新特性。


在尝试将 Flutter 模块项目集成到宿主 Android 应用之前,请先确保宿主 Android 应用的 build.gradle 文件的 android { } 块中声明了以下源兼容性,例如:


android {
//...
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}

采用AAR资源包的方式导入Flutter模块


flutter 工程作为独立的项目开发迭代,原生工程不直接使用Flutter项目,而是通过导入flutter 的资源包来引用Flutter 模块。



创建Flutter module 工程。



image.png



编译生成AAR包



image.png



flutter 工程会创建一个本地maven仓库和aar文件,同时在Flutter 项目也会输出指引导入的步骤文本,按照提示步骤操作即可。
为方便使用将该maven仓库拷贝到native 项目中。



image.png



提示步骤如下



Consuming the Module




  1. Open \app\build.gradle




  2. Ensure you have the repositories configured, otherwise add them:


    String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "storage.googleapis.com"
    repositories {
    maven {
    url 'D:<path>\build\host\outputs\repo'
    }
    maven {
    url '$storageUrl/download.flutter.io'
    }
    }




  3. Make the host app depend on the Flutter module:




dependencies {
debugImplementation 'com.example.untitled1:flutter_debug:1.0'
profileImplementation 'com.example.untitled1:flutter_profile:1.0'
releaseImplementation 'com.example.untitled1:flutter_release:1.0'
}


  1. Add the profile build type:


android {
buildTypes {
profile {
initWith debug
}
}
}

To learn more, visit flutter.dev/go/build-aa…
Process finished with exit code 0


在 Android 应用中添加 Flutter 页面


步骤 1:在 AndroidManifest.xml 中添加 FlutterActivity


Flutter 提供了 FlutterActivity,用于在 Android 应用内部展示一个 Flutter 的交互界面。和其他的 Activity 一样,FlutterActivity 必须在项目的 AndroidManifest.xml 文件中注册。将下边的 XML 代码添加到你的 AndroidManifest.xml 文件中的 application 标签内:


<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>

上述代码中的 @style/LaunchTheme 可以替换为想要在你的 FlutterActivity 中使用的其他 Android 主题。主题的选择决定 Android 系统展示框架所使用的颜色,例如 Android 的导航栏,以及 Flutter UI 自身的第一次渲染前 FlutterActivity 的背景色。


步骤 2:加载 FlutterActivity


在你的清单文件中注册了 FlutterActivity 之后,根据需要,你可以在应用中的任意位置添加打开 FlutterActivity 的代码。下边的代码展示了如何在 OnClickListener 的点击事件中打开 FlutterActivity


myButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(currentActivity)
);
}
});

Flutter 启动优化


每一个 FlutterActivity 默认会创建它自己的 FlutterEngine。每一个 FlutterEngine 会有一个明显的预热时间。这意味着加载一个标准的 FlutterActivity 时,在你的 Flutter 交互页面可见之前会有一个短暂的延迟。想要最小化这个延迟时间,你可以在抵达你的 FlutterActivity 之前,初始化一个 FlutterEngine,然后使用这个已经预热好的 FlutterEngine
如果直接启动FlutterActivity则无法避免预热时间,用户会感受到一个较长时间的白屏等待。


优化


提前初始化一个  FlutterEngine,启动的FlutterActivty时直接使用已经初始化的FlutterEngine.



提前初始化



public class MyApplication extends Application {
public FlutterEngine flutterEngine;

@Override
public void onCreate() {
super.onCreate();
// Instantiate a FlutterEngine.
flutterEngine = new FlutterEngine(this);

// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);

// Cache the FlutterEngine to be used by FlutterActivity.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
}
}


使用预热的FlutterEngine



myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(currentActivity)
);
}
});

作者:hujian
链接:https://juejin.cn/post/7113874811675213832
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

黑科技!让Native Crash 与ANR无处发泄!

ANR
前言 高产似母猪的我,又带来了干货记录,本次是对signal的一个总结与回顾。不知道你们开发中,是否会遇到小部分的nativecrash 或者 anr,这部分往往是由第三方库导致的或者当前版本没办法修复的bug导致的,往往这些难啃的crash,对现有的cras...
继续阅读 »

前言


高产似母猪的我,又带来了干货记录,本次是对signal的一个总结与回顾。不知道你们开发中,是否会遇到小部分的nativecrash 或者 anr,这部分往往是由第三方库导致的或者当前版本没办法修复的bug导致的,往往这些难啃的crash,对现有的crash数据指标造成一定影响,同时也对这小部分crash用户不友好,那么我们有没有办法实现一套crash or anr重启机制呢?其实是有的,相信在各个大厂都有一套“安全气囊”装置,比如crash一定次数就启用轻量版本或者自动重新启动等等,下面我们来动手搞一个这样的装置!这也是我第三个s开头的开源库Signal


注意:前方高能!阅读本文最好有一点ndk开发的知识噢!没有也没关系,冲吧!


Native Crash


native crash不同于java/kotlin层的crash,在java环境中,如果程序出现了不可预期的crash(即没有捕获),就会往上抛出给最终的线程uncaghtexceptionhandler,在这里我们可以再次处理,比如屏蔽某个exception即可保持app的稳定,然后native层的crash不一样,native 层的crash大多数是“不可恢复”的,比如某个内存方面的错误,这些往往是不可处理的,需要中断当前进程,所以如果发生了native crash,我们转移到自定义的安全处理,比如自动重启后提示用户等等,就会提高用户很大的体验感(比起闪退)


信号量机制


当native 层发生异常的时候,往往是通过信号的方式发送,给相对应的信号处理器处理


image.png
我们可以从signal.h看到,大概已经定义的信号量有


/**
* #define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
## define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29
#define SIGPOLL SIGIO
#define SIGPWR 30
#define SIGSYS 31
*/

具体的含义可自定百度或者google,相信如果开发者都能在bugly等bug平台上看到


信号量处理函数sigaction


一般的我们有很多种方式定义信号量处理函数,这里介绍sigaction
头文件:#include<signal.h>


定义函数:int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact)


函数说明:sigaction会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。如参数结构sigaction定义如下


struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

信号处理函数可以采用void (*sa_handler)(int)或void (*sa_sigaction)(int, siginfo_t *, void *)。到底采用哪个要看sa_flags中是否设置了SA_SIGINFO位,如果设置了就采用void (*sa_sigaction)(int, siginfo_t *, void *),此时可以向处理函数发送附加信息;默认情况下采用void (*sa_handler)(int),此时只能向处理函数发送信号的数值。


sa_handler:此参数和signal()的参数handler相同,代表新的信号处理函数,其他意义请参考signal();
sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号集搁置;
sa_restorer:此参数没有使用;
sa_flags :用来设置信号处理的其他相关操作,下列的数值可用。sa_flags还可以设置其他标志:
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。参考


即我们可以通过这个函数,注册我们想要的信号处理,如果当SIGABRT信号到来时,我们希望将其引到自我们自定义信号处理,即可采用以下方式


 sigaction(SIGABRT, &sigc, nullptr);

其中sigc为sigaction结构体的变量


struct sigaction sigc;
//sigc.sa_handler = SigFunc;
sigc.sa_sigaction = SigFunc;
sigemptyset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO;

SigFunc为我们定义处理函数的指针,我们可以设定这样一个函数,去处理我们想要拦截的信号


void SigFunc(int sig_num, siginfo *info, void *ptr) {
自定义处理
}

native crash拦截


有了前面这些基础知识,我们就开始封装我们的crash拦截吧,作为库开发者,我们希望把拦截的信号量交给上层去处理,所以我们的层次是这样的


image.png
所以我们可以有以下代码,具体细节可以看Signal
我们给出函数处理器


jobject currentObj;
JNIEnv *currentEnv = nullptr;

void SigFunc(int sig_num, siginfo *info, void *ptr) {
// 这里判空并不代表这个对象就是安全的,因为有可能是脏内存

if (currentEnv == nullptr || currentObj == nullptr) {
return;
}
__android_log_print(ANDROID_LOG_INFO, TAG, "%d catch", sig_num);
__android_log_print(ANDROID_LOG_INFO, TAG, "crash info pid:%d ", info->si_pid);
jclass main = currentEnv->FindClass("com/example/lib_signal/SignalController");
jmethodID id = currentEnv->GetMethodID(main, "callNativeException", "(I)V");
if (!id) {
return;
}
currentEnv->CallVoidMethod(currentObj, id, sig_num);
currentEnv->DeleteGlobalRef(currentObj);


}

当so库被加载的时候由系统自动调用JNI_OnLoad
extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved) {
jint result = -1;
// 直接用vm进行赋值,不然不可靠
if (vm->GetEnv((void **) &currentEnv, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
return JNI_VERSION_1_4;
}

其中currentEnv代表着当前jni环境,我们在JNI_OnLoad阶段进行初始化即可,currentObj即代表我们要调用的方法对象,因为我们要回调到java层,所以native肯定需要一个java对象,具体可以看到Signal里面的处理,值得注意的是,我们在native想要在其他函数使用java对象的话,在初始函数赋值的时候,就必须采用env->NewGlobalRef方式分配一个全局变量,不然在该函数结束的时候,对象的内存就会变成脏变量(注意不是NULL)。


Spi机制的运用


如果还不明白spi机制的话,可以查看我之前写的这篇spi机制,因为我们最终会将信号信息传递给java层,所以最终会在java最后执行我们的重启处理,但是重启前我们可能会使用各种自定义的处理方案,比如弹出toast或者各种自定义操作,那么这种自定义的处理就很合适用spi接口暴露给具体的使用者即可,所以我们Signal定义了一个接口


interface CallOnCatchSignal {
fun onCatchSignal(signal: Int,context: Context)
}

外部库的调用者实现这个接口,将实现类配置在META-INF.services目录即可,如图


image.png
如此一来,我们就可以在自定义的MyHandler实现自己的重启逻辑,比如重启/自定义上报crash等等,demo可以看Signal的处理


ANR


关于anr也是一个很有趣的话题,我们可以看到anr也会导致闪退,主要是国内各个厂商都有自己的自定义化处理,比如常规的弹出anr框或者主动闪退,无论是哪一种,对于用户来说都不是一个好的体验。


ANR传递过程


以android 11为例子,最终anr被检测发生后,会调用ProcessErrorStateRecord类的appNotResponding方法,去进行dump 墓碑文件的操作,这个时候就会调用发送一个信号为Signal_Quit的信号,对应的常量为3,所以如果我们想检测到anr后去进行自定义处理的话,按照上面所说直接用sigaction可以吗?


image.png


然而如果直接用sigaction去注册Signal_Quit信号进行处理的话,会发现居然什么都没有回调!那么这发生了什么!


原因就是我们进程继承Zygote进行的时候就把主线程信号的掩码也继承了,Zygote进程把这三个信号量加入了掩码,该方法被调用在init方法中


image.png
掩码的作用就是使得当前的线程不相应这三个信号量,交给其他线程处理


那么其他线程这里指的是什么?其实就是SignalCatcher线程,通常我们发生anr的时候也能看到log输出,最终在run方法注册处理函数


image.png
最终调用WaitForSignal


image.png
调用wait方法


image.png
这个sigwait方法也是一个注册信号处理函数的方法,跟sigaction的区别可参考


取消block


经过上面的分析,相信能了解到为什么Signal_Quit监听不了了,我们也知道,zygote通过掩码把信号进行了屏蔽,那么我们有办法把这个屏蔽给打开吗?答案是有的


pthread_sigmask(SIG_UNBLOCK, &mask, &old))

sigemptyset(&mask);
sigaddset(&mask, SIGQUIT);

我们可以通过pthread_sigmask设置为非block,即参数1的标志,把要取消屏蔽的信号放入即可,如图就是把SIGQUIT取消了,这样一来我们再使用sigaction去注册SIGQUIT就可以在信号出发时执行我们的anr处理逻辑了。值得注意的是,SIGQUIT触发也不一定由anr发生,这是一个必要但不充分的条件,所以我们还要添加其他的判断,比如我们可以判断一个queue里面的当前message的when参数来判断这个消息在队列待了多久,又或者是我们自定义一个异步消息去查看这个消息什么时候回调了handler等等方法,最终判断是否是anr,当然这个不是百分百准确,目前我也没想到百分百准确的方法,因为FileObserve监听traces文件已经在android5以上不能用了,所以Signal里面没有给出具体的判断,只给了一个参考例子。


最后


上述所讲的都在Signal这个库里面有源码与注释,用起来吧!自定义处理可以用作检测crash,anr,也可以用作一个安全装置,发生crash重启等等,只要有脑洞,都可以实现!最后记得点个赞啦!


作者:Pika
链接:https://juejin.cn/post/7114181318644072479
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

破防了!Web3还没整明白,Web5居然出现了?

自Web3问世并爆火以后,关于“Web3是什么、有何用”的解读文章层出不穷。就在多数人还是云里雾里的当下,神奇的事情发生了。Twitter的创始人Jack Dorsey在日前公布了打造“Web5”的计划,声称“这很可能将是我们对于互联网最重大的贡献”,并戏谑地...
继续阅读 »

自Web3问世并爆火以后,关于“Web3是什么、有何用”的解读文章层出不穷。就在多数人还是云里雾里的当下,神奇的事情发生了。

Twitter的创始人Jack Dorsey在日前公布了打造“Web5”的计划,声称“这很可能将是我们对于互联网最重大的贡献”,并戏谑地为Web3的创投人们“点蜡”,意指Web3必凉,Web5才是未来的“天命之子”。


图片来源:推特截图

那么,Web5到底是何方神圣?

PART 01 Web5横空出世

围绕Web5的定义和核心概念,Jack Dorsey的团队(Block旗下的比特币部门TBD)出具了一份报告。报告不长,包括联系页在内,总共18页PPT。

第一眼看到Web5,可能会有不少人诧异Web4去哪儿了,这个横跳是有什么寓意吗?Dorsey团队给出的答案是:Web5相当于Web2和Web3的集合。


图片来源:Dorsey团队报告截图

也就是说,因为2+3=5,Web5之名由此诞生。乍看之下简单粗暴,细看一下又如何呢?

他们提出,所谓Web5,就是要建立一个去中心化的Web平台(DWP),使开发人员能够利用去中心化的身份标识(DIDs)和去中心化的网络节点(DWNs),来编写去中心化的Web应用程序(DWAs),从而将个人身份和数据的所有权和控制权交还给个人。

从这个概念可以看到,Web5的核心跟 Web3一样,突出“去中心化”特性。其目的是要打破当前互联网世界中用户数据被沉淀在不同的应用中,用户本身无法掌控和自由使用的现状。

从这一点出发,Web5中提及的几个新概念,某种程度上都可以和Web3进行对标。有位名为“ntkris”的网友对此进行了精要的总结。

  • 去中心化身份标识(DID)=公钥

  • 去中心化网络节点(DWN)=智能合约(但在本地运行)

  • 可验证凭证(VCs)=零知识证明

  • 去中心化应用程序(DWAs)=dApps


图片来源:推特截图

不过也有人因此提出质疑,Web5相较Web3来说似乎是“新瓶装旧酒”。但事实上,就目前披露的资料来看,两者还是有两点核心不同。

其一,Web5只有身份标识存储在区块链上,其他所有内容都存储在用户运行的节点上。而Web3中所有用户数据都通过区块链的形式存储在所有的节点中。

其二,Web5的实现最大的依仗是比特币网络,而不是当前在Web3中被广泛使用的以太坊以及其他基于智能合约的区块链。可以说,如果有朝一日Web5真的成功,那么除了比特币外的其他加密货币都将失去意义。

Jack Dorsey及其团队之所以会提出Web5,其实都有迹可循。

首先,Dorsey本人一直都是比特币的狂信徒。他早在2018年就预言未来世界上只有一种通用货币——比特币,比特币会是现有货币体系的终结者。歌手Cardi B曾在Twitter上问Dorsey比特币是否会取代美元,Dorsey坚定地回答“Yes,Bitcon will”。

再者,Dorsey认为,Web3根本不能实现真正的去中心化。去年年底Dorsey就曾公开表示,Web3不是所有人的,它只是贴了个不同标签的集权模式,不要白日做梦。所谓的Web3更像是一个营销热词,实际话语权仍掌握在少数风投和公司手里。如今的Web5可能就是他对于“去中心化”实现路径给出的新答案。

PART 02 Web3的“九宗罪”

虽然Web5的横空出世似乎让人看到了下一代互联网形态的新解,但目前为止它还只是个存在于PPT中的“新蓝图”。相比之下,仍旧是Web3离我们更近,虽然它现在依然“面目模糊”。

在对Web3一探究竟之前,我们先回顾一下Web的三个主要时代:

  • Web1:以静态网页为主的“活化石”

  • Web2:大量的通信和商业行为都集中在少数科技巨头所拥有的封闭平台上。大多数情况下,用户对自己的数据只拥有使用权,而平台却对用户数据和用户创作内容拥有所有权

  • Web3:以用户为主导,基于区块链技术打造的去中心化的互联网生态。用户数据的所有权和控制权均归属用户本人


图片来源于网络

虽然关于Web3的讨论总是毁誉参半,但不可否认的是,从形态上来说,Web3相较Web2来说是一种进化。

Web3创建了一种无需准入的数据存储方式。所有数据都被存储在区块链网络中的公共账本上。不再是某个公司或平台拥有数据,而是由多个节点共同存储数据,并就数据的真实性和有效性达成共识。以此为基础,Web3可以开启无数崭新的用例。

但Web3要面对的现实是,天马行空的想象和天花乱坠的陷阱总是相伴而生,也正因为如此,关于Web3的争议总是不绝于耳。

不久前,分析公司Forrester发布了两份评估Web3的文件,犀利地评价其“包含了一场反乌托邦噩梦的种子”。

分析文章指出,一方面,“Web3”之名正在被滥用。“几乎在一夜之间”,无数“区块链项目”、“NFT倡议”和“元宇宙”相关事务都被神奇地命名为“Web3项目”;另一方面,虽然Web3“承诺了一个更好的在线未来”,但其关键内容却不堪一击。

Forrester定义了Web3的九个关键原则,然后又将其一一推翻。

1、愿景:去中心化

现实:这是不可能实现的,实证就是目前的众多加密货币项目通常都由大型平台或公司主导

2、愿景:相信代码,而不是公司

现实:智能合约及其规则通常由某一个公司开发并执行。那么我们真的能信任这些陌生的开发人员吗

3、愿景:始终使用公开透明的代码

现实:这并不会阻止垄断的形成,这反而会导致依赖于一小部分有能力评估代码的人

4、愿景:加密经济原则的设计使系统普惠所有参与者

现实:只是有利于富人和发展垄断

5、愿景:用户能够拥有和控制他们创建的数据和内容

现实:“所有权”的概念是模糊的。大多数用户不愿意或没有能力对他们的数据做出持续一致的决定

6、愿景:用户自己管理自己的身份和凭证

现实:没有多少人愿意为此费心,部分原因是这很难

7、愿景:用户能控制他们所使用的应用程序和网络

现实:除了少数精通技术的人之外,这种状况极其罕见

8、愿景:去中心化的自治组织和实体作为智能合约的集合而存在

现实:它们没有法律基础,并在一个乌托邦式的假设下工作,即所有的可能性都可以被编码

9、愿景:去中心化金融(DeFi)

现实:虽然是个不错的主意,可惜缺乏对消费者的保护,而风控需要代码检查,这很少有人能做到

自诞生伊始就饱受质疑的Web3真的会有未来吗?如果它真的到来,又会对我们的工作和生活产生何种影响呢?

PART 03 你期待Web3的到来吗

如何打造一个更加开放、共享、安全的数字环境在每一次互联网技术革命中都是核心议题。Web3虽然离我们好像很远,但其思想内核是否值得希冀未来者再次下注呢?毕竟一切都只是刚刚开始。

其一,数字化转型是个不可逆的过程。但随着转型的深化,以及重大创新技术的每一次进步,其目标会不断变化。以Web3为前景,区块链技术的发展已经经历了充分的时间考验,并在一定程度上获得了更加繁盛的土壤。

其二,去中心化或许是IT系统的重要发展趋向。企业运营业务所需的许多重要数据将越来越多地保存在更私密和受保护的地方,存储在区块链和其他类型的分布式账本中。随着时间的推移,越来越多的应用程序将更类似于开源项目,并使所有利益相关者可以公开透明地查看、验证、达成共识。

其三,一些更直接的转变,例如接受某些形式的加密货币作为支付或以NFT的形式发行知识产权。

PART 04 结语

Web3如今还是一个混沌未开的世界,这里有创业者、梦想家、理想主义者,也有骗子、吹牛大王、浑水摸鱼者。这个概念下,关于数字世界的一切本质似乎都在受到质疑和重塑。但不管下一代互联网形态如何,是循规蹈矩地过渡,还是摧枯拉朽地颠覆,我们都应该思考:在一个未知的数字世界里,信任、安全、隐私应该是何种模样,应该如何保障。

参考链接:

https://stackoverflow.blog/2022/05/25/web3-skeptics-and-believers-both-need-a-reality-check/

https://www.pingwest.com/a/265452

https://developer.tbd.website/docs/Decentralized%20Web%20Platform%20-%20Public.pdf

https://www.theregister.com/2022/04/01/forrester_web3_criticism/

https://www.zdnet.com/article/how-decentralization-and-web3-will-impact-the-enterprise/

来源:mp.weixin.qq.com/s/3VjcqRjxta-acEoM1v3Ndw

收起阅读 »

不要滥用effect哦

你或你的同事在使用useEffect时有没有发生过以下场景:当你希望状态a变化后发起请求,于是你使用了useEffect:useEffect(() => { fetch(xxx); }, [a])这段代码运行符合预期,上线后也没问题。随着需求不断迭代...
继续阅读 »

你或你的同事在使用useEffect时有没有发生过以下场景:

当你希望状态a变化后发起请求,于是你使用了useEffect

useEffect(() => {
fetch(xxx);
}, [a])

这段代码运行符合预期,上线后也没问题。

随着需求不断迭代,其他地方也会修改状态a。但是在那个需求中,并不需要状态a改变后发起请求。

你不想动之前的代码,又得修复这个bug,于是你增加了判断条件:

useEffect(() => {
if (xxxx) {
fetch(xxx);
}
}, [a])

某一天,需求又变化了!现在请求还需要b字段。

这很简单,你顺手就将b作为useEffect的依赖加了进去:

useEffect(() => {
if (xxxx) {
fetch(xxx);
}
}, [a, b])

随着时间推移,你逐渐发现:

  • 是否发送请求if条件相关
  • 是否发送请求还与a、b等依赖项相关
  • a、b等依赖项又与很多需求相关

根本分不清到底什么时候会发送请求,真是头大...

如果以上场景似曾相识,那么React新文档里已经明确提供了解决办法。

欢迎加入人类高质量前端框架群,带飞

一些理论知识

新文档中这一节名为Synchronizing with Effects,当前还处于草稿状态。

但是其中提到的一些概念,所有React开发者都应该清楚。

首先,effect这一节隶属于Escape Hatches(逃生舱)这一章。


从命名就能看出,开发者并不一定需要使用effect,这仅仅是特殊情况下的逃生舱。

React中有两个重要的概念:

  • Rendering code(渲染代码)
  • Event handlers(事件处理器)

Rendering code开发者编写的组件渲染逻辑,最终会返回一段JSX

比如,如下组件内部就是Rendering code

function App() {
const [name, update] = useState('KaSong');

return <div>Hello {name}</div>;
}

Rendering code的特点是:他应该是不带副作用的纯函数

如下Rendering code包含副作用(count变化),就是不推荐的写法:

let count = 0;

function App() {
count++;
const [name, update] = useState('KaSong');

return <div>Hello {name}</div>;
}

处理副作用

Event handlers组件内部包含的函数,用于执行用户操作,可以包含副作用

下面这些操作都属于Event handlers

  • 更新input输入框
  • 提交表单
  • 导航到其他页面

如下例子中组件内部的changeName方法就属于Event handlers

function App() {
const [name, update] = useState('KaSong');

const changeName = () => {
update('KaKaSong');
}

return <div onClick={changeName}>Hello {name}</div>;
}

但是,并不是所有副作用都能在Event handlers中解决。

比如,在一个聊天室中,发送消息是用户触发的,应该交给Event handlers处理。

除此之外,聊天室需要随时保持和服务端的长连接,保持长连接的行为属于副作用,但并不是用户行为触发的。

对于这种:在视图渲染后触发的副作用,就属于effect,应该交给useEffect处理。

回到开篇的例子:

当你希望状态a变化后发起请求,首先应该明确,你的需求是:

状态a变化,接下来需要发起请求

还是

某个用户行为需要发起请求,请求依赖状态a作为参数

如果是后者,这是用户行为触发的副作用,那么相关逻辑应该放在Event handlers中。

假设之前的代码逻辑是:

  1. 点击按钮,触发状态a变化
  2. useEffect执行,发送请求

应该修改为:

  1. 点击按钮,在事件回调中获取状态a的值
  2. 在事件回调中发送请求

经过这样修改,状态a变化发送请求之间不再有因果关系,后续对状态a的修改不会再有无意间触发请求的顾虑。

总结

当我们编写组件时,应该尽量将组件编写为纯函数。

对于组件中的副作用,首先应该明确:

用户行为触发的还是视图渲染后主动触发的

对于前者,将逻辑放在Event handlers中处理。

对于后者,使用useEffect处理。

这也是为什么useEffect所在章节在新文档中叫做Escape Hatches —— 大部分情况下,你不会用到useEffect,这只是其他情况都不适应时的逃生舱。

原文:https://segmentfault.com/a/1190000041942007

收起阅读 »

web前端-JavaScript中的函数(创建,参数,返回值,方法,函数作用域,立即执行函数)

文章目录 简介函数的创建1 用构造函数创建2 用函数声明创建3 用函数表达式创建 函数的参数 参数特性1 调用函数时解析器不会检查实参的类型2 调用函数时解析器不会检查实参的数量3 当形参和实参过多,可以用一个对象封装 函数的返回值...
继续阅读 »


文章目录

  • 简介
  • 函数的创建

  • 函数的参数






  • 函数的返回值
  • 立即执行函数
  • 方法
  • 函数作用域
  • 补充:JavaScript中的作用域相关概念
  •  

    简介


    函数(Function


    • 函数也是一个对象
    • 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)。
    • 函数中可以保存一些代码,在需要的时候调用。

    函数的创建




    在JavaScript中有三种方法来创建函数


    1. 构造函数创建
    2. 函数声明创建
    3. 函数表达式创建

    其中第一种方法在实际使用中并不常用。创建函数之后需调用函数才可执行函数体内的代码。
    函数的调用:





    语法:函数名();



    1 用构造函数创建



    语法:var 函数名 = new Function(“语句;”)





    使用new关键字创建一个函数,将要封装的功能(代码)以字符串的形式传递给封装函数,在调用函数时,封装的功能(代码)会按照顺序执行。


    2 用函数声明创建



    语法:function 函数名([形参1,形参2....]){语句...}



    用函数声明显而易见的要简便许多,小括号中的形参视情况而写,语句写在中括号内。与构造函数不同的是不需要以字符串的形式写入。




    3 用函数表达式创建



    语法:var 变量(函数名)=function([形参1,形参2....]){语句...};



    函数表达式和函数声明的方式创建函数的方法相似,不同的是用函数表达式创建函数是将一个匿名函数赋值给一个变量,同时在语句结束后需加分号结尾。













    web前端-JavaScript中的函数(创建,参数,返回值,方法,函数作用域,立即执行函数)







    苏凉.py

    已于 2022-06-16 00:40:01 修改

    596



    收藏

    88


















    🐚作者简介:苏凉(专注于网络爬虫,数据分析,正在学习前端的路上)
    🐳博客主页:苏凉.py的博客
    🌐系列专栏:web前端基础教程
    👑名言警句:海阔凭鱼跃,天高任鸟飞。
    📰要是觉得博主文章写的不错的话,还望大家三连支持一下呀!!!
    👉关注✨点赞👍收藏📂






    简介


    函数(Function


    • 函数也是一个对象
    • 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)。
    • 函数中可以保存一些代码,在需要的时候调用。

    函数的创建


    在JavaScript中有三种方法来创建函数


    1. 构造函数创建
    2. 函数声明创建
    3. 函数表达式创建

    其中第一种方法在实际使用中并不常用。创建函数之后需调用函数才可执行函数体内的代码。
    函数的调用:



    语法:函数名();



    1 用构造函数创建



    语法:var 函数名 = new Function(“语句;”)



    使用new关键字创建一个函数,将要封装的功能(代码)以字符串的形式传递给封装函数,在调用函数时,封装的功能(代码)会按照顺序执行。


    在这里插入图片描述


    2 用函数声明创建



    语法:function 函数名([形参1,形参2....]){语句...}



    用函数声明显而易见的要简便许多,小括号中的形参视情况而写,语句写在中括号内。与构造函数不同的是不需要以字符串的形式写入。


    在这里插入图片描述


    3 用函数表达式创建



    语法:var 变量(函数名)=function([形参1,形参2....]){语句...};



    函数表达式和函数声明的方式创建函数的方法相似,不同的是用函数表达式创建函数是将一个匿名函数赋值给一个变量,同时在语句结束后需加分号结尾。


    在这里插入图片描述


    函数的参数


    • 可以在函数的()中来指定一个或多个形参(形式参数)。
    • 多个形参之间使用,隔开,声明形参就相当于在函数内部声明了对应的变量但是并不赋值。
    • 调用函数时,可以在()中指定实参(实际参数),实参将会赋值给函数中对应的形参。



    参数特性


    1 调用函数时解析器不会检查实参的类型


    函数的实参可以时任意数据类型,在调用函数时传递的实参解析器并不会检查实参的类型,因此需要注意,是否有可能接收到非法的参数,如果有可能则需要对参数进行类型的检查。




    2 调用函数时解析器不会检查实参的数量


    在调用函数传入实参时,解析器不会检查实参的数量,当实参数大于形参数时,多余实参不会被赋值




    当实参数小于形参数时,没有被赋值的形参为undefined。


    3 当形参和实参过多,可以用一个对象封装


    当形参和实参数量过多时,我们很容易将其顺序搞乱或者传递参数时出错,此时我们可以将数据封装在一个对象中,在进行实参传递时,传入该对象即可。




    函数的返回值


    可以使用return来设置函数的返回值



    语法:return 值


     


  • return后的值将会作为函数的执行结果返回
  • 可以定义一个变量,来接收该结果。
  • 在return后的语句都不会执行。

  • 若return后不跟任何值或者不写return,函数的返回值都是undefined。


    另外,在函数体中return返回的是什么,变量接受的就是什么。


    立即执行函数


    • 函数定义完,立即被调用,这种函数叫做立即执行函数
    • 立即执行函数往往只会执行一次
    • 通常为匿名函数的调用。


    语法:(function(形参...){语句...})(实参...);





    方法


    对象的属性值可以时任意的数据类型,当属性值为一个函数时,在对象中调用该函数,就叫做调用该对象的方法。




    函数作用域




  • 调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁
  • 每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的
    在函数作用域中可以访问到全局作用域

  • 的变量,在全局作用域中无法访问到函数作用域的变量


    当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错ReferenceError




    补充:JavaScript中的作用域相关概念







    在全局作用域中有一个全局对象window它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用

  • 作用域指一个变量的作用范围
  • 在JavaScript中有两种作用域1.全局作用域 2.函数作用域
  • 直接编写在script标签中的JS代码,都在全局作用域
  • 全局作用域在页面打开时创建,在页面关闭时销毁

  • 简而言之我们创建的全局变量都作为一个属性保存在window这个对象中。


    而在函数中创建局部变量时,必须使用var关键字创建,否则为全局变量。



    收起阅读 »