SRM是笔者在学习nodejs过程中的产物,主要实现了将style标签与link标签的css样式,动态解析到dom上。最终生成用style属性写样式的html文本,以符合大多数邮件系统对邮件模板的要求。
另外SRM没有用户体系,用户在进入首页时后,第一次保存时,会同步刷新页面,生成一个唯一链接,用户需要保存该链接。在关闭浏览器后,若想继续编辑之前的内容,访问之前的链接即可。
SRM线上DEMO截图:
SRM功能非常简单,没有复杂的业务逻辑,只有需求明确的功能开发,就是样式的解析,后续我们将一步一步讲解整个编辑器的开发过程。
第一步,选择nodejs建站框架,笔者最近常用express,想当然就是他了。
通过express初始化目录。命令:express projectName
开始构建路由,这里路由我没有从根目录开始构建,考虑到后期可能做首页,所以直接将路由命名为workspace
数据存储设计:
我们的数据读取是根据URL匹配的,而URL只有一个命叫id的参数。每一个id对应moogodb中一份code代码。是滴的。就是这么简单!简单粗爆的设计方式,设计初衷本就是内部工具,没有用户体系这个概念,后期即使需要做用户体系,也将直接接入公司的用户体系,保证公司用户体系的完整性。
页面数据结构如下:1
2
3
4{
urlId: id //页面id
code: str //未解析的html
}
后端开发部分:
一、编辑器页面渲染路由:
构建思路:
- 判断url是否带id参数
- 若无id,直接渲染编辑器首页。
- 若带有id,进入数据查询是否存在,若id不存在,重定向到首页,若id存在,取出对应的code
- 将code返回前端用作源码编辑器显示,将code时行解析返回前端用作邮件html显示。
代码如下:
1 | /* 编辑器页面渲染 */ |
二、核心功能jMailParse介绍
jMailParse模块是作者从同事JMail响应式邮件的工具包中,拆分出来的一个小模块。进行了简单修改,使其适用于SRM项目。该模块主要基于juice处理。非常easy,感兴趣的朋友可以移步 juice 查看具体信息。
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//清理空的style标签
var cleanEmptyStyle = function($){
var $styles = $("style");
$styles.each(function(index, style){
if($(style).is(":empty")){
$(style).remove();
}
});
};
//进行样式解析,此处是个异步操作,提供callback处理后续业务
var parse = function(htmlStr,callback){
juice.juiceResources(htmlStr, {
preserveMediaQueries: true,
removeStyleTags: true,
webResources: {
images: false, // 忽略图片
scripts: false // 忽略 js
}
},function(err, code){
var cheerio = Cheerio.load(code);
cleanEmptyStyle(cheerio);
code = tools.trim(cheerio.html());//由pana-tools模块提供trim处理
if(typeof callback ==="function") callback(code);
});
};
module.exports = parse;
三、运行接口
构建思路:
- 获取postData。
- 执行db update操作, 将urlId做为查询条件 ,有对应数据更新,无对应数据新增。
- 结合前端代码实时刷新邮件html,预览界面。
1 | /* 运行接口 */ |
三、预览路由
构建思路:
- 使用iframe嵌入页面。
- 获取页面参数id,若id不存在,渲染预览界面html为空
- 若id存在将查询数据,若id在数据库中不存在,渲染预览界面html为空
- 若id在数据库中存在,获取对应数据code值。
- 将获取的code利用jMailParse进行解析,用作预览html显示。
1 | /* 预览界面 */ |
后台代码基本结束,下面开始前端部分。
前端开发部分:
前端代码较为简单,注释比较全,大家直接看代码吧,有问题留言即可。
前端主要用到:
- materialize框架
- jQuery库
- codeMirror编辑器
模板部分:
workspace.jade1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17doctype html
html
include header
body
nav.nospace
.nav-wrapper.blue.darken-2.nospace
h1.logo
a(href='/') #{pageConfig.projectName}
span.desc 邮件在线编辑器
include menu
span.tips.green Save & Run : Mac (command+s) | Win (ctrl+s)
.content
.row.nospace
#left.col.l7.nospace
block htmlEditor
#right.col.l5.nospace.fullHeight
block htmlPreview
header.jade1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18head(lang="en")
title #{pageConfig.title}
meta(http-equiv="content-type", content="text/html; charset=UTF-8")
meta(name="description", content="")
meta(name="keywords", content="")
meta(name="viewport", content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;")
- if (pageConfig.devMode == true)
link(rel="stylesheet", href="/stylesheets/base/base.css", type="text/css")
link(rel="stylesheet", href="/stylesheets/#{pageName}.css", type="text/css")
script(type="application/javascript", src="/javascripts/lib/jquery.js")
script(type="application/javascript", src="/javascripts/lib/materialize.min.js")
script(type="application/javascript", src="/javascripts/lib/codemirror.js")
script(type="application/javascript", src="/javascripts/common/api.js")
block beforeJS
script(type="application/javascript", src="/javascripts/#{pageName}.js")
- else
link(rel="stylesheet", href="/stylesheets/dist/#{pageName}.css", type="text/css")
script(type="application/javascript", src="/javascripts/dist/#{pageName}.js")
页面workspace.jade1
2
3
4
5
6
7
8
9
10
11
12
13
14
15extends template/workspace
block beforeJS
script(src="/javascripts/common/codeEditor.js" type="application/javascript")
block htmlEditor
div#htmlEditor
textarea(id="htmlEditorTextarea" placeholder="Code goes here..." name="htmlEditorTextarea") #{code}
div.clearfix.sub-title 邮件HTML:
div#parseResult
textarea(id="htmlParseResult" disabled="disabled" placeholder="result is here..." name="htmlEditorTextarea") #{parsedCodeStr}
block htmlPreview
.htmlPreview.fullHeight
iframe(id="previewIframe" src="/workspace/preview?id=#{urlId}", border="0" width="100%" height="100%")
input(type="hidden",value="#{urlId}",id="urlId")
交互部分:
javascript代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102//获取地址栏参数
function getQueryString(name){
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if(r!=null)return unescape(r[2]); return null;
}
//消息通知,仅用于refresh通知
function message(str){
this.create=function(){
$("body").append('<div id="message" class="message blue">'+str+'</div>');
};
this.destory=function(){
$("#message").remove();
};
}
//计算codeMirror高度
function getHeight($dom,isSetHeight){
var offset=$dom.offset(),
top=offset.top;
if(isSetHeight){
var setHeight=($(window).height()-top)/1.5;
}else{
var setHeight=($(window).height()-top);
}
return setHeight;
}
$(function(){
//实例化源码编辑器,codeEditor基于codeMirror二次封装 ,集成高度自适应功能。
var htmlEditor=new codeEditor({id: "htmlEditorTextarea",isSetHeight: true});
//实例化邮件模板html编辑器,仅用于显示
var htmlParseResultEditor=new codeEditor({id: "htmlParseResult"});
//设置预览页面高度
$(".htmlPreview").height($("#left").height());
var urlId=$("#urlId").val();
//调用后端run接口
var run=function(){
var htmlContent=htmlEditor.getValue();
if(!$.trim(htmlContent)){
return;
}
var msg=new message("refresh...");
msg.create();
//api是笔者封装的api方式,集成统一的消息处理机制,统一路径管理等功能。
api.post({
url: api.url.run,
abort: true,
data: {
code: htmlContent,
urlId: urlId
},
success: function(data){
var queryId=getQueryString("id")
if(!queryId){
//在新页面,执行run时,刷新页面带上urlId;
window.location.href="/workspace?id="+urlId;
}else{
//window.location.reload();
//在已有id的页面,执行run时,仅异步刷新预览界面与邮件模板html编辑器,
$("#previewIframe").attr("src","/workspace/preview?id="+urlId);
htmlParseResultEditor.setValue(data.result.parsedCodeStr);
}
msg.destory();
},
error: function(data){
//console.log(data)
msg.destory();
}
})
}
//ctrl+s保存
$("#htmlEditor").on("keydown.workspace",function(e){
if((e.metaKey && e.keyCode===83) || (e.ctrlKey && e.keyCode===83)){
e.preventDefault()
e.stopPropagation()
run();
}
});
//运行按钮邦定运行方式
$("#runBtn").on("click.workspace",function(){
run();
});
//窗口缩放,自适应高度
$(window).resize(function(){
$htmlEditor=$("#htmlEditor")
$htmlEditor.height(getHeight($htmlEditor,true));
htmlEditor.refresh();
$htmlParseResultEditor=$("#parseResult")
$htmlParseResultEditor.height(getHeight($htmlParseResultEditor));
htmlParseResultEditor.refresh();
$(".htmlPreview").height($("#left").height());
})
})