周小e丶

周小e丶 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

周小e丶 赞了文章 · 2020-10-12

纠结的链接——ln、ln -s、fs.symlink、require

本文首发于我的博客,转载请注明出处:http://kohpoll.github.io/blog/2016/05/30/hardlink-symlink-require-in-nodejs/

最近在使用 fs.symlink 实现软链时,发现文档里面写的是:fs.symlink(target, path);然而 man ln 的时候显示的是:ln source_file target_file;而且,require 模块的时候其实还会处理软链但是处理的又不是想象中那样。于是,我彻底被相关东西绕晕。这篇文章算是我的学习笔记,希望对你有帮助。

inode

我们首先来看看 linux 系统里面的一个重要概念:inode。

我们知道,文件存储在硬盘上,硬盘存储的最小单位是扇区(sector,每个扇区 512 B)。而操作系统读取文件时,按块读取(连续的多个扇区),也就是说文件存取的最小单位是块(block,块通常是 4 KB)。

除了文件数据,我们还必须存储文件的元信息(如:文件大小、文件创建者、文件数据的块位置、文件读/写/执行权限、文件时间戳等等),这种存储文件元信息的结构就称为 inode。我们可以使用 stat 命令查看文件的 inode 信息。在 Node 中,调用 fs.stat 后返回的结果中也有相关信息

stat

每个 inode 都有一个唯一的号码标志,linux 系统内部使用 inode 的号码来识别文件,并不使用文件名。我们打开一个文件时,系统首先找到文件名对应的 inode 号码,然后通过 inode 号码获取 inode 信息,最后根据 inode 信息中的文件数据所在的 block 读出数据。

实际上,在 linux 系统中,目录也是一种文件。目录文件包含一系列目录项,每个目录项由两部分组成:所包含文件的文件名,以及该文件名对应的 inode 号码。我们可以使用 ls -i 来列出目录中的文件以及它们的 inode 号码。这其实也解释了仅更改目录的读权限,并不能实现读取目录下所有文件内容的原因,通常需要 chmod -R 来进行递归更改。

总结下:

  • 硬盘存取的最小单位是扇区,文件存取的最小单位是块(连续的扇区)

  • 存储文件元信息(文件大小、创建者、块位置、时间戳、权限等非数据信息)的结构称为 inode

  • 每个 inode 拥有一个唯一号码,系统内部通过它来识别文件

  • 目录也是一种文件,其内容包含一系列目录项(每个目录项由文件的文件名和文件对应的 inode 号码组成)

硬链接和软链接

硬链接

一般情况,一个文件名“唯一”对应一个 inode。但是,linux 允许多个文件名都指向同一个 inode。这表示我们可以使用不同的文件名访问同样的内容;对文件内容进行修改将“反映”到所有文件;删除一个文件不影响另一个文件的访问 。这种机制就被称为“硬链接”。

我们可以使用 ln source target 来建立硬链接(注意:source 是本身已存在的文件,target 是将要建立的链接)。

hard link

形象化的表示为下图:

hard link graph

需要注意的是,只能给文件建立硬链接,而不能给目录建立硬链接。另外,source 文件必须存在,否则将会报错。

删除一个文件为什么不影响另一个文件的访问呢?实际上,文件 inode 中还有一个链接数的信息,每多一个文件指向这个 inode,该数字就会加 1,每少一个文件指向这个 inode,该数字就会减 1,当值减到 0,系统就自动回收 inode 及其对应的 block 区域。很像是一种引用计数的垃圾回收机制。

当我们对某个文件建立了硬链接后,对应的 inode 的链接数会是 2(原文件本身已经有一个指向),当删除一个文件时,链接数变成 1,并没达到回收的条件,所以我们还是可以访问文件。

软链接

软链接类似于 windows 中的”快捷方式“。两个文件虽然 inode 号码不一样,但是文件 A 内部会指向文件 B 的 inode。当我们读取文件 A 时,系统就自动导向文件 B,文件 A 就是文件 B 的软链接(或者叫符号链接)。这表示我们同样可以使用不同的文件名访问同样的内容;对文件内容修改将”反映“到所有文件。但是当我们删除掉源文件 B 时,再访问文件 A 时会报错 “No such file or directory”。

我们可以使用 ln -s source target 来建立软链接(注意:表示让 target “指向”source)。

soft link

形象化的表示为下图:

soft link graph

和硬链接不同,我们可以给目录建立软链接,这带来许多便利。比如我们有一个模块有很多个版本,分别存放在 1.0.0、2.0.0 这样的目录下面,当更新模块时,只需要建立一个软链接指向最新版本号的目录就能很方便的切换版本。

另外,建立软链接时,source 是可以不存在的。这很像一种”运行时“机制,而不是“编译时”机制,建立的时候不报错,等执行的时候发现找不到就报错了。

danggling soft link

总结

  • 使用 ln source target 建立硬链接;使用 ln -s source target 建立软链接

  • 硬链接不会创建额外 inode,和源文件共用同一个 inode;软链接会创建额外一个文件(额外 inode),指向源文件的 inode

  • 建立硬链接时,source 必须存在且只能是文件;建立软链接时,source 可以不存在而且可以是目录

  • 删除源文件不会影响硬链接文件的访问(因为 inode 还在);删除源文件会影响软链接文件的访问(因为指向的 inode 已经不存在了)

  • 对于已经建立的同名链接,不能再次建立,除非删掉或者使用 -f 参数

关于软链接的补充

上面的例子 ln -s file file-soft 给我们的感觉像是 file-soft 是“凭空”出现的。当我们跨目录来创建软链接时,可能会“幻想”这样的命令也是可以生效的:ln -s ~/development/mod ~/production/dir-not-exits/mod

这里并没有 ~/production/dir-not-exits/ 这个目录,而软链接本质上是一个新的“文件”,所以,我们不可能正确建立软链接(会报错说 “no such file or directory”)。

如果我们先通过 mkdir 建立好目录 ~/production/dir-not-exits/,再进行软链接,即可达到预期效果。

fs.symlink

在 node 中,我们可以使用方法 fs.symink(target, path) 建立软链接(符号链接),没有直接的方法建立硬链接(就算通过子进程的方式直接指向 shell 命令也不能跨平台)。

如果是对目录建立链接,请总是传递第三个参数 dir(虽然第三个参数只在 windows 下生效,这可以保证代码跨平台):fs.symlink(target, path, 'dir')

