vue.js實現一個會動的簡歷(包含底部導航功能,編輯功能)

在網上看到一個這樣的網站,STRML它的效果看着十分有趣,如下圖所示:
圖片描述

這個網站是用react.js來寫的,於是,我就想着用vue.js也來寫一版,開始擼代碼。

首先要分析打字的原理實現,假設我們定義一個字符串str,它等於一長串註釋加CSS代碼,並且我們看到,當css代碼寫完一個分號的時候,它寫的樣式就會生效。我們知道要想讓一段CSS代碼在頁面生效,只需要將其放在一對<style>標籤對中即可。比如:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  紅色字體
  <style>
    body{
      color:#f00;
    }
  </style>
</body>
</html>

你可以狠狠點擊此處具體示例查看效果。

當看到打字效果的時候,我們不難想到,這是要使用間歇調用(定時函數:setInterval())超時調用(延遲函數:setTimeout())遞歸去模擬實現間歇調用。一個包含一長串代碼的字符串,它是一個個截取出來,然後分別寫入頁面中,在這裏,我們需要用到字符串的截取方法,如slice(),substr(),substring()等,選擇用哪個截取看個人,不過需要注意它們之間的區別。好了,讓我們來實現一個簡單的這樣打字的效果,如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
    <div id="result"></div>
    <script>
        var r =  document.getElementById('result');
        var c = 0;
        var code = 'body{background-color:#f00;color:#fff};'
        var timer = setInterval(function(){
          c++;
          r.innerHTML = code.substr(0,c);
          if(c >= code.length){
            clearTimeout(timer);
          }
        },50)
    </script>
</body>
</html> 

你可以狠狠點擊此處具體示例查看效果。好的,讓我們來分析一下以上代碼的原理,首先放一個用於包含代碼顯示的標籤,然後定義一個包含代碼的字符串,接着定義一個初始值爲0的變量,爲什麼要定義這樣一個變量呢?我們從實際效果中看到,它是一個字一個字的寫入到頁面中的。初始值是沒有一個字符的,所以,我們就從第0個開始寫入,c一個字一個字的加,然後不停的截取字符串,最後渲染到標籤的內容當中去,當c的值大於等於了字符串的長度之後,我們需要清除定時器。定時函數看着有些不太好,讓我們用超時調用結合遞歸來實現。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <div id="result"></div>
  <script>
     var r =  document.getElementById('result');
     var c = 0;
     var code = 'body{background-color:#f00;color:#fff};';
     var timer;
     function write(){
        c++;
        r.innerHTML = code.substr(0,c);
        if(c >= code.length && timer){
            clearTimeout(timer)
        }else{
           setTimeout(write,50);
       }
    }
    write();
 </script>
</body>
</html>

你可以狠狠點擊此處具體示例查看效果。

好了,到此爲止,算是實現了第一步,讓我們繼續,接下來,我們要讓代碼保持空白和縮進,這可以使用<pre>標籤來實現,但其實我們還可以使用css代碼的white-space屬性來讓一個普通的div標籤保持這樣的效果,爲什麼要這樣做呢,因爲我們還要實現一個功能,就是編輯它裏面的代碼,可以讓它生效。更改一下代碼,如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <style>
    #result{
      white-space:pre-wrap;
      oveflow:auto;
    }
  </style>
</head>
<body>
  <div id="result"></div>
  <script>
     var r =  document.getElementById('result');
     var c = 0;
     var code = `
        body{
            background-color:#f00;
            color:#fff;
        }
     `
     var timer;
     function write(){
        c++;
        r.innerHTML = code.substr(0,c);
        if(c >= code.length && timer){
            clearTimeout(timer)
        }else{
           setTimeout(write,50);
       }
    }
    write();
 </script>
</body>
</html>

你可以狠狠點擊此處具體示例查看效果。

接下來,我們還要讓樣式生效,這很簡單,將代碼在style標籤中寫一次即可,請看:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <style>
    #result{
      white-space:pre-wrap;
      overflow:auto;
    }
  </style>
</head>
<body>
   <div id="result"></div>
   <style id="myStyle"></style>
   <script>
      var r = document.getElementById('result'),
          t = document.getElementById('myStyle');
      var c = 0;
      var code = `
         body{
            background-color:#f00;
            color:#fff;
         }
      `;
     var timer;
     function write(){
       c++;
       r.innerHTML = code.substr(0,c);
       t.innerHTML = code.substr(0,c);
       if(c >= code.length){
         clearTimeout(timer);
       }else{
         setTimeout(write,50);
       }
     }
     write();
   </script>
  
</body>
</html> 

你可以狠狠點擊此處具體示例查看效果。

我們看到代碼還會有高亮效果,這可以用正則表達式來實現,比如以下一個demo:

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

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>代碼編輯器</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .ew-code {
            tab-size: 4;
            -moz-tab-size: 4;
            -o-tab-size: 4;
            margin-left: .6em;
            background-color: #345;
            white-space: pre-wrap;
            color: #f2f2f2;
            text-indent: 0;
            margin-right: 1em;
            display: block;
            overflow: auto;
            font-size: 20px;
            border-radius: 5px;
            font-style: normal;
            font-weight: 400;
            line-height: 1.4;
            font-family: Consolas, Monaco, "宋體";
            margin-top: 1em;
        }

        .ew-code span {
            font-weight: bold;
        }
    </style>
</head>

<body>
    <code class="ew-code">
        &lt;div id="app"&gt;
            &lt;p&gt;{{ greeting }} world!&lt;/p&gt;
        &lt;/div&gt;
    </code>
    <code class="ew-code">
        //定義一個javascript對象
        var obj = { 
            greeting: "Hello," 
        }; 
        //創建一個實例
        var vm = new Vue({ 
            data: obj 
        });
        /*將實例掛載到根元素上*/
        vm.$mount(document.getElementById('app'));
    </code>
    <script>
        var lightColorCode = {
            importantObj: ['JSON', 'window', 'document', 'function', 'navigator', 'console', 'screen', 'location'],
            keywords: ['if', 'else if', 'var', 'this', 'alert', 'return', 'typeof', 'default', 'with', 'class', 'export', 'import', 'new'],
            method: ['Vue', 'React', 'html', 'css', 'js', 'webpack', 'babel', 'angular', 'bootstap', 'jquery', 'gulp','dom'],
            // special: ["*", ".", "?", "+", "$", "^", "[", "]", "{", "}", "|", "\\", "(", ")", "/", "%", ":", "=", ';']
        }
        function setHighLight(el) {
            var htmlStr = el.innerHTML;
            //匹配單行和多行註釋
            var regxSpace = /(\/\/\s?[^\s]+\s?)|(\/\*(.|\s)*?\*\/)/gm,
                matchStrSpace = htmlStr.match(regxSpace),
                spaceLen;
            //匹配特殊字符
            var regxSpecial = /[`~!@#$%^&.{}()_\-+?|]/gim,
                matchStrSpecial = htmlStr.match(regxSpecial),
                specialLen;
            var flag = false;
            if(!!matchStrSpecial){
                    specialLen = matchStrSpecial.length;
                }else{
                    specialLen = 0;
                    return;
                }
                for(var k = 0;k < specialLen;k++){
                    htmlStr = htmlStr.replace(matchStrSpecial[k],'<span style="color:#b9ff01;">' + matchStrSpecial[k] + '</span>');
                }
            for (var key in lightColorCode) {
                if (key === 'keywords') {
                    lightColorCode[key].forEach(function (imp) {
                        htmlStr = htmlStr.replace(new RegExp(imp, 'gim'), '<span style="color:#00ff78;">' + imp + '</span>')
                    })
                    flag = true;
                } else if (key === 'importantObj') {
                    lightColorCode[key].forEach(function (kw) {
                        htmlStr = htmlStr.replace(new RegExp(kw, 'gim'), '<span style="color:#ec1277;">' + kw + '</span>')
                    })
                    flag = true;
                } else if (key === 'method') {
                    lightColorCode[key].forEach(function (mt) {
                        htmlStr = htmlStr.replace(new RegExp(mt, 'gim'), '<span style="color:#52eeff;">' + mt + '</span>')
                    })
                    flag = true;
                }
            }
            if (flag) {
                if (!!matchStrSpace) {
                    spaceLen = matchStrSpace.length;
                } else {
                    spaceLen = 0;
                    return;
                }
                for(var i = 0;i < spaceLen;i++){
                    var curFont;
                    if(window.innerWidth <= 1200){
                        curFont = '12px';
                    }else{
                        curFont = '14px';
                    }
                    htmlStr = htmlStr.replace(matchStrSpace[i],'<span style="color:#899;font-size:'+curFont+';">' + matchStrSpace[i] + '</span>');
                }
                el.innerHTML = htmlStr;
            }
        }
        var codes = document.querySelectorAll('.ew-code');
        for (var i = 0, len = codes.length; i < len; i++) {
            setHighLight(codes[i])
        }

    </script>
</body>

</html>

你可以狠狠點擊此處具體示例查看效果。

不過這裏爲了方便,我還是使用插件Prism.js,另外在這裏,我們還要用到將一個普通文本打造成HTML網頁的插件marked.js

接下來分析如何暫停動畫和繼續動畫,很簡單,就是清除定時器,然後重新調用即可。如何讓編輯的代碼生效呢,這就需要用到自定義事件.sync事件修飾符,自行查看官網vue.js

雖然這裏用原生js也可以實現,但我們用vue-cli結合組件的方式來實現,這樣更簡單一些。好了,讓我們開始吧:

新建一個vue-cli工程(步驟自行百度):

新建一個styleEditor.vue組件,代碼如下:

<template>
    <div class="container">
        <div class="code" v-html="codeInstyleTag"></div>
        <div class="styleEditor" ref="container" contenteditable="true" @input="updateCode($event)" v-html="highlightedCode"></div>
    </div>
</template>
<script>
    import Prism from 'prismjs'
    export default {
        name:'Editor',
        props:['code'],
        computed:{
            highlightedCode:function(){
                //代碼高亮
                return Prism.highlight(this.code,Prism.languages.css);
            },
            // 讓代碼生效
            codeInstyleTag:function(){
                return `<style>${this.code}</style>`
            }
        },
        methods:{
            //每次打字到最底部,就要滾動
            goBottom(){
                this.$refs.container.scrollTop = 10000;
            },
            //代碼修改之後,可以重新生效
            updateCode(e){
                this.$emit('update:code',e.target.textContent);
            }
        }
    }
</script>
<style scoped>
    .code{
        display:none;
    }
</style>

新建一個resumeEditor.vue組件,代碼如下:

<template>
    <div class = "resumeEditor" :class="{htmlMode:enableHtml}" ref = "container">
        <div v-if="enableHtml" v-html="result"></div>
        <pre v-else>{{result}}</pre>
    </div>
</template>
<script>
    import marked from 'marked'
    export default {
        props:['markdown','enableHtml'],
        name:'ResumeEditor',
        computed:{
            result:function(){
                return this.enableHtml ? marked(this.markdown) : this.markdown
            }
        },
        methods:{
            goBottom:function(){
                this.$refs.container.scrollTop = 10000
            }
        }
    }
</script>
<style scoped>
    .htmlMode{
        anmation:flip 3s;
    }
    @keyframes flip{
        0%{
            opactiy:0;
        }
        100%{
            opactiy:1;
        }
    }
</style> 

新建一個底部導航菜單組件bottomNav.vue,代碼如下:

<template>
    <div id="bottom">
        <a  id="pause" @click="pauseFun">{{ !paused ? '暫停動畫' : '繼續動畫 ||' }}</a>
        <a  id="skipAnimation" @click="skipAnimationFun">跳過動畫</a>
        <p>
            <span v-for="(url,index) in demourl" :key="index">
                <a :href="url.url">{{ url.title }}</a>
            </span>
        </p>
        <div id="music" @click="musicPause" :class="playing ? 'rotate' : ''" ref="music"></div>
    </div>
</template>
<script>
    export default{
        name:'bottom',
        data(){
            return{
                demourl:[
                    {url:'http://eveningwater.com/',title:'個人網站'},
                    {url:'https://github.com/eveningwater',title:'github'}
                ],
                paused:false,//暫停
                playing:false,//播放圖標動畫
                autoPlaying:false,//播放音頻
                audio:''
            }
        },
        mounted(){
            
        },
        methods:{
            // 播放音樂
            playMusic(){
                this.playing = true;
                this.autoPlaying = true;
                // 創建audio標籤
                this.audio = new Audio();
                this.audio.src = "http://eveningwater.com/project/newReact-music-player/audio/%E9%BB%84%E5%9B%BD%E4%BF%8A%20-%20%E7%9C%9F%E7%88%B1%E4%BD%A0%E7%9A%84%E4%BA%91.mp3";
                this.audio.loop = 'loop';
                this.audio.autoplay = 'autoplay';
                this.$refs.music.appendChild(this.audio);
            },
            // 跳過動畫
            skipAnimationFun(e){
                e.preventDefault();
                this.$emit('on-skip');
            },
            // 暫停動畫
            pauseFun(e){
                e.preventDefault();
                this.paused = !this.paused;
                this.$emit('on-pause',this.paused);
            },
            // 暫停音樂
            musicPause(){
                this.playing = !this.playing;
                if(!this.playing){
                    this.audio.pause();
                }else{
                    this.audio.play();
                }
            }
        }
    }
</script>
<style scoped>
    #bottom{
        position:fixed;
        bottom:5px;
        left:0;
        right:0;
    }
    #bottom p{
        float:right;
    }
    #bottom a{
        text-decoration: none;
        color: #999;
        cursor:pointer;
        margin-left:5px;
    }
    #bottom a:hover,#bottom a:active{
        color: #010a11;
    }
</style>

接下來是核心APP.vue組件代碼:

<template>
    <div id="app">
        <div class="main">
            <StyleEditor ref="styleEditor" v-bind.sync="currentStyle"></StyleEditor>
            <ResumeEditor ref="resumeEditor" :markdown = "currentMarkdown" :enableHtml="enableHtml"></ResumeEditor>
        </div>
        <BottomNav ref ="bottomNav" @on-pause="pauseAnimation" @on-skip="skipAnimation"></BottomNav>
    </div>
</template>
<script>
    import ResumeEditor from './components/resumeEditor'
    import StyleEditor from './components/styleEditor'
    import BottomNav from './components/bottomNav'
    import './assets/common.css'
    import fullStyle from './style.js'
    import my from './my.js'
    export default {
        name: 'app',
        components: {
            ResumeEditor,
            StyleEditor,
            BottomNav
        },
        data() {
            return {
                interval: 40,//寫入字的速度
                currentStyle: {
                    code: ''
                },
                enableHtml: false,//是否打造成HTML網頁
                fullStyle: fullStyle,
                currentMarkdown: '',
                fullMarkdown: my,
                timer: null
            }
        },
        created() {
            this.makeResume();
        },
        methods: {
            // 暫停動畫
            pauseAnimation(bool) {
                if(bool && this.timer){
                    clearTimeout(this.timer);
                }else{
                    this.makeResume();
                }
            },
            // 快速跳過動畫
            skipAnimation(){
                if(this.timer){
                    clearTimeout(this.timer);
                }
                let str = '';
                this.fullStyle.map((f) => {
                    str += f;
                })
                setTimeout(() => {
                    this.$set(this.currentStyle,'code',str);
                },100)
                this.currentMarkdown = my;
                this.enableHtml = true;
                this.$refs.bottomNav.playMusic();
            },
            // 加載動畫
            makeResume: async function() {
                await this.writeShowStyle(0)
                await this.writeShowResume()
                await this.writeShowStyle(1)
                await this.writeShowHtml()
                await this.writeShowStyle(2)
                await this.$nextTick(() => {this.$refs.bottomNav.playMusic()});
            },
            // 打造成HTML網頁
            writeShowHtml: function() {
                return new Promise((resolve, reject) => {
                    this.enableHtml = true;
                    resolve();
                })
            },
            // 寫入css代碼
            writeShowStyle(n) {
                return new Promise((resolve, reject) => {
                    let showStyle = (async function() {
                        let style = this.fullStyle[n];
                        if (!style) return;
                        //計算出數組每一項的長度
                        let length = this.fullStyle.filter((f, i) => i <= n).map((it) => it.length).reduce((t, c) => t + c, 0);
                        //當前要寫入的長度等於數組每一項的長度減去當前正在寫的字符串的長度
                        let prefixLength = length - style.length;
                        if (this.currentStyle.code.length < length) {
                            let l = this.currentStyle.code.length - prefixLength;
                            let char = style.substring(l, l + 1) || ' ';
                            this.currentStyle.code += char;
                            if (style.substring(l - 1, l) === '\n' && this.$refs.styleEditor) {
                                this.$nextTick(() => {
                                    this.$refs.styleEditor.goBottom();
                                })
                            }
                            this.timer = setTimeout(showStyle, this.interval);
                        } else {
                            resolve();
                        }
                    }).bind(this)
                    showStyle();
                })
            },
            // 寫入簡歷
            writeShowResume() {
                return new Promise((resolve, reject) => {
                    let length = this.fullMarkdown.length;
                    let showResume = () => {
                        if (this.currentMarkdown.length < length) {
                            this.currentMarkdown = this.fullMarkdown.substring(0, this.currentMarkdown.length + 1);
                            let lastChar = this.currentMarkdown[this.currentMarkdown.length - 1];
                            let prevChar = this.currentMarkdown[this.currentMarkdown.length - 2];
                            if (prevChar === '\n' && this.$refs.resumeEditor) {
                                this.$nextTick(() => {
                                    this.$refs.resumeEditor.goBottom()
                                });
                            }
                            this.timer = setTimeout(showResume, this.interval);
                        } else {
                            resolve()
                        }
                    }
                    showResume();
                })
            }
        }
    }
</script>
<style scoped>
    #app {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }

    .main {
        position: relative;
    }

    html {
        min-height: 100vh;
    }

    * {
        transition: all 1.3s;
    }
</style>

到此爲止,一個可以快速跳過動畫,可以暫停動畫,還有音樂播放,還能自由編輯代碼的會動的簡歷已經完成,代碼已上傳至git源碼,歡迎fork,也望不吝嗇star

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章