浏览器原理

浏览器进程

  1. 主进程 只有一个,负责调度主控整个浏览器
  2. 插件进程 每个插件都有一个进程,只在插件被调用的时候创建
  3. GPU进程 只有一个,负责 3d 绘制
  4. 渲染进程 通常每个标签页一个,负责网页的渲染、脚本的执行和事件的处理等

浏览器渲染进程

  1. GUI 线程 负责页面的构建和渲染,当页面需要被绘制的时候就会启动这个线程,要注意的是该线程和 JS 引擎线程是互斥的,不能并行执行
  2. JS 引擎线程 负责解析和执行 JS 脚本,因为他和GUI线程的互斥性,所以 JS 代码是会导致页面渲染不连贯的,也就是常说的阻塞页面渲染
  3. 事件触发线程 归属于浏览器,而不是 JS 引擎,他主要就是控制事件循环,将一系列的任务加入一个队列等 JS 引擎空闲下来后去执行
  4. 定时器线程 管理定时器的计时,等时间到了就把事件推入任务队列,等待 JS 引擎执行
  5. HTTP 请求线程 每发送一个请求就会开启一个新的线程,等待响应后把回调函数推入任务队列,等 JS 引擎执行。

Pyodide

Pyodide 是一个在浏览器环境中运行的 Python 解释器,利用了 CPython 技术和 WebAssembly 技术,从而可以在浏览器中运行 Python 软件包;并且 Pyodide 保证了 Python 与 JavaScript 的兼容,允许 Python 使用浏览器的 API

基本使用

使用 CDN 形式向项目中引入 Pyodide 软件包

1
<script defer src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>

然后调用已注入到全局的 loadPyodide() 方法获取到 Pyodide 实例

1
const pyodide = await loadPyodide()

调用 Pyodide 实例的 runPython() 方法以执行 Python 代码

该方法传入的字符串,代表需要执行的 Python 代码

该方法的返回值,执行 Python 代码的输出

1
pyodide.runPython(`print('Hello world!')`)

调用 print() 方法的效果相当于调用 console.log() 方法

1
2
3
4
5
6
pyodide.runPython('1 + 2')
pyodide.runPython('2 ** 10')
pyodide.runPython(`
import sys
sys.version
`)

同样可以在 Pyodide 中使用内置软件包,也可以导入外部软件包

Pyodide 实例的 runPythonAsync() 方法用于异步执行 Python 代码

1
await pyodide.runPythonAsync('1 * 1')

JS 获取 Python 作用域

Python 全局作用域通过 pyodide.globals 对象暴露

调用其 get() 方法以获取 Python 全局作用域的变量

1
2
3
4
5
6
7
pyodide.runPython('x = 1')
pyodide.runPython(`y = 'xes'`)
pyodide.runPython('z = [2, 4]')

pyodide.globals.get('x')
pyodide.globals.get('y')
pyodide.globals.get('z').toJs()

Python 中复杂类型变量,如列表等,可以通过调用 toJs() 来转换为一个 JS 对象

调用其 set() 方法以设置 Python 全局作用域的变量

1
2
3
4
5
6
7
pyodide.globals.set('xx', 'xxxxx')
pyodide.globals.set('alert', alert)
pyodide.globals.set('square', x => x ** 2)

pyodide.runPython('print(xx)')
pyodide.runPython(`alert('xxxx')`)
pyodide.runPython('print(square(20))')

可以设置普通变量、对象、函数,以及 JS 或 DOM 内置函数等等

Python 获取 JS 作用域

在 Python 中导入 JS 包以使用 JS 的全局变量

1
2
3
import js

js.confirm('message')

使用类似与调用 window 变量

Worker 环境使用

使用 importScripts() 方法导入 CDN 脚本,随后通过暴露在全局的 loadPyodide() 方法使用

1
self.importScripts("https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js")

特别的,若在 ServiceWorker 中使用,需要导入 XMLHttpRequest 的 shim 包,因为该类在 ServiceWorker 中无法使用但 Pyodide 依赖于该包,如

1
2
importScripts("./node_modules/xhr-shim/src/index.js")
self.XMLHttpRequest = self.XMLHttpRequestShim

模块 Worker 环境下需要改变导入方式为 ESM

1
2
3
4
import "./node_modules/xhr-shim/src/index.js"
self.XMLHttpRequest = self.XMLHttpRequestShim
import "./pyodide.asm.js"
import { loadPyodide } from "./pyodide.mjs"

文件系统

Pyodide 基于 Emscripten File System API 实现了自定义的文件系统

在 Python 环境中直接调用相关文件操作方法;通过 pyodide.FS 对象向浏览器环境暴露对文件系统的操作

1
2
3
4
5
6
7
with open("/hello.txt", "r") as fh:
data = fh.read()
print(data)

with open("/hello.txt", "r") as fh:
data = fh.read()
print(data)
1
2
3
const file = pyodide.FS.readFile("/hello.txt", { encoding: "utf8" })

pyodide.FS.writeFile("/hello.txt", data, { encoding: "utf8" })

需要注意的是,Pyodide 默认使用的是 MEMFS,可以通过调用其 mount 方法绑定至其他的文件系统

1
pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: "." }, "/mnt")

或者可以使用原生 File System API 绑定至 Origin Private File System 或 Native File System

1
2
3
4
5
const root = await navigator.storage.getDirectory()

const nativefs = await pyodide.mountNativeFS("/mnt", root)

await nativefs.syncfs()

PWA

PWA 即 Progressive Web App ———— 渐进式网络应用

PWA 技术允许用户像 Native 应用一样使用 Web 应用,支持多平台和多设备访问,并且支持在线或离线访问

PWA 技术的核心是 ServiceWorker 技术,并基于 ServiceWorker 技术支持离线访问

PWA 特点

  • 可通过 URL 访问并被搜索引擎抓取
  • 可以安装到本地(A2HS)
  • 可以使用 URL 分享
  • 可以在离线状态访问
  • 适配老版浏览器访问,并支持在新浏览器中使用更多新特性
  • 支持在有新内容时更新
  • 能适配各种尺寸屏幕
  • 仅通过 HTTPS 提供服务

PWA 创建

  1. 页面通过 link 标签引入 manifest 文件

    文件后缀名可以是 json 或者 webapp、webmanifest

    1
    <link rel="manifest" href="manifest.json" />

    该文件是一个 JSON 的语法,必须指定的项为 nameicons(对于 Chromium 系浏览器 start_urldisplaydisplay_override 也是需要指定的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "name": "My PWA",
    "icons": [
    {
    "src": "icons/512.png",
    "type": "image/png",
    "sizes": "512x512"
    }
    ]
    }

    name 指定应用的名称,接受一个字符串

    icons 指定应用的图标,接受一个数组,数组各项可以指定 srcsizestype 项,分别代表图标的 URL、尺寸及 MIME 类型

    manifest.json 文件的详细配置可以参考相关的文档

  2. 页面中注册 ServiceWorker,且 ServiceWorker 中监听了 fetch 事件

  3. 页面须启用 Secure Context,即使用 HTTPS 协议或者为本地资源

PWA 下载

PWA 下载可以通过浏览器访问对应的网页实现

通常浏览器检测到网页支持下载为 PWA 时,会显示一个默认的“下载为 PWA”的按钮,也可以自定义“下载为 PWA”的按钮

通过监听全局的 beforeinstallprompt 事件获取到 BeforeInstallPromptEvent 事件实例并存储,通常该事件在页面加载时即触发;在必要时刻调用 BeforeInstallPromptEvent.prompt() 方法以使用户确认下载 PWA 应用;下载完成后会在全局触发 appinstalled 事件

PWA 下载亦可以通过应用商店等场景下载

PWA 原理

PWA 实质是仅将应用行为上类似于原生应用,如在桌面显示图标,在应用列表显示,支持卸载等;并不会将应用程序的资源文件主动下载至本地,具体策略由开发者通过 IndexedDB、ServiceWorker、Cache Storage 等开发实施

示例

性能优化

构建相关

路由懒加载

最主要在于降低首屏加载资源大小,仅加载所需的页面资源文件,加快页面的显示

bad
1
2
3
4
5
import C from 'c'

{
component: C,
}
good
1
2
3
{
component: () => import('c'),
}

原理即将导航中的路由组件从静态 import 导入改为动态 import() 导入

组件懒加载

原理同路由懒加载

worse
1
2
3
4
5
6
7
import C from 'c'

export default {
components: {
C,
},
}
better
1
2
3
4
5
6
7
const C = () => import('c')

export default {
components: {
C,
},
}

做组件懒加载一般在某些特别条件下使用,如组件仅在特定条件下才展示、当前页面文件过大、组件复用性较强

外部依赖懒加载

较大外部依赖可动态导入

worse
1
import * as THREE from 'three'
better
1
import('three').then((THREE) => { /* do */ })

但建议谨慎采取此方式

Tree Shaking 和 SideEffects

依赖 ESM 的静态特性,进行静态分析,在生成产物中去除无用的模块或代码,从而降低生成产物的大小

webpack 默认在构建阶段会启用 Tree Shaking,在开发阶段需手动配置

webpack.config.js
1
2
3
4
5
6
module.exports = {
mode: 'development',
optimization: {
usedExports: true,
},
}
webpack.config.js
1
2
3
module.exports = {
mode: 'production',
}

使用副作用

package.json
1
2
3
{
"sideEffects": false
}

某些情况下需手动标记 /*#__PURE__*/ 以标记代码,以标记语句是可执行 Tree Shaking 的

vite 原生基于 rollup 支持在构建阶段启用 Tree Shaking

构建产物压缩

webpack 可以使用 terser-webpack-plugin 插件来执行代码构建产物的压缩

webpack.config.js
1
2
3
4
5
6
7
8
const TerserPlugin = require("terser-webpack-plugin")

module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
}

vite 内部默认集成 esbuild 进行代码构建产物的压缩,同时支持配置为使用 terser 来执行压缩

vite.config.js
1
2
3
4
5
6
7
8
9
10
11
export default {
esbuild: {},
build: {
cssMinify: 'esbuild',
minify: 'esbuild',
terserOptions: {},
},
optimizeDeps: {
esbuildOptions: {},
},
}

静态资源构建产物(特别是图片)的压缩可以使用一些插件实现,如 compression-webpack-plugin

外部库按需加载

外部库(特别是 UI 组件库)使用插件(如 babel-plugin-import 等)进行按需加载

代码分割

可以适当进行代码分割,避免一次性加载过大的资源文件,阻碍页面的展示;也需要避免过度分割,一次性执行过多的资源获取请求

vite.config.js
1
2
3
4
5
6
7
8
9
10
11
export default {
build: {
rollupOptions: {
output: {
manualChunks: {},
// or: manualChunks: (id) => id,
},
},
cssCodeSplit: {},
},
}

内联代码文件

部分小体量的 JS 文件或 CSS 文件,可以内联到 HTML 文件中,减少请求的数量

分析外部依赖

可通过 webpack-bundle-analyzer 插件或 rollup-plugin-visualizer 插件来分析查看

webpack.config.js
1
2
3
4
5
6
7
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
}
vite.config.js
1
2
3
4
5
6
7
import { visualizer } from 'rollup-plugin-visualizer'

export default {
plugins: [
visualizer(),
],
}

检查项目的依赖包是否有重复引用的情况,避免出现使用同样名称不同版本的依赖包引用的情况

渲染相关

骨架屏

主要应用于缩短白屏时长,特别是 SPA 单页应用

原理是直接把展示骨架屏的内容放在 html 文件内,在真正内容加载完后再隐藏骨架屏的内容

虚拟滚动

只渲染可视区域的列表项,非可见区域的不渲染

原理为计算列表的总高度,并在触发滚动事件时根据滚动高度更新起始下标和结束下标,从而取出相应的数据渲染元素

Worker 长任务优化

将一些长任务逻辑移入到 Worker 中,避免长任务的执行阻碍 UI 渲染而影响用户体验

是否使用 Worker,需要比较 Worker 通信时长与运算时长相比是否具有足够的优势

利用 requestAnimationFrame 周期任务

可以利用 requestAnimationFrame 处理周期任务

特别是需要较严格固定周期频率执行的情况(setInterval 和 setTimeout 无法保证准确的时间间隔)

同时 requestAnimationFrame 支持在页面隐藏或最小化时暂停执行周期任务,以节省性能(setInterval 和 setTimeout 不会因页面隐藏或最小化等因素暂停执行)

使用 CSS 动画过渡变换替代 JS

CSS 动画 Animation、过渡 Transition、变换 Transform 相较于 JS 性能通过上更具优势,并且浏览器更易于针对性地做优化

简化 CSS 选择器

避用通配符选择器 *

减少使用标签选择器

优先使用默认的样式继承

避免层数过大的选择器

代码复用及代码封装

提升代码复用率,进行功能代码封装等

CSS 的样式简化,可以使用 TailwindCSSUnoCSS 等方案

使用防抖节流

防抖,使得指定函数至少间隔 n 秒才会执行一次

节流,使得指定函数在 n 秒中最多执行一次

对于容易连续触发的事件,如 mousemovepointermovescrolltouchmovewheelresize 等,通过将事件处理方法绑定为防抖节流版本的方法,避免持续多次重复执行方法

使用 will-change 优化动态效果

预先将执行动画的元素设置 will-change CSS 属性,以便浏览器引擎将其视为单独图层来进行优化

注意点是,避免过度应用 will-change 属性;建议仅在需要时候 JS 动态设置该属性

减少页面重排重绘

重绘指元素的非几何样式改变引起的浏览器重新渲染目标元素的现象

重绘指元素的几何样式改变引起的浏览器重新渲染整个渲染树或的现象

使用 GPU 渲染

CSS中可使用如下一些方式将目标元素独立为合成层,从而进行独立渲染,以触发 GPU 渲染

  • 指定 will-change 属性
  • 3D 或者透视变换 perspective transform
  • 使用加速视频解码的 video 元素
  • 拥有 3D 上下文(WebGL)或者加速 2D 上下文的 canvas 元素
  • 使用 opacity、filter 实现 CSS 动画或使用一个动画 webkit 变换的元素
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个包含复合层的后代节点
  • 元素有一个兄弟元素在复合图层渲染,且具备较低的 z-index

避免无效请求

避免出现无效请求,例如表单提交频繁点击的问题,或路由切换时还有未完成的请求;对于服务器和用户来说,会造成不必要的困扰

网络相关

使用 HTTP2

HTTP2 支持头部压缩,能够减少数据传输量,节省消息投占用的网络的流量

且 HTTP2 支持多路复用、服务器推送等功能

gzip 压缩

HTTP 头部及资源启用 gzip 压缩,能够大大减少网络传输的数据量

可以使用 compression-webpack-pluginvite-plugin-compression 压缩打包资源至 gzip

webpack.config.js
1
2
3
4
5
const compression = require('compression-webpack-plugin')

module.exports = {
plugins: [new compression()],
}
vite.config.js
1
2
3
4
5
import compression from 'vite-plugin-compression'

export default {
plugins: [compression()]
}

对资源服务开启 gzip 支持

nginx.conf
1
2
3
4
5
6
7
8
9
gzip on;
gzip_min_length 1k;
gzip_comp_level 5;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
gzip_disable "MSIE [1-6]\.";
gzip_proxied any;
gzip_vary on;

一般推荐提前处理完成 gzip 文件,再直接交由 nginx 服务

执行网络请求时,浏览器会自动带上同源的 Cookie 信息,一定程度上会增大请求头的大小

可以通过精简 Cookie 的内容,来降低请求信息的大小

同时可以对静态资源单独部署,避免请求携带不必要的 Cookie 信息

启用 Keep-Alive

通过给请求头或响应头设置 Keep-Alive 头,通常用于提示连接超时时间和最大请求量

1
2
Connection: Keep-Alive
Keep-Alive: timeout=5, max=1000

减少预检请求发起

可以在跨域请求设置 Access-Control-Max-Age 响应头指定预检请求的缓存期限,从而在指定期限内的跨域请求无需进行预检请求可以直接发起请求

有效期 10min
1
Access-Control-Max-Age: 600

资源相关

script 加载方式

  • 正常模式

JS 会阻碍 DOM 渲染

<script src="main.js"></script>

  • async 模式

异步加载 JS,执行无顺序,加载完成后立即执行

可以用于加载与 DOM 无关的 JS 资源,如埋点统计等

<script async src="main.js"></script>

  • defer 模式

异步加载 JS,执行有顺序,加载完成后统一在 DOMContentLoaded 事件触发前执行

一般情况均可使用 defer 优化 JS 资源的加载,避免 JS 脚本加载与执行阻碍网页的渲染

<script defer src="main.js"></script>

  • module 模式

行为上会类似于 defer 模式

<script type="module" src="main.js"></script>

  • fetchpriority 资源加载优先级

可以利用 fetchpriority HTML 属性指定 script 脚本加载的优先级,优先加载级别高的脚本,延后加载级别低的脚本

资源预加载

需要避免 preloadprefetch 的混用,以避免不必要的二次自由加载

  • preload

预先下载当前页面将使用的资源并缓存(不会执行),会提升资源的优先级

需同时指定 as 属性与 href 属性

<link rel="preload" href="style.css" as="style" />

<link rel="preload" href="main.js" as="script" />

建议指定 type 属性,以避免浏览器下载格式不支持的资源

建议同时指定 crossorigin 属性

  • prefetch

预加载未来页面将使用的资源,并保存在缓存内一段时间,会降低资源的优先级

要求当前页面需为安全上下文

<link rel="prefetch" href="main.js" />

  • modulepreload

类似于 preload

预加载当前页面将使用的模块脚本资源,并进行解析与执行

<link rel="modulepreload" href="main.js" />

  • prerender

预加载目标资源并提前在后台处理执行

仅部分浏览器支持该非标准特性

网络预连接

一般情况下,dns-prefetchpreconnect 都是配对使用

但不建议过度使用 preconnect,仅用于未来一段时间极可能访问或请求的 origin;否则仅应用 dns-prefetch

同时 dns-prefetch 的浏览器兼容性优于 preconnect

建议使用以上两属性的同时指定 crossorigin 属性

  • dns-prefetch

提前执行目标 origin 的 DNS 解析,可以加快未来将访问或请求的 origin 的处理速度(直接使用已预先解析的 DNS 缓存)

<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />

  • preconnect

提前执行目标 origin 的连接 —— DNS 解析、TCP 连接(及 TLS 握手),可以加快未来将访问或请求的 origin 的处理速度

<link rel="preconnect" href="https://fonts.googleapis.com/" />

避用外部依赖

尽量减少对非必要的外部依赖的使用,使用轻量级别替代方案或者自行实现

常见的如:

使用轻量级 day.js 替代 moment.js

使用 ESM 的 lodash-es 替代 CJS 的 lodash

使用 CDN 服务静态资源

CDN 即 Content Delivery Network,其具有分布于多个地域的服务器阵列

CDN 可以降低私有服务器的访问压力

地理位置的距离可能相对更近,一定程度上可以降低网络资源加载的时延

CDN 保证了比较正确的缓存配置

合理配置缓存策略

服务器在响应资源时,通过指定 Expires 响应头或 Cache-Control 响应头来控制浏览器该资源的缓存策略;若被指定为强缓存并且在有效期内直接使用缓存;反之若为被禁止使用缓存,则进行协商缓存,通过 If-Modified-Since 头向服务器提供浏览器缓存的资源的修改时间(在获取资源时服务器通过 Last-Modified 头指定)或通过 If-None-Match 头向服务器提供浏览器缓存的资源的标识符(在获取资源时服务器通过 ETag 头指定)

  • Expires(推荐使用 Cache-Control 代替,其优先级更高)【响应头】

    指定缓存失效的时间

  • Cache-Control【响应头,请求头】

    配置缓存的策略及有效期

    • no-store 不允许缓存

    • no-cache 允许缓存,但使用前需进行服务端验证

    • must-revalidate 允许缓存,有效期内直接使用缓存,超出有效期需进行服务端验证,通常结合 max-age=N 使用

    • max-age=N 指定缓存的有效期

  • Last-Modified【响应头】/ If-Modified-Since【请求头】

    资源最近修改时间

  • ETag【响应头】/ If-None-Match【请求头】

    资源唯一标识符

ServiceWorker 实现可控缓存

利用 ServiceWorker 结合 CacheStorage 实现可控缓存,原理是基于受 ServiceWorker 控制的上下文会在 ServiceWorker 全局触发 fetch 事件,可以通过调用返回的 FetchEventrespondWith() 方法自定义响应

图片字体相关

webp 图片

webp 格式图片大小通常比同等情况下的其他格式图片大小有较大优势,因此若浏览器支持 webp 格式图片,优先使用 webp 格式图片

可以利用离线或在线 webp 图片格式转换工具转换图片格式为 webp

图片懒加载

  • JS 手动控制

    初始不指定图片标签的 src 属性,直到图片需要展示时再指定其 src 属性,避免图片的自动预加载

    1
    <img data-src="/img/png" />
    1
    2
    3
    4
    // 适当情况下调用该方法
    function loadImg(el) {
    el.src = el.getAttribute('data-src')
    }
  • 利用 img 标签特性(更推荐)

    可以设置 img 标签的 loading 属性实现懒加载功能,将属性值指定为 lazy 以惰性加载图片

    同时可以指定 img 标签的 fetchpriority 属性,以控制获取图片资源的优先级,设定为 high 以提升获取的优先级,设定为 low 以降低获取的优先级

    同时可以指定 img 标签的 decoding 属性,以设定解码图片的模式(是否允许在图片解码完成前展示图片),设定为 sync 以同步解码图片,设定为 async 以异步解码图片

    1
    <img src="/img/png" alt="" loading="lazy" fetchpriority="auto" decoding="auto" />

字体图标

将小图标利用字体形式加载,如 IconFont

1
@import url('//at.alicdn.com/t/font_8d5l8fzk5b87iudi.css');
1
<i class="iconfont icon-xxx"></i>

通常加载资源大小会更小,并且能够避免重复加载图片并降低请求数量,且支持修改各类字体样式

内联图片

将小图片转换为 base64 编码内联入 html 文档,可以减少请求数量

webpack 中可以使用 url-loader 插件自动转换内联图片

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 1024 * 8,
},
},
],
},
],
},
}

