./avatar.jpg

别再只 console.log 了:前端错误收集与日志治理

大部分前端开发一开始排查问题都是靠:

console.log('看看这里是什么');

这没问题,但当项目变大、用户变多、场景变复杂时,只靠 console.log 就完全不够用了——用户出问题时你根本看不到现场。

这篇文章聊聊我在实际项目里做前端错误收集和日志治理的一些经验。


1. 我们到底需要记录什么?

先看一个真实场景:

  • 用户反馈:「页面有时候会白屏」
  • 操作步骤:「我就是正常点点点,突然就白了」
  • 浏览器:未知
  • 系统:未知
  • URL:未知

如果我们没有任何前端错误监控工具,这个问题几乎无从排查。

所以,一套基础的前端日志/错误系统至少要能回答:

  • 什么时间?
  • 哪个用户?
  • 在哪个页面?
  • 用的什么浏览器/系统?
  • 做了什么操作?
  • 报了什么错(堆栈、接口信息等)?

所以日志结构(特别是错误日志)要包含几个核心字段:

interface FrontendLog {
  level: 'info' | 'warn' | 'error';
  message: string;
  stack?: string;
  url: string;
  userId?: string;
  time: number;
  ua: string; // user agent
  extra?: Record<string, any>;
}

2. 捕获错误的几个入口

2.1 全局 JS 运行时错误

window.onerror = function (message, source, lineno, colno, error) {
  reportError({
    message,
    stack: error && error.stack,
    url: window.location.href,
    extra: { source, lineno, colno },
  });
};

注意:

  • 这个能捕获同步脚本错误
  • 对跨域脚本,如果没有正确配置,会只得到 Script error.,需要在 script 标签上加 crossorigin 且服务端正确设置 CORS/SourceMap。

2.2 Promise 未处理异常

现代前端很多逻辑是基于 Promise / async 的,window.onerror 捕不到,需要监听:

中后台「表格 + 查询」通用化设计实践

中后台系统有一个经典页面模样:

上面一块查询条件,下面一张表格,再加一个分页条。

做过几套之后你会发现:字段变了、接口变了、样式改改,本质都差不多。与其每个项目都重写,不如抽象出一套可复用的「查询 + 表格」方案。

这篇文章记录我在中后台项目里总结的一套通用设计思路。


1. 标准「列表页」长什么样?

通常一个「标准列表页」包含:

  1. 查询区(Search Form)

    • 输入框、下拉、日期区间、状态选择等
    • 「查询」、「重置」按钮
  2. 操作区(Toolbar)

    • 新增、批量操作、导出等
  3. 结果表格(Table)

    • 列信息、操作列
    • 勾选、多选
  4. 分页条(Pagination)

    • 当前页、总数、页大小

可以抽象成一个数据结构:

interface ListPageConfig {
  searchItems: SearchItem[];
  columns: Column[];
  actions: ActionItem[]; // 顶部操作
  rowActions: ActionItem[]; // 行内操作
}

换句话说:绝大部分差异都可以通过配置表达出来。


2. 查询表单的配置化设计

先从搜索区开始。

2.1 基本配置格式

我习惯用一个数组描述查询项:

const searchItems = [
  {
    prop: 'keyword',
    label: '关键词',
    type: 'input',
    placeholder: '请输入订单号/姓名',
  },
  {
    prop: 'status',
    label: '状态',
    type: 'select',
    options: [
      { label: '全部', value: '' },
      { label: '待处理', value: 'PENDING' },
      { label: '已完成', value: 'DONE' },
    ],
  },
  {
    prop: 'dateRange',
    label: '创建日期',
    type: 'daterange',
  },
];

再写一个通用的 <SearchForm /> 组件去渲染它:

单页应用路由设计心得:守护好你的「返回键」

单页应用(SPA)带来了更流畅的交互体验,但也制造了一个经典问题:「返回键」不好使了。

  • 用户点浏览器返回,结果直接退回登录页,心态爆炸;
  • 或者在一个中台系统里,从列表点到详情、从详情点到编辑、再返回时,筛选条件全没了,只剩一个「空空如也」。

这篇文章就结合我自己踩过的坑,聊聊:如何在 SPA 里设计更「靠谱」的路由结构和返回行为。


1. 路由不是简单的 URL 映射

很多项目的路由配置看起来像这样:

const routes = [
  { path: '/list', component: List },
  { path: '/detail/:id', component: Detail },
];

能跑,但如果你问:

  • 列表页的筛选条件放哪?
  • 返回列表时要不要还原滚动位置?
  • 详情页打开方式是「覆盖当前」还是「新标签页」?

这些问题路由本身没有回答。
所以对我来说,路由不仅仅是 path → component 的映射,它还应该承载一些页面行为约定


2. 基本路由结构:从「业务模块」出发

在中后台项目里,我现在更倾向按「业务模块」来规划路由:

const routes = [
  {
    path: '/order',
    component: Layout,
    children: [
      { path: 'list', name: 'OrderList', component: OrderList },
      { path: 'detail/:id', name: 'OrderDetail', component: OrderDetail },
      { path: 'edit/:id', name: 'OrderEdit', component: OrderEdit },
    ],
  },
];

