# 2020年02月技术日常

# 2020/02/29 周六

# 添加到我的小程序引导tips被原生组件遮挡的问题

在小程序里,为了增加用户留存,会做一个引导用户添加到我的小程序的提示面板

今天自己实现了下,发现原生组件遮挡了这个提示,貌似暂时没有很好的解决方法

所以,当设计小程序UI时,尽量不要在顶部使用原生组件。

参考:

# border三角形边框问题

在给小程序添加引导时,里面有个带边框的三角形,如下图

border边框.png

一般用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 里使用了富文本编辑器,发现校验规则校验这个值会有异常:

  1. 当 change 或 blur 时,根本没有触发校验(提示错误)
  2. 提交表单时,当该字段校验失败会提示错误,但该字段符合要求时,validate的回调一直没触发,导致无法进行校验成功之后的下一步操作

将富文本编辑器换成 el-input 正常,换成普通的 input 也会异常,感觉一头雾水。

使用 this.$refs.ruleForm.validateField('xxx') 单读校验也不行,这个应该是element表单输入组件特有的

于是粗略看了下源码,发现错误信息在form-item(也就是el-form-item)组件里处理,当el-select或el-input值改变时会将事件传递给form-item

两个不同的组件,一个组件里怎么捕获到另一个组件的事件呢,在element内部使用了发布订阅设计模式来处理:

  1. 在form-item里订阅事件
  2. 当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组件就不会触发校验

怎么来处理呢?这里暂时采用自定义方法来处理:

  1. 从rules移除对应的字段 required,至于label前面的红星,直接在el-form-item__label类上加一个before属性来设置
  2. 对于行内显示错误信息,可以在el-form-item__label的after里显示错误信息,通过一个父类的error-class来控制隐藏显示
  3. 单独写校验逻辑,如果校验失败加上一个error-class,如果想做的更逼真一点,在错误时给输入组件加一个红色的border

# element怎么动态改变校验rules且实时生效

需要动态改变rules场景,有两个功能点:

  1. 某个checkbox的值改变,有部分字段需要在必须和可选间切换
  2. 某个cascader组件值改变时,需要动态切换部分字段(有删有减)

需要注意的地方

  1. 可选和必选切换,只需要改变rules里的require属性,true和false之间切换(我之前直接暴力删rule里的fields,这种方法不能关闭原来必选时触发的错误提示)
  2. 对于动态修改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 数据

node文件上传进度.png

来看源码:

<!-- 前端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)。这两种有什么区别呢?

  1. 它们都是用来储存字符数值小于255的字符, mysql5.0之前是varchar支持最大255。
  2. varchar(40)存入"Bill Gates",取出数据时字符串长度为10;char(40)存入"Bill Gates",取出数据时字符串长度为40, 后面会被加多余的空格。
  3. varchar使用可能会更方便、所占用内存空间更小,特别是当数据库比较大时,内存和磁盘空间的节省会非常重要。
  4. 从系统性能讲,char处理速度更快,有时可以超出varchar处理速度的50%。
  5. 在设计数据库时应综合考虑各方面因素,来达到一个平衡。

# 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;
}

如下图

nginx_config.png

测试是否生效

# 打开浏览器的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三者之间的关系

  1. Node.js 提供了JS运行时(运行js的环境,类似的概念有JRE提供了运行java的环境)。Node.js通过内部集成Chrome V8引擎来解析执行js
  2. 浏览器里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。注意:

  1. ul 的padding-left要修改为0,而不是1em,因为发现语雀、gaylab对应的大纲实现里,focus时都有左侧border,菜单的padding-left根据其depth来生成,padding-left: (depth * 1)em

  2. 这里大纲的每一个标题都没有使用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

上次更新: 2020/10/29 22:59:19