面試官: 聊一聊Babel

来源:https://www.cnblogs.com/duxinyi/archive/2019/09/24/11576583.html
-Advertisement-
Play Games

點擊關註本 "公眾號" 獲取文檔最新更新,並可以領取配套於本指南的 《前端面試手冊》 以及 最標準的簡歷模板 . 前言 Babel 是現代 JavaScript 語法轉換器,幾乎在任何現代前端項目中都能看到他的身影,其背後的原理對於大部分開發者還屬於黑盒,不過 Babel 作為一個工具真的有瞭解背後 ...


點擊關註本公眾號獲取文檔最新更新,並可以領取配套於本指南的 《前端面試手冊》 以及最標準的簡歷模板.

前言

Babel 是現代 JavaScript 語法轉換器,幾乎在任何現代前端項目中都能看到他的身影,其背後的原理對於大部分開發者還屬於黑盒,不過 Babel 作為一個工具真的有瞭解背後原理的必要嗎?

如果只是 Babel 可能真沒有必要,問題是其背後的原理在我們開發中應用過於廣泛了,包括不限於: eslint jshint stylelint css-in-js prettier jsx vue-template uglify-js postcss less 等等等等,從模板到代碼檢測,從混淆壓縮到代碼轉換,甚至編輯器的代碼高亮都與之息息相關.

如果有興趣就可以搞一些黑魔法: 前端工程師可以用編譯原理做什麼?

前置

Babel 大概分為三大部分:

  • 解析: 將代碼(其實就是字元串)轉換成 AST( 抽象語法樹)
  • 轉換: 訪問 AST 的節點進行變換操作生成新的 AST
  • 生成: 以新的 AST 為基礎生成代碼

我們主要通過打造一個微型 babel 來瞭解 babel 的基本原理,這個微型 babel 的功能很單一也很雞肋,但是依然有400行代碼,其實現細節與 babel 並不相同,因為我們省去了很多額外的驗證和信息解析,因為單單一個相容現代 JavaScript 語法的 parser 就需要5000行代碼,並不利於我們快速瞭解 babel 的基本實現,所以這個微型 babel可以說比較雞肋(因為除了展示之外沒啥用處),但是比較完整展示了 babel 的基本原理,你可以以此作為入門,在入門之後如果仍有興趣,可以閱讀:

  • estree規範
  • acorn: 輕量級現代 JavaScript 解析器, babel 最初就是基於此項目

代碼解析

parser 概念

代碼解析,也就是我們常說的 Parser, 用於將一段代碼(文本)解析成一個數據結構.

例如這段 es6的代碼

const add = (a, b) => a + b

我們用 babel 解析後便是這種形式:

{
  "type": "File",
  "start": 0,
  "end": 27,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 1,
      "column": 27
    }
  },
  "program": {
    "type": "Program",
    "start": 0,
    "end": 27,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 1,
        "column": 27
      }
    },
    "sourceType": "module",
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 0,
        "end": 27,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 1,
            "column": 27
          }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 6,
            "end": 27,
            "loc": {
              "start": {
                "line": 1,
                "column": 6
              },
              "end": {
                "line": 1,
                "column": 27
              }
            },
            "id": {
              "type": "Identifier",
              "start": 6,
              "end": 9,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 6
                },
                "end": {
                  "line": 1,
                  "column": 9
                },
                "identifierName": "add"
              },
              "name": "add"
            },
            "init": {
              "type": "ArrowFunctionExpression",
              "start": 12,
              "end": 27,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 12
                },
                "end": {
                  "line": 1,
                  "column": 27
                }
              },
              "id": null,
              "generator": false,
              "expression": true,
              "async": false,
              "params": [
                {
                  "type": "Identifier",
                  "start": 13,
                  "end": 14,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 13
                    },
                    "end": {
                      "line": 1,
                      "column": 14
                    },
                    "identifierName": "a"
                  },
                  "name": "a"
                },
                {
                  "type": "Identifier",
                  "start": 16,
                  "end": 17,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 16
                    },
                    "end": {
                      "line": 1,
                      "column": 17
                    },
                    "identifierName": "b"
                  },
                  "name": "b"
                }
              ],
              "body": {
                "type": "BinaryExpression",
                "start": 22,
                "end": 27,
                "loc": {
                  "start": {
                    "line": 1,
                    "column": 22
                  },
                  "end": {
                    "line": 1,
                    "column": 27
                  }
                },
                "left": {
                  "type": "Identifier",
                  "start": 22,
                  "end": 23,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 22
                    },
                    "end": {
                      "line": 1,
                      "column": 23
                    },
                    "identifierName": "a"
                  },
                  "name": "a"
                },
                "operator": "+",
                "right": {
                  "type": "Identifier",
                  "start": 26,
                  "end": 27,
                  "loc": {
                    "start": {
                      "line": 1,
                      "column": 26
                    },
                    "end": {
                      "line": 1,
                      "column": 27
                    },
                    "identifierName": "b"
                  },
                  "name": "b"
                }
              }
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  }
}

我們以解析上面的 es6箭頭函數為目標,來寫一個簡單的 parser.

文本 ---> AST 的過程中有兩個關鍵步驟:

  • 詞法分析: 將代碼(字元串)分割為token流,即語法單元成的數組
  • 語法分析: 分析token流(上面生成的數組)並生成 AST

詞法分析(Tokenizer -- 詞法分析器)

要做詞法分析,首先我們需要明白在 JavaScript 中哪些屬於語法單元

  • 數字:JavaScript 中的科學記數法以及普通數組都屬於語法單元.
  • 括弧:『(』『)』只要出現,不管任何意義都算是語法單元
  • 標識符:連續字元,常見的有變數,常量(例如: null true),關鍵字(if break)等等
  • 運算符:+、-、*、/等等
  • 當然還有註釋,中括弧等

在我們 parser 的過程中,應該換一個角度看待代碼,我們平時工作用的代碼.本質是就是字元串或者一段文本,它沒有任何意義,是 JavaScript 引擎賦予了它意義,所以我們在解析過程中代碼只是一段字元串.

仍然以下麵代碼為例

const add = (a, b) => a + b

我們期望的結果是類似這樣的

[
  { type: "identifier", value: "const" },
  { type: "whitespace", value: " " },
  ...
]

那麼我們現在開始打造一個Tokenizer(詞法分析器)


// 詞法分析器,接收字元串返回token數組
export const tokenizer = (code) => {

    // 儲存 token 的數組
    const tokens  = [];

    // 指針
    let current = 0;

    while (current < code.length) {
        // 獲取指針指向的字元
        const char = code[current];

        // 我們先處理單字元的語法單元 類似於`;` `(` `)`等等這種
        if (char === '(' || char === ')') {
            tokens.push({
                type: 'parens',
                value: char,
            });

            current ++;

            continue;
        }

        // 我們接著處理標識符,標識符一般為以字母、_、$開頭的連續字元
        if (/[a-zA-Z\$\_]/.test(char)) {
            let value = '';
            value += char;
            current ++;

            // 如果是連續字那麼將其拼接在一起,隨後指針後移
            while (/[a-zA-Z0-9\$\_]/.test(code[current]) && current < code.length) {
                value += code[current];
                current ++;
            }

            tokens.push({
                type: 'identifier',
                value,
            });

            continue;
        }


        // 處理空白字元
        if (/\s/.test(char)) {
            let value = '';
            value += char;
            current ++;

            //道理同上
            while (/\s]/.test(code[current]) && current < code.length) {
                value += code[current];
                current ++;
            }

            tokens.push({
                type: 'whitespace',
                value,
            });

            continue;
        }


        // 處理逗號分隔符
        if (/,/.test(char)) {

            tokens.push({
                type: ',',
                value: ',',
            });

            current ++;
            continue;
        }

        // 處理運算符
        if (/=|\+|>/.test(char)) {
            let value = '';
            value += char;
            current ++;

            while (/=|\+|>/.test(code[current])) {
                value += code[current];
                current ++;
            }

            // 當 = 後面有 > 時為箭頭函數而非運算符
            if (value === '=>') {
                tokens.push({
                    type: 'ArrowFunctionExpression',
                    value,
                });
                continue;
            }

            tokens.push({
                type: 'operator',
                value,
            });

            continue;
        }

        // 如果碰到我們詞法分析器以外的字元,則報錯
        throw new TypeError('I dont know what this character is: ' + char);
    }

    return tokens;
};

那麼我們基本的詞法分析器就打造完成,因為只針對這一個es6函數,所以沒有做額外的工作(額外的工作量會非常龐大).

const result = tokenizer('const add = (a, b) => a + b')

console.log(result);

/**
[ { type: 'identifier', value: 'const' },
  { type: 'whitespace', value: ' ' },
  { type: 'identifier', value: 'add' },
  { type: 'whitespace', value: ' ' },
  { type: 'operator', value: '=' },
  { type: 'whitespace', value: ' ' },
  { type: 'parens', value: '(' },
  { type: 'identifier', value: 'a' },
  { type: ',', value: ',' },
  { type: 'whitespace', value: ' ' },
  { type: 'identifier', value: 'b' },
  { type: 'parens', value: ')' },
  { type: 'whitespace', value: ' ' },
  { type: 'ArrowFunctionExpression', value: '=>' },
  { type: 'whitespace', value: ' ' },
  { type: 'identifier', value: 'a' },
  { type: 'whitespace', value: ' ' },
  { type: 'operator', value: '+' },
  { type: 'whitespace', value: ' ' },
  { type: 'identifier', value: 'b' } ]
**/

1.3 語法分析

語法分析要比詞法分析複雜得多,因為我們接下來的是示意代碼,所以做了很多“武斷”的判斷來省略代碼,即使這樣也是整個微型 babel 中代碼量最多的.

語法分析之所以複雜,是因為要分析各種語法的可能性,需要開發者根據token流(上一節我們生成的 token 數組)提供的信息來分析出代碼之間的邏輯關係,只有經過詞法分析 token 流才能成為有結構的抽象語法樹.

做語法分析最好依照標準,大多數 JavaScript Parser 都遵循estree規範

由於標準內容很多,感興趣的可以去閱讀,我們目前只介紹幾個比較重要的標準:

語句(Statements): 語句是 JavaScript 中非常常見的語法,我們常見的迴圈、if 判斷、異常處理語句、with 語句等等都屬於語句

// 典型的for 迴圈語句
for (var i = 0; i < 7; i++) {
  console.log(i);
}

表達式(Expressions): 表達式是一組代碼的集合,它返回一個值,表達式是另一個十分常見的語法,函數表達式就是一種典型的表達式,如果你不理解什麼是表達式, MDN上有很詳細的解釋.

// 函數表達式
var add = function(a, b) {
  return  a + b
}

聲明(Declarations): 聲明分為變數聲明和函數聲明,表達式(Expressions)中的函數表達式的例子用聲明的寫法就是下麵這樣.

// 函數聲明
function add(a, b) {
  return a + b
}

你可能有點糊塗,為了理清其中的關係,我們就下麵的代碼為例來解讀

// 函數表達式
var add = function(a, b) {
  return  a + b
}

首先這段代碼的整體本質是是一個變數聲明(VariableDeclarator):

而變數被聲明為一個函數表達式(FunctionExpression):

函數表達式中的大括弧在內的為塊狀語句(BlockStatement):

塊狀語句內 return 的部分是返回語句(ReturnStatement):

而 return 的其實是一個二元運算符或者叫二元表達式(BinaryExpression):

上面提到的這些有些屬於表達式,有些屬於聲明也有些屬於語句,當然還有更多我們沒提到的,它們被語法分析之後被叫做AST(抽象語法樹).

我們做語法分析的時候思路也是類似的,要分析哪一層的 token 到底屬於表達式或者說語句,如果是語句那麼是塊狀語句(BlockStatement)還是Loops,如果是 Loops 那麼屬於while 迴圈(WhileStatement)還是for 迴圈(ForStatement)等等,其中甚至難免要考慮作用域的問題,因此語法分析的複雜也體現在此.


