Перевод демонстрации расширения JS (CEF4Delphi) из Delphi в C++Builder завершается с ошибкой OnWebKitInitialized

Я хочу собрать демонстрационный проект расширения JS, включенный в CEF4Delphi, загруженный из https://github.com/salvadordf/CEF4Delphi, который был установлен на C++Builder XE7. Моя цель - отправлять сообщения и переменные из Chromium (веб-страница, javascript) в собственную функцию C++.

Я нашел демо-версию Delphi, которая работает хорошо. Но мне нужно перевести на C++Builder, а я перевел почти весь код. Это работает хорошо, если я не устанавливаю член OnWebKitInitialized TCefApplication. Но мне нужно установить, чтобы мое расширение было зарегистрировано, поэтому, когда я это делаю, приложение компилируется и строится хорошо, но браузер имеет белый фон и ничего не показывает. Мне нужно, чтобы эта демонстрация работала в C++Builder. Я прикрепил исходный код ниже.

Единица1.ч

//---------------------------------------------------------------------------

#ifndef Unit1H
#define Unit1H
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>
#include "uCEFChromium.hpp"
#include "uCEFWinControl.hpp"
#include "uCEFWindowParent.hpp"
#include <Vcl.ComCtrls.hpp>
#include <Vcl.ExtCtrls.hpp>

#include "uTestExtensionHandler.h"
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:    // IDE-managed Components
    TPanel *NavControlPnl;
    TEdit *Edit1;
    TButton *GoBtn;
    TStatusBar *StatusBar1;
    TCEFWindowParent *CEFWindowParent1;
    TChromium *Chromium1;
    TTimer *Timer1;
    void __fastcall Chromium1AfterCreated(TObject *Sender, ICefBrowser * const browser);
    void __fastcall Chromium1BeforeClose(TObject *Sender, ICefBrowser * const browser);
    void __fastcall Chromium1BeforeContextMenu(TObject *Sender, ICefBrowser * const browser,
          ICefFrame * const frame, ICefContextMenuParams * const params,
          ICefMenuModel * const model);
    void __fastcall Chromium1BeforePopup(TObject *Sender, ICefBrowser * const browser,
          ICefFrame * const frame, const ustring targetUrl, const ustring targetFrameName,
          TCefWindowOpenDisposition targetDisposition, bool userGesture,
          const TCefPopupFeatures &popupFeatures, TCefWindowInfo &windowInfo,
          ICefClient *&client, TCefBrowserSettings &settings, bool &noJavascriptAccess,
          bool &Result);
    void __fastcall Chromium1Close(TObject *Sender, ICefBrowser * const browser, bool Result);
    void __fastcall Chromium1ContextMenuCommand(TObject *Sender, ICefBrowser * const browser,
          ICefFrame * const frame, ICefContextMenuParams * const params,
          int commandId, DWORD eventFlags, bool Result);
    void __fastcall Chromium1ProcessMessageReceived(TObject *Sender, ICefBrowser * const browser,
          TCefProcessId sourceProcess, ICefProcessMessage * const message,
          bool Result);
    void __fastcall Timer1Timer(TObject *Sender);
    void __fastcall GoBtnClick(TObject *Sender);
    void __fastcall FormClose(TObject *Sender, TCloseAction &Action);
    void __fastcall FormCreate(TObject *Sender);
    void __fastcall FormShow(TObject *Sender);
    void __fastcall FormDestroy(TObject *Sender);





private:    // User declarations
public:     // User declarations
    __fastcall TForm1(TComponent* Owner);
