# 2020年02月技术日常
# 2020/02/29 周六
# 添加到我的小程序引导tips被原生组件遮挡的问题
在小程序里,为了增加用户留存,会做一个引导用户添加到我的小程序的提示面板
今天自己实现了下,发现原生组件遮挡了这个提示,貌似暂时没有很好的解决方法
所以,当设计小程序UI时,尽量不要在顶部使用原生组件。
参考:
# border三角形边框问题
在给小程序添加引导时,里面有个带边框的三角形,如下图
一般用css画三角形使用的是border,但三角形边的边框怎么画呢?一般用两个三角形叠加来实现
<view class="add-to-mymptips">
<view class="atm-angle-a"></view>
<view class="atm-angle-b"></view>
<view class="atm-main">
点击 <image src="/images/three_point.png"></image> 添加到我的小程序,微信首页下拉即可快速访问小程序
</view>
</view>
来看css样式
.add-to-mymptips {
position: absolute;
right: 15px;
width: 270px;
margin-top:15px;
box-sizing: border-box;
}
/* 主内容区域 */
.atm-main {
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 0 10px #ccc;
color:rgb(53, 53, 53);
}
/* 三个点图片样式 */
.atm-main image {
width: 33px;
height: 15px;
}
/* 三角形+边框 区域 */
.atm-angle-a, .atm-angle-b {
position: absolute;
margin-left:200px;
width: 0;
height: 0;
border: 10px solid;
}
.atm-angle-a {
top: -20px;
border-color: transparent transparent #ccc;
}
.atm-angle-b {
top:-19px;
border-color: transparent transparent #fff;
}
参考:
# 2020/02/27 周四
# el-form-item里非elment输入组件时,校验回调函数不触发的问题
今天写表单校验规则,有个 el-form-item 里使用了富文本编辑器,发现校验规则校验这个值会有异常:
- 当 change 或 blur 时,根本没有触发校验(提示错误)
- 提交表单时,当该字段校验失败会提示错误,但该字段符合要求时,validate的回调一直没触发,导致无法进行校验成功之后的下一步操作
将富文本编辑器换成 el-input 正常,换成普通的 input 也会异常,感觉一头雾水。
使用 this.$refs.ruleForm.validateField('xxx') 单读校验也不行,这个应该是element表单输入组件特有的
于是粗略看了下源码,发现错误信息在form-item(也就是el-form-item)组件里处理,当el-select或el-input值改变时会将事件传递给form-item
两个不同的组件,一个组件里怎么捕获到另一个组件的事件呢,在element内部使用了发布订阅设计模式来处理:
- 在form-item里订阅事件
- 当el-input或el-select等elemnt表单输入组件的值改变时,发布事件
来看源码
// 在 form-item 里订阅事件
// https://github.com/ElemeFE/element/blob/1.x/packages/form/src/form-item.vue
if (rules.length || this._props.hasOwnProperty('required')) {
this.$on('el.form.blur', this.onFieldBlur);
// 订阅了el.form.change事件
this.$on('el.form.change', this.onFieldChange);
}
// 当 el-input 值改变时,发布事件
// https://github.com/ElemeFE/element/blob/1.x/packages/input/src/input.vue
// el-input
setCurrentValue(value) {
if (value === this.currentValue) return;
this.$nextTick(_ => {
this.resizeTextarea();
});
this.currentValue = value;
if (this.validateEvent) {
// 发布el.form.change事件
this.dispatch('ElFormItem', 'el.form.change', [value]);
}
}
// el-select
// https://github.com/ElemeFE/element/blob/1.x/packages/select/src/select.vue
value(val) {
if (this.multiple) {
this.resetInputHeight();
if (val.length > 0 || (this.$refs.input && this.query !== '')) {
this.currentPlaceholder = '';
} else {
this.currentPlaceholder = this.cachedPlaceHolder;
}
}
this.setSelected();
if (this.filterable && !this.multiple) {
this.inputLength = 20;
}
this.$emit('change', val);
this.dispatch('ElFormItem', 'el.form.change', val);
}
所以,如果表单里使用了非element输入组件,比如普通的input,当值改变或输入框失去焦点时,没有发布对应的事件,那么form-item组件就不会触发校验
怎么来处理呢?这里暂时采用自定义方法来处理:
- 从rules移除对应的字段 required,至于label前面的红星,直接在el-form-item__label类上加一个before属性来设置
- 对于行内显示错误信息,可以在el-form-item__label的after里显示错误信息,通过一个父类的error-class来控制隐藏显示
- 单独写校验逻辑,如果校验失败加上一个error-class,如果想做的更逼真一点,在错误时给输入组件加一个红色的border
# element怎么动态改变校验rules且实时生效
需要动态改变rules场景,有两个功能点:
- 某个checkbox的值改变,有部分字段需要在必须和可选间切换
- 某个cascader组件值改变时,需要动态切换部分字段(有删有减)
需要注意的地方
- 可选和必选切换,只需要改变rules里的require属性,true和false之间切换(我之前直接暴力删rule里的fields,这种方法不能关闭原来必选时触发的错误提示)
- 对于动态修改rules后,必选的小红星以及之前的错误信息还在的问题,需要完全改变rules的值,才能重新触发校验,使前端页面更新
// 强制触发表单校验更新
this.rules = JSON.parse(JSON.stringify(this.rules))
# 2020/02/26 周三
# 一条命令安装多个npm包
# 安装多个包koa、koa-router、koa-static
npm install koa koa-router koa-static --save
# npm notice created a lockfile as package-lock.json. You should commit this file.
# npm WARN upload@1.0.0 No description
# npm WARN upload@1.0.0 No repository field.
# + koa-router@8.0.8
# + koa@2.11.0
# + koa-static@5.0.0
# added 61 packages from 29 contributors in 22.996s
# kevindeMacBook-Air:upload上传进度 kevin$
如果需要卸载已经写入package.json里的npm包
npm uninstall 对应的包 --save
# axios文件上传进度及后台接收demo
axios的config参数里,可以传入onUploadProgress参数来接收upload进度事件,在koa处理时使用 post-bodyparser 可以很好的解析 multipart/form-data 数据
来看源码:
<!-- 前端HTML -->
<body>
<div>
<input type="file" name="file" id="test">
<div id="progressDiv"></div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
let fileInput = document.getElementById('test')
let progressDiv = document.getElementById('progressDiv')
// 当input文件输入框值改变时
fileInput.onchange = () => {
let file = fileInput.files[0]
this.uploadFile(file)
}
// 上传文件到后台
async function uploadFile(file) {
let fd = new FormData()
fd.append('file', file)
fd.append('type', 'mask')
try {
let payload = fd
let res = await axios.post('/upload', payload, {
// axios 接收进度事件文档
// https://github.com/axios/axios#request-config
onUploadProgress: function (progressEvent) {
// {loaded: 1687552, total: 35353356, ...}
console.log('接收到进度事件', progressEvent)
progressDiv.innerHTML = `
<div>上传中,当前进度:${((progressEvent.loaded / progressEvent.total) * 100).toFixed(2) }% </div>
<div>文件大小: ${progressEvent.loaded}/${progressEvent.total}
`
},
})
console.log(res)
} catch(e) {
cosnoel.error(e)
}
}
</script>
</body>
koa后端接收处理 upload.js
const Koa = require('koa')
const Router = require('koa-router')
const static = require('koa-static')
const BodyParser = require('post-bodyparser')
const router = new Router()
const app = new Koa()
app.use(static(__dirname + '/'))
router.post('/upload', async (ctx, next) => {
console.log('upload', ctx.url)
let { req } = ctx.request
const parser = new BodyParser(req);
let body = await parser.formData()
console.log(body)
ctx.body = body
})
app.use(router.routes())
app.listen(3000)
// 打印内容
// upload
// /upload
// { file:
// { value:
// '/var/folders/mw/hbp6ytc9753gcm3zhqbmfkp40000gn/T/RAzvcR/9cd892b3-3243-4469-8cfa-ecbe2190a6ee.mongodb-macos-x86_64-4.2.2.tar',
// name: 'file',
// filename: 'mongodb-macos-x86_64-4.2.2.tar',
// contentType: 'application/x-tar' },
// type: 'mask' }
完整demo,参见: upload文件上传demo - github (opens new window)
# koa ctx.body 写在异步里接口会返回404
今天mock上传的接口时,发现总是404,刚开始以为是代理的问题,后来单独写了个demo,发现如果ctx.body放在异步的回调,后端接收到请求了,但还是会返回404
// 最简单的验证方法
setTimeout(() => {
ctx.body = {} // 只要是异步,前端就会返回404
}, 0)
怎么解决呢?只要 await 对应的异步就好了
// 将文件数据接收,放到Promise里然后await,这样前端就不会404了
let data = await getUploadData(req)
ctx.body = {}
// 用 promise 封装一层
function getUploadData(req) {
return new Promise((resolve, reject) => {
let chunk = []
let size = 0
req.on('data', (data) => {
console.log('data', data)
chunk.push(data)
size += data.length
})
req.on('end', () => {
console.log('end')
const data = Buffer.concat(chunk, size)
resolve(data)
})
})
}
# 2020/02/24 周一
# element cascader高度过长问题
当cascader里选项比较多时,组件高度会异常,主要是 .el-cascader-menu__wrap
这个样式高度为100%,将cascader里其任意一父元素手动指定高度即可,但el-cascader-menu__wrap设置的效果最好
.el-cascader-menu__wrap {
max-height: 300px;
overflow: scroll;
}
但这样设置后,可能会影响全局,这个是直接挂载在body元素下的,对其他模块会有影响,怎么解决这个影响呢?
发现组件提供了一个 popper-class 属性,可以自定义浮层类名
// 用 popper-class指定一个class,比如my-container,防止污染全局样式
.my-container {
.el-cascader-menu__wrap {
max-height: 300px;
overflow: scroll;
}
}
总结:当写类似的组件时,如果需要在body里插入,需要有入口可以指定对应的自定义class,这样当多个页面需要时,不会产生样式干扰
参考:
# element required 提示语修改
在element组件中,想要label文字前面加红色*,需要当在el-form-item元素加上 required 属性,但使用 rules 添加blur校验规则后,两者会有冲突
required 默认的提示是英文,怎么自定义提示呢? 需要将el-form-item元素上的 required属性去掉,放到rules规则里
rules: [
name: [
// 必填项
{ required: true, message: '姓名不能为空', trigger: 'blur' },
{ validator: validatorName, trigger: 'blur'}
]
]
参考:vue element-ui 使用required进行表单校验时自定义提示语 (opens new window)
# element 表单校验函数没生效
根据element官网的dmeo,加入表单校验,发现校验的rules根本没执行。
注意:el-form-item 标签也需要设置 prop 属性,并且名称需要和对应model的名称一致
我这次漏写了,所以一直没生效
参考:
# 2020/02/20 周四
# 使用js调用vue单文件组件
在封装组件时,如果是dialog组件,一般封装好后,通过component引入,然后把标签放到html里,通过true或false来隐藏和显示,每次都要写一些重复代码。
怎么能够像ElementUI的message函数一样直接调用呢,首先需要搞懂怎么用js来调用vue单文件组件,下面来看方法
// 假设写好了 showInfo.vue 组件,执行clickShow函数直接显示dialog
// 组件中 dialog :visible.sync="dialogTableVisible"初始值设置为true
// demo.vue 在需要调用的vue文件中引入该组件
import ShowInfo from 'showInfo.vue'
// ...
clickShow() {
const Component = Vue.extend(ShowInfo)
// 挂载后返回对应组件的vm
let showInfoVue = new Component().$mount()
// 将组件vm的dom,append到当前页面
this.$el.appendChild(showInfoVue.$el)
}
// ...
参考:
# 2020/02/18 周二
# ERROR 1396 (HY000): Operation DROP USER failed for 'zhangsan'@'localhost'
在看mysql账号相关内容时,发现在root用户下,更新、删除用户均报错。后面发现居然是localhost的字母拼错了,但新建user时居然没报错。下面来复盘整个记录:
# 1. 先创建一个账号,用来测试修改用户名
CREATE USER zhangsan@localost IDENTIFIED BY '123';
# 2. 在使用 RENAME USER ... TO ... 时,发现更改不了名字,一直报错
# mysql> RENAME USER 'zhangsan'@'localhost' TO 'wangwu'@'localhost';
ERROR 1396 (HY000): Operation RENAME USER failed for 'zhangsan'@'localhost'
# 3. 于是我试了下删除,发现还是错误
# mysql> DROP USER zhangsan@localhost;
ERROR 1396 (HY000): Operation DROP USER failed for 'zhangsan'@'localhost'
# 4. 于是搜索了下,偶然看到一个测试的命令,仔细看发现host拼写错误,而我之前修改删除时host的localhost都是拼写正确的,所以没匹配到
select user,host from mysql.user where user = 'zhangsan';
+----------+----------+
| user | host |
+----------+----------+
| zhangsan | localost |
+----------+----------+
1 row in set (0.00 sec)
# 5. 于是我将host改了下,再删除,就成功了。
mysql> DROP USER zhangsan@localost;
Query OK, 0 rows affected (0.01 sec)
# 2020/02/12 周三
# 为什么书上SQL语句一般都是大写
一般我比较喜欢小写的sql语句,比如:
select * from tb_user; # 一般习惯用法
SELECT * FROM tb_user; # 教材或书上的写法
为什么小写更直观,而不使用小写呢?今天在看语法时,有了一个答案,来看一个例子
# select ... from 内连接语法
# 先来看全小写的写法
select some_colums from tb1 inner join tb2 on some_conditions;
# 教科书上的写法
SELECT some_colums FROM tb1 INNER JOIN tb2 ON some_confitions;
你会发现可变动的内容一般是小写(比如:列名,表名,一些条件),而SQL语法相关的单词都是大写,这样更好理解。全小写描述语法时,对于初学者来看分不清哪些是SQL语句中必须的,哪些是可变动的。以后还是要习惯大写,更规范。
# char与varchar的区别
在创建表,指定字段数据类型时,如果是字符串数据类型可以是varchar(50),也可以是char(50)。这两种有什么区别呢?
- 它们都是用来储存字符数值小于255的字符, mysql5.0之前是varchar支持最大255。
- varchar(40)存入"Bill Gates",取出数据时字符串长度为10;char(40)存入"Bill Gates",取出数据时字符串长度为40, 后面会被加多余的空格。
- varchar使用可能会更方便、所占用内存空间更小,特别是当数据库比较大时,内存和磁盘空间的节省会非常重要。
- 从系统性能讲,char处理速度更快,有时可以超出varchar处理速度的50%。
- 在设计数据库时应综合考虑各方面因素,来达到一个平衡。
# 2020/02/08 周六
# nginx 访问不带www的域名,自动切到www
在seo时,搜索引擎可能会将xx.com和www.xx.com一起收录。这里需要进行处理,当使用一级域名直接访问时(xx.com),自动切到www.xx.com
# 修改nginx配置,加入如下转换
if ($host = 'zuo11.com') {
rewrite ^/(.*)$ http://www.zuo11.com/$1 permanent;
}
如下图
测试是否生效
# 打开浏览器的console,测试是否有转换成功
location.host # zuo11.com 或 www.zuo11.com
参考:
nginx 域名跳转 Nginx跳转自动到带www域名规则配置、nginx多域名向主域名跳转 (opens new window)
Converting rewrite rules - nginx (opens new window)
# 2020/02/04 周二
# Node.js、js、v8三者之间的关系
- Node.js 提供了JS运行时(运行js的环境,类似的概念有JRE提供了运行java的环境)。Node.js通过内部集成Chrome V8引擎来解析执行js
- 浏览器里js无法操作文件、无法开启http服务器、而Node.js里可以,主要是因为Node.js里面扩展加入了很多功能。比如使用libuv,提供了文件系统、网络、子进程、管道、信号处理、轮询、流等功能;使用llhttp提供了HTTP解析功能;使用OpenSSL提供tls、crypto加密相关功能等等。
参考之前的笔记:https://www.yuque.com/guoqzuo/rdrqd5/ms0w14#Libraries
# node中4种模块类型
Node.js的模块化使用CommonJS规范,在node中你会发现使用某个模块时,是否需要require,是否需要npm install会有区别。
- 核心模块 require都不需要直接使用,比如global、buffer、module、process等
- 内置模块 需要require才能使用,不需要npm install,比如:os、fs、path、http、util等
- 第三方模块 不仅需要require,还需要npm install才能使用,比如: download-git-repo、ora、commander等
- 本地自己写的模块,自己写的模块一般require就行,但如果里面包含了需要npm install的包,也需要安装
# 2020/02/02 周日
# for...in遍历顺序问题
Object.entries()遍历顺序和for...in的遍历顺序一致。for...in 以任意顺序遍历一个对象除Symbol以外的可枚举属性
来看一个示例:
var a = {
1:"a",
7:"b",
4:"c",
5:"d",
"-3":"e",
f:"f",
"2.2":"g",
6:"h",
0:"i",
"2" : "j"
};
for (key in a) {
console.log(key)
}
// 在chrome、safari、火狐浏览器中结果一致
// 0 1 2 4 5 6 7 -3 f 2.2
再来个例子,都是字符串的情况
var category = {
'web': 1,
'微信小程序': 2,
'数据库': 3,
'观点': 4,
'iOS': 5,
'UNIX环境高级编程': 5,
'C语言': 6
}
// 调整上面的属性顺序,均按照定时时的顺序来,以属性定义的先后顺序来
for (var key in category) {
console.log(key)
}
再来个例子,属性都可以转换为数字的情况,这样就会按数字属性的大小来排序了
var nums = {
3: 1,
1: 2,
4: 3,
}
// 调整上面的属性顺序,均按照定时时的顺序来,以属性定义的先后顺序来
for (var key in nums) {
console.log(key) // 1 3 4
}
总结:总之,for...in遍历属性是无序的。确定顺序,还是用数组来
参考:
for...in - JavaScript | MDN (opens new window)
for...in遍历的顺序 (opens new window)
# 提取markdown文件的大纲结构数据
知道将md转换为html文件的方法后,需要生成对应的大纲数据。方法如下:
// 截取至 zuo-blog 源码
// 读取文件内容,通过maked转换为html字符串
const fileStr = fs.readFileSync(articlePath).toString()
// let htmlStr = marked(fileStr)
let headers = marked.lexer(fileStr).filter(item => item.type === 'heading')
let outline = _generateOutline(headers) // 根据文件内容生成大纲数据
/**
* @description 将md文件heading列表,转换为层级结构,用于生成大纲
* @param {*} headers 原数据格式
* [ { type: 'heading', depth: 1, text: '站点优化 页面打开较慢处理' },
* { type: 'heading', depth: 2, text: '代码托管' },
* { type: 'heading', depth: 2, text: '速度慢的原因分析' },
* { type: 'heading', depth: 3, text: '代码分析' },
* { type: 'heading', depth: 2, text: '速度测试' } ]
* @returns [ { text: 'xx', children: [ { text:'xxx', children:[...] } ] } ]
*/
function _generateOutline(headers) {
let tree = []
// 加try catch是为了如果中间出现跨越的层级问题,直接返回错误
try {
for (let i = 0, len = headers.length; i < len; i++) {
let item = headers[i]
// 如果是一级目录,直接挂载到tree下
if (item.depth === 1) {
tree.push(item)
} else {
let target
// 如果是二级目录,挂载到当前tree最后一个元素的children上
if (item.depth === 2) {
target = tree.slice(-1)[0]
} else {
// 如果是3级+,遍历到最近一个层级的list
let count = item.depth - 2
target = tree.slice(-1)[0]
while(count--) {
target = target.children.slice(-1)[0]
}
}
!target.children && (target.children = [])
target.children.push(item)
}
}
} catch(e) {
console.log(e)
let text = '目录生成异常,请确保目录层级从H1到H6是正常顺序,对于没有H1或目录中间断层的情况需要修正'
return [ { text } ]
}
return tree
}
// 最开始比较low的写法
// let tree = []
// for (let i = 0, len = headers.length; i < len; i++) {
// let item = headers[i]
// if (item.depth === 1) {
// tree.push(item)
// } else if (item.depth === 2) {
// // 找最近的一个1级目录,加入到其list里面
// let level1 = tree[tree.length - 1]
// !level1.list && (level1.list = [])
// level1.list.push(item)
// } else if (item.depth === 3) {
// // 找最近的一个二级目录
// let level1 = tree[tree.length - 1]
// let level2 = level1.list[level1.list.length - 1]
// !level2.list && (level2.list = [])
// level2.list.push(item)
// } else if (item.depth === 4) {
// // 找最近的一个三级目录
// let level1 = tree[tree.length - 1]
// let level2 = level1.list[level1.list.length - 1]
// let level3 = level2.list[level2.list.length - 1]
// !level3.list && (level3.list = [])
// level3.list.push(item)
// } else if (item.depth === 5) {
// // 找最近的一个4级目录
// let level1 = tree[tree.length - 1]
// let level2 = level1.list[level1.list.length - 1]
// let level3 = level2.list[level2.list.length - 1]
// let level4 = level3.list[level3.list.length - 1]
// !level4.list && (level4.list = [])
// level4.list.push(item)
// } else if (item.depth === 6) {
// // 找最近的一个5级目录
// let level1 = tree[tree.length - 1]
// let level2 = level1.list[level1.list.length - 1]
// let level3 = level2.list[level2.list.length - 1]
// let level4 = level3.list[level3.list.length - 1]
// let level5 = level4.list[level4.list.length - 1]
// !level5.list && (level5.list = [])
// level5.list.push(item)
// } else if (item.depth === 7) {
// // 找最近的一个6级目录
// let level1 = tree[tree.length - 1]
// let level2 = level1.list[level1.list.length - 1]
// let level3 = level2.list[level2.list.length - 1]
// let level4 = level3.list[level3.list.length - 1]
// let level5 = level4.list[level4.list.length - 1]
// let level6 = level5.list[level5.list.length - 1]
// !level6.list && (level6.list = [])
// level6.list.push(item)
// }
// }
# node 文件操作路径path问题
在写 zuoblog init 命令执行的操作时,需要操作当前命令执行时所在的目录,而__dirname是程序文件的路径。这就需要用到 process.cwd()了,可以获取到当前命令执行时所在的目录
const path = require('path')
// delPath = path.join(__dirname, delPath) // 这个是当前文件的路径
// process.cwd() 当前命令执行时所在的目录
delPath = path.join(process.cwd(), delPath)
# 根据大纲数据生成html
在md文件显示的右侧,显示大纲html,将大纲JSON数据,生成html。注意:
ul 的padding-left要修改为0,而不是1em,因为发现语雀、gaylab对应的大纲实现里,focus时都有左侧border,菜单的padding-left根据其depth来生成,padding-left: (depth * 1)em
这里大纲的每一个标题都没有使用a标签,不是走hash,而是直接通过点击js来滚动到对应id的位置。
/**
* @description 根据大纲数据(JSON)生成侧边栏html
* @param {*} outline
*/
_getAsideHtml(outline) {
function handlerId(id) {
let newId = id.toLowerCase().replace(/\s/g, '-')
newId = newId.replace(/[\(\)\/\,\=\>\.\:\+]/g, '')
return newId
}
let asideHtml = ''
let backupOutline = JSON.parse(JSON.stringify(outline))
for (let i = 0, len = outline.length; i < len; i++) {
asideHtml += '<ul>'
asideHtml += `<li><span class="ul-span" data-id="${handlerId(outline[i].text)}" style="padding-left:${outline[i].depth + 'em'}">${outline[i].text}<span></li>`
if (outline[i].children) {
asideHtml += getChildrenAsideHtml(outline[i].children)
}
asideHtml += '</ul>'
}
function getChildrenAsideHtml(outline) {
if (!outline || outline.length === 0) {
return ''
}
let asideHtml = ''
for (let i = 0, len = outline.length; i < len; i++) {
asideHtml += '<ul>'
asideHtml += `<li><span class="ul-span" data-id="${handlerId(outline[i].text)}" style="padding-left:${outline[i].depth + 'em'}">${outline[i].text}<span></li>`
if (outline[i].children) {
asideHtml += getChildrenAsideHtml(outline[i].children)
}
asideHtml += '</ul>'
}
return asideHtml
}
return asideHtml
}
# 页面滚动时,自动切换大纲focus
直接上代码,里面包含大纲点击事件、滚动页面后自动改变大纲focus,这里的核心问题是: 怎么获取页面滚动到了哪个标题区域?
每次一进入页面,将每个标题(h1,h2,..)的id,offsetTop(距离页面顶部距离)按顺序存到数组,监听页面滚动事件,根据document.documentElement.scrollTop的高度,来匹配之前的数组,就可以找到滚动到哪个标题了
// 监听大纲的点击事件
let asideDiv = document.getElementsByTagName('aside')[0]
asideDiv.onclick = (e) => {
let id = e.target.dataset.id
if (!id) return
// 移除所有的active
let nodes = document.getElementsByClassName('ul-span')
for (let i = 0, len = nodes.length; i < len; i++) {
nodes[i].classList.remove('active')
}
e.target.classList.add('active')
document.getElementById(id).scrollIntoView(true)
document.documentElement.scrollBy(0, -70)
}
let headersArr = []
window.onload = () => {
// 如果是category,且有hash值,向上滚动 -70
// 通过category.html#web进入页面时, 由于顶部fixed会有遮挡,fix方案
let { pathname, hash } = location
pathname.includes('category.html') && hash && document.documentElement.scrollBy(0, -70)
// 将每个标题的高度,存到数组里,当滚动时,自动focus右侧大纲
let nodes = document.getElementsByClassName('ul-span')
for (let i = 0, len = nodes.length; i < len; i++) {
// console.log(nodes.dataset)
let id = nodes[i].dataset.id
headersArr.push({id: id, offsetTop: document.getElementById(id).offsetTop})
}
// console.log(headersArr)
window.onscroll = () => {
focusAsideSpan()
// debounce(focusAsideSpan)
}
}
// 效果不好,没有实时滚动的感觉,关闭防抖
// function debounce(method, context) {
// clearTimeout(method.tId)
// method.tId = setTimeout(function() {
// method.call(context)
// }, 100)
// }
function focusAsideSpan() {
let scrollTop = document.documentElement.scrollTop
let curNode
for (let i = 0, len = headersArr.length; i < len; i++) {
if (headersArr[i].offsetTop - scrollTop >= 0) {
// 移除所有的active
let nodes = document.getElementsByClassName('ul-span')
for (let j = 0, len = nodes.length; j < len; j++) {
if (headersArr[i].id === nodes[j].dataset.id) {
nodes[j].classList.remove('active')
nodes[j].classList.add('active')
} else {
nodes[j].classList.remove('active')
}
}
return
}
}
// 如果走到这里,说明滚到底部了
// 移除所有的active
let nodes = document.getElementsByClassName('ul-span')
for (let i = 0, len = nodes.length; i < len; i++) {
nodes[i].classList.remove('active')
}
nodes[nodes.length - 1].classList.add('active')
}
# node复制或删除文件夹
node只提供了复制文件、删除空文件夹的方法,如果需要复制文件夹或删除文件夹,就需要自己写方法了,参考: https://github.com/zuoxiaobai/zuo-blog/blob/master/vendor/utils/FSExtend.js