普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月3日技术

在 Flutter 中使用 go_router 管理路由名称和路径的最佳实践

作者 JarvanMo
2025年7月3日 09:26

你是否厌倦了在 Flutter 应用中导航时输入冗长的硬编码路径?或者在跨多个功能模块组织路由时担心出现循环导入问题?本文将探讨如何使用go_router以简洁且可维护的方式管理路由名称和路径。


1. 为何避免硬编码路由?

在代码库中频繁使用 context.go("/some/really/long/path/42") 这样的字符串会导致两大问题:易出错且难以维护。例如,当需要将路径从 /some/really/long/path 修改为 /shorter/path 时,你不得不逐行查找并修改所有调用该路径的地方。

更好的做法是定义命名路由集中式路由常量,这能带来以下优势:

  • 避免拼写错误:通过常量引用而非字符串字面量,减少因手误导致的路由错误。
  • 保持代码 DRY 原则:避免重复编写相同路径,提升代码复用性。
  • 简化后续路由修改:只需在一处修改常量定义,即可全局生效。

2. go_router 中的命名路由

go_router 允许为路由同时定义路径(path)和名称(name),之后可通过 context.goNamed() 替代 context.go() 进行导航。

import 'package:go_router/go_router.dart';

final router = GoRouter(
  routes: [
    GoRoute(
      name: 'home',
      path: '/home',
      builder: (context, state) => const HomePage(),
    ),
    GoRoute(
      name: 'profile',
      path: '/profile/:userId',
      builder: (context, state) {
        final userId = state.pathParameters['userId'];
        return ProfilePage(userId: userId);
      },
    ),
  ],
);

// Navigate by route name
context.goNamed('profile', params: {'userId': '42'});

使用命名路由: 如果将 /profile/:userId 重命名为 /users/:id,你只需更新一个地方(路由定义),而不是项目中每个 go() 调用。

3. 集中管理路由名称和路径

创建一个专门的文件或类来存放所有路由常量。例如:

// app_routes.dart
abstract class AppRouteName {
  static const home = 'home';
  static const profile = 'profile';
}

abstract class AppRoutePath {
  static const home = '/home';
  static const profile = '/profile/:userId';
}

然后,在定义路由时:

GoRoute(
  name: AppRouteName.profile,
  path: AppRoutePath.profile,
  builder: (context, state) => ...
);

当要导航时:

context.goNamed(
  AppRouteName.profile,
  params: {'userId': '42'},
);

这种方法使路径结构变更的管理变得简单,并确保路由定义的单一事实来源。

4. 在大型应用中组织路由

如果你采用**特性优先(feature-first)**的架构方式:

  • 核心层(Core Layer): 定义共享资源、服务或基类

  • 特性层(Feature Layer: 每个特性可独立定义自己的 UI、逻辑和路由

  • 应用(组合)层(App/Composition Layer): 导入核心层和所有特性模块, 在单一 GoRouter 中合并所有路由

这种方式可避免循环导入:

  • 核心层不导入特性层
  • 特性层按需导入核心层
  • 应用层导入核心层特性层以组装最终路由

典型的文件夹结构可能如下:

lib/
 ├── core/
 |    └── app_routes.dart
 ├── features/
 |    ├── feature_a/
 |    └── feature_b/
 └── app/
      ├── app_router.dart
      └── main.dart

app_router.dart将从核心层和各特性层收集路由,创建统一的路由器。

5. 最终小提示

  • 使用go_router_builder:如需生成类型安全的路由导航函数(如context.goToProfile(userId: 42)),可集成该工具。
  • 保持路由定义简洁:避免在路由中嵌入复杂逻辑。
  • 动态页面参数处理:使用pathParametersqueryParameters解析 ID 和标志位。

通过遵循这些最佳实践(命名路由、集中常量管理、分层架构),你将大幅减少维护成本,确保 Flutter 代码库的可扩展性。编码愉快!

最后,请关注我的公众号:OpenFlutter,感激。

初识XPath——了解XML与HTML文档中的导航神器

作者 烛阴
2025年7月3日 09:08

引言

在Web开发和自动化测试中,常常需要定位和操作页面中的元素。传统上,我们用CSS选择器,但在某些复杂场景下,XPath是一种更强大、更灵活的工具。本文将带你由浅入深,了解XPath的基本概念和用法。


什么是XPath?

XPath(XML Path Language)是一种用于在XML文档中查找信息的语言。由于HTML是HTML5的标准变体,可视为XML的一种,只要遵守标准,同样适用XPath。

XPath的用途

  • 选择特定元素或一组元素
  • 计算元素的路径
  • 提取元素的内容或属性
  • 在自动化测试框架(如Selenium)中定位元素

XPath的基本结构

XPath表达式类似路径,用于从文档的根节点开始,逐层筛选目标。

例子

<html>
  <body>
    <div id="main">
      <h1>标题</h1>
      <p class="text">这是一段文字。</p>
    </div>
  </body>
</html>

对应的XPath:

  • 选择<h1>//h1
  • 选择<p>//p[@class='text']
  • 选择<div id="main">//div[@id='main']

常用的XPath表达式

表达式 描述 示例
/ 从根节点开始,绝对路径 /html/body/div
// 在文档中查找匹配的元素,不考虑层级 //p
. 当前节点 ./span
.. 父节点 ../div
@属性名 指定属性 //a[@href='https://']
* 任意元素 //*/a

结合条件过滤

  • [条件]:筛选出满足条件的元素
  • 例://div[@class='main']:选择class为main的div
  • 叠加过滤://ul/li[1]:第一个li元素

实战演练:用XPath定位元素

如果你安装了谷歌浏览器,可以安装Xpath测试器进行实战演练

screenshot_2025-07-02_19-02-00.png


小结

XPath是网页元素定位的重要工具,掌握其基础语法可以帮助你更高效地进行网页自动化、数据抓取与测试验证。


如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript开发干货。

跨域问题解决方案:开发代理

2025年7月3日 08:39

由于浏览器的同源策略,当开发环境中的前端应用试图与后端服务进行通信时,经常会遇到跨域问题,开发代理为我们提供了一种简单而有效的解决方案。

一、开发代理的概念与适用场景

(一)开发代理

开发代理是一种在开发环境中使用的代理机制,它允许前端应用在开发阶段绕过浏览器的同源策略。开发代理的核心思想是,将前端应用的请求转发到后端服务,从而解决跨域问题。

(二)适用场景

开发代理适用于以下场景:

  • 生产环境不发生跨域,但开发环境发生跨域:在生产环境中,前端应用和后端服务通常部署在同一域名下,不会发生跨域问题。但在开发环境中,前端应用和后端服务通常运行在不同的端口或域名下,会发生跨域问题。
  • 开发阶段的快速迭代:开发代理可以帮助开发者在开发阶段快速迭代,无需担心跨域问题。

二、如何配置开发代理

(一)Vue.js开发服务器代理配置

在Vue.js项目中,可以通过vue.config.js文件配置开发服务器的代理。以下是一个示例配置:

// vue.config.js
module.exports = {
  devServer: { // 配置开发服务器
    proxy: { // 配置代理
      "/api": { // 若请求路径以 /api 开头
        target: "http://dev.taobao.com", // 将其转发到 http://dev.taobao.com
        changeOrigin: true, // 允许跨域
      },
    },
  },
};

(二)配置说明

  • /api:这是代理的路径前缀。当请求路径以/api开头时,开发服务器会将请求转发到指定的target地址。
  • target:这是后端服务的地址。开发服务器会将请求转发到这个地址。
  • changeOrigin:这是一个可选配置项,当设置为true时,允许跨域。

(三)其他框架的代理配置

其他前端框架(如React.js、Angular.js)也提供了类似的代理配置功能。例如,在React.js项目中,可以通过package.json文件配置代理:

{
  "proxy": "http://dev.taobao.com"
}

三、开发代理的优势

(一)简单易用

开发代理的配置非常简单,只需在开发服务器的配置文件中添加几行代码即可。

(二)无需修改后端代码

开发代理在前端应用和后端服务之间起到了中间人的作用,无需修改后端代码即可解决跨域问题。

(三)适用于开发环境

开发代理主要适用于开发环境,不会影响生产环境的部署。

四、总结

开发代理是一种简单而有效的解决前端开发中跨域问题的方法。通过在开发服务器中配置代理,可以轻松解决开发环境中的跨域问题,无需修改后端代码。

前端多层加密?让“伪安全”更“伪”一点(狗头)

2025年7月3日 08:38

在浏览器端做加密,听上去就有点自欺欺人。很多开发者对前端加密的态度是:

“反正浏览器都能看到源码,前端加密有意义吗?”

实际上,这个问题的答案并不简单。前端加密无法抗拒主动攻击,但在某些场景下,比如数据混淆、防小白逆向、教学演示,它仍然具备合理存在的价值。

本篇:想聊的,就是一种典型的“前端多层加密”的实现思路。

我做了一个网站,大家可以简单体验一下:encryption-demo.pages.dev/

image.png

多层加密的核心设计

前端多层加密本质上是将多个简单的加密/编码/哈希算法串联起来,形成一个相对复杂的处理链,从而提升对抗简单逆向的门槛。

image.png

  • 字符位移:基础可逆加密

  • 加盐:防止简单模式识别

  • Base64:编码增强,便于传输

  • 哈希迭代:引入不可逆过程,增加验证机制

  • 二次字符位移+编码:强化混淆效果

原理与实现

🔸 步骤1:字符位移加密

  • 类似凯撒密码,每个字母 +3
  • 极易破解,但适合作为第一步混淆
function simpleEncrypt(str) {
  return str.split('').map(c => {
    const code = c.charCodeAt(0);
    if (code >= 65 && code <= 90) {
      return String.fromCharCode(((code - 65 + 3) % 26) + 65);
    }
    if (code >= 97 && code <= 122) {
      return String.fromCharCode(((code - 97 + 3) % 26) + 97);
    }
    return c;
  }).join('');
}

🔸 步骤2:加盐

  • 在末尾增加一串随机盐(一般8位)
  • 盐 = 防止模式识别 + 防止撞库
function generateSalt(length = 8) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  return Array.from({length}).map(() => chars[Math.floor(Math.random() * chars.length)]).join('');
}

🔸 步骤3:Base64编码

  • 本质是编码,不是加密
  • 提供可见的编码形式,防止二进制乱码
const encoded = btoa(textWithSalt);

🔸 步骤4:再次字符位移

  • 加强混淆
  • 和第一次字符位移逻辑相同

🔸 步骤5:100次哈希迭代

  • 不可逆
  • 增加校验强度
  • 理论上,攻击者必须知道前面所有步骤,并暴力跑哈希
function customHash(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = (hash << 5) - hash + str.charCodeAt(i);
    hash |= 0;
  }
  return hash.toString(16);
}
let result = input;
for (let i = 0; i < 100; i++) {
  result = customHash(result);
}

🔸 步骤6:最终Base64编码

  • 便于展示和传输
  • 增强文本兼容性

到底安全吗?

目标 多层加密能否达成?
防止数据被直接肉眼看到 ✅ 完全达成
防止抓包获取原始数据 ❌ 无法防御
防止逆向还原加密逻辑 ❌ 无法防御
增加小白逆向的时间成本 ✅ 有效
商业级数据安全保护 ❌ 完全不适用
教学/实验/流程展示 ✅ 非常合适

结论一句话:

这是一种“伪加密”...

尽管这种多层加密“不安全”,但它的流程化设计非常适合: 一步步拆解加密过程 —— 从字符到编码到哈希,看到每一步如何变化 —— 练习如何组合多个算法。

前端加密 ≠ 信息安全,但 ≠ 完全没用。

OK,以上便是本次分享~

欢迎加我:atar24,进技术群、交盆友,我会第一时间通过

Vue路由模式大揭秘:选对模式,页面跳转不再"迷路"!

2025年7月3日 08:26

大家好,我是小杨,一个干了6年前端的老兵。今天咱们聊聊Vue路由的模式问题,很多新手在配置路由时,往往对hashhistory模式傻傻分不清楚,结果部署上线后各种404、页面刷新白屏,甚至SEO不友好。今天我就带大家彻底搞懂Vue路由的几种模式,让你在项目里游刃有余!


一、Vue路由的两种核心模式

Vue Router默认支持两种路由模式:

  1. Hash模式mode: 'hash'
  2. History模式mode: 'history'

此外,还有Memory模式(主要用于非浏览器环境,比如SSR或移动端),但今天我们主要讨论前两种。


二、Hash模式:带#号的URL

1. 特点

  • URL里带#,比如 http://example.com/#/home
  • 不依赖服务器配置,刷新不会404
  • 兼容性好,IE9+都能跑

2. 原理

Hash模式利用的是浏览器的锚点(hash) 特性,#后面的变化不会触发页面刷新,但会触发hashchange事件,Vue Router监听这个事件来实现路由切换。

3. 适用场景

  • 静态网站托管(GitHub Pages、Netlify等)
  • 不想折腾服务器配置的情况
  • 需要兼容老浏览器的项目

4. 代码示例

const router = new VueRouter({
  mode: 'hash', // 默认就是hash,可以不写
  routes: [...]
})

三、History模式:优雅的URL

1. 特点

  • URL干净,比如 http://example.com/home
  • 依赖服务器配置,否则刷新会404
  • 需要后端支持(Nginx/Apache/Node.js等)

2. 原理

History模式利用HTML5的history.pushStatehistory.replaceStateAPI,让URL变化但不刷新页面,同时能记录浏览历史。

3. 适用场景

  • 需要SEO友好的项目
  • 企业级应用,追求专业URL风格
  • 能控制服务器配置的情况

4. 代码示例

const router = new VueRouter({
  mode: 'history', // 使用history模式
  routes: [...]
})

5. 服务器配置(避免刷新404)

Nginx配置

location / {
  try_files $uri $uri/ /index.html; # 所有路径都回退到index.html
}

Node.js(Express)

app.get('*', (req, res) => {
  res.sendFile(path.resolve(__dirname, 'dist', 'index.html'))
})

四、Memory模式:无URL变化的路由

1. 特点

  • URL不会变,完全由前端JS控制
  • 适用于非浏览器环境(如React Native、Electron、SSR)
  • 不会影响SEO,因为压根没有URL变化

2. 代码示例

const router = new VueRouter({
  mode: 'abstract', // Vue 2叫abstract,Vue 3叫memory
  routes: [...]
})

五、如何选择路由模式?

模式 适用场景 是否需要服务器配置 SEO友好 兼容性
Hash 静态托管、兼容老浏览器 ❌ 不需要 ❌ 不友好 IE9+
History 企业级应用、SEO优化 ✅ 需要 ✅ 友好 IE10+
Memory 非浏览器环境(SSR、Electron) ❌ 不需要 ❌ 不适用 所有环境

我的经验总结

  1. 个人博客/静态网站 → Hash模式(省事)
  2. 企业后台/电商网站 → History模式(专业)
  3. React Native/Electron → Memory模式(无URL需求)

六、常见坑点 & 解决方案

1. History模式刷新404?

  • 原因:服务器没正确配置回退到index.html
  • 解决:参考上面的Nginx/Node.js配置

2. Hash模式SEO差?

  • 解决:用服务端渲染(SSR)或预渲染(Prerender)

3. 微信内置浏览器兼容性问题?

  • 解决:强制使用Hash模式,避免微信的奇葩history兼容问题

七、总结

  • Hash模式:简单省事,适合静态网站
  • History模式:专业优雅,但需要服务器支持
  • Memory模式:非浏览器环境专属

选对模式,能让你的项目少踩很多坑!如果你在项目里遇到过路由的奇葩问题,欢迎在评论区分享,我帮你分析~

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

路由守卫通关秘籍:这些钩子函数让你的页面跳转稳如老狗!

2025年7月3日 08:21

大家好,我是小杨,一个做了6年前端的老司机。今天我们来聊聊路由守卫这个在前端开发中特别实用的功能,它能帮我们控制页面的跳转流程,就像给网站请了个尽职的保安。

一、什么是路由守卫?

简单说,路由守卫就是页面跳转时的"安检门"。比如:

  • 用户没登录想进会员中心?拦住!
  • 页面数据没保存就想离开?弹窗提醒!
  • 普通员工想访问管理员页面?门都没有!

二、Vue路由的三大守卫钩子

1. 全局守卫 - 整个网站的保安队长

// 全局前置守卫(每次跳转前都会触发)
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !我.store.state.isLogin) {
    next('/login') // 去登录页
  } else {
    next() // 放行
  }
})

// 全局解析守卫(适合做权限校验)
router.beforeResolve(async to => {
  if (to.meta.requiresAdmin) {
    await 我.checkAdminRole() // 异步检查权限
  }
})

// 全局后置钩子(跳转完成后触发)
router.afterEach((to, from) => {
  sendToAnalytics(to.fullPath) // 可以在这里做页面统计
})

2. 路由独享守卫 - 特定页面的专属安检

const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    beforeEnter: (to, from, next) => {
      if (!我.store.state.userInfo.vip) {
        next('/upgrade') // 非VIP跳转到升级页
      } else {
        next()
      }
    }
  }
]

3. 组件内守卫 - 组件自己的小门卫

export default {
  beforeRouteEnter(to, from, next) {
    // 注意!这里还不能用this
    next(vm => {
      vm.initData() // 通过vm访问组件实例
    })
  },
  
  beforeRouteUpdate(to, from, next) {
    // 当前路由改变但组件被复用时触发
    this.fetchData(to.params.id)
    next()
  },
  
  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      if (confirm('有未保存的修改,确定离开吗?')) {
        next()
      } else {
        next(false) // 取消导航
      }
    } else {
      next()
    }
  }
}

三、React路由守卫实现方案

React Router没有内置守卫,但我们可以自己实现:

1. 高阶组件方式

function PrivateRoute({ component: Component, ...rest }) {
  return (
    <Route
      {...rest}
      render={props =>
        我.isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Redirect to="/login" />
        )
      }
    />
  )
}

2. 自定义Hook方式

function useAuthGuard() {
  const history = useHistory()
  
  useEffect(() => {
    if (!我.store.getState().auth.isLogin) {
      history.replace('/login')
    }
  }, [history])
}

// 在需要守卫的组件中使用
function AdminPage() {
  useAuthGuard()
  return <div>管理员页面</div>
}

四、实战中的骚操作

  1. 动态路由加载:在beforeEach中按需加载路由
router.beforeEach(async (to) => {
  if (to.meta.requiresAdmin && !我.hasAdminRoute) {
    await 我.loadAdminRoutes() // 动态添加路由
    return to.fullPath // 重定向到目标页
  }
})
  1. 页面离开确认
beforeRouteLeave(to, from, next) {
  window.onbeforeunload = () => "数据可能丢失!" // 浏览器原生提示
  // ...其他逻辑
}
  1. 滚动行为控制
router.afterEach((to) => {
  if (to.meta.scrollToTop) {
    window.scrollTo(0, 0)
  }
})

五、常见坑点指南

  1. 死循环陷阱
// 错误示范!会导致无限循环
router.beforeEach((to, from, next) => {
  if (!isLogin) next('/login')
})

// 正确做法
router.beforeEach((to, from, next) => {
  if (!isLogin && to.path !== '/login') next('/login')
  else next()
})
  1. 异步操作处理
// 记得要调用next!
beforeRouteEnter(to, from, next) {
  fetchData().then(() => next()) // 别忘了next
}
  1. meta字段妙用
{
  path: '/admin',
  meta: {
    requiresAuth: true,
    requiredRole: 'admin'
  }
}

六、总结

路由守卫用得好,能帮我们实现:

  • ✅ 登录状态验证
  • ✅ 权限精细控制
  • ✅ 数据变更提示
  • ✅ 页面访问统计
  • ✅ 动态路由加载

记住守卫钩子的执行顺序:全局beforeEach → 路由beforeEnter → 组件beforeRouteEnter → 全局beforeResolve → 全局afterEach

希望这篇能帮到大家!如果有问题欢迎在评论区交流,我会把6年踩过的坑都分享出来~

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

[Python3/Java/C++/Go/TypeScript] 一题一解:模拟(清晰题解)

作者 lcbin
2025年7月3日 06:45

方法一:模拟

我们可以使用一个数组 $\textit{word}$ 来存储每次操作后的字符串,当 $\textit{word}$ 的长度小于 $k$ 时,我们不断地对 $\textit{word}$ 进行操作。

最后返回 $\textit{word}[k - 1]$ 即可。

###python

class Solution:
    def kthCharacter(self, k: int) -> str:
        word = [0]
        while len(word) < k:
            word.extend([(x + 1) % 26 for x in word])
        return chr(ord("a") + word[k - 1])

###java

class Solution {
    public char kthCharacter(int k) {
        List<Integer> word = new ArrayList<>();
        word.add(0);
        while (word.size() < k) {
            for (int i = 0, m = word.size(); i < m; ++i) {
                word.add((word.get(i) + 1) % 26);
            }
        }
        return (char) ('a' + word.get(k - 1));
    }
}

###cpp

class Solution {
public:
    char kthCharacter(int k) {
        vector<int> word;
        word.push_back(0);
        while (word.size() < k) {
            int m = word.size();
            for (int i = 0; i < m; ++i) {
                word.push_back((word[i] + 1) % 26);
            }
        }
        return 'a' + word[k - 1];
    }
};

###go

func kthCharacter(k int) byte {
word := []int{0}
for len(word) < k {
m := len(word)
for i := 0; i < m; i++ {
word = append(word, (word[i]+1)%26)
}
}
return 'a' + byte(word[k-1])
}

###ts

function kthCharacter(k: number): string {
    const word: number[] = [0];
    while (word.length < k) {
        word.push(...word.map(x => (x + 1) % 26));
    }
    return String.fromCharCode(97 + word[k - 1]);
}

###rust

impl Solution {
    pub fn kth_character(k: i32) -> char {
        let mut word = vec![0];
        while word.len() < k as usize {
            let m = word.len();
            for i in 0..m {
                word.push((word[i] + 1) % 26);
            }
        }
        (b'a' + word[(k - 1) as usize] as u8) as char
    }
}

时间复杂度 $O(k)$,空间复杂度 $O(k)$。其中 $k$ 为输入参数。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-找出第 K 个字符 I🟢

2025年7月3日 00:00

Alice 和 Bob 正在玩一个游戏。最初,Alice 有一个字符串 word = "a"

给定一个正整数 k

现在 Bob 会要求 Alice 执行以下操作 无限次 :

  • word 中的每个字符 更改 为英文字母表中的 下一个 字符来生成一个新字符串,并将其 追加 到原始的 word

例如,对 "c" 进行操作生成 "cd",对 "zb" 进行操作生成 "zbac"

在执行足够多的操作后, word至少 存在 k 个字符,此时返回 word 中第 k 个字符的值。

注意,在操作中字符 'z' 可以变成 'a'

 

示例 1:

输入:k = 5

输出:"b"

解释:

最初,word = "a"。需要进行三次操作:

  • 生成的字符串是 "b"word 变为 "ab"
  • 生成的字符串是 "bc"word 变为 "abbc"
  • 生成的字符串是 "bccd"word 变为 "abbcbccd"

示例 2:

输入:k = 10

输出:"c"

 

提示:

  • 1 <= k <= 500

3304. 找出第 K 个字符 I

作者 stormsunshine
2024年9月29日 21:29

解法一

思路和算法

最直观的思路是模拟每次操作。初始时字符串中只有一个字符 $\text{`a'}$,每次操作将字符串中已有的每个字符的后一个字符拼接到字符串的末尾,直到字符串中至少有 $k$ 个字符时结束操作。当字符串中至少有 $k$ 个字符时,答案字符为字符串的下标 $k - 1$ 的字符。

代码

###Java

class Solution {
    public char kthCharacter(int k) {
        StringBuffer sb = new StringBuffer("a");
        while (sb.length() < k) {
            int length = sb.length();
            for (int i = 0; i < length; i++) {
                sb.append((char) (sb.charAt(i) + 1));
            }
        }
        return sb.charAt(k - 1);
    }
}

###C#

public class Solution {
    public char KthCharacter(int k) {
        StringBuilder sb = new StringBuilder("a");
        while (sb.Length < k) {
            int length = sb.Length;
            for (int i = 0; i < length; i++) {
                sb.Append((char) (sb[i] + 1));
            }
        }
        return sb[k - 1];
    }
}

复杂度分析

  • 时间复杂度:$O(k)$,其中 $k$ 是给定的正整数。结束操作时的字符串的长度一定小于 $2k$,每个字符的生成时间都是 $O(1)$。

  • 空间复杂度:$O(k)$,其中 $k$ 是给定的正整数。结束操作时的字符串的长度一定小于 $2k$。

解法二

思路和算法

为了计算字符串中第 $k$ 个字符的值,需要首先计算使字符串中至少有 $k$ 个字符的操作次数。由于初始字符串中有 $1$ 个字符,每次操作之后都会将字符串中的字符个数乘以 $2$,因此对于任意非负整数 $x$,经过 $x$ 次操作之后字符串中的字符个数等于 $2^x$,计算满足 $2^x \ge k$ 的最小整数 $x$,可以得到 $x \ge \lceil \log k \rceil$,为了方便计算,当 $k > 1$ 时可以转换成 $x \ge \lfloor \log (k - 1) \rfloor + 1$,即 $x$ 的最小值等于 $k$ 的二进制位数。计算字符串中第 $k$ 个字符的值需要考虑前 $x$ 次操作。

当位于下标 $\textit{index}$ 时,计算方法如下。

  • 如果 $\textit{index} < 0$,则不执行操作,字符的值等于 $\text{`a'}$。

  • 如果 $\textit{index} \ge 0$,则前 $\textit{index}$ 次操作之后的字符串中的字符个数等于 $2^{\textit{index}}$,需要比较 $k$ 和 $2^{\textit{index}}$ 的大小,然后执行计算。

    • 如果 $k \le 2^{\textit{index}}$,则下标 $\textit{index} - 1$ 的操作不影响第 $k$ 个字符,因此计算位于下标 $\textit{index} - 1$ 时的第 $k$ 个字符的值。

    • 如果 $k > 2^{\textit{index}}$,则下标 $\textit{index} - 1$ 的操作影响第 $k$ 个字符,第 $k$ 个字符由第 $k - 2^{\textit{index}}$ 个字符经过一次操作得到,因此计算位于下标 $\textit{index} - 1$ 时的第 $k - 2^{\textit{index}}$ 个字符的值,该字符的后一个字符的值即为第 $k$ 个字符的值。

上述过程是一个递归的过程。

递归的终止条件是 $\textit{index} < 0$,此时字符的值等于 $\text{`a'}$。当 $\textit{index} \ge 0$ 时,比较 $k$ 和 $2^{\textit{index}}$ 的大小,递归计算第 $k$ 个字符的值。

代码

###Java

class Solution {
    public char kthCharacter(int k) {
        int binaryLength = getBinaryLength(k - 1);
        return findCharacter(k, binaryLength - 1);
    }

    public int getBinaryLength(long num) {
        int length = 0;
        while (num > 0) {
            num /= 2;
            length++;
        }
        return length;
    }

    public char findCharacter(long k, int index) {
        if (index < 0) {
            return 'a';
        }
        long prevPosition = 1L << index;
        if (k <= prevPosition) {
            return findCharacter(k, index - 1);
        } else {
            char prev = findCharacter(k - prevPosition, index - 1);
            return (char) (prev + 1);
        }
    }
}

###C#

public class Solution {
    public char KthCharacter(int k) {
        int binaryLength = GetBinaryLength(k - 1);
        return FindCharacter(k, binaryLength - 1);
    }

    public int GetBinaryLength(long num) {
        int length = 0;
        while (num > 0) {
            num /= 2;
            length++;
        }
        return length;
    }

    public char FindCharacter(long k, int index) {
        if (index < 0) {
            return 'a';
        }
        long prevPosition = 1L << index;
        if (k <= prevPosition) {
            return FindCharacter(k, index - 1);
        } else {
            char prev = FindCharacter(k - prevPosition, index - 1);
            return (char) (prev + 1);
        }
    }
}

复杂度分析

  • 时间复杂度:$O(\log k)$,其中 $k$ 是给定的正整数。操作次数是 $O(\log k)$,每次操作的计算时间是 $O(1)$。

  • 空间复杂度:$O(\log k)$,其中 $k$ 是给定的正整数。递归调用栈的深度是 $O(\log k)$。

解法三

思路和算法

递归实现可以改成迭代实现。

首先计算使字符串中至少有 $k$ 个字符的操作次数 $x$,然后反向遍历 $x$ 次操作的过程,计算第 $k$ 个字符的值的增加次数,即可得到字符串中第 $k$ 个字符的值。

代码

###Java

class Solution {
    public char kthCharacter(int k) {
        int increments = 0;
        int binaryLength = getBinaryLength(k - 1);
        for (int i = binaryLength - 1; i >= 0; i--) {
            long prevPosition = 1L << i;
            if (k > prevPosition) {
                k -= prevPosition;
                increments++;
            }
        }
        return (char) ('a' + increments);
    }

    public int getBinaryLength(long num) {
        int length = 0;
        while (num > 0) {
            num /= 2;
            length++;
        }
        return length;
    }
}

###C#

public class Solution {
    public char KthCharacter(int k) {
        int increments = 0;
        int binaryLength = GetBinaryLength(k - 1);
        for (int i = binaryLength - 1; i >= 0; i--) {
            long prevPosition = 1L << i;
            if (k > prevPosition) {
                k = (int) (k - prevPosition);
                increments++;
            }
        }
        return (char) ('a' + increments);
    }

    public int GetBinaryLength(long num) {
        int length = 0;
        while (num > 0) {
            num /= 2;
            length++;
        }
        return length;
    }
}

复杂度分析

  • 时间复杂度:$O(\log k)$,其中 $k$ 是给定的正整数。操作次数是 $O(\log k)$,每次操作的计算时间是 $O(1)$。

  • 空间复杂度:$O(1)$。

O(1) 做法,一行代码解决(Python/Java/C++/Go)

作者 endlesscheng
2024年9月29日 12:19

本题相当于 3307. 找出第 K 个字符 II 所有 $\textit{operations}[i]=1$ 的版本,做法是一样的,见 我的题解

根据迭代写法二,我们相当于在遍历 $k-1$ 二进制的每个比特 $1$,累加 $1$ 对应的 $\textit{operations}[i]$。由于本题 $\textit{operations}[i]=1$,所以我们计算的是 $k-1$ 二进制中的 $1$ 的个数。

最终答案为 $\texttt{a}$ 加上 $k-1$ 二进制中的 $1$ 的个数。

由于数据范围保证 $k\le 500$,二进制中的 $1$ 的个数至多为 $8$,无需与 $26$ 取模。

###py

class Solution:
    def kthCharacter(self, k: int) -> str:
        return ascii_lowercase[(k - 1).bit_count()]

###java

class Solution {
    public char kthCharacter(int k) {
        return (char) ('a' + Integer.bitCount(k - 1));
    }
}

###cpp

class Solution {
public:
    char kthCharacter(int k) {
        return 'a' + popcount((uint32_t) k - 1);
    }
};

###go

func kthCharacter(k int) byte {
return 'a' + byte(bits.OnesCount(uint(k-1)))
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(1)$。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

模拟

作者 tsreaper
2024年9月29日 12:15

解法:模拟

按题意模拟即可。复杂度 $\mathcal{O}(k)$。

参考代码(c++)

###cpp

class Solution {
public:
    char kthCharacter(int k) {
        string s = "a";
        while (s.size() < k) {
            string t;
            for (char c : s) t.push_back((c - 'a' + 1) % 26 + 'a');
            s += t;
        }
        return s[k - 1];
    }
};

JavaScript 闭包作用域、原型链继承面试题解析教程

作者 天涯学馆
2025年7月2日 21:42

说到 JavaScript 面试,闭包、作用域、原型链、继承这些关键词几乎是“必考题”。很多同学一刷题就头大,明明看过好几遍原理,结果一到面试官面前,还是词不达意、思路混乱。这是为什么?

其实不是你不懂,而是没能用“讲故事”的方式把它讲清楚。面试不只是考概念,更是在考你能不能把复杂问题讲“简单”。比如,闭包到底是“函数里面套函数”这么简单吗?作用域链和执行上下文到底谁先谁后?原型链继承又是怎么一层一层传下去的?

在这篇文章里,我会带你一口气理清这些高频知识点,不讲花哨术语,只用最通俗的例子和最常见的面试题,帮你把零散的知识点串成“系统的知识树”。看完这篇,下一次再遇到相关题目,不仅能答对,还能讲得漂亮!

闭包与作用域

闭包的定义与原理

闭包(Closure)是 JavaScript 的核心特性,指一个函数能够“记住”并访问其定义时所在的作用域,即使该函数在其他作用域中执行。闭包由两部分组成:

  • 函数本身:定义的函数体。
  • 词法环境(Lexical Environment):函数定义时绑定的变量环境。

理论背景

  • JavaScript 使用词法作用域(Lexical Scoping),变量的作用域在代码编写时静态确定。
  • 每个函数创建时,会绑定其定义时的作用域链(Scope Chain),包含外部变量引用。
  • 闭包通过维持对外部变量的引用,延长变量的生命周期。

简单示例

function outer() {
    let count = 0;
    function inner() {
        count++;
        console.log(count);
    }
    return inner;
}

const counter = outer();
counter(); // 输出: 1
counter(); // 输出: 2

逐步分析

  1. outer 定义了变量 count 和函数 inner
  2. inner 引用了外部的 count,形成闭包。
  3. outer 返回 innercountinner 捕获,保存在内存中。
  4. 每次调用 counter()inner 更新并访问 count,实现计数器功能。

闭包的内存机制

  • count 存储在 inner 的词法环境中,不会因 outer 执行结束而销毁。
  • 垃圾回收器(GC)无法回收闭包引用的变量,可能导致内存泄漏,需谨慎管理。

作用域与作用域链

作用域(Scope)定义了变量的可见性和生命周期。JavaScript 有以下作用域类型:

  • 全局作用域:全局变量,生命周期贯穿整个程序。
  • 函数作用域:函数内定义的变量,仅在函数内可见。
  • 块级作用域:使用 letconst{} 内定义的变量(ES6 引入)。

作用域链

  • 当访问变量时,JavaScript 引擎从当前作用域开始,沿作用域链向上查找,直到全局作用域。
  • 作用域链由函数定义时的词法环境决定。

示例

let globalVar = "global";
function outer() {
    let outerVar = "outer";
    function inner() {
        let innerVar = "inner";
        console.log(innerVar, outerVar, globalVar);
    }
    inner();
}
outer();

输出

inner outer global

逐步分析

  1. inner 访问 innerVar(本地),outerVar(外层函数),globalVar(全局)。
  2. 作用域链:inner -> outer -> global
  3. 查找顺序:先本地作用域,再逐级向上。

闭包的常见面试题

面试题 1:闭包计数器

问题:修改以下代码,使每次调用返回不同的计数器实例。

function createCounter() {
    let count = 0;
    return function() {
        return count++;
    };
}
const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1

答案

function createCounter() {
    let count = 0;
    return function() {
        return count++;
    };
}

const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 0
console.log(counter1()); // 1
console.log(counter2()); // 0
console.log(counter2()); // 1

分析

  • 每次调用 createCounter 创建新的闭包,count 是独立的。
  • counter1counter2 引用不同的词法环境。

面试题 2:循环中的闭包

问题:以下代码输出什么?如何修复?

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
}

输出

3
3
3

原因

  • var 具有函数作用域,i 是全局变量,setTimeout 回调执行时,i 已变为 3。
  • 闭包捕获的是变量引用,而非值。

修复方法 1:使用 let(块级作用域):

for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
}

输出

0
1
2

修复方法 2:使用 IIFE(立即执行函数表达式):

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(() => console.log(j), 1000);
    })(i);
}

分析

  • let 为每次循环创建新的绑定。
  • IIFE 每次循环创建新的作用域,捕获当前 i 的值。

面试题 3:私有变量

问题:实现一个带有私有变量的模块。

function createPerson(name) {
    let _age = 0; // 私有变量
    return {
        getName: () => name,
        getAge: () => _age,
        setAge: (age) => { _age = age; }
    };
}

const person = createPerson("Alice");
console.log(person.getName()); // Alice
console.log(person.getAge()); // 0
person.setAge(25);
console.log(person.getAge()); // 25
console.log(person._age); // undefined

分析

  • _age 是闭包中的私有变量,无法直接访问。
  • 通过返回对象的方法控制访问,模拟封装。

闭包的应用场景

  1. 数据封装:如上例的私有变量。
  2. 状态维护:如计数器、事件处理。
  3. 函数柯里化
function curryAdd(a) {
    return function(b) {
        return a + b;
    };
}

const add5 = curryAdd(5);
console.log(add5(3)); // 8
  1. 事件处理
function setupButton(id) {
    let count = 0;
    document.getElementById(id).addEventListener('click', () => {
        console.log(`Clicked ${++count} times`);
    });
}
setupButton('myButton');

分析

  • 闭包维护 count,确保按钮点击次数持久化。
  • 避免全局变量污染。

原型链与继承

原型链的定义与原理

JavaScript 使用原型链(Prototype Chain)实现继承。每个对象有一个内部 [[Prototype]] 属性(通过 __proto__Object.getPrototypeOf 访问),指向其原型对象。原型链是对象查找属性的路径。

核心概念

  • 原型对象:每个函数有一个 prototype 属性,指向原型对象。
  • 构造函数:通过 new 创建对象时,对象的 [[Prototype]] 指向构造函数的 prototype
  • 属性查找:访问对象属性时,若对象本身没有,则沿原型链向上查找。

示例

function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};

const alice = new Person("Alice");
alice.sayHello(); // Hello, I'm Alice
console.log(alice.__proto__ === Person.prototype); // true

逐步分析

  1. Person 是一个构造函数,其 prototype 属性指向原型对象。
  2. new Person("Alice") 创建对象 alice,其 [[Prototype]] 指向 Person.prototype
  3. alice.sayHello() 查找 sayHello,在 alice 自身找不到,沿原型链找到 Person.prototype.sayHello

原型链的继承

JavaScript 通过原型链实现继承,子类原型指向父类实例。

示例

function Animal(type) {
    this.type = type;
}

Animal.prototype.eat = function() {
    console.log(`${this.type} is eating`);
};

function Dog(name, type) {
    Animal.call(this, type); // 继承属性
    this.name = name;
}

Dog.prototype = Object.create(Animal.prototype); // 继承方法
Dog.prototype.constructor = Dog; // 修复构造函数
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog is eating
dog.bark(); // Max barks

逐步分析

  1. Animal.call(this, type) 调用父类构造函数,继承 type 属性。
  2. Object.create(Animal.prototype) 创建新对象,继承 Animal.prototype 的方法。
  3. 修复 constructor 确保 dog instanceof Dog 正确。
  4. 原型链:dog -> Dog.prototype -> Animal.prototype -> Object.prototype

原型链的常见面试题

面试题 1:原型链查找

问题:以下代码输出什么?

function Foo() {}
Foo.prototype.x = 1;

const foo = new Foo();
console.log(foo.x); // 1
foo.x = 2;
console.log(foo.x); // 2
console.log(Foo.prototype.x); // 1

分析

  • foo.x 初始查找 Foo.prototype.x,输出 1。
  • foo.x = 2foo 自身创建属性 x,不影响原型。
  • Foo.prototype.x 仍为 1。

面试题 2:继承实现

问题:实现一个继承方法,支持多级继承。

function inherit(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function() {
    console.log(`${this.type} eats`);
};

function Dog(name, type) {
    Animal.call(this, type);
    this.name = name;
}
inherit(Dog, Animal);
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • inherit 函数封装原型链继承,复用性高。
  • Object.create 避免直接修改父类原型。

面试题 3:instanceof 原理

问题:以下代码输出什么?

console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true

分析

  • instanceof 检查对象原型链是否包含构造函数的 prototype
  • dog 的原型链:Dog.prototype -> Animal.prototype -> Object.prototype

ES6 Class 继承

ES6 引入 class 语法,简化继承:

class Animal {
    constructor(type) {
        this.type = type;
    }
    eat() {
        console.log(`${this.type} eats`);
    }
}

class Dog extends Animal {
    constructor(name, type) {
        super(type);
        this.name = name;
    }
    bark() {
        console.log(`${this.name} barks`);
    }
}

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • class 是原型继承的语法糖,super 调用父类构造函数。
  • 更直观,但底层仍是原型链。

数据结构与算法在前端面试中的重要性

为什么重要

数据结构与算法(DSA)在前端面试中至关重要,原因如下:

  • 性能优化:高效算法减少 DOM 操作、渲染时间,提升用户体验。
  • 逻辑能力:算法题考察逻辑思维和问题解决能力。
  • 跨领域应用:前端与后端(如 Node.js)、机器学习(如 CNN 可视化)交互需要 DSA 知识。
  • 竞争力:顶级公司(如 Google、Meta)要求扎实的算法基础。

前端场景

  • 数组操作:过滤、排序、去重(如用户列表处理)。
  • 树结构:DOM 树遍历、组件树优化。
  • 图算法:依赖解析(如 Webpack 模块依赖)。
  • 时间复杂度:优化大数据量渲染(如虚拟列表)。

常见数据结构与算法

数组与字符串

面试题:反转字符串

问题:编写函数反转字符串,不使用内置方法。

function reverseString(s) {
    let arr = s.split('');
    let left = 0, right = arr.length - 1;
    while (left < right) {
        [arr[left], arr[right]] = [arr[right], arr[left]];
        left++;
        right--;
    }
    return arr.join('');
}

console.log(reverseString("hello")); // "olleh"

分析

  • 时间复杂度:O(n),空间复杂度:O(n)。
  • 使用双指针交换字符,避免额外空间。

链表

面试题:反转链表

class ListNode {
    constructor(val, next = null) {
        this.val = val;
        this.next = next;
    }
}

function reverseList(head) {
    let prev = null, curr = head;
    while (curr) {
        let next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

const list = new ListNode(1, new ListNode(2, new ListNode(3)));
const reversed = reverseList(list);
console.log(reversed.val); // 3

分析

  • 时间复杂度:O(n),空间复杂度:O(1)。
  • 链表在前端用于事件队列、历史记录。

面试题:二叉树前序遍历

class TreeNode {
    constructor(val, left = null, right = null) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

function preorderTraversal(root) {
    const result = [];
    function traverse(node) {
        if (!node) return;
        result.push(node.val);
        traverse(node.left);
        traverse(node.right);
    }
    traverse(root);
    return result;
}

const tree = new TreeNode(1, new TreeNode(2), new TreeNode(3));
console.log(preorderTraversal(tree)); // [1, 2, 3]

分析

  • 时间复杂度:O(n),空间复杂度:O(h)(h 为树高)。
  • 前端应用:DOM 树遍历、组件树解析。

面试题:深度优先搜索(DFS)

function dfs(graph, start) {
    const visited = new Set();
    function traverse(node) {
        visited.add(node);
        console.log(node);
        for (let neighbor of graph[node]) {
            if (!visited.has(neighbor)) {
                traverse(neighbor);
            }
        }
    }
    traverse(start);
}

const graph = {
    A: ['B', 'C'],
    B: ['A', 'D', 'E'],
    C: ['A', 'F'],
    D: ['B'],
    E: ['B', 'F'],
    F: ['C', 'E']
};
dfs(graph, 'A'); // A, B, D, E, F, C

分析

  • 时间复杂度:O(V + E),空间复杂度:O(V)。
  • 应用:依赖解析、组件关系图。

算法在前端的实际应用

虚拟列表优化

处理大数据量列表(如 10,000 条记录):

function createVirtualList(container, items, itemHeight, visibleHeight) {
    let startIndex = 0;
    let endIndex = Math.ceil(visibleHeight / itemHeight);
    
    function render() {
        container.innerHTML = '';
        for (let i = startIndex; i < endIndex; i++) {
            const div = document.createElement('div');
            div.style.height = `${itemHeight}px`;
            div.textContent = items[i];
            container.appendChild(div);
        }
    }
    
    container.addEventListener('scroll', () => {
        startIndex = Math.floor(container.scrollTop / itemHeight);
        endIndex = startIndex + Math.ceil(visibleHeight / itemHeight);
        render();
    });
    
    render();
}

const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
createVirtualList(document.getElementById('list'), items, 50, 500);

分析

  • 仅渲染可视区域,降低 DOM 操作开销。
  • 时间复杂度:O(k),k 为可视项数。

CNN 结果可视化

结合 Python CNN 项目,前端可视化训练结果:

fetch('/api/cnn_results')
    .then(response => response.json())
    .then(data => {
        const ctx = document.getElementById('chart').getContext('2d');
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: data.epochs,
                datasets: [{
                    label: '验证准确率',
                    data: data.val_accuracy,
                    borderColor: '#007bff',
                    fill: false
                }]
            }
        });
    });

分析

  • 使用 Chart.js 绘制 CNN 训练曲线。

  • 后端(Node.js 或 Python Flask)提供数据:

    from flask import Flask, jsonify
    app = Flask(__name__)
    
    @app.route('/api/cnn_results')
    def cnn_results():
        return jsonify({
            'epochs': list(range(1, 51)),
            'val_accuracy': [0.65, 0.70, 0.75, ...]
        })
    

企业级实践

Node.js 与 Python 交互

前端通过 Node.js 调用 Python CNN 模型:

const { spawn } = require('child_process');

function runPythonScript(scriptPath, args) {
    return new Promise((resolve, reject) => {
        const python = spawn('python', [scriptPath, ...args]);
        let output = '';
        python.stdout.on('data', (data) => {
            output += data.toString();
        });
        python.stderr.on('data', (data) => {
            reject(data.toString());
        });
        python.on('close', () => {
            resolve(output);
        });
    });
}

runPythonScript('cifar10_project/scripts/predict.py', ['image.jpg'])
    .then(result => console.log(result))
    .catch(err => console.error(err));

Python 脚本 (predict.py):

import sys
import tensorflow as tf
import numpy as np

model = tf.keras.models.load_model('cifar10_project/models/cifar10_model.h5')
image = tf.keras.preprocessing.image.load_img(sys.argv[1], target_size=(32, 32))
image = tf.keras.preprocessing.image.img_to_array(image) / 255.0
image = np.expand_dims(image, axis=0)
prediction = model.predict(image)
print(np.argmax(prediction[0]))

分析

  • Node.js 使用 child_process 调用 Python 脚本。
  • 适合前端展示 CNN 预测结果。

Docker 部署

部署前端与 CNN 后端:

echo 'FROM node:16
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "server.js"]' > Dockerfile
docker build -t frontend_app .

server.js:

const express = require('express');
const { runPythonScript } = require('./utils');

const app = express();
app.use(express.static('public'));

app.get('/api/predict', async (req, res) => {
    const result = await runPythonScript('predict.py', ['image.jpg']);
    res.json({ prediction: result });
});

app.listen(3000, () => console.log('Server running on port 3000'));

分析

  • 前端通过 Express 提供静态文件和 API。
  • Docker 容器化部署,确保环境一致。

深入闭包与作用域

闭包的底层实现

闭包的实现依赖于 JavaScript 引擎(如 V8)的词法环境(Lexical Environment)和执行上下文(Execution Context)。以下是其底层机制:

  • 词法环境:每个函数创建时,V8 为其生成一个词法环境对象,包含:
    • 变量对象:存储本地变量(如 letconst)。
    • 外部引用:指向外层函数的词法环境。
  • 执行上下文:包含变量环境、词法环境和 this 绑定,栈式管理(调用栈)。
  • 闭包捕获:当函数返回时,其词法环境被保留,外部变量引用不会被垃圾回收。

示例(深入分析):

function createCounter() {
    let count = 0;
    return {
        increment: () => ++count,
        getCount: () => count
    };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.getCount()); // 1
console.log(counter.increment()); // 2

逐步分析

  1. createCounter 创建词法环境,包含 count = 0
  2. 返回对象 { increment, getCount },两个函数共享同一词法环境。
  3. V8 引擎为 count 分配堆内存,闭包函数通过引用访问。
  4. 垃圾回收器无法回收 count,因为 incrementgetCount 仍在使用。

内存管理

  • 内存泄漏风险:闭包可能导致未释放的变量累积。例如,事件监听器未移除:
function setupLeak() {
    let data = new Array(1000000).fill(0); // 大数组
    document.getElementById('button').addEventListener('click', () => {
        console.log(data.length); // 闭包引用 data
    });
}

解决

  • 手动移除监听器:

    const button = document.getElementById('button');
    const handler = () => console.log('Clicked');
    button.addEventListener('click', handler);
    // 移除
    button.removeEventListener('click', handler);
    

作用域的进阶应用

块级作用域与 Temporal Dead Zone(TDZ)

ES6 的 letconst 引入块级作用域,并伴随 TDZ(暂时性死区),防止变量在声明前使用。

面试题:以下代码输出什么?

function testTDZ() {
    console.log(x); // ReferenceError
    let x = 10;
}
testTDZ();

分析

  • let x 在声明前不可访问,触发 TDZ 错误。
  • var 无 TDZ,可能导致 undefined

模块作用域

ES6 模块(ESM)引入模块作用域,变量默认私有。

// counter.js
let count = 0;
export function increment() {
    return ++count;
}
export function getCount() {
    return count;
}

// main.js
import { increment, getCount } from './counter.js';
console.log(increment()); // 1
console.log(getCount()); // 1
console.log(increment()); // 2

分析

  • 模块作用域类似闭包,count 仅在模块内可访问。
  • ESM 支持静态分析,优化 Tree Shaking。

面试题 4:闭包与模块

问题:使用闭包重写模块模式。

const counterModule = (function() {
    let count = 0;
    return {
        increment: () => ++count,
        getCount: () => count
    };
})();

console.log(counterModule.increment()); // 1
console.log(counterModule.getCount()); // 1

分析

  • IIFE(立即执行函数表达式)创建私有作用域,模拟模块。
  • 与 ESM 相比,IIFE 动态但不支持 Tree Shaking。

原型链与继承进阶

原型链的底层机制

原型链基于 JavaScript 的对象模型,V8 引擎通过 [[Prototype]] 实现属性查找。以下是关键点:

  • 原型对象Function.prototypeObject.prototype 是原型链的根。
  • 属性遮蔽:对象自身属性优先于原型属性。
  • 性能:深层原型链查找可能影响性能。

示例(属性遮蔽):

function Person(name) {
    this.name = name;
}
Person.prototype.name = "Default";

const person = new Person("Alice");
console.log(person.name); // Alice
delete person.name;
console.log(person.name); // Default

分析

  • delete person.name 移除自身属性,暴露原型属性。
  • 原型链:person -> Person.prototype -> Object.prototype

高级继承模式

寄生组合继承

寄生组合继承是高效的继承方式,避免重复调用父类构造函数。

function inherit(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function() {
    console.log(`${this.type} eats`);
};

function Dog(name, type) {
    Animal.call(this, type);
    this.name = name;
}
inherit(Dog, Animal);
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • Object.create 创建中间对象,避免 Dog.prototype = new Animal() 的副作用。
  • Animal.call 继承属性,inherit 继承方法。

Mixin 模式

Mixin 允许多重继承,复用代码。

const canRun = {
    run() {
        console.log(`${this.name} runs`);
    }
};

function Dog(name) {
    this.name = name;
}
Object.assign(Dog.prototype, canRun);

const dog = new Dog("Max");
dog.run(); // Max runs

分析

  • Object.assign 将 Mixin 方法复制到原型。
  • 适合复用独立功能,如日志、事件处理。

面试题 5:原型链修改

问题:以下代码输出什么?如何避免问题?

function Person() {}
Person.prototype.name = "Shared";

const p1 = new Person();
const p2 = new Person();
p1.name = "Alice";
console.log(p1.name); // Alice
console.log(p2.name); // Shared
Person.prototype.name = "Modified";
console.log(p1.name); // Alice
console.log(p2.name); // Modified

分析

  • p1.name = "Alice"p1 自身创建属性,不影响原型。
  • 修改 Person.prototype.name 影响未遮蔽的实例(如 p2)。
  • 避免问题:避免直接修改原型,使用 Mixin 或实例属性。

面试题 6:instanceof 实现

问题:手动实现 instanceof

function myInstanceof(obj, constructor) {
    let proto = Object.getPrototypeOf(obj);
    while (proto) {
        if (proto === constructor.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

console.log(myInstanceof(dog, Dog)); // true
console.log(myInstanceof(dog, Animal)); // true

分析

  • 遍历 obj 的原型链,检查是否包含 constructor.prototype
  • 时间复杂度:O(n),n 为原型链长度。

数据结构与算法进阶

动态规划

面试题:最长公共子序列(LCS)

问题:求两个字符串的最长公共子序列长度。

function longestCommonSubsequence(text1, text2) {
    const m = text1.length, n = text2.length;
    const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
    
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (text1[i - 1] === text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[m][n];
}

console.log(longestCommonSubsequence("ABCD", "ACDF")); // 3 (ACD)

分析

  • 时间复杂度:O(m_n),空间复杂度:O(m_n)。
  • 前端应用:文本差异比较(如代码编辑器高亮)。

图算法

面试题:广度优先搜索(BFS)

function bfs(graph, start) {
    const visited = new Set();
    const queue = [start];
    visited.add(start);
    
    while (queue.length) {
        const node = queue.shift();
        console.log(node);
        for (let neighbor of graph[node]) {
            if (!visited.has(neighbor)) {
                visited.add(neighbor);
                queue.push(neighbor);
            }
        }
    }
}

const graph = {
    A: ['B', 'C'],
    B: ['A', 'D', 'E'],
    C: ['A', 'F'],
    D: ['B'],
    E: ['B', 'F'],
    F: ['C', 'E']
};
bfs(graph, 'A'); // A, B, C, D, E, F

分析

  • 时间复杂度:O(V + E),空间复杂度:O(V)。
  • 前端应用:组件依赖解析、路由导航。

LeetCode 高频题

面试题:两数之和

问题:给定数组和目标值,找出两个数的索引,使其和等于目标值。

function twoSum(nums, target) {
    const map = new Map();
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        if (map.has(complement)) {
            return [map.get(complement), i];
        }
        map.set(nums[i], i);
    }
    return [];
}

console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]

分析

  • 使用哈希表,时间复杂度:O(n),空间复杂度:O(n)。
  • 前端应用:快速查找 DOM 元素对。

前端性能优化

节流与防抖

节流(Throttle):限制函数在固定时间间隔内执行一次。

function throttle(fn, delay) {
    let last = 0;
    return function(...args) {
        const now = Date.now();
        if (now - last >= delay) {
            fn.apply(this, args);
            last = now;
        }
    };
}

const scrollHandler = throttle(() => console.log('Scrolled'), 1000);
window.addEventListener('scroll', scrollHandler);

防抖(Debounce):延迟执行,直到事件停止触发。

function debounce(fn, delay) {
    let timer;
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

const resizeHandler = debounce(() => console.log('Resized'), 500);
window.addEventListener('resize', resizeHandler);

分析

  • 节流适合高频事件(如滚动),防抖适合输入验证。
  • 优化前端交互性能,减少不必要的计算。

虚拟 DOM 优化

React 的虚拟 DOM 优化 DOM 操作:

class List extends React.Component {
    shouldComponentUpdate(nextProps) {
        return this.props.items !== nextProps.items;
    }
    
    render() {
        return (
            <div>
                {this.props.items.map(item => <div key={item.id}>{item.text}</div>)}
            </div>
        );
    }
}

分析

  • shouldComponentUpdate 避免不必要的重新渲染。
  • 时间复杂度:O(n) 比较虚拟 DOM 树。

与 CNN 项目的整合

前端可视化 CNN 结果

使用 Chart.js 可视化 Python CNN 训练结果:

fetch('/api/cnn_results')
    .then(response => response.json())
    .then(data => {
        const ctx = document.getElementById('accuracyChart').getContext('2d');
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: data.epochs,
                datasets: [
                    {
                        label: '训练准确率',
                        data: data.accuracy,
                        borderColor: '#007bff',
                        fill: false
                    },
                    {
                        label: '验证准确率',
                        data: data.val_accuracy,
                        borderColor: '#28a745',
                        fill: false
                    }
                ]
            },
            options: {
                scales: {
                    y: {
                        beginAtZero: true,
                        max: 1
                    }
                }
            }
        });
    });

Python 后端(Flask):

from flask import Flask, jsonify
app = Flask(__name__)

@app.route('/api/cnn_results')
def cnn_results():
    return jsonify({
        'epochs': list(range(1, 51)),
        'accuracy': [0.65, 0.70, 0.75, ...], # 训练数据
        'val_accuracy': [0.60, 0.65, 0.70, ...]
    })

if __name__ == '__main__':
    app.run(port=5000)

分析

  • 前端通过 Fetch API 获取数据,Chart.js 绘制曲线。
  • 后端使用 Flask 提供 REST API,结合 Anaconda 环境运行。

WebAssembly 调用 CNN

使用 TensorFlow.js 或 ONNX.js 运行 CNN 模型:

import * as tf from '@tensorflow/tfjs';

async function predict(imageElement) {
    const model = await tf.loadLayersModel('/models/cifar10_model.json');
    const img = tf.browser.fromPixels(imageElement).resizeNearestNeighbor([32, 32]).toFloat().div(255).expandDims();
    const prediction = model.predict(img);
    const result = await prediction.data();
    console.log(result);
}

const img = document.getElementById('inputImage');
predict(img);

分析

  • TensorFlow.js 在浏览器运行 CNN 模型,无需后端。

  • 需将 Python 模型转换为 TF.js 格式:

    tensorflowjs_converter --input_format keras cifar10_project/models/cifar10_model.h5 cifar10_project/models/web_model
    

Node.js 与 Python 交互

Node.js 调用 Python CNN 预测:

const { spawn } = require('child_process');

function runPrediction(imagePath) {
    return new Promise((resolve, reject) => {
        const python = spawn('python', ['predict.py', imagePath]);
        let output = '';
        python.stdout.on('data', (data) => output += data);
        python.stderr.on('data', (data) => reject(data.toString()));
        python.on('close', () => resolve(output));
    });
}

runPrediction('image.jpg').then(result => console.log(`Prediction: ${result}`));

predict.py

import sys
import tensorflow as tf
import numpy as np

model = tf.keras.models.load_model('cifar10_project/models/cifar10_model.h5')
image = tf.keras.preprocessing.image.load_img(sys.argv[1], target_size=(32, 32))
image = tf.keras.preprocessing.image.img_to_array(image) / 255.0
image = np.expand_dims(image, axis=0)
prediction = model.predict(image)
print(np.argmax(prediction[0]))

分析

  • 使用 child_process 调用 Python 脚本。

  • Linux 命令管理进程:

    ps aux | grep python
    kill -9 <pid>
    

企业级实践

微前端架构

使用 Module Federation 实现微前端:

// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
    plugins: [
        new ModuleFederationPlugin({
            name: 'host',
            remotes: {
                app1: 'app1@http://localhost:3001/remoteEntry.js'
            }
        })
    ]
};

// app1.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
    plugins: [
        new ModuleFederationPlugin({
            name: 'app1',
            filename: 'remoteEntry.js',
            exposes: {
                './Chart': './src/Chart.js'
            }
        })
    ]
};

分析

  • 微前端分解大型应用,独立部署。
  • 适合 CNN 可视化模块的动态加载。

CI/CD 集成

使用 GitHub Actions 自动化部署:

name: Deploy Frontend
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16'
      - run: npm install
      - run: npm run build
      - name: Deploy to S3
        run: aws s3 sync ./dist s3://my-bucket

分析

  • 自动化构建和部署前端代码。

  • 结合 Docker 部署 CNN 后端:

    docker push myrepo/cnn_app:latest
    

Kubernetes 部署

部署前端与 CNN 服务:

kubectl create -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cnn-frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: cnn-frontend
  template:
    metadata:
      labels:
        app: cnn-frontend
    spec:
      containers:
      - name: frontend
        image: frontend_app:latest
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: cnn-frontend-service
spec:
  selector:
    app: cnn-frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer
EOF

分析

  • 部署前端服务,负载均衡提高可用性。
  • 可扩展到 CNN 后端,分配 GPU 资源。
昨天 — 2025年7月2日技术

【拒绝平庸】Vue+ECharts图表美化--柱状图展示优化教程

作者 Easy_Y
2025年7月2日 20:01

优化后的饼图效果

6.gif

Scss样式部分

html,body{
  width: 100%;
  height: 100%;
  padding:0px; 
  box-sizing: border-box;
  overflow: hidden;
}
 
body{
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
}
 
.layout-demo-box{
  display: flex;
  flex-direction: column;
  width: 540px;
  height: 300px;
  background: linear-gradient(
    to bottom,
  #000e2a 0%,
  #000000 10%,
  #001134 100%
  );
  border: 1px solid #00436e;
  border-radius: 5px;
  *{
    box-sizing: border-box;
  }
  .title-box{
    display: flex;
    align-items: center;
    width: 100%; 
    height: 50px;
    flex-shrink: 0;
    padding: 20px 30px 0px 20px; 
    span{
      flex-shrink: 0;

      &:nth-child(1){
        width: 0px;
        flex-grow: 1;
      }
    }
    .btn-box{
      display: block;
      color:#6bf6fc;
      cursor: pointer;
    }
    h1{
      font-size: 14px; 
      line-height: 16px; 
      margin: 0px;
      background: linear-gradient(to top, #00d1fe, #fff);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;  
    }
    p{
      font-size: 12px;
      margin:2px 0px;
      color:#416387;
    }
  }
  .chart-box{
    width: 100%;
    height: 0px;
    flex:1;
  } 
}  

HTML页面部分

<div id="app">
  <!-- demo内容 start -->  
  <div class="layout-demo-box">
    <div class="title-box">
      <span>
        <h1>柱状图面板</h1>
        <p>统计日期(2025-07-02 12:00:00)</p> 
      </span>  
    </div> 
    <div class="chart-box" id="chartId"></div>
  </div>
  <!-- demo内容 end --> 
</div>

JS页面部分

  methods: {
    /**
     * 初始化并渲染 ECharts 图表
     * 功能说明:
     * 1. 创建 ECharts 实例并渲染图表
     * 2. 自动响应窗口大小变化
     * 3. 组件销毁时自动清理资源防止内存泄漏 
     */
    initEcharts() {
      // 1. 获取 DOM 元素 - 添加空检查
      const chartDom = document.getElementById('chartId'); 
      if (!chartDom) {
        console.warn(' 图表容器不存在');
        return;
      }
  
      // 2. 初始化图表实例
      this.myChart  = echarts.init(chartDom); 
      
      // 3. 设置图表配置 
      const option = {
        // option 配置 start ---------------------------------------
        
        // option 配置 end ---------------------------------------
      };
      
      // 4. 应用配置
      try {
        this.myChart.setOption(option); 
      } catch (error) {
        console.error(' 图表配置错误:', error);
      }
  
      // 5. 响应式处理 - 使用防抖优化性能
      this.handleResize  = debounce(() => {
        this.myChart  && this.myChart.resize(); 
      }, 300);
      
      window.addEventListener('resize',  this.handleResize); 
    },
    
    // 清理资源 
    destroyEcharts() {
      if (this.myChart)  {
        window.removeEventListener('resize',  this.handleResize); 
        this.myChart.dispose(); 
        this.myChart  = null;
      }
    }
  },
  
  // Vue生命周期钩子-组件挂载完成后调用
  mounted() {
    this.$nextTick(() => {
      this.initEcharts(); 
    });
  }, 

  // Vue生命周期钩子-组件销毁前调用
  beforeDestroy() {
    this.destroyEcharts(); 
  }

定义data数据

  // 数据
  chartData:{
    xAxisData: ['语文','数学','英语','科学','历史'],
    seriesData : [20, 80, 100, 40, 34, 90, 60]
  }, 

柱状图的option配置

tooltip:{
  trigger: 'axis',
  axisPointer: {
    type: 'shadow',
    shadowStyle: { // 鼠标经过背景色
      color: 'rgba(0, 67, 110, 0.1)',
    } 
  },
  formatter: function(params) { 
    return params[0].marker + params[0].name + "成绩:" + params[0].data
  }
}, 
animation: true,
grid: {
  top: "40",
  bottom: "40",
  left: "50", 
  right: "20"  
},
xAxis: {
  data: chartData.xAxisData,
  axisLine: {
    show: true, //隐藏X轴轴线
    lineStyle: {
      color: '#0A376C'
    }
  },
  axisTick: {
    show: false //隐藏X轴刻度
  },
  axisLabel: {
    show: true,
    margin: 14,
    fontSize: 12,
    textStyle: {
      color: "#A3C0DF" //X轴文字颜色
    }
  }  
},
yAxis: [
  {
    type: "value",
    gridIndex: 0,  
    splitLine: {
      show: true,
      lineStyle: {
        type: 'dashed', // 关键属性:虚线
        color: '#011731',
        width: 1
      },
    },
    axisTick: {
      show: false
    },
    axisLine: {
      show: false, 
    },
    axisLabel: {
      show: true,
      margin: 14,
      fontSize: 10,
      textStyle: {
        color: "#A3C0DF" //X轴文字颜色
      }
    }
  }
], 
series: [
  {
    name: "单科成绩",
    type: "bar",
    barWidth: 16,
    itemStyle: {
      normal: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
            offset: 0,
            color: "#07ecd9"
          },
          {
            offset: 1,
            color: "#034881"
          }
        ]), 
      }
    }, 
    data: chartData.seriesData,
    z: 10,
    zlevel: 2,
    label: {
      show: true,
      position: "top",
      distance: 5,
      fontSize:12,
      color: "#01fff4"
    }
  },   
  {
    // 分隔
    type: "pictorialBar",
    itemStyle: {
      normal:{
        color:"#0F375F"
      }
    },
    symbolRepeat: "fixed",
    symbolMargin: 2,
    symbol: "rect",
    symbolClip: true,
    symbolSize: [16, 2],
    symbolPosition: "start",
    symbolOffset: [0, -1], 
    data: chartData.seriesData, 
    z: 0,
    zlevel: 3,
  }
]      

在线Demo

下方可在线查看Demo完整代码

总结

通过以上步骤,我们成功地使用 Echarts 制作并优化了一个柱状图。在实际应用中,大家可以根据具体的数据和业务需求,进一步调整图表的样式和交互效果,让数据可视化更加美观和实用。希望这篇教程能对大家在前端数据可视化开发中有所帮助

告别FLIP动画:View Transition API带来的革命性变革

作者 柚子816
2025年7月2日 19:35

前端动画发展这么多年了,transition、animation等,但他们都有一个问题,就是页面元素结构发生变化时,无法应用动画。所以就有了FLIP动画,它可以在页面元素结构发生变化时,应用动画。

FLIP动画: 曾经的王者

FLIP是First, Last, Invert, Play的缩写,它是一种动画效果,它的核心思想是:在元素结构发生变化时,先记录元素的初始状态,然后记录元素的最终状态,然后计算出元素的变化量,最后应用这些变化量,就可以实现动画效果。

  1. First : 记录元素的当前状态(位置、大小)
  2. Last : 修改元素,记录元素的最终状态(位置、大小)
  3. Invert : 计算元素的变化量, 并通过transform将元素恢复到初始状态
  4. Play : 对transform应用动画,将元素移动到最终状态

这种方式被广泛应用于拖拽, 列表重排等场景中。但是它太麻烦了。需要手动计算元素初始及结束状态,及变化量,然后应用到元素上。

View Transition API: 革命性的变革

View Transition API是一种新的API,它可以在页面元素结构发生变化时,通过动画的方式完成变化

document.startViewTransition(() => {
  // 这里是修改页面元素的代码
  // 比如更新DOM,调整元素位置等
});

只需要将DOM变更的代码放在startViewTransition的回调函数中,就可以实现动画效果了。浏览器会自动:

  • 捕获前后状态
  • 自动为旧视图和新视图创建snapshot
  • 管理动画过程

使用这个新的api你只需要专注于我想改什么, 不再关注怎么应用动画

CSS

View Transition API 还有配套的CSS属性,用于控制动画效果,主要有以下几个属性:

::view-transition, ::view-transition-old, ::view-transition-new, ::view-transition-group, ::view-transition-image-pair

兼容性

除了Firefox, 主流现代浏览器都已经支持了


曾经我们用FLIP实现流畅的动画效果,是因为浏览器不给力。今天,浏览器已经足够给力,是时候说一句

再见,FLIP。你好,View Transition API。

告别盲测:Jest--JavaScript测试之道

作者 烛阴
2025年7月2日 19:04

为什么要测试,测试什么?

为什么要测试?

  • 提升代码质量和可靠性: 尽早发现并修复bug,减少线上事故。
  • 增强重构信心: 有测试用例保驾护航,你可以大胆地优化和重构代码,因为你知道它们会立即发现潜在的回归错误。
  • 提高开发效率: 避免了手动重复测试的繁琐,让你可以更快地迭代新功能。

测试什么?

我们通常将测试分为几个层次:

  • 单元测试 (Unit Tests): 针对代码中最小的可独立测试单元进行测试,如单个函数、类的方法。它们应该快速、独立、可重复。这是JTest的基础和核心。

  • 集成测试 (Integration Tests): 测试多个单元或模块协同工作时的行为,验证它们之间的接口和交互是否正确。例如,测试一个UI组件与数据层API的交互。


JTest初体验——告别盲测的第一步 (Jest入门)

环境搭建

首先,我们来安装Jest。这是一个零配置的测试框架,非常适合快速上手。

# 进入你的项目目录
cd your-js-project

# 安装 Jest
npm install --save-dev jest

然后,在 package.json 中添加一个 test 脚本:

{
  "name": "your-js-project",
  "version": "1.0.0",
  "scripts": {
    "test": "jest"
  },
  "devDependencies": {
    "jest": "^30.0.3"
  }
}

2.2 你的第一个JTest用例

让我们从一个最简单的纯函数开始:加法。

src/sum.js

// 这是一个简单的加法函数
function sum(a, b) {
  return a + b;
}

module.exports = sum; // 导出函数

接下来,我们为 sum.js 创建一个测试文件。根据Jest的约定,测试文件通常与源文件放在同一目录下,并以 .test.js.spec.js 结尾。

src/sum.test.js

const sum = require('./sum'); // 导入要测试的函数

// describe 块用于组织相关的测试用例
describe('sum 函数', () => {
    // test (或 it) 定义一个具体的测试用例
    test('应该正确计算两个正数的和', () => {
        // expect(value) 是 Jest 的全局函数,用于声明一个断言
        // .toBe(expected) 是一个匹配器 (matcher),用于比较值是否相等
        expect(sum(1, 2)).toBe(3);
    });

    test('应该正确计算一个正数和一个负数的和', () => {
        expect(sum(5, -3)).toBe(2);
    });

    test('应该正确计算两个零的和', () => {
        expect(sum(0, 0)).toBe(0);
    });
});

2.3 运行测试

现在,打开你的终端,运行测试命令:

npm test

你将看到类似以下的输出:

> jest

 PASS  ./sum.test.js
  sum 函数
     应该正确计算两个正数的和 (2 ms)
     应该正确计算一个正数和一个负数的和
     应该正确计算两个零的和

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.387 s, estimated 1 s
Ran all test suites.

恭喜你!你已经成功编写并运行了你的第一个JTest用例。PASS 意味着你的代码通过了测试,一切正常。


深入浅出:JTest核心断言与组织

更多的断言匹配器 (Matchers)

Jest提供了丰富的匹配器,用于检查各种条件。

匹配器 描述 示例
toBe(value) 严格相等 (===),用于基本类型 expect(1).toBe(1);
toEqual(value) 递归比较对象或数组的内容相等,用于引用类型 expect({a:1}).toEqual({a:1});
not.toBe(value) 不严格相等 expect(1).not.toBe(2);
toBeTruthy() 检查是否为真值 (truthy) expect(1).toBeTruthy();
toBeFalsy() 检查是否为假值 (falsy) expect(0).toBeFalsy();
toBeNull() 检查是否为null expect(null).toBeNull();
toBeUndefined() 检查是否为undefined expect(undefined).toBeUndefined();
toBeDefined() 检查是否已定义 expect(1).toBeDefined();
toBeInstanceOf(Class) 检查是否是某个类的实例 expect(new Array()).toBeInstanceOf(Array);
toContain(item) 检查数组中是否包含某个元素 expect([1, 2, 3]).toContain(2);
toMatch(regexp) 检查字符串是否匹配正则表达式 expect('hello').toMatch(/ll/);
toThrow(error?) 检查函数是否抛出错误 expect(() => { throw new Error(); }).toThrow();
resolves.toBe(value) 检查Promise是否成功解决并匹配值 await expect(Promise.resolve(1)).resolves.toBe(1);
rejects.toThrow(error?) 检查Promise是否失败并抛出错误 await expect(Promise.reject('error')).rejects.toThrow('error');

示例:src/stringUtils.js

function capitalize(str) {
  if (typeof str !== 'string' || str.length === 0) {
    throw new Error('Input must be a non-empty string.');
  }
  return str.charAt(0).toUpperCase() + str.slice(1);
}

module.exports = { capitalize };

src/stringUtils.test.js

const { capitalize } = require('./stringUtils');

describe('capitalize 函数', () => {
    test('应该将字符串的第一个字母大写', () => {
        expect(capitalize('hello')).toBe('Hello');
    });

    test('应该返回相同的大写字符串,如果第一个字母已经是大写', () => {
        expect(capitalize('World')).toBe('World');
    });

    test('应该处理单字符字符串', () => {
        expect(capitalize('a')).toBe('A');
    });

    test('应该抛出错误,如果输入不是字符串', () => {
        expect(() => capitalize(123)).toThrow('Input must be a non-empty string.');
        expect(() => capitalize(null)).toThrow('Input must be a non-empty string.');
    });

    test('应该抛出错误,如果输入是空字符串', () => {
        expect(() => capitalize('')).toThrow('Input must be a non-empty string.');
    });
});

测试生命周期函数:beforeEachafterEach

在某些场景下,你可能需要在每个测试用例运行之前或之后执行一些设置或清理工作。Jest提供了 beforeEach, afterEach, beforeAll, afterAll 等生命周期函数。

  • beforeEach(fn): 在每个 test (或 it) 运行之前执行。
  • afterEach(fn): 在每个 test (或 it) 运行之后执行。
  • beforeAll(fn): 在当前 describe 块中的所有 test 运行之前执行一次。
  • afterAll(fn): 在当前 describe 块中的所有 test 运行之后执行一次。

示例:计数器模块的测试

src/counter.js

let count = 0;

function increment() {
    count++;
}

function decrement() {
    count--;
}

function getCount() {
    return count;
}

function reset() {
    count = 0;
}

module.exports = {
    increment,
    decrement,
    getCount,
    reset
};

src/counter.test.js

const counter = require('./counter');

describe('计数器模块', () => {
    // 在每个测试用例运行前,将计数器重置为0,确保每个测试的独立性
    beforeEach(() => {
        counter.reset();
    });

    test('increment 应该使计数器加一', () => {
        counter.increment();
        expect(counter.getCount()).toBe(1);
    });

    test('decrement 应该使计数器减一', () => {
        counter.decrement();
        expect(counter.getCount()).toBe(-1);
    });

    test('连续调用 increment 应该正确累加', () => {
        counter.increment();
        counter.increment();
        expect(counter.getCount()).toBe(2);
    });

    test('reset 应该将计数器重置为0', () => {
        counter.increment();
        counter.increment();
        counter.reset();
        expect(counter.getCount()).toBe(0);
    });
});

测试覆盖率

衡量你的测试质量的一个重要指标是测试覆盖率 (Test Coverage) 。Jest内置了此功能。

package.jsontest 脚本中添加 --coverage 标志:

{
  "scripts": {
    "test": "jest --coverage"
  }
}

运行 npm test 后,你会在终端看到一个报告,也会在项目根目录生成一个 coverage 文件夹,其中包含详细的HTML报告,你可以打开 coverage/lcov-report/index.html 查看。

覆盖率指标:

  • Statements (语句): 代码中的语句有多少被执行了。
  • Branches (分支): if/else, switch, 三元表达式等分支有多少被执行了。
  • Functions (函数): 函数有多少被调用了。
  • Lines (行): 代码行有多少被执行了。

注意: 100% 覆盖率不代表代码没有bug,它只能说明你的测试执行了所有代码路径,但无法保证这些路径的逻辑都是正确的。你应该追求有意义的覆盖率,而不是盲目追求数字。


结语


如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript开发干货。

深入理解BFC:前端布局中的“隔离结界”

2025年7月2日 19:04

在前端开发中,CSS布局始终是核心技能之一。无论是新手还是资深开发者,都会遇到诸如浮动元素导致的父容器高度塌陷、外边距重叠、元素重叠等问题。而解决这些问题的核心概念之一,就是BFC(Block Formatting Context,块级格式化上下文)。本文将从BFC的定义、触发条件、特性、应用场景及注意事项等方面进行深入解析,帮助你掌握这一布局利器。


一、什么是BFC?

BFC(Block Formatting Context)是一个独立的渲染区域,它遵循特定的布局规则。简单来说,BFC就像一个“结界”:内部元素的布局不会影响外部元素,外部元素也不会干扰内部布局。这种隔离性使得BFC成为解决复杂布局问题的强大工具。

1.1 BFC的核心特性

  • 垂直排列:BFC内的块级元素会垂直排列,间距由margin决定。
  • 外边距折叠:同一BFC内的相邻块级元素的垂直外边距会合并(如margin-top:20pxmargin-bottom:30px合并为30px),但不同BFC之间的外边距不会折叠。
  • 包含浮动元素:BFC会计算内部浮动元素的高度,避免父容器高度塌陷。
  • 隔离浮动:BFC区域不会与浮动元素重叠。
  • 独立布局:BFC内部的布局规则不会影响外部元素,反之亦然。

二、如何触发BFC?

BFC的触发条件是理解其应用的关键。以下是一些常见的触发方式:

2.1 根元素

HTML的根元素<html>默认就是一个BFC。

2.2 浮动元素

当元素的float属性值为leftright时,会创建BFC。

.float-element {
  float: left;
  width: 100px;
  height: 100px;
}

2.3 绝对定位元素

设置positionabsolutefixed的元素会脱离文档流并创建BFC。

.absolute-element {
  position: absolute;
  top: 20px;
  left: 20px;
}

2.4 行内块元素

display设置为inline-blocktable-celltable-caption等值时,元素会创建BFC。

.inline-block-element {
  display: inline-block;
}

2.5 溢出容器

当元素的overflow属性值不为visible(如hiddenautoscroll)时,会触发BFC。

.overflow-container {
  overflow: hidden;
}

2.6 弹性盒子和网格布局

display设置为flexgrid等现代布局模式时,也会创建BFC。

.flex-container {
  display: flex;
}

三、BFC的应用场景

BFC的核心价值在于解决布局中的常见问题。以下是几个典型的应用场景:


3.1 清除浮动(解决父容器高度塌陷)

问题描述:

当子元素使用浮动时,父容器的高度会塌陷为0,导致布局混乱。

解决方案:

通过触发父容器的BFC,使其包裹浮动子元素。

<div class="container">
  <div class="float-box">浮动内容</div>
</div>
.container {
  overflow: hidden; /* 触发BFC */
}

.float-box {
  float: left;
  width: 200px;
  height: 100px;
  background: lightblue;
}

原理:

BFC会计算内部浮动元素的高度,从而避免父容器高度塌陷。


3.2 防止外边距折叠

问题描述:

相邻块级元素的垂直外边距会合并,导致间距不符合预期。

解决方案:

为其中一个元素包裹BFC容器,阻止外边距折叠。

<div class="bfc-container">
  <div class="element1">元素A</div>
</div>
<div class="element2">元素B</div>
.bfc-container {
  overflow: hidden; /* 触发BFC */
}

.element1 {
  margin-bottom: 30px;
}

.element2 {
  margin-top: 20px;
}

原理:

不同BFC之间的外边距不会折叠,因此element1element2的间距为30px + 20px = 50px


3.3 实现两栏布局

问题描述:

左侧固定宽度,右侧自适应宽度的布局。

解决方案:

左侧浮动,右侧触发BFC以自适应剩余空间。

<div class="container">
  <div class="sidebar">侧边栏</div>
  <div class="content">内容区域</div>
</div>
.sidebar {
  float: left;
  width: 200px;
  background: lightgray;
}

.content {
  overflow: hidden; /* 触发BFC */
}

原理:

BFC区域不会与浮动元素重叠,右侧内容会自动填充剩余空间。


3.4 防止元素重叠

问题描述:

浮动元素可能与非浮动元素重叠。

解决方案:

为非浮动元素触发BFC,避免重叠。

<div class="float-box">浮动元素</div>
<div class="bfc-box">非浮动元素</div>
.float-box {
  float: left;
  width: 100px;
  height: 100px;
  background: lightblue;
}

.bfc-box {
  overflow: hidden; /* 触发BFC */
  background: lightgreen;
}

原理:

BFC区域不会与浮动元素重叠,非浮动元素会自动避开浮动元素。


四、BFC的注意事项

尽管BFC强大且实用,但在使用时仍需注意以下几点:

4.1 性能影响

创建过多BFC可能会增加浏览器的计算负担,尤其是在复杂页面中。建议合理使用,避免不必要的触发。

4.2 兼容性

大多数现代浏览器都支持BFC,但在某些旧版本浏览器中可能存在兼容性问题。建议测试后再部署。

4.3 与现代布局技术的结合

随着Flexbox和Grid的普及,许多传统BFC场景已被更直观的现代布局技术替代。例如:

  • Flexbox替代BFC清除浮动

    .container {
      display: flex; /* 自动包含子元素 */
    }
    
  • Grid替代BFC实现多栏布局

    .container {
      display: grid;
      grid-template-columns: 200px 1fr;
    }
    

五、总结

BFC是前端布局中不可或缺的概念,它通过创建独立的渲染区域,帮助开发者解决浮动、外边距折叠、元素重叠等常见问题。掌握BFC的触发条件和应用场景,不仅能提升布局效率,还能避免许多潜在的兼容性问题。

在实际开发中,BFC常与现代布局技术(如Flexbox、Grid)结合使用,既保留了传统方法的灵活性,又兼顾了现代布局的简洁性。通过不断实践和优化,你将能够更高效地构建稳定、美观的网页布局。


附录:BFC触发条件自查清单

在设计布局时,可以通过以下清单检查是否需要触发BFC:

场景 是否需要触发BFC
需要清除浮动
防止外边距折叠
实现自适应布局
避免元素重叠
使用Flexbox/Grid时 ❌(优先使用现代布局技术)

通过本文的讲解,相信你已经对BFC有了更深入的理解。在未来的开发中,合理运用BFC,定能让你的布局更加得心应手!

十万级设备接入的微前端架构设计实践

作者 krysliang
2025年7月2日 18:44

十万级设备接入的微前端架构设计实践

本文以我实际参与的某大厂的十万级设备接入的项目为例,详细阐述在高并发、大数据量场景下,如何构建可扩展、高可用的微前端架构。结合 "领域解耦、性能优先、安全可控" 的设计思想,从架构拆分到落地实现形成完整解决方案。

一、核心挑战

  1. 设备搜索体验:十万级设备列表中,如何实现亚秒级搜索与分页加载?
  2. 第三方集成:与 10 + 外部系统互联时,如何保障鉴权时效性与操作安全性?
  3. 模块通信:5 + 微应用间需共享设备状态、全局筛选条件,如何实现低耦合通信?
  4. 技术栈兼容:在公司强制使用 OpenTiny 组件库的约束下,如何适配多技术栈?
  5. 旧系统迁移:需保留 50 + 旧系统页面功能,如何实现平滑集成与双向通信?

二、架构设计:基于领域驱动设计(DDD)模块拆分

以"业务域 - 微应用" 一一映射原则,将系统划分为四个核心子域,每个子域都是一个独立的微应用,通过主应用网关聚合:

核心子域 核心功能 技术栈选型 部署策略
设备管理域 设备接入、状态监控、批量操作 Angular + OpenTiny 独立容器化部署
认证鉴权域 统一身份管理、第三方系统授权 React + Redux 鉴权中台单独集群
数据可视化域 实时数据看板、设备拓扑渲染 Vue3 + ECharts CDN 静态资源加速
集成兼容域 旧系统嵌套、多技术栈适配 原生 JS + Web Component 混合部署兼容层

架构设计原则

  • 独立性:微应用可单独开发、测试、部署,主应用仅通过注册中心管理
  • 扩展性:支持单域横向扩展(如设备管理域按区域拆分微应用)
  • 兼容性:预留旧系统迁移接口,支持 "渐进式替换" 而非 "一刀切" 重构

三、工程化实践

1. 项目结构标准化

采用 Lerna 管理多包架构,实现 "源码集中管理、应用独立部署":

graph TD
    A[root] --> B[packages]
    A --> C[shared]
    A --> D[config]
    
    B --> E[main-app]
    B --> F[device-manager]
    B --> G[auth-center]
    B --> H[data-visualization]
    B --> I[legacy-adapter]
    
    C --> J[components]
    C --> K[utils]
    C --> L[types]
    
    D --> M[eslint]
    D --> N[typescript]
    D --> O[build]

2. 依赖管理优化

  • 跨应用依赖:通过 Lerna 符号链接(symlink)实现共享模块本地修改实时生效lerna bootstrap --hoist 自动提升公共依赖至根目录,减少冗余安装

  • 版本控制:采用独立版本模式(independent),微应用可单独发布

    // lerna.json
    {
      "version": "independent",
      "npmClient": "pnpm",
      "command": {
        "publish": {
          "conventionalCommits": true,
          "message": "chore(release): publish",
          "registry": "https://registry.npmjs.org/",
          "ignoreChanges": [
            "**/*.md",
            "**/test/**",
            "**/docs/**"
          ]
        },
        "bootstrap": {
          "ignore": "component-*",
          "npmClientArgs": ["--no-package-lock"]
        }
      },
      "changelogPreset": "angular",
      "stream": true,
      "useWorkspaces": true
    }
    

3. 构建与部署流程

  • 差异化构建:支持单应用构建(提升开发效率)lerna run build --scope=device-app
  • 增量构建:仅构建变更过的应用(结合 Git 提交记录)lerna run build --since=last-release
  • 统一部署:通过主应用 nginx 配置反向代理,微应用独立部署至 CDN

4. 代码规范与质量

  • 统一校验:根目录配置 ESLint/Prettier,所有微应用继承规范

  • 提交检查:husky+commitlint 强制 Conventional Commits 格式

    # 安装钩子
    npx husky add .husky/commit-msg "npx commitlint --edit $1"
    

5. 开发体验优化

  • 本地调试:主应用通过qiankun配置本地微应用地址,实现联合调试
  • 热更新支持:各微应用独立开启 HMR(热模块替换),修改即时生效

四、 核心问题解决方案

问题1:万级设备搜索性能优化

目标: 实现”输入即反馈“的搜索体验,本地查询响应≤100ms

分层搜索架构设计:

graph LR
A[本地缓存层] --> B[设备ID索引库]
C[Web Worker线程] --> D[本地模糊匹配]
E[体验优化] --> F[预加载]
  • 本地缓存层
    • 采用分页策略,首次加载 1000 条设备基础信息(ID、名称、状态)至 IndexedDB,建立 Bloom Filter 索引,实现 O (1) 复杂度的设备 ID 存在性检测。
    • 缓存策略:设备基础信息 24 小时过期,状态变更通过 WebSocket 实时更新。
  • 搜索分层处理
    • 第一层:Web Worker 中执行设备 ID 前缀匹配,支持设备名称模糊查询、状态筛选等基础条件
    • 第二层:复杂条件查询(如所属区域、所属产品)触发后端请求,采用分片加载协议(每次加载 500 条),通过请求优先级队列(用户输入停顿 0.5 秒后发送请求)减少无效请求
  • 体验优化
    • 本地保存用户查询与分页状态,再次进入页面时自动恢复
    • 实现“预请求后10页”机制,当用户进行查询时,如果有切换分页操作,预先请求后10页数据。
// 初始化IndexedDB与Bloom Filter
async initDeviceCache() {
  const db = await openIndexedDB('deviceDB', 1, {
    devices: { keyPath: 'id', indexes: [{ name: 'status', keyPath: 'status' }] }
  });
  // 首次加载1000条基础数据
  const initialData = await fetch('/api/devices?page=1&size=1000');
  await db.devices.bulkAdd(initialData);
  // 构建Bloom Filter(预估1000万条数据,误判率0.01%)
  window.deviceBloomFilter = new BloomFilter(10000000, 0.0001);
  initialData.forEach(device => deviceBloomFilter.add(device.id));
}
// 前端缓存层实现(IndexedDB + BloomFilter)
class DeviceCache {
  constructor() {
    this.db = new Dexie('DeviceCacheDB');
    this.db.version(1).stores({ devices: 'id' });
  }

  async initCache(deviceList) {
    // BloomFilter初始化(简化版)
    this.bloom = new BloomFilter(32 * 1024 * 1024, 3); 
    
    await this.db.devices.bulkPut(deviceList);
    deviceList.forEach(d => this.bloom.add(d.id));
  }

  async search(keyword) {
    if (!this.bloom.test(keyword)) return []; // 布隆过滤器预判
    
    return this.db.devices
      .where('id').startsWithIgnoreCase(keyword)
      .limit(50)
      .toArray();
  }
}

// Web Worker搜索处理
const worker = new Worker('search.worker.js');
worker.postMessage({ type: 'INIT_INDEX', data: allDeviceIds });
worker.onmessage = (e) => {
  if (e.data.type === 'SEARCH_RESULT') {
    renderResults(e.data.results);
  }
};

问题2:第三方系统集成与鉴权保鲜

  • 鉴权中台设计,设计统一令牌管理中心:

    • 第三方系统通过 OAuth2.0 协议获取短期访问令牌(2 小时有效期)
    • 主应用通过定时任务(每 30 分钟)向第三方系统推送刷新令牌,避免鉴权过期
    // 鉴权中台核心逻辑
    class AuthManager {
      private tokens = new Map<string, { token: string; expires: number }>();
      
      // 注册第三方系统
      registerSystem(systemId: string, refreshFn: () => Promise<string>) {
        this.scheduleRefresh(systemId, refreshFn);
      }
    
      private scheduleRefresh(systemId: string, refreshFn: () => Promise<string>) {
        setInterval(async () => {
          const newToken = await refreshFn();
          this.tokens.set(systemId, {
            token: newToken,
            expires: Date.now() + 55 * 60 * 1000
          });
          this.notifySystems(systemId, newToken);
        }, 54 * 60 * 1000); // 提前1分钟刷新
      }
    
      private notifySystems(systemId: string, token: string) {
        // 通过消息总线通知所有相关模块
        eventBus.emit('TOKEN_UPDATE', { systemId, token });
      }
    }
    
    
  • 安全沙箱机制

    • 第三方页面通过**<iframe sandbox="allow-scripts allow-same-origin">**嵌入,限制其对主应用 DOM 和本地存储的访问
    • 通信方式:主应用与第三方页面通过postMessage传递加密令牌(AES-256 加密,密钥定期轮换),示例如下:
    // 主应用发送令牌
    function sendTokenToThirdParty(iframeEl, token) {
      const encryptedToken = encrypt(token, getDynamicKey());
      iframeEl.contentWindow.postMessage({
        type: 'TOKEN_REFRESH',
        data: encryptedToken,
        timestamp: Date.now()
      }, 'https://trusted-thirdparty.com');
    }
    
    // 第三方页面接收令牌
    window.addEventListener('message', (e) => {
      if (e.origin !== 'https://main-app.com') return;
      if (e.data.type === 'TOKEN_REFRESH') {
        const token = decrypt(e.data.data, getDynamicKey());
        updateLocalToken(token); // 更新本地令牌
      }
    });
    

问题 3:跨模块通信方案

sequenceDiagram
    AppA->>EventBus: emit('deviceSelected', {id:123})
    EventBus->>AppB: on('deviceSelected', callback)
    AppB->>AppC: 通过SharedState更新全局设备状态
  • 通信分层
    • 轻量级通信:使用CustomEvent发布订阅模式
    • 状态共享:通过Redux维护跨模块共享状态(设备选择态、全局筛选条件)
    • 深度耦合:采用qiankun的initGlobalState机制
// 基于RxJS的跨应用通信总线
const eventBus = new Subject();

// 设备选择事件发布
const publishDeviceSelect = (deviceId) => {
  eventBus.next({
    type: 'DEVICE_SELECTED',
    payload: deviceId,
    source: 'device-manager'
  });
};

// 在监控模块订阅事件
eventBus.subscribe(event => {
  if (event.type === 'DEVICE_SELECTED') {
    loadDeviceDetails(event.payload);
  }
});

// Qiankun全局状态管理
import { initGlobalState } from 'qiankun';

const initialState = { currentDevice: null };
const actions = initGlobalState(initialState);

// 设备模块更新状态
actions.setGlobalState({ currentDevice: selectedDevice });

// 数据模块监听变化
actions.onGlobalStateChange((state, prev) => {
  if (state.currentDevice !== prev.currentDevice) {
    fetchDeviceData(state.currentDevice);
  }
});

问题4:多技术栈整合

技术栈 适配方案 示例场景
Vue3 直接挂载微应用 设备实时监控面板
Angular 封装为Web Component 设备批量操作向导
旧系统 iframe+消息通道 传统设备配置页面

关键实现

// Angular组件封装为Web Component
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { createCustomElement } from '@angular/elements';

@Component({
  selector: 'device-batch-operation',
  template: `...`
})
export class BatchOperationComponent {
  @Input() deviceIds: string[];
  @Output() complete = new EventEmitter<void>();
}

// 注册为Web Component
const BatchOperationElement = createCustomElement(BatchOperationComponent, { injector: injector });
customElements.define('device-batch-op', BatchOperationElement);

// 微应用中直接使用
// <device-batch-op device-ids="['id1','id2']" on-complete="handleComplete()"></device-batch-op>

// Vue3微应用接入
export const deviceMicroApp = {
  name: 'device-monitor',
  entry: '//dev.example.com/vue-app',
  container: '#vue-container',
  activeRule: '/monitor',
  props: {
    onEvent: (event) => eventBus.next(event)
  }
};

问题5:旧系统页面集成

  • 智能路由代理:
    • 主应用路由拦截旧系统 URL,自动判断使用 iframe 嵌入还是跳转至新页面
    • 示例:/legacy/config?deviceId=123 路由自动映射至 <iframe src="/legacy-app/config?deviceId=123">
# 前端网关路由配置
location ~ ^/legacy/ {
  proxy_pass http://old-system:8080;
  add_header X-Frame-Options "ALLOW-FROM https://new-system.com";
}
  • 双向通信桥接
    • 旧系统通过window.parent.postMessage发送操作指令(如 "设备配置完成")
    • 主应用通过注入script标签向旧系统注入 API(如获取当前选中设备,需做好防抖)
// 智能路由代理中间件
app.use('/legacy/:path', (req, res) => {
  const legacyUrl = buildLegacyUrl(req.params.path);
  
  // 添加安全头
  res.setHeader('X-Frame-Options', `ALLOW-FROM ${currentDomain}`);
  res.setHeader('Content-Security-Policy', "default-src 'self' legacy.example.com");
  
  // 代理请求
  axios.get(legacyUrl, {
    headers: { 'X-Auth-Token': generateToken() }
  }).then(response => res.send(response.data));
});

// iframe双向通信桥
class LegacyBridge {
  constructor(iframe) {
    this.iframe = iframe;
    window.addEventListener('message', this.handleMessage);
  }

  handleMessage = (event) => {
    if (event.origin !== LEGACY_ORIGIN) return;
    
    switch (event.data.type) {
      case 'AUTH_EXPIRED':
        this.renewToken();
        break;
      case 'DATA_UPDATE':
        eventBus.next(event.data);
        break;
    }
  };

  sendCommand(command) {
    this.iframe.contentWindow.postMessage({
      type: 'COMMAND',
      payload: command
    }, LEGACY_ORIGIN);
  }
}
}

五、性能优化关键实现

1. 微应用预加载策略

  • 预测性加载:基于用户行为分析(如 80% 用户进入设备管理后会访问监控面板),在主应用初始化时预加载 高频微应用
  • 优先级调度:首屏微应用(如设备列表)优先加载,非首屏微应用(如报表统计)延迟至空闲时加载
// 基于qiankun的预加载配置
import { preloadApps } from 'qiankun';

// 首屏应用立即加载
start();

// 空闲时预加载其他应用
window.addEventListener('load', () => {
  requestIdleCallback(() => {
    preloadApps([
      { name: 'data-visualization', entry: '/apps/visualization' },
      { name: 'report-center', entry: '/apps/report' }
    ]);
  });
});

2. 分片加载

class ChunkedDataLoader {
  private chunkSize = 1000;// 每片数据大小private loadedChunks = new Set<number>();

  async loadChunk(chunkIndex: number) {
    if (this.loadedChunks.has(chunkIndex)) return;

    const start = chunkIndex * this.chunkSize;
    const devices = await this.api.getDevices({
      skip: start,
      limit: this.chunkSize
    });

    await this.localCache.addDevices(devices);
    this.loadedChunks.add(chunkIndex);
  }

// 预测性加载async preloadNextChunks(currentIndex: number) {
    const nextChunk = currentIndex + 1;
    if (!this.loadedChunks.has(nextChunk)) {
      await this.loadChunk(nextChunk);
    }
  }
}

3. 资源加载优化

  • 静态资源 CDN 加速:微应用 JS/CSS 资源部署至 CDN,启用 HTTP/2 多路复用
  • 组件懒加载:非核心组件(如高级筛选器)采用动态 import,减少初始包体积
  • 图片优化:设备图标等静态资源使用 WebP 格式,配合响应式图片加载(srcset 属性

六、安全控制实现

1. CSP策略配置

通过 Content-Security-Policy 限制资源加载与脚本执行,示例:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' https://trusted-cdn.com 'unsafe-inline' (仅开发环境);
  frame-src 'self' https://trusted-thirdparty.com;
  img-src 'self' data: https://device-icons-cdn.com;
  style-src 'self' https://trusted-cdn.com;
  object-src 'none'

2. 令牌安全处理

  • 令牌存储:访问令牌存储在内存,刷新令牌加密后存储在 HttpOnly Cookie
  • 传输安全:所有 API 请求启用 HTTPS,关键接口(如批量操作)额外添加签名参数(时间戳 + 设备 ID + 密钥哈希)
// HttpOnly + Secure Cookie设置
app.post('/login', (req, res) => {
  const token = generateJWT(req.user);

  res.cookie('auth_token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict',
    maxAge: 3600000 // 1小时
  });

  res.sendStatus(200);
});

// JWT刷新中间件
const refreshMiddleware = (req, res, next) => {
  if (req.path.startsWith('/api') && isTokenExpiring(req.token)) {
    const newToken = refreshToken(req.token);
    res.setHeader('X-Refresh-Token', newToken);
  }
  next();
};

七、监控体系实现

1.全链路监控

  • 微应用性能:监控微应用加载时间(JS 下载、渲染完成)、资源加载成功率
  • 运行时错误:通过 window.onerror 捕获 JS 错误,结合 source-map 还原真实报错位置
  • 用户行为:记录关键操作(如设备搜索、批量操作)的响应时间,建立性能基准线

2.告警机制

  • 当微应用加载失败率 > 1% 或平均响应时间 > 500ms 时,触发邮件 + 钉钉告警
  • 第三方系统令牌刷新失败时,立即通知运维团队
// 微应用性能监控
const startPerfMonitor = () => {
  const perfMetrics = {
    appLoadStart: Date.now(),
    resourcesLoaded: 0
  };

  // 资源加载监控
  performance.getEntriesByType('resource').forEach(res => {
    if (res.initiatorType === 'script') {
      perfMetrics.resourcesLoaded++;
    }
  });

  // 帧率监控
  const fpsMonitor = new FPSMonitor();
  fpsMonitor.start();

  // 错误监控
  window.addEventListener('error', (e) => {
    sendErrorLog({
      type: 'RUNTIME_ERROR',
      message: e.message,
      stack: e.error.stack,
      timestamp: Date.now()
    });
  });

  // 应用加载完成事件
  window.addEventListener('DOMContentLoaded', () => {
    perfMetrics.loadTime = Date.now() - perfMetrics.appLoadStart;
    sendPerfMetrics(perfMetrics);
  });
};

// 设备列表滚动性能追踪
const trackScrollPerf = (container) => {
  let lastKnownScrollPosition = 0;
  let frameCount = 0;

  container.addEventListener('scroll', () => {
    frameCount++;

    if (frameCount % 10 === 0) {
      const scrollPosition = container.scrollTop;
      const scrollSpeed = Math.abs(scrollPosition - lastKnownScrollPosition);
      lastKnownScrollPosition = scrollPosition;

      sendPerfData({
        type: 'SCROLL_PERF',
        speed: scrollSpeed,
        position: scrollPosition
      });
    }
  });
};

八、架构拓扑实现

graph TD
  A[主应用网关] -->|路由分发| B[设备管理Vue应用]
  A -->|状态共享| C[认证中心Angular应用]
  A -->|事件总线| D[数据可视化React应用]
  A -->|API代理| E[旧系统接入层]
  
  subgraph 微前端集群
    B --> F[设备列表]
    B --> G[设备详情]
    C --> H[OAuth认证]
    C --> I[令牌管理]
    D --> J[实时监控]
    D --> K[拓扑图]
  end
  
  E -->|iframe| L[传统设备配置]
  E -->|API桥接| M[旧设备管理]

九、其他问题及优化方案

1. 数据一致性问题

挑战:本地缓存(IndexedDB)与服务端数据同步可能产生不一致。

解决方案

  • 乐观更新策略
class DeviceDataManager {
  private localCache: IndexedDB;
  private serverApi: DeviceApi;
  private versionMap: Map<string, number> = new Map();

  async updateDevice(deviceId: string, updates: Partial<Device>) {
// 1. 乐观更新本地缓存const currentVersion = this.versionMap.get(deviceId) || 0;
    const newVersion = currentVersion + 1;

    await this.localCache.update(deviceId, {
      ...updates,
      _version: newVersion,
      _pendingSync: true
    });

    try {
// 2. 异步同步到服务器await this.serverApi.updateDevice(deviceId, {
        ...updates,
        _version: newVersion
      });

// 3. 确认同步成功await this.localCache.update(deviceId, { _pendingSync: false });
      this.versionMap.set(deviceId, newVersion);
    } catch (error) {
// 4. 同步失败,回滚本地更新await this.localCache.update(deviceId, {
        _version: currentVersion,
        _pendingSync: false
      });
      throw new SyncError('设备更新同步失败', error);
    }
  }
}

  • 定期全量同步
class DataSyncManager {
  private syncInterval = 5 * 60 * 1000; // 5分钟

  async startPeriodicSync() {
    setInterval(async () => {
      const pendingSyncs = await this.localCache.getPendingSyncs();
      
      for (const item of pendingSyncs) {
        await this.syncItem(item);
      }
      
      // 获取服务器端的最新变更
      const serverChanges = await this.serverApi.getChangesSince(
        this.lastSyncTimestamp
      );
      
      await this.applyServerChanges(serverChanges);
    }, this.syncInterval);
  }
}

2. 版本管理方案

挑战:微应用版本兼容性和平滑升级。

解决方案

  1. 版本兼容性管理
interface VersionCompatibility {
  microApp: string;
  version: string;
  compatibleWith: {
    mainApp: string[];
    microApps: Record<string, string[]>;
  };
}

class VersionManager {
  private compatibilityMatrix: VersionCompatibility[] = [];

  async checkCompatibility(microApp: string, version: string): Promise<boolean> {
    const compatibility = this.compatibilityMatrix.find(
      c => c.microApp === microApp && c.version === version
    );

    if (!compatibility) return false;

// 检查主应用兼容性const mainAppVersion = this.getMainAppVersion();
    if (!compatibility.compatibleWith.mainApp.includes(mainAppVersion)) {
      return false;
    }

// 检查其他微应用兼容性const loadedApps = this.getLoadedMicroApps();
    for (const [app, version] of Object.entries(loadedApps)) {
      if (
        !compatibility.compatibleWith.microApps[app]?.includes(version)
      ) {
        return false;
      }
    }

    return true;
  }
}

3. 国际化支持

挑战:多语言、多时区支持。

解决方案

  • 统一的国际化框架
class I18nManager {
  private translations: Record<string, Record<string, string>> = {};
  private currentLocale: string = 'zh-CN';
  private fallbackLocale: string = 'en-US';

  async loadTranslations(locale: string) {
    if (this.translations[locale]) return;

    const translations = await fetch(`/i18n/${locale}.json`);
    this.translations[locale] = await translations.json();
  }

  translate(key: string, params: Record<string, string> = {}) {
    let template = this.translations[this.currentLocale]?.[key]
      || this.translations[this.fallbackLocale]?.[key]
      || key;

    return template.replace(/\${(\w+)}/g, (_, param) => params[param] || '');
  }
}

  • 时区处理
class TimeZoneManager {
  private userTimeZone: string;

  constructor() {
    this.userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  }

  formatDate(date: Date, format: string = 'full'): string {
    return new Intl.DateTimeFormat('zh-CN', {
      timeZone: this.userTimeZone,
      ...this.getFormatOptions(format)
    }).format(date);
  }

// 转换时间戳到用户时区convertToUserTime(timestamp: number): Date {
    return new Date(timestamp);
  }

// 转换用户时间到UTCconvertToUTC(localDate: Date): number {
    return localDate.getTime();
  }
}

// 在Vue组件中使用@Component
class DeviceTimeline extends Vue {
  @Inject() timeZoneManager!: TimeZoneManager;

  formatEventTime(timestamp: number) {
    const localDate = this.timeZoneManager.convertToUserTime(timestamp);
    return this.timeZoneManager.formatDate(localDate, 'short');
  }
}

基于 ethers.js 的区块链事件处理与钱包管理实践指南

作者 木西
2025年7月2日 18:40

前言

本文将围绕 事件检索与监听HD 钱包批量生成与加密存储静态调用与 callData 构造ERC 标准合约识别 等关键场景,结合代码示例与最佳实践,展示如何利用 ethers.js 完成从基础交互到高级功能的完整流程。无论是初学者还是有经验的开发者,都能通过本指南快速掌握 ethers.js 的核心用法,并将其应用于实际项目中

Event事件

检索事件

const { ethers } = require("hardhat");
async function SearchEvent() {
    try {
        const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
        const signer = await provider.getSigner();
        const TokenAddress = "0xxxxx";//合约地址
        const TokenABI =[]//合约的abi;
        const TokenContract = new ethers.Contract(TokenAddress, TokenABI, signer);//创建合约
        //读取合约
        const name = await TokenContract.name();
        console.log("Contract Name:", name);
        const symbol = await TokenContract.symbol();
        console.log("Contract Symbol:", symbol);
        const totalSupply = await TokenContract.totalSupply();
        console.log("Total Supply:", totalSupply.toString());
        //合约转eth
        const arr1="0xxxxxxxx"
        await TokenContract.transfer(arr1,10);//给arr1转10;
        
        const block = await provider.getBlockNumber()//得到当前block
         const transferEvents = await TokenContract.queryFilter('Transfer', block - x, block);//检索合约Transfer,从block - x,到block之间的解析事件
          console.log(`Transfer事件数量: ${transferEvents.length}`);
         //transferEvents是个数组,我们可以解析他的参数
         console.log(...transferEvents[0].args);//返回form,to ,value
       }catch (error) {
        console.error("Error:", error);
    }
    }

监听事件

//以上同上
TokenContract.on("Transfer", (from, to, value, event) => {
            console.log(`Transfer事件触发:`);
            console.log(`From: ${from}`);
            console.log(`To: ${to}`);
            console.log(`Value: ${value.toString()}`);
            console.log(` 从 ${from}=> 到 ${to} = ${value.toString()}`); 
            console.log(`Event Details:`, event);   
        });

过滤事件

设置过滤规则:contract.filters.EVENT_NAME( ...args )说明:EVENT_NAME:过滤事件,...args:过滤规则

基础规则汇总

规则 含义 示例
null 该位置不限制,匹配任意值 contract.filters.Transfer(null, addr)
单个值 必须完全匹配 contract.filters.Transfer(addr)
数组 至少匹配数组中任意一个值 contract.filters.Transfer(null, [addr1, addr2])
以上代码如上
//设置规则
# 规则1
let addr1="0xf39Fd6e51aad88F6F4ce6axxxxxxx"
let addr2="0x70997970C51812dc3A010C7xxxxxx"
let addr3="0xb0997970C51812dcxxxxxxxxxxxxx"
let rule1 = TokenContract.filters.Transfer(addr1);//过滤来自`addr1`地址的`Transfer`事件
let rule2 = TokenContract.filters.Transfer(null,addr2);//过滤所有发给 addr2`地址的`Transfer`事件
let rule3 = TokenContract.filters.Transfer(addr1,addr2);//过滤所有从 `addr1`发给`addr2`的`Transfer`事件
let rule3 = TokenContract.filters.Transfer(addr1,addr2);//过滤所有从 `addr1`发给`addr2`的`Transfer`事件
let rule4 = TokenContract.filters.Transfer(null,[addr2,addr3]);//过滤所有发给 addr2`地址的或者addr3`的Transfer`事件
# 其他就是各种组合使用了
# 过滤使用
TokenContract.on(rule1, (res) => {
  console.log('---------监听开始过滤--------');
  console.log(
    `${res.args[0]} -> ${res.args[1]} ${res.args[2]}`
  )
})
# 其他同上 把过滤规则给监听事件即可

批量生成HD钱包

BIP汇总
BIP编号 主要用途 典型格式示例
BIP-32 HD 钱包路径 m/44'/0'/0'/0/0
BIP-39 助记词生成种子 12/24 个单词
BIP-44 多币种路径 m/44'/60'/0'/0/0
BIP-49 隔离见证兼容地址 m/49'/0'/0'/0/0
BIP-84 原生隔离见证地址 m/84'/0'/0'/0/0
BIP-173 Bech32 地址编码 bc1q...
BIP-350 Taproot 地址编码 bc1p...
以BIP-44为例代码实践
  • 助记词生成
 const mnemonic = ethers.Mnemonic.entropyToPhrase(ethers.randomBytes(32))
  • 创建HD基钱包
    BIP-44
    基路格式:"m / purpose' / coin_type' / account' / change" 参数说明
    • m:主密钥(Master Key)
    • purpose':固定为 44'(表示遵循 BIP-44 多账户标准)
    • coin_type':币种标识(如 0' = BTC,60' = ETH,501' = SOL)详细可查看SLIP-44
    • account':账户编号(从 0' 开始)
    • change:比特币专用(0 = 外部地址,1 = 找零地址);其他链通常为 0
    • address_index:地址索引(从 0 开始)
 # BIP-44
 // 基路径:
 const basePath = "44'/60'/0'/0"
 # 生成第一对外的链接
 const baseWallet = ethers.HDNodeWallet.fromPhrase(mnemonic, basePath)
  • 批量生成
const WalletNumber = 10;//钱包数
 for (let i = 0; i < WalletNumber; i++) {
     let NewBaseWallet = baseWallet.derivePath(i.toString());
     console.log(`第${i+1}个钱包地址: ${baseWalletNew.address}`)
     wallets.push(baseWalletNew);//生成10个钱包
 }
console.log("钱包地址列表:", wallets.map(wallet => wallet.address));
  • 加密JSON保存
async function saveWalletJson() {
 const wallet = ethers.Wallet.fromPhrase(mnemonic);//助记词
 console.log("通过助记词创建钱包:")
 console.log(wallet)
 // 加密json用的密码,可以更改成别的
 const pwd = "XXXX";
 const json = await wallet.encrypt(pwd)
 console.log("钱包的加密json:")
 console.log(json)
 require("fs").writeFileSync("keystoreBatch.json", json);//在当前文件夹下生成一个 keystoreBatch.json文件
 }
 saveWalletJson();
  • 通过加密json读取钱包信息
async function ReadWalletJson() {
console.log("开始读取json文件");
const json=require("fs").readFileSync("keystoreBatch.json", "utf8");
const walletJson =await ethers.Wallet.fromEncryptedJson(json, "xxx");//生成json时设置的密码
console.log("Wallet from JSON:",walletJson);
console.log("Address:", walletJson.address);
console.log("Private Key:", walletJson.privateKey);
console.log("Mnemonic:", walletJson.mnemonic.phrase);
}
ReadWalletJson();

staticCall和callStatic:

名称 所属模块 作用 返回值 适用场景
staticCall ethers.Contract 实例方法 只读方式 调用合约函数,不修改状态 函数返回值 任何函数(读/写)
callStatic ethers.Contract 实例方法(v6 新增) 只读方式 调用合约函数,不修改状态 函数返回值 任何函数(读/写)
# 代码实例
# staticCall
const from="0xf39xxx"
const to="0x70xxx"
const result = await TokenContract.transfer.staticCall(to,10,{  
                     // 可选 overrides
                    from: from, // 指定调用者(模拟不同账户)
                });
                console.log('模拟结果:', result);
# callStatic
const result = await TokenContract.transfer.staticCall(to,10,{                 
                    // 可选 overrides
                    from: from, // 指定调用者(模拟不同账户)
                });
                console.log('模拟结果:', result);

callData

  • 接口abi:infce=new ethers.Interface(abi);//两者是一样的功能

  • callData:infce=TokenContract.interface;//两者是一样的功能

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const signer = await provider.getSigner();
const TokenAddress = "0xxxx";//合约地址
const TokenABI =[];//abi
const TokenContract = new ethers.Contract(TokenAddress, TokenABI, signer);
const param = TokenContract.interface.encodeFunctionData(
    "balanceOf",
    ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]
  );
  console.log("param:", param);
  const tx = {
    to: TokenAddress,
    data: param
}
// 发起交易,可读操作(view/pure)可以用 provider.call(tx)
const balanceWETH = await provider.call(tx)
console.log(`存款前WETH持仓: ${ethers.formatEther(balanceWETH)}\n`)

