SegmentFault 菜鸟杨@最新的文章
2023-06-13T23:13:00+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
早已忘却的面试题,需要在隆冬忆起
https://segmentfault.com/a/1190000043897623
2023-06-13T23:13:00+08:00
2023-06-13T23:13:00+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p><strong><em>哇哇哇哇。。。一边擦泪,一边誊写,早已忘却的面试题,需要在隆冬忆起。。</em></strong></p><h2>vue</h2><h4>vue自定义指令</h4><ul><li>通过自定义指令,我们可以扩展Vue的行为,让它在渲染DOM元素时添加额外的特性和事件,从而更好地完成业务需求</li><li>vue自定义指令分为两种类型:全局指令和局部指令(组件内指令)</li><li>全局指令会注册到Vue.directive上,可以全局使用,局部指令则只能在组件内使用</li></ul><p><strong>下面是一个全局自定义指令的示例:注册一个名为v-focus的全局自定义指令</strong><br><em>注册了一个名为v-focus的全局自定义指令,并实现了inserted钩子函数,当该指令所绑定的元素插入到DOM中时,该钩子函数会被调用,从而实现元素的聚焦功能</em></p><pre><code>Vue.directive("focus",{
// 当绑定元素插入到DOM中执行
inserted:function(el){
//聚焦元素
el.focus()
}
})</code></pre><p><strong>下面是一个局部自定义指令的示例:在组件内定义一个名为v-highlight的局部自定义指令</strong></p><pre><code>export default {
derectives:{
highlight:{
//当绑定元素插入到DOM中时执行
inserted:function(el){
//添加样式类
el.classList.add("highlight")
},
//当绑定元素从DOM中移出时执行
unbind:function(el){
//移除样式类
el.classList.remove("highlight")
}
}
}
}</code></pre><h4>Vue 中hash路由与history路由的区别</h4><pre><code>hash模式
在hash模式下,路由路径会带有一个#符号
hash模式的路由通过监听 window.location.hash的变化来进行路由切换。
hash模式的好处是兼容性较好,可以在不支持HTML5 History API的浏览器中正常运行
缺点是URL中带有#符号,不够美观
History模式:
在history模式下,路由路径不带有#符号
history模式利用HTML5 HitoryAPI中的pushState和replaceState方法来实现路由切换
history模式的好处是URL更加美观,没有#符号
缺点是兼容性较差,需要在服务器端进行配置,以防止在刷新页面时出现404错误</code></pre><h5>自定义指令在权限控制方面的应用</h5><ol><li>可以根据用户的角色信息来控制某些按钮或者表格中的行列是否可见,可编辑,可删除,<br>这个时候就可以通过自定义指令来实现这样的权限控制.</li><li>定义一个名为v-permission的全局自定义指令,并实现了bind钩子函数,在该函数中<br>通过当前用户的角色信息,判断该用户是否具有该元素的权限,如果没有,则将该元素隐藏<br><strong>下面是一个示例代码</strong>:</li></ol><pre><code>//定义一个名为v-permission的全局自定义指令
Vue.directive("permission",{
//bind 钩子函数只在指令第一次绑定到元素时执行一次
//如果需要对指令的绑定值进行响应式的操作,应该在update钩子函数中进行
bind:function(el,binding,vnode){
//获取当前登录用户的角色信息
const currentUser = getUserInfoFromLocalStorage().role;
//获取绑定的值
const {value} = binding
//判断当前用户是否有该按钮的权限
if(value&&value.length&&!value.includes(currentUser)){
el.style.display = "none"; //隐藏该元素
}
}
})
<button v-permission="['admin','superAdmin']">Delete</button>
</code></pre><h4>Vue的动态路由</h4><pre><code>Vue中的动态路由是指在路由中使用参数来匹配路径的一种方式,通过动态路由,我们可以轻松实现页面参数传递和多个类似页面的复用
{
path:'/user/:id',
name:'user',
component:User
}
:id表示该路由是一个动态路由,所以其被称为参数,它的值会被传递给User组件进行处理</code></pre><h4>Vue的key的作用</h4><p>key是用来唯一标识一个节点的属性。当Vue渲染Dom时,它会根据节点的key来判断是否需要重新渲染<br>当Vue发现节点的key发生变化时。它会将该节点从DOM树中移出,然后重新创建一个新的节点插入到合适的位置,这样可以减少DOM操作次数,提高渲染性能。</p><h4>Router路由守卫</h4><p>Router中的一项重要功能,它允许开发者在导航到某个路由或离开当前路由时执行一些控制和验证逻辑。Vue Router提供了全局的、路由级别的和组件级别的三种不同类型的路由守卫,包括:</p><ol><li>全局前置守卫beforeEach用于验证用户是否登录等全局控制</li><li>全局解析守卫beforeResolve用于在全局前置守卫之后在组件渲染之前被调用</li><li>全局后置钩子afterEach用于在路由完成后进行清理</li><li>路由独享的守卫beforeEnter用于在特定路由进入之前进行验证</li><li><p>组件内部的守卫beforeRouteEnter、beforeRouteUpdate和beforeRouteLeave用于处理页面内部控制逻辑</p><h2>javascript</h2><h4>Event Loop</h4><p>事件循环(Event Loop)是一种用于处理异步任务的机制,它是js运行时的一部分,用于管理和调度任务的执行顺序。<br>在js中,任务可以分为两种类型:<br>1.同步任务:按照代码的顺序依次执行,直到执行完成<br>2.异步任务:不会立即执行,而是在将来的某个时间执行。异步任务通常涉及网络请求、定时器、事件监听等。<br>事件循环的工作原理如下:</p></li></ol><pre><code>1 执行同步任务,直到遇到第一个异步任务。
2 将异步任务放入相应的任务队列(如宏任务队列,微任务队列)中。
3 继续执行后续的同步任务,直到执行栈为空。
4 检查微任务队列,如果有任务则按顺序执行所有的微任务。
5 执行宏任务队列中的一个人物
6.回到第三步,重复以上步骤</code></pre><p>在每个事件循环中,会先执行所有的微任务,然后执行一个宏任务。这样的机制保证了异步任务的执行顺序,并且能够及时响应用户的交互。<br>常见的宏任务包括setTimeout, setInterval,网络请求等,而微任务包括Promise、MutationObserver等。<br>理解实际那循环对于编写高效的异步代码非常重要,它能够帮助我们合理地处理任务并避免阻塞主线程。</p><h4>作用域链:</h4><ul><li>在js中每个函数都有自己的作用域,</li><li>当在一个函数内部引用一个变量,js会按照代码中出现的顺序,从当前作用域开始依次向上查找</li><li>直到找到第一个包含这个变量的作用域为止,这个过程被称为作用域链的查找</li><li>如果整个作用域链都没有找到,就会报错抛出ReferenceError异常</li></ul><pre><code>function outer(){
const a = 10
function inner(){
console.log(a);
console.log(b);//Uncaught: ReferenceError:b is not defined
}
inner()
}
outer()</code></pre><h4>js在转时间时时分秒不满10补0的几种方式</h4><pre><code>const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
1 使用ES6模版字符串
padStart指定字符串的长度,如果不满,则在前面补上自己定义的字符串
`${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`
2 使用三元运算符
(h<10?"0"+h:h)+ ":" + (m<10?"0"+m:m)+":"+(s<10?"0"+s:s)
3 使用Array.map和Array.join方法
const timeArr = [h,m,s].map(value=>{
return value <10?"0"+value:value
})
timeArr.join(":")</code></pre><h4>当我们在浏览器地址栏中输入一个网址时,发生了什么</h4><p>1.DNS解析: 浏览器会检查自己有无缓存,如果有该域名对应的IP地址就直接使用,否则则向DNS发起请求<br>2.发起Http请求,浏览器会根据URL的协议头向服务器发起相应的网络请求,同时浏览器也会发送一些请求头和请求参数<br>3.建立TCP连接:在HTTP请求建立之前,浏览器需要和服务器建立TCP连接,TCP是一种面向连接的、可靠的、基于字节流的传输协议,通过三次握手来确保数据能够准确<br>可靠的发送到服务端,而不会出现数据丢失或乱序的情况<br>4.发送HTTP请求,在建立了TCP连接之后,浏览器就能够向服务器发送HTTP请求报文。HTTP请求报文包括请求头,请求行,和请求体<br>5.接受服务器相响应报文:当服务器收到请求报文后,会解析请求,然后返回相应的响应报文给客户端。HTTP响应报文包括状态行、响应头和响应体三个部分,其中状态行包含了该请求的结果状态码。<br>6.解析渲染页面:当浏览器接收到服务器返回的响应报文之后,会根据相应报文中的内容(如HTML,CSS、javascript等资源),解析出对应的DOM树、CSS规则树和javascript代码<br>并且根据它们构建出一个渲染树。最后将这些内容交给浏览器的渲染引擎进行渲染,生成我们最终看到的页面。<br>7.断开TCP连接,当浏览器接收到服务器返回的响应报文后,会关闭该连接,释放资源。如果浏览器需要请求更多的资源,则需要重新建立新的TCP连接</p><h4>如何减少重排和重绘</h4><p>重排和重绘是浏览器渲染过程中的两个关键步骤<br>重排是指浏览器计算元素的位置和大小属性,并重新布局页面的过程。<br>重绘是指根据元素的样式属性,重新绘制元素的外观<br>重排和重绘是耗费性能的操作,因此减少重排和重绘可以提高页面的性能和响应速度</p><pre><code>1. 批量修改样式:避免对元素的样式属性进行频繁的单独修改,而是尽量将多个样式的修改合并为一个操作。可以使用Css类名的方式一次性地修改多个样式属性。
2. 使用文档片段:如果需要通过js动态地插入一系列元素,可以先将它们添加到文档片段(Document Fragment)中,然后再一次性的讲文档片段添加到文档中,这样可以减少重排的次数。
3. 避免频繁读取布局信息,当获取元素的位置、大小等布局信息时,尽量避免在每次操作中都去读取这些信息,而是将其缓存起来,以减少重排的次数
4. 使用CSS3动画和过渡:CSS3动画和过渡是基于浏览器的硬件加速,可以减少重绘和重排的开销。尽量使用CSS3动画和过渡来实现动画效果,而不是使用js来操作样式属性。
5. 使用requestAnimationFrame: 使用requestAnimationFrame来执行动画可以更好的与浏览器的重绘机制结合,减少不必要的重绘和重排操作
6. 避免频繁的DOM操作:DOM操作会导致重排和重绘,尽量避免频繁的对DOM进行增删改操作,可以先将需要操作的DOM元素从文档中移除,进行批量操作后再重新插入。
7. 使用css布局工具:使用Css的flexBox和Grid等布局工具可以更好地控制页面布局,减少重排的次数。</code></pre><h4>import 和require是js两种不同的模块化规范</h4><ol><li>用法不同 <br>import时ES6中新增的模块化语法,用于在代码中引入其他ES6模块的导出对象。它是一个顶级声明<br>只能出现在js代码最外层或其他类似的顶级位置,不能在其他代码块中使用 import {debounce} from "lodash"<br>require 是CommonJS规范中的模块化语法,用于在代码中引入CommonJS模块的导出对象,它可以在任何位置使用,包括函数内部或代码内部 const debounce = require("lodash/debounce")</li><li>加载时机不同<br>import 在代码编辑时就会被处理,因此在代码执行前就已经加载了相应的模块。这使得import可以在代码运行之前进行静态分析,从而在构建时进行优化<br>require 是在运行时才会被加载,这意味着当代码中使用require时,它会在代码被执行的到的时候加载模块,并将其导出对象作为结果返回</li><li><p>语法不同<br>import的语法相对简洁,并且可以进行模块的命名空间分离和解构。同时,它也支持异步加载模块</p><pre><code>//引入lodash模块中的debounce方法并重命名为myDebounce
//解构 只导出需要的内容,减少导出对象的大小
import {debounce as myDebounce} from "lodash"
import * as myModule from "./model.js" //命名空间分离 通过一个对象承载模块的所有导出内容。从而实现命名空间分离
//使用异步函数动态引入模块
const someAsyncModule = await import ('./path/to/module')</code></pre></li></ol><p>require的语法相对复杂,特别是在需要进行多层路径嵌套时更为明显,它不能进行命名空间分离,也不支持ES6中的解构语法。同时也不能异步加载模块,需要额外的库或者手动编写异步加载逻辑</p><p>总之,如果需要使用Es6中的新特性或者异步加载模块,使用import, 如果需要兼容node或者CommonJS环境,可以使用require</p><h4>WebSocket的使用</h4><p>WebSocket是一种在Web浏览器和服务器之间进行全双工通信的协议。它提供了更强大的实时数据传输能力,相对于传统的HTTP请求-响应模式,WebSocket允许服务器主动向客户端推送数据,实现了真正的双向通信。</p><pre><code>//创建WebSocket连接 得到一个Websocket对象,url是websocket服务器的地址
const socket = new WebSocket("ws://example.com/socket")
监听事件: WebSocket对象支持多个事件,如`open`,'message',"error"和'close'。通过给WebSocket对象添加对应的事件监听器函数,
可以处理连接打开、接收到消息、出现错误和连接关闭等情况
// 监听连接打开事件
socket.addEventListener("open",event=>{
console.log("WebSocket 连接已打开");
})
// 监听接受到消息事件
socket.addEventListener("message",event=>{
console.log("收到消息",event.data);
})
// 监听连接错误事件
socket.addEventListener("error",error=>{
console.log("WebSocket 错误:",error);
})
//监听连接关闭事件
socket.addEventListener("close",event=>{
console.log("Websocket 连接已关闭");
})
//发送消息 send(data)方法可以向服务器发送消息。服务器接收到消息后,可以通过‘message’ 事件监听器处理。
//客户端可以使用event.data获取服务器发送的消息内容
socket.send("Hello,Websocket")
//关闭连接:当不再需要连接时,可以使用WebSocket对象的close方法来关闭连接
socket.close()
</code></pre><p>WebSocket连接需要服务器支持,服务器需要实现相应的WebSocket协议来处理连接和消息的传输。<br>在实际使用中,可以使用诸如Node.js的ws模块或者Websocket框架来实现服务器端的功能</p><h4>微任务和宏任务以及使用场景</h4><p>微任务和宏任务是用来管理js异步任务执行顺序的机制。它们决定了任务在事件循环中的执行顺序。<br>微任务是由js引擎提供的任务队列,它的执行优先级高于宏任务,常见的微任务有Promise的回调函数,MutationObserver和progress.nextTick<br>宏任务是由浏览器提供的任务队列,它的执行优先级较低,常见的宏任务有定时器setTimeout,setinterval, DOM事件回调和Ajax请求等</p><p>使用场景:</p><p>微任务的使用场景:<br> 1 需要在当前事件循环的末尾执行的任务,可以使用微任务,例如Promise的回调函数。<br> 2 需要立即执行的任务,可以使用微任务,例如MutationObserver监听DOM变化并立即做出相应</p><p>宏任务的使用场景:<br> 1 需要延迟执行的任务,可以使用宏任务,例如定时器的回调函数。<br> 2 需要在事件循环中的下一个循环中执行的任务,可以使用宏任务,例如DOM时间回调和Ajax请求</p><p>在一个事件循环中,当所有的宏任务执行完毕后,会先执行所有的微任务,然后再进行下一个宏任务的执行,这样可以保证微任务的优先级高于宏任务,确保及时响应和更新<br>总结起来,微任务和宏任务是用来管理异步任务执行顺序的机制,微任务的执行优先级高于宏任务,适合处理需要立即执行或在当前循环末尾执行的任务。宏任务适合处理需要延迟执行或在下一个循环中执行的任务</p><h4>ES6中一些常用的特性和语法</h4><ol><li>let 和 const 关键字</li><li>箭头函数</li><li>解构赋值</li><li>扩展运算符</li><li>类和继承</li><li>模版字符串</li><li>promise 和async/await</li><li>模块化<br>Symbol、Map、Set、Proxy、Reflect</li></ol><h2>React</h2><h4>Vue和React的区别</h4><p>分五个方面来说:</p><ol><li>模版语法:<br>Vue使用了HTML的模版语法来编写组件模版,比较易于理解和学习,使得开发者可以快速的编写出页面。这样做的优点在于,将HTML和JavaScript代码分离开来,<br>有利于代码的维护性和阅读性。Vue的模版语法也提供了一些强大的功能,如条件渲染,循环渲染,事件处理等.<br>React则推崇:一切都是javascript,它采用了jsx语法,即Javascript和Xml的混合语法,使用jsx可以轻松地创建复杂的UI,并提高了应用程序的性能,<br>不过使用jsx的同事,你需要学习更多的语法和编程范式</li><li>数据绑定<br>Vue提供了双向数据绑定的功能,可以实现视图和数据的自动同步,极大方便了数据管理和操作。双向绑定包括两个部分:数据模型和视图模型。在Vue中,数据模型即组件实例中的数据;视图模型则负责数据模型中的数据绑定到视图中去。<br>React的数据流是则是单向的,采用了组件间props和State的传递和管理数据,通过props向组件传递属性值,并监听这个属性的更改事件来更新UI<br>从而实现数据的单向流动;而state则是保存组件内部状态的地方,是可变的数据,推荐在合适的情况下尽量使用不可变数据。</li><li>组件分类<br>Vue将组件分为有状态组件和无状态组件 functional两种,有状态组件包含了应用程序中的逻辑和数据,可以实现更复杂的操作和交互;无状态组件只提供了一个对应的UI界面,没有数据和逻辑的处理,通常用于小组件或纯展示类组件。Vue的组件具有生命周期函数,可以在组件的不同阶段执行不同的处理逻辑。<br>React没有严格的组件分类,但通常会将组件分为函数式组件和Class组件两种。函数式组件是一种纯javascript函数,接受props对象作为参数,返回一个React元素,不支持状态和生命周期函数;class组件则是使用面向对象编程的方式来构建组件,支持状态和生命周期函数。所以React组件也具有生命周期,可以在组件的不同阶段执行相关操作。</li><li>生命周期<br>Vue和React都拥有各自的生命周期函数,并分别在不同的组件阶段调用这些函数。Vue的生命周期函数包括了created、mounted、updated和destroyed等每个函数都有特定的用途和功能<br>React的生命周期函数包含了componentWillMount、componentDidMount、shouldComponentUpdate、componentWillUnmount等也都有各自的特点和用途。<br>生命周期函数可以用于解决组件挂载、更新、销毁等过程的一系列问题和逻辑</li><li>渲染效率</li></ol><p>Vue采用了虚拟DOM和异步渲染等技术来提高程序的性能,虚拟DOM是将真实的DOM抽象成js对象,可以快速的进行对比和计算,从而减少DOM操作带来的性能损耗;<br>异步渲染则是让浏览器在空闲时间渲染组件,从而提高渲染效率。<br>React则使用了一种名为reconciliation的算法在不重新渲染整个组件数的情况下更新UI,这也使得React具有较高的渲染效率。<br>在组件更新时,React会通过对比虚拟DOM的变化来更新UI,从而避免大量的DOM操作</p><p><em>Vue和React都是非常优秀的前端框架,采用不同的实现方式,适用于不同的开发场景。Vue更加注重开发体验和易用性,适合快速开发小型和中型的应用;React更加注重应用程序的可维护性和性能,适合大型应用或需要更好的可扩展性的应用。选择哪个框架取决于项目的需求和团队的偏好</em></p><h2>Webpack</h2><h4>webpack的摇树</h4><pre><code>摇树(tree Shaking)是指在打包过程中,通过静态分析的方式,去掉没有使用的代码,从而减小最终打包后的文件体积
在Webpack中摇树是通过ES6模块化语法和静态分析工具(如UglifyJS)来实现的。
当Webpack打包时,它会分析模块之间的依赖关系,并且检查哪些代码被实际使用了,哪些去除掉没有被使用的代码
摇树的原理是基于ES6模块化的静态特性,它可以在编译时进行静态分析,因为ES6模块化的导入和导出是静态的,而CommonJS模块化的导入和导出是动态的
要实现摇树,需要满足以下条件:
1 使用ES6模块化语法进行导入和导出
2 代码中的导入必须是静态的,不能使用动态导入
3 代码中的导出必须是静态的,不能使用动态导出
当满足这些条件时,Webpack在打包的过程中会自动进行摇树优化,去掉没有使用的代码,从而减小打包后的文件体积
需要注意的是,摇树只能去掉没有使用的代码,而不能去除没有被导入但被使用的代码。</code></pre><h4>npm run dev时,webpack做了什么</h4><pre><code>当你运行npm run dev命令时,webpack会执行一系列的操作来构建和打包你的项目。
1. 根据配置文件(通常是webpack.config.js),webpack会读取配置中的入口文件(entry)和出口文件(output)的路径信息。
2. 根据入口文件的路径,webpack会分析项目的依赖关系,找到所有需要打包的模块。
3. webpack会根据配置中的加载器(loader)对模块进行处理。加载器可以将非js文件(如css,图片等)转换为js模块,或者对js模块进行预处理(如使用Babel进行ES6转换)
4. webpack会根据配置中的插件(plugin)对模块进一步的处理。插件可以用于优化打包结果、拓展webpack的功能等。
5. webpack会根据配置中的出口文件路径和文件名,将打包后的模块输出到指定的目录中
6. 在开发模式下,webpack会启动一个开发服务器( dev server),并监听文件的变化,当文件发生变化时,webpack会自动重新构建并刷新浏览器
7 webpack 还会生成一个包含构建信息的统计文件,可以用于分析打包结果、性能优化等</code></pre><p>总的就是:webpack在运行npm run dev 时,会根据配置文件对项目进行构建和打包,并提供开发服务器以及自动编译的功能,方便开发人员进行实时调试和开发</p><h4>webpack缓存</h4><p>Webpack提供了缓存机制,可以通过缓存来提高投建性能。webpack的缓存机制有两个方面:</p><ol><li><p>Loader缓存:<br> 在webpack构建过程中,Loader可以使用缓存来提高性能<br> Loader缓存可以避免对于同一个文件的重复处理,从而加快构建速度。<br> 通过设置 cache:true 来开启Loader缓存,例如:</p><pre><code> module:{
rules:[{
test:/\.js$/,
use:'babel-loader',
options:{
cacheDirectory:true
}
}]
}
</code></pre></li><li>Module缓存:<br>在webpack构建过程中,Webpack会根据模块的内容生成一个唯一的标识符(hash).<br>如果模块内的内容没有发生变化,Webpack会复用之前的构建结果,避免重新构建该模块,从而提高构建速度。<br>Module缓存是基于文件的,只有在文件内容发生变化时才会重新构建<br>Module缓存是默认开启的,可以通过设置cache:true 来显示启用缓存。<br>通过使用Loader缓存和Module缓存,Webpack可以避免重复处理和构建不变的模块,从而提高构建的性能和速度。<br>需要注意的是,缓存机制只有在构建过程找那个文件内容没有发生变化时才会生效,如果文件内容发生变化时,Webpack会重新构建整个模块和依赖树。</li></ol><h2>TypeScript</h2><h4>ts中如何新增一个类型</h4><p>在ts中有几种方式可以创建新类型:</p><pre><code>1. 类型别名(Type Alias):
使用type 关键字创建一个类型别名,可以给现有类型起一个新的名字
例如: type myType = string|Number;
2.接口(interface):
使用interface 关键字创建一个接口,用于定义一个对象的结构。
例如interface MyInterface{name:string;age:number;}
3.类(Class):
使用class 关键字创建一个类,用于定义一个对象的结构和行为。
例如: class MyClass{name:string;age:number}
4.枚举(Enum):
使用enum关键字创建一个枚举类型,用于定义一组具名的常量值。
例如:enum MyEnum{A,B,C}</code></pre><h4>any与unkown的区别</h4><p>在Typescript中,any和unkown都是用来表示不确定类型的,都是表示任意类型,但是它们之间有一些区别:</p><pre><code>1 可赋值性:
any 类型可以赋值给任何类型,也可以从任何类型中获取值
unknown类型只能被赋值给unkown和any类型,不能直接从中获取值。
2 类型检查和类型判断:
any类型的变量不会进行类型检查,编译器不会对其进行类型推断或类型检查。
unknow类型的变量在编译器中会进行类型检查,使用之前必须进行类型检查或类型断言
3 方法和属性的调用:
any类型的变量可以调用任何方法和属性,而不会报错。
unknown类型的变量不能调用任何方法或属性,除非先进行类型断言或类型检查
4 类型安全性:
使用any类型会丧失类型安全性,因为它可以接受任何类型的值。
使用unkonw类型可以提供更好的类型安全性,因为在使用之前必须进行类型检查。
因此,unkown类型相比于any类型提供了更好的类型检查和类型推断,以及更好的类型安全性,因此,尽量使用unkown类型来表示不确定类型,只有在必要的情况下才使用any类型。</code></pre>
WebGL与ThreeJs
https://segmentfault.com/a/1190000043333362
2023-01-13T17:49:30+08:00
2023-01-13T17:49:30+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<p><strong>如果要说WebGL与ThreeJs肯定就要说3D了</strong></p><h3>3D的基础概念</h3><p>3D分为纹理和贴图和材质</p><ul><li>纹理即纹路,每个物体表面上不同的样子,比如木头的木纹状</li><li>贴图是图 最简单的形式是ps之类的软件做出来的一张图,这些图在3D中用来贴到物体的表面,用来表现物体的纹理</li><li>材质主要用来表现物体对光的交互(反射,折射等)性质的,比如金属对光的反射和毛毯对光的反射性质完全不一样,那么对3D程序来说,这样的差别就通过材质这个属性来计算出不同的颜色</li></ul><h4>日常中有哪些3D周边</h4><ul><li><p>数据可视化</p><ul><li>echarts</li><li>Highcharts</li><li>D3.js</li></ul></li><li><p>跨界AR和VR</p><ul><li>Reacr-VR/AR</li><li>ar.js</li><li>renderloop</li></ul></li><li><p>游戏开发</p><ul><li>Cocos2d-JS</li><li>Egret</li><li>CreateJS</li></ul></li><li><p>3D开发</p><ul><li>Three.js</li><li>xeolgl</li><li>cesiumo</li><li>babylonjs</li></ul></li></ul><h3>WEBGL概念知识</h3><blockquote>显示器不认识数学家YY的数学模型 他只认识像素图<br>图形工程师就是把数学家YY的数学模型给转化成图片扔给显示器显示出来<br>要做好上面的事,学习WebGL = 学数学家Yy的数学模型 + 怎么把数学模型转化成图片<br>OpenGL/Dx 规定与封装了把数学模型转换成图片的过程</blockquote><p><em>WebGL就是OpenGL和Javascript生的傻儿子</em></p><h3>WebGL怎么把数学模型转换成图片?</h3><ol><li>OpenGL中有固定渲染管线和可编程渲染管线</li><li><p>WebGL目前只有可编程渲染管线</p><ol><li>可编程渲染管线:固定的MVP套路计算机会跑,但有时候其中某些套路不想让计算机自动处理,手动编写某个套路就是可编程渲染管线</li><li><p>着色器:就是帮助我们做可编程渲染管线的工具。WebGL常用的有顶点着色器,片元着色器</p><ol><li><p>顶点着色器:顾名思义它能够处理订单坐标、大小等(矩阵计算后的结果),能够把数学坐标光栅化</p><ol><li>片元中的每一个元素对应与缓冲区中的一个像素。光栅化其实是一种将几何图元变成二维图像的过程。该过程包含了两部分的工作,第一部分的工作:决定窗口坐标中的哪些整形栅格区域被基本图元占用;第二部分的工作:分配一个颜色值和一个深度值到各个区域。光栅化过程产生的是片元。</li><li>光栅化:把物体的数学描述以及物体相关的颜色信息转换为屏幕上用于对应位置的像素及用于填充色素的颜色。这个过程就是光栅化,这是一个将模拟信号转化为离散信号的过程。光栅化就是把顶点数据转换为片元的过程</li></ol></li><li>片元着色器:能够接受光栅化数据并加以处理使其显示到屏幕上(光栅化数据包含了像素的位置,颜色等信息)</li></ol></li></ol></li></ol><ol start="3"><li><p>固定渲染管线:就是把一套把数学模型转成图片的套路 ---模型坐标转换(M)、视图坐标转换(V)、投影坐标转换(P)简称MVP套路</p><ol><li>模型坐标转换:模型的基准点(原点)、模型的大小、模型旋转角度等</li><li>视图坐标转换:从哪个方向、角度观看这个模型</li><li>投影坐标转换:离得越近的呈现出来的图像就应该越大,越远越小</li></ol></li></ol><h6>javascript程序 ==> 顶点着色器 ==> 片元着色器 ==> webGL程序</h6><blockquote><p>在构建3D物体时通过顶点组成三角形网格,但这些三角形网格都是矢量图形,最终在屏幕上显示时还是需要转换为像素图形,这种转化过程被称为栅格化,是计算机图形学的关键技术之一,</p><p>webGL程序和普通的javascript程序不一样,WebGL程序除了Javascript部分之外,还包含两个着色程序,分别是顶点着色器和片元着色器;在ThreeJs中描述一个3D物体,需要轮廓,材质,和纹理三个要素;那么可以简单理解为顶点着色器用来处理物体的轮廓程序,片色着色器是用来处理物体材质和纹理的程序</p></blockquote><h3>Three.js</h3><ol><li><p>threejs三大组件</p><ol><li>Camera 相机</li><li>Renderers(渲染器) ----Shaders</li><li>Scenes(屏幕)</li></ol></li><li><p>图像的表示</p><ol><li><p>Lights(光线)</p><ol><li>Shadows</li></ol></li><li>Geometries (几何图形)</li><li>Materials(材质)</li><li>Objects(图形对象)</li><li><p>Loaders(加载器)</p><ol><li>Managers</li></ol></li><li>Textures(纹理)</li></ol></li><li><p>框架原理</p><ol><li><p>Math数学库</p><ul><li>Interpolants (插值)</li></ul></li><li><p>Extras(附件)</p><ul><li>core</li><li>Curves</li><li>Helpers</li><li>Objects</li></ul></li><li>Constant(常量)</li><li><p>Core (核心)</p><ul><li>BufferAttribute</li></ul></li></ol></li><li><p>动画和声音</p><ol><li><p>Animation(动画)</p><ul><li>Tracks</li></ul></li><li>Audio(声音)</li></ol></li></ol><h5>Three.js必备三个条件(scene、camera、renderer渲染)</h5><ol><li>场景Scene是一个物体的容器(通俗理解装东西的),开发者可以将需要的角色放入到场景中,例如苹果,葡萄。同时角色自身也管理着其在场景中的位置</li><li>相机camera的作用就是面对场景,在场景中取一个合适的景,把它拍下来。(可以想象成人的眼睛)</li><li><p>渲染器renderer的作用就是将相机拍摄下来的图片,放到浏览器中去显示</p><h5>坐标系的位置和指向</h5><p>坐标系的原点在画布中心 (canvas.width/2,canvas.height/2)</p><p>通过Three.js提供的THREE.AxisHelper()辅助方法将坐标系可视化</p><h5>THREE.PerspectiveCamera(fov,aspect,near,far)</h5><ul><li>For :视场,即摄像机能看到的视野。比如,人类有接近180度的视场,而有些鸟类有接近360度的视场。但是由于计算机不能完全显示我们能够看到的场景,所以一般会选择一块较小的区域。对于游戏而言,视场的大小通常为60-90度。 推荐默认值为:50</li><li>Aspect:指定渲染结果的横向尺寸和纵向尺寸的比值。在我们的示例中,由于使用窗口作为输出界面,所以使用的是窗口的长度比。推荐默认值:window.innerWdth/window.innerHeight</li><li>Near: 指定从距离摄像机多近的距离开始渲染。推荐默认值: 0.1</li><li>Far:指定摄像机从它所处的位置开始能看到多远。若过小,那么场景中的远处不会被渲染;若过大,可能会影响性能,推荐默认值:1000</li></ul></li></ol><h5>Mesh</h5><p>网格就是一系列的多边形组成的,三角形或者四边形,网格一般由顶点来描绘,我们看见的三维开发的模型就是由一些列的点组成的</p><blockquote><p>Mesh好比一个包装工</p><p>将可视化的材质 粘合在一个数学世界里的几何体上,形成一个可添加到场景中的对象</p><p>创建的材质和几何体可以多次使用</p><p>还有Points(点集)、Line(线/虚线)等</p></blockquote><table><thead><tr><th>BoxGeometry 长方体</th><th>CircleGeometr 圆形</th><th>ConeGeometry圆锥体</th><th>CylinderGeometry 圆柱体</th></tr></thead><tbody><tr><td>DodecahedronGeometry(十二面体)</td><td>IcosahedronGeometry(二十面体)</td><td>LatheGeometry(让任意曲线绕y轴旋转生成一个形状,如花瓶)</td><td>OctahedronGeometry(八面体)</td></tr><tr><td>ParametricGeometry(根据参数生成形状)</td><td>PolyhedronGeometry(多面体)</td><td>RingGeometry(环形)</td><td>ShapeGemetry(二锥形)</td></tr><tr><td>SphereGeometry(球体)</td><td>TetrahedronGeometry(四面体)</td><td>TorusGeometry(圆环形)</td><td>TorusKnotGeometry(换面钮结体)</td></tr><tr><td>TubeGeometry(管道)</td><td>\</td><td>\</td><td>\</td></tr></tbody></table><h5>Material材质</h5><ol><li>MeshBaseMaterial(网格基础材质)是一种非常简单的材质,这种材质不会考虑光照的影响。使用这种材质网格会渲染成简单的平面多边形,并且可以显示几何体的线框</li><li>MeshDepthMaterial(网格深度材质)使用该材质的物体的外观不是由某个材质属性决定的,而是有物体到相机的距离决定的,离相机越近越亮,离相机越远越暗。该材质的属性很少,没有设置物体颜色的属性, 如果想改变物体的颜色,就需要创建多材质的物体</li><li>MeshNormalMaterial(网格法向材质)通过法向量来隐射RGB颜色。每个法向量不同的面赋予不同的颜色</li><li>MeshFaceMaterial (网格面材质)可以为几何体每一个面指定不同的材质,比如一个立方体有6个面 你可以为每一个面指定一个材质</li><li>MeshLambertMaterial(网格朗伯材质)用于创建看上去暗淡的、不光亮的表面,可以对光源产生阴影的效果</li><li>MeshPhongMaterial(网格Phong式材质)用于创建光亮表面的材质。可以产生阴影的效果</li><li>ShaderMaterial(着色器材质)该材质是最复杂的一种材质 可以使用自己定制的着色器</li></ol><h5>Light光源</h5><ol><li>环境光源没有位置概念,会将颜色应用到场景的每一个物体上,主要作用是弱化阴影 给场景添加颜色</li><li>点光源类似于照明弹,朝所有的方向发光,因此不产生阴影</li><li>聚光灯光源类似于手电筒,形成锥形的光束、随着距离的增加而变弱,可以设定生成阴影</li><li>方向光光源类似于太阳,从很远的地方发出的平行光束 距离越远,衰减的越多</li><li>想要一个自然的室外效果 除了添加环境光弱化阴影,添加聚光灯为场景增加光线,还需要使用半球光光源将天空和空气泵以及地面的散射计算进去 使得更自然更真实</li><li>平面光光源定义了一个发光的发光体,需要使用webgl的延迟渲染机制</li><li>炫光效果 在有太阳的时候使用炫光光源 会使得场景更真实</li></ol><h5>粒子</h5><p>粒子一直面向摄像机(无论你旋转摄像机还是设置粒子的rotation属性)</p><blockquote><p>THREE.Sprite(meterial) 可以加载图片作为粒子的纹理</p><p>THREE.Points(geometry.material) 创建纯点作为粒子</p></blockquote><h5>射线</h5><blockquote>用于鼠标去获取在3D世界被鼠标选中的一些物体</blockquote><pre><code>mouse.x = (e.clientX/window.innerWidth)*2 -1
mouse.y = (e.clientY/window.innerHeight)*2 -1</code></pre><p>推导过程:</p><blockquote><p>设A点为点击点(x1,y1)x1 = e.clientX, y1 = e.clinetY</p><p>设A点在世界坐标中的坐标值为B(x2,y2)</p><p>由于A点的坐标值的原点是以屏幕左上角为(0,0)</p><p>我们可以计算可得以屏幕为原点的B值</p><p>X2 = x1-innerWidth/2</p><p>Y2 = innerHeight/2 - y1</p><p>又由于在世界坐标的范围是[-1,1],要得到正确的B值我们必须要将坐标标准化</p><p>X2 = (x1 - innerWidth/2)/(innerWidth/2) = (x1/innerWidth)*2-1</p><p>同理可得y2 = -(y1/innerHeight)*2 + 1</p></blockquote><h5>一般需要用到的插件</h5><blockquote><p>Dat.GUI提供了可视化调参的面板,对参数调整的操作提供了极大的便利</p><p>Stats.js 帧率,每帧的渲染时间、内存占用量、用户自定义</p></blockquote><h5>适合开发VR的开发工具</h5><ul><li>VR盒子 大部分的VR盒子都是通过光学透镜把手机屏幕的画面转变为VR画质,使得用户享有沉浸式的体验,这一产品的代表作为谷歌Cardboard 这个需要500元,建议买国产</li><li>VR一体机 是指哪些具备了独立处理器的VR设备 他们不再通过手机或电脑的处理器运行,具有独立输入输出的能力,自成一体,故称一体机,推荐一款 柔宇</li><li>VR头显 是目前最顶级的VR设备 在这个领域里有HTC Vive和Oculus Rift配合一部高配置的电脑主机,但是你想要有顶级的VR体验 花个六七千是必须得</li><li>使用过的: 暴风VR 零镜小白VR</li></ul><h5>Three.js智慧工厂3D项目</h5><p><strong>借助一些库实现快速开发</strong></p><ol><li>缓存离屏渲染(两层canvas)</li><li>Lower-canvas和upper-canvas(底层绘制上层处理用户的相应)</li><li>射线算法优化相交的点</li><li>处理Retina屏,canvas.width,canvas.height放大至dpi倍</li></ol><p><strong>需要组件</strong></p><pre><code>yarn add three -D
yarn add obj2gltf -D</code></pre><pre><code> <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
margin:0;
padding:0;
}
body{
background: #000;
}
</style>
</head>
<body>
<script src="./node_modules/three/build/three.js"></script>
<script src="./node_modules/three/examples/jsm/loaders/GLTFLoader.js"></script>
<script src="./node_modules/three/examples/jsm/controls/OrbitControls.js"></script>
<script src="./node_modules/three/examples/jsm/loader/DRACOLoader.js"></script>
<script src="./index.js"></script>
</body>
</html></code></pre><p>Index.js</p><pre><code>/*
* @Author: yangyuguang
* @Date: 2023-01-15 11:10:55
* @LastEditors: yangyuguang
* @LastEditTime: 2023-01-15 15:48:54
* @FilePath: /tt/index.js
*/
let container,camera,scene,light,rendered,controls;
init()
function init(){
container = document.createElement('div')
document.body.appendChild(container)
// 创建眼睛
camera = new THREE.PerspectiveCamera(45,window.innerWidth/window.innerHeight,0.25,1000)
camera.position.set(0,80,200)
controls = new THREE.OrbitControls(camera,container) //轨道控制器 随着相机和容器走的
controls.target.set(0,-0.2,-0.2) //设置目标的距离
controls.update()
scene = new THREE.Scene()//容器
light = new THREE.HemisphereLight(0xbbbbfff,0x444422)//光线
scene.add(light)
light.position.set(0,1,0)//光线角度
const loader = new THREE.GLTFLoader().setPath('model/')
const _DRACOLoader = new THREE.DRACOLoader()
//利用DRACOLoader进行解压
_DRACOLoader.setDecoderPath("./javascript")
loader.setDRACOLoader(_DRACOLoader)
loader.load('model.gltf',function(gltf){
scene.add(gltf.scene)
},undefined,function(e){
gltf.scene.scale.set(0.03,0.03,0.03)//场景过大,缩小一些
gltf.scene.position.set(-80,0,0)//缩放之后,模型的位置也调整一下
scene.add(gltf.scene)
})
rendered = new THREE.WebGL1Renderer({
antialias:true
})
rendered.setPixelRatio(window.devicePixelRatio)
rendered.setSize(window.innerWidth,window.innerHeight)
container.appendChild(rendered.domElement)
// 打一些辅助线
const axesHelper = new THREE.AxesHelper(60)
scene.add(axesHelper)
const hemisphereLight = new THREE.HemisphereLight(light,60)
scene.add(hemisphereLight)
const helper = new THREE.CameraHelper(camera)
scene.add(helper)
}
function animate(){
requestAnimationFrame(animate)
rendered.render(scene,camera)
}
animate()</code></pre><p>使用obj2gltf将obj文件转成gltf</p><pre><code>"gltf":"obj2gltf -i ./static/model.obj -t"</code></pre><p>然后加载的是gltf文件,但是gltf的体积还是太大,所以需要压缩,使用draco<br><img src="/img/bVc50aa" alt="image.png" title="image.png"></p><ol><li>把这个编译成js的文件夹下载下来,放到自己的项目中,</li><li>官方提供了一个APi,所以可以做一个package.json的压缩</li><li>先下载 yarn add gltf-pipeline -D</li></ol><pre><code>"scripts": {
"gltf":"obj2gltf -i ./static/model.obj -t",
"min":"gltf-pipeline -i ./static/model.gltf -d -s"
},</code></pre><p>-d 压缩 -s是分离(它把它绘图的数据变成二进制的bin,把一些基本文件的配置变成原生的gltf),也就生成了新的gltf文件,<br>但是压缩归压缩,回显还是需要解压的,利用DRACOLoader进行解压</p><p><strong>上面的是将Obj转成gltf类型的模型,那么也可以直接渲染Obj模型</strong></p><h3>HTML方式</h3><pre><code>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加载OBJ和MTL文件</title>
<style>
html,
body{
width: 100%;
height: 100%;
overflow: hidden;
}
*{
margin:0;
padding:0;
}
</style>
<!-- 引入three.js三维引擎 -->
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script>
<script src="http://www.yanhuangxueyuan.com/threejs/examples/js/controls/OrbitControls.js"></script>
<script src="http://www.yanhuangxueyuan.com/threejs/examples/js/loaders/OBJLoader.js"></script>
<script src="http://www.yanhuangxueyuan.com/threejs/examples/js/loaders/MTLLoader.js"></script>
</head>
<body>
<script>
var scene = new THREE.Scene(),
camera,
renderer = new THREE.WebGLRenderer();
/* OBJ和材质文件mtl加载 */
var OBJLoader = new THREE.OBJLoader(); //obj加载器
var MTLLoader = new THREE.MTLLoader();//材质文件加载器
MTLLoader.load('./static/material.mtl',function(meterials){
console.log(meterials);
OBJLoader.setMaterials(meterials);
OBJLoader.load('./static/model.obj',function(obj){
/* 返回的组对象插入场景中 */
scene.add(obj)
obj.children[0].scale.set(5,5,5)//网络模型播放
})
})
createLight()
createCamera()
createRenderer()
/* 通过requestAnimationFrame()操作三维场景 */
var controls = new THREE.OrbitControls(camera,renderer.domElement); //创建控件对象
// 创建一个时钟对象Clock
var clock = new THREE.Clock()
function render(){
renderer.render(scene,camera);//执行渲染操作
requestAnimationFrame(render) //请求再次执行渲染函数render
}
render()
// 正投影相机设置
function createCamera(){
var width = window.innerWidth; //窗口设置
var height = window.innerHeight;//窗口高度
var k = width/height;//窗口宽高比
var s = 150;//三维场景显示范围控制系数,系数越大,显示的范围越大
// 创建相机对象
camera = new THREE.OrthographicCamera(-s*k,s*k,s,-s,1,1000)
camera.position.set(200,300,200) //设置相机位置
camera.lookAt(scene.position);//设置相机方向(指向的场景对象)
}
// 透视相机
function createPerspectiveCamera(){
var width = window.innerWidth;//窗口宽度
var height = window.innerHeight//窗口高度
/* 透视投影相机对象 */
camera= new THREE.PerspectiveCamera(60,width/height,1,1000)
camera.position.set(200,300,200)//设置相机位置
camera.lookAt(scene.position)//设置相机方向(指向的场景对象)
}
/* 创建渲染器对象 */
function createRenderer(){
renderer.setSize(window.innerWidth,window.innerHeight)//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff,1)//设置背景颜色
document.body.appendChild(renderer.domElement)//body元素中插入canvas对象
}
/* 创建光源 */
function createLight(){
// 点光源
var point = new THREE.PointLight(0xffffff)
point.position.set(400,200,300)//点光源位置
scene.add(point);//点光源添加到场景中
// 环境光
var ambient = new THREE.AmbientLight(0x444444)
scene.add(ambient)
}
</script>
</body>
</html></code></pre><h3>在vue中使用</h3><p><strong>第一种方法使用three-obj-mtl-loader插件</strong></p><p>使用npm install three-obj-mtl-loader --save</p><p>在组件中引入:import { OBJLoader, MTLLoader } from 'three-obj-mtl-loader'</p><p>使用OBJLoader和MTLLoader加载文件:</p><pre><code>import {OBJLoader,MTLLoader} from "three-obj-mtl-loader"
// 加载obj和mtl模型
let _this = this
let mtlLoader = new MTLLoader()
mtlLoader.load('./static/material.mtl',function(materials){
materials.preload();
let objLoader = new OBJLoader()
objLoader.load('./static/model.obj',function(obj){
obj.scale.x = obj.scale.y = obj.scale.z = 100
obj.rotation.y = 500
let mesh =obj
mesh.position.y = -50
_this.scene.add(mesh)
})
})</code></pre><p><strong>第二种方法使用vue-3d-model组件</strong></p><p>npm install vue-3d-model --save</p><p>引入组件:</p><pre><code>import { ModelObj } from 'vue-3d-model'
components: {
ModelObj
},
data() {
return {
publicPath: process.env.BASE_URL
}
},</code></pre><p>使用组件:</p><pre><code> <model-obj
id="place"
:src="`${publicPath}model/model.obj`"
:mtl="`${publicPath}model/material.mtl`"
backgroundColor="rgb(0,0,0)"
:scale="{ x: 0.8, y: 0.8, z: 0.8 }"
>
</model-obj></code></pre><p> 一般使用这个会存在找不到obj模块的情况,这是因为经过了webpack的处理,需要把.obj文件放到vue处理静态文件的文件夹中,vue-cli3是放在static文件夹下,但是vue-cli3及之后就需要放到public文件夹下,并且在组件中通过process.env.BASE_URL+public文件夹下的.obj文件的路径来引用</p><h4>在uniapp中使用threeJS加载Obj模型</h4><pre><code>
<template>
<view id="d3Container" ref="mainContent">
</view>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader'
import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
// import { ModelObj } from 'vue-3d-model'
export default {
name: 'ThreePage',
components: {
// ModelObj
},
data () {
return {
publicPath: process.env.BASE_URL,
mesh: null,
camera: null,
scene: null,
originX: 20,
originY: 0,
originZ: 20,
renderer: null,
controls: null,
animationId: null
}
},
mounted () {
this.init()
},
methods: {
destroyed () {
this.clear()
},
init () {
this.createScene() //创建场景
this.loadLoader() //加载P模型
this.createLight() //创建光源
this.createCamera() //创建相机
this.createRender() //创建渲染器
this.createControls() //创建空间对象
this.render() //渲染
},
clear () {
this.mesh = null
this.camera = null
this.scene = null
this.renderer = null
this.controls = null
cancelAnimationFrame(this.animationId)
},
createScene () {
this.scene = new THREE.Scene()
},
loadLoader () {
let MTLloader = new MTLLoader();
let loader = new OBJLoader()
MTLloader.load('/model/material.mtl', materials => {
loader.setMaterials(materials) //设置obj使用的材质贴图
loader.load('/model/model.obj', geometry => {
// 如果obj模型是由多个子模型构成的模型组合,调用.traverse来对每个子模型进行处理
geometry.traverse(function (child) {
if (child.isMesh) {
// 设置为false,不管渲染对象是否在摄像机的视椎体中,都会渲染对象
child.frustumCulled = false
// 开启投射阴影
child.castShadow = true
/* 设置通道颜色 */
child.material.emissive = child.material.color
/* 设置纹理 */
child.material.emissiveMap = child.materials.map
}
})
/* mesh:添加到场景中的对象 */
this.mesh = geometry
this.mesh.scale.set(0.5, 0.5, 0.5) //设置大小比例
this.scene.add(this.mesh)
})
})
},
/**
* @description: 创建光源
*/
createLight () {
// 环境光
let pointColor = '#fff'
/* 创建光 */
const ambientLight = new THREE.AmbientLight(0x222222, 0.35)
this.scene.add(ambientLight)
/* 创建聚光灯 */
const spotLight = new THREE.SpotLight(0xffffff)
spotLight.position.set(50, 50, 50)
// 平行光开启阴影
spotLight.castShadow = true
/* 接受投影效果 */
spotLight.receiveShadow = true
this.scene.add(spotLight)
},
// 创建相机
createCamera () {
const element = this.$refs.mainContent
const width = element.clientWidth
const height = element.clientHeight
this.cWidth = width
this.cHeight = height
/* 窗口宽高比 */
const k = width / height
this.aspect = k
this.camera = new THREE.PerspectiveCamera(35, k, 1, 10000)
/* 设置相机位置 */
this.camera.position.set(this.originX, this.originY, this.originZ)
// 如果需要改变默认的up值,需要先重置up值
this.camera.up.set(0, 0, 1)
/* 设置相机方向 */
this.camera.lookAt(
new THREE.Vector3(this.originX, this.originY, this.originZ)
)
this.scene.add(this.camera)
},
createRender () {
const element = this.$refs.mainContent
this.renderer = new THREE.WebGLRenderer({
antialias: true, //是否执行抗锯齿
alpha: true, //画布是否包含alpha(透明度)缓冲区,默认false
/* 是否保留缓冲区直到手动清除或覆盖,默认false */
preserveDrawingBuffer: true
})
/* 设置渲染区域尺寸 */
this.renderer.setSize(element.clientWidth, element.clientHeight)
/* 显示阴影 */
this.renderer.shadowMap.enabled = true
// 阴影类型
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
this.renderer.setClearColor(new THREE.Color(0xeeeeee)) //设置canvas的背景颜色
element.innerHTML = ''
element.appendChild(this.renderer.domElement)
},
render () {
this.animationId = requestAnimationFrame(this.render) //旋转动画
this.renderer.render(this.scene, this.camera)
// 更新控制器
this.controls.update()
},
createControls () {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
},
onWindowResze () {
console.log('====================================')
console.log('获取屏幕尺寸')
console.log('====================================')
this.camera.aspect = this.aspect
/* 重新计算相机对象的投影矩阵 */
this.camera.updateProjectionMatrix()
}
}
}
</script>
<style lang="scss" scoped>
#d3Container {
width: 90vw;
height: 90vh;
z-index: 888;
border: 1px black solid;
}
</style>
</code></pre><pre><code><template>
<div ref="container" class="container"></div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export default {
data () {
return {
scene: null,
camera: null,
renderer: null
}
},
methods: {
init () {
this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(
90,
document.body.clientWidth / document.body.clientHeight,
0.1,
100
)
this.camera.position.set(0, 0, 3)
this.renderer = new THREE.WebGLRenderer()
this.renderer.setSize(
document.body.clientWidth,
document.body.clientHeight
)
this.$refs.container.appendChild(this.renderer.domElement)
var controls = new OrbitControls(this.camera, this.renderer.domElement)
// 创建立方体 BoxGeometry(width, height, depth)长 宽 深度
const geometry = new THREE.BoxGeometry(1, 1, 1)
// MeshBasicMaterial 一种非常简单的材质,设置材质的颜色
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const cube = new THREE.Mesh(geometry, material)
this.scene.add(cube)
this.loop()
},
loop () {
requestAnimationFrame(this.loop)
this.renderer.render(this.scene, this.camera)
}
},
mounted () {
this.init()
}
}
</script>
<style lang="scss" scoped>
.container {
width: 100vw;
height: 100vh;
}
</style>
</code></pre><h3>用例</h3><pre><code>
<template>
<view class="login-container">
<view id="login-three-container" ref="container"></view>
</view>
</template>
<script>
import * as THREE from 'three'
import {GUI} from "three/examples/jsm/libs/dat.gui.module"
import _ from 'lodash'
import Stats from 'three/examples/jsm/libs/stats.module'
export default {
data () {
return {
/* 容器 */
container: null,
/* 声明视口的宽度 */
width: null,
/* 声明视口的高度 */
height: null,
// 盒模型的深度
depth: 1400,
/* 场景 */
scene: null,
renderer: null,
/* 球组 */
sphere_Group: null,
/* 声明性能监控 */
stats: new Stats(),
/* 声明云流动的渲染函数1 */
renderCloudMove_first: null,
/* 声明云流动的渲染函数2 */
renderCloudMove_second: null,
/* 声明流动的云对象1(包含路径,云实例) */
cloudParameter_first: null,
/* 声明流动的云对象2 (包含路径,云实例) */
cloudParameter_second: null,
/* 声明相机在z轴的位置 */
zAxisNumber: null,
/* 声明相机 */
camera: null,
/* 声明相机目标点 THREE.Vector3三维向量 分别是x,y,z值*/
cameraTarget: new THREE.Vector3(0, 0, 0),
/* 声明球体几何 */
sphereGeometry: null,
/* 声明完整球 */
sphere: null,
/* 声明点的参数 */
parameters: null,
material: [],
/* 声明点在z轴上移动的进度 */
zprogress: null,
/* 声明同上(第二个几何点) */
zprogress_second: null,
/* 盒模型的深度 */
depth: 1400,
/* 声明粒子1的初始位置 */
particles_init_position: null,
/* 声明粒子1 */
particles_first: [],
/* 声明粒子2 */
particles_second: [],
/* 声明调试对象 */
gui:new GUI()
}
},
mounted () {
this.container = document.getElementById('login-three-container')
this.width = this.container.clientWidth
this.height = this.container.clientHeight
this.initScene()
this.initSceneBg()
this.initCamera()
this.initLight()
this.initSphereModal()
this.initSphereGroup()
this.particles_init_position = -this.zAxisNumber - depth/2
this.zprogress = this.particles_init_position
this.zprogress_second = this.particles_init_position*2
this.particles_first = this.initSceneStar(this.particles_init_position)
this.particles_second = this.initSceneStar(this.zprogress_second)
this.cloudParameter_first = this.initTubeRoute([
new THREE.Vector3(-this.width/10,0,-this.depth/2),
new THREE.Vector3(-this.width/4,this.height/8,0),
new THREE.Vector3(-this.width/4,0,this.zAxisNumber)
],400,200)
this.cloudParameter_second = this.initTubeRoute(
[
new THREE.Vector3(this.width / 8, this.height / 8, -this.depth / 2),
new THREE.Vector3(this.width / 8, this.height / 8, this.zAxisNumber)
],
200,
100
)
this.initRenderer()
this.initOrbitControls() //控制器必须放在renderer函数后面
this.animate()
},
Params(){
this.color = "#000"
this.length =10
this.size = 3
this.visible = true
this.x = 0
this.y = 0
this.z = 100
this.widthSegments = 64
this.heightSegments = 32
this.radius = 16
},
initGUI(){
const params = new this.Params()
this.gui.add(params,'x',-1500,1500).onChange(x=>{
/* 点击颜色面板,e为返回的10进制颜色 */
this.Sphere_Group.position.x = x
})
this.gui.add(params,'y',-50,1500).onChange(y=>{
this.Sphere_Group.position.y = y
})
this.gui.add(params,'z',-200,1000).onChange(z=>{
this.Sphere_Group.position.z = z
})
this.gui.add(params,'widthSegments',0,64).onChange(widthSegments=>{
this.sphereGeometry.parameters.widthSegments = widthSegments
})
this.gui.add(params,'heightSegments',0,32).onChange(heightSegments=>{
this.sphereGeometry.parameters.heightSegments = heightSegments
})
this.gui.add(params,'radius',5,30).onChange(radius=>{
this.sphereGeometry.parameters.radius = radius
this.renderer.render(this.scene,this.camera)
})
},
/* 初始化相机 */
initCamera () {
/*
方式1: 固定视野的距离,求满足完整的视野画面需要多大的视域角度
tan 正切值(直角边除以临边)
const mathTan_value = width/2/depth
视域角度 Math.atan返回的是改角的弧度值
弧度等于角度度乘于PI再除于180
角度等于弧度乘于180再除于PI
const fov_angle = (Math.atan(mathTan_value)*180)/Math.PI
创建透视相机
new THREE.PerspectiveCamera(for_angle,width/height,1,depth) //角度 长宽比 近端面 远端面
场景是一个矩形容器(坐标(0,0,0)是矩形容器的中心),相机能看到的距离是depth
camera.position.set(0,0,depth/2)
使用透视相机
参数值分别是:
fov(filed of view) - 摄像机视椎体垂直视野角度
aspect - 摄像机视椎体长宽比
near - 摄像机视锥体近端面
far --摄像机视椎体远端面
这里需要注意:透视相机鱼眼效果,如果视域越大,边缘变形越大
为了避免变形,可以将fov角度设置小一些,距离拉远一些
固定视域角度,求需要多少距离才能满足完整的视野画面
15度等于 (Math.PI / 12)
*/
const fov = 15
const distance = this.width / 2 / Math.tan(Math.PI / 12)
this.zAxisNumber = Math.floor(distance - this.depth / 2)
this.camera = new THREE.PerspectiveCamera(
fov,
this.width / this.height,
1,
30000
)
// 躺着看
this.camera.position.set(0, 0, this.zAxisNumber)
// camera.lookAt坐标中心 侧着看
this.camera.lookAt(this.cameraTarget)
},
// 初始化球体模型
initSphereModal () {
/* 创建一个光亮表面的材质效*/
let material = new THREE.MeshPhongMaterial()
/* 将纹理赋值给材质 */
material.map = new THREE.TextureLoader().load(
require('./images/earth_bg.png')
)
/* 设置材质透明度 */
meterial.blendDstAlpha = 1
// 几何体 球体 (半径 经度切片数 纬度切片数)
this.sphereGeometry = new THREE.SphereGeometry(50, 64, 32)
// 模型
this.sphere = new THREE.Mesh(this.sphereGeometry, material)
},
initSphereGroup () {
/*
THREE.Group声明组容器
可以将一系列的mesh模型组合
*/
this.Sphere_Group = new THREE.Group()
this.Sphere_Group.add(this.sphere)
this.Sphere_Group.position.x = -400
this.Sphere_Group.position.y = 200
this.Sphere_Group.position.z = -200
this.scene.add(this.Sphere_Group)
},
/* 光源 */
initLight () {
/* 添加光源 AmbientLight光源的颜色将会影响到所有物体的每一面,没有特别来源,没有阴影 */
const ambientLight = new THREE.AmbientLight(0xffffff, 1)
// 右下角点光源 (PointLight创建点光源,类似照明弹,迎面亮一点,背面暗一些,没有阴影)
const light_rightBottom = new THREE.PointLight(0x0655fd, 5, 0)
/* 设置发光位置 */
light_rightBottom.position.set(0, 100, -200)
this.scene.add(light_rightBottom)
this.scene.add(ambientLight)
},
// 渲染器
initRenderer () {
// 开启抗锯齿
// 在css中设置背景色透明显示渐变色
this.renderer = new THREE.WebGLRenderer({
antialias: true, //抗锯齿
alpha: true //alpha透明系数
})
// 定义渲染器的尺寸,在这里它会填满整个屏幕
this.renderer.setSize(this.width, this.height)
this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
this.container.appendChild(renderer.domElement)
this.container.appendChild(this.stats.dom)
},
/* 初始化场景 */
initScene () {
this.scene = new THREE.Scene()
/* 场景中添加雾的效果,Fog参数分别代表雾的颜色,开始雾化的视线距离,刚好雾化至看不见的视线距离 */
this.scene.fog = new THREE.Fog(0x000000, 0, 10000)
},
initRenderer () {},
initSceneBg () {
/* 纹理加载器 加载纹理 */
new THREE.TextureLoader().loader(require('./images/sky.png'), texture => {
/* 创建立方体 */
const geometry = new THREE.BoxGeometry(
this.width,
this.height,
this.depth
)
/* map:为材质设置纹理贴图 side:设定在几何体的哪个面应用材质 THREE.BackSide:内面*/
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.BackSide
}) //创建基础为网格基础材料
const mesh = new THREE.Mesh(geometry, material)
this.scene.add(mesh)
})
},
// 初始化轨道控制器
initOrbitControls () {
const controls = new initOrbitControls(
this.camera,
this.renderer.domElement
)
// enabled设置为true是可以使用鼠标控制视角
controls.enabled = false
controls.update()
},
/* 初始化流动路径 */
initTubeRoute (route, geometryWidth, geometryHeight) {
// CatmullRomCurve3函数用来创建曲线 false:是否闭合
const curve = new THREE.CatmullRomCurve3(route, false)
/* 创建管道几何体
path:样条曲线
segments:管道的分段数
radius:管道半径
radiusSegments:管道截面圆的分段数
closed:是否收尾连接
*/
const tubeGeometry = new THREE.TubeGeometry(curve, 100, 2, 50, false)
// 创建一个简单的材质
const tubeMaterial = new THREE.MeshBasicMaterial({
opacity: 0,
transparent: true
})
const tube = new THREE.Mesh(tubeGeometry, tubeMaterial)
this.scene.add(tube)
/* 创建一个矩形 */
const clondGeometry = new THREE.PlaneGeometry(
this.geometryWidth,
this.geometryHeight
)
/* 纹理加载 */
const textureLoader = new THREE.TextureLoader()
const cloudTexture = textureLoader.load(require('./images/cloud.png'))
// https://juejin.cn/post/6956852013758054436
const clondMaterial = new THREE.MeshBasicMaterial({
map: cloudTexture, //添加纹理贴图
/* THREE.AdditiveBlending:饱和度叠加渲染 */
blending: THREE.AdditiveBlending, //blending:物体的材质如何与背景融合
depthTest: false, //是否使用像素深度
transparent: true //开启透明度
})
const cloud = new THREE.Mesh(clondGeometry, clondMaterial)
this.scene.add(cloud)
return {
clound,
curve
}
},
/* 初始化场景星星效果 */
initSceneStar (initZposition) {
/* 创建一个geometry实例 控制物体的几何形状*/
const geometry = new THREE.BufferGeometry()
const vertices = []
const pointsGeometry = []
/* 纹理加载*/
const textureLoader = new THREE.TextureLoader()
const sprite1 = textureLoader.load(require('./images/starflake1.png'))
const sprite2 = textureLoader.load(require('./images/starflake2.png'))
this.parameters = [
[[0.6, 100, 0.75], sprite1, 50],
[[0, 0, 1], sprite2, 20]
]
// 初始化500个节点
for (let i = 0; i < 500; i++) {
/*
const x = Math.random()*2*width-width
等价
THREE.MathUtils.randFloatSpread(width)
在(-width/2,width/2)之间的一个随机浮点数
*/
const x = THREE.MathUtils.randFloatSpread(width)
const y = _.radom(0, height / 2)
const z = _.radom(-this.depth / 2, this.zAxisNumber)
vertices.push(x, y, z)
}
/*给当前几何体设置属性
BufferGeometry 缓冲区类型几何体
BufferAttribute用于存储与BufferGeometry相关联的attribute(顶点位置向量,面片索引,法向量,UV坐标,以及任何自定义的attribute)
*/
geometry.setAttribute(
'position',
new THREE.Float32BufferAttribute(vertices, 3)
)
/* 创建2种不同材质的节点 500*2 */
for (let i = 0; i < this.parameters.length; i++) {
const color = this.parameters[i][0]
const sprite = this.parameters[i][1]
const size = this.parameters[i][2]
/*
PointsMaterial点材质,点模型的时候需要
*/
this.materials[i] = new THREE.PointsMaterial({
size,
map: sprite, //添加纹理贴图
blending: THREE.AdditiveBlending, //物体的颜色与背景叠加
depthTest: true, //是否使用像素深度
transparent: true //是否透明
})
/*
setHSL(色相,饱和度,亮度)
*/
this.materials[i].color.setHSL(color[0], color[1], color[2])
/* 创建粒子系统对象 */
const particles = new THREE.Points(geometry, materials[i])
// 旋转
particles.rotation.x = Math.random() * 0.2 - 0.15
particles.rotation.y = Math.radom() * 0.2 - 0.15
particles.rotation.z = Math.radom() * 0.2 - 0.15
particles.position.setZ(initZposition)
pointsGeometry.push(particles)
this.scene.add(particles)
}
return pointsGeometry
},
/* 旋转星球的自转 */
renderSphereRotate () {
if (this.sphere) {
this.Sphere_Group.rotateY(0.001)
}
},
/* 渲染星星的运动 */
renderStarMove () {
const time = Date.now() * 0.00005
this.zprogress += 1
this.zprogress_second += 1
if (this.zprogress >= this.zAxisNumber + this.depth / 2) {
this.zprogress = this.particles_init_position
} else {
this.particles_first.forEach(item => {
item.position.setZ(this.zprogress)
})
}
if (this.zprogress_second >= this.zAxisNumber + this.depth / 2) {
this.zprogress_second = this.particles_init_position
} else {
this.particles_second.forEach(item => {
item.position.setZ(this.zprogress_second)
})
}
for (let i = 0; i < this.materials.length; i++) {
const color = this.parameters[i][0]
const h = ((360 * (color[0] + time)) % 360) / 360
this.meterial[i].color.setHSL(
color[0],
color[1],
parseFloat(h.toFixed(2))
)
}
},
initCloudMove (cloudParameter, speed, scaleSpeed, maxScale, startScale) {
let cloudProgress = 0
return () => {
if (startScale < maxScale) {
startScale += scaleSpeed
cloudParameter.cloud.scale.setScalar(startScale)
}
if (cloudProgress > 1) {
cloudProgress = 0
startScale = 0
} else {
cloudProgress += speed
if(cloudParameter.curve){
const point = cloudParameter.curve.getPoint(cloudProgress)
if(point&&point.x){
cloudParameter.cloud.position.set(point.x,point.y,point.z)
}
}
}
}
},
/* 动画刷新 */
animate(){
requestAnimationFrame(this.animate)
this.renderSphereRotate()
this.renderStarMove()
this.renderCloudMove_first()
this.renderCloudMove_second()
renderer.render(this.scene,this.camera)
},
}
</script>
<style lang="scss" scoped>
.login-container {
width: 100%;
height: 100vh;
position: relative;
#login-three-container {
width: 100%;
height: 100%;
}
}
</style>
</code></pre><h2>其他好用的3D技术</h2><p><a href="https://link.segmentfault.com/?enc=ZIgFmmXUpVNqt0rjpIVjFA%3D%3D.sys8hxPeSVsMqLnMzLn0yFhpJ%2BC9MddRKSKdgkykWRA%3D" rel="nofollow">xeogl</a></p>
Js高级API
https://segmentfault.com/a/1190000043333315
2023-01-13T17:31:38+08:00
2023-01-13T17:31:38+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h3>Decorator装饰器</h3><p><strong>针对属性 / 方法的装饰器</strong></p><pre><code>// decorator 外部可以包装一个函数,函数可以带参数
function Decorator (type) {
/**
* 这里是真正的decorator
* @description: 装饰的对象的描述对象
* @target:装饰的属性所述类的原型,不是实例后的类。如果装饰的是Animal的某个属性,这个target就是Animal.prototype
* @name 装饰的属性的key
*/
return function (target, name, desciptor) {
// 因为babel的缘故 通过value并不能获取值,以此可以获取实例化的时候此属性的默认值
let v = desciptor.initializer && desciptor.initializer.call(this)
// 返回一个新的描述对象,或者直接修改desciptor也可以
return {
enumerable: true, //可以遍历
configurable: true, //可以删除
get: function () {
return v
},
set: function (c) {
v = c
}
}
}
}
// 上面的不能和业界商用的Decorator混用
function Check (type) {
return function (target, name, desciptor) {
let v = desciptor.initializer && desciptor.initializer.call(this)
// 将属性名字以及需要的类型的对应关系记录到类的原型上
if (!target.constructor._checkers_) {
// 将这个隐藏属性定义成no enumerable,遍历的时候是取不到的
Object.defineProperty(target.constructor, '_checkers_', {
value: {},
enumerable: false,
writable: true,
configurable: true
})
}
target.constructor._checkers_[name] = {
type: type
}
return desciptor
}
}
// 装饰函数的第一个参数 target 是包装属性所属的类的原型(prototype)
// 也就是把对应关系挂载到了开发定义的子类上。</code></pre><h4>vue中使用Decorator</h4><ul><li><em>ts开发一定对vue-property-decorator不会感到陌生,这个插件提供了许多装饰器</em></li><li><em>在methods里面的方法上面使用装饰器,这时候装饰器的target对应的是methods。</em></li><li><em>可以在生命周期钩子函数上面使用装饰器,这时候target对应的是整个组件对象。</em></li></ul><pre><code> import {log,confirmation} from "./test"
methods: {
@log()
name() {
console.log("获取数据");
},
@confirmation('此操作将永久删除文件,是否继续?')
deleteFile(data){
//删除文件操作
}
},
mounted () {
this.name()
}</code></pre><p><strong>test.js</strong></p><pre><code> import {MessageBox}from "element-ui"
export function confirmation(message){
return function(target,name,descriptor){
let oldValue = descriptor.value
descriptor.value = function(...args){
MessageBox.confirm(message,'提示').then(oldValue.bind(this,...args)).catch(()=>{})
}
return descriptor
}
}
export function log(){
/**
* @description:
* @param {*} target 对应methods
* @param {*} name 对应属性方法的名称
* @param {*} descriptor 对应属性方法的修饰符
* @return {*}
*/
return function(target,name,descriptor){
console.log(target,name,descriptor);
// 获取实例化的时候此属性的默认值
const fn = descriptor.value
/* 重写 */
descriptor.value = function(...rest){
console.log(`调用${name}方法打印的`);
fn.call(this,...rest)
}
}
}</code></pre><h3>Iterator 迭代器</h3><ul><li>目的是为不同的数据结构提供统一的数据访问机制 主要为for of 服务的 (<em>当for of执行的时候,循环过程中会自动调用这个对象上的迭代器方法,依次执行迭代器对象的next方法,并把next返回结果赋值给for of的变量,从而得到具体的值</em>)</li><li>迭代器对象,返回此对象的方法叫做迭代器方法 此对象有一个next方法 每次调用next方法都会返回一个结果值</li><li>这个结果值是一个object 包含两个属性value和done</li><li>value表示具体的返回值 done是布尔类型的,表示集合是否遍历完成或者后续还有可用数据,没有可用返回true, 否则返回false</li><li><p>内部会维护一个指针 用来指向当前集合的位置 每调用一次next方法 指针都会向后移动一个位置(可以想象成数组的索引)</p><p><strong>代码实现</strong></p></li></ul><pre><code> getInterator(list){
var i = 0;
return {
next:function(){
var done = (i>=list.length);
var value = !done ? list[i++]:undefined
return {
done:done,
value:value
}
}
}
}
var it = this.getInterator(['a','b','c'])
console.log(it.next());// {done: false, value: 'a'}
console.log(it.next());//{done: false, value: 'b'}
console.log(it.next());//{done: false, value: 'c'}
console.log(it.next());//{done: true, value: undefined}</code></pre><h5><em>可迭代对象</em></h5><ul><li>Symbol.Iterator是一个表达式 返回Symbol的Iterator属性, 这是一个预定好的类型为Symbol的特殊值</li><li>ES6规定,只要在对象上部署了Iterator接口,具体实现为给对象添加Symbol.Iterator属性,此属性指向一个迭代器方法,这个迭代器会返回一个迭代器对象。</li><li><p>而部署了这个属性,并且实现迭代器方法 返回的对象就是迭代器对象,此时这个对象就是可迭代的 可以被for for遍历</p><p><strong>实现一个可迭代对象</strong></p><pre><code> getIterator(){
let iteratorObj = {
items:[100,200,300],
[Symbol.iterator]: function(){
var self = this
var i =0
return {
next:function(){
var done = (i>=self.items.length)
var value = !done?self.items[i++]:undefined
return {
done:done,
value:value
}
}
}
}
}
for(var item of iteratorObj){
console.log(item); //100 200 300
}
}
//上面的对象就是可迭代对象,可以被for of遍历
this.getIterator()</code></pre><h5><em>for of 中断</em></h5><p>如果for of 循环提前退出,则会自动调用return方法,需要注意的是return 方法必须有返回值,且返回值必须是一个object</p><pre><code> var arr = [100, 200, 300]
arr[Symbol.iterator] = function () {
var self = this
var i = 0
return {
next: function () {
var done = i >= self.length
var value = !done ? self[i++] : undefined
return {
done: done,
value: value
}
},
return (){
console.log('提前退出');
return { //必须返回一个对象
done:true
}
}
}
}
for (var o of arr) {
if(o == 200){
break;
}
console.log(o) // 100 提前退出
}</code></pre><p><strong><em>除了for of 会调用对象的Iterator, 结构赋值也会</em></strong></p><pre><code> var str = '123'
let [a,b]=str
console.log(a,b); // 1 2
let map = new Map()
map.set('q','1')
map.set('a','2')
map.set('b','3')
let [c,d] = map
console.log(c,d); //['q', '1'] ['a', '2']</code></pre><p>因为普通对象不是可迭代对象。</p><p>自定义的可迭代对象进行解构赋值</p><pre><code> var interatorObj = {
items: ['橙', '红', '白'],
[Symbol.iterator]: function () {
let i = 0
let self = this
return {
next: function () {
let done = i >= self.items.length
let value = !done ? self.items[i++] : undefined
return {
done: done,
value: value
}
}
}
}
}
let [a,b] = interatorObj
console.log(a,b); //橙 红</code></pre><p><em>解构赋值的变量的值就是迭代器对象next方法的返回值 且是按顺序返回</em></p><h5>扩展运算符</h5><p><em>// 扩展运算符的执行(...)也会默认调用它的Symbol.iterator方法,可以将当前迭代对象转换为数组</em></p><pre><code>*/\* 字符串 \*/*
var str = "1234"
console.log([...str]);*//【1,2,3,4】转换成数组*
*/\* map对象\*/*
var map = new Map([[1,2],[3,4]])
[...map]*//[[1,2],[3,4]]*
*/\* set对象 \*/*
var set = new Set([1,2,3])
[...set] *//[1,2,3]*</code></pre></li></ul><p>使用普通对象是不可以转换为数组的</p><pre><code>var obj = {name:'zhang'}
[...obj] *//报错*</code></pre><h5>作为数据源</h5><p>作为一些数据的数据源 比如某些参数是数组的API方法,都会默认的调用自身的迭代器</p><p>例如 map set Array.from等</p><p>为了证明,先把一个数组的默认迭代器给覆盖掉</p><pre><code>var arr= [100,200,300]
arr[Symbol.iterator]=function(){
var self = this
var i = 0;
return {
next:function(){
var done = (i>=self.length)
var value = !done?self[i++]:undefined
return {
done:done,
value:value + '前端'
}
}
}
}
</code></pre><p><strong>用for of</strong></p><pre><code> for(var o of arr){
console.log(o);//100前端 200前端 300前端
}</code></pre><p><strong>生成set对象</strong></p><p></p><pre><code> var set = new Set(arr)
set*//Set(3) {'100前端', '200前端', '300前端'}*</code></pre><p> </p><p> <strong><em>调用Array.from方法</em></strong></p><pre><code> Array.from(arr) *//['100前端', '200前端', '300前端']*</code></pre><h5><em>yield</em>*关键字</h5><p> <em>yield</em>*后面跟的是一个可遍历的结构,执行时也会调用迭代器函数</p><p></p><pre><code> let foo = function*(){
*yield* 1;
*yield** [1,2,3];
*yield* 5;
}</code></pre><h5>判断对象是否可迭代</h5><p> <em>既然可迭代对象的规则必须在对象上部署Symbol.iterator属性,那么就可以依次来判断是否为可迭代对象,然后就知道是否能使用for of 取值了</em></p><p></p><pre><code> function isIterable(*object*){
*return* typeof *object*[Symbol.iterator] === 'function'
}
console.log(isIterable('asdd'));*//true*
console.log(isIterable([1,2,3]));*//true*
console.log(isIterable(new Map()));*//true*
console.log(isIterable(new Set()));*//true*
console.log(isIterable(new WeakMap()));*//false*
console.log(isIterable(new WeakSet()));*//false</code></pre><h5>Generate生成器*</h5><pre><code> let obj = {
*[Symbol.iterator]("Symbol.iterator"){
*yield* 'hello';
*yield* 'world';
}
}
*for*(let x of obj){
console.log(x);
} *//hello world</code></pre><h3>Generator</h3><p>Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同</p><p>Generator函数将javascript异步编程带入了一个全新的阶段</p><p> <strong>声明</strong></p><p> <em>与函数声明类似,不同的是function关键字与函数名之间有一个星号,以及函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是产出)</em></p><p></p><pre><code class="javascript"> function* foo(){
yield 'result1'
yield 'result2'
yield 'result3'
}
const gen = foo()
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);</code></pre><p><strong>vue中使用</strong></p><pre><code> textMy () {
const gen = this.foo()
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
},
* foo(){
yield 'result1'
yield 'result2'
yield 'result3'
}</code></pre><p> 执行Generator会返回一个对象,而不是像普通函数返回return 后面的值</p><p> 调用指针next方法,会从函数的头部或上一次停下来的位置开始执行,直到遇到下一个yield表达式或者return语句暂停,也就是执行yeild这一行</p><p> value就是执行yield后面的值 done表示函数是否执行完毕</p><p></p><pre><code>console.log(g.next()); *//{value: 7, done: false}*
console.log(g1.next()); *//{value: 2, done: false}*
console.log(g.next());*//{value: undefined, done: true}</code></pre><p> 因为最后一行 <em>return</em> y 被执行完成 所以done为true</p><p> 调用Generator函数后,该函数并不执行,返回的也不是函数运行结果 而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object).必须调用遍历器对象的next方法</p><p> 使得指针移向下一个状态</p><pre><code> function* fetch(){
*yield* ajax('aaa')
*yield* ajax('bbb')
*yield* ajax('ccc')
}
let gen = fetch()
let res1 = gen.next() *//{value:'aaa',done:false}*
let res2 = gen.next() *//{value:'bbb',done:false}*
let res3 = gen.next() *//{value:'ccc',done:false}*
let res4 = gen.next() *//{value:undefined,done:true} //done为true表示执行结果</code></pre><p>由于Generator函数返回的是遍历器对象 只有调用next方法才会遍历下一个内部状态 所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志</p><p> 遍历器对象的next方法运行逻辑:</p><p> 1 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的表达式的值,作为返回对象的value属性值</p><p> 2. 如果下次调用next方法时,再继续往下执行,直到遇到下一个yield表达式</p><p> 3 如果没有再遇到新的yiled表达式 就一直运行到函数结束 </p><p> 4 如果该函数没有return 语句 则返回对象的value属性值就是underfined</p><p> yiled表达式本身没有返回值 或者说总返回underfined next 方法可以带一个参数,该参数就会被当做上一个yield表达式的返回值</p><p> 一个函数可以有多个yield,但是只能有一个return</p><h3>Proxy</h3><p> - Proxy用于创建对象的代理,用于监听对象的相关操作。代理对象可以监听我们对原对象的操作</p><p> - 在实例化Proxy对象时,第二个参数传入的是捕获器集合,我们在其对象内定义一个get捕获器,用于监听获取对象值的操作</p><p> - objProxy 对象的拦截器中set捕获器,用于监听对象的某个属性被设置时触发</p><pre><code>// 定义一个普通的对象
const obj = {
name:'_isLand'
}
// 代理obj这个对象 并传入get捕获器
const objProxy = new Proxy(obj,{
// get捕获器
get:function(target,key){
console.log(`触发对象捕获${key}`);
return target[key]
},
set:function(target,key,val){
console.log(`捕获到对象设置${key},新值为${val}`);
return target[key] = val
}
})
objProxy.name = 'liming'
// 通过代理对象操作obj对象
console.log(objProxy.name);//触发对象捕获name _isLand
// 捕获到对象获取name属性值的操作</code></pre><p>如果不想这个属性被设定成这个值,你可以抛出异常告诉开发者,这个值不能设定</p><pre><code> set:function(target,key,val){
if(key==='age' && typeof val ==='number'){
return target[key] = val
}else{
throw new TypeError('该属性的值必须是number类型')
}
}</code></pre><p>监听对象是否调用了getPrototypeof操作,使用getPrototypeOf捕获器</p><pre><code> getPrototypeOf:function(){
console.log('监听到对象的protptypeOf操作');
}</code></pre><p>Proxy一共有十三个捕获器,用于我们对 对象或者函数的方法调用的监听</p><p><img src="/img/remote/1460000043472472" alt="请添加图片描述" title="请添加图片描述"></p><h5>this指向的问题</h5><p> Proxy对象可以对我们的目标对象进行访问,但没有做任何拦截时,也不能保证与目标行为一致,因为目标对象 内部的this会自动改变为Proxy代理对象</p><pre><code> let obj = {
name:'lili',
foo:function(){
return this === objProxy
}
}
let objProxy = new Proxy(obj,{})
console.log(obj.foo()); //false
console.log(objProxy.foo()); //true</code></pre><h5>对象监听</h5><p> 在vue2中的watchApi是使用的Es5的Object.defineProperty(对象属性描述符)对对象监听,将一个对象进行遍历,</p><p> 并设定setter。getter方法进行监听和拦截</p><pre><code>const obj = {
name: 'liki',
age: 12
}
Object.keys(obj).forEach(key => {
let val = obj[key]
Object.defineProperty(obj, key, {
get: function () {
console.log('触发get')
return val
},
set: function (newVal) {
console.log('触发set')
val = newVal
}
})
})
obj.age = 100
console.log(obj.name);</code></pre><ul><li>Object.defineProperty的设计初衷并不是为了去拦截拦截一个对象中的属性,且他也实现不了更加丰富的操作,例如删除、添加属性等操作</li></ul><p> - 所以在Es6中新增了Proxy,对象,用于监听Object,Function的操作。</p><p> - 在Vue3框架中的响应式原理也是用到了Proxy对象进行对属性的监听操作</p><p><strong>proxy是一个代理对象 他可以代理我们对原目标的操作。相比Object.defineProperty方法 Proxy监听的事件更加方便</strong></p><h3>Reflect隐射对象</h3><blockquote><p>Reflect是一个对象,翻译过来就是反射的意思,它提供了很多操作Javascript对象的方法,是为弥补Object中对象的一些缺陷。且所有的属性和方法都是静态的</p><p>最初,js的一些内部方法都是放在Object这个对象上的,比如getPrototye,defineProperty等API,</p><p>in、delete操作符都放在了Object对象上。但是Object作为一个构造函数,这些方法都放在Object上并不合适,</p><p>所以ES6之后的内部新方法都在Reflect对象中。Refelect不是构造函数</p><p>使用Reflect对象操作Object对象</p><p>Reflect对象让我们操作Object对象不再是通过点语法 而是变成了函数行为</p><p>所以 获取对象属性可以使用Reflect.get方法,将对象的属性赋值使用Reflect.set方法</p></blockquote><pre><code> const obj = {
name:'lili',
age:12
}
/* 获取对象的属性值 */
console.log(obj.name);
console.log(Reflect.get(obj,'name'));
// 对对象的属性赋值
obj.name = 'lala'
Reflect.set(obj,'name','lala')
console.log(Reflect.get(obj,'name'));
// 判断一个对象中是否有该属性
console.log('name' in obj);
console.log(Reflect.has(obj,'name'));</code></pre><p>Reflect中的方法</p><p><img src="/img/remote/1460000043472473" alt="请添加图片描述" title="请添加图片描述"></p><p>在返回值方面Reflect对象中的方法设计的更加合理 比如defineProperty方法,如果没有将属性设置</p><p>成功,在Reflect中会返回boolean值,而Object对象中如果没有定义成功则会抛出TypeError</p><h5>Reflect搭配Proxy</h5><p> Reflect对象中的方法和Proxy对象中的方法对应的,Proxy对象中的方法也能在Reflect对象中调用</p><p> 通常我们将Reflect对象搭配Proxy一起使用</p><p>在下面Proxy对象中get,set捕获器多了一个recerive参数 这是这两个捕获器特有的 代表的是当前的代理对象</p><p> 当Proxy和Reflect搭配使用时 Proxy对象会拦截对应的操作 后者完成对应的操作,如果传入receiver,那么Reflect.get属性</p><p> 会触发Proxy.definProperty捕获器。如果没有传入receive参数 则不会触发defineProperty捕获器</p><pre><code> let obj = {
name:'lili'
}
const objProxy = new Proxy(obj,{
get:function(target,key,receiver){
return Reflect.get(target,key,receiver)
},
set:function(target,key,val,receiver){
return Reflect.set(target,key,val,receiver)
},
defineProperty:function(target,key,attr){
console.log('defineProperty',target,key,attr); //defineProperty {name: 'lili'} name {value: 'ppp'}
return Reflect.defineProperty(target,key,attr)
}
})
objProxy.name = 'ppp'
console.log(objProxy.name);
//传入在我们获取代理对象中的name属性时,当Reflect有receive</code></pre><p><strong>总结</strong></p><p> - Reflect 对象中集合了javascript内部方法</p><p> - 操作Object对象的方式变成了函数行为</p><p> - Reflect 对象中的方法返回结果更加合理</p><h3>ES6中的class</h3><p> 在ES6之前,js语法是不支持类的,导致面向对象编程无法直接使用,而是通过function来实现模拟类,随着js的更新,ES6中出现了Class,</p><p> 用于定义类</p><p></p><pre><code> class Animal {}
const Animal = class {}</code></pre><h5>类的构造函数</h5><p> <em>每一个类都可以有一个自己的构造函数,这个名称是固定的construtor,当我们通过new 调用一个类时,这个类就会调用自己的constructor方法(构造函数)</em></p><p> - 它用于创建对象时给类传递一些参数</p><p> - 每一个类只能有一个构造函数</p><p> - 通过 new 调用一个类时 会调用构造函数 执行如下操作</p><p> 1 在内存中开辟一块新的空间用于创建新的对象</p><p> 2 这个对象内部的_proto_属性会被赋值为该类的prototype属性</p><p> 3 构造函数内部的this,指向创建出来的新对象</p><p> 4 执行构造函数内部的代码</p><p> 5 如果函数没有返回值 则返回this</p><pre><code> class Animal {
constructor(name){
this.name = name
}
}
var a = new Animal("ABC")
console.log(a); //Animal {name: 'ABC'}
</code></pre><p>class 中定义的constructor,这个就是构造方法</p><p>而this代表的是实例对象</p><p>这个class 可以看作是构造函数的另一种写法 因为它和它的构造函数的相等的,即是 类本身指向构造函数</p><pre><code>console.log(Animal === Animal.prototype.constructor); *//true*</code></pre><p><strong>所以其实类上的所有方法都会放到prototype属性上</strong></p><h5>类中的属性</h5><p> <strong>实例属性</strong></p><p> 实例的属性必须定义在类的方法中</p><pre><code>class Animal {
constructor(name,height,weight){
this.name = name
this.height = height
this.weight = weight
}
}</code></pre><p><strong>静态属性</strong></p><p> 当我们把一个属性赋值给类本身,而不是赋值给它的prototype,这样子的属性被称之为静态属性(static)</p><p> 静态属性直接通过类来访问 无需在实例中访问</p><pre><code>class Foo{
static name = "liLI"
}
console.log(Foo.name);</code></pre><p><strong>私有属性</strong></p><p> 私有属性只能在类中读取,写入,不能通过外部引用私有字段</p><pre><code> class Person{
#age;
constructor(age,name){
this.#age = age
this.name = name
}
}
var a = new Person(17,'lili')
console.log(a); //Person {name: 'lili'}
console.log(a.name);//lili
console.log(a.age);//undefined
console.log(a.#age);// Private field '#age' must be declared in an enclosing class
</code></pre><p><strong>console.log(Object.getOwnPropertyDescriptors(a));</strong></p><p>通过getOwnPropertyDescriptors获取属性同样获取不到</p><pre><code> {
name: {
value: '_island',
writable: true,
enumerable: true,
configurable: true
}
}</code></pre><p><strong>在ES6之前,我们定义类中的方法是类中的原型上定义的 防止类中的方法重复在多个对象中</strong></p><p></p><pre><code> function Animal{}
Animal.prototype.eating = function(){
console.log(this.name + 'eating');
}</code></pre><p> <strong>在Es6中定义类中的方法更简洁 直接在类中定义即可 这样的写法优雅可读性也强</strong></p><pre><code> class Animal{
eating(){
console.log(this.name + 'eating');
}
}</code></pre><h5>静态方法</h5><p> <strong>静态方法是与静态属性是一样的 在方法前面加上stati关键字声明,之后调用这个方法时不需要通过类的实例来调用,可以直接通过类名来调用它</strong></p><p></p><pre><code> class Animal{
static creatName(*name*){
*return* *name*
}
}
var a2 = Animal.creatName('lll')
console.log(a2); *//lll</code></pre><h5>私有方法</h5><p> <em>在面向对象中 私有方法是一个常见需求 ES6中没有提供,可以通过某个方法实现它</em></p><pre><code> class Foo{
_getBoodType(){
*return* '0'
}
}</code></pre><p> <em>需要注意的是,通过下划线开头通常我们会局限它是一个私有方法 但是在类的外部还是可以正常调用到这个方法的</em></p><h5>类的继承</h5><blockquote><p>extends关键字用于扩展子类,创建一个类作为另一个类的子类</p><p> 它会将父类中的属性和方法一起继承到子类的,减少子类中重复的业务代码</p><p> 这比之前在ES5中修改原型链实现继承的方法可读性要强的多,而且写法更简洁</p></blockquote><p></p><pre><code> extends的使用
class Animal{}
dog继承Animal类
class dog extends Animal{}</code></pre><p> <strong>继承类的属性和方法</strong></p><ul><li>定义一个dog类 通过extends关键字继承Animal类的属性和方法</li><li>在子类的construtor方法中 使用super关键字,在子类中它上市必须存在的 否则新建实例会抛出异常。</li><li>这是因为子类的this对象是继承父类的this对象 如果不调用super方法 子类就得不到this对象</li></ul><p></p><pre><code>class Animal {
constructor (*name*) {
this.name = *name*
}
eating () {
console.log(this.name + 'eating')
}
}
class dog extends Animal{
constructor(*name*,*legs*){
super(*name*)
this.legs = *legs*
}
speaking(){
console.log(this.name + "speaking");
}
}
var wang = new dog('tom',4)
wang.speaking()
wang.eating()
console.log(wang.name);</code></pre><h5>Super</h5><blockquote><p>super关键字用于访问调用一个对象的父对象上的函数</p><p> super指的是超级,顶级,父类的意思</p><p> 在子类的构造函数中使用this或者返回默认对象之前,必须先使用super调用父类的构造函数</p><p> 子类的construtor方法先调用了super方法,它代表了父类的构造函数,也就是我们把参数传递进去之后,其实它调用了父类的构造函数</p></blockquote><p> </p><p></p><pre><code> class Animal{
constructor(*name*)
}
class dog{
constructor(*name*,*type*,*weight*){}
super(*name*)
this.type = type
this.weight = weight
}</code></pre><p> <strong>使用super调用父类的方法</strong></p><pre><code> class Animal{
constructor(*name*){
this.name = name
}
eating(){
console.log(this.name + 'eating');
}
}
*// dog继承Animal类*
class dog extends Animal{
constructor(*name*,*lengs*){
super(*name*)
this.lengs = *lengs*
}
speaking(){
super.eating()
console.log(this.name + 'speaking');
}
}
var a = new dog('旺财',4)
a.speaking() *//旺财eating 旺财speaking</code></pre><h5>Getter和Setter</h5><p> <em>在类内部可以使用get和set关键字,对应某个属性设置存值和取值函数,拦截属性的存取行为</em></p><p></p><pre><code> class Animal{
constructor(){
this._age = 3
}
get age(){
console.log('get');
*return* this._age
}
set age(*val*){
console.log('set');
this._age = val
}
}
var a = new Animal()
console.log(a.age);
a.age = 4
console.log(a.age);</code></pre><h5>关于class扩展</h5><p> <strong>严格模式</strong></p><blockquote>在类和模块的内部 默认是严格模式 所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用</blockquote><p> <strong>name属性</strong></p><blockquote>Es6中的类只是Es5构造函数的一层包装,所以函数的许多属性都被class继承了,包括name属性</blockquote><p></p><pre><code> class Animal{}
console.log(Animal.name);</code></pre><p> <strong>变量提升</strong></p><blockquote>class不存在变量提升,这与Es5中实现类是不同的, function关键字会存在变量提升</blockquote><pre><code> new Foo() *//// ReferenceError*
class Foo{}</code></pre><blockquote><p>在Es6之后,在定义类以及它内部的属性方法,还有继承操作的语法变得很简洁易懂</p><p>class 是一个语法糖 其内部还是通过Es5中的语法实现的。且有些浏览器不支持class语法 我们可以通过babel来进行转换</p></blockquote><h3>函数式组合函数</h3><blockquote>组合函数就是将多个函数按照顺序进行执行,前一个函数返回的数据,作为下一个函数的参数,最后返回最终的结果</blockquote><p><em>例子: 假如我们需要将一个数字先进行乘法运算,后进行求平方,可以将这两部分拆分成两个基础函数double、square.再通过double函数返回值传入到square中调用</em></p><pre><code>function double (num) {
return num * 2
}
function square (num) {
return num ** 2
}
const count = 10
const res = square(double(count))
console.log(res);</code></pre><p>柯里化和纯函数很容易写出可读性不强的代码square(double(count))<br>上面的调用方式代码可读性不是很好,我们可以组合上面两个基础函数</p><pre><code>function composeFn (m, n) {
return function (count) {
return n(m(count))
}
}</code></pre><p>使用組合函數 將兩個函數傳入,composeFn會返回一個新的函數,我們只需要將要處理的數據傳入到返回的這個新函數,這個新函數將會幫我們處理並返回處理好的數據</p><pre><code> const newFn = composeFn(double, square)
const res2 = newFn(count)
console.log(res2, 'res2')</code></pre><p>通過compose組合而成的函數只能是一個參數,往往我們的基礎參數可能不止一個,這個時候我們將會用到柯里化函數進行處理</p><p><strong>实现组合函数</strong></p><pre><code> function MyCompose (...fns) {
var length = fns.length
/* 判断传入参数是否为函数 */
for (var i = 0; i < length; i++) {
if (typeof fns[i] !== 'function') {
throw new TypeError('Expected arguments are function')
}
}
// 返回一个接收参数的函数
function compose (...args) {
// 当前执行函数索引
var index = 0
// 调用当前索引对应的函数
var result = length ? fns[index].apply(this, args) : args
while (++index < length) {
// 将上一次返回的结果,传入到下一个函数中
var result = fns[index].call(this, result)
}
return result
}
return compose
}
const newFns = MyCompose(double, square)
const res3 = newFns(count)
console.log(res3, 'res3')</code></pre><p>1 在一开始 函数接收一系列的函数<br>2 先判断传入的参数是否全为函数,如果有一个非函数的函数则抛出异常<br>3 创建一个新函数 它需要接收要处理的参数<br>4 函数内,声明一个变量,记录当前执行函数的索引 将传入的参数按函数顺序执行<br>5 最终返回这个函数</p><h6>结合律</h6><p><em>函数的组合要满足结合律,例如a、b、c三个函数进行组合,可以将a和b进行组合,或者b和c进行组合。这个特性就是计算结合律</em></p><pre><code> let fn = compose(a,b,c)
let ass1 = compose(compose(a,b),c)
let ass2 = compose(compose,compose(b,c))</code></pre><h5>pointfree</h5><p><em>pointfree是一种编程风格,这种风格也被称为Tacit programming,point代表形参,意思是没有形参的编程风格</em></p><pre><code> pointfree风格 带有word形参
var word = (word)=>word.toUppercase()
// pointfree风格 没有任何形参
var word = compose(toUppercase)</code></pre><p>也就是说 我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合在一起即可<br>它省去了对参数命名的麻烦 代码更加简洁 pointfree 是基础函数组合的高级函数 这些高级函数往往应用在我们的业务中</p><h5>Debug</h5><p><strong>对中间的值进行打印 并且知道当前执行位置</strong></p><pre><code> var log = MyCurring((t,v)=>{
console.log(t,v);
return v
})</code></pre><p>在组合函数找那个 按照从左到右的顺序 给函数套个log函数,就可以知道每次输出的值</p><pre><code>var newFn = MyCompose(double,log('double--'),square,log('square--'))
var res2 = newFn(10)
//double-- 20
// square -- 400</code></pre><h3>函数式之柯里化</h3><p>柯里化(currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术它能减少代码冗余 增加代码的可读性<br><strong>例子</strong></p><pre><code> function sum(a,b,c,d){
return a + b + c + d
}
sum(10,20,30,60)</code></pre><p>上面的sum函数是将传入的参数进行相加,如果把sum函数改成柯里化函数</p><pre><code>function sum (a) {
return function (b) {
return function (c) {
return function (d) {
return a + b + c + d
}
}
}
}
// 按照上面的写法,这个sum调用方式将是
sum(10)(20)(30)(40) //100
const add = sum(10)
add(20)(30)(40)</code></pre><p>通过这个例子你可以知道 柯里化即是把较多参数的函数转为可以分段传入函数参数的函数,可以减少对函数备份参数的传入</p><p><strong>柯里化应用场景</strong><br>在实际场景上的一个例子 MyURL函数用于生成一个拼接之后的url链接,它需要传入三个参数,分别是protocol,domain,path</p><pre><code> function MyURL (protocol, domain, path) {
return protocol + '://' + domain + '/' + path
}
MyURL('https','fanyi.qq.com','user/2858385965322935');
MyURL('https','fanyi.qq.com','post/122');
MyURL('https','fanyi.qq.com','user/33');</code></pre><h5>柯里化函数实现</h5><p>实现将函数转换为柯里化函数的函数<br> <strong>柯里化函数的实现</strong></p><pre><code>/* 思路步骤:
1 创建一个名为myCurrying的函数,接受参数为需要变为函数柯里化的函数,这里用fn表示
2 函数内部返回一个行为curried的函数 接受参数的个数为fn函数的参数(fn.length)个数 ,这里使用剩余参数 ...
3 curried内部需要去判断当前已经接受的参数个数 是否与函数本身需要接收的参数个数一致
4 如果当前传入的参数大于等于需要接收的个数时 执行函数fn.apply(this,args)
5 如果不满足上述条件,也就是传入的参数没有达到个数要求,需要返回一个新函数,这里用curried2表示,接受的参数为第一次接受的参数后剩下的参数,...args2表示
6.接受到参数后curried2函数执行 递归调用curried函数 继续判断传入的参数个数是够一致 一致则执行 步骤4,反之,继续递归调用curried函数
7 调用myCurring函数 传入我们需要柯里化的函数作为参数
*/
function myCurrying (fn) {
return function curried (...args) {
// 判断当前已经接受的参数个数 是否与参数本身需要接收的参数个数一致
// 当前传入的参数,大于等于需要接收的参数个数时,执行函数
if (args.length >= fn.length) {
return fn.apply(this, args)
} else {
// 当前传入的参数,没有达到个数时 需要返回一个新的函数,继续接受剩余的参数
return function curried2 (...arg2) {
// 接受到参数后,需要递归调用curried来再一次检查函数的个数是否达标
return curried.apply(this, [...args, ...arg2])
}
}
}
}
function sum (a, b, c) {
return a + b + c
}
/* 使用myCurryging将sum函数进行柯里化转化 */
var result = myCurrying(sum)
console.log(result(1)(2)(3));</code></pre><p><strong> 简单来说就是</strong><br> 1 在一开始 函数接收一个函数 将这个函数进行柯里化处理<br> 2 先进行判断当前函数传入的参数数量是否大于原函数参数的数量,如果大于,通过apply方式调用函数<br> 3 如果没有达到原函数参数的数量: 将返回一个函数继续接受剩余的参数<br> 4 调用返回的函数,当参数达到原函数参数的数量时,通过apply方式调用函数</p><p><strong>思路</strong><br>利用闭包的原理,将每次传递进来的参数存起来, 当参数不符合预期时,返回一个新的函数继续接受剩余参数,继续调用,不符合则再递归</p><p><strong>知识点</strong><br>1 Funtion.prototype.length 是获取函数参数的个数<br>2 如果不使用apply方式调用原函数, 会发生this指向不正确</p><p><strong>柯里化的性能</strong><br>在使用柯里化意味着有额外的内存开销<br>使用arguments对象比直接操作命名参数慢<br>作用域,闭包对内存的开销,性能下降<br>使用call、apply调用函数比直接调用函数会慢些,而且产生嵌套关系<br>总结:<br>柯里化只要是以闭包为基本 利用闭包将函数的参数存起来,等到参数达到一定数量时执行函数,使用函数柯里化会让代码更加灵活性<br>但也有一定的弊端,它用到了arguments,递归,闭包等会带来性能影响,所以结合实际情况使用</p>
你与绝美文章只差一个Typora(Mac版)
https://segmentfault.com/a/1190000043262246
2023-01-06T15:46:39+08:00
2023-01-06T15:46:39+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h4>1. 标题</h4><p>⌘ + num(后面可跟1到6,对应title 1到6)</p><h4>2. 下划线 (⌘ + u)</h4><p><u>hhh</u></p><h4>3 . 删除线 (^ + ⇧ + `)</h4><p><del>d'd</del></p><h4>4. 字体(加粗 ⌘+b, 斜体 ⌘+ i)</h4><h4>5. 无序列表 (<strong>⌥ + ⌘ + u</strong>)</h4><ul><li>我</li><li><p>的</p><ul><li>无</li><li>序</li></ul></li></ul><h4>6. 有序列表(<strong>⌥ + ⌘ + o</strong>)</h4><ol><li>s</li><li><p>r</p><ol><li>s</li></ol></li></ol><p></p><h4>7. 任务列表 <strong>(⌥ + ⌘ + x)</strong></h4><ul><li>[ ] 起床</li><li>[ ] 吃早饭</li></ul><h4>8. 引用(<strong>⌥ + ⌘ + q</strong>)</h4><blockquote>萨哈哈哈</blockquote><h4>9.插入链接</h4><p><a href="https://link.segmentfault.com/?enc=Xh2yq%2F0kaQZV6dmARVTbdA%3D%3D.gnOm4qdAn0QB5gwaFDuNAXZlbU7krJmPekGGDYZ%2BagA%3D" rel="nofollow">这是一条链接</a></p><h4>10.插入图片(鼠标直接拖拽进来或者⌘+ ^ + i)</h4><p>默认居中,可调整</p><h4>11.插入代码块(<strong>⌘ + ⌥ + c</strong>或者^+`)</h4><pre><code><div></div></code></pre><h4>12.文章跳转 (<strong>⌘ + 方向键</strong>)</h4><p>跳到文章开头 (⌘ + 向上箭头)</p><p>跳到文章结尾 (⌘ + 向下箭头)</p><h4>13.选中英文单词/中文</h4><p>连续选中英文单词或中文 (⌘ + d)</p><h4>14.按行选中 (⌘ + l)</h4><h4>15.查找替换 (⌘ + f)</h4><h4>16.快速生成表格 (<strong>⌘ + ⌥ + T</strong>)</h4><h4>17.全局快速查找文件(<strong>⇧ + ⌘ + o</strong>)</h4><h4>18.快速生成目录</h4><p><img src="/img/bVc5GEd" alt="image.png" title="image.png"></p><h4>19.插入emjo符号(<strong>*⌃ + ⌘ + space*</strong>)</h4><p><img src="/img/bVc5GEb" alt="image.png" title="image.png"></p><h4>20.新建文件 (<strong>⌘ + n</strong>)</h4><h4>21.全屏切换 (⌘ + ^ + F)</h4><h4>22.分割线 (⌘ + ⌥ + - 或---+enter)</h4><hr><h4>23.字体颜色、背景色、大小、字体种类</h4><pre><code><font color=green>绿色</font>
<font size=5>尺寸</font>
<font face="微软雅黑">微软雅黑</font>
复合写法:<font face="微软雅黑" size=5 color=green>微软雅黑</font></code></pre><p><font face="微软雅黑" size=3 color=green>微软雅黑</font></p><h4>24.公式块 (option + ⌘ + B)</h4><p>$$
公式
$$</p><h4>25.定义脚注 (⌘ +⌥ + r )</h4><h4>26.导出</h4><p><img src="/img/bVc5GEr" alt="image.png" title="image.png"></p><h4>27 文本和标题居中</h4><p><code><h1 align="center">标题</h1></code></p><p><h1 align="center">标题</h1></p><p><code><div align="center">文本</div></code></p><p><div align="center"><font align="center">文本</font></div></p><h4>28.改变字体 标题颜色,选中文字颜色</h4><p><img src="/img/bVc5GD9" alt="image.png" title="image.png"></p><p><strong>打开主题文件夹,修改github.css</strong></p><p><strong>修改字体为Hannotate SC,找到body,添加Hannotate SC</strong></p><pre><code>body {
font-family: "Hannotate SC","Open Sans","Clear Sans", "Helvetica Neue", Helvetica, Arial, 'Segoe UI Emoji', sans-serif;
color: rgb(51, 51, 51);
line-height: 1.6;
}</code></pre><p><strong>修改标题的颜色,同一个文件,找到这段代码,添加color</strong></p><pre><code>h1,
h2,
h3,
h4,
h5,
h6 {
position: relative;
margin-top: 1rem;
margin-bottom: 1rem;
font-weight: bold;
line-height: 1.4;
cursor: text;
color: rgb(26, 143, 55);
}</code></pre><p>修改在Typora里面选中文字后的颜色</p><p>首先把这个选中<br><img src="/img/bVc5GD4" alt="image.png" title="image.png"></p>
Css知识扫盲
https://segmentfault.com/a/1190000043238361
2023-01-04T17:17:10+08:00
2023-01-04T17:17:10+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
2
<h3>超出点点点</h3><pre><code>一、单行文本溢出显示省略号点点点…
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
二、多行文本溢出显示省略号点点点…
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; //这里是在第二行有省略号
overflow: hidden;</code></pre><h3>font-family</h3><p>字体总共分为五大类,如下:</p><ul><li>serif :矢量字体,线条有粗细,可等比例缩放。</li><li>sans-serif :这应该是我们用的最多的字体家族了,它和 serif 的区别就是,H 和 I 上下没有小横线。</li><li>monospace :等宽字体。</li><li>cursive :手写字体。</li><li>fantasy :其他各种无法归类的字体,比较个性的字体了都属于这一类了。</li></ul><h3>text-transform 以css控制字母的大小写</h3><ul><li>text-transform:capitalize 文本中的每个单词以大写字母开头</li><li>text-transform:lowercase 定义无大写字母,仅有小写字母</li><li>text-transform:uppercase 定义无小写字母,仅有大写字母</li></ul><h3>place-content: center; 水平垂直居中</h3><p><strong>可用于</strong></p><pre><code>display:grid;
place-content: center;</code></pre><p><strong>相当于</strong></p><pre><code> display:flex;
justify-content:center;
align-items:center;</code></pre><h3>letter-spacing 字符间距</h3><p><code>letter-spacing: 0.2em; </code></p><h3>禁止用户选中文字</h3><p><code>user-select:none;</code></p><h3>flex布局换行之后,下面一行的布局会异常</h3><pre><code class="javascript">.homeItemBox{
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.homeItem{
display: flex;
width: calc((100% - 20rpx) / 4);
flex-direction: column; align-items: center;
flex-shrink: 0;
margin-top:30rpx;
}
.homeItem:nth-of-type(4n+0){margin-right: 0;} //每一行的第四个margin right是0</code></pre><h2>nth-of-type(4n+0)</h2><ul><li>4n+0 就是每隔四个</li></ul><ul><li><p>odd even关键词表示奇偶数这个是算术表达式</p><p><strong>p:nth-of-type(3n+0)使用公式 (an + b)。</strong><br>*描述:表示周期的长度,n 是计数器(从 0 开始),b 是偏移值。在这里,<br>我们指定了下标是 3 的倍数的所有 p 元素的背景色*</p></li></ul><h3>flex-shrink: 0;</h3><p>倘若给父元素设置了flex布局后,若要其子元素的width有效果,必须给子元素设置flex-shrink为0<br>来固定元素不被挤压</p><h3>filter</h3><p>*filter:brightness 亮度/曝光度<br> filter:brightness(0.2)<br>filter: opacity( %) ---- 透明度<br>filter: drop-shadow(offset-x offset-y blur color) ---- 阴影<br>filter:drop-shadow(10px 15px 20px #000)<br>filter: grayscale( %) ---- 灰度图像<br>filter: sepia( %) ---- 深褐色<br>filter: hue-rotate( deg ) ---- 色相旋转<br>filter: invert( %) ---- 反转图像 使用invert滤镜可以把对象的可视化属性全部翻转,包括色彩、饱和度和亮度值<br>filter: saturate( %) ---- 饱和度<br>filter: contrast( %) ---- 对比度 值0%代表全灰色图像,而100%代表原始图像<br>filter: blur( px) ---- 高斯模糊*</p><h3>全站置灰</h3><pre><code class="javascript">html {
filter: grayscale(.95);
-webkit-filter: grayscale(.95);
}</code></pre><h3>filter VS backdrop-filter</h3><ul><li>filter:该属性将模糊或颜色偏移等图形效果应用于元素。</li><li>backdrop-filter:该属性可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。它适用于元素背后的所有元素,为了看到效果,必须使元素或其背景至少部分透明。</li><li>两者之间的差异,filter 是作用于元素本身,而 backdrop-filter 是作用于元素背后的区域所覆盖的所有元素。而它们所支持的滤镜种类是一模一样的。</li><li>backdrop-filter 最为常见的使用方式是用其实现毛玻璃效果。<br><strong>filter 和 backdrop-filter 使用上最明显的差异在于:</strong><br>*filter 作用于当前元素,并且它的后代元素也会继承这个属性<br>backdrop-filter 作用于元素背后的所有元素<br>仔细区分理解,一个是当前元素和它的后代元素,一个是元素背后的所有元素。*</li></ul><h3>inset</h3><p>*inset 属性只作用于定位元素<br>inset 属性用作定位元素的 top、right、bottom、left 这些属性的简写。类似于 margin 和 padding 属性,依照“上右下左”的顺序。*</p><h3>置灰网站的首屏</h3><p><strong>兼容更好的混合模式</strong></p><pre><code class="javascript"> html{
position:relative;
width: 100%;
height: 100%;
overflow: scroll;
background-color: #fff;
}
html::before{
content:"";
position:absolute;
inset:0;
background: rgba(0,0,0,1);
/* mix-blend-mode: color; 颜色*/
/* mix-blend-mode: hue; 色相*/
mix-blend-mode: saturation; //饱和度
以上三种模式都可
pointer-events: none; /* 点击穿透 */
z-index:10;
}
.box{
background: url('./one.png'),url('./two.png');
background-size: cover;
width: 400px;
height: 400px;
background-blend-mode:lighten;
}
</code></pre><p><strong>backdrop-filter 实现一种遮罩滤镜效果</strong></p><pre><code class="javascript"> html {
width: 100%;
height: 100%;
position: relative;
overflow: scroll;
}
html::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
z-index: 10;
inset: 0;
backdrop-filter: grayscale(0.9);
pointer-events: none;
}</code></pre><h3>table使用</h3><p><strong>子元素均分父元素的长度</strong></p><p>*display:table-row; //padding和margin会失效<br>父元素设置display: table时; 注意padding会失效<br>子元素设置 display: table-cell; //margin会失效*</p><pre><code class="javascript">h3{
display: table-cell;
}
nav{
display: table;
width:100%;
position: sticky;
top:0;
background-color: red;
}
<nav>
<h3>导航1</h3>
<h3>导航2</h3>
<h3>导航3</h3>
</nav></code></pre><p><strong>利用table居中</strong></p><pre><code class="javascript">.parentBox{
display: table;
width: 100vw;
height: 600rpx;
border:1px solid #AAA;
}
.sonItem{
display: table-cell;
vertical-align: middle;
text-align: center;
}</code></pre><h3>吸顶</h3><p><em>注意如果父元素设置了overflow-hidden 则吸顶会失效</em></p><pre><code class="javascript"> width:100%;
position: sticky;
top:0;</code></pre><h3>滚动视差 background-attachment</h3><p><em>视差滚动(Parallax Scrolling)是指让多层背景以不同的速度移动,形成立体的运动效果,带来非常出色的视觉体验</em></p><pre><code class="javascript"> <div class="gImg gImg1"></div>
<div class="gImg gImg2"></div>
<div class="gImg gImg3"></div>
.gImg{
height: 100vh;
width: 100%;
background-attachment:fixed;
background-size: cover;
background-position: center center;
}
.gImg1{
background-image: url('./one.png');
}
.gImg2{
background-image: url('./two.png');
}
.gImg3{
background-image: url('./three.png');
}</code></pre><p>效果大概就是<br><img src="/img/remote/1460000043238363" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h3>CSS3 transform 中的 matrix</h3><p><img src="/img/remote/1460000043238364" alt="在这里插入图片描述" title="在这里插入图片描述"><br>2D 的转换是由一个 3*3 的矩阵表示的,前两行代表转换的值,分别是 a b c d e f,要注意是竖着排的,第一行代表 x 轴发生的变化,第二行代表 y 轴发生的变化,第三行代表 z 轴发生的变化,因为这里是 2D 不涉及 z 轴,所以这里是 0 0 1。</p><ul><li>缩放 scale对应的是矩阵中的 a 和 d,x 轴的缩放比例对应 a,y 轴的缩放比例对应 d,</li><li>平移 translate对应的是矩阵中的 e 和 f,平移的 x 和 y 分别对应 e 和 f。</li><li>偏移 skew对应矩阵中的 c 和 b x 对应 c,y 对应 b, 这个对应并不是相等,需要对 skew 的 x 值 和 y 值进行 tan 运算。transform: matrix(a, tan(30deg), tan(20deg), d, e, f)</li><li><p>旋转 rotate影响的是a/b/c/d四个值 a=cosθ b=sinθ c=-sinθ d=cosθ<br>将 30° 转换为弧度,传递给三角函数计算</p><pre><code class="javascript">// 弧度和角度的转换公式:弧度=π/180×角度
const radian = Math.PI / 180 * 30 // 算出弧度
const sin = Math.sin(radian) // 计算 sinθ
const cos = Math.cos(radian) // 计算 cosθ</code></pre></li></ul><pre><code class="javascript">所以这个: transform: scale(1,5, 1.5) translate(0, 190.5)
对应: transform: matrix(1.5, 0, 0, 1.5, 0, 190.5);</code></pre><p>如果旋转+缩放+偏移+位移一起的话<br>按照transform里面rotate/scale/skew/translate所写的顺序相乘。<br><img src="/img/remote/1460000043238365" alt="在这里插入图片描述" title="在这里插入图片描述"><br><a href="https://link.segmentfault.com/?enc=OK9lCXpo6zbI9UczWQ%2Bjrg%3D%3D.i2hcaCTZv2in7e8a5NC7Hte9JyseYN2G2Pu0vcxVsWNYaQG9wVsY%2F%2FIYVPT9Lx%2Fn" rel="nofollow">具体</a></p><h3>利用js控制css之setProperty</h3><p><strong>通过js修改body的css变量</strong></p><pre><code class="javascript">body{
width:100vw;
height:100vh;
overflow: hidden;
background-color: #111;
perspective: 1000px;
--c-eyeSocket:rgb(41,104,217);
--c-eyeSocket-outer:#02ffff;
--c-eyeSocket-outer-shadow:transparent;
--c-eyeSocket-inner:rgb(35,22,140);
}
// 通过js修改body的css变量
document.body.style.setProperty('--c-eyeSocket','rgba(255,187,255)');
//css使用css变量
.eyeSocket::before {
width: calc(100% + 20px);
height: calc(100% + 20px);
border: 6px solid var(--c-eyeSocket);
}</code></pre><p><strong>js控制设置当前的元素上面的类名来控制动画</strong></p><pre><code class="javascript">this.$refs.bigEye.className = 'eyeSocket eyeSocketSleeping'
.eyeSocket {
position: absolute;
left: calc(50%-75px);
top: calc(50%-75px);
width: 150px;
aspect-ratio: 1;
border-radius: 50%;
z-index: 1;
border: 4px solid var(--c-eyeSocket);
box-shadow: 0px 0px 50px var(--c-eyeSocket-outer-shadow);
/* 当生气时添加红色外发光,常态则保持透明 */
transition: border 0.5s ease-in-out,box-shadow 0.5s ease-in-out;
/* 添加过渡效果 */
}
.eyeSocket::before,
.eyeSocket::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
box-sizing: border-box;
transition: all 0.5s ease-in-out;
}
.eyeSocket::before {
width: calc(100% + 20px);
height: calc(100% + 20px);
border: 6px solid var(--c-eyeSocket-outer);
}
.eyeSocket::after {
width: 100%;
height: 100%;
border: 4px solid var(--c-eyeSocket-inner);
box-shadow: inset 0px 0px 30px var(--c-eyeSocket-inner);
}
.eyeSocketSleeping{
animation:sleeping 6s infinite;
}
@keyframes sleeping {
0%{
transform: scale(1);
}
50%{
transform: scale(1.2);
}
100%{
transform: scale(1);
}
}</code></pre><h3>any-hover any-pointer</h3><ul><li>any-hover通过css判断设备是否支持鼠标经过行为</li><li>而pointer则是与点击事件相关<br><strong>any-hover</strong></li><li>none: 没有什么输入装置可以实现hover悬停,即没有鼠标输入设备</li><li><p>hover: 一个或多个输入装置可以触发元素的hover悬停交互,即支持鼠标设备</p><pre><code>
button:active {
background-color: #f0f0f0;
}
/* 兼容PC端: */
@media (any-hover: hover) {
button:hover {
background-color: #f5f5f5;
}
}
</code></pre></li></ul><p><strong>pointer语法</strong><br>同样也是支持下面3个关键字值:</p><ul><li>none主输入装置点击不可用。</li><li>coarse主输入装置点击不精确。</li><li>fine主输入装置点击很OK。</li></ul><p>例如点击不精确的时候让复选框尺寸变大:</p><pre><code>@media (pointer: coarse) {
input[type="checkbox"] {
width: 30px;
height: 30px;
}
}</code></pre><h3>通过getComputedStyle 样式做响应式</h3><h5>判断是出于移动端还是pc端</h5><pre><code>@media (any-hover: none) {
body::before {
content: 'hoverNone';
display: none;
}
}
var strContent = getComputedStyle(document.body, '::before').content;
</code></pre><ul><li>strContent结果是'none'则表示支持 hover,是 PC 端,</li><li>strContent结果是'"hoverNone"'则表示不支持 hover 经过,需要换成 click 事件,是 Mobile 端</li></ul><h5>JS 判断当前是处于黑暗模式,还是浅色主题</h5><p><strong>prefers-color-scheme 媒体特性。它能够帮助检测设备的深色模式开启情况</strong></p><pre><code>:root {
--mode: 'unknown';
}
@media (prefers-color-scheme: dark) {
/* 黑暗模式 */
:root {
--mode: 'dark';
--colorLink: #bfdbff;
--colorMark: #cc0000;
--colorText: #ffffff;
--colorLight: #777777;
}
}
@media (prefers-color-scheme: light) {
/* 浅色主题 */
:root {
--mode: 'light';
--colorLink: #34538b;
--colorMark: #cc0000;
--colorText: #000000;
--colorLight: #cccccc;
}
}
var mode = getComputedStyle(document.documentElement).getPropertyValue('--mode').trim();
// mode结果是'"dark"'则表示黑夜主题,深色模式,黑暗风格,护眼模式。
</code></pre><h3>图片格式多格式展示</h3><p><strong>图片从上往下的一个匹配展示</strong></p><pre><code><picture>
<!-- 可能是一些对兼容性有要求的,但是性能表现更好的现代图片格式-->
<source src="image.avif" type="image/avif">
<source src="image.jxl" type="image/jxl">
<source src="image.webp" type="image/webp">
<!-- 最终的兜底方案-->
<img src="image.jpg" type="image/jpeg">
</picture> </code></pre><h3>fetchpriority页面图片加载展示设置优先级</h3><pre><code><img src="hero-image.jpg" fetchpriority="high" alt="Hero">
<!-- 降低嵌入的第三方iframe资源的加载优先级 -->
<iframe src="https://www.baidu.com" fetchpriority="low"></iframe>
<!-- 为非必须的js脚本指定低加载优先级 -->
<script src="blocking_but_unimportant.js" fetchpriority="low"></script></code></pre><h3>object-fit object-position 用于图片的显示方式</h3><p>object-fit 有五个值</p><ol><li>fill 替换内容填充拉伸填满整个盒子,不保证原有的比例</li><li>contain 替换内容保持原有尺寸比例,保证内容一定在容器内,但是容器内可能留下空白</li><li>cover 覆盖保持原有比例,保证替换内容尺寸一定大于容器的尺寸,宽度和高度至少有一个和容器一致</li><li>none 保持原有尺寸比例</li><li>scale-down: 呈现尺寸较小</li></ol><p>object-fit,我们还有<strong>object-position</strong>属性,它负责将图像定位在其容器中<br><img src="/img/bVc5DNI" alt="image.png" title="image.png">。</p><h3>滚动</h3><h5>scroll-padding scroll-margin 在自动滚动定位时预留指定的间距</h5><ul><li>scroll-padding作用对象是滚动元素</li><li><p>scroll-margin作用对象是目标元素</p><pre><code> h2{
scroll-margin: 6rem;
}
html{
scroll-padding: 6rem
}</code></pre><h5>scrollIntoView 将指定元素滚动到视线范围内</h5><pre><code>el.scrollIntoView(); // 等同于el.scrollIntoView(true)
el.scrollIntoView(false);</code></pre></li><li>如果为true,表示元素的顶部与当前区域的可见部分的顶部对齐(前提是当前区域可滚动);相当于 {behavior: 'auto', block: 'start', inline: 'nearest'}</li><li>如果为false,表示元素的底部与当前区域的可见部分的尾部对齐(前提是当前区域可滚动)。{behavior: 'auto', block: 'end', inline: 'nearest'}</li><li>当未传入参数时,默认值为:{behavior: 'auto', block: 'start', inline: 'nearest'}</li></ul><p><strong>scrollIntoViewOptions,一个包含下列属性的对象。</strong></p><ul><li>behavior定义过渡动画,默认值为auto。<br>auto,表示没有平滑的滚动动画效果。<br>smooth,表示有平滑的滚动动画效果。</li><li>block定义垂直方向的对齐,默认值为start。<br>start,表示顶端对齐。<br>center,表示中间对齐<br>end,表示底端对齐。<br>nearest:<br>如果元素完全在视口内,则垂直方向不发生滚动。<br>如果元素未能完全在视口内,则根据最短滚动距离原则,垂直方向滚动父级容器,使元素完全在视口内。</li><li>inline定义水平方向的对齐,默认值为nearest。<br>start,表示左端对齐。<br>center,表示中间对齐。<br>end,表示右端对齐。<br>nearest:<br>如果元素完全在视口内,则水平方向不发生滚动。<br>如果元素未能完全在视口内,则根据最短滚动距离原则,水平方向滚动父级容器,使元素完全在视口内</li></ul><pre><code>//这个title-part元素将以平滑的滚动方式滚动到与视口底部齐平地方(有兼容性问题)
document.querySelector("#title-part").scrollIntoView({
block: 'end',
behavior: 'smooth'
})
//这个titleMU-part元素将木讷的瞬间滚动到与视口底部齐平地方(无滚动动画效果)
document.querySelector("#titleMU-part").scrollIntoView(false)</code></pre><p>默认也是紧贴滚动元素的,如果设置了scroll-padding 或者scroll-margin,<br>就可以在滚动定位时预留一定间距</p><h5>focus 聚焦时自动将元素滚动到视线范围内</h5><p><em>如果有fixed定位元素遮住了,可使用scroll-padding,scroll-margin</em><br>:target CSS 伪类 代表一个唯一的页面元素 (目标元素),其id 与当前 URL 片段匹配。<br><em>白话就是:target可用于当前活动target元素的样式,通过a标签绑定id元素,实现点击a标签修改a标签链接元素的属性</em></p><pre><code> :target{
border: 1px solid #ccc;
}</code></pre><p><strong>可用于tab栏</strong></p><pre><code>
.box{
display:flex;
}
div{
width:200px;
height:200px;
background-color:skyblue;
display:block;
margin-left:20px;
transtion:flex 1s;
}
div:target{
flex:1;
background-color:red;
}
<div>
<p>
<a href="#a">1</a>
<a href="#b">2</a>
<a href="#c">3</a>
</p>
<div class="box">
<div id="a"></div>
<div id="b"></div>
<div id="c"></div>
</div>
</div>
</code></pre><h5>overscroll-behavior:contain</h5><p><strong>阻止滚动链接,滚动不会传播给祖先元素</strong></p><ul><li>overscroll-behavior是overscroll-behavior-x和overscroll-behavior-y属性的合并写法,</li><li>有三个值: auto,contain,none,inherit<br> auto:默认会传播给祖先元素<br> contain:阻止滚动传播给祖先元素<br> none:none与contain的效果一样<br>inherit:从父元素继承滚动行为</li></ul><pre><code>
.son{
height:300px;
width:300px;
overflow-x:scroll,
overscroll-behavior-x:contain;
}
</code></pre><h5>scroll snap</h5><ul><li>scroll snap目前仅支持 scroll-snap-align和scroll-snap-type<br>scroll-snap-align用于子元素,定义滚停止后,子元素的对齐方式</li><li><p>有三个值:</p><ol><li>start: 当滚动停止时,浏览器会滚动到子元素在容器的起始位置</li><li>end: 当滚动停止时,浏览器会滚动到子元素在容器的结束位置</li><li>enter: 当滚动停止时,浏览器会滚动到子元素在容器的中点位置</li></ol></li><li><p>scroll-snap-type:用于容器定义滚动的方向和类型有两个值:</p><ol><li>mandatory:当用户停止滚动的时候,会自动的选择元素的一个点</li><li><p>proximity:当用户滚动到接近足够的值的时候,才会选择元素的一个点进行滚动<br> <strong> 比如</strong></p><pre><code>
scroll-snap-align:center;
scroll-snap-type:x proximity;
这个大概是滚动超过中间点150px的位置发生滚动</code></pre></li></ol></li></ul><p>完整x轴的案例</p><pre><code><div class="container">
<div ><img src="1.jpeg" /></div>
<div ><img src="2.jpeg" /></div>
<div ><img src="3.jpeg" /></div>
<div ><img src="4.jpeg" /></div>
<div ><img src="5.jpeg" /></div>
<div ><img src="2.jpeg" /></div>
<div ><img src="3.jpeg" /></div>
<div ><img src="4.jpeg" /></div>
</div>
.container{
display:flex;
flex-drection:row;
height:200px;
padding:1rem;
width:200px;
overflow-x:auto;
overscroll-behavior-x:contain;
scroll-snap-type:x mandatory;;
}
div{
margin:10px;
scroll-snap-align:center;
}
img{
width:180px;
max-width:none;
object-fit:contain;
border-radius:1rem;
}</code></pre><p>所以scroll-snap-type这个属性可以让滚动时自动捕捉临界点。正常情况下,滚动临界点是紧贴滚动容器的</p><pre><code> scroll-snap-type:y proximity;
scroll-snap-slign:start;
scroll-margin: 0是起始位置 正数是记录起始的位置 负数可以看到下一个盒子的内容</code></pre><h5>提升页面滚动性能</h5><p>##### pc端 <br> 避免在页面滚动的时候频繁触发包括hover在内的,任何鼠标事件,从而提升页面滚动时的性能</p><pre><code>body{
point-events:none;
}</code></pre><p><strong>使用滚动监听可灵活控制</strong></p><pre><code>let timer = null
window.addEventListener('scroll',function(){
document.body.style.pointerEvents = 'none'
if(timer){this.clearTimeout(timer)}
timer = this.setTimeout(()=>{
document.body.style.pointerEvents = 'auto'
},100)
})</code></pre><p>##### 移动端<br>touch-action可以禁用浏览器在移动端处理手势的事件,进而提高页面滚动的性能,还可解决移动端点击延迟<br>none:阻止所有原生的touch事件,但是我们的轮动也属于touch事件,<br>所以要使用manipulation,这个值只允许滚动和持续缩放操作,也就是相当于禁用了其他手势</p><pre><code> html{
touch-action:manipulation;
}</code></pre><p><strong> 也可使用js控制</strong></p><pre><code> // 在需要时主动触发对全局的所有手势禁用
document.documentElement.style.touchAction = "none"
// 某一个区域的手势禁用
document.getElementById('XX').style.touchAction="none"
// 不需要时还原 比如在抬手事件中处理
document.addEventListener('touchend',function(){
document.documentElement.style.touchAction = "manipulation"
})</code></pre><p>preventDefault阻止默认行为 一般设置passive为 false 声明不是被动的,<br>浏览器执行完回调函数才知道有没有调用preventDefault,再去执行默认行为,这样会造成滚动不流畅</p><pre><code> window.addEventListener('touchmove',e=>e.preventDefault(),{
passive:false //设置passive为 false,声明不是被动的
})</code></pre><p>所以touchAction先声明可用触摸事件再配合preventDefault阻止默认行为,且不会影响触摸事件</p><pre><code> document.documentElement.style.touchAction = "manipulation"
window.addEventListener('touchmove',e=>e.preventDefault(),)
//将 manipulation 值改为 none,就可以完全阻止滑动默认事件</code></pre><h3>css控制节流</h3><pre><code> <div class="body">
<button class="throttle" @click="clickFunc">节流提交</button>
</div>
.body{
display: grid;
place-content: center;
height: 100vh;
margin: 0;
//gap属性实际上是column-gap属性和row-gap属性的缩写 设置网格行列间距
gap: 15px;
background: #f1f1f1;
user-select: none; //鼠标不可选择
}
button {
user-select: none;
}
.throttle {
animation: throttle 2s step-end forwards;
}
//:active 伪类匹配被用户激活的元素
.throttle:active {
animation: none;
}
@keyframes throttle {
from {
pointer-events: none;
opacity: .5;
}
to {
pointer-events: all;
opacity: 1;
}
}</code></pre><p><strong>或者通过:active去触发transiton的变化 然后通过transition回调去动态设置设置按钮的禁用状态</strong></p><pre><code><div class="body">
<button class="throttle" @click="clickFunc">节流提交</button>
</div>
mounted () {
document.addEventListener('transitionstart', function (ev) {
ev.target.disabled = true
})
document.addEventListener('transitionend', function (ev) {
ev.target.disabled = false
})
}
<style scoped>
button {
opacity: 0.99;
transition: opacity 2s;
}
button:not(:disabled):active { //button非禁用状态的样式
opacity: 1;
transition:0s;
}
</style></code></pre>
微信小程序归结
https://segmentfault.com/a/1190000043103150
2022-12-18T20:59:34+08:00
2022-12-18T20:59:34+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<p>是的,在这个框架满天飞的年代,我既然有有幸使用了原生小程序开发项目,除了麻烦些,倒也不是一无所获,耕耘总有收货嘛,写博客本身不是为了炫技还是什么,单纯的是记性不好,有些知识点 自己是花了时间去查找的,时间久了,下次会忘,所以仅做记录的成份高一些,前言不搭后语莫怪。然后早上看到一句诗也不错:追风赶月莫停留,平芜尽头是春山。<br>回正题吧:</p><h3>上传头像</h3><pre><code>changeAvatar() {
var that = this;
wx.chooseImage({
count: 1, // 最多可以选择的图片张数,默认9
sizeType: ['original', 'compressed'],
// original 原图,compressed 压缩图,默认二者都有
sourceType: ['album', 'camera'],
// album 从相册选图,camera 使用相机,默认二者都有
success: async function (res) {
var avatar = res.tempFilePaths;
await that.filebBatchDeletes()
let resImage = await uploadImage(avatar[0])
if (resImage.data.code == 200) {
that.setData({
avatar: avatar[0],
picId: resImage.data.result
})
}
},
fail: function () {
// console.log("上传图片没有成功");
},
complete: function () {
// complete
} }) },
export const uploadImage = (uploadFile: string) => {
return new Promise((resolve, reject) => {
wx.uploadFile({
url: config.otherBaseUrl + 'load/file-upload',
filePath: uploadFile,
header: {
"Content-Type": "multipart/form-data",
'token': wx.getStorageSync('accessToken'),
},
formData: {
'caseNum': wx.getStorageSync('userId'),
"caseType": 'image',
},
name: 'file',
success: (res) => {
const data = JSON.parse(res.data)
resolve({ data })
},
fail: (err) => {
reject(err)
}
})
})}</code></pre><h3>前端小程序拿到图片流处理成Base64async</h3><pre><code> readImageByDataIds(id: string) {
let res = await getPicStream(`/readImageByDataId/${id}`)
this.setData({
processPic: res
}) },
export const getPicStream = (url:string) => {
return new Promise((resolve,reject) => {
wx.request({
url: config.baseUrl + url,
header: {
'X-Access-Token': wx.getStorageSync('token'),
'X-TIMESTAMP': getDateTimeToString(),
},
responseType: 'arraybuffer',
success: res => {
let url ='data:image/png;base64,'+ wx.arrayBufferToBase64(res.data)
resolve(url)
},
fail: err => {
reject(err)
}
})
})}</code></pre><h3>返回上一个页面并触发上一个页面的方法因为小程序不像浏览器会自动刷新,所以需要手动刷新</h3><pre><code>var pages = getCurrentPages();
var beforePage = pages[pages.length - 2];
wx.navigateBack({
delta: 1,
success: function () {
beforePage.getInfo(); // 执行前一个页面的getInfo方法
}
})</code></pre><h3>Base64形式的文件做预览</h3><pre><code>preClick(event: any) {
if (event.detail.type == 'file') {
const fileSystemManager = wx.getFileSystemManager()
try {
let strPath = event.detail.url.substring(event.detail.url.indexOf(',') + 1)
fileSystemManager.writeFileSync(
wx.env.USER_DATA_PATH + `/${event.detail.materialName}`, strPath,"base64");
wx.openDocument({
filePath: wx.env.USER_DATA_PATH + `/${event.detail.materialName}`
})
} catch (err) {
console.log("调用失败", err);
}
}
return true
}</code></pre><h3>v-button自定义透明用于绑定或获取用户信息</h3><pre><code><button bind:getuserinfo="onGetUserInfo" open-type='{{openType}}' plain='{{true}}' class="container">
<slot name="img"></slot>
</button>
Component({ /** * 组件的属性列表 */
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
// externalClasses: ['ex-btn-class'],
properties: {
openType: {
type: String
},
imageSrc: {
type: String
},
bindgetuserinfo: {
type: String
} },
/** * 组件的初始数据 */
data: { },
/** * 组件的方法列表 */
methods: {
onGetUserInfo(event) {
this.triggerEvent('getuserinfo', event.detail, {})
},
}})
.container{ padding: 0 !important; border:none !important;}</code></pre><h3>一个小程序跳转到另一个小程序</h3><pre><code>async getPayInfos(item:any) {
const { buildId, id, estateId } = item
let params = {
buildId,
estateId,
houseId: id,
}
let infos = await getPayInfo(params)
let that = this
wx.navigateToMiniProgram({
appId: 'wxe7550', //appid
path: `pages/index/index?token=${infos.data.payData}`,//path
success(res) {
that.data.isPay = true
that.data.mchOrderNo = infos.data.mchOrderNo
}
})
},</code></pre><h3>wxs</h3><p><em>wxs是小程序的一套脚本语言 ,结合WXML,可以构建出页面的结构 wxs不依赖于运行时的基础库版本 可以在所有版本的小程序中运行wxs与javascript是不同的语言 有自己的语法 并不和javascript一致wxs的运行环境和其他javascript代码是隔离的 wxs不能调用其他JavaScript文件中定义的函数 也不能调用小程序提供的APIwxs函数不能作为组件的事件回调与es5相似,不能用es6的语法这里作为单独文件使用</em><br><img src="/img/remote/1460000043103152" alt="图片" title="图片"></p><p><img src="/img/remote/1460000043103153" alt="图片" title="图片"></p><p><img src="/img/remote/1460000043103154" alt="图片" title="图片"><br><strong>也可直接写在wxml文件中</strong></p><pre><code><block wx:for="{{util.limit(comments,15)}}">
<tag-cmp class="tag" text="{{item.content}}">
<text class="num" slot="after">{{'+' + item.nums}}</text>
</tag-cmp>
</block>
<wxs module="util">
var limit = function(array, length) {
return array.slice(0, length)
}
var format = function(text){
if(!text){ return }
var reg = getRegExp('\\\\n','g')
var text = text.replace(reg,'\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
return text }
module.exports = { limit: limit, format:format }
</wxs></code></pre><p><strong>属于wxs的正则</strong><br><img src="/img/remote/1460000043103155" alt="图片" title="图片"></p><h3>点击事件</h3><ul><li>bind事件 catch事件</li><li>bind事件绑定不会阻止事件冒泡 </li><li><p>catch事件可以阻止事件向上冒泡</p><pre><code><view bind:tab="handleClick"></view>
<view catch:tab="handleClick"></view>wx:for遍历 <block wx:for="{{data}}">
<v-item item="{{item}}"></v-item>
</block></code></pre><h3>表单校验要自己写表单,所以也要自己写校验</h3><p><strong>使用的是WxValidate.jsWxValidate的验证规则:</strong> <br>(1)required:true/false,是否为必填字段。<br>(2)email:true/false,是否遵守电子邮件格式。 <br>(3)tel:true/false,是否遵守11位手机号码。 <br>(4)url:true/false,是否遵守域名格式。 <br>(5)idcarad:true/false,是否遵守18位身份证号格式。 <br>(6)digits: true/false,只能输入数字。 <br>(7)min:数值,指定最小值。 <br>(8)max:数值,指定最大值。 <br>(9)range:[min,max],指定范围。 <br>(10)minlength:数值,指定最少输入的字符个数。 <br>(11)maxlength:数值,指定最多输入的字符个数。 <br>(12)rangelength:[minlength,maxlength],指定输入字符个数的范围。 <br>(13)dateISO:true/false,说服遵守日期格式(yyyy-mm-dd、yyyy/mm/dd)。 <br>(14)equalTo:字符串,指定必须输入完全相同的内容。 <br>(15)contains:字符串,指定必须输入包含指定字符串的内容。 </p></li><li><p>方法就不说了,简单且繁琐,为什么只写规则,规则不好找😄*</p><h3>小程序的跳转</h3><p><em>小程序的跳转分为两种:页面跳转 保留当前页面,跳转到应用内的某个页面</em></p><pre><code>wx.navigateTo({ url: "/pages/login/login" })</code></pre><p><em>关闭当前页面,跳转到应用内的某个页面</em><br>`wx.redirectTo({<br>url: ''<br>}`<br><em>关闭所有页面,打开到应用内的某个页面</em></p><pre><code>wx.reLaunch 方法则会清空当前的堆栈 通过wx.navigateBack没有可返回的页面了</code></pre><p><em>底部tab跳转跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面</em><br>`wx.switchTab({<br>url: '/index'<br>})`</p><h3>小程序的事件传参</h3><p><em>data-</em> 自定义属性传参,其中 <em> 代表的是参数的名字例子:</em></p><pre><code><view class="homeItem" bindtap="goList" data-showType="add"></code></pre><p><em>事件内可通过e.currentTarget.dataset.showtype拿到值</em></p><pre><code>goList(e: any) {
cosole.log(e.currentTarget.dataset.showtype)
//注意这里showtype 可不是写错,而是小程序会把小驼峰大写转成小写
}</code></pre><h3>onPullDownRefresh onReachBottom 下拉刷新 上拉加载</h3><pre><code>Page({ freshList(){
let myList = this.selectComponent("#yg-list")
myList.pullList()
},
data: {
list: [],
pageIndex:-1,
pageSize:5,
loading: false,
total:0,
isMore:false
},
/* 刷新列表数据 */
reloadList() {
this.data.list = [];
this.setData({
pageIndex: -1,
},);
this.getData();
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
this.reloadList()
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
this.getData()
},
getData() {
if (this.data.pageIndex!=-1&&this.data.list.length == this.data.total) return
if (this.data.loading) return
this.setData({
pageIndex: this.data.pageIndex + 1,
loading: true
})
api
.queryMyOrderForPage(app.globalData.custId,this.data.pageIndex,this.data.pageSize)
.then((res) => {
let list = res.result.data;
for (let i = 0; i < list.length; i++) {
list[i].payTime = formatUnixTimestamp(list[i].payTime);
}
let dataArr = [...this.data.list,...list]
this.setData({
list:dataArr,
loading: false,
total:res.result.count
})
if (dataArr.length != 0 && dataArr.length == res.result.count) {
this.setData({
isMore: true
})
}
},)
.finally(() => {
this.setData({
loading: false
})
});
},</code></pre><p><strong>也可将方法挂在滚动元素scroll-view上</strong></p></li></ul><pre><code><scroll-view class="scrollWrapper" scroll-y refresher-enabled model:refresher-triggered="{{loading}}"
bindrefresherrefresh="onPullDownRefresh" bindscrolltolower="onReachBottom"></code></pre><p><em>可在app.json配置在距离什么位置触发 </em></p><pre><code>"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,//开启下拉刷新 "onReachBottomDistance":100,//可在app.json配置在距离什么位置触发
},</code></pre><p>本来以为这个没有什么难的,但是还是踩坑了</p><ol><li>若页面不小心写了两个onReachBottom, 这个事件是不会触发的</li><li><p>上拉加载,下拉刷新只能用在page页面组件中,在Component组件里面不触发的😌所以在page页面监听到到达页面的底部,如何通知列表组件的加载更多,引出下面的小程序中触发子组件的方式</p><h3>小程序中触发子组件的方法</h3><p><em>那这里用上上个例子的代码 </em></p><pre><code><yg-list applyStatus="1" id="yg-list"/>
onReachBottom: function() {
//滚动到底部通知子组件加载新的数据
let myList = this.selectComponent("#yg-list")
myList.getList()
}</code></pre><h3>小程序组件中子组件触发父组件的的方法,</h3><p><em>在这里触发indexFunction函数 </em></p><pre><code> <button bindTap="clickTap">点击</button>
clickTap:function(){
this.triggerEvent('indexFunction',{value:this.properties.count})
}</code></pre></li></ol><p><em>在父组件中</em></p><pre><code><yg-list bind:indexFunction="indexFunction"/>
Page({
indexFunction:function(e){
console.log('父容器中的方法被调用了',e);
}
})</code></pre><h3>wx:if 与 hidden</h3><p>*wx:if 与hidden都可以控制微信小程序中元素的显示与否。<br>wx:if不满足条件是不渲染,hidden是隐藏对于性能这一块,<br>hidden页面的隐藏代替页面跳转,不会重新触发ready 或者created生命周期。 <br>所以具体用法与vue的v-if和v-show相似*</p><h3>小程序组件</h3><p>如果写小程序每个页面纯手写重复的列表,表单,太臃肿,<br>所以肯定要写组件,组件的使用方式:<br>外层创建components文件夹,在里面写组件,<br>如果其中一个页面需要使用这个组件,可在当前页面的json中配置</p><pre><code>{ "usingComponents": { "yg-list":"/components/yg-list/yg-list" }}</code></pre><p><em>组件传值 properties 类似于vue的props</em></p><pre><code>//在properties里面定义我们要的属性
properties: {
btText: {
value: '默认值',//value表示默认值
type: String //type是我们定义的类型,这里是String字符串类型
}
},</code></pre><p><em>组件自身的方法定义在methods,同vue一样</em></p><pre><code>methods: {
showLog:function(){
}
}
组件使用插槽
Comment({
options:{
muitipleSlots:true //开启插槽
}
})
<view class="container">
<slot name="before"></slot>
<text>{{text}}</text>
<slot name="after"></slot>
</view>
使用
<v-tag>
<text text="哈哈" slot="after">{{text}}</text>
</v-tag></code></pre><h3>组件的外部样式externalClasses的使用</h3><p>*子组件定义一个外部样式名字 <br>子组件的元素用上这个类名*</p><pre><code>Comment({
externalClasses:['my-class']
})
<view class="my-class">{{text}}</view></code></pre><p><em>在父组件使用这个组件的时候 是在父组件中定义的样式</em></p><pre><code><yg-list my-class="yg-class"></yg-list>
.yg-class{color:red;}</code></pre><p>*注意:若是当前子组件的元素上还有别的样式,那组件内的样式会覆盖外部样式,<br>所以在父容器编写样式的时候,后面可加上impotant,提升等级关系。*</p><h3>组件的behaviorbehaviors</h3><ul><li>是小程序中,用于实现组件间代码共享的特性,</li><li>类似于 Vue.js 中的 “mixins”</li><li>每个 behaviors 可以包含一组属性、数据、生命周期函数和方法。</li><li>组件引用它时,它的属性、数据和方法会被合并到组件中,每个组件可以引用多个behaviors,- - behavior也可以引用其它behavior。</li><li><p>在外层创建一个文件夹behavior,里面可放各个用途的behavior js文件,<br><em>创建一个behavior文件 </em></p><pre><code>let classicBeh = Behavior({
properties:{
img:String,
content:String
},
data:{},
methods:{}
})
export {
classicBeh
}
在组件中使用
import { classicBeh } from '../behaviors/classicBeh.js'
Comment({
behaviors:
[classicBeh]
})</code></pre><p>behavior与组件的优先级若遇到重名的情况,组件会覆盖behavior,<br>但是如果是生命周期,会先执行behavior内的,再执行组件内的</p><h3>小程序中引入lodash</h3><p>在utils文件夹下新建lodash.js<br>文件把压缩过的lodash.min.js文件内容放进去直接引入会报错<br>再创建一个lodash-fix.js文件</p><pre><code>/** * 修复 微信小程序中lodash 的运行环境 */
global.Object = Object;
global.Array = Array;
// global.Buffer = Buffer
global.DataView = DataView;
global.Date = Date;
global.Error = Error;
global.Float32Array = Float32Array;
global.Float64Array = Float64Array;
global.Function = Function;
global.Int8Array = Int8Array;
global.Int16Array = Int16Array;
global.Int32Array = Int32Array;
global.Map = Map;
global.Math = Math;
global.Promise = Promise;
global.RegExp = RegExp;
global.Set = Set;
global.String = String;
global.Symbol = Symbol;
global.TypeError = TypeError;
global.Uint8Array = Uint8Array;
global.Uint8ClampedArray = Uint8ClampedArray;
global.Uint16Array = Uint16Array;
global.Uint32Array = Uint32Array;
global.WeakMap = WeakMap;
global.clearTimeout = clearTimeout;
global.isFinite = isFinite;
global.parseInt = parseInt;
global.setTimeout = setTimeout;
使用import "../../utils/lodash-fix.js"
import _ from "../../utils/lodash"</code></pre><p><strong>使用</strong></p><pre><code> getBuildList: _.throttle(function (str: any) {
const { cellId, searchCell } = this.data
lifePayModels.getBuild({ estateId: cellId, buildName: searchCell },
(res => {
let arr = res.map(item => {
return {
label: item.buildName,
value: item.id
}
})
this.setData({
columns: arr
})
}))
}, 3000),</code></pre><h3>第三方库</h3><p>Vant Weappvan-dialog <br>van-field model:value支持双向绑定<br>van-dialog给里面的输入框做校验的时候,它会关闭,所以这里用上beforeClose阻止默认关闭事件</p><pre><code><van-dialog use-slot
data-showType="rejectShow"
show="{{rejectShow}}"
show-cancel-button
confirm-button-open-type="toReject"
beforeClose="beforeClose"
bind:close="closeDialog"> <van-field
model:value="{{rejectComment}}"
label=""
type="textarea"
placeholder="驳回原因"
autosize
required/>
</van-dialog>
Page({
data: {
beforeClose (action) {
return new Promise(resolve => {
setTimeout(() => {
if (action === 'confirm') {
// 拦截确认操作
resolve(false)
} else {
resolve(true)
}
}, 0)
})
}
},
toReject(){
if(!this.data.comment){
tips('请填写原因')
return
}
this.setData({
show:false
})
}
})</code></pre><h3>微信支付</h3><p><img src="/img/remote/1460000043103156" alt="图片" title="图片"><br>具体的做法:</p></li><li>打开某小程序,点击直接下单 wx.login获取用户临时登录凭证code,发送到后端服务器换取</li><li>openId 在下单时,小程序需要将购买的商品Id,商品数量,以及用户的openId传送到服务器</li><li>在下单时,小程序需要将购买的商品Id,商品数量,以及用户的openId传送到服务器</li><li>服务器在接收到商品Id、商品数量、openId后,生成服务期订单数据,同时经过一定的签名算法,向微信支付发送请求,获取预付单信息(prepay_id),同时将获取的数据再次进行相应规则的签名,向小程序端响应必要的信息 </li><li><p>小程序端在获取对应的参数后,调用wx.requestPayment()发起微信支付,唤醒支付工作台,进行支付 <br>6 接下来的一些列操作都是由用户来操作的包括了微信支付密码,指纹等验证,确认支付之后执行鉴权调起支付<br>7鉴权调起支付:在微信后台进行鉴权,微信后台直接返回给前端支付的结果,前端收到返回数据后对支付结果进行展示<br>8 推送支付结果:微信后台在给前端返回支付的结果后,也会向后台也返回一个支付结果,后台通过这个支付结果来更新订单的状态<br>9 其中后端响应数据必要的信息则是wx.requestPayment方法所需要的参数,大致如下:``wx.requestPayment({<br>// 时间戳<br>timeStamp: '',<br>// 随机字符串<br>nonceStr: '',<br>// 统一下单接口返回的 prepay_id 参数值<br>package: '',<br>// 签名类型<br>signType: '',<br>// 签名<br>paySign: '',<br>// 调用成功回调<br>success () {},<br>// 失败回调<br>fail () {},<br>// 接口调用结束回调<br>complete () {}<br>})``<br>注意:以上信息中timeStamp、nonceStr、prepay_id、signType、paySign各参数均建议必须都由服务端返回(这样会尽最大可能性保证签名数据一致性),小程序端不做任何处理<br>官方支付文档地址<a href="https://link.segmentfault.com/?enc=Cj85P93h7pfwrKAMFReigQ%3D%3D.ET1Me6wSPotPGpcsMmenu7Dfkc10QjY4zeE4QuKQjQGWZrn00Cm%2FVtLRsuP5S1z3K%2FBRqjaxoNpZA7HgTVpJg1LKGcVWu7avnQ3p68m1Kv8%3D" rel="nofollow">https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_...</a><br>后端的操作 <a href="https://link.segmentfault.com/?enc=qrdqCk468COBBlPpGeZj9w%3D%3D.gsbTrgSTA7Pld8El0FKMLCKVWP4QjtrovtzG5RubrKvlzc8LN0mQ8MkZujJgfgOx" rel="nofollow">https://juejin.cn/post/7063318750790942733</a></p><h3>引入离线iconfont字体图标</h3></li></ul><p><a href="https://link.segmentfault.com/?enc=QQuG5IRMV95ngApCrnhwkA%3D%3D.p47RdaMAPyU4yA2ZfeMi0NiOFeweUBRDkO7fOo2ip6uwdydOoGiVDnTt4tgYBAejX6%2Fmy8BPZPuNtKNBLbRD7A%3D%3D" rel="nofollow">https://blog.csdn.net/qq_15064263/article/details/126396051</a></p><h3>域名不合法的解决</h3><p>小程序刚开始开发的时候测试环境一般都是打开 <br>不检验合法域名但是当我们发测试版本或者发生产环境的时候,<br>发的小程序是不请求接口的,这是因为我们没有设置合法域名,<br>在这里设置,注意域名必须是https的<br><img src="/img/remote/1460000043103157" alt="图片" title="图片"></p><h3>扫二维码调到小程序指定页面首先微信平台设置</h3><p>设置位置: 开发管理 开发设置(最后生成连接发布的时候要确定 代码已经在线上)<br><img src="/img/remote/1460000043103158" alt="图片" title="图片"></p><p><img src="/img/remote/1460000043103159" alt="图片" title="图片"><br><strong>然后可以拿着这个url去生成二维码</strong><br><img src="/img/remote/1460000043103160" alt="图片" title="图片"><br><em>然后在当前页面接受参数,判断是否已绑定手机号,登录</em></p><pre><code>onLoad(options: any) {
if (options.q) {
let url = decodeURIComponent(options.q)
let obj = this.getUrlParam(url)
this.initPhone()
this.setData({
cellName: obj.estateName,
cellId: obj.estateId,
isRead: true
})
}
},
getUrlParam(url) {
let params = url.split("?")[1].split("&");
let obj = {};
params.map(v => (obj[v.split("=")[0]] = v.split("=")[1])); return obj
}</code></pre><h3>wx.nextTick</h3><p>延迟一部分操作到下一个时间片再执行<br>使用方法同vue的this.$nextTick</p><pre><code> getUrlParam(url) {
let params = url.split("?")[1].split("&");
let obj = {};
params.map(v => (obj[v.split("=")[0]] = v.split("=")[1]));
return obj
},
onLoad(options: any) {
this.initPhone()
if (options.q) {
let url = decodeURIComponent(options.q)
console.log("url", url);
let obj = this.getUrlParam(url)
if (Reflect.ownKeys(obj).length > 0) {
wx.nextTick(() => {
this.setData({
cellName: obj.estateName,
cellId: obj.estateId,
isRead: true,
})
})
}
}
},</code></pre><p><em>最后如果当前文章给了你提示和帮助,还希望点赞和关注,鼓励,谢谢啦</em></p>
uniapp中使用canvas实现二维码分享海报
https://segmentfault.com/a/1190000042430699
2022-09-04T22:05:05+08:00
2022-09-04T22:05:05+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h3>案例1</h3><p><strong>实现背景:</strong></p><ul><li>海报的尺寸根据图片大小来的,宽度百分百,高度同比例缩放,图上覆盖二维码</li><li>如果没有设置海报图片,使用别的图片,在底部添加一个底部的固定图片,和二维码,和图片的左上角覆盖log图<br><strong>难点:</strong></li><li>因为是使用canvas渲染的图片,但是图片的尺寸不是固定,也就是canvas的尺寸不是固定(解决:在外部先获取图片的尺寸,传进这个组件中)</li><li>h5是渲染正常的,微信小程序图片渲染不出来(解决:微信不支持canvas直接渲染网络图片,所以需要先缓存到本地)</li></ul><pre><code><template>
<view class="poster-box" ref="posterBox" @click="hidePoster">
<canvas
:style="{
width: posterOptions.poster.canvasWidth + 'px',
height: posterOptions.poster.canvasHeight + 'px',
}"
canvas-id="myCanvas"
></canvas>
<!-- #ifdef MP -->
<image
:src="base64"
class="poster-image"
:draggable="false"
show-menu-by-longpress="true"
></image>
<!-- #endif -->
<!-- #ifdef H5 -->
<image class="poster-image" :src="base64" :draggable="false"></image>
<!-- #endif -->
</view>
</template>
<script>
import Qr from '@/utils/wxqrcode'
export default {
name: 'poster_canvas',
data() {
return {
base64: '', // 海报绘制成图片以便保存
}
},
props: {
//图片地址
posterOptions: {
type: Object,
require: true,
},
},
mounted() {
this.$nextTick(() => {
this.imgToCanvas()
})
},
methods: {
async imgToCanvas() {
let that = this
let canvasHeight = this.posterOptions.isHavePosterImg
? this.posterOptions.poster.canvasHeight
: this.posterOptions.poster.canvasHeight -
this.posterOptions.buttonOption.h
// 渲染大图
let ctx = uni.createCanvasContext('myCanvas', that)
console.log(11111,this.posterOptions)
ctx.drawImage(
this.posterOptions.poster.url,
0,
0,
this.posterOptions.poster.canvasWidth,
canvasHeight
)
if (!this.posterOptions.isHavePosterImg) {
//渲染底部的图片
ctx.drawImage(
this.posterOptions.buttonOption.bottomUrl,
this.posterOptions.buttonOption.x,
this.posterOptions.buttonOption.y,
this.posterOptions.buttonOption.w,
this.posterOptions.buttonOption.h
)
}
// 渲染二维码
if (this.posterOptions.QrOption.isWeChatMiniApp) {
ctx.drawImage(
this.posterOptions.QrOption.QrUrl,
this.posterOptions.QrOption.x,
this.posterOptions.QrOption.y,
this.posterOptions.QrOption.w,
this.posterOptions.QrOption.h
)
} else {
//普通二维码
let qrCodeImg = Qr.createQrCodeImg(this.posterOptions.QrOption.QrUrl, {
size: parseInt(300),
})
ctx.drawImage(
qrCodeImg,
this.posterOptions.QrOption.x,
this.posterOptions.QrOption.y,
this.posterOptions.QrOption.w,
this.posterOptions.QrOption.h
)
}
if (!this.posterOptions.isHavePosterImg) {
// 渲染log 和文字
ctx.drawImage(
this.posterOptions.logOption.logUrl,
this.posterOptions.logOption.x,
this.posterOptions.logOption.y,
this.posterOptions.logOption.w,
this.posterOptions.logOption.h
)
ctx.fillStyle = this.posterOptions.textOption.color
ctx.setFontSize(22)
ctx.font = 'bold arial'
ctx.fillText(
this.posterOptions.textOption.text,
this.posterOptions.textOption.x,
this.posterOptions.textOption.y
)
}
ctx.save() //保存
ctx.draw() //绘制
// 不加延迟的话,base64有时候会赋予undefined
// 把当前画布指定区域的内容导出生成指定大小的图片,并返回文件路径
setTimeout(() => {
uni.canvasToTempFilePath(
{
canvasId: "myCanvas",
fileType: "jpg",
width:that.posterOptions.poster.canvasWidth,
height:that.posterOptions.poster.canvasHeight,
destWidth:that.posterOptions.poster.canvasWidth,
destHeight:that.posterOptions.poster.canvasHeight,
success: function (res) {
that.base64 = res.tempFilePath;
},
fail: function (error) {
console.log(error, "错误");
},
},that
);
}, 500);
},
hidePoster() {
// #ifdef H5
this.$parent.$parent.hidePoster()
// #endif
// #ifndef H5
this.$parent.hidePoster()
// #endif
},
},
}
</script>
<style lang="scss" scoped>
.poster-box {
position: fixed;
top: 0;
z-index: 999;
background-color: rgba(0, 0, 0, 0.8);
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.poster-image {
position: absolute;
top: 0;
left: 0;
z-index: 1000;
width: 100%;
height: 100%;
opacity: 0;
}
</style>
</code></pre><p><strong>使用</strong></p><pre><code><template>
<view
class="contentBgc"
:style="{ backgroundColor: detailObj.backgroundColor || defaultBgc }"
>
<image
class="width-100"
:src="detailObj.activityDetailHeadImg"
mode="widthFix"
/>
<view v-for="item in detailObj.plateDtoList" :key="item.id">
<TopicThree v-if="detailObj.currentTemplate == 3" :itemObj="item" />
<TopicTwo v-if="detailObj.currentTemplate == 2" :itemObj="item" />
<TopicOne v-if="detailObj.currentTemplate == 1" :itemObj="item" />
</view>
<generateaposter v-if="isGetOk" @pChangeType="showPoster" />
<SharePoster v-if="posterShow" :posterOptions="posterOptions"></SharePoster>
</view>
</template>
<script>
import TopicOne from './components/topicOne.vue'
import TopicTwo from './components/topicTwo.vue'
import TopicThree from './components/topicThree.vue'
import { getSpecialTopicData } from '@/api/specialTopic.js'
// #ifdef H5
import { getShopWxConfig, share } from '@/utils/wxShare.js'
// #endif
import mixinWxshare from '@/utils/wxShareTimeline.js' //微信分享朋友圈
import generateaposter from '@/components/gpage/generateaposter.vue'
import SharePoster from './components/sharePoster.vue'
import { cmsWxacodeGetwxacode } from '@/api/product'
export default {
mixins: [mixinWxshare],
components: {
TopicOne,
TopicTwo,
TopicThree,
generateaposter,
SharePoster,
},
onLoad(options) {
this.activityUniqueCode = options.activityUniqueCode
this.initData()
},
data() {
return {
defaultBgc: '#fff', //默认背景色
share: this.$utils.getImgUrl('/static/mcShopImage/share.png'), //分享
detailObj: {},
activityUniqueCode: '',
posterShow: false,
isGetOk: false,
posterOptions: {
isHavePosterImg: false, //用户是否上传了分享海报的图片
activityName: '',
textOption: {
text: '',
x: 0, // X轴坐标
y: 20, // Y轴坐标
color: '#9c6d06',
},
poster: {
url:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fci.xiaohongshu.com%2F0429f29e-85f3-5826-9608-946fc1ee103a%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fci.xiaohongshu.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1664356680&t=349aa739d52cd748c50b3afd89da2ba2',
canvasWidth: 0,
canvasHeight: 0,
},
QrOption: {
QrUrl: '', // 二维码链接
x: 20, // 图片X轴坐标
y: 20, // 图片Y轴坐标
w: 90, // 图片宽度
h: 90, // 图片高度
isWeChatMiniApp: false, // 是否是微信小程序二维码 true是微信小程序, false 不是微信小程序
},
logOption: {
//log
logUrl:
'https://jemsfile.mochouu.com/provider/91949676/20220831/1661926456418.png',
x: 15, // 图片X轴坐标
y: 15, // 图片Y轴坐标
w: 120, // 图片宽度
h: 45, // 图片高度
},
buttonOption: {
//底部的图片
bottomUrl:
'https://jemsfile.mochouu.com/provider/91949676/20220831/1661930915954.png',
x: 0, // 图片X轴坐标
y: 20, // 图片Y轴坐标
w: 100, // 图片宽度
h: 100, // 图片高度
},
},
screenWidth: 0,
}
},
mounted() {},
// #ifndef H5
/* 微信小程序分享 */
onShareAppMessage: function(res) {
// console.log('分享的userCode:'+this.userInfo.userCode)
let userInfo = this.$utils.getStorageSync('userInfo')
let routes = getCurrentPages() // 获取当前打开过的页面路由数组
let curParam =
routes[routes.length - 1].options ||
routes[routes.length - 1].$route.query //获取路由参数
if (userInfo) {
curParam.userCode = userInfo.userCode
}
let a = this.$utils.pageliks(curParam, routes)
let obj = {
title: this.detailObj.shareTitle || this.detailObj.shareContent || '',
desc:this.detailObj.shareContent||'',
path: a,
imageUrl: this.detailObj.shareImg || this.detailObj.verticalImgUrl || '',
}
return obj
},
// #endif
methods: {
hidePoster() {
this.posterShow = false
},
getWxCode() {
uni.showLoading({
title: '加载中',
})
let that = this
this.isGetOk = false
let routes = getCurrentPages() // 获取当前打开过的页面路由数组
let curRoute = routes[routes.length - 1].route //获取当前页面路由
let curParam = routes[routes.length - 1].options //获取路由参数
let obj = {}
obj.curRoute = curRoute
obj.curParam = curParam
// 拼接参数
let str = ''
for (let key in obj.curParam) {
str += '&' + key + '=' + obj.curParam[key]
}
str = str ? '?' + str.substr(1, str.length) : ''
let data = {
page: '/' + obj.curRoute + str,
}
cmsWxacodeGetwxacode(data).then((res) => {
uni.hideLoading()
if (res.code === 1000) {
this.downImg(res.data, that.posterOptions.QrOption, 'QrUrl')
that.$set(that.posterOptions.QrOption, 'isWeChatMiniApp', true)
that.isGetOk = true
}
})
},
// 显示分享海报
showPoster() {
this.posterShow = true
},
/* 任务分配 */
assignTasks(data) {
/* 设置头部的title */
uni.setNavigationBarTitle({
title: data.activityName,
})
// 初始化分享朋友圈
// #ifndef H5
this.initWxShareData(
this.detailObj.shareTitle || this.detailObj.shareContent,
this.detailObj.shareImg || this.detailObj.verticalImgUrl
)
// #endif
// #ifdef H5
switch (uni.getSystemInfoSync().platform) {
case 'android':
this.fx(1)
break
case 'ios':
this.fx(2)
break
}
// #endif
},
/* 初始化数据 */
initData() {
let that = this
getSpecialTopicData(this.activityUniqueCode).then((res) => {
if (res.code == 1000) {
const { plateDtoList, ...rest } = res.data
let filterData = plateDtoList.map((it) => {
const { productJson, ...re } = it
let objArr = JSON.parse(productJson)
return {
productJson: objArr||[],
...re,
}
})
this.detailObj = { plateDtoList: filterData, ...rest }
this.assignTasks(this.detailObj)
// #ifdef MP
uni.getSystemInfo({
success: (res) => {
that.screenWidth = res.windowWidth
},
})
this.getWxCode()
// #endif
// #ifdef H5
that.screenWidth = document.documentElement.clientWidth
this.posterOptions.QrOption.QrUrl = window.location.href
this.isGetOk = true
// #endif
// 判断是否设置了分享海报的图片
let isHaveImg = this.detailObj.posterImg ? true : false
if(!isHaveImg){
this.downImg(that.posterOptions.logOption.logUrl, that.posterOptions.logOption, 'logUrl')
this.downImg(that.posterOptions.buttonOption.bottomUrl, that.posterOptions.buttonOption, 'bottomUrl')
}
this.$set(this.posterOptions, 'isHavePosterImg', isHaveImg)
this.$set(
this.posterOptions.textOption,
'text',
this.detailObj.activityName
)
// 设置img图片和canvas大小
let posterImg = this.detailObj.posterImg
? this.detailObj.posterImg
: this.detailObj.activityDetailHeadImg
// 设置大图的url
this.downImg(posterImg, this.posterOptions.poster, 'url')
uni.getImageInfo({
src: posterImg,
success(res) {
let imgW = res.width //图片宽度
let imgH = res.height //图片高度
let per = imgW / that.screenWidth
that.$set(
that.posterOptions.poster,
'canvasWidth',
that.screenWidth
)
// 是否设置分享海报
let canvasHeight = isHaveImg
? imgH / per
: imgH / per + that.posterOptions.buttonOption.h
that.$set(that.posterOptions.poster, 'canvasHeight', canvasHeight)
// 设置二维码位置
let codeX = isHaveImg ? that.screenWidth - 95 : 5
that.$set(that.posterOptions.QrOption, 'x', codeX)
let codeY = isHaveImg ? imgH / per - 95 : canvasHeight - 95
that.$set(that.posterOptions.QrOption, 'y', codeY)
if (!isHaveImg) {
// 如果没有设置分享海报
that.$set(
that.posterOptions.buttonOption,
'w',
that.screenWidth
)
that.$set(that.posterOptions.buttonOption, 'y', imgH / per)
that.$set(that.posterOptions.textOption, 'x', codeX + 120)
that.$set(that.posterOptions.textOption, 'y', codeY + 40)
}
},
})
}
})
},
// 将网络图片缓存到本地
downImg(img, obj, name) {
let that = this
uni.downloadFile({
url: img,
success: (res) => {
that.$set(obj, name, res.tempFilePath)
},
})
},
// 分享方法
fx(val) {
let url = window.location.href
// 获取usercode
let urls
let userInfo = this.$utils.getStorageSync('userInfo')
let routes = getCurrentPages() // 获取当前打开过的页面路由数组
let curParam =
routes[routes.length - 1].options ||
routes[routes.length - 1].$route.query //获取路由参数
if (userInfo) {
curParam.userCode = userInfo.userCode
}
let a = this.$utils.pageliks(curParam, routes)
let b = window.location.origin
if (userInfo) {
urls = `${b}${a}`
} else {
urls = url
}
let data = {
url: url,
}
let shares = {
title: this.detailObj.shareTitle || this.$config.serviceProvider,
link: urls,
desc:this.detailObj.shareContent||'',
imgUrl: this.detailObj.shareImg || '',
}
if (val == 1) {
getShopWxConfig(data, shares, val)
} else {
share(shares)
}
},
},
}
</script>
<style lang="scss" scoped>
.width-100 {
width: 100%;
}
.contentBgc {
min-height: 100vh;
width: 100vw;
padding-bottom: 20rpx;
}
</style>
</code></pre><h4>另一种</h4><pre><code><template>
<div class="poster-wrapper" @click="closePoster($event)">
<div class="poster-content">
<canvas
canvas-id="qrcode"
v-if="qrcode"
:style="{ opacity: 0, position: 'absolute', top: '-1000px' }"
></canvas>
<canvas
canvas-id="poster"
v-if="!iscomplete"
:style="{
width: cansWidth + 'px',
height: cansHeight + 'px',
opacity: 0,
}"
></canvas>
<image
v-if="iscomplete"
:style="{ width: cansWidth + 'px', height: cansHeight + 'px' }"
:src="tempFilePath"
@longpress="longpress"
></image>
</div>
</div>
</template>
<script>
export default {
data() {
return {
bgImg:
'https://cdn.img.up678.com/ueditor/upload/image/20211130/1638258070231028289.png', //画布背景图片
cansWidth: 288, // 画布宽度
cansHeight: 410, // 画布高度
projectImgWidth: 223, // 中间展示图片宽度
projectImgHeight: 167, // 中间展示图片高度
schoolImgWidth: 110, // 中间展示高校宽度
schoolImgHeight: 110, // 中间展示高校高度
qrShow: true, // 二维码canvas
qrData: null, // 二维码数据
tempFilePath: '', // 生成图路径
iscomplete: false, // 是否生成图片
}
},
created() {
this.ctx = uni.createCanvasContext('poster', this)
},
methods: {
closePoster(e) {
if (e.target.id === e.currentTarget.id) {
// 关闭
this.$emit('close')
}
},
// 绘制分享图片
async drawerposter(name = '南京雨花台区', url, projectImg) {
uni.showLoading({
title: '加载中....',
mask: true,
})
await this.createQrcode(url)
//背景
await this.drawWebImg({
url: this.bgImg,
x: 0,
y: 0,
width: this.cansWidth,
height: this.cansHeight,
})
// 展示图
await this.drawWebImg({
url: projectImg,
x: 33,
y: 90,
width: this.projectImgWidth,
height: this.projectImgHeight,
})
await this.drawerText({
text: name,
x: 15,
y: 285,
color: '#241D4A',
size: 15,
bold: true,
center: true,
shadowObj: { x: '0', y: '4', z: '4', color: 'rgba(173,77,0,0.22)' },
})
// 二维码
await this.drawQrcode()
this.tempFilePath = await this.saveCans()
this.iscomplete = true
uni.hideLoading()
},
// 绘制分享学校
async drawposterschool(name, url, shoolImg) {
uni.showLoading({
title: '加载中...',
mask: true,
})
await this.createQrcode()
// 背景
await this.drawWebImg({
url: this.bgImg,
x: 0,
y: 0,
width: this.schoolImgWidth,
height: this.schoolImgHeight,
})
await this.drawText({
text: name,
x: 15,
y: 285,
color: '#241D4A',
size: 15,
bold: true,
center: true,
shadowObj: { x: '0', y: '4', z: '4', color: 'rgba(173,77,0,0.22' },
})
// 二维码
await this.drawQrcode()
this.tempFilePath = await this.saveCans()
this.iscomplete = true
uni.hideLoading()
},
drawWebImg(conf) {
return new Promise((resolve, reject) => {
uni.downloadFile({
url: conf.url,
success: (res) => {
this.ctx.drawImage(
res.tempFilePath,
conf.x,
conf.y,
conf.width ? conf.width : '',
conf.height ? conf.height : ''
)
this.ctx.draw(true, () => {
resolve()
})
},
fail: (err) => {
reject(err)
},
})
})
},
drawSchoolImg(conf) {
return new Promise((resolve,reject)=>{
uni.downloadFile({
url:conf.url,
success:(res)=>{
this.ctx.save()
this.ctx.beginPath()
this.ctx.arc(135,170,70,0,2*Math.PI)
// this.ctx.setFillStyle('blue')
// this.ctx.fill()
this.ctx.clip()
this.ctx.drawImage(res.tempFilePath,conf.x,conf.y,conf.width,conf.height)
this.ctx.restore()
this.ctx.draw(true,()=>{
resolve()
})
},
fail:err=>{
reject(err)
}
})
})
},
drawText(conf){
return new Promise((resolve,reject)=>{
this.ctx.restore()
this.stx.setFillStyle(conf.color)
if(conf.bold)this.ctx.font = `normal bold ${conf.size}px sans-serif`
this.ctx.setFontSize(conf.size)
let x = conf.x
conf.text = this.fittingString(this.ctx,conf.text,280)
if(conf.center){
let len = this.ctx.measureText(conf.text)
x = this.cansWidth/2 - len.width/2 +2
}
this.ctx.fillText(conf.text,x,conf.y)
this.ctx.draw(true,()=>{
this.ctx.save()
resolve()
})
})
},
// 文本标题溢出隐藏处理
fittingString(_ctx,str,maxWidth){
let strWidth = _ctx.measureText(str).width;
const ellipsis = "..."
const ellipsisWidth = _ctx.measureText(ellipsis).width;
if(strWidth<=maxWidth||maxWidth<=ellipsisWidth){
return str
}else{
var len = str.length
while(strWidth >= maxWidth-ellipsis&&len-->0){
str = str.slice(0,len)
strWidth = _ctx.measureText(str).width
}
return str + ellipsis
}
},
// 生成二维码
createQrcode(qrcodeUrl){
const config = {host:window.location.origin}
return new Promise((resolve,reject)=>{
let url = `${config.host}${qrcodeUrl}`
try{
new qrCode({
canvasId:'qrcode',
usingComponents:true,
context:this,
text:url,
size:130,
cbResult:(res)=>{
this.qrShow = false
this.qrData = res
resolve()
}
})
}catch(err){
reject(err)
}
})
},
// 画二维码 this.qrData为生成的二维码资源
drawQrcode(conf = {x:185,y:335,width:100,height:50}){
return new Promise((resolve,reject)=>{
this.ctx.drawImage(this.qrData,conf.x,conf.y,conf.width,conf.height)
this.ctx.draw(true,()=>{
resolve()
})
})
},
// canvs => images
saveCans(){
return new Promise((resolve,reject)=>{
uni.canvasToTempFilePath({
x:0,
y:0,
canvasId:'poster',
success:(res)=>{
resolve(res.tempFilePath)
},
fail:(err)=>{
uni.hideLoading()
reject(err)
}
},this)
})
}
},
}
</script>
<style lang="scss" scoped></style>
</code></pre>
拼多多排行榜
https://segmentfault.com/a/1190000042359637
2022-08-22T22:12:10+08:00
2022-08-22T22:12:10+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h4>vue配合animate和Velocity</h4><pre><code>data(){
return {
otherList: [
{
order: 4,
id:1,
word: "小明1",
time: "21`15`234",
src: "@/assets/bg_cjfx.jpg"
},
{
order: 5,
id:2,
word: "小明2",
time: "21`15`234",
src: "@/assets/bg_cjfx.jpg"
},
{
order: 6,
id:3,
word: "小明",
time: "21`15`234",
src: "@/assets/bg_cjfx.jpg"
},
{
order: 7,
id:4,
word: "小明4",
time: "21`15`234",
src: "@/assets/bg_cjfx.jpg"
},
{
order: 8,
id:5,
word: "小明5",
time: "21`15`234",
src: "@/assets/bg_cjfx.jpg"
},
{
order: 9,
id:6,
word: "小明666",
time: "21`15`234",
src: "@/assets/bg_cjfx.jpg"
},
{
order: 10,
id:7,
word: "小明777",
time: "21`15`234",
src: "@/assets/bg_cjfx.jpg"
}
],
length: "",
nowIndex: "",
preIndex: ""
}
}</code></pre><p><strong>方法</strong></p><pre><code> moveFunc(moveItem) {
this.length = this.otherList.length;
this.nowIndex = this.otherList.findIndex(
item => item.id == moveItem.id
);
if (this.nowIndex > -1) {
this.preIndex = this.nowIndex - 1;
}
let nowItem = {};
let beforeItem = {};
if (this.nowIndex > -1) {
beforeItem = this.otherList[this.preIndex];
nowItem = this.otherList[this.nowIndex];
this.$set(nowItem,'order',moveItem.order)
if (nowItem.order <= beforeItem.order) {
this.$set(beforeItem,'order',beforeItem.order+1)
nowItem = this.otherList.splice(this.nowIndex, 1)[0];
this.otherList.splice(this.preIndex, 0, nowItem);
this.downMoveClass(beforeItem);
this.upMoveClass(nowItem);
setTimeout(()=>{
this.moveFunc(moveItem)
},400)
}else {
return false
}
} else {
this.otherList.splice(this.length, 0, moveItem);
this.upMoveClass(moveItem);
}
},
downMoveClass(downItem) {
let str = `ref${downItem.id}`;
this.$nextTick(() => {
this.Velocity(
this.$refs[str][0],
{ opacity: 1 },
{
easing: "bounceOut"
}
);
this.$refs[str][0].setAttribute(
"class",
"animate__animated animate__fadeInDown"
);
});
},
upMoveClass(addItem) {
let str = `ref${addItem.id}`;
this.$nextTick(() => {
this.Velocity(
this.$refs[str][0],
{
opacity: 1,
backgroundColor: "#ccc"
},
{ easing: "bounceUpIn" }
);
this.Velocity(this.$refs[str][0], {
opacity: 1,
backgroundColor: "#fff"
});
this.$refs[str][0].removeAttribute(
"class",
"animate__animated animate__fadeInUp"
);
this.$refs[str][0].setAttribute(
"class",
"animate__animated animate__fadeInUp"
);
});
},
//启动动画
this.$refs.actionItem[0].moveFunc({
order: 4,
id:7,
word: "小红666",
time: "21`15`234",
src: "@/assets/bg_cjfx.jpg"
});</code></pre><p><img src="/img/bVc1TTe" alt="image.png" title="image.png"></p>
浅剖0.1 + 0.2 = ?
https://segmentfault.com/a/1190000042084035
2022-07-08T00:36:41+08:00
2022-07-08T00:36:41+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h3>溯源</h3><p>数字计算机当中是以二进制存储,所以这里我们需要先知道十进制转二进制的规则,和二进制转十进制的规则<br><strong>十进制转换二进制</strong><br>分两种规则:</p><ol><li>整数转二进制:除2取余,逆序排列。</li><li>小数转二进制:乘2取整,正序排列</li></ol><p>怎么理解这两句:比如 9.375这个数字,整数是9,小数是0.375</p><p>整数9转成二进制就是: 1001<br><img src="/img/bVc0J2f" alt="image.png" title="image.png"></p><p>那么小数0.375转成二进制就是:011<br><img src="/img/bVc0J2k" alt="image.png" title="image.png"><br>那么9.375转成二进制就是 1001.011</p><p><strong>可利用下面方法验证</strong></p><pre><code>console.log((9.375).toString(2))
console.log(Number.prototype.toString.call(9.375,2));
console.log(Number.prototype.toString.call(Number(9.375),2));</code></pre><p><img src="/img/bVc0J2v" alt="image.png" title="image.png"></p><p><strong>同样二进制转十进制</strong></p><ol><li>小数点前:从右往左用二进制的每个数乘以2的相应次方递增<br><img src="/img/bVc0J5S" alt="image.png" title="image.png"></li><li>小数点后:从左往右用二进制的每个数乘以2的相应负次方递增<br><img src="/img/bVc0J54" alt="image.png" title="image.png"></li></ol><p><strong>IEEE 754 双精度64位浮点数</strong><br><img src="/img/bVc0J8l" alt="image.png" title="image.png"><br>十进制有个叫科学计数法表示:<br><img src="/img/bVc0J8w" alt="image.png" title="image.png"><br><img src="/img/bVc0J8B" alt="image.png" title="image.png"></p><h3>所以</h3><p><strong>0.1的二进制</strong><br>e = -4;<br>m = 1.1001100110011001100110011001100110011001100110011010 (52位)<br><img src="/img/bVc0J7B" alt="image.png" title="image.png"><br><strong>0.2的二进制</strong> <br>e = -3;<br>m = 1.1001100110011001100110011001100110011001100110011010 (52位)<br>然后我们把它相加,这里有一个问题,就是指数不一致时,一般是往右移,因为即使右边溢出了,损失的精度远远小于左移时的溢出<br>所以转化后为</p><pre><code>e = -3; m = 0.1100110011001100110011001100110011001100110011001101 (52位)
+
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)</code></pre><p>得到</p><pre><code>e = -3; m = 10.0110011001100110011001100110011001100110011001100111 (52位)
保留一位整数
e = -2; m = 1.00110011001100110011001100110011001100110011001100111 (53位)</code></pre><p>超过了52位,做四舍五入<br>舍入与原来的数最接近的,原则是保留偶数最终的二进制数</p><pre><code>1.0011001100110011001100110011001100110011001100110100 * 2 ^ -2
=0.010011001100110011001100110011001100110011001100110100</code></pre><p>转化为十进制为<code>0.30000000000000004</code></p><h3>如何避免这个事情的发生</h3><p>我们认为可以理解的数,在计算机内部它反而是一个无限循环的小数,<br>它没有办法,它的尾数的尾数只有52位,它只有截断,进行规格化,进行舍入处理的时候可能就会导致一个精度的偏差,所以得到的是一个相对精准的结果但不是绝对的精准<br><strong>有什么办法让0.1+0.2等于0.3</strong><br><code>(0.2*100 + 0.1*100)/100</code><br><strong>那就是避免计算的时候出现小数位</strong></p>
日常ProComponent(Ant Design Pro)
https://segmentfault.com/a/1190000041813589
2022-05-08T22:03:47+08:00
2022-05-08T22:03:47+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
4
<p>ProComponent就是算是对antd的又一次集成和封装,减少了前端对于细节和联动的处理,总之就是用起来特别爽。<br>那这里就不对ProComponent做过多介绍了,我们直奔主题,如何使用,或者说如何更优雅、更方便的使用组件和编写代码一直是任何一位程序员的核心追求,我也是PiuPiuPiu~!</p><h2>columns的配置</h2><h5>treeSelect</h5><p>request可以用来调接口获取值<br>labelInValue:是设置的取到的是这个表单项值的对象,不仅仅包括value,下面的例子取到的就是fieldNames对象</p><pre><code>{
title: '区域',
dataIndex: 'areaIds',
hideInTable: true,
valueType: 'treeSelect',
request: async () => {
let res = await areaSituation();
if (res?.status == '00') {
return res.data;
}
},
fieldProps: {
showArrow: false,
filterTreeNode: true,
showSearch: true,
dropdownMatchSelectWidth: false,
autoClearSearchValue: true,
treeNodeFilterProp: 'title',
labelInValue: true,
fieldNames: {
label: 'title',
lvl: 'lvl',
value: 'nodeId',
children: 'childrenList',
},
onSelect: (_, dataObj) => {
setAreaIds([]);
let arr = [];
let getData = (data) => {
arr = [...arr, data?.nodeId];
if (data?.childrenList.length > 0) {
data?.childrenList?.forEach((item) => {
getData(item);
});
}
};
getData(dataObj);
setAreaIds(arr);
},
},
},</code></pre><h5>dateTimeRange</h5><p>ProFormDateTimePicker这个是可取值到年月日时分秒<br>ProFormDatePicker 是支持到年月日<br>可用moment设置初始值,<br>这个默认取得值是一个数组 如果搜索后端需要的这个时间字段不是数组而是其他的两个字段;可用下面的写法</p><pre><code>{
title: '操作时间',
dataIndex: 'actTime',
hideInTable: true,
valueType: 'dateRange',
// initialValue: [moment().subtract(30, 'days'), moment()],
initialValue: [
moment().subtract(1, 'month').format('YYYY-MM-DD'),
moment().format('YYYY-MM-DD'),
], //给时间搜索框设置默认开始时间一个月前--到现在的年月
fieldProps: {
placeholder: ['入库开始时间', '入库结束时间'],
},
search: {
transform: (value) => {
return {
startTime: value[0],
endTime: value[1],
};
},
},
},</code></pre><h5>radio</h5><p>status可设置渲染到table的状态<br>sorter表格的某一项排序</p><pre><code>{
title: '提成方式',
dataIndex: 'commissionMode',
sorter: true,
formItemProps:{
initialValue:1,
},
valueType: 'radio',
valueEnum: { 0: '固定', 1: '比例' },
},
{
title: '状态',
dataIndex: 'taskState',
key: 'taskState',
valueEnum: {
false: { text: '失败', status: 'error' },
true: { text: '成功', status: 'success' },
},
hideInSearch: true,
},</code></pre><h5>renderFormItem</h5><p>可用SelectAccount直接渲染组件在单列表框里面 比如在EditableProTable里面,<br>当然这里是用在搜索框的(还是提倡用valueType的方式,但是如果在多个地方使用,封装成组件这样使用也是上选)</p><pre><code> {
title: '冷藏费记账账户',
dataIndex: 'coldStorageFeeAccountId',
formItemProps: { rules: [{ required: true }] },
renderFormItem: (_, { type, ...rest }) => (
<SelectAccount labelInValue params={{ projectId: rest.recordKey }} />
),
},</code></pre><h5>digit</h5><p>渲染成只能输入数字的搜索框</p><pre><code>{
title: '件数',
dataIndex: 'quantity',
valueType: 'digit',
fieldProps: { precision: 0, min: 1 },
formItemProps: { rules: [{ required: true }] },
},
</code></pre><h4>或者可自定义在search和table的title文字不一样</h4><p>fieldProps:可设置搜素框的状态</p><pre><code>{
title: (_, type) => (type === 'table' ? '操作账号' : ''),
dataIndex: 'operatorMobile',
key: 'operatorMobile',
//fieldProps: { readOnly: true, placeholder: '请先选择杂费' },
fieldProps: {
placeholder: '操作人账号',
},
},</code></pre><h2>ProForm表单项</h2><h5>ProFormDependency</h5><p>可用来监听表单项的某一项改变之后所做的操作,<br>name为什么是数组呢?</p><ul><li>是因为在表单提交的时候呢contractType是作为inboundAppointment对象的一个属性值进行提交的,</li><li>这样写的好处就是前端在提交的函数中不用再重新组装一下这个提交参数了</li></ul><pre><code><ProFormSelect
width="sm"
name={['inboundAppointment', 'contractType']}
label="合同类型"
rules={[{ required: true, message: '请选择合同类型' }]}
initialValue={'2'}
options={[
// { value: '0', label: '零仓' },
{ value: '1', label: '包仓' },
{ value: '2', label: '无合同' },
]}
placeholder="包仓/零仓"
/>
<ProFormDependency name={[['inboundAppointment', 'contractType']]}>
{({ inboundAppointment }) => {
if (
inboundAppointment?.contractType == '1' ||
inboundAppointment?.contractType == '0'
) {
return (
<ProFormSelect
width="sm"
name={['inboundAppointment', 'contractNo']}
rules={[{ required: true, message: '请选择合同' }]}
label="合同"
options={[
{ value: '>', label: '大于' },
{ value: '<', label: '小于' },
]}
placeholder="请选择合同"
/>
);
}
}}
</ProFormDependency></code></pre><h5>ProFormFieldSet</h5><p>上面我们说到name设置成数组的形式,可以帮我们省略组装参数的操作。<br>那这个更厉害了,可以帮我们提前组装或者拆分提交表单的参数。<br>那我们看一下官方这里是怎么说的:“ProFormFieldSet 可以将内部的多个 children 的值组合并且存储在 ProForm 中,并且可以通过 transform 在提交时转化”,<br>言而简之,就是我刚说的意思,对吧~<br>下面的例子也就是将取到的SelectSubject 对象值进行拆分,保存在ProForm中。</p><pre><code> <ProFormFieldSet
width="md"
name={['subject']}
transform={(v) => ({ subjectName: v[0]?.label, subjectId: v[0]?.value })}
label="费用类型"
rules={[{ required: true }]}
>
<SelectSubject className="pro-field-md" labelInValue />
</ProFormFieldSet></code></pre><h5>ProFormText ProForm.Group ProFormDateTimePicker</h5><p>ProFormText一个简单的表单项<br>ProFormDateTimePicker:时间选择器年月日可精确到时分秒<br>ProForm.Group: 可将表单项在空间范围内允许的情况下,做一行展示</p><pre><code>const [time, setTime] = useState([moment().startOf('year'), moment().endOf('year')])
<ProForm.Group title="运输车辆">
<ProFormText
width="sm"
name={['outboundAppointment', 'plateNum']}
label="车牌号"
placeholder="请输入车牌号"
/>
<ProFormDateTimePicker
width="sm"
name={['outboundAppointment', 'expectArriveTime']}
label="预计到场时间"
/>
</ProForm.Group></code></pre><h5>ProFormDigit</h5><p>一个只能输入数字的表单项</p><pre><code><ProFormDigit
label="预收天数"
name="unitTotal"
min={1}
max={10}
placeholder="请输入预收天数"
fieldProps={{ precision: 0 }}
/></code></pre><h5>ProFormGroup</h5><p>列表项归类</p><pre><code> <ProFormGroup label="甲方信息">
<ProFormText
width="sm"
name={'namejia'}
label="甲方名称"
placeholder="请输入甲方名称"
/>
<ProFormText width="sm" name={'namejia'} label="手机号" placeholder="请输入手机号" />
<ProFormText
width="sm"
name={'namejia'}
label="身份证号"
placeholder="请输入身份证号"
/>
</ProFormGroup></code></pre><h4>DrawerForm</h4><pre><code> const [form] = ProForm.useForm();
<DrawerForm
submitter={{
render: () => {
return [
<Button key="export" type="primary" onClick={async () => {
await exportColdFeeList(exportData)
}}>
导出
</Button>,
];
},
}}
width={'70%'}
layout="horizontal"
form={form}
title="客户账单明细"
visible={visible}
onVisibleChange={setVisible}
drawerProps={{
destroyOnClose: true,
}}
/></code></pre><h4>ProDescriptions</h4><p>官方言:高级描述列表组件,提供一个更加方便快速的方案来构建描述列表。<br>我是用在列表项的详情里面的,通table的用法类似。</p><pre><code><ProDescriptions
columns={[
{ title: '客户', dataIndex: '' },
{ title: '合同类型', dataIndex: '' },
{ title: '移位日期', dataIndex: '' },
{ title: '操作人', dataIndex: 'operator' },
{ title: '操作时间', dataIndex: '' },
{ title: '确认人', dataIndex: '' },
{ title: '确认时间', dataIndex: '', span: 3 },
{
span: 3,
render: () => (
<ProTable
headerTitle="移位商品"
style={{ width: '100%' }}
rowKey="id"
dataSource={[{ id: 0 }]}
options={false}
search={false}
columns={[
{ title: '批次号', dataIndex: '' },
{ title: '商品', dataIndex: '', sorter: true },
{ title: '移出件数', dataIndex: '' },
{ title: '移出重量', dataIndex: '', sorter: true },
{ title: '移出板数', dataIndex: '', sorter: true },
{ title: '移出库位', dataIndex: '' },
{ title: '移入库位', dataIndex: '', sorter: true },
{ title: '移入板数', dataIndex: '', sorter: true },
]}
/>
),
},
{
span: 3,
render: () => (
<ProTable
headerTitle="杂费项目"
style={{ width: '100%' }}
rowKey="id"
dataSource={[{ id: 0 }]}
options={false}
search={false}
columns={[
{ title: '杂费名称', dataIndex: '' },
{ title: '收费依据', dataIndex: '', sorter: true },
{ title: '单价', dataIndex: '' },
{ title: '件数', dataIndex: '', sorter: true },
{ title: '小计', dataIndex: '', sorter: true },
]}
/>
),
},
{
span: 3,
render: () => (
<ProTable
headerTitle="作业人员"
style={{ width: '100%' }}
rowKey="id"
dataSource={[{ id: 0 }]}
options={false}
search={false}
columns={[
{ title: '姓名', dataIndex: '' },
{ title: '所属团队', dataIndex: '' },
{ title: '作业类型', dataIndex: '' },
{ title: '杂费名称', dataIndex: '' },
{ title: '作业数量', dataIndex: '' },
{ title: '作业费用', dataIndex: '' },
]}
/>
),
},
{
title: '相关附件',
dataIndex: 'filePath',
span: 3,
render: (text) => {
// let dataArr = text?.includes(',') ? text?.split(',') : [text];
// return (
// <>
// {dataArr?.map((item, index) => {
// return (
// <a key={index} href={item.url}>
// {item.name}
// </a>
// );
// })}
// </>
// );
},
},
{ title: '备注', dataIndex: 'remark', span: 3 },
]}
/></code></pre><h2>ProTable EditableProTable</h2><p>两种表格, 一种用的最多的普通表格,一种是可编辑的表格<br>使用相似度很高。</p><h6>ProTable</h6><p>这个ProTable的配置有意思的地方有三:</p><ol><li>params自带pageSize,pageNumber</li><li>rowSelection可做批量配置操作</li><li>只有单个条件搜索的时候可search设置为false,options的search单独设置,那如果你需要多个条件搜索的时候,可单独对搜索框做一些定制化配置<br>当然作为一个高级定制化组件,只有这么些集成肯定是不够的,</li></ol><p><em>高级配置</em><br><strong>tableExtraRender</strong><br>这是配置table列表上部,搜索区域的下部的中间区域,当然如果你有把这个移动到最顶部的需求,可利用css样式去覆盖,倾囊写上</p><pre><code><ProTable
tableExtraRender={() => (
<Row gutter={16}>
<Col span={6}>
<Card>
<Statistic title="入库单总数" value={topData?.all?.totalCount} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="入库吨重" value={topData?.all?.weight} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="今日入库单数" value={topData?.today?.totalCount} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="今日入库吨重" value={topData?.today?.weight} />
</Card>
</Col>
</Row>
)}
//search={{
//labelWidth: 0,
// collapsed: false,
// collapseRender: false,
// }}
rowKey="id"
scroll={{ x: 960 }}
columns={[
{ title: '商品名称', dataIndex: 'goodsName' },
{ title: '所属分类', dataIndex: 'category' },
]}
params={{ status: 0 }}
request={async (params = {}) => {
const json = await getGlobalCfgGoodsPageList(params);
return {
data: json.records,
page: params.current,
success: true,
total: json.total,
};
}}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
}}
search={false}
toolBarRender={false}
options={{
search: {
name: 'goodsName',
placeholder: '请输入商品名称',
},
}}
/>
//css样式
.inListContent {
.ant-pro-table {
display: flex;
flex-direction: column;
}
.ant-pro-table-search {
order: 2;
}
.ant-pro-table-extra {
order: 1;
}
.ant-pro-card {
order: 3;
}
}
</code></pre><h6>EditableProTable</h6><p>这个组件的使用场景一般是在修改或添加的弹框中使用居多,不要问我怎么知道,因为我常用~<br>1.trigger="onValuesChange":保证了我当前的诸如DrawerForm类似的form组件可实时拿到这个EditableProTable的值,比如下面在onfinsh中拿到的就是inboundGoodsList的值<br>2.recordCreatorProps:是手动填加列表的一些配置<br>3.editable:是编辑的一些配置</p><pre><code> <ProForm.Item label="" name="inboundGoodsList" initialValue={[]} trigger="onValuesChange">
<EditableProTable
headerTitle="入库商品"
rowKey="id"
columns={[
{
title: '商品',
dataIndex: 'goodsId',
formItemProps: { rules: [{ required: true }] },
valueType: 'select',
request: async () => {
let res = await getBasicGoodsPageList({
current: 1,
pageSize: 999,
projectId: initialState?.project?.projectId,
});
if (res.status == '00') {
return res?.records?.map((item) => {
return {
label: item.goodsName,
value: item.id,
};
});
}
},
fieldProps: () => {
return {
labelInValue: true,
};
},
},
{
title: '件重(件/kg)',
dataIndex: 'packageUnit',
formItemProps: { rules: [{ required: true }] },
valueType: 'digit',
fieldProps: { precision: 0, min: 1 },
},
{
title: '重量(kg)',
dataIndex: 'weight',
editable: false,
valueType: 'digit',
renderText: (_, record, index) => {
if (record.quantity && record.packageUnit) {
return Math.round(record.quantity * record.packageUnit);
}
},
},
{ title: '生产日期', dataIndex: 'productionDate', valueType: 'date', width: 200 },
{ title: '操作', width: 50, valueType: 'option' },
]}
recordCreatorProps={{
newRecordType: 'dataSource',
record: () => ({
id: Date.now(),
}),
}}
editable={{
type: 'multiple',
form: tableForm,
editableKeys,
onChange: setEditableRowKeys,
actionRender: (row, _, dom) => {
return [dom.delete];
},
}}
/>
</ProForm.Item>
recordCreatorProps={ref?.current?.getRowsData()[ref?.current?.getRowsData().length-1]?.weightEnd !=='∞'?{
newRecordType: 'dataSource',
record: (index, dataSource) => {
const id = Date.now();
debugger
let weightStart;
let weightEnd;
if (index === 0) {
weightStart = 0;
weightEnd = 0;
}
if (index - 1 >= 0) {
weightStart = dataSource[index - 1]?.weightEnd;
weightEnd='∞'
}
if (
ref?.current?.getRowsData().length > 0 &&
ref?.current?.getRowsData().length < index
) {
setTimeout(() => {
ref.current.setRowData(index - 1, {
weightStart: ref?.current?.getRowData(index - 2)?.weightEnd,
});
}, 0);
}
return {
weightEnd,
weightStart,
id,
};
},
}:false}
</code></pre><p>EditableProTable作为这个组件库我觉得最牛逼的组件,虽然使用起来很爽,但是还是有点点坑的存在的:<br>联动效果来说:当我联动的是一个设置了不能编辑的一项,那这个联动效果会失效,什么意思呢,好的,那上代码:</p><pre><code>{
title: '杂费',
dataIndex: 'poundage',
valueType: "select",
request: async () => {
let res = await getPoundageList({ projectId: initialState?.project?.projectId })
if (res.status == "00") {
return res?.data?.map(item => {
return {
label: item.name,
value: item.id
}
})
}
},
fieldProps: (_, { rowIndex }) => {
return {
onChange: async (value) => {
debugger
let res = await getPoundageDetail(value)
if (res.status == "00") {
let { mode, chargeDescribe } = res?.obj
// tableRef?.current?.setRowData(rowIndex,{
// modeDescribe: mode.toString(),
// chargeDescribe: chargeDescribe || ""
// })
const data = form.getFieldValue('inboundPoundageItemList')
data[rowIndex].modeDescribe = mode.toString()
data[rowIndex].chargeDescribe = chargeDescribe
form.setFieldsValue({
inboundPoundageItemList: data,
})
}
}
}
}
},</code></pre><p>一开始我尝试使用setRowData对当前这一行的其他项做联动数据修改,但是失效的,<br>这里通俗讲就是:他的编辑和不能编辑是对应的两个表单,当我编辑可编辑的这个表单,是设置不了数据到不可编辑的那个表单的,<br>对,那怎么办?<br>从数据源处下手呗:form.getFieldValue得到数据修改完之后<br>form.setFieldsValue重新赋值给这个EditableProTable对应的表单项</p><h5>ModalForm</h5><p>这样使用trigger的好处:是组件的状态visible不需要从外部传入了,<br>只要引入这个组件,这个组件内部维护这个状态。<br>submitter:可以自定义这个表单的确认按钮和取消按钮的文字</p><pre><code> <ModalForm
width="400px"
title="导入"
modalProps={{
maskClosable: false,
}}
trigger={<Button key="add">导入</Button>}
submitter={{
searchConfig: {
submitText: '导入',
resetText: '取消',
},
}}
onFinish={async (values) => onSubmit()}
></code></pre><h4>ProList ProFormTreeSelect ProFormRadio.Group</h4><p>ProFormTreeSelect、ProFormRadio.Group选取值得时候修改不了的情况在这里出现了<br>加上value属性对应修改的值</p><pre><code> const [libraryValue, setLibraryValue] = useState([]);
const [areaValue, setAreaValue] = useState({});
/* 头部元素 */
const ProListHeader = () => {
return (
<Form
form={form}
>
<ProFormTreeSelect
label="选择区域"
name="name"
placeholder="请选择区域"
value={areaValue}
allowClear
width={330}
secondary
request={async () => {
let res = await areaSituation({})
if (res.status == "00") {
return res?.data
}
}}
fieldProps={{
showArrow: false,
filterTreeNode: true,
showSearch: true,
dropdownMatchSelectWidth: false,
labelInValue: true,
autoClearSearchValue: true,
// multiple: true,
treeNodeFilterProp: 'title',
fieldNames: {
label: 'title',
lvl: 'lvl',
value: 'nodeId',
children: 'childrenList',
},
onChange: (item) => {
setAreaValue(item)
initDataList({
limitIdList: [item?.value]
})
initTotalData({
limitIdList: [item?.value]
})
}
}}
/>
<ProFormRadio.Group
width="md"
name="libraryValue"
value={libraryValue}
label=""
options={typeData}
onChange={(e) => {
setLibraryValue(e?.target?.value)
initDataList({ resourceType: e?.target?.value })
}}
/>
</Form>
);
};
const ItemColorMap = {
0: {
bgColor: 'rgba(0, 153, 255, 1)',
},
1: {
bgColor: 'gray',
},
2: {
bgColor: 'rgba(0, 153, 255, 1)',
},
3: {
bgColor: 'red',
},
};
<ProList
className="operateContent"
pagination={{
defaultPageSize: 8,
showSizeChanger: false,
}}
grid={{ gutter: 16, column: 1 }}
metas={{
title: {},
subTitle: {},
type: {},
avatar: {},
content: {},
actions: {},
}}
rowKey="id"
headerTitle={<ProListHeader />}
dataSource={
listData.map((unit, unitIndex) => (
{
title: ``,
subTitle: false,
actions: false,
avatar: false,
content: (
<div key={unit?.id} >
<div className='identification'><span className='lineStyle' />{unit?.floorNumber}楼</div>
<div className="floorBox">
{unit?.list?.map((item, index) => (
<div key={item?.locationId} className="floor" style={{ backgroundColor: ItemColorMap[item?.type]?.bgColor
}} onClick={()=>{setItemObj(item),setVisible(true)}}>
<div className='actualAreaBox'>
<span>{item?.locationName}</span>
<span className='actualArea'>{item?.actualArea}m²</span>
<span>{item?.inventory}T</span>
</div>
<div className='actualAreaBox'>
<span>{item?.type == 1 ? "闲置" : item?.usage}</span>
{
item?.type == 2 && <span className='overDay'>即将过期</span>
}
{
item?.type == 3 && <span className='overDay'>已过期</span>
}
</div>
</div>
))
}
</div>
</div>
)
}
))
}
/></code></pre><h4>StatisticCard 指标卡</h4><pre><code> <StatisticCard.Group direction={responsive ? 'column' : undefined}>
<StatisticCard
statistic={{
title: '仓库容量',
value: `${statistic?.totalCapacity}T`,
icon: <IconFont type="icon-shujuku" style={{ fontSize: 42 }} />,
}}
/>
</StatisticCard.Group></code></pre><p><img src="/img/bVc0DBt" alt="image.png" title="image.png"></p><p>最后这篇文章,如果帮助到你,或者让你有所领悟,还希望可以不吝 点赞关注~</p>
前端导出
https://segmentfault.com/a/1190000041804932
2022-05-07T08:06:27+08:00
2022-05-07T08:06:27+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p>对于表格或者文件的导出前端所能做的处理一般分为两种情况:</p><ol><li>一是后端返回文件路径的形式,前端直接下载</li><li>一是后端返回文件流的形式</li></ol><p>第一种情况需要文件服务器,前端去文件服务器去下载,这种不做讨论。<br>本文侧重于第二种情况:后端返回文件流的形式:<br>处理方法1:<br>请求头处理 application/vnd.ms-excel</p><pre><code>
export async function getTeamBillDetail(params) {
// 团队对账单-明细
return request(`/wms/bill/getTeamBillDetail`, {
method: 'POST',
headers: { 'Content-Type': 'application/vnd.ms-excel' },
responseType:'blob',
data: params,
});
}</code></pre><p><strong>使用</strong></p><pre><code>let res = await exportTeamBill(exportData);
const excel = 'application/vnd.ms-excel'
const blob = new Blob([res], { type: excel })
const objectUrl = URL.createObjectURL(blob)
const btn = document.createElement('a') // 转换完成,创建一个a标签用于下载
// btn.download = "a.excel"
const name = res.headers['content-disposition']
btn.download = name.split('=')[1]
btn.href = objectUrl
btn.click()
URL.revokeObjectURL(objectUrl)
btn = null
</code></pre><p><strong>或者</strong></p><pre><code> //下载处理逻辑
const filename = res.headers["content-disposition"];
const blob = new Blob([res.data]);
var downloadElement = document.createElement("a");
// URL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL. 这个URL的生命仅存在于它被创建的这个文档里. 新的对象URL指向执行的File对象或者是Blob对象.
var href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = decodeURIComponent(filename.split("filename=")[1]);
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
// URL.revokeObjectURL()方法会释放一个通过URL.createObjectURL()创建的对象URL. 当你要已经用过了这个对象URL,然后要让浏览器知道这个URL已经不再需要指向对应的文件的时候,就需要调用这个方法.
window.URL.revokeObjectURL(href);</code></pre><p><strong>方法二 后端返回application/msword</strong></p><ul><li>前端对于content-type还用application/json</li><li><p>但是后端的返回的响应头response header中要有这两个<br><img src="/img/bVcZzxp" alt="image.png" title="image.png"><br><strong>封装公用的工具函数excelDownload</strong></p><pre><code>function download(blobData, forDownLoadFileName) {
const aLink = document.createElement('a');
document.body.appendChild(aLink);
aLink.style.display = 'none';
aLink.href = window.URL.createObjectURL(blobData);
aLink.setAttribute('download', forDownLoadFileName);
aLink.click();
document.body.removeChild(aLink);
}
export async function excelDownload(url, options = {}) {
const keys = getSecretToken();
options.headers = {
...keys,
'content-type': 'application/json',
};
const response = await fetch(url, options);
const forDownLoadFileName = response.headers.get('content-disposition').split('=')[1];
const blobData = await response.blob();
await download(blobData, decodeURIComponent(forDownLoadFileName));
return forDownLoadFileName;
}</code></pre></li></ul><p><strong>使用 前端直接调接口函数</strong></p><pre><code>export async function exportTeamBill(params) {
return excelDownload(`/wms/bill/exportTeamBill`, {
method: 'POST',
body: JSON.stringify(params),
});
}</code></pre><p><strong>最后奉上getSecretToken函数以及MD5的封装</strong></p><pre><code>export const getSecretToken = () => {
const token = localStorage.token; // 从user model里取出token
const secret = localStorage.secret; // 从user model里取出token
let code = '';
code += token;
code += secret;
let midSign = MD5.md5(code.split('').sort().join(''));
const nonce = new Date().getTime();
midSign += nonce;
const signature = MD5.md5(midSign.split('').sort().join(''));
return {
managerToken: token,
managerSecret: secret,
signature,
nonce,
};
};</code></pre><pre><code>/* eslint-disable */
function md5(string) {
let x = Array();
let k;
let AA;
let BB;
let CC;
let DD;
let a;
let b;
let c;
let d;
const S11 = 7;
const S12 = 12;
const S13 = 17;
const S14 = 22;
const S21 = 5;
const S22 = 9;
const S23 = 14;
const S24 = 20;
const S31 = 4;
const S32 = 11;
const S33 = 16;
const S34 = 23;
const S41 = 6;
const S42 = 10;
const S43 = 15;
const S44 = 21;
str = Utf8Encode(string);
x = ConvertToWordArray(str);
a = 0x67452301;
b = 0xefcdab89;
c = 0x98badcfe;
d = 0x10325476;
for (k = 0; k < x.length; k += 16) {
AA = a;
BB = b;
CC = c;
DD = d;
a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478);
d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);
c = FF(c, d, a, b, x[k + 2], S13, 0x242070db);
b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);
a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);
d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a);
c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613);
b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501);
a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8);
d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);
c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);
b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be);
a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122);
d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193);
c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e);
b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821);
a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562);
d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340);
c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51);
b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);
a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d);
d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);
b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);
a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);
d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6);
c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);
b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed);
a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);
d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);
c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9);
b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);
a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942);
d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681);
c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);
b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c);
a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44);
d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);
c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);
b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);
a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);
d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);
c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);
b = HH(b, c, d, a, x[k + 6], S34, 0x4881d05);
a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);
d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);
c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);
b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);
a = II(a, b, c, d, x[k + 0], S41, 0xf4292244);
d = II(d, a, b, c, x[k + 7], S42, 0x432aff97);
c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7);
b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039);
a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3);
d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);
c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d);
b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1);
a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);
d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);
c = II(c, d, a, b, x[k + 6], S43, 0xa3014314);
b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1);
a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82);
d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235);
c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);
b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391);
a = AddUnsigned(a, AA);
b = AddUnsigned(b, BB);
c = AddUnsigned(c, CC);
d = AddUnsigned(d, DD);
}
const temp = WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d);
return temp.toUpperCase();
}
function RotateLeft(lValue, iShiftBits) {
return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
}
function AddUnsigned(lX, lY) {
const lX4 = lX & 0x40000000;
const lY4 = lY & 0x40000000;
const lX8 = lX & 0x80000000;
const lY8 = lY & 0x80000000;
const lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);
if (lX4 & lY4) {
return lResult ^ 0x80000000 ^ lX8 ^ lY8;
}
if (lX4 | lY4) {
if (lResult & 0x40000000) {
return lResult ^ 0xc0000000 ^ lX8 ^ lY8;
}
return lResult ^ 0x40000000 ^ lX8 ^ lY8;
}
return lResult ^ lX8 ^ lY8;
}
function F(x, y, z) {
return (x & y) | (~x & z);
}
function G(x, y, z) {
return (x & z) | (y & ~z);
}
function H(x, y, z) {
return x ^ y ^ z;
}
function I(x, y, z) {
return y ^ (x | ~z);
}
function FF(a, b, c, d, x, s, ac) {
a1 = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a1, s), b);
}
function GG(a, b, c, d, x, s, ac) {
a1 = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a1, s), b);
}
function HH(a, b, c, d, x, s, ac) {
a1 = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a1, s), b);
}
function II(a, b, c, d, x, s, ac) {
a1 = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
return AddUnsigned(RotateLeft(a1, s), b);
}
function ConvertToWordArray(string) {
let lWordCount;
const lMessageLength = string.length;
const lNumberOfWords_temp1 = lMessageLength + 8;
const lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
const lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
const lWordArray = Array(lNumberOfWords - 1);
let lBytePosition = 0;
let lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
// eslint-disable-next-line operator-assignment
lWordArray[lWordCount] =
lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition);
lByteCount += 1;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
// eslint-disable-next-line operator-assignment
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
}
function WordToHex(lValue) {
let WordToHexValue = '';
let WordToHexValue_temp = '';
let lByte;
let lCount;
for (lCount = 0; lCount <= 3; lCount += 1) {
lByte = (lValue >>> (lCount * 8)) & 255;
WordToHexValue_temp = `0${lByte.toString(16)}`;
WordToHexValue = `${WordToHexValue}${WordToHexValue_temp.substr(
WordToHexValue_temp.length - 2,
2,
)}`;
}
return WordToHexValue;
}
function Utf8Encode(string) {
let utftext = '';
for (let n = 0; n < string.length; n += 1) {
const c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if (c > 127 && c < 2048) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
}
module.exports = {
md5,
};
</code></pre>
React-native 开发小技巧
https://segmentfault.com/a/1190000041212722
2022-01-01T16:38:15+08:00
2022-01-01T16:38:15+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h3>1. isFocused</h3><p>我们知道对于App来说,对于页面的跳转不像PC端,Pc端如果跳转页面,则上一个页面会卸载,APP则不然,它是一个页面盖在另一个页面上面,怎么理解呢,就是当前页面盖在上一个页面上。<br>那这个特性也就会导致我们开发的时候需要去考虑规避二件事:<br>1.页面不会卸载,返回的时候,不会重新请求页面 <br>2.或者说有的是高消耗的页面不在当前页面了,也会一直在消耗手机性能<br><strong>这时候就要隆重介绍一个新的属性了:isFocused</strong><br>这个属性可以让我们得知:是否在当前页面,如果是:true,否则就是false<br>长话短说直接上代码:</p><ol><li><p>解决返回页面不会刷新的问题</p><pre><code>import {useIsFocused} from "@react-navigation/native";
const isFocused = useIsFocused()
useEffect(()=>{
if(isFocused){
getListData()
}
},[isFocused])</code></pre></li><li><p>解决不在高消耗页面,还在消耗</p><pre><code>isFocused && (
<Camera onCodeRead={(code)=>{
const url = parse(code);
navigate('WebScreen',{uri:url})
}}/>
)</code></pre></li></ol><h3>2.react-native flex布局</h3><p>一般我们使用flex布局的主轴是row,但是在react-native中主轴是column <br>为什么会是这样呢,因为手机横屏没有竖屏长,react-native才故意这样设计的。下面的例子是兼容性的展示九宫格,每行三个。<br><strong> 例子1</strong></p><p><img src="/img/bVcW5sU" alt="Image.png" title="Image.png"></p><p>在移动端页面获取屏幕的宽度一般是vw,vh,但是在RN中则是这样的:</p><pre><code>import {Dimensions} from 'react-native'
const {width:screenWidth,height:screenHeight} = Dimensions.get('window')
export {screenHeight,screenWidth};</code></pre><p>然后设置间隙和每个盒子的宽高:<br><em>整屏幕的宽度 - 两边的padding - 头像的宽度-头像右侧的宽度-两个间隙</em></p><pre><code>let cellGap = 5;
let cellWidth = (screenWidth - 10*2-32-10-cellGap*2)/3
<View style={{
width:cellWidth,
height:cellHeight,
backgroundColor:"blue"
}} /></code></pre><p><strong> 例子二:</strong><br><img src="/img/bVcW5ts" alt="Image.png" title="Image.png"></p><p><em>alignSelf 不遵从父元素的排列规则,按自己的规则来</em></p><pre><code>
<View style={{
width:cellWidth,
height:cellHeight,
backgroundColor:"blue",
alignSelf:'flex-end',
}} /></code></pre><p><strong>例子三:</strong> marginRight:'auto'自动边距</p><p><img src="/img/bVcW6xf" alt="企业微信截图_16411939908399.png" title="企业微信截图_16411939908399.png"></p><pre><code><View
style={{
width:cellWidth,
height:cellHeight,
marginRight:'auto',
backgroundColor:'yellow'
}}
/></code></pre><h3>利用useState对数据源过滤或初始化</h3><pre><code>const [feelLikes,setFeelLikes] = React.useState(
item.feelLikes?Map(({useId})=>useId)||[],
)</code></pre><h3>qs</h3><p>项目中Get请求,往接口传参一般是key=value&key=value格式,对于多参数,或条件参数多有不便,<br> 可以使用qs(queryString)插件<br> -- qs.stringify() 将对象解析成url<br> -- qs.parse() 是将url解析成对象</p><pre><code>
import qs from 'qs'
const {data} = await get(
`/feed?${qs.stringify({
offset:isRefresh?0:listData.length,
limit,//跨度值
useId:showMyself?user.id:undefined
})}`
)</code></pre><h3>useRef用于成员变量</h3><p>通常我们我们在组件内let 定义一个状态变量,平常使用没什么问题,<br>但是当我们想要频繁使用的时候(在接口还没反应过来的时候),<br>第一次后的每一次都会给这个变量重新初始化,那这个状态变量对于整个逻辑来说就乱了,所以这里需要使用useRef来定义成员变量</p><pre><code>const isEndReached = React.useRef(false)
const isFetching = React.useRef(false)
if(isRefresh){
currentCount = data.rows.length;
setListData(data.rows)
}else{
currentCount = data.rows.length + listData.length
}
if(currentCount>=data.count){
isEndReached.current = true
} else {
isEndReached.current = false
}
</code></pre><h3>多个相同的子组件的条件下,在父组件中维护一个变量</h3><p><strong>子组件 FeedItem</strong></p><pre><code><View style={StyleSheet.metaTextContainer}>
<Text style={StyleSheet.metaText}>
{fromNow}
</Text>
<TouchableOpacity
style={styles.likeButton}
onPress={onPress}
>
<Icon
style={[styles.likeIcon,choosen&&styles.action]}
name={choosen?'heart':'heart-o'}
/>
<Text style={styles.metaText}>{feedLikes.length}</Text>
</TouchableOpacity>
</View></code></pre><p><strong>父组件</strong></p><pre><code><SafeAreaView
style={styles.container}
<FlatList <Feed>
data={listData}
refreshing={loading === 'refresh'}
onRefresh={()=>getListData(true)}
keyExtractor={(item)=>String(item.id)}
renderItem={({item,index})=>(
<FeedItem
choosen={choseIndex === index}
item={item}
onPress={()=>setChoseIndex(index)}
/>
)}
/>
>
</SafeAreaView></code></pre>
React-Native运行报错问题汇总 以及Taro小程序异常
https://segmentfault.com/a/1190000041160277
2021-12-22T13:29:39+08:00
2021-12-22T13:29:39+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h2>RN</h2><h4>1.本地打包失败,缺少sentry配置</h4><p><img src="/img/bVcWRNo" alt="image.png" title="image.png"><br><strong>注释</strong><br><img src="/img/bVcWRNp" alt="image.png" title="image.png"></p><h4>2.运行pod install的时候,use_native_modules 找不到本地的包</h4><p><img src="/img/bVcWRNq" alt="image.png" title="image.png"><br><strong>手动修改@react-native-commutiy下的包的路径 找到这个包下的bin.js本地的路径</strong><br><img src="/img/bVcWRNv" alt="image.png" title="image.png"></p><h4>3.运行yarn android的时候报错</h4><p><img src="/img/bVcWRNx" alt="image.png" title="image.png"><br><strong>手动修改android目录下的build.gradle文件,新增</strong><br><img src="/img/bVcWRNy" alt="image.png" title="image.png"></p><h5>4.运行yarn android的时候报错</h5><p><img src="/img/bVcWRNA" alt="image.png" title="image.png"><br><strong>手动复制jdk文件地址(注意替换为自己的地址)</strong></p><pre><code>sudo cp /Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/lib/tools.jar /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/lib</code></pre><h5>5.运行yarn android的时候报错</h5><p><img src="/img/bVcWRNJ" alt="image.png" title="image.png"><br><strong>手动修改android下的compileSdkVersion和targetSdkVeision为29</strong><br><img src="/img/bVcWRNL" alt="image.png" title="image.png"></p><h5>6.运行yarn android的时候报错 cannot find moudle react-native-pager-view at @ant-design/react-native/lib/carousel</h5><p><strong>手动安装react-native-pager-view</strong></p><h4>7运行yarn start的时候报错,找不到模块</h4><p><img src="/img/bVcWRNP" alt="image.png" title="image.png"><br><strong>手动安装@react-native-picker</strong></p><h4>8APP拉起小程序支付后无法返回APP</h4><p><strong><a href="https://link.segmentfault.com/?enc=mxG12ebRRyWoU8p6jR%2BTVg%3D%3D.QG5oBQ19nXOjLdW1UMih6WRGIJkIxaT%2BmTg7rBou4gdxJIZTJOS6UeSiTaJc8TjcngOylUfnVKoA29hZyw1mpbZRgeNqWImAFsCL39ExpqP11RcvpEsjYj%2FloucMFt1t" rel="nofollow">https://developers.weixin.qq....</a> 此接口用户可手动触发返回App 只能返回拉起小程序的App</strong></p><h4>9react-native-image-picker 502</h4><p><img src="/img/bVcWRNW" alt="image.png" title="image.png"><br><strong>手动注释android/build.gradle中圈中的部分</strong><br><img src="/img/bVcWRNX" alt="image.png" title="image.png"></p><h4>10 maven { url '<a href="https://link.segmentfault.com/?enc=ZFWfmPUArutBg6M719ueRw%3D%3D.cNuLEA3lIDW%2BRaVWzc2vgrXaJIhs%2BCTGBb4ddCUYs40agtjreDE1KYV%2Bqiw8vblB" rel="nofollow">https://dl.bintray.com/umsdk/...</a>' } 访问不了</h4><p><img src="/img/bVcWRNY" alt="image.png" title="image.png"></p><p><strong>按图示中替换maven { url '<a href="https://link.segmentfault.com/?enc=%2F8TldU9y5b3P9j0vqOiWtA%3D%3D.REMNYu4IOCvL4rF69HzCXh1A%2BLfnbJr9s%2FRoetmR3k8%3D" rel="nofollow">https://repo1.maven.org/maven2/</a>' }</strong><br><img src="/img/bVcWRN5" alt="image.png" title="image.png"></p><h4>11 RN 图片不显示</h4><pre><code>xcode12导致 在文件react-native/Libraries/Image/RCTUIImageViewAnimated.m中的
if (_currentFrame) {...}后加
else {
[super displayLayer:layer];
}
</code></pre><h4>12 升级到最新的mac 系统 monterey, xcode 13,运行yarn ios报错</h4><p><img src="/img/bVcWRN8" alt="image.png" title="image.png"><br><strong>解决方法</strong><br><img src="/img/bVcWROa" alt="image.png" title="image.png"></p><h4>13 Xcode报错An organization slug is required (provide with --org)</h4><p><img src="/img/bVcWRR3" alt="企业微信截图_537845f3-7664-46f0-9eba-15485d450429.png" title="企业微信截图_537845f3-7664-46f0-9eba-15485d450429.png"><br><strong>在ios文件下新建sentry.properties文件</strong></p><pre><code>defaults.project=sentry新建的具体项目名
defaults.org=组织名 // 比如https://sentry.xxxx.com/organizations/sentry1111/projects/,组织名为sentry1111
defaults.url=https://sentry.xxxx.com // 私有化部署sentry的,取自己域名;走官方sentry的该值不需要配置,直接删除
auth.token=authtoken // 这个值的获取比较麻烦, 私有化参照下面步骤获取,官方的应该走项目设置直接能生成
</code></pre><h4>14 如果Xcode点击这个文件运行不起来</h4><p><img src="/img/bVcWSE8" alt="企业微信截图_2430d6a1-0069-4c8e-92d6-3668c172cd31.png" title="企业微信截图_2430d6a1-0069-4c8e-92d6-3668c172cd31.png"></p><pre><code>
1. 可以在ios文件夹下运行yarn start
2. 在项目的最外层运行 yarn run ios</code></pre><h2>Taro</h2><h4>15 项目编译小程序报错</h4><p><strong> taro相关依赖 要与taro-cli保持一致</strong></p><h4>16 修改tabbar尺寸</h4><p><strong>app.less中直接修改.weui-tabbar__icon的宽高</strong></p><h4>17 进入列表页闪屏</h4><pre><code>重新返回列表页的生命周期
componentDidShow 只用来init
在程序切后台生命周期
componentDidHide中重置查询参数(包括loading参数)
</code></pre><h4>18 开发环境小程序启动白屏</h4><pre><code>src/app.config.ts
底部plugins中存在未授权插件 注释掉即可</code></pre><h4>19 微信开发者工具运行会出现白屏,编译失败</h4><p><img src="/img/bVcWRPw" alt="image.png" title="image.png"><br><strong>升级系统,升级微信开发者工具</strong></p><h4>20 启动后 跳转页面报错 空页面</h4><p><img src="/img/bVcWRPx" alt="image.png" title="image.png"><br><strong>升级微信开发者工具</strong></p><h4>Xcode 打包的内容与自己编写内容不一致</h4><p><img src="/img/bVcW0vq" alt="image.png" title="image.png"></p><pre><code>jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];</code></pre><p>修改成</p><pre><code>jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];//开发包</code></pre>
前端怎么配置刚到手的mac和常用的快捷键
https://segmentfault.com/a/1190000041132195
2021-12-16T22:39:56+08:00
2021-12-16T22:39:56+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h3>安装终端iTerm2</h3><p><code>https://iterm2.com/</code></p><h3>Homebrew 包管理工具</h3><h5>安装brew</h5><p>自动脚本(全部国内地址)<a href="镜像">https://zhuanlan.zhihu.com/p/111014448</a></p><pre><code>/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"
</code></pre><p>苹果电脑 卸载脚本:</p><pre><code>/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/HomebrewUninstall.sh)"
</code></pre><h3>安装git</h3><p>如果有装Xcode,则会自带git,Xcode用于Ios开发。如果没有可以用brew下载<br>`brew install git<br><strong>配置基础信息和 SSH</strong></p><pre><code>$ git config --global user.name "jianhao" // 设置用户名
$ git config --global user.email "jianXXXia@163.com" // 设置邮箱</code></pre><p><strong>生成公钥</strong><br><code>ssh-keygen</code><br><strong>查看公钥</strong><br><code>cat ~/.ssh/id_rsa.pub</code></p><h3>安装node环境</h3><p>安装 nvm 之后最好先删除下已安装的 node 和全局 node 模块:</p><pre><code>npm ls -g --depth=0 #查看已经安装在全局的模块,以便删除这些全局模块后再按照不同的 node 版本重新进行全局安装
sudo rm -rf /usr/local/lib/node_modules #删除全局 node_modules 目录
sudo rm /usr/local/bin/node #删除 node
cd /usr/local/bin && ls -l | grep "../lib/node_modules/" | awk '{print $9}'| xargs rm #删除全局 node 模块注册的软链
</code></pre><p><strong>安装 nvm</strong></p><pre><code>curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.29.0/install.sh | bash</code></pre><h2>node版本管理 n</h2><pre><code>brew install n
sudo n 12.22.2 下载版本
sudo n 多版本选择版本</code></pre><p><strong>brew 安装</strong></p><pre><code>brew install nvm
安装完成后,必须在你的 .bash_profile 加入以下这行,让你可以直接在shell使用nvm指令
source $(brew --prefix nvm)/nvm.sh
保存环境配置,重新source你的 .bash_profile 来让设定生效,不然每次都得重配
source ~/.bash_profile</code></pre><p><strong>如果是第二次需重配 – 解决方式</strong><br>打开文件open .bash_profile<br>复制下面的进去</p><pre><code>source ~/.bashrc
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion</code></pre><p><strong>把nvm下载配置成国内镜像</strong></p><pre><code>NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node</code></pre><p>安装完成后请重新打开终端环境,Mac 下推荐使用 oh-my-zsh 代替默认的 bash shell。</p><p><strong>选择合适的版本安装</strong></p><pre><code>nvm install 10.15.3 // 安装指定版本的node,会自动切换到该版本
nvm install node // 安装最新稳定版本的node(即current版本)
nvm use 10.15.3 //设置使用的node版本
nvm alias default 0.12.7 #设置默认 node 版本为 0.12.7 (不然终端关掉后 重新打开就又恢复第一个下载的版本)
</code></pre><p><strong>查看安装是否成功,以及nvm当前使用的node版本</strong><br> <code>node -v</code></p><h4>卸载nvm</h4><p>卸载nvm<br>移除nvm内容以及删除nvm,切记使用完命令后重启终端。<br>`1: cd ~<br>2: rm -rf .nvm`<br><em>删除nvm中某一个node版本</em><br><code>nvm uninstall 版本号</code></p><h3>修改hosts文件</h3><p>打开finder,Command + Shift + G ,输入/etc/hosts,<br>把host文件拖到桌面,修改之后,放回原文件夹,即可成功修改host</p><h3>下载vscode</h3><p>然后vscode的前端插件</p><h3>下载搜狗输入法</h3><p>别问为什么下载这个输入法,问就是自带的赶脚不好用</p><h3>印象笔记 一处记录,多端可看</h3><p>云笔记 记得东西比较多,</p><h3>chorme浏览器 前端标配</h3><p>yyds,然后浏览器插件 redux-tools...</p><ul><li>AdBlock 最佳广告拦截工具</li><li>Eye Dropper 颜色选取工具</li><li>FeHelper JSON 自动格式化、手动格式化,支持排序、解码、下载...</li><li>Google 翻译 翻译网页</li><li>Infinity 新标签页主题自定义工具</li><li>The Great Suspender 冻结暂时用不到的标签页,以便释放系统资源</li><li>The QR Code Extension 将链接转为二维码,方便访问</li><li>Wappalyzer 分析当前网页用的技术</li><li>沙拉查词 划词翻译工具</li><li>Tampermonkey <a href="https://link.segmentfault.com/?enc=JE%2Fg4inY6MJ4Q66Y%2FHAx5A%3D%3D.w0taQ7U2Dp22AZNEZ96KNqtFhgQY5XewwmatOEKosys%3D" rel="nofollow">油猴</a>通过安装各类脚本对网站进行定制</li><li>Toby for Chrome 整理多个标签页,类似的还有 One Tab</li><li>FeHelper JSON自动格式化、简易postman、时间戳转换...</li></ul><h3>微信 企业微信 WPS 网易云</h3><h3>超级右键</h3><p>与window功能相似,右键可以显示要打开那个软件之类的</p><h3>钥匙串访问中的WiFi密码</h3><p>有时候忘了访问过的 wifi 密码,这时候就可以通过钥匙串访问查看密码。Command + 空格打开聚焦搜索,搜索钥匙串访问并打开,选择<strong>系统,</strong>找到你想要查看的 wifi 账号,双击打开,勾选显示密码即可看到 wifi 密码。</p><h2>重点 使用mc电脑快捷键</h2><p>Mac和Windows按键对应关系</p><pre><code>control ctrl
option Alt
command 功能键,即苹果键</code></pre><p><strong>访达</strong></p><pre><code>command + shift + 3 //截取全屏到桌面
command + shift + 4 //截取所选区域到桌面
command + shift + N //新建文件夹
command + shift + . //显示/隐藏文件
command + shift + G //调出窗口,可输入绝对窗口直达
command + C //复制文件
command + V //粘贴文件
command + Option + V //剪切文件 需要先复制文件
comand + option + C // 复制选中文件的路径
command + o //打开文件
command + 上 // 到上一层
command + delete //将文件移动到废纸篓
command + shift + delete //清空废纸篓
空格键 快速查看选中的文件,也就是预览功能
剪切文件 先command + c之后到需要粘贴的位置command + option + v</code></pre><p><strong>浏览器</strong></p><pre><code>command + L //光标直接跳至地址栏
command + T //打开一个新的标签页
command + 数字键 N(number) //切换到第N个标签页
command + '+-' //放大、缩小页面
command + 左右箭头 //返回上一页或者下一页
Control + Tab //转向下一个标签页
control + shift + tab //转向上一个标签页
</code></pre><p><strong>应用程序</strong></p><pre><code>Command-Option-H 隐藏(Hide)其他应用程序窗口
command + H //隐藏非全屏的应用程序
command + W //关闭当前应用窗口
command + Q //完全退出当前应用
command + N //在当前应用外新建窗口
command + T //在当前应用内新开窗口
command + , //打开当前应用的偏好设置
command + 空格 //聚焦搜索
command + option + esc //打开强制退出的窗口
command + control + F //应用全屏
command + control + 空格 //打开表情符号选择页面
comand + tab //转到最近打开的app
Command-Option-esc 打开“强制退出”窗口,如果有应用程序无响应,可在窗口列表中选择强制退出</code></pre><p><strong>文本</strong></p><pre><code>control + b //加粗
fn + 上方向 翻上一页
fn + 下方向 翻到下一页
fn + 左方向 翻到文本的最顶页
fn + 右方向 翻到文本的最底页
command + 上 光标移动到文本的开头
command + 下 光标移动到文本的末尾
command + 左 光标移动到当前行的头部
command + 右 光标移动到当前行的尾部
option + 左 光标移动当前单词的开头
opton + 右 光标移动到当前单词的末尾
control + A 移动到当前行的头部</code></pre><p><strong>其他</strong><br><code>command + control + Q //锁定屏幕</code><br><code>option + 空格 //打开你安装的utools</code></p><h3>屏幕录制和截图</h3><p>mac自带屏幕录制和截图工具 <br><code>Command + shift + 5 打开录制屏幕和截图工具</code></p><p>更多配置<a href="https://link.segmentfault.com/?enc=RdyW1Hri4B4NA0gE7D7gEw%3D%3D.eJZqHrRSxw%2BY1MJmSL%2FbC7qujz%2FMZqi5NdEs09Aly%2B4yZDn07KwqoZQCP85WL2gn" rel="nofollow">详细配置</a><br><a href="https://link.segmentfault.com/?enc=cs%2FVyXi%2FCrwUxG%2FcMvn9GA%3D%3D.Y6LSCV2omR4VlF68KnMpgQID4l4IoFz6W8y5XTSLXafZ16KsVhSwxRGHS%2B8BZWORCkFRt0s46jMc3lgnR1KJDg%3D%3D" rel="nofollow">在这里</a></p>
前端电商 sku 的全排列算法
https://segmentfault.com/a/1190000040834064
2021-10-19T16:29:54+08:00
2021-10-19T16:29:54+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p><strong>需求</strong></p><p><em>需求描述起来很简单,有这样三个数组:</em></p><p>let names = ["iPhone",'iPhone xs']</p><p>let colors = ['黑色','白色']</p><p>let storages = ['64g','256g']</p><p><em>需要把他们的所有组合穷举出来,最终得到这样一个数组:</em></p><pre><code>[
["iPhone X", "黑色", "64g"],
["iPhone X", "黑色", "256g"],
["iPhone X", "白色", "64g"],
["iPhone X", "白色", "256g"],
["iPhone XS", "黑色", "64g"],
["iPhone XS", "黑色", "256g"],
["iPhone XS", "白色", "64g"],
["iPhone XS", "白色", "256g"],
]</code></pre><p>由于这些属性数组是不定项的,所以不能简单的用三重的暴力循环来求解了</p><p><strong> 思路</strong></p><p>如果我们选用递归溯法来解决这个问题,那么最重要的问题就是设计我们的递归函数</p><p><strong> 思路分解</strong></p><p>以上文所举的例子来说,比如我们目前的属性数组就是 names,colors,storages,首先我们会处理names数组<br>很显然对于每个属性数组 都需要去遍历它 然后一个一个选择后再去和下一个数组的每一项进行组合</p><p>我们设计的递归函数接收两个参数</p><ul><li>index 对应当前正在处理的下标,是names还是colors 或者storage。</li><li>prev 上一次递归已经拼接成的结果 比如['iphoneX','黑色']</li></ul><p>进入递归函数:</p><ol><li>处理属性数组的下标0:假设我们在第一次循环中选择了iphone XS 那此时我们有一个未完成的结果状态,假设我们叫它prev,此时prev = ['iphone Xs']。</li><li>处理属性数组的下标1: 那么就处理到colors数组的了,并且我们拥有prev,在遍历colors的时候继续递归的去把prev 拼接成prev.concat(color),也就是['iphoneXs','黑色'] 这样继续把这个prev交给下一次递归</li><li>处理属性数组的下标2: 那么就处理到storages数组的了 并且我们拥有了 name+ color 的prev,在遍历storages的时候继续递归的去把prev拼接成prev.concat(storage) 也就是['iPhoneXS','黑色','64g'],并且此时我们发现处理的属性数组下标已经达到了末尾,那么就放入全局的结果变量res中,作为一个结果</li></ol><p><strong> 编码实现</strong></p><pre><code>let names = ['iphoneX',"iPhone XS"]
let colors = ['黑色','白色']
let storages = ['64g','256g']
let combine = function(...chunks){
let res = []
let helper = function(chunkIndex,prev){
let chunk = chunks[chunkIndex]
let isLast = chunkIndex === chunks.length -1
for(let val of chunk){
let cur = prev.concat(val)
// ['iphoneX','黑色','64g'],['iphoneX','黑色','256g'],['iphoneX','白色','64g']
if(isLast){
// 如果已经处理到数组的最后一项 则把拼接的结果放入返回值中
res.push(cur)
}else{
helper(chunkIndex+1,cur)
}
}
}
//从属性数组下标为0开始处理
// 并且此时的prev是一个空数组
helper(0,[])
return res
}
console.log(combine(names,colors,storages));
["iphoneX", "黑色", "64g"]
["iphoneX", "黑色", "256g"]
["iphoneX", "白色", "64g"]
["iphoneX", "白色", "256g"]
["iPhone XS", "黑色", "64g"]
["iPhone XS", "黑色", "256g"]
["iPhone XS", "白色", "64g"]
["iPhone XS", "白色", "256g"]</code></pre><p><strong>万能模板</strong></p><p>给定两个整数n和k 返回1...n中所有可能的k个数的组合<br>输入: n = 4, k = 2<br>输出:</p><pre><code>[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]</code></pre><p><strong>解答</strong></p><pre><code>let combine = function (n,k){
let ret = []
let helper = (start,prev)=>{
let len = prev.length
if(len === k){
ret.push(prev)
return //[[1,2]]
}
for(let i = start;i<=n;i++){
helper(i+1,prev.concat(i))
//helper(2,[1]) [1,2]
//helper(3,[1]), [1,3]
//helper(4,[1]) [1,4]
//helper(3,[2]) [2,3]
//helper(4,[2])[2,4]
// helper(4,[3])[3,4]
}
}
helper(1,[])
return ret
}
</code></pre><ul><li>可以看出这题和我们求解电商排列组合的代码竟然如此相似 只需要设计一个接受start排列起始位置,prev上一次拼接结果为参数的递归helper函数</li><li>然后对于每一个起点下标start,先拼接上start位置对应的值,再不断的再以其他剩余的下标作为起点去做下一次拼接。</li><li>当prev这个中间状态的拼接数组到达题目的要求长度k后 就放入结果数组中</li></ul><p><strong>优化</strong></p><ul><li>在这个解法中 有一些递归分支是明显不可能获取到结果的</li><li>我们每次递归都会循环尝试 <= n 的所有项去作为start 假设我们要求的数组长度k=3,</li><li>最大值n=4而我们以prev = [1], 再去以 n=4为start 作为递归的起点</li><li>那么显然是不可能得到结果的,因为n=4的话只剩下4这一项可以拼接, 最多</li><li>就拼成[1,4], 不可能满足k=3的条件所以在进入递归之前</li><li>就果断的把这些废枝给减掉 这就叫做<strong>减枝</strong></li></ul><hr><pre><code>let combine = function (n,k){
let ret = []
let helper = (start,prev)=>{
let len = prev.length
if(len === k){
ret.push(prev)
return
}
// 还有rest个位置待填补
let rest = k - prev.length
for(let i = start;i<=n;i++){
if(n-i+1<rest){
continue
}
helper(i+1,prev.concat(i))
}
}
helper(1,[])
return ret
}
</code></pre><h3><strong>相似题型</strong></h3><p>给定一个可能包含重复元素的整数数组nums,返回该数组所有可能的子集(幂集)<br>说明: 解题不能包含重复的子集</p><pre><code>输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
</code></pre><p><em>剪枝的思路也是和之前相似的 如果循环的时候发现剩余的数字不足以凑成目标长度 就直接剪掉</em></p><pre><code>var subsetsWithDup = function(nums){
let n = nums.length
let res = []
if(!n){
return res
}
nums.sort()
let used = {}
let helper = (start,prev,target)=>{ //0,[],2
if(prev.length === target){
let key = genKey(prev)
if(!used[key]){
res.push(prev)
used[key] = true
}
return
}
for(let i = start; i<= n;i++){
let rest = n - i
let need = target - prev.length
if(rest<need){
continue
}
helper(i + 1,prev.concat(nums[i]),target)//1,[1],2 2,[2],2 3,[2],2
// 2,[1,2],2, 2,[2,2],2
//1,[1,]3 2,[2],3 3,[3],3
//2,[1,2],3 3,[2,2],3
//3,[1,2,3],3
}
}
for(let i = 1;i<=n;i++){
helper(0,[],i) //0,[],3
}
return [[],...res]
}
function genKey(arr){
return arr.join('~')
}</code></pre><h4>数组总和</h4><p><em>给定一个数组candidates和一个目标数target,找出candidates中所有可以使数字和为target的组合candidates中的每个数字在每个组合中只能使用一次</em></p><p>说明:<br>所有数字(包含目标数) 都是正整数<br>解集不能包含重复的组合</p><pre><code>
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]</code></pre><h5>思路</h5><ul><li>与上面思路类似 只不过由于不需要考虑同一个元素重复使用的情况 每次的递归start起点应该是prevStart + 1。</li><li>由于数组中可能出现多个相同的元素 他们可能会生成相同的解 比如 [1,1,7]去凑8的时候,可能会用下标为0的1和7去凑8,也可能用下标为1的1和7去凑8</li><li>所以在把解放入到数组之前 需要先通过唯一的key去判断这个解是否生成过,但是考虑到[1,2,1,2,7]这种情况去凑10,可能会生成[1,2,7]和[2,1,7]</li><li>这样顺序不同但是结果相同的解,这是不符合题目要求的 所以一个简单的方法就是 先把数组排序后再求解 这样就不会出现顺序不同相同的解了</li><li>此时只需要做简单的数组拼接即可生成key[1,2,7]->1~2~7</li></ul><pre><code>/**
* @param {number[]}candidates
* @param {number} target
* @return {number[][]}
*/
let combinationSum2 = function(candidates,target){
let res = []
if(!candidates.length){
return res
}
candidates.sort()
let used = {}
let helper = (start,prevSum,prevArr) =>{
// 由于全是正整数 所以一旦和大于目标值了 直接结束本次递归即可
if(prevSUm >target){
return
}
// 目标值达成
if(prevSum === target){
let key = genkey(prevArr)
if(!used[key]){
res.push(prevArr)
used[key] = true
}
return
}
for(let i = start;i<candidates.length; i++){
// 这里还是继续从start本身开始 因为多个重复值是允许的
let cur = candidates[i]
let sum = prevSum + cur
let arr = prevArr.concat(cur)
helper(i + 1,sum,arr)
}
}
helper(0,0,[])
return res
}
let genKey = (arr)=> arr.join('~')</code></pre>
vuex命名空间
https://segmentfault.com/a/1190000040724781
2021-09-23T16:09:25+08:00
2021-09-23T16:09:25+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
2
<p><em>mapState、mapGetters、mapMutations、mapActions第一个参数是字符串(命名空间名称),第二个参数是数组(不需要重命名)/对象(需要重命名)。</em></p><pre><code>mapXXXs('命名空间名称',['属性名1','属性名2'])
mapXXXs('命名空间名称',{
'组件中的新名称1':'Vuex中的原名称1',
'组件中的新名称2':'Vuex中的原名称2',
})</code></pre><h3>项目结构</h3><p><img src="/img/bVcU2wS" alt="image.png" title="image.png"></p><h3>mian.js</h3><pre><code>import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store/index";
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");</code></pre><h3>index.js</h3><pre><code>import Vue from "vue";
import Vuex from "vuex";
import cat from "./modules/cat";
import dog from "./modules/dog";
Vue.use(Vuex);
export default new Vuex.Store({
modules: { cat, dog }
});</code></pre><h3>cat.js</h3><pre><code>export default {
namespaced: true,
// 局部状态
state: {
name: "蓝白英短",
age: 1
},
// 局部读取
getters: {
desc: state => "宠物:" + state.name
},
// 局部变化
mutations: {
increment(state, payload) {
state.age += payload.num;
}
},
// 局部动作
actions: {
grow(context, payload) {
setTimeout(() => {
context.commit("increment", payload);
}, 1000);
}
}
};</code></pre><h3>dog.js</h3><pre><code>export default {
namespaced: true,
// 局部状态
state: {
name: "拉布拉多",
age: 1
},
// 局部读取
getters: {
desc: state => "宠物:" + state.name
},
// 局部变化
mutations: {
increment(state, payload) {
state.age += payload.num;
}
},
// 局部动作
actions: {
grow(context, payload) {
setTimeout(() => {
context.commit("increment", payload);
}, 1000);
}
}
};</code></pre><h3>hello.vue</h3><pre><code><template>
<div class="hello">
<h3>Vuex状态树</h3>
<div>{{this.$store.state}}</div>
<h3>mapState</h3>
<div>{{catName}} {{catAge}}</div>
<div>{{dogName}} {{dogAge}}</div>
<h3>mapGetters</h3>
<div>{{catDesc}}</div>
<div>{{dogDesc}}</div>
<h3>mapMutations</h3>
<button @click="catIncrement({num:1})">猫变化</button>
<button @click="dogIncrement({num:1})">狗变化</button>
<h3>mapActions</h3>
<button @click="catGrow({num:1})">猫动作</button>
<button @click="dogGrow({num:1})">狗动作</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
export default {
name: "HelloWorld",
computed: {
...mapState("cat", {
catName: "name",
catAge: "age"
}),
...mapState("dog", {
dogName: "name",
dogAge: "age"
}),
...mapGetters("cat", {
catDesc: "desc"
}),
...mapGetters("dog", {
dogDesc: "desc"
})
},
methods: {
...mapMutations("cat", { catIncrement: "increment" }),
...mapMutations("dog", { dogIncrement: "increment" }),
...mapActions("cat", { catGrow: "grow" }),
...mapActions("dog", { dogGrow: "grow" })
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style></code></pre><h3>运行效果</h3><p><img src="/img/bVcU2w7" alt="image.png" title="image.png"></p>
React + TS 封装密码强度组件
https://segmentfault.com/a/1190000040282352
2021-07-03T10:59:36+08:00
2021-07-03T10:59:36+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<p><strong>在antd的Progress的基础上封装</strong><br><em>PwdStrength.tsx</em></p><pre><code>import { Col, Progress, Row } from 'antd';
import { FC } from 'react';
import styles from './index.less';
interface IPwdStrengthProps {
pwdStrength: 0 | 1 | 2 | 3;
}
const PwdStrength: FC<IPwdStrengthProps> = ({ pwdStrength }) => {
return (
<div className={styles.passwordStrongBox}>
<Row gutter={12}>
<span className={styles.passWord}>密码强度</span>
<Col span={3}>
<Progress className={styles.weak} percent={pwdStrength > 0 ? 100 : 0} showInfo={false} />
</Col>
<Col span={3}>
<Progress className={styles.middle} percent={pwdStrength > 1 ? 100 : 0} showInfo={false} />
</Col>
<Col span={3}>
<Progress className={styles.strong} percent={pwdStrength > 2 ? 100 : 0} showInfo={false} />
</Col>
<span className="passStrong">
{pwdStrength === 1 && '弱'}
{pwdStrength === 2 && '中'}
{pwdStrength === 3 && '强'}
</span>
</Row>
</div>
);
};
export default PwdStrength;
</code></pre><p><strong>覆盖原有样式,根据强度各个进度显式不同颜色,样式献上</strong><br><em>index.less</em></p><pre><code>.passwordStrongBox {
margin-top: 4px;
.weak {
:global .ant-progress-bg {
background-color: #f50 !important;
}
}
.middle {
:global .ant-progress-bg {
background-color: #e4ce2b !important;
}
}
.strong {
:global .ant-progress-bg {
background-color: #87d068 !important;
}
}
.passWord {
display: inline-block;
margin: 3px 8px 0 6px;
color: rgba(140, 140, 140, 100);
font-size: 12px;
}
.passStrong {
display: inline-block;
margin: 3px 0 0 8px;
color: rgba(89, 89, 89, 100);
font-size: 11px;
}
}</code></pre><p><strong>利用正则判断用户输入的密码的强度</strong><br><em>useChangePassword.ts</em></p><pre><code>const pattern_1 = /^.*([0-9])+.*$/i; //数字
const pattern_2 = /[a-z]/; //小写字母
const pattern_3 = /[A-Z]/; //大写字母
const pattern_4 = /[`~!@#$%^&*()\-_+=\\|{}':;\",\[\].<>\/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]/; //特殊字符
export function useChangePassword() {
const getPwdStrength = (pwd: string): 0 | 1 | 2 | 3 => {
let level = 0;
if (pwd) {
pwd = pwd.trim();
if (pwd.length >= 6) {
if (pattern_1.test(pwd)) {
level++;
}
if (pwd.length > 10) {
level++;
}
if (pattern_2.test(pwd) || pattern_3.test(pwd)) {
level++;
}
if (pattern_4.test(pwd)) {
level++;
}
if (level > 3) {
level = 3;
}
}
}
return level as 0 | 1 | 2 | 3;
};
return {
getPwdStrength,
};
}
</code></pre><p><strong>数据管理</strong><br><em>store.ts</em></p><pre><code>import { reduxStore } from '@/createStore';
export const store = reduxStore.defineLeaf({
namespace: 'personal',
initialState: {
pwdStrength: 0 as 0 | 1 | 2 | 3,
},
reducers: {
changePwdStrength(state, payload: 0 | 1 | 2 | 3) {
state.pwdStrength = payload;
},
},
});</code></pre><p><strong>使用</strong></p><pre><code>import { store } from '../../store';
import PwdStrength from '../PwdStrength';
const { pwdStrength } = store.useState();
const { getPwdStrength } = useChangePassword();
<ProFormText.Password
placeholder="新密码"
rules={[
{
required: true,
message: '请输入新密码',
},
]}
width="md"
name="newPassword"
label="新密码"
help={<PwdStrength pwdStrength={pwdStrength} />}
fieldProps={{ onChange: (e) => store.changePwdStrength(getPwdStrength(e.target.value)) }}
/></code></pre><p><strong>判断密码逻辑</strong></p><pre><code> handleSubmit = (e: any) => {
e.preventDefault();
this.props.form.validateFields((err: Error, values: any) => {
if (!err) {
const { oldPwd, newPwd } = values;
QMFetch({
host: 'v_app_api',
url: 'api/store/account/modify/password',
method: 'POST',
body: {
oldPassword: oldPwd,
newPassword: newPwd,
},
})
.then((data: any) => {
const res = data.data;
if (typeof res.success === 'boolean' && res.success === true) {
this.props.close();
this.timer();
Modal.success({
title: '修改成功!',
content: '3秒后将退出到登录页面',
okText: '确定',
onOk: () => {
const url = QMConst.HOST['v_sso_server'] + '/logout?r=changepwd&all=modifyPwd';
location.href = url;
},
});
} else {
message.error(res.message);
}
})
.catch((err: Error) => message.warning(err.message));
}
});
};
pwdStrength = (pwd: string) => {
const pattern_1 = /^.*([0-9])+.*$/i; //数字
const pattern_2 = /[a-z]/; //小写字母
const pattern_3 = /[A-Z]/; //大写字母
const pattern_4 = /[`~!@#$%^&*()\-_+=\\|{}':;\",\[\].<>\/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]/; //特殊字符
let level = 0;
pwd = pwd.trim();
if (pwd.length >= 6) {
if (pattern_1.test(pwd)) {
level++;
}
if (pwd.length > 10) {
level++;
}
if (pattern_2.test(pwd) || pattern_3.test(pwd)) {
level++;
}
if (pattern_4.test(pwd)) {
level++;
}
if (level > 3) {
level = 3;
}
}
return level;
};
newPwdChange = (e: any) => {
const newPwd = e.target.value;
// 计算密码强度
this.setState({
level: this.pwdStrength(newPwd),
});
};
checkNewPwd = (rule: any, value: any, callback: any) => {
const form = this.props.form;
value = typeof value == 'undefined' ? '' : value.trim();
if (value.length > 5 && value.length < 20) {
if (value === form.getFieldValue('oldPwd')) {
callback('新密码不能和老密码相同');
}
const confirmPwd = form.getFieldValue('confirmPwd');
if (confirmPwd && confirmPwd !== '' && confirmPwd !== value) {
callback('确认密码与新密码不一致');
} else {
callback();
}
} else {
callback('新密码长度保持在6-20个字符之间');
}
};
checkOldPwd = (rule: any, value: any, callback: any) => {
value = typeof value == 'undefined' ? '' : value.trim();
if (value.length < 6) {
callback('原密码至少包含6个字符');
} else {
callback();
}
};
checkPassword = (rule: any, value: any, callback: any) => {
const form = this.props.form;
value = typeof value == 'undefined' ? '' : value.trim();
if (value !== form.getFieldValue('newPwd')) {
callback('确认密码与新密码不一致');
} else {
callback();
}
};
timer = () => {
let i = 3;
const time = window.setInterval(() => {
if (i === 0) {
const url = QMConst.HOST['v_sso_server'] + '/logout';
location.href = url;
clearInterval(time);
} else {
i--;
}
}, 1000);
};
</code></pre>
webpack编译速度优化
https://segmentfault.com/a/1190000040157280
2021-06-10T16:03:07+08:00
2021-06-10T16:03:07+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p><em>项目过大,编译就会非常慢,每次入手新项目看着爬一样的速度,实在忍不住会想优化一下编译速度</em></p><p>用speed-measure-webpack-plugin和webpack-bundle-analyzer 分析项目 <br><img src="/img/bVcSEHk" alt="image.png" title="image.png"></p><h3>优化方案</h3><p><strong>缓存优化</strong><br> hard-source-webpack-plugin,这插件为模块提供中间缓存步骤,但项目得跑两次,第一次构建时间正常,第二次大概能省去90%左右的时间。<br><code>npm i hard-source-webpack-plugin -D</code></p><pre><code>plugins: [
new HardSourceWebpackPlugin()
]</code></pre><p>但是项目中其实没有独立的webpack.config.js文件,所以只能放在vue.config.js文件中,使用chainWebpack来将配置插入到webpack中去<br>官网中的使用cache缓存<br><img src="/img/bVcSET0" alt="image.png" title="image.png"></p><pre><code>chainWebpack: (config) => {
config.cache(true)
}</code></pre><p>配合HardSourceWebpackPlugin</p><pre><code>chainWebpack: (config) => {
config.plugin('cache').use(HardSourceWebpackPlugin)
}</code></pre>
antd的Form表单的回显
https://segmentfault.com/a/1190000040059939
2021-05-25T13:40:07+08:00
2021-05-25T13:40:07+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<p>最近刚开始用antd,回显网上搜了一圈,都不太清晰。所以保存一下我的用法。</p><p><strong>1 利用initialValue</strong></p><pre><code> render() {
// @ts-ignore
const {location} = this.props;
const data = location.state;
let responsiblePeoplePhone = {};
if(data){
responsiblePeoplePhone={initialValue:data.responsiblePeoplePhone||''};
}
return( <FormItem
{...formItemLayout}
label="负责人联系方式"
required={true}
hasFeedback={true}
name="responsiblePeoplePhone"
{...responsiblePeoplePhone}
rules={[
{
required: true,
message: '请填写负责人联系方式',
},
{
pattern: ValidConst.phone,
message: '请输入正确的负责人联系方式',
},
]}
>
<Input />
</FormItem>
)</code></pre><p><strong>2 使用setFieldsValue</strong></p><pre><code> render() {
// @ts-ignore
const {location} = this.props;
const data = location.state;
// ref form
this.formRef.current.setFieldsValue({
responsiblePeoplePhone: data.responsiblePeoplePhone
})
return( <FormItem
{...formItemLayout}
label="负责人联系方式"
required={true}
hasFeedback={true}
name="responsiblePeoplePhone"
rules={[
{
required: true,
message: '请填写负责人联系方式',
},
{
pattern: ValidConst.phone,
message: '请输入正确的负责人联系方式',
},
]}
>
<Input />
</FormItem>
)
getData()
{ 获取输入值
const fieldsValue = this.formRef.current.getFieldsValue();
let result = {
responsiblePeoplePhone:
fieldsValue.responsiblePeoplePhone ,
};
return result;
}
</code></pre><p><strong>单行编辑操作</strong></p><pre><code><Column
title="操作"
key="action"
dataIndex="action"
render={(_text,record: any) => <a onClick={()=>remindEdit(record)}>{isOpen?'停用':'启用'}</ a>}
/></code></pre>
React的plume2使用
https://segmentfault.com/a/1190000039989774
2021-05-12T20:34:24+08:00
2021-05-12T20:34:24+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h3>Hello, plume2.</h3><pre><code>import React, { Component } from 'react'
import {Actor,Store,StoreProvider,Relax,ViewAction} from 'plume2'
//MapReduce
class HelloActor extends Actor{
defaultState(){
return {text:"Hello,plume2"}
}
}
//reactive ui event 反应式ui事件
class AppViewAction extends ViewAction{
sayHello = (text:any) =>{
this.store.dispatch('say:hello',text)
}
}
// Single Data Source 单一数据源
class AppStore extends Store{
//bind data transform 绑定数据转换
bindActor(){
//after plume2 directly pass Actor class 直接通过Actor类
return [HelloActor]
}
//bind ui event 绑定ui事件
bindViewAction(){
return {
AppViewAction
}
}
}
//Auto compute relaxProps 自动计算打开Props
@Relax
class Text extends React.Component{
static relaxProps = {
//auto injected by store.state().get('text') 由store.state().get('text')自动注入
text:"text",
//auto injected by store's bindViewAction 由store的bindViewAction自动注入
ViewAction:'viewAction'
}
_handleClick = () =>{
const {text,viewAction} = this.props.relaxProps
viewAction.AppViewAction.sayHello(text)
}
render(){
const {text,viewAction} = this.props.relaxProps;
return (
<div onClick={this._handleClick}>{text}</div>
)
}
}
@StoreProvider(AppStore) //应用程序入口
export default class Index extends Component {
render() {
return (
<div>
<Text/>
</div>
)
}
}</code></pre><p><img src="/img/bVcRW5W" alt="image.png" title="image.png"></p><h3>API Actor</h3><p>Actor 计算模型,一个独立的计算单元,主要作用就是转换我们的状态数据</p><pre><code>import { Actor,Action} from 'plume2'
// 是的 这就是一个Actor简单世界
class HelloActor extends Actor{
// 领域的初始数据 该数据会被自动的转换为immutable
defaultState(){
// 返回的对象会被自动的转化为immutable
// 除非有特殊数据结构如(set OrderedMap之类)
//不需要特殊指定immutable数据结构
return {text:'hello plume2'}
}
//** 通过@Action来建立store的dispatch和actor的handler(处理程序)之间的关联
// API规范
// @param state actor上一次的immutable状态
// @param text store dispatch的参数值 尽量保持单值设计
@Action('change:text')
change(state,text){
// immutable api
return state.set('text',text)
}
}</code></pre><h3>Store</h3><p>什么是 Store?<br> Store 我们的数据状态容器中心 管理着整个app的数据的生命周期<br> 我们坚守单根数据源的思想(single data source),store 中保持着完整的业务与UI状态<br> Stor的主要职责:<br> 1 聚合actor<br> 2 分派actor(单分派 事务分派)<br> 3 通过bigQuery 计算我们的查询语言(QL/PQL)<br> 4 响应页面的事件(ViewAction)<br> 5 注册响应 RL</p><pre><code>import {Store,ViewAction} from 'plume2'
import LoadingActor from 'loading-actor'
import UserActor from 'user-actor'
import TodoActor from 'todo-actor'
// 响应页面事件的逻辑处理
class AppViewAction extends ViewAction {
//show simple dispatch 显示简单分派
// when dispatch finished if status had changed, 如果状态已更改,则在调度完成时,
//each Relax component received message 每个组件都接收到消息
update = () => {
//将计算的任务分派的到actor
//然后根据actor的返回值 重新聚合新的store的state
//该为单分派 当dispatch结束 store的state发生改变的时候
//UI容器组件(StoreProvider,Relax) 会收到通知重新re-render UI
this.store.dispatch('update')
}
// show multiple dispatch in a transaction 在事务中显示多个分派
save = () =>{
//事务分派
//很多场景下 计算应该是原子类型的 我们想一想 dispatch 结束才通知UI去re-render
//这个时候我们就可以开启事务控制
// transaction 会返回值来判断在dispatch 过程中有没有发生错误
// 如果发生错误 数据会自动回滚到上一次的状态 避免脏数据
// 我们也可以指定 自定义的回滚处理
// this.transaction(()=>{/*正常逻辑*/},()=>{/*自定义的回滚函数*/})
this.store.transaction(()=>{
this.store.dispatch('loading:end')
//这个地方可以得到上一次的dispatch之后的结果
//如:
const loading = this.state().get('loading')
this.store.dispatch('init:user',{id:1,name:'plume2'})
this.store.dispatch('save')
})
}
}
class AppStore extends Store{
// 聚合Actor
// 通过reduce各个actor的defaultState
// 聚合出store的state作为source data
bindActor(){
//plume2 直接传递Actor的class
return [LoadingActor,UserActor,TodoActor]
}
bindViewAction(){
return{
AppViewAction
}
}
}</code></pre><p><strong>Store public-API</strong></p><pre><code>// 绑定需要聚合的Actor
bindActor(): Array<Actor | typeof Actor>
bindViewAction(): IViewActionMapper
// 事务控制dispatch
// dispatch: 正常逻辑
// rollBack: 自定义回滚逻辑 默认是自动回滚到上一次的状态
//返回是否发生回滚
transaction(dispatch:Dispatch,rollBack:RollBack):boolean;
// 计算QL
bigQuery(ql:QueryLang):any
// 当前store聚合的状态
state():IMap;
// 定义store状态更新通知
subscribe(cb:Handler):void;
// 取消订阅
unsubscribe(cb:Handler):void;
</code></pre><h3>StoreProvider</h3><p><strong>StoreProvider 容器组件衔接我们的React组件和AppStore。向React组件提供数据源</strong></p><p>在StoreProvider中的主要任务是:<br>1 初始化我们的AppStore<br>2 将AppStore的对象绑定到React组件的上下文<br>3 Relay 就是通过上下文取的store对象<br>4 监听Store的state变化</p><p>友情提示:我们还提供了 debug模式</p><p>开启debug 模式 我们就可以对数据进行全链路跟踪</p><p>跟踪store的dispatch actor的处理 relax对QL的计算等</p><pre><code>import React, { Component } from 'react'
import {StoreProvider} from 'iflux2'
import AppStore from './store'
//enable debug
@StoreProvider(AppStore,{debug:true})
class ShoppingCart extends Component{
render(){
return(
<Scene>
<HeaderContainer/>
<ShoppingCart/>
<BottomToolBarContainer/>
</Scene>
)
}
}</code></pre><h3>Relax</h3><p>Relax是plume2中非常重要的容器组件 类似Spring容器的依赖注入一样<br>核心功能会根据子组件的relaxProps中声明的数据,<br>通过智能计算属性的值 然后作为this.props.relaxProps透传给子组件</p><p>以此来解决React的props层层透传的verbose的问题</p><p>计算的规则<br>1 store 的state的值,直接给出值 得 immutable的路径,如 count:'count',todoText:['todo',1,'text']<br>2 store 的method 直接和method同名就ok 如:destroy:noop, 我们更希望通过ActionCreator来单独处理UI的side effect<br>3 如果属性是'viewAction' 直接注入store 中绑定的ViewAction<br>4 如果属性是QL 注入QL 计算之后的结果 如果PQL会自动绑定store的上下文</p><pre><code>@Relax
export default class Footer extends React.Component{
static relaxProps = {
changeFilter:noop,
clearCompleted:noop,
count:countQL,
loadingPQL:loadingPQL,
filterStatus:'filterStataus',
viewAction:'viewAction'
}
render(){
const{
changeFilter,
clearCompleted,
count,
filterStataus,
viewAction
} = this.props.relaxProps
}
//...
}
</code></pre><h3>QL/PQL</h3><p>为什么我们需要一个QL<br>1 我们把store state 看成source data,因为UI展示的数据,可能需要根据我们的源数据进行组合<br>2 我们需要UI的数据具有reactive的能力 当source data变化的时候 @Relax 会去重新计算我们的QL<br>3 命令式的编程手动的精确的处理数据之间的依赖和更新 Reactive会自动处理数据的依赖 但是同一个QL 可能会被执行多次 造成计算上的浪费<br>不过不需要担心 QL支持cache 确保path对应的数据没有变化的时候 QL 不会重复计算</p><p>QL = Query Lang<br>自定义查询语法 数据的源头是store的state返回的数据</p><p>Syntax QL(displayName,[string|array|QL...RelaxContainer,fn])</p><p>displayName,主要是帮助我们在debug状态更好的日志跟踪</p><p>string array QL:string array 都是immutable的get的path,QL其他的QL(支持无线嵌套)</p><p>fn:可计算状态的回调函数 bigQuery会取得所有的所有的数组中的path对应的值 作为参数传递给fn</p><pre><code>
// 返回:{
// id:1,
// name:'iflux2',
// address:{
// city:'南京'
// }
// }
store.state()
//QL计算的结果值是'iflux2南京'
const helloQL = QL('helloQL',[
'name',
['address','city'],
(name,city)=>`${name}${city}`
])
Store.bigQuery(helloQL)</code></pre><p><strong>QL in QL</strong></p><pre><code>import {QL} from 'plume2'
const loadingQL = QL('loading',['loading',loading=>loading])
const userQL = QL('userQL',[
//query lang 支持嵌套
loadingQL,
['user','id'],
(loading,id)=>({id,loading})
])</code></pre><p>在0.3.2版本中我们做了些较大的改变</p><p>plume2 是我们的一个新的起点 是我们走向typescript的起点 plume2完全站在typescript 静态和编译角度去思考框架的特点和实现我们希望plume2足够轻量 简单 一致 同时给出优雅的代码检查错误提示<br>全链路的log跟踪 就想我们的开发能够轻松一点<br>在我们实践过程中 也会一些不够细致的地方 我们需要不断的去改进 在怎么去思考改进都不为过 划重点 开发体验同用户体验一样重要</p><h3>inprovements</h3><p>1 干掉DQL,DQL有些鸡肋 这就是现实有理想的差别 实现过程中需要动态递归的替换模板变量 也是比较受罪<br>更重要的事 DQL的动态数据的来源只能是React的Component的props,这就带来了一些不够灵活 比较受限 我们设计DQL<br>或者QL的本意是什么 是获取数据声明式(Declarative) 以及数据本身的反应式(reactive) 为了解决这个问题 我们设计了更简单的<br>PQL(partial Query Lang)</p><pre><code>import {PQL,Relax} from 'plume2'
const helloPQL = PQL(index =>QL([
['user',index,'name'],
(name)=>name
]))
@Relax
class HelloContainer extends React.Component{
static relaxProps = {
hello:helloPQL //自动绑定store的上下文
}
render(){
const value = hello(1)
return <div>{value}</div>
}
}</code></pre><p>简单清晰实现 更灵活的参数入口 目前不支持QPL嵌套QPL.</p><p>1 更舒服的开发体验<br>有时候我们为了快速的在浏览器的控制台 如(chrome console) 去快速测试我们的一些store的方法 我们会写</p><pre><code> class AppStore extends Store {
constructor(props: IOptions){
if(_DEV_){
window['store'] = this
}
}
}
</code></pre><p>这样可以在控制台直接调用_store 去快速测试 但是经常这样写 每个页面这样写 就有点小烦躁。<br>无缘无故去写个构造方法 。也挺无趣。<br>在一些SPA或者react-native的多页面中_store会被重复覆盖。<br>可从框架层面去解决这个问题。当开启应用的debug特定的时候 框架自动绑定。来简化这个流程</p><pre><code>开启debug-mode
@StoreProvder(AppStore,{debug:true})
class HelloApp extends React.Component{
render(){
return <div/>
}
}</code></pre><p>plume2 会自动在window上面绑定_plume2App,各个key就是storeprovider下的组件名称<br><img src="/img/bVcRXUy" alt="image.png" title="image.png"><br>这样小伙伴尽情玩耍就可以了。</p><p>1 更好的事件处理模块目前我们的UI交互的事件的handler都在store中,因为我们希望UI是less-logic这样才好通用我们业务层。<br>之前都是通过relax和relaxProps去injected我们store的方法给UI 的交互逻辑如:</p><pre><code>
const noop = ()=>{}
@Relax
class HelloApp extends React.Component{
props:{
relaxProps?:{
OnInt:Function;
onReady:Function;
onShow:Function
}
}
static relaxProps = {
onInit:Function,
onReady:Function,
onShow:Function
}
}</code></pre><p>这样特点是简单 通过relax注入就完事了 就一个规则只要 方法的名字和store的method名字相同就OK 但是实际操作发现写一遍注入 再写一遍typeScript类型定义 心里真是万马奔腾 太重太累。<br>更有甚者 我们可能某个叶子节点的组件 仅仅是想回调一个事件 都要通过relax来注入 如果有列表数据的场景设计的不当,<br>如果每个item都是relax 页面200条数据 那就是200relax组件。<br>relax本质是subscribe container Component,<br>它会实时监听store的变化 这200个哗啦啦的性能下降 。<br>SO 我们需要思考 在react中 怎么定义UI UI = s(state) 其实这个并不完整这个仅仅是定义了UI的展现部分 UI还有交互 交互在函数式观点事件就是副作用 因此更完整的定义应该是UI = f(state,action),<br>继续往下思考 是么是state ? 站在flux的角度去看 state = store(initState,action),是不是很熟悉 都有Action,是不是有什么关联? 其实就是一个是出口 一个是入口</p><p><img src="/img/bVcRX1r" alt="image.png" title="image.png"><br>所以想到这里,我们就可以设计一个 ActionCreator 模块。</p><p>来来来,上代码。</p><pre><code>const actionCreator = ActionCreator()
actionCreator.creat('INIT',(store,num:string)=>{
//biz logic
store.dispatch('init',num)
})
class AppStore extends Store{
//将store绑定到AcitonCreator
bindActionCreator(){
return actionCreator;
}
// 除了在用actionCreator.create创建
//event handler 也可以直接在store中
@Action("INIT")
init(num:string){
this.dispatch('init',num)
}
}
const HelloApp = () =>{
<div>
<a onClick={actionCreator.fire('INIT',1)}>吼吼吼</a>
</div>
}</code></pre><p>这种方式有个问题就是ActionCreator是个单例 这样会导致多次重复render一个页面的时候 会有事件被store的上下文覆盖的问题<br>基于这样的考虑还是通过上下文注入绑定 所以在1.0.0设计了ViewAction来解决这个问题</p><h3>ViewAction</h3><pre><code>import {ViewAction,Store} from 'plume2'
class LoadingViewAction extends ViewAction{
loading = () =>{
this.store.dispatch('loading:start')
}
}
class FilterViewAction extends ViewAction{
filter = (text:string) =>{
this.store.dispatch('filter:text',text)
}
}
//bind to store
class AppStore extends Store{
bindViewAction(){
return{
LoadingViewAction,
FilterViewAction
}
}
}
//how to injected to ui
class Filter extends React.Component{
props:{
relaxProps?:{
//代码自动提示 参考example中的例子
viewAction:TViewAction<typeof {LoadingViewAction,FilterViewAction}>
}
}
static relaxProps = {
viewAction:'viewAction'
}
render(){
const {viewAction} = this.props.relaxProps
}
}</code></pre><h5>都什么年代了 你还裸用字符串,你这是魔鬼字符串。。</h5><p>是的 我们加 我们加字符串的枚举类型 一次来解决dispatch到actor等各种常量字符串</p><pre><code>export default ActionType('INCREMENT','DECREMENT')</code></pre><p><img src="/img/bVcRX8H" alt="image.png" title="image.png"><br>更复杂的结构,</p><pre><code>const Actions = actionType({
PDF:'application/pdf',
Text:'text/plain',
JPEG:'image/jpeg'
})</code></pre><p>所以直接使用就好了,推荐使用常量字符串枚举,为什么?</p><pre><code>export const enum Command{
LOADING="loading",
FILTER_TEXT="filter:text"
}</code></pre><p>金无足赤人无完人,在实<img src="/img/bVcRX96" alt="image.png" title="image.png"><br><img src="/img/bVcRYaa" alt="image.png" title="image.png"><br><img src="/img/bVcRYae" alt="image.png" title="image.png"></p>
浅入深知React
https://segmentfault.com/a/1190000039856539
2021-04-20T11:39:52+08:00
2021-04-20T11:39:52+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p><strong>React.creatElement 创建虚拟dom</strong></p><p><strong>关于虚拟DOM:</strong></p><pre><code>本质是Object类型的对象 (一般对象)
虚拟DOM比较轻 真实DOM比较重 因为虚拟DOM是React内部使用 无需真实DOM上那么多属性
虚拟DOM最终会被React转化为真实DOM 呈现在页面上</code></pre><p><strong>jsx的语法规则</strong></p><pre><code>1.定义虚拟Dom时不要写引号
2 标签中混入js表达式时用{}
3 样式的类名指定不要用class 要用className
4 内联样式 要用style={{key:value}}的形式去写
4 只有一个根标签
6 标签必须闭合
7 标签首字母
1 若小写字母开头 则将标签转为html 中的同名元素 ,若html中无该标签对应的同名元素 则报错。
2 若大写字母开头 react 就去渲染对应的组件 若组件没有定义 则报错</code></pre><p><strong>js语句(代码)与js表达式</strong></p><pre><code>
1 表达式:一个表达式会产生一个值 可以放在任何一个需要值的地方
下面这些都是表达式:
1 a
2 a+b
3 demo(1)
4 arr.map()
5 function test(){}
2 语句(代码)
下面这些都是语句(代码)
1 if(){}
2 for(){}
3 switch(){}</code></pre><h3>React中的ref</h3><p><strong>字符串的ref</strong><br><img src="/img/bVcRoE6" alt="image.png" title="image.png"><br><strong>回调形式的ref</strong></p><pre><code> class GetValue extendsReact.Component{
getValue = () =>{
let {input1} = this
console.log(input1.value);
}
changeHot= ()=>{
let {isHot} = this.state
this.setState({
isHot:!isHot
})
}
state = {
isHot:false
}
refInput1=(c)=>{
this.input1 = c
}
render(){
console.log(this);
let {isHot} = this.state
return(
<div>
<h1>今天天气{isHot?'晴朗':'阴天'}</h1>
<button onClick={this.changeHot}>改变天气</button>
<input ref={this.refInput1} type="text"/>&nbsp;
<button onClick={this.getValue}>获取Input1的value</button>
</div>
)
}
}
ReactDOM.render(<GetValue/>,document.getElementById('test'))</code></pre><p><strong>createRef的ref</strong></p><pre><code>class GetValue extendsReact.Component{
myRef = React.createRef() //React.createRef调用后返回一个容器,该容器返回可以存储被ref标识的节点。该容器是专人专用的
myRef2 = React.createRef()
getValue = () =>{
console.log(this.myRef.current.value);
}
showData= ()=>{
console.log(this.myRef2.current.value);
}
render(){
return(
<div>
<input ref={this.myRef} type="text"/>&nbsp;
<input ref={this.myRef2} type="text" onBlur={this.showData}/>&nbsp;
<button onClick={this.getValue}>获取Input1的value</button>
</div>
)
}
}
ReactDOM.render(<GetValue/>,document.getElementById('test'))</code></pre>
H5唤醒app
https://segmentfault.com/a/1190000039411109
2021-03-14T14:55:58+08:00
2021-03-14T14:55:58+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p><strong>url scheme方式</strong></p><pre><code> var downloader,
scheme = ":lklk" //需要打开的app scheme地址
iosDownload = "jjjj",//如果打开scheme失效的app下载地址
anDownload = "http://xxx.com"
var u = navigator.userAgent
var isAndroid = u.indexOf("Android")>-1 || u.indexOf("Linux")>-1 //g
var isIos = !!u.match(/\(i[^;]+;(u;)?CPU.+Mac OS X/);//ios终端
// 给id为openBtn的按钮添加点击事件处理函数
document.getElementById("openBtn").onclick = function(){
window.location.href = scheme //尝试打开scheme
//设置3秒的定时下载任务 3秒之后下载app
downloader = setTimeout(function(){
if(isAndroid){
window.location.href = anDownload
}
if(isIos){
windwo.location.href = iosDownload
}
},3000)
},
document.addEventListener('visibilitychange webkitvisibilitychange',function(){
//如果页面隐藏 推测打开scheme成功 清除下载任务
if(docuemnt.hidden || docuemnt.webkitHidden){
clearInterval(downloader)
}
})
window.addEventListener('pagehide',function(){
clearInterval(downloader)
})</code></pre><pre><code> /**
* @description: 安卓唤起App
*/
// 方案1
// openAndroidApp() {
// var d = new Date()
// var t0 = d.getTime()
// if (this.openApp('opengloud://app.gloud.com/detail?tab=1')) {
// this.openApp('opengloud://app.gloud.com/detail?tab=1')
// } else {
// var d = new Date()
// var t1 = d.getTime()
// // 由于打开需要1~2秒,利用这个时间差来处理--打开app后,返回h5页面会出现页面变成app下载页面,影响用户体验
// var delay = setInterval(() => {
// var d = new Date()
// var t1 = d.getTime()
// if (t1 - t0 < 3000 && t1 - t0 > 2000) {
// this.$util.viewAPPDownload()
// }
// if (t1 - t0 >= 3000) {
// clearInterval(delay)
// }
// }, 1000)
// }
// },
// openApp(url) {
// location.href = url
// },
// 方案2:
openAndroidApp() {
// e = e || window.event
window.location.href = 'opengloud://app.gloud.com/detail?tab=1' //尝试打开scheme
// if (e.preventDefault) {
// e.preventDefault()
// } else {
// e.returnValue = false
// }
//设置3秒的定时下载任务 3秒之后下载app
this.downloader = setTimeout(() => {
// window.open("about:blank","_self").close();
this.$util.viewAPPDownload()
}, 3000)
},
// 方案三
// openAndroidApp() {
// var ifr = document.createElement('iframe')
// ifr.src = 'opengloud://app.gloud.com/detail?tab=1'
// ifr.style.display = 'none'
// document.body.appendChild(ifr)
// this.downloader = window.setTimeout(()=>{
// document.body.removeChild(ifr)
// // 这里写兜底策略的逻辑,比如下载。
// // 当然,也可以不加任何的兜底策略,调不起就算了。
// this.$util.viewAPPDownload()
// }, 300)
// },
//方案四
// openAndroidApp(){
// const options = {
// scheme: 'opengloud://app.gloud.com/detail?tab=1',
// intent: {
// package: 'opengloud://app.gloud.com/detail?tab=1',
// scheme: 'opengloud://app.gloud.com/detail?tab=1'
// },
// appstore: 'https://wcgcenter.wostore.cn/gameInfos/appDownload/h5.html?chid=h5start',
// yingyongbao: 'https://wcgcenter.wostore.cn/gameInfos/appDownload/h5.html?chid=h5start',
// fallback: 'https://wcgcenter.wostore.cn/gameInfos/appDownload/h5.html?chid=h5start',
// timeout: 4000,
// }
// const callLib = new CallApp(options)
// callLib.open({})
// },</code></pre>
前端url链接带的参数加密
https://segmentfault.com/a/1190000039252712
2021-02-22T15:35:23+08:00
2021-02-22T15:35:23+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<h4>简单普通的方式:字符串进行编码。</h4><h6>Base64</h6><p><code>Base64</code>是网络上最常见的用于<code>传输8Bit字节码</code>的编码方式之一, Base64就是一种基于<code>64个可打印字符</code>来表示<code>二进制数据</code>的方法。</p><p><strong>Base64编码是从二进制到字符的过程,可用于<code>在HTTP环境下传递较长的标识信息</code></strong></p><pre><code>encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。
URI = Universal Resource Identifier 统一资源标志符,用来标识抽象或物理资源的一个紧凑字符串。
btoa() 方法用于创建一个 base-64 编码的字符串。
atob() 方法用于解码使用 base-64 编码的字符串
decodeURIComponent() 函数可对 encodeURIComponent() 函数编码的 URI 进行解码。
window.encodeURIComponent(window.btoa(123456))
window.atob(window.decodeURIComponent("MTIzNDU2"))</code></pre><p><img src="/img/bVcOQSa" alt="image.png" title="image.png"></p><h4>AES加密</h4><pre><code>import CryptoJS from 'crypto-js/crypto-js'
var pswd="我的密码";
var mi=CryptoJS.AES.encrypt("你好",pswd);
console.log("加密结果四"+mi);
//解密
var result=CryptoJS.AES.decrypt(mi,pswd).toString(CryptoJS.enc.Utf8);
console.log("解密结果:"+result);</code></pre><p><strong>AES解密</strong></p><pre><code>decrypt(word,keyStr){
let key = CryptoJS.enc.Utf8.parse(keyStr)
let decrypt = CryptoJS.AES.decrypt(word,key,{mode:CryptoJS.mode.ECB,padding:CryptoJS.pad.Pkcs7})
return CryptoJS.enc.Utf8.stringify(decrypt).toString()
}</code></pre><p><img src="/img/bVcOUSH" alt="image.png" title="image.png"></p>
React-使用装饰器
https://segmentfault.com/a/1190000039153233
2021-02-02T15:36:11+08:00
2021-02-02T15:36:11+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p>create-react-app默认不支持装饰器的,需要做以下配置。<br>打开 package.json ,可以看到eject。运行 npm run eject 可以让由create-react-app创建的项目的配置项暴露出来。</p><p><strong>安装babel插件</strong></p><p> Babel >= 7.x<br><code>npm install --save-dev @babel/plugin-proposal-decorators</code></p><p> Babel@6.x</p><p><code>npm install --save-dev babel-plugin-transform-decorators-legacy</code></p><p><strong>修改package.json文件的babel配置项</strong><br> Babel >= 7.x</p><pre><code> "babel": { "plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }]
], "presets": [ "react-app" ]
}</code></pre><p> Babel@6.x</p><pre><code>"babel": { "plugins": [ "transform-decorators-legacy" ], "presets": [ "react-app" ]
}</code></pre><p>至此,就可以在项目中使用装饰器了</p><pre><code>@MyContainer
class B extends Component{
render(){ return ( <p>B组件</p>
)
}
}
export default B;</code></pre>
React之Hook
https://segmentfault.com/a/1190000038567479
2020-12-21T17:21:38+08:00
2020-12-21T17:21:38+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h2>什么是hook?</h2><p>Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。</p><h2>State Hook</h2><pre><code>import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量。 const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}</code></pre><p>在这里,<code>useState</code> 就是一个 Hook<br>通过在函数组件里调用它来给组件添加一些内部 state。<br>React 会在重复渲染时保留这个 state<br><code>useState</code> 会返回一对值:<strong>当前</strong>状态和一个让你更新它的函数,<br>你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 <code>this.setState</code></p><p><code>useState</code> 唯一的参数就是初始 state。在上面的例子中,我们的计数器是从零开始的,所以初始 state 就是 <code>0</code>。值得注意的是,不同于 <code>this.state</code>,这里的 state 不一定要是一个对象 —— 如果你有需要,它也可以是。这个初始 state 参数只有在第一次渲染时会被用到。</p><h5>声明多个 state 变量</h5><p>你可以在一个组件中多次使用 State Hook:</p><pre><code>function ExampleWithManyStates() {
// 声明多个 state 变量!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}</code></pre><p><a href="https://link.segmentfault.com/?enc=ZrePvgq9DRTPWufJMPzY4w%3D%3D.xcXXoGTGfH6NUVt%2FW%2BGVN4HQxIo6IlDkB1zYhDGjWqUZH8bJttAR1KGo25emYSmqArY7DlfxBQmlI6LFvZ7bCrUSnjTsS9RtltTUJVPv%2BstQe4zzxMB4o90wuyZCttlfkCHP1t6M0jWnQL5EXQYmTfZnizq1ieSLyA8e47d0obk%3D" rel="nofollow">数组解构</a>的语法让我们在调用 <code>useState</code> 时可以给 state 变量取不同的名字</p><p><img src="/img/bVcLX6X" alt="image.png" title="image.png"></p><p><strong>第一行:</strong> 引入 React 中的 <code>useState</code> Hook。它让我们在函数组件中存储内部 state</p><ul><li><strong>第四行:</strong> 在 <code>Example</code> 组件内部,我们通过调用 <code>useState</code> Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 <code>count</code>,因为它存储的是点击次数。我们通过传 <code>0</code> 作为 <code>useState</code> 唯一的参数来将其初始化为 <code>0</code>。第二个返回的值本身就是一个函数。它让我们可以更新 <code>count</code> 的值,所以我们叫它 <code>setCount</code>。</li><li><strong>第九行:</strong> 当用户点击按钮后,我们传递一个新的值给 <code>setCount</code>。React 会重新渲染 <code>Example</code> 组件,并把最新的 <code>count</code> 传给它。</li></ul><h2>Effect Hook</h2><p><strong>在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。</strong></p><p><code>useEffect</code> 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 <code>componentDidMount</code>、<code>componentDidUpdate</code> 和 <code>componentWillUnmount</code> 具有相同的用途,只不过被合并成了一个 API。</p><p><strong>在 React 更新 DOM 后会设置一个页面标题:</strong></p><pre><code>import React,{useState,useEffect} from 'react'
function Example(){
const [count,setCount] = useState(0)
// 相当于componentDidMount和componentDidUpdate
useEffect(()=>{
// 使用浏览器的API更新页面标题
document.title = `You clicked ${count}times`
});
return (
<div>
<p>
You clicked {count} times
</p>
<button onClick={()=>setCount(count+1)}>
Click me
</button>
</div>
)
}</code></pre><p>当你调用 <code>useEffect</code> 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。<br>由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。<br>默认情况下,React 会在每次渲染后调用副作用函数 —— <strong>包括</strong>第一次渲染的时候。</p><p>副作用函数还可以通过返回一个函数来指定如何“清除”副作用。<br>在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作:</p><pre><code>import React,{useState,useEffect} from 'react'
function FriendStatus(props){
const [isOnline,setIsOnline] = useState(null)
function handleStatusChange(status){
setIsOnline(status.isOnline)
}
useEffect(()=>{
ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange);
return ()=>{
ChatAPI.unsubscribeFromFriendStatus(props.friend.id,handleStatusChange)
}
})
if(isOnline===null){
return 'loadding...'
}
return isOnline? 'Online':'Offline'
}</code></pre><p>React 会在组件销毁时取消对 <code>ChatAPI</code> 的订阅,然后在后续渲染时重新执行副作用函数。</p><p>跟 <code>useState</code> 一样,你可以在组件中多次使用 <code>useEffect</code> :</p><pre><code>import React,{useState,useEffect} from 'react'
function FriendStatus(props){
const [count,setCount] = useState(0)
useEffect(()=>{
document.title = `you clicked ${count} times`
})
const [isOnline,setIsOnline] = useState(null)
function handleStatusChange(status){
setIsOnline(status.isOnline)
}
useEffect(()=>{
ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange);
return ()=>{
ChatAPI.unsubscribeFromFriendStatus(props.friend.id,handleStatusChange)
}
})
if(isOnline===null){
return 'loadding...'
}
return isOnline? 'Online':'Offline'
}</code></pre><p>通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。</p><h4>无需清除的 effect</h4><p>有时候,我们只想<strong>在 React 更新 DOM 之后运行一些额外的代码。</strong>比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。对比一下使用 class 和 Hook 都是怎么实现这些副作用的。</p><h6>使用 class 的示例</h6><p>在 React 的 class 组件中,<code>render</code> 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作<br>这就是为什么在 React class 中,我们把副作用操作放到 <code>componentDidMount</code> 和 <code>componentDidUpdate</code> 函数中<br>回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性</p><pre><code>import React,{ Component} from "react";
class Example extends Component{
constructor(props){
super(props)
this.state = {
count:0
}
}
componentDidMount(){
document.title = `You clicked ${this.state.count} times`
}
componentDIdUpdate(){
document.title = `You clicke ${this.state.count} times`
}
render(){
return (
<div>
<p>you clicked{this.state.count} times</p>
<button onClick={()=>this.setState({count:this.state.count + 1})}>Click Me</button>
</div>
)
}
}</code></pre><p><strong>在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。</strong><br>这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。</p><h6>使用 Hook 的示例</h6><p>使用 <code>useEffect</code> 执行相同的操作。</p><pre><code>import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `You clicked ${count} times`; });
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}</code></pre><p>我们声明了 <code>count</code> state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 <code>useEffect</code> Hook。此函数就是我们的 effect。然后使用 <code>document.title</code> 浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 <code>count</code> 值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。</p><p>通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。</p><p><strong>为什么在组件内部调用 <code>useEffect</code>?</strong> 将 <code>useEffect</code> 放在组件内部让我们可以在 effect 中直接访问 <code>count</code> state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。</p><p><strong><code>useEffect</code> 会在每次渲染后都执行吗?</strong> 是的,默认情况下,它在第一次渲染之后_和_每次更新之后都会执行。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。</p><p><strong>与 <code>componentDidMount</code> 或 <code>componentDidUpdate</code> 不同的是</strong>,使用 <code>useEffect</code> 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 <a href="https://link.segmentfault.com/?enc=z%2BROAfKBtDPikBgBRs7SXA%3D%3D.12PbOnsgLj7aeGlX0Cj8JwgZ3dzJ1FvjNejoZzRAUujSAv5%2BC%2FjUn%2BTNuTfor0XEQmCHuCtIYJWm4xH%2BNBhhko825XOtBVQ1tkS9jGzWjUg%3D" rel="nofollow"><code>useLayoutEffect</code></a> Hook 供你使用,其 API 与 <code>useEffect</code> 相同。</p><h5>需要清除的 effect</h5><p>些副作用是需要清除的。例如<strong>订阅外部数据源</strong>。这种情况下,清除工作是非常重要的,可以防止引起内存泄露, 如何用 Class 和 Hook 来实现?</p><h6>使用 Class 的示例</h6><p>在 React class 中,你通常会在 <code>componentDidMount</code> 中设置订阅,并在 <code>componentWillUnmount</code> 中清除它。例如,假设我们有一个 <code>ChatAPI</code> 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:</p><pre><code>import React, { Component } from 'react'
class FriendStatus extends Component {
constructor(prop) {
super(props)
this.state = { isOnline: null }
this.handleStatusChange = this.handleStatusChange.bind(this)
}
componentDidMount(){
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUnmount(){
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
)
}
handleStatusChange(status){
this.setState({
isOnline:status.isOnline
})
}
render(){
if(this.state.isOnline === null){
return 'loadding...'
}
return this.state.isOnline?'Online':'Offline'
}
}
</code></pre><p>你会注意到 <code>componentDidMount</code> 和 <code>componentWillUnmount</code> 之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。</p><h6>使用 Hook 的示例</h6><p>添加和删除订阅的代码的紧密性,所以 <code>useEffect</code> 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:</p><pre><code>import React, { useState,useEffect } from 'react'
function FriendStatus(props){
const [isOnline,setIsOnline] = useState(null)
useEffect(()=>{
function handleStatusChange(status){
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange);
return function cleanUp(){
ChatAPI.unsubscribeFromFriendStatus(props.friend.id,handleStatusChange)
}
})
if(isOnline === null){
return 'loading...'
}
return isOnline?'Online':"Offline"
}</code></pre><p>并不是必须为 effect 中返回的函数命名。这里我们将其命名为 <code>cleanup</code> 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。</p><p><strong>为什么要在 effect 中返回一个函数?</strong> 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。<br><strong>React 何时清除 effect?</strong> React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React _会_在执行当前 effect 之前对上一个 effect 进行清除。</p><p>其他的 effect 可能不必清除,所以不需要返回。</p><pre><code>useEffect(() => {
document.title = `You clicked ${count} times`;
});</code></pre><h5>多个 Effect 实现关注点分离</h5><p>使用 Hook 其中一个<a href="https://link.segmentfault.com/?enc=p1K%2BsWjC4rpLcRfrdzeHUQ%3D%3D.Kd9ucYH2nDZPiJtG%2F6gQq2zMSJtzAXUp6noz%2FSLsMam8ryFqLZ4YCQG%2Fn4rd8VGKEWC%2BQpB3leJH05mioQWXVcInBApbn%2Be1lhtP6ZUs5pzoDBl%2FTJFBycyZLvvJjtAB" rel="nofollow">目的</a>就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。<br>下述代码是将前述示例中的计数器和好友在线状态指示器逻辑组合在一起的组件:</p><pre><code>class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...</code></pre><p>可以发现设置 <code>document.title</code> 的逻辑是如何被分割到 <code>componentDidMount</code> 和 <code>componentDidUpdate</code> 中的,订阅逻辑又是如何被分割到 <code>componentDidMount</code> 和 <code>componentWillUnmount</code> 中的。而且 <code>componentDidMount</code> 中同时包含了两个不同功能的代码。<br>那么 Hook 如何解决这个问题呢?就像<a href="https://link.segmentfault.com/?enc=60RwForhf7E5xmuQlLRfKw%3D%3D.iaqPUF8lzZIOnxNo6iIOjHkGpR7vsVbkj%2FTlgX13X9b9QfqttBRhIqc9lAgCo4Y0HoU2BdwfT8Vw4D8Qdg0n0by3U1OnZO%2B8YkEoS7E2CvCuSb%2FeRGFfZm9Dh7cA1N5T" rel="nofollow">你可以使用多个 <em>state</em> 的 Hook</a> 一样,你也可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中:</p><pre><code>function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}</code></pre><p><strong>Hook 允许我们按照代码的用途分离他们,</strong> 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的_每一个_ effect。</p><h6>解释: 为什么每次更新的时候都要运行 Effect</h6><p>effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。<br>从 class 中 props 读取 <code>friend.id</code>,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:</p><pre><code>componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}</code></pre><p><strong>但是当组件已经显示在屏幕上时,<code>friend</code> prop 发生变化时会发生什么?</strong> 我们的组件将继续展示原来的好友状态。这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。<br>在 class 组件中,我们需要添加 <code>componentDidUpdate</code> 来解决这个问题:</p><pre><code>componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) { // 取消订阅之前的 friend.id ChatAPI.unsubscribeFromFriendStatus( prevProps.friend.id, this.handleStatusChange ); // 订阅新的 friend.id ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}</code></pre><p><strong>使用 Hook 的版本:</strong></p><pre><code>function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});</code></pre><p>它并不会受到此 bug 影响<br>并不需要特定的代码来处理更新逻辑,因为 <code>useEffect</code> _默认_就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。</p><h3>通过跳过 Effect 进行性能优化</h3><p>在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题<br>在 class 组件中,我们可以通过在 <code>componentDidUpdate</code> 中添加对 <code>prevProps</code> 或 <code>prevState</code> 的比较逻辑解决:</p><pre><code>componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}</code></pre><p>这是很常见的需求,所以它被内置到了 <code>useEffect</code> 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React <strong>跳过</strong>对 effect 的调用,只要传递数组作为 <code>useEffect</code> 的第二个可选参数即可:</p><pre><code>useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新</code></pre><p>我们传入 <code>[count]</code> 作为第二个参数。这个参数是什么作用呢?如果 <code>count</code> 的值是 <code>5</code>,而且我们的组件重渲染的时候 <code>count</code> 还是等于 <code>5</code>,React 将对前一次渲染的 <code>[5]</code> 和后一次渲染的 <code>[5]</code> 进行比较。因为数组中的所有元素都是相等的(<code>5 === 5</code>),React 会跳过这个 effect,这就实现了性能的优化。<br>如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。<br>对于有清除操作的 effect 同样适用:</p><pre><code>useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅</code></pre><p>如果你要使用此优化方式,请确保数组中包含了<strong>所有外部作用域中会随时间变化并且在 effect 中使用的变量</strong>,否则你的代码会引用到先前渲染中的旧变量。<br>如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(<code>[]</code>)作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。<br>如果你传入了一个空数组(<code>[]</code>),effect 内部的 props 和 state 就会一直拥有其初始值。<br> React 会等待浏览器完成画面渲染之后才会延迟调用 <code>useEffect</code>,因此会使得额外操作很方便。</p><p>推荐启用 <a href="https://link.segmentfault.com/?enc=bVsFkhgJ0UDdpOfV5Dz%2FcQ%3D%3D.7egOvYAiWd2JzodlXC9D8u4q%2BFOGObJucGn1UOcePvYQ7Yih1Yvceur4lD%2Bf4LKvbRHmeFtZrPwPBO823COSos6wOcbADcWADUppO9quunQ%3D" rel="nofollow"><code>eslint-plugin-react-hooks</code></a> 中的 <a href="https://link.segmentfault.com/?enc=Df5H80oguDDSKYEdSjCxqQ%3D%3D.cMzlfY3O8vipP236xY3FlMjTieT8%2Fpvl3DPGeGqXs0mw%2BWLxAkMZfiP3o78PvXlf" rel="nofollow"><code>exhaustive-deps</code></a> 规则。此规则会在添加错误依赖时发出警告并给出修复建议。</p><h3>Hook 使用规则</h3><p>Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:</p><ul><li>只能在<strong>函数最外层</strong>调用 Hook。不要在循环、条件判断或者子函数中调用。</li><li>只能在 <strong>React 的函数组件</strong>中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中)<p><a href="https://link.segmentfault.com/?enc=zBgs%2BjBrRXH%2FAAwIaOQvEg%3D%3D.3kyYQ0vQ0hzs9Udjplz%2BwQV2i3TdCqabOutgo5frg6ARC%2BJrwq4508udPYKylLUetehiT0nHcqICDY9CWS5%2FVA%3D%3D" rel="nofollow">linter 插件</a>来自动执行这些规则</p></li></ul><h2>自定义 Hook</h2><p>想要在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:<a href="https://link.segmentfault.com/?enc=0GCOL8va%2FamznLeV6gkN%2Fg%3D%3D.nu7ThQzIqLM%2B80DCbIHLvwLKW8rhQpm72fR8wrt%2FuwWj44RKCUd0cT3hX84VBUQSU2dqP0nkWMM60nOw7UO19A%3D%3D" rel="nofollow">高阶组件</a>和 <a href="https://link.segmentfault.com/?enc=qX8a9wvw2q2MvlkRABbRVA%3D%3D.gzu6XAADAI4rqLnOlB96gAzxdCRFVYagDmH8ziUfgkfCVGJfCJovlVHKrCnw%2ByC8sZssOPSIc%2BaqSOcGkf9qTg%3D%3D" rel="nofollow">render props</a>。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。</p><p><code>FriendStatus</code> 的组件,它通过调用 <code>useState</code> 和 <code>useEffect</code> 的 Hook 来订阅一个好友的在线状态。假设我们想在另一个组件里重用这个订阅逻辑。</p><p>首先,我们把这个逻辑抽取到一个叫做 <code>useFriendStatus</code> 的自定义 Hook 里:</p><pre><code>import React,{useState,useEffect} form 'react'
function useFriendStatus(friendID){
const [isOnline,setIsOnline] = setState(null)
function handleStatusChange(status){
setIsOnline(status.isOnline)
}
useEffect(()=>{
ChatAPI.subscribeToFriendStatus(friendID,handleStatusChange)
return ()=>{
ChatAPI.unsubscribeFromfriendStatus(friendID,handleStatusChange)
}
})
return isOnline
}</code></pre><p>它将 <code>friendID</code> 作为参数,并返回该好友是否在线:</p><p>现在我们可以在两个组件中使用它:</p><pre><code>function FriendStatus(props){
const isOnline = useFriendStatus(props.friend.id)
if(isOnline === null){
return 'loading....'
}
return isOnline?'Online':'Offline'
}</code></pre><pre><code>function FriendListItem(props){
const isOnline = useFriendStatus(props.friend.id)
return (
<li style={{color:isOnline?'green':'black'}}>
{props.friend.name}
</li>
)
}</code></pre><p>每个组件间的 state 是完全独立的。Hook 是一种复用_状态逻辑_的方式,它不复用 state 本身。事实上 Hook 的每次_调用_都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。</p><p>自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “<code>use</code>” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 <code>useSomething</code> 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。</p><p>你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有更多我们没想到的场景。</p><h2>其他 Hook</h2><p>除此之外,还有一些使用频率较低的但是很有用的 Hook。比如,<a href="https://link.segmentfault.com/?enc=Lo6OLKkh1IIH8wBq2dORow%3D%3D.xQ%2FeKYfJMcOkUH9atAqyUOYbd9yI3eST0WDAkgt%2B71qh4W5QGiPqCkesSnM%2BMnwe8SgGKme7yLRU3atrbieMAi0OCiaOjaU7F5uvCkoIv5Y%3D" rel="nofollow"><code>useContext</code></a> 让你不使用组件嵌套就可以订阅 React 的 Context。</p><pre><code>function Example(){
const local = useContext(LocaleContent)
const theme = useContext(ThemeContext)
// ...
}</code></pre><p>另外 <a href="https://link.segmentfault.com/?enc=vu3KmDM3Fbs96bxKhG5j2g%3D%3D.CV0i0v0SIcAedmnwdhd52ovHKs4cnnNCMIy550MB1URgOvAg7eZCRF6RS3GRqsBPBrfRMOb33Kd5ovKwMtePPtSaNOSH7rZnSIex7h8zB9E%3D" rel="nofollow"><code>useReducer</code></a> 可以让你通过 reducer 来管理组件本地的复杂 state。</p><pre><code>function Todos(){
const [todos,dispatch] = useReducer(todosReducer)
}</code></pre>
Powershell 快捷键
https://segmentfault.com/a/1190000038505839
2020-12-17T10:57:49+08:00
2020-12-17T10:57:49+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<p>Powershell的快捷键和cmd,linux中的shell,都比较像。</p><ul><li>ALT+F7 清除命令的历史记录</li><li>PgUp PgDn 显示当前会话的第一个命令和最后一个命令</li><li>Enter 执行当前命令</li><li>End 将光标移至当前命令的末尾</li><li>Del 从右开始删除输入的命令字符</li><li>Esc 清空当前命令行</li><li>F2 自动补充历史命令至指定字符</li><li>(例如历史记录中存在Get-Process,按F2,提示"Enter char to copy up to",键入‘s’,自动补齐命令:Get-Proce)</li><li>F4 删除命令行至光标右边指定字符处</li><li>F7 对话框显示命令行历史记录</li><li>F8 检索包含指定字符的命令行历史记录</li><li>F9 根据命令行的历史记录编号选择命令,历史记录编号可以通过F7查看</li><li>左/右方向键 左右移动光标</li><li>上/下方向键 切换命令行的历史记录</li><li>Home 光标移至命令行最左端</li><li>Backspace 从右删除命令行字符</li><li>Ctrl+C 取消正在执行的命令</li><li>Ctrl+左/右方向键 在单词之间移动光标</li><li>Ctrl+Home 删除光标最左端的所有字符</li><li>Tab 自动补齐命令或者文件名</li></ul>
npm 报错
https://segmentfault.com/a/1190000038481610
2020-12-15T11:31:26+08:00
2020-12-15T11:31:26+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p><strong>1</strong>.“This is probably not a problem with npm. There is likely additional logging output above. ”<br><strong>解决方法:</strong></p><pre><code>rm -rf node_modules
rm package-lock.json
npm cache clear --force
npm install</code></pre><p><strong>2</strong> “npm ERR! A complete log of this run can be found in: npm ERR! D:\node\node_cache\_logs\2020-06-13T08_12_35_648Z-debug.log”</p><p><strong>解决方法</strong></p><pre><code>npm install npm -g
npm install 安装一下依赖即可</code></pre><h2>npm 报错一般是</h2><blockquote><ul><li>缺少依赖 【视情况 仔细看报错信息】</li><li>文件引用错误 【视情况 仔细看报错信息】</li><li>node_moudule依赖问题 【删除重新下载】</li><li>webpack版本问题 【版本过高,降低版本】</li><li>node的版本问题 【版本过高,降低版本】</li></ul></blockquote>
性能优化之笔记
https://segmentfault.com/a/1190000038198470
2020-11-17T15:38:08+08:00
2020-11-17T15:38:08+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h2>性能优化分为网络优化和渲染优化</h2><p>从输入 URL 到页面加载完成,发生了什么? <br>首先我们需要通过 DNS(域名解析系统)将 URL 解析为对应的 IP 地址,然后与这个 IP 地址确定的那台服务器建立起 TCP 网络连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就可以开始走一个渲染的流程。渲染完毕,页面便呈现给了用户,并时刻等待响应用户的操作<br><strong>各个优化</strong><br>DNS 解析花时间,能不能尽量减少解析次数或者把解析前置?能——浏览器 DNS 缓存和 DNS prefetch<br>TCP 每次的三次握手都急死人,有没有解决方案?有——长连接、预连接、接入 SPDY 协议<br>这两个过程的优化往往需要我们和团队的服务端工程师协作完成,<br> HTTP 请求 减少请求次数和减小请求体积方面</p><p><strong>浏览器端的性能优化</strong>——这部分涉及资源加载优化、服务端渲染、浏览器缓存机制的利用、DOM 树的构建、网页排版和渲染过程、回流与重绘的考量、DOM 操作的合理规避等等</p><h4>先说网络优化</h4><p>从输入 URL 到显示页面这个过程中,涉及到网络层面的,有三个主要过程:</p><blockquote><ul><li>DNS 解析</li><li>TCP 连接</li><li>HTTP 请求/响应</li></ul></blockquote><p>对于 DNS 解析和 TCP 连接两个步骤,我们前端可以做的努力非常有限。相比之下,HTTP 连接这一层面的优化才是我们网络优化的核心<br> HTTP 优化有两个大的方向:</p><blockquote><ul><li>减少请求次数</li><li>减少单次请求所花费的时间</li></ul></blockquote><p>指向了我们日常开发中非常常见的操作——资源的压缩与合并<br>这就是我们用构建工具在做的事情</p><h3>webpack 的性能瓶颈</h3><p>webpack 的优化瓶颈,主要是两个方面:</p><ul><li>webpack 的构建过程太花时间</li><li>webpack 打包的结果体积太大</li></ul><h3>webpack 优化方案</h3><h3>构建过程提速策略</h3><h4>不要让 loader 做太多事情——以 babbabel-loader 无疑是强大的,但它也是慢的。</h4><p>babel-loader 无疑是强大的,但它也是慢的。</p><p>最常见的优化方式是,用 include 或 exclude 来帮我们避免不必要的转译,比如 webpack 官方在介绍 babel-loader 时给出的示例</p><pre><code>module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}</code></pre><p>这段代码帮我们规避了对庞大的 node_modules 文件夹或者 bower_components 文件夹的处理。但通过限定文件范围带来的性能提升是有限的。除此之外,如果我们选择开启缓存将转译结果缓存至文件系统,则至少可以将 babel-loader 的工作效率提升两倍。要做到这点,我们只需要为 loader 增加相应的参数设定:</p><pre><code>loader: 'babel-loader?cacheDirectory=true'</code></pre><p>这个规则仅作用于这个 loader,像一些类似 UglifyJsPlugin 的 webpack 插件在工作时依然会被这些庞大的第三方库拖累,webpack 构建速度依然会因此大打折扣。</p><h3>第三方库的处理</h3><p>处理第三方库的姿势有很多,其中,Externals 不够聪明,一些情况下会引发重复打包的问题;而 CommonsChunkPlugin 每次构建时都会重新构建一次 vendor;出于对效率的考虑,我们这里为大家推荐 DllPlugin。<br>DllPlugin 是基于 Windows 动态链接库(dll)的思想被创作出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。<strong>这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化时才会重新打包</strong>。</p><p>用 DllPlugin 处理文件,要分两步走:</p><ul><li>基于 dll 专属的配置文件,打包 dll 库</li><li>基于 webpack.config.js 文件,打包业务代码<br>以一个基于 React 的简单项目为例,我们的 dll 的配置文件可以编写如下:</li></ul><pre><code>const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 依赖的库数组
vendor: [
'prop-types',
'babel-polyfill',
'react',
'react-dom',
'react-router-dom',
]
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
library: '[name]_[hash]',
},
plugins: [
new webpack.DllPlugin({
// DllPlugin的name属性需要和libary保持一致
name: '[name]_[hash]',
path: path.join(__dirname, 'dist', '[name]-manifest.json'),
// context需要和webpack.config.js保持一致
context: __dirname,
}),
],
}</code></pre><p>编写完成之后,运行这个配置文件,我们的 dist 文件夹里会出现这样两个文件:</p><pre><code>vendor-manifest.json
vendor.js</code></pre><p>vendor.js 不必解释,是我们第三方库打包的结果。这个多出来的 vendor-manifest.json,则用于描述每个第三方库对应的具体路径,我这里截取一部分给大家看下:</p><pre><code>
{
"name": "vendor_397f9e25e49947b8675d",
"content": {
"./node_modules/core-js/modules/_export.js": {
"id": 0,
"buildMeta": {
"providedExports": true
}
},
"./node_modules/prop-types/index.js": {
"id": 1,
"buildMeta": {
"providedExports": true
}
},
...
}
}
</code></pre><p>随后,我们只需在 webpack.config.js 里针对 dll 稍作配置:</p><pre><code>const path = require('path');
const webpack = require('webpack')
module.exports = {
mode: 'production',
// 编译入口
entry: {
main: './src/index.js'
},
// 目标文件
output: {
path: path.join(__dirname, 'dist/'),
filename: '[name].js'
},
// dll相关配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest就是我们第一步中打包出来的json文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}</code></pre><p>以上也可用有些繁琐也可用<a href="https://link.segmentfault.com/?enc=NErXBCK%2BLx2EQjaRBtzGVw%3D%3D.Sg93b6qj7Lmvdg2gi7WiCINqmzYJjmkXZ%2FdpJkh2XIZMj%2B0AWp9o7gdskDvxtA0sWYrTAmjXVUhkunP0zoYUsQ%3D%3D" rel="nofollow">AutoDllPlugin</a>替代</p><pre><code> npm install --save-dev autodll-webpack-plugin</code></pre><p><strong>使用</strong></p><pre><code>
const AutoDllPlugin = require('autodll-webpack-plugin');
plugins: [
new AutoDllPlugin({
inject: true, // will inject the DLL bundles to html
context: path.join(__dirname, '..'),
filename: '[name]_[hash].dll.js',
path: 'res/js',
plugins: mode === 'online' ? [
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
})
] : [],
entry: {
vendor: ['vue/dist/vue.esm.js', 'vuex', 'axios', 'vue-router', 'babel-polyfill', 'lodash']
}
})
]
</code></pre><p>一次基于 dll 的 webpack 构建过程优化,便大功告成了!</p><h4>Happypack——将 loader 由单进程转为多进程</h4><p>大家知道,webpack 是单线程的,就算此刻存在多个任务,你也只能排队一个接一个地等待处理。这是 webpack 的缺点,好在我们的 CPU 是多核的,Happypack 会充分释放 CPU 在多核并发方面的优势,帮我们把任务分解给多个子进程去并发执行,大大提升打包效率。</p><p>HappyPack 的使用方法也非常简单,只需要我们把对 loader 的配置转移到 HappyPack 中去就好,我们可以手动告诉 HappyPack 我们需要多少个并发的进程</p><pre><code>const HappyPack = require('happypack')
// 手动创建进程池
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
module.exports = {
module: {
rules: [
...
{
test: /\.js$/,
// 问号后面的查询参数指定了处理这类文件的HappyPack实例的名字
loader: 'happypack/loader?id=happyBabel',
...
},
],
},
plugins: [
...
new HappyPack({
// 这个HappyPack的“名字”就叫做happyBabel,和楼上的查询参数遥相呼应
id: 'happyBabel',
// 指定进程池
threadPool: happyThreadPool,
loaders: ['babel-loader?cacheDirectory']
})
],
}</code></pre><h3>构建结果体积压缩</h3><h4>文件结构可视化,找出导致体积过大的原因</h4><p>这里为大家介绍一个非常好用的包组成可视化工具——<a href="https://link.segmentfault.com/?enc=xbyay9ioV9jydryxVOuZmA%3D%3D.ihTgz8mcU%2FDR08bTTrE%2Fu%2FcfSw5m4TJvkk5VI7muaweW9ofCed3HH94YFxtJgnaygN5VOck8IUctRTHQBUax2g%3D%3D" rel="nofollow">webpack-bundle-analyzer</a>,配置方法和普通的 plugin 无异,它会以矩形树图的形式将包内各个模块的大小和依赖关系呈现出来,格局如官方所提供这张图所示:</p><p><a>!--<img src="/img/remote/1460000038199674" alt="" title="">--</a>--)<br><img src="/img/bVcKrrf" alt="image.png" title="image.png"><br>在使用时,我们只需要将其以插件的形式引入</p><pre><code>const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}</code></pre><h4>拆分资源</h4><p>这点仍然围绕 DllPlugin 展开</p><h4>删除冗余代码</h4><p>一个比较典型的应用,就是 <code>Tree-Shaking</code><br>基于 import/export 语法,Tree-Shaking 可以在编译的过程中获悉哪些模块并没有真正被使用,这些没用的代码,在最后打包的时候会被去除。<br>适合用来处理模块级别的冗余代码。至于<strong>粒度更细</strong>的冗余代码的去除,往往会被整合进 JS 或 CSS 的压缩或分离过程中。<br>这里我们以当下接受度较高的 UglifyJsPlugin 为例,看一下如何在压缩过程中对碎片化的冗余代码(如 console 语句、注释等)进行自动化删除:</p><pre><code>const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
plugins: [
new UglifyJsPlugin({
// 允许并发
parallel: true,
// 开启缓存
cache: true,
compress: {
// 删除所有的console语句
drop_console: true,
// 把使用多次的静态值自动定义为变量
reduce_vars: true,
},
output: {
// 不保留注释
comment: false,
// 使输出的代码尽可能紧凑
beautify: false
}
})
]
}</code></pre><p>webpack4 中,我们是通过配置 optimization.minimize 与 optimization.minimizer 来自定义压缩相关的操作的。</p><h4>按需加载</h4><ul><li>一次不加载完所有的文件内容,只加载此刻需要用到的那部分(会提前做拆分)</li><li>当需要更多内容时,再对用到的内容进行即时加载</li></ul><p>当我们不需要按需加载的时候,我们的代码是这样的:</p><pre><code>import BugComponent from '../pages/BugComponent'
...
<Route path="/bug" component={BugComponent}></code></pre><p>为了开启按需加载,我们要稍作改动。 <br>首先 webpack 的配置文件要走起来:</p><pre><code>output: {
path: path.join(__dirname, '/../dist'),
filename: 'app.js',
publicPath: defaultSettings.publicPath,
// 指定 chunkFilename
chunkFilename: '[name].[chunkhash:5].chunk.js',
},</code></pre><p>路由处的代码也要做一下配合;</p><pre><code>const getComponent => (location, cb) {
require.ensure([], (require) => {
cb(null, require('../pages/BugComponent').default)
}, 'bug')
},
<Route path="/bug" getComponent={getComponent}></code></pre><p>核心就是这个方法:</p><pre><code>require.ensure(dependencies, callback, chunkName)</code></pre><p>这是一个异步的方法,webpack 在打包时,BugComponent 会被单独打成一个文件,只有在我们跳转 bug 这个路由的时候,这个异步方法的回调才会生效,才会真正地去获取 BugComponent 的内容。这就是按需加载。 </p><p>按需加载的粒度,还可以继续细化,细化到更小的组件、细化到某个功能点,都是 ok 的。</p><h2>Gzip 压缩原理</h2><p><strong>开启 Gzip。 </strong> <br>具体的做法非常简单,只需要你在你的 request headers 中加上这么一句:</p><pre><code>accept-encoding:gzip</code></pre><p>我们前端关系更密切的话题:HTTP 压缩。</p><pre><code>HTTP 压缩是一种内置到网页服务器和网页客户端中以改进传输速度和带宽利用率的方式。在使用 HTTP 压缩的情况下,HTTP 数据在从服务器发送前就已压缩:兼容的浏览器将在下载所需的格式前宣告支持何种方法给服务器;不支持压缩方法的浏览器将下载未经压缩的数据。最常见的压缩方案包括 Gzip 和 Deflate。</code></pre><p><strong>HTTP 压缩就是以缩小体积为目的,对 HTTP 内容进行重新编码的过程</strong><br>Gzip 的内核就是 Deflate,目前我们压缩文件用得最多的就是 Gzip。可以说,Gzip 就是 HTTP 压缩的经典例题。</p><h6>该不该用 Gzip</h6><p>压缩 Gzip,服务端要花时间;解压 Gzip,浏览器要花时间。中间节省出来的传输时间,真的那么可观吗?<br>我们处理的都是具备一定规模的项目文件。实践证明,这种情况下压缩和解压带来的时间开销相对于传输过程中节省下的时间开销来说,可以说是微不足道的。</p><h6>Gzip 是万能的吗</h6><p>首先要承认 Gzip 是高效的,压缩后<strong>通常</strong>能帮我们减少响应 70% 左右的大小。</p><p>但它并非万能。Gzip 并不保证针对每一个文件的压缩都会使其变小。</p><p>Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大。反之亦然。</p><h6>webpack 的 Gzip 和服务端的 Gzip</h6><p>一般来说,Gzip 压缩是服务器的活儿:服务器了解到我们这边有一个 Gzip 压缩的需求,它会启动自己的 CPU 去为我们完成这个任务。而压缩文件这个过程本身是需要耗费时间的,大家可以理解为我们以服务器压缩的时间开销和 CPU 开销(以及浏览器解析压缩文件的开销)为代价,省下了一些传输过程中的时间开销。</p><p>既然存在着这样的交换,那么就要求我们学会权衡。服务器的 CPU 性能不是无限的,如果存在大量的压缩需求,服务器也扛不住的。服务器一旦因此慢下来了,用户还是要等。Webpack 中 Gzip 压缩操作的存在,事实上就是为了在构建过程中去做一部分服务器的工作,为服务器分压。</p><p>因此,这两个地方的 Gzip 压缩,谁也不能替代谁。它们必须和平共处,好好合作。作为开发者,我们也应该结合业务压力的实际强度情况,去做好这其中的权衡。</p><h2>图片优化——质量与性能的博弈</h2><p>图片是电商平台的重要资源,甚至有人说“做电商就是做图片”。</p><p>就图片这块来说,与其说我们是在做“优化”,不如说我们是在做“权衡”。因为我们要做的事情,就是去压缩图片的体积(或者一开始就选取体积较小的图片格式)。但这个优化操作,是以牺牲一部分成像质量为代价的。因此我们的主要任务,是尽可能地去寻求一个质量与性能之间的平衡点。<br>时下应用较为广泛的 Web 图片格式有 JPEG/JPG、PNG、WebP、Base64、SVG 等<br>不谈业务场景的选型都是耍流氓</p><p>在计算机中,像素用二进制数来表示。不同的图片格式中像素与二进制位数之间的对应关系是不同的。一个像素对应的二进制位数越多,它可以表示的颜色种类就越多,成像效果也就越细腻,文件体积相应也会越大。 <br>一个二进制位表示两种颜色(0|1 对应黑|白),如果一种图片格式对应的二进制位数有 n 个,那么它就可以呈现 2^n 种颜色。</p><h3>JPEG/JPG</h3><p>关键字:<strong>有损压缩、体积小、加载快、不支持透明</strong></p><h4>JPG 的优点</h4><p>JPG 最大的特点是<strong>有损压缩</strong><br>。这种高效的压缩算法使它成为了一种非常轻巧的图片格式。另一方面,即使被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当我们把图片体积压缩至原有体积的 50% 以下时,JPG 仍然可以保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,可以呈现多达 1600 万种颜色,足以应对大多数场景下对色彩的要求,这一点决定了它压缩前后的质量损耗并不容易被我们人类的肉眼所察觉<br>JPG 适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的背景图、轮播图或 Banner 图出现。</p><h4>JPG 的缺陷</h4><p>有损压缩在上文所展示的轮播图上确实很难露出马脚,但当它处理<strong>矢量图形</strong>和 <strong>Logo</strong> 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。</p><p>此外,JPEG 图像<strong>不支持透明度处理</strong>,透明图片需要召唤 PNG 来呈现。</p><h3>PNG-8 与 PNG-24</h3><p>关键字:<strong>无损压缩、质量高、体积大、支持透明</strong></p><h4>PNG 的优点</h4><p>PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照我们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的可以呈现约 1600 万种颜色。<br>PNG 图片具有比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文我们提到的 JPG 的局限性,唯一的 BUG 就是<strong>体积太大</strong>。<br>前面我们提到,复杂的、色彩层次丰富的图片,用 PNG 来处理的话,成本会比较高,我们一般会交给 JPG 去存储。</p><p>考虑到 PNG 在处理线条和颜色对比度方面的优势,我们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。</p><h4>SVG</h4><p><strong>文本文件、体积小、不失真、兼容性好</strong></p><p>SVG(可缩放矢量图形)是一种基于 XML 语法的图像格式。它和本文提及的其它图片种类有着本质的不同:SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述。</p><p>和性能关系最密切的一点就是:SVG 与 PNG 和 JPG 相比,<strong>文件体积更小,可压缩性更强</strong>。</p><p>当然,作为矢量图,它最显著的优势还是在于<strong>图片可无限放大而不失真</strong>这一点上。这使得 SVG 即使是被放到视网膜屏幕上,也可以一如既往地展现出较好的成像品质——1 张 SVG 足以适配 n 种分辨率。</p><p>此外,<strong>SVG 是文本文件</strong>。我们既可以像写代码一样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也可以把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。这使得 SVG 文件可以被非常多的工具读取和修改,具有较强的<strong>灵活性</strong>。</p><p>SVG 的局限性主要有两个方面,一方面是它的渲染成本比较高,这点对性能来说是很不利的。另一方面,SVG 存在着其它图片格式所没有的学习成本(它是可编程的)</p><h4>SVG 的使用方式与应用场景</h4><p>SVG 是文本文件,我们既可以像写代码一样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也可以把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。</p><ul><li>将 SVG 写入 HTML:</li></ul><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<circle cx="50" cy="50" r="50" />
</svg>
</body>
</html></code></pre><p>将 SVG 写入独立文件后引入 HTML:</p><pre><code><img src="文件名.svg" alt=""></code></pre><p>在实际开发中,我们更多用到的是后者。很多情况下设计师会给到我们 SVG 文件,就算没有设计师,我们还有非常好用的 <a href="https://link.segmentfault.com/?enc=t%2FNmSIo17KFKw0kabYT7EA%3D%3D.f3tPwuC6NEfvOlCQm12%2B2BE1OdiQFSmHF6qrljCyE3w%3D" rel="nofollow">在线矢量图形库</a>。对于矢量图,我们无须深究过多,只需要对其核心特性有所掌握、日后在应用时做到有迹可循即可。</p><h4>最经典的小图标解决方案——雪碧图(CSS Sprites)</h4><p>一种将小图标和背景图像合并到一张图片上,然后利用 CSS 的背景定位来显示其中的每一部分的技术。 </p><p>被运用于众多使用大量小图标的网页应用之上。它可取图像的一部分来使用,使得使用一个图像文件替代多个小文件成为可能。相较于一个小图标一个图像文件,单独一张图片所需的 HTTP 请求更少,对内存和带宽更加友好。</p><h4>Base64</h4><p>不难看出,每次加载图片,都是需要单独向服务器请求这个图片对应的资源的——这也就意味着一次 HTTP 请求的开销。<br><strong>Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行 Base64 编码,我们可以直接将编码结果写入 HTML 或者写入 CSS,从而减少 HTTP 请求的次数</strong></p><p>按照一贯的思路,我们加载图片需要把图片链接写入 img 标签:</p><pre><code><img src="https://user-gold-cdn.xitu.io/2018/9/15/165db7e94699824b?w=22&h=22&f=png&s=3680"></code></pre><p>浏览器就会针对我们的图片链接去发起一个资源请求.</p><p>但是如果我们对这个图片进行 Base64 编码,我们会得到一个这样的字符串:</p><pre><code>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAMJGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU8kagOeWJCQktEAEpITeBCnSpdfQpQo2QhJIKDEkBBU7uqjgWlARwYquitjWAshiw14Wwd4fiKgo62LBhsqbFNDV89477z9n7v3yzz9/mcydMwOAehxbJMpFNQDIExaI48MCmeNT05ikR4AECIAKRgEamyMRBcTFRQEoQ+9/yrubAJG9r9nLfP3c/19Fk8uTcABA4iBncCWcPMiHAMDdOCJxAQCEXqg3m1YggkyEWQJtMUwQsrmMsxTsIeMMBUfJbRLjgyCnA6BCZbPFWQCoyfJiFnKyoB+1pZAdhVyB</code></pre><p>字符串比较长,我们可以直接用这个字符串替换掉上文中的链接地址。你会发现浏览器原来是可以理解这个字符串的,它自动就将这个字符串解码为了一个图片,而不需再去发送 HTTP 请求</p><h4>Base64 的应用场景</h4><p>上面这个实例,其实源自我们 <a href="https://link.segmentfault.com/?enc=5rQbq%2BKlnhOnTWIc2PxgJw%3D%3D.Cf8yUFALl7cHldQFYnxr1RkNKj1Ipt%2BdiBi%2B0eRMtdM%3D" rel="nofollow">掘金</a> 网站 Header 部分的搜索栏 Logo:<br>Base64 编码后,图片大小会膨胀为原文件的 4/3(这是由 Base64 的编码原理决定的)。如果我们把大图也编码到 HTML 或 CSS 文件中,后者的体积会明显增加,即便我们减少了 HTTP 请求,也无法弥补这庞大的体积带来的性能开销,得不偿失。 </p><p>在传输非常小的图片的时候,Base64 带来的文件体积膨胀、以及浏览器解析 Base64 的时间开销,与它节省掉的 HTTP 请求开销相比,可以忽略不计,这时候才能真正体现出它在性能方面的优势。 </p><p>因此,Base64 并非万全之策,我们往往在一张图片满足以下条件时会对它应用 Base64 编码:</p><ul><li>图片的实际尺寸很小(大家可以观察一下掘金页面的 Base64 图,几乎没有超过 2kb 的)</li><li>图片无法以雪碧图的形式与其它小图结合(合成雪碧图仍是主要的减少 HTTP 请求的途径,Base64 是雪碧图的补充)</li><li><p>图片的更新频率非常低(不需我们重复编码和修改文件内容,维护成本较低)</p><h4>Base64 编码工具推荐</h4><p>这里最推荐的是利用 webpack 来进行 Base64 的编码——webpack 的 <a href="https://link.segmentfault.com/?enc=cGK8qZxXMPiemUG4b%2BNuxw%3D%3D.WqsIiRe%2FAgr1gdYdqGYN3MHigknFNKiaKZdcTkiVHmihC1LzLsrwjM3C9rj1Y6LL" rel="nofollow">url-loader</a> 非常聪明,它除了具备基本的 Base64 转码能力,还可以结合文件大小,帮我们判断图片是否有必要进行 Base64 编码。</p></li></ul><p>除此之外,市面上免费的 Base64 编解码工具种类是非常多样化的,有很多网站都提供在线编解码的服务,大家选取自己认为顺手的工具就好。</p><h3>WebP</h3><p>WebP 像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片——它集多种图片文件格式的优点于一身。 </p><p>与 PNG 相比,WebP 无损图像的尺寸缩小了 26%。在等效的 SSIM 质量指数下,WebP 有损图像比同类 JPEG 图像小 25-34%。</p><p>无损 WebP 支持透明度(也称为 alpha 通道),仅需 22% 的额外字节。对于有损 RGB 压缩可接受的情况,有损 WebP 也支持透明度,与 PNG 相比,通常提供 3 倍的文件大小。 <br>WebP 纵有千般好 都逃不开兼容性的大坑</p><p>此外,WebP 还会增加服务器的负担——和编码 JPG 文件相比,编码同样质量的 WebP 文件会占用更多的计算资源。</p><h4>WebP 的应用场景</h4><p>现在限制我们使用 WebP 的最大问题不是“这个图片是否适合用 WebP 呈现”的问题,而是“浏览器是否允许 WebP”的问题,即我们上文谈到的兼容性问题。具体来说,一旦我们选择了 WebP,就要考虑在 Safari 等浏览器下它无法显示的问题,也就是说我们需要准备 PlanB,准备降级方案。</p><p>目前真正把 WebP 格式落地到网页中的网站并不是很多,这其中淘宝首页对 WebP 兼容性问题的处理方式就非常有趣。我们可以打开 Chrome 的开发者工具搜索其源码里的 WebP 关键字</p><pre><code><img src="//img.alicdn.com/tps/i4/TB1CKSgIpXXXXccXXXX07tlTXXX-200-200.png_60x60.jpg_.webp" alt="手机app - 聚划算" class="app-icon"></code></pre><p>.webp 前面,还跟了一个 .jpg 后缀! <br>这个图片应该至少存在 jpg 和 webp 两种格式,程序会根据浏览器的型号、以及该型号是否支持 WebP 这些信息来决定当前浏览器显示的是 .webp 后缀还是 .jpg 后缀。带着这个预判,我们打开并不支持 WebP 格式的 Safari 来进入同样的页面,再次搜索 WebP 关键字:<br>Safari 提示我们找不到,这也是情理之中。我们定位到刚刚示例的 WebP 图片所在的元素,查看一下它在 Safari 里的图片链接</p><pre><code><img src="//img.alicdn.com/tps/i4/TB1CKSgIpXXXXccXXXX07tlTXXX-200-200.png_60x60.jpg" alt="手机app - 聚划算" class="app-icon"></code></pre><p>在 Safari 中的后缀从 .webp 变成了 .jpg!<br>站点确实是先进行了兼容性的预判,在浏览器环境支持 WebP 的情况下,优先使用 WebP 格式,否则就把图片降级为 JPG 格式(本质是对图片的链接地址作简单的字符串切割)。 <br>此外,还有另一个维护性更强、更加灵活的方案——把判断工作交给后端,由服务器根据 HTTP 请求头部的 Accept 字段来决定返回什么格式的图片。当 Accept 字段包含 image/webp 时,就返回 WebP 格式的图片,否则返回原图。这种做法的好处是,当浏览器对 WebP 格式图片的兼容支持发生改变时,我们也不用再去更新自己的兼容判定代码,只需要服务端像往常一样对 Accept 字段进行检查即可。</p><p>由此也可以看出,我们 WebP 格式的局限性确实比较明显,如果决定使用 WebP,兼容性处理是必不可少的</p><h2>浏览器缓存机制与缓存策略</h2><p><strong>缓存可以提高网络IO消耗 提高访问速度</strong><br>通过网络获取内容既速度缓慢又开销巨大。较大的响应需要在客户端与服务器之间进行多次往返通信,这会延迟浏览器获得和处理内容的时间,还会增加访问者的流量费用。因此,缓存并重复利用之前获取的资源的能力成为性能优化的一个关键方面。<br>浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下</p><ol><li>Memory Cache</li><li>Service Worker Cache</li><li>HTTP Cache</li><li>Push Cache</li></ol><h4>HTTP 缓存机制</h4><p>分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。</p><p>强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。</p><p>命中强缓存的情况下,返回的 HTTP 状态码为 200</p><h4>强缓存的实现:从 expires 到 cache-control</h4><p>实现强缓存,过去我们一直用 expires。<br>当服务器返回响应时,在 Response Headers 中将过期时间写入 expires 字段。像这样:</p><pre><code>expires: Wed, 11 Sep 2019 16:12:18 GMT</code></pre><p>expires 是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,如果本地时间小于 expires 设定的过期时间,那么就直接去缓存中取这个资源。</p><p>expires 是有问题的,它最大的问题在于对“本地时间”的依赖。如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。<br>考虑到 expires 的局限性,HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务。</p><p>expires 能做的事情,Cache-Control 都能做;expires 完成不了的事情,Cache-Control 也能做。因此,Cache-Control 可以视作是 expires 的完全替代方案。在当下的前端实践里,我们继续使用 expires 的唯一目的就是向下兼容。</p><p>现在我们给 Cache-Control 字段一个特写:</p><pre><code>cache-control: max-age=31536000</code></pre><p>通过 max-age 来控制资源的有效期。max-age 不是一个时间戳,而是一个时间长度。在本例中,max-age 是 31536000 秒,它意味着该资源在 31536000 秒以内都是有效的,完美地规避了时间戳带来的潜在问题。<br>Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。<br>Cache-Control 的神通,可不止于这一个小小的 max-age。如下的用法也非常常见</p><pre><code>cache-control: max-age=3600, s-maxage=31536000</code></pre><p>s-maxage 优先级高于 max-age,两者同时出现时,优先考虑 s-maxage。如果 s-maxage 未过期,则向代理服务器请求其缓存内容。<br>在项目不是特别大的场景下,max-age 足够用了。但在依赖各种代理的大型架构中,我们不得不考虑代理服务器的缓存问题。s-maxage 就是用于表示 cache 服务器上(比如 cache CDN)的缓存的有效时间的,并只对 public 缓存有效。<br>那么什么是 public 缓存呢</p><h5>public 与 private</h5><p>public 与 private 是针对资源是否能够被代理服务缓存而存在的一组对立概念。<br>如果我们为资源设置了 public,那么它既可以被浏览器缓存,也可以被代理服务器缓存;如果我们设置了 private,则该资源只能被浏览器缓存。private 为默认值。但多数情况下,public 并不需要我们手动设置,比如有很多线上网站的 cache-control 是这样的:<br><img src="/img/bVcKJEg" alt="image.png" title="image.png"><br>设置了 s-maxage,没设置 public,那么 CDN 还可以缓存这个资源吗?答案是肯定的。因为明确的缓存信息(例如“max-age”)已表示响应是可以缓存的。</p><h5>no-store与no-cache</h5><p>no-cache 绕开了浏览器:我们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存情况,而是直接向服务端去确认该资源是否过期</p><p>no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。</p><h3>协商缓存:浏览器与服务器合作之下的缓存策略</h3><p>协商缓存依赖于服务端与浏览器之间的通信。<br>协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。<br>如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304(如下图)<br><img src="/img/bVcKJEW" alt="image.png" title="image.png"></p><h4>协商缓存的实现:从 Last-Modified 到 Etag</h4><p>Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:</p><pre><code>Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT</code></pre><p>随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:</p><pre><code>If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT</code></pre><p>服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。<br>使用 Last-Modified 存在一些弊端,这其中最常见的就是这样两个场景:<br>1 我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。<br>2 当我们修改文件的速度过快时(比如花了 100ms 完成了改动),由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了。</p><p>这两个场景其实指向了同一个 bug——服务器并没有正确感知文件的变化。为了解决这样的问题,Etag 作为 Last-Modified 的补充出现了。</p><p>Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,反之亦然。因此 Etag 能够精准地感知文件的变化<br>Etag 和 Last-Modified 类似,当首次请求时,我们会在响应头里获取到一个最初的标识符字符串,举个🌰,它可以是这样的:</p><pre><code>ETag: W/"2a3b-1602480f459"</code></pre><p>那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对了:</p><pre><code>If-None-Match: W/"2a3b-1602480f459"</code></pre><p>Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。因此启用 Etag 需要我们审时度势。正如我们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充和强化存在。<br>Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。<br><img src="/img/bVcKJMM" alt="image.png" title="image.png"><br>解读一下这张流程图<br>当我们的资源内容不可复用时,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;否则考虑是否每次都需要向服务器进行缓存有效确认,如果需要,那么设 Cache-Control 的值为 no-cache;否则考虑该资源是否可以被代理服务器缓存,根据其结果决定是设置为 private 还是 public;然后考虑该资源的过期时间,设置对应的 max-age 和 s-maxage 值;最后,配置协商缓存需要用到的 Etag、Last-Modified 等参数。</p><h4>MemoryCache</h4><p>MemoryCache,是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。<br> 内存缓存是快的,也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭以后,内存里的数据也将不复存在。</p><p>资源存不存内存,浏览器秉承的是“节约原则”。我们发现,Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以视作浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的几率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘</p><h4>Service Worker Cache</h4><p>Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。<br>这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。<br>我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache<br>Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件。 <br>Service Worker 如何为我们实现离线缓存(注意看注释):入口文件中插入这样一段 JS 代码,用以判断和引入 Service Worker:</p><pre><code>window.navigator.serviceWorker.register('/test.js').then(()=>{
console.log('注册成功')
}).catch((error)=>{
console.log('注册失败')
})</code></pre><p>在 test.js 中,我们进行缓存的处理。假设我们需要缓存的文件分别是 test.html,test.css 和 test.js:</p><pre><code> self.addEventListener('install',event=>{
event.waitUntill(
// 考虑到缓存也需要更新, open内传入的参数为缓存的版本号
caches.open('test-v1').then(cache=>{
return cache.addAll([
//此处传入指定的需缓存的文件名
'/test.html',
'/test.css',
'test.js'
])
})
)
})
//Service Worker会监听所有的网络请求,网络请求的产生触发的是fetch事件,我们可以在其对应的监听函数中实现对请求的拦截
//进而判断是否对应到该请求的缓存 实现从Service Worker中取缓存的目的
self.addEventListener('fetch',event=>{
event.respondWith(
//尝试匹配该请求对应的缓存值
caches.match(event.request).then(res=>{
//如果匹配到了,调用Server Worker缓存
if(res){
return res
}
//如果没有匹配到 向服务器发起这个资源请求
return fetch(event.request).then(response=>{
if(!response||response.status!==200){
return response
}
//请求成功的话,将请求缓存起来
caches.open('test-v1').then((cache)=>{
cache.put(event.request,response)
})
return response.clone()
})
})
)
})</code></pre><p>Server Worker 对协议是有要求的,必须以 https 协议为前提。</p><h4>Push Cache</h4><p><a href="https://link.segmentfault.com/?enc=K7ZLRF6xpzqJhFIcTrf%2FNg%3D%3D.JrvjGgbx5%2BsUS937SyNhD%2B7QsZMlUK3l6Zh9tDvHhVVZ55G%2FsCIKUOMfg3E8seeB%2BYFSkDNnQJBGx51m4KcL1g%3D%3D" rel="nofollow">https://jakearchibald.com/201...</a></p><p>Push Cache 是指 HTTP2 在 server push 阶段存在的缓存。这块的知识比较新,应用也还处于萌芽阶段,<br>但应用范围有限不代表不重要——HTTP2 是趋势、是未来。</p><blockquote>*Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。<p>*Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。 <br>*不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。</p></blockquote><h2>本地存储——从 Cookie 到 Web Storage、IndexDB</h2><h4>从 Cookie 说起</h4><p>HTTP 协议是一个无状态协议,服务器接收客户端的请求,返回一个响应 服务器并没有记录下关于客户端的任何信息。<br>Cookie 说白了就是一个存储在浏览器里的一个小小的文本文件,它附着在 HTTP 请求上,在浏览器和服务器之间“飞来飞去”。它可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态。</p><h4>Cookie的性能劣势</h4><p>Cookie 不够大</p><p>Cookie 是有体积上限的,它最大只能有 4KB。当 Cookie 超过 4KB 时,它将面临被裁切的命运。这样看来,Cookie 只能用来存取少量的信息。</p><h4>过量的 Cookie 会带来巨大的性能浪费</h4><p><strong>Cookie 是紧跟域名的</strong>。我们通过响应头里的 Set-Cookie 指定要存储的 Cookie 值。默认情况下,domain 被设置为设置 Cookie 页面的主机名,我们也可以手动设置 domain 的值:</p><pre><code>Set-Cookie: name=xiuyan; domain=xiuyan.me</code></pre><p><strong>同一个域名下的所有请求,都会携带 Cookie</strong></p><p>请求一张图片或者一个 CSS 文件,我们也要携带一个 Cookie 跑来跑去(关键是 Cookie 里存储的信息我现在并不需要),这是一件多么劳民伤财的事情。Cookie 虽然小,请求却可以有很多,随着请求的叠加,这样的不必要的 Cookie 带来的开销将是无法想象的。</p><h4>Web Storage</h4><p>Web Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。它又分为 Local Storage 与 Session Storage。<br>两者的区别在于<strong>生命周期</strong>与<strong>作用域</strong>的不同。</p><ul><li>生命周期:Local Storage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。</li><li>作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们<strong>不在同一个浏览器窗口中</strong>打开,那么它们的 Session Storage 内容便无法共享。</li></ul><h3>Web Storage 的特性</h3><ul><li>存储容量大: Web Storage 根据浏览器的不同,存储容量可以达到 5-10M 之间。</li><li>仅位于浏览器端,不与服务端发生通信。</li><li><h3>Web Storage 核心 API 使用示例</h3></li></ul><p>Web Storage 保存的数据内容和 Cookie 一样,是文本内容,以键值对的形式存在。Local Storage 与 Session Storage 在 API 方面无异,这里我们以 localStorage 为例:</p><ul><li>存储数据:setItem()</li></ul><pre><code>localStorage.setItem('user_name', 'xiuyan')</code></pre><ul><li>读取数据: getItem()</li></ul><pre><code>localStorage.getItem('user_name')</code></pre><ul><li>删除某一键名对应的数据: removeItem()</li></ul><pre><code>localStorage.removeItem('user_name')</code></pre><ul><li>清空数据记录:clear()</li></ul><pre><code>localStorage.clear()</code></pre><h3>应用场景</h3><p>倾向于用它来存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串:<br>有的网站还会用它存储一些不经常更新的 CSS、JS 等静态资源。</p><h4>Session Storage</h4><p>Session Storage 更适合用来存储生命周期和它同步的<strong>会话级别</strong>的信息。这些信息只适用于当前会话,当你开启新的会话时,它也需要相应的更新或释放。比如微博的 Session Storage 就主要是存储你本次会话的浏览足迹:<br><img src="/img/bVcKKaw" alt="image.png" title="image.png"><br>lasturl 对应的就是你上一次访问的 URL 地址,这个地址是即时的。当你切换 URL 时,它随之更新,当你关闭页面时,留着它也确实没有什么意义了,干脆释放吧。这样的数据用 Session Storage 来处理再合适不过<br>Web Storage 是一个从定义到使用都非常简单的东西。它使用键值对的形式进行存储,这种模式有点类似于对象,却甚至连对象都不是——它只能存储字符串,要想得到对象,我们还需要先对字符串进行一轮解析。</p><p>说到底,Web Storage 是对 Cookie 的拓展,它只能用于存储少量的简单数据。当遇到大规模的、结构复杂的数据时,Web Storage 也爱莫能助了。这时候我们就要清楚我们的终极大 boss——IndexDB!</p><h4>终极形态:IndexDB</h4><p>IndexDB 是一个<strong>运行在浏览器上的非关系型数据库</strong>。既然是数据库了,那就不是 5M、10M 这样小打小闹级别了。理论上来说,IndexDB 是没有存储上限的(一般来说不会小于 250M)。它不仅可以存储字符串,还可以存储二进制数据。</p><p>遵循 MDN 推荐的操作模式 操作一个基本的 IndexDB 使用流程<br>1 打开/创建一个 IndexDB 数据库(当该数据库不存在时,open 方法会直接创建一个名为 xiaoceDB 新数据库)。</p><pre><code> //后面的回调中 我们可以通过event.target.result拿到数据库实例
let db
//参数1位数据库名 参数2为版本号
const request = window.indexedDB.open('xiaoceDB',1)
//使用IndexDB失败时的监听函数
request.onerror = function(event){
console.log('无法使用IndexDB');
}
//成功
request.onsuccess = function(event){
//此处就可以获取到db实例
db = event.target.result
console.log('您打开了IndexDB');
}</code></pre><p>2 创建一个object store(object store对标到数据库中的表单位)</p><pre><code> //onupgradeneeded事件会在初始化数据库/版本发生更新时调用,我们在它的监听函数中创建object store
request.onupgradeneeded = function(event){
let objectStore
//如果同名表未被创建过 则新建test表
if(!db.objectStoreNames.contains('test')){
objectStore = db.createObjectStore('test',{keyPath:'id'})
}
}</code></pre><p>3 构建一个事务来执行一些数据库操作,像增加或提取数据等。</p><pre><code> //创建事务 指定表格名称和读写功能
const transaction = db.transaction(["test"],"readwrite")
// 拿到Object Store对象
const objectStore = transaction.objectStore("test")
//向表格写入数据
objectStore.add({id:1,name:'xiuyan'})</code></pre><p>4 通过监听正确类型的事件以等待操作完成。</p><pre><code> // 操作完成时的监听函数
transaction.oncomplete = function(event){
console.log('操作完成')
}
// 操作失败时的监听函数
transaction.onerror = function(event){
console.log('这里有一个error')
}</code></pre><h4>IndexDB 的应用场景</h4><p>在 IndexDB 中,我们可以创建多个数据库,一个数据库中创建多张表,一张表中存储多条数据——这足以 hold 住复杂的结构性数据。IndexDB 可以看做是 LocalStorage 的一个升级,当数据的复杂度和规模上升到了 LocalStorage 无法解决的程度,我们毫无疑问可以请出 IndexDB 来帮忙。 </p><p>浏览器缓存/存储技术的出现和发展,为我们的前端应用带来了无限的转机。近年来基于缓存/存储技术的第三方库层出不绝,此外还衍生出了 <a href="https://link.segmentfault.com/?enc=S3BcyZ2C%2FoXUDKlb5sxfvQ%3D%3D.ix5flsB3YwGHJBza%2B1GSO8j%2F2hm91JuqnWoori1y0yY%3D" rel="nofollow">PWA</a> 这样优秀的 Web 应用模型。可以说,现代前端应用,尤其是移动端应用,之所以可以发展到在体验上叫板 Native 的地步,主要就是仰仗缓存/存储立下的汗马功劳</p><h2>CDN 的缓存与回源机制解析</h2><p>CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响</p><p>缓存、本地存储带来的性能提升,是不是只能在“获取到资源并把它们存起来”这件事情发生之后?也就是说,首次请求资源的时候,这些招数都是救不了我们的。要提升首次请求的响应能力,我们还需要借助 CDN 的能力</p><h4>CDN 如何工作</h4><p>*假设我的根服务器在杭州<br>此时有一位北京的用户向我请求资源。在网络带宽小、用户访问量大的情况下,杭州的这一台服务器或许不那么给力,不能给用户非常快的响应速度。于是我灵机一动,把这批资源 copy 了一批放在北京的机房里。当用户请求资源时,就近请求北京的服务器,北京这台服务器低头一看,这个资源我存了,离得这么近,响应速度肯定噌噌的!那如果北京这台服务器没有 copy 这批资源呢?它会再向杭州的根服务器去要这个资源。在这个过程中,北京这台服务器就扮演着 CDN 的角色。*</p><h3>CDN的核心功能特写</h3><p>CDN 的核心点有两个,一个是<strong>缓存</strong>,一个是<strong>回源</strong>。</p><p>“缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。</p><h3>CDN 与前端性能优化</h3><p><strong>CDN 往往被用来存放静态资源</strong>。上文中我们举例所提到的“根服务器”本质上是业务服务器,它的核心任务在于<strong>生成动态页面或返回非纯静态页面</strong>,这两种过程都是需要计算的。业务服务器仿佛一个车间,车间里运转的机器轰鸣着为我们产出所需的资源;相比之下,CDN 服务器则像一个仓库,它只充当资源的“栖息地”和“搬运工”。<br>所谓“静态资源”,就是像 JS、CSS、图片等<strong>不需要业务服务器进行计算即得的资源</strong>。而“动态资源”,顾名思义是需要<strong>后端实时动态生成的资源</strong>,较为常见的就是 JSP、ASP 或者依赖服务端渲染得到的 HTML 页面。<br>什么是“非纯静态资源”呢?它是指<strong>需要服务器在页面之外作额外计算的 HTML 页面</strong>。具体来说,当我打开某一网站之前,该网站需要通过权限认证等一系列手段确认我的身份、进而决定是否要把 HTML 页面呈现给我。这种情况下 HTML 确实是静态的,但它<strong>和业务服务器的操作耦合</strong>,我们把它丢到CDN 上显然是不合适的。</p><h3>CDN 的实际应用</h3><p>静态资源本身具有访问频率高、承接流量大的特点,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN 是静态资源提速的重要手段,在许多一线的互联网公司,“静态资源走 CDN”并不是一个建议,而是一个规定。<br>比如以淘宝为代表的阿里系产品,就遵循着这个“规定”。</p><p>打开淘宝首页,我们可以在 Network 面板中看到,“非纯静态”的 HTML 页面,是向业务服务器请求来的:<br><img src="https://segmentfault.com/img/bVcKLoU" alt="image.png" title="image.png"><br>我们点击 preview,可以看到业务服务器确实是返回给了我们一个尚未被静态资源加持过的简单 HTML 页面,所有的图片内容都是先以一个 div 占位: <br><img src="https://segmentfault.com/img/bVcKLo4" alt="image.png" title="image.png"></p><p>相应地,我们随便点开一个静态资源,可以看到它都是从 CDN 服务器上请求来的。</p><p>比如说图片: <br><img src="https://segmentfault.com/img/bVcKLpt" alt="image.png" title="image.png"></p><p>再比如 JS、CSS 文件:<br><img src="https://segmentfault.com/img/bVcKLpv" alt="image.png" title="image.png"></p><h3>CDN 优化细节</h3><p>如何让 CDN 的效用最大化?这又是需要前后端程序员一起思考的庞大命题。它涉及到 CDN 服务器本身的性能优化、CDN 节点的地址选取等。谈离前端最近的这部分细节:CDN 的域名选取。<br>淘宝首页的例子,我们注意到业务服务器的域名是这个:</p><pre><code>www.taobao.com</code></pre><p>而 CDN 服务器的域名是这个</p><pre><code>g.alicdn.com</code></pre><p>我们讲到 Cookie 的时候,为了凸显 Local Storage 的优越性,曾经提到过<br>同一个域名下的请求会不分青红皂白地携带 Cookie,而静态资源往往并不需要 Cookie 携带什么认证信息。把静态资源和主页面置于不同的域名下,完美地避免了不必要的 Cookie 的出现!<br>看起来是一个不起眼的小细节,但带来的效用却是惊人的。以电商网站静态资源的流量之庞大,如果没把这个多余的 Cookie 拿下来,不仅用户体验会大打折扣,每年因性能浪费带来的经济开销也将是一个非常恐怖的数字。</p><p>如此看来,性能优化还真是要步步为营!</p><h2>服务端渲染的探索与实践、</h2><h4>服务端渲染的运行机制</h4><h4>客户端渲染</h4><p>客户端渲染模式下,服务端会把渲染需要的静态文件发送给客户端,客户端加载过来之后,自己在浏览器里跑一遍 JS,根据 JS 的运行结果,生成相应的 DOM。这种特性使得客户端渲染的源代码总是特别简洁,</p><pre><code><!doctype html>
<html>
<head>
<title>我是客户端渲染的页面</title>
</head>
<body>
<div id='root'></div>
<script src='index.js'></script>
</body>
</html></code></pre><p>根节点下到底是什么内容呢?你不知道,我不知道,只有浏览器把 index.js 跑过一遍后才知道,这就是典型的客户端渲染。</p><p><strong>页面上呈现的内容,你在 html 源文件里里找不到</strong>——这正是它的特点。</p><h3>服务端渲染</h3><p>服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。 <br>使用服务端渲染的网站,可以说是“所见即所得”,<strong>页面上呈现的内容,我们在 html 源文件里也能找到</strong>。</p><p>比如知乎就是典型的服务端渲染案例:</p><h4>服务端渲染解决了什么性能问题</h4><p>事实上,很多网站是出于效益的考虑才启用服务端渲染,性能倒是在其次。 <br>假设 A 网站页面中有一个关键字叫“前端性能优化”,这个关键字是 JS 代码跑过一遍后添加到 HTML 页面中的。那么客户端渲染模式下,我们在搜索引擎搜索这个关键字,是找不到 A 网站的——搜索引擎只会查找现成的内容,不会帮你跑 JS 代码。A 网站的运营方见此情形,感到很头大:搜索引擎搜不出来,用户找不到我们,谁还会用我的网站呢?为了把“现成的内容”拿给搜索引擎看,A 网站不得不启用服务端渲染。</p><p>但性能在其次,不代表性能不重要。服务端渲染解决了一个非常关键的性能问题——首屏加载速度过慢。在客户端渲染模式下,我们除了加载 HTML,还要等渲染所需的这部分 JS 加载完,之后还得把这部分 JS 在浏览器上再跑一遍。这一切都是发生在用户点击了我们的链接之后的事情,在这个过程结束之前,用户始终见不到我们网页的庐山真面目,也就是说用户一直在等!相比之下,服务端渲染模式下,服务器给到客户端的已经是一个直接可以拿来呈现给用户的网页,中间环节早在服务端就帮我们做掉了,用户岂不“美滋滋”?</p><h4>服务端渲染的应用实例</h4><p>先来看一下在一个 React 项目里,服务端渲染是怎么实现的。本例中,我们使用 Express 搭建后端服务。</p><p>项目中有一个叫做 VDom 的 React 组件,它的内容如下。</p><p>VDom.js:</p><pre><code> import React from 'react'
const VDom = ()=>{
return <div>我是一个被渲染为真是DOM的虚拟DOM</div>
}
export default VDom</code></pre><p>在服务端的入口文件中,我引入这个组件,对它进行渲染:</p><pre><code> import express from 'express'
import React from 'react'
import {renderToString} from 'react-dom/server'
import VDom from './VDom'
// 创建一个express应用
const app = express()
//renderToString 是把虚拟DOM转化为真实DOM内容
const Page = `
<html>
<head>
<title>test</title>
</head>
<body>
<span>服务端渲染出了真实DOM:</span>
${RDom}
</body>
</html>
`
//配置HTML内容对应的路由
app.get('/index',function(req,res){
res.send(Page)
})
// 配置端口号
const server = app.listen(8000)</code></pre><p>根据我们的路由配置,当我访问 <a href="https://link.segmentfault.com/?enc=rK5NtTpAx%2F56K2Wm6HRtgg%3D%3D.Fp2yMbsyAv7%2FMHkQiC5rXYXT2QZ6OOnWc9R4c%2BGtr1s%3D" rel="nofollow">http://localhost:8000/index</a> 时,就可以呈现出服务端渲染的结果了:</p><p>我们可以看到,VDom 组件已经被 renderToString 转化为了一个内容为<code><div data-reactroot="">我是一个被渲染为真实DOM的虚拟DOM</div></code>的字符串,这个字符串被插入 HTML 代码,成为了真实 DOM 树的一部分。<br>那么 Vue 是如何实现服务端渲染的呢?<br>该示例直接将 Vue 实例整合进了服务端的入口文件中:</p><pre><code>const Vue = require('vue')
// 创建一个express应用
const server = require('express')()
//提取出renderer实例
const renderer = require('vue-server-renderer').createRenderer()
server.get('*',(req,res)=>{
// 编写Vue实例(虚拟DOM节点)
const app = new Vue({
data:{
url:req.url
},
// 编写模板HTML的内容
template:`<div>访问的URL是:{{url}}</div>`
})
// renderToString是把Vue实例转换为真实DOM的关键方法
renderer.renderToString(app,(err,html)=>{
if(err){
res.status(500).end("Internal Server Error")
return
}
// 把渲染出来的真实DOM字符串插入HTML模板中
res.end(`
<!DOCTYPE HTML>
<html>
<head>
<title>hello</title>
</head>
<body>
${html}
</body>
</html>
`)
})
})
server.listen(8080)</code></pre><p>实际项目比这些复杂很多,但万变不离其宗。强调的只有两点:一是这个 renderToString() 方法;二是把转化结果“塞”进模板里的这一步。这两个操作是服务端渲染的灵魂操作。在虚拟 DOM“横行”的当下,服务端渲染不再是早年 JSP 里简单粗暴的字符串拼接过程,它还要求这一端要具备将虚拟 DOM 转化为真实 DOM 的能力。与其说是“把 JS 在服务器上先跑一遍”,不如说是“把 Vue、React 等框架代码先在 Node 上跑一遍”。</p><h4>服务端渲染的应用场景</h4><p>服务端渲染本质上是<strong>本该浏览器做的事情,分担给服务器去做</strong>。这样当资源抵达浏览器时,它呈现的速度就快了。乍一看好像很合理:浏览器性能毕竟有限,服务器多牛逼!能者多劳,就该让服务器多干点活!</p><p>但仔细想想,在这个网民遍地的时代,几乎有多少个用户就有多少台浏览器。用户拥有的浏览器总量多到数不清,那么一个公司的服务器又有多少台呢?我们把这么多台浏览器的渲染压力集中起来,分散给相比之下数量并不多的服务器,服务器肯定是承受不住的。服务端渲染也并非万全之策。<br>在实践中,建议大家先忘记服务端渲染这个事情——服务器稀少而宝贵,但首屏渲染体验和 SEO 的优化方案却很多——我们最好先把能用的低成本“大招”都用完。除非网页对性能要求太高了,以至于所有的招式都用完了,性能表现还是不尽人意,这时候我们就可以考虑向老板多申请几台服务器,把服务端渲染搞起来了~</p><h2>浏览器背后的运行机制</h2><p>目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。<br>可能会听说过 Chrome 的内核就是 Webkit,殊不知 Chrome 内核早已迭代为了 Blink。但是换汤不换药,Blink 其实也是基于 Webkit 衍生而来的一个分支,因此,Webkit 内核仍然是当下浏览器世界真正的霸主。</p><p>什么是渲染过程?简单来说,渲染引擎根据 HTML 文件描述构建相应的数学模型,调用浏览器各个零部件,从而将网页资源代码转换为图像结果,这个过程就是渲染过程</p><p>我们最需要关注的,就是HTML 解释器、CSS 解释器、图层布局计算模块、视图绘制模块与JavaScript 引擎这几大模块:</p><ul><li>HTML 解释器:将 HTML 文档经过词法分析输出 DOM 树。</li><li>CSS 解释器:解析 CSS 文档, 生成样式规则。</li><li>图层布局计算模块:布局计算每个对象的精确位置和大小。</li><li>视图绘制模块:进行具体节点的图像绘制,将像素渲染到屏幕上。</li><li>JavaScript 引擎:编译执行 Javascript 代码。</li></ul><h5>浏览器渲染过程解析</h5><p><img src="/img/remote/1460000038323079" alt="在这里插入图片描述" title="在这里插入图片描述"></p><ul><li>解析 HTML</li></ul><p>在这一步浏览器执行了所有的加载解析逻辑,在解析 HTML 的过程中发出了页面渲染所需的各种外部资源请求。</p><ul><li>计算样式</li></ul><p>浏览器将识别并加载所有的 CSS 样式信息与 DOM 树合并,最终生成页面 render 树(:after :before 这样的伪元素会在这个环节被构建到 DOM 树中)。</p><ul><li><p>计算图层布局<br> 页面中所有元素的相对位置信息,大小等信息均在这一步得到计算。</p><ul><li>绘制图层</li></ul><p>在这一步中浏览器会根据我们的 DOM 代码结果,把每一个页面图层转换为像素,并对所有的媒体文件进行解码。</p><ul><li>整合图层,得到页面</li></ul><p>最后一步浏览器会合并合各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上。(复杂的视图层会给这个阶段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,我们有时会手动区分不同的图层)。<br>段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,我们有时会手动区分不同的图层)。</p></li></ul><p><strong>几棵重要的“树”</strong><br><img src="/img/remote/1460000038323081" alt="在这里插入图片描述" title="在这里插入图片描述"></p><ul><li>DOM 树:解析 HTML 以创建的是 DOM 树(DOM tree ):渲染引擎开始解析 HTML 文档,转换树中的标签到 DOM 节点,它被称为“内容树”。</li><li>CSSOM 树:解析 CSS(包括外部 CSS 文件和样式元素)创建的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的。</li><li>渲染树:CSSOM 与 DOM 结合,之后我们得到的就是渲染树(Render tree )。</li><li>布局渲染树:从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,我们便得到了基于渲染树的布局渲染树(Layout of the render tree)。</li><li>绘制渲染树: 遍历渲染树,每个节点将使用 UI 后端层来绘制。整个过程叫做绘制渲染树(Painting the render tree)。</li></ul><p>渲染过程说白了,首先是基于 HTML 构建一个 DOM 树,这棵 DOM 树与 CSS 解释器解析出的 CSSOM 相结合,就有了布局渲染树。最后浏览器以布局渲染树为蓝本,去计算布局并绘制图像,我们页面的初次渲染就大功告成了。<br><strong>基于渲染流程的 CSS 优化建议</strong><br>CSS 引擎查找样式表,对每条规则都按从右到左的顺序去匹配。 看如下规则:</p><pre><code>#myList li {}</code></pre><p>习惯了从左到右阅读的文字阅读方式,会本能地以为浏览器也是从左到右匹配 CSS 选择器的,因此会推测这个选择器并不会费多少力气:#myList 是一个 id 选择器,它对应的元素只有一个,查找起来应该很快。定位到了 myList 元素,等于是缩小了范围后再去查找它后代中的 li 元素,没毛病。</p><p>事实上,CSS 选择符是从右到左进行匹配的。我们这个看似“没毛病”的选择器,实际开销相当高:浏览器必须遍历页面上每个 li 元素,并且每次都要去确认这个 li 元素的父元素 id 是不是 myList</p><p>总结出如下性能提升的方案:</p><ul><li>避免使用通配符,只对需要用到的元素进行选择。</li><li>关注可以通过继承实现的属性,避免重复匹配重复定义。</li><li>少用标签选择器。如果可以,用类选择器替代,举个🌰:</li><li>错误示范:</li></ul><pre><code>- #myList li{}</code></pre><p>正确:</p><pre><code>.myList_li {}</code></pre><ul><li>减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。</li></ul><h3>CSS 与 JS 的加载顺序优化</h3><h5>CSS 的阻塞</h5><p>DOM 和 CSSOM 合力才能构建渲染树。这一点会给性能造成严重影响:默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。即便 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK(这主要是为了避免没有 CSS 的 HTML 页面丑陋地“裸奔”在用户眼前)。 <br>只有当我们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才登场,CSSOM 的构建才开始。很多时候,DOM 不得不等待 CSSOM。</p><pre><code>CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。</code></pre><ul><li>将 CSS 放在 head 标签里 和尽快(启用 CDN 实现静态资源加载速度的优化)</li></ul><h5>JS 的阻塞</h5><p>JS 的作用在于修改,它帮助我们修改网页的方方面面:内容、样式以及它如何响应用户交互。这“方方面面”的修改,本质上都是对 DOM 和 CSSDOM 进行修改。因此 JS 的执行会阻止 CSSOM,在我们不作显式声明的情况下,它也会阻塞 DOM。</p><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JS阻塞测试</title>
<style>
#container {
background-color: yellow;
width: 100px;
height: 100px;
}
</style>
<script>
// 尝试获取container元素
var container = document.getElementById("container")
console.log('container', container)
</script>
</head>
<body>
<div id="container"></div>
<script>
// 尝试获取container元素
var container = document.getElementById("container")
console.log('container', container)
// 输出container元素此刻的背景色
console.log('container bgColor', getComputedStyle(container).backgroundColor)
</script>
<style>
#container {
background-color: blue;
}
</style>
</body>
</html></code></pre><p>三个 console 的结果分别为:<br><img src="/img/remote/1460000038323075" alt="在这里插入图片描述" title="在这里插入图片描述"><br>第一次尝试获取 id 为 container 的 DOM 失败,这说明 JS 执行时阻塞了 DOM,后续的 DOM 无法构建;第二次才成功,这说明脚本块只能找到在它前面构建好的元素。这两者结合起来,“阻塞 DOM”得到了验证。再看第三个 console,尝试获取 CSS 样式,获取到的是在 JS 代码执行前的背景色(yellow),而非后续设定的新样式(blue),说明 CSSOM 也被阻塞了。<br>JS 引擎是独立于渲染引擎存在的。我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 因此与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。 <br>可以通过对它使用 defer 和 async 来避免不必要的阻塞,这里我们就引出了外部 JS 的三种加载方式。</p><h5>JS的三种加载方式</h5><p><strong>- 正常模式:</strong></p><pre><code><script src="index.js"></script></code></pre><p>这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情<br><strong>async 模式:</strong></p><pre><code><script async src="index.js"></script></code></pre><p>async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。</p><p><strong>defer 模式:</strong></p><pre><code><script defer src="index.js"></script></code></pre><p>defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。 </p><p>脚本与 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,我们会选用 defer。 <br>通过审时度势地向 script 标签添加 async/defer,我们就可以告诉浏览器在等待脚本可用期间不阻止其它的工作,这样可以显著提升性能。</p><h2>DOM 优化原理与基本实践</h2><p>把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接</p><p>JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。 <br>JS 引擎和渲染引擎(浏览器内核)是独立实现的。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”<br><img src="/img/remote/1460000038323077" alt="在这里插入图片描述" title="在这里插入图片描述"></p><p>过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题</p><h3>对 DOM 的修改引发样式的更迭</h3><p>过桥很慢,到了桥对岸,我们的更改操作带来的结果也很慢。 </p><p>很多时候,我们对 DOM 的操作都不会局限于访问,而是为了修改它。当我们对 DOM 的修改会引发它外观(样式)上的改变时,就会触发回流或重绘。 <br>个过程本质上还是因为我们对 DOM 的修改触发了渲染树(Render Tree)的变化所导致的: <br><img src="/img/remote/1460000038323076" alt="在这里插入图片描述" title="在这里插入图片描述"><br>回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。 </p><p>重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。 </p><p>由此我们可以看出,重绘不一定导致回流,回流一定会导致重绘。硬要比较的话,回流比重绘做的事情更多,带来的开销也更大。</p><h3>给你的 DOM “提提速”</h3><p><strong>减少 DOM 操作:少交“过路费”、避免过度渲染</strong></p><pre><code> <!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>DOM操作测试</title>
</head>
<body>
<div id="container"></div>
</body>
</html></code></pre><p>此时我有一个假需求——我想往 container 元素里写 10000 句一样的话。如果我这么做:</p><pre><code>for(var count=0;count<10000;count++){
document.getElementById('container').innerHTML+='<span>我是一个小测试</span>'
} </code></pre><p>这段代码有两个明显的可优化点。</p><p>第一点,过路费交太多了。我们每一次循环都调用 DOM 接口重新获取了一次 container 元素,相当于每次循环都交了一次过路费。前后交了 10000 次过路费,但其中 9999 次过路费都可以用缓存变量的方式节省下来:</p><pre><code>// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){
container.innerHTML += '<span>我是一个小测试</span>'
} </code></pre><p>第二点,不必要的 DOM 更改太多了。我们的 10000 次循环里,修改了 10000 次 DOM 树。我们前面说过,对 DOM 的修改会引发渲染树的改变、进而去走一个(可能的)回流或重绘的过程,而这个过程的开销是很“贵”的。这么贵的操作,我们竟然重复执行了 N 多次!其实我们可以通过就事论事的方式节省下来不必要的渲染:</p><pre><code>let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){
// 先对内容进行操作
content += '<span>我是一个小测试</span>'
}
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content</code></pre><p>JS 层面的事情,JS 自己去处理,处理好了,再来找 DOM 打报告<br>事实上,考虑JS 的运行速度,比 DOM 快得多这个特性。我们减少 DOM 操作的核心思路,就是让 JS 去给 DOM 分压。 <br>这个思路,在 DOM Fragment 中体现得淋漓尽致。</p><h3>DocumentFragment</h3><p>DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题<br>在我们上面的例子里,字符串变量 content 就扮演着一个 DOM Fragment 的角色。其实无论字符串变量也好,DOM Fragment 也罢,它们本质上都作为脱离了真实 DOM 树的容器出现,用于缓存批量化的 DOM 操作。 </p><p>前面我们直接用 innerHTML 去拼接目标内容,这样做固然有用,但却不够优雅。相比之下,DOM Fragment 可以帮助我们用更加结构化的方式去达成同样的目的,从而在维持性能的同时,保住我们代码的可拓展和可维护性。我们现在用 DOM Fragment 来改写上面的例子:</p><pre><code>let container = document.getElementById('container')
// 创建一个DOM Fragment 对象作为容器
let content = document.createDocumentFragment()
for(let count = 0;count<1000;count++){
// span此时可以通过DOM API去创建
let oSpan = document.createElement("span")
oSpan.innerHTML = "我是一个小测试"
// 像操作真实DOM一样操作DOM Fragment对象
content.appendChild(oSpan)
}
// 内容处理好了 最后再触发真实的DOM的更改
container.appendChild(content)</code></pre><p>DOM Fragment 对象允许我们像操作真实 DOM 一样去调用各种各样的 DOM API,我们的代码质量因此得到了保证。并且它的身份也非常纯粹:当我们试图将其 append 进真实 DOM 时,它会在乖乖交出自身缓存的所有后代节点后全身而退,完美地完成一个容器的使命,而不会出现在真实的 DOM 结构中。这种结构化、干净利落的特性,使得 DOM Fragment 作为经典的性能优化手段大受欢迎,这一点在 jQuery、Vue 等优秀前端框架的源码中均有体现。</p><h2>Event Loop 与异步更新策略</h2><h3>Event Loop 中的“渲染时机”</h3><h4>Micro-Task 与 Macro-Task</h4><p>事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。 <br>常见的 macro-task 比如: setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。 <br>常见的 micro-task 比如: process.nextTick、Promise、MutationObserver 等。</p><h3>Event Loop 过程解析</h3><p>一个完整的 Event Loop 过程,可以概括为以下阶段: <br>初始状态:调用栈空。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。<br>全局上下文(script 标签)被推入调用栈,同步代码执行。在执行的过程中,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。<br>上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的(如下图所示)。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。<br><img src="/img/remote/1460000038323080" alt="在这里插入图片描述" title="在这里插入图片描述"></p><ul><li>执行渲染操作,更新界面</li><li>检查是否存在 Web worker 任务,如果有,则对其进行处理 。</li></ul><p>(上述过程循环往复,直到两个队列都清空) </p><p>我们总结一下,每一次循环都是一个这样的过程: <br><img src="/img/remote/1460000038323078" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h3>渲染的时机</h3><p>假如我想要在异步任务里进行DOM更新,我该把它包装成 micro 还是 macro 呢? <br>我们先假设它是一个 macro 任务,比如我在 script 脚本中用 setTimeout 来处理它:</p><pre><code>// task是一个用于修改DOM的回调
setTimeout(task, 0)</code></pre><p>现在 task 被推入的 macro 队列。但因为 script 脚本本身是一个 macro 任务,所以本次执行完 script 脚本之后,下一个步骤就要去处理 micro 队列了,再往下就去执行了一次 render,对不对? <br>但本次render我的目标task其实并没有执行,想要修改的DOM也没有修改,因此这一次的render其实是一次无效的render。 </p><p>macro 不 ok ,我们转向 micro 试试看。我用 Promise 来把 task 包装成是一个 micro 任务:</p><pre><code>Promise.resolve().then(task)</code></pre><p>我们更新 DOM 的时间点,应该尽可能靠近渲染的时机。当我们需要在异步任务中实现 DOM 修改时,把它包装成 micro 任务是相对明智的选择。</p><h3>异步更新策略——以 Vue 为例</h3><p>什么是异步更新?</p><p>当我们使用 Vue 或 React 提供的接口去更新数据时,这个更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。<br>异步更新可以帮助我们避免过度渲染,是我们上节提到的“让 JS 为 DOM 分压”的典范之一。</p><h5>异步更新的优越性</h5><p>异步更新的特性在于它只看结果,因此渲染引擎不需要为过程买单。 </p><p>最典型的例子,比如有时我们会遇到这样的情况:</p><pre><code>// 任务一
this.content = '第一次测试'
// 任务二
this.content = '第二次测试'
// 任务三
this.content = '第三次测试'</code></pre><p>我们在三个更新任务中对同一个状态修改了三次,如果我们采取传统的同步更新策略,那么就要操作三次 DOM。但本质上需要呈现给用户的目标内容其实只是第三次的结果,也就是说只有第三次的操作是有意义的——我们白白浪费了两次计算。 </p><p>但如果我们把这三个任务塞进异步更新队列里,它们会先在 JS 的层面上被批量执行完毕。当流程走到渲染这一步时,它仅仅需要针对有意义的计算结果操作一次 DOM——这就是异步更新的妙处。</p><h3>Vue状态更新手法:nextTick</h3><p>Vue 每次想要更新一个状态的时候,会先把它这个更新操作给包装成一个异步操作派发出去。这件事情,在源码中是由一个叫做 nextTick 的函数来完成的:</p><pre><code>export function nextTick(cb?:Function, ctx?:Object){
let _resolve
callbacks.push(()=>{
if(cb){
try{
cb.call(ctx)
}catch(e){
handleError(e,ctx,'nextTick')
}
}else if(_resolve){
_resolve(ctx)
}
})
// 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了 pending此处相当于一个锁
if(!pending){
// 若上一个异步任务队列已经执行完毕 则将pending设为true(把锁锁上)
pending = true
// 是否要求一定要派发为macro任务
if(useMacroTask){
macroTimerFunc()
}else{
// 如果不说明一定要marco 你们就全都是micro
microTimerFunc()
}
}
// $flow-disable-line
if(!cb && typeof Promise !== 'undefined'){
return new Promise(resolve => {
_resolve = resolve
})
}
}</code></pre><p>Vue 的异步任务默认情况下都是用 Promise 来包装的,也就是是说它们都是 micro-task。这一点和我们“前置知识”中的渲染时机的分析不谋而合。<br>细化解析一下 macroTimeFunc() 和 microTimeFunc() 两个方法。</p><p>macroTimeFunc() 是这么实现的:</p><pre><code>// macro首选setImmediate 这个兼容性最差
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
// 兼容性最好的派发方式是setTimeout
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}</code></pre><p>microTimeFunc() 是这么实现的:</p><pre><code> // 简单粗暴 不是ios全都给我去Promise 如果不兼容promise 那么你只能将就一下变成marco了
if(typeof Promise !== 'undefined'&& isNative(Promise)){
const p = Promise.resolve()
microTimerFunc=()=>{
p.then(flushCallbacks)
if(isIOS)setTimeout(noop)
}
}else{
// 如果无法派发micro 就退而求次派发为macro
microTimerFunc = macroTimerFunc
}</code></pre><p>我们注意到,无论是派发 macro 任务还是派发 micro 任务,派发的任务对象都是一个叫做 flushCallbacks 的东西,这个东西做了什么呢? </p><p>flushCallbacks 源码如下:</p><pre><code>function flushCallbacks(){
pending = false
//callbacks在nextick中出现过 它是任务数组(队列)
const copies = callbacks.slice(0)
callbacks.length = 0
//将callback中的任务逐个取出执行
for(let i =0;i<copies.length;i++){
copies[i]()
}
}</code></pre><p>Vue 中每产生一个状态更新任务,它就会被塞进一个叫 callbacks 的数组(此处是任务队列的实现形式)中。这个任务队列在被丢进 micro 或 macro 队列之前,会先去检查当前是否有异步更新任务正在执行(即检查 pending 锁)。如果确认 pending 锁是开着的(false),就把它设置为锁上(true),然后对当前 callbacks 数组的任务进行派发(丢进 micro 或 macro 队列)和执行。设置 pending 锁的意义在于保证状态更新任务的有序进行,避免发生混乱</p><h2>回流(Reflow)与重绘(Repaint)</h2><p>回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。</p><p>重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。</p><p>重绘不一定导致回流,回流一定会导致重绘。硬要比较的话,回流比重绘做的事情更多,带来的开销也更大</p><h3>回流的“导火索”</h3><p><strong>最“贵”的操作:改变 DOM 元素的几何属性</strong></p><p>这个改变几乎可以说是“牵一发动全身”——当一个DOM元素的几何属性发生变化时,所有和它相关的节点(比如父子节点、兄弟节点等)的几何属性都需要进行重新计算,它会带来巨大的计算量。<br>常见的几何属性有 width、height、padding、margin、left、top、border 等等<br><strong>“价格适中”的操作:改变 DOM 树的结构</strong> <br>这里主要指的是节点的增减、移动等操作。浏览器引擎布局的过程,顺序上可以类比于树的前序遍历——它是一个从上到下、从左到右的过程。通常在这个过程中,当前元素不会再影响其前面已经遍历过的元素。</p><p><strong>最容易被忽略的操作:获取一些特定属性的值</strong> <br>当你要用到像这样的属性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 时,你就要注意了!<br>“像这样”的属性,到底是像什么样?——这些值有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流。 <br>除此之外,当我们调用了 getComputedStyle 方法,或者 IE 里的 currentStyle 时,也会触发回流。原理是一样的,都为求一个“即时性”和“准确性”。</p><h3>如何规避回流与重绘</h3><h6>将“导火索”缓存起来,避免频繁改动</h6><p>有时我们想要通过多次计算得到一个元素的布局位置,我们可能会这样做:</p><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
#el{
width:100%;
height:100%;
background-color: yellow;
position:absolute;
}
</style>
<body>
<div id="el"></div>
<script>
// 获取el元素
const el = document.getElementById('el')
// 这里循环判定比较简单实际中获取会拓展出比较复杂的判定需求
for(let i =0;i<10;i++){
el.style.top = el.offsetHeight + 10 + 'px'
el.style.left = el.offsetLeft + 10 + 'px'
}
</script>
</body>
</html></code></pre><p>这样做,每次循环都需要获取多次“敏感属性”,是比较糟糕的。我们可以将其以 JS 变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求:</p><pre><code>// 缓存offsetLeft与offsetTop的值
const el = document.getElementById('el')
let offLeft = el.offsetLeft,offTop = el.offsetTop
// 在js层面进行计算
for(let i =0;i<10;i++){
offsetLeft += 10
offsetTop += 10
}
// 一次性将计算结果应用到DOM上
el.style.left = offLeft + 'px'
el.style.top = offTop + 'px'</code></pre><h4>避免逐条改变样式,使用类名去合并样式</h4><p>比如我们可以把这段单纯的代码</p><pre><code>const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'</code></pre><p>优化成一个有 class 加持的样子:</p><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.basic_style {
width: 100px;
height: 200px;
border: 10px solid red;
color:red;
}
</style>
<body>
<div id="container"></div>
<script>
let container = document.getElementById('container')
container.classList.add('basic_style')
</script>
</body>
</html></code></pre><p>前者每次单独操作,都去触发一次渲染树更改,从而导致相应的回流与重绘过程。</p><p>合并之后,等于我们将所有的更改一次性发出,用一个 style 请求解决掉了。</p><h2>将 DOM “离线”</h2><p>所说的回流和重绘,都是在“该元素位于页面上”的前提下会发生的。一旦我们给元素设置 display: none,将其从页面上“拿掉”,那么我们的后续操作,将无法触发回流与重绘——这个将元素“拿掉”的操作,就叫做 DOM 离线化。</p><p>仍以这段代码片段为例:</p><pre><code>const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'</code></pre><p>离线化后就是这样:</p><pre><code>const container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
.....
container.style.display = 'block'</code></pre><p>把它拿下来了,后续不管我操作这个元素多少次,每一步的操作成本都会非常低。当我们只需要进行很少的 DOM 操作时,DOM 离线化的优越性确实不太明显。一旦操作频繁起来,这“拿掉”和“放回”的开销都将会是非常值得的。</p><h2>Flush 队列:浏览器并没有那么简单</h2><pre><code>let container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'</code></pre><p>这段代码里,浏览器进行了多少次的回流或重绘呢<br>“width、height、border是几何属性,各触发一次回流;color只造成外观的变化,会触发一次重绘。”<br> 因为现代浏览器是很聪明的。浏览器自己也清楚,如果每次 DOM 操作都即时地反馈一次回流或重绘,那么性能上来说是扛不住的。于是它自己缓存了一个 flush 队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。因此我们看到,上面就算我们进行了 4 次 DOM 更改,也只触发了一次 Layout 和一次 Paint。</p><p>提到过有一类属性很特别,它们有很强的“即时性”。当我们访问这些属性时,浏览器会为了获得此时此刻的、最准确的属性值,而提前将 flush 队列的任务出队——这就是所谓的“不得已”时刻。 </p><p>并不是所有的浏览器都是聪明的。Chrome 里行得通的东西,到了别处(比如 IE)就不一定行得通了。而我们并不知道用户会使用什么样的浏览器。如果不手动做优化,那么一个页面在不同的环境下就会呈现不同的性能效果,这对我们、对用户都是不利的。因此,养成良好的编码习惯、从根源上解决问题,仍然是最周全的方法。</p><h2>优化首屏体验——Lazy-Load</h2><p>Lazy-Load,翻译过来是“懒加载”。它是针对图片加载时机的优化:在一些图片量比较大的网站,<br>,如果我们尝试在用户打开页面的时候,就把所有的图片资源加载完毕,那么很可能会造成白屏、卡顿等现象,因为图片真的太多了,一口气处理这么多任务,浏览器做不到啊!</p><p>只要我们可以在页面打开的时候把首屏的图片资源加载出来,用户就会认为页面是没问题的。至于下面的图片,我们完全可以等用户下拉的瞬间再即时去请求、即时呈现给他。这样一来,性能的压力小了,用户的体验却没有变差——这个延迟加载的过程,就是 Lazy-Load。</p><h3>写一个 Lazy-Load</h3><p>Lazy-Load 的思路及实现方式为大厂面试常考题<br>我们在 index.html 中,为这些图片预置 img 标签:</p><pre><code><!--
* @Author: yang
* @Date: 2020-11-29 15:02:59
* @LastEditors: yang
* @LastEditTime: 2020-11-29 15:28:43
* @FilePath: \gloud-h5-demo\src\component\index\index.html
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.img{
width: 200px;
height: 200px;
background-color: gray;
}
</style>
</head>
<body>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/1.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/2.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/3.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/4.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/5.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/6.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/7.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/8.png">
</div>
<div class="img">
<img class="pic" alt="加载中" data-src="./images/9.png">
</div>
</body>
</html></code></pre><p>在懒加载的实现中,有两个关键的数值:一个是当前可视区域的高度,另一个是元素距离可视区域顶部的高度。 <br>当前可视区域的高度, 在和现代浏览器及 IE9 以上的浏览器中,可以用 window.innerHeight 属性获取。在低版本 IE 的标准模式中,可以用 document.documentElement.clientHeight 获取,这里我们兼容两种情况:</p><pre><code>const viewHeight = window.innerHeight||document.documentElement.clientHeight </code></pre><p>而元素距离可视区域顶部的高度,我们这里选用 getBoundingClientRect() 方法来获取返回元素的大小及其相对于视口的位置<br><em>(DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right 和 bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。)</em></p><p>可以看出,top 属性代表了元素距离可视区域顶部的高度,正好可以为我们所用</p><p>Lazy-Load 方法开工</p><pre><code> // 获取所有的图片标签
const imgs = document.getElementsByTagName('img')
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight
// num用于统计当前显示到了哪一张图片 避免每次都从第一张图片开始检查是否漏出
let num = 0
function lazyload(){
for(let i = num;i<imgs.length;i++){
// 用可视区域高度减去元素顶部据可视区域顶部的高度
let distance = viewHeight - imgs[i].getBoundingClientRect().top
// 如果可视区域高度大于元素顶部距离可视区域顶部的高度,说明元素露出
if(distance>=0){
//给元素写入真实的src 展示图片
imgs[i].src = imgs[i].getAttribute('data-src')
// 前i张图片已经加载完毕 下次从i+1张开始检查是否露出
num = i+1
}
}
}
// 监听Scroll事件
window.addEventListener('scroll',lazyload,false)</code></pre><p>这个 scroll 事件,是一个危险的事件——它太容易被触发了。试想,用户在访问网页的时候,是不是可以无限次地去触发滚动?尤其是一个页面死活加载不出来的时候,疯狂调戏鼠标滚轮(或者浏览器滚动条)的用户可不在少数啊! </p><p>再回头看看我们上面写的代码。按照我们的逻辑,用户的每一次滚动都将触发我们的监听函数。函数执行是吃性能的,频繁地响应某个事件将造成大量不必要的页面计算。因此,我们需要针对那些有可能被频繁触发的事件作进一步地优化。</p><h2>事件的节流(throttle)与防抖(debounce)</h2><p>scroll 事件是一个非常容易被反复触发的事件。其实不止 scroll 事件,resize 事件、鼠标事件(比如 mousemove、mouseover 等)、键盘事件(keyup、keydown 等)都存在被频繁触发的风险。</p><p>频繁触发回调导致的大量计算会引发页面的抖动甚至卡顿。为了规避这种情况,我们需要一些手段来控制事件被触发的频率。就是在这样的背景下,throttle(事件节流)和 debounce(事件防抖)出现了。</p><h3>“节流”与“防抖”的本质</h3><p>这两个东西都以闭包的形式存在。 </p><p>它们通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。</p><h3>Throttle: 第一个人说了算</h3><p>throttle 的中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。<br>所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。<br>每当用户触发了一次 scroll 事件,我们就为这个触发操作开启计时器。一段时间内,后续所有的 scroll 事件都会被当作“一辆车的乘客”——它们无法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。</p><pre><code> // fn是我们需要包装的事件回调 interval是时间间隔的阈值
function throttle(fn,interval){
// last为上一次触发回调的时间、
let last = 0
// 将throwttle处理结果当做函数返回
return function(){
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
//记录本次触发回调的时间
let now = + new Date()
//判断上次触发的时间和本次触发的时间差是否小于时间间隔的阀值
if(now - last >= interval){
// 如果时间间隔大于我们设定的时间间隔阀值 则执行回调
last = now
fn.apply(context,args)
}
}
}
// 用throwttle来包装scroll的回调
document.addEventListener('scroll',throttle(()=>console.log('触发了滚动事件'),1000))</code></pre><h3>Debounce: 最后一个人说了算</h3><p>防抖的中心思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。</p><pre><code> // fn是需要包装的事件回调 delay是每次推迟执行的等待时间
function debounce(fn,delay){
//定时器
let timer = null
// 将debounce处理结果当做函数返回
return function(){
//保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
//每次事件被触发时 都去清除之前的旧定时器
if(timer){
clearTimeout(timer)
}
// 设立新定时器
timer = setTimeout(function(){
fn.apply(context,args)
},delay)
}
}
document.addEventListener('scroll',debounce(()=>console.log('触发了滚动事件'),1000))</code></pre><h3>用 Throttle 来优化 Debounce</h3><p>debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。 </p><p>为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中:</p><pre><code> // fn是我们需要包装的事件回调, delay是时间间隔的阀值
function throttle(fn,delay){
// last 为上次触发回调的事件 timer是定时器
let last = 0,timer = null;
// 将throttle处理结果当做函数返回
return function(){
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = +new Date()
// 判断上次触发时间和本地触发的时间差是否小于时间间隔的阀值
if(now-last>delay){
// 如果时间间隔小于我们设定的时间间隔阀值 则为本次触发操作设立一个新的定时器
clearTimeout(timer)
timer = setTimeout(function(){
last = now
fn.apply(contxt,args)
},delay)
}else{
// 如果时间间隔超出了我们设定的时间间隔阀值 那就不等了 无论如何要反馈给用户一次响应
last = now
fn.apply(context,args)
}
}
}
// 用新的throttle包装scroll的回调
document.addEventListener('scroll',throttle(()=>console.log('触发了滚动事件'),1000))</code></pre><h2>性能监测</h2><h3>可视化监测:从 Performance 面板说起</h3><p>Performance是Chrome<br>提供给我们的开发者工具,用于记录和分析我们的应用在运行时的所有活动。它呈现的数据具有实时性、多维度的特点,可以帮助我们很好地定位性能问题。 <br><strong>开始记录</strong><br>右键打开开发者工具,选中我们的<br>Performance<br>面板: <br><img src="/img/remote/1460000038323082" alt="在这里插入图片描述" title="在这里插入图片描述"><br>当我们选中图中所标示的实心圆按钮,Performance</p><p>会开始帮我们记录我们后续的交互操作;当我们选中圆箭头按钮,Performance<br>会将页面重新加载,计算加载过程中的性能表现。 <br>tips:使用<br>Performance<br>工具时,为了规避其它<br>Chrome<br>插件对页面的性能影响,我们最好在无痕模式下打开页面</p><h3>挖掘性能瓶颈</h3><p>看 Main 栏目下的火焰图和 Summary 提供给我们的饼图——这两者和概述面板中的 CPU 一栏结合,可以帮我们迅速定位性能瓶颈<br><img src="/img/remote/1460000038323088" alt="在这里插入图片描述" title="在这里插入图片描述"><br>从上到下,依次为概述面板、详情面板<br>观察一下概述面板<br>FPS:这是一个和动画性能密切相关的指标,它表示每一秒的帧数。图中绿色柱状越高表示帧率越高,体验就越流畅。若出现红色块,则代表长时间帧,很可能会出现卡顿。图中以绿色为主,偶尔出现红块,说明网页性能并不糟糕,但仍有可优化的空间。</p><p>CPU:表示CPU的使用情况,不同的颜色片段代表着消耗CPU资源的不同事件类型。这部分的图像和下文详情面板中的Summary内容有对应关系,我们可以结合这两者挖掘性能瓶颈。</p><p>NET:粗略的展示了各请求的耗时与前后顺序。这个指标一般来说帮助不大。<br>先看 CPU 图表和 Summary 饼图。CPU 图表中,我们可以根据颜色填充的饱满程度,确定 CPU 的忙闲,进而了解该页面的总的任务量。而 Summary 饼图则以一种直观的方式告诉了我们,哪个类型的任务最耗时(从本例来看是脚本执行过程)。这样我们在优化的时候,就可以抓到“主要矛盾”,进而有的放矢地开展后续的工作了。 </p><p>再看 Main 提供给我们的火焰图。这个火焰图非常关键,它展示了整个运行时主进程所做的每一件事情(包括加载、脚本运行、渲染、布局、绘制等)。x 轴表示随时间的记录。每个长条就代表一个活动。更宽的条形意味着事件需要更长时间。y 轴表示调用堆栈,我们可以看到事件是相互堆叠的,上层的事件触发了下层的事件。 </p><p>CPU 图标和 Summary 图都是按照“类型”给我们提供性能信息,而 Main 火焰图则将粒度细化到了每一个函数的调用。到底是从哪个过程开始出问题、是哪个函数拖了后腿、又是哪个事件触发了这个函数,这些具体的、细致的问题都将在 Main 火焰图中得到解答。</p><h3>可视化监测: 更加聪明的 LightHouse</h3><p>Performance 无疑可以为我们提供很多有价值的信息,但它的展示作用大于分析作用。它要求使用者对工具本身及其所展示的信息有充分的理解,能够将晦涩的数据“翻译”成具体的性能问题。 </p><p>程序员们许了个愿:如果工具能帮助我们把页面的问题也分析出来就好了!上帝听到了这个愿望,于是给了我们 LightHouse:<br>Lighthouse 是一个开源的自动化工具,用于改进网络应用的质量。 你可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。 为Lighthouse 提供一个需要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。<br>首先在 Chrome 的应用商店里下载一个 LightHouse。这一步 OK 之后,我们浏览器右上角会出现一个小小的灯塔 ICON。打开我们需要测试的那个页面,点击这个 ICON,唤起如下的面板:<br><img src="/img/remote/1460000038323083" alt="在这里插入图片描述" title="在这里插入图片描述"><br>然后点击“Generate report”按钮,只需静候数秒,LightHouse 就会为我们输出一个完美的性能报告。 </p><p>这里我拿掘金小册首页“开刀”: <br>稍事片刻,Report 便输出成功了,LightHouse 默认会帮我们打开一个新的标签页来展示报告内容。报告内容非常丰富,首先我们看到的是整体的跑分情况:<br><img src="/img/remote/1460000038323084" alt="在这里插入图片描述" title="在这里插入图片描述"><br>上述分别是页面性能、PWA(渐进式 Web 应用)、可访问性(无障碍)、最佳实践、SEO 五项指标的跑分。孰强孰弱,我们一看便知。</p><p>向下拉动 Report 页,我们还可以看到每一个指标的细化评估: <br><img src="/img/remote/1460000038323086" alt="在这里插入图片描述" title="在这里插入图片描述"><br>在“Opportunities”中,LightHouse 甚至针对我们的性能问题给出了可行的建议、以及每一项优化操作预期会帮我们节省的时间。这份报告的可操作性是很强的——我们只需要对着 LightHouse 给出的建议,一条一条地去尝试,就可以看到自己的页面,在一秒一秒地变快。</p><p><strong>除了直接下载,我们还可以通过命令行使用</strong> LightHouse:</p><pre><code> npm install -g lighthouse
lighthouse https://juejin.im/books</code></pre><p>同样可以得到掘金小册的性能报告。</p><p>此外,从 Chrome 60 开始,DevTools 中直接加入了基于 LightHouse 的 Audits 面板:<br><img src="/img/remote/1460000038323085" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h3>可编程的性能上报方案: W3C 性能 API Performance</h3><p>W3C 规范为我们提供了 Performance 相关的接口。它允许我们获取到用户访问一个页面的每个阶段的精确时间,从而对性能进行分析。我们可以将其理解为 Performance 面板的进一步细化与可编程化。 </p><p>当下的前端世界里,数据可视化的概念已经被炒得非常热了,Performance 面板就是数据可视化的典范。那么为什么要把已经可视化的数据再掏出来处理一遍呢?这是因为,需要这些数据的人不止我们前端——很多情况下,后端也需要我们提供性能信息的上报。此外,Performance 提供的可视化结果并不一定能够满足我们实际的业务需求,只有拿到了真实的数据,我们才可以对它进行二次处理,去做一个更加深层次的可视化。 </p><p>在这种需求背景下,我们就不得不祭出 Performance API了。<br>访问 performance 对象</p><p>performance 是一个全局对象。我们在控制台里输入 window.performance,就可一窥其全貌: </p><p><img src="/img/remote/1460000038323090" alt="在这里插入图片描述" title="在这里插入图片描述"><br>关键时间节点</p><p>在 performance 的 timing 属性中,我们可以查看到如下的时间戳:<br><img src="/img/remote/1460000038323087" alt="在这里插入图片描述" title="在这里插入图片描述"><br>这些时间戳与页面整个加载流程中的关键时间节点有着一一对应的关系<br><img src="/img/remote/1460000038323089" alt="在这里插入图片描述" title="在这里插入图片描述"><br>通过求两个时间点之间的差值,我们可以得出某个过程花费的时间,举个🌰:</p><pre><code>const timing = window.performance.timing
// DNS查询耗时
timing.domainLookupEnd - timing.domainLookupStart
// TCP连接耗时
timing.connectEnd - timing.connectStart
// 内容加载耗时
timing.responseEnd - timing.requestStart
···</code></pre><p>除了这些常见的耗时情况,我们更应该去关注一些关键性能指标:firstbyte、fpt、tti、ready 和 load 时间。这些指标数据与真实的用户体验息息相关,是我们日常业务性能监测中不可或缺的一部分:</p><pre><code>// firstbyte:首包时间
timing.responseStart – timing.domainLookupStart
// fpt:First Paint Time, 首次渲染时间 / 白屏时间
timing.responseEnd – timing.fetchStart
// tti:Time to Interact,首次可交互时间
timing.domInteractive – timing.fetchStart
// ready:HTML 加载完成时间,即 DOM 就位的时间
timing.domContentLoaded – timing.fetchStart
// load:页面完全加载时间
timing.loadEventStart – timing.fetchStart</code></pre><p>以上这些通过 Performance API 获取到的时间信息都具有较高的准确度。我们可以对此进行一番格式处理之后上报给服务端,也可以基于此去制作相应的统计图表,从而实现更加精准、更加个性化的性能耗时统计。 </p><p>此外,通过访问 performance 的 memory 属性,我们还可以获取到内存占用相关的数据;通过对 performance 的其它属性方法的灵活运用,我们还可以把它耦合进业务里,实现更加多样化的性能监测需求——灵活,是可编程化方案最大的优点。</p>
canvas笔记
https://segmentfault.com/a/1190000038187527
2020-11-16T17:50:50+08:00
2020-11-16T17:50:50+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p><a href="https://link.segmentfault.com/?enc=tcnOiycqJuSRB8rlEZEaKQ%3D%3D.YZpVHBwSS0dMGAVb7jECedmGDZDh6jNG0xrKj9c00KEBqyg2Iuwhy5aKwzPRhyur" rel="nofollow">canvas代码</a></p><h4><strong>基本使用</strong></h4><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
#canvas {
background: #000;
}
</style>
</head>
<body>
<canvas id="canvas" width="400" height="400">
</canvas>
<script>
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.beginPath();
context.arc(100, 100, 50, 0, Math.PI * 2, true);
context.closePath();
context.fillStyle = 'rgb(255,255,255)';
context.fill();
</script>
</body>
</html></code></pre><h6>也可使用js设置canvas的宽高,以及所代表的的意思</h6><pre><code><template>
<canvas id="canvas" ref="canvas"></canvas>
</template>
<script>
export default {
methods: {
init() {
let canvas = this.$refs.canvas
let context = canvas.getContext('2d')//获取到 Canvas 的上下文环境 代表一个二维渲染上下文
let cx = canvas.width = 400
let cy = canvas.height = 400
context.beginPath() // 起始一条路径,或重置当前路径
context.arc(100,100,50,0,Math.PI*2,true)// 创建弧/曲线
context.closePath()// 创建从当前点回到起始点的路径
context.fillStyle = 'rgb(255,255,255)' // 设置或返回用于填充绘画的颜色、渐变或模式
context.fill()// 填充当前绘图(路径)
}
},
mounted () {
this.init()
},
}
</script>
<style lang="scss" scoped>
#canvas{
background-color: #000;
}
</style></code></pre><p><strong>注意</strong><br>*不要使用 CSS 设置。因为默认创建一个 300 150 的画布,<br>如果使用 CSS 来设置宽高的话,画布就会按照 <code>300 150</code> 的比例进行缩放,也就是将 <code>300 150</code> 的页面显示在 <code>400 400</code> 的容器中*</p><h4>绘制路径</h4><p><img src="/img/bVcKn8Q" alt="image.png" title="image.png"></p><p>使用 Canvas 绘制图像的步骤<br><img src="/img/bVcKn9N" alt="image.png" title="image.png"></p><h4>绘制弧/曲线</h4><p><code>arc()</code> 方法创建弧/曲线(用于创建圆或部分圆)。</p><p>context.arc(x,y,r,sAngle,eAngle,counterclockwise);</p><ul><li>x:圆心的 x 坐标</li><li>y:圆心的 y 坐标</li><li>r:圆的半径</li><li>sAngle:起始角,以弧度计(弧的圆形的三点钟位置是 0 度)</li><li>eAngle:结束角,以弧度计</li><li>counterclockwise:可选。规定应该逆时针还是顺时针绘图。false 为顺时针,true 为逆时针</li></ul><p><img src="/img/bVcKoaq" alt="image.png" title="image.png"></p><p>画一个顺时针的四分之一圆</p><pre><code>let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
context.beginPath()
context.arc(100,100,50,0,Math.PI*0.5,false)
context.strokeStyle = "white"
context.stroke()</code></pre><p>因为我们设置的起始角是 0,对照 w3cschool 上的截图可知弧度的 0 的位置是 3 点钟方向,然后结束角我们设置为 0.5 PI,也就是 6 点钟方向</p><p>stroke()和fill()的区别</p><ul><li><code>stroke()</code> :描边</li><li><code>fill()</code> :填充</li></ul><p>我们可以通过 <code>strokeStyle</code>属性 和 <code>fillStyle</code>属性来设置描边和填充的颜色</p><h4>绘制直线</h4><pre><code>let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
context.beginPath()
context.moveTo(50,50)
context.lineTo(100,100)
context.strokeStyle = "white"
context.stroke()</code></pre><ul><li><code>moveTo(x,y)</code>:把路径移动到画布中的指定点,不创建线条</li><li><code>lineTo(x,y)</code>:添加一个新点,然后在画布中创建从该点到最后指定点的线条</li></ul><p>这里需要注意以下几点:</p><ul><li>如果没有 moveTo,那么第一次 lineTo 的就视为 moveTo</li><li>每次 lineTo 后如果没有 moveTo,那么下次 lineTo 的开始点为前一次 lineTo 的结束点。</li></ul><p>也就是这种情况:</p><pre><code> let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
context.beginPath()
context.lineTo(100,100)
context.lineTo(50,100)
context.lineTo(200,200)
context.lineTo(20,10)
context.strokeStyle = "white"
context.stroke()</code></pre><p>我们没有设置 moveTo,而是设置了三个 lineTo,这也是可以的,将三个 lineTo 设置的点依次连接就好~</p><h5>给绘制的直线添加样式</h5><p><img src="/img/bVcKofl" alt="image.png" title="image.png"><br>其宽度设置为 10,并且加上“圆角”的效果</p><pre><code>let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
context.beginPath()
context.moveTo(50,100)
context.lineTo(200,200)
context.lineWidth = 10
context.lineCap = 'round'
context.strokeStyle = "white"
context.stroke()</code></pre><p><img src="/img/bVcKoi3" alt="image.png" title="image.png"></p><h4>绘制矩形</h4><pre><code> let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
context.beginPath()
context.fillStyle = '#fff'
context.fillRect(10,10,100,100)
context.strokeStyle = '#fff'
context.strokeRect(130,10,100,100)</code></pre><p><img src="/img/bVcKokG" alt="image.png" title="image.png"></p><ul><li><code>fillRect(x,y,width,height)</code>:绘制一个实心矩形</li><li><code>strokeRect(x,y,width,height)</code>:绘制一个空心矩形</li></ul><h4>颜色、样式和阴影</h4><p><img src="/img/bVcKolD" alt="image.png" title="image.png"></p><pre><code> let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
context.beginPath()
context.arc(100,100,50,0,2*Math.PI,false);
context.fillStyle = '#fff';
context.shadowBlur = 20
context.shadowColor = "#fff"
context.fill()</code></pre><p><img src="/img/bVcKomS" alt="image.png" title="image.png"></p><h4>设置渐变</h4><p><img src="/img/bVcKona" alt="image.png" title="image.png"><br>绘制渐变主要用到了 <code>createLinearGradient()</code> 方法,我们来看一下这个方法:<code>context.createLinearGradient(x0,y0,x1,y1);</code></p><ul><li>x0:开始渐变的 x 坐标</li><li>y0:开始渐变的 y 坐标</li><li>x1:结束渐变的 x 坐标</li><li>y1:结束渐变的 y 坐标</li></ul><pre><code>let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
var grd = context.createLinearGradient(100,100,100,200)
grd.addColorStop(0,'pink');
grd.addColorStop(1,'lightBlue')
context.fillStyle = grd;
context.fillRect(100,100,200,200)</code></pre><p><img src="/img/bVcKooS" alt="image.png" title="image.png"><br><code>createLinearGradient()</code> 的参数是两个点的坐标,这两个点的连线实际上就是渐变的方向。我们可以使用 <code>addColorStop()</code> 方法来设置渐变的颜色。</p><p><code>gradient.addColorStop(stop,color);</code>:</p><ul><li><code>stop</code>:介于 0.0 与 1.0 之间的值,表示渐变中开始与结束之间的位置</li><li><code>color</code>:在结束位置显示的 CSS 颜色值</li></ul><pre><code> let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
var grd = context.createLinearGradient(0,0,0,400)
grd.addColorStop(0,'pink');
grd.addColorStop(0.2,'lightBlue')
grd.addColorStop(0.4,'red')
grd.addColorStop(0.6,'pink')
grd.addColorStop(0.8,'black')
grd.addColorStop(1,'yellow')
context.fillStyle = grd;
context.fillRect(0,0,400,400)</code></pre><p><img src="/img/bVcKoqS" alt="image.png" title="image.png"></p><h4>图形转换</h4><p><img src="/img/bVcKoq5" alt="image.png" title="image.png"></p><pre><code>let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
context.strokeStyle = 'white'
context.strokeRect(5,5,50,25)
context.scale(2,2)
context.strokeRect(5,5,50,25)
context.scale(2,2)
context.strokeRect(5,5,50,25)</code></pre><p><img src="/img/bVcKorO" alt="image.png" title="image.png"></p><h4>旋转</h4><pre><code>let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
let cx = canvas.width = 400
let cy = canvas.height = 400
context.fillStyle = 'white'
context.rotate(20*Math.PI/180)
context.fillRect(70,30,200,100)</code></pre><p><img src="/img/bVcKotp" alt="image.png" title="image.png"></p><p><code>context.rotate(angle);</code></p><ul><li><code>angle</code> : 旋转角度,以弧度计。如需将角度转换为弧度,请使用 <code>degrees*Math.PI/180</code> 公式进行计算。举例:如需旋转 5 度,可规定下面的公式:<code>5*Math.PI/180</code>。</li><li>、我们将画布旋转了 20°,然后再画了一个矩形。</li></ul><p>在进行图形变换的时候,我们需要画布旋转,然后再绘制图形,<br>使用的图形变换的方法都是作用在画布上的,既然对画布进行了变换,那么在接下来绘制的图形都会变换。<br>比如我对画布使用了 <code>rotate(20*Math.PI/180)</code> 方法,就是将画布旋转了 20°,然后之后绘制的图形都会旋转 20°。</p><h4>图像绘制</h4><p>Canvas 还有一个经常用的方法是<code>drawImage()</code>。<br><img src="/img/bVcKotO" alt="image.png" title="image.png"><br><code>context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);</code></p><ul><li><code>img</code>:规定要使用的图像、画布或视频</li><li><code>sx</code>:可选。开始剪切的 x 坐标位置</li><li><code>sy</code>:可选。开始剪切的 y 坐标位置</li><li><code>swidth</code>:可选。被剪切图像的宽度</li><li><code>sheight</code>:可选。被剪切图像的高度</li><li><code>x</code>:在画布上放置图像的 x 坐标位置</li><li><code>y</code>:在画布上放置图像的 y 坐标位置</li><li><code>width</code>:可选。要使用的图像的宽度(伸展或缩小图像)</li><li><code>height</code>:可选。要使用的图像的高度(伸展或缩小图像)</li></ul><h4>炫酷背景特效的通性</h4><ul><li><p>背景</p><ul><li>单一颜色</li><li>渐变</li><li>平铺</li></ul></li><li><p>炫酷</p><ul><li>动</li><li>随机</li></ul></li><li><p>特效(与用户交互)</p><ul><li>鼠标跟随</li><li>视觉差</li></ul></li></ul><h6>背景</h6><p>景往往是纯色的或者是渐变的,再或者就是有规律的可以平铺的图形。<br>封面图一般都是一个渐变的背景 + 文字。简洁却不简单<br>过渡色获取:<a href="https://link.segmentfault.com/?enc=SCO0cTKYT%2Bm7ebf48qFmKw%3D%3D.K11KUHTMbI%2FFISyt1iB4lQDznkfkZ6%2FrHv7AcSI9qKzyUWysa01fUMLNMI0jRKWK" rel="nofollow">uigradients</a>。<br>这个网站可以自己生成渐变色,你的配色也可以跟大家分享,可以保存为图片,也可以导出为 CSS 样式。</p><p>我们可以从这个网站上找到喜欢的配色,然后导出为 CSS 样式使用。<br><img src="/img/bVcKrV0" alt="image.png" title="image.png"><br>星空背景的渐变实际上不是使用 Canvas 写的,只是使用 CSS 写出的效果。实现的方式是:</p><p>下面的树是一个 png 的背景<br><img src="/img/bVcKrXH" alt="image.png" title="image.png"><br>然后我们将 <code>body</code> 的颜色设置为黑色到蓝色的由上向下的渐变:</p><pre><code>background:linear-gradient(to bottom,#000000 0%,#5788fe 100%)</code></pre><p>接下来我们要设置一个全屏的遮罩,将这个遮罩的背景色设置为红色,然后使用 CSS3 的 <code>animation</code> 属性,使用 <code>animation</code> 改变其透明度,由 0 变为 0.9。</p><pre><code>.filter{
width:100%;
height:100%;
position:absolute;
top:0;
left:0;
background:#fe5757;
animation:colorChange 30s ease-in-out infinite;
animation-fill-mode:both;
mix-blend-mode:overlay;
}
@keyframs colorChange{
0%,100%{
opacity:0;
}
50%{
opacity:.9;
}
}</code></pre><p>效果就和上面动态的效果一样。</p><h6>炫酷</h6><ul><li>动</li><li>随机</li></ul><p><strong>让元素动起来</strong></p><pre><code>* gif 图
* CSS3 动画
* js 控制
* svg
* Canvas</code></pre><p><strong>随机</strong><br>使用 gif 图大家都知道,只能是有规律的“动”,并且 gif 图片的尺寸不宜过大,在我们的网页背景中,基本上是不会用到的。</p><p>CSS3 实现的动画效果,也是只能做有规律的“动”,并且 CSS 只能操纵单个的 DOM 元素,一旦元素到达一定的数量,代价是比较大的。</p><p>所以我们选择 js + Canvas 来实现“随机”的“动”。</p><h5>效果</h5><p>主要是与鼠标之间的交互效果。</p><p>与鼠标之间有互动的效果主要是产生用户行为的反馈,比如在网页制作中,我们经常使用 hover 变色表示用户的鼠标在元素上方悬停。这就是用户行为的一种反馈。</p><p>我们经常使用的与鼠标之间的交互效果主要有两种:</p><ul><li>鼠标跟随</li><li>视觉差<br>用户很喜欢这种鼠标跟随的效果,个人觉得就是因为它使得网站的显示效果和用户的行为产生了联系,使用户的行为得到了反馈。</li></ul><p>还有一种经常见到的效果是数据差的效果,比如:</p><p><img src="/img/remote/1460000038201105" alt="视觉差效果" title="视觉差效果"></p><p>这是锤子官网的一个特效,鼠标移动到哪哪就会下沉,并且如果你仔细看的话就会发现,上面的月份数字和底部的图片不是在一个层级上的,更加有立体的感觉,这就是视觉差的特效。</p><p>这种特效不需要用 Canvas,只需要 CSS 就可以实现</p><h3>怎么实现随机粒子</h3><p>如果只是一个纯色或者渐变的背景,肯定会显得有点单调,我们还需要在渐变的基础上加一点 “料”,而这些 “料”通常都是粒子特效。 那么“粒子特效” 都有什么特点呢?</p><ul><li>粒子</li><li>规则图形</li><li>随机</li><li>数量多</li></ul><p>将无数的单个粒子组合使其呈现出固定形态,借由控制器,脚本来控制其整体或单个的运动,模拟出现真实的效果。</p><p>粒子特效的首要特点是数量多,在物理学中,粒子是能够以自由状态存在的最小物质组成部分,所以粒子的第一个特点就是数量多。</p><p>粒子特效的第二个特点是运动,正是因为组成物体的粒子在不断运动,我们才能看到物体在不断运动。</p><p>粒子特效第三个特点是随机,排列有整齐排列之美,凌乱有凌乱之美,整齐排列的可以直接平铺背景实现,直接使用 img 图片就可以。</p><p>但是要想有随机效果使用 img 图片就不可以了,所以我们主要使用 Canvas 实现随机粒子效果。各项参数都是随机生成的。<br>现在我们来一起实现一个随机粒子特效。</p><p>效果如下:</p><p><img src="/img/bVcKYEe" alt="image.png" title="image.png"></p><h4>创建全屏 Canvas</h4><p>首先,我们需要一个全屏的 Canvas 画布</p><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
html,
body {
margin: 0;
overflow: hidden;
width: 100%;
height: 100%;
background-color: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
var ctx = document.getElementById('canvas'),
content = ctx.getContext('2d'),
WIDTH, HEIGHT;
WIDTH = document.documentElement.clientWidth;
HEIGHT = document.documentElement.clientHeight;
initRoundPopulation = 80,
WIDTH, HEIGHT;
</script>
</html></code></pre><p>我们使用 <code>WIDTH</code>、<code>HEIGHT</code> 两个常量储存屏幕宽度和高度信息,我们习惯使用大写来表示改变量为常量,不可变,将屏幕宽度和高度信息储存在常量中是因为我们在稍后还会用到。</p><p>这时,你应该得到一个全屏的并且为黑色的 Canvas。</p><h4>设置 <code>Round_item</code> 类</h4><p>创建单个的 <code>Round_item</code> 类。<br>要设置的是位置随机、透明度随机、半径随机的圆。为了区分不同的圆,我们还应该设置一个唯一的 <code>index</code> 参数。</p><p>所以我们需要的参数有:</p><ul><li>x 坐标</li><li>y 坐标</li><li>半径</li><li>透明度</li><li>index</li></ul><p>根据上面这些可以得出我们的 <code>Round_item</code> 类:</p><pre><code> function Round_item(index,x,y){
this.index = index
this.x = x;
this.y = y;
this.r = Math.random()*2+1; //随机半径
var alpha = (Math.floor(Math.random()*10)+1)/10/2
this.color = "rgba(255,255,255,"+alpha+")"
}</code></pre><p>这里我们使用了构造函数的方式来创建单个的圆,我们还需要一个变量 <code>initRoundPopulation</code> 来设置 round 的个数,然后我们便可以通过 <code>for</code> 循环创建出 <code>initRoundPopulation</code> 个圆。</p><h4>设置 <code>draw()</code> 方法</h4><p>在设置了单个的 <code>Round_item</code> 类之后,我们还要给每一个 round 设置 <code>draw()</code> 方法,所以我们需要将 <code>draw()</code> 方法设置在 <code>Round_item</code> 的原型中,这样我们创建出来的每一个 <code>Round_item</code> 实例对象都拥有了 <code>draw()</code> 方法。</p><pre><code> Round_item.prototype.draw = function(){
content.fillStyle = this.color;
// shadowBlur阴影的模糊级别
content.shadowBlur = this.r*2;
content.beginPath()
// x 坐标 y坐标 半径 起始角 结束角 顺时针逆时针
content.arc(this.x,this.y,this.r,0,2*Math.PI,false)
content.closePath()
content.fill()
}</code></pre><h4>设置初始化 <code>init()</code> 函数</h4><p>然后我们就需要设置初始化 <code>init()</code> 函数了,在 <code>init()</code> 函数中,我们的主要任务是创建单个的 round,然后使用其 <code>draw()</code> 方法。</p><pre><code>function init(){
for (var i = 0; i < initRoundPopulation; i++) {
round[i] = new Round_item(i,Math.random()*WIDTH,Math.random().HEIGHT)
round[i].draw()
}
}</code></pre><p>至此,我们已经完成了随机粒子的实现,完整的代码如下:</p><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
html,
body {
margin: 0;
overflow: hidden;
width: 100%;
height: 100%;
background-color: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
var ctx = document.getElementById('canvas'),
content = ctx.getContext('2d'),
initRoundPopulation = 80,
round = [],
WIDTH, HEIGHT;
WIDTH = document.documentElement.clientWidth;
HEIGHT = document.documentElement.clientHeight;
ctx.width = WIDTH;
ctx.height = HEIGHT;
function Round_item(index,x,y){
this.index = index
this.x = x;
this.y = y;
this.r = Math.random()*2+1; //随机半径
var alpha = (Math.floor(Math.random()*10)+1)/10/2
this.color = "rgba(255,255,255,"+alpha+")"
}
Round_item.prototype.draw = function(){
content.fillStyle = this.color;
// shadowBlur阴影的模糊级别
content.shadowBlur = this.r*2;
content.beginPath()
// x 坐标 y坐标 半径 起始角 结束角 顺时针逆时针
content.arc(this.x,this.y,this.r,0,2*Math.PI,false)
content.closePath()
content.fill()
}
function init(){
for (var i = 0; i < initRoundPopulation; i++) {
round[i] = new Round_item(i,Math.random()*WIDTH,Math.random().HEIGHT)
round[i].draw()
}
}
init()
</script>
</html></code></pre><h4>使你的随机粒子动起来</h4><h5><code>animate()</code> 函数</h5><p>Canvas 制作动画是一个不断擦除再重绘的过程,跟最原始实现动画的方式类似。在纸片上画每一帧,然后以很快的速度翻动小本本,就会有动画的效果。<br>现在我们实现动画需要在很短的时间内不断的清除内容再重新绘制,新的图形和原先清除的图形之间有某种位置关系,速度足够快的话,我们就会看到动画的效果。<br>所以我们需要一个 <code>animate()</code> 函数,这个函数的作用是帮助我们形成动画,我们在这个函数中首先需要清除当前屏幕,这里的清除函数用到的是 <code>content.clearRect()</code> 方法。</p><p>canvas 的 <code>content.clearRect()</code> 方法:</p><p><code>context.clearRect(x,y,width,height);</code></p><ul><li>x:要清除的矩形左上角的 x 坐标</li><li>y:要清除的矩形左上角的 y 坐标</li><li>width:要清除的矩形的宽度,以像素计</li><li>height:要清除的矩形的高度,以像素计</li></ul><p>我们需要清除的区域是整个屏幕,所以 <code>content.clearRect()</code> 的参数就是 <code>content.clearRect(0, 0, WIDTH, HEIGHT);</code>,这里我们就用到了之前获取的屏幕宽度和高度的常量:<code>WIDTH</code> 和 <code>HEIGHT</code>。</p><p>粒子匀速上升。粒子匀速上升,也就是 y 坐标在不断地变化,既然是匀速的,那么也就是在相同的时间位移是相同的。<br>重新绘制完图形之后,我们就完成了清除屏幕内容再重新绘制新的图形的任务。那么还需要有一个步骤 —— “</p><p>不断”,要想实现动画的效果,就需要 “不断” 地进行清除再重绘,并且中间间隔的时间还不能过长。<br>js 的 <code>setTimeout()</code> 方法,但是 <code>setTimeout</code> 和 <code>setInterval</code> 的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。</p><p>另外一个函数 —— <code>requestAnimationFrame()</code> 。</p><p><code>window.requestAnimationFrame()</code> 方法告诉浏览器,你希望执行动画,并请求浏览器调用指定的函数在下一次重绘之前更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。</p><p><strong><code>requestAnimationFrame()</code> 函数可以说是专门用来写动画的。</strong><br>编写动画循环的关键是要知道延迟时间多长合适。一方面,循环间隔必须足够短,这样才能让不同的动画效果显得平滑流畅;另一方面,循环间隔还要足够长,这样才能确保浏览器有能力渲染产生的变化。</p><p>大多数电脑显示器的刷新频率是 60Hz,大概相当于每秒钟重绘 60 次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是 1000ms/60,约等于 16.6ms</p><p><code>requestAnimationFrame</code> 采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。</p><p>使用 <code>requestAnimationFrame()</code> 函数递归的调用 <code>animate()</code> 函数来实现动画的效果。</p><pre><code> function animate(){
content.clearRect(0,0,WIDTH,HEIGHT)
for(var i in round){
round[i].move()
}
requestAnimationFrame(animate)
}</code></pre><h5>创建 <code>move()</code> 函数</h5><p>使用 <code>move()</code> 函数来改变 round 的 y 坐标<br>将 <code>move()</code> 方法写在 <code>Round_item</code> 的原型上,这样我们创建的每一个 round 都具有了 <code>move()</code> 方法。</p><p>在 <code>move()</code> 方法中,我们只需要改变 round 的 y 坐标即可,并且设置边界条件,当 y 坐标的值小于 <code>-10</code>(也可以是其他负值),代表该 round 已经超出了屏幕,这个时候我们要将其移动到屏幕的最底端,这样才能保证我们创建的粒子数不变,一直是 <code>initRoundPopulation</code> 的值。<br>这样就是一个粒子在不断地上升,上升到了最顶端再移动到最底端的循环过程,看起来像是有源源不断的粒子,但其实总数是不变的。</p><p>在 y 坐标的变化之后,我们还需要使用新的 y 坐标再来重新绘制一下该 round。</p><h4>在 <code>init()</code> 中加入 <code>animate()</code></h4><p>我们想要实现动画的效果,还需要在 <code>init()</code> 中加入 <code>animate()</code> 函数。</p><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
overflow: hidden;
width: 100%;
height: 100%;
cursor: none;
background: black;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
var ctx = document.getElementById('canvas'),
content = ctx.getContext('2d'),
round = [],
WIDTH,
HEIGHT,
initRoundPopulation = 80;
WIDTH = document.documentElement.clientWidth;
HEIGHT = document.documentElement.clientHeight;
ctx.width = WIDTH;
ctx.height = HEIGHT;
function Round_item(index, x, y) {
this.index = index;
this.x = x;
this.y = y;
this.r = Math.random() * 2 + 1;
var alpha = (Math.floor(Math.random() * 10) + 1) / 10 / 2;
this.color = "rgba(255,255,255," + alpha + ")";
}
Round_item.prototype.draw = function () {
content.fillStyle = this.color;
content.shadowBlur = this.r * 2;
content.beginPath();
content.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
content.closePath();
content.fill();
};
function animate() {
content.clearRect(0, 0, WIDTH, HEIGHT);
for (var i in round) {
round[i].move();
}
requestAnimationFrame(animate)
}
Round_item.prototype.move = function () {
this.y -= 0.15;
if (this.y <= -10) {
this.y = HEIGHT + 10;
}
this.draw();
};
function init() {
for (var i = 0; i < initRoundPopulation; i++) {
round[i] = new Round_item(i, Math.random() * WIDTH, Math.random() * HEIGHT);
round[i].draw();
}
animate();
}
init();
</script>
</body>
</html></code></pre><h4>使你的鼠标和屏幕互动</h4><p><img src="/img/bVcK3oT" alt="image.png" title="image.png"></p><p>鼠标移动,会在经过的地方创建一个圆,圆的半径由小变大,达到某个固定大小时该圆消失。圆的颜色也是在随机变化的</p><h3>创建 Canvas 元素</h3><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
* {
padding: 0;
margin: 0;
}
#canvas {
background: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
WIDTH = canvas.width = document.documentElement.clientWidth,
HEIGHT = canvas.height = document.documentElement.clientHeight,
para = {
num: 100,
color: false, // 颜色 如果是false 则是随机渐变颜色
r: 0.9, // 圆每次增加的半径
o: 0.09, // 判断圆消失的条件,数值越大,消失的越快
a: 1
},
color,
color2,
round_arr = []; // 存放圆的数组
</script>
</body>
</html></code></pre><h3><code>onmousemove</code> 事件</h3><p>在鼠标移动的过程中,不断地在鼠标滑过的位置产生一个逐渐变大的圆。</p><p>Canvas 中创建动画的方式就是不断地清除屏幕内容然后重绘。</p><p>移动的轨迹是由一个一个的圆构成的,如果移动的速度过快的话,那么就可以明显看出一个一个的圆。</p><p>既然轨迹是由很多圆构成,那么我们就应该使用数组储存圆的信息(坐标、半径),然后在鼠标移动的时候将鼠标的位置信息储存在数组中。</p><p>所以在鼠标移动的过程我们首先要获得鼠标的坐标,然后将鼠标的坐标以及其他信息 push 到数组中去:</p><pre><code>window.onmousemove = function(event){
mouseX = event.clintX;
mouseY = event.clientY;
round_arr.push({
mouseX:mouseX,
mouseY:mouseY,
r:para.r,//设置半径每次增大的数值
o:l //判断圆消失的条件 数值越大 消失的越快
})
}</code></pre><h3>设置 <code>color</code></h3><p>已经将圆的相关信息储存在 <code>round_arr</code> 数组中了,现在要在 <code>animate()</code> 函数中将圆显示出来。<br>创建圆需要的坐标信息以及半径,我们在鼠标移动的事件中都已经将其 push 到 <code>round_arr</code> 数组中了,还有一个条件是需要设置的,那就是颜色。<br>在 <code>para</code> 参数中,我们可以看出,其中有设置 <code>color</code> 值。如果 <code>color</code> 值不为 <code>false</code>,那么设置的圆的颜色就是设置的 <code>color</code> 值;如果设置的 <code>color</code> 值为 <code>false</code>,那么圆的颜色就是随机的。</p><pre><code>if(para.color){
color2 = para.color
}else{
color = Math.random()*360
}</code></pre><p>那么怎么设置颜色的渐变呢?我们将 <code>color</code> 的颜色值依次增加一个增量。</p><pre><code>if (!para.color) {
color += .1;
color2 = 'hsl(' + color + ',100%,80%)';
}</code></pre><p>要让颜色一直改变,我们要将上面颜色改变的代码放在一个一直执行的函数。将上面改变颜色的代码放在<code>animate()</code> 函数中。</p><h3><code>animate()</code> 函数</h3><p>一个一直在执行的函数,这个函数主要负责动画的 <code>animate()</code> 函数。从函数名就可以看出这个函数的作用,的确,我们需要在该函数中写动画。<br>清除屏幕再重新绘制,</p><pre><code>ctx.clearRect(0, 0, WIDTH, HEIGHT);</code></pre><p>接着使用 <code>round_arr</code> 数组中的数据将一个一个的圆绘制出来。</p><p><strong>然后我们还需要一直执行这个函数:</strong></p><pre><code>window.requestAnimationFrame(animate);</code></pre><ol><li>创建 Canvas 元素,设置参数</li><li>鼠标移动事件,将坐标信息 push 到数组</li><li>设置颜色</li><li>设置动画 <code>animate()</code> 函数</li></ol><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
* {
padding: 0;
margin: 0;
}
#canvas {
background: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
WIDTH = canvas.width = document.documentElement.clientWidth,
HEIGHT = canvas.height = document.documentElement.clientHeight,
para = {
num: 100,
color: false, // 颜色 如果是false 则是随机渐变颜色
r: 0.9,
o: 0.09, // 判断圆消失的条件,数值越大,消失的越快
a: 1,
},
color,
color2,
round_arr = [];
window.onmousemove = function (event) {
mouseX = event.clientX;
mouseY = event.clientY;
round_arr.push({
mouseX: mouseX,
mouseY: mouseY,
r: para.r,
o: 1
})
};
// 判断参数中是否设置了 color,如果设置了 color,就使用该值、
// 如果参数中的 color 为 false,那么就使用随机的颜色
if (para.color) {
color2 = para.color;
} else {
color = Math.random() * 360;
}
function animate() {
if (!para.color) {
color += .1;
color2 = 'hsl(' + color + ',100%,80%)';
}
ctx.clearRect(0, 0, WIDTH, HEIGHT);
for (var i = 0; i < round_arr.length; i++) {
ctx.fillStyle = color2;
ctx.beginPath();
ctx.arc( round_arr[i].mouseX ,round_arr[i].mouseY,round_arr[i].r,0, Math.PI * 2);
ctx.closePath();
ctx.fill();
round_arr[i].r += para.r;
round_arr[i].o -= para.o;
if( round_arr[i].o <= 0){
round_arr.splice(i,1);
i--;
}
}
window.requestAnimationFrame(animate);
};
animate();
</script>
</body>
</html></code></pre><p><img src="/img/bVcK4bV" alt="image.png" title="image.png"></p><h2>canvas特效</h2><ul><li>背景颜色不宜过多</li><li>粒子数量多</li><li>粒子在动</li><li>能和鼠标进行交互</li></ul><h4>背景颜色</h4><p>因为网站还是以阅读为主,所以网站的背景颜色要适合阅读,最好还是设置为传统的 “白纸黑字”,使用浅色颜色作为背景,同时饱和度不宜过高,最好设置透明度。</p><p>并且背景颜色最好是 1~2 种颜色,不要设置过多的颜色,不然会影响阅读。</p><p>背景颜色可以直接使用 CSS 样式设置,不需要使用 Canvas。</p><h4>粒子在动</h4><p>大多数用户比较喜欢动效,但是对于网页的背景来说,动作的幅度又不能太大,动作也不要过于复杂,只是一些简单的位移并且动作的幅度也要小一点,让用户的潜意识里面知道这些粒子在动就可以,不能使用户的全部注意力都在粒子上面而忽视了网页的内容</p><h4>和鼠标进行交互</h4><p>用户一般还喜欢自己的操作能够得到网页的响应,所以我们可以设置鼠标跟随的效果或者视觉差的效果,加上和鼠标交互的特效,会使用户感到你的网站与众不同。</p><h2>使你的 Canvas 更加优雅</h2><h3>常见的 Canvas 优化方法</h3><h4>避免浮点数的坐标点</h4><p>绘制图形时,长度与坐标应选取整数而不是浮点数,原因在于 Canvas 支持半个像素绘制。</p><p>会根据小数位实现插值算法实现绘制图像的反锯齿效果,如果没有必要请不要选择浮点数值。</p><h4>使用多层画布去画一个复杂的场景</h4><p>一般在游戏中这个优化方式会经常使用,但是在我们的背景特效中不经常使用,这个优化方式是将经常移动的元素和不经常移动的元素分层,避免不必要的重绘。</p><p>比如在游戏中,背景不经常变换和人物这些经常变换的元素分成不同的层,这样需要重绘的资源就会少很多。</p><h4>用 CSS <code>transform</code> 特性缩放画布</h4><p>如果你使用 <code>left</code>、<code>top</code> 这些 CSS 属性来写动画的话,那么会触发整个像素渲染流程 —— <code>paint</code>、<code>layout</code> 和 <code>composition</code>。</p><p>但是使用 <code>transform</code> 中的 <code>translateX/Y</code> 来切换动画,你将会发现,这并不会触发 <code>paint</code> 和 <code>layout</code>,仅仅会触发 <code>composition</code> 的阶段。</p><p>这是因为 <code>transform</code> 调用的是 GPU 而不是 CPU。</p><h4>离屏渲染</h4><p>名字听起来很复杂,什么离屏渲染,其实就是设置缓存,绘制图像的时候在屏幕之外的地方绘制好,然后再直接拿过来用,这不就是缓存的概念吗?!︿( ̄︶ ̄)︿.</p><p>建立两个 Canvas 标签,大小一致,一个正常显示,一个隐藏(缓存用的,不插入 DOM 中)。先将结果 draw 到缓存用的 canvas 上下文中,因为游离 Canvas 不会造成 UI 的渲染,所以它不会展现出来;再把缓存的内容整个裁剪再 draw 到正常显示用的 Canvas 上,这样能优化不少。</p><h3>离屏渲染</h3><p><strong>使用之前的demo</strong></p><p>离屏渲染的主要过程就是将一个一个的粒子先在屏幕之外创建出来,然后再使用 <code>drawImage()</code> 方法将其“放入”到我们的主屏幕中。</p><p>我们首先要在全局设置一个变量 <code>useCache</code> 来存放我们是否使用离屏渲染这种优化方式。</p><pre><code>var useCache = true;</code></pre><h4><code>Round_item</code> 方法</h4><p>然后我们在 <code>Round_item</code> 原型的 <code>draw()</code> 方法中创建每一个离屏的小的 <code>canvas</code>。</p><pre><code>function Round_item(index,x,y){
this.index = index;
this.x = x;
this.y = y;
this.useCache = useChache;
this.cacheCanvas = document.createElement('canvas')
this.cacheCtx = this.cacheCanvas.getContext('2d')
this.r = Math.random()*2 + 1;
this.cacheCtx.width = 6* this.r;
this.cacheCtx.height = 6*this.r;
var alpha = (Math.floor(Math.random()*10)+1)/10/2;
this.color = "rgba(255,255,255,"+ alpha+")";
if(useChache){
this.cache()
}
}</code></pre><p>这里的 <code>cacheCanvas</code> 画布的宽度要设置为 6 倍的半径 是因为,我们创建的 <code>cacheCanvas</code> 不仅仅是有圆,还包括圆的阴影,所以我们要将 <code>cacheCanvas</code> 的面积设置得稍微大一些,这样才能将圆带阴影一起剪切到我们的主 Canvas 中。<br>在 <code>draw()</code> 方法中,我们新创建了 <code>cacheCanvas</code>,并获取到了 <code>cacheCanvas</code> 的上下文环境,然后设置其宽高。</p><p>然后我们判断了 <code>useChache</code> 变量的值,也就是说,如果我们将 <code>useChache</code> 设置为 <code>true</code>,也就是使用缓存,我们就调用 <code>this.cache()</code> 方法</p><h4><code>this.cache()</code> 方法</h4><p><code>Round_item</code> 的原型中设置 <code>this.cache()</code> 方法。</p><p>在 <code>this.cache()</code> 方法中,我们的主要任务是在每一个 <code>cacheCanvas</code> 中都绘制一个圆。</p><pre><code> Round_item.prototype.cache = function(){
this.cacheCtx.save();
this.cacheCtx.fillStyle = this.color;
this.cacheCtx.shadowColor = "white";
this.cacheCtx.shadowBlur = this.r*2;
this.cacheCtx.beginPath();
this.cacheCtx.arc(this.r*3,this.r*3,this.r,0,2*Math.PI);
this.cacheCtx.closePath();
this.cacheCtx.fill();
// restore(): 用来恢复Canvas旋转、缩放等之后的状态 当和canvas.save( )一起使用时,恢复到canvas.save( )保存时的状态。
this.cacheCtx.restore();
}</code></pre><p>在 <code>draw()</code> 方法中画的圆不同之处是,要注意这里设置的圆心坐标,是 <code>this.r * 3</code>,因为我们创建的 <code>cacheCanvas</code> 的宽度和高度都是 <code>6 * this.r</code>,我们的圆是要显示在 <code>cacheCanvas</code> 的正中心,所以设置圆心的坐标应该是 <code>this.r * 3,this.r * 3</code>。</p><h4><code>draw()</code> 方法</h4><p>在 <code>draw()</code> 中,就需要使用 Canvas 的 <code>drawImage</code> 方法将 <code>cacheCanvas</code> 中的内容显示在屏幕上。</p><pre><code> Round_item.prototype.draw = function(){
if(!useCache){
content.fillStyle = this.color
content.shadowBlur = this.r*2
content.beginPath()
content.arc(this.x,this.y,this.r,0,2*Math.PI,false);
content.closePath()
content.fill()
}else{
// drawImage() 方法绘制图像的某些部分,以及/或者增加或减少图像的尺寸 在画布上定位图像,并规定图像的宽度和高度:
content.drawImage(this.cacheCanvas,this.x-this.r,this.y-this.r)
}
}</code></pre><p>如果没有使用缓存的话,还是使用最原始的创建圆的方式。</p><p>就完成了离屏渲染的优化,我们来一起看一下完整的代码:</p><pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
html,body{
width: 100%;
height: 100%;
margin:0;
overflow: hidden;
cursor: none;
background-color: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
var ctx = document.getElementById('canvas'),
content = ctx.getContext('2d'),
round = [],
WIDTH,HEIGHT,
initRoundPopulation = 80,
useChache = true;
WIDTH = document.documentElement.clientWidth;
HEIGHT = document.documentElement.clientHeight;
ctx.width = WIDTH;
ctx.height = HEIGHT;
function Round_item(index,x,y){
this.index = index;
this.x = x;
this.y = y;
this.useChache = useChache;
this.cacheCanvas = document.createElement('canvas');
this.cacheCtx = this.cacheCanvas.getContext('2d');
this.cacheCtx.width = 6*this.r;
this.cacheCtx.height = 6*this.r;
this.r = Math.random()*2 +1;
var alpha = (Math.floor(Math.random()*10)+1)/10/2;
this.color = "rgba(255,255,255,"+alpha+")";
if(useChache){
this.cache()
}
}
Round_item.prototype.draw = function(){
if(!useChache){
content.fillStyle = this.color;
content.shadowBlur = this.r*2;
content.beginPath();
content.arc(this.x,this.y,this.r,0,2*Math.PI,false);
content.closePath()
content.fill()
}else{
content.drawImage(this.cacheCanvas,this.x-this.r,this.y - this.r)
}
}
Round_item.prototype.cache = function(){
this.cacheCtx.save();
this.cacheCtx.fillStyle = this.color;
this.cacheCtx.shadowColor = "white";
this.cacheCtx.shadowBlur = this.r*2;
this.cacheCtx.beginPath();
this.cacheCtx.arc(this.r*3,this.r*3,this.r,0,2*Math.PI);
this.cacheCtx.closePath();
this.cacheCtx.fill();
this.cacheCtx.restore()
};
function animate(){
content.clearRect(0,0,WIDTH,HEIGHT);
for(var i in round){
round[i].move()
}
requestAnimationFrame(animate)
};
Round_item.prototype.move = function(){
this.y -= 0.15;
if(this.y <= -10){
this.y = HEIGHT + 10
}
this.draw()
}
function init(){
for(var i = 0; i < initRoundPopulation;i++){
round[i] = new Round_item(i,Math.random()*WIDTH,Math.random()*HEIGHT);
round[i].draw()
}
animate()
}
init()
</script>
</body>
</html></code></pre>
Unicode的部分用法
https://segmentfault.com/a/1190000038179954
2020-11-16T10:34:26+08:00
2020-11-16T10:34:26+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h3>何为unicode?</h3><p>为了将全世界的文字都统一的记录下来,并将每个字符都用唯一的数字记录下来,于是就产生了Unicode。</p><p>Unicode 也称为 UCS(Universal Coded Character Set:国际编码字符集合) 是一个字符集合,对世界上大部分的文字系统进行了整理,编码,使电脑可以用更为简单的方式来呈现和处理文字。最新的版本 Unicode 11.0 已经包含了 137439 个字符</p><h3>String.fromCodePoint()方法</h3><p>ES5 提供<code>String.fromCharCode()</code>方法,用于从码点返回对应字符</p><pre><code> `String.fromCharCode(0x20BB7)`
`// "ஷ"`</code></pre><p>但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于<code>0xFFFF</code>)</p><p>ES6 提供了<code>String.fromCodePoint()</code>方法,可以识别大于<code>0xFFFF</code>的字符,弥补了<code>String.fromCharCode()</code>方法的不足。在作用上,正好与<code>codePointAt()</code>方法相反。</p><pre><code> `String.fromCodePoint(0x20BB7)`
`// "?"`
`String.fromCodePoint(0x78, 0x1f680, 0x79) === 'xuD83DuDE80y'`
`// true`</code></pre><p>如果<code>String.fromCodePoint()</code>方法有多个参数,则它们会被合并成一个字符串返回。<br><code>fromCodePoint()</code>方法定义在<code>String</code>对象上,而<code>codePointAt()</code>方法定义在字符串的实例对象上。<br>。</p><h3>String.prototype.codePointAt()</h3><p><code>codePointAt()</code> 方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。返回的是码点的十进制值。如果想要十六进制的值,可以使用toString()方法转换一下。</p><pre><code>let s = '?a';
s.codePointAt(0).toString(16) // "20bb7"
s.codePointAt(2).toString(16) // "61"</code></pre><p><code>codePointAt()</code> 方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。</p><pre><code>function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
is32Bit("?") // true
is32Bit("a") // false</code></pre><h3>正则表达式u修饰符</h3><p>此修饰符标识能够正确处理大于\uFFFF的Unicode字符<br>也就是说,会正确处理四个字节的UTF-16编码。<br>不加u修饰符,那么就无法将四个字节的UTF-16编码识别为一个字符,所以就可以产生匹配。<br><code>/^.$/.test(</code>`"uD842uDFB7"<code>)</code>//false`</p><p><code>/^.$/u.test(</code>`"uD842uDFB7"<code>)</code>//true`</p><pre><code>\u6211=>\u{6211} unicode字符的最佳写法</code></pre>
Vue容易且常用的方法
https://segmentfault.com/a/1190000037784242
2020-11-10T11:59:44+08:00
2020-11-10T11:59:44+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<h3><strong>深克隆</strong></h3><p>*<code>Object.prototype.toString()</code> 方法,会返回一个形如 <code>"[object XXX]"</code> 的字符串<br>万物皆对象,.call继承对象的toString方法*</p><pre><code>function typeOf(obj){
const toString = Object.prototype.toString
const map = {
'[object Boolean]':'boolean',
'[object Number]':'number',
'[object String]':'string',
'[object Function]':'function',
'[object Array]':'array',
'[object Date]':'date',
'[object RegExp]':'regExp',
'[object Undefined]':'undefined',
'[object Null]':'null',
'[object Object]':'object'
}
return map[toString.call(obj)]
}
//deepCopy
function deepCopy(data){
const t = typeOf(data)
let o;
if(t==='array'){
o = []
for(let i = 0;i<data.length;i++){
o.push(deepCopy(data[i]))
}
}else if(t === 'object'){
o = {}
for(let key in data){
o[key] = deepCopy(data[key])
}
}else{
return data
}
return o
}</code></pre><h3><strong>vue中类名的根据条件动态显示</strong></h3><p><em>class属性(数组语法) ,对象内如果右侧满足,则有这个class名称 类名是根据右侧的条件满足,则存在</em></p><pre><code> <label :class="wrapClasses"></label>
computed:{
wrapClasses(){
return [
`${prefixCls}-wrapper`,
{
[`${prefixCls}-group-item`]:this.group,
[`${prefixCls}-wrapper-checked`]:this.currentValue,
[`${prefixCls}-wrapper-disabled`]:this.disabled,
[`${prefixCls}-${this.size}`]:!!this.size
}
]
}
}</code></pre><h3>限制输入框的输入数字(整数或带有两位小数)</h3><p><strong>两位小数</strong></p><pre><code> <el-input
size="small"
v-model="scope.row.x"
@input="limitInput($event)"
/>
// 两位小数
limitInput(value) {
this.input4 =
("" + value) // 第一步:转成字符串
.replace(/[^\d^\.]+/g, "") // 第二步:把不是数字,不是小数点的过滤掉
.replace(/^0+(\d)/, "$1") // 第三步:第一位0开头,0后面为数字,则过滤掉,取后面的数字
.replace(/^\./, "0.") // 第四步:如果输入的第一位为小数点,则替换成 0. 实现自动补全
.match(/^\d*(\.?\d{0,2})/g)[0] || "";
},
或者
<el-input-number
style="width: 150px"
v-model="scope.row.lastNum"
controls-position="right"
:min="0"
:precision="2"
></el-input-number></code></pre><p><strong>正整数</strong></p><pre><code><el-input
size="small"
v-model="scope.row.y"
@input="isNumber($event)"
/>
// 正整数
isNumber(val) {
val = val.replace(/[^\d]/g, "");
this.input3 = val;
},
或者
<el-input-number
style="width: 150px"
v-model="scope.row.surplusNum"
controls-position="right"
:min="0"
:precision="0"
></el-input-number></code></pre><h3>vue中css的条件渲染</h3><pre><code><div class="timeChoice" :class="{timeActive:activeId==it.id}"></code></pre><h3>路由传值取值</h3><pre><code>this.$router.push({
path: this.$route.path + "/add",
query:{
type:'edit'
}
});
const { type } = this.$route.query;</code></pre><h3>input</h3><pre><code><el-input
slot="reference"
type="textarea"
readonly
class="width-100"
v-model="form[item.bind]"
:autosize="{
minRows: item.minRows || 2,
maxRows: item.maxRows || 4,
}"
></el-input></code></pre>
Vue版轮播图
https://segmentfault.com/a/1190000037771255
2020-11-09T13:51:50+08:00
2020-11-09T13:51:50+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p><strong>mySwiper.vue</strong></p><pre><code><template>
<div class="swiper-box" ref="swiperBox">
<!-- 滑块主体 -->
<div class="swiper-wrapper" :style="{width: width + 'rem', height: oHeight}" @touchstart="startMove"
@touchend="endMove" @touchmove="move" ref="sliderBox">
<slot class="swiper-sliber" />
</div>
<!-- 按钮 -->
<div class="btn-box" v-if="nextBtn" @touchstart="startMove" @touchend="endMove" @touchmove="move">
<p @click.stop="handleLast" :class="{'click': isClickL}"><i class="iconfont icon-arrow-left-s-fill"></i></p>
<p @click.stop="handleNext" :class="{'click': isClickR}"><i class="iconfont icon-arrow-right-s-fill"></i>
</p>
</div>
<!-- 分页文字 -->
<div v-if="pagingText !== 'none'" class="paging-text-box"
:class="{left: pagingPosition === 'left', right: pagingPosition === 'right'}">
<span class="paging">{{pagingText !== 'none'? pagingText : ''}}{{loop ? length-2 + '/' + index : length + '/' + (index + 1)}}</span>
</div>
<!-- 分页器 -->
<div class="hint-list-box" v-if="paging !== 'none'">
<ul class="hint-list" :class="{center: paging === 'center', right: paging === 'right' }">
<li class="item" v-for="item in hintLen" :key="item"
:class="{active: (item === index && loop === true) || (!loop && item === index + 1)}">
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'my-swiper',
data() {
return {
startX: 0,
endX: 0,
nowLeft: 0,
isClickL: false,
isClickR: false,
length: 0,
index: 0,
width: 0,
resistance: 1,
itemWidth: 0,
timer: null,
autoTimer: null,
zoomTimer: null
}
},
props: {
oHeight: { //设置组件高度
type: String,
default: 'auto'
},
nextBtn: { //是否显示切换按钮默认false
type: Boolean,
default: false
},
loop: { //是否循环播放默认false
type: Boolean,
default: false
},
paging: { // 是否显示分页器默认none不显示 可选 left, center, right
type: String,
default: 'center'
},
pagingText: { // 是否显示页数 默认none不显示 传入字符串为页数文字前缀
type: String,
default: 'none'
},
pagingPosition: { //页数定位默认center居中 可选left right
type: String,
default: 'center'
},
defaultIndex: { //默认开始轮播索引 默认0 请填写你传入元素长度内的 数字
type: Number,
default: 0
},
automation: { // 是否自动轮播 为0时不自动轮播 默认3000 每3000ms轮播一次
type: Number,
default: 0
},
zoom: { // 是否启动zoom模式 要配合子组件宽度一起使用 必须在loop模式下开启
type: Boolean,
default: false
},
retract: { // 是否缩进显示 要配合子组件宽度一起使用 必须在loop模式下开启
type: Boolean,
default: false
}
},
methods: {
// 手指点击时触发方法, 主要记录手指要开始滑动时x坐标用于计算滑动结束与开始的差值, 触控时清除 自动轮播与轮播动画的定时器
startMove(e) {
clearInterval(this.timer) // 清除轮播动画定时器
clearInterval(this.autoTimer) // 清除自动轮播定时器
this.startX = e.targetTouches[0].clientX // 获取触控时x坐标
this.nowLeft = this.$refs.sliderBox.offsetLeft // 记录当前滑块left值
},
// 手指拖动时触发的,滑动动画
slider(nowX) {
if (this.$refs.sliderBox.offsetLeft > 0 && !this.loop) { // 不是循环模式下 在第一个元素向右拖拽时速度衰减
this.resistance += 0.03
window.console.log(55555)
this.$refs.sliderBox.style.left = (nowX - this.startX) / this.resistance + 'px'
return
} else if (this.$refs.sliderBox.offsetLeft < -this.maxLeft && !this.loop) { // 不是循环模式下 在最后一个元素向左拖拽时速度衰减
this.resistance += 0.03
this.$refs.sliderBox.style.left = this.nowLeft + (nowX - this.startX) / this.resistance + 'px'
return
}
this.$refs.sliderBox.style.left = this.nowLeft + (nowX - this.startX) + 'px'
},
// 根据子元素计算 滑块宽度
getItemDom() {
let itemArr = Array.prototype.slice.call(this.$refs.sliderBox.children, 0)
this.length = this.$refs.sliderBox.children.length
itemArr.forEach((ele) => {
this.width += parseFloat(ele.style.width)
})
},
// zoom模式下滑动动画
zoomMove(nowX) {
if (nowX - this.startX < 0) {
let scale = (-(nowX - this.startX) * 0.001) + 0.8 > 1 ? 1 : (-(nowX - this.startX) * 0.001) + 0.8
let opacity = (-(nowX - this.startX) * 0.001) + 0.5 > 1 ? 1 : (-(nowX - this.startX) * 0.0015) + 0.5
this.$refs.sliderBox.children[this.index + 1].style.transform = 'scale(' + scale + ')'
this.$refs.sliderBox.children[this.index + 1].style.opacity = opacity
} else {
let scale = ((nowX - this.startX) * 0.001) + 0.8 > 1 ? 1 : ((nowX - this.startX) * 0.001) + 0.8
let opacity = ((nowX - this.startX) * 0.001) + 0.5 > 1 ? 1 : ((nowX - this.startX) * 0.0015) + 0.5
this.$refs.sliderBox.children[this.index - 1].style.transform = 'scale(' + scale + ')'
this.$refs.sliderBox.children[this.index - 1].style.opacity = opacity
}
},
// 手指拖动时触发该方法
move(e) {
let nowX = e.targetTouches[0].clientX
this.slider(nowX) // 调用滑动动画函数
this.zoom && this.zoomMove(nowX) // 调用zoom动画函数
},
// 拖动结束时触发 该方法
endMove(e) {
this.endX = e.changedTouches[0].clientX
if (this.resistance !== 1) this.resistance = 1
// 判断拖动距离 是否改变index
if (this.distance > 50) {
this.handleLast('touch')
} else if (this.distance < -50) {
this.handleNext('touch')
} else {
this.animation(this.$refs.sliderBox, this.target)
}
// 在自动轮播时 拖动结束重新启动定时器
this.autoTimer = this.automation ? this.handleAuto() : null
},
// 切换下一个函数, 主要以改变index方式切换
handleLast(isTouch) {
this.isClickL = true
if (!this.loop) {
if (!this.index && isTouch !== 'touch') {
this.index = this.length - 1
} else if (this.index) {
this.index--
} else {
this.animation(this.$refs.sliderBox, this.target)
}
} else {
if (this.index === 1) {
this.transposition()
this.index = this.length - 2
} else {
this.index--
}
}
this.zoom && this.$refs.sliderBox && this.zoomAnimation(this.$refs.sliderBox.children[this.index])
setTimeout(() => {
this.isClickL = false
}, 300)
},
// 切换上一个函数, 主要以改变index方式切换
handleNext(isTouch) {
this.isClickR = true
if (!this.loop) {
if (this.index === this.length - 1 && isTouch !== 'touch') {
this.index = 0
} else if (this.index !== this.length - 1) {
this.index++
} else {
this.animation(this.$refs.sliderBox, this.target)
}
} else {
if (this.index === this.length - 2) {
this.transposition('isLast')
this.index = 1
} else {
this.index++
}
}
this.zoom && this.$refs.sliderBox && this.zoomAnimation(this.$refs.sliderBox.children[this.index])
setTimeout(() => {
this.isClickR = false
}, 300)
},
// 切换动画
animation(dom, target) {
clearInterval(this.timer)
let origin = null
let speed = null
this.timer = setInterval(() => {
origin = dom.offsetLeft
speed = (target - origin) / 9
speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed)
if (Math.abs(target - dom.offsetLeft) < 2) {
dom.style.left = target + 'px'
clearInterval(this.timer)
return
}
dom.style.left = origin + speed + 'px'
}, 16)
this.endX = 0
this.startX = 0
if (this.zoom) {
this.isZoomActive()
}
},
// zoom动画
zoomAnimation(dom) {
clearInterval(this.zoomTimer)
let originScale = Number(this.getDomTransform(dom, 'scale'))
let originOpacity = Number(dom.style.opacity)
if (originScale === 1 && originOpacity === 1) {
clearInterval(this.zoomTimer)
return
}
let speed = 0.02
this.zoomTimer = setInterval(() => {
if (originScale >= 1 && originOpacity >= 1) {
clearInterval(this.zoomTimer)
return
}
originScale += speed
originOpacity += speed
dom.style.transform = originScale > 1 ? `scale(1)` : `scale(${originScale})`
dom.style.opacity = originOpacity
}, 16)
},
// 获取元素transform函数
getDomTransform(dom, type) {
let transformStr = dom.style.transform
let tarnArr = transformStr.split(' ')
let obj = {}
tarnArr.forEach(item => {
let tempArr = item.split('(')
obj[tempArr[0]] = tempArr[1].slice(0, -1)
})
return type ? obj[type] : obj
},
// loop模式的前期渲染函数, 感觉这里写的不是很美丽关键是想把,滑块中的每一项以标签形式传进来,没想到更好的方法,如果是数组的形式就要好些
renderLoop() {
if (this.loop) {
let divArr = Array.prototype.slice.call(this.$refs.sliderBox.children, 0)
let len = divArr.length
let deviceWidth = document.documentElement.clientWidth
let first = divArr[0].cloneNode(true)
let last = divArr[len - 1].cloneNode(true)
this.$refs.sliderBox.appendChild(first)
this.$refs.sliderBox.insertBefore(last, this.$refs.sliderBox.children[0])
setTimeout(() => {
this.$refs.sliderBox.style.left = -(this.defaultIndex + 1) * this.$refs.sliderBox.children[0].offsetWidth + (deviceWidth - this.$refs.sliderBox.children[0].offsetWidth)/2 + 'px'
window.console.log('-----', this.$refs.sliderBox.style.left)
if (this.defaultIndex <= this.length - 1) {
this.index = this.defaultIndex + 1
}
})
} else {
setTimeout(() => {
this.$refs.sliderBox.style.left = -this.defaultIndex * this.$refs.sliderBox.children[0].offsetWidth + 'px'
this.index = this.defaultIndex
})
}
this.getItemDom()
},
// zoom模式中样式调整
isZoomActive() {
if (this.$refs.sliderBox) {
for (let i = 0; i < this.$refs.sliderBox.children.length; i++) {
if (i !== this.index) {
this.$refs.sliderBox.children[i].style.transform = 'scale(0.8)'
this.$refs.sliderBox.children[i].style.opacity = '0.5'
} else {
this.$refs.sliderBox.children[i].style.transform = 'scale(1)'
this.$refs.sliderBox.children[i].style.opacity = '1'
}
}
}
},
// 循环模式中的换位函数
transposition(isLast) {
if (isLast) {
if (this.$refs.sliderBox) this.$refs.sliderBox.style.left = this.distance + 'px'
return
}
this.$refs.sliderBox.style.left = -(this.length - 1) * this.$refs.sliderBox.children[0].offsetWidth + this.distance + 'px'
},
// 启动自动模式
handleAuto() {
if (this.automation) {
return (setInterval(() => {
this.handleNext()
}, this.automation))
}
}
},
computed: {
// 手指触碰与离开的差值
distance() {
return this.endX - this.startX
},
// 计算滑块目的地
target() {
if (this.swiperWidth - this.$refs.sliderBox.children[0].offsetWidth !== 0 && this.retract) {
return -this.index * this.$refs.sliderBox.children[0].offsetWidth + (this.swiperWidth - this.$refs.sliderBox.children[0].offsetWidth) / 2
}
return -this.index * this.$refs.sliderBox.children[0].offsetWidth
},
// 分页个数
hintLen() {
return this.loop && this.length ? this.length - 2 : this.length
},
// box宽度
swiperWidth() {
return this.$refs.swiperBox && this.$refs.swiperBox.offsetWidth
},
// 滑块极限值
maxLeft() {
return this.$refs.sliderBox && this.$refs.sliderBox.offsetWidth - this.$refs.sliderBox.children[0].offsetWidth
}
},
watch: {
// 监控index变化
index() {
this.animation(this.$refs.sliderBox, this.target)
}
},
mounted() {
this.renderLoop()
this.autoTimer = this.automation ? this.handleAuto() : null
},
beforeDestroy() {
clearInterval(this.timer)
clearInterval(this.autoTimer)
clearInterval(this.zoomTimer)
}
}
</script>
<style lang="scss" scoped>
.swiper-box {
width: 100%;
overflow: hidden;
position: relative;
.swiper-wrapper {
position: relative;
overflow: hidden;
display: flex;
}
.btn-box {
position: absolute;
width: 100%;
top: 50%;
display: flex;
justify-content: space-between;
transform: translateY(-50%);
overflow: hidden;
p {
overflow: hidden;
color: rgba(4, 4, 4, .4);
background-color: rgba(8, 8, 8, .1);
&.click {
background-color: rgba(8, 8, 8, .6);
.iconfont {
color: rgba(4, 4, 4, .6);
}
}
.iconfont {
font-size: .8rem;
}
}
}
.hint-list-box {
position: absolute;
width: 100%;
bottom: .2rem;
.hint-list {
display: flex;
&.center {
justify-content: center;
}
&.right {
justify-content: flex-end;
}
}
.item {
width: .2rem;
height: .2rem;
border-radius: 50%;
background-color: #eeeeec;
margin-left: .2rem;
&.active {
background: linear-gradient(#6a3, #4e6);
}
}
}
.paging-text-box {
position: absolute;
top: .1rem;
text-align: center;
width: 100%;
&.right {
text-align: right;
span {
margin-right: 1rem;
}
}
&.left {
text-align: left;
span {
margin-left: 1rem;
}
}
}
}
</style>
</code></pre><p><strong>mySwiperItem.vue</strong></p><pre><code><!--
* @Author: yang
* @Date: 2020-11-09 07:04:39
* @LastEditors: yang
* @LastEditTime: 2020-11-09 10:45:21
* @FilePath: \gloud-h5\src\component\index\mySwiperItem.vue
-->
<template>
<div :style="{height: height + 'rem', width: width + 'rem'}" class="item-wrapper" >
<slot :ref="'item' + name"></slot>
</div>
</template>
<script>
export default {
name: "my-swiper-item",
props: {
width: {
type: Number,
required: true
},
height: {
type: String,
default: '4'
}
}
}
</script>
<style scoped>
</style>
</code></pre><p><strong>使用的是css预编译less</strong></p><pre><code>npm install --save-dev less less-loader css-loader style-loader</code></pre><p><strong>使用</strong><br><strong>index.vue</strong></p><pre><code><template>
<div class="show-swiper-wrapper">
<p class="show-toast-title">轮播图展示</p>
<div class="item">
<divider>基础调用</divider>
<my-swiper>
<my-swiper-item :width="7.5" v-for="item in list" :key="item.id" >
<img :src="item.img" alt="">
</my-swiper-item>
</my-swiper>
</div>
<div class="item">
<divider>自动轮播</divider>
<my-swiper :automation="2000" paging="right">
<my-swiper-item :width="7.5" v-for="item in list" :key="item.id">
<img :src="item.img" alt="">
</my-swiper-item>
</my-swiper>
</div>
<div class="item">
<divider>循环有按钮</divider>
<my-swiper :nextBtn="true" :automation="2000" :loop="true" paging="left">
<my-swiper-item :width="7.5" v-for="item in list" :key="item.id">
<img :src="item.img" alt="">
</my-swiper-item>
</my-swiper>
</div>
<div class="item">
<divider>缩进模式</divider>
<my-swiper :zoom="true" :retract="true" :automation="3000" :oHeight="'3rem'" eight="3" :loop="true" paging="none">
<my-swiper-item :width="5" height="3" v-for="item in list" :key="item.id">
<img :src="item.img" alt="">
</my-swiper-item>
</my-swiper>
</div>
<divider>查看源码</divider>
<my-button class="button-item" @button-click="pushLink" text="查看demo源码" size="lang" />
</div>
</template>
<script>
import MySwiper from './components/slider/mySwiper'
import MySwiperItem from './components/slider/mySwiperItem'
import Divider from './components/divider'
import MyButton from './components/myButton'
export default {
name: 'showSwiper',
components: {
MySwiper,
MySwiperItem,
Divider,
MyButton
},
data() {
return {
// 拿到本地实验要换路径偶
list: [
{
img: require('./static/1.jpg'),
id: 1
},
{
img: require('./static/2.jpg'),
id: 2
},
{
img: require('./static/3.jpg'),
id: 3
},
{
img: require('./static/4.jpg'),
id: 4
}
]
}
},
methods: {
pushLink() {
this.$router.push(`/soundCodeShow${this.$route.fullPath}`)
}
}
}
</script>
<style scoped lang="less">
.show-swiper-wrapper {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding-bottom: 1rem;
}
.show-swiper-wrapper .item {
width: 100%;
}
.show-swiper-wrapper .item img {
width: 100%;
height: 100%;
}
</style>
</code></pre><p><strong>效果:</strong><br><img src="/img/bVcIEbp" alt="image.png" title="image.png"></p>
vue组件略佳操作
https://segmentfault.com/a/1190000037618253
2020-10-27T11:43:29+08:00
2020-10-27T11:43:29+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<h3>button组件</h3><p>亮点:prop接收参数</p><pre><code><template>
<button :class="'i-button-size' + size" :disabled="disabled">
</button>
</template>
<script>
// 判断参数是否是其中之⼀
function oneOf (value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) { return true; }
}
return false;
}
export default {
props: {
size: {
validator (value)
{ return oneOf(value, ['small', 'large', 'default']); },
default: 'default' },
disabled: {
type: Boolean, default: false
} } }
</script></code></pre><p><strong>使⽤组件</strong>:</p><pre><code><i-button size="large">
</i-button>
<i-button disabled></i-button></code></pre><h2>mixins</h2><p><em>如果你的项⽬⾜够复杂,或需要多⼈协同开发时,在 app.vue ⾥会 写⾮常多的代码,多到结构复杂难以维护。这时可以使⽤ Vue.js 的 混合 mixins,将不同的逻辑分开到不同的 js ⽂件⾥。</em><br><strong>user.js</strong></p><pre><code>export default {
data () { return { userInfo: null } },
methods: {
getUserInfo(){
$.ajax('/user/info', (data) => { this.userInfo = data; }); } },
mounted () {
this.getUserInfo();
} }</code></pre><p><strong>然后在 app.vue 中混合: app.vue:</strong></p><pre><code><script>
import mixins_user from '../mixins/user.js'
export default {
mixins: [mixins_user],
data() {
return {}
},
}
</script></code></pre><p><strong>跟⽤户信息相关的逻辑,都可以在 user.js ⾥维护</strong></p><h2>$on 与 $emit</h2><p><em>$emit 会在当前组件实例上触发⾃定义事件,并传递⼀些参数给监 听器的回调,⼀般来说,都是在⽗级调⽤这个组件时,使⽤ @on 的 ⽅式来监听⾃定义事件的,⽐如在⼦组件中触发事件:$on 监听了⾃⼰触发的⾃定义事件 test,因为有时不确定何时会触 发事件,⼀般会在 mounted 或 created 钩⼦中来监听。</em><br><strong>子组件</strong></p><pre><code> methods: {
handleEmitEvent() {
this.$emit('test', 'Hello Vue.js')
},
},</code></pre><p><strong>父组件</strong></p><pre><code> mounted() {
this.$on('test', (text) => {
window.alert(text)
})
},</code></pre><h2>⾃⾏实现 dispatch 和 broadcast ⽅法</h2><p>*思路:<br>在⼦组件调⽤ dispatch ⽅法,向上级指定的组件实例(最近 的)上触发⾃定义事件,并传递数据,且该上级组件已预先通 过 $on 监听了这个事件; <br>相反,在⽗组件调⽤ broadcast ⽅法,向下级指定的组件实例 (最近的)上触发⾃定义事件,并传递数据,且该下级组件已 预先通过 $on 监听了这个事件。*<br><em>该⽅法可能在很多组件中都会使⽤,复⽤起⻅,我们封装在混合(mixins)⾥。那它的使⽤样例可能是这样的:</em><br><strong>有 A.vue 和 B.vue 两个组件,其中 B 是 A 的⼦组件,中间可能跨多级,在 A 中向 B 通信:</strong></p><pre><code><!-- A.vue -->
<template><button @click="handleClick">触发事件</button></template>
<script>
import Emitter from '../mixins/emitter.js'
export default {
name: 'componentA',
mixins: [Emitter],
methods: {
handleClick() {
this.broadcast('componentB', 'on- message', 'Hello Vue.js')
},
},
}
</script></code></pre><pre><code>// B.vue
export default {
name: 'componentB',
created () {
this.$on('on-message', this.showMessage);
},
methods: {
showMessage (text) { window.alert(text);
} } }</code></pre><p><strong>在独⽴组件 (库)⾥,每个组件的 name 值应当是唯⼀的,name 主要⽤于递归 组件</strong></p><h5>emitter.js</h5><pre><code>function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name ===componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
} })
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent; if (parent) {
name = parent.$options.name;
} }
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
} },
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
} };</code></pre><p><strong>同理,如果是 B 向 A 通信,在 B 中调⽤ dispatch ⽅法,在 A 中使 ⽤ $on 监听事件即可。</strong></p><hr><p>因为是⽤作 mixins 导⼊,所以在 methods ⾥定义的 dispatch 和 broadcast ⽅法会被混合到组件⾥,⾃然就可以⽤ this.dispatch 和 this.broadcast 来使⽤。 </p><p>这两个⽅法都接收了三个参数,第⼀个是组件的 name 值,⽤于向上 或向下递归遍历来寻找对应的组件,第⼆个和第三个就是上⽂分析的 ⾃定义事件名称和要传递的数据。 可以看到,在 dispatch ⾥,通过 while 语句,不断向上遍历更新当 前组件(即上下⽂为当前调⽤该⽅法的组件)的⽗组件实例(变量 parent 即为⽗组件实例),直到匹配到定义的 componentName 与 某个上级组件的 name 选项⼀致时,结束循环,并在找到的组件实例 上,调⽤ $emit ⽅法来触发⾃定义事件 eventName。broadcast ⽅法与之类似,只不过是向下遍历寻找。</p><hr><p>*相⽐ Vue.js 1.x,有以下不同: <br>需要额外传⼊组件的 name 作为第⼀个参数; <br>⽆冒泡机制;<br>第三个参数传递的数据,只能是⼀个(较多时可以传⼊⼀个对 象),⽽ Vue.js 1.x 可以传⼊多个参数,当然,你对 emitter.js 稍作修改,也能⽀持传⼊多个参数,只是⼀般场景 传⼊⼀个对象⾜以*</p><h2>组件的通信找到任意组件实例 ——findComponents 系列⽅法</h2><p><em>是组件通信的终极⽅案。通过递归、遍历,找到指定组件的 name 选项 匹配的组件实例并返回。 findComponents 系列⽅法最终都是返回组件的实例,进⽽可以读 取或调⽤该组件的数据和⽅法</em><br>*它适⽤于以下场景: <br>由⼀个组件,向上找到最近的指定组件;<br>由⼀个组件,向上找到所有的指定组件; <br>由⼀个组件,向下找到最近的指定组件; <br>由⼀个组件,向下找到所有指定的组件;<br>由⼀个组件,找到指定组件的兄弟组件。*</p><h4>utils/assits.js</h4><pre><code>// 由⼀个组件,向上找到最近的指定组件
//context 上下文
//componentName 组件名称
function findComponentUpward (context, componentName) {
let parent = context.$parent;
let name = parent.$options.name;
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}
// 由⼀个组件,向上找到所有的指定组件
function findComponentsUpward(context, componentName) {
let parents = []
const parent = context.$parent
if (parent) {
if (parent.$options.name === componentName) parents.push(parent)
return parents.concat(findComponentsUpward(parent, componentName))
} else {
return []
}
},
// 由⼀个组件,向下找到最近的指定组件
function findComponentDownward(context, componentName) {
//context.$children 得到的是当前组件的全部⼦组件
const childrens = context.$children
let children = null
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name
if (name === componentName) {
children = child
break
} else {
children = findComponentDownward(child, componentName)
if (children) break
}
}
}
return children
},
// 由⼀个组件,向下找到所有指定的组件
//使⽤ reduce 做累加器,为数组中的每一个元素依次执行回调函数,并 ⽤递归将找到的组件合并为⼀个数组并返回,
function findComponentsDownward(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child)
const foundChilds = findComponentsDownward(child, componentName)
return components.concat(foundChilds)
}, [])
},
// 由⼀个组件,找到指定组件的兄弟组件
function findBrothersComponents(context, componentName, exceptMe = true) {
//⽗组件的 全部⼦组件,这⾥⾯当前包含了本身
//exceptMe true是不包括自己
let res = context.$parent.$children.filter((item) => {
return item.$options.name === componentName
})
// Vue.js 在渲染组件时,都会给每个组件加⼀个内置的属 性 _uid,这个 _uid 是不会重复的,借此我们可以从⼀系列兄弟组 件中把⾃⼰排除掉
let index = res.findIndex((item) => item._uid === context._uid)
if (exceptMe) res.splice(index, 1)
return res
},
export { findComponentUpward,findComponentsUpward,findComponentDownward,findComponentsDownward,findBrothersComponents };</code></pre><p><strong>使用(A 是 B 的⽗组件)</strong></p><pre><code><!-- component-a.vue -->
<template>
<div>组件 A <component-b></component-b></div>
</template>
<script>
import componentB from './component-b.vue'
import { findComponentDownward } from '../utils/assist.js';
export default {
name: 'componentA',
components: { componentB },
data() {
return { name: 'Aresn' }
},
methods: {
sayHello() {
console.log('Hello, Vue.js')
},
},
mounted(){
const comB = findComponentDownward(this, 'componentB');
if (comB) { console.log(comB.name);
}
}
</script></code></pre><pre><code><!-- component-b.vue -->
<template>
<div>组件 B</div>
</template>
<script>
import { findComponentUpward,findBrothersComponents } from '../utils/assist.js'
export default {
name: 'componentB',
mounted() {
const comA = findComponentUpward(this, 'componentA')
if (comA) {
console.log(comA.name) // Aresn
comA.sayHello() // Hello, Vue.js
}
const comsB = findBrothersComponents(this, 'componentB');
console.log(comsB); // ① [],空数组
//如果在 A 中再写⼀个 B:这时就会打印出 [VueComponent],有⼀个组件了
},
}
</script></code></pre><h2>组合多选框组件—— CheckboxGroup & Checkbox</h2><p><strong>checkbox.vue</strong><br><em>updateModel⽅法在 Checkbox ⾥的 mounted 初始化时调⽤。这 个⽅法的作⽤就是在 CheckboxGroup ⾥通过 findComponentsDownward ⽅法找到所有的 Checkbox,然后把 CheckboxGroup 的 value,赋值给 Checkbox 的 model,并根 据 Checkbox 的 label,设置⼀次当前 Checkbox 的选中状态。 这样⽆论是由内⽽外选择,或由外向内修改数据,都是双向绑定的, ⽽且⽀持动态增加 Checkbox 的数量。</em></p><pre><code><!-- checkbox.vue -->
<template>
<label>
<span>
<input
v-if="group"
type="checkbox"
:disabled="disabled"
:value="label"
v-model="model"
@change="change"
/>
<input
v-else
type="checkbox"
:disabled="disabled"
:checked="currentValue"
@change="change"
/>
</span>
<slot></slot>
</label>
</template>
<script>
import { findComponentUpward } from '../../utils/assist.js'
export default {
name: 'iCheckbox',
props: {
disabled: { type: Boolean, default: false },
value: { type: [String, Number, Boolean], default: false },
trueValue: { type: [String, Number, Boolean], default: true },
falseValue: { type: [String, Number, Boolean], default: false },
label: { type: [String, Number, Boolean] },
},
data() {
return {
currentValue: this.value,
model: [],
group: false,
parent: null,
}
},
methods: {
change(event) {
if (this.disabled) {
return false
}
const checked = event.target.checked
this.currentValue = checked
const value = checked ? this.trueValue : this.falseValue
this.$emit('input', value)
if (this.group) {
this.parent.change(this.model)
} else {
this.$emit('on-change', value)
this.dispatch('iFormItem', 'on-form-change', value)
}
},
updateModel() {
this.currentValue = this.value === this.trueValue
},
},
watch: {
value(val) {
if (val === this.trueValue || val === this.falseValue) {
this.updateModel()
} else {
throw 'Value should be trueValue or falseValue.'
}
},
},
mounted() {
this.parent = findComponentUpward(this, 'iCheckboxGroup')
if (this.parent) {
this.group = true
}
if (this.group) {
this.parent.updateModel(true)
} else {
this.updateModel()
}
},
}
</script></code></pre><p><strong>checkbox-group.vue</strong></p><pre><code><!-- checkbox-group.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<script>
import { findComponentsDownward } from '../../utils/assist.js'
import Emitter from '../../mixins/emitter.js'
export default {
name: 'iCheckboxGroup',
mixins: [Emitter],
props: {
value: {
type: Array,
default() {
return []
},
},
},
data() {
return { currentValue: this.value, childrens: [] }
},
methods: {
updateModel(update) {
this.childrens = findComponentsDownward(this, 'iCheckbox')
if (this.childrens) {
const { value } = this
this.childrens.forEach((child) => {
child.model = value
if (update) {
child.currentValue = value.indexOf(child.label) >= 0
child.group = true
}
})
}
},
change(data) {
this.currentValue = data
this.$emit('input', data)
this.$emit('on-change', data)
this.dispatch('iFormItem', 'on-form-change', data)
},
},
mounted() {
this.updateModel(true)
},
watch: {
value() {
this.updateModel(true)
},
},
}
</script></code></pre><h2>Vue的构造器extend 与⼿ 动挂载$mount</h2><p><em>Vue 的$mount()为手动挂载,在项目中可用于延时挂载(例如在挂载之前要进行一些其他操作、判断等),之后要手动挂载上。new Vue时,el和$mount并没有本质上的不同。</em></p><p><em>创建⼀个 Vue 实例时,都会有⼀个选项 el,来指定 实例的根节点,如果不写 el 选项,那组件就处于未挂载状 态。Vue.extend 的作⽤,就是基于 Vue 构造器,创建⼀个“⼦ 类”,它的参数跟 new Vue 的基本⼀样,但 data 要跟组件⼀样, 是个函数,再配合 $mount ,就可以让组件渲染,并且挂载到任意 指定的节点上,⽐如 body</em>。</p><pre><code>import Vue from 'vue'
//创建了⼀个构造器,这个过程就可以解决异步获取 template 模板的问题
const AlertComponent = Vue.extend({
template: '<div>{{ message }}</div>',
data() {
return { message: 'Hello, Aresn' }
},
})</code></pre><p><strong>⼿动渲染组件,并把它挂载到 body 下:</strong></p><pre><code>const component = new AlertComponent().$mount();
//$mount ⽅法对组件进⾏了⼿动渲染,但它仅 仅是被渲染好了,并没有挂载到节点上,也就显示不了组件。此时的 component 已经是⼀个标准的 Vue 组件实例,因此它的 $el 属性 也可以被访问:
document.body.appendChild(component.$el);</code></pre><p>$mount 也有⼀些快捷的挂载⽅式,以下两种都是可以的:</p><p> <strong>在 $mount ⾥写参数来指定挂载的节点</strong></p><pre><code> new AlertComponent().$mount('#app'); </code></pre><p> <strong>不⽤ $mount,直接在创建实例时指定 el 选项 </strong></p><pre><code> new AlertComponent({ el: '#app' });</code></pre><p><strong>实现同样的效果,除了⽤ extend 外,也可以直接创建 Vue 实例, 并且⽤⼀个 Render 函数来渲染⼀个 .vue ⽂件</strong></p><pre><code>import Vue from 'vue'
import Notification from './notification.vue'
const props = {} // 这⾥可以传⼊⼀些组件的 props 选 项
const Instance = new Vue({
render(h) {
return h(Notification, { props: props })
},
})
const component = Instance.$mount()
document.body.appendChild(component.$el)
</code></pre><p><strong>渲染后, 操作 Render 的 Notification 实例</strong></p><pre><code>const notification = Instance.$children[0];
//因为 Instance 下只 Render 了 Notification ⼀个⼦组件,所以可以 ⽤ $children[0] 访问到。</code></pre><p><strong>⽤ $mount ⼿动渲染的组件,如果要销毁, 也要⽤ $destroy 来⼿动销毁实例,必要时,也可以⽤ removeChild 把节点从 DOM 中移除。</strong></p><h2>动态渲染 .vue ⽂件的组件—— Display</h2><p>⼀个常规的 .vue ⽂件⼀般都会包含 3个部分:<br> <template>:组件的模板 <br> <script>:组件的选项,不包含 el; <br> <style>:CSS 样式。</p><h6>思路</h6><p>1.⽗级传递 code 后,将其分割,并保存在 data 的 html、js、css <br>2.使⽤正则,基于 <> 和 </> 的特性进⾏分割:</p><p><strong>utils/random_str.js</strong></p><pre><code>// ⽣成随机字符串
// 是从指定的 a-zA-Z0-9 中随机⽣成 32 位的字 符串。
export default function (len = 32) {
const $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV WXYZ1234567890';
const maxPos = $chars.length; let str = '';
for (let i = 0; i < len; i++) {
// Math.floor(Math.random() * maxPos) 0到32的整数
// charAt(int index) 方法用于返回指定索引处的字符
str += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return str;
},</code></pre><p><strong>组件display.vue</strong></p><pre><code><!-- display.vue -->
<template>
<div ref="display"></div>
</template>
<script>
import Vue from 'vue'
import randomStr from '../../utils/random_str.js'
export default {
props: { code: { type: String, default: '' } },
data() {
return {
html: '',
js: '',
css: '',
component: null,
id: randomStr()
}
},
// 当 this.code 更新时,整个过程要重新来⼀次,所以要对 code 进 ⾏ watch 监听:
watch: {
code() {
this.destroyCode()
this.renderCode()
},
},
methods: {
getSource(source, type) {
const regex = new RegExp(`<${type}[^>]*>`)
let openingTag = source.match(regex)
if (!openingTag) return ''
else openingTag = openingTag[0]
return source.slice(
source.indexOf(openingTag) + openingTag.length,
source.lastIndexOf(`</${type}>`)
)
},
// getSource ⽅法接收两个参数: source:.vue ⽂件代码,即 props: code;
// type:分割的部分,也就是 template、script、style。
// 分割后,返回的内容不再包含 <template> 等标签,直接是对应的 内容,在 splitCode ⽅法中,把分割好的代码分别赋值给 data 中声 明的 html、js、css。
// 有两个细节需要注意: 1. .vue 的 <script> 部分⼀般都是以 export default 开始 的,可以看到在 splitCode ⽅法中将它替换为了 return,这 个在后⽂会做解释,当前只要注意,我们分割完的代码,仍然 是字符串; 2. 在分割的 <template> 外层套了⼀个 <div id="app">,这 是为了容错,有时使⽤者传递的 code 可能会忘记在外层包⼀ 个节点,没有根节点的组件,是会报错的。
splitCode() {
const script = this.getSource(this.code, 'script').replace(
/export default/,
'return '
)
const style = this.getSource(this.code, 'style')
const template =
'<div id="app">' + this.getSource(this.code, 'template') + '</div>'
this.js = script
this.css = style
this.html = template
},
renderCode() {
this.splitCode()
if (this.html !== '' && this.js !== '') {
// new Function ([arg1[, arg2[, ...argN]],] functionBody)
// arg1, arg2, ... argN 是被函数使⽤的参数名称,functionBody 是 ⼀个含有包括函数定义的 JavaScript 语句的字符串。也就是说,示 例中的字符串 return a + b 被当做语句执⾏了。
// 当前的 this.js 是字符串, ⽽ extend 接收的选项可不是字符串,⽽是⼀个对象类型,那就要先 把 this.js 转为⼀个对象
// const sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 6)); // 8
// new Function eval 函数也可以使⽤
// this.js 中是将 export default 替换为 return 的,如 果将 this.js 传⼊ new Function ⾥,那么 this.js 就执⾏了,这时 因为有 return,返回的就是⼀个对象类型的 this.js 了
const parseStrToFunc = new Function(this.js)()
parseStrToFunc.template = this.html
const Component = Vue.extend(parseStrToFunc)
this.component = new Component().$mount()
// extend 构造的实例通过 $mount 渲染后,挂载到了组件唯⼀的⼀ 个节点 <div ref="display"> 上。
this.$refs.display.appendChild(this.component.$el)
if (this.css !== '') {
const style = document.createElement('style')
style.type = 'text/css'
// 创建⼀个 <style> 标签,然后把 css 写进去,再插⼊到⻚ ⾯的 <head> 中,这样 css 就被浏览器解析了。为了便于后⾯在 this.code 变化或组件销毁时移除动态创建的 <style> 标签,我 们给每个 style 标签加⼀个随机 id ⽤于标识。
style.id = this.id
style.innerHTML = this.css
document.getElementsByTagName('head')[0].appendChild(style)
}
}
},
// 当 Display 组件销毁时,也要⼿动销毁 extend 创建的实例以及上 ⾯的 css:
destroyCode() {
const $target = document.getElementById(this.id)
if ($target) $target.parentNode.removeChild($target)
if (this.component) {
this.$refs.display.removeChild(this.component.$el)
this.component.$destroy()
this.component = null
}
},
},
mounted() {
this.renderCode()
},
beforeDestroy() {
this.destroyCode()
},
}
</script></code></pre><h6> <strong>使⽤</strong></h6><p><em>新建⼀条路由,并在 src/views 下新建⻚⾯ display.vue 来使 ⽤ Display 组件</em><br><strong>src/views/display.vue</strong></p><pre><code><!-- src/views/display.vue -->
<template>
<div>
<h3>动态渲染 .vue ⽂件的组件—— Display</h3>
<i-display :code="code"></i-display>
</div>
</template>
<script>
import iDisplay from '../components/display/display.vue'
import defaultCode from './default-code.js'
export default {
components: { iDisplay },
data() {
return { code: defaultCode }
},
}
</script></code></pre><p><strong>// src/views/default-code.js</strong></p><pre><code>
const code = `<template>
<div>
<input v-model="message"> {{ message }}
</div>
</template>
<script> export default { data () { return { message: '' } } }
</script>`;
export default code;</code></pre><p><em>如果使⽤的是 Vue CLI 3 默认的配置,直接运⾏时,会抛出下⾯的 错误:</em></p><p>*这涉及到另⼀个知识点,就是 Vue.js 的版本。<br>在使⽤ Vue.js 2 时,有独⽴构建(standalone)和运⾏时构建(runtime-only)<br>两 种版本可供选择,<br>Vue CLI 3 默认使⽤了 vue.runtime.js,<br>它不允许编译 template 模板,<br>因为我们在 Vue.extend 构造实例时,<br>⽤了 template 选 项,所以会报错。<br>解决⽅案有两种,⼀是⼿动将 template 改写为 Render 函数,但这成本太⾼;<br>另⼀种是对 Vue CLI 3 创建的⼯程做 简单的配置。我们使⽤后者。*<br>在项⽬根⽬录,新建⽂件 vue.config.js:</p><pre><code>
module.exports = { runtimeCompiler: true };</code></pre><p>*它的作⽤是,是否使⽤包含运⾏时编译器的 Vue 构建版本。设置为 true 后就可以在 Vue 组件中使⽤ template 选项了,</p><p>这个⼩⼩的 Display 组件,能做的事还有很多,⽐如要写⼀套 Vue 组件库的⽂档,传统⽅法是在开发环境写⼀个个的 .vue ⽂件,然后 编译打包、上传资源、上线,如果要修改,哪怕⼀个标点符号,都要 重新编译打包、上传资源、上线。有了 Display 组件,只需要提供 ⼀个服务来在线修改⽂档的 .vue,就能实时更新,不⽤打包、上 传、上线*,</p><h2>全局提示组件—— $Alert</h2><p>this.$Alert 可以在任何位置调⽤,⽆需单独引⼊。<br>该⽅法接收两 个参数:<br>content:提示内容;<br>duration:持续时间,单位秒,默认 1.5 秒,到时间⾃动消 失<br>Alert 组件不同于常规的组件使⽤⽅式,它最终是通过 JS 来调⽤ 的,因此组件不⽤预留 props 和 events 接⼝</p><h6><strong>在 src/component 下新建 alert ⽬录,并创建⽂件 alert.vue</strong></h6><pre><code><template>
<div class="alert">
<div class="alert-main" v-for="item in notices" :key="item.name">
<div class="alert-content">{{ item.content }}</div>
</div>
</div>
</template>
<script>
let seed = 0
function getUuid() {
return 'alert_' + seed++
}
// JS 调⽤ Alert 的⼀个⽅法 add,并 将 content 和 duration 传⼊进来:
export default {
data() {
// 通知可以是多个,我们⽤⼀个数组 notices 来管理每条通知
return { notices: [] }
},
// 在 add ⽅法中,给每⼀条传进来的提示数据,加了⼀个不重复的 name 字段来标识,并通过 setTimeout 创建了⼀个计时器,当到 达指定的 duration 持续时间后,调⽤ remove ⽅法,将对应 name 的那条提示信息找到,并从数组中移除。 由这个思路,Alert 组件就可以⽆限扩展,只要在 add ⽅法中传递 更多的参数,就能⽀持更复杂的组件,⽐如是否显示⼿动关闭按钮、 确定 / 取消按钮,甚⾄传⼊⼀个 Render 函数都可以,完成本例 后,
methods: {
add(notice) {
const name = getUuid()
let _notice = Object.assign({ name: name }, notice)
this.notices.push(_notice)
// 定时移除,单位:秒
const duration = notice.duration
setTimeout(() => {
this.remove(name)
}, duration * 1000)
},
remove(name) {
const notices = this.notices
for (let i = 0; i < notices.length; i++) {
if (notices[i].name === name) {
this.notices.splice(i, 1)
break
}
}
},
},
}
</script>
<style>
.alert {
position: fixed;
width: 100%;
top: 16px;
left: 0;
text-align: center;
pointer-events: none;
}
.alert-content {
display: inline-block;
padding: 8px 16px;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
margin-bottom: 8px;
}
</style></code></pre><p>对 Alert 组件进⼀步封装,让它能够实例化,⽽不是常 规的组件使⽤⽅法<br>使⽤ Vue.extend 或 new Vue,然后⽤ $mount 挂载到 body 节点下。<br>notification.js 并不是最终的⽂件,它只是对 alert.vue 添加了⼀个 ⽅法 newInstance。虽然 alert.vue 包含了 template、script、 style 三个标签,并不是⼀个 JS 对象,那怎么能够给它扩展⼀个⽅法 newInstance 呢?事实上,alert.vue 会被 Webpack 的 vue- loader 编译,把 template 编译为 Render 函数,最终就会成为⼀ 个 JS 对象,⾃然可以对它进⾏扩展。</p><p>Alert 组件没有任何 props,这⾥在 Render Alert 组件时,还是给 它加了 props,当然,这⾥的 props 是空对象 {},⽽且即使传了内 容,也不起作⽤。这样做的⽬的还是为了扩展性,如果要在 Alert 上 添加 props 来⽀持更多特性,是要在这⾥传⼊的。不过话说回来, 因为能拿到 Alert 实例,⽤ data 或 props 都是可以的。</p><h6> notification.js</h6><pre><code>import Alert from './alert.vue'
import Vue from 'vue'
Alert.newInstance = (properties) => {
const props = properties || {}
const Instance = new Vue({
data: props,
render(h) {
return h(Alert, { props: props })
},
})
const component = Instance.$mount()
document.body.appendChild(component.$el)
const alert = Instance.$children[0]
return {
add(noticeProps) {
alert.add(noticeProps)
},
remove(name) {
alert.remove(name)
},
}
}
export default Alert</code></pre><h6> ⼊⼝</h6><p>最后要做的,就是调⽤ notification.js 创建实例,并通过 add 把数 据传递过去,这是组件开发的最后⼀步,也是最终的⼊⼝。<br>在 src/component/alert 下创建⽂件 <br><strong>alert.js:</strong></p><pre><code> // alert.js
import Notification from './notification.js'
let messageInstance
// getMessageInstance 函数⽤来获取实例,它不会重复创建,如 果 messageInstance 已经存在,就直接返回了,只在第⼀次调⽤ Notification 的 newInstance 时来创建实例。
function getMessageInstance() {
messageInstance = messageInstance || Notification.newInstance()
return messageInstance
}
function notice({ duration = 1.5, content = '' }) {
let instance = getMessageInstance()
instance.add({ content: content, duration: duration })
}
export default {
info(options) {
return notice(options)
},
}
// alert.js 对外提供了⼀个⽅法 info,如果需要各种显示效果,⽐如 成功的、失败的、警告的,可以在 info 下⾯提供更多的⽅法,⽐如 success、fail、warning 等,并传递不同参数让 Alert.vue 知道显 示哪种状态的图标。本例因为只有⼀个 info,事实上也可以省略 掉,直接导出⼀个默认的函数,这样在调⽤时,就不⽤ this.$Alert.info() 了,直接 this.$Alert()。</code></pre><p>把 alert.js 作为插件注册到 Vue ⾥就⾏,在⼊⼝⽂件 src/main.js中,通过 prototype 给 Vue 添加⼀个实例⽅法:</p><h6>src/main.js</h6><pre><code> import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Alert from '../src/components/alert/alert.js'
Vue.config.productionTip = false
Vue.prototype.$Alert = Alert
new Vue({ router, render: h => h(App) }).$mount('#app')</code></pre><p><strong>这样在项⽬任何地⽅,都可以通过 this.$Alert 来调⽤ Alert 组 件了</strong><br><strong>src/views/alert.vue</strong></p><pre><code><template>
<div>
<button @click="handleOpen1">打开提示 1</button>
<button @click="handleOpen2">打开提示 2</button>
</div>
</template>
<script>
export default {
methods: {
handleOpen1() {
this.$Alert.info({ content: '我是提示信息 1' })
},
handleOpen2() {
this.$Alert.info({ content: '我是提示信息 2', duration: 3 })
},
},
}
</script></code></pre><p><strong>是同类组件中值得注意的:</strong></p><p>1. Alert.vue 的最外层是有⼀个 .alert 节点的,它会在第⼀次调 ⽤ $Alert 时,在 body 下创建,因为不在 <router-view> 内,</p><p>它不受路由的影响,也就是说⼀经创建,除⾮刷新⻚⾯, </p><p>这个节点是不会消失的,所以在 alert.vue 的设计中,并没有 主动销毁这个组件,</p><p>⽽是维护了⼀个⼦节点数组 notices。 </p><p>2. .alert 节点是 position: fixed 固定的,因此要合理设计它 的 z-index,否则可能被其它节点遮挡。 </p><p>3. notification.js 和 alert.vue 是可以复⽤的,如果还要开发其 它同类的组件,⽐如⼆次确认组件 $Confirm, </p><p>只需要再写⼀ 个⼊⼝ confirm.js,并将 alert.vue 进⼀步封装,将 notices 数组的循环体写为⼀个新的组件,</p><p>通过配置来决定是 渲染 Alert 还是 Confirm,这在可维护性上是友好的。 </p><p>4. 在 notification.js 的 new Vue 时,使⽤了 Render 函数来渲 染 alert.vue,</p><p>这是因为使⽤ template 在 runtime 的 Vue.js 版本下是会报错的。</p><p>5. 本例的 content 只能是字符串,如果要显示⾃定义的内容,除 了⽤ v-html 指令,也能⽤ Functional Render。 </p><p>结语Vue.js 的精髓是组件,组件的精髓是 JavaScript。将 JavaScript 开 发中的技巧结合 Vue.js 组件,就能玩出不⼀样的东⻄。</p><h2>更灵活的组件:Render 函数与 Functional</h2><p>Render Render 函数 返回的是⼀个 JS<br>对象,没有传统 DOM 的层级关系,配合上 if、 else、for 等语句,将节点拆分成不同JS对象再组装。</p><p><strong> template 和 Render 写法的对照:</strong></p><pre><code><template>
<div id="main" class="container" style="color: red">
<p v-if="show">内容 1</p>
<p v-else>内容 2</p>
</div>
</template>
<script>
export default {
data() {
return { show: false }
},
}
</script></code></pre><p><strong>Render</strong></p><pre><code>export default {
data() {
return { show: false }
},
render: (h) => {
let childNode
if (this.show) {
childNode = h('p', '内容 1')
} else {
childNode = h('p', '内容 2')
}
return h(
'div',
{
attrs: { id: 'main' },
class: { container: true },
style: { color: 'red' },
},
[childNode]
)
},
}</code></pre><p>这⾥的 h,即 createElement,是 Render 函数的核⼼。<br> 可以看 到,template 中的 v-if / v-else 等指令,都被 JS 的 if / else 替 代了,<br> 那 v-for ⾃然也会被 for 语句替代。<br> h 有 3 个参数,分别是:</p><ol><li>要渲染的元素或组件,可以是⼀个 html 标签、组件选项或⼀ 个函数(不常⽤),该参数为必填项。示例:</li></ol><pre><code> // 1. html 标签
h('div');
// 2. 组件选项
import DatePicker from '../component/date- picker.vue'; h(DatePicker);</code></pre><ol><li>对应属性的数据对象,⽐如组件的 props、元素的 class、绑 定的事件、slot、⾃定义指令等,</li></ol><p>该参数是可选的,上⽂所说 的 Render 配置项多,指的就是这个参数。</p><p>该参数的完整配置 和示例,可以到 Vue.js 的⽂档查看,没必要全部记住,</p><p>⽤到 时查阅就好:createElement 参数 (<a href="https://link.segmentfault.com/?enc=kEHH8B2sHP%2BD7yM1od8Mtg%3D%3D.UjUfxS1PW7gyRajSaTfwtS42Dx4zmV3NCD1dITh%2Blu9kaTvi1rhL%2FSFXbt6EBttn" rel="nofollow">https://cn.vuejs.org/v2/guide...</a> function.html#createElement-参数)。</p><ol><li>⼦节点,可选,String 或 Array,它同样是⼀个 h。示例:</li></ol><p>[ </p><p>'内容', h('p', '内容'),</p><p>h(Component, { props: { someProp: 'foo' } }) </p><p>]<br> <strong>所有的组件树中,如果 vNode 是组件或含有组件的 slot,那么 vNode 必须唯⼀</strong></p><p><strong>重复渲染多个组件或元素,可以通过⼀个循环和⼯⼚函数来解决:</strong></p><pre><code>const Child = {
render: (h) => {
return h('p', 'text')
},
}
export default {
render: (h) => {
const children = Array.apply(null, { length: 5 }).map(() => {
return h(Child)
})
return h('div', children)
},
}</code></pre><p><strong>对于含有组件的 slot,复⽤⽐较复杂,需要将 slot 的每个⼦节点都 克隆⼀份,例如</strong>:</p><pre><code> {
render: (h) => {
function cloneVNode(vnode) {
//递归遍历所有⼦节点,并克隆
const clonedChildren =
vnode.children && vnode.children.map((vnode) => cloneVNode(vnode))
const cloned = h(vnode.tag, vnode.data, clonedChildren)
cloned.text = vnode.text
cloned.isComment = vnode.isComment
cloned.componentOptions = vnode.componentOptions
cloned.elm = vnode.elm
cloned.context = vnode.context
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
return cloned
}
const vNodes =
this.$slots.default === undefined ? [] : this.$slots.default
const clonedVNodes =
this.$slots.default === undefined
? []
: vNodes.map((vnode) => cloneVNode(vnode))
return h('div', [vNodes, clonedVNodes])
}
}</code></pre><ul><li>在 Render 函数⾥创建了⼀个 cloneVNode 的⼯⼚函数,通过递归 将 slot 所有⼦节点都克隆了⼀份,并对 VNode 的关键属性也进⾏ 了复制<br>深度克隆 slot 并⾮ Vue.js 内置⽅法,<br>在⼀些特殊的场景才会使⽤到,正常业务⼏乎是⽤不到的。⽐如 iView 组件库的穿梭框组件 Transfer,就⽤到了这种⽅法:<br>slot 是⼀个 Refresh 按钮,使⽤者只写了⼀遍,但在 Transfer 组件中,是通过克隆 VNode 的⽅法,显示了两遍。<br>如果 不这样做,就要声明两个具名 slot,但是左右两个的逻辑可能是完 全⼀样的,使⽤者就要写两个⼀模⼀样的 slot,这是不友好的*<br>Render 函数的基本⽤法还有很多,⽐如 v-model 的⽤ 法、事件和修饰符、slot 等,读者可以到 Vue.js ⽂档阅 读。Vue.js 渲染函数 (<a href="https://link.segmentfault.com/?enc=8IIpGrd1AnConXj5wIZdeA%3D%3D.TqkC1GozqVGAjT5uAKPmJm95PsvoqhdtjarCdsZeIIwSBXYzQVUXUkUNok4uZBbO" rel="nofollow">https://cn.vuejs.org/v2/guide...</a> function.html)</li></ul><h6>Render 函数使⽤场景</h6><p>⼀般情况下是不推荐直接使⽤ Render 函数的,使⽤ template ⾜以,在 Vue.js 中,使⽤ Render 函数的场景,主要有 以下 4 点</p><ol><li>使⽤两个相同 slot。在 template 中,Vue.js 不允许使⽤两个 相同的 slot,⽐如下⾯的示例是错误的:</li></ol><pre><code><template>
<div>
<slot></slot>
<slot></slot>
</div>
</template></code></pre><p><strong>解决⽅案就是上⽂中讲到的约束,使⽤⼀个深度克隆 VNode 节点的⽅法</strong></p><ol><li>在 SSR 环境(服务端渲染),如果不是常规的 template 写 法,⽐如通过 Vue.extend 和 new Vue 构造来⽣成的组件实 例,是编译不过的,使⽤ Render 函 数来渲染</li><li>在 runtime 版本的 Vue.js 中,如果使⽤ Vue.extend ⼿动构 造⼀个实例,使⽤ template 选项是会报错的,解决⽅案也很简单,把 template 改写为 Render 就可以了。需要注意的是,在开发独⽴组件时,可以通过配置 Vue.js 版本来使 template 选项可⽤,但这是在⾃⼰的环境, ⽆法保证使⽤者的 Vue.js 版本,所以对于提供给他⼈⽤的组 件,是需要考虑兼容 runtime 版本和 SSR 环境的。</li><li>这可能是使⽤ Render 函数最重要的⼀点。⼀个 Vue.js 组 件,有⼀部分内容需要从⽗级传递来显示,如果是⽂本之类 的,直接通过 props 就可以,如果这个内容带有样式或复杂 ⼀点的 html 结构,可以使⽤ v-html 指令来渲染,⽗级传递 的仍然是⼀个 HTML Element 字符串,不过它仅仅是能解析 正常的 html 节点且有 XSS ⻛险。当需要最⼤化程度⾃定义显 示内容时,就需要 Render 函数,它可以渲染⼀个完整的 Vue.js 组件。你可能会说,⽤ slot 不就好了?的确,slot 的 作⽤就是做内容分发的,但在⼀些特殊组件中,可能 slot 也不 ⾏。⽐如⼀个表格组件 Table,它只接收两个 props:列配置 columns 和⾏数据 data,不过某⼀列的单元格,不是只将数 据显示出来那么简单,可能带有⼀些复杂的操作,这种场景只 ⽤ slot 是不⾏的,没办法确定是那⼀列的 slot。这种场景有两 种解决⽅案,其⼀就是 Render 函数,另⼀种是⽤作⽤域 slot(slot- scope)</li></ol><h4>Functional Render</h4><p>*Vue.js 提供了⼀个 functional 的布尔值选项,设置为 true 可以 使组件⽆状态和⽆实例,也就是没有 data 和 this 上下⽂。这样⽤ Render 函数返回虚拟节点可以更容易渲染,因为函数化组件 (Functional Render)只是⼀个函数,渲染开销要⼩很多。<br>使⽤函数化组件,Render 函数提供了第⼆个参数 context 来提供临 时上下⽂。组件需要的 data、props、slots、children、parent 都 是通过这个上下⽂来传递的,⽐如 this.level 要改写为 context.props.level,this.$slots.default 改写为 context.children,您可以阅读 Vue.js ⽂档—函数式组件 (<a href="https://link.segmentfault.com/?enc=G7mpr8CesR%2Fz4Ew4yuXOiQ%3D%3D.4aR1yrwYf3PCMrA1zIxZX5qZf6IYODvn9l34n%2Bq9sdPO4%2BTdPqwioFbYEaPgqyTJ" rel="nofollow">https://cn.vuejs.org/v2/guide...</a> function.html#函数式组件) 来查看示例。<br>函数化组件在业务中并不是很常⽤,⽽且也有类似的⽅法来实现,⽐ 如某些场景可以⽤ is 特性来动态挂载组件。函数化组件主要适⽤于 以下两个场景*<br>1.程序化地在多个组件中选择⼀个; <br>2.在将 children、props、data 传递给⼦组件之前操作它们。 <br>某个组件需要使⽤ Render 函数来⾃定义,⽽不是 通过传递普通⽂本或 v-html 指令,这时就可以⽤ Functional Render,来看下⾯的示例:<br><strong>⾸先创建⼀个函数化组件 render.js:</strong></p><pre><code> // 它只定义了⼀个 props:render,格式为 Function,因为是 functional,所以在 render ⾥使⽤了第⼆个参数 ctx 来获取 props。这是⼀个中间⽂件,并且可以复⽤,其它组件需要这 个功能时,都可以引⼊它
// render.js
export default {
functional: true,
props: { render: Function },
render: (h, ctx) => {
return ctx.props.render(h)
},
}</code></pre><p><strong> 创建组件:</strong></p><pre><code>
<!-- my-component.vue -->
<template>
<div><Render :render="render"></Render></div>
</template>
<script>
import Render from './render.js'
export default {
components: { Render },
props: { render: Function }
}
</script></code></pre><p><strong>使⽤上⾯的 my-compoennt 组件:</strong></p><pre><code><template>
<div>
<my-component :render="render"></my-component>
</div>
</template>
<script>
import myComponent from '../components/my-component.vue'
export default {
components: { myComponent },
data() {
return {
render: (h) => {
return h('div', { style: { color: 'red' } }, '⾃定义内容')
},
}
},
}
</script></code></pre><p>这⾥的 render.js 因为只是把 demo.vue 中的 Render 内容过继, 并⽆其它⽤处,所以⽤了 Functional Render。 就此例来说,完全可以⽤ slot 取代 Functional Render,那是因为 只有 render 这⼀个 prop。如果示例中的 <Render> 是⽤ v-for ⽣成的,也就是多个时,⽤ ⼀个 slot 是实现不了的,那时⽤Render 函数就很⽅便了.</p><h2>实战 5:可⽤ Render ⾃定义列 的表格组件——Table</h2><p>*正规的表格,是由 <table>、<thead>、<tbody>、<tr>、<th>、<td> 这些标签 组成,⼀般分为表头 columns 和数据 data。<br> ⽀持使⽤ Render 函数来⾃定义某⼀列*。<br><em>写⼀个个的 table 系列标签是很麻烦并且重复的,⽽组件的好处就是省去这些基础的⼯作,我们直接给 Table 组件传递列的配置columns 和⾏数据 data,其余的都交给 Table 组件做了。</em><br>**columns:列配置,格式为数组,其中每⼀列 column 是⼀个<br>对象,⽤来描述这⼀列的信息,它的具体说明如下:**</p><pre><code>title:列头显示⽂字;
key:对应列内容的字段名;
render:⾃定义渲染列,使⽤ Vue 的 Render 函数,不
定义则直接显示为⽂本。</code></pre><p>*column 定义的 key 值,与 data 是⼀⼀对应的,这是⼀种常⻅的<br>数据接⼝定义规则,*<br>**data:显示的结构化数据,格式为数组,其中每⼀个对象,就<br>是⼀⾏的数据,**</p><p>先来完成⼀个基础的表格组件,之后再接⼊ Render 来配置⾃定<br>义列。<br>在 src/components ⽬录下新建 table-render ⽬录,并创建<br>table.vue ⽂件:</p><pre><code><template>
<table>
<thead>
<tr>
<th v-for="(col,i) in columns" :key="i">
{{col.title}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,index) in data" :key="index">
<td v-for="(col,inx) in columns" :key="inx">
<template v-if="'render' in col">
<Render :row="row" :column="col" :index="rowIndex" :render="col.render">
</Render>
</template>
<template v-else>
{{row[col.key]}}
</template>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import Render from './render.js'
export default {
props:{
columns:{
type:Array,
default(){
return []
}
},
data:{
type:Array,
default(){
return[]
}
}
},
components:{
Render
}
}
</script>
<style lang="scss" scoped>
table{
width:100%;
border-collapse:collapse;
border-spacing:0;
empty-cells:show;//隐藏表格中空单元格上的边框和背景:
border:1px solid #e9e9e9;
}
table th{
background: #f7f7f7;
color:#5c6b77;
font-weight:600;
white-space:nowrap;//段落中的文本不进行换行
}
table td, table th{
padding:8px 16px;
border: 1px solid #e9e9e9;
text-align:center;
}
</style></code></pre><p>**新建路由 table-render,并在 src/views/ ⽬录下新建⻚⾯<br>table-render.vue**</p><p>*如果 columns 中的某⼀列配置了 render 字段,那就通过<br>render.js 完成⾃定义模板,否则以字符串形式渲染。⽐如对出⽣⽇<br>期这列显示为标准的⽇期格式,可以这样定义 column:*</p><pre><code><template>
<div>
<table-render :columns="columns" :data="data"></table-render>
</div>
</template>
<script>
// 整⾏数据编辑的功能。
// 操作这⼀列,默认是⼀个修改按钮,点击后,变为保存和取消两个按
// 钮,同时本⾏其它各列都变为了输⼊框,并且初始值就是刚才单元格
// 的数据。变为输⼊框后,可以任意修改单元格数据,点击保存按钮保
// 存整⾏数据,点击取消按钮,还原⾄修改前的数据。
// 当进⼊编辑状态时,每⼀列的输⼊框都要有⼀个临时的数据使⽤ vmodel 双向绑定来响应修改,所以在 data ⾥再声明四个数据:
import TableRender from './receive'
export default {
components: {
TableRender,
},
data() {
return {
editName: '', // 第⼀列输⼊框
editAge: '', // 第⼆列输⼊框
editBirthday: '', // 第三列输⼊框
editAddress: '', // 第四列输⼊框
// editIndex 默认给了 -1,也就是⼀个不存在的⾏号,当点击修改 按钮时,再将它置为正确的⾏号。
editIndex: -1, // 当前聚焦的输⼊框的⾏数
columns: [
// 除编辑列,其它各数据列都有两种状态:
// 1. 当 editIndex 等于当前⾏号 index 时,呈现输⼊框状态;
// 2. 当 editIndex 不等于当前⾏号 index 时,呈现默认数据。
// edit 根据 editIndex 呈现不同的节点,还是先看 else,直接
// 显示了对应字段的数据。在聚焦时(this.editIndex ===
// index),渲染⼀个 input 输⼊框,初始值 value 通过 render 的
// domProps 绑定了 row.name(这⾥也可绑定 editName),并监
// 听了 input 事件,将输⼊的内容,实时缓存在数据 editName 中,
// 供保存时使⽤。事实上,这⾥绑定的 value 和事件 input 就是语法
// 糖 v-model 在 Render 函数中的写法,在 template 中,经常写作
// <input v-model="editName">。
{
title: '姓名',
key: 'name',
render:(h,{row,index})=>{
let edit;
// 当前行为聚焦行时
if(this.editIndex == index){
edit = [h('input',{
domProps:{
value:row.name
},
on:{
input:(event)=>{
this.editName = event.target.value
}
}
})]
}else{
edit = row.name
}
return h('div',[edit])
}
},
{
title: '年龄',
key: 'age',
render:(h,{row,index})=>{
let editAge;
if(this.editIndex == index){
editAge = [h('input',{
domProps:{
value: row.age
},
on:{
input:(event)=>{
this.editAge = event.target.value
}
}
})]
}else{
editAge = row.age
}
return h('div',[editAge])
}
},
{
title: '出⽣⽇期',
key: 'birthday',
render: (h, { row, column, index }) => {
let editBirth;
if(this.editIndex == index){
editBirth = [h('input',{
domProps:{
value:row.birthday
},
on:{
input:(event)=>{
this.editBirthday = event.target.value
}
}
})]
}else{
const date = new Date(parseInt(row.birthday))
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
editBirth = `${year}-${month}-${day}`
}
return h('div', [editBirth])
},
},
{
title: '地址',
key: 'address',
render:(h,{row,index})=>{
let editAddress;
if(this.editIndex ==index){
editAddress=[h('input',{
domProps:{
value:row.address
},
on:{
input:(event)=>{
this.editAdress = event.target.value
}
}
})]
}else{
editAddress = row.address
}
return h('div',[editAddress])
}
},
// 先定义操作列的 render:
// 为默认是⾮
// 编辑状态,
// 也就是说 editIndex 还是 -1。当点击修改按钮时,把 render 中
// 第⼆个参数 { row } 中的各列数据赋值给了之前在 data 中声明的
// 4 个数据,这样做是因为之后点击取消按钮时,editName 等值已经
// 修改了,还没有还原,所以在开启编辑状态的同时,初始化各输⼊框
// 的值(当然也可以在取消时重置)。最后再把 editIndex 置为了对
// 应的⾏序号 { index },此时 render 的 if 条件
// this.editIndex === index 为真,编辑列变成了两个按钮:保
// 存和取消。点击保存,直接修改表格源数据 data 中对应的各字段
// 值,并将 editIndex 置为 -1,退出编辑状态;点击取消,不保存源
// 数据,直接退出编辑状态。
{
title: '操作',
render:(h,{row,index})=>{
//如果当前行是编辑状态,则渲染两个按钮
if(this.editIndex === index){
return [
h('button',{
on:{
click:()=>{
this.data[index].name = this.editName
this.data[index].age = this.editAge
this.data[index].address = this.editAddress
this.data[index].birthday = this.editBirthday
this.editIndex = -1
}
}
},'保存'),
h('button',{
style:{
marginLeft:'6px'
},
on:{
click:()=>{
this.editIndex = -1
}
}
},'取消')
]
}else{
// 当前行是默认状态,渲染为一个按钮
return h('button',{
on:{
click:()=>{
this.editName = row.name
this.editAge = row.age
this.editAddress = row.address
this.editBirthday = row.birthday
this.editIndex = index
}
}
},'修改')
}
}
},
],
data: [
{
name: '王⼩明',
age: 18,
birthday: '919526400000',
address: '北京市朝阳区芍药居',
},
{
name: '张⼩刚',
age: 25,
birthday: '696096000000',
address: '北京市海淀区⻄⼆旗',
},
{
name: '李⼩红',
age: 30,
birthday: '563472000000',
address: '上海市浦东新区世纪⼤道',
},
{
name: '周⼩伟',
age: 26,
birthday: '687024000000',
address: '深圳市南⼭区深南⼤道',
},
],
}
},
}
</script>
<style lang="scss" scoped></style></code></pre><p>*columns ⾥定义的 render,是有两个参数的,第⼀ 个是 createElement(即 h),</p><pre><code> 第⼆个是从 render.js 传过来的对象,</code></pre><p>它包含了当前⾏数据(row)、当前列配置(column)、当前<br>是第⼏⾏(index),使⽤者可以基于这 3 个参数得到任意想要的<br>结果。由于是⾃定义列了,显示什么都是使⽤者决定的,因此在使⽤<br>了 render 的 column ⾥可以不⽤写字段 key 。*<br>*columns ⾥定义的 render 字段,它<br>仅仅是名字叫 render 的⼀个普通函数,并⾮ Vue.js 实例的 render<br>选项,只是我们恰巧把它叫做 render ⽽已,如果愿意,也可以改为<br>其它名字,⽐如 renderRow。真正的 Render 函数只有⼀个地⽅,<br>那就是 render.js 中的 render 选项,只是它代理了 column 中的<br>render*。</p><p>*显示正常的⽇期 写⼀个计算属性(computed) 但对于操作这⼀列就不可取了,因为它带有业<br>务逻辑,点击编辑按钮,是可以对当前⾏数据进⾏修改的。这时就要 ⽤到 Render 函数。*<br><strong>先在 src/components/table-render ⽬录下新建 render.js ⽂件</strong><br>*使⽤ Render ⾃定义列模板<br>函数式组件 Functional Render 的⽤法,它 没有状态和上下⽂,主要⽤于中转⼀个组件*</p><pre><code>// render.js 定义了 4 个 props:
// row:当前⾏的数据;
// column:当前列的数据;
// index:当前是第⼏⾏;
// render:具体的 render 函数内容。
// 这⾥的 render 选项并没有渲染任何节点,⽽是直接返回 props 中
// 定义的 render,并将 h 和当前的⾏、列、序号作为参数传递出去。
// 然后在 table.vue ⾥就可以使⽤ render.js 组件:
export default {
functional: true,
props:{
row:Object,
column:Object,
index:Number,
render:Function
},
render:(h,ctx)=>{
const params = {
row:ctx.props.row,
column:ctx.props.column,
index:ctx.props.index
};
return ctx.props.render(h,params)
}
}</code></pre><p>*⼀个完整的 Table 组件功能要复杂的多,⽐如排序、筛选、列<br>固定、表头固定、表头嵌套等*<br>**很多 Vue.js 的开发难题,都可以⽤ Render 函数来解决,<br>它⽐ template 模板更灵活,可以完全发挥 JavaScript 的编程能<br>⼒,因此很多 JS 的开发思想都可以借鉴。**</p><h2>实战 6:可⽤ slot-scope ⾃定 义列的表格组件——Table</h2><p>Render 函数能够完全发挥 JavaScript 的编程能⼒,实<br>现⼏乎所有的⾃定义⼯作,但本质上,使⽤者写的是⼀个庞⼤的 JS<br>对象,它不具备 DOM 结构,可读性和可维护性都⽐较差。实现⼀种达到同样渲染效果,但对使⽤者更友好的 slot-scope<br>写法。<br>常规的 slot ⽆法实现对组件循环体的每⼀项进⾏不同的内容分发,<br>这就要⽤到 slot-scope,它本质上跟 slot ⼀样,只不过可以传递参<br>数。</p><pre><code><ul>
<li v-for="book in books" :key="book.id">
<slot :book="book">
{{book.name}}
</slot>
</li>
</ul></code></pre><p>*在 slot 上,传递了⼀个⾃定义的参数 book,它的值绑定的是当前<br>循环项的数据 book,这样在⽗级使⽤时,就可以在 slot 中访问它<br>了:*</p><pre><code> <table-list>
<template slot-scope="slotProps">
<span v-if="slotProps.book.scale">限时优惠</span>
{{slotProps.book.name}}
</template>
</table-list></code></pre><p>*除了可以传递参数,其它⽤法跟 slot 是⼀样的,⽐如也可以“具<br>名”:使⽤ ES6 的解构,能让参数使⽤起来更⽅便*</p><pre><code><slot :book="book" name="book">
{{ book.name }}
</slot>
// ES6 的解构
<template slot-scope="{ book }" slot="book">
<span v-if="book.sale">限时优惠</span>
{{ book.name }}
</template></code></pre><p>*这就是作⽤域 slot(slot-scope),能够在组件的循环体中做内容<br>分发,有了它,Table 组件的⾃定义列模板就不⽤写⼀⻓串的<br>Render 函数了,slot-scope作用域插槽实现了父组件调用子组件内部的数据,子组件的数据通过slot-scope属性传递到了父组件*<br><strong>⽤ 3 种⽅法来改写 Table,实现 slot-scope ⾃定义列模板</strong><br><strong>第⼀种⽅案,</strong><br>⽤最简单的 slot-scope 实现,同时也兼容 Render 函<br>数的旧⽤法。拷⻉上⼀节的 Table 组件⽬录,更名为 tableslot,同时也拷⻉路由,更名为 table-slot.vue。为了兼容旧<br>的 Render 函数⽤法,在 columns 的列配置 column 中,新增⼀个<br>字段 slot 来指定 slot-scope 的名称:</p><pre><code><!-- src/components/table-slot/table.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="(col,i) in columns" :key="i">
{{col.title}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,index) in data" :key="index">
<td v-for="(col,inx) in columns" :key="inx">
<template v-if="'render' in col">
<Render :row="row" :column="col" :index="rowIndex" :render="col.render">
</Render>
</template>
<template v-else-if="'slot' in col">
<slot :index="rowIndex" :name="row.slot" :column="col">
</slot>
</template>
<template v-else>
{{row[col.key]}}
</template>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import Render from './render.js'
export default {
props:{
columns:{
type:Array,
default(){
return []
}
},
data:{
type:Array,
default(){
return[]
}
}
},
components:{
Render
}
}
</script>
<style lang="scss" scoped>
table{
width:100%;
border-collapse:collapse;
border-spacing:0;
empty-cells:show;//隐藏表格中空单元格上的边框和背景:
border:1px solid #e9e9e9;
}
table th{
background: #f7f7f7;
color:#5c6b77;
font-weight:600;
white-space:nowrap;//段落中的文本不进行换行
}
table td, table th{
padding:8px 16px;
border: 1px solid #e9e9e9;
text-align:center;
}
</style>
</code></pre><p>*相⽐原先的⽂件,只在 'render' in col 的条件下新加了⼀个<br>template 的标签,如果使⽤者的 column 配置了 render 字段,<br>就优先以 Render 函数渲染,然后再判断是否⽤ slot-scope 渲染。<br>在定义的作⽤域 slot 中,将⾏数据 row、列数据 column 和第⼏⾏<br>index 作为 slot 的参数,并根据 column 中指定的 slot 字段值,<br>动态设置了具名 name。使⽤者在配置 columns 时,只要指定了某<br>⼀列的 slot,那就可以在 Table 组件中使⽤ slot-scope。我们以上<br>⼀节的可编辑整⾏数据为例,⽤ slot-scope 的写法实现完全⼀样的<br>效果:*</p><pre><code><!-- src/views/table-slot.vue -->
<template>
<div>
<table-slot :columns="columns" :data="data">
<template slot-scope="{ row, index }" slot="name">
<input type="text" v-model="editName" v-if="editIndex === index" />
<span v-else>{{ row.name }}</span>
</template>
<template slot-scope="{ row, index }" slot="age">
<input type="text" v-model="editAge" v-if="editIndex === index" />
<span v-else>{{ row.age }}</span>
</template>
<template slot-scope="{ row, index }" slot="birthday">
<input type="text" v-model="editBirthday" v-if="editIndex === index" />
<span v-else>{{ getBirthday(row.birthday) }}</span>
</template>
<tempalte slot-scope="{ row, index }" slot="address">
<input type="text" v-model="editAddress" v-if="editIndex === index" />
<span v-else>{{ row.address }}</span>
</tempalte>
<template slot-scope="{ row, index }" slot="action">
<div v-if="editIndex === index">
<button @click="handleSave(index)">保存</button>
<button @click="editIndex = -1">取消</button>
</div>
<div v-else>
<button @click="handleEdit(row, index)">操作</button>
</div>
</template>
</table-slot>
</div>
</template>
<script>
import TableSlot from './table.vue'
export default {
components: {
TableSlot,
},
data() {
return {
editName: '', // 第⼀列输⼊框
editAge: '', // 第⼆列输⼊框
editBirthday: '', // 第三列输⼊框
editAddress: '', // 第四列输⼊框
editIndex: -1, // 当前聚焦的输⼊框的⾏数
columns: [
{
title: '姓名',
slot: 'name',
},
{
title: '年龄',
slot: 'age',
},
{
title: '出⽣⽇期',
slot: 'birthday',
},
{
title: '地址',
slot: 'address',
},
{
title: '操作',
slot: 'action',
},
],
data: [
{
name: '王⼩明',
age: 18,
birthday: '919526400000',
address: '北京市朝阳区芍药居',
},
{
name: '张⼩刚',
age: 25,
birthday: '696096000000',
address: '北京市海淀区⻄⼆旗',
},
{
name: '李⼩红',
age: 30,
birthday: '563472000000',
address: '上海市浦东新区世纪⼤道',
},
{
name: '周⼩伟',
age: 26,
birthday: '687024000000',
address: '深圳市南⼭区深南⼤道',
},
],
}
},
methods: {
handleEdit(row,index) {
this.editName = row.name
this.editAge = row.age
this.editAddress = row.address
this.editBirthday = row.birthday
this.editIndex = index
},
handleSave(index){
this.data[index].name = this.editBirthday
this.data[index].age = this.editAge
this.data[index].address = this.editAddress
this.data[index].birtyhday = this.editBirthday
this.editIndex = -1
},
getBirthday(birthday){
const date = new Date(parseInt(birthday))
const year = date.getFullYear()
const month = date.getMonth()+1
const day = date.getDate()
return `${year}-${month}-${day}`
}
},
}
</script>
<style lang="scss" scoped></style>
</code></pre><p>*<table-slot> 内的每⼀个 <template> 就对应某⼀列<br>的 slot-scope 模板,通过配置的 slot 字段,指定具名的 slotscope。可以看到,基本是把 Render 函数还原成了 html 的写法,<br>这样看起来直接多了,渲染效果是完全⼀样的。在 slot-scope 中,<br>平时怎么写组件,这⾥就怎么写,Vue.js 所有的 API 都是可以直接<br>使⽤的。*</p><p><strong>第⼆种⽅案</strong>,<br>不需要修改原先的 Table 组件代码,只是在使⽤层⾯<br>修改即可。先来看具体的使⽤代码,然后再做分析。注意,这⾥使⽤<br>的 Table 组件,仍然是上⼀节 src/components/table-render<br>的组件,它只有 Render 函数,没有定义 slot-scope:</p><pre><code><!-- src/views/table-render.vue -->
<template>
<div>
<table-render ref="table" :columns="columns" :data="data">
<template slot-scope="{ row, index }" slot="name">
<input type="text" v-model="editName" v-if="editIndex === index" />
<span v-else>{{ row.name }}</span>
</template>
<template slot-scope="{ row, index }" slot="age">
<input type="text" v-model="editAge" v-if="editIndex === index" />
<span v-else>{{ row.age }}</span>
</template>
<template slot-scope="{ row, index }" slot="birthday">
<input type="text" v-model="editBirthday" v-if="editIndex === index" />
<span v-else>{{ getBirthday(row.birthday) }}</span>
</template>
<tempalte slot-scope="{ row, index }" slot="address">
<input type="text" v-model="editAddress" v-if="editIndex === index" />
<span v-else>{{ row.address }}</span>
</tempalte>
<template slot-scope="{ row, index }" slot="action">
<div v-if="editIndex === index">
<button @click="handleSave(index)">保存</button>
<button @click="editIndex = -1">取消</button>
</div>
<div v-else>
<button @click="handleEdit(row, index)">操作</button>
</div>
</template>
</table-render>
</div>
</template>
<script>
import TableRender from './table.vue'
export default {
components: {
TableRender,
},
data() {
return {
editName: '', // 第⼀列输⼊框
editAge: '', // 第⼆列输⼊框
editBirthday: '', // 第三列输⼊框
editAddress: '', // 第四列输⼊框
editIndex: -1, // 当前聚焦的输⼊框的⾏数
columns: [
{
title: '姓名',
render:(h,{row,column,index})=>{
// $scopedSlots获取到父组件中创建的插槽 向插槽内传递参数 name插槽传递
return h('div',this.$refs.table.$scopedSlots.name({
row:row,
column:column,
index:index
}))
}
},
{
title: '年龄',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.age({
row:row,
column:column,
index:index
}))
}
},
{
title: '出⽣⽇期',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.birthday({
row:row,
column:column,
index:index
}))
}
},
{
title: '地址',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.address({
row:row,
column:column,
index:index
}))
}
},
{
title: '操作',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots({
row:row,
column:column,
index:index
}))
}
},
],
data: [
{
name: '王⼩明',
age: 18,
birthday: '919526400000',
address: '北京市朝阳区芍药居',
},
{
name: '张⼩刚',
age: 25,
birthday: '696096000000',
address: '北京市海淀区⻄⼆旗',
},
{
name: '李⼩红',
age: 30,
birthday: '563472000000',
address: '上海市浦东新区世纪⼤道',
},
{
name: '周⼩伟',
age: 26,
birthday: '687024000000',
address: '深圳市南⼭区深南⼤道',
},
],
}
},
methods: {
handleEdit(row,index) {
this.editName = row.name
this.editAge = row.age
this.editAddress = row.address
this.editBirthday = row.birthday
this.editIndex = index
},
handleSave(index){
this.data[index].name = this.editBirthday
this.data[index].age = this.editAge
this.data[index].address = this.editAddress
this.data[index].birtyhday = this.editBirthday
this.editIndex = -1
},
getBirthday(birthday){
const date = new Date(parseInt(birthday))
const year = date.getFullYear()
const month = date.getMonth()+1
const day = date.getDate()
return `${year}-${month}-${day}`
}
},
}
</script>
<style lang="scss" scoped></style>
</code></pre><p>在 slot-scope 的使⽤上(即 template 的内容),与⽅案⼀是完全<br>⼀致的,可以看到,在 column 的定义上,仍然使⽤了 render 字<br>段,只不过每个 render 都渲染了⼀个 div 节点,⽽这个 div 的内<br>容,是指定来在 <table-render> 中定义的 slot-scope:</p><pre><code><!-- src/views/table-render.vue -->
<template>
<div>
<table-render ref="table" :columns="columns" :data="data">
<template slot-scope="{ row, index }" slot="name">
<input type="text" v-model="editName" v-if="editIndex === index" />
<span v-else>{{ row.name }}</span>
</template>
<template slot-scope="{ row, index }" slot="age">
<input type="text" v-model="editAge" v-if="editIndex === index" />
<span v-else>{{ row.age }}</span>
</template>
<template slot-scope="{ row, index }" slot="birthday">
<input type="text" v-model="editBirthday" v-if="editIndex === index" />
<span v-else>{{ getBirthday(row.birthday) }}</span>
</template>
<tempalte slot-scope="{ row, index }" slot="address">
<input type="text" v-model="editAddress" v-if="editIndex === index" />
<span v-else>{{ row.address }}</span>
</tempalte>
<template slot-scope="{ row, index }" slot="action">
<div v-if="editIndex === index">
<button @click="handleSave(index)">保存</button>
<button @click="editIndex = -1">取消</button>
</div>
<div v-else>
<button @click="handleEdit(row, index)">操作</button>
</div>
</template>
</table-render>
</div>
</template>
<script>
import TableRender from './table.vue'
export default {
components: {
TableRender,
},
data() {
return {
editName: '', // 第⼀列输⼊框
editAge: '', // 第⼆列输⼊框
editBirthday: '', // 第三列输⼊框
editAddress: '', // 第四列输⼊框
editIndex: -1, // 当前聚焦的输⼊框的⾏数
columns: [
{
title: '姓名',
render:(h,{row,column,index})=>{
// $scopedSlots获取到父组件中创建的插槽 插槽内容放在div标签中,向插槽内传递参数 name插槽传递
return h('div',this.$refs.table.$scopedSlots.name({
row:row,
column:column,
index:index
}))
}
},
{
title: '年龄',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.age({
row:row,
column:column,
index:index
}))
}
},
{
title: '出⽣⽇期',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.birthday({
row:row,
column:column,
index:index
}))
}
},
{
title: '地址',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.address({
row:row,
column:column,
index:index
}))
}
},
{
title: '操作',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots({
row:row,
column:column,
index:index
}))
}
},
],
data: [
{
name: '王⼩明',
age: 18,
birthday: '919526400000',
address: '北京市朝阳区芍药居',
},
{
name: '张⼩刚',
age: 25,
birthday: '696096000000',
address: '北京市海淀区⻄⼆旗',
},
{
name: '李⼩红',
age: 30,
birthday: '563472000000',
address: '上海市浦东新区世纪⼤道',
},
{
name: '周⼩伟',
age: 26,
birthday: '687024000000',
address: '深圳市南⼭区深南⼤道',
},
],
}
},
methods: {
handleEdit(row,index) {
this.editName = row.name
this.editAge = row.age
this.editAddress = row.address
this.editBirthday = row.birthday
this.editIndex = index
},
handleSave(index){
this.data[index].name = this.editBirthday
this.data[index].age = this.editAge
this.data[index].address = this.editAddress
this.data[index].birtyhday = this.editBirthday
this.editIndex = -1
},
getBirthday(birthday){
const date = new Date(parseInt(birthday))
const year = date.getFullYear()
const month = date.getMonth()+1
const day = date.getDate()
return `${year}-${month}-${day}`
}
},
}
</script>
<style lang="scss" scoped></style>
</code></pre><p>在 slot-scope 的使⽤上(即 template 的内容),与⽅案⼀是完全<br>⼀致的,可以看到,在 column 的定义上,仍然使⽤了 render 字<br>段,只不过每个 render 都渲染了⼀个 div 节点,⽽这个 div 的内<br>容,是指定来在 <table-render> 中定义的 slot-scope:</p><pre><code> {
title: '姓名',
render:(h,{row,column,index})=>{
// $scopedSlots获取到父组件中创建的插槽 插槽内容放在div标签中,向插槽内传递参数 name插槽传递
return h('div',this.$refs.table.$scopedSlots.name({
row:row,
column:column,
index:index
}))
}
},</code></pre><p>*name 这⼀列仍然是使⽤ Functional Render,只不过 Render 的是⼀个预先定义好的slot-scope 模板<br>有⼀点需要注意的是,示例中的 data 默认是空数组,⽽在<br>mounted ⾥才赋值的,是因为这样定义的 slot-scope,初始时读<br>取 this.$refs.table.$scopedSlots 是读不到的,会报错,当<br>没有数据时,也就不会去渲染,也就避免了报错。<br>这种⽅案虽然可⾏,但归根到底是⼀种 hack,不是⾮常推荐*</p><p><strong>⽅案三</strong><br>第 3 中⽅案的思路和第 2 种是⼀样的,它介于⽅案 1 与⽅案 2 之<br>间。这种⽅案要修改 Table 组件代码,但是⽤例与⽅案 1 完全⼀<br>致。<br>在⽅案 2 中,我们是通过修改⽤例使⽤ slot-scope 的,也就是说<br>Table 组件本身没有⽀持 slot-scope,是我们“强加”上去的,如果<br>把强加的部分,集成到 Table 内,那对使⽤者就很友好了,同时也<br>避免了初始化报错,不得不把 data 写在 mounted 的问题。<br>保持⽅案 1 的⽤例不变,修改 src/components/table-render<br>中的代码。为了同时兼容 Render 与 slot-scope,我们在 tablerender 下新建⼀个 slot.js 的⽂件:</p><pre><code>export default {
functional:true,
inject:['tableRoot'],
props:{
row:Object,
column:Object,
index:Number
},
render:(h,ctx)=>{
return h('div',ctx.inject.tableRoot.$scopedSlots[ctx.props.column.slot]{
row:ctx.props.row,
column:ctx.props.column,
index:ctx.props.index
})
}
}</code></pre><p>它仍然是⼀个 Functional Render,使⽤ inject 注⼊了⽗级组件<br>table.vue(下⽂改写) 中提供的实例 tableRoot。在 render<br>⾥,也是通过⽅案 2 中使⽤ $scopedSlots 定义的 slot,不过这<br>是在组件级别定义,对⽤户来说是透明的,只要按⽅案 1 的⽤例来<br>写就可以了。<br>table.vue 也要做⼀点修改:</p><pre><code><template>
<table>
<thead>
<tr>
<th v-for="(col,i) in columns" :key="i">
{{col.title}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,rowIndex) in data" :key="rowIndex">
<td v-for="(col,inx) in columns" :key="inx">
<template v-if="'render' in col">
<Render :row="row" :column="col" :index="rowIndex" :render="col.render">
</Render>
</template>
<template v-else-if="'slot' in col">
<slot-scope :index="rowIndex" :name="row.slot" :column="col">
</slot-scope>
</template>
<template v-else>
{{row[col.key]}}
</template>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import Render from './render.js'
import SlotScope from './slot.js'
export default {
provide(){
return {
tableRoot:this
}
},
props:{
columns:{
type:Array,
default(){
return []
}
},
data:{
type:Array,
default(){
return[]
}
}
},
components:{
Render,
SlotScope
}
}
</script>
<style lang="scss" scoped>
table{
width:100%;
border-collapse:collapse;
border-spacing:0;
empty-cells:show;//隐藏表格中空单元格上的边框和背景:
border:1px solid #e9e9e9;
}
table th{
background: #f7f7f7;
color:#5c6b77;
font-weight:600;
white-space:nowrap;//段落中的文本不进行换行
}
table td, table th{
padding:8px 16px;
border: 1px solid #e9e9e9;
text-align:center;
}
</style>
</code></pre><p>*因为 slot-scope 模板是写在 table.vue 中的(对使⽤者来说,相当<br>于写在组件 <table-slot></table-slot> 之间),所以在<br>table.vue 中使⽤ provide 向下提供了 Table 的实例,这样在<br>slot.js 中就可以通过 inject 访问到它,继⽽通过 $scopedSlots<br>获取到 slot。需要注意的是,在 Functional Render 是没有 this 上<br>下⽂的,都是通过 h 的第⼆个参数临时上下⽂ ctx 来访问 prop、<br>inject 等的。<br>⽅案 3 也是推荐使⽤的,当 Table 的功能⾜够复杂,层级会嵌套的<br>⽐较深,那时⽅案 1 的 slot 就不会定义在第⼀级组件中,中间可能<br>会隔许多组件,slot 就要⼀层层中转,相⽐在任何地⽅都能直接使<br>⽤的 Render 就要麻烦了。所以,如果你的组件层级简单,推荐⽤第<br>⼀种⽅案;如果你的组件已经成型(某 API 基于 Render 函数),<br>但⼀时间不⽅便⽀持 slot-scope,⽽使⽤者⼜想⽤,那就选⽅案<br>2;如果你的组件已经成型(某 API 基于 Render 函数),但组件层<br>级复杂,要按⽅案 1 那样⽀持 slot-scope 可能改动较⼤,还有可<br>能带来新的 bug,那就⽤⽅案 3,它不会破坏原有的任何内容,但<br>会额外⽀持 slot-scope ⽤法,关键是改动简单*。</p><p><strong>理论上,绝⼤多数能⽤ Render 的地⽅,都可以⽤ slot-scope</strong></p><h2>动态组件</h2><p>*根据⼀些条件,动态地切换某个组件,或动态地选择渲染某个组件<br>使⽤ Render 或 Functional Render 可以解决动<br>态切换组件的需求,不过那是基于⼀个 JS 对象(Render 函数),<br>⽽ Vue.js 提供了另外⼀个内置的组件 <component> 和 is 特性,<br>可以更好地实现动态组件*<br>先来看⼀个 <component> 和 is 的基本示例,⾸先定义三个普通组<br>件:</p><pre><code><!--
* @Author: yang
* @Date: 2020-10-28 17:45:13
* @LastEditors: yang
* @LastEditTime: 2020-10-28 17:53:43
* @FilePath: \gloud-h5\src\component\index\a.vue
-->
<template>
<div>
a组件
</div>
</template>
<script>
export default {
beforeDestroy () {
console.log('组件销毁');
},
mounted () {
console.log('组件创建');
},
}
</script>
<style lang="scss" scoped>
</style></code></pre><pre><code><!--
* @Author: yang
* @Date: 2020-10-28 17:45:17
* @LastEditors: yang
* @LastEditTime: 2020-10-28 17:45:46
* @FilePath: \gloud-h5\src\component\index\b.vue
-->
<template>
<div>
b组件
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>
</code></pre><pre><code><!--
* @Author: yang
* @Date: 2020-10-28 17:45:22
* @LastEditors: yang
* @LastEditTime: 2020-10-28 17:45:54
* @FilePath: \gloud-h5\src\component\index\c.vue
-->
<template>
<div>
c组件
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style></code></pre><p><strong>然后在⽗组件中导⼊这 3 个组件,并动态切换:</strong><br>只要切换到 A 组件,mounted 就会触发⼀次,切换到其它组<br>件,beforeDestroy 也会触发⼀次,说明组件再重新渲染,这样<br>有可能导致性能问题。为了避免组件的重复渲染,可以在<br><component> 外层套⼀个 Vue.js 内置的 <keep-alive> 组件,<br>这样,组件就会被缓存起来:<br>*keep-alive 还有⼀些额外的 props 可以配置:<br>include:字符串或正则表达式。只有名称匹配的组件会被缓<br>存。<br>exclude:字符串或正则表达式。任何名称匹配的组件都不会<br>被缓存。<br>max:数字。最多可以缓存多少组件实例。*</p><pre><code><template>
<div>
<button @click="handleChange('a')">显示组件a</button>
<button @click="handleChange('b')">显示组件b</button>
<button @click="handleChange('c')">显示组件c</button>
<keep-alive>
<component :is="component"></component>
</keep-alive>
</div>
</template>
<script>
import a from './a.vue';
import b from './b.vue';
import c from './c.vue';
export default {
data() {
return {
component: a
}
},
methods: {
handleChange(item) {
switch(item){
case 'a':
this.component = a
break;
case 'b':
this.component = b
break;
case 'c':
this.component = c
break;
}
}
},
}
</script>
<style lang="scss" scoped>
</style
</code></pre><p>这⾥的 is 动态绑定的是⼀个组件对象(Object),它直接指向 a /<br>b / c 三个组件中的⼀个</p><h5><strong>动态渲染标签</strong></h5><p>除了直接绑定⼀个 Object,还可以是⼀ 个 String,⽐如标签名、组件名。下⾯的这个组件,将原⽣的按钮 button 进⾏了封装,如果传⼊了 prop: to,那它会渲染为⼀个 标签,⽤于打开这个链接地址,如果没有传⼊ to,就当作普通 button 使⽤。来看下⾯的示例:</p><p><strong>button.vue</strong></p><pre><code><template>
<component :is="tagName" v-bind="tagProps">
<slot></slot>
</component>
</template>
<script>
export default {
props:{
to:{
type:String,
default:""
},
target:{
type:String,
default:'_self'
}
},
computed: {
// 动态渲染不同的标签
tagName() {
return this.to === ''?'button':'a'
},
// 如果是链接,把这些属性绑定到component上
tagProps(){
let props = {}
if(this.to){
props={
target:this.target,
href:this.to
}
}
return props
}
},
}
</script>
<style lang="scss" scoped>
</style></code></pre><p><strong>使用</strong></p><pre><code><template>
<div>
<i-button>普通按钮</i-button>
<i-button to="http://www.baidu.com">链接按钮</i-button>
<i-button target="_bank" to="http://www.baidu.com">新窗口打开链接</i-button>
</div>
</template>
<script>
import iButton from './button.vue';
export default {
components: {
iButton,
},
}
</script>
<style lang="scss" scoped>
</style></code></pre><p>i-button 组件中的 <component> is 绑定的就是⼀个标签名称<br>button / a,并且通过 v-bind 将⼀些额外的属性全部绑定到了<br><component> 上。</p><h2>递归组件</h2><p>*递归组件就是指组件在模板中调⽤⾃⼰,开启递归组件的必要条件,<br>就是在组件中设置⼀个 name 选项。*<br>在 Webpack 中导⼊⼀个 Vue.js 组件,⼀般是通过 import<br>myComponent from 'xxx' 这样的语法,然后在当前组件(⻚<br>⾯)的 components: { myComponent } ⾥注册组件。这种组件<br>是不强制设置 name 字段的,组件的名字都是使⽤者在 import 进来<br>后⾃定义的,但递归组件的使⽤者是组件⾃身,它得知道这个组件叫<br>什么,因为没有⽤ components 注册,所以 name 字段就是必须的<br>了。除了递归组件⽤ name,⽤⼀些特殊的⽅法,通过遍历匹配组件的 name 选项来寻找组件实例。</p><pre><code><template>
<div>
<my-component></my-component>
</div>
</template>
<script>
export default {
name: 'my-component'
}
</script>
<style lang="scss" scoped>
</style></code></pre><p>*不过呢,上⾯的示例是有问题的,如果直接运⾏,会抛出 max<br>stack size exceeded 的错误,因为组件会⽆限递归下去,死循<br>环。解决这个问题,就要给递归组件⼀个限制条件,⼀般会在递归组<br>件上⽤ v-if 在某个地⽅设置为 false 来终结。*<br>⽐如我们给上⾯的示例加⼀个属性 count,当⼤于 5 时就不再递归:</p><pre><code><template>
<div>
<my-component :count="count+1" v-if="count<=5"></my-component>
哈哈哈
</div>
</template>
<script>
export default {
name: 'my-component',
props:{
count:{
type:Number,
default:1
}
}
}
</script>
<style lang="scss" scoped>
</style></code></pre><p>实现⼀个递归组件的必要条件是:<br>1.要给组件设置 name;<br>2.要有⼀个明确的结束条件<br>递归组件常⽤来开发具有未知层级关系的独⽴组件<br>⽐如常⻅的有级联选择器和树形控件:<br>这类组件⼀般都是数据驱动型的,⽗级有⼀个字段 children,然后递归<br><strong>assist.js</strong></p><pre><code>/*
* @Author: yang
* @Date: 2020-10-29 19:21:02
* @LastEditors: yang
* @LastEditTime: 2020-11-03 13:37:07
* @FilePath: \gloud-h5demo\src\component\index\utils\assist.js
*/
import Vue from 'vue'
// 当前Vue实例是否运行于服务器,属性值为true表示实例运行于服务器,每个Vue实例都可以通过该属性判断。该属性一般用于服务器渲染,用以区分代码是否在服务器上运行。
// vue脚手架,运行的项目的这个值为false
const isServer = Vue.prototype.$isServer
export function oneOf(value, validList) {
// for(let i = 0;i<validList.length;i++){
// if(value==validList[i]){
// return true
// }
// return false
// }
let bool = validList.some((item) => value === item)
return bool
}
// 驼峰结构
export function camelcaseToHyphen(str) {
// $1是第一个小括号里匹配的内容,$2是第二个小括号里面匹配的内容
return str.replace(/([a-z](A-Z))/g, '$1-$2').toLowerCase()
}
// 获取滚动条宽度
let cached
export function getScollBarSize(fresh) {
if (isServer) return 0
if (fresh || cached === undefined) {
const inner = document.createElement('div')
inner.style.width = '100%'
inner.style.height = '200px'
const outer = document.createElement('div')
const outerStyle = outer.style
outerStyle.position = 'absolute'
outerStyle.top = 0
outerStyle.left = 0
// 鼠标不会穿透当前层 元素永远不会成为鼠标事件的target
outerStyle.pointerEvents = 'none'
outerStyle.visibility = 'hidden'
outerStyle.width = '200px'
outerStyle.height = '150px'
outerStyle.overflow = 'hidden'
outer.appendChild(inner)
document.body.appendChild(outer)
// offsetWidth 是对象的可见宽度,包滚动条等边线
const widthContained = outer.offsetWidth
outer.style.overflow = 'scroll'
let widthScroll = inner.offsetWidth
if (widthContained === widthScroll) {
//clientWidth 是对象可见的宽度,不包滚动条等边线
widthScroll = outer.clientWidth
}
document.body.removeChild(outer)
cached = widthContained - widthScroll
}
return cached
}
//watch dom change
// Mutation Observer API 用来监视 DOM 变动。比如节点的增减、属性的变动、文本内容的变动。MutationObserver使用observe方法进行监听指定的元素节点变化
export const MutationObserver = isServer
? false
: window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver ||
false
const SPECIAL_CHARS_REGEXP = /([\:\-\_])+(.)/g
const MOZ_HACK_REGEXP = /^moz(A-Z)/
function camelCase(name) {
return name
.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
return offset ? letter.toUpperCase() : letter
})
.replace(MOZ_HACK_REGEXP, 'Moz$1') //$1第一次匹配的结果
}
// getStyle 获取样式
export function getStyle(element, styleName) {
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'cssFloat'
}
try {
// document.defaultView.getComputedStyle()可以获取对象的css样式;他返回的是一个CSS样式对象。
// 使用:document.defaultView.getComputedStyle(a, b); a这对象是要想要获取的对象。 b,伪类,若果不是则为null。
const computed = document.defaultView.getComputedStyle(element, '')
return element.style[styleName] || computed ? computed[styleName] : null
} catch (e) {
return element.style[styleName]
}
}
// firstUpperCase 将首字母大写
function firstUpperCase(str) {
return str.toString()[0].toUpperCase() + str.toString().slice(1)
}
export { firstUpperCase }
// warn
export function warnProp(component, prop, correctType, wrongType) {
correctType = firstUpperCase(correctType)
wrongType = firstUpperCase(wrongType)
console.error(
//eslint-disable-line
`[iView warn]:Invalid prop:type check failed for prop ${prop},Expected ${correctType},got ${wrongType}.(found in component:${component})`
)
}
function typeOf(obj){
const toString = Object.prototype.toString
const map = {
'[object Boolean]':'boolean',
'[object Number]':'number',
'[object String]':'string',
'[object Function]':'function',
'[object Array]':'array',
'[object Date]':'date',
'[object RegExp]':'regExp',
'[object Undefined]':'undefined',
'[object Null]':'null',
'[object Object]':'object'
}
return map[toString.call(obj)]
}
//deepCopy
function deepCopy(data){
const t = typeOf(data)
let o;
if(t==='array'){
o = []
for(let i = 0;i<data.length;i++){
o.push(deepCopy(data[i]))
}
}else if(t === 'object'){
o = {}
for(let key in data){
o[key] = deepCopy(data[key])
}
}else{
return data
}
return o
}
export {deepCopy}
//scrollTop animation
// window.requestAnimationFrame() 这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数,以满足开发者操作动画的需要。即可以这样说,该方法接受一个函数为参,该函数会在重绘前调用。
// 回调函数会被传入一个参数,DOMHighResTimeStamp,指示requestAnimationFrame() 开始触发回调函数的当前时间
export function scrollTop(el,from=0,to,duration = 500,endCallback){
if(!window.requestAnimationFrame){
window.requestAnimationFrame = (
window.webkitRequestAnimationFrame||
window.mozRequestAnimationFrame||
window.msRequestAnimationFrame||
function (callback){
return window.setTimeout(callback,1000/60);
}
)
}
// 返回一个数的绝对值
const difference = Math.abs(from - to)
// 向上取整
const step = Math.ceil(difference/duration*50)
function scroll(start,end,step){
if(start === end){
endCallback &&endCallback()
return
}
let d = (start + step >end)?end:start+ step;
if(start>end){
d = (start - step< end)?end : start-step
}
if(el === window){
// 滚动到文档中的某个坐标
// 需要在页面打开后,定位滚动条的位置,比如,横向和纵向滚动条居中,实现页面滚动的方法有三种:scroll、scrollBy和 scrollTo,三个方法都带两个参数:x(X轴上的偏移量)和y(Y轴上的偏移量)。因此我们只需修改x,y的偏移量来设置滚动条的位置。
window.scrollTo(d,d)
}else{
// 规定相对滚动条顶部的偏移
el.scrollTop = d
}
window.requestAnimationFrame(()=>scroll(d,end,step))
}
scroll(from,to,step)
}
//Find components upward
function findComponentUpward(contxt,componentName,componentNames){
if(typeof componentName === 'string'){
componentNames = [componentName]
}else{
componentNames = componentName
}
let parent = context.$parent
// $options 可以获取自定义属性
let name = parent.$options.name
while(parent&&(!name||componentNames.indexoOf(name)<0)){
parent = parent.$parent
if(parent)name = parent.$options.name
}
return parent
}
export {findComponentUpward}
//Find component downward
export function findComponentDownward(context,componentName){
const childrens = context.$children
let children = null
if(childrens.length){
for(const child in childrens){
const name = child.$options.name
if(name === componentName){
children = child
break;
}else{
children = findComponentDownward(child,componentName)
if(children)break
}
}
}
return children
}
//Find compoents downward
export function findComponentsDownward(context,componentName,ignoreComponentNames = []){
// Array.isArray用于判断一个对象是否为数组。
if(!Array.isArray(ignoreComponentNames)){
ignoreComponentNames = [ignoreComponentNames]
}
// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
return context.$children.reduce((components,child)=>{
if(child.$options.name = componentName)components.push(child)
if(ignoreComponentNames.indexOf(child.$options.name)<0){
const foundChilds = findComponentsDownward(child,componentName)
return components.concat(foundChilds)
}else{
return components
}
},[])
}
//Find components upward
export function findComponentsUpward(context,componentName){
let parents = []
const parent = context.$parent
if(parent){
if(parent.$options.name === componentName)parents.push(parent)
return parents.concat(findComponentUpward(parent,componentName))
}else{
return []
}
}
// Find brothers components
export function findBrothersComponents(context,componentName,exceptMe = true){
let res = context.$parent.$children.filter(item=>{
return item.$options.name === componentName
})
let index = res.findIndex(item=>item._uid === context._uid)
// splice 从数组中添加或者删除项目,返回被删除的项目,同时也会改变原数组。
if(exceptMe) res.splice(index,1)
return res
}
//istanbul ignore next
// 去掉前后空格的
const trim = function(string){
// \s 匹配任何不可见字符,包括空格、制表符、换页符等等
// \s:空格 \uFEFF:字节次序标记字符(Byte Order Mark),也就是BOM,它是es5新增的空白符 \xA0:禁止自动换行空白符,相当于html中的&nbsp;
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g,'')
}
// istanbul igonre next
// 确定元素中是否包含指定的类名 返回值为true 、false
export function hasClass(el,cls){
if(!el || !cls) return false
if(cls.indexOf(' ')!==-1)throw new Error('className should not contain space.')
if(el.classList){
//classList.contains 确定元素中是否包含指定的类名 返回值为true 、false
return el.classList.contains(cls)
}else{
return(' ' + el.className + ' ').indexoOf(' ' + cls + ' ')>-1
}
}
export function addClass(el,cls){
if(!el) return
let curClass = el.className
const classes = (cls || '').split(' ')
for(let i = 0; i<classes.length;i++){
const clsName = classes[i]
if(!clsName) continue
if(el.classList){
el.classList.add(clsName)
}else{
if(!hasClass(el,clsName)){
curClass += ' ' + clsName
}
}
}
if(!el.classList){
el.className = curClass
}
}
// istanbul ignore next
export function removeClass(el,cls){
if(!el || !cls) return
const classes = cls.split(' ')
let curClass = " " + el.className + ' '
for(let i =0;i<classes.length;i++){
const clsName = classes[i]
if(!clsName) continue
if(el.classList){
el.classList.removeClass(clsName)
}else{
if(hasClass(el,clsName)){
curClass = curClass.replace(' ' + clsName + '',' ')
}
}
}
if(!el.classList){
el.className = trim(curClass)
}
}
export const dimensionMap = {
xs:'480px',
sm:'576px',
md:'768px',
lg:'992px',
xl:'1200px',
xxl:'1600px'
}
export function setMatchMedia(){
if(typeof window !== 'undefined'){
const matchMediaPolyfill = mediaQuery => {
return {
media:mediaQuery,
matches:false,
on(){},
off(){}
}
}
// matchMedia() 返回一个新的 MediaQueryList 对象,表示指定的媒体查询字符串解析后的结果。
// alert(window.matchMedia("(max-width:100px)").media); //(max-width: 100px)
window.matchMedia = window.matchMedia||matchMediaPolyfill
}
}
export const sharpMatcherRegx = /#([^#]+)$/</code></pre><p><strong>checkbox.vue</strong></p><pre><code><!--
* @Author: yang
* @Date: 2020-10-29 15:52:18
* @LastEditors: yang
* @LastEditTime: 2020-10-29 17:48:38
* @FilePath: \gloud-h5\src\component\index\checkbox.vue
-->
<template>
<label :class="wrapClasses">
<span :class="checkboxClasses">
<span :class="innerClasses"></span>
<input type="checkbox" v-if="group" :class="inputClasses" :disabled="disabled" :value="label" v-model="model" :name="name" @change="change" @focus="onFocus" @blur="onBlur">
<input type="checkbox" v-else :class="inputClasses" :disabled="disabled" :checked="currentValue" :name="name" @change="change" @focus="onFocus" @blur="onBlur">
</span>
<slot>
<span v-if="showSlot">{{label}}</span>
</slot>
</label>
</template>
<script>
import { findComponentUpward, oneOf } from './utils/assist'
import Emitter from './mixinx/emitter'
const prefixCls = 'ivu-checkbox'
export default {
mixins:[Emitter],
props:{
disabled:{
type:Boolean,
default: false
},
value:{
type:[String,Number,Boolean],
default:false
},
trueValue:{
type:[String,Number,Boolean],
default:true
},
falseValue:{
type:[String,Number,Boolean],
default:false
},
label:{
type:[String,Number,Boolean]
},
indeterminate:{
type:Boolean,
default:false
},
size:{
validator(value){
return oneOf(value, ['small','large','default'])
},
default(){
return !this.$IVIEW||this.$IVIEW.size===''?'default':this.$IVIEW.size;
}
},
name:{
type:String
}
},
data() {
return {
model: [],
currentValue:this.value,
group:false,
showSlot:true,
parent:findComponentUpward(this,'CheckboxGroup'),
focusInner:false
}
},
computed:{
wrapClasses(){
// class属性(数组语法) ,对象内如果右侧满足,则有这个class名称
return [
`${prefixCls}-wrapper`,
{
[`${prefixCls}-group-item`]:this.group,
[`${prefixCls}-wrapper-checked`]:this.currentValue,
[`${prefixCls}-wrapper-disabled`]:this.disabled,
[`${prefixCls}-${this.size}`]:!!this.size
}
]
},
checkboxClasses(){
return [
`${prefixCls}`,
{
[`${prefixCls}-checked`]:this.currentValue,
[`${prefixCls}-disabled`]:this.disabled,
[`${prefixCls}-indeterminate`]:this.indeterminate
}
]
},
innerClasses(){
return [
`${prefixCls}-inner`,
{
[`${prefixCls}-focus`]:this.focusInner
}
]
},
inputClasses(){
return `${prefixCls}-input`
}
},
mounted(){
this.parent = findComponentUpward(this,'CheckboxGroup');
if(this.parent){
this.group = true
}
if(this.group){
this.parent.updateModel(true)
}else{
this.updateModel()
this.showSlot = this.$slots.default!==undefined
}
},
methods: {
change(event) {
if(this.disabled){
return false
}
const checked = event.target.checked;
this.currentValue = checked
const value = checked?this.trueValue:this.falseValue
this.$emit('input',value)
if(this.group){
this.parent.change(this.model)
}else{
this.$emit('on-change',value)
this.dispatch('FormItem','on-form-change',value)
}
},
updateModel(){
this.currentValue = this.value===this.currentValue
},
onBlur(){
this.focusInner = false
},
onFocus(){
this.focusInner = true
}
},
watch:{
value(val){
if(val===this.currentValue||val===this.falseValue){
this.updateModel()
}else{
throw 'Value should be trueValue or falseValue.'
}
}
}
}
</script>
<style lang="scss" scoped></style>
</code></pre><p>*deepCopy 函数会递归地对数组或对象进⾏逐⼀判断,如果某项是<br>数组或对象,再拆分继续判断,⽽其它类型就直接赋值了,所以深拷<br>⻉的数据不会破坏原有的数据*</p><h5>tree.vue</h5><pre><code><template>
<div>
<tree-node v-for="(item,index)in cloneData" :key="index" :data="item" :show-checkbox="showCheckbox"></tree-node>
</div>
</template>
<script>
import TreeNode from './node.vue'
import {deepCopy} from './utils/assist.js'
export default {
name:tree,
components:{
TreeNode
},
props:{
data:{
type:Array,
default(){
return []
}
},
showCheckbox:{
type:Boolean,
default:false
}
},
data() {
return {
cloneData: []
}
},
created(){
this.rebuildData()
},
watch:{
data(){
this.rebuildData()
}
},
methods: {
rebuildData() {
this.cloneData = deepCopy(this.data)
},
emitEvent(eventName,data){
this.$emit(eventName,data,this.cloneData)
}
},
}
</script>
<style lang="scss" scoped>
</style></code></pre><p>*在组件 created 时(以及 watch 监听 data 改变时),调⽤了<br>rebuildData ⽅法克隆源数据,并赋值给了 cloneData。<br>在 template 中,先是渲染了⼀个 node.vue 组件(<treenode>),这⼀级是 Tree 的根节点,因为 cloneDate 是⼀个数<br>组,所以这个根节点不⼀定只有⼀项,有可能是并列的多项。不过这<br>⾥使⽤的 node.vue 还没有⽤到 Vue.js 的递归组件,它只处理第⼀<br>级根节点。*<br><tree-node> 组件(node.vue)接收两个 props:</p><ol><li>showCheckbox:与 tree.vue 的 showCheckbox 相同,只</li></ol><p>是进⾏传递;</p><ol><li>data:node.vue 接收的 data 是⼀个 Object ⽽⾮ Array,因</li></ol><p>为它只负责渲染当前的⼀个节点,并递归渲染下⼀个⼦节点<br>(即 children),所以这⾥对 cloneData 进⾏循环,将每⼀<br>项节点数据赋给了 tree-node。</p><h4>递归组件 node.vue</h4><p>node.vue 是树组件 Tree 的核⼼,⽽⼀个 tree-node 节点包含 4<br>个部分:</p><ol><li>展开与关闭的按钮(+或-);</li><li>多选框;</li><li>节点标题;</li><li>递归⼦节点。</li></ol><pre><code><template>
<ul class="tree-ul">
<li class="tree-li">
<span class="tree-expand" @click="handleExpand">
<span v-if="data.children&&data.children.length&&!data.expand">+</span>
<span v-if="data.children&&data.children.length&&data.expand">-</span>
</span>
<i-checkbox v-if="showCheckbox" :value="data.checked" @input="handleCheck"></i-checkbox>
<span>{{data.title}}</span>
<tree-node v-if="data.expand" v-for="(item,index)in data.children" :key="index" :data="item" :show-checkbox="showCheckbox"></tree-node>
</li>
</ul>
</template>
<script>
import iCheckbox from './checkbox'
export default {
name:'TreeNode',
components:{
iCheckbox
},
data() {
return {
tree: findComponentUpward(this,'tree')
}
},
props:{
data:{
type:Object,
default(){
return {}
}
},
showCheckbox:{
type:Boolean,
default:false
}
},
// 点击 + 号时,会展开直属⼦节点,点击 - 号关闭,这⼀步只需在
// handleExpand 中修改 data 的 expand 数据即可,同时,我们通
// 过 Tree 的根组件(tree.vue)触发⼀个⾃定义事件 @on-toggleexpand(上⽂已介绍):
methods: {
handleExpand() {
this.$set(this.data,'expand',!this.data.expand)
if(this.tree){
this.tree.emitEvent('on-toggle-expand',this.data)
}
},
handleCheck(checked){
this.updateTreeDown(this.data,checked)
if(this.tree){
this.tree.emitEvent('on-check-change',this.data)
}
},
updateTreeDown(data,checked){
this.$set(data,'checked',checked)
if(data.children&&data.children.length){
data.children.forEach(item=>{
this.updateTreeDown(item,checked)
})
}
}
},
watch:{
// node.vue 是⼀个递归组
// 件,那每⼀个组件⾥都会有 watch 监听 data.children,要知
// 道,当前的节点有两个”身份“,它既是下属节点的⽗节点,同时也是
// 上级节点的⼦节点,它作为下属节点的⽗节点被修改的同时,也会触
// 发上级节点中的 watch 监听函数。这就是递归。
'data.children':{
handler(data){
if(data){
const checkedAll = !data.some(item=>!item.checked)
this.$set(this.data,'checked',checkedAll)
}
},
deep:true
}
}
}
</script>
<style lang="scss" scoped>
.tree-ul,.tree-li{
list-style:none;
padding-left:10px;
}
.tree-expand{
cursor:pointer;
}
</style>
</code></pre><h3>v-model语法糖</h3><p>v-model 常⽤于表单元素上进⾏数据的双向绑定,⽐如 <input>。除了原⽣的元素,它还能在⾃定义组件中使⽤。<br> v-model 是⼀个语法糖,可以拆解为 props: value 和 events:<br>input。就是说组件必须提供⼀个名为 value 的 prop,以及名为<br>input 的⾃定义事件,满⾜这两个条件,使⽤者就能在⾃定义组件上<br>使⽤ v-model。<br>⽐如下⾯的示例,实现了⼀个数字选择器:<br><strong>input-number.vue</strong></p><pre><code> <template>
<div>
<button @click="increase(-1)">减一</button>
<span style="color:red;padding:6px">{{currentValue}}</span>
<button @click="increase(1)">加一</button>
</div>
</template>
<script>
export default {
name:'InputNumber',
props:{
value:{
type:Number
}
},
data(){
return{
currentValue:this.value
}
},
watch:{
value(value){
this.currentValue = this.value
}
},
methods:{
increase(val){
this.currentValue+=val
this.$emit('input',this.currentValue)
}
}
}
</script>
<style lang="scss" scoped>
</style></code></pre><p>*props ⼀般不能在组件内修改,它是通过⽗级修改的,因此实现 vmodel ⼀般都会有⼀个 currentValue 的内部 data,初始时从<br>value 获取⼀次值,当 value 修改时,也通过 watch 监听到及时更<br>新;组件不会修改 value 的值,⽽是修改 currentValue,同时将修<br>改的值通过⾃定义事件 input 派发给⽗组件,⽗组件接收到后,由<br>⽗组件修改 value。*<br>上面的数字选择器组件可以有下⾯两种使<br>⽤⽅式:<br><strong>默认情况下,一个组件上的 <code>v-model</code> 会把 <code>value</code> 用作 prop 且把 <code>input</code> 用作 event</strong><br><strong>写法一</strong></p><pre><code><template>
<div>
<input-number v-model="value"></input-number>
</div>
</template>
<script>
import inputNumber from './input-number'
export default {
components:{
inputNumber
},
data(){
return{
value:1
}
}
}
</script>
<style lang="scss" scoped>
</style></code></pre><p><strong>或者</strong></p><pre><code><template>
<div>
<input-number :value="value" @input="changeValue"></input-number>
</div>
</template>
<script>
import inputNumber from './input-number'
export default {
components:{
inputNumber
},
data(){
return{
value:1
}
},
methods: {
changeValue(val) {
this.value = val
}
},
}
</script>
<style lang="scss" scoped>
</style></code></pre><p>如果你不想⽤ value 和 input 这两个名字,从 Vue.js 2.2.0 版本<br>开始,提供了⼀个 model 的选项,可以指定它们的名字,根据写法一,所以数字选择器组件也可以这样写:</p><pre><code> <template>
<div>
<button @click="increase(-1)">减一</button>
<span style="color:red;padding:6px">{{currentValue}}</span>
<button @click="increase(1)">加一</button>
</div>
</template>
<!-- model选项,在定义组件的时候,指定prop的值和监听的事件 -->
<script>
export default {
name:'InputNumber',
props:{
number:{
type:Number
}
},
model:{
prop:'number',
event:'change'
},
data(){
return{
currentValue:this.number
}
},
watch:{
value(value){
this.currentValue = value
}
},
methods:{
increase(val){
this.currentValue+=val
this.$emit('number',this.currentValue)
}
}
}
</script>
<style lang="scss" scoped>
</style></code></pre><p>*在 model 选项⾥,就可以指定 prop 和 event 的名字了,⽽不⼀定<br>⾮要⽤ value 和 input,因为这两个名字在⼀些原⽣表单元素⾥,有<br>其它⽤处。*</p><h3>.sync 修饰符</h3><p>. 是⼀个语法糖,修改数据还是在⽗组件完成的,并⾮在⼦组件。<br>自动更新父组件属性的 v-on 监听器</p><p><input-number :value.sync="value"></input-number></p><p>会被扩展为</p><p><input-number:value="value"@update:value="val=>value=val"></input-number> </p><p>当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:</p><p>this.$emit('update:value', newValue)</p><p><strong>子组件</strong></p><pre><code> <template>
<div>
<button @click="increase(-1)">减一</button>
<span style="color:red;padding:6px">{{value}}</span>
<button @click="increase(1)">加一</button>
</div>
</template>
<script>
export default {
name:'InputNumber',
props:{
value:{
type:Number
}
},
methods:{
increase(val){
this.$emit('update:value',this.value+val)
}
}
}
</script>
<style lang="scss" scoped>
</style></code></pre><p><strong>父组件</strong></p><pre><code><template>
<div>
<!-- 自动更新父组件属性的 v-on 监听器 -->
<input-number :value.sync="value"></input-number>
<!-- 会被扩展为<input-number :value="value" @update:value="val=>value=val"></input-number>
当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:
this.$emit('update:value', newValue) -->
</div>
</template>
<script>
import inputNumber from './input-number'
export default {
components:{
inputNumber
},
data(){
return{
value:1
}
}
}
</script>
<style lang="scss" scoped>
</style></code></pre><p>看起来要⽐ v-model 的实现简单多,实现的效果是⼀样的。vmodel 在⼀个组件中只能有⼀个,但 .sync 可以设置很多个。.sync<br>虽好,但也有限制,⽐如:<br>不能和表达式⼀起使⽤(如 vbind:title.sync="doc.title + '!'" 是⽆效的);<br>不能⽤在字⾯量对象上(如 v-bind.sync="{ title:<br>doc.title }" 是⽆法正常⼯作的)。</p><h3>$set</h3><p>在上⼀节已经介绍过 $set,有两种情况会⽤到它:</p><ol><li>由于 JavaScript 的限制,Vue 不能检测以下变动的数组:</li><li>当利⽤索引直接设置⼀个项时,例</li></ol><p>如:this.items[index] = value;</p><ol><li>当修改数组的⻓度时,例如:vm.items.length =</li></ol><p>newLength。</p><ol><li>由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删</li></ol><p>除。</p><pre><code> data(){
return{
items:['q','a','c']
}
},
methods: {
handler() {
this.items[1] = 'x'//// 不是响应性的
}
},</code></pre><p><strong>使⽤ $set:</strong></p><pre><code> data(){
return{
items:['q','a','c']
}
},
methods: {
handler() {
this.$set(this.items,1,'X')//响应的
}
},</code></pre><p>另外,数组的以下⽅法,都是可以触发视图更新的,也就是响应性<br>的:<strong>push()、shift()、 unshift()、 sort()、 splice()、 pop()</strong></p><p>还有⼀种⼩技巧,就是先 copy ⼀个数组,然后通过 index 修改<br>后,再把原数组整个替换,⽐如:</p><pre><code> methods: {
handler() {
let data = [...this.items]
data[1] = '11'
this.items = data
}
},</code></pre><h3>计算属性的 set</h3><p>计算属性(computed)很简单,⽽且也会⼤量使⽤,但⼤多数时<br>候,我们只是⽤它默认的 get ⽅法,也就是平时的常规写法,通过<br>computed 获取⼀个依赖其它状态的数据。⽐如:</p><pre><code>computed:{
fullName(){
return `${this.firstName} ${this.lastName}`
}
}</code></pre><p>这⾥的 fullName 事实上可以写为⼀个 Object,⽽⾮ Function,只<br>是 Function 形式是我们默认使⽤它的 get ⽅法,当写为 Object<br>时,还能使⽤它的 set ⽅法:</p><pre><code> computed:{
fullName:{
get(){
return `${firstName}-${lastname}`
},
set(val){
let name = val.split('')
this.firstName = name[0]
this.lastname = name[name.length-1]
}
}
}</code></pre><p>计算属性⼤多时候只是读取⽤,使⽤了 set 后,就可以写⼊了,⽐<br>如上⾯的示例,如果执⾏ this.fullName = 'Aresn Liang',<br>computed 的 set 就会调⽤,firstName 和 lastName 会被赋值为<br>Aresn 和 Liang。</p><h3>剩余值得注意的 API</h3><p>还有⼀些 API,可能不常⽤,也⽐较简单,只需知道就好,可以通过指引到 Vue.js ⽂档查看。<br><strong>delimiters</strong><br>(<a href="https://link.segmentfault.com/?enc=HmGTymSgTN7L0Plterw4lQ%3D%3D.%2BjTGYuaFfbiFAjh7S4bOOmGI8HAy10y3MoUZckQlaR0NLwmeVaAUTqUwxdZyM874" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a><br>改变纯⽂本插⼊分隔符,Vue.js 默认的是 {{ }},如果你使⽤其它<br>⼀些后端模板,⽐如 Python 的 Tornado 框架,那 Vue.js 和<br>Tornado 的 {{ }} 就冲突了,这时⽤它可以修改为指定的分隔符。</p><p><strong>v-once</strong> <br>(<a href="https://link.segmentfault.com/?enc=BZlS%2Byl737bBTzeskPstYg%3D%3D.GbUpftuSnBAWP0oTlqZcJgQ7%2FwZPwgCPrMS1U4gjDmEnaXEKxiVJk4bPEDFyQD3k" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a><br>只渲染元素和组件⼀次。随后的重新渲染,元素/组件及其所有的⼦<br>节点将被视为静态内容并跳过。这可以⽤于优化更新性能。</p><p><strong>vm.$isServer</strong> <br>(<a href="https://link.segmentfault.com/?enc=r%2F8VjYEZTs%2FcT%2Bgvom846g%3D%3D.1Mc7sSy5rk9DmjOyl%2BzcI7jxY2r5W%2B%2BM%2FqLnnv51farBFbxLsxYQfVTXdZ%2BTYEDN" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a><br>当前 Vue 实例是否运⾏于服务器,如果你的组件要兼容 SSR,它会<br>很有⽤。</p><p><strong>inheritAttrs</strong><br>(<a href="https://link.segmentfault.com/?enc=p%2FbIjSPsai6eLjpKtblYmg%3D%3D.cZAdbl7zsVaj6ofP3U1aWzCOpmr3Yrl1jejy3vCtvMPt%2FFjc33q2Y16t0kvyoS6K" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a><br>⼀些原⽣的 html 特性,⽐如 id,即使没有定义 props,也会被集<br>成到组件根节点上,设置 inheritAttrs 为 false 可以关闭此特性。</p><pre><code>当设置inheritAttrs: true(默认)时,子组件的顶层标签元素中会渲染出父组件传递过来的属性
当设置inheritAttrs: false时,子组件的顶层标签元素中不会渲染出父组件传递过来的属性
不管inheritAttrs为true或者false,子组件中都能通过$attrs属性获取到父组件中传递过来的属性。</code></pre><p><strong>errorHandler</strong><br>(<a href="https://link.segmentfault.com/?enc=djizNkOPVmwvr1DXTqs5wQ%3D%3D.2sB0nQ8joq9Ru5kszaREiMzbt7BvOk3GL7tGg9FKXEGgKj8bKJhZeeCYzmJH7yBb" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a><br>使⽤ errorHandler 可以进⾏异常信息的获取。</p><p><strong>watch</strong> (<a href="https://link.segmentfault.com/?enc=tWYaEEV7OFlf8VYNW4rsAA%3D%3D.h7JjznxKjFligwvsS73Fd%2F01QitzFnfgQ5tjuOpH94SdBsdu8aqGHqUDt5ptPBns" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a><br>监听状态的变化,⽤的也很多了,但它和 computed ⼀样,也有<br>Object 的写法,这样能配置更多的选项,⽐如:<br>handler 执⾏的函数<br>deep 是否深度<br>immediate 是否⽴即执⾏</p><pre><code>var vm = new Vue({
data: {
a: 1,
b: 2,
c: 3,
d: 4,
e: {
f: {
g: 5
}
}
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: 'someMethod',
immediate: true
},
// 你可以传入回调数组,它们会被逐一调用
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
/* ... */
}
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
})
vm.a = 2 // => new: 2, old: 1</code></pre><p><strong>不应该使用箭头函数来定义 watcher 函数</strong> (例如 <code>searchQuery: newValue => this.updateAutocomplete(newValue)</code>)。理由是箭头函数绑定了父级作用域的上下文,所以 <code>this</code> 将不会按照期望指向 Vue 实例,<code>this.updateAutocomplete</code> 将是 undefined。</p><p>完整的配置可以阅读⽂档。</p><p><strong>comments</strong><br>(<a href="https://link.segmentfault.com/?enc=iYAJSayROHz8SEX1SSZJYA%3D%3D.QcHkKZI53Ybi8PSnIlexDuM75zFS4t5NIQlOKMh2%2F1zehAbCESIVLFrqbpFTVpws" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a><br>开启会保留 html 注释。</p><p><strong>transition</strong><br>(<a href="https://link.segmentfault.com/?enc=rqaPfV4zMi1eQ97%2FKfn9iQ%3D%3D.gy1G7wwrZ8CpknajOjX25t3pfAcXhuFCqi%2FX11%2F6h56zNqH7fBwZnwAbi1S0r%2FSS" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a><br>内置的组件,可做过渡效果,⽐如 CSS 的⾼度从 0 到 auto(使⽤<br>纯 CSS 是⽆法实现动画的)。</p><h3>Vue常见面试题</h3><p><strong>v-show 与 v-if 区别</strong></p><blockquote><ul><li>v-show 只是 CSS 级别的 display: none; 和 display:</li></ul><p>block; 之间的切换,⽽ v-if 决定是否会选择代码块的内容(或组<br>件)。</p><ul><li>频繁操作时,使⽤ v-show,⼀次性渲染完的,使⽤ v-if,只要意</li></ul><p>思对就好。</p></blockquote><p><strong>使⽤ v-if在性能优化上有什么经验?</strong><br>*因为当 v-if="false" 时,内部组件是不会渲染的,所以在特定条<br>件才渲染部分组件(或内容)时,可以先将条件设置为 false,需<br>要时(或异步,⽐如 $nextTick)再设置为 true,这样可以优先渲<br>染重要的其它内容,合理利⽤,可以进⾏性能优化。*</p><h3>绑定 class 的数组⽤法</h3><p>动态绑定 class 应该不陌⽣吧,这也是最基本的,但是这个问题却有<br>点绕,什么叫绑定 class 的数组⽤法?我们看⼀下,最常⽤的绑定<br>class 怎么写:</p><pre><code><template>
<div :class="{show: isShow}">内容</div>
</template>
<script>
export default {
data () {
return {
isShow: true
}
}
}
</script></code></pre><p>绑定 class 的对象⽤法能满⾜⼤部分业务需求,不过,在复杂的场景<br>下,会⽤到数组,来看示例:</p><pre><code><template>
<div :class="classes"></div>
</template>
<script>
export default {
computed: {
classes () {
return [
`${prefixCls}`,
`${prefixCls}-${this.type}`,
{
[`${prefixCls}-long`]: this.long,
[`${prefixCls}-${this.shape}`]:
!!this.shape,
[`${prefixCls}-${this.size}`]:
this.size !== 'default'
,
[`${prefixCls}-loading`]:
this.loading != null && this.loading,
[`${prefixCls}-icon-only`]:
!this.showSlot && (!!this.icon ||
!!this.customIcon || this.loading),
[`${prefixCls}-ghost`]: this.ghost
}
];
}
}
}
</script></code></pre><p>示例来⾃ iView 的 Button 组件,可以看到,数组⾥,可以是固定的<br>值,还有动态值(对象)的混合。</p><h3>计算属性和 watch 的区别</h3><blockquote><ul><li>计算属性是⾃动监听依赖值的变化,从⽽动态返回内容,监听是⼀个</li></ul><p>过程,在监听的值变化时,可以触发⼀个回调,并做⼀些事情。<br>*所以区别来源于⽤法,只是需要动态值,那就⽤计算属性;需要知道<br>值的改变后执⾏业务逻辑,才⽤ watch,</p></blockquote><p>这个问题会延伸出⼏个问题:</p><ol><li>computed 是⼀个对象时,它有哪些选项?</li><li>computed 和 methods 有什么区别?</li><li>computed 是否能依赖其它组件的数据?</li><li>watch 是⼀个对象时,它有哪些选项?</li></ol><blockquote><p>*问题1:有 get 和 set 两个选项。</p><ul><li>问题 2,methods 是⼀个⽅法,它可以接受参数,⽽ computed 不</li></ul><p>能;computed 是可以缓存的,methods 不会;⼀般在 v-for<br>⾥,需要根据当前项动态绑定值时,只能⽤ methods ⽽不能⽤<br>computed,因为 computed 不能传参。</p><ul><li>问题 3,computed 可以依赖其它 computed,甚⾄是其它组件的</li></ul><p>data。<br>*问题 4,有以下常⽤的配置:<br>handler 执⾏的函数<br>deep 是否深度<br>immediate 是否⽴即执⾏</p></blockquote><p>**怎样给这个⾃定义组件 custom-component 绑定⼀个原<br>⽣的 click 事件?**</p><pre><code><custom-component @click.native="xxx">内容
</custom-component></code></pre><p><strong> exact 修饰符</strong><br>exact 是 Vue.js 2.5.0 新加的,它允许你控制由精确的<br>系统修饰符组合触发的事件,⽐如:</p><pre><code> <!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button></code></pre><ul><li><strong><code>stop</code></strong>:等同于JavaScript中的<code>event.stopPropagation()</code>,防止事件冒泡</li><li><strong><code>.prevent</code></strong>:等同于JavaScript中的<code>event.preventDefault()</code>,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播)</li><li><strong><code>.capture</code></strong>:与事件冒泡的方向相反,事件捕获由外到内</li><li><strong><code>.self</code></strong>:只会触发自己范围内的事件,不包含子元素</li><li><strong><code>.once</code></strong>:只会触发一次</li></ul><p><strong>组件中 data 为什么是函数</strong><br>为什么组件中的 data 必须是⼀个函数,然后 return ⼀个对象,⽽<br>new Vue 实例⾥,data 可以直接是⼀个对象?<br>因为组件是⽤来复⽤的,JS ⾥对象是引⽤关系,这样作⽤域没有隔<br>离,⽽ new Vue 的实例,是不会被复⽤的,因此不存在引⽤对象的<br>问题。<br><strong>递归组件的要求</strong><br>要给组件设置 name;<br>要有⼀个明确的结束条件。<br><strong>⾃定义组件的语法糖 v-model 是怎样实现的</strong></p><pre><code><template>
<div>
{{ currentValue }}
<button @click="handleClick">Click</button>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 0
}
},
data () {
return {
currentValue: this.value
}
},
methods: {
handleClick () {
this.currentValue += 1;
this.$emit('input'
, this.currentValue);
}
},
watch: {
value (val) {
this.currentValue = val;
}
}
}
</script></code></pre><p>这个组件中,只有⼀个 props,但是名字叫 value,内部还有⼀个<br>currentValue,当改变 currentValue 时,会触发⼀个⾃定义事件<br>@input,并把 currentValue 的值返回。这就是⼀个 v-model 的<br>语法糖,它要求 props 有⼀个叫 value 的项,同时触发的⾃定义事<br>件必须叫 input。这样就可以在⾃定义组件上⽤ v-model 了</p><pre><code><custom-component v-model="value"></customcomponent></code></pre><p>也可以用model 选项,<br><strong>Vuex 中 mutations 和 actions 的区别</strong><br>主要的区别是,actions 可以执⾏异步。actions 是调⽤<br>mutations,⽽ mutations 来修改 store。</p><p><strong>Render 函数</strong><br><strong>什么是 Render 函数,它的使⽤场景是什么</strong>。<br>说到 Render 函数,就要说到虚拟 DOM(Virtual DOM),Virtual<br>DOM 并不是真正意义上的 DOM,⽽是⼀个轻量级的 JavaScript 对<br>象,在状态发⽣变化时,Virtual DOM 会进⾏ Diff 运算,来更新只<br>需要被替换的 DOM,⽽不是全部重绘。<br>它的使⽤场景,就是完全发挥 JavaScript 的编程能⼒,有时需要结<br>合 JSX 来使⽤。<br><strong>createElement 是什么?</strong><br>createElement 是 Render 函数的核⼼,它构成了 Vue Virtual<br>DOM 的模板<br>Render 函数有哪些常⽤的参数?<br>1 {String | Object | Function}<br>⼀个 HTML 标签、一个含有数据选项的对象、Function返回一个含有数据选项的对象</p><pre><code> Vue.component('child', {
props: ['level'],
render: function (createElement) {
//string:html标签
return createElement('h1')
//object:一个含有数据选项的对象
return createElement({
template: '<div>谈笑风生</div>'
})
//function:返回一个含有数据选项的对象
var domFun = function () {
return {
template: `<div>谈笑风生</div>`
}
}
return createElement(domFun())
}
})</code></pre><p>2.第二个参数是数据对象。只能是object</p><ul><li>class</li><li>style</li><li>attrs</li><li>domProps</li></ul><pre><code>createElement('div', {
class: {
foo: true,
baz: false
},
style: {
height: '34px',
background: 'orange',
fontSize: '16px'
},
//正常的html特性(除了class和style)
attrs: {
id: 'foo',
title: 'baz'
},
//用来写原生的DOM属性
domProps: {
innerHTML: '<span >江心比心</span>'
}</code></pre><p>第三个参数可选 代表子节点<br> String|Array</p><pre><code> Vue.component('child', {
props: ['level'],
render: function (createElement) {
return createElement('div', [
createElement('h1', '我是大标题'),
createElement('h2', '我是二标题'),
createElement('h3', '我是三标题')
])
}
})</code></pre><p><strong>怎样理解单向数据流</strong><br>这个概念出现在组件通信。⽗组件是通过 prop 把数据传递到⼦组件<br>的,但是这个 prop 只能由⽗组件修改,⼦组件不能修改,否则会报<br>错。⼦组件想修改时,只能通过 $emit 派发⼀个⾃定义事件,⽗组<br>件接收到后,由⽗组件修改。<br>⼀般来说,对于⼦组件想要更改⽗组件状态的场景,可以有两种⽅<br>案:</p><ol><li>在⼦组件的 data 中拷⻉⼀份 prop,data 是可以修改的,但</li></ol><p>prop 不能:</p><pre><code>export default {
props: {
value: String
},
data () {
return {
currentValue: this.value
}
}
}</code></pre><ol><li>如果是对 prop 值的转换,可以使⽤计算属性:</li></ol><pre><code>export default {
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase();
}
}
}</code></pre><p><strong>⽣命周期</strong><br>Vue.js ⽣命周期 (<a href="https://link.segmentfault.com/?enc=jgpm1cvLLhwEdqbFJVDX9Q%3D%3D.NiS1KXfiFqpMoD9fLO2UWylfkSuq1g0ZVYgcaEkNOYK%2BwvfP1tX2dNFQFOzg1qVr%2F8DAbv93y%2FKqbvQ7%2FXTKZg%3D%3D" rel="nofollow">https://cn.vuejs.org/v2/api/#...</a>⽣命周期<br>钩⼦) 主要有 8 个阶段:</p><blockquote>*创建前 / 后(beforeCreate / created):在 beforeCreate<br>阶段,Vue 实例的挂载元素 el 和数据对象 data 都为<br>undefined,还未初始化。在 created 阶段,Vue 实例的数据<br>对象 data 有了,el 还没有。<br>*载⼊前 / 后(beforeMount / mounted):在 beforeMount<br>阶段,Vue 实例的 $el 和 data 都初始化了,但还是挂载之前<br>为虚拟的 DOM 节点,data 尚未替换。在 mounted 阶段,<br>Vue 实例挂载完成,data 成功渲染。<br>*更新前 / 后(beforeUpdate / updated):当 data 变化<br>时,会触发 beforeUpdate 和 updated ⽅法。这两个不常<br>⽤,且不推荐使⽤。<br>*销毁前 / 后(beforeDestroy / destroyed):<br>beforeDestroy 是在 Vue 实例销毁前触发,⼀般在这⾥要通<br>过 removeEventListener 解除⼿动绑定的事件。实例销毁<br>后,触发 destroyed。</blockquote><p><strong>组件间通信</strong></p><ol><li>⽗⼦通信:</li></ol><p>⽗向⼦传递数据是通过 props,⼦向⽗是通过<br>events($emit);通过⽗链 / ⼦链也可以通信($parent /<br>$children);ref 也可以访问组件实例;provide / inject<br>API。</p><ol><li>兄弟通信:</li></ol><p>Bus;Vuex;</p><ol><li>跨级通信:</li></ol><p>Bus;Vuex;provide / inject API。</p><p><strong>路由的跳转⽅式</strong><br>⼀般有两种:</p><ol><li>通过 ,router-link 标签会渲 染为 标签,在 template 中的跳转都是⽤这种;</li><li>另⼀种是编程式导航,也就是通过 JS 跳转,⽐如 router.push('/home')。</li></ol><p><strong>Vue.js 2.x 双向绑定原理</strong></p><pre><code>核⼼的 API 是通过 Object.defineProperty() 来劫持各个属性的
setter / getter,在数据变动时发布消息给订阅者,触发相应的监听
回调,这也是为什么 Vue.js 2.x 不⽀持 IE8 的原因(IE 8 不⽀持此
API,且⽆法通过 polyfill 实现)
Vue.js ⽂档已经对 深⼊响应式原理
(https://cn.vuejs.org/v2/guide/reactivity.html) 解释的很透彻
了。</code></pre><p><strong>什么是 MVVM,与 MVC 有什么区别</strong></p><pre><code>MVVM 模式是由经典的软件架构 MVC 衍⽣来的。当 View(视图
层)变化时,会⾃动更新到 ViewModel(视图模型),反之亦然。
View 和 ViewModel 之间通过双向绑定(data-binding)建⽴联
系。与 MVC 不同的是,它没有 Controller 层,⽽是演变为
ViewModel。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,
⽽ View 和 Model 之间的同步⼯作是由 Vue.js 完成的,我们不需
要⼿动操作 DOM,只需要维护好数据状态。
</code></pre>
看<<玩转VS Code>> 记录vscode快捷键
https://segmentfault.com/a/1190000037595775
2020-10-25T15:03:52+08:00
2020-10-25T15:03:52+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<h3>1. 针对单词的光标移动</h3><p>你只需按下 Option(Windows 上是 Ctrl 键)和左方向键。相反,如果要把光标移动到单词的末尾,只需要按下 Option 和右方向键就好了。可以在文档中以单词为单位不停地移动光标。</p><p><strong>把光标移动到行首或者行末。</strong></p><pre><code>Fn+Home/End</code></pre><p><strong>代码块的光标移动</strong></p><pre><code>按下 Cmd + Shift + (Windows 上是 Ctrl + Shift + )</code></pre><p><strong>移动到文档的第一行或者最后一行</strong></p><pre><code>你只需按住 Cmd + 左方向键(Windows 上是 ctr +Home 键),就可以把光标移动到了这行的第一列;而如果你按住 Cmd 和右方向键(Windows 上是 ctr +End 键)</code></pre><h3>3. 删除操作</h3><p><strong>当前行中光标之前的文本全部删除,</strong></p><pre><code>Home + Shift (fn+shift) +delete,macOS: Cmd + Left + Shift +delete
</code></pre><h3>2. 文本选择</h3><pre><code>对于基于单词、行和整个文档的光标操作,你只
需要多按一个 Shift 键,就可以在移动光标的同时选中其中的文本。 ctr+方向选取 ctr+shift+delete删除</code></pre><p><strong>删除单行</strong></p><pre><code>
Cmd + Shift + K ” (Windows 上是 “Ctrl + Shift + K”)</code></pre><p><strong>剪切这行代码</strong></p><pre><code> Cmd + x ” (Windows 上是 “Ctrl + x”) 即可。</code></pre><p><strong>移动一段代码</strong></p><pre><code>Option + 上下方向键”(Windows中就是“Alt + 上下方向键”</code></pre><p><strong>复制这几行,然后粘贴到当前行的上面或者下面。</strong></p><pre><code>“Option + Shift + 上下方向键”(Windows中就是“Alt + shift + 上下方向键”)</code></pre><h3>添加注释</h3><pre><code>
“ Cmd + / ” (Windows 上时 “Ctrl + /”)</code></pre><p>。</p><h3>折叠代码</h3><pre><code>全部、
折叠:Ctrl/Command + k + 0
展开:Ctrl/Command + k + j
部分:
折叠
ctr + shift + [
打开
ctr + shift + ]</code></pre><h3>代码格式化</h3><pre><code>Option + Shift + F” (Windows 上是 Alt + Shift + F)</code></pre><p><strong>调换字符的位置</strong></p><pre><code>Ctrl + t” (Windows 上未绑定快捷键,可以打开命令面板,搜索 ”转置游标处的字符</code></pre><p><strong>调整字符的大小写</strong></p><pre><code>命令面板里运行“转换为大写”或 “转换为小写”</code></pre><p><strong>合并代码行</strong></p><pre><code>
“ Ctrl + j ” (Windows 上未绑定快捷键,可以打开命令面板,搜索 ”合并行排序“ 命令面板,然后搜索 “按升序排列行” 或者 “按降序排列行” 命令执行</code></pre><p><strong>查看当前文件所有的关于选中的单词</strong></p><pre><code>ctr + d</code></pre><p><strong>关闭当前文件</strong></p><pre><code>ctr + w</code></pre><p><strong>打开最近打开文件的历史记录</strong><br><strong>返回上一个开启代码篇</strong></p><pre><code>ctr + t </code></pre><p><strong>撤销光标的移动和选择</strong></p><pre><code>Cmd + U”(Windows 上是 “Ctrl + U”</code></pre><p><strong>当前行的上面新开始一行</strong></p><pre><code>“Cmd + Shift + Enter” (Windows 上是 “Ctrl + Shift + Enter”)</code></pre><p><strong>当前行的下面新开始一行</strong></p><pre><code>Cmd + Enter” (Windows 上是 “Ctrl + Enter”)</code></pre><h2>多光标</h2><p><strong>使用鼠标</strong></p><pre><code>Option(Windows上是Alt)按住点下一个需要的位置</code></pre><p><strong>使用键盘</strong></p><pre><code>
Cmd + Option + 下方向键”(Windows 上是 “Ctrl + Alt + 下方向键”)
Cmd + 右方向键”(Windows 上是 End) 移动到每一行的末尾,可对css添加pz之类的
或者
Cmd + D”(Windows 上是 Ctrl + D)实现相同文字多选多光标 进行别的操作
esc退出多选
或者
Option + Shift + i” (Windows 上是 Alt + Shift + i) 每一行的最后都会创建一个新的光标。</code></pre><h2><strong>跳转</strong></h2><pre><code>“Ctrl+Tab”
或者
“Cmd + P” (Windows 上是 Ctrl + P) 跳出一个最近打开文件的列表,同时在列表的顶部还有一个搜索框。
找到目标文件后,可以按下 “Cmd + Enter ” (Windows 上是 Ctrl + Enter)组合键, 这个文件在一个新的编辑器窗口中打开</code></pre><p><strong>行跳转</strong></p><pre><code>Ctrl + g”</code></pre><p><strong>移动到定义处</strong></p><pre><code>ctr + f12</code></pre><p><strong>跳转到某个文件的某一行</strong></p><pre><code>先按下 “Cmd + P”,输入文件名,然后在这之后加上 “:”和指定行号即可。</code></pre><p><strong>符号 (Symbols) 跳转</strong>( VS Code 提供了一套 API 给语言服务插件,它们可以分析代码,告诉 VS Code 项目或者文件里有哪些类、哪些函数或者标识符(我们把这些统称为符号)。)</p><pre><code>
Cmd + Shift + O” (Windows 上是 Ctrl + Shift + O),就能够看到当前文件里的所有符号</code></pre><p><strong>使用方向键,或者搜索,找到你想要的符号后,按下回车,就能够立刻跳转到那个符号的位置</strong></p><pre><code>“Cmd + Shift +O”后,输入框里有一个 “@”符号
输入 “:”,就可以将当前文件的所有符号,进行分类</code></pre><p><strong>多个文件里进行符号跳转</strong></p><pre><code>“Cmd + T” (Windows 上是 Ctrl + T),搜索这些文件里的符号。</code></pre><p><strong>跳转到函数的实现的位置。</strong></p><pre><code>“Cmd + F12” (Windows 上是 Ctrl + F12)</code></pre><p><strong>跳转到定义的代码文件返回</strong></p><pre><code>ctr + -</code></pre><p><strong>文件目录切换</strong></p><pre><code>ctr + r
或者
按下 Ctrl + R 调出最近打开的文件夹的列表后,也能够按下 Cmd(ctr) + 回车键,将它在一个新的窗口中打开。</code></pre><h2>显示</h2><p><strong>放大当前页面的代码</strong></p><pre><code>ctr +</code></pre><p><strong>缩小</strong></p><pre><code>ctr -</code></pre><p><strong>侧边栏显示隐藏</strong></p><pre><code>ctr + B</code></pre><p><strong>全屏</strong></p><pre><code>F11</code></pre><p><strong>显示操作台</strong></p><pre><code>ctr + shift + u</code></pre><h2>引用 (Reference) 跳转</h2><hr><p><strong>当前行的下面新开始一行</strong></p><pre><code>Cmd + Enter” (Windows 上是 “Ctrl + Enter”)</code></pre><p><strong>在一个 js 文件里 export 了一个函数,在另外一个文件里引用了它但是 shift + F12 找不到</strong></p><p><em>可以在这个项目下添加一个 jsconfig.json 文件,这个文件会让 VSCode 知道,当前这个文件夹下的文件都是属于同一个项目的,从而进行索引。</em></p><pre><code>
{
"compilerOptions": {
"target": "ES6"
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}</code></pre><p><strong>webpack里面配置了路径别名,vscode就找不到定义</strong></p><pre><code>{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"ClientApp/*": ["./ClientApp/*"]
}
}
}
</code></pre><h2>鼠标操作</h2><pre><code>连续按下鼠标三次,则会选中当前代码
四次 则会选中整篇代码</code></pre><p><strong>悬停提示窗口</strong></p><pre><code> Cmd 键(Windows 上是 Ctrl),则能够在悬停提示窗口里直接看到 `foo`的实现。
点击可实现跳转</code></pre><p><strong>操作左侧的资源管理目录</strong></p><pre><code>ctr + shift + E</code></pre><p><strong>左侧跨文件搜索</strong></p><pre><code>ctr + shift + F</code></pre><p><strong>管理自己的git存储库</strong></p><pre><code>ctr + shift + G</code></pre><p><strong>同步自己的 vs code 设置</strong></p><pre><code>插件 Settings Sync </code></pre><p><strong>启动和调试</strong></p><pre><code>ctr + shift + D</code></pre><p><strong>管理扩展</strong></p><pre><code>ctr + shift + X</code></pre><p><strong>查找并运行所有命令</strong></p><pre><code>ctr + shift + p</code></pre><h2>重构</h2><p><strong>修改当前页面的相同函数或者变量名</strong></p><pre><code>把光标放到函数或者变量名上,然后按下 F2,这样这个函数或者变量出现的地方就都会被修改。</code></pre><p><strong>长代码抽取出来转成一个单独的函数</strong></p><pre><code>我们只需选中那段代码,点击黄色的灯泡图标,然后选择对应的重构操作即可。</code></pre><h2>代码片段</h2><pre><code>1 打开命令面板,搜索“配置用户代码片段”(Configure User Snippets)并且执行
2选择 JavaScript
3 选择完语言后,我们就能看到一个 JSON 文件被打开了,这个文件里的内容,现在都是被注释掉的。我们可以选中第七行到第十四行,按下 “Cmd+ /”取消注释。
{
"Print to console": {
"prefix": "log",
"body": [
"console.log('$1');",
"$2"
],
"description": "Log output to console"
}
}
</code></pre><p><em>这个代码片段的名字叫做 “Print to console”。这个代码片段对象的值,也就是花括号里的代码,必须要包含 “prefix” 前缀和 “body” 内容这两个属性。同时,这个值还可以包含 “description” 描述这个属性,但这个属性不是必须的。</em><br><em>“prefix” 的作用是,当我们在编辑器里打出跟 “prefix” 一样的字符时,我们就能在建议列表里看到这个代码片段的选项,然后我们按下 Tab 键,就能够将这个代码片段的 “body” 里面的内容插入到编辑器里。如果这个代码片段有 “description” 这个属性的话,那么我们还能够在建议列表的快速查看窗口里看到这段 “description”</em><br><strong>Tab Stop</strong></p><pre><code>当 “body” 内容被插入到编辑器后,你会发现,内容里 的`$1`和 `$2`不见了,取而代之的是两个竖线。这`$1`和 `$2`就是 Tab Stop,意思是,当我们按下 Tab 键之后,光标移动到的位置。当这段代码片段被插入到编辑器后,编辑器会把光标移动到`$1`所在的位置,然后如果你再按一次 Tab 键,光标则会立刻移动到 `$2`的位置。
光标移动到上一个 Tab Stop 的位置的快捷键
“Shift + Tab” </code></pre><p><strong>占位符</strong><br>*在我们插入 Tab Stop 的时候,除了 <code>$1</code>、 <code>$2</code> 这样的语法,我们还可以填入 <code>${1:label}</code>,在这个格式下,代码片段被插入编辑器里时,$1 的位置处,会预先填入 <code>label</code>这个值,并且 <code>label</code> 会被选中。<br>对于这个值我们称之为占位符,顾名思义,这个值是我们在代码片段中预先设置好的。如果我们觉得这个值可以用,那就不需要修改了,直接按 Tab 键跳到下一个 Tab Stop 继续编辑。如果觉得要换成一个新的值,那么也只需直接打字就可以将其替换,因为这个占位符已经被光标选中了。<br>这里我们对上面的代码片段进行一点修改*:</p><pre><code> "Print to console": {
"prefix": "log",
"body": [
"console.log(${1:i});",
"$2"
],
"description": "Log output to console"
}</code></pre><p>*我们将 <code>$1</code> 改成了 <code>${1:i}</code>,那么当log 这个代码片段被插入时,我们将看到<code>console.log(i);</code>,同时 <code>i</code> 被选中<br>直接按下 Escape 键,跳出代码片段的编辑模式,之后继续我们的其他编辑操作。*<br><strong>多光标</strong><br>可以用 ${1:label} 来指定 Tab Stop 和占位符<br>也可以在代码片段的多个位置使用同样的 Tab Stop 。<br>代码片段中三个不同的位置插入 $1,这样编辑器就会为这三个不同的位置,分别创建一个光标,然后当我们打字的时候,他们就会被一起修改。</p><pre><code> "Print to console": {
"prefix": "log",
"body": [
"console.log(${1:i});",
"console.log(${1:i} + 1); // ${1:i} + 1",
"$2"
],
"description": "Log output to console"
}</code></pre><p><strong>预设变量</strong><br>提前预设好一些值一些变量。</p><pre><code>https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables</code></pre><h5>代码折叠</h5><pre><code>Cmd + Option + 左方括号”(Windows 上是 Ctrl + Shift + 左方括号</code></pre><p><strong>从当前光标位置开始,一直到最外层的,所有可以被折叠的部分递归地折叠起来</strong></p><pre><code>“Cmd + K”“Cmd + 0”(Windows 上是 Ctrl + K,Ctrl + 0)</code></pre><p><strong>全部展开</strong></p><pre><code>“Cmd + K”“Cmd + J” (Windows 上是 Ctrl + K,Ctrl + J)</code></pre><p><strong>代码展开</strong></p><pre><code>Cmd + Option + 右方括号”(Windows 上是 Ctrl + Shift + 右方括号)</code></pre><p><strong> 基于语言定义的代码折叠</strong><br>代码折。的判断方式,是通过花括号或者代码缩进的检测来实现的。但若遇到不使用花括号或者缩进不正确的代码时,可能就不能实现这样的操作了。<br>VS Code 给语言服务提供了一个接口,语言服务可以动态地检测代码,然后告诉 VS Code 哪段代码是可以被折叠的。<br>可以通过在代码注释里书写特殊的关键词来申明,哪一行是可折叠代码的开始,哪一段则是这个可折叠代码块的结束。</p><pre><code>public class Main {
// region Main
public static void main(String[] args) {
}
// endregion
}</code></pre><p><code>// region Main</code> 申明了一个可折叠代码块的开始,而<code>// endregion</code>则申明了这段可折叠代码的结束。当我们把鼠标指针移动到行号附近时,我们能够看到三个加号,说明这段代码包含了三个可折叠的代码块,两个是由花括号控制的,一个则是基于特殊的语言的定义。<br>关键词来控制代码的折叠,还请参考 <a href="https://link.segmentfault.com/?enc=54rmEenxAUVa8CILwIFE1Q%3D%3D.lY73D7Jw%2Fui23l49oegjTZ03my0ibYa0b9Kib2uw14H9X3z%2BfNt5wbrnUjXbJJcmTP37lUDb5grgB025u8%2FsnA%3D%3D" rel="nofollow">VS Code 的官方文档</a>。</p><h4>搜索</h4><p><strong>单文件搜索</strong></p><pre><code>“Cmd + F” (Windows 上是 Ctrl + F)
调出搜索窗口的时候,编辑器就会把当前光标所在位置的单词自动填充到搜索框中。
先选中一段文本,然后按下 “Ctrl + F” 调出搜索框,这之后点击这个按钮,就可以将这段文本的范围设置为接下来的搜索区域。</code></pre><p><strong>搜索结果里自下而上地跳转</strong></p><pre><code>Cmd + Shift + G” (Windows 上是 Shift + F3)</code></pre><p><strong>调出替换窗口</strong></p><pre><code>Cmd + Option + F”(Windows 上是 Ctrl + H)</code></pre><p><strong>多文件搜索和替换</strong></p><pre><code>Cmd + Shift + F” (Windows 上是 Ctrl + Shift + F)</code></pre><p><strong>vscode基础配置</strong></p><pre><code>* editor cursor, 是跟光标渲染和多光标相关的设置;
* editor find, 是与编辑器内搜索相关的设置;
* editor font, 是与字体有关的设置;
* editor format, 是代码格式化;
* editor suggest, 是和自动补全、建议窗口等相关的配置。</code></pre><p><strong>命令面板符号</strong></p><pre><code>1. `>`(大于号) ,用于显示所有的命令。
2. @ ,用于显示和跳转文件中的“符号”(Symbols),在@符号后添加冒号:则可以把符号们按类别归类。
3. `#`号,用于显示和跳转工作区中的“符号”(Symbols)。
4. :(冒号), 用于跳转到当前文件中的某一行。</code></pre>
Vue项目的瀑布流部分代码分析
https://segmentfault.com/a/1190000037583150
2020-10-23T17:31:10+08:00
2020-10-23T17:31:10+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<pre><code><!--
* @Author: yang
* @Date: 2020-10-18 15:58:57
* @LastEditors: yang
* @LastEditTime: 2020-10-23 17:27:59
* @FilePath: \gl\src\component\index\receive.vue
-->
<template>
<div class="about">
<p>瀑布流,点击图片可删除一个</p>
<div class="page">
<div
class="content"
v-for="(item, index) in list"
:key="item.id"
:style="{
width: waterfallW + 'px',
height: item.imgH + 'px',
left: item.left + 'px',
top: item.top + 'px',
}"
ref="col"
@click="clickMe(index)"
>
<img :src="item.image" alt="" />
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
list: [
{
image:
'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
imgH: 122,
title: '标题只有1行哦长砍',
desc: 'Bon Cake(徐家汇店)这家店不要条好吃啊',
praiseNum: 322,
top: 0,
left: 0,
itemH: 0,
},
{
image:
'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
imgH: 334,
title: '标题只有1行哦长砍标题只有1行哦长砍标题只有1行哦长砍',
desc:
'Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店',
praiseNum: 32232,
top: 0,
left: 0,
itemH: 0,
},
{
image:
'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
imgH: 173,
title: '标题只有1行哦长砍',
desc:
'Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店',
praiseNum: 32,
top: 0,
left: 0,
itemH: 0,
},
{
image:
'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
imgH: 225,
title: '标题只有1行哦长砍',
desc: 'Bon Cake(徐家汇店)这家店',
praiseNum: 32,
top: 0,
left: 0,
itemH: 0,
},
{
image:
'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
imgH: 89,
title: '标题只有1行哦长砍',
desc:
'Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店',
praiseNum: 32,
top: 0,
left: 0,
itemH: 0,
},
{
image:
'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
imgH: 112,
title: '标题只有1行哦长砍',
desc: 'Bon Cake(徐家汇店)这家店',
praiseNum: 32,
top: 0,
left: 0,
itemH: 0,
}
],
initLeft:'',
waterfallW:'',
screenWidth:document.clientWidth,//屏幕宽度
gap:10,//图片之间的间距
leftH : 0,//左侧高度
rightH:0//右侧高度
}
},
created () {
this.waterfallW = (this.screenWidth-30)/2;
this.initLeft = (this.screenWidth - this.waterfallW)/2;
},
mounted () {
const nodeList = this.$refs.col;
this.doSort(nodeList)
},
methods: {
// 排序
doSort(nodeList) {
for(let i =0;i<nodeList.length;i++){
nodeList[i].style.position = 'absolute';
const domHeight = nodeList[i].clientHeight; //获取图片的高度
let top,left,itemH;
// 排列数据的形式高的在左侧,低矮的在右侧
if(this.leftH>this.rightH){//如果左侧的比右侧图片高
left = this.gap * 2 + this.waterfallW; //右侧的left
top = this.rightH + this.gap;//图片高度加间距
itemH = domHeight;
this.rightH += this.gap + domHeight;//右侧的整体高度
}else{
left = this.gap;
top = this.leftH + this.gap;//左侧的top
itemH = domHeight;//图片的高度
this.leftH += this.gap + domHeight;//左侧的高度
}
this.list[i].top = top;
this.list[i].left = left;
this.list[i].itemH = itemH;
this.list[i].itemW = this.waterfallW;
}
},
clickMe(index){
const renderedList = this.list.slice(0,index)//得到索引前的数据
const afreshRenderList = this.list.slice(index+1)//得到点击索引后的数据
if(this.list[index].left>this.gap){//就是右侧的图片
this.rightH = this.list[index].top - this.gap //去除一个间距,被删除数据列无需重排数据的高度
this.leftH = this.checkHeight(renderedList,'left')
}else{
this.rightH = this.checkHeight(renderedList,'right')
this.leftH = this.list[index].top-this.gap//去除一个间距,被删除数据列无需重排数据的高度
}
const newList = this.restartSort(afreshRenderList)
this.list = [...renderedList,...newList]
},
// 查找不需要重新排列的数据中非被删除列的高度
checkHeight(list,col){
let needHeight = 0;
for(let i=0;i<list.length;i++){
if(col == 'left'&& list[i].left == this.gap&&list[i].top>needHeight){
needHeight = list[i].top+list[i].itemH
}else if(col = 'right'&&
list[i].left>this.gap&&
list[i].top>needHeight){
needHeight = list[i].top + list[i].itemH;
}
}
return needHeight;
},
//重新排列列表中被删除数据之后的所有数据
restartSort(list){//重排之后 长的在左边,短的在右边
const newList = list
newList.forEach((item)=>{
if(this.leftH>this.rightH){
item.left = this.gap*2+item.itemW //右侧的left
item.top = this.rightH + this.gap //右侧的top
this.rightH +=this.gap+item.itemH//右侧的高度
}else{
item.left = this.gap //左侧的left
item.top = this.leftH + this.gap //左侧的top
this.leftH += this.gap+item.itemH
}
})
return newList;
}
},
}
</script>
<style lang="scss" scoped>
.page {
position: relative;
width: 100%;
height: 100%;
}
.content {
position: fixed;
top: 100%;
img {
display: block;
width: 100%;
height: 100%;
}
}
</style>
</code></pre>
Vue 轮播图中间大两头小
https://segmentfault.com/a/1190000037556260
2020-10-21T18:13:37+08:00
2020-10-21T18:13:37+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<p><strong>使用插件版本号(决定成败,非常重要)</strong></p><pre><code>"swiper": "^5.3.1",
"vue-awesome-swiper": "^4.1.1",</code></pre><p><strong>引入</strong></p><pre><code>import Vue from 'vue'
import vueSwiper from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
Vue.use(vueSwiper)</code></pre><p><strong>模板</strong></p><pre><code> <div class="top-banner">
<swiper
:options="swiperOption"
ref="mySwiper"
class="swiper"
v-if="bannerData.length > 0"
>
<swiper-slide
class="list-item"
v-for="(item, index) in bannerData"
:key="index"
>
<div class="choice-box" @click="picStartGame(item)" >
<img :src="item.imgUrl" alt class="choice-pic" />
</div>
</swiper-slide>
</swiper>
</div></code></pre><p><strong>swiper配置</strong></p><pre><code>data(){
return{
bannerData: [],
swiperOption: {
initialSlide: 0, //设定初始化时slide的索引
effect: 'coverflow',
slidesPerView: 1.1,
spaceBetween: 20,
centeredSlides: true, //活动块会居中,而不是默认状态下的居左
loop: true,
speed: 1000,
autoplay: {
delay: 5000,
stopOnLastSlide: false,
disableOnInteraction: false,
},
observer: true, //修改swiper自己或子元素时,自动初始化swiper
observeParents: true, //修改swiper的父元素时,自动初始化swiper
coverflowEffect: {
rotate: 28, // slide做3d旋转时Y轴的旋转角度。默认50。
stretch: 3, // 每个slide之间的拉伸值,越大slide靠得越紧。
depth: 100, // slide的位置深度。值越大z轴距离越远,看起来越小。
modifier: 1, // depth和rotate和stretch的倍率,相当于depth*modifier、rotate*modifier、stretch*modifier,值越大这三个参数的效果越明显。默认1。
slideShadows: false, // 开启slide阴影。默认 true。
},
},
}
}</code></pre><p><strong>样式</strong></p><pre><code>.top-banner {
height: 3.68rem;
// width: 6.48rem;
width: 100%;
text-align: center;
margin-top: 1rem;
img {
width: 100%;
height: 3.68rem;
border-radius: 0.2rem;
}
}</code></pre>
Vue多页面nginx配置下的webpack配置
https://segmentfault.com/a/1190000037513923
2020-10-18T16:44:33+08:00
2020-10-18T16:44:33+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
2
<h3>package.json</h3><pre><code>{
"name": "Vue",
"version": "2.0.0",
"description": "",
"main": "index.js",
"directories": {
"test": "webpack4+vue2"
},
"dependencies": {
"animate.css": "^3.7.0",
"babel-preset-es2015": "^6.24.1",
"vant": "^1.6.7",
"vue-hot-reload-api": "^2.3.1",
"vue-html-loader": "^1.2.4",
"vue-lazyload": "^1.2.6",
"vue-resource": "^1.5.1",
"vue-router": "^2.8.1",
"vue-style-loader": "^2.0.5",
"vue2-toast": "^2.0.2"
},
"devDependencies": {
"autoprefixer": "^9.4.9",
"axios": "^0.18.0",
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-import": "^1.11.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-runtime": "^6.26.0",
"clean-webpack-plugin": "^1.0.0",
"css-loader": "^1.0.1",
"eslint": "^5.9.0",
"eslint-plugin-flowtype": "^3.2.0",
"expose-loader": "^0.7.5",
"file-loader": "^2.0.0",
"html-webpack-plugin": "^3.2.0",
"html-withimg-loader": "^0.1.16",
"install": "^0.12.2",
"jquery": "^3.3.1",
"less": "^3.8.1",
"less-loader": "^4.1.0",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.11.0",
"postcss": "^7.0.14",
"postcss-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"timeago.js": "^4.0.0-beta.2",
"uglifyjs-webpack-plugin": "^2.0.1",
"url-loader": "^1.1.2",
"vue": "^2.6.6",
"vue-bus": "^1.2.0",
"vue-loader": "^15.4.2",
"vue-template-compiler": "^2.6.6",
"vue-timeago": "^5.1.2",
"vuex": "^3.1.0",
"webpack": "^4.26.0",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10"
},
"scripts": {
"test": "webpack --mode=development --progress --colors --config ./webpack.test.config.js",
"dev": "webpack --mode=development --progress --colors --config ./webpack.dev.config.js",
"test-w": "webpack --mode=development --progress --colors --config ./webpack.test.config.js --watch",
"dev-w": "webpack --mode=development --progress --colors --config ./webpack.dev.config.js --watch",
"build": "webpack --mode=production --progress --colors --config ./webpack.prod.config.js --watch",
"prod": "webpack --mode=production --progress --colors --config ./webpack.prod.config.js"
},
"babel": {
"presets": [
"env"
]
},
"author": "LF",
"license": "ISC"
}
</code></pre><h3>postcss.config.js</h3><pre><code>/*
* @Author: yang
* @Date: 2020-10-18 15:58:57
* @LastEditors: yang
* @LastEditTime: 2020-10-18 16:10:01
* @FilePath: \gloud-h5\postcss.config.js
*/
module.exports = {
plugins: [
require('autoprefixer')({
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8"
//'last 10 versions', // 所有主流浏览器最近10版本用
],grid: true})
]
}</code></pre><h3>.gitignore</h3><pre><code>node_modules/
npm-debug.log
.idea/
dist/
.html</code></pre><h3>webpack.base.config.js</h3><pre><code>/*
* @Author: yang
* @Date: 2020-10-18 15:58:57
* @LastEditors: yang
* @LastEditTime: 2020-10-18 16:01:02
* @FilePath: \gloud-h5\webpack.base.config.js
*/
/**
* Created by Lee on 2019/2/13.
*/
let HtmlWebpackPlugin = require('html-webpack-plugin')
require('babel-polyfill')
let entry = {
index: ['babel-polyfill', './src/views/index.js'],
}
let plugins = []
Object.keys(entry).forEach(function(e) {
let plugin = new HtmlWebpackPlugin({
template: `./src/views/${e}.html`,
filename: `../${e}.html`,
hash: true,
chunks: [e, 'common'],
})
plugins.push(plugin)
})
module.exports = {
entry,
plugins,
}
</code></pre><h3>webpack.dev.config.js</h3><pre><code>let webpack = require('webpack');
let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let CleanWebpackPlugin = require('clean-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const uglifyjs = require('uglifyjs-webpack-plugin');
let config = require('./webpack.base.config')
module.exports = {
entry: config.entry,
//入口文件输出配置 (即入口文件最终要生成什么名字的文件、存放到哪里)
output: {
path: path.resolve('dist'),
publicPath: './dist/',
filename: '[name].js',
},
module: {
rules: [
{
test: require.resolve('jquery'),
use: [{
loader: 'expose-loader',
options: 'jQuery'
}, {
loader: 'expose-loader',
options: '$'
}]
},
{test: /\.vue$/, loader: 'vue-loader'},
{test: /\.js$/, exclude: /node_modules/,loader: 'babel-loader'},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192,
outputPath: 'images/'
}
}]
},
{
test: /\.css$/,
use: ["style-loader", "css-loader", "postcss-loader"]
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader',
]
},
{test: /\.(eot|svg|ttf|woff|woff2)\w*/, loader: 'file-loader'}
]
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
name: 'common',
chunks: 'initial',
minChunks: 2,
}
}
}
},
plugins: [
new CleanWebpackPlugin(['dist']),//打包前删除dist
new VueLoaderPlugin(),
new uglifyjs(),
new webpack.DefinePlugin({
'base_api': '"http://xiaowoxuetang.com/"',
}),
...config.plugins
],
//解决vue报错
resolve: {
extensions: ['.js', '.vue'],
alias:{'vue$': 'vue/dist/vue.common.js',}
},
// devServer: {
// contentBase: './dist',
// host: 'localhost', // 默认是localhost
// port: 8000, // 端口
// open: true, // 自动打开浏览器
// hot: true, // 开启热更新
// compress: true,
// },
mode: 'development' // 模式配置;development
}</code></pre><h3>webpack.prod.config</h3><pre><code>/*
* @Author: yang
* @Date: 2020-10-18 15:58:57
* @LastEditors: yang
* @LastEditTime: 2020-10-18 16:31:02
* @FilePath: \gloud-h5\webpack.prod.config.js
*/
let webpack = require('webpack');
let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let CleanWebpackPlugin = require('clean-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const uglifyjs = require('uglifyjs-webpack-plugin');
let config = require('./webpack.base.config')
module.exports = {
// entry: {
// index: './src/index.js', //首页入口JS
// // share: './src/share.js'
// },
entry:config.entry,
//入口文件输出配置 (即入口文件最终要生成什么名字的文件、存放到哪里)
output: {
path: path.resolve('dist'),
publicPath: './dist/',
filename: '[name].js',
},
module: {
rules: [
{ test: require.resolve('jquery'),
use: [{
loader: 'expose-loader',
options: 'jQuery'
}, {
loader: 'expose-loader',
options: '$'
}]
},
{ test: /\.vue$/, loader: 'vue-loader' },
// { test: /\.css$/, loader: 'style-loader!css-loader' },
{
test: /\.css$/,
use: ["style-loader", "css-loader", "postcss-loader"]
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader',
]
},
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' ,},
{ test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
//当加载的图片小于limit时,会将图片编译成base64字符串的格式(limit单位 byte)
//当加载的图片大于limit时,需要使用url-loader模块进行加载 输入路径 outputPath
limit: 8192,
outputPath: 'images/'
}
}]
},
{ test: /\.(eot|svg|ttf|woff|woff2)\w*/, loader: 'file-loader' }
]
},
optimization: {
splitChunks: {
cacheGroups: {//默认的规则不会打包,需要单独定义缓存策略,默认设置了分割node_modules和公用模块。内部的参数可以和覆盖外部的参数
vendor: {
name: 'common',//分割的js名称
chunks: 'initial',//也会同时打包同步和异步,但是异步内部的引入不再考虑,直接打包在一起,会将vue和b的内容直接打包成chunk,
minChunks: 2,//最小公用模块次数
}
}
}
},
plugins: [
new CleanWebpackPlugin(['dist']),//打包前删除dist
new VueLoaderPlugin(),
new uglifyjs(),
new webpack.DefinePlugin({
'base_api': '"http://xiaowoxuetang.com/',
}),
...config.plugins,
],
//解决vue报错
resolve: {
extensions: ['.js', '.vue'],
alias: {
'vue$': 'vue/dist/vue.common.js',
}
},
mode: 'production' // 模式配置;development
}</code></pre>
全局gif组件
https://segmentfault.com/a/1190000037500842
2020-10-16T16:15:33+08:00
2020-10-16T16:15:33+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<h3>gif-loading.js</h3><pre><code>
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.GIFLoading = factory();
}
})(this, function() {
var GIFLoading = {};
GIFLoading.version = '0.2.0';
//配置图片的地方
var loadingDefault = require('../images/loading.gif')
var Settings = GIFLoading.settings = {
minimum: 0.08,
easing: 'linear',
positionUsing: '',
speed: 200,
trickle: true,
trickleSpeed: 200,
showSpinner: true,
barSelector: '[role="bar"]',
spinnerSelector: '[role="spinner"]',
parent: 'body',
template:`<div class="gif-loading"><div class="gif-loading-bg"></div><div class="gif-loading-main"><img src="${loadingDefault}" alt=""></div></div>`
// template: '<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'
};
/**
* Updates configuration.
*
* GIFLoading.configure({
* minimum: 0.1
* });
*/
GIFLoading.configure = function(options) {
var key, value;
for (key in options) {
value = options[key];
if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
}
return this;
};
/**
* Last number.
*/
GIFLoading.status = null;
/**
* Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
*
* GIFLoading.set(0.4);
* GIFLoading.set(1.0);
*/
GIFLoading.set = function(n) {
var started = GIFLoading.isStarted();
n = clamp(n, Settings.minimum, 1);
GIFLoading.status = (n === 1 ? null : n);
var progress = GIFLoading.render(!started),
bar = progress.querySelector(Settings.barSelector),
speed = Settings.speed,
ease = Settings.easing;
progress.offsetWidth; /* Repaint */
queue(function(next) {
// Set positionUsing if it hasn't already been set
if (Settings.positionUsing === '') Settings.positionUsing = GIFLoading.getPositioningCSS();
// Add transition
// css(bar, barPositionCSS(n, speed, ease));
if (n === 1) {
// Fade out
// css(progress, {
// transition: 'none',
// opacity: 1
// });
progress.offsetWidth; /* Repaint */
setTimeout(function() {
// css(progress, {
// transition: 'all ' + speed + 'ms linear',
// opacity: 0
// });
setTimeout(function() {
GIFLoading.remove();
next();
}, speed);
}, speed);
} else {
setTimeout(next, speed);
}
});
return this;
};
GIFLoading.isStarted = function() {
return typeof GIFLoading.status === 'number';
};
/**
* Shows the progress bar.
* This is the same as setting the status to 0%, except that it doesn't go backwards.
*
* GIFLoading.start();
*
*/
GIFLoading.start = function() {
if (!GIFLoading.status) GIFLoading.set(0);
var work = function() {
setTimeout(function() {
if (!GIFLoading.status) return;
GIFLoading.trickle();
work();
}, Settings.trickleSpeed);
};
if (Settings.trickle) work();
return this;
};
/**
* Hides the progress bar.
* This is the *sort of* the same as setting the status to 100%, with the
* difference being `done()` makes some placebo effect of some realistic motion.
*
* GIFLoading.done();
*
* If `true` is passed, it will show the progress bar even if its hidden.
*
* GIFLoading.done(true);
*/
GIFLoading.done = function(force) {
if (!force && !GIFLoading.status) return this;
return GIFLoading.inc(0.3 + 0.5 * Math.random()).set(1);
};
/**
* Increments by a random amount.
*/
GIFLoading.inc = function(amount) {
var n = GIFLoading.status;
if (!n) {
return GIFLoading.start();
} else if(n > 1) {
return;
} else {
if (typeof amount !== 'number') {
if (n >= 0 && n < 0.2) { amount = 0.1; }
else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
else if (n >= 0.5 && n < 0.8) { amount = 0.02; }
else if (n >= 0.8 && n < 0.99) { amount = 0.005; }
else { amount = 0; }
}
n = clamp(n + amount, 0, 0.994);
return GIFLoading.set(n);
}
};
GIFLoading.trickle = function() {
return GIFLoading.inc();
};
/**
* Waits for all supplied jQuery promises and
* increases the progress as the promises resolve.
*
* @param $promise jQUery Promise
*/
(function() {
var initial = 0, current = 0;
GIFLoading.promise = function($promise) {
if (!$promise || $promise.state() === "resolved") {
return this;
}
if (current === 0) {
GIFLoading.start();
}
initial++;
current++;
$promise.always(function() {
current--;
if (current === 0) {
initial = 0;
GIFLoading.done();
} else {
GIFLoading.set((initial - current) / initial);
}
});
return this;
};
})();
/**
* (Internal) renders the progress bar markup based on the `template`
* setting.
*/
GIFLoading.render = function(fromStart) {
if (GIFLoading.isRendered()) return document.getElementById('GIFLoading');
addClass(document.documentElement, 'GIFLoading-busy');
var progress = document.createElement('div');
progress.id = 'GIFLoading';
progress.innerHTML = Settings.template;
var bar = progress.querySelector(Settings.barSelector),
perc = fromStart ? '-100' : toBarPerc(GIFLoading.status || 0),
parent = document.querySelector(Settings.parent),
spinner;
// css(bar, {
// transition: 'all 0 linear',
// transform: 'translate3d(' + perc + '%,0,0)'
// });
if (!Settings.showSpinner) {
spinner = progress.querySelector(Settings.spinnerSelector);
spinner && removeElement(spinner);
}
if (parent != document.body) {
addClass(parent, 'GIFLoading-custom-parent');
}
parent.appendChild(progress);
return progress;
};
/**
* Removes the element. Opposite of render().
*/
GIFLoading.remove = function() {
removeClass(document.documentElement, 'GIFLoading-busy');
removeClass(document.querySelector(Settings.parent), 'GIFLoading-custom-parent');
var progress = document.getElementById('GIFLoading');
progress && removeElement(progress);
};
/**
* Checks if the progress bar is rendered.
*/
GIFLoading.isRendered = function() {
return !!document.getElementById('GIFLoading');
};
/**
* Determine which positioning CSS rule to use.
*/
GIFLoading.getPositioningCSS = function() {
// Sniff on document.body.style
var bodyStyle = document.body.style;
// Sniff prefixes
var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
('MozTransform' in bodyStyle) ? 'Moz' :
('msTransform' in bodyStyle) ? 'ms' :
('OTransform' in bodyStyle) ? 'O' : '';
if (vendorPrefix + 'Perspective' in bodyStyle) {
// Modern browsers with 3D support, e.g. Webkit, IE10
return 'translate3d';
} else if (vendorPrefix + 'Transform' in bodyStyle) {
// Browsers without 3D support, e.g. IE9
return 'translate';
} else {
// Browsers without translate() support, e.g. IE7-8
return 'margin';
}
};
/**
* Helpers
*/
function clamp(n, min, max) {
if (n < min) return min;
if (n > max) return max;
return n;
}
/**
* (Internal) converts a percentage (`0..1`) to a bar translateX
* percentage (`-100%..0%`).
*/
function toBarPerc(n) {
return (-1 + n) * 100;
}
/**
* (Internal) returns the correct CSS for changing the bar's
* position given an n percentage, and speed and ease from Settings
*/
function barPositionCSS(n, speed, ease) {
var barCSS;
if (Settings.positionUsing === 'translate3d') {
barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
} else if (Settings.positionUsing === 'translate') {
barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
} else {
barCSS = { 'margin-left': toBarPerc(n)+'%' };
}
barCSS.transition = 'all '+speed+'ms '+ease;
return barCSS;
}
/**
* (Internal) Queues a function to be executed.
*/
var queue = (function() {
var pending = [];
function next() {
var fn = pending.shift();
if (fn) {
fn(next);
}
}
return function(fn) {
pending.push(fn);
if (pending.length == 1) next();
};
})();
/**
* (Internal) Applies css properties to an element, similar to the jQuery
* css method.
*
* While this helper does assist with vendor prefixed property names, it
* does not perform any manipulation of values prior to setting styles.
*/
var css = (function() {
var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
cssProps = {};
function camelCase(string) {
return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
return letter.toUpperCase();
});
}
function getVendorProp(name) {
var style = document.body.style;
if (name in style) return name;
var i = cssPrefixes.length,
capName = name.charAt(0).toUpperCase() + name.slice(1),
vendorName;
while (i--) {
vendorName = cssPrefixes[i] + capName;
if (vendorName in style) return vendorName;
}
return name;
}
function getStyleProp(name) {
name = camelCase(name);
return cssProps[name] || (cssProps[name] = getVendorProp(name));
}
function applyCss(element, prop, value) {
prop = getStyleProp(prop);
element.style[prop] = value;
}
return function(element, properties) {
var args = arguments,
prop,
value;
if (args.length == 2) {
for (prop in properties) {
value = properties[prop];
if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
}
} else {
applyCss(element, args[1], args[2]);
}
}
})();
/**
* (Internal) Determines if an element or space separated list of class names contains a class name.
*/
function hasClass(element, name) {
var list = typeof element == 'string' ? element : classList(element);
return list.indexOf(' ' + name + ' ') >= 0;
}
/**
* (Internal) Adds a class to an element.
*/
function addClass(element, name) {
var oldList = classList(element),
newList = oldList + name;
if (hasClass(oldList, name)) return;
// Trim the opening space.
element.className = newList.substring(1);
}
/**
* (Internal) Removes a class from an element.
*/
function removeClass(element, name) {
var oldList = classList(element),
newList;
if (!hasClass(element, name)) return;
// Replace the class name.
newList = oldList.replace(' ' + name + ' ', ' ');
// Trim the opening and closing spaces.
element.className = newList.substring(1, newList.length - 1);
}
/**
* (Internal) Gets a space separated list of the class names on the element.
* The list is wrapped with a single space on each end to facilitate finding
* matches within the list.
*/
function classList(element) {
return (' ' + (element && element.className || '') + ' ').replace(/\s+/gi, ' ');
}
/**
* (Internal) Removes an element from the DOM.
*/
function removeElement(element) {
element && element.parentNode && element.parentNode.removeChild(element);
}
return GIFLoading;
});</code></pre><h3>使用</h3><pre><code>import GIFLoading from '../js/gif-loading'
// 请求拦截器
axios.interceptors.request.use(
(config) => {
isShowLoading && GIFLoading.start()
})
响应拦截器
axios.interceptors.response.use(
(response) => {
isShowLoading && GIFLoading.done()
},(error) => {
// 对响应错误做点什么
// NProgress.done();
GIFLoading.done()
)</code></pre>
移动端调试面板插件vconsole
https://segmentfault.com/a/1190000037494730
2020-10-16T10:14:57+08:00
2020-10-16T10:14:57+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<p><a href="https://link.segmentfault.com/?enc=NtbU2rE7xRTFEvOusuMsJw%3D%3D.hl8CQv3ZP7WW%2BxSYGMFllNZO0H9EbgSeruYEMBUqcRoKiBanaog5gilzhOvn%2FJ6G" rel="nofollow">github地址</a></p><h4>1. 安装 vconsole-webpack-plugin 插件</h4><pre><code>npm install vconsole-webpack-plugin --save-dev</code></pre><h4>2. 修改配置文件vue.config.js</h4><pre><code>const vConsolePlugin = require('vconsole-webpack-plugin')
module.exports = {
configureWebpack: config => {
const debug = process.env.NODE_ENV !== 'production'
let pluginsDev = [
new vConsolePlugin({
filter: [],
enable: debug
})
]
config.plugins = [...config.plugins, ...pluginsDev]
}
}
或是
configureWebpack: config => {
config.plugins.push(
//手机端调试
new vConsole({
filter: [], // 需要过滤的入口文件
enable: isVConsole === true // 生产环境不打开
})
)
}
</code></pre><h4>3.使用</h4><pre><code>import Vue from 'vue'
import VConsole from 'vconsole'
const vConsole = new VConsole()
Vue.use(vConsole)</code></pre><h4>4.日志类型</h4><p>支持 5 种不同类型的日志,会以不同的颜色输出到前端面板:</p><pre><code class="jsx">console.log('foo'); // 白底黑字
console.info('bar'); // 白底紫字
console.debug('oh'); // 白底黄字
console.warn('foo'); // 黄底黄字
console.error('bar'); // 红底红字 </code></pre><p>支持以下 console 方法:</p><pre><code class="jsx">console.time('foo'); // 启动名为 foo 的计时器
console.timeEnd('foo'); // 停止 foo 计时器并输出经过的时间 </code></pre><p>Object/Array 结构化展示<br>支持打印 Object 或 Array 变量,会以结构化 JSON 形式输出(并折叠):</p><pre><code class="jsx">var obj = {};
obj.foo = 'bar';
console.log(obj); </code></pre><p>多态<br>支持传入多个参数,会以空格隔开:</p><pre><code class="jsx">var uid = 666;
console.log('UserID:', uid); // 打印出 UserID: 233 </code></pre><h4>5:公共属性及方法</h4><pre><code class="dart">//当前 vConsole 的版本号。
vConsole.version
//显示 vConsole 主面板
vConsole.show()
//隐藏 vConsole 主面板
vConsole.hide()
//析构一个 vConsole 对象实例,并将 vConsole 面板从页面中移除。
var vConsole = new VConsole();
vConsole.destroy();
//显示 vConsole 的开关按钮。
vConsole.showSwitch()
//隐藏 vConsole 的开关按钮
vConsole.hideSwitch() </code></pre><h2>vConsole.option配置项。</h2><p><img src="/img/bVcHufg" alt="image.png" title="image.png"></p><pre><code class="csharp">// get
vConsole.option // => {...}
// set
vConsole.setOption('maxLogNumber', 5000);
// 或者:
vConsole.setOption({maxLogNumber: 5000});</code></pre><p><a href="https://link.segmentfault.com/?enc=61yn%2F3e1aBPCCF3D25u2Sw%3D%3D.6vnA9p6rYqa0VLt3AQD5brFztcSGSNru6AgErQvmXMZYw4kR%2Bow2Pn3dPZkD8CQguF43Iy7jAhIJv%2F4YdfUlDQ%3D%3D" rel="nofollow">其他的调试工具</a></p>
vue中H5的progress标签仿写一个进度条和逻辑
https://segmentfault.com/a/1190000037473686
2020-10-14T17:10:57+08:00
2020-10-14T17:10:57+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<h6>要求:</h6><p>*载入:<br>0~5秒,每秒增加8%进度;<br>5~10秒,每秒增加4%进度;<br>10~20秒,每秒增加2%进度;<br>超过20秒,每秒增加0.2%进度,最多不超过95%*</p><pre><code><template>
<progress :value="progressValue" max="100" class="pr"></progress>
</template>
<script>
data(){
return{
progressValue: 0,
}
}
methods:{
// 进度条的控制
getProgress() {
let num = 0;
let progress = 0.0;
this.firstTime = setInterval(() => {
num += 1;
if (num <= 5) {
progress += 8;
} else if (num <= 10) {
progress += 4;
} else if (num <= 20) {
progress += 2;
} else if (num <= 30) {
if (progress < 95){
progress += 0.2;
}
}else{
clearInterval(this.firstTime)
}
this.progressValue = progress;
var picInstance = progress - 2;
this.$refs.followPic.style.left = picInstance + "%";
}, 1000);
},
// 接口返回的状态是成功的时候调用
progerssValue(num,timer) {
clearInterval(this.firstTime)
this.secondTime = setInterval(() => {
if (this.progressValue >= num) {
clearInterval(this.secondTime)
}else{
this.progressValue += 1
let picInstance = this.progressValue - 2
this.$refs.followPic.style.left = picInstance + '%'
}
},timer)
},
getData(){
getData().then((res)=>{
if(res.code==0){
this.progerssValue(100,5)
}else{
clearInterval(this.firstTime)//停止进度条
}
})
}
}
mounted() {
this.getData()
this.getProgress()
}
</script>
<style>
.progress-box {
position: relative;
width: 67%;
height: 0.25rem;
line-height: 0.25rem;
margin: 0rem 0 0.3rem 14vw;
.pr {
width: 100%;
height: 0.15rem;
background: #ededed;
border-radius: 15px;
}
progress::-webkit-progress-bar {
background: #ededed;
border-radius: 15px;
}
progress::-webkit-progress-value {
border-radius: 15px;
background-image: url(./img/progress.png);
}
}
<style></code></pre>
webpack封装配置详解
https://segmentfault.com/a/1190000037432117
2020-10-11T16:43:30+08:00
2020-10-11T16:43:30+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<p><strong>更详尽</strong></p><pre><code>https://blog.csdn.net/qq_24147051/article/details/81533739</code></pre><h3>config/dev.env.js</h3><pre><code>'use strict'
// 引入webpack-merge模块
const merge = require('webpack-merge')
// 引入刚才打开的prod.env.js
const proEnv = require('./prod.env')
// 引入webpack-merge后这个文件又引入了prod.env.js,接着就将prod.env.js的配置和新的配置,即指明开发环境(development)进行了merge。(我有点儿不太理解为什么要这样做,如果不merge直接写module.exports={NODE_ENV:'"development'}也是可以的,难道是为了优雅降级?)
module.exports = merge(proEnv,{
NODE_ENV:'"development"'
}) </code></pre><h3>config/prod.env.js</h3><pre><code>'use strict'
module.exports = {
// 执行环境
NODE_ENV:'"production"'
}</code></pre><h3>config/index.js</h3><pre><code>/*
* @Author: yang
* @Date: 2020-10-12 12:58:04
* @LastEditors: yang
* @LastEditTime: 2020-10-14 13:41:33
* @FilePath: \新建文件夹 (4)\config\index.js
*/
'use strict'
const path = require('path')
module.exports = {
dev:{
// Path
assetsSubDirectory:'static',//子目录,一般存放css,js,image等文件
assetsPublicPath:'/',//根目录
proxyTable:{},//可利用该属性解决跨域的问题
//Various Dev Server settings
host:'localhost', //can be overwritten by progress.env.HOST 地址
port:8080,//可以被覆盖进程.env.PORT,如果端口正在使用,则将确定一个空闲端口 端口号设置,端口号占用出现问题可在此处修改
autoOpenBrowser:false,
errorOverlay:true,//浏览器错误提示
notifyOnErrors:true,//跨平台错误提示
poll:false,//使用文件系统(file system)获取文件改动的通知devServer.watchOptions
useEslint:true,//是否开启eslint 错误显示在console
showEslintErrorsInOverlay:false,//如果开启错误显示在浏览器中
devtool:'eval-source-map',
//使缓存失效
cacheBusting:true, //如果在devtools中调试vue文件遇到问题,把这个设置为false
cssSourceMap:false//代码压缩后进行bug定位困难,true开启 sourcemap记录压缩前后的位置信息记录,当产生错误可直接定位到未压缩前的位置,方便调试
},
build:{
// 生产环境下面的配置
index:path.resolve(__dirname,'../dist/index.html'),//index 编译后生成的位置和名字
assetsRoot:path.resolve(__dirname,'../dist'),//编译后存放生成环境代码的位置
assetsSubDirectory:'static',//js ,css img存放文件夹名
assetsPublicPath:'/',//发布的根目录,通常本地打包dist后打开文件会报错,此处修改为./。如果是上线的文件,可根据文件存放位置进行更改路径
productionSourceMao:true,//设置生产环境的 source map 开启与关闭。是否生成 sourceMap 文件
devtool:'#source-map',//生成.map文件
productionGzip: false,//unit的gzip命令用来压缩文件 gzip模式下需要压缩的文件的扩展名有css和js
productionGzipExtensions:['js','css'],
bundleAnalyzerReport:process.env.npm_config_report//build 对象的 bundleAnalyzerReport 优化打包之后文件提交的工具。是否开启打包后的分析报告
}
}
</code></pre><h2>utils.js</h2><pre><code>// utils.js文件主要是用来处理各种css loader的,比如css-loader,less-loader等。
// 引入path模块
const path = require('path')
// 引入之前的config模块
const config = require('../config')
// 引入"extract-text-webpack-plugin"它的作用是打包后将生成的css文件通过link的方式引入到html中,如果不使用这个插件,那么css就打包到<head>的style中
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// 引入package.json
const pkg = require('../package.json')
exports.assetsSubDirectory = function (_path) {
// 结合config文件的代码可以知道 当环境为生产环境时 assetsSubDirectory为static开发环境也是static
const assetsSubDirectory =
progress.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
// path.posix.join()是path.join的一种兼容写法 它的作用是路径的拼接 比如path.posix.join('/aa/s','bb')
return path.posix.join(assetsSubDirectory,_path)
}
// 用来生成Loader的函数,本身可以用在vue-loader的配置上,同时也是为styleLoader函数使用
exports.cssLoaders = function(options){
// 如果没有传参就默认空对象
options = options||{}
// 配置css-loader,css-loader可以让处理css中的@import或者url()
const cssLoader = {
loader:'css-loader',
options:{
sourceMap:options.sourceMap
}
}
// 配置postcss-loader,主要功能是补全css中的兼容性前缀 比如“-webkit-”等
var postcssLoader = {
loader:"postcss-laoder",
options:{
sourceMap:options.sourceMap
}
}
// 生成loader的私有方法
function generateLoaders(loader,loaderOptions){
// 参数的usePostCss属性是否为true 是就使用两个loader,否则只使用css-loader
const loaders = options.usePostCSS? [cssLoader,postcssLoader]:[cssLoader]
if(loader){
// 给generateLoaders传loader参数的话 比如less 或者sass 就将这个loader的配置传入到loaders数组中
loaders.push({
loader:loader+'-loader',
// Object.assign()是es6的语法 用来合并对象
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// 如果options参数的extract属性为true 即使用extract text plugin 将css抽成单独的文件 否则就将css写进style
if(options.extract){
return ExtractTextPlugin.extract({
use:loaders,
// vue-style-loader可以理解为vue版的style-loader 它可将css放进style中
fallback:'vue-style-loader'
})
}else{
return ['vue-style-loader'].concat('loaders')
}
}
return {
// 返回各种loader
css:generateLoaders(),
postcss:generateLoaders(),
less:generateLoaders('less'),
sass:generateLoaders('sass',{indentedSyntax:true}),
scss:generateLoaders('sass'),
stylus:generateLoaders('stylus'),
styl:generateLoaders('stylus')
}
}
// 生成开发环境下的loader的配置,使用在(webpack.dev.config.js中)
exports.styleLoaders = function(options){
const output = []
// 调用cssLoader方法 返回loaders的对象
const loaders = exports.cssLoaders(options)
// 遍历每一个loader 并配置成对应的格式 传给output数组
for(const extension in loaders){
const loader = loaders[extension]
output.push({
test:new RegExp('\\.'+extension + '$'),
use:loader
})
}
return output
}
</code></pre><h3>vue-loader.config</h3><pre><code>'use strict'
// 引入上面的utils文件
const utils = require('./utils')
// 引入config文件
const config = require('./config')
// 判断当前是否为生产环境,如果是则返回true
const isProdction = progress.env.NODE_ENV === 'prodction'
// 是否使用sourceMap如果是生产环境就使用config文件中的index.js中的生产环境配置,否则是否开发环境的配置
const sourceMapEnabled = isProdction ? config.build.productionSourceMap:config.dev.cssSourceMap
module.exports = {
//utils文件cssLoaders返回的配置项,返回的格式为
// loaders:{
// css:ExtractTextPlugin.extract({
// use:[cssLoader],
// extract默认行为先使用css-loader编译css,如果一切顺利的话,结束之后把css导出到规定的文件去。但是如果编译过程中出现了错误,则继续使用vue-style-loader处理css。
// fallback:'vue-style-loader'
// }),
// postCss:{
// ...
// }
// }
loaders:utils.cssLoaders({
sourceMap:sourceMapEnabled,
extract:isProdction
}),
// 是否使用sourceMap
cssSourceMap:sourceMapEnabled,
// 是否使用cacheBusting,这个配置在config的文件
cacheBusting:config.dev.cacheBusting
}</code></pre><h3>webpack.base.config.js</h3><pre><code>/*
* @Author: yang
* @Date: 2020-10-16 13:48:48
* @LastEditors: yang
* @LastEditTime: 2020-10-17 14:43:50
* @FilePath: \新建文件夹 (4)\webpack.base.config.js
*/
const path = require('./path')
const utils = require('./utils')
const config = require('./config')
const vueLoaderConfig = require('./vue-loader.config')
// resolve函数返回根路径下的文件或文件夹
function resolve(dir){
return path.join(__dirname,'..',dir)
}
module.exports = {
// 返回根路径
context:path.resolve(__dirname,'../'),
// 设置入口文件
entry:{
app:'./src/main.js'
},
// 设置出口文件
output:{
//根据config模块得知是根目录下的dist文件夹
path:config.build.assetsRoot,
filename:'[name].js',
//公共路径统一为'/'
publicPath:ProgressEvent.env.NODE_ENV === 'prodution'?config.build.assetsPublicPath:config.dev.assetsPublicPath
},
resolve:{
//自动解析的扩展,js vue ,json这三种格式得文件引用时不需要加上扩展了
//import File from '../path/fo/file'
extensions:['.js','.vue','.json'],
alias:{
// 精准匹配,使用vue来替代vue/dist/vue.esm.js路径
'vue$':'vue/dist/vue.esm.js',
'@':resolve('src')
}
},
module:{
rules:[
//vue-loader,module
{
test: /\.vue$/,
loader:'vue-loader',
options:vueLoaderConfig
},
{
test:/\.js$/,
loader:'babel-loader',
// 把要处理的目录包括进来
include:[resolve('src'),resolve('test'),resolve('node_modules/webpack-dev-server/client')]
},
{
test:/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader:'url-loader',
options:{
limit:10000,//超过10kb的才使用url-loader来映射到文件
name:utils.assetsPath('media/[name].[hash:7].[ext]')//其他的资源转移到静态资源文件夹
}
},
{
test:/\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader:'url-loader',
options:{
limit:10000,
name:utils.assetsPublicPath('fonts/[name].[hash:7].[text]')
}
}
]
}
}</code></pre><h3>webpack.dev.conf.js</h3><pre><code>/*
* @Author: yang
* @Date: 2020-10-17 16:16:57
* @LastEditors: yang
* @LastEditTime: 2020-10-18 14:24:04
* @FilePath: \新建文件夹 (4)\webpack.dev.conf.js
*/
const utils = require('./utils')
const webpack = require('webpack')
const config = require('./config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.config')
// 拷贝资源的插件
const CopyWebapckPlugin = require('copy-webpack-plugin')
// 生成html模板的插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 展示错误日志的插件
const FriendlyErrorsPlugin = require('frendly-errors-webpack-plugin')
// 一个自动打开可用端口的包
const portfinder = require('portfinder')
// 当前环境的host
const HOST = progress.env.HOST
//当前环境的port
const PORT = progress.env.PORT&&Number(progress.env.PORT)
// 开发环境的配置
const devWebpackConfig = merge(baseWebpackConfig,{
module:{
//loader的配置
rules:utils.styleLoaders({
sourceMap:config.dev.cssSourceMap,usePostCSS:true
}),
devtool:config.dev.devtool,
devServer:{
// 重新加载server时,控制台对一些错误以warning的方式提示
clientLogLevel:'warning',
//当使用HTML5 history API时 任意的404响应都可能需要被替代为index.html
historyApiFallback:{
rewrites:[
{from:/.*/,to:path.posix.join(config.dev.assetsSubDirectory,'index.html')}
]
},
// 启用webpack的模块热替换特性
hot:true,
//告诉服务器从哪里提供内容。只有在你想要提供静态文件是才需要
contentBase:false,
// 是否压缩
compress:false,
host:HOST||config.dev.host,
port:PORT||config.dev.port,
// 是否自动打开浏览器
open:config.dev.autoOpenBrowser,
// 编译出错时是否有提示
overlay:config.dev.errorOverlay?{warning:false,errors:true}:false,
//静态内容的路径,此路径下的打包文件可在浏览器中访问
publicPath:config.dev.assetsPublicPath,
//接口的代理
proxy:config.dev.proxyTable,
// 启用quiet后,除了初始启动信息外的任务内容都不会被打印到控制台。这也意味着来自webpack的错误或警告在控制台不可见
quite:true,
// 监视文件的选项
watchOptions:{
poll:config.dev.poll
}
},
plugins:[
// DefinePlugin允许创建一个在编译时可以配置的全局常量。这里生成了一个当前环境的常量
new webpack.DefinePlugin({
'progress.env':require('../config/dev.env')
}),
//模块热替换插件 修改模块是不需要刷新页面
new webpack.HotModuleReplacementPlugin(),
//当开启HMR的时候使用该插件会显示模块的相对路径
new webpack.NamedModulesPlugin(),
// 在编译出现错误时,使用NoEmitOnErrorsPlugin来跳过输出阶段 这样可以确保输出资源不会包含错误
new webpack.NoEmitOnErrorsPlugin(),
new HtmlWebpackPlugin({
filename:'index.html',
template:'index.html',
// 打包后js文件放在body的后面
inject:true
}),
//将static的内容拷贝到开发路径,忽略这个文件夹下的'.XX'的文件
new CopyWebapckPlugin([
{
from:path.resolve(__dirname,'../static'),
to:config.dev.assetsSubDirectory,
ignore:['.*']
}
])
]
}
})</code></pre><h3><strong>webpack.prod.conf.js</strong></h3><pre><code>/*
* @Author: yang
* @Date: 2020-10-17 14:45:20
* @LastEditors: yang
* @LastEditTime: 2020-10-17 16:15:15
* @FilePath: \新建文件夹 (4)\webpack.prod.conf.js
*/
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('./config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.config')
// 资源拷贝的插件
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 把css打包成css文件已link的方式引入包
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// 压缩css的包
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
// 压缩js代码的包
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = require('./config/prod.env')
const webpackConfig = merge(baseWebpackConfig,{
module:{
//loader配置,可在介绍utils的文章中查看
rules:utils.styleLoaders({
sourceMap:config.build.productionSourceMap,
extract:true,
usePostCSS:true
})
},
devtool:config.build.productionSourceMap? config.build.devtool:false,
output:{
path:config.build.assetsRoot,
filename:config.build.assetsPath('js/[name].[chunkhash].js'),
//chunkFilename用于命名那些异步加载的模块,比如通过require.ensure()
chunkFilename:utils.assetsPath('js/[id].[chunkhash].js')
},
plugins:[
new webpack.DefinePlugin({
'progress.env':env
}),
//压缩js代码
new UglifyJsPlugin({
uglifyOptions:{
compress:{
warnings:false
}
},
sourceMap:config.build.productionSourceMap,
parallel:true
}),
// 从所有额外的chunk(additional chunk)提取(默认情况下,它仅从初Zchunk(init chunk)中提取)
// 当使用CommonsChunkPlugin 并且在公共chunk中提取的chunk(来自ExtractText)时 allChunks **必须设置为true
new ExtractTextPlugin({
filename:utils.assetsPath('css/[name].[contenthash].css'),
allChunks:true,
}),
//压缩css
//优化最小化css代码 如果只简单使用extract-text-plugin可能会造成css重复
new OptimizeCSSPlugin({
cssProgressorOptions:config.build.productionSourceMap?
{safe:true,map:{inline:false}}:{safe:true}
}),
//在dist目录生成html文件
//将产品文件的引用注入到index.html
new HtmlWebpackPlugin({
filename:config.build.index,
template:'index.html',
inject:true,
minify:{
// 删除index.html中的注释
removeComments:true,
// 删除index.html中的空格
collapseWhitespace:true,
//删除各种html标签属性值的双引号
removeAttributeQuotes:true,
},
// 注入依赖的时候按照依赖先后顺序进行注入,比如 需要先注入vendor.js,再注入app.js
chunksSortMode:"dependency"
}),
// 该插件会根据模块的相对路径生成一个四位数的hash作为模块id 建议用于生产环境
new webpack.HashedModuleIdsPlugin(),
// 去webpack打包时的一个取舍是将bundle中各个模块单独打包成闭包 这些打包函数使你的js在浏览器中处理的很慢,
//相比之下,一些工具 想Closure Compiler和RollupJS可以提升(hoist)或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度
new webpack.optimize.ModuleConcatenationPlugin(),
// 将第三方的包分离出来 将所有从node-modules中引的js提取到vendor.js,即抽取文件
new webpack.optimize.CommonsChunkPlugin({
name:'vendor',
minChunks(module){
return(
module.resource&&/\.js$/.test(module.resource)&&module.resource.indexof(path.join(__dirname,'../node_modules'))===0
)
}
}),
// 为了避免每次更改项目代码时导致venderchunk的chunkHash改变,我们还会单独生成一个manifestchunk
new webpack.optimize.CommonsChunkPlugin({
name:'manifest',
minChunks:Infinity
}),
// 我们主要逻辑的js文件
new webpack.optimize.CommonsChunkPlugin({
name:'app',
async:'vendor-async',
children:true,
minChunks:3
}),
// 拷贝资源 将static文件夹里面的静态资源复制到dist/static
new CopyWebpackPlugin([
{
from:path.resolve(__dirname,'../static'),
to:config.build.assetsSubDirectory,
ignore:['.*']
}
])
]
})
module.exports = webpackConfig
</code></pre>
app端部分机型关于使用flex的兼容适配
https://segmentfault.com/a/1190000024567494
2020-09-24T10:45:48+08:00
2020-09-24T10:45:48+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<pre><code>/* flex弹性盒布局兼容性写法样式文件
* 常用类
* display__flex
* flex_direction__column
* flex_wrap__wrap
* justify_content__center
* justify_content__space_between
* justify_content__space_around
* align_items__center
* flex_grow__1
* flex_shrink__0
*/
/*
设置在弹性容器上的属性
*/
.display__flex {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
/*flex-direction属性决定主轴的方向(即项目的排列方向)。*/
.flex_direction__row_reverse {
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse;
}
.flex_direction__column {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
.flex_direction__column_reverse {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse;
}
/*默认情况下,项目都排在一条线(又称"轴线")上。flex-wrap属性定义,如果一条轴线排不下,如何换行。*/
.flex_wrap__nowrap {
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
}
.flex_wrap__wrap {
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.flex_wrap__wrap_reverse {
-ms-flex-wrap: wrap-reverse;
flex-wrap: wrap-reverse;
}
/*justify-content属性定义了项目在主轴上的对齐方式。*/
.justify_content__flex_start {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.justify_content__flex_end {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
}
.justify_content__center {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
}
.justify_content__space_between {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.justify_content__space_around {
-ms-flex-pack: distribute;
justify-content: space-around;
}
/*align-items属性定义项目在交叉轴上如何对齐。*/
.align_items__flex_start {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
.align_items__flex_end {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
}
.align_items__center {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.align_items__baseline {
-webkit-box-align: baseline;
-ms-flex-align: baseline;
align-items: baseline;
}
/*align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。*/
.align_content__flex_start {
-ms-flex-line-pack: start;
align-content: flex-start;
}
.align_content__flex_end {
-ms-flex-line-pack: end;
align-content: flex-end;
}
.align_content__center {
-ms-flex-line-pack: center;
align-content: center;
}
.align_content__space_between {
-ms-flex-line-pack: justify;
align-content: space-between;
}
.align_content__space_around {
-ms-flex-line-pack: distribute;
align-content: space-around;
}
/*
设置在弹性项目上的属性
*/
/*order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。以下是兼容样式写法示例,可根据需要修改属性值。*/
.order__1 {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
/*flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。以下是兼容样式写法示例,可根据需要修改属性值。*/
.flex_grow__1 {
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
}
/*flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。以下是兼容样式写法示例,可根据需要修改属性值。*/
.flex_shrink__0 {
-ms-flex-negative: 0;
flex-shrink: 0;
}
/*
flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
以下是兼容样式写法示例,可根据需要修改属性值。
*/
.flex_basis__100px {
-ms-flex-preferred-size: 100px;
flex-basis: 100px;
}
/*align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。*/
.align_self__flex_start {
-ms-flex-item-align: start;
align-self: flex-start;
}
.align_self__flex_end {
-ms-flex-item-align: end;
align-self: flex-end;
}
.align_self__center {
-ms-flex-item-align: center;
align-self: center;
}
.align_self__baseline {
-ms-flex-item-align: baseline;
align-self: baseline;
}</code></pre><h3>safari</h3><blockquote>父元素设为display:flex;没有问题,但子元素flex:1这种标注在safari中不能用!子元素使用的话只能设为flex:auto,如果想实现flex:1这种效果,请用:</blockquote><pre><code>flex-grow:1;
flex-shrink:1;
flex-basis:0;</code></pre>
Vue.js 独立构建和运行时构建的区别
https://segmentfault.com/a/1190000024560834
2020-09-23T17:09:57+08:00
2020-09-23T17:09:57+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
2
<p>在使用 Vue.js 2.0 时,有独立构建(standalone)和运行时构建(runtime-only)两种版本可供选择。而在 Vue.js 1.0 中,却没有这种版本区别。到底该使用哪一个版本?这让我有点懵逼的感觉。本着知其然还要知其所以然的精神,我决定好好研究下两者间的区别。</p><p>Vue.js 的官方教程上是这么说的:</p><blockquote><ul><li>独立构建包括编译和支持 template 选项。 它也依赖于浏览器的接口的存在,所以你不能使用它来为服务器端渲染。</li></ul></blockquote><ul><li>运行时构建不包括模板编译,不支持 template 选项。运行时构建,可以用 render 选项,但它只在单文件组件中起作用,因为单文件组件的模板是在构建时预编译到 render 函数中,运行时构建只有独立构建大小的 30%,只有 16Kb min+gzip 大小。</li></ul><p>看了半天,实在没搞清两者之间的区别。经过一番搜索,终于搞清楚了问题的本源,且听我娓娓道来。</p><p>Vue.js 的运行过程实际上包含两步。第一步,编译器将字符串模板(template)编译为渲染函数(render),称之为编译过程;第二步,运行时实际调用编译的渲染函数,称之为运行过程。</p><p>由于 Vue.js 1.0 的编译过程需要依赖浏览器的 DOM,所以无法(或者说没有意义)将编译器和运行时分开。因此在 Vue.js 1.0 分发包中,编译器和运行时是打包在一起,都在浏览器端执行。</p><p>然而到了 Vue.js 2.0,为了支持服务端渲染(server-side rendering),编译器不能依赖于 DOM,所以必须将编译器和运行时分开。这就形成了独立构建(编译器 + 运行时)和运行时构建(仅运行时)。显而易见,运行时构建要小于独立构建。</p><p>在现代前端工程构建中,通常会使用 vue-loader 和 vueify 预编译模板。在这种情况下,只需要打包运行时,而不需要打包编译器,运行时构建即可满足所需。当然,如果你需要在前端使用 template 选项实时编译模板,那么还是需要使用独立构建将编译器发送到浏览器。</p>
vue组件略佳操作
https://segmentfault.com/a/1190000024526776
2020-09-21T10:26:08+08:00
2020-09-21T10:26:08+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<h3>button组件</h3><p>亮点:prop接收参数</p><pre><code><template>
<button :class="'i-button-size' + size" :disabled="disabled">
</button>
</template>
<script>
// 判断参数是否是其中之⼀
function oneOf (value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) { return true; }
}
return false;
}
export default {
props: {
size: {
validator (value)
{ return oneOf(value, ['small', 'large', 'default']); },
default: 'default' },
disabled: {
type: Boolean, default: false
} } }
</script></code></pre><p><strong>使⽤组件</strong>:</p><pre><code><i-button size="large">
</i-button>
<i-button disabled></i-button></code></pre><h2>mixins</h2><p><em>如果你的项⽬⾜够复杂,或需要多⼈协同开发时,在 app.vue ⾥会 写⾮常多的代码,多到结构复杂难以维护。这时可以使⽤ Vue.js 的 混合 mixins,将不同的逻辑分开到不同的 js ⽂件⾥。</em><br><strong>user.js</strong></p><pre><code>export default {
data () { return { userInfo: null } },
methods: {
getUserInfo(){
$.ajax('/user/info', (data) => { this.userInfo = data; }); } },
mounted () {
this.getUserInfo();
} }</code></pre><p><strong>然后在 app.vue 中混合: app.vue:</strong></p><pre><code><script>
import mixins_user from '../mixins/user.js'
export default {
mixins: [mixins_user],
data() {
return {}
},
}
</script></code></pre><p><strong>跟⽤户信息相关的逻辑,都可以在 user.js ⾥维护</strong></p><h2>$on 与 $emit</h2><p><em>$emit 会在当前组件实例上触发⾃定义事件,并传递⼀些参数给监 听器的回调,⼀般来说,都是在⽗级调⽤这个组件时,使⽤ @on 的 ⽅式来监听⾃定义事件的,⽐如在⼦组件中触发事件:$on 监听了⾃⼰触发的⾃定义事件 test,因为有时不确定何时会触 发事件,⼀般会在 mounted 或 created 钩⼦中来监听。</em><br><strong>子组件</strong></p><pre><code> methods: {
handleEmitEvent() {
this.$emit('test', 'Hello Vue.js')
},
},</code></pre><p><strong>父组件</strong></p><pre><code> mounted() {
this.$on('test', (text) => {
window.alert(text)
})
},</code></pre><h2>⾃⾏实现 dispatch 和 broadcast ⽅法</h2><p>*思路:<br>在⼦组件调⽤ dispatch ⽅法,向上级指定的组件实例(最近 的)上触发⾃定义事件,并传递数据,且该上级组件已预先通 过 $on 监听了这个事件; <br>相反,在⽗组件调⽤ broadcast ⽅法,向下级指定的组件实例 (最近的)上触发⾃定义事件,并传递数据,且该下级组件已 预先通过 $on 监听了这个事件。*<br><em>该⽅法可能在很多组件中都会使⽤,复⽤起⻅,我们封装在混合(mixins)⾥。那它的使⽤样例可能是这样的:</em><br><strong>有 A.vue 和 B.vue 两个组件,其中 B 是 A 的⼦组件,中间可能跨多级,在 A 中向 B 通信:</strong></p><pre><code><!-- A.vue -->
<template><button @click="handleClick">触发事件</button></template>
<script>
import Emitter from '../mixins/emitter.js'
export default {
name: 'componentA',
mixins: [Emitter],
methods: {
handleClick() {
this.broadcast('componentB', 'on- message', 'Hello Vue.js')
},
},
}
</script></code></pre><pre><code>// B.vue
export default {
name: 'componentB',
created () {
this.$on('on-message', this.showMessage);
},
methods: {
showMessage (text) { window.alert(text);
} } }</code></pre><p><strong>在独⽴组件 (库)⾥,每个组件的 name 值应当是唯⼀的,name 主要⽤于递归 组件</strong></p><h5>emitter.js</h5><pre><code>function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name ===componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
} })
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent; if (parent) {
name = parent.$options.name;
} }
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
} },
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
} };</code></pre><p><strong>同理,如果是 B 向 A 通信,在 B 中调⽤ dispatch ⽅法,在 A 中使 ⽤ $on 监听事件即可。</strong></p><hr><p>因为是⽤作 mixins 导⼊,所以在 methods ⾥定义的 dispatch 和 broadcast ⽅法会被混合到组件⾥,⾃然就可以⽤ this.dispatch 和 this.broadcast 来使⽤。 </p><p>这两个⽅法都接收了三个参数,第⼀个是组件的 name 值,⽤于向上 或向下递归遍历来寻找对应的组件,第⼆个和第三个就是上⽂分析的 ⾃定义事件名称和要传递的数据。 可以看到,在 dispatch ⾥,通过 while 语句,不断向上遍历更新当 前组件(即上下⽂为当前调⽤该⽅法的组件)的⽗组件实例(变量 parent 即为⽗组件实例),直到匹配到定义的 componentName 与 某个上级组件的 name 选项⼀致时,结束循环,并在找到的组件实例 上,调⽤ $emit ⽅法来触发⾃定义事件 eventName。broadcast ⽅法与之类似,只不过是向下遍历寻找。</p><hr><p>*相⽐ Vue.js 1.x,有以下不同: <br>需要额外传⼊组件的 name 作为第⼀个参数; <br>⽆冒泡机制;<br>第三个参数传递的数据,只能是⼀个(较多时可以传⼊⼀个对 象),⽽ Vue.js 1.x 可以传⼊多个参数,当然,你对 emitter.js 稍作修改,也能⽀持传⼊多个参数,只是⼀般场景 传⼊⼀个对象⾜以*</p><h2>组件的通信找到任意组件实例 ——findComponents 系列⽅法</h2><p><em>是组件通信的终极⽅案。通过递归、遍历,找到指定组件的 name 选项 匹配的组件实例并返回。 findComponents 系列⽅法最终都是返回组件的实例,进⽽可以读 取或调⽤该组件的数据和⽅法</em><br>*它适⽤于以下场景: <br>由⼀个组件,向上找到最近的指定组件;<br>由⼀个组件,向上找到所有的指定组件; <br>由⼀个组件,向下找到最近的指定组件; <br>由⼀个组件,向下找到所有指定的组件;<br>由⼀个组件,找到指定组件的兄弟组件。*</p><h4>utils/assits.js</h4><pre><code>// 由⼀个组件,向上找到最近的指定组件
//context 上下文
//componentName 组件名称
function findComponentUpward (context, componentName) {
let parent = context.$parent;
let name = parent.$options.name;
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}
// 由⼀个组件,向上找到所有的指定组件
function findComponentsUpward(context, componentName) {
let parents = []
const parent = context.$parent
if (parent) {
if (parent.$options.name === componentName) parents.push(parent)
return parents.concat(findComponentsUpward(parent, componentName))
} else {
return []
}
},
// 由⼀个组件,向下找到最近的指定组件
function findComponentDownward(context, componentName) {
//context.$children 得到的是当前组件的全部⼦组件
const childrens = context.$children
let children = null
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name
if (name === componentName) {
children = child
break
} else {
children = findComponentDownward(child, componentName)
if (children) break
}
}
}
return children
},
// 由⼀个组件,向下找到所有指定的组件
//使⽤ reduce 做累加器,为数组中的每一个元素依次执行回调函数,并 ⽤递归将找到的组件合并为⼀个数组并返回,
function findComponentsDownward(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child)
const foundChilds = findComponentsDownward(child, componentName)
return components.concat(foundChilds)
}, [])
},
// 由⼀个组件,找到指定组件的兄弟组件
function findBrothersComponents(context, componentName, exceptMe = true) {
//⽗组件的 全部⼦组件,这⾥⾯当前包含了本身
//exceptMe true是不包括自己
let res = context.$parent.$children.filter((item) => {
return item.$options.name === componentName
})
// Vue.js 在渲染组件时,都会给每个组件加⼀个内置的属 性 _uid,这个 _uid 是不会重复的,借此我们可以从⼀系列兄弟组 件中把⾃⼰排除掉
let index = res.findIndex((item) => item._uid === context._uid)
if (exceptMe) res.splice(index, 1)
return res
},
export { findComponentUpward,findComponentsUpward,findComponentDownward,findComponentsDownward,findBrothersComponents };</code></pre><p><strong>使用(A 是 B 的⽗组件)</strong></p><pre><code><!-- component-a.vue -->
<template>
<div>组件 A <component-b></component-b></div>
</template>
<script>
import componentB from './component-b.vue'
import { findComponentDownward } from '../utils/assist.js';
export default {
name: 'componentA',
components: { componentB },
data() {
return { name: 'Aresn' }
},
methods: {
sayHello() {
console.log('Hello, Vue.js')
},
},
mounted(){
const comB = findComponentDownward(this, 'componentB');
if (comB) { console.log(comB.name);
}
}
</script></code></pre><pre><code><!-- component-b.vue -->
<template>
<div>组件 B</div>
</template>
<script>
import { findComponentUpward,findBrothersComponents } from '../utils/assist.js'
export default {
name: 'componentB',
mounted() {
const comA = findComponentUpward(this, 'componentA')
if (comA) {
console.log(comA.name) // Aresn
comA.sayHello() // Hello, Vue.js
}
const comsB = findBrothersComponents(this, 'componentB');
console.log(comsB); // ① [],空数组
//如果在 A 中再写⼀个 B:这时就会打印出 [VueComponent],有⼀个组件了
},
}
</script></code></pre><h2>组合多选框组件—— CheckboxGroup & Checkbox</h2><p><strong>checkbox.vue</strong><br><em>updateModel⽅法在 Checkbox ⾥的 mounted 初始化时调⽤。这 个⽅法的作⽤就是在 CheckboxGroup ⾥通过 findComponentsDownward ⽅法找到所有的 Checkbox,然后把 CheckboxGroup 的 value,赋值给 Checkbox 的 model,并根 据 Checkbox 的 label,设置⼀次当前 Checkbox 的选中状态。 这样⽆论是由内⽽外选择,或由外向内修改数据,都是双向绑定的, ⽽且⽀持动态增加 Checkbox 的数量。</em></p><pre><code><!-- checkbox.vue -->
<template>
<label>
<span>
<input
v-if="group"
type="checkbox"
:disabled="disabled"
:value="label"
v-model="model"
@change="change"
/>
<input
v-else
type="checkbox"
:disabled="disabled"
:checked="currentValue"
@change="change"
/>
</span>
<slot></slot>
</label>
</template>
<script>
import { findComponentUpward } from '../../utils/assist.js'
export default {
name: 'iCheckbox',
props: {
disabled: { type: Boolean, default: false },
value: { type: [String, Number, Boolean], default: false },
trueValue: { type: [String, Number, Boolean], default: true },
falseValue: { type: [String, Number, Boolean], default: false },
label: { type: [String, Number, Boolean] },
},
data() {
return {
currentValue: this.value,
model: [],
group: false,
parent: null,
}
},
methods: {
change(event) {
if (this.disabled) {
return false
}
const checked = event.target.checked
this.currentValue = checked
const value = checked ? this.trueValue : this.falseValue
this.$emit('input', value)
if (this.group) {
this.parent.change(this.model)
} else {
this.$emit('on-change', value)
this.dispatch('iFormItem', 'on-form-change', value)
}
},
updateModel() {
this.currentValue = this.value === this.trueValue
},
},
watch: {
value(val) {
if (val === this.trueValue || val === this.falseValue) {
this.updateModel()
} else {
throw 'Value should be trueValue or falseValue.'
}
},
},
mounted() {
this.parent = findComponentUpward(this, 'iCheckboxGroup')
if (this.parent) {
this.group = true
}
if (this.group) {
this.parent.updateModel(true)
} else {
this.updateModel()
}
},
}
</script></code></pre><p><strong>checkbox-group.vue</strong></p><pre><code><!-- checkbox-group.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<script>
import { findComponentsDownward } from '../../utils/assist.js'
import Emitter from '../../mixins/emitter.js'
export default {
name: 'iCheckboxGroup',
mixins: [Emitter],
props: {
value: {
type: Array,
default() {
return []
},
},
},
data() {
return { currentValue: this.value, childrens: [] }
},
methods: {
updateModel(update) {
this.childrens = findComponentsDownward(this, 'iCheckbox')
if (this.childrens) {
const { value } = this
this.childrens.forEach((child) => {
child.model = value
if (update) {
child.currentValue = value.indexOf(child.label) >= 0
child.group = true
}
})
}
},
change(data) {
this.currentValue = data
this.$emit('input', data)
this.$emit('on-change', data)
this.dispatch('iFormItem', 'on-form-change', data)
},
},
mounted() {
this.updateModel(true)
},
watch: {
value() {
this.updateModel(true)
},
},
}
</script></code></pre><h2>Vue的构造器extend 与⼿ 动挂载$mount</h2><p><em>Vue 的$mount()为手动挂载,在项目中可用于延时挂载(例如在挂载之前要进行一些其他操作、判断等),之后要手动挂载上。new Vue时,el和$mount并没有本质上的不同。</em></p><p><em>创建⼀个 Vue 实例时,都会有⼀个选项 el,来指定 实例的根节点,如果不写 el 选项,那组件就处于未挂载状 态。Vue.extend 的作⽤,就是基于 Vue 构造器,创建⼀个“⼦ 类”,它的参数跟 new Vue 的基本⼀样,但 data 要跟组件⼀样, 是个函数,再配合 $mount ,就可以让组件渲染,并且挂载到任意 指定的节点上,⽐如 body</em>。</p><pre><code>import Vue from 'vue'
//创建了⼀个构造器,这个过程就可以解决异步获取 template 模板的问题
const AlertComponent = Vue.extend({
template: '<div>{{ message }}</div>',
data() {
return { message: 'Hello, Aresn' }
},
})</code></pre><p><strong>⼿动渲染组件,并把它挂载到 body 下:</strong></p><pre><code>const component = new AlertComponent().$mount();
//$mount ⽅法对组件进⾏了⼿动渲染,但它仅 仅是被渲染好了,并没有挂载到节点上,也就显示不了组件。此时的 component 已经是⼀个标准的 Vue 组件实例,因此它的 $el 属性 也可以被访问:
document.body.appendChild(component.$el);</code></pre><p>$mount 也有⼀些快捷的挂载⽅式,以下两种都是可以的:</p><p> <strong>在 $mount ⾥写参数来指定挂载的节点</strong></p><pre><code> new AlertComponent().$mount('#app'); </code></pre><p> <strong>不⽤ $mount,直接在创建实例时指定 el 选项 </strong></p><pre><code> new AlertComponent({ el: '#app' });</code></pre><p><strong>实现同样的效果,除了⽤ extend 外,也可以直接创建 Vue 实例, 并且⽤⼀个 Render 函数来渲染⼀个 .vue ⽂件</strong></p><pre><code>import Vue from 'vue'
import Notification from './notification.vue'
const props = {} // 这⾥可以传⼊⼀些组件的 props 选 项
const Instance = new Vue({
render(h) {
return h(Notification, { props: props })
},
})
const component = Instance.$mount()
document.body.appendChild(component.$el)
</code></pre><p><strong>渲染后, 操作 Render 的 Notification 实例</strong></p><pre><code>const notification = Instance.$children[0];
//因为 Instance 下只 Render 了 Notification ⼀个⼦组件,所以可以 ⽤ $children[0] 访问到。</code></pre><p><strong>⽤ $mount ⼿动渲染的组件,如果要销毁, 也要⽤ $destroy 来⼿动销毁实例,必要时,也可以⽤ removeChild 把节点从 DOM 中移除。</strong></p><h2>动态渲染 .vue ⽂件的组件—— Display</h2><p>⼀个常规的 .vue ⽂件⼀般都会包含 3个部分:<br> <template>:组件的模板 <br> <script>:组件的选项,不包含 el; <br> <style>:CSS 样式。</p><h6>思路</h6><p>1.⽗级传递 code 后,将其分割,并保存在 data 的 html、js、css <br>2.使⽤正则,基于 <> 和 </> 的特性进⾏分割:</p><p><strong>utils/random_str.js</strong></p><pre><code>// ⽣成随机字符串
// 是从指定的 a-zA-Z0-9 中随机⽣成 32 位的字 符串。
export default function (len = 32) {
const $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV WXYZ1234567890';
const maxPos = $chars.length; let str = '';
for (let i = 0; i < len; i++) {
// Math.floor(Math.random() * maxPos) 0到32的整数
// charAt(int index) 方法用于返回指定索引处的字符
str += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return str;
},</code></pre><p><strong>组件display.vue</strong></p><pre><code><!-- display.vue -->
<template>
<div ref="display"></div>
</template>
<script>
import Vue from 'vue'
import randomStr from '../../utils/random_str.js'
export default {
props: { code: { type: String, default: '' } },
data() {
return {
html: '',
js: '',
css: '',
component: null,
id: randomStr()
}
},
// 当 this.code 更新时,整个过程要重新来⼀次,所以要对 code 进 ⾏ watch 监听:
watch: {
code() {
this.destroyCode()
this.renderCode()
},
},
methods: {
getSource(source, type) {
const regex = new RegExp(`<${type}[^>]*>`)
let openingTag = source.match(regex)
if (!openingTag) return ''
else openingTag = openingTag[0]
return source.slice(
source.indexOf(openingTag) + openingTag.length,
source.lastIndexOf(`</${type}>`)
)
},
// getSource ⽅法接收两个参数: source:.vue ⽂件代码,即 props: code;
// type:分割的部分,也就是 template、script、style。
// 分割后,返回的内容不再包含 <template> 等标签,直接是对应的 内容,在 splitCode ⽅法中,把分割好的代码分别赋值给 data 中声 明的 html、js、css。
// 有两个细节需要注意: 1. .vue 的 <script> 部分⼀般都是以 export default 开始 的,可以看到在 splitCode ⽅法中将它替换为了 return,这 个在后⽂会做解释,当前只要注意,我们分割完的代码,仍然 是字符串; 2. 在分割的 <template> 外层套了⼀个 <div id="app">,这 是为了容错,有时使⽤者传递的 code 可能会忘记在外层包⼀ 个节点,没有根节点的组件,是会报错的。
splitCode() {
const script = this.getSource(this.code, 'script').replace(
/export default/,
'return '
)
const style = this.getSource(this.code, 'style')
const template =
'<div id="app">' + this.getSource(this.code, 'template') + '</div>'
this.js = script
this.css = style
this.html = template
},
renderCode() {
this.splitCode()
if (this.html !== '' && this.js !== '') {
// new Function ([arg1[, arg2[, ...argN]],] functionBody)
// arg1, arg2, ... argN 是被函数使⽤的参数名称,functionBody 是 ⼀个含有包括函数定义的 JavaScript 语句的字符串。也就是说,示 例中的字符串 return a + b 被当做语句执⾏了。
// 当前的 this.js 是字符串, ⽽ extend 接收的选项可不是字符串,⽽是⼀个对象类型,那就要先 把 this.js 转为⼀个对象
// const sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 6)); // 8
// new Function eval 函数也可以使⽤
// this.js 中是将 export default 替换为 return 的,如 果将 this.js 传⼊ new Function ⾥,那么 this.js 就执⾏了,这时 因为有 return,返回的就是⼀个对象类型的 this.js 了
const parseStrToFunc = new Function(this.js)()
parseStrToFunc.template = this.html
const Component = Vue.extend(parseStrToFunc)
this.component = new Component().$mount()
// extend 构造的实例通过 $mount 渲染后,挂载到了组件唯⼀的⼀ 个节点 <div ref="display"> 上。
this.$refs.display.appendChild(this.component.$el)
if (this.css !== '') {
const style = document.createElement('style')
style.type = 'text/css'
// 创建⼀个 <style> 标签,然后把 css 写进去,再插⼊到⻚ ⾯的 <head> 中,这样 css 就被浏览器解析了。为了便于后⾯在 this.code 变化或组件销毁时移除动态创建的 <style> 标签,我 们给每个 style 标签加⼀个随机 id ⽤于标识。
style.id = this.id
style.innerHTML = this.css
document.getElementsByTagName('head')[0].appendChild(style)
}
}
},
// 当 Display 组件销毁时,也要⼿动销毁 extend 创建的实例以及上 ⾯的 css:
destroyCode() {
const $target = document.getElementById(this.id)
if ($target) $target.parentNode.removeChild($target)
if (this.component) {
this.$refs.display.removeChild(this.component.$el)
this.component.$destroy()
this.component = null
}
},
},
mounted() {
this.renderCode()
},
beforeDestroy() {
this.destroyCode()
},
}
</script></code></pre><h6> <strong>使⽤</strong></h6><p><em>新建⼀条路由,并在 src/views 下新建⻚⾯ display.vue 来使 ⽤ Display 组件</em><br><strong>src/views/display.vue</strong></p><pre><code><!-- src/views/display.vue -->
<template>
<div>
<h3>动态渲染 .vue ⽂件的组件—— Display</h3>
<i-display :code="code"></i-display>
</div>
</template>
<script>
import iDisplay from '../components/display/display.vue'
import defaultCode from './default-code.js'
export default {
components: { iDisplay },
data() {
return { code: defaultCode }
},
}
</script></code></pre><p><strong>// src/views/default-code.js</strong></p><pre><code>
const code = `<template>
<div>
<input v-model="message"> {{ message }}
</div>
</template>
<script> export default { data () { return { message: '' } } }
</script>`;
export default code;</code></pre><p><em>如果使⽤的是 Vue CLI 3 默认的配置,直接运⾏时,会抛出下⾯的 错误:</em></p><p>*这涉及到另⼀个知识点,就是 Vue.js 的版本。<br>在使⽤ Vue.js 2 时,有独⽴构建(standalone)和运⾏时构建(runtime-only)<br>两 种版本可供选择,<br>Vue CLI 3 默认使⽤了 vue.runtime.js,<br>它不允许编译 template 模板,<br>因为我们在 Vue.extend 构造实例时,<br>⽤了 template 选 项,所以会报错。<br>解决⽅案有两种,⼀是⼿动将 template 改写为 Render 函数,但这成本太⾼;<br>另⼀种是对 Vue CLI 3 创建的⼯程做 简单的配置。我们使⽤后者。*<br>在项⽬根⽬录,新建⽂件 vue.config.js:</p><pre><code>
module.exports = { runtimeCompiler: true };</code></pre><p>*它的作⽤是,是否使⽤包含运⾏时编译器的 Vue 构建版本。设置为 true 后就可以在 Vue 组件中使⽤ template 选项了,</p><p>这个⼩⼩的 Display 组件,能做的事还有很多,⽐如要写⼀套 Vue 组件库的⽂档,传统⽅法是在开发环境写⼀个个的 .vue ⽂件,然后 编译打包、上传资源、上线,如果要修改,哪怕⼀个标点符号,都要 重新编译打包、上传资源、上线。有了 Display 组件,只需要提供 ⼀个服务来在线修改⽂档的 .vue,就能实时更新,不⽤打包、上 传、上线*,</p><h2>全局提示组件—— $Alert</h2><p>this.$Alert 可以在任何位置调⽤,⽆需单独引⼊。<br>该⽅法接收两 个参数:<br>content:提示内容;<br>duration:持续时间,单位秒,默认 1.5 秒,到时间⾃动消 失<br>Alert 组件不同于常规的组件使⽤⽅式,它最终是通过 JS 来调⽤ 的,因此组件不⽤预留 props 和 events 接⼝</p><h6><strong>在 src/component 下新建 alert ⽬录,并创建⽂件 alert.vue</strong></h6><pre><code><template>
<div class="alert">
<div class="alert-main" v-for="item in notices" :key="item.name">
<div class="alert-content">{{ item.content }}</div>
</div>
</div>
</template>
<script>
let seed = 0
function getUuid() {
return 'alert_' + seed++
}
// JS 调⽤ Alert 的⼀个⽅法 add,并 将 content 和 duration 传⼊进来:
export default {
data() {
// 通知可以是多个,我们⽤⼀个数组 notices 来管理每条通知
return { notices: [] }
},
// 在 add ⽅法中,给每⼀条传进来的提示数据,加了⼀个不重复的 name 字段来标识,并通过 setTimeout 创建了⼀个计时器,当到 达指定的 duration 持续时间后,调⽤ remove ⽅法,将对应 name 的那条提示信息找到,并从数组中移除。 由这个思路,Alert 组件就可以⽆限扩展,只要在 add ⽅法中传递 更多的参数,就能⽀持更复杂的组件,⽐如是否显示⼿动关闭按钮、 确定 / 取消按钮,甚⾄传⼊⼀个 Render 函数都可以,完成本例 后,
methods: {
add(notice) {
const name = getUuid()
let _notice = Object.assign({ name: name }, notice)
this.notices.push(_notice)
// 定时移除,单位:秒
const duration = notice.duration
setTimeout(() => {
this.remove(name)
}, duration * 1000)
},
remove(name) {
const notices = this.notices
for (let i = 0; i < notices.length; i++) {
if (notices[i].name === name) {
this.notices.splice(i, 1)
break
}
}
},
},
}
</script>
<style>
.alert {
position: fixed;
width: 100%;
top: 16px;
left: 0;
text-align: center;
pointer-events: none;
}
.alert-content {
display: inline-block;
padding: 8px 16px;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
margin-bottom: 8px;
}
</style></code></pre><p>对 Alert 组件进⼀步封装,让它能够实例化,⽽不是常 规的组件使⽤⽅法<br>使⽤ Vue.extend 或 new Vue,然后⽤ $mount 挂载到 body 节点下。<br>notification.js 并不是最终的⽂件,它只是对 alert.vue 添加了⼀个 ⽅法 newInstance。虽然 alert.vue 包含了 template、script、 style 三个标签,并不是⼀个 JS 对象,那怎么能够给它扩展⼀个⽅法 newInstance 呢?事实上,alert.vue 会被 Webpack 的 vue- loader 编译,把 template 编译为 Render 函数,最终就会成为⼀ 个 JS 对象,⾃然可以对它进⾏扩展。</p><p>Alert 组件没有任何 props,这⾥在 Render Alert 组件时,还是给 它加了 props,当然,这⾥的 props 是空对象 {},⽽且即使传了内 容,也不起作⽤。这样做的⽬的还是为了扩展性,如果要在 Alert 上 添加 props 来⽀持更多特性,是要在这⾥传⼊的。不过话说回来, 因为能拿到 Alert 实例,⽤ data 或 props 都是可以的。</p><h6> notification.js</h6><pre><code>import Alert from './alert.vue'
import Vue from 'vue'
Alert.newInstance = (properties) => {
const props = properties || {}
const Instance = new Vue({
data: props,
render(h) {
return h(Alert, { props: props })
},
})
const component = Instance.$mount()
document.body.appendChild(component.$el)
const alert = Instance.$children[0]
return {
add(noticeProps) {
alert.add(noticeProps)
},
remove(name) {
alert.remove(name)
},
}
}
export default Alert</code></pre><h6> ⼊⼝</h6><p>最后要做的,就是调⽤ notification.js 创建实例,并通过 add 把数 据传递过去,这是组件开发的最后⼀步,也是最终的⼊⼝。<br>在 src/component/alert 下创建⽂件 <br><strong>alert.js:</strong></p><pre><code> // alert.js
import Notification from './notification.js'
let messageInstance
// getMessageInstance 函数⽤来获取实例,它不会重复创建,如 果 messageInstance 已经存在,就直接返回了,只在第⼀次调⽤ Notification 的 newInstance 时来创建实例。
function getMessageInstance() {
messageInstance = messageInstance || Notification.newInstance()
return messageInstance
}
function notice({ duration = 1.5, content = '' }) {
let instance = getMessageInstance()
instance.add({ content: content, duration: duration })
}
export default {
info(options) {
return notice(options)
},
}
// alert.js 对外提供了⼀个⽅法 info,如果需要各种显示效果,⽐如 成功的、失败的、警告的,可以在 info 下⾯提供更多的⽅法,⽐如 success、fail、warning 等,并传递不同参数让 Alert.vue 知道显 示哪种状态的图标。本例因为只有⼀个 info,事实上也可以省略 掉,直接导出⼀个默认的函数,这样在调⽤时,就不⽤ this.$Alert.info() 了,直接 this.$Alert()。</code></pre><p>把 alert.js 作为插件注册到 Vue ⾥就⾏,在⼊⼝⽂件 src/main.js中,通过 prototype 给 Vue 添加⼀个实例⽅法:</p><h6>src/main.js</h6><pre><code> import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Alert from '../src/components/alert/alert.js'
Vue.config.productionTip = false
Vue.prototype.$Alert = Alert
new Vue({ router, render: h => h(App) }).$mount('#app')</code></pre><p><strong>这样在项⽬任何地⽅,都可以通过 this.$Alert 来调⽤ Alert 组 件了</strong><br><strong>src/views/alert.vue</strong></p><pre><code><template>
<div>
<button @click="handleOpen1">打开提示 1</button>
<button @click="handleOpen2">打开提示 2</button>
</div>
</template>
<script>
export default {
methods: {
handleOpen1() {
this.$Alert.info({ content: '我是提示信息 1' })
},
handleOpen2() {
this.$Alert.info({ content: '我是提示信息 2', duration: 3 })
},
},
}
</script></code></pre><p><strong>是同类组件中值得注意的:</strong></p><p>1. Alert.vue 的最外层是有⼀个 .alert 节点的,它会在第⼀次调 ⽤ $Alert 时,在 body 下创建,因为不在 <router-view> 内,</p><p>它不受路由的影响,也就是说⼀经创建,除⾮刷新⻚⾯, </p><p>这个节点是不会消失的,所以在 alert.vue 的设计中,并没有 主动销毁这个组件,</p><p>⽽是维护了⼀个⼦节点数组 notices。 </p><p>2. .alert 节点是 position: fixed 固定的,因此要合理设计它 的 z-index,否则可能被其它节点遮挡。 </p><p>3. notification.js 和 alert.vue 是可以复⽤的,如果还要开发其 它同类的组件,⽐如⼆次确认组件 $Confirm, </p><p>只需要再写⼀ 个⼊⼝ confirm.js,并将 alert.vue 进⼀步封装,将 notices 数组的循环体写为⼀个新的组件,</p><p>通过配置来决定是 渲染 Alert 还是 Confirm,这在可维护性上是友好的。 </p><p>4. 在 notification.js 的 new Vue 时,使⽤了 Render 函数来渲 染 alert.vue,</p><p>这是因为使⽤ template 在 runtime 的 Vue.js 版本下是会报错的。</p><p>5. 本例的 content 只能是字符串,如果要显示⾃定义的内容,除 了⽤ v-html 指令,也能⽤ Functional Render。 </p><p>结语Vue.js 的精髓是组件,组件的精髓是 JavaScript。将 JavaScript 开 发中的技巧结合 Vue.js 组件,就能玩出不⼀样的东⻄。</p><h2>更灵活的组件:Render 函数与 Functional</h2><p>Render Render 函数 返回的是⼀个 JS<br>对象,没有传统 DOM 的层级关系,配合上 if、 else、for 等语句,将节点拆分成不同JS对象再组装。</p><p><strong> template 和 Render 写法的对照:</strong></p><pre><code><template>
<div id="main" class="container" style="color: red">
<p v-if="show">内容 1</p>
<p v-else>内容 2</p>
</div>
</template>
<script>
export default {
data() {
return { show: false }
},
}
</script></code></pre><p><strong>Render</strong></p><pre><code>export default {
data() {
return { show: false }
},
render: (h) => {
let childNode
if (this.show) {
childNode = h('p', '内容 1')
} else {
childNode = h('p', '内容 2')
}
return h(
'div',
{
attrs: { id: 'main' },
class: { container: true },
style: { color: 'red' },
},
[childNode]
)
},
}</code></pre><p>这⾥的 h,即 createElement,是 Render 函数的核⼼。<br> 可以看 到,template 中的 v-if / v-else 等指令,都被 JS 的 if / else 替 代了,<br> 那 v-for ⾃然也会被 for 语句替代。<br> h 有 3 个参数,分别是:</p><ol><li>要渲染的元素或组件,可以是⼀个 html 标签、组件选项或⼀ 个函数(不常⽤),该参数为必填项。示例:</li></ol><pre><code> // 1. html 标签
h('div');
// 2. 组件选项
import DatePicker from '../component/date- picker.vue'; h(DatePicker);</code></pre><ol><li>对应属性的数据对象,⽐如组件的 props、元素的 class、绑 定的事件、slot、⾃定义指令等,</li></ol><p>该参数是可选的,上⽂所说 的 Render 配置项多,指的就是这个参数。</p><p>该参数的完整配置 和示例,可以到 Vue.js 的⽂档查看,没必要全部记住,</p><p>⽤到 时查阅就好:createElement 参数 (<a href="https://link.segmentfault.com/?enc=ZE%2FJmAQcPA1MkiBB50ZiLQ%3D%3D.1QsIqt6%2FaCafVvqt9W0qI36OJI0b9HOHdBBiPEborLdz58o1oVFr0%2F4Xj6xyf%2BTQ" rel="nofollow">https://cn.vuejs.org/v2/guide...</a> function.html#createElement-参数)。</p><ol><li>⼦节点,可选,String 或 Array,它同样是⼀个 h。示例:</li></ol><p>[ </p><p>'内容', h('p', '内容'),</p><p>h(Component, { props: { someProp: 'foo' } }) </p><p>]<br> <strong>所有的组件树中,如果 vNode 是组件或含有组件的 slot,那么 vNode 必须唯⼀</strong></p><p><strong>重复渲染多个组件或元素,可以通过⼀个循环和⼯⼚函数来解决:</strong></p><pre><code>const Child = {
render: (h) => {
return h('p', 'text')
},
}
export default {
render: (h) => {
const children = Array.apply(null, { length: 5 }).map(() => {
return h(Child)
})
return h('div', children)
},
}</code></pre><p><strong>对于含有组件的 slot,复⽤⽐较复杂,需要将 slot 的每个⼦节点都 克隆⼀份,例如</strong>:</p><pre><code> {
render: (h) => {
function cloneVNode(vnode) {
//递归遍历所有⼦节点,并克隆
const clonedChildren =
vnode.children && vnode.children.map((vnode) => cloneVNode(vnode))
const cloned = h(vnode.tag, vnode.data, clonedChildren)
cloned.text = vnode.text
cloned.isComment = vnode.isComment
cloned.componentOptions = vnode.componentOptions
cloned.elm = vnode.elm
cloned.context = vnode.context
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
return cloned
}
const vNodes =
this.$slots.default === undefined ? [] : this.$slots.default
const clonedVNodes =
this.$slots.default === undefined
? []
: vNodes.map((vnode) => cloneVNode(vnode))
return h('div', [vNodes, clonedVNodes])
}
}</code></pre><ul><li>在 Render 函数⾥创建了⼀个 cloneVNode 的⼯⼚函数,通过递归 将 slot 所有⼦节点都克隆了⼀份,并对 VNode 的关键属性也进⾏ 了复制<br>深度克隆 slot 并⾮ Vue.js 内置⽅法,<br>在⼀些特殊的场景才会使⽤到,正常业务⼏乎是⽤不到的。⽐如 iView 组件库的穿梭框组件 Transfer,就⽤到了这种⽅法:<br>slot 是⼀个 Refresh 按钮,使⽤者只写了⼀遍,但在 Transfer 组件中,是通过克隆 VNode 的⽅法,显示了两遍。<br>如果 不这样做,就要声明两个具名 slot,但是左右两个的逻辑可能是完 全⼀样的,使⽤者就要写两个⼀模⼀样的 slot,这是不友好的*<br>Render 函数的基本⽤法还有很多,⽐如 v-model 的⽤ 法、事件和修饰符、slot 等,读者可以到 Vue.js ⽂档阅 读。Vue.js 渲染函数 (<a href="https://link.segmentfault.com/?enc=ZJTYMNrdLlxmuwnGgAGczg%3D%3D.TDXh%2BbPPvB8U6wGGCsU%2BDZ5CVyHnD7CwKjoS3pOLIbQwdXhwOlLsVu6c1U%2BPMK0x" rel="nofollow">https://cn.vuejs.org/v2/guide...</a> function.html)</li></ul><h6>Render 函数使⽤场景</h6><p>⼀般情况下是不推荐直接使⽤ Render 函数的,使⽤ template ⾜以,在 Vue.js 中,使⽤ Render 函数的场景,主要有 以下 4 点</p><ol><li>使⽤两个相同 slot。在 template 中,Vue.js 不允许使⽤两个 相同的 slot,⽐如下⾯的示例是错误的:</li></ol><pre><code><template>
<div>
<slot></slot>
<slot></slot>
</div>
</template></code></pre><p><strong>解决⽅案就是上⽂中讲到的约束,使⽤⼀个深度克隆 VNode 节点的⽅法</strong></p><ol><li>在 SSR 环境(服务端渲染),如果不是常规的 template 写 法,⽐如通过 Vue.extend 和 new Vue 构造来⽣成的组件实 例,是编译不过的,使⽤ Render 函 数来渲染</li><li>在 runtime 版本的 Vue.js 中,如果使⽤ Vue.extend ⼿动构 造⼀个实例,使⽤ template 选项是会报错的,解决⽅案也很简单,把 template 改写为 Render 就可以了。需要注意的是,在开发独⽴组件时,可以通过配置 Vue.js 版本来使 template 选项可⽤,但这是在⾃⼰的环境, ⽆法保证使⽤者的 Vue.js 版本,所以对于提供给他⼈⽤的组 件,是需要考虑兼容 runtime 版本和 SSR 环境的。</li><li>这可能是使⽤ Render 函数最重要的⼀点。⼀个 Vue.js 组 件,有⼀部分内容需要从⽗级传递来显示,如果是⽂本之类 的,直接通过 props 就可以,如果这个内容带有样式或复杂 ⼀点的 html 结构,可以使⽤ v-html 指令来渲染,⽗级传递 的仍然是⼀个 HTML Element 字符串,不过它仅仅是能解析 正常的 html 节点且有 XSS ⻛险。当需要最⼤化程度⾃定义显 示内容时,就需要 Render 函数,它可以渲染⼀个完整的 Vue.js 组件。你可能会说,⽤ slot 不就好了?的确,slot 的 作⽤就是做内容分发的,但在⼀些特殊组件中,可能 slot 也不 ⾏。⽐如⼀个表格组件 Table,它只接收两个 props:列配置 columns 和⾏数据 data,不过某⼀列的单元格,不是只将数 据显示出来那么简单,可能带有⼀些复杂的操作,这种场景只 ⽤ slot 是不⾏的,没办法确定是那⼀列的 slot。这种场景有两 种解决⽅案,其⼀就是 Render 函数,另⼀种是⽤作⽤域 slot(slot- scope)</li></ol><h4>Functional Render</h4><p>*Vue.js 提供了⼀个 functional 的布尔值选项,设置为 true 可以 使组件⽆状态和⽆实例,也就是没有 data 和 this 上下⽂。这样⽤ Render 函数返回虚拟节点可以更容易渲染,因为函数化组件 (Functional Render)只是⼀个函数,渲染开销要⼩很多。<br>使⽤函数化组件,Render 函数提供了第⼆个参数 context 来提供临 时上下⽂。组件需要的 data、props、slots、children、parent 都 是通过这个上下⽂来传递的,⽐如 this.level 要改写为 context.props.level,this.$slots.default 改写为 context.children,您可以阅读 Vue.js ⽂档—函数式组件 (<a href="https://link.segmentfault.com/?enc=CVFIfrFi%2B8Q3vVnTEkTczA%3D%3D.Iiet9J%2B5QbKZmKR2ik1OU1z%2FMZfDpHA8wRxPUFDK0YEcPmX8AUqMPGR1b73q8ScW" rel="nofollow">https://cn.vuejs.org/v2/guide...</a> function.html#函数式组件) 来查看示例。<br>函数化组件在业务中并不是很常⽤,⽽且也有类似的⽅法来实现,⽐ 如某些场景可以⽤ is 特性来动态挂载组件。函数化组件主要适⽤于 以下两个场景*<br>1.程序化地在多个组件中选择⼀个; <br>2.在将 children、props、data 传递给⼦组件之前操作它们。 <br>某个组件需要使⽤ Render 函数来⾃定义,⽽不是 通过传递普通⽂本或 v-html 指令,这时就可以⽤ Functional Render,来看下⾯的示例:<br><strong>⾸先创建⼀个函数化组件 render.js:</strong></p><pre><code> // 它只定义了⼀个 props:render,格式为 Function,因为是 functional,所以在 render ⾥使⽤了第⼆个参数 ctx 来获取 props。这是⼀个中间⽂件,并且可以复⽤,其它组件需要这 个功能时,都可以引⼊它
// render.js
export default {
functional: true,
props: { render: Function },
render: (h, ctx) => {
return ctx.props.render(h)
},
}</code></pre><p><strong> 创建组件:</strong></p><pre><code>
<!-- my-component.vue -->
<template>
<div><Render :render="render"></Render></div>
</template>
<script>
import Render from './render.js'
export default {
components: { Render },
props: { render: Function }
}
</script></code></pre><p><strong>使⽤上⾯的 my-compoennt 组件:</strong></p><pre><code><template>
<div>
<my-component :render="render"></my-component>
</div>
</template>
<script>
import myComponent from '../components/my-component.vue'
export default {
components: { myComponent },
data() {
return {
render: (h) => {
return h('div', { style: { color: 'red' } }, '⾃定义内容')
},
}
},
}
</script></code></pre><p>这⾥的 render.js 因为只是把 demo.vue 中的 Render 内容过继, 并⽆其它⽤处,所以⽤了 Functional Render。 就此例来说,完全可以⽤ slot 取代 Functional Render,那是因为 只有 render 这⼀个 prop。如果示例中的 <Render> 是⽤ v-for ⽣成的,也就是多个时,⽤ ⼀个 slot 是实现不了的,那时⽤Render 函数就很⽅便了.</p>
最实用的正则表达式整理
https://segmentfault.com/a/1190000024495306
2020-09-17T16:30:05+08:00
2020-09-17T16:30:05+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
6
<p>作者:慕课网<br>链接:<a href="https://link.segmentfault.com/?enc=rfluzorMGcNu2uPQoawY0g%3D%3D.dq3bTQqTlh1%2BtHPW2%2FPw6iK4RDvXUn%2FOJCohNESKpG2OG99m3qqmL1YOr3h%2BFR8n" rel="nofollow">https://zhuanlan.zhihu.com/p/...</a><br>来源:知乎<br>著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。</p><h2>一、校验数字的表达式</h2><pre><code class="text">1 数字: ^[0-9]$
2 n位的数字: ^d{n}$
3 至少n位的数字: ^d{n,}$
4 m-n位的数字:^d{m,n}$
5 零和非零开头的数字:^(0|[1-9][0-9]*)$
6 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
7 带1-2位小数的正数或负数:^(-)?d+(.d{1,2})?$
8 正数、负数、和小数:^(-|+)?d+(.d+)?$
9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数:^[1-9]d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^+?[1-9][0-9]*$
12 非零的负整数:^-[1-9][]0-9"*$ 或 ^-[1-9]d*$
13 非负整数:^d+$ 或 ^[1-9]d*|0$
14 非正整数:^-[1-9]d*|0$ 或 ^((-d+)|(0+))$
15 非负浮点数:^d+(.d+)?$ 或 ^[1-9]d*.d*|0.d*[1-9]d*|0?.0+|0$
16 非正浮点数:^((-d+(.d+)?)|(0+(.0+)?))$ 或 ^(-([1-9]d*.d*|0.d*[1-9]d*))|0?.0+|0$
17 正浮点数:^[1-9]d*.d*|0.d*[1-9]d*$ 或 ^(([0-9]+.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*.[0-9]+)|([0-9]*[1-9][0-9]*))$
18 负浮点数:^-([1-9]d*.d*|0.d*[1-9]d*)$ 或 ^(-(([0-9]+.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*.[0-9]+)|([0-9]*[1-9][0-9]*)))$
19 浮点数:^(-?d+)(.d+)?$ 或 ^-?([1-9]d*.d*|0.d*[1-9]d*|0?.0+|0)$
20 正则表达式 1到100之间的整数: ^([1-9][0-9]{0,1}|100)$
21 1-20000的正整数: /^((1[0-9]{0,1}\d{0,3})|(20000))$/</code></pre><h2>二、校验字符的表达式</h2><pre><code class="text">1 汉字:^[u4e00-u9fa5]{0,}$
2 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
3 长度为3-20的所有字符:^.{3,20}$
4 由26个英文字母组成的字符串:^[A-Za-z]+$
5 由26个大写英文字母组成的字符串:^[A-Z]+$
6 由26个小写英文字母组成的字符串:^[a-z]+$
7 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
8 由数字、26个英文字母或者下划线组成的字符串:^w+$ 或 ^w{3,20}$
9 中文、英文、数字包括下划线:^[u4E00-u9FA5A-Za-z0-9_]+$
10 中文、英文、数字但不包括下划线等符号:^[u4E00-u9FA5A-Za-z0-9]+$ 或 ^[u4E00-u9FA5A-Za-z0-9]{2,20}$
11 可以输入含有^%&',;=?$"等字符:[^%&',;=?$x22]+
12 禁止输入含有~的字符:[^~x22]+</code></pre><h2>三、特殊需求表达式</h2><pre><code class="text">1 Email地址:^w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*$
2 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
3 InternetURL:[a-zA-z]+://[^s]* 或 ^http://([w-]+.)+[w-]+(/[w-./?%&=]*)?$
4 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])d{8}$
5 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^((d{3,4}-)|d{3.4}-)?d{7,8}$
6 国内电话号码(0511-4405222、021-87888822):d{3}-d{8}|d{4}-d{7}
7 身份证号(15位、18位数字):^d{15}|d{18}$
8 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
9 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
10 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]w{5,17}$
11 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
12 日期格式:^d{4}-d{1,2}-d{1,2}
13 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
14 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
16 1.有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$
17 2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$
18 3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$
19 4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
20 5.必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$
21 6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
22 7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$
23 8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$
24 备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
25 xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+.[x|X][m|M][l|L]$
26 中文字符的正则表达式:[u4e00-u9fa5]
27 双字节字符:[^x00-xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))28 空白行的正则表达式:ns*r (可以用来删除空白行)
29 HTML标记的正则表达式:<(S*?)[^>]*>.*?</1>|<.*? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)30 首尾空白字符的正则表达式:^s*|s*$或(^s*)|(s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
31 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
32 中国邮政编码:[1-9]d{5}(?!d) (中国邮政编码为6位数字)
33 IP地址:d+.d+.d+.d+ (提取IP地址时有用)34 IP地址:((?:(?:25[0-5]|2[0-4]d|[01]?d?d).){3}(?:25[0-5]|2[0-4]d|[01]?d?d))
"^d+$" //非负整数(正整数 + 0)
"^[0-9]*[1-9][0-9]*$" //正整数
"^((-d+)|(0+))$" //非正整数(负整数 + 0)
"^-[0-9]*[1-9][0-9]*$" //负整数
"^-?d+$" //整数
"^d+(.d+)?$" //非负浮点数(正浮点数 + 0)
"^(([0-9]+.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*.[0-9]+)|([0-9]*[1-9][0-9]*))$" //正浮点数
"^((-d+(.d+)?)|(0+(.0+)?))$" //非正浮点数(负浮点数 + 0)
"^(-(([0-9]+.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*.[0-9]+)|([0-9]*[1-9][0-9]*)))$" //负浮点数
"^(-?d+)(.d+)?$" //浮点数
"^[A-Za-z]+$" //由26个英文字母组成的字符串
"^[A-Z]+$" //由26个英文字母的大写组成的字符串
"^[a-z]+$" //由26个英文字母的小写组成的字符串
"^[A-Za-z0-9]+$" //由数字和26个英文字母组成的字符串
"^w+$" //由数字、26个英文字母或者下划线组成的字符串
"^[w-]+(.[w-]+)*@[w-]+(.[w-]+)+$" //email地址
"^[a-zA-z]+://(w+(-w+)*)(.(w+(-w+)*))*(?S*)?$" //url
整数或者小数:^[0-9]+.{0,1}[0-9]{0,2}$
只能输入数字:"^[0-9]*$"。
只能输入n位的数字:"^d{n}$"。
只能输入至少n位的数字:"^d{n,}$"。
只能输入m~n位的数字:。"^d{m,n}$"
只能输入零和非零开头的数字:"^(0|[1-9][0-9]*)$"。
只能输入有两位小数的正实数:"^[0-9]+(.[0-9]{2})?$"。
只能输入有1~3位小数的正实数:"^[0-9]+(.[0-9]{1,3})?$"。
只能输入非零的正整数:"^+?[1-9][0-9]*$"。
只能输入非零的负整数:"^-[1-9][]0-9"*$。
只能输入长度为3的字符:"^.{3}$"。
只能输入由26个英文字母组成的字符串:"^[A-Za-z]+$"。
只能输入由26个大写英文字母组成的字符串:"^[A-Z]+$"。
只能输入由26个小写英文字母组成的字符串:"^[a-z]+$"。
只能输入由数字和26个英文字母组成的字符串:"^[A-Za-z0-9]+$"。
只能输入由数字、26个英文字母或者下划线组成的字符串:"^w+$"。
验证用户密码:"^[a-zA-Z]w{5,17}$"正确格式为:以字母开头,长度在6~18之间,只能包含字符、数字和下划线。
验证是否含有^%&'',;=?$"等字符:"[^%&'',;=?$x22]+"。
只能输入汉字:"^[u4e00-u9fa5]{0,}$"
验证Email地址:"^w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*$"。
验证InternetURL:"^http://([w-]+.)+[w-]+(/[w-./?%&=]*)?$"。
验证电话号码:"^((d{3,4}-)|d{3.4}-)?d{7,8}$"正确格式为:"XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX"。
验证身份证号(15位或18位数字):"^d{15}|d{18}$"。
验证一年的12个月:"^(0?[1-9]|1[0-2])$"正确格式为:"01"~"09"和"1"~"12"。
验证一个月的31天:"^((0?[1-9])|((1|2)[0-9])|30|31)$"正确格式
为;"01"~"09"和"1"~"31"。
整数或者小数:^[0-9]+.{0,1}[0-9]{0,2}$
"^w+$" //由数字、26个英文字母或者下划线组成的字符串
"^[w-]+(.[w-]+)*@[w-]+(.[w-]+)+$" //email地址
"^[a-zA-z]+://(w+(-w+)*)(.(w+(-w+)*))*(?S*)?$" //url
可输入形如2008、2008-9、2008-09、2008-9-9、2008-09-09. ^(d{4}|(d{4}-d{1,2})|(d{4}-d{1,2}-d{1,2}))$
邮箱验证正则表达式 w+([-+.']w+)*@w+([-.]w+)*.w+([-.]w+)* </code></pre><h2>四、网络验证应用技巧</h2><ul><li>验证 E-mail格式</li></ul><p><code>public bool IsEmail(string str_Email)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_Email,@"^([w-.]+)@(([−9]1,3.[−9]1,3.[−9]1,3.)|(([w−]+.)+))([a−zA−Z]2,4|[−9]1,3)(?)$");</code> <code>}</code></p><ul><li>验证 IP 地址</li></ul><p><code>public bool IPCheck(string IP)</code> `{<br>string num =<code> </code>"(25[0-5]|2[0-4]d|[0-1]d{2}|[1-9]?d)";<code> </code>return<code> </code>Regex.IsMatch(IP,("^"<code> </code>+ num +<code> </code>"."<code> </code>+ num +<code> </code>"."<code> </code>+ num +<code> </code>"."<code> </code>+ num +<code> </code>"$"));<code> </code>}`</p><ul><li>验证 URL</li></ul><p><code>public bool IsUrl(string str_url)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_url,</code> <code>@"http(s)?://([w-]+.)+[w-]+(/[w- ./?%&=]*)?");</code> <code>}</code></p><pre><code class="text">五、 常用数字验证技巧
• 验证电话号码</code></pre><p><code>public bool IsTelephone(string str_telephone)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_telephone,</code> <code>@"^(d{3,4}-)?d{6,8}$");</code> <code>}</code></p><ul><li>输入密码条件(字符与数据同时出现)</li></ul><p><code>public bool IsPassword(string str_password)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_password,</code> <code>@"[A-Za-z]+[0-9]");</code> <code>}</code></p><ul><li>邮政编号</li></ul><p><code>public bool IsPostalcode(string str_postalcode)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_postalcode,</code> <code>@"^d{6}$");</code> <code>}</code></p><ul><li>手机号码</li></ul><p><code>public bool IsHandset(string str_handset)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_handset,</code> <code>@"^[1]+[3,5]+d{9}$");</code> <code>}</code></p><ul><li>身份证号</li></ul><p><code>public bool IsIDcard(string str_idcard)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_idcard,</code> <code>@"(^d{18}$)|(^d{15}$)");</code> <code>}</code></p><ul><li>两位小数</li></ul><p><code>public bool IsDecimal(string str_decimal)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_decimal,</code> <code>@"^[0-9]+(.[0-9]{2})?$");</code> <code>}</code></p><ul><li>一年的12个月</li></ul><p><code>public bool IsMonth(string str_Month)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_Month,</code> <code>@"^(0?[[1-9]|1[0-2])$");</code> <code>}</code></p><ul><li>一个月的31天</li></ul><p><code>public bool IsDay(string str_day)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_day,</code> <code>@"^((0?[1-9])|((1|2)[0-9])|30|31)$");</code> <code>}</code></p><ul><li>数字输入</li></ul><p><code>public bool IsNumber(string str_number)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_number,</code> <code>@"^[0-9]*$");</code> <code>}</code></p><ul><li>密码长度 (6-18位)</li></ul><p><code>public bool IsPasswLength(string str_Length)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_Length,</code> <code>@"^d{6,18}$");</code> <code>}</code></p><ul><li>非零的正整数</li></ul><p><code>public bool IsIntNumber(string str_intNumber)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_intNumber,</code> <code>@"^+?[1-9][0-9]*$");</code> <code>}</code></p><pre><code class="text">六、 常用字符验证技巧</code></pre><h2>1.大写字母</h2><p><code>public bool IsUpChar(string str_UpChar)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_UpChar,</code> <code>@"^[A-Z]+$");</code> <code>}</code></p><pre><code class="text">2.小写字母</code></pre><p><code>public bool IsLowChar(string str_UpChar)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_UpChar,</code> <code>@"^[a-z]+$");</code> <code>}</code></p><pre><code class="text">3.检查字符串重复出现的词</code></pre><p><code>private</code> <code>void btnWord_Click(object sender,</code> <code>EventArgs e)</code> <code>{</code> <code>System.Text.RegularExpressions.MatchCollection matches =</code> <code>System.Text.RegularExpressions.Regex.Matches(label1.Text,</code> <code>@"b(?<word>w+)s+(k<word>)b",</code> <code>System.Text.RegularExpressions.RegexOptions.Compiled</code> <code>|</code> <code>System.Text.RegularExpressions.RegexOptions.IgnoreCase);</code> <code>if</code> <code>(matches.Count</code> <code>!=</code> <code>0)</code> `{<br>foreach (System.Text.RegularExpressions.Match match in matches)<code> </code>{<br>string word = match.Groups["word"].Value;<code> </code>MessageBox.Show(word.ToString(),"英文单词");<code> </code>}<code> </code>}<code> </code>else<code> </code>{<code> </code>MessageBox.Show("没有重复的单词");<code> </code>}<code> </code>}`</p><pre><code class="text">4.替换字符串</code></pre><p><code>private</code> <code>void button1_Click(object sender,</code> <code>EventArgs e)</code> `{<br>string strResult =<code> </code>System.Text.RegularExpressions.Regex.Replace(textBox1.Text,<code> </code>@"[A-Za-z]*?", textBox2.Text);<code> </code>MessageBox.Show("替换前字符:"<code> </code>+<code> </code>"n"<code> </code>+ textBox1.Text<code> </code>+<code> </code>"n"<code> </code>+<code> </code>"替换的字符:"<code> </code>+<code> </code>"n"<code> </code>+ textBox2.Text<code> </code>+<code> </code>"n"<code> </code>+<code> </code>"替换后的字符:"<code> </code>+<code> </code>"n"<code> </code>+ strResult,"替换");<code> </code>}`</p><pre><code class="text">5·拆分字符串</code></pre><p><code>private</code> <code>void button1_Click(object sender,</code> <code>EventArgs e)</code> <code>{</code> `//实例: 甲025-8343243乙0755-2228382丙029-32983298389289328932893289丁<br>foreach (string s in System.Text.RegularExpressions.Regex.Split(textBox1.Text,@"d{3,4}-d*"))<code> </code>{<br>textBox2.Text+=s;<code> </code>//依次输出 "甲乙丙丁"<code> </code>}<code> </code>}`</p><pre><code class="text">6.验证输入字母</code></pre><p><code>public bool IsLetter(string str_Letter)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_Letter,</code> <code>@"^[A-Za-z]+$");</code> <code>}</code></p><pre><code class="text">7.验证输入汉字</code></pre><p><code>public bool IsChinese(string str_chinese)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_chinese,</code> <code>@"^[u4e00-u9fa5],{0,}$");</code> <code>}</code></p><pre><code class="text">8.验证输入字符串 (至少8个字符)</code></pre><p><code>public bool IsLength(string str_Length)</code> <code>{</code> <code>return</code> <code>System.Text.RegularExpressions.Regex.IsMatch(str_Length,</code> <code>@"^.{8,}$");</code> <code>}</code></p><h5>ES6 unicode正则表达式验证特殊字符(比emoji)</h5><pre><code>特殊字符的编码会大于65535,会在ffff至fffff之间
if(v.match(/\u{ffff}-\u{fffff}/u)){//表示用Es6的unicode的编译方式
return {
type:'ltFFF',
message:"你输入了非法字符"
}
}</code></pre><h6>ES6匹配不在unicode字符中的字符</h6><pre><code>if(v.match(/[\p{C}]/u)){//p是匹配的意思 C是其他
return{
type:"noOther",
message:"您输入了非法字符"
}
}</code></pre><h5>可以匹配一下各种特殊字符的正则表达式</h5><pre><code>((?=[\x21-\x7e]+)[^A-Za-z0-9])
x21-\x7e]+)[^A-Za-z0-9])</code></pre><h6>匹配所有键盘上可见的非字母和数字的符号</h6><pre><code>var patrn = /[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘',。、]/im;
if (!patrn.test(str)) {// 如果包含特殊字符返回false
return false;
}
return true;</code></pre><p>输入框防止特殊字符勿输入验证,包括键盘上所有特殊字符的英文和中文状态</p><h6>匹配非空 非字母 非数字</h6><p><code>[^\w\s]+</code></p><p>1 数字:^[0-9]*$</p><p>2 n位的数字:^\d{n}$</p><p>3 至少n位的数字:^\d{n,}$</p><p>4 m-n位的数字:^\d{m,n}$</p><p>5 零和非零开头的数字:^(0|1-9*)$</p><p>6 非零开头的最多带两位小数的数字:^(1-9*)+(.[0-9]{1,2})?$</p><p>7 带1-2位小数的正数或负数:^(-)?\d+(.\d{1,2})?$</p><p>8 正数、负数、和小数:^(-|+)?\d+(.\d+)?$</p><p>9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$</p><p>10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$</p><p>11 非零的正整数:^[1-9]\d<em>$ 或 ^([1-9][0-9]*){1,3}$ 或 ^+?1-9</em>$</p><p>12 非零的负整数:^-[1-9][]0-9"*$ 或 ^-[1-9]\d*$</p><p>13 非负整数:^\d+$ 或 ^[1-9]\d*|0$</p><p>14 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$</p><p>15 非负浮点数:^\d+(.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$</p><p>16 非正浮点数:^((-\d+(.\d+)?)|(0+(.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$</p><p>17 正浮点数:^[1-9]\d<em>.\d</em>|0.\d<em>[1-9]\d</em>$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$</p><p>18 负浮点数:^-([1-9]\d<em>.\d</em>|0.\d<em>[1-9]\d</em>)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$</p><p>19 浮点数:^(-?\d+)(.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$</p><p><strong>校验字符的表达式</strong><br>1 汉字:^[\u4e00-\u9fa5]{0,}$</p><p>2 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$</p><p>3 长度为3-20的所有字符:^.{3,20}$</p><p>4 由26个英文字母组成的字符串:^[A-Za-z]+$</p><p>5 由26个大写英文字母组成的字符串:^[A-Z]+$</p><p>6 由26个小写英文字母组成的字符串:^[a-z]+$</p><p>7 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$</p><p>8 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$</p><p>9 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$</p><p>10 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$</p><p>11 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+</p><p>12 禁止输入含有~的字符:<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>+</p><h3>特殊需求表达式</h3><p>1 Email地址:^\w+([-+.]\w+)<em>@\w+([-.]\w+)</em>.\w+([-.]\w+)*$</p><p>a、自定义完美的邮箱验证:(java)</p><pre><code>^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$</code></pre><p>2 域名:a-zA-Z0-9{0,62}(/.a-zA-Z0-9{0,62})+/.?</p><p>3 InternetURL:[a-zA-z]+://<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup><em> 或 ^http://([\w-]+.)+[\w-]+(/[\w-./?%&=]</em>)?$<br>4 手机号码最新:</p><pre><code>^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$(java)
^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\d{8}$(js或jq)</code></pre><p>5 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-</p><pre><code>XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$ </code></pre><p>6 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}</p><p>7 身份证号(15位、18位数字):^\d{15}|\d{18}$</p><p>8 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$</p><p>9 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^a-zA-Z{4,15}$</p><p>10 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$</p><p>11 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.<em>\d)(?=.</em>[a-z])(?=.*[A-Z]).{8,10}$</p><p>12 日期格式:^\d{4}-\d{1,2}-\d{1,2}</p><p>13 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$</p><p>14 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$</p><p>15 钱的输入格式:</p><p>16 1.有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^1-9*$</p><p>17 2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|1-9*)$<br>18 3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?1-9*)$<br>19 4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$</p><p>20 5.必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$</p><p>21 6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$</p><p>22 7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$</p><p>23 8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$</p><p>24 备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里</p><p>25 xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\.x|X[l|L]$</p><p>26 中文字符的正则表达式:[\u4e00-\u9fa5]</p><p>27 双字节字符:<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup> (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))</p><p>28 空白行的正则表达式:\n\s*\r (可以用来删除空白行)</p><p>29 HTML标记的正则表达式:<(\S<em>?)<sup id="fnref-4"><a href="#fn-4" class="footnote-ref">4</a></sup></em>>.<em>?</\1>|<.</em>? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)</p><p>30 首尾空白字符的正则表达式:^\s<em>|\s</em>$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)</p><p>31 腾讯QQ号:1-9{4,} (腾讯QQ号从10000开始)</p><p>32 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字) 33 IP地址:\d+.\d+.\d+.\d+ (提取IP地址时有用) 34 IP地址:((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))</p><p>正则表达式可以为空值,不为空则要格式。</p><p>格式如下:</p><p>^$|^(\d+|\-){7,}$ ("|"后边的是要符合格式。)</p><p>如果可以为空的空可以是空格和制表符那就这么写:^\s*$|^(\d+|\-){7,}$</p><p>正则表达式不为空用\S匹配,不能有空格可以用<sup id="fnref-5"><a href="#fn-5" class="footnote-ref">5</a></sup>匹配,[]中^后面是一个空格。</p><p>[size=12px]1。^d+$ //匹配非负整数(正整数 + 0)<br>2。^[0-9]<em>1-9</em>$ //匹配正整数<br>3。^((-d+)|(0+))$ //匹配非正整数(负整数 + 0)<br>4。^-[0-9]<em>1-9</em>$ //匹配负整数<br>5。^-?d+$ //匹配整数<br>6。^d+(.d+)?$ //匹配非负浮点数(正浮点数 + 0)<br>7。^(([0-9]+.[0-9]<em>1-9</em>)|([0-9]<em>1-9</em>.[0-9]+)|([0-9]<em>1-9</em>))$ //匹配正浮点数<br>8。^((-d+(.d+)?)|(0+(.0+)?))$ //匹配非正浮点数(负浮点数 + 0)<br>9。^(-(([0-9]+.[0-9]<em>1-9</em>)|([0-9]<em>1-9</em>.[0-9]+)|([0-9]<em>1-9</em>)))$ //匹配负浮点数<br>10。^(-?d+)(.d+)?$ //匹配浮点数<br>11。^[A-Za-z]+$ //匹配由26个英文字母组成的字符串<br>12。^[A-Z]+$ //匹配由26个英文字母的大写组成的字符串<br>13。^[a-z]+$ //匹配由26个英文字母的小写组成的字符串<br>14。^[A-Za-z0-9]+$ //匹配由数字和26个英文字母组成的字符串<br>15。^w+$ //匹配由数字、26个英文字母或者下划线组成的字符串<br>16。^[w-]+(.[w-]+)*@[w-]+(.[w-]+)+$ //匹配email地址<br>17。^[a-zA-z]+://匹配(w+(-w+)<em>)(.(w+(-w+)</em>))<em>(?S</em>)?$ //匹配url <br> 18。匹配中文字符的正则表达式: [u4e00-u9fa5]<br> 19。匹配双字节字符(包括汉字在内):<sup id="fnref-6"><a href="#fn-6" class="footnote-ref">6</a></sup><br> 20。应用:计算字符串的长度(一个双字节字符长度计2,ASCII字符计1)<br> String.prototype.len=function(){return this.replace(<sup id="fnref-6"><a href="#fn-6" class="footnote-ref">6</a></sup>/g,"aa").length;}</p><p>21。匹配空行的正则表达式:n[s| ]*r<br> 22。匹配HTML标记的正则表达式:/<(.<em>)>.</em></1>|<(.*) />/ <br> 23。匹配首尾空格的正则表达式:(^s<em>)|(s</em>$)</p><p>### 正则表达式用例</p><ul><li>1、^S+[a-z A-Z]$ 不能为空 不能有空格 只能是英文字母</li><li>2、S{6,} 不能为空 六位以上</li><li>3、^d+$ 不能有空格 不能非数字</li><li>4、(.*)(.jpg|.bmp)$ 只能是jpg和bmp格式</li><li>5、^d{4}-d{1,2}-d{1,2}$ 只能是2004-10-22格式</li><li>6、^0$ 至少选一项</li><li>7、^0{2,}$ 至少选两项</li><li>8、^[s|S]{20,}$ 不能为空 二十字以上</li><li>9、^+?<a href="([-+.]|[_]+">a-z0-9</a>?[a-z0-9]+)*@([a-z0-9]+(.|-))+[a-z]{2,6}$邮件</li><li>10、w+([-+.]w+)<em>@w+([-.]w+)</em>.w+([-.]w+)<em>([,;]s</em>w+([-+.]w+)<em>@w+([-.]w+)</em>.w+([-.]w+)<em>)</em> 输入多个地址用逗号或空格分隔邮件</li><li><p>11、^(([0-9]+))?[0-9]{7,8}$电话号码7位或8位或前面有区号例如(022)87341628</p><ul><li>12、^[a-z A-Z 0-9 _]+@[a-z A-Z 0-9 _]+(.[a-z A-Z 0-9 _]+)+(,[a-z A-Z 0-9 _]+@[a-z A-Z 0-9 _]+(.[a-z A-Z 0-9 _]+)+)*$</li><li>只能是字母、数字、下划线;必须有@和.同时格式要规范 邮件</li></ul></li><li>13 ^w+@w+(.w+)+(,w+@w+(.w+)+)*$上面表达式也可以写成这样子,更精练。<br>14 ^w+((-w+)|(.w+))<em>@w+((.|-)w+)</em>.w+$ [/size]</li></ul><p>### 正则表达式中的.<em>,.</em>?,.+?的含义</p><ol><li>.*</li></ol><p>. 表示匹配除换行符 \n 之外的任何单字符,<em>表示零次或多次。所以.</em>在一起就表示任意字符出现零次或多次。没有?表示贪婪模式。比如a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。<br>又比如模式src=<code>.*</code>, 它将会匹配最长的以 src=<code> 开始,以</code>结束的最长的字符串。用它来搜索 <img src=<code>test.jpg` width=`60px` height=`80px`/> 时,将会返回 src=</code>test.jpg<code> width=</code>60px<code> height=</code>80px`</p><ol start="2"><li>.*?</li></ol><p>?跟在*或者+后边用时,表示懒惰模式。也称非贪婪模式。就是匹配尽可能少的字符。就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。<br>a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。</p><p>又比如模式 src=<code>.*?</code>,它将会匹配 src=<code> 开始,以 </code> 结束的尽可能短的字符串。且开始和结束中间可以没有字符,因为*表示零到多个。用它来搜索 <img src=<code>test.jpg` width=`60px` height=`80px`/> 时,将会返回 src=</code>。</p><ol start="3"><li>.+?</li></ol><p>同上,?跟在*或者+后边用时,表示懒惰模式。也称非贪婪模式。就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。<br>a.+?b匹配最短的,以a开始,以b结束的字符串,但a和b中间至少要有一个字符。如果把它应用于ababccaab的话,它会匹配abab(第一到第四个字符)和aab(第七到第九个字符)。注意此时匹配结果不是ab,ab和aab。因为a和b中间至少要有一个字符。<br>又比如模式 src=<code>.+?</code>,它将会匹配 src=<code> 开始,以 </code> 结束的尽可能短的字符串。且开始和结束中间必须有字符,因为+表示1到多个。用它来搜索 <img src=<code>test.jpg` width=`60px` height=`80px`/> 时,将会返回 src=</code>test.jpg<code>。注意与.*?时的区别,此时不会匹配src=</code>`,因为src=<code> 和 </code> 之间至少有一个字符。</p><h3>正则表达式中\w 和\w的区别,</h3><p>一、定义不同:</p><p>\W:匹配包括下划线的任何单词字符,等价于 [A-Z a-z 0-9_]</p><p>\W:匹配任何非单词字符,等价于 <sup id="fnref-7"><a href="#fn-7" class="footnote-ref">7</a></sup></p><p>[\w.\_] 相当于[0-9a-zA-Z.\_] ,就是比\w多匹配 '.' 和 ‘_’ 两种字符。</p><p>二、用途不同:</p><p>w前面加了反斜杠,是\w,与[a-zA-Z0-9_]等价,</p><p>也就是包含下划线以下的所有字母和数字,</p><p>而后面的加号,则是匹配紧跟其前面那个字符的 一次或多次,</p><p>中括号[]表示匹配其中任意字符,</p><p>[\w./]+就是一或多次匹配,任何数字,字母,下划线,斜杠,还有英文的句号(一个点)。</p><p>三、字符不同:</p><p>W内的字符可以以任意次序出现。</p><p>W后有了+,X内的字符可以取任意多个。于是[]内的字符可以以任意次序出现任意多次,直到遇到第一个非[]内的字符。</p><p>如[AB]+ 既可以匹配AAABBB又可以匹配BBBAAA BABAAABA等,不是一定要A....B....的次序。<br><img src="/img/bVcT7UF" alt="image.png" title="image.png"></p><div class="footnotes"><hr><ol><li id="fn-1">~\x22 <a href="#fnref-1" class="footnote-backref">↩</a></li><li id="fn-2">\s <a href="#fnref-2" class="footnote-backref">↩</a></li><li id="fn-3">\x00-\xff <a href="#fnref-3" class="footnote-backref">↩</a></li><li id="fn-4">> <a href="#fnref-4" class="footnote-backref">↩</a></li><li id="fn-5"> <a href="#fnref-5" class="footnote-backref">↩</a></li><li id="fn-6">x00-xff <a href="#fnref-6" class="footnote-backref">↩</a></li><li id="fn-7">A-Z a-z 0-9_ <a href="#fnref-7" class="footnote-backref">↩</a></li></ol></div>
Fetch在项目中灵活使用
https://segmentfault.com/a/1190000024440786
2020-09-13T16:02:06+08:00
2020-09-13T16:02:06+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
1
<h3>fetch的兼容</h3><pre><code>import 'fetch-detector' //fetch的兼容包
import 'fetch-ie8'</code></pre><h3>Fetch的post请求</h3><pre><code>const fetchPost = (url,params)=>{
return fetch(url,{
method:"POST",
header:{
"Content-Type":"application/x-www-form-urlencode"
},
params:params,
credentials:'include',//设置了这个之后 请求才会带上cookie
}).then(res=>{
if(!res.OK){
throw Error(res.statusText)
}
return res.json()
})
}
export {fetchPost}</code></pre><h3>Fetch mock数据</h3><pre><code>
import FetchMock from 'fetch-mock'
FetchMock.mock('/login',(url,opts)=>{
const params = opts.params
if(params.account === '17521077157'){
if(params.password === '12345'){
}else{
return {
code:401,message:'密码错误'
}
}
}else{
return {code:400,message:'用户名错误'}
}
}
)</code></pre>
前端javascript封装SDK 简易版
https://segmentfault.com/a/1190000023938951
2020-09-08T17:15:09+08:00
2020-09-08T17:15:09+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
0
<p>**由于我不确定中台在使用时,有没有axios之类的依赖库,<br>所以在sdk内部封装了ajax。尽量做到一处封装,处处可用。**</p><h3>unicomMiddle.js</h3><pre><code>/*
* @Author: yang
* @Date: 2020-09-08 10:13:32
* @LastEditors: yang
* @LastEditTime: 2020-09-08 16:53:24
* @FilePath: \vuex\src\components\unicomMiddle.js
*/
var unicomMiddle = {
/**
* 创建Ajax
* @param options
*/
Ajax(options) {
// 新建一个对象,用途接受XHR对象
var xhr = null;
// 第一步创建XMLHttpRequest对象 || 同时兼任IE
// 首先检测原生XHR对象是否存在,如果存在则返回它的新实例
if (typeof XMLHttpRequest != "undefined") {
xhr = new XMLHttpRequest();
// 然后如果原生对象不存在,则检测ActiveX对象
} else if (typeof ActiveXObject != "undefined") {
// 如果存在,则创建他的对象,但这个对象需要一个传入参数,如下:
if (typeof arguments.callee.activeXString != 'string') {
// 对象版本
var versions = [
'Microsoft.XMLHTTP',
'Msxml2.XMLHTTP.7.0',
'Msxml2.XMLHTTP.6.0',
'Msxml2.XMLHTTP.5.0',
'Msxml2.XMLHTTP.4.0',
'MSXML2.XMLHTTP.3.0',
'MSXML.XMLHTTP'
], i, len;
for (i = 0, len = versions.length; i < len; i++) {
try {
// 需要versions数组中的某个项,数组的7个项分别对应7个版本.
new ActiveXObject(versions[i]);
// arguments是javascript函数的内置对象,代表传入参数的集合,
// callee就代表对象本身即new createXHR
arguments.callee.activeXString = versions[i];
break;
} catch (e) {
// 跳过
}
}
}
// 直到循环创建成功为止,然后给自己添加一个属性叫activeXString
xhr = new ActiveXObject(arguments.callee.activeXString);
} else {
// 如果这两种对象都不存在,就抛出一个错误
throw new Error('No XHR object available');
}
/**
** options形参解析:
* data 发送的参数,格式为对象类型
* url 发送请求的url,服务器地址(api)
* async 否为异步请求,true为异步的,false为同步的
* method http连接的方式,包括POST和GET两种方式
*/
options = options || {};
options.success = options.success || function () {
};
options.fail = options.fail || function () {
};
var data = options.data,
url = options.url,
async = options.async === undefined ? true : options.async,
method = options.method.toUpperCase(),
dataArr = [];
// 遍历参数
for (var k in data) {
dataArr.push(k + '=' + data[k]);
}
// GET请求
if (method === 'GET') {
url = url + '?' + dataArr.join('&');
xhr.open(method, url.replace(/\?$/g, ''), async);
xhr.send();
}
// POST请求
if (method === 'POST') {
xhr.open(method, url, async);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(dataArr.join('&'));
}
// 响应接收完毕后将触发load事件
xhr.onload = function () {
/**
* XHR对象的readyState属性
* 0:未初始化。尚未调用open()方法。
* 1:启动。已经调用open()方法,但尚未调用send()方法。
* 2:发送。已经调用send()方法,但尚未接收到响应。
* 3:接收。已经接收到部分响应数据。
* 4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。
*/
if (xhr.readyState == 4) {
// 得到响应
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
// 处理成功数据
var res;
if (options.success && options.success instanceof Function) {
res = xhr.responseText;
if (typeof res === 'string') {
res = JSON.parse(res);
options.success.call(xhr, res);
}
}
} else {
// 处理错误数据
if (options.fail && options.fail instanceof Function) {
options.fail.call(xhr, res)
}
}
} else {
// 抛出检测XHR对象的readyState属性
console.log('XHR was readyState:', xhr.readyState);
}
}
},
getMsgCode(obj){
let url = `http://**/**/cuauth/smscode?mobile=${obj.tel}&clientId=${obj.clientId}&smsType=1`
return new Promise((resolve,reject)=>{
this.Ajax({
url: url,
method: 'GET',
async: true,
success: function (res) {
// console.log('successful', res);
resolve(res)
},
fail: function (err) {
console.log('fail', err);
reject(err)
}
})
})
},
getCodeLogin(obj){
let url = `http://*****/**/**/smslogin?clientId=${obj.clientId}&mobile=${obj.tel}&code=${obj.captcha}&appType=2&redirectUrl=${obj.redirectUrl}`
return new Promise((resolve,reject)=>{
this.Ajax({
url: url,
method: 'GET',
async: true,
success: function (res) {
// console.log('登录成功,即将跳转能力平台!')
if(res.code==200&&obj.redirectUrl){
window.location.href = obj.redirectUrl
}
resolve(res)
},
fail: function (err) {
console.log('fail', err);
reject(err)
}
})
})
}
}
export default unicomMiddle</code></pre><h3>使用</h3><pre><code>
import unicomMiddle from './unicomMiddle.js'
export default {
methods: {
async getCode() {
let parm = {
tel: '17521077157',
clientId: 'hfgo',
}
let result = await unicomMiddle.getMsgCode(parm)
console.log(result)
},
async getLogin(){
let parms = {
tel: '17521077157',
clientId: '**',
captcha:'575356',
redirectUrl:'http://www.baidu.com'
}
unicomMiddle.getCodeLogin(parms).then(res=>{
console.log(res)//{code: 200, msg: "短信登录成功."} code:200代表登录成功
})
}
},
}
</script></code></pre><h3>API</h3><p><img src="/img/bVbMBKq" alt="image.png" title="image.png"><br><img src="/img/bVbMBKz" alt="image.png" title="image.png"><br><img src="/img/bVbMBKP" alt="image.png" title="image.png"><br><img src="/img/bVbMBK1" alt="image.png" title="image.png"></p>
React的Hooks
https://segmentfault.com/a/1190000023909557
2020-09-06T16:51:03+08:00
2020-09-06T16:51:03+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
3
<p>Hooks是react16.8一个新增项,它可以在你不编写class的情况下使用state以及其他的React特性</p><p>Hooks的特点:<br>使你在无需修改组件结构的情况下复用状态逻辑<br>可将组件中相互关联的部分拆分成更小的函数,复杂组件将变得更容易理解<br>更简洁,更易理解的代码</p><p><strong>状态钩子 State Hook</strong></p><pre><code>/*
* @Author: yang
* @Date: 2020-09-03 18:04:27
* @LastEditors: yang
* @LastEditTime: 2020-09-06 13:01:06
* @FilePath: \reacty\src\ContextText.js
*/
import React, { Component,useState } from 'react'
function FruitList({fruits,setFruit}){
return (
fruits.map( f => <li key={f} onClick={()=> setFruit(f)}>{f}</li>))
}
function FriutAdd(props){
const [pname,setPname] = useState("")
const onAddFruit = (e)=> {
if (e.key === "Enter") {
props.onAddFruit(pname)
setPname("")
}
}
return (
<input
type="text"
value={pname}
onChange={e=>setPname(e.target.value)}
onKeyDown={onAddFruit}
/>
)
}
export default function HookText(){
// useState参数是状态初始值
// 返回一个数组,第一个元素是状态变量,第二个元素是状态变更函数
const [fruit,setFruit] = useState('草莓')
const [fruits,setFruits] = useState(['草莓','苹果'])
return(
<div>
<p>{fruit===''?'请选择喜爱的水果':`你选择的水果是${fruit}`}</p>
<FriutAdd onAddFruit={pname => setFruits([...fruits,pname])}/>
<FruitList fruits={fruits} setFruit={setFruit}/>
</div>
)
}
</code></pre><h4>副作用钩子Effect Hook</h4><p>useEffect给函数组件增加了执行副作用操作的能力<br>数据获取,设置订阅以及手动更改React组件中的DOM都属于<br>副作用</p><pre><code>/*
* @Author: yang
* @Date: 2020-09-03 18:04:27
* @LastEditors: yang
* @LastEditTime: 2020-09-06 13:32:26
* @FilePath: \reacty\src\ContextText.js
*/
import React, { Component,useState,useEffect, } from 'react'
function FruitList({fruits,setFruit}){
return (
fruits.map( f => <li key={f} onClick={()=> setFruit(f)}>{f}</li>))
}
function FriutAdd(props){
const [pname,setPname] = useState("")
const onAddFruit = (e)=> {
if (e.key === "Enter") {
props.onAddFruit(pname)
setPname("")
}
}
return (
<input
type="text"
value={pname}
onChange={e=>setPname(e.target.value)}
onKeyDown={onAddFruit}
/>
)
}
export default function HookText(){
// useState参数是状态初始值
// 返回一个数组,第一个元素是状态变量,第二个元素是状态变更函数
const [fruit,setFruit] = useState('草莓')
const [fruits,setFruits] = useState([''])
// 使用useEffect操作副作用
//请务必设置依赖选项,如果没有则设置空数组 表示仅执行一次
useEffect(()=>{
console.log('get Fruit')
setTimeout(()=>{
setFruits(['草莓','苹果'])
},1000)
},[])
useEffect(()=>{
document.title = fruit
},[fruit])
useEffect(()=>{
const timer = setInterval(() => {
console.log('应用启动了')
}, 1000);
// 返回清除函数
return function(){
clearInterval(timer)
}
},[])
return(
<div>
<p>{fruit===''?'请选择喜爱的水果':`你选择的水果是${fruit}`}</p>
<FriutAdd onAddFruit={pname => setFruits([...fruits,pname])}/>
<FruitList fruits={fruits} setFruit={setFruit}/>
</div>
)
}
</code></pre><h3>useReducer</h3><p>useReducer是useState的可选项,常用于组件的复杂状态逻辑。类似于reducx中的reducer概念<br> <code>`</code>~~~~<br>/*</p><ul><li>@Author: yang</li><li>@Date: 2020-09-03 18:04:27</li><li>@LastEditors: yang</li><li>@LastEditTime: 2020-09-06 16:04:13</li><li>@FilePath: reactysrcContextText.js</li></ul><p>*/<br>import React, { Component,useState,useEffect,useReducer } from 'react'<br>function FruitList({fruits,setFruit}){</p><pre><code>return (
fruits.map( f => <li key={f} onClick={()=> setFruit(f)}>{f}</li>))</code></pre><p>}<br>// 将状态移至全局<br>function fruitReducer(state,action){</p><pre><code>switch(action.type){
case "init":
return action.payload;
case "add":
return [...state,action.payload];
default:
return state;
}</code></pre><p>}<br>function FriutAdd(props){</p><pre><code>const [pname,setPname] = useState("")
const onAddFruit = (e)=> {
if (e.key === "Enter") {
props.onAddFruit(pname)
setPname("")
}
}
return (
<input
type="text"
value={pname}
onChange={e=>setPname(e.target.value)}
onKeyDown={onAddFruit}
/>
)</code></pre><p>}<br>export default function HookText(){</p><pre><code>// useState参数是状态初始值
// 返回一个数组,第一个元素是状态变量,第二个元素是状态变更函数
const [fruit,setFruit] = useState('草莓')
// const [fruits,setFruits] = useState([''])
// 参数一是相关的Reducer 参数二是初始值
const [fruits,dispatch] = useReducer(fruitReducer,[])
// 使用useEffect操作副作用
//请务必设置依赖选项,如果没有则设置空数组 表示仅执行一次
useEffect(()=>{
console.log('get Fruit')
setTimeout(()=>{
// setFruits(['草莓','苹果'])
dispatch({type:"init",payload:['草莓','苹果']})
},1000)
},[])
useEffect(()=>{
document.title = fruit
},[fruit])
useEffect(()=>{
const timer = setInterval(() => {
console.log('应用启动了')
}, 1000);
// 返回清除函数
return function(){
clearInterval(timer)
}
},[])
return(
<div>
<p>{fruit===''?'请选择喜爱的水果':`你选择的水果是${fruit}`}</p>
{/* <FriutAdd onAddFruit={pname => setFruits([...fruits,pname])}/> */}
<FriutAdd onAddFruit={pname => dispatch({type:"add",payload:pname})}/>
<FruitList fruits={fruits} setFruit={setFruit}/>
</div>
)</code></pre><p>}</p><pre><code>### useContext
useContext用于快速在函数组件中导入上下文</code></pre><p>import React, { Component,useState,useEffect,useReducer,useContext } from 'react'<br>const Context = React.createContext()<br>function FruitList({fruits,setFruit}){</p><pre><code>return (
fruits.map( f => <li key={f} onClick={()=> setFruit(f)}>{f}</li>))</code></pre><p>}<br>// 将状态移至全局<br>function fruitReducer(state,action){</p><pre><code>switch(action.type){
case "init":
return action.payload;
case "add":
return [...state,action.payload];
default:
return state;
}</code></pre><p>}<br>function FriutAdd(props){</p><pre><code>const [pname,setPname] = useState("");
const {dispatch} = useContext(Context);//拿到传入的value
const onAddFruit = (e)=> {
if (e.key === "Enter") {
dispatch({type:"add",payload:pname})
// props.onAddFruit(pname)
setPname("")
}
}
return (
<input
type="text"
value={pname}
onChange={e=>setPname(e.target.value)}
onKeyDown={onAddFruit}
/>
)</code></pre><p>}<br>export default function HookText(){</p><pre><code>// useState参数是状态初始值
// 返回一个数组,第一个元素是状态变量,第二个元素是状态变更函数
const [fruit,setFruit] = useState('草莓')
// const [fruits,setFruits] = useState([''])
// 参数一是相关的Reducer 参数二是初始值
const [fruits,dispatch] = useReducer(fruitReducer,[])
// 使用useEffect操作副作用
//请务必设置依赖选项,如果没有则设置空数组 表示仅执行一次
useEffect(()=>{
console.log('get Fruit')
setTimeout(()=>{
// setFruits(['草莓','苹果'])
dispatch({type:"init",payload:['草莓','苹果']})
},1000)
},[])
useEffect(()=>{
document.title = fruit
},[fruit])
useEffect(()=>{
const timer = setInterval(() => {
console.log('应用启动了')
}, 1000);
// 返回清除函数
return function(){
clearInterval(timer)
}
},[])
return(
<Context.Provider value={{fruits,dispatch}}>
<div>
<p>{fruit===''?'请选择喜爱的水果':`你选择的水果是${fruit}`}</p>
{/* <FriutAdd onAddFruit={pname => setFruits([...fruits,pname])}/> */}
<FriutAdd />
<FruitList fruits={fruits} setFruit={setFruit}/>
</div>~~~~
</Context.Provider>
)</code></pre><p>}</p>
react项目利用react-app-rewired实现按需打包
https://segmentfault.com/a/1190000023888223
2020-09-04T10:08:48+08:00
2020-09-04T10:08:48+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
2
<p><img src="/img/bVbMozt" alt="image.png" title="image.png"></p><pre><code>cnpm i react-app-rewired customize-cra babel-plugin-import -D</code></pre><p><strong>根目录新建config-overrides.js</strong></p><pre><code>const {override,fixBabelImports} = require('customize-cra')
// override返回一个函数 该函数返回对象作为webpack的配置对象
module.exports = override(
fixBabelImports("import",{
libraryName:'antd', //库名
libraryDirectory:'es', //文件夹名
style:'css' //一个叫css.js的文件
})
)</code></pre><p><strong>package.json</strong></p><pre><code>script改成
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},</code></pre><p><strong>使用</strong></p><pre><code>import React, { Component } from 'react'
import {Button} from 'antd'
export default class Text extends Component {
render() {
return (
<div>
<Button type="primary">BUtton</Button>
</div>
)
}
}</code></pre>
React高阶组件
https://segmentfault.com/a/1190000023888132
2020-09-04T10:04:52+08:00
2020-09-04T10:04:52+08:00
HappyCodingTop
https://segmentfault.com/u/yang_5c8f63fe0d5aa
2
<h5>**扩充组件。</h5><h5>创建一个函数扩充之后返回另一个函数**</h5><pre><code>import React, { Component } from 'react'
function bala(props){
return(
<div>
{props.age} - {props.name}
</div>
)
}
function moreStage(Component){
const moreComponent = props =>{
return <Component {...props} name="jack"/>
}
return moreComponent
}
export default moreStage(bala)</code></pre><h5><strong>App.js中使用</strong></h5><pre><code>import React from 'react';
import './App.css';
import Hoc from './Hoc'
function App() {
return (
<div className="App">
<Hoc age="12"/>
</div>
);
}
export default App;</code></pre><h5><strong>链式调用</strong></h5><pre><code>import React, { Component } from 'react'
function bala(props){
return(
<div>
{props.age} - {props.name}
</div>
)
}
function moreStage(Component){
const moreComponent = props =>{
return <Component {...props} name="jack"/>
}
return moreComponent
}
function withlog(Component){
console.log(Component.name+'加强的')
return props =>{
return <Component {...props} />
}
}
export default moreStage(withlog(moreStage(withlog(bala))))</code></pre><h5>装饰器写法</h5><pre><code>npm i @babel/plugin-proposal-decorators -D</code></pre><p><strong>在config-overrides.js中添加</strong></p><pre><code>const {override,fixBabelImports,addDecoratorsLegacy} = require('customize-cra')
// override返回一个函数 该函数返回对象作为webpack的配置对象
module.exports = override(
fixBabelImports("import",{
libraryName:'antd',
libraryDirectory:'es',
style:'css'
}),
addDecoratorsLegacy()
)</code></pre><pre><code>import React, { Component } from 'react'
function withlog(Component){
console.log(Component.name+'加强的')
return props =>{
return <Component {...props} />
}
}
function moreStage(Component){
const moreComponent = props=>{
return <Component {...props} name="jack"/>
}
return moreComponent
}
@withlog
@moreStage
@withlog
class bala extends Component{
render(){
return(
<div>
{this.props.age} - {this.props.name}
</div>
)
}
}
// export default moreStage(withlog(moreStage(withlog(bala))))
export default bala</code></pre><h5>组件复合</h5><pre><code>import React, { Component } from 'react'
function Dialog(props){
let color = props.color||'yellow'
return (
<div style={{border:`1px solid ${color}`}}>
{/* children是固定的名称 相当于slot,匿名插槽 */}
{props.children}
{props.foo('子组件传过来的')}
<div>
{/* 具名插槽 */}
{props.footer}
</div>
</div>
)
}
function WelcomeDialog(){
let footer = <button onClick={()=>alert('...')}>确认</button>
return (
//传递任意合法表达式
<Dialog color="lightBlue" footer={footer} foo={(c)=><p>{c}</p>}>
<p>React牛逼</p>
</Dialog>
)
}
export default function Composition(){
return <WelcomeDialog>
}</code></pre><p><img src="/img/bVbMoxM" alt="image.png" title="image.png"></p><h5>简易版的redux</h5><pre><code>import React, { Component } from 'react'
const Context = React.createContext()
const store = {
token:'kkk'
}
export default class ContextText extends Component {
render() {
return (
<Context.Provider value={store}>
<div>
<Context.Consumer>
{store=> <p>{store.token}</p>}
</Context.Consumer>
</div>
</Context.Provider>
)
}
}</code></pre>