1. 程式人生 > 程式設計 >在JavaScript中如何使用巨集詳解

在JavaScript中如何使用巨集詳解

在語言當中,巨集常見用途有實現 DSL 。通過巨集,開發者可以自定義一些語言的格式,比如實現 jsX 語法。在 WASM 已經實現的今天,用其他語言來寫網頁其實並不是沒有可能。像 Rust 語言就帶有強大的巨集功能,這使得基於 Rust 的 Yew 框架,不需要實現類似 Babel 的東西,而是靠語言本身就能實現類似 JSX 的語法。 一個 Yew 元件的例子,支援類 JSX 的語法。

impl Component for MyComponent {
    // ...

    fn view(&self) -> Html {
        let onclick = self.link.callback(|_| Msg::Click);
        html! {
            <button onclick=onclick>{ self.props.button_text }</button>
        }
    }
}

javascript 巨集的侷限性

不同於 Rust ,javaScript 本身是不支援巨集的,所以整個工具鏈也是沒有考慮巨集的。因此,你是可以寫個識別自定義語法的巨集,但是由於配套的工具鏈並不支援,比如最常見的 VSCode 和 Typescript ,你會得到一個語法錯誤。同樣對於 babel 本身所用的 parser 也是不支援擴充套件語法的,除非你另 Fork 出來一個 Babel 。因此 babel-plugin-macroswww.cppcns.com 不支援自定義語法。 不過,藉助模板字串函式,我們可以曲線救國,至少獲得部分自定義語法樹的能力。 一個 GraphQL 的例子,支援在 JavaScript 中直接編寫 GraphQL。

import { gql } from 'graphql.macro';

const query = gql`
  query User {
    user(id: 5) {
      lastName
      ...UserEntry1
    }
  }
`;

//  在編譯期會轉換成 ↓ ↓ ↓ ↓ ↓ ↓

const query = {
  "kind": "Document","definitions": [{
    ...

為什麼要用巨集而非 Babel 外掛

Babel 外掛的能力確實遠大於巨集,而且有些情況下確實是不得不用外掛。巨集比起 Babel 外掛好的一點在於,巨集的理念在於開箱即用。使用 React 的開發者,相信都聽過的大名鼎鼎的 Create-React-App ,幫你封裝好了各種底層細節,開發者專注於編寫程式碼即可。但是 CRA 的問題在於其封裝的太嚴了,但凡你有一點需要自定義 Babel 外掛的需求,基本上就需要執行yarn react-script eject,將所有底層細節暴露出來。 而對於巨集來說,你只需要在專案的 Babel 配置內新增一個 babel-plugin-macros 外掛,那麼對於任何自定義的 Babel 巨集都可以完美支援,而不是像外掛一樣,需要下載各種各樣的外掛。 CRA 已經內建了 babel-plugin-macros ,你可以在 CRA 專案中使用任意的 Babel 巨集。

如何寫一個巨集?

介紹

一個巨集非常像一個 Babel 外掛,因此事先了解如何編寫 Babel 外掛是非常有幫助的,對於如何編寫 Babel 外掛, Babel 官方有一本手冊,專門介紹瞭如何從零編寫一個 Babel 外掛。 在知道如何編寫 Babel 外掛之後,我們首先通過一個使用巨集的例子,來介紹下, Babel 是如何識別檔案中的巨集的。是某種的特殊的語法,還是用爛的 $ 符號?

import preval from 'preval.macro'

const one = preval`module.exports = 1 + 2 - 1 - 1`

這是非常常見的一個巨集,其作用是在編譯期間執行字串中的 JavaScript 程式碼,然後將執行的結果替換到相應的地方,如上的程式碼在編譯期會被展開為:

import preval from 'preval.macro'

const one = 1

從使用來方式來看,唯一與識別巨集沾點關係的就是*.macro字元,這也確實就是 Babel 如何識別巨集的方式,實際上不僅對於*.macro的形式, Babel 認為庫名匹配正則/[./]macro(\.c?js)?$/表示式的庫就是 Babel 巨集,這些匹配表示式的一些例子:

'my.macro'
'my.macro.js'
'my.macro.cjs'
'my/macro'
'my/macro.js'
'my/macro.cjs'

編寫

接下來,我們將簡單編寫一個importURL巨集,其作用是通過 url 來引入一些庫,並在編譯期間將這些庫的程式碼預先拉取下來,處理一下然後引入到檔案中。我知道有些 Webpack 外掛已經支援 從 url 來引入庫,不過這同樣是一個很好的例子來學習如何編寫巨集,為了有趣!以及如何在 NodeJS 中發起同步請求! :)

準備

首先建立一個名為 importURL 的資料夾,執行npm init -y,來快速建立一個專案。在專案使用巨集的人需要安裝babel-plugin-macros,同樣的,編寫巨集的同樣需要安裝這個外掛,在寫之前,我們也需要提前安裝一些其他的庫來輔助我們編寫巨集,在開發之前,需要事先:

  • 在package.json將name改為import-url.macro,符合 Babel 識別巨集的格式
  • 我們需要用 Babel 提供的輔助方法來建立巨集。執行yarn add babel-plugin-macros
  • yarn add fs-extra,一個更容易使用的代替 Nodefs模組的庫
  • yarn add find-root,編寫巨集的過程我們需要根據所處理檔案的路徑找到其所在的工作目錄,從而寫入快取,這是一個已經封裝好的庫

示例

我們的目標就是將如下程式碼轉換成

import importURL from 'importurl.macros';

const React = importURL('https://unpkg.com/[email protected]/umd/react.development.js');

// 編譯成

import importURL from 'importurl.macros';

const React = require('../cache/pkg1.js');

我們會解析程式碼 importURL 函http://www.cppcns.com數的第一個引數,當做遠端庫的地址,然後在編譯期間同步的通過 Get 請求拉取程式碼內容。然後寫入專案頂層資料夾下.chache下,並替換相應的 importURL 語句成require(...)語句,路徑...則是使用importURL的檔案相對.cache檔案中的相對路徑,使得 webpack 在最終打包的時候能夠找到對應的程式碼。

開始

我們先看看最終的程式碼長什麼樣子

import { execSync } from 'child_process';
import findRoot from 'find-root';
import path from 'path';
import fse from 'fs-extra';

import { createMacro } from 'babel-plugin-macros';

const syncGet = (url) => {
  const data = execSync(`curl -L ${url}`).toString();
  if (data === '') {
    throw new Error('empty data');
  }
  return data;
}

let count = 0;
export const genUniqueName = () => `pkg${++count}.js`;

module.exports = createMacro((ctx) => {
  const {
    references,// 檔案中所有對巨集的引用
    babel: {
    www.cppcns.com  types: t,}
  } = ctx;
  // babel 會把當前處理的檔案路徑設定到 ctx.state.filename
  const workspacePath = findRoot(ctx.state.filename);
  // 計算出快取資料夾
  const cacheDirPath = path.join(workspacePath,'.cache');
  //
  const calls = references.default.map(path => path.findParent(path => path.node.type === 'CallExpression' ));
  calls.forEach(nodePath => {
    // 確定 astNode 的http://www.cppcns.com型別
    if (nodePath.node.type === 'CallExpressio程式設計客棧n') {
      // 確定函式的第一個引數是純字串
      if (nodePath.node.arguments[0]?.type === 'StringLiteral') {
        // 獲取一個引數,當做遠端庫的地址
        const url = nodePath.node.arguments[0].value;
        // 根據 url 拉取程式碼
        const codes = syncGet(url);
        // 生成一個唯一包名,防止衝突
        const pkgName = genUniqueName();
        // 確定最終要寫入的檔案路徑
        const cahceFilename = path.join(cacheDirPath,pkgName);
        // 通過 fse 庫,將內容寫入, outputFileSync 會自動建立不存在的資料夾
        fse.outputFileSync(cahceFilename,codes);
        // 計算出相對路徑
        const relativeFilename = path.relative(ctx.state.filename,cahceFilename);
        // 最終計算替換 importURL 語句
        nodePath.replaceWith(t.stringLiteral(`require('${relativeFilename}')`))
      }
    }
  });
});

建立一個巨集

我們通過createMacro函式來建立一個巨集,createMacro接受我們編寫的函式當做引數來生成一個巨集,但實際上我們並不關心createMacro的返回時值是什麼,因為我們的程式碼最終都將會被自己替換掉,不會在執行期間執行到。 我們編寫的函式的第一個引數是 Babel 傳遞給我們的一些狀態,我們可以大概看下其型別都有什麼。

function createMacro(handler: MacroHandler,options?: Options): any;
interface MacroParams {
      references: { default: Babel.NodePath[] } & References;
      state: Babel.PluginPass;
      babel: typeof Babel;
      config?: { [key: string]: any };
  }
export interface PluginPass {
    file: BabelFile;
    key: string;
    opts: PluginOptions;
    cwd: string;
    filename: string;
    [key: string]: unknown;
}

視覺化 AST

我們可以通過astexplorer來觀察我們將要處理程式碼的語法樹,對於如下程式碼

import importURL from 'importurl.macros';

const React = importURL('https://unpkg.com/[email protected]/umd/react.development.js');

會生成如下語法樹

在JavaScript中如何使用巨集詳解

紅色標紅的語法樹節點,就是 Babel 會通過ctx.references傳遞給我們的,因此我們需要通過.findParent()方法來向上找到父節點CallExpresstion,才能去獲取arguments屬性下的引數,拿到遠端庫的 URL 地址。

同步請求

這裡的一個難點在於, Babel 不支援非同步轉換,所有的轉換操作都是同步的,因此在發起請求時也必須是同步的請求。我本來以為這是一件很簡單的事情, Node 會提供一個類似sync: true的選項。但是並沒有的, Node 確實不支援任何同步請求,除非你選擇用下面這種很怪異的方式

const syncGet = (url) => {
  const data = execSync(`curl -L ${url}`).toString();
  if (data === '') {
    throw new Error('empty data');
  }
  return data;
}

收尾

在拿到程式碼後,我們將程式碼寫入到開始計算出的檔案路徑中,這裡我們使用fs-extra的目的在於,fs-extra在寫入的時候如果遇到不存在資料夾,不會像fs一樣直接丟擲錯誤,而是自動建立相應的檔案件。在寫入完成後,我們通過 Babel 提供的輔助方法stringLiteral創字串節點,隨後替換掉我們的importURL(...),自此我們的整個轉換流程就結束了。

最後

這個巨集存在一些缺陷,有興趣的同學可以繼續完善:

沒有識別同一 URL 的庫,進行復用,不過我想這些已經滿足如何編寫一個巨集的目的了。

genUniqueName在跨檔案是會計算出重複包名,正確的演算法應該是根據 url 計算雜湊值來當做唯一包名

到此這篇關於在JavaScript中如何使用巨集的文章就介紹到這了,更多相關JavaScript使用巨集內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!