How a Single Input Box Can Power Unlimited Backend Configurations
This article explains a flexible solution that lets backend administrators write JavaScript functions directly in an input box, using parameter interpolation and a sandboxed Node.js VM to dynamically generate HTML, CSS, or script tags, handling complex logic, data transformation, and conditional branching safely.
Why Write Functions Directly in an Input Box?
Traditional form‑based configuration works for simple cases but quickly fails when dealing with dynamic logic, complex data transformations, conditional branches, or inter‑dependent parameters. Embedding a JavaScript function in the input field gives unlimited flexibility, dynamic execution, reusable logic, and easy debugging.
Defining a Function Input Box
We expose a special input named output. Users write a JavaScript function that receives config, option, and optionally query. The function returns an array of objects, each describing an HTML tag to be inserted into the page.
Component object fields : type: tag type (e.g., script, style, link, meta) name: a human‑readable identifier attrs: an array of attribute objects
{name, value} content: the inner HTML or script content
Basic Example
async function output(config, option, query) {
const content = await fetchData({
url: config.url,
});
return [{
type: 'script',
name: 'example-component',
attrs: [{ name: 'type', value: 'text/javascript' }],
content: content,
}];
}Security: Sandbox Isolation
To execute user‑provided code safely we create a restricted sandbox using Node.js's vm module.
import vm from 'node:vm';
// Create a limited sandbox context
const sandbox = {
_, // lodash utilities
console,
fetchData,
transformCustomVariables,
URL,
};
const context = vm.createContext(sandbox);The sandbox only exposes the APIs listed above, preventing access to the host system.
Execution Flow
const handleTemplate = async (userFunctionString, config, option) => {
try {
// 1️⃣ Parameter interpolation
const replacedUserFunctionString = replaceInterpolate(userFunctionString, { config, option });
// 2️⃣ Run the user code inside the sandbox
const userFunction = vm.runInContext(`(${replacedUserFunctionString})`, context);
// 3️⃣ Execute the function and get the raw result
const result = await userFunction(config, option);
// 4️⃣ Post‑process (e.g., minify) each returned item
const processedResult = result.map(item => ({
...item,
content: uglifyCode(item.content),
}));
return processedResult;
} catch (error) {
throw new Error(`函数执行错误: ${error.message}`);
}
};Parameter Interpolation Mechanism
Users can reference config and option values inside their function using the {{}} syntax. The interpolation process first parses the raw input values, then replaces the placeholders with actual JavaScript expressions.
function replaceInterpolate(template, { config, option }) {
const reg = /\{\{\s*(config\.[^}]+|option\.[^}]+)\s*\}\}/g;
if (!reg.test(template)) return template;
return _.template(template, { interpolate: reg })({
config: formatParams(config),
option: formatParams(option),
});
}Helper utilities:
// Convert a value field from string to real JS value
function parseValueField(obj) {
return Object.entries(obj).reduce((acc, [key, valueObj]) => {
if (['object','array','arrayByItems','boolean','function','number'].includes(valueObj.type)) {
try {
valueObj.value = evalStrToObj(valueObj.value);
acc[key] = valueObj;
} catch (e) {
console.error(`解析 ${key} 的值失败: `, e);
}
} else {
acc[key] = valueObj;
}
return acc;
}, {});
}
// Turn a string into a JS expression safely
const evalStrToObj = (str) => new Function(`return (${str})`)();
// Recursively format parameters for interpolation
function formatParams(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, stringifyValueAsExpression(v)]));
}
// Convert any JS value to a valid JS literal string
function stringifyValueAsExpression(value) {
if (value instanceof RegExp) return value.toString();
if (Array.isArray(value)) return `[${value.map(stringifyValueAsExpression).join(',')}]`;
if (typeof value === 'object' && value !== null) {
return `{${Object.entries(value).map(([k, v]) => `${k}:${stringifyValueAsExpression(v)}`).join(',')}}`;
}
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
if (value == null) return 'null';
return String(value);
}Interpolation Example
function output(config, option) {
const theme = '{{config.theme}}';
const buttonColor = '{{option.buttonColor}}';
const features = {{option.features}};
return [{
type: 'style',
name: 'dynamic-style',
attrs: [{ name: 'type', value: 'text/css' }],
content: `
.container {
theme: ${theme};
--button-color: ${buttonColor};
}
.features::after {
content: '${features.join(', ')}';
}
`,
}];
}After interpolation with
option = { buttonColor: 'blue', features: ['login','register'], config: { theme: 'dark' } }the function becomes:
function output(config, option, query) {
const color = 'blue';
const features = ['login','register'];
const timeout = 5000;
return [{
type: 'script',
name: 'debug-info',
attrs: [{ name: 'type', value: 'text/javascript' }],
content: `
console.log('颜色:blue');
console.log('功能:', ["login","register"]);
console.log('超时:5000ms');
`,
}];
}Real‑World Cases
Case 1 – Conditional Font Management Component
This component generates @font-face rules, CSS variables, and utility classes based on which fonts the user enables in option.
async function output(config, option) {
const { 'custom-regular': enableRegular, 'custom-medium': enableMedium, 'custom-bold': enableBold } = option;
const fontList = [
{ name: 'custom-regular', content: '@font-face { font-family: custom-regular; src: url("https://cdn.example.com/fonts/CustomFont_Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }' },
{ name: 'custom-medium', content: '@font-face { font-family: custom-medium; src: url("https://cdn.example.com/fonts/CustomFont_Medium.woff2") format("woff2"); font-weight: 500; font-display: swap; }' },
{ name: 'custom-bold', content: '@font-face { font-family: custom-bold; src: url("https://cdn.example.com/fonts/CustomFont_Bold.woff2") format("woff2"); font-weight: 700; font-display: swap; }' },
];
const activeKeys = Object.keys(option).filter(k => option[k]);
const filterList = fontList.filter(item => activeKeys.includes(item.name));
const content = _ .template(`
<% _.forEach(list, function(item) { %>
<%= item.content %>
<% }); %>
:root {
<% _.forEach(list, function(item) { var clean = item.name.replace('custom-',''); %>
--font-<%= clean %>: custom-<%= clean %>;
<% }); %>
}
<% _.forEach(list, function(item) { var clean = item.name.replace('custom-',''); %>
.f-<%= clean.split('-').map(w=>w[0]).join('') %> { font-family: var(--font-<%= clean %>); }
<% }); %>
`)({ list: filterList });
return [{
type: 'style',
name: '字体管理 CSS 变量版',
attrs: [{ name: 'type', value: 'text/css' }],
content,
}];
}Case 2 – Page Grayscale Component
async function output(config, option, query) {
const customCss = option.customCss || '';
return [{
type: 'script',
name: '网页置灰',
attrs: [],
content: `
(function () {
let grayscaleStyleElement = null;
let intervalId = null;
function enableGrayscale() {
if (grayscaleStyleElement) return;
const style = document.createElement('style');
style.innerHTML = `html {filter: grayscale(0.95);-webkit-filter: grayscale(0.95);}` + ${customCss};
document.documentElement.insertBefore(style, document.head);
grayscaleStyleElement = style;
}
function disableGrayscale() {
if (grayscaleStyleElement) {
document.documentElement.removeChild(grayscaleStyleElement);
grayscaleStyleElement = null;
}
}
function getMillTime(d) { return d ? new Date(d).getTime() : Date.now(); }
function handleGrayscale(start, end) {
const now = getMillTime();
if (now >= start && now <= end) enableGrayscale(); else disableGrayscale();
if (now > end && !grayscaleStyleElement) { clearInterval(intervalId); intervalId = null; }
}
function scheduleGrayscaleEffect() {
const start = getMillTime({{option.startDate}});
const end = getMillTime({{option.endDate}});
handleGrayscale(start, end);
intervalId = setInterval(() => handleGrayscale(start, end), 10000);
}
scheduleGrayscaleEffect();
})();
`,
}];
}Conclusion
Embedding a JavaScript function in a configuration input box provides:
Extreme flexibility : users can encode any business logic.
Full expressive power : all JavaScript features (conditionals, loops, async calls) are available.
Secure execution : a sandbox isolates user code from the main process.
Rich parameter support : primitive, array, object, and even function types can be interpolated.
Easy extensibility : new APIs can be added to the sandbox without breaking existing functions.
While the approach requires users to know basic JavaScript, the payoff for complex backend scenarios is substantial. Future enhancements may include a template library, visual debugging tools, and version control for user‑written functions.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
