跳到主要内容

从0到1搭建个人博客(前端篇)

阅读需 15 分钟

前端 - Client 篇

一. flag

  • 开启打包分析:webpack-bundle-analyzer
  • 开启 gzip
  • 开启库 cdn
  • 图片 cdn
  • webpack 按需加载
  • SSR

二、交互

  • 向上滚导航显示,向下滚导航消失
  • 页面滚动一定距离后,向上按钮出现
  • 屏幕缩小出一个小按钮弹出目录

三. 解决的问题

1. vue-cli3 安装错误

注意,需设置 npm 源,改为淘宝源后错误减少,安装成功。

npm audit fix,npm@6之后 https://docs.npmjs.com/cli/audit

2. build 之后空白页

打开生成的 index.html 后,你会发现是空白,检查代码后,你会发现代码是“/app.js"这样的,部署到 http 服务器就好了,比如 nginx 。

在本地 docker run 一个 nginx 容器,非常方便。

3. vue.config.js

对于 vue.config.js 的相关配置,可以在 “node_modules/@vue/cli-service/lib/option.js” 中查看其结构以及默认配置。配合文档 Vue CLI 配置参考 阅读,效果更加。

其中 chainWebpack 的配置,在 vue inspect 这个命令输出的文件中,看看代码和注释就明白了。

webpack 相关 | 审查项目的-webpack-配置

chainWebpackOutput

4. 初步优化

首先配置:babal+router+vuex+eslint,均在 Chrome 无痕模式下。

5. 文件压缩

build 之后的文件是压缩的,不需要多加配置。

6. 安装 webpack-bundle-analyzer

① 其实在当前文件夹目录下输入命令,构建后会自动生成一个 report.html。不需要安装此插件。

npx vue-cli-service build --report

相关资料:vue-cli-service-build,看文档的时候,看到这个 --report 参数就感觉是这个,果然 😁

具体逻辑代码:

② 上述方式需要自己打开文件,可通过一些步骤实现自动化。既然已经使用了 vue-cli3,具体包也有,或许改改哪里的配置就好?尝试不安装此插件,就地取材试试。

Github issue:vue-cli-service build --report #998

"scripts": {
"report": "vue-cli-service build --report"
},

③ 直接安装插件:重启 Server 后会启动一个小服务器(端口),自动弹出分析页面。安装方式:

会影响 docker-compose up - -build 构建,需注释掉,建议更加细分环境。

vue-cli3.0配置 webpack-bundle-analyzer 插件

https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/README.md

npm intall webpack-bundle-analyzer –D

// vue.config.js 内添加
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
configureWebpack = {
return { // 返回一个将会被合并的对象
plugins: [
new BundleAnalyzerPlugin()
]
}
}

// 或者这样添加:
chainWebpack: config => {
config
.plugin('webpack-bundle-analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
}

npm run serve
localhost:8888

7. 批量导入和备份 CDN

  • JS CDN
  • CSS CDN

发现某地方可以存数据,同时考虑到 CDN 失效的问题,并结合其他方案,设计了这样的结构。

externals

名字不能自定义,其中 key 值是包名,value 值是其导出的方法名称。

小技巧:在浏览器 Console 中,可测试。

// vue.config.js
configureWebpack: config => { // 此时 configureWebpakc 为函数
config.externals = {
'vue': 'Vue', // import Vue from 'vue'
'vue-router': 'VueRouter', // import VueRouter from 'vue-router'
'vuex': 'Vuex' // import Vuex from 'vuex'
}
}

此外如果遇到 "Vue is not defined" 等等. 请将 <script> 放在 index.html <head></head> 内部,而不是 body 后面。这是血的教训 😭 。

vueIsNotDefined

配置

vue.config.js 内配置数据:

module.exports = {
pages: { // 对象的 key 是入口名字,比如这里的 index, subpage
index: { // 此处是 HtmlWebpackPlugin 的参数,由此设想 CDN 存放位置
entry: 'src/main.js',
...
cdn: {
css: [],
js: [
{
name: externals['vue'], //将 externals 数据提出了
url: 'https://cdn.bootcss.com/vue/2.6.10/vue.runtime.min.js',
backUpUrl: '/js/vue@2.6.10.runtime.min.js'
}, ....

模板

publicindex.html 的模板:

<head>
...
<!--引入 CDN,如果 CDN 挂掉就用本地文件-->
<% if (process.env.NODE_ENV === 'production') { %>
<% for(let js of htmlWebpackPlugin.options.cdn.js) { %>
<link href="<%=js.url%>" rel="preload" as="script">
<script src="<%=js.url%>"></script>
<script>
if (typeof window['<%=js.name%>'] == 'undefined') {
document.write("<script src='<%=js.backUpUrl%>'><\/script>")
}
</script>
<% } %>
<% } %>
</head>

构建生成效果

build 构建之后的效果:

批量导入和备份cdn

网络加载情况

无CDN:

无CDN情况

有CDN:

cdn后网络加载

8. 开启 gzip

  • 最小压缩值的最优解

compression-webpack-plugin

前端性能优化之gzip

const CompressionPlugin = require('compression-webpack-plugin')
...
configureWebpack: config => { // object | Funtion,此时 configureWebpack 为函数
config.externals = externals // 直接修改
return { // 返回一个将会被合并的对象
new CompressionPlugin({ // gzip 的设置
threshold: 8192 // 只有大于此大小的文件才压缩,字节为单位
})
}
}

nginx

ngx_http_gzip_module

gzip on;
gzip_comp_level 4;
gzip_disable "MSIE [1-6]\.";
gzip_min_length 8192;
gzip_types application/javascript test/html text/css application/json image/svg+xml application/xml application/xhtml+xml text/plain application/x-javascript text/javascript;
gzip_static on;

网络加载情况

Gzip

其他

本想测试最小压缩值最优为多少,咳咳, 不太好测试,测试数据不足。暂时先设置 8KB 了(Github文档上设置的是8KB)。

Window 下创建固定大小文件:

fsutil file createnew C:\test.js 10240

一段 bat 脚本,不过都是 0,压缩后基本一样大:

// 当前目录下建立 1KB - 10KB 的文件
for /l %%i in (1024,1024,10240) DO fsutil file createnew %cd%\test%%i.js %%i

9. 开启 Brotli

  • 需要知道在 gz 和 br 文件中,谁先返回。应该是 br。

与 Gzip 相同,都是用于压缩,添加方式相同。

const BrotliPlugin = require('brotli-webpack-plugin')
...
configureWebpack: config => {
return {
new BrotliPlugin({
threshold: 8192
})
}
}

nginx.conf:

brotli on;
brotli_comp_level 5;
brotli_min_length 8192;
brotli_types application/javascript test/html text/css application/json image/svg+xml application/xml application/xhtml+xml text/plain application/x-javascript text/javascript;
brotli_static on;
brotli_window 512K;

10. Vuetify - UI 框架

vue-cli-plugin-vuetify

默认选项即可,会自动生成 src/plugins/vuetify.js 和修改对应文件;

记得在 plugins 内加上 new VuetifyLoaderPlugin()

相关:'installComponents' has already been declared

//vuetify.js
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/src/stylus/app.styl'
import 'material-design-icons-iconfont'
Vue.use(Vuetify)

//index.html
<link rel="stylesheet" href="https://cdn.bootcss.com/vuetify/1.1.15/vuetify.min.css">

② 安装 material-design-icons-iconfont 开发依赖,Material Design 图标;

https://vuetifyjs.com/en/framework/a-la-carte#vuetify-loader

//vuetify.js
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/src/stylus/app.styl'

// 图标这里还有些问题
Vue.use(Vuetify, {
iconfont: 'md' // 'md' || 'mdi' || 'fa' || 'fa4'
})

滚动页面-组件显示与隐藏

vuetify 官方文档上的检测滚动事件示例并不成功,只写 v-scroll 就好。

<v-btn v-show="show" v-scroll="onScroll">按钮</v-btn>

小坑

文档应该是描述失当: https://vuetifyjs.com/en/framework/a-la-carte#vuetify-loader>

正确做法: https://github.com/vuetifyjs/vuetify-loader/issues/20

建议

这个框架很不错的,值得学习。多看文档,每个例子代码都理解下。有什么不会的去 Github issues 里面搜索。

11. Markdown

选择的是 marked.js 库,配合 highlight.js 进行代码高亮。

详细请查看项目:markdown-toc,相关代码。

  • 生成对应目录
  • 美化显示效果
  • 文章锚点在哪里,目录哪个标签高亮

TOC

考虑的情况:标题并不是按顺序写的,也就是说,顺序是乱的:开局一个二级标题,接下来就是五级标题,三级标题这样。

规则:

* [index].level==当前level:栈顶如果是</li>,则弹出栈;输入<li>+text;推</li>入栈;index++
* [index].level>当前level:如果栈顶是</ul>,则输入<li><ul>,推</li>入栈,推</ul>入栈;如果栈顶不是</ul>,则输入<ul>,推</ul>入栈;level++
* [index].level<当前level:弹出 [(当前level-匹配level) * 2+1] 个值出栈;level=[index].level。
* index 达到最大,栈内数据全弹出。

美化

zhangjikai/markdown-css 的基础上,结合 github-markdown-css 进行配置。

  • 修改与整理:.markdown-body
  • 目录样式:自行添加了 .markdown-toc
  • 自适应:px, em, rem:lib-flexible和px2rem-loader

联动-对应目录条高亮

  • 对应目标条标题高亮:根据 md 中有锚点的标题与生成的 toc 目录数量相同,所以只需要一个监听,按照顺序递增或者递减即可。
  • Vue 无法读取 HTMLCollection 列表的 length

mounted 不会承诺所有的子组件也都一起被挂载。地址

  • 获得元素距离浏览器顶部距离(不是页面滚动距离)
w// 其中的 top 属性,超过则为负
getBoundingClientRect()
  • style.color 后,hover 消失:!important
  • 刷新页面后,index 变为原始值,需要根据 URL 初始化 index。
  • 目录过长,对应目录不在可视区内,滚动
  • 页面向下滚动,当导航消失不见时,目录紧贴页面上端

有关 CSS

在页面右侧设置了目录,有以下需求:

  • 高度根据屏幕可视区域自适应
  • 内部可滚动:右侧滚动条 auto
  • 吸顶效果:导航不可见时,自动向上移动一定距离
  • 右侧滚动条出现时,紧贴屏幕右侧

目录吸顶效果

采用的是 flex 布局,12 栅格。

<v-layou>
<v-flex xs11 offset-xs1>
<v-layout>
<v-flex md8> md </v-flex>
<v-flex md4> toc </v-flex>
</v-layout>
</v-flex>
</v-layout>

以下两钟方法看自己取舍,我根据目前的需求,采用了 fixed 的方式。

① position: fixed

postion 设置为 fixed,则其高度为 window.innerHeight ,宽度为 window.innerWeight

position: fixed; 
height: 70%;
overflow-y: auto;
width: calc(30.5555%);

toc 目录区域的宽度计算:

100 % *11 / 12))*4 / 12// 30.555···%

吸顶效果:

// v-Scroll:监听滚动条滚动
let nowTop = document.documentElement.scrollTop + document.body.scrollTop;
this.oMarkdown.chain.tocDOM.style.marginTop = nowTop > 64 ? "-64px" : "0"

② postion: sticky

它的吸顶效果是天生的,无需监听滚动条,会自动吸顶。需要注意兼容性。

position: sticky;
top: 3em;

高度是容器的高度,无法根据屏幕来自适应,需要计算。

// 初始化设置高度,且 v-resize:监听页面缩放
height = window.innerHeight * 0.7 + "px"

宽度是容器的宽度,计算比较方便,但吸顶后会同 position: fixed,需要重新设置宽度,这好像也得监听,要不怎么知道吸顶了...

width: calc(100%);
// 吸顶后需要重新设置宽度
width: calc(30.5555%);

12. 懒加载

v_layzy

13. 刷新空白

history-mode.html#后端配置例子/nginx,加上 try_files 即可

location / {
root /var/www/website/client/dist; # html 文件夹位置
try_files $uri $uri/ /index.html; # 解决路由 history 模式下刷新 404
}

14. 交互

  • 向上滚导航显示,向下滚导航消失

  • 页面滚动一定距离后,向上按钮出现

  • 屏幕缩小出一个小按钮弹出目录

有时间把网站统计加进去。

滚动页面-组件显示与隐藏

vuetify 官方文档上的检测滚动事件示例并不成功。只写 v-scroll 就好。

<v-btn v-show="show" v-scroll="onScroll">按钮</v-btn>

onScroll() {
let top = document.documentElement.scrollTop + document.body.scrollTop
this.show = top > 150 ? true : false
}

跳转 && 滚动

  • 根据 URL 地址跳转,如果有 # ,则滚动至对应锚点位置(文章页)
  • 浏览文章内容时:
    • 对应目标标题高亮
    • 若目录标题过多:
      • 目录区域会自动滚动
      • 高亮标题置于中间(首尾除外)
  • 目录区域可手动滚动
  • 点击文章目录标题后:
    • 页面滚动至对应文章内容
    • 目录标题颜色切换
    • 修改 window.location.hash
  • 均平滑滚动
  • 无标题情况
  • 滚动兼容?

小坑:如果不设置延时,根本不会跳转。具体原因暂时不明。以及 scrollIntoView 被目录区域滚动条滚动所影响,换成了 window.scrollTo

参考:From testing, without a brief timeout, it won't work.

mounted() {
if(this.$route.hash) {
setTimeout(()=> {
let anchor = decodeURI(this.$route.hash).slice(1)
document.getElementById(anchor).scrollIntoView()
}, 1000)
}
}

同时如果给 scrollIntoView 设置缓慢动画,但滚动距离太远,它就不会完全到达。

似乎是 window.location.hash 与 scrollIntoView 之间的冲突。或许 scrollIntoView 是 window.location.hash 的补充?加了动画?

并不改变 URL

中途 hash 改变会停止

anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
})
setTimeout(()=> {
window.location.hash = this.getAttribute('href')
})
})