const parser = tokens => {
    // 聲明一個全時指針,它會一直存在
    let current = -1;

    // 聲明一個暫存棧,用於存放臨時指針
    const tem = [];

    // 指針指向的當前token
    let token = tokens[current];

    const parseDeclarations = () => {

        // 暫存當前指針
        setTem();

        // 指針後移
        next();

        // 如果字元為'const'可見是一個聲明
        if (token.type === 'identifier' && token.value === 'const') {
            const declarations = {
                type: 'VariableDeclaration',
                kind: token.value
            };

            next();

            // const 後面要跟變數的,如果不是則報錯
            if (token.type !== 'identifier') {
                throw new Error('Expected Variable after const');
            }

            // 我們獲取到了變數名稱
            declarations.identifierName = token.value;

            next();

            // 如果跟著 '=' 那麼後面應該是個表達式或者常量之類的,額外判斷的代碼就忽略了,直接解析函數表達式
            if (token.type === 'operator' && token.value === '=') {
                declarations.init = parseFunctionExpression();
            }

            return declarations;
        }
    };

    const parseFunctionExpression = () => {
        next();

        let init;
        // 如果 '=' 後面跟著括弧或者字元那基本判斷是一個表達式
        if (
            (token.type === 'parens' && token.value === '(') ||
            token.type === 'identifier'
        ) {
            setTem();
            next();
            while (token.type === 'identifier' || token.type === ',') {
                next();
            }

            // 如果括弧後跟著箭頭,那麼判斷是箭頭函數表達式
            if (token.type === 'parens' && token.value === ')') {
                next();
                if (token.type === 'ArrowFunctionExpression') {
                    init = {
                        type: 'ArrowFunctionExpression',
                        params: [],
                        body: {}
                    };

                    backTem();

                    // 解析箭頭函數的參數
                    init.params = parseParams();

                    // 解析箭頭函數的函數主體
                    init.body = parseExpression();
                } else {
                    backTem();
                }
            }
        }

        return init;
    };

    const parseParams = () => {
        const params = [];
        if (token.type === 'parens' && token.value === '(') {
            next();
            while (token.type !== 'parens' && token.value !== ')') {
                if (token.type === 'identifier') {
                    params.push({
                        type: token.type,
                        identifierName: token.value
                    });
                }
                next();
            }
        }

        return params;
    };

    const parseExpression = () => {
        next();
        let body;
        while (token.type === 'ArrowFunctionExpression') {
            next();
        }

        // 如果以(開頭或者變數開頭說明不是 BlockStatement,我們以二元表達式來解析
        if (token.type === 'identifier') {
            body = {
                type: 'BinaryExpression',
                left: {
                    type: 'identifier',
                    identifierName: token.value
                },
                operator: '',
                right: {
                    type: '',
                    identifierName: ''
                }
            };
            next();

            if (token.type === 'operator') {
                body.operator = token.value;
            }

            next();

            if (token.type === 'identifier') {
                body.right = {
                    type: 'identifier',
                    identifierName: token.value
                };
            }
        }

        return body;
    };

    // 指針後移的函數
    const next = () => {
        do {
            ++current;
            token = tokens[current]
                ? tokens[current]
                : { type: 'eof', value: '' };
        } while (token.type === 'whitespace');
    };

    // 指針暫存的函數
    const setTem = () => {
        tem.push(current);
    };

    // 指針回退的函數
    const backTem = () => {
        current = tem.pop();
        token = tokens[current];
    };

    const ast = {
        type: 'Program',
        body: []
    };

    while (current < tokens.length) {
        const statement = parseDeclarations();
        if (!statement) {
            break;
        }
        ast.body.push(statement);
    }
    return ast;
};

至此我們暴力 parser 了token 流,最終得到了簡陋的抽象語法樹:


{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "identifierName": "add",
            "init": {
                "type": "ArrowFunctionExpression",
                "params": [
                    {
                        "type": "identifier",
                        "identifierName": "a"
                    },
                    {
                        "type": "identifier",
                        "identifierName": "b"
                    }
                ],
                "body": {
                    "type": "BinaryExpression",
                    "left": {
                        "type": "identifier",
                        "identifierName": "a"
                    },
                    "operator": "+",
                    "right": {
                        "type": "identifier",
                        "identifierName": "b"
                    }
                }
            }
        }
    ]
}

代碼轉換

如何轉換代碼?

在 Babel 中我們使用者最常使用的地方就是代碼轉換,大家常用的 Babel 插件就是定義代碼轉換規則而生的,而代碼解析和生成這一頭一尾都主要是 Babel 負責。

比如我們要用 babel 做一個React 轉小程式的轉換器,babel工作流程的粗略情況是這樣的:

  1. babel 將 React 代碼解析為抽象語法樹
  2. 開發者利用 babel 插件定義轉換規則,根據原本的抽象語法樹生成一個符合小程式規則的新抽象語法樹
  3. babel 則根據新的抽象語法樹生成代碼,此時的代碼就是符合小程式規則的新代碼

例如 Taro就是用 babel 完成的小程式語法轉換.

到這裡大家就明白了,我們轉換代碼的關鍵就是根據當前的抽象語法樹,以我們定義的規則生成新的抽象語法樹,轉換的過程就是生成新抽象語法樹的過程.

遍歷抽象語法樹(實現遍歷器traverser)

抽象語法樹是一個樹狀數據結構,我們要生成新語法樹,那麼一定需要訪問 AST 上的節點,因此我們需要一個工具來遍歷抽象語法樹的節點.

const traverser = (ast, visitor) => {

    // 如果節點是數組那麼遍曆數組
    const traverseArray = (array, parent) => {
        array.forEach((child) => {
            traverseNode(child, parent);
        });
    };

    // 遍歷 ast 節點
    const traverseNode = (node, parent) => {
        const method = visitor[node.type];

        if (method) {
            method(node, parent);
        }

        switch (node.type) {
        case 'Program':
            traverseArray(node.body, node);
            break;

        case 'VariableDeclaration':
            traverseArray(node.init.params, node.init);
            break;

        case 'identifier':
            break;

        default:
            throw new TypeError(node.type);
        }
    };
    traverseNode(ast, null);
};

轉換代碼(實現轉換器transformer)

我們要轉換的代碼const add = (a, b) => a + b其實是個變數聲明,按理來講我們要轉換為es5的代碼也應該是個變數聲明,比如這種:

var add = function(a, b) {
  return  a + b
}

當然也可以不按規則,直接生成一個函數聲明,像這樣:

function add(a, b) {
  return a + b
}

這次我們把代碼轉換為一個es5的函數聲明

我們之前的遍歷器traverser接收兩個參數,一個是 ast 節點對象,一個是 visitor,visitor本質是掛載不同方法的 JavaScript 對象,visitor 也叫做訪問者,顧名思義它會訪問 ast 上每個節點,然後根據針對不同節點用相應的方法做出不同的轉換.

const transformer = (ast) => {

    // 新 ast
    const newAst = {
        type: 'Program',
        body: []
    };

    // 在老 ast 上加一個指針指向新 ast
    ast._context = newAst.body;

    traverser(ast, {
        // 對於變數聲明的處理方法
        VariableDeclaration: (node, parent) => {
            let functionDeclaration = {
                params: []
            };
            if (node.init.type === 'ArrowFunctionExpression') {
                functionDeclaration.type = 'FunctionDeclaration';
                functionDeclaration.identifierName = node.identifierName;
            }


            if (node.init.body.type === 'BinaryExpression') {
                functionDeclaration.body = {
                    type: 'BlockStatement',
                    body: [{
                        type: 'ReturnStatement',
                        argument: node.init.body
                    }],
                };
            }

            parent._context.push(functionDeclaration);
        },

        //對於字元的處理方法
        identifier: (node, parent) => {
            if (parent.type === 'ArrowFunctionExpression') {
            // 忽略我這暴力的操作....領略大意即可..
                ast._context[0].params.push({
                    type: 'identifier',
                    identifierName: node.identifierName
                });
            }
        }
    });

    return newAst;
};

生成代碼(實現生成器generator)

我們之前提到過,生成代碼這一步實際上是根據我們轉換後的抽象語法樹來生成新的代碼,我們會實現一個函數, 他接受一個對象( ast),通過遞歸生成最終的代碼

const generator = (node) => {
    switch (node.type) {
    // 如果是 `Program` 結點,那麼我們會遍歷它的 `body` 屬性中的每一個結點,並且遞歸地
    // 對這些結點再次調用 codeGenerator,再把結果列印進入新的一行中。
    case 'Program':
        return node.body.map(generator)
            .join('\n');

    // 如果是FunctionDeclaration我們分別遍歷調用其參數數組以及調用其 body 的屬性
    case 'FunctionDeclaration':
        return 'function' + ' ' + node.identifierName + '(' + node.params.map(generator) + ')' + ' ' + generator(node.body);

    // 對於 `Identifiers` 我們只是返回 `node` 的 identifierName
    case 'identifier':
        return node.identifierName;

    // 如果是BlockStatement我們遍歷調用其body數組
    case 'BlockStatement':
        return '{' + node.body.map(generator) + '}';

    // 如果是ReturnStatement我們調用其 argument 的屬性
    case 'ReturnStatement':
        return 'return' + ' ' + generator(node.argument);
    
    // 如果是ReturnStatement我們調用其左右節點並拼接
    case 'BinaryExpression':
        return generator(node.left) + ' ' + node.operator + ' ' + generator(node.right);

    // 沒有符合的則報錯
    default:
        throw new TypeError(node.type);

    }
};

