1. 程式人生 > >使用模擬器混淆前端程式碼

使用模擬器混淆前端程式碼

前言

很多時候,我們都會覺得混淆指令碼程式是件困難的事,效果遠不及傳統程式的混淆力度。畢竟,指令碼的初衷就是簡單易用。諸多先天不足的特徵,使得混淆難以深入實施。

然而從理論上這似乎也說不通,只要是圖靈完備的語言,解決問題的能力都是相同的。舉個最簡單的例子,網上有使用 JavaScript 實現的 x86 模擬器,我們拋開效能不說,單論功能,它和本地系統是一樣的。因此使用傳統工具混淆的程式,同樣也是能在瀏覽器中執行的!

當然,這個代價不免有些太大。為了保護一段邏輯,還得載入一個龐大的模擬器和作業系統,顯然是難以接受的。但是這個思路還是很有意義的 —— 將需要保護的程式碼邏輯,放入模擬器中執行。

事實上類似的方案也早已存在,例如大名鼎鼎的 VMProtect。在瀏覽器端同樣也有應用的案例,例如 Google 曾經開發的 reCaptcha 驗證系統,也用到了模擬器來保護重要邏輯。

如何將前端指令碼程式,變成可被模擬器執行的指令?我們從最簡單的案例開始講解。

位元組碼

和傳統的編譯型程式不同,指令碼程式始終是帶語法的文字程式碼。如何將一段充滿各種可讀單詞的程式碼,儘可能多得使用數字來描述?例如這段程式碼:

var el = document.createElement('script');
el.text = 'alert(123)';
document.body.appendChild(el);

其中就有變數名 el、字串 'script'、全域性變數 document、屬性 body 等可讀單詞。

對於變數名來說,普通的壓縮工具就能很好處理,變成諸如 a、b、c 這樣的短名字;但是字串和屬性,又該如何處理?

熟悉 JS 的都知道 obj.keyobj['key'] 是相等的。而且全域性變數都是 window 下的屬性。因此,我們可把全域性變數和屬性都變成字串的形式:

var el = window['document']['createElement']('script');
el['text'] = 'alert(123)';
window['document']['body']['appendChild'](el);

這時,整個程式碼中除了 window 之外,都是字串了。

既然我們的目標是將程式碼數字化,那就將數字以外的常量都提取出來,放到一個單獨的數組裡:

var MEM = [
    window, 'document', 'createElement', 'script',
    'text', 'alert(123)', 'body', 'appendChild'
];

這樣,就可以用 MEM[數字] 代替一切了:

var el = MEM[0][ MEM[1] ][ MEM[2] ]( MEM[3] );
el[ MEM[4] ] = MEM[5];
MEM[0][ MEM[1] ][ MEM[6] ][ MEM[7] ](el);

看起來有些眼花繚亂了吧。不過這只是對常量進行替換,語法仍然存在,因此還是能推測出大致的邏輯。不少基於語法樹的混淆工具,大多就到這一步。

下面我們進一步,將語法展開:

var A, X, Y, Z

A = MEM[0]       //  window
X = MEM[1]       //  'document'
X = A[X]         //  X = window['document']

A = MEM[2]       //  'createElement'
Y = MEM[3]       //  'script'
A = X[A](Y)      //  A = document['createElement']('script')

Y = MEM[4]       //  'text'
Z = MEM[5]       //  'alert(123)'
A[Y] = Z         //  A['text'] = 'alert(123)'

Y = MEM[6]       //  'body'
X = X[Y]         //  X = document['body']

Y = MEM[7]       //  'appendChild'
X[Y](A)          //  body['appendChild'](A)

由於失去了語法,因此需要一些臨時變數來儲存中間值,這裡使用 A、X、Y、Z 四個變數來暫存。

這時的每一步,都是一個基本操作。我們到了指令碼層面最低階的形式。(可以試著粘到控制檯,仍能正常執行~ 或者點選 jsfiddle.net/qLtojr5z/ 演示)

觀察上述程式碼,其中有大量相似操作,我們嘗試用代號來進行替換。例如讀取 MEM[i] 操作,使用 LDR(Load Reg)來描述:

r = MEM[i]      =>    LDR r, i

同樣的,屬性讀寫操作,也進行類似替換:

r1 = r2[r3]     =>    GET r1, r2, r3
r1[r2] = r3     =>    SET r1, r2, r3

對於方法呼叫操作,暫且用 CAL 來表示引數正好為 1 個的情況,並且返回值統一存放在 A 中:

A = r1[r2](r3)  =>    CAL r1, r2, r3

現在,我們用這個幾個虛擬代號,重新描述上述邏輯:

LDR A, 0
LDR X, 1
GET X, A, X

LDR A, 2
LDR Y, 3
CAL X, A, Y

LDR Y, 4
LDR Z, 5
SET A, Y, Z

LDR Y, 6
GET X, X, Y

LDR Y, 7
CAL X, Y, A

這是不是有一種彙編指令的感覺!之後的處理過程自然就很明確了,我們將這些可讀的文字彙編碼,轉換成二進位制位元組碼。

例如用 1 代表 LDR 指令,2 代表 GET 指令。。。同樣的,暫存器也可以用數字表示,例如用 0 代表 A ,1 代表 X。。。

彙編碼 位元組碼
LDR A, 5 01 00 00 05
GET X, Y, Z 02 01 02 03
SET Z, Y, X 03 03 02 01
... ...

於是之前那段程式邏輯,最終就能用純數字表示了:

01 00 00 00 01 01 00 01 02 01 00 01 01 00 00 02
01 02 00 03 04 01 00 02 01 02 00 04 01 03 00 05
03 00 02 03 01 02 00 06 02 01 01 02 01 02 00 07
04 01 02 00

注意,這部分只是程式邏輯的指令資料,那些字串等常量資料並不在此,需要另外儲存。

模擬器

我們的位元組碼在瀏覽器看來,只是一堆資料而已,並無實際意義。因此需要一個模擬器,來解釋執行這些資料。

模擬器聽起來高大上,其實原理是非常簡單的 —— 根據指令資料,做相應操作而已。例如遇到 1,執行讀取儲存操作;遇到 2,執行訪問屬性操作。。。

REG = [];   // 暫存器

do {
    opcode = MEM[pc++];

    switch (opcode) {
        case 1:     // LDR
            ...
        case 2:     // GET
            ...
        case 3:     // SET
            r1 = MEM[pc++];
            r2 = MEM[pc++];
            r3 = MEM[pc++];

            obj = REG[r1];
            key = REG[r2];
            val = REG[r3];

            obj[key] = val;
        ...
    }
} while (...)

[執行演示]

我們將位元組碼當做二進位制資料載入到儲存中,然後使用一個計數器,指向當前指令所在的儲存位置,暫且稱之 pc(program counter)。每執行一條指令,pc 進行相應增加,指向下一條指令。周而復始。

這樣,一個模擬器的雛形就出現了。

我們可以新增更多的指令,例如算數、位運算等等,使模擬器變得更完善。同一個指令,也可以有多種模式。例如 LDR 指令,地址可以是立即數、暫存器,或是 暫存器+立即數、暫存器+暫存器 等多種模式,方便各種定址操作。

指令越豐富,相應的邏輯實現就越簡單。相反,指令越少,同樣的操作就需要多個指令組合才能完成。一個極端的例子就是 Brainfuck 程式,它只提供極少的指令,因此即便非常簡單的功能,也需要大量冗長的組合才能完成。

當然,指令越豐富模擬器也會越龐大,因此得根據實際需求折中考慮。

跳轉指令

程式不可能永遠都是順著執行的,否則一下就執行完了。因此還需跳轉操作,可反覆執行先前指令。最簡單的跳轉,就是無條件跳轉,我們暫且用 JMP(Jump)來表示:

Label:
  ...
  JMP Label

