大前端时代,浅谈JavaScript开发重型跨平台应用以及架构

  • 2019 年 10 月 10 日
  • 筆記

大前端时代以及即将到来的5G时代,3D可视化,音视频直播技术,IM即时通讯场景应用我觉得都是大有可为的。前段时间爆款换脸应用出现,到近段时间头像加?的火爆,这是好事。

不知不觉,九月就要过去,由于这个月工作上,被C++折磨得很难受,而且其他时间都在学习,所以没有时间写文章,好在技术提升很大。今天准备好好谈一谈重型应用的架构以及技术选型,为接下来我的正式架构设计做个铺垫。

为什么写重型应用的架构和技术选型

  1. 传统的web前端,只能发个ajax请求,画画页面。了不得写个webApp
  2. 想让后端的同学们,了解下目前大前端的世界,现在的前端跟以前不一样了,特别现在市场很缺高级前端,但是术业有专攻,这点我承认
  3. 大前端的定义,太广泛,在我看来,必须深入前端某个方向,以及能独立设计不那么复杂场景下的后端架构。
  4. 在极客时间上提问了winter老师,我自我感觉已经良好,但是迷茫了。他回怼了我:想一想你用你的技术做出了什么nb的东西把。
  5. 是人都想做出点什么事情,我想引起大家的共鸣去使用某些技术,或者朝着这个方向去发展,共同提升 社区的技术整体层次

什么是重型应用

例如微信,QQ,Telegram, 以及一些工具类的应用

说到这些大家肯定觉得,为什么不说是游戏? 当然游戏也算,可是我相信做出1000万人每天都在用的产品是大家的梦想,起码能吹一辈子吧

工具类的东西其实是最难做的,比如vsCodeExcelPhotoShop这些。这也是为什么这么多年出现成功的工具类产品这么少。这里不得不提到vsCode,它其实就是用ELectron开发,基于TypeScript。当然肯定使用了不少C++插件,说到这里,留下伤心的眼泪,最近也是被折磨得很难受

成功开发一个重型应用的好处

  1. 出去面试基本上很容易成功,特别是专业性强的岗位,例如你在QQ开发了十几年,你根本不用出去找工作,当然你应该也不会跑
  2. 技术全面,复杂场景你都能hold
  3. 有能吹的地方,可以跟谁说:我开发的东西,多少万人在用,老了还能吹。 程序员嘛,一半时间都在吹水,还有接近一半时间在划水,只有一丁点时间在写代码
  4. 更容易财务自由,生活自由,例如现在很多有过成功的重型应用开发者已经不单纯靠代码产出维持生活。他们做技术顾问,卖课程,出书,办培训都甚至比单纯写业务代码赚得多很多

正式开始

  • 目前跨平台框架,移动端比较成熟的是React-native,但是大家有所了解的都应该知道,这个框架虽然生态比较成熟,但是在面对众多手机的适配难度,以及性能方面存在的缺陷,如果用它制作重型应用我觉得是不合适的,如果要做重型应用,移动端应该使用原生。
  • 库克说过,中国的移动端开发确实很强,美国人要做一个应用,首先考虑的是PC桌面端,而国内首先考虑的是移动端。

国内移动端开发人员,在我看来已经人目前已经够多了,如果说你现在不会React-nativeFlutter,我也不建议你去深入学习,特别是Flutter.

为什么要这么说?

React-native刚出来的时候,坑多吧。现在Flutter也是,可是当你从RN最初的版本踩坑踩到现在,之前踩的坑大都没有意义(说这些话想过被喷,但是…此处省略一万字,建议去了解下原理和基本使用,不要耗费太多时间)

一个技术你去使用,并不是它多流行,只要它足够流行。 —来自某位国内大佬

技术的学习,应该多往底层钻研,如果你走错了路,钻错了方向,浪费了时间得不偿失,我之前有说过,前端最核心的几个基础知识点,应用层的东西从来不会很难。前提你的基础足够扎实

既然说了移动端没有合适的重型跨平台应用开发框架,那么只有PC端了。还有多少小伙伴在PC端开发呢?

