SFRA Architecture
SFRA provides an app_storefront_base
cartridge and a
server
module. A storefront site uses the SFRA base cartridge and overlay
plugin, LINK, and custom cartridge functionality to create a cartridge stack. Define the order
of the cartridge stack by configuring the cartridge path in Business Manager.
The base cartridge contains only the functionality common to most sites. You can layer functionality over the base cartridge with plug-in cartridges, LINK cartridges, and custom code cartridges. B2C Commerce provides plug-in cartridges that provide other features, such as gift registries, Apple Pay, product comparisons, and middleware capability. LINK partners, such as PayPal and Bazaarvoice, provide LINK cartridges for third-party integrations. You can create one or more custom cartridges to override portions of the base cartridge and customize the functionality and branding of your storefront site.
The app_storefront_base
cartridge includes multiple models These models use the
B2C Commerce script API to retrieve data from the platform for a functional area of the
application, such as orders. The models then construct a JSON object that you can use to
render a template.
The server
module provides objects containing data from HTTP requests,
responses, and session objects. The server
module registers routes that map
a URL to code that's executed when B2C Commerce detects the URL. It uses a modern JavaScript
approach that is conceptually similar to NodeJS's Express.
The app_storefront_base
cartridge models and server
module
objects are guaranteed to be backward compatible between major point releases. The guarantee
lets you adopt new features more easily and maintains a clear distinction between base and
custom code, which can help you troubleshoot issues and adopt bug fixes.
app_storefront_base
cartridge or
server
module voids the guarantee of backward compatibility and hinders
feature and fix adoption. Instead, use B2C Commerce script methods to extend base cartridge
functionality. The JSON objects created by the server
module and the
app_storefront_base
models retain their structure and don't change
properties between point releases. However, Commerce Cloud Engineering reserves the right to
change how these objects are created.
A typical cartridge stack includes several layers, as shown in this table:
Layer | Description | Tips |
---|---|---|
Custom | Adds specific customizations for your brand and organization. Perform all customizations of the base, LINK, and product cartridges in custom cartridges for easy adoption of future features. | Rename all custom cartridges with
app_custom_* to make them easy to
distinguish.
|
LINK | Adds third-party functionality to your site. You can integrate features from LINK partners, such as payment providers and tax services. | You can import LINK partner data, such as tax tables or inventory feeds. See specific LINK cartridge documentation for more information. |
Plug-in | Enhances the ecommerce capabilities provided by Commerce Cloud or anyone else in the Salesforce community. Cartridges provided by Commerce Cloud let you integrate (optional) products and features such as product compare and gift registry. | Plug-ins can create custom objects or data that are specific to a product or feature. |
Base | Core functionality modified only by the Commerce Cloud team or through contributions to GitHub. The core cartridge includes best-practice code for features used by most customers. In addition to the default features, the base cartridge contains features that can be configured in Business Manager. | Some of the features in the base cartridge are configured in Business Manager, such as pick up in store. |
The following graphic shows a typical cartridge stack:
The cartridge path is always searched left to right, and the first controller or pipeline found with a particular name is used. This behavior allows cartridges earlier on the path to override the functionality of cartridges later on the path. The cartridge path for this stack is:
app_custom_mybrand:app_custom_mysite:LINK_bazaarvoice:LINK_wordpress:plugin_applepay:plugin_comparison:app_storefront_base
B2C Commerce provides demo data that lets you view and explore the base cartridge as a working site.
Base Cartridge Architecture
We highly recommend that you do not modify app_storefront_base
. Instead, create
your own cartridge and add it as an overlay in the Business Manager cartridge path. Using an
overlay cartridge lets you upgrade to a newer version of SFRA without having to manually
cherry-pick changes and perform manual merges. You can still adjust your custom code, but
the upgrade and feature adoption process is quicker and less painful.
storefront-reference-architecture
dw.json //used to upload code
package.json
cartridges
βββ app_storefront_base
βββ client //client-side JavaScript and CSS
β βββ js
β βββ scss
βββ controllers // business logic for the application
βββ forms
βββ models //gets data from server and provides it as JSON objects
βββ scripts //reusable functionality
β βββ cart
β βββ factories
β βββ helpers
β βββ payment
β βββ search
βββ static //static resources unlikely to change, such as branding images
βββ templates //ISML templates
βββ default
βββ resources
...
βββ modules
βββ server
...
The Modules Folder and the Server Module
A
server module in the SFRA global modules
folder provides
the routing functionality for controllers. Every SFRA controller requires
this module. Any JavaScript module included in the
modules
folder is globally available and can be required
without a path in any controller or script.
The following example
requires the server
module in the
modules
folder.
var server = require('server');
The modules
folder is a separate cartridge, so you can easily upload it to the
platform. However, you don't have to include it on the cartridge path. For information on
using the server
module, see Extending the Base Cartridge Architecture.
Don't directly extend or customize the
server
module. If you customize it directly, B2C Commerce can't be held responsible for changes that are not backward
compatible in future versions of SFRA.
However, if you want to
extend or customize server
module functionality, you can
create your own module and place it in the modules
folder. You can require server
module functions and
extend them in your own module as you would any JavaScript
function.
ViewModels and Controllers
SFRA uses a variant
of Model-View-Controller architecture. Controllers handle information from
the user, create ViewModels, and render pages. The ViewModels request
data from B2C Commerce, convert B2C Commerce script API objects into pure JSON
objects, and apply business logic.
A controller requests data from B2C Commerce and passes the returned objects to a ViewModel to be converted into a serializable JavaScript object.
- A B2C Commerce script object sometimes behaves like a Java object, not a JavaScript object, whereas a model is a serializable JavaScript object.
- A ViewModel provides the data to render pages in the application and often combines data from multiple B2C Commerce script objects.
Defining Endpoints
SFRA defines endpoints (URIs) with a Controller-RouteName
syntax. Defining an
endpoint depends on your controller's filename and the routes defined within it.
The syntax of your endpoint depends on the SEO options you choose for your site. When developing, you can use the Commerce Cloud Standard URL Syntax without SEO options. This approach makes it easy to test your controllers.
Example: Defining an Endpoint for Your Home Page
http://www.mystore.com/on/demandware.store/βSites-YourShopHere-Site/βEN_US/Home-Show
You create a Home.js module in the controller folder with the following code:
'use strict';
var server = require('server'); //the server module is used by all controllers
var cache = require('*/cartridge/scripts/middleware/cache');
server.get('Show', cache.applyDefaultCache, function (req, res, next) { //registers the Show route for the Home module
res.render('/home/homepage'); //renders the hompage template
next(); //notifies middleware chain that it can move to the next step or terminate if this is the last step.
});
module.exports = server.exports();
The
Home-Show
route consists of the module name (Home.js
)
and the first parameter in the server.get
function
(Show
).
Defining Routes
The server
module use
function takes the RouteName
of the function to execute as the first argument. SFRA provides two
utility methods to call the use
function
(server.get
and server.post
) that make
sure the URI is either a GET or POST request.
This functionality is different from many other frameworks, which define routes by overriding anchor functionality in the URI to define actions to execute.
How Controllers Are Executed
Controller-RouteName
to execute. In this example, the
filename is Page.js
and the route name is
Show
. The Page.js
file must be stored in
the controllers
folder for B2C Commerce to recognize it as a
controller.
http://www.mystore.com/on/demandware.store/Sites-YourShopHere-Site/EN_US/Page-Show
The B2C Commerce server executes the first controller in the cartridge path with the correct name. B2C Commerce recognizes both SiteGenesis JavaScript Controller (SGJC) and SFRA controllers as equal and doesn't prioritize one over the other. If no controller is found in the cartridge path, the server executes the first pipeline file found in the cartridge path. Because SFRA doesn't use pipelines, this pipeline is the first one found in a custom cartridge.
When the controller is located, all
require
statements at the beginning of the controller are
executed. These statements must include a require
for the
server
module. The server
module is
located in the modules
folder, which is a peer of the
app_storefront_base
cartridge. Requiring the
server
module returns an empty server
object.
module.exports = server.exports();
This
line is present in all controllers. Calling
server.exports()
causes the server to register all
functions in the controller that use the server.get
,
server.post
, or server.use
functions as
routes. The server then executes the function whose first parameter
matches the route name in the URI.
For example, if you assume the
URI in the previous example that ends in Page-Show
,
B2C Commerce registers all functions in Page.js
and then
executes the Show
function.
server.get('Show', locale, function (req, res, next) {
res.render('/home/homepage');
next();
});
Rendering a JSON String
Page.js
.
var server = require('server');
server.get('Show', function(req, res, next) {
res.json({ value: 'Hello World'});
next();
});
module.exports = server.exports();
Page.js
file creates a route for a URL.
http://sandbox-host-name/on/demandware.store/site-name/en_US/Page-Show
Whenever
that URL is called, the provided function is executed and renders a page with the following
Content-Type
header:
Content-Type: application/json
The rendered page also includes the
following body:
{ value: 'Hello World '}
Protecting Route Access
You can enhance this
code by adding the server.middleware.https
parameter
after Show
, to limit this route to only allow HTTPS
requests. This example restricts the Account-Show
route
to HTTPS.
server.get('Show', server.middleware.https, function (req, res, next) {
var accountModel = getModel(req);
if (accountModel) {
res.render('account/accountdashboard', {
account: accountModel,
accountlanding: true
});
} else {
res.redirect(URLUtils.url('Login-Show'));
}
next();
});
server
module.Using server.use, server.get, or server.post
For
a server.get
or server.post
function,
the first parameter is always the name of the route (the URL endpoint).
The last parameter is always the main function for the endpoint. Usually,
the main function in the controller renders a page for the storefront or
redirects to another controller.
You can add as many parameters in
between the first and last parameter as you want. Each parameter specifies
a function to be executed in order and can let the next step be executed
(by calling next()
or reject a request by calling
next(new Error())
.
Example 1: Conditionally Executing a Middleware Step
This example shows a main function that conditionally executes
next()
or next(new Error())
depending on
whether an Apple Pay order is being placed.
server.post('Submit', function (req, res, next) {
var order = OrderMgr.getOrder(req.querystring.order_id);
if (!order && req.querystring.order_token !== order.getOrderToken()) {
return next(new Error('Order token does not match'));
}
var orderPlacementStatus = orderHelpers.placeOrder(order);
if (orderPlacementStatus.error) {
return next(new Error('Could not place order'));
}
var orderModel = orderHelpers.buildOrderModel(order);
res.render('checkout/confirmation/confirmation', { order: orderModel });
return next();
});
The code executed between the first and last parameter is referred to as middleware and
the entire process is called chaining. You can create middleware functions to limit
route access, add information to the data object passed to the template for rendering, or
for any other purpose. One limitation to this approach is that you must call the
next
function at the end of every step in the chain. Otherwise, the next
function in the chain is not executed.
Middleware
Each step of a middleware chain is a function that takes
three arguments: req
, res
, and next
, in
that order.
req
This argument is short for Request
. It contains information about the
server request that initiated execution. The req
object contains user input
information, such as the content-type that the user accepts, the user's login and locale
information, or session information. The req
argument parses query string
parameters and assigns them to the req.querystring
object.
res
This argument is short for Response
. It contains functionality for
outputting data back to the client. For example:
-
res.cacheExpiration(24)
: Sets cache expiration to 24 hours from now. -
res.render(templateName, data)
: Outputs an ISML template back to the client and assignsdata
topdict
. -
res.json(data)
: Prints a JSON object back to the screen. It's helpful in creating AJAX service endpoints that you want to execute from the client-side scripts. -
res.setViewData(data)
: Doesn't render anything, but sets the output object. This behavior can be helpful if you want to add multiple objects to thepdict
of the template. Thepdict
contains the information for rendering that is passed to the template.setViewData
merges all the data that you passed into a single object, so you can call it at every step of the middleware chain. For example, you can create a separate middleware function that retrieves information about a user's locale to render a language switch on the page. The output object of the ISML template or JSON is set after every step of the middleware chain is complete.You can also use the ViewData object to extend the data created in a controller that you are extending. You don't have to duplicate the logic used in the original controller to get the data. You only have to add the additional data to the ViewData object and render it.
next()
Executing the next
function notifies the server that you are done with a
middleware step so that it can execute the next step in the chain.
By chaining multiple middleware functions, you can compartmentalize your code and extend or modify routes without having to rewrite them.
Event Emitters
The server
module emits events at every step of execution and you can subscribe
and unsubscribe to events from a given route. Use an event emitter to override the
middleware chain by removing the event listener and creating a new one. However, if you have
to change individual steps in a middleware chain, we recommend that you replace a route.
While SFRA does supply removeListener
and
removeAllListener
functions, they don't recognize named event emitters.
For this reason, it isn't possible to use Step
event emitters to override a
specific step in the middleware chain.
The following is a list of currently supported events:
-
route:BeforeComplete
is emitted before theroute:Complete
event but after all middleware functions. Used to store user submitted data to the database; most commonly in forms. -
route:Complete
is emitted after all steps in the chain finish execution. Subscribed to by the server to render ISML or JSON back to the client. -
route:Redirect
is emitted beforeres.redirect
execution. -
route:Start
is emitted as before middleware chain execution. -
route:Step
is emitted before execution of each step in the middleware chain.
All events provide both req
and
res
as parameters to all handlers.
Subscribing or unsubscribing to an event lets you do complex and interesting things. For example,
the server subscribes to the route:Complete
event to render ISML back to
the client. If you want to use something other than ISML to render the content of your
template, you can unsubscribe from the route:Complete
event. You can
subscribe to it again with a function that uses your own rendering engine instead of ISML,
without modifying any of the existing controllers.
OnRequest and OnSession Event Handlers
The OnRequest
and OnSession
event handlers that were
implemented as pipelines in SiteGenesis Pipeline Processor (SGPP) and as controllers in SGJC
are not used in SFRA. You still have access to request and session data using the middleware
req
(request) and res
(response) objects. However, SFRA
avoids using OnRequest and OnSession anywhere in our code outside of the
server
module.
If you want to implement OnRequest
and OnSession
, they must be
implemented through hooks. B2C Commerce looks for OnSession
as the controller
name, but the new architecture doesn't do that. The only difference between a hook and
controller is that the hook doesn't have access to the req
and
res
objects.
Extending Base Cartridge Architecture
B2C Commerce owns and maintains the app_storefront_base
cartridge. The base
cartridge is hosted on GitHub and anyone can contribute to it through a pull request. We
test all pull requests before they are approved.
app_storefront_base
cartridge contains controllers for
business logic, models with JSON objects populated from the B2C Commerce script
API, and ISML templates. It also contains the modules
directory with a server
module for SFRA. modules
folder or TopLevel
package is globally available and can be required without a path. You can
add your own custom modules to the modules
folder.
Extension example Plugin_applepay
We provide a sample plugin cartridge as part of the SFRA project that demonstrates how to selectively add custom functionality to the base cartridge. Add this cartridge to the left of the base cartridge on the cartridge path to observe the functionality.
We publish an npm node named sgmf-scripts. This node has tools to compile the CSS and scripts for your storefront site. Use the tools to create a CSS and client-side JavaScript that includes functionality from other cartridges.
Best Practices for Storefront Reference Architecture Maintenance
Make sure to regularly update your app_cartridge_base
and
server
module to have access to the most up-to-date security and bug
fixes. Regularly updating your base cartridge.