<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何在 JavaScript 中使用宏

          共 6514字,需瀏覽 14分鐘

           ·

          2021-05-16 01:42

          來(lái)源 | https://mp.weixin.qq.com/s/8Dtjy9clLINaxIdIko0bWw
          作者 | 字節(jié)前端 ByteFE

          在語(yǔ)言當(dāng)中,宏常見(jiàn)用途有實(shí)現(xiàn) DSL 。通過(guò)宏,開(kāi)發(fā)者可以自定義一些語(yǔ)言的格式,比如實(shí)現(xiàn) jsX 語(yǔ)法。在 WASM 已經(jīng)實(shí)現(xiàn)的今天,用其他語(yǔ)言來(lái)寫(xiě)網(wǎng)頁(yè)其實(shí)并不是沒(méi)有可能。
          像 Rust 語(yǔ)言就帶有強(qiáng)大的宏功能,這使得基于 Rust 的 Yew 框架,不需要實(shí)現(xiàn)類(lèi)似 Babel 的東西,而是靠語(yǔ)言本身就能實(shí)現(xiàn)類(lèi)似 jsX 的語(yǔ)法。一個(gè) Yew 組件的例子,支持類(lèi) JSX 的語(yǔ)法。
          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 本身是不支持宏的,所以整個(gè)工具鏈也是沒(méi)有考慮宏的。因此,你是可以寫(xiě)個(gè)識(shí)別自定義語(yǔ)法的宏,但是由于配套的工具鏈并不支持,比如最常見(jiàn)的 VSCode 和 Typescript ,你會(huì)得到一個(gè)語(yǔ)法錯(cuò)誤。

          同樣對(duì)于 babel 本身所用的 parser 也是不支持?jǐn)U展語(yǔ)法的,除非你另 Fork 出來(lái)一個(gè) Babel 。因此 babel-plugin-macros 不支持自定義語(yǔ)法。

          不過(guò),借助模板字符串函數(shù),我們可以曲線(xiàn)救國(guó),至少獲得部分自定義語(yǔ)法樹(shù)的能力。一個(gè) GraphQL 的例子,支持在 JavaScript 中直接編寫(xiě) GraphQL。

          import { gql } from 'graphql.macro';
          const query = gql` query User { user(id: 5) { lastName ...UserEntry1 } }`;
          // 在編譯期會(huì)轉(zhuǎn)換成 ↓ ↓ ↓ ↓ ↓ ↓
          const query = { "kind": "Document", "definitions": [{ ...

          為什么要用宏而非 Babel 插件

          Babel 插件的能力確實(shí)遠(yuǎn)大于宏,而且有些情況下確實(shí)是不得不用插件。宏比起 Babel 插件好的一點(diǎn)在于,宏的理念在于開(kāi)箱即用。

          使用 react 的開(kāi)發(fā)者,相信都聽(tīng)過(guò)的大名鼎鼎的 Create-React-App ,幫你封裝好了各種底層細(xì)節(jié),開(kāi)發(fā)者專(zhuān)注于編寫(xiě)代碼即可。

          但是 CRA 的問(wèn)題在于其封裝的太嚴(yán)了,但凡你有一點(diǎn)需要自定義 Babel 插件的需求,基本上就需要執(zhí)行 yarn react-script eject ,將所有底層細(xì)節(jié)暴露出來(lái)。

          而對(duì)于宏來(lái)說(shuō),你只需要在項(xiàng)目的 Babel 配置內(nèi)添加一個(gè) babel-plugin-macros 插件,那么對(duì)于任何自定義的 Babel 宏都可以完美支持,而不是像插件一樣,需要下載各種各樣的插件。CRA 已經(jīng)內(nèi)置了 babel-plugin-macros ,你可以在 CRA 項(xiàng)目中使用任意的 Babel 宏。

          如何寫(xiě)一個(gè)宏?

          01、介紹

          一個(gè)宏非常像一個(gè) Babel 插件,因此事先了解如何編寫(xiě) Babel 插件是非常有幫助的,對(duì)于如何編寫(xiě) Babel 插件, Babel 官方有一本手冊(cè) [1] ,專(zhuān)門(mén)介紹了如何從零編寫(xiě)一個(gè) Babel 插件。

          在知道如何編寫(xiě) Babel 插件之后,我們首先通過(guò)一個(gè)使用宏的例子,來(lái)介紹下, Babel 是如何識(shí)別文件中的宏的。是某種的特殊的語(yǔ)法,還是用爛的 $ 符號(hào)?

          import preval from 'preval.macro'
          const one = preval`module.exports = 1 + 2 - 1 - 1`

          這是非常常見(jiàn)的一個(gè)宏,其作用是在編譯期間執(zhí)行字符串中的 JavaScript 代碼,然后將執(zhí)行的結(jié)果替換到相應(yīng)的地方,如上的代碼在編譯期會(huì)被展開(kāi)為:

          import preval from 'preval.macro'
          const one = 1

          從使用來(lái)方式來(lái)看,唯一與識(shí)別宏沾點(diǎn)關(guān)系的就是 *.macro 字符,這也確實(shí)就是 Babel 如何識(shí)別宏的方式,實(shí)際上不僅對(duì)于 *.macro 的形式, Babel 認(rèn)為庫(kù)名匹配正則 /[./]macro(\.c?js)?$/ 表達(dá)式的庫(kù)就是 Babel 宏,這些匹配表達(dá)式的一些例子:

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

          02、編寫(xiě)

          接下來(lái),我們將簡(jiǎn)單編寫(xiě)一個(gè) importURL 宏,其作用是通過(guò) url 來(lái)引入一些庫(kù),并在編譯期間將這些庫(kù)的代碼預(yù)先拉取下來(lái),處理一下然后引入到文件中。

          我知道有些 webpack 插件已經(jīng)支持 從 url 來(lái)引入庫(kù),不過(guò)這同樣是一個(gè)很好的例子來(lái)學(xué)習(xí)如何編寫(xiě)宏,為了有趣!以及如何在 NodeJS 中發(fā)起同步請(qǐng)求! :)

          03、準(zhǔn)備

          首先創(chuàng)建一個(gè)名為 importURL 的文件夾,執(zhí)行 npm init -y ,來(lái)快速創(chuàng)建一個(gè)項(xiàng)目。在項(xiàng)目使用宏的人需要安裝 babel-plugin-macros ,同樣的,編寫(xiě)宏的同樣需要安裝這個(gè)插件,在寫(xiě)之前,我們也需要提前安裝一些其他的庫(kù)來(lái)輔助我們編寫(xiě)宏,在開(kāi)發(fā)之前,需要事先:

          package.jsonnameimport-url.macro

          我們需要用 Babel 提供的輔助方法來(lái)創(chuàng)建宏。執(zhí)行 yarn add babel-plugin-macros

          yarn add fs-extra ,一個(gè)更容易使用的代替 Node fs 模塊的庫(kù)

          yarn add find-root ,編寫(xiě)宏的過(guò)程我們需要根據(jù)所處理文件的路徑找到其所在的工作目錄,從而寫(xiě)入緩存,這是一個(gè)已經(jīng)封裝好的庫(kù)。

          04、示例

          我們的目標(biāo)就是將如下代碼轉(zhuǎn)換成。

          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');

          我們會(huì)解析代碼 importURL 函數(shù)的第一個(gè)參數(shù),當(dāng)做遠(yuǎn)程庫(kù)的地址,然后在編譯期間同步的通過(guò) Get 請(qǐng)求拉取代碼內(nèi)容。

          然后寫(xiě)入項(xiàng)目頂層文件夾下 .chache 下,并替換相應(yīng)的 importURL 語(yǔ)句成 require(...) 語(yǔ)句,路徑 ... 則是使用 importURL 的文件相對(duì) .cache 文件中的相對(duì)路徑,使得 webpack 在最終打包的時(shí)候能夠找到對(duì)應(yīng)的代碼。

          05、開(kāi)始

          我們先看看最終的代碼長(zhǎng)什么樣子

          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, // 文件中所有對(duì)宏的引用 babel: { types: t, } } = ctx; // babel 會(huì)把當(dāng)前處理的文件路徑設(shè)置到 ctx.state.filename const workspacePath = findRoot(ctx.state.filename); // 計(jì)算出緩存文件夾 const cacheDirPath = path.join(workspacePath, '.cache'); // const calls = references.default.map(path => path.findParent(path => path.node.type === 'CallExpression' )); calls.forEach(nodePath => { // 確定 astNode 的類(lèi)型 if (nodePath.node.type === 'CallExpression') { // 確定函數(shù)的第一個(gè)參數(shù)是純字符串 if (nodePath.node.arguments[0]?.type === 'StringLiteral') { // 獲取一個(gè)參數(shù),當(dāng)做遠(yuǎn)程庫(kù)的地址 const url = nodePath.node.arguments[0].value; // 根據(jù) url 拉取代碼 const codes = syncGet(url); // 生成一個(gè)唯一包名,防止沖突 const pkgName = genUniqueName(); // 確定最終要寫(xiě)入的文件路徑 const cahceFilename = path.join(cacheDirPath, pkgName); // 通過(guò) fse 庫(kù),將內(nèi)容寫(xiě)入, outputFileSync 會(huì)自動(dòng)創(chuàng)建不存在的文件夾 fse.outputFileSync(cahceFilename, codes); // 計(jì)算出相對(duì)路徑 const relativeFilename = path.relative(ctx.state.filename, cahceFilename); // 最終計(jì)算替換 importURL 語(yǔ)句 nodePath.replaceWith(t.stringLiteral(`require('${relativeFilename}')`)) } } });});

          06、創(chuàng)建一個(gè)宏

          我們通過(guò) createMacro 函數(shù)來(lái)創(chuàng)建一個(gè)宏, createMacro 接受我們編寫(xiě)的函數(shù)當(dāng)做參數(shù)來(lái)生成一個(gè)宏,但實(shí)際上我們并不關(guān)心 createMacro 的返回時(shí)值是什么,因?yàn)槲覀兊拇a最終都將會(huì)被自己替換掉,不會(huì)在運(yùn)行期間執(zhí)行到。

          我們編寫(xiě)的函數(shù)的第一個(gè)參數(shù)是 Babel 傳遞給我們的一些狀態(tài),我們可以大概看下其類(lèi)型都有什么。

          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;}

          07、可視化 AST

          我們可以通過(guò) astexplorer [2] 來(lái)觀察我們將要處理代碼的語(yǔ)法樹(shù),對(duì)于如下代碼。

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

          會(huì)生成如下語(yǔ)法樹(shù)


          紅色標(biāo)紅的語(yǔ)法樹(shù)節(jié)點(diǎn),就是 Babel 會(huì)通過(guò) ctx.references 傳遞給我們的,因此我們需要通過(guò) .findParent() 方法來(lái)向上找到父節(jié)點(diǎn) CallExpresstion ,才能去獲取 arguments 屬性下的參數(shù),拿到遠(yuǎn)程庫(kù)的 URL 地址。

          08、同步請(qǐng)求

          這里的一個(gè)難點(diǎn)在于, Babel 不支持異步轉(zhuǎn)換,所有的轉(zhuǎn)換操作都是同步的,因此在發(fā)起請(qǐng)求時(shí)也必須是同步的請(qǐng)求。我本來(lái)以為這是一件很簡(jiǎn)單的事情, Node 會(huì)提供一個(gè)類(lèi)似 sync: true 的選項(xiàng)。但是并沒(méi)有的, Node 確實(shí)不支持任何同步請(qǐng)求,除非你選擇用下面這種很怪異的方式:

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

          09、收尾

          在拿到代碼后,我們將代碼寫(xiě)入到開(kāi)始計(jì)算出的文件路徑中,這里我們使用 fs-extra 的目的在于, fs-extra 在寫(xiě)入的時(shí)候如果遇到不存在文件夾,不會(huì)像 fs 一樣直接拋出錯(cuò)誤,而是自動(dòng)創(chuàng)建相應(yīng)的文件件。

          在寫(xiě)入完成后,我們通過(guò) Babel 提供的輔助方法 stringLiteral 創(chuàng)字符串節(jié)點(diǎn),隨后替換掉我們的 importURL(...) ,自此我們的整個(gè)轉(zhuǎn)換流程就結(jié)束了。

          最后

          這個(gè)宏存在一些缺陷,有興趣的同學(xué)可以繼續(xù)完善:

          沒(méi)有識(shí)別同一 URL 的庫(kù),進(jìn)行復(fù)用,不過(guò)我想這些已經(jīng)滿(mǎn)足如何編寫(xiě)一個(gè)宏的目的了。

          genUniqueName 在跨文件是會(huì)計(jì)算出重復(fù)包名,正確的算法應(yīng)該是根據(jù) url 計(jì)算哈希值來(lái)當(dāng)做唯一包名

          參考資料

          [1] 手冊(cè): https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/README.md

          [2] astexplorer: https://astexplorer.net/



          學(xué)習(xí)更多技能

          請(qǐng)點(diǎn)擊下方公眾號(hào)


          瀏覽 35
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  超碰人人人人操 | 日本二级黄免费在线观看 | 男人的天堂资源网 | 北条麻妃视频在线观看 | 亚洲欧美性色图 |