几个小习惯:

  1. 路径短而语义化/order/list/orderListPage 更统一。
  2. 给路由起 name:后面做编程式导航、标签页缓存都会用到。
  3. 模块下的子路由聚在一起:方便看清一个模块的整体结构。

3. 返回行为:列表页的「状态保留」

最常见的场景:

  1. 在订单列表页选择了各种条件,翻到第 4 页;
  2. 点进某个订单详情;
  3. 返回时希望停留在「刚才那一页 + 筛选条件」。

如果不做额外处理,很多 SPA 会出现:

从 jQuery 到模块化:一次前端工程意识的转变

背景

在较早阶段的前端开发中,常见技术栈以 jQuery + 传统页面结构 为主。
这种模式在页面数量有限、业务逻辑简单时,具备实现成本低、交付速度快的优势。

但随着功能逐步增多、页面之间产生联动,代码结构问题开始显现,对可维护性提出了更高要求。


传统写法暴露的问题

1. 全局作用域污染

var list = [];
function init() {}
function render() {}

常见问题包括:

  • 变量与函数暴露在全局作用域
  • 多页面脚本容易互相覆盖
  • 问题定位与调试成本逐渐升高

2. 文件职责混乱

在缺乏结构约束的情况下:

  • 单个 JS 文件体积不断膨胀
  • DOM 操作、业务逻辑、接口请求混杂
  • 修改局部逻辑时容易产生连锁影响

3. 代码复用能力弱

相似功能在不同页面中往往通过复制实现:

  • 复制后再做细微修改
  • 长期来看难以统一维护
  • Bug 修复成本被成倍放大

引入模块化思路

为缓解上述问题,开始尝试在不依赖复杂工具链的前提下,引入基础的模块化设计思想。


1. 按职责拆分代码结构

js/
├── api.js
├── render.js
├── event.js
└── main.js

通过人为约定实现基础分层:

  • 接口请求集中管理
  • 渲染逻辑独立拆分
  • 事件绑定避免分散在各处

即使不依赖构建工具,也能显著提升代码可读性。


2. 使用 IIFE 隔离作用域

(function () {
  var state = {};
  function init() {}
  window.pageInit = init;
})();

这种方式在早期用于:

从零搭一个前端构建脚手架:Webpack 实践

那会儿刚开始上手 Webpack 的时候,看官方文档一堆术语:entry、output、loader、plugin,看得云里雾里。直到自己从零搭了一套非常简陋的脚手架,才真正把这些概念串起来。

这篇文章就用一个「最小可用前端脚手架」为例,帮你把 Webpack 入门阶段最常见的概念打通。


目标:一个最小可用的前端脚手架

希望做到:

  • 支持打包 ES6 代码(import箭头函数 等)。
  • 支持打包 CSS。
  • 支持本地开发服务器 + 热更新。
  • 打包输出一份 dist,可以直接丢到静态服务器上跑。

目录长这样:

my-webpack-starter
├── package.json
├── webpack.config.js
├── /src
│   ├── index.js
│   └── index.css
└── /dist  # 构建后生成

第一步:初始化项目

mkdir my-webpack-starter
cd my-webpack-starter
npm init -y

安装 Webpack 相关依赖:

npm install webpack webpack-cli webpack-dev-server --save-dev

第二步:准备一个简单入口文件

src/index.js

import './index.css';

const root = document.getElementById('app');
root.innerText = 'Hello Webpack Starter!';

src/index.css

body {
  margin: 0;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

#app {
  padding: 24px;
  font-size: 20px;
}

public/index.html(作为模板):

复杂表单实战:我在项目里踩过的 Vue 坑

表单是前端最常见、也最容易写成一坨的东西。简单表单随便放几个 v-model 都能跑,一旦字段多起来、存在动态联动、跨步校验,代码就开始失控。

这篇文章基于我做过的一个"几十个字段 + 多步 + 条件显示"的表单项目,总结一些在 Vue 里踩过的坑和解决方案。


需求背景:一个略显「超标」的表单

当时的表单大概长这样:

  • 3 个步骤:基础信息 → 业务信息 → 确认提交
  • 每步有 8~15 个字段
  • 部分字段需要「根据上一步选择动态显示或隐藏」
  • 有跨字段校验(比如:结束日期 ≥ 开始日期)
  • 需要支持「草稿保存」+「再次打开自动回填」

如果一股脑全写在一个组件里,大概会出现:

  • data 里一个巨大的 form = { ... }
  • 各种 watch 做联动逻辑。
  • methods 里各种 validateXXX,互相依赖。
  • 维护成本爆炸。

核心思路:拆数据结构 + 拆 UI + 拆校验

1. 数据结构要先定清楚

我一般会在一开始,就写出一个完整的 formModel

const defaultForm = () => ({
  basic: {
    name: '',
    idNo: '',
    phone: '',
  },
  business: {
    type: '',
    amount: null,
    startDate: '',
    endDate: '',
  },
  extra: {
    agree: false,
    comment: '',
  },
});

在组件里: