普通视图

发现新文章,点击刷新页面。
今天 — 2025年9月16日首页

Babel 配置完全指南:从基础安装到业务项目与 Library 开发的最佳实践

2025年9月15日 10:56
npm install --save-dev @babel/core @babel/cli @babel/preset-env
touch babel.config.json
touch .browserslistrc
// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": "3.22"
      }
    ]
  ]
}
// .browserslistrc
> 0.25%
not dead

@babel/core

Babel 的核心功能

@babel/cli

命令行使用的工具

@babel/preset-env

  • 转换 JavaScript 最新的 Syntax
  • 转换 JavaScript 最新的 API
  • stage 3 以上

参数

useBuiltIns

如何处理 polyfills

usage

根据环境每个文件都会引入 polyfills

corejs

  • 建议指定次要版本
  • 如果想使用 proposals
    • useBuiltIns: usage
      • shippedProposals :true 浏览器中发布了一段时间的提案
      • { version: "3.8", proposals: true } core-js@3.8 支持的提案

@babel/plugin-transform-runtime

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime // 需要根据 core-js 选项安装
  • 重用 babel helper
  • 解决 polyfill 全局污染
  • 不得设置 @babel/preset-env 中的 useBuiltIns 选项

如果需要提案,使用 {proposals: true}

