在網上看到一個這樣的網站,STRML它的效果看着十分有趣,以下圖所示:javascript
這個網站是用react.js
來寫的,因而,我就想着用vue.js
也來寫一版,開始擼代碼。css
首先要分析打字的原理實現,假設咱們定義一個字符串str
,它等於一長串註釋加CSS
代碼,而且咱們看到,當css
代碼寫完一個分號的時候,它寫的樣式就會生效。咱們知道要想讓一段CSS
代碼在頁面生效,只須要將其放在一對<style>
標籤對中便可。好比:html
<!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>
複製代碼
你能夠狠狠點擊此處具體示例查看效果。vue
當看到打字效果的時候,咱們不難想到,這是要使用間歇調用(定時函數:setInterval())
或超時調用(延遲函數:setTimeout())
加遞歸
去模擬實現間歇調用
。一個包含一長串代碼的字符串,它是一個個截取出來,而後分別寫入頁面中,在這裏,咱們須要用到字符串的截取方法,如slice(),substr(),substring()
等,選擇用哪一個截取看我的,不過須要注意它們之間的區別。好了,讓咱們來實現一個簡單的這樣打字的效果,以下:java
<!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
的值大於等於了字符串的長度以後,咱們須要清除定時器。定時函數看着有些不太好,讓咱們用超時調用結合遞歸來實現。react
<!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>
複製代碼
你能夠狠狠點擊此處具體示例查看效果。jquery
好了,到此爲止,算是實現了第一步,讓咱們繼續,接下來,咱們要讓代碼保持空白和縮進,這能夠使用<pre>
標籤來實現,但其實咱們還能夠使用css代碼的white-space
屬性來讓一個普通的div
標籤保持這樣的效果,爲何要這樣作呢,由於咱們還要實現一個功能,就是編輯它裏面的代碼,可讓它生效。更改一下代碼,以下:webpack
<!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>
複製代碼
你能夠狠狠點擊此處具體示例查看效果。c++
接下來,咱們還要讓樣式生效,這很簡單,將代碼在style
標籤中寫一次便可,請看:git
<!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">
<div id="app">
<p>{{ greeting }} world!</p>
</div>
</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
結合組件的方式來實現,這樣更簡單一些。好了,讓咱們開始吧: [1]: /img/bVbqXwx
新建一個vue-cli
工程(步驟自行百度):
新建一個styleEditor.vue
組件,代碼以下:
<template>
<div class="container">
<div class="code" v-html="codeInstyleTag"></div>
<div class="styleEditor" ref="container" contenteditable="true" @blur="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.loop = 'loop';
this.audio.autoplay = 'autoplay';
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.$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
。
在線預覽。
鄙人建立了一個QQ羣,供你們學習交流,但願和你們合做愉快,互相幫助,交流學習,如下爲羣二維碼: