当前位置: 首页 > news >正文

前端Rust开发WebAssembly与Swc插件快速入门

前言

现代前端对速度的追求已经进入二进制工具时代,Rust 开发成为每个人的必修课。

一般我们将常见的前端 Rust 开发分为以下几类,难度由上至下递增:

  1. 开发 wasm 。

  2. 开发 swc 插件。

  3. 开发代码处理工具。

我们将默认读者具备最简单的 Rust 知识,进行快速入门介绍。

正文

开发 wasm

意义

开发 wasm 的意义在于利用浏览器运行 wasm 的优势,在 wasm 中进行大量复杂的计算、音视频、图像处理等,当你有此类需求,可以优先考虑使用 Rust 开发 wasm 分发至浏览器。

初始化

我们使用 wasm-pack 构建 wasm ,参考 wasm-pack > Quickstart 得到一个模板起始项目。

实战 case

使用 tsify 支持输出结构体的 TypeScript 类型,实现一个简单的加法运算:

# Cargo.toml 确保你含有这些依赖
[dependencies]
serde = { version = "1.0.163", features = ["derive"] }
tsify = "0.4.5"
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use tsify::Tsify;#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Rect {pub width: u32,pub height: u32
}#[wasm_bindgen]
pub fn plus(mut rect: Rect) -> Rect {rect.width += rect.height;rect
}

构建

  # devwasm-pack build --verbose --out-dir pkg --out-name index --dev# releasewasm-pack build --verbose --out-dir pkg --out-name index --release

这将在当前目录的 pkg/* 下生成 wasm 产物与 index.js 等胶水代码,导入 index.js 便即开即用,非常方便。

运行 wasm

为了支持直接导入 .wasm 文件,我们需要 webpack 5 的 asyncWebAssembly 特性支持,此处以在 Umi 4 项目中调试为例,创建一个 Umi 4 Simple 模板项目:

  pnpm create umi wasm-demo

参考 FAQ > 怎么用 WebAssembly 配置开启 wasm 支持:

// .umirc.tsexport default {chainWebpack(config) {config.set('experiments', {...config.get('experiments'),asyncWebAssembly: true})const REG = /\.wasm$/config.module.rule('asset').exclude.add(REG).end();config.module.rule('wasm').test(REG).exclude.add(/node_modules/).end().type('webassembly/async').end()},
}

之后便可在项目中直接导入刚刚打包好,在 pkg/* 的 wasm 即开即用:

import * as wasm from './path/to/pkg'
const ret = wasm.plus({width: 1,height: 2,
})
// { width: 3, height: 2 }
console.log('ret: ', ret);

注:

  1. 由于 wasm 文件可能较大,当你需要优化时,可将使用 .wasm 的组件手动 React.lazy(() => import('./Component')) 拆包,之后在 useEffect 中懒加载 await import('./path/to/pkg')

  2. 对于非 Umi 4 的 webpack 5 项目,请自行开启 experiments.asyncWebAssembly 即可一键支持 wasm 导入。

缺点

由于当下浏览器和 PC 设备性能已足够强大,更多场合下,运行 wasm 进行数据计算,传递数据花费的时间将 远远超出使用 JavaScript 进行同逻辑计算的时间

所以除音视频场景外,你很可能不需要 wasm ,而是优先考虑使用 Worker 等优化策略。

开发 swc 插件

意义

现代前端高效构建往往将 babel 替代为 swc 化,为了替代 babel 插件实现代码转换,开发 swc 插件成为了一门必修课。

初始化

参考 swc > Create a project ,我们用 swc 脚手架初始化得到一个插件的模板起始项目。

实战 case

我们编写一个最简单的功能,将所有的 react 导入转换为 preact

# Cargo.toml 确保你含有这些依赖
[dependencies]
serde = "1.0.163"
serde_json = "1.0.96"
swc_core = { version = "0.76.39", features = ["ecma_plugin_transform"] }
use swc_core::ecma::{ast::{Program, ImportDecl, ImportSpecifier},visit::{as_folder, FoldWith, VisitMut, VisitMutWith},
};
use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata};
use serde::{Deserialize, Serialize};#[derive(Deserialize, Serialize)]
pub struct TransformPluginConfig {from: String,to: String,
}pub struct TransformVisitor {config: TransformPluginConfig,
}impl VisitMut for TransformVisitor {fn visit_mut_import_decl(&mut self, n: &mut ImportDecl) {n.visit_mut_children_with(self);if n.specifiers.len() == 1 {if let ImportSpecifier::Default(_) = n.specifiers[0] {if n.src.value == self.config.from {n.src = Box::new(self.config.to.clone().into());}}}}
}#[plugin_transform]
pub fn process_transform(program: Program, metadata: TransformPluginProgramMetadata) -> Program {let config = serde_json::from_str(&metadata.get_transform_plugin_config().unwrap()).expect("invalid config");program.fold_with(&mut as_folder(TransformVisitor { config }))
}

在编写过程中,以下文档可供参考:

  • Swc :Implementing a plugin

  • Swc :Rust docs

构建

  # devcargo build --target wasm32-wasi# releasecargo build --target wasm32-wasi --release

通过构建,你可以在当前目录下得到 wasm 形式的 swc 插件产物。

运行 swc 插件

import { transformSync } from '@swc/core'const transform = async () => {const { code } = transformSync(`
import React from 'react'`,{jsc: {experimental: {plugins: [[require.resolve('./target/wasm32-wasi/debug/my_first_plugin.wasm'),{from: 'react',to: 'preact',},],],},parser: {syntax: 'typescript',dynamicImport: true,tsx: true,},target: 'es2015',minify: {compress: false,mangle: false,},transform: {react: {runtime: 'automatic',throwIfNamespace: true,development: true,useBuiltins: true,},},},module: {type: 'es6',ignoreDynamic: true,},minify: false,isModule: true,sourceMaps: false,filename: 'index.tsx',})// import React from 'preact'console.log('code: ', code)
}transform()

缺点

为了避免多平台差异,我们分发了 wasm32-wasi 目标的 wasm 包,好处是只需构建一次即可全平台通用,缺点是产物较大,同时 wasm 运行速度不如 .node ;但现代前端已无需担心只在本地编译阶段使用的包大小,如 Nextjs 单包依赖已达 40 M 以上,TypeScript 20 M ,你可以无需关心产物体积问题。

至此,我们介绍了如何借助 swc 插件实现 babel 插件的替代,在下文中,我们将继续深入,真正构建多平台分发的二进制包,同时不会做过多细节介绍,推荐只学习到此处为止

开发代码处理工具

意义

目前最主流的前端 Rust 开发即是借助 Swc 来解析 JavaScript 、TypeScript 代码,从而实现代码信息提取、转换、编译等,我们会将 Rust 编译为 Node addon .node 文件,以获得远比 wasm 更快的运行速度。

初始化

使用 napi-rs 构建 ,参考 napi > Create project 得到一个模板起始项目。

实战 case

此处同样我们实现一个:将所有 react 导入转换为 preact 的需求,所需要的依赖与模板代码如下:

# Cargo.toml 确保你含有这些依赖
[dependencies]
napi = { version = "2.12.2", default-features = false, features = ["napi4", "error_anyhow"] }
napi-derive = "2.12.2"
swc_common = { version = "0.31.12", features = ["sourcemap"] }
swc_ecmascript = { version = "0.228.27", features = ["parser", "visit", "codegen"] }
#[macro_use]
extern crate napi_derive;use std::path::{Path, PathBuf};
use std::str;use swc_common::comments::SingleThreadedComments;
use swc_common::{sync::Lrc, FileName, Globals, SourceMap};
use swc_ecmascript::ast;
use swc_ecmascript::codegen::text_writer::JsWriter;
use swc_ecmascript::parser::lexer::Lexer;
use swc_ecmascript::parser::{EsConfig, Parser, StringInput, Syntax, TsConfig};
use swc_ecmascript::visit::{VisitMut, VisitMutWith};#[napi]
pub fn transform(code: String, options: ImportChange) -> String {let is_jsx = true;let is_typescript = true;let filename_path_buf = PathBuf::from("filename.tsx");let syntax = if is_typescript {Syntax::Typescript(TsConfig {tsx: is_jsx,decorators: true,..Default::default()})} else {Syntax::Es(EsConfig {jsx: is_jsx,export_default_from: true,..Default::default()})};let source_map = Lrc::new(SourceMap::default());let source_file = source_map.new_source_file(FileName::Real(filename_path_buf.clone()),code.clone().into(),);let comments = SingleThreadedComments::default();let lexer = Lexer::new(syntax,Default::default(),StringInput::from(&*source_file),Some(&comments),);let mut parser = Parser::new_from(lexer);let mut module = parser.parse_module().expect("failed to parse module");swc_common::GLOBALS.set(&Globals::new(), || {let mut visitor = options;module.visit_mut_with(&mut visitor);});let (code, _map) = emit_source_code(Lrc::clone(&source_map),Some(comments),&module,None,false,).unwrap();code
}#[napi(object)]
pub struct ImportChange {pub from: String,pub to: String,
}impl ImportChange {pub fn new(from: String, to: String) -> Self {Self { from, to }}
}impl VisitMut for ImportChange {fn visit_mut_module_decl(&mut self, decl: &mut ast::ModuleDecl) {if let ast::ModuleDecl::Import(import_decl) = decl {if import_decl.src.value == self.from {import_decl.src = Box::new(self.to.clone().into());}}}
}pub fn emit_source_code(source_map: Lrc<SourceMap>,comments: Option<SingleThreadedComments>,program: &ast::Module,root_dir: Option<&Path>,source_maps: bool,
) -> Result<(String, Option<String>), napi::Error> {let mut src_map_buf = Vec::new();let mut buf = Vec::new();{let writer = Box::new(JsWriter::new(Lrc::clone(&source_map),"\n",&mut buf,if source_maps {Some(&mut src_map_buf)} else {None},));let config = swc_ecmascript::codegen::Config {minify: false,target: ast::EsVersion::latest(),ascii_only: false,omit_last_semi: false,};let mut emitter = swc_ecmascript::codegen::Emitter {cfg: config,comments: Some(&comments),cm: Lrc::clone(&source_map),wr: writer,};emitter.emit_module(program)?;}let mut map_buf = vec![];let emit_source_maps = if source_maps {let mut s = source_map.build_source_map(&src_map_buf);if let Some(root_dir) = root_dir {s.set_source_root(Some(root_dir.to_str().unwrap()));}s.to_writer(&mut map_buf).is_ok()} else {false};if emit_source_maps {Ok((unsafe { str::from_utf8_unchecked(&buf).to_string() },unsafe { Some(str::from_utf8_unchecked(&map_buf).to_string()) },))} else {Ok((unsafe { str::from_utf8_unchecked(&buf).to_string() }, None))}
}

构建

  # devnapi build --platform# releasenapi build --release

一般情况下,我们通常会分发至以下 9 个平台:

  "napi": {"triples": {"defaults": false,"additional": ["x86_64-apple-darwin","aarch64-apple-darwin","x86_64-pc-windows-msvc","aarch64-pc-windows-msvc","x86_64-unknown-linux-gnu","aarch64-unknown-linux-gnu","x86_64-unknown-linux-musl","aarch64-unknown-linux-musl","armv7-unknown-linux-gnueabihf"]}}

通常本地只能编译自己平台的 .node 二进制文件,所以需要依赖 Github Actions CI 等云环境进行多平台的构建,并且在 CICD 中构造好 npm 包使用 Npm Token 发布,此部分内容往往是大量的模板代码与调试过程,请自行研究学习。

运行二进制包

import { transform } from './index'console.log(// import React from 'preact'transform(`import React from 'react'`,{ from: 'react', to: 'preact' })
)

直接导入 napi 生成的 index.js 胶水代码即可使用 .node 二进制包。

缺点

  1. 通过构建 .node 分发至不同平台是目前最高运行效率、最小下载体积的方法,但相对应需要手动管理所有 Rust 代码,且多平台构建也强依赖云环境,这提出了一些较高的要求。

  2. 随着对 napi / Rust 异步、并发编程 / Swc 的理解精进,你可以写出更高运行效率的代码,得到更快的执行速度。但最简单的代码依然够用,因为现代计算机性能已经足够快,1s 还是 10s 的争论没有意义。

  3. 在开发过程中,你可能会遇到各种 Rust 构建相关的问题,请自行研究并解决。

总结

本文对 Rust 浅尝辄止,如希望更有所作为,你可以通过不断精进 Rust ,组织出更优雅的代码结构,实现更高的执行效率。

前端 AST 人尽皆知,如同开发 Babel 插件一样,开发 Rust Swc 插件已然成为现代前端的必修课,文本推荐只入门至 Swc 插件为止,已经能应对绝大多数场景。

另外,对于性能上无需过多追求,由于计算机的性能已经过剩,不管是 wasm 还是 .node 速度都是很快的,秒级之争没有意义。

以上。

相关文章:

前端Rust开发WebAssembly与Swc插件快速入门

前言 现代前端对速度的追求已经进入二进制工具时代&#xff0c;Rust 开发成为每个人的必修课。 一般我们将常见的前端 Rust 开发分为以下几类&#xff0c;难度由上至下递增&#xff1a; 开发 wasm 。 开发 swc 插件。 开发代码处理工具。 我们将默认读者具备最简单的 Rus…...

【C++ 学习 ⑧】- STL 简介

目录 一、什么是 STL&#xff1f; 二、STL 的版本 三、STL 的 6 大组件和 13 个头文件 四、学习 STL 的 3 个境界 五、STL 的缺陷 参考资料&#xff1a; STL教程&#xff1a;C STL快速入门&#xff08;非常详细&#xff09; (biancheng.net)。 C STL是什么&#xff0c;有…...

论文笔记--Deep contextualized word representations

论文笔记--Deep contextualized word representations 1. 文章简介2. 文章概括3 文章重点技术3.1 BiLM(Bidirectional Language Model)3.2 ELMo3.3 将ELMo用于NLP监督任务 4. 文章亮点5. 原文传送门 1. 文章简介 标题&#xff1a;Deep contextualized word representations作者…...

【MySQL高级篇笔记-性能分析工具的使用 (中) 】

此笔记为尚硅谷MySQL高级篇部分内容 目录 一、数据库服务器的优化步骤 二、查看系统性能参数 三、统计SQL的查询成本&#xff1a;last_query_cost 四、定位执行慢的 SQL&#xff1a;慢查询日志 1、开启慢查询日志参数 2、查看慢查询数目 3、慢查询日志分析工具&#xf…...

大学生数学建模题论文

大学生数学建模题论文篇1 浅论高中数学建模与教学设想 论文关键词&#xff1a;数学建模 数学 应用意识 数学建模教学 论文摘要&#xff1a;为增强学生应用数学的意识&#xff0c;切实培养学生解决实际问题的能力&#xff0c;分析了高中数学建模的必要性&#xff0c;并通过对高中…...

论文阅读 —— 滤波激光SLAM

文章目录 FAST-LIO2FAST-LIOIMUR2LIVER3LIVEEKFLINS退化摘要第一句 FAST-LIO2 摘要&#xff1a; 本文介绍了FAST-LIO2&#xff1a;一种快速、稳健、通用的激光雷达惯性里程计框架。 FAST-LIO2建立在高效紧耦合迭代卡尔曼滤波器的基础上&#xff0c;有两个关键的新颖之处&#…...

JavaScript键盘事件

目录 一、keydown&#xff1a;按下键盘上的任意键时触发。 二、keyup&#xff1a;释放键盘上的任意键时触发。 三、keypress&#xff1a;在按下并释放能够产生字符的键时触发&#xff08;不包括功能键等&#xff09;。 四、input&#xff1a;在文本输入框或可编辑元素的内容…...

opengl灯光基础:2.1 光照基础知识

光照&#xff1a; 光照以不同的方式影响着我们看到的世界&#xff0c;有时甚至是以很戏剧化的方式。当手电筒照射在物体上时&#xff0c;我们希望物体朝向光线的一侧看起来更亮。我们所居住的地球上的点&#xff0c;在中午朝向太阳时候被照得很亮&#xff0c;但随着地球的自转…...

大屏时代:引领信息可视化的新潮流

在信息时代的浪潮下&#xff0c;数据已经成为推动各行各业发展的重要动力。然而&#xff0c;海量的数据如何快速、直观地呈现给用户&#xff0c;成为了一个亟待解决的难题。在这样的背景下&#xff0c;可视化大屏应运而生&#xff0c;以其出色的表现力和交互性成为信息展示的佼…...

ChatGTP全景图 | 背景+技术篇

引言&#xff1a;人类以为的丰功伟绩&#xff0c;不过是开端的开端……我们在未来100年取得的技术进步&#xff0c;将远超我们从控制火种到发明车轮以来所取得的一切成就。——By Sam Altman 说明&#xff1a;ChatGPT发布后&#xff0c;我第一时间体验了它的对话、翻译、编程、…...

计算机专业学习的核心是什么?

既然是学习CS&#xff0c;那么在这里&#xff0c;我粗浅的把计算机编程领域的知识分为三个部分&#xff1a; 基础知识 特定领域知识 框架和开发技能 基础知识是指不管从事任何方向的软件工程师都应该掌握的&#xff0c;比如数据结构、算法、操作系统。 特定领域知识就是你…...

基于springboot地方旅游系统的设计与实现

摘 要 本次设计内容是基于Springboot的旅游系统的设计与实现&#xff0c;采用B/S三层架构分别是Web表现层、Service业务层、Dao数据访问层&#xff0c;并使用Springboot&#xff0c;MyBatis二大框架整合开发服务器端&#xff0c;前端使用vue&#xff0c;elementUI技术&…...

一些学习资料链接

组件化和CocoaPods iOS 组件化的三种方案_迷曳的博客-CSDN博客 CocoaPods 私有化 iOS组件化----Pod私有库创建及使用 - 简书 CocoaPods1.9.1和1.8 使用 出现CDN: trunk URL couldnt be downloaded: - 简书 cocoapod制作私有库repo - 简书 【ios开发】 上传更新本地项目到…...

Webpack打包图片-JS-Vue

1 Webpack打包图片 2 Webpack打包JS代码 3 Babel和babel-loader 5 resolve模块解析 4 Webpack打包Vue webpack5打包 的过程&#xff1a; 在webpack的配置文件里面编写rules&#xff0c;type类型有多种&#xff0c;每个都有自己的作用&#xff0c;想要把小内存的图片转成bas…...

进程控制(Linux)

进程控制 fork 在Linux中&#xff0c;fork函数是非常重要的函数&#xff0c;它从已存在进程中创建一个新进程。新进程为子进程&#xff0c;而原进程为父进程。 返回值&#xff1a; 在子进程中返回0&#xff0c;父进程中返回子进程的PID&#xff0c;子进程创建失败返回-1。 …...

C Primer Plus第十四章编程练习答案

学完C语言之后&#xff0c;我就去阅读《C Primer Plus》这本经典的C语言书籍&#xff0c;对每一章的编程练习题都做了相关的解答&#xff0c;仅仅代表着我个人的解答思路&#xff0c;如有错误&#xff0c;请各位大佬帮忙点出&#xff01; 由于使用的是命令行参数常用于linux系…...

又名管道和无名管道

一、进程间通信&#xff08;IPC&#xff0c;InterProcess Communication&#xff09; 概念&#xff1a;就是进程和进程之间交换信息。 常用通信方式 无名管道&#xff08;pipe&#xff09; 有名管道 &#xff08;fifo&#xff09; 信号&#xff08;signal&#xff09; 共…...

操作系统复习4.1.0-文件管理结构

定义 一组有意义的信息的集合 属性 文件名、标识符、类型、位置、大小、创建时间、上次修改时间、文件所有者信息、保护信息 操作系统向上提供的功能 创建文件、删除文件、读文件、写文件、打开文件、关闭文件 这6个都是系统调用 创建文件 创建文件时调用Create系统调用…...

【嵌入式烧录/刷写文件】-2.6-剪切/保留Intel Hex文件中指定地址范围内的数据

案例背景&#xff1a; 有如下一段HEX文件&#xff0c;保留地址范围0x9140-0x91BF内的数据&#xff0c;删除地址范围0x9140-0x91BF外的数据。 :2091000058595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F70717273747576775F :2091200078797A7B7C7D7E7F808182838485868788898A…...

JavaScript表单事件(下篇)

目录 八、keydown: 当用户按下键盘上的任意键时触发。 九、keyup: 当用户释放键盘上的键时触发。 十、keypress: 当用户按下键盘上的字符键时触发。 十一、focusin: 当表单元素或其子元素获得焦点时触发。 十二、focusout: 当表单元素或其子元素失去焦点时触发。 十三、c…...

大麦网自动抢票神器:5分钟配置,告别抢票焦虑的终极指南

大麦网自动抢票神器&#xff1a;5分钟配置&#xff0c;告别抢票焦虑的终极指南 【免费下载链接】ticket-purchase 大麦自动抢票&#xff0c;支持人员、城市、日期场次、价格选择 项目地址: https://gitcode.com/GitHub_Trending/ti/ticket-purchase 还在为心仪演唱会门票…...

ProperTree:重新定义Plist编辑的技术哲学与设计范式

ProperTree&#xff1a;重新定义Plist编辑的技术哲学与设计范式 【免费下载链接】ProperTree Cross platform GUI plist editor written in python. 项目地址: https://gitcode.com/gh_mirrors/pr/ProperTree 在macOS和iOS开发的世界里&#xff0c;Property List&#x…...

GitHub加速终极指南:5分钟告别访问卡顿,让开发效率翻倍

GitHub加速终极指南&#xff1a;5分钟告别访问卡顿&#xff0c;让开发效率翻倍 【免费下载链接】FastGithub github定制版的dns服务&#xff0c;解析访问github最快的ip 项目地址: https://gitcode.com/gh_mirrors/fa/FastGithub 你是否经历过这样的场景&#xff1a;在关…...

老板惊呆了!Laravel 接入 OnlyOffice 后,团队协作效率翻 3 倍(附安全加固方案)

文章目录老板惊呆了&#xff01;Laravel 接入 OnlyOffice 后&#xff0c;团队协作效率翻 3 倍&#xff08;附安全加固方案&#xff09;一、整体架构二、准备工作&#xff1a;OnlyOffice 服务&#xff08;Docker 版&#xff09;三、Laravel 后端集成1. 安装必要依赖2. 配置 Only…...

如何高效使用开源视频下载插件:专业用户的终极指南

如何高效使用开源视频下载插件&#xff1a;专业用户的终极指南 【免费下载链接】VideoDownloadHelper Chrome Extension to Help Download Video for Some Video Sites. 项目地址: https://gitcode.com/gh_mirrors/vi/VideoDownloadHelper VideoDownloadHelper是一款专为…...

软件架构设计师考试——系统安全性与保密性设计知识点全总结(考前冲刺版,超1万字)

临近软件架构设计师考试&#xff0c;系统安全性与保密性设计是考试的核心模块&#xff0c;贯穿上午场信息系统综合知识&#xff08;15-20分&#xff09;、下午场案例分析&#xff08;25-35分&#xff09;及论文写作&#xff08;高频命题方向&#xff09;&#xff0c;是“稳拿分…...

数字孪生赋能设备预测性维护:构建工业设备全生命周期智能运维新模式

在智能制造加速推进的今天&#xff0c;工业设备作为生产体系的核心资产&#xff0c;其稳定运行直接决定着企业的生产效率、产品质量与经济效益。但据行业统计&#xff0c;全球制造业每年因设备非计划停机造成的损失超过 5000 亿美元&#xff0c;单台关键设备每分钟停机损失可达…...

嵌入式JavaScript混合开发:C与JS高效互调实践指南

1. 项目概述&#xff1a;当嵌入式遇上JavaScript最近几年&#xff0c;在嵌入式开发圈子里&#xff0c;一个老话题又有了新热度&#xff1a;用JavaScript来写嵌入式应用。这听起来有点“跨界”&#xff0c;毕竟传统印象里&#xff0c;嵌入式是C/C的天下&#xff0c;讲究的是对硬…...

工业边缘计算实战:基于Wind River Helix与App Cloud的云原生应用部署与管理

1. 项目概述&#xff1a;当工业边缘计算遇上云原生应用最近在跟几个做工业物联网和智能网关项目的朋友聊天&#xff0c;发现一个挺有意思的现象&#xff1a;大家手里的硬件平台越来越强&#xff0c;但软件开发和部署的效率却成了新的瓶颈。一个典型的场景是&#xff0c;你有一台…...

前端html字体包体积压缩,网站工程下字体压缩裁剪工具

整个网站项目如果字体包体积太大就会影响其加载速度&#xff0c;字体加载完会让页面字体突然变换。做一个工具他会自动检索网站上所有展现给用户的字符&#xff0c;然后原地裁剪字体。来解决这个问题。实现效果如下&#xff1a; 执行py文件以后&#xff0c;在网站字体文件所在目…...