jifengg

jifengg 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 blog.woniufun.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

jifengg 赞了文章 · 1月8日

从一个优秀开源项目来谈前端架构

何为系统架构师?

  • 系统架构师是一个最终确认和评估系统需求,给出开发规范,搭建系统实现的核心构架,并澄清技术细节、扫清主要难点的技术人员。主要着眼于系统的“技术实现”。因此他/她应该是特定的开发平台、语言、工具的大师,对常见应用场景能给出最恰当的解决方案,同时要对所属的开发团队有足够的了解,能够评估自己的团队实现特定的功能需求需要的代价。 系统架构师负责设计系统整体架构,从需求到设计的每个细节都要考虑到,把握整个项目,使设计的项目尽量效率高,开发容易,维护方便,升级简单等
这是百度百科的答案

大多数人的问题

如何成为一名前端架构师?
  • 其实,前端架构师不应该是一个头衔,而应该是一个过程。我记得掘金上有人写过一篇文章:《我在一个小公司,我把我们公司前端给架构了》 , (我当时还看成《我把我们公司架构师给上了》)
  • 我面试过很多人,从小公司出来(我也是从一个很小很小的公司出来,现在也没在什么BATJ ),最大的问题在于,觉得自己不是leader,就没有想过如何去提升、优化项目,而是去研究一些花里胡哨的东西,却没有真正使用在项目中。(自然很少会有深度)
  • 在一个两至三人的前端团队小公司,你去不断优化、提升项目体验,更新迭代替换技术栈,那么你就是前端架构师

正式开始

我们从一个比较不错的项目入手,谈谈一个前端架构师要做什么
  • SpaceX-API
  • SpaceX-API 是什么?
  • SpaceX-API 是一个用于火箭、核心舱、太空舱、发射台和发射数据的开源 REST API(并且是使用Node.js编写,我们用这个项目借鉴无可厚非)
为了阅读的舒适度,我把下面的正文尽量口语化一点
先把代码搞下来
git clone https://github.com/r-spacex/SpaceX-API.git
  • 一个优秀的开源项目搞下来以后,怎么分析它?大部分时候,你应该先看它的目录结构以及依赖的第三方库(package.json文件)
找到package.json文件的几个关键点:
  • main字段(项目入口)
  • scripts字段(执行命令脚本)
  • dependenciesdevDependencies字段(项目的依赖,区分线上依赖和开发依赖,我本人是非常看中这个点,SpaceX-API也符合我的观念,严格的区分依赖按照)
 "main": "server.js",
   "scripts": {
    "test": "npm run lint && npm run check-dependencies && jest --silent --verbose",
    "start": "node server.js",
    "worker": "node jobs/worker.js",
    "lint": "eslint .",
    "check-dependencies": "npx depcheck --ignores=\"pino-pretty\""
  },
  • 通过上面可以看到,项目入口为server.js
  • 项目启动命令为npm run start
  • 几个主要的依赖:
    "koa": "^2.13.0",
    "koa-bodyparser": "^4.3.0",
    "koa-conditional-get": "^3.0.0",
    "koa-etag": "^4.0.0",
    "koa-helmet": "^6.0.0",
    "koa-pino-logger": "^3.0.0",
    "koa-router": "^10.0.0",
    "koa2-cors": "^2.0.6",
    "lodash": "^4.17.20",
    "moment-range": "^4.0.2",
    "moment-timezone": "^0.5.32",
    "mongoose": "^5.11.8",
    "mongoose-id": "^0.1.3",
    "mongoose-paginate-v2": "^1.3.12",
    "eslint": "^7.16.0",
    "eslint-config-airbnb-base": "^14.2.1",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-jest": "^24.1.3",
    "eslint-plugin-mongodb": "^1.0.0",
    "eslint-plugin-no-secrets": "^0.6.8",
    "eslint-plugin-security": "^1.4.0",
    "jest": "^26.6.3",
    "pino-pretty": "^4.3.0"
  • 都是一些通用主流库: 主要是koa框架,以及一些koa的一些中间件,monggose(连接使用mongoDB),eslint(代码质量检查)
这里强调一点,如果你的代码需要两人及以上维护,我就强烈建议你不要使用任何黑魔法,以及不使用非主流的库,除非你编写核心底层逻辑时候非用不可(这个时候应该只有你维护)
项目目录

  • 这是一套标准的REST API,严格分层
  • 几个重点目录 :

    • server.js 项目入口
    • app.js 入口文件
    • services 文件夹=>项目提供服务层
    • scripts 文件夹=>项目脚本
    • middleware 文件夹=>中间件
    • docs 文件夹=>文档存放
    • tests 文件夹=>单元测试代码存放
    • .dockerignore docker的忽略文件
    • Dockerfile 执行docker build命令读取配置的文件
    • .eslintrc eslint配置文件
    • jobs 文件夹=>我想应该是对应检查他们api服务的代码,里面都是准备的一些参数然后直接调服务

逐个分析

从项目依赖安装说起
  • 安装环境严格区分开发依赖和线上依赖,让阅读代码者一目了然哪些依赖是线上需要的
  "dependencies": {
    "blake3": "^2.1.4",
    "cheerio": "^1.0.0-rc.3",
    "cron": "^1.8.2",
    "fuzzball": "^1.3.0",
    "got": "^11.8.1",
    "ioredis": "^4.19.4",
    "koa": "^2.13.0",
    "koa-bodyparser": "^4.3.0",
    "koa-conditional-get": "^3.0.0",
    "koa-etag": "^4.0.0",
    "koa-helmet": "^6.0.0",
    "koa-pino-logger": "^3.0.0",
    "koa-router": "^10.0.0",
    "koa2-cors": "^2.0.6",
    "lodash": "^4.17.20",
    "moment-range": "^4.0.2",
    "moment-timezone": "^0.5.32",
    "mongoose": "^5.11.8",
    "mongoose-id": "^0.1.3",
    "mongoose-paginate-v2": "^1.3.12",
    "pino": "^6.8.0",
    "tle.js": "^4.2.8",
    "tough-cookie": "^4.0.0"
  },
  "devDependencies": {
    "eslint": "^7.16.0",
    "eslint-config-airbnb-base": "^14.2.1",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-jest": "^24.1.3",
    "eslint-plugin-mongodb": "^1.0.0",
    "eslint-plugin-no-secrets": "^0.6.8",
    "eslint-plugin-security": "^1.4.0",
    "jest": "^26.6.3",
    "pino-pretty": "^4.3.0"
  },
项目目录划分
  • 目录划分,严格分层
  • 通用,清晰简介明了,让人一看就懂
正式开始看代码
  • 入口文件,server.js开始
const http = require('http');
const mongoose = require('mongoose');
const { logger } = require('./middleware/logger');
const app = require('./app');

const PORT = process.env.PORT || 6673;
const SERVER = http.createServer(app.callback());

// Gracefully close Mongo connection
const gracefulShutdown = () => {
  mongoose.connection.close(false, () => {
    logger.info('Mongo closed');
    SERVER.close(() => {
      logger.info('Shutting down...');
      process.exit();
    });
  });
};

// Server start
SERVER.listen(PORT, '0.0.0.0', () => {
  logger.info(`Running on port: ${PORT}`);

  // Handle kill commands
  process.on('SIGTERM', gracefulShutdown);

  // Prevent dirty exit on code-fault crashes:
  process.on('uncaughtException', gracefulShutdown);

  // Prevent promise rejection exits
  process.on('unhandledRejection', gracefulShutdown);
});
  • 几个优秀的地方

    • 每个回调函数都会有声明功能注释
    • SERVER.listen的host参数也会传入,这里是为了避免产生不必要的麻烦。至于这个麻烦,我这就不解释了(一定要有能看到的默认值,而不是去靠猜)
    • 对于监听端口启动服务以后一些异常统一捕获,并且统一日志记录,process进程退出,防止出现僵死线程、端口占用等(因为node部署时候可能会用pm2等方式,在 Worker 线程中,process.exit()将停止当前线程而不是当前进程)
app.js入口文件
  • 这里是由koa提供基础服务
  • monggose负责连接mongoDB数据库
  • 若干中间件负责 跨域、日志、错误、数据处理等
const conditional = require('koa-conditional-get');
const etag = require('koa-etag');
const cors = require('koa2-cors');
const helmet = require('koa-helmet');
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const mongoose = require('mongoose');
const { requestLogger, logger } = require('./middleware/logger');
const { responseTime, errors } = require('./middleware');
const { v4 } = require('./services');

const app = new Koa();

mongoose.connect(process.env.SPACEX_MONGO, {
  useFindAndModify: false,
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useCreateIndex: true,
});

const db = mongoose.connection;

db.on('error', (err) => {
  logger.error(err);
});
db.once('connected', () => {
  logger.info('Mongo connected');
  app.emit('ready');
});
db.on('reconnected', () => {
  logger.info('Mongo re-connected');
});
db.on('disconnected', () => {
  logger.info('Mongo disconnected');
});

// disable console.errors for pino
app.silent = true;

// Error handler
app.use(errors);

app.use(conditional());

app.use(etag());

app.use(bodyParser());

// HTTP header security
app.use(helmet());

// Enable CORS for all routes
app.use(cors({
  origin: '*',
  allowMethods: ['GET', 'POST', 'PATCH', 'DELETE'],
  allowHeaders: ['Content-Type', 'Accept'],
  exposeHeaders: ['spacex-api-cache', 'spacex-api-response-time'],
}));

// Set header with API response time
app.use(responseTime);

// Request logging
app.use(requestLogger);

// V4 routes
app.use(v4.routes());

module.exports = app;
  • 逻辑清晰,自上而下,首先连接db数据库,挂载各种事件后,经由koa各种中间件,而后真正使用koa路由提供api服务(代码编写顺序,即代码运行后的业务逻辑,我们写前端的react等的时候,也提倡由生命周期运行顺序去编写组件代码,而不是先编写unmount生命周期,再编写mount),例如应该这样:
//组件挂载
componentDidmount(){

}
//组件需要更新时
shouldComponentUpdate(){

}
//组件将要卸载
componentWillUnmount(){

}
...
render(){}
router的代码,简介明了
const Router = require('koa-router');
const admin = require('./admin/routes');
const capsules = require('./capsules/routes');
const cores = require('./cores/routes');
const crew = require('./crew/routes');
const dragons = require('./dragons/routes');
const landpads = require('./landpads/routes');
const launches = require('./launches/routes');
const launchpads = require('./launchpads/routes');
const payloads = require('./payloads/routes');
const rockets = require('./rockets/routes');
const ships = require('./ships/routes');
const users = require('./users/routes');
const company = require('./company/routes');
const roadster = require('./roadster/routes');
const starlink = require('./starlink/routes');
const history = require('./history/routes');
const fairings = require('./fairings/routes');

const v4 = new Router({
  prefix: '/v4',
});

v4.use(admin.routes());
v4.use(capsules.routes());
v4.use(cores.routes());
v4.use(crew.routes());
v4.use(dragons.routes());
v4.use(landpads.routes());
v4.use(launches.routes());
v4.use(launchpads.routes());
v4.use(payloads.routes());
v4.use(rockets.routes());
v4.use(ships.routes());
v4.use(users.routes());
v4.use(company.routes());
v4.use(roadster.routes());
v4.use(starlink.routes());
v4.use(history.routes());
v4.use(fairings.routes());

module.exports = v4;
模块众多,找几个代表性的模块
  • admin模块
const Router = require('koa-router');
const { auth, authz, cache } = require('../../../middleware');

const router = new Router({
  prefix: '/admin',
});

// Clear redis cache
router.delete('/cache', auth, authz('cache:clear'), async (ctx) => {
  try {
    await cache.redis.flushall();
    ctx.status = 200;
  } catch (error) {
    ctx.throw(400, error.message);
  }
});

// Healthcheck
router.get('/health', async (ctx) => {
  ctx.status = 200;
});

module.exports = router;
  • 分析代码
  • 这是一套标准的restful API , 提供的/admin/cache接口,请求方式为delete,请求这个接口,首先要经过authauthz两个中间件处理
这里补充一个小细节
  • 一个用户访问一套系统,有两种状态,未登陆和已登陆,如果你未登陆去执行一些操作,后端应该返回401。但是登录后,你只能做你权限内的事情,例如你只是一个打工人,你说你要关闭这个公司,那么对不起,你的状态码此时应该是403
回到admin
  • 此刻的你,想要清空这个缓存,调用/admin/cache接口,那么首先要经过auth中间件判断你是否有登录
/**
 * Authentication middleware
 */
module.exports = async (ctx, next) => {
  const key = ctx.request.headers['spacex-key'];
  if (key) {
    const user = await db.collection('users').findOne({ key });
    if (user?.key === key) {
      ctx.state.roles = user.roles;
      await next();
      return;
    }
  }
  ctx.status = 401;
  ctx.body = 'https://youtu.be/RfiQYRn7fBg';
};
  • 如果没有登录过,那么意味着你没有权限,此时为401状态码,你应该去登录.如果登录过,那么应该前往下一个中间件authz. (所以redux的中间件源码是多么重要.它可以说贯穿了我们整个前端生涯,我以前些过它的分析,有兴趣的可以翻一翻公众号)
/**
 * Authorization middleware
 *
 * @param   {String}   role   Role for protected route
 * @returns {void}
 */
module.exports = (role) => async (ctx, next) => {
  const { roles } = ctx.state;
  const allowed = roles.includes(role);
  if (allowed) {
    await next();
    return;
  }
  ctx.status = 403;
};
  • authz这里会根据你传入的操作类型(这里是'cache:clear'),看你的对应所有权限roles里面是否包含传入的操作类型role.如果没有,就返回403,如果有,就继续下一个中间件 - 即真正的/admin/cache接口
// Clear redis cache
router.delete('/cache', auth, authz('cache:clear'), async (ctx) => {
  try {
    await cache.redis.flushall();
    ctx.status = 200;
  } catch (error) {
    ctx.throw(400, error.message);
  }
});
  • 此时此刻,使用try catch包裹逻辑代码,当redis清除所有缓存成功即会返回状态码400,如果报错,就会抛出错误码和原因.接由洋葱圈外层的error中间件处理
/**
 * Error handler middleware
 *
 * @param   {Object}    ctx       Koa context
 * @param   {function}  next      Koa next function
 * @returns {void}
 */
module.exports = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    if (err?.kind === 'ObjectId') {
      err.status = 404;
    } else {
      ctx.status = err.status || 500;
      ctx.body = err.message;
    }
  }
};
  • 这样只要任意的server层内部出现异常,只要抛出,就会被error中间件处理,直接返回状态码和错误信息. 如果没有传入状态码,那么默认是500(所以我之前说过,代码要稳定,一定要有显示的指定默认值,要关注代码异常的逻辑,例如前端setLoading,请求失败也要取消loading,不然用户就没法重试了,有可能这一瞬间只是用户网络出错呢)
补一张koa洋葱圈的图

再接下来看其他的services
  • 现在,都非常轻松就能理解了
// Get one history event
router.get('/:id', cache(300), async (ctx) => {
  const result = await History.findById(ctx.params.id);
  if (!result) {
    ctx.throw(404);
  }
  ctx.status = 200;
  ctx.body = result;
});

// Query history events
router.post('/query', cache(300), async (ctx) => {
  const { query = {}, options = {} } = ctx.request.body;
  try {
    const result = await History.paginate(query, options);
    ctx.status = 200;
    ctx.body = result;
  } catch (error) {
    ctx.throw(400, error.message);
  }
});
通过这个项目,我们能学到什么
  • 一个能上天的项目,必然是非常稳定、高可用的,我们首先要学习它的优秀点:用最简单的技术加上最简单的实现方式,让人一眼就能看懂它的代码和分层
  • 再者:简洁的注释是必要的
  • 从业务角度去抽象公共层,例如鉴权、错误处理、日志等为公共模块(中间件,前端可能是一个工具函数或组件)
  • 多考虑错误异常的处理,前端也是如此,js大多错误发生来源于a.b.c这种代码(如果a.bundefined那么就会报错了)
  • 显示的指定默认值,不让代码阅读者去猜测
  • 目录分区必定要简洁明了,分层清晰,易于维护和拓展
成为一个优秀前端架构师的几个点
  • 原生JavaScript、CSS、HTML基础扎实(系统学习过)
  • 原生Node.js基础扎实(系统学习过),Node.js不仅提供服务,更多的是用于制作工具,以及现在serverless场景也会用到,还有ssr
  • 熟悉框架和类库原理,能手写简易的常用类库,例如promise redux 等
  • 数据结构基础扎实,了解常用、常见算法
  • linux基础扎实(做工具,搭环境,编写构建脚本等有会用到)
  • 熟悉TCP和http等通信协议
  • 熟悉操作系统linux Mac windows iOS 安卓等(在跨平台产品时候会遇到)
  • 会使用docker(部署相关)
  • 会一些c++最佳(在addon场景等,再者Node.js和JavaScript本质上是基于C++
  • 懂基本数据库、redis、nginxs操作,像跨平台产品,基本前端都会有个sqlite之类的,像如果是node自身提供服务,数据库和redis一般少不了
  • 再者是要多阅读优秀的开源项目源码,不用太多,但是一定要精
以上是我的感悟,后面我会在评论中补充,也欢迎大家在评论中补充探讨!
写在最后
  • 这是我今年的第一篇原创文章,也是[前端巅峰]公众号开通留言功能后的第一篇文章
  • 如果感觉我写得不错,帮我点个在看/赞转发支持我一下,可以的话,来个星标关注吧!
查看原文

赞 37 收藏 23 评论 7

jifengg 赞了文章 · 2020-11-26

种草ECMAScript2021新特性

观感度:🌟🌟🌟🌟🌟

口味:赛螃蟹

烹饪时间:5min

本文已收录在前端食堂同名仓库Github github.com/Geekhyt,欢迎光临食堂,如果觉得酒菜还算可口,赏个 Star 对食堂老板来说是莫大的鼓励。

ECMAScript

ECMAScriptEcma International 颁布的一部语言标准,编号为 262,又称为 ECMA-262

Ecma International 则是一个制定信息和通讯技术方面的国际标准的组织,前身是欧洲计算机制造商协会(European Computer Manufacturers Association),随着计算机的国际化,机构名称改为其英文单词首字母缩写。

ECMAScriptEMCA InternationalTC39Technical Committee 39)技术委员会编写。

TC39 会将编写完成的 ECMAScript 标准文档提交给 Ecma International,并由其正式发布。

从 2015 年开始,ECMAScript 每年都会发布一个正式版,并在标题中写上年份,比如「ECMAScript® 2020 Language Specification, 11th edition」,可简称为ES2020ES11

五个流程阶段

如果想要新增或是改写规范,一般要经历5个阶段,如TC39 Process中所示:

  • Strawperson
  • Proposal
  • Draft
  • Candidate
  • Finished

经历过这5个阶段,进入 Finished 状态的修改才会被列入正式版的规范。

ECMAScript2021

https://github.com/tc39/proposals/blob/master/finished-proposals.md

了解了 ECMAScript,下面就进入正文,让我们来看看已经确定的 ECMAScript2021 的新特性吧。

image

1.String.prototype.replaceAll

https://github.com/tc39/proposal-string-replaceall

先来回顾下 String.prototype.replace 的用法:

const str = 'Stay Hungry. Stay Foolish.'
const newStr = str.replace('Stay', 'Always')
console.log(newStr) // Always Hungry. Stay Foolish.

如果我们这样写,只有第一个匹配的会被替换。

想要做到全部替换就需要使用正则表达式。

const str = 'Stay Hungry. Stay Foolish.'
const newStr = str.replace(/Stay/g, 'Always')
console.log(newStr) // Always Hungry. Always Foolish.

不过在使用正则的时候,如果需求是匹配 + 等符号时,还需要进行转义。如:

/\+/g

聪明的你也许会想到另外一种方案:使用 split + join 的方式

这里借用下官方的例子:

const queryString = 'q=query+string+parameters';
const withSpaces = queryString.split('+').join(' ');
// q=query string parameters

但这样做也是有性能开销的,加上这种操作十分常见。于是就诞生了 String.prototype.replaceAll 这个 API,我们可以更加方便的来进行操作。

const str = 'Stay Hungry. Stay Foolish.'
const newStr = str.replaceAll('Stay', 'Always')
console.log(newStr) // Always Hungry. Always Foolish.
String.prototype.replaceAll(searchValue, replaceValue)

注意:当 searchValue 是非全局正则表达式时,replaceAll 会引发异常。如果 searchValue 是全局正则表达式时,replaceAllreplace 行为是一致的。

2.Promise.any

https://github.com/tc39/proposal-promise-any

const p = Promise.all([p1, p2, p3]);
  • Promise.all (ES2015) 只有当传入的每个 Promise 实例(p1,p2,p3)的状态都变成 fulfilled 时,p 才 fulfilled,只要(p1,p2,p3)有一个被 rejected,p 的状态就变成 rejected。
  • Promise.race (ES2015) 当传入的 Promise 实例(p1,p2,p3)中有一个率先改变状态,那么 p 的状态就跟着改变,也就是说返回最先改变的 Promise 实例的返回值。
  • Promise.allSettled (ES2020) 只有等到所有传入的 Promise 实例(p1,p2,p3)都返回结果,不管是 fulfilled 还是 rejected,包装实例才会结束。
  • Promise.any (ES2021) 当其中任何一个 Promise 完成(fulfilled)时,就返回那个已经有完成值的 Promise。如果所有的 Promise 都拒绝 (rejected), 那么返回一个拒绝的 Promise。

对比记忆

  • 我们可以把 Promise.any() 理解成 Promise.all() 的反向操作。

image

致敬韦神!

  • Promise.any()Promise.race() 方法很像,有一个不同点是:前者不会因为某个 Promise 变成 rejected 状态而结束。

想要了解更多细节可以看阮老师的ECMAScript 6 入门

Promise.any(promises).then(
    (first) => {
        // 任何一个Promise完成
    },
    (error) => {
        // 所有的 Promise都拒绝了
    }
)

any 名字的由来

any 顾名思义,不仅清楚的描述了它的作用,而且在提供此功能的第三方库中都是这样命名的,用过的同学们一定觉得很亲切。

可预见的作用

官方提供了一个例子,可以应用 Promise.any() 检查哪个站点访问最快。

Promise.any([
  fetch('https://v8.dev/').then(() => 'home'),
  fetch('https://v8.dev/blog').then(() => 'blog'),
  fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {
  // Any of the promises was fulfilled.
  console.log(first);
  // → 'home'
}).catch((error) => {
  // All of the promises were rejected.
  console.log(error);
});

3.WeakRefs

https://github.com/tc39/proposal-weakrefs

注意:要尽量避免使用 WeakRefFinalizationRegistry,垃圾回收机制依赖于 JavaScript 引擎的实现,不同的引擎或是不同版本的引擎可能会有所不同。

这个提案主要包括两个主要的新功能:

  • 使用 WeakRef 类创建对象的弱引用
  • 使用 FinalizationRegistry 类对对象进行垃圾回收后,运行用户定义的终结器

它们可以分开使用也可以一起使用。

WeakRef 实例不会阻止 GC 回收,但是 GC 会在两次 EventLoop 之间回收 WeakRef 实例。GC 回收后的 WeakRef 实例的 deref() 方法将会返回 undefined

let ref = new WeakRef(obj)
let isLive = ref.deref() // 如果 obj 被垃圾回收了,那么 isLive 就是 undefined

FinalizationRegistry 注册 Callback,某个对象被 GC 回收后调用。

const registry = new FinalizationRegistry(heldValue => {
  // ....
});

// 通过 register 注册任何你想要清理回调的对象,传入该对象和所含的值
registry.register(theObject, "some value");

关于更多的细节你可以查阅:

4.Logical Assignment Operators 逻辑赋值操作符

https://github.com/tc39/proposal-logical-assignment

先来回顾下 ES2020 新增的空值合并操作符 ??

在当左侧操作数为 undefinednull 时,该操作符会将右侧操作数赋值给左侧变量。

const name = null ?? '前端食堂'
console.log(name) // 前端食堂

有了逻辑赋值运算符,我们可以替换掉如下旧的写法:

const demo = function() {
    // 旧的写法1
    // if (!a) {
    //     a = '西瓜'
    // }
    // 旧的写法2
    // a = a || '西瓜'

    // 新的写法
    a ||= '西瓜' 
}

a ||= b; // 等同于 a || (a = b);

a &&= b; // 等同于 a && (a = b);

a ??= b; // 等同于 a ?? (a = b);

5.Numeric separators 数字分隔符

https://github.com/tc39/proposal-numeric-separator

数字的可读性随着数字变长而变差,数字分隔符会让长数字更加清晰。

const x = 1000000000000
const y = 1_000_000_000_000
console.log(x === y) // true

在二进制、十六进制、BigInt 等中都可以使用。

❤️爱心三连击

1.如果你觉得食堂酒菜还合胃口,就点个赞支持下吧,你的是我最大的动力。

2.关注公众号前端食堂,吃好每一顿饭!

3.点赞、评论、转发 === 催更!

查看原文

赞 9 收藏 2 评论 0

jifengg 赞了文章 · 2020-08-17

阿里的秒杀系统是怎么设计的?

背景

我之前写过一个秒杀系统的文章不过有些许瑕疵,所以我准备在之前的基础上进行二次创作,不过让我决心二创秒杀系统的原因是我最近面试了很多读者,动不动就是秒杀系统把我整蒙蔽了,我懵的主要是秒杀系统的细节大家都不知道,甚至不知道电商公司一个秒杀系统的组成部分。

我之前在某电商公司就是做电商活动的,所以这样的场景和很多解决方案我是比较清楚的,那我就从我自身去带着大家看看一个秒杀的设计细节以及中间各种解决方案的利弊,以下就是我设计的秒杀系统,几乎涵盖了市面上所有秒杀的实现细节:

正文

首先设计一个系统之前,我们需要先确认我们的业务场景是怎么样子的,我就带着大家一起假设一个场景好吧。

我们现场要卖1000件下面这个婴儿纸尿裤,然后我们根据以往这样秒杀活动的数据经验来看,目测来抢这100件纸尿裤的人足足有10万人。(南极人打钱!)

你一听,完了呀,这我们的服务器哪里顶得住啊!说真的直接打DB肯定挂,但是别急嘛,有暖男敖丙在,任何系统我们开始设计之前我们都应该去思考会出现哪些问题?这里我罗列了几个非常经典的问题:

问题

高并发:

是的高并发这个是我们想都不用想的一个点,一瞬间这么多人进来这不是高并发什么时候是呢?

是吧,秒杀的特点就是这样时间极短瞬间用户量大

正常的店铺营销都是用极低的价格配合上短信、APP的精准推送,吸引特别多的用户来参与这场秒杀,爽了商家苦了开发呀

秒杀大家都知道如果真的营销到位,价格诱人,几十万的流量我觉得完全不是问题,那单机的Redis我感觉3-4W的QPS还是能顶得住的,但是再高了就没办法了,那这个数据随便搞个热销商品的秒杀可能都不止了。

大量的请求进来,我们需要考虑的点就很多了,缓存雪崩缓存击穿缓存穿透这些我之前提到的点都是有可能发生的,出现问题打挂DB那就很难受了,活动失败用户体验差,活动人气没了,最后背锅的还是开发

超卖:

但凡是个秒杀,都怕超卖,我这里举例的只是尿不湿,要是换成100个MacBook Pro,商家的预算经费卖100个可以赚点还可以造势,结果你写错程序多卖出去200个,你不发货用户投诉你,平台封你店,你发货就血亏,你怎么办? (没事看了敖丙的文章直接不怕)

那最后只能杀个开发祭天解气了,秒杀的价格本来就低了,基本上都是不怎么赚钱的,超卖了就恐怖了呀,所以超卖也是很关键的一个点。

恶意请求:

你这么低的价格,假如我抢到了,我转手卖掉我不是血赚?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛...)肯定也知道的。

那简单啊,我知道你什么时候抢,我搞个几十台机器搞点脚本,我也模拟出来十几万个人左右的请求,那我是不是意味着我基本上有80%的成功率了。

真实情况可能远远不止,因为机器请求的速度比人的手速往往快太多了,在贵州的敖丙我每年回家抢高铁票都是秒光的,我也不知道有没有黄牛的功劳,我要Diss你,黄牛。杰伦演唱会门票抢不到,我也Diss你。

Tip:科普下,小道消息了解到的,黄牛的抢票系统,比国内很多小公司的系统还吊很多,架构设计都是顶级的,我用顶配的服务加上顶配的架构设计,你还想看演唱会?还想回家?

不过不用黄牛我回家都难,我们云贵川跟我一样要回家过年的仔太多了555!

链接暴露:

前面几个问题大家可能都很好理解,一看到这个有的小伙伴可能会比较疑惑,啥是链接暴露呀?

相信是个开发同学都对这个画面一点都不陌生吧,懂点行的仔都可以打开谷歌的开发者模式,然后看看你的网页代码,有的就有URL,但是我写VUE的时候是事件触发然后去调用文件里面的接口看源码看不到,但是我可以点击一下查看你的请求地址啊,不过你好像可以对按钮在秒杀前置灰。

