这里文章都是从个人的github博客直接复制过来的,排版可能有点乱. 原始地址  
http://benq.im 
 
前段时间用hexo重新搭了个人博客,顺便写了个简单的博客搭建教程.
用markdown写起博客流畅很多,但是用了几个markdown编辑器,都没有一个适合自己使用的。于是就想自己动手做一个,当然不是完全从0开始做,语法高亮和markdown解析都用的是开源的项目.
从这篇开始,我会把整个开发过程记录成系列随笔,因此开发进度较为缓慢.
博客写得少,像这样写长一点的随笔就有点混乱,看不懂的请用力喷,我会努力改进.
简介
先介绍下开发过程中用到的一些比较重要的开源项目:
- nw.js,原名node-webkit,用webkit和node来做基于web技术的跨平台客户端软件.
 
- CodeMirror,基于web技术实现的文本编辑器,实现了大部分的IDE功能以及几乎全部你会用到的语言的支持.目前我日常开发都是用这个IDE,甚至在做hexomd这个项目时用的IDE也是CodeMirror做的.
 
- angularjs,google的mvvm开发框架,这个相信不用我多做介绍.我用的不熟,觉得好用就拿来即用,没有深入的了解过.
 
关于这些开源项目的使用,我在这系列文章里不会详细解释,如果有疑问,可以去看官网的入门教程和wiki,当然也欢迎讨论.
项目结构

图片里的是我目前的项目结构,大概讲解一下一些目录和文件的用途。
- 
icudtl.dat,nw.exe,nw.pak
这3个是nw.js在windows运行所必须的文件.
 
- 
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",      "toolbar": true,      "frame": false,      "width": 1000,      "height": 700,      "position": "center",      "min_width": 600,      "min_height": 400    } }
 | 
 
- 
app目录
程序的所有源代码的根目录.
 
- 
app/lib
存放angular,jquery,codemirror等开源库/框架的源代码
 
- 
app/helpers
存放一些node的工具函数
 
- 
app/modules
程序代码在这个目录,按功能模块分成不同的子目录.
modules/app.js是整个程序的入口点
 
- 
app/package.json
node模块配置,注意与上层的package.json意义不同
 
- 
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>      <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.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) {                     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>    <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>    <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);          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) {          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项目地址