Markdown Previewer

前端工程展示页

项目介绍

源代码 + 效果图

See the Pen Challenges [07] by MaverickNone (@MaverickNone) on CodePen.


如果客官您觉得不够大,看着不太爽!请点击这里或者这边~


源代码解构

HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
!!! 5
%html(lang="en" )
%head
%meta(charset="UTF-8" )
%title
Test [07]
%script(src="https://kit.fontawesome.com/83ef1fc2b2.js" )
%link(rel="stylesheet" href="style.css" )
%body
#app
%script(src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js" )
%script(src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js" )
%script(src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.7.0/marked.min.js" )
%script(src="https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js" )
%script(src="script.js" )
  • 本代码由Haml编写
  • 添加库的时候,同时把ReactReact-DOM都加上
  • 这次渲染Markdown使用了GitHub上的Marked项目
  • 倒数第二个是freeCodeCamp的项目测评库
  • 其他没啥可讲的,都是基本功

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
@import url("https://fonts.googleapis.com/css?family=Russo+One");
@import url("https://fonts.font.im/css?family=Quantico");

$darkAccent: #09d5da;
$lightAccent: #2cda9d;
$backgroundBase: #5368a4;

$shadow: 1px 1px 15px 8px darken($backgroundBase, 20%);
$default-border: 1px solid darken($backgroundBase, 35%);

body {
background: url("https://i.loli.net/2019/08/14/GSTWDn2sqt3YL17.jpg") fixed;
background-size: 100%;
}

.colorScheme {
background-color: lighten($backgroundBase, 30%);
box-shadow: $shadow;
border-top-style: none;
}

.toolbar {
position: relative;
background-color: lighten($darkAccent, 25%);
padding: 4px 4px 3px 3px;
border: $default-border;
box-shadow: $shadow;
font-family: Russo One, sans-serif;
font-size: 15px;
i {
width: 25px;
color: black;
margin-left: 5px;
}
.fa-compress,
.fa-expand {
position: absolute;
right: -5px;
}
}

.fa-compress,
.fa-expand {
&:hover {
animation: btn 0.5s;
animation-fill-mode: forwards;
cursor: pointer;
}
}

@keyframes btn {
100% {
color: lighten($lightAccent, 10%);
}
}

.fa-chess {
margin-right: 3px;
}

.editorWrap {
width: 600px;
margin: 18px auto;
.toolbar {
position: relative;
right: 18px;
width: 100%;
padding-right: 36px;
border-radius: 5px;
}
textarea {
@extend .colorScheme;
width: 98.5%;
padding-left: 1vw;
min-height: 60vh;
margin-bottom: -5px;
resize: vertical;
outline: none;
padding-top: 5px;
font-family: Quantico, sans-serif;
font-size: 15px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
&::-webkit-scrollbar { width: 0 };
}
}

.previewWrap {
@extend .colorScheme;
width: 800px;
margin: 20px auto;
overflow-wrap: break-word;
padding-right: 20px;
border-bottom-left-radius: 25px;
border-bottom-right-radius: 25px;
.toolbar {
position: relative;
right: 12px;
width: 100%;
padding-right: 36px;
border-radius: 5px;
}
#preview {
margin-left: 5px;
margin-top: -10px;
width: 98.5%;
border-bottom-left-radius: 25px;
border-bottom-right-radius: 25px;
padding-left: 1vw;
}
}

@media screen and (max-width: 850px) {
.previewWrap {
width: 630px;
}
.editorWrap {
width: 550px;
}
}

.maximized {
width: 90%;
min-height: 90vh;
margin: auto;
textarea {
min-height: 90vh;
resize: none;
}
}

.hide {
display: none;
}

@media screen and (max-width: 650px) {
body {
margin: 5px 0;
}
.editorWrap {
width: 80vw;
margin: 0 auto;
}
.maximized {
width: 90%;
margin: auto;
}
.previewWrap {
width: 90vw;
#preview {
width: 100%;
img {
height: 200px;
}
}
}
}

// MARKDOWN STYLES
#preview {
blockquote {
border-left: 3px solid #224b4b;
color: #224b4b;
padding-left: 5px;
margin-left: 25px;
}

code {
background: rgba(9, 250, 255, 0.31);
padding: 1px 4px 2px 4px;
font-size: 12px;
font-weight: bold;
}

pre {
background: white;
padding: 5px 0 5px 5px;
}

h1 {
border-bottom: 2px solid $darkAccent;
}

h2 {
border-bottom: 1px solid $darkAccent;
}

table {
border-collapse: collapse;
}

td,
th {
border: 2px solid $darkAccent;
padding-left: 5px;
padding-right: 5px;
}
}
  • &::-webkit-scrollbar { width: 0 };这行代码隐藏了编辑框滚动条,该特性是非标准的,请尽量不要在生产环境中使用它!
  • 最后的Markdown预览框渲染的标签由Marked项目自动生成,这里只是添加了一点点渲染改进

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
marked.setOptions({
breaks: true,
smartLists: true
});
const placeholder = `# Welcome to my React Markdown Previewer!

## This is a sub-heading...
### And here's some other cool stuff:

Heres some code, \`<div></div>\`, between 2 backticks.

\`\`\`
// this is multi-line code:

function anotherExample(firstLine, lastLine) {
if (firstLine == '\`\`\`' && lastLine == '\`\`\`') {
return multiLineCode;
}
}
\`\`\`

You can also make text **bold**... whoa!
Or _italic_.
Or... wait for it... **_both!_**
And feel free to go crazy ~~crossing stuff out~~.

There's also [links](https://www.freecodecamp.com), and
> Block Quotes!

And if you want to get really crazy, even tables:

Wild Header | Crazy Header | Another Header?
------------ | ------------- | -------------
Your content can | be here, and it | can be here....
And here. | Okay. | I think we get it.

- And of course there are lists.
- Some are bulleted.
- With different indentation levels.
- That look like this.


1. And there are numbererd lists too.
1. Use just 1s if you want!
1. But the list goes on...
- Even if you use dashes or asterisks.
* And last but not least, let's not forget embedded images:

![React Logo w/ Text](https://goo.gl/Umyytc)
`;
  • marked.setOptions()里面是Marked的配置代码
  • placeholder里面是默认的输入待转化字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Toolbar = (props) => {
return (
<div className="toolbar">
<i className="fas fa-chess" />
{props.text}
<i onClick={props.onClick} className={props.icon} />
</div>
);
};
const Editor = (props) => {
return (
<textarea
id="editor"
value={props.markdown}
onChange={props.onChange}
/>
);
};
const Preview = (props) => {
return (
<div
id="preview"
dangerouslySetInnerHTML={{
__html: marked(props.markdown)
}}
/>
);
};
  • 这部分抽离了一些React组件
  • dangerouslySetInnerHTML={{ __html: marked(props.markdown) }}因为不合时宜的使用innerHTML可能会导致**cross-site scripting (XSS)**攻击。 净化用户的输入来显示的时候,经常会出现错误,不合适的净化也是导致网页攻击的原因之一。所以,dangerouslySetInnerHTML这个 prop 的命名是故意这么设计的,以此来警告,它的prop值( 一个对象而不是字符串 )应该被用来表明净化后的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
markdown: placeholder,
editorMaximized: false,
previewMaximized: false
};
this.handleChange = this.handleChange.bind(this);
this.handleEditorMaximize = this.handleEditorMaximize.bind(this);
this.handlePreviewMaximize = this.handlePreviewMaximize.bind(this);
}
handleChange(e) {
this.setState({
markdown: e.target.value
});
}
handleEditorMaximize() {
this.setState((prevState) => {
return {editorMaximized: !prevState.editorMaximized};
});
}
handlePreviewMaximize() {
this.setState((prevState) => {
return {previewMaximized: !prevState.previewMaximized};
});
}
render() {
const classes = this.state.editorMaximized
? ["editorWrap maximized", "previewWrap hide", "fas fa-compress"]
: this.state.previewMaximized
? ["editorWrap hide", "previewWrap maximized", "fas fa-compress"]
: ["editorWrap", "previewWrap", "fas fa-expand"];
return (
<div>
<div className={classes[0]}>
<Toolbar
icon={classes[2]}
onClick={this.handleEditorMaximize}
text="MaverickNone's Editor"
/>
<Editor
markdown={this.state.markdown}
onChange={this.handleChange}
/>
</div>
<div className={classes[1]}>
<Toolbar
icon={classes[2]}
onClick={this.handlePreviewMaximize}
text="MaverickNone's Previewer"
/>
<Preview markdown={this.state.markdown} />
</div>
</div>
);
}
}

ReactDOM.render(<App />, document.getElementById("app"));
  • 这里使用了三目运算符,根据React保存的状态来判断是隐藏还是显示某组件(通过选择class的方式),工具栏应该用什么图标
  • 三目运算符后面的数组用的非常巧妙,应该注意一下

项目总结

  1. 这次项目深化了React熟练度,以及如何配合React来动态渲染CSS(比如:如何隐藏组件)
  2. 练习了React的代码风格之一,组件抽离
  3. 这次尝试使用了Marked项目,该项目非常好用,值得推荐和收藏
  4. 学习了如何隐藏滚动条