不管怎么样子都有危险,撇开外面的所有的东西你都挡住了,你卖这个东西实在便宜得过分,有诱惑力,你能保证开发不动心?开发知道地址,在秒杀的时候自己提前请求。。。(开发:怎么TM又是我)

数据库:

每秒上万甚至十几万的QPS(每秒请求数)直接打到数据库,基本上都要把库打挂掉,而且你服务不单单是做秒杀的还涉及其他的业务,你没做降级、限流、熔断啥的,别的一起挂,小公司的话可能全站崩溃404

反正不管你秒杀怎么挂,你别把别的搞挂了对吧,搞挂了就不是杀一个程序员能搞定的。

程序员:我TM好难啊!

问题都列出来了,那怎么设计,怎么解决这些问题就是接下去要考虑的了,我们对症下药。

我会从我设计的秒杀系统从上到下去给大家介绍我们正常电商秒杀系统在每一层做了些什么,每一层存在的问题,难点等。

我们从前端开始:

前端

秒杀系统普遍都是商城网页、H5、APP、小程序这几项。

在前端这一层其实我们可以做的事情有很多,如果用node去做,甚至能直接处理掉整个秒杀,但是node其实应该属于后端,所以我不讨论node Service了。

资源静态化:

秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。

秒杀链接加盐:

我们上面说了链接要是提前暴露出去可能有人直接访问url就提前秒杀了,那又有小伙伴要说了我做个时间的校验就好了呀,那我告诉你,知道链接的地址比起页面人工点击的还是有很大优势

我知道url了,那我通过程序不断获取最新的北京时间,可以达到毫秒级别的,我就在00毫秒的时候请求,我敢说绝对比你人工点的成功率大太多了,而且我可以一毫秒发送N次请求,搞不好你卖100个产品我全拿了。

那这种情况怎么避免?

简单,把URL动态化,就连写代码的人都不知道,你就通过MD5之类的摘要算法加密随机的字符串去做url,然后通过前端代码获取url后台校验才能通过。

这个只能防止一部分没耐心继续破解下去的黑客,有耐心的人研究出来还是能破解,在电商场景存在很多这样的羊毛党,那怎么做呢?

后面我会说。

限流:

限流这里我觉得应该分为前端限流后端限流

物理控制:

大家有没有发现没到秒杀前,一般按钮都是置灰的,只有时间到了,才能点击。

这是因为怕大家在时间快到的最后几秒秒疯狂请求服务器,然后还没到秒杀的时候基本上服务器就挂了。

这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点再给按钮可用状态。

按钮可以点击之后也得给他置灰几秒,不然他一样在开始之后一直点的。

你敢说你们秒杀的时候不是这样的?

前端限流:这个很简单,一般秒杀不会让你一直点的,一般都是点击一下或者两下然后几秒之后才可以继续点击,这也是保护服务器的一种手段。

后端限流:秒杀的时候肯定是涉及到后续的订单生成和支付等操作,但是都只是成功的幸运儿才会走到那一步,那一旦100个产品卖光了,return了一个false,前端直接秒杀结束,然后你后端也关闭后续无效请求的介入了。

Tip:真正的限流还会有限流组件的加入例如:阿里的Sentinel、Hystrix等。我这里就不展开了,就说一下物理的限流。

我们卖1000件商品,请求有10W,我们不需要把十万都放进来,你可以放1W请求进来,然后再进行操作,因为秒杀对于用户本身就是黑盒的,所以你怎么做的他们是没感知的,至于为啥放1W进来,而不是刚好1000,是因为会丢掉一些薅羊毛的用户,至于怎么判断,后面的风控阶段我会说。

Nginx:

Nginx大家想必都不陌生了吧,这玩意是高性能的web服务器,并发也随便顶几万不是梦,但是我们的Tomcat只能顶几百的并发呀,那简单呀负载均衡嘛,一台服务几百,那就多搞点,在秒杀的时候多租点流量机

Tip:据我所知国内某大厂就是在去年春节活动期间租光了亚洲所有的服务器,小公司也很喜欢在双十一期间买流量机来顶住压力。

这样一对比是不是觉得你的集群能顶很多了。

恶意请求拦截也需要用到它,一般单个用户请求次数太夸张,不像人为的请求在网关那一层就得拦截掉了,不然请求多了他抢不抢得到是一回事,服务器压力上去了,可能占用网络带宽或者把服务器打崩、缓存击穿等等。

风控

我可以明确的告诉大家,前面的所有措施还是拦不住很多羊毛党,因为他们是专业的团队,他们可以注册很多账号来薅你的羊毛,而且不用机器请求,就用群控,操作几乎跟真实用户一模一样。

那怎么办,是不是无解了?

这个时候就需要风控同学的介入了,在请求到达后端之前,风控可以根据账号行为分析出这个账号机器人的概率大不大,我现在负责公司的某些特殊系统,每个用户的行为都是会送到我们大数据团队进行分析处理,给你打上对应标签的。

那黑客其实也有办法:养号

他们去黑市买真实用户有过很多记录的账号,买到了还不闲着,帮他们去购物啥的,让系统无法识别他们是黑号还是真实用户的号。

怎么办?

通杀!是的没有办法,只能通杀了,通杀的意思就是,我们通过风管分析出来这个用户是真实用户的概率没有其他用户概率大,那就认为他是机器了,丢弃他的请求。

之前的限流我们放进来10000个请求,但是我们真正的库存只有1000个,那我们就算出最有可能是真实用户的1000人进行秒杀,丢弃其他请求,因为秒杀本来就是黑盒操作的,用户层面是无感知的,这样设计能让真实的用户买到东西,还可以减少自己被薅羊毛的概率。

风控可以说是流量进入的最后一道门槛了,所以很多公司的风控是很强的,蚂蚁金服的风控大家如果了解过就知道了,你的资金在支付宝被盗了,他们是能做到全款补偿是有原因的。

后端

服务单一职责:

设计个能抗住高并发的系统,我觉得还是得单一职责

什么意思呢,大家都知道现在设计都是微服务的设计思想,然后再用分布式的部署方式

也就是我们下单是有个订单服务,用户登录管理等有个用户服务等等,那为啥我们不给秒杀也开个服务,我们把秒杀的代码业务逻辑放一起。

单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(高可用)

Redis集群:

之前不是说单机的Redis顶不住嘛,那简单多找几个兄弟啊,秒杀本来就是读多写少,那你们是不是瞬间想起来我之前跟你们提到过的,Redis集群主从同步读写分离,我们还搞点哨兵,开启持久化直接无敌高可用!

库存预热:

秒杀的本质,就是对库存的抢夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,撇开性能因数,你不觉得这样好繁琐,对业务开发人员都不友好,而且数据库顶不住啊。

开发:你tm总算为我着想一次了。

那怎么办?

我们都知道数据库顶不住但是他的兄弟非关系型的数据库Redis能顶啊!

那不简单了,我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。

但是用了Redis就有一个问题了,我们上面说了我们采用主从,就是我们会去读取库存然后再判断然后有库存才去减库存,正常情况没问题,但是高并发的情况问题就很大了。

**多品几遍!!!**就比如现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的。咋办?

事务:

Redis本身是支持事务的,而且他有很多原子命令的,大家也可以用LUA,还可以用他的管道,乐观锁他也知支持。

限流&降级&熔断&隔离:

这个为啥要做呢,不怕一万就怕万一,万一你真的顶不住了,限流,顶不住就挡一部分出去但是不能说不行,降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。

消息队列(削峰填谷):

一说到这个名词,很多小伙伴就知道了,对的MQ,你买东西少了你直接100个请求改库我觉得没问题,但是万一秒杀一万个,10万个呢?服务器挂了,程序员又要背锅的

秒杀就是这种瞬间流量很高,但是平时又没有流量的场景,那消息队列完全契合这样的场景了呀,削峰填谷。

Tip:可能小伙伴说我们业务达不到这个量级,没必要。但是我想说我们写代码,就不应该写出有逻辑漏洞的代码,至少以后公司体量上去了,别人一看居然不用改代码,一看代码作者是敖丙?有点东西!

你可以把它放消息队列,然后一点点消费去改库存就好了嘛,不过单个商品其实一次修改就够了,我这里说的是某个点多个商品一起秒杀的场景,像极了双十一零点。

数据库

数据库用MySQL只要连接池设置合理一般问题是不大的,不过一般大公司不缺钱而且秒杀这样的活动十分频繁,我之前所在的公司就是这样秒杀特卖这样的场景一直都是不间断的。

单独给秒杀建立一个数据库,为秒杀服务,表的设计也是竟可能的简单点,现在的互联网架构部署都是分库的。

至于表就看大家怎么设计了,该设置索引的地方还是要设置索引的,建完后记得用explain看看SQL的执行计划。(不了解的小伙伴也没事,MySQL章节去康康)

分布式事务

这为啥我不放在后端而放到最后来讲呢?

因为上面的任何一步都是可能出错的,而且我们是在不同的服务里面出错的,那就涉及分布式事务了,但是分布式事务大家想的是一定要成功什么的那就不对了,还是那句话,几个请求丢了就丢了,要保证时效和服务的可用可靠。

所以TCC最终一致性其实不是很适合,TCC开发成本很大,所有接口都要写三次,因为涉及TCC的三个阶段。

最终一致性基本上都是靠轮训的操作去保证一个操作一定成功,那时效性就大打折扣了。

大家觉得不那么可靠的**两段式(2PC)三段式(3PC)**就派上用场了,他们不一定能保证数据最终一致,但是效率上还算ok。

总结

到这里我想我已经基本上把该考虑的点还有对应的解决方案也都说了一下,不知道还有没有没考虑到的,但是就算没考虑到我想我这个设计,应该也能撑住一个完整的秒杀流程。

最后大家再看看这个秒杀系统或许会有新的感悟,是不是一个系统真的没有大家想的那么简单,而且我还是有漏掉的细节,这是一定的。

秒杀这章我脑细胞死了很多,考虑了很多个点,最后还是出来了,忍不住给自己点赞

总结

我们玩归玩,闹归闹,别拿面试开玩笑。

秒杀不一定是每个同学都会问到的,至少肯定没Redis基础那样常问,但是一旦问到,大家一定要回答到点上。

至少你得说出可能出现的情况需要注意的情况,以及对于的解决思路和方案,因为这才是一个coder的基本素养,这些你不考虑你也很难去进步。

最后就是需要对整个链路比较熟悉,注意是一个完整的链路,前端怎么设计的呀,网关的作用呀,怎么解决Redis的并发竞争啊,数据的同步方式呀,MQ的作用啊等等,相信你会有不错的收获。

不知道这是一次成功还是失败的二创,我里面所有提到的技术细节我都写了对应的文章,大家可以关注我去历史文章看看,天色已晚,我溜了。

我是敖丙,你知道的越多,你不知道的越多,我们下期见!

人才们的 【三连】 就是敖丙创作的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言!

查看原文

赞 117 收藏 82 评论 3

jifengg 赞了文章 · 2020-08-04

报告老板,我们的H5页面在iOS11系统上白屏了!

0802.png

时间回到一周前,当时刚开发完公司A项目的一个新的版本,等待着测试完成就进行发布。此时的我也准备从连续多日的紧张开发状态中走出来,以为可以稍稍放松一下。而那时的我还不知道,我即将面临一个强大的Bug选手,更不知道我要跟这个Bug来来回回进行多次的搏斗。当然,我们能看到这篇文章也就说明了我最终解决了这个Bug,而且这个过程也是相当的精彩的。什么?你不相信,那就让我来带你进入这个“跌宕起伏”的经历中吧。

友情提示:接下来的文章也许有一点长,但是希望你能够坚持读下去。我相信我在解决这个Bug的过程中的一些思路会给你带来一些思考。当然也希望你在这个过程中能够像我一样学习到一些新的知识,为以后排查类似的Bug积累一些经验。好啦,话不多说,让我们开始吧。

项目介绍

先来简单介绍一下A项目,这是一个基于Vue框架的项目,项目使用的也是Vue CLI这个开发工具。这个项目是需要集成在别的APP中的,也就是页面需要在APP中进浏览和操作。这个项目在我接手之前已经开发过一段时间了。所以项目中的一些依赖库和工具库版本相对比较低,这也给我后续的调试以及解决Bug的过程增加了一些困难。

BUG初现

当时开发完成之后,就交给我们这边的测试和另一个城市的相关同学去验收这次开发的功能。在我们这边一切都很正常,测试这边也没有反馈有什么问题。但是在另一个城市的同学小C的iPhone手机上却发现了白屏,打开页面之后什么内容也没有。

发现了这个问题之后,我再次跟我们这边的测试同学确认了一下,看看我们这边测试的iOS系统的iPhone手机有没有这个问题。经过测试的测试,发现我们这边的几台iPhone手机都没有问题。然后就问了小C他使用的测试手机的系统版本是多少,当时感觉应该跟iOS的系统版本有关系。

小C反馈说他的iPhone是6S Plus,然后系统的版本是11.1.2。我问了我们这边测试使用的iPhone版本都是多少,测试反馈说系统的版本都是12以上的。所以到这里,我确定了这个白屏Bug的出现肯定跟iPhone手机的系统有关系

重现BUG之路

虽然确定了问题出现的环境,但是因为我身边没有系统是11的iPhone手机,所以想让这个问题重现就变成了一个难题。询问了身边的同事,大家的系统版本也都普遍高于12,所以借用别人的手机进行调试这个方法暂时也不可行。

在平时的开发中,如果网页在iOS系统的APP中有一些问题的话,我们一般都会通过Safari浏览器进行调试。但是因为这次出现问题的iPhone手机不在我这里,并且我这边也没有相同系统的手机。所以想通过真机进行调试就不太可能了。那怎么办呢?这个问题肯定是要解决的,我也相信办法总比困难多

想要进行调试,最简单的办法就是让我有一个系统是11的iPhone手机。所以我就搜索看看有没有什么办法可以给iPhone手机安装11的系统。一搜索还真的有,过程也不算是很复杂。但是其中有一个步骤是需要到一些论坛或者第三方的助手网站下载跟自己手机型号相匹配的iOS系统,这个步骤让我有点感觉不安全。毕竟不是官方的,不能够保证安全性。而且也未必有版本是11的系统。所以这个方案就暂时作罢

在我搜索的过程中,我发现有网友说可以使用Xcode安装相应系统版本的iPhone模拟器来进行调试。哎,你说我怎么没有想到这个办法呢?这确实是一个不错的办法。因为之前跟公司的同事学习过Swift,也了解过Xcode的一些操作。突然感慨,真是技多不压身,你不知道你什么时候就会用上你学过的知识。所以有条件的话,还是多学习一些知识。额,有点跑题了。

安装Xcode

我打开公司的电脑,开始安装Xcode,但是发现公司的电脑系统版本太低,安装Xcode需要升级系统,所以没办法,先升级系统吧。因为升级的时间比较长,我想到自己家中的Mac电脑上是有安装过Xcode,所以决定先回家。留下公司的电脑慢慢升级。

回到家,二话不说就开始准备调试,但是发现我的Xcode上面的iPhone模拟器的系统版本也都是12以上的,查了一下资料,Xcode是可以安装不同系统版本的模拟器的,于是我就安装了系统版本是11的模拟器。这个过程需要我们打开Xcode的偏好设置,然后在Components选项中,选择下载你要安装的对应系统版本的模拟器。

安装iOS11的模拟器

安装成功之后,运行iPhone 6S Plus模拟器,使用模拟器的Safari打开h5的页面地址,果然是白屏。

iPhone 6S Plus模拟器出现白屏

小样,终于把这个问题给复现了,这样就距离解决这个Bug不远了。我打开MacSafari浏览器,进入开发者模式,发现了如下所示的报错

Safari浏览器控制台的报错

我搜索了一下这个错误,发现是因为项目中使用了...ES6扩展运算符,然后iOS 11系统不支持这个这个运算符。这么容易就找到问题了,开心。想到这个问题还是比较好解决的,可以通过使用Babel的一些插件,很容易就可以将这个问题解决掉。然后我就开心的睡觉去了,心想这个问题也不是什么大问题,明天处理一下就好了。

安装Safari Technology Preview

第二天到公司,我就在项目中的babel的配置文件中添加了相应的插件

{
  ...  // 省略原来的配置内容
  "plugins": ["@babel/plugin-proposal-object-rest-spread"]
}

然后发布到测试环境中。告诉了小C同学再次测试一下,我也在等着解决这个Bug的好消息。但是,出现的却不是好消息,小C给我回复说还是不可以。什么,不可能呀,我就马上用公司的电脑再次进行测试。当我用公司电脑的Safari调试系统是iOS 11iPhone 6S PLus模拟器的时候,却发现出现了下面这个情况:审核警告:“data-custom”太新,无法在此检查的页面上运行

审核警告:“data-custom”太新,无法在此检查的页面上运行

我就又搜索了一下为什么会出现这个问题,终于让我找到了答案Safari浏览器的Web Inspector工程师也说这是一个Bug,不过他们已经修复了,在下个发布的版本中就可以正常使用新的Safari浏览器去调试比较老的iOS系统的模拟器了。知道现在这个版本的Safari调试不了模拟的iOS 11系统的页面。我有点沮丧,总不能我现在回家把我的电脑拿过来吧😂?当我想着该如何解决的时候,我发现了上面那个回答中提到了Safari Technology PreviewSafari技术预览

stackoverflow上面Safari浏览器的Web检查器的开发者的回复

我看这个名字感觉有点希望,然后就搜索了一下Safari Technology Preview是什么。然后就发现它相对于Safari就跟Chromium相对于Chrome是一样,都相当于是开发版本的浏览器。

Safari Technology Preview

这时,我觉得可以使用Safari Technology Preview进行调试。所以就下载了Safari Technology Preview,当我打开Safari Technology Preview然后进入开发者模式后,发现确实可以调试iOS 11系统的页面。然后我就看了一下为什么还是白屏的问题。发现出现的错误还是上次的问题:

SyntaxError: Unexpected token '...'. Expected a property name.

也就是说这个问题还没有解决掉,因为打包后的代码是没有SourceMap的,所以要想看更详细的报错信息,需要在本地进行调试。本地的环境中是有SourceMap的,可以定位到更详细的错误信息,我在本地运行了项目,然后我打开了控制台的错误详情,发现是使用的一个第三方的库出现了问题。

找到了出现问题的使用的第三方库

那么到这里为止,可以说明上面我们使用的Babel插件没有处理这个第三方的库,所以现在我们的问题就变成了:如何解决第三方库中出现的...扩展运算符没有被编译为ES5语法的问题

将第三方库中的ES6语法进行编译

查看Vue CLI中相关的配置方法

这时我又仔细的看了一下Vue CLI的相关文档,发现确实在浏览器的兼容性这个章节中,提到了一些处理的方法。原来我们在项目中写的代码默认会帮我们转换为ES5的语法的,但是如果项目中依赖的第三方库需要polyfill的话,那需要我们手动进行配置。一看到这里,我感觉黎明就要来了

Vue CLI浏览器兼容性

我就开始尝试这三种方法。我发现第一种方法是比较简单的,也很好配置。于是我就尝试了第一种方法。在项目的vue.config.js中添加如下的配置:

...  // 省略的配置
transpileDependencies: [
  'module-name/library-name' // 出现问题的那个库
],
...  // 省略的配置

重新运行项目,当我将要为即将到来的成功欢呼鼓掌时,控制台突然报告了如下的错误:
Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'

Uncaught TypeError: Cannot assign to read only property ...

这个报错是在Chrome浏览器的控制台出现的,因为项目在本地重新运行之后会首先打开Chrome浏览器。真是的,一个问题还没有解决,又出来了一个新的问题。然后再次查询资料后发现,原来是因为这个第三方的库是一个CommonJS类型的库,而Babel默认处理的是ES6module类型的库,所以这里就又出现了新的问题。

https://github.com/webpack/webpack/issues/4039 sokra的回复

第一种方法遇到了阻碍,先暂停一下。我准备继续尝试下面两种方法。但是因为后面两种方法对原来的项目改动有点大,所以我直接通过Vue CLI创建了一个新的项目,在package.json中加入项目中使用的那个第三方包的依赖,使用公司的包管理工具安装了依赖。然后运行项目,打开控制台确实发现了相同的错误。但是打开详情以后,发现出错的路径跟我原来项目不一致。然后我这次抱着试一试的心态,继续使用了第一种方法尝试看看可不可以。然后复制了出错路径的包名称,在vue.config.js文件中的对应位置添加了如下的配置代码:

...  // 省略的配置
transpileDependencies: [
  'module-name-new/library-name-new' // 出现问题的那个库
],
...  // 省略的配置

然后重新运行项目,发现居然可以了。啊,居然可以了。为什么我在原来的项目中这样却不可以呢?我看了一下原来项目的依赖以及现在新的测试项目的依赖,发现它们的vue, babel版本差了好多。我猜测可能是因为这个原因。但是现在肯定不可以贸然升级这些依赖的版本,因为为了解决这个问题再次带来新的问题就得不偿失了。

还有一个问题就是为什么同样的第三方库,在原来的项目中和现在的项目中报错的路径不一样。而且看着像是使用了两个不一样的第三方库。这里先留个悬念,我会在后面的文章中进行解释。

接下来,我开始在测试项目中继续尝试剩下的两种方法,对于第二种方法,因为老项目中使用的presets是没有polyfills这个配置选项的,到现在为止出问题的这个第三方库我不知道除了这个...对象扩展操作符之外还有没有别的依赖。所以这个方法我暂时也放弃了。

对于第三个方法,我觉得可以尝试,首先我将测试项目中的一些关键依赖进行了手动降级,然后按照上面的第三个方法的步骤在测试项目中使用。但是发现测试项目运行之后,提示需要安装core-js,安装core-js之后还报错,再次提示需要安装es.module.regex.match等等很多依赖,继续查资料,发现需要把配置中的 useBuiltIns修改,但是因为我接手的这个项目是老项目,依赖比较多,不确定修改useBuiltIns这个配置选项后会不会出现新的问题。所以也不敢贸然修改这个配置选项,所以也暂时放弃了这个方法。

我后来想了一下,对于...扩展运算符来说,这是一个新的语法。是不能够通过一些polyfills去解决的。需要Babel对这个语法进行编译,然后才可以在低版本的系统中使用,所以解决的办法还是要让Babel对这个库再次进行编译。

寻找新的突破口

当进行到了这里的时候,似乎没有了出路。一时间我感觉我要被这个Bug打败了,我似乎听到了它无情的嘲笑,“小伙子,是不是被我折磨的没有脾气啦;放弃吧,你是没办法打倒我的。哈哈哈。。。

Photo by sebastiaan stam on Unsplash

但是,它看错我了,Bug越是难解决,我对它就越有兴趣。所以我决定好好理一下思路,准备再次扬帆起航。

我发现第一种办法其实是起作用的,只不过是因为一个是CommonJS类型的,一个需要是ES6 module类型的。所以我决定从这个地方入手,于是我决定查查相关的资料,看看Babel有没有办法可以即能够处理CommonJS模块,又能够处理ES6 module模块呢?终于,功夫不负有心人,我发现了Babel里面有这么一个配置sourceType,如果把sourceType设置为unambiguous就可以解决这个问题

https://babeljs.io/docs/en/options#sourcetype

这样Babel就会根据模块文件中有没有import/export来决定使用哪种解析模块的方式。于是我再次使用了第一种方法,在vue.config.js中添加了transpileDependencies选项的配置,然后在项目中的Babel配置文件中添加了如下的配置:

module.exports = {
  ...  // 省略的配置
  sourceType: 'unambiguous',
  ...  // 省略的配置
};

发现的确可以,这一刻成功的喜悦再次降临。然后我再次打包,再次把代码部署到测试环境,赶忙让小C同学再次测试一下,发现的确可以。欧耶,终于解决这个问题了。我终于可以松一口气了,哈哈哈。。。小样,这怎么会难得到我呢?

但是,当我仔细阅读将这个选项设置为unambiguous时,我发现了一些问题。因为这样的话会有一些风险,因为就算不使用import/export语句的这些模块也可能是完全有效的ES6 module,所以这样的话就有可能会出现一些意外的情况。怎么办,我似乎在一不留神的时候又被Bug卡住了脖子

https://babeljs.io/docs/en/options#sourcetype

我觉得老天总是给我开玩笑,当我从一个坑里跳出来,以为没有危险的时候。前面突然又多出来一个坑,我一不留心就又掉了进去。我感觉既然都走到了这里,肯定要继续走下去,一定有办法可以优化我现在遇到的问题。我就很仔细的再次看了一下Babel的配置说明文档,这个时候就心想如果我对Babel再熟悉一些就好了。没关系,继续努力。终于,我似乎看到了什么了不得的配置选项。

https://babeljs.io/docs/en/options#overrides

我在Config Merging options里发现了overrides选项,这个配置选项不正是我需要的吗?我可以利用这个配置选项将我需要的第三方包使用unambiguous的处理方式,然后其他的第三方库都按照之前的方式处理不就可以了。哈哈哈,我真是个天才,我心里这样对自己说😂。

Photo by bruce mars on Unsplash

所以只需要在项目的babel.config.js中写下如下的配置就可以了:

module.exports = {
  ...  // 省略的配置
  overrides: [
    {
      include: './node_modules/module-name/library-name/name.common.js',  // 使用的第三方库
      sourceType: 'unambiguous'
    }
  ],
  ...  // 省略的配置
};

对了,还有一件事情还没有说,那就是上文提到的关于为什么使用公司自己的包管理工具下载下来的node_modules包的名称跟使用官方的npm包管理工具下载的包的名称不一致的问题。原因是公司使用的包管理工具是cnpm的一个修改版本。又因为cnpm为了提高下载的速度,使用了cnpm/npminstall,所以才会出现下载的包名比较混乱的情况,详情可以看这里

到此完结撒花,总结一下:出现白屏的原因是因为使用的第三方库的包中使用了...扩展运算符,然后因为第三方的包默认是没有被Babel处理过的,所以在不支持...iOS 11系统上就出现了白屏。解决的方式就是通过给vue.config.js的配置文件中transpileDependencies配置选项中添加上出问题的包的名称就可以了。当然如果项目比较老,可能还需要像文章上面写的那样的处理方式。

解决这个Bug过程就像是升级打怪一样,不断失败,不断尝试,只要不放弃,终有成功的那一天。如果你坚持看到了这里,那说明你也很棒呀。在当今这个信息爆炸的时代里,能够坚持看完一篇很长的文章已经很不错了。

一点反思与思考:这个过程中我也发现了自己对BabelVue CLI其实没有那么熟练,如果我对它们比较熟练的话,那我解决这个Bug应该会花费更少的时间。当然,现在把它们学习好也不算晚。要抱着学习的态度,这次解决这个Bug的过程,就是我以后解决其它类似Bug的经验。还有在解决Bug的这个过程中要有耐心,当然在尝试之后也要学会放弃错误的方向

写这篇文章也花费了我不少的时间,如果你有所收获或者感悟,不妨点赞,转发,收藏走一波,这个要求应该不算过分吧😂?

如果你对本篇文章有什么意见和建议,都可以直接在文章下面留言,也可以在这里提出来。也欢迎大家关注我的公众号关山不难越,学习更多实用的前端知识,让我们一起努力进步吧。

公众号:关山不难越

查看原文

赞 21 收藏 10 评论 11

jifengg 回答了问题 · 2019-07-26

解决为啥js 调试的时候 没有打断点还有debug 出现呢?

clipboard.png

最后一个,“在出异常时暂停”,你启用了,去掉就行。

关注 4 回答 3

jifengg 回答了问题 · 2019-07-26

关于html5 video标签视频格式支持问题

一个是上传后,在后端转码。
一个是上传前,在前端app里转码。

不要想在播放的时候由前端实时转码,不现实。

关注 2 回答 2

jifengg 回答了问题 · 2019-05-21

JSON.stringify 引用对象类型报错

如果你非得循环引用,又非得序列化成字符串,那么就自己写

var obj = {
     title: '标题'
}
obj.content = obj;
JSON.stringify(obj,function(key,val){
    if(key=='content'){
        //这里我给忽略了,自己按照业务修改
        return undefined
    }else{
        return val
    }
});

重点就是stringify的第二个参数。

关注 5 回答 3

jifengg 回答了问题 · 2019-05-21

类似qq聊天对话框图片

给你个词:九宫格

关注 4 回答 3

jifengg 回答了问题 · 2019-05-21

解决IE和Edge每次开机后都连接不到网络,科学上网后就能用了,之后关闭梯子也能正常用了,是什么原因?

最有可能的就是SSR没有正常关闭。
你是不是开着SSR,就点关机了?

关注 5 回答 6

jifengg 回答了问题 · 2019-05-15

解决npm发布包,提示permissions must be either "write" or "read",这是啥情况?

如果账号什么的没有问题,最有可能的就是你这个包名已经被别人使用了。你可以在上面搜索一下

关注 2 回答 2

认证与成就

  • 获得 24 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-12-05
个人主页被 714 人浏览