这里文章都是从个人的github博客直接复制过来的,排版可能有点乱. 原始地址  http://benq.im
 

前段时间用hexo重新搭了个人博客,顺便写了个简单的博客搭建教程.

markdown写起博客流畅很多,但是用了几个markdown编辑器,都没有一个适合自己使用的。于是就想自己动手做一个,当然不是完全从0开始做,语法高亮和markdown解析都用的是开源的项目.

从这篇开始,我会把整个开发过程记录成系列随笔,因此开发进度较为缓慢.

博客写得少,像这样写长一点的随笔就有点混乱,看不懂的请用力喷,我会努力改进.

简介

先介绍下开发过程中用到的一些比较重要的开源项目:

  1. nw.js,原名node-webkit,用webkit和node来做基于web技术的跨平台客户端软件.
  2. CodeMirror,基于web技术实现的文本编辑器,实现了大部分的IDE功能以及几乎全部你会用到的语言的支持.目前我日常开发都是用这个IDE,甚至在做hexomd这个项目时用的IDE也是CodeMirror做的.
  3. angularjs,google的mvvm开发框架,这个相信不用我多做介绍.我用的不熟,觉得好用就拿来即用,没有深入的了解过.

关于这些开源项目的使用,我在这系列文章里不会详细解释,如果有疑问,可以去看官网的入门教程和wiki,当然也欢迎讨论.

项目结构

图片里的是我目前的项目结构,大概讲解一下一些目录和文件的用途。

  1. icudtl.dat,nw.exe,nw.pak
    这3个是nw.js在windows运行所必须的文件.

  2. package.json
    nw.js的配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
    "name": "HexoMD",
    "description": "Markdown for hexo",
    "main": "app/index.html",//程序入口页面
    "author": "hmjlr123@gmail.com",
    "license": "MIT",
    "directories": {
    "test": "no"
    },
    "devDependencies": {},
    //窗口配置
    "window": {
    "title": "HexoMD",
    "icon": "app/img/logo.png", //logo
    "toolbar": true, //是否显示地址栏工具条(调试的时候启用)
    "frame": false, //是否显示程序边框
    "width": 1000, //默认宽度
    "height": 700, //默认高度
    "position": "center", //启动时在屏幕中的位置
    "min_width": 600, //最小宽度
    "min_height": 400 //最小高度
    }
    }
  3. app目录
    程序的所有源代码的根目录.

  4. app/lib
    存放angular,jquery,codemirror等开源库/框架的源代码

  5. app/helpers
    存放一些node的工具函数

  6. app/modules
    程序代码在这个目录,按功能模块分成不同的子目录.
    modules/app.js是整个程序的入口点

  7. app/package.json
    node模块配置,注意与上层的package.json意义不同

  8. app/index.html
    程序的主界面窗口

程序主界面

index.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hexo Markdown</title>
<link href="css/bootstrap.css" rel="stylesheet">
<link href="lib/codemirror/lib/codemirror.css" rel="stylesheet" />
<link href="css/index.css" rel="stylesheet">
</head>
<body>
<!--导航栏-->
<nav class="navbar navbar-inverse navbar-fixed-top">
省略...
</nav>
<!--模块视图区域-->
<article class="container app" ui-view ng-animate="'view'"></article>
<!--工具栏-->
<footer class="tool"></footer>
<!--end codemirror-->
<script src="lib/jquery-2.1.3.js"></script>
<script src="lib/angular.js"></script>
<script src="lib/angular-ui-router.js"></script>
<!--程序入口函数-->
<script src="modules/app.js"></script>
<script>
//初始化angular,hmd为自定义的根模块名
angular.bootstrap($('body'), ['hmd']);
</script>
</body>
</html>

只贴出部分代码.以后的所有代码也类似,都只会把重要的贴出来,并给出完整的链接.


界面采用比较简洁的三栏布局,分别为导航栏内容区状态栏/工具条.
最顶部的地址栏只有在开发的时候为了方便调试才开启,发布时会关闭掉.

拖动窗口

为了美观,我们在配置里去掉了系统自带的边框.因此要实现自定义的拖动窗口功能还需要增加一些设置.
所谓的设置,其实只要加上对应的样式即可,功能都由nw.js实现了.

1
2
3
.navbar{
-webkit-app-region: drag;
}

带有此样式的元素可以作为窗口的拖拽区域,并且双击时最大化/还原窗口.

1
2
3
.navbar .navbar-collapse a {
-webkit-app-region: no-drag;
}

被标志为可drag的容器里的链接将不可点击,因此要特别为链接加上no-drag

另外为了让程序看起来更像客户端一点,我默认禁用掉了文本选择,防止一些被作为按钮的a标签的文本被选中

1
2
3
4
html {
height: 100%;
-webkit-user-select: none;
}

app.js

app.js作为程序的入口点,定义了整个项目代码的结构,需要特别拿出来说明一下.

1
angular.module('hmd', ['ui.router','hmd.studio'])

定义angular模块,modules所有的业务模块都会放到单独的子目录里,如这里注册的hmd.studio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//模块根目录
var baseModuleDir = './app/modules/';
//引入模块,模块内js文件会被自动加载到页面中
hmd.regModule = function (name, reqModule) {
hmd[name] = angular.module('hmd.' + name, reqModule || []);
hmd[name].moduleName = name;
//模块存储数据的目录
hmd[name].dataPath = hmd.storeDir + '\\' + hmd[name].moduleName;
fs.readdirSync(baseModuleDir + name)
.forEach(function (file) {
if (~file.indexOf('.js')) {
document.write('<script src="modules/' + name + '/' + file + '"></script>');
}
});
};

regModule方法实现最简单的模块载入,自动加载模块内的所有脚本到页面中,并为每个模块赋予一个单独的数据存储目录dataPath

1
hmd.storeDir =  require('nw.gui').App.dataPath;

程序的数据存储目录

导航栏按钮

导航栏右边有4个按钮,分别为:检查更新最小化最大化关闭

1
2
3
4
5
6
7
8
9
10
...
<!--导航栏功能按钮-->
<div class="btn-group window-tool">
<a class="btn rectbtn" href="javascript://" title="点击检查更新">
<i class="glyphicon mdfi_action_system_update_tv"></i></a>
<a class="btn rectbtn" href="javascript://"><i class="glyphicon glyphicon-minus"></i></a>
<a class="btn rectbtn" href="javascript://"><i class="glyphicon glyphicon-fullscreen"></i></a>
<a class="btn rectbtn" href="javascript://"><i class="glyphicon glyphicon-remove"></i></a>
</div>
...

检查更新等以后再实现.现在先实现后面3个功能
因为这3个功能是全局的,因此在modules根目录新建directives.js用于实现全局的Directive.

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
(function () {
var gui = require('nw.gui'), win = gui.Window.get(),winMaximize = false;
angular.module('hmd.directives', [])
//最小化窗口
.directive('hmdMinisize', [function () {
return function (scope, elem) {
$(elem[0]).on('click', function () {
win.minimize();
});
};
}])
//最大化与还原窗口
.directive('hmdMaxToggle', [function () {
return function (scope, elem) {
//窗口最大化和还原时会触发对应的事件,在事件里去控制按钮样式.
//TODO:这里的实现应该可以优化得更优雅一点,以后再说
win.on('maximize', function () {
winMaximize = true;
$(elem[0]).find('i').removeClass('glyphicon-fullscreen').addClass('glyphicon-resize-small');
});
win.on('unmaximize', function () {
winMaximize = false;
$(elem[0]).find('i').removeClass('glyphicon-resize-small').addClass('glyphicon-fullscreen');
});
//切换窗口状态
$(elem[0]).on('click', function () {
if (winMaximize) {
win.unmaximize();
}
else {
win.maximize();
}
});
};
}])
//关闭应用程序
.directive('hmdClose', [function () {
return function (scope, elem) {
$(elem[0]).on('click', function () {
require('nw.gui').Window.get().close();
});
};
}]);
})();

定义了全局directive模块angular.module('hmd.directives', []),实现了3个Directive.

接下来将directive应用到按钮上

1
2
3
4
5
...
<a class="btn rectbtn" href="javascript://" hmd-minisize><i class="glyphicon glyphicon-minus"></i></a>
<a class="btn rectbtn" href="javascript://" hmd-max-toggle><i class="glyphicon glyphicon-fullscreen"></i></a>
<a class="btn rectbtn" href="javascript://" hmd-close><i class="glyphicon glyphicon-remove"></i></a>
...

将脚本引用<script src="modules/directives.js"></script>添加到index.html的app.js之后

app.js里的angular模块注册里增加hmd.directives模块angular.module('hmd', ['ui.router','hmd.directives','hmd.studio'])

刷新程序,三个按钮已经生效.

实现简单的markdown编辑器

先在页面添加相应的codemirror脚本引用

1
2
3
4
5
6
7
8
9
10
11
...
<footer class="tool"></footer>
<!--codemirror-->
<script src="lib/codemirror/lib/codemirror.js"></script>
<script src="lib/codemirror/addon/mode/overlay.js"></script>
<script src="lib/codemirror/addon/edit/continuelist.js"></script>
<script src="lib/codemirror/mode/markdown/markdown.js"></script>
<script src="lib/codemirror/mode/gfm/gfm.js"></script>
<!--end codemirror-->
<script src="lib/jquery-2.1.3.js"></script>
...

然后在modules目录下新增studio子目录,所有编辑器功能都在这个模块里实现.
app.js里增加加载studio模块的代码

1
hmd.regModule('studio');

每个子模块一般都会包含route.js,controllers.js,directive.js这三个基本的angular功能.以及views子目录,用于存放模块用到的html视图

studio模块多了一个editor.js,我们将编辑器的一些基本功能封装在这个脚本里

定义路由

route.js

1
2
3
4
5
6
7
8
hmd.studio.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('studio', {
url: "/studio",
templateUrl: "modules/studio/views/studio.html",
controller: 'studio'
});
});

修改app.js,将默认路由指定到/studio模块

1
2
3
hmd.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/studio");
});

实现controller

controllers.js

1
2
3
4
5
var studio = hmd.studio;
studio
.controller('studio', function ($scope, $state, $stateParams) {
console.log('stuido controller');
});

添加视图模版

views/studio.html

1
2
3
<div class="content studio-wrap">
<textarea name="" cols="30" rows="10"></textarea>
</div>

重新打开应用,可以看到模块跳到了studio路由,并且执行了对应的控制器

实现editor

editor.js

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
var util = require('./helpers/util');
var defaultConfig = {
theme: 'ambiance',
mode: 'gfm',
lineNumbers: false,
extraKeys: {"Enter": "newlineAndIndentContinueMarkdownList"},
dragDrop: false,
autofocus: true,
lineWrapping: true,
foldGutter: true,
styleActiveLine: true
}; hmd.editor = {
init: function (options,filepath) {
var el = options.el,txt,me = this;
options = $.extend({}, defaultConfig, options);
//编辑器样式文件动态加载,用于以后增加样式选择功能
if(options.theme != 'default'){
$('head').append('<link href="lib/codemirror/theme/'+options.theme+'.css" rel="stylesheet" />');
}
this.cm = this.cm || CodeMirror.fromTextArea(el, options);
//指定要打开的文件,如果未指定,则保存时会弹出文件选择对话框
filepath && this.setFile(filepath);
//编辑器内容修改时触发change事件
this.cm.on('change', function (em, changeObj) {
me.hasChange = true;
me.fire('change', {
em: em,
changeObj: changeObj
});
});
//绑定按键
this.cm.addKeyMap({
"Ctrl-S": function () {
me.save();
}
});
},
//设置当前文件
setFile:function(filepath){
var txt = util.readFileSync(filepath);
this.filepath = filepath;
this.cm.setValue(txt);
},
//弹出保存文件对话框
saveAs:function(){
hmd.msg('保存新文件');
},
//保存文件
save: function () {
var txt = this.cm.getValue();
if(this.filepath){
util.writeFileSync(this.filepath, txt);
this.hasChange = false;
var fileNameArr = this.filepath.split('\\');
hmd.msg('文件:' + fileNameArr[fileNameArr.length - 1] + '保存成功!');
this.fire('save');
}
else{
this.saveAs();
}
},
events: {},
fire: function (eventName, obj) {
var me = this;
this.events[eventName] && this.events[eventName].forEach(function (fn) {
fn.call(me,obj);
});
},
on: function (eventName, fn) {
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(fn);
}
};

我们将编辑器的实现封装在hmd.editor这个对象上.

编辑器的模式设置为GFM.

实现directive

directives.js

1
2
3
4
5
6
7
8
var studio = hmd.studio;

studio.directive('hmdEditor', function () {
return function ($scope, elem) {
//第二个参数为测试用的本地md文件,因为选择文件的功能还没实现.你可以改成你电脑上的文件.
hmd.editor.init({el:elem[0]},'E:\\Temp\\test\\test.md');
};
});

定义了'hmd-editor,用于绑定hmd.editor的调用.
在视图模版里调用hmd-deitor

1
2
3
<div class="content studio-wrap">
<textarea name="" cols="30" rows="10" hmd-editor></textarea>
</div>

刷新应用,可以看到textarea已经变成markdown编辑器,按ctrl+s保存会有简单的提示.

最终效果

总结

到目前为止,只是搭建了开发环境,实现了基础的编辑器功能,还完全不能真正的使用.
接下来几篇暂定计划是:

  • 打开文件,保存新文件,系统设置等基本功能.
  • 自动更新功能.
  • 实时预览窗口.
  • 自动上传图片.
  • 表情功能.
  • 集成hexo命令.

附件

本篇结果打包
github项目地址

自己动手制作更好用的markdown编辑器-01的更多相关文章

  1. 自己动手制作更好用的markdown编辑器-03

    这里文章都是从个人的github博客直接复制过来的,排版可能有点乱. 原始地址 http://benq.im/2015/04/24/hexomd-03/ 文章目录 1. 系统模块 2. 记录上次打开的 ...

  2. 自己动手制作更好用的markdown编辑器-02

    这里文章都是从个人的github博客直接复制过来的,排版可能有点乱. 原始地址 http://benq.im 文章目录 1. 工具条 1.1. 样式 1.2. 工具条截图 2. 状态栏消息 3. 文件 ...

  3. 自己动手开发更好用的markdown编辑器-04(实时预览)

    这里文章都是从个人的github博客直接复制过来的,排版可能有点乱. 原始地址 http://benq.im/2015/04/25/hexomd-04/   程序打包   文章目录 1. 打开新窗口 ...

  4. 自己动手开发更好用的markdown编辑器-07(扩展语法)

    这里文章都是从个人的github博客直接复制过来的,排版可能有点乱. 原始地址 http://benq.im/2015/05/19/hexomd-07/   文章目录 1. 准备工作 2. 目录语法 ...

  5. 自己动手开发更好用的markdown编辑器-06(自动更新)

    这里文章都是从个人的github博客直接复制过来的,排版可能有点乱. 原始地址 http://benq.im/2015/05/12/hexomd-06/   文章目录 1. 自动更新方案 2. 实现 ...

  6. 自己动手开发更好用的markdown编辑器-05(粘贴上传图片)

    这里文章都是从个人的github博客直接复制过来的,排版可能有点乱. 原始地址 http://benq.im/2015/04/28/hexomd-05/   文章目录 1. 七牛云存储 1.1. 系统 ...

  7. 任由文字肆意流淌,更自由的开源 Markdown 编辑器

    对于创作平台来说内容编辑器是十分重要的功能,强大的编辑器可以让创作者专注于创作"笔"下生花.而最好取悦程序员创作者的方法之一就是支持 Markdown 写作,因为大多数程序员都是用 ...

  8. 市面上有没有靠谱的PM2.5检测仪?如何自己动手制作PM2.5检测仪

     市面上能买到的11中常见的pm2.5检测仪 网上大佬实测并不是很准,我这里没测过(全买下来有点贵,贫穷限制了我的想象力) 这些检测仪多数是复合式.多功能的空气质量检测仪.具体就不一一介绍了.这篇文章 ...

  9. 更轻便的markdown 编辑器Typora

    更轻便的markdown 编辑器 Typora 所见即所得的键入方式 https://typora.io 文章来源:刘俊涛的博客 欢迎关注,有问题一起学习欢迎留言.评论.

随机推荐

  1. Codeforces 891B - Gluttony

    891B - Gluttony 题意 给出一个数字集合 \(a\),要求构造一个数组 \(b\) 为 \(a\) 的某个排列,且满足对于所有下标集合的子集 \(S=\{x_1,x_2,...,x_k\ ...

  2. Trie树&kmp&AC自动机&后缀数组&Manacher

    Trie 计数+Trie,读清题意很重要 https://vjudge.net/problem/UVALive-5913 kmp AC自动机 模板:https://vjudge.net/problem ...

  3. Codeforces Round #394 (Div. 2) E. Dasha and Puzzle(分形)

    E. Dasha and Puzzle time limit per test 2 seconds memory limit per test 256 megabytes input standard ...

  4. jvm内存参数配置

    qunar国内旗舰店master  (4核 8G) qunar国内旗舰店hub(4核 8G) qunar国内旗舰店provider(4核 8G)

  5. WebLogic Cluster Sevlet的配置

    虽然生产环境中不建议使用,但因为客户需要考试可能用到,所以又做了一遍 1. 配置受管Server,ProxyServer,过程略 2.构建Proxy Application 建立一个ProxyApp的 ...

  6. javascript 的事件绑定和取消事件

    研究fabricjs中发现,它提供canvas.on('mousemove', hh) 来绑定事件, 提供 canvas.off()来取消绑定事件这样的接口,很是方便, 那我们就不妨探究一下内在的实现 ...

  7. 转:java 进阶之路

    转: https://www.zhihu.com/question/39139518 一.基础篇1.1 JVM1.1.1. Java内存模型,Java内存管理,Java堆和栈,垃圾回收 http:// ...

  8. 倍福TwinCAT(贝福Beckhoff)应用教程12.3 TwinCAT控制松下伺服 NC进阶

    在前面一节,我们简单介绍了通过PLC+HMI实现完整控制松下伺服的上使能-运动,采集位置,速度等功能,这里我们会大量简化用到的贝福功能块(为了更加实用).首先依然是对单个轴的封装,我们之前的做法,例如 ...

  9. $HTTP_RAW_POST_DATA 与$_POST

    出处:http://blog.163.com/gwo-cce@126/blog/static/325736492008101142422345/ 这是手册里写的 总是产生变量包含有原始的 POST 数 ...

  10. Selenium webdriver Java 查找元素

    1.简单查找 By ID: WebElement element=driver.findElement(By.id("userId")); By Name:WebElement e ...