vite 原生支持内联图片,默认在图片大小小于 4KB 时启用

可以通过 build.assetsInlineLimit 选项配置启用的阈值

vite.config.js
1
2
3
4
5
export default {
build: {
assetsInlineLimit: 4096,
}
}

图片裁剪

对图片生成多个尺寸的备用图片,使用时根据需要加载不同尺寸的图片,减少不必要的资源流量

图片单独部署

将图片等静态资源部署在单独的静态资源服务器或是 CDN 上,避免直接打包到项目中

图片尺寸指定

设置图片标签的尺寸大小,防止图片加载中导致页面布局抖动,影响 CLS 指标

字体按需生成

使用第三方字体库时,尽可能按需生成,避免不必要的全量引入字体库

代码相关

JSON 字符串使用

对于大对象数据,尽量采用 JSON 格式而非 JS 对象格式,因为 JSON 语法比 JS 简单,解析速度更快

如 vite 支持将 JSON 文件打包为 JSON 字符串而非 JS 对象

vite.config.js
1
2
3
4
5
export default {
json: {
stringify: true,
},
}

if 逻辑提前跳出

提前结束的逻辑利于编译器的优化

bad
1
2
3
4
5
6
7
8
9
10
11
function () {
if (A) {
if (B) {
return 'good'
} else {
return 'bad'
}
} else {
return 'bad'
}
}
good
1
2
3
4
5
6
7
function () {
if (A && B) {
return 'good'
}

return 'bad'
}

逻辑能提前结束就提前结束

switch 连续值优化

switch 对于连续值会处理成数组,而数组会具有更高效率的随机访问性能

bad
1
2
3
4
5
6
7
function get(/* @type {1 - 10} */ level) {
if (level >= 10) return 100
if (level >= 9) return 80
if (level >= 6) return 50
if (level >= 1) return 20
return 10
}
good
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getPrice(level) {
switch(level) {
case 10: return 100
case 9: return 80
case 8:
case 7:
case 6: return 50
case 5:
case 4:
case 3:
case 2:
case 1: return 20
default: return 10
}
}

若条件可以处理成连续的数字,可以使用 switch 来进行优化

循环减少执行次数

对于循环,满足条件或完成任务后即刻跳出,避免不必要的执行损耗

bad
1
2
3
4
5
6
7
8
9
function find(data) {
let result = null
for (let i = 0; i < data.length; i++) {
if (data[i].key === KEY) {
result = data[i]
}
}
return result
}
good
1
2
3
4
5
6
7
function find(data) {
for (let i = 0; i < data.length; i++) {
if (data[i].key === KEY) {
return data[i]
}
}
}

提取集合数组长度

在循环处理数组、集合等容器的元素,若可以保证容器的容量不会发生变化,可以提前提取容器的容量,避免在循环中重复获取

适当使用位运算

对于一些和 2 相关的乘除法或者条件相关的,可以用位运算替代

bad
1
const A = 2 ** 8
good
1
2
const A = 2 << 3
const isPowerOfTwo = n => (n > 0) && (n & (n - 1) === 0)

svg

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<svg width="500" height="500" style="background-color: #eee;">

<!-- 矩形 rect -->
<rect width="100" height="100" x="100" y="100" rx="10" ry="10"></rect>
<!-- 圆形 circle -->
<circle cx="250" cy="250" r="100"></circle>
<!-- 椭圆 ellipse-->
<ellipse cx="475" cy="450" rx="25" ry="50"></ellipse>
<!-- 直线 line -->
<line x1="0" y1="0" x2="500" y2="500" stroke="green"></line>
<!-- 折线 polyline -->
<polyline points="500 0, 100 100, 0 500" stroke="blue" fill="none"></polyline>
<!-- 多边形 polygon -->
<polygon points="500 0, 400 400, 0 500" stroke="none" fill="green"></polygon>
<!-- 直线路径 path -->
<path d="M 0 0 L 50 50 L 50 100 L 100 400 L 400 400 L 400 100 L 50 50 Z" stroke="orange" fill="none" ></path><path d="M 0 0 l 250 150 l 100 0 l 0 -100 l 50 50 l 50 0 l 0 350 l -350 0 l 0 -50 l -50 -50 l 100 0 l 0 -100 Z" stroke="red" fill="none" stroke-width="5" stroke-dasharray="25 5 10 5" stroke-dashoffset="5" stroke-linecap="round" stroke-linejoin="round"></path>
<!--
属性样式 直接设置在元素属性上
内联样式 设置在元素 style 属性内
内部样式 写在 style 标签内
外部样式 写在独立的 css 文件中
-->
<!--
svg 常见属性
fill 填充颜色
stroke 描边颜色
fill-opacity 填充颜色的不透明度
stroke-opacity 描边颜色的不透明度
stroke-width 描边宽度
stroke-dasharray 描边样式 - 可以用于设置虚线
stroke-dashoffset 设置偏移量
stroke-linecap 线帽样式
butt 平头 | 默认
round 圆头
square 方头
stroke-linejoin 拐角样式
miter 尖角 | 默认
round 圆角
bevel 平角
shape-rendering 消除锯齿
crispEdges 关闭反锯齿功能
geometricPrecision 开启反锯齿功能
-->
<!--
svg 支持颜色
颜色关键字
十六进制
RGB 和 RGBA
HSL 和 HSLA
-->

<!-- 文本元素 text -->
<text x="250" y="250" fill="pink" font-size="50" font-weight="bold" text-decoration="underline" text-anchor="middle" dominant-baseline="middle">SVG</text>
<!-- 多行文本 tspan -->
<text font-size="25">
<tspan x="400" y="380">S</tspan>
<tspan x="400" y="400">V</tspan>
<tspan x="400" y="420">G</tspan>
</text>
<!--
文本元素属性
font-size 字号
font-weight 粗体
text-decoration 装饰线
text-anchor 水平对齐方式
dominant-baseline 垂直对齐方式
writing-mode 文字方向
-->

<!-- 超链接 a -->
<a xlink:href="https://developer.mozilla.org/zh-CN/docs/Web/SVG" xlink:title="svg" target="_blank">
<text x="50" y="50" font-size="25">SVG</text>
</a>

<!-- 图片 image -->
<image xlink:href="https://img.zcool.cn/community/0167b95fc9ea7a11013ee04dc55982.jpg@1280w_1l_2o_100sh.jpg" width="50" height="50" x="100" y="100"></image>

</svg>
typescript

typescript

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282

/** TS变量类型 */
let an: any;
let str: string = '12';
let num: number = 20;
let flag: boolean = true;

let arr: number[] = [1];
let ar: Array<number> = [2];
let tuple: [string, number] = ['test', 10];

enum Color {
Red,
Green,
Blue,
};
let co: Color = Color.Red;

function hello(): void {
console.log('hello');
}

let nu: null;
let un: undefined;
let ne: never;

/** 类型断言 */
var num0: number = <number> <any> str;

/** 类型推断 */

/**
* 变量作用域
* - 全局作用域
* - 类作用域
* - 局部作用域
*/

/** 运算符 */

/** 条件 */

/** 循环 */

/** 函数 */
// 可选参数 默认参数 剩余参数
function add(x: number = 0, y: number = 0, z?: number/* 可选参数: 必须在参数列表最后 */, ...other: number[]): number {
return x + y + (z ?? 0) + add(...other);
}
const sub = (x: number, y: number): number => x - y
const add_plus: (x: number, y:number) => number = (x, y) => x + y;

// 匿名函数 自动执行函数 递归函数 箭头函数
var res = function(a: number, b: number) {
return a * b;
};
(() => console.log('Hello!'))();

// 函数重载

/** 字面量类型 */
// 使用具体值作为类型
let fu: '0' | '1' | '2' | '3' = '1';
const fv = 1;

/** Number String Boolean 包装类型 */

/** Array 数组 元组 Map */
// 数组
let arr1: number[] = [1, 2];
let arr2: Array<number> = [3, 4];
// 元组 Tuple
let position: [number, string, boolean] = [1, '2', true];
// Map
const m: Map<number, number> = new Map();

/** 联合类型 */
var union: number | number[];
union = 12;
union = [12, 34];

/** 枚举类型 */
enum Direction {
Up,
Down,
Left,
Right,
};
// 枚举成员值默认是自第一个值(默认为0)开始的数值,即默认为数字枚举
var dir: Direction = Direction.Up;
enum Direction1 {
Left = 10,
Right,
};
// 字符串枚举必须有初始值
enum Direction2 {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right',
};

/** typeof */
var cc: typeof position;

/** 接口 interface */
// 描述一个对象类型
// 当然也可以使用 type 关键字声明
interface Person {
name: string,
age: number,
birth?: Date,
sayHi: string | string[] | (() => string),
}
var csy: Person = {
name: 'CSY',
age: 20,
birth: new Date(),
sayHi: (): string => 'Hi',
}
const ccc: {
name: string,
sex?: boolean,
} = {
name: 'ccc',
};
// 接口的继承
interface Human extends Person {
father: Person,
mother: Person
}

/** 类型推论 */
// 自动推断变量类型
// 1. 声明变量并初始化
// 2. 决定函数返回值
let c = 20;
function f(a: number, b:number) {
return a + b;
}
/** 类型断言 */
// (可类型推论变量类型)自行指定变量的类型
const alink1 = document.getElementById('link') as HTMLAnchorElement;
const alink2 = <HTMLAnchorElement>document.getElementById('link');

/**
* 类 对象
* - 构造函数
* - 实例属性及实例方法
* - 访问控制修饰符 public protected private
* - 继承 extends 类 | implements 接口
* - 只读 readonly (仅适用方法)
*/
abstract class Animal {}
class Human extends Animal implements Person {
public a: string;
protected b: number;
private c: boolean;

name = 'CSY';
age = 40;
birth = new Date();
sayHi = () => 'Hi';

readonly d: String;

constructor (v: boolean) {
super();
this.c = v;
};

static isHuman = (o: any) => typeof o === 'object' && o instanceof Human;

get e () {
return this.a + this.a
}
set e (s) {
this.a = s.toLowerCase();
}
}
const son: Human = new Human(true);

/**
* 类型兼容 不同名称相同结构的类型是等价的
*
* - 类 | 若A类型内容包含B类型内容(非严格包含),则A类型变量可赋值给B类型变量
* - 接口 | 若A类型内容包含B类型内容(非严格包含),则A类型变量可赋值给B类型变量
* 类与接口亦可相互兼容
* - 函数 | 若B函数参数表包含A函数参数表(非严格包含),则A类型函数可赋值给B类型函数;相同位置参数需相同或兼容(对象多数服从少数);返回值需相同或兼容(对象少数服从多数)
*/

/**
* 交叉类型(类似接口继承)
* 将多个类型组合为同一个类型
* 重复的属性会合并为联合类型,相当于重载
*/
interface Co1 {
a: number,
}
interface Co2 {
b: string,
}
type Co = Co1 & Co2;
const co0: Co = {
a: 12,
b: '',
};

/** 泛型 */
// 泛型方法
function print <T> (v: T): void {
console.log(v);
}
// T 相当于类型变量
// 具体类型需用户使用时指定
print<number>(10)
print<string | boolean>('')
// 某些情况下可自动类型推定
print(1)
// 类型约束 结合interface使用 extends
function print0 <T extends Array<string> | string[]> (v: T): void {
console.log(v);
}
function print1 <T, K extends keyof T> (v: T, k: K): void {
console.log(v[k]);
}
// keyof 关键字接受对象类型并生成键名称(字符串和数字)的联合类型

// 泛型接口
interface PrintInterface <T> {
do: (v: T) => void
}

// 泛型类
class PrintClass <T> {
value: T;
}

// 泛型工具类
// Partial<T> 创建一个类型且T中所有属性均可选
type partial = Partial<Person>
// Readonly<T> 创建一个类型且T中所有属性均只读
type readonly = Readonly<Person>
// Pick<T, K extends keyof T> 创建一个类型并从给定类型中选出一组属性
type pick = Pick<Person, 'name' | 'age'>
// Record<K extends keyof any, T> 构造一个对象类型,属性键为keys,属性类型为Type
type record = Record<'a' | 'b', string>

// 索引签名类型
interface Obj {
[K: string]: number,
}
// [K: string] 表示任意string类型属性名称均可作为对象出现,且属性值为number类型变量

// 映射类型 in 关键字和 keyof 关键字
type p = {
[K in 'x' | 'y' | 'z']: number
}
type q = {
[K in keyof Person]: string
}

// 索引查询类型
type props = { a: number };
type typeA = props['a'];

/** 命名空间(可嵌套) */
namespace n {
export interface Person {};

namespace nn {}
}
var d: n.Person = {};

// 单独引用ts文件
/// <reference path="SomeFileName.ts" />

/** 模块 */

/** 声明 */
declare var jQuery: (selector: string) => any;

ajax

ajax

AJAX

AJAX

Ajax 简介

AJAX = Asynchronous JavaScript And XML

  1. 网页中发生一个事件(页面加载、按钮点击)
  2. 由 JavaScript 创建 XMLHttpRequest 对象
  3. XMLHttpRequest 对象向 web 服务器发送请求
  4. 服务器处理该请求
  5. 服务器将响应发送回网页
  6. 由 JavaScript 读取响应
  7. 由 JavaScript 执行正确的动作(比如更新页面)

Ajax 使用

XMLHttpRequest 对象用于同后台服务器交换数据

1
2
3
4
5
6
7
8
9
10
11
12
13
let request = new XMLHttpRequest();

request.open('POST', 'https://www.baidu.com', true);

request.setRequestHeader('Content-Type', 'application/json');

request.onreadystatechange = function() {
if(this.readyState === 4 && this.status === 200) {
console.log(this.responseText)
}
}

request.send({});
  • readyState 请求状态
    • 0:已创建对象未调用open方法
    • 1:已调用open方法
    • 2:已调用send方法已接收响应头
    • 3:数据接收中
    • 4:请求完成,数据接收完成或失败
  • status 服务器响应状态
  • responseText 请求返回的数据

请求数据类型 Content-Type

  • application/x-www-form-urlencoded

url 末尾加, ? 接 = 连接的键值对, 以 & 分隔多个参数

https://www.baidu.com?id=1&name=Lily

中文字符等会进行 URL 编码

使用 decodeURL() 编码,encodeURL() 解码

Ajax 默认请求数据类型

  • application/json

json 数据类型

  • multipart/form-data

常用于上传文件

Ajax 新特性

  • 设置请求时限
1
2
3
4
5
6
// 请求时限
request.timeout = 3000;
// 超时回调函数
request.ontimeout = (e) => {
console.log(e);
}
  • 使用 FormData 对象管理表单
1
2
let data = new FormData();
data.append('key', value);
  • 上传文件
1
2
3
4
5
6
7
8
// 获取文件
let files = document.querySelector('input[type=file]').files;
// 检测文件是否已选中
if(files.length <= 0)
return alert('ERROR');
// 创建 FormData 实例
let data = new FormData();
data.append('file', files[0]);
  • 获取数据传输进度信息
1
2
3
4
5
6
7
8
request.upload.onprogress = function (e) {
// lengthComputable 表示上传的资源是否具有可计算的长度
if(e.lengthComputable) {
// loaded 已传输的子节
// total 需传输的总子节
let percentComplete = Math.ceil((e.loaded / e.total) * 100);
}
}

jQuery 的 Ajax

  • $.ajax() 方法
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
$('#button').on('click', function () {

const files = $('#file')[0].files;
if(files.length <= 0) {
return;
}

const data = new formData();
data.append('file', files[0]);

$.ajax({
method: 'POST',
url: 'https://www.baidu.com',
data: data,
// 内容编码类型
// 默认值: "application/x-www-form-urlencoded"
contentType: false,
// 是否进行url编码
// 默认值: true
processData: false,
success: function (res) {
console.log(res);
},
});

});
  • $(document).ajaxStart() 方法

在 Ajax 请求发送前执行函数

1
2
3
$(document).ajaxStart(function () {
$('#loading').show();
});
  • $(document).ajaxStop() 方法

在 Ajax 请求结束执行函数

axios

专注于网络数据请求的库

目前最主流的

官方网站

  • axios.get & axios.post
1
2
3
4
5
6
7
8
9
10
11
12
axios.get(url, params)
.then(function (res) {
// 处理成功情况
console.log(res);
})
.catch(function (err) {
// 处理错误情况
console.log(err);
})
.then(function () {
// 总是会执行
});

axios.get(url[, config])

axios.post(url[, data[, config]])

  • axios({})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// promise 语法
axios({
url: '',
method: '',
params: {}, // GET 数据:url参数
data: {}, // POST 数据:默认json参数对象
}).then(res => {
// do something with res.data
});

// async-await 语法
const {data} = await axios({
url: '',
method: '',
params: {},
data: {},
});
// do something with data
regex

regex

正则表达式

正则表达式是使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串的搜索模式

语法

/正则表达式主体/修饰符(可选)

字符串方法
  • search() 搜索字符串,返回匹配的字符串下标或-1
1
'123456'.search(/234/)   // 1
  • replace() 替换匹配的字符串,返回修改后的字符串
1
'123456'.replace(/345/, 'abc')   // 12abc6
  • split() 从指定位置分割字符串,返回一个数组
  • match() 搜索字符串,返回由所有子串组成的数组或null
正则表达式修饰符
  • i 忽略区别大小写
  • g 执行全局匹配
  • m 执行多行匹配
正则表达式元字符
  • . 查找单个非换行符字符

  • \d 查找数字

  • \s 查找空白字符

  • \b 匹配单词边界

  • \w 查找数字、大小写字母及下划线

正则表达式量词
  • n+ 匹配一个或多个字符串n
  • n* 匹配零个或多个字符串n
  • n? 匹配零个或一个字符串n
  • ^ 匹配字符串开始(第一个字符)
  • $ 匹配字符串结束(最后一个字符)
正则表达式括号
  • [0-9] 匹配任何0-9数字
  • [a-zA-Z] [A-z] 匹配任意大小写字母
  • [abc] 查找[]内的任意字符
  • [^abc] 查找[]外的任意字符
  • (x|y) 查找()内任意选项
RegExp 对象及其方法
  • test() 匹配字符串是否符合给定模式

返回一个布尔值

  • exec() 匹配字符串中正则表达式的匹配

返回一个包含搜索结果数组,未查找到返回null

参考链接

MDN 正则表达式

菜鸟教程 正则表达式

W3school 正则表达式

前端学习路线

基础

  • 学习资料

初学时可以参考菜鸟教程W3School文档

学习到一定程度后推荐使用MDN(特别是英文文档,因为中文文档存在更新不及时的情况)或参考相应的官方文档

  • IDE

推荐使用 VSCode IDE

亦可使用 WebStorm IDE(可以申请学生版)

HTML

HTML 作为前端三件套之一,是非常重要的部分

学习角度来说,主要是掌握 HTML 的基本语法及常用元素,以及语义化标签的概念,此外,后续对一些 Web API 也需要有足够的了解,如 Web Storage API、Web Worker API、SVG 和 Canvas

CSS

CSS 作为前端三件套之一,是非常重要的部分

学习角度来说,主要是掌握 CSS 的选择器、盒模型、浮动布局、定位、弹性盒子布局、响应式设计等有足够的了解

JavaScript

JavaScript 作为前端三件套之一,是非常重要的部分

学习角度来说,主要是掌握 JavaScript 的基本语法,同时特别注意 JavaScript 的语言特性,并关注 ES6 的发展,此外还需要了解浏览器的 BOM 模型和 DOM 模型、浏览器事件机制、异步编程等方法

Ajax

Ajax 是前端中主流的物理请求方式,主要包括 XMLHttpRequest APIFetch API 两种,此外可以了解一下前端常用的 axios 网络请求库

学习角度来说,主要是了解 XMLHttpRequest 的基本使用以及封装,Fetch 可以作为进阶了解

计算机网络基础

前端中计算机网络也是重要的一部分

学习角度来说,主要了解 OSI 七层网络模型与 TCP/IP 五层网络模型、HTTP 请求方法和请求头及响应头以及状态码、HTTP 协议及 HTTPS 协议、DNS 协议、TCP 协议与 UDP 协议、WebSocket 协议等

Git

Git 是编程领域中目前最主流的版本管理工具,基本上不管是日常开发还是工作开发都在广泛地使用 Git

同时,Github 作为全球最大的技术交流平台,积极参与社区建设也是很重要的一环

学习角度来说,主要了解一些 Git 的基本概念、一些 Git 的基本命令以及 GitHub 的基本操作

Node.js

Node.js 允许在浏览器之外的环境运行 JavaScript 代码

学习角度来说,主要了解 Node.js 的使用以及一些 Node.js 的常见模块

包管理工具

目前主流的包管理工具主要是 npm yarn pnpm

学习角度来说,主要了解 npm 的基本使用及常用指令;yarn 和 pnpm 与 npm 大同小异,可作为进阶内容了解

项目构建工具

目前主流的项目构建工具主要是 webpack vite

学习角度来说,两者都有必要学习,webpack 主要关注其基本概念,vite 主要关注其基本原理

Vue

Vue 框架是国内主流的前端框架之一,被一些中小互联网企业广泛使用

学习角度来说,学习 Vue 可以直接从 Vue3 composition API 开始,除了了解 Vue 的基本开发之外,还需要重点了 Vue 的基本原理,此外需要注意 Vue 的性能优化

TypeScript

TypeScript 是目前前端领域使用最广泛的 JavaScript 超集语言,主要是解决 JavaScript 弱语言的问题,增加了强大且自由的类型系统,在一些大型互联网公司的大型项目中使用非常广泛

学习角度来说,学习 TypeScript,类似与学习 JavaScript,主要是对语言特性的学习;但就 TypeScript 而言,不需要专门去了解类型体操

React

React 框架是国内主流的前端框架之一,被一些大型互联网企业广泛使用

学习角度来说,学习 React 可以直接从 React functional component 开始,除了了解 React 的基本开发之外,此外需要注意 React 的性能优化

参考

黑马程序员前端学习路线 个人比较推荐这个学习路线,更加突出重点
尚硅谷前端学习路线 个人觉得这个的内容非常全面,但需要注意没必要都学习其中的内容


:D 一言句子获取中...