Added inlineAssets, split out inlineAssetsFromVirtualDirectory

This commit is contained in:
Tobe O 2018-07-09 09:49:25 -04:00
parent 3faa5509ea
commit d997734f0f
3 changed files with 124 additions and 49 deletions

View file

@ -6,10 +6,12 @@ the infamous
and other SEO optimizations that can easily become tedious to perform by hand. and other SEO optimizations that can easily become tedious to perform by hand.
## `inlineAssets` ## `inlineAssets`
This function is a simple one; it wraps a `VirtualDirectory` to patch the way it sends A
`.html` files. [response finalizer](https://angel-dart.gitbook.io/angel/the-basics/request-lifecycle)
that can be used in any application to patch HTML responses, including those sent with
a templating engine like Jael.
In any `.html` file sent down, `link` and `script` elements that point to internal resources In any `text/html` response sent down, `link` and `script` elements that point to internal resources
will have the contents of said file read, and inlined into the HTML page itself. will have the contents of said file read, and inlined into the HTML page itself.
In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or
@ -21,6 +23,32 @@ import 'package:angel_seo/angel_seo.dart';
import 'package:angel_static/angel_static.dart'; import 'package:angel_static/angel_static.dart';
import 'package:file/local.dart'; import 'package:file/local.dart';
main() async {
var app = new Angel()..lazyParseBodies = true;
var fs = const LocalFileSystem();
var http = new AngelHttp(app);
app.responseFinalizers.add(inlineAssets(fs.directory('web')));
app.use(() => throw new AngelHttpException.notFound());
var server = await http.startServer('127.0.0.1', 3000);
print('Listening at http://${server.address.address}:${server.port}');
}
```
## `inlineAssetsFromVirtualDirectory`
This function is a simple one; it wraps a `VirtualDirectory` to patch the way it sends
`.html` files.
Produces the same functionality as `inlineAssets`.
```dart
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_seo/angel_seo.dart';
import 'package:angel_static/angel_static.dart';
import 'package:file/local.dart';
main() async { main() async {
var app = new Angel()..lazyParseBodies = true; var app = new Angel()..lazyParseBodies = true;
var fs = const LocalFileSystem(); var fs = const LocalFileSystem();
@ -41,5 +69,4 @@ main() async {
var server = await http.startServer('127.0.0.1', 3000); var server = await http.startServer('127.0.0.1', 3000);
print('Listening at http://${server.address.address}:${server.port}'); print('Listening at http://${server.address.address}:${server.port}');
} }
``` ```

View file

@ -1,6 +1,7 @@
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_seo/angel_seo.dart'; import 'package:angel_seo/angel_seo.dart';
import 'package:angel_static/angel_static.dart'; import 'package:angel_static/angel_static.dart';
import 'package:dart2_constant/convert.dart';
import 'package:file/local.dart'; import 'package:file/local.dart';
main() async { main() async {
@ -8,7 +9,8 @@ main() async {
var fs = const LocalFileSystem(); var fs = const LocalFileSystem();
var http = new AngelHttp(app); var http = new AngelHttp(app);
var vDir = inlineAssets( // You can wrap a [VirtualDirectory]
var vDir = inlineAssetsFromVirtualDirectory(
new VirtualDirectory( new VirtualDirectory(
app, app,
fs, fs,
@ -18,6 +20,20 @@ main() async {
app.use(vDir.handleRequest); app.use(vDir.handleRequest);
// OR, just add a finalizer. Note that [VirtualDirectory] *streams* its response,
// so a response finalizer does not touch its contents.
//
// You likely won't need to use both; it just depends on your use case.
app.responseFinalizers.add(inlineAssets(fs.directory('web')));
app.get('/using_response_buffer', (ResponseContext res) async {
var indexHtml = fs.directory('web').childFile('index.html');
var contents = await indexHtml.readAsString();
res
..headers['content-type'] = 'text/html; charset=utf-8'
..buffer.add(utf8.encode(contents));
});
app.use(() => throw new AngelHttpException.notFound()); app.use(() => throw new AngelHttpException.notFound());
var server = await http.startServer('127.0.0.1', 3000); var server = await http.startServer('127.0.0.1', 3000);

View file

@ -15,7 +15,81 @@ import 'package:path/path.dart' as p;
/// ///
/// In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or /// In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or
/// `foo/bar/baz.js`. /// `foo/bar/baz.js`.
VirtualDirectory inlineAssets(VirtualDirectory vDir) => new _InlineAssets(vDir); RequestMiddleware inlineAssets(Directory assetDirectory) {
return (req, res) {
if (res.willCloseItself ||
res.streaming ||
res.contentType.mimeType != 'text/html') {
return new Future<bool>.value(true);
} else {
var doc = html.parse(utf8.decode(res.buffer.takeBytes()));
return inlineAssetsIntoDocument(doc, assetDirectory).then((_) {
res.buffer.add(utf8.encode(doc.outerHtml));
return false;
});
}
};
}
/// Wraps a `VirtualDirectory` to patch the way it sends
/// `.html` files.
///
/// In any `.html` file sent down, `link` and `script` elements that point to internal resources
/// will have the contents of said file read, and inlined into the HTML page itself.
///
/// In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or
/// `foo/bar/baz.js`.
VirtualDirectory inlineAssetsFromVirtualDirectory(VirtualDirectory vDir) =>
new _InlineAssets(vDir);
/// Replaces `link` and `script` tags within a [doc] with the static contents they would otherwise trigger an HTTP request to.
///
/// Powers both [inlineAssets] and [inlineAssetsFromVirtualDirectory].
Future inlineAssetsIntoDocument(
html.Document doc, Directory assetDirectory) async {
var linksWithRel = doc.head
?.getElementsByTagName('link')
?.where((link) => link.attributes['rel'] == 'stylesheet') ??
<html.Element>[];
for (var link in linksWithRel) {
if (link.attributes.containsKey('data-no-inline')) {
link.attributes.remove('data-no-inline');
} else {
var uri = Uri.parse(link.attributes['href']);
if (uri.scheme.isEmpty) {
var styleFile = assetDirectory.childFile(uri.path);
if (await styleFile.exists()) {
var style = new html.Element.tag('style')
..innerHtml = await styleFile.readAsString();
link.replaceWith(style);
}
}
}
}
var scripts = doc
.getElementsByTagName('script')
.where((script) => script.attributes.containsKey('src'));
for (var script in scripts) {
if (script.attributes.containsKey('data-no-inline')) {
script.attributes.remove('data-no-inline');
} else {
var uri = Uri.parse(script.attributes['src']);
if (uri.scheme.isEmpty) {
var scriptFile = assetDirectory.childFile(uri.path);
if (await scriptFile.exists()) {
script.attributes.remove('src');
script.innerHtml = await scriptFile.readAsString();
}
}
}
}
}
class _InlineAssets extends VirtualDirectory { class _InlineAssets extends VirtualDirectory {
final VirtualDirectory inner; final VirtualDirectory inner;
@ -34,49 +108,7 @@ class _InlineAssets extends VirtualDirectory {
if (p.extension(file.path) == '.html') { if (p.extension(file.path) == '.html') {
var contents = await file.readAsString(); var contents = await file.readAsString();
var doc = html.parse(contents, sourceUrl: file.uri.toString()); var doc = html.parse(contents, sourceUrl: file.uri.toString());
await inlineAssetsIntoDocument(doc, inner.source);
var linksWithRel = doc.head
?.getElementsByTagName('link')
?.where((link) => link.attributes['rel'] == 'stylesheet') ??
<html.Element>[];
for (var link in linksWithRel) {
if (link.attributes.containsKey('data-no-inline')) {
link.attributes.remove('data-no-inline');
} else {
var uri = Uri.parse(link.attributes['href']);
if (uri.scheme.isEmpty) {
var styleFile = inner.source.childFile(uri.path);
if (await styleFile.exists()) {
var style = new html.Element.tag('style')
..innerHtml = await styleFile.readAsString();
link.replaceWith(style);
}
}
}
}
var scripts = doc
.getElementsByTagName('script')
.where((script) => script.attributes.containsKey('src'));
for (var script in scripts) {
if (script.attributes.containsKey('data-no-inline')) {
script.attributes.remove('data-no-inline');
} else {
var uri = Uri.parse(script.attributes['src']);
if (uri.scheme.isEmpty) {
var scriptFile = inner.source.childFile(uri.path);
if (await scriptFile.exists()) {
script.attributes.remove('src');
script.innerHtml = await scriptFile.readAsString();
}
}
}
}
res res
..headers['content-type'] = 'text/html; charset=utf8' ..headers['content-type'] = 'text/html; charset=utf8'