为啥这个接口的参数会是 targetpath。实际上这是一个 linux 的 API,symlink(target, linkpath)。它是这样描述的:建立一个名为 linkpath 的符号链接并且含有内容 target。其实就是让 linkpath 指向 target,和 ln -s source target 功能一样,让 target 指向 source

是不是有点晕?其实我们只需要明白 ln -sfs.symlink 后面传递的两个参数顺序是一致的,只是叫法不一样,使用起来也就没那么纠结了:

ln -s file file-soft # file-soft -> file
ln -s dir dir-soft # dir-soft -> dir
fs.symlinkSync('file', 'file-soft'); // file-soft -> file
fs.symlinkSync('dir', 'dir-soft', 'dir'); // dir-soft -> dir

require

在 Node 中,我们经常通过 require 来引用模块。非常有趣的是,require 引用模块时,会“考虑”符号链接,但是却使用模块的真实路径作为 __filename__dirname,而不是符号链接的路径。

考虑下面的目录结构:

- app
  - index.js // require('dep1')
  - node_modules
    - dep1 -> ../../mods/dep1 //符号链接
- mods
  - dep1
    - index.js

以及下面的文件内容:

// index.js
console.log('index.js', __dirname, __filename);
require('dep1');

// dep1/index.js
console.log('dep1', __dirname, __filename);
console.log(module.paths);

执行 node index.js 后输出是下面这样:

index.js /Users/kohpoll/Workspace/test/app /Users/kohpoll/Workspace/test/app/index.js

dep1 /Users/kohpoll/Workspace/test/mods/dep1 /Users/kohpoll/Workspace/test/mods/dep1/index.js
[ '/Users/kohpoll/Workspace/test/mods/dep1/node_modules',
  '/Users/kohpoll/Workspace/test/mods/node_modules',
  '/Users/kohpoll/Workspace/test/node_modules',
  '/Users/kohpoll/Workspace/node_modules',
  '/Users/kohpoll/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

我们发现,index.js 可以成功的 require('dep1')。这很好啊,这让我们调试本地开发中的 npm 模块很方便。我们只需要去 require 模块的文件所在的 node_modules 下面建立一个符号链接就行了。

但是在模块 dep1 中,__dirname__filename 都变成了模块实际的路径,更要命的是模块查找路径 module.paths 也变成了从实际路径开始查找。

这会带来什么问题?

再考虑下面的目录结构:

- app
  - index.js // require('dep1')
  - node_modules
    - dep1 -> ../../mods/dep1 // require('dep2')
    - dep2 -> ../../mods/dep2 // 符号连接
- mods
  - dep1
    - index.js
  - dep2
    - index.js

以及下面的文件内容:

// index.js
console.log('index.js', __dirname, __filename);
require('dep1');

// dep1/index.js
console.log('dep1', __dirname, __filename);
console.log(module.paths);
require('dep2');

// dep2/index.js
console.log('dep2', __dirname, __filename);
console.log(module.paths);

当我们再执行 node index.js 时,输出是下面这样:

index.js /Users/kohpoll/Workspace/test/app /Users/kohpoll/Workspace/test/app/index.js

dep1 /Users/kohpoll/Workspace/test/mods/dep1 /Users/kohpoll/Workspace/test/mods/dep1/index.js
[ '/Users/kohpoll/Workspace/test/mods/dep1/node_modules',
  '/Users/kohpoll/Workspace/test/mods/node_modules',
  '/Users/kohpoll/Workspace/test/node_modules',
  '/Users/kohpoll/Workspace/node_modules',
  '/Users/kohpoll/node_modules',
  '/Users/node_modules',
  '/node_modules' ]
  
module.js:339
    throw err;
    ^
Error: Cannot find module 'dep2'
    at Function.Module._resolveFilename (module.js:337:15)
    at Function.Module._load (module.js:287:25)
    at Module.require (module.js:366:17)
    at require (module.js:385:17)
    at Object.<anonymous> (/Users/kohpoll/Workspace/test/mods/dep1/index.js:6:1)
    at Module._compile (module.js:435:26)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:311:12)
    at Module.require (module.js:366:17)

发现了吗?dep1 根本就 require 不到 dep2,因为 dep2 不在它的查找路径里面!

关于这个问题,github 上有一个冗长的 issue 在讨论。问题解决起来确实很麻烦,而且会 break 掉一大堆已有功能,所以,最终的结论是在找到更好的方法前给 node v6 增加了一个 --preserve-symlinks 选项来禁止这种 require 的行为,而是使用全新的 require 逻辑。有兴趣和闲情的可以去围观:https://github.com/nodejs/node/issues/3402(真的好长......)。

至于全新的 require 逻辑会不会有新的坑,在没有具体实践前,我也不知道。

那我们上面的情况有办法解决吗?其实也有,那就是将目录结构调整成下面这样,从而让 dep2 能在 dep1 的查找路径里面:

- app
  - index.js // require('dep1')
  - node_modules
    - dep1 -> ../../mods/node_modules/dep1 // 符号链接
    - dep2 -> ../../mods/node_modules/dep2 // 符号链接
- mods
  - node_modules
    - dep1
      - index.js
    - dep2
      - index.js

参考链接

查看原文

赞 3 收藏 11 评论 0

周小e丶 赞了文章 · 2020-10-10

万字长文+图文并茂+全面解析微前端框架 qiankun 源码 - qiankun 篇

写在开头

微前端系列文章:

本系列其他文章计划一到两个月内完成,点个 关注 不迷路。

计划如下:

  • 生命周期篇;
  • IE 兼容篇;
  • 生产环境部署篇;
  • 性能优化、缓存方案篇;

引言

本文将针对微前端框架 qiankun 的源码进行深入解析,在源码讲解之前,我们先来了解一下什么是 微前端

微前端 是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。同时,它们也可以在共享组件的同时进行并行开发——这些组件可以通过 NPM 或者 Git Tag、Git Submodule 来管理。

qiankun(乾坤) 就是一款由蚂蚁金服推出的比较成熟的微前端框架,基于 single-spa 进行二次开发,用于将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。(见下图)

qiankun

那么,话不多说,我们的源码解析正式开始。

初始化全局配置 - start(opts)

我们从两个基础 API - registerMicroApps(apps, lifeCycles?) - 注册子应用start(opts?) - 启动主应用 开始,由于 registerMicroApps 函数中设置的回调函数较多,并且读取了 start 函数中设置的初始配置项,所以我们从 start 函数开始解析。

我们从 start 函数开始解析(见下图):

qiankun

我们对 start 函数进行逐行解析:

  • 第 196 行:设置 window__POWERED_BY_QIANKUN__ 属性为 true,在子应用中使用 window.__POWERED_BY_QIANKUN__ 值判断是否运行在主应用容器中。
  • 第 198~199 行:设置配置参数(有默认值),将配置参数存储在 importLoaderConfiguration 对象中;
  • 第 201~203 行:检查 prefetch 属性,如果需要预加载,则添加全局事件 single-spa:first-mount 监听,在第一个子应用挂载后预加载其他子应用资源,优化后续其他子应用的加载速度。
  • 第 205 行:根据 singularMode 参数设置是否为单实例模式。
  • 第 209~217 行:根据 jsSandbox 参数设置是否启用沙箱运行环境,旧版本需要关闭该选项以兼容 IE。(新版本在单实例模式下默认支持 IE,多实例模式依然不支持 IE)。
  • 第 222 行:调用了 single-spastartSingleSpa 方法启动应用,这个在 single-spa 篇我们会单独剖析,这里可以简单理解为启动主应用。

从上面可以看出,start 函数负责初始化一些全局设置,然后启动应用。这些初始化的配置参数有一部分将在 registerMicroApps 注册子应用的回调函数中使用,我们继续往下看。

注册子应用 - registerMicroApps(apps, lifeCycles?)

registerMicroApps 函数的作用是注册子应用,并且在子应用激活时,创建运行沙箱,在不同阶段调用不同的生命周期钩子函数。(见下图)

qiankun

从上面可以看出,在 第 70~71 行registerMicroApps 函数做了个处理,防止重复注册相同的子应用。

第 74 行 调用了 single-sparegisterApplication 方法注册了子应用。

我们直接来看 registerApplication 方法,registerApplication 方法是 single-spa 中注册子应用的核心函数。该函数有四个参数,分别是

  • name(子应用的名称)
  • 回调函数(activeRule 激活时调用)
  • activeRule(子应用的激活规则)
  • props(主应用需要传递给子应用的数据)

这些参数都是由 single-spa 直接实现,这里可以先简单理解为注册子应用(这个我们会在 single-spa 篇展开说)。在符合 activeRule 激活规则时将会激活子应用,执行回调函数,返回一些生命周期钩子函数(见下图)。

注意,这些生命周期钩子函数属于 single-spa,由 single-spa 决定在何时调用,这里我们从函数名来简单理解。(bootstrap - 初始化子应用,mount - 挂载子应用,unmount - 卸载子应用)

qiankun

如果你还是觉得有点懵,没关系,我们通过一张图来帮助理解。(见下图)

qiankun

获取子应用资源 - import-html-entry

我们从上面分析可以看出,qiankunregisterMicroApps 方法中第一个入参 apps - Array<RegistrableApp<T>> 有三个参数 name、activeRule、props 都是交给 single-spa 使用,还有 entryrender 参数还没有用到。

我们这里需要关注 entry(子应用的 entry 地址)render(子应用被激活时触发的渲染规则) 这两个还没有用到的参数,这两个参数延迟到 single-spa 子应用激活后的回调函数中执行。

那我们假设此时我们的子应用已激活,我们来看看这里做了什么。(见下图)

qiankun

从上图可以看出,在子应用激活后,首先在 第 81~84 行 处使用了 import-html-entry 库从 entry 进入加载子应用,加载完成后将返回一个对象(见下图)

qiankun

我们来解释一下这几个字段

字段解释
template将脚本文件内容注释后的 html 模板文件
assetPublicPath资源地址根路径,可用于加载子应用资源
getExternalScripts方法:获取外部引入的脚本文件
getExternalStyleSheets方法:获取外部引入的样式表文件
execScripts方法:执行该模板文件中所有的 JS 脚本文件,并且可以指定脚本的作用域 - proxy 对象

我们先将 template 模板getExternalScriptsgetExternalStyleSheets 函数的执行结果打印出来,效果如下(见下图):

qiankun

从上图我们可以看到我们外部引入的三个 js 脚本文件,这个模板文件没有外部 css 样式表,对应的样式表数组也为空。

然后我们再来分析 execScripts 方法,该方法的作用就是指定一个 proxy(默认是 window)对象,然后执行该模板文件中所有的 JS,并返回 JS 执行后 proxy 对象的最后一个属性(见下图 1)。在微前端架构中,这个对象一般会包含一些子应用的生命周期钩子函数(见下图 2),主应用可以通过在特定阶段调用这些生命周期钩子函数,进行挂载和销毁子应用的操作。

qiankun

qiankun

qiankunimportEntry 函数中还传入了配置项 getTemplate,这个其实是对 html 目标文件的二次处理,这里就不作展开了,有兴趣的可以自行去了解一下。

主应用挂载子应用 HTML 模板

我们回到 qiankun 源码部分继续看(见下图)

qiankun

从上图看出,在 第 85~87 行 处,先对单实例进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载之后才开始。

第 88 行 中,执行注册子应用时传入的 render 函数,将 HTML Templateloading 作为入参,render 函数的内容一般是将 HTML 挂载在指定容器中(见下图)。

qiankun

在这个阶段,主应用已经将子应用基础的 HTML 结构挂载在了主应用的某个容器内,接下来还需要执行子应用对应的 mount 方法(如 Vue.$mount)对子应用状态进行挂载。

此时页面还可以根据 loading 参数开启一个类似加载的效果,直至子应用全部内容加载完成。

沙箱运行环境 - genSandbox

我们回到 qiankun 源码部分继续看,此时还是子应用激活时的回调函数部分(见下图)

qiankun

第 90~98 行qiankun 比较核心的部分,也是几个子应用之间状态独立的关键,那就是 js 的沙箱运行环境。如果关闭了 useJsSandbox 选项,那么所有子应用的沙箱环境都是 window,就很容易对全局状态产生污染。

我们进入到 genSandbox 内部,看看 qiankun 是如何创建的 (JS)沙箱运行环境。(见下图)

qiankun

从上图可以看出 genSandbox 内部的沙箱主要是通过是否支持 window.Proxy 分为 LegacySandboxSnapshotSandbox 两种。

扩展阅读:多实例还有一种 ProxySandbox 沙箱,这种沙箱模式目前看来是最优方案。由于其表现与旧版本略有不同,所以暂时只用于多实例模式。

ProxySandbox 沙箱稳定之后可能会作为单实例沙箱使用。

LegacySandbox

我们先来看看 LegacySandbox 沙箱是怎么进行状态隔离的(见下图)

qiankun

我们来分析一下 LegacySandbox 类的几个属性:

字段解释
addedPropsMapInSandbox记录沙箱运行期间新增的全局变量
modifiedPropsOriginalValueMapInSandbox记录沙箱运行期间更新的全局变量
currentUpdatedPropsValueMap记录沙箱运行期间操作过的全局变量。上面两个 Map 用于 关闭沙箱 时还原全局状态,而 currentUpdatedPropsValueMap 是在 激活沙箱 时还原沙箱的独立状态
name沙箱名称
proxy代理对象,可以理解为子应用的 global/window 对象
sandboxRunning当前沙箱是否在运行中
active激活沙箱,在子应用挂载时启动
inactive关闭沙箱,在子应用卸载时启动
constructor构造函数,创建沙箱环境

我们现在从 window.Proxysetget 属性来详细讲解 LegacySandbox 是如何实现沙箱运行环境的。(见下图)

qiankun

注意:子应用沙箱中的 proxy 对象(第 62 行)可以简单理解为子应用的 window 全局对象(代码如下),子应用对全局属性的操作就是对该 proxy 对象属性的操作,带着这份理解继续往下看吧。
// 子应用脚本文件的执行过程:
eval(
  // 这里将 proxy 作为 window 参数传入
  // 子应用的全局对象就是该子应用沙箱的 proxy 对象
  (function(window) {
    /* 子应用脚本文件内容 */
  })(proxy)
);

第 65~72 行中,当调用 set 向子应用 proxy/window 对象设置属性时,所有的属性设置和更新都会先记录在 addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox 中,然后统一记录到
currentUpdatedPropsValueMap 中。

第 73 行 中修改全局 window 的属性,完成值的设置。

当调用 get 从子应用 proxy/window 对象取值时,会直接从 window 对象中取值。对于非构造函数的取值将会对 this 指针绑定到 window 对象后,再返回函数。

LegacySandbox 的沙箱隔离是通过激活沙箱时还原子应用状态,卸载时还原主应用状态(子应用挂载前的全局状态)实现的,具体实现如下(见下图)。

qiankun

从上图可以看出:

  • 第 37 行:在激活沙箱时,沙箱会通过 currentUpdatedPropsValueMap 查询到子应用的独立状态池(沙箱可能会激活多次,这里是沙箱曾经激活期间被修改的全局变量),然后还原子应用状态。
  • 第 44~45 行:在关闭沙箱时,通过 addedPropsMapInSandbox 删除在沙箱运行期间新增的全局变量,通过 modifiedPropsOriginalValueMapInSandbox 还原沙箱运行期间被修改的全局变量,从而还原到子应用挂载前的状态。

从上面的分析可以得知,LegacySandbox 的沙箱隔离机制利用快照模式实现,我们画一张图来帮助理解(见下图)

qiankun

多实例沙箱 - ProxySandbox

ProxySandbox 是一种新的沙箱模式,目前用于多实例模式的状态隔离。在稳定后以后可能会成为 单实例沙箱,我们来看看 ProxySandbox 沙箱是怎么进行状态隔离的(见下图)

qiankun

我们来分析一下 ProxySandbox 类的几个属性:

字段解释
updateValueMap记录沙箱中更新的值,也就是每个子应用中独立的状态池
name沙箱名称
proxy代理对象,可以理解为子应用的 global/window 对象
sandboxRunning当前沙箱是否在运行中
active激活沙箱,在子应用挂载时启动
inactive关闭沙箱,在子应用卸载时启动
constructor构造函数,创建沙箱环境

我们现在从 window.Proxysetget 属性来详细讲解 ProxySandbox 是如何实现沙箱运行环境的。(见下图)

qiankun

注意:子应用沙箱中的 proxy 对象可以简单理解为子应用的 window 全局对象(代码如下),子应用对全局属性的操作就是对该 proxy 对象属性的操作,带着这份理解继续往下看吧。
// 子应用脚本文件的执行过程:
eval(
  // 这里将 proxy 作为 window 参数传入
  // 子应用的全局对象就是该子应用沙箱的 proxy 对象
  (function(window) {
    /* 子应用脚本文件内容 */
  })(proxy)
);

当调用 set 向子应用 proxy/window 对象设置属性时,所有的属性设置和更新都会命中 updateValueMap,存储在 updateValueMap 集合中(第 38 行),从而避免对 window 对象产生影响(旧版本则是通过 diff 算法还原 window 对象状态快照,子应用之间的状态是隔离的,而父子应用之间 window 对象会有污染)。

当调用 get 从子应用 proxy/window 对象取值时,会优先从子应用的沙箱状态池 updateValueMap 中取值,如果没有命中才从主应用的 window 对象中取值(第 49 行)。对于非构造函数的取值将会对 this 指针绑定到 window 对象后,再返回函数。

如此一来,ProxySandbox 沙箱应用之间的隔离就完成了,所有子应用对 proxy/window 对象值的存取都受到了控制。设置值只会作用在沙箱内部的 updateValueMap 集合上,取值也是优先取子应用独立状态池(updateValueMap)中的值,没有找到的话,再从 proxy/window 对象中取值。

相比较而言,ProxySandbox 是最完备的沙箱模式,完全隔离了对 window 对象的操作,也解决了快照模式中子应用运行期间仍然会对 window 造成污染的问题。

我们对 ProxySandbox 沙箱画一张图来加深理解(见下图)

qiankun

SnapshotSandbox

在不支持 window.Proxy 属性时,将会使用 SnapshotSandbox 沙箱,我们来看看其内部实现(见下图)

qiankun

我们来分析一下 SnapshotSandbox 类的几个属性:

字段解释
name沙箱名称
proxy代理对象,此处为 window 对象
sandboxRunning当前沙箱是否激活
windowSnapshotwindow 状态快照
modifyPropsMap沙箱运行期间被修改过的 window 属性
constructor构造函数,激活沙箱
active激活沙箱,在子应用挂载时启动
inactive关闭沙箱,在子应用卸载时启动

SnapshotSandbox 的沙箱环境主要是通过激活时记录 window 状态快照,在关闭时通过快照还原 window 对象来实现的。(见下图)

qiankun

我们先看 active 函数,在沙箱激活时,会先给当前 window 对象打一个快照,记录沙箱激活前的状态(第 38~40 行)。打完快照后,函数内部将 window 状态通过 modifyPropsMap 记录还原到上次的沙箱运行环境,也就是还原沙箱激活期间(历史记录)修改过的 window 属性。

在沙箱关闭时,调用 inactive 函数,在沙箱关闭前通过遍历比较每一个属性,将被改变的 window 对象属性值(第 54 行)记录在 modifyPropsMap 集合中。在记录了 modifyPropsMap 后,将 window 对象通过快照 windowSnapshot 还原到被沙箱激活前的状态(第 55 行),相当于是将子应用运行期间对 window 造成的污染全部清除。

SnapshotSandbox 沙箱就是利用快照实现了对 window 对象状态隔离的管理。相比较 ProxySandbox 而言,在子应用激活期间,SnapshotSandbox 将会对 window 对象造成污染,属于一个对不支持 Proxy 属性的浏览器的向下兼容方案。

我们对 SnapshotSandbox 沙箱画一张图来加深理解(见下图)

qiankun

挂载沙箱 - mountSandbox

qiankun

我们继续回到这张图,genSandbox 函数不仅返回了一个 sandbox 沙箱,还返回了一个 mountunmount 方法,分别在子应用挂载时和卸载时的时候调用。

我们先看看 mount 函数内部(见下图)

qiankun

首先,在 mount 内部先激活了子应用沙箱(第 26 行),在沙箱启动后开始劫持各类全局监听(第 27 行),我们这里重点看看 patchAtMounting 内部是怎么实现的。(见下图)

qiankun

patchAtMounting 内部调用了下面四个函数:

  • patchTimer(计时器劫持)
  • patchWindowListener(window 事件监听劫持)
  • patchHistoryListener(window.history 事件监听劫持)
  • patchDynamicAppend(动态添加 Head 元素事件劫持)

上面四个函数实现了对 window 指定对象的统一劫持,我们可以挑一些解析看看其内部实现。

计时器劫持 - patchTimer

我们先来看看 patchTimer 对计时器的劫持(见下图)

qiankun

从上图可以看出,patchTimer 内部将 setInterval 进行重载,将每个启用的定时器的 intervalId 都收集起来(第 23~24 行),以便在子应用卸载时调用 free 函数将计时器全部清除(见下图)。

qiankun

我们来看看在子应用加载时的 setInterval 函数验证即可(见下图)

qiankun

从上图可以看出,在进入子应用时,setInterval 已经被替换成了劫持后的函数,防止全局计时器泄露污染。

动态添加样式表和脚本文件劫持 - patchDynamicAppend

patchWindowListenerpatchHistoryListener 的实现都与 patchTimer 实现类似,这里就不作复述了。

我们需要重点对 patchDynamicAppend 函数进行解析,这个函数的作用是劫持对 head 元素的操作(见下图)

qiankun

从上图可以看出,patchDynamicAppend 主要是对动态添加的 style 样式表和 script 标签做了处理。

我们先看看对 style 样式表的处理(见下图)

qiankun

从上图可以看出,主要的处理逻辑在 第 68~74 行,如果当前子应用处于激活状态(判断子应用的激活状态主要是因为:当主应用切换路由时可能会自动添加动态样式表,此时需要避免主应用的样式表被添加到子应用 head 节点中导致出错),那么动态 style 样式表就会被添加到子应用容器内(见下图),在子应用卸载时样式表也可以和子应用一起被卸载,从而避免样式污染。同时,动态样式表也会存储在 dynamicStyleSheetElements 数组中,在后面还会提到其用处。

qiankun

我们再来看看对 script 脚本文件的处理(见下图)

qiankun

对动态 script 脚本文件的处理较为复杂一些,我们也来解析一波:

第 83~101 行 处对外部引入的 script 脚本文件使用 fetch 获取,然后使用 execScripts 指定 proxy 对象(作为 window 对象)后执行脚本文件内容,同时也触发了 loaderror 两个事件。

第 103~106 行 处将注释后的脚本文件内容以注释的形式添加到子应用容器内。

第 109~113 行 是对内嵌脚本文件的执行过程,就不作复述了。

我们可以看出,对动态添加的脚本进行劫持的主要目的就是为了将动态脚本运行时的 window 对象替换成 proxy 代理对象,使子应用动态添加的脚本文件的运行上下文也替换成子应用自身。

HTMLHeadElement.prototype.removeChild 的逻辑就是多加了个子应用容器判断,其他无异,就不展开说了。

最后我们来看看 free 函数(见下图)

qiankun

这个 free 函数与其他的 patches(劫持函数) 实现不太一样,这里缓存了一份 cssRules,在重新挂载的时候会执行 rebuild 函数将其还原。这是因为样式元素 DOM 从文档中删除后,浏览器会自动清除样式元素表。如果不这么做的话,在重新挂载时会出现存在 style 标签,但是没有渲染样式的问题。

卸载沙箱 - unmountSandbox

我们再回到 mount 函数本身(见下图)

qiankun

从上图可以看出,在 patchAtMounting 函数中劫持了各类全局监听,并返回了解除劫持的 free 函数。在卸载应用时调用 free 函数解除这些全局监听的劫持行为(见下图)

qiankun

从上图可以看到 sideEffectsRebuildersfree 后被返回,在 mount 的时候又将被调用 rebuild 重建动态样式表。这块环环相扣,是稍微有点绕,没太看明白的同学可以翻上去再看一遍。

到这里,qiankun 的最核心部分-沙箱机制,我们就已经解析完毕了,接下来我们继续剖析别的部分。

在这里我们画一张图,对沙箱的创建过程进行一个总梳理(见下图)

qiankun

注册内部生命周期函数

在创建好了沙箱环境后,在 第 100~106 行 注册了一些内部生命周期函数(见下图)

qiankun

在上图中,第 106 行mergeWith 方法的作用是将内置的生命周期函数与传入的 lifeCycles 生命周期函数。

这里的 lifeCycles 生命周期函数指的是全子应用共享的生命周期函数,可用于执行多个子应用间相同的逻辑操作,例如 加载效果 之类的。(见下图)

qiankun

除了外部传入的生命周期函数外,我们还需要关注 qiankun 内置的生命周期函数做了些什么(见下图)

qiankun

我们对上图的代码进行逐一解析:

  • 第 13~15 行:在加载子应用前 beforeLoad(只会执行一次)时注入一个环境变量,指示了子应用的 public 路径。
  • 第 17~19 行:在挂载子应用前 beforeMount(可能会多次执行)时可能也会注入该环境变量。
  • 第 23~30 行:在卸载子应用前 beforeUnmount 时将环境变量还原到原始状态。

通过上面的分析我们可以得出一个结论,我们可以在子应用中获取该环境变量,将其设置为 __webpack_public_path__ 的值,从而使子应用在主应用中运行时,可以匹配正确的资源路径。(见下图)

