forwardRef 该怎么用?

React.forwardRef 对于ref转发,官网是这样描述的 Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。 想详细了解的可以去看官方文档 Refs 转发 – React (docschina.org) 现在让我们直奔主题吧! React.forwardRef(render)的返回值是react组件,接收的参数是一个 render函数,函数签名为render(props, ref),第二个参数将其接受的 ref 属性转发到render返回的组件中。 这项技术并不常见,但在以下两种场景中特别有用: 转发 ref 到组件内部的DOM 节点上 在高阶组件中转发ref 转发 ref到组件内部的DOM节点 比如我们想要将一个组件内部的某个元素暴露出去, 就可以这么做 // App.js import React from 'react'; import Foo from './component/Foo'; export default class App extends React.Component { constructor(props) { super(props); this.state = {}; this.input = React.createRef(); // 1 // ↑5 } handleClick = (e) => { const input = this.input.current; // 6 console.log(input); console.log(input.value); input.focus(); } render() { return ( <> <button onClick={this.handleClick}>click to get value</button> {/*2*/} <Foo ref={this.input}/> </> ) } } // Foo.jsx import React from 'react'; // 3 const Foo = React.forwardRef((props, myRef) => { return ( <div> <p>....一些其他节点</p> {/*4*/} <input type="text" defaultValue='ref 成功转发到 Foo 组件内部的 input节点上' ref={myRef}/> <p>....一些其他节点</p> <p>....一些其他节点</p> </div> ); }); export default Foo; 仔细看代码中标记的数字,这是ref转发的流程: ...

December 17, 2024

react 状态提升

在开始介绍状态提升之前,我想先介绍一下 Prop Drilling(属性钻取)的概念。 什么是Prop Drilling ? 那么,什么是状态提升呢?我们都知道 React 中的prop drilling(属性钻取)。prop drilling意味着从父组件向子组件传递一些 props。通过这种方式,我们可以在不同的组件之间共享数据或函数。可以使用 prop 传递任何类型的数据。 在上面的示例代码中,你可以看到,Home 组件具有一个名为 name 的状态,并且她与子组件共用 name 变量。在 React 中,只能将数据从父组件传递到子组件。 什么是 State Lifting 一般来说,状态提升允许你将数据从子组件移动到父组件,与Props Drilling不同,你可以在父组件使用子组件的状态值。这是不是很疯狂? 在 React 生态中,我们知道数据只能从父组件流向子组件,但现在将学习如何让数据反过来流动。 但是,为什么要这么做? 我们可以在父组件中定义状态并将其以 prop 的方式传递给子组件,但是这么做有可能会导致性能损失和代码复杂度提升。在我之前的博客中有提到:保持状态本地化很重要。因为当状态更新时,组件以及该组件的子组件都会重新渲染。 此外对于代码复杂性,对于如 Modal 这样的组件,选择在子组件中声明状态而不是在父组件中声明状态,将能够使代码更易于阅读。使用这种方法,你不必在每个父组件中声明状态。 通常对应用程序中的大多数组件来说,这是不必要的。但是,对于某些类型的组件,特别是可重用组件,会有这种需求。下面描述一下最通用的场景。 多说无用,上代码: 例如,我们有一个常见的 Modal 组件,我们想要在 Modal 中保留可见性状态。当一些组件想要使用这个 Modal 时,通常会定义自己的可见状态,但是通过 State Lifting,这个步骤将不再必要。 在 上面的 Modal 组件中,做了如下操作: 首先,使用forwardRef执行 ref 转发,ref 转发是一种自动将 ref 通过组件传递给子组件的技术。 使用 useState 声明状态以控制Modal组件的展示或隐藏。 使用 React 内置的 useImperativeHandle hook 将属性或者方法暴露出去,这里我们添加了一些数据和函数供父组件使用。现在父组件可以直接调用子组件的show、hide、isVisble这些方法和属性。 ...

December 17, 2024

代码分割

什么是代码分割 代码分割是一种将代码分割成多个小块的方式,然后按需加载或并行加载所需的块的技术。代码分割可以用于减少应用程序的初始加载时间或将代码切割成可按需加载的块,从而减少应用程序所需的总体代码量。 为什么要使用代码分割 在大型应用程序中,将所有代码放在一个文件中会导致应用程序加载时间过长,影响用户体验。为了解决这个问题,我们需要将代码分割成小块,然后按需加载或并行加载所需的块。 代码分割的方式 1. 动态导入 import import()是 JavaScript 中的一个动态导入语法,它允许在运行时异步加载模块。它返回一个 Promise,该 Promise 在模块加载完成后被解析为一个包含导出内容的模块对象。 使用 import()的语法如下: import(modulePath) .then((module) => { // 使用导入的模块 }) .catch((error) => { // 处理错误 }); 在这个语法中,modulePath 是一个字符串,用于指定要加载的模块路径。它可以是相对路径或绝对路径,也可以是一个包名。 当调用 import(modulePath)时,它会返回一个 Promise。这个 Promise 会在模块加载完成后被解析为一个包含导出内容的模块对象。你可以使用.then()方法来处理解析后的模块对象,并在其中使用导入的模块。 如果模块加载失败,Promise 会被拒绝,并触发.catch()方法中的错误处理逻辑。 注意事项 import()只能在模块的顶层作用域中使用,不能在函数内部或条件语句中使用。这是因为 import()是静态解析的,它在代码加载时就会执行,而不是在运行时。 另外,import()可以与其他语法结合使用,例如动态模块路径和对象解构。 动态模块路径: const modulePath = "./myModule"; import(modulePath) .then((module) => { // 使用导入的模块 }) .catch((error) => { // 处理错误 }); 在这个示例中,modulePath 是一个变量,它的值在运行时确定。这样可以根据需要动态加载不同的模块。 对象解构: import("./myModule") .then(({ myFunction, myVariable }) => { // 使用导入的函数和变量 myFunction(); console.log(myVariable); }) .catch((error) => { // 处理错误 }); 2. React.lazy React.lazy 是 React 16.6 版本引入的一个特性,它可以让你以动态的方式进行代码拆分(code splitting)。通过 React.lazy,你可以延迟加载(lazy load)一个组件,只有在需要时才会加载该组件,从而提高应用程序的性能。 React.lazy 的用法如下: ...

December 17, 2024

性能优化

本文篇幅较长,将从 编译阶段 -> 路由阶段 -> 渲染阶段 -> 细节优化 -> 状态管理 -> 海量数据源,长列表渲染 方向分别加以探讨。 一 不能输在起跑线上,优化babel配置,webpack配置为项 1 真实项目中痛点 当我们用create-react-app或者webpack构建react工程的时候,有没有想过一个问题,我们的配置能否让我们的项目更快的构建速度,更小的项目体积,更简洁清晰的项目结构。 随着我们的项目越做越大,项目依赖越来越多,项目结构越来越来复杂,项目体积就会越来越大,构建时间越来越长,久而久之就会成了一个又大又重的项目,所以说我们要学会适当的为项目‘减负’,让项目不能输在起跑线上。 2 一个老项目 拿我们之前接触过的一个react老项目为例。我们没有用dva,umi快速搭建react,而是用react老版本脚手架构建的,这对这种老的react项目,上述的问题都会存在,下面让我们一起来看看。 我们首先看一下项目结构。 再看看构建时间。 为了方便大家看构建时间,我简单写了一个webpack,plugin ConsolePlugin ,记录了webpack在一次compilation所用的时间。 const chalk = require('chalk') /* console 颜色 */ var slog = require('single-line-log'); /* 单行打印 console */ class ConsolePlugin { constructor(options){ this.options = options } apply(compiler){ /** * Monitor file change 记录当前改动文件 */ compiler.hooks.watchRun.tap('ConsolePlugin', (watching) => { const changeFiles = watching.watchFileSystem.watcher.mtimes for(let file in changeFiles){ console.log(chalk.green('当前改动文件:'+ file)) } }) /** * before a new compilation is created. 开始 compilation 编译 。 */ compiler.hooks.compile.tap('ConsolePlugin',()=>{ this.beginCompile() }) /** * Executed when the compilation has completed. 一次 compilation 完成。 */ compiler.hooks.done.tap('ConsolePlugin',()=>{ this.timer && clearInterval( this.timer ) const endTime = new Date().getTime() const time = (endTime - this.starTime) / 1000 console.log( chalk.yellow(' 编译完成') ) console.log( chalk.yellow('编译用时:' + time + '秒' ) ) }) } beginCompile(){ const lineSlog = slog.stdout let text = '开始编译:' /* 记录开始时间 */ this.starTime = new Date().getTime() this.timer = setInterval(()=>{ text += '█' lineSlog( chalk.green(text)) },50) } } 构建时间如下: ...

December 17, 2024

浏览器的安全性

浏览器安全性是前端开发中的重要考察点之一,主要指浏览器在访问网站过程中如何防止攻击者利用漏洞或机制实施攻击、窃取数据、破坏用户体验等。 一、常见的浏览器安全威胁 1. XSS(跨站脚本攻击) 原理:攻击者注入恶意脚本到网页中,在用户浏览页面时执行。 危害:窃取 cookie、伪造操作、传播蠕虫。 防御: 对输出进行HTML转义; 使用 Content Security Policy(CSP); 严格控制用户输入(白名单); 使用框架自动防御(如 React 的 JSX 自动转义)。 2. CSRF(跨站请求伪造) 原理:用户登录目标网站后,被诱导访问恶意链接,触发网站上的有状态请求。 危害:修改密码、转账等敏感操作被伪造。 防御: 使用 CSRF Token; Referer 验证; SameSite Cookie 属性限制第三方请求。 3. 点击劫持(Clickjacking) 原理:攻击者在页面上嵌入透明 iframe,引诱用户点击。 防御: 禁止网页被嵌入 iframe:X-Frame-Options: DENY / SAMEORIGIN; 使用 CSP 中的 frame-ancestors 指定允许嵌入的来源。 4. 恶意文件上传 原理:上传可执行脚本,触发服务端或客户端执行。 防御: 严格限制文件类型与大小; 不在上传目录下执行脚本; 设置 CDN 或存储桶只读访问权限。 5. 恶意第三方脚本(供应链攻击) 原理:攻击者污染 CDN 或依赖源,注入恶意代码。 防御: 使用子资源完整性校验(Subresource Integrity, SRI); 只信任可靠的依赖源; 上线前锁定依赖版本。 二、浏览器原生安全机制 1. 同源策略(Same-Origin Policy) 限制不同源之间访问 Cookie、DOM、LocalStorage 等。 同源指:协议、域名、端口号都相同。 2. CORS(跨域资源共享) 浏览器通过预检请求和响应头判断是否允许跨域访问。 3. Content Security Policy(CSP) 通过设置 HTTP Header 控制资源加载策略,防止 XSS 和数据泄露。 示例: Content-Security-Policy: default-src 'self'; script-src 'self' https://trust.cdn.com; 4. HTTP-only & Secure Cookie HttpOnly: 防止 JavaScript 读取 Cookie; Secure: 仅在 HTTPS 下传输 Cookie; SameSite: 限制第三方请求携带 Cookie。 5. Sandbox(iframe 安全沙箱) <iframe sandbox> 属性限制 iframe 的行为; 可防止脚本执行、表单提交等危险操作。 三、前端开发中的安全实践 安全措施 说明 输入校验 客户端和服务端都要做,优先使用白名单策略 输出编码 HTML、JavaScript、URL 编码避免 XSS 注入 HTTPS 加密传输防止中间人攻击(MITM) 使用现代框架 React/Vue/Angular 等框架天然防御 XSS 限制权限 用户行为应严格授权与验证 CSP 策略 强制资源加载来源、禁止内联脚本 四、总结 浏览器安全是前端必须掌握的重要基础知识,核心目标是 防止前端受到攻击者控制或操纵。它涉及浏览器机制、HTTP协议、安全头部、数据验证等多个维度,需要前端开发者在日常开发中养成良好安全意识与编码习惯。 ...

December 17, 2024

Fragment 和 Portals

一、Portals 某些情况下,我们希望渲染的内容独立于父组件,甚至独立于当前挂在的DOM元素(默认都是挂载到id为root的DOM元素上的) Portal提供了一种将子节点渲染到存在于父组件以外的DOM节点的优秀的方案: ReactDOM.createPortal(child, container) 第一个参数:是任何可渲染的React子元素,例如一个元素、字符串或fragment 第二个参数:是一个DOM元素 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点: render() { // React 挂载了一个新的 div,并且把子元素渲染其中 return ( <div> {this.props.children} </div> ); } 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的: render() { // React 并没有创建一个新的 div。它只是把子元素渲染到 `domNode` 中。 // `domNode` 是一个可以在任何位置的有效 DOM 节点。 return ReactDOM.createPortal( this.props.children, domNode ); } 一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框 案例 比如将h2挂在到id为zs的节点下 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <div id="zs"></div> </body> </html> App.jsx ...

December 17, 2024

高阶组件

一 前言 React高阶组件(HOC),对于很多react开发者来说并不陌生,它是灵活使用react组件的一种技巧,高阶组件本身不是组件,它是一个参数为组件,返回值也是一个组件的函数。 高阶作用用于强化组件,复用逻辑,提升渲染性能等作用。高阶组件也并不是很难理解,其实接触过后还是蛮简单的,接下来将按照,高阶组件理解?,高阶组件具体怎么使用?应用场景, 高阶组件实践(源码级别) 为突破口,带大家详细了解一下高阶组件。 我们带着问题去开始今天的讨论: 1 什么是高阶组件,它解决了什么问题? 2 有几种高阶组件,它们优缺点是什么? 3 如何写一个优秀高阶组件? 4 hoc怎么处理静态属性,跨层级ref等问题? 5 高阶组件怎么控制渲染,隔离渲染? 6 高阶组件怎么监控原始组件的状态? … 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 二 全方位看高阶组件 1 几种包装强化组件的方式 ① mixin模式 原型图 老版本的react-mixins 在react初期提供一种组合方法。通过React.createClass,加入mixins属性,具体用法和vue 中mixins相似。具体实现如下。 const customMixin = { componentDidMount(){ console.log( '------componentDidMount------' ) }, say(){ console.log(this.state.name) } } const APP = React.createClass({ mixins: [ customMixin ], getInitialState(){ return { name:'alien' } }, render(){ const { name } = this.state return <div> hello ,world , my name is { name } </div> } }) 这种mixins只能存在createClass中,后来React.createClass连同mixins这种模式被废弃了。mixins会带来一些负面的影响。 ...

December 17, 2024

跨域资源共享(CORS)

跨域资源共享(CORS,Cross-Origin Resource Sharing)是浏览器用来放宽同源策略限制的一种机制,是前端跨域请求的主流解决方案。面试时考察点主要围绕它的原理、工作流程、配置细节及相关安全问题。 一、CORS 基础概念 作用:允许浏览器从不同源的服务器请求资源,突破同源策略限制。 原理:服务器通过设置特定的 HTTP 响应头,告诉浏览器允许跨域访问。 二、CORS 的关键响应头 头部 说明 Access-Control-Allow-Origin 指明允许访问的源,可以是具体域名(如 https://example.com)或 *(表示允许所有域访问) Access-Control-Allow-Methods 允许的请求方法(GET、POST、PUT、DELETE 等) Access-Control-Allow-Headers 允许请求携带的自定义头部字段 Access-Control-Allow-Credentials 是否允许携带 Cookie 或 HTTP 认证信息,值为 true 时允许 Access-Control-Max-Age 预检请求的结果缓存时间,单位秒 三、CORS 请求分类 1. 简单请求 满足以下条件,浏览器直接发请求: 请求方法是 GET、POST、HEAD 中之一。 请求头只包含简单头(如 Accept, Content-Type 仅限 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)。 不携带自定义 Cookie、认证信息。 2. 预检请求(Preflight) 当请求不满足简单请求条件时,浏览器先发送 OPTIONS 请求询问服务器是否允许该跨域请求。 服务器响应通过特定头部告诉浏览器是否放行。 四、CORS 工作流程 浏览器发送跨域请求。 如果是简单请求,直接带上请求头发送实际请求,服务器返回响应带有 CORS 相关头,浏览器决定是否允许。 如果是非简单请求,浏览器先发 OPTIONS 预检请求。 服务器响应预检,决定是否允许后,浏览器再发实际请求。 浏览器根据响应头决定是否允许前端访问响应内容。 五、携带 Cookie 的跨域请求 默认情况下,跨域请求不发送 Cookie。 前端请求时必须设置:xhr.withCredentials = true 或 fetch 的 credentials: 'include'。 服务器必须设置:Access-Control-Allow-Credentials: true。 Access-Control-Allow-Origin 不能使用 *,必须指定具体域名。 常见考点 CORS 是什么?为什么需要它? 同源策略和 CORS 的关系? 简单请求和预检请求的区别? 预检请求的触发条件有哪些? Access-Control-Allow-Origin 设置为 * 有什么限制? 如何支持带 Cookie 的跨域请求? CORS 配置中的常用响应头说明。 如何解决跨域请求失败问题? JSONP 和 CORS 的区别? 服务器如何配置支持 CORS?

December 17, 2024

浏览器的同源策略

概述 本文从以下三个问题,循序渐进,一步步拆解,何为浏览器的同源策略和跨域问题: 什么是同源策略和跨域问题? 怎样才算同源? 同源策略下会有什么限制? 什么是同源策略和跨域问题? 同源政策是 1995 年由 Netscape 公司(就是创造了 JS 的那家网景公司)引入浏览器的一种策略。 想要理解同源策略的话,可以先假设如果没有同源策略会发生什么事情。接下来我们通过一则恐怖故事代入理解一下没有同源策略的世界: 在某个深夜,小明独自一人在家浏览网页,无意间打开了一些特殊网站,而恰巧的是,小明之前刚好打开过某些重要支付平台的网站且还没关闭,这时特殊网站里面可能有一些特殊 JS 脚本,在偷偷获取小明支付平台上的信息,并且偷偷利用了这些信息再结合某些手段,成功把小明支付平台上的钱转走了,小明上完厕所回来后,突然收到支付平台转账成功的短信,然后然后然后瘫坐在地上,泣不成声,仰天长啸,大喊一声:“还我攒了半年的 9.99~” 恐怖故事听完了,我们来先上个概念,什么是同源策略? 借用 MDN 上的描述: 同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。 怎么理解这个概念呢?举个栗子,随着汽车数量的上升,现在很多城市都有各种限行政策,就拿广州来说,如果不是 粤A 的车牌进入城区的话会有开四停四的限行限制。 同源策略跟汽车限行政策有一点点像,同源策略规定在“同源”下的脚本、文档等资源访问不受限(有点像 粤A 牌的汽车在广州内行驶不会被限行),如果不是“同源”则无法互相访问脚本、文档等资源(非 粤A 牌的汽车在广州市区内受开四停四政策限制) 理解了同源策略后,对于跨域就很好理解了,同源策略是好,我们也需要同源策略,但有些时候我们确实需要不同源的两个网页互相读取信息,这是就产生了跨域问题。 额外补充,同源策略是浏览器的行为,是浏览器的行为,是浏览器的行为(对于这点的解析可以参考:同源策略下,服务器会收到浏览器的请求吗?) 怎样才算同源? 所谓“同源”,就是以下三者必须都相同: 协议相同 协议相同的两个域名:(都是 https 协议) https://juejin.cn https://juejin.cn 协议不同的两个域名:(一个是 http 协议,一个是 https 协议) http://juejin.cn https://juejin.cn 域名相同 域名相同的两个域名: https://juejin.cn https://juejin.cn 域名不同的两个域名:(一个是二级域名,一个是三级域名)(可能这个是最容易记错的) https://juejin.cn https://www.juejin.cn 端口相同 端口相同的两个域名: https://juejin.cn https://juejin.cn 端口不同的两个域名: https://juejin.cn https://juejin.cn:8081 再重复一遍, 同协议、同域名、同端口,才算同源 同协议、同域名、同端口,才算同源 同协议、同域名、同端口,才算同源 同源策略下会有什么限制? 在同源策略下,如果非同源,会有以下三个方面的行为受到限制: DOM 如果非同源,其 JS 脚本,不能互相对 DOM 进行读写操作 数据 如果非同源,不能互相读写 Cookies、LocalStorage、SessionStorage 和 IndexedDB 网络请求 如果非同源,不能通过 AJAX 的方式互相发送、接收数据 为了方便理解,接下来我们展开说说这三条规则 ↓↓↓ ...

December 17, 2024

受控组件和非受控组件

前端开发经常会涉及表单的处理,或者其他一些用于输入的组件,比如日历组件。 涉及到输入,就绕不开受控模式和非受控模式的概念。 什么是受控,什么是非受控呢? 想一下,改变表单值只有两种情况: 用户去改变 value 或者代码去改变 value。 如果不能通过代码改表单值 value,那就是非受控,也就是不受我们控制。 但是代码可以给表单设置初始值 defaultValue。 代码设置表单的初始 value,但是能改变 value 的只有用户,代码通过监听 onChange 来拿到最新的值,或者通过 ref 拿到 dom 之后读取 value。 这种就是非受控模式。 反过来,代码可以改变表单的 value,就是受控模式。 注意,value 和 defaultValue 不一样: defaultValue 会作为 value 的初始值,后面用户改变的是 value。 而一旦你给 input 设置了 value,那用户就不能修改它了,可以输入触发 onChange 事件,但是表单的值不会变。 用户输入之后在 onChange 事件里拿到输入,然后通过代码去设置 value。 这就是受控模式。 其实绝大多数情况下,非受控就可以了,因为我们只是要拿到用户的输入,不需要手动去修改表单值。 但有的时候,你需要根据用户的输入做一些处理,然后设置为表单的值,这种就需要受控模式。 或者你想同步表单的值到另一个地方的时候,类似 Form 组件,也可以用受控模式。 value 由用户控制就是非受控模式,由代码控制就是受控模式。 我们写代码试一下: npx create-vite 创建 vite + react 的项目。 去掉 main.tsx 的 index.css 和 StrictMode: ...

December 17, 2024