encodeFunctionData

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const signer = await provider.getSigner();
const TokenAddress = "0xxxxxxx";//合约地址
const TokenContract = new ethers.Contract(TokenAddress, TokenABI, signer);//构造合约
# 使用合约的transfer 向0x70997970C51812dc3A010C7d01b50e0d17dc79C8 转10n
const calldata = TokenContract.interface.encodeFunctionData('transfer', [
  '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // 收款地址
  10n                                           // 转账数量 (BigInt)
]);
console.log(calldata)//生成callData
const wallet = new ethers.Wallet("钱包的私钥", provider);
const tx = await wallet.sendTransaction({
  to: "0x5Fxxxxxxx",//合约地址
  data: calldata,
});
await tx.wait();
console.log("交易成功生成的txHash:", tx.hash);
//通过交易hash 
//交易的详细信息
const hash = await provider.getTransaction(tx.hash);
//交易收据
const receipt = await provider.getTransactionReceipt(tx.hash);

识别ERC20、ERC721、ERC115标准合约

识别关键说明:所有现代标准(ERC721、ERC1155)都实现了 ERC165,通过 supportsInterface(bytes4 interfaceId) 函数声明支持的接口,ERC20 不支持 ERC165

  • ERC20

    说明:识别关键ERC20不是基于ERC165,但是ERC20包含totalSupply,识别关键通过totalSupply
    const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
    const signer = await provider.getSigner();//
    const TokenAddress = "0x5Fbxxxxx";//合约地址
    const TokenABI = []//abi
    const TokenContract = new ethers.Contract(TokenAddress, TokenABI, signer);//创建合约
    const totalSupplyValue=await TokenContract.totalSupply(); 
    console.log(totalSupplyValue)//说明是ERC20
    
  • ERC721

    说明:识别关键是ERC721基于ERC165,ERC165标准包含supportsInterface(bytes4 interfaceId)
     创建合约如上
     const isERC721 = await contract.supportsInterface("0x80ac58cd");
     console.log(isERC721); // true 或 false
    
  • ERC1155

    说明:识别关键是ERC721基于ERC165,ERC165标准包含supportsInterface(bytes4 interfaceId)
     创建合约如上
     const isERC721 = await contract.supportsInterface("0xd9b67a26");
     console.log(isERC721); // true 或 false
    
  • 总结

    调用函数/方法 返回值 识别结果 备注
    supportsInterface(0x80ac58cd) true ERC721 NFT 标准接口标识符
    supportsInterface(0xd9b67a26) true ERC1155 多代币标准接口标识符
    totalSupply() 等函数调用成功 成功 ERC20 同质化代币标准(无 ERC165)

