# 如何解决跨域?

九种跨域方式实现原理 (opens new window)

  • jsonp

  • cors

  • postMessage

  • websocket

  • Node 中间件代理(两次跨域)

  • nginx 反向代理

  • window.name + iframe

  • location.hash + iframe

  • document.domain + iframe

不受跨域限制的标签和属性

link、script、img、background: url()、@font-face() 等均不受跨域限制。

# JSONP 的原理和优缺点

原理

  • jsonp 的原理本质就是利用 script 标签的 src 属性的天然可跨域特性实现跨域,让服务器接收浏览器传过来的函数名,然后返回一个带参的函数调用,浏览器接收数据,执行函数调用,对返回的数据进行操作。

优点

  • 可以实现跨域。

  • 兼容性好。

  • 在请求完毕后可以通过调用 callback 的方式回传结果。将回调方法的权限给了调用方。如果有两个页面需要渲染同一份数据,只需要有不同的渲染逻辑就可以了,逻辑都可以使用同一个 jsonp 服务。

缺点

  • 只支持 GET 请求。

  • jsonp 在调用失败的时候不会返回各种 HTTP 状态码。

  • 不安全可能会遭受 XSS 攻击。

# CORS 的原理

  1. CORS(Cross-Origin-Resource-Sharing) 是在服务器端设置的,不需要在客户端进行操作。

  2. CORS 背后的思想就是使用自定义的 http 头部让浏览器和服务器进行沟通,从而决定请求或响应应该成功还是失败。浏览器向服务器发送请求,如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中返回相同的源信息(如果是公共资源,则返回 *);如果没有这个头部,或者有这个头部但信息源不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。请求和响应都不包含 cookie 信息。

  3. 设置 CORS 之后,在发送请求时,会出现两种情况:简单请求复杂请求

(1)简单请求

只要同时满足以下两大条件,就属于简单请求。

条件 1:使用下列方法之一:

  • GET

  • HEAD

  • POST

条件 2:Content-Type 的值仅限于下列三者之一:

  • text/plain

  • multipart/form-data

  • application/x-www-form-urlencoded

对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是头信息之中,增加一个 Origin 字段。Origin 字段用来说明请求来自哪个源(协议+域名+端口号)。服务端根据这个值,决定是否同意本次请求。

(2)复杂请求

不符合以上条件的请求就肯定是复杂请求了。比如请求方法为 PUT 或 DELETE,或者 Content-Type 字段为 application/json。

复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 “预检” 请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。预检请求的头信息中,关键字段有以下几个:

  • Origin:表示请求来自哪个域;

  • Access-Control-Request-Method:必选,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法;

  • Access-Control-Request-Headers:该字段是一个用逗号分割的字符串,执行浏览器 CORS 请求会额外发送的头信息字段。

服务器收到预检请求以后,检查了以上三个字段后,确认允许跨域请求,就可以做出回应。如果浏览器否定了 “预检” 请求,就会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段,这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被 XMLHttpRequest 对象的 onerror 回调函数捕获。

一旦服务器通过了预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。

  1. 服务端如何设置 CORS (opens new window)

详细信息可参考:CORS 原理及 @koa/cors 源码解析 (opens new window)

# 图片懒加载和预加载的实现原理

1. 懒加载

  • 懒加载就是延迟加载。

  • 它的优点是页面加载速度快,可以减轻服务器的压力,节约流量,用户体验好。

  • 它的实现原理是:先不设置图片的 src 属性,而是自定义一个 data-src 属性来储存图片的路径,由于不是 src,所以不会发送 http 请求。当图片滚动到可视区域时,再将 data-src 的值赋给 src。

2. 预加载

  • 预加载就是提前加载图片,当用户需要查看时可直接从本地缓存中获取图片进行渲染。

  • 预加载可以说是牺牲服务器的性能,增加服务器的压力,以换取更好的用户体验,这样可以使用户的操作得到最快的反应。

  • 常用的是 new Image(),设置其 src 来实现预加载,再使用 onload 方法回调预加载完成事件。只要浏览器把图片下载到本地,同样的 src 就会使用缓存,这是最基本也是最实用的预加载方法。当 Image 下载完图片头后,会得到宽和高,因此可以在预加载前得到图片的大小(方法是用记时器轮循宽高变化)。

# 按要求实现 sum 函数

实现一个 sum 函数,有如下效果:

console.log(sum(1, 2, 3).sumOf()); // 6
console.log(sum(2, 3)(2).sumOf()); // 7
console.log(sum(1)(2)(3)(4).sumOf()); // 10
console.log(sum(1)(10, 100)(1000).sumOf()); // 1111
1
2
3
4

实现如下:

function sum(...arg1) {
  function fn(...arg2) {
    Array.prototype.push.apply(arg1, arg2);
    return fn;
  }
  fn.sumOf = function() {
    return arg1.reduce((prev, cur) => (prev += cur), 0);
  };
  return fn;
}
1
2
3
4
5
6
7
8
9
10

# 按要求实现 go 函数

console.log(go("1")); // go1
console.log(go()("1")); // goo1
console.log(go()()()("1")); // goooo1
1
2
3

这道题实际上考察的就是闭包,原理就是当没有传入参数时就默认传 'o',然后累加起来返回函数本身;当最后有参数时就返回之前累计的参数拼接字符串返回。

function go(...arg1) {
  let str = "go";
  function fn(...arg2) {
    if (arg2[0]) {
      str += arg2[0];
      return str;
    } else {
      str += "o";
      return fn;
    }
  }
  return fn(...arg1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 按要求实现 curry 函数

function add(a, b) {
  return a + b;
}

function curry(fn) {}

console.log(curry(add)(1)(2)); // 3
console.log(curry(add)(1, 2)); // 3
1
2
3
4
5
6
7
8

实现如下:

function curry(fn) {
  const args = [];
  return function cb() {
    args.push(...arguments);
    if (args.length === fn.length) {
      return fn(...args);
    }
    return cb;
  };
}
1
2
3
4
5
6
7
8
9
10

# 实现大整数相加

  • ~ 是按位取反运算,~~ 是取反两次。因为位运算的操作值要求是整数,其结果也是整数,所以经过位运算的都会自动变成整数。

  • 与 Math.floor() 不同的是,它只是单纯的去掉小数部分,不论正负都不会改变整数部分。

function addLargeInt(a, b) {
  var res = "",
    c = 0;
  a = a.split("");
  b = b.split("");
  while (a.length || b.length || c) {
    c += ~~a.pop() + ~~b.pop();
    res = (c % 10) + res;
    c = c > 9;
  }
  return res.replace(/^0+/, "");
}
1
2
3
4
5
6
7
8
9
10
11
12

# 图片转换成 base64 的优缺点

优点

  • base64 格式的图片是文本格式,占用内存小,转换后的大小比例大概为 1/3,降低了资源服务器的消耗。

  • 减少 http 请求。

  • 没有跨域问题,更适合不同平台、不同语言的传输。

缺点

  • base64 无法缓存,要缓存只能缓存包含 base64 的文件,比如 js 或者 css,这比直接缓存图片要差很多。

  • 使用 base64 不代表性能优化,虽然能够减少 http 请求,但是与之同时付出的代价则是 CSS 文件体积的增大。CSS 文件体积的增大意味着 CRP 的阻塞。

补充

CRP(Critical Rendering Path,关键渲染路径):当浏览器从服务器接收到一个 HTML 页面的请求时,到屏幕上渲染出来要经过很多个步骤。浏览器完成这一系列的运行,或者说渲染出来我们常常称之为 “关键渲染路径”。

总而言之,就是图片不会导致关键渲染路径的阻塞,而转化为 base64 的图片大大增加了 CSS 文件的体积,CSS 文件的体积直接影响渲染,导致用户会长时间注视空白屏幕。

# 数组去重时间复杂度为 n 的方法

  • 时间复杂度为 O(n^2)
function fn(arr) {
  return arr.filter((item, index, arr) => arr.indexOf(item) === index);
}
1
2
3
  • 时间复杂度为 O(n)
function fn(arr) {
  const obj = {};
  arr.map((item) => {
    obj[item] = item;
  });
  return Object.values(obj);
}
1
2
3
4
5
6
7

# 如何判断一个元素在可视窗口范围内

1. 使用 getBoundingClientRect 方法

通过获取元素的位置信息,直接比较元素的位置和可视窗口的位置来判断。

function isElementInViewport(element) {
  const rect = element.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <=
      (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}

// 用法示例
const myElement = document.getElementById("myElement");
if (isElementInViewport(myElement)) {
  console.log("Element is in the viewport!");
} else {
  console.log("Element is not in the viewport!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

2. 使用 Intersection Observer API

使用 Intersection Observer API 能够异步地监听目标元素与视口的交叉,并在交叉状态发生改变时触发回调函数。

// 创建一个 IntersectionObserver 实例
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log("Element is in the viewport!");
    } else {
      console.log("Element is not in the viewport!");
    }
  });
});

// 监听特定元素
const targetElement = document.getElementById("myElement");
observer.observe(targetElement);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 长列表的优化

# 虚拟列表的实现原理

# 成熟的虚拟列表组件

# 判断两个整数相除的结果

如何判断两个整数相除的结果是有限小数还是无限循环小数呢?

当除数(分母)只有 2 或者 5 这两个质因数时,就是有限小数;当除数(分母)含有 2 和 5 以外的质因数时就是无限循环小数。

补充知识

  • 被除数 ÷ 除数 = 商 …… 余数,因此分子是被除数,分母是除数。

  • 自然数是指表示物体个数的数,即由 0 开始,0,1,2,3,4,…… 一个接一个,组成一个无穷的集体,即指非负整数。

  • 质数是指在大于 1 的自然数中,除了 1 和它本身以外不再有其他因数的自然数。

  • 质因数(素因数或质因子)在数论里是指能整除给定正整数的质数。除了 1 以外,两个没有其他共同质因子的正整数称为互质。因为 1 没有质因子,1 与任何正整数(包括 1 本身)都是互质。

  • 若整数 b 除以非零整数 a,商为整数,且余数为零, 我们就说 b 能被 a 整除(或说 a 能整除 b),b 为被除数,a 为除数,即 a|b(“|”是整除符号),读作“a 整除 b”或“b 能被 a 整除”。a 叫做 b 的约数(或因数),b 叫做 a 的倍数。

# 实现大文件上传和断点续传

大文件上传:

  1. 前端上传大文件时使用 Blob.prototype.slice (opens new window) 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片。

  2. 服务端接收切片并存储,收到合并请求后使用 fs.appendFileSync (opens new window) 对多个切片进行合并。

  3. 使用原生 XMLHttpRequest 的 upload.onprogress (opens new window) 实现对切片上传进度的监听。

  4. 使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度。

断点续传:

  1. 使用 spark-md5 (opens new window) 根据文件内容算出文件 hash。

  2. 通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)。

  3. 通过 XMLHttpRequest 的 abort (opens new window) 方法暂停切片的上传。

  4. 重新上传前服务端返回已经上传的切片名,前端跳过这些切片的上传。

实现大文件上传和断点续传 (opens new window)

# 前端常见的设计模式有哪些

1. 单例模式(Singleton Pattern)

  • 介绍:保证一个类只有一个实例,并提供一个全局访问点。

  • 应用:用于创建唯一的对象,例如全局状态管理、资源管理等。

  • 示例:Redux 和 Vuex 中的 Store 就是一个单例模式的应用。

2. 观察者模式(Observer Pattern)

  • 介绍:定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。

  • 应用:用于事件处理、数据监听等场景。

  • 示例:Vue 中的响应式数据就是基于观察者模式的。

3. 工厂模式(Factory Pattern)

  • 介绍:定义一个用于创建对象的接口,但由子类决定要实例化的类是哪一个,工厂方法使得一个类的实例化延迟到其子类。

  • 应用:用于对象的创建和初始化。

  • 示例:React 中的组件工厂函数,根据传入的参数创建不同的组件。

4. 适配器模式(Adapter Pattern)

  • 介绍:将一个类的接口转换成客户端希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

  • 应用:用于解决不同接口之间的兼容性问题。

  • 示例:封装第三方库的接口以适应自己的代码结构。

5. 装饰者模式(Decorator Pattern)

  • 介绍:动态地给一个对象添加一些额外的职责,就增加功能而言,装饰者模式比生成子类更为灵活。

  • 应用:用于动态添加对象的功能。

  • 示例:React 中使用高阶组件。

6. 策略模式(Strategy Pattern)

  • 介绍:定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。

  • 应用:用于在运行时动态选择算法。

  • 示例:JavaScript 中的排序算法,可以根据需要选择不同的排序策略。

7. 代理模式

  • 介绍:为其他对象提供一种代理以控制对这个对象的访问。

  • 应用:

    • 虚拟代理,延迟加载大型图片,等图片需要显示时再加载;

    • 保护代理,限制某些对象的访问权限。

# 内嵌 h5 和普通浏览器 h5 的区别

1. 环境和容器

  • 内嵌 H5:指的是将 HTML5 页面嵌入到其他应用程序或容器中,通常是移动应用程序。这种情况下,H5 页面运行在应用程序的 webview 中。

  • 普通浏览器 H5:指的是直接在普通的网页浏览器中打开的 HTML5 页面。

2. 性能和访问权限

  • 内嵌 H5:可以访问应用程序的原生功能和设备硬件,例如相机、位置信息等。但有时可能受到应用程序容器的限制。

  • 普通浏览器 H5:通常受到浏览器的安全策略和沙箱环境的限制,可能无法访问某些原生功能。

3. 用户体验

  • 内嵌 H5:由于可以与应用程序深度集成,可能提供更一致的用户体验,包括共享样式、导航等。

  • 普通浏览器 H5:在浏览器中打开,可能会有一些差异,例如导航栏、工具栏等可能不同于应用程序的设计。

4. 更新和维护

  • 内嵌 H5:更新可能需要通过应用程序的更新来实现,因此可能不如网页那样灵活。

  • 普通浏览器 H5:可以通过 web 更新,用户只需刷新页面即可获取最新版本。

# iframe 嵌套判断方法问题

请问使用如下代码判断当前页面是否被 iframe 嵌套会有什么缺陷吗?

if (window.self === window.top) {
  console.log("不在iframe中");
} else {
  console.log("在iframe中");
}
1
2
3
4
5

这种判断方法会存在以下 3 个问题:

1. 同源策略限制

如果页面被嵌入到一个具有不同源(不同的协议、域名或端口)的 iframe 中,浏览器的同源策略可能会阻止访问 window.top。在这种情况下,尝试访问 window.top 会报错。

2. 安全性考虑不全面

比如点击劫持,除了检测页面是否在 iframe 中,还应该采取额外的安全措施,比如设置 HTTP 头部的 X-Frame-Options 为 DENY 或 SAMEORIGIN 来防止页面被其他站点嵌入。

3. 兼容性问题

有些什么 QQ 或者微信内核啥的浏览器不一定对。

可以使用 postMesssage 通信的方式来告知当前页面是否被嵌套。

# 微前端

微前端是将前端单体应用拆分成一系列小型、独立的子应用,每个子应用都可以独立开发、测试、部署和运行。这些子应用可以独立存在,也可以组合成一个完整的前端应用。

# 组成部分

  • 主应用(Shell):整个应用的外壳,负责加载和组合各个子应用,并提供整体的导航和布局。

  • 子应用(Micro Frontends):拆分出的独立的功能单元,每个子应用都有自己的开发团队,可以使用不同的技术栈和框架。

# 通信机制

微前端的子应用之间需要进行通信,以便在整体上协同工作。通信可以通过以下方式实现:

  • 自定义事件:子应用通过发布和订阅自定义事件的方式进行通信。

  • 共享状态:使用状态管理工具,例如 Redux,将状态共享给其他子应用。

  • 跨域通信:子应用可以通过跨域通信的方式实现数据的交互。

# 路由管理

微前端中的路由管理可以由主应用或者子应用自行处理。主应用可以负责整体路由,也可以将路由委托给各个子应用自行处理。这样,每个子应用都可以独立定义自己的路由规则。

# 独立部署

每个子应用都可以独立部署,这使得团队可以更加灵活地进行开发和发布。不同的子应用可以按照自己的发布周期进行部署,而不会影响整个应用的稳定性。

# 技术栈多样性

不同的子应用可以使用不同的技术栈和框架,因为它们之间是独立的。这样,团队可以选择最适合自己业务需求的技术栈,而不会受到整体应用的限制。

# 优势

  • 松耦合:子应用之间相互独立,减少了耦合度。

  • 独立开发和部署:提高团队的开发和发布效率,降低冲突和合并的复杂性。

  • 技术栈多样性:允许团队根据业务需求选择最适合的技术栈。

  • 可维护性:将大型应用拆分成小块,更容易理解、测试和维护。

# 挑战

  • 版本管理:管理各个子应用的版本兼容性可能是一个挑战。

  • 共享状态管理:在微前端架构中,如何有效地管理和共享状态是一个需要考虑的问题。

  • 安全性:子应用之间的通信需要考虑安全性问题,防止潜在的攻击。

# 实现方案

# Serverless

