之前我写过一篇文章 《打造一个优雅的微信文章编辑器》,那时候是直接 fork 大神小胡子哥 的线上排版编辑器过来揣摩了一番,顺便改了点样式,加了一个代码主题色 Material Dark,就上线了。实际上,项目的代码和体验一直我都感觉挺别扭的,便彻底重构了一番。

新版访问地址:md.ironmaxi.com

新版界面:

操作效果:

大体介绍一下,我在原项目的基础上做了什么工作:

  • 添加 webpack 配置,支持本地调试;
  • 引入 Vue,虽然没必要,但是有了数据双向绑定,代码写起来简洁,维护起来方便;
  • 添加实时预览功能,左侧写出来的 Markdown 文字,右侧立即预览,没有延迟;
  • 左侧与右侧视图同步滚动,进一步提升使用体验;
  • 点击复制内容,所有文字及排版样式统统拷贝进剪贴板,不用再多按一次 crtl + c
  • 根据微信公众编辑器的样式及限制,多做了一些兼容;
  • 增加了 3 种不同样式的 blockqoute;
  • 站点升级 https 协议;
  • 重磅:Service Worker 加持,只要访问过一次线上地址,那么静态资源都会被缓存,离线可用!
  • 重磅:Gitlab CI/CD 加持,我只要往 master 主干提交代码,项目便可以自动打包构建,并部署到我的个人服务器中,而且还能自动帮我 push 到 github 仓库中,省去了我人工操作的步骤,非常优雅,这个后面详细说!

接下来,我详细介绍一下完成这个项目的大致步骤与思路。具体的项目代码,大家可以访问 github 仓库。如果这款工具好用、解决了你的痛点,请给仓库一个 Star⭐️!

1. 项目结构

.
├── .babelrc
├── .gitignore
├── .gitlab-ci.yml  // CI/CD 配置文件
├── LICENSE
├── README.md
├── build  // 存放 webpack 配置文件
├── md  // 构建输出目标文件夹
├── node_modules
├── package-lock.json
├── package.json
├── service-worker-plugin.js  // service worker 应用插件
├── src  // 源文件夹
└── sw-register.js  // 注册 service worker 的脚本文件5 directories
复制代码

只要重点关注一下如下文件或文件夹即可:

  • .gitlab-ci.yml
  • service-worker-plugin.js
  • sw-register.js
  • build
  • src

2. 核心功能

在这个编辑器中,最核心的功能只有两个:

  1. 将 markdown 转为 html;
  2. 给不同语言的代码设置高亮。

2.1 转 markdown 为 html

我们要引入第三方库:showdownjs/showdown

使用很简单,看看官方 demo:

// converter.js
var showdown  = require('showdown'),converter = new showdown.Converter(),text      = '# hello, markdown!',html      = converter.makeHtml(text);// output
//  <h1 id="hellomarkdown">hello, markdown!</h1>
复制代码

还支持自己写插件,插件格式有两种:

  • 解析自定义 markdown 语法
  • 自定义修改 markdown 转为 html 的结果

举个例子:

// showdown-myExtension.js
import showdown from 'showdown';showdown.extension('myExtension', function () {return [// 格式 1:解析自定义 markdown 语法{type: 'language',filter (source) {source = source.replace(/```!([\s\S]*?)```/, function (match, content) {return '<blockquote class="danger">' + content + '</blockquote>'});// 继续解析其他自定义的 markdown 语法return source; }},// 格式 2:自定义修改 markdown 转为 html 的结果{type: 'output',filter: function (source) {source = source.replace(/<pre([^>]*)>([\s\S]*?)<\/pre>/gi, function (match, preClass, content) {console.log(arguments);return '<pre '+ preClass +'><section class="pre-content">'+ content +'</section></pre>';});// 继续自定义修改 markdown 转为 html 的结果return source;}}];
});// converter.js
import showdown from 'showdown';
import './showdown-plugins/output-prettify';
import './showdown-plugins/language-blockquote';const converter = new showdown.Converter({// 扩展extensions: ['myExtension'],
});
export default converter;
复制代码

我们创建 src/plugins/converter.js,并引入 showdown.js

// 引入 showdown.js
import showdown from 'showdown';
// 引入自定义 showdown 的插件
import './showdown-prettify';// 实例化 showdown.Converter
const converter = new showdown.Converter({// 扩展extensions: ['prettify', 'widget-blockquote-warn'],parseImgDimensions: true,strikethrough: true,tables: true,tasklists: true,emoji: true,
});converter.setFlavor('github');export default converter;
复制代码

我们在 src/views/App.vue 中:

<template><div class="view-app"><!-- ... --><div class="markdowner-wrapper"><!-- 编辑框 START --><div class="input-wrapper"><textarea id="input" ref="input" spellcheck="false" v-model="editorContent"placeholder="即刻,在这里写下你的 markdown 格式文章 ..."></textarea></div><!-- 预览框 START --><div class="output-wrapper"><div id="output" ref="output" v-html="previewContent"></div></div></div></div>
</template><script>// ...import converter from '@SRC/plugins/showdown-converter';export default {// ...watch: {// 监听 textarea 的内容改动editorContent (newVal, oldVal) {this.editorContentChangedHandler(newVal);},},methods: {// 编辑器内容变化回调editorContentChangedHandler (editorContent) {this.updatePreview(editorContent);},// 更新预览视图updatePreview (editorContent) {// 核心代码this.previewContent = converter.makeHtml(editorContent);// 等待 DOM 更新完毕Vue.nextTick(() => {this.scrollHandler(this.editorElm);});},},}
</script>
复制代码

上面代码中,我将最核心的代码抽取了出来,其中,最重要的一句代码就是:

// 将 markdown 转换为 html
this.previewContent = converter.makeHtml(editorContent);
复制代码

是不是超简单?!

2.2 给不同语言的代码设置高亮

依赖的核心第三方插件就是 google/code-prettify,我给大家总结下官方推荐用法:

  1. 引入该插件:
    <script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>
  2. 查看入门文档,配置你所需要的引入 url;
  3. 查看皮肤库并选择你所喜欢的一款;
  4. 将代码写进带 prettyprint 样式名的 pre 或者 code 元素中,插件就会自动高亮代码了。

然后,在我的项目里面,是这样做的,还是在 src/views/App.vue 中:

<script>// ...import '@ASSETS/scripts/google-code-prettify/run_prettify';export default {// ...methods: {// ...// 更新预览视图updatePreview (editorContent) {this.previewContent = converter.makeHtml(editorContent);// 等待 DOM 更新完毕Vue.nextTick(() => {// 重新高亮渲染PR.prettyPrint();this.scrollHandler(this.editorElm);});},},};
</script>
复制代码

注意到,要想让 run_prettify.js 去高亮代码,必须给 precode 元素加上 prettyprint 样式名,如果还需要行号的话,还得加上 linenums 样式名。我们就借助 showdown 的插件,实现给所有转换出来的 html 中的 precode 加样式名。在 src/plugins/showdown-plugins/output-prettify.js 中:

import showdown from 'showdown';showdown.extension('output-prettify', function () {return [{type:   'output',filter: function (source) {source = source.replace(/(<pre[^>]*>)?[\n\s]?<code([^>]*)>/gi, function (match, pre, codeClass) {if (pre) {return '<pre class="prettyprint linenums" style="font-size:12px;"><code' + codeClass + ' style="font-size:12px;">';} else {return ' <code class="prettyprint code-in-text" style="font-size:12px;">';}});},}];
});
复制代码

3. 如何复制渲染后的 html

当我们点击「复制全部内容」按钮时,会将渲染后的 html 全部复制到剪贴板里面。这里我们借助的是第三方库 zenorocha/clipboard.js。

先来看看官方文档的用法:

var clipboard = new ClipboardJS('.btn');clipboard.on('success', function(e) {console.info('Action:', e.action);console.info('Text:', e.text);console.info('Trigger:', e.trigger);e.clearSelection();
});clipboard.on('error', function(e) {console.error('Action:', e.action);console.error('Trigger:', e.trigger);
});
复制代码

就是那么简单。

然后我们在 src/views/App.vue 中这么干:

<template><!-- ... --><div class="btn-group"><button class="btn copy-button" ref="clipboarddBtn"data-clipboard-action="copy" data-clipboard-target="#output">复制全部内容</button></div><!-- ... -->
</template><script>
// 剪贴板
import Clipboard from 'clipboard';// 剪贴板实例容器
let clipboard = null;// ...export default {// ...mounted () {clipboard = new Clipboard(this.$refs['clipboarddBtn']);clipboard.on('success', (e) => {this.$weui.toast('复制成功', 1000);// console.info('Action:', e.action);// console.info('Text:', e.text);// console.info('Trigger:', e.trigger);});clipboard.on('error', (e) => {this.$weui.alert('复制失败,原因请查看控制台');console.error('Action:', e.action);console.error('Trigger:', e.trigger);});},destroyed () {clipboard.destroy();}
};
</script>
复制代码

4. 如何使用 Service Worker 加持?

大家如果访问了我的线上版本:md.ironmaxi.com,那么你现在可以尝试一下,关闭网络,关闭所有浏览器;然后重新打开一个刚才访问过这个网站的浏览器,访问该域名,你会发现,照常显示,功能正常。

大家可以打开开发者工具,切换到 Network,可以看到静态资源的 Size,都是 (from ServiceWorker),这样我们就在断网的环境都能够使用。当然了,断网的环境我们也不能到微信公众平台发文,所以,最主要的目的还是让这款排版编辑器在网络差或者平常情况下,能够实现瞬间加载。

由于我们使用了 webpack 来搭建工程项目,我们就可以很方便地引入第三方的 webpack 插件:

  • lavas-project/sw-register-webpack-plugin
  • goldhand/sw-precache-webpack-plugin

这两个插件有点相辅相成的味道。玩过 Service Worker 的朋友们都知道,想要使用 Service Worker 一般都有两个步骤:

步骤 1,注册 service worker 的一段 js:

navigator.serviceWorker && navigator.serviceWorker.register('/service-worker.js').then(() => {// ...
});
复制代码

步骤2,实现 service worker 缓存策略的逻辑代码:

self.addEventListener('install', function () {// ...
});
self.addEventListener('activate', function () {// ...
});
复制代码

同时,service worker 能够给我们带来优秀缓存策略的同时,也给我们出了一个难题,如何优雅地实现更新策略

当浏览器检测到实现缓存策略文件的 service-worker.js 有更新时,第一次会进入 install 阶段,用户刷新浏览器或者关闭所有相关会话,再重新打开时,新的 service-worker.js 才会进入 activate 阶段。而且,这还是理想情况,如果浏览器对 service-worker.js 进行了缓存呢?那用户浏览器就会陷入无法获取最新应用的噩梦之中!

即使通过在服务器上显式声明对 service-worker.js 不设置缓存,也就是每次都能够获取最新的,那么还是要在第二次才能进入 activate 阶段,从而起作用。对用户来说是黑盒,如果用户一直不刷新页面呢?

这些情况太可怕了。那么到底如何优雅地实现更新策略

4.1 使用 sw-register-webpack-plugin 插件优雅地注册 service-worker

我们可以将注册 service worker 的 js 代码单独抽取出来,作为一个单独的文件 sw-register.js,我们就每次多花一个请求去请求最新的 sw-register.js,如何能够绕过 service worker 和浏览器的缓存策略,每次都拿到最新的呢?答案就是加时间戳,如下:

<script>window.onload = function () {var script = document.createElement('script');var firstScript = document.getElementsByTagName('script')[0];script.type = 'text/javascript';script.async = true;script.src = '${publicPath}/sw-register.js?_t=' + Date.now();firstScript.parentNode.insertBefore(script, firstScript);};
</script>
复制代码

当然了,以上这段代码,以及 sw-register.js 文件,sw-register-webpack-plugin 插件都帮我们做好了。我们只需要在 webpack 配置文件中直接使用:

// webpack.config.js
import SwRegisterWebpackPlugin from 'sw-register-webpack-plugin';
// ...module.exports = {plugins: [new SwRegisterWebpackPlugin({/* options */});]// ...
};
复制代码

另外,我们可以同步地翻一下该仓库提供的源码文件 sw-register.js,有这么一段代码:

navigator.serviceWorker.addEventListener('message', e => {// service-worker.js 如果更新成功会 postMessage 给页面,内容为 'sw.update'if (e.data === 'sw.update') {// ...}
});
复制代码

可以看到注释,「service-worker.js 如果更新成功会 postMessage 给页面,内容为 'sw.update'」,我们在条件判断语句中,就能做一些主动刷新页面或者提示用户应用更新的操作,通过 service-worker.js 去加载最新的资源。

接下来,如何在 sw-register.js 文件中加载最新的 service-worker.js 呢?其实我们要想,什么时候才需要加载最新的 service-worker.js?那就是在每一次构建之后!每一次构建都会有一个构建完成时间,我们故技重施,这样去请求 'service-worker.js?_buildTime=' + webpackBuildTime

来看如何去加载最新的 service-worker.js,查阅下 sw-register-webpack-plugin 提供的入口文件 index.js,其中有那么段代码:

let con = fs.readFileSync(swRegisterFilePath, 'utf-8');
let version = me.version;/* eslint-disable max-nested-callbacks */
con = babelCompiler(con).replace(/(['"])([^\s;,()]+?\.js[^'"]*)\1/g, item => {let swFilePath = RegExp.$2;if (/\.js/g.test(item)) {item = item.replace(/\?/g, '&');}// if is full url path or relative pathif (/^(http(s)?:)?\/\//.test(swFilePath) || swFilePath[0] !== '/') {// 加构建时间戳return item.replace(/\.js/g, ext => `${ext}?v=${version}`);}// if is absolute pathif (swFilePath.indexOf(publicPath) !== 0) {let ret = item.replace(swFilePath,(publicPath + '/' + swFilePath).replace(/\/{1,}/g, '/')// 加构建时间戳.replace(/\.js/g, ext => `${ext}?v=${version}`));return ret;}// 加构建时间戳return item.replace(/\.js/g, ext => `${ext}?v=${version}`);
});
复制代码

说白了就是对 sw-register.js 文件中的,所有 .js 文件路径都加上构建时间戳。

4.2 使用 sw-precache-webpack-plugin 优雅地设置缓存策略

有了注册 service worker 的脚本代码,现在来实现最后一步,使用 service worker 设置缓存策略。

也就是设置 service worker 在不同的生命周期阶段(例如:install、activate 等)如何表现,在 fetch 事件发生时,如何对资源做响应和缓存。

我们在这里借助 goldhand/sw-precache-webpack-plugin 插件,其内部帮我们对以上情况做好了一系列的通用缓存策略,剩下来的,我们只需要配置,在不同的场景下,要缓存那些静态资源或者异步请求资源。

在 webpack 配置中引入插件,并设置如下:

// webpack.config.js
// ...
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');module.exports = {// ...plugins: [new SWPrecacheWebpackPlugin({/* 配置项 */}),],
};
复制代码

这样,该插件会自动帮我们在 output.path 指定的路径下生成 service-worker.js,我们只需要将其注册即可,但这我们在上一步已经做好啦!

来看看本项目的配置项:

// webpack.config.js
// ...
module.exports = {// ...plugins: [new SWPrecacheWebpackPlugin({cacheId: 'app-cache',// 生成的文件名称filename: 'service-worker.js',// webpack生成的静态资源全部缓存mergeStaticsConfig: true,// 忽略的文件staticFileGlobsIgnorePatterns: [/\.map$/ // map文件不需要缓存],// 是否压缩,默认不压缩minify: true,// 注入的动态脚本,可以加载自定义插件importScripts: ['service-worker-plugin.js'],verbose: true,// 缓存动态资源runtimeCaching: [{urlPattern: /demo\.md/,handler: 'networkFirst'},]}),],
};
复制代码

看到了吗?我们只需要对想要缓存的资源去做配置即可,省去了一堆的缓存策略逻辑,是不是非常便捷高效?!

5. Gitlab CI/CD 加持

这篇文章到这里,已经很长了,而且超纲了很多。但我还是想记录一下,一步步优化工程的细节点,这一步骤纯属是为了加速集成发布的,关于 CI/CD 的文章,我还在筹备当中,如果读者们感兴趣的话,可以去找些入门资料来阅读一下。

我这里用上 CI/CD 有什么好处呢?首先要说一下构建、发布项目代码的痛点:

  1. 不同平台安装的 npm 包有可能是不一样的,或许明明在 mac 上打包构建是成功的,去到 windows 上居然失败了,mmp;
  2. 每次打包构建完我都要打开 Filezilla,然后手动拖一下?要是我一小时内,不断地集成快速发布呢?代码功能有回滚呢?我就要不断地命令行打包构建,鼠标触摸板拖动上传发布,不是心累二字能够形容!

然而,当我们用上了 CI/CD,可以起码做到一些什么呢?

  1. 每次保证同样的平台进行依赖安装和构建,解决了不同平台差异性导致的安装、构建、打包的隐患;
  2. 基于 git 提交,自动安装依赖、打包构建、测试检查、发布上线,全自动,解放双手,拥抱未来。

限于主题和篇幅,我贴一下该项目使用 CI/CD 配置文件,内容非常简单,也是为了能让新手看懂,入门这个东西,并不困难:

# 定义 stages
stages:
  - install_build_deploy# 定义 job
job_install_build_deploy:
  stage: install_build_deploy
  only:
    - master
  except:
    changes:
      - README.md
  script:# 打印一些相关信息
    - pwd
    - whoami# 安装依赖
    - echo "Starting job_install"
    - npm install# 打包构建
    - echo "Starting job_build"
    - npm run build# 部署
    - echo "Starting deploy"
    - sudo rm -rf /var/data/sword/md
    - sudo cp -r md /var/data/sword
复制代码

总结

其实原本实现这个排版编辑器的核心功能是很简单的,但是不断地去思考如何优化项目、优化工程,我真的是从中收获到很多。

如果本文对你有帮助,不妨给我一个喜欢❤️。

如果这个项目对你有帮助,不妨给我一个 Star⭐️。

感谢你们的阅读。


觉得本文不错的话,分享一下给小伙伴吧~

嫌微信公众号排版太丑?这里让你一步到位相关推荐

  1. php图文排版样式模版,微信公众号排版,我的妈呀,这些图文排版模板也太好看了吧!...

    原标题:微信公众号排版,我的妈呀,这些图文排版模板也太好看了吧! 胖友们大家好呀 我是135编辑器 [www.135editor.com]的三儿 上个月!还是上上个月! 不重要! 我们推送了一篇微信排 ...

  2. 轻松玩转微信公众号排版

    新手如何快速上手微信公众号排版? 有人推荐你秀米,有人推荐你壹伴等等.我在这里实名diss壹伴,我刚开通微信公众号那一天,迫不及待的想发一篇文章.我在某呼上看到有人吹壹伴甚至还有官方号自买自夸,我搞了 ...

  3. 用Typora+PicGo搞定多个平台发文和微信公众号排版

    文章目录 如何开启公众号写作新思路 1. 本文概述 1.1 适用人群 1.2 阅读完本文你可以获得什么 1.3 你需要什么 1.4 原理 2. 安装npm 3. 注册码云Gitee 3.1 新建仓库 ...

  4. 微信公众号排版多少钱一篇?

    公众号运营可以拆解为文章撰写和图文排版,公众号推文可以整体来做,也可以拆开来做,当你写好公众号文章以后,再找专业的人士进行排版美化. 微信公众号排版多少钱一篇?今天伯乐网络传媒就来给大家聊聊这个话题. ...

  5. qlabel可以选中吗_惊现凡尔赛式排版!原来微信公众号排版样式还可以“变装”?...

    各位小伙伴们,要集中注意力了!接下来就是考验你们观察力的时候啦! 快跟着小妹儿看一下,一个样式到底能有多少种玩法?文中使用工具为公众号编辑器-小蚂蚁编辑器. 1.添加/删除背景 编辑器里的内容样式是可 ...

  6. 微信公众号使用Chrome插件:Markdown Nice优化微信公众号排版教程

    Markdown Nice 是一个为了解决微信公众号排版而生的 Markdown 编辑器,当前有在线编辑器和 Chrome 插件 2 种产品形态. 下面介绍Chrome 插件:Markdown Nic ...

  7. 强推Markdown神器,一秒钟拯救微信公众号排版

    我一直觉得微信公众号是最难用的文章编辑器,直到我开始写知乎专栏.作为两个UGC内容为主的产品,用户体验如此之差真的大大降低了写作者的创作欲望. 基于这个痛点,滋养了一大批像365编辑器.壹伴.秀米等第 ...

  8. 微信公众号排版神器Markdown Nice

    Markdown Nice 体验地址 公元2019年,微信公众号排版能力孱弱,始终为运营者所诟病,秀米.135编辑器等工具割据一方. 但无论是微信原生工具,还是其他编辑器,都让创作者不得不将有限的创作 ...

  9. 微信营销十(微信公众号排版技巧)

    大家好,今天给大家介绍如何进行微信公众号的排版,排版是一个非常基础性的工作.在我们的微信公众号的维护过程当中,如果你写出了非常漂亮的文章,却不能进行精美的排版,那就非常遗憾了.所以,今天我们就要给大家 ...

最新文章

  1. java语言程序设计期末复习综合练习题_Java语言程序设计期末复习综合练习题答案...
  2. 使用SSMS操作数据-sql
  3. 通过Github Teams进行代码仓库的权限访问控制
  4. 尚硅谷Java学习笔记Lecture1
  5. 学生用计算机如何解方程,学生党必备神器!一键解方程计算器App
  6. 网络协议、socket、webSocket
  7. 计算机二级vf上机考试题库,计算机等级考试二级VF上机题库
  8. win7计算机时间显示错误,Win7每次重新启动时计算机显示时间都是错误的
  9. win7 GHOST删除桌面上IE图标
  10. Maven Resources Plugin的Filtering功能的Bug
  11. google外链怎么做?谷歌网站做外链的方法
  12. 北大数学系「扫地僧」韦东奕爆红!拒绝哈佛offer,留任北大,却因长相引热议...
  13. 资本寒冬融资难,具备这四大特质的创业者更受青睐
  14. 电子秤称重系统设计,HX711压力传感器,51单片机(Proteus仿真、C程序、原理图、论文等全套资料)
  15. 光伏行业十个人的江湖:霸道总裁pk硬汉书生
  16. dataframe如何定义列名称
  17. linux串口设备配置方法(固定ID)
  18. 够迫履门夹钾灼敛墒套谮姑韩立对墨大夫一年后是否真的信守承诺,很是怀疑,若真是像对方所说
  19. 小说里的编程 【连载之七】元宇宙里月亮弯弯
  20. 通信的基本概念与通信系统的组成

热门文章

  1. C语言兔子生兔子问题
  2. 夏敏捷第28本著作《Flash ActionScript3.0动画基础与游戏设计》(Flash CC版)
  3. elastic-php实现多个OR并列查询的优化
  4. From Microservices to Data Microservices-pivotal-专题视频课程
  5. mysql查询是第几条记录_MySQL查询第几行到第几行记录
  6. 基于JavaSwing开发模拟电梯系统+分析报告 课程设计 大作业源码
  7. Navicat Premium 15.0.26 MacOS
  8. npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! platform@1.0.0 start: `node build/dev-server.js`
  9. NTP对时服务器(NTP电子时钟)在生物制药业应用
  10. 有向图转强连通图最少加边数