跳到主要内容

发布订阅模式和观察者模式的区别

· 阅读需 4 分钟
素明诚
Full stack development

首先,无论是观察者模式还是发布-订阅模式,都是为了在对象之间实现低耦合的通信。这两种模式都涉及到 "订阅者" 和 "发布者" 的概念。订阅者订阅某个事件或数据的更新,当发布者发布这个事件或更新这个数据时,所有的订阅者都会得到通知。

观察者模式:在观察者模式中,发布者和订阅者之间有直接的关联。换句话说,发布者直接调用订阅者的方法来通知他们。因此,发布者需要知道它的订阅者是谁。例如,我们可以想象一个报纸订阅的情形,报纸公司(发布者)有一个明确的订阅者列表,当有新的报纸出版时,它会直接将报纸送到订阅者的门前。

class Publisher {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
publish() {
this.observers.forEach((observer) => {
observer.notify();
});
}
}

class Observer {
notify() {
console.log('Observer has been notified.');
}
}

const publisher = new Publisher();
const observer = new Observer();

publisher.subscribe(observer);
publisher.publish(); // Log: Observer has been notified.


发布-订阅模式:与观察者模式不同,发布-订阅模式中的发布者和订阅者之间不存在直接关联。而是通过一个 "消息中心" 或 "事件总线" 来进行通信的。发布者发布事件到消息中心,订阅者则从消息中心订阅事件。因此,发布者并不需要知道订阅者是谁。我们可以想象电视广播的情形,电视台(发布者)只负责将节目(事件)发送到电视信号中(消息中心),而作为观众(订阅者),你只需要在电视上调到正确的频道(订阅事件)就可以看到节目。

class EventBus {
constructor() {
this.listeners = {};
}
subscribe(eventName, listener) {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
this.listeners[eventName].push(listener);
}
publish(eventName) {
if (this.listeners[eventName]) {
this.listeners[eventName].forEach((listener) => {
listener();
});
}
}
}

const eventBus = new EventBus();

eventBus.subscribe('news', () => console.log('Subscriber has been notified.'));
eventBus.publish('news'); // Log: Subscriber has been notified.

  • 在观察者模式中,发布者和订阅者直接进行交互,而在发布-订阅模式中,发布者和订阅者通过一个第三方(消息中心/事件总线)进行交互。
  • 观察者模式中的发布者知道订阅者是谁,而发布-订阅模式中的发布者并不知道订阅者是谁。

总结

从理论上讲,两种模式都可以实现实时通知。无论是观察者模式还是发布-订阅模式,一旦有事件发生或数据变化,订阅者都可以立即收到通知。

区别主要在于这两种模式通知订阅者的方式不同:

  • 在观察者模式中,发布者直接通知订阅者。因此,只要发布者发出通知,订阅者就可以立即收到。
  • 在发布-订阅模式中,发布者将通知发送到一个中间代理(通常被称为事件总线或消息中心),然后由这个中间代理将通知传递给订阅者。这意味着,尽管可能存在一点延迟(因为通知需要首先到达中间代理,然后再从中间代理传递给订阅者),但这个延迟通常可以忽略不计,所以订阅者依然可以近乎实时地收到通知。

实际上,发布-订阅模式能够提供更多的灵活性,因为它解耦了发布者和订阅者。由于发布者和订阅者都只与事件总线交互,而不是直接交互,因此可以更容易地添加、删除或修改发布者和订阅者,而无需改变其他部分的代码。这使得发布-订阅模式特别适合大型、复杂的系统,其中的组件可能需要在运行时动态地添加或删除。

同时,通过使用事件总线,发布-订阅模式还可以实现更复杂的消息传递模式,例如消息过滤、消息持久化、消息排序等,这是观察者模式无法直接实现的。

JS中this有什么意义

· 阅读需 2 分钟
素明诚
Full stack development

方法借用:一个对象可以借用另一个对象的方法。例如,我们有一个 person 对象,有一个 introduce 方法可以引用 this.name

let person = {
name: "Alice",
introduce: function() {
console.log(`Hello, my name is ${this.name}`);
}
};

// 这个对象有一个 name 属性,但是没有 introduce 方法
let anotherPerson = {
name: "Bob"
};

// 我们可以借用 person 的 introduce 方法
// 实际意思就是说,我通过call方法,把person上的方法拿来了,在你的身上执行

person.introduce.call(anotherPerson); // 输出 "Hello, my name is Bob"

使用 apply() 以数组形式传递函数参数:如果一个函数接受一组值,而你的值是存储在数组中,apply() 可以帮助你以数组形式传递参数。

function sum(a, b, c) {
return a + b + c;
}

let numbers = [1, 2, 3];

console.log(sum.apply(null, numbers)); // 输出 6

使用 bind() 为事件处理程序创建持久性上下文:在处理事件或回调函数时,你可能想要函数在特定上下文中执行,bind() 可以帮助你实现这一点。

let button = {
content: 'Show message',
clickHandler: function() {
console.log(this.content);
}
};

// 这样做会导致 this 丢失,因为 addEventListener 会调用 clickHandler,将 this 设置为调用它的元素(这里是 button 元素)
// document.querySelector('button').addEventListener('click', button.clickHandler);

// 使用 bind,我们可以确保 clickHandler 内的 this 始终指向 button 对象
document.querySelector('button').addEventListener('click', button.clickHandler.bind(button));

比较 TCPHTTP 和 WebSocket 三种协议

· 阅读需 1 分钟
素明诚
Full stack development
特性 / 协议TCPHTTPWebSocket
是否基于连接是,是一种面向连接的协议是,但默认每次请求都需要重新建立连接(HTTP/1.1 引入了 keep-alive 机制以复用连接)是,一次握手后建立持久连接
可靠性是,通过 ACK、序列号、重传机制等保证是,因为基于 TCP是,因为基于 TCP
通信方式全双工半双工,客户端发送请求后等待并接收响应全双工
数据流向双向单向,从客户端发送请求到服务端响应双向,可以从客户端到服务端,也可以从服务端到客户端
头部信息无,仅在握手时有少量开销每次请求和响应都有头部信息,可能导致额外开销握手时有开销,但之后数据传输的头部开销非常小
使用场景低级别的网络通信,不特定于应用场景网页请求,API 请求,短连接实时或需要服务器主动推送的应用,如聊天应用、在线游戏等

Vue3 中的 provide 和 inject

· 阅读需 2 分钟
素明诚
Full stack development

在 Vue 3 中,provideinject 的值默认是不响应式的。这意味着如果你提供一个值,然后在提供该值之后改变它,那么使用 inject 的组件将不会看到这个变化。

然而,你可以通过 Vue 3 的 reactive 或者 ref 方法来创建响应式的值,然后提供这个响应式的值。这样,当这个值改变时,所有注入该值的组件都会重新渲染,以反映这个变化。

以下是一个例子:

// 在 Vue 3 中的 main.js 或者 main.ts 文件中
import { createApp, reactive } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 创建一个响应式的值
const globalState = reactive({ user: 'John Doe' })

// 提供这个响应式的值
app.provide('globalState', globalState)

app.mount('#app')

然后在任何子组件中,你可以使用 inject API 来接收这个响应式的值,并在模板或者计算属性中使用它:

// 在任何子组件中
import { inject } from 'vue'

export default {
setup() {
const globalState = inject('globalState')

// 在模板或者计算属性中使用 globalState.user
return { globalState }
}
}

在 Vue 3 中,你需要使用 globalState.user 来访问 user 属性,而不能直接使用 globalState,因为 globalState 是一个响应式的对象,而不是一个值。如果你需要提供一个响应式的值(例如一个字符串或数字),你可以使用 ref 方法。

微前端和PNPM工作空间两种方案对比

· 阅读需 3 分钟
素明诚
Full stack development

微前端(Micro Frontends):

微前端是一种设计理念,将前端应用分解成一些较小,更易于管理和交互的部分。每个部分都可以由不同的团队独立开发和部署。

优点:

  1. 每个微应用可以使用最适合它的技术栈。这可以帮助团队利用新技术,也使得代码更加清晰和可维护。
  2. 由于每个微应用都可以独立开发和部署,团队可以更快速地交付新的功能和改进。
  3. 每个微应用可以独立扩展,以满足特定业务需求。

缺点:

  1. 微前端架构可能会导致技术栈的混乱和维护成本的增加。
  2. 有可能遇到跨微应用的数据共享和状态管理问题。
  3. 如果微应用之间需要频繁交互,可能会导致性能问题。

PNPM 工作空间(PNPM Workspaces):

PNPM 工作空间允许您在单一的仓库中管理多个包。它能帮助您在仓库中共享和重用代码,以及更容易地管理多个项目的依赖。

优点:

  1. 更好的代码重用和更高效的依赖管理。
  2. 更容易协调和管理多个项目。
  3. 更容易一次性构建和测试所有的项目。

缺点:

  1. 工作空间可能会导致代码的混乱和复杂性的增加。
  2. 如果没有良好的依赖和版本管理,可能会导致问题。
  3. 与微前端相比,PNPM 工作空间可能不如微前端适应大型项目。

总结起来,这两种方案都有各自的优点和缺点,选择哪一种取决于项目的特性和团队的能力。考虑到这是一个大型的前端项目,由多个小项目组成,我会建议选择微前端。微前端可以让每个小项目独立开发和部署,这对于大型的、由多个小项目组成的项目来说,可以提高开发和部署的效率,同时也使得每个小项目更易于管理和扩展。当然,微前端也有它的挑战,如技术栈的管理和跨应用的数据共享,需要团队有一定的经验和技术能力来解决这些问题。

个人认为选微服务可能后期问题会更少些

对等依赖peer dependency

· 阅读需 2 分钟
素明诚
Full stack development

对等依赖(peer dependency)指的是一个库需要在其工作环境中预先存在一个特定的库或者工具,但并不直接包含或导入这个特定库。这种特定库被称为这个库的对等依赖。

假设你正在开发一个名为 my-plugin 的 Vue.js 插件。这个插件需要在一个已经安装了 Vue.js 的环境中运行,因为它需要使用 Vue.js 的一些特性或 API。然而,my-plugin 并不会直接包含 Vue.js 的代码,它只是假设 Vue.js 已经存在于其运行环境中。在这种情况下,我们说 Vue.js 是 my-plugin 的对等依赖。

当你在 my-pluginpackage.json 文件中声明 Vue.js 为对等依赖时,你在告诉使用 my-plugin 的开发者,他们需要自己手动安装 Vue.js。

对等依赖的一个主要应用场景是插件系统,如上面的例子。插件需要使用主程序(如 Vue.js)的 API,但并不直接包含主程序的代码。通过将主程序声明为对等依赖,插件可以确保自己总是使用与主程序相同的版本,避免版本冲突或者不兼容的问题。

另外,对等依赖也可以用于强制执行某些版本的兼容性。例如,如果 my-plugin 只能和 Vue.js 3.x 兼容,那么你可以在 my-pluginpackage.json 文件中声明 Vue.js 的版本为 "^3.0.0"。这样,如果用户尝试在 Vue.js 2.x 的环境中使用 my-plugin,他们会收到一个警告。

例如,你可以这样声明

5000b9e047739b1338ba0661f58072e3

为什么清除浮动要使用display table

· 阅读需 2 分钟
素明诚
Full stack development

display: table

display: table;:这行代码会将伪元素变成一个块级元素。这样,我们就可以在它上面应用 clear:both。为什么选择 display: table; 而不是 display: block; 呢?