总结

以上就是系统介绍了使用 ethers.js 进行区块链开发的关键技术,涵盖事件处理、钱包管理、合约交互及标准识别四大核心模块,并通过代码示例与最佳实践提供完整解决方案;

vue3中pinia

2025年7月2日 18:27

Pinia 是 Vue 官方推荐的新一代状态管理库,专为 Vue 3 设计(同时兼容 Vue 2),它简化了状态管理流程,并优化了 TypeScript 支持。以下从核心概念、使用方法和与 Vuex 的对比三方面详细说明:


一、Pinia 的核心概念与特点

  1. 精简架构

    • 无 Mutation:直接通过 actions 修改状态(同步/异步均可),无需 commit
    • 扁平化 Store:每个 Store 独立管理(如 useUserStoreuseCartStore),无需嵌套模块或命名空间。
    • 响应式状态:基于 Vue 3 的 reactive 实现,直接修改状态自动触发更新。
  2. TypeScript 友好

    • 开箱即用的类型推断,无需额外类型声明。
  3. 轻量高效

    • 体积仅约 1KB(gzip),性能优于 Vuex。

二、基本使用

<template>
  <div>实际参数={{count}}</div>
  <div>
    <button @click="addByPina">点击</button>
  </div>
</template>

<script setup lang="ts">
import {useCounterStore} from "@/stores/counter.ts";
import {storeToRefs} from "pinia";