Serverless 是一种云计算服务模型,允许开发者构建和运行应用程序,而无需关心底层服务器的管理。

在 Serverless 架构中,应用程序由事件触发的小型函数组成,这些函数由云服务提供商动态地管理和扩展。

# 解决的问题

  • 运维复杂性:传统的服务器管理涉及到配置、维护和监控,而 Serverless 将这些任务交给云服务提供商,开发者无需关注。

  • 弹性扩展:Serverless 提供弹性伸缩,按需自动调整资源,无需手动调整服务器规模,从而更好地适应应用的变化。

  • 成本优化:通过按使用量计费,Serverless 让开发者只需支付实际使用的计算资源,而无需预先购买和维护庞大的服务器群。

  • 快速部署:Serverless 允许开发者快速部署应用,而无需关心服务器的配置和环境问题。

# 特点和优势

  • 事件驱动:Serverless 应用通常是事件驱动的,函数在特定事件发生时被触发执行,如 HTTP 请求、文件上传等。

  • 无服务器架构:开发者不需要关心服务器的配置、维护和扩展,只需编写函数代码,云服务商负责处理底层基础设施。

  • 按需计费:Serverless 模型按照实际使用的资源进行计费,开发者只需支付实际消耗的计算资源,而不需要预先购买。

  • 自动扩展:Serverless 提供自动伸缩能力,根据请求负载自动调整资源,确保应用在高负载时能够保持响应。

  • 多语言支持:支持多种编程语言,开发者可以根据项目需求选择最适合的语言来编写函数。

  • 无状态函数:Serverless 函数通常是无状态的,每个函数都是独立的,不保留上下文信息,使得水平扩展更容易实现。

  • 快速部署:Serverless 应用可以更快速地部署和更新,无需关心底层基础设施的变化。

# 应用场景

  • API 和后端服务:构建 RESTful API、后端服务、微服务等,使开发者能够快速部署和扩展。

  • 数据处理:适用于数据处理、转换、清洗、分析等场景,例如处理实时日志、图像处理等。

  • 实时通知:Serverless 可以用于构建实时通知系统,例如推送通知、即时聊天等。

  • 定时任务:用于执行定时触发的任务,如定时备份、数据清理等。

  • Web 应用前端:用于构建静态网站、单页应用等前端应用,例如使用服务端渲染框架。

  • IoT 应用:处理大量传感器数据,进行实时分析和响应,适用于物联网应用。

  • 事件驱动的处理:使用 Serverless 构建事件驱动的处理系统,例如处理队列消息、数据库变更通知等。函数可以根据事件动态触发,响应不同的事件类型。

  • 自动化工作流:Serverless 可以用于构建自动化工作流,例如自动化部署、自动化测试、自动化数据流程等。函数可以作为工作流的各个步骤。

# WebAssembly

WebAssembly (opens new window)(通常缩写为 Wasm)不是一种语言,而是规定了一种虚拟指令集,可以作为各个语言的编译目标,然后通过 wasm 的虚拟机运行到浏览器还有其他各个平台中。

对于前端领域,WebAssembly 是⼀种为⽹络浏览器设计的低级编程语⾔,它允许在⽹⻚中运⾏接近原⽣性能的⾼效代码。WebAssembly 被⼴泛⽤于各种应⽤场景,特别是在性能要求较⾼的场合。

WebAssembly 的应⽤⼴泛,涉及游戏开发、图形处理、⾳频处理、机器学习、科学模拟等多个领域。它为 Web 开发带来了前所未有的性能提升,使得以前只能在桌⾯应⽤中实现的功能现在可以通过浏览器实现,极⼤地拓展了 Web 技术的应⽤范围。

1. 游戏开发

原⽣游戏移植:将 C++ 或其他语⾔编写的原⽣游戏移植到 Web 上,使其能够在浏览器中以接近原⽣的性能运⾏。

2. 图形和视频处理

图像编辑器:开发⾼性能的在线图像和视频编辑⼯具,例如 Photoshop 的在线版或其他图形处理软件。

3. ⾳频处理

⾳频合成:实现在浏览器中的⾼效⾳频合成和处理,例如⾳乐制作软件。

4. 3D 渲染

WebGL 集成:结合 WebGL 使⽤ WebAssembly 进⾏ 3D 渲染,适⽤于在线游戏、CAD 应⽤和虚拟现实(VR)体验。

5. 加密和安全

密码学应⽤:在客户端实现⾼效的加密和哈希算法,提升数据处理的安全性和速度。

6. 机器学习和⼈⼯智能

浏览器端机器学习:在浏览器中运⾏机器学习模型,例如使⽤ TensorFlow.js。

7. 科学模拟和数据可视化

物理模拟:进⾏复杂的物理和科学模拟,如分⼦建模、天⽓模拟等。

8. 移动应⽤和跨平台解决⽅案

PWA(渐进式⽹络应⽤):提⾼ PWA 的性能,使其接近原⽣应⽤体验。

9. ⾼性能计算

数值计算:执⾏复杂的数值计算任务,如⾦融建模、⼯程计算等。

10. 编程语⾔和⼯具链

编译器和⼯具链:在浏览器中运⾏编程语⾔的编译器或解释器,实现浏览器端的代码编译和执⾏。

# Web Component

Web Component (opens new window) 是⼀组不同的技术,允许创建可重⽤的⾃定义元素,并在 Web 应⽤中封装功能和样式

它的核心技术包括 Custom Elements (opens new window)Shadow DOM (opens new window)HTML Templates (opens new window)

  • Custom element(自定义元素):允许开发者定义自己的 HTML 元素。通过创建自定义元素,你可以将复杂的功能打包成一个独立的元素,使其可以在不同的页面和项目中重复使用。

  • Shadow DOM(影子 DOM):提供了一种将样式和脚本封装在元素内部的方法,以防止它们影响到外部文档。这有助于避免全局样式和脚本的冲突,并提高组件的封装性。

  • HTML template(HTML 模板):<template><slot> 元素允许开发者将标记代码定义为模板,并在需要时克隆和插入到文档中。这有助于创建可重复使用的结构,并通过 JavaScript 进行实例化和填充。

Web Component 作为 Web 标准的⼀部分,由浏览器原⽣⽀持。它可以与任何 JavaScript 框架⼀起使⽤,或者完全独⽴使⽤。不过,虽然现代浏览器⼤多⽀持 Web Components,但在旧版浏览器中可能需要额外的 polyfills。

# Tailwind CSS

Tailwind CSS (opens new window) 是一个基于原子类的 CSS 框架,它提供了一组低级、单一职责的 CSS 类,以构建灵活且高度可定制的界面。与传统的 CSS 框架(如 Bootstrap 或 Foundation)不同,Tailwind 不会提供预定义的组件,而是专注于提供原子级的样式类,让开发者能够组合这些类来构建自己的界面。

  • 原子类:Tailwind 的主要特点是原子类。这些类名通常表示单一样式属性,比如 text-center 用于文本居中,bg-blue-500 用于设置背景颜色为蓝色。

  • 响应式设计:Tailwind 提供了一套响应式的工具类,用于在不同屏幕尺寸下设置样式。例如,你可以使用 lg:text-xl 来指定在大屏幕上使用更大的文字大小。

  • 自定义配置:Tailwind 是高度可定制的,你可以通过配置文件调整默认的颜色、字体大小、边距等值。这使得它非常适用于各种设计系统和项目需求。

  • 插件系统:Tailwind 允许你通过插件系统扩展其功能。这意味着你可以集成第三方插件,或者编写自己的插件以满足特定的需求。

  • 简化开发流程:Tailwind 的使用可以极大地简化开发流程,减少需要手写的 CSS 代码。通过原子类的组合,你能够快速构建出样式,而无需深入了解每个具体的 CSS 属性。

  • 易于学习:相对于其他复杂的 CSS 框架,Tailwind 的学习曲线相对较低。它提供了清晰简单的类名,而且文档详尽,使得开发者能够快速上手。

  • 无需编写自定义 CSS:在使用 Tailwind 时,你通常不需要编写大量的自定义 CSS。通过合理地组合现有的类,你可以实现各种样式和布局。

尽管 Tailwind 在许多项目中得到了广泛的应用,但也有一些人认为它可能使 HTML 变得更加臃肿,因为需要添加大量的类名。不过,这个问题也可以通过使用 @apply 提取类 (opens new window)来优化:

<!-- Before extracting a custom class -->
<button
  class="py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
>
  Save changes
</button>

<!-- After extracting a custom class -->
<button class="btn-primary">
  Save changes
</button>
1
2
3
4
5
6
7
8
9
10
11
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn-primary {
    @apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
  }
}
1
2
3
4
5
6
7
8
9

