Skip to content

Chrome 80 兼容约束 ​

ClassIntra 必须兼容 Chrome 80+(教育终端常见版本)。 本文档列出禁用语法、已 polyfill 的 API、替代方案和检查方法。

禁用语法 ​

ES2020+ 语法(Chrome 80 不支持) ​

语法引入版本Chrome 80 支持替代方案
?.(可选链)ES2020❌ Chrome 80+ 才支持显式 && 判断
??(空值合并)ES2020❌ Chrome 80+ 才支持|| 配合显式 null 检查
??=(逻辑空赋值)ES2021❌显式赋值
||=(逻辑或赋值)ES2021❌显式赋值
&&=(逻辑与赋值)ES2021❌显式赋值

ES2022+ 语法(Chrome 80 不支持) ​

语法引入版本Chrome 80 支持替代方案
class 字段声明ES2022❌构造函数中赋值
私有字段 #fieldES2022❌命名约定 _field
static 块ES2022❌静态方法
顶层 awaitES2022❌IIFE 或 module.exports 包装

项目代码风格约定(与 Chrome 80 无关,但项目要求遵守) ​

语法项目约定原因替代方案
class 语法❌ 禁用与现有代码风格不一致构造函数 + prototype
let / const❌ 禁用项目约定使用 varvar
模板字符串 `❌ 禁用与现有代码风格不一致字符串拼接 '...' + var
箭头函数 =>❌ 禁用与现有代码风格不一致function
import() 动态导入⚠️ 慎用Vite 构建会处理,但运行时 Chrome 80 不支持使用 Vite 的静态 import

后端代码 ​

后端运行在 Node.js(不受 Chrome 80 约束),但同样遵守项目代码风格约定(var / 单引号 / function / 2 空格缩进)。

例外:可以安全使用 Node.js 内置的现代 API(如 crypto.timingSafeEqual / fetch(Node 18+))。

已 Polyfill 的 API ​

client/src/main.js 在入口处 polyfill 了以下 API:

Object.hasOwn (ES2022, Chrome 93+) ​

javascript
// polyfill 实现
if (!Object.hasOwn) {
  Object.hasOwn = function(obj, prop) {
    return Object.prototype.hasOwnProperty.call(obj, prop);
  };
}

使用方式:

javascript
// ✓ 可直接使用(polyfill 已生效)
if (Object.hasOwn(obj, 'key')) {
  // ...
}

// ✓ 也可用旧 API
if (Object.prototype.hasOwnProperty.call(obj, 'key')) {
  // ...
}

String.prototype.replaceAll (ES2021, Chrome 85+) ​

javascript
// polyfill 实现
if (!String.prototype.replaceAll) {
  String.prototype.replaceAll = function(search, replacement) {
    if (typeof search === 'string') {
      return String(this).split(search).join(replacement);
    }
    if (Object.prototype.toString.call(search) === '[object RegExp]') {
      if (!search.global) throw new TypeError('replaceAll must be called with a global RegExp');
      return String(this).replace(search, replacement);
    }
    return String(this).split(String(search)).join(replacement);
  };
}

使用方式:

javascript
// ✓ 可直接使用
var normalized = str.replaceAll('-', '_');

// ✓ 也可用旧 API
var normalized = str.split('-').join('_');
// 或
var normalized = str.replace(/-/g, '_');

Promise.any + AggregateError (ES2021, Chrome 85+) ​

javascript
// polyfill 实现
if (!Promise.any) {
  Promise.any = function(promises) {
    return new Promise(function(resolve, reject) {
      var errors = [];
      var remaining = 0;
      var list = Array.from(promises || []);
      if (list.length === 0) {
        reject(new AggregateErrorImpl([], 'All promises were rejected'));
        return;
      }
      remaining = list.length;
      for (var i = 0; i < list.length; i++) {
        (function(idx) {
          Promise.resolve(list[idx]).then(function(val) {
            resolve(val);
          }, function(err) {
            errors[idx] = err;
            remaining--;
            if (remaining === 0) {
              reject(new AggregateErrorImpl(errors, 'All promises were rejected'));
            }
          });
        })(i);
      }
    });
  };
}

使用方式:

javascript
// ✓ 可直接使用
Promise.any([
  fetch('/api/primary').then(function(r) { return r.json(); }),
  fetch('/api/fallback').then(function(r) { return r.json(); })
]).then(function(data) {
  // 第一个成功的响应
}).catch(function(aggregateError) {
  // 全部失败
  console.error(aggregateError.errors);  // 错误数组
});

Array.prototype.at (ES2022, Chrome 92+) ​

javascript
// polyfill 实现
if (!Array.prototype.at) {
  Array.prototype.at = function(index) {
    var len = this.length;
    var relativeIndex = index < 0 ? len + index : index;
    if (relativeIndex < 0 || relativeIndex >= len) return undefined;
    return this[relativeIndex];
  };
}

if (!String.prototype.at) {
  String.prototype.at = function(index) {
    var len = this.length;
    var relativeIndex = index < 0 ? len + index : index;
    if (relativeIndex < 0 || relativeIndex >= len) return undefined;
    return this.charAt(relativeIndex);
  };
}

使用方式:

javascript
// ✓ 可直接使用
var last = arr.at(-1);          // 等价于 arr[arr.length - 1]
var first = arr.at(0);          // 等价于 arr[0]
var char = 'hello'.at(-1);      // 'o'

未 Polyfill 的 API(需手动避免) ​

以下 API Chrome 80 不支持,且未 polyfill,必须用替代方案:

API引入版本替代方案
Array.prototype.flatES2019, Chrome 69+Array.prototype.reduce + concat,或 [].concat.apply([], arr)
Array.prototype.flatMapES2019, Chrome 69+arr.map(fn).reduce(function(a,b){return a.concat(b);},[])
Object.fromEntriesES2019, Chrome 73+reduce 手动构建
String.prototype.matchAllES2020, Chrome 73+RegExp.exec 循环
globalThisES2020, Chrome 71+window(浏览器)/ global(Node)
BigIntES2020, Chrome 67+一般场景用 Number,超大数用第三方库
Promise.allSettledES2020, Chrome 76+自行实现(如 outbound-dispatcher.js 中的降级)

注意:上述部分 API 在 Chrome 80 实际可用(如 flat / flatMap / Object.fromEntries),但为保险起见建议避免。

替代方案速查 ​

可选链 ?. ​

javascript
// ❌ 禁用
var name = user?.profile?.name;
var first = arr?.[0];

// ✓ 替代方案 1:&& 链
var name = user && user.profile && user.profile.name;

// ✓ 替代方案 2:try/catch(性能略差,但可读性好)
var name;
try { name = user.profile.name; } catch (e) { name = undefined; }

// ✓ 数组访问
var first = arr && arr[0];

空值合并 ?? ​

javascript
// ❌ 禁用
var x = a ?? b;

// ✓ 替代方案 1:显式 null/undefined 检查
var x = (a !== null && a !== undefined) ? a : b;

// ✓ 替代方案 2:|| (仅当 0/''/false 是有效值时不能用)
var x = a || b;
// 注意:a 为 0、''、false、NaN 时会取 b,与 ?? 行为不同!

逻辑赋值 ??= / ||= / &&= ​

javascript
// ❌ 禁用
obj.prop ??= defaultValue;
obj.count ||= 1;
obj.enabled &&= false;

// ✓ 替代方案
if (obj.prop === null || obj.prop === undefined) {
  obj.prop = defaultValue;
}

if (!obj.count) {
  obj.count = 1;
}

if (obj.enabled) {
  obj.enabled = false;
}

模板字符串 ​

javascript
// ❌ 禁用
var msg = `Hello ${name}, you have ${count} messages`;
var html = `
  <div>${content}</div>
