Как то раз мне захотелось сделать "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¶m2=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 увидеть что кто-то сделал свою библиотеку или виджет по этому принципу. Всем всего хорошего!
