嫌微信公衆號排版太醜?這裏讓你一步到位

以前我寫過一篇文章 《打造一個優雅的微信文章編輯器》,那時候是直接 fork 大神小鬍子哥線上排版編輯器過來揣摩了一番,順便改了點樣式,加了一個代碼主題色 Material Dark,就上線了。實際上,項目的代碼和體驗一直我都感受挺彆扭的,便完全重構了一番。javascript

新版訪問地址:md.ironmaxi.comhtml

新版界面:vue

新版界面

操做效果:java

操做效果

大致介紹一下,我在原項目的基礎上作了什麼工做:node

  • 添加 webpack 配置,支持本地調試;
  • 引入 Vue,雖然不必,可是有了數據雙向綁定,代碼寫起來簡潔,維護起來方便;
  • 添加實時預覽功能,左側寫出來的 Markdown 文字,右側當即預覽,沒有延遲;
  • 左側與右側視圖同步滾動,進一步提高使用體驗;
  • 點擊複製內容,全部文字及排版樣式通通拷貝進剪貼板,不用再多按一次 crtl + c
  • 根據微信公衆編輯器的樣式及限制,多作了一些兼容;
  • 增長了 3 種不一樣樣式的 blockqoute;
  • 站點升級 https 協議;
  • 重磅:Service Worker 加持,只要訪問過一次線上地址,那麼靜態資源都會被緩存,離線可用!
  • 重磅:Gitlab CI/CD 加持,我只要往 master 主幹提交代碼,項目即可以自動打包構建,並部署到個人我的服務器中,並且還能自動幫我 push 到 github 倉庫中,省去了我人工操做的步驟,很是優雅,這個後面詳細說!

接下來,我詳細介紹一下完成這個項目的大體步驟與思路。具體的項目代碼,你們能夠訪問 github 倉庫。若是這款工具好用、解決了你的痛點,請給倉庫一個 Star⭐️!webpack

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
複製代碼

只要重點關注一下以下文件或文件夾便可:git

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

2. 核心功能

在這個編輯器中,最核心的功能只有兩個:github

  1. 將 markdown 轉爲 html;
  2. 給不一樣語言的代碼設置高亮。

2.1 轉 markdown 爲 html

咱們要引入第三方庫: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>
複製代碼

還支持本身寫插件,插件格式有兩種:

  • 解析自定義 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 插件:

這兩個插件有點相輔相成的味道。玩過 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 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 文件路徑都加上構建時間戳。

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⭐️。

感謝大家的閱讀。


微信公衆號 以爲本文不錯的話,分享一下給小夥伴吧~

相關文章
相關標籤/搜索