和傳統語言 BASIC 或 C 的 goto 一樣,在彙編文字層面,可以使用 label 作為跳轉的目標。當然 label 只是個標記而已,並不存在於最終的位元組碼中。最終儲存的,只是目標指令所在的位置。

因此當模擬器解釋 JMP 指令時,僅僅是修改 pc 而已:

...
switch (opcode) {
    ...
    case OP_JMP:
        ...
        pc = r;
    ...
}

有跳轉指令,我們就可以靈活操控流程,完全不必按照 JS 那死板的流程控制了。

事實上,這個指令集和 JS 原始碼已經毫無關係。我們完全可以使用其他語言,編譯出相應的虛擬指令。最終的位元組碼,顯然也是無法還原出 語義化 的 JS 程式碼的。

分支指令

除了無條件跳轉,還有帶條件的。例如這段程式碼:

var str = prompt('password'); 

if (str == 'hello') {
    alert('OK');
} else {
    alert('Fail');
}

按照先前的方式,我們將其轉換成最低階的 JS 程式碼:

var MEM = [window, 'prompt', 'password', 'hello', 'alert', 'OK', 'Fail']
var A, X, Y, Z

X = MEM[0]      // X = window
A = MEM[1]
Y = MEM[2]
A = X[A](Y)     // A = window['prompt']('password')

Y = MEM[3]      // Y = 'hello'

if (A == Y)
    A = MEM[5]  // A = 'OK'
else
    A = MEM[6]  // A = 'Fail'

Y = MEM[4]
X[Y](A)         // window['alert'](A)

相比之前,現在多了判斷操作。因此,我們再新增一個帶條件的跳轉指令。例如當 r1 != r2 時執行跳轉:

JNE r1, r2, label

這樣,我們就能和 JMP 指令組合,來表達上述邏輯了:

  ...                 ; 註釋
  LDR Y, 3            ; Y = 'hello'

  JNE A, Y, L_ELSE    ; if (A != Y) goto L_ELSE

  LDR A, 5            ; A = 'OK'
  JMP L_END
L_ELSE:
  LDR A, 6            ; A = 'Fail'
L_END:
  ...
  CAL X, Y, A         ; alert(A)

有了 != 判斷,自然也可實現 == 判斷。不過為了方便使用,我們可提供更豐富的分支操作。例如 JS 中的各種判斷:

跳轉指令 條件 備註
JE r1 == r2 Jump if Equal
JNE r1 != r2 Jump if Not Equal
JES r1 === r2 Jump if Equal Strict
JNES r1 !== r2 Jump if Not Equal Strict
JG r1 > r2 Jump if Greater
JGE r1 >= r2 Jump if Greater or Equal
JL r1 < r2 Jump if Less
JLE r1 <= r2 Jump if Less or Equal
JIN r1 in r2 Jump if IN
JINSOF r1 instanceof r2 Jump if INStanceOF

甚至對於一些常見情況,還可再進一步封裝:

跳轉指令 條件
JTRUE r1 === true
JFALSE r1 === false
JZERO r1 === 0
JNULL r1 === null
JUNDEF r1 === undefined
... ...

不過,有時我們只想判斷,未必要跳轉。例如:

isOK = (stat == 200);

對於這種情況,使用跳轉指令也能滿足,只是顯得略為累贅。如果想更精簡,則可新增純粹的判斷指令,例如:

A = (r1 != r2)    =>    TEST_NE r1, r2
A = (r1 in r2)    =>    TEST_IN r1, r2
...

當然,其本質都是一樣的。

JS 操作

既然我們的模擬器是用於瀏覽器環境,顯然應該提供完善的 JS/DOM 操作。因此我們再新增幾個指令碼相關的指令,例如:

指令 功能 備註
CONCAT r1, r2, r3 r1 = r2 + r3 字元拼接
OBJECT r1 r1 = {} 建立物件
TYPEOF r1, r2 r1 = typeof r2 typeof
DELETE r1, r2 delete r1[r2] delete
NEWCAL r1, ... A = new r1(...) new

這裡提一下 JS 的 + 操作符:它既可以用於數字加法,也可用於字串拼接。為了不和 ADD 指令混在一起,我們可單獨提供一個字串拼接的指令。

現在來思考一個問題:如何提供回撥函式?

從理論上說,我們可實現一個完全相容 JS 的位元組碼模擬器,但事實上這是相當複雜的。JS 有眾多靈活的特徵,例如閉包、with、eval 等等,要實現這些,相當於得重新造一個 JS 引擎,顯然是不現實的。

因此,我們只需提供一些常用的操作就可以了。閉包之類的特性,就可以不考慮了。不過回撥函式還是需要支援的,例如這段程式碼:

button.onclick = function() { ... };

我們可設計一個指令,將相應的 label 封裝成一個函式物件:

FUN  r, label      ; r = makeCallback(...)

label:
  ...

這樣,就能提供給 DOM 使用了:

L_CLICK:
  ...

L_MAIN:
  ...                ; A = button, X = 'onclick'

  FUNC  Y, L_CLICK   ; Y = makeCallback(...)
  SET   A, X, Y      ; A['onclick'] = Y

至於封裝的細節,大致就這樣:

function makeCallback(pc) {
    return function() {
        return vm.run(pc);
    };
}

在回撥函式裡,讓模擬器從 pc 的位置開始解釋,這樣就讓某些指令非同步執行了。

這裡簡單的演示一下。例如這個回撥函式:

var i = 0;

function render() {
    txt.value = i++;
    if (i <= 255) {
        requestAnimationFrame(render);
    }
}
render();

將其轉換成位元組碼:

0000    05 03 00 00             MOV Z, 0
0004    01 00 00 00  L_TIMER:   LDR A, 0
0008    01 01 00 01             LDR X, 1
000C    02 02 00 01             GET Y, A, X
0010    01 01 00 02             LDR X, 2
0014    03 02 01 03             SET Y, X, Z
0018    06 03 00 00             INC Z
001C    05 01 00 ff             MOV X, 255
0020    07 03 01 10             JG  Z, X, L_END
0024    01 01 00 03             LDR X, 3
0028    08 02 00 04             FUN Y, L_TIMER
002C    04 00 01 02             CAL A, X, Y
0030    00 00 00 00  L_END:     BRK

執行演示

在指令碼層面上還有個特殊流程,那就是錯誤捕獲。例如這樣的 JS 邏輯:

try {
    // safe
} catch (...) {
    // handler
}

這使用指令並不難描述。我們可定義兩個指令,分別用於捕獲的開啟和關閉:

  CATCH L_ERR
  ...           ; safe
  ...
  UNCATCH
  ...
L_ERR:
  ...           ; handler

當模擬器遇到 CATCH 指令時,使用 try 解釋後續指令,若有錯誤發生,則進入 label 的位置;當遇到 UNCATCH 指令時,則退出當前遞迴,返回上一層的捕獲:

function run(...) {
    ...
    case OP_CATCH:
        try {
            run(...);     // 安全模式 遞迴
        } catch (e) {
            pc = ...      // 錯誤處理流程
        }
        ...
    case OP_UNCATCH:
        return;

這樣,就能放心地執行一些可能報錯的操作了。

類似的邏輯實現還有很多,這裡就不詳細介紹了。關於模擬器的基本原理簡介,就到此為止。不過我們的目標並非只是為了實現一個模擬器,而是利用模擬器來保護程式碼邏輯。

邏輯保護

相比過去那些基於 AST(抽象語法樹)的混淆方案,使用模擬器可以實施得更深入。大致可以在這幾點上對抗:

  • 編譯過程

  • 指令編碼

  • 指令混淆

編譯過程

從源程式到位元組碼,需要一個編譯的過程。這個過程本身就有一定的混淆效果,例如一些優化工作會對邏輯進行調整。和傳統的編譯型語言一樣,這個過程是不可逆的。反編譯的程式碼,是很難回到原始語義的。(不知大家是否見過那些自稱能把 exe 程式還原成 c 程式碼的工具,結果當然是慘不忍睹)

由於模擬器難以完全相容 JS 所有的特性,因此不能直接用於現有的指令碼。需混淆的程式碼必須遵循一定的規範編寫,例如不能使用 with、eval 等高階特性。所以,不推薦對整個程式都進行混淆,而是隻針對一些核心邏輯。

如果核心部分只是演算法,甚至完全可以不用 JS 編寫,而是選擇 C 這種更適合計算的語言。我們可以使用 clang 編譯出 LLVM 中間碼,然後開發一個 LLVM Backend 外掛,將中間碼編譯成我們模擬器的目標指令。

LLVM 是個非常有意義的系統。它不僅可用於程式的優化,同樣也可實現程式的「劣化」,讓邏輯變得更亂更難分析。例如在計算過程中,插入大量的中間步驟,干擾邏輯的分析。

指令編碼

因為模擬器的指令是我們自創的,所以對方在逆向分析之前,必須瞭解指令的編碼格式,才能成功反編譯。因此,在編碼上又可以進行一些對抗。

傳統的指令編碼大多都有規律,因為那是從解碼複雜度以及效能上考慮。例如:

switch (opcode) {
    ...
    case OP_SET:
        r1 = MEM[pc++];
        r2 = MEM[pc++];
        r3 = MEM[pc++];
        ...

這麼簡單明瞭的解碼過程,顯然是很容易分析的。而我們最終目標是混淆,效能並非是第一位。因此可使出各種千奇百怪的編碼格式,來增加解碼的複雜度。

例如,使用各種邏輯位運算,並且不同的指令格式也各不相同,沒有任何規律。在效能損失可接受的範圍內,將解碼過程變得極其複雜,使分析變得更困難。

a = MEM[pc++]
b = MEM[pc++]
if (a & 128)
    if (a & 64)    // OP_SET
        r1 = (a >> 4) & 16
        r2 = (b & 16) ^ ~r1
        r3 = (b >> 4 & 16) ^ r1
        ...

當然再複雜的格式也有破解的時候。因此我們不能永遠使用一種格式,而必須不定期的進行升級。不過,每次升級都得重新設計一遍,會不會很麻煩?

如果編碼格式由人工制定,那顯然是很麻煩的。因此必須藉助工具,自動化生成「編碼器」和「直譯器」。我們只需設計一些策略就可以了,讓工具將這些套路隨機組合,生成千奇百怪的格式。最終格式是什麼樣的,我們自己都不需要了解:)

總之,用最簡單的正向設計達到最困難的逆向分析,這就符合對抗的意義了。

指令混淆

指令本身也是記憶體中的資料。因此和普通資料一樣,指令資料也能被修改,例如當前指令可以修改即將執行的下一條指令,這樣就可以在執行時動態調整程式行為了。

利用這個特徵,我們可對程式的大部分指令事先進行加密,然後在執行時再逐步解密。假如程式有 a、b、c、d 幾個部分,我們事先將 b、c、d 部分進行簡單加密,只保留明文的 a 部分。

當程式執行 a 部分時,將 b 部分的二進位制資料進行解密,還原出明文指令;執行到 b 部分時,還原 c 部分,同時再將 a 部分加密回去。。。這樣變執行邊釋放,就能避免一出來就能看到所有指令,從而增加分析成本。

另外,在位元組碼的層面上,跳轉是以位元組為單位的,因此可跳到某個指令的中間:

位置     位元組碼          彙編碼
0000    02 01 02 03    GET X, Y, Z
0004    05 00 01       JMP 0001

這樣就能執行 01 02 03 05 這串位元組碼,即 LDR Y, 0x0305 了。利用這個方法,就可以將一些指令偽裝起來,實現花指令的效果。

類似的對抗思路還有很多,這裡就不詳細討論了。事實上,這些大多是傳統程式的混淆方案,之所以能用到 JS 上,得益於模擬器消除了平臺間的差距,從而使得前端指令碼也能享受到前人積累的對抗技術,完全不必自創一些看似炫酷實則毫無意義的混淆方案。

相關推薦

使用模擬器混淆前端程式碼

前言 很多時候,我們都會覺得混淆指令碼程式是件困難的事,效果遠不及傳統程式的混淆力度。畢竟,指令碼的初衷就是簡單易用。諸多先天不足的特徵,使得混淆難以深入實施。 然而從理論上這似乎也說不通,只要是圖靈完備的語言,解決問題的能力都是相同的。舉個最簡單的例子,網上有使用 JavaScript 實現的 x86 模擬

前端程式碼混淆工具javascript-obfuscator

安裝: sudo npm install -g javascript-obfuscator 可能遇到的問題: npm WARN saveError ENOENT: no such file or directory, open '/Users/t

程式設計師用軟體生成前端程式碼前端妹子看完直接傻眼!卻遭領導批評

作為程式設計師經常會有一些口頭禪,比如說“程式碼複用思想”,“寫高質量的程式碼”,“程式碼在精不在多”,“思路比努力更重要”,這些話中大多強調的是策略的重要性,在工作中,勤奮固然重要,如果能在勤奮的基礎上強調一下方法與策略,那便會使工作起到事半功倍的效果,最近有一個程式設計師網友異於其他程式設計師,

webpack使用來打包前端程式碼

使用webpack打包js檔案(隔行變色案例) 1.webpack安裝的兩種方式 執行npm i webpack -g全域性安裝webpack,這樣就能在全域性使用webpack的命令 在專案根目錄中執行npm i webpack --save-dev安裝到專案依賴中 2.初步使用webp

前端程式碼規範大全

初衷 不管參與專案的人數有多少,確保每一行程式碼都像是同一個人編寫的; 根據實際情況制定良好的程式碼規範; 遵守編碼風格使程式碼更容易維護,對長期專案大有裨益; 實施程式碼規範增加程式碼可讀性,提高協作開發效率; 實施程式碼規範減少低

在工作中如何優化前端程式碼

此為知乎問答,我把我的答案稍作整理放到這裡來了。 原則 首先說一個最重要的優化原則:程式碼優化是每天都要進行的,而不是一兩個月做一次大優化,那時做就已經晚了。另外由於優化是每天做的,所以你不需要一次的就過度優化,保持小步快跑即可。 這個原則為什麼重要?因為很多程式設計師會在寫程式碼的時候說「先不優化

大公司裡如何開發和部署前端程式碼

  這是一個非常有趣的 非主流前端領域,這個領域要探索的是如何用工程手段解決前端開發和部署優化的綜合問題,入行到現在一直在學習和實踐中。 在我的印象中,facebook是這個領域的鼻祖,有興趣、有梯子的同學可以去看看facebook的頁面原始碼,體會一下什麼叫工程化。

前端熱部署(更改前端程式碼,不用重啟tomcat,重新整理瀏覽器就可以)

注:如果使用Springboot可以是一個devtool實現熱部署 主要在於 On frame deactivation選項配置選擇為 Update classes and resourses(當且僅當在Deployment配置頁,對應的目的war形式配置為exploded

Prettier 1.15.3 釋出,前端程式碼格式化“神器”

Prettier 是一個“有主見”的程式碼格式化工具,能夠使輸出程式碼保持風格一致。支援列表: JavaScript,包括 ES2017 JSX Flow TypeScript CSS、LESS 和 SCSS JSON GraphQL Prettier

前端程式碼佈置到伺服器端後找不到靜態資源

將前端頁面程式碼佈置到伺服器SpringMVC後,經常出現靜態資源找不到的問題。 首先,應該設定springMVC,使其不要攔截靜態資源。在springMVC的配置檔案中新增如下程式碼: <mvc:annotation-driven /> <mvc:resource

webpack打包nodejs專案(前端程式碼

適用情況 首先說明,此情況不具備普遍性。若你的情況與筆者類似那麼希望這篇文章能夠幫到你。 我的專案情況是這樣的:用node.js做後臺,ejs做模板引擎(即整個頁面是一個ejs檔案)由node.js將資料渲染完成後,再將完整頁面返回給使用者。 那麼這樣做會遇到的問題: 1. 本專案沒有html頁面,ejs的作

Web前端程式碼規範

Web技術中心程式碼規範 前言 軟體的長期價值直接源於其編碼質量。在它的整個生命週期裡,一個程式可能會被許多人閱讀或修改。如果一個程式可以清晰的展現出它的結構和特徵,那就能減少在以後對其進行修改時出錯的可能性,程式設計規範可以幫助程式設計師們增加程式的健壯性。基本所有的前

使用VS2013自帶的PreEmptive Dotfuscator and Analytis來混淆C#程式碼

1. 使用VS2013編譯你要打包的程式,會在資料夾中的 ..\bin\Release中 2. 點選VS2013中的TOOLS -> PreEmptive Dotfuscator and Analytics :                   開啟的介面如

前端程式碼靜態檢測

在日常的開發工作中,我們很容易犯一些比較低階的錯誤,比如少了一個右括號,多了一個點號或者少了一個點號等等,有時候這些錯誤排查起來甚至很費時間,那麼今天,我們就來聊聊這個問題的一些解決辦法: 在我們的日常開發中,只需要用上程式碼的靜態檢測就可以很好的規避這些問題,下面我們將介紹在前端開發中

編寫靈活、穩定、高質量的前端程式碼的規範一(推薦收藏)

編寫靈活、穩定、高質量的HTML程式碼的規範 雖然現在前端很多都已經成為SPA(單頁面應用)開發,或許連渲染的html都是自動生成的.但是歸根結底,還是html和css,必要時候我們還是需要好好了解這些基礎的開發規範 當然這都是基礎性的規範,和公司規範的並不衝突.你也可以當作課外知識閱讀~ 一、唯一定律

編寫靈活、穩定、高質量的前端程式碼的規範二(推薦收藏)

編寫靈活、穩定、高質量的HTML程式碼的規範二 一、程式碼組織 1.1 注意 (1)以元件為單位組織程式碼段。 (2)制定一致的註釋規範。 (3)使用一致的空白符將程式碼分隔成塊,這樣利於掃描較大的文件。 (4)如果使用了多個 CSS 檔案,將其按照元件而非頁面的形式分拆,因為頁面會被重組,而元件只會被移

如何獲取小程式前端程式碼

準備材料 1.第一步下載一個模擬器(推薦使用自帶root的模擬器),我使用的是夜神模擬器 下載地址:https://www.yeshen.com/download/fullPackage 2.node.js執行環境 下載地址:https://nodejs.org/en/ 3.反編譯的指令

MyEclipse匯入前端程式碼所有的js檔案出錯

MyEclipse提供比較嚴謹的js校驗功能,因此ExtJs、jQuery等前端框架匯入到MyEclipse後均會提示錯誤,只需將校驗去掉即可 方法一: 1、在MyEclipse選擇選單欄window 2、選擇preferences 3、左側選單樹中展開myeclipse 4、選擇下面的valida

Linux安裝npm並打包前端程式碼

檢視node版本$ node -v檢視npm版本$ npm -v如果沒有安裝node及npm,需要先安裝node及npm#yum install node# yum install npm安裝cnpm並切換淘寶映象(使用國外映象速度慢)$ npm install -g cnpm --registry=http

前端程式碼壓縮的方式

Html程式碼壓縮 1.使用線上網站進行壓縮 2.nodejs提供了html-minifier工具 3.後端模板引擎渲染壓縮 Css程式碼壓縮 1.使用線上網站進行壓縮 2.使用html-minifier對html中的css進行壓縮 3.使用clean-css對css進行