corejs option Install command
false npm install --save @babel/runtime
2 npm install --save @babel/runtime-corejs2
3 npm install --save @babel/runtime-corejs3
  • 如果是业务项目开发者@babel/plugin-transform-runtime ,建议关闭 corejs,polyfill 的引入由 @babel/preset-env 完成,即开启 useBuiltIns(如需其他配置,自行根据诉求配置)。
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false
      }
    ]
  ]
}
  • 如果是 Library 开发者@babel/plugin-transform-runtime ,建议开启 corejs,polyfill 由 @babel/plugin-transform-runtime 引入。 @babel/preset-env 关闭 useBuiltIns
{
  "presets": [
    [
      "@babel/preset-env",
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ]
}
昨天 — 2025年9月15日首页

Web安全必备:关键 HTTP 标头解析

2025年9月15日 18:09

保护您的网站免受注入漏洞的侵害

内容安全政策 (CSP)

跨站脚本攻击 (XSS) 是一种攻击,攻击者会利用网站上的漏洞注入和执行恶意脚本。

Content-Security-Policy 通过限制网页可以执行哪些脚本,提供了额外的层级来缓解 XSS 攻击。

Content-Security-Policy:
  script-src 'nonce-{RANDOM1}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

可信类型

基于 DOM 的 XSS 是一种攻击,攻击者会将恶意数据传递到支持动态代码执行的接收器(例如 eval() 或 .innerHTML)。

Content-Security-Policy: require-trusted-types-for 'script'
// Feature detection
if (window.trustedTypes && trustedTypes.createPolicy) {
  // Name and create a policy
  const policy = trustedTypes.createPolicy('escapePolicy', {
    createHTML: str => {
      return str.replace(/</g, '&lt;').replace(/>/g, '&gt;');
    }
  });
}
// Assignment of raw strings is blocked by Trusted Types.
el.innerHTML = &#39;some string&#39;; // This throws an exception.

// Assignment of Trusted Types is accepted safely.
const escaped = policy.createHTML(&#39;&lt;img src=x onerror=alert(1)&gt;&#39;);
el.innerHTML = escaped;  // &#39;&amp;lt;img src=x onerror=alert(1)&amp;gt;&#39;

X-Content-Type-Options

如果恶意 HTML 文档是从您的网域中提交的(例如,上传到照片服务中的图片包含有效的 HTML 标记),某些浏览器会将其视为有效文档,并允许其在应用上下文中执行脚本,从而导致跨网站脚本漏洞

X-Content-Type-Options: nosniff 通过指示浏览器为给定响应在 Content-Type 标头中设置的 MIME 类型正确无误来防止此类问题。

X-Content-Type-Options: nosniff

将您的网站与其他网站隔离

X-Frame-Options

如果恶意网站可以将您的网站嵌入为 iframe,攻击者就可能会通过点击欺骗来诱导用户执行意外操作。此外,在某些情况下,Spectre 类型的攻击会让恶意网站有机会了解嵌入式文档的内容。

X-Frame-Options 用于指示是否应允许浏览器在 <frame><iframe><embed> 或 <object> 中渲染网页。

X-Frame-Options: DENY

跨源资源政策 (CORP)

攻击者可以嵌入来自其他来源(例如您的网站)的资源,以利用基于网络的跨网站数据泄露来了解这些资源。

Cross-Origin-Resource-Policy 通过指明可由哪些网站加载来缓解此风险。标头采用以下三个值之一:same-originsame-site 和 cross-origin。建议所有资源发送此标头,以指明它们是否允许由其他网站加载。

Cross-Origin-Resource-Policy: same-origin

跨源打开者政策 (COOP)

攻击者的网站可以利用基于网页的跨网站数据泄露,在弹出式窗口中打开另一个网站,以了解该网站的相关信息。在某些情况下,这可能还会允许利用基于 Spectre 的旁道攻击。

Cross-Origin-Opener-Policy 标头提供了一种方法,可让文档与通过 window.open() 打开的跨源窗口或不带 rel="noopener" 的 target="_blank" 链接隔离。因此,文档的任何跨源打开器都不会引用该文档,也无法与其互动。

Cross-Origin-Opener-Policy: same-origin-allow-popups

跨源资源共享 (CORS)

与本文中的其他内容不同,跨源资源共享 (CORS) 不是标头,而是一种用于请求和允许访问跨源资源的浏览器机制。

默认情况下,浏览器会强制执行同源政策,以防止网页访问跨源资源。例如,在加载跨源图片时,即使该图片在网页上以可视方式显示,网页上的 JavaScript 也无法访问该图片的数据。资源提供方可以通过选择启用 CORS 来放宽限制,并允许其他网站读取资源。

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

安全地构建强大的网站

跨源嵌入器政策 (COEP)

为了降低基于 Spectre 的攻击窃取跨源资源的能力,SharedArrayBuffer 或 performance.measureUserAgentSpecificMemory() 等功能默认处于停用状态。

Cross-Origin-Embedder-Policy: require-corp 会阻止文档和工作器加载跨源资源(例如图片、脚本、样式表、iframe 等),除非这些资源明确选择通过 CORS 或 CORP 标头加载。COEP 可与 Cross-Origin-Opener-Policy 结合使用,以选择将文档纳入跨源隔离

如需为文档启用跨源隔离,请使用 Cross-Origin-Embedder-Policy: require-corp

Cross-Origin-Embedder-Policy: require-corp

加密指向您网站的流量

HTTP 严格传输安全协议 (HSTS)

通过普通 HTTP 连接进行的通信不会加密,因此网络级窃听者可以访问传输的数据。

Strict-Transport-Security 标头会告知浏览器绝不应使用 HTTP 加载网站,而应改用 HTTPS。设置完毕后,在标头中定义的时间段内,浏览器将使用 HTTPS(而非 HTTP)访问网域,且不会重定向。

Strict-Transport-Security: max-age=31536000

优雅表格设计:CSS 美化技巧详解

2025年9月15日 18:08

一个普通 Table 分步骤进行美化 代码

<table>
  <thead>
    <tr>
      <th>Country</th>
      <th>Mean temperature change (°C)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>United Kingdom</th>
      <td>1.912</td>
    </tr>
    <tr>
      <th>Afghanistan</th>
      <td>2.154</td>
    </tr>
    <tr>
      <th>Australia</th>
      <td>0.681</td>
    </tr>
    <tr>
      <th>Kenya</th>
      <td>1.162</td>
    </tr>
    <tr>
      <th>Honduras</th>
      <td>0.945</td>
    </tr>
    <tr>
      <th>Canada</th>
      <td>1.284</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th>Global average</th>
      <td>1.4</td>
    </tr>
  </tfoot>
</table>
  • 添加标题
<table>
<caption>Annual surface temperature change in 2022</caption>
</table>
  • 增加行距,对齐方式

第一列左对齐,其他列右对齐

body {
  font-family: "Open Sans", sans-serif;
  line-height: 1.5;
}
table {
  text-align: left;
}
th,
caption {
  text-align: start;
}
thead th:not(:first-child),
td {
  text-align: end;
}
th,
td {
  padding: 0.25rem 0.75rem;
}
  • 添加边框
table {
  border-collapse: collapse;
}
th,
td {
  border: 1px solid;
}
  • 修改表格头部和尾部
thead {
  border-block-end: 2px solid;
  background: whitesmoke;
}
tfoot {
  border-block: 2px solid;
  background: whitesmoke;
}
th,
td {
  border: 1px solid lightgrey;
}
  • 添加颜色
table {
  --color: #d0d0f5;
}
thead,
tfoot {
  background: var(--color);
}
tbody tr:nth-child(even) {
  background: color-mix(in srgb, var(--color), transparent 60%);
}
  • 固定第一列
th:first-child {
  position: sticky;
  inset-inline-start: 0;
}
td:first-of-type,
:where(thead, tfoot) th:nth-child(2) {
  border-inline-start: none;
}

th:first-child::after {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  width: 1px;
  height: 100%;
  background: lightgrey;
}
  • 垂直对齐
th,
td {
  vertical-align: baseline;
}

thead th {
  vertical-align: bottom;
}
  • 列宽度自适应
thead th:not(:first-child) {
  width: 9rem;
}
table {
  width: max(65rem, 100%);
  /* 浏览器忽略单元格内容,而是使用在第一个表格行的列或单元格上定义的宽度来解析列宽 */
  table-layout: fixed;
}
th:first-of-type {
  width: 10rem;
}
  • 无障碍
<div
  class="wrapper"
  tabindex="0"
  role="region"
  aria-labelledby="tableCaption_01"
>
  <table>
    <caption id="tableCaption_01"></caption>
    <thead>
      <tr>
        <th scope="column">Country</th>
        <th scope="column">Mean temperature change (°C)</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">United Kingdom</th>
        <td>1.912</td>
      </tr>
      <tr>
        <th scope="row">Afghanistan</th>
        <td>2.154</td>
      </tr>
      <tr>
        <th scope="row">Australia</th>
        <td>0.681</td>
      </tr>
      <tr>
        <th scope="row">Kenya</th>
        <td>1.162</td>
      </tr>
      <tr>
        <th scope="row">Honduras</th>
        <td>0.945</td>
      </tr>
      <tr>
        <th scope="row">Canada</th>
        <td>1.284</td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <th scope="row">Global average</th>
        <td>1.4</td>
      </tr>
    </tfoot>
  </table>
</div>
[role="region"][aria-labelledby][tabindex]:focus {
  outline: 0.1em solid rgba(0, 0, 0, 0.1);
}

div[tabindex="0"][aria-labelledby][role="region"] {
  background: linear-gradient(to right, transparent 30%, rgba(255, 255, 255, 0)),
    linear-gradient(to right, rgba(255, 255, 255, 0), white 70%) 0 100%,
    radial-gradient(
      farthest-side at 0% 50%,
      rgba(0, 0, 0, 0.2),
      rgba(0, 0, 0, 0)
    ),
    radial-gradient(
        farthest-side at 100% 50%,
        rgba(0, 0, 0, 0.2),
        rgba(0, 0, 0, 0)
      )
      0 100%;
  background-repeat: no-repeat;
  background-color: #fff;
  background-size: 4em 100%, 4em 100%, 1.4em 100%, 1.4em 100%;
  background-position: 0 0, 100%, 0 0, 100%;
  background-attachment: local, local, scroll, scroll;
}

网页深色模式完整实现:从响应式设计到系统主题联动

2025年9月15日 17:33

需求

  • 页面跳转时主题不要发生变化
  • 如果是第一个页面,自动使用主题
  • 默认 light 还是 dark
  • 在一个浏览器选项卡中更改主题时,网站的所有其他选项卡也应随之更改
  • 用户修改操作系统主题模式时,网站应该对此做出反应
  • 根据时间变化自动切换主题

实现

在 <head> 中添加 <meta name="color-scheme" content="light dark">

浏览器只会改变那些没有主动设置颜色的元素

CSS 中通过 light-dark() 设置不同的颜色

:root {
    color-scheme: light dark;
}

@media (prefers-color-scheme: light) {
    .element {
        color: black;
        background-color: white;
    }
}

@media (prefers-color-scheme: dark) {
    .element {
        color: white;
        background-color: black;
    }
}
:root {
    color-scheme: light dark;
}

.element {
    /* fallback 的颜色,当用户浏览器不支持 color: light-dark(black, white); 时,回退到这个颜色 */
    color: black;
    /* light mode 下 color 用 black, dark mode 下 color 用 white */
    color: light-dark(black, white);
    background-color: white;
    background-color: light-dark(white, black);
}
<html class="theme-light">
  <form class="theme-selector">
  <button
    aria-label="Enable light theme"
    aria-pressed="false"
    role="switch"
    type="button"
    id="theme-light-button"
    class="theme-button enabled"
    onclick="enableTheme('light', true)"
  >Light theme</button>
  <button
    aria-label="Enable dark theme"
    aria-pressed="false"
    role="switch"
    type="button"
    id="theme-dark-button"
    class="theme-button"
    onclick="enableTheme('dark', true)"
  >Dark theme</button>
  </form>
  <!--- Rest of the website --->
</html>
$theme-light-text-color: #111;
$theme-dark-text-color: #EEE;

@mixin color($property, $var, $fallback){
  #{$property}: $fallback; // This is a fallback for browsers that don't support the next line.
  #{$property}: var($var, $fallback);
}

p{
  @include color(color, --text-color, $theme-light-text-color);
}
.theme-dark{
  --text-color: #{$theme-dark-text-color};
}
// Find if user has set a preference and react to changes
(function initializeTheme(){
  syncBetweenTabs()
  listenToOSChanges()
  enableTheme(
    returnThemeBasedOnLocalStorage() ||
    returnThemeBasedOnOS() ||
    returnThemeBasedOnTime(),
    false)
}())

// Listen to preference changes. The event only fires in inactive tabs, so theme changes aren't applied twice.
function syncBetweenTabs(){
  window.addEventListener('storage', (e) => {
    const root = document.documentElement
    if (e.key === 'preference-theme'){
      if (e.newValue === 'light') enableTheme('light', true, false)
      else if (e.newValue === 'dark') enableTheme('dark', true, false) // The third argument makes sure the state isn't saved again.
    }
  })
}

// Add a listener in case OS-level preference changes.
function listenToOSChanges(){
  let mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')

  mediaQueryList.addListener( (m)=> {
    const root = document.documentElement
    if (m.matches !== true){
      if (!root.classList.contains('theme-light')){
        enableTheme('light', true)
      }
    }
    else{
      if(!root.classList.contains('theme-dark')) enableTheme('dark', true)
    }
  })
}

// If no preference was set, check what the OS pref is.
function returnThemeBasedOnOS() {
  let mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')
  if (mediaQueryList.matches) return 'dark'
else {
    mediaQueryList = window.matchMedia('(prefers-color-scheme: light)')
    if (mediaQueryList.matches) return 'light'
    else return undefined
}
}

// For subsequent page loads
function returnThemeBasedOnLocalStorage() {
  const pref = localStorage.getItem('preference-theme')
  const lastChanged = localStorage.getItem('preference-theme-last-change')
  let now = new Date()
  now = now.getTime()
  const minutesPassed = (now - lastChanged)/(1000*60)

  if (
    minutesPassed < 120 &&
    pref === "light"
  ) return 'light'
  else if (
    minutesPassed < 120 &&
    pref === "dark"
  ) return 'dark'
  else return undefined
}

// Fallback for when OS preference isn't available
function returnThemeBasedOnTime(){
  let date = new Date
  const hour = date.getHours()
  if (hour > 20 || hour < 5) return 'dark'
  else return 'light'
}

// Switch to another theme
function enableTheme(newTheme = 'light', withTransition = false, save = true){
  const root = document.documentElement
  let otherTheme
  newTheme === 'light' ? otherTheme = 'dark' : otherTheme = 'light'
  let currentTheme
  (root.classList.contains('theme-dark')) ? currentTheme = 'dark' : 'light'

  if (withTransition === true && newTheme !== currentTheme) animateThemeTransition()

  root.classList.add('theme-' + newTheme)
  root.classList.remove('theme-' + otherTheme)

  let button = document.getElementById('theme-' + otherTheme + '-button')
  button.classList.add('enabled')
  button.setAttribute('aria-pressed', false)

  button = document.getElementById('theme-' + newTheme + '-button')
  button.classList.remove('enabled')
  button.setAttribute('aria-pressed', true)

  if (save) saveToLocalStorage('preference-theme', newTheme)
}

// Save the state for subsequent page loads
function saveToLocalStorage(key, value){
  let now = new Date()
  now = now.getTime()
  localStorage.setItem(key, value)
  localStorage.setItem(key+"-last-change", now)
}

// Add class to smoothly transition between themes
function animateThemeTransition(){
  const root = document.documentElement
  root.classList.remove('theme-change-active')
  void root.offsetWidth // Trigger reflow to cancel the animation
  root.classList.add('theme-change-active')
}
(function removeAnimationClass(){
  const root = document.documentElement
  root.addEventListener(supportedAnimationEvent(), ()=>root.classList.remove('theme-change-active'), false)
}())

function supportedAnimationEvent(){
  const el = document.createElement("f")
  const animations = {
    "animation"      : "animationend",
    "OAnimation"     : "oAnimationEnd",
    "MozAnimation"   : "animationend",
    "WebkitAnimation": "webkitAnimationEnd"
  }

  for (t in animations){
    if (el.style[t] !== undefined) return animations[t]   // Return the name of the event fired by the browser to indicate a CSS animation has ended
  }
}

参考

A guide to implementing dark modes on websites | Koos Looijesteijn

内容安全策略(CSP)深度指南:从基础配置到高级防护

2025年9月15日 17:32

概念

HTTP 响应标头 Content-Security-Policy 允许站点管理者控制用户代理能够为指定的页面加载哪些资源。除了少数例外情况,设置的政策主要涉及指定源服务器和脚本端点。这将帮助防止跨站脚本攻击

解释:哪些文件可以在网站上运行

注意语法规则标点符号使用

Content-Security-Policy: script-src 'self' https://safe-external-site.com; style-src 'self'

Fetch 指令

Fetch 指令控制指定资源类型可以从哪里加载。

  • default-src: 默认策略,没有具体指定策略 default-src 'self' trusted-domain.com
  • img-src: 图片 img-src 'self' img.mydomain.com
  • font-src: 字体
  • object-src: <object><embed>
  • media-src: 视频、音频
  • script-src 脚本
  • style-src css

Fetch 指令语法

  • 'none' :不匹配任何内容
  • 'self':匹配当前主机域(同源,即主机和端口)。但是,不要匹配子域
  • 'unsafe-inline':允许内联 JavaScript 和 CSS,尽量不要使用,nonce 代替 unsafe-inline
  • 'unsafe-eval':允许动态文本用于 JavaScript eval
  • domain.example.com:允许从指定域加载资源。要匹配任何子域,请使用 * 通配符,例如 *.example.com
  • https: 或 ws:: 仅允许通过 HTTPS 或 WebSocket 加载资源
  • nonce-{token}:允许包含相同 nonce 属性值的内联脚本或 CSS
  • 'strict-dynamic' 关键字使得通过 nonce 或 hash 信任的脚本扩展到此脚本动态加载的脚本,例如通过使用 Document.createElement() 创建新的 <script> 标签,然后通过 Node.appendChild() 将其插入文档中

使用

  • 服务器 nodenginx
    • Nginx 中可以使用内置变量的 $request_id 作为唯一 id,而当 nginx 版本不支持时,则可以借助 lua 去生产一个 uuid
    • 接着通过 Nginx 的 sub_filter NONCE_TOKEN 'id' 将页面中的 NONCE_TOKEN 占位符替换为 id,或者使用 lua 进行替换
    • 最后使用 add_header Content-Security-Policy "script-src 'nonce-{id}'" 添加对应的 CSP 返回头
Content-Security-Policy: script-src 'nonce-5fAifFSghuhdf' 'strict-dynamic'
  • html meta 标签
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://safe-external-site.com">

HTTP Content-Security-Policy-Report-Only响应头允许 web 开发人员通过监测 (但不强制执行) 政策的影响来尝试政策。这些违反报告由 JSON 文档组成通过一个 HTTP POST 请求发送到指定的 URI。

Reporting-Endpoints: name-of-endpoint="后端请求地址"
Content-Security-Policy: default-src 'self'; report-to name-of-endpoint
❌
❌