Интересный способ сделать config для web js библиотеки

от автора

Как то раз мне захотелось сделать «Contact us» виджет и возникла дилемма, как задать настройки кнопки?

Хотелось чтобы:

  • Всё было понятно для не(до)программистов

  • Легко было написать генератор

  • Всё работало сразу же

Всё работало сразу же

Имеется ввиду что нужно только подключить скрипт. Без создания экземпляра класса и вызова где-то там в коде. Мне разу же пришла в голову идея передавать параметры в GET параметрах URL.

Но также хотелось бы выложить код на github без использования серверной части… Я задал вопрос на Toster QNA Habr

Как получить GET параметры ссылки по которой был загружен скрипт? Что я имею в виду например есть какой-то скрипт залитый на github httрs;//mуsitе.github.iо/script.js На сайте example.com мы его загружаем <script defer src="https://mysite.github.io/script.js?param1=1&param2=2"></script> <!-- в ссылке передаются статические параметры --> Github был выбран просто для примера, что нет возможности на стороне сервера отобразить параметры в скрипте. Возможно ли из js узнать из какого элемента script он был загружен? Или каким-то другим образом получить параметры из URL?

Но после 5 минут поиска в интернете я нашел интересное свойство объекта document. document.currentScript

Получения параметров из адреса скрипта

Дальше дело за малым, получаем ссылку, парсим и радуемся что не пришлось писать backend логику

 <head>    ...    <script defer src="https://mysite.github.io/script.js?color=fff&text=helloWorld"></script> </head>
let selfElement = document.currentScript;  if (selfElement?.src) {   let urlR = selfElement.src,   url = new URL(urlR), params = url.searchParams  if (params.has("color")) {     mycustomelement.setColor(params.get("color"))   }  if (params.has("text")) {   mycustomelement.setText(params.get("text"))   } ...  }

Это успех осталось только написать логику дальше… НО меня всегда интересовала одна штука. А точнее тег <script> если указать параметр src то эго содержание не будет выполняться. Но поскольку мы уже смогли получить свой родительский элемент мы можем исправить это недоразумение

eval(selfElement.innerText);

Новая эра конфигов

И тут меня осенило можно же использовать это пространство, чтобы задавать конфигурацию плагина

<head>   ...   <script defer src="https://mysite.github.io/script.js">     {       "color":"#fff",       "text":"Hello world!"    }   </script> </head>

И теперь всё очень просто

let config = JSON.parse(selfElement.innerText);

Всё бы было хорошо если бы VScode не ругался на JSON между тегов script В принципе это не столь критично, но мне не хотелось лесть в настройки чтобы оно игнорировалось. Было решено как-то сделать так, чтобы IDE думало что это javascript

Можно бы было добавить кавычки и регуляркой парсить значения внутри. Но это ужасный вариант и я решил сделать так

<head>    ...    <script defer src="https://mysite.github.io/script.js">   return {     color: "#fff",       text: "Hello JS world!",     }   </script> </head>

Да использовать js внутри тега script кто бы мог подумать) Решил не использовать eval поскольку это не очень безопасно

let config = new Function(selfElement.innerText)();

Супер мы в шоколаде?! Не совсем( поскольку теперь помимо нашего return можно выполнить любой js код, это никуда не годиться… Что делать? В голову сразу же приходит SandBox. Но как изолировано запустить js код в js? Желания тянуть какую-то библиотеку нет, надо найти какое-то элегантное решение в несколько строк. После полчаса поисков нашёлся один gist

function construct(constructor, args) {   function F() {       return constructor.apply(this, args);   }   F.prototype = constructor.prototype;   return new F(); } // Sanboxer  function sandboxcode(string, inject) {   "use strict";   var globals = ["Function"];   for (var i in window) {     // <--REMOVE THIS CONDITION     if (i != "console")     // REMOVE THIS CONDITION -->     globals.push(i);   }   // The strict mode prevents access to the global object through an anonymous function (function(){return this;}()));   globals.push('"use strict";\n'+string);   return construct(Function, globals).apply(inject ? inject : {}); } sandboxcode('console.log( this, window, top , self, parent, this["jQuery"], (function(){return this;}()));');  // => Object {} undefined undefined undefined undefined undefined undefined  sandboxcode('return this;', {window:"sanboxed code"});  // => Object {window: "sanboxed code"}

Мне не сразу стало понятно как оно работает, но сейчас поясню. Мы видим 2 функции construct и sandboxcode Первая на вход принимает какую-то функцию и массив аргументов, создает новою функцию F которая принимает все аргументы с массива и переопределяет прототип, на выходе получаем анонимную функцию с кучей аргументов

function anonymous(top,window,location,external,chrome,document,...) {  "use strict";  return this; }

В этом и заключается вся магия мы просто переопределяем все существующие глобальные объекты и переменные.

Но код придётся немножко подредактировать, потому что мы все же сможем получить доступ во вне используя eval(); или globalThis

//https://gist.github.com/gornostay25/3ea24d743c90b2cd6b2aaadb9241fec9 function sandboxcode(s) {     function construct(c, a) {         function F(){return c.apply(this, a)}         F.prototype = c.prototype;         return new F()     }     let g = ["Function","globalThis","eval"]     for (let i in globalThis){g.push(i)}     g.push(s);     return construct(Function, g).apply({}); }

Я убрал передачу своих глобальных переменных поскольку это мне не было нужно, также немного укоротил код. В массиве g находятся все объекты, которые нужно перезаписать:

//function F(){return c.apply(this, a)} Function.apply({},["test","ttest","ttest2","alert(123);"]) /* ƒ anonymous(test,ttest,ttest2 ) { alert(123); } */

Теперь момент истины:

<head>   ...   <script defer src="https://mysite.github.io/script.js">     alert("bad code"); //Uncaught TypeError: alert is not a function     console.log(window,this,globalThis,Function,eval);     // => undefined {} undefined undefined undefined     return {       color: "#fff",       text: "Hello JS world!",     }   </script> </head>
let config = JSON.parse(JSON.stringify(sandboxcode(selfElement.innerText))); // => {color: "#fff", text: "Hello JS world!"}

Чтобы каким то образом не передалась функция или гетер класса, делаем фильтр через JSON И всё, мы справились!!!

Надеюсь, это кому-то пригодится. Мне будет приятно, гуляя по github увидеть что кто-то сделал свою библиотеку или виджет по этому принципу. Всем всего хорошего!


ссылка на оригинал статьи https://habr.com/ru/articles/576356/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *