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.
## `inlineAssets`
This function is a simple one; it wraps a `VirtualDirectory` to patch the way it sends
`.html` files.
A
[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.
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: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 {
var app = new Angel()..lazyParseBodies = true;
var fs = const LocalFileSystem();
@ -41,5 +69,4 @@ main() async {
var server = await http.startServer('127.0.0.1', 3000);
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_seo/angel_seo.dart';
import 'package:angel_static/angel_static.dart';
import 'package:dart2_constant/convert.dart';
import 'package:file/local.dart';
main() async {
@ -8,7 +9,8 @@ main() async {
var fs = const LocalFileSystem();
var http = new AngelHttp(app);
var vDir = inlineAssets(
// You can wrap a [VirtualDirectory]
var vDir = inlineAssetsFromVirtualDirectory(
new VirtualDirectory(
app,
fs,
@ -18,6 +20,20 @@ main() async {
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());
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
/// `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 {
final VirtualDirectory inner;
@ -34,49 +108,7 @@ class _InlineAssets extends VirtualDirectory {
if (p.extension(file.path) == '.html') {
var contents = await file.readAsString();
var doc = html.parse(contents, sourceUrl: file.uri.toString());
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();
}
}
}
}
await inlineAssetsIntoDocument(doc, inner.source);
res
..headers['content-type'] = 'text/html; charset=utf8'