普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月21日首页

追觅科技成央视春晚智能科技生态战略合作伙伴

2026年1月21日 22:07
1月21日,追觅科技正式成为中央广播电视总台2026年春节联欢晚会智能科技生态战略合作伙伴。据了解,追觅科技致力于将尖端科技用于生活普惠,构建起覆盖智能出行、智能清洁、智能家电、智能厨电、智能影音及户外生活等在内的全场景高端智能生活产品生态,为全球超4200万家庭、上亿用户构建全场景智能科技生态。

恒帅股份:实控人及其一致行动人拟合计减持公司不超2.83%股份

2026年1月21日 20:36
36氪获悉,恒帅股份公告,公司实控人俞国梅计划以集中竞价和大宗交易方式减持公司股份不超过242万股(占公司总股本的2.16%);其一致行动人宁波玉米计划以集中竞价交易方式减持公司股份不超过75.48万股(占公司总股本的0.67%)。上述减持主体合计减持股份不超2.83%。

沙特人工智能企业Humain募资最高12亿美元

2026年1月21日 20:29
沙特阿拉伯国家基础设施基金与该国人工智能企业Humain于周三宣布达成一项最高12亿美元的融资协议,旨在助力沙特本土人工智能及数字基础设施的扩张建设。一份声明显示,该协议就建设最高250兆瓦算力的人工智能数据中心制定了不具约束力的融资条款,以服务Humain的客户,相关合作于瑞士达沃斯正式对外公布。(新浪财经)

红松CEO李乔:红松社区愿意向新东方开放所有社区资源

2026年1月21日 20:24
36氪获悉,中国银发文娱赛道领军企业红松创始人、CEO李乔表示,欢迎新东方加入银发文娱赛道,红松社区愿意向新东方开放所有社区资源。有媒体报道,“北京新东方退休俱乐部”账号近日在社交平台上线,其主要面向北京50-75岁退休人群推出兴趣课程与本地社交活动。

苏州“十五五”规划建议:推进人工智能驱动的新型科研范式、技术研发、工程实现、产品落地一体化协同发展

2026年1月21日 20:20
36氪获悉,中共苏州市委关于制定苏州市国民经济和社会发展第十五个五年规划的建议发布,其中提到,高水平建设“人工智能+”城市。深入推进数字苏州建设,抢抓国家新一代人工智能创新发展试验区和人工智能赋能新型工业化先导区建设机遇,全面实施“人工智能+”行动,建设国家人工智能应用中试基地(制造领域),以人工智能赋能制造业全面转型升级。推进人工智能驱动的新型科研范式、技术研发、工程实现、产品落地一体化协同发展,积极部署人工智能芯片、量子等创新应用技术,研发推广垂类模型。构建人工智能“芯片—算法—平台—应用”全链条产业生态,培育人工智能优势特色产业集群。

苏州“十五五”规划建议:推动具身智能等产业加快形成新的经济增长点

2026年1月21日 20:13
36氪获悉,中共苏州市委关于制定苏州市国民经济和社会发展第十五个五年规划的建议发布,其中提到,培育壮大未来产业。主动谋划未来制造、未来信息、未来材料、未来能源、未来空间、未来健康等产业。探索多元技术路线、典型应用场景、可行商业模式,推动具身智能、生物制造、细胞和基因技术、量子科技、氢能和核聚变能、脑机接口、前沿新材料、原子级制造等产业加快形成新的经济增长点。大力发展创业投资,推动形成未来产业多元投入、风险共担的发展格局。争创国家级未来产业先导区。

前端ESLint 和 Babel对比

2026年1月21日 20:05

ESLint 和 Babel 虽然都基于 AST(抽象语法树)工作,但它们的设计目的、工作流和 API 设计有着本质的区别。

  1. ESLint 插件实战:开发一个 eslint-plugin-clean-arch,用于强制执行“整洁架构”的依赖原则(例如:禁止 UI 层直接导入 DAO 层,必须经过 Service 层)。
  2. Babel 插件实战:开发一个 babel-plugin-auto-try-catch,用于在编译时自动给 async/await 函数包裹 try-catch 块,并上报错误信息,避免手动写大量重复代码。

第一部分:核心差异概览

