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

简介: 我打造了一个简历在线生成应用(二)

实现


项目目录结构


./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
  • 电脑 - 打印
相关文章
|
6月前
|
存储 前端开发 搜索推荐
​开源在线生成简历器:快速打造个人的简历利器
​开源在线生成简历器:快速打造个人的简历利器
153 0
|
6月前
|
人工智能 算法 JavaScript
【简历优化平台-06】为什么很多简历必须写项目经验?有的简历没有项目经验?
【简历优化平台-06】为什么很多简历必须写项目经验?有的简历没有项目经验?
|
前端开发 算法 JavaScript
【简历优化平台开发教程-12】测试用模版简历!
【简历优化平台开发教程-12】测试用模版简历!
|
运维 前端开发 架构师
适合小白的人事面试回答模板
当我们有幸通过几轮技术面试后,最有心机的人事面试就来了,人事面试不是技术面试,会就是会,不会就是不会,这是一个勾心斗角的过程,好些不善于表达的工程师也有可能在人事面试被刷掉,以下我列举了一下常见的人事面试问题
147 0
|
数据可视化 数据管理 BI
招聘管理综合实践——生成在线简历库|学习笔记
快速学习招聘管理综合实践——生成在线简历库
招聘管理综合实践——生成在线简历库|学习笔记
|
弹性计算 Linux 云计算
将简历上传到服务器 | 学习笔记
快速学习 将简历上传到服务器
将简历上传到服务器 | 学习笔记
|
消息中间件 前端开发 JavaScript
校招大学生简历制作模板(ps:程序员简历)
本文献给准备春招秋招的小伙伴们!
229 0
校招大学生简历制作模板(ps:程序员简历)
|
弹性计算
搭建自己的简历平台
我是就读于广东财经大学物流管理专业的一名大二学生,在老师的课堂指导下,参与了飞天加速计划,领用到云服务器ECS,并运用云服务器ECS搭建了自己的简历平台和个人博客,接下来,我将简单介绍一下我利用云服务器ECS搭建简历平台的过程。
搭建自己的简历平台
|
数据库 数据安全/隐私保护 Python
【Django | 开发】面试招聘信息网站(用户登录注册&投在线递简历)
【Django | 开发】面试招聘信息网站(用户登录注册&投在线递简历)
【Django | 开发】面试招聘信息网站(用户登录注册&投在线递简历)
|
JSON 前端开发 JavaScript
我打造了一个简历在线生成应用(一)
我打造了一个简历在线生成应用(一)