1. 程式人生 > >我打造了一個線上簡歷生成應用

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

# 我打造了一個線上簡歷生成應用 ## 前言 半個月前,我寫了一篇文章[如何書寫一份好的網際網路校招簡歷](https://juejin.cn/post/6928390537946857479),目的是幫助即將開始投遞校招的同學更好的完善自己的簡歷 在文章中也立下了一個Flag ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUwMTIwOTAzMQ==614501209031) 看了一下Github的 [commit記錄](https://github.com/ATQQ/resume/commits/main),截止目前大概花了一週的時間,把心中所設想的第一版做了出來,也許不完美,但我想應該也能幫助到部分同學 好東西當然展示三遍,O(∩_∩)O~~ * [體驗連結](https://resume.sugarat.top/) * [體驗連結](https://resume.sugarat.top/) * [體驗連結](https://resume.sugarat.top/) 對模板樣式(顏色,排版)不滿意的,懂前端魔法的同學可以clone[倉庫](https://github.com/ATQQ/resume),施展一下自己的魔法美化 對專案感興趣的同學也歡迎[貢獻](https://github.com/ATQQ/resume/blob/main/README.md)一下自己喜歡的簡歷模板(程式碼),理論上不限制開發技術棧,當然也歡迎提issues或者建議 本文主要講一下此專案的設計思路,技術方案以及遇到的一些問題與解決思路(用了不少hack技巧) ## 專案設計 ### 佈局 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUwNzM0ODgzNw==614507348837) 整個應用的基本頁面結構 ```html
``` 可能有朋友在這裡會疑惑為什麼要用iframe? >這裡先給大家簡單介紹一下,後面在講技術方案的時候會給大家解釋 在我的設想中簡歷部分**只有展示邏輯**,可以看作是一個獨立的純靜態頁面 既然是隻做展示,那麼無論什麼前端魔法都可以做這個工作,於是為了方便各種魔法師施法,就把這一塊獨立了出來,簡歷模板貢獻者也只需要關心自己如何復原一個靜態頁面就行,其餘的互動邏輯都交給父頁面統一處理 ### 技術選型 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUwODgzNzIxMA==614508837210) > Vanilla JS——世界上最輕量的JavaScript框架(沒有之一) ---- **原生js** 整個應用的主體部分採用原生js實現 簡歷展示部分理論上可以採用任意前端技術棧實現,與父頁面低耦合 ### 通訊 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUwOTM4NDkwOQ==614509384909) * 通過導航欄切換各種簡歷模板 * 簡歷上的改動自動同步到控制區域中的頁面描述資訊 * 控制區域中改動頁面描述資訊,簡歷內容實時更新 ### 描述簡歷 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxMDA2Nzk0MA==614510067940) * 使用json 對簡歷的結構與內容進行描述 * 一個模板對應一個json ### 頁面描述資訊展示 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxMTMxNTQyMg==614511315422) * 使用JSON描述簡歷上的各種資訊 * 提供一個JSON編輯器 * 這裡json編輯器採用 [jsoneditor](https://github.com/josdejong/jsoneditor) ### 資料存取 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxMDU2Njg0NQ==614510566845) * 整個資料流是單向的,外部負責更新,內部(簡歷展示部分)只負責讀取 * 資料存放在本地,因此不擔心個人資訊洩露 * 這裡採用 `localStorage` ### 第一版效果 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxMTcxNzAwNQ==614511717005) ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxODIzOTU1OQ==614518239559) 下面就介紹專案實現的關鍵部分內容 ## 實現 ### 專案目錄結構 ``` ./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配置**程式碼可移步**至[這裡](https://github.com/ATQQ/resume/blob/2c5e75f8b7b824b2436d3f02c5e304390d05d83c/config/fileUtil.js#L99-L118)檢視 自動生成的結果如下 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxNDA2NTU5MQ==614514065591) 每個HTMLWebpackPlugin的內容格式如下 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxNDI3ODI5NQ==614514278295) ### 自動生成導航欄 首頁頂部有一個導航欄用於切換簡歷模板的路由 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxNDU5Mzc1MA==614514593750) 這部分的連結內容如果手動填寫是很無趣的,**如何實現自動生成的呢**? 首先首頁模板的header nav 部分內容為 ```html
``` `htmlWebpackPlugin.options` 表示 `HTMLWebpackPlugin`物件的的`userOptions`屬性 咱們上面拿到了了所有Page的title,將所有title使用`,`連線拼接在一起,然後繫結到`userOptions.pageNames`上,則頁面初次渲染結果就變成了 ```html
``` 有了初次渲染結果,接下來咱們寫一個方法把這些內容轉為`a`標籤即可 ```js 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中讀取 ```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新增自己模板,容易造成衝突,所以乾脆自動生成 工具方法移步至[這裡](https://github.com/ATQQ/resume/blob/2c5e75f8b7b824b2436d3f02c5e304390d05d83c/config/fileUtil.js#L30-L32)檢視 ```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** ```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](https://github.com/josdejong/jsoneditor) 當然jsoneditor也支援各種資料操作(CRUD)都支援,還提供了快捷操作按鈕 這裡採用cdn的方式引入jsoneditor ```html
``` 初始化 ```js /** * 初始化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') ``` 展示效果 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDUxODc2MDE5Ng==614518760196) json資料展示/更新時機 * 因為每次切換路由都會觸發iframe的onload事件 * 所以將獲取editor更新json內容的時機放在這裡 ```js function getPageKey() { return document.getElementById('page').contentWindow.location.pathname.replace(/\/$/, '') } document.getElementById('page').onload = function (e) { // 更新editor中顯示的內容 editor.set(getSchema(getPageKey())) } ``` ### 編寫模板頁面 下面提供了4種方式實現同一頁面 **期望的效果** ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDQ4MDYyMjQ1Ng==614480622456) **描述檔案** 在schema目錄下建立頁面的json描述檔案,如abc.js ``` ./src ├── constants │ └── schema │ └────── abc.js ``` abc.js ```js export default { name: '王五', position: '求職目標: Web前端工程師', infos: [ '1:很多文字', '2:很多文字', '3:很多文字', ] } ``` **期望的渲染結構** ```html

王五

求職目標: Web前端工程師

  • 1:很多文字
  • 2:很多文字
  • 3:很多文字
``` 下面開始子編寫程式碼 與父頁面**唯一相關的邏輯**就是需要在子頁面的window上掛載一個refresh方法,用於父頁面主動呼叫更新 **原生js** ```js import { getSchema } from "../../utils" window.refresh = function () { const schema = getSchema() const { name, position, infos } = schema // ... render邏輯 } ``` **vue** ```vue ``` **react** ```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 ( { /* 渲染dom的邏輯 */ } ) } ``` **為方便閱讀,程式碼進行了摺疊** 首先是樣式,這裡選擇sass預處理語言,當然也可以用原生css
index.scss ```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 ```html ```
**下面就開始使用各種技術棧進行邏輯程式碼編寫**
原生js **目錄結構** ``` ./src ├── pages │ └── abc │ └───── index.html │ └───── index.scss │ └───── index.js ``` **index.js** ```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 = `

${name}

${position}

` document.getElementById('app').innerHTML += html } function renderInfos(infos = []) { if (infos?.length === 0) { return } const html = `
    ${infos.map(info => { return `
  • ${info}
  • ` }).join('')}
` document.getElementById('app').innerHTML += html } window.onload = function () { refresh() } ```
Vue **目錄結構** ``` ./src ├── pages │ └── abc │ └───── index.html │ └───── index.scss │ └───── index.js │ └───── App.vue ``` **index.js** ```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** ```vue ```
React **目錄結構** ``` ./src ├── pages │ └── abc │ └───── index.html │ └───── index.scss │ └───── index.js │ └───── App.jsx ``` **index.js** ```js import React from 'react' import ReactDOM from 'react-dom'; import App from './App.jsx' import './index.scss' ReactDOM.render( , document.getElementById('app') ) ``` **App.jsx** ```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 (

{name}

{position}

{ infos.map((info, i) => { return

{info}

}) } ) } ```
jQuery **目錄結構** ``` ./src ├── pages │ └── abc │ └───── index.html │ └───── index.scss │ └───── index.js ``` **index.js** ```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 = `

${name}

${position}

` $('#app').append(html) } function renderInfos(infos = []) { if (infos?.length === 0) { return } const html = `
    ${infos.map(info => { return `
  • ${info}
  • ` }).join('')}
` $('#app').append(html) } window.onload = function () { refresh() } ```
如果覺得導航欄展示abc不友好,當然也可以更改 ``` ./src ├── constants │ ├── index.js 存放路徑與中文title的對映 ``` **./src/constants/index.js** 中加入別名 ```js export const navTitle = { 'abc': '開發示例' } ``` ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDQ5MDMyMDA3Nw==614490320077) ### 子頁面更新 前面在例項化editor的時候有一個 `updatePage` 方法 如果子頁面有refresh方法則直接 呼叫其進行頁面的更新,當然在更新之前父頁面會把最新的資料存入到localStorage中 這樣頁面之間實際沒有直接交換資料,一個負責寫,一個負責讀,即使寫入失敗也不影響子頁面讀取原有的資料 ```js 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()` 咱們這裡程式碼裡使用第三種方案 **如何確保列印的內容只有簡歷部分?** 這個就要用到媒體查詢 方式一 ```css @media print { /* 此部分書寫的樣式還在列印時生效 */ } ``` 方式二 ```html ``` 只需要在列印樣式中將無關內容進行隱藏即可 ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDU2NjIzMjYyNQ==614566232625) 基本能做到1比1的還原 #### 移動端 採用[jsPDF](https://github.com/MrRio/jsPDF) + [html2canvas](https://github.com/niklasvh/html2canvas) 1. html2canvas 負責將頁面轉為圖片 2. jsPDF負責將圖片轉為PDF ```js 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](https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver)了 它提供了監視對DOM樹所做更改的能力 ```js /** * 高亮變化的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** 當然是在更新頁面之前的時候註冊事件,頁面完成變動渲染後停止監聽 ```js function updatePage(data) { // 非同步的微任務,本輪event loop結束停止觀察 initObserver() // 同步 setSchema(data, getPageKey()) // 同步 + 渲染頁面 refreshIframePage() } ``` **效果** ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDU2ODM4MjQzNw==%E6%B7%B1%E5%BA%A6%E5%BD%95%E5%B1%8F_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20210301111206.gif) ### 點哪改哪 **期望效果** ![圖片](https://img.cdn.sugarat.top/mdImg/MTYxNDU3MzIyODM5Mw==resume-update.gif) 訴求: * 點選需要修改的部分,就能進行修改操作 * 修改結果在簡歷上與json編輯器中進行內容同步 下面闡述一下實現 **1. 獲取點選的Dom** ```js document.getElementById('page').contentDocument.body.addEventListener('click', function (e) { const $target = e.target }) ``` **2. 獲取dom內容在頁面中出現的次數與相對位置** 1. 子頁面只包含展示邏輯,所以需要父頁面做hack操作才能在定位點選內容在json中對應位置 2. 擁有相同內容的dom不止一個,所以需要全部找出來 ```js /** * 遍歷目標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在同內容節點列表中的相對位置進行匹配 ```js // 監聽簡歷頁的點選事件 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中 ```js // 監聽輸入事件,並做一個簡單的防抖 $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. 接入更多的