浏览器的安全性

浏览器安全性是前端开发中的重要考察点之一,主要指浏览器在访问网站过程中如何防止攻击者利用漏洞或机制实施攻击、窃取数据、破坏用户体验等。 一、常见的浏览器安全威胁 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

跨域通信

什么是跨域 在了解跨域之前,我们必须要了解一下同源策略。 跨域问题其实就是浏览器的同源策略造成的。 同源策略 同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议、端口号、域名必须一致。 跨源资源共享([CORS],或通俗地译为跨域资源共享)是一种基于 [HTTP] 头的机制,该机制通过允许服务器标示除了它自己以外的其他[源] (域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。 为什么会产生跨域 下表给出了与 URL wmq.com/dir/page.ht… 的源进行对比的示例: URL 是否跨域 原因 wmq.com/dir/page.ht… 同源 完全相同 wmq.com/dir/inner/a… 同源 只有路径不同 wmq.com/secure.html 跨域 协议不同 wmq.com:81/dir/etc.htm… 跨域 端口不同 ( http:// 默认端口是80) news.company.com/dir/other.h… 跨域 主机不同 同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致。 同源政策主要限制了三个方面: 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。 当前域下 ajax 无法发送跨域请求。 源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。 有哪些解决跨域的办法 CORS CORS: 通过设置服务器端的响应头来允许跨域请求。这需要在服务器端进行配置,以允许特定的来源访问资源。例如,在Node.js的Express框架中,可以使用以下代码来设置CORS: const express = require('express'); const app = express(); app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); // 允许所有来源访问 res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); // 允许的请求头 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); // 允许的请求方法 next(); }); // 其他路由和中间件 app.listen(3000, () => { console.log('Server is running on port 3000'); }); 减少OPTIONS请求次数 在跨域请求中,浏览器会自动处理一些非简单请求的预检流程,这包括"预检请求"(也被称为CORS预检请求)和"OPTIONS请求"。预检请求是发起一个HTTP OPTIONS请求到服务器,以确认目标资源是否支持跨域。这种机制是为了兼容同源策略而产生的,但有时这种额外的请求会导致性能问题。 ...

October 14, 2024

Grid布局

Grid 布局是什么? Grid 布局即网格布局,是一种新的 CSS 布局模型,比较擅长将一个页面划分为几个主要区域,以及定义这些区域的大小、位置、层次等关系。号称是最强大的的 CSS 布局方案,是目前唯一一种 CSS 二维布局。利用 Grid 布局,我们可以轻松实现类似下图布局,演示地址 Grid 布局和 flex 布局 讲到布局,我们就会想到 flex 布局,甚至有人认为竟然有 flex 布局了,似乎没有必要去了解 Grid 布局。但 flex 布局和 Grid 布局有实质的区别,那就是 flex 布局是一维布局,Grid 布局是二维布局。flex 布局一次只能处理一个维度上的元素布局,一行或者一列。Grid 布局是将容器划分成了“行”和“列”,产生了一个个的网格,我们可以将网格元素放在与这些行和列相关的位置上,从而达到我们布局的目的。 Grid 布局远比 flex 布局强大! flex布局示例: Grid 布局示例: Grid 的一些基础概念 我们使用 Grid 实现一个小例子,演示 Grid 的一些基础概念,演示地址 <div class="wrapper"> <div class="one item">One</div> <div class="two item">Two</div> <div class="three item">Three</div> <div class="four item">Four</div> <div class="five item">Five</div> <div class="six item">Six</div> </div> .wrapper { margin: 60px; /* 声明一个容器 */ display: grid; /* 声明列的宽度 */ grid-template-columns: repeat(3, 200px); /* 声明行间距和列间距 */ grid-gap: 20px; /* 声明行的高度 */ grid-template-rows: 100px 200px; } .one { background: #19CAAD; } .two { background: #8CC7B5; } .three { background: #D1BA74; } .four { background: #BEE7E9; } .five { background: #E6CEAC; } .six { background: #ECAD9E; } .item { text-align: center; font-size: 200%; color: #fff; } ...

October 8, 2024

vue事件处理

一、DOM 事件流 想必看过《红宝书》的前端 er 对标题都不会特别陌生,我们来看看定义:当一个 HTML 元素产生一个事件时,该事件会在树形结构的 DOM 上面沿着元素节点路径进行传播,事件所经过的路径结点都会收到该事件,这个传播过程可称为 DOM 事件流。 二、DOM 事件流模型 DOM 事件流分为 捕获型事件流 和 冒泡型事件流。两种事件流分别对应三阶段 DOM 事件流模型中的捕获阶段和冒泡阶段: 捕获阶段:事件从最外面的祖先节点依次传递到最里面的后代节点 目标阶段:真正的目标节点正在处理事件的阶段 冒泡阶段:事件又从最里面的后代节点逐层传出到最外面的祖先节点 三、Vue 之事件处理 Vue 中提供了事件绑定的语法糖,我们可以很简单地在标签中直接使用 @click="handleClick($event)" 就可以绑定点击事件。 而且在 Vue 里面,Vue 的事件触发默认为冒泡过程监听,意思就是上面 @click="handleClick($event)" 中点击事件的执行是冒泡过程触发的。 四、Vue 之事件修饰符 4.1 .capture 捕获监听器 <div @click="log(1)" @click.capture="log(1)" style="background-color: #00f"> <div @click="log(2)" @click.capture="log(2)" style="background-color: #66f"> <div @click="log(3)" @click.capture="log(3)" style="background-color: #ccf"> <div @click="log(4)" @click.capture="log(4)" style="background-color: #fff"> 点击这里 </div> </div> </div> </div> 控制台打印结果为:1 2 3 4 4 3 2 1 ...

October 5, 2024

Map与Set

每当去学习ES6的时候,我都会不由自主的打开阮一峰老师的《ES6入门》去学习和查找用法,相信大多数同学也都和我一样看过阮老师写的文章。 当然大家也都知道ES6里常用的API就那么些,不多不少,用在项目中也是刚刚好。 不过在每次读到Set和Map数据结构那一章的时候,总是有点不知所措,因为我不明白实现这样的数据结构,目的是什么,意义又是什么呢 Set和Map主要的应用场景在于数组去重和数据存储,幸运的是在读了关于数据结构和算法之类的书籍后,恍然大悟的发现 原来Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构 那么下面就来了解下这两种数据结构,最后来亲手实现的一个ES6中的Set和Map吧 集合 集合是由一组无序且唯一(即不能重复)的项组成的,可以想象成集合是一个既没有重复元素,也没有顺序概念的数组 ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值 Set 本身是一个构造函数,用来生成 Set 数据结构 这里说的Set其实就是我们所要讲到的集合,先来看下基础用法 const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); // 2 3 5 4 } // 去除数组的重复成员 let array = [1,2,1,4,5,3]; [...new Set(array)] // [1, 2, 4, 5, 3] 具体用法如果还有不清楚的,这里我会在后面一一细说。现在还是来看一下以ES6中Set类(数据结构)为基础实现的集合吧 Set实例的属性和方法 Set的属性: size:返回集合所包含元素的数量 Set的方法: 操作方法 add(value):向集合添加一个新的项 delete(value):从集合中移除一个值 has(value):如果值在集合中存在,返回true,否则false clear(): 移除集合里所有的项 遍历方法 keys():返回一个包含集合中所有键的数组 values():返回一个包含集合中所有值的数组 entries:返回一个包含集合中所有键值对的数组(感觉没什么用就不实现了) forEach():用于对集合成员执行某种操作,没有返回值 创建一个集合 function Set(arr = []) { // 可以传入数组 let items = {}; this.size = 0; // 记录集合中成员的数量 } module.exports = Set; 这里用{}对象来表示集合,也是因为对象不允许一个键指向两个不同的属性,保证了集合里的元素都是唯一的 ...

October 2, 2024

高阶函数

高阶函数是指那些可以接受函数作为参数或者返回函数作为结果的函数。这种特性使得JavaScript的函数变得异常灵活和强大,为开发者提供了丰富的工具和手段来编写更加简洁、可重用和模块化的代码。 本文将详细介绍JavaScript高阶函数的概念、特性、常见用法以及在实际开发中的应用。 一、高阶函数的概念 高阶函数是函数式编程中的一个核心概念。在JavaScript中,函数是一等公民,这意味着函数可以像其他数据类型(如数字、字符串、对象等)一样被传递和操作。高阶函数正是利用了这一特性,使得函数可以作为参数传递给其他函数,或者作为其他函数的返回值。 二、高阶函数的特性 函数作为参数:高阶函数可以接受一个或多个函数作为参数。这使得我们可以将特定的行为(即函数)作为参数传递给高阶函数,从而实现更加灵活和可重用的代码。 函数作为返回值:高阶函数可以返回一个函数作为结果。这种特性允许我们创建可以动态生成新函数的函数,这些新函数可以根据需要执行不同的行为。 闭包:闭包是高阶函数的一个重要特性。闭包允许函数访问其外部作用域中的变量,即使该函数在其外部作用域之外执行。这使得高阶函数可以捕获和存储状态,从而创建更加复杂和强大的函数行为。 三、高阶函数的常见用法 数组方法:JavaScript中的数组方法(如map、filter、reduce等)都是高阶函数的典型例子。这些方法接受一个函数作为参数,并对数组中的每个元素执行该函数,从而返回一个新的数组或单一结果。 map方法:用于创建一个新数组,其结果是该数组中的每个元素调用一个提供的函数后返回的结果。 filter方法:创建一个新数组,其包含通过所提供函数实现的测试的所有元素。 reduce方法:对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。 函数组合:函数组合是指将多个函数组合在一起形成一个新的函数。通过高阶函数,我们可以轻松地将多个函数组合在一起,从而创建一个更加复杂和强大的函数。这种技术在实际开发中非常有用,特别是当我们需要处理一系列转换或操作时。 柯里化:柯里化是一种将接收多个参数的函数转换为一系列接收单一参数的函数的技术。虽然柯里化本身不是高阶函数,但高阶函数在实现柯里化时扮演着重要角色。通过高阶函数,我们可以创建一个接受原始函数作为参数的柯里化函数,并返回一个新的函数来处理单一参数。 创建高阶函数:我们可以编写自己的高阶函数来实现特定的行为。例如,我们可以编写一个高阶函数来接受一个函数和一个数组作为参数,并返回该数组中应用该函数后的结果。 四、高阶函数在实际开发中的应用 1. 数据转换 高阶函数非常适合用于数据的转换和映射。例如,我们可以使用Array.prototype.map方法将一个数组中的每个元素转换为另一种形式。 // 定义一个高阶函数,它接受一个转换函数和一个数组作为参数 const transformArray = (transformFn, array) => array.map(transformFn); // 定义一个简单的转换函数,将数字转换为它的平方 const square = x => x * x; // 使用高阶函数和转换函数来处理数组 const numbers = [1, 2, 3, 4, 5]; const squaredNumbers = transformArray(square, numbers); console.log(squaredNumbers); // 输出: [1, 4, 9, 16, 25] 在这个例子中,transformArray是一个高阶函数,它接受一个转换函数transformFn和一个数组array作为参数。然后,它使用Array.prototype.map方法来遍历数组,并对每个元素应用转换函数。 2. 事件处理 在Web开发中,事件处理是一个常见的任务。通过高阶函数,我们可以创建更加灵活和可重用的事件处理器。 // 定义一个高阶函数,它接受一个事件处理函数和一个事件类型作为参数 const createEventHandler = (handlerFn, eventType) => event => { if (event.type === eventType) { handlerFn(event); } }; // 定义一个简单的事件处理函数,用于打印事件信息 const logEvent = event => console.log(`Event type: ${event.type}, Target: ${event.target}`); // 创建一个点击事件处理器 const handleClick = createEventHandler(logEvent, 'click'); // 假设我们有一个DOM元素,我们可以为它添加事件监听器 // const button = document.querySelector('button'); // button.addEventListener('click', handleClick); // 为了演示,我们直接调用处理器并传递一个模拟的事件对象 const mockEvent = { type: 'click', target: document.body }; handleClick(mockEvent); // 输出: Event type: click, Target: [object HTMLBodyElement] 在这个例子中,createEventHandler是一个高阶函数,它接受一个事件处理函数handlerFn和一个事件类型eventType作为参数,并返回一个新的函数。这个新的函数会检查传入的事件对象的类型是否与指定的事件类型匹配,如果匹配,则调用事件处理函数。 ...

October 1, 2024

适配器模式

定义 适配器模式(Adapter Pattern)是一种结构型设计模式,它将一个类的接口转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作。 适配器模式的核心思想是:通过一个中间层(适配器)来使不兼容的接口能够一起工作,而无需修改原有代码。 为什么需要适配器模式 适配器模式要解决的核心问题是:让接口不兼容的类能够一起工作,而不需要修改原有代码。 问题场景:假设我们的App原来使用的是自己封装的网络库,现在想要切换到Alamofire,但项目中有大量地方直接使用了旧接口。 // 旧的网络接口 - 项目中到处都在使用 protocol OldNetworkClient { func get(url: String, callback: @escaping (Data?, Error?) -> Void) func post(url: String, body: Data?, callback: @escaping (Data?, Error?) -> Void) } // 新引入的Alamofire有完全不同的接口 // AF.request(url).response { response in ... } 最直接的方式是修改所有调用处: // 需要修改项目中所有使用旧接口的地方 // 从: oldClient.get(url: "https://api.example.com/users") { data, error in ... } // 改为: AF.request("https://api.example.com/users").response { response in ... } 这种方式有什么问题? 工作量巨大:需要修改项目中所有使用网络请求的地方 风险高:大量修改容易引入bug 难以回退:如果新库有问题,很难切换回旧实现 违反开闭原则:修改了大量现有代码 适配器模式的解决思路: 创建一个适配器,让新库"伪装"成旧接口,现有代码无需修改: // 适配器 - 让Alamofire适配旧接口 class AlamofireAdapter: OldNetworkClient { func get(url: String, callback: @escaping (Data?, Error?) -> Void) { // 内部使用Alamofire,但对外暴露旧接口 AF.request(url).response { response in callback(response.data, response.error) } } func post(url: String, body: Data?, callback: @escaping (Data?, Error?) -> Void) { AF.request(url, method: .post, parameters: nil, encoding: URLEncoding.default) .response { response in callback(response.data, response.error) } } } // 现有代码完全不需要修改 let client: OldNetworkClient = AlamofireAdapter() client.get(url: "https://api.example.com/users") { data, error in // 原有的处理逻辑保持不变 } 适配器模式的好处: ...

June 21, 2026

Swift 宏

Swift 5.9(WWDC 2023)引入的宏(Macro)是近几年语言层最大的一次能力扩展。它让 Swift 第一次具备了编译期代码生成的官方机制——你可以在源码层面写一段"能生成代码的代码",编译器会在类型检查前把它展开成真正的 Swift 源码,再走正常的编译流程。SwiftUI 的 #Preview、Observation 框架的 @Observable、SwiftData 的 @Model、Swift Testing 的 @Test / #expect、FoundationModels 的 @Generable / @Tool,全都是宏。 一、为什么要有宏 1.1 Swift 之前能做什么,又差在哪 在宏之前,Swift 已经有一整套"元编程"手段,但每一种都有明显的边界: 机制 做什么 局限 @propertyWrapper 给属性注入行为(如 @Published、@State) 只能包属性,不能生成方法、不能生成类型、不能访问其他成员 @resultBuilder 把一段 DSL 收集成表达式(如 SwiftUI 的 ViewBuilder) 只作用于闭包里,无法改类型定义 KeyPath + Mirror 运行时反射读写属性 只能读,类型安全差,性能差(见 iOS反射) Codable 等编译器魔法 自动合成 init(from:) / encode(to:) 只有编译器内部能写,开发者写不出同等能力的东西 Sourcery / SwiftGen 文本级代码生成 游离于编译器之外,无类型信息、无法访问 AST Objective-C #define 纯文本替换 没有作用域、没有类型、没有卫生(hygiene),对调试器和 IDE 极不友好 宏就是要一次性补齐这块能力:在保留类型安全与 IDE 体验的前提下,把"只有 Swift 编译器团队能写的代码生成"开放给所有开发者。 ...

May 4, 2026

神经网络原理

想象你手写了一个数字"3",然后拿给计算机看——它是怎么"认出"这是3而不是8的?这个看似简单的问题,背后隐藏着整个深度学习的核心思想。让我们从最直觉的方式开始,一层一层揭开神经网络的面纱。 一、从像素到数字:问题的本质 1.1 手写数字识别 假设我们有一张28x28像素的灰度手写数字图片。每个像素是一个0到1之间的数字——0代表全黑,1代表全白。这意味着一张图片其实就是一个包含784个数字的列表。 图片 → [0.0, 0.0, 0.12, 0.85, 0.93, ..., 0.0] (784个数字) 我们的目标是设计一个函数,输入这784个数字,输出10个数字,分别代表这张图是0~9每个数字的概率: f(784个像素值) → [P(0), P(1), P(2), ..., P(9)] 比如输入一张手写的"3",我们期望输出类似: [0.01, 0.02, 0.05, 0.89, 0.01, 0.01, 0.00, 0.01, 0.00, 0.00] ^^^^ "3"的概率最高 这就是神经网络要解决的问题。但关键问题是:这个函数 f 长什么样? 1.2 为什么传统编程行不通 你可能会想:能不能写一堆if-else规则?比如"如果上方有一个圆弧,下方也有一个圆弧,那就是3"。 问题在于:人类写字的方式千变万化。同一个"3",有人写得圆润,有人写得尖锐,有人偏左,有人偏右。你几乎不可能用明确的规则覆盖所有情况。 我们需要的是一种从数据中自动学习规则的方法——这就是神经网络。 二、神经网络的结构 2.1 神经元:最基本的单元 让我们从最简单的单元开始。一个神经元本质上就是一个持有数字的容器。这个数字叫做激活值(activation),取值范围通常在0到1之间。 你可以把它想象成一个灯泡:0表示完全熄灭,1表示完全点亮,中间的值表示不同亮度。 2.2 层:神经元的组织方式 神经网络把神经元组织成层(layer): graph LR subgraph 输入层 [输入层 - 784个神经元] I1[像素1] I2[像素2] I3[...] I784[像素784] end subgraph 隐藏层1 [隐藏层1 - 16个神经元] H1_1[h1] H1_2[h2] H1_3[...] H1_16[h16] end subgraph 隐藏层2 [隐藏层2 - 16个神经元] H2_1[h1] H2_2[h2] H2_3[...] H2_16[h16] end subgraph 输出层 [输出层 - 10个神经元] O0[0] O1[1] O2[...] O9[9] end I1 --> H1_1 I1 --> H1_2 I784 --> H1_16 H1_1 --> H2_1 H1_16 --> H2_16 H2_1 --> O0 H2_16 --> O9 输入层:784个神经元,每个对应一个像素值 隐藏层:中间的层(这里用2层,每层16个神经元) 输出层:10个神经元,对应数字0~9 为什么叫"隐藏层"?因为在训练数据中,我们只知道输入(像素)和输出(标签),中间层在"学什么"我们并不直接指定——它们是被自动学出来的。 ...

May 2, 2026

单页面架构

传统的多页面应用构建方式: 纯服务端渲染,前后端不分离,使用jsp,jade,’ejs’,’tempalte’等技术在后台先拼接成对应的HTML结构,然后转换成字符串,在每个对应的路由返回对应的数据(文件)即可 Jade模版服务端渲染,代码实现: const express= require('express') const app =express() const jade = require('jade') const result = *** const url path = *** const html = jade.renderFile(url, { data: result, urlPath })//传入数据给模板引擎 app.get('/',(req,res)=>{ res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回给客户端 }) //RestFul接口 app.listen(3000,err=>{ //do something }) 使用jQuery等传统库绘制的前端页面 传统前后端不分离,服务端渲染的优缺点: 优点: SEO友好,因为返回给前端的是渲染好的HTML结构,里面的内容都可以被爬虫抓取到。 对于一些应用性能等要求不高的项目,比如某个公司的静态网页,内容很少的情况下,直接一把梭就好,不用再搭建工程化的环境等 对于后端程序员(全干工程师)来说,不用去特意学习前端框架,公司也不用特意去招聘前端 兼容性好,传统服务端渲染多页面应用吐出来的都是字符串,HTML结构 缺点: 如果项目很大,不利于维护,据我所知,目前很多云计算公司,还有不少都是使用非单页面应用,例如一个几十万行的项目是用jQuery写的,如果注释和文档不是非常齐全,那么真的会无从下手 性能和用户体验,不能跟单页面应用相比 后期迭代,升级空间不大,目前大部分写得比较好的库,都建立vue,react等框架基础上,他们都有一套自己的运行机制,有自己的生命周期,并且不像传统的应用,还加上了一层虚拟DOM以及diff算法 现在类似Ant-Design-pro这样的开箱即用的库已经很多,单页面应用的学习和开发成本已经很低很低,如果还在使用传统的技术去开发新的应用,对于开发人员多内心来说也是一种折磨。 这里并不是说多页面应用不好,只能说各有各自的好,单页面应用如果通过大量的极致优化手段,是可以从不少方面跟原生一拼。 目前的单页面应用: 只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站 其实只有一个空的DIV标签,其他都是js动态生态的内容 单页面应用实现步骤: 代码实现: 首先是一个静态模板文件 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="root"></div> </body> <script> </script> </html> 在vue react框架的入口文件中指定对应的渲染元素: import React from 'react; import ReactDOM from 'react-dom'; ReactDOM.render( <App/>, document.querySelector("#root") ) 引入react-router或者 react-router-dom,dva等路由跳转的库 配置路由跳转 <HashRouter>//这里使用HashRouter <ErrorBoundary>//React错误边界 <Switch> <Route path="/login" component={Login} /> <Route path="/home" component={Home} /> <Route path="/" component={NotFound} />//404路由或者重定向都可以 </Switch> </ErrorBoundary> </HashRouter> 单页面应用所谓路由跳转,其实最终结果就是: 浏览器的url地址发生变化,但是其实并没有发送请求,也没有刷新整个页面 根据我们配置的路由信息,每次点击切换路由,会切换到不同的组件显示,类似于选项卡功能的实现,但是同时url地址栏会变化 分为HashRouter和BrowserRouter两种模式 自己实现一个粗略的路由跳转: 自己实现传统的Hash模式跳转: hash 就是指 url 后的 # 号以及后面的字符。例如www.baidu.com/#segmentfault,那么#segmentfault就是hash值 ...

August 7, 2025