使用并改造editor.md在JAVA-WEB项目中实现Markdown编辑器

Markdown和Editor.md简介

Markdwon编辑器在技术工作者圈子中已经越来越流行,简单的语法,统一的格式,强大的扩展功能,最重要的是:你可以用Markdown,设计一篇精彩绝伦的文档而完全不需要将你的右手从键盘上移到鼠标上去,这是我和很多编程工作者最热爱的。长期使用Leanote的原因,也是基于有着强大的WEB端和客户端的Markdown编辑器(个人甚至偏向于客户端Leanote)。

Editor.md 是国人开发的开源在线Markdown编辑器,单纯基于前端JavaScript,无需后台代码加持,适用于任何语言(仅在上传图片功能时需要一点后台代码与之配合,其余都交给Editor.md吧),因为是中国人开发的,对中文支持得相当到位。在我的个人博客设计过程中,相当长一段时间都是使用的百度的ueditor作为文档编辑器,说实话,配置那玩意儿相当繁琐,而要想把ueditor配置得符合自己心意更是烦上加烦。在经历了一次SpringMVC与ueditor上传组件冲突的bug后,有种彻底想放弃ueditor的冲动。
长期使用Leanote的Markdown编辑器的我,苦苦寻求一种能取代ueditor的Markdown编辑器,Editor.md 正是我想要的。

Editor.md的安装使用

1.基本使用及表单提交

基本使用markdown是相当简单的,比ueditor还要简单,从git上下载回来的Editor.md是1.5版,压缩包里有分门别类详细的文件夹。

在examples文件夹中有一个简单的示例simple.html,可以在浏览器里打开,并查看源代码,我这里做一个简单的总结,并加上表单提交的配置:

  • 在HTML中加载CSS:editormd.css
  • 在HTML中加载JS:顺序为jQuery,editormd.min.js,
  • 在HTML中写一个div节点,包含两个textarea,格式如下:
    <div class="editormd" id="test-editormd">
      <textarea class="editormd-markdown-textarea" name="test-editormd-markdown-doc"></textarea>
      <!-- 第二个隐藏文本域,用来构造生成的HTML代码,方便表单POST提交,这里的name可以任意取,后台接受时以这个name键为准 -->
      <textarea class="editormd-html-textarea" name="text"></textarea>
    </div>
    
  • 在HTML中写一句javascript:

    <script type="text/javascript">
      $(function() {
          editormd("test-editormd", {
              width   : "90%",
              height  : 640,
              syncScrolling : "single",
              //你的lib目录的路径,我这边用JSP做测试的
              path    : "<%=request.getContextPath()%>/resources/editormd/lib/",
              //这个配置在simple.html中并没有,但是为了能够提交表单,使用这个配置可以让构造出来的HTML代码直接在第二个隐藏的textarea域中,方便post提交表单。
              saveHTMLToTextarea : true
          });
      });
    </script>
    

    OK,这样就完成了一个最简单的editor.md的编辑器了,你可以在这里面书写你熟悉的Markdown文档,里面可以包含代码,右侧有实时的预览。如图所示:

    2.图片上传

    有了基本的Markdown功能,集成editor.md就完成了一半了,下面开始处理图片上传。
    图片上传的语法是![alt](url),这个用来嵌入互联网上现成的图片是很方便的,但是如果想要上传本地图片就要后台代码配合了,我下面以JAVA为例(官方文档有PHP的示例),配合SpringMVC和commons-fileupload-1.3.1.jar,简单给个DEMO:
    根据Editor.md的官方文档介绍,上传图片功能需要添加一点配置,如下:

    editormd("test-editormd", {
      width   : "90%",
      height  : 640,
      syncScrolling : "single",
      path    : "<%=request.getContextPath()%>/resources/editormd/lib/",
      imageUpload : true,
      imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
      imageUploadURL : "/uploadfile",
      saveHTMLToTextarea : true,
    });
      //editor.md期望得到一个json格式的上传后的返回值,格式是这样的:
      /*
      {
          success : 0 | 1,           // 0 表示上传失败,1 表示上传成功
          message : "提示的信息,上传成功或上传失败及错误信息等。",
          url     : "图片地址"        // 上传成功时才返回
      }
      */
    

    以上代码并不难理解,也就加了三行配置,关键的是imageUploadURL : "/uploadfile"这个配置,这里的URL指向了你处理图片上传的action,与之对应的,我的SpringMVC控制器是这样的,这里贴出了整个代码,防止有小伙伴对JAVA以及SpringMVC处理文件上传还不太熟练:

    package com.newflypig.jblog.controller;
    import java.io.File;
    import java.io.IOException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.apache.commons.io.FileUtils;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.multipart.MultipartFile;
    @Controller
    public class UploadController {
    
      @RequestMapping(value="/uploadfile",method=RequestMethod.POST)
      public void hello(HttpServletRequest request,HttpServletResponse response,@RequestParam(value = "editormd-image-file", required = false) MultipartFile attach){
          try {
              request.setCharacterEncoding( "utf-8" );
              response.setHeader( "Content-Type" , "text/html" );
              String rootPath = request.getSession().getServletContext().getRealPath("/resources/upload/");
    
              /**
               * 文件路径不存在则需要创建文件路径
               */
              File filePath=new File(rootPath);
              if(!filePath.exists()){
                  filePath.mkdirs();
              }
    
              //最终文件名
              File realFile=new File(rootPath+File.separator+attach.getOriginalFilename());
              FileUtils.copyInputStreamToFile(attach.getInputStream(), realFile);
    
              //下面response返回的json格式是editor.md所限制的,规范输出就OK
              response.getWriter().write( "{\"success\": 1, \"message\":\"上传成功\",\"url\":\"/resources/upload/" + attach.getOriginalFilename() + "\"}" );
          } catch (Exception e) {
              try {
                  response.getWriter().write( "{\"success\":0}" );
              } catch (IOException e1) {
                  e1.printStackTrace();
              }
          }
      }
    }
    

    这样就完成了图片上传了,上传后,后台action返回了一个url给editor.md,editor.md使用这个url作为你嵌套在文档中的图片url。
    这样就大功告成了,是不是很爽,要比ueditor的上传配置简单100倍。

    3.Editor.md代码黑色主题

    用惯了sublime text等编辑器,是不是对代码的渲染有点要求呢,先上博主两个IDE的截图吧,一个是sublime text3,一个是myeclipse2015:


    习惯了黑色背景的代码样式,就希望editor.md也能实现代码黑色样式,果然,editor.md从1.5版本以后为大家提供了dark样式主题,但是会让除代码以外的其他编辑区域也变黑色,所以根据个人需要来小小的改造一下:

  • 首先添加样式配置,在原来的editor.md配置基础上,添加配置项:

    $(function() {
      editormd("test-editormd", {
      width   : "90%",
      height  : 640,
      syncScrolling : "single",
      path    : "<%=request.getContextPath()%>/resources/editormd/lib/",
      imageUpload : true,
      imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
      imageUploadURL : "/uploadfile",
      saveHTMLToTextarea : true,
      //下面这一行将使用dark主题
      previewTheme : "dark"
      });
    });
    

    配置好dark主题以后,编辑区还是原来的编辑区,预览区已经使用了暗黑模式,但是代码以外的部分也都变成黑色背景了,这不是我想要的,所以我对editormd.css做了一些修正,将dark主题代码以外的部分取消了样式定义,这样预览起来只有代码块是暗黑模式,截图如下:

    修改了editormd.css后,别忘了使用CSS压缩工具再压缩一遍,生成editormd.min.css,这样正式部署时能减轻一点服务器压力,提高加载效率。我把压缩好重新生成的editormd.min.css放出来,有需要的可以直接下载。
    editormd.min.css

    4. 文档的显示

    编辑区的代码格式已经调整成为我们喜欢的样式了,在表单POST提交时,editormd将我们的markdown语法文档翻译成了HTML语言,并将html字符串提交给了我们的后台,后台应该将这些HTML字符串持久化到数据库中,在文章显示时将他们显示在页面上。
    具体的做法是:

    <link rel="stylesheet" href="<%=request.getContextPath()%>/resources/editormd/css/editormd.preview.min.css" />
    <link rel="stylesheet" href="<%=request.getContextPath()%>/resources/editormd/css/editormd.css" />
    <!-- 因为我们使用了dark主题,所以在容器div上加上dark的主题类,实现我们自定义的代码样式 -->
    <div class="content editormd-preview-theme-dark" id="content">${article.text }</div>
    <script src="<%=request.getContextPath()%>/resources/js/jquery.min.js"></script>
    <script src="<%=request.getContextPath()%>/resources/editormd/lib/marked.min.js"></script>
    <script src="<%=request.getContextPath()%>/resources/editormd/lib/prettify.min.js"></script>
    <script src="<%=request.getContextPath()%>/resources/editormd/editormd.min.js"></script>
    <script type="text/javascript">
      editormd.markdownToHTML("content");
    </script>
    

    至此,我们所有的工作都完成了。(另外还有些editor.md高级功能,比如[TOC]标签自动生成文档目录结构、流程图语法等,我还没研究,不过现在已经满足我的所有要求了,感兴趣的朋友可以继续阅读examples文件夹中各种示例。)
    如果您各项基础知识掌握得都还可以的话,将editor.md这个编辑器引入你的项目是相当轻松加愉快的。写这篇blog也确实因为对这个编辑器的喜爱,加上官方尚未有一个系统的cookbook,都是一个个小demo,希望能帮到想使用editor.md的朋友。

