喋喋不休

真正的勇士,胖还贪吃,困还熬夜,穷还追星,丑还颜控

0%

keep-alive

  • 组件是vue的内置组件,被其包裹的组件会缓存下来,不活动的组件不会销毁,减少了很大的性能开销,它自身并不会渲染dom元素,也不会出现在父组件链中。

属性:

  • include - 需要缓存的匹配的组件的name
  • exclude - 不需要缓存的匹配的组件的name
  • max - 做多可以缓存的组件实例num(2.5.0新增的)

用法:

只需要把需要缓存的组件放在keep-alive中,或者include需要缓存的组件的name。
注意:这里的name指的是vue组件中export default中的name,还有路由表里定义的name,两个name必须一样,才能准确的缓存到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view />
</keep-alive>
</transition>

//include=cachedViews 这个是自己维护的需要缓存的组件name list 放在vuex的store中
<script>
export default {
data(){
return{}
}
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
},
}
</script>

路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
path: '/my-project',
component: Layout,
redirect: '/my-project/project-list',
name: 'Myproject',
meta: { title: 'title', icon: 'icon' },
children: [
{
path: 'project-list',
name: 'ProjectList', //说的就是这里的name
component: () => import('@/views/my-project/project-list/index'),
meta: { title: 'title' }
},
]
}

project-list中index组件

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="project-list">....</div>
</template>
<script>
export default {
name:'ProjectList', //这里的name
data(){
return{}
}
}
</script>

【上述的两个name必须一致】(切记 name 命名时候尽量保证唯一性 切记不要和某些组件的命名重复了,不然会递归引用最后内存溢出等问题)


缓存不适合的场景

有些场景是不太适合做缓存的,例如,详情页面,详情页面都是一个,/detail/1,/detial/2,都是共用一个组件,虽然路由不同,意味着他们的name是
是一样的,就如前面提到的,keep-alive的 include 只能根据组件名来进行缓存,所以这样就出问题了。

  • 解决方案,可以不用include,强制缓存所有组件,当然也是有弊端的,不能动态的删除缓存,只能设置一个最大缓存实例;
  • 也可以使用浏览器的缓存方案替代,自己进行缓存(还在实践中)

activated 和 deactivated 钩子 (2.2.0以上)

当你需要在不同的缓存组件切换显示的时候,需要做数据刷新的时候,你会发现 created,mounted 不起作用了,当然的,既然缓存了。这个时候,我们需要请求的时候就必须放在 activated 和 deactivated 生命周期中。

  • 注:这两个钩子一定是要在使用了keep-alive之后才会有效果的,否则不存在。

使用了keep-alive,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。

事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中, activated 中的不管是否需要缓存多会执行。


最后需要注意的是:

是用在其一个直属的子组件被开关的情形。如果你在其中有 v-for 则不会工作。如果有上述的多个条件性的子元素, 要求同时只有一个子元素被渲染。

组件缓存的一些要点都在这里了,自己开发过程中确实经历了这些东西,这里记录下来,希望后来人不会入坑

有好的见解,欢迎留言,留下联系方式,大家互相沟通!

不喜勿喷,谢谢!

最近手头做一个项目,左侧菜单这块类似后台管理系统,需要动态的获取到当前用户对应【角色】下面的菜单,需求就是这样的。

权限控制 permission.js

  • 实现方式:通过获取当前用户的权限菜单去比对路由表,生成当前用户具的权限可访问的路由表,通过 router.addRoutes 动态挂载到 router 上。

在main.js中引入permission.js文件,我们再路由钩子beforeEach中做处理,判断是否有token,才往下走,利用vuex对权限路由做控制,保存到state中的addRouters上面,没有值的话我们就做请求,获取权限菜单路由。

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
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // get token from cookie
const whiteList = ['/login']

router.beforeEach(async(to, from, next) => {
if (getToken('token')) {
if (to.path === '/login') {
//如果登录了,就跳转首页
next({ path: '/' })
} else {
if (!store.getters.addRouters.length) { //无权限路由,dispatch获取路由
await store.dispatch('GetAsyncRoutes').then((res) => {
//此处不知道为啥。。。router.options.routes没有添加的话,addroutes不起作用,还望各位大神指导
router.options.routes = router.options.routes.concat(store.getters.addRouters)
router.addRoutes(store.getters.addRouters)
next({ ...to, replace: true })
}).catch((err) => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
} else {
next()
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login') // 否则全部重定向到登录页
}
}
})

router.afterEach(() => {
// finish
})

在store.dispatch(‘GetAsyncRoutes’)中获取,做数据比对,映射。
dispatch(‘GetAsyncRoutes’) 代码,保存在store文件夹下面,对项目的状态进行管控

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
90
import { asyncRouterMap, constantRouterMap, constantAfterRouterMap, childrenRouter } from '@/router'
import { listSystemMenuByUser } from '@/api/meun'

//asyncRouterMap 是本地存的需要和后端返回权限菜单做比对映射的路由
//constantRouterMap 主要是存不需要权限的路由 如/login
//constantAfterRouterMap 是保存/404 等必须放到路由最后的
//childrenRouter 存放不是菜单的一些路由。。。感觉有点假哈,菜单哈哈(但是我们菜单权限配置的时候只配左侧主要菜单,详情之类的不配)

【强调一点,/404路由必须放到所有路由的最后,要不然的话,首次加载是没有问题的,刷新页面就会跳转404】

// 计算获取component于path的映射表(打平路由嵌套,由于我们路由就两层,懒得递归了。。。)
const LoopForFn = function(data) {
const biz = []
data.forEach(v => {
if (v.children && v.children.length) {
v.children.forEach(d => {
biz.push(d)
})
}
biz.push(v)
})
return biz
}
// 根据后台返回的权限菜单树的path 来动态匹配component映射表
function hasPermissionMenu(routerList, hasPermissionData) {
var list = LoopForFn(asyncRouterMap)
hasPermissionData.forEach((v, i) => {
list.forEach(d => {
//比对后端url和本地的path,如果匹配到了的话,我们就组合响应的菜单
if (v.url.replace(/^\s*|\s*$/g, '') === d.path.replace(/^\s*|\s*$/g, '')) {
const menu = {
path: d.path,
component: d.component,
name: d.name,
meta: { title: v.name, icon: d.meta.icon },
hidden: !JSON.parse(v.isUse),
children: []
}
if (v.children && v.children.length) { //递归比对
hasPermissionMenu(menu.children, v.children)
}
routerList.push(menu)
}
})
})
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
CLEAR: (state, routerList) => {
state.addRouters = routerList
},
SET_ROUTERS: (state, routerList) => {
routerList.map(x => {
// 过滤详情页等子路由,子路层级也是按照路由表走的,若组合过的权限菜单匹配到映射表里的path,就把映射表里的children追加到权限菜单中
const c = childrenRouter.filter(y => y.path === x.path)[0]
if (c.children && c.children.length > 0) {
x.children = [...x.children, ...c.children]
}
})
//更新动态添加的路由,后续有用处
state.addRouters = [...routerList, ...constantAfterRouterMap] //必须把/404路由追加到最后
//更新所有的路由
state.routers = [...constantRouterMap, ...state.addRouters]
}
},
actions: {
//清理动态添加的路由
ClearAddRouters({ commit }) {
commit('CLEAR', [])
},
//动态获取权限菜单
GetAsyncRoutes({ commit }) {
return new Promise(resolve => {
//权限菜单接口
listSystemMenuByUser().then(response => {
if (!response.data || response.data.length === 0) return
const routerList = []
hasPermissionMenu(routerList, response.data)
commit('SET_ROUTERS', routerList)
resolve()
})
})
}
}
}
export default permission

到这里基本上的动态获取权限菜单的方式就差不多了,添加就是这么些逻辑。有一点需要注意,在退出的时候一定要清楚添加过的权限菜单,store.dispatch(‘ClearAddRouters’),不然的话,下次登录的话就不会去调接口,上面这里有个判断

1
2
3
4
5
6
7
8
9
10
11
12
13
if (!store.getters.addRouters.length) {  //无权限路由,dispatch获取路由
await store.dispatch('GetAsyncRoutes').then((res) => {
//此处不知道为啥。。。router.options.routes没有添加的话,addroutes不起作用,还望各位大神指导
router.options.routes = router.options.routes.concat(store.getters.addRouters)
router.addRoutes(store.getters.addRouters)
next({ ...to, replace: true })
}).catch((err) => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
} else {
next()
}


vue动态添加路由就到这里了,下回希望讨论一下,组件缓存的问题,项目中也需要用到,keep-alive组件,里面也遇到一些坑,分享出来,希望后来者不必再走弯路。

有好的见解,欢迎留言,留下联系方式,大家互相沟通!

不喜勿喷,谢谢!

好久没有总结一下了。。。。蛋疼,有时候想写,不知道该写什么。最近工作闲暇之余,撸了一下京东凹凸实验室的Taro;
感觉学习越多,越感觉自己是个菜鸟了,呵呵;听说Taro有一段时间了,一直没有机会过一遍,这次抽时间搞了一回;

Taro简介

根据官网所说,Taro是一套遵循 React 语法规范的 多端开发 解决方案。 目的就是一套代码,多端通用
Taro主要是通过遵循react的语法规范,它采用与 React 一致的组件化思想,组件生命周期与 React 保持一致,同时支持使用 JSX 语法,让代码具有更丰富的表现力,使用 Taro 进行开发可以获得和 React 一致的开发体验。
是不是很爽,react框架不陌生吧,虽然我只是做了两个小项目而已,不得感叹react的强大,react不懂的自行去学习一下,https://zh-hans.reactjs.org/

环境搭建

首先,你需要使用 npm 或者 yarn 全局安装@tarojs/cli,或者直接使用npx:

1
2
3
4
5
6
# 使用 npm 安装 CLI
npm install -g @tarojs/cli
# OR 使用 yarn 安装 CLI
yarn global add @tarojs/cli
# OR 安装了 cnpm,使用 cnpm 安装 CLI
cnpm install -g @tarojs/cli

注:个人不建议用cnpm,后续会有包丢失的情况。

使用

1
2
taro init myApp
//依赖初始化的时候已经装好了,后续需要什么自己 再安装就是了。

Taro各端的开发,部署命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//打包
"build:weapp": "taro build --type weapp",
"build:swan": "taro build --type swan",
"build:alipay": "taro build --type alipay",
"build:tt": "taro build --type tt",
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"build:qq": "taro build --type qq",
"build:quickapp": "taro build --type quickapp",
//开发
"dev:weapp": "npm run build:weapp -- --watch",
"dev:swan": "npm run build:swan -- --watch",
"dev:alipay": "npm run build:alipay -- --watch",
"dev:tt": "npm run build:tt -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch",
"dev:qq": "npm run build:qq -- --watch",
"dev:quickapp": "npm run build:quickapp -- --watch"

注意事项

开发注意事项的话,就不细说了,每个框架都有自己的使用规范,谁让你要用呢?自己遵守罗。。。
自己可以到官网上面看 https://nervjs.github.io/taro/docs/before-dev-remind.html

Taro 主要是立足微信小程序开发的,所以,大多数的组件库,api和微信原生小程序用法相同;
但是小程序原生框架不是特别友好,Taro做了很多的优化。

目录结构

我的开发主要是用在h5和微信小程序端,别的端不做兼容考虑处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── config                 配置目录
| ├── dev.js 开发时配置
| ├── index.js 默认配置
| └── prod.js 打包时配置
├── src 源码目录
| ├── components 公共组件目录
| ├── pages 页面文件目录
| | ├── index index 页面目录
| | | ├── banner 页面 index 私有组件
| | | ├── index.js index 页面逻辑
| | | └── index.scss index 页面样式
| ├── utils 公共方法库
| ├── app.scss 项目总通用样式
| └── app.js 项目入口文件
└── package.json
└── project.config.json 微信小程序配置文件

大致的目录结构如上,我们要在pages目录下面新建页面开发就行。

入口文件app.js,app.scss可以写全局样式。

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
import Taro, { Component } from '@tarojs/taro'
import Index from './pages/index'

import './app.scss'
import 'taro-ui/dist/style/index.scss' // 全局引入一次即可

// 如果需要在 h5 环境中开启 React Devtools
// 取消以下注释:
// if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5') {
// require('nerv-devtools')
// }

class App extends Component {

config = {
pages: [
'pages/index/index',
'pages/upload/upload',
'pages/edit/edit',
'pages/login/login',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#0090F7',
navigationBarTitleText: '岁月不弃',
navigationBarTextStyle: 'white'
}
}

componentDidMount () {}

componentDidShow () {}

componentDidHide () {}

componentDidCatchError () {}

// 在 App 类中的 render() 函数没有实际作用
// 请勿修改此函数
render () {
return (
<Index />
)
}
}

Taro.render(<App />, document.getElementById('app'))

注:我的项目用的是taro-ui作为ui框架,自行安装即可。。。
但是,taro-ui安装的时候会出现很多问题,有时候半天安装没有反应,或者就直接失败,出现这种情况的话,可以把node_modules目录先干掉,然后重新安装,就好了。

project.config.json 文件为微信小程序配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"miniprogramRoot": "dist/",
"projectname": "taro-app",
"description": "a app",
"appid": "",
"setting": {
"urlCheck": true,
"es6": false,
"postcss": false,
"minified": false
},
"compileType": "miniprogram",
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"condition": {}
}

路由配置:
Taro路由不需要自己再配置,我们只需要在入口文件app.js的 config 配置中指定好 pages,然后就可以在代码中通过 Taro 提供的 API 来跳转到目的页面
Taro的路由跳转api

1
2
3
4
5
6
7
8
9
10
11
12
13
// 跳转到目的页面,打开新页面
Taro.navigateTo({
url: '/pages/page/path/name'
})

// 跳转到目的页面,在当前页面打开
Taro.redirectTo({
url: '/pages/page/path/name'
})

Taro.navigateBack({
delta:1 //向前返回级数
})

以上就是Taro的一些基本知识,接下来,我开始做自己的项目;

我的项目

主要是一个图片为主的项目,包括几个模块:

  • 首页
  • 图片描述编辑,删除页
  • 登录页面
  • 上传图片页面

首页代码

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import Taro, { Component } from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components'
import './index.scss'
import { AtIcon } from 'taro-ui'
import { query } from '../../api/my-api'
import msgList from "../../utils/message";

export default class Index extends Component {

config = {
navigationBarTitleText: '岁月不弃'
}
constructor(props) {
super(props)
this.state = {
pageNum: 1,
list: [],
canLoad: false,
total:0,
}
}

componentWillMount() { }

// onPageScroll (e) {
// this.pageScrollFn(e.scrollTop)
// }

// 小程序页面触底时执行
onReachBottom() {
this.getList()
}

pageScrollFn() {
//真实内容的高度
var pageHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight);
//视窗的高度
var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
//隐藏的高度
var scrollHeight = window.pageYOffset || document.documentElement.scrollTop;
if (pageHeight - viewportHeight - scrollHeight < 10 && this.state.canLoad) {//如果满足触发条件,执行
this.getList()
}
}
componentDidMount() {
this.getList()
if (process.env.TARO_ENV === 'h5') {
window.addEventListener('scroll', this.pageScrollFn())
}

}

componentWillUnmount() {
if (process.env.TARO_ENV === 'h5') {
window.removeEventListener('scroll', () => { })
}
}

componentDidShow() { }

componentDidHide() { }

//获取列表
getList() {
Taro.showLoading({
title: '加载中。。。'
})
let {total,pageNum} = this.state
if (this.state.pageNum <= parseInt(total / 10) + 1) {
this.setState({
canLoad:true,
})
} else {
this.setState({
canLoad:false
})
Taro.hideLoading();
return false;
}

query({ pageNum: pageNum }).then(response => {
let res = response.data
let { list } = this.state
list = [...list, ...res.data.list]
total = res.data.total;
Taro.hideLoading();
this.setState({
list: list,
pageNum: pageNum+1,
total:total
})

})
}

random() {
let l = msgList.length;
let num = Math.floor(Math.random() * l);
return msgList[num];
}
//渲染消息
getMsg(subItem) {
if (subItem.description !== "undefined" && subItem.description != "" && subItem.description != null) {
return subItem.description;
} else {
return this.random();
}
}
//预览图片
preview(list, index) {
let imgs = [];
list.map(x => imgs.push(x.imgUrl));
Taro.previewImage({
urls: imgs,
current: imgs[index]
})
}
goTop() {
if (process.env.TARO_ENV === 'h5') {
window.scrollTo({
top: 0,
behavior: "smooth"
})
}
if (process.env.TARO_ENV === 'weapp') {
wx.pageScrollTo({
scrollTop: 0
})
}

}
//编辑信息
editMessage(id) {
Taro.navigateTo({
url: '/pages/edit/edit?imgId=' + id
})
}

goUpload() {
Taro.navigateTo({
url: '/pages/upload/upload'
})
}



render() {
const { list } = this.state;
const mainContent = list.map((x, i) => {
return (
<View className='list-item' taroKey={String(i)}>
<View className='date'>{x.date}</View>
{
x.imgList.map((y, j) => {
return (
<View className='img-item' taroKey={String(i)}>
<Image className='img' mode='widthFix' src={y.imgUrl} onClick={this.preview.bind(this, x.imgList, j)} />
<Text className='des' onClick={this.editMessage.bind(this, y.imgId)}>{this.getMsg(y)}</Text>
</View>
)
})
}
</View>
)
})
return (
<View
className='home'
>
<View className='btns'>
<AtIcon onClick={this.goUpload.bind(this)} value='image' size='30' color='#08aaea'></AtIcon>
<AtIcon onClick={this.goTop.bind(this)} value='chevron-up' size='30' color='#08aaea'></AtIcon>
</View>
{mainContent}
{
!this.state.canLoad &&
<View className='noMore'>没有更多了。。。</View>
}
{
this.state.canLoad &&
<View className='noMore'>加载中。。。</View>
}
</View >
)
}
}

这里涉及到一个滚动加载,需要区分h5和weapp环境来做滚动加载,因为在weapp中没有办法绑定滚动时间监听,用weapp的钩子onReachBottom()
触底之后我们再做接口请求。注意react事件监听绑定到this。

编辑页面代码

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import Taro, { Component } from '@tarojs/taro'
import { View } from '@tarojs/components'
import './edit.scss'
import { AtButton, AtTextarea } from 'taro-ui'
import { editMessage, deleteImg } from '../../api/my-api'

export default class Edit extends Component {
config = {
navigationBarTitleText: '编辑'
}
constructor(props) {
super(props)
this.state = {
imgId: ''
}
}

componentWillMount() {
let id = this.$router.params.imgId
this.setState({
imgId: id,
message: ''
})
}

componentDidMount() {
}

componentWillUnmount() { }

componentDidShow() { }

componentDidHide() { }

submit() {
Taro.showLoading({
title: '保存中。。。'
})
editMessage({
message: this.state.message,
imgId: this.state.imgId
}).then((res) => {
if (res.data.code == 0) {
Taro.showToast({
title: '更新成功!'
})
Taro.redirectTo({
url: '/pages/index/index'
})
}
})
}
handleChange(e) {
this.setState({
message: e.target.value
})
}

goBack() {
Taro.navigateBack({
delta: 1
})
}

delImg() {
Taro.showModal({
title: '删除图片',
content: '是否确认删除?',
})
.then(res => {
if (res.confirm) {
deleteImg({
imgId:this.state.imgId
}).then(response=>{
if(response.data.code==0){
Taro.showToast({
title:'删除成功!'
})
setTimeout(()=>{
Taro.redirectTo({
url: '/pages/index/index'
})
},2000)
}
})
}
})

}

render() {
return (
<View className='edit'>
<View className='title'>请输入</View>
<AtTextarea
value={this.state.message}
maxLength={200}
placeholder='描述信息...'
height='300px'
onChange={this.handleChange.bind(this)}
>
</AtTextarea>
<View className='btns'>
<AtButton onClick={this.goBack.bind(this)}>取消</AtButton>
<AtButton onClick={this.delImg.bind(this)}>删除</AtButton>
<AtButton onClick={this.submit.bind(this)} type='primary'>确定</AtButton>
</View>
</View>
)
}
}

编辑页面主要是编辑图片描述性的文字,以及删除当前的图片,没有过多注意事项。每个页面可以单独配置config。

登录页面

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import Taro, { Component } from '@tarojs/taro'
import { View } from '@tarojs/components'
import './login.scss'
import { AtForm, AtInput, AtButton } from 'taro-ui'
import { login } from '../../api/my-api'

import md5 from 'md5'


export default class Upload extends Component {
config = {
navigationBarTitleText: '登录'
}
constructor(props) {
super(props)
this.state = {
userName: '',
userPwd: ''
}
}

componentWillMount() { }

componentDidMount() {

}

componentWillUnmount() { }

componentDidShow() { }

componentDidHide() { }

handleChangeName(v) {
this.setState({
userName: v
})
}
handleChangePwd(v) {
this.setState({
userPwd: v
})
}
onSubmit() {
if(!this.state.userName || !this.state.userPwd){
Taro.showToast({
title:'请输入正确的用户名和密码~',
icon:'none'
})
return false;
}
Taro.showLoading({
title:'登录中',
})
login({
userName: this.state.userName.trim(),
userPwd: md5(this.state.userPwd.trim())
}).then(res=>{
// console.log(res)
if(res.data.code==0){
Taro.hideLoading()
Taro.setStorageSync('photo-token',res.data.token)
Taro.navigateBack({
delta:1
})
}
})
}
onCancel() {
Taro.navigateBack({
delta:1
})
}

render() {
return (
<View className='login'>
<View className='title'>请登录</View>
<AtForm
onSubmit={this.onSubmit.bind(this)}
>
<AtInput
name='value'
title='用户名'
type='text'
placeholder='请输入用户名'
value={this.state.userName}
onChange={this.handleChangeName.bind(this)}
/>
<AtInput
name='value'
title='密码'
type='password'
placeholder='请输入密码'
value={this.state.userPwd}
onChange={this.handleChangePwd.bind(this)}
/>
<View className='btns'>
<AtButton onClick={this.onCancel.bind(this)}>取消</AtButton>
<AtButton onClick={this.onSubmit.bind(this)} type='primary'>确定</AtButton>
</View>
</AtForm>
</View>
)
}
}

上传图片页面

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import Taro, { Component } from '@tarojs/taro'
import { View, Image, Picker } from '@tarojs/components'
import './upload.scss'
import { AtInput, AtButton, AtIcon } from 'taro-ui'
import { parseTime } from "../../utils";

let inputList = []

export default class Upload extends Component {
config = {
navigationBarTitleText: '图片上传'
}
constructor(props) {
super(props)
this.uploadFile = this.uploadFile.bind(this)
this.state = {
files: [],
isShow: true,
desList: [],
date: '',
urls: []
}
}

componentWillMount() {
this.setState({
date: parseTime(new Date())
})
}

componentDidMount() {

}

componentWillUnmount() { }

componentDidShow() { }

componentDidHide() { }

delFile(index) {
let { files, urls } = this.state;
files.splice(index, 1)
urls.splice(index, 1)

this.setState({
files: files,
urls: urls
})
}
addFile() {
Taro.chooseImage({
count: 4,
imageId: 'file',
success: (v) => {
console.log(v)
this.setState({
files: v.tempFiles
})
}
})
}
uploadChange(v) {
let files = v.target.files;
if (files.length > 4) {
Taro.showToast({
title: '不能超过4张图片。。。',
icon: 'none'
})
this.setState({
files: []
})
return false;
}

let list = this.state.files
let urls = this.state.urls

for (let i = 0; i < files.length; i++) {
list.push(files[i]);
let reader = new FileReader();
reader.readAsDataURL(files[i]);
reader.onload = () => {
urls.push(reader.result)
this.setState({
files: list,
urls: urls,
isShow: list.length < 4,
})
}
}

//初始化 图片描述变量
list.map(() => inputList.push(''))
}
//图片点击预览
onImageClick(index) {
let imgs = [];
this.state.files.map(x => imgs.push(x.path));
Taro.previewImage({
urls: imgs,
current: imgs[index]
})
}

//输入框变化
handleChange(i, v) {
inputList[i] = v
this.setState({
desList: inputList
})

return v
}

//取消回退
onCancel() {
Taro.navigateBack({
delta: 1
})
}
//提交
onSubmit() {
if (this.state.files.length < 0) {
return false
}
let list = [];
this.state.files.map((x, i) => {
list.push({
file: x,
discription: this.state.desList[i],
createDate: this.state.date
});
});
Taro.showLoading({
title: '上传中...'
})
console.log(list)
// return
list.map(x => {
this.uploadFile(x);
});
}
uploadFile(params) {
Taro.uploadFile({
url: '',
filePath: params.file.path,
name: 'file',
header: { 'token': Taro.getStorageSync('photo-token') },
formData: {
'discription': params.discription,
'createDate': params.createDate
},
success(res) {
// console.log(res)
const data = JSON.parse(res.data)
//do something
if (data.code == 100 || data.code == 101) {
Taro.hideLoading();
Taro.navigateTo({
url: '/pages/login/login'
})
}
if (data.code == 0) {
Taro.showToast({
title: "上传成功!",
});
setTimeout(() => {
Taro.navigateTo({
url: '/pages/index/index'
})
}, 2000);
}
}
})
}

//日期选择
onDateChange = e => {
let arr = e.detail.value.split('-')
//格式化一下
if (arr[1].length == 1) {
arr[1] = '0' + arr[1]
}
if (arr[2].length == 1) {
arr[2] = '0' + arr[2]
}
this.setState({
date: arr.join('-')
})
}

goHome() {
Taro.redirectTo({
url: '/pages/index/index'
})
}
goLogin() {
Taro.redirectTo({
url: '/pages/login/login'
})
}

render() {
return (
<View className='upload'>
<View className='pic-title'>图片上传(最多四张)</View>
<View className='pic-content'>
{
this.state.files.map((x, i) => {
return (
<View className='custom-img' taroKey={x}>
<Image className='preview-img' onClick={this.onImageClick.bind(this, i)} src={x.path}></Image>
<AtIcon onClick={this.delFile.bind(this, i)} value='subtract-circle' size='10' color='#08aaea'></AtIcon>
</View>
)
})
}
{
this.state.isShow &&
<View className='add-icon' onClick={this.addFile.bind(this)}>
<AtIcon value='add' size='30' color='#08aaea'></AtIcon>
</View>
}
</View>
{/* <AtImagePicker
count={4}
length={4}
multiple
showAddBtn={this.state.isShow}
files={this.state.files}
onChange={this.onChange.bind(this)}
onImageClick={this.onImageClick.bind(this)}
/> */}
<View className='pic-des'>图片描述(顺序填写,可不填)</View>
{
this.state.files.map((x, i) => {
return (
<AtInput
title={'图片' + (i + 1)}
type='text'
placeholder='请输入描述'
value={this.state.desList[i]}
onChange={this.handleChange.bind(this, i)}
taroKey={i}
>
</AtInput>
)
})
}
<View className='date-des'>
<Picker mode='date' value='YYYY-MM-DD' onChange={this.onDateChange}>
<View className='picker'>
时间(可选):{this.state.date}
</View>
</Picker>
</View>
<View className='btns'>
<AtButton onClick={this.onCancel.bind(this)}>取消</AtButton>
<AtButton onClick={this.onSubmit.bind(this)} type='primary'>确定</AtButton>
</View>
<View className='flow-btns'>
<AtIcon onClick={this.goHome.bind(this)} value='home' size='30' color='#08aaea'></AtIcon>
<AtIcon onClick={this.goLogin.bind(this)} value='user' size='30' color='#08aaea'></AtIcon>
</View>
</View>
)
}
}

上传图片页面用到了Taro.chooseImage()这个api获取图片的路径实现预览,提交图片的时候用到Taro.uploadFile(),
具体配置见https://nervjs.github.io/taro/docs/apis/multimedia/images/chooseImage.html

接口api单独放到一个文件夹api下面的my-api.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
34
35
import request  from '../utils/request';

//查询接口
export function query(data){
return request({
url:'/query',
method:'post',
data
})
}

//修改浏留言接口
export function editMessage(data){
return request({
url:'/editMessage',
method:'post',
data
})
}
//删除图片
export function deleteImg(data){
return request({
url:'/deleteImg',
method:'post',
data
})
}
//登录
export function login(data){
return request({
url:'/login',
method:'post',
data
})
}

接口请求单独封装request.js 文件 ,用Taro.request()方法,注意一点,返回的数据是整个请求返回的东西,并不是后台的data。

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
import Taro from '@tarojs/taro'

let baseUrl = '';
if(process.env.NODE_ENV === 'development'){
baseUrl = ''
// baseUrl = 'http://127.0.0.1:9000'
}else{
baseUrl = ''
}

const service = (params)=>{
let {url,method,data} = params
let contentType = 'application/json;charset=UTF-8'
const option = {
url: baseUrl + url,
data: data,
method: method.toUpperCase(),
header: { 'content-type': contentType || params.contentType, 'token': Taro.getStorageSync('photo-token') },
success(res) {
if (res.statusCode === 200) {
if(res.data.code==100 || res.data.code==101){
Taro.showToast({
title:res.data.msg || 'error',
icon:'none'
})
setTimeout(()=>{
Taro.navigateTo({
url:'/pages/login/login'
})
},2000)
return false;
}else if(res.data.code=='-1'){
Taro.showToast({
title:res.data.msg || 'error',
icon:'none'
})
return false;
}
return res.data
}else{
Taro.showToast({
title:res.statusCode,
icon:'none'
})
return false;
}
},
error(e) {
console.log('api', '请求接口出现问题', e)
}
}
return Taro.request(option)
}

export default service

通过 process.env.NODE_ENV 这个环境变量判断环境,加载对应环境的api;
公共方法封装在utils文件夹的index.js中

服务端

服务端这里不细说了,我采用的是express开发的,pm2 进程管理;数据库用的是mysql,没有太复杂的操作数据库代码。

总结

本项目只是对Taro的一次探索性的开发,用到的也只是部分api,和ui组件;很多东西没哟涉及,例如,redux状态管理等。
总的来说,开发体验还是不错的,可以用react开发小程序就很爽,哈哈哈。。。

具体成品效果,h5端可以访问http://mp.ufade.com;小程序可以搜索岁月不弃可以查看到。

有好的见解,欢迎留言,留下联系方式,大家互相沟通!

不喜勿喷,谢谢!

h5页面和小程序交互

小程序web-view 组件是一个可以用来承载网页的容器,会自动铺满整个小程序页面。个人类型与海外类型的小程序暂不支持使用

引入jsdk文挡

<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>

h5跳转小程序页面 demo

$('.span-icon1').click(function(){
    wx.miniProgram.navigateTo({url:'/pages/share/share'});
})

h5嵌入小程序传递message给小程序

wx.miniProgram.postMessage(Util.shareToWechart(params));   // 传递消息给webview

web-view 通过属性 bindmessage 触发得到h5传过来的消息。
相关的api
wx.miniProgram.navigateTo     //跳转小程序页面
wx.miniProgram.navigateBack   //返回小程序上一个路由
wx.miniProgram.switchTab    //切换tab
wx.miniProgram.reLaunch    //重新启动
wx.miniProgram.redirectTo   // 跳转小程序,删除当前页面,类似 history.replace()
wx.miniProgram.postMessage  //向小程序传递信息
wx.miniProgram.getEnv    //获取当前环境

//获取小程序环境
wx.miniProgram.getEnv(function(res) {
    console.log(res.miniprogram) // true 
})

其他api 包括图像接口、音频接口、设备信息、地理位置等,自行查看https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html

tip

网页内 iframe 的域名也需要配置到域名白名单。
开发者工具上,可以在 <web-view> 组件上通过右键 - 调试,打开 <web-view> 组件的调试。
每个页面只能有一个 <web-view>,<web-view> 会自动铺满整个页面,并覆盖其他组件。
<web-view> 网页与小程序之间不支持除 JSSDK 提供的接口之外的通信。
在 iOS 中,若存在JSSDK接口调用无响应的情况,可在 <web-view> 的 src 后面加个#wechat_redirect解决。
避免在链接中带有中文字符,在 iOS 中会有打开白屏的问题,建议加一下 encodeURIComponent

这些东西实际上在小程序开发文档都可以查看到,我主要记录一下我在开发web-view的时候,所遇到的一些问题

屏幕适配 (除了屏幕适配外,其他的东西jssdk有提供就用,没有提供就没有办法罗。。。。o( ̄︶ ̄)o)

iPhoneX

ipx再浏览器手机模拟器中,我们可以看到设备的宽高分别为375、812、dpr=3,我们的设计稿是ip6的设计稿,对于ipx需要做单独的样式。

按照我们的思路,可以获取设备信息,动态增减类,来控制样式;也可以写媒体查询,根据屏幕的宽或则高,或则dpr来做控制。

首先,我们项目中不知道谁写了一个判断ipx的的方法。。。。如下

isIphoneX: function () {
    return /iphone/gi.test(navigator.userAgent) && (screen.height == 812 && screen.width == 375)
},

同事,告诉我,他们就是用的这个方法,我也就没有在意,粗略一看,没什么问题,是判断ipx的方法,于是乎。。。。。经过我在小程序测试,发现这个

方法,并没有起到作用,原因是web-view组件中嵌入h5页面,你在h5页面中获取的屏幕高度并不是ipx通屏的高度。。。。还记得小程序头顶上的一些东西吗?

经过测试,在小程序web-view中,ipx的height只有724px。。。

发现问题了,立马弃用公共的判断方法,当然,也可以重新判断高度的。我们决定采用媒体查询来做,代码如下:

//ipx 适配
@media only screen and (device-width: 375px) and (device-height:724px) and (-webkit-device-pixel-ratio:3),
only screen and (device-width: 375px) and (device-height:812px) and (-webkit-device-pixel-ratio:3) {
    .christmas_activity {
        background: url('../images/home-bg-ipx.jpg') no-repeat;
        background-size: 100% 100%;

        .foot {
            height: 2.5rem;
            bottom: 0;
        }
    }


}

这里媒体查询写死了两种屏幕的宽高,一种是ipx通屏,一种是ipx在小程序中的,这样就可以做到ipx的适配。嗯,不错。

但是,只是做到了ipx的适配,如果尺寸不是ipx的尺寸,但是屏幕比ipx还长,或则比它短一点呢?又是另外的问题了,目前,我采用的方法是ip6用一张背景图,比ip6-plus高度还大的手机换ipx的背景图,这样基本可以做到长屏幕手机上面背景图片不会拉伸了,哈哈哈。

其实web-view组件嵌套h5页面还是非常有必要的,就拿我们目前的小程序项目来说的话,主包除外,已经分包两个了,如果不用h5嵌套,过不了多久小程序直接就超额了,还搞个毛线。包的大小都是做了限制的,没办法罗。。。

css对于我们前端来说,非常重要,使我们开发静态页面的利器,虽然一些颜色、大小都是很容易调整,基本上没有什么难度,记忆力好点也就OK了,
我感觉布局这块对于我们尤其重要,我平时自己使用的时候,忘记总结了,这里总结一下。

垂直水平居中

我们经常用到的就是水平居中,垂直居中一旦遇到的话,有哪些方法可以解决呢?

display:table-cell

  • 父级元素display:table-cell;vertical-align:midde;
1
2
3
4
5
6
7
8
9
10
11

.parent{
display: table-cell;
vertical-align: middle;
}
.child{
margin:0 auto;
}
<div class="parent">
<div class="child"></div>
</div>

绝对定位

  • 父元素相对定位,子元素绝对定位。
  • 若子元素定宽高,写margin-left:-50px;margin-top:-50px也行,不定的话,transform: translate(-50%,-50%)即可
1
2
3
4
5
6
7
8
9
10
11
12
.parent{
position: relative;
}
.child{
position: absolute;
left: 50%;
top:50%;
transform: translate(-50%,-50%);
}
<div class="parent">
<div class="child"></div>
</div>

也可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.parent{
position: relative;
}
.child{
position: absolute;
left: 0;
top:0;
bottom:0;
right:0;
margin:auto;
}
<div class="parent">
<div class="child"></div>
</div>

flex布局

  • 直接设置父元素 display:flex(设置为flex布局),justify-content:center(主轴对齐方式),align-items:center(垂直轴上的对齐方式)
1
2
3
4
5
6
7
8
9
10
	.parent{
display: flex;
justify-content: center;
align-items: center;
}
.child{
}
<div class="parent">
<div class="child"></div>
</div>

文本水平垂直居中

  • 父元素设置height,line-height相等
1
2
height:400px;
line-height:400px;

垂直居中图片

1
2
3
4
5
6
.parent {
line-height: 200px;
}
.parent img {
vertical-align: middle;
}

grid网格布局

  • 网格布局代码量比flex布局还要少,但是兼容性不好
    1
    2
    3
    4
    5
    6
    7
    .parent {
    display: grid;
    }
    .child {
    align-self: center;
    justify-self: center;
    }

两列布局

左测定宽浮动,右侧宽100%;

1
2
3
4
5
6
7
8
.left{
width:200px
float: left;
}
.right{
width: 100%;
margin-left: 200px;
}

flex布局(flex布局出现了之后,很多问题就很好处理了)

1
2
3
4
5
6
body{
display: flex;
}
.right{
flex:1
}

负margin

  • 首先修改页面结构,为自适应部分添加容器 .container, 同时改变左右部分的位置,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="container">
<section class="right">Right</section>
</div>
<aside class="left">Left</aside>

.left{
float:left;
margin-left: -100%;
}
.right{
margin-left: 200px;
}
.container{
float:left;
width:100%
}

定位

1
2
3
4
5
6
7
8
9
.left{
position: absolute;
left:0;
}
.right{
position: absolute;
left:200px;
width:100%
}

table 布局

1
2
3
4
5
6
7
8
9
10
body{
display: table;
width:100%;
}
.left{
display: table-cell;
}
.right{
display: table-cell;
}

圣杯布局

圣杯布局就是两侧宽度固定,中间宽度自适应的三列布局

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
<div class="header">头</div>
<div class="content">
<div class="center">中</div>
<div class="left">左</div>
<div class="right">右</div>
</div>
<div class="footer">脚</div>

.header,.footer{
width: 100%;
height: 50px;
background: #ddd;
clear: both;
}
.left,.right,.center{
height: 80vh;
}
.content{
padding: 0 200px;
}
.center{
width: 100%;
background: #00f;
float: left;
}
.left {
background: #0f0;
width: 200px;
float: left;
margin-left: -100%;
position: relative;
right:200px;
}

.right {
width: 200px;
background: #f00;
float:left;
margin-right: -200px;
}

圣杯布局其实就是三列都浮动,中间列在元素最前面,width为100%,左侧部分margin-left:-100%,相对定位,right:左侧元素宽度;
右侧margin-right:右侧元素宽度;

这个布局也是我们最常用的布局,前端工程师面试的时候经常会被问到,双飞翼布局的话,原理和圣杯布局类似;还有就是flex布局和grid盒子可以实现,这里不做讲解了,
简单但是有兼容性问题。

css常见的两列、三列布局和垂直居中对齐大概就是这么多了,有没有总结到的地方以后继续。

有好的见解,欢迎留言,留下联系方式,大家互相沟通!

不喜勿喷,谢谢!

本次需求分析:

  1. 从原来城市的数据,变为省市区的数据;
  2. 地图需要根据缩放级别绘制icon;
  3. 地图点击需要切换到当前点击区域的数据;
  4. 实时投递弹窗和实时清运弹窗;
  5. 有权限区域的绘制;

1. 权限区域绘制

目前项目使用行政区划浏览 绘制权限区域 具体用法如下:

//加载DistrictExplorer,loadUI的路径参数为模块名中 'ui/' 之后的部分 
AMapUI.loadUI(['geo/DistrictExplorer'], function(DistrictExplorer) {
//启动页面
initPage(DistrictExplorer);
});

function initPage(DistrictExplorer) {
//创建一个实例
var districtExplorer = new DistrictExplorer({
    map: map //关联的地图实例
});

var adcode = 100000; //全国的区划编码

districtExplorer.loadAreaNode(adcode, function(error, areaNode) {

    if (error) {
        console.error(error);
        return;
    }

    //绘制载入的区划节点
    renderAreaNode(districtExplorer, areaNode);
});
}

function renderAreaNode(districtExplorer, areaNode) {

//清除已有的绘制内容
districtExplorer.clearFeaturePolygons();

//just some colors
var colors = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00"];

//绘制子级区划
districtExplorer.renderSubFeatures(areaNode, function(feature, i) {

    var fillColor = colors[i % colors.length];
    var strokeColor = colors[colors.length - 1 - i % colors.length];

    return {
        cursor: 'default',
        bubble: true,
        strokeColor: strokeColor, //线颜色
        strokeOpacity: 1, //线透明度
        strokeWeight: 1, //线宽
        fillColor: fillColor, //填充色
        fillOpacity: 0.35, //填充透明度
    };
});

//绘制父级区划,仅用黑色描边
districtExplorer.renderParentFeature(areaNode, {
    cursor: 'default',
    bubble: true,
    strokeColor: 'black', //线颜色
    fillColor: null,
    strokeWeight: 3, //线宽
});

//更新地图视野以适合区划面
map.setFitView(districtExplorer.getAllFeaturePolygons());
}

avatar
新需求是要求到区一级,有权限的区域可能不是一个市的全部区域,行政区划浏览会把一个城市的所有区域绘制出来,或者把一个省的所有市绘制出来,所以,这种绘制区域的方案pass掉。。。。

avatar

我新的方案采用的是 高德的 “行政区查询” 
AMap.plugin('AMap.DistrictSearch', function () {
    // 创建行政区查询对象
    var district = new AMap.DistrictSearch({
        // 返回行政区边界坐标等具体信息
        extensions: 'all',
        // 设置查询行政区级别为 区 
        level: 'district'
    })

    district.search('110110', function(status, result) {
        // 获取朝阳区的边界信息
        var bounds = result.districtList[0].boundaries
        var polygons = []
        if (bounds) {
        for (var i = 0, l = bounds.length; i < l; i++) {
        //生成行政区划polygon
        var polygon = new AMap.Polygon({
            map: map,
            strokeWeight: 1,
            path: bounds[i],
            fillOpacity: 0.7,
            fillColor: '#CCF3FF',
            strokeColor: '#CC66CC'
        })
        polygons.push(polygon)
        }
        // 地图自适应
        map.setFitView()
    }
    })
})

我们要做的只有,把有权限区域的区域编码拿到,就可以绘制权限区域了。

2. 地图点击需要切换到当前区域的数据

avatar
例如,当前点击的是 广东省深圳市南山区,我们没法判断到底是点击的是广东省,深圳市,还是南山区,所以区分不了点击的省市区;

而且,点击地图的时候只会返回城市级别的code,用code做请求的时候,来获取数据。

这里我利用了地图的“缩放级别”来做控制。利用map.getZoom()来获取地图的缩放级别,我分为三个档次:x<8,8<=x<=10,x>10,根据地图当前的级别,分别三个档次的点击代表省市区,这样我们可以获取到地图返回的中文省市区,拿中文省市区,去后台给到的有权限的三级区域列表做匹配,然后,拿到对应的code做请求,获取数据。

点击地图之后的具体方法,如下:

geocoder.getAddress(e.lnglat, function (status, result) {
    if (status === 'complete' && result.info === 'OK') {
        // console.log(JSON.stringify(result))
        // console.log(map.getZoom())
        //当前的名称
        var currentName = ''
        //权限列表是否包含当前name
        var isContain = true;
        //各级名称
        var provinceName = result.regeocode.addressComponent.province
        var cityName = result.regeocode.addressComponent.city
        var districtName = result.regeocode.addressComponent.district

        //没区的时候直接取cityCode
        var noDistrictCityCode = result.regeocode.addressComponent.adcode
        if (map.getZoom() < 8) { //此处去拿省的回收机数量 传给MachineNumWindow
            // 拿省的名称去匹配 
            selectName = currentName = provinceName
            let list = provinceList.filter(x => x.cityName.includes(currentName))[0]
            if (list) {
                isContain = true
                currentAdcode = list.cityCode
            } else {
                isContain = false
            }

        } else if (map.getZoom() >= 8 && map.getZoom() <= 10) {
            //北京、上海等地可能没有cityName
            selectName = currentName = cityName ? cityName : provinceName
            // console.log(currentName)
            let list = cityList.filter(x => x.cityName.includes(currentName))[0]
            // console.log(list)
            if (list) {
                isContain = true
                currentName = provinceName + cityName
                currentAdcode = list.cityCode
            } else {
                isContain = false
            }

        } else {
            currentName = districtName
            if (districtName == '') {
                currentName = provinceName + cityName;
                currentAdcode = noDistrictCityCode * 1000000;
                isContain = true;
            } else {
                let list = districtList.filter(x => x.cityName.includes(currentName))[0]
                // console.log(list)
                if (list) {
                    isContain = true
                    currentName = provinceName + cityName + districtName
                    currentAdcode = list.cityCode
                } else {
                    isContain = false;
                }
            }
            selectName = cityName ? cityName + districtName : provinceName + districtName
        }
        // console.log(isContain)
        //如果没有匹配到return
        if (!isContain) {
            map.clearInfoWindow();
            return false;
        }

        //拿到当前的名称去请求当前区域中心经纬度
        geocoder.getLocation(currentName, function (status, result) {
            if (status === 'complete' && result.info === 'OK') {
                // result中对应详细地理坐标信息
                // console.log(JSON.stringify(result))
                let location = result.geocodes[0].location;
                currentLnglat = [location.lng, location.lat]
            }
        })

        // console.log(currentLnglat)

        //打开名称数量信息窗体
        openNumWindow(clickLnglat, {
            name: currentName,
            // num: currentNum
        })


    }

});

我们是需要弹出当前点击的区域名称,然后点击当前的信息窗体,请求数据。。。。

3. 根据地图缩放级别展示不同的icon 回收机

avatar
这个之前做城市数据墙的时候,可能回收机的数量不是特别大,只是显示一个城市的回收机,而且也不用变换图标,所以性能上面没有发现问题,后来,我沿用之前添加marker的方法,由于我的地图是需要显示大量的点,而且会做切换,上了生产环境之后发现严重的性能问题,pass掉了,测试,预发布。。。。。没有生产那么多的数据,并没有发现。。。。。悲催

avatar
后来,经过团队的讨论,选了高德地图海量点的方案,此方案“据高德说”,10万+ 以下都可以hold住,牛鼻。。。。。。

//海量点的style  图标提前定义写死,后面更换时直接修改数据里的style
var massStyle = [{
        url: './images/point.png',
        anchor: new AMap.Pixel(3, 6),
        size: new AMap.Size(6, 6)
    },
    {
        url: './images/city_full.png',
        anchor: new AMap.Pixel(27, 57),
        size: new AMap.Size(54, 57)
    },
    {
        url: './images/city_unfull.png',
        anchor: new AMap.Pixel(27, 57),
        size: new AMap.Size(54, 57)
    },
    {
        url: './images/district_full.png',
        anchor: new AMap.Pixel(27, 61),
        size: new AMap.Size(54, 61)
    },
    {
        url: './images/district_unfull.png',
        anchor: new AMap.Pixel(27, 61),
        size: new AMap.Size(54, 61)
    },
]
var massMarks = [] //海量点
//海量点  需要提前创建实例,避免每次权限变动多次实例化,造成性能问题,创建实例时先放空数据进去,按照官网不放数据setData不行
massMarks = new AMap.MassMarks([], {
    zIndex: 111,
    cursor: 'pointer',
    style: massStyle
});



/**
* 绘制回收机
* @date 2018-12-04
* @param {*} data 中心地图原始回收机列表
* @returns
*/
function getMapFun(data) {
    // console.log(data)
    if (!data || data.length == 0) { //此处要判断data  如不的话,试试,神坑。。。地图样式加载不出来
        return false;
    }
    machineList = []
    data.map(x => {
        machineList.push({
            lnglat: [x.longitude, x.latitude],
            siteCode: x.siteCode,
            isFull: x.isFull,
            style: 0
        })
    })

    massMarks.clear()
    massMarks.setData(machineList)
    massMarks.setMap(map);

    //渲染回收机
    renderMap(machineList)

}


/**
* 渲染中心地图数据回收机,不同缩放级别替换图标
* @date 2018-12-04
* @param {*} data  处理过后的回收机列表
* @returns
*/
function renderMap(data) {
    // console.log(data)
    //监听地图缩放前后级别,如果在同一区间内就不做渲染了,节约性能
    if (startZoom <= 9 && startZoom >= 3 && endZoom <= 9 && endZoom >= 3) {
        return false;
    }
    if (startZoom > 9 && startZoom <= 14 && endZoom > 9 && endZoom <= 14) {
        return false;
    }
    if (startZoom >= 15 && endZoom >= 15) {
        return false;
    }
    // console.log(map.getZoom())
    //不同级别的data中style要换,图标动态更换,重新渲染style
    if (map.getZoom() <= 9) {
        massMarks.setStyle(massStyle[0])
    }
    if (map.getZoom() > 9 && map.getZoom() <= 14) {
        data.map(x => {
            x.isFull == 1 ? x.style = 1 : x.style = 2
        })
        massMarks.setStyle(massStyle)
    }
    if (map.getZoom() >= 15) {
        data.map(x => {
            x.isFull == 1 ? x.style = 3 : x.style = 4
        })
        massMarks.setStyle(massStyle)
    }
}

avatar
avatar
avatar
详细的方法都在上面了,图标的话根据级别不同,共有5种。

至此,高德地图项目的坑都被我踩了好多了。。。过程其实蛮曲折的,记录下来的话,好像没有什么东西似的,不过平时多记录以下踩坑的话,会后相信遇到类似的问题,会好很多,希望大家一样。。。。。

哦,好像还有个实时弹窗的问题

4. 实时投递弹窗和实时清运弹窗

实时弹窗利用的就是new Amap.Marker()方法,绘制的marker,为什么用这个呢?不用信息窗体,信息窗体在地图中只能同时存在一个,这个就操蛋了。。。。。所以,这里实时弹窗放弃信息窗体,因为其他地方我已经用过信息窗体。

直接上代码吧 显示3s,然后去掉,这里有个判断就是不在可视区域内的话,放在右上角,一个dom操作,在可视区域的话,需要显示在当前的实时的点上面,大概就这样。

/**
* 绘制实时投机信息窗体
* @date 2018-12-04
* @param {*} data
*/
function renderToudiWindow(data) {
    // console.dir(data)
    let lng = data.longitude;
    let lat = data.latitude;
    var bound = map.getBounds(); //地图可视区域
    //判断该点是否在可视范围内(针对东经,北纬)
    if (lng < bound.northeast.lng && lng > bound.southwest.lng && lat < bound.northeast.lat && lat > bound.southwest.lat) {
        recoveryMarker = new AMap.Marker({
            position: [lng, lat],
            content: createToudiWindow(data),
            offset: new AMap.Pixel(14, -26)
        });
        map.add(recoveryMarker)
        setTimeout(function () {
            map.remove(recoveryMarker)
            recoveryMarker = null
        }, 3000)
    } else {
        $(".module4 .nowWindow").append(createToudiWindow(data))
        setTimeout(function () {
            $(".module4 .nowWindow").children().first().remove();
        }, 3000)
    }
}



/**
* 实时投递信息窗体
* @date 2018-12-04
* @param {*} data
* @returns
*/
function createToudiWindow(data) {
    let info = `<div class="toudi-window">
                    <div class="qingyun-content">
                        <div class="title">回收机实时投递</div>
                        <div class="time">
                            <span>${data.cityName}</span>
                        </div>
                        ${data.list.map(x=>
                            `<div class="num">
                                <span>${x.typeName}:</span>
                                <span>
                                    <b>${x.type=='0'?Number(x.value):x.value}</b>
                                    ${x.valueCompany}
                                </span>
                            </div>`
                        ).join('')}
                        <div class="weight">
                            <span>${data.nickName}</span>
                            <span>
                                ${data.time}
                            </span>
                        </div>
                    </div>
                </div>`
    return info
}

在一个无序整数数组中,找出连续增长片段最长的一段, 增长步长是1。Example: [3,2,4,5,6,1,9], 最长的是[4,5,6]

let arr = [3,2,1,14,5,5,8,1,2,3,4,5,6,76,7,1,2,9];

function fn(arr){
    let temp = [];
    let sub = [];
    for ( let i = 0; i < arr.length; i++ ){
        if(arr[i]+1 === arr[i+1]){
            temp.push(arr[i]);
        }else{
            if(temp.length!=0){
                let temp1 = [];
                temp.push(arr[i]);

                for( let i = 0 ; i < temp.length; i++){
                    temp1.push(temp[i])
                }

                if(sub.length===0||sub.length<temp1.length){
                    sub = temp1
                }
                temp = [];
            }
        }
    }
    return sub;
}
let arr1 = fn(arr);
console.log(arr1);  //[1,2,3,4,5,6]

打平嵌套数组 [1, [2, [3], 4], 5] => [1, 2, 3, 4, 5]

const arr = [1,[2,[3],4],5]
function sort(arr) {
    for (let i in arr) {
        if (Array.isArray(arr[i])) {
            arr.splice(i, 1, ...sort(arr[i]))
        }
    }
    return arr
}
sort(arr)   //[1, 2, 3, 4, 5]

渐进增强和优雅降级

定义:

  • 优雅降级(graceful degradation): 一开始就构建站点的完整功能,然后针对浏览器测试和修复

  • 渐进增强(progressive enhancement): 一开始只构建站点的最少特性,然后不断针对各浏览器追加功能。

–都关注于同一网站在不同设备里不同浏览器下的表现程度

区别:

  • “优雅降级”观点认为应该针对那些最高级、最完善的浏览器来设计网站. 而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。

  • “渐进增强”观点则认为应关注于内容本身。请注意其中的差别:我甚至连“浏览器”三个字都没提。

理解:

  • “优雅降级”就是首先完整地实现整个网站,包括其中的功能和效果. 然后再为那些无法支持所有功能的浏览器增加候选方案, 使之在旧式浏览器上以某种形式降级体验却不至于完全失效.

  • “渐进增强”则是从浏览器支持的基本功能开始, 首先为所有设备准备好清晰且语义化的html及完整内容, 然后再以无侵入的方法向页面增加无害于基础浏览器的额外样式和功能. 当浏览器升级时, 它们会自动呈现并发挥作用.


进程和线程的区别

  • 一个程序至少有一个进程,一个进程至少有一个线程.
  • 线程的划分尺度小于进程,使得多线程程序的并发性高。
  • 线程是独立调度的基本单位, 进程是拥有资源的基本单位
  • 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
  • 线程在执行过程中与进程还是有区别的。
    • 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。
    • 但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 从逻辑角度来看,
    • 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

js实现对象的深克隆

因为js中数据类型分为基本数据类型(number, string, boolean, null, undefined)和引用类型值(对象, 数组, 函数). 这两类对象在复制克隆的时候是有很大区别的. 原始类型存储的是对象的实际数据, 而对象类型存储的是对象的引用地址(对象的实际内容单独存放, 为了减少数据开销通常放在内存中). 此外, 对象的原型也是引用对象, 它把原型的属性和方法放在内存中, 通过原型链的方式来指向这个内存地址.

于是克隆也会分为两类:

  • 浅度克隆: 原始类型为值传递, 对象类型仍为引用传递
  • 深度克隆: 所有元素或属性均完全复制, 与原对象完全脱离, 也就是说所有对于新对象的修改都不会反映到原对象中

深度克隆实现:

function clone(obj){
    if(typeof(obj)== 'object'){
        var result = obj instanceof Array ? [] : {};
        for(var i in obj){
            var attr = obj[i];
            result[i] = arguments.callee(attr);
        }
        return result;
    } else {
        return obj;
    }
};

点击一个ul的五个li元素,分别弹出他们的序号,怎么做?

for(var i=0; i<lis.length; i++){
    oLis[i].onclick = (function(j){
        return function(){
            alert(j);
        }
    })(i);
}
或 立即执行函数
for(var i=0; i<lis.length; i++){
    (function(j){
        oLi[j].onclick = function(){
            alert(j);
        };
    })(i);
}

typeof与instanceof

相同点:

JavaScript 中 typeof 和 instanceof 常用来判断一个变量是否为空,或者是什么类型的。

typeof的定义和用法:返回值是一个字符串,用来说明变量的数据类型。

细节:

(1)、typeof 一般只能返回如下几个结果:number,boolean,string,function,object,undefined。

(2)、typeof 来获取一个变量是否存在,如 if(typeof a!="undefined"){alert("ok")},而不要去使用 if(a) 因为如果 a 不存在(未声明)则会出错。

(3)、对于 Array,Null 等特殊对象使用 typeof 一律返回 object,这正是 typeof 的局限性。

Instanceof定义和用法:instanceof 用于判断一个变量是否属于某个对象的实例。

var a = new Array(); 
alert(a instanceof Array);  // true
alert(a instanceof Object)  // true

function test(){};
var a = new test();
alert(a instanceof test)   // true

谈谈垃圾回收机制方式及内存管理

回收机制方式
1、定义和用法:垃圾回收机制(GC:Garbage Collection),执行环境负责管理代码执行过程中使用的内存。

2、原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

3、实例如下

function fn1() {
    var obj = {name: 'hanzichi', age: 10};
}
function fn2() {
    var obj = {name:'hanzichi', age: 10};
    return obj;
}
var a = fn1();
var b = fn2();

fn1中定义的obj为局部变量,而当调用结束后,出了fn1的环境,那么该块内存会被js引擎中的垃圾回收器自动释放;在fn2被调用的过程中,返回的对象被全局变量b所指向,所以该块内存并不会被释放。

垃圾回收策略:标记清除(较为常用)和引用计数。

标记清除:

  定义和用法:当变量进入环境时,将变量标记”进入环境”,当变量离开环境时,标记为:”离开环境”。某一个时刻,垃圾回收器会过滤掉环境中的变量,以及被环境变量引用的变量,剩下的就是被视为准备回收的变量。

  到目前为止,IE、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。

引用计数:

  定义和用法:引用计数是跟踪记录每个值被引用的次数。

  基本原理:就是变量的引用次数,被引用一次则加1,当这个引用计数为0时,被视为准备回收的对象。

内存管理

1、什么时候触发垃圾回收?

垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。

IE6的垃圾回收是根据内存分配量运行的,当环境中的变量,对象,字符串达到一定数量时触发垃圾回收。垃圾回收器一直处于工作状态,严重影响浏览器性能。

IE7中,垃圾回收器会根据内存分配量与程序占用内存的比例进行动态调整,开始回收工作。

2、合理的GC方案:(1)、遍历所有可访问的对象; (2)、回收已不可访问的对象。

3、GC缺陷:(1)、停止响应其他操作;

4、GC优化策略:(1)、分代回收(Generation GC);(2)、增量GC

开发过程中遇到的内存泄露情况,如何解决的?

  • (1)、当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露。

  • (2)、由于是函数内定义函数,并且内部函数–事件回调的引用外暴了,形成了闭包。闭包可以维持函数内局部变量,使其得不到释放。
    实例如下:


jQuery 库中的 $() 是什么?

$() 函数是 jQuery() 函数的别称。$() 函数用于将任何对象包裹成 jQuery 对象,接着你就被允许调用定义在 jQuery 对象上的多个不同方法。你可以将一个选择器字符串传入 $() 函数,它会返回一个包含所有匹配的 DOM 元素数组的 jQuery 对象。

$(this) 和 this 关键字在 jQuery 中有何不同?

$(this) 返回一个 jQuery 对象,你可以对它调用多个 jQuery 方法,比如用 text() 获取文本,用val() 获取值等等。

而 this 代表当前元素,它是 JavaScript 关键词中的一个,表示上下文中的当前 DOM 元素。你不能对它调用 jQuery 方法,直到它被 $() 函数包裹,例如 $(this)。

JQuery有几种选择器?

(1)、基本选择器:#id,class,element,*;

(2)、层次选择器:parent > child,prev + next ,prev ~ siblings

(3)、基本过滤器选择器::first,:last ,:not ,:even ,:odd ,:eq ,:gt ,:lt

(4)、内容过滤器选择器: :contains ,:empty ,:has ,:parent

(5)、可见性过滤器选择器::hidden ,:visible

(6)、属性过滤器选择器:[attribute] ,[attribute=value] ,[attribute!=value] ,[attribute^=value] ,[attribute$=value] ,[attribute*=value]

(7)、子元素过滤器选择器::nth-child ,:first-child ,:last-child ,:only-child

(8)、表单选择器: :input ,:text ,:password ,:radio ,:checkbox ,:submit 等;

(9)、表单过滤器选择器::enabled ,:disabled ,:checked ,:selected

$(document).ready()方法和window.onload有什么区别?

(1)、window.onload方法是在网页中所有的元素(包括元素的所有关联文件)完全加载到浏览器后才执行的。

(2)、$(document).ready() 方法可以在DOM载入就绪时就对其进行操纵,并调用执行绑定的函数。

如何用jQuery禁用浏览器的前进后退按钮?

<script type="text/javascript" language="javascript">
  $(document).ready(function() {
    window.history.forward(1);
    //OR window.history.forward(-1);
  });
</script>

jQuery的事件委托方法bind 、live、delegate、on之间有什么区别?

(1)、bind 【jQuery 1.3之前】

定义和用法:主要用于给选择到的元素上绑定特定事件类型的监听函数;

语法:bind(type,[data],function(eventObject));

特点:

  (1)、适用于页面元素静态绑定。只能给调用它的时候已经存在的元素绑定事件,不能给未来新增的元素绑定事件。

  (2)、当页面加载完的时候,你才可以进行bind(),所以可能产生效率问题。

实例如下:$( “#members li a” ).bind( “click”, function( e ) {} );

(2)、live 【jQuery 1.3之后】
定义和用法:主要用于给选择到的元素上绑定特定事件类型的监听函数;

语法:live(type, [data], fn);

特点:

  (1)、live方法并没有将监听器绑定到自己(this)身上,而是绑定到了this.context上了。

  (2)、live正是利用了事件委托机制来完成事件的监听处理,把节点的处理委托给了document,新添加的元素不必再绑定一次监听器。

  (3)、使用live()方法但却只能放在直接选择的元素后面,不能在层级比较深,连缀的DOM遍历方法后面使用,即$(“ul””).live…可以,但$(“body”).find(“ul”).live…不行;

实例如下:$( document ).on( “click”, “#members li a”, function( e ) {} );

(3)、delegate 【jQuery 1.4.2中引入】
定义和用法:将监听事件绑定在就近的父级元素上

语法:delegate(selector,type,[data],fn)

特点:

  (1)、选择就近的父级元素,因为事件可以更快的冒泡上去,能够在第一时间进行处理。

  (2)、更精确的小范围使用事件代理,性能优于.live()。可以用在动态添加的元素上。

实例如下:

$(“#info_table”).delegate(“td”,”click”,function(){/显示更多信息/});

$(“table”).find(“#info”).delegate(“td”,”click”,function(){/显示更多信息/});

(4)、on 【1.7版本整合了之前的三种方式的新事件绑定机制】
定义和用法:将监听事件绑定到指定元素上。

语法:on(type,[selector],[data],fn)

实例如下:$(“#info_table”).on(“click”,”td”,function(){/显示更多信息/});参数的位置写法与delegate不一样。

说明:on方法是当前JQuery推荐使用的事件绑定方法,附加只运行一次就删除函数的方法是one()。

总结:.bind(), .live(), .delegate(),.on()分别对应的相反事件为:.unbind(),.die(), .undelegate(),.off()


简述一下src与href的区别

href 是指向网络资源所在位置,建立和当前元素(锚点)或当前文档(链接)之间的链接,用于超链接。

src是指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在位置;在请求src资源时会将其指向的资源下载并应用到文档内,例如js脚本,img图片和frame等元素。

当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,图片和框架等元素也如此,类似于将所指向资源嵌入当前标签内。这也是为什么将js脚本放在底部而不是头部。

浏览器的内核分别是什么?

IE: trident内核
Firefox:gecko内核
Safari:webkit内核
Opera:以前是presto内核,Opera现已改用Google Chrome的Blink内核
Chrome:Blink(基于webkit,Google与Opera Software共同开发)

随着大型单页应用程序的兴起,Javascript和CSS变得越来越交织在一起。通常情况下,在两者之间复制值。但是

保留两个相同值的方法,会导致有时更新只会有一个更新。使用wepack和css module有一种更好的办法;

第一步,安装依赖:

1
npm install sass-loader node-sass webpack --save-dev

接下来配置webpack,以便我们可以从js访问sass代码:

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
}
}

下面是最有趣的部分,在sass中导出定义的变量,css module有个实用的工具 :export。工作原理和es6 export关键字基本相同

。你的sass代码就会导出包含变量名称的对象,可以在js中使用,这些值都作为字符串导出。

1
2
3
4
5
6
7
8
9
10
11
12
13
// styles/animation.scss
$animation-length: 250;
$animation-length-ms: $animation-length + 0ms;

:export {
animationMillis: $animation-length-ms;
}

.component-enter {
...

transition: all $animation-length-ms ease-in;
}

现在,在Javascript中,我们只需要从样式表中导入样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// js/animation.js
import styles from '../styles/animation.scss'
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'

const millis = parseInt(styles.animationMillis)

...

<CSSTransitionGroup
transitionName="component"
transitionEnterTimeout={millis}
transitionLeaveTimeout={millis}
/>

...

什么是跨域?

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。

广义的跨域:

1.) 资源跳转: A链接、重定向、表单提交

2.) 资源嵌入: <link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、
    @font-face()等文件外链

3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等

其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。

什么是同源策略?

同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器
最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协
议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) AJAX 请求不能发送

常见跨域场景

URL                                      说明                    是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js         同一域名,不同文件或路径           允许
http://www.domain.com/lab/c.js

http://www.domain.com:8000/a.js
http://www.domain.com/b.js         同一域名,不同端口                不允许

http://www.domain.com/a.js
https://www.domain.com/b.js        同一域名,不同协议                不允许

http://www.domain.com/a.js
http://192.168.4.12/b.js           域名和域名对应相同ip              不允许

http://www.domain.com/a.js
http://x.domain.com/b.js           主域相同,子域不同                不允许
http://domain.com/c.js

http://www.domain1.com/a.js
http://www.domain2.com/b.js        不同域名                         不允许

跨域解决方案

1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域

一、 通过jsonp跨域

通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。

1.)原生实现:

<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参并指定回调执行函数为onBack
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
    document.head.appendChild(script);

    // 回调执行函数
    function onBack(res) {
        alert(JSON.stringify(res));
    }
</script>

服务端返回如下(返回时即执行全局函数):

onBack({"status": true, "user": "admin"})

2.)jquery ajax:

$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "onBack",    // 自定义回调函数名
    data: {}
});

3.)vue.js

this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'onBack'
}).then((res) => {
    console.log(res); 
})

后端node.js代码示例:

var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
    var params = qs.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

jsonp缺点:只能实现get一种请求。

二、 document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

1.)父窗口:(http://www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2.)子窗口:(http://child.domain.com/b.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

三、 location.hash + iframe跨域

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);

    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2.)b.html:(http://www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3.)c.html:(http://www.domain1.com/c.html)

<script>
// 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

四、 window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

1.)a.html:(http://www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2.)proxy.html:(http://www.domain1.com/proxy….

中间代理页,与a.html同域,内容为空即可。

3.)b.html:(http://www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

五、 postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的iframe消息传递
d.) 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为”*”,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2.)b.html:(http://www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

六、 跨域资源共享(CORS)

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。

需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:七、nginx反向代理中设置proxy_cookie_domain 和 八、NodeJs中间件代理中cookieDomainRewrite参数的设置。

目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。

1、 前端设置:

1.)原生ajax

// 前端设置是否带cookie
xhr.withCredentials = true;

示例代码:

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

2.)jQuery ajax

$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是否带cookie
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});

3.)vue框架

a.) axios设置:

axios.defaults.withCredentials = true

b.) vue-resource设置:

Vue.http.options.credentials = true
2、 服务端设置:

若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。

1.)Java后台:

/*
* 导入包:import javax.servlet.http.HttpServletResponse;
* 接口参数中定义:HttpServletResponse response
*/

// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); 

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true"); 

// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");

2.)Nodejs后台示例

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
            * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
            * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
            */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');

七、 nginx代理跨域

1、 nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
    add_header Access-Control-Allow-Origin *;
}
2、 nginx反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

1.) 前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2.) Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

八、 Nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

1、 非vue框架的跨域(2次跨域)

利用node + express + http-proxy-middleware搭建一个proxy服务器。

1.)前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();

2.)中间件服务器:

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

3.)Nodejs后台同(六:nginx)

2、 vue框架的跨域(1次跨域)

利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

九、 WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

1.)前端代码:

<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2.)Nodejs socket后台

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

感谢作者分享这么齐全的跨域解决方案,原文链接https://segmentfault.com/a/1190000011145364

事件模型以及周边

  • 事件捕获

  • 事件冒泡

  • 事件触发

  • 移动端事件模拟

  • 事件委托

事件捕获&事件冒泡

<div class="out">
    <p class="inner"></p>
</div>

给inner,out均绑定点击事件.点击inner,如果out先执行,inner后执行.则是事件捕获.若inner先执行.out后执行则是事件冒泡.(这两种模型来自于早期浏览器之争)

W3C模型

先事件捕获,到达目标后再进行冒泡

示例演示

out.addEventListener('click', (e) => {
    console.log('out clicked! ')
}, true)
inner.addEventListener('click', (e) => {
    console.log('inner clicked! ')
}, false)
document.addEventListener('click', (e) => {
    console.log('document clicked! ')
}, true)

点击inner后,执行顺序:document clicked => out clicked => inner clicked

out.addEventListener('click', (e) => {
    console.log('out clicked! ')
}, true)
inner.addEventListener('click', (e) => {
    console.log('inner clicked! ')
}, false)
document.addEventListener('click', (e) => {
    console.log('document clicked! ')
}, false)

点击inner后,执行顺序: out clicked => inner clicked => document clicked

事件模型

1.DOM0级事件(默认发生在冒泡阶段.只能绑定一个事件)

事件绑定

ele.onclick = function (){
    //
}

事件解除绑定

ele.onclick = null;

2.DOM2级事件(默认发生在冒泡阶段,由第三个参数决定,可绑定多个事件)

事件绑定

ele.addEventListener(eventType, handler, useCapture)//eventType不带on,如click
//IE下用attachEvent
ele.attachEvent(eventType, handler);

事件解除绑定

ele.removeEventListener(eventType, handler, useCapture)
//IE下使用detachEvent
ele.detachEvent(eventType, handler);

事件对象

DOM事件模型中的事件对象常用属性:

  • type用于获取事件类型

  • target获取事件目标

  • stopPropagation()阻止事件冒泡

  • preventDefault()阻止事件默认行为

IE事件模型中的事件对象常用属性:

  • srcElement获取事件目标

  • cancelBubble阻止事件冒泡

  • returnValue阻止事件默认行为

兼容处理

var eventUtil = {
    addEvent: function(ele, event, func, bool) {
        bool = bool || false;
        if (ele.addEventListener) {
            ele.addEventListener(event, func, bool)
        } else {
            ele.attachEvent('on' + event, func, bool);
        }
    },
    removeEvent: function(ele, event, func, bool) {
        bool = bool || false;
        if (ele.removeEventListener) {
            ele.removeEventListener(event, func, bool);
        } else {
            ele.detachEvent('on' + event, func, bool);
        }
    },
    getEvent: function(event) {
        return event || window.event;
    },
    getTarget: function(event) {
        return event.target || event.srcElement;
    },
    preventDefault:function (event) {
        if (event.preventDefault) {
            event.preventDefault();
        }else {
            event.returnValue = false;//IE
        }
    },
    stopPropagation:function  (event) {
        if (event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBubble = true;//IE
        }
    }
};

移动端事件

Touch事件

touchstart:当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。
touchmove:当手指在屏幕上滑动时连续的触发。在这个事件发生期间,调用preventDefault()可阻止滚动

touchend:当手指从屏幕上移开时触发。
touchcancel:当系统停止跟踪触摸时触发。关于此事件的确切触发事件,文档中没有明确说明。
以上事件的event对象上面都存在如下属性:
touches:表示当前跟踪的触摸操作的Touch对象的数组。
targetTouches:特定于事件目标的Touch对象的数组。
changeTouches:表示自上次触摸以来发生了什么改变的Touch对象的数组。

每个Touch对象包含下列属性:
identifier:表示触摸的唯一ID。

clientX:触摸目标在视口中的X坐标。
clientY:触摸目标在视口中的Y坐标。
pageX:触摸目标在页面中的x坐标。
pageY:触摸目标在页面中的y坐标。
screenX:触摸目标在屏幕中的x坐标。
screenY:触摸目标在屏幕中的y坐标。
target:触摸的DOM节点坐标

触发过程

touchstart =>touchmove =>touchend =>click(延迟300~200ms)

Tap事件封装原理:利用touchstart =>touchmove =>touchen模拟click

HTMLElement.prototype.tap = function (handler,interval) {
    that = this
    this.isMove = false,
    this.startTime = 0,
    this.addEventListener('touchstart',function(e){
            startTime = Date.now();
        }),
    this.addEventListener('touchmove',function(e){
            isMove = true;
        }),
    this.addEventListener('touchend',function(e){
            if(!this.isMove && (Date.now()-startTime) < interval){
                handler();
            }
            this.isMove = false;
            this.startTime = 0;
        })
};

事件委托

把本应绑定在自身的事件绑定到其他元素上来触发即事件委托

//HTML
<ul id="ul">
    <li>a</li>
    <li>b</li>
</ul>

//JS
    var oUl = document.getElementById("ul");
    oUl.onmouseover = function(ev){
        var ev = ev || window.event;
        var target = ev.target || ev.srcElement;
        if((/l\i/i).test(target.nodeName)){
        target.style.background = "red";
        }
    }
    oUl.onmouseout = function(ev){
        var ev = ev || window.event;
        var target = ev.target || ev.srcElement;
        if((/l\i/i).test(target.nodeName)){
            target.style.background = "";
        }
    }
//JS

感谢源著作者 https://segmentfault.com/a/1190000008774838