在我们之前的概念验证演示中,我们构建了一个基本的管理界面,用于生成网页,并具有编辑页面上某些文本以及设置网站标题和描述的功能。 对于下一个演示,我们将在此基础上进行扩展,并添加富文本编辑和图像上传功能。
文章系列
- 为无服务器静态网站生成器构建自定义 CMS + 仓库
- 构建自定义无服务器 CMS:第 2 部分(您在此处!) + 仓库
富文本编辑
TinyMCE 是最广泛使用的基于 Web 的富文本编辑器,因此让我们使用它。 我们可以很容易地将其添加到我们的管理表单中。 TinyMCE 提供了许多 配置选项。 对于这个演示,我们只需要几个。
tinymce.init({
selector: '#calloutText',
menubar: false,
statusbar: false,
toolbar: 'undo redo | styleselect | bold italic | link',
plugins: 'autolink link'
});

富文本编辑器将使用标记对其内容进行编码,因此我们必须更新 JSRender 模板以将 calloutText
的数据值输出为 HTML。
<div class="jumbotron">
<div class="container">
<h1 class="display-3">{{>calloutHeadline}}</h1>
{{:calloutText}}
...
图像上传
现在,我们为 Jumbotron 添加一个图像背景。 首先,我们需要添加一个新的表单字段,以便管理员可以选择要上传的文件,然后更新我们的表单提交处理程序,将图像上传到 S3。

由于此处发生了多次上传和回调,我们可以创建一个上传辅助方法并使用 Deferred 对象 使我们的 Ajax 调用 同时运行。
$('body').on('submit','#form-admin',function(e) {
e.preventDefault();
var formData = {};
var $formFields = $('#form-admin').find('input, textarea, select').not(':input[type=button], :input[type=submit], :input[type=reset]');
$formFields.each(function() {
formData[$(this).attr('name')] = $(this).val();
});
var jumbotronHTML = '<!DOCTYPE html>' +
$.render.jumbotronTemplate(formData);
var fileHTML = new File([jumbotronHTML], 'index.html', {type: "text/html", lastModified: new Date()});
var fileJSON = new File([JSON.stringify(formData)], 'admin.json');
var uploadHTML = $.Deferred();
var uploadJSON. = $.Deferred();
var uploadImage = $.Deferred();
upload({
Key: 'index.html',
Body: fileHTML,
ACL: 'public-read',
ContentDisposition: 'inline',
ContentType: 'text/html'
}, uploadHTML);
upload({
Key: 'admin/index.json',
Body: fileJSON,
ACL: 'public-read'
}, uploadJSON);
if ($('#calloutBackgroundImage').prop('files').length) {
upload({
Key: 'img/callout.jpg',
Body: $('#calloutBackgroundImage').prop('files')[0],
ACL: 'public-read'
}, uploadImage);
} else {
uploadImage.resolve();
}
$.when(uploadHTML, uploadImage, uploadJSON).then(function() {
$('#form-admin').prepend('<p id="success">Update successful! View Website</p>');
})
});
function upload(uploadData, deferred) {
s3.upload(uploadData, function(err, data) {
if (err) {
return alert('There was an error: ', err.message);
deferred.reject();
} else {
deferred.resolve();
}
});
}
接下来,我们将更新 Jumbotron 以显示呼叫背景图像。
.jumbotron {
background-image: url(../img/callout.jpg);
background-repeat: no-repeat;
background-attachment: fixed;
background-position: center;
background-size: cover;
}