# GraphQL

GraphQL (opens new window) 是由 Facebook 开发的⼀种数据查询和操作语⾔,以及与之配套的运⾏时。它有以下优点:

  • 精确查询:客户端可以精确指定所需的数据结构,避免过度获取或不⾜获取数据。

  • 单⼀端点:与 RESTful API 不同,GraphQL 通常只有一个端点(Endpoint),客户端可以通过这个端点发送不同的查询来获取所需的数据,而不是依赖多个固定的端点。

  • 类型系统:GraphQL 有⼀个强类型系统,可以在查询执⾏前进⾏验证。

  • 实时数据(通过订阅):⽀持实时数据更新功能,服务器能够主动推送数据变更给客户端,而不需要客户端轮询。

  • 可扩展性:GraphQL 是一个灵活的查询语言,允许在后端进行逐步演进和扩展,而不会破坏现有的客户端查询。

  • 文档性:GraphQL 支持自动文档生成。通过查询端点,可以获得 API 的详细文档,包括可用的类型、字段和关联关系。

  • 生态系统:GraphQL 已经在多个编程语言中得到实现和支持,使其成为一个跨平台、跨语言的解决方案。

GraphQL 的核心思想是将数据的获取权力交给客户端,使得客户端能够更灵活地获取所需的数据,而不受服务器端的限制。它适合需要⾼度灵活性和精确控制数据交互的复杂应⽤,尤其是在多变的产品需求和多端数据需求频繁的场景。

# tRPC

tRPC(Typed RPC) (opens new window)是一个用于 TypeScript 的端到端类型安全的远程过程调用(RPC)框架。它强调使用 TypeScript 的类型系统,通过在 TypeScript 中定义接口来确保远程调用的类型安全性。

1. 类型安全

tRPC 是一个强类型的 RPC 框架,它利用 TypeScript 的类型系统来确保在编译时捕获潜在的远程调用错误。这意味着你可以在开发过程中受益于 TypeScript 的强大类型检查,避免在运行时发现类型错误。

2. 自动代码生成

通过在 TypeScript 中定义接口,tRPC 提供了自动代码生成的功能,它可以为你生成客户端和服务端的代码。这消除了手动同步客户端和服务端代码的问题,确保它们一直保持同步,减少了潜在的错误。

3. 减少文档不一致性

tRPC 中的接口定义即是文档。你不需要额外的文档来描述远程调用的接口,因为接口已经以代码的形式存在。这有助于避免文档和代码不一致的问题。

4. 开发效率提升

使用强类型的远程调用接口,可以在开发过程中获得智能感知、自动完成和类型检查等功能,从而提高开发效率。你可以在编辑器中直接看到可用的远程调用方法,以及它们的输入和输出类型。

5. 错误处理

tRPC 提供了强大的错误处理机制。你可以定义各种错误类型,并在服务端和客户端上进行捕获和处理。这有助于更好地处理错误情况,提高应用的稳定性。

6. 扩展性

tRPC 是可扩展的,支持插件和中间件。这意味着你可以根据自己的需求自定义和扩展功能,使 tRPC 更好地适应你的项目要求。

7. 支持多种传输协议

tRPC 支持多种传输协议,包括 HTTP、WebSocket 等,使其适用于不同的应用场景。你可以选择最适合你项目的传输方式。

# Monorepo

Monorepo (opens new window) 是一种项目代码管理方式,指单个仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。

1. 优点

  • 一个仓库中多个相关项目,很容易看到整个代码库的变化趋势,更好的团队协作。

  • 多项目代码都在一个仓库中,相同版本依赖提升到顶层只安装一次,节省磁盘内存。

  • 代码复用高,方便进行代码重构。

  • 依赖调试方便,依赖包迭代场景下,借助工具自动 npm link,直接使用最新版本依赖,简化了操作流程。

  • 多项目在一个仓库,工程配置一致,代码质量标准及风格也很容易一致。

  • 构建性 Monorepo 工具可以配置依赖项目的构建优先级,可以实现一次命令完成所有的部署。

2. 缺点

  • 增加了非 owner 改动代码的风险。

  • 多个项目代码都在一个仓库中,没有项目粒度的权限管控,一个项目出问题,可能影响所有项目。

  • 多项目在一个仓库中,代码体积多达几个 G,git clone 时间较长。

# 面试资源

上次更新时间: 2024年01月16日 18:30:05