最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 我打造了一个简历在线生成应用

    正文概述 掘金(粥里有勺糖)   2021-03-01   524

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

    前言

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

    在文章中也立下了一个Flag

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

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

    好东西当然展示三遍,O(∩_∩)O~~

    • 体验链接
    • 体验链接
    • 体验链接

    对模板样式(颜色,排版)不满意的,懂前端魔法的同学可以clone仓库,施展一下自己的魔法美化

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

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

    项目设计

    布局

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

    整个应用的基本页面结构

    <body>
        <header>
            <!-- 导航 -->
            <nav></nav>
        </header>
        <div>
            <!-- 展示简历 -->
            <iframe></iframe>
            <!-- 控制区域 -->
            <div></div>
        </div>
    </body>
    

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

    在我的设想中简历部分只有展示逻辑,可以看作是一个独立的纯静态页面

    既然是只做展示,那么无论什么前端魔法都可以做这个工作,于是为了方便各种魔法师施法,就把这一块独立了出来,简历模板贡献者也只需要关心自己如何复原一个静态页面就行,其余的交互逻辑都交给父页面统一处理

    技术选型

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

    整个应用的主体部分采用原生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年春季校招已经开启了,欢迎大家投递

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


    起源地下载网 » 我打造了一个简历在线生成应用

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元