`;

// ✓ 替代方案:字符串拼接
var msg = 'Hello ' + name + ', you have ' + count + ' messages';
var html = '<div>' + content + '</div>';

箭头函数 ​

javascript
// ❌ 禁用
var sum = (a, b) => a + b;
arr.map(x => x * 2);
setTimeout(() => console.log('hi'), 1000);

// ✓ 替代方案:function
var sum = function(a, b) { return a + b; };
arr.map(function(x) { return x * 2; });
setTimeout(function() { console.log('hi'); }, 1000);

class 语法 ​

javascript
// ❌ 禁用
class MyClass {
  constructor() { this.x = 1; }
  method() { return this.x; }
  static create() { return new MyClass(); }
}

// ✓ 替代方案:构造函数 + prototype
function MyClass() {
  this.x = 1;
}

MyClass.prototype.method = function() {
  return this.x;
};

MyClass.create = function() {
  return new MyClass();
};

let / const ​

javascript
// ❌ 禁用
let counter = 0;
const MAX = 100;

// ✓ 替代方案:var
var counter = 0;
var MAX = 100;

解构赋值 ​

解构赋值 Chrome 80 支持,可以使用。但部分场景需注意:

javascript
// ✓ 可用:解构
var { name, age } = user;
var [first, second] = arr;

// ✓ 可用:默认值
var { name = '匿名' } = user;

// ✓ 可用:重命名
var { name: userName } = user;

// ⚠️ 慎用:嵌套解构(Chrome 80 支持,但代码可读性差)
var { profile: { name } } = user;  // 建议拆分

展开运算符 ... ​

展开运算符 Chrome 80 支持,可以使用:

javascript
// ✓ 可用:数组展开
var newArr = [].concat(arr, [1, 2, 3]);
// 等价于 ES6: var newArr = [...arr, 1, 2, 3];

// ✓ 可用:对象展开(Chrome 60+)
var newObj = Object.assign({}, obj, { key: 'value' });
// 等价于 ES6: var newObj = { ...obj, key: 'value' };

// ✓ 可用:函数参数展开
fn.apply(null, args);
// 等价于 ES6: fn(...args);

项目约定:与现有代码风格一致,优先使用 Object.assign / concat / apply,但展开运算符也可接受。

async/await ​

async/await Chrome 80 支持,可以使用。但项目约定优先用 Promise 链:

javascript
// ✓ 项目偏好:Promise 链
function fetchData() {
  return api.get('/api/data')
    .then(function(res) {
      return res.data;
    })
    .catch(function(err) {
      console.error('失败:', err);
      throw err;
    });
}

// ✓ 也可接受:async/await(Chrome 80 支持)
async function fetchData() {
  try {
    var res = await api.get('/api/data');
    return res.data;
  } catch (err) {
    console.error('失败:', err);
    throw err;
  }
}

项目代码风格约定 ​

强制约定 ​

约定原因
var 不用 let/const与现有代码一致
单引号 '...'与现有代码一致
2 空格缩进与现有代码一致
Vue Options API项目使用 Vue 2.7,统一 Options API
function 不用箭头函数与现有代码一致
字符串拼接不用模板字符串与现有代码一致
构造函数 + prototype 不用 class与现有代码一致

推荐约定 ​

约定原因
模块级函数放在 export default 之前提升可读性
import X from 'path' 不用 require前端 ES Module
单例模式 getXxx()与核心层一致
文件名 kebab-case与现有代码一致
Vue 组件名 PascalCase与现有代码一致
CSS 类 kebab-case与现有代码一致

后端约定 ​

约定原因
require 不用 import后端 CommonJS
module.exports = {}同上
其他约定与前端一致-

检查方法 ​

diag compat 自动扫描(推荐) ​

手动 grep 容易漏、也容易误报(注释里的示例代码)。仓库内置了工具化扫描:

bash
node scripts/diag.js compat [app-name]   # 不带名字扫描全部 apps/ + market-apps/ 前端
node scripts/diag.js all                 # 全量校验,自动附带 compat
  • 扫描 .js/.vue/.css,自动跳过块注释与 // 行注释(防文档误报)
  • 分级输出 文件:行号 + 违规内容:语法类(?. ?? &&= \|\|= ??=)与 CSS 新特性(aspect-ratio / inset / dvh / :is())在直出型(market-apps)判 FAIL、构建型(apps)可被 esbuild 转译;structuredClone / replaceChildren / .at() / .findLast 等运行时 API 一律 FAIL;flex gap 直出 FAIL、构建型 WARN
  • FAIL 影响退出码(pre-commit 钩子同标准);node scripts/build-app.js <name> 构建前也会先跑

完整规则表见 CLI 工具 § 校验器 diag。

手动 grep 检查禁用语法 ​

bash
# 检查可选链
grep -rn '?\.' client/src/ shared/src/ --include='*.js' --include='*.vue'

# 检查空值合并
grep -rn '??' client/src/ shared/src/ --include='*.js' --include='*.vue'

# 检查模板字符串
grep -rn '`' client/src/ shared/src/ --include='*.js' --include='*.vue'

