控制强制缓存的字段:添加Expires和Cache-Control Header,其中Cache-Control优先级比Expires高
控制协商缓存的字段:Last-Modified / If-Modified-Since和Etag / If-None-Match, 其中Etag / If-None-Match优先级高于Last-Modified / If-Modified-Since,同时存在则只有Etag / If-None-Match生效
Cache-Control
: 控制网页的缓存,位于通用信息头
(1)public
:所有内容都将被缓存(客户端和代理服务器都可缓存)
(2)private
:所有内容只有客户端可以缓存,Cache-Control的默认取值
(3)no-cache
:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
(4)no-store
:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
(5)max-age=xxx
(xxx is numeric):缓存内容将在xxx秒后失效
在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)
浏览器如何缓存?
浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中
总结
强制缓存优先于协商缓存进行,若强制缓存(Expires
和Cache-Control
)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since
和Etag / If-None-Match
),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存,主要过程如下:
简单请求:HEAD GET POST
请求 | GET | POST |
---|---|---|
定义 | 从指定的资源请求数据 | 向指定的资源提交要被处理的数据 |
大小 | 不同的浏览器和服务器不同,一般限制在2~8K之间,更加常见的是1k以内 | —— |
底层协议 | TCP/IP | TCP/IP |
数据包 | 一个TCP包 | 两个TCP包 |
具体请求情况 | 浏览器会把http header和data一并发送出去,服务器响应200(返回数据) | 浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据) |
常见的状态码:
200 OK:表示从客户端发送给服务器的请求被正常处理并返回;
204 No Content:表示客户端发送给客户端的请求得到了成功处理,但在返回的响应报文中不含实体的主体部分(没有资源可以返回);
206 Patial Content:表示客户端进行了范围请求,并且服务器成功执行了这部分的GET请求,响应报文中包含由Content-Range指定范围的实体内容。
301 Moved Permanently:永久性重定向,表示请求的资源被分配了新的URL,之后应使用更改的URL;
302 Found:临时性重定向,表示请求的资源被分配了新的URL,希望本次访问使用新的URL;
301与302的区别:前者是永久移动,后者是临时移动(之后可能还会更改URL)
303 See Other:表示请求的资源被分配了新的URL,应使用GET方法定向获取请求的资源;
302与303的区别:后者明确表示客户端应当采用GET方式获取资源
304 Not Modified:表示客户端发送附带条件(是指采用GET方法的请求报文中包含if-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since中任一首部)的请求时,服务器端允许访问资源,但是请求为满足条件的情况下返回改状态码;
307 Temporary Redirect:临时重定向,与303有着相同的含义,307会遵照浏览器标准不会从POST变成GET;(不同浏览器可能会出现不同的情况);
400 Bad Request:表示请求报文中存在语法错误;
401 Unauthorized:未经许可,需要通过HTTP认证;
403 Forbidden:服务器拒绝该次访问(访问权限出现问题)
404 Not Found:表示服务器上无法找到请求的资源,除此之外,也可以在服务器拒绝请求但不想给拒绝原因时使用;
500 Inter Server Error:表示服务器在执行请求时发生了错误,也有可能是web应用存在的bug或某些临时的错误时;
503 Server Unavailable:表示服务器暂时处于超负载或正在进行停机维护,无法处理请求;
http: 无状态的协议
https:基于SSL加密的http协议
http2.0的改进:
HTTP协议是直接通过明文在浏览器和服务器之间传递信息的,而HTTPS协议则是采用对称加密和非对称加密结合的方式来保护浏览器和服务器之间的通信安全。
HTTPS为了追求性能,又要保证安全,采用了共享密钥加密和公开密钥加密混合的方式进行报文传输。
HTTPS协议通信:HTTPS是HTTP报文直接将报文信息传输给SSL套接字进行加密,SSL加密后将加密后的报文发送给TCP套接字,然后TCP套接字再将加密后的报文发送给目的主机,目的主机将通过TCP套接字获取加密后的报文给SSL套接字,SSL解密后交给对应进程。
https的加密过程
HTTP协议的优缺点
优点: 安全可靠、确保数据的完整性、可以防止中间人攻击
缺点:https握手费时,增加页面加载时延;缓存不如http高效,增加数据开销;ssl证书费用高,且需要绑定ip
https如何防止中间人攻击
CA证书判定,公证人
HTTPS不仅进行加密,还进行身份验证服务器。当客户端连接时,服务器会显示其域具有有效且可信任的证书。这个证书不能简单地被中间人欺骗或重播。
中间人攻击(MITM):服务器向客户端发送公钥。攻击者截获公钥,保留在自己手上。然后攻击者自己生成一个【伪造的】公钥,发给客户端。客户端收到伪造的公钥后,生成加密hash值发给服务器。攻击者获得加密hash值,用自己的私钥解密获得真秘钥。同时生成假的加密hash值,发给服务器。服务器用私钥解密获得假秘钥。
HSTS(Http Strict Transport Security):HTTP 严格传输协议,作用是强制客户端和服务端之间建立安全的HTTPS连接,而非不安全的HTTP连接,可以很大程度上防止SSLTrip
参考文章:
]]>操作系统的概念: 管理硬件、提供用户交互的软件系统
OS统一管理系统资源(处理器资源、IO设备资源、存储器资源、文件资源),实现了对系统资源的抽象
并发性、共享性、虚拟性、异步性
五大功能:进程管理、存储管理、作业管理、文件管理、设备管理
进程实体、五状态模型、进程同步、Linux进程管理
进程——资源分配和调度的基本单位
线程——操作系统进行运行调度的最小单位
创建状态:创建进程时拥有PCB,但是其他资源尚未就绪的状态
终止状态:进程结束由系统清理或者归还PCB的状态 (PCB归还)
操作系统提供fork函数接口创建进程
Key:
临界资源
原则: 空闲让进、忙则等待、有限等待、让权等待
方法:消息队列、共享存储、信号量
前台进程、后台进程、守护进程
后台进程: &符号结束
守护进程:进程名字以“d”结尾的一般都是守护进程
进程🆔
操作Linux进程的相关命令
◆ fg命令将一个后台命令调换至前台终端继续执行
◆ bg命令将一个后台暂停的命令变成继续执行
◆ ctrl+z将前台工作暂停
内存分配与回收、段页式存储管理、虚拟内存、Linux存储管理
◆ 确保计算机有足够的内存处理数据
◆ 确保程序可以从可用内存中获取一部分内存使用
◆ 确保程序可以归还使用后的内存以供其他程序使用
内存分配过程:
内存回收过程:
◆ 不需要新建空闲链表节点
◆ 只需要把空闲区1的容量增大为空闲区即可
◆ 将回收区与空闲区合并
◆ 新的空闲区使用回收区的地址
◆ 字块是相对物理设备的定义 ◆ 页面则是相对逻辑空间的定义
◆ 分页可以有效提高内存利用率(虽然说存在页内碎片)
◆ 分段可以更好满足用户需求
◆ 两者结合,形成段页式存储管理
◆ 先将逻辑空间按段式管理分成若干段
◆ 再把段内空间按页式管理等分成若干页
◆ 将进程逻辑空间等分成若干大小的页面
◆ 相应的把物理内存空间分成与页面大小的物理块
◆ 以页面为单位把进程空间装进物理内存中分散的物理块
◆ 页表记录进程逻辑空间与物理空间的映射
现代计算机系统中,可以支持非常大的逻辑 地址空间(232~264),这样,页表就 变得非常大,要占用非常大的内存空间,如, 具有32位逻辑地址空间的分页系统,规定页 面大小为4KB,则在每个进程页表中的页表 项可达1M(2^20)个,如果每个页表项占用 1Byte,故每个进程仅仅页表就要占用1MB 的内存空间。
◆ 有一段连续的逻辑分布在多个页面中,将大大降低执行效率
◆ 将进程逻辑空间划分成若干段(非等分)
◆ 段的长度由连续逻辑的长度决定
◆ 主函数MAIN、子程序段X、子函数Y等
段式存储和页式存储都离散地管理了进程的逻辑空间
◆ 页是物理单位,段是逻辑单位
◆ 分页是为了合理利用空间,分段是满足用户要求 ◆ 页大小由硬件固定,段长度可动态变化
◆ 页表信息是一维的,段表信息是二维的
◆ 虚拟内存概述
◆ 程序的局部性原理
◆ 虚拟内存的置换算法
◆ 有些进程实际需要的内存很大,超过物理内存的容量
◆ 多道程序设计,使得每个进程可用物理内存更加稀缺
◆ 不可能无限增加物理内存,物理内存总有不够的时候
局部性原理
局部性原理是指CPU访问存储器时,无论是存取指令 还是存取数据,所访问的存储单元都趋于聚集在一个 较小的连续区域中
虚拟内存实际是对物理内存的补充,速度接近于内存,成本接近于辅存
虚拟内存的置换算法
◆ 先进先出算法(FIFO)
◆ 最不经常使用算法(LFU)
◆ 最近最少使用算法(LRU)
高速缓存的替换时机
虚拟内存的置换页面
◆ 替换策略发生在Cache-主存层次、主存-辅存层次
◆ Cache-主存层次的替换策略主要是为了解决速度问题
◆ 主存-辅存层次主要是为了解决容量问题
Linux的存储管理
◆ Buddy内存管理算法 ◆ Linux交换空间
Buddy内存管理策略
◆ Buddy算法是经典的内存管理算法
◆ 算法基于计算机处理二进制的优势具有极高的效率
◆ 算法主要是为了解决内存外碎片的问题
页内碎片: 内部碎片是已经被分配出去(能明确 指出属于哪个进程)的内存空间大于 请求所需的内存空间,不能被利用的 内存空间就是内部碎片
页外碎片:外部碎片是指还没有分配出去(不属 于任何进程),但是由于大小而无法 分配给申请内存空间的新进程的内存 空闲块
努力让内存分配与相邻内存合并能快速进行
Linux交换碎片
◆ 交换空间(Swap)是磁盘的一个分区
◆ Linux物理内存满时,会把一些内存交换至Swap空间
◆ Swap空间是初始化系统时配置的
Linux交换空间
◆ 冷启动内存依赖 ◆ 系统睡眠依赖 ◆ 大进程空间依赖
进程调度、死锁
进程调度是指计算机通过决策决定哪个就绪进程可以获得CPU使用权
◆ 先来先服务调度算法
◆ 短进程优先调度算法 (短进程优先调度算法不利于长作业进程的执行)
◆ 高优先权优先调度算法 (进程附带优先权,调度程序优先选择权重高的进程)
◆ 时间片轮转调度算法(是相对公平的调度算法,但不能保证及时响应用户)
死锁是指两个或两个以上的进程在执行过程 中,由于竞争资源或者由于彼此通信而造成 的一种阻塞的现象,若无外力作用,它们都 将无法推进下去。此时称系统处于死锁状态 或系统产生了死锁,这些永远在互相等待的 进程称为死锁进程。
死锁的产生
◆ 竞争资源(共享资源数量不满足各个进程需求;各个进程之间发生资源进程导致死锁)
◆ 进程调度顺序不当
死锁的四个必要条件
◆ 互斥条件
进程对资源的使用是排他性的使用;某资源只能由一个进程使用,其他进程需要使用只能等待
◆ 请求保持条件
进程至少保持一个资源,又提出新的资源请求;新资源被占用,请求被阻塞;被阻塞的进程不释放自己保持的资源
◆ 不可剥夺条件
进程获得的资源在未完成使用前不能被剥夺;获得的资源只能由进程自身释放
◆ 环路等待条件
发生死锁时,必然存在进程-资源环形链
预防死锁的方法
银行家算法
◆ 客户申请的贷款是有限的,每次申请需声明最大资金量
◆ 银行家在能够满足贷款时,都应该给用户贷款
◆ 客户在使用贷款后,能够及时归还贷款
操作系统的文件管理、Linux的文件系统、Linux文件的基本操作
◆ 逻辑结构的文件类型
◆ 顺序文件
◆ 索引文件
◆ 文件内容由定长记录和可变长记录组成
◆ 定长记录存储文件格式、文件描述等结构化数据项 ◆ 可变长记录存储文件具体内容
无结构文件:
◆ 也称为流式文件
◆ 文件内容长度以字节为单位
顺序文件
◆ 顺序文件是指按顺序存放在存储介质中的文件
◆ 磁带的存储特性使得磁带文件只能存储顺序文件
◆ 顺序文件是所有逻辑文件当中存储效率最高的
索引文件
◆ 可变长文件不适合使用顺序文件格式存储
◆ 索引文件是为了解决可变长文件存储而发明的一种文件格式
◆ 索引文件需要配合索引表完成存储的操作
连续分配
◆ 顺序读取文件内容非常容易,速度很快
◆ 对存储要求高,要求满足容量的连续存储空间
链接分配
◆ 链接分配可以将文件存储在离散的盘块中
◆ 需要额外的存储空间存储文件的盘块链接顺序
索引分配
◆ 把文件的所有盘块集中存储(索引)
◆ 读取某个文件时,将文件索引读取进内存即可
空闲链表
◆ 空闲链表法把所有空闲盘区组成一个空闲链表
◆ 每个链表节点存储空闲盘块和空闲的数目
位示图
◆ 位示图维护成本很低
◆ 位示图可以非常容易找到空闲盘块
◆ 位示图使用0/1比特位,占用空间很小
任何文件或目录都只有唯一路径
文件描述信息
Linux文件类型
◆ FAT(File Allocation Table)
◆ FAT16、FAT32等,微软Dos/Windows使用的文件系统 ◆ 使用一张表保存盘块的信息
◆ NTFS (New Technology File System)
◆ WindowsNT环境的文件系统
◆ NTFS对FAT进行了改进,取代了旧的文件系统
◆ EXT(Extended file system):扩展文件系统
◆ Boot Sector:启动扇区,安装开机管理程序 ◆ Block Group:块组,存储数据的实际位置
◆ Linux的文件系统
◆ EXT2/3/4 数字表示第几代
操作系统的设备管理
IO设备的缓冲区
◆ 减少CPU处理IO请求的频率
◆ 提高CPU与IO设备之间的并行性
CPU与IO设备的速率不匹配
◆ 专用缓冲区只适用于特定的IO进程
◆ 当这样的IO进程比较多时,对内存的消耗也很大
◆ 操作系统划出可供多个进程使用的公共缓冲区,称之为缓冲池
spooling技术
◆ 是关于慢速字符设备如何与计算机主机交换信息的一种技术 ◆ 利用高速共享设备将低速的独享设备模拟为高速的共享设备 ◆ 逻辑上,系统为每一个用户都分配了一台独立的高速独享设备
SPOOLing技术把同步调用低速设备改为异步调用
◆ 在输入、输出之间增加了排队转储环节(输入井、输出井) ◆ SPOOLing负责输入(出)井与低速设备之间的调度
◆ 逻辑上,进程直接与高速设备交互,减少了进程的等待时间
小程序的呈现模式是 WebView + 原生组件,Hybrid 方式
双线程模式是小程序的最大特点
小程序的渲染层和逻辑层分别由 2 个线程管理:渲染层的界面使用了 WebView
进行渲染,逻辑层采用 JsCore
线程运行 JS 脚本。
逻辑层: 创建一个单独的线程去执行 JavaScript,在这个环境下执行的都是有关小程序业务逻辑的代码
渲染层: 界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以渲染层存在多个 WebView 线程
微信小程序的底层原理
JavaScript
、WXML
、WXSS
三种技术进行开发,本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口webview
和 appService
。其中 webview 主要用来展现UI ,appService 有来处理业务逻辑、数据及接口调用。它们在两个进程中运行,通过系统层 JSBridge
实现通信,实现 UI 的渲染、事件的处理在某种程度上,它也限制了开发者的权限:
• 不允许开发者把页面跳转到其他在线网页
• 不允许开发者直接访问DOM
• 不允许开发者随意使用window上的某些未知的可能有危险的API
抹平多端语法差异
实现多端之间投放,码路由
类型 | 小程序 | H5 | App |
---|---|---|---|
运行环境 | App(微信、支付宝、百度),基于浏览器内核完全重构的一个内置解析器 | 浏览器 | 直接运行在操作系统的单独进程中(在 Android 中还可以开启多进程) |
开发成本 | 开发者工具,规范的开发标准 | 涉及开发工具(vscode、Atom等)、前端框架(Angular、react等)、模块管理工具(Webpack 、Browserify 等)、任务管理工具(Grunt、Gulp等),还有 UI 库选择、接口调用工具(ajax、Fetch Api等)、浏览器兼容性等等 | 开发成本较高 |
系统权限 | 比H5更多的系统权限(网络通信状态、数据缓存能力等),但不能绕过App直接使用系统API | 太多系统权限受限 | 调用系统资源 |
运行流畅 | 只能通过webview渲染 | 通过浏览器内核渲染 | 直接调用 GPU 进行渲染 |
小程序中无法使用浏览器中常用的 window 对象和 document 对象,H5 可以随意使用
小程序的开发成本低,不用考虑浏览器的兼容性
小程序并非替代APP,只是想降低APP的用户流失率
小程序是类Vue的开发,但是还是和Vue有着很大的区别:
, vue则是:value
)bindtap
(bind+event),或者 catchtap
(catch+event)绑定事件, vue:使用v-on:event
绑定事件,或者使用@event
绑定事件)wx-if
和hidden
,vue则是v-if
和v-show
)React-native的框架原型图
Flutter的框架原型图
RN 🆚 Flutter
框架 | React Native | Flutter |
---|---|---|
语言 | JavaScript | Dart |
渲染机制 | 基于前端框架的考虑,复杂的UI渲染是需要依赖多个view叠加 | 自己实现GDI(Graphics Device Interface,图形设备接口),用了新语言Dart,避免了RN的那种通过桥接器与Javascript通讯导致效率低下的问题,不使用原生控件进行渲染 |
UI框架 | 原生的UI | 自实现,兼容性很高 |
通信方式 | 类插件式 | 桥接方式 |
完善性 | 发展早,相对完善 | 第三方库较少,道阻且长 |
性能 | 效率优于H5,但是对于复杂UI渲染不友好 | 很好,接近原生的native |
]]>Dart是AOT(运行前编译)的编译的,编译成快速、可预测的本地代码
Dart语言可以编译原生的代码,直接跟原生通信
Flutter将UI组件和渲染器从平台移动到应用程序中,这使得它们可以自定义和可扩展。Flutter唯一要求系统提供的是canvas,以便定制的UI组件可以出现在设备的屏幕上,以及访问事件(触摸,定时器等)和服务(位置、相机等)。这是Flutter可以做到跨平台而且高效的关键。另外Flutter学习了RN的UI编程方式,引入了状态机,更新UI时只更新最小改变区域。
h5
➡️ 通过某种方式触发一个url
➡️ native捕获到url,进行分析
➡️ 原生做处理
➡️ native 调用h5的JSBridge对象传递回调
传统的情况下:js可以和native之间通过api注入的方式实现相互通信,但JSBridge的出现还有下面原因:
addJavaScriptInterface
方式有安全漏洞实现一个JSBridge的方法:
全局桥对象
具体和JS侧相关的是前两个部分
// 名称: JSBridge 挂在 window上的一个属性 var JSBridge = window.JSBridge || (window.JSBridge = {}); /** 该对象有如下方法: registerHandler(String, Function) 注册本地 js 方法,注册后 native可通过 JSBridge调用,注册后会将方法注册到本地变量 messageHandles中 sendHandler(String, JSON, Function) h5 调用原生开放的api,调用后实际上还是本地通过 url scheme触发,调用时会将回调 id 存放到本地变量responseCallbacks 中 _handleMessageFromNative h5 调用native之后的回调通知 参数为 {reposeId: 回调id, responseData: 回调数据} */ var JSBridge = { // 注册本地方法供原生调用 registerHandler: function(method, cb) { // 会将cb 放入 messageHandlers里面,待原生调用 }, messageHandles: {}, // h5注册方法集合,供native通知后回调调用 // h5 主动调用native,需生成唯一的callbackId sendHandler: function(mathod, data, succCb, errCb) { // 内部通过iframe src url scheme 向native发送请求 // 并将对应的回调注册进 responseCallbacks // native 处理结束后将结果信息通知到h5 通过 _handleMessageFromNative // h5 拿到返回信息处理 responseCallbacks 里对应的回调 }, responseCallbacks: {}, // 回调集合 // native 通知 h5 _handleMessageFromNative: function(message) { // 解析 message,然后根据通知类型执行 messageHandles 或 responseCallbacks里的回调 } } /** 注意: 1. native 调用_handleMessageFromNative通知h5,参数为 json 字符串 2. native 主动调用h5方法时 {methodName: api名, data, callbackId} methodName: 开放api的名称 data: 原生处理后传递给 h5 参数 需要把回调函数的值 return 出去,供native拿到, 或者再发一个 bridge 回去,方法名是 methodNameSuccess,或者严禁掉,方法名为native生产的callbackId */ 如: bridge.register("hupu.ui.datatabupdate", (name) => { if(name) { // 再发一个bridge通知原生tab更新成功,,,method 可以为native生成的 callbackId bridge.send('hupu.ui.datatabsuccess', {}) } });
方式:通过原生的setHandler
方法调用原生
// sendHandler 执行步骤1. 判断是否有回调函数,如果有,生成一个回调函数id,并将id,和对应的回调添加放入回调函数集合 responseCallbacks 中2. 通过特定的参数转换方法,将传入的数据,方法名一起拼接成一个 url scheme,如下: var param = { method: 'methodName', data: {xx: 'xx'}, success: 'successId', error: 'errorId' } // 变成字符串并编码 var url = scheme://ecape(JSON.stringify(param))3. 使用内部创建好的iframe来触发scheme(location.href = 可能会造成跳转问题) ...创建iframe var iframe = document.createElment('iframe'); iframe.src = url; document.head.appendChild(iframe); setTimeout(() => document.head.removeChild('iframe'), 200)
]]>最常见的轻量级的代表便是小程序,可以在避开下载新应用的前提下,呈现在不同的端上。同样是无需下载,主打“一键直达”式的快应用也在去年便出现在人们视野之中。除了以上两者之外,还有号称下一代web应用模型的PWA,这些都是无需安装就能快速使用的轻量级模型。
模型名称 | 优点 | 缺点 | 运行环境 | 兼容性 | 开发语言 |
---|---|---|---|---|---|
小程序 | 用完即走 | 不易留存、体验感差 | 依赖于APP,比如微信、支付宝 | 可跨平台 | 类Vue |
快应用 | 成本低、体验好、留存高、与操作系统深度集成 | 支持度低 | 依赖于 安卓手机系统,由国内九家手机厂商联合推出 | 显然不支持九家之外的系统 和 iOS 系统 | 类 Vue |
PWA | 可链接、独立于网络、渐进式、可重用、响应性和安全的,支持离线、通知、推送 | 浏览器对技术支持还不够全面,仍然只是个网站 | 依赖于 浏览器,由 Google 提出,现在 微软 、苹果 都在支持 | Android 支持较好,iOS 需要适配,国产浏览器支持较差 | 无限制 |
现阶段小程序的载体类型五花八门,不在局限于微信,支付宝、京东、美团、字节跳动都陆陆续续着手小程序的开发,但面临一个非常严重的问题就是不同端的小程序有着不同的开发规则,小程序的赛道越来越拥挤,开发人员需要适配的小程序平台越来越多,因此实现多端适配也成了小程序开发中的趋势。
👉 Taro 是一个开放式跨端跨框架解决方案
这些小程序开发框架最主要的区别是 DSL
推荐非常不错的文章:📒《小程序跨框架开发的探索与实践》演讲
快应用、快游戏这些都非常贴合原生的应用,使用体验更佳
与操作系统集成的性质使得它能够摆脱类似小程序之类的框架的限制
PWA并不是单指某一项技术,你更可以把它理解成是一种思想和概念,目的就是对标原生app,将Web网站通过一系列的Web技术去优化它,提升其安全性、性能、流畅性、用户体验等各方面指标,最后达到用户就像在用app一样的感觉。
PWA中包含的核心功能及特性如下:
原理
当用户从主屏幕启动时,service work可以立即加载渐进式Web应用程序,完全不受网络环境的影响。service work就像一个客户端代理,它控制缓存以及如何响应资源请求逻辑,通过预缓存关键资源,可以消除对网络的依赖,确保为用户提供即时可靠的体验。
Web应用程序中,可以通过manifest.json控制应用程序的显示方式和启动方式,指定主屏幕图标、启动应用程序时要加载的页面、屏幕方向,甚至可以指定是否显示浏览器Chrome。
主要技术实现
Service Worker + HTTPS +Cache Api + indexedDB
等一系列web技术实现离线加载和缓存Background Sync
后台同步技术Push&Notification
实现推送与通知manifest.json
文件配置,使得可以直接添加到手机的桌面上腾讯 Wepy :让小程序支持组件化开发的框架
美团 Mpvue :是一个使用 Vue.js 开发小程序的前端框架
百度 Lavas :是一套基于 Vue 的 PWA 解决方案
考拉 Megalo :基于 Vue 的小程序框架(支持微信小程序、支付宝小程序、百度智能小程序)
Google Workbox 开发 PWA 时 需要使用 ServiceWorker,Workbox 是 ServiceWorker 的一个封装
普通情形下,浏览器中访问应用时登录,需要我们输入用户名和密码,完成登录认证。这时,我们用户的session信息中标记的登录状态是yes(已登录),同时在浏览器中写入用户的唯一标识——Cookie。
之后再次访问该应用时,请求中会配上这个Cookie,服务端便能够通过这个Cookie找到对应的session,通过session来判断该用户是否已经登录。
(若是不做特殊配置,这个Cookie的名字就叫做jsessionid,值在服务端是唯一的)
在sso.a.com中写下cookie,如何让app1.a.com和app2.a.com登录?
cookie不能跨域,cookie的domain属性是sso.a.com,在给app1.a.com和app2.a.com发送请求的过程中是带不上的
解决cookie的问题:
SSO登录之后,可以将Cookie的域设置为顶域,也即a.com,这样所有的子域的系统都可以访问到顶域的Cookie
sso、app1和app2是不同的应用,他们的session存在于自己的应用中,是不共享的
解决session的问题:
把这三个系统实现如下图的Session共享。还有其他共享Session的解决方案,Spring-Session
但严格意义上来说,这不属于真正的单点登录
同域下的可借助于cookie的顶域,但是不同域之间的cookie是不共享的。这时候就需要了解一下单点登录的标准流程——CAS流程
具体流程如下:
至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。
这样,app2系统不需要走登录流程,就已经是登录了。SSO,app和app2在不同的域,它们之间的session不共享也是没问题的
总结一下,SSO的特点:
前两者实现帧动画存在着不足:
- 不能灵活控制动画的暂停⏸️和播放
- 不能对帧动画做更加灵活的扩展
- Gif不能捕捉到动画完成事件
使用JS实现帧动画的原理主要有两个:
<imgae>
标签承载图片,定时改变src
属性( 需发送多个http请求💔)background-image
,定时改变元素的background-position
属性(推荐🙆)任务链: 图像预加载 ➡️ 动画执行 ➡️ 动画结束
任务链有两种类型的任务:同时执行完毕;异步定时执行
getStyle
方法——可以获取到某些样式的属性值currentStyle
针对IE浏览器,getComputedStyle
针对firefox浏览器
function getStyle(obj, attr){ if(obj.currenrStyle){ return obj.currentStyle[attr]; } return getComputedStyle(obj,false)[attr];}
支持传入多个属性值
支持同时运动的动画
支持链式动画(回调)
function startMove(obj, attrs, fn){ clearInterval(obj.timer); obj.timer = setInterval(function(){ let flag = true; // 是否清空计时器的标志 for(let a in attrs){ // 1. 获取当前值 let iTarget = attrs[a]; let curStyle = 0; if(a === 'opacity'){ curStyle = Math.round(parseFloat(getStyle(obj, a))*100); } else{ curStyle = parseInt(getStyle(obj, a)); } // 2. 计算速度 let speed = (iTarget - curStyle) / 8; speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed); // 3. 检测停止(只要有属性未达到目标值,就继续运动) if(curStyle !== iTarget){ flag = false; } if(a === 'opacity'){ obj.style.filter = 'opacity(' + (curStyle+speed) + ')'; obj.style.opacity = (curStyle + speed) / 100 } else { obj.style[a] = curStyle + speed + 'px'; } } if(flag){ clearInterval(obj.timer); if(fn){ fn(); } } }, 30)}
帧动画类库设计在Animation.js
中
使用原生的js实现动画需要挂在window.onload()
上
window.onload = function(){ var oDiv = document.getElementById('move'); var aList = oDiv.getElementsByTagName('a'); for(let i in aList){ aList[i].onmouseover = function(){ var _this = this.getElementsByTagName('i')[0]; startMove(_this, {top: -25, opacity: 0 }, function(){ _this.style.top = 30 + 'px'; startMove(_this, {top: 20, opacity: 100}); }); } } }
使用jquery也可以实现以上,并且非常方便
$(function(){ $('#move a').mouseenter(function(){ $(this).find('i').animate({top:"-25px", opacity: "0"}, 300, function(){ $(this).css({top: "30px"}); $(this).css({top: "20px", opacity: "1"}, 200) }) }) })
更多完整的原生的js实现动画的实例放到了个人仓库上 JS Animation
]]> lottieAnimationView.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { // 判断动画加载结束 if (valueAnimator.getAnimatedFraction() == 1f) { if (dialog.isShowing() && getActivity() != null) dialog.dismiss(); } } });
lottieAnimationView.pauseAnimation();lottieAnimationView.cancelAnimation();lottieAnimationView.playAnimation();
lottieAnimationView.loop(true);
播放动画某个部分
setMinFrame(...)setMaxFrame(...)setMinProgress(...)setMaxProgress(...)setMinAndMaxFrame(...)setMinAndMaxProgress(...)
lottieAnimationView.useHardwareAcceleration(true)
按照GC的引用强度,可以划分为强引用缓存和弱引用缓存
// 在动画文件加载完成后会根据设置的缓存策略缓存动画,方便下次使用lottieAnimationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Strong); //强缓存lottieAnimationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Weak); //弱缓存
根据进度缓存,为下次播放作准备
lottieAnimationView.setProgress(progress); //设置当前进度lottieAnimationView.buildDrawingCache(); //强制缓存绘制数据Bitmap image = lottieAnimationView.getDrawingCache(); //获取当前绘制数据
😊优点:
参考文章:
]]>高阶组件:
higherOrderComponent(WrappedComponent);
@higherOrderComponent
安装前提: node.js, 安装 npm install -g yarn
yarn 淘宝源安装
yarn config set registry https://registry.npm.taobao.org -g
yarn config set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass -g
安装yarn npm install -g yarn
安装成功后,查看版本号 yarn --version
创建文件夹 yarn md yarn
进入yarn文件夹 cd yarn
初始化项目
yarn init
// 同npm init,执行输入信息后,会生成package.json文件
yarn的配置项:
yarn config list // 显示所有配置项
yarn config get
yarn config delete
yarn config set
安装包:
yarn install
//安装package.json里所有包,并将包及它的所有依赖项保存进yarn.lockyarn install --flat
//安装一个包的单一版本yarn install --force
//强制重新下载所有包yarn install --production
//只安装dependencies里的包yarn install --no-lockfile
//不读取或生成yarn.lockyarn install --pure-lockfile
//不生成yarn.lock
添加包(会更新package.json和yarn.lock):
yarn add [package]
// 在当前的项目中添加一个依赖包,会自动更新到package.json和yarn.lock文件中yarn add [package]@[version]
// 安装指定版本,这里指的是主要版本,如果需要精确到小版本,使用-E参数yarn add [package]@[tag]
// 安装某个tag(比如beta,next或者latest)
//不指定依赖类型默认安装到dependencies里,你也可以指定依赖类型:
yarn add --dev/-D
// 加到 devDependenciesyarn add --peer/-P
// 加到 peerDependenciesyarn add --optional/-O
// 加到 optionalDependencies
//默认安装包的主要版本里的最新版本,下面两个命令可以指定版本:
yarn add --exact/-E
// 安装包的精确版本。例如yarn add foo@1.2.3会接受1.9.1版,但是yarn add foo@1.2.3 --exact只会接受1.2.3版yarn add --tilde/-T
// 安装包的次要版本里的最新版。例如yarn add foo@1.2.3 --tilde会接受1.2.9,但不接受1.3.0
发布包
yarn publish
移除一个包yarn remove <packageName>
:移除一个包,会自动更新package.json和yarn.lock
更新一个依赖yarn upgrade
用于更新包到基于规范范围的最新版本
运行脚本yarn run
用来执行在 package.json 中 scripts 属性下定义的脚本
显示某个包的信息yarn info <packageName>
可以用来查看某个模块的最新版本信息
缓存
yarn cache
yarn cache list
# 列出已缓存的每个包yarn cache dir
# 返回 全局缓存位置yarn cache clean
# 清除缓存
作为fb推出的用来取代npm的包管理工具,yarn自然具备了许多优点:
npm | yarn |
---|---|
npm install | yarn |
npm isnatlll react --save | yarn add react |
npm uninstall react --save | yarn remove react |
npm install react --save-dev | yarn add react --dev |
npm update --save | yarn upgrade |
npm 也做出了一些优化和改进:
相信在相互促进之下,两者都会在速度和使用上有着更好地提升,为我们带来更好的使用体验
]]>useEffect(() => { // Async Action}, [dependencies])
其实useEffect
就相当于原生React中的componentDidMount
,用来引入具有副作用的操作,最常见的就是向服务器请求数据。在请求数据的过程中往往需要进行异步操作,如何在useEffect中使用异步函数呢?
在界面加载的时候,我希望异步请求数据,最初按照我个人对useEffect的使用理解,我这样实现的:
const MyComponent: React.FC = props => { const [data, setData] = this.useState(''); useEffect(async () => { const data = await loadContent(); setData(data); }, []); return {data}; }
但是在跳转路由(离开当前界面)的时候会遇到如下错误 ❌
看了下Github上的讨论区🙌🏻TypeError: func.apply is not a function / Uncaught TypeError: destroy is not a function,很多人也遇到了这个问题,有个高赞的评论是这样解析的:‘ you’re returning a value from useEffect that isn’t a function’ ,真实报错原因是——如此使用异步函数导致我们在使用useEffect()
的时候返回的是一个值而非函数。
也就是说我们在使用useEffect(()=>{},[])
时,需要将async或者Promise异步函数单独拎出来写,然后在effect内部的第一个参数中调用这个异步函数。
🤔️🤔️🤔️究竟为什么会这样呢?
回顾下异步函数的定义:
A function that allows to use asynchronous instructions with the await keyword which will block the statement execution as long as the Promise after which the await keyword is doesn’t resolve…
This function will also return a Promise, no matter if you explicitly return something or not. In case you return data, it will be wrapped in the resolving content of the promise that the function will create and return automatically.
大致意思是异步函数允许将异步指令与await关键字一起使用的函数,只要不解决await关键字之后的Promise,它将阻止语句执行…
但是下面这句却表明,无论您是否明确返回某些内容,此函数都会返回一个Promise。万一你返回数据,它将包装在Promise的解决内容中,自动创建并返回。
而反观useEffect hooks官方文档的描述:
Often, effects create resources that need to be cleaned up before the component leaves the screen, such as a subscription or timer ID. To do this, the function passed to useEffect may return a clean-up function. For example, to create a subscription.
通常,effects需要在组件离开屏幕之前清除创建的资源,例如订阅或计时器ID。为此,传递给useEffect的函数应该返回一个清理函数。
原因已经十分清楚了,原本需要返回的是一个cleanup
函数,而使用异步函数会使callback返回Promise
而不是cleanup
函数,也就不难解释为何当我离开界面的时候,才遇到报错的情况。
针对如何使用异步,上面其实也已经说过了,主要解决思路就是避免直接将Promise作为返回,把异步函数独立出来,可以像上面一样在useEffect
外部写一个函数内部调用,也可以直接在useEffect
内部封装一个内部函数,然后调用。
const MyComponent: React.FC = props => { const [data, setData] = this.useState(''); useEffect(() => { async getData () { return await loadContent(); } const data = getData(); setData(data); }, []); return {data}; }
当然亦可以使用IIFE( Immediately Invoked Function Expression),即使用一个匿名函数表达式,适合没有返回数据的异步情况
const MyComponent: React.FC = props => { // IIFE useEffect(() => { (async function updateData(){ await updateContent(); })(); }, []); return ; }
🌟最后附上《一份完整的useEffect使用指南》,感觉蛮细致,值得学习借鉴
]]>小有成长,空间辽阔
这个月算是一个过渡的月份,从学习状态到开发状态的切换,投身到真实的项目开发中,节奏比我想象中的要快,但是自己努力协调的话问题不大。在这个过程中我认为自己是有微小的进步的(此处涉嫌自卖自夸),感觉自己的适应能力还不错,欣然接受新的节奏,前提是遇到了低调高能的前辈们~lucky尽管很多新的问题摆在眼前,但还是要徐徐解开,戒骄戒躁,感觉自己很多地方很薄弱,唯一的优势就是上身空间很大,可塑性强。
懂得平衡,专注细致
keep learning ,无论是手头上的工作还是其他,都不能耽搁学习的进程,在月末忙于适应新状态的时候,忽略了自身的学习规划,总是被各种事情占据,自己的主动性很低。还是多去做平衡吧,即便事情再多,也必须保持清晰且专注~注重高效学习高效开发。
突然忏悔🧎♀️关于自己的拖延,之前的Attention机制的综述blog还没完结
琐碎
简单回忆下使用Router的步骤
npm i vue-router -S
import VueRouter from 'vue-router'
Vue.use(VueRouter)
let router = new VueRouter({toutes: [{path: '/home', component: Home}]})
router: router
<router-view></router-view>
除了基本使用还有动态路由匹配、嵌套路由和编程式导航以及重定向等多种使用详情,可以查看Vue-Router | 官方文档
在深度研究Vue-Router之前,先分析下单页面应用的特点,对单、多页面应用还不太了解的可以移步 《前端:你要懂的单页面和多页面》这篇文章,但原文有点小问题。总结大概就是:
综上所述,可以发现SPA的特点就是单个页面应用程序,加载页面的时候,不全部加载,只更新一个指定容器的内容。因此,SPA的核心之一是——更新视图不重新请求页面
Vue-Router的原理核心是:更新视图不重新请求页面
Vue-Router在实现单页前端路由时,提供了一种方式:Hash模式,History模式,Abstract模式,根据模式参数决定采用哪种方式
Router运行模式:
VueRouter的构造方法(src/index.js
):
import { HashHistory } from './history/hash'import { HTML5History } from './history/html5'import { AbstractHistory } from './history/abstract' // ... more constructor(options: RouterOptions = {}) { // ... more // 默认hash模式 let mode = options.mode || 'hash' // 是否降级处理 this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false // 进行降级处理 if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.mode = mode // 根据不同的mode进行不同的处理 switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } }
可以看出,根据mode确定类型,首先会判断是否支持history
,然后根据 fallback
来确定是否要降级。然后,根据不同的 mode,分别实例化不同的 history 。 (HTML5History
、HashHistory
、AbstractHistory
)
默认的模式,浏览器url中#后面的内容,包含#。
hash是URL中的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会加载相应位置的内容,不会重新加载页面。
原理: Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。
HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState
API 来完成 URL 跳转而无须重新加载页面。在不支持history.pushState
的浏览器,会自动回退到hash模式。
由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history模式,只需要在配置路由规则时,加入
"mode: 'history'"
,充分利用上面的history.pushState
API,不需要重新加载页面
是否回退到hash模式可以通过fallback
配置项来控制,默认值是true
// main.jsconst router = new VueRouter({ mode: 'history', routes: [...]})
有时,history模式下也会出现问题:
baidu.com/#/id=5
请求地址为 baidu.com
, 没有问题baidu.com/id=5
请求地址为 baidu.com/id=5
,如果后端没有对应的路由处理,就会返回404错误解决: 为了应对这种情况,需要后台配置支持——在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是我们app 依赖的页面。
export const routes = [ {path: "/", name: "homeLink", component:Home} {path: "/register", name: "registerLink", component: Register}, {path: "/login", name: "loginLink", component: Login}, {path: "*", redirect: "/"}]
此处就设置任意匹配,如果URL输入错误或者是URL匹配不到任何静态资源,就自动跳到到首页。
History类的定义位于base.js
,其中还定义了一系列方法,hash
、html5
模式分别继承了这些方法,并实现了自己特有的逻辑。从外部调用的时候,会直接调用到 this.history
, 然后,由于初始化对象的不同,而进行不同的操作。
abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。
根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。
当然,也可以明确指定在所有情况下都使用 abstract 模式。
其中,VueRouter对象,就在vue-router的入口文件src/index.js
中
VueRouter 原型上定义了一系列的函数,我们日常经常会使用到。主要有 : go
、 push
、 replace
、 back
、 forward
。
以及一些导航守护 : beforeEach
、beforeResolve
、afterEach
等等
上面html 中使用到的 router-view ,以及经常用到的 router-link 则存在 src/components
目录下。
Vue.js 要求插件应该有一个公开方法 install。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。
在 install 方法里面,便可以做相关的处理:
mixin
方法添加一些组件选项,Vue.prototype
上实现。install的实现逻辑:
1) 防止重复安装
2) 通过全局 mixin
注入一些生命周期的处理
3) 挂载变量到原型上
4) 注册全局组件
install.js 的完整代码:
import View from './components/view';import Link from './components/link';export let _Vue;// 插件安装方法export function install(Vue) { // 防止重复安装 if (install.installed && _Vue === Vue) return; install.installed = true; _Vue = Vue; const isDef = v => v !== undefined; // 注册实例 const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode; if ( isDef(i) && isDef((i = i.data)) && isDef((i = i.registerRouteInstance)) ) { i(vm, callVal); } }; // 混入生命周期的一些处理 Vue.mixin({ beforeCreate() { if (isDef(this.$options.router)) { // 如果 router 已经定义了,则调用 this._routerRoot = this; this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive( this, '_route', this._router.history.current ); } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; } // 注册实例 registerInstance(this, this); }, destroyed() { registerInstance(this); } }); // 挂载变量到原型上 Object.defineProperty(Vue.prototype, '$router', { get() { return this._routerRoot._router; } }); // 挂载变量到原型上 Object.defineProperty(Vue.prototype, '$route', { get() { return this._routerRoot._route; } }); // 注册全局组件 Vue.component('RouterView', View); Vue.component('RouterLink', Link); // 定义合并的策略 const strats = Vue.config.optionMergeStrategies; // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;}
在进行路由跳转的时候,通常都是使用this.$router.push(...)
的形式进行调用
export default class VueRouter { // ... more push(location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.push(location, onComplete, onAbort); }}
在src/index.js
中的VueRouter对象上有一个push方法,但是直接转发到了this.history.push(location, onComplete, onAbort)
,引起前面提到了,根据history初始化对象不同做不同的处理。
hash模式下:(mode === hash
)
export class HashHistory extends History { // ...more // 跳转到 push(location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this; this.transitionTo( location, route => { pushHash(route.fullPath); handleScroll(this.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort ); }}// 切换路由// 会判断是否支持pushState ,支持则使用pushState,否则切换hashfunction pushHash(path) { if (supportsPushState) { pushState(getUrl(path)); } else { window.location.hash = path; }}
history模式下:(mode === history
)
export class HTML5History extends History { // ...more // 增加 hash push(location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this; this.transitionTo( location, route => { pushState(cleanPath(this.base + route.fullPath)); handleScroll(this.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort ); }}
都调用了trasitionTo
,两种模式下push的区别在于:一个调用 pushHash
, 一个调用 pushState
,而其他的 go
、 replace
、getCurrentLocation
都是类似的实现方式。
在进行路由跳转的时候,通常有以下四种方式:
// 字符串router.push('home');// 对象router.push({ path: 'home' });// 命名的路由router.push({ name: 'user', params: { userId: '123' } });// 带查询参数,变成 /register?plan=privaterouter.push({ path: 'register', query: { plan: 'private' } });
但是push的具体对象与routes之间如何进行匹配,就涉及到了路由匹配的问题。
路由匹配的具体步骤主要有:
1) 实例化的时候,创建匹配器 ,并生成路由的映射关系 。匹配器中包含 match
方法
2) push
的时候,调用到 match
方法
3) match
方法里面,从路由的映射关系里面,通过编译好的正则来判定是否匹配,返回最终匹配的路由对象
4) transitionTo
中,拿到匹配的路由对象,进行路由跳转
前端通常可以设计一些有趣的内容来增加爬虫的难度和成本,从而实现反爬虫的目的。
场景: 猫眼电影的票房数据
具体方法:页面使用font-face定义字符集,并且通过unicode去映射展示。除去图像识别,还要同时爬取字符集,才能识别出数字,并且伴随着刷新页面,字符集的url会不断发生变化,定期更新字体文件和映射表可以来增大难度。
介绍: @font-face 是 CSS3 的一个模块,其主要作用是可将自定义字体嵌入到网页中,让网页字体的运用不只是限定在 Web 安全字体中
font-face加载网络字体,也可以创建一套字体,之后自定义一套映射关系表设置0xefab
是映射字符1,0xeba2
是映射字符2,以此类推。网页的源码只会显示编码,不影响正常用户的浏览,因为浏览器会记载css的font渲染好,实时显示在网页中
破解方法: 人工处理下,读出请求html文件对应数字的unicode,自己更新映射表,可以设置自动转换,还可以用在其他地方。
场景: 美团的价格
具体方法: 美团用的background拼凑,数字其实是图片,根据不同的background
偏移,显示出不同的字符。在不同的页面上,图片的字符排序也是有区别的,理论上只需要生成0-9与小数点。利用css混淆视听,用户可以看到网页内容,但是代码上显示是错误的,所见非所得。
场景: 汽车之家的厂商信息
具体方法: 把关键的厂商信息,做到了伪元素的content中。这样就会导致爬网页的时候,不得不解析css,因为需要拿到伪元素的content,这样就变相地增大了爬虫的难度。但是在框架好像行不通,比如vue中如何在css的content中显示价格。
场景: 去哪儿的机票价格
具体方法: 将价格数据按不同的位进行分别处理,比如对于一个4位数字的机票价格,先用四个i标签渲染,再用两个b标签去绝对定位偏移量,覆盖故意展示错误的i标签,最后在视觉上形成正确的价格…
解决方法: 结合css计算价格
场景: 网易云音乐
具体方式: 网易页面的html源码中几乎只有一个iframe,并且src是空白的:about: blank
。接着js开始运行,整个页面的框架异步塞到了iframe里面
不过这个方式带来的难度并不大,只是在异步与iframe处理上绕了个弯(或者有其他原因,不完全是基于反爬虫考虑),无论你是用selenium还是phantom,都有API可以拿到iframe里面的content信息。
场景: 全网代理IP
具体方式: 在代理IP信息的页面,需要对IP进行保护,具体办法是把IP数字和符号分割为dom及诶单,再在中间插入迷惑人的数字,爬虫不注意的情况下,很容易误以为拿到了成功的数值;但一旦发觉,很容易解决。
场景: 微信公众号文章
具体方法: 为了保护原文章,在某些微信公众号的文章里,穿插了各种迷之字符,并且通过样式把这些字符隐藏掉。
这种方式虽然令人震惊…但其实没有太大的识别与过滤难度,甚至可以做得更好,不过也算是一种脑洞吧。
场景: 去哪儿移动版
具体方法: 重新定义字符集,与实际的字符显示不一致,html里明明写的3211,视觉上展示的却是1233。原来他们重新定义了字符集,3与1的顺序刚好调换得来的结果…
在绘制canvas图片的时候,不同机器,不同浏览器绘制的图片特征是相同并且独一无二的,这样的,只要提取最简单的md5值便可以唯一标识和跟踪这个用户
反爬虫与爬虫真的是相爱相杀的死对头了,哈哈哈哈哈哈哈~
]]>安装好node环境之后,依次运行下列代码,创建名为todolist
的React项目,并运行
npm install -g create-react-app
create-react-app todolist
cd todlist
npm start
react创建项目很慢,因此建议使用淘宝镜像替代默认的npm
npm install -g cnpm --registry=https://registry.npm.taobao.org
, 将npm替换成cnpm(淘宝镜像)使得安装速度变快npm config set registry https://registry.npm.taobao.org
, 解决cnpm安装的时候,也会去请求registry.npmjs.ord网站出现的速度变慢,或者报错的问题
验证是否配置成功npm config get registry
或者npm info express
实现项目代码的精简化,删除无关的主项目文件的样式文件和默认图片,只保留index.js和App.js
1)在App.js中
import React from 'react';// 定义一个React组件class App extends React.Component { render() { return ( Hello , Casey Lu! );}}// 定义完之后要导出,这样在其他地方才可以importexport default App;
2)在index.js中,
import React from 'react';import ReactDOM from 'react-dom';import App from './App'ReactDOM.render( , document.getElementById('root'));// 把App组件渲染到root标签中
大写字母开头的内容都是组件,引入react理解语法
render()的意义在于可以让我们的一个组件挂载到页面的某个节点上,
App.js即定义一个名为App的组件定义完之后要导出export default App
,这样其他才可以import导入这个组件
在React直接使用标签进行定义的语法叫做JSX语法,允许我们直接在代码中使用尖括号的标签结构
具体表现为:
<div>
等html标签定义{ 1+2 }
,显示3在React中数据存储在state中,类似于vue的data, 修改数据时,不能直接对state的数据进行push,而是使用setState配合ES6的语法,li中的每一个key都要不一样
constructor
相当于Vue中的created
,是在组件刚被创建的时候,自动执行的constructor(props){ super(props); this.state = { list: [ 'Learn React', 'Write Blog' ], inputValue: '' }}
列表项
绑定添加事件
button上面绑定事件使用onClick,示例:<button onClick={this.addList.bind(this)}>add</button>
(使用bind改变this的指向,否则在addList方法中无法通过this.state访问本组件中的数据)
由于React也是基于MVVM的,所以在事件中并非对DOM进行操作,而是通过改变数据,进而影响组件的视图
addList(){ this.setState({ list: [...this.state.list, 'Dance 《Yes!Ok》'] })}
将事件绑定在li标签上,监听onClick事件,根据index删除对应的item
deleteList(idx){ const list = [...this.state.list]; list.splice(idx,1); this.setState({list});}
同样基于TodoList的项目,将每一个列表项作为一个组件,不是一个li标签
父子组件之间的传值:
content = {item}
,子:{this.props.content}
delete={this.handleDelete}
,子 :this.props.handleDelete(this.props.index)
把函数调用的bind(this)迁移到constructor
里面简化render()里面的代码,绑定事件时直接使用方法即可
constructor(){ this.handleInputChange = this.handleInputChange.bind(this);}
render()
内部的渲染结构很复杂的时候,可以写一个单独的方法进行渲染,render内部就可以直接调用该方法
getTodoItems() { return ( ... )}
在React项目中,增加样式主要有两种方式:
{}
定义,markdown文章编译不允许双重,所以下面示例写了单层,实际style后面的内容在双层花括号内)<button style={background: 'yellowgreen', color: 'white'}></button>
className
和 css
样式文件, 在index.js中import<button className="add-btn"></button>
逻辑写好了之后,根据我的直女审美又加了点样式,中规中矩的todoList哈哈哈哈哈哈
感兴趣的小伙伴可以移步我的 My Todo-List,源码已经push上去了,开发中存在的问题欢迎大家留言交流~
]]>Dom的本质: 浏览器中的概念,用JS对象表示页面上的元素,并提供了操作DOM对象的API
React中的虚拟DOM: 是框架中的概念,是程序员用JS对象来模拟页面上的DOM和DOM嵌套,目的是实现页面中DOM元素的高效更新。
虚拟DOM的原理:
DOM vs 虚拟DOM:
虚拟DOM总损耗 = 虚拟DOM增删改 + (与Diff算法效率有关)真实DOM差异增删改 + (较少节点)排版和重绘
真实DOM总损耗 = 真实DOM完全增删改 + (可能较多的节点)排版与重绘
Diff算法实际上就是一个调和的具体实现,作用是计算出Virtual DOM中真正变化的部分,并且只针对该部分进行原生的DOM操作,而非重新渲染整个页面。
当节点处于同一层级时,Diff提供三种DOM操作:删除、移动、插入。
git push remote rejected{change ### closed}
,仔细看了下问题,主要是因为自己本地的commit中出现了这样的情况:多个commit之间虽然commit-id不同,但是change-id 重复了,而在push的时候会把多个commit都提交上去,导致出错。在网上查了下这个问题,大多数的解决方案是:
重新执行commit --amend
删除掉原来的commit id, 之后查看会自动生成新的commit-id,这样就会解决commit id 重复的问题。
但是实际上我的问题在于change-id 冲突,即便我按照这种方式重置了commit-id,依旧没办法解决问题。
一直在纠结要不要备份之后再reset的时候,师兄给我一个好的建议,就是——把本地的多个commit合并,再重新push
Git合并多个commit 的方式
git log 查看提交日志
举个例子,查看到的git提交历史(由近及远):
$ git logcommit 3bf6es ....commit 7h90ed ....commit 54g8ji ....commit 67udif ....
git rebase 合并多个commit
想要合并1~3条commit有两个办法:
git rebase -i HEAD~3
git rebase -i 67udif
两个方法的效果相同,都是将前三条commit合并为一个
pick 选取合并之后的提交
执行完rebase
命令之后,弹出的内容大概如下
pick 3bf6es 'issueID #145: ****'....pick 7h90ed 'fix: *****'....pick 54g8ji 'feat: ******'....
这种情况下,只能pick一个,其余修改为squash
或者s
,修改之后如下:
pick 3bf6es 'issueID #145: ****'....s 7h90ed 'fix: *****'....s 54g8ji 'feat: ******'....
:wq
保存并退出,Git会主动压缩我们的提交历史,
1) 如果有冲突:修改冲突,保留最新的历史,不然修改被discard了。修改之后记得
git add .git rebase
如果想放弃本次合并,执行git rebase --absort
2) 如果没有冲突或者解决完冲突,则会进入编辑界面
# This is a combination of 4 commits# The first commit's message is:....# The 2nd commit's message is:....# The 3rd commit's message is:.... # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an ....
:wq
保存并退出,输入git log 检查commit的历史就会发现commit已经合并了
将这个合并之后,我的change-ID冲突也就解决了,确保工作区干净之后,可以push了~
]]>CSS动画的特点:
CSS 3 既有帧动画,又有过渡动画
动画的两个属性:transition
和animation
动画常和transform
属性常用,但是它并不是动画属性,它会主动开启浏览器的加速,也即实现GPU加速,让我们的动画更加细腻,不会改变原有的文档流的结构,防止页面的频繁闪现。
动画的兼容:IE9及之前不支持动画
动画的应用:
CSS动画的扩展:
Vue-transition , sniper, Animate.css, svg 等都需要动画基础
属性名称(property)
过渡效果周期和(duration)和延迟时间(delay)
速度效果的速度曲线(timing-function)
使用示范:
.box{ width: 100px; height: 100px; background: #6d6d6d;}.demo-1{ &:hover{ width: 500px; } // linear ease transition: width 2s linear 2s; // ease}.demo-2{ &:hover{ transform: rotate(45deg); // 旋转45度 } transition: transform 1s ease-out; }
Tips:
动画名称(name)— @keyframes
动画完成的周期(duration)延迟时间(delay)
动画的播放速度(timing-function)
使用示例:
.demo-2{ &:hover{ .cell{ animation: move 2s linear; } } .cell{ width: 200px; height: 200px; background: red; }}@keyframes move{ 100%{ transform: translateX(200px); }}
animation相当于升级版的transition,还可以定义下列属性:
播放次数(iteration-count)
播放方向(direction)即是否轮流播放和反向播放
停止播放的状态(fill-mode ) 是否暂停⏸️(play-state)
infinite
表示可以无限循环播放alternate
表示正反向播放,reverse
表示反向播放forwards
表示停留在最后一帧, running
无限播放
实例:animation: move 2s linear 2 alternate forwards
使用场景:
跑马灯,loading
animation解决了transition display:none bug
跳动的元素:
.demo-4{ margin: 40px auto; border-radius: 50%; animation: jump 2s ease-in infinite;}@keyframes jump{ 0%{ transform: translateY(0px); } 40%{ transform: translateY(200px); } 50%{ transform: translateY(200px); } 100%{ transform: translateY(0px); }}
.loading{ width: 108px; height: 108px; background: url(./img/loading.png) no-reapt; border-radius: 50%; animation: round 1s step(12) infinite;}@keyframes round{ 100%{ transform: rorate(360deg); }}.tuizi{ width: 200px; height: 200px; animation: run .5s steps(6) infinite; background: url(./img/tuzi.png) no-repeat;}@keyframes run{ 50%{ background-position-x: -1200px; } 100%{ background-position-x: -2400px; }}
js可以使动画的实现变得更加强大
animationstart
animationend、transitionend
animationiteration
对上面的例子实现监听
let runstart = () => { console.log('run start') }let runend = () => { console.log('run end') }let iteration = () => { console.log('iteration') }let $loading = document.querySelector('.loading')$loading.addEventListener('animationstart', runstart) $loading.addEventListner('animationend', runend)$loading.addEventListner('animationiteration', iteration)
监听事件的兼容性
$loading.addEventListener('webkitAnimationStart', runstart)
body,html{ height: 100%;}.content{ position: realtive; height: 100%; background: #f2f2f2; overflow: hidden; .yudi{ position: absolute; opciaty: 0; animation: drops 1.2s cubic-bezier(.54,.02,.51,.25) infinite; width: 60px; height: 60px; background: url(../img/hongbao.png) no-repeat; background-size: auto 60px; }}@keyframes drops{ 0%{ opacity: 0; } 20%{ opacity: 1; } 90%{ opacity: 1; } 100%{ opacity: 0; transform: translate3d(10px,100vh,-10px); }}
使用jquery代码如下:
let $content = $('.content');let initNumber = 0;for(let i = 0; i < 30; i++){ let lefts = Math.floor(Math.random()*5+2); let delay = Math.floor(Math.random()*50+2); initNumber += lefts; let $div = $('').addClass('yudi').css({ "left": `${initNumber}%`, "top": `${lefts}%`, "animation-delay": `${delay/10}s` }); $content.append($div);}
实现类似<marquee>
的弹幕滚动效果
实现swiper
三个小栗子我都在这个git仓库里: CSS3 Animation
]]>在H5之前,有cookie 和 userdata
1) cookies的三个特点:
因此这些特性决定cookies的应用多是携带身份验证
2)Userdata也可以实现客户端存储,但是只有IE支持,不符合w3c标准并且平台支持不够广泛,最终存放在xml文件中
H5存储的目标在于:
存储形式: key -> value
过期时间:LocalStorage永久存储,永不失效,除非手动删除;SessinStorage只有在当前会话中有效
大小:每个域名5M
API:getItem, setItem,removeItem,key,clear
存储的类型:数组,json数据,图片,脚本,样式文件
使用注意⚠️
使用限制🚫
1)存储更新策略,过期控制
浏览器虽然永不失效,但是我们可以自己去实现封装,实现数据过期的效果,具体的实现示🌰
function setStorage(key, value){ var curTime = new Date().getTime() localStorage.setItem(key, JSON.stringify({data: value,time:curTime}))}function getStorage(key,exp){ var data = localStorage.getItem(key) var dataObj = JSON.parse(data) if(new Date().getTime() - dataObj.time > exp){ console.log(‘expires’) } else{ console.log(‘data =’ + dataObj.data) }}
2)子域名之间不能共享存储数据postMessage
做跨域共享
3)超出存储大小之后如何存储(LRU/FIFO)
及时删除旧数据
4)server端如何获取
在get、post请求的参数中的携带数据,发给server端
navigator.onLine
<html manifest = "sample.appcache">
更新: 如果有修改资源文件,必须通过修改manifest文件来刷新被缓存的文件列表
优点: 1)完全离线 2)资源被缓存,加载更快 3)降低server负载
缺点: 1)含manifest怎样都会被缓存2)更新时需要两次刷新才能获取新资源 3)全局更新 4)链接的参数变化是敏感的,任何参数修改都会重新缓存
应用场景: 1)单地址页面 2)对实时性要求不高的业务 3)离线webapp
一种能在浏览器中持久地存储结构化数据的数据库,并且为web应用提供了丰富的查询能力。
浏览器支持: Chrome 11+, Opera不支持、Firefox 4+、IE 10+
存储结构:按照域名分配独立空间,一个独立域名下面可以创建多个数据库,每个数据库可以创建多个对象存储空间(表),一个对象存储空间可以存储多个对象数据
indexDB的实际操作:增删查改
借助事务、游标、查询索引
transaction.objectStore(tableName) // 获取Store对象
userData
: 只支持IE,5.0-9.0, 大小64k, 逐渐被弃用google Gears
:64SQLite, chrome, 12.0之后放弃支持,用户授权
H5的存储优势:
存储劣势:
时间: 2020.03.19 14:00 - 15:20
内容: 行测 + 性格测试
时间: 2020.03.22 10:10 - 11:24
第一次碰到女面试官,除了开心就是开心啊~ 希望是以后工作的前辈吧!面试时间是周末,但是面试官在家加班还很认真的问了我超级多的问题~
Vue项目中自定义的组件?
如何实现组件在其他项目上的扩展使用?比如像npm的包给别的项目引用
小程序开发中的问题,机制?你主要负责的是什么部分,项目中你觉得有意思的点是什么?
对于Vue的问题,虚拟DOM的渲染,虚拟DOM对于普通DOM的提升在哪里?因为什么才优于普通DOM
对于Promise的原理,以及解析,有什么状态?状态之间可以逆转吗?方法,如何捕获异常,可不可以对Promise内部的错误进行try…catch,(不可以,那为什么?。。。)
ES6,let和var的对比,为啥要变量提升?
场景题: console.log(i)
var i = 1
结果是什么? Undefined,为什么?
只是声明的变量会提升,初始化不会提升
所以你觉得变量的提升是声明的提升还是赋值的提升?(声明的提升)
箭头函数的操作,跟普通函数比有什么区别,this指向哪里?可以修改不?
对于改变this指向,知道哪些函数?有什么区别(bind,call,apply)
Vue的核心机制是什么?有什么特点?
Vue和 React的区别?
响应事件的触发(只说了冒泡、其余不知道)
Position有哪些值,属性相对参照的是什么?
盒子模型,如何控制盒子模型的位置
如何使用css画一个三角形
三列布局的实现方式?分别怎么实现(Float、Position、Flex逻辑有点混乱)
Flex布局的默认对齐方式是什么(说错了)?如果一行装不下的话怎么去实现flex-wrap? 考虑子元素的换行方式
对同源策略有了解吗?跨域的实现方式都说一下?
JSONP的原理,以及jsonp中的回调函数应该挂载到那里?
CORS的实现原理?服务器端是实现接口还是配置?使用注意?
如何去判别Array的数据类型?(typeof不可以,instanceOf,object.prototype.toString().all())
CSS的选择器哪些?优先级是什么?
场景题:div .ab{} 和 .ab div{}哪个效果更好一些?为什么?
关于数组的操作问了很多:map()和forEach()的对比
1)如果只是遍历数组不修改用什么?(应该是使用forEach吧)
foreach的用法(function(item,index){},this), this默认undifined
2)如何删除数组中的元素(splice(stratIndex,
count , content…)})每个参数的含义,如果第二个参数设为0什么意思?
3)你刚才说的slice()是在原数组上操作吗?(不是,生成新的,返回一个新数组)
那splice呢?是在原数组上面修改的吗?
ES5中的什么属性可以清除掉?
编程题:
for(let i = 0; i < 5 ;i++){ let i = 3}
会不会报错?为什么?(依旧是考察变量的提升)
升序数组,求和为num的两个元素。(双指针)
数组的去重,时间复杂度 O(n)(不能利用Set) 实现方式:indexOf、Object、Map
提问:
Q:目前主要使用的技术栈是什么? 基于React的H5 和 小程序都有
Q:业务覆盖范围有哪些?理财啊,黄金啊之类的金融相关的内容
二面记错面试时间了,约的时候就以为是十点钟,在日历上标记了也是10:00,所以10:20的时候去问了HR以为是面试官迟到了,之后接到电话赶紧给面试官道歉!
时间: 2020.03.25 10:30 - 11:06
1.有用过Canvas?和Svg的区别(绘制图片的格式,Svg是矢量图;事件处理器之类;适用范围不一样)
2.说一下项目吧?有什么难点和亮点
介绍了大概15min项目
3.了解Flex布局吗?主轴和交叉轴讲解下
三列布局讲解下(flex实现)
webSocket和传统的axios轮询的区别?
跨域有什么了解?有没有实现跨域的方式?对比jsonp和Cors
如何实现深拷贝?为啥要深拷贝?
熟悉node吗?有用过吗?
ES6中的新语法用过没?跟我讲下箭头函数吧?
浏览器从输入URL到加载页面发生了什么?
cookie,sessionStorage、localStorage的区别
移动端你了解吧?那怎么办解决iphone的显示适配问题
事件的传递,监听
算法题:
使用js实现一个洗牌的函数
/*洗牌函数的封装*/function getRandom(min,max){ return Math.floor(Math.random() * (max - min +1) + min);}function shuffle(arr){ //不修改原数组 let _arr=arr.slice(); for(let i=0;i<_arr.length;i++){ let j=getRandom(0,i); let t=_arr[i]; _arr[i]=_arr[j]; _arr[j]=t; } return _arr;}
提问:
Q:对实习生的管理?会有那种职位的集体培训之类吗?
A:是直接边干活边学习的~~~
Q:谈了下自己的缺点,React还不是很熟练
时间: 2020.03.28 12:34-13:01
浏览器输入URL到解析的全过程
在这个过程中,我们前端可以从哪些方面进行优化?如何优化?
那CDN这种在里面担任什么角色,CDN的原理是什么?
首页白屏加载?
计网的ip是osi中的哪一层,tcp呢,是干嘛的
操作系统的内存管理方式
说一下项目的具体细节(讲了登录token验证之类)
你使用的是vue,那你觉得vue和react这些都有什么特点?
说一下你未来的工作规划
本科期间有什么项目吗?承担什么职责?选择前端?
公司: 字节跳动
部门: 头条研发
时间: 2020.03.25 14:30-16:39
简单的自我介绍,项目中的技术之类,提到什么问什么
项目中的websocket具体实现,有没有测过触及率
事件传递的阶段(事件捕获,处于目标,事件冒泡)
手写addEventListener
实现监听
什么时候使用onclick ? 什么时候使用addEventListener
?参数有几个?(x)
如果我们想捕获触发阶段的事件怎么办? (我后来才明白面试官是在牵引我)
v-if 和 v-show的区别
一个url里面有哪些成分?给出一个网址分析下
DNS解析的原理(x)
CSS的盒模型,如何实现的?
谈一下深拷贝?以及如何实现
typeof []
和 []instance of Object
的结果
手写下你项目中的throttle
函数?有没有什么可以改下?
问了我项目的代码量
观察程序的输出结果
for(var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i) },5)}
如何让输出结果变为01234
面试官人很好,还帮我解答了上面的onClick
和addEventListener
,我之前说感觉没区别,然后面试官让我去查下addEventListenter
的可选参数setCapture
,瞬间就明白了~ 让我稍等下,联系二面的面试官
for(var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i) },1000)}
这个是怎样的输出?每个1s输出一个还是怎样?解释下为什么觉得这样
如何使用异步?说说你理解的异步
又给了一个程序看运行结果,解释下为什么这样
console.log('script start')let promise1 = new Promise(function (resolve) { console.log('promise1') resolve() console.log('promise1 end')}).then(function () { console.log('promise2')})setTimeout(function(){ console.log('settimeout')}, 1)console.log('script end')
(考察Promise以及异步)
实际的场景发生了改变,结合了Promise()
setTimeout
(func(){},1)会执行吗?为什么? 4ms
说一下跨域吧,以及跨域的实现方式
自己从代理跨域延伸到了webSocket,顺带就结合自己的项目谈了谈
你的项目中实现登陆功能?多角色的登录?
你们在项目中实现清除token或者判断token过期?
了解webpack的打包中如何分块打包
Router的底层原理(x)?怎么去实现name、path这些的管理
webpack里面的treeshaking和 code splitting(不了解,所以说了相关的webpack的机制)
结合项目谈Vuex的状态管理(mutation、action、state)
说一下Vue中的MVVM的模式
跨域中的JSONP有什么局限?CORS中怎么设置Http?服务端和前端?
题目1: 手写call
Function.prototype._call = function(ctx) {}fn.call()fn._call()
题目2:
for (var i = 0; i < 5; i++) { setTimeout(function () { console.log(i) },1000); }console.log('script start')let promise1 = new Promise(function (resolve) { console.log('promise1') resolve() console.log('promise1 end')}).then(function () { console.log('promise2')})setTimeout(function(){ console.log('settimeout')}, 1)console.log('script end')
题目3:请写出一个可以生成整形随机数数组(内部元素不重复)的函数,并可以根据参数设置随机数生成的范围和数量,例如:函数madeRandomList(a, b, c)
,可以生成 [a, b]
范围内,长度为 c 的随机数数组
题目4:DFS深度优先遍历DOM树[“div”, “span”, “a”, “div”, “a”, “span”, “p”]
这个开始没思路,因为对DOM操作太不熟悉了,然后面试官跟我讲,说下树的深度优先遍历也可以,感觉面试官真的非常Nice啊!
灵活一些的问题:
提问环节:
时间: 2020.03.30 11:30-12:30
说一下单点登录(我上来絮絮叨叨说了半天项目中登录逻辑的实现,直到面试官重新说了一遍)SSO
ES6的新属性
Promise以及Promise中的race方法
谈谈你对缓存的理解
CSS中进入进出动画的实现
AI看图作曲项目中负责的内容
小程序的生命周期(应用&页面)
JS实现数组去重的多种方式
建议:对专业术语都不清楚,知识点的掌握有点囫囵吞枣,没有真正理解Set的概念,稳固扎实,简历的项目很简单
学习计划,学习前端的时间
PC端的开放平台toB产品
时间: 2020.03.31 11:15-11:46
ZOOM视频面
在这里必须说,我爱死了字节的效率!!!
.
]]>