在进入代码之前,先通过表格建立核心认知:

特性 ESLint 插件 Babel 插件
核心目标 代码质量检查与风格统一(Linting) 代码转换与编译(Transpiling)
输出结果 报告错误/警告,或进行源码级的字符串替换(Fix) 生成全新的、兼容性更好的 JavaScript 代码
AST 标准 ESTree (使用 espree 解析) Babel AST (基于 ESTree 但有细微差异,如 Literal 分类)
遍历方式 扁平化的选择器遍历 (Selectors) 访问者模式 (Visitor Pattern)
修改能力 弱。主要通过 fixer 提供文本范围替换,必须保持 AST 有效性比较难 强。可以随意增删改查节点,生成完全不同的代码结构
运行时机 开发时(IDE提示)、提交时(Husky)、CI/CD 阶段 构建打包阶段(Webpack/Vite/Rollup 加载器中)

第二部分:ESLint 自定义插件实战 (深度代码)

场景描述

在大型项目中,我们需要控制模块间的依赖关系。假设项目结构如下:

  • src/views (UI层)
  • src/services (业务逻辑层)
  • src/api (数据访问层)

规则src/views 下的文件,禁止直接 import 来自 src/api 的文件,必须通过 src/services 调用。

1. 插件入口结构

通常定义在 index.js 中。

/**
 * @fileoverview eslint-plugin-clean-arch
 * 强制执行项目架构分层依赖规则的 ESLint 插件
 */
'use strict';

// 导入我们即将编写的规则定义
const restrictLayerImports = require('./rules/restrict-layer-imports');

// 插件主入口
module.exports = {
  // 插件元数据
  meta: {
    name: 'eslint-plugin-clean-arch',
    version: '1.0.0'
  },
  // 暴露配置预设(用户可以直接 extends: ['plugin:clean-arch/recommended'])
  configs: {
    recommended: {
      plugins: ['clean-arch'],
      rules: {
        'clean-arch/restrict-layer-imports': 'error'
      }
    }
  },
  // 规则定义集合
  rules: {
    'restrict-layer-imports': restrictLayerImports
  },
  // 处理器(可选,用于处理非 JS 文件,如 .vue 中的 script)
  processors: {
    // 这里简单示意,通常 vue-eslint-parser 已经处理了
  }
};

2. 规则实现核心 (rules/restrict-layer-imports.js)

这是最核心的部分,包含了 AST 分析逻辑。

/**
 * @fileoverview 禁止跨层级直接调用
 */
'use strict';

const path = require('path');

// 辅助函数:标准化路径分隔符,兼容 Windows
function normalizePath(filePath) {
  return filePath.split(path.sep).join('/');
}