Electron开发,来了

我不止一次提到过这个框架,我觉得它真是一个非常棒的框架,为什么这么说呢?

  1. 我跟很多朋友说过,如果想开发APP,不会写原生,那么你肯定达不到某种境界。因为你始终有很多很多的黑盒过程,可是Electron就会大大降低这个概率。
  2. 基本上没有适配和差异性,linuxMac以及Windows三者都可以运行,除了Mac上某些特殊场景需要自己设计下菜单快捷键之类,以及一些文件IOMAC默认行为
  3. 最新的Node版本、运行的V8环境以及最新的谷歌浏览器一起被打包,最新的技术和API都可以用,无需适配担心兼容性,真正放飞自我,可以随时随刻用Node.js实现功能,甚至调用大量C++插件,著名的VSCode就是这样而来

你甚至可以看成Electronweb网页套上一层壳,你可以在主进程写你的Node.js去实现功能,渲染进程你怎么写怎么写,还可以呼叫封装好的原生接口。遇到特别复杂的需求,用C++插件去实现吧

最终打包出来的安装包跟正常的桌面应用是一样的,正常安装卸载等,都已经封装好。

目前GitHub上已经有77.2Kstar

应用层面的东西,大都不会太难,Electron的文档已经非常全面,基于它出现了很多复杂,而且成功的工具类重型应用。我相信它

whatsApp也是基于它,国外还有一些很NB的应用也是。这里不做过多阐述,可以确定它是一个成熟而且成功的框架

可能很多人看到这里又要说标题党了,别急,下面来干货。

重型应用架构注重的核心问题

  1. 项目本身的最重要功能是什么
  2. 项目本身出发点是为客户提供什么方便
  3. 项目的核心竞争力是什么

一个好的开发,它一定能懂一些产品,甚至测试,当然他也应该会炒河粉,35岁以后好维持生活

我们今天举一个例子,IM,即时通讯,Telegram,20万人超级群端到端加密的核心卖点产品

电报Telegram

现在回答上面三个问题:

项目本身的最重要功能是什么

答案:即时通讯,信息的收发

项目本身出发点是为客户提供什么方便

使用产品进行消息传递

项目的核心竞争力是什么

20万人的超级群,端到端加密,隐私足够安全

核心竞争力,往往代表了这个应用产物的技术最难点,因为谁都能做,那么就不是核心竞争力了

所以我们其他的都忽略,关注第三点,开始进行技术选型,架构。

单线程的Node.jsJavaScript重型应用架构设计

要想写好这个架构,我觉得你首先在自身的擅长领域不能有太多的黑盒过程。例如框架源码,库原理实现,浏览器和Node.js的事件EventLopp以及他们的缺陷,你要熟知在心。因为像这种应用,一个小方向可能就会掏空你的技术栈,耗尽你的精力,例如音视频、图片处理等。

单线程的Node.js以及js主解析引擎,让我们又爱又恨。

这款应用的核心竞争力,是20万人超级群,那么数据量很大,大批量渲染压力和频繁加解密计算耗时、频繁数据库写入压力都是不可避免的,那么我们的Node.js擅长异步非阻塞,以及前端渲染进程的异步就显得尤为重要

前端架构整体的核心除了技术选型以及大体框架外,就是任务调度。

这里的任务调度分两种:

1.渲染任务调度

2.非渲染任务调度

单个渲染任务调度

1.React框架中,多次传入对象,setState会自动合并到一次执行,其实就是一种节流思想

2.ReactFiber架构思想,把若干个任务分割成多个小任务执行,中间根据你的任务优先级安排去选择时机执行

3.淘宝的分片渲染方案,跟上面第二条有一些类似

我之前说过,应用层的东西都不难,只要你基础足够扎实,能手写出简单的框架,以及库。你绝对能非常轻松应对前端百分80以上的性能问题和需求,技术最终都是相似的

上面只是说了别人的一些比较简单的优化方案,下面才是开始我们自己的渲染任务调度:

回到我们的Telegram架构设计方案:

渲染任务架构过程需要着重考虑的几个问题:

1.渲染数据量特别大

2.更新特别频繁

3.尽可能手动回收垃圾,避免消息量过大,v8垃圾回收的时间不确定性导致内存被白白占用,引起卡顿

4.考虑大批量数据到达渲染进程的用户应用体验,确定用户交互属于高优先级任务,其他的哪些是低优先级-但必须执行的任务,哪些是中优先级任务,这里说的任务,都是渲染任务。

今天在学习一篇小册,里面有一句话引起了我的共鸣,在计算机的世界,如果有解决不了的问题,那就加一个中间层,如果还不行,那就加两个。 -后面这句是我加的

这个是我自己编写的Reactmini-react源码地址

PReact源码中,是将需要更新的组件放入队列中,然后一次清空,伪代码:

   if (setStateQueue.length === 0) {      //清空队列的办法是异步执行      defer(flush);    }       setStateQueue.push({      stateChange,      component    });        function defer(fn) {        //requestIdleCallback的兼容性不好,对于用户交互频繁多次合并更新来说,requestAnimation更有及时性高优先级,requestIdleCallback则适合处理可以延迟渲染的任务~        //   if (window.requestIdleCallback) {        //     console.log('requestIdleCallback');        //     return requestIdleCallback(fn);        //   }        //高优先级任务        return requestAnimationFrame(fn);      }        while ((component = renderQueue.shift())) {      renderComponent(component);    }

上面这段代码其实很重要,核心思想就是:

每当进入这个函数,如果发现队列队列里没有任务就去执行defer函数

defer函数执行是异步,此时defer下面的setStateQueue已经被push了进去数据,这样达到一帧完成一次渲染任务调度

当然上面仅仅一个小的任务调度,这个必须要了解,才能往下看


requestAnimationFramerequestIdleCallback使用:

你需要深入了解React框架的Fiber架构,这块尤其重要,是性能优化,任务调度的基础,上面有提到,React在每次diff对比阶段,将任务分割成若干个小任务,此时如果有RAFRID的任务,就要考虑去执行了

RAF的任务会每次在下一次小任务前执行

RID的任务只有在下一次小人物前,有空余时间才会执行,所以它不一定会执行。(特别高频必须执行的任务)

Fiber架构配合单个任务分割已经介绍完毕,下面出思维导图出整体的任务调度


整体渲染任务调度

核心的两点:

1.释放主线程的占用,让用户的操作最快得到响应

2.合理调度任务,分高、中、低三种优先级别任务

理清思路:

1.数据通过IPC通信到达渲染进程

2.全部交给子线程去进行计算,组装数据,通过异步的postMessage事件通信,拿到渲染数据

3.调度渲染任务,用户操作交互

4.释放主线程

这里特别提示,为什么我一直强调不要使用定时器,一旦应用变得很复杂,如果任务调度不合理,定时器里的代码是要很久很久才能执行的。当然,只有重型应用会这样

渲染任务调度这块,主要是微任务,RAF,RID分片渲染以及同步代码,队列调度等手段。

主进程,接入层任务调度

核心思想跟渲染进程大概一致:

1.尽量释放主进程,保持空闲,让用户的操作即时得到反馈,因为很多操作会调用主进程的接口

2.异步调度任务,写入数据库异步,解密计算可以使用nextTick等方式去调度添加队列

这里提到,任务调度的核心一点就是,频繁触发的任务必须加入队列,异步清空,否则像解密这种同步计算耗时,一旦被频繁触发就会引起阻塞。即使交给了其他进程,也要做队列

整体架构以及技术选型注意点

1.技术选型时,尽量选择自己熟悉它原理的库,以及能用Demo测试模拟场景的技术,测试通过再选用,不同技术之间解决问题出发点不一样,可能会有冲突

2.队列和多进程、多线程的开启,并不是一定需要的,你可以自己设定一套规则,当一段时间的任务到达多少次被触发时候选择开启多线程,多进程。否则就是浪费

3.重型应用架构远不止这些,所以标题是浅谈,等下个月技术作者再度飞速提升一波,再来谈这些。