This post is going to describe how you can build next-generation web client technologies into Share and standalone web-clients. I’ve taken a pre-existing tutorial that I found on the Smashing Magazine web site and have adapted it for use in Surf. This is simply a demonstration that Share does not prohibit the use of any of these technologies and the method shown here is by no means the only way in which this task could be accomplished. The intention here is to shed further light on some of the Surf concepts.
You might be wondering why would you want to do this? Why not just go the whole hog and re-implement everything on a Node stack?
One reason is that you might simply want to add an existing page into Share and that you don’t want to have to reimplement all the features that Share currently provides. It’s also important to remember that through its use of Surf, Share is able to take care of a host of issues that you never need worry about - a single point of authentication to an Alfresco Repository across multiple REST APIs (CMIS, WebScripts, Public API) being the most obvious.
Rather than working with Share though, in this instance we’re going to create a standalone client using the Aikau Maven Archetype. We’re not going to be using Aikau, but it’s worth being aware that the archetype will build you a client that has everything you need to authenticate against an Alfresco Repository.
I’m not going to go through the details of the next-generation concepts as these are well documented in the Smashing Magazine article and you can read up on them there if you’re not familiar with them yourself.
Essentially we’re going to be writing ES6 JavaScript that is transpiled to ES5 via Babel, compiled into a single module (with associated source maps) using WebPack and minimized using Uglify. The whole JavaScript build process will be taken care of using Gulp and will all be wrapped within a Maven build.
If you don't want to manually follow all the steps you can find the source code that you would otherwise build in this GitHub repository.
You have Node.js, NPM and Maven installed
Execute the following command in a location where you want to create your project
mvn archetype:generate -DarchetypeCatalog=https://artifacts.alfresco.com/nexus/content/groups/public/archetype-catalog.xml -DarchetypeGroupId=org.alfresco -DarchetypeArtifactId=aikau-sample-archetype -DarchetypeVersion=RELEASE
Enter suitable suitable group and artifact ids and accept the remaining defaults (for example, I’ve used “org.alfresco” as the groupId and “next-gen” as the artifactId).
Building from an archetype is described in more detail in the Aikau tutorial.
Run the following command:
npm install -g gulp, webpack
Create your package.json file in the root of the project
{
'name': 'next-gen',
'version': '0.0.1',
'devDependencies': {
'babel': '^5.8.23',
'babel-core': '^5.8.24',
'babel-eslint': '^4.1.1',
'babel-loader': '^5.3.2',
'eslint': '^1.4.1',
'gulp': '^3.9.0',
'gulp-babel': '^6.1.2',
'gulp-rename': '^1.2.2',
'gulp-sourcemaps': '^1.5.2',
'gulp-uglify': '^1.4.1',
'webpack': '^1.12.1',
'webpack-stream': '^2.1.0'
}
}
Create a webpack.config.js file in the root folder:
module.exports = {
entry: './src/js/index.js',
output: {
library: 'legoQuotes',
libraryTarget: 'umd',
filename: 'lib/legoQuotes.js'
},
externals: [
{
lodash: {
root: '_',
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash'
}
}
],
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: {
compact: false
}
}
]
}
};
Create a gulpfile.js file in the root folder:
var gulp = require( 'gulp' );
var webpack = require( 'webpack-stream' );
var sourcemaps = require( 'gulp-sourcemaps' );
var babel = require('gulp-babel');
var rename = require( 'gulp-rename' );
var uglify = require( 'gulp-uglify' );
gulp.task('default', function() {
return gulp.src( 'src/js/index.js' )
.pipe( babel() )
.pipe( webpack( require( './webpack.config.js' ) ) )
.pipe( gulp.dest( './src/js/dist' ) )
.pipe( sourcemaps.init( { loadMaps: true } ) )
.pipe( uglify() )
.pipe( rename( 'legoQuotes.min.js' ) )
.pipe( sourcemaps.write( './' ) )
.pipe( gulp.dest( './src/js/dist/' ) );
});
For more information on what these files do, you should read the Smashing Magazine article - the purpose of this blog is not to explain these things, simply to show how they can be used with Share/Surf.
This should be placed in the “src/js” folder of your project - note that it is intentionally not in the “src/main” folder as we are going to build the JavaScript separately from the rest of the project. The files you want are:
These can be found in the linked source from the Smashing Magazine article, they are only provided for something to build!
Copy the 'style.css' from the linked source into a new folder called 'css' in 'src/main/webapp/'. We could use WebPack to bundle up CSS resources but we're going to let Surf take care of this as we'll see later.
We now want to update our pom.xml to allow us to build our next-gen resources as part of the Maven build. Add the following to the <build> <plugins> element:
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<id>Installation</id>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<echo>NPM Package Installation</echo>
<exec executable='npm' dir='${project.basedir}'>
<arg line='install' />
</exec>
<echo>Run Gulp Build</echo>
<echo>Build and overlay JS</echo>
<exec executable='gulp' dir='${project.basedir}'>
</exec>
</target>
</configuration>
</execution>
</executions>
</plugin>
Here we’re using the Maven Ant Run plugin to get all the required Node packages (defined in the package.json file) and call Gulp to perform the build.
So somewhat impressively we’re using Ant, Maven and Gulp!
Next we want to ensure that we copy our JavaScript and required Node modules into the web application that we’re going to build. Add the following plugin configuration after the previous entry that you just added:
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>copy-node-modules</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/aikau-sample/node_modules</outputDirectory>
<resources>
<resource>
<directory>${basedir}/node_modules</directory>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-javascript</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/aikau-sample/js</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/js</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
Finally we need to make one more change. The Aikau archetype configures the Jetty plugin to use the “src” rather than “target” location to enable fast development, but for a cleaner build we are copying the “node_modules” into target so need to change the plugin configuration appropriately.
Update the <webApp> section so that all instances of “src/main/webapp” are replaced with “target/aikau-sample”
We now have our build in place, so it’s time to create the required Surf objects.
Surf has a complex but powerful set of objects for constructing pages. One of the goals of Aikau was to hide these objects away from developers to allow them to focus on creating pages with just WebScripts. We’re now going to build some of these objects and it may become apparent why we wanted to hide this complexity away.
The first thing we need to create is a Page object. This is defined as XML file and the key thing you can define in this file is what level of authentication the user requires to access the page. Here we’re going to use “none” because we want to avoid a login step - however, you could just as easily set the value to be “user” or “admin”.
Create a file called “next-gen-page.xml” in the “src/main/webapp/WEB-INF/surf-config/pages/” folder. It should contain the following XML.
<?xml version='1.0' encoding='UTF-8'?>
<page>
<id>ngp</id>
<template-instance>next-gen-template-instance</template-instance>
<authentication>none</authentication>
</page>
The Page references a Template-Instance to be rendered. This is the next file to declare. Create a file called “next-gen-template-instance.xml” in the “src/main/webapp/WEB-INF/surf-config/template-instances” folder. It should contain the following XML:
<?xml version='1.0' encoding='UTF-8'?>
<template-instance>
<template-type>next-gen-template-type</template-type>
</template-instance>
The Template-Instance references a Template-Type - this needs to be declared. Create a file called “next-gen-template-type.xml” in the “src/main/webapp/WEB-INF/surf-config/template-types” folder. It should contain the following XML:
<?xml version='1.0' encoding='UTF-8'?>
<template-type>
<title>Next Gen Page</title>
<processor mode='view'>
<id>webscript</id>
<uri>/next/gen/page/template</uri>
</processor>
</template-type>
A Template-Type can have a number of different processor modes. We really only care about the “view” mode (the other modes were created for WCM purposes that are not widely used anymore). Surf supports a number of different processors out-of-the-box and it is possible to configure in additional processors. Here we’re using a WebScript processor and are providing the URI to match against a WebScript.
This WebScript needs to be defined. In the “src/main/webapp/WEB-INF/webscripts” folder create the following files:
next-gen-page.get.desc.xml
<webscript>
<shortname>Template For Next Gen Pages</shortname>
<family>Page Templates</family>
<url>/next/gen/page/template</url>
</webscript>
next-gen-page.get.html.ftl
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Lego Quote Module Example</title>
</head>
<body>
<div class='container'>
<blockquote id='quote'></blockquote>
<button id='btnMore'>Get Another Quote</button>
</div>
<script src='${url.context}/node_modules/lodash/index.js'></script>
<script src='${url.context}/node_modules/babel-core/browser-polyfill.js'></script>
<script src='${url.context}/js/dist/legoQuotes.min.js'></script>
<script>
(function(legoQuotes) {
var btn = document.getElementById('btnMore');
var quote = document.getElementById('quote');
function writeQuoteToDom() {
quote.innerHTML = legoQuotes.getRandomQuote();
}
btn.addEventListener('click', writeQuoteToDom);
writeQuoteToDom();
})(legoQuotes);
</script>
</body>
</html>
We don’t want to have to create all of these files (page, template-instance, template-type, WebScript) for every single page in our application (of course if you’re building a Single Page Application then you’re only going to need to do this once). So instead we’re going to abstract the WebScript file contents into an include template and then make it possible to parametrize a WebScript to run as is done in Aikau.
Let’s update the template file so that is looks like this:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Next Gen Example</title>
<script src='${url.context}/node_modules/lodash/index.js'></script>
<script src='${url.context}/node_modules/babel-core/browser-polyfill.js'></script>
<@outputJavaScript/>
<@outputCSS/>
</head>
<body>
<#assign regionId = page.url.templateArgs.webscript?replace('/', '-')/>
<@autoComponentRegion uri='/${page.url.templateArgs.webscript}'/>
</body>
</html>
In this example we are using 3 custom FreeMarker directives that Surf provides:
The first two mark the location where page specific JavaScript and CSS will be output - in a moment we’ll show how to define what should be output.
The <@autoComponentRegion> is used to automatically create a Surf Region and a Surf Component for the WebScript that we’re going to use to define the page. The URI for the WebScript to use is taken from the same UriTemplate that is configured for Aikau pages.
Let’s now create the WebScript for our page. Create the following files in the “src/main/webapp/WEB-INF/webscripts/pages” folder.
First the descriptor: “LegoQuotes.get.desc.xml”
<webscript>
<shortname>LegoQuotes</shortname>
<family>Next Gen Pages</family>
<url>/legoQuotes</url>
</webscript>
The key thing to note here is the <url> - we’re going to be using that when we load our page.
Now the template, “LegoQuotes.get.html.ftl”
<@link rel='stylesheet' type='text/css' href='${url.context}/res/css/style.css'/>
<@script type='text/javascript' src='${url.context}/res/js/dist/legoQuotes.min.js'/>
<@inlineScript>
(function(legoQuotes) {
document.addEventListener('DOMContentLoaded', function(event) {
var btn = document.getElementById( 'btnMore' );
var quote = document.getElementById( 'quote' );
function writeQuoteToDom() {
quote.innerHTML = legoQuotes.getRandomQuote();
}
btn.addEventListener( 'click', writeQuoteToDom );
writeQuoteToDom();
});
})(legoQuotes);
</@>
<div class='container'>
<blockquote id='quote'></blockquote>
<button id='btnMore'>Get Another Quote</button>
</div>
Again, we’re using 3 new FreeMarker directives
The <@link> directive is how we reference CSS files to be output into the location of the <@outputCSS/> directive that we declared in our template.
The <@script> directive references JavaScript files to be output into the location of the <@outputJavaScript/>.
The <@inlineScript> directive allows us to write snippets of JavaScript that will be inserted at the location of the <@outputJavaScript/> directive.
There are two benefits to be aware of here - firstly all JavaScript and all CSS will be combined so that only a single resource of each type will be loaded onto the page - this reduces HTTP handshaking and improves performance.
Secondly the generated resources will have a name that is an MD5 checksum matched to their content. This means that these files can be infinitely cached on the browser as if the content changes a different resource name will be generated.
Now run the following command:
mvn clean install jetty:run
Be aware, it will probably take a few minutes to build and startup - please be patient.
Once you see the message: “[INFO] Started Jetty Server”, open the URL “http://localhost:8090/aikau-sample/page/ngp/ws/legoQuotes” in your browser and you should see the following:
You can now click the button to generate random Lego Movie quotes.
In this post I've demonstrated that it is possible to make use of the current crop of tools for building web applications without discarding the benefits that Surf brings to the table with regards to creating clients for Alfresco. In the process I've hopefully been able to provide some useful information on creating Surf objects.