// 辅助函数:判断文件属于哪个层级
function getLayer(filePath) {
  const normalized = normalizePath(filePath);
  if (normalized.includes('/src/views/')) return 'views';
  if (normalized.includes('/src/services/')) return 'services';
  if (normalized.includes('/src/api/')) return 'api';
  return 'other';
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: 'problem', // problem | suggestion | layout
    docs: {
      description: 'Enforce strict layer dependency rules: Views -> Services -> API',
      category: 'Architecture',
      recommended: true,
      url: 'https://my-company.wiki/arch-rules'
    },
    fixable: null, // 本规则不提供自动修复,因为架构调整需要人工介入
    // 定义错误消息模板
    messages: {
      restrictedImport: '架构违规: "{{currentLayer}}" 层禁止直接引入 "{{targetLayer}}" 层模块。请通过 Service 层中转。',
      invalidPath: '无法解析的导入路径: {{importPath}}'
    },
    // 规则配置 Schema
    schema: [
      {
        type: 'object',
        properties: {
          // 允许用户自定义层级映射
          layers: {
            type: 'object'
          }
        },
        additionalProperties: false
      }
    ]
  },

  /**
   * create 方法返回一个对象,该对象的方法名为 AST 选择器
   * ESLint 遍历 AST 时会回调这些方法
   * @param {import('eslint').Rule.RuleContext} context
   */
  create(context) {
    // 获取当前正在被 Lint 的文件名
    const currentFilename = context.getFilename();
    const currentLayer = getLayer(currentFilename);

    // 如果当前文件不在受控层级中,直接忽略
    if (currentLayer === 'other') {
      return {};
    }

    // 定义层级依赖约束表
    // Key: 当前层级, Value: 禁止引入的层级集合
    const RESTRICTED_MAP = {
      'views': ['api'], // View 层禁止引入 API 层
      'services': [],   // Service 层可以引入 API
      'api': ['views', 'services'] // API 层通常是底层的,不应反向依赖
    };

    /**
     * 核心校验逻辑
     * @param {ASTNode} node - ImportDeclaration 节点
     */
    function verifyImport(node) {
      // 获取 import 的路径值,例如: import x from '@/api/user' 中的 '@/api/user'
      const importPath = node.source.value;

      // 忽略第三方库 (通常不以 . / @ 开头,或者是 node_modules)
      // 这里简单判断:如果不是相对路径也不是别名路径,认为是 npm 包
      if (!importPath.startsWith('.') && !importPath.startsWith('/') && !importPath.startsWith('@')) {
        return;
      }

      // 尝试解析导入路径对应的实际层级
      // 注意:在 ESLint 规则中做完整的文件系统解析比较重,
      // 通常我们会根据字符串特征判断,或者依赖 resolver
      let targetLayer = 'other';
      
      if (importPath.includes('/api/') || importPath.includes('@/api/')) {
        targetLayer = 'api';
      } else if (importPath.includes('/services/') || importPath.includes('@/services/')) {
        targetLayer = 'services';
      } else if (importPath.includes('/views/') || importPath.includes('@/views/')) {
        targetLayer = 'views';
      }

      // 检查是否违规
      const forbiddenLayers = RESTRICTED_MAP[currentLayer] || [];
      
      if (forbiddenLayers.includes(targetLayer)) {
        context.report({
          node: node.source, // 错误红线标在路径字符串上
          messageId: 'restrictedImport', // 使用 meta.messages 中定义的 ID
          data: {
            currentLayer: currentLayer,
            targetLayer: targetLayer
          }
        });
      }
    }

    return {
      // 监听 ES6 Import 语句
      // 例如: import { getUser } from '@/api/user';
      ImportDeclaration(node) {
        verifyImport(node);
      },

      // 监听动态 Import
      // 例如: const user = await import('@/api/user');
      ImportExpression(node) {
        // 动态 import 的 source 就是调用的参数
        verifyImport(node);
      },

      // 监听 CommonJS require (如果项目混用)
      // 例如: const api = require('@/api/user');
      CallExpression(node) {
        if (
          node.callee.name === 'require' &&
          node.arguments.length > 0 &&
          node.arguments[0].type === 'Literal'
        ) {
          // 构造成类似的结构以便复用 verifyImport
          const mockNode = {
            source: node.arguments[0]
          };
          verifyImport(mockNode);
        }
      }
    };
  }
};

3. 单元测试 (tests/rules/restrict-layer-imports.test.js)

ESLint 提供了 RuleTester 工具,非常方便进行 TDD 开发。

'use strict';

const rule = require('../../rules/restrict-layer-imports');
const { RuleTester } = require('eslint');

const ruleTester = new RuleTester({
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module'
  }
});

// 定义测试用例
ruleTester.run('restrict-layer-imports', rule, {
  // 1. 合法代码测试 (Valid)
  valid: [
    {
      // Service 层调用 API 层 -> 合法
      code: "import { getUser } from '@/api/user';",
      filename: '/Users/project/src/services/userService.js'
    },
    {
      // View 层调用 Service 层 -> 合法
      code: "import { getUserService } from '@/services/userService';",
      filename: '/Users/project/src/views/UserDetail.vue'
    },
    {
      // 引入第三方库 -> 合法
      code: "import axios from 'axios';",
      filename: '/Users/project/src/views/UserDetail.vue'
    },
    {
      // 相对路径引用同层级文件 -> 合法
      code: "import Header from './Header';",
      filename: '/Users/project/src/views/Footer.vue'
    }
  ],

  // 2. 违规代码测试 (Invalid)
  invalid: [
    {
      // View 层直接调用 API 层 -> 报错
      code: "import { getUser } from '@/api/user';",
      filename: '/Users/project/src/views/UserDetail.vue',
      errors: [
        {
          message: '架构违规: "views" 层禁止直接引入 "api" 层模块。请通过 Service 层中转。',
          type: 'Literal' // 报错节点类型
        }
      ]
    },
    {
      // API 层反向依赖 Views 层 -> 报错
      code: "import router from '@/views/router';",
      filename: '/Users/project/src/api/http.js',
      errors: [
        {
          message: '架构违规: "api" 层禁止直接引入 "views" 层模块。请通过 Service 层中转。'
        }
      ]
    },
    {
      // 动态 Import 也要拦截
      code: "const api = import('@/api/user');",
      filename: '/Users/project/src/views/Home.vue',
      parserOptions: { ecmaVersion: 2020 },
      errors: [{ messageId: 'restrictedImport' }]
    }
  ]
});

第三部分:Babel 自定义插件实战 (深度代码)

场景描述

前端开发中,异步操作如果不加 try-catch,一旦报错可能导致页面白屏。手动给每个 awaittry-catch 很繁琐且代码臃肿。 目标:编写一个 Babel 插件,自动识别 async 函数中的 await 语句,如果它没有被 try-catch 包裹,则自动包裹,并注入错误上报逻辑。

转换前

async function fetchData() {
  const res = await api.getData();
  console.log(res);
}

转换后

async function fetchData() {
  try {
    const res = await api.getData();
    console.log(res);
  } catch (e) {
    console.error('Auto Captured Error:', e);
    // window.reportError(e); // 可以在插件配置中传入上报函数名
  }
}

1. Babel 插件基础结构

Babel 插件导出一个函数,返回一个包含 visitor 属性的对象。

// babel-plugin-auto-try-catch.js

/**
 * Babel Types 库提供了用于构建、验证和转换 AST 节点的工具方法
 * @param {import('@babel/core')} babel
 */
module.exports = function(babel) {
  const { types: t, template } = babel;

  return {
    name: 'babel-plugin-auto-try-catch',
    // visitor 是访问者模式的核心
    visitor: {
      // 我们关注 FunctionDeclaration, FunctionExpression, ArrowFunctionExpression
      // 可以合并为一个选择器 'Function'
      Function(path, state) {
        // 1. 如果函数不是 async 的,跳过
        if (!path.node.async) {
          return;
        }

        // 2. 如果函数体已经是空的,跳过
        if (path.node.body.body.length === 0) {
          return;
        }

        // 3. 检查函数体是否已经被 try-catch 包裹
        // 获取函数体的第一条语句
        const firstStatement = path.node.body.body[0];
        // 如果只有一条语句且是 TryStatement,说明已经处理过或用户手动写了,跳过
        if (path.node.body.body.length === 1 && t.isTryStatement(firstStatement)) {
          return;
        }

        // 4. 获取用户配置的排除项 (例如排除某些文件或函数名)
        const exclude = state.opts.exclude || [];
        // 获取当前处理的文件路径
        const filename = state.file.opts.filename || 'unknown';
        // 简单的排除逻辑示例
        if (exclude.some(pattern => filename.includes(pattern))) {
          return;
        }
        
        // 5. 开始执行转换
        // 核心逻辑:将函数体原来的内容,塞入 try 块中
        
        // 步骤 A: 生成 catch 子句的 error 参数节点 (identifier)
        // 使用 path.scope.generateUidIdentifier 防止变量名冲突 (例如防止用户原代码里已经有个变量叫 err)
        const errorParam = path.scope.generateUidIdentifier('err');

        // 步骤 B: 构建 catch 块的内容
        // 这里我们可以根据配置,生成 console.error 或 reportError 调用
        const reporterName = state.opts.reporter || 'console.error';
        
        // 使用 babel template 快速构建 AST 节点,比手动 t.callExpression 更直观
        // %%err%% 是占位符,会被替换为上面生成的 errorParam
        const catchBodyTemplate = template.statement(`
          ${reporterName}('Async Error:', %%err%%);
        `);
        
        const catchBlockStatement = t.blockStatement([
          catchBodyTemplate({ err: errorParam })
        ]);

        // 步骤 C: 构建 catch 子句节点
        const catchClause = t.catchClause(
          errorParam,
          catchBlockStatement
        );

        // 步骤 D: 构建 try 语句节点
        // path.node.body 是 BlockStatement,包含 body 属性(语句数组)
        const originalBodyStatements = path.node.body.body;
        
        const tryStatement = t.tryStatement(
          t.blockStatement(originalBodyStatements), // try 块内容
          catchClause, // catch 块
          null // finally 块 (可选)
        );

        // 步骤 E: 替换原函数体
        // 注意:直接替换 body 可能会导致死循环(因为新生成的节点也包含函数体),
        // 但这里我们要替换的是 Function 的 body (BlockStatement) 的内容,
        // 或者直接替换 body 为包含 tryStatement 的新 BlockStatement。
        
        path.get('body').replaceWith(
          t.blockStatement([tryStatement])
        );

        // 标记该节点已被访问,避免递归处理死循环 (Babel 默认会重新访问新插入的节点)
        path.skip(); 
      }
    }
  };
};

