# 2021年04月技术日常

# 2021/04/25 周日

# v-model 怎么优雅的绑定 Vuex 状态管理中的值,三种方法优缺点

在需要将 vuex 中的值,直接与表单 v-model 对应时,如果我们按照 vuex 强调的规范,只能通过 mutation 来改变 vuex state,那么会比较麻烦。假设我们脱离规范,关闭严格模式。那么 v-model 可以直接绑定 vuex state 值,会非常方便,但貌似又不合规范,不利于追踪。

那到底要怎么做合适呢?个人建议是:怎么方便怎么来,只要项目可控即可。下面来看看几种方法对比

# 1. 官方推荐:使用 computed 的 get 和 set

  • 优点:遵循 vuex 规范,通过 mutation 更改 state,有利于追踪
  • 缺点:需要将表单的每个字段都使用 computed 计算属性,对于字段较少的情况,还可以。但当字段较多,且包含逻辑处理时,会比较麻烦不好维护。
// <input v-model="message">
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      // 通过 updateMessage mutation 方法更改值
      this.$store.commit('updateMessage', value)
    }
  }
}

参考:表单处理 | Vuex (opens new window)

# 2. 非严格模式:直接将 state 设置到 v-model

关闭严格模式后,直接将 vuex state 当全局变量用,可以任意修改操作

const store = new Vuex.Store({
  // ...
  strict: false // 关闭严格模式,或者直接不写这个属性
  // strict: true // 开启严格模式 
})
  • 优点:可以将 obj.message 直接设置到 v-model,值实时更新,而且不用什么额外操作。
  • 缺点:破坏了 vuex 的规则约束,不利于追踪,可能会造成难以维护的情况。
<input v-model="obj.message">

# 3. 中间对象转换:watch 监听 state 改变,更新中间对象,v-model 中间对象

  • 优点:折中方法,不破坏 vuex 规则约束,操作比一个一个加 computed 要简单
  • 缺点:需要使用 watch 来初始化或更改值,触发时机需要特别注意
<template>
  <div>
    <div>{{ obj }}</div>
    <div>{{ objCopy }}</div>
    <el-form>
      <el-input v-model="objCopy.message" />
      <el-button @click="confirm">提交</el-button>
    </el-form>
  </div>
</template>

<script>
import { mapState } from "vuex";
export default {
  data() {
    return {
      objCopy: {
        message: ""
      }
    };
  },

  computed: {
    ...mapState(["obj"])
  },

  watch: {
    obj: {
      handler() {
        this.initData();
      },
      deep: true
    }
  },

  created() {
    this.initData();
  },

  methods: {
    initData() {
      Object.assign(this.objCopy, this.obj);
    },
    confirm() {
      this.$store.commit("updateObj", JSON.parse(JSON.stringify(this.objCopy)));
    }
  }
};
</script>

# String.prototype.replace 多个匹配替换时注意要使用正则

在做字符串替换时可以使用 repalce,但这里要注意,当需要替换多个时,第一个参数不能是字符串,要使用正则表达式

来看一个例子,将字符串 "1,000,000" 中的 "," 替换为空字符串 ""

let str = "1,000,000"
str.replace(",", "") // "1000,000"

注意:上面的例子中,如果单纯的字符串替换,只会替换第一个,全局匹配时,第一个参数就要用正则了

let str = "1,000,000"
str.replace(/,/g, "") // "1000000"

# Vue filters 中 this 为 undefined,建议使用传参或 method 处理

在 i18n 国际化,将 code 转换为国际化文本的场景中,需要使用 this.$i18n 这个变量,但发现 fitlers 中的 this 是 undefined,无法使用,查了下。

这个是 Vue 设计问题,以下是作者 尤雨溪 在 issue this undefined in filters · Issue #5998 · vuejs/vue (opens new window) 下的回复:

filter-this.png

This is intentional in 2.x. Filters should be pure functions and should not be dependent on this context. If you need this you should use a computed property or just a method e.g. $translate(foo)

在 2.x 的版本中这个处理是有意的,Fitlers 应该是纯函数,不应该依赖 this 上下文。如果需要依赖 this,那就应该使用 computed 计算属性或 method 方法

解决方法:在 filters 中我们是可以传参数的,我们可以把需要的值,通过参数传入。如果涉及到多个变量的使用,传参比较冗余,那就使用计算属性或方法,使用示例:

// {{ currencyCode | currencyText($i18n) }}
filters: {
  currencyText: function(code, i18n) {
    // filter 中不能使用 this,需要传参数 this.$i18n
    // console.log(this); // undefined
    let { currencys, currencyCodes } = i18n.messages[i18n.locale].base;
    // 根据 code 找到对应的 index
    let index = currencyCodes.indexOf(code);
    return currencys[index];
  }
},

参考: vue中过滤器filters的this指向问题。 (opens new window)

# vue-i18n 国际化相关用法、实践总结

Vue 使用 vue-i18n 国际化 - dev-zuo 技术日常 (opens new window) 中,我们简单介绍了 vue-i18n 的基本使用。如果想将它实际应用到项目中,我们还需要考虑怎么做到更加简洁、优雅、可维护,下面是一些实践总结。

# 1. i18n 单独放一个目录,避免在 main.js 中写入太多内容

i18n 相关内容较多,如果都写在 main.js 里,内容会很多,不够优雅,这里可以进行简单封装。在 src 目录下新建一个 i18n 目录,专门用于存放国际化相关内容,使用 index.js 作为入口文件

// src/i18n/index.js
import Vue from "vue";
import VueI18n from "vue-i18n";

Vue.use(VueI18n);

const i18n = new VueI18n({
  locale: "zh-CN", // 设置默认语言环境
  // 在 vue template 的 {{}} 中,使用 $t('un') 即可拿到 un 属性指定的值
  // 不需要在 data() {} 中设置什么
  messages: {
    en: {
      name: "Zhang san",
      hello: "hello world"
    },
    "zh-CN": {
      name: "张三",
      hello: "你好,世界"
    }
  }
});

export default i18n; // 将 i18n 实例导出,用于在 new Vue 时引入

这样,我们在 main.js 就只需要修改两行就可以了



 


 





// main.js
// ...
import i18n from "./i18n/index"; // 引入 src/i18n/index,得到 i18n 实例

new Vue({
  i18n, // 在 new Vue() 时加入 i18n
  router,
  store,
  render: h => h(App)
}).$mount("#app");

# 2. 方便实时测试:语言切换组件

在测试国际化时,改代码的方式切国际化语言不够方便。我们可以做一个测试用的语言切换组件,在主页面引入后,会出现在页面右上角,方便调试。相关配置存到 lcoalStorage 中,防止调试时刷新页面后需要再次手动切换语言。

i18n-lang-change.gif

<!-- BaseLanguageSelect.vue -->
<template>
  <div class="demo-i18n">
    国际化测试:
    <el-select v-model="lang" @change="langChange">
      <el-option label="中文" value="zh-CN"></el-option>
      <el-option label="英语" value="en"></el-option>
    </el-select>
  </div>
</template>

<script>
// 将当前语言存到 localStorage 中的字段,要特殊,防止和其他项目冲突
const LANG_NAME = "demo-lang";
export default {
  data() {
    return {
      lang: "en"
    };
  },
  created() {
    // 从 localStorage 中读取默认语言,便于页面刷新后调试
    this.lang = localStorage.getItem(LANG_NAME) || "en";
    // 设置到 i18n
    this.$i18n.locale = this.lang;
  },
  methods: {
    // 语言切换后,设置到 i18n、localStorage
    langChange(value) {
      this.$i18n.locale = value;
      localStorage.setItem(LANG_NAME, value);
    }
  }
};
</script>

<style lang="less" scoped>
.demo-i18n {
  position: absolute;
  top: 10px;
  right: 10px;
}
</style>

使用示例

<!-- i18nTest.vue -->
<template>
  <div>
    <BaseLanguageSelect />
    {{ $t("name") }}, {{ $t("hello") }}
  </div>
</template>

<script>
export default {
  components: {
    BaseLanguageSelect: () => import("./BaseLanguageSelect")
  }
};
</script>

# 3. 避免单文件过大、混乱:模块化

当项目较大、国际化内容较多时,如果将全部内容都写在 i18n/index.js 主文件中,有两个缺点:

  • i18n/index.js 内容太多,不好查找
  • 多人同时维护时,容易产生冲突,合并代码一不小心就会合出问题

这就需要模块化了,怎么优雅的模块化呢?下面是我的一些思考,仅供参考

  • 按功能模块划分、不同的模块放到不同的目录、不同的语言使用不同的文件
  • 新增模块和语言时,最小化改动公共文件 i18n/index.js

目录结构规划如下

├── i18n
│   ├── base # 通用模块,想用 common 但是使用时,模块名越短越方便,改为 base 
│   │   ├── en.js
│   │   └── zh-CN.js 
│   ├── user # 用户模块
│   │   ├── en.js
│   │   └── zh-CN.js 
│   ├── xxx # 其他模块
│   │   ├── en.js
│   │   └── zh-CN.js 
│   └── index.js # 入口文件、导出 i18n 实例

这里就需要重构 i18n/index.js 了,利用 import() 动态导入,使用 modules 模块数组,languages 语言数组,自动去合并、整理 messages。后面新增模块或新增语言非常方便、快捷,代码如下

import Vue from "vue";
import VueI18n from "vue-i18n";

Vue.use(VueI18n);

/**
 * @description 根据模块、语言提取 messages
 * @author zuoxiaobai <i@zuoguoqing.com>
 * @param { Array } modules ["base", "user"]; // 模块数组
 * @param { Array } languages ["en", "zh-CN"]; // 语言数组
 * @returns { Object} messages
 */
function getMessages(modules, languages) {
  // 初始化 messages { en: {}, zh-CN: {}}
  let messages = {};
  languages.forEach(lang => {
    messages[lang] = {};
  });

  // 遍历模块,将内容添加到 messages.语言.上
  modules.forEach(moduleName => {
    languages.forEach(async lang => {
      // 加 try ... catch 防止语言文件缺失 import 报 error,影响执行
      try {
        let { default: obj } = await import("./" + `${moduleName}/${lang}.js`);
        // { en: { base: { } }, zh-CN: { base: {} }}
        !messages[lang][moduleName] && (messages[lang][moduleName] = {});
        Object.assign(messages[lang][moduleName], obj);
      } catch (e) {
        console.warn(e.message);
      }
    });
  });
  console.log(messages);
  return messages;
}

const modules = ["base", "user"]; // 模块数组
const languages = ["en", "zh-CN", "zh-TW"]; // 语言数组
let messages = getMessages(modules, languages);

const i18n = new VueI18n({
  locale: "zh-CN", // 设置默认语言环境
  messages
  // messages: {
  //   en: {
  //     name: "Zhang san",
  //     hello: "hello world"
  //   },
  //   "zh-CN": {
  //     name: "张三",
  //     hello: "你好,世界"
  //   }
  // }
});

export default i18n;

合并后的效果如下图,不同的模块放到不同的大对象上

i18n-modlues.png

在 vue 组件中使用时,加上模块前缀即可

<template>
  <div>
    <BaseLanguageSelect />
    <!-- {{ $t("name") }}, {{ $t("hello") }} -->
    {{ $t("user.name") }}, {{ $t("user.hello") }}
    {{ $t("base.year") }}, {{ $t("base.month") }}, {{ $t("base.time") }}
  </div>
</template>

# 4. 列表渲染(code 值)处理:js 取值、过滤器 filter 设计

对于列表相关国际化,如果是前端纯展示还好,我们直接遍历即可,以 currency 币种为例,纯展示可以像下面这样写

<!-- currencys: ["RMB", "USD", "HKD"] -->
<!-- currencys: ["人民币", "美元", "港币"] -->
<div v-for="item in $t('base.currencys')" :key="item">{{ item }}</div>

但如果涉及到表单,就需要一些处理了。比如表单中需要选择币种,一般每个币种会对应不同的 code,我们需要处理两个问题:

  1. 用户选择国际化币种后,表单 v-model 值自动变成 code
  2. 当后端返回币种 code 时,前端可以渲染其国际化币种显示

可以在国际化中新增一个字段比如 currencysCode,按照字段顺序一一对应,比如 currencyCodes: ['10', '12', '15'],这样根据 index,可以将 code 设置到表单的 value 中,另外知道 code 计算国际化文本时,可以使用 filter,从 this.$i18n 这个变量中,通过 js 来取值,例子如下

<template>
  <div>
    <BaseLanguageSelect />

    当前币种:{{ currency | currencyText($i18n) }} {{ currency }}
    <el-select v-model="currency">
      <el-option
        v-for="(item, index) in $t('base.currencys')"
        :key="item"
        :label="item"
        :value="$t('base.currencyCodes')[index]"
      ></el-option>
    </el-select>
  </div>
</template>

<script>
export default {
  components: {
    BaseLanguageSelect: () => import("./BaseLanguageSelect")
  },

  filters: {
    currencyText: function(code, i18n) {
      // filter 中不能使用 this,需要传参数 this.$i18n
      // console.log(this); // undefined
      let { currencys, currencyCodes } = i18n.messages[i18n.locale].base;
      // 根据 code 找到对应的 index
      let index = currencyCodes.indexOf(code);
      return currencys[index];
    }
  },
  data() {
    return {
      currency: ""
    };
  }
};
</script>

除了在国际化中,加入对应的 code 对应关系字段外,也可以单独用一个 const 文件来保存 code。当语言种类不多的情况下,感觉直接放到国际化字段中会简单一点,操作时不用额外引入文件。

除了模板语法写法外,我们也可以通过 Vue 的实例属性 this.$i18n. 来操作国际化数据,这样基本就可以处理所有情况了。

# vue 组件中 css 路径简写 @ 不可用,需要使用 ~@

vue css src 路径,css @ 不生效,css src 简写@, webpack 解析 css 路径

在 vue-cli 创建的 vue 项目中,可以使用 @ 来表示 src 路径。但在 css 中,图片路径使用 @ 就会出错。那 css 中要怎么使用 src 相对路径呢?

需要在前面加 ~,也就是 ~@,这样就不必使用相对路径了。

#img {
  height: 100px;
  width: 100px;
  background: url("~@/assets/logo.png");
  background-size: contain;
}

为什么呢?可能的原因是 @ 是 css 的保留关键字,在 @import 的时候会用到,为了避免冲突,才使用 ~@ 以示区分。

webpack 中使用 css-loader 来处理 css,具体逻辑可以看 css-loader 源码 (opens new window)

参考:

让 footer 一直保持在最底部是比较常见的需求,css 和 js 都可以实现,一般推荐使用 css 实现,下面来看看具体实现,以及他们的优缺点

footer-bottom.gif

以下面的结构为例

<body>
 <article class="container">
    <header>顶部</header>
    <section class="main">中间内容部分</section>
    <footer>底部</footer>
  </article>
</body>

需要考虑两种情况:

  1. 内容没占满视窗时,footer 在最底部,需要 body 有最小高度,才能撑起来
  2. 内容较多时,滚动到底部才能看到 footer,且不遮挡内容区域

# 方法 1:css 方式 - position: absolute

  1. footer 使用 position: absolute; bottom: 0; 保持在底部。
  2. 对于可能遮挡中间内容区域的问题,将 body 设置为 relative,注意不是 container,这样可以让 footer 内容保持在body最底部
  3. 内容较多时,footer position: absolute 会遮挡内容部分,需要将 container 加一个 padding-bottom: footer 高度,来防止遮挡

方法 1 - 在线示例 (opens new window)方法 1 - 代码 | github (opens new window)

优点:css 即可实现

缺点:需要知道 footer 具体高度。对于 footer 高度不确定的场景,就不合适了。

* { margin: 0; }
body {
  position: relative; /* 当 footer 为 position 为 absolute 时,放置到 body 底部 */
  min-height: 100vh; /* 最小高度 100% 视窗高度 */
}
.container {
  padding-bottom: 50px; /* 防止内容被 footer 遮挡 */
}
footer { 
  position: absolute; /* 放到页面最底部 */
  bottom: 0;
  left: 0;
  height: 50px;
  width: 100%;
  background: yellow;
}

# 方法 2:css 方式 - 中间部分 flex: 1

使用 flex 纵向布局,中间部分使用 flex: 1,内容不足时自动撑开

方法 2 - 在线示例 (opens new window)方法 2 - 代码 | github (opens new window)

优点:css 即可实现,不需要知道 footer 区域高度

缺点:需要注意父容器高度,最小高度要占满屏幕,有嵌套时,需要使用 height: 100%;且整体需要使用 flex 布局

* { margin: 0 }
.container { 
  display: flex; /* 使用 flex 纵向布局 */
  flex-direction: column;
  min-height: 100vh;
  box-sizing: border-box;
}
.main {
  flex: 1; /* 中间内容部分 flex-grow: 1 内容不足时自动撑开 */
}
footer {  /* 不需要知道底部高度 */
  padding: 10px;
  background: yellow;
}

# 方法 3:js 方式 - 监听页面整体滚动高度与视窗高度动态设置样式

页面 mounted 后,判断视窗高度 和 整体滚动高度(包含 footer高度),如果小于视窗高度,则添加一个 固定底部的 class,否则去掉该 class

方法 3 - 在线示例 (opens new window)方法 3 - 代码 | github (opens new window)

优点:无

缺点:页面不刷新的情况下,动态增加内容,需要重新判断一次,不推荐

下面是 vue 的实现

// :class="fixed ? 'flexd-bottom' : ''"
// data: { fixed: false }
 // 处理 footer 
methods: {
  handleFooter() {
    let { clientHeight } = document.documentElement;
    this.fixed = false 
    this.$nextTick(() => {
      let { scrollHeight: pageScrollHeight } = document.querySelector('.container')
      // 如果整体页面高度(包含默认加载的 bottom) < 视窗高度 固定到底部
      this.fixed = pageScrollHeight < clientHeight
    })
  }
},
mounted() {
  this.handleFooter()
}
// &.fixed-bottom {
//   position: absolute;
//   bottom: 0;
// }

# 测试 demo

<head>
  <meta charset="UTF-8">
  <title>测试 footer</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <style>
    * { margin: 0 }
    footer { height: 50px; background: yellow; }
  </style>
</head>
<body>
  <article id="app" class="container">
    <header>
      <h2>foot 始终在顶部测试</h2>
    </header>
    <section class="main">
      占位数:<button v-for="item in list" :key="`btn-${item}`" @click="changeCount(item)">{{item}}</button>
      <div v-for="item in count" :key="`gap-${item}`">
        占位 {{item}}
      </div>
    </section>
    <footer>
      Copyright © 2016-2021 zuo11.com. 鄂ICP备16014741号-1
    </footer>
  </article>
  <script>
    let app = new Vue({
      el: '#app',
      data() {
        return {
          list: [5, 50, 100],
          count: 5
        }
      },
      methods: {
        changeCount(value) {
          this.count = value
        }
      }
    })
  </script>
</body>
</html>

# Vue 组件封装,通过发布订阅模式和 Vue 实例方法实现 js 操作组件

以消息组件为例,如果多个组件共用一个全局的消息组件,那怎么优雅的显示消息呢?

普通的组件 props 传值的方式会受限,因为组件层级是不确定的。

你可能会想到状态管理。将是否显示消息、消息内容存到状态管理 state 中,如果需要显示就修改 vuex 值即可。

但这样调用起来会不够方便、简洁。这里我们可以使用发布订阅模式,结合 Vue 实例属性来优雅的实现该功能。下面来看看使用示例

<!-- 先在 main.js 里全局注册 -->
<!-- 
import MessageInfo from "./components/message-info/index.js";
Vue.use(MessageInfo);
-->

<!-- msgTest 测试页面 -->
<template>
  <div>
    <button @click="showMsg">Show msg</button>
    <!-- 消息组件 message-info -->
    <message-info>
      <!-- slot -->
      <button @click="closeMsg">手动关闭 message</button>
    </message-info>
  </div>
</template>

<script>
export default {
  methods: {
    // 显示弹窗
    showMsg() {
      this.$showMsg(["消息1", "消息2"]);
    },

    // 手动关闭弹窗
    closeMsg() {
      this.$closeMsg();
    }
  }
};
</script>
<style></style>

上面的例子中,我们使用 this.$showMsg() 和 this.$closeMsg() 来轻松实现了消息的显示与关闭。这样做的好处是,可以在任何子组件中来操作消息的显示和隐藏,简单方便。

下面来看 message-info 组件的具体实现,组件目录如下,相比其他组件多了 EventBus.js,MessageInfo.js 主要用于发布订阅事件处理

├── message-info
│   ├── src 
│   │   ├── EventBus.js # 发布订阅 bus
│   │   ├── index.vue # vue 组件
│   │   └── MessageInfo.js # js 对象
│   └── index.js # 组件入口文件,Vue.use 时注册全局组件,绑定实例属性

message-info/index.js 入口文件

import MessageInfo from "./src/index.vue";
import MessageInfoCore from "./src/MessageInfo.js";

MessageInfo.install = function(Vue) {
  // 注册全局组件 message-vue
  Vue.component(MessageInfo.name, MessageInfo);

  // 绑定实例属性
  Vue.prototype.$showMsg = MessageInfoCore.showMsg;
  Vue.prototype.$closeMsg = MessageInfoCore.closeMsg;
};

export default MessageInfo;

message-info/src/index.vue 组件代码

<template>
  <!-- 父元素遮罩层-->
  <div class="msg-info-wrap" v-if="showMsg" @click="closeMsg">
    <!-- 消息弹窗 -->
    <div class="msg-info" @click.stop>
      <!-- 消息列表 -->
      <div v-for="msg in messages" :key="msg">{{ msg }}</div>
      <slot></slot>
    </div>
  </div>
</template>

<script>
import Bus from "./EventBus";

export default {
  name: "MessageInfo",
  data() {
    return {
      showMsg: false,
      messages: []
    };
  },

  created() {
    Bus.$on("showMsg", msgList => {
      this.showMsg = true; // 显示消息
      this.messages = msgList; // 显示对应的消息列表
    });
    Bus.$on("closeMsg", this.closeMsg);
  },

  destroyed() {
    Bus.$off("showMsg");
    Bus.$off("closeMsg");
  },

  methods: {
    closeMsg() {
      this.showMsg = false;
      this.messages = [];
    }
  }
};
</script>

<style lang="less" scoped>
.msg-info-wrap {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
  background: rgba(0, 0, 0, 0.1);
  .msg-info {
    position: absolute;
    top: 30%;
    left: 50%;
    width: 50%;
    padding: 20px;
    border-radius: 5px;
    transform: translate(-50%, -50%);
    background: #fff;
  }
}
</style>

message-info/src/EventBus.js

import Vue from "vue";

let Bus = new Vue();

export default Bus;

message-info/src/MessageInfo.js

import Bus from "./EventBus";

class MessageInfo {
  static showMsg(...args) {
    Bus.$emit("showMsg", ...args);
  }

  static closeMsg(...args) {
    Bus.$emit("closeMsg", ...args);
  }
}

export default MessageInfo;

完整 github 代码地址: message 组件 代码 | github (opens new window)

# 正则表达式使用 () 和 match 或 replace 提取 url 路径参数

来看一个问题,使用正则表达式从 url 中提取区域、城市id、模块、页数id。url 示例如下

  1. http://www.xx.com/region/gd/module
  2. http://www.xx.com/region/gd-c222/module
  3. http://www.xx.com/region/gd-c222/module/p2

下面来看看怎么实现

首先,回顾下正则表达式(Regular Expression)基础。正则表表达式以 /pattern/flags 表示,是 RegExp 对象的实例

/[a-z]*/g instanceof RegExp // true

pattern 模式由下面的字符组成

  • 边界符号(^、$) ^ 以 xx 开头,$ 以 xx 结尾

    • /^abc/ 表示以 abc 开头
    • /abc$/ 表示以 abc 结尾
  • 字符集合([]、^、-) 一系列的字符集合

    • [abc] 表示 abc 里面的任意一个字符
    • [^abc] 表示任意一个非 abc 字符,注意 ^ 在 [] 里面表示非 xx 字符
    • [a-z0-9] 表示 a 到 z 的字符,0 到 9 字符
  • 预定义模式字符(.、\d、\w、\s、\n) 一些常见字符集合的简写

    • . 表示除 \r\n 之外的所有字符
    • \d[0-9] 的简写,表示数字
    • \D[^0-9] 的简写,表示非数字
    • \w[0-9a-zA-Z_] 的简写,表示数字、字母、下划线
    • \W[^0-9a-zA-Z_] 的简写,表示非数字、字母、下划线
    • \s[ ] 的简写,表示空白符(空格)," a b c ".replace(/\s/g, '') === "abc"
    • \S[^ ] 的简写,表示非空白符
    • \n 表示 换行符
  • 匹配次数/数量(?、*、+、{})

    • ? 匹配前面的模式 0 次或 1 次 {0, 1}
    • * 匹配前面的模式 0 次或多次 {0, }
    • + 匹配前面的模式 1 次或多次 {1, }
    • {n} 匹配前面的模式 n 次
    • {n,m} 匹配前面的模式 至少 n 次,至多 m 次
  • 其他 ()、|

    • () 用于匹配字符串中的多个部分,比如 (\w+)\s(\w+) 匹配 aaa bbb,第一部分 $1 为 'aaa',第二部分 $2 为 'bbb'。可以使用 regExp.exec(str) 返回对象的 index 来获取对应的部分值。或者 replace 时,使用 $1 $2 等格式替代。
    • | 表示或 /a|b/ 表示 a 或者 b
  • 转义字符* + ? $ ^ . | \ ( ) { } [ ] 正则表达式保留字符都需要在前面加 \ 进行转义,比如字符 / 需要使用 \/ 表示

flags 标志位:g 不仅仅匹配第一个,全局匹配。i 忽略大小写

正则表达式中,一个 () 表示一个部分,来看一个例子。

let str = "zuo guoqing"
let reg = /(\w+)\s(\w+)/ // 匹配两个部分 '第一部分 第二部分'
str.replace(reg, "$2 $1") // 将两个部分对调,"guoqing zuo"
str.match(reg) // { 0: "zuo guoqing", 1: "zuo", 2: "guoqing" }
reg.exec(str) // { 0: "zuo guoqing", 1: "zuo", 2: "guoqing" }

现在我们可以使用 () 来分部分(块)提取 url 中的字符了,将 url 拆解 .com之前的部分\/区域部分\/城市以及id部分\/模块部分\/页数部分

// .com 之前的部分 [\S]+.com\/  匹配一个或多个非空白符.com\/
// 区域部分 ([\w]*)\/ 匹配 0 个 或多个 数字字母下划线
// 城市及id部分 ([\w-]*)\/ 匹配 0 个或多个 数字字母下划线 -
// 模块部分 ([\w]*)\/?
// 页数部分 ([\w]*)$
let regExp = /[\S]+.com\/([\w]*)\/([\w-]*)\/([\w]*)\/?([\w]*)$/
let a = `http://www.xx.com/region/gd/module`
let b = `http://www.xx.com/region/gd-c222/module`
let c = `http://www.xx.com/region/gd-c222/module/p2`
a.replace(regExp, '$1 $2 $3 $4') // "region gd module "
b.replace(regExp, '$1 $2 $3 $4') // "region gd-c222 module "
c.replace(regExp, '$1 $2 $3 $4') // "region gd-c222 module p2"
a.match(regExp) // { 1: "region", 2: "gd", 3: "module", 4: "" }
c.match(regExp) // { 1: "region", 2: "gd-c222", 3: "module", 4: "p2" }

# 2021/04/10 周六

# Make sure you configure your ‘user.name’ and ‘user.email’ in git

在 vscode 中,使用可视化工具,而非命令的方式提交代码时,如果没有配置 git 的 user.name 和 user.email,可能会弹出 Make sure you configure your ‘user.name’ and ‘user.email’ in git 的错误,代码无法提交。这就需要先配置好,再提交了。

另外,在 github 或 gitlab 等页面中,可能会遇到提交没有绿点或者看不到头像的情况,可能是因为配置的 user.email 发生了变化,和平台的 email 不一致导致的。修改成一致的就正常了。

下面来看看怎么设置 user.name 和 user.email。git 配置(configure)分两种,一种是全局的,一种是针对单个项目仓库的(项目目录下)。可以使用 git config 来管理管理配置(查看、设置)

查看配置

可以使用 git config --list 查看当前配置,如果没有 user.name,和 user.email,就说明还没有配置。

# 查看当前目录下项目的配置
git config --list
# credential.helper=osxkeychain
# user.name=zuoxiaobai
# user.email=guoqzuo@gmail.com
# remote.origin.url=git@github.com:zuoxiaobai/fenote.git
# remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
# branch.master.remote=origin
# branch.master.merge=refs/heads/master

# 查看全局配置
git config --list --global
# user.name=zuoxiaobai
# user.email=guoqzuo@gmail.com
# core.quotepath=false

设置(修改配置)

可以使用 git config 属性名 属性值,来设置 git 配置,默认是项目内的(局部的),如果需要设置全局的,需要加上 --global,设置完成后,再使用查看配置的命令 git config --list 就可以看到配置生效了。

# 进入项目目录,单个仓库(项目),局部设置
git config user.name "zuoxiaobai" 
git config user.email "guoqzuo@gmail.com"

# 全局设置
git config --global user.name "zuoxiaobai" 
git config --global user.email "guoqzuo@gmail.com"

WARNING

注意 git config 属性名 属性值 时,如果属性值没有用双引号 "" 包裹,那么中间不能有空格,比如 git config user.name zuo xiaobai,相当于 git config user.name zuo,会丢弃空格后面的部分

更多用法,可以使用 git config --help 查看帮助,或查看对应的官方文档 Git - git-config Documentation (opens new window)

# 用游戏中的场景理解节流与防抖,最简单的 js 实现

之前对节流(throttle) 和防抖(debounce)的理解有偏差,以为 scroll 或 resize 时,为了减少执行事件函数频率,需要用节流,减少执行次数。而防抖在于点击某个按钮后,多长时间内不允许再次点击。防止多次重复提交表单,或执行下一步函数。

这种理解是反的。我们可以用游戏的中的场景来理解节流和防抖

节流(throttle)

对应 技能冷却时间,如果冷却(cooldown/cd)是 5 秒,使用技能后,5秒内,无法再次执行。用于控制事件执行一次后,多长时间内不允许再次执行。一般用于防止按钮多次点击,重复触发事件。

throttle.gif

来实现节流的功能,比较好的实现是,创建一个可复用的节流函数,它可以将普通函数转换为增加了节流功能的函数,基本结构如下

/**
 * throttle
 * @param { Function } func 执行函数
 * @param { Interger } time 多长时间内不能第二次执行
 * @returns function 返回经过节流处理的函数
 */
function throttle(func, time) {
  return (...args) => {
    // 返回一个可以正常执行的函数
    func.apply(this, args)
  }
}

在基本的机构中,增加节流控制处理

/**
 * throttle
 * @param { Function } func 执行函数
 * @param { Interger } time 多长时间内不能第二次执行
 * @returns function 返回经过节流处理的函数
 */
function throttle(func, time) {
  let isLock = false // 是否冷却(cooldown)中
  // 返回一个经过节流处理的 func
  return function (...args) {
    if (isLock) { // 如果是冷却中,不执行函数
      console.log('冷却(cd)中...')
      return 
    }
    // 非冷却中
    func.apply(this, args) // 执行函数
    isLock = true // 执行函数后设置为冷却中
    setTimeout(() => {
      isLock = false // 经过 time 微秒后,设置为非冷却中
    }, time);
  }
}

使用示例

<button id="skillsABtn">释放技能A</button>
<script>
  let skillsABtn = document.querySelector('#skillsABtn')
  releaseASkills = () => {
    console.log(`释放A技能, ${+new Date()}`)
  }
  // 技能冷却时间 3 秒
  skillsABtn.onclick = throttle(releaseASkills, 3000)
</script>

防抖(去抖动 debounce、de + bounce)

对应 回城,在推塔游戏中,回城等待时间是 8 秒。可以理解为回城时,会开启一个定时任务,8 秒后执行完成回城的函数。

防抖/去抖动,就是在定时任务等待执行的时间内,如果再次触发发了函数,会取消上一次的定时任务,重新开始一个定时任务。这样可以将一段时间内连续的多次触发转化为一次触发,单位时间内仅执行最后一次。一般用于 window 的 scroll、resize 事件、搜索框输入搜索内容后实时查询接口等。

function debounce(func, time) {
  let timer = null // 定时任务计时器
  // 返回一个经过防抖处理的 func
  return function (...args) {
    if (timer) {
      // 如果上一次定时任务还在等待执行的过程中,取消定时任务
      clearTimeout(timer)
      console.log('取消上一次的计时任务')
    }

    console.log(`重新开启定时任务,${time} 毫秒后真正执行`)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, time);
  }
}

实例

<button id="goBackBtn">回城</button>
<script>
  let goBackBtn = document.querySelector('#goBackBtn')
  goBack = () => {
    console.log(`成功回城, ${+new Date()}`)
  }
  // 3 秒之后回城
  goBackBtn.onclick = debounce(goBack, 3000) 
</script>

完整 demo 示例代码:节流和防抖实现 - fedemo | Github (opens new window),在线示例 节流与防抖简单实现 demo - 需要打开 console 看执行效果 (opens new window)

# 2021/04/06 周二

# props Right-hand side of 'instanceof' is not an object

在 Vue 中写 props 属性时,一般最简单的方式是使用数组的形式: props: ['属性名1', '属性名2'],但这样没有类型校验、默认值。在使用对象的写法时,发现出现 props Right-hand side of 'instanceof' is not an object,错误,写法如下

export default {
  props: {
    tips: {
      type: “Array”,
      default: () => []
    }
  }
}

后面发现是 type 属性设置的有问题。限定为数组,Array 类型,直接用 Array 就行,不要加引号,它不是字符串,而是对象 object(类)。

tips: {
  type: Array,
  default: () => []
}

参考: Prop 验证 — Vue.js (opens new window)

# 小程序代码丢失后,代码找回过程记录(反编译、云开发函数恢复)

最近想把之前写的一个已上线的小程序开源,发现当时居然没有用 git 管理,换电脑、折腾 mac 双系统后,代码丢失了。尝试用数据恢复软件恢复都没有找回代码,尽管小程序的文件特征很好找,比如 .wxml、project.config.json 等。

后面在网上找了反编译线上小程序的方法。这样可以拿到 uglify 混淆压缩后的代码,至少比没有强。另外,云函数的代码也是可以恢复的,因为之前开发时上传过,它是可以下载的。

WARNING

注意:反编译小程序要求必须是已上线的小程序。对于云函数的恢复/下载,需要知道之前开发的 appid,且微信账号拥有对应的管理员/开发权限。

为什么小程序可以反编译?

理论上小程序也是属于前端代码,前端的 js、css、html 一般是可以拿到的。微信在打开小程序时,会先下载对应小程序的包(.wxapkg文件)来执行。只要我们拿到这个文件,就可以通过反编译拿到混淆压缩后的代码。这里反编译使用的是 wxappUnpacker (opens new window),虽然删除了,但点开 fork,有很多之前备份的代码。

获取小程序的 wxapkg 文件

  1. 下载安装 夜神模拟器 (opens new window),注意:如果是 mac 系统,打开后一直卡在 99%,可能是因为 VirtualBox 的原因,安装该模拟器时,会自动安装 VirtualBox,可以在 app 中手动打开 VirtualBox 并启动,再打开模拟器。
  2. 下载 RE 文件管理器 (opens new window),文件名为:com.speedsoftware.rootexplorer_999496.apk
  3. 在 模拟器中 安装 RE文件管理器,点击模拟器右侧的 添加 apk 文件图标,选择刚才下好的 apk 文件进行安装。如下图,安装完成后打开该 app,并允许获取权限。

wxapkg_1_1.png

  1. 在模拟器中搜索 微信,安装好后,运行微信,登录后,打开对应的小程序
  2. 打开小程序后,在文件管理器的 /data/data/com.tencent.mm/MicroMsg/{数字串}/appbrand/pkg/ 目录,可以看到对应的 .wxapkg 文件,如下图。注意:如果找不到对应的文件,可以点击右上角的三个点,搜索对应的文件。

wxapkg_1_2.png

  1. 导出 .wxapkg 文件。鼠标长按对应的文件,多选两个 wxapkg 文件,点击右上角三个点,zip 压缩,压缩后,查看对应的文件,再点击右上角三个点,发送到微信即可。

wxapkg_1_3.png

使用 wxappUnpacker 反编译 wxapkg 文件

拿到小程序的 wxapkg 文件后,我们将之前在 fork 仓库中下载好的 wxappUnpacker 在 vscode 中打开,使用 Terminal cd(进入)到对应的目录。运行 npm install 或 yarn add 安装对应的依赖。安装完成后,在当前目录下,运行 node wuWxapkg.js 对应的小程序wxapkg文件 命令,就可以得到反编译后的代码。

# 安装依赖
npm install css-tree cssbeautify escodegen esprima js-beautify uglify-es vm2
# 还原小程序代码示例
node wuWxapkg.js '/Users/zuo/Desktop/test1234/_626200936_1.wxapkg'

运行后,虽然生成了代码,但是 terminal 出现了下面的错误,css 文件未被还原

/wxappUnpacker-master/node_modules/vm2/lib/main.js:890
                                throw this._internal.Decontextify.value(e);
                                ^
ReferenceError [Error]: __vd_version_info__ is not defined

这里需要修改 wxappUnpacker 项目中 wuWxss.js 中的 runOnce() 函数代码

// wuWxss.js
// function runOnce(){
// 	for(let name in runList)runVM(name,runList[name]);
// }
function runOnce() {
  for (let name in runList) {
    // console.log(name, runList[name]);
    var start = `var window = window || {}; var __pageFrameStartTime__ = Date.now(); var __webviewId__; var __wxAppCode__={}; var __mainPageFrameReady__ = function(){}; var __WXML_GLOBAL__={entrys:{},defines:{},modules:{},ops:[],wxs_nf_init:undefined,total_ops:0}; var __vd_version_info__=__vd_version_info__||{};
    
    $gwx=function(path,global){
    
    if(typeof global === 'undefined') global={};if(typeof __WXML_GLOBAL__ === 'undefined') {__WXML_GLOBAL__={};
    
    }__WXML_GLOBAL__.modules = __WXML_GLOBAL__.modules || {};
    
    }`;
    runVM(name, start + " \r\n" + runList[name]);
  }
}

修改后再次执行就 OK 了,代码大致就恢复了。目录结构如下

wxapkg_2_1.png

小程序云开发,云函数代码恢复

WARNING

理论上云开发和小程序 appid,微信号开发权限是相关联的,如果没有权限是无法还原云函数的

这个项目是小程序云开发的项目,而 wxappUnpacker 恢复的只是普通小程序的目录结构。下面是小程序云开发的目录结构

wxapkg_2_2.png

我们需要进一步处理

  1. 创建一个新的文件夹,比如 my-app,然后在目录下创建 cloudfunctions 和 miniprogram 文件夹,将恢复的代码,拷贝到 miniprogram 中
  2. 创建 project.config.json 文件,找到之前开发该小程序时,使用的该小程序的 appid,修改 project.config.json 文件
{
  "miniprogramRoot": "miniprogram/",
  "cloudfunctionRoot": "cloudfunctions/",
  "appid": "wx45333c9fc02af773",
  "projectname": "my-app"
}
  1. 打开微信小程序开发工具,选择导入项目,选择 my-app 目录,即可打开项目,如下图

wxapkg_3_1.png

由于我们之前创建过云开发环境,因此我们右键 cloudfunctions 目录,可以选择当前环境比如 test。这时 cloudfunctions 是空的,可以右键后,选择同步云函数列表

wxapkg_3_2.png

同步后,cloudfunctions 目录下就会创建之前的云函数文件夹,如下图,右键对应的云函数文件夹,选择下载,即可下载对应云函数的代码。

wxapkg_3_3.png

云函数代码,不是混淆压缩的,是 100% 还原的,如下图

wxapkg_3_4.png

自此,项目就大致还原了。美中不足的是,样式没有恢复完全,另外 js 都是混淆压缩后的代码,需要慢慢修改还原。

不过项目还是可以跑起来的,比完全丢失了好,还原后的该小程序已开源,后面会慢慢修复还原混淆压缩后的代码。开源地址:remicade-record - github (opens new window)

参考:

# 提示 hints、tips、prompt 以及 message 的区别

在程序开发中,关于提示组件的命名,有 tips、hints、prompt、message 等,一般怎么使用呢?下面来看看

  • tips,n. 小贴士、温馨提示、小窍门。主要用于文字提示,比如 tooltip 组件。

  • hints,n. 暗示、提示,v. 暗示、示意。开发中用的较少,相比于 tips,它有间接含蓄, 暗示,不直接提示的意思。

  • prompt,n. 提示、提词,v. 提示、鼓励、促进。在开发中用的较少,BOM API 中带输入框的提示,使用的就是 prompt window.prompt('最近还好吗?')

  • message n. 消息、信息,v. 通知。主要是全局提示,强调信息、消息。收到一条通知、消息。常用于 message 消息/信息提示/显示。

另外还有一个 alert,属于 警告提示

参考: 英语hints和tips区别? (opens new window)

上次更新: 2022/11/28 22:03:27