实际上,两者都可以。但是,使用 display: table; 还可以避免某些其他的布局问题,例如外边距(margin)塌缩。

因此,.clearfix::after { content: ""; display: table; clear: both; } 这样的代码可以有效地清除浮动,并避免布局问题。

那边距塌缩又是什么呢?

在 CSS 中,“边距塌缩”是一个常见的现象,即当两个垂直方向的块级元素相邻,并且没有任何内容、padding 或 border 将它们分隔开时,它们之间的间距不会是两个 margin 的总和,而是两个 margin 中的最大值。这就是所谓的“边距塌缩”。

例如,假设我们有两个元素,第一个元素的下边距(margin-bottom)是 20px,第二个元素的上边距(margin-top)是 30px。在边距塌缩的现象下,两个元素之间的距离将是 30px(最大的那个),而不是 20px + 30px = 50px。

那么,为什么display: table;能够防止边距塌缩呢?

这是因为在 CSS 规范中,display: table;会创建一个新的块格式化上下文(Block Formatting Context, BFC)。在新的 BFC 中,内部的元素会在垂直方向上一个接一个地放置,并且可以管理浮动元素、防止外部元素与内部元素重叠,并防止边距塌缩。

这就是为什么要用display: table的好处

npm 安装包时常见参数

· 阅读需 1 分钟
素明诚
Full stack development

-g:全局安装,将包安装到全局环境中,可以在命令行中直接使用

-D(--save-dev):该模块只需要用于开发环境,不用于生产环境,例如测试工具、构建工具等。安装后会被添加到 package.json 文件的 devDependencies 中。

-Y(--yes):在安装模块时,自动选择 yes ,不需要用户确认。

-S(--save):将模块添加到 package.json 文件中的 dependencies 中,该模块在生产环境和开发环境都需要使用。

-E--save-exact:在 package.json 文件中添加精确的版本号,而不是一个范围。

-O--save-optional:将包安装到项目的可选依赖中,同时在 package.json 文件中添加可选依赖项

举例来说,假设你需要安装 Lodash 这个库,如果你只在开发环境使用,可以使用 -D 参数:

npm install lodash -D

如果你需要在生产环境和开发环境都使用,可以使用 -S 参数:

npm install lodash -S

如果你需要在安装模块时自动确认,可以使用 -Y 参数:

npm install lodash -D -Y

解决跨域的九种方案

· 阅读需 3 分钟
素明诚
Full stack development

一、Proxy

配置 vue.config.js

amodule.exports = {
devServer: {
host: '127.0.0.1',
port: 8084,
open: true,// vue项目启动时自动打开浏览器
proxy: {
'/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
changeOrigin: true, //是否跨域
pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
'^/api': ""
}
}
}
}
}

通过 Axios 发送请求

axios.defaults.baseURL = '/api'

二、中间层

express 框架

var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:8088',
changeOrigin: false
}));

module.exports = app

三、Nginx

server {
listen 80;
# server_name www.josephxia.com;
location / {
root /var/www/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://127.0.0.1:8888;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

四、JSONP

跨域获取JSON数据的一种非官方的使用模式

  1. JSONJSONP不是一个类型。
  2. JSON是数据交换格式。
  3. JSONP是一种跨域获取JSON数据的交互技术 非正式的协议。
  4. JSONP抓取的资源并不直接是JSON数据,而是带有JSON数据参数的函数执行。

优点

  1. 兼容性好,可以运行在远古的浏览器中,不需要 XMLHttpReques t 或 ActiveX 的支持。
  2. 请求后可以通过 callback 的方式回传结果,JQ 有封装好的 JSONP 方法可以使用。

缺点

  1. 它只支持 GET 请求,不支持其他类型的请求。因为 Script 是通过 GET 请求发送的
  2. 不能解决两个页面直接进行 JavaScript 调用的问题

五、设置基础域名同源+iframe

前端来做,前提是基础域名必须相同

Document.domain - Web API 接口参考 | MDN## 六、window.name+iframe(传比较简单的值)

window.name的特点:

  1. 每个浏览器窗口都有一个全局变量window(包含iframe框架contentWindow)

  2. 每个window对象都有一个name属性(注意:一个窗口只有一个name属性)

  3. 该窗口被关闭前(生命周期内),所有页面共享一个name属性并有读写的权限

  4. 无论该窗口在被关闭前,载入什么页面,都不会改变name

  5. 存储约为2M的字符串

如果父级窗口地址源和iframe的地址源不同,父级无法通过iframe.contentWindow.name获取值,但iframe内部不受该规则限制

解决方案:先让iframe中的页面程序保存window.name, 然后跳转与父级窗口同源的另一个页面,父级页面可以从当前的iframe拿到该页面的window.name

七、postmessage+iframe(不常用)

不常用原因:

  1. 伪造数据端漏洞
  2. XSS攻击
  3. 兼容性问题
  4. 调试的时候也容易出现问题

变量参数:otherWindow.postMessage(message, targetOrigin)

  • otherWindow: 接收方的引用
  • message: 要发送到接受方的数据
  • targetOrigin: 接收方的源,还有必须要有监听message事件

八、hash+iframe(不常用)

e0d765cff3769a77cd9906828427d3b9## 九、cors 跨域

后端可能会存在安全问题,这种方式需要协商

89910dfd25b533653c07cb085e44dbe4

HTML meta 标签常见属性

· 阅读需 2 分钟
素明诚
Full stack development

HTML meta 标签可以用来提供关于 HTML 文档的元数据(metadata),即数据的数据,可以包含文档的描述、作者、关键字、编码方式、是否重新加载等等信息,有助于浏览器正确地显示页面内容,提高 SEO 优化、跨平台分享等效果。

下面是一些常见的 meta 标签及其作用:

  1. <meta name="description" content="网页描述">:指定网页的描述信息,有助于搜索引擎正确地显示搜索结果。
  2. <meta charset="UTF-8">:指定文档的字符集编码方式。
width=device-width——将页面宽度设置为跟随屏幕宽度变化而变化
initial-scale=1.0——设置浏览器首次加载页面时的初始缩放比例(0.0-10.0正数)
maximum-scale=1.0——允许用户缩放的最大比例(0.0-10.0正数),必须大于等于minimum-scale
minimum-scale=1.0——允许用户缩放的最小比例(0.0-10.0正数),必须小于等于maximum-scale
user-scalable=no——是否允许用户手动缩放(yes或者no)
  1. <meta name="viewport" content="width=device-width, initial-scale=1.0">:指定移动设备的视口(viewport)大小,有助于页面在不同设备上展示更好的效果。
  2. <meta name="keywords" content="关键词1,关键词2,关键词3">:指定网页的关键词,有助于搜索引擎正确地理解页面内容。
  3. <meta http-equiv="refresh" content="5;url=https://www.example.com/">:指定页面的刷新和跳转,可以在指定的时间内自动跳转到其他页面。
  4. <meta name="robots" content="index,follow">:指定搜索引擎的抓取行为,如何索引页面内容。
  5. <meta name="author" content="作者名称">:指定网页作者的名称。
  6. <meta http-equiv="Cache-Control" content="no-transform">的作用是告诉浏览器不要对页面内容进行转换,例如压缩、编码转换等,这样可以保证页面内容不被改变。这个标签的 content 属性的值是 no-transform,表示不进行转换。
  7. <meta http-equiv="Cache-Control" content="no-siteapp">的作用是告诉搜索引擎和浏览器不要将页面转换为移动应用的展示方式。这个标签的 content 属性的值是 no-siteapp,表示不要展示为移动应用。

除了上面列举的几个 meta 标签,还有很多其他的 meta 标签可以用来提供不同的元数据信息。