Stapes JS 教程
本教程将展示如何使用 Stapes JS 和 JQuery 设计一个 HTML 编辑器。
引言
当我写完关于 require.js 的教程时,我曾承诺会写一篇关于 stapes.js 的教程。我为此有几个原因
- 我一直在寻找一个 JavaScript 框架,我可以用它像在 Java 或 C# 中一样编写类。Stapes JS 是最接近我期望的。
- 使用 Stapes JS 和 Require JS 比单独使用 JQuery 要容易得多,可以创建可重用的组件。
- 这个框架封装在一个 JS 文件中,为应用程序添加和配置非常容易。
- 这个框架非常容易学习,并且只需最少的精力即可与 JQuery 集成,它是 JQuery 的理想包装器。
这些是我在 Stapes JS 方面的经验所基于的原因。在开始全新的应用程序开发时,可以使用 JQuery 以外的这个框架。只要 Stapes JS 使用得当,它将大大提高应用程序的代码质量。
在本教程中,我设计了一个基于 JavaScript 的 HTML 编辑器,只是为了展示如何使用 Stapes JS 创建一个组件,然后另一个组件来利用这个 HTML 编辑器。让我们开始吧。
架构
应用程序的屏幕截图如下所示
如上所示,该应用程序有两个部分。顶部是预览部分。底部是用户输入 HTML 内容的地方,然后更改将反映在顶部。
这是一个单页 Web 应用程序。页面 index.html 被包装在一个 Spring Boot 应用程序中。这个 Spring Boot 应用程序只做一件事,即启动一个 Web 应用程序,并将 index 页面以及 CSS 文件和 JavaScript 文件提供给用户的浏览器。
我使用 Bootstrap 进行 CSS 标记,并使用 JQuery 和 StapesJS 进行客户端应用程序开发。JQuery 用于初始化 Bootstrap,并与 Stapes JS 紧密集成。查询 HTML 元素和设置事件处理的功能都通过 JQuery 完成。正如你稍后将看到的,它的使用方式是不可辨认的。
HTML 标记
首先,让我们看一下网页的 HTML 标记。网页的标记位于名为 "index.html" 的文件中。
HTML 内容预览部分定义如下
<div class="row">
<div class="col-xs-12 col-sm-offset-1 col-sm-10">
<div id="htmlContent" class="html-content"></div>
</div>
</div>
此代码片段定义了一个占位符,可以向其附加其他 HTML 代码,这就是用户的输入 HTML 将在预览区域中显示的方式。
为了通过单击某个按钮来装饰文本区域中输入的文本,我必须创建一个下拉菜单。下拉菜单如下所示
此下拉菜单的 HTML 标记如下
<div class="row combo-box">
<div class="col-xs-12 col-sm-6 col-md-3">
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
HTML Formatting <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a id="h1Action">Header 1</a></li>
<li><a id="h2Action">Header 2</a></li>
<li><a id="h3Action">Header 3</a></li>
<li><a id="h4Action">Header 4</a></li>
<li><a id="h5Action">Header 5</a></li>
<li role="separator" class="divider"></li>
<li><a id="pAction">Paragraph</a></li>
<li><a id="bAction">Bold</a></li>
<li><a id="iAction">Italic</a></li>
<li><a id="uAction">Underline</a></li>
<li><a id="stAction">Strike Through</a></li>
</ul>
</div>
</div>
</div>
用于输入 HTML 内容的文本区域如下
<textarea class="form-control" id="textEditor" rows="6" cols="40">
</textarea>
用于清除文本区域 HTML 内容的红色按钮定义如下
<div class="col-xs-12 col-sm-offset-1 col-sm-10">
<div class="row">
<div class="col-xs-12 col-sm-offset-7 col-sm-5">
<button id="clearBtn" class="form-control btn btn-danger app-btn">Clear</button>
</div>
</div>
</div>
最后,我在一个单独的部分(单独放置)中添加了另一个按钮。当用户单击此按钮时,它将在控制台输出 HTML 编辑器中的 HTML 内容。
<div class="container" id="otherServices">
<div class="row">
<div class="col-xs-4">
<button class="btn btn-default" id="getHtml">Get HTML</button>
</div>
</div>
</div>
此按钮的目的是演示一个组件如何访问另一个组件的数据,以及 JavaScript 部分将展示这种交互是如何完成的。
在进入 JavaScript 部分之前,我将向您展示 script
声明部分
<script src="/assets/jquery/js/jquery.min.js"></script>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/underscore/underscore-min-1.9.2.js"></script>
<script src="/assets/stapes/stapes-min-1.0.0.js"></script>
<script src="/assets/app/app.js"></script>
如您所见,所有 HTML 标记都没有附加事件处理程序。事件处理全部在 app.js 加载和执行时附加。app.js 是实现主要 JavaScript 代码的地方。接下来我将展示给您。
JavaScript 应用程序
我之前提到过,使用 Stapes JS,我可以像在 Java 或 C# 中一样定义类。我将向您展示为什么是这样。如上一节的结尾所示,HTML 文件中包含的最后一个文件是 app.js,其中实现了所有 UI 交互。在此 JS 文件中,您将看到我声称的,类似于 Java 或 C# 中定义的类的类定义。让我发布 HTML 编辑器对象类的源代码。这是
var testApp = Stapes.subclass({
htmlContentView: null,
textEditor: null,
textFormat: null,
updateBtn: null,
clearBtn: null,
h1Action: null,
h2Action: null,
h3Action: null,
h4Action: null,
h5Action: null,
pAction: null,
bAction: null,
iAction: null,
uAction: null,
stAction: null,
getHtmlVal: function () {
var self = this;
var retVal = self.textEditor.val();
return retVal;
},
constructor : function() {
var self = this;
self.$el = $("#htmlEditing");
self.htmlContentView = self.$el.find("#htmlContent");
self.textEditor = self.$el.find("#htmlEditorDiv #textEditor");
self.updateBtn = self.$el.find("#updateBtn");
self.clearBtn = self.$el.find("#clearBtn");
self.h1Action = self.$el.find(".btn-group #h1Action");
self.h2Action = self.$el.find(".btn-group #h2Action");
self.h3Action = self.$el.find(".btn-group #h3Action");
self.h4Action = self.$el.find(".btn-group #h4Action");
self.h5Action = self.$el.find(".btn-group #h5Action");
self.pAction = self.$el.find(".btn-group #pAction");
self.bAction = self.$el.find(".btn-group #bAction");
self.iAction = self.$el.find(".btn-group #iAction");
self.uAction = self.$el.find(".btn-group #uAction");
self.stAction = self.$el.find(".btn-group #stAction");
self.textEditor.on("input", function (e) {
var htmlText = self.textEditor.val();
self.htmlContentView.html(htmlText);
});
self.clearBtn.on("click", function(e) {
self.textEditor.val("");
});
self.$el.on("submit", function(e) {
e.preventDefault();
});
self.h1Action.on("click", function (e) {
formatHtml("<h1>", "</h1>");
});
self.h2Action.on("click", function (e) {
formatHtml("<h2>", "</h2>");
});
self.h3Action.on("click", function (e) {
formatHtml("<h3>", "</h3>");
});
self.h4Action.on("click", function (e) {
formatHtml("<h4>", "</h4>");
});
self.h5Action.on("click", function (e) {
formatHtml("<h5>", "</h5>");
});
self.pAction.on("click", function (e) {
formatHtml("<p>", "</p>");
});
self.bAction.on("click", function (e) {
formatHtml("<strong>", "</strong>");
});
self.iAction.on("click", function (e) {
formatHtml("<span style='font-style: italic'>", "</span>");
});
self.uAction.on("click", function (e) {
formatHtml("<span style='text-decoration: underline'>", "</span>");
});
self.stAction.on("click", function (e) {
formatHtml("<span style='text-decoration: line-through'>", "</span>");
});
var formatHtml = function (beginTag, endTag) {
var htmltext = self.textEditor.val();
if (htmltext != null && htmltext.length >= 0) {
var selStart = self.textEditor[0].selectionStart;
var selEnd = self.textEditor[0].selectionEnd;
if (selStart >= 0 && selEnd >= 0) {
var seg1 = htmltext.substring(0, selStart);
var seg2 = htmltext.substring(selStart, selEnd);
var seg3 = htmltext.substring(selEnd);
if (seg1 == null) {
seg1 = "";
}
if (seg2 == null) {
seg2 = "";
}
if (seg3 == null) {
seg3 = "";
}
var htmltext2 = seg1 + beginTag + seg2 + endTag + seg3;
self.textEditor.val(htmltext2);
}
}
};
}
});
此列表中的内容可以分解为几个不同的部分。第一个是组件类的定义,即
var testApp = Stapes.subclass({
...
...
});
接下来,我想定义一些类属性。这些属性保存对 HTML 元素的引用。有了这些引用,我就可以向它们附加事件处理程序。我所描述的将类似于 C# WinForm 编程。这是类属性的定义
...
htmlContentView: null,
textEditor: null,
textFormat: null,
updateBtn: null,
clearBtn: null,
h1Action: null,
h2Action: null,
h3Action: null,
h4Action: null,
h5Action: null,
pAction: null,
bAction: null,
iAction: null,
uAction: null,
stAction: null,
...
为了初始化这些属性,我为该类定义了一个对象构造函数。此构造函数的作用是使用 JQuery 查找 HTML 页面上的元素。然后将事件处理方法附加到它们
...
constructor : function() {
var self = this;
self.$el = $("#htmlEditing");
self.htmlContentView = self.$el.find("#htmlContent");
self.textEditor = self.$el.find("#htmlEditorDiv #textEditor");
self.updateBtn = self.$el.find("#updateBtn");
self.clearBtn = self.$el.find("#clearBtn");
self.h1Action = self.$el.find(".btn-group #h1Action");
self.h2Action = self.$el.find(".btn-group #h2Action");
self.h3Action = self.$el.find(".btn-group #h3Action");
self.h4Action = self.$el.find(".btn-group #h4Action");
self.h5Action = self.$el.find(".btn-group #h5Action");
self.pAction = self.$el.find(".btn-group #pAction");
self.bAction = self.$el.find(".btn-group #bAction");
self.iAction = self.$el.find(".btn-group #iAction");
self.uAction = self.$el.find(".btn-group #uAction");
self.stAction = self.$el.find(".btn-group #stAction");
self.textEditor.on("input", function (e) {
var htmlText = self.textEditor.val();
self.htmlContentView.html(htmlText);
});
self.clearBtn.on("click", function(e) {
self.textEditor.val("");
});
self.$el.on("submit", function(e) {
e.preventDefault();
});
self.h1Action.on("click", function (e) {
formatHtml("<h1>", "</h1>");
});
self.h2Action.on("click", function (e) {
formatHtml("<h2>", "</h2>");
});
self.h3Action.on("click", function (e) {
formatHtml("<h3>", "</h3>");
});
self.h4Action.on("click", function (e) {
formatHtml("<h4>", "</h4>");
});
self.h5Action.on("click", function (e) {
formatHtml("<h5>", "</h5>");
});
self.pAction.on("click", function (e) {
formatHtml("<p>", "</p>");
});
self.bAction.on("click", function (e) {
formatHtml("<strong>", "</strong>");
});
self.iAction.on("click", function (e) {
formatHtml("<span style='font-style: italic'>", "</span>");
});
self.uAction.on("click", function (e) {
formatHtml("<span style='text-decoration: underline'>", "</span>");
});
self.stAction.on("click", function (e) {
formatHtml("<span style='text-decoration: line-through'>", "</span>");
});
var formatHtml = function (beginTag, endTag) {
var htmltext = self.textEditor.val();
if (htmltext != null && htmltext.length >= 0) {
var selStart = self.textEditor[0].selectionStart;
var selEnd = self.textEditor[0].selectionEnd;
if (selStart >= 0 && selEnd >= 0) {
var seg1 = htmltext.substring(0, selStart);
var seg2 = htmltext.substring(selStart, selEnd);
var seg3 = htmltext.substring(selEnd);
if (seg1 == null) {
seg1 = "";
}
if (seg2 == null) {
seg2 = "";
}
if (seg3 == null) {
seg3 = "";
}
var htmltext2 = seg1 + beginTag + seg2 + endTag + seg3;
self.textEditor.val(htmltext2);
}
}
};
}
...
Stapes JS 与 HTML 元素交互的方式是首先找到一个容器元素。容器元素是许多子元素的父元素。一旦 Stapes JS 对象获得了对父元素的引用,它就可以用于查找其中的所有子元素。以下是获取父元素引用的方法
constructor : function() {
var self = this;
self.$el = $("#htmlEditing");
...
}
在此示例中,容器元素是 id 为 "#htmlEditing
" 的某个元素。
请注意,局部变量 self
是 Stapes JS 类对象的引用。有时,关键字 this
是函数的引用。因此,找到关键字 this
实际引用什么很重要。在这种情况下,由于我们将一个对象传递给了 Stapes.subclass()
,关键字 this
引用该对象。这允许我像这样初始化类的所有属性
...
self.htmlContentView = self.$el.find("#htmlContent");
self.textEditor = self.$el.find("#htmlEditorDiv #textEditor");
self.updateBtn = self.$el.find("#updateBtn");
self.clearBtn = self.$el.find("#clearBtn");
self.h1Action = self.$el.find(".btn-group #h1Action");
self.h2Action = self.$el.find(".btn-group #h2Action");
self.h3Action = self.$el.find(".btn-group #h3Action");
self.h4Action = self.$el.find(".btn-group #h4Action");
self.h5Action = self.$el.find(".btn-group #h5Action");
self.pAction = self.$el.find(".btn-group #pAction");
self.bAction = self.$el.find(".btn-group #bAction");
self.iAction = self.$el.find(".btn-group #iAction");
self.uAction = self.$el.find(".btn-group #uAction");
self.stAction = self.$el.find(".btn-group #stAction");
...
此代码段和之前的代码段都使用 JQuery 来查找容器中子元素的引用。一旦获得了引用,我就可以将事件处理方法附加到所有这些元素
self.textEditor.on("input", function (e) {
var htmlText = self.textEditor.val();
self.htmlContentView.html(htmlText);
});
self.clearBtn.on("click", function(e) {
self.textEditor.val("");
});
self.$el.on("submit", function(e) {
e.preventDefault();
});
self.h1Action.on("click", function (e) {
formatHtml("<h1>", "</h1>");
});
self.h2Action.on("click", function (e) {
formatHtml("<h2>", "</h2>");
});
self.h3Action.on("click", function (e) {
formatHtml("<h3>", "</h3>");
});
self.h4Action.on("click", function (e) {
formatHtml("<h4>", "</h4>");
});
self.h5Action.on("click", function (e) {
formatHtml("<h5>", "</h5>");
});
self.pAction.on("click", function (e) {
formatHtml("<p>", "</p>");
});
self.bAction.on("click", function (e) {
formatHtml("<strong>", "</strong>");
});
self.iAction.on("click", function (e) {
formatHtml("<span style='font-style: italic'>", "</span>");
});
self.uAction.on("click", function (e) {
formatHtml("<span style='text-decoration: underline'>", "</span>");
});
self.stAction.on("click", function (e) {
formatHtml("<span style='text-decoration: line-through'>", "</span>");
});
这里有几件事我想提一下。首先,文本区域元素位于一个 form
元素内。同一表单中还有一个按钮。因此,只要单击此按钮,表单就会被“提交”。我首先要做的就是禁用这种行为。这是
...
self.$el.on("submit", function(e) {
e.preventDefault();
});
...
文本区域的事件处理方法是每当在文本区域中键入字符时更新 HTML 预览区域。我做了一些研究。"change"
事件对于键入文本区域的字符不起作用。经过一些搜索,发现在文本区域中调用以反映更改的事件称为 "input"
。因此,我添加了一个回调函数,该函数获取文本区域中的内容并将其设置为预览区域的 HTML 内容。这是
...
self.textEditor.on("input", function (e) {
var htmlText = self.textEditor.val();
self.htmlContentView.html(htmlText);
});
...
看看有多简单?我获取文本区域中的文本值,并将其作为 inner HTML 附加到预览区域。这很酷的一点是,用户在文本区域中输入的任何内容都是未转义的文本。在不更改文本的情况下,可以将其作为 HTML 元素附加到预览区域的 <div></div>
中。
如前所述,HTML 编辑器有一个下拉菜单,其中包含许多可单击的选项。这些选项将为文本区域中的文本添加装饰。用户可以选择高亮显示一段文本,然后单击这些选项以在突出显示的文本的开头和结尾添加 HTML 标签;或者,如果没有突出显示文本,则将封闭的 HTML 标签放在文本光标位置。请在运行示例应用程序时进行尝试。共有 10 个选项,每个选项都有一个相关的事件处理方法。其中一个如下
...
self.iAction.on("click", function (e) {
formatHtml("<span style='font-style: italic'>", "</span>");
});
...
此事件处理方法用于为突出显示的文本添加文本装饰,以便可以将其显示为斜体格式。我们使用 JQuery 的 on("event_name", function (...) { ... })
方法来附加事件处理方法。所有这些都调用一个名为 formatHtml()
的独立函数。接下来我将讨论它的作用。
处理文本高亮和光标位置
此应用程序中最难的部分是获取文本区域中突出显示文本的开始和结束位置的功能。对于任何基于文本的输入,这些输入 HTML 元素都附带特殊的属性。只要使用这些特殊属性,就可以确定突出显示文本的开始和结束位置。如果没有突出显示文本,则开始位置和结束位置相同。
只要知道开始和结束位置,我们就可以将整个文本分成三个不同的部分
- 第一部分是整个内容开头到突出显示文本开始之前的文本。
- 第二部分是从开始到结束的突出显示文本。如果没有突出显示文本,这将是一个空字符串。
- 最后一部分是从选定内容的末尾到整个内容的末尾的文本。
一旦有了这三部分,我就可以在第二部分的开头和结尾添加 HTML 标签装饰。然后将第一部分、装饰过的第二部分和第三部分全部组合在一起。这就是如何为文本区域中突出显示的文本的开头和结尾添加 HTML 标签。这就是 formatHtml()
函数的作用
var formatHtml = function (beginTag, endTag) {
var htmltext = self.textEditor.val();
if (htmltext != null && htmltext.length >= 0) {
var selStart = self.textEditor[0].selectionStart;
var selEnd = self.textEditor[0].selectionEnd;
if (selStart >= 0 && selEnd >= 0) {
var seg1 = htmltext.substring(0, selStart);
var seg2 = htmltext.substring(selStart, selEnd);
var seg3 = htmltext.substring(selEnd);
if (seg1 == null) {
seg1 = "";
}
if (seg2 == null) {
seg2 = "";
}
if (seg3 == null) {
seg3 = "";
}
var htmltext2 = seg1 + beginTag + seg2 + endTag + seg3;
self.textEditor.val(htmltext2);
}
}
};
从上面的代码片段可以看出,我是如何获取所选文本的开始位置和结束位置的
...
if (htmltext != null && htmltext.length >= 0) {
var selStart = self.textEditor[0].selectionStart;
var selEnd = self.textEditor[0].selectionEnd;
...
}
...
文本区域具有 selectionStart
和 selectionEnd
属性。这里有一个有趣的转折。文本区域的引用是一个 JQuery 引用,它始终是一个匹配元素的数组。我非常确定页面上只有一个文本区域。因此,匹配元素数组的第一个元素将是我需要的文本区域。
接下来,我需要将文本区域中的整个文本值分成三部分,我是这样做的
...
if (selStart >= 0 && selEnd >= 0) {
var seg1 = htmltext.substring(0, selStart);
var seg2 = htmltext.substring(selStart, selEnd);
var seg3 = htmltext.substring(selEnd);
...
}
...
它看起来 exactly 就像这样,第一部分从 0 开始,到突出显示文本的开始结束。第二部分从突出显示文本的开始到结束。最后一部分从突出显示文本的结束到整个文本值的结尾。
formatHtml
方法有两个输入参数。第一个是开始 HTML 标签,可以添加到突出显示文本的开头。第二个参数将添加到突出显示文本的末尾。然后,我将这三部分拼接起来以重建整个文本值。这是
...
if (seg1 == null) {
seg1 = "";
}
if (seg2 == null) {
seg2 = "";
}
if (seg3 == null) {
seg3 = "";
}
var htmltext2 = seg1 + beginTag + seg2 + endTag + seg3;
self.textEditor.val(htmltext2);
...
最后一行将值重新赋值给文本区域的值。这就是我如何通过下拉菜单选项来操作文本区域的文本内容。
HTML 编辑器作为可重用组件
这个简单的 HTML 编辑器旨在作为可重用组件。想一想。像这样的 HTML 编辑器可以在许多不同的情况下重复使用(只要 HTML 内容可以在后端安全地消耗)。因此,在应用程序中复制代码是没有意义的。复制代码并更改某些属性以使复制在不同情况下工作,这是一种糟糕的做法。
由于本教程没有使用 RequireJS 进行依赖管理(不想让教程过于复杂),我将定义另一个引用此 HTML 编辑器的组件。新组件上只有一个控件,那就是按钮
单击此按钮后,文本区域的内容将输出到开发者工具控制台。这个新组件定义如下
var otherServiceApp = Stapes.subclass({
btnGetHtml: null,
testAppObj: new testApp(),
constructor: function() {
var self = this;
self.$el = $("#otherServices");
self.btnGetHtml = self.$el.find("#getHtml");
self.btnGetHtml.on("click", function(e) {
var htmlContent = self.testAppObj.getHtmlVal();
console.log(htmlContent);
});
}
});
new otherServiceApp();
基本上,我定义了另一种名为 otherServiceApp
的类类型。这个类类型有两个属性,一个是按钮引用。另一个是 HTML 编辑器的引用。在该类类型的构造函数中,它创建按钮的事件处理程序以及 HTML 编辑器的实例。对于按钮单击的事件处理函数,它将调用 HTML 编辑器类公开的方法 getHtmlVal()
来获取文本区域中的文本值。对 console.log()
的调用会将文本值转储到调试器控制台。
此代码片段的最后一行将构造此第二个组件的匿名实例。这将启动 JavaScript 应用程序。
运行示例应用程序
在构建和运行示例应用程序之前,请查找 src/main/resources/static/assets 文件夹及其子文件夹中所有名为 *.sj 的文件,并将这些文件重命名为 *.js。如果不这样做,示例应用程序将无法工作。
要编译示例应用程序,请转到可以找到 maven POM.xml 文件的基本目录,并运行以下命令
mvn clean install
当您第一次运行此命令时,将花费很长时间。Maven 将下载所有必需的依赖项。最终,它将成功。构建成功后,在同一目录(POM.xml 文件所在的位置)下运行以下命令以启动应用程序
java -jar target/testapp-0.0.1-SNAPSHOT.jar
应用程序将绑定到端口 8080。您可以在此位置访问应用程序
https://:8080/
使用浏览器访问此位置后,您将看到本教程开头显示的第一个屏幕截图。
摘要
本教程到此结束。希望您和我一样喜欢写这篇教程。本教程的目的是展示如何将 Stapes JS 与 JQuery 结合使用来创建客户端应用程序。为了演示,我创建了一个带有实时预览的简单 HTML 编辑器作为示例应用程序。通过这个示例应用程序,我能够证明使用 Stapes JS,我可以创建一个类定义,类似于 Java 或 C# 中定义的类(具有相似的结构)。
示例应用程序还展示了如何定义一个容器元素,并将其分配给 Stapes JS 组件。从那里,可以查询子元素并为其附加事件处理程序。可以通过 Jquery 特有的方法或属性来检索或设置元素包含的值。示例应用程序还展示了如何获取实际 HTML 元素的引用并使用本机 JavaScript 与 HTML 元素进行交互(如何获取文本区域中文本的突出显示位置),以及如何以编程方式更改 HTML 元素持有的值。
这个示例应用程序很棒的一点是,它可以轻松扩展以获得更好的功能。因此,如果您能使应用程序正常工作,就可以通过添加更多编辑功能来扩展它,例如用于装饰的更多 HTML 标签。您还可以提取该组件并在您自己的应用程序中使用它,只要您不介意使用 StapesJS。
历史
- 2020年6月14日 - 初稿