qiankun

触发 beforeLoad 生命周期钩子函数

在注册完了生命周期函数后,立即触发了 beforeLoad 生命周期钩子函数(见下图)

qiankun

从上图可以看出,在 第 108 行 中,触发了 beforeLoad 生命周期钩子函数。

随后,在 第 110 行 执行了 import-html-entryexecScripts 方法。指定了脚本文件的运行沙箱(jsSandbox),执行完子应用的脚本文件后,返回了一个对象,对象包含了子应用的生命周期钩子函数(见下图)。

qiankun

第 112~121 行 对子应用的生命周期钩子函数做了个检测,如果在子应用的导出对象中没有发现生命周期钩子函数,会在沙箱对象中继续查找生命周期钩子函数。如果最后没有找到生命周期钩子函数则会抛出一个错误,所以我们的子应用一定要有 bootstrap, mount, unmount 这三个生命周期钩子函数才能被 qiankun 正确嵌入到主应用中。

这里我们画一张图,对子应用挂载前的初始化过程做一个总梳理(见下图)

qiankun

进入到 mount 挂载流程

在一些初始化配置(如 子应用资源、运行沙箱环境、生命周期钩子函数等等)准备就绪后,qiankun 内部将其组装在一起,返回了三个函数作为 single-spa 内部的生命周期函数(见下图)

qiankun

single-spa 内部的逻辑我们后面再展开说,这里我们可以简单理解为 single-spa 内部的三个生命周期钩子函数:

  • bootstrap:子应用初始化时调用,只会调用一次;
  • mount:子应用挂载时调用,可能会调用多次;
  • unmount:子应用卸载时调用,可能会调用多次;

我们可以看出,在 bootstrap 阶段调用了子应用暴露的 bootstrap 生命周期函数。

我们这里对 mount 阶段进行展开,看看在子应用 mount 阶段执行了哪些函数(见下图)

qiankun

我们进行逐行解析:

  • 第 127~133 行:对单实例模式进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载之后才开始。(由于这里是串行顺序执行,所以如果某一处发生阻塞的话,会阻塞所有后续的函数执行)
  • 第 134 行:执行注册子应用时传入的 render 函数,将 HTML Templateloading 作为入参。这里一般是在发生了一次 unmount 后,再次进行 mount 挂载行为时将 HTML 挂载在指定容器中(见下图)

    由于初始化的时候已经调用过一次 render,所以在首次调用 mount 时可能已经执行过一次 render 方法。

    在下面的代码中也有对重复挂载的情况进行判断的语句 - if (frame.querySelector("div") === null,防止重复挂载子应用。

qiankun

  • 第 135 行:触发了 beforeMount 全局生命周期钩子函数;
  • 第 136 行:挂载沙箱,这一步中激活了对应的子应用沙箱,劫持了部分全局监听(如 setInterval)。此时开始子应用的代码将在沙箱中运行。(反推可知,在 beforeMount 前的部分全局操作将会对主应用造成污染,如 setInterval
  • 第 137 行:触发子应用的 mount 生命周期钩子函数,在这一步通常是执行对应的子应用的挂载操作(如 ReactDOM.render、Vue.$mount。(见下图)

qiankun

  • 第 138 行:再次调用 render 函数,此时 loading 参数为 false,代表子应用已经加载完成。
  • 第 139 行:触发了 afterMount 全局生命周期钩子函数;
  • 第 140~144 行:在单实例模式下设置 prevAppUnmountedDeferred 的值,这个值是一个 promise,在当前子应用卸载时才会被 resolve,在该子应用运行期间会阻塞其他子应用的挂载动作(第 134 行);

我们在上面很详细的剖析了整个子应用的 mount 挂载流程,如果你还没有搞懂的话,没关系,我们再画一个流程图来帮助理解。(见下图)

qiankun

进入到 unmount 卸载流程

我们刚才梳理了子应用的 mount 挂载流程,我们现在就进入到子应用的 unmount 卸载流程。在子应用激活阶段, activeRule 未命中时将会触发 unmount 卸载行为,具体的行为如下(见下图)

qiankun

从上图我们可以看出,unmount 卸载流程要比 mount 简单很多,我们直接来梳理一下:

  • 第 148 行:触发了 beforeUnmount 全局生命周期钩子函数;
  • 第 149 行:这里与 mount 流程的顺序稍微有点不同,这里先执行了子应用的 unmount 生命周期钩子函数,保证子应用仍然是运行在沙箱内,避免造成状态污染。在这里一般是对子应用的一些状态进行清理和卸载操作。(如下图,销毁了刚才创建的 vue 实例)

qiankun

  • 第 150 行:卸载沙箱,关闭了沙箱的激活状态。
  • 第 151 行:触发了 afterUnmount 全局生命周期钩子函数;
  • 第 152 行:触发 render 方法,并且传入的 appContent 为空字符串,此处可以清空主应用容器内的内容。
  • 第 153~156 行:当前子应用卸载完成后,在单实例模式下触发 prevAppUnmountedDeferred.resolve(),使其他子应用的挂载行为得以继续进行,不再阻塞。

我们对 unmount 卸载流程也画一张图,帮助大家理解(见下图)。

qiankun

总结

到这里,我们对 qiankun 框架的总流程梳理就差不多了。这里应该做个总结,大家看了这么多文字,估计大家也看累了,最后用一张图对 qiankun 的总流程进行总结吧。

qiankun

彩蛋

qiankun

展望

传统的云控制台应用,几乎都会面临业务快速发展之后,单体应用进化成巨石应用的问题。我们要如何维护一个巨无霸中台应用?

上面这个问题引出了微前端架构理念,所以微前端的概念也越来越火,我们团队最近也在尝试转型微前端架构。

工欲善其事必先利其器,所以本文针对 qiankun 的源码进行解读,在分享知识的同时也是帮助自己理解。

这是我们团队对微前端架构的最佳实践(见下图),如果有需求的话,可以在评论区留言,我们会考虑出一篇《微前端框架 qiankun 最佳实践》来帮助大家搭建一套微前端架构。

架构图

最后一件事

如果您已经看到这里了,希望您还是点个赞再走吧~

您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!

如果觉得本文对您有帮助,请帮忙在 github 上点亮 star 鼓励一下吧!

personal

查看原文

赞 113 收藏 60 评论 27

周小e丶 赞了文章 · 2020-01-19

HTTPS 详解一:附带最精美详尽的 HTTPS 原理图

HTTPS 详解系列:
HTTPS 详解一:附带最精美详尽的 HTTPS 原理图
HTTPS 详解二:SSL / TLS 工作原理和详细握手过程

前言

作为一个有追求的程序员,了解行业发展趋势和扩充自己的计算机知识储备都是很有必要的,特别是一些计算机基础方面的内容,就比如本篇文章要讲的计算机网络方面的知识。本文将为大家详细梳理一下 HTTPS 的实现原理。

近年来,随着用户和互联网企业安全意识的提高和 HTTPS 成本的下降,HTTPS 已经越来越普及。很多互联网巨头也在力推 HTTPS,比如谷歌的 Chrome 浏览器在访问 HTTP 网站时会在地址栏显示不安全的提醒,微信要求所有的小程序必须使用 HTTPS 传输协议,苹果也要求所有在 App Store 上架的应用必须采用 HTTPS ,国内外的大部分主流网站也都已迁移至 HTTPS,可见 HTTPS 全面取代 HTTP 只是时间问题。

说了这么多,究竟什么是 HTTPS?它与 HTTP 相比有什么优缺点?其底层实现原理又是怎样的呢?下面就为你一一解答,先来看一下 HTTP 的弊端吧。

1、HTTP 的最大弊端——不安全

HTTP 之所以被 HTTPS 取代,最大的原因就是不安全,至于为什么不安全,看了下面这张图就一目了然了。

HTTP数据传输过程.png

图1. HTTP数据传输过程

由图可见,HTTP 在传输数据的过程中,所有的数据都是明文传输,自然没有安全性可言,特别是一些敏感数据,比如用户密码和信用卡信息等,一旦被第三方获取,后果不堪设想。这里可能有人会说,我在前端页面对敏感数据进行加密不就行了,比如 MD5 加盐加密。这么想就太简单了。首先 MD5 并不是加密算法,其全称是 Message Digest Algorithm MD5,意为信息摘要算法,是一种不可逆的哈希算法,也就是说经过前端 MD5 处理过的数据在服务器端是无法复原的。这里以密码举例,前端把用户密码通过 MD5 进行处理,并把得到的哈希值发送给服务器,服务器由于无法复原密码,就会直接用这个哈希值处理用户请求。所以第三方在获取这个哈希值后,可以绕过前端登录页面直接访问服务器,造成安全问题。另外,MD5 算法本身的安全性也存在缺陷,这里就不展开谈了。

总之 MD5,SHA-1 之类的哈希算法并不能让 HTTP 变得更安全。要想让 HTTP 更安全,只能使用真正的加密算法,因为加密算法可以用密钥加密或还原数据,只要确保密钥不被第三方获取,那就能确保数据传输的安全了。而这正是 HTTPS 的解决方案,那下面就来了解一下加密算法吧。

2、加密算法

HTTPS 解决数据传输安全问题的方案就是使用加密算法,具体来说是混合加密算法,也就是对称加密和非对称加密的混合使用,这里有必要先了解一下这两种加密算法的区别和优缺点。

2.1 对称加密

对称加密,顾名思义就是加密和解密都是使用同一个密钥,常见的对称加密算法有 DES、3DES 和 AES 等,其优缺点如下:

  • 优点:算法公开、计算量小、加密速度快、加密效率高,适合加密比较大的数据。
  • 缺点:

    1. 交易双方需要使用相同的密钥,也就无法避免密钥的传输,而密钥在传输过程中无法保证不被截获,因此对称加密的安全性得不到保证。
    2. 每对用户每次使用对称加密算法时,都需要使用其他人不知道的惟一密钥,这会使得发收信双方所拥有的钥匙数量急剧增长,密钥管理成为双方的负担。对称加密算法在分布式网络系统上使用较为困难,主要是因为密钥管理困难,使用成本较高。

本文不对具体的加密算法做详细介绍,有兴趣的同学可以参考 对称加密算法详解,如果直接将对称加密算法用在 HTTP 中,会是下面的效果:

对称加密数据传输过程

图2. 对称加密数据传输过程

从图中可以看出,被加密的数据在传输过程中是无规则的乱码,即便被第三方截获,在没有密钥的情况下也无法解密数据,也就保证了数据的安全。但是有一个致命的问题,那就是既然双方要使用相同的密钥,那就必然要在传输数据之前先由一方把密钥传给另一方,那么在此过程中密钥就很有可能被截获,这样一来加密的数据也会被轻松解密。那如何确保密钥在传输过程中的安全呢?这就要用到非对称加密了。

2.2 非对称加密

非对称加密,顾名思义,就是加密和解密需要使用两个不同的密钥:公钥(public key)和私钥(private key)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公钥对外公开;得到该公钥的乙方使用公钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的私钥对加密后的信息进行解密。如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。常用的非对称加密算法是 RSA 算法,想详细了解的同学点这里:RSA 算法详解一RSA 算法详解二,其优缺点如下:

  • 优点:算法公开,加密和解密使用不同的钥匙,私钥不需要通过网络进行传输,安全性很高。
  • 缺点:计算量比较大,加密和解密速度相比对称加密慢很多。

由于非对称加密的强安全性,可以用它完美解决对称加密的密钥泄露问题,效果图如下:

非对称加密发送 KEY 的过程.png

图3. 客户端通过非对称加密把密钥 KEY 发送给服务器

在上述过程中,客户端在拿到服务器的公钥后,会生成一个随机码 (用 KEY 表示,这个 KEY 就是后续双方用于对称加密的密钥),然后客户端使用公钥把 KEY 加密后再发送给服务器,服务器使用私钥将其解密,这样双方就有了同一个密钥 KEY,然后双方再使用 KEY 进行对称加密交互数据。在非对称加密传输 KEY 的过程中,即便第三方获取了公钥和加密后的 KEY,在没有私钥的情况下也无法破解 KEY (私钥存在服务器,泄露风险极小),也就保证了接下来对称加密的数据安全。而上面这个流程图正是 HTTPS 的雏形,HTTPS 正好综合了这两种加密算法的优点,不仅保证了通信安全,还保证了数据传输效率。

3、HTTPS 原理详解

先看一下维基百科对 HTTPS 的定义

Hypertext Transfer Protocol Secure (HTTPS) is an extension of the Hypertext Transfer Protocol (HTTP). It is used for secure communication over a computer network, and is widely used on the Internet. In HTTPS, the communication protocol is encrypted using Transport Layer Security (TLS) or, formerly, its predecessor, Secure Sockets Layer (SSL). The protocol is therefore also often referred to as HTTP over TLS, or HTTP over SSL.

HTTPS (Hypertext Transfer Protocol Secure) 是基于 HTTP 的扩展,用于计算机网络的安全通信,已经在互联网得到广泛应用。在 HTTPS 中,原有的 HTTP 协议会得到 TLS (安全传输层协议) 或其前辈 SSL (安全套接层) 的加密。因此 HTTPS 也常指 HTTP over TLS 或 HTTP over SSL。

可见HTTPS 并非独立的通信协议,而是对 HTTP 的扩展,保证了通信安全,二者关系如下:

HTTP和HTTPS的关系.png

图4. HTTP和HTTPS的关系

也就是说 HTTPS = HTTP + SSL / TLS。

接下来就是最重要的 HTTPS 原理解析了,老规矩先上图。

HTTPS 加密、解密、验证及数据传输过程.png

图5. HTTPS 加密、解密、验证及数据传输过程

看上去眼花缭乱,不要怕,且听我细细道来。HTTPS 的整个通信过程可以分为两大阶段:证书验证和数据传输阶段,数据传输阶段又可以分为非对称加密和对称加密两个阶段。具体流程按图中的序号讲解。

1.客户端请求 HTTPS 网址,然后连接到 server 的 443 端口 (HTTPS 默认端口,类似于 HTTP 的80端口)。

2.采用 HTTPS 协议的服务器必须要有一套数字 CA (Certification Authority)证书,证书是需要申请的,并由专门的数字证书认证机构(CA)通过非常严格的审核之后颁发的电子证书 (当然了是要钱的,安全级别越高价格越贵)。颁发证书的同时会产生一个私钥和公钥。私钥由服务端自己保存,不可泄漏。公钥则是附带在证书的信息中,可以公开的。证书本身也附带一个证书电子签名,这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。

3.服务器响应客户端请求,将证书传递给客户端,证书包含公钥和大量其他信息,比如证书颁发机构信息,公司信息和证书有效期等。Chrome 浏览器点击地址栏的锁标志再点击证书就可以看到证书详细信息。

CA证书.png

图6. B站 CA 证书

4.客户端解析证书并对其进行验证。如果证书不是可信机构颁布,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。就像下面这样:
浏览器安全警告.png

图7. 浏览器安全警告

如果证书没有问题,客户端就会从服务器证书中取出服务器的公钥A。然后客户端还会生成一个随机码 KEY,并使用公钥A将其加密。

5.客户端把加密后的随机码 KEY 发送给服务器,作为后面对称加密的密钥。

6.服务器在收到随机码 KEY 之后会使用私钥B将其解密。经过以上这些步骤,客户端和服务器终于建立了安全连接,完美解决了对称加密的密钥泄露问题,接下来就可以用对称加密愉快地进行通信了。

7.服务器使用密钥 (随机码 KEY)对数据进行对称加密并发送给客户端,客户端使用相同的密钥 (随机码 KEY)解密数据。

8.双方使用对称加密愉快地传输所有数据。

好了,以上就是 HTTPS 的原理详解了,如此精美的图搭配这么详细的过程解析,你再搞不懂就说不过去了吧哈哈。

4、总结

再来总结一下 HTTPS 和 HTTP 的区别以及 HTTPS 的缺点吧:

HTTPS 和 HTTP 的区别:

  • 最最重要的区别就是安全性,HTTP 明文传输,不对数据进行加密安全性较差。HTTPS (HTTP + SSL / TLS)的数据传输过程是加密的,安全性较好。
  • 使用 HTTPS 协议需要申请 CA 证书,一般免费证书较少,因而需要一定费用。证书颁发机构如:Symantec、Comodo、DigiCert 和 GlobalSign 等。
  • HTTP 页面响应速度比 HTTPS 快,这个很好理解,由于加了一层安全层,建立连接的过程更复杂,也要交换更多的数据,难免影响速度。
  • 由于 HTTPS 是建构在 SSL / TLS 之上的 HTTP 协议,所以,要比 HTTP 更耗费服务器资源。
  • HTTPS 和 HTTP 使用的是完全不同的连接方式,用的端口也不一样,前者是 443,后者是 80。

HTTPS 的缺点:

  • 在相同网络环境中,HTTPS 相比 HTTP 无论是响应时间还是耗电量都有大幅度上升。
  • HTTPS 的安全是有范围的,在黑客攻击、服务器劫持等情况下几乎起不到作用。
  • 在现有的证书机制下,中间人攻击依然有可能发生。
  • HTTPS 需要更多的服务器资源,也会导致成本的升高。

另外,关于 SSL/TLS 握手的详细过程和相关重要概念,作者将在HTTPS 详解二:SSL / TLS 工作原理和详细握手过程中进行详细介绍。
好了,以上就是本篇文章的全部内容了,如有错误,欢迎指正。最后贴几篇参考文章

查看原文

赞 162 收藏 105 评论 18

周小e丶 回答了问题 · 2020-01-19

关于vue路由跳转的问题

你可以把参数放在hash路由后面

关注 6 回答 6

周小e丶 回答了问题 · 2020-01-19

解决小程序层级问题

弹出popup时将文本隐藏吧

关注 2 回答 2

周小e丶 回答了问题 · 2020-01-16

vue父组件开启缓存之后父组件里的子组件状态会缓存吗

会 子组件不会被重新 mount

关注 3 回答 1

周小e丶 回答了问题 · 2020-01-14

怎么在github上预览我的vue-cli4搭建的小项目?

可以用tarvis-ci 帮你自动上传到github page 当然前提是打包好的代码

关注 5 回答 4

周小e丶 回答了问题 · 2020-01-10

解决react的组件里,如何监听redux store值的改变呢?

在componentWillReceiveProps或者getDrivedStateFromProps里判断吧

关注 2 回答 2

周小e丶 回答了问题 · 2020-01-06

typeScript 装饰器注入类

@Component包装过的应该是一个对象 无法使用new操作符的 但是你可以试试在导出这个组件后再用Instance包装,vue-loader会帮你处理的

关注 2 回答 1

周小e丶 回答了问题 · 2020-01-06

Vue.nextTick

组件更新的逻辑也是放在微任务里的,这时候你执行了nextTick其实微任务队列里有两个微任务,当然是顺序执行了

关注 3 回答 2

认证与成就

  • 获得 47 次点赞
  • 获得 8 枚徽章 获得 1 枚金徽章, 获得 1 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-11-10
个人主页被 975 人浏览