您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關如何在JavaScript中使用宏,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
JS是JavaScript的簡稱,它是一種直譯式的腳本語言,其解釋器被稱為JavaScript引擎,是瀏覽器的一部分,主要用于web的開發,可以給網站添加各種各樣的動態效果,讓網頁更加美觀。
在語言當中,宏常見用途有實現 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> } } }
不同于 Rust ,JavaScript 本身是不支持宏的,所以整個工具鏈也是沒有考慮宏的。因此,你是可以寫個識別自定義語法的宏,但是由于配套的工具鏈并不支持,比如最常見的 VSCode 和 Typescript ,你會得到一個語法錯誤。同樣對于 babel 本身所用的 parser 也是不支持擴展語法的,除非你另 Fork 出來一個 Babel 。因此 babel-plugin-macros 不支持自定義語法。 不過,借助模板字符串函數,我們可以曲線救國,至少獲得部分自定義語法樹的能力。 一個 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 插件好的一點在于,宏的理念在于開箱即用。使用 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/react@17.0.1/umd/react.development.js'); // 編譯成 import importURL from 'importurl.macros'; const React = require('../cache/pkg1.js');
我們會解析代碼 importURL 函數的第一個參數,當做遠程庫的地址,然后在編譯期間同步的通過 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: { 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 的類型 if (nodePath.node.type === 'CallExpression') { // 確定函數的第一個參數是純字符串 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/react@17.0.1/umd/react.development.js');
會生成如下語法樹
紅色標紅的語法樹節點,就是 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; }
看完上述內容,你們對如何在JavaScript中使用宏有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。