# 检查箭头函数
grep -rn '=>' client/src/ shared/src/ --include='*.js' --include='*.vue'

# 检查 class 语法
grep -rn '^class \| class [A-Z]' client/src/ shared/src/ --include='*.js' --include='*.vue'

# 检查 let / const
grep -rn '^\s*let \|^\s*const ' client/src/ shared/src/ --include='*.js' --include='*.vue'

构建验证 ​

bash
# 前端构建(Vite 会自动转译,但不能保证运行时兼容)
cd client && npx vite build

# 实际测试:在 Chrome 80 中打开应用
# 1. 安装 Chrome 80(或使用 BrowserStack / Sauce Labs)
# 2. 打开应用 URL
# 3. F12 控制台检查是否有语法错误
# 4. 测试核心功能

Vite 构建配置 ​

client/vite.config.js 中应配置 target: 'chrome80':

javascript
export default {
  build: {
    target: 'chrome80'  // 确保 Vite 输出兼容 Chrome 80 的代码
  }
};

注意:即使 Vite 配置了 target,源代码中的禁用语法(如 ?.)依然会被保留。必须在编写代码时就避免使用。

常见错误 ​

SyntaxError: Unexpected token '.' ​

javascript
// 错误代码
var name = user?.name;
//           ^ Unexpected token '.'

