mkcert是什么?

因为最近在做公司的一些移动端的web项目,经常需要在手机上进行真机调试,很多场景下都需要使用到 https,而且在开发阶段为了尽可能与线上的环境保持一致,所以打算直接在本地 使用https进行开发调试

然后发现了一款工具,它用于在本地搭建受信证书环境。试用了一下,非常便捷。

首次运行时,它会生成一个本地CA,即本地公证机构根证书,然后把此CA添加到系统受信列表。最后,由此CA颁发(签名)新的下级证书给各网站使用。

安装mkcert

$ brew install mkcert

Created a new local CA at "/Users/ChrisChen/Library/Application Support/mkcert" ?
The local CA is now installed in the system trust store! ⚡️
The local CA is now installed in the Firefox trust store (requires restart)! ?

生成本地 Root CA证书

$ mkcert -install

``/Users/chrischen/Library/Application Support/mkcert` 生成的CA证书的路径

生成下级证书

$ mkcert localhost 127.0.0.1

如果要生成泛域名证书也可以使用 mkcert "*.wps.cn" 类似的域名(要注意使用双引号才可以)

配置Nginx使用证书

server {
listen 443 ssl;
server_name localhost;

ssl_certificate /Users/chrischen/cert/local/localhost+1.pem;
ssl_certificate_key /Users/chrischen/cert/local/localhost+1-key.pem;

ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

location / {

}
}

重启nginx,访问 https://localhost

sudo nginx -s reload

最小化实现

最小化实现 ,inserted, 和bind的区别可以取官网看,不同的使用场景

Vue.directive('collect', {
inserted (el,bindbing) {
// 执行自定义指令要做的操作
}
});
  • collect 表示自定义指令的名称 bindbing.name
  • close_vip 这部分表示自定义指令的参数 bindbing.arg
  • .click .stop 表示修饰符 binding.modifiers.click, binding.modifiers.stop
  • {title: "关闭弹窗"} 这部分表示 自定义指令的值. binding.value

Vue自定义指令设计

Vue自定义指令其实就是html 的属性,通过给 Vue自定义指令也是 html属性的一种语法糖,所以自定义其实就是通过 html 标签属性的形式,通过一系列规则的封装成的一些特定功能,下面看看如何实现

实现自定义指令需要弄明白一下几个问题

  1. 自定义指令使用的使用的多种方法,name,修饰符等等
  2. 自定义指令插件化开发,如何解耦合?

自定义指令编写

import { dw } from '@/utils';

export class Collect {

inserted(el, binding) {
let { value, arg, modifiers } = binding;
let { click, show, prevent, stop } = modifiers; // 自定义修饰符

if (show) {
dw.onEvent(arg, value);
}

if (click) {
el.addEventListener('click', event => {
stop && event.stopPropagation();
prevent && event.preventDefault();
dw.onEvent(arg, value);
}, false);
}
}

bind() {

}

unbind(el, bingding) {
const { click = false } = bingding.modifiers;
const { value } = bingding;
if (click) {
el.removeEventListener('click',() => {
COLLECT(value);
},false);
}
}

// 导出的模块一定要有一个静态的 install的方法, Vue.use(module) 会执行到
static install(Vue) {
console.log('执行安装');
Vue.directive('vascollect', new Collect()); // 这里也可以写成对象的形式
}
}

使用Vue.use安装

使用Vue.use的形式使用,使其Vue项目更加规范化

main.js

import Vue from 'vue'
import Collect from '@/directives/collect'; // 自定义指令
Vue.use(Collect) // 注册自定义指令

使用

通过collect点击时上报 数据埋点事件

<button v-collect:close_vip.click.stop="{ title: "关闭弹窗"} ">Submit</button>

Cookie是在服务端返回数据的时候通过Set-Cookie这个header 设置到浏览器里面的一个内容,浏览器保存之后,在下一次同域的请求中,就会带上Cookie,这样就可以在一个用户的会话过程中,可以返回对应的用户的数据

Cookie有几个特点

  • 客户端存储: Cookie 数据存储在客户端浏览器中,有大小限制(一般最大为4KB)。
  • 自动发送: 客户端向同一个服务器再次发起请求时,会自动携带该网站的所有Cookie。
  • 跨会话持久性: Cookie可以设置过期时间,即使关闭浏览器也可以保持状态信息。
  • 易于篡改: 存储在客户端,容易被用户或第三方篡改,安全性较低。
  • 性能考虑: 每次HTTP请求都会携带Cookie,如果Cookie过多或过大,会增加请求的负担。

使用场景

跟踪用户行为,如广告定制。
保存用户偏好设置,如主题、语言等。
实现无状态的HTTP请求的某种程度上的“状态化”。

Cookie的属性

  • 通过max-age 和 expires 设置过期时间

  • Secure 只在 https 请求时发送

  • 设置 HttpOnly 后 无法通过 document.cookie 访问

无法通过Javascript 去访问到,这样做是为了安全性,预防 CSRF 攻击 ( 跨站请求伪造 ) ,通过使用脚本注入,或者外部链接跳转等方式, 获取用户的登录信息token 从而达到入侵的目的,HttpOnly 禁止Javascript访问 cookie 可以很好的预防这类攻击

Nginx 设置 Cookie

下面设置代表了看,设置了多个Cookie

server {
add_header Set-Cookie "id=1;max-age=3";
add_header Set-Cookie "name=chris";
add_header Set-Cookie "age=18;HttpOnly";
}
  1. 第一条 id=123的这条Cookie 过期时间为 3秒后
  2. 第二条cookie 则没有设置过期时间,默认为浏览器关闭,会话结束(会话Cookie)
  3. 第三条cookie 设置了HttpOnly 字段,在浏览器的Application 中是可以看到的,但是用document.cookie 访问不了

tips: 可以通过给主域名设置Cookie 然后子域名访问,达到一个跨域访问Cookie的效果

Session

Session(会话)和Cookie都是用来保存客户端与服务器之间交互状态信息的技术,它们在Web开发中广泛应用于用户身份验证、状态保持等方面。

Session 的特点

  • 服务器端存储: Session 数据存储在服务器端,客户端无法直接访问,这提供了更好的安全性。
  • 唯一标识: 每个用户的Session都有一个唯一的Session ID,通常在Cookie中存储或通过URL重写传递。
  • 有状态: Session可以跟踪用户的状态,如登录状态、购物车内容等。
  • 资源消耗: 比起Cookie,Session会在服务器上占用内存资源,当大量用户并发时可能会影响服务器性能。
  • 失效控制: 服务器可以控制Session的失效时间,可以在用户关闭浏览器后或经过特定时间自动失效。

使用场景

  • 用户身份验证和授权。
  • 保存用户的购物车信息。
  • 跟踪用户的操作路径和行为。

Session与Cookie的区别

  • 存储位置: Session存储在服务器,Cookie存储在客户端。
  • 安全性: Session相对Cookie更安全。
  • 存储大小: Session大小由服务器内存决定,Cookie大小有限制, 通常为4KB。
  • 生命周期: Session可以由服务器管理生命周期,Cookie则由客户端管理。

前言

找工作第三天了,线上线下面了有不少了 ,其中有部分的问题回答的不是很nice,用这篇文章来记录一下,再去剖析一番

HTML

<script> ,<script async> <script defer> 的区别分别是什么

在HTML中类似 asyncdeferchecked 等等 这种叫做 布尔属性,布尔属性的存在意味着 true 值,布尔属性的缺失意味着 false 值。

当时不太了解 defer这个属性,现在做个对比

  • <script>不管是内部代码,还是 src 加载的远程代码,都会阻塞 DOM 的解析
  • <script async> (多个请求加载时顺序不能保证一致)
    • 内部代码:会阻塞 DOM 解析
    • 远程代码:请求时不会阻塞, 执行代码时会阻塞
  • <script defer> 无论何时都不阻塞代码,触发 DOMContentLoaded 事件前执行
script标签 JS执行顺序 是否阻塞解析 DOM
<script> HTML 中的顺序 阻塞
<script async> 网络请求返回顺序 有代码执行阻塞,没代码执行不阻塞
<script defer> 在HTML 中的顺序 不阻塞

CSS

哪些属性可以触发BFC ?

当时说了一些没太说清楚

列下几个常用的

  • 浮动元素: float 不为 none
  • 绝对定位元素: positionabsolutefixed
  • 行内快元素:display 属性为 inline-block
  • 表格单元格为 table-cell 值 (table的默认值)

rem 如何计算 ?

移动端没追问太多,移动端项目少

rem 是一个相对单位

layout-viewport (布局视口) / 设计稿宽度

document.body.clientWidth / 750 * 基数 = Html fontSize

(function () {
var a = document.documentElement.clientWidth || document.body.clientWidth;
if (a > 460) { // 最大460宽度
a = 460;
} else {
if (a < 320) { // 最小320宽度
a = 320;
}
}
document.documentElement.style.fontSize = (a / 750 * 100) + 'px';
})();

不过移动端适配都有方案处理好了,postcss-px2rem等

Javascript

Javascript实现帧动画 ?

当时一时没反应过来😢,后来才想起,不过这个API用的比较少

window.requestAnimationFrame(callback) 它接受一个回调函数,该方法会在浏览器在下次重绘之前调用指定的回调函数更新动画

该回调函数执行次数通常是每秒60次,但是在大多数浏览器中进行了优化,运行在后台标签页中或者是 <iframe/> 中会被暂停调用

详细参考 👉🏻 window.requestAnimationFrame - Web API 接口参考 | MDN (mozilla.org)

try 中写异步代码报错了,catch 中是否能捕获到? 为什么?

​ 不能,因为JavaScript事件循环机制导致的

try {
setTimeout(() => {
throw new Error('1')
})
} catch (e) {
console.log(e)
}

在上面这段代码中,并不能捕获到错误,可以复制在浏览器控制台中去试试。

其原因就是在JavaScript的执行过程中,执行到第二行的时候(setTimeout这个位置),会把setTimeout中的回掉函数放入 任务队列,等到同步代码执行完成后,才回去执行队列中的任务,所以在 try 代码块执行完后,程序此时还没有报错,于是就不会走到 catch 里面,所以setTimeout 其实是最后才执行的,所以不会被捕获到,如下图 👎🏻

Promise

这个后面单独写一篇博客,这个问的太多了

generator 的 迭代器中 next方法的参数怎么用?

next 的参数会在generator 函数中用作 yield 值返回的结果(只在函数体里有效,不会影响到next.value的值)

使用值调用next。 注意,第一次调用没有记录任何内容,因为生成器最初没有产生任何结果。

function* generator() {
while(true) {
var value = yield null;
console.log(value);
}
}

let g = generator();
g.next(1);
// "{ value: null, done: false }"

g.next(2);
// 2
// "{ value: null, done: false }"

解释一下JavaScript的同步异步,宏任务和微任务 ?

HTTP

http哪些头可以设置强缓存和协商缓存?

强缓存 ( 不会发起请求 )

  • Cache-Control
  • Expires (过期时间)

协商缓存 (发起请求校验,如果命中缓存则返回304)

  • Last-Modified / If-Modified-Since (都是GMT格式的时间字符串,如何客户端和服务端时间不统一,可能会存在问题)
  • Etag / If-None-Match (值由服务器生成唯一标识,如果文件有变化,这个值也会改变)

Etag 就是为了解决,Last-Modified 可能会存在时间不一致的另一种策略。

优先级: Cache-Control > Expires > Etag > Last-Modified

预防CSRF (跨站请求伪造)攻击,设置后的cookie字段,则通过JavaScript 访问不到(document.cookie)也就操作不了 从根本上解决问题😂

但是可以通过浏览器调试工具Application 这一栏看到

CORS中预检请求中(Prefilight request)的作用是什么 ?

个人理解:在发送一些不安全的请求之前,浏览器会自动的发送一个options请求,也是就 CORS 预检请求,从而获取服务端是否允许该跨域的请求,以及是否需要携带身份凭证,最终决定客户端是否能否跨域访问资源,来保障网站的安全

那么什么样的情况下不会触发呢?MDN把这些归类为简单请求(不会触发CORS 预检请求 )

  • 使用下列方法之一:
  • 除了被用户代理自动设置的首部字段(例如 ConnectionUser-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合,该集合为:
  • Content-Type的值仅限于下列三者之一:(也就是html原生 form表单支持的数据类型)
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
  • 请求中没有使用 ReadableStream

Vue

computed 和 watch的区别 ?

computed 只要有依赖的项更新了,就会重新计算,内部还有做缓存,去做对比,如果值上一次计算的值,和更新的值没有发生变化,也不会更新到页面上(适合在模版里面使用,减少DOM更新)

watch 只要是被监听的值有变化,就会立刻执行 (适合做数据的监听的逻辑处理)

React

class中哪些操作可以触发到 render ?

  • state更新
  • props更新
  • class的父组件更新,子组件也会更新
  • setState() 啥也不设置也会更新(只要调用了setState就会更新)

setState 是同步还是异步 ? 内部机制 ?

是异步的,执行setState 后不会立刻更新,而是把要更新的操作放入一个队列中

React 的事件操作 和 原生事件有什么区别 ? 合成事件 ?

HOC 高阶组件是什么 ?作用是什么 ?

  • 高阶组件函数接收一个组件 作为参数
  • 高阶组件必须输出一个新的组件

更好的做代码分层复用,解决耦合等问题

React中不用第三方库,如何组件通信? (不用Mobx,Redux等)

  1. Props + 事件触发 (父子组件通信)
  2. 使用 ref 获取到对应React实例 直接操作其他组件的方法,属性,进行通信
  3. Provide ( Mobx 也是使用从根组件开始注入的方式,实现状态下发 )
  4. Context (不好维护)

tips: 项目大起来还是用Redux 和 Mobx 库去管理比较好

Typescript

type 和 interface 的区别 ?

说了一些,没说全,补充下

  • interface 可以描述class,type 不可以
  • interface 继承方式是 extends ,type 是用联合类型 &
  • interface 只能描述对象和 class ,type可以描述任意数据类型 (数据类型可以,class不行)
  • interface 不能使用 in 关键字进行遍历,type 可以使用 [key in Types] 进行对象的key约束

keyof 如何使用 ?

webpack

说下webpack 的构建流程 ?

  1. 合并配置文件和参数,生成最终的配置文件 (如何有多个的话,例如:shell 脚本中的参数,Vue.config.js 等等)
  2. 从入口文件开始( entry字段),递归加载出所有依赖文件,生成依赖树
  3. 根据loader 配置的规则转化对应的文件
  4. plugin …
  5. 代码提取,公共模块,代码分离,等等
  6. 生成文件写入 output 字段配置的文件夹

数据结构

树节点操作的优化 ?

堆栈是什么 ?

说下深度优先和广度遍历优先 ?

whlie 循环遍历做树结构的优化 ?

笔试题

手写一个 防抖函数

当时直接写了思路,觉得在纸上写代码有点难受

/**
* 防抖函数
* @param fn {function}
* @param delay {number}
* @return {(function(...[*]): void)|*}
*/
function debounce(fn, delay) {
let timer = null;
// 这里使用 普通函数,this可以执行到本身
return function (...args) {
// 如果有 timer 说明已经已经触发倒计时了,则清掉倒计时,重新定时
if (timer) {
clearTimeout(timer);
}

// 倒计时开始,
timer = setTimeout(() => {
fn.call(this, ...args);
timer = null;
}, delay);
};
}

还有算法的笔试题

刚开始没看懂,后来他和解释了一波题意,然后马上就讲出思路来了,面试官表示比较满意,后来面试评价说我,反应灵活。。。😂,好吧

性能优化篇

说一下你知道的web性能指标

​ 当时脑子抽了,说了一些,没太全,实际上就是问的,从浏览器输入URL到页面渲染这个过程中的一些指标啊。。。😂

  • 网络层
    • 重定向时间 (浏览器先检查本地是否有标记过,该URL 有被永久的重定向 httpCode 301)
    • DNS解析时间 (是域名的话,会递归的去解析拿到IP地址)
    • TCP 完成握手时间 (拿到IP后,开始于服务器建立TCP连接,中间会有三次握手过程)
    • HTTP请求响应时间 (三次握手成功后,HTTP开始发送请求,然后响应数据)
  • 应用层
    • DOM解析时间 (获取到HTML后,浏览器开始解析)
    • Script 脚本加载时间( 如果DOM中包含了script,则会执行脚本)
    • onload 时间 (整个页面加载完成后,包括DOM 和 CSS ,Javascript 全局加载完成,比如JS阻塞,Image 的src 图片下载完成才触发onload事件,亲测)
    • pageload (页面完全加载是时间)

PNG,JPG,GIF,WEBP等图片格式有什么不一样,分别用于哪些场景?

前言

一提到 Object.defineProperty 搞前端的应该都会想起数据响应式,Vue2 数据响应式主要原理是通过的Object.defineProperty 去实现的,Vue3是则使用浏览器原生的 Proxy 方法,同样是实现数据相应是,这两组API有什么不一样的吗?通过这篇文章给大家分析下👇🏻

Object.defineProperty VS Proxy

Object.defineProperty 方法会在一个对象上定义一个新属性,或者修改一个对象的现有属性并返回对象

它提供了一些 约束对象操作的 属性 以及 set,get 方法,因此Object.defineProperty 监听的是对象中的属性,而和Proxy不同的是 new Proxy 时传入一个对象,它内部直接监听了整个对象的操作行为,因此可以得出一下结论:

Object.defineProperty 监听的是对象的属性

Proxy 监听的是整个对象

由于Object.defineProperty 监听的是属性,当递归一个深层结构的对象的时候,数组里层的对象就不能遍历到了(因为Object.defineProperty方法不适用于数组) ,而Proxy能代理数组,所以从根源生解决了问题Object.defineProperty 代理的目标是对象上的属性,属性新增和删除也就监听不到了(handlers 中只有get,set方法)

以下方法在Object.defineProperty 中不能被监听到

pushpopshiftunshiftsortreversesplice

Object.defineProperty的缺陷,由上得出结论

Object.defineProperty 不能监听数组的增删改操作

Object.defineProperty 无法监听属性的新增和删除

Object.defineProperty 是对象的方法,因此只要有Javascript的地方它基本都能支持(IE9以下就不支持了)

而 Proxy 直接不支持 IE …

Proxy作为新标准将受到浏览器厂商重点持续的性能优化

在Vue3中的变化

  1. Vue2数据驱动是使用的Object.defineProperty 去递归监听对象的,Vue3则是用的 Proxy 代理

  2. Vue2是一次性递归完data对象,Vue3 是用Proxy 是在调用属性时并且值时object时才递归

    源码 vue-next/packages/reactivity/src/baseHandlers.ts 127行

前言

今天写下如何避免 XSS 跨站脚本攻击,以及CSP的使用

内容安全策略(CSP)是什么?

内容安全策略是一个对Web网站的安全层,用于检测并削弱某些特定类型的攻击,数据注入攻击等,无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的攻击手段。

简单理解,CSP其实就是白名单制度,开发者明确告诉客户端,哪些资源可以被允许加载执行

CSP 大大增强了网页的安全性。攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的可信主机。

CSP 被设计成完全向后兼容,老版本的CSP 字段为 X-Content-Security-Policy

默认为网页内容使用标准的同源策略。如果网站不提供 CSP 头部,浏览器也使用标准的同源策略。

下面说下 CSP 如何开启👇🏻

限制参数

下面选项限制各类资源的加载,主要分为以下两种

  • default-scr 限制全局,所有根据链接加载的东西都会被限制(优先级最高)

    Content-Security-Policy: default-src 'self'		#设置各项的默认值
  • 制定资源类型

    • content-src 所有请求资源限制 ( HTTP ,WebSocket , EventSource 等)
    • script-src 外部脚本
    • img-src 图片资源
    • style-src 外部CSS
    • font-src 字体文件
    • worker-src worker脚本
    • frame-src 嵌入的外部资源(比如<frame>、<iframe>、<embed>和<applet>
    • manifest-src Manifest 文件 (WebApp)
  • 其他限制

    限制了一些其他的安全规范,也放在了CSP里面

    • block-all-mixed-content HTTPS 网页不得加载 HTTP 资源(浏览器已经默认开启)
    • upgrade-insecure-requests 自动将网页上所有加载外部资源的 HTTP 链接换成 HTTPS 协议
    • plugin-types 限制可以使用的插件格式
    • sandbox 浏览器行为的限制,比如不能有弹出窗口等

    上面资源类型一下比较通用的, 其中还包含 font-src,frame-src,media-src等等,只要是可以通过外链形式加载的几乎资源都可以被限制

参数选项

每个选项可以设置一下这几项

  • 主机名 chrisorz.cn https://chrisorz.cn:443 (指定端口)

  • 路径 blog.chrisorz.cn/api/

  • 通配符 *.chrisorz.cn *://chrisorz.cn:* ( 所有协议,所有端口)

  • 协议名 http: https: file: stp:

  • 关键字

    • "none" 禁止加载任何外部资源 需要引号
    • "self" 当前域名,需要引号

通过参数和参数选项就可以实现整条完整的规则了

语法规则

每个规则可以指定一个或者多个选限,如果有多个则用 空格分开

每条规则用;结尾 例如:

server {
Content-Security-Policy: "img-src cdn.chrischen.top cdn.chrisorz.cn;
script-src 'self';
style-src 'self';
";
}

上面代码中,CSP做了如下配置

  • 图片:只能信任 cdn.chrischen.top cdn.chrisorz.cn这两个域名加载的资源
  • 脚本:只信任本域名下的脚本
  • 样式:只信任本域名下的样式

开启CSP的两种方式

  1. 通过配置 HTTP 头信息的 Content-Security-Policy 字段 (服务器)
server {
# 不能使用行内script只能从http或者https中使用外链
add_header "Content-Security-Policy" "default-src http: https:";
}

写了个Demo,nginx上配置了 add_header "Content-Security-Policy" "default-src http: https:"后,可以看到内嵌式的script 代码已经不生效了,网页上没有任何的改变

同样CSS也是, 行内样式和内嵌样式都被拦截掉了

  1. <meta> 标签设置
<meta http-equiv="content-security-policy" content="style-src http: https:" charset="UTF-8">

<meta/>属性设置后也是一样的效果,这里就不贴图了

这里只是做一个演示效果,实际生产环境中一般不会这样设置 Content-Security-Polity ,生产环境中一般会有多域名,根据实际业务去进行可视化的配置,例如:

  • 静态资源上CDN
  • 注入百度统计,Google统计等脚本
  • 后端多域名的API(微服务更甚之,可能一个项目调7,8个域名,不过可以通过通配符去解决)
  • 等等

总结

CSP 可以避免 XSS 攻击

CSP 的设置分为两种

  • 客户端 meta 标签
  • 服务端 响应头

无论是客户端,还是服务端设置,最终生效的CSP 安全策略是权限范围最小的那个

前言

Vue3已经出来几个月了,API几天就上手了,想学习更深层次的东西还是的看看源码

从Vue创建实例开始

Vue3创建实例是用的 Vue.createApp 方法,先去源码中找到 createApp,看看它做了如何处理

  1. packages/vue/src/index.ts Vue包入口文件 ->
  2. packages/runtime-dom/src/index.ts 57行

Vue.createApp做了啥?

创建app实例

通过 createRenderer 创建实例 调用createApp 方法生成 app 实例

定义 app.mount 方法

在mount方法中,又做了以下几件事情

  1. 通过app.mount传入的selector,获取到DOM节点,用作Vue挂载的根节点

  2. 获取模版,如果createApp有传入 render,template,就使用它们作为模版,否则就使用根节点的innerHTML作为模版

  3. 调用mount 如果没有挂在过,开始挂载

    在有在第一次渲染会走mount,以后更新都是走的patch对比 的逻辑

  4. 挂载成功后返回一个代理对象,里面包含了 data 和 setup方法return的值,如果两个方法的return的值有冲突,则会优先使用setup中的值

可以看到定义两个一样的 title 属性已经被覆盖了,而 hd 只在data中定义 setup中没有定义就不存在覆盖的问题,所以对于模版属性 setup的优先级是要高于data

Vue3数据更新操作

源码查找 packages/reactivity/src/reactive.ts 88行

  1. 定义reactive方法(用户调用执行)
  2. 使用new Proxy创建代理对象 createReactiveObject, get,set在
  3. 在get中收集依赖

未完待续。。。

前言

这应该算是一个高频面试题了吧,来来输出一下… 什么是http,为何要使用它之类的就不讲了..

​ 尽可能写详细点吧,不然一个表格就搞定了


区别

语义不通

最直观的就是语义不同了

说到语义,http客户端在当时设计这些API的时候(我没去参与设计过,我猜测的哈哈哈[狗头]),也是有一套规范的,当然这个规范不是强约束,你不按照这个语言规则来,你的程序也能跑

http不同方法代表的语义,分别对应来CURD的4个操作 RESUful API

  • GET 在服务器检索某个资源
  • POST 在服务器创建资源
  • PUT 在服务器更改资源状态或对其进行更新
  • DELETE 在服务器删除某个资源

RESUful API 风格就是遵循的http的语义化去设计的

参数传递形式不一样

GET

GET请求的参数是拼接在URL后面的,通过 = &符号进行分割 url?key1=value2&key2=value2

例如: http://chrischen.top/api?id=10&name=chris

服务端获取参数需要从 request中的 query 字段去获取

由于get参数都是明文显示,所以例如身份证号码,用户名密码,这类敏感信息是不应该用get去请求的

POST

post请求的参数则会包含在请求体中,通常服务端用request中的 body字段去获取的,

而且发送post请求传递参数的内容格式(content-type)也是不一样的,既然聊到content-type,下面就多写点

在响应中: Content-Type标头告诉客户端实际返回的内容的内容类型。浏览器会在某些情况下进行MIME查找,并不一定遵循此标题的值; 为了防止这种行为,可以将标题 [X-Content-Type-Options] 设置为nosniff

在请求中: 在 post 或 put 请求,客户端告诉服务器实际发送的数据类型 通过content-type去设置发送的数据类型

常用的POST/PUT请求数据类型

更多 请求类型响应类型具体可参考 👉🏻 MIME 类型 - HTTP | MDN (mozilla.org)

// {id:123,name:'chris'}
Content-Type: application/json;

// id=123&name=chris
Content-Type: application/x-www-form-urlencoded;

幂等

​ GET请求是幂等的,POST请求不幂等

什么是幂等?

一个HTTP方法是幂等的,指的是同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)

下面几种方法是幂等

  • GET
  • HEAD
  • PUT
  • DELETE

未完待续。。。

背景

最近在找工作,然后看到boss直聘的论坛上,一个前端招聘者在抱怨招不到合适的人,面了一大堆人,工作2-3年的,用JavaScript实现一个链表都寥寥无几,都没有聊下去的欲望….

所以在这了准备在写这篇博客,表明我实现过哈哈哈😂,抖个机灵,毕竟自己之前也是面试官,喜欢出一些开放型的题目去考察面试者,其实我看来这位面试官考察的这个问题,除了考察这个问题本身,更多的是看你的分析问题的能力,和抽象思维

下面就来分析下如何用JavaScript实现一个链表


链表是什么?

线性表的链式存储表示的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素与其直接后继数据元素 之间的逻辑关系

上面是百度百科的解释,我画了一张图让大家更便于理解

如果用生活中的例子代替火车就是最抽象的例子

每一个节点包含自己外,还包含一个指针,这个指针可以指向到下一个节点,形成一种,以此类推,形成的一个链式结构,就叫链表

链表还分为

  • 单向链表

    每个节点包含一个指针,指向下一个节点

  • 双向链表

    每一个节点有两个指针,一个指向上一个节点,一个指向到下一个节点

  • 循环链表

    把单向链表的最后一个指针从Null改成指向链表头,就形成了循环链表

知道链表是什么后,下面来分析一下如何用JavaScript实现一个单链表

JavaScript如何实现链表?

首先,链表是基于一个个节点来组装实现的,首先我们需要创建一个辅助类来实现

其次,由于JavaScript没有指针的概念,我们在实现节点的时候需要用对象的属性去模拟指针

然后,我们要知道链表都有哪些API,如何去操作链表,然后再去逐步的实现这些方法

最后整理出来需要哪些功能

  1. 实现辅助类 Node,模拟链表中的节点
    1. 节点属性的:本书节点
    2. 节点的属性:next指向
  2. 链表类的API
    1. getHeader 获取链表头
    2. append 向链表中追加元素
    3. insert 向链表中的某一个位置插入元素
    4. removeAt 删除链表中的某一个元素
    5. indexOf 查询链表中是否有这个元素

捋清楚这个整体思路后,下面按照这个大纲来实现

Node辅助类实现

class Node {
constructor(item) {
this.item = item; // 节点内容
this.next = null; // 指向下一个节点的指针
}
}

链表类的实现

class LinkedList {
head = null;
length = 0
}

append

class ListedList {

head = null; // 链表头
length = 0 // 链表长度

/**
* 追加节点
* @param item { Node } 节点
*/
append(item) {
let node = new Node(item); // 创建一个节点
if (this.head) {
// 有head头则遍历到获取到最后一个节点,把next指向新追加的节点
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = node;
} else {
// 无head头,则直接把head头替换为新的节点
this.head = node;
}
// 追加成功后,链表的长度加一
this.length++;
}


}

insert

class LinkedList {
/**
* 向链表中的某一个位置插入元素
* @param position { Number }
* @param item { Node }
*/
insert(position, item) {
// 边界判断 , 防止 position 越界
if (position > -1 && position < this.length) {
let node = new Node(item); // 创建一个节点

// 插入的位置是head头前面
if (position === 0) {
let current = this.head;
this.head = node; // 把head替换为新节点
node.next = current; // 把新节点的next指向原来的head节点
} else {
/**
* 其他位置,则需要遍历链表获取到对应到插入的位置
* 思路: 1. 通过positon获取到上一个节点和下一个节点
* 2. 把上一个节点的next指向到新节点
* 3. 把新节点的next指向到下一个节点
*/
let index = 0;
let previous = null;
let current = this.head;

while (index < position) {
previous = current;
current = current.next;
index++;
}
previous.next = node;
node.next = current;
}
this.length++; // 插入成功后,链表的长度加1
} else {
console.warn('position的值越界了');
}
}
}

removeAt

/**
* 通过位置删除节点
* @param position
*/
class LinkedList {

removeAt(position) {
/**
* 思路:
* 1. 通过position 遍历链表和 找到上一个元素和当前元素
* 2. 把上一个元素的next指向到当前元素的next (也就是当前元素的下一个元素)
*/
// postion越界判断,保证传入的位置是正确的
if (position > -1 && position < this.length) {
if (position === 0) {
let current = this.head;
this.head = current.next;
} else {
let { current, previous } = this.#getPrevAndCurrentNode(position);
previous.next = current.next;
}
} else {
console.warn('position的值越界了');
}
this.length--; // remove掉后,链表长度减1
}
}

indexOf


/**
* 通过节点找到 position
* @param item { Node }
* @return { number }
*/
class LinkedList {
indexOf(item) {
let index = 0;
let current = this.head;
while (current.next) {
current = current.next;
if (current === item) {
break;
} else {
index++;
}
}
return index
}
}

getHead

class LinkedList {
getHead() {
return this.head;
}
}

代码优化 && 完成例子

从上面我们观察到,insert,和removeAt这两个方法都用到了position 去获取上一个元素和当前元素,那么我们可以把这方法抽离出来进行一个代码结构的优化,下面给大家展示一下完整的例子

/**
* Created by WebStorm.
* User: chrischen
* Date: 2021/3/18
* Time: 11:50 下午
*/

/**
* 节点类
*/
class Node {
/**
* @param item { Node }
*/
constructor(item) {
this.item = item;
this.next = null;
}
}

/**
* 链表类
*/
export class LinkedList {

head = null;
length = 0;

/**
* 追加节点
* @param item { Node } 节点
*/
append(item) {
let node = new Node(item); // 创建一个节点

if (this.head) {
// 有head头则遍历到获取到最后一个节点,把next指向新追加的节点
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = node;

} else {
// 无head头,则直接把head头替换为新的节点
this.head = node;
}

// 追加成功后,链表的长度加1
this.length++;

}

/**
* 向链表中的某一个位置插入 节点
* @param position { number }
* @param item { Node }
*/
insert(position, item) {
// 边界判断 , 防止 position 越界
if (position > -1 && position < this.length) {
let node = new Node(item); // 创建一个节点

// 插入的位置是head头前面
if (position === 0) {
let current = this.head;
this.head = node; // 把head替换为新节点
node.next = current; // 把新节点的next指向原来的head节点
} else { // 其他位置,则需要遍历链表获取到对应到插入的位置
/**
* 思路:
* 1. 通过positon获取到上一个节点和当前节点
* 2. 把上一个节点的next指向到新节点
* 3. 把新节点的next指向到当前节点 (插入后就是新节点的下一个节点了)
*/
let { current, previous } = this.#getPrevAndCurrentNode(position);
let node = new Node(item);
previous.next = node;
node.next = current;
}
this.length++; // 插入成功后,链表的长度加1
} else {
console.warn('position的值越界了');
}

}

/**
* 通过positon获取上一个节点和当前节点
* @param position { number }
* @return { { current, previous } | undefined }
*/
#getPrevAndCurrentNode(position) { // #表示私有变量esnext新标准
let index = 0;
let current;
let previous;

if (!this.length > 0) {
console.warn('当前为空');
return undefined;
}

if (position > -1 && position < this.length) {
let current = 0;
if (position === 0) {
previous = null;
current = this.head;
} else {
let current = this.head;
while (index < position) {
previous = current;
current = current.next;
}
}
} else {
console.error(`position的值 "${ position }" 不符合规范`);
}
return { current, previous };
}

/**
* 通过位置删除节点
* @param position
*/
removeAt(position) {
/**
* 思路:
* 1. 通过position 遍历链表和 找到上一个节点和当前节点
* 2. 把上一个节点的next指向到当前节点的next (也就是当前节点的下一个节点)
*/
// postion越界判断,保证传入的位置是正确的
if (position > -1 && position < this.length) {
if (position === 0) {
let current = this.head;
this.head = current.next;
} else {
let { current, previous } = this.#getPrevAndCurrentNode(position);
previous.next = current.next;
}
} else {
console.warn('position的值越界了');
}
this.length--; // remove掉后,链表长度减1
}

/**
* 通过节点找到 position
* @param item { Node }
* @return { number }
*/
indexOf(item) {
let index = 0;
let current = this.head;
while (current.next) {
current = current.next;
if (current === item) {
break;
} else {
index++;
}
}
return index;
}

/**
* 获取链表头
* @return { Node | null }
*/
getHead() {
return this.head;
}
}

不到200行代码,就这样一个单向链表就实现了😂

最后

最后说下我个人的理解,其实学习数据结构出了数据结构本身,更多的是学习一种解决问题的思维,能够举一反三,你知道的东西多了都熟悉了,才有能力去创新。

后面打算写一些非技术型的文章,输出下别的内容哈哈🤪

前言

HTTP是web编程最为基础的了知识了,有 互联网基石 的称号可想有多重要, 准备重新整理输出一篇博客,通过这种方式去加深印象


HTTP/0.9

在最早的时候呢,第一个定稿http协议是http0.9,在这个版本的http协议非常简单

只有一个GET命令

没有header等相关描述数据的信息

服务器发送完毕,就关闭了TCP连接

短连接,一个TCP只能发送一个HTTP请求 

因此,为了解决这些特性带来的问题, 在后续的http1.1 就进行了升级,在同一个TCP连接中可以发送 多个请求,更快的提升了http的传输效率和服务性能

HTTP/1.0

新增了很多命令

POST,PUT,HEADER等

新增了status code等 header头相关信息

发送和请求数据的一些描述

新增了多字符集,多部分发送,权限,缓存等

有了这些东西后,能够更好的有利于的去实现web服务

HTTP/1.1 (目前应用最多最广泛)

新增持久链接 keep-alive( 默认开启 )

在一个TCP连接中可以发送多个HTTP请求, 一个HTTP请求肯定是在在某个TCP连接中去发送的

keep-alive: 在HTTP1.1的升级最主要的是 keep-alive 持久链接 大大的减少了每次请求都要建立一次TCP连接带来的耗时,从而提升了HTTP请求性能,keep-alive 带来这么大性能提升的同时也也有他的缺点

当然keep-alive也有缺点: 就算是在空闲状态,它还是会消耗服务器资源,而且在重负载时,还有可能遭受 DoS attack 攻击。这种场景下,可以使用非长连接,即尽快关闭那些空闲的连接,也能对性能有所提升。

DOS攻击类型

  • 带宽攻击
  • 服务器请求泛滥
  • SYN 泛滥攻击
  • ICMP 泛滥攻击
  • 点对点攻击
  • 永久 DoS 攻击
  • 应用层泛滥攻击

新增 pipeline ( 默认关闭 )

HTTP流水线模型 ,服务端在多个连续的请求甚至都不用等待立即返回就可以被发送 )

新增域名分片 ( 默认关闭,在HTTP2 中就没必要使用这个了,集成了更好的解决方案 )

作为 HTTP/1.x 的连接,请求是序列化的,哪怕本来是无序的,在没有足够庞大可用的带宽时,也无从优化。一个解决方案是,浏览器为每个域名建立多个连接,以实现并发请求。曾经默认的连接数量为 2 到 3 个,现在比较常用的并发连接数已经增加到 6 条。如果尝试大于这个数字,就有触发服务器 DoS 保护的风险。域名分片主要解决的就是这个问题

如果服务器端想要更快速的响应网站或应用程序的应答,它可以迫使客户端建立更多的连接。例如,不要在同一个域名下获取所有资源,假设有个域名是 www.example.com,我们可以把它拆分成好几个域名:www1.example.comwww2.example.comwww3.example.com。所有这些域名都指向同一台服务器,浏览器会同时为每个域名建立 6 条连接(在我们这个例子中,连接数会达到 18 条)。这一技术被称作域名分片。

HTTP/2 (未来的趋势,目前还没有普及)

所有数据都是以二进制来进行传输

HTTP/2之前都是用的字符串进行传输

同一个连接中发送多个请求不再需要按照顺序来

HTTP/2之前都是通过串行请求来响应请求,有了这个功能后可以并行返回请求了

头信息压缩

推送(服务端能够向客户端推送消息)

有了推送功能后,可以实现html/css/js 并行下载,而不是等到加载到html后,再解析DOM时再去加载css/js/image等资源文件

0%