Skip to content

Commit

Permalink
Include initial JS dependencies in the HTML (#1232)
Browse files Browse the repository at this point in the history
All @javascript dependencies known when initial HTML is generated are now included as <javascript> tags with deferred attribute in the main page.

Fixes #748
  • Loading branch information
SomeoneToIgnore authored and Legioth committed Feb 16, 2017
1 parent 24e5d75 commit 14cadc5
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 200 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ phantomjsdriver.log
bower_components
node
node_modules

# Intellij Idea files
.idea
*.iml
6 changes: 3 additions & 3 deletions .settings/org.eclipse.jdt.core.prefs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6
org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import java.util.Locale;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
Expand All @@ -40,7 +39,6 @@
import com.vaadin.external.jsoup.nodes.Element;
import com.vaadin.external.jsoup.nodes.Node;
import com.vaadin.external.jsoup.parser.Tag;
import com.vaadin.hummingbird.util.JsonUtils;
import com.vaadin.server.communication.AtmospherePushConnection;
import com.vaadin.server.communication.UidlWriter;
import com.vaadin.shared.ApplicationConstants;
Expand Down Expand Up @@ -73,24 +71,19 @@ public class BootstrapHandler extends SynchronizedRequestHandler {
+ "hummingbird.gwtStatsEvents = [];"
+ "window.__gwtStatsEvent = function(event) {"
+ "hummingbird.gwtStatsEvents.push(event); " + "return true;};};";

private static final String TYPE_TEXT_JAVASCRIPT = "text/javascript";
private static final String CONTENT_ATTRIBUTE = "content";
private static final String META_TAG = "meta";
private static final String DEFER_ATTRIBUTE = "defer";

/**
* Location of client nocache file, relative to the context root.
*/
private static final String CLIENT_ENGINE_NOCACHE_FILE = ApplicationConstants.CLIENT_ENGINE_PATH
+ "/client.nocache.js";
private static final Pattern SCRIPT_END_TAG_PATTERN = Pattern
.compile("</(script)", Pattern.CASE_INSENSITIVE);
private static final String BOOTSTRAP_JS;

private static String bootstrapJS;
static String clientEngineFile;

private static Pattern scriptEndTagPattern = Pattern.compile("</(script)",
Pattern.CASE_INSENSITIVE);

private static Logger getLogger() {
return Logger.getLogger(BootstrapHandler.class.getName());
}
Expand All @@ -100,10 +93,10 @@ private static Logger getLogger() {
try (InputStream stream = BootstrapHandler.class
.getResourceAsStream("BootstrapHandler.js");
BufferedReader bf = new BufferedReader(new InputStreamReader(
stream, StandardCharsets.UTF_8));) {
stream, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
bf.lines().forEach(sb::append);
bootstrapJS = sb.toString();
BOOTSTRAP_JS = sb.toString();
} catch (IOException e) {
throw new ExceptionInInitializerError(e);
}
Expand Down Expand Up @@ -303,8 +296,7 @@ public boolean synchronizedHandleRequest(VaadinSession session,
return true;
}

static Document getBootstrapPage(BootstrapContext context)
throws IOException {
static Document getBootstrapPage(BootstrapContext context) {
Document document = new Document("");
DocumentType doctype = new DocumentType("html", "", "",
document.baseUri());
Expand Down Expand Up @@ -350,7 +342,7 @@ private static void setupDocumentHead(Element head, JsonObject initialUIDL,
head.appendElement("title").appendText(title.get());
}

includeDependencies(head, initialUIDL, context);
includeDependencies(head, initialUIDL, context.getUriResolver());

Element styles = head.appendElement("style").attr("type", "text/css");
styles.appendText("html, body {height:100%;margin:0;}");
Expand Down Expand Up @@ -379,36 +371,40 @@ private static void setupDocumentHead(Element head, JsonObject initialUIDL,

if (context.getSession().getBrowser().isPhantomJS()) {
// Collections polyfill needed only for PhantomJS

head.appendElement("script").attr("type", "text/javascript").attr(
"src",
context.getUriResolver()
.resolveVaadinUri("context://"
+ ApplicationConstants.VAADIN_STATIC_FILES_PATH
+ "server/es6-collections.js"));
head.appendChild(createJavaScriptElement(context.getUriResolver()
.resolveVaadinUri("context://"
+ ApplicationConstants.VAADIN_STATIC_FILES_PATH
+ "server/es6-collections.js")));
}

head.appendElement("script").attr("type", "text/javascript")
.attr("src",
context.getUriResolver()
.resolveVaadinUri("context://"
+ ApplicationConstants.VAADIN_STATIC_FILES_PATH
+ "server/webcomponents-lite.min.js"))
.attr(DEFER_ATTRIBUTE, true);
head.appendChild(createJavaScriptElement(context.getUriResolver()
.resolveVaadinUri("context://"
+ ApplicationConstants.VAADIN_STATIC_FILES_PATH
+ "server/webcomponents-lite.min.js")));

if (context.getPushMode().isEnabled()) {
head.appendChild(getPushScript(context));
}

if (context.getPreRenderMode().includeLiveVersion()) {
head.appendChild(getBootstrapScript(initialUIDL, context));
head.appendChild(getClientEngineScript(context));
head.appendChild(
createJavaScriptElement(getClientEngineUrl(context)));
}

}

private static Element createJavaScriptElement(String sourceUrl) {
Element jsElement = new Element(Tag.valueOf("script"), "")
.attr("type", "text/javascript").attr("defer", true);
if (sourceUrl != null) {
jsElement = jsElement.attr("src", sourceUrl);
}
return jsElement;
}

private static void includeDependencies(Element head,
JsonObject initialUIDL, BootstrapContext context) {
JsonObject initialUIDL, VaadinUriResolver resolver) {
// Extract style sheets and load them eagerly
JsonArray dependencies = initialUIDL
.getArray(DependencyList.DEPENDENCY_KEY);
Expand All @@ -417,25 +413,39 @@ private static void includeDependencies(Element head,
return;
}

Predicate<? super JsonObject> includeStyleSheets = object -> DependencyList.TYPE_STYLESHEET
.equals(object.getString(DependencyList.KEY_TYPE));

JsonUtils.objectStream(dependencies)
.filter(includeStyleSheets).forEach(stylesheet -> {
Element link = head.appendElement("link");
link.attr("rel", "stylesheet");
link.attr("type", "text/css");
String url = stylesheet.getString(DependencyList.KEY_URL);
url = context.getUriResolver().resolveVaadinUri(url);
link.attr("href", url);
});
JsonArray uidlDependencies = Json.createArray();
int uidlDependenciesIndex = 0;
for (int i = 0; i < dependencies.length(); i++) {
JsonObject dependency = dependencies.getObject(i);
String dependencyKey = dependency
.getString(DependencyList.KEY_TYPE);
if (DependencyList.TYPE_STYLESHEET.equals(dependencyKey)) {
addStyleSheet(head, resolver, dependency);
} else if (DependencyList.TYPE_JAVASCRIPT.equals(dependencyKey)) {
addJavaScript(head, resolver, dependency);
} else {
uidlDependencies.set(uidlDependenciesIndex, dependency);
uidlDependenciesIndex += 1;
}
}

// Remove from initial UIDL
JsonArray otherDependencies = JsonUtils
.objectStream(dependencies).filter(includeStyleSheets.negate())
.collect(JsonUtils.asArray());
initialUIDL.put(DependencyList.DEPENDENCY_KEY, otherDependencies);
initialUIDL.put(DependencyList.DEPENDENCY_KEY, uidlDependencies);
}

private static void addStyleSheet(Element head, VaadinUriResolver resolver,
JsonObject styleSheet) {
Element link = head.appendElement("link").attr("rel", "stylesheet")
.attr("type", "text/css");
String url = styleSheet.getString(DependencyList.KEY_URL);
link.attr("href", resolver.resolveVaadinUri(url));
}

private static void addJavaScript(Element head, VaadinUriResolver resolver,
JsonObject javaScript) {
String url = javaScript.getString(DependencyList.KEY_URL);
head.appendChild(
createJavaScriptElement(resolver.resolveVaadinUri(url)));
}

private static void setupDocumentBody(Document document,
Expand All @@ -460,10 +470,8 @@ private static void setupDocumentBody(Document document,
// Mark body and children so we know what to remove when
// transitioning to the live version
body.attr(ApplicationConstants.PRE_RENDER_ATTRIBUTE, true);
body.children()
.forEach(element -> element.attr(
ApplicationConstants.PRE_RENDER_ATTRIBUTE,
true));
body.children().forEach(element -> element
.attr(ApplicationConstants.PRE_RENDER_ATTRIBUTE, true));
} else {
document.head().after("<body></body>");
body = document.body();
Expand All @@ -489,33 +497,26 @@ private static Element getPushScript(BootstrapContext context) {
String versionQueryParam = "?v=" + Version.getFullVersion();

// Load client-side dependencies for push support
String pushJS = ServletHelper.getContextRootRelativePath(request) + "/";
String pushJSPath = ServletHelper.getContextRootRelativePath(request)
+ "/";
if (request.getService().getDeploymentConfiguration()
.isProductionMode()) {
pushJS += ApplicationConstants.VAADIN_PUSH_JS;
pushJSPath += ApplicationConstants.VAADIN_PUSH_JS;
} else {
pushJS += ApplicationConstants.VAADIN_PUSH_DEBUG_JS;
pushJSPath += ApplicationConstants.VAADIN_PUSH_DEBUG_JS;
}

pushJS += versionQueryParam;
pushJSPath += versionQueryParam;

return new Element(Tag.valueOf("script"), "")
.attr("type", TYPE_TEXT_JAVASCRIPT).attr("src", pushJS)
.attr(DEFER_ATTRIBUTE, true);
return createJavaScriptElement(pushJSPath);
}

private static Element getBootstrapScript(JsonValue initialUIDL,
BootstrapContext context) {
Element mainScript = new Element(Tag.valueOf("script"), "").attr("type",
TYPE_TEXT_JAVASCRIPT);

StringBuilder builder = new StringBuilder();
builder.append("//<![CDATA[\n");
builder.append(getBootstrapJS(initialUIDL, context));

builder.append("//]]>");
mainScript.appendChild(
new DataNode(builder.toString(), mainScript.baseUri()));
String scriptData = "//<![CDATA[\n"
+ getBootstrapJS(initialUIDL, context) + "//]]>";
Element mainScript = createJavaScriptElement(null);
mainScript.appendChild(new DataNode(scriptData, mainScript.baseUri()));
return mainScript;
}

Expand All @@ -535,7 +536,7 @@ private static String getBootstrapJS(JsonValue initialUIDL,
String initialUIDLString = JsonUtil.stringify(initialUIDL, indent);
// Browser interpret </script> as end of script no matter if it is
// inside a string or not so we must escape it
initialUIDLString = scriptEndTagPattern.matcher(initialUIDLString)
initialUIDLString = SCRIPT_END_TAG_PATTERN.matcher(initialUIDLString)
.replaceAll("<\\\\x2F$1");

if (!productionMode) {
Expand All @@ -551,13 +552,6 @@ private static String getBootstrapJS(JsonValue initialUIDL,
return result;
}

private static Element getClientEngineScript(BootstrapContext context) {
return new Element(Tag.valueOf("script"), "")
.attr("type", TYPE_TEXT_JAVASCRIPT)
.attr("src", getClientEngineUrl(context))
.attr(DEFER_ATTRIBUTE, true);
}

protected static JsonObject getApplicationParameters(
BootstrapContext context) {
VaadinRequest request = context.getRequest();
Expand Down Expand Up @@ -682,8 +676,6 @@ protected static Optional<String> resolvePageTitle(

protected UI createAndInitUI(Class<? extends UI> uiClass,
VaadinRequest request, VaadinSession session) {
Integer uiId = Integer.valueOf(session.getNextUIid());

UI ui = ReflectTools.createInstance(uiClass);

// Initialize some fields for a newly created UI
Expand All @@ -698,11 +690,8 @@ protected UI createAndInitUI(Class<? extends UI> uiClass,

// Set thread local here so it is available in init
UI.setCurrent(ui);

ui.doInit(request, uiId.intValue());

ui.doInit(request, session.getNextUIid());
session.addUI(ui);

return ui;
}

Expand Down Expand Up @@ -754,11 +743,11 @@ private static void putValueOrNull(JsonObject object, String key,
}

private static String getBootstrapJS() {
if (bootstrapJS == null) {
if (BOOTSTRAP_JS == null) {
throw new BootstrapException(
"BootstrapHandler.js has not been loaded during initialization");
}
return bootstrapJS;
return BOOTSTRAP_JS;
}

private static String getClientEngineUrl(BootstrapContext context) {
Expand Down
Loading

0 comments on commit 14cadc5

Please sign in to comment.