// 修复
var name = user && user.name;

SyntaxError: Unexpected token '?' ​

javascript
// 错误代码
var x = a ?? b;
//         ^ Unexpected token '?'

// 修复
var x = (a !== null && a !== undefined) ? a : b;

SyntaxError: Unexpected token '`' ​

javascript
// 错误代码
var msg = `Hello ${name}`;
//         ^ Unexpected token '`'

// 修复
var msg = 'Hello ' + name;

SyntaxError: Unexpected token '=>' ​

javascript
// 错误代码
var fn = () => console.log('hi');
//          ^ Unexpected token '=>'

// 修复
var fn = function() { console.log('hi'); };

SyntaxError: Unexpected token 'class' ​

javascript
// 错误代码(在某些低版本浏览器中)
class MyClass {}
//     ^ Unexpected token 'class'

// 修复
function MyClass() {}
MyClass.prototype.method = function() {};

TypeError: Object.hasOwn is not a function ​

javascript
// 错误:polyfill 未加载或加载顺序错误
// 确保 main.js 入口的 polyfill 在所有其他代码之前执行

// 检查 main.js 是否包含 polyfill
// 应该在文件最顶部(import 之前)有 IIFE 包裹的 polyfill

TypeError: arr.at is not a function ​

javascript
// 错误:polyfill 未加载
// 或使用了非数组的 .at(如某些自定义对象)

// 修复:使用传统索引访问
var last = arr[arr.length - 1];

附录:Chrome 80 支持的 ES 特性 ​

以下是 Chrome 80 支持的主要 ES 特性,可安全使用:

特性ES 版本Chrome 版本
let / constES6Chrome 49+
箭头函数ES6Chrome 45+
模板字符串ES6Chrome 41+
解构赋值ES6Chrome 49+
类(class)ES6Chrome 49+
PromiseES6Chrome 32+
for...ofES6Chrome 38+
SymbolES6Chrome 38+
Map / Set / WeakMap / WeakSetES6Chrome 36+
默认参数ES6Chrome 49+
展开运算符 ...ES6Chrome 46+(数组)/ 60+(对象)
async/awaitES2017Chrome 55+
Object.entries / Object.valuesES2017Chrome 54+
Object.getOwnPropertyDescriptorsES2017Chrome 54+
String.prototype.padStart / padEndES2017Chrome 57+
SharedArrayBuffer / AtomicsES2017Chrome 68+
异步迭代 for await...ofES2018Chrome 63+
Promise.prototype.finallyES2018Chrome 63+
Rest/Spread 属性ES2018Chrome 60+
Array.prototype.flat / flatMapES2019Chrome 69+
Object.fromEntriesES2019Chrome 73+
String.prototype.matchAllES2020Chrome 73+
globalThisES2020Chrome 71+
BigIntES2020Chrome 67+
Promise.allSettledES2020Chrome 76+

注意:虽然上述特性 Chrome 80 支持,但项目代码风格约定仍然禁用 let / const / 箭头函数 / 模板字符串 / class。请遵守项目约定。