Как то раз мне захотелось сделать "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 увидеть что кто-то сделал свою библиотеку или виджет по этому принципу. Всем всего хорошего!
Комментарии (16)
justboris
05.09.2021 13:32+8Картинка про троллейбус из буханки
А почему нельзя сделать так?
<script defer src="https://mysite.github.io/script.js"></script> <script> window.MyWidgetConfig = { color: "#fff", text: "Hello JS world!", } </script>
printf
05.09.2021 14:40Скажем, два одинаковых виджета на странице, каждый с другим конфигом.
(Я не защищаю решение в статье, просто, ну, очевидный ответ.)
justboris
05.09.2021 14:48+2Если виджет на странице может повторяться, это это решение тоже не годится. Потому что придется один и тот же скрипт подключать и исполнять дважды.
Для такой ситуации лучше явные конструкторы создавать.
nin-jin
05.09.2021 15:01Если с кешом не напортачить, то распарсенный скрипт может быть взят из кеша, так что надо будет лишь исполнить его ещё раз.
justboris
05.09.2021 15:02+1Тем не менее, все объекты создадутся второй копией и все сайд-эффекты произойдут дважды
nin-jin
05.09.2021 15:29+1Всё зависит от того сколько там общего кода, а сколько специфичного для места использования. Хотя, конечно, лучше всё же просто подключить скрипт, который просто зарегистрирует веб-компонент, а потом вставлять эти веб-кмпоненты куда и когда хочешь.
CoolCmd
05.09.2021 15:02+11а я передаю параметры через атрибуты:
<script data-coolparameter="coolcmd">
GORNOSTAY25 Автор
20.11.2021 17:06Это также хорошая идея, но если параметров много это выглядит не очень...
yanhaifa
05.09.2021 16:39А можно аттрибутами передать:
<script defer color="#fff" src="https://mysite.github.io/script.js"></script>
И получить:
document.currentScript.getAttribute('color')
dynamicult
05.09.2021 17:00+5<script src="https://example.com/script.js?a=1&b=2&c=3" type="module">
// script.js let params = Object.fromEntries( new URL(import.meta.url).searchParams.entries() ) console.log(params)
i360u
06.09.2021 04:23+1У инлайн-js - проблема с CSP, поэтому, на мой взгляд, из всех вариантов, JSON - самый перспективный. В атрибутах тегов, которые тут уже упомянули, не удобно работать со сложными конфигами, с объектами и массивами в значениях. Но, при этом, настройки лучше вынести в отдельный тег (не script). Это даст возможность связывать разные конфиги с разными экземплярами виджетов, если их в документе несколько.
Zibx
06.09.2021 05:39Проще текстовыми функциями обкусить начальный return, а потом JSON.parse. sandbox — это стрелять микроскопом по воробьям.
Ну и как заметили выше — явный подход лучше магического.
antirius
06.09.2021 11:37Попробуйте использовать trim (хотя у меня и без него работает)
let selfElement = document.currentScript;
let config = JSON.parse(selfElement.innerText.trim());
alert(config.test)
вот пример
antirius
06.09.2021 13:02Чтобы не было ошибок в VSCode - можно попробовать тип файла поставить JSX. Мне кажется так все же проще, чем городить кучу дополнительного кода.
nin-jin
По поводу "безопасности" стоит глянуть эту статью. Но тут она и не нужна, ибо тот, кто имеет доступ к содержимому тега script, - тот и так может выполнить произвольный код.