2. 增强版 Babel 插件逻辑 (处理细节)

上面的版本比较粗暴(把整个函数体包起来)。但在实际中,我们可能只想包裹包含 await 的代码段,或者如果用户已经写了部分 try-catch 该怎么办?

下面是更精细的 AST 操作逻辑:

// 进阶工具函数:检查 BlockStatement 中是否包含 await 表达式
function hasAwaitExpression(path) {
  let hasAwait = false;
  // 使用 path.traverse 可以在当前路径下进行子遍历
  path.traverse({
    AwaitExpression(childPath) {
      // 必须确保 await 是属于当前函数的,而不是嵌套在内部其他 async 函数里的
      const parentFunction = childPath.getFunctionParent();
      if (parentFunction === path) {
        hasAwait = true;
        childPath.stop(); // 找到一个就停止
      }
    },
    // 防止遍历进入内部函数的陷阱
    Function(childPath) {
      childPath.skip();
    }
  });
  return hasAwait;
}

// 修改 visitor 部分
visitor: {
  Function(path, state) {
    if (!path.node.async) return;
    
    // 进阶优化:如果没有 await,其实不需要包裹 try-catch (虽然 async 函数报错会返回 reject promise,但这里假设只捕获 await 异常)
    if (!hasAwaitExpression(path)) {
      return;
    }

    // 处理 React/Vue 组件方法名排除
    const functionName = path.node.id ? path.node.id.name : '';
    if (['render', 'setup', 'componentDidCatch'].includes(functionName)) {
      return;
    }
    
    // ... 后续转换逻辑同上 ...
  }
}

3. Babel 插件单元测试

Babel 插件测试通常使用 babel-plugin-tester 或直接调用 @babel/coretransformSync

const babel = require('@babel/core');
const autoTryCatchPlugin = require('./babel-plugin-auto-try-catch');
const assert = require('assert');

// 辅助测试函数
function transform(code, options = {}) {
  const result = babel.transformSync(code, {
    plugins: [
      [autoTryCatchPlugin, options] // 加载插件并传入配置
    ],
    // 禁用 Babel 默认生成严格模式,减少干扰
    sourceType: 'script', 
    compact: false // 格式化输出代码
  });
  return result.code;
}

console.log('--- 开始测试 Babel 插件 ---');

// 测试用例 1: 普通 Async 函数转换
const code1 = `
async function getData() {
  const res = await api.get('/user');
  return res;
}
`;
const output1 = transform(code1);
console.log('[Case 1 Output]:\n', output1);
/*
预期输出:
async function getData() {
  try {
    const res = await api.get('/user');
    return res;
  } catch (_err) {
    console.error('Async Error:', _err);
  }
}
*/
assert.match(output1, /try \{/, 'Case 1 Failed: try block missing');
assert.match(output1, /catch \(_err\)/, 'Case 1 Failed: catch block missing');


// 测试用例 2: 箭头函数转换
const code2 = `
const doWork = async () => {
  await sleep(1000);
  console.log('done');
};
`;
const output2 = transform(code2);
console.log('[Case 2 Output]:\n', output2);
assert.match(output2, /try \{/, 'Case 2 Failed');


// 测试用例 3: 已经有 Try-Catch 的函数 (应跳过)
const code3 = `
async function safe() {
  try {
    await risky();
  } catch (e) {
    handle(e);
  }
}
`;
const output3 = transform(code3);
// 输出应该和输入几乎一样(除了格式化差异)
// 我们通过判断 catch 块的数量来验证没有重复插入
const catchCount = (output3.match(/catch/g) || []).length;
assert.strictEqual(catchCount, 1, 'Case 3 Failed: Should not add extra try-catch');


// 测试用例 4: 自定义 Reporter 配置
const code4 = `async function test() { await fn(); }`;
const output4 = transform(code4, { reporter: 'window.reportToSentry' });
console.log('[Case 4 Output]:\n', output4);
assert.match(output4, /window\.reportToSentry/, 'Case 4 Failed: Custom reporter not working');

console.log('--- 所有测试通过 ---');

第四部分:底层机制深度对比

这部分解释为什么代码要这么写,这对于理解 1000 行级别的复杂插件开发至关重要。

1. 遍历机制:Scope (作用域) 管理

这是 Babel 和 ESLint 插件开发中最难的部分。

  • ESLint:

    • context.getScope() 获取当前节点的作用域。
    • 主要用于查找变量定义(References)。例如:no-undef 规则就是通过遍历 Scope 中的 references 列表,看是否有未定义的变量。
    • ESLint 的 Scope 分析是静态只读的。你不能在 lint 过程中修改 Scope。
  • Babel:

    • path.scope 对象非常强大。
    • path.scope.generateUidIdentifier('name'):自动生成唯一变量名(如 _name, _name2),这在转换代码注入变量时必不可少(如上面 try-catch 中的 err)。
    • path.scope.push({ id: ... }):可以将变量声明提升到作用域顶部。
    • Binding:Babel 维护了极其详细的变量绑定信息。你可以通过 path.scope.bindings['x'] 找到变量 x 的所有引用位置(referencePaths)和赋值位置(constantViolations)。这使得做“死代码消除”或“常量折叠”成为可能。

2. 状态管理 (State)

  • ESLint:

    • 状态通常保存在闭包变量中,或者 create 函数的局部变量中。
    • 因为 ESLint 是按文件处理的,create 每次处理新文件都会重新执行,所以闭包变量是文件隔离的。
    • 如果需要跨文件信息(极其少见且不推荐,因为破坏缓存),需要用到全局单例。
  • Babel:

    • 状态通过 state 参数在 Visitor 方法间传递。
    • state.file 包含当前文件的元数据。
    • state.opts 包含用户在 .babelrc 传入的插件配置。
    • 可以在 pre()post() 钩子中初始化和清理状态。
// Babel 状态管理示例
module.exports = {
  pre(state) {
    this.cache = new Map(); // 初始化文件级缓存
  },
  visitor: {
    Identifier(path, state) {
      // this.cache 在这里可用
    }
  },
  post(state) {
    // 清理
  }
};

3. 节点构造与替换

  • ESLint Fixer:

    • 基于文本索引(Index)。
    • API: replaceText(node, 'newText'), insertTextAfter(node, ';').
    • 非常脆弱。如果你删除了一个逗号,可能导致后续的代码语法错误。ESLint 会尝试多次运行 fix 直到不再有变动,但它不保证生成的代码 AST 结构正确,只保证文本替换。
  • Babel Types:

    • 基于对象构建
    • API: t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2))
    • 非常健壮。只要符合 AST 规范,Babel Generator 就能生成合法的 JavaScript 代码(自动处理括号优先级、分号等)。

总结

  • 如果你的需求是 “阻止开发者提交烂代码” 或者 “统一团队的代码风格”,请选择 ESLint 插件。它的核心是 Context 和 Report。
  • 如果你的需求是 “减少重复的样板代码”“兼容低版本浏览器” 或者 “实现一种新的语法糖”,请选择 Babel 插件。它的核心是 Path、Visitor 和 Types Builder。

以上代码展示了从零构建一个架构级 ESLint 规则和一个编译级 Babel 转换插件的完整过程,涵盖了 AST 分析、上下文判断、节点构建和单元测试等核心环节。在实际工程中,这两者往往结合使用:Babel 负责把代码变样,ESLint 负责保证变样前的源码符合规范。

❌
❌