{"success":true,"data":[{"id":"5fe1c9ad96b2cb0032c388da","author_id":"58739c71a9c1282817afbf7d","tab":"share","content":"<div class=\"markdown-text\"><p>万水千山总是情\n给个 star 行不行</p>\n<ul>\n<li><a href=\"https://github.com/febobo/web-interview\">github传送门</a></li>\n</ul>\n<p>点击<a href=\"https://static.vue-js.com/b4b71a30-443b-11eb-85f6-6fac77c0c9b3.png\">@公众号:JS每日一题</a>扫码关注，跟踪我们最新动态</p>\n<h3>Vue系列</h3>\n<ul>\n<li><a href=\"https://github.com/febobo/web-interview/issues/1\">面试官：说说你对vue的理解?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/2\">面试官：说说你对双向绑定的理解?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/3\">面试官：说说你对SPA（单页应用）的理解?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/4\">面试官：Vue中的v-show和v-if怎么理解？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/5\">面试官：Vue实例挂载的过程中发生了什么?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/6\">面试官：说说你对Vue生命周期的理解?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/7\">面试官：为什么Vue中的v-if和v-for不建议一起用?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/8\">面试官：SPA（单页应用）首屏加载速度慢怎么解决？?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/9\">面试官：为什么data属性是一个函数而不是一个对象？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/10\">面试官：Vue中给对象添加新属性界面不刷新?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/11\">面试官：Vue中组件和插件有什么区别</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/12\">面试官：Vue组件间通信方式都有哪些?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/14\">面试官：说说你对nexttick的理解?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/15\">面试官：说说你对vue的mixin的理解，有什么应用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/16\">面试官：说说你对slot的理解？slot使用场景有哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/17\">面试官：Vue.observable你有了解过吗？说说看</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/18\">面试官：你知道vue中key的原理吗？说说你对它的理解？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/19\">面试官：怎么缓存当前的组件？缓存后怎么更新？说说你对keep-alive的理解是什么？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/20\">面试官：Vue常用的修饰符有哪些？有什么应用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/21\">面试官：你有写过自定义指令吗？自定义指令的应用场景有哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/22\">面试官：Vue中的过滤器了解吗？过滤器的应用场景有哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/23\">面试官：什么是虚拟DOM？如何实现一个虚拟DOM？说说你的思路</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/24\">面试官：了解过vue中的diff算法吗？说说看</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/25\">面试官：Vue项目中有封装过axios吗？怎么封装的？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/26\">面试官：你了解Axios的原理吗？有看过它的源码吗？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/27\">面试官：SSR解决了什么问题？有做过SSR吗？你是怎么做的？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/28\">面试官：说下你的Vue项目的目录结构，如果是大型项目你该怎么划分结构和划分组件呢？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/29\">面试官：Vue要做权限管理该怎么做？控制到按钮级别的权限怎么做？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/30\">面试官：跨域是什么？Vue项目中你是如何解决跨域的呢？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/31\">面试官：Vue项目如何部署？有遇到布署服务器后刷新404问题吗？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/32\">面试官：你是怎么处理vue项目中的错误的？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/33\">面试官：Vue3有了解过吗？能说说跟Vue2的区别吗？</a></li>\n</ul>\n<h3>Vue3系列</h3>\n<ul>\n<li><a href=\"https://github.com/febobo/web-interview/issues/45\">面试官：Vue3.0的设计目标是什么？做了哪些优化?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/46\">面试官：Vue3.0 性能提升主要是通过哪几方面体现的？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/47\">面试官：Vue3.0里为什么要用 Proxy API 替代 defineProperty API ？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/48\">面试官：Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/67\">面试官：说说Vue 3.0中Treeshaking特性？举例说明一下？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/50\">面试官：用Vue3.0 写过组件吗？如果想实现一个 Modal你会怎么设计？</a></li>\n</ul>\n<h3>ES6系列</h3>\n<ul>\n<li><a href=\"https://github.com/febobo/web-interview/issues/34\">面试官：说说var、let、const之间的区别</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/35\">面试官：ES6中数组新增了哪些扩展?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/36\">面试官：ES6中对象新增了哪些扩展?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/37\">面试官：ES6中函数新增了哪些扩展?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/38\">面试官：ES6中新增的Set、Map两种数据结构怎么理解?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/40\">面试官：你是怎么理解ES6中 Promise的？使用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/41\">面试官：怎么理解ES6中 Generator的？使用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/42\">面试官：你是怎么理解ES6中Proxy的？使用场景?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/43\">面试官：你是怎么理解ES6中Module的？使用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/44\">面试官：你是怎么理解ES6中 Decorator 的？使用场景？</a></li>\n</ul>\n<h3>Javascript系列</h3>\n<ul>\n<li><a href=\"https://github.com/febobo/web-interview/issues/51\">面试官：说说Javascript中的数据类型？区别？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/52\">面试官：Javscript数组的常用方法有哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/53\">面试官：Javascript字符串的常用方法有哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/54\">面试官：谈谈 Javascript 中的类型转换机制</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/55\">面试官：== 和 ===区别，分别在什么情况使用</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/56\">面试官：深拷贝浅拷贝的区别？如何实现一个深拷贝？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/57\">面试官：说说你对闭包的理解</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/58\">面试官：说说你对作用域链的理解</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/59\">面试官：JavaScript原型，原型链 ? 有什么特点？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/60\">面试官：Javascript如何实现继承？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/62\">面试官：谈谈this对象的理解</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/63\">面试官：JavaScript中执行上下文和执行栈是什么？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/64\">面试官：说说JavaScript中的事件模型</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/65\">面试官：typeof 与 instanceof 区别</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/66\">面试官：解释下什么是事件代理？应用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/69\">面试官：说说new操作符具体干了什么？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/70\">面试官：ajax原理是什么？如何实现？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/71\">面试官：bind、call、apply 区别？如何实现一个bind?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/72\">面试官：说说你对正则表达式的理解？应用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/73\">面试官：说说你对事件循环的理解</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/75\">面试官：DOM常见的操作有哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/76\">面试官：说说你对BOM的理解，常见的BOM对象你了解哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/77\">面试官：举例说明你对尾递归的理解，有哪些应用场景</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/78\">面试官：说说 JavaScript 中内存泄漏的几种情况？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/79\">面试官：Javascript本地存储的方式有哪些？区别及应用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/80\">面试官：说说你对函数式编程的理解？优缺点？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/81\">面试官：Javascript中如何实现函数缓存？函数缓存有哪些应用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/82\">面试官：说说 Javascript 数字精度丢失的问题，如何解决？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/83\">面试官：什么是防抖和节流？有什么区别？如何实现？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/84\">面试官：如何判断一个元素是否在可视区域中？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/89\">面试官：大文件上传如何做断点续传？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/90\">面试官：如何实现上拉加载，下拉刷新？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/91\">面试官：什么是单点登录？如何实现？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/92\">面试官：web常见的攻击方式有哪些？如何防御？</a></li>\n</ul>\n<h3>CSS系列</h3>\n<ul>\n<li><a href=\"https://github.com/febobo/web-interview/issues/93\">面试官：说说你对盒子模型的理解?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/95\">面试官：css选择器有哪些？优先级？哪些属性可以继承？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/96\">面试官：说说em/px/rem/vh/vw区别?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/97\">面试官：说说设备像素、css像素、设备独立像素、dpr、ppi 之间的区别？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/98\">面试官：css中，有哪些方式可以隐藏页面元素？区别?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/99\">面试官：谈谈你对BFC的理解？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/102\">面试官：元素水平垂直居中的方法有哪些？如果元素不定宽高呢？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/103\">面试官：如何实现两栏布局，右侧自适应？三栏布局中间自适应呢？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/104\">面试官：说说flexbox（弹性盒布局模型）,以及适用场景？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/105\">面试官：介绍一下grid网格布局</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/106\">面试官：CSS3新增了哪些新特性？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/109\">面试官：css3动画有哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/107\">面试官：怎么理解回流跟重绘？什么场景下会触发？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/108\">面试官：什么是响应式设计？响应式设计的基本原理是什么？如何做？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/114\">面试官：如果要做优化，CSS提高性能的方法有哪些？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/115\">面试官：如何实现单行／多行文本溢出的省略样式？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/116\">面试官：如何使用css完成视差滚动效果?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/117\">面试官：CSS如何画一个三角形？原理是什么？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/118\">面试官：让Chrome支持小于12px 的文字方式有哪些？区别？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/119\">面试官：说说对Css预编语言的理解？有哪些区别?</a></li>\n</ul>\n<h3>webpack系列</h3>\n<ul>\n<li><a href=\"https://github.com/febobo/web-interview/issues/121\">面试官：说说你对webpack的理解？解决了什么问题？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/122\">面试官：说说webpack的构建流程?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/123\">面试官：说说webpack中常见的Loader？解决了什么问题？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/124\">面试官：说说webpack中常见的Plugin？解决了什么问题？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/125\">面试官：说说Loader和Plugin的区别？编写Loader，Plugin的思路？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/126\">面试官：说说webpack的热更新是如何做到的？原理是什么？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/130\">面试官：说说webpack proxy工作原理？为什么能解决跨域?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/131\">面试官：说说如何借助webpack来优化前端性能？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/132\">面试官：如何提高webpack的构建速度？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/133\">面试官：与webpack类似的工具还有哪些？区别？</a></li>\n</ul>\n<h3>HTTP系列</h3>\n<ul>\n<li><a href=\"https://github.com/febobo/web-interview/issues/134\">面试官：什么是HTTP? HTTP 和 HTTPS 的区别?</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/135\">面试官：为什么说HTTPS比HTTP安全? HTTPS是如何保证安全的？</a></li>\n<li><a href=\"https://github.com/febobo/web-interview/issues/136\">面试官：如何理解UDP 和 TCP? 区别? 应用场景?</a></li>\n</ul>\n</div>","title":"金三银四，分享一波大厂高频面试题，持续更新！！！","last_reply_at":"2020-12-22T10:25:49.160Z","good":false,"top":true,"reply_count":0,"visit_count":14775,"create_at":"2020-12-22T10:25:49.160Z","author":{"loginname":"febobo","avatar_url":"https://avatars.githubusercontent.com/u/9276376?v=4&s=120"}},{"id":"653881f75784a80033303943","author_id":"5caea1435e4f3c032e4ea735","tab":"share","content":"<div class=\"markdown-text\"><p>Flutter全套教程_Flutter+Getx仿小米商城项目实战视频教程-V3版</p>\n<p><strong>2023年新出</strong></p>\n<p>Dart Flutter网盘下载地址：<a href=\"https://pan.baidu.com/s/1iaLGIyR9UmyJyHDGH6hepg\">https://pan.baidu.com/s/1iaLGIyR9UmyJyHDGH6hepg</a>   提取码：abcd</p>\n<p>在线学习地址：<a href=\"https://www.bilibili.com/video/BV1S4411E7LY?p=17\">https://www.bilibili.com/video/BV1S4411E7LY?p=17</a></p>\n</div>","title":"Flutter全套教程_Flutter+Getx仿小米商城项目实战视频教程-V3版","last_reply_at":"2023-10-25T02:48:23.033Z","good":false,"top":false,"reply_count":0,"visit_count":9721,"create_at":"2023-10-25T02:48:23.033Z","author":{"loginname":"phonegap100","avatar_url":"https://avatars.githubusercontent.com/u/5773766?v=4&s=120"}},{"id":"6538812b5784a80033303942","author_id":"5caea1435e4f3c032e4ea735","tab":"share","content":"<div class=\"markdown-text\"><p>Vue3.x教程_Vue3.x+Ts+Vuex+Antd Ui框架入门进阶视频教程分享：\nVue3.x视频 课件及源码在线学习及网盘下载地址：\n<a href=\"https://www.itying.com/goods-1159.html\">https://www.itying.com/goods-1159.html</a></p>\n</div>","title":"Vue3.x教程_Vue3.x+Ts+Vuex+Antd Ui框架入门进阶视频教程-免费学","last_reply_at":"2023-10-25T02:44:59.556Z","good":false,"top":false,"reply_count":0,"visit_count":9961,"create_at":"2023-10-25T02:44:59.556Z","author":{"loginname":"phonegap100","avatar_url":"https://avatars.githubusercontent.com/u/5773766?v=4&s=120"}},{"id":"61e67704ad745b003c9df797","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><h2>阅读本文📖</h2>\n<p>你将:</p>\n<ul>\n<li>弄明白： <code>v-model</code> 是什么的语法糖？ <code>vue2</code> 对原生组件究竟做了什么特殊处理？</li>\n<li>弄明白： <code>v-model</code> 到底是<strong>单向数据流</strong>还是<strong>数据双向绑定</strong>？</li>\n<li>弄明白： <code>v-model</code> 在语法糖之外的『副作用』？</li>\n<li>学会如何让你的组件也支持 <code>v-model</code> 语法。</li>\n</ul>\n<h2>一、<code>v-model</code> 的本质是语法糖。</h2>\n<blockquote>\n<p>『<code>v-model</code> 本质上不过是语法糖。它负责监听用户的输入事件以更新数据，并对一些极端场景进行一些特殊处理。』 – 官方文档</p>\n</blockquote>\n<p>什么是语法糖？</p>\n<blockquote>\n<p>语法糖，简单来说就是『便捷写法』。</p>\n</blockquote>\n<p>在大部分情况下， <code>v-model=&quot;foo&quot;</code> 等价于 <code>:value=&quot;foo&quot;</code> 加上 <code>@input=&quot;foo = $event&quot;</code>；</p>\n<pre class=\"prettyprint language-html\"><code>&lt;!-- 在大部分情况下，以下两种写法是等价的 --&gt;\n&lt;el-input v-model=&quot;foo&quot; &#x2F;&gt;\n\n&lt;el-input :value=&quot;foo&quot; @input=&quot;foo = $event&quot; &#x2F;&gt;\n</code></pre><p>没错，在<strong>大部分情况下</strong>如此。<br>\n但也有例外：</p>\n<ol>\n<li>\n<p><code>vue2</code> 给组件提供了 <code>model</code> 属性，可以让用户自定义<strong>传值的prop名</strong>和<strong>更新值的事件名</strong>。这个暂且略过，第四节会细说。</p>\n</li>\n<li>\n<p>对于原生 <code>html</code> 原生元素，<code>vue</code> 干了大量『脏活儿』，目的是为了能让我们忽视 <code>html</code> 在api上的差异性。以下元素的左右两种写法是等价的：</p>\n</li>\n</ol>\n<ul>\n<li><code>textarea</code> 元素：<br>\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2f51a6c8b3c94a928a81b453789921f2~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"textarea\"></li>\n<li><code>select</code> 下拉框：\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a7e13f5ebee470998e443ed99a5a03f~tplv-k3u1fbpfcp-zoom-1.image\" alt></li>\n<li><code>input type='radio'</code> 单选框：\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/13e59496402e49e5b09dec4a5b5dae87~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"radio\"></li>\n<li><code>input type='checkbox'</code> 多选框：\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5bf128dd051e46fc884899566646fe6b~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"checkbox\"></li>\n</ul>\n<p>在编程思想上，这种帮助使用者『隐藏细节』的方式叫<strong>封装</strong>。</p>\n<h2>二、v-model 仅仅是语法糖吗？(冷知识)</h2>\n<blockquote>\n<p><code>v-model</code> 不仅仅是语法糖，它还有副作用。</p>\n</blockquote>\n<p>副作用如下：<strong>如果 <code>v-model</code> 绑定的是响应式对象上某个不存在的属性，那么 <code>vue</code> 会悄悄地增加这个属性，并让它响应式。</strong></p>\n<p>举个例子，看下面的代码：</p>\n<pre class=\"prettyprint language-javascript\"><code>&#x2F;&#x2F; template中：\n&lt;el-input v-model=&quot;user.tel&quot;&gt;&lt;&#x2F;el-input&gt;\n&#x2F;&#x2F; script中：\nexport default {\n  data() {\n    return {\n      user: {\n        name: &#x27;公众号: 前端要摸鱼&#x27;,\n      }\n    }\n  }\n}\n</code></pre><p>响应式数据中没有定义 <code>user.tel</code> 属性，但是 <code>template</code> 里却用 <code>v-model</code> 绑定了 <code>user.tel</code>，猜一猜当你输入时会发生什么？</p>\n<p>看效果：<br>\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd32fbc4b8274d4782b1abf8b31022a6~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"副作用\"></p>\n<p>揭晓答案吧：\n<code>user</code> 上会新增 <code>tel</code> 属性，并且 <code>tel</code> 这个属性还是<strong>响应式</strong>的。</p>\n<p>这就是『副作用』带来的效果，你学会了吗？</p>\n<h2>三、 <code>v-model</code> 是双向绑定还是单向数据流？</h2>\n<h3>2.1 <code>v-model</code> 是双向绑定吗？</h3>\n<blockquote>\n<p>是，官方说是。</p>\n</blockquote>\n<p>『你可以用 v-model 指令在表单 <code>&lt;input&gt;</code>、<code>&lt;textarea&gt;</code> 及 <code>&lt;select&gt;</code> 元素上创建双向数据绑定。』 —— vue2官方文档</p>\n<h3>2.2 那 <code>v-model</code> 是单向数据流吗？</h3>\n<blockquote>\n<p>是的，它甚至是单向数据流的典型范式。</p>\n</blockquote>\n<p>虽然官方没有明确表示这点，但我们可以捋一捋两者的关系。</p>\n<ul>\n<li>什么是单项数据流？<br>\n子组件不能改变父组件传递给它的 <code>prop</code> 属性，推荐的做法是它抛出事件，通知父组件自行改变绑定的值。</li>\n<li><code>v-model</code> 的做法是怎样的？<br>\n<code>v-model</code> 做法完全符合单项数据流。甚至于，它给出了一种在命名和事件定义上的规范。</li>\n</ul>\n<p>众所周知 <code>.sync</code> 修饰符是单向数据流的另一个典型范式。\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fb7cb9ef059347f6b8ed76c97830230e~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"单向数据流8字真诀\"></p>\n<p>『单向数据流』总结起来其实也就8个字：『数据向下，事件向上』。</p>\n<h2>四、如何让你开发的组件支持 <code>v-model</code></h2>\n<blockquote>\n<p>虽然不想说，但这确实是高频面试题。🤣</p>\n</blockquote>\n<p>在定义 <code>vue</code> 组件时，你可以提供一个 <code>model</code> 属性，用来定义该组件以何种方式支持 <code>v-model</code>。</p>\n<p><code>model</code> 属性本身是有默认值的，如下：</p>\n<pre class=\"prettyprint language-javascript\"><code>&#x2F;&#x2F; 默认的 model 属性\nexport default {\n  model: {\n    prop: &#x27;value&#x27;,\n    event: &#x27;input&#x27;\n  }\n}\n</code></pre><p>也就是说，如果你不定义 <code>model</code> 属性，或者你按照当面方法定义属性，当其他人使用你的自定义组件时，<code>v-model=&quot;foo&quot;</code> 就完全等价于 <code>:value=&quot;foo&quot;</code> 加上 <code>@input=&quot;foo = $event&quot;</code>。</p>\n<p>如果把 <code>model</code> 属性进行一些改装，如下：</p>\n<pre class=\"prettyprint language-javascript\"><code>&#x2F;&#x2F; 默认的 model 属性\nexport default {\n  model: {\n    prop: &#x27;ame&#x27;,\n    event: &#x27;zard&#x27;\n  }\n}\n</code></pre><p>那么，<code>v-model=&quot;foo&quot;</code> 就等价于 <code>:ame=&quot;foo&quot;</code> 加上 <code>@zard=&quot;foo = $event&quot;</code>。</p>\n<p>没错，就是这么容易，让我们看个例子。</p>\n<p>先定义一个自定义组件：</p>\n<pre class=\"prettyprint language-javascript\"><code>&lt;template&gt;\n&lt;div&gt;\n  我们是TI{{ ame }}冠军\n  &lt;el-button @click=&quot;playDota2(1)&quot;&gt;加&lt;&#x2F;el-button&gt;\n  &lt;el-button @click=&quot;playDota2(-1)&quot;&gt;减&lt;&#x2F;el-button&gt;\n&lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\nexport default {\n  props: {\n    ame: {\n      type: Number,\n      default: 8\n    }\n  },\n  model: { &#x2F;&#x2F; 自定义v-model的格式\n    prop: &#x27;ame&#x27;, &#x2F;&#x2F; 代表 v-model 绑定的prop名\n    event: &#x27;zard&#x27; &#x2F;&#x2F; 代码 v-model 通知父组件更新属性的事件名\n  },\n  methods: {\n    playDota2(step) {\n      const newYear = this.ame + step\n      this.$emit(&#x27;zard&#x27;, newYear)\n    }\n  }\n}\n&lt;&#x2F;script&gt;\n</code></pre><p>然后我们在父组件中使用该组件：</p>\n<pre class=\"prettyprint language-javascript\"><code>&#x2F;&#x2F; template中\n&lt;dota v-model=&quot;ti&quot;&gt;&lt;&#x2F;dota&gt;\n&#x2F;&#x2F; script中\nexport default {\n  data() {\n    return {\n      ti: 8\n    }\n  }\n}\n</code></pre><p>看看效果：\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bed10463fbab45f6b3baad5467e9170b~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"我们是TI冠军\"></p>\n<p>让你的组件支持 <code>v-model</code> 就这么容易。</p>\n<h2>五、demo和源码</h2>\n<p>获取源码请访问github <a href=\"https://github.com/zhangshichun/blog-vue2-demos/tree/master/src/views/about-v-model\">zhangshichun的github</a></p>\n<h2>六、结束</h2>\n<p>我是<code>春哥</code>。<br>\n我热爱 <code>vue.js</code> , <code>ElementUI</code> , <code>Element Plus</code> 相关技术栈，我的目标是给大家分享最实用、最有用的知识点，希望大家都可以早早下班，并可以飞速完成工作，淡定摸鱼🐟。</p>\n<p>你可以在<strong>掘金</strong>关注我，也可以在<strong>公众号</strong>里找到我：<code>前端要摸鱼</code>。<br>\n希望大家玩得开心。</p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: 春哥的梦想是摸鱼\n        原文链接：<a href=\"https://juejin.im/post/7049135444310622245\">https://juejin.im/post/7049135444310622245</a>\n      </p></div>","title":"面试官：你真的了解 v-model 吗？(vue2)__Vue.js__前端","last_reply_at":"2022-06-24T03:02:03.938Z","good":false,"top":false,"reply_count":2,"visit_count":7596,"create_at":"2022-01-18T08:15:00.950Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"6286edb0cd48c10031428a9f","author_id":"6286ec463046490032bbe9c2","tab":"ask","content":"<div class=\"markdown-text\"><h2>問題:</h2>\n<pre class=\"prettyprint\"><code>new Promise((resolve) =&gt; {\n    console.log(1)\n    resolve()\n}).then(async () =&gt; {\n    console.log(2)\n}).then(async () =&gt; {\n    console.log(3)\n})\nnew Promise((resolve) =&gt; {\n    console.log(&#x27;a&#x27;)\n    resolve()\n}).then(() =&gt; {\n    console.log(&#x27;b&#x27;)\n}).then(() =&gt; {\n    console.log(&#x27;c&#x27;)\n}).then(() =&gt; {\n    console.log(&#x27;d&#x27;)\n}).then(() =&gt; {\n    console.log(&#x27;e&#x27;)\n})\n</code></pre><h2>正解:</h2>\n<p>1\na\n2\nb\nc\nd\n3\ne</p>\n<blockquote>\n<p>求問思路，我自己的答案應該是 1 a 2 b 3 c d e，就是 then 之後的我覺得都會放入 microtask</p>\n</blockquote>\n<p><strong>爬了很多篇文作了以下筆記，但還是不知道這題為何這樣?</strong></p>\n<ul>\n<li>已知的是 async 如果沒有接 await 還是會回傳 Promise 物件</li>\n<li>.then 中沒有回傳值的話，還是會回傳 Promise 物件 return new Promise.resolve()</li>\n<li>.then 後的內容會被放入 microtask</li>\n<li>會先在 task 處理完同步的，再以先進先出的方式將 microtask 任務取出執行</li>\n<li>Promise 的狀態只能改一次改成 resolved 或者 rejected，前者可執行 then 後者 catch</li>\n<li>Promise 在 resolve/reject 前的內容是同步的</li>\n<li>本題貌似沒有出現會被放入 macrotask 的</li>\n</ul>\n</div>","title":"JavaScript 中 Promise 高階面試題求問","last_reply_at":"2022-05-20T01:24:00.350Z","good":false,"top":false,"reply_count":0,"visit_count":12812,"create_at":"2022-05-20T01:24:00.350Z","author":{"loginname":"BoisonChang","avatar_url":"https://avatars.githubusercontent.com/u/26113547?v=4&s=120"}},{"id":"6226ae6ecd48c10031428a78","author_id":"6226ab7fcd48c10031428a77","tab":"ask","content":"<div class=\"markdown-text\"><p>官方给到实例是vue2的\n<img src=\"https://static.vue-js.com/0456f0f0-9e7d-11ec-b69d-715259609f68.png\" alt=\"企业微信截图_16467020782042.png\">\n在vue3里要怎么用啊 <img src=\"https://static.vue-js.com/513f5b50-9e7d-11ec-b69d-715259609f68.png\" alt=\"企业微信截图_16467022157160.png\">\n我把它放到App.vue里了打包不起作用</p>\n</div>","title":"vue3里怎么使用prerender-spa-plugin","last_reply_at":"2022-03-08T01:16:30.036Z","good":false,"top":false,"reply_count":0,"visit_count":11412,"create_at":"2022-03-08T01:16:30.036Z","author":{"loginname":"dragonnext","avatar_url":"https://avatars.githubusercontent.com/u/23499045?v=4&s=120"}},{"id":"6219adae3046490032bbe99a","author_id":"61d311d103eaf00040d48053","tab":"ask","content":"<div class=\"markdown-text\"><p>为什么我用token登陆不了？</p>\n</div>","title":"vue开源社区项目","last_reply_at":"2022-03-01T02:26:14.003Z","good":false,"top":false,"reply_count":1,"visit_count":5313,"create_at":"2022-02-26T04:33:50.573Z","author":{"loginname":"getPokemon","avatar_url":"https://avatars.githubusercontent.com/u/74648895?v=4&s=120"}},{"id":"58abbd21a9c1282817afc28d","author_id":"5873a573a9c1282817afbf7f","tab":"share","content":"<div class=\"markdown-text\"><h1>前言</h1>\n<p>初学vue时曾在网上搜索vue的实战项目源码，无奈大部分都是简单的demo，对于深究vue没有太大的帮助，剩下的一些大部分都是像音乐播放器之类的展示型项目，交互没有预期那么复杂。但我们实际在工作中，经常会遇到有购物车的项目，这类项目因为涉及到money，所以对逻辑严谨度要求高，页面之间交互复杂，又会伴随着登陆、注册、用户信息等等，常常会让我们很头疼。既然还没人用vue写过这样的项目，那不如我来写，开源出来对能看到的人也会有帮助。</p>\n<p>这种功能性的项目很实用但是往往也很枯燥，没有音乐播放器那么看起来绚丽，思来想去发现饿了么是一个不错的素材，一来它足够复杂，开放的外卖平台比一般的公司独有商店更加复杂。二来 见到那么多美食，大家也不会感觉到厌烦。</p>\n<p>为啥是饿了么，而不是百度，美团？原因很简单，三个外卖大佬里，饿了么的色调和布局是最漂亮的，看起来最舒服。</p>\n<p>此项目大大小小共 45 个页面，涉及注册、登陆、商品展示、购物车、下单等等，是一个完整的流程。一般公司即便是官网的单页面项目都没这么复杂，如果这个项目能驾驭的了，相信大部分公司的其他单页面应用也就不在话下，即便更复杂，也不会比这个高到哪里去。</p>\n<p>因为利用业余时间来做，年前就开始写，又跨个年，周期有点长，项目从零布局到完成共用了2个多月的时间，目前项目已经完成，正在进行一些性能的优化，增加详细的注释。</p>\n<p>另外，这个项目和慕课网视频的那个饿了么没有任何关系，慕课网的项目只有一个页面，我在看完vue的官方文档后直接写了这个项目，没有参照任何人的代码，请大家不要混为一谈。</p>\n<p><strong>注：此项目纯属个人瞎搞，正常下单请选择饿了么官方客户端。</strong></p>\n<h1>源码地址：</h1>\n<p><a href=\"https://github.com/bailicangdu/vue2-elm\">https://github.com/bailicangdu/vue2-elm</a></p>\n<h2>技术栈</h2>\n<p>vue2 + vuex + vue-router + webpack + ES6/7 + fetch + sass + flex + svg</p>\n<h1>项目运行</h1>\n<h4>注意：由于涉及大量的 ES6/7 等新属性，nodejs 必须是 6.0 以上版本 ，node 7 是测试版，有可能会出问题，建议使用 node 6 稳定版 😱</h4>\n<pre class=\"prettyprint\"><code>git clone https:&#x2F;&#x2F;github.com&#x2F;bailicangdu&#x2F;vue2-elm.git  \n\ncd vue2-elm\n\nnpm install\n\n</code></pre><h3>编译环境</h3>\n<pre class=\"prettyprint\"><code>npm run dev\n\n访问 http:&#x2F;&#x2F;localhost:8088\n</code></pre><h3>线上版本</h3>\n<pre class=\"prettyprint\"><code>npm run build\n\n生成的elm文件夹放在服务器即可正常访问\n</code></pre><h1>说明</h1>\n<blockquote>\n<p>本项目主要用于熟悉如何用 vue2 架构一个大型项目</p>\n</blockquote>\n<blockquote>\n<p>如果对您有帮助，您可以点右上角 “Star” 支持一下 谢谢！ ^_^</p>\n</blockquote>\n<blockquote>\n<p>或者您可以 “follow” 一下，我会不断开源更多的有趣的项目</p>\n</blockquote>\n<blockquote>\n<p>开发环境 macOS 10.12.3  Chrome 55</p>\n</blockquote>\n<blockquote>\n<p>特别感谢辰妹子，在百忙之中抽出时间和我一起完成了这个项目，辛苦了🌹</p>\n</blockquote>\n<blockquote>\n<p>如有问题请直接在 Issues 中提，或者您发现问题并有非常好的解决方案，欢迎 PR 👍</p>\n</blockquote>\n<blockquote>\n<p>推荐一个 react + redux 开源项目，对react感兴趣的朋友赶紧去看看。<a href=\"https://github.com/bailicangdu/react-pxq\">地址在这里</a></p>\n</blockquote>\n<blockquote>\n<p>另外一个 vue2 + vuex 的入门项目，比当前的项目简单很多，非常适合入门练习。<a href=\"https://github.com/bailicangdu/vue2-happyfri\">地址在这里</a></p>\n</blockquote>\n<h1>关于 demo 与 数据 的说明🤔</h1>\n<p>1、下载代码运行后，因为开启了反向代理，可以获取真实的官方数据，最终可以进行下单(真实的下单，而不是模拟，下单后可以在官方App中查看并付款，亲自试过，且成功付款点餐)，但是为了安全起见，登陆的帐号为固定的帐号，以免泄露个人信息，不过照样可以点餐。</p>\n<p>2、demo的数据为模拟的固定数据，只做为效果演示，因为反向代理必须在PC端运行代码才行。</p>\n<h2>效果演示</h2>\n<h4>(demo使用的是模拟数据，数据是固定的，只做为样式的演示，要获取真实的数据，请clone代码并运行);</h4>\n<p><a href=\"https://cangdu.org/elm/\">查看demo请戳这里</a>（请用chrome手机模式预览）</p>\n<h4>移动端扫描下方二维码</h4>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/elm_ewm.png\" alt></p>\n<h1>目标功能</h1>\n<ul>\n<li>[x] 定位功能 – 完成</li>\n<li>[x] 选择城市 – 完成</li>\n<li>[x] 搜索地址 – 完成</li>\n<li>[x] 展示所选地址附近商家列表 – 完成</li>\n<li>[x] 搜索美食，餐馆 – 完成</li>\n<li>[x] 根据距离、销量、评分、特色菜、配送方式等进行排序和筛选 – 完成</li>\n<li>[x] 餐馆食品列表页 – 完成</li>\n<li>[x] 购物车功能 – 完成</li>\n<li>[x] 店铺评价页面 – 完成</li>\n<li>[x] 单个食品详情页面 – 完成</li>\n<li>[x] 商家详情页 – 完成</li>\n<li>[x] 登陆、注册 – 完成</li>\n<li>[x] 修改密码 – 完成</li>\n<li>[x] 个人中心 – 完成</li>\n<li>[x] 发送短信、语音验证 – 完成</li>\n<li>[x] 下单功能 – 完成 ✨✨🎉🎉</li>\n<li>[x] 订单列表 – 完成</li>\n<li>[x] 订单详情 – 完成</li>\n<li>[x] 下载App – 完成</li>\n<li>[x] 添加、删除、修改收货地址 – 完成</li>\n<li>[x] 帐户信息 – 完成</li>\n<li>[x] 服务中心 – 完成</li>\n<li>[x] 红包 – 完成</li>\n<li>[x] 上传头像 – 完成\n</br>\n<s>付款 – 臣妾做不到啊</s></li>\n</ul>\n<h1>总结</h1>\n<p>1、因为并不是elm官方，而且因为要开代理，必须在pc端打开，最多只能做到下单这一步，下单成功后可以在手机客户端查看并付款。</p>\n<p>2、一般涉及到money的网页逻辑都比较复杂，尤其像饿了么这样一个开放的平台，商家和食品种类繁多，页面与页面之间交互复杂，在写到 购物车 和 下单 功能时众多的数据和逻辑一度让人很头疼，又没有设计和接口api文档，只能一步步摸索。</p>\n<p>3、vue因其轻量级的框架在中小型项目中表现亮眼，在大型单页面应用中因为vuex的存在，表现依然出色，在处理复杂交互逻辑的时候，vuex的存在是不可或缺的。所以说利用 vue + vuex 完全可以去做大型的单页面项目。</p>\n<p>4、项目写到现在，从 登陆注册到、首页、搜索、商家列表、购物车、下单、订单列表、个人中心 一个流程走完之后、不但对vue的理解更深一层，而且对以后掌控大型项目的时候也有非常多的帮助，做一个实际的项目才能对自己有很大的提升。</p>\n<p>5、曾一度怀疑，花几个月的时间做这样一个项目到底有没有意义，本来只是想做一个小项目练练手，没想到后来越写越多，不过坚持下来后我相信一切都是值得的。</p>\n<p>6、项目已经完成，共45个页面。</p>\n<h1>最终目标</h1>\n<p>1、用node.js构建一个模拟外卖平台的后台系统。(已经开始制作)</p>\n<p>2、利用 react-native 写出跨 Android 和 IOS 的原生APP版本。</p>\n<p>3、如果时间来的及，会出一个pc端的网页版。</p>\n<p>所以我的目的是构建一个横跨前后端，移动IOS、Android的完整生态圈。</p>\n<p>。。。敬请期待</p>\n<h1>部分截图</h1>\n<h2>商铺列表页</h2>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/msite.png\" width=\"365\" height=\"619\" /> <img src=\"http://test.fe.ptdev.cn/elm/screenshots/msite.gif\" width=\"365\" height=\"619\" /></p>\n<h2>商铺筛选页</h2>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/food.png\" width=\"365\" height=\"619\" /> <img src=\"http://test.fe.ptdev.cn/elm/screenshots/food.gif\" width=\"365\" height=\"619\" /></p>\n<h2>搜索页</h2>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/search.png\" width=\"365\" height=\"619\" /> <img src=\"http://test.fe.ptdev.cn/elm/screenshots/search.gif\" width=\"365\" height=\"619\" /></p>\n<h2>餐馆食品列表与购物车</h2>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/shop_cart.png\" width=\"365\" height=\"619\" /> <img src=\"http://test.fe.ptdev.cn/elm/screenshots/shop_cart.gif\" width=\"365\" height=\"619\" /></p>\n<h2>登陆页</h2>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/login1.png\" width=\"365\" height=\"619\" /> <img src=\"http://test.fe.ptdev.cn/elm/screenshots/login.gif\" width=\"365\" height=\"619\" /></p>\n<h2>个人中心</h2>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/profile.png\" width=\"365\" height=\"619\" /> <img src=\"http://test.fe.ptdev.cn/elm/screenshots/profile.gif\" width=\"365\" height=\"619\" /></p>\n<h2>确认订单页</h2>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/confirm1.png\" width=\"365\" height=\"619\" /> <img src=\"http://test.fe.ptdev.cn/elm/screenshots/confrimOrder.gif\" width=\"365\" height=\"619\" /></p>\n<h2>订单列表页</h2>\n<p><img src=\"http://test.fe.ptdev.cn/elm/screenshots/order.png\" width=\"365\" height=\"619\" /> <img src=\"http://test.fe.ptdev.cn/elm/screenshots/order.gif\" width=\"365\" height=\"619\" /></p>\n<h1>项目布局</h1>\n<pre class=\"prettyprint\"><code>|-- build                            &#x2F;&#x2F; webpack配置文件\n|-- config                           &#x2F;&#x2F; 项目打包路径\n|-- elm                           \t &#x2F;&#x2F; 上线项目文件，放在服务器即可正常访问\n|-- screenshots                      &#x2F;&#x2F; 项目截图\n|-- src                              &#x2F;&#x2F; 源码目录\n|   |-- components                   &#x2F;&#x2F; 组件\n|       |-- common                   &#x2F;&#x2F; 公共组件\n|\t\t\t|-- buyCart.js           &#x2F;&#x2F; 购物车组件\n|\t\t\t|-- loading.js           &#x2F;&#x2F; 页面初始化加载数据的动画组件\n|\t\t\t|-- mixin.js             &#x2F;&#x2F; 组件混合(包括：指令-下拉加载更多，处理图片地址)\n|\t\t\t|-- ratingStar.js        &#x2F;&#x2F; 评论的五颗星组件\n|\t\t\t|-- shoplist.js          &#x2F;&#x2F; msite和shop页面的餐馆列表公共组件\n|       |-- footer                   &#x2F;&#x2F; 底部公共组件\n|       |-- header                 \t &#x2F;&#x2F; 头部公共组件\n|   |-- config                       &#x2F;&#x2F; 基本配置\n|       |-- env.js                   &#x2F;&#x2F; 环境切换配置\n|       |-- fetch.js                 &#x2F;&#x2F; 获取数据\n|       |-- mUtils.js                &#x2F;&#x2F; 常用的js方法\n|       |-- rem.js                   &#x2F;&#x2F; px转换rem\n|   |-- images                       &#x2F;&#x2F; 公共图片\n|   |-- pages                        &#x2F;&#x2F; 页面组件\n|       |-- city                     &#x2F;&#x2F; 当前城市页\n|\t\t|-- food                 \t &#x2F;&#x2F; 食品筛选排序页\n|\t\t|-- confirmOrder             &#x2F;&#x2F; 确认订单页\n|\t\t  |--children\n|\t\t\t|--invoice\t\t\t     &#x2F;&#x2F;\t选择发票页\n|\t\t\t|--remark\t\t\t     &#x2F;&#x2F;\t订单备注页\n|\t\t\t|--payment\t\t\t     &#x2F;&#x2F;\t付款页\n|\t\t\t|--userValidation\t\t &#x2F;&#x2F;\t用户验证页\n|\t\t\t|--chooseAddress         &#x2F;&#x2F;\t选择地址页\n|\t\t      |--children\n|\t\t\t\t|--addAddress        &#x2F;&#x2F;\t添加地址页\n|\t\t\t\t  |--children\n|\t\t\t\t\t|--searchAddress &#x2F;&#x2F; 搜索地址页\n|       |-- find                     &#x2F;&#x2F; 发现页\n|       |-- forget                   &#x2F;&#x2F; 忘记密码，修改密码页\n|       |-- home                     &#x2F;&#x2F; 首页\n|       |-- login                    &#x2F;&#x2F; 登陆注册页\n|       |-- msite                    &#x2F;&#x2F; 商铺列表页\n|       |-- order                    &#x2F;&#x2F; 订单列表页\n|\t\t\t|--children\n|\t\t\t\t|--orderDetail\t\t &#x2F;&#x2F; 订单详情页\n|       |-- profile                  &#x2F;&#x2F; 个人中心\n|\t\t\t|--children\n|\t\t\t\t|--balance\t\t\t &#x2F;&#x2F; 我的余额\n|\t\t\t\t|--benefit\t\t\t &#x2F;&#x2F; 我的优惠\n|\t\t\t\t|--info\t\t\t\t &#x2F;&#x2F; 帐户信息\n|\t\t\t\t|--points\t\t\t &#x2F;&#x2F; 我的积分\n|\t\t\t\t|--service\t\t\t &#x2F;&#x2F; 服务中心\n|       |-- search                   &#x2F;&#x2F; 搜索页\n|       |-- shop                     &#x2F;&#x2F; 商铺筛选页\n|\t\t\t|-- children             \n|\t\t\t  \t|-- foodDetail       &#x2F;&#x2F; 商铺信息页     \n|\t\t\t  \t|-- shopDetail       &#x2F;&#x2F; 单个商铺信息页\n|\t\t\t\t\t|-- children             \n|\t\t\t\t  \t\t|-- shopSafe &#x2F;&#x2F; 商铺认证信息页     \n|       |-- vipcard                  &#x2F;&#x2F; vip办理页\n|\n|   |-- plugins                      &#x2F;&#x2F; 引用的插件\n|\n|   |-- router                       &#x2F;&#x2F; 路由配置\n|\n|   |-- service                      &#x2F;&#x2F; 数据交互统一调配\n|\t\t|-- template                 &#x2F;&#x2F; 开发阶段的临时数据\n|\t\t|-- getData.js               &#x2F;&#x2F; 获取数据的统一调配文件，对接口进行统一管理\n|\n|   |-- store                        &#x2F;&#x2F; vuex的状态管理\n|       |-- modules                  &#x2F;&#x2F; store模块\n|       |-- action.js                &#x2F;&#x2F; 配置actions\n|       |-- getters.js               &#x2F;&#x2F; 配置getters\n|       |-- index.js                 &#x2F;&#x2F; 引用vuex，创建store\n|       |-- mutation-types.js        &#x2F;&#x2F; 定义常量muations名\n|       |-- mutations.js             &#x2F;&#x2F; 配置mutations\n|\n|   |-- style                        &#x2F;&#x2F; 各种样式文件\n|       |-- common.scss              &#x2F;&#x2F; 公共样式文件\n|       |-- mixin.scss               &#x2F;&#x2F; 样式配置文件\n|\n|   |-- App.vue                      &#x2F;&#x2F; 页面入口文件\n|\n|   |-- main.js                      &#x2F;&#x2F; 程序入口文件，加载各种公共组件\n|\n|-- .babelrc                         &#x2F;&#x2F; ES6语法编译配置\n|-- .editorconfig                    &#x2F;&#x2F; 代码编写规格\n|-- .gitignore                       &#x2F;&#x2F; 忽略的文件\n|-- favicon.ico                      &#x2F;&#x2F; 页面左上角小图标\n|-- index.html                       &#x2F;&#x2F; 入口html文件\n|-- package.json                     &#x2F;&#x2F; 项目及工具的依赖配置文件\n|-- README.md                        &#x2F;&#x2F; 说明\n</code></pre></div>","title":"基于 vue2 + vuex 构建一个具有 45 个页面的大型单页面应用","last_reply_at":"2022-02-15T03:05:53.201Z","good":true,"top":false,"reply_count":121,"visit_count":89191,"create_at":"2017-02-21T04:08:01.333Z","author":{"loginname":"bailicangdu","avatar_url":"https://avatars2.githubusercontent.com/u/20297227?v=4&s=120"}},{"id":"61e668f4ad745b003c9df796","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><hr>\n<h2>highlight: darcula\ntheme: smartblue</h2>\n<h2>1. 前言</h2>\n<blockquote>\n<p>大家好，我是<a href=\"https://lxchuan12.gitee.io\">若川</a>。<a href=\"https://rank.juejin.cn/rank/2021/1415826704971918\">正在参加掘金年度人气作者活动，可以点此帮我投票</a>。<strong>为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学</strong>。我倾力组织了<a href=\"https://www.yuque.com/ruochuan12/topics/1\">源码共读活动</a>，感兴趣的可以加我微信 <a href=\"https://juejin.cn/pin/7005372623400435725\">ruochuan12</a> 参与。每周大家一起学习200行左右的源码，已进行5个月。</p>\n</blockquote>\n<p>想学源码，极力推荐关注我写的专栏（目前2K人关注）<a href=\"https://juejin.cn/column/6960551178908205093\">《学习源码整体架构系列》</a> 包含<code>jQuery</code>、<code>underscore</code>、<code>lodash</code>、<code>vuex</code>、<code>sentry</code>、<code>axios</code>、<code>redux</code>、<code>koa</code>、<code>vue-devtools</code>、<code>vuex4</code>、<code>koa-compose</code>、<code>vue 3.2 发布</code>、<code>vue-this</code>、<code>create-vue</code>、<code>玩具vite</code>等20余篇源码文章。</p>\n<p><a href=\"https://github.com/lxchuan12/dotenv-analysis.git\">本文仓库 https://github.com/lxchuan12/dotenv-analysis.git，求个star^_^</a></p>\n<p><a href=\"https://www.yuque.com/ruochuan12/topics/1\">源码共读活动</a> 每周一期，已进行到18期。于是搜寻各种值得我们学习，且代码行数不多的源码。<a href=\"https://github.com/motdotla/dotenv/blob/master/lib/main.js\">dotenv 主文件仅118行</a>，非常值得我们学习。</p>\n<p>阅读本文，你将学到：</p>\n<pre class=\"prettyprint language-bash\"><code>1. 学会 dotenv 原理和实现\n2. 学会使用 fs模块 获取文件并解析\n3. 等等\n</code></pre><h2>2. 环境准备</h2>\n<pre class=\"prettyprint language-bash\"><code># 推荐克隆我的项目，保证与文章同步\ngit clone https:&#x2F;&#x2F;github.com&#x2F;lxchuan12&#x2F;dotenv-analysis.git\n# npm i -g yarn\ncd dotenv-analysis&#x2F;dotenv &amp;&amp; yarn i\n# VSCode 直接打开当前项目\n# code .\n# 我写的例子都在 examples 这个文件夹中，可以启动服务本地查看调试\n# 在 dotenv-analysis 目录下\nnode examples&#x2F;index.js\n\n# 或者克隆官方项目\ngit clone https:&#x2F;&#x2F;github.com&#x2F;motdotla&#x2F;dotenv.git\n# npm i -g yarn\ncd dotenv &amp;&amp; yarn i\n# VSCode 直接打开当前项目\n# code .\n</code></pre><p>如果需要对源码进行调试，可以看我的这篇文章：<a href=\"https://juejin.cn/post/7030584939020042254\">新手向：前端程序员必学基本技能——调试JS代码</a>，这里就不再赘述了。</p>\n<h2>3. dotenv 的作用</h2>\n<p><a href=\"https://github.com/motdotla/dotenv\">dotenv</a></p>\n<p><code>Dotenv</code> 是一个零依赖模块，可将 <code>.env</code> 文件中的环境变量加载到 <code>process.env</code> 中。</p>\n<p>如果需要使用变量，则配合如下扩展包使用。</p>\n<p><a href=\"https://github.com/motdotla/dotenv-expand\">dotenv-expand</a></p>\n<p>众所周知，<code>.env</code> 文件在我们项目中非常常见，在 <code>vue-cli</code> 和 <code>create-react-app</code> 中都有使用。</p>\n<p><a href=\"https://cli.vuejs.org/zh/guide/mode-and-env.html#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F\">vue-cli .env</a></p>\n<p><a href=\"https://create-react-app.dev/docs/adding-custom-environment-variables/#what-other-env-files-can-be-used\">create-react-app .env</a></p>\n<h2>4. .env 文件使用</h2>\n<p>我们项目中经常会用到<code>.env 文件</code>写法：</p>\n<pre class=\"prettyprint language-bash\"><code>NAME=若川\nAGE=18\nBLOG=https:&#x2F;&#x2F;lxchuan12.gitee.io\nMP_WEIXIN=&#x27;若川视野&#x27;\nACTIVITY=每周一起学200行左右的源码共读活动\nWEIXIN=加我微信 ruochuan12 参与\n</code></pre><p>单从这个文件来看，我们可以知道有如下功能需要实现：</p>\n<ol>\n<li>读取 .env 文件</li>\n<li>解析 .env 文件拆成键值对的对象形式</li>\n<li>赋值到 process.env 上</li>\n<li>最后返回解析后得到的对象</li>\n</ol>\n<h2>5. 简单实现</h2>\n<p>根据分析问题，我们最终可以简单把代码实现如下：</p>\n<pre class=\"prettyprint language-js\"><code>const fs = require(&#x27;fs&#x27;);\nconst path = require(&#x27;path&#x27;);\n\nconst parse = function parse(src){\n    const obj = {};\n    &#x2F;&#x2F; 用换行符 分割\n    &#x2F;&#x2F; 比如\n    &#x2F;**\n     * NAME=若川\n     * AGE=18\n     * MP_WEIXIN=若川视野\n     * BLOG=https:&#x2F;&#x2F;lxchuan12.gitee.io\n     * ACTIVITY=每周一起学200行左右的源码共读活动\n     * WEIXIN=加我微信 ruochuan12 参与\n    *&#x2F;\n    src.toString().split(&#x27;&#x5C;n&#x27;).forEach(function(line, index){\n        &#x2F;&#x2F; 用等号分割\n        const keyValueArr = line.split(&#x27;=&#x27;);\n        &#x2F;&#x2F; NAME\n        key = keyValueArr[0];\n        &#x2F;&#x2F; 若川\n        val = keyValueArr[1] || &#x27;&#x27;;\n        obj[key] = val;\n    });\n    &#x2F;&#x2F; { NAME: &#x27;若川&#x27;, ... }\n    return obj;\n}\n\nconst config = function(){\n    &#x2F;&#x2F; 读取 node 执行的当前路径下的 .env 文件\n    let dotenvPath = path.resolve(process.cwd(), &#x27;.env&#x27;);\n    &#x2F;&#x2F; 按 utf-8 解析文件，得到对象\n    &#x2F;&#x2F; { NAME: &#x27;若川&#x27;, ... }\n    const parsed = parse(fs.readFileSync(dotenvPath, &#x27;utf-8&#x27;));\n\n    &#x2F;&#x2F; 键值对形式赋值到 process.env 变量上，原先存在的不赋值\n    Object.keys(parsed).forEach(function(key){\n        if(!Object.prototype.hasOwnProperty.call(process.env, key)){\n            process.env[key] = parsed[key];\n        }\n    });\n\n    &#x2F;&#x2F; 返回对象\n    return parsed;\n};\n\nconsole.log(config());\nconsole.log(process.env);\n\n&#x2F;&#x2F; 导出 config parse 函数\nmodule.exports.config = config;\nmodule.exports.parse = parse;\n</code></pre><h2>6. 继续完善 config 函数</h2>\n<p>简版的 config 函数还缺失挺多功能，比如：</p>\n<pre class=\"prettyprint\"><code>可由用户自定义路径\n可由用户自定义解析编码规则\n添加 debug 模式\n完善报错输出，用户写的 env 文件自由度比较大，所以需要容错机制。\n</code></pre><p>根据功能，我们很容易实现以下代码：</p>\n<pre class=\"prettyprint language-js\"><code>function resolveHome (envPath) {\n    return envPath[0] === &#x27;~&#x27; ? path.join(os.homedir(), envPath.slice(1)) : envPath\n}\n\nconst config = function(options){\n    &#x2F;&#x2F; 读取 node 执行的当前路径下的 .env 文件\n    let dotenvPath = path.resolve(process.cwd(), &#x27;.env&#x27;);\n    &#x2F;&#x2F; utf8\n    let encoding = &#x27;utf8&#x27;;\n    &#x2F;&#x2F; debug 模式，输出提示等信息\n    let debug = false;\n    &#x2F;&#x2F; 对象\n    if (options) {\n        if (options.path != null) {\n            &#x2F;&#x2F; 解析路径\n            dotenvPath = resolveHome(options.path)\n        }\n        &#x2F;&#x2F; 使用配置的编码方式\n        if (options.encoding != null) {\n            encoding = options.encoding\n        }\n        &#x2F;&#x2F; 有配置就设置为 true\n        if (options.debug != null) {\n            debug = true\n        }\n    }\n\n    try {\n        &#x2F;&#x2F; 按 utf-8 解析文件，得到对象\n        &#x2F;&#x2F; { NAME: &#x27;若川&#x27;, ... }\n        &#x2F;&#x2F; debug 传递给 parse 函数 便于\n        const parsed = parse(fs.readFileSync(dotenvPath, { encoding }), { debug });\n\n        &#x2F;&#x2F; 键值对形式赋值到 process.env 变量上，原先存在的不赋值\n        Object.keys(parsed).forEach(function(key){\n            if(!Object.prototype.hasOwnProperty.call(process.env, key)){\n                process.env[key] = parsed[key];\n            } else if (debug) {\n                console.log(&#96;&quot;${key}&quot; is already defined in &#x5C;&#96;process.env&#x5C;&#96; and will not be overwritten&#96;);\n            }\n        });\n\n        &#x2F;&#x2F; 返回对象\n        return parsed;\n    }\n    catch (e) {\n        return { error: e };\n    }\n};\n</code></pre><p><code>dotenv</code> 源码中，<code>parse</code> 函数主要是一些正则和单双引号、跨平台等细致处理。这里就暂时不阐述，读者朋友可以查看<a href=\"https://github.com/motdotla/dotenv/blob/master/lib/main.js\">dotenv 源码</a>。</p>\n<h2>7. 总结</h2>\n<p>鉴于文章不宜过长，文章只比较深入的分析了 <code>config</code> 函数。<code>parse</code> 函数目前没有深入分析。</p>\n<p>一句话总结 <code>dotenv</code> 库的原理。<strong>用 <code>fs.readFileSync</code> 读取 <code>.env</code> 文件，并解析文件为键值对形式的对象，将最终结果对象遍历赋值到 <code>process.env</code> 上</strong>。</p>\n<p>我们也可以不看 <code>dotenv</code> 源码，根据 <code>api</code> 倒推，自己来实现这样的功能。最终看看和  <code>dotenv</code> 源码本身有什么差别。这样也许更能锻炼自己。或者用 <code>ts</code> 重构它。</p>\n<p>本文同时也给我们启发：围绕工作常用的技术包和库值得深入学习，做到<strong>知其然，知其所以然</strong>。</p>\n<p>值得一提的是：<code>dotenv</code> 源码使用的是 <code>flow</code> 类型。vue2 源码也是用的 <code>flow</code>。vue3 源码改用 <code>ts</code>了。</p>\n<p>最后可以持续关注我@若川。欢迎加我微信 <a href=\"https://juejin.cn/pin/7005372623400435725\">ruochuan12</a> 交流，参与 <a href=\"https://juejin.cn/pin/7005372623400435725\">源码共读</a> 活动，每周大家一起学习200行左右的源码，共同进步。<p>\n版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n作者: 若川\n原文链接：<a href=\"https://juejin.im/post/7045057475845816357\">https://juejin.im/post/7045057475845816357</a>\n</p></p>\n</div>","title":"面试官：项目中常用的 .env 文件原理是什么？如何实现？__前端__JavaScript__Vue.js","last_reply_at":"2022-01-18T07:15:00.852Z","good":false,"top":false,"reply_count":0,"visit_count":13825,"create_at":"2022-01-18T07:15:00.852Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e65ae4ad745b003c9df794","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><ul>\n<li>小知识，大挑战！本文正在参与“<a href=\"https://juejin.cn/post/7008476801634680869\" title=\"https://juejin.cn/post/7008476801634680869\">程序员必备小知识</a>”创作活动。</li>\n</ul>\n<hr>\n<p>先来一个图镇楼！</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec1d1b52edb748a8bdf909aa4375cb5d~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<p>终于等到一个活动可以让我分享在去年定下的目标</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9883d795f5494760b4e82d9329eee6dd~tplv-k3u1fbpfcp-watermark.image?\" alt=\"0_82ac840b685208ae729eb6b958e22573.png\"></p>\n<p><strong>我要成为<code>Vue3</code>的<code>Contributor</code></strong></p>\n<p>然而，在定下目标60秒后，我就完成了它！</p>\n<p><strong>我成为了<code>Vue3</code>的<code>Contributor</code></strong></p>\n<p>我这个PR<strong>举世无双</strong>的水，说是<strong>世界第一水</strong>也不过分！</p>\n<p>但是不好意思啊，即便是如此水的一个PR，尤大也不得不Merge！</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1d5a092265834f08ba4acad6b4ab320d~tplv-k3u1fbpfcp-watermark.image?\" alt=\"54_91e2cee52fecd67ed6bf892f954400ed.gif\"></p>\n<p><strong>尤大亲手让我成为了<code>Vue3</code>的<code>Contributor</code></strong></p>\n<p>因为我确实修正了源码中一处错误！</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc1776b7ad9f496ba93eaed12e0b7130~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<p>哈哈哈，我甚至连那个TODO都没帮忙完成！</p>\n<p>我知道还有很多顶级项目都有这类问题，因为拼错单词是很常见的，手快了、脑抽了、晃神了都有可能拼错，这问题还不太好发现。因为错词的可读性依然很高。</p>\n<blockquote>\n<p>Aoccdrnig to a rscheearch at an Elingsh uinervtisy, it deosn’t mttaer in waht oredr the ltteers in a wrod are, the olny iprmoetnt tihng is taht frist and lsat ltteer is at the rghit pclae. The rset can be a toatl mses and you can sitll raed it wouthit porbelm. Tihs is bcuseae we do not raed ervey lteter by itslef but the wrod as a wlohe.</p>\n</blockquote>\n<p>对不对？</p>\n<p>那么只要找出开源项目中那些拼错的单词再改掉，岂不是各种顶级项目的<code>Contributor</code>标记拿到手软？</p>\n<p>下面我来教大家如何对开源项目进行单词拼写检查</p>\n<p><strong>把代码文件copy到Word里</strong></p>\n<p><strong>把代码文件copy到Word里</strong></p>\n<p><strong>把代码文件copy到Word里</strong></p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/da10ecc78c6447029fa7cf03e4cf2da3~tplv-k3u1fbpfcp-watermark.image?\" alt=\"3_a252147afe65161272c362aa89e3cdce.jpg\"></p>\n<p>就这的话我是不是要被大家的唾沫淹死？</p>\n<p>原理就是这么个简单的原理</p>\n<p>有没有帮我们搞效检查项目代码中拼写错误的仓库呢？</p>\n<p>有的！</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/401fcea35c9345818b0bb83a11d920ba~tplv-k3u1fbpfcp-watermark.image?\" alt=\"0_8501779.jpg\"></p>\n<p>请大家自行前往<code>Github</code>搜索<code>SpellCheck</code>\n或点击下方链接</p>\n<p><a href=\"https://github.com/search?q=spellcheck\">https://github.com/search?q=spellcheck</a></p>\n<p>One more times and Follow me 大声say!</p>\n<p><strong>大帅是<code>Vue3</code>的<code>Contributor</code></strong></p>\n<p>弱弱的问大家一句，尤大要是看到这篇文章，还有能力撤销这个Merge吗？</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ece20490be0b4a848050e25dc144799d~tplv-k3u1fbpfcp-watermark.image?\" alt=\"6_469436.jpg\"></p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: 大帅老猿\n        原文链接：<a href=\"https://juejin.im/post/7012449788255813669\">https://juejin.im/post/7012449788255813669</a>\n      </p></div>","title":"花60秒给Vue3提的PR，竟然被尤大亲自Merge了~__JavaScript__Vue.js__开源","last_reply_at":"2022-01-18T06:15:00.823Z","good":false,"top":false,"reply_count":0,"visit_count":7488,"create_at":"2022-01-18T06:15:00.823Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e64cd4ad745b003c9df792","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><h1>1. 前言</h1>\n<p>最近自己学习写了一个基于Vue3的组件库，感觉<code>有点意思</code>，这篇文章来记录一下我是怎么从0快速打造一个UI组件库的</p>\n<ul>\n<li>\n<p>😊先来个预览\n<img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/baf189fa7b324957a22966b30e99c7ca~tplv-k3u1fbpfcp-watermark.image?\" alt=\"jw-ui演示.gif\"></p>\n</li>\n<li>\n<p>😘附上访问地址<a href=\"https://coderyjw.github.io/jw-ui-website\">jw-ui</a></p>\n</li>\n</ul>\n<h1>2. 使用Vite搭建官网</h1>\n<p>Vite是尤雨溪开发的一种新型前端构建工具，具体介绍可以查看<a href=\"https://cn.vitejs.dev/\">官方文档</a></p>\n<h2>2.1 创建项目</h2>\n<h3>2.1.1. 全局安装vite（这里我装的时候是2.7.2）</h3>\n<pre class=\"prettyprint\"><code>$ yarn create vite@2.7.2\n</code></pre><h3>2.1.2. 构建一个vue模板(项目名可以改成自己的名字)</h3>\n<pre class=\"prettyprint language-shell\"><code>yarn create vite jw-ui --template vue\n</code></pre><h3>2.1.3. 装好之后按照提示逐步执行命令</h3>\n<pre class=\"prettyprint language-shell\"><code>cd jw-ui\nyarn\nyarn dev\n</code></pre><p>可以看到界面</p>\n<p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a98637e10a7249618b49965d220466c1~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<p><code>ps: 推荐的IDE和设置：VSCode + Volar</code></p>\n<h2>2.2 基本完成官网的搭建</h2>\n<h3>2.2.1. 下载vue-router</h3>\n<pre class=\"prettyprint language-shell\"><code>yarn add vue-router@4\n</code></pre><h3>2.2.2. 创建home首页与doc文档页 以及顶部导航栏</h3>\n<pre class=\"prettyprint language-html\"><code>&#x2F;* &#x2F;views&#x2F;home&#x2F;index.vue 首页*&#x2F;\n&lt;template&gt;\n    &lt;div&gt;\n        Home\n    &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n</code></pre><pre class=\"prettyprint\"><code>&#x2F;* &#x2F;views&#x2F;doc&#x2F;index.vue 文档页面 *&#x2F;\n&lt;template&gt;\n  &lt;div&gt;\n    Doc\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n</code></pre><pre class=\"prettyprint\"><code>&#x2F;* &#x2F;components&#x2F;Topnav.vue 顶部导航栏组件 *&#x2F;\n&lt;template&gt;\n  &lt;div class=&quot;topnav&quot;&gt;\n    &lt;router-link to=&quot;&#x2F;home&quot;&gt;首页&lt;&#x2F;router-link&gt;\n    &lt;router-link to=&quot;&#x2F;doc&quot;&gt;文档&lt;&#x2F;router-link&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n</code></pre><h3>2.2.3. 配置路由</h3>\n<p>创建路由配置文件</p>\n<pre class=\"prettyprint language-typescript\"><code>&#x2F;&#x2F; router&#x2F;index.ts \nimport { createRouter, createWebHashHistory } from &quot;vue-router&quot;;\nconst history = createWebHashHistory();\n\nconst router = createRouter({\n  history,\n  routes: [\n    { path: &quot;&#x2F;&quot;, redirect: &quot;&quot; },\n  ],\n});\n\nexport default router;\n</code></pre><p>在main.ts里导入，使得整个应用支持路由。</p>\n<pre class=\"prettyprint language-TYPESCRIPT\"><code>import { createApp } from &quot;vue&quot;;\nimport App from &quot;.&#x2F;App.vue&quot;;\nimport router from &quot;.&#x2F;router&quot;;\n\nconst app = createApp(App);\napp.use(router);\n\napp.mount(&quot;#app&quot;);\n\n</code></pre><p>修改App.vue</p>\n<pre class=\"prettyprint\"><code>&lt;template&gt;\n  &lt;Topnav &#x2F;&gt;\n  &lt;router-view &#x2F;&gt;\n&lt;&#x2F;template&gt;\n&lt;script setup&gt;\nimport Topnav from &quot;.&#x2F;components&#x2F;Topnav.vue&quot;;\n&lt;&#x2F;script&gt;\n</code></pre><p>到目前为止的效果</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/565b1b1621944fd2b9713255873bed19~tplv-k3u1fbpfcp-watermark.image?\" alt=\"jw-ui演示1.gif\"></p>\n<p>装饰一下顶部导航栏后的效果</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9083365473f7475580f28d796d63a193~tplv-k3u1fbpfcp-watermark.image?\" alt=\"jw-ui演示2.gif\"></p>\n<p>这里首页按照自己喜欢的来写CSS就好了，接下来讲一下文档页面。</p>\n<p>文档页需要一个侧边栏来切换不同组件的文档，这里我就举例做一个Button组件</p>\n<pre class=\"prettyprint language-html\"><code>&#x2F;&#x2F; doc&#x2F;index.vue\n&lt;template&gt;\n  &lt;div class=&quot;layout&quot;&gt;\n    &lt;div class=&quot;content&quot;&gt;\n      &lt;aside&gt;\n        &lt;router-link class=&quot;menu-item text-overflow&quot; to=&quot;&#x2F;doc&#x2F;button&quot;\n          &gt;Button 组件&lt;&#x2F;router-link\n        &gt;\n      &lt;&#x2F;aside&gt;\n      &lt;main style=&quot;padding-left: 302px&quot;&gt;\n        &lt;router-view &#x2F;&gt;\n      &lt;&#x2F;main&gt;\n    &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n</code></pre><pre class=\"prettyprint language-TYPESCRIPT\"><code>&#x2F;&#x2F; router&#x2F;index.ts 添加一个展示的button页面\nimport { createRouter, createWebHashHistory } from &quot;vue-router&quot;;\n\nimport Home from &quot;..&#x2F;views&#x2F;home&#x2F;index.vue&quot;;\nimport Doc from &quot;..&#x2F;views&#x2F;doc&#x2F;index.vue&quot;;\nimport ButtonDoc from &quot;..&#x2F;views&#x2F;doc&#x2F;button&#x2F;index.vue&quot;;\n\nconst history = createWebHashHistory();\n\nconst router = createRouter({\n  history,\n  routes: [\n    { path: &quot;&#x2F;&quot;, redirect: &quot;&#x2F;home&quot; },\n    { path: &quot;&#x2F;home&quot;, component: Home },\n    {\n      path: &quot;&#x2F;doc&quot;,\n      component: Doc,\n      children: [{ path: &quot;button&quot;, component: ButtonDoc }],\n    },\n  ],\n});\n\nexport default router;\n\n</code></pre><pre class=\"prettyprint language-TYPESCRIPT\"><code>&#x2F;&#x2F; &#x2F;views&#x2F;doc&#x2F;button&#x2F;index\n&lt;template&gt;\n  &lt;Button &#x2F;&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script setup&gt;\nimport Button from &#x27;..&#x2F;..&#x2F;..&#x2F;lib&#x2F;button&#x2F;index.vue&#x27;\n&lt;&#x2F;script&gt;\n\n&lt;style lang=&quot;scss&quot; scoped&gt;\n\n&lt;&#x2F;style&gt;\n\n</code></pre><p>展示效果\n<img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1c062c4386c34b2cab67db872eda3c8b~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<p>好了到这里官网总算是基本搭建完了，我们终于就可以愉快的在<code>src/lib/button/index.vue</code>文件里封装组件啦。（<strong>封装的组件都放在lib文件夹里，以后打包用</strong>）</p>\n<h1>3. 封装一个Button组件</h1>\n<p>下面附上我写的一个Button组件以及使用效果</p>\n<p><code>PS: 需要注意的一点是封装的样式一定要加</code><strong><code>自己独特的前缀</code></strong><code>我这里是</code> <strong><code>jw</code></strong> <code>以避免在项目中产生样式重叠</code></p>\n<pre class=\"prettyprint language-typescript\"><code>&lt;template&gt;\n  &lt;button class=&quot;jw-button&quot; :class=&quot;classes&quot;&gt;\n    &lt;span v-if=&quot;loading&quot; class=&quot;jw-loadingIndicator&quot;&gt;&lt;&#x2F;span&gt;\n    &lt;slot&gt; {{ theme }} &lt;&#x2F;slot&gt;\n  &lt;&#x2F;button&gt;\n&lt;&#x2F;template&gt;\n&lt;script setup lang=&quot;ts&quot;&gt;\nimport { computed } from &quot;vue&quot;;\nconst props = defineProps({\n  theme: {\n    type: String,\n    default: &quot;default&quot;,\n  },\n  dashed: {\n    type: Boolean,\n    default: false,\n  },\n  size: {\n    type: String,\n    default: &quot;default&quot;,\n  },\n  round: {\n    type: Boolean,\n    default: false,\n  },\n  disabled: {\n    type: Boolean,\n    default: false,\n  },\n  loading: {\n    type: Boolean,\n    default: false,\n  },\n});\n\nconst { theme, dashed, size, round, disabled } = props;\nconst classes = computed(() =&gt; {\n  return {\n    [&#96;jw-theme-${theme}&#96;]: theme,\n    [&#96;jw-theme-dashed&#96;]: dashed,\n    [&#96;jw-size-${size}&#96;]: size,\n    [&#96;is-round&#96;]: round,\n    [&#96;is-disabled&#96;]: disabled,\n  };\n});\n&lt;&#x2F;script&gt;\n\n&lt;script lang=&quot;ts&quot;&gt;\nexport default {\n  name: &quot;JwButton&quot;,\n};\n&lt;&#x2F;script&gt;\n\n&lt;style lang=&quot;scss&quot; scoped&gt;\n$h-default: 32px;\n$h-small: 20px;\n$h-large: 48px;\n$white: #fff;\n$default-color: #333;\n$primary-color: #36ad6a;\n$info-color: #4098fc;\n$success-color: #85ce61;\n$warning-color: #f0a020;\n$error-color: #d03050;\n$grey: grey;\n\n$default-border-color: #d9d9d9;\n\n$radius: 3px;\n$green: #18a058;\n\n.jw-button {\n  box-sizing: border-box;\n  height: $h-default;\n  background-color: #fff;\n  padding: 0 12px;\n  cursor: pointer;\n  display: inline-flex;\n  justify-content: center;\n  align-items: center;\n  white-space: nowrap;\n  border-radius: $radius;\n  box-shadow: 0 1px 0 fade-out(black, 0.95);\n  transition: all 250ms;\n  color: $default-color;\n  border: 1px solid $default-border-color;\n  user-select: none;\n\n  &amp;:focus {\n    outline: none;\n  }\n\n  &amp;::-moz-focus-inner {\n    border: 0;\n  }\n\n  &amp;.jw-size-large {\n    font-size: 24px;\n    height: $h-large;\n    padding: 0 16px;\n  }\n  &amp;.jw-size-small {\n    font-size: 12px;\n    height: $h-small;\n    padding: 0 8px;\n  }\n\n  &amp;.is-round.jw-size-default {\n    border-radius: calc($h-default &#x2F; 2);\n  }\n  &amp;.is-round.jw-size-large {\n    border-radius: calc($h-large &#x2F; 2);\n  }\n  &amp;.is-round.jw-size-small {\n    border-radius: calc($h-small &#x2F; 2);\n  }\n\n  &amp;.jw-theme-default {\n    &amp;:hover {\n      color: $green;\n      border-color: $green;\n\n      &gt; .jw-loadingIndicator {\n        border-style: dashed;\n        border-color: $green $green $green transparent;\n      }\n    }\n    &amp;:active {\n      color: darken($green, 20%);\n      border-color: darken($green, 20%);\n\n      &gt; .jw-loadingIndicator {\n        border-style: dashed;\n        border-color: darken($green, 20%) darken($green, 20%)\n          darken($green, 20%) transparent;\n      }\n    }\n    &amp;.jw-theme-dashed {\n      border-style: dashed;\n    }\n    &gt; .jw-loadingIndicator {\n      border-style: dashed;\n      border-color: $default-color $default-color $default-color transparent;\n    }\n  }\n  &amp;.jw-theme-primary {\n    background-color: $primary-color;\n    border-color: $primary-color;\n    color: $white;\n\n    &amp;:hover {\n      background: lighten($primary-color, 20%);\n      border-color: lighten($primary-color, 20%);\n    }\n    &amp;:active {\n      background-color: darken($primary-color, 20%);\n      border-color: darken($primary-color, 20%);\n    }\n\n    &amp;.is-disabled {\n      cursor: not-allowed;\n      background: lighten($primary-color, 20%);\n      border-color: lighten($primary-color, 20%);\n      &amp;:hover {\n        background: lighten($primary-color, 20%);\n        border-color: lighten($primary-color, 20%);\n      }\n    }\n\n    &amp;.jw-theme-dashed {\n      border-style: dashed;\n      background-color: $white !important;\n      color: $primary-color;\n\n      &gt; .jw-loadingIndicator {\n        border-style: dashed;\n        border-color: $primary-color $primary-color $primary-color transparent;\n      }\n    }\n  }\n\n  &amp;.jw-theme-info {\n    background-color: $info-color;\n    border-color: $info-color;\n    color: $white;\n    &amp;:hover {\n      background: lighten($info-color, 20%);\n      border-color: lighten($info-color, 20%);\n    }\n    &amp;:active {\n      background-color: darken($info-color, 20%);\n      border-color: darken($info-color, 20%);\n    }\n\n    &amp;.is-disabled {\n      cursor: not-allowed;\n      background: lighten($info-color, 20%);\n      border-color: lighten($info-color, 20%);\n      &amp;:hover {\n        background: lighten($info-color, 20%);\n        border-color: lighten($info-color, 20%);\n      }\n    }\n\n    &amp;.jw-theme-dashed {\n      border-style: dashed;\n      background-color: $white !important;\n      color: $info-color;\n\n      &gt; .jw-loadingIndicator {\n        border-style: dashed;\n        border-color: $info-color $info-color $info-color transparent;\n      }\n    }\n  }\n\n  &amp;.jw-theme-success {\n    background-color: $success-color;\n    border-color: $success-color;\n    color: $white;\n    &amp;:hover {\n      background: lighten($success-color, 20%);\n      border-color: lighten($success-color, 20%);\n    }\n    &amp;:active {\n      background-color: darken($success-color, 20%);\n      border-color: darken($success-color, 20%);\n    }\n\n    &amp;.is-disabled {\n      cursor: not-allowed;\n      background: lighten($success-color, 20%);\n      border-color: lighten($success-color, 20%);\n      &amp;:hover {\n        background: lighten($success-color, 20%);\n        border-color: lighten($success-color, 20%);\n      }\n    }\n\n    &amp;.jw-theme-dashed {\n      border-style: dashed;\n      background-color: $white !important;\n      color: $success-color;\n\n      &gt; .jw-loadingIndicator {\n        border-style: dashed;\n        border-color: $success-color $success-color $success-color transparent;\n      }\n    }\n  }\n\n  &amp;.jw-theme-warning {\n    background-color: $warning-color;\n    border-color: $warning-color;\n    color: $white;\n    &amp;:hover {\n      background: lighten($warning-color, 20%);\n      border-color: lighten($warning-color, 20%);\n    }\n    &amp;:active {\n      background-color: darken($warning-color, 20%);\n      border-color: darken($warning-color, 20%);\n    }\n\n    &amp;.is-disabled {\n      cursor: not-allowed;\n      background: lighten($warning-color, 20%);\n      border-color: lighten($warning-color, 20%);\n      &amp;:hover {\n        background: lighten($warning-color, 20%);\n        border-color: lighten($warning-color, 20%);\n      }\n    }\n\n    &amp;.jw-theme-dashed {\n      border-style: dashed;\n      background-color: $white !important;\n      color: $warning-color;\n\n      &gt; .jw-loadingIndicator {\n        border-style: dashed;\n        border-color: $warning-color $warning-color $warning-color transparent;\n      }\n    }\n  }\n\n  &amp;.jw-theme-error {\n    background-color: $error-color;\n    border-color: $error-color;\n    color: $white;\n    &amp;:hover {\n      background: lighten($error-color, 20%);\n      border-color: lighten($error-color, 20%);\n    }\n    &amp;:active {\n      background-color: darken($error-color, 20%);\n      border-color: darken($error-color, 20%);\n    }\n\n    &amp;.is-disabled {\n      cursor: not-allowed;\n      background: lighten($error-color, 20%);\n      border-color: lighten($error-color, 20%);\n      &amp;:hover {\n        background: lighten($error-color, 20%);\n        border-color: lighten($error-color, 20%);\n      }\n    }\n\n    &amp;.jw-theme-dashed {\n      border-style: dashed;\n      background-color: $white !important;\n      color: $error-color;\n\n      &gt; .jw-loadingIndicator {\n        border-style: dashed;\n        border-color: $error-color $error-color $error-color transparent;\n      }\n    }\n  }\n\n  &gt; .jw-loadingIndicator {\n    width: 14px;\n    height: 14px;\n    display: inline-block;\n    margin-right: 4px;\n    border-radius: 8px;\n    border-color: $white $white $white transparent;\n    border-style: solid;\n    border-width: 2px;\n    animation: jw-spin 1s infinite linear;\n  }\n}\n\n@keyframes jw-spin {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n&lt;&#x2F;style&gt;\n\n</code></pre><p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55721c1cee1c46cc9c453d7b2a791071~tplv-k3u1fbpfcp-watermark.image?\" alt=\"jw-ui演示3.gif\"></p>\n<p>虽然有不完美，但差不多就这个意思吧👀</p>\n<h1>4. 封装Markdown组件介绍文档</h1>\n<h2>4.1. 下载</h2>\n<p><code>vite-plugin-markdown</code>：一个插件可以让你导入Markdown文件作为各种格式的vite项目。</p>\n<p><code>github-markdown-css</code>：复制GitHub Markdown风格</p>\n<pre class=\"prettyprint language-shell\"><code>yarn add github-markdown-css vite-plugin-markdown\n</code></pre><h2>4.2. main.ts中引入</h2>\n<pre class=\"prettyprint language-TYPESCRIPT\"><code>import &quot;github-markdown-css&quot;;\n</code></pre><h2>4.3. vite.config.js中配置vite-plugin-markdown插件</h2>\n<pre class=\"prettyprint language-TYPESCRIPT\"><code>import { defineConfig } from &#x27;vite&#x27;\nimport vue from &#x27;@vitejs&#x2F;plugin-vue&#x27;\nconst md = require(&quot;vite-plugin-markdown&quot;);\n\nexport default defineConfig({\n  plugins: [vue(),\n    md.plugin({\n    mode: [&quot;html&quot;, &quot;vue&quot;],\n  }),]\n})\n\n</code></pre><h2>4.4. 封装Markdown组件</h2>\n<pre class=\"prettyprint language-TYPESCRIPT\"><code>&#x2F;&#x2F; &#x2F;components&#x2F;Markdown.vue\n&lt;template&gt;\n  &lt;article class=&quot;markdown-body&quot; v-html=&quot;content&quot;&gt;&lt;&#x2F;article&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script setup lang=&quot;ts&quot;&gt;\n&#x2F;&#x2F; 传入的md文件\nconst props = defineProps({\n  content: {\n    type: String,\n    required: true,\n  },\n});\n&lt;&#x2F;script&gt;\n\n</code></pre><h2>4.5. 创建介绍页面路由</h2>\n<pre class=\"prettyprint language-typescript\"><code>import { h } from &quot;vue&quot;;\nimport { createRouter, createWebHashHistory } from &quot;vue-router&quot;;\n\nimport Home from &quot;..&#x2F;views&#x2F;home&#x2F;index.vue&quot;;\nimport Doc from &quot;..&#x2F;views&#x2F;doc&#x2F;index.vue&quot;;\nimport ButtonDoc from &quot;..&#x2F;views&#x2F;doc&#x2F;button&#x2F;index.vue&quot;;\n\nconst history = createWebHashHistory();\n\nimport Markdown from &quot;..&#x2F;components&#x2F;Markdown.vue&quot;;\nconst md = (string) =&gt; h(Markdown, { content: string, key: string });\nimport { html as Intro } from &quot;..&#x2F;..&#x2F;markdown&#x2F;intro.md&quot;;\nconst IntroDoc = md(Intro);\n\nconst router = createRouter({\n  history,\n  routes: [\n    { path: &quot;&#x2F;&quot;, redirect: &quot;&#x2F;home&quot; },\n    { path: &quot;&#x2F;home&quot;, component: Home },\n    {\n      path: &quot;&#x2F;doc&quot;,\n      component: Doc,\n      children: [\n        { path: &quot;intro&quot;, component: IntroDoc },\n        { path: &quot;button&quot;, component: ButtonDoc },\n      ],\n    },\n  ],\n});\n\nexport default router;\n\n</code></pre><p>可以看到，最终md就能导入，并且生成了github上md的样式了😁</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/64457a76cf894f9a8564ff06a7436c99~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h1>5. 自定义代码块获取组件展示源代码</h1>\n<h2>5.1. 自定义插件vue-custom-blocks-plugin</h2>\n<pre class=\"prettyprint language-typescript\"><code>import path from &quot;path&quot;;\nimport fs from &quot;fs&quot;;\nimport { baseParse } from &quot;@vue&#x2F;compiler-core&quot;;\nconst vitePluginVue = {\n  name: &quot;preview&quot;,\n  transform(code, id) {\n    if (\n      !&#x2F;&#x5C;&#x2F;src&#x5C;&#x2F;views&#x5C;&#x2F;doc&#x5C;&#x2F;.*&#x5C;.preview&#x5C;.vue&#x2F;.test(id) ||\n      !&#x2F;vue&amp;type=preview&#x2F;.test(id)\n    ) {\n      return;\n    }\n\n    let path = &#96;.${id.match(&#x2F;&#x5C;&#x2F;src&#x5C;&#x2F;views&#x5C;&#x2F;doc&#x5C;&#x2F;.*&#x5C;.preview&#x5C;.vue&#x2F;)[0]}&#96;;\n    const file = fs.readFileSync(path).toString();\n    const parsed = baseParse(file).children.find((n) =&gt; n.tag === &quot;preview&quot;);\n    const title = parsed.children[0].content;\n    const main = file.split(parsed.loc.source).join(&quot;&quot;).trim();\n    return &#96;export default function (Component) {\n      Component.__sourceCode = ${JSON.stringify(main)}\n      Component.__sourceCodeTitle = ${JSON.stringify(title)}\n    }&#96;.trim();\n  },\n};\n\nexport default vitePluginVue;\n\n\n</code></pre><h2>5.2. 在vite.config.ts中配置</h2>\n<pre class=\"prettyprint language-TYPESCRIPT\"><code>import { defineConfig } from &#x27;vite&#x27;\nimport vue from &#x27;@vitejs&#x2F;plugin-vue&#x27;\nconst md = require(&quot;vite-plugin-markdown&quot;);\nimport vitePluginVue from &quot;.&#x2F;plugins&#x2F;vue-custom-blocks-plugin&quot;;\n\nexport default defineConfig({\n  plugins: [vue(),\n    md.plugin({\n    mode: [&quot;html&quot;, &quot;vue&quot;],\n  }),\n  vitePluginVue]\n})\n\n</code></pre><h2>5.3. 封装Preview组件展示</h2>\n<pre class=\"prettyprint language-TYPESCRIPT\"><code>&lt;template&gt;\n  &lt;div class=&quot;pre&quot;&gt;\n    &lt;h2&gt;\n      {{ component.__sourceCodeTitle }}\n      &lt;Button @click=&quot;hideCode&quot; v-if=&quot;codeVisible&quot;&gt;隐藏代码&lt;&#x2F;Button&gt;\n      &lt;Button @click=&quot;showCode&quot; v-else&gt;查看代码&lt;&#x2F;Button&gt;\n    &lt;&#x2F;h2&gt;\n    &lt;div class=&quot;pre-component&quot;&gt;\n      &lt;component :is=&quot;component&quot; &#x2F;&gt;\n    &lt;&#x2F;div&gt;\n\n    &lt;div class=&quot;pre-code&quot; v-if=&quot;codeVisible&quot;&gt;\n      &lt;pre class=&quot;language-html&quot;&gt;{{ component__sourceCOde }}&lt;&#x2F;pre&gt;\n    &lt;&#x2F;div&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script setup lang=&quot;ts&quot;&gt;\nimport Button from &quot;..&#x2F;lib&#x2F;button&#x2F;index.vue&quot;;\nimport { computed, ref } from &quot;vue&quot;;\nconst props = defineProps({\n  component: Object,\n});\n\nconst showCode = () =&gt; (codeVisible.value = true);\nconst hideCode = () =&gt; (codeVisible.value = false);\nconst codeVisible = ref(false);\n&lt;&#x2F;script&gt;\n\n&lt;style lang=&quot;scss&quot; scoped&gt;\n$border-color: #d9d9d9;\n\n.pre {\n  border: 1px solid $border-color;\n  margin: 16px 0px 32px;\n  max-width: 700px;\n  min-width: 300px;\n  &gt; h2 {\n    font-size: 20px;\n    padding: 8px 16px;\n    border-bottom: 1px solid $border-color;\n    display: flex;\n    justify-content: space-between;\n  }\n\n  &amp;-component {\n    padding: 16px;\n  }\n\n  &amp;-actions {\n    padding: 8px 16px;\n    border-top: 1px dashed $border-color;\n  }\n\n  &amp;-code {\n    padding: 8px 16px;\n    border-top: 1px dashed $border-color;\n\n    &gt; pre {\n      line-height: 1.1;\n      font-family: Consolas, &quot;Courier New&quot;, Courier, monospace;\n      margin: 0;\n      background-color: #fff;\n    }\n  }\n}\n&lt;&#x2F;style&gt;\n\n</code></pre><h2>5.4. 使用Preview组件</h2>\n<p><code>views/doc/button/index.vue</code></p>\n<pre class=\"prettyprint language-typescript\"><code>&lt;template&gt;\n  &lt;div&gt;\n    &lt;Preview :component=&quot;Button1&quot; &#x2F;&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script setup&gt;\nimport Button1 from &quot;.&#x2F;Button1.preview.vue&quot;;\nimport Preview from &quot;..&#x2F;..&#x2F;..&#x2F;components&#x2F;Preview.vue&quot;;\n&lt;&#x2F;script&gt;\n\n&lt;style lang=&quot;scss&quot;&gt;\n.jw-button + .jw-button {\n  margin-left: 20px;\n}\n&lt;&#x2F;style&gt;\n\n</code></pre><p><code>/views/doc/button/Button1.preview.vue</code></p>\n<pre class=\"prettyprint language-TYPESCRIPT\"><code>&lt;preview&gt;基础示例&lt;&#x2F;preview&gt;\n&lt;template&gt;\n  &lt;Button &#x2F;&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script setup lang=&quot;ts&quot;&gt;\nimport Button from &quot;..&#x2F;..&#x2F;..&#x2F;lib&#x2F;button&#x2F;index.vue&quot;;\n&lt;&#x2F;script&gt;\n\n</code></pre><p>现在，只要编写上面的以.preview.vue后缀的文件就行了。</p>\n<ul>\n<li>效果：</li>\n</ul>\n<p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ce6bd9f36ea7477a9cbda782701cd462~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h2>5.5. 高亮源代码</h2>\n<p>下载prismjs</p>\n<pre class=\"prettyprint\"><code>yarn add prismjs\n</code></pre><p>对Preview组件做修改</p>\n<pre class=\"prettyprint language-typescript\"><code>&lt;template&gt;\n  &lt;div class=&quot;pre&quot;&gt;\n    &lt;h2&gt;\n      {{ component.__sourceCodeTitle }}\n      &lt;Button @click=&quot;hideCode&quot; v-if=&quot;codeVisible&quot;&gt;隐藏代码&lt;&#x2F;Button&gt;\n      &lt;Button @click=&quot;showCode&quot; v-else&gt;查看代码&lt;&#x2F;Button&gt;\n    &lt;&#x2F;h2&gt;\n    &lt;div class=&quot;pre-component&quot;&gt;\n      &lt;component :is=&quot;component&quot; &#x2F;&gt;\n    &lt;&#x2F;div&gt;\n\n    &lt;div class=&quot;pre-code&quot; v-if=&quot;codeVisible&quot;&gt;\n      &lt;pre class=&quot;language-html&quot; v-html=&quot;html&quot; &#x2F;&gt;\n    &lt;&#x2F;div&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script setup lang=&quot;ts&quot;&gt;\nimport Button from &quot;..&#x2F;lib&#x2F;button&#x2F;index.vue&quot;;\nimport { computed, ref } from &quot;vue&quot;;\nimport &quot;prismjs&quot;;\nimport &quot;prismjs&#x2F;themes&#x2F;prism.css&quot;;\nconst Prism = (window as any).Prism;\nconst props = defineProps({\n  component: Object,\n});\n\nconsole.log(props.component.__sourceCode);\nconst html = computed(() =&gt; {\n  return Prism.highlight(\n    props.component.__sourceCode,\n    Prism.languages.html,\n    &quot;html&quot;\n  );\n});\nconst showCode = () =&gt; (codeVisible.value = true);\nconst hideCode = () =&gt; (codeVisible.value = false);\nconst codeVisible = ref(false);\n&lt;&#x2F;script&gt;\n</code></pre><ul>\n<li>效果</li>\n</ul>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/700c8478db074bb8a18fd57d130c3f0e~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h1>6. 去掉示例中的文件导入</h1>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/381de80dcb194c2b837f234f31d47de8~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h2>6.1. 在lib目录下创建main.ts 这个也是作为之后打包上传至npm的入口</h2>\n<pre class=\"prettyprint language-TYPESCRIPT\"><code>import { App } from &quot;vue&quot;;\nimport JwButton from &quot;.&#x2F;button&#x2F;index.vue&quot;;\nexport {  JwButton };\nconst components = [JwButton];\n\n&#x2F;&#x2F; 全局注册主键\nexport function registerJwUi(app: App): void {\n  for (const component of components) {\n    app.component(component.name, component);\n  }\n}\n\nexport default registerJwUi;\n\n</code></pre><h2>6.2. main.ts中导入注册</h2>\n<pre class=\"prettyprint\"><code>import JwUi from &quot;.&#x2F;lib&#x2F;index&quot;;\napp.use(JwUi);\n</code></pre><h2>6.3. 这样在示例中就可以直接用了<code>/src/views/doc/button/Button1.preview</code></h2>\n<pre class=\"prettyprint\"><code>&lt;preview&gt;基础示例&lt;&#x2F;preview&gt;\n&lt;template&gt;\n  &lt;jw-button &#x2F;&gt;\n&lt;&#x2F;template&gt;\n</code></pre><h2>6.4. 效果</h2>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b4ffbecdfe84d0f98af14330f981490~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h1>7. 部署到github官网</h1>\n<h2>7.1. 打包</h2>\n<pre class=\"prettyprint language-shell\"><code>yarn build\n</code></pre><h2>7.2. 上传至github</h2>\n<p>github创建一个新的仓库\n将dist上传只仓库</p>\n<h2>7.3. 进入仓库Settings最底层</h2>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f8fd96278942417b8129f2ba18d45fe8~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h2>7.4. 找到GitHub Pages</h2>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/991888189a03436e98e73cb0cbcd7125~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h2>7.5. 选择master分支 点击保存 链接就生成了</h2>\n<p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1a3ee7e034843bc9422a13c99a2ec55~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h2>7.6 一键部署</h2>\n<p>创建deploy.sh文件</p>\n<pre class=\"prettyprint language-SH\"><code>rm -rf dist &amp;&amp;\nyarn build &amp;&amp;\ncd dist &amp;&amp;\ngit init &amp;&amp;\ngit add . &amp;&amp;\ngit commit -m &quot;update&quot; &amp;&amp;\ngit branch -M master &amp;&amp;\ngit remote add origin git@github.com:coderyjw&#x2F;jw-ui-website.git &amp;&amp;\ngit push -f -u origin master &amp;&amp;\ncd -\necho https:&#x2F;&#x2F;coderyjw.github.io&#x2F;jw-ui-website&#x2F;\n</code></pre><p>执行命令</p>\n<pre class=\"prettyprint\"><code>sh deploy.sh\n</code></pre><h1>8. 上传至npm</h1>\n<h2>8.1. 创建rollup.config.js配置文件</h2>\n<pre class=\"prettyprint language-JAVASCRIPT\"><code>&#x2F;&#x2F; 为了保证版本一致，请复制我的 package.json 到你的项目，并把 name 改成你的库名\nimport esbuild from &#x27;rollup-plugin-esbuild&#x27;\nimport vue from &#x27;rollup-plugin-vue&#x27;\nimport scss from &#x27;rollup-plugin-scss&#x27;\nimport dartSass from &#x27;sass&#x27;;\nimport { terser } from &quot;rollup-plugin-terser&quot;\nimport alias from &#x27;@rollup&#x2F;plugin-alias&#x27;\nimport path from &quot;path&quot;;\nimport resolve from &#x27;rollup-plugin-node-resolve&#x27;\n\nexport default {\n  input: &#x27;src&#x2F;lib&#x2F;index.ts&#x27;,\n  output: [{\n    globals: {\n      vue: &#x27;Vue&#x27;\n    },\n    name: &#x27;Yjw-ui&#x27;,\n    file: &#x27;dist&#x2F;lib&#x2F;yjw-ui.js&#x27;,\n    format: &#x27;umd&#x27;,\n    plugins: [terser()]\n  }, {\n    name: &#x27;Yjw-ui&#x27;,\n    file: &#x27;dist&#x2F;lib&#x2F;yjw-ui.esm.js&#x27;,\n    format: &#x27;es&#x27;,\n    plugins: [terser()]\n  }],\n  plugins: [\n    scss({ include: &#x2F;&#x5C;.scss$&#x2F;, sass: dartSass }),\n    esbuild({\n      include: &#x2F;&#x5C;.[jt]s$&#x2F;,\n      minify: process.env.NODE_ENV === &#x27;production&#x27;,\n      target: &#x27;es2015&#x27; \n    }),\n    vue({\n      include: &#x2F;&#x5C;.vue$&#x2F;,\n    }),\n    alias({\n      entries: [\n        {\n          find: &#x27;@&#x27;, &#x2F;&#x2F; 别名名称，作为依赖项目需要使用项目名\n          replacement: path.resolve(__dirname, &#x27;src&#x27;), \n          customResolver: resolve({\n            extensions: [&#x27;.js&#x27;, &#x27;.jsx&#x27;, &#x27;.vue&#x27;, &#x27;.sass&#x27;, &#x27;.scss&#x27;]\n          })\n        }\n      ]\n    }),\n  ],\n}\n</code></pre><h2>8.2. 执行命令打包</h2>\n<pre class=\"prettyprint\"><code>rollup -c\n</code></pre><h2>8.3. 效果</h2>\n<p>可以看到dist文件下有lib文件，就是打包后的文件</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/521cd318fe4e48f5b235d88845e0be92~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h2>8.4. 上传至npm</h2>\n<p>需要先注册npm账号</p>\n<pre class=\"prettyprint language-shell]\"><code>npm login &#x2F;&#x2F; 先登录\nnpm publish &#x2F;&#x2F; 发布\n</code></pre><h1>9. 最后</h1>\n<p>ok,终于写完了😌，如果你觉得我写的不错，麻烦点个赞再走吧~</p>\n<p>如果觉得我写的有错的，麻烦指出再点个赞鼓励一下吧😭🤗。</p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: 前端想进大厂QAQ\n        原文链接：<a href=\"https://juejin.im/post/7052717075168493598\">https://juejin.im/post/7052717075168493598</a>\n      </p></div>","title":"用Vue3.2  + Vite2.7 快速从0快速打造一个UI组件库__Vue.js__前端","last_reply_at":"2022-01-18T05:15:00.802Z","good":false,"top":false,"reply_count":0,"visit_count":10069,"create_at":"2022-01-18T05:15:00.802Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e63ec4ad745b003c9df791","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><hr>\n<h2>theme: awesome-green</h2>\n<blockquote>\n<ul>\n<li>本文作者：<strong><a href=\"https://github.com/danranVm\">DanranVm</a></strong></li>\n</ul>\n</blockquote>\n<p><code>@idux</code> 是一个基于 <code>Vue 3.x</code> 和 <code>TypeScript</code> 开发的开源组件库，它拥有以下特性:</p>\n<ul>\n<li>Monorepo 管理模式：<code>cdk</code>, <code>components</code>, <code>pro</code></li>\n<li>开箱即用的高质量组件</li>\n<li>全面拥抱 Composition API，从源码到文档</li>\n<li>完全使用 TypeScript 开发，提供完整的类型定义</li>\n<li>灵活的全局配置</li>\n<li>深入细节的主题定制能力</li>\n<li>国际化语言支持</li>\n</ul>\n<p>相关链接：</p>\n<ul>\n<li>Github: <a href=\"https://github.com/IDuxFE/idux\">IDuxFE/idux</a></li>\n<li>官方文档: <a href=\"https://idux.site\">idux.site</a></li>\n</ul>\n<h2>特性</h2>\n<h3>Monorepo 模式</h3>\n<p><code>@idux</code> 总共分成了 3 个包:</p>\n<ul>\n<li><code>@idux/cdk</code>\n<ul>\n<li>它是一组用于构建 UI 组件库的开发套件</li>\n<li>它主要包含了一些 UI 无关或者轻 UI 的功能</li>\n<li>其灵感来自于 <a href=\"https://material.angular.cn/cdk\">@angular/cdk</a></li>\n</ul>\n</li>\n<li><code>@idux/components</code>\n<ul>\n<li>它是传统意义上的基础组件库，包含了数十种常规 UI 组件</li>\n<li>我们对每一个组件的 API 都进行了精心的设计，保证了 API 一致性</li>\n<li>组件设计参考了 <a href=\"https://ant.design/components/overview-cn/\">antd</a> 和 <a href=\"https://ng.ant.design/docs/introduce/zh\">ng-zorro</a></li>\n</ul>\n</li>\n<li><code>@idux/pro</code>\n<ul>\n<li>它是对 <code>@idux/components</code> 的进一步封装与组合，使用更少的代码去完成业务开发</li>\n<li>它主要来源于我们对自身业务的提炼</li>\n<li>它目前数量有限，我们会逐渐补充</li>\n</ul>\n</li>\n</ul>\n<h3>开箱即用</h3>\n<p>完全基于 <code>esm</code> 构建的包，支持开箱即用的 Tree Shaking 和按需加载，也可以再配合一些自动导入插件，可以让你的编码更加高效</p>\n<pre class=\"prettyprint language-ts\"><code>import { IxButton } from &quot;@idux&#x2F;components&#x2F;button&quot;;\n</code></pre><p>更多内容请参见<a href=\"https://idux.site/docs/getting-started/zh\">快速上手</a></p>\n<p>我们有覆盖率 <code>80+</code> 的单元测试和完善的 CI 流程，来保证我们的组件质量</p>\n<h3>Composition API</h3>\n<p><code>Vue 3.x</code> 的一个重要特性就是 Composition API，我们所有的源代码和示例代码，都使用它来完成</p>\n<p>它其实是一种全新的代码组织范式，让开发者可以更好的组织代码，越是复杂的组件，就越能体现它的优雅</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2c08464bc34842c2801414a92e36c1c9~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"table\"></p>\n<h3>TypeScript</h3>\n<p>我们所有的源代码都是由 <code>TypeScript</code> + <code>TSX</code> 编写，为了给用户提供最佳的使用体验，我们为此付出了诸多努力</p>\n<p>无论用户使用 <code>TSX</code> 还是使用 <code>SFC</code> (需要配合 <code>Vue Language Features</code> 插件)都可以享受到完整的类型提示与保护</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cbe40b4deee2452e93b1fc5a4a8ef6c9~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"button\"></p>\n<h3>国际化</h3>\n<p>我们针对部分组件的静态文案内置了 <code>i18n</code> 的支持，不过目前仅支持中文和英文</p>\n<p>PR Welcome, 我们也渴望得到社区力量的支持和帮助，让 <code>@idux</code> 变得更加完善</p>\n<p>更多内容请参见<a href=\"https://idux.site/docs/i18n/zh\">国际化</a></p>\n<h3>全局配置</h3>\n<p>我们给众多组件添加了全局配置功能，这应该是你见过全局配置最多的组件库之一</p>\n<p>用户可以通过全局配置来定义组件的默认行为，从而减少在模板中需要写的代码</p>\n<p>支持在运行时修改全局配置项，还支持局部覆盖全局配置项</p>\n<p>更多内容请参见<a href=\"https://idux.site/docs/config/zh\">全局配置</a></p>\n<h3>主题定制</h3>\n<p>我们尽最大努力地提供了丰富的主题变量，既有全局变量，也有组件级别的变量\n便于用户百变百搭，灵活定制自己想要的主题</p>\n<p>注意: 当前该功能并不稳定，我们正在考虑使用 <code>css</code> 变量的方案替换掉 <code>less</code> 变量\n等待方案完善后，我们会在每个组件的文档中声明所支持的主题变量</p>\n<h2>更多特性</h2>\n<h3>响应式表单系统</h3>\n<p>响应式表单系统提供了一种模型驱动的方式来处理表单输入，参考了 <code>@angular/forms</code> 的实现</p>\n<ul>\n<li>显式的表单模型：<code>FormGroup</code>, <code>FormArray</code>, <code>FormControl</code></li>\n<li>响应式的表单状态：<code>value</code>, <code>status</code></li>\n<li>统一且易扩展的表单验证：<code>Validators</code></li>\n</ul>\n<p>如果你使用过 <code>@angular/forms</code>, 那么你对下面的代码应该会感觉到很熟悉</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d5ff26d948ce47d1b6aebcd41d65089d~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"forms\"></p>\n<h3>更加受控的组件</h3>\n<p>我们为所有组件支持 <code>v-model</code> 的 <code>Props</code> 都添加了受控模式，参见 <a href=\"https://github.com/IDuxFE/idux/issues/510\">issue #510</a></p>\n<p>其他的 <code>Props</code> 绝大多数也都是受控的，这是来自我们内部低代码平台的需求，他们需要完全受控的组件。</p>\n<h3>数据驱动理念的 API 设计</h3>\n<p>对于一些复杂组件，例如，<code>table</code>, <code>tree</code>, <code>menu</code>, 我们更加推荐用户使用数据源(<code>dateSource</code>)而不是在直接在 <code>template</code> 中使用子组件。</p>\n<p>在实际的开发过程中，数据源往往是动态的，直接操作数据会比在 <code>template</code> 中使用 <code>v-if</code>, <code>v-for</code> 更加直接，也更利于维护。</p>\n<pre class=\"prettyprint language-diff\"><code>- &lt;IxMenu&gt;\n-   &lt;template v-for=&quot;item in dataSource&quot;&gt;\n-     &lt;IxMenuItem v-if=&quot;item.type === &#x27;item&#x27;&quot; :key=&quot;item.key&quot; :icon=&quot;item.icon&quot;&gt;&lt;&#x2F;IxMenuItem&gt;\n-     &lt;IxMenuDivider v-else-if=&quot;item.type === &#x27;divider&#x27;&quot; :key=&quot;item.key&quot;&gt;&lt;&#x2F;IxMenuDivider&gt;\n-   &lt;&#x2F;template&gt;\n- &lt;&#x2F;IxMenu&gt;\n\n+ &lt;IxMenu :dataSource=&quot;dataSource&quot;&gt;&lt;&#x2F;IxMenu&gt;\n</code></pre><p>更多特性等待你的发掘，快来上手体验吧。</p>\n<h2>下一步计划</h2>\n<p>目前只是 <code>@idux</code> 的开始，它还有许多不够完善的地方，接下来我们将在持续的迭代中完善它，这是我们的下一步计划：</p>\n<ul>\n<li>组件文档的完善与补充</li>\n<li>视觉优化以及设计指南完善与补充</li>\n<li>单元测试的完善与补充</li>\n<li>主题变量方案的完善</li>\n</ul>\n<p>另外，我们也在准备一个后台管理系统的模板，以供大家快速的搭建项目原型，敬请期待。</p>\n<p>最后，开源不易，希望 <code>@idux</code> 能够得到大家的喜欢和支持。我们也将不忘开源初心，积极拥抱社区，依靠社区的力量来让 <code>@idux</code> 获得更好的成长。非常欢迎热爱开源的朋友来一起参与 <code>@idux</code> 项目，无论是 <code>star</code>, <code>issue</code> 还是 <code>PR</code> 都是对我们的帮助与支持，感谢。</p>\n<h2>关于 IDuxFE</h2>\n<p>我们是来自深信服科技的前端团队，<code>@idux</code> 已经在云计算&amp;安全多场景落地，未来会实现集团内全产品覆盖，所以不用担心我们弃坑，放弃维护。</p>\n<p>如果对我们感兴趣的话，欢迎加入我们，可以投递简历到 <code>uedc@sangfor.com.cn</code> 或者直接私信我。</p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: IDuxFE\n        原文链接：<a href=\"https://juejin.im/post/7042565546189488159\">https://juejin.im/post/7042565546189488159</a>\n      </p></div>","title":"Hello @idux, 一个全新的 vue 3.x 组件库来了__Vue.js__开源","last_reply_at":"2022-01-18T04:15:00.921Z","good":false,"top":false,"reply_count":0,"visit_count":5766,"create_at":"2022-01-18T04:15:00.921Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e630b4cbbfd1003b11fa1c","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><p><code>uni-app</code>对<code>vue3 &amp; Vite</code>的升级，是一个渐进式过程：</p>\n<ul>\n<li>2020年9月：小程序平台支持 vue3 开发，小程序平台编译器依然使用webpack；</li>\n<li>2021年5月：H5平台支持 vue3 开发，H5平台编译器升级为 Vite；</li>\n<li>2021年8月：App平台支持 vue3 开发，App平台编译器升级为 Vite；</li>\n<li>2021年11月：小程序平台编译器升级为 Vite；</li>\n</ul>\n<p>至此，<code>uni-app</code>在全平台支持了 <code>Vite</code> 编译及<code>Vue 3.x</code> 运行。</p>\n<p>so，这场持续一年之久的大版本升级，究竟给<code>uni-app</code>项目带来了哪些提升？</p>\n<p>是时候总结（秀）一波了。</p>\n<p>新版 uni-app 框架主要做了三大改进：</p>\n<ul>\n<li>重写框架内核：基于<code>vue3 + ts</code>重写内置组件和API，实现更彻底、更高效的<code>tree-shaking</code>；</li>\n<li>新增支持 Vite 构建工具，在H5平台实现秒开预览；</li>\n<li>新增支持 Vue3.x，实现更灵活的开发方式，及更高的运行性能；</li>\n</ul>\n<p>基于这三大改进，uni-app项目获得了<strong>多快好省</strong>四大收益：</p>\n<ul>\n<li>更多的语法支持，支持组合式API，业务聚焦，开发效率更高；</li>\n<li>更快的编译速度，H5平台十倍加速，小程序、App加速30%以上；</li>\n<li>更好的运行性能，用户端响应更快，体验更好；</li>\n<li>更小的代码体积，瘦身30%以上，更省体积、更省流量</li>\n</ul>\n<h2>更多的语法支持</h2>\n<p>新版<code>uni-app</code>支持Vue 3.x框架，支持组合式API，可实现更聚焦的业务开发。</p>\n<p>Vue 3.x的一些新增特性，<code>uni-app</code>也已经完全支持，如：</p>\n<ul>\n<li>支持<code>&lt;script setup&gt;</code></li>\n<li>支持<code>&lt;style scoped&gt;</code>、<code>&lt;style module&gt;</code>、<code>State-Driven Dynamic CSS(v-bind)</code></li>\n<li>支持<code>jsx</code>、<code>tsx</code>（h5,app 平台支持，小程序不支持）</li>\n</ul>\n<p>另外，在小程序平台，新版<code>uni-app</code>也扩展了更多的语法，如：</p>\n<ul>\n<li>更完善的模板语法支持（如 <code>class</code>、<code>style</code> 支持函数、变量等，不再局限数组、对象类型）</li>\n<li>更完整的 <code>props</code> 支持（如传递函数）</li>\n<li>更完善的 <code>slot</code> 支持（如作用域插槽）</li>\n</ul>\n<h2>更快的编译速度</h2>\n<p>开发者日常工作中，最无聊的就是等待编译构建。</p>\n<p>某乎上还有一个”程序员在等待编译的时候都做什么？“的讨论帖，可见编译时间对开发者而言，是一个多么尴尬无聊的碎片时间。</p>\n<p><code>uni-app</code>本次升级<code>vue3 &amp; Vite</code>后，在编译时间上有多少改进？带给开发者多少福利？我们安排真实测试，以数据说话。</p>\n<p>测试环境说明：</p>\n<pre class=\"prettyprint\"><code>硬件：RedmiBook 14 二代\n处理器：Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz \n内存：16.0 GB\n操作系统：Windows 11 专业版 64 位操作系统\n</code></pre><p>关于编译速度，我们做了两个维度的对比：</p>\n<ul>\n<li>纵向对比：挑选<code>uni-app</code>常用项目模板，在H5、小程序、App平台，分别测试<code>vue 2.6</code>和<code>vue 3.x</code>的编译时间</li>\n<li>横向对比：使用业内优秀的其它跨端框架，创建默认项目模板，记录其编译时间，和<code>uni-app</code>的<code>vue 3.x</code>版本进行对比</li>\n</ul>\n<h3>uni-app 历史版本纵向对比</h3>\n<p>我们选择<code>uni-app默认模板</code>、<code>uni-starter</code>、<code>hello-uniapp</code>三个项目模板，分别测试<code>vue 2.6</code>和<code>vue 3.x</code>的编译时间。</p>\n<p>uni-app项目编译时间的采集方式：</p>\n<ul>\n<li><code>vue 2.6</code>版本编译时间 = webpack 的 stats.endTime - stats.startTime</li>\n<li><code>vue 3.x</code>版本编译时间 = 构建工具入口处记录 global.__vite_start_time = performance.now()，构建工具编译完成时：performance.now() - global.__vite_start_time</li>\n</ul>\n<h4>H5平台</h4>\n<p>对<code>uni-app</code>的三个项目模板分别运行到H5平台，进行多次编译测试，并求其均值后，获得如下数据：</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8e2353cca266411d825875799c4e3131~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>由此，我们可以观察到：</p>\n<ul>\n<li>在<code>vue 2.6</code>环境下，随着项目复杂度的提升，H5首页预览所需编译时间会直线增加；这是因为在<code>vue 2.6</code>版本下，虽然仅预览首页，但依然会使用 <code>webpack</code> 编译整个项目资源；故项目越复杂，编译时间越长；</li>\n<li>在<code>vue 3.x</code>环境下，H5首页预览的编译时间跟项目复杂度也有关系，但增幅不大；这是因为在<code>vue 3.x</code>版本下，使用 <code>Vite</code> 进行构建，预览首页时仅编译首页及首页所依赖资源，不会编译其它页面资源。</li>\n</ul>\n<p>通过图表对比，我们可以直观得出结论：<code>vue 3.x</code>环境下的首页编译时间，平均不到<code>vue 2.6</code>环境下的十分之一。</p>\n<p>换言之，<code>vue 3.x</code>版本下的首页编译速度，相比<code>vue 2.6</code>版本，有<strong>十倍效率提升</strong>。</p>\n<p>这个十倍效率提升，主要得益于新版采用<code>Vite</code>作为构建工具，由此带来了两大好处：</p>\n<ul>\n<li>使用原生 ESM 文件，无需打包，实现极速的服务启动；</li>\n<li>预览（运行）使用<code>esbuild</code>作为打包工具，相比<code>vue 2.6</code>环境下的<code>webpack</code>，构建速度快 10-100 倍（这不是我们夸大，详见<a href=\"https://esbuild.docschina.org/\">esbuild</a>）</li>\n</ul>\n<p>本着这个<strong>十倍效率提升</strong>，小伙伴们还不赶紧上手试试？</p>\n<h4>小程序平台</h4>\n<p>对<code>uni-app</code>的三个模板项目运行到小程序平台，多次编译测试，并求其均值后，获得如下数据：</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6075ea69904d4fcdbe1977ed2ff4d1a7~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>从上图对比数据来看，我们可以得出结论：小程序平台，<code>vue 3.x</code>版本下的运行编译，相比<code>vue 2.6</code>版本，编译性能至少提升30%；且项目越复杂，编译性能提升越明显，可以达到40% ~ 50%。</p>\n<h4>App平台</h4>\n<p>对<code>uni-app</code>的三个项目模板继续运行到App平台，多次编译测试，并求其均值后，获得如下数据：</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27f0ffcd92d24f08bd539273a913b9d9~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"vue3-08\"></p>\n<p>从上图对比数据来看，我们可以得出结论：App平台，<code>vue 3.x</code>版本下的运行编译，相比<code>vue 2.6</code>版本，编译性能提升将近50%。</p>\n<p>虽没有H5平台的十倍效率提升那么刺激，但将近50%的速度提升，经常开发小程序/App的小伙伴，还不心动？</p>\n<h3>业内优秀框架横向对比</h3>\n<p>除了采用不同版本的<code>uni-app</code>进行纵向对比外，我们还使用业内优秀的跨端框架<code>Taro</code>，创建空的项目模板，进行横向对比测试。</p>\n<p>具体测试方案：</p>\n<ol>\n<li>安装<code>Taro</code>的最新cli，本文测试时使用的版本为&quot;@Tarojs/Taro&quot;: “3.3.16”</li>\n<li>使用<code>Taro init</code>命令，分别选择<code>react</code>、<code>vue</code>、<code>vue3</code>框架，创建三个默认项目模板，三个项目名称分别为<code>taro3-react</code>、<code>taro3-vue</code>、<code>taro3-vue3</code>，如下图：\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a796e778344f43798a2c80393c12918a~tplv-k3u1fbpfcp-zoom-1.image\" alt></li>\n<li>使用<code>npm run dev:h5</code>，运行到H5平台进行预览，记录每次预览编译时间，重复执行，求其均值</li>\n</ol>\n<p>关于<code>Taro</code>编译时间的计算方案：</p>\n<ol>\n<li>开发一个<code>Taro</code>扩展插件，插件规范参考<a href=\"http://Taro-docs.jd.com/Taro/docs/plugin/\">Taro官网 - 插件功能</a></li>\n<li>在<code>ctx.onBuildStart</code>中记录开始编译时间</li>\n<li>在<code>ctx.onBuildFinish</code>中记录编译结束时间</li>\n<li>两者的时间差，即为编译过程消耗时间</li>\n</ol>\n<p>然后使用<code>uni-app</code>的<code>cli</code>命令行，创建基于<code>vue3.x</code>的空项目模板，项目命名为<code>uni-app-vue3</code>。</p>\n<p>我们使用各自框架的命令行，将如上创建的5个项目分别编译到H5平台和小程序平台，多次测试，并求其均值。</p>\n<p>同框架版本在H5平台上的编译时间，结果如下：</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6ec0d69f1f4047adaf6daa6969e28383~tplv-k3u1fbpfcp-watermark.image?\" alt=\"vue3-18.png\"></p>\n<p>从图中可以看出，<code>uni-app</code>的<code>vue3</code>版本，在H5平台上的首页编译预览性能是遥遥领先的。</p>\n<p>继续编译到小程序平台，多次测试，求其均值，结果如下：</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dceaa0a94a51448cbf594d1139d0fdc1~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>从图中可以看出，<code>uni-app</code>的<code>vue3</code>版本，在小程序平台上的编译性能也是非常领先的。</p>\n<h2>更好的运行速度</h2>\n<p>开发环节编译快了，那面向最终用户的软件，运行性能怎么样？</p>\n<p>我们进入性能测试章节。</p>\n<p>测试方案：</p>\n<ul>\n<li>开发内容：开发一个仿微博小程序首页的复杂长列表，支持下拉刷新、上拉翻页、点赞。</li>\n<li>界面如下：\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4dd6fb97b0c44992949eac222c422ed6~tplv-k3u1fbpfcp-zoom-1.image\" alt></li>\n<li>测试机型：小米 Mi 10 pro、MIUI 12.5 (21.11.3 开发版) 、微信版本 8.0.16</li>\n<li>准备工作：每次开始测试前，杀掉各App进程、清空内存，保证测试机环境基本一致；每次从本地读取静态数据，屏蔽网络差异。</li>\n<li>评测点：长列表中的某个组件，比如点赞组件，点击时是否能及时的修改未赞和已赞状态？</li>\n</ul>\n<p>测试计时方式：</p>\n<ul>\n<li>选中某微博，点击“点赞”按钮，实现点赞状态状态切换（已赞高亮、未赞灰色），</li>\n<li>点赞按钮 onclick函数开头开始计时，setData回调函数开头结束计时；</li>\n</ul>\n<p>在小米手机上进行多次测试，求其平均值，结果如下：</p>\n<table>\n<thead>\n<tr>\n<th>记录条数</th>\n<th>200</th>\n<th>400</th>\n<th>600</th>\n<th>800</th>\n<th>1000</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>vue2</td>\n<td>30ms</td>\n<td>43ms</td>\n<td>56ms</td>\n<td>72ms</td>\n<td>90ms</td>\n</tr>\n<tr>\n<td>vue3</td>\n<td>8ms</td>\n<td>9ms</td>\n<td>9ms</td>\n<td>8ms</td>\n<td>9ms</td>\n</tr>\n</tbody>\n</table>\n<p>从表格中可以看出：</p>\n<ul>\n<li>随着页面记录的增加，<code>vue 2.6</code>版本的<code>uni-app</code>项目，点赞组件响应时间快速增加，响应越来越慢；</li>\n<li>基于<code>vue 3.x</code>的<code>uni-app</code>项目，点赞组件的响应时间跟页面条数无关，一直保持极高的响应灵敏度，性能体验远高于<code>vue 2.6</code>版本。</li>\n</ul>\n<p>从这个常见的长列表组件响应实验来看，<code>vue 3.x</code>的性能体验要远高于<code>vue 2.6</code>版本。</p>\n<h2>更小的代码体积</h2>\n<p>项目发行后的代码体积，是一个很重要的考量指标：</p>\n<ul>\n<li>H5平台：更小的代码体积，可以帮助开发者节省服务端带宽及CDN流量，可实现更快的资源加载及页面渲染；</li>\n<li>小程序平台：更小的代码体积，可加速小程序包的下载（甚至可能免了分包加载的繁琐），帮助用户更快进入小程序业务界面；</li>\n<li>App平台：更小的代码体积，可实现更快的App启动，帮助用户更快进入App首页</li>\n</ul>\n<p>为了测试<code>vue 3.x</code>新版升级后，代码体积的变化，我们同样做了两个维度的测试：</p>\n<ul>\n<li>纵向对比：选择<code>uni-app</code>常用项目模板，在H5、小程序、App平台，分别测试<code>vue 2.6</code>和<code>vue 3.x</code>的编译包大小</li>\n<li>横向对比：使用业内优秀的其它跨端框架，创建默认项目模板，记录其编译后的包体积大小，和<code>uni-app</code>版本进行对比</li>\n</ul>\n<p>Tips：</p>\n<ul>\n<li>开发阶段重在编译速度，对应<code>npm run dev</code>操作</li>\n<li>发行阶段重在编译包大小，对应<code>npm run build</code>操作</li>\n</ul>\n<h3>uni-app 不同版本纵向对比</h3>\n<p>我们复用之前创建的<code>uni-app默认模板</code>、<code>uni-starter</code>、<code>hello-uniapp</code>三个项目模板，分别测试<code>vue 2.6</code>和<code>vue 3.x</code>的编译包体积。</p>\n<p><code>uni-app</code>项目编译包体积的采集方式：编译到对应平台后，记录编译后文件夹的大小。</p>\n<h4>H5平台</h4>\n<p>H5平台编译后代码体积记录如下：</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b054f08c66224e8cb203f3f45d8d89a0~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>从统计结果来看，<code>uni-app</code>的<code>vue3.x</code>版本，在H5平台上的编译包体积至少瘦身30%以上。</p>\n<p>H5平台的瘦身优化，主要得益于<code>uni-app</code>框架的底层全面重构，实现了更彻底的摇树优化。</p>\n<h4>小程序平台</h4>\n<p>小程序平台编译后代码体积记录如下：</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d20ce20582c944aa9702854af727110c~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>从统计结果来看，<code>uni-app</code>的<code>vue3.x</code>版本，在小程序平台上也有大幅瘦身。</p>\n<h4>App平台</h4>\n<p>App平台编译后代码体积记录如下：\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5c0460ee198844168bc35146c8f1c047~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>从统计结果来看，<code>uni-app</code>的<code>vue3.x</code>版本，在App平台上根据项目不同，会有不同幅度的瘦身。</p>\n<p>从理论上来讲，项目中的页面模板越复杂，App平台的瘦身效果越明显。</p>\n<h3>业内优秀框架横向对比</h3>\n<p>关于编译后的代码体积，我们也和业内优秀的跨端框架<code>Taro</code>进行了对比，复用前面章节创建的三个<code>Taro</code>项目，分别编译到H5平台和小程序平台，计算其编译后的源码文件夹大小。</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/012e95bd1e3343e6b3c6e741a28a7e45~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>从图中可以看出，<code>uni-app</code>的vue3版本，在H5平台上编译包体积是最小的，只有友商的十分之一左右。</p>\n<p>我们继续测试，不同版本框架发行到微信小程序平台，记录其编译包大小：</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c6aa5fdca3084951ba2fb2ea0389032d~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>从图中可以看出，<code>uni-app</code>的vue3版本，在小程序平台上编译包体积也是最小的。</p>\n<p>**Tips：**细心的开发者会发现，所有框架版本编译到小程序上的代码包体积都远小于其在H5平台上的包体积，这是因为小程序由平台厂商提供内置组件及接口实现，而H5平台则需跨端框架自己实现内置组件及接口，故H5平台的代码包普遍要大一些。</p>\n<h2>总结</h2>\n<p>综上，我们以数字说话，阐述了vue3版本<code>uni-app</code>开发的诸多好处，再回顾一遍：</p>\n<ul>\n<li>更多的语法</li>\n<li>更快的编译</li>\n<li>更好的运行</li>\n<li>更少的代码</li>\n</ul>\n<p>你还不赶紧升级新版<code>uni-app</code>来试试吗？</p>\n<p>对文本测试过程及结果有疑问的同学，欢迎到github上提交<a href=\"https://github.com/dcloudio/uni-app\">issue</a>，欢迎指正。<p>\n版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n作者: CHB\n原文链接：<a href=\"https://juejin.im/post/7049913295242788871\">https://juejin.im/post/7049913295242788871</a>\n</p></p>\n</div>","title":"vue3和Vite双向加持，uni-app性能再次提升__uni-app__Vue.js","last_reply_at":"2022-01-18T03:15:00.832Z","good":false,"top":false,"reply_count":0,"visit_count":5862,"create_at":"2022-01-18T03:15:00.832Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e622a4cbbfd1003b11fa19","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><h2>背景</h2>\n<p>这件事大约发生在半年前，有人在 <a href=\"https://github.com/vitejs/vite\">vite</a> 的仓库下提了一个关于 <code>vue-router</code> 的 <a href=\"https://github.com/vitejs/vite/issues/1775\">issue</a>：</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a111ba10af1e453a8d15b8a596999a11~tplv-k3u1fbpfcp-watermark.image?\" alt=\"angry.jpg\"></p>\n<p>提问者的意思是 <a href=\"https://github.com/vuejs/vue-router-next\"><code>vue-router</code></a> 有个未解决的 bug，影响到他即将上线的项目了，而 <code>vue-router</code> 的维护者没能解决这个问题，希望尤大来帮忙解决。</p>\n<p>但是，提问者跑到 vite 仓库下来发这个 issue，就显得非常不合适了。</p>\n<p>尤大显然对这个行为很不高兴，回复也很霸气：</p>\n<ol>\n<li>\n<p>不要在不相关的仓库提不相关的 issue。</p>\n</li>\n<li>\n<p>大家都很忙，没空管就是没空管，不要催。</p>\n</li>\n<li>\n<p>再犯会被 block。</p>\n</li>\n</ol>\n<p>提问者跑来这里发 issue 的理由是尤大经常活跃在 vite 社区。而在我看来，尤大应该早就知道这个 issue 了，因为 <code>vue-router</code> 的维护者也是 Vue 核心成员之一，他们肯定私下聊过这个问题。</p>\n<p>那么为什么迟迟不解决这个问题呢，我猜测有两点原因：</p>\n<ol>\n<li>\n<p>这个问题本身不好解决，可能要牵涉到不少的代码改动。</p>\n</li>\n<li>\n<p>Vue 团队正在全力做他们认为重要而紧急的事情，这个事情的优先级并不高。</p>\n</li>\n</ol>\n<p>那么，提问者究竟是遇到了什么问题呢？为何我会关注到这个 issue？因为最近我也遇到一个类似的问题。</p>\n<h2>类似的问题</h2>\n<p>在我的 <a href=\"https://coding.imooc.com/class/503.html\">《Vue3 开发企业级音乐课 App》课程</a>问答区，有个学生反馈了一个问题：<code>RouterView</code> 配合 <code>KeepAilve</code> 组件使用后，二级路由的歌手详情页的 <code>created</code> 钩子函数会执行两次。</p>\n<p>我测试后发现确实有这个问题，最开始我怀疑是 Vue3 或者是 <code>vue-router</code> 某个版本的 bug，于是我把 Vue3 和 <code>vue-router</code> 都升级到最新版本，发现这个问题仍然存在。</p>\n<p>那么，会不会是我的业务代码写出的问题呢？直觉告诉不会，为了找出问题的根本原因，同时减少调试的复杂度，我写了个最小化复现问题的 <a href=\"https://github.com/ustbhuangyi/vue3-keep-alive-issue\">demo</a>。</p>\n<p>Demo 页面共有二级路由，它的定义如下：</p>\n<pre class=\"prettyprint language-js\"><code>import { createRouter, createWebHashHistory } from &#x27;vue-router&#x27;\nconst Home = import(&#x27;..&#x2F;views&#x2F;Home.vue&#x27;)\nconst HomeSub = import(&#x27;..&#x2F;views&#x2F;HomeSub.vue&#x27;)\nconst Sub = import(&#x27;..&#x2F;views&#x2F;Sub.vue&#x27;)\nconst About = import(&#x27;..&#x2F;views&#x2F;About.vue&#x27;)\n\nconst routes = [\n  {\n    path: &#x27;&#x2F;&#x27;,\n    redirect: &#x27;&#x2F;home&#x27;\n  },\n  {\n    path: &#x27;&#x2F;home&#x27;,\n    name: &#x27;Home&#x27;,\n    component: Home,\n    children: [\n      {\n        path: &#x27;sub&#x27;,\n        component: HomeSub\n      }\n    ]\n  },\n  {\n    path: &#x27;&#x2F;about&#x27;,\n    name: &#x27;About&#x27;,\n    component: About,\n    children: [\n      {\n        path: &#x27;sub&#x27;,\n        component: Sub\n      }\n    ]\n  }\n]\n\nconst router = createRouter({\n  history: createWebHashHistory(),\n  routes\n})\n\nexport default router\n</code></pre><p>这里要注意，<strong>必须两个主路由页面都嵌套子路由</strong>。</p>\n<p>接着来看页面的几个 Vue 组件的定义，其中 <code>App.vue</code> 为页面入口组件：</p>\n<pre class=\"prettyprint language-html\"><code>&lt;template&gt;\n  &lt;div id=&quot;nav&quot;&gt;\n    &lt;router-link to=&quot;&#x2F;&quot;&gt;Home&lt;&#x2F;router-link&gt; |\n    &lt;router-link to=&quot;&#x2F;about&quot;&gt;About&lt;&#x2F;router-link&gt;\n  &lt;&#x2F;div&gt;\n  &lt;router-view v-slot=&quot;{ Component }&quot;&gt;\n    &lt;keep-alive&gt;\n      &lt;component :is=&quot;Component&quot;&#x2F;&gt;\n    &lt;&#x2F;keep-alive&gt;\n  &lt;&#x2F;router-view&gt;\n&lt;&#x2F;template&gt;\n</code></pre><p><code>Home.vue</code> 是一级路由组件：</p>\n<pre class=\"prettyprint language-vue\"><code>&lt;template&gt;\n  &lt;div class=&quot;home&quot;&gt;\n    &lt;img alt=&quot;Vue logo&quot; src=&quot;..&#x2F;assets&#x2F;logo.png&quot;&gt;\n    &lt;button @click=&quot;showSub&quot;&gt;click me&lt;&#x2F;button&gt;\n    &lt;router-view&gt;&lt;&#x2F;router-view&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script&gt;\n\nexport default {\n  name: &#x27;Home&#x27;,\n  created() {\n    console.log(&#x27;home page created&#x27;)\n  },\n  methods: {\n    showSub() {\n      this.$router.push(&#x27;&#x2F;home&#x2F;sub&#x27;)\n    }\n  }\n}\n&lt;&#x2F;script&gt;\n</code></pre><p><code>HomeSub</code> 是 <code>Home</code> 组件中的二级路由组件：</p>\n<pre class=\"prettyprint language-vue\"><code>&lt;template&gt;\n  &lt;div&gt;This is home sub&lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script&gt;\n  export default {\n    name: &#x27;HomeSub&#x27;,\n    created() {\n      console.log(&#x27;home sub created&#x27;)\n    }\n  }\n&lt;&#x2F;script&gt;\n</code></pre><p><code>About.vue</code> 是一级路由组件：</p>\n<pre class=\"prettyprint language-vue\"><code>&lt;template&gt;\n  &lt;div class=&quot;about&quot;&gt;\n    &lt;h1&gt;This is an about page&lt;&#x2F;h1&gt;\n    &lt;button @click=&quot;showSub&quot;&gt;click me&lt;&#x2F;button&gt;\n    &lt;router-view&gt;&lt;&#x2F;router-view&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\n  export default {\n    name: &#x27;About&#x27;,\n    created() {\n      console.log(&#x27;about page created&#x27;)\n    },\n    methods: {\n      showSub() {\n        this.$router.push(&#x27;&#x2F;about&#x2F;sub&#x27;)\n      }\n    }\n  }\n&lt;&#x2F;script&gt;\n</code></pre><p><code>Sub.vue</code> 是 <code>About</code> 组件中的二级路由组件：</p>\n<pre class=\"prettyprint language-vue\"><code>&lt;template&gt;\n  &lt;div&gt;This is sub&lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script&gt;\n  export default {\n    name: &#x27;Sub&#x27;,\n    created() {\n      console.log(&#x27;sub created&#x27;)\n    }\n  }\n&lt;&#x2F;script&gt;\n</code></pre><p>复现的步骤很简单，首先进入 <code>Home</code> 页：</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68df82eaef904298b4b8afed4dbcb276~tplv-k3u1fbpfcp-watermark.image?\" alt=\"home.jpg\"></p>\n<p>然后点击 <code>About</code> 标签进入 <code>About</code> 页：</p>\n<p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/02c7f013c4f0407b8c335b496cf72267~tplv-k3u1fbpfcp-watermark.image?\" alt=\"about.jpg\"></p>\n<p>接着点击按钮，渲染 <code>Sub</code> 子路由组件：</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa7459efc8e141e9b8705352695ca88d~tplv-k3u1fbpfcp-watermark.image?\" alt=\"about-sub.jpg\"></p>\n<p>页面渲染都是正常的，但是我们发现 <code>Sub</code> 组件的 <code>created</code> 钩子函数执行了两次，输出了两次 <code>sub created</code>。这就相当于渲染了两次 <code>Sub</code> 组件，显然是有问题的。</p>\n<h2>bug 分析</h2>\n<p>我开启了调试大法，在 <code>Sub</code> 组件的 <code>created</code> 钩子函数中打上 <code>debugger</code> 断点，然后顺着函数的调用堆栈一步步往前看。</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd529a3a867149a196a89ed04ca68d39~tplv-k3u1fbpfcp-watermark.image?\" alt=\"debugger.jpg\"></p>\n<p>显然，<code>debugger</code> 是在 <code>created</code> 钩子函数内部，而该钩子函数的执行是在组件挂载阶段，那么是什么操作触发了组件的挂载呢？</p>\n<p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/831ce83bbd4140abab4531628f091bb2~tplv-k3u1fbpfcp-watermark.image?\" alt=\"debugger1.jpg\"></p>\n<p>顺着调用堆栈继续查找，我们发现最终原因是因为修改了路由中的 <code>currentRoute</code>，触发了 <code>setter</code>，然后触发了 <code>RouterView</code> 组件的重新渲染，最终触发了 <code>Sub</code> 组件的渲染。</p>\n<p>那么为什么 <code>currentRoute</code> 的修改会触发 <code>RouterView</code> 组件的重新渲染呢？这要从 <code>RouterView</code> 的实现原理说起：</p>\n<pre class=\"prettyprint language-js\"><code>const RouterViewImpl = defineComponent({\n  name: &#x27;RouterView&#x27;,\n  inheritAttrs: false,\n  props: {\n    name: {\n      type: String,\n      default: &#x27;default&#x27;,\n    },\n    route: Object,\n  },\n  setup(props, { attrs, slots }) {\n    (process.env.NODE_ENV !== &#x27;production&#x27;) &amp;&amp; warnDeprecatedUsage()\n    const injectedRoute = inject(routerViewLocationKey)\n    const routeToDisplay = computed(() =&gt; props.route || injectedRoute.value)\n    const depth = inject(viewDepthKey, 0)\n    const matchedRouteRef = computed(() =&gt; routeToDisplay.value.matched[depth])\n    provide(viewDepthKey, depth + 1)\n    provide(matchedRouteKey, matchedRouteRef)\n    provide(routerViewLocationKey, routeToDisplay)\n    const viewRef = ref()\n    watch(() =&gt; [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) =&gt; {\n      if (to) {\n        to.instances[name] = instance\n        if (from &amp;&amp; from !== to &amp;&amp; instance &amp;&amp; instance === oldInstance) {\n          if (!to.leaveGuards.size) {\n            to.leaveGuards = from.leaveGuards\n          }\n          if (!to.updateGuards.size) {\n            to.updateGuards = from.updateGuards\n          }\n        }\n      }\n      if (instance &amp;&amp;\n        to &amp;&amp;\n        (!from || !isSameRouteRecord(to, from) || !oldInstance)) {\n        (to.enterCallbacks[name] || []).forEach(callback =&gt; callback(instance))\n      }\n    }, { flush: &#x27;post&#x27; })\n    return () =&gt; {\n      const route = routeToDisplay.value\n      const matchedRoute = matchedRouteRef.value\n      const ViewComponent = matchedRoute &amp;&amp; matchedRoute.components[props.name]\n      const currentName = props.name\n      if (!ViewComponent) {\n        return normalizeSlot(slots.default, { Component: ViewComponent, route })\n      }\n      const routePropsOption = matchedRoute.props[props.name]\n      const routeProps = routePropsOption\n        ? routePropsOption === true\n          ? route.params\n          : typeof routePropsOption === &#x27;function&#x27;\n            ? routePropsOption(route)\n            : routePropsOption\n        : null\n      const onVnodeUnmounted = vnode =&gt; {\n        if (vnode.component.isUnmounted) {\n          matchedRoute.instances[currentName] = null\n        }\n      }\n      const component = h(ViewComponent, assign({}, routeProps, attrs, {\n        onVnodeUnmounted,\n        ref: viewRef,\n      }))\n      return (\n        normalizeSlot(slots.default, { Component: component, route }) ||\n        component)\n    }\n  },\n})\n</code></pre><p><code>RouterView</code> 组件是基于 Composition API 实现的，我们重点看它的渲染部分，由于 <code>setup</code> 函数的返回值是一个函数，那这个函数就是它的渲染函数。</p>\n<p><code>RouterView</code> 主要的思路就是根据路径 <code>route</code> 和当前 <code>RouterView</code> 嵌套的深度来匹配路由配置中对应的路由组件并渲染。</p>\n<p>在整个渲染过程中，会访问计算属性 <code>routeToDisplay</code>，它的定义如下：</p>\n<pre class=\"prettyprint language-js\"><code>const injectedRoute = inject(routerViewLocationKey)\nconst routeToDisplay = computed(() =&gt; props.route || injectedRoute.value)\n</code></pre><p><code>routeToDisplay</code> 内部又会访问 <code>injectedRoute</code>，而 <code>injectedRoute</code> 注入的是 <code>key</code> 为 <code>routerViewLocationKey</code> 的数据。</p>\n<p>在执行 <code>createRouter</code> 创建路由的时候，内部会创建 <code>currentRoute</code> 响应式变量来维护当前的路径。</p>\n<pre class=\"prettyprint language-js\"><code>const currentRoute = shallowRef(START_LOCATION_NORMALIZED)\n</code></pre><p>然后在执行 <code>createApp(App).use(router)</code> 安装路由的时候，会执行 <code>router</code> 对象提供的 <code>install</code> 方法，其中会把 <code>currentRoute</code> 通过 <code>routerViewLocationKey</code> 提供给应用使用。</p>\n<pre class=\"prettyprint language-js\"><code>app.provide(routerViewLocationKey, currentRoute)\n</code></pre><p>因此在渲染 <code>RouterView</code> 组件的时候，访问了 <code>routeToDisplay</code>，内部会访问 <code>injectedRoute</code>，进而也就访问到了 <code>currentRoute</code>，而又由于 <code>currentRoute</code> 是响应式对象，进而会触发它的依赖收集过程。</p>\n<p>这样当我们执行 <code>router</code> 对象的 <code>push</code> 方法修改路由路径时，内部会执行 <code>finalizeNavigation</code> 方法，然后修改了 <code>currentRoute</code>，就会触发<strong>所有的</strong> <code>RouterView</code> 组件的重新渲染。</p>\n<p>默认情况下，这个逻辑是没有任何问题的，那么为什么加上 <code>KeepAlive</code> 就有问题了呢？</p>\n<p>回答这个问题前，我们不妨先思考另一个问题：示例中，在正常情况下，路由从 <code>Home</code> 切到 <code>About</code> 后，此时我们修改 <code>currentRoute</code>，会触发 <code>Home</code> 组件内部的 <code>RouterView</code> 重新渲染吗？</p>\n<p>答案是不会的，因为当路由从 <code>Home</code> 切到 <code>About</code> 时，会触发 <code>Home</code> 组件的卸载，进而也会触发其内部的 <code>RouterView</code> 组件卸载。</p>\n<p><code>RouterView</code> 组件在卸载过程中，会清除组件作用域下的所有依赖，当然也包括 <code>currentRoute</code> 收集的组件的 <code>render effect</code>。因此当我们修改 <code>currentRoute</code> 时，就不会触发 <code>Home</code> 组件内部的 <code>RouterView</code> 组件重新渲染了。</p>\n<p>但是，一旦 <code>Home</code> 组件对应的 <code>RouterView</code> 被 <code>KeepAlive</code> 组件包裹后，当路由从 <code>Home</code> 切到 <code>About</code> 时，是不会执行 <code>Home</code> 组件的卸载过程的，也就不会卸载内部的 <code>RouterView</code> 组件，当然也就没有清除其作用域下的依赖。</p>\n<p>那么当我们修改 <code>currentRoute</code> 时，不仅会渲染 <code>About</code> 组件内部的 <code>RouterView</code> 组件，也会触发 <code>Home</code> 组件内部的 <code>RouterView</code> 重新渲染。</p>\n<p>由于 <code>Home</code> 组件内部的 <code>RouterView</code> 和 <code>About</code> 组件内部的 <code>RouterView</code> 都是二级路由组件，根据 <code>RouterView</code> 渲染的逻辑，此时 <code>Home</code> 组件内部的 <code>RouterView</code> 也会渲染成 <code>Sub</code> 组件，这就是为何 <code>Sub</code> 组件渲染两次的原因。</p>\n<h2>给 Vue3 提 issue</h2>\n<p>虽然定位出这个 bug，但一时半会儿我也想不出好的解决方案，于是我尝试给 Vue3 提了个 <a href=\"https://github.com/vuejs/vue-next/issues/4708\">issue</a> 。</p>\n<p>这里顺便与你分享一下提 issue 的一些注意事项：</p>\n<ol>\n<li>\n<p>通常一些不错的开源项目都会有 issue template，你可以根据它的指引创建 issue。</p>\n</li>\n<li>\n<p>为了让开源项目的维护者更快、更精确的定位问题，通常你需要最小化复现问题，提供一个可复现问题的 demo，而不是提供一个出问题的项目。</p>\n</li>\n<li>\n<p>建议提问前能加上一些自己对问题的分析和思考，这虽然不是必要的，但这个过程会让你更加熟悉这个开源项目，而且也可以帮助维护者更容易定位问题。</p>\n</li>\n<li>\n<p>如果确实是个 bug 且你有能力修复的话，提完 issue 可以顺便提一个 pull request，直接参与到开源项目的共建中，这个过程对自身的技术成长会有非常大的帮助。</p>\n</li>\n</ol>\n<p>不过令我尴尬的是，我在提完 issue 后还不到五分钟，issue 就被关闭了，原因是它与 <code>vue-router-next</code> 项目中的一个 <a href=\"https://github.com/vuejs/vue-router-next/issues/626\">issue</a> 重复了。</p>\n<p>我对该 issue 做了大致的浏览，发现它早在 2020 年 12 月 1 号就被提出了，而且有相当多的人都遇到了类似的问题。在该 issue 下面可以发现很多相关联的 issue，其中也包括文章开头提到的 issue，这就是为何我能关注到它的原因。</p>\n<p><code>vue-router</code> 的维护者也尝试解决过，但是遇到了一些麻烦，详情可以看他在 issue 中的回复。</p>\n<p>遗憾的是到目前为止，该 issue 也没有被解决，维护者给它贴上了 <code>help wanted</code> 的标签，希望得到来自社区的帮助。</p>\n<h2>Vue2 也有这个问题吗？</h2>\n<p>因为我司目前还在使用 Vue2，所以我最关心的是 Vue2 是否也存在该问题。</p>\n<p>于是我用 Vue2 写了同样的 demo，令我欣慰的是 Vue2 并未有这个 bug，那这又是什么原因呢？</p>\n<p>由于 Vue 使用的是 <code>vue-router</code> 的 3.x 版本，它对应的 <code>RouterView</code> 组件的实现如下：</p>\n<pre class=\"prettyprint language-js\"><code>var View = {\n  name: &#x27;RouterView&#x27;,\n  functional: true,\n  props: {\n    name: {\n      type: String,\n      default: &#x27;default&#x27;\n    }\n  },\n  render: function render(_, ref) {\n    var props = ref.props\n    var children = ref.children\n    var parent = ref.parent\n    var data = ref.data\n\n    data.routerView = true\n\n    var h = parent.$createElement\n    var name = props.name\n    var route = parent.$route\n    var cache = parent._routerViewCache || (parent._routerViewCache = {})\n\n    var depth = 0\n    var inactive = false\n    while (parent &amp;&amp; parent._routerRoot !== parent) {\n      var vnodeData = parent.$vnode ? parent.$vnode.data : {}\n      if (vnodeData.routerView) {\n        depth++\n      }\n      if (vnodeData.keepAlive &amp;&amp; parent._directInactive &amp;&amp; parent._inactive) {\n        inactive = true\n      }\n      parent = parent.$parent\n    }\n    data.routerViewDepth = depth\n    \n    if (inactive) {\n      var cachedData = cache[name]\n      var cachedComponent = cachedData &amp;&amp; cachedData.component\n      if (cachedComponent) {\n        if (cachedData.configProps) {\n          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)\n        }\n        return h(cachedComponent, data, children)\n      } else {\n        return h()\n      }\n    }\n\n    var matched = route.matched[depth]\n    var component = matched &amp;&amp; matched.components[name]\n    \n    if (!matched || !component) {\n      cache[name] = null\n      return h()\n    }\n    \n    cache[name] = { component: component }\n    \n    data.registerRouteInstance = function(vm, val) {\n      var current = matched.instances[name]\n      if (\n        (val &amp;&amp; current !== vm) ||\n        (!val &amp;&amp; current === vm)\n      ) {\n        matched.instances[name] = val\n      }\n    }\n    \n    (data.hook || (data.hook = {})).prepatch = function(_, vnode) {\n      matched.instances[name] = vnode.componentInstance\n    }\n    \n    data.hook.init = function(vnode) {\n      if (vnode.data.keepAlive &amp;&amp;\n        vnode.componentInstance &amp;&amp;\n        vnode.componentInstance !== matched.instances[name]\n      ) {\n        matched.instances[name] = vnode.componentInstance\n      }\n      \n      handleRouteEntered(route)\n    }\n\n    var configProps = matched.props &amp;&amp; matched.props[name]\n    if (configProps) {\n      extend(cache[name], {\n        route: route,\n        configProps: configProps\n      })\n      fillPropsinData(component, data, route, configProps)\n    }\n\n    return h(component, data, children)\n  }\n}\n</code></pre><p><code>RouterView</code> 组件的渲染逻辑和新版本的 <code>vue-router-next</code> 实现一致：根据路径 <code>route</code> 和当前 <code>RouterView</code> 嵌套的深度来匹配路由配置中对应的路由组件并渲染。</p>\n<p>不同的是，3.x 版本的 <code>vue-router</code> 处理了 <code>KeepAlive</code> 的情况：如果当前的 <code>RouterView</code> 组件所在的父组件实例身处 <code>KeepAlive</code> 构造的树中，且是 <code>inactive</code> 状态，那么它只会被渲染成上一次渲染的视图。</p>\n<p>因此这里有两个关键的点：一是能够判断当前所处的环境，二是需要缓存 <code>RouterView</code> 上一次渲染的视图。</p>\n<p>显然，在 <code>vue-router-next</code> 中，是没有对应的逻辑的，主要是因为组件实例中没有存储 <code>KeepAlive</code> 组件相关的 <code>inactive</code> 状态，<code>RouterView</code> 组件也没法知道自己当前所处的环境。</p>\n<p>在我看来，如果 <code>vue-router-next</code> 想要解决这个问题，可能还牵涉到 Vue3 内部的一些改动，提供更多的信息数据，让 <code>RouterView</code> 组件在渲染的时候能够知道自己当前所处的环境。</p>\n<p>目前解决这个问题的一个办法是在这种嵌套路由的场景下，不使用 <code>KeepAlive</code> 包裹 <code>RouterView</code>。</p>\n<h2>总结</h2>\n<p>我们在做技术选型时，其实就要考虑到这层风险，当开源项目出现 bug 或者不能满足你的需求，且不能很快的响应时，你有没有办法帮助开源项目共建，或者通过魔改的方式来解决遇到的问题。</p>\n<p>我司使用的 Vue2 CSP 版本，就是基于 Vue.js 2.6.11 版本基础上魔改的，社区不提供支持，就需要自己动手了。</p>\n<p>对于开源项目的维护者而言，他们自然有自己的计划和考量，你不能因为自己的项目紧急就要求维护者立马帮你解决问题。当然，实在想寻求紧急帮助，情商高一点的做法是给开源维护者捐赠，通过付费的方式可能会提升 bug 被处理的优先级。</p>\n<p>当然，最靠谱的方式还是让自己靠谱起来，遇到 bug 后不要慌，先定位到出现 bug 的根本原因，然后找到合适的解决方案。</p>\n<p>如果你此时正在用 Vue3 开发项目，那么请务必注意这个 bug，有能力的可以好好研究，如果给 Vue3 提个 pull request 就更棒了。相比于改改拼写错误混个 contributor，我觉得能解决这类问题的人才能算真正意义上的 contributor。<p>\n版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n作者: 黄轶\n原文链接：<a href=\"https://juejin.im/post/7016628275074039815\">https://juejin.im/post/7016628275074039815</a>\n</p></p>\n</div>","title":"是什么事让尤大如此生气？__Vue.js__前端框架__JavaScript","last_reply_at":"2022-01-18T02:15:00.665Z","good":false,"top":false,"reply_count":0,"visit_count":3954,"create_at":"2022-01-18T02:15:00.665Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e61494cbbfd1003b11fa16","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><hr>\n<h2>theme: smartblue</h2>\n<p>这是我参与11月更文挑战的第1天，活动详情查看：<a href=\"https://juejin.cn/post/7023643374569816095\" title=\"https://juejin.cn/post/7023643374569816095\">2021最后一次更文挑战</a></p>\n<p>前一段时间一直在研究 Vue 源码，2和3的源码变化还是挺大的，但是 Vue3 在开发上需要改变以往习惯的地方真没什么，毕竟基础概念是一模一样的，可偏偏 3 的项目写起来就是能让人感觉爽得多，不动手是真体会不到</p>\n<p>所以咯，一定要动手！敲代码！不能学而不用，这是不可取的</p>\n<p>这里我总结了几个 Vue3 的知识点，如果你是 Vue2 的迁移者、学习 Vue3 或者准备面试的话，相信看完本文一定会有所收获</p>\n<h2>Vue3 有哪些变化</h2>\n<p>Vue3 是怎么得更快的？</p>\n<ul>\n<li>新增了三个组件：<code>Fragment</code> 支持多个根节点、<code>Suspense</code> 可以在组件渲染之前的等待时间显示指定内容、<code>Teleport</code> 可以让子组件能够在视觉上跳出父组件(如父组件overflow:hidden)</li>\n<li>新增指令 <code>v-memo</code>，可以缓存 html 模板，比如 v-for 列表不会变化的就缓存，简单说就是用内存换时间</li>\n<li>支持 <code>Tree-Shaking</code>，会在打包时去除一些无用代码，没有用到的模块，使得代码打包体积更小</li>\n<li>新增 <code>Composition API</code> 可以更好的逻辑复用和代码组织，同一功能的代码不至于像以前一样太分散，虽然 Vue2 中可以用 minxin 来实现复用代码，但也存在问题，比如方法或属性名会冲突，代码来源也不清楚等</li>\n<li>用 <code>Proxy</code> 代替 Object.defineProperty 重构了响应式系统，可以监听到数组下标变化，及对象新增属性，因为监听的不是对象属性，而是对象本身，还可拦截 apply、has 等13种方法</li>\n<li>重构了虚拟 DOM，在编译时会将事件缓存、将 slot 编译为 lazy 函数、保存静态节点直接复用(静态提升)、以及添加静态标记、Diff 算法使用 最长递增子序列 优化了对比流程，使得虚拟 DOM 生成速度提升 <code>200%</code></li>\n<li>支持在 <code>&lt;style&gt;&lt;/style&gt;</code> 里使用 <code>v-bind</code>，给 CSS 绑定 JS 变量(<code>color: v-bind(str)</code>)</li>\n<li>用 <code>setup</code> 代替了 beforeCreate 和 created 这两个生命周期</li>\n<li>新增了<strong>开发环境</strong>的两个钩子函数，在组件更新时 <code>onRenderTracked</code> 会跟踪组件里所有变量和方法的变化、每次触发渲染时 <code>onRenderTriggered</code> 会返回发生变化的新旧值，可以让我们进行有针对性调试</li>\n<li>毕竟 Vue3 是用 <code>TS</code> 写的，所以对 <code>TS</code> 的支持度更好</li>\n<li>Vue3 不兼容 <code>IE11</code></li>\n</ul>\n<h2>说一下 Composition API</h2>\n<p>和 Options API 的区别？</p>\n<p><code>Composition API</code> 也叫<strong>组合式 API</strong>，它主要就是为了解决 Vue2 中 Options API 的问题。</p>\n<p>一是在 Vue2 中只能固定用 <code>data</code>、<code>computed</code>、<code>methods</code> 等选项组织代码，在组件越来越复杂的时候，一个功能相关的属性和方法就会在文件上中下到处都有，很分散，变越来越难维护</p>\n<p>二是 Vue2 中虽然可以用 <code>minxin</code> 来做逻辑的提取复用，但是 minxin 里的属性和方法名会和组件内部的命名冲突，还有当引入多个 minxin 的时候，我们使用的属性或方法是来于哪个 minxin 也不清楚</p>\n<p>而 <code>Composition API</code> 刚才就解决了这两个问题，可以让我们自由的组织代码，同一功能相关的全部放在一起，代码有更好的可读性更便于维护，单独提取出来也不会造成命名冲突，所以也有更好的可扩展性</p>\n<h2>说一下 setup</h2>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; 方法\nsetup(props, context){ return { name:&#x27;沐华&#x27; } }\n\n&#x2F;&#x2F; 语法糖\n&lt;script setup&gt; ... &lt;&#x2F;script&gt;\n</code></pre><p><code>setup()</code> 方法是在 <code>beforeCreate()</code> 生命周期函数之前执行的函数；</p>\n<p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f6372e90f1f41e3b95b9a69da505470~tplv-k3u1fbpfcp-watermark.image\" alt></p>\n<p>它接收两个参数 <code>props</code> 和 <code>context</code>。它里面不能使用 <code>this</code>，而是通过 context 对象来代替当前执行上下文绑定的对象，context 对象有四个属性：<code>attrs</code>、<code>slots</code>、<code>emit</code>、<code>expose</code></p>\n<p>里面通过 <code>ref</code> 和 <code>reactive</code> 代替以前的 data 语法，<code>return</code> 出去的内容，可以在模板直接使用，包括变量和方法</p>\n<p>而使用 <code>setup</code> 语法糖时，不用写 <code>export default {}</code>，子组件只需要 <code>import</code> 就直接使用，不需要像以前一样在 components 里注册，属性和方法也不用 return。</p>\n<p>并且里面不需要用 <code>async</code> 就可以直接使用 <code>await</code>，因为这样默认会把组件的 <code>setup</code> 变为 <code>async setup</code></p>\n<p>用语法糖时，props、attrs、slots、emit、expose 的获取方式也不一样了</p>\n<p>3.0~3.2版本变成了通过 import 引入的 API：<code>defineProps</code>、<code>defineEmit</code>、<code>useContext</code>(在3.2版本已废弃)，useContext 的属性 <code>{ emit, attrs, slots, expose }</code></p>\n<p>3.2+版本不需要引入，而直接调用：<code>defineProps</code>、<code>defineEmits</code>、<code>defineExpose</code>、<code>useSlots</code>、<code>useAttrs</code></p>\n<h2>watch 和 watchEffect 的区别</h2>\n<p><code>watch</code> 作用是对传入的某个或多个值的变化进行监听；触发时会返回新值和老值；也就是说第一次不会执行，只有变化时才会重新执行</p>\n<pre class=\"prettyprint language-js\"><code>const name = ref(&#x27;沐华&#x27;)\nwatch(name, (newValue, oldValue)=&gt;{ ... }, {immediate:true, deep:true})\n\n&#x2F;&#x2F; 响应式对象\nconst boy = reactive({ age:18 })\nwatch(()=&gt;boy.age, (newValue, oldValue)=&gt;{ ... })\n\n&#x2F;&#x2F; 监听多个\nwatch( [name, ()=&gt;boy.age], (newValue, oldValue)=&gt;{ ... } )\n</code></pre><p><code>watchEffect</code> 是传入一个立即执行函数，所以默认第一次也会执行一次；不需要传入监听内容，会自动收集函数内的数据源作为依赖，在依赖变化的时候又会重新执行该函数，如果没有依赖就不会执行；而且不会返回变化前后的新值和老值</p>\n<pre class=\"prettyprint language-js\"><code>watchEffect(onInvalidate =&gt;{ ... })\n</code></pre><p>共同点是 <code>watch</code> 和 <code>watchEffect</code> 会共享以下四种行为：</p>\n<ul>\n<li><code>停止监听</code>：组件卸载时都会自动停止监听</li>\n<li><code>清除副作用</code>：onInvalidate 会作为回调的第三个参数传入</li>\n<li><code>副作用刷新时机</code>：响应式系统会缓存副作用函数，并异步刷新，避免同一个 tick 中多个状态改变导致的重复调用</li>\n<li><code>监听器调试</code>：开发模式下可以用 onTrack 和 onTrigger 进行调试</li>\n</ul>\n<p><a href=\"https://v3.cn.vuejs.org/guide/reactivity-computed-watchers.html\">官方文档描述</a> 如下\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/39daec0928154f9cad014224020ec9a4~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h2>Vue3 响应式原理和 Vue2 的区别</h2>\n<p>众所周知 <code>Vue2</code> 数据响应式是通过 <code>Object.defineProperty()</code> 劫持各个属性 getter 和 setter，在数据变化时发布消息给订阅者，触发相应的监听回调，而这之间存在几个问题</p>\n<ul>\n<li>初始化时需要遍历对象所有 key，如果对象层次较深，性能不好</li>\n<li>通知更新过程需要维护大量 dep 实例和 watcher 实例，额外占用内存较多</li>\n<li>Object.defineProperty 无法监听到数组元素的变化，只能通过劫持重写数方法</li>\n<li>动态新增，删除对象属性无法拦截，只能用特定 set/delete API 代替</li>\n<li>不支持 Map、Set 等数据结构</li>\n</ul>\n<p>而在 <code>Vue3</code> 中为了解决这些问题，使用原生的 <code>proxy</code> 代替，支持监听对象和数组的变化，并且多达13种拦截方法，动态属性增删都可以拦截，新增数据结构全部支持，对象嵌套属性只代理第一层，运行时递归，用到才代理，也不需要维护特别多的依赖关系，性能取得很大进步</p>\n<p>更多细节和源码剖析可以看我另一篇文章 <a href=\"https://juejin.cn/post/7021046293627666463\">Vue3.2 响应式原理源码剖析，及与 Vue2响应式的区别</a></p>\n<h2>defineProperty 和 Proxy 的区别</h2>\n<p>为什么要用 Proxy 代替 defineProperty ？好在哪里？</p>\n<ul>\n<li>Object.defineProperty 是 Es5 的方法，Proxy 是 Es6 的方法</li>\n<li>defineProperty 不能监听到数组下标变化和对象新增属性，Proxy 可以</li>\n<li>defineProperty 是劫持对象属性，Proxy 是代理整个对象</li>\n<li>defineProperty 局限性大，只能针对单属性监听，所以在一开始就要全部递归监听。Proxy 对象嵌套属性运行时递归，用到才代理，也不需要维护特别多的依赖关系，性能提升很大，且首次渲染更快</li>\n<li>defineProperty 会污染原对象，修改时是修改原对象，Proxy 是对原对象进行代理并会返回一个新的代理对象，修改的是代理对象</li>\n<li>defineProperty 不兼容 IE8，Proxy 不兼容 IE11</li>\n</ul>\n<h2>Vue3 的生命周期</h2>\n<p>基本上就是在 Vue2 生命周期钩子函数名基础上加了 <code>on</code>；beforeDestory 和 destoryed 更名为 onBeforeUnmount 和 onUnmounted；然后删了两个钩子函数 beforeCreate 和 created；新增了两个开发环境用于调试的钩子</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/60f518fa89ba44bb8b59911ecd65a8a4~tplv-k3u1fbpfcp-watermark.image?\" alt=\"未命名文件 (1).png\"></p>\n<h2>Vue3 的 8 种组件通信</h2>\n<p>可以看我另一文章，就不搬过来了 <a href=\"https://juejin.cn/post/6999687348120190983\">Vue3的8种和Vue2的12种组件通信，值得收藏</a></p>\n<h2>Vue3 Diff算法和 Vue2 的区别</h2>\n<p>我们知道在数据变更触发页面重新渲染，会生成虚拟 DOM 并进行 patch 过程，这一过程在 Vue3 中的优化有如下</p>\n<p>编译阶段的优化：</p>\n<ul>\n<li>事件缓存：将事件缓存(如: @click)，可以理解为变成静态的了</li>\n<li>静态提升：第一次创建静态节点时保存，后续直接复用</li>\n<li>添加静态标记：给节点添加静态标记，以优化 Diff 过程</li>\n</ul>\n<p>由于编译阶段的优化，除了能更快的生成虚拟 DOM 以外，还使得 Diff 时可以跳过&quot;永远不会变化的节点&quot;，Diff 优化如下</p>\n<ul>\n<li>Vue2 是全量 Diff，Vue3 是静态标记 + 非全量 Diff</li>\n<li>使用最长递增子序列优化了对比流程</li>\n</ul>\n<p>根据尤大公布的数据就是 Vue3 <code>update</code> 性能提升了 <code>1.3~2 倍</code></p>\n<p>源码剖析可以看我另一篇文章 <a href=\"https://juejin.cn/post/7010594233253888013\">深入浅出虚拟 DOM 和 Diff 算法，及 Vue2 与 Vue3 中的区别</a></p>\n<h2>往期精彩</h2>\n<ul>\n<li><a href=\"https://juejin.cn/post/7021021063647592456\">12 个 Vue 开发中的性能优化小技巧</a></li>\n<li><a href=\"https://juejin.cn/post/7021688091513454622\">一次弄懂 Vue2 和 Vue3 的 nextTick 实现原理</a></li>\n<li><a href=\"https://juejin.cn/post/7021046293627666463\">Vue3.2 响应式原理源码剖析，及与 Vue2 .x响应式的区别</a></li>\n<li><a href=\"https://juejin.cn/editor/drafts/7009900485691834398\">深入浅出 Vue2 响应式原理源码剖析</a></li>\n<li><a href=\"https://juejin.cn/post/7010594233253888013\">深入浅出虚拟 DOM 和 Diff 算法，及 Vue2 与 Vue3 中的区别</a></li>\n<li><a href=\"https://juejin.cn/post/6999687348120190983\">Vue3的8种和Vue2的12种组件通信，值得收藏</a></li>\n<li><a href=\"https://juejin.cn/post/6995591618371780622\">JavaScript进阶知识点</a></li>\n<li><a href=\"https://juejin.cn/post/6994629873985650696\">20分钟助你拿下HTTP和HTTPS，巩固你的HTTP知识体系</a></li>\n</ul>\n<h2>结语</h2>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98f3b95c5cd94175bdc2db5b2055c5c3~tplv-k3u1fbpfcp-watermark.image\" alt></p>\n<p>老铁，求点赞啊~~</p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: 沐华\n        原文链接：<a href=\"https://juejin.im/post/7026249448233631752\">https://juejin.im/post/7026249448233631752</a>\n      </p></div>","title":"聊一聊 Vue3 的 9 个知识点__前端__Vue.js__面试","last_reply_at":"2022-01-18T01:15:00.831Z","good":false,"top":false,"reply_count":0,"visit_count":3830,"create_at":"2022-01-18T01:15:00.831Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e587f4ad745b003c9df78d","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><hr>\n<h2>highlight: darcula\ntheme: smartblue</h2>\n<p>本文已参与「<a href=\"https://juejin.cn/post/7012210233804079141/\" title=\"https://juejin.cn/post/7012210233804079141/\">掘力星计划</a>」，赢取创作大礼包，挑战创作激励金。</p>\n<h2>1. 前言</h2>\n<blockquote>\n<p>大家好，我是<a href=\"https://lxchuan12.gitee.io\">若川</a>。欢迎关注我的<a href=\"https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/13/16efe57ddc7c9eb3~tplv-t2oaga2asx-image.image\" title=\"https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/13/16efe57ddc7c9eb3~tplv-t2oaga2asx-image.image\">公众号若川视野</a>，最近组织了<a href=\"https://www.yuque.com/ruochuan12/notice\"><strong>源码共读活动</strong></a>，感兴趣的可以加我微信 <a href=\"https://juejin.cn/pin/7005372623400435725\">ruochuan12</a> 参与，已进行两个多月，大家一起交流学习，共同进步。</p>\n</blockquote>\n<p>想学源码，极力推荐之前我写的<a href=\"https://juejin.cn/column/6960551178908205093\">《学习源码整体架构系列》</a> 包含<code>jQuery</code>、<code>underscore</code>、<code>lodash</code>、<code>vuex</code>、<code>sentry</code>、<code>axios</code>、<code>redux</code>、<code>koa</code>、<code>vue-devtools</code>、<code>vuex4</code>、<code>koa-compose</code>、<code>vue-next-release</code>、<code>vue-this</code>等十余篇源码文章。</p>\n<blockquote>\n<p>美国时间 2021 年 10 月 7 日早晨，Vue 团队等主要贡献者举办了一个 <code>Vue Contributor Days</code> 在线会议，<a href=\"https://github.com/sodatea\">蒋豪群</a>（<a href=\"https://www.zhihu.com/people/sodatea\">知乎胖茶</a>，Vue.js 官方团队成员，Vue-CLI 核心开发），在会上公开了<a href=\"https://github.com/vuejs/create-vue\"><code>create-vue</code></a>，一个全新的脚手架工具。</p>\n</blockquote>\n<p><code>create-vue</code>使用<code>npm init vue@next</code>一行命令，就能<code>快如闪电般</code>初始化好基于<code>vite</code>的<code>Vue3</code>项目。</p>\n<p>本文就是通过调试和大家一起学习这个300余行的源码。</p>\n<p>阅读本文，你将学到：</p>\n<pre class=\"prettyprint language-sh\"><code>1. 学会全新的官方脚手架工具 create-vue 的使用和原理\n2. 学会使用 VSCode 直接打开 github 项目\n3. 学会使用测试用例调试源码\n4. 学以致用，为公司初始化项目写脚手架工具。\n5. 等等\n</code></pre><h2>2. 使用 npm init vue@next 初始化 vue3 项目</h2>\n<p><a href=\"https://github.com/vuejs/create-vue\">create-vue github README</a>上写着，<code>An easy way to start a Vue project</code>。一种简单的初始化vue项目的方式。</p>\n<pre class=\"prettyprint language-sh\"><code>npm init vue@next\n</code></pre><p>估计大多数读者，第一反应是<strong>这样竟然也可以，这么简单快捷？</strong></p>\n<p>忍不住想动手在控制台输出命令，我在终端试过，见下图。</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c91fdfddab854e0d8846cb65f6df9b54~tplv-k3u1fbpfcp-watermark.image?\" alt=\"npm-init-vue@next\"></p>\n<p>最终<code>cd vue3-project</code>、<code>npm install</code> 、<code>npm run dev</code>打开页面<a href=\"http://localhost:3000\">http://localhost:3000</a>。</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3c6cdb2cf6c8452e92e451e24f1bbb7c~tplv-k3u1fbpfcp-watermark.image?\" alt=\"初始化页面\"></p>\n<h3>2.1 npm init &amp;&amp; npx</h3>\n<p>为啥 <code>npm init</code> 也可以直接初始化一个项目，带着疑问，我们翻看 <code>npm</code> 文档。</p>\n<p><a href=\"https://docs.npmjs.com/cli/v6/commands/npm-init\">npm init</a></p>\n<p>npm init 用法：</p>\n<pre class=\"prettyprint language-sh\"><code>npm init [--force|-f|--yes|-y|--scope]\nnpm init &lt;@scope&gt; (same as &#96;npx &lt;@scope&gt;&#x2F;create&#96;)\nnpm init [&lt;@scope&gt;&#x2F;]&lt;name&gt; (same as &#96;npx [&lt;@scope&gt;&#x2F;]create-&lt;name&gt;&#96;)\n</code></pre><p><code>npm init &lt;initializer&gt;</code> 时转换成<code>npx</code>命令：</p>\n<ul>\n<li>npm init foo -&gt; npx create-foo</li>\n<li>npm init @usr/foo -&gt; npx @usr/create-foo</li>\n<li>npm init @usr -&gt; npx @usr/create</li>\n</ul>\n<p>看完文档，我们也就理解了：</p>\n<pre class=\"prettyprint language-sh\"><code># 运行\nnpm init vue@next\n# 相当于\nnpx create-vue@next\n</code></pre><p>我们可以在这里<a href=\"https://registry.npmjs.org/create-vue\">create-vue</a>，找到一些信息。或者在<a href=\"https://www.npmjs.com/package/create-vue\">npm create-vue</a>找到版本等信息。</p>\n<p>其中<code>@next</code>是指定版本，通过<code>npm dist-tag ls create-vue</code>命令可以看出，<code>next</code>版本目前对应的是<code>3.0.0-beta.6</code>。</p>\n<pre class=\"prettyprint language-sh\"><code>npm dist-tag ls create-vue\n- latest: 3.0.0-beta.6\n- next: 3.0.0-beta.6\n</code></pre><p>发布时 <code>npm publish --tag next</code> 这种写法指定 <code>tag</code>。默认标签是<code>latest</code>。</p>\n<p>可能有读者对 <code>npx</code> 不熟悉，这时找到<a href=\"http://www.ruanyifeng.com/blog/2019/02/npx.html\">阮一峰老师博客 npx 介绍</a>、<a href=\"http://nodejs.cn/learn/the-npx-nodejs-package-runner\">nodejs.cn npx</a></p>\n<p><code>npx 是一个非常强大的命令，从 npm 的 5.2 版本（发布于 2017 年 7 月）开始可用。</code></p>\n<p>简单说下容易忽略且常用的场景，<code>npx</code>有点类似小程序提出的随用随走。</p>\n<p><strong>轻松地运行本地命令</strong></p>\n<pre class=\"prettyprint language-sh\"><code>node_modules&#x2F;.bin&#x2F;vite -v\n# vite&#x2F;2.6.5 linux-x64 node-v14.16.0\n\n# 等同于\n# package.json script: &quot;vite -v&quot;\n# npm run vite\n\nnpx vite -v\n# vite&#x2F;2.6.5 linux-x64 node-v14.16.0\n</code></pre><p><strong>使用不同的 Node.js 版本运行代码</strong>\n某些场景下可以临时切换 <code>node</code> 版本，有时比 <code>nvm</code> 包管理方便些。</p>\n<pre class=\"prettyprint language-sh\"><code>npx node@14 -v\n# v14.18.0\n\nnpx -p node@14 node -v \n# v14.18.0\n</code></pre><p><strong>无需安装的命令执行</strong></p>\n<pre class=\"prettyprint language-sh\"><code># 启动本地静态服务\nnpx http-server\n</code></pre><pre class=\"prettyprint language-sh\"><code># 无需全局安装\nnpx @vue&#x2F;cli create vue-project\n# @vue&#x2F;cli 相比 npm init vue@next npx create-vue@next 很慢。\n\n# 全局安装\nnpm i -g @vue&#x2F;cli\nvue create vue-project\n</code></pre><p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ab5eef2c6a74801b021aeab5b6e5832~tplv-k3u1fbpfcp-watermark.image?\" alt=\"npx-vue-cli\"></p>\n<p><code>npm init vue@next</code> （<code>npx create-vue@next</code>） 快的原因，主要在于依赖少（能不依赖包就不依赖），源码行数少，目前<code>index.js</code>只有300余行。</p>\n<h2>3. 配置环境调试源码</h2>\n<h3>3.1 克隆 create-vue 项目</h3>\n<p><a href=\"https://github.com/lxchuan12/create-vue-analysis.git\">本文仓库地址 create-vue-analysis</a>，求个<code>star</code>~</p>\n<pre class=\"prettyprint language-sh\"><code># 可以直接克隆我的仓库，我的仓库保留的 create-vue 仓库的 git 记录\ngit clone https:&#x2F;&#x2F;github.com&#x2F;lxchuan12&#x2F;create-vue-analysis.git\ncd create-vue-analysis&#x2F;create-vue\nnpm i\n</code></pre><p>当然不克隆也可以直接用 <code>VSCode</code> 打开我的仓库\n<a href=\"https://open.vscode.dev/lxchuan12/create-vue-analysis\"><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b3be78836f24b24aff91caeaadaf246~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"Open in Visual Studio Code\"></a></p>\n<p>顺带说下：我是怎么保留 <code>create-vue</code> 仓库的 <code>git</code> 记录的。</p>\n<pre class=\"prettyprint language-sh\"><code># 在 github 上新建一个仓库 &#96;create-vue-analysis&#96; 克隆下来\ngit clone https:&#x2F;&#x2F;github.com&#x2F;lxchuan12&#x2F;create-vue-analysis.git\ncd create-vue-analysis\ngit subtree add --prefix=create-vue https:&#x2F;&#x2F;github.com&#x2F;vuejs&#x2F;create-vue.git main\n# 这样就把 create-vue 文件夹克隆到自己的 git 仓库了。且保留的 git 记录\n</code></pre><p>关于更多 <code>git subtree</code>，可以看<a href=\"https://segmentfault.com/a/1190000003969060\">Git Subtree 简明使用手册</a></p>\n<h3>3.2 package.json 分析</h3>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; create-vue&#x2F;package.json\n{\n  &quot;name&quot;: &quot;create-vue&quot;,\n  &quot;version&quot;: &quot;3.0.0-beta.6&quot;,\n  &quot;description&quot;: &quot;An easy way to start a Vue project&quot;,\n  &quot;type&quot;: &quot;module&quot;,\n  &quot;bin&quot;: {\n    &quot;create-vue&quot;: &quot;outfile.cjs&quot;\n  },\n}\n</code></pre><p><code>bin</code>指定可执行脚本。也就是我们可以使用 <code>npx create-vue</code> 的原因。</p>\n<p><code>outfile.cjs</code> 是打包输出的<code>JS</code>文件</p>\n<pre class=\"prettyprint language-js\"><code>{\n  &quot;scripts&quot;: {\n    &quot;build&quot;: &quot;esbuild --bundle index.js --format=cjs --platform=node --outfile=outfile.cjs&quot;,\n    &quot;snapshot&quot;: &quot;node snapshot.js&quot;,\n    &quot;pretest&quot;: &quot;run-s build snapshot&quot;,\n    &quot;test&quot;: &quot;node test.js&quot;\n  },\n}\n</code></pre><p>执行 <code>npm run test</code> 时，会先执行钩子函数 <code>pretest</code>。<code>run-s</code> 是 <a href=\"https://github.com/mysticatea/npm-run-all/blob/HEAD/docs/run-s.md\">npm-run-all</a> 提供的命令。<code>run-s build snapshot</code> 命令相当于 <code>npm run build &amp;&amp; npm run snapshot</code>。</p>\n<p>根据脚本提示，我们来看 <code>snapshot.js</code> 文件。</p>\n<h3>3.3 生成快照 snapshot.js</h3>\n<p>这个文件主要作用是根据<code>const featureFlags = ['typescript', 'jsx', 'router', 'vuex', 'with-tests']</code> 组合生成<code>31种</code>加上 <code>default</code> 共计 <code>32种</code> 组合，生成快照在 <code>playground</code>目录。</p>\n<p>因为打包生成的 <code>outfile.cjs</code> 代码有做一些处理，不方便调试，我们可以修改为<code>index.js</code>便于调试。</p>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; 路径 create-vue&#x2F;snapshot.js\nconst bin = path.resolve(__dirname, &#x27;.&#x2F;outfile.cjs&#x27;)\n&#x2F;&#x2F; 改成 index.js 便于调试\nconst bin = path.resolve(__dirname, &#x27;.&#x2F;index.js&#x27;)\n</code></pre><p>我们可以在<code>for</code>和 <code>createProjectWithFeatureFlags</code> 打上断点。</p>\n<p><code>createProjectWithFeatureFlags</code>其实类似在终端输入如下执行这样的命令</p>\n<pre class=\"prettyprint language-sh\"><code>node .&#x2F;index.js --xxx --xxx --force\n</code></pre><pre class=\"prettyprint language-js\"><code>function createProjectWithFeatureFlags(flags) {\n  const projectName = flags.join(&#x27;-&#x27;)\n  console.log(&#96;Creating project ${projectName}&#96;)\n  const { status } = spawnSync(\n    &#x27;node&#x27;,\n    [bin, projectName, ...flags.map((flag) =&gt; &#96;--${flag}&#96;), &#x27;--force&#x27;],\n    {\n      cwd: playgroundDir,\n      stdio: [&#x27;pipe&#x27;, &#x27;pipe&#x27;, &#x27;inherit&#x27;]\n    }\n  )\n\n  if (status !== 0) {\n    process.exit(status)\n  }\n}\n\n&#x2F;&#x2F; 路径 create-vue&#x2F;snapshot.js\nfor (const flags of flagCombinations) {\n  createProjectWithFeatureFlags(flags)\n}\n</code></pre><blockquote>\n<p><strong>调试</strong>：<code>VSCode</code>打开项目，<code>VSCode</code>高版本(1.50+)可以在 <code>create-vue/package.json</code> =&gt; <code>scripts</code> =&gt; <code>&quot;test&quot;: &quot;node test.js&quot;</code>。鼠标悬停在<code>test</code>上会有调试脚本提示，选择调试脚本。如果对调试不熟悉，可以看我之前的文章<a href=\"https://lxchuan12.gitee.io/koa-compose\">koa-compose</a>，写的很详细。</p>\n</blockquote>\n<p>调试时，大概率你会遇到：<code>create-vue/index.js</code> 文件中，<code>__dirname</code> 报错问题。可以按照如下方法解决。在 <code>import</code> 的语句后，添加如下语句，就能愉快的调试了。</p>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; 路径 create-vue&#x2F;index.js\n&#x2F;&#x2F; 解决办法和nodejs issues\n&#x2F;&#x2F; https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;64383909&#x2F;dirname-is-not-defined-in-node-14-version\n&#x2F;&#x2F; https:&#x2F;&#x2F;github.com&#x2F;nodejs&#x2F;help&#x2F;issues&#x2F;2907\n\nimport { fileURLToPath } from &#x27;url&#x27;;\nimport { dirname } from &#x27;path&#x27;;\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n</code></pre><p>接着我们调试 index.js 文件，来学习。</p>\n<h2>4. 调试 index.js 主流程</h2>\n<p>回顾下上文 <code>npm init vue@next</code> 初始化项目的。</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c91fdfddab854e0d8846cb65f6df9b54~tplv-k3u1fbpfcp-watermark.image?\" alt=\"npm-init-vue@next\"></p>\n<p>单从初始化项目输出图来看。主要是三个步骤。</p>\n<pre class=\"prettyprint language-sh\"><code>1. 输入项目名称，默认值是 vue-project\n2. 询问一些配置 渲染模板等\n3. 完成创建项目，输出运行提示\n</code></pre><pre class=\"prettyprint language-js\"><code>async function init() {\n  &#x2F;&#x2F; 省略放在后文详细讲述\n}\n\n&#x2F;&#x2F; async 函数返回的是Promise 可以用 catch 报错\ninit().catch((e) =&gt; {\n  console.error(e)\n})\n</code></pre><h3>4.1 解析命令行参数</h3>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; 返回运行当前脚本的工作目录的路径。\nconst cwd = process.cwd()\n&#x2F;&#x2F; possible options:\n&#x2F;&#x2F; --default\n&#x2F;&#x2F; --typescript &#x2F; --ts\n&#x2F;&#x2F; --jsx\n&#x2F;&#x2F; --router &#x2F; --vue-router\n&#x2F;&#x2F; --vuex\n&#x2F;&#x2F; --with-tests &#x2F; --tests &#x2F; --cypress\n&#x2F;&#x2F; --force (for force overwriting)\nconst argv = minimist(process.argv.slice(2), {\n    alias: {\n        typescript: [&#x27;ts&#x27;],\n        &#x27;with-tests&#x27;: [&#x27;tests&#x27;, &#x27;cypress&#x27;],\n        router: [&#x27;vue-router&#x27;]\n    },\n    &#x2F;&#x2F; all arguments are treated as booleans\n    boolean: true\n})\n</code></pre><p><a href=\"https://github.com/substack/minimist\">minimist</a></p>\n<p>简单说，这个库，就是解析命令行参数的。看例子，我们比较容易看懂传参和解析结果。</p>\n<pre class=\"prettyprint language-bash\"><code>$ node example&#x2F;parse.js -a beep -b boop\n{ _: [], a: &#x27;beep&#x27;, b: &#x27;boop&#x27; }\n\n$ node example&#x2F;parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz\n{ _: [ &#x27;foo&#x27;, &#x27;bar&#x27;, &#x27;baz&#x27; ],\n  x: 3,\n  y: 4,\n  n: 5,\n  a: true,\n  b: true,\n  c: true,\n  beep: &#x27;boop&#x27; }\n</code></pre><p>比如</p>\n<pre class=\"prettyprint language-sh\"><code>npm init vue@next --vuex --force\n</code></pre><h3>4.2 如果设置了 feature flags 跳过 prompts 询问</h3>\n<p>这种写法方便代码测试等。直接跳过交互式询问，同时也可以省时间。</p>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; if any of the feature flags is set, we would skip the feature prompts\n  &#x2F;&#x2F; use &#96;??&#96; instead of &#96;||&#96; once we drop Node.js 12 support\n  const isFeatureFlagsUsed =\n    typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) ===\n    &#x27;boolean&#x27;\n\n&#x2F;&#x2F; 生成目录\n  let targetDir = argv._[0]\n  &#x2F;&#x2F; 默认 vue-projects\n  const defaultProjectName = !targetDir ? &#x27;vue-project&#x27; : targetDir\n  &#x2F;&#x2F; 强制重写文件夹，当同名文件夹存在时\n  const forceOverwrite = argv.force\n</code></pre><h3>4.3 交互式询问一些配置</h3>\n<p>如上文<code>npm init vue@next</code> 初始化的图示</p>\n<ul>\n<li>输入项目名称</li>\n<li>还有是否删除已经存在的同名目录</li>\n<li>询问使用需要 JSX Router vuex cypress 等。</li>\n</ul>\n<pre class=\"prettyprint language-js\"><code>let result = {}\n\n  try {\n    &#x2F;&#x2F; Prompts:\n    &#x2F;&#x2F; - Project name:\n    &#x2F;&#x2F;   - whether to overwrite the existing directory or not?\n    &#x2F;&#x2F;   - enter a valid package name for package.json\n    &#x2F;&#x2F; - Project language: JavaScript &#x2F; TypeScript\n    &#x2F;&#x2F; - Add JSX Support?\n    &#x2F;&#x2F; - Install Vue Router for SPA development?\n    &#x2F;&#x2F; - Install Vuex for state management? (TODO)\n    &#x2F;&#x2F; - Add Cypress for testing?\n    result = await prompts(\n      [\n        {\n          name: &#x27;projectName&#x27;,\n          type: targetDir ? null : &#x27;text&#x27;,\n          message: &#x27;Project name:&#x27;,\n          initial: defaultProjectName,\n          onState: (state) =&gt; (targetDir = String(state.value).trim() || defaultProjectName)\n        },\n        &#x2F;&#x2F; 省略若干配置\n        {\n          name: &#x27;needsTests&#x27;,\n          type: () =&gt; (isFeatureFlagsUsed ? null : &#x27;toggle&#x27;),\n          message: &#x27;Add Cypress for testing?&#x27;,\n          initial: false,\n          active: &#x27;Yes&#x27;,\n          inactive: &#x27;No&#x27;\n        }\n      ],\n      {\n        onCancel: () =&gt; {\n          throw new Error(red(&#x27;✖&#x27;) + &#x27; Operation cancelled&#x27;)\n        }\n      }\n    ]\n    )\n  } catch (cancelled) {\n    console.log(cancelled.message)\n    &#x2F;&#x2F; 退出当前进程。\n    process.exit(1)\n  }\n</code></pre><h3>4.4 初始化询问用户给到的参数，同时也会给到默认值</h3>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; &#96;initial&#96; won&#x27;t take effect if the prompt type is null\n  &#x2F;&#x2F; so we still have to assign the default values here\n  const {\n    packageName = toValidPackageName(defaultProjectName),\n    shouldOverwrite,\n    needsJsx = argv.jsx,\n    needsTypeScript = argv.typescript,\n    needsRouter = argv.router,\n    needsVuex = argv.vuex,\n    needsTests = argv.tests\n  } = result\n  const root = path.join(cwd, targetDir)\n\n  &#x2F;&#x2F; 如果需要强制重写，清空文件夹\n\n  if (shouldOverwrite) {\n    emptyDir(root)\n    &#x2F;&#x2F; 如果不存在文件夹，则创建\n  } else if (!fs.existsSync(root)) {\n    fs.mkdirSync(root)\n  }\n\n  &#x2F;&#x2F; 脚手架项目目录\n  console.log(&#96;&#x5C;nScaffolding project in ${root}...&#96;)\n\n &#x2F;&#x2F; 生成 package.json 文件\n  const pkg = { name: packageName, version: &#x27;0.0.0&#x27; }\n  fs.writeFileSync(path.resolve(root, &#x27;package.json&#x27;), JSON.stringify(pkg, null, 2))\n</code></pre><h3>4.5 根据模板文件生成初始化项目所需文件</h3>\n<pre class=\"prettyprint language-js\"><code>  &#x2F;&#x2F; todo:\n  &#x2F;&#x2F; work around the esbuild issue that &#96;import.meta.url&#96; cannot be correctly transpiled\n  &#x2F;&#x2F; when bundling for node and the format is cjs\n  &#x2F;&#x2F; const templateRoot = new URL(&#x27;.&#x2F;template&#x27;, import.meta.url).pathname\n  const templateRoot = path.resolve(__dirname, &#x27;template&#x27;)\n  const render = function render(templateName) {\n    const templateDir = path.resolve(templateRoot, templateName)\n    renderTemplate(templateDir, root)\n  }\n\n  &#x2F;&#x2F; Render base template\n  render(&#x27;base&#x27;)\n\n   &#x2F;&#x2F; 添加配置\n  &#x2F;&#x2F; Add configs.\n  if (needsJsx) {\n    render(&#x27;config&#x2F;jsx&#x27;)\n  }\n  if (needsRouter) {\n    render(&#x27;config&#x2F;router&#x27;)\n  }\n  if (needsVuex) {\n    render(&#x27;config&#x2F;vuex&#x27;)\n  }\n  if (needsTests) {\n    render(&#x27;config&#x2F;cypress&#x27;)\n  }\n  if (needsTypeScript) {\n    render(&#x27;config&#x2F;typescript&#x27;)\n  }\n</code></pre><h3>4.6 渲染生成代码模板</h3>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; Render code template.\n  &#x2F;&#x2F; prettier-ignore\n  const codeTemplate =\n    (needsTypeScript ? &#x27;typescript-&#x27; : &#x27;&#x27;) +\n    (needsRouter ? &#x27;router&#x27; : &#x27;default&#x27;)\n  render(&#96;code&#x2F;${codeTemplate}&#96;)\n\n  &#x2F;&#x2F; Render entry file (main.js&#x2F;ts).\n  if (needsVuex &amp;&amp; needsRouter) {\n    render(&#x27;entry&#x2F;vuex-and-router&#x27;)\n  } else if (needsVuex) {\n    render(&#x27;entry&#x2F;vuex&#x27;)\n  } else if (needsRouter) {\n    render(&#x27;entry&#x2F;router&#x27;)\n  } else {\n    render(&#x27;entry&#x2F;default&#x27;)\n  }\n</code></pre><h3>4.7 如果配置了需要 ts</h3>\n<p>重命名所有的 <code>.js</code> 文件改成 <code>.ts</code>。\n重命名 <code>jsconfig.json</code> 文件为 <code>tsconfig.json</code> 文件。</p>\n<p><a href=\"https://code.visualstudio.com/docs/languages/jsconfig\">jsconfig.json</a> 是VSCode的配置文件，可用于配置跳转等。</p>\n<p>把<code>index.html</code> 文件里的 <code>main.js</code> 重命名为 <code>main.ts</code>。</p>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; Cleanup.\n\nif (needsTypeScript) {\n    &#x2F;&#x2F; rename all &#96;.js&#96; files to &#96;.ts&#96;\n    &#x2F;&#x2F; rename jsconfig.json to tsconfig.json\n    preOrderDirectoryTraverse(\n      root,\n      () =&gt; {},\n      (filepath) =&gt; {\n        if (filepath.endsWith(&#x27;.js&#x27;)) {\n          fs.renameSync(filepath, filepath.replace(&#x2F;&#x5C;.js$&#x2F;, &#x27;.ts&#x27;))\n        } else if (path.basename(filepath) === &#x27;jsconfig.json&#x27;) {\n          fs.renameSync(filepath, filepath.replace(&#x2F;jsconfig&#x5C;.json$&#x2F;, &#x27;tsconfig.json&#x27;))\n        }\n      }\n    )\n\n    &#x2F;&#x2F; Rename entry in &#96;index.html&#96;\n    const indexHtmlPath = path.resolve(root, &#x27;index.html&#x27;)\n    const indexHtmlContent = fs.readFileSync(indexHtmlPath, &#x27;utf8&#x27;)\n    fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace(&#x27;src&#x2F;main.js&#x27;, &#x27;src&#x2F;main.ts&#x27;))\n  }\n</code></pre><h3>4.8 配置了不需要测试</h3>\n<p>因为所有的模板都有测试文件，所以不需要测试时，执行删除 <code>cypress</code>、<code>/__tests__/</code> 文件夹</p>\n<pre class=\"prettyprint language-js\"><code>  if (!needsTests) {\n    &#x2F;&#x2F; All templates assumes the need of tests.\n    &#x2F;&#x2F; If the user doesn&#x27;t need it:\n    &#x2F;&#x2F; rm -rf cypress **&#x2F;__tests__&#x2F;\n    preOrderDirectoryTraverse(\n      root,\n      (dirpath) =&gt; {\n        const dirname = path.basename(dirpath)\n\n        if (dirname === &#x27;cypress&#x27; || dirname === &#x27;__tests__&#x27;) {\n          emptyDir(dirpath)\n          fs.rmdirSync(dirpath)\n        }\n      },\n      () =&gt; {}\n    )\n  }\n</code></pre><h3>4.9 根据使用的 npm / yarn / pnpm <a href=\"http://xn--README-hz6l727j.md\">生成README.md</a> 文件，给出运行项目的提示</h3>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; Instructions:\n  &#x2F;&#x2F; Supported package managers: pnpm &gt; yarn &gt; npm\n  &#x2F;&#x2F; Note: until &lt;https:&#x2F;&#x2F;github.com&#x2F;pnpm&#x2F;pnpm&#x2F;issues&#x2F;3505&gt; is resolved,\n  &#x2F;&#x2F; it is not possible to tell if the command is called by &#96;pnpm init&#96;.\n  const packageManager = &#x2F;pnpm&#x2F;.test(process.env.npm_execpath)\n    ? &#x27;pnpm&#x27;\n    : &#x2F;yarn&#x2F;.test(process.env.npm_execpath)\n    ? &#x27;yarn&#x27;\n    : &#x27;npm&#x27;\n\n  &#x2F;&#x2F; README generation\n  fs.writeFileSync(\n    path.resolve(root, &#x27;README.md&#x27;),\n    generateReadme({\n      projectName: result.projectName || defaultProjectName,\n      packageManager,\n      needsTypeScript,\n      needsTests\n    })\n  )\n\n  console.log(&#96;&#x5C;nDone. Now run:&#x5C;n&#96;)\n  if (root !== cwd) {\n    console.log(&#96;  ${bold(green(&#96;cd ${path.relative(cwd, root)}&#96;))}&#96;)\n  }\n  console.log(&#96;  ${bold(green(getCommand(packageManager, &#x27;install&#x27;)))}&#96;)\n  console.log(&#96;  ${bold(green(getCommand(packageManager, &#x27;dev&#x27;)))}&#96;)\n  console.log()\n</code></pre><h2>5. npm run test =&gt; node test.js 测试</h2>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; create-vue&#x2F;test.js\nimport fs from &#x27;fs&#x27;\nimport path from &#x27;path&#x27;\nimport { fileURLToPath } from &#x27;url&#x27;\n\nimport { spawnSync } from &#x27;child_process&#x27;\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\nconst playgroundDir = path.resolve(__dirname, &#x27;.&#x2F;playground&#x2F;&#x27;)\n\nfor (const projectName of fs.readdirSync(playgroundDir)) {\n  if (projectName.endsWith(&#x27;with-tests&#x27;)) {\n    console.log(&#96;Running unit tests in ${projectName}&#96;)\n    const unitTestResult = spawnSync(&#x27;pnpm&#x27;, [&#x27;test:unit:ci&#x27;], {\n      cwd: path.resolve(playgroundDir, projectName),\n      stdio: &#x27;inherit&#x27;,\n      shell: true\n    })\n    if (unitTestResult.status !== 0) {\n      throw new Error(&#96;Unit tests failed in ${projectName}&#96;)\n    }\n\n    console.log(&#96;Running e2e tests in ${projectName}&#96;)\n    const e2eTestResult = spawnSync(&#x27;pnpm&#x27;, [&#x27;test:e2e:ci&#x27;], {\n      cwd: path.resolve(playgroundDir, projectName),\n      stdio: &#x27;inherit&#x27;,\n      shell: true\n    })\n    if (e2eTestResult.status !== 0) {\n      throw new Error(&#96;E2E tests failed in ${projectName}&#96;)\n    }\n  }\n}\n</code></pre><p>主要对生成快照时生成的在 <code>playground</code> 32个文件夹，进行如下测试。</p>\n<pre class=\"prettyprint language-sh\"><code>pnpm test:unit:ci\n\npnpm test:e2e:ci\n</code></pre><h2>6. 总结</h2>\n<p>我们使用了快如闪电般的<code>npm init vue@next</code>，学习<code>npx</code>命令了。学会了其原理。</p>\n<pre class=\"prettyprint language-sh\"><code>npm init vue@next =&gt; npx create-vue@next\n</code></pre><p>快如闪电的原因在于依赖的很少。很多都是自己来实现。如：<code>Vue-CLI</code>中 <code>vue create vue-project</code> 命令是用官方的<code>npm</code>包<a href=\"https://github.com/npm/validate-npm-package-name\">validate-npm-package-name</a>，删除文件夹一般都是使用 <a href=\"https://github.com/isaacs/rimraf\">rimraf</a>。而 <code>create-vue</code> 是自己实现<code>emptyDir</code>和<code>isValidPackageName</code>。</p>\n<p>非常建议读者朋友按照文中方法使用<code>VSCode</code>调试 <code>create-vue</code> 源码。源码中还有很多细节文中由于篇幅有限，未全面展开讲述。</p>\n<p>学完本文，可以为自己或者公司创建类似初始化脚手架。</p>\n<p>目前版本是<code>3.0.0-beta.6</code>。我们持续关注学习它。</p>\n<p>最后欢迎加我微信 <a href=\"https://juejin.cn/pin/7005372623400435725\">ruochuan12</a> 交流，参与 <a href=\"https://www.yuque.com/ruochuan12/notice\">源码共读</a> 活动，大家一起学习源码，共同进步。</p>\n<h2>7. 参考资料</h2>\n<p>发现 create-vue 时打算写文章加入到<a href=\"https://www.yuque.com/ruochuan12/notice\">源码共读</a>计划中，大家一起学习。而源码共读群里小伙伴<a href=\"https://github.com/upupming/frontend-learning-map\"><code>upupming</code></a>比我先写完文章。</p>\n<p><a href=\"https://mp.weixin.qq.com/s?__biz=MzA5MjQwMzQyNw==&amp;mid=2650757588&amp;idx=1&amp;sn=c6fb11eb09d5820957bcbece98a24087&amp;chksm=88665a58bf11d34e569a4e1f6de101f8fa7637a620d5d9f7944cb47f41c5c47ab6920920c4ce&amp;token=633613769&amp;lang=zh_CN#rd\">@upupming  vue-cli 将被 create-vue 替代？初始化基于 vite 的 vue3 项目为何如此简单？</a></p>\n<hr>\n<h2>关于 &amp;&amp; 交流群</h2>\n<p>最近组织了<a href=\"https://juejin.cn/pin/7005372623400435725\"><strong>源码共读活动</strong></a>，感兴趣的可以加我微信 <a href=\"https://juejin.cn/pin/7005372623400435725\">ruochuan12</a> 参与，长期交流学习。</p>\n<p>作者：常以<strong>若川</strong>为名混迹于江湖。欢迎加我微信<code>ruochuan12</code>。前端路上 | 所知甚少，唯善学。<br>\n<a href=\"https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/13/16efe57ddc7c9eb3~tplv-t2oaga2asx-image.image\" title=\"https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/13/16efe57ddc7c9eb3~tplv-t2oaga2asx-image.image\">关注公众号若川视野</a>，每周一起学源码，学会看源码，进阶高级前端。<br>\n<a href=\"https://link.juejin.cn?target=https%3A%2F%2Flxchuan12.gitee.io\" title=\"https://lxchuan12.gitee.io\">若川的博客</a><br>\n<a href=\"https://link.juejin.cn/?target=https%3A%2F%2Fsegmentfault.com%2Fblog%2Flxchuan12\" title=\"https://link.juejin.cn/?target=https%3A%2F%2Fsegmentfault.com%2Fblog%2Flxchuan12\"><code>segmentfault</code>若川视野专栏</a>，开通了<strong>若川视野</strong>专栏，欢迎关注~<br>\n<a href=\"https://juejin.im/user/1415826704971918/posts\" title=\"https://juejin.im/user/1415826704971918/posts\">掘金专栏</a>，欢迎关注~<br>\n<a href=\"https://link.juejin.cn/?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Flxchuan12\" title=\"https://link.juejin.cn/?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Flxchuan12\">知乎若川视野专栏</a>，开通了<strong>若川视野</strong>专栏，欢迎关注~<br>\n<a href=\"https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Flxchuan12%2Fblog\" title=\"https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Flxchuan12%2Fblog\">github blog</a>，求个<code>star</code>^_^~</p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: 若川\n        原文链接：<a href=\"https://juejin.im/post/7018344866811740173\">https://juejin.im/post/7018344866811740173</a>\n      </p></div>","title":"Vue 团队公开快如闪电的全新脚手架工具 create-vue，未来将替代 Vue-CLI，才300余行代码，学它！__前端__JavaScript__Vue.js","last_reply_at":"2022-01-17T15:15:00.586Z","good":false,"top":false,"reply_count":0,"visit_count":3396,"create_at":"2022-01-17T15:15:00.586Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e579e4cbbfd1003b11fa11","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><hr>\n<h2>theme: cyanosis\nhighlight: atom-one-light</h2>\n<h3>主题样式修改</h3>\n<p>曾经为了修改组件风格，一个项目用了上百个样式穿透，后来才发现定制一个主题就够了！</p>\n<h4>第一步，在官网的主题页面，修改背景色、字体颜色及边框颜色</h4>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5389bfb8437f4671a1abcde50e5deccf~tplv-k3u1fbpfcp-watermark.image?\" alt=\"01.jpg\"></p>\n<h4>第二步，下载主题</h4>\n<p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f83bcb6472c644499dca3c17baa8d57a~tplv-k3u1fbpfcp-watermark.image?\" alt=\"1639302082(1).jpg\"></p>\n<h4>第三步，用下载的css文件替换掉默认的css文件</h4>\n<p><img src=\"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7342d4f166f5486cabaed5ce16a45502~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h4>效果如下：</h4>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5f37ae06ca1f461088fff172422103f2~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h3>el-backtop 回到顶部组件</h3>\n<p>有时候页面很长，我们需要一个回到顶部的按钮，推荐大家使用 <code>el-backtop</code>，自带过渡效果。</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12e604091c60439c8034fa6cb2dde77f~tplv-k3u1fbpfcp-watermark.image?\" alt=\"20211213_211233 00_00_00-00_00_30.gif\"></p>\n<h4>代码如下：</h4>\n<pre class=\"prettyprint language-js\"><code>&lt;template&gt;\n  &lt;div class=&quot;container&quot;&gt;\n    &lt;div class=&quot;box&quot;&gt;Scroll down to see the bottom-right button.&lt;&#x2F;div&gt;\n    &lt;el-backtop target=&quot;.container&quot;&gt;\n      &lt;i class=&quot;el-icon-caret-top&quot;&gt;&lt;&#x2F;i&gt;\n    &lt;&#x2F;el-backtop&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n\n&lt;style lang=&quot;scss&quot; scoped&gt;\n.container {\n  height: 100vh;\n  overflow-x: hidden;\n  .box {\n    padding: 20px;\n    height: 1500px;\n  }\n}\n&lt;&#x2F;style&gt;\n</code></pre><p><code>注意：需要将 body 的 margin 设置为 0，不然会出现两个滚动条。</code></p>\n<h3>el-form-item label宽度自适应</h3>\n<p>通常我们是给 <code>label</code> 设置一个固定宽度，但这完全限制了 UI 的发挥，如何让 <code>label</code> 宽度自适应呢？答案是给控件设置一个固定宽度。</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa1f4571719548e9b730f548ec1af57a~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h4>代码如下：</h4>\n<pre class=\"prettyprint language-js\"><code>&lt;template&gt;\n  &lt;el-form&gt;\n    &lt;el-form-item label=&quot;教师&quot;&gt;\n      &lt;el-input v-model=&quot;form.teacher&quot; style=&quot;width: 625px;&quot;&gt;&lt;&#x2F;el-input&gt;\n    &lt;&#x2F;el-form-item&gt;\n    &lt;el-form-item label=&quot;请选择在线教学平台名称&quot;&gt;\n      &lt;el-select v-model=&quot;form.platform&quot; style=&quot;width: 500px;&quot;&gt;\n        &lt;el-option label=&quot;平台一&quot; value=&quot;1&quot;&gt;&lt;&#x2F;el-option&gt;\n        &lt;el-option label=&quot;平台二&quot; value=&quot;2&quot;&gt;&lt;&#x2F;el-option&gt;\n      &lt;&#x2F;el-select&gt;\n    &lt;&#x2F;el-form-item&gt;\n  &lt;&#x2F;el-form&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\nexport default {\n  data() {\n    return {\n      form: {\n        teacher: &#x27;&#x27;,\n        platform: &#x27;&#x27;\n      }\n    }\n  }\n}\n&lt;&#x2F;script&gt;\n</code></pre><h3>el-form-item 循环校验</h3>\n<p>有时候表单项是一个循环的列表，需要对每一项进行校验。</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/09f36b9bde724160bf849cc7747c893e~tplv-k3u1fbpfcp-watermark.image?\" alt=\"1639314939(1).jpg\"></p>\n<h4>代码如下：</h4>\n<pre class=\"prettyprint language-js\"><code>&lt;template&gt;\n  &lt;div&gt;\n    &lt;el-form :model=&quot;form&quot; label-width=&quot;100px&quot; ref=&quot;form&quot;&gt;\n      &lt;el-form-item\n        v-for=&quot;(item, index) in form.list&quot;\n        :key=&quot;index&quot;\n        :label=&quot;&#x27;评价项目&#x27; + (index + 1)&quot;\n        :prop=&quot;&#x27;list.&#x27; + index + &#x27;.evaluateName&#x27;&quot;\n        :rules=&quot;rules.evaluateName&quot;\n      &gt;\n        &lt;el-input v-model=&quot;item.evaluateName&quot; &#x2F;&gt;\n      &lt;&#x2F;el-form-item&gt;\n      &lt;el-form-item&gt;\n        &lt;el-button type=&quot;primary&quot; @click=&quot;submitForm&quot;&gt;提交&lt;&#x2F;el-button&gt;\n      &lt;&#x2F;el-form-item&gt;\n    &lt;&#x2F;el-form&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script&gt;\nexport default {\n  data() {\n    return {\n      form: {\n        list: [\n          {\n            evaluateName: &#x27;&#x27;\n          },\n          {\n            evaluateName: &#x27;&#x27;\n          },\n          {\n            evaluateName: &#x27;&#x27;\n          }\n        ]\n      },\n      rules: {\n        evaluateName: [\n          {\n            required: true,\n            message: &#x27;请输入评价项目&#x27;,\n            trigger: &#x27;blur&#x27;\n          }\n        ]\n      }\n    }\n  },\n  methods: {\n    submitForm() {\n      this.$refs.form.validate(valid =&gt; {\n        if (valid) {\n          alert(&#x27;submit!&#x27;)\n        } else {\n          console.log(&#x27;error submit!!&#x27;)\n          return false\n        }\n      })\n    }\n  }\n}\n&lt;&#x2F;script&gt;\n</code></pre><h3>el-form-item 表单嵌套表格校验</h3>\n<p>如果在表单里嵌套表格，那么如何对表格里面的控件进行校验呢？</p>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2f61fb571bba4da18f14bec455d10162~tplv-k3u1fbpfcp-watermark.image?\" alt=\"1639315824(1).jpg\"></p>\n<h4>代码如下：</h4>\n<pre class=\"prettyprint language-js\"><code>&lt;template&gt;\n  &lt;el-form :model=&quot;form&quot; ref=&quot;form&quot;&gt;\n    &lt;el-table border :data=&quot;form.list&quot; style=&quot;margin-bottom: 10px;&quot;&gt;\n      &lt;el-table-column\n        label=&quot;评价项目&quot;\n        prop=&quot;evaluateName&quot;\n        align=&quot;center&quot;\n      &gt;&lt;&#x2F;el-table-column&gt;\n      &lt;el-table-column label=&quot;评价等级&quot; align=&quot;center&quot;&gt;\n        &lt;template slot-scope=&quot;scope&quot;&gt;\n          &lt;el-form-item\n            :prop=&quot;&#x27;list.&#x27; + scope.$index + &#x27;.evaluateLevel&#x27;&quot;\n            :rules=&quot;rules.evaluateLevel&quot;\n            class=&quot;evaluate-level&quot;\n          &gt;\n            &lt;el-radio-group class=&quot;radio-group&quot; v-model=&quot;scope.row.evaluateLevel&quot;&gt;\n              &lt;el-radio\n                v-for=&quot;evaluationItem in evaluateOptions&quot;\n                :key=&quot;evaluationItem.value&quot;\n                :label=&quot;evaluationItem.value&quot;\n              &gt;\n                {{ evaluationItem.label }}\n              &lt;&#x2F;el-radio&gt;\n            &lt;&#x2F;el-radio-group&gt;\n          &lt;&#x2F;el-form-item&gt;\n        &lt;&#x2F;template&gt;\n      &lt;&#x2F;el-table-column&gt;\n    &lt;&#x2F;el-table&gt;\n    &lt;el-form-item&gt;\n      &lt;el-button type=&quot;primary&quot; class=&quot;submit-btn&quot; @click=&quot;submitOpinionCheck&quot;&gt;\n        提交\n      &lt;&#x2F;el-button&gt;\n    &lt;&#x2F;el-form-item&gt;\n  &lt;&#x2F;el-form&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script&gt;\nexport default {\n  data() {\n    return {\n      form: {\n        list: [\n          {\n            id: &#x27;01&#x27;,\n            evaluateName: &#x27;指标一&#x27;,\n            evaluateLevel: &#x27;&#x27;\n          },\n          {\n            id: &#x27;02&#x27;,\n            evaluateName: &#x27;指标二&#x27;,\n            evaluateLevel: &#x27;&#x27;\n          },\n          {\n            id: &#x27;03&#x27;,\n            evaluateName: &#x27;指标二&#x27;,\n            evaluateLevel: &#x27;&#x27;\n          }\n        ]\n      },\n      evaluateOptions: [\n        {\n          value: &#x27;1&#x27;,\n          label: &#x27;优&#x27;\n        },\n        {\n          value: &#x27;2&#x27;,\n          label: &#x27;良&#x27;\n        },\n        {\n          value: &#x27;3&#x27;,\n          label: &#x27;中&#x27;\n        },\n        {\n          value: &#x27;4&#x27;,\n          label: &#x27;差&#x27;\n        }\n      ],\n      rules: {\n        evaluateLevel: [{ required: true, message: &#x27;请选择评价等级&#x27;, trigger: &#x27;change&#x27; }]\n      }\n    }\n  },\n  methods: {\n    submitOpinionCheck() {\n      this.$refs.form.validate(valid =&gt; {\n        if (valid) {\n          alert(&#x27;submit!&#x27;)\n        } else {\n          console.log(&#x27;error submit!!&#x27;)\n          return false\n        }\n      })\n    }\n  }\n}\n&lt;&#x2F;script&gt;\n\n&lt;style lang=&quot;scss&quot; scoped&gt;\n.evaluate-level {\n  ::v-deep .el-form-item__error {\n    width: 100px;\n    &#x2F;* 使校验信息居中 *&#x2F;\n    left: calc(50% - 50px);\n  }\n}\n&lt;&#x2F;style&gt;\n</code></pre><h3>el-form-item 表单校验 label只保留 *</h3>\n<p>有时候我们只需要表单校验的 <code>*</code>， 而不需要 lable 文字，像这样：</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ae92a837ac804673a130dd41358653cf~tplv-k3u1fbpfcp-watermark.image?\" alt=\"1639312147(1).jpg\"></p>\n<h4>代码如下：</h4>\n<pre class=\"prettyprint language-js\"><code>&lt;template&gt;\n  &lt;el-form :model=&quot;ruleForm&quot; :rules=&quot;rules&quot; ref=&quot;ruleForm&quot; label-width=&quot;30px&quot;&gt;\n    &lt;el-form-item label=&quot; &quot; prop=&quot;name&quot;&gt;\n      &lt;el-input v-model=&quot;ruleForm.name&quot; placeholder=&quot;请输入活动名称&quot;&gt;&lt;&#x2F;el-input&gt;\n    &lt;&#x2F;el-form-item&gt;\n    &lt;el-form-item label=&quot; &quot; prop=&quot;region&quot;&gt;\n      &lt;el-select v-model=&quot;ruleForm.region&quot; placeholder=&quot;请选择活动区域&quot;&gt;\n        &lt;el-option label=&quot;区域一&quot; value=&quot;shanghai&quot;&gt;&lt;&#x2F;el-option&gt;\n        &lt;el-option label=&quot;区域二&quot; value=&quot;beijing&quot;&gt;&lt;&#x2F;el-option&gt;\n      &lt;&#x2F;el-select&gt;\n    &lt;&#x2F;el-form-item&gt;\n    &lt;el-form-item&gt;\n      &lt;el-button type=&quot;primary&quot; @click=&quot;submitForm(&#x27;ruleForm&#x27;)&quot;&gt;提交&lt;&#x2F;el-button&gt;\n    &lt;&#x2F;el-form-item&gt;\n  &lt;&#x2F;el-form&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\nexport default {\n  data() {\n    return {\n      ruleForm: {\n        name: &#x27;&#x27;,\n        region: &#x27;&#x27;\n      },\n      rules: {\n        name: [{ required: true, message: &#x27;请输入活动名称&#x27;, trigger: &#x27;blur&#x27; }],\n        region: [{ required: true, message: &#x27;请选择活动区域&#x27;, trigger: &#x27;change&#x27; }]\n      }\n    }\n  },\n  methods: {\n    submitForm(formName) {\n      this.$refs[formName].validate(valid =&gt; {\n        if (valid) {\n          alert(&#x27;submit!&#x27;)\n        } else {\n          console.log(&#x27;error submit!!&#x27;)\n          return false\n        }\n      })\n    }\n  }\n}\n&lt;&#x2F;script&gt;\n</code></pre><h3>el-select 下拉框样式修改</h3>\n<p>使用样式穿透修改下拉框样式，你会发现打死都不生效，那是因为下拉框是默认挂载在 body 下面。解决办法：设置 <code>:popper-append-to-body=&quot;false&quot;</code></p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/74f10805b5824ca2819709e561451281~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h4>代码如下：</h4>\n<pre class=\"prettyprint language-js\"><code>&lt;template&gt;\n  &lt;el-form&gt;\n    &lt;el-form-item label=&quot;活动区域&quot;&gt;\n      &lt;el-select\n        class=&quot;form-select&quot;\n        :popper-append-to-body=&quot;false&quot;\n        v-model=&quot;form.region&quot;\n        placeholder=&quot;请选择活动区域&quot;\n      &gt;\n        &lt;el-option label=&quot;区域一&quot; value=&quot;shanghai&quot;&gt;&lt;&#x2F;el-option&gt;\n        &lt;el-option label=&quot;区域二&quot; value=&quot;beijing&quot;&gt;&lt;&#x2F;el-option&gt;\n      &lt;&#x2F;el-select&gt;\n    &lt;&#x2F;el-form-item&gt;\n  &lt;&#x2F;el-form&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\nexport default {\n  data() {\n    return {\n      form: {\n        region: &#x27;&#x27;\n      }\n    }\n  }\n}\n&lt;&#x2F;script&gt;\n\n&lt;style lang=&quot;scss&quot; scoped&gt;\n.form-select {\n  ::v-deep .el-select-dropdown__item.selected {\n    background: pink;\n  }\n}\n&lt;&#x2F;style&gt;\n</code></pre><h3>el-input 打开弹框 自动聚焦</h3>\n<p>很多时候 form 表单是嵌套在 dialog 下面，为了提高用户体验，需要在打开弹框后 input 框自动聚焦。</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a32cde61fe7a4e61bae715783be56c13~tplv-k3u1fbpfcp-watermark.image?\" alt=\"20211213_205355 00_00_00-00_00_30.gif\"></p>\n<h4>代码如下：</h4>\n<pre class=\"prettyprint language-js\"><code>&lt;template&gt;\n  &lt;div&gt;\n    &lt;el-button type=&quot;text&quot; @click=&quot;showDialog&quot;&gt;\n      打开嵌套表单的 Dialog\n    &lt;&#x2F;el-button&gt;\n\n    &lt;el-dialog title=&quot;活动&quot; :visible.sync=&quot;dialogVisible&quot;&gt;\n      &lt;el-form :model=&quot;form&quot;&gt;\n        &lt;el-form-item label=&quot;活动名称&quot; label-width=&quot;80px&quot;&gt;\n          &lt;el-input ref=&quot;nameInput&quot; v-model=&quot;form.name&quot;&gt;&lt;&#x2F;el-input&gt;\n        &lt;&#x2F;el-form-item&gt;\n        &lt;el-form-item label=&quot;活动内容&quot; label-width=&quot;80px&quot;&gt;\n          &lt;el-input v-model=&quot;form.content&quot;&gt;&lt;&#x2F;el-input&gt;\n        &lt;&#x2F;el-form-item&gt;\n      &lt;&#x2F;el-form&gt;\n      &lt;div slot=&quot;footer&quot; class=&quot;dialog-footer&quot;&gt;\n        &lt;el-button @click=&quot;dialogVisible = false&quot;&gt;取 消&lt;&#x2F;el-button&gt;\n        &lt;el-button type=&quot;primary&quot; @click=&quot;dialogVisible = false&quot;&gt;确 定&lt;&#x2F;el-button&gt;\n      &lt;&#x2F;div&gt;\n    &lt;&#x2F;el-dialog&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n\n&lt;script&gt;\nexport default {\n  data() {\n    return {\n      dialogVisible: false,\n      form: {\n        name: &#x27;&#x27;,\n        content: &#x27;&#x27;\n      }\n    }\n  },\n  methods: {\n    showDialog() {\n      this.dialogVisible = true\n      &#x2F;&#x2F; 如果弹框显示，则自动聚焦\n      this.$nextTick(() =&gt; {\n        this.$refs.nameInput.focus()\n      })\n    }\n  }\n}\n&lt;&#x2F;script&gt;\n</code></pre><p>今天的分享就到到这里，希望对你有所帮助！如果觉得有用，不要忘了点个赞呦~</p>\n<p>听说喜欢点赞的你，今年年终奖拿到手软😍</p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: 前端阿飞\n        原文链接：<a href=\"https://juejin.im/post/7041320033884045326\">https://juejin.im/post/7041320033884045326</a>\n      </p></div>","title":"Element-UI 奇淫技巧第二弹！提升开发效率，延长摸鱼时间~__Element__前端","last_reply_at":"2022-01-17T14:15:00.481Z","good":false,"top":false,"reply_count":0,"visit_count":4425,"create_at":"2022-01-17T14:15:00.481Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e56bd4cbbfd1003b11fa09","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><hr>\n<h2>theme: smartblue</h2>\n<h2>前言</h2>\n<p>大家好，我是林三心，<strong>用最通俗的话讲最难的知识点</strong>是我的座右铭，<strong>基础是进阶的前提</strong>是我的初心，今天跟大家来唠唠嗑，如果后端真的返回给前端10万条数据，咱们前端要怎么优雅地展示出来呢？（哈哈假设后端真的能传10万条数据到前端）</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4cc34033138345488065107afcc4adf1~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<h2>前置工作</h2>\n<p>先把前置工作给做好，后面才能进行测试</p>\n<h3>后端搭建</h3>\n<p>新建一个<code>server.js</code>文件，简单起个服务，并返回给前端<code>10w</code>条数据，并通过<code>nodemon server.js</code>开启服务</p>\n<blockquote>\n<p>没有安装<code>nodemon</code>的同学可以先全局安装<code>npm i nodemon -g</code></p>\n</blockquote>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; server.js\n\nconst http = require(&#x27;http&#x27;)\nconst port = 8000;\n\nhttp.createServer(function (req, res) {\n  &#x2F;&#x2F; 开启Cors\n  res.writeHead(200, {\n    &#x2F;&#x2F;设置允许跨域的域名，也可设置*允许所有域名\n    &#x27;Access-Control-Allow-Origin&#x27;: &#x27;*&#x27;,\n    &#x2F;&#x2F;跨域允许的请求方法，也可设置*允许所有方法\n    &quot;Access-Control-Allow-Methods&quot;: &quot;DELETE,PUT,POST,GET,OPTIONS&quot;,\n    &#x2F;&#x2F;允许的header类型\n    &#x27;Access-Control-Allow-Headers&#x27;: &#x27;Content-Type&#x27;\n  })\n  let list = []\n  let num = 0\n\n  &#x2F;&#x2F; 生成10万条数据的list\n  for (let i = 0; i &lt; 100000; i++) {\n    num++\n    list.push({\n      src: &#x27;https:&#x2F;&#x2F;p3-passport.byteacctimg.com&#x2F;img&#x2F;user-avatar&#x2F;d71c38d1682c543b33f8d716b3b734ca~300x300.image&#x27;,\n      text: &#96;我是${num}号嘉宾林三心&#96;,\n      tid: num\n    })\n  }\n  res.end(JSON.stringify(list));\n}).listen(port, function () {\n  console.log(&#x27;server is listening on port &#x27; + port);\n})\n</code></pre><h3>前端页面</h3>\n<p>先新建一个<code>index.html</code></p>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; index.html\n\n&#x2F;&#x2F; 样式\n&lt;style&gt;\n    * {\n      padding: 0;\n      margin: 0;\n    }\n    #container {\n      height: 100vh;\n      overflow: auto;\n    }\n    .sunshine {\n      display: flex;\n      padding: 10px;\n    }\n    img {\n      width: 150px;\n      height: 150px;\n    }\n  &lt;&#x2F;style&gt;\n\n&#x2F;&#x2F; html部分\n&lt;body&gt;\n  &lt;div id=&quot;container&quot;&gt;\n  &lt;&#x2F;div&gt;\n  &lt;script src=&quot;.&#x2F;index.js&quot;&gt;&lt;&#x2F;script&gt;\n&lt;&#x2F;body&gt;\n</code></pre><p>然后新建一个<code>index.js</code>文件，封装一个<code>AJAX</code>函数，用来请求这<code>10w</code>条数据</p>\n<pre class=\"prettyprint language-js\"><code>&#x2F;&#x2F; index.js\n\n&#x2F;&#x2F; 请求函数\nconst getList = () =&gt; {\n    return new Promise((resolve, reject) =&gt; {\n        &#x2F;&#x2F;步骤一:创建异步对象\n        var ajax = new XMLHttpRequest();\n        &#x2F;&#x2F;步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数\n        ajax.open(&#x27;get&#x27;, &#x27;http:&#x2F;&#x2F;127.0.0.1:8000&#x27;);\n        &#x2F;&#x2F;步骤三:发送请求\n        ajax.send();\n        &#x2F;&#x2F;步骤四:注册事件 onreadystatechange 状态改变就会调用\n        ajax.onreadystatechange = function () {\n            if (ajax.readyState == 4 &amp;&amp; ajax.status == 200) {\n                &#x2F;&#x2F;步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的\n                resolve(JSON.parse(ajax.responseText))\n            }\n        }\n    })\n}\n\n&#x2F;&#x2F; 获取container对象\nconst container = document.getElementById(&#x27;container&#x27;)\n</code></pre><h2>直接渲染</h2>\n<p>最直接的方式就是直接渲染出来，但是这样的做法肯定是不可取的，因为一次性渲染出<code>10w</code>个节点，是非常耗时间的，咱们可以来看一下耗时，差不多要消耗<code>12秒</code>，非常消耗时间</p>\n<p><img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9396a045360744e7b73dd626a1456bcd~tplv-k3u1fbpfcp-watermark.image?\" alt=\"截屏2021-11-18 下午10.07.45.png\"></p>\n<pre class=\"prettyprint language-js\"><code>const renderList = async () =&gt; {\n    console.time(&#x27;列表时间&#x27;)\n    const list = await getList()\n    list.forEach(item =&gt; {\n        const div = document.createElement(&#x27;div&#x27;)\n        div.className = &#x27;sunshine&#x27;\n        div.innerHTML = &#96;&lt;img src=&quot;${item.src}&quot; &#x2F;&gt;&lt;span&gt;${item.text}&lt;&#x2F;span&gt;&#96;\n        container.appendChild(div)\n    })\n    console.timeEnd(&#x27;列表时间&#x27;)\n}\nrenderList()\n</code></pre><h2>setTimeout分页渲染</h2>\n<p>这个方法就是，把<code>10w</code>按照每页数量<code>limit</code>分成总共<code>Math.ceil(total / limit)</code>页，然后利用<code>setTimeout</code>，每次渲染1页数据，这样的话，渲染出首页数据的时间大大缩减了</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/faeac54d664a40ab97ac75325e0984ff~tplv-k3u1fbpfcp-watermark.image?\" alt=\"截屏2021-11-18 下午10.14.46.png\"></p>\n<pre class=\"prettyprint language-js\"><code>const renderList = async () =&gt; {\n    console.time(&#x27;列表时间&#x27;)\n    const list = await getList()\n    console.log(list)\n    const total = list.length\n    const page = 0\n    const limit = 200\n    const totalPage = Math.ceil(total &#x2F; limit)\n\n    const render = (page) =&gt; {\n        if (page &gt;= totalPage) return\n        setTimeout(() =&gt; {\n            for (let i = page * limit; i &lt; page * limit + limit; i++) {\n                const item = list[i]\n                const div = document.createElement(&#x27;div&#x27;)\n                div.className = &#x27;sunshine&#x27;\n                div.innerHTML = &#96;&lt;img src=&quot;${item.src}&quot; &#x2F;&gt;&lt;span&gt;${item.text}&lt;&#x2F;span&gt;&#96;\n                container.appendChild(div)\n            }\n            render(page + 1)\n        }, 0)\n    }\n    render(page)\n    console.timeEnd(&#x27;列表时间&#x27;)\n}\n</code></pre><h2>requestAnimationFrame</h2>\n<p>使用<code>requestAnimationFrame</code>代替<code>setTimeout</code>，减少了<code>重排</code>的次数，极大提高了性能，建议大家在渲染方面多使用<code>requestAnimationFrame</code></p>\n<pre class=\"prettyprint language-js\"><code>const renderList = async () =&gt; {\n    console.time(&#x27;列表时间&#x27;)\n    const list = await getList()\n    console.log(list)\n    const total = list.length\n    const page = 0\n    const limit = 200\n    const totalPage = Math.ceil(total &#x2F; limit)\n\n    const render = (page) =&gt; {\n        if (page &gt;= totalPage) return\n        &#x2F;&#x2F; 使用requestAnimationFrame代替setTimeout\n        requestAnimationFrame(() =&gt; {\n            for (let i = page * limit; i &lt; page * limit + limit; i++) {\n                const item = list[i]\n                const div = document.createElement(&#x27;div&#x27;)\n                div.className = &#x27;sunshine&#x27;\n                div.innerHTML = &#96;&lt;img src=&quot;${item.src}&quot; &#x2F;&gt;&lt;span&gt;${item.text}&lt;&#x2F;span&gt;&#96;\n                container.appendChild(div)\n            }\n            render(page + 1)\n        })\n    }\n    render(page)\n    console.timeEnd(&#x27;列表时间&#x27;)\n}\n</code></pre><h2>文档碎片 + requestAnimationFrame</h2>\n<p><code>文档碎片</code>的好处</p>\n<ul>\n<li>1、之前都是每次创建一个<code>div</code>标签就<code>appendChild</code>一次，但是有了<code>文档碎片</code>可以先把1页的<code>div</code>标签先放进<code>文档碎片</code>中，然后一次性<code>appendChild</code>到<code>container</code>中，这样减少了<code>appendChild</code>的次数，极大提高了性能</li>\n<li>2、页面只会渲染<code>文档碎片</code>包裹着的元素，而不会渲染<code>文档碎片</code></li>\n</ul>\n<pre class=\"prettyprint language-js\"><code>const renderList = async () =&gt; {\n    console.time(&#x27;列表时间&#x27;)\n    const list = await getList()\n    console.log(list)\n    const total = list.length\n    const page = 0\n    const limit = 200\n    const totalPage = Math.ceil(total &#x2F; limit)\n\n    const render = (page) =&gt; {\n        if (page &gt;= totalPage) return\n        requestAnimationFrame(() =&gt; {\n            &#x2F;&#x2F; 创建一个文档碎片\n            const fragment = document.createDocumentFragment()\n            for (let i = page * limit; i &lt; page * limit + limit; i++) {\n                const item = list[i]\n                const div = document.createElement(&#x27;div&#x27;)\n                div.className = &#x27;sunshine&#x27;\n                div.innerHTML = &#96;&lt;img src=&quot;${item.src}&quot; &#x2F;&gt;&lt;span&gt;${item.text}&lt;&#x2F;span&gt;&#96;\n                &#x2F;&#x2F; 先塞进文档碎片\n                fragment.appendChild(div)\n            }\n            &#x2F;&#x2F; 一次性appendChild\n            container.appendChild(fragment)\n            render(page + 1)\n        })\n    }\n    render(page)\n    console.timeEnd(&#x27;列表时间&#x27;)\n}\n</code></pre><h2>懒加载</h2>\n<p>为了比较通俗的讲解，咱们启动一个<code>vue</code>前端项目，后端服务还是开着</p>\n<p>其实实现原理很简单，咱们通过一张图来展示，就是在列表尾部放一个空节点<code>blank</code>，然后先渲染第1页数据，向上滚动，等到<code>blank</code>出现在视图中，就说明到底了，这时候再加载第二页，往后以此类推。</p>\n<p>至于怎么判断<code>blank</code>出现在视图上，可以使用<code>getBoundingClientRect</code>方法获取<code>top</code>属性</p>\n<blockquote>\n<p><code>IntersectionObserver</code> 性能更好，但是我这里就拿<code>getBoundingClientRect</code>来举例</p>\n</blockquote>\n<p><img src=\"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af9e2974b1d84d3c852c0e7f606794cd~tplv-k3u1fbpfcp-watermark.image?\" alt=\"截屏2021-11-18 下午10.41.01.png\"></p>\n<pre class=\"prettyprint language-js\"><code>&lt;script setup lang=&quot;ts&quot;&gt;\nimport { onMounted, ref, computed } from &#x27;vue&#x27;\nconst getList = () =&gt; {\n  &#x2F;&#x2F; 跟上面一样的代码\n}\n\nconst container = ref&lt;HTMLElement&gt;() &#x2F;&#x2F; container节点\nconst blank = ref&lt;HTMLElement&gt;() &#x2F;&#x2F; blank节点\nconst list = ref&lt;any&gt;([]) &#x2F;&#x2F; 列表\nconst page = ref(1) &#x2F;&#x2F; 当前页数\nconst limit = 200 &#x2F;&#x2F; 一页展示\n&#x2F;&#x2F; 最大页数\nconst maxPage = computed(() =&gt; Math.ceil(list.value.length &#x2F; limit))\n&#x2F;&#x2F; 真实展示的列表\nconst showList = computed(() =&gt; list.value.slice(0, page.value * limit))\nconst handleScroll = () =&gt; {\n  &#x2F;&#x2F; 当前页数与最大页数的比较\n  if (page.value &gt; maxPage.value) return\n  const clientHeight = container.value?.clientHeight\n  const blankTop = blank.value?.getBoundingClientRect().top\n  if (clientHeight === blankTop) {\n    &#x2F;&#x2F; blank出现在视图，则当前页数加1\n    page.value++\n  }\n}\n\nonMounted(async () =&gt; {\n  const res = await getList()\n  list.value = res\n})\n&lt;&#x2F;script&gt;\n\n&lt;template&gt;\n  &lt;div id=&quot;container&quot; @scroll=&quot;handleScroll&quot; ref=&quot;container&quot;&gt;\n    &lt;div class=&quot;sunshine&quot; v-for=&quot;(item) in showList&quot; :key=&quot;item.tid&quot;&gt;\n      &lt;img :src=&quot;item.src&quot; &#x2F;&gt;\n      &lt;span&gt;{{ item.text }}&lt;&#x2F;span&gt;\n    &lt;&#x2F;div&gt;\n    &lt;div ref=&quot;blank&quot;&gt;&lt;&#x2F;div&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n</code></pre><h2>虚拟列表</h2>\n<p>虚拟列表需要讲解的比较多，在这里我分享一下我的一篇<code>虚拟列表</code>的文章，哈哈我自认为讲的不错吧哈哈哈哈哈哈</p>\n<p><a href=\"https://juejin.cn/post/6966179727329460232\">结合“康熙选秀”，给大家讲讲“虚拟列表”</a></p>\n<h2>结语</h2>\n<p>如果你觉得此文对你有一丁点帮助，点个赞，鼓励一下林三心哈哈。</p>\n<p><strong>如果你想一起学习前端或者摸鱼，那你可以加我，加入我的摸鱼学习群，点击这里</strong> —&gt; <a href=\"https://juejin.cn/pin/7035153948126216206\" title=\"https://juejin.cn/pin/7035153948126216206\">摸鱼沸点</a></p>\n<p><strong>如果你是有其他目的的，别加我，我不想跟你交朋友，我只想简简单单学习前端，不想搞一些有的没的！！！</strong></p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: Sunshine_Lin\n        原文链接：<a href=\"https://juejin.im/post/7031923575044964389\">https://juejin.im/post/7031923575044964389</a>\n      </p></div>","title":"后端一次给你10万条数据，如何优雅展示，到底考察我什么?__前端__Vue.js__面试","last_reply_at":"2022-01-17T13:15:00.478Z","good":false,"top":false,"reply_count":0,"visit_count":4005,"create_at":"2022-01-17T13:15:00.478Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e55dc4ad745b003c9df77a","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0234702e819c4f1c94fce6869e0d88c6~tplv-k3u1fbpfcp-zoom-1.image\" alt=\"file\"></p>\n<p>本文首发于：<a href=\"https://github.com/bigo-frontend/blog/\">https://github.com/bigo-frontend/blog/</a> 欢迎关注、转载。</p>\n<h2>一、前言</h2>\n<p>入职的第一个需求是跟着一位前端大佬一起完成的一个活动项目。</p>\n<p>由于是一起开发，当然不会放过阅读大佬的代码的机会。</p>\n<p>因为我的页面中需要使用到倒计时功能，发现大佬的已经写了个现成的倒计时组件，于是直接就拿过来用了。<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/19cbeea00ba94feda62b720dcca08098~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>传个参数就实现了功能的感觉真是太棒了。项目完成后，就膜拜了一下大佬的倒计时组件的代码。真是让我学到了不少。列举如下：</p>\n<ol>\n<li>计时器为什么要用setTimeout而不用setInterval</li>\n<li>为什么不直接将剩余时间-1。</li>\n<li>如何将所需要的时间返回出去（有可能我只需要分钟和秒数，那就只返回分钟和秒数，也有可能我全都要）。</li>\n<li>不确定接口返回的是剩余时间还是截止日期，该怎么同时兼容这两种情况。</li>\n<li>不确定接口返回的时间是秒还是毫秒单位。</li>\n</ol>\n<p>好了，你可能不太理解这些问题，但是没关系，看完下面的解释，相信你会豁然开朗。\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5313d084d073497a863901a6a907f0ab~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<h2>二、开始手操</h2>\n<h3>1. 先创建一个vue组件</h3>\n<pre class=\"prettyprint language-html\"><code>&lt;template&gt;\n  &lt;div class=&quot;_base-count-down&quot;&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\n\nexport default {\n  data: () =&gt; ({\n   \n  }),\n  props: {\n    \n  },\n};\n&lt;&#x2F;script&gt;\n&lt;style lang=&#x27;scss&#x27; scoped&gt;\n\n&lt;&#x2F;style&gt;\n</code></pre><h3>2. 实现基本的倒计时组件</h3>\n<p>接下来，假设接口获得的是一个剩余时间。</p>\n<p>将剩余时间<code>time</code>传入这个倒计时组件，由于time可能是秒为单位的，也有可能是毫秒为单位的，所以我们需要在传入<code>time</code>的是有也传入一个<code>isMilliSecond</code>来告诉倒计时组件这个<code>time</code>是毫秒还是秒为单位的。如下代码中的<code>props</code>所示。</p>\n<pre class=\"prettyprint language-html\"><code>&lt;template&gt;\n  &lt;div class=&quot;_base-count-down&quot;&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\n\nexport default {\n  data: () =&gt; ({\n  }),\n  props: {\n    time: {\n      type: [Number, String],\n      default: 0\n    },\n    isMilliSecond: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    duration() {\n      const time = this.isMiniSecond ? Math.round(+this.time &#x2F; 1000) : Math.round(+this.time);\n      return time;\n    }\n  },\n};\n&lt;&#x2F;script&gt;\n&lt;style lang=&#x27;scss&#x27; scoped&gt;\n\n&lt;&#x2F;style&gt;\n</code></pre><p><code>computed</code>中的duration是将time进行转化的结果，不管<code>time</code>是毫秒还是秒，都转化为秒\n不知道你注意到了没有：<code>+this.time</code>。为什么要在前面加个‘<strong>+</strong>’号。这点很值得我们学习，因为接口返回的一串数字有时候是字符串的形式，有时候是数字的形式（<s>不能过分相信后端同学，必须自己做好防范</s>）。所以通过前面加个‘<strong>+</strong>’号 通通转化为数字。现在的<code>duration</code>就是转化后的<code>time</code>啦！</p>\n<p>我们获得duration之后就可以开始倒计时了</p>\n<pre class=\"prettyprint language-html\"><code>&lt;template&gt;\n  &lt;div class=&quot;_base-count-down&quot;&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\n\nexport default {\n  data: () =&gt; ({\n  }),\n  props: {\n    time: {\n      type: [Number, String],\n      default: 0\n    },\n    isMilliSecond: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    duration() {\n      const time = this.isMiniSecond ? Math.round(+this.time &#x2F; 1000) : Math.round(+this.time);\n      return time;\n    }\n  },\n  &#x2F;&#x2F; 新增代码：\n  mounted() {\n    this.countDown();\n  },\n  methods: {\n    countDown() {\n      this.getTime(this.duration);\n    },\n  }\n};\n&lt;&#x2F;script&gt;\n&lt;style lang=&#x27;scss&#x27; scoped&gt;\n\n&lt;&#x2F;style&gt;\n</code></pre><p>在这里创建了一个countDown方法，表示开始倒计时的意思，已进入页面就开始执行countdown方法。</p>\n<p><code>countDown</code>方法调用了getTime方法，getTime需要传入duration这个参数，也就是我们获得的剩余时间。</p>\n<p>现在来实现一下这个方法。</p>\n<pre class=\"prettyprint language-html\"><code>&lt;template&gt;\n  &lt;div class=&quot;_base-count-down&quot;&gt;\n    还剩{{day}}天{{hours}}:{{mins}}:{{seconds}}\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\n\nexport default {\n  data: () =&gt; ({\n    days: &#x27;0&#x27;,\n    hours: &#x27;00&#x27;,\n    mins: &#x27;00&#x27;,\n    seconds: &#x27;00&#x27;,\n    timer: null,\n  }),\n  props: {\n    time: {\n      type: [Number, String],\n      default: 0\n    },\n    isMilliSecond: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    duration() {\n      const time = this.isMiniSecond ? Math.round(+this.time &#x2F; 1000) : Math.round(+this.time);\n      return time;\n    }\n  },\n  mounted() {\n    this.countDown();\n  },\n  methods: {\n    countDown() {\n      this.getTime(this.duration);\n    },\n    &#x2F;&#x2F; 新增代码：\n    getTime(duration) {\n      this.timer &amp;&amp; clearTimeout(this.timer);\n      if (duration &lt; 0) {\n        return;\n      }\n      const { dd, hh, mm, ss } = this.durationFormatter(duration);\n      this.days = dd || 0;\n      this.hours = hh || 0;\n      this.mins = mm || 0;\n      this.seconds = ss || 0;\n      this.timer = setTimeout(() =&gt; {\n        this.getTime(duration - 1);\n      }, 1000);\n    }\n  }\n};\n&lt;&#x2F;script&gt;\n&lt;style lang=&#x27;scss&#x27; scoped&gt;\n\n&lt;&#x2F;style&gt;\n</code></pre><p>可以看到，<strong>getTime</strong>的目的就是获得 days，hours,mins，seconds，然后显示到html上，并且通过定时器实时来刷新days，hours,mins，seconds这个几个值。从而实现了倒计时。很简单，有木有？</p>\n<p><code>durationFormatter</code>是一个将<code>duration</code>转化成天数，小时，分钟，秒数的方法，很简单，可以看下它的具体实现。</p>\n<pre class=\"prettyprint language-js\"><code>durationFormatter(time) {\n  if (!time) return { ss: 0 };\n  let t = time;\n  const ss = t % 60;\n  t = (t - ss) &#x2F; 60;\n  if (t &lt; 1) return { ss };\n  const mm = t % 60;\n  t = (t - mm) &#x2F; 60;\n  if (t &lt; 1) return { mm, ss };\n  const hh = t % 24;\n  t = (t - hh) &#x2F; 24;\n  if (t &lt; 1) return { hh, mm, ss };\n  const dd = t;\n  return { dd, hh, mm, ss };\n},\n</code></pre><p>好了，问题开始来了！！\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/36794496b1f4436499617fa23eb621e7~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<h3>3. 为什么要<strong>用setTimeout来模拟setInterval的行为</strong>？</h3>\n<p>这里用setInerval不是更方便吗？</p>\n<pre class=\"prettyprint\"><code>setTimeout(function(){··· }, n); &#x2F;&#x2F; n毫秒后执行function\n</code></pre><pre class=\"prettyprint\"><code>setInterval(function(){··· }, n); &#x2F;&#x2F; 每隔n毫秒执行一次function\n</code></pre><p>可以看看setInterval有什么缺点：</p>\n<blockquote>\n<p>再次强调，定时器指定的时间间隔，表示的是何时将定时器的代码添加到消息队列，而不是何时执行代码。所以真正何时执行代码的时间是不能保证的，取决于何时被主线程的事件循环取到，并执行。</p>\n</blockquote>\n<pre class=\"prettyprint\"><code>setInterval(function, N)  \n&#x2F;&#x2F;即：每隔N秒把function事件推到消息队列中\n</code></pre><p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/39aa93f16e274e499f567cf7e31e33f6~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>上图可见，setInterval每隔100ms往队列中添加一个事件；100ms后，添加T1定时器代码至队列中，主线程中还有任务在执行，所以等待，some event执行结束后执行T1定时器代码；又过了100ms，T2定时器被添加到队列中，主线程还在执行T1代码，所以等待；又过了100ms，理论上又要往队列里推一个定时器代码，但由于此时T2还在队列中，所以T3不会被添加，结果就是此时被跳过；这里我们可以看到，T1定时器执行结束后马上执行了T2代码，所以并没有达到定时器的效果。</p>\n<p>综上所述，setInterval有两个缺点：</p>\n<ol>\n<li>使用setInterval时，某些间隔会被跳过；</li>\n<li>可能多个定时器会连续执行；</li>\n</ol>\n<p>可以这么理解：<strong>每个setTimeout产生的任务会直接push到任务队列中；而setInterval在每次把任务push到任务队列前，都要进行一下判断(看上次的任务是否仍在队列中)</strong>。</p>\n<p>因而我们一般用setTimeout模拟setInterval，来规避掉上面的缺点。</p>\n<h3>4. 为什么要clearTimeout(this.timer)</h3>\n<p>第二问：为什么要有<code>this.timer &amp;&amp; clearTimeout(this.timer);</code>这一句？</p>\n<p>假设一个场景：</p>\n<p>如图所示，在倒计时的父组件中，有两个按钮，点击活动一就会传入活动一的剩余时间，点击活动二，就会传入活动二的时间。</p>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1f5ba9e8f4df440f80df305a0860a4f9~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>如果此时倒计时组件正在做活动一的倒计时，然后点击活动二，就要会马上传入新的time，这个时候就需要重新计时。当然，这里并不会重新计时，因为组件的mounted只会执行一次。也就是说<code>this.countDown();</code>只会执行一次，也就是说<code>this.getTime(this.duration);</code>只会执行一次，因此duration还是活动一的时间，怎么办呢？watch派上用场了。</p>\n<p>我们来监听duration，如果发现duration变化，说明新的时间time传入组件，这时就要重新调用this.countDown()。</p>\n<p>代码如下：</p>\n<pre class=\"prettyprint language-html\"><code>&lt;template&gt;\n  &lt;div class=&quot;_base-count-down&quot;&gt;\n    还剩{{day}}天{{hours}}:{{mins}}:{{seconds}}\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\n\nexport default {\n  data: () =&gt; ({\n    days: &#x27;0&#x27;,\n    hours: &#x27;00&#x27;,\n    mins: &#x27;00&#x27;,\n    seconds: &#x27;00&#x27;,\n    timer: null,\n  }),\n  props: {\n    time: {\n      type: [Number, String],\n      default: 0\n    },\n    isMilliSecond: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    duration() {\n      const time = this.isMiniSecond ? Math.round(+this.time &#x2F; 1000) : Math.round(+this.time);\n      return time;\n    }\n  },\n  mounted() {\n    this.countDown();\n  },\n  &#x2F;&#x2F; 新增代码：\n  watch: {\n    duration() {\n      this.countDown();\n    }\n  },\n  methods: {\n    countDown() {\n      this.getTime(this.duration);\n    },\n    durationFormatter(){...}\n    getTime(duration) {\n      this.timer &amp;&amp; clearTimeout(this.timer);\n      if (duration &lt; 0) {\n        return;\n      }\n      const { dd, hh, mm, ss } = this.durationFormatter(duration);\n      this.days = dd || 0;\n      this.hours = hh || 0;\n      this.mins = mm || 0;\n      this.seconds = ss || 0;\n      this.timer = setTimeout(() =&gt; {\n        this.getTime(duration - 1);\n      }, 1000);\n    }\n  }\n};\n&lt;&#x2F;script&gt;\n&lt;style lang=&#x27;scss&#x27; scoped&gt;\n\n&lt;&#x2F;style&gt;\n</code></pre><p>好了，但是并没有解释上面提出的那个问题：为什么要有<code>this.timer &amp;&amp; clearTimeout(this.timer);</code>这一句？</p>\n<p>这样，假设现在页面显示的是活动一的时间，这时，执行到setTimeout，在<strong>一秒后</strong>就会把setTimeout里的回调函数放到任务队列中，<strong>注意是一秒后哦</strong>！这时，然而，在这一秒的开头，我们点击了活动二按钮，这时候的活动二的时间就会传入倒计时组件中，然后触发<code>countDown()</code>,也就调用<code>this.getTime(this.duration);</code>，然后执行到setTimeout，也会一秒后把回调函数放到任务队列中。</p>\n<p>这时，任务队列中就会有两个setTimeout的回调函数了。等待一秒过去，两个回调函数相继执行，我们就会看到页面上的时间一下子背减了2，实际上是很快速地进行了两遍减1的操作。</p>\n<p>这就是为什么要添加上<code>this.timer &amp;&amp; clearTimeout(this.timer);</code>这一句的原因了。就是要把上一个setTimeout清除掉。</p>\n<h3>5. 使用 diffTime</h3>\n<p>当你认为这是一个完美的组件的时候，你想把这个组件用到项目上，假设你也确实用了，而且还上线了，确发现出现了个大问题：当页面打开的时候，倒计时开始了，时间是 <code>还剩1天12：25：25</code>，然后有人给你发微信，你马上切换到微信，回复消息后切回浏览器，发现倒计时时间却还是<code>还剩1天12：25：25</code>。你慌了：<strong>你写的代码出现bug了！</strong></p>\n<p>这是怎么回事？</p>\n<p>出于节能的考虑, 部分浏览器在进入后台时(或者失去焦点时), <strong>会将 setTimeout 等定时任务暂停\n待用户回到浏览器时, 才会重新激活定时任务</strong></p>\n<p>说是暂停, 其实应该说是延迟, 1s 的任务延迟到 2s, 2s 的延迟到 5s, 实际情况因浏览器而异。</p>\n<p>原来如此，看来不能每次都只是减1这么简单了（毕竟你把浏览器切到后台之后setTimeout就冷却了，等几秒后切回，然后执行setTimeout，只是减了一秒而已）。</p>\n<p>所以我们需要改写一下getTime方法。</p>\n<pre class=\"prettyprint language-html\"><code>&lt;template&gt;\n  &lt;div class=&quot;_base-count-down&quot;&gt;\n    还剩{{day}}天{{hours}}:{{mins}}:{{seconds}}\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\n\nexport default {\n  data: () =&gt; ({\n    days: &#x27;0&#x27;,\n    hours: &#x27;00&#x27;,\n    mins: &#x27;00&#x27;,\n    seconds: &#x27;00&#x27;,\n    timer: null,\n    curTime: 0,&#x2F;&#x2F; 新增代码：\n  }),\n  props: {\n    time: {\n      type: [Number, String],\n      default: 0\n    },\n    isMilliSecond: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    duration() {\n      const time = this.isMiniSecond ? Math.round(+this.time &#x2F; 1000) : Math.round(+this.time);\n      return time;\n    }\n  },\n  mounted() {\n    this.countDown();\n  },\n  \n  watch: {\n    duration() {\n      this.countDown();\n    }\n  },\n  methods: {\n    countDown() {\n      &#x2F;&#x2F; 新增代码：\n      this.curTime = Date.now();\n      this.getTime(this.duration);\n    },\n    durationFormatter(){...}\n    getTime(duration) {\n      this.timer &amp;&amp; clearTimeout(this.timer);\n      if (duration &lt; 0) {\n        return;\n      }\n      const { dd, hh, mm, ss } = this.durationFormatter(duration);\n      this.days = dd || 0;\n      this.hours = hh || 0;\n      this.mins = mm || 0;\n      this.seconds = ss || 0;\n      this.timer = setTimeout(() =&gt; {\n        &#x2F;&#x2F; 新增代码：\n        const now = Date.now();\n        const diffTime = Math.floor((now - this.curTime) &#x2F; 1000);\n        this.curTime = now;\n        this.getTime(duration - diffTime);\n      }, 1000);\n    }\n  }\n};\n&lt;&#x2F;script&gt;\n&lt;style lang=&#x27;scss&#x27; scoped&gt;\n\n&lt;&#x2F;style&gt;\n</code></pre><p>可以看到，我们在三个位置添加了新的代码。</p>\n<p>首先在data了添加了curTime这个变量，然后在执行countDown的时候给<code>curTime</code>赋值<code>Date.now()</code>，也就是当前的时刻，也就是显示在页面上的那个时刻。</p>\n<p>然后看修改的第三处代码。可以看到是将<code>-1</code>改成了<code>-diffTime</code>。</p>\n<p>now 是 setTimeout的回调函数执行的时候的那个时刻。</p>\n<p>因而 diffTime 则 表示 <strong>当前这个setTimeout的回调函数执行的时刻距离上 页面上的剩余时间上一次变化的时间段</strong>。其实也就是 <strong>当前这个setTimeout的回调函数执行的时刻距离上 一个setTimeout的回调函数执行的时刻时间段。</strong></p>\n<p>可能你还是不太能理解diffTime。举个例子：</p>\n<p>你打开了这个倒计时页面，于是执行了countDown，也就是说要执行getTime这个方法了。也就是会马上执行下列的代码。</p>\n<pre class=\"prettyprint\"><code>this.days = dd || 0;\nthis.hours = hh || 0;\nthis.mins = mm || 0;\nthis.seconds = ss || 0;\n</code></pre><p>执行完这些代码页面上就会出现剩余时间。</p>\n<p>而<code>this.curTime = Date.now();</code> 就记录下了此刻的时间点。</p>\n<p>然后一秒后执行setTimeout里的回调函数：</p>\n<p><code>const now = Date.now();</code> 记录当前这个setTimeout的回调函数执行的时间点。</p>\n<p><code>const diffTime = Math.floor((now - this.curTime) / 1000);</code> 记录当前这个setTimeout的回调函数执行的时间点距离页面上开始 渲染 剩余时间的 这一段时间。其实此时的diffTime就是=1。</p>\n<p>然后<code>this.curTime = now;</code> 将curTime的值变成当前这个setTimeout的回调函数执行的时间点。</p>\n<p><code>this.getTime(duration - diffTime);</code> 其实就是<code>this.getTime(duration - 1);</code></p>\n<p>然后又执行getTime，就会重新执行下面的代码，有渲染了新的剩余时间。</p>\n<pre class=\"prettyprint\"><code>this.days = dd || 0;\nthis.hours = hh || 0;\nthis.mins = mm || 0;\nthis.seconds = ss || 0;\n</code></pre><p>然后一秒后又要执行setTmieout的回调函数，在这一秒还没结束的时候，我们将浏览器切到后台，此时setTimeout冷却了。等5秒后再切回。于是setTmieout的回调函数才得以执行。</p>\n<p>这时<code>const now = Date.now();</code> 记录当前这个setTimeout的回调函数执行的时间点。</p>\n<p>而curTime是上一个setTimeout的回调函数执行的时间。</p>\n<p>所以<code>const diffTime = Math.floor((now - this.curTime) / 1000);</code>实际上，diffTime的值就是5秒。</p>\n<p>因而<code>this.getTime(duration - diffTime);</code> 其实就是<code>this.getTime(duration - 5);</code></p>\n<p>这样就完美解决了因为浏览器切到后台，导致剩余时间不变的问题。</p>\n<h3>6. 添加新功能：可以传入到期时间。</h3>\n<p>之前是只能传入剩余时间的，现在希望也支持传入到期时间。</p>\n<p>只需要改动一下duration就好了。</p>\n<pre class=\"prettyprint language-js\"><code>  computed: {\n    duration() {\n      if (this.end) {\n        let end = String(this.end).length &gt;= 13 ? +this.end : +this.end * 1000;\n        end -= Date.now();\n        return end;\n      }\n      const time = this.isMiniSecond ? Math.round(+this.time &#x2F; 1000) : Math.round(+this.time);\n      return time;\n    }\n  },\n</code></pre><p>判断传入的end的长度是否大于13来判断是秒还是毫秒。轻松！</p>\n<h3>7. 添加新功能：可以选择要显示的内容，例如只显示秒，或者只显示小时。</h3>\n<p>只需要改动一下html：</p>\n<pre class=\"prettyprint language-js\"><code>&lt;template&gt;\n  &lt;div class=&quot;_base-count-down no-rtl&quot;&gt;\n    &lt;div class=&quot;content&quot;&gt;\n      &lt;slot v-bind=&quot;{\n        d: days, h: hours, m: mins, s: seconds,\n        hh: &#96;00${hours}&#96;.slice(-2),\n        mm: &#96;00${mins}&#96;.slice(-2),\n        ss: &#96;00${seconds}&#96;.slice(-2),\n      }&quot;&gt;&lt;&#x2F;slot&gt;\n    &lt;&#x2F;div&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n</code></pre><p>很巧妙有没有，只需要用插槽，就把倒计时组件，也就是把子组件的值传递给父组件了。</p>\n<p>看看父组件是怎么使用这个组件的。</p>\n<pre class=\"prettyprint\"><code>&lt;base-counter v-slot=&quot;timeObj&quot; :time=&quot;countDown&quot;&gt;\n  &lt;div class=&quot;count-down&quot;&gt;\n    &lt;div class=&quot;icon&quot;&gt;&lt;&#x2F;div&gt;\n    {{timeObj.d}}天{{timeObj.hh}}小时{{timeObj.mm}}分钟{{timeObj.ss}}秒\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;base-counter&gt;\n</code></pre><p>看，如此巧妙又简单。</p>\n<p>发现<code>00${hours}</code>.slice(-2) 这种写法也很值得学习。以前在获得到分钟的时候，要手动判断获得的分钟是两位数还是一位数，如果是一位数的话就要在前面手动补上0。就像下面的代码：</p>\n<pre class=\"prettyprint language-js\"><code>var StartMinute = startDate.getMinutes().toString().length &gt;= 2 ? startDate.getMinutes() : &#x27;0&#x27; + startDate.getHours();\n</code></pre><p>而<code>00${hours}</code>.slice(-2) 则不用判断，先补上0再说，然后再从后面往前截取两位。\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/72a2f7a957e64e9d96b00a420fa2725b~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<p>到此。</p>\n<p>一个完美的倒计时组件就完成了。\n<img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f23f102146a1443db651239f5cba83df~tplv-k3u1fbpfcp-zoom-1.image\" alt></p>\n<h2>三、学习总结</h2>\n<ol>\n<li>明白了setInterval的缺点以及用setTimeout代替setInterval。</li>\n<li>学到了<code>“+”</code>，操作，不管三七二十一，将接口得到的长串数字转化为数字保平安。</li>\n<li>利用clearTimeout来清除掉之前的计时器，以防止造成影响。</li>\n<li>学会使用v-slot来子传父传值</li>\n<li>学会一个倒计时组件，为了以后方便cv操作。把组件完整代码贴上：</li>\n</ol>\n<pre class=\"prettyprint language-html\"><code>&lt;template&gt;\n  &lt;div class=&quot;_base-count-down no-rtl&quot;&gt;\n    &lt;div class=&quot;content&quot;&gt;\n      &lt;slot v-bind=&quot;{\n        d: days, h: hours, m: mins, s: seconds,\n        hh: &#96;00${hours}&#96;.slice(-2),\n        mm: &#96;00${mins}&#96;.slice(-2),\n        ss: &#96;00${seconds}&#96;.slice(-2),\n      }&quot;&gt;&lt;&#x2F;slot&gt;\n    &lt;&#x2F;div&gt;\n  &lt;&#x2F;div&gt;\n&lt;&#x2F;template&gt;\n&lt;script&gt;\n&#x2F;* eslint-disable object-curly-newline *&#x2F;\n\nexport default {\n  data: () =&gt; ({\n    days: &#x27;0&#x27;,\n    hours: &#x27;00&#x27;,\n    mins: &#x27;00&#x27;,\n    seconds: &#x27;00&#x27;,\n    timer: null,\n    curTime: 0\n  }),\n  props: {\n    time: {\n      type: [Number, String],\n      default: 0\n    },\n    refreshCounter: {\n      type: [Number, String],\n      default: 0\n    },\n    end: {\n      type: [Number, String],\n      default: 0\n    },\n    isMiniSecond: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    duration() {\n      if (this.end) {\n        let end = String(this.end).length &gt;= 13 ? +this.end : +this.end * 1000;\n        end -= Date.now();\n        return end;\n      }\n      const time = this.isMiniSecond ? Math.round(+this.time &#x2F; 1000) : Math.round(+this.time);\n      return time;\n    }\n  },\n  mounted() {\n    this.countDown();\n  },\n  watch: {\n    duration() {\n      this.countDown();\n    },\n    refreshCounter() {\n      this.countDown();\n    }\n  },\n  methods: {\n    durationFormatter(time) {\n      if (!time) return { ss: 0 };\n      let t = time;\n      const ss = t % 60;\n      t = (t - ss) &#x2F; 60;\n      if (t &lt; 1) return { ss };\n      const mm = t % 60;\n      t = (t - mm) &#x2F; 60;\n      if (t &lt; 1) return { mm, ss };\n      const hh = t % 24;\n      t = (t - hh) &#x2F; 24;\n      if (t &lt; 1) return { hh, mm, ss };\n      const dd = t;\n      return { dd, hh, mm, ss };\n    },\n    countDown() {\n      &#x2F;&#x2F; eslint-disable-next-line no-unused-expressions\n      this.curTime = Date.now();\n      this.getTime(this.duration);\n    },\n    getTime(time) {\n      &#x2F;&#x2F; eslint-disable-next-line no-unused-expressions\n      this.timer &amp;&amp; clearTimeout(this.timer);\n      if (time &lt; 0) {\n        return;\n      }\n      &#x2F;&#x2F; eslint-disable-next-line object-curly-newline\n      const { dd, hh, mm, ss } = this.durationFormatter(time);\n      this.days = dd || 0;\n      &#x2F;&#x2F; this.hours = &#96;00${hh || &#x27;&#x27;}&#96;.slice(-2);\n      &#x2F;&#x2F; this.mins = &#96;00${mm || &#x27;&#x27;}&#96;.slice(-2);\n      &#x2F;&#x2F; this.seconds = &#96;00${ss || &#x27;&#x27;}&#96;.slice(-2);\n      this.hours = hh || 0;\n      this.mins = mm || 0;\n      this.seconds = ss || 0;\n      this.timer = setTimeout(() =&gt; {\n        const now = Date.now();\n        const diffTime = Math.floor((now - this.curTime) &#x2F; 1000);\n        const step = diffTime &gt; 1 ? diffTime : 1; &#x2F;&#x2F; 页面退到后台的时候不会计时，对比时间差，大于1s的重置倒计时\n        this.curTime = now;\n        this.getTime(time - step);\n      }, 1000);\n    }\n  }\n};\n&lt;&#x2F;script&gt;\n&lt;style lang=&#x27;scss&#x27; scoped&gt;\n@import &#x27;~@assets&#x2F;css&#x2F;common.scss&#x27;;\n\n._base-count-down {\n  color: #fff;\n  text-align: left;\n  position: relative;\n  .content {\n    width: auto;\n    display: flex;\n    align-items: center;\n  }\n  span {\n    display: inline-block;\n  }\n  .section {\n    position: relative;\n  }\n}\n&lt;&#x2F;style&gt;\n</code></pre><p><strong>你学到了什么，欢迎补充！！</strong></p>\n<p>欢迎大家留言讨论，祝工作顺利、生活愉快！</p>\n<p>我是bigo前端，下期见。</p>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: bigo前端\n        原文链接：<a href=\"https://juejin.im/post/7038405108371030047\">https://juejin.im/post/7038405108371030047</a>\n      </p></div>","title":"研究大佬写的倒计时组件(Vue)，学到了不少东西__Vue.js__前端","last_reply_at":"2022-01-17T12:15:00.706Z","good":false,"top":false,"reply_count":0,"visit_count":5010,"create_at":"2022-01-17T12:15:00.706Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}},{"id":"61e54fb4ad745b003c9df779","author_id":"5928dcd1fbaee51e2432c158","tab":"share","content":"<div class=\"markdown-text\"><hr>\n<h2>theme: juejin</h2>\n<p>本文已参与 <a href=\"https://juejin.cn/post/7012210233804079141\" title=\"https://juejin.cn/post/7012210233804079141\">「掘力星计划」</a> ，赢取创作大礼包，挑战创作激励金。</p>\n<h2>前言</h2>\n<p>「欢迎在评论区讨论，掘金官方将在<a href=\"https://juejin.cn/post/7012210233804079141\" title=\"https://juejin.cn/post/7012210233804079141\">掘力星计划</a>活动结束后，在评论区抽送100份掘金周边，抽奖详情见活动文章」</p>\n<blockquote>\n<p>开发要对线上环境有一颗敬畏之心，任何一个点都有可能导致线上故障，也有可能让你的年终奖泡汤(⊙︿⊙)。比如使用了<code>JSON.stringify</code>，这个无比熟悉但又无比陌生的API。</p>\n</blockquote>\n<p>看完本文您可以收获：</p>\n<ol>\n<li>了解一个差点让我年终奖打水漂的悲伤的故事o(╥﹏╥)o</li>\n<li>学习JSON.stringify的9大特性和转换规则 <strong>(重点)</strong></li>\n<li>了解如何判断一个对象是否存在循环引用 <strong>(重点)</strong></li>\n<li>从零开始手写一个JSON.stringify <strong>(重点)</strong></li>\n<li>等等</li>\n</ol>\n<h2>说一个悲伤的故事</h2>\n<blockquote>\n<p>最近组内有个小伙伴离职了，他所负责的一块业务由我去维护，结果刚接手，代码还没捂热乎，差点背上p0的锅。请让我花一点时间和你说清楚来龙去脉。</p>\n</blockquote>\n<h3>悲伤伊始</h3>\n<blockquote>\n<p>这一天<strong>胖头鱼</strong>正徜徉在代码的海洋不可自拔，突然被拉进了一个线上问题排查群，群里不可谓不热闹。\n<img src=\"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e2a584a72ebc4440b53f8b34d943e391~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n</blockquote>\n<p><strong>产品同学</strong>在诉苦：线上用户不能提交表单了，带来了好多客诉，估计会是p0故障，希望尽快解决。</p>\n<p><strong>测试同学</strong>在纳闷：这个场景测试和预发环境明明验过的，怎么线上就不行了。</p>\n<p><strong>后端同学</strong>在讲原因：接口缺少了value字段，导致出错了。</p>\n<p><code>就是木有人说问题怎么解决!!!</code></p>\n<p><code>就是木有人说问题怎么解决!!!</code></p>\n<p><code>就是木有人说问题怎么解决!!!</code></p>\n<p>这样的场景不知道你是不是也似曾相识呢？o(╥﹏╥)o，不管咋说第一要务还是先把线上问题解决掉，减少持续影响，赶紧把交接的代码翻出来，开始了排查过程。</p>\n<h3>问题原因</h3>\n<blockquote>\n<p>如下图：有这样一个动态表单搜集页面，用户选择或者填写了信息之后（<code>各字段非必填情况下也可以直接提交</code>），接着前端把数据发送给后端，结束，看起来没有多复杂的逻辑。</p>\n</blockquote>\n<p><img src=\"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a48b74e1bad74c2588047e8ffbe8825f~tplv-k3u1fbpfcp-watermark.image?\" alt=\"image.png\"></p>\n<p><strong>直接错误原因</strong></p>\n<blockquote>\n<p>非必填情况下，signInfo字段中经过<code>JSON.stringify</code>后的字符串对象缺少<code>value</code> key，导致后端parse之后无法正确读取value值，进而报接口系统异常，用户无法进行下一步动作。</p>\n</blockquote>\n<pre class=\"prettyprint language-javascript\"><code>&#x2F;&#x2F; 异常入参数据，数组字符串中没有value key\n{\n  signInfo: &#x27;[{&quot;fieldId&quot;:539},{&quot;fieldId&quot;:540},{&quot;fieldId&quot;:546,&quot;value&quot;:&quot;10:30&quot;}]&#x27;\n}\n\n&#x2F;&#x2F; 正常入参数据\n{\n  signInfo: &#x27;[{&quot;fieldId&quot;:539,&quot;value&quot;:&quot;银卡&quot;},{&quot;fieldId&quot;:540,&quot;value&quot;:&quot;2021-03-01&quot;},{&quot;fieldId&quot;:546,&quot;value&quot;:&quot;10:30&quot;}]&#x27;\n}\n\n</code></pre><p><strong>异常数据是如何产生的</strong></p>\n<pre class=\"prettyprint language- javascript\"><code>&#x2F;&#x2F; 默认情况下数据是这样的\nlet signInfo = [\n  {\n    fieldId: 539,\n    value: undefined\n  },\n  {\n    fieldId: 540,\n    value: undefined\n  },\n  {\n    fieldId: 546,\n    value: undefined\n  },\n]\n&#x2F;&#x2F; 经过JSON.stringify之后的数据,少了value key,导致后端无法读取value值进行报错\n&#x2F;&#x2F; 具体原因是&#96;undefined&#96;、&#96;任意的函数&#96;以及&#96;symbol值&#96;，出现在&#96;非数组对象&#96;的属性值中时在序列化过程中会被忽略\nconsole.log(JSON.stringify(signInfo))\n&#x2F;&#x2F; &#x27;[{&quot;fieldId&quot;:539},{&quot;fieldId&quot;:540},{&quot;fieldId&quot;:546}]&#x27;\n\n</code></pre><h3>解决方案</h3>\n<blockquote>\n<p>问题的原因找到了，解决方式 <strong>(这里只讲前端的解决方案，当然也可以由后端解决)</strong> 也很简单，将value值为undefined的项转化为空字符串再提交即可。</p>\n</blockquote>\n<p><strong>方案一：新开一个对象处理</strong></p>\n<pre class=\"prettyprint language- javascript\"><code>let signInfo = [\n  {\n    fieldId: 539,\n    value: undefined\n  },\n  {\n    fieldId: 540,\n    value: undefined\n  },\n  {\n    fieldId: 546,\n    value: undefined\n  },\n]\n\nlet newSignInfo = signInfo.map((it) =&gt; {\n  const value = typeof it.value === &#x27;undefined&#x27; ? &#x27;&#x27; : it.value\n  return {\n    ...it,\n    value\n  }\n})\n\nconsole.log(JSON.stringify(newSignInfo))\n&#x2F;&#x2F; &#x27;[{&quot;fieldId&quot;:539,&quot;value&quot;:&quot;&quot;},{&quot;fieldId&quot;:540,&quot;value&quot;:&quot;&quot;},{&quot;fieldId&quot;:546,&quot;value&quot;:&quot;&quot;}]&#x27;\n\n</code></pre><p>方案二：利用<code>JSON.stringify</code>第二个参数，直接处理</p>\n<blockquote>\n<p>方案一的缺陷是需要新开一个对象进行一顿操作才能解决，<code>不够优雅</code></p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>let signInfo = [\n  {\n    fieldId: 539,\n    value: undefined\n  },\n  {\n    fieldId: 540,\n    value: undefined\n  },\n  {\n    fieldId: 546,\n    value: undefined\n  },\n]\n\n&#x2F;&#x2F; 判断到value为undefined，返回空字符串即可\nJSON.stringify(signInfo, (key, value) =&gt; typeof value === &#x27;undefined&#x27; ? &#x27;&#x27; : value)\n&#x2F;&#x2F; &#x27;[{&quot;fieldId&quot;:539,&quot;value&quot;:&quot;&quot;},{&quot;fieldId&quot;:540,&quot;value&quot;:&quot;&quot;},{&quot;fieldId&quot;:546,&quot;value&quot;:&quot;&quot;}]&#x27;\n\n</code></pre><h3>故事后续</h3>\n<blockquote>\n<p>原本这是一个已经上线有一段时间的页面，为何会突然出现这个问题，之前却没有呢？仔细询问下，原来是中途产品同学提了一个小的优化点，离职的小伙伴感觉点比较小直接就改了代码上线了，未曾想出现了线上问题。</p>\n</blockquote>\n<p>后面针对这件事从产品到测试、到后端、到前端单独做了一个完整的复盘，细节就不再展开说了。</p>\n<p>因为从发现问题到解决问题速度较快、影响用户数较少，还未达到问责程度，<strong>俺的年终奖可算是保住了o(╥﹏╥)o。</strong></p>\n<h2>重学JSON.stringify</h2>\n<blockquote>\n<p>经过这件事情，我觉得有必要重新审视一下<code>JSON.stringify</code>这个方法，彻底搞清楚转换规则，并尝试手写实现一个<code>JSON.stringify</code></p>\n</blockquote>\n<p>如果你曾遇到和我一样的问题，欢迎一起来重新学习一次，一定会有不一样的收获噢！</p>\n<h2>学透JSON.stringify</h2>\n<blockquote>\n<p><code>JSON.stringify()</code> 方法将一个 <code>JavaScript 对象</code>或<code>值</code>转换为 JSON 字符串，如果指定了一个 replacer 函数，则可以选择性地替换值，或者指定的 replacer 是数组，则可选择性地仅包含数组指定的属性。</p>\n</blockquote>\n<p>以下信息来自<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#syntax\">MDN</a></p>\n<h3>语法</h3>\n<pre class=\"prettyprint language- javascript\"><code>JSON.stringify(value[, replacer [, space]])\n</code></pre><p><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#parameters\" title=\"Permalink to 参数\">参数</a></p>\n<ul>\n<li>\n<p><code>value</code></p>\n<p>将要序列化成 一个 JSON 字符串的值。</p>\n</li>\n<li>\n<p><code>replacer</code> 可选</p>\n<ol>\n<li>如果该参数是一个函数，则在序列化过程中，被序列化的值的每个属性都会经过该函数的转换和处理；</li>\n<li>如果该参数是一个数组，则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中；</li>\n<li>如果该参数为 null 或者未提供，则对象所有的属性都会被序列化。</li>\n</ol>\n</li>\n<li>\n<p><code>space</code> 可选</p>\n<ol>\n<li>指定缩进用的空白字符串，用于美化输出（pretty-print）；</li>\n<li>如果参数是个数字，它代表有多少的空格；上限为10。</li>\n<li>该值若小于1，则意味着没有空格；</li>\n<li>如果该参数为字符串（当字符串长度超过10个字母，取其前10个字母），该字符串将被作为空格；</li>\n<li>如果该参数没有提供（或者为 null），将没有空格。</li>\n</ol>\n</li>\n</ul>\n<p><strong>返回值</strong></p>\n<pre class=\"prettyprint\"><code>一个表示给定值的JSON字符串。\n</code></pre><p><a href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#%E5%BC%82%E5%B8%B8\" title=\"Permalink to 异常\">异常</a></p>\n<ul>\n<li>当在循环引用时会抛出异常<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypeError\"><code>TypeError</code></a> (“cyclic object value”)（循环对象值）</li>\n<li>当尝试去转换 <a href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/BigInt\"><code>BigInt</code></a> 类型的值会抛出<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypeError\"><code>TypeError</code></a> (“BigInt value can’t be serialized in JSON”)（BigInt值不能JSON序列化）.</li>\n</ul>\n<h3>基本使用</h3>\n<p><strong>注意</strong></p>\n<ol>\n<li>JSON.stringify可以转换对象或者值（平常用的更多的是转换对象）</li>\n<li>可以指定<code>replacer</code>为函数选择性的地替换</li>\n<li>也可以指定<code>replacer</code>为数组，可转换指定的属性</li>\n</ol>\n<p>这里仅仅是<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify\">MDN</a>上关于<code>JSON.stringify</code>其中最基础的说明，咱们先打个码试试这几个特性</p>\n<pre class=\"prettyprint language- javascript\"><code>&#x2F;&#x2F; 1. 转换对象\nconsole.log(JSON.stringify({ name: &#x27;前端胖头鱼&#x27;, sex: &#x27;boy&#x27; })) &#x2F;&#x2F; &#x27;{&quot;name&quot;:&quot;前端胖头鱼&quot;,&quot;sex&quot;:&quot;boy&quot;}&#x27;\n\n&#x2F;&#x2F; 2. 转换普通值\nconsole.log(JSON.stringify(&#x27;前端胖头鱼&#x27;)) &#x2F;&#x2F; &quot;前端胖头鱼&quot;\nconsole.log(JSON.stringify(1)) &#x2F;&#x2F; &quot;1&quot;\nconsole.log(JSON.stringify(true)) &#x2F;&#x2F; &quot;true&quot;\nconsole.log(JSON.stringify(null)) &#x2F;&#x2F; &quot;null&quot;\n\n&#x2F;&#x2F; 3. 指定replacer函数\nconsole.log(JSON.stringify({ name: &#x27;前端胖头鱼&#x27;, sex: &#x27;boy&#x27;, age: 100 }, (key, value) =&gt; {\n  return typeof value === &#x27;number&#x27; ? undefined : value\n}))\n&#x2F;&#x2F; &#x27;{&quot;name&quot;:&quot;前端胖头鱼&quot;,&quot;sex&quot;:&quot;boy&quot;}&#x27;\n\n&#x2F;&#x2F; 4. 指定数组\nconsole.log(JSON.stringify({ name: &#x27;前端胖头鱼&#x27;, sex: &#x27;boy&#x27;, age: 100 }, [ &#x27;name&#x27; ]))\n&#x2F;&#x2F; &#x27;{&quot;name&quot;:&quot;前端胖头鱼&quot;}&#x27;\n\n&#x2F;&#x2F; 5. 指定space(美化输出)\nconsole.log(JSON.stringify({ name: &#x27;前端胖头鱼&#x27;, sex: &#x27;boy&#x27;, age: 100 }))\n&#x2F;&#x2F; &#x27;{&quot;name&quot;:&quot;前端胖头鱼&quot;,&quot;sex&quot;:&quot;boy&quot;,&quot;age&quot;:100}&#x27;\nconsole.log(JSON.stringify({ name: &#x27;前端胖头鱼&#x27;, sex: &#x27;boy&#x27;, age: 100 }, null , 2))\n&#x2F;*\n{\n  &quot;name&quot;: &quot;前端胖头鱼&quot;,\n  &quot;sex&quot;: &quot;boy&quot;,\n  &quot;age&quot;: 100\n}\n*&#x2F;\n</code></pre><h3>9大特性要记住</h3>\n<blockquote>\n<p>以前仅仅是使用了这个方法，却没有详细了解他的转换规则，居然有9个之多。</p>\n</blockquote>\n<h4>特性一</h4>\n<ol>\n<li><code>undefined</code>、<code>任意的函数</code>以及<code>symbol值</code>，出现在<code>非数组对象</code>的属性值中时在序列化过程中会被忽略</li>\n<li><code>undefined</code>、<code>任意的函数</code>以及<code>symbol值</code>出现在<code>数组</code>中时会被转换成 <code>null</code>。</li>\n<li><code>undefined</code>、<code>任意的函数</code>以及<code>symbol值</code>被<code>单独转换</code>时，会返回 undefined</li>\n</ol>\n<pre class=\"prettyprint language- javascript\"><code>&#x2F;&#x2F; 1. 对象中存在这三种值会被忽略\nconsole.log(JSON.stringify({\n  name: &#x27;前端胖头鱼&#x27;,\n  sex: &#x27;boy&#x27;,\n  &#x2F;&#x2F; 函数会被忽略\n  showName () {\n    console.log(&#x27;前端胖头鱼&#x27;)\n  },\n  &#x2F;&#x2F; undefined会被忽略\n  age: undefined,\n  &#x2F;&#x2F; Symbol会被忽略\n  symbolName: Symbol(&#x27;前端胖头鱼&#x27;)\n}))\n&#x2F;&#x2F; &#x27;{&quot;name&quot;:&quot;前端胖头鱼&quot;,&quot;sex&quot;:&quot;boy&quot;}&#x27;\n\n&#x2F;&#x2F; 2. 数组中存在着三种值会被转化为null\nconsole.log(JSON.stringify([\n  &#x27;前端胖头鱼&#x27;,\n  &#x27;boy&#x27;,\n  &#x2F;&#x2F; 函数会被转化为null\n  function showName () {\n    console.log(&#x27;前端胖头鱼&#x27;)\n  },\n  &#x2F;&#x2F;undefined会被转化为null\n  undefined,\n  &#x2F;&#x2F;Symbol会被转化为null\n  Symbol(&#x27;前端胖头鱼&#x27;)\n]))\n&#x2F;&#x2F; &#x27;[&quot;前端胖头鱼&quot;,&quot;boy&quot;,null,null,null]&#x27;\n\n&#x2F;&#x2F; 3.单独转换会返回undefined\nconsole.log(JSON.stringify(\n  function showName () {\n    console.log(&#x27;前端胖头鱼&#x27;)\n  }\n)) &#x2F;&#x2F; undefined\nconsole.log(JSON.stringify(undefined)) &#x2F;&#x2F; undefined\nconsole.log(JSON.stringify(Symbol(&#x27;前端胖头鱼&#x27;))) &#x2F;&#x2F; undefined\n\n</code></pre><h4>特性二</h4>\n<blockquote>\n<p><code>布尔值</code>、<code>数字</code>、<code>字符串</code>的包装对象在序列化过程中会自动转换成对应的原始值。</p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>console.log(JSON.stringify([new Number(1), new String(&quot;前端胖头鱼&quot;), new Boolean(false)]))\n&#x2F;&#x2F; &#x27;[1,&quot;前端胖头鱼&quot;,false]&#x27;\n</code></pre><h4>特性三</h4>\n<blockquote>\n<p>所有以<code>symbol</code>为属性键的属性都会被完全忽略掉，即便 <code>replacer</code> 参数中强制指定包含了它们。</p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>console.log(JSON.stringify({\n  [Symbol(&#x27;前端胖头鱼&#x27;)]: &#x27;前端胖头鱼&#x27;}\n)) \n&#x2F;&#x2F; &#x27;{}&#x27;\nconsole.log(JSON.stringify({\n  [ Symbol(&#x27;前端胖头鱼&#x27;) ]: &#x27;前端胖头鱼&#x27;,\n}, (key, value) =&gt; {\n  if (typeof key === &#x27;symbol&#x27;) {\n    return value\n  }\n}))\n&#x2F;&#x2F; undefined\n</code></pre><h4>特性四</h4>\n<blockquote>\n<p>NaN 和 Infinity 格式的数值及 null 都会被当做 null。</p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>console.log(JSON.stringify({\n  age: NaN,\n  age2: Infinity,\n  name: null\n}))\n&#x2F;&#x2F; &#x27;{&quot;age&quot;:null,&quot;age2&quot;:null,&quot;name&quot;:null}&#x27;\n\n</code></pre><h4>特性五</h4>\n<blockquote>\n<p>转换值如果有 toJSON() 方法，该方法定义什么值将被序列化。</p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>const toJSONObj = {\n  name: &#x27;前端胖头鱼&#x27;,\n  toJSON () {\n    return &#x27;JSON.stringify&#x27;\n  }\n}\n\nconsole.log(JSON.stringify(toJSONObj))\n&#x2F;&#x2F; &quot;JSON.stringify&quot;\n</code></pre><h4>特性六</h4>\n<blockquote>\n<p>Date 日期调用了 toJSON() 将其转换为了 string 字符串（同Date.toISOString()），因此会被当做字符串处理。</p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>const d = new Date()\n\nconsole.log(d.toJSON()) &#x2F;&#x2F; 2021-10-05T14:01:23.932Z\nconsole.log(JSON.stringify(d)) &#x2F;&#x2F; &quot;2021-10-05T14:01:23.932Z&quot;\n\n</code></pre><h4>特性七</h4>\n<blockquote>\n<p>对包含循环引用的对象（对象之间相互引用，形成无限循环）执行此方法，会抛出错误。</p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>let cyclicObj = {\n  name: &#x27;前端胖头鱼&#x27;,\n}\n\ncyclicObj.obj = cyclicObj\n\nconsole.log(JSON.stringify(cyclicObj))\n&#x2F;&#x2F; Converting circular structure to JSON\n</code></pre><h4>特性八</h4>\n<blockquote>\n<p>其他类型的对象，包括 Map/Set/WeakMap/WeakSet，仅会序列化可枚举的属性</p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>let enumerableObj = {}\n\nObject.defineProperties(enumerableObj, {\n  name: {\n    value: &#x27;前端胖头鱼&#x27;,\n    enumerable: true\n  },\n  sex: {\n    value: &#x27;boy&#x27;,\n    enumerable: false\n  },\n})\n\nconsole.log(JSON.stringify(enumerableObj))\n&#x2F;&#x2F; &#x27;{&quot;name&quot;:&quot;前端胖头鱼&quot;}&#x27;\n</code></pre><h4>特性九</h4>\n<blockquote>\n<p>当尝试去转换 <a href=\"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/BigInt\"><code>BigInt</code></a> 类型的值会抛出错误</p>\n</blockquote>\n<pre class=\"prettyprint language- javascript\"><code>const alsoHuge = BigInt(9007199254740991)\n\nconsole.log(JSON.stringify(alsoHuge))\n&#x2F;&#x2F; TypeError: Do not know how to serialize a BigInt\n</code></pre><h2>手写一个JSON.stringify</h2>\n<blockquote>\n<p>终于重新学完<code>JSON.stringify</code>的众多特性啦！咱们根据这些特性来手写一个简单版本的吧（<strong>无replacer函数和space</strong>）</p>\n</blockquote>\n<h3>源码实现</h3>\n<pre class=\"prettyprint language- javascript\"><code>const jsonstringify = (data) =&gt; {\n  &#x2F;&#x2F; 确认一个对象是否存在循环引用\n  const isCyclic = (obj) =&gt; {\n  &#x2F;&#x2F; 使用Set数据类型来存储已经检测过的对象\n  let stackSet = new Set()\n  let detected = false\n\n  const detect = (obj) =&gt; {\n    &#x2F;&#x2F; 不是对象类型的话，可以直接跳过\n    if (obj &amp;&amp; typeof obj != &#x27;object&#x27;) {\n      return\n    }\n    &#x2F;&#x2F; 当要检查的对象已经存在于stackSet中时，表示存在循环引用\n    if (stackSet.has(obj)) {\n      return detected = true\n    }\n    &#x2F;&#x2F; 将当前obj存如stackSet\n    stackSet.add(obj)\n\n    for (let key in obj) {\n      &#x2F;&#x2F; 对obj下的属性进行挨个检测\n      if (obj.hasOwnProperty(key)) {\n        detect(obj[key])\n      }\n    }\n    &#x2F;&#x2F; 平级检测完成之后，将当前对象删除，防止误判\n    &#x2F;*\n      例如：对象的属性指向同一引用，如果不删除的话，会被认为是循环引用\n      let tempObj = {\n        name: &#x27;前端胖头鱼&#x27;\n      }\n      let obj4 = {\n        obj1: tempObj,\n        obj2: tempObj\n      }\n    *&#x2F;\n    stackSet.delete(obj)\n  }\n\n  detect(obj)\n\n  return detected\n}\n\n  &#x2F;&#x2F; 特性七:\n  &#x2F;&#x2F; 对包含循环引用的对象（对象之间相互引用，形成无限循环）执行此方法，会抛出错误。\n  if (isCyclic(data)) {\n    throw new TypeError(&#x27;Converting circular structure to JSON&#x27;)\n  }\n\n  &#x2F;&#x2F; 特性九:\n  &#x2F;&#x2F; 当尝试去转换 BigInt 类型的值会抛出错误\n  if (typeof data === &#x27;bigint&#x27;) {\n    throw new TypeError(&#x27;Do not know how to serialize a BigInt&#x27;)\n  }\n\n  const type = typeof data\n  const commonKeys1 = [&#x27;undefined&#x27;, &#x27;function&#x27;, &#x27;symbol&#x27;]\n  const getType = (s) =&gt; {\n    return Object.prototype.toString.call(s).replace(&#x2F;&#x5C;[object (.*?)&#x5C;]&#x2F;, &#x27;$1&#x27;).toLowerCase()\n  }\n\n  &#x2F;&#x2F; 非对象\n  if (type !== &#x27;object&#x27; || data === null) {\n    let result = data\n    &#x2F;&#x2F; 特性四：\n    &#x2F;&#x2F; NaN 和 Infinity 格式的数值及 null 都会被当做 null。\n    if ([NaN, Infinity, null].includes(data)) {\n      result = &#x27;null&#x27;\n      &#x2F;&#x2F; 特性一：\n      &#x2F;&#x2F; &#96;undefined&#96;、&#96;任意的函数&#96;以及&#96;symbol值&#96;被&#96;单独转换&#96;时，会返回 undefined\n    } else if (commonKeys1.includes(type)) {\n      &#x2F;&#x2F; 直接得到undefined，并不是一个字符串&#x27;undefined&#x27;\n      return undefined\n    } else if (type === &#x27;string&#x27;) {\n      result = &#x27;&quot;&#x27; + data + &#x27;&quot;&#x27;\n    }\n\n    return String(result)\n  } else if (type === &#x27;object&#x27;) {\n    &#x2F;&#x2F; 特性五:\n    &#x2F;&#x2F; 转换值如果有 toJSON() 方法，该方法定义什么值将被序列化\n    &#x2F;&#x2F; 特性六:\n    &#x2F;&#x2F; Date 日期调用了 toJSON() 将其转换为了 string 字符串（同Date.toISOString()），因此会被当做字符串处理。\n    if (typeof data.toJSON === &#x27;function&#x27;) {\n      return jsonstringify(data.toJSON())\n    } else if (Array.isArray(data)) {\n      let result = data.map((it) =&gt; {\n        &#x2F;&#x2F; 特性一:\n        &#x2F;&#x2F; &#96;undefined&#96;、&#96;任意的函数&#96;以及&#96;symbol值&#96;出现在&#96;数组&#96;中时会被转换成 &#96;null&#96;\n        return commonKeys1.includes(typeof it) ? &#x27;null&#x27; : jsonstringify(it)\n      })\n\n      return &#96;[${result}]&#96;.replace(&#x2F;&#x27;&#x2F;g, &#x27;&quot;&#x27;)\n    } else {\n      &#x2F;&#x2F; 特性二：\n      &#x2F;&#x2F; 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。\n      if ([&#x27;boolean&#x27;, &#x27;number&#x27;].includes(getType(data))) {\n        return String(data)\n      } else if (getType(data) === &#x27;string&#x27;) {\n        return &#x27;&quot;&#x27; + data + &#x27;&quot;&#x27;\n      } else {\n        let result = []\n        &#x2F;&#x2F; 特性八\n        &#x2F;&#x2F; 其他类型的对象，包括 Map&#x2F;Set&#x2F;WeakMap&#x2F;WeakSet，仅会序列化可枚举的属性\n        Object.keys(data).forEach((key) =&gt; {\n          &#x2F;&#x2F; 特性三:\n          &#x2F;&#x2F; 所有以symbol为属性键的属性都会被完全忽略掉，即便 replacer 参数中强制指定包含了它们。\n          if (typeof key !== &#x27;symbol&#x27;) {\n            const value = data[key]\n            &#x2F;&#x2F; 特性一\n            &#x2F;&#x2F; &#96;undefined&#96;、&#96;任意的函数&#96;以及&#96;symbol值&#96;，出现在&#96;非数组对象&#96;的属性值中时在序列化过程中会被忽略\n            if (!commonKeys1.includes(typeof value)) {\n              result.push(&#96;&quot;${key}&quot;:${jsonstringify(value)}&#96;)\n            }\n          }\n        })\n\n        return &#96;{${result}}&#96;.replace(&#x2F;&#x27;&#x2F;, &#x27;&quot;&#x27;)\n      }\n    }\n  }\n}\n\n\n</code></pre><h3>测试一把</h3>\n<pre class=\"prettyprint language- javascript\"><code>&#x2F;&#x2F; 1. 测试一下基本输出\nconsole.log(jsonstringify(undefined)) &#x2F;&#x2F; undefined \nconsole.log(jsonstringify(() =&gt; { })) &#x2F;&#x2F; undefined\nconsole.log(jsonstringify(Symbol(&#x27;前端胖头鱼&#x27;))) &#x2F;&#x2F; undefined\nconsole.log(jsonstringify((NaN))) &#x2F;&#x2F; null\nconsole.log(jsonstringify((Infinity))) &#x2F;&#x2F; null\nconsole.log(jsonstringify((null))) &#x2F;&#x2F; null\nconsole.log(jsonstringify({\n  name: &#x27;前端胖头鱼&#x27;,\n  toJSON() {\n    return {\n      name: &#x27;前端胖头鱼2&#x27;,\n      sex: &#x27;boy&#x27;\n    }\n  }\n}))\n&#x2F;&#x2F; {&quot;name&quot;:&quot;前端胖头鱼2&quot;,&quot;sex&quot;:&quot;boy&quot;}\n\n&#x2F;&#x2F; 2. 和原生的JSON.stringify转换进行比较\nconsole.log(jsonstringify(null) === JSON.stringify(null));\n&#x2F;&#x2F; true\nconsole.log(jsonstringify(undefined) === JSON.stringify(undefined));\n&#x2F;&#x2F; true\nconsole.log(jsonstringify(false) === JSON.stringify(false));\n&#x2F;&#x2F; true\nconsole.log(jsonstringify(NaN) === JSON.stringify(NaN));\n&#x2F;&#x2F; true\nconsole.log(jsonstringify(Infinity) === JSON.stringify(Infinity));\n&#x2F;&#x2F; true\nlet str = &quot;前端胖头鱼&quot;;\nconsole.log(jsonstringify(str) === JSON.stringify(str));\n&#x2F;&#x2F; true\nlet reg = new RegExp(&quot;&#x5C;w&quot;);\nconsole.log(jsonstringify(reg) === JSON.stringify(reg));\n&#x2F;&#x2F; true\nlet date = new Date();\nconsole.log(jsonstringify(date) === JSON.stringify(date));\n&#x2F;&#x2F; true\nlet sym = Symbol(&#x27;前端胖头鱼&#x27;);\nconsole.log(jsonstringify(sym) === JSON.stringify(sym));\n&#x2F;&#x2F; true\nlet array = [1, 2, 3];\nconsole.log(jsonstringify(array) === JSON.stringify(array));\n&#x2F;&#x2F; true\nlet obj = {\n  name: &#x27;前端胖头鱼&#x27;,\n  age: 18,\n  attr: [&#x27;coding&#x27;, 123],\n  date: new Date(),\n  uni: Symbol(2),\n  sayHi: function () {\n    console.log(&quot;hello world&quot;)\n  },\n  info: {\n    age: 16,\n    intro: {\n      money: undefined,\n      job: null\n    }\n  },\n  pakingObj: {\n    boolean: new Boolean(false),\n    string: new String(&#x27;前端胖头鱼&#x27;),\n    number: new Number(1),\n  }\n}\nconsole.log(jsonstringify(obj) === JSON.stringify(obj)) \n&#x2F;&#x2F; true\nconsole.log((jsonstringify(obj)))\n&#x2F;&#x2F; {&quot;name&quot;:&quot;前端胖头鱼&quot;,&quot;age&quot;:18,&quot;attr&quot;:[&quot;coding&quot;,123],&quot;date&quot;:&quot;2021-10-06T14:59:58.306Z&quot;,&quot;info&quot;:{&quot;age&quot;:16,&quot;intro&quot;:{&quot;job&quot;:null}},&quot;pakingObj&quot;:{&quot;boolean&quot;:false,&quot;string&quot;:&quot;前端胖头鱼&quot;,&quot;number&quot;:1}}\nconsole.log(JSON.stringify(obj))\n&#x2F;&#x2F; {&quot;name&quot;:&quot;前端胖头鱼&quot;,&quot;age&quot;:18,&quot;attr&quot;:[&quot;coding&quot;,123],&quot;date&quot;:&quot;2021-10-06T14:59:58.306Z&quot;,&quot;info&quot;:{&quot;age&quot;:16,&quot;intro&quot;:{&quot;job&quot;:null}},&quot;pakingObj&quot;:{&quot;boolean&quot;:false,&quot;string&quot;:&quot;前端胖头鱼&quot;,&quot;number&quot;:1}}\n\n&#x2F;&#x2F; 3. 测试可遍历对象\nlet enumerableObj = {}\n\nObject.defineProperties(enumerableObj, {\n  name: {\n    value: &#x27;前端胖头鱼&#x27;,\n    enumerable: true\n  },\n  sex: {\n    value: &#x27;boy&#x27;,\n    enumerable: false\n  },\n})\n\nconsole.log(jsonstringify(enumerableObj))\n&#x2F;&#x2F; {&quot;name&quot;:&quot;前端胖头鱼&quot;}\n\n&#x2F;&#x2F; 4. 测试循环引用和Bigint\n\nlet obj1 = { a: &#x27;aa&#x27; }\nlet obj2 = { name: &#x27;前端胖头鱼&#x27;, a: obj1, b: obj1 }\nobj2.obj = obj2\n\nconsole.log(jsonstringify(obj2))\n&#x2F;&#x2F; TypeError: Converting circular structure to JSON\nconsole.log(jsonStringify(BigInt(1)))\n&#x2F;&#x2F; TypeError: Do not know how to serialize a BigInt\n\n</code></pre><p>通过上面测试可以看出，<code>jsonstringify</code>基本和<code>JSON.stringify</code>表现一致，（也有可能测试用例不够全面，欢迎提出一起学习）</p>\n<h2>结尾</h2>\n<blockquote>\n<p>因为一个BUG，重学了<code>JSON.stringify</code>,了解到原来它还有这么多平时没有注意到特性，前端娱乐圈水太深了，愿大家都被温柔以待，少些bug，多些关怀。晚安</p>\n</blockquote>\n<p>\n        版权声明：著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n        作者: 前端胖头鱼\n        原文链接：<a href=\"https://juejin.im/post/7017588385615200270\">https://juejin.im/post/7017588385615200270</a>\n      </p></div>","title":"就因为JSON.stringify，我的年终奖差点打水漂了__JavaScript__Vue.js__前端","last_reply_at":"2022-01-17T11:15:00.860Z","good":false,"top":false,"reply_count":0,"visit_count":4753,"create_at":"2022-01-17T11:15:00.860Z","author":{"loginname":"banyungong","avatar_url":"//gravatar.com/avatar/03a45cfe3f797edb29d8faf2e3da64bc?size=48"}}]}