weifo

选择你所爱的,爱你所选择的


  • 首页

  • 存档

  • 分类

  • 标签

  • Search

TypeScript 在前端项目中的最佳实践

Posted on 2024-09-16 | In TypeScript

TypeScript 已成为现代前端开发的主流选择之一。它提供了类型系统、接口、泛型等语言特性,极大地提升了代码的可维护性和开发效率。本文将结合实际场景和代码示例,分享 TypeScript 在前端项目中的最佳实践,帮助你构建更稳健、更高效的应用。

一、项目初始化建议

1. 使用官方推荐配置

使用 tsc --init 创建配置文件后,可以启用一些推荐的选项:

1
2
3
4
5
6
7
8
{
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"skipLibCheck": true
}

这些配置确保了更严格的类型检查,有助于发现潜在问题。

2. 使用 ESLint + Prettier 统一规范

结合 TypeScript 插件,确保团队代码风格统一。

1
npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev

在 .eslintrc.js 中配置:

1
2
3
4
5
6
7
8
9
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier'
]
};

二、类型系统最佳实践

1. 避免使用 any

any 会绕过类型系统,容易引发运行时错误。

1
2
3
4
5
6
7
8
9
10
// 尽量避免
let data: any = fetchData();

// 更好的方式
interface User {
id: number;
name: string;
}

const user: User = { id: 1, name: 'Alice' };

2. 善用联合类型和类型守卫

1
2
3
4
5
6
7
function printId(id: string | number) {
if (typeof id === 'string') {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}

3. 使用类型别名和接口管理结构

1
2
3
4
5
6
7
8
9
10
11
12
type Status = 'loading' | 'success' | 'error';

interface ApiResponse<T> {
status: Status;
data: T;
message?: string;
}

const response: ApiResponse<string[]> = {
status: 'success',
data: ['a', 'b', 'c']
};

三、模块化和可复用性

1. 使用泛型编写通用函数

1
2
3
4
5
6
function identity<T>(arg: T): T {
return arg;
}

const str = identity<string>('hello');
const num = identity<number>(123);

2. 封装工具函数类型声明

1
2
3
4
5
6
7
// utils.ts
export function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}

// 使用
const obj = merge({ foo: 1 }, { bar: 'baz' });

四、在 React 中使用 TypeScript

1. 函数组件类型

1
2
3
4
5
6
7
8
9
10
import React from 'react';

type Props = {
title: string;
onClick: () => void;
};

const Button: React.FC<Props> = ({ title, onClick }) => {
return <button onClick={onClick}>{title}</button>;
};

2. 使用 useState 的类型

1
const [count, setCount] = useState<number>(0);

3. 事件对象类型

1
2
3
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};

五、集成第三方库

1. 安装类型定义

1
npm install --save-dev @types/lodash

2. 为无类型库添加声明

1
2
3
4
// types/custom-lib.d.ts
declare module 'my-lib' {
export function customFunction(): void;
}

六、与 API 接口结合的技巧

1. 使用接口或类型描述接口返回值

1
2
3
4
5
6
7
8
9
interface LoginResponse {
token: string;
expires: number;
}

async function login(): Promise<LoginResponse> {
const res = await fetch('/api/login');
return res.json();
}

2. 搭配 Axios 使用

1
2
3
4
5
6
7
8
9
10
import axios from 'axios';

interface User {
id: number;
name: string;
}

axios.get<User[]>('/api/users').then((res) => {
console.log(res.data);
});

七、避免常见陷阱

1. 不合理的类型断言

1
2
3
4
5
6
7
8
// 不推荐
const element = document.getElementById('root') as HTMLDivElement;

// 推荐
const element = document.getElementById('root');
if (element instanceof HTMLDivElement) {
element.innerText = 'Hello';
}

2. 忽视类型提示或覆盖类型

过度使用 as any 或者 @ts-ignore 会掩盖潜在问题,需慎用。

八、总结与展望

TypeScript 是现代前端工程不可或缺的一环。它不仅提升了代码的可读性、可靠性,也为大型团队协作带来极大的便利。

通过合理使用类型系统、模块化、泛型、React 类型定义等手段,你将能构建更加可维护和可拓展的应用。

性能优化:打造更快更流畅的前端体验

Posted on 2024-08-16 | In 性能优化

无论是企业级应用还是个人项目,前端性能始终是影响用户体验的重要因素之一。一款加载缓慢、交互迟钝的网站,极有可能让用户选择关闭页面。本文将从页面加载、资源管理、渲染优化和开发实践等方面,深入浅出地讲解前端性能优化的核心技巧,并通过示例帮助你快速上手。

1. 页面加载优化

1.1 减少请求数量

  • 合并 CSS/JS 文件
  • 使用图标字体或 SVG sprite 替代多张小图标
  • 使用 HTTP/2 以支持多路复用

1.2 资源懒加载(Lazy Load)

图片、组件、数据等都可以懒加载,只在需要时才加载,减少初始加载压力。

示例:懒加载图片(原生方式)

1
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="" />

或使用 JavaScript:

1
2
const img = document.querySelector('img');
img.src = img.dataset.src;

1.3 使用 CDN 加速资源加载

将静态资源托管到 CDN 可减少服务器压力,提高下载速度。

1
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>

2. 资源优化技巧

2.1 图片优化

  • 使用 WebP 格式
  • 通过压缩工具(如 TinyPNG、ImageOptim)处理图片
  • 设置合适的图片尺寸,避免大图缩小展示

2.2 压缩与 Tree-shaking

  • 使用 webpack 的 mode: production 自动启用压缩与 Tree-shaking
  • 移除未使用代码(Dead Code Elimination)
1
2
3
4
// webpack.config.js
module.exports = {
mode: 'production'
};

3. 渲染性能优化

3.1 避免频繁 DOM 操作

使用虚拟 DOM(React/Vue)是良好的方式,但即使如此,也需注意更新策略。

1
2
3
4
5
// React 中避免不必要重渲染
import React, { memo } from 'react';
const MyComponent = memo(function MyComponent(props) {
return <div>{props.value}</div>;
});

3.2 使用 requestAnimationFrame 优化动画

1
2
3
4
5
function animate() {
// 动画逻辑
requestAnimationFrame(animate);
}
animate();

3.3 虚拟列表(Virtual Scrolling)

用于渲染大数据列表,只渲染可视区的内容。

示例:React 虚拟列表(使用 react-window)

1
2
3
4
5
6
7
8
9
10
11
12
import { FixedSizeList as List } from 'react-window';

<List
height={400}
itemCount={1000}
itemSize={35}
width={300}
>
{({ index, style }) => (
<div style={style}>Row {index}</div>
)}
</List>

4. 开发与构建阶段优化

4.1 代码分割(Code Splitting)

使用 webpack 动态 import 实现按需加载。

1
2
3
4
// 动态导入模块
import('lodash').then(_ => {
// 使用 lodash
});

4.2 懒加载路由组件(以 Vue Router 为例)

1
2
3
4
const UserPage = () => import('./views/User.vue');
const routes = [
{ path: '/user', component: UserPage }
];

4.3 使用现代打包工具

如 Vite、esbuild、Rollup 相比 webpack 更快,构建性能优越。

1
npm init vite@latest my-project

5. 性能监测与分析

5.1 浏览器 DevTools

  • Performance 标签页分析帧率、渲染耗时
  • Network 标签页查看资源加载时间

5.2 Lighthouse 分析报告

Chrome 内置 Lighthouse 可评估页面性能、可访问性、SEO 等综合得分。

1
2
# 也可用 CLI 工具
npx lighthouse https://example.com --view

6. 常见误区

  • 滥用动画效果:可能导致掉帧
  • 未开启压缩打包:体积大,加载慢
  • 忽视移动端网络环境:过大资源拖慢加载

7. 结语

性能优化其实并不神秘,它更像是日常开发中的一项好习惯。哪怕只优化一两项,也足以让页面加载快上一截。

前端测试:从单元测试到端到端测试的全面指南

Posted on 2024-08-12 | In 前端测试

前端开发不仅仅是页面的编写和功能的实现,测试同样是不可忽视的重要环节。一套完整的测试体系能够帮助我们在快速迭代中保障质量,避免 Bug 重复出现。本文将用通俗易懂的语言,带你了解前端测试的核心知识:单元测试、组件测试、集成测试和端到端测试,并通过代码示例让你轻松上手。

1. 为什么需要前端测试?

测试的目标不是证明程序没有错误,而是尽早发现错误并降低修复成本。尤其是在多人协作和持续集成(CI)环境中,自动化测试可以极大地提高开发效率和代码稳定性。

常见的问题场景:

  • 某个按钮突然失效?
  • 页面改动影响了其他组件?
  • 重构之后页面一片空白?

这些问题都可以通过合适的测试手段在上线前发现。

2. 单元测试(Unit Testing)

单元测试是测试最小功能单元,比如一个函数的输出是否正确。

工具推荐:Jest

示例:测试一个格式化函数

1
2
3
4
5
6
7
8
9
10
11
12
// utils/format.js
export function formatPrice(price) {
return `$${price.toFixed(2)}`;
}

// __tests__/format.test.js
import { formatPrice } from '../utils/format';

test('formats price correctly', () => {
expect(formatPrice(5)).toBe('$5.00');
expect(formatPrice(5.1)).toBe('$5.10');
});

3. 组件测试(Component Testing)

测试 Vue、React 等框架中的组件是否能正确渲染、响应交互。

工具推荐:React Testing Library / Vue Test Utils

示例:测试 React 组件按钮点击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Button.jsx
export function Button({ onClick }) {
return <button onClick={onClick}>Click Me</button>;
}

// Button.test.jsx
import { render, fireEvent } from '@testing-library/react';
import { Button } from './Button';

test('button calls onClick when clicked', () => {
const handleClick = jest.fn();
const { getByText } = render(<Button onClick={handleClick} />);
fireEvent.click(getByText('Click Me'));
expect(handleClick).toHaveBeenCalled();
});

4. 集成测试(Integration Testing)

测试多个组件或模块组合后的整体行为是否符合预期。

比如一个表单是否能完整地填写并提交:

  • 用户输入内容
  • 点击提交按钮
  • 是否触发正确的回调或 API 请求

建议使用组件测试库模拟用户行为,不需要测试具体实现,而是关注行为:输入、点击、跳转等。

5. 端到端测试(E2E Testing)

端到端测试关注整个系统的行为:从用户打开页面、执行操作,到看到期望的结果。

工具推荐:Playwright / Cypress

示例:使用 Playwright 进行登录流程测试

1
2
3
4
5
6
7
8
9
10
// login.spec.ts
import { test, expect } from '@playwright/test';

test('user can login', async ({ page }) => {
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type=submit]');
await expect(page.locator('text=Welcome')).toBeVisible();
});

E2E 测试适合关键路径验证,例如登录、下单、支付等。虽然写起来比单元测试慢,但它覆盖的是整个真实用户的行为路径,非常有价值。

6. 测试策略建议

一个成熟项目中,应包含以下几种测试:

类型 目的 工具推荐
单元测试 验证函数逻辑正确性 Jest
组件测试 检查组件行为与 UI 渲染 Testing Library / Vue Test Utils
集成测试 模块组合是否正常工作 RTL / Vue Test Utils
E2E 测试 用户端到端行为验证 Playwright / Cypress

7. 写测试的小技巧

  • 用行为驱动的方式写测试(以“用户会做什么”为主)
  • 避免过度测试实现细节(容易因重构破坏)
  • 测试名要清晰表述测试目的
  • 每次提交前跑一次测试用例(通过 CI 或 pre-commit)

8. 结语

前端测试听起来可能有些枯燥,但它的确是保障应用质量的关键部分。你不需要一开始就覆盖所有测试类型,只要从最容易上手的单元测试开始,一步一步扩展,最终你会构建出一套坚实的测试体系。

测试就像给你的代码买了一份保险,一旦出问题,它会第一时间跳出来提醒你。长期来看,它能为你省下无数加班修 Bug 的夜晚。

希望这篇文章能帮你迈出前端测试的第一步。只要肯动手,你一定能写出既稳定又靠谱的前端项目。加油!

前端微前端架构:如何构建模块化 Web 应用

Posted on 2024-07-22 | In 微前端

随着 Web 应用变得日益复杂,单一前端项目逐渐难以满足大型团队的开发和维护需求。微前端(Micro Frontends)作为一种新兴架构思想,借鉴微服务理念,将前端应用拆分为多个独立的子应用。本文将介绍微前端的基本原理、常用技术方案,并提供实践建议与代码示例。

1. 微前端概述

1.1 定义

微前端是一种架构模式,将一个大型 Web 应用拆分成多个独立的子应用,每个子应用可以独立开发、测试和部署。

1.2 优势

  • 独立部署:每个子应用可单独发布,不影响其他模块。
  • 技术栈自由:子应用可以使用不同的前端框架。
  • 团队协作优化:不同团队可并行开发各自负责的模块。

2. 主流实现方案

2.1 基于 iframe

每个子应用嵌入在 iframe 中,优点是隔离性强,但缺点是通信复杂、SEO 不友好。

1
<iframe src="http://subapp.example.com"></iframe>

2.2 JavaScript 集成(single-spa)

single-spa 是目前最流行的微前端框架之一。

安装

1
npm install single-spa --save

注册子应用

1
2
3
4
5
6
7
8
9
import { registerApplication, start } from 'single-spa';

registerApplication({
name: 'nav',
app: () => System.import('nav'),
activeWhen: ['/']
});

start();

2.3 基于模块联邦(Module Federation)

Webpack 5 的 Module Federation 插件允许多个应用共享模块。

主应用配置

1
2
3
4
5
6
7
8
// webpack.config.js
plugins: [
new ModuleFederationPlugin({
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
})
]

子应用导出模块

1
2
3
4
5
6
7
8
9
10
// webpack.config.js
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.vue'
},
})
]

3. 微前端中的通信机制

3.1 URL 参数或 Hash

适合简单场景,如状态同步。

3.2 自定义事件(CustomEvent)

1
window.dispatchEvent(new CustomEvent('user:login', { detail: { userId: 123 } }));

3.3 全局状态管理(如 Redux 或 pinia)

推荐使用共享状态库进行统一管理。

4. 实践建议

  • 尽量统一子应用的公共依赖,如 React、Vue 版本。
  • 使用主应用加载子应用的样式,避免样式污染。
  • 为子应用设定加载超时处理,提升用户体验。

5. 结语

微前端不是银弹,但它为大型应用的可扩展性、团队协作与技术多样性提供了坚实基础。通过合理拆分模块、选择合适的集成方式,并建立良好的通信机制,可以让团队在保障独立性的同时实现统一的用户体验。

当然,引入微前端也意味着增加架构复杂性,因此需要根据团队规模和项目需求权衡使用。未来的前端开发,注定会是组件化和服务化的结合体。

Web安全:前端开发者必须掌握的安全防护措施

Posted on 2024-07-14 | In Web安全

Web安全是前端开发中不可忽视的重要本文将介绍前端开发者必须掌握的安全防护措施,并提供代码示例,帮助开发者提升 Web 应用的安全性。

1. 防范 XSS(跨站脚本攻击)

XSS(Cross-Site Scripting)攻击是指攻击者在网页中注入恶意脚本,影响用户。

1.1 输入验证和输出编码

错误示例(未处理用户输入)

1
2
3
4
5
<div id="output"></div>
<script>
const params = new URLSearchParams(window.location.search);
document.getElementById("output").innerHTML = params.get("name"); // XSS 风险!
</script>

正确示例(使用 textContent 进行转义)

1
2
3
4
5
<div id="output"></div>
<script>
const params = new URLSearchParams(window.location.search);
document.getElementById("output").textContent = params.get("name");
</script>

1.2 使用 CSP(内容安全策略)

CSP 可以有效防止 XSS 攻击。

1
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted.com">

2. 防范 CSRF(跨站请求伪造)

CSRF(Cross-Site Request Forgery)攻击是指攻击者伪造用户请求执行恶意操作。

2.1 使用 CSRF 令牌

在请求中加入 CSRF 令牌,确保请求来源合法。

1
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">

2.2 限制跨域请求

1
Access-Control-Allow-Origin: same-origin

3. 防止点击劫持(Clickjacking)

Clickjacking 是通过隐藏的 iframe 诱骗用户点击恶意链接。

3.1 禁止 iframe 嵌套

1
X-Frame-Options: DENY

3.2 使用 CSP 规则

1
Content-Security-Policy: frame-ancestors 'none';

4. 数据安全与存储保护

4.1 避免在前端存储敏感信息

错误示例(在 localStorage 存储密码)

1
localStorage.setItem("password", "123456"); // ❌ 高风险!

正确示例(仅存储 Token,敏感数据存后端)

1
localStorage.setItem("authToken", token);

4.2 使用 HTTPS 加密传输

1
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

5. 结论

前端开发者应掌握 XSS、CSRF、Clickjacking 等安全攻击的防范措施,并通过 CSP、CSRF 令牌、X-Frame-Options 等安全策略来提高 Web 应用的安全性。

Vue 3 新特性与最佳实践

Posted on 2024-06-24 | In Vue3

Vue3 作为 Vue.js 的重大升级,带来了 Composition API、Teleport、Fragments、Suspense 等一系列新特性。本文将详细介绍 Vue 3 的新特性,并结合代码示例,帮助开发者更高效地构建 Vue 应用。

1. Composition API

Vue 3 引入了 Composition API,使代码更具可复用性和可读性。

1.1 setup 函数

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { ref } from 'vue';

const count = ref(0);
const increment = () => {
count.value++;
};
</script>

<template>
<button @click="increment">Count: {{ count }}</button>
</template>

1.2 reactive vs ref

1
2
3
4
5
6
<script setup>
import { ref, reactive } from 'vue';

const count = ref(0);
const state = reactive({ count: 0 });
</script>

1.3 如何组织逻辑代码

使用 Composition API 可以将不同的逻辑抽离为独立的函数模块,例如:

1
2
3
4
5
6
7
8
// useCounter.ts
import { ref } from 'vue';

export function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}
1
2
3
4
5
6
7
8
9
<script setup>
import { useCounter } from './useCounter';

const { count, increment } = useCounter();
</script>

<template>
<button @click="increment">Count: {{ count }}</button>
</template>

2. Teleport 组件

Teleport 允许将子组件渲染到 DOM 的其他位置。

1
2
3
4
5
<template>
<teleport to="body">
<div class="modal">This is a modal</div>
</teleport>
</template>

使用场景

  • 弹窗(Modal)
  • 提示框(Tooltip)
  • 全局层级组件(如 Loading Spinner)

3. Suspense 组件

用于异步组件的加载。

1
2
3
4
5
6
7
8
9
10
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>

注意事项

  • 只适用于 <script setup> 或函数式组件中的异步组件
  • Suspense 的 fallback 不应太复杂,以避免性能问题

4. Fragments 组件

支持多个根节点。

1
2
3
4
5
6
<template>
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
</template>

小提示

Fragments 是编译器层面支持,最终仍会转成普通元素包裹,因此对样式布局基本无影响。

5. 其他增强

更快的虚拟 DOM

Vue 3 在底层对 Virtual DOM 进行了重写,整体性能提升约 1.5~2 倍。

更好的 TypeScript 支持

Vue 3 完全用 TypeScript 重写,支持 IDE 自动提示和类型推导,更适合大型项目。

更轻量的打包体积

Tree-shaking 支持让 Vue 3 的打包更小,更精简。

6. 结论

Vue 3 通过 Composition API、Teleport、Suspense 等特性提升了代码组织能力和性能。掌握这些新特性,将让你的 Vue 开发更加高效。

前端性能优化:提升 Web 应用速度的关键策略

Posted on 2024-06-14 | In 性能优化

前端性能优化是提升用户体验、减少加载时间和提高转化率的关键环节。本文将深入探讨优化 Web 应用性能的核心策略,并提供相应的代码示例。

1. 资源优化

1.1 代码分割(Code Splitting)

代码分割可以减少首屏加载时间,按需加载所需的代码。

在 React 中使用动态导入

1
2
3
4
5
6
7
8
9
10
11
12
import React, { Suspense, lazy } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
export default App;

在 Webpack 中配置代码分割

1
2
3
4
5
6
7
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
};

1.2 压缩 JavaScript 和 CSS

使用工具如 Terser、CSSNano 进行代码压缩,以减少传输大小。

1
npm install terser cssnano --save-dev

1.3 资源懒加载(Lazy Loading)

在图片加载时使用 loading="lazy" 进行懒加载。

1
<img src="image.jpg" loading="lazy" alt="Lazy Loaded Image" />

2. 网络优化

2.1 启用 HTTP/2 或 HTTP/3

HTTP/2 和 HTTP/3 可以显著提升资源加载速度。确保服务器支持这些协议。

2.2 预加载和预获取资源

  • preload 适用于关键资源,如字体或脚本。
  • prefetch 用于提前加载未来可能使用的资源。
1
2
<link rel="preload" href="styles.css" as="style">
<link rel="prefetch" href="next-page.js">

2.3 使用 Service Worker 进行缓存

1
2
3
4
5
6
7
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});

3. 渲染优化

3.1 避免不必要的重排(Reflow)和重绘(Repaint)

尽量减少 DOM 操作,并使用 requestAnimationFrame 进行动画。

1
2
3
requestAnimationFrame(() => {
element.style.transform = 'translateX(100px)';
});

3.2 使用 Virtual DOM(React)

React 内部优化了 DOM 操作,使用 useMemo 和 useCallback 进行优化。

1
const memoizedValue = useMemo(() => computeExpensiveValue(data), [data]);

3.3 服务器端渲染(SSR)

使用 Next.js 进行服务器端渲染,加快首屏加载。

1
2
3
4
export async function getServerSideProps() {
const data = await fetchData();
return { props: { data } };
}

4. 结论

通过代码分割、懒加载、网络优化和渲染优化等手段,可以显著提升 Web 应用的性能。合理使用这些优化策略,将让你的 Web 应用运行得更快,提供更好的用户体验。

TypeScript 在前端项目中的最佳实践

Posted on 2024-05-18 | In TypeScript

TypeScript(TS)已经成为现代前端开发的首选语言之一,它提供了强类型检查、增强的代码可读性以及更好的工具支持。本文将介绍 TypeScript 在前端项目中的最佳实践,并提供相应的代码示例,帮助开发者更高效地编写可维护的代码。

1. TypeScript 的优势

  • 静态类型检查:在编译阶段捕获错误,减少运行时错误。
  • 更好的代码提示:借助 IDE(如 VS Code),提供智能补全和类型推导。
  • 更强的可维护性:强类型系统让代码更加可读,降低 bug 率。
  • 良好的 JavaScript 兼容性:可以逐步引入 TypeScript,不影响现有 JavaScript 代码。

2. TypeScript 基本语法

2.1 类型注解

1
2
3
let message: string = "Hello, TypeScript!";
let count: number = 10;
let isDone: boolean = false;

2.2 接口(Interface)

接口用于定义对象的结构,有助于提高代码的可维护性。

1
2
3
4
5
6
7
8
9
10
interface User {
id: number;
name: string;
age?: number; // 可选属性
}

const user: User = {
id: 1,
name: "Alice",
};

2.3 类型别名(Type Alias)

类型别名与接口类似,但可以用于更复杂的类型组合。

1
2
type ID = string | number;
type Status = "active" | "inactive" | "banned";

2.4 泛型(Generics)

泛型提供了更强的类型复用能力。

1
2
3
4
5
function identity<T>(value: T): T {
return value;
}

let output = identity<string>("Hello");

3. TypeScript 在 React 项目中的应用

3.1 使用 Props 和 State

1
2
3
4
5
6
7
8
type ButtonProps = {
label: string;
onClick: () => void;
};

const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};

3.2 使用 useState 和 useEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState, useEffect } from "react";

const Counter = () => {
const [count, setCount] = useState<number>(0);

useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};

4. TypeScript 在 Vue 3 项目中的应用

Vue 3 完全支持 TypeScript,结合 defineProps 和 defineEmits 使得开发更加类型安全。

4.1 Vue 组件使用 TypeScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts">
import { ref } from "vue";

type Props = {
message: string;
};

defineProps<Props>();

const count = ref<number>(0);
</script>

<template>
<div>
<p>{{ message }}</p>
<p>Count: {{ count }}</p>
<button @click="count++">Increment</button>
</div>
</template>

5. TypeScript 最佳实践

5.1 启用严格模式

在 tsconfig.json 中启用严格模式,以确保类型安全。

1
2
3
4
5
6
7
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}

5.2 避免使用 any

使用 any 可能会导致类型安全性降低,推荐使用 unknown 或更具体的类型。

1
2
3
4
5
function processInput(input: unknown) {
if (typeof input === "string") {
console.log(input.toUpperCase());
}
}

5.3 使用 Readonly 保护数据

1
2
3
4
5
6
interface Config {
readonly apiUrl: string;
}

const config: Config = { apiUrl: "https://api.example.com" };
// config.apiUrl = "https://new-api.com"; // ❌ 错误

5.4 使用 Record 构造对象类型

1
2
3
4
5
type Role = "admin" | "user";
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
user: ["read"]
};

5.5 使用 Pick 和 Omit 进行类型裁剪

1
2
3
4
5
6
7
8
9
interface User {
id: number;
name: string;
email: string;
password: string;
}

type PublicUser = Omit<User, "password">;
type UserCredentials = Pick<User, "email" | "password">;

6. 结论

TypeScript 在前端开发中的应用越来越广泛,它提供了更好的类型安全性、代码维护性和开发体验。通过掌握 TypeScript 的核心概念,并在 React 和 Vue 项目中合理使用,可以显著提升项目的质量和可维护性。

React 18 新特性与最佳实践

Posted on 2024-05-12 | In React

React 18 是 React 生态中的一个重要版本,带来了许多优化和新特性,尤其是在并发渲染、自动批量更新和新的 Hooks 方面。本文将深入解析 React 18 的新功能,并提供实际代码示例,帮助开发者更好地利用这些特性。

1. 并发渲染(Concurrent Rendering)

并发渲染是 React 18 最重要的更新之一,它使得 React 能够更好地调度 UI 更新,提高应用的流畅度。

1.1 startTransition API

startTransition 允许开发者将某些状态更新标记为“过渡”,从而避免阻塞关键 UI 更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState, startTransition } from 'react';

function TransitionExample() {
const [text, setText] = useState('');
const [searchResults, setSearchResults] = useState([]);

const handleChange = (e) => {
setText(e.target.value);
startTransition(() => {
// 模拟搜索结果更新
setSearchResults(new Array(10000).fill(e.target.value));
});
};

return (
<div>
<input type="text" value={text} onChange={handleChange} />
<ul>
{searchResults.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}

1.2 useDeferredValue

useDeferredValue 允许延迟计算某个状态,以防止界面卡顿。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useState, useDeferredValue } from 'react';

function DeferredExample() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);

return (
<div>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<p>Deferred Value: {deferredText}</p>
</div>
);
}

2. 自动批量更新(Automatic Batching)

React 18 之前,React 只会在事件处理函数中进行批量更新,而在 setTimeout、Promise 或者 fetch 回调中,状态更新不会自动合并。但在 React 18 中,所有的状态更新都会被自动批量处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useState, useEffect } from 'react';

function BatchingExample() {
const [count, setCount] = useState(0);
const [text, setText] = useState('Hello');

useEffect(() => {
setTimeout(() => {
setCount((c) => c + 1);
setText('World');
}, 1000);
}, []);

return (
<div>
<p>Count: {count}</p>
<p>Text: {text}</p>
</div>
);
}

3. 新的 React Hook

3.1 useId

useId 允许在无障碍(a11y)场景中生成唯一 ID。

1
2
3
4
5
6
7
8
9
10
11
import { useId } from 'react';

function Form() {
const id = useId();
return (
<div>
<label htmlFor={id}>Name: </label>
<input id={id} type="text" />
</div>
);
}

4. 结论

React 18 带来了许多优化和新特性,使得开发更加高效。本文介绍了并发渲染、自动批量更新以及新 Hooks 的使用。希望这些示例能帮助你更好地理解和应用 React 18 的新特性。

Vue 3 深入解析:Composition API vs Options API

Posted on 2024-05-03 | In Vue3

Vue 3 引入了 Composition API,它与 Options API 在代码组织和逻辑复用上有很大不同。本篇文章将深入对比这两种 API,分析它们的优缺点,并通过代码示例展示如何在实际项目中应用它们。

1. 什么是 Options API?

Options API 是 Vue 2 及 Vue 3 仍然支持的一种组件编写方式,它通过 data、methods、computed 和 watch 等选项组织代码。

1.1 Options API 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<p>计数器:{{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>

<script>
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
};
</script>

1.2 Options API 的优点

  • 代码结构清晰,适合小型组件。
  • 对 Vue 2 用户友好,迁移成本低。
  • 逻辑通过选项(如 data、methods)组织,易于理解。

1.3 Options API 的缺点

  • 逻辑复用不够灵活,通常需要 mixins 或 Vuex。
  • 复杂组件中,逻辑分散在多个选项中,导致代码难以维护。

2. 什么是 Composition API?

Composition API 允许开发者通过 setup 函数和 Vue 的内置 Hooks 来管理组件逻辑。

2.1 Composition API 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<p>计数器:{{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>

<script>
import { ref } from 'vue';

export default {
setup() {
const count = ref(0);
const increment = () => count.value++;

return { count, increment };
}
};
</script>

2.2 Composition API 的优点

  • 逻辑组织更加灵活,可复用性强。
  • 适用于大型应用,逻辑集中在 setup,更易维护。
  • 更好地支持 TypeScript。

2.3 Composition API 的缺点

  • 语法相对复杂,学习成本较高。
  • 需要理解 reactive、ref、computed 等概念。

3. 逻辑复用对比

在 Options API 中,逻辑复用通常依赖 mixins,但 mixins 可能导致变量命名冲突。

3.1 Options API 的 mixins

1
2
3
4
5
6
7
8
9
10
11
12
<script>
export const counterMixin = {
data() {
return { count: 0 };
},
methods: {
increment() {
this.count++;
}
}
};
</script>
1
2
3
4
5
6
<script>
import { counterMixin } from './counterMixin';
export default {
mixins: [counterMixin]
};
</script>

在 Composition API 中,逻辑复用可以通过 Composable 函数来实现。

3.2 Composition API 的 Composable

1
2
3
4
5
6
7
// useCounter.js
import { ref } from 'vue';
export function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}
1
2
3
4
5
6
7
8
<script>
import { useCounter } from './useCounter';
export default {
setup() {
return useCounter();
}
};
</script>

4. 何时使用 Composition API?

  • 需要高度复用逻辑的项目。
  • 组件逻辑较复杂时。
  • 使用 TypeScript 进行类型约束时。

5. 结论

Options API 适用于小型项目,而 Composition API 适用于大型项目。Vue 3 提供了更多的灵活性,使开发者可以根据需求选择最佳方式进行开发。

Vue Keep-alive中的钩子函数

Posted on 2022-05-05 | In Vue

–

说到Vue的钩子函数,可能很多人只停留在一些很简单常用的钩子(created,mounted),而且对于里面的区别,什么时候该用什么钩子,并没有仔细的去研究过,熟悉这些函数后对我们写逻辑时会更有帮助。下面就用代码示例简单介绍下这些钩子吧。


Vue-Router导航守卫:

有的时候,我们需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。

为此我们有很多种方法可以植入路由的导航过程:全局的, 单个路由独享的, 或者组件级的,推荐优先阅读路由文档

全局守卫

vue-router全局有三个守卫:

  1. router.beforeEach 全局前置守卫 进入路由之前
  2. router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
  3. router.afterEach 全局后置钩子 进入路由之后

使用方法:

    // main.js 入口文件
    import router from './router'; // 引入路由
    router.beforeEach((to, from, next) => { 
      next();
    });
    router.beforeResolve((to, from, next) => {
      next();
    });
    router.afterEach((to, from) => {
      console.log('afterEach 全局后置钩子');
    });
复制代码

to,from,next 这三个参数:

to和from是将要进入和将要离开的路由对象,路由对象指的是平时通过this.$route获取到的路由对象。

next:Function 这个参数是个函数,且必须调用,否则不能进入路由(页面空白)。

  • next() 进入该路由。

  • next(false): 取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)。

  • next 跳转新路由,当前的导航被中断,重新开始一个新的导航。

      我们可以这样跳转:next('path地址')或者next({path:''})或者next({name:''})
      且允许设置诸如 replace: true、name: 'home' 之类的选项
      以及你用在router-link或router.push的对象选项。
    复制代码
    

路由独享守卫

如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫:

    const router = new VueRouter({
      routes: [
        {
          path: '/foo',
          component: Foo,
          beforeEnter: (to, from, next) => { 
            // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
            // ...
          }
        }
      ]
    })
复制代码

路由组件内的守卫:

  1. beforeRouteEnter 进入路由前
  2. beforeRouteUpdate (2.2) 路由复用同一个组件时
  3. beforeRouteLeave 离开当前路由时

文档中的介绍:

  beforeRouteEnter (to, from, next) {
    // 在路由独享守卫后调用 不!能!获取组件实例 `this`,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 `this`
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用,可以访问组件实例 `this`
  }
复制代码

beforeRouteEnter访问this

因为钩子在组件实例还没被创建的时候调用,所以不能获取组件实例 this,可以通过传一个回调给next来访问组件实例 。

但是回调的执行时机在mounted后面,所以在我看来这里对this的访问意义不太大,可以放在created或者mounted里面。

    beforeRouteEnter (to, from, next) {
    console.log('在路由独享守卫后调用');
      next(vm => {
        // 通过 `vm` 访问组件实例`this` 执行回调的时机在mounted后面,
      })
    }
复制代码

beforeRouteLeave:

导航离开该组件的对应路由时调用,我们用它来禁止用户离开,比如还未保存草稿,或者在用户离开前,将setInterval销毁,防止离开之后,定时器还在调用。

    beforeRouteLeave (to, from , next) {
      if (文章保存) {
        next(); // 允许离开或者可以跳到别的路由 上面讲过了
      } else {
        next(false); // 取消离开
      }
    }
复制代码

关于钩子的一些知识:

路由钩子函数的错误捕获

如果我们在全局守卫/路由独享守卫/组件路由守卫的钩子函数中有错误,可以这样捕获:

    router.onError(callback => { 
    // 2.4.0新增 并不常用,了解一下就可以了 
      console.log(callback, 'callback');
    });
复制代码

在路由文档中还有更多的实例方法:动态添加路由等,有兴趣可以了解一下。

跳转死循环,页面永远空白

我了解到的,很多人会碰到这个问题,来看一下这段伪代码:

    router.beforeEach((to, from, next) => {
      if(登录){
         next()
      }else{
          next({ name: 'login' }); 
      }
    });
复制代码

看逻辑貌似是对的,但是当我们跳转到login之后,因为此时还是未登录状态,所以会一直跳转到login然后死循环,页面一直是空白的,所以:我们需要把判断条件稍微改一下。

    if(登录 || to.name === 'login'){ next() } // 登录,或者将要前往login页面的时候,就允许进入路由
复制代码

全局后置钩子的跳转:

文档中提到因为router.afterEach不接受next函数所以也不会改变导航本身,意思就是只能当成一个钩子来使用,但是我自己在试的时候发现,我们可以通过这种形式来实现跳转:

    // main.js 入口文件
    import router from './router'; // 引入路由
    router.afterEach((to, from) => {
      if (未登录 && to.name !== 'login') {
        router.push({ name: 'login' }); // 跳转login
      }
    });
复制代码

额,通过router.beforeEach 也完全可以实现且更好,我就骚一下。

完整的路由导航解析流程(不包括其他生命周期):

  1. 触发进入其他路由。
  2. 调用要离开路由的组件守卫beforeRouteLeave
  3. 调用局前置守卫:beforeEach
  4. 在重用的组件里调用 beforeRouteUpdate
  5. 调用路由独享守卫 beforeEnter。
  6. 解析异步路由组件。
  7. 在将要进入的路由组件中调用beforeRouteEnter
  8. 调用全局解析守卫 beforeResolve
  9. 导航被确认。
  10. 调用全局后置钩子的 afterEach 钩子。
  11. 触发DOM更新(mounted)。
  12. 执行beforeRouteEnter 守卫中传给 next 的回调函数

你不知道的keep-alive[我猜你不知道]

在开发Vue项目的时候,大部分组件是没必要多次渲染的,所以Vue提供了一个内置组件keep-alive来缓存组件内部状态,避免重新渲染,文档在这里。

文档:和 <transition>相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。

用法:

缓存动态组件:

<keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们,此种方式并无太大的实用意义。

    <!-- 基本 -->
    <keep-alive>
      <component :is="view"></component>
    </keep-alive>

    <!-- 多个条件判断的子组件 -->
    <keep-alive>
      <comp-a v-if="a > 1"></comp-a>
      <comp-b v-else></comp-b>
    </keep-alive>
复制代码

缓存路由组件:

使用keep-alive可以将所有路径匹配到的路由组件都缓存起来,包括路由组件里面的组件,keep-alive大多数使用场景就是这种。

    <keep-alive>
        <router-view></router-view>
    </keep-alive>
复制代码

生命周期钩子:

这篇既然是Vue钩子函数的专场,那肯定要扣题呀~

在被keep-alive包含的组件/路由中,会多出两个生命周期的钩子:activated 与 deactivated。

文档:在 2.2.0 及其更高版本中,activated 和 deactivated 将会在 树内的所有嵌套组件中触发。

activated在组件第一次渲染时会被调用,之后在每次缓存组件被激活时调用。

activated调用时机:

第一次进入缓存路由/组件,在mounted后面,beforeRouteEnter守卫传给 next 的回调函数之前调用:

    beforeMount=> 如果你是从别的路由/组件进来(组件销毁destroyed/或离开缓存deactivated)=>
    mounted=> activated 进入缓存组件 => 执行 beforeRouteEnter回调
复制代码

因为组件被缓存了,再次进入缓存路由/组件时,不会触发这些钩子:

    // beforeCreate created beforeMount mounted 都不会触发。
复制代码

所以之后的调用时机是:

    组件销毁destroyed/或离开缓存deactivated => activated 进入当前缓存组件 
    => 执行 beforeRouteEnter回调
    // 组件缓存或销毁,嵌套组件的销毁和缓存也在这里触发
复制代码

deactivated:组件被停用(离开路由)时调用

使用了keep-alive就不会调用beforeDestroy(组件销毁前钩子)和destroyed(组件销毁),因为组件没被销毁,被缓存起来了。

这个钩子可以看作beforeDestroy的替代,如果你缓存了组件,要在组件销毁的的时候做一些事情,你可以放在这个钩子里。

如果你离开了路由,会依次触发:

    组件内的离开当前路由钩子beforeRouteLeave =>  路由前置守卫 beforeEach =>
    全局后置钩子afterEach => deactivated 离开缓存组件 => activated 进入缓存组件(如果你进入的也是缓存路由)
    // 如果离开的组件没有缓存的话 beforeDestroy会替换deactivated 
    // 如果进入的路由也没有缓存的话  全局后置钩子afterEach=>销毁 destroyed=> beforeCreate等
复制代码

那么,如果我只是想缓存其中几个路由/组件,那该怎么做?

缓存你想缓存的路由:

Vue2.1.0之前:

想实现类似的操作,你可以:

  1. 配置一下路由元信息

  2. 创建两个keep-alive标签

  3. 使用v-if通过路由元信息判断缓存哪些路由。

     <keep-alive>
         <router-view v-if="$route.meta.keepAlive">
             <!--这里是会被缓存的路由-->
         </router-view>
     </keep-alive>
     <router-view v-if="!$route.meta.keepAlive">
         <!--因为用的是v-if 所以下面还要创建一个未缓存的路由视图出口-->
     </router-view>
     //router配置
     new Router({
       routes: [
         {
           path: '/',
           name: 'home',
           component: Home,
           meta: {
             keepAlive: true // 需要被缓存
           }
         },
         {
           path: '/:id',
           name: 'edit',
           component: Edit,
           meta: {
             keepAlive: false // 不需要被缓存
           }
         }
       ]
     });
    复制代码
    

Vue2.1.0版本之后:

使用路由元信息的方式,要多创建一个router-view标签,并且每个路由都要配置一个元信息,是可以实现我们想要的效果,但是过于繁琐了点。

幸运的是在Vue2.1.0之后,Vue新增了两个属性配合keep-alive来有条件地缓存 路由/组件。

新增属性:

  • include:匹配的 路由/组件 会被缓存
  • exclude:匹配的 路由/组件 不会被缓存

include和exclude支持三种方式来有条件的缓存路由:采用逗号分隔的字符串形式,正则形式,数组形式。

正则和数组形式,必须采用v-bind形式来使用。

缓存组件的使用方式:

    <!-- 逗号分隔字符串 -->
    <keep-alive include="a,b">
      <component :is="view"></component>
    </keep-alive>

    <!-- 正则表达式 (使用 `v-bind`) -->
    <keep-alive :include="/a|b/">
      <component :is="view"></component>
    </keep-alive>

    <!-- 数组 (使用 `v-bind`) -->
    <keep-alive :include="['a', 'b']">
      <component :is="view"></component>
    </keep-alive>
复制代码

但更多场景中,我们会使用keep-alive来缓存路由:

<keep-alive include='a'>
    <router-view></router-view>
</keep-alive>
复制代码

匹配规则:

  1. 首先匹配组件的name选项,如果name选项不可用。
  2. 则匹配它的局部注册名称。 (父组件 components 选项的键值)
  3. 匿名组件,不可匹配。

比如路由组件没有name选项,并且没有注册的组件名。

  1. 只能匹配当前被包裹的组件,不能匹配更下面嵌套的子组件。

比如用在路由上,只能匹配路由组件的name选项,不能匹配路由组件里面的嵌套组件的name选项。

  1. 文档:<keep-alive>不会在函数式组件中正常工作,因为它们没有缓存实例。
  2. exclude的优先级大于include

也就是说:当include和exclude同时存在时,exclude生效,include不生效。

  <keep-alive include="a,b" exclude="a">
    <!--只有a不被缓存-->
    <router-view></router-view>
  </keep-alive>
复制代码

当组件被exclude匹配,该组件将不会被缓存,不会调用activated 和 deactivated。


组件生命周期钩子:

关于组件的生命周期,是时候放出这张图片了:

这张图片已经讲得很清楚了,很多人这部分也很清楚了,大部分生命周期并不会用到,这里提一下几点:

  1. ajax请求最好放在created里面,因为此时已经可以访问this了,请求到数据就可以直接放在data里面。

    这里也碰到过几次,面试官问:ajax请求应该放在哪个生命周期。

  2. 关于dom的操作要放在mounted里面,在mounted前面访问dom会是undefined。

  3. 每次进入/离开组件都要做一些事情,用什么钩子:

  • 不缓存:

    进入的时候可以用created和mounted钩子,离开的时候用beforeDestory和destroyed钩子,beforeDestory可以访问this,destroyed不可以访问this。

  • 缓存了组件:

    缓存了组件之后,再次进入组件不会触发beforeCreate、created 、beforeMount、 mounted,如果你想每次进入组件都做一些事情的话,你可以放在activated进入缓存组件的钩子中。

    同理:离开缓存组件的时候,beforeDestroy和destroyed并不会触发,可以使用deactivated离开缓存组件的钩子来代替。


触发钩子的完整顺序:

将路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件:

  1. beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
  2. beforeEach: 路由全局前置守卫,可用于登录验证、全局路由loading等。
  3. beforeEnter: 路由独享守卫
  4. beforeRouteEnter: 路由组件的组件进入路由前钩子。
  5. beforeResolve:路由全局解析守卫
  6. afterEach:路由全局后置钩子
  7. beforeCreate:组件生命周期,不能访问this。
  8. created:组件生命周期,可以访问this,不能访问dom。
  9. beforeMount:组件生命周期
  10. deactivated: 离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
  11. mounted:访问/操作dom。
  12. activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
  13. 执行beforeRouteEnter回调函数next。

结语

Vue提供了很多钩子,但很多钩子我们几乎不会用到,只有清楚这些钩子函数的触发顺序以及背后的一些限制等,这样我们才能够正确的使用这些钩子,希望看了本文的同学,能对这些钩子有更加清晰的认识,使用起来更加得心应手。

设计模式简介

Posted on 2022-05-03 | In Interview

设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。

设计模式原则

  • S – Single Responsibility Principle 单一职责原则
    • 一个程序只做好一件事
    • 如果功能过于复杂就拆分开,每个部分保持独立
  • O – OpenClosed Principle 开放/封闭原则
    • 对扩展开放,对修改封闭
    • 增加需求时,扩展新代码,而非修改已有代码
  • L – Liskov Substitution Principle 里氏替换原则
    • 子类能覆盖父类
    • 父类能出现的地方子类就能出现
  • I – Interface Segregation Principle 接口隔离原则
    • 保持接口的单一独立
    • 类似单一职责原则,这里更关注接口
  • D – Dependency Inversion Principle 依赖倒转原则
    • 面向接口编程,依赖于抽象而不依赖于具体
    • 使用方只关注接口而不关注具体类的实现
SO体现较多,举个栗子:(比如Promise)
  • 单一职责原则:每个then中的逻辑只做好一件事
  • 开放封闭原则(对扩展开放,对修改封闭):如果新增需求,扩展then
再举个栗子:(此例来源-守候-改善代码的各方面问题)
//checkType('165226226326','mobile')
//result:false
let checkType=function(str, type) {
    switch (type) {
        case 'email':
            return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str)
        case 'mobile':
            return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
        case 'tel':
            return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
        default:
            return true;
    }
}

