共计 11753 个字符,预计需要花费 30 分钟才能阅读完成。
在网上看到一个这样的网站,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”>
<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 结合组件的方式来实现,这样更简单一些。好了,让我们开始吧:
新建一个 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。
在线预览。