PuzzleJs implements micro-services architecture into front-end. The idea of micro-frontend comes from BigPipe, Facebook’s fundamental redesign of the dynamic web page serving system. Here is how a PuzzleJs website architecture looks like.
There are two types of PuzzleJs implementation, storefront and gateway.
Storefront is the main application that handles requests coming from user’s browser. A basic storefront application has two important configurations.
Whenever a storefront Application starts, PuzzleJs executes some steps before creating http server to handle requests.
After compiling html files into javascript functions it creates a http server. Whenever a request comes, response is sent with several steps.
Gateway is the application where you can implement your fragments and apis. It is responsible for collecting data from other applications and rendering smaller html contents(fragments) with them. Gateways can be used for these features.
Fragments are small html contents which can work standalone and has independent data for other html contents. Think about an e-commerce website.
There are 5 fragments in this page. They are completely different applications independent from each other and communicating others through a shared publish-subscribe bus.
Fragments can have multiple parts. Same fragment can put content into header and footer or even a meta tag. Check Template
There are 4 types of fragments
Name | Description |
---|---|
Chunked | Storefront will stream this fragment’s contents into browser with individual chunk |
ShouldWait | Storefront will send this fragments contents in initial chunk |
Primary | Storefront will send this fragments contents in initial chunk and reflect gateways status code |
Static | This fragment contents are fetched on compile time and storefront won’t request to gateway again for this. It is sent on initial chunk |
Chunked fragments are sent after initial chunk whenever they are ready. You can check them in tcp stream using curl --raw http://127.0.0.1:8080
, and example below.
0xf3
<html>
<head></head>
<body>
<div>Initial chunk</div>
0xa2
<div>Chunked Div</div>
</body>
</html>
There two hex numbers in stream (0xf3, 0xa2). They are representing the size of the chunk. Lets assume that second chunk is sent after 600ms. Browser already parsed and rendered contents of the first chunk. Whenever second chunk is arrived browser parses it too and renders it’s contents.
Chunked fragments are sent after the initial chunk. Like the example above <div>Chunked Div</div>
is a chunked fragment
ShouldWait fragments are the fragments that should be waited and injected into initial chunk. Let’s assume a page with 2 fragments. One is shouldWait and the other is chunked.
0xf3
<html>
<head></head>
<body>
<div>ShouldWait fragment</div>
0xa2
<div>Chunked Fragment</div>
</body>
</html>
If a fragment or it’s partial is in <head>
, PuzzleJs makes that fragment shouldWait automatically.
ShouldWait fragments will be requested from gateways by storefront on each request. Adding meta tags is a great example for usage of shouldWait fragments.
These fragments has all features of ShouldWait Fragments, but in addition primary fragments are unique and the main content of the page.
A primary fragment can change status code and the headers of the response. Assume that there is a fragment that brings product contents by product id. But requested product id doesn’t exists. Gateway can decide to send reponse status code 404 with product not found content.
Or gateway can redirect storefront using 301 and location
header.
These fragments are fetched during compile time and directly injected into compiled function. They are sent on initial chunk. Only gateway can decide if a fragment should be static or not.
To install PuzzleJs with Yarn or Npm, simply:
yarn add @puzzle-js/core
npm install @puzzle-js/core
Then you can start using it
const PuzzleJs = require('@puzzle-js/core');
An example of starting storefront with simple configuration.
const Storefront = require('@puzzle-js/core');
const storefront = new Storefront({
serverOptions: {
port: 4444
},
gateways: [{
name: 'Gateway',
url: 'https://127.0.0.1:4448/',
}],
pages: [{
name: 'page-name',
url: '/mypage',
html: '<template><!DOCTYPE html><html><head></head><body><div>PuzzeJs</div><fragment from="Gateway" name="example"></fragment></body></html></template>'
}],
dependencies: []
});
storefront.init(() => {
console.log('Storefront ready to respond');
});
You can provide configuration as object or you can use Configurator.
Property | Type | Required | Description |
---|---|---|---|
serverOptions | ServerOptions | True | server options |
gateways | gateway | True | Gateway Configuration |
pages | page | True | Page Configuration |
dependencies | dependency[] | True | Shared dependencies (React, angular, etc.) or [] |
Property | Type | Required | Description |
---|---|---|---|
name | string | True | Port to listen |
url | string | True | Storefront will try to request this url to get configuration and fragments |
assetUrl | string | False | This url is for browsers to connect gateways |
As fragment requests are between storefront and gateways at back-end, we can optimize it for best network performance. It is really useful if you are using Kubernetes etc.
Let’s assume two applications running on different port on same host.
Name | Public | Inner |
---|---|---|
Storefront | https://storefront.com | https://127.0.0.1:4444 |
Gateway | https://gateway.com | https://127.0.0.1:4445 |
When browser wants to access gateway, it should use its public link. But whenever storefront wants to access gateway, it can use localhost. So, for the best performance we can use this config for gateways.
{
name: 'Gateway',
url: 'https://127.0.0.1:4445',
assetUrl: 'https://gateway.com'
}
Page configuration defines how PuzzleJs will respond to matching routes.
Property | Type | Required | Description |
---|---|---|---|
name | string | True | Port to listen |
url | string or string[] or regex or regex[] | True | PuzzleJs uses ExpressJs for routing, check ExpressJs documentation for advanced usage |
html | string | True | html content of page. Please check Templating for detailed info |
PuzzleJs templates are consist of two parts. <script>
is the controller of the page, <template>
is controlled content.
A simple template example.
<script>
module.exports = {
onCreate(){
this.cacheBuster = Date.now();
}
onRequest(req){
//Each page has its own scope.
this.rand = Math.random();
//Or you can change req, it will be exposed to <template>
}
}
</script>
<template>
<!DOCTYPE html>
<html>
<head>
<title>Page Version: ${this.cacheBuster}</title>
</head>
<body>
<div>Random: ${this.rand}</div>
<div>Requested Url: ${req.url}</div>
<fragment from="GatewayName" name="header"></fragment>
<div>
<h1>Content</h1>
<fragment from="AnotherGatewayName" name="content"></footer>
</div>
</body>
</html>
</template>
PuzzleJs creates a page instance where you can add listeners to some events and change scope variables. Page scripts are optional. You will still be able to access request or this with template expressions.
These are the events you can use.
Name | params | When it is triggered |
---|---|---|
onCreate | () | When PuzzleJs starts compiling the page for the first time |
onRequest | (req) | on each request |
onChunk | (string) | on each chunk sent |
onResponseEnd | () | on all chunks sent, connection closed |
PuzzleJs compiles this part of html into executable javascript function. Read Compiling for more information about this process. You can use PuzzleJs expressions inside this part to access page instance or request.
Expressions
Expressions can be written with ${expression}
. It can be multiple line too.
<template>
<html>
<head></head>
<body>
Requested Url: ${req.url}
</body>
</html>
</template>
Expressions also support conditional statements, and few loop statements. Full support list: if, for, else, switch}
Example if
<div>
${if(this.rand > 5){}
<div>It is higher than 5</div>
${}}
</div>
Example for
<div>
${for(var x = 0; x < this.rand; x++){}
<div>Iterator: ${x}</div>
${}}
</div>
Fragments
You can use <fragment>
tag to define fragments. It has some attributes.
name | required | example | description |
---|---|---|---|
name | true | name=”fragment-name” | Name of the fragment (it has to be exactly the name you defined on your gateway) |
from | true | from=”gateway-name” | Name of the gateway fragment will be fetched from |
shouldWait | false | shouldWait | PuzzleJs will wait for this fragment to send first response. Check Fragment Types |
primary | false | primary | PuzzleJs will wait for this fragment, and reflect its status code too. There can be only one primary fragment on each page. Check Fragment Types |
partial | false | partial=”meta” | If a fragment wants to content into two different places you can use partial . Common usage: A product fragment which has product html but also has meta tags for it. Default partial is main |
All the other attributes will be passed to gateway in query string. So you can configure same fragment with different configuration on each page.
Partial Example
<html>
<head></head>
<body>
<header>
<fragment from="Gateway" name="Product" partial="header-content"></fragment>
</header>
<main>
<fragment from="Gateway" name="Product"></fragment>
</main>
</body>
</html>
Inline Scripts
To inline scripts you should use <puzzle-script>console.log('inline content')</puzzle-script>
Configurator is used for creating configuration for PuzzleJs with dependency injection and validation. There are two types of configurator, storefront and gateway.
To create a storefront configurator:
const { StorefrontConfigurator } = require('@puzzle-js/core');
const configurator = new StorefrontConfigurator();
To create a gateway configurator:
const { GatewayConfigurator } = require('@puzzle-js/core');
const configurator = new GatewayConfigurator();
Adding config and injecting into PuzzleJs
const { GatewayConfigurator, Gateway } = require('@puzzle-js/core');
const configurator = new GatewayConfigurator();
configurator.config({
api: [],
name: 'Gateway',
url: 'http://gateway.com/',
serverOptions: {
port: 32
},
fragments: [
{
versions: {
'1.0.0': {
assets: [],
dependencies: [],
}
},
version: '1.0.0',
testCookie: '',
render: {
url: '',
},
name: 'test'
}
]
});
const gateway = new Gateway(configurator);
Configurator will help you validate your configuration for PuzzleJs
configurator.config({
...
port: 'not a valid port'
...
})
Configurator will throw error telling you that port should be number
not string
. Also whenever port is not provided it will throw error too.
Custom objects can be injected into configuration.
Let’s assume we want to inject middleware into api. We can do it without configurator like this
new Gateway({
api: [
{
name: 'api',
liveVersion: '1.0.0',
testCookie: 'test',
versions: {
'1.0.0': {
endpoints: [
{
path: '/',
middlewares: [(req, res, next) => {
req.middlewareWorker = true;
next();
}],
method: HTTP_METHODS.POST,
controller: 'getItems'
}
]
}
}
}
],
fragmentsFolder: '',
name: 'Gateway',
url: 'http://gateway.com',
serverOptions: {
port: 32
},
fragments: []
})
When you want to split config into standalone json files you can’t use js in it. When you need this, you can use configurator.
configurator.register("{middleware}", ENUMS.INJECTABLE.MIDDLEWARE, (req, res, next) => {
req.middlewareWorker = true;
next();
});
configurator.config({
api: [
{
name: 'api',
liveVersion: '1.0.0',
testCookie: 'test',
versions: {
'1.0.0': {
endpoints: [
{
path: '/',
middlewares: ['{middleware}'],
method: HTTP_METHODS.POST,
controller: 'getItems'
}
]
}
}
}
],
name: 'Gateway',
url: 'http://gateway.com/',
serverOptions: {
port: 32
},
fragments: []
});
const gateway = new Gateway(configurator);
With this feature, you can easily manage your configuration on a separate file. There are 3 types of injectables.
Name | Description |
---|---|
Middleware | Used for adding express middleware |
Handler | Used for custom handlers, read Handler |
Custom | Can be used for anything |
Whenever a custom handler is not provided for an api or fragment, PuzzleJs tries to require its module by itself. Let’s assume an api like this exists on gateway.
{
name: "api-example",
testCookie: "api_cookie",
liveVersion: "1.0.0",
versions: {
"1.0.0": {
endpoints: [
{
method: ENUMS.HTTP_METHODS.GET,
path: "/items/?",
controller: "getItems"
}
]
}
}
}
PuzzleJs tries to handler = require('./src/api/api-example/1.0.0/index.js');
and whenever a request comes it will try to run handler.getItems(req, res)
. If that module doesn’t exist, it will throw an error. But you can also provide custom handlers using dependency injection feature of Configurator.
const configurator = new GatewayConfigurator();
configurator.register("{customhandler}", ENUMS.INJECTABLE.HANDLER, {
getItems(req, res){
res.send('PuzzleJs')
}
});
configurator.config({
...
name: "api-example",
testCookie: "api_cookie",
liveVersion: "1.0.0",
versions: {
"1.0.0": {
handler: '{customhandler}'
endpoints: [
{
method: ENUMS.HTTP_METHODS.GET,
path: "/items/",
controller: "getItems"
}
]
}
}
...
})
PuzzleJs has some inner configurations you can’t change using any Storefront or Gateway configuration. They can be changed using envrionment variables
Env Variable Name | Default | Description |
---|---|---|
DEFAULT_POLLING_INTERVAL | 1250 | Interval in ms storefront checks if gateway is updated |
CONTENT_NOT_FOUND_ERROR | <script>console.log('Fragment Part does not exists')</script> |
Whenever fragment content not found, it is injected into html |
DEFAULT_CONTENT_TIMEOUT | 15000 | PuzzleJs waits for miliseconds for gateway fragment response. Status Code will be 500 on timeout |
RENDER_MODE_QUERY_NAME | ‘__renderMode’ | Storefront sends request to gateway using this query parameter to get stream type response |
PREVIEW_PARTIAL_QUERY_NAME | ‘__partial’ | This query parameter is used for selecting a partial to render on preview mode |
API_ROUTE_PREFIX | ‘api’ | prefix for apis: gateway.com/api/api-name/endpoint |
GATEWAY_PREPERATION_CHECK_INTERVAL | 200 | When storefront is booting up, it checks for gateways in 200 ms interval |
CHEERIO_CONFIGURATION | {normalizeWhitespace: true,recognizeSelfClosing: true,xmlMode: true,lowerCaseAttributeNames: true,decodeEntities: false} |
Cheerio html parsing configuration, stringify object to change config |
TEMPLATE_FRAGMENT_TAG_NAME | ‘fragment’ | Tag name of fragments in templates |
DEFAULT_GZIP_EXTENSIONS | ['.js', '.css'] |
These extensions will be gzipped. You can use Json string to change |
DEBUG_QUERY_NAME | ‘__debug’ | It enables debug information on console |
DEBUG_INFORMATION | false | It enables debug information globally |
NO_COMPRESS_QUERY_NAME | ‘__noCompress’ | It disables compression for that request |
** HTTP/2 Does not supported yet | Property | Type | Required | Description | |-|-|-|-| | port | number | True | Port for server to listen | | hostname | string | False | Hostname for server | | http2 | boolean | False | HTTP2 option (Not Supported Yet)| | https | serverHttpsOptions | False | HTTPS options |
| Property | Type | Required | Description | |-|-|-|-| | cert | string | True | Certificate for HTTPS | | key | string | True | Key for HTTPS |