复制代码

有以下两个问题:

  • 如果想添加其他规则就得在函数里面增加 case 。添加一个规则就修改一次!这样违反了开放-封闭原则(对扩展开放,对修改关闭)。而且这样也会导致整个 API 变得臃肿,难维护。
  • 比如A页面需要添加一个金额的校验,B页面需要一个日期的校验,但是金额的校验只在A页面需要,日期的校验只在B页面需要。如果一直添加 case 。就是导致A页面把只在B页面需要的校验规则也添加进去,造成不必要的开销。B页面也同理。

建议的方式是给这个 API 增加一个扩展的接口:

let checkType=(function(){
    let rules={
        email(str){
            return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
        },
        mobile(str){
            return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
        }
    };
    //暴露接口
    return {
        //校验
        check(str, type){
            return rules[type]?rules[type](str):false;
        },
        //添加规则
        addRule(type,fn){
            rules[type]=fn;
        }
    }
})();

//调用方式
//使用mobile校验规则
console.log(checkType.check('188170239','mobile'));
//添加金额校验规则
checkType.addRule('money',function (str) {
    return /^[0-9]+(.[0-9]{2})?$/.test(str)
});
//使用金额校验规则
console.log(checkType.check('18.36','money'));

复制代码

此例更详细内容请查看-> 守候i-重构-改善代码的各方面问题

设计模式分类(23种设计模式)

  • 创建型
    • 单例模式
    • 原型模式
    • 工厂模式
    • 抽象工厂模式
    • 建造者模式
  • 结构型
    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 外观模式
    • 桥接模式
    • 组合模式
    • 享元模式
  • 行为型
    • 观察者模式
    • 迭代器模式
    • 策略模式
    • 模板方法模式
    • 职责链模式
    • 命令模式
    • 备忘录模式
    • 状态模式
    • 访问者模式
    • 中介者模式
    • 解释器模式

工厂模式

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。

class Product {
    constructor(name) {
        this.name = name
    }
    init() {
        console.log('init')
    }
    fun() {
        console.log('fun')
    }
}

class Factory {
    create(name) {
        return new Product(name)
    }
}

// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()

复制代码
适用场景
  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
  • 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
  • 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性
优点
  • 创建对象的过程可能很复杂,但我们只需要关心创建结果。
  • 构造函数和创建者分离, 符合“开闭原则”
  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
缺点
  • 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
  • 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
什么时候不用

当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。

由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。

例子
  • 曾经我们熟悉的JQuery的$()就是一个工厂函数,它根据传入参数的不同创建元素或者去寻找上下文中的元素,创建成相应的jQuery对象

    class jQuery {

    constructor(selector) {
        super(selector)
    }
    add() {
    
    }
    

    // 此处省略若干API
    }

    window.$ = function(selector) {

    return new jQuery(selector)
    

    }

复制代码
  • vue 的异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

复制代码

单例模式

一个类只有一个实例,并提供一个访问它的全局访问点。

 class LoginForm {
    constructor() {
        this.state = 'hide'
    }
    show() {
        if (this.state === 'show') {
            alert('已经显示')
            return
        }
        this.state = 'show'
        console.log('登录框显示成功')
    }
    hide() {
        if (this.state === 'hide') {
            alert('已经隐藏')
            return
        }
        this.state = 'hide'
        console.log('登录框隐藏成功')
    }
 }
 LoginForm.getInstance = (function () {
     let instance
     return function () {
        if (!instance) {
            instance = new LoginForm()
        }
        return instance
     }
 })()

let obj1 = LoginForm.getInstance()
obj1.show()

let obj2 = LoginForm.getInstance()
obj2.hide()

console.log(obj1 === obj2)
复制代码

优点

  • 划分命名空间,减少全局变量
  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
  • 且只会实例化一次。简化了代码的调试和维护

缺点

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。

场景例子

  • 定义命名空间和实现分支型方法
  • 登录框
  • vuex 和 redux中的store

适配器模式

将一个类的接口转化为另外一个接口,以满足用户需求,使类之间接口不兼容问题通过适配器得以解决。

class Plug {
  getName() {
    return 'iphone充电头';
  }
}

class Target {
  constructor() {
    this.plug = new Plug();
  }
  getName() {
    return this.plug.getName() + ' 适配器Type-c充电头';
  }
}

let target = new Target();
target.getName(); // iphone充电头 适配器转Type-c充电头
复制代码

优点

  • 可以让任何两个没有关联的类一起运行。
  • 提高了类的复用。
  • 适配对象,适配库,适配数据

缺点

  • 额外对象的创建,非直接调用,存在一定的开销(且不像代理模式在某些功能点上可实现性能优化)
  • 如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,尽量把文档完善

场景

  • 整合第三方SDK
  • 封装旧接口

    // 自己封装的ajax, 使用方式如下
    ajax({

    url: '/getData',
    type: 'Post',
    dataType: 'json',
    data: {
        test: 111
    }
    

    }).done(function() {})
    // 因为历史原因,代码中全都是:
    // $.ajax({….})

    // 做一层适配器
    var $ = {

    ajax: function (options) {
        return ajax(options)
    }
    

    }
    复制代码

  • vue的computed

    <div id="example">
        <p>Original message: "{{ message }}"</p>  <!-- Hello -->
        <p>Computed reversed message: "{{ reversedMessage }}"</p>  <!-- olleH -->
    </div>
    

    复制代码

原有data 中的数据不满足当前的要求,通过计算属性的规则来适配成我们需要的格式,对原有数据并没有改变,只改变了原有数据的表现形式

不同点

适配器与代理模式相似

  • 适配器模式: 提供一个不同的接口(如不同版本的插头)
  • 代理模式: 提供一模一样的接口

装饰者模式

  • 动态地给某个对象添加一些额外的职责,,是一种实现继承的替代方案
  • 在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象

    class Cellphone {

    create() {
        console.log('生成一个手机')
    }
    

    }
    class Decorator {

    constructor(cellphone) {
        this.cellphone = cellphone
    }
    create() {
        this.cellphone.create()
        this.createShell(cellphone)
    }
    createShell() {
        console.log('生成手机壳')
    }
    

    }
    // 测试代码
    let cellphone = new Cellphone()
    cellphone.create()

    console.log(‘————‘)
    let dec = new Decorator(cellphone)
    dec.create()
    复制代码

场景例子

  • 比如现在有4 种型号的自行车,我们为每种自行车都定义了一个单

独的类。现在要给每种自行车都装上前灯、尾 灯和铃铛这3 种配件。如果使用继承的方式来给 每种自行车创建子类,则需要 4×3 = 12 个子类。 但是如果把前灯、尾灯、铃铛这些对象动态组 合到自行车上面,则只需要额外增加3 个类

  • ES7 Decorator 阮一峰
  • core-decorators

优点

  • 装饰类和被装饰类都只关心自身的核心业务,实现了解耦。
  • 方便动态的扩展功能,且提供了比继承更多的灵活性。

缺点

  • 多层装饰比较复杂。
  • 常常会引入许多小对象,看起来比较相似,实际功能大相径庭,从而使得我们的应用程序架构变得复杂起来

代理模式

是为一个对象提供一个代用品或占位符,以便控制对它的访问

假设当A 在心情好的时候收到花,小明表白成功的几率有

60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。 小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花 被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。 但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选 择A 心情好的时候把花转交给A,代码如下:

let Flower = function() {}
let xiaoming = {
  sendFlower: function(target) {
    let flower = new Flower()
    target.receiveFlower(flower)
  }
}
let B = {
  receiveFlower: function(flower) {
    A.listenGoodMood(function() {
      A.receiveFlower(flower)
    })
  }
}
let A = {
  receiveFlower: function(flower) {
    console.log('收到花'+ flower)
  },
  listenGoodMood: function(fn) {
    setTimeout(function() {
      fn()
    }, 1000)
  }
}
xiaoming.sendFlower(B)
复制代码

场景

  • HTML元 素事件代理


    • 1

    • 2

    • 3



    复制代码

  • ES6 的 proxy 阮一峰Proxy

  • jQuery.proxy()方法

优点

  • 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
  • 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;

缺点

处理请求速度可能有差别,非直接访问存在开销

不同点

装饰者模式实现上和代理模式类似

  • 装饰者模式: 扩展功能,原有功能不变且可直接使用
  • 代理模式: 显示原有功能,但是经过限制之后的

外观模式

为子系统的一组接口提供一个一致的界面,定义了一个高层接口,这个接口使子系统更加容易使用

  1. 兼容浏览器事件绑定

    let addMyEvent = function (el, ev, fn) {

    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn
    }
    

    };
    复制代码

  2. 封装接口

    let myEvent = {

    // ...
    stop: e => {
        e.stopPropagation();
        e.preventDefault();
    }
    

    };
    复制代码

场景

  • 设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade
  • 在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。
  • 在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。

参考: 大话设计模式

优点

  • 减少系统相互依赖。
  • 提高灵活性。
  • 提高了安全性

缺点

  • 不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

观察者模式

定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己,当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。

  • 发布 & 订阅
  • 一对多

    // 主题 保存状态,状态变化之后触发所有观察者对象
    class Subject {
    constructor() {

    this.state = 0
    this.observers = []
    

    }
    getState() {

    return this.state
    

    }
    setState(state) {

    this.state = state
    this.notifyAllObservers()
    

    }
    notifyAllObservers() {

    this.observers.forEach(observer => {
      observer.update()
    })
    

    }
    attach(observer) {

    this.observers.push(observer)
    

    }
    }

    // 观察者
    class Observer {
    constructor(name, subject) {

    this.name = name
    this.subject = subject
    this.subject.attach(this)
    

    }
    update() {

    console.log(`${this.name} update, state: ${this.subject.getState()}`)
    

    }
    }

    // 测试
    let s = new Subject()
    let o1 = new Observer(‘o1’, s)
    let o2 = new Observer(‘02’, s)

    s.setState(12)
    复制代码

场景

  • DOM事件

    document.body.addEventListener(‘click’, function() {

    console.log('hello world!');
    

    });
    document.body.click()
    复制代码

  • vue 响应式

优点

  • 支持简单的广播通信,自动通知所有已经订阅过的对象
  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
  • 增加了灵活性
  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。

缺点

过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解


状态模式

允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了它的类

// 状态 (弱光、强光、关灯)
class State {
    constructor(state) {
        this.state = state
    }
    handle(context) {
        console.log(`this is ${this.state} light`)
        context.setState(this)
    }
}
class Context {
    constructor() {
        this.state = null
    }
    getState() {
        return this.state
    }
    setState(state) {
        this.state = state
    }
}
// test 
let context = new Context()
let weak = new State('weak')
let strong = new State('strong')
let off = new State('off')

// 弱光
weak.handle(context)
console.log(context.getState())

// 强光
strong.handle(context)
console.log(context.getState())

// 关闭
off.handle(context)
console.log(context.getState())
复制代码

场景

  • 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
  • 一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态

优点

  • 定义了状态与行为之间的关系,封装在一个类里,更直观清晰,增改方便
  • 状态与状态间,行为与行为间彼此独立互不干扰
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然

缺点

  • 会在系统中定义许多状态类
  • 逻辑分散

迭代器模式

提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。

class Iterator {
    constructor(conatiner) {
        this.list = conatiner.list
        this.index = 0
    }
    next() {
        if (this.hasNext()) {
            return this.list[this.index++]
        }
        return null
    }
    hasNext() {
        if (this.index >= this.list.length) {
            return false
        }
        return true
    }
}

class Container {
    constructor(list) {
        this.list = list
    }
    getIterator() {
        return new Iterator(this)
    }
}

// 测试代码
let container = new Container([1, 2, 3, 4, 5])
let iterator = container.getIterator()
while(iterator.hasNext()) {
  console.log(iterator.next())
}
复制代码

场景例子

  • Array.prototype.forEach
  • jQuery中的$.each()
  • ES6 Iterator

特点

  • 访问一个聚合对象的内容而无需暴露它的内部表示。
  • 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作

总结

对于集合内部结果常常变化各异,不想暴露其内部结构的话,但又想让客户代码透明的访问其中的元素,可以使用迭代器模式


桥接模式

桥接模式(Bridge)将抽象部分与它的实现部分分离,使它们都可以独立地变化。

class Color {
    constructor(name){
        this.name = name
    }
}
class Shape {
    constructor(name,color){
        this.name = name
        this.color = color 
    }
    draw(){
        console.log(`${this.color.name} ${this.name}`)
    }
}

//测试
let red = new Color('red')
let yellow = new Color('yellow')
let circle = new Shape('circle', red)
circle.draw()
let triangle = new Shape('triangle', yellow)
triangle.draw()

复制代码

优点

  • 有助于独立地管理各组成部分, 把抽象化与实现化解耦
  • 提高可扩充性

缺点

  • 大量的类将导致开发成本的增加,同时在性能方面可能也会有所减少。

组合模式

  • 将对象组合成树形结构,以表示“整体-部分”的层次结构。
  • 通过对象的多态表现,使得用户对单个对象和组合对象的使用具有一致性。

    class TrainOrder {

    create () {
        console.log('创建火车票订单')
    }
    

    }
    class HotelOrder {

    create () {
        console.log('创建酒店订单')
    }
    

    }

    class TotalOrder {

    constructor () {
        this.orderList = []
    }
    addOrder (order) {
        this.orderList.push(order)
        return this
    }
    create () {
        this.orderList.forEach(item => {
            item.create()
        })
        return this
    }
    

    }
    // 可以在购票网站买车票同时也订房间
    let train = new TrainOrder()
    let hotel = new HotelOrder()
    let total = new TotalOrder()
    total.addOrder(train).addOrder(hotel).create()
    复制代码

场景

  • 表示对象-整体层次结构
  • 希望用户忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象(方法)

缺点

如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。


原型模式

原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。

class Person {
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
class Student extends Person {
  constructor(name) {
    super(name)
  }
  sayHello() {
    console.log(`Hello, My name is ${this.name}`)
  }
}

let student = new Student("xiaoming")
student.sayHello()
复制代码

原型模式,就是创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复的对象,带来性能上的提升。


策略模式

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

<html>
<head>
    <title>策略模式-校验表单</title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">
        用户名:<input type="text" name="userName">
        密码:<input type="text" name="password">
        手机号码:<input type="text" name="phoneNumber">
        <button type="submit">提交</button>
    </form>
    <script type="text/javascript">
        // 策略对象
        const strategies = {
          isNoEmpty: function (value, errorMsg) {
            if (value === '') {
              return errorMsg;
            }
          },
          isNoSpace: function (value, errorMsg) {
            if (value.trim() === '') {
              return errorMsg;
            }
          },
          minLength: function (value, length, errorMsg) {
            if (value.trim().length < length) {
              return errorMsg;
            }
          },
          maxLength: function (value, length, errorMsg) {
            if (value.length > length) {
              return errorMsg;
            }
          },
          isMobile: function (value, errorMsg) {
            if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
              return errorMsg;
            }                
          }
        }

        // 验证类
        class Validator {
          constructor() {
            this.cache = []
          }
          add(dom, rules) {
            for(let i = 0, rule; rule = rules[i++];) {
              let strategyAry = rule.strategy.split(':')
              let errorMsg = rule.errorMsg
              this.cache.push(() => {
                let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg)
                return strategies[strategy].apply(dom, strategyAry)
              })
            }
          }
          start() {
            for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
              let errorMsg = validatorFunc()
              if (errorMsg) {
                return errorMsg
              }
            }
          }
        }

        // 调用代码
        let registerForm = document.getElementById('registerForm')

        let validataFunc = function() {
          let validator = new Validator()
          validator.add(registerForm.userName, [{
            strategy: 'isNoEmpty',
            errorMsg: '用户名不可为空'
          }, {
            strategy: 'isNoSpace',
            errorMsg: '不允许以空白字符命名'
          }, {
            strategy: 'minLength:2',
            errorMsg: '用户名长度不能小于2位'
          }])
          validator.add(registerForm.password, [ {
            strategy: 'minLength:6',
            errorMsg: '密码长度不能小于6位'
          }])
          validator.add(registerForm.phoneNumber, [{
            strategy: 'isMobile',
            errorMsg: '请输入正确的手机号码格式'
          }])
          return validator.start()
        }

        registerForm.onsubmit = function() {
          let errorMsg = validataFunc()
          if (errorMsg) {
            alert(errorMsg)
            return false
          }
        }
    </script>
</body>
</html>
复制代码

场景例子

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的’行为’,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  • 一个系统需要动态地在几种算法中选择一种。
  • 表单验证

优点

  • 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
  • 提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
  • 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案

缺点

  • 会在程序中增加许多策略类或者策略对象
  • 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy

享元模式

运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式

let examCarNum = 0         // 驾考车总数
/* 驾考车对象 */
class ExamCar {
    constructor(carType) {
        examCarNum++
        this.carId = examCarNum
        this.carType = carType ? '手动档' : '自动档'
        this.usingState = false    // 是否正在使用
    }

    /* 在本车上考试 */
    examine(candidateId) {
        return new Promise((resolve => {
            this.usingState = true
            console.log(`考生- ${ candidateId } 开始在${ this.carType }驾考车- ${ this.carId } 上考试`)
            setTimeout(() => {
                this.usingState = false
                console.log(`%c考生- ${ candidateId } 在${ this.carType }驾考车- ${ this.carId } 上考试完毕`, 'color:#f40')
                resolve()                       // 0~2秒后考试完毕
            }, Math.random() * 2000)
        }))
    }
}

/* 手动档汽车对象池 */
ManualExamCarPool = {
    _pool: [],                  // 驾考车对象池
    _candidateQueue: [],        // 考生队列

    /* 注册考生 ID 列表 */
    registCandidates(candidateList) {
        candidateList.forEach(candidateId => this.registCandidate(candidateId))
    },

    /* 注册手动档考生 */
    registCandidate(candidateId) {
        const examCar = this.getManualExamCar()    // 找一个未被占用的手动档驾考车
        if (examCar) {
            examCar.examine(candidateId)           // 开始考试,考完了让队列中的下一个考生开始考试
              .then(() => {
                  const nextCandidateId = this._candidateQueue.length && this._candidateQueue.shift()
                  nextCandidateId && this.registCandidate(nextCandidateId)
              })
        } else this._candidateQueue.push(candidateId)
    },

    /* 注册手动档车 */
    initManualExamCar(manualExamCarNum) {
        for (let i = 1; i <= manualExamCarNum; i++) {
            this._pool.push(new ExamCar(true))
        }
    },

    /* 获取状态为未被占用的手动档车 */
    getManualExamCar() {
        return this._pool.find(car => !car.usingState)
    }
}

ManualExamCarPool.initManualExamCar(3)          // 一共有3个驾考车
ManualExamCarPool.registCandidates([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])  // 10个考生来考试
复制代码

场景例子

  • 文件上传需要创建多个文件实例的时候
  • 如果一个应用程序使用了大量的对象,而这些大量的对象造成了很大的存储开销时就应该考虑使用享元模式

优点

  • 大大减少对象的创建,降低系统的内存,使效率提高。

缺点

  • 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,

不应该随着内部状态的变化而变化,否则会造成系统的混乱

模板方法模式

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法和封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

class Beverage {
    constructor({brewDrink, addCondiment}) {
        this.brewDrink = brewDrink
        this.addCondiment = addCondiment
    }
    /* 烧开水,共用方法 */
    boilWater() { console.log('水已经煮沸=== 共用') }
    /* 倒杯子里,共用方法 */
    pourCup() { console.log('倒进杯子里===共用') }
    /* 模板方法 */
    init() {
        this.boilWater()
        this.brewDrink()
        this.pourCup()
        this.addCondiment()
    }
}
/* 咖啡 */
const coffee = new Beverage({
     /* 冲泡咖啡,覆盖抽象方法 */
     brewDrink: function() { console.log('冲泡咖啡') },
     /* 加调味品,覆盖抽象方法 */
     addCondiment: function() { console.log('加点奶和糖') }
})
coffee.init() 
复制代码

场景例子

  • 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
  • 子类中公共的行为应被提取出来并集中到一个公共父类中的避免代码重复

优点

  • 提取了公共代码部分,易于维护

缺点

  • 增加了系统复杂度,主要是增加了的抽象类和类间联系

职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止

// 请假审批,需要组长审批、经理审批、总监审批
class Action {
    constructor(name) {
        this.name = name
        this.nextAction = null
    }
    setNextAction(action) {
        this.nextAction = action
    }
    handle() {
        console.log( `${this.name} 审批`)
        if (this.nextAction != null) {
            this.nextAction.handle()
        }
    }
}

let a1 = new Action("组长")
let a2 = new Action("经理")
let a3 = new Action("总监")
a1.setNextAction(a2)
a2.setNextAction(a3)
a1.handle()
复制代码

场景例子

  • JS 中的事件冒泡
  • 作用域链
  • 原型链

优点

  • 降低耦合度。它将请求的发送者和接收者解耦。
  • 简化了对象。使得对象不需要知道链的结构
  • 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任
  • 增加新的请求处理类很方便。

缺点

  • 不能保证某个请求一定会被链中的节点处理,这种情况可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
  • 使程序中多了很多节点对象,可能再一次请求的过程中,大部分的节点并没有起到实质性的作用。他们的作用仅仅是让请求传递下去,从性能当面考虑,要避免过长的职责链到来的性能损耗。

命令模式

将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。

// 接收者类
class Receiver {
    execute() {
      console.log('接收者执行请求')
    }
  }

// 命令者
class Command {  
    constructor(receiver) {
        this.receiver = receiver
    }
    execute () {    
        console.log('命令');
        this.receiver.execute()
    }
}
// 触发者
class Invoker {   
    constructor(command) {
        this.command = command
    }
    invoke() {   
        console.log('开始')
        this.command.execute()
    }
}

// 仓库
const warehouse = new Receiver();   
// 订单    
const order = new Command(warehouse);  
// 客户
const client = new Invoker(order);      
client.invoke()
复制代码

优点

  • 对命令进行封装,使命令易于扩展和修改
  • 命令发出者和接受者解耦,使发出者不需要知道命令的具体执行过程即可执行

缺点

  • 使用命令模式可能会导致某些系统有过多的具体命令类。

备忘录模式

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。

