【下面的内容参考了很多大佬的文章,没有贴出来是因为怕链接参考太多会被审核人员认为该文章故意引流,后面会把参考文章全部放在一个笔记里面,希望各位大佬多多包涵】
【非常感谢大佬们的分享】https://segmentfault.com/n/13...
Vue篇章
常问原理
1. 什么是AST
1.1 AST(abstract syntax tree)意为抽象语法树,其实就是树形数据结构的表现形式,有父节点、子节点、兄弟节点等概念...(就是一个对象)
1.2 本身就是树形结构的HTML为什么还要转化?
因为真实DOM含不需要的属性太多了,如果筛选出我们需要的属性,再对其进行操作,将大大优化性能!
1.3 AST和虚拟节点vnode有什么关系?
它们结构很相似,AST其实算得上是vnode的前身,AST经过一系列的指令解析、数据渲染就会变成vnode!这边的AST其实只是简单的html解析。
2. 响应式原理,也就是双向绑定的原理
双向数据绑定真正的流程顺序(截止20210603)
先放一张图
第一步,template代码转换成AST
vue文件编译准备在浏览器看效果的时候,是经过了webpack的loader处理,这个我算它初始化吧,初始化的时候,把template通过各种正则,函数解析,注意这个时候还没处理vue的指令那些;
template
<div class="container">
<span :class="{active: isActive}">{{msg}}</span>
<ul>
<li v-for="item in list">{{item + $index}}</li>
</ul>
<button @click="handle">change msg</button>
</div>
转换后
第二步,AST转换成render函数
到这一步还是处于初始化那里,这一步主要解析vue的指令,v-for,v-if,{{}}也是通过一堆正则,函数解析
AST转render函数过程中,解析指令的时候,比如v-model,它会为这个dom添加input监听事件,为了后面的双向绑定服务;
因为双向绑定可以视图影响数据,也可以数据影响视图;
AST语法树转的render函数长这样:
function _render() {
with (this) {
return __h__(
'div',
{staticClass: "container"},
[
" ",
__h__('span', {}, [String((msg))]),
" ",
__h__('button', {class: {active: isActive},on:{"click":handle}}, ["change msg"]),
" "
]
)
};
}
这个render函数我说多两句
- render函数执行后,返回的就是虚拟dom,vnode,其实也是js对象,跟AST长的很像;
- render函数的唯一参数叫做,createElement,render函数执行的时候,是return createElement(),所以最后作用的其实是createElement,createElement函数执行后,返回vnode;
- render函数会存在到watcher订阅者对象里面,这个下面是重点,关于watcher的,注意,这个时候render函数还没执行,看下面
第三步,先劫持data的get,set方法,然后再触发render函数,render函数触发new watch
- 遍历data劫持是在生命周期的bfc和c之间,而执行render函数,生成vnode,是bfm和m之间;
- 遍历data,利用object.defineProperty劫持get和set的方法,每个属性有一个dep数组,里面插入的都是watch实例,这个插入逻辑有个术语叫依赖收集,依赖收集是在get方法收集,而set是派发通知,把dep数组里面所有watch对象都执行自己的update函数,这个update函数里面会跑render函数;
- 劫持完了后,这个时候render函数触发,函数内部new watch;
new watch这个类,new的时候,constructor方法里面会触发一个get的方法,它里面其实Dep.target = this,指向自己,然后let value = this.vm.data[this.exp],触发了咱们get方法,然后get方法开始收集依赖,往数组里面.push watch实例,这样就完成依赖收集,
3.1 这个watch实例大概有这些属性吧,
Watcher(this, this.render, this._update); render:就是上面转的render; update:更新数据(应该只是更新数据,让后面render的时候,能够拿到准确数据的vnode)
- 3.2. 上面那步render解析的里面,有多少指令,比如{{name}},v-model=name,,执行render后,name属性的订阅中心[]里面就有多少个watch对象
第四步,上面render函数执行后,生成vnode,然后通过diff算法比较,更新vnode,然后把vnode转换成真实dom,插入dom上面
第五步,当this.name赋值时,触发set方法,循环对应属性的dep数组的所有watch实例的update方法
vue的视图更新是异步的,为什么是异步,是因为watch的update函数把当前的所有准备执行的watch实例都放到的异步队列里,然后执行每个update函数,更新数据,然后执行render函数,获取最新vnode,通过diff算法,对比dom,更新vnode,然后再更新到dom上
2.生命周期
这个是我截图vue.js的initMixin方法
(个人理解)
template转ast
ast转render
bfc生命:所以这里拿不到data,因为还没遍历data初始化
initState:这个是遍历data劫持,双向绑定那些
c生命:可以拿到了并操作
(个人理解,不是很懂先跑子组件那里逻辑是怎样的)
bfm生命:看不出前面干了什么
定义了一个updateComponent方法,里面先跑render,得出虚拟vnode,然后再执行update,
new了一个watch,把这个方法放进去了当做回调函数
虽然是回调函数,不然它内部new的时候应该有执行了一次,
m生命:所以这里可以访问真实dom,$el有值(真实dom)
如果在编译的时候遇到子组件,在进入m钩子之前,会先跑完子组件的bfc到m钩子,跑完之后再回到父组件的mounted,因为这样生成的虚拟dom才是完整的;
bfu和u阶段,如果data里面数据改变,会重新生成虚拟dom(执行render函数),然后拿新的虚拟dom和旧的比(给update函数),看看要更细哪些,当对比完映射真实dom之后,u钩子就触发;
bfd和d阶段,一般在bfd清除定时器这种;
3.渲染原理
template是无法直接给浏览器解析的,不是正常的htmll语法,所以渲染就是解析这个template,把跟vue相关转成可以运行的一个东西,就是render函数,跟jsx有点像;这个过程是用webpack的loaders的vue-loader和依赖包的vue-template-compiler实现;
4.虚拟dom
dom节点在js里面表现的一个抽象数据结构对象,因为如果直接操作dom更新会有性能问题,当每次响应式数据更新的时候,会生成新的虚拟dom,然后新旧对比,匹配找出最少的真正需要更新的dom,再操作;
5.vue3.0和vue2.0的区别
- 响应式重写了,用proxy代替了object。defineproperty
- 生命周期改变了;
- 支持typescript,也建议用;
6.main.js的是new Vue()和组件的实例关系
原来,根的new Vue和组件的new vue是继承关系,
从打印this可以看出,
组件的this.__proto__ 指向的是构造函数VueComponent.prototype
然后这个组件的原型对象VueComponent.prototype.__protp__指向的是根vue的原型对象
所以可以得出这两个就是继承的关系 VueComponent.prototype.__proto__ = Vue.prototype
而且呢,vue的东西,比如vue.mixin在组件里面也是生效的,说明VueComponent和Vue不单单是继承这么简单,底层里面VueComponent肯定也有调用了Vue的东西,要不然为什么vue.mixin的东西,也会再VueComponent生效
7. 挂在根vue实例的东西,可以直接在组件模板用
挂在this(组件的)上面的东西都可以在模板使用,比如直接this.xxxx = '哈哈',挂在data里面只是为了实现响应式而已,
原来挂在vue原型对象上的也可以,不过那不是响应式的,这也说明了为什么以前发现$router和$store这种为什么能直接在组件模板使用,因为挂在了vue的原型对象了,原来如此
8. 为什么this.name,而不用this.data.name
也是用了Object.defineProperty,大概是这样
Object.defineProperty(this, name, {
get: function proxyGetter () {
return that.data['name'];
},
set: function proxySetter (val) {
that.data['name'] = val;
}
})
this.name,然后get里面返回的是this.data.name,所以能拿到
9. vue几个版本
在用脚手架构建记得曾经看过有选安装vue的版本,要runtime+Compiler,还是只runtime,最近百度了下,原来
Compiler这个是用来帮我们解析模板的,就是我们平时
<template>
<div id=app>
<div>{{name}}</div>
</div>
</template>
vue文件都是这样写,但是这个结构是要解析器来解析,最终转成render函数,对吧,这个Compiler(应该是个函数,一堆正则那种)干这事
所以如果选vue的时候,不要Compiler,那代码就不能这么写
还有,纯index用vuejs那个就是这种完成的版本,我贴个图
然后,我们现在用的webpack开发,用的版本是只带rumtime的,那为什么我们还能用vue文件写法,因为有另外一个工具人vue-loader,这个来实现
接下来看vue语法的一些原理
1.computed与watch的区别
简单的运算可以用computed,不支持异步,computed计算获取值的时候是会拿缓存的,从原理来看它是提供set跟get,如果用函数写法,return值就是get,如果是对象写法,就提供get和set;
watch推荐有复杂的操作,可以异步;
2.为什么组件的data必须是一个函数
因为组件是可以复用,如果父组件在多个地方引用同一个组件,这时候data是对象的话,因为引用类型的问题,引用同一个组件,修改data会影响到其他组件的data,可以用构造函数的prototype理解;
3.nexttick原理
vue的数据更新时,dom不会立即更新,而是放到异步队列(微任务队列)里,如果这时候获取dom拿到的是旧值,用了nexttick就可以保证是dom更新后,
底层是用了promise.resolve.then,把nexttick的回调插在then后面,就相当于插入了微任务队列的尾巴,当主线程的宏任务执行完,执行异步任务,微任务有dom的更新代码,然后尾巴的nexttick就可以拿到更新后的dom,
(PS:如果promise不支持,会变成setTimeout0,如果再不支持,用new MutationObserver ,这个API可以监听到dom的更新,然后执行回调)
4.为什么style加了scoped就不会影响子组件样式
原理:
原来style加了scoped之后,webpack编译文件的时候,会在你所有标签加一个属性data-v-xxxxx,亲测如果不加scoped就不会有
然后注意看右边的选择器,默认会在最后一个加.xxxx[data-v-xxxxxxx],这个是属性选择器的写法,意思是这个dom带有data-v-xxxx这个属性才会生效,举个栗子
父组件.hello .sky2[data-v-469af010],表示htllo下面,sky2并且带属性data-v-46才会生效,但是子组件的data-v和父组件不一样,所以命中不了
如何生效呢?
下面两种都要注意权重够不够
>>> (css语法?)或者 /deep/
.hello >>> .sky2 { color: pink; } .hello /deep/ .sky2 { color: pink; }
写多一个style,多个style最终编译会合并一个
<style scoped> h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } </style> <style> .hello .sky2 { color: pink; } .smg[bbb]{ font-size: 40px; } </style>
接下来看有关vue全家桶的问题
vue-router
原理
浏览器的url变了的话,是会重新刷新页面,并且发出请求,这个是规则,
那为什么spa,单页面项目url变了却不会刷新页面?其实因为浏览器url有两个模式
hash模式:
原来的浏览器url有一个模式叫做hash(哈希),基于window.location.hash来实现的,如果你的url是这样,https://xxxxx.html#1111,
然后你url变成https://xxx.html#2222,
整个页面并不会刷新,然后你再监听
window.addEventListener('hashchange',() => {})
当你#后面改变了值,监听触发,这样就可以在监听里面替换dom的内容,看起来就像页面跳转了一样
history模式:
这个模式是H5的window.history新增的API支撑的,用pushState 和 replaceState操作历史栈,整个页面也不会刷新,然后监听window.addEventListener('popstate',() => {}),其实看起来效果跟hash模式差不多的
注意:
用了history后,url可以不带#,更美观,但是,去掉了#,意味着如果用户自己手动改url,刷新了,会发送了请求给服务端,如果把路由改了一个不存在的地址,页面会报错,无法访问您的文件,这个时候需要服务器支持,重定向到某个页面
初始化
一般用Vue.use使用的东西,比如这个router插件,router内部其实有一个install方法,router在这个install方法里面初始化,这是一定的,因为这个是use内部的方法实现,会调用传进去对象的install方法,如果没有,就默认传进来的是个函数,直接执行
Vue.use(router)之后,还要把router放在new Vue的里面选项里面,是为了调用了vue的.mixin,在全局的beforecreate的生命周期把当前的实例this绑上router等属性,并且指向根实例的$router,没错,组件里面的this.$router调用的是根实例的,
1. route,routes,router
route: 当前激活的路由的信息,this.$route可以拿到比如,name、path、query、params等属性
routes:一个数组,多个路由的集合
router:管理,调用push,go操作路由,页面跳转会在routes查询路由地址
2. 什么是动态路由
动态路由的配置就是在路径的后面用:加参数名,然后跳转传参的时候,用params传参,在url的表现是/tem/123,这样页面刷新的时候,参数就不会丢失
而如果不用动态路由,用query传参,也可以刷新不丢参数,url的表现是/tem?id=123
2.参数改变,重复跳转同个路由,如何监听
重复跳转同个路由,组件会被复用,生命钩子不会重新跑,可以watch监听$route,或者组件里面的更新路由钩子,一共三个,进入前,更新,进入后
3.路由懒加载,优化性能
在路由定义那里,component的值写法不同,可以用vue的require,或者webpack的require.ensure
{
path: '/home',
name: 'home',
component: resolve => require(['@/components/home'],resolve)
}
{
path: '/home',
name: 'home',
component: r => require.ensure([], () => r(require('@/components/home')), 'demo')
}
4.路由守卫
全局的router实例有两个钩子
// r.beforeEach((to,from,next) => {
// console.log('进入前');
// next()
// })
// r.afterEach((to,from) => {
// console.log('进入后');
// })
而路由独享,在配置里面写的只有一个
// {
// path: '/que',
// name: 'que',
// component: que,
// beforeEnter: (to, from, next) => {
// console.log('独有进入前');
// next()
// }
// },
组件里面有三个
// beforeRouteEnter (to, from, next) {
// console.log('组件路由进入前');
// next()
// },
// beforeRouteUpdate (to, from, next) {
// console.log('组件路由更新前');
// },
// beforeRouteLeave (to, from, next) {
// console.log('组件路由离开后');
// next()
// },
他们的一个顺序是
全局>路由的>组件的
4.一般登录拦截怎么做
在全局的路由守卫beforeEach,通过在路由的meta字段加一些属性,判断这个路由需要登录,然后next到login页面
vuex
原理
vuex其实跟router是一样的,用use,也是写在根的new Vue的选项里面,因为也要在Vue.mixin,在beforecreated里面全局混入,当然this.$store也是指向的是根的,(这里我提一个疑惑点,$router用hasOwnProperty在组件里面判断是false,说明$router没有挂在this上面,而$store是true,说明它挂在了this上面,但是呢,这两者用的时候引用的都是根vue实例的)
为什么state可以是响应式的?
原来vuex内部用了new Vue({data:state})把state对象塞了进去,变成了响应式,所以vuex是只能给vue用的,不能给其他框架用,这里再说一下为什么new Vue了一个就可以,我自己的看法
var object = {
name: '我是名字'
}
new Vue({
data: {
objectTrue : object
}
})
Vue.prototype.$temObject = temObject
组件的
{{$temObject.name}}
data里面的数据全部走了一遍object.definedproto,所以一旦触发值的改变,就会触发set的方法,然后set的方法本来存的就是一些dom的更新操作,然后呢,必须是对象,因为对象赋值才能变成引用关系,所以你改了object的值,也相当于你直接改了objectTrue的值,如果是object是字符串是不行的,我试过,
然后,组件模板我明明用的是$temObject,跟objectTrue的名字不一样,不懂为什么dom绑定事件会自动塞到objectTrue的set里面,是因为是对象赋值是引用地址共享,所以两个看做一样吗?
1. 为什么异步不推荐在mutations使用
主要会影响到vue-devtools调试工具的数据,如果你在浏览器那里观察vuex值的变化,在mustation里面用了异步,会不准,逻辑上是不会出错;
2.module的模块引入
一般到后面模块太多,都会抽出来用module引入,每个模块定义自己的state,mutation那些配置,除了sate分模块,action,mutation,getter都会在全局注册,action,mutation如果重名触发的时候回触发,可以设置命名空间namespaced为true;
3.浏览器刷新之后vuex数据消失
可以结合浏览器缓存localstorage使用,或者重新重新请求再存进去
4.传值
祖孙级别,用$attrs,$listeners
<div class="hello"> <div>祖父</div> <f2 :number="number" :msg="msg" @openDoor="openDoor" @closeDoor="closeDoor"></f2> </div> provide: { number2: 999, cld:function(){ console.log('你好'); } },
<div class="hello"> <div>父级</div> <f3 v-bind="$attrs" v-on="$listeners"></f3> </div> created(){ console.log('来自祖级的',this.$attrs); console.log('来自祖级的事件',this.$listeners); console.log('inject的祖级',this.number2); console.log('inject的祖级',this.cld); },
<div class="hello"> <div>子级</div> <div @click="openDoor">触发</div> </div> created(){ console.log('来自父级的',this.$attrs); console.log('来自父级的事件',this.$listeners); console.log('inject的祖级',this.number2); console.log('inject的祖级',this.cld); }, methods:{ openDoor(){ console.log('开门'); this.$emit('openDoor','参数') }, closeDoor(){ console.log('关门'); this.msg = '关门' } }
祖孙级别,用provide,inject
父组件直接provide提供数据,孙子直接injuect变量名字就可以Vue.component('ye',{ template: '<div>我是爷爷<baba></baba></div>', provide:{ number: 10000 } }) Vue.component('baba',{ template: '<div>我是爸爸<erzi></erzi></div>' }) Vue.component('erzi',{ template: '<div>我是儿子</div>', inject:['number'], created(){ console.log('我是儿子',this.number) } })
axios
1.axios特点
基于promise的http库,它不是插件,所以引入的时候不用vue.use,为了方便其他组件使用,可以定义全局的request方法,或者将axios赋值给vue的原型对象使用;
2.如何配合登录,大致思路
2.1 全局封装request,在request的then用一个函数判断code是否正常,是过期还是正常
export function request(options,retryTime = 2){
if (retryTime === 0) {
return Promise.reject(null);
}
return axios({
...options,
headers:{
...options.headers
}
})
.then(res => {
return checkStatus(res,options,retryTime)
})
.catch(error => console.log(error))
}
2.2 在请求拦截那里,全局为请求头headers添加token,
// 拦截器
axios.interceptors.request.use(function(config){
config.headers['X-Auth0-Token'] = newToken
window.axiosCancel.forEach((item,index) => {
item()
window.axiosCancel.splice(index,1)
})
config.cancelToken = new axios.CancelToken(function(c){
window.axiosCancel.push(c)
})
return config
})
当接口返回token失效,用第一个返回的接口回调请求登录接口,然后用一个空数组将所有请求存起来,等登录接口回调之后,更新token,把数组的请求全部执行
// 检查token是否过期,并且把请求塞进一个数组,请求完成后把数组的请求全部执行
let isRefreshing = true;
let subscribers = [];
function checkStatus(response,options,retryTime){
if(response && response.data.code === -401){
if(isRefreshing){
console.log('正在请求token');
setTimeout(() => {
console.log('请求token成功');
oldToken = newToken
isRefreshing = true
subscribers.forEach((callback)=>{
callback();
})
subscribers = [];
},3000)
}
isRefreshing = false
return new Promise((resolve) => {
subscribers.push(() => {
resolve(request(options,retryTime - 1))
})
})
}else{
return response;
}
}
3.axios如何取消请求
我个人理解,取消请求,就是判断短时间内保证只有一个相同的请求发出去,
可以用常见的防抖实现,
也可以用数组存储,判断url是否相同,
上面两种方法都是从发起请求前的解决
如果是发起请求后,就需要用到axios内置提供的方法,下面说一下
// 拦截器
axios.interceptors.request.use(function(config){
config.headers['X-Auth0-Token'] = newToken
window.axiosCancel.forEach((item,index) => {
item()
window.axiosCancel.splice(index,1)
})
config.cancelToken = new axios.CancelToken(function(c){
window.axiosCancel.push(c)
})
return config
})
- config默认有一个属性叫cancelToken,给它赋值,值是 new axios.CancelToken(fn(c){})
- 还要定义一个全局的数组,可以挂在window,也可以挂在vue.prototype
- 数组里面插入CancelToken的函数的第一个参数,c,是一个函数
- 每次请求循环数组,执行这个函数,然后network会显示这个请求canceled了,红色的
webpack
1.先说说流程
(1)初始化参数
解析 Webpack 配置参数,合并 Shell 传入和 webpack.config.js 文件配置的参数,形成最后的配置结果。(个人理解,就是你执行npm run dev的时候,实际上是跑了webpack的服务,它的服务里面有加载webpack.base.conf.js和webpack.dev.conf.js,所以拿到了入口配置,loader配置等等参数)
(2)开始编译
用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,插件开始监听webpack的生命周期(compile,after-compile等),执行对象的 run 方法开始执行编译。
(3)确定入口
读取配置文件( webpack.config.js )中指定的 entry 入口,准备出发编译
(4)开始编译
从配置文件( webpack.config.js )中指定的 entry 入口,开始解析文件构建 AST 语法树,找出依赖,递归下去。
(个人理解,从入口文件,一般是main.js出发,根据文件不同的类型,调用不同的loader,转AST,然后根据AST找这个文件的依赖,并且也把这个AST转成浏览器可以运行的代码(字符串的JS代码),然后再根据那些依赖,也就是文件里面import,require那些文件,再重复这个步骤),等到所有文件都处理了之后,它还会根据所有处理后的文件,生成一份所有文件的依赖图,依赖图里有code,filename和depend依赖
大概长这样
(5)输出资源
根据上图的依赖图(一个数组),生成不同的chunk文件
(6)输出完成
根据chunk文件,输出bundle文件,写入dist文件夹里面(你的webpack配置输入路径)
插件会在不同的生命周期处理代码,比如压缩代码插件UglifyPlugin会在loader编译完代码后的那个节点压缩代码
2. 关于规范,Commonjs规范、AMD规范、CMD规范、UMD规范、ESModule规范
2.1 Commonjs规范(服务器环境运行)
首先必须明确一点,规范,规范,就是我们通常说的这里应该大写比较好,这里不加分号比较好这种,只是一种文案,并没有实际的输出,比如说没有语法
网上教程说的commonjs语法,moudle.exports导出,require引入,这些语法的提供者是nodejs,nodejs才是编程语言,跟java,js这种一样
nodejs按照commonjs规范创造出来,
nodejs是后端语法,在服务器运行,所以前端html写moudle.exports,require语法会报错
为什么在webpack构建的项目可以用?
你看看webpack配置文件的那些js,语法全部都是node语法,而且它的工作原理就是在本地开了一个服务器,我们代码保存之后,在他服务器环境走他的逻辑处理。
语法
module.exports和module.exports.a = a的作用一样,最后引入那里都是一个对象,所以一般用module.exports = {a,b}是为了方便,不用一个一个.a,.b输出
exports只是对module.exports的一个引用// 相当于这里还有一行:var exports = module.exports;代码 exports.a = a; // 相当于:module.exports.a = 'Hello world'; // 输出 var a = 100; var b = ['1'] function addA(){ a = a+1 console.log('值增加',a) b.push(a) console.log('数组增加',b); } module.exports = { a, b, addA } // module.exports.a = a // module.exports.addA = addA // 引入 var ma = require('../module/com.js') console.log('看看',ma); //{a:100,addA:fn,b:['1]}
任意位置引入,并且加载的方式是同步,也就是说会阻塞下面的代码
任意位置是区别于es6的import,它只能在代码的最上面引入,而这个require随便位置都行
如果require那里引入的文件太大,在引入完成前,下面console不会输出//.vue文件 created(){ var ma = require('../module/com.js') console.log('看看',ma);//{a:100,addA:fn,b:['1]} ma.addA() console.log('看看',ma); ma.b.push('我是自己加的') setTimeout(() => { console.log('5秒后再加载'); var ma = require('../module/com.js') console.log('看看',ma); },5000) }
这个文件只会初始化一次,后续全部拿的是缓存,并且赋值是浅拷贝,会互相影响
先解释什么叫做初始化一次var ma = require('../module/com.js') //初始化 console.log('看看',ma);//{a:100,b:['1']} ma.addA() //这一步,在comjs里面把a++,和数组push console.log('看看',ma);//{a:100,b:['1',100]},a没变是因为module.exports只输出了一次,当时a的值是100,而且a是基础数据类型,而b是数组,引用类型 setTimeout(() => { console.log('5秒后再加载'); var ma = require('../module/com.js') //第二次加载,拿缓存,就是第一次初始化的值,所以a还是100,而b是数组 console.log('看看',ma);//{a:100,b:['1',100]} // },5000)
再解释什么是赋值浅拷贝
module.exports = { a, b, addA },这句代码输出的是一个对象{},然后你外面引入进来也是这个对象,所以关系已经引用了var ma = require('../module/com.js') //初始化 console.log('看看',ma);//{a:100,b:['1']} ma.a = 999 ma.b.push(100) setTimeout(() => { console.log('5秒后再加载'); var ma = require('../module/com.js') 第二次加载,拿缓存, 这个缓存就是你上一步初始化的对象 然后你上面改了这里对象的值 所以第二次加载对象也变了 console.log('看看',ma);//{a:999,b:['1',100]} // },5000)
2.2 AMD规范(前端就可以运行)
amd规范是浏览器的规范,所以html那里直接用就行没问题
实现了amd规范的是一个叫requirejs的库,define,require语法都是这个requirejs提供的,所以要用amd规范要引入这个js文件
语法
导出
define(['./bfde.js','第二个依赖'],function(data,第二个data) {})
前面那个数组装的这个文件依赖了哪些文件,data里面可以拿到,数组里面有多少个依赖,后面回调的data就有多少个
引入
require(['../module/de.js','第二个模块'],function(modeule,第二个module){})
跟上面的其实一样规则console.log('这个是de初始化') define(['./bfde.js'],function(data) { console.log('看看依赖的',data); let flag = true var a = 100; var b = ['1'] function addA(){ a = a+1 console.log('值增加',a) b.push(a) console.log('数组增加',b); } setTimeout(() => { console.log('3m变成false'); flag = false },3000) function say(){ if(flag){ console.log('这是1'); }else{ console.log('这是0'); } } return { a, b, addA, say } }); .vue文件 require(['../module/de.js'],function(modeule){ console.log('加载的',modeule); modeule.addA() modeule.say() }) // console.log('看看',a); // console.log('看看',a2); // console.log('看看',b); // // a = 666 // a2.n = 9999 // b.push('自己的') // // addA() // console.log('看看',a2); // console.log('看看',b); setTimeout(() => { require(['../module/de.js'],function(modeule){ console.log('加载的',modeule); modeule.say() }) },5000) }
- 是异步加载,其他跟commonjs一样
- 数据的引用跟commonjs一模一样
2.3 CMD规范(前端就可以运行)
cmd规范也是浏览器的规范,所以html那里直接用就行没问题
个人理解,amd和cmd的区别在于,amd要全部load完依赖才会进入逻辑,而cmd不用,他可以在需要的时候才load,我举个例子
// AMD
define(['a.js','b.js','c.js'],function(require, factory) {
var tem = function(a,b){
return a+b
}
return {
add: tem
}
});
// CMD
define(function(require, exports,module) {
if(true){
require('a.js')
}
if(false){
require('b.js')
}
if(true){
require('c.js')
}
var tem = function(a,b){
return a+b
}
module.exports = {
add: tem
}
});
大概是这个感觉,不一定对,因为讲道理,如果bjs用不上,我也不会插入到amd数组那里面啊
我看了下其他资料,CMD其实是这样,它会扫描代码块,抓require的关键字,然后加载完require里面的js文件之后,才会执行它这个函数
js文件如果是http拿回来的话,是有分两个步骤,一个是加载,一个是执行
我猜amd应该是加载+执行,而cmd只有加载,然后当代码真正运行到那里的时候,再执行执行,算是懒加载把
实现了cmd规范的是一个叫seajs的库,语法都是这个seajs提供的,所以要用cmd规范要引入这个js文件
语法
导出// 前面没有数组,取而代之的是用函数参数require来引入前置依赖 define(function(require, exports,module) { var a = require('./a') a.doSomething() var tem = function(a,b){ return a+b } module.exports = { // 这个跟amd不一样 add: tem } });
引入
// 唯一区别就是require变成seajs.use seajs.use(['./cmd_math.js','xxxx'],function(module,m2){ console.log('看看',module.add(1,2)) })
其他应该是amd是一样的 (没有验证,猜想)
2.4 UMD规范
这个不太清楚,没去查太多资料,也不知道是浏览器用,还是服务器用的,只知道一点,这个是我们用平时用的最多的那种,
比如index文件用script引入vueminjs,或者引入上面说的requirejs,seajs等等,echartsjs等等
2.5 ESModule规范 (前后端都能用)
这个规范是es6提出来的,所以体现这个规范的语法就是我们平常用的import,export,exportdefault,这些
查了下资料,有人说从 ES6 开始,在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代 CommonJS 和 CMD、AMD 规范,成为浏览器和服务器通用的模块解决方案。而且nodejs版本发布也是慢慢的支持import代替以前的cj那种modules.export
经过百度,纯html如果要用的话,
首先,第一,script标签要加type=module,这个要看浏览器是否支持,
<script src="./esm_main.js" type="module"></script>
第二,必须开本地服务器访问模式才行
// 文件协议的访问不行,会报跨域错误
file:///E:/myproject/mytest/xieyi.html
// 这种才可以
http://localhost:5500/xieyi.html
至于为什么会报跨域错误,有资料说因为加了type=module后,会相当于发一个请求去解析这个es6模块,既然是请求,那就肯定有跨域的问题,file协议不行,改成http后,因为都是一个localhost下的,所以不会跨域
语法
// 输出规则 // 1. export default后面跟的是值,它原理是把值赋值给这个default,所以 // export default var a = 10 // 报错,不是值 // export default var a = function(){} // 报错,不是值 // var a = 10 // export default a // 正确 // export default {} // 正确 // export default function(){} // 正确 // export default { // a:a // } // 正确 // 2.export有两种语法,后面跟的不是值,是接口,文档说是接口,但是理解成变量名字就行 // export var a = 10 //正确,输出一个变量a,值10 // export var a //正确,输出一个变量a,值undefined // export 10 // 报错,不能跟值 // var a = 10 // export a // 报错,本质还是输出值 // var a = 10 // export { //第二种语法,加括号,里面是变量名字 // a // } // export { //我一开始还以为上面是es6的key跟value值相同的缩写,结果不行的,下面这样写会报错,(注意,default那个是可以的) // a:a // } // export { //第三种语法,as后面跟名字,随便你定 // a as b // } // 引入规则 // 1. export default的变量名随便取 // import tem from 'xxxxx.js' //tem名字随便,值取决于你导出来的值 // 2. export的要一一对应好,有三种写法 // import {b,c} from 'xxxxx.js'; // 第一种,要加括号,名字对应好 // import {b as tem,c} from 'xxxxx.js'; // 第二种,要加括号,改一下名字 // import * as tem from 'xxxxx.js'; // 第三种,全部引入,值是对象 // 3.一个模块js只能有一个export default,可以有多个export,并且两个可以同时存在 // import tem,{b,c} from './shuchushuru.js'; // 同时存在的引入
不能任意位置引用,写在{}类似代码块里会报错,并且会变量提升,总是升到最前面,如果真的需要任意位置引用,可以用import(),而且它是编译的时候加载,所以不阻塞(控制台直接报错)
es6文件 console.log('es6666666666666666666') var a = 100 var b = ['1'] function addA(){ a = a+1 console.log('值增加',a) b.push(a) console.log('数组增加',b); } export { a, b, addA } .vue文件 console.log('===========') import {a,b,addA} from '../module/es6.js' export default { name: 'HelloWorld', 上面步骤控制台先输出es66666666,再输出===========,这个是变量提升 .vue文件 这样就是任意位置加载,不会变量提升 created(){ import('../module/es6.js').then(modeule => { console.log('看看',modeule); }) // console.log('看看',a); // console.log('看看',b); // b.push('自己的') // addA() // console.log('看看',a); // console.log('看看',b); }
引入的值是实时的,并且非引用类型的值不允许外部引用方修改,只能内部改,不然会报错
es6文件 console.log('es6666666666666666666') var a = 100 var b = ['1'] var a2 = { n:999 } function addA(){ a = a+1 console.log('值增加',a) b.push(a) console.log('数组增加',b); } export { a, b, a2, addA } .vue文件 created(){ console.log('看看',a); //100 console.log('看看',a2); //{n:999} console.log('看看',b); // ['1'] a = 666 这个报错,a不允许修改,只允许通过addA()修改 a2.n = 9999 可以改,a2是对象 b.push('自己的') 可以改,数组也是对象 console.log('看看',a2); //{n:9999} console.log('看看',b); //['1','自己的'] setTimeout(() => { import('../module/es6.js').then(modeule => { console.log('看看',modeule); //全部都是新的值 }) },5000) }
2. loader和插件的区别
loader是对模块源码的转换,比如把一些非js模块转换js,scss语法转成css;插件是解决loader无法实现的事,比如打包优化和压缩
3.常见的loader
1.sass-loader: 用sass语法
2.postcss-loader: css3补前缀
3.postcss-pxtorem: px转rem
4.babel-loader:es6转es5
4.打包时如何优化
- 把js,css用相关插件压缩
- 大图,静态资源放存储桶,用url代替本地图,减少代码包
5.打包遇到的问题
- 打包dist文件夹路径不对,图片不显示,把assetsPublicPath的绝对路径/,改成相对路径./
CSS篇章
flex: 1代表有哪些属性
flex-grow: 剩余空间是否占用,默认0不占用,数字越大占的越多;
flex-shrink: 空间不足是否缩小,默认1缩小,0就不缩小;
Flex-basis:原本占有的大小,默认auto,可以写px固定值;
下面两个等价
flex:1
.item {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%;
}
flex: 0%
.item1 {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%;
}
transition和animation区别
参数位置一样,
属性/动画名字 时间 效果 延迟
display:none =》block
transition:不生效
animation:生效
display:block =》none
transition:不生效
animation:不生效
.donghua {
width: 100px;
height: 100px;
background-color: skyblue;
display: none;
}
.show {
display: block;
transition: all 1s linear 0s;
}
.showanimation {
display: block;
animation: donghuala 5s ease 0s;
}
@keyframes donghuala {
0%{
opacity: 0;
}
100%{
opacity: 1;
}
}
居中方式
- flex
- 定位,left,top:50%,如果宽高确定,margin-left,top自身负的一半,如果不确定,用transform,translate-50%
- display:table-cell,vertical-align: middle,子元素如果块级元素,自己加margin0 uato,如果是行内块级,父元素加一个text-align:center
:root
:root是一个伪类,表示文档根元素,非IE及ie8及以上浏览器都支持,在:root中声明相当于全局属性,只要当前页面引用了:root segment所在文件,都可以使用var()来引用
:root {
--main-bg-color: pink;
}
// 所有页面都能用
body {
background-color: var(--main-bg-color);
}
html篇章
js篇章
1.JS引擎解析过程
第一步:解释阶段
这个步骤主要读取js代码,把代码转成机器码,最终电脑执行的是机器码
第二步:预处理阶段
到这里还没跑JS代码,它会处理一点东西,比如说
分号补全
执行JS是需要分号的,原来我们平时用eslint把分号搞掉,但是在这个阶段它又补回去了,
这里提一个规则,当有换行符的时候,如果下一个语法没办法跟前面语法匹配,会自动补function b() { return { a: 'a' }; }
由于分号补全机制,所以它变成了:
function b() { return; { a: 'a' }; }
所以运行后是undefined
- 变量提升
函数声明提升,和变量提升,记住几个点
1.函数表达式是不会提升的,只有函数声明会
2.函数里面没有用var,let定义的变量,函数执行的时候,默认变成全局的var 定义
3.函数提升 > 变量提升
第三步:执行阶段
这里真正开始跑逻辑了,首先是这样
1.浏览器首次执行JS代码,会创建一个全局的执行上下文,然后压执行栈,JS代码执行完毕前在执行栈底部永远有个全局执行上下文
执行栈:装的都是执行上下文,跑代码其实都是先创建一个执行上下文,放到执行栈里,然后运行逻辑,
上下文只有两种东西会创建,一个是全局的script标签,一个是函数
这里提到一个执行上下文,我按自己的理解说一下,
它是一个对象,对象里面有三个属性,分别叫,
变量对象(VO,AO):就是存那些变量,VO和AO只是一个名字,对于全局来说,它的变量对象叫VO,对于函数来说,函数在运行的时候,这个VO会变成AO;
作用域链:所有父级的全部变量对象,大概[VO,VO,VO],这种感觉,而且当你函数运行的时候,你的AO会插到这个前面,变成[AO,VO,VO,VO],所以拿数据都是拿自己先,找不到再来父级找
this:确认this指向
2.然后往下执行,遇到作用域(js的作用域指的是,函数和全局,所以上面一开始执行JS代码,也算遇到作用域),会创建执行上下文,每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内
3.当函数运行,把自己的VO变成AO,插到作用域链最前面,这里说一点,闭包是因为,你的AO对象的某个变量,是上一个作用域的VO里面的,执行完不会被回收
2.回收机制
垃圾收集器(garbage collector,简称GC),后台进程负责监控、清理对象,并及时回收空闲内存。
1 简单描述
一般来说没有被引用的对象,就是垃圾,比如函数执行后,没有被外部引用,,那它内部的变量值就会清除,如果有被引用,不会清除,这个也是我们常说的闭包
2 回收方法
- 引用计数 (过时,简单说一说)
记录每一个值被引用的次数,被引用一次+1,去掉引用-1,当垃圾回收触发时,引用次数0的会被清掉 - 标记 (主流)
树形结构 - 从全局出发,标记值(对象)
- 再标记这些值有没有引用了其他的值,有就标记
- 最后删掉没有被引用,也就是没有标记的那些对象
PS:可能有其他说法是删除带标记的,不过它那种是全部标记,然后去掉正在引用的标记,剩下的那些都是不引用,所以删掉,不过这两种都是一个意思
3. 回归基础
3.1 Symbol到底是什么鬼
简单说一下就好,它是es6新增基础数据类型之一,typeof Symbol返回的是函数,要typeof Symbol() 返回的才是Symbol,作用的话比如你这个实例this的原型对象有一个say方法,但是你又想在this上面加一个say方法,不想冲突,就可以用这个
function Foo(name) {
this.name = name
}
Foo.prototype.say = function() {
return this.name
}
var f = new Foo('f')
var say = Symbol('say')
f[say] = function() {
return 'other name'
}
f.say() // 'f'
f[say]() // 'other name'
3.2 类型转换
【显示转】
转字符串
特别:[],对象
String(123); // "123"
String(true); // "true"
String(null); // "null"
String(undefined);// "undefined"
String([1,2,3]) // "1,2,3"
String({}); // "[object Object]"
转数字(可以直接在值前面加个+,比如+null == 0)
特别:[],对象,undefined
Number(undefined) //NAN
Number({}) //NAN
Number([999,1000]) //NAN //数组没值,或者只有一个就能转,多一个都不行
Number([999]) //999
Number([]) //0
转布尔(可以直接在值前面加两个!!,第一个!转成相反布尔,第二个!转回来,比如!{} false , !!{} true)
【隐示转】
加法
- 只要有一个是字符串,转字符串拼接
- 没有字符串,没有引用,转数字相加
- 没有字符串,有引用,转字符串拼接
除了加法,其他比如*,/都直接转number来执行
加法提个有意思的
1 + {} // '1[object Object]'
{} + 1 // 1
({} + 1) // '[object Object]1'
如果{}在前面,没有括号,会当做代码块执行,所以会变成 +1,如果有( ),就当做加法处理
3.3 JSON.parse(JSON.stringify(obj))实现深拷贝的弊端
这个不是万能的,如果对象里面有
new Date:会变成时间文字的字符串
RegExp:正则对象会丢失
function:函数也会丢失
3.4 JS的运行机制
首先,事件循环机制中,有任务的概念,
宏任务:script标签里面的,定时器那些
微任务:promise的then,最常见
当执行代码的时候,遇到不同的任务,就放到不同的任务队列(宏任务队列,微任务队列)
任务的优先度是微任务>宏任务,就是说微任务先执行,微任务没了,再执行宏任务,
然后是执行栈,执行栈是先进后出,队列是先进先出,先说明这个,
执行栈
function A(){
console.log('a')
B()
console.log('c')
}
function B(){
console.log('b')
}
A()
A() 执行,先进执行栈,开始运行,输出a
这时候遇到B(),B()也进入栈,开始运行,输出b
B结束,出栈,回到A,输出c,A结束,出栈
一开始是A先进,但是中途遇到B,反而是先处理完B,再处理A,这就是先进后出
队列就不说明了,就是哪个先插入队列,到时候就先跑哪个
举例:
<script>
console.log('1');
setTimeout(() => {
console.log('2')
}, 1000);
new Promise((resolve, reject) => {
console.log('3');
resolve();
console.log('4');
}).then(() => {
console.log('5');
});
console.log('6');// 1,3,4,6,5,2
</script>
- 执行script标签代码,进栈,运行,输出1
- 遇到setTimeout,塞到宏任务队列,继续走
- 遇到promise,输出3,4,然后then是微任务,塞到微任务队列,
- 输出6,出栈,
- 执行栈空了,微任务队列拿任务塞出来,跑
- 输出5,微任务没了,
- 完成一轮事件循环,页面进行一次渲染
- 开始下一轮,从宏任务拿队列
- 输出2,出栈,
- 队列空,执行栈空,程序执行结束
总结:
- 先一次宏任务,宏任务结束,清空微任务,就算一轮事件循环
- 一轮循环后页面渲染
- 然后再开始第二轮,始终是先宏后微
4. 常见算法
写一个new
function fa(name,age){ this.name = name this.age = age } function mynew(){ let arr = [...arguments] //类数组变成数组,可以用数组的方法,slice arr = arr.slice(1) //参数,第一个是函数 let obj = {} obj.__proto__ = arguments[0].prototype arguments[0].call(obj,...arr) //...扩张运算符,展开参数 return obj } var tem = mynew(fa,'名字',20) console.log('可以吗',tem);
写一个promise
class mypromise { constructor(fn){ let that = this // 因为第一层的resolve是外面调用的,所以里面最好用that保存this,不然丢失 this.state = 'pending' this.value = '' this.rescallback = [] this.rejcallback = [] this.resolve = function(value){ if(that.state == 'pending'){ //用that that.state = 'resolve' that.value = value that.rescallback.forEach((item,index) => {item() }) } } this.reject = function(value){ if(that.state == 'pending'){ //用that that.state = 'reject' that.value = value that.rejcallback.forEach((item,index) => {item() }) } } fn(this.resolve,this.reject) } then(fns,fnj){ let that = this if(this.state == 'resolve'){ return new mypromise(function(resolve,reject){ let nextlvaue = fns(that.value) //处理then方法 resolve(nextlvaue) // 无视上面那个then,整个看起来就是新的new promise用法 }) } if(this.state == 'reject'){ return new mypromise(function(resolve,reject){ let nextlvaue = fnj(that.value) resolve(nextlvaue) }) } if(this.state == 'pending'){ return new mypromise(function(resolve,reject){ that.rescallback.push(function(){ let nextlvaue = fns(that.value) resolve(nextlvaue) }) that.rejcallback.push(function(){ let nextlvaue = fnj(that.value) resolve(nextlvaue) }) }) } } } // new mypromise(function(res,rej){ setTimeout(() => { res('eoeo') },2000) }).then(res => { console.log('这是第一个res',res) return '啦啦啦' },rej => { console.log('我是拒绝',rej) return '啦' }).then(res => { console.log('第二个',res) })
写一个总线 //那个函数的问题
function mybus(){ this.handle = {} this.add = function(name,fn){ if(!this.handle[name]){ this.handle[name] = [] } fn.uid = new Date().getTime() this.handle[name].push(fn) } this.emit = function(name,par){ if(this.handle[name].length){ this.handle[name].forEach((item,index) => { item.call(this,par) // 这个this命名就是bus的 }) } } this.delone = function(name,fn){ this.handle[name].some((item,index) => { if(item.uid == fn.uid){ this.handle[name].splice(index,1) return true } }) } this.delall = function(name){ this.handle[name] = [] } } var bus = new mybus() new Vue({ el: '#app', data: { message: '信息', kaiguan: false }, methods:{ switch111: function(name){ // 必须箭头函数,拿vue的this // bus.add('mu',(name) => { // console.log('有吗',this.message) // console.log('这个是参数',name) // }) bus.add('mu',this.chufa) }, switch222(){ bus.emit('mu',{lala:'啦啦'}) }, chufa(name){ console.log('有吗',this.message) console.log('这个是参数',name) } } })
写一个防抖,节流
// 防抖 function fangdou(fn){ let timer = null return function(){ clearTimeout(timer) let that = this let arg = arguments timer = setTimeout(() => { fn.call(that,...arg) },2000) } } // 节流 function jieliu(fn){ let timer = null return function(){ if(timer) return let that = this let arg = arguments timer = setTimeout(() => { fn.call(that,...arg) timer = null },2000) } }
写一个快排
var tem = [1,4,3,2,14,61,7,9,8,120] function kuaipai(arr){ if(arr.length < 2){ return arr } let keyindex = Math.floor(arr.length/2) // 取中间值索引 let keynumber = arr.splice(keyindex,1)[0] //取中间值,顺便删掉 let left = [] let right = [] for(var i = 0; i<arr.length;i++){ if(arr[i]>keynumber){ right.push(arr[i]) }else{ left.push(arr[i]) } } return kuaipai(left).concat(keynumber,kuaipai(right)) } var tem2 = kuaipai(tem) console.log('看看',tem2);
深拷贝
var a = { name: 'nihao', number: 100, boolean: true, shuzu: [1,2,3], obj: { oname: '对象名字', }, fn: function(){ console.log('我是函数') }, date: new Date() } function deepClone(src){ // 字符串,数字,布尔 if(typeof src !== 'object'){ return src } // 数组 if(src instanceof Array){ let tem = [] for(let i = 0; i< src.length;i++){ tem[i] = deepClone(src[i]) } return tem } // 函数 if(src instanceof Function){ return src } // 时间 if(src instanceof Date){ return src } let tem = {} for(var k in src){ if(src.hasOwnProperty(k)){ tem[k] = deepClone(src[k]) } } return tem }
4. ES6易忘
reucde
[1,2,3].reduce(function(累加值,当前值,索引,正在循环的数组){},设置的初始值)
累加值:默认取你数组的第一个值,也就是1,但是如果你设置了【设置的初始值】,那就拿它,
每次循环return的值都会成为它下次的值,
当前值:不解释
索引:不解释
正在循环的数组: [1,2,3]
设置的初始值:你可以设0,[],{},随便
PS:如果你设置了【设置的初始化】,索引从0开始循环。
如果你没有设置,索引从1开始循环,也就是跳过了第一个
var total = [ 0, 1, 2, 3 ].reduce(( acc, cur, index ) => {
console.log(cur) //0
console.log(index) //0
return acc + cur //开始0,然后0+0=0,然后0+1=1,每次都是return新值
},0)
var total = [ 0, 1, 2, 3 ].reduce(( acc, cur, index ) => {
console.log(cur) //1
console.log(index) //1
return acc + cur //开始0,然后0+1=1,然后1+2=3,每次都是return新值
})
new Set(字符串,或者数组)
传字符串:一边去重一边排序
传数组:去重
new Set('123467643333'),返回的是Set(6) {"1", "2", "3", "4", "6", …},跟arguments一样的类数组对象,不能直接用数组的方法,要转换一下
[...new Set('123467643333')] => ["1", "2", "3", "4", "6", "7"]
new Set(['3','3','1','2','4','2'])
返回:Set(4) {"3", "1", "2", "4"}
转换 [...Set(4) {"3", "1", "2", "4"}] => ["3", "1", "2", "4"]
方法
.add() 添加值,自动判断是否重复,重复不加进去
has() 是否有某值,返布尔
性能篇章
http协议篇章
1.常见请求头字段
1.1 Connection:keep-alive 跟握手,接口请求有关
1.2 content-type:告诉服务器我是用什么方式编码的,你到时候要用什么方式解码
text/plain: 纯文本,传json格式的时候也可以用这个,但是这样的话后端需要自己判断用什么格式解码,为了保证规范,不能随便设
application/x-www-form-urlencoded:把参数用key=&key=
form-data:一般用传文件
application/json:把参数用{ 'name':'abc'},一般作用复杂的数据结构
1.3 referer:标明资源请求是来自哪个地址,比如经常有复制图片贴到自己的网站,结果出现图片无法访问,这个也是防盗链
1.4 cache-control:缓存,这个非常常见,常见的值有
max-age=xxx 在这段时间内有效
no-cache 不进行强缓存,协商缓存会走
no-store 不强缓存,也不协商缓存
2.常见接口状态码
- 200: 成功,ok
- 3xx:重定向,请求一个资源,接口内部通知浏览器发送一个新的请求,即请求了两次,而转发是只请求一次
- 304:(缓存相关,如果命中缓存,状态码是这个)
- 401:(token失效)
- 403: forbidden,表示对请求资源的访问被服务器拒绝
- 404: not found,表示在服务器上没有找到请求的资源
- 5xx: 服务器的问题
3.跨域
2.1 JSONP
原理:后端触发前端传过去的函数,前端的那个函数会触发,不细说
2.2 CORS
原理:后端设置响应头,response headers,设置Access-Control-Allow-Origin,表示允许哪些域名可以访问,如果设置星号,则表示允许所有,
后端设置完之后,你会发现在network的请求里面,请求头会多出几个字段,比如origin,这些是浏览器自动加的
res.header('Access-Control-Allow-Origin', 'https://xxxxx.com')
res.header('Access-Control-Allow-Origin', '*')
用cors跨域会把请求分为两种,简单请求和复杂请求
2.2.1 区分:
满足以下条件的请求即为简单请求,否则就是复杂请求:
- 请求方法:GET、POST、HEAD
- 除了以下的请求头字段之外,没有自定义的请求头
Accept
Accept-Language
Content-Language
Content-Type
DPR
Downlink
Save-Data
Viewport-Width
Width - Content-Type的值只有以下三种(Content-Type一般是指在post请求中,get请求中设置没有实际意义)
text/plain
multipart/form-data
application/x-www-form-urlencoded
2.2.2 实现:
针对简单请求,后端在进行CORS设置的时候,只需要设置响应头
// 通配
res.header('Access-Control-Allow-Origin', '*')
// 如果只是针对某一个请求源进行设置的话,可以设置为具体的值
res.header('Access-Control-Allow-Origin', 'http://www.yourwebsite.com')
浏览器发现这次跨域请求是简单请求,自动在头信息之中,添加一个Origin字段;
Origin字段用来说明请求来自哪个源(协议+域名+端口号)。服务端根据这个值,决定是否同意本次请求。
如果请求方法为PUT或DELETE,或者Content-Type字段为application/json,那这种就是复杂请求;
复杂请求需要设置至少三个
设置三个的原因是,复杂请求底层的判断逻辑,就是先判断ogrigin,再判断methods,最后再判断headers,后面再走其他判断,所以必须设置这三个
res.header('Access-Control-Allow-Origin', 'http://www.yourwebsite.com')
// 允许哪些请求跨域,设置后会发现预检请求的请求头会有Access-Control-Request-Method字段
res.header('Access-Control-Allow-Methods', 'POST, GET')
// 需要读取哪些额外的字段,设置后会发现预检请求的请求头会有Access-Control-Request-Headers字段
res.header('Access-Control-Allow-Headers', 'X-PINGOTHER, Content-Type')
复杂请求会发送两次请求,第一次叫预检请求,第二次才是真正请求
- 预检请求
方法:OPTIONS
在请求头那里,除了Origin字段,预检请求的头信息多了两个特殊字段: - Access-Control-Request-Method: (必有),用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是POST(个人理解:你请求的时候写的那个method)
- Access-Control-Request-Headers:(这个不一定有,看浏览器,谷歌有,火狐没有这个),该字段是一个用逗号分割的字符串,执行浏览器CORS请求会额外发送的头信息字段,上例是Content-Type;(根据xml里面cors.supportedHeaders配置的进行比对,不是很懂)
如果预检通过,则发送真正的请求,这个表现跟简单请求一样
如果不通过,就会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段,这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获(个人理解:options通过,真正的请求报错)
2.2.3 补充:
设置max age,浏览器端会进行缓存。没有过期之前针对同一个复杂请求只会发送一次预检请求,即每个请求第一次会发送,后面就不会
res.header('Access-Control-Max-Age', '86400')
如果需要发送cookie,响应头需要设置,客户端也要(这个不清楚),而且这个时候,Access-Control-Allow-Origin不能设置*,要指定域名
res.header('Access-Control-Allow-Origin', 'http://www.yourwebsite.com') res.header('Access-Control-Allow-Credentials', 'true')
- 预检请求可以单独过滤掉,不做业务验证,只验证能不能发送请求,业务验证在第二次真正的请求里面处理
4.缓存
先说几个要点:
- 缓存分强缓存,协商缓存;
- 默认是不缓存,是服务端设置响应头cache-control开启缓存,一旦开启,强缓存和协商缓存同时生效;
- 先走强缓存,后走协商缓存,就是说先判断强缓存,命中就结束,然后没命中,再判断协商缓存;
- 请求头也可以设置cache-control,但是只能'no-store' | 'no-cache' | 'max-age=0'才会生效,比如响应头设置了缓存时间,请求头设置了不要缓存,那最终表现是不要缓存,如果请求头设置了要缓存,那最终表现是按后端设置的响应头为准
4.1 强缓存
补充1点,http1.0是设置Expires,Expires一般对应服务器端时间
Max-Age相比Expires?
Expires使用的是服务器端的时间
但是有时候会有这样一种情况-客户端时间和服务端不同步
那这样,可能就会出问题了,造成了浏览器本地的缓存无用或者一直无法过期
所以一般http1.1后不推荐使用Expires
而Max-Age使用的是客户端本地时间的计算,因此不会有这个问题
因此推荐使用Max-Age。
注意,如果同时启用了Cache-Control与Expires,Cache-Control优先级高。
比如某张图片,后端设置了响应头
// max-age单位是秒,下面是60s
res.header('cache-control', 'max-age=60')
开始请求
- 用户请求一张图片
浏览器查找缓存表,
- 没有,请求服务器,服务器返回资源(状态码200),把它存到缓存表 (走接口)
有,再读max-age有没有过期
- 没有过期,拿缓存返回(状态码200)(没走接口)
- 过期了,请求服务器,服务器返回资源(状态码200),把它存到缓存表(走接口)
强缓存是通过缓存表判断有没有过期,是否拿缓存表,所以存在可能走接口,也可能不走接口
4.2 协商缓存
当强缓存失效,(设置了no-cache或者max-age过期了)就会走这个
当看到network的响应头有ETag,Last-Modified,说明协商缓存正在生效
也是设置响应头开启
res.header('etag', '5c20abbd-e2e8')
res.header('last-modified', 'Mon, 24 Dec 2018 09:49:49 GMT') (这个是http1.0的)
ETag:每个文件有一个,改动文件了就变了,可以看似md5
Last-Modified:文件的修改时间
同时,在请求时在 request header 就把这两个带上
但是名字变了
ETag-->If-None-Match,
Last-Modified-->If-Modified-Since ,(这个是http1.0的难怪我测试没测出来)
服务端把你带过来的标识,资源目前的标识,进行对比,然后判断资源是否更改了
开始请求(我测试的时候没看到Last-Modified字段,所以下面就不写了)
- 用户请求一张图片
浏览器查找缓存表,
- 没有(找不到etg),请求服务器,服务器返回资源(状态码200),把它存到缓存表 (走接口)
有,把ETag这个key换个名字If-None-Match,请求服务器,判断有没有过期 (走接口)
- 没有过期,去缓存表拿吧(状态码304)
- 过期了,返回资源(状态码200),顺便把新的etg给你,把它存到缓存表
协商缓存是通过接口判断有没有过期,是否拿缓存表,所以必定走接口
4.3 流程
注意强缓存那里,如果同时有Cache-Control和Expires,优先处理Cache-Control
注意协商那里,如果同时有etag和last--mod,优先处理etg
4.4 跟开发相关的
- 行为
- vue项目
webpack打包的时候将更改的文件做 hash 处理了,一般是34234234234ouorerfdf232342.js 这种随机的字符串,因为名字不同了,所以请求的时候绝对不会走缓存,不用担心拿到旧文件;
但是index.html,我们需要在 nginx 上做 no-store 处理,即完全不缓存 index.html,每次都请求最新的html。。。因为 html 中会外链 css、js,如果我 html 还是走的缓存,那链接的还是老的 css ,js,
或者也可以手动加一个时间戳的参数,保证文件不一样,比如index.html?time=new Date().getTime() - cdn
个人理解,cdn只是代替了你的原始服务器,优点就是它的响应更快,然后强缓存和协商缓存还是依旧有执行判断的
一般图片的cdn,放到腾讯云的cdn存储桶里面,
然后一般的请求,是后端把数据放到对应cdn的数据库,然后url也是cdn的url,不存在中间经过后端转发之类
5.get和post的区别
说几个关键点就好,
- get参数带在url后面,不能传太长的参数,会被浏览器限制切断;
- get是可以走缓存,post不行;
- get请求只能进行url编码,而POST支持多种编码方式,设置content-type;
- get参数暴露在url,敏感信息还是得用post请求,post放在Request body中;
6.udp和tcp的区别
内容太多,看不懂,只说几个好了
- UDP 协议是面向无连接的,跟TCP对比,它不需要三次握手,更加的快,但是网络不好会丢包,无法保证传输;
- UDP数据包小,实时性高,对实时性要求高的,比如直播,电话会议,会用到;
- TCP建立连接需要三次握手,断开连接需要四次挥手
7.浏览输入url发生了什么,简单版
1 首先解析域名,先看看浏览缓存有没有,没有的话在本机的host文件找域名对应的ip地址,再没找到去DNS服务器解析(这里可以优化,dns解析比较慢),得出ip地址
2 然后开始tcp请求链接,三次数据包传输,浏览器对同一域名下并发的tcp连接是有限制的(2-10个不等),这里可以优化
三次握手,简化
A=B,B=A,A=B
客户端发送一个syn报文给服务端,
服务端收到,发送syn+ack报文给客户端,(前两步说明服务端的接受,发送没问题)
客户端收到,发送ack报文给服务端 (最后一步说明客户端的发送,接受没问题)
四次挥手,简化
A=B,B=A,B=A,A=B
客户端发送一个FIN报文给服务端,
服务端收到,发送ack报文给客户端,
(为什么客户端收到后不关闭,因为可能还有服务端没处理完的请求,要服务端主动发送FIN才说明业务处理完了)
服务端发送FIN报文给客户端,
客户端收到,发送ack报文给服务端 ,
服务端关闭,客户端等待一段时间后,也关闭
2.1 上面两个步骤可以总结
- 应用层(dns,http) DNS解析成IP并发送http请求
- 传输层(tcp,udp) 建立tcp连接(三次握手)
- 网络层(IP,ARP) IP寻址
- 数据链路层(PPP) 封装成帧
- 物理层(利用物理介质传输比特流) 物理传输(然后传输的时候通过双绞线,电磁波等各种介质)
3 开始请求资源,走强缓存和协商缓存的判断(说点相关的优化)
优化:
1.cookie是由服务端写入,一般用于登录状态的sessionkey,但是一些静态资源的引入不需要这个,可以把静态资源的域名换成别的域名,这样子请求的时候就不会自动携带
2.换成别的域名后,解析域名也需要时间,可以用dns-prefetch(让浏览器空闲时提前解析dns域名,不过也请合理使用,勿滥用)
3.http1.0使用的是短连接,请求一次就关闭通道,http1.1.使用的是长连接,Connection: keep-alive,请求一次之后,后面的请求都不用重新握手,有持续时间
4.https需要交费,请求前SSL/TLS握手建立连接,传输过程数据加密,
4 解析页面流程
4.1. 接口返回html代码,有可能是压缩,如果是压缩则先解压,然后开始解析,从上往下
4.2. 当遇到script标签,(这里包括加载和执行代码,内联只有执行)会阻塞页面构建dom树,阻塞了dom树构建自然而然也阻塞了页面的渲染,也就是我们常说的页面空白,下面说几个点
- 内联script,经过本地测试,无论放在哪个位置,就算放在最后面都会阻塞,所以针对内联,一般我们说放在最后面是考虑这个js有操作dom的语句,不然你放在上面的话会报错,因为dom都没渲染你拿不到
- 外部script,外部js的加载和执行会阻塞,经过本地测试,如果位置是插到节点的中间,那么前面的会先渲染,之后再执行js的内容,如果是插到最后面,则完全不会阻塞,这里看到别人文章的说法,如果在外联script标签之前已经有DOM元素生成,则浏览器会优先渲染一次。可能因为浏览器不知道脚本的内容,因而碰到脚本时,只好先渲染页面,确保脚本能获取到最新的DOM元素信息,尽管脚本可能不需要这些信息
- defer和async,这两个针对外部js,内联是没用的,defer是加载不阻塞,执行会等页面渲染后再跑,也就是说defer优化了加载时间,async也是加载不阻塞,但是执行不会等页面渲染后再跑,它是只要加载好了立马跑,注意执行是一定会阻塞的,这个优化不了
4.3 当遇到style标签,(这里包括加载和解析,内联只有解析),link加载是异步,不会阻塞dom构建,但是解析会阻塞渲染,因为渲染是要等dom树和css树一起合并render树才会渲染,所以style太多的时候也会出现页面空白
4.4 当遇到图片资源,全程异步下载,等下载好了自动替换标签上面的src
4.5 经过了上面,现在dom树和css树都已经解析好了,两个合并生成render树,开始走渲染流程
- layout:布局,定位坐标和大小
- Painting:绘制,layout之后,根据每个节点的css属性,画到节点上面
回流:js改变了节点的大小,位置,内容,窗口resize,或者js获取dom的某些属性,比如offset,width,会触发dom树更新(非常耗时) =》render树更新 =》layout =》paint,所以一般要避免,如何避免?
- 一次性操作样式,比如替换一个class,不要一样一样改
- 复杂元素脱离文档流,避免大规模影响子节点和兄弟节点
- 重绘:js只是改变了外观,比如背景色,visibility,触发 css树更新 =》render树更新 =》paint,少了dom树和lay!
项目篇章
1.canvas
记录我用过的api,和某些常见的问题,模糊,省略号,跨域
canvas的width,和style.width区别
width:是画布的宽高,默认300x150,如果没有设置style,会自动同步设置style
style.width:是样式的,并不会同步设置width,如果两个不一致,画布会拉伸变型
为什么模糊
模糊的根本原因不是上面那个,原因可以这样说
跟iphone的dpr类似,像素点被放大,所以可以获取dpr,把画布放大相同倍数,然后绘制的时候也要放大相同倍数,跟图片有2倍图,在一倍的css宽高显示一样
width,height放大倍数后,绘制的时候记得也要放大,为什么
因为如果你不放大,你把画布放大了,到时候缩小的时候,你的元素会很小啊,本来就要两个一起放大的
省略号
需要用ctx.measureText,一点一点计算文本的长度,当超过指定宽度,就把最后一个文本换成...
跨域
跨域是调用posterCanvas.toDataURL('image/png'),生成base64图片的时候报的错,需要服务端把图片换同域,或者把图片转成base64本地来画,这样就不会有跨域问题,
ctx.drawImage(posterBgDom, 0, 0, width ratio, height ratio),传入的是img标签的dom,并且需要确保img加载完了再调用,所以一般会用new Image(),监听onload事件
2.websocket
h5版本的,顺序是这样
(1)this.socket = new WebSocket(URL) //初始化
(2)this.socket.onopen //是否建立连接
(2.1)this.socket.onmessage //开始监听发送
(2.2)this.getlist() //这里请求历史聊天记录
(3)this.socket.send //发送
(4)this.websocket.close //关闭,在销毁钩子
初始化,并且监听onopen事件,在回调里面监听onmessage
const URL = `${_.websocketPort}/chat-Refresh-websocket/web/${this.searchQuery.id}/${window._.userId}` this.websocket = new WebSocket(URL) this.websocket.onopen = () => { // socket创建成功开始通信,这里会收到send的数据 this.websocket.onmessage = res => { } // socket创建成功才开始请求历史记录 this.initChatList() }
发信息,send,触发onmessage
this.websocket.send(JSON.stringify(data))
在vue销毁钩子,调用close关闭
this.websocket.close()
小程序的,顺序是这样
(1)self.SocketTask = wx.connectSocket({url:'xxxxxx'}) //初始化
(2)self.SocketTask.onOpen //是否建立连接
(2.1)this.startHeartBeat //开始发送心跳
(2.2)this.getlist() //这里请求历史聊天记录
(3)self.SocketTask.onMessage //监听发送self.SocketTask.onClose
(4)self.SocketTask.onClose //监听关闭
(5)self.SocketTask.send //发送
(6)self.SocketTask.close //关闭,在销毁钩子
初始化,监听onopen事件,监听onMessage事件,监听onclose事件,这里跟h5不同,它没有放到回调里面,而是跟初始化时同一个时间点
self.SocketTask = wx.connectSocket({ url: options.url, success: function(res) { if (options) { options.success && options.success(res); } }, fail: function(res) { if (options) { options.fail && options.fail(res); } }, complete: function(res) { if (options) { options.complete && options.complete(res); } } }) // 是否建立链接 self.SocketTask.onOpen(res => { // 开始心跳 self.startHeartBeat(); // socket创建成功才开始请求历史记录 this.initChatList() }) // 监听发送 self.SocketTask.onMessage(res => { let data = JSON.parse(res.data); //心跳重连的不做响应 if (data.data == "heart_beat") return; }) // 监听关闭 self.SocketTask.onClose(res => { // 重新连接 self.connectSocket(self.options); })
发信息,send,触发onmessage
self.SocketTask.send({ data: options.data, success: function(res) { if (options) { options.success && options.success(res); } }, fail: function(res) { if (options) { options.fail && options.fail(res); } }, complete: function(res) { if (options) { // 无论成功与否都执行的回调 options.complete && options.complete(res); } } })
在销毁钩子,调用close关闭
self.SocketTask.close({ success: function(res) { if (options) { options.success && options.success(res); } }, fail: function(res) { if (options) { options.fail && options.fail(res); } }, complete: function(res) { if (options) { options.complete && options.complete(res); } } })
说一下心跳包,在onopen里面调用,为了保证通话是否正常,会一直send一个数据,跟后端约定好类型,然后onmessage的时候过滤掉这个类型的数据
self.SocketTask.send({ data: options.data, success: function(res) { // 延时器,一直调用自身,一直发 self.heartBeatTimeOut = setTimeout(() => { self.heartBeat(); }, 5000); }, fail: function(res) { // 一旦失败,关闭socket,触发onclose,重新连接 self.closeSocket(); }, complete: function(res) { } })
3.图片上传
h5版本
都是传base64格式的图片给接口,然后接口会返回带http的图片给回你
你可以拿base64图片本地回显,又或者拿接口的
axios.post(_.baseURL + imageUploadPathPC, {
imgFileBase: url,//base64地址
})
如何转base64?
第一种,new FileReader()
<div id="app">
<input type="file" name="" @change="xuanze" ref="wenjian">
<img :src="imgUrl" alt="">
</div>
new Vue({
el: '#app',
data: {
imgUrl: ''
},
methods: {
xuanze(event){
console.log('看看',event.target.files)
let file = event.target.files
var reader = new FileReader();
reader.readAsDataURL(files[0]);
reader.onload = () => {
this.imgUrl = reader.result; //这个就是base64
}
}
}
})
第二种,用URL.createObjectURL,并且用canvas生成base64
<div id="app">
<input type="file" name="" @change="xuanze" ref="wenjian">
<img :src="imgUrl" alt="">
</div>
new Vue({
el: '#app',
data: {
imgUrl: ''
},
methods: {
xuanze(event){
console.log('看看',event.target.files)
let vuethis = this
let img = new Image()
img.src = URL.createObjectURL(event.target.files[0]) //blob对象,字符串,这里也可以用来回显
img.onload = function () {
let that = this
let quality = 1
let maxWidth = 2560
let w = that.width; let h = that.height; let scale = w / maxWidth // 生成比例
if (w > maxWidth) { // 如果图片大于最大宽度
quality = 0.8 // 压缩图片质量0-1,值越大质量越好
w = maxWidth
h = h / scale
}
// 生成canvas
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
canvas.width = w
canvas.height = h
ctx.drawImage(that, 0, 0, w, h)
let base64 = canvas.toDataURL('image/jpeg', quality)
vuethis.imgUrl = base64
console.log('看看',base64)
}
}
}
})
小程序版本
先wx.chooseImage选择手机图片,拿到临时路径,然后直接用wx.uploadFile传给接口,
可以拿临时路径回显,也可以拿接口的
wx.chooseImage({
count: 9 - length,
sizeType: ['original'],
sourceType: ['album', 'camera'],
success: res => {
const src = res.tempFilePaths
src.forEach((item, index) => {
wx.showLoading({
title: '上传图片中...',
mask: true
});
wx.uploadFile({
url: constants.BASE_IMGUPLOAD_API,
filePath: item,
name: 'file',
formData: {
userId: wx.getStorageSync('imgUploadId')
},
success: res => {
wx.hideLoading()
let data = JSON.parse(res.data)
if (data.code === constants.OK_CODE) {
let img = constants.BASE_URL_IMG + data.data.urlString;
this.setData({
['userInfo.albumsList']: this.data.userInfo.albumsList.concat(img)
});
}
}
});
});
}
});
4.文件上传
h5版本
代码
找不到代码,看以前代码是直接传file对象,不像图片要转一层base64
下载方式
第一种,用a标签的download属性,属性href设置url,属性download设置文件名,然后点击一下就可以这个可以用在pc和安卓机
如果是批量的话,就是循环创建而已,不会出现弹窗确认
let title = file.fileName + '.' + file.fileType
var linkDom = document.createElement('a')
linkDom.setAttribute('href', file.fileUrl)
linkDom.setAttribute('download', title)
document.body.appendChild(linkDom)
setTimeout(() => {
linkDom.click()
document.body.removeChild(linkDom)
}, 60)
第二种,用iframe,给苹果用
如果是批量的话,每一个文件会出现一次弹窗确认好像是
var src = file.fileUrl;
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = "javascript: '<script>location.href=\"" + src + "\"<\/script>'";
document.getElementsByTagName('body')[0].appendChild(iframe);
小程序
没整过
5.画图
h5版本
我当时用的是HighCharts,所以记录下,一般是先定义个dom,给这个dom一个配置变量,就是一个对象各种配置,最后在js里面初始化
第一种,圆饼图
<div
id="ringChart"
class="ring-chart"
:option="ringOption"
/>
drawRing (option) {
// 对我感兴趣
const a = option[0].value
// 对产品感兴趣
const b = option[1].value
// 对官网感兴趣
const c = option[2].value
const total = a + b + c
const percentA = total ? Math.floor(a / total * 100) : 0
const percentB = total ? Math.floor(b / total * 100) : 0
const percentC = total ? Math.floor(c / total * 100) : 0
this.ringOption = {
credits: { // 去掉右下角logo
enabled: false
},
chart: {
type: 'pie',
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false
},
title: {
text: ''
},
tooltip: {
enabled: false// 关闭鼠标滑过提示
},
plotOptions: {
pie: {
allowPointSelect: false, // 是否可以点击
dataLabels: {
enabled: true// 每个饼图的延伸出来的名字
},
showInLegend: true, // 饼图的分类下标
animation: {
duration: 500// 动画渲染效果
},
states: {// 关掉hover效果
hover: {
enabled: false
}
}
}
},
series: [{
size: '100%',
innerSize: '50%',
data: [
{
y: percentA,
name: '对我感兴趣',
color: '#5585F0',
dataLabels: {
format: a + '次'
}
},
{
y: percentB,
name: '对产品感兴趣',
color: '#03D890',
dataLabels: {
format: b + '次'
}
},
{
y: percentC,
name: '对官网感兴趣',
color: '#FFCF63',
dataLabels: {
format: c + '次'
}
}
]
}]
}
// 过滤0的数值
let newData = this.ringOption.series[0].data.filter((val, index) => {
return val.y != 0
})
this.ringOption.series[0].data = newData
if (!newData.length) {
this.nodataFlag = true
} else {
this.nodataFlag = false
}
if (!this.ringChart) {
// 初始化
this.ringChart = HighCharts.chart('ringChart', this.ringOption)
} else {
// 这里的更新如果带上size和innerSize不知道为什么就失去动画效果
this.ringChart.update({
series: [{
data: this.ringOption.series[0].data
}]
})
}
},
第二种,折线图
<div
id="lineChart"
class="line-chart"
:option="lineOption"
/>
drawLine (option) {
const dateArray = option.dateArray
const visitNumber = option.visitNumber
this.lineOption = {
credits: { // 去掉右下角logo
enabled: false
},
chart: {
type: 'line'
},
title: {
text: ''
},
xAxis: {
categories: dateArray,
minTickInterval: 2,
crosshair: {
width: 1,
color: '#7FACEB'
}
},
yAxis: {
title: {
text: ''
}
},
plotOptions: {
line: {
showInLegend: false, // 饼图的分类下标
allowPointSelect: false
},
series: {
marker: {
enabled: false
}
}
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
borderColor: 'rgba(0,0,0,0)',
shadow: false,
useHTML: true,
headerFormat: '<p style="color:#fff">{point.key}</p>',
pointFormat: '<p style="color:#fff">{point.y}次 关注</p>'
},
series: [{
data: []
}]
}
visitNumber.forEach((val, index) => {
this.lineOption.series[0].data.push({
y: val,
name: this.showData[index].date
})
})
// 绘制图表
if (!this.lineChart) {
// 初始化
this.lineChart = HighCharts.chart('lineChart', this.lineOption)
} else {
// 更新
this.lineChart.update({
xAxis: this.lineOption.xAxis,
series: this.lineOption.series
})
}
}
小程序版本
用的是ec-canvas,是echarts的小程序版本
第一种,圆饼图
canvas-id不知道干嘛的,随便填一个
id="mychart-dom-bar" 这个给js用
ec是一些配置,不是配置信息,这个不要搞错
<ec-canvas class="ring-chart" id="mychart-dom-bar" canvas-id="mychart-bar" ec="{{ ec }}" style></ec-canvas>
data: {
ec: {
lazyLoad: true
},
maskFlag: false,
finishFlag: true,
nodataFlag: false,
tipFlag: false
},
initChart() {
if (this.data.option.length) {
this.echartsComponent = this.selectComponent('#mychart-dom-bar');
this.echartsComponent.init((canvas, width, height) => {
RingChart = echarts.init(canvas, null, {
width: width,
height: height
});
RingChart.setOption(this.getBarOption(this.data.option[0].value, this.data.option[1].value, this.data.option[2].value));
// 安卓手机在加载绘画过程中,如果点击了图片,会出现圆环图中心变色问题,所以在加载完成之前用一个透明遮罩盖住防止用户点击
RingChart.on('finished',() => {
if(this.data.finishFlag){
this.setData({
maskFlag: true
});
}
this.setData({
finishFlag: true
});
});
return RingChart;
});
} else {
setTimeout(() => {
this.initChart();
}, 200);
}
},
getBarOption(a, b, c) {
const total = a + b + c;
const percentA = total? Math.floor(a / total * 100) : 0;
const percentB = total? Math.floor(b / total * 100) : 0;
const percentC = total? Math.floor(c / total * 100) : 0;
let config = {
series: [{
type: 'pie',
radius: ['40%', '75%'],
center: ['50%', '50%'],
// 关闭圆环触发交互
silent: true,
data: [{
value: percentB,
name: b + '次',
itemStyle: {
color: '#03D890',
},
label: {
color: '#0A1735'
},
labelLine: {
show: true,
smooth: 0,
length: 5,
length2: 30
}
},
{
value: percentC,
name: c + '次',
itemStyle: {
color: '#FFCF63'
},
label: {
color: '#0A1735'
},
labelLine: {
show: true,
smooth: 0,
length: 5,
length2: 30
}
},
{
value: percentA,
name: a + '次',
itemStyle: {
color: '#5585F0'
},
label: {
color: '#0A1735'
},
labelLine: { //指示线状态
show: true,
smooth: 0,
length: 5,
length2: 30
}
}
],
}]
};
//过滤0的数值
let newData = config.series[0].data.filter((val,index) => {
return val.value != 0;
});
config.series[0].data = newData;
if(!newData.length){
this.setData({
nodataFlag: true,
maskFlag: true
});
}else{
this.setData({
tipFlag: true,
nodataFlag: false
});
}
return config;
},
第二种,折线图
<ec-canvas id="mychart-dom-bar" canvas-id="mychart-bar" ec="{{ ec }}"></ec-canvas>
initChart() {
if (!this.data.upFlag) return;
this.echartsComponent = this.selectComponent('#mychart-dom-bar');
this.echartsComponent.init((canvas, width, height) => {
LineChart = echarts.init(canvas, null, {
width: width,
height: height
});
LineChart.setOption(this.getBarOption(this.data.option.dateArray, this.data.option.visitNumber));
return LineChart;
});
},
getBarOption(dateArray, visitNumber) {
return {
// 左右两边缩的时候,横坐标也会自动适应隐藏个别的显示
grid: {
left: '10%',
right: '5%',
top: '5%'
},
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
color: '#085ABF'
}
},
position: function (pos, params, dom, rect, size) {
if (+(pos[0] < size.viewSize[0] / 2)) {
return {
left: pos[0],
top: pos[1] - 80
};
} else {
return {
right: size.viewSize[0] - pos[0],
top: pos[1] - 80
};
}
},
padding: [5, 10],
textStyle: {
color: '#fff'
},
extraCssText: 'background: rgba(144,144,144,0.5)',
formatter: (datas) => {
return this.data.showData[datas[0].dataIndex].date + '\n' + datas[0].data + '次' + ' ' + datas[0].seriesName;
}
},
dataZoom: [{
type: 'slider',
xAxisIndex: 0,
height: '0',
show: false,
start: 0,
end: 100,
// 手柄的样式
handleStyle: {
width: '20',
color: "#085ABF",
borderColor: "#085ABF"
},
// 背景
backgroundColor: "#f7f7f7",
// 数据背景
dataBackground: {
lineStyle: {
color: "#dfdfdf"
},
areaStyle: {
color: "#dfdfdf"
}
},
// 被start和end遮住的背景
fillerColor: "#FFEFBE",
// 拖动时两端的文字提示
labelFormatter: function (value, params) {
var str = "";
if (params.length > 4) {
str = params.substring(0, 4) + "…";
} else {
str = params;
}
return str;
}
}],
xAxis: {
type: 'category',
// name:'日期',
data: dateArray,
boundaryGap: false,
position: 'end',
offset: 5,
nameLocation: 'end',
nameGap: 5,
// 竖直线
splitLine: {
show: false,
interval: 'auto',
lineStyle: {
color: ['#EEEEEE']
}
},
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#85868F'
}
},
axisLabel: {
margin: 0,
textStyle: {
color: '#85868F',
fontSize: 13
}
}
},
yAxis: {
type: 'value',
// name:'浏览+转发(次)',
splitLine: {
lineStyle: {
color: ['#D8DADD']
}
},
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#85868F'
}
},
axisLabel: {
margin: 5,
textStyle: {
color: '#85868F',
fontSize: 13
}
}
},
series: [{
name: '关注',
type: 'line',
smooth: false,
showSymbol: false,
symbol: 'circle',
symbolSize: 1,
data: visitNumber,
areaStyle: {
normal: {
color: '#EEF2FD'
}
},
itemStyle: {
normal: {
color: 'rgba(16, 79, 193,1)'
}
},
lineStyle: {
normal: {
width: 0
}
}
},
{
name: '折线',
type: 'line',
symbol: 'circle',
symbolSize: 0,
data: visitNumber,
itemStyle: {
normal: {
color: '#5585F0'
}
},
lineStyle: {
normal: {
color: '#5585F0',
}
}
}
]
}
},
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。