本文来自作者 孙亖 在 GitChat 上分享 「使用 Angular2+ 开发 Markdown 编辑器」
编辑 | 弗利萨
前言
一直想写一个 Angular2+ 的分享,但是没有一个好的切入点。前段时间我向分享 Chat 的白宦成老师请教 Markdown 的问题,他向我推荐 Typora 编辑器,我觉着这就是我一直想要的 Markdown 编辑器,于是我就想到了这个主题。
当然,我的水平一时是难于写出 Typora 那样的编辑器的,但人生已经如此艰难,大家就不要拆穿了,我主要是想通过一个实际应用分享一下 Angular(文中提到的 Angular 指的都是Angular2+,实际版本是 Angular5)的开发过程,主要内容包括:
Angular 项目建立
Angular 中的服务
Angular 中使用第三方传统库
打包桌面版本
制作一个安装程序
Angular 项目的默认语言是 TypeScript,如果你不太熟悉请看我的另外一个Chat:TypeScript 快速入门
说了一大堆,我们是要使用 Angular 来开发一个新的项目,如果你以前没安装过,那么你需要先安装 Node 和 npm。然后使用下面的命令来安装 Angular/Cli,我们是通过 Angular/Cli 进行项目的管理。
npm install -g @angular/cli
如果你已经安装过老版本的 Angular/Cli,建议你使用下面的命令,升级到最新的版本。
npm uninstall -g @angular/cli npm cache clean # if npm version is > 5 then use `npm cache verify` to avoid errors (or to avoid using --force) npm install -g @angular/cli@latest
升级过程中可能会报错误,类似于:
npm WARN [email protected] requires a peer of ajv@^6.0.0 but none is installed. You must install peer dependencies your
self.npm ERR! code EINTEGRITY
npm ERR! sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= integrity checksum failed when using sha1: wanted sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= but got sha1-D6HriBx1DgZRGWjwAGT4GuPfJE4=. (988 bytes)npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\sunjipeng\AppData\Roaming\npm-cache_logs\2018-02-21T06_26_16_216Z-debug.log
我们可以手动删除解决:
删除用户目录下./npmrc。
删除 Roaming 目录下 npm 和 npm_cache。
接下来我们要用下面的命令,新建一个项目:
ng new ngMDEditor
新建项目后,有可能因为大局域网的原因,部分依赖未成功安装,可以通过 npm i 完成安装,安装之后,可以通过如下命令,运行预览调试:
ng serve
首先,我是一个前端渣渣;其次,开源世界有很多现成的东西供我们使用,不需要我们再花精力在我们不熟悉的领域。
现在我们依赖别人,以后依赖人工智能,最后会不会替代?
要做一个 Markdown 编辑器,首先需要解析 Markdown,根据 Github 上 most stars 排序,JavaScript 类的 marked 排第一,网上用的人也比较多,但是我喜欢另外一个 markdown-it,主观感觉特性丰富,而且插件比较多,有 MathJax 的插件,挺好用的。
我们先安装 markdown-it,使用 npm 命令如下:
npm install markdown-it --save
因为 Angular 默认使用 Typescript,通常我们需要编写声明文件告诉 TypeScript 我们的 JavaScript 库的存在,很多库已经有现成的声明文件,所以,使用 npm 安装一下:
npm install --save @types/markdown-it
另外,我需要界面布局,前面说了,我是前端渣渣,所以我常常使用其他工具来布局和写界面,我用得最多的就是 Bootstrap。
虽然这很不 Angular,还非常的 jQuery,但不得不承认如今这几种框架混用的现象是非常普遍的。我们先安装 Bootstrap,同样使用 npm 命令:
npm install bootstrap
由于 Bootstrap 还依赖 jQuery 和 popper.js,所以我们用同样的方法安装这两个:
npm i jquery popper.js
我们在根目录下,打开 .angular-cli.json,编辑 scripts 项如下:
"scripts": [ "../node_modules/markdown-it/dist/markdown-it.min.js", "../node_modules/jquery/dist/jquery.min.js", "../node_modules/popper.js/dist/umd/popper.min.js", "../node_modules/bootstrap/dist/js/bootstrap.min.js" ],
这样就算把这些第三方库加入到框架中了,注意那个 poper 使用的是 umd 库,用错了不行,我们也可以在 index.html 中使用传统方式引入,但这样更专业一些,Bootstrap 还包含了样式,我们修改上述文件的 styles 项如下:
"styles": [ "styles.css", "../node_modules/bootstrap/dist/css/bootstrap.min.css" ],
类似的,我们可以在 styles.css 中引入项目文件,例如:
@import "~bootstrap/dist/css/bootstrap.min.css";
接下来,我们就正式开始编写一个 Markdown 语法的编辑器,借用流行的左右两栏模式,左边是 Markdown 语法,右边是解析渲染后的样子。我们用 Angular/Cli 命令行工具生成一个 component,也就是这个编辑器:
ng g c page/Editor
上面 ng 就是 Angular/Cli 的命令行工具,参数 g 是 generator 的简写,c 是 component 的简写,合起来意思就是生成一个组件,最后的参数就是组件的名称和位置(相对于源码)。好了,这是自动生成四个文件:
editor.component.css
editor.component.html
editor.component.spec.ts
editor.component.ts
看后缀我们知道 css 是样式,html 是页面,ts 是业务逻辑。我们先来把页面写出来:
<div class="container d-flex h-100 p-3 mx-auto flex-column"> <div class="row h-100"> <div class="col-6"> <textarea class="h-100 w-100">这是左边textarea> div> <div class="col-6"> <p> 这是右边 p> div> div>div>
我不想详细讲太多 HTML 页面知识,总之就是左边一个文本输入框,右边在 p 元素内将页面渲染出来。
我们要把文本区域的内容获取,传统的做法就是通过 getElementById 等方法获取元素,然后根据不同元素的属性获取值,大多数时候使用 getElmentById(id).getValue()。
但是,我们现在不是传统做法,用的是 Angular,可以使用双向绑定。
双向绑定说白了就是元素值和业务代码里的某个变量或方法是双向流通的,详细的内容可以参考官方文档,但常用的方法我要说一下,方括号 [] 就是代码值传到页面,圆括号 () 就是页面值传到代码,事件就是页面传到代码。
当只有一种括号时是单向绑定,两种括号一起用时就是双向绑定。例如:
<input type="text" [(value)]="srcTxt"/>
对于 form 控件,更常用的是 ngModel,使用 ngModel 前我们需要在 module 中引入 FormsModule:
imports: [ BrowserModule, FormsModule ],
基于我们前面的需求,现在代码中新增一个属性 srcTxt 保存文本区域输入的 Markdown 代码:
srcTxt:string = "源文件"
然后,在页面上通过 ngModel 绑定此属性:
<textarea class="h-100 w-100" [(ngModel)]="srcTxt">这是左边textarea>
我们现在通过 srcTxt 获取了 Markdown 源码,接下来我们通过 Markdown-it 解析出结果并显示出来。
前面我们已经添加了 Markdown-it 的类型库,现在我们引入它就可以使用:
import * as MarkdownIt from 'markdown-it'
当 Markdown 源码改变时,我们需要重新解析源码,因此在页面添加监听 ngModel 的改变:
<textarea class="h-100 w-100" [(ngModel)]="srcTxt" (ngModelChange)="srcChanged($event)">这是左边textarea>
在逻辑代码中实现这个监听,解析源码为 HTML 格式:
srcChanged($event) { var md = new MarkdownIt(); this.outTxt = md.render($event) }
这里的 outTxt 是我们定义的一个结果属性,通过页面将这个结果展现出去,页面就完成了。
<div [innerHTML]="outTxt">div>
另外一种方式是使用 pipe,Angular 里面 pipe 的目的就是将输入的值转换为目标值,比如时间、货币、数字的格式化等。我们先写一个 Markdown 解析转换的 pipe,先生成一个:
ng g pipe pipe/Md
Angular 中内置了一些 pipe,也可以自定义 pipe,我们修改生成的 pipe,这里的代码很简单:
transform(value: any, args?: any): any { var md = new MarkdownIt(); return md.render(value); }
相应的,界面上需要根据 pipe 的用法改变:
<div class="col-6"> <span> <div [innerHTML]="srcTxt | md">div> span>div>
首先,我们从这里下载一个 MathJax 的库,本来 Markdown-it 有 MathJax 的扩展,但是使用起来也有各种问题,就只好直接使用 MathJax 官网库,但编译后提示有文件找不到,最后就找了这么一个单文件库,总算可以了。直接加载文件是这样配置 .angular-cli.json 的:
"scripts": [ ... "./assets/MathJax.min.js" ],
导入库后需要配置,由于我们不是用 npm 导入的,也没有类型库,所以我们先定义以下,然后再配置:
declare var MathJax:any ngOnInit() { MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]}}); }
这次我们编写一个 directive 来实现这个功能,本来我打算通过 pipe,或者监听文本变化用 MathJax 解析公式,但是由于 MathJax 解析方法不太好实现,所以我们用 directive:
ng g directive directive/MathJax
directive 代码如下:
import { Directive, Input, ElementRef } from '@angular/core';declare var MathJax:any@Directive({ selector: '[MathJax]'})export class MathJaxDirective { @Input('MathJax') fractionString: string; constructor(private el: ElementRef) { } ngOnChanges() { this.el.nativeElement.innerHTML = this.fractionString; MathJax.Hub.Queue(["Typeset", MathJax.Hub, this.el.nativeElement]); } }
最后,我们在标签上使用 directive 来处理公式:
<div [MathJax]="outTxt">div>
这是我们输出公式就可以正常显示了。
这一节我们主要是想讲 Angular 使用 HttpClient 与远程服务器通讯的功能,这是技术上的,从功能上我们还可以将各种功能进行设计,在 Angular 中可以将它们抽象成服务,新建一个服务如下:
ng g s service/Md
通过上面的命令,我们在 Service 目录下新建了一个 Md 服务,我们要在这个服务中,添加一个将 Markdown 文档保存到服务器的功能。
Angular 通过 HttpClient 来和远程服务通讯,HttpClient 也是一个服务,要是用这个服务我们首先要在 app.module.ts 中导入 HttpClientModule,app.module.ts 变成了如下内容:
@NgModule({ declarations: [ ... ], imports: [ ... HttpClientModule, ... ], providers: [], bootstrap: [AppComponent] })export class AppModule { }
然后,我们将 HttpClient 注入到要使用的地方,这里具体就是 Md 服务:
import { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http'; @Injectable()export class MdService { constructor(private http:HttpClient) { } }
现在,我们就可以通过 HttpClient 来保存数据了,新建一个服务方法:
save(id:number, title:string, content:string) { return this.http.post("http://localhost:8080/post", {id: id, title:title, content:content}) }
有了服务之后,我们需要在界面中调用它,首先,需要在 app.module.ts 的 providers 中定义:
@NgModule({ declarations: [ AppComponent, EditorComponent, MdPipe ], imports: [ BrowserModule, FormsModule, HttpClientModule ], providers: [MdService], bootstrap: [AppComponent] }) export class AppModule { }
同样和 HttpClient 一样,我们需要在使用的地方注入它,我们在 Editor 页面新建一个按钮事件:
<div class="row"> <button type="button" class="btn btn-primary" (click)="doSave()">保存button> div> <div class="row"> <input type="text" [(ngModel)]="title"> div>
按钮点击通过圆括号绑定到 doSave 方法,doSave 方法通过注入的 Md 服务保存内容到服务器:
constructor(private mdSvc:MdService) { } doSave() { this.mdSvc.save(null, this.title, this.srcTxt).subscribe(data=> { alert("保存成功") }, err => { alert("保存失败") }) }
我这里仅演示一下,服务使用的是我前面的 Chat:Kotlin开发SpringBoot之Data JPA,有请兴趣的朋友可以阅读该 Chat,另外一个编辑器的功能不单是保存,还有打开等功能,就留作大家的作业吧。
现在前端的发展其实是很快的,网页、桌面、手机一网打尽了,Angular 的主页写的就是一种框架,适于多个平台。
把 JavaScript 项目打包成桌面,现在主流有两种方案 NW.js 和 Electron,我查找学习的资料大多是 Electron,所以这里我们来看看如何用 Electron 把 Angular 项目打包成桌面应用。
Electron 打包桌面应用说白了就是网页套个壳,高级点可以提供 API 访问本地原生功能,基于此,我们甚至可以给一个网站做个桌面应用,我们先初始化一个 npm 的空项目:
扫描下方二维码
阅读完整原文