projecthabits

文章目录

项目习惯

在开始项目的时候,想想业务逻辑,可以画画图看看各个业务之间的联系。再先划分目录,比如在 vue 项目中,划分 src 的目录,别一开始直接动手。

vue-cli4 (不选择 vue-router 和 vuex 的情况下)中已经默认在 src 目录下建立了 viewsassetscomponents 三个文件夹。

大目录划分

这时候,我们需要新建几个目录。

  • 新建 store 目录,用于 vuex

  • 新建 router 目录用于 vue-router

  • 新建 plugins 用于之后可能会引用的插件库,如 element-ui

  • 新建 network 用于网络请求的封装,先建立一个简单封装 axios 的 js 文件

  • 新建 common 目录,用于存放可能会定义的常量、工具代码、混入代码等,里面可能放置 utils.jsconst.jsmixin.js 这种文件。

大的目录划分好了之后再划分小的目录。

小目录划分

  • components 中新建几个目录

    • 新建 common 目录,里面存放完全公共的组件,与业务完全无关的组件,可以直接拖到下个项目中复用的组件
    • 新建 content 目录,里面存放与业务相关的组件,可能是多个组件复用了这个组件,所以放到这里更加合适,无法被下个项目复用
  • assets 中新建几个目录

    • 新建 css 目录,存放公共样式及初始化样式。在创建项目的时候,一般都会直接去找到 normalize.css 这个 css 文件,并引入,github 上直接搜或者 npm install normalize.css --save 就行,用于把各种标签让它们样式变得差不多,风格统一。之后你可以自己新建一个 base.css ,用于控制项目整体的基本设置。先把这两个 css 文件搞定,样式的初始化就完成了。之后自己如果还有代码,就接着写在 base.css 里。
    • 新建 img 目录,存放图片,到时候每个页面用到的资源也单独分一个文件夹,如在 img 目录下新建 homedetailprofile 等文件夹
    • 新建 font 目录,存放可能引用的字体图标(阿里矢量图标库)

单个文件整理

.editorconfig

在根目录下新建 .editorconfig ,里面配置项目的代码风格,vue-cli4 中已经自动生成了,但你可以修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root = true                     根据 root 来解析,只有 root 为 true 时才会解析下面的。

[*] * 表示所有文件
charset = utf-8 表示使用 utf-8 编码
indent_style = space 表示使用空格缩进而不是 tab
indent_size = 2 表示缩进 2 个空格
end_of_line = lf 表示换行用的字符为 lf,不同操作系统中换行的字符是不一样的
insert_final_newline = true 表示在代码最后一行的下面拆入一行空行
trim_trailing_whitespace = true 表示清除每行代码结束之后的无用空格,
如 const a = 123, 这前面的一直到 , 的空格都会被删除

这个文件定制了代码格式,同时 VSCode 上有插件可以让你每次格式化的时候自动调用项目下的 .editorconfig 文件的格式来格式化。

关闭 eslint,在 vue.config 里写一下,useEslint: true 改成 useEslint: false 即可

vue.config.js

在根目录下新建 vue.config.js,可以自定义 webpack 配置。一般先配置一下目录别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
configureWebpack: {
resolve: {
extensions: ['.js', '.vue'], // 用来省略后缀名的,这里只是举个例子, 这个配置在 vue-cli 里已经写好了。
alias: { // 路径别名, resolve() 方法需要 require('path') 导入 path 模块,vue-cli 已经导入了
// '@': path.resolve('src'), // 这个在 vue-cli 里已经写了,拿来用就行
'assets': '@/assets',
'components': '@/components',
'views': '@/views',
'network': '@/network',
'common': '@/common'
}
}
}
}
// 这样设置别名之后,在 js 区域,就只要 import 'components/Home.vue' 就引入组件了,不需要 import '../../components/Home.vue' 这样写一堆 ../ 了, 但注意的一点是,在 HTML 区域,得 在 路径前加上 ~ ,如 <img src="~assets/img/home/img1.png">

base.css

assets/css 目录下新建 base.css 文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@import "./normalize.css";

:root { /* 这个伪类指代 html 标签,我们一般这样写,而不是写 html */
/* 下面这一堆 -- 开头的,表示自定义的变量,是的 css 也能自定义变量,不是只有预编译器才有,到时候使用就可以写成 如 font-size: var(--font-size) 的写法来使用自定义的变量 */
--color-text: #666;
--color-high-text: #ff5777;
--color-tint: #ff8198; /* 一般指整体的背景颜色,导航这些地方 */
--color-background: #fff;
--font-size: 14px;
--line-height: 1.5;
}

*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
user-select: none; /* 禁止用户选中文字图标等 */
-webkit-tap-highlight-color: transparent; /* webkit 是苹果浏览器引擎,tap点击背景高亮,color颜色,颜色用数值调节 */
background: var(--color-background);
color: var(--color-text);
width: 100vw;
}

a {
color: var(--color-text);
text-decoration: none;
}

.clear-fix::after { /* 清除浮动,以后只要用到了浮动的元素给它这个类名即可 */
clear: both;
content: '';
display: block;
width: 0;
height: 0;
visibility: hidden;
}

.clear-fix {
zoom: 1; /* 针对 IE 低版本(IE6\7) 起到类似 BFC 的效果 */
}

.left {
float: left;
}

.right {
float: right;
}

引入 request.js 的网络请求

将每个页面需要请求的接口全部再多封装一层,达到统一控制的效果,只需要控制新的这个 js 文件,就可以改变多个页面的请求配置。举例,比如有个 home 组件需要发送请求,我们就在 network 目录下新建一个 home.js 来统一控制

1
2
3
4
5
6
7
8
// network/home.js
import { request } from './request'

export function getHomeMultiData() {
return request({
url: '/home/multidata'
})
}

import 的顺序与分组

在组件中引入已封装好的内容时,需要分组,如下,下面的注释内容在真正写的时候应该为空行,用于分组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
// 子组件
import HomeSwiper from './childComps/HomeSwiper'
import HomeRecommendViews from './childComps/HomeRecommendViews'
import HomeFeatureViews from './childComps/HomeFeatureViews'
// 公共组件
import NavBar from 'components/common/navbar/NavBar'
import TabControl from 'components/content/tabControl/TabControl'
// 方法
import { getHomeMultiData } from 'network/home'
// 并且注册组件时也应该按照上面的导入顺序
export default {
components: {
HomeSwiper,
HomeRecommendViews,
HomeFeatureViews,
NavBar,
TabControl
},
}
</script>

复杂数据的处理

遇到复杂数据时,先决定一下要使用什么样的数据结构再开始写

1
2
3
4
5
6
7
8
goods: { // 举例总共三组数据,每组数据当前 page 都不一样,需要分组记录,各自独立自己的 page
'pop': {page: 1, list: [](30个)},
'news': {page: 5, list: [](150个)},
'sell': {page: 2, list: [](60个)}
}

// 再复杂点可能使用 Map 结构来映射,像这样
// Map -> Map -> Array

提取数据的处理

有时候会遇到很分散的数据,他们分布在不同对象中,他们里面有很多属性(可能有的数据对我们来说一点用也没有,因为它们是给 Android 或 iOS 用的,大家用的都是同一个 API 服务器),但我们需要同时使用它们这几个对象里面的某几个属性。这时候就需要把松散的数据整合一下,再进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// detail.js
export class Goods {
// itemInfo 这个对象里可能还有 itemInfo[a], itemInfo[b],itemInfo[c] 这样的属性,这样写我们就把其他的属性都被过滤了,只取出了我们需要的并且把它们重新整合起来成为一个对象。
constructor(itemInfo, columns, services) {
this.title = itemInfo.title
this.desc = itemInfo.desc
this.newPrice = itemInfo.newPrice
this.columns = columns
this.services = services
}
}

// 在某个组件中使用
import { Goods } from 'network/detail.js // 这个类和 network 写一起了,所以从这里引

{
created() {
getData().then(res => {
const data = res.result
this.goods = new Goods(data.itemInfo, data.column, data.shopInfo.services)
})
}
}

对象的方法的调用(异步)

我们在对对象上的方法进行调用时,需要先做一层判断,如果存在这个对象(或者对象不为空),我们才尝试进行调用方法,否则能回变成调用 null 这个对象上的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj = null
// 模拟异步获取 obj
setTimeout(() => {
obj = {
func() {}
}
}, 1000)

// 调用时的写法
obj && obj.func()

// 更严谨的写法
obj && obj.func && obj.func()

// 判断是否为空对象 也可以这样写
Object.keys(obj).length !== 0 && obj.func()

数组长度的获取(异步)

我们在获取数组的长度时,需要先做一层判断,该数组长度是否为 0,之后再获取长度。并且之后如果有某个属性想实时监听数组的长度,可以使用 watch 来监听该数组的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
data() {
return {
arr: []
arrLength: 0
}
},
created() {
// 模拟异步获取 arr
setTimeout(() => {
this.arr = [1, 2, 3]
}, 1000)
this.arrLength = this.arr.length !== 0 && this.arr.length
},
watch: { // 监听数组的变化,一旦发生变化,给 arrLength 赋予新值。
arr() {
this.arrLength = this.arr.length
}
}

}

计算属性的返回值

在计算属性中,我们可能要根据请求的结果返回不同的值,我们可以使用 ||

1
2
3
4
5
6
7
8
9
10
// 某个可复用组件中
{
computed: {
showImg() {
// 发送请求得到 product 后,可能不同页面中引用该组件得到的数据不同,可能这个页面的返回对象有 image 属性,另一个页面的返回对象没有,它只有 show.img 属性,但我们想做到两个页面共同使用同一组件,就可以这样。
// 先找 image 属性,如果找到了直接返回(短路),如果没有 image 属性, 就去找 show.img。当然这样写必然是先检测 image,再检测 show.img
return this.product.image || this.product.show.img
}
}
}

better-scroll

它是移动端的滚动插件。优化滚动,原生滚动太卡了,并且这个插件有触底回弹效果。它是用 transform: translate 来实现上下滚动的,所以写在 .wrap 里的原生的 position: fixed 会失效,因为并不是由于内容高度撑起导致的滚动条,所以 fixed 无效。在 translate 向上移动的时候, 被 fixed 的元素也被上移了。

注意点

它需要挂载 DOM,所以应该在 mounted 钩子内调用,并且必须给该容器元素一个固定高度,该高度必须小于它的直接子元素高度,并且它的直接子元素只允许有一个,我们可以在它的直接子元素内填写相应的内容,不允许直接把内容直接添加在容器元素内并成为容器元素的直接子元素。

最后记得给容器加上 overflow: hidden

由于它替代了原生的滚动,所以如果需要对滚动进行处理,需要先拿到 new BScroll 这个对象实例,使用它的方法来触发滚动事件。

refresh

注意涉及异步的操作都需要使用 BS 的 refresh() 方法来重新确定 .content 这个 DOM 的高度,如涉及图片加载需要用到 图片的 onload 事件,表示图片加载完成,再调用一次 refresh() ,网络请求成功的 success 也需要 refresh()

keep-alive

如果使用 BS 的组件被包裹在 <keep-alive> 内,则每次 activated 都需要先 refresh,再进行其他的 BS 操作,否则高度获取会出现问题。如果想保存当前 BS 高度,让下次激活页面时仍停留在这里,写法是

1
2
3
4
5
6
7
8
9
{
activated() {
this.$refs.scroll.refresh()
this.$refs.scroll.scrollTo(0, this.saveY, 0) // 参数(x, y, durationTime)
},
deactived() {
this.saveY = this.$refs.scroll.scroll.y // 拿到名为 scoll 的组件里的 scroll 对象的 y
}
}

关于图片、请求等异步操作

如果需要获取涉及图片的 DOM 元素的高度,需要用到 图片的 onload 事件,表示图片加载完成,此时再获取 DOM 才是真正的高度,否则由于图片异步加载,获取高度时图片可能还未加载,高度为 0,获取到错误高度。应该在 onload 事件的回调函数中获取 DOM 高度。

同理,发送网络请求也应该在 success 的回调函数中获取 DOM 高度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!-- 正确写法 -->
<div class="wrap">
<ul class="contentWrap">
<li></li>
<!-- ... -->
<li></li>
</ul>
</div>

<!-- 错误写法,因为直接子元素仅允许存在一个。 -->
<div class="wrap">
<p class="wrapItem"></p>
<!-- ... -->
<p class="wrapItem"></p>
</div>

<script>
// probe: 侦测
const bs = new BScroll('.wrap', {
probeType: 0, // 默认为 0,值为 0 时,不监听实时位置, 1 表示会在屏幕滑动超过一定时间后派发 scroll 事件,2 表示在手指滚动的过程中侦测,手指离开后的惯性滚动过程和回弹过程不侦测,3 表示 只要是滚动就侦测,惯性和回弹也侦测
click: false, // 默认为 false,不监听容器内部 非表单元素 的点击事件
pullUpLoad: false // 默认为 false,不开启上拉加载时间,也就是页面拉到底了之后会触发的钩子
})

bs.on('scroll', position => {
console.log(position) // 实时滚动位置
})

bs.on('pullingUp', () => {
console.log('上拉加载更多')

// 设置延时,模拟异步请求
setTimeout(() => {
// 表示结束上拉加载,调用这个方法之后,才会在触底时再次触发上拉加载事件,否则 '上拉加载更多只会打印一次'
bs.refresh() // 由于 better-scroll 是获取 .wrap 和 .content 的高度来决定可以滚动的范围。但图片加载和网络请求是异步的,可能会影响获取的 .content 高度,而此时 .content 的高度已经被 BS 获取了(BS 将会遍历 .content 里的元素,获取高度),比如此时 .wrap 是 50px, .content 是 100px, 但图片加载完成后 .content 是 1000px, 但此时可滚动距离已经被确定好了,是 50px,即使后来图片加载完成,可滚动范围仍然是 50px。因此有了 refresh() 这个方法,作用是重新获取 .content 的高度,应当在图片触发 onload 事件时,重新调用一次,BS 的高度可以通过 bs.scrollerHeight 获取
bs.finishPullup()
}, 2000)
})

// 到达指定位置的方法 bs.scrollTo(x, y, time) ,如
bs.scrollTo(0, 0, 300) // 达到 0, 0 的位置,滚动的动画效果将持续 300ms
</script>

<!-- 如果同时需要上下(上有 navbar 下有 tabbar)有一个空出来的高度,可以用绝对定位,也可以用 calc -->
<style>
.wrap {
position: absolute;
top: 44px;
bottom: 49px;
left: 0;
right: 0;
}
</style>

当然,如果用原生的 scroll (overflow: scroll) ,就只需要一层包装即可,不像上面需要两层才能写真正的内容。

插件库的依赖问题

不要直接依赖任何插件库,最好都自己再封装一层,让这些组件依赖于自己封装的 js 文件,让自己封装的 js 文件依赖于插件库,这样如果插件库出了问题,就只需要修改自己封装的 js 文件即可,不需要到每个组件中去替换并引入新的插件。

1
2
3
// 不要这样写
import BScroll from 'better-scroll'
import axios from 'axios'

防抖节流

虽然我们有时要用到防抖节流,但用户卡了也不知道是网页的问题还是网络的问题。。就这样,卡了等于自己的问题hhh

防抖

当某个事件频繁触发导致某个事件处理函数频繁调用的时候,可以考虑使用防抖来提升性能。将被频繁调用的函数传入 debounce 来生产新的优化过后的函数,并把事件处理函数修改成它。效果是,当连续的几次事件触发事件非常短时,前几次方法都将被取消 (clearTimeout) ,只调用最后一次方法。

1
2
3
4
5
6
7
8
9
function debounce(func, delay) {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}

节流

在一定时间内只调用一次真正的处理函数 (外面的 if 将被执行多次) 。不管事件触发多少次。

1
2
3
4
5
6
7
8
9
10
function throttle(func, delay) {
let flag = true
return function (...args) {
if (flag) return false
setTimeout(() => {
func.apply(this, args)
flag = tue
}, delay)
}
}

z-index

z-index 只对定位元素起效果,所以要用的话,至少加个 position: relative;

如果元素脱离了标准流,如果它和另一个标准流元素在同一个位置展示,它将会覆盖在标准流元素的上面。

数据提交(文件上传)

注意:当一个表单包含<input type="file">时,表单的 enctype 必须指定为 multipart/form-datamethod 必须指定为 post ,浏览器才能正确编码并以multipart/form-data格式发送表单的数据。

出于安全考虑,浏览器只允许用户点击<input type="file"> 来选择本地文件,用JavaScript对<input type="file">value 赋值是没有任何效果的。当用户选择了上传某个文件后,JavaScript也无法获得该文件的真实路径。

常见的数据提交有两种形式(还可以有 websocket,但这种方法需要服务器单独开发接口,而且它也无法获取上传进度,比较少见,还有 flash,已经淘汰了)。

form

使用 form 进行提交,这是比较古老的提交方式,在以前都是后端老哥干的,那时候没有 ajax,由于这种提交方式是同步的,并且点击之后直接跳转到请求地址,所以一般会在页面内嵌入 iframe。

表单有几个属性用来控制提交表单时的行为,详见 MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<form action="url" method="post" enctype="multipart/form-data">
<!--
action: 处理表单提交的 URL,这个值可被 <button>、<input type="submit"> 或 <input type="image"> 元素上的 formaction 属性覆盖。它应该是一个绝对路径,因为一般前端网页服务器和后端 API 接口地址不同,无法使用相对路径提交。
method: 浏览器使用这种 HTTP 方式来提交表单,可能的值有
post: 指的是 HTTP POST 方法,表单数据会包含在表单体内然后发送给服务器
get: 指的是 HTTP GET 方法,表单数据会附加在 action 属性的 URL 中,并以 ? 作为分隔符。
dialog: 如果表单在 <dialog> 元素中,提交时关闭对话框。这个属性值很少见。
enctype: 当 method 属性值为 post 时, enctype 就是将表单的内容提交给服务器的 MIME 类型,可能的取值有三种
application/x-www-form-urlencoded: 默认值
multipart/form-data: 当表单包含 type="file" 的 input 元素时使用
text/plain: 出现于 HTML5,用于调试
这个值可被 <button>、<input type="submit"> 或 <input type="image"> 元素上的 formaction 属性覆盖。
-->
<input type="file" name="file">
<input type="submit" value="提交">
</form>

<!--
上面的请求头看起来像这样
#request header
POST /foo HTTP/1.1
Content-Length: 68137
Content-Type: multipart/form-data; boundary=---------------------------974767299852498929531610575

#request body
---------------------------974767299852498929531610575
Content-Disposition: form-data; name="description"

some text
---------------------------974767299852498929531610575
Content-Disposition: form-data; name="myFile"; filename="foo.txt"
Content-Type: text/plain

(content of the uploaded file foo.txt 显示 foo.txt 这个文件的内容)
---------------------------974767299852498929531610575

而普通表单的请求头长这样
#request header
POST /foo HTTP/1.1
Content-Length: 68137
Content-type : x-www-form-urlencoded;setchart:UTF-8

#request body
form-data
name:xxx
age:111
-->

AJAX

这种就是我们比较熟悉的写法了,在 Vue 中一般通过 axios 来完成。

FormData 接口提供了一种表示表单数据的键值对的构造方式,经过它的数据可以使用了XMLHttpRequest.send() 方法送出,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div>
<input type="file" @change="doAjaxUpload($event)">
<img :src="img" alt="" @dblclick="delImg">
</div>
</template>

<script>
import axios from 'axios'
export default {
data() {
return {
img: ''
}
},
methods: {
doAjaxUpload(e) {
// 可以通过 e.target.files 得到上传文件的列表
const file = e.target.files[0]
// FormData() 对象用以将数据编译成键值对,以便用 XMLHttpRequest 来发送数据。
const fd = new FormData()
fd.append('file', file)
/*
你还可以通过 fd.append 添加其他参数
fd.append('name', 'zhangsan')
fd.append('number', '150121123')
*/

axios.post('yourUrl', fd)
.then(res => {
// 上传成功后,让 img 标签显示服务器上的图片路径
this.img = this.host + '/' + res.data.url
})
}
}
}
</script>

axios

在 axios 中,发送 post 请求时,它默认会将请求头的 Content-Type 变成 application/json;charset=utf-8,然后直接发送 JSON 数据,但实际上,我们的后端一般要求的都是 'Content-Type': 'application/x-www-form-urlencoded',这时候我们需要先将请求头的 Content-Type 改一下,然后引入 qs,用于修改我们的数据格式,后端需要的数据一般都是键值对,而不是 JSON,因为后端处理起 JSON 格式较为麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
这是一个 post 和 get 请求的封装。。使用如
import request from './request'
getR(data) {
return request({
url: 'yourUrl', // method 默认为 get,可以不写了
data // 由于下面封装的参数名为 data, 所以这里也写 data,不用写 params 了,达成 get 和 post 传参的统一
})
},
postR(data) {
return request({
url: 'yourUrl',
method: 'post',
data // 由于下面封装的参数名为 data, 所以这里也写 data,不过 post 本来就是 data,达成 get 和 post 传参的统一。下面封装的参数名想修改成别的名字那么这里也记得修改即可
})
}
*/
import axios from 'axios'
import qs from 'qs'

const service = axios.create({
timeout: 5000 // request timeout
})

function request(configOptions) {
const config = {}
config.url = configOptions.url
if (configOptions.method?.toLowerCase() === 'post') {
config.method = 'post'
config.headers = {}
// 修改请求头的 Content-Type
config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
// 以下就是 qs 的作用,帮我们把对象或数组转成键值对
// 1. { a: { b: { c: 'd', e: 'f' } } } ==> 'a.b.c=d&a.b.e=f' (这是 allowDots: true 的作用,不然默认会变成 a[b][c]=d&a[b][e]=f,作用也就是把默认的 [] 转成 .)
// 2. { a: ['b', 'c'] } ==> 'a=b&a=c' (这是 arrayFormat: 'repeat' 的作用,一般后端接收的都是这种数据,不然默认会变成 a[0]=b&a[1]=c,不合要求,当然具体用哪种还是和后端交流)
config.data = qs.stringify(configOptions.data, { allowDots: true, arrayFormat: 'repeat' })
} else {
config.params = configOptions.data
}
return service(config)
}

export default request

Vue 的 data 数据重置

this.$options.data 里保存着最开始定义的 data 函数,可以通过 Object.assign(this._data, this.$options.data) 来重置成最开始我们设置的数据,但是它也会把网络请求得到的数据一并消除,因为最开始定义的那些数据都为空值。this.$options.data 实际是 this.$options.__proto__.data, data 从原型链上继承而来。

日期格式处理

请求时我们经常从后端拿到时间戳,并且页面展示的要求是 yyyy-mm-dd,所以我们需要对它进行转化,而由于时间戳转日期格式的需求非常常见,所以我们需要把它封装一下以供复用。不要每次都用 + 拼接字符串了。

不过, js 里没有内置这种格式化日期的方法,需要手动实现,其他语言大部分都有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* utils.js */
export function formatDate(data, fmt) {
// 年与下面的处理不一样,因为它不需要加前置 0
if (/(y+)/.test(fmt)) {
// substr 根据写的 y 的个数决定从哪里开始截取,如 2020,写 yyyy,从第 0 位,变成 2020;写 yyy 从第一位,变成 020;写 yy 从第二位,变成 20;写 y 从第三位,变成 0。
fmt = fmt.replace(RegExp.$1, (data.getFullYear() + '').substr(4 - RegExp.$1.length))
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
const str = o[k] + ''
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 2) ? str : padLeftZero(str))
}
}
return fmt
}

function padLeftZero(str) {
// 先在前面加两个 0,再根据字符串长度决定从哪里开始截取,如 len = 1,就从第一位开始截取。从 X 变成 00X 再变成 0X 返回
return ('00' + str).substr(str.length)
}

/* 使用 */
import { formatDate } from 'common/utils.js'

// 自动替换 yyyy 这些字符,而 '-' 和 ' ' 将会被保留。 只想要年,就写成 'yyyy',只想要日就写成 'dd'
const date = formatDate(1383910321802, 'yyyy-MM-dd hh-mm-ss')

关于 this.$nextTick() 的使用

由于 mounted 不会承诺所有的子组件也都一起被挂载,因此可能会遇上这样的情况。

在页面中,如果引入了很多组件,那么显然,当该页面自身的元素渲染完毕时,组件内的元素可能还未渲染完毕,此时获取 DOM 可能并不正确,如果希望在全部组件都渲染完毕时 (图片等资源的内容可能是异步的,但该元素已被加载到页面中,此时可能拿不到正确的图片大小) 获取 DOM,可以在 mounted 钩子中加入 this.$nextTick ,它里面的回调函数将在下一次 DOM 渲染完毕时调用。

1
2
3
4
5
6
7
{
mounted() {
this.$nextTick() {
// ... 操作 DOM
}
}
}

for … in …

for ... in ... 是对象遍历使用的,但如果用在数组上,返回的 key 就是下标,而且是字符串类型的下标。注意使用。

判断一个数的区间

需求是给一个数,拿到该数所在的区间下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const zo = [0, 2000, 4000, 6000]
/*
0: 0 ~ 2000 [0, 2000)
1: 2000 ~ 4000 [2000, 4000)
2: 4000 ~ 6000 [4000, 6000)
3: 6000 + [6000, +∞)
*/
function getZo(num) {
// 普通写法
for (let i = 0; i < zo.length; i++) {
if ((i < zo.length - 1 && num >= z[i] && num < z[i + 1]) || (i === zo.length - 1 && num >= z[i] ) ) {
return i
}
}

// 普通写法的 hack 写法,在数组中最后 push 入一个无穷大(Number.MAX_VALUE),然后就可以把 关于 i 与 length 的判断和 || 之后的判断都删去了,变成 if (num >= zo[i] && num < zo[i+1]),同时 for 的结束条件改为 i < zo.length - 1。 (PS:无穷大可能会多占内存,但空间换时间就是这样吧。)
const zoClone = [...zo, Number.MAX_VALUE]
for (let i = 0; i < zoClone.length - 1; i++) {
if (num >= zoClone[i] && num < zoClone[i + 1]) {
return i
}
}

// 反序写法,推荐。
for (let i = zo.length - 1; i >= 0; i--) {
if (num > z[i]) {
return i // 检索到了就必须中断循环,否则下一个 i 的值必定也满足条件,将覆盖掉当前正确的值。当然用 break 也行。
}
}
}

Vuex

每个 mutation 都建议执行单一功能,比如在购物车中,可能会有 addCart (添加购物车) 的 mutation,它里面可能会有 商品数量 + 1 ,和添加新商品两种功能。我们应该把这两种功能写成两个 mutation,而不是被整合到 addCart 中。而判断数量 + 1还是添加新商品的逻辑,建议放到 action 中,它不止可以处理异步,还可以处理复杂的逻辑。

Vuex 的文件管理

Vuex 的各个部分也应该被拆分。以下是简单的购物车例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

import mutations from './mutations.js'
import actions from './actions.js'
import getters from './getters.js'

Vue.use(Vuex)

// 创建 state 对象,
const state = {
cartList: []
}

const store = new Vuex.store({
state,
mutations,
actions,
getters
})

export default store

// store/mutations.js
// 导入方法名的变量
import {
ADD_COUNTER,
ADD_TO_CART
} from './mutation-types'

export default {
// 变量名不能直接作为函数名,需要加上 []
[ADD_COUNTER](state, payload) {
payload.count++
},
[ADD_TO_CART](state, payload) {
payload.checked = false // 注意,如果需要用到 checkbox 的选中与否,一定是在商品的对象中定义的,不要新建映射,这一点很重要。
state.cartList.push(payload)
}
}

// store/actions.js
import {
ADD_COUNTER,
ADD_TO_CART
} from './mutation-types'

export default {
// 有时候也会把 context 解构,写成 addCart({ state, commit }, payload) 直接拿取 context 对象上的 state 和 commit 方法
// 这里虽然不是异步操作,但复杂逻辑应该写在 actions 而不是 mutations 中
addCart(context, payload) {
let oldProduct = context.state.cartList.find(item => item.id === payload.id)
if (oldProduct) {
context.commit(ADD_COUNTER, oldProduct)
} else {
payload.count = 1
context.commit(ADD_TO_CART, payload)
}
}

/*
想写个异步也行,直接返回 Promise 即可
addCart(context, payload) {
return new Promise((resolve, reject) => {
let oldProduct = context.state.cartList.find(item => item.id === payload.id)
if (oldProduct) {
context.commit(ADD_COUNTER, oldProduct)
resolve('商品数量 + 1')
} else {
payload.count = 1
context.commit(ADD_TO_CART, payload)
resolve('添加商品')
}
})
}
*/
}

// store/getters.js
export default {
cartLength(state) {
return state.cartList.length
}
}

// store/mutation-types.js ,这个文件定义常量,如果需要修改方法名,直接在这里修改会非常方便。
export const ADD_COUNTER = 'add_counter'
export const ADD_TO_CART = 'add_to_cart'

css 顺序

改变布局 》 改变盒子的 》 颜色之类的普通样式调整。如

1
2
3
4
5
6
7
8
9
10
.content {
position: relative;
display: flex;

height: 40px;
width: 40px;

color: red;
background: grey;
}

移动端的点击优化

由于移动端的 click 事件默认有 300ms 的延迟判断时间(用于判断是否为双击放大),但我们基本上不需要这个 300ms 的延迟。所以引入 fastClick 来处理这 300ms 的延迟。

1
2
3
4
5
6
// npm 安装
npm install fastclick --save
// main.js
import FastClick from 'fastclick'
// 插件内置方法, attach 到 body 上
FastClick.attach(document.body)

图片懒加载

有时候我们希望当我们当前视口里能看到图片时才加载图片,可以使用 vue-lazyload 插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// npm 安装
npm install vue-lazyload --save
// main.js
import VueLazyLoad from 'vue-lazyload'

// 简单实用
// Vue.use(VueLazyLoad)

// 允许传入参数使用
Vue.use(VueLazyLoad, {
// 有很多参数,详细可以去看看文档,这里随便例举两个。
loading: require('~assets/img/common/loading.png'), // 图片在加载时显示的图片,需要用到 require() 获取图片, import() 还未尝试。
error: require('~assets/img/common/error.png') // 图片加载失败显示的图片
})

// 组件中使用,把原本的 <img :src="imgSrc"> 的 :src 改成 v-lazy 就会使用懒加载
/*
<template>
<div>
<img v-lazy="imgSrc">
</div>
</template>
*/

px2rem、px2vm

移动端响应式的解决方案,将 px 转换成 rem 或者 vw。可以下载 px2rem 或 px2vw 这种插件,它们可以帮我们写的 px 在打包时转化为 rem 或 vw,两个选一个即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 安装 px2vm,它的配置项需要单独新建一个 postcss.config.js
npm install postcss-px-to-viewport --save-dev

// postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
'postcss-px-to-viewport': {
viewportWidth: 750, // 视窗的宽度,对应的是我们设计稿的宽度,一般是750
viewportHeight: 1334, // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数
viewportUnit: "vw", //指定需要转换成的视窗单位,建议使用vw
selectorBlackList: ['.ignore'],// 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false // 允许在媒体查询中转换`px`
}
}
}

// 安装 px2rem,它是一个 loader 去 vue.config.js 中配置。
npm install px2rem-loader --save-dev

题外

另一种封装及使用组件的方式

我们希望在全局都能使用某个弹窗(toast)时,可以使用这种形式的组件封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 组件内容在该代码框最后。
/* components/common/toast/index.js */
import Toast from './Toast.vue'

const obj = {}

obj.install = function (Vue) {
// 创建组件构造器
const ToastConstructor = Vue.extend(Toast)
// 拿到组件实例对象
const toast = new ToastConstructor()
// 把这个组件挂载到新建的 div 元素上
toast.$mount(document.createElement('div'))
// 把新建的挂载着 toast 组件的 div 添加到 body 中,该 div 通过 toast.$el 来获取
document.body.appendChild(toast.$el)
// 把这个组件实例挂载到 Vue 的原型上,此时可以使用该组件的方法。
Vue.prototype.$toast = toast

/*
因为此时 import 而来的 Toast 已经是被编译过的,所以不能直接加入到 body 中,必须挂载到某个元素上,然后在页面初始化时该元素会被加入到 body 中,之后由 Vue 把该元素替换为 Toast 元素。这也是上面代码的流程。
document.body.appendChild(Toast.$el) // error
*/
}

export default obj

/* main.js */
import Vue from 'vue'
import toast from 'components/common/toast/index.js'

// Vue.use 实际上是调用传入对象上的 install 方法。
Vue.use(toast)

/* 某组件中使用 */
{
methods: {
toastShow() {
this.$toast.show(msg)
}
}
}

/* components/common/toast/Toast.vue */
/* 以下是组件内容,由于高亮会出错,直接整体注释了。
<template>
<div class="toast" v-show="isShow">
{{msg}}
</div>
</template>

<script>
export default {
data() {
return {
msg: 'hhh',
isShow: false
}
},
methods: {
show(msg, duration = 2000) {
this.isShow = true
this.msg = msg

setTimeout(() => {
this.isShow = false
}, duration)
}
}
}
</script>

<style lang="stylus" scoped>
.toast
position fixed
top 50%
left 50%
transform translate(-50%, -50%)
background rgba(0, 0, 0, 0.6)
color #fff
padding 5px 8px
border-radius 4px
</style>
*/

判断 undefined 和 null

判断值是否为 undefined 或 null,可以用 obj == null 来判断,如果 obj 为 undefined,为真,如果为 null,也会真,这是偷懒的写法。正常应该是 obj === undefined || obj === null

行内 background-image 样式

在行内写 backgound-image 的三元表达式的时候,一定要加 () !!!!

1
2
3
4
5
6
7
8
9
10
<!-- 错误写法 -->
<view :style="{backgroundImage: 'url(' + pageData.cover_img ? pageData.cover_img : '' + ')'}" />
<!--
backgroundImage 将会被解析为 url("pageData.cover_img ? pageData.cover_img : ''")
显然不存在,于是行内就没有样式,这个标签就变成了 <view></view>,没有行内样式。而且由于被解析为字符串了,也不会在 cover_img 值改变的时候重新加载。
并且 H5 上运行不会报错!!!只是页面会变成空白
-->

<!-- 正确写法,在三元表达式两边加上括号即可,让其成为括号表达式 -->
<view class="bottomImg" :style="{backgroundImage: 'url(' + (pageData.cover_img ? pageData.cover_img : '') + ')'}" />
分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理