如何居中一个div元素

本文翻译自《How To Center a Div》。

长期以来,将元素置于其父元素内是一件非常棘手的事情。随着CSS的发展,我们获得了越来越多的工具来解决这个问题。如今,我们有很多选择!

我决定创建本教程来帮助你了解不同方法之间的权衡,并为你提供一系列可用于处理各种场景的居中策略。

老实说,这比我最初想象的要有趣得多。即使你已经使用CSS一段时间了,我敢打赌你至少会学到1种新策略!

使用自动边距(auto margin)来居中

我们将要介绍的第一个策略是最古老的策略之一。如果我们想将元素水平居中,我们可以使用设置为特殊值auto的边距(margin)来实现:

.element {
  max-width: fit-content;
  margin-left: auto;
  margin-right: auto;
}

(译者注:案例请看原文)

首先,我们需要限制元素的宽度;默认情况下,流式布局(Flow layout)中的元素将水平扩展其宽度以填充可用空间,我们无法真正将全宽的东西居中。

我可以用一个固定值(例如200px)来限制宽度,但在上述场景下,我真正想要的是让元素收缩到其内容的周围。 fit-content是一个神奇的值,它就是用来做这个的。本质上,它使“宽度”表现得像“高度”,这样元素的大小就由其内容决定了。

为什么我要使用max-width而不是width因为我的目标是阻止元素水平扩展。我想限制其最大尺寸。如果我改用width,它会将其宽度锁定为其内容的宽度,当容器非常窄时,这个元素就会溢出。使用max-width,如果将改变“容器宽度”的滑块一直拖到最左侧,你会看到该元素的宽度随其容器一起缩小。

现在我们的元素受到限制,我们可以用自动边距将其居中。 我喜欢将自动边距想象成“饥饿的河马”。每个自动边距都会尝试吞噬尽可能多的空间。例如,看看如果我们只设置margin-left: auto会发生什么:

.element {
  max-width: fit-content;
  margin-left: auto;
}

译者注:案例请看原文)

margin-left是唯一具有自动边距的一侧时,所有额外空间都会作为边距应用于该侧。当我们同时设置margin-left: automargin-right: auto时,两只河马会各自占用相同大小的空间。这就会将元素强制置于中间。

另外:我一直使用margin-leftmargin-right,因为我对它们很熟悉,但有一种更好、更现代的方法可以做到这一点:

.element {
  max-width: fit-content;
  margin-inline: auto;
}

(译者注:案例请看原文)

margin-inline将把margin-leftmargin-right设置为相同的值(例如上例的auto)。它具有非常好的浏览器支持,几年前就已登陆所有主流浏览器。

尽管这种居中方法已经存在很久了,但我仍然发现自己经常使用它!当我们想将单个子元素居中而不影响其任何兄弟元素时,这种方法特别有用(例如,博客文章中段落之间的图像)。

逻辑属性(logical properties)

margin-inline不仅仅是margin-left + margin-right的便捷简写。它是逻辑属性集合的一部分,旨在使网络国际化变得更加容易。

在英语中,字符从左到右水平书写。这些字符组成单词和句子,并组装成“块”(段落、标题、列表等)。块从上到下垂直堆叠。我们可以将此视为英语网站的内容方向。

但这并不是通用的!有些语言,如阿拉伯语和希伯来语,是从右到左书写的。其他语言,如中文,历史上是垂直书写的,字符从上到下,块从一边到另一边。

逻辑属性的主要目标是创建一个超越这些差异的抽象。我们可以使用margin-inline-start,而不是为从左到右的语言设置margin-left并将其翻转为margin-right以适应从右到左的语言。margin-inline-start会根据页面的语言,边距将自动应用于正确的一侧。

使用弹性盒(Flexbox)布局来居中

Flexbox旨在让我们在沿主轴分布一组元素时拥有强大的控制力。它提供了一些非常强大的居中工具!

使用SSH客户端导出同一个id_ecdsa_256私钥,每次内容都不一样的原因

在 Xshell 或其他 SSH 客户端中,每次导出相同的 ECDSA 私钥(id_ecdsa_256),但其文件内容不同,通常是因为 PEM 格式的加密方式和随机化 影响了最终的存储结果。具体而言,有以下几个关键原因。

1. PEM 格式中的加密随机性

如果你的私钥是加密存储的(即在导出时选择了密码保护),那么PEM 格式(通常是 OpenSSL 兼容的)会使用 PBKDF2 或 bcrypt 进行密钥派生,并为每次导出生成 随机盐值。由于加密盐值不同,即使私钥的核心内容相同,最终的加密数据会有所不同。

可以检查你的私钥文件头部,例如:

-----BEGIN ENCRYPTED PRIVATE KEY-----

如果看到 ENCRYPTED,则意味着该私钥是加密存储的,每次导出的加密数据可能不同。

2. ASN.1 序列化中的随机性

即使你的私钥未加密,它仍然可能以 ASN.1(Abstract Syntax Notation One)编码的 DER(Distinguished Encoding Rules)格式存储:

  • ASN.1 的 DER 编码可能会有不同的填充方式,导致即使内容相同,字节级表示不同。
  • 在某些情况下,ECDSA 私钥的元数据(如生成时间)可能会导致数据的微小变化。

3. 不同的软件导出方式

不同的 SSH 客户端或 OpenSSL 版本可能会使用不同的方式序列化私钥:

  • Xshell 可能在导出时重新编码了私钥,使其格式不同,但实际上仍然是同一个密钥。
  • 使用 OpenSSH 或 OpenSSL 重新导出同一私钥,也可能会生成不同的文件内容,但仍然对应相同的公钥。

4. Base64 编码的多样性

如果你的私钥是以 PEM 格式(Base64 编码)存储,则:

  • 行长(Line Wrapping) 可能不同,即 Base64 编码的换行方式可以有所变化。
  • PEM 头部或尾部的额外信息 可能发生改变,但不影响核心私钥内容。

5. 验证是否是相同私钥

你可以使用以下方法验证你的私钥是否实际上是相同的:

5.1 查看公钥是否一致

ssh-keygen -y -f id_ecdsa_256

如果你每次导出的私钥 生成的公钥相同,则可以确认它们是相同的私钥,只是存储格式不同。

5.2 查看私钥指纹

ssh-keygen -lf id_ecdsa_256

这个命令会计算私钥的指纹,若指纹相同,则说明私钥内容本质上是一样的。

5.3 比较 DER 格式

你可以将 PEM 格式转换为 DER 格式并比较:

openssl ec -in id_ecdsa_256 -outform DER -out id_ecdsa_256.der

md5sum id_ecdsa_256.der

如果 MD5 哈希值相同,说明私钥实际上是一样的。

Rust库程序编译为.dll或.so文件的方法

Rust 语言的库代码可以编译为 Windows 系统的 .dll 文件或 Linux 系统的 .so 文件。Rust 提供了对动态链接库(shared library,共享库)的良好支持,可以生成这些共享库以供 C、C++ 或其他语言使用。

1 编译为 Windows .dll 文件 在 Cargo.toml 中,需要指定 crate-type 为 cdylib 或 dylib:

[lib]
name = "mylib"
crate-type = ["cdylib"]

然后编写 Rust 代码(src/lib.rs):

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

使用 cargo build –release 进行编译后,在 target/release/ 目录下会生成 mylib.dll 文件。

2 编译 Linux .so 文件

在 Linux 上,同样的 Cargo.toml 适用于 .so 文件,Rust 会自动根据目标平台生成 .so:

[lib]
name = "mylib"
crate-type = ["cdylib"]

Rust 代码保持不变,使用 cargo build –release,Rust 会在 target/release/ 目录下生成 libmylib.so。

3 crate-type的值

  • cdylib:生成 C 兼容的动态库,适用于其他语言(如 Python、C++)调用。
  • dylib:Rust 专用动态库,带有 Rust 运行时,通常用于 Rust 内部。

如果是给其他语言(如 C 或 Python)使用,推荐 cdylib,因为它不会带 Rust 运行时。

4 跨平台编译(交叉编译)

Rust 允许通过 –target 选项进行交叉编译。 Windows 交叉编译 Linux .so:

cargo build --release --target x86_64-unknown-linux-gnu

Linux 交叉编译 Windows .dll:

cargo build --release --target x86_64-pc-windows-gnu

5 示例:C 语言调用 Rust 共享库

在 C 代码中调用 Rust 编写的 add 函数:

#include <stdio.h>

extern int add(int a, int b);

int main() {
    printf("Result: %d\n", add(2, 3));
    return 0;
}

然后

  • Windows 需链接 mylib.dll
  • Linux 需链接 libmylib.so

结论

Rust 可以生成 .dll 和 .so 文件,并且可以通过 #[no_mangle] 和 extern “C” 让 Rust 代码能被 C 语言或其他语言调用。使用 cargo build –release 可编译出目标共享库,并可结合 –target 进行跨平台编译。

使用Solana主网账户和程序(合约)

通常,本地测试依赖于默认情况下本地验证器上不可用的程序(合约)和帐户。Solana CLI允许:

  • 下载程序(合约)和帐户
  • 将程序(合约)和帐户加载到本地验证器

如何从主网下载帐户

可以将JUP代币铸币帐户下载到本地文件中:

# solana account -u <source cluster> --output <output format> --output-file <destination file name/path> <address of account to fetch>
solana account -u m --output json-compact --output-file jup.json JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN

然后通过在启动验证器时(在本地集群上)传递帐户的文件和目标地址将其加载到你的本地网络:

# solana-test-validator --account <address to load the account to> <path to account file> --reset
solana-test-validator --account JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN jup.json --reset

同样,也可以下载 Openbook 程序:

# solana program dump -u <source cluster> <address of account to fetch> <destination file name/path>
solana program dump -u m srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX openbook.so

然后通过在启动验证器时(在本地集群上)传递程序的文件和目标地址将其加载到你的本地网络:

# solana-test-validator --bpf-program <address to load the program to> <path to program file> --reset
solana-test-validator --bpf-program srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX openbook.so --reset

参考

Using Mainnet Accounts and Programs

订阅Solana事件

Solana提供了一个基于Websocket的发布/订阅RPC接口,你可以在其中监听某些事件。你无需每隔一段时间ping一个HTTP端点来获取频繁的更新,而是可以只在更新事件发生时接收这些更新。

当你创建新的连接(Connection)实例时,Solana的web3 Connection 会生成一个websocket端点并注册一个websocket客户端(请参阅此处的源代码)。

Connection类公开了发布pub/订阅sub方法 – 它们都以on开头,就像事件发射器一样。当你调用这些监听器方法时,它会向该Connection实例的websocket客户端注册一个新的订阅。下面我们使用的示例发布/订阅方法是onAccountChange。回调将通过参数提供更新的状态数据(请参阅AccountChangeCallback作为示例)。

/* subscribe-to-events.ts */
import { clusterApiUrl, Connection, Keypair } from "@solana/web3.js";

(async () => {
  // 建立到开发网devnet的新连接 - 连接到开发网devnet的websocket客户端也在此处注册
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  // 创建一个测试钱包来监听事件
  const wallet = Keypair.generate();

  // 注册一个回调函数来来监听这个钱包的账户变化信息(即websocket订阅)
  connection.onAccountChange(
    wallet.publicKey,
    (updatedAccountInfo, context) =>
      console.log("Updated account info: ", updatedAccountInfo),
    "confirmed",
  );
})();

参考

Subscribing to Events

获取测试用SOL币

当你在本地工作时,你需要一些SOL才能进行交易。在非主网环境中,你可以通过将SOL空投到你的地址来接收SOL:

/* TS使用grill库 */
import {
  address,
  lamports,
  airdropFactory,
  createSolanaClient,
  LAMPORTS_PER_SOL,
} from "gill";