博客文章
让我们将富文本编辑和图像上传结合使用来创建博客文章。 由于我们正在进行大量模板化,我们可以通过编写一个辅助函数来自动注册 JSX 模板,从而简化操作。
$('script[type="text/x-jsrender"]').each(function() {
$.templates($(this).attr('id'), '#'+$(this).attr('id'));
});
我们可以通过在我们的管理页面上添加导航栏模板部分,来管理网站的不同区域,在本例中是博客。
<script type="text/x-jsrender" id="adminNav">
<nav class="navbar navbar-light rounded bg-faded my-4">
<div class="navbar-collapse" id="navbarNav">
<ul class="nav navbar-nav d-flex flex-row">
<li class="nav-item pl-2 pr-3 mr-1 border-right">
<a class="nav-link text-primary" href="#adminIndex">Admin</a>
</li>
<li class="nav-item px-2{{if active=='adminIndex'}} active{{/if}}">
<a class="nav-link" href="#adminIndex">Home {{if active=='adminIndex'}}<span class="sr-only">(current)</span>{{/if}}</a>
</li>
<li class="nav-item px-2{{if active=='adminBlog'}} active{{/if}}">
<a class="nav-link" href="#adminBlog">Blog {{if active=='adminBlog'}}<span class="sr-only">(current)</span>{{/if}}</a>
...
接下来,使用导航栏和新 ID 更新我们现有的管理页面。
<script type="text/x-jsrender" id="adminHome">
{{include tmpl='adminNav' /}}
<form class="py-2" id="form-admin">
<h3 class="py-2">Site Info</h3>
...

为了在我们的管理视图中添加导航,当单击导航按钮时,我们可以添加一个事件处理程序,该处理程序将加载关联的数据并呈现相应的模板。
我们将在编辑网站的不同区域时使用富文本编辑器,因此创建另一个辅助函数将使我们能够轻松地使用不同的设置配置编辑器。
$('body').on('click','.nav-link', function(e) {
e.preventDefault();
loadPage($(this).attr('href').slice(1));
});
function loadPage(pageId) {
adminData = {};
$.getJSON(pageId+'.json', function( data ) {
adminData = data;
}).always(function() {
$('.container').html($.render[pageId]($.extend(adminData,{active:pageId})));
initRichTextEditor();
});
}
function initRichTextEditor(settings) {
tinymce.init($.extend({
selector:'textarea',
menubar: false,
statusbar: false,
toolbar: 'undo redo | styleselect | bold italic | link',
plugins: 'autolink link',
init_instance_callback : function(editor) {
$('.mce-notification-warning').remove();
}
}, settings ? settings : {}));
}
创建一个新的管理部分,用于管理博客,并使用一个按钮来创建新帖子。
<script type="text/x-jsrender" id="adminBlog">
{{include tmpl='adminNav' /}}
<div id="blogPosts">
<h3 class="py-2">Blog Posts</h3>
<button id="newPostButton" class="btn btn-primary">+ New Post</button>
...
此外,我们还需要一个表单来编写这些帖子。 请注意,我们包含了一个隐藏的文件输入,我们将使用它来允许富文本编辑器上传图像。
<script type="text/x-jsrender" id="editBlogPost">
<form class="py-2" id="form-blog">
{{if postTitle}}
<h3 class="py-2">Edit Blog Post</h3>
{{else}}
<h3 class="py-2">New Blog Post</h3>
{{/if}}
<div class="form-group">
<label for="postTitle">Title</label>
<input type="text" value="{{>postTitle}}" class="form-control" id="postTitle" name="postTitle" />
</div>
<div class="form-group pb-2">
<textarea class="form-control" id="postContent" name="postContent" rows="12">{{>postContent}}</textarea>
</div>
<div class="hidden-xs-up">
<input type="file" id="imageUploadFile" />
</div>
<div class="text-xs-right">
<button class="btn btn-link">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</script>

允许管理员编辑网站的多个页面将需要我们以不同的方式构建网站生成。 每当对网站标题和信息进行更改时,我们需要将更改传播到主页和博客。
首先,我们为网站导航创建模板部分,我们可以在每个页面模板中包含这些部分。
<script type="text/x-jsrender" id="siteNav">
<nav class="navbar navbar-static-top navbar-dark bg-inverse">
<a class="navbar-brand pr-2" href="#">{{>siteTitle}}</a>
<ul class="nav navbar-nav">
<li class="nav-item{{if active=='index'}} active{{/if}}">
<a class="nav-link" href="{{>navPath}}index.html">Home {{if active=='index'}}<span class="sr-only">(current)</span>{{/if}}</a>
</li>
<li class="nav-item{{if active=='blog'}} active{{/if}}">
<a class="nav-link" href="{{>navPath}}blog.html">Blog {{if active=='blog'}}<span class="sr-only">(current)</span>{{/if}}</a>
...
<body>
{{include tmpl='siteNav' /}}
...
当我们的管理员单击“新建帖子”按钮时,应该会显示我们的编辑表单。 我们可以创建一个函数来执行此操作,并将该函数附加到按钮上的单击事件。
此外,我们希望能够向博客文章中添加图像。 为了做到这一点,我们需要在富文本编辑器中添加一个自定义图像上传窗口,并使用一些配置设置。
function editPost(postData) {
$('.container').append($.render.editBlogPost(postData));
initRichTextEditor({
toolbar: 'undo redo | styleselect | bold italic | bullist numlist | link addImage',
setup: function(editor) {
editor.addButton('addImage', {
text: 'Add Image',
icon: false,
onclick: function() {
// Open window
editor.windowManager.open({
title: 'Add Image',
body: [{
type: 'button',
name: 'uploadImage',
label: 'Select an image to upload',
text: 'Browse',
onclick: function(e) {
$('#imageUploadFile').click();
},
onPostRender: function() {
addImageButton = this;
}
}, {
type: 'textbox',
name: 'imageDescription',
label: 'Image Description'
}],
buttons: [{
text: 'Cancel',
onclick: 'close'
}, {
text: 'OK',
classes: 'widget btn primary first abs-layout-item',
disabled: true,
onclick: 'close',
id: 'addImageButton'
}]
});
}
});
}
});
}
$('body').on('click', '#addImageButton', function() {
if ($(this).hasClass('mce-disabled')) {
alert('Please select an image');
} else {
var fileUploadData,
extension = 'jpg',
mimeType = $('#imageUploadFile')[0].files[0].type; // You can get the mime type
if (mimeType.indexOf('png') != -1) {
extension = 'png';
}
if (mimeType.indexOf('gif') != -1) {
extension = 'gif';
}
var filepath = 'img/blog/' + ((new Date().getMonth()) + 1) + '/' + Date.now() + '.' + extension;
upload({
Key: filepath,
Body: $('#imageUploadFile').prop('files')[0],
ACL: 'public-read'
}).done(function() {
var bucketUrl = 'http://serverless-cms.s3-website-us-east-1.amazonaws.com/';
tinyMCE.activeEditor.execCommand('mceInsertRawHTML', false, '<p><img src="' + bucketUrl + filepath + '" alt="' + $('.mce-textbox').val() + '" /></p>');
$('#imageUploadFile').val();
tinyMCE.activeEditor.windowManager.close();
});
}
});
上面的代码将一个 添加图像 按钮放置到富文本编辑器控件中,该按钮将打开一个模态窗口,供管理员选择要上传的图像。 当管理员单击 浏览 时,我们已向编辑表单中的隐藏文件输入添加了一个单击触发器。

一旦他们选择了要添加到帖子的图像,单击 确定 关闭窗口也将图像上传到 S3,然后将图像插入富文本编辑器中的光标位置。

接下来,我们需要保存博客帖子。 为此,我们将表单数据与模板结合起来以呈现 HTML 并上传到 S3。 存在一个用于博客和单个帖子的模板。 我们还需要存储帖子数据,以便管理员可以查看帖子列表并进行编辑。
$('body').on('submit', '#form-blog', function(e) {
e.preventDefault();
var updateBlogPosts = $.Deferred();
if ($(this).attr('data-post-id') === '') {
postId = Date.now();
} else {
postId = $(this).attr('data-post-id');
}
if (!adminData.posts) {
adminData.posts = [];
}
var postUrl = 'posts/' + ($('#title').val().toLowerCase().replace(/[^\w\s]/gi, '').replace(/\s/g, '-')) + '.html';
var postTitle = $('#title').val();
var postContent = tinyMCE.activeEditor.getContent({
format: 'raw'
});
adminData.posts.push({
url: postUrl,
title: postTitle,
excerpt: $(postContent)[0].innerText
});
var uploads = generateHTMLUploads('blog');
uploads.push(generateAdminDataUpload());
var postHTML = '<!DOCTYPE html>' + $.render['blogPostTemplate']($.extend(adminData, {
active: 'blog',
title: postTitle,
content: postContent,
navPath: '../'
}));
var fileHTML = new File([postHTML], postUrl, {
type: "text/html",
lastModified: new Date()
});
uploads.push(upload({
Key: postUrl,
Body: postHTML,
ACL: 'public-read',
ContentDisposition: 'inline',
ContentType: 'text/html'
}))
$.when.apply($, uploads).then(function() {
loadPage('adminBlog');
});
});
在我们的管理博客页面中,我们将列出我们发布的帖子。

<script type="text/x-jsrender" id="adminBlog">
{{include tmpl='adminNav' /}}
<div id="blogPosts">
<h3 class="py-2">Blog Posts</h3>
<button id="newPostButton" class="btn btn-primary my-1">+ New Post</button>
{{if posts}}
<div class="container p-0">
<ul class="list-group d-inline-block">
{{for posts}}
<li class="list-group-item">
<span class="pr-3">{{>title}}</span>
<a href="../{{>url}}" target="_blank" class="pl-3 float-xs-right">view</a>
<a href="#" data-id="{{:#getIndex()}}" data-url="{{>url}}" class="edit-post pl-3 float-xs-right">edit</a>
</li>
{{/for}}
</ul>
</div>
{{/if}}
</div>
</script>
最后,我们将扩展我们的新帖子单击处理程序以处理编辑帖子,方法是将帖子数据加载到表单模板中。
$('body').on('click', '#newPostButton, .edit-post', function(e) {
e.preventDefault();
$('#blogPosts').remove();
if ($(this).is('#newPostButton')) {
editPost({});
} else {
var postId = $(this).attr('data-id');
var postUrl = $(this).attr('data-url');
$('<div />').load('../' + postUrl, function() {
editPost({
id: postId,
title: $(this).find('h1').text(),
content: $(this).find('#content').html()
});
});
}
});
下一步
显然,这是一个基本示例,缺少很多关键功能,例如草稿帖子、删除帖子和分页功能。
随着网站范围的扩大,在客户端生成批量的 HTML 文件将变得繁琐且不可靠。 但是,我们可以通过将网站生成卸载到 AWS Lambda 并为更新网站信息和管理博客文章创建微服务来保持我们的架构无服务器。
通过更新存储在 S3 上的扁平 JSON 文件来管理我们的网站数据结构既便宜,又可以轻松地设置备份和恢复。 但是,对于比简单的博客或营销网站更复杂的项目,可以使用 AWS Dynamo DB 来存储数据,浏览器中的 AWS SDK for JavaScript 也支持它。
博客只是可以用这种方式构建的众多示例之一。 无服务器 Web 应用程序架构(又称后端即服务)的兴起使得前端能够控制用户和创作体验,并从头到尾地设计 Web 产品和内容。
这是一个在 Web 上构建事物的激动人心的时刻。
如果您需要重新利用数据会怎样?
您如何检索这些数据,例如,AWS Dynamo DB 可以帮助您将数据放到电子表格中吗?
在这个简单的示例中,数据存储在一个扁平的 JSON 文件中。 是的,使用 Dynamo DB 将是一个不错的选择。 他们的 JavaScript SDK 还支持 AWS RDS,如果您想走这条路。
正如我在第一部分中评论的那样,我认为这是一个很棒的概念。 但是,我不喜欢这部分中的 jQuery 代码混乱。
说实话,这里介绍的整个架构都很僵化,难以理解,几乎不可能扩展。 例如,如果博客帖子需要两张图像怎么办? 我们将另一个图像上传硬编码到管理逻辑中吗(例如
Body: $('#imageUploadFile2').prop('files')[0]
)?整个架构渴望拥有一个合适的抽象层来处理字段和内容类型。 不必过于复杂; 一个简单的字段名称、类型和值列表(图像也上传到 S3)就足够了,这将极大地提高代码的可读性和易用性。
并非有意冒犯作者。 再说一次,这个概念很棒。 但是,代码示例本身对读者来说是不利的,特别是可能认为这是实现这种功能的好方法的新手读者。
没有冒犯。 这本意是展示如何开始并以基本的方式演示正在发生的事情。 我本来可以开始将其转变为更完善的框架,但这不是我的目标。
希望人们能够看到我在这里的实验,并想出更稳健的方法来完成这类工作,因为有很多潜在的方向可以发展。