protected:
    // Variables to control when can we destroy the form safely
    bool FCanClose;  // Set to True in TChromium.OnBeforeClose
    bool FClosing;  // Set to True in the CloseQuery event.
    void __fastcall BrowserCreatedMsg(TMessage &Message);
    void __fastcall BrowserDestroyMsg(TMessage &Message);
    void __fastcall WMMove(TMessage &Message);
    void __fastcall WMMoving(TMessage &Message);
    void __fastcall WMEnterMenuLoop(TMessage &Message);
    void __fastcall WMExitMenuLoop(TMessage &Message);

    BEGIN_MESSAGE_MAP
     MESSAGE_HANDLER(CEF_AFTERCREATED, TMessage, BrowserCreatedMsg)
     MESSAGE_HANDLER(CEF_DESTROY, TMessage, BrowserDestroyMsg)
     MESSAGE_HANDLER(WM_MOVE, TMessage, WMMove)
     MESSAGE_HANDLER(WM_MOVING, TMessage, WMMoving)
     MESSAGE_HANDLER(WM_ENTERMENULOOP, TMessage, WMEnterMenuLoop)
     MESSAGE_HANDLER(WM_EXITMENULOOP, TMessage, WMExitMenuLoop)
    END_MESSAGE_MAP(TForm)

};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

Unit1.cpp

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma link "uCEFChromium"
#pragma link "uCEFWinControl"
#pragma link "uCEFWindowParent"
#pragma resource "*.dfm"
TForm1 *Form1;

void GlobalCEFApp_OnWebKitInitialized()
{
  String TempExtensionCode;
  //ICefv8Handler *TempHandler;
  _di_ICefv8Handler TempHandler;

  // This is a JS extension example with 2 functions and several parameters.
  // Please, read the "JavaScript Integration" wiki page at
  // https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration.md

  TempExtensionCode = "var myextension;\
                       if (!myextension)\
                         myextension = {};\
                       (function() {\
                         myextension.mouseover = function(a) {\
                           native function mouseover();\
                           mouseover(a);\
                         };\
                         myextension.sendresulttobrowser = function(b,c) {\
                           native function sendresulttobrowser();\
                           sendresulttobrowser(b,c);\
                         };\
                       })();";

  try {
    TempHandler = TTestExtensionHandler();
    CefRegisterExtension("myextension", TempExtensionCode, TempHandler);
  }
  __finally {
    TempHandler = NULL;
  }
}
class TWebKitInitRef : public TCppInterfacedObject<TOnWebKitInitializedEvent>
{
public:
    //TWebKitInitRef(){  Invoke();}
    INTFOBJECT_IMPL_IUNKNOWN(TInterfacedObject);
    void __fastcall Invoke() {    GlobalCEFApp_OnWebKitInitialized(); }
};

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
    GlobalCEFApp = new TCefApplication();
    GlobalCEFApp->CheckCEFFiles =true;
    GlobalCEFApp->OnWebKitInitialized = _di_TOnWebKitInitializedEvent(new TWebKitInitRef());

    GlobalCEFApp->LogFile             = "debug.log";
    GlobalCEFApp->LogSeverity         = LOGSEVERITY_INFO;

    if (! GlobalCEFApp->StartMainProcess())Form1->Close();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Chromium1AfterCreated(TObject *Sender, ICefBrowser * const browser)