const { rpc, rpcSubscriptions, sendAndConfirmTransaction } = createSolanaClient(
  {
    urlOrMoniker: "devnet", // or `localnet`, etc
  },
);

const wallet = address("nick6zJc6HpW3kfBm4xS2dmbuVRyb5F3AnUvj5ymzR5");

const { value: initialBalance } = await rpc.getBalance(wallet).send();
console.log("Initial balance:", initialBalance);

/**
 * 注意:开发网devnet和测试网testnet有空投速率限制。
 * 强烈建议使用本地测试网localnet和本地测试验证器
 */
await airdropFactory({ rpc, rpcSubscriptions })({
  commitment: "confirmed",
  lamports: lamports(LAMPORTS_PER_SOL), // 申请1 SOL空投
  recipientAddress: wallet,
});

const { value: newBalance } = await rpc.getBalance(wallet).send();
console.log("New balance:", newBalance);

/* TS使用web3.js库 */
import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";

const connection = new Connection("http://127.0.0.1:8899", "confirmed");

const wallet = new PublicKey("nick6zJc6HpW3kfBm4xS2dmbuVRyb5F3AnUvj5ymzR5");

/**
 * 注意:开发网devnet和测试网testnet有空投速率限制。
 * 强烈建议使用本地测试网localnet和本地测试验证器
 */
const signature = await connection.requestAirdrop(wallet, LAMPORTS_PER_SOL); // 申请1 SOL空投

const { blockhash, lastValidBlockHeight } =
  await connection.getLatestBlockhash();

// 注意:确认空投交易是非常重要的,用来确保你的钱包拿到空投
await connection.confirmTransaction({
  blockhash,
  lastValidBlockHeight,
  signature,
});

参考

Getting Test SOL

连接到Solana环境

当你进行Solana开发时,你需要连接到特定的RPC API端点。Solana有3个公共开发环境:

  • 主网beta(mainnet-beta):https://api.mainnet-beta.solana.com
  • 开发网(devnet):https://api.devnet.solana.com
  • 测试网(testnet):https://api.testnet.solana.com

使用网络昵称进行连接

使用昵称或名称连接到Solana公共RPC端点:

/* TS使用grill库 */
import { createSolanaClient } from "gill";

const { rpc, rpcSubscriptions } = createSolanaClient({
  urlOrMoniker: "devnet", // or `mainnet`, `localnet`, etc
});

/* TS使用web3.js库 */
import { clusterApiUrl, Connection } from "@solana/web3.js";

const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");

使用特定RPC URL进行连接

要连接到特定RPC API端点:本地测试网验证器或远程RPC提供程序,请使用以下命令:

/* TS使用grill库 */
import { createSolanaRpc, createSolanaRpcSubscriptions, devnet } from "gill";

const rpc = createSolanaRpc(devnet("https://api.devnet.solana.com"));

const rpcSubscriptions = createSolanaRpcSubscriptions(
  devnet("wss://api.devnet.solana.com"),
);

/* TS使用web3.js库 */
import { Connection } from "@solana/web3.js";

// 以下一行代码将连接到本地测试网验证器
const connection = new Connection("http://127.0.0.1:8899", "confirmed");

参考

Connecting to a Solana Environment

如何启动一个Solana本地验证者节点(Local Validator)

在本地测试你的程序代码比在devnet上测试更可靠,并且可以帮助你在devnet上试用之前进行测试。

你可以通过安装Solana CLI工具套件并运行以下命令来设置本地测试验证器:

solana-test-validator

使用本地测试节点的好处包括:

  • 无RPC速率限制
  • 无空投数量限制
  • 直接链上程序部署 (–bpf-program …)
  • 从公共集群克隆帐户,包括程序 (–clone …)
  • 可配置的交易历史保留 (–limit-ledger-size …)
  • 可配置的epoch长度 (–slots-per-epoch …)
  • 跳转到任意一个slot (–warp-slot …)

常见问题及解决方法

启动solana-test-validator报错Aborted (core dumped)

$ solana-test-validator
Aborted (core dumped)

你可以使用--log选项查看具体错误信息:

$ solana-test-validator --log
[2025-02-28T08:47:17.500858651Z INFO  solana_test_validator] agave-validator 2.1.14 (src:3ad46824; feat:3271415109, client:Agave)
[2025-02-28T08:47:17.500903233Z INFO  solana_test_validator] Starting validator with: ArgsOs {
        inner: [
            "solana-test-validator",
            "--log",
        ],
    }
[2025-02-28T08:47:17.500940423Z WARN  solana_perf] CUDA is disabled
[2025-02-28T08:47:17.500970698Z INFO  solana_perf] AVX detected
[2025-02-28T08:47:17.500975929Z ERROR solana_perf] Incompatible CPU detected: missing AVX2 support. Please build from source on the target
Aborted (core dumped)

以上报错表明你的CPU不支持AVX2指令集。运行以下命令,如果没有任何输出就表明你的CPU不支持AVX2指令集:

$ grep avx2 /proc/cpuinfo

AVX2(Advanced Vector Extensions 2)是 Intel 和 AMD 的现代处理器中提供的一种 SIMD(Single Instruction, Multiple Data)指令集扩展。AVX2是AVX(Advanced Vector Extensions)指令集的后续版本,增加了更多的指令以提高数据并行性和处理效率。

主要特点:

  • SIMD计算:AVX2 提供对128位和256位宽度的SIMD指令支持,用于并行处理多个数据。
  • 整数指令扩展:与AVX相比,AVX2不仅扩展了对浮点数运算的支持,还增强了对整数运算的支持,允许更高效的向量化处理。
  • 增强的性能:AVX2在执行大规模数据计算时,特别是在进行矩阵运算、图形处理、加密、压缩算法等方面能显著提升性能。

哪些CPU支持AVX2:

  • Intel: 从**Haswell(第四代Core系列处理器)**开始,Intel处理器支持AVX2。
  • AMD: 从**Steamroller(Kaveri架构)**及后续处理器开始,AMD也开始支持AVX2。

因此,如果你的CPU是Intel Haswell架构及以后的系列,或者是AMD Kaveri架构及以后的系列,那么它就支持AVX2指令集。

如果你的CPU不支持AVX2指令集,那么方法一是下载Solana源码自己构建,方法二是使用Docker运行非AVX版本的Solana。

方法一,下载Solana源码自己构建

步骤如下:

1 先更新一下系统的软件包

对于Ubuntu:

$ sudo apt-get update

2 安装依赖项

$ sudo apt-get install libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang make

3 安装Rust

$ curl https://sh.rustup.rs -sSf | sh
$ source $HOME/.cargo/env
$ rustup component add rustfmt

4 下载Solana源码

$ mkdir ~/solana && cd ~/solana
$ git clone https://github.com/solana-labs/solana.git

5 构建

$ cd solana

切换到稳定版本分支:

$ git checkout v1.18.26

执行构建:

$ cargo clean
$ export RUST_REED_SOLOMON_ERASURE_ARCH=native
$ export RUSTFLAGS="-C target-feature=-avx2"
$ ./scripts/cargo-install-all.sh .

6 运行solana-test-validator

我们是从克隆Solana的git存储库构建得到二进制文件,就不需要先执行solana-install init命令了,直接执行solana-test-validator启动一个本地验证者节点:

$ ./target/release/solana-test-validator

如果输出如下信息:

Ledger location: test-ledger
Log: test-ledger/validator.log
⠤ Initializing...                                                                                                                                                                                               Waiting for fees to stabilize 1...
Identity: H4gRUMNMJfd8fsVDhnvmnt4SenqELPdtUAEuGFLBFj5c
Genesis Hash: 4NHw8wya1tD2eFbLPpRwtVeAKBAKSd8EiARk8h3u3QGj
Version: 1.18.26
Shred Version: 47462
Gossip Address: 127.0.0.1:1024
TPU Address: 127.0.0.1:1027
JSON RPC URL: http://127.0.0.1:8899
WebSocket PubSub URL: ws://127.0.0.1:8900

