# 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) 下的回复:
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 中,防止调试时刷新页面后需要再次手动切换语言。
<!-- 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;
合并后的效果如下图,不同的模块放到不同的大对象上
在 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,我们需要处理两个问题:
- 用户选择国际化币种后,表单 v-model 值自动变成 code
- 当后端返回币种 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 实现优缺点对比
让 footer 一直保持在最底部是比较常见的需求,css 和 js 都可以实现,一般推荐使用 css 实现,下面来看看具体实现,以及他们的优缺点
以下面的结构为例
<body>
<article class="container">
<header>顶部</header>
<section class="main">中间内容部分</section>
<footer>底部</footer>
</article>
</body>
需要考虑两种情况:
- 内容没占满视窗时,footer 在最底部,需要 body 有最小高度,才能撑起来
- 内容较多时,滚动到底部才能看到 footer,且不遮挡内容区域
# 方法 1:css 方式 - position: absolute
- footer 使用 position: absolute; bottom: 0; 保持在底部。
- 对于可能遮挡中间内容区域的问题,将 body 设置为 relative,注意不是 container,这样可以让 footer 内容保持在body最底部
- 内容较多时,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 示例如下
http://www.xx.com/region/gd/module
http://www.xx.com/region/gd-c222/module
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
* @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 文件
- 下载安装 夜神模拟器 (opens new window),注意:如果是 mac 系统,打开后一直卡在 99%,可能是因为 VirtualBox 的原因,安装该模拟器时,会自动安装 VirtualBox,可以在 app 中手动打开 VirtualBox 并启动,再打开模拟器。
- 下载 RE 文件管理器 (opens new window),文件名为:com.speedsoftware.rootexplorer_999496.apk
- 在 模拟器中 安装 RE文件管理器,点击模拟器右侧的 添加 apk 文件图标,选择刚才下好的 apk 文件进行安装。如下图,安装完成后打开该 app,并允许获取权限。
- 在模拟器中搜索 微信,安装好后,运行微信,登录后,打开对应的小程序
- 打开小程序后,在文件管理器的
/data/data/com.tencent.mm/MicroMsg/{数字串}/appbrand/pkg/
目录,可以看到对应的 .wxapkg 文件,如下图。注意:如果找不到对应的文件,可以点击右上角的三个点,搜索对应的文件。
- 导出 .wxapkg 文件。鼠标长按对应的文件,多选两个 wxapkg 文件,点击右上角三个点,zip 压缩,压缩后,查看对应的文件,再点击右上角三个点,发送到微信即可。
使用 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 了,代码大致就恢复了。目录结构如下
小程序云开发,云函数代码恢复
WARNING
理论上云开发和小程序 appid,微信号开发权限是相关联的,如果没有权限是无法还原云函数的
这个项目是小程序云开发的项目,而 wxappUnpacker 恢复的只是普通小程序的目录结构。下面是小程序云开发的目录结构
我们需要进一步处理
- 创建一个新的文件夹,比如 my-app,然后在目录下创建 cloudfunctions 和 miniprogram 文件夹,将恢复的代码,拷贝到 miniprogram 中
- 创建 project.config.json 文件,找到之前开发该小程序时,使用的该小程序的 appid,修改 project.config.json 文件
{
"miniprogramRoot": "miniprogram/",
"cloudfunctionRoot": "cloudfunctions/",
"appid": "wx45333c9fc02af773",
"projectname": "my-app"
}
- 打开微信小程序开发工具,选择导入项目,选择 my-app 目录,即可打开项目,如下图
由于我们之前创建过云开发环境,因此我们右键 cloudfunctions 目录,可以选择当前环境比如 test。这时 cloudfunctions 是空的,可以右键后,选择同步云函数列表
同步后,cloudfunctions 目录下就会创建之前的云函数文件夹,如下图,右键对应的云函数文件夹,选择下载,即可下载对应云函数的代码。
云函数代码,不是混淆压缩的,是 100% 还原的,如下图
自此,项目就大致还原了。美中不足的是,样式没有恢复完全,另外 js 都是混淆压缩后的代码,需要慢慢修改还原。
不过项目还是可以跑起来的,比完全丢失了好,还原后的该小程序已开源,后面会慢慢修复还原混淆压缩后的代码。开源地址:remicade-record - github (opens new window)
参考:
- 小程序源码丢失了怎么在微信平台反编译找回 (opens new window)
- 最新解决小程序反编译$gwx is not defined和__vd_version_info__ is not defined (opens new window)
- 2020微信小程序反编译(逆向),仅用于学习请勿商用 (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,属于 警告提示。