这里文章都是从个人的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. 【Docker】基本命令使用介绍

    # docker命令行学习 ## docker run- docker run --help:老实说这条最管用了- docker run -it:交互模式,允许控制台输出 - docker run - ...

  2. CentOS通过yum安装MariaDB(MySQL)无法启动服务或者找不到mysql.sock

    转载自:http://www.linuxidc.com/Linux/2016-03/129396.htm 今天在Linux主机(CentOS 7)安装(yum install方式)Mariadb(即开 ...

  3. 【树状数组】【枚举约数】 - Ambitious Experiment

    给定一个序列,支持以下操作: 对区间[l,r]的每个i,将1i,2i,3i,...这些位置的数都加d. 询问某个位置的数的值. 如果把修改看作对区间[l,r]的每个数+d,那么询问x位置上的数时,显然 ...

  4. 【二分答案】【最大流】bzoj3130 [Sdoi2013]费用流

    二分最大的边的cap,记作Lim. 把所有的边的cap设为min(Lim,cap[i]). Bob一定会把单位费用加到最大边上. #include<cstdio> #include< ...

  5. 通过python的logging模块输出日志文件

    import logging import sys #获取logger实例 logger = logging.getLogger("baseSpider") # 括号后面填运行的文 ...

  6. 1.7(学习笔记)过滤器(Fliter)

    一.过滤器(Fliter)简介 过滤器是位于客户端与服务器之间的滤网,在访问资源时会经过一系列的过滤器, 满足条件则放行,不满足条件的将其拦截. 过滤器可以看做是一个特殊的Servlet,设置了过滤器 ...

  7. Java多线程——ReentrantReadWriteLock源码阅读

    之前讲了<AQS源码阅读>和<ReentrantLock源码阅读>,本次将延续阅读下ReentrantReadWriteLock,建议没看过之前两篇文章的,先大概了解下,有些内 ...

  8. Scala高手实战****第20课:Scala提取器、注解深度实战详解及Spark源码鉴赏

    Spark中的源码的提取器和注解 @SparkContext.scala @ volatile 线程专用 保证线程间共享内容的一致性 @volatile private var _dagSchedul ...

  9. Scala零基础教学【61-80】

    第61讲:Scala中隐式参数与隐式转换的联合使用实战详解及其在Spark中的应用源码解析 第62讲:Scala中上下文界定内幕中的隐式参数与隐式参数的实战详解及其在Spark中的应用源码解析 /** ...

  10. MySQL查询时区分大小写(转)

    说明:在MySQL查询时要区分大小写会涉及到两个概念character set和collation,这两个概念在表设计时或者在查询时都可以指定的,详细参考:http://www.cnblogs.com ...