{
 PostMessage(Handle, CEF_AFTERCREATED, 0, 0);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Chromium1BeforeClose(TObject *Sender, ICefBrowser * const browser)

{
 FCanClose = true;
 PostMessage(Handle, WM_CLOSE, 0, 0);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Chromium1BeforeContextMenu(TObject *Sender, ICefBrowser * const browser,
          ICefFrame * const frame, ICefContextMenuParams * const params,
          ICefMenuModel * const model)
{
  // Adding some custom context menu entries
  model->AddSeparator();
  model->AddItem(MENU_ID_USER_FIRST + 1,  "Set mouseover event");
  model->AddItem(MENU_ID_USER_FIRST + 2,  "Visit DOM in JavaScript");
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Chromium1BeforePopup(TObject *Sender, ICefBrowser * const browser,
          ICefFrame * const frame, const ustring targetUrl, const ustring targetFrameName,
          TCefWindowOpenDisposition targetDisposition, bool userGesture,
          const TCefPopupFeatures &popupFeatures, TCefWindowInfo &windowInfo,
          ICefClient *&client, TCefBrowserSettings &settings, bool &noJavascriptAccess,
          bool &Result)
{
  Result = targetDisposition==WOD_NEW_FOREGROUND_TAB || targetDisposition==WOD_NEW_BACKGROUND_TAB || targetDisposition==WOD_NEW_POPUP || targetDisposition==WOD_NEW_WINDOW ? true: false;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Chromium1Close(TObject *Sender, ICefBrowser * const browser,
          bool Result)
{
 PostMessage(Handle, CEF_DESTROY, 0, 0);
 Result = true;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Chromium1ContextMenuCommand(TObject *Sender, ICefBrowser * const browser,
          ICefFrame * const frame, ICefContextMenuParams * const params,
          int commandId, DWORD eventFlags, bool Result)

{
  Result = false;

  // Here is the code executed for each custom context menu entry

  switch( commandId)
  {
    case (MENU_ID_USER_FIRST + 1) :
      if ((browser != NULL) && (browser->MainFrame != NULL))
        browser->MainFrame->ExecuteJavaScript("document.body.addEventListener('mouseover', function(evt){\
            function getpath(n){\
              var ret = '<' + n.nodeName + '>';\
              if (n.parentNode){return getpath(n.parentNode) + ret} else \
              return ret\
            };\
            myextension.mouseover(getpath(evt.target))})",
            // This is the call from JavaScript to the extension with DELPHI code in uTestExtensionHandler.pas
          "about:blank", 0);

    case (MENU_ID_USER_FIRST + 2) :
      if ((browser != NULL) && (browser->MainFrame != NULL))
        browser->MainFrame->ExecuteJavaScript("var testhtml = document.body.innerHTML;\
          myextension.sendresulttobrowser(testhtml, " + QuotedStr((AnsiString)"customname") + ");",  // This is the call from JavaScript to the extension with DELPHI code in uTestExtensionHandler.pas
          "about:blank", 0);
  }
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Chromium1ProcessMessageReceived(TObject *Sender, ICefBrowser * const browser,
          TCefProcessId sourceProcess, ICefProcessMessage * const message,
          bool Result)
{
  if ((message == NULL) || (message->ArgumentList == NULL)) exit;

  // This function receives the messages with the JavaScript results

  // Many of these events are received in different threads and the VCL
  // doesn't like to create and destroy components in different threads.

  // It's safer to store the results and send a message to the main thread to show them.

  // The message names are defined in the extension or in JS code.

  if (message->Name == "mouseover")
    {
      StatusBar1->Panels->Items[0]->Text = message->ArgumentList->GetString(0);
      Result = true;
    }
  else
    if (message->Name == "customname")
      {
        StatusBar1->Panels->Items[0]->Text = message->ArgumentList->GetString(0);
        Result = true;
      }
  else Result = false;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
   Timer1->Enabled = false;
  if (! (Chromium1->CreateBrowser(CEFWindowParent1, "")) && !(Chromium1->Initialized))
    Timer1->Enabled = true;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::GoBtnClick(TObject *Sender)
{
  Chromium1->LoadURL(Edit1->Text);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
  //Action = FCanClose ? caFree : caNone;

  if (!FClosing)
    {
      FClosing = true;
      Visible  = false;
      Chromium1->CloseBrowser(true);
    }
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
 FCanClose = false;
 FClosing  = false;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormShow(TObject *Sender)
{
   StatusBar1->Panels->Items[0]->Text= "Initializing browser. Please wait...";

  // GlobalCEFApp.GlobalContextInitialized has to be TRUE before creating any browser
  // If it's not initialized yet, we use a simple timer to create the browser later.
  if (!Chromium1->CreateBrowser(CEFWindowParent1, "")) Timer1->Enabled = true;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
 GlobalCEFApp->~TCefApplication();
 DestroyGlobalCEFApp();
}
//---------------------------------------------------------------------------


void __fastcall TForm1::WMMove(TMessage &Message)
{
  if (Chromium1 != NULL) Chromium1->NotifyMoveOrResizeStarted();
}

void __fastcall TForm1::WMMoving(TMessage &Message)
{
  if (Chromium1 != NULL) Chromium1->NotifyMoveOrResizeStarted();
}

void __fastcall TForm1::WMEnterMenuLoop(TMessage &Message)
{
  if ((Message.WParam == 0) && (GlobalCEFApp != NULL)) GlobalCEFApp->OsmodalLoop = true;
}

void __fastcall TForm1::WMExitMenuLoop(TMessage &Message)
{
  if ((Message.WParam == 0) && (GlobalCEFApp != NULL)) GlobalCEFApp->OsmodalLoop = false;
}

void __fastcall TForm1::BrowserCreatedMsg(TMessage &Message)
{
  StatusBar1->Panels->Items[0]->Text = "";
  CEFWindowParent1->UpdateSize();
  NavControlPnl->Enabled = true;
  GoBtn->Click();
}

void __fastcall TForm1::BrowserDestroyMsg(TMessage &Message)
{
  CEFWindowParent1->Free();
}

uTestExtensionHandler.h

// ************************************************************************
// ***************************** CEF4Delphi *******************************
// ************************************************************************
//
// CEF4Delphi is based on DCEF3 which uses CEF3 to embed a chromium-based
// browser in Delphi applications.
//
// The original license of DCEF3 still applies to CEF4Delphi.
//
// For more information about CEF4Delphi visit :
//         https://www.briskbard.com/index.php?lang=en&pageid=cef
//
#ifndef uTestExtensionHandlerH
#define uTestExtensionHandlerH


#include "uCEFRenderProcessHandler.hpp"
#include "uCEFBrowserProcessHandler.hpp"
#include "uCEFInterfaces.hpp"
#include "uCEFProcessMessage.hpp"

#include "uCEFv8Context.hpp"
#include "uCEFTypes.hpp"
#include "uCEFv8Handler.hpp";

class TTestExtensionHandler : public _di_ICefv8Handler//public ICefv8Handler
{
    protected:
      //bool __fastcall Execute(ustring name, ICefv8Value *obj, TCefv8ValueArray arguments, ICefv8Value *retval, ustring exception);
      bool __fastcall Execute(const Uceftypes::ustring name, const _di_ICefv8Value obj, const TCefv8ValueArray arguments, _di_ICefv8Value &retval, Uceftypes::ustring &exception);

};



//uses uCEFMiscFunctions, uCEFConstants, uJSExtension;

bool __fastcall  TTestExtensionHandler::Execute(const Uceftypes::ustring name, const _di_ICefv8Value obj, const TCefv8ValueArray arguments, _di_ICefv8Value &retval, Uceftypes::ustring &exception)
{
  ICefProcessMessage *msg;
  bool Result;
  if (name == "mouseover")
  {
      if ((arguments.Length > 0) && arguments[0]->IsString())
        {
          msg = TCefProcessMessageRef::New("mouseover");
          msg->ArgumentList->SetString(0, arguments[0]->GetStringValue());

          TCefv8ContextRef::Current()->Browser->SendProcessMessage(PID_BROWSER, msg);
        }

      Result = true;
  }
   else
    if (name == "sendresulttobrowser")
      {
        if ((arguments.Length > 1) && arguments[0]->IsString() && arguments[1]->IsString())
          {
            msg = TCefProcessMessageRef::New(arguments[1]->GetStringValue());
            msg->ArgumentList->SetString(0, arguments[0]->GetStringValue());

            TCefv8ContextRef::Current()->Browser->SendProcessMessage(PID_BROWSER, msg);
          }

        Result = true;
      }
     else
      Result = false;
 return Result;

}

#endif

Я ожидаю, что правильно зарегистрирую свое расширение, чтобы иметь возможность возвращать сообщения и переменные результатов с веб-страницы в собственную функцию С++. Я сделал почти то же самое из демо-версии Delphi, но не могу заставить его работать в C++Builder. Что я должен изменить, чтобы заставить это работать?

Обновлено:

Я пробовал второй метод:

#include "uCEFv8Value.hpp"

class MyV8Handler : public _di_ICefv8Handler {
public:
MyV8Handler() {} ;

virtual bool __fastcall Execute(const Uceftypes::ustring name, const _di_ICefv8Value obj, const TCefv8ValueArray arguments, _di_ICefv8Value &retval, Uceftypes::ustring &exception)
{         
  if (name == "myfunc") { 
// Extract argument values
// Return my string value.
 retval = TCefv8ValueRef::NewString("My Value!");
return true;
}

// Function does not exist.
return false;
}

// Provide the reference counting implementation for this class.
IMPLEMENT_REFCOUNTING(MyV8Handler);
};

class TContextRef : public TCppInterfacedObject<TOnContextCreatedEvent>
{
public:
    //TContextRef(){  Invoke();}
    INTFOBJECT_IMPL_IUNKNOWN(TInterfacedObject);
    void __fastcall Invoke(const _di_ICefBrowser browser, const _di_ICefFrame frame, const _di_ICefv8Context context)
    {    
      // Retrieve the context's window object.
      _di_ICefv8Value object = context->GetGlobal();

        // Create an instance of my CefV8Handler object.
        ICefv8Handler *handler = dynamic_cast<ICefv8Handler*>(new MyV8Handler());

        // Create the "myfunc" function.
        _di_ICefv8Value func = TCefv8ValueRef::NewFunction("myfunc", handler);

        // Add the "myfunc" function to the "window" object.
        object->SetValueByKey("myfunc", func, V8_PROPERTY_ATTRIBUTE_NONE);
    }
};

...

GlobalCEFApp->OnContextCreated = _di_TOnContextCreatedEvent(new TContextRef());

Chromium1->ExecuteJavaScript("alert(window.myfunc('someString'));", "", 0);

Я думаю, что GlobalCEFApp->OnContextCreated не Ivoked, то же самое, что и GlobalCEFApp->OnWebKitInitialized. Может быть, ссылка на функцию передана неправильно?

Может связаны? гр. OnWebKitInitialized не вызывается

Remy Lebeau 23.03.2019 15:49

Если вы получаете белый экран при включении кода GlobalCEFApp_OnWebKitInitialized, значит процесс рендеринга дает сбой. CEF4Delphi по умолчанию использует несколько процессов, а GlobalCEFApp.OnWebKitInitialized выполняется в процессе рендеринга. Вам необходимо включить режим «Один процесс» для отладки этого кода, установив для GlobalCEFApp.SingleProcess значение true. Этот режим используется только для отладки и никогда не должен включаться в вашей окончательной сборке, но он позволяет вам получать сообщения об ошибках. Установите точку останова в GlobalCEFApp_OnWebKitInitialized и другую в TTestExtensionHandler::Execute.

Salvador Díaz Fau 23.03.2019 16:47

@RemyLebeau Я попробовал второй метод, вы можете видеть в обновленном вопросе, но это то же самое, и я думаю, что GlobalCEFApp->OnContextCreated не вызывается, например GlobalCEFApp->OnWebKitInitialized не вызывается. Может быть, ссылка на функцию передана неправильно?

Aziz 23.03.2019 20:56

@SalvadorDíazFau Я сделал то, что вы сказали, и если я сделаю SingleProcess, он вылетит без ошибок. Но я вижу в debug.log это: [0323/195017.740:ERROR:CEF4Delphi(1)] TCefApplication.ExecuteProcess error : Access violation at address 10CDBC59 in module 'libcef.dll'. Read of address 00000000 . Я думаю, проблема может быть вызвана неправильной ссылкой на функцию OnWebKitInitialized или OnContextCreated, потому что она не вызывается

Aziz 23.03.2019 21:01

Попробуйте изменить объявление TOnWebKitInitializedEvent в коде CEF4Delphi на что-то другое, что проще назначить процедуре в C++. Извините за неясность, но мои знания C++ Builder ограничены.

Salvador Díaz Fau 23.03.2019 21:46

@AyayMatty проблема не в том, как назначаются обработчики событий. Происходит что-то еще. В сообщении об ошибке говорится, что осуществляется доступ к указателю NULL. Используйте отладчик, чтобы перейти к адресу, указанному в сообщении об ошибке, и посмотреть, какой код фактически выполняется по этому адресу, и посмотреть, к какому указателю он пытается получить доступ.

Remy Lebeau 24.03.2019 00:31

@SalvadorDíazFau Пожалуйста, скажите мне, что такое TValue из class procedure Register(const name: ustring; const value: TValue; SyncMainThread: Boolean = False); в TCefRTTIExtension = class(TCefv8HandlerOwn) Если можно, покажите мне кусок кода в delphi, как вы создаете и передаете TValue в процедуре регистрации

Aziz 24.03.2019 12:50

TValue определено в System.Rtti: docwiki.embarcadero.com/Libraries/Rio/en/System.Rtti.TValue

Salvador Díaz Fau 24.03.2019 13:08

Если вы хотите избежать использования TValue, попробуйте преобразовать другую демонстрацию. JSRTTIExtension использует класс TCefRTTIExtension, но JSExtension делает то же самое без него. Единственный код, который я могу вам показать, это: github.com/salvadordf/CEF4Delphi/blob/…

Salvador Díaz Fau 24.03.2019 13:15

@SalvadorDíazFau Я хочу знать, для чего используется этот объект TValue, какие данные я должен поместить в объект TValue. Я нашел это в паскалевских файлах uCEFv8Handler, и есть только одна процедура регистрации, которая имеет этот прототип: class procedure Register(const name: ustring; const value: TValue; SyncMainThread: Boolean = False); так я понимаю другие параметры, но для чего используется второй параметр, что я должен в него вставить?

Aziz 24.03.2019 18:49

Процедура TCefRTTIExtension.Register, используемая в демонстрации JSRTTIExtension, представляет собой другой способ регистрации расширения. Демонстрация JSExtension следует инструкциям CEF для создания расширения к письму. У него есть TTestExtensionHandler, который выполняет код расширения, а событию OnWebKitInitialized достаточно вызвать CefRegisterExtension с кодом JS и TTestExtensionHandler для регистрации расширения. Однако демо JSRTTIExtension использует класс TCefRTTIExtension для более простой регистрации расширения <продолжение в следующем комментарии>

Salvador Díaz Fau 24.03.2019 21:37

TCefRTTIExtension.Register требуется только имя расширения и тип класса с несколькими функциями класса в качестве второго параметра (TValue). В случае JSRTTIExtension он использует тип класса TTestExtension в качестве второго параметра в TCefRTTIExtension.Register. TTestExtension имеет 2 функции класса, которые выполняются, когда код JavaScript вызывает «myextension.mouseover» или «myextension.sendresulttobrowser».

Salvador Díaz Fau 24.03.2019 21:43

Я не знаю, легко ли это перевести в C++ Builder, но если вы посмотрите на код внутри TCefRTTIExtension.Register, вы также увидите, что он использует «определитьGetter» и «определить сеттер», которые устарели. Вместо этого я бы предложил вам использовать демонстрацию JSExtension.

Salvador Díaz Fau 24.03.2019 21:55
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
13
739
0

Другие вопросы по теме