当前位置: 首页 > 科技观察

从前端到全端:JavaScript的逆袭

时间:2023-03-14 09:19:42 科技观察

背景近年来,前端技术日新月异。具有后端能力的node、具有移动开发能力的reactnative、具有游戏渲染能力的cocos2d-js、iOS上的热修复技术JSPatch等新技术相继涌现。乍一看,几乎每一端都被JavaScript攻陷,大有称霸江湖之势。毕竟,JavaScript是如何做到万能的呢?JavaScript真的能称霸江湖吗?领域代表了web前端的各种MVVM框架,recat、angular、vue……故事要从JavaScript的起源说起。乱世英雄:JavaScript的诞生背景:1995年,SUN开发了Java技术,这是第一个通用软件平台。Java具有跨平台、面向对象、泛型编程等特点,广泛应用于企业级Web应用开发和移动应用开发。Java也随着互联网的飞速发展而发展起来,逐渐成为一种重要的网络编程语言。一时间名声在外。1994年,网景公司成立,推出了自己的免费版浏览器NetscapeNavigator,迅速占领了浏览器市场。1995年,微软开始加入并很快发布了自己的InternetExplorer1.0。1995年,当时在Netscape工作的布伦丹·艾奇(BrendanEich)正在为NetscapeNavigator2.0开发浏览器。名为LiveScript的脚本语言,以及后来Netscape和SunMicrosystems组成的开发联盟,为了让这门语言赶上Java这门编程语言的“热词”,暂时将其更名为“JavaScript”。许多误解的原因之一。JavaScript最初是由Java设计的,目的之一就是“看起来像Java”,因此在语法上有相似之处,一些名称和命名约定也借鉴了Java。但是JavaScript的主要设计原则是从Self和Scheme中衍生出来的。JavaScript和Java名称的相似是当时Netscape和SUN出于营销考虑而达成协议的结果。因此,JavaScript和Java实际上没有任何关系。JavaScript推出后,在浏览器中大获成功,不久后微软推出了用于InternetExplorer3.0浏览器的JScript,与市场领先的Netscape产品竞争。JScript也是一种JavaScript实现。这两种JavaScript语言版本在浏览器端并存意味着语言标准化的缺失。这种语言的标准化已经提上日程。1997年,Netscape、SUN、Microsoft、Baolan等公司组织和个人组成的技术委员会在ECMA(欧洲计算机制造商协会)中确定并定义了一种新的脚本语言标准,称为ECMAScript,规范称为ECMA-262.JavaScript成为ECMAScript的实现之一。ECMA-262的第五版是ES5。ECMA-262,包括ES5、ES6等是一个标准,而JavaScript是ECMAScript的一种实现。一个完整的JavaScript实现应该包括三个部分:名称描述ECMAScript(语言核心),也就是基本语法、代码块、作用域、数据类型等。在NetscapeNavigation2.0和IE3.0出现后的这些年,Netscape微软不断发布新版本的浏览器以支持更多新功能。从此拉开了浏览器大战的序幕。这场浏览器大战还在继续,下一张图清楚地展示了这个过程。从浏览器之争我们可以看出,浏览器的竞争大致有两个方面:视觉体验(渲染排版)和速度(脚本运行)。所以一个完整的浏览器至少由两部分组成:浏览器组件描述排版引擎(kernel)全称是Layoutengine,也称为浏览器内核(webbrowserengine),页面渲染引擎(renderingengine)或样本版本引擎)是一个负责获取标记内容(如HTML、XML和图像文件等)、组织信息(如CSS和XSL等)并将排版内容输出到显示器或打印机的软件组件。所有Web浏览器、电子邮件客户端和其他根据表示标记显示内容的应用程序都需要排版引擎。JavaScript引擎JavaScript引擎是专门处理JavaScript脚本的虚拟机,通常包含在Web浏览器中。常见的有SpiderMonkey、V8等。添加通用浏览器内核和JavaScript引擎搭配:浏览器名称排版引擎(kernel)JavaScript引擎GoogleChrome使用webkit排版,借鉴了Safari和FirefoxV8等JavaScript引擎的一些成果,Rhino,由Mozilla基金会管理,开源代码,完全用Java编写,可以看作是Java版的SpiderMonkey。注意:webkit不仅仅是一个排版引擎,webkit=排版引擎+JavaScript引擎。>因此,JavaScript是一种动态语言,它的运行是基于JavaScript引擎的。大多数引擎都是用C++、Java等静态语言实现的。JavaScript的能力也是由引擎赋予的。不管是浏览器环境下的窗口,还是node环境下的进程,都是引擎提供的。(番外:Mozilla的人不知道为什么这么喜欢猴子,他们经常用猴子来命名技术,所以当他们看到Monkey的时候,很可能就是这么干的。)诺曼底登陆:JAVASCRIPTBINDING/BRIDGE桥接技术在浏览器中environment、DOM、BOM、window对象、setTimeout/setInterval、alert、console等方法都不是JavaScript本身的能力,而是浏览器原生实现,然后通过JavaScript引擎注入到JS运行的全局上下文中进行JS来使用。识别方法在调试器控制台打出来,带【nativecode】的是:合理:1.JavaScript运行→依赖JavaScript引擎←浏览器集成JavaScript引擎,同时注入nativecode通过JavaScript引擎来工作JS脚本的使用2.传播思路,只要你有JavaScript引擎,就可以运行JS脚本,不管你有没有浏览器!只是缺少了浏览器提供的alert、window等方法。3、既然浏览器可以向JavaScript引擎注入代码,赋予JS脚本在网页中特殊的能力,同样我们可以自己集成JavaScript引擎,定义自己的方法注入JavaScript引擎,赋予JS更多更强的功能.定制能力!注入的关键是:值类型相互对应,Obj映射为class的实例,function映射为句柄或者引用JavaScript的坑JavaScriptC++Javanumberint/long/float/doubleint/long/float/doubleJAVASCRIPTinternal,所有数字都存储为64位浮点数,甚至是整数。因此,1和1.0是相同的,相同的数字。也就是说,在JavaScript语言的底层,根本没有整数,所有数字都是小数(64位浮点数)。令人困惑的是,有些操作只能用整数来完成。这时,JavaScript会自动将64位浮点数转换为32位整数,然后进行运算。因为浮点数不是精确值,所以涉及小数的比较和运算应该特别小心。尽量避免使用JavaScript进行精确和密集的计算。根据国际标准IEEE754,JavaScript浮点数的64个二进制位,从最左边开始,是这样组成的。*第1位:符号位,0表示正数,1表示负数*第2位至第12位:存放指数部分*第13位至第64位:存放小数部分(即有效数)符号位决定了一个符号数的指数部分决定了数值的大小,小数部分决定了数值的精度。IEEE754规定第一有效位默认始终为1,不存储在64位浮点数中。也就是说,有效数总是1.xx...xx的形式,其中xx..xx部分存储的是64位浮点数,最长可能是52位。因此,JavaScript提供了最多53位二进制数字的有效数字(64位浮点数的最后52位+第一个有效数字的1)。内部表达式:(-1)^signbit*1.xx...xx*2^exponentbit精度最多只能达到53位二进制数,表示绝对值小于2的53次方整数,即-(253-1)到253-1可以精确表达。大多数后端语言,如C++、Java、Python等,最多可以支持64位的long类型,所以long类型的数据从后端语言传递给JavaScript时会被截断。这种情况一般使用String来处理。如果你需要在JavaScript中进行长时间的计算,你需要自己实现一个计算器。有了自己注入JavaScript引擎的想法,接下来就是分析可行性了。大多数JavaScript引擎都是用C++编写的。如果你自己的程序使用C++,你可以很容易地注入它。如果是OC,可以OC和C++混合使用。其他语言怎么破?要在静态语言和动态语言JavaScript上互相调用,最方便的方法是找一个用这种语言实现的JavaScript引擎(开源),直接集成注入。如果不是,则需要额外使用一层桥接,将这种语言的接口暴露给C++,然后C++实现的JavaScript引擎会注入接口供JavaScript使用。服务端集成思路&实践:我们都知道nodeJS在nodeJS中做桥接,但是nodeJS的运行依赖于谷歌的V8引擎,V8是用C++实现的,底层使用C++实现底层功能,比如网络和数据库IO,并暴露一个构造函数接口注入上下文。注意,这里暴露的只是一个构造函数接口,而不是创建的实例。然后实现一个require钩子函数。使用require加载JS模块时,与在网页中使用AMD的require没有区别。当使用require加载系统库时,它是一个C++模块,它会调用暴露的构造函数接口来获取实例对象。不管是加载JS模块还是C++模块,得到的都可以看做是一个ModuleObject。Node会将加载的模块缓存在binding_cache中。下次在其他代码中使用require加载模块时,会先去binding_cache中搜索,如果找到则返回模块对象,如果没有找到则执行上面的加载过程。这就是node的基本原理:C++封装底层操作,通过V8注入,让JS脚本具备网络和IO能力。上面说的基于Spring的桥接都是C++级别的应用,那么经典的Java怎么玩呢?难道Java一定要是静态语言,就没有办法像C++一样利用JS的动态特性吗?当然不是。这时候我们就需要说说Rhino这个前面介绍过的JS引擎。Rhino完全用Java编写。可以想象,Rhino几乎就是为Java应用而生的。使用方法如下:1.首先在我们的Java应用中集成Rhino;2、所有的IO操作、网络操作等,都封装成服务,提供增删改查、setter&&getter等多种方法3、通过spring,将这些服务bean注入到Rhino中;4.将业务逻辑写到JS代码中,JS代码调用多个注入的Java服务处理业务逻辑,并将数据组装回来!好处:修改业务逻辑不需要修改Java代码,也就是不需要重新编译部署,只需要刷新Rhino中运行的JS代码即可。过去Java应用的痛点之一就是部署,需要重新编译、打包、部署,重启服务器。现在开发成这种形式,可以实现服务器的热更新和热部署。既可以享受到Java服务的稳定可靠,又可以享受到JS的灵活性。这种技术和用法几乎在十年前就存在了。基于EMC著名的商业产品Documentum,前EMC工程师设计了一套面向中小企业的Java开源CMS系统Alfresco,并在该系统中实现了该技术。这个技术是基于spring的,叫做spring-surf,它做了一个胶水层。也算是十年前的节点了。Demo,一个使用spring-surf框架的系统中的webscript模块。1、categorynode.get.xml定义了URL拦截器和权限控制;2、.get表示正在处理GET请求,RESTful;3、在categorynode.get.js中调用注入的JavaBean处理业务逻辑;4.如果是网页请求返回.html.ftl,如果是Ajax,返回.json.ftl;(这里使用FreeMarker模板引擎)categorynode.get.desc.xmlcategorynode.get.jscategorynode.get.html.ftlcategorynode.get.json.ftl移动端集成思路&实践:ReactNative中的ReactNativebridge在目前,RN程序的运行依赖于Facebook的RN框架。在iOS、Android模拟器或者真机上,ReactNative使用的是JavaScriptCore引擎,也就是Safari使用的JavaScript引擎。但是,JavaScriptCore在iOS上并没有使用即时编译技术(JIT),因为iOS中的应用程序没有权限拥有可写和可执行的内存页(因而不能动态生成代码),在Android上,它可以理论上使用。JavaScriptCore引擎也是用C++编写的。在iOS和Android中,JavaScriptCore都有一层封装,所以你不需要关心桥接引擎和系统的层。iOS/Android系统通过JavaScriptCore引擎注入各种自定义的原生组件,如listview、text等。Cocos2d-JS中的bridgecocos2dx是游戏开发中非常常用的游戏渲染引擎。它拥有一系列产品,例如:cocos2dx(C++)、cocos2d-lua(lua)、cocos2d-js(JavaScript)等众多产品。其中,cocos2dx的JS版本cocos2d-js是最新退出的。相比C++和lua,编写游戏渲染特效代码非常方便。对于需要经常更新的渲染场景,C++是静态语言,每次修改都需要重新编译运行,显然是不合适的。自然也想到了脚本语言,lua和js,都有些类似,都是动态语言,只需要集成一个运行引擎,提供一个运行容器运行,同时通过脚本调用的引擎。lua的优点是精简,语法简化,引擎页面很小很简单,所以代码量必然会比js多,学习成本比较高。js的优点是有ECMAScrtpt的核心,语法比较丰富,支持一些高级属性。在cocos2d-js中,cocos2dx(C++)集成了SpiderMonkey(C++)作为JS运行引擎,中间做了一个胶水层作为JSBinding,通过引擎注入一个cc全局对象,映射单个实例底层C++C++实例。表面上写的是JS代码,实际操作的是底层的C++。cocos2d-js是一个可以在多种环境下运行的代码。在web环境下运行时,使用cocos2d-html5引擎,底层运行canvas;在客户端运行时,使用的是cocos2dx引擎,底层运行是C++,然后C++控制openGL进行绘制和渲染。提供相同的API对开发者来说几乎是透明的,没有区别,开发者只需要关注实现效果即可。实现一套代码,多终端操作(网页,客户端)。JSPatch技术中的桥接JSPatch是目前iOS上流行的热修复技术。JSPatch之所以能够通过JS调用和重写OC方法的根本原因在于Objective-C是一门动态语言,OC上的所有方法调用/类的生成都是在运行时通过Objective-CRuntime进行的,我们可以得到对应的通过类名/方法名反射的类和方法。JSPatch的基本原理是:JS传递字符串给OC,OC通过Runtime接口调用和替换OC的方法。其中一个关键技术就是JS和OC的消息交互。JSPatch包含一个JS引擎JavaScriptCore(Safari,与ReactNative使用的模型相同)。使用JavaScriptCore接口,OC端在启动JSPatch引擎时会创建一个JSContext实例。JSContext是JS代码的执行环境。可以在JSContext中添加方法,JS可以直接调用这个方法。本质上是通过JavaScriptCore引擎注入,暴露OC方法供JS调用,实现动态修改OC反射。Demo、iOS热更新、热修复:1、集成JavaScriptCore引擎;2、通过引擎桥接JS和OC;3.通过JS修改OC反射。关于JSPatch技术的详细介绍请访问:https://github.com/bang590/JSPatch/wiki关于JavaScript引擎:有4种JavaScript引擎可以在iOS或android上运行:JavaScriptCore、SpiderMonkey、V8、Rhino。下表显示了每个引擎在iOS和Android上的兼容性。JavaScript引擎iOSAndroidJavaScriptCoreInterpreteronly(interpretermodeonly)InterpreterandJIT(interpretermodeandjust-in-timecompilationmode)因为iOS平台不支持JIT即时编译,而V8只有JIT模式,所以V8不能iOS平台上使用(越狱设备除外,想体验iOSJIT的同学可以自行越狱)。因此,目前能够跨越iOS和Android平台的JS引擎只有两种,分别是SpiderMonkey和JavaScriptCore。JavaScript引擎受很多因素的影响,例如交叉编译器的版本、引擎的版本和操作系统的类型。至于如何选择,可以参考:《Part I: How to Choose a JavaScript Engine for iOS and Android Development》至此,JavaScript从立足前端到征服全端的逆袭之路可以概括为“携引擎造世界”。不足之处请各位评委轻拍~