作为一名懂前端的程序员,天天在嘴上谈样式,可是自己的个人博客网站却没有时间打理。就好像农民伯伯把最好的菜卖给别人,让自己的傻儿子却吃“长势不太好”的蔬菜,可农民伯伯其实是非常心疼自己的孩子的。
好了,废话不多说,先来看看成果吧。
样式借鉴了tower —— 一款团队任务管理的产品的样式,非常的简洁干净。然后,同时对移动端进行了适配:
简介
该博客是根据开源项目deepzz0/goblog修改而来。服务器端采用go语言,使用beego作为服务器端框架,前端采用bootstrap,采用golang模板技术,同时原项目使用了docker,但docker部分被我弃用了。
github地址:https://github.com/deepzz0/goblog
首先,让我介绍一下该项目的一些优势吧。
优势
- 功能齐全,基本可以满足个人博客的所有需求
- 运行在docker上,可以不关心操作系统的一些差异
- 数据库采用mongodb,更改数据库和表结构非常容易,而且向前兼容比较实现。
- 前端采用bootstrap,兼容移动端
- 采用beego和golang模板技术,而且开发时修改网页代码,刷新后立即见效,大大提高了开发效率。
- 配置文件齐全,可以高度定制自己的专属博客
- 后台管理功能齐全,同时有统计功能
- 博客采用
markdown
编辑
那么,有啥缺点呢?
缺点
- 界面有些丑陋
- 采用docker,没有安装docker,所以带来了一系列问题(主要还是环境变量已经文件路径的问题)
-
markdown
编辑不支持文件上传以及全屏编辑,且编辑器所依赖的库太久,有些markdown
语法不支持
总之,该项目非常值得借鉴,接下来就讲一下遇到的问题,以及解决的方案。
遇到的问题及解决方案
1. 环境变量
os.Setenv("MGO", "127.0.0.1")
由于之前采用docker:
ENV MGO 192.168.0.1
现在改如何转变呢?
首先是开发中,由于采用VSCode
编辑器,自然支持运行时支持环境变量的设置,launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch project",
"type": "go",
"request": "launch",
"mode": "auto",
"remotePath": "",
"port": 2345,
"host": "127.0.0.1",
"program": "${workspaceRoot}/src",
"env": {
"MGO": "127.0.0.1",
"CON_PATH": "${workspaceRoot}"
},
"args": [],
"showLog": true
}
]
}
其次,是在项目部署时,需要提前设置环境变量:
export MGO="127.0.0.1"
export CON_PATH="$HOME/git/goblog"
2. 让markdown
支持图片插入
首先要支持,图片的显示,这里就直接略过。
其次,需要能让编辑器插入图片文本:
$("#editor-area").insertAtCaret(
"![" + v.Name + "](" + v.Path + ")\n"
);
$("#editor-area").change();
这里#editor-area
是area
文本编辑框,后面调用change
事件,是为了该控件能够触发onChange
事件。
然后就是文件上传了,这里讲一下服务器是如何接上图片的:
type Response struct {
Status int
Data interface{}
Err Error
}
type Error struct {
Level string
Msg string
}
...
func NewResponse() *Response {
return &Response{Status: RS.RS_success}
}
func (m *MaterialController) Post() {
resp := NewResponse()
defer resp.WriteJson(m.Ctx.ResponseWriter)
flag := m.GetString("flag")
var allfiles = m.Ctx.Request.MultipartForm.File
var keys []string
var files []*multipart.FileHeader
for k, vals := range allfiles {
keys = append(keys, k)
files = append(files, vals...)
}
if !dir.IsExist(models.ResTmpPath) {
err := os.MkdirAll(models.ResTmpPath, 777)
if err != nil {
resp.Status = RS.RS_failed
resp.Err = helper.Error{Level: helper.WARNING, Msg: "临时目录创建失败。"}
return
}
}
// var retArray []interface{}
for i, h := range files {
f, err := h.Open()
defer f.Close()
if err != nil {
resp.Status = RS.RS_failed
resp.Err = helper.Error{Level: helper.WARNING, Msg: "文件上传失败。"}
return
}
path := models.ResTmpPath + "/" + h.Filename
dst, err := os.Create(path)
defer dst.Close()
if err != nil {
resp.Status = RS.RS_failed
resp.Err = helper.Error{Level: helper.WARNING, Msg: "文件上传失败。"}
return
}
io.Copy(dst, f)
logd.Infof("文件上传:%d,%s", i, path)
}
}
3. 关于文章摘要提取以及图片的提取
采用golang
的正则表达式来提取,正则表达式的妙用就不多说了,直接上代码。
import (
"fmt"
"regexp"
"strings"
"gopkg.in/russross/blackfriday.v2"
)
...
// 解析成html
p := bluemonday.UGCPolicy()
p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code")
html := string(p.SanitizeBytes(blackfriday.Run([]byte(markDownText))))
这是将markdown
文本转化为HTML
代码。
// 提取摘要
reg, _ := regexp.Compile(`<[^>]+>`)
pre := reg.ReplaceAllString(html, "")
rs := []rune(pre)
min := func(a int, b int) int {
if a > b {
return b
}
return a
}
l := 120
preview = string(rs[:min(len(rs), l)]) //+ "..."
if len(rs) > l {
preview = preview + "..."
}
这就是提取摘要的方法,其实就是去掉<HTML标签>
,然后加上省略号。
// 提取图片路径
var getURL = func(html string, num int) []string {
regURL := regexp.MustCompile(`<img [^>]*src="([^>"]+)"[^>]*>`)
var arr = regURL.FindAllSubmatch([]byte(html), -1)
var URLs = make([]string, 0)
for i, v := range arr {
if len(v) > 1 && i < num {
URLs = append(URLs, string(v[1]))
}
}
return URLs
}
imageURLs = getURL(html, 3)
这是提取HTML
中<img>
中的(最多3个)链接,不过这个是有问题的,HTML
代码的一些符号被转义了,如:< : <
,因此这里需要采用原生的markdown
文本来提取链接:[图片上传失败...(image-6e30dd-1552179914203)]
// 提取图片路径
var getURL = func(html string, num int) []string {
regURL := regexp.MustCompile(`![[][^]]*[]][(]([^()]*)[)]`)
var arr = regURL.FindAllSubmatch([]byte(html), -1)
var URLs = make([]string, 0)
for i, v := range arr {
if len(v) > 1 && i < num {
URLs = append(URLs, string(v[1]))
}
}
return URLs
}
imageURLs = getURL(markDownText, 3)
是不是[]()
有点傻傻弄不清呢,其实呢,这个只要多试几次,总能够找到提取的方法的,这个正则表达式的提取部分为:([^()]*)
,即小括号中的内容,只不过为了区分链接与图片链接,所以才这么多波折。哈哈,终于写成别人也看不懂的正则表达式了,好开森^0^。
4.关于markdown的“编译”
这里更新到了markdown
的最新的库,但是呢,功能还是有些偏弱。最典型的就是对表格的支持和对列的支持都偏弱。对于表格的支持:--
不能支持,只能写成---
;对于列的支持,必须换行,也就是上一行不能有内容。
所以,在js
层提交markdown
文本提交的时候做了一下处理,处理如下:
/**
* 修正md5部分代码无法解析的问题
*/
function correctionTopicMd5(e) {
var content = $(e).val();
if (!content) return;
// 修复表格无法解析的问题 以及列表需要换行的问题
content = content
.replace(/\n--\|/g, "\n---|")
.replace(/\|--\n/g, "|---\n")
.replace(/\|--\|/g, "|---|")
.replace(/\n(.+)\n([\-\*] )/g, "\n$1\n\n$2")
.replace(/([\-\*] .+)\n(.+)\n/g, "$1\n\n$2\n");
$(e).val(content);
}
correctionTopicMd5("#editor-area");
这里采用的是js
的正则表达式,有没有感觉正则表达式的妙用无穷呢?
嗯,为了加深正则表达式的印象,这里举几个栗子,关于正则表达式在VSCode
中重构代码时的使用吧。
5. 拓展:正则表达式的替换
换行缩进
查找:\n+
替换:\n
这个命令可以执行多次,最终的效果就是将多行空行转化为一行空行。
数组分段
将字母A,B,C,D,...,Z
按每行4列展开
解决方案:
查找:(([^,]+[,]){4})
替换:$1\n
Key-Value位置替换
{
int[] age,
long time,
string name
}
替换为
{
age: int[],
time: long,
name: string
}
解决方案:
查找:([\w\[\]]+) ([\w]+)
替换:$2: $1
常量替换
const RED:string = "red";
const YELLOW:string = "yellow";
const BLUE:string = "blue";
const BLACK:string = "black";
const WHITE:string = "white";
...
替换一系列常量:
原本:var color = RED;
目标:var color = tran(RED);
解决方案:
如果有前缀,会比较好处理,可是没有前缀怎么办呢?
查找:= (RED|YELLOW|BLUE|BLACK|WHITE)
替换:= tran($1)
去掉所有小数后面多余的0
0.0000100000
0.000 aaa
0.12300
0.bbb
0.00233
123000bb
1.000100vvv
替换为:
0.00001
0 aaa
0.123
0bbb
0.00233
123000bb
1.0001vvv
解决方案:
查找:(\.|(\..+?))[0]*([^0-9]*)$
替换:$2$3
只要找到待替换文本得异同,然后用正则表达式匹配出来,轻易就能够完成替换。值得注意的是:不要把非目标替换文本匹配进去。
6.一键切换网页模板
重构代码最最重要的原则就是随时可以终止。所以,一般我们在重构代码的时候,会设置一个开关,以便切换为原来的版本。
由于博客采用了新的样式,所以之前的页面不能用了,这时候就需要想办法,但是这样才能做到这么多网页一个个的修改呢。答案很简单,采用配置文件就行了。
这里展示一下我新增的网页配置文件吧,tmpcontroller.yaml:
mode: new
old:
page404: views/404.html
home: homelayout.html
homePage: homeTemplate.html
about: aboutTemplate.html
group: groupTemplate.html
login: login.html
message: messageTemplate.html
useragent: plugin/useragent.html
topic: topicTemplate.html
new:
page404: views/404.html
home: sp/homelayout.html
homePage: sp/homeTemplate.html
about: sp/aboutTemplate.html
group: sp/groupTemplate.html
login: login.html
message: sp/messageTemplate.html
useragent: plugin/useragent.html
topic: sp/topicTemplate.html
这样的话,只要改变mode
的值就可以切换页面的指向了,其实这也算是给博客定义多个主题了。至于怎样加载yaml
配置文件这里就不多讲了,毕竟想法更重要。
7.关于前端的优化
统计
首先是统计:
看的人不多,但是接入统计是非常有用的。这里接入的是google分析。
至于如何接入呢,其实很简单,不过最终是否成功,还在于你是否能够翻越那一道qiang。
首先就是去注册,网址:https://analytics.google.com/analytics/web/#
然后就是将代码嵌入到你的网页中:
<script>
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date(); a = s.createElement(o),
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
})(window, document, 'script', '/static/js/analytics.js', 'ga');
ga('create', '<你的ID>', 'auto');
ga('send', 'pageview');
</script>
<script type="text/javascript">
$('#btn-search').on('click', function () {
var content = $('#search-content').val();
if (content == "") {
pushMessage('info', "sorry|请输入你搜索的标题。")
return;
}
location.href = "/search?title=" + content;
});
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<你的ID>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', '<你的ID>');
</script>
这里的ID可以查询到,如果想进一步拓展,可以查看Google分析的文档。
分词与不分词
这个是什么意思呢?其实就是换行时,是否需要保持词语的完整性。比如,标签,那就不能让标签的字在换行时被拆开,这时候,应该采用如下样式:
.tag{
word-break: keep-all;
}
这样之所以采用keep-all
,主要是因为中文分词无效,就是单独的字。
然后,就是代码部分:<pre><code>...</code></pre>
,代码样式可以采用单词分词:
pre{
word-break: keep-all;
word-wrap: break-word; // 只对英文起作用,以单词作为换行依据。
white-space: pre-wrap; //只对中文起作用,强制换行。
}
当然如果不采用换行也是可以的,这样就需要支持横向滚动:
pre{
pverflow-x:auto;
}
关于文章图片的嵌入
查看博客图片样例,可以看到,图片其实是嵌入到文章的,那这是怎样做到的呢。
首先,是HTML
代码,记得图片一定要在文本内容前面哦。
<div class="topic">
<p>
<a class="img" href="{{.URL}}">
{{ range .ImageURLs }}
<img src="{{ . }}" />
{{end}}
{{.Preview}}
</a>
</p>
</div>
这是golang
的模板语法,.
代码当前元素。可以看到,图片是在文本内容{{.Preview}}
前面的。
那么接下来就是样式了。
.topic {
display: inline-block;
width: 100%;
}
.topic p {
margin: 6px 0px;
font-size: 13px;
line-height: 24px;
color: #999;
}
.topic a.img {
width: 100%;
text-decoration: none;
color: #666;
word-break: break-all;
}
.topic a.img img {
width: auto;
height: auto;
max-width: 25%;
max-height: 100px;
float: right;
overflow: hidden;
text-align: center;
background-color: #f0f0f0;
border-radius: 4px;
border: 1px solid #f0f0f0;
}
可以看到,图片采用了右浮动,另外宽高都是auto
,只限定了最大宽度和最大高度,这样的好处是,图片是等比例缩放的。
关于返回到顶部按钮
$(window).scroll(function () {
if($(window).scrollTop()>=100 && !$(".go-top").is(':visible')) {
$(".go-top").fadeIn().css("display","inline-block");;
}else if($(window).scrollTop()<100 && $(".go-top").is(':visible')){
$(".go-top").fadeOut();
}
});
$(".go-top").click(function(event){
$('html,body').animate({scrollTop:0}, 100);
return false;
});
这是返回顶部按钮的代码,但是呢,博客在移动端显示时,却出现按钮无法显示的问题,只要原因是移动端滚动层不再是全局。所以为了兼容移动端,添加了对移动端返回到顶部的支持:
function bindScroll(e) {
$(e).scroll(function() {
if ($(e).scrollTop() >= 100 && !$(".go-top").is(":visible")) {
$(".go-top")
.fadeIn()
.css("display", "inline-block");
} else if ($(e).scrollTop() < 100 && $(".go-top").is(":visible")) {
$(".go-top").fadeOut();
}
});
}
bindScroll(window);
bindScroll("#scroll-dev");
$(".go-top").click(function(event) {
$("html,body").animate({ scrollTop: 0 }, 100);
$("#scroll-dev").animate({ scrollTop: 0 }, 100);
return false;
});
好了,讲了这么多,接下来就再讲一点点吧。
那就是例图中的搜索,可以看见,没有搜索按钮,那怎么提交呢?其实很简单,只需要按回车就行了。
<form action="javascript:void(0)" id="search-content" method="GET">
<input type="text" placeholder="搜索文章" />
</form>
js
代码:
var content = $("#search-content input")
.eq(0)
.val();
if (content == "") {
alert("info", "sorry|请输入你搜索的标题。");
return;
}
location.href = "/search?title=" + content;
哈哈,如果你看这里,那么恭喜你,我已经没什么要讲的了。
有疑问加站长微信联系(非本文作者)