表示本地测试网启动成功!

通过上述步骤构建得到的Solana的可执行的二进制文件在~/solana/solana/target/release/和~/solana/solana/bin/里。可以把以下这行代码写入家目录里的.profile文件里,就可以直接使用可执行的二进制文件的名字来运行它了:

export PATH="~/solana/solana/bin:$PATH"

使.profile文件的修改生效:

$ source ~/.profile

7 创建一个Solana账户

执行以下命令创建一个Solana账户:

$ solana-keygen new

要记住生成的Solana账户的种子和公钥。

8 在本地测试网申请空投(水龙头币)

连接到本地测试网:

$ solana config set --url http://localhost:8899

在本地测试网请求空投:

$ solana airdrop 1

返回如下结果:

Requesting airdrop of 1 SOL

Signature: 5a6YXCwMVgwmxBz7p4g2LnTuhK2hVKqGW9QfUguJmruxLDvWKwq1xx1W2XuUiAJuqLmxEtPUAgQcri5RRjjouNQP

500000001 SOL

可以看到我们的账号里有500000001个SOL。因为默认情况下,本地测试网会向你的本地钱包账户(~/.config/solana/id.json)空投50000000个SOL,以便你能够进行测试、部署智能合约和交易。 可以使用以下命令查询本地钱包账户的余额:

$ solana balance
500000001 SOL

也可以通过RPC接口查询账户的余额:

curl --location --request POST 'http://localhost:8899' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getBalance",
    "params": [
        "AoKpX1XF22sJWpuv2gE36YEYekQV5EeQghyPSnYvLTkv"
    ]
}'

其中

  • http://localhost:8899是本地测试网的网址
  • AoKpX1XF22sJWpuv2gE36YEYekQV5EeQghyPSnYvLTkv是账户的公钥,也是账户的地址

返回如下结果:

{"jsonrpc":"2.0","result":{"context":{"apiVersion":"1.18.26","slot":4506},"value":500000001000000000},"id":1}

可以看到余额是500000001000000000,因为SOL的精度是10^9,所以也就是500000001个SOL。

方法二,使用Docker运行非AVX版本的Solana

我暂时没有在dockerhub网站找到非AVX版本的Solana镜像。

参考

How to Start a Local Validator

https://solana.stackexchange.com/questions/4703/how-to-fix-solana-test-validator-illegal-instruction-core-dumped-error-even-af

https://github.com/solana-labs/solana/issues/21233

https://stackoverflow.com/questions/71770717/anchor-test-without-avx2-cpu

基于CSS网格布局的全屏布局实现

本文翻译自《Full-Bleed Layout Using CSS Grid》。

过去,每个人都力图创建一种黄金标准的网站布局:圣杯布局。但众所周知,这种布局很难做到正确。

这看起来似乎不是那么棘手,对吧?但那是flex布局出现之前的时代;我们用于这项工作的工具是表格和浮动布局,但两者都不能真正胜任这项任务。这在技术上是可行的,但需要一些额外技巧。

一旦flex获得主流浏览器支持,这种布局就从“圣杯”变成了“喷泉饮料”;它无处不在,因为它提供了出色的用户体验,并且所有开发人员都可以使用。

随着网络的发展,我发现了一种新的理想的布局。它提供了极好的用户体验,尤其是对于新闻文章或文档等长篇文本内容。但是,就像它的前辈一样,它很难实现;大多数实现都需要晦涩难懂的黑客手段或违反直觉的技巧。

我最近发现了一个使用CSS Grid的优雅解决方案。在这篇文章中,我们将了解它的工作原理!

问题所在

你曾经尝试过在超大屏幕上阅读维基百科吗?它看起来像这样:

这些段落太宽了!维基百科根本不限制容器的宽度。这导致一行的长度有数百个字符。

当我们到达一行的末尾时,我们的眼睛很难回过头来。如果你像我一样,你最终会使用鼠标来辅助:

除了换行问题之外,太宽的文本行通常也难以阅读;它会使眼睛疲劳。

研究表明理想的行长约为65个字符。在罗马字母的背景下,45到85之间的任何字符通常都被认为是可以接受的。阅读是一个复杂的过程,我们应该努力让它尽可能简单。

此问题的标准解决方案是在页面中心创建一个固定宽度的列。你到处都见过这种布局:在线杂志、文档、新闻网站和博客。你现在正在当前网站上看到它!

然而,还有一个复杂的因素——并非所有内容都应受到限制。我们应该允许图像、视频和自定义小部件的宽度可自由调节:

这种事情的常用术语是“全出血(full-bleed)”。这是从出版界借用的术语;当某些东西以全出血方式打印时,它会延伸到纸张的最边缘。

这项新要求使问题变得更加棘手。限制所有子元素相对容易,但CSS实际上没有选择性地限制某些子元素的机制。

解决方案

让我们从最后开始,看看我们的解决方案:

.wrapper {
  display: grid;
  grid-template-columns:
    1fr
    min(65ch, 100%)
    1fr;
}

.wrapper > * {
  grid-column: 2;
}

.full-bleed {
  width: 100%;
  grid-column: 1 / 4;
}

这些样式应用于以下HTML:

<main class="wrapper">
  <h1>Some Heading</h1>
  <p>Some content and stuff</p>
  <img class="full-bleed" alt="cute meerkat" src="/meerkat.jpg" />
</main>

让我们一步步解释以上样式。

网格

.wrapper {
  display: grid;
  grid-template-columns:
    1fr
    min(65ch, 100%)
    1fr;
}

如果你不熟悉CSS Grid布局,这可能看起来像是很多随机字符和关键字。不要害怕!一切都会解释清楚。

grid-template-columns是一种属性,可让我们定义网格的形状。通过提供3个离散的属性值,我们表示我们想要3列。

这些值定义每列的宽度。第一列是1fr,与最后一列相同。fr单位是一个可填充可用空间的灵活单位。它在原理上类似于flex-grow;它是列应占用多少可用空间的比率。

我们的中心列是固定宽度的。我们使用min助手函数来选择最终较小的值。在大屏幕上,它将占用65ch宽度。在较小的屏幕上,如果没有足够的水平空间容纳65个字符,它将被限制为可用容器宽度的100%。

(如果你使用Sass,min关键字将无法正常工作,因为它已经是Sass预处理器中的辅助程序。查看解决方法。)

实际上,上述HTML代码在浏览器中看起来是这样的:

它很有特色

正如我们讨论过的,我们希望限制文本的长度,以便每行大约有65个字符宽。我们可以使用像素“估计”宽度,但CSS有一个模糊的单位可以让我们的生活更轻松一些:ch

.wrapper {
  /* 估计的宽度 */
  width: 800px;

  /* 理想的宽度 */
  width: 65ch;
}

ch是一个单位,就像px或rem。它对应于当前字体中0字符的宽度,适用于指定的字体大小。我们不是通过逆向工程获得像素宽度,而是以字符为单位指定宽度。

实际上,你可能仍需要进行一些调整;例如,如果你的字体中0字符特别窄,则需要稍微调大一点一行的宽度。

分配子列

我们已经定义了一个3列网格,现在是时候为其分配子元素了。 默认情况下,子元素将被插入到第一个可用的网格单元中。但我们想覆盖此默认行为;所有子元素都应位于中间列,而第一列和第三列则留空。

.wrapper > * {
  grid-column: 2;
}

在CSS Grid布局中,列索引从1开始,因此2是对中间列的引用。

星号(*)是通配符,将匹配所有类型的元素。我们说每个子元素都应分配给第二个列(即中间列)。每个新的子元素都会创建一个新行,如下所示:

通配符性能

你可能听说过使用通配符选择器(*)是一种不好的做法。有些人会告诉你它很慢,并且会影响页面的整体性能。

幸运的是,事实并非如此。2009年的一篇文章深入探讨了这个问题,即使在当时,当计算机速度较慢且浏览器优化程度较低时,这也不是问题。关于通配符选择器的性能问题是一个都市传说。

全出血子元素(Full Bleed Children)

我们已经了解了网格如何约束所有类型的元素,但是当我们想让子元素挣脱束缚,填满可用宽度时该怎么办呢?

这就是以下CSS的用武之地:

.full-bleed {
  width: 100%;
  grid-column: 1 / 4;
}

这个特殊的.full-bleed类允许特定的子元素跨越所有3列。1 / 4是开始/结束语法;我们说元素应该从第1列(包含)开始,一直跨越到第4列(不包含)。

网格的列

你可能想知道;我们的网格有3列,那么为什么我们要使用1/4中的4?

事实上,网格的列是通过列线而不是单元格来索引的。想象一下绘制一个3列网格:

当我们说我们的.full-bleed类应该跨越第1到第4列时,我们的意思是它应该从第1个列线之后开始,并在第4个列线之前结束。

更简洁的代码

一位读者指出:我们可以通过将列分配更改为以下方式来“确保”我们的代码“面向未来”:

.full-bleed {
  grid-column: 1 / -1;
}

通过将4改为-1,我们从末尾开始计数,而不是从开头开始。我们的意思是,我们的网格应该从第1个列线之后开始,并在最后1个列线之前结束。

如果你熟悉javascript的.slice运算符,它们的工作方式类似;arr.slice(-1)抓取最后一个数组元素,因为负数意味着它应该从右边而不是左边计数。

内填充

在较小的屏幕尺寸上,我们想要添加一些填充,这样我们的文本就不会正好位于显示屏的边缘。

最简单的解决方案是向网格容器添加一些水平内填充:

.wrapper {
  display: grid;
  grid-template-columns: 1fr min(42rem, 100%) 1fr;
  padding-inline: 16px;
}

在这篇博文的上一个版本中,我通过gap属性来解决这个问题,即在列之间添加间隙。这需要减少主列的宽度来进行补偿。为了记录,我也会在这里分享这个解决方案,但老实说,我认为使用padding是更好的方法。

.wrapper {
  --viewport-padding: 16px;
  display: grid;
  grid-template-columns:
    1fr
    min(42rem, calc(100% - var(--viewport-padding) * 2))
    1fr;
  gap: 0 var(--viewport-padding);
}

结论

一些读者建议使用flexbox或根本不使用包装器来实现相同的效果。不幸的是,有一些权衡使得这些替代方案不可行。这超出了本文的范围,但我在HackerNews评论中写了几点。

这个问题的历史解决方案是使用负边距。它工作得很好,但我觉得有点像是黑客技巧。你可以在CSS Tricks上阅读有关该解决方案的更多信息。 还有一件事:不要害怕调整这些样式!本教程的案例只是一个起点。例如,你可能希望将最大宽度应用于容器,以限制超宽显示器上的.full-bleed子元素。

Laravel框架基于外键关系的级联删除的两种实现方法

例如一个论坛系统有帖子表topics和评论表replies。一个帖子可以有0条或多条评论。评论表replies有一个topic_id列,是指向帖子表topics的id列的外键。

当删除一个帖子时,应该级联删除其所有评论。有两种方法:

  • 方法一,在评论表replies的迁移文件中,定义外键约束时调用onDelete('cascade')方法表示删除应级联:
$table->foreignId('topic_id')
      ->constrained()
      ->onDelete('cascade');

让Laravel框架帮我们做基于外键关系的级联删除操作。参考官方文档外键约束。这种方法简单有效,缺点是不够灵活,有些开发规范不推荐使用外键约束。

  • 方法二, 在Eloquent模型的deleted事件的监听器中自己写代码实现级联删除,好处是灵活、扩展性强,不受底层数据库约束,坏处是要自己写很多代码,容易产生bug。

在实际开发中,方法二用得更多,因为更灵活、扩展性强。例如可以方便实现“不真正删除而是放入回收站表”这种功能。

参考

9.3. 防止数据损坏