至此我們完成了一個簡陋的微型 babel,我們開始試驗:

const compiler = (input) => {
    const tokens = tokenizer(input);
    const ast =  parser(tokens);
    const newAst = transformer(ast);
    const output = generator(newAst);

    return output;
};

const str = 'const add = (a, b) => a + b';

const result = compiler(str);

console.log(result);
// function add(a,b) {return a + b}

我們成功地將一個es6的箭頭函數轉換為es5的function函數.

最後

我們可以通過這個微型 babel 瞭解 babel 的工作原理,如果讓你對編譯原理產生興趣並去深入那是更好的, babel集合包 是有數十萬行代碼的巨大工程,我們用區區幾百行代碼只能展示其最基本的原理,代碼有很多不合理之處,如果想真正的瞭解 babel 歡迎閱讀器源碼.

前端可以利用編譯原理相關的東西還有很多,除了我們常見的es6轉換工具 babel,代碼檢測的 eslint等等,我們還可以:

  1. 小程式多端轉義Taro
  2. 小程式熱更新js 解釋器
  3. babel與錯誤監控瀏覽器端 JavaScript 異常監控
  4. 模板引擎
  5. css 預處理後處理等等
  6. ...

這篇文章受the-super-tiny-compiler啟發而來.


公眾號

想要實時關註筆者最新的文章和最新的文檔更新請關註公眾號程式員面試官,後續的文章會優先在公眾號更新.

簡歷模板: 關註公眾號回覆「模板」獲取

《前端面試手冊》: 配套於本指南的突擊手冊,關註公眾號回覆「fed」獲取

2019-08-12-03-18-41


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一.示例代碼 註意:如果進行本地測試請在右上角 `本地設置 不校驗合法性打鉤` 二.參數 "微信官方解釋" 參數 | 屬性 | 類型 | 預設值 | 必填 | 說明 | | : | : : | : : | | | | url | string | | 是 | 開發者伺服器介面地址 | | data ...
  • layui獲取全部覆選框checkbox選中的值,layui獲取表單開關switch的值 ...
  • 學習jQuer對錶單、表格操作的過程中,按照書上的例子發現一個問題: 以下代碼同樣使用prop()函數,使用attr()方法也不能實現預期 ...
  • 小程式製作扭蛋機 2019-09-24 13:26:53 公司要製作活動小程式,其中有一個扭蛋機的效果實現抽獎的功能。在網上找了好久竟沒有找到(不知道是不是我找代碼的方式有問題)。最後還是自己做一個吧- _ - ,效果如下: 1.wxml 當然我這裡沒有用wx:for遍歷 2.wxss 這一步比較麻 ...
  • 跨域:顧名思義,跨埠,功能變數名稱,協議都算跨域, 平常中請求後臺,發送http請求,就一般用的就是axios跟jquery,用這個兩個發送請求時,在同域也就是不跨域條件下了瀏覽器會自動帶cookie 那現在webpack他有了proxy設置,就是解決了跨域問題,也就是說如果我本地項目想要請求一個http ...
  • webpack在build包的時候,有時候會遇到打包時間很長的問題,這裡提供了一個解決方案,讓打包如絲般順滑~ 1. 介紹 在用 Webpack 打包的時候,對於一些不經常更新的第三方庫,比如 ,`lodash vue` 我們希望能和自己的代碼分離開,Webpack 社區有兩種方案 CommonsC ...
  • 最近朋友圈和微博都刷了一波傑倫的回憶殺–說好不哭,想想都9012了,在學習react如火如荼的路上,也不妨停下腳步來總結總結,朝花夕拾一下。 為了便於闡述,我們還是來段小明和禪師的故事吧。 小明在學習路上遇到了一些問題,於是有了以下對話: <1> npm 對 yarn 小明:經歷了從 npm -> ...
  • 瀏覽器與新技術 面試題來源於我的項目 "「前端面試與進階指南」" 本章關於瀏覽器原理部分的內容主要來源於 "瀏覽器工作原理" ,這是一篇很長的文章,可以算上一本小書了,有精力的非常建議閱讀。 常見的瀏覽器內核有哪些? | 瀏覽器/RunTime | 內核(渲染引擎) | JavaScript 引擎 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...