//备忘类
class Memento{
    constructor(content){
        this.content = content
    }
    getContent(){
        return this.content
    }
}
// 备忘列表
class CareTaker {
    constructor(){
        this.list = []
    }
    add(memento){
        this.list.push(memento)
    }
    get(index){
        return this.list[index]
    }
}
// 编辑器
class Editor {
    constructor(){
        this.content = null
    }
    setContent(content){
        this.content = content
    }
    getContent(){
     return this.content
    }
    saveContentToMemento(){
        return new Memento(this.content)
    }
    getContentFromMemento(memento){
        this.content = memento.getContent()
    }
}

//测试代码

let editor = new Editor()
let careTaker = new CareTaker()

editor.setContent('111')
editor.setContent('222')
careTaker.add(editor.saveContentToMemento())
editor.setContent('333')
careTaker.add(editor.saveContentToMemento())
editor.setContent('444')

console.log(editor.getContent()) //444
editor.getContentFromMemento(careTaker.get(1))
console.log(editor.getContent()) //333

editor.getContentFromMemento(careTaker.get(0))
console.log(editor.getContent()) //222
复制代码

场景例子

  • 分页控件
  • 撤销组件

优点

  • 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态

缺点

  • 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。

中介者模式

解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的 相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知 中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者 模式使网状的多对多关系变成了相对简单的一对多关系(类似于观察者模式,但是单向的,由中介者统一管理。)

class A {
    constructor() {
        this.number = 0
    }
    setNumber(num, m) {
        this.number = num
        if (m) {
            m.setB()
        }
    }
}
class B {
    constructor() {
        this.number = 0
    }
    setNumber(num, m) {
        this.number = num
        if (m) {
            m.setA()
        }
    }
}
class Mediator {
    constructor(a, b) {
        this.a = a
        this.b = b
    }
    setA() {
        let number = this.b.number
        this.a.setNumber(number * 10)
    }
    setB() {
        let number = this.a.number
        this.b.setNumber(number / 10)
    }
}

let a = new A()
let b = new B()
let m = new Mediator(a, b)
a.setNumber(10, m)
console.log(a.number, b.number)
b.setNumber(10, m)
console.log(a.number, b.number)
复制代码

场景例子

  • 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象
  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。

优点

  • 使各对象之间耦合松散,而且可以独立地改变它们之间的交互
  • 中介者和对象一对多的关系取代了对象之间的网状多对多的关系
  • 如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码

缺点

  • 系统中会新增一个中介者对象,因 为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。

解释器模式

给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。

此例来自心谭博客

class Context {
    constructor() {
      this._list = []; // 存放 终结符表达式
      this._sum = 0; // 存放 非终结符表达式(运算结果)
    }

    get sum() {
      return this._sum;
    }
    set sum(newValue) {
      this._sum = newValue;
    }
    add(expression) {
      this._list.push(expression);
    }
    get list() {
      return [...this._list];
    }
  }

  class PlusExpression {
    interpret(context) {
      if (!(context instanceof Context)) {
        throw new Error("TypeError");
      }
      context.sum = ++context.sum;
    }
  }
  class MinusExpression {
    interpret(context) {
      if (!(context instanceof Context)) {
        throw new Error("TypeError");
      }
      context.sum = --context.sum;
    }
  }

  /** 以下是测试代码 **/
  const context = new Context();

  // 依次添加: 加法 | 加法 | 减法 表达式
  context.add(new PlusExpression());
  context.add(new PlusExpression());
  context.add(new MinusExpression());

  // 依次执行: 加法 | 加法 | 减法 表达式
  context.list.forEach(expression => expression.interpret(context));
  console.log(context.sum);
复制代码

优点

  • 易于改变和扩展文法。
  • 由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法

缺点

  • 执行效率较低,在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度慢
  • 对于复杂的文法比较难维护

访问者模式

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

// 访问者  
class Visitor {
    constructor() {}
    visitConcreteElement(ConcreteElement) {
        ConcreteElement.operation()
    }
}
// 元素类  
class ConcreteElement{
    constructor() {
    }
    operation() {
       console.log("ConcreteElement.operation invoked");  
    }
    accept(visitor) {
        visitor.visitConcreteElement(this)
    }
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
element.accept(visitor)
复制代码

场景例子

  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,也不希望在增加新操作时修改这些类。

优点

  • 符合单一职责原则
  • 优秀的扩展性
  • 灵活性

缺点

  • 具体元素对访问者公布细节,违反了迪米特原则
  • 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
  • 具体元素变更比较困难

React Hooks使用案例学习

Posted on 2022-03-03 | In React

清除 effect

通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。也就是说,要想在组件销毁的时候搞一些事情,需要useEffect 末尾返回一个函数,在这个函数里面可以写具体销毁的内容。

看下面的例子,在当前页面里面,页面的标题是’测试title’,当切换到其他页面时,页面的标题变成‘正式title’。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import React, { useEffect } from 'react';

function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
return () => {
console.log('销毁1————————————————');
document.title = '正式title';
};
}, [title]);
}

export default function CheckboxDemo() {
useDocumentTitle('测试title');

return <div />;
}

监听页面大小变化,网络是否断开

效果:在组件调用 useWindowSize 时,可以拿到页面大小,并且在浏览器缩放时自动触发组件更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

import React, { useEffect, useState } from 'react';

function getSize() {
return {
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
outerHeight: window.outerHeight,
outerWidth: window.outerWidth,
};
}

function useWindowSize() {
const [windowSize, setWindowSize] = useState(getSize());

function handleResize() {
setWindowSize(getSize());
}

useEffect(() => {
window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return windowSize;
}

export default function Demo() {
const windowSize = useWindowSize();
return <div>页面宽度{windowSize.innerWidth}</div>;
}

动态注入 css

效果:在页面注入一段 class,并且当组件销毁时,移除这个 class。

1
2
3
4
5
6

const className = useCss({
color: "red"
});

return <div className={className}>Text.</div>;

实现:可以看到,Hooks 方便的地方是在组件销毁时移除副作用,所以我们可以安心的利用 Hooks 做一些副作用。注入 css 自然不必说了,而销毁 css 只要找到注入的那段引用进行销毁即可。

react-redux

Posted on 2020-02-10 | In React

rudux

redux 运行流程图:

简单概述:click -> store.dispatch(action) -> reduer -> newState -> viewUpdate

react-readux 中 通过 connect 链接组件和 redux , this.props.dispatch() 调用

后面将会讲到…

redux 依赖包也是十分的简洁

先来个demo

Read more »

react - hooks(v16.7)

Posted on 2020-02-08 | In React

前言

本文不做概念性的解析,旨在实操 hooks,相关资源可以自行谷歌。以下提供相关参考资料:

  • Introducing Hooks
  • 理解 React Hooks
  • React Hooks 实用指南 - 大都借鉴这篇文章
  • Hooks 一览
Read more »

react - 生命周期

Posted on 2020-02-05 | In React

v16.3+

  • Mounting
    • constructor(props)
    • static getDerivedStateFromProps(props, state)
    • render()
    • componentDidMount()
  • Updating
    • static getDerivedStateFromProps()
    • shouldComponentUpdate(nextProps, nextState)
    • render()
    • getSnapshotBeforeUpdate(prevProps, prevState)
    • componentDidUpdate(prevProps, prevState, snapshot)
Read more »

react-context

Posted on 2020-02-03 | In React

简单使用

Context 设计目的是为共享那些被认为对于一个组件树而言是“全局”的数据,你可以看做为 redux,因为 redux 也是通过这个东东实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { Component } from 'react'

/**
* 1. 创建 context
* 2. 根组件 App 包裹 MyContext.Provider
* 3. App => Father => Child => MyContext.Consumer => context.age 取出结果
*/
const MyContext = React.createContext()

const Child = () => (
<MyContext.Consumer>{({ age }) => <p>My age is {age}</p>}</MyContext.Consumer>
)

const Father = () => <Child />

class App extends Component {
render() {
return (
<MyContext.Provider value={{ age: 22 }}>
<Father />
</MyContext.Provider>
)
}
}

export default App
Read more »

react 入门

Posted on 2020-02-02 | In React
1
2
3
4
5
6
7
8
9
10
11
12
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- 生产环境中不建议使用 -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>

<div id="example"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
</script>
  • react.min.js - React 的核心库
  • react-dom.min.js - 提供与 DOM 相关的功能
  • babel.min.js - Babel 可以将 ES6 代码转为 ES5 代码
Read more »

git 实用指南

Posted on 2020-01-23 | In 开发工具

commit 规范速查

  • feat:新功能(feature)
  • fix:修补 bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改 bug 的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动
  • revert: 撤销以前的 commit

    1
    revert: feat(pencil): add 'graphiteWidth' option
Read more »

HTTP - 缓存机制

Posted on 2020-01-23 | In HTTP

缓存实现的步骤

  • 首先是当用户请求资源时,会判断是否有缓存,如果没有,则会向原服务器请求资源。
  • 如果有缓存,则会进入强缓存的范畴,判断缓存是否新鲜
    • 如果缓存新鲜,则会直接返回缓存副本给客户端。
    • 如果缓存不新鲜了,则表示强缓存失败,将会进入到协商缓存。
  • 协商缓存将判断是否存在 Etag 和 Last-Modified 首部
    • 如果未发生变化,则表示命中了协商缓存,会重定向到缓存副本,将资源返回给客户端
    • 否则的话表示协商缓存未命中,服务器会返回新的资源。

强缓存

服务端告知客户端缓存时间后,由客户端判断并决定是否使用缓存。

强缓存是通过 Expires 首部或 Cache-Control: max-age 来实现的。

  • Expires: 响应头,代表该资源的过期时间。
  • Cache-Control: 请求/响应头,缓存控制字段,精确控制缓存策略。
Read more »
12
weifo

weifo

行到水穷处,坐看云起时

37 posts
17 categories
19 tags
RSS
GitHub
© 2025 weifo
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4