MFElements is a Web UI Platform As A Service made for creating service applications.
Originally created to provide to developers an easy access for Metapolis Freeland infrastructure.
Client is requesting the method API getIndex(), with no parameters, witch returns an object of main page.
The structure of the main page:
{
type: 'page'
children: (Element | string)[]
title: string
}
Element — all elements can be found by this link: https://github.com/mfelements/service-demo/blob/master/index.mjs
string — any text, will be represented as a text node.
title - not implemented yet. Could be passed.
Client need to make a request to: server_name/method_name with mandatory http headers:
Content-Type: application/json
Origin: https://mfelements.github.io
If the request doesn't contain those headers it will be refused.
Request could be: GET, POST or OPTIONS (last one automatically made by browsers before POST for optimization).
On OPTIONS the response jest need to reply with 200 code.
On POST should read the body and parse JSON. It contains an array of arguments that must be passed to method.
On GET method will be processed without arguments.
The response should contain http headers:
Content-Type: application/json
Access-Control-Allow-Origin: https://mfelements.github.io
Access-Control-Allow-Headers: content-type
Response body - encoded object in JSON with just one field - error or data.
error should be returned only when exception occurs on backend side and represents a string with error description for user. Example:
{"error":"User VPupkin does not exist"}
data should be returned when the execution was success. It represents the massive of elements, lines or pages, that will be displayed as a result. Example:
{"data":{"type":"page","children":["Some page text",{"type":"button","onClick":{"action":"getPage","args":["main"]},"text":"⬅️ Back"}]}}
You also can use client-side code to generate pages/components/etc similar to server-side generation.
The design of the modules is inspired by CommonJS system, but is not its exact implementation. See notes below:
- To handle actions you need to use
registerAction(name: string, callback: (...args: any[]) => any | Promise<any>): Promise<void> - You can use top-level await in your modules
- Like CommonJS, MFElements Module System (MfeMS) does not evaluating same module file twice so changes made to exported object will be transferred to any other modules that using same exported object
requireAsyncis default module loader due to unusability of its sync version in InternetAsyncFunctionconstructor is available at the current module scopeAsyncFunctionandFunctionconstructors creates functions at the current module scope- No
windowvariable. Modules are evaluated in separated workers/threads. To check if some global variable exists usetypeof variableName !== "undefined"expression __filename,__dirname,module,module.exportsandexportsare defined and accessible like in CommonJSmoduleobject has no more props- Separated workers are created for each module specified in the page template but not for its children
- You have no direct access to worker's methods and objects like
onmessage,onerror,onmessageerror,postMessage,terminate,importScriptsandglobalThis. - Global
APIobject to access API controller from main thread. Try to not to spawn deadlocks withregisterAction😄
Function to register an API listener internally by a script (prevents requests to API server). You can define your own actions and use them in page template. You can also redefine "standard" methods like getIndex() — all you need is jast a
function nextIndexHandler(){
return {
type: 'page',
children: ['Hello from script']
}
}
registerAction('getIndex', nextIndexHandler);A-a-and... It's done) just try to load first page after visiting the second one
The main module loader. Any module loaded can contain top-level await directive. Supports relative paths. Supported module types (by MIME):
application/javascript
* WASM and JSON modules planned to be supported in the near future
Best practices:
If you need to load few modules best you can do is to load them parallel way:
const [ { module1Method1, module1Method2 }, module2, module3 ] = await Promise.all([
'./module1.js',
'./module2.wasm',
'./module3.json'
].map(requireAsync));Using awaits for each requireAsync call is the same with using sync require: modules will be downloaded and evaluated only after previvious module done. This will increase waiting time and decrease chance to register action handler before user interacts with page. So next code will be evaluated but is COMPLETELY WRONG. Do not do something like this:
const module1 = await requireAsync('module1.js');
const module2 = await requireAsync('module2.wasm');
const module3 = await requireAsync('module3.json');Sync version of requireAsync. Supports all the same like last one do. Is not recommended to use, works just as a fallback. Will be deprecated since 1.0.0 release and completely removed in 2.0.0
Strings which contains full URL to the module and its directory respectively (https://... and so on). Is the same like in CommonJS
module object differs from CommonJS standard: it has no properties except exports
module.exports and exports is one object, like in CommonJS. It means you can export named variables directly to exports. But if you reassign module.exports to something else all the named variables will be lost. To achieve the best development practice, just do export only named variables or only default export from one module. To achieve the problem try something like this:
Module
module.exports = { key: 'reassigned module.exports' };
exports.myVar = 'test export';Main
const module = await requireAsync('./module.js');
console.log(module); // { key: 'reassigned module.exports' }This constructors just creates functions in module scope. The new AsyncFunction constructor creates asynchronous function with support for await directives. How to use such constructors and advantages/disadvantages you may find at MDN.
Is just a Proxy object, works the same as API controller in the main thread.
Usage:
const result = await API.getIndexFallback();