從前端到全端:JavaScript逆襲之路

發表於2018-05-29

近年來,前端技術日新月異,前端已經不僅僅是網頁,更多的開始由狹義向廣義發展。

先後湧現出了具備後端能力的node,具備移動開發能力的react native,具備遊戲渲染能力的cocos2d-js,以及iOS上的熱修復技術JSPatch等等新技術。

咋一看,幾乎各個端都被JavaScript攻陷,大有一統江湖之勢。

究竟,JavaScript如何做到上天入地無所不能?JavaScript真的能一統江湖嗎?

image

亂世出英雄:JavaScript的誕生故事要從JavaScript的由來說起。

高能瞎扯淡版,正經臉的同學可以忽略

有人的地方就有江湖,有江湖的地方就有紛爭。

故事要從當年的瀏覽器之戰說起。

時間回到1994年,

(→ 那時候我還是個寶寶~ #天真臉#)

景兄弟橫空出世,並自帶神器網景導航,戰鬥力爆表,勢如劈竹,瞬時間威震天下。

一出世就武裝到牙齒,武力值這麼高還自帶兵器,這個科學嗎?

港真,我也覺得不科學,也許跟熊孩子哪吒、女漢子雅典娜是一個品種吧?

這一切北方的老前輩微軟大溼,都看在眼裡,不甘天下盡歸景兄弟這個初出茅廬的毛孩子,大溼積澱多年,潛心修煉一年,終於帶著大殺器IE 1.0出關了,誓於景兄弟爭個高低。

自此景兄弟的網景導航 VS 微軟大溼的IE 的軍備戰爭開始。

景兄弟仔細掂量,微軟大溼財大氣粗,內功深厚,臣妾實在是辦不到啊啊啊啊啊啊。

景兄弟緊急召集門人商議對策,有一門人曰:”以我們微薄之力硬磕,是萬萬使不得的。如今我們,一是宜施行合縱之策,抱大腿,組成聯盟!二是避其鋒芒,出奇招致勝。“

於是景兄弟依照此策略,一方面找到了當時德高為重的另一位前輩SUN,組成了開發者聯盟。

(微軟大溼:握草,聯盟都粗來了,那我是不是得搞個部落?)

另一方面,景兄弟找到了鍛造大師布蘭登,請布大師幫忙升級兵器網景導航,大師就是大師,不費吹灰之力就完成了強化升級,然而布大師突發奇想,本來這是近距離攻擊兵器,要是有多一個遠距離攻擊的能力那豈不是更好?Just do it. 想罷大師就加了一個遠距離攻擊的feature。於是有了自帶遠距離攻擊能力的網景導航2.0。景兄弟一看這麼流弊心裡甚是歡喜,不過遠距離攻擊的技能叫做LiveScript,感覺不是特別Fashion。特然想到這不是跟SUN前輩聯盟嘛,SUN家的Java正是獨霸武林之時。不如把名字改成跟Java有關,蹭一把東風,蹭點光環。一拍腦袋,JavaScript!!!眾門人一聽:”好好好,JavaScript 流弊炫酷吊炸天!“

果然第一節下半場,景兄弟攜強化過的網景導航2.0 戰個痛快,那是槓槓的!人家一問,你咋還能遠端攻擊,你這個遠端攻擊用的是啥?答曰:JavaScript。“JavaScript,一定是跟SUN家Java是一個系列產品,一定很流弊!”#光環加成,各種膜拜臉#

微軟大溼虧了一場,痛定思痛,也要搞遠端攻擊功能,果然不久,就祭出了同樣帶有遠端攻擊能力的IE 3.0,鑑於景兄弟的遠端攻擊叫做JavaScript,J開頭的感覺應該比較流弊,所以微軟大溼的叫做JScript。

然後戰爭就從地面貼身肉搏戰,開始逐步升級到了遠距離核戰爭。

正所謂,城門失火,殃及池魚。這麼打下去苦逼的是搬磚的頁面仔,就是我這種,到處都是雷區,無處下腳。

最後到了1997年,“聯合國安理會祕書長”艾瑪(ECMA)出來調停,多方簽署了“核不擴散條約”,約束各種遠端攻擊武器的使用,這才走上了正軌。

1995年SUN開發了Java技術,這是第一個通用軟體平臺。Java擁有跨平臺、物件導向、泛型程式設計的特性,廣泛應用於企業級Web應用開發和移動應用開發。Java也伴隨著網際網路的迅猛發展而發展,逐漸成為重要的網路程式語言。名噪一時。

1994年Netscape公司成立,並推出了自己的瀏覽器的免費版本 Netscape Navigator,很快就佔有了瀏覽器市場。到了 1995 年,微軟公司開始加入,並很快釋出了自己的 Internet Explorer 1.0。

1995年,當時在Netscape就職的Brendan Eich(布蘭登·艾克),正為Netscape Navigator 2.0瀏覽器開發的一門名為LiveScript的指令碼語言,後來Netscape與Sun Microsystems組成的開發聯盟,為了讓這門語言搭上Java這個程式語言“熱詞”,將其臨時改名為“JavaScript”,日後這成為大眾對這門語言有諸多誤解的原因之一。

JavaScript最初受Java啟發而開始設計的,目的之一就是“看上去像Java”,因此語法上有類似之處,一些名稱和命名規範也借自Java。但JavaScript的主要設計原則源自Self和Scheme。JavaScript與Java名稱上的近似,是當時Netscape為了營銷考慮與SUN達成協議的結果。

> 所以,JavaScript和Java其實沒有半毛錢關係。

JavaScript推出後在瀏覽器上大獲成功,微軟在不久後就為Internet Explorer 3.0瀏覽器推出了JScript,以與處於市場領導地位的Netscape產品同臺競爭。JScript也是一種JavaScript實現,這兩個

JavaScript語言版本在瀏覽器端共存意味著語言標準化的缺失,對這門語言進行標準化被提上了日程,在1997年,由Netscape、SUN、微軟、寶藍等公司組織及個人組成的技術委員會在ECMA(歐洲計算機制造商協會)確定定義了一種名叫ECMAScript的新指令碼語言標準,規範名為ECMA-262。JavaScript成為了ECMAScript的實現之一。ECMA-262 第五版,即是ES5。

> ECMA-262,包括ES5, ES6等是一個標準,JavaScript是ECMAScript的一個實現。

完整的JavaScript實現應該包含三個部分:

image

在網景導航2.0和IE 3.0出現之後的幾年間,網景和微軟公司不停的釋出新版本的瀏覽器,支援更多的新功能。自此拉開了瀏覽器之戰的序幕。這場瀏覽器之戰到現在還在繼續,以下一張圖看清楚過程。

image

從瀏覽器之戰可以看出,各家瀏覽器比拼的大致兩個方面視覺體驗(渲染排版)和速度(指令碼執行)。

> 所以一個完整的瀏覽器組成,至少包含兩個部分:

image

補充一個市面常見瀏覽器的核心和JavaScript引擎搭配:

image

其他JavaScript引擎,Rhino,由Mozilla基金會管理,開放原始碼,完全以Java編寫,可以看做SpiderMonkey的Java版。

注意:webkit不單單只是一個排版引擎,webkit = 排版引擎 + JavaScript引擎。

> 所以,JavaScript是動態語言,它的執行都是基於JavaScript引擎,引擎大都是由靜態語言實現C++、Java、and so on。JavaScript的能力也是由引擎賦予。不管是瀏覽器環境中是window,亦或是node環境中的process,均是由引擎提供。

(番外:Mozilla的人不知道為啥特別喜歡猴子,經常以猴子命名技術,所以看到帶Monkey的,十有八九估計是他們搞的。)

諾曼底登陸:JavaScript Binding/Bridge 橋接技術

在瀏覽器環境中,DOM、BOM、window物件、setTimeout/setInterval,alert,console等方法均不是JavaScript自身具備的能力,而是瀏覽器native實現,然後通過JavaScript引擎注入到JS執行的全域性上下文中,供JS使用。

鑑別方式,在偵錯程式console中打出來,帶有[native code]的即是:

image

講道理:

  1. JavaScript執行 → 依賴於JavaScript引擎 ← 瀏覽器整合了JavaScript引擎,同時通過JavaScript引擎注入native程式碼工JS指令碼使用
  2. 發散一下思維,只要有JavaScript引擎,就能執行JS指令碼,不管有沒有瀏覽器!只是缺少瀏覽器提供的alert,window等方法。
  3. 既然瀏覽器可以往JavaScript引擎中注入程式碼,賦予JS指令碼在網頁中特殊的能力,同理我們可以自己整合JavaScript引擎,自己定義自己的方法往JavaScript引擎中注入,賦予JS更多更強的自定義能力!

    注入的關鍵是:值型別相互對應,Obj對映class的一個例項,function對映一個控制程式碼或者引用

    image

JavaScript數值型中的坑

JavaScript內部,所有數字都是以64位浮點數形式儲存,即使整數也是如此

這就是說,在JavaScript語言的底層,根本沒有整數,所有數字都是小數(64位浮點數)。容易造成混淆的是,某些運算只有整數才能完成,此時JavaScript會自動把64位浮點數,轉成32位整數,然後再進行運算。由於浮點數不是精確的值,所以涉及小數的比較和運算要特別小心。儘量避免使用JavaScript做精準計算和密集計算。

image

根據國際標準IEEE 754,JavaScript浮點數的64個二進位制位,從最左邊開始,是這樣組成的。

  • 第1位:符號位,0表示正數,1表示負數
  • 第2位到第12位:儲存指數部分
  • 第13位到第64位:儲存小數部分(即有效數字)

    符號位決定了一個數的正負,指數部分決定了數值的大小,小數部分決定了數值的精度。

    IEEE 754規定,有效數字第一位預設總是1,不儲存在64位浮點數之中。也就是說,有效數字總是1.xx…xx的形式,其中xx..xx的部分儲存在64位浮點數之中,最長可能為52位。因此,JavaScript提供的有效數字最長為53個二進位制位(64位浮點的後52位+有效數字第一位的1)。

內部表現公式:(-1)^符號位 1.xx…xx 2^指數位

精度最多隻能到53個二進位制位,這意味著,絕對值小於2的53次方的整數,即-(253-1)到253-1,都可以精確表示。

而大部分的後端語言,C++、Java、Python等的long型都是可以支援到64位,因此long型資料從後端語言傳給JavaScript會發生低位截斷。遇到這種情況一般使用String處理,如需要在JavaScript中做long型計算,需要自行實現計算器。

image

有了自行往JavaScript引擎中注入的想法,接下來就是分析可行性。

大部分是JavaScript引擎是使用C++編寫,如果自己的程式使用的是C++可以很方便的進行注入,如果是OC,可以使用OC和C++混編的形式。

其他語言怎麼破?

要在一門靜態語言上與動態語言JavaScript相互呼叫,最便捷的方式是找到一個這門語言實現的JavaScript引擎(開源),直接進行整合,注入。如果沒有,則需要使用多一層橋接,把這門語言的介面暴露給C++,再由C++實現的JavaScript引擎將介面注入供JavaScript使用。

服務端整合思路&實踐:

image

nodeJS中的橋接

我們都知道nodeJS,但是nodeJS的執行依賴於Google的V8 引擎,V8是C++實現,底層使用C++實現底層功能,比如網路,資料庫IO,對外暴露一個構造器介面注入到上下文中,注意此處暴露的只是一個構造器介面而不是一個建立完的例項。然後實現了一個require的hook函式。當使用require載入一個JS模組時,跟網頁中使用AMD 的require並無異樣,當使用require載入系統庫,既是C++的模組時,會呼叫暴露出來的構造器介面,得到一個例項物件。不管是裝載JS模組還是裝載C++模組,得到的都可以看做是一個Module Object,node會將裝載完的模組快取到bindingcache中,下次在別處的程式碼中使用require裝載模組時,就會先去bindingcache中查詢,如果找到了則返回該module object,如果沒找到再執行上面的裝載流程。

這就是node的基本原理:C++封裝底層操作,通過V8注入,使得JS指令碼有網路和IO能力

基於Spring的橋接

以上說到的幾個都是C++層面的應用,那麼經典的Java怎麼玩?是不是Java就必須是靜態語言的玩法,沒有辦法像C++之類的,可以使用JS的動態特性?

當然不是。這個時候,我們需要說起前面介紹過的一個JS引擎 Rhino,Rhino是完全由Java編寫,可想而知,Rhino幾乎就是為Java應用而生的。

用法是這樣:

  1. 首先在我們的Java應用中整合Rhino;
  2. 所有的IO操作,網路操作等,都封裝成service,並提供增刪改查,setter && getter等多種方法
  3. 通過spring,把這些service bean注入到Rhino中;
  4. 把業務邏輯寫到JS程式碼中,JS程式碼呼叫多個已注入的Java service處理業務邏輯,拼裝資料返回!

好處:修改業務邏輯不需要修改Java程式碼,也就是不需要重新編譯和部署,只需要重新整理下跑在Rhino中的JS程式碼即可。以往Java應用的一個痛點是部署,需要重新編譯,打包,部署重啟伺服器,現在以這種形式開發,可以達到服務端的熱更新和熱部署。既可以享有Java服務的穩定性和可靠性,又可以享有JS的靈活性。

這種技術和用法在差不多十年前就有過,前EMC的工程師基於EMC著名的商業產品Documentum,設計了一套Java開源的中小企業CMS系統Alfresco,在該系統中實現了這種技術,這種技術基於spring,叫做spring-surf,做了一個膠水層。可以看做小十年前的node吧。

Demo,使用spring-surf框架的系統中一個webscript模組

image

  1. categorynode.get.xml定義URL攔截器和許可權控制;
  2. .get指明是處理GET請求,RESTful;
  3. 在categorynode.get.js中呼叫已注入的Java Bean處理業務邏輯;
  4. 若為網頁請求返回.html.ftl,若為Ajax,返回.json.ftl;

(此處配套使用的是FreeMarker模板引擎)

> categorynode.get.desc.xml

image

> categorynode.get.js

image

> categorynode.get.html.ftl

image

> categorynode.get.json.ftl

image

移動端整合思路&實踐:

image

React Native中的橋接

React Native目前也是異常火爆,RN程式的執行依賴於Facebook的RN框架。在iOS、Android的模擬器或是真機上,React Native使用的是JavaScriptCore引擎,也就是Safari所使用的JavaScript引擎。但是在iOS上JavaScriptCore並沒有使用即時編譯技術(JIT),因為在iOS中應用無權擁有可寫可執行的記憶體頁(因而無法動態生成程式碼),在安卓上,理論上是可以使用的。JavaScriptCore引擎也是使用C++編寫,在iOS和安卓中,JavaScriptCore都做了一層封裝,可以無須關心引擎和系統橋接的那一層。iOS/Android系統通過JavaScriptCore引擎將定製好的各種原生元件注入,如:listview,text等。

Cocos2d-JS中的橋接

cocos2dx是遊戲開發中非常常用的遊戲渲染引擎,有一系列的產品,如: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執行引擎,中間做了一個膠水層既是JS Binding,通過引擎注入了一個cc的全域性物件,對映的是底層C++的一個單例C++例項。表面上寫的是JS程式碼,實際上操作的是底層的C++。cocos2d-js是程式碼可以執行在多種環境中,當執行的網頁環境中時,使用的是cocos2d-html5引擎,底層操作的是canvas;當執行在客戶端上時,使用的是cocos2dx引擎,底層操作的是C++,再由C++去操控openGL做繪製和渲染。提供相同的API,對開發者幾乎是透明無差異的,開發者只需要關注實現效果即可。達到一套程式碼,多端執行(網頁端,客戶端)。

image

JSPatch技術中的橋接

JSPatch是目前比較流行的iOS上的熱修復技術,JSPatch 能做到通過 JS 呼叫和改寫 OC 方法最根本的原因是 Objective-C 是動態語言,OC 上所有方法的呼叫/類的生成都通過 Objective-C Runtime 在執行時進行,我們可以通過類名/方法名反射得到相應的類和方法。JSPatch 的基本原理就是:JS 傳遞字串給 OC,OC 通過 Runtime 介面呼叫和替換 OC 方法。

關鍵技術之一是 JS 和 OC 之間的訊息互傳。JSPatch裡包含了,一個JS引擎JavaScriptCore(Safari,React Native用的同款)。用到了 JavaScriptCore 的介面,OC 端在啟動 JSPatch 引擎時會建立一個 JSContext 例項,JSContext 是 JS 程式碼的執行環境,可以給 JSContext 新增方法,JS 就可以直接呼叫這個方法。本質上就是通過JavaScriptCore引擎注入,暴露OC的方法供JS呼叫來實現動態修改OC的反射。

Demo,iOS熱更新,熱修復:

  1. 整合JavaScriptCore引擎;
  2. 通過引擎,橋接JS和OC;
  3. 通過JS修改OC反射。

image

詳細的JSPatch技術介紹請移步:bang590/JSPatch

關於JavaScript引擎:

在iOS 或 android 上能夠執行的JavaScript 引擎有4個:JavaScriptCore,SpiderMonkey,V8,Rhino。下面這個表格展示各個引擎在iOS 和 Android 的相容性。

image

因為iOS平臺不支援JIT即時編譯,而V8只有JIT模式,所以V8無法在iOS平臺使用(越獄裝置除外,想體驗iOS JIT的同學可以自行越獄)。

所以,目前可以做到橫跨iOS和Android雙平臺的JS引擎,只有兩款,即是SpiderMonkey和JavaScriptCore。

JavaScript引擎會受很多東西影響,比如交叉編譯器的版本、引擎的版本和作業系統的種類等。

至於如何選擇,可以參考:《Part I: How to Choose a JavaScript Engine for iOS and Android Development》

至此,JavaScript從立足於前端,到征戰全端的逆襲之路,可以總結為“攜引擎以令天下”。

不足之處,還請各位看官輕拍~

參考文章:

bang590/JSPatch中文參考文件
Cocos2d-JS | Cocos2d-x官方參考文件
Alfresco官方參考文件
《Browser Wars: The End or Just the Beginning?》 《Part I: How to Choose a JavaScript Engine for iOS and Android Development》 《React Native 從入門到原始碼》

相關文章