以前我寫過一篇文章 《打造一個優雅的微信文章編輯器》,那時候是直接 fork 大神小鬍子哥 的線上排版編輯器過來揣摩了一番,順便改了點樣式,加了一個代碼主題色 Material Dark,就上線了。實際上,項目的代碼和體驗一直我都感受挺彆扭的,便完全重構了一番。javascript
新版訪問地址:md.ironmaxi.comhtml
新版界面:vue
操做效果:java
大致介紹一下,我在原項目的基礎上作了什麼工做:node
crtl + c
;接下來,我詳細介紹一下完成這個項目的大體步驟與思路。具體的項目代碼,你們能夠訪問 github 倉庫。若是這款工具好用、解決了你的痛點,請給倉庫一個 Star⭐️!webpack
.
├── .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
複製代碼
只要重點關注一下以下文件或文件夾便可:git
.gitlab-ci.yml
service-worker-plugin.js
sw-register.js
build
src
在這個編輯器中,最核心的功能只有兩個:github
咱們要引入第三方庫:showdownjs/showdownweb
使用很簡單,看看官方 demo:npm
// converter.js
var showdown = require('showdown'),
converter = new showdown.Converter(),
text = '# hello, markdown!',
html = converter.makeHtml(text);
// output
// <h1 id="hellomarkdown">hello, markdown!</h1>
複製代碼
還支持本身寫插件,插件格式有兩種:
舉個例子:
// 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);
複製代碼
是否是超簡單?!
依賴的核心第三方插件就是 google/code-prettify,我給你們總結下官方推薦用法:
<script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>
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
去高亮代碼,必須給 pre
和 code
元素加上 prettyprint
樣式名,若是還須要行號的話,還得加上 linenums
樣式名。咱們就藉助 showdown 的插件,實現給全部轉換出來的 html 中的 pre
和 code
加樣式名。在 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;">';
}
});
},
}];
});
複製代碼
當咱們點擊「複製所有內容」按鈕時,會將渲染後的 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>
複製代碼
你們若是訪問了個人線上版本:md.ironmaxi.com,那麼你如今能夠嘗試一下,關閉網絡,關閉全部瀏覽器;而後從新打開一個剛纔訪問過這個網站的瀏覽器,訪問該域名,你會發現,照常顯示,功能正常。
你們能夠打開開發者工具,切換到 Network,能夠看到靜態資源的 Size,都是 (from ServiceWorker)
,這樣咱們就在斷網的環境都可以使用。固然了,斷網的環境咱們也不能到微信公衆平臺發文,因此,最主要的目的仍是讓這款排版編輯器在網絡差或者日常狀況下,可以實現瞬間加載。
因爲咱們使用了 webpack 來搭建工程項目,咱們就能夠很方便地引入第三方的 webpack 插件:
這兩個插件有點相輔相成的味道。玩過 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
階段,從而起做用。對用戶來講是黑盒,若是用戶一直不刷新頁面呢?
這些狀況太可怕了。那麼到底如何優雅地實現更新策略?
咱們能夠將註冊 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 path
if (/^(http(s)?:)?\/\//.test(swFilePath) || swFilePath[0] !== '/') {
// 加構建時間戳
return item.replace(/\.js/g, ext => `${ext}?v=${version}`);
}
// if is absolute path
if (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
文件路徑都加上構建時間戳。
有了註冊 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'
},
]
}
),
],
};
複製代碼
看到了嗎?咱們只須要對想要緩存的資源去作配置便可,省去了一堆的緩存策略邏輯,是否是很是便捷高效?!
這篇文章到這裏,已經很長了,並且超綱了不少。但我仍是想記錄一下,一步步優化工程的細節點,這一步驟純屬是爲了加速集成發佈的,關於 CI/CD 的文章,我還在籌備當中,若是讀者們感興趣的話,能夠去找些入門資料來閱讀一下。
我這裏用上 CI/CD 有什麼好處呢?首先要說一下構建、發佈項目代碼的痛點:
然而,當咱們用上了 CI/CD,能夠起碼作到一些什麼呢?
限於主題和篇幅,我貼一下該項目使用 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⭐️。
感謝大家的閱讀。
以爲本文不錯的話,分享一下給小夥伴吧~