给editor.md添加七牛上传插件



editor.md 是一款支持markdown的WEB编辑器,用户可以在自己的网站上使用其进行内容编辑,这在之前的博客里有详细的使用介绍。
本文主要介绍如何自定义一个支持七牛上传的editor.md插件。

editor.md 自带图片上传组件

首先介绍一下使用editor.md的自带图片上传组件的配置,这对下面开发七牛上传插件有很大的帮助。
初始化 editor.md 时使用以下配置即可打开图片上传功能。

<div class="editormd" id="id_editormd">
    <textarea class="editormd-markdown-textarea" name="markdown"></textarea>
    <textarea class="editormd-html-textarea" name="html"></textarea>
</div>
<script>
editormd("id_editormd", {
    width : "100%",
    height : 540,
    syncScrolling : "single",
    path : "${rc.contextPath}/resources/editormd/lib/",
    imageUpload : true,
    imageFormats : [ "jpg", "jpeg", "gif", "png", "bmp", "webp" ],
    imageUploadURL : "/uploadfile",
    saveHTMLToTextarea : true
}
</script>

其中的/uploadfile指向了图片上传的后台URL地址,在editor.md进行图片上传时,将构造一个 enctype="multipart/form-data" 的form表单,然后使用iframe的方式向该URL异步POST上传,上传后后台返回一个固定格式json,包含了上传成功与否,以及上传成功后的图片URL信息,json格式如下:{"success":0|1,"message":"xxx","fileName":"imageURL"}

以JAVA为例,编写后台上传URL服务器端代码(SpringMVC):

@RequestMapping(value="/uploadfile",method=RequestMethod.POST)
public ModelAndView upload(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "editormd-image-file", required = false) MultipartFile attach){
    String rootPath = request.getSession().getServletContext().getRealPath("/resources/upload/");
    response.setHeader( "Content-Type" , "text/html" );
    ModelAndView mv = new ModelAndView("upload_result");
    try {
        File filePath=new File(rootPath);        
        /**
         * 文件路径不存在则需要创建文件路径
         */
        if(!filePath.exists()){
            filePath.mkdirs();
        }

        //最终文件名
        File realFile=new File(rootPath + File.separator + UUID.randomUUID().toString() + ".jpg");
        FileUtils.copyInputStreamToFile(attach.getInputStream(), realFile);

        mv.addObject("success", 1);
        mv.addObject("message", "上传成功");
        mv.addObject("fileName", realFile.getName());
    } catch (Exception e) {
        mv.addObject("success", 0);
        mv.addObject("message", "上传失败,异常信息:" + e.getMessage());
    }

    return mv;
}

//upload_result.json:
//{"success": ${success}, "message":"${message}"<#if fileName ??>,"url":"/resources/upload/${fileName}"</#if>}

以上就是使用editor.md图片上传的整个流程了。

七牛云存储

七牛云存储 是国内比较著名的云存储服务,为免费用户提供高达10G的存储空间,API齐全,文档够详细,上传下载速度快,丰富的水印、缩放、防盗链等功能。
使用云存储对于博客类网站是非常不错的主意,节省了大量的带宽和流量,毕竟我们购买的VPS,包括阿里云、腾讯云等产品,都是流量计费比包年付费划算的,如果采用流量计费的方式,图片流量绝对会比HTML文本大得多,而如果采用包年包月计费,这时候带宽是一定的,加载图片会占用大量带宽,从而影响页面加载速度。此外在进行博客搬家时,我们无需面对高达几百M甚至上G的上传文件夹,仅需将WEB代码和mySQL数据库拷贝一份出来即可。综上所述,为了节省带宽和流量资源,提高网页加载速度,并且简化搬家过程,使用云服务器进行图片资源的第三方存储,是绝对有好处的。

为 editor.md 设计七牛云存储插件

我百度了一下 editor.md 的云存储插件,并没有发现任何可以拿来直接用的插件,于是萌生了自己开发editor.md插件的想法。

上传流程

七牛的上传流程是这样的:
第一步:浏览器客户端向本地URL发送GET请求,请求一串上传令牌token,服务器端通过七牛的公钥、密钥和空间名称,以及自定义的上传策略,构造一串token给客户端,这个token是一组加密过的散列码,长度不固定。这个过程推荐使用ajax请求和json传输。
第二步:浏览器将拿到的令牌token和要上传的图片一起通过multipart加密form传输给七牛服务器,七牛服务器对token使用密钥验证和解析,得到是谁上传的,上传到哪个资源空间等信息,然后返回一个处理结果给浏览器。这其中包含上传成功与否,图片上传后的URL后缀地址,错误信息等。
以上两步即完成了七牛云服务器上传的全部,下面着手来开发:

插件入口

首先editor.md提供了自定义插件接口,这对我们编写插件带来了可行性性,官方的自定义插件文档在这里:https://pandao.github.io/editor.md/examples/define-plugin.html
依照这个示例程序,改造editor.md的初始化配置函数:

editormd("domId", {
    width : "100%",
    height : 540,
    syncScrolling : "single",
    path : "${rc.contextPath}/resources/editormd/lib/",
    imageUpload : true,
    imageFormats : [ "jpg", "jpeg", "gif", "png", "bmp", "webp" ],
    imageUploadURL : "/uploadfile",
    saveHTMLToTextarea : true,
    previewTheme : "dark",
    toolbarIcons : function() {
    return  ["bold", "del", "italic", "hr", "image", "qiniu", "table", "datetime", "|", "preview", "watch", "|", "fullscreen"];
  },
  //配置七牛上传插件
  toolbarIconsClass : {
      qiniu : "fa-cloud-upload"
  },
  toolbarHandlers : {
      qiniu : function(cm, icon, cursor, selection) {
          this.imageDialogQiniu();
      }
  },
  qiniuTokenUrl : "/getQiniuToken",                        //本地服务器获取七牛token的url
  qiniuPublishUrl : "https://files.hexcode.cn/"    //远程七牛服务器个人发布地址
});

可以看到上面我们配置了一个一个名为qiniu的插件,并且定义了它的入口函数:imageDialogQiniu();,并添加了两个配置字符串qiniuTokenUrlqiniuPublishUrl,这两个字符串第一个是本地服务器URL,用来从本地获取上传令牌,这个后面会讲;第二个URL是在你注册七牛帐户后七牛分配给你的默认空间URL前缀,你可以付费开通正式帐号来自定义这个前缀,我暂时没有特别的需求,先使用的七牛免费功能,等后续有更多需求的时候再考虑是否成为七牛付费会员。

七牛空间前缀及上传令牌


注册成为七牛会员后,进入七牛控制台,添加一个「对象存储」资源,如图所示,右边的「我的资源」中会出现这个资源空间,点击这个资源,即可看到自己的上传空间的URL前缀了,上面讲过,免费用户的空间前缀是随机的HASH码,付费以后可以自定义这个前缀,界面如下:

下面我们来查看七牛分配给我们构造上传令牌时所需的公钥和密钥:

右上方「个人面板」->「密钥管理」即可查看,其中AK是公钥,SK是密钥,虽说现在的RSA加密机制中公钥是可以传播的,密钥是不可以传播的,但我还是建议这两个密钥都不要随意公布,所以这里我隐藏了。
好了,URL前缀,公钥,密钥都有了,七牛给我们的这三个看上去像乱码,却蕴含特殊意义的字符串都拿到手了,继续我们的开发吧。

编写插件JS


在editor.md的文档结构中,plugins文件夹是所有的自带的插件,我们添加七牛插件也是如此,添加一个文件夹,名为image-dialog-qiniu,并创建image-dialog-qiniu.js
其中的代码我大部分模仿了原有的image-dialog插件,但是改造了其有点不知所谓的iframe上传方法,使用我偏爱的jQuery ajax post异步上传,掌握这些知识点的朋友应该能够看明白,代码如下:

/*!
 * Image (upload) dialog By Qiniu plugin for Editor.md
 *
 * @file        image-dialog-qiniu.js
 * @author      newflydd@189.cn
 * @version     1.3.4
 * @updateTime  2016-09-02
 * {@link       https://www.hexcode.cn}
 * @license     MIT
 */

(function() {

    var factory = function (exports) {

        var pluginName   = "image-dialog-qiniu";

        exports.fn.imageDialogQiniu = function() {

            var _this       = this;
            var cm          = this.cm;
            var lang        = this.lang;
            var editor      = this.editor;
            var settings    = this.settings;
            var cursor      = cm.getCursor();
            var selection   = cm.getSelection();
            var imageLang   = lang.dialog.image;
            var classPrefix = this.classPrefix;
            var iframeName  = classPrefix + "image-iframe";
            var dialogName  = classPrefix + pluginName, dialog;
            var ajaxToken    = "";        //向本地服务器请求七牛的上传token

            cm.focus();

            var loading = function(show) {
                var _loading = dialog.find("." + classPrefix + "dialog-mask");
                _loading[(show) ? "show" : "hide"]();
            };

            if (editor.find("." + dialogName).length < 1) {
                var guid   = (new Date).getTime();
                var action = settings.imageUploadURL + (settings.imageUploadURL.indexOf("?") >= 0 ? "&" : "?") + "guid=" + guid;

                if (settings.crossDomainUpload)
                {
                    action += "&callback=" + settings.uploadCallbackURL + "&dialog_id=editormd-image-dialog-" + guid;
                }

                var dialogContent = ( (settings.imageUpload) ? "<form id=\"qiniuUploadForm\" method=\"post\" enctype=\"multipart/form-data\" class=\"" + classPrefix + "form\" onsubmit=\"return false;\">" : "<div class=\"" + classPrefix + "form\">" ) +                                      
                                        "<label>" + imageLang.url + "</label>" +
                                        "<input type=\"text\" data-url />" + (function(){
                                            return (settings.imageUpload) ? "<div class=\"" + classPrefix + "file-input\">" +
                                                                                "<input type=\"file\" name=\"file\" accept=\"image/*\" />"            +
                                                                                "<input type=\"submit\" value=\"七牛上传\" click=\"alert('dd')\" />" +
                                                                            "</div>" : "";
                                        })() +
                                        "<br/>" +
                                        "<input name=\"token\" type=\"hidden\" value=\"" + ajaxToken + "\">"    +        //七牛的上传token
                                        "<label>" + imageLang.alt + "</label>" +
                                        "<input type=\"text\" value=\"" + selection + "\" data-alt />" +
                                        "<br/>" +
                                        "<label>" + imageLang.link + "</label>" +
                                        "<input type=\"text\" value=\"https://\" data-link />" +
                                        "<br/>" +
                                    ( (settings.imageUpload) ? "</form>" : "</div>");

                dialog = this.createDialog({
                    title      : imageLang.title,
                    width      : (settings.imageUpload) ? 465 : 380,
                    height     : 254,
                    name       : dialogName,
                    content    : dialogContent,
                    mask       : settings.dialogShowMask,
                    drag       : settings.dialogDraggable,
                    lockScreen : settings.dialogLockScreen,
                    maskStyle  : {
                        opacity         : settings.dialogMaskOpacity,
                        backgroundColor : settings.dialogMaskBgColor
                    },
                    buttons : {
                        enter : [lang.buttons.enter, function() {
                            var url  = this.find("[data-url]").val();
                            var alt  = this.find("[data-alt]").val();
                            var link = this.find("[data-link]").val();

                            if (url === "") {
                                alert(imageLang.imageURLEmpty);
                                return false;
                            }

                            var altAttr = (alt !== "") ? " \"" + alt + "\"" : "";

                            if (link === "" || link === "https://")
                            {
                                cm.replaceSelection("![" + alt + "](" + url + altAttr + ")");
                            }
                            else
                            {
                                cm.replaceSelection("[![" + alt + "](" + url + altAttr + ")](" + link + altAttr + ")");
                            }

                            if (alt === "") {
                                cm.setCursor(cursor.line, cursor.ch + 2);
                            }

                            this.hide().lockScreen(false).hideMask();

                            return false;
                        }],

                        cancel : [lang.buttons.cancel, function() {
                            this.hide().lockScreen(false).hideMask();

                            return false;
                        }]
                    }
                });

                dialog.attr("id", classPrefix + "image-dialog-" + guid);

                if (!settings.imageUpload) {
                    return ;
                }

                var fileInput  = dialog.find("[name=\"file\"]");

                var submitHandler = function() {
                    $.ajax({
                        url         : settings.qiniuTokenUrl,
                        type     : "post",
                        dataType : "json",
                        timeout  : 2000,
                        beforeSend : function(){loading(true);},
                        success  : function(result){
                            if(result.resultCode == 0){
                                ajaxToken = result.data;

                                if(ajaxToken === ""){
                                    loading(false);
                                    alert("没有获取到有效的上传令牌,无法上传!");
                                    return;
                                }
                                dialog.find("[name=\"token\"]").val(ajaxToken);
                                var formData = new FormData( $("#qiniuUploadForm")[0] );
                                dialog.find("[name=\"token\"]").val();    //隐藏令牌
                                $.ajax({
                                    url: 'https://upload.qiniu.com/' ,  
                                    type: 'POST',  
                                    data: formData,
                                    dataType: "json",
                                    beforeSend:function(){loading(true);},
                                    cache: false,
                                    contentType: false,
                                    processData: false,
                                    timeout : 30000,
                                    success: function (result) {
                                        dialog.find("[data-url]").val(settings.qiniuPublishUrl + result.key);
                                    },
                                    error : function(){alert("上传超时");},
                                    complete:function(){loading(false);}
                                });
                            }
                            else
                                alert(result.message);
                        }
                    });
                };

                dialog.find("[type=\"submit\"]").bind("click", submitHandler);

                fileInput.bind("change", function() {
                    var fileName  = fileInput.val();
                    var isImage   = new RegExp("(\\.(" + settings.imageFormats.join("|") + "))$"); // /(\.(webp|jpg|jpeg|gif|bmp|png))$/

                    if (fileName === "")
                    {
                        alert(imageLang.uploadFileEmpty);

                        return false;
                    }

                    if (!isImage.test(fileName)){
                        alert(imageLang.formatNotAllowed + settings.imageFormats.join(", "));
                        return false;
                    }

                    dialog.find("[type=\"submit\"]").trigger("click");
                });
            }

            dialog = editor.find("." + dialogName);
            dialog.find("[type=\"text\"]").val("");
            dialog.find("[type=\"file\"]").val("");
            dialog.find("[data-link]").val("https://");

            this.dialogShowMask(dialog);
            this.dialogLockScreen();
            dialog.show();

        };

    };

    // CommonJS/Node.js
    if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
    {
        module.exports = factory;
    }
    else if (typeof define === "function")  // AMD/CMD/Sea.js
    {
        if (define.amd) { // for Require.js

            define(["editormd"], function(editormd) {
                factory(editormd);
            });

        } else { // for Sea.js
            define(function(require) {
                var editormd = require("./../../editormd");
                factory(editormd);
            });
        }
    }
    else
    {
        factory(window.editormd);
    }

})();

