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

в 10:01, , рубрики: config, currentScript, document, javascript

Как то раз мне захотелось сделать "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://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js