Web开发ABC
背景
在Web前端开发三大MVVM框架AngularJS、Vue、React加持下,前端开发的工作只需关注业务逻辑,无论是业务接口数据更新需要更新到UI,还是用户操作UI,需要更新数据到本地存储或后台接口,都为开发者节省了大量的时间。但是框架使用旧了,不可避免的产生一些新问题,比如我们工作中可能只需要一个简单的页面,用标准Web技术HTML、CSS、JS足以Cover。但是很多前端新人对Web开发原生技术似乎不了解,像我这种10年前端开发的从业人员也挺长时间没有更新Web开发原生技术的知识了。本着知其然也要知其所以然的原则,打算把MDN文档啃一啃,对Web开发原生知识查缺补漏一下,学习过程中会做一些笔记供自己回顾,所以促成了此文。
HTML
块级元素和内联元素(也可称为行内元素)
- 块级元素在页面中以块的形式展现。一个块级元素出现在它前面的内容之后的新行上。任何跟在块级元素后面的内容也会出现在新的行上。块级元素通常是页面上的结构元素。一个块级元素不会嵌套在一个内联元素里面,但它可能嵌套在另一个块级元素里面。
- 内联元素通常出现在块级元素中并环绕文档内容的一小部分,而不是一整个段落或者一组内容。内联元素不会导致文本换行。
响应式图片
- 分辨率切换:srcset 和 sizes?
<img srcset="elva-fairy-480w.jpg 480w, elva-fairy-800w.jpg 800w" sizes="(max-width: 600px) 480px, 800px" src="elva-fairy-800w.jpg" alt="Elva dressed as a fairy" />
- 美术设计:当你想为不同布局提供不同剪裁的图片
<picture> <source media="(max-width: 799px)" srcset="elva-480w-close-portrait.jpg" /> <source media="(min-width: 800px)" srcset="elva-800w.jpg" /> <img src="elva-800w.jpg" alt="Chris standing up holding his daughter Elva" /></picture>
CSS
层叠层
- 普通层、嵌套层和匿名层
@layer theme,layout,utilities;
/* 未分层的样式 */body { color: #333;}
/* 创建第一个层:`layout` */@layer layout { main { display: grid; }}
/* 创建第二个层:一个未命名的匿名层 */@layer { body { margin: 0; }}
/* 创建第三和第四个层:`theme` 和 `utilities` */@layer theme,layout,utilities;/* 向已经存在的 `layout` 层添加样式 */@layer layout { main { color: #000; }}
/* 创建第五个层:一个未命名的匿名层 */@layer { body { margin: 1vw; }}
在上面的 CSS 中,我们创建了五个层:layout、<anonymous(01)>、theme、utilities 和 <anonymous(02)>——按这个顺序——第六个隐含的未分层样式层包含在 body 样式块中。
- 使用 @import 将样式表导入具名层和匿名层
@import url("components-lib.css") layer(components);@import url("dialog.css") layer(components.dialog);@import url("marketing.css") layer();
JavaScript
暂时性死区
用 let、const 或 class 声明的变量可以称其从代码块的开始一直到代码执行到变量声明的位置并被初始化前,都处于一个“暂时性死区”(Temporal dead zone,TDZ)中。
语句和声明
- let、const、class、function、async function、function*、async function*、class、export、import以上是声明,其他都是语句。
- 你可以将声明看作“绑定标识符到值”的过程,而语句则是“执行操作”的过程。
var vs let
- let 声明的作用域是块或函数。
let x = 1;if (x === 1) { let x = 2; console.log(x); // Expected output: 2}console.log(x);// Expected output: 1
- let 声明的变量只能在执行到声明所在的位置之后才能被访问(参见暂时性死区)。因此,let 声明通常被视为是非提升的。
- let 声明在脚本的顶级作用域上声明变量时不会在全局对象上创建属性。
- let 声明的变量不能被同一个作用域中的任何其他声明重复声明。
- let 是声明,而不是语句的开头。这意味着,你不能将单独的 let 声明当做块的主体使用(因为这样做会让变量无法被访问)。
变量命名
- 小写驼峰命名法
算术运算符
7 ** 3 // 幂,相当于Math.pow(7, 3) ,% // 求余(或叫取模)
事件处理器
const controller = new AbortController();
btn.addEventListener("click", () => { const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`; document.body.style.backgroundColor = rndCol; }, { signal: controller.signal } // 向该处理器传递 AbortSignal);
controller.abort(); // 移除任何/所有与该控制器相关的事件处理器
事件监听器机制
- (推荐)使用 addEventListener() 来注册事件处理器。
- (不推荐)事件处理器属性,不能为一个事件添加一个以上的处理程序。
const btn = document.querySelector("button");btn.onclick= () => {};
- (不推荐)内联事件处理器
<button onclick="bgChange()">按下我</button>
JS对象
对象基础
- 对象是一个包含数据和方法集合(通常由一些变量和函数组成,称之为对象的属性和方法)
- 当对象的成员是函数时,语法会更简单。我们可以写 bio() 来代替 bio: function()。像这样,这种写法是ES6引入的
const person = { name: ["Bob", "Smith"], age: 32, bio() { console.log(`${this.name[0]} ${this.name[1]} 现在 ${this.age} 岁了。`); }, introduceSelf() { console.log(`你好!我是 ${this.name[0]}。`); },};
- 访问对象的属性两种方法,点表示法和括号表示法
JS类
常见的方式
class Person { name;
constructor(name) { this.name = name; }
introduceSelf() { console.log(`Hi! I'm ${this.name}`); }}
省略构造函数
- 如果你不需要任何特殊的初始化内容,你可以省略构造函数,默认的构造函数会被自动生成。
class Animal { sleep() { console.log("zzzzzzz"); }}const spot = new Animal();spot.sleep(); // 'zzzzzzz'
类中的私有属性和私有方法(ES2022中实现)
class Student extends Person { #year;
constructor(name, year) { super(name); this.#year = year; }
introduceSelf() { console.log(`Hi! I'm ${this.name}, and I'm in year ${this.#year}.`); this.#canStudyArchery(); }
#canStudyArchery() { return this.#year > 1; }}
从服务器获取数据
- 在早期,这种通用技术被称为异步的 JavaScript 与 XML(Ajax)。
浏览器存储
- sessionStorage 是浏览器标签页(tab页)维度的。
Web表单
input类型
<input type="email" id="email" name="email" /><input type="search" id="search" name="search" /><input type="tel" id="tel" name="tel" /><input type="url" id="url" name="url" /><input type="number" name="age" id="age" min="1" max="10" step="2" />
<label for="price">Choose a maximum house price: </label><input type="range" name="price" id="price" min="50000" max="500000" step="100" value="250000" /><output class="price-output" for="price"></output>
<input type="datetime-local" name="datetime" id="datetime" /><input type="color" name="color" id="color" />
其他类型
<select id="groups" name="groups"> <optgroup label="fruits"> <option>Banana</option> <option selected>Cherry</option> <option>Lemon</option> </optgroup> <optgroup label="vegetables"> <option>Carrot</option> <option>Eggplant</option> <option>Potato</option> </optgroup></select>
表单安全问题
- XSS 攻击利用用户对 web 站点的信任,而 CSRF 攻击则利用网站对其用户的信任。
- SQL 注入,通常发生在应用服务器试图存储由用户发送的数据时。
- 永远不要相信你的用户,包括你自己。
Web性能
导致 Web 性能问题的原因主要有两种,一是网络延迟,二是大部分情况下的浏览器单线程执行。
Web性能包含哪些方案?
- 减少总体负载时间
- 一般策略是使文件尽可能小,尽可能减少 HTTP 请求的次数,并采用巧妙的加载技术(例如 preload)使文件更快可用。
- 尽快使网站可用
- 有时我们也会在实际需要时才加载资源(这被称为懒加载)。
- 流畅性和交互性
- 使用 CSS 动画而不是 JavaScript 来制作动画,并尽量减少由于 DOM 变化而引起重绘 UI 的次数。
- 感知性能
- 用户所体验到的,是网站看起来有多快,而不是网站实际有多快。
- 性能测量
内容如何渲染
浏览器的工作原理
- 导航
- DNS查询
- TCP 握手:SYN/SYN-ACK/ACK
- TLS协商
- 响应
- 拥塞控制 / TCP 慢启动
- 解析
- 即使请求页面的 HTML 大于初始的 14KB 数据包,浏览器也将根据其拥有的数据开始解析并尝试渲染。这就是为什么在前 14KB 中包含浏览器开始渲染页面所需的所有内容。
- 构建 DOM 树
- 预加载扫描器
- 其他过程,JavaScript 编译,脚本被解析为抽象语法树。有些浏览器引擎会将抽象语法树输入编译器,输出字节码。这就是所谓的 JavaScript 编译。
- 渲染
- 样式,合成渲染树
- 布局
- 绘制
- 合成
源顺序
关键路径
文档对象模型
延迟
- 使用CDN和http2
- 最小化初始加载
- 当响应时间超过 50 毫秒时,用户会感受到延迟。
JavaScript 性能优化
- 优化 JavaScript 的下载
- 预加载
-
<head>...<!-- 预加载 JavaScript 文件 --><link rel="preload" href="important-js.js" as="script" /><!-- 预加载 JavaScript 模块 --><link rel="modulepreload" href="important-module.js" />...</head>
- 预加载并不能保证脚本在你包含它时已经加载完成,但它确实意味着它将尽早开始下载。即使未完全移除阻塞渲染的时间,渲染阻塞时间仍将缩短。 -
- 将计算任务移动到主线程外
- 异步代码
- web worker
- WebGPU,进行高性能计算和绘制复杂的图像,比Web Worker有更好的性能优势。
HTML性能优化
HTML性能关键问题
- 图像和视频大小
- 嵌入内容的交付,iframe,将内容加载到
<iframe>
中可能会显著影响性能,因此应该仔细考虑。 - 资源加载顺序
响应式处理替代元素
<img srcset="320w.jpg, 480w.jpg 1.5x, 640w.jpg 2x" src="640w.jpg" alt="家庭照" />
iframe
- 除非非常必要,否则不要使用嵌入式
<iframe>
- 需要额外的http请求加载内容
- 浏览器需要为每个
iframe
创建一个单独的页面实例
- 懒加载 iframe
loading="lazy"
JS模块
<script type="module"> /* JavaScript 模块代码 */</script>
模块与经典脚本的不同
- 本地测试——如果你通过本地加载 HTML 文件(比如一个 file:// 路径的文件),你将会遇到 CORS 错误,因为 JavaScript 模块安全性需要。你需要通过一个服务器来测试。
- 模块内部定义的脚本部分获得与标准脚本中不同的行为。这是因为模块自动使用严格模式。
- 加载一个模块脚本时不需要使用 defer 属性,模块会自动延迟加载。
- 模块功能的导入范围——它们仅限于被导入的脚本文件。
动态加载模块
import("/modules/mymodule.js").then((module) => { // 使用模块做一些事情。});
- 另一个动态导入的优点是它们始终可用,即使在脚本环境中也是如此。
- 顶层 await 是模块中可用的一个特性。
- 使用 polyfill 提供缺失特性的回退。
编写“同构”模块
- 将你的模块分为“核心”和“绑定”。
- 使用之前检测特定的全局变量是否存在。
- 如果你想使用 fetch 函数,它仅在 Node.js v18 及更高版本中支持,你可以使用类似的 API,如 node-fetch 提供的 API。
MathML
- MathML 是用于在网页中编写数学公式的标记语言。