我打造了一个简历在线生成应用

图片

前言

半个月前,我写了一篇文章如何书写一份好的互联网校招简历,目的是帮助即将开始投递校招的同窗更好的完善本身的简历css

在文章中也立下了一个Flaghtml

图片

看了一下Github的 commit记录,截止目前大概花了一周的时间,把心中所设想的方案作了出来,也许不完美,但我想应该也能帮助到部分同窗前端

好东西固然展现三遍,O(∩_∩)O~~vue

对模板样式(颜色,排版)不满意的,懂前端魔法的同窗能够clone仓库,施展一下本身的魔法美化node

对项目感兴趣的同窗也欢迎贡献一下本身喜欢的简历模板(代码),理论上不限制开发技术栈,固然也欢迎提issues或者建议react

本文主要讲一下此项目的设计思路,技术方案以及遇到的一些问题与解决思路(用了很多hack技巧),还有后续的规划webpack

项目设计

布局

图片

整个应用的基本页面结构git

<body>
    <header>
        <!-- 导航 -->
        <nav></nav>
    </header>
    <div>
        <!-- 展现简历 -->
        <iframe></iframe>
        <!-- 控制区域 -->
        <div></div>
    </div>
</body>
复制代码

可能有朋友在这里会疑惑为何要用iframe?github

这里先给你们简单介绍一下,后面在讲技术方案的时候会给你们解释web

在个人设想中简历部分只有展现逻辑,能够看做是一个独立的纯静态页面

既然是只作展现,那么不管什么前端魔法均可以作这个工做,因而为了方便各类魔法师施法,就把这一块独立了出来,简历模板贡献者也只须要关心本身如何复原一个静态页面就行,其他的交互逻辑都交给父页面统一处理

技术选型

图片

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. 搬运更多的简历模板

感谢你能坚持读到这里,谢谢捧场,若是你也感兴趣欢迎贡献代码(简历/功能)

相关连接

招聘

美团21年春季校招已经开启了,欢迎你们投递

笔者在到店事业群-平台技术部,欢迎你们加入

相关文章
相关标签/搜索