const useCounter = useCounterStore()
let {count} = storeToRefs(useCounter)

// 第1种直接修改pina中数据
function add () {
  count.value++
}
// 第2种利用$patch 批量修改
useCounter.$patch({
  count: 100
})
// 可直接使用pina的方法
function addByPina () {
  useCounter.increment()
}
// 通过subscribe方法监听state变化
useCounter.$subscribe((mutation, state)=>{
  console.log('$subscribe ===', mutation, state)
})
</script>


三、Pinia 与 Vuex 的核心区别

特性 Pinia Vuex
架构 多 Store 独立管理(扁平化) 单一 Store + 嵌套 Modules
状态更新 直接修改 state无需 Mutation 必须通过 commit 触发 Mutation
异步处理 Actions 直接修改状态 Actions 需调用 Mutations 更新状态
TypeScript 原生类型推断,零配置支持 需手动声明类型,配置复杂
模块化 文件即模块(如 userStore.ts namespaced: true 避免命名冲突
代码简洁度 减少 40% 样板代码(无 Mutation) 冗余代码多(State + Mutation + Action)
DevTools 支持时间旅行调试,结构扁平更清晰 支持但嵌套模块路径深
适用场景 Vue 3 新项目、TS 项目、快速迭代 大型遗留项目、需兼容 Vue 2

四、pinia原理

  1. 模块化设计
    每个 Store 独立定义(defineStore),通过唯一 ID 注册到全局 Pinia 实例的 Map 中。Store 之间完全隔离,无命名空间冲突。

  2. 响应式绑定
    使用 Vue 的 reactive() 将 State 转为响应式对象,Getter 通过 computed 实现缓存。解构时用 storeToRefs 将状态转为 Ref 保持响应性。

  3. 状态直改机制
    移除 Vuex 的 Mutation 层,允许在 Action 中直接修改 State(通过 this.state 操作),同步/异步操作统一处理。

  4. 依赖注入
    通过 Vue 的 provide/inject 实现跨组件访问:

    • useStore() 内部 inject 获取 Pinia 实例
    • 从全局 Map 中检索对应 Store
  5. 初始化流程

    • 执行 useStore() 时,若 Store 不存在:
      • 选项式:自动包装 State(reactive)、Getter(computed
      • 组合式:直接执行 setup 函数(类似组件逻辑)
    • 缓存 Store 实例避免重复创建
  6. 插件扩展
    通过 pinia.use() 添加插件,可拦截:

    • Store 创建(添加新属性/方法)
    • Action 执行(日志/监控)
    • 状态变更(持久化存储)
  7. 性能优化

    • markRaw 标记 Pinia 实例避免被转为响应式
    • 精准响应式绑定(仅 State/Getter 响应,Action 不代理)

本质:Pinia 是 Vue 响应式系统的增强应用,通过精简 API 设计(去 Mutation)+ 复用 Composition API 能力,提供比 Vuex 更简洁高效的状态管理。

五、何时选择 Pinia?

  • 推荐 Pinia
    • Vue 3 新项目、需深度 TypeScript 集成、追求简洁代码。
    • 示例:动态权限管理、跨组件表单状态共享。
  • 保留 Vuex
    • 维护 Vue 2 旧项目、需兼容深度嵌套模块的复杂场景。

迁移建议:小型项目直接重写为 Pinia;大型项目逐步替换模块,利用 $patch 兼容旧逻辑。


附:Pinia 进阶技巧

插件扩展
使用 pinia-plugin-persistedstate 实现状态持久化。

Pinia 通过简化 API 和强化 TypeScript 支持,大幅提升了开发体验。其设计理念更贴合 Vue 3 的响应式系统,是未来 Vue 生态状态管理的首选方案。

❌
❌