這里文章都是從個人的github博客直接復制過來的,排版可能有點亂. 原始地址
http://benq.im/2015/04/25/hexomd-04/
上一篇我們實現了系統模塊的一些功能,對angular
的使用更深入了一點.
今天這篇我們要實現實時預覽
的功能,將學習到如何使用nw.js
打開額外新窗口,窗口之間如何通信,並將引入新的開源框架marked,用於markdown的解析.
打開新窗口
預覽的功能我將在編輯器之外的新窗口里實現,因為我平常都習慣使用雙顯示器,這樣能把預覽放在另一個顯示器.
先在studio/views
里新增preview.html,作為預覽的窗口頁面.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>預覽</title> </head> <body> <article class="markdown-body" id="content"> </article> <script src="../../../lib/jquery-2.1.3.js"></script> <script src="../preview.js"></script> </body> </html> |
studio/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 |
studio.directive('studioPreview',function(){ return function($scope,elem){ $(elem[0]).on('click',function(){ var previewWinUrl = ('file:///' + require('path').dirname(process.execPath) + '/app/modules/studio/views/preview.html').replace(/\\/g,'/'); if (!hmd.previewWin) { hmd.previewWin = require('nw.gui').Window.open(previewWinUrl, { position: 'center', "toolbar": true, "frame": true, "width": 800, "height": 600, "min_width": 600, "min_height": 400, "icon": "app/img/logo.png" }); hmd.previewWin.on('close', function () { hmd.previewWin = null; this.close(true); }); } }); }; }); |
預覽窗口每次只能打開一個,所以打開之前會先判斷hmd.previewWin
是否已存在,並且窗口關閉事件里將hmd.previewWin
置空.
studio/views/studio.html
里綁定預覽按鈕
1 2 3 |
... <a studio-preview href="javascript://" class="btn btn-primary" title="預覽"><i class="glyphicon glyphicon-eye-open"></i></a> ... |
這樣就實現了點擊預覽按鈕打開預覽窗口

預覽功能
markdown
的解析我使用開源的marked.
安裝marked
打開命令行,進入app
目錄,輸入安裝命令:

為editor.js增加markdown解析的方法,輸出當前編輯器內容解析后的結果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
init: function (options,filepath) { ... this.initMarked(); this.cm = CodeMirror.fromTextArea(el, options); ... },
initMarked:function(){ this.marked = require('../app/node_modules/marked'); this.marked.setOptions({ renderer: new this.marked.Renderer(), gfm: true, tables: true, breaks: false, pedantic: false, sanitize: true, smartLists: true, smartypants: false }); },
parse:function(){ return this.marked(this.cm.getValue()); }, |
這里要注意的是this.marked = require('../app/node_modules/marked');
,而不是直接require('marked')
,這是因為nw.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 |
studio.directive('studioPreview',function(){ return function($scope,elem){
var changeTimer; hmd.editor.on('change',function(){ clearTimeout(changeTimer); changeTimer = setTimeout(function(){ hmd.previewWin && hmd.previewWin.emit('change', hmd.editor.parse()); },200); }); hmd.editor.on('setFiled',function(filepath){ hmd.previewWin && hmd.previewWin.emit('change', hmd.editor.parse()); });
$(elem[0]).on('click',function(){ hmd.previewWin.on('loaded',function(){ hmd.previewWin && hmd.previewWin.emit('change', hmd.editor.parse()); }); hmd.previewWin.on('close', function () { hmd.previewWin = null; this.close(true); }); } }); }; }); |
我們通過自定義事件emit('change', hmd.editor.parse())
來與previewWin
窗口通訊. 在初始化窗口,打開文件,修改文件時都觸發窗口的change事件,將解析后的內容作為事件參數傳遞.
新建腳本文件studio/preview.js
,並在preview.html
里引用
1 2 3 4 |
var gui = require('nw.gui'), win = gui.Window.get(); win.on('change', function (mdHtml) { $('#content').html(mdHtml); }); |
preview.js里監聽change
事件,然后將解析后的內容直接顯示到頁面上.

優化體驗
現在已經可以實時的預覽了,但功能還是過於簡單,使用起來很不方便,這一節將優化預覽窗口的使用體驗.
滾動條隨動
如果文本太多導致出現滾動條,預覽窗口還是會一直顯示在第一屏,並不會跟隨我們在編輯器中的查看位置來實時的更新預覽的位置.我們要看預覽還要手動去調整預覽窗口的滾動條高度,這樣的體驗完全等於沒法使用,因此現在來實現預覽窗口隨着編輯器的滾動條高度等比隨動.
codemirror已經實現了scroll事件,節省了我們大量的工作量,這個框架的作者考慮的真是周到,不得不贊一下.
我們在editor.js對scroll
事件進行封裝.
1 2 3 4 |
//滾動事件 this.cm.on('scroll',function(cm){ me.fire('scroll',cm.getScrollInfo()); }); |
在directive
里將編輯器滾動事件傳遞給預覽窗口
1 2 3 4 5 6 7 8 9 10 11 12 |
studio.directive('studioPreview',function(){ ... var scrollTimer; hmd.editor.on('scroll',function(scrollInfo){ clearTimeout(scrollTimer); scrollTimer = setTimeout(function(){ hmd.previewWin && hmd.previewWin.emit('editorScroll',scrollInfo); },200); }); ... } |
同樣的道理,我們應該防止太頻繁的觸發
最后在preview.js
里響應editorScroll
事件,並更新預覽頁面的滾動條高度
1 2 3 4 |
win.on('editorScroll',function(scrollInfo){ var scrollTop = $(document.body).height()*scrollInfo.top/scrollInfo.height; $(document.body).scrollTop(scrollTop); }); |
樣式美化
默認的無樣式界面看起來太不舒服了,現在來實現跟編輯器一樣的可以選擇或者自定義的樣式.
我們將預覽的樣式放在/app/css/previewtheme
目錄下,先在里面增加兩個測試用的樣式文件

增加預覽樣式設置
這個跟上一篇的編輯器樣式設置類似.
system/model.js
增加默認配置
1 2 3 4 5 6 7 8 9 |
var defaultSystemData = { lastFile: null, theme:'ambiance', preViewTheme:'default' }; |
system/views/system.html
增加表單字段
1 2 3 4 5 6 7 8 9 10 11 |
<div class="content studio-wrap"> <form class="system-form" name="systemForm"> ... <div class="form-group"> <label>預覽樣式</label> <select name="preViewTheme" ng-model="systemSetting.preViewTheme" ng-options="k as v for (k, v) in preViewThemes"> </select> </div> ... </form> </div> |
system/controllers.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var system = hmd.system, fs = require('fs');
var readCssList = function(path){ var files = fs.readdirSync(path),themes={}; files.forEach(function (file) { if(~file.indexOf('.css')){ file = file.replace('.css',''); themes[file] = file; } }); return themes; }; system.controller('system', function ($scope) { $scope.themes = readCssList('./app/lib/codemirror/theme'); $scope.preViewThemes = readCssList('./app/css/previewtheme'); $scope.systemSetting = system.get(); $scope.save = function (systemSetting) { system.save(systemSetting); }; }); |
將讀取目錄所有樣式文件生成鍵值對的代碼封裝成方法readCssList
,然后增加$scope.preViewThemes
綁定即可..

再一次感受angular的方便.
應用樣式
預覽頁面加載成功后,通過事件setTheme
將系統設置傳遞給預覽窗口
1 2 3 4 5 6 7 8 |
studio.directive('studioPreview',function(){ ... hmd.previewWin.on('loaded',function(){ hmd.previewWin.emit('setTheme',hmd.system.get()); hmd.previewWin && hmd.previewWin.emit('change', hmd.editor.parse()); }); ... }); |
preview.js
1 2 3 |
win.on('setTheme',function(setting){ $('head').append('<link href="../../../css/previewtheme/'+setting.preViewTheme+'.css" rel="stylesheet" />'); }); |
從網上找幾個常用的marddown樣式文件來看看效果,你可以自己找或寫更多樣式.



代碼塊高亮
作為一個碼農,寫的markdown文件里都有好多代碼塊,肯定要把代碼塊弄好看點.
安裝highlight.js
1 |
npm install highlight.js |

安裝完成后,代碼高亮的樣式文件在目錄node_modules/highlight.js/styles/
在系統設置里增加預覽代碼樣式設置,跟之前的預覽樣式類似,這里直接上代碼,不再重復描述了.
model.js
1 2 3 4 5 6 7 8 9 10 11 |
var defaultSystemData = { lastFile: null, theme:'ambiance', preViewTheme:'github', preViewHighLightTheme:'default' }; |
system.html
1 2 3 4 5 6 7 |
... <div class="form-group"> <label>代碼預覽樣式</label> <select name="preViewHighLightTheme" ng-model="systemSetting.preViewHighLightTheme" ng-options="k as v for (k, v) in preViewHighLightThemes"> </select> </div> ... |
controllers.js
1 2 3 4 5 6 7 8 9 |
system.controller('system', function ($scope) { $scope.themes = readCssList('./app/lib/codemirror/theme'); $scope.preViewThemes = readCssList('./app/css/previewtheme'); $scope.preViewHighLightThemes = readCssList('./app/node_modules/highlight.js/styles'); $scope.systemSetting = system.get(); $scope.save = function (systemSetting) { system.save(systemSetting); }; }); |
系統設置截圖

preview.js
1 2 3 4 |
win.on('setTheme',function(setting){ $('head').append('<link href="../../../node_modules/highlight.js/styles/' + setting.preViewHighLightTheme +'.css" rel="stylesheet" />'); $('head').append('<link href="../../../css/previewtheme/'+setting.preViewTheme+'.css" rel="stylesheet" />'); }); |
這樣就完成了,很簡單,沒幾行代碼.
關閉主程序前先自動關閉預覽窗口
現在還有個小問題,主程序關掉后,預覽窗口還在.
modules/directives.js
1 2 3 4 5 6 7 |
... win.on('close', function () { var me = this; hmd.previewWin && hmd.previewWin.close(); me.close(true); }); ... |
監聽主窗口的關閉事件,如果有預覽窗口,就先關閉預覽窗口再關閉自己
總結
現在我們的markdown編輯器應該是挺好用的了,至少比一些在線的方便些,可以很靈活的定制各種樣式.
做預覽這個功能的時候我的想法是:一個重要的功能,不僅要實現基本功能,更重要的是完善體驗,太差的體驗跟沒有這個功能沒區別,因此我們把時間都花在優化預覽的體驗上.與其增加10個不常用的功能,不如把最常用的一個功能做好.
最終效果截圖

主窗口

預覽窗口
附件
本篇程序打包
項目地址