后面一段的// CommonJS/Node.js是原模板自带的,可能是为Node.js编写的WEB服务器提供功能,我们用不到,这里就不深究了。

本地服务器令牌生成

在刚刚那个插件JS中,我们首先向本地服务器索取上传令牌,这个令牌是一串hash字符串,内容包括了从申请令牌到上传图片成功或者失败之间的上传时限,上传的空间,以及更多的上传策略。构造这一串令牌的代码在本地WEB服务器中实现,以JAVA为例:

/**
 * 七牛上传令牌获取
 * @return
 * @throws JblogException 
 */
@RequestMapping(value="/getQiniuToken",method=RequestMethod.POST)
public ModelAndView qiniu() throws JblogException{
    Result result = new Result();

    if(StringUtils.isEmpty(BlogConfig.QN_ACCESSKEY) || StringUtils.isEmpty(BlogConfig.QN_SECRETKEY)){
        result.setCode(Result.FAIL);
        result.setMessage("数据库中没有正确的七牛服务器密钥,无法生成令牌");
        return new ModelAndView("ajaxResult", "result", result);
    }

    Auth auth = Auth.create(BlogConfig.QN_ACCESSKEY, BlogConfig.QN_SECRETKEY);
    String token = auth.uploadToken(
        "blog-images",    //空间名称
        null,            //key,最终资源名称,一般为空,让服务器自己生成
        3600,            //3600秒,上传时限
        new StringMap()    //其他上传策略
            .put("saveKey", UUID.randomUUID().toString() + "$(ext)")
    );

    result.setData("\"" + token + "\"");

    return new ModelAndView("ajaxResult", "result", result);
}

其中的Auth是七牛官网提供的jar包中的,不需要直接下载,可以使用maven或者gradle获取,七牛已经将这个jar提交到了maven国际公共库了,介绍文档在这里:https://developer.qiniu.com/code/v7/sdk/java.html
我用的gradle,直接添加:

compile 'com.qiniu:qiniu-java-sdk:7.x.+'

到此,我们整个的editor.md七牛插件就编写好了。如下方所示:

回顾一下开发步骤

  1. 在editor.md的初始化函数中添加插件的描述。
  2. 在editor.md的plugins文件中添加相关的插件文件夹和插件JS,js代码可以根据插件模板书写。
  3. 到七牛官网注册,并在个人中心添加资源空间,收集三个字符串数据:资源空间的url前缀,AK公钥,SK私钥。
  4. 编写本地服务器端代码,使用AK和SK构造token上传令牌,构造时使用的类来自七牛JAVA文档。

其中最复杂的是第二步,我所公布的插件JS篇幅比较长,需要耐心观察。其中的知识点包括jQuery;动态生成form;ajax提交等,这些都是互联网应用开发的基础知识点。

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd

  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • Git OSChina 上积极参与开源社区