通过属性来传递值。index

如果 addEventListener,()=>传递当前的this,如果是匿名函数里面就是它自己的this,可以获取属性,所以很蛮烦。两个不互通。 只能通过匿名函数获得属性再在里面调用函数传递。

this.tocDOM.querySelectorAll('a[href^="#"]') // NodeList
this.tocDOM.getElementsByTagName('a') // HTMLCollection
offsetTop // 它距离父元素顶部的距离
getBoundingClientRect().top // 距离相对于浏览器顶部的距离

15. npx

npmv5.2 版本增加。

目标:调用项目内部安装的模块

16. 部署 url

本地其服务没问题,但是 dist 之后请求的 localhost:3000 就会出现问题,需要配置 nginx,修改 axios。

  • PC 端访问没问题,移动端有问题(请求不到数据,请求的是 localhost:3000)

17. Chrome://inspect调试

  • 科学上网
  • USB 调试

18. 跨域

process.env.NODE_ENV

  • .env.production
  • .env.development

axios, nginx

在本地,直接访问 localhost:3000 获取 api,在服务端,接口请求 /api/,直接在路由加 /api/就是了

写个express Router的小demo

本地服务没问题,但是 dist 之后请求的 localhost:3000 就会出现问题,需要配置 nginx,修改 axios。

pc跨域成功,移动端不可以,感觉是请求地址发生的问题,访问的是 /post/,而不是设置的 /api/post

移动端失效 PC 正常访问

其实这个问题并不是跨域导致的,而是请求路径。

在解决跨域问题时,用真机测试发现的问题。通过此问题对 Nginx 和网络熟练了些~

① 场景:通过 ipconfig 找到 IPv4 地址,手机访问 192.168.1.123 打开页面

无线局域网适配器 WLAN192.168.1.123

问题:手机获取不到 api 的数据,请求 apiaxiosbaseURLlocalhost:3000/api

关键:手机访问的 localhost 并不是电脑端的 localhost !手机有自己的 localhost

解决:将 localhost:3000/api 改为 192.168.1.123:3000/api

参考:vue运行之后,真机无法获取数据!在手机上发送不出去axios 或者 ajax,请求失败

② 场景: Nginx 配置反向代理,将 evgo2017.com/api 转发至本机 3000 端口

问题:localhost127.0.0.1 均无效,内网 ip 有效

关键:用的是 Docker 啊亲

解决:写作容器名称或采用内网 ip

location /api {
proxy_pass http://evgo-server:3000;
}

如果有配置 HTTPS

① 复制一份 proxy 过去;

proxy_pass 请求协议为 http ,不需要改成 https

③ 建议 axiosbaseURL 请求协议改为 https

此外,下面两个地址是不一样的,注意 / !

proxy_pass http://localhost:3000;
proxy_pass http://localhost:3000/;

19. require

关于Node.js的__dirname,__filename,process.cwd(),./文件路径的一些坑

fs.readFile 能请求到文件,但是 require 就不可以

20. axios

配置

异步

21. 向上按钮可拖动 - 指令

功能:

  • 点击向上滚动

  • 可拖拽

    • PC 端 drag
    • 移动端 touch
    • 不可超出可视范围
  • 屏幕尺寸变小:

    • 按钮位置不在可视宽度内,修改其宽度
    • 按钮位置不在可视高度内,修改其高度
  • 浏览器效果测试

  • 整理元素宽高距离等

  • 盒子模型

推动效果:el.style.left = e.clientX + 'px', 鼠标推着走

① 推动太慢:原生JS快速拖动元素失效问题

mousemove 绑定在 body 或 document 上,会很流畅,而 div 自己身上会跟不上

② 移动卡顿:v-btn class 的问题

自己使用 div 模拟了此按钮

由知乎的下一个回答按钮想到的

https://juejin.im/post/5afbe5ad51882542ba07f21f

https://cn.vuejs.org/v2/guide/custom-directive.html#ad

代码复用和抽象的主要形式是组件

对普通DOM进行底层操作->自定义指令

推动效果:el.style.left = e.clientX + 'px', 鼠标推着走

推动太慢:

mousemove 绑定在 body 或 document 上,会很流畅

而 div 自己身上会跟不上

https://segmentfault.com/a/1190000015313127

触摸事件:

touchstart, move,cancel

拖动时会滚动屏幕:

chrome 默认 passive: true,忽略 event.preventDefault(),提高性能

css:style="touch-action: none;"

document.addEventListener('touchmove', callback,  {passive: false})

但是禁止滚动后,移动图标会很慢

我以为是著名的300ms问题,结果经过测试

无论禁止不禁止滚动,每次移动的间隔时间基本都差不多

那么是设置left的问题?

结果显示时间戳差值不过1

touchmove 是在不停的计算的,没一点问题

甚至差值很大的时候也很快设置好了

但是为什么显示这么慢?去掉passive: false就很快?

我突然想到,e.preventDefault();

禁止了

那么原先的是否有动画?

慢速的移动并没有什么问题,突然一大截的距离,

没有缓冲的感觉,直接就过去,这不就会表现出“卡顿”?

此外原先是有 click 事件的(回到顶部)

在手机上绑定了 touch 事件,300ms,触发

https://blog.csdn.net/u014346301/article/details/53536714

可能是 chrome 的问题!用手机的打开这个页面没问题!

用可以正常移动的代码测试,变得很卡了!

在APP.vue里面测试div2,正常,但加到组件TheGoTop就慢了,注销掉组件GoTop内部,正常了,最后问题在于 v-btn!

似乎是 v-btn__content class 的问题!加上去就卡了!

mouseup 等会触发 click

为了拖动换成了 drag

切记在元素上加上 "draggable='true'"

移动位置,缩小屏幕,不见

vue 获取 dom

ref='btn'
this.$refs.btn.style.left = "0px"
this.$refs.btn.style.top = "16px"

inherit

字面量声明的当时是无法获取到对象的。

获取滚动条宽度

window.innerWidth - body.clientWidth 是滚动条宽度,无论是否有y轴滚动条

但是 window.innerHeight - body.clientHeight 却不是,收到x轴滚动条影响

22. 各元素宽高

document.body.scroll\client\offset

window.inner\outer

screen.width\availWidth

document.scrollingElement.clientWidth

滚动条:window.innerWidth - document.scrollingElement.clientWidth

23. lint Error ELIFYCYCLE

出现这个报错,但是不合规范的代码依旧被美化了,说明它是在工作的。

https://github.com/vuejs/vue-cli/issues/3224

经过查询,发现也许是我的代码有问题,出现了 ESLint 不可修复的问题。

改掉之前所有的报错后,在 plugins 内加上 new VuetifyLoaderPlugin(),之前以为是不需要加的,今天再看了看文档,昂,我错了。

之后所有错误都没啦,ELIFYCYCLE 也没有了。

暂时未加入评论功能,请在对应公众号文章下或 GitHub Issues下留言反馈。