我打造了一個線上簡歷生成應用

粥裡有勺糖發表於2021-03-07

我打造了一個線上簡歷生成應用

前言

半個月前,我寫了一篇文章如何書寫一份好的網際網路校招簡歷,目的是幫助即將開始投遞校招的同學更好的完善自己的簡歷

在文章中也立下了一個Flag

圖片

看了一下Github的 commit記錄,截止目前大概花了一週的時間,把心中所設想的第一版做了出來,也許不完美,但我想應該也能幫助到部分同學

好東西當然展示三遍,O(∩_∩)O~~

對模板樣式(顏色,排版)不滿意的,懂前端魔法的同學可以clone倉庫,施展一下自己的魔法美化

對專案感興趣的同學也歡迎貢獻一下自己喜歡的簡歷模板(程式碼),理論上不限制開發技術棧,當然也歡迎提issues或者建議

本文主要講一下此專案的設計思路,技術方案以及遇到的一些問題與解決思路(用了不少hack技巧)

專案設計

佈局

圖片

整個應用的基本頁面結構

<body>
    <header>
        <!-- 導航 -->
        <nav></nav>
    </header>
    <div>
        <!-- 展示簡歷 -->
        <iframe></iframe>
        <!-- 控制區域 -->
        <div></div>
    </div>
</body>

可能有朋友在這裡會疑惑為什麼要用iframe?

這裡先給大家簡單介紹一下,後面在講技術方案的時候會給大家解釋

在我的設想中簡歷部分只有展示邏輯,可以看作是一個獨立的純靜態頁面

既然是隻做展示,那麼無論什麼前端魔法都可以做這個工作,於是為了方便各種魔法師施法,就把這一塊獨立了出來,簡歷模板貢獻者也只需要關心自己如何復原一個靜態頁面就行,其餘的互動邏輯都交給父頁面統一處理

技術選型

圖片

Vanilla JS——世界上最輕量的JavaScript框架(沒有之一) ---- 原生js

整個應用的主體部分採用原生js實現

簡歷展示部分理論上可以採用任意前端技術棧實現,與父頁面低耦合

通訊

圖片

  • 通過導航欄切換各種簡歷模板
  • 簡歷上的改動自動同步到控制區域中的頁面描述資訊
  • 控制區域中改動頁面描述資訊,簡歷內容實時更新

描述簡歷

圖片

  • 使用json 對簡歷的結構與內容進行描述
  • 一個模板對應一個json

頁面描述資訊展示

圖片

  • 使用JSON描述簡歷上的各種資訊
  • 提供一個JSON編輯器
  • 這裡json編輯器採用 jsoneditor

資料存取

圖片

  • 整個資料流是單向的,外部負責更新,內部(簡歷展示部分)只負責讀取
  • 資料存放在本地,因此不擔心個人資訊洩露
  • 這裡採用 localStorage

第一版效果

圖片

圖片

下面就介紹專案實現的關鍵部分內容

實現

專案目錄結構

./config                         webpack配置檔案
├── webpack.base.js             -- 公共配置
├── webpack.config.build.js     -- 生產環境特有配置
├── webpack.config.dev.js       -- 開發環境特有配置
├── webpack.config.js           -- 引用的配置檔案
│
./public            公共靜態資源
├── css   
│   └── print.css  列印時用的樣式
│
./src       核心程式碼
├── assets          靜態資源css/img
├── constants       常量
│   ├── index.js    存放導航的名稱對映資訊
│   ├── schema      存放每個簡歷模板的預設JSON資料,與pages中的模板一一對應
│   └────── demo1.js   
├── pages           簡歷模板目錄
│   └── demo1       -- 其中的一個模板
│
├── utils           工具方法
├── app.js          專案的入口js
├── index.html      專案的入口頁面

約定優於配置

根據約定好的目錄結構,通過自動化的指令碼

所有模板都統一在 src/pages/xxx 目錄下

頁面模板約定為 index.html,該目錄下的所有js檔案將被自動新增到webpack的entry中,自動注入到 當前 頁面模板中

例如

./src
├── pages          
│   └── xxx
│   └───── index.html
│   └───── index.scss
│   └───── index.js

此處自動化生成entry/page配置程式碼可移步這裡檢視

自動生成的結果如下

圖片

每個HTMLWebpackPlugin的內容格式如下

圖片

自動生成導航欄

首頁頂部有一個導航欄用於切換簡歷模板的路由

圖片

這部分的連結內容如果手動填寫是很無趣的,如何實現自動生成的呢

首先首頁模板的header nav 部分內容為

<header>
    <nav id="nav">
        <%= htmlWebpackPlugin.options.pageNames %>
    </nav>
</header>

htmlWebpackPlugin.options 表示 HTMLWebpackPlugin物件的的userOptions屬性

我們們上面拿到了了所有Page的title,將所有title使用,連線拼接在一起,然後繫結到userOptions.pageNames上,則頁面初次渲染結果就變成了

<header>
    <nav id="nav">
        abc,demo1,vue1,react1,introduce
    </nav>
</header>

有了初次渲染結果,接下來我們們寫一個方法把這些內容轉為a標籤即可

const navTitle = {
    'demo1': '模板1',
    'react1': '模板2',
    'vue1': '模板3',
    'introduce': '使用文件',
    'abc': '開發示例'
}

function createLink(text, href, newTab = false) {
    const a = document.createElement('a')
    a.href = href
    a.text = text
    a.target = newTab ? '_blank' : 'page'
    return a
}

/**
 * 初始化導航欄
 */
function initNav(defaultPage = 'react1') {
    const $nav = document.querySelector('header nav')
    // 獲取所有模板的連結---處理原始內容
    const links = $nav.innerText.split(',').map(pageName => {
        const link = createLink(navTitle[pageName] || pageName, `./pages/${pageName}`)
        // iframe中開啟
        return link
    })

    // 加入自定義的連結
    links.push(createLink('Github', 'https://github.com/ATQQ/resume', true))
    links.push(createLink('貢獻模板', 'https://github.com/ATQQ/resume/blob/main/README.md', true))
    links.push(createLink('如何書寫一份好的網際網路校招簡歷', 'https://juejin.cn/post/6928390537946857479', true))
    links.push(createLink('建議/反饋', 'https://www.wenjuan.com/s/MBryA3gI/', true))

    // 渲染到頁面中
    const t = document.createDocumentFragment()
    links.forEach(link => {
        t.appendChild(link)
    })
    $nav.innerHTML = ''
    $nav.append(t)
}

initNav()

這樣導航欄就“自動“生成了

自動匯出頁面描述

目錄

./src
├── constants      
│   ├── index.js
│   ├── schema.js
│   ├── schema    
│   ├────── demo1.js  
│   ├────── react1.js  
│   └────── vue1.js

每個頁面的預設資料從./src/constants/schema.js中讀取

import abc from './schema/abc'
import demo1 from './schema/demo1'
import react1 from './schema/react1'
import vue1 from './schema/vue1'

export default{
    abc,demo1,react1,vue1
}

而每個模板的描述內容分佈在 schema目錄下,如果讓每個開發者手動往schema.js新增自己模板,容易造成衝突,所以乾脆自動生成

工具方法移步至這裡檢視

/**
 * 自動建立src/constants/schema.js 檔案
 */
function writeSchemaJS() {
    const files = getDirFilesWithFullPath('src/constants/schema')
    const { dir } = path.parse(files[0])
    const targetFilePath = path.resolve(dir, '../', 'schema.js')
    const names = files.map(file => path.parse(file).name)
    const res = `${names.map(n => {
        return `import ${n} from './schema/${n}'`
    }).join('\n')}

export default{
    ${names.join(',')}
}`
    fs.writeFileSync(targetFilePath, res)
}

資料存取

資料的存取操作在父頁面和子頁面都會用到,抽離為公共方法

資料存放於localStorage中,以每個簡歷模板的路由作為key

./src/utils/index.js

import defaultSchema from '../constants/schema'

export function getSchema(key = '') {
    if (!key) {
        // 預設key為路由 如 origin.com/pages/react1
        // key就為 pages/react1
        key = window.location.pathname.replace(/\/$/, '')
    }
    // 先從本地取
    let data = localStorage.getItem(key)
    // 如果沒有就設定一個預設的再取
    if (!data) {
        setSchema(getDefaultSchema(key), key)
        return getSchema()
    }
    // 如果預設是空物件的則再取一次預設值
    if (data === '{}') {
        setSchema(getDefaultSchema(key), key)
        data = localStorage.getItem(key)
    }
    return JSON.parse(data)
}

export function getDefaultSchema(key) {
    const _key = key.slice(key.lastIndexOf('/') + 1)
    return defaultSchema[_key] || {}
}

export function setSchema(data, key = '') {
    if (!key) {
        key = window.location.pathname.replace(/\/$/, '')
    }
    localStorage.setItem(key, JSON.stringify(data))
}

json描述的展示

需要在控制區域展示json的描述資訊,展示部分採用 jsoneditor

當然jsoneditor也支援各種資料操作(CRUD)都支援,還提供了快捷操作按鈕

這裡採用cdn的方式引入jsoneditor

<link rel="stylesheet" href="https://img.cdn.sugarat.top/css/jsoneditor.min.css">
<script src="https://img.cdn.sugarat.top/js/jsoneditor.min.js"></script>

初始化

/**
 * 初始化JSON編輯器
 * @param {string} id 
 */
function initEditor(id) {
    let timer = null
    // 這裡做了一個簡單的防抖
    const editor = new JSONEditor(document.getElementById(id), {
        // json內容改動時觸發
        onChangeJSON(data) {
            if (timer) {
                clearTimeout(timer)
            }
            // updatePage方法用於通知子頁面更新
            setTimeout(updatePage, 200, data)
        }
    })
    return editor
}

const editor = initEditor('jsonEditor')

展示效果

圖片

json資料展示/更新時機

  • 因為每次切換路由都會觸發iframe的onload事件
  • 所以將獲取editor更新json內容的時機放在這裡
function getPageKey() {
    return document.getElementById('page').contentWindow.location.pathname.replace(/\/$/, '')
}

document.getElementById('page').onload = function (e) {
    // 更新editor中顯示的內容
    editor.set(getSchema(getPageKey()))
}

編寫模板頁面

下面提供了4種方式實現同一頁面

期望的效果

圖片

描述檔案

在schema目錄下建立頁面的json描述檔案,如abc.js

./src
├── constants
│   └── schema
│   └────── abc.js  

abc.js

export default {
    name: '王五',
    position: '求職目標: Web前端工程師',
    infos: [
        '1:很多文字',
        '2:很多文字',
        '3:很多文字',
    ]
}

期望的渲染結構

<div id="resume">
    <div id="app">
        <header>
            <h1>王五</h1>
            <h2>求職目標: Web前端工程師</h2>
        </header>
        <ul class="infos">
            <li>1:很多文字<li>
            <li>2:很多文字<li>
            <li>3:很多文字<li>
        </ul>
    </div>
</div>

下面開始子編寫程式碼

與父頁面唯一相關的邏輯就是需要在子頁面的window上掛載一個refresh方法,用於父頁面主動呼叫更新

原生js

import { getSchema } from "../../utils"

window.refresh = function () {
    const schema = getSchema()
    const { name, position, infos } = schema
    // ... render邏輯
}

vue

<script>
import { getSchema } from '../../utils';
export default {
  data() {
    return {
      schema: getSchema(),
    };
  },
  mounted() {
    window.refresh = this.refresh;
  },
  methods: {
    refresh() {
      this.schema = getSchema();
    },
  },
};
</script>

react

import React, { useEffect, useState } from 'react'
import { getSchema } from '../../utils'

export default function App() {
    const [schema, updateSchema] = useState(getSchema())
    const { name, position, infos = [] } = schema
    useEffect(() => {
        window.refresh = function () {
            updateSchema(getSchema())
        }
    }, [])
    return (
        <div>
            { /* 渲染dom的邏輯 */ }
        </div>
    )
}

為方便閱讀,程式碼進行了摺疊

首先是樣式,這裡選擇sass預處理語言,當然也可以用原生css

index.scss
@import './../../assets/css/base.scss';
html,
body,
#resume {
  height: 100%;
  overflow: hidden;
}
// 上面部分是推薦引入的通用樣式

// 下面書寫我們的樣式
$themeColor: red;

#app {
  padding: 1rem;
}

header {
  h1 {
    color: $themeColor;
  }
  h2 {
    font-weight: lighter;
  }
}

.infos {
  list-style: none;
  li {
    color: $themeColor;
  }
}

其次是頁面描述檔案

index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>
        <%= htmlWebpackPlugin.options.title %>
    </title>
</head>

<body>
    <div id="resume">
        <div id="app">

        </div>
    </div>
</body>

</html>

下面就開始使用各種技術棧進行邏輯程式碼編寫

原生js

目錄結構

./src
├── pages          
│   └── abc
│   └───── index.html
│   └───── index.scss
│   └───── index.js

index.js

import { getSchema } from "../../utils"
import './index.scss'

window.refresh = function () {
    const schema = getSchema()
    const { name, position, infos } = schema

    clearPage()
    renderHeader(name, position)
    renderInfos(infos)
}

function clearPage() {
    document.getElementById('app').innerHTML = ''
}

function renderHeader(name, position) {
    const html = `
    <header>
        <h1>${name}</h1>
        <h2>${position}</h2>
    </header>`
    document.getElementById('app').innerHTML += html
}

function renderInfos(infos = []) {
    if (infos?.length === 0) {
        return
    }
    const html = `
    <ul class="infos">
    ${infos.map(info => {
        return `<li>${info}</li>`
    }).join('')}
    </ul>`
    document.getElementById('app').innerHTML += html
}

window.onload = function () {
    refresh()
}
Vue

目錄結構

./src
├── pages          
│   └── abc
│   └───── index.html
│   └───── index.scss
│   └───── index.js
│   └───── App.vue

index.js

import Vue from 'vue'
import App from './App.vue'
import './index.scss'

Vue.config.productionTip = process.env.NODE_ENV === 'development'

new Vue({
    render: h => h(App)
}).$mount('#app')

App.vue

<template>
  <div id="app">
    <header>
      <h1>{{ schema.name }}</h1>
      <h2>{{ schema.position }}</h2>
    </header>
    <div class="infos">
      <p
        v-for="(info,
        i) in schema.infos"
        :key="i"
      >
        {{ info }}
      </p>
    </div>
  </div>
</template>

<script>
import { getSchema } from '../../utils';
export default {
  data() {
    return {
      schema: getSchema(),
    };
  },
  mounted() {
    window.refresh = this.refresh;
  },
  methods: {
    refresh() {
      this.schema = getSchema();
    },
  },
};
</script>
React

目錄結構

./src
├── pages          
│   └── abc
│   └───── index.html
│   └───── index.scss
│   └───── index.js
│   └───── App.jsx

index.js

import React from 'react'
import ReactDOM from 'react-dom';
import App from './App.jsx'
import './index.scss'

ReactDOM.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
    document.getElementById('app')
)

App.jsx

import React, { useEffect, useState } from 'react'
import { getSchema } from '../../utils'

export default function App() {
    const [schema, updateSchema] = useState(getSchema())
    const { name, position, infos = [] } = schema
    useEffect(() => {
        window.refresh = function () {
            updateSchema(getSchema())
        }
    }, [])
    return (
        <div>
            <header>
                <h1>{name}</h1>
                <h2>{position}</h2>
            </header>
            <div className="infos">
                {
                    infos.map((info, i) => {
                        return <p key={i}>{info}</p>
                    })
                }
            </div>
        </div>
    )
}
jQuery

目錄結構

./src
├── pages          
│   └── abc
│   └───── index.html
│   └───── index.scss
│   └───── index.js

index.js

import { getSchema } from "../../utils"
import './index.scss'

window.refresh = function () {
    const schema = getSchema()
    const { name, position, infos } = schema

    clearPage()
    renderHeader(name, position)
    renderInfos(infos)
}

function clearPage() {
    $('#app').empty()
}

function renderHeader(name, position) {
    const html = `
    <header>
        <h1>${name}</h1>
        <h2>${position}</h2>
    </header>`
    $('#app').append(html)
}

function renderInfos(infos = []) {
    if (infos?.length === 0) {
        return
    }
    const html = `
    <ul class="infos">
    ${infos.map(info => {
        return `<li>${info}</li>`
    }).join('')}
    </ul>`
    $('#app').append(html)
}

window.onload = function () {
    refresh()
}

如果覺得導航欄展示abc不友好,當然也可以更改

./src
├── constants    
│   ├── index.js    存放路徑與中文title的對映

./src/constants/index.js 中加入別名

export const navTitle = {
    'abc': '開發示例'
}

圖片

子頁面更新

前面在例項化editor的時候有一個 updatePage 方法

如果子頁面有refresh方法則直接 呼叫其進行頁面的更新,當然在更新之前父頁面會把最新的資料存入到localStorage中

這樣頁面之間實際沒有直接交換資料,一個負責寫,一個負責讀,即使寫入失敗也不影響子頁面讀取原有的資料

function refreshIframePage(isReload = false) {
    const page = document.getElementById('page')
    if (isReload) {
        page.contentWindow.location.reload()
        return
    }
    if (page.contentWindow.refresh) {
        page.contentWindow.refresh()
        return
    }
    page.contentWindow.location.reload()
}

function updatePage(data) {
    setSchema(data, getPageKey())
    refreshIframePage()
}

/**
 * 初始化JSON編輯器
 * @param {string} id 
 */
function initEditor(id) {
    let timer = null
    // 這裡做了一個簡單的防抖
    const editor = new JSONEditor(document.getElementById(id), {
        // json內容改動時觸發
        onChangeJSON(data) {
            if (timer) {
                clearTimeout(timer)
            }
            // updatePage方法用於通知子頁面更新
            setTimeout(updatePage, 200, data)
        }
    })
    return editor
}

const editor = initEditor('jsonEditor')

匯出pdf

PC端

首先PC端瀏覽器支援列印匯出pdf

如何觸發列印呢?

  • 滑鼠右鍵選擇列印
  • 快捷鍵 Ctrl + P
  • window.print()

我們們這裡程式碼裡使用第三種方案

如何確保列印的內容只有簡歷部分?

這個就要用到媒體查詢

方式一

@media print {
    /* 此部分書寫的樣式還在列印時生效 */
}

方式二

<!-- 引入的css資源只在列印時生效 -->
<link rel="stylesheet" href="./css/print.css" media="print">

只需要在列印樣式中將無關內容進行隱藏即可

圖片

基本能做到1比1的還原

移動端

採用jsPDF + html2canvas

  1. html2canvas 負責將頁面轉為圖片
  2. jsPDF負責將圖片轉為PDF
function getBase64Image(img) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0, img.width, img.height);
    var dataURL = canvas.toDataURL("image/png");
    return dataURL;
}
// 匯出pdf
// 當然這裡確保圖片資源被轉為了base64,否則匯出的簡歷無法展示圖片
html2canvas(document.getElementById('page').contentDocument.body).then(canvas => {
    //返回圖片dataURL,引數:圖片格式和清晰度(0-1)
    var pageData = canvas.toDataURL('image/jpeg', 1.0);
    //方向預設豎直,尺寸ponits,格式a4[595.28,841.89]
    var doc = new jsPDF('', 'pt', 'a4');
    //addImage後兩個引數控制新增圖片的尺寸,此處將頁面高度按照a4紙寬高比列進行壓縮
    // doc.addImage(pageData, 'JPEG', 0, 0, 595.28, 592.28 / canvas.width * canvas.height);
    doc.addImage(pageData, 'JPEG', 0, 0, 595.28, 841.89);
    doc.save(`${Date.now()}.pdf`);
});

但目前此種匯出方式還存在一些問題尚未解決,後續換用其它方案進行處理

  1. 不支援超連結
  2. 不支援iconfont
  3. 字型的留白部分會被剔除

小結

到這裡整個專案的雛形算完成了

  • 導航欄切換簡歷模板
  • 在JSON編輯器中改動json -> 頁面資料更新
  • 匯出pdf
    • 移動端 - jspdf
    • 電腦 - 列印

高能操作

高亮變動的內容

訴求:在json編輯器中進行了內容的更新,期望能在簡歷中高亮展示出變動的內容

轉為技術需求就是期望能監聽到變動的dom,然後高亮

這個地方就用到 MutationObserver

它提供了監視對DOM樹所做更改的能力

/**
 * 高亮變化的Dom
 */
function initObserver() {
    // 包含子孫節點
    // 將監視範圍擴充套件至目標節點整個節點樹中的所有節點
    // 監視指定目標節點或子節點樹中節點所包含的字元資料的變化
    const config = { childList: true, subtree: true, characterData: true };

    // 例項化監聽器物件
    const observer = new MutationObserver(debounce(function (mutationsList, observer) {
        for (const e of mutationsList) {
            let target = e.target
            if (e.type === 'characterData') {
                target = e.target.parentElement
            }
            // 高亮
            highLightDom(target)
        }
    }, 100))
    // 監聽子頁面的body
    observer.observe(document.getElementById('page').contentDocument.body, config);
    // 因為 MutationObserver 是微任務,微任務後面緊接著就是頁面渲染
    
    // 停止觀察變動
    // 這裡使用巨集任務,確保此輪Event loop結束
    setTimeout(() => {
        observer.disconnect()
    }, 0)
}

function highLightDom(dom, time = 500, color = '#fff566') {
    if (!dom?.style) return
    if (time === 0) {
        dom.style.backgroundColor = ''
        return
    }
    dom.style.backgroundColor = '#fff566'
    setTimeout(() => {
        dom.style.backgroundColor = ''
    }, time)
}

何時呼叫 initObserver

當然是在更新頁面之前的時候註冊事件,頁面完成變動渲染後停止監聽

function updatePage(data) {
    // 非同步的微任務,本輪event loop結束停止觀察
    initObserver()
    // 同步
    setSchema(data, getPageKey())
    // 同步 + 渲染頁面
    refreshIframePage()
}

效果

圖片

點哪改哪

期望效果
圖片

訴求:

  • 點選需要修改的部分,就能進行修改操作
  • 修改結果在簡歷上與json編輯器中進行內容同步

下面闡述一下實現

1. 獲取點選的Dom

document.getElementById('page').contentDocument.body.addEventListener('click', function (e) {
    const $target = e.target
})

2. 獲取dom內容在頁面中出現的次數與相對位置

  1. 子頁面只包含展示邏輯,所以需要父頁面做hack操作才能在定位點選內容在json中對應位置
  2. 擁有相同內容的dom不止一個,所以需要全部找出來
/**
 * 遍歷目標Dom樹,找出文字內容與目標一致的dom組
 */
function traverseDomTreeMatchStr(dom, str, res = []) {
    // 如果有子節點則繼續遍歷子節點
    if (dom?.children?.length > 0) {
        for (const d of dom.children) {
            traverseDomTreeMatchStr(d, str, res)
        }
        // 相等則記錄下來
    } else if (dom?.textContent?.trim() === str) {
        res.push(dom)
    }

    return res
}

// 監聽簡歷頁的點選事件
document.getElementById('page').contentDocument.body.addEventListener('click', function (e) {
    const $target = e.target
    // 點選的內容
    const clickText = $target.textContent.trim()
    // 只包含點選內容的節點
    const matchDoms = traverseDomTreeMatchStr(document.getElementById('page').contentDocument.body, clickText)
    // 點選的節點在 匹配的 節點中的相對位置
    const mathIndex = matchDoms.findIndex(v => v === $target)
    // 不包含則不做處理
    if (mathIndex < 0) {
        return
    }
})

3. 獲取jsoneditor中對應的節點

  • 與上面邏輯類似
  • 先過濾出只包含此節點內容的幾個節點
  • 然後根據點選dom在同內容節點列表中的相對位置進行匹配
// 監聽簡歷頁的點選事件
document.getElementById('page').contentDocument.body.addEventListener('click', function (e) {
    // ...省略上述列出的程式碼

    // 解除上次點選的dom高亮
    highLightDom($textarea.clickDom, 0)
    // 高亮這次的10s
    highLightDom($target, 10000)


    // 更新jsoneditor中的search內容
    editor.searchBox.dom.search.value = clickText
    // 主動觸發搜尋
    editor.searchBox.dom.search.dispatchEvent(new Event('change'))

    // 將點選內容顯示在textarea中
    $textarea.value = clickText
    
    // 自動聚焦輸入框
    if (document.getElementById('focus').checked) {
        $textarea.focus()
    }

    // 記錄點選的dom,掛載$textarea上
    $textarea.clickDom = e.target

    // jsoneditor 搜尋過濾的內容為模糊匹配,比如搜尋 a 會匹配 ba,baba,a,aa,aaa
    // 根據上面得到的matchIndex,進行精確匹配全等的json節點
    let i = -1
    for (const r of editor.searchBox.results) {
        // 全等得時候下標才變動
        if (r.node.value === clickText) {
            i++
            // 匹配到json中的節點
            if (i === mathIndex) {
                // 高亮一下$textarea
                $textarea.style.boxShadow = '0 0 1rem yellow'
                setTimeout(() => {
                    $textarea.style.boxShadow = ''
                }, 200)
                return
            }
        }
        // 手動觸發jsoneditor的next search match  按鈕, 切換jsoneditor中active的節點
        editor.searchBox.dom.input.querySelector('.jsoneditor-next').dispatchEvent(new Event('click'))
        // active的節點可以通過下面方式獲取
        // editor.searchBox.activeResult.node
    }
})

4. 更新節點內容

  1. 上面兩個步驟將簡歷中的dom與jsoneditor的dom都獲取到了
  2. 通過textarea輸入的內容
  3. 將輸入的內容分別更新到這兩個dom上,並把最新的json寫入的localStorage中
// 監聽輸入事件,並做一個簡單的防抖
 $textarea.addEventListener('input', debounce(function () {
    if (!editor.searchBox?.activeResult?.node) {
        return
    }
    // 啟用dom變動事件
    initObserver()

    // 更新點選dom
    $textarea.clickDom.textContent = this.value

    // 更新editor的dom
    editor.searchBox.activeResult.node.value = this.value
    editor.refresh()

    // 更新到本地
    setSchema(editor.get(), getPageKey())

}, 100))

這樣就完成了兩側(簡歷/jsoneditor)資料的更新

後續規劃

  1. 接入更多的框架支援
  2. 優化pdf的匯出
    1. 超連結
    2. 字型圖示
  3. 優化使用者體驗
    1. 降低jsoneditor的存在感,當前的新增與刪除操作依賴jsoneditor,對不懂前端魔法的同學不友好
    2. 優化移動端的互動
    3. 美化介面
  4. 加入自動生成程式碼模板指令
  5. 接入更多的模板

相關文章