加一个button.js

image.png
在index.html中使用
image.png
image.png

小结:在button.js中写的Vue.component(‘g-button’,{ template:<button>hi</button> }) ‘g-button’指的是在index中使用的标签名,template是具体的html代码 {//这是一个对象 template:<button>hi</button> }


button样式

image.png
这里设置了g-button的字体大小,但是有可能”你好请点击”的字体大小不是14px,将会导致不协调,此处可以让用户自己设置。

方法一:

使用变量(推荐)
image.png
效果:
image.png

方法二:

使用css覆盖(不推荐,比较麻烦)


使用单文件的模式开发

从上面的代码可以看出,button是写在js文件里面的,样式是写在css里面的,这样不好管理代码,所以使用parcel来进行开发。
parcel官网
C.R.M学习法启动:

``` yarn global add parcel-bundler //yarn方式全局安装 npm install -g parcel-bundler //npm方式全局安装 推荐不使用全局安装所以可以改写成: npm install -D parcel-bundler

相比于项目开始的时候安装vue使用全局的原因是,vue是给用户使用的,所以使用全局安装

  1. <a name="hllOC"></a>
  2. ### 在项目根目录添加src文件夹
  3. 1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597980978688-df8a7e04-8f55-43d5-85cf-9e1a2d3c19a2.png#align=left&display=inline&height=300&margin=%5Bobject%20Object%5D&name=image.png&originHeight=300&originWidth=363&size=20344&status=done&style=none&width=363)
  4. 1. 在src目录下创建app.js并且把index文件的js迁移过去
  5. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597981088029-a58d8a07-8496-4e2f-b3d6-a16b3b258827.png#align=left&display=inline&height=167&margin=%5Bobject%20Object%5D&name=image.png&originHeight=167&originWidth=441&size=10307&status=done&style=none&width=441)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597981108479-ba587beb-20d8-44b1-b1f5-187024f64191.png#align=left&display=inline&height=193&margin=%5Bobject%20Object%5D&name=image.png&originHeight=193&originWidth=423&size=14248&status=done&style=none&width=423)<br />3.在index文件中使用依赖的方式引入app.js<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597981201820-63ac9e9f-7900-4de3-8dde-e1fcd01cc33a.png#align=left&display=inline&height=200&margin=%5Bobject%20Object%5D&name=image.png&originHeight=200&originWidth=467&size=15917&status=done&style=none&width=467) <br />4.使用vue文件重构button.js<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597981532860-43822799-8e32-489b-9c28-9bb26e92a721.png#align=left&display=inline&height=188&margin=%5Bobject%20Object%5D&name=image.png&originHeight=188&originWidth=515&size=21071&status=done&style=none&width=515)
  6. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597981561748-aae8c3bb-bf15-408b-ab2a-20735d70034a.png#align=left&display=inline&height=738&margin=%5Bobject%20Object%5D&name=image.png&originHeight=738&originWidth=690&size=93215&status=done&style=none&width=690)
  7. > <template></template>里面存放html内容
  8. > <script></script> 使用 export default导出对象
  9. > <style></style>用来存放css文件
  10. 在代码迁移完以后,删除button.js文件<br />5.app.js初始化ID为'app',但是index无法识别<g-button><br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597981932414-de29f3d1-85d5-4cb7-b48a-a579011cdd90.png#align=left&display=inline&height=174&margin=%5Bobject%20Object%5D&name=image.png&originHeight=174&originWidth=467&size=25410&status=done&style=none&width=467)<br />需要在app.js中导入vue<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597982298675-5142c40c-c35a-48d6-bbf1-71177789182e.png#align=left&display=inline&height=182&margin=%5Bobject%20Object%5D&name=image.png&originHeight=182&originWidth=629&size=20097&status=done&style=none&width=629)
  11. > 1.引入button.vue并且取名Button(类似变量名)
  12. > 2.声明为Vue的组件
  13. 6.测试<br />控制台输入./node_modules/.bin/parcel 运行项目会报错,找不到入口<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597982741437-15e04543-73f5-43d7-a962-5bfa84d64812.png#align=left&display=inline&height=119&margin=%5Bobject%20Object%5D&name=image.png&originHeight=119&originWidth=1122&size=27637&status=done&style=none&width=1122)<br />使用./node_modules/.bin/parcel index.html (这里指定运行的入口为index.html)即可解决
  14. 在浏览器运行的时候,出现问题了:页面没有东西,控制台报错<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597984625617-07e6c786-1ac5-4e2d-9234-a7a61d57eb91.png#align=left&display=inline&height=257&margin=%5Bobject%20Object%5D&name=image.png&originHeight=257&originWidth=1640&size=55355&status=done&style=none&width=1640)<br />提示我的vue使用的是run-time版本,所以报错,在Vue官方文档中的[切换版本](https://cn.vuejs.org/v2/guide/installation.html)内容上面找到对应的版本进行代码添加<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597984799581-ea6f3e6d-8ac4-4b25-aa1e-1c637dae3c3e.png#align=left&display=inline&height=281&margin=%5Bobject%20Object%5D&name=image.png&originHeight=281&originWidth=719&size=17118&status=done&style=none&width=719)<br />添加完成后再次运行项目,即可解决。<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1597984846853-443033c8-2585-47b7-8be1-e87257d4d29a.png#align=left&display=inline&height=189&margin=%5Bobject%20Object%5D&name=image.png&originHeight=189&originWidth=349&size=3472&status=done&style=none&width=349)
  15. ---
  16. <a name="nFmin"></a>
  17. ## 插槽的使用
  18. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1598018556966-dfa43724-6d3f-4321-9f8d-40d4d31a9ab1.png#align=left&display=inline&height=122&margin=%5Bobject%20Object%5D&name=image.png&originHeight=122&originWidth=492&size=12298&status=done&style=none&width=492)<br />button组件中,写死了一个按钮1,用户更希望这些按钮的文字由自己决定,如果直接写在index文件中,是无法读取的<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1598018636887-a11daa6a-71ef-4435-aa2f-9d50bfe3e973.png#align=left&display=inline&height=87&margin=%5Bobject%20Object%5D&name=image.png&originHeight=87&originWidth=316&size=6150&status=done&style=none&width=316)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1598018647731-2755c9ef-a416-4159-8e7d-afd79c180b84.png#align=left&display=inline&height=122&margin=%5Bobject%20Object%5D&name=image.png&originHeight=122&originWidth=194&size=1697&status=done&style=none&width=194)<br />解决办法:使用<slot></slot>(插槽)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1598018709410-23336deb-8a07-46eb-b4d7-7df60dddd21a.png#align=left&display=inline&height=108&margin=%5Bobject%20Object%5D&name=image.png&originHeight=108&originWidth=362&size=8616&status=done&style=none&width=362)<br />添加了插槽以后,就可以在index文件中添加文字了<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1598018736190-fbdd0977-1844-441b-8309-198f158867d8.png#align=left&display=inline&height=83&margin=%5Bobject%20Object%5D&name=image.png&originHeight=83&originWidth=106&size=909&status=done&style=none&width=106)
  19. ---
  20. <a name="0sQul"></a>
  21. ## 添加Icon
  22. 在[Icon仓库](https://www.iconfont.cn/)查找自己想要的Icon,然后在index页面引入js<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1598019912503-e8b918ba-d414-41bf-a908-8118c4737930.png#align=left&display=inline&height=37&margin=%5Bobject%20Object%5D&name=image.png&originHeight=37&originWidth=734&size=8520&status=done&style=none&width=734)<br />这时候刷新页面是看不到Icon的,但是在控制台可以看到svg标签出现在html中<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1598019991233-ee46c318-55d9-4601-85e8-f455820d95d2.png#align=left&display=inline&height=169&margin=%5Bobject%20Object%5D&name=image.png&originHeight=169&originWidth=656&size=39477&status=done&style=none&width=656)
  23. <a name="Wwlf0"></a>
  24. ### 让Icon出现在页面中
  25. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/1444038/1598020120472-ad92355f-9e10-4d26-984f-8af2910a85a4.png#align=left&display=inline&height=85&margin=%5Bobject%20Object%5D&name=image.png&originHeight=85&originWidth=838&size=12490&status=done&style=none&width=838)
  26. ```html
  27. <g-button><svg><use xlink:href="#icon-setting"></use></svg></g-button>
  28. #icon-setting => 指的是在IconFont中设置的Icon的名字

image.png

设置Icon的大小(全局样式)

image.png

把Icon封装到对应的组件中,方便使用者,让用户自己选择

image.png
从代码中分析得出,选择哪个button是由”#icon-xxx”中的xxx来决定的,所以这里可以使用prop来接收用户外部传进来的参数
button组件中,props:[‘icon’]可以理解为形参,index文件中的icon=”setting”可以理解为实参

image.png
image.png
现在就可以接收外部传进来的参数了
在button组件中,把use标签修改为如下:
image.png

  1. <use :xlink:href="`#icon-${icon}`"></use>
  2. xlink左边的 : 是v-bind的缩写,指的是绑定xlink:href这个属性
  3. "" 可以去掉
  4. ``反引号表示的是这是一句js字符串
  5. ${} 表示这是一个插值,这个值是icon,icon的值是从props中获取的

解决一个Bug

如果用户不传icon,页面上面不显示icon,但是会出现空白的展位,控制台能查看到由svg标签
image.png
解决办法:使用v-if判断icon是否存在,如果存在,就让svg出现
image.png
如果不存在则让svg不出现
image.png

实现根据用户设置icon的位置(左、右)、icon居中

思路:让用户传入一个 iconPosition ,值为:left 或者 right
image.png
实现方法1(使用v-if v-else):
image.png
image.png
方法2:通过css控制
image.png
绑定一个class,值为一个对象,key为icon-right\left\undefined,value为true
image.png
在slot外层添加一层div,原因:如果在slot上面添加class,这个class会自动消失,所以通过添加一个class为content的div解决
image.png
让g-button变成flex布局,并且让icon居中
image.png
使用order实现默认情况下,icon在前面,content在后面
image.png
如果是right,那么icon在后面,content在前面
image.png
image.png

三个按钮无法上下对齐bug

image.png
原因:内联元素没有对齐,所有inline元素都有这个问题
解决办法:添加如下代码

  1. vertical-align: middle;

image.pngimage.png

设置icon与文字的间距

image.png
image.png

如果iconPositon的值为undefined时

使用props对象解决,key为值的名字,value为值的配置
image.png
image.png

使用属性检查器确保用户只能输入left和right

image.png
简化写法
image.png

icon单文件

创建icon.vue文件,然后把icon的html、css迁移过来,再使用一个变量name用来接收用户输入的参数
image.png
创建icon组件
image.png
使用icon组件
image.png
image.png
此时发现button的icon都是出现在左侧,在button.vue文件的添加class=’icon’即可解决
image.png
image.png

实现icon的loading动画特效

给g-icon添加class=”loading”
image.png
插入动画帧并且给动画帧取名spin
image.png
设置动画帧的属性
image.png
效果
image.png

实现点击button出现、关闭loading

添加loading属性,默认值为false
image.png
绑定一个loading属性
image.png
判断是否为loading,如果true,则显示为loading
image.png

解决一个bug

如果button已经有icon,并且它的loading为true,那么就会出现下面的情况
image.png
解决:添加判断,如果有icon并且没有loading,那么就显示原来的icon,否则不显示原有的icon,显示loading
image.png
添加icon类,让loading出现的位置与原来的icon的位置一致(使用同一个css)
image.png

点击button出现loading,再次点击消失

要实现可以切换true/false,就必须让:loading的值是一个变量,不能写死
image.png
所以true改成变量loading1,并且在app.js中添加一个data,默认值为false
image.png
当click时,变量的值取反
image.png
此时点击这个button没有反应,原因是因为是我们自己写的,不是原本自带的,所以需要在button中触发这个click,这里有个关于this的知识点,vue为了让开发者更容易地使用vue,所以做了个语法糖,也就是下面的简便写法
这是简便的写法
image.png
这是复杂的写法,其实是还原为this的写法
image.png
上面的代码,可以再简写:
image.png
由于vue的机制是在template里面写代码的时候,不需要加this,所以可以把this去掉,最终变成了简便写法

解决一个bug

由于在指定click触发事件的时候,把click事件写在g-icon上,导致点击button无法触发,在把click触发事件放在g-button上即可解决
image.png

Kapture 2020-08-22 at 23.51.01.gif

工具推荐

在上一小节解决bug的时候发现,如果不用gif录下来,会导致很多效果看不了,所以google了一下有没有mac适合的gif录制软件,结果找到了,在这里分享一下,叫做 Kap 有兴趣的可以点击看看,纯开源,软件有点大,但是用起来挺方便,还在摸索当中


button group(当两个按钮放在一起)

从开发者角度实现原始效果:

image.png

让两个按钮合并在一起

创建button-group组件
image.png
组件全局化
image.png
遇到问题:不能使用slot作为根结点,因为slot有可能是两个按钮,vue不支持有两个组件:
image.png
解决办法:在slot外层添加一层div
image.png
目前的效果:
image.png
优化:
image.png

发现问题1(这是一个坑)

image.png
如果我在两个按钮之间再添加一个按钮,那么根据上面的css,就会把更多按钮作为第二个子元素,导致后面的两个元素出现问题,解决办法就是把nth-child(2)改成last-child
image.png
image.png
接下来,让边框合并:
image.png
此时第一个元素的做边框不见了,解决办法:判断如果不是第一个子元素,则让做边框为none
image.png


发现问题2(通过负margin z-index解决)

使用上面的方法会出现一个问题:做边框并不是真的合并了,而是被隐藏了而已,
Kapture 2020-08-24 at 22.35.52.gif
添加负margin以后的效果:
image.png
Kapture 2020-08-24 at 22.35.52.gif
此时由于边框被遮挡了,所以添加hover,让这个button往上浮
Kapture 2020-08-24 at 22.42.34.gif
接下来让下一页的icon往右
image.png


防止用户误操作导致样式丢失

有些用户可能会在button group的子元素中包裹一层div,那么会导致我们的css丢失
image.png
解决办法:使用mounted函数(在元素被挂载的时候调用),判断用户是否有在子元素外面包裹div或者其他标签,如果有,则给出警告
image.png


单元测试

使用chai.js

npm install -D chai安装

开始单元测试

image.png

image.png
image.png
给动态生成的按钮添加一个icon
image.png

image.png
浏览器提示与你期望的setting不匹配,测试不通过
image.png
修改为#icon-setting后,测试通过,期望值与原本的值一致
image.png

测试的依据:

一般是由输入参数决定的,比如项目当中使用的icon、loading、iconPosition还有触发的事件
image.png

image.png

监听click事件的时候需要使用mock

npm i -D spies
image.png
image.png

自动化测试

后期补充


input组件

创建input.vue文件

  1. //input.vue
  2. <template>
  3. <div>
  4. <input type="text">
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. }
  10. </script>
  11. <style lang="scss"></style>

注册全局组件

  1. //app.js
  2. import Input from './input'
  3. Vue.component('g-input',Input)

页面中添加组件

  1. //index.html
  2. <g-input></g-input>

image.png


scoped

不使用scoped:

  1. //input.vue
  2. <template>
  3. <div class="wrapper">
  4. <input type="text">
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name:'GuluInput'
  10. }
  11. </script>
  12. <style lang="scss">
  13. /*--button-height: 32px;*/
  14. /*--font-size: 14px;*/
  15. /*--button-bg: white;*/
  16. /*--button-active-bg: #eee;*/
  17. /*--border-radius: 4px;*/
  18. /*--color: #999;*/
  19. /*--border-color: #999;*/
  20. /*--border-color-hover: #666;*/
  21. $height:32px;
  22. $border-color:#999;
  23. .wrapper{
  24. > input{
  25. height:$height;
  26. border: 1px solid $border-color;
  27. }
  28. }
  29. </style>

image.png
使用scoped:

  1. //input.vue
  2. <template>
  3. <div class="wrapper">
  4. <input type="text">
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name:'GuluInput'
  10. }
  11. </script>
  12. <style lang="scss" scoped>
  13. /*--button-height: 32px;*/
  14. /*--font-size: 14px;*/
  15. /*--button-bg: white;*/
  16. /*--button-active-bg: #eee;*/
  17. /*--border-radius: 4px;*/
  18. /*--color: #999;*/
  19. /*--border-color: #999;*/
  20. /*--border-color-hover: #666;*/
  21. $height:32px;
  22. $border-color:#999;
  23. .wrapper{
  24. > input{
  25. height:$height;
  26. border: 1px solid $border-color;
  27. }
  28. }
  29. </style>

image.png
在wrapper中添加border属性后,再次对比:
image.png
在button.vue组件中添加scoped:

  1. <style lang="scss" scoped>
  2. @keyframes spin {
  3. 0% {
  4. transform: rotate(0deg);
  5. }
  6. 100% {
  7. transform: rotate(360deg)
  8. }
  9. }
  10. .g-button {
  11. height: var(--button-height);
  12. padding: 0 1em;
  13. font: inherit;
  14. border-radius: var(--border-radius);
  15. border: 1px solid var(--border-color);
  16. background: var(--button-bg);
  17. display: inline-flex;
  18. justify-content: center;
  19. align-items: center;
  20. vertical-align: middle;
  21. &:hover {
  22. border-color: var(--border-color-hover);
  23. }
  24. &:active {
  25. background-color: var(--button-active-bg);
  26. }
  27. &:focus {
  28. outline: none;
  29. }
  30. > .icon {
  31. order: 1;
  32. margin-left: 0;
  33. margin-right: 0.1em;
  34. }
  35. > .content {
  36. order: 2;
  37. }
  38. &.icon-right {
  39. > .icon {
  40. order: 2;
  41. margin-right: 0;
  42. margin-left: 0.1em;
  43. }
  44. > .content {
  45. order: 1;
  46. }
  47. }
  48. > .loading {
  49. animation: spin 1s infinite linear;
  50. }
  51. }
  52. </style>

image.png

通过对比可以发现,添加了scoped后,scoped会对当前文件里面的标签的所有元素上面自动添加一个唯一的id属性(也是这个组件的唯一ID),这样就可以防止别人跟自己的选择器发生冲突导致样式错乱了。


template的另一个用法

image.png
要完成上图的效果,有两种方式:
方法一:
在判断的时候用template标签包裹

  1. <template>
  2. <div class="wrapper" :class="{'error':error}">
  3. <input type="text" :value="value" :disabled="disable" :readonly="readonly"/>
  4. <template v-if="error">
  5. <icon name="setting"></icon>
  6. <span>{{error}}</span>
  7. </template>
  8. </div>
  9. </template>

方法二:
使用div包裹,然后写两个v-if

  1. <template>
  2. <div class="wrapper" :class="{'error':error}">
  3. <input type="text" :value="value" :disabled="disable" :readonly="readonly"/>
  4. <div class="xxx">
  5. <icon v-if="error" name="setting"></icon>
  6. <span v-if="error">{{error}}</span>
  7. </div>
  8. </div>
  9. </template>

相对于方法一和方法二,方法二还需要再给div取一个class,再去匹配样式,比较麻烦,推荐使用方法一,使用template


测试驱动开发

  1. it('接收 只读',()=>{
  2. const Constructor = Vue.extend(Input)
  3. const vm = new Constructor({
  4. propsData:{
  5. readonly:true
  6. }
  7. }).$mount()
  8. const inputElement = vm.$el.querySelector('input')
  9. console.log(inputElement.outerHTML)//输出inputElemeent的值
  10. expect(inputElement.readOnly).to.equal(true)
  11. vm.$destroy()
  12. })

在测试的时候发现,如果expect(inputElement.readOnly).to.equal(true)的readOnly写成readonly会导致测试不通过。
image.png

遇到问题

  1. //正确的测试用例
  2. it('接收 error',()=>{
  3. const Constructor = Vue.extend(Input)
  4. const vm = new Constructor({
  5. propsData:{
  6. error:"错误信息"
  7. }
  8. }).$mount()
  9. const useElement = vm.$el.querySelector('use')
  10. expect(useElement.getAttribute('xlink:href')).to.equal('#icon-error')
  11. const errorMessage = vm.$el.querySelector('.error-message')
  12. expect(errorMessage.innerText).to.equal('错误信息')
  13. vm.$destroy()
  14. })
  15. })

在测试error的时候,分为几个步骤:

  1. 测试icon是否为error
  2. 测试props传入的错误信息是否与期待值是一致的

在进行测试的时候发现,截止到测试icon都是正确的,但是到测试错误信息的时候发现怎么也不通过,原因:
把innerText写成了innerHtml
image.png
再次尝试又出现问题:
image.png

  1. const errorMessage = vm.$el.querySelector('.error-message')

我把 .error-message 写成了~~.errorMessage ~~ , 这里指的是input组件中错误信息的class

  1. <span class="error-message">{{error}}</span>

小结

要注意你要测试的那个元素若果有自己的class,那么写用例的时候一定要保证一致,在用例中写了props,那么你expect的结果也要跟你props的一致,否则不通过。

事件监听测试

  1. describe('事件',()=>{
  2. const Constructor = Vue.extend(Input)
  3. let vm
  4. afterEach(()=>{
  5. vm.$destroy()
  6. })
  7. it('支持 change事件',()=>{
  8. vm = new Constructor({}).$mount()
  9. const callback = sinon.fake()
  10. vm.$on('change',callback)
  11. var event = new Event('change')
  12. let inputElement=vm.$el.querySelector('input')
  13. inputElement.dispatchEvent(event)
  14. expect(callback).to.have.been.calledWith(event)
  15. })
  16. it('支持 input事件',()=>{
  17. vm = new Constructor({}).$mount()
  18. const callback = sinon.fake()
  19. vm.$on('input',callback)
  20. var event = new Event('input')
  21. let inputElement=vm.$el.querySelector('input')
  22. inputElement.dispatchEvent(event)
  23. expect(callback).to.have.been.calledWith(event)
  24. })
  25. it('支持 focus事件',()=>{
  26. vm = new Constructor({}).$mount()
  27. const callback = sinon.fake()
  28. vm.$on('focus',callback)
  29. var event = new Event('focus')
  30. let inputElement=vm.$el.querySelector('input')
  31. inputElement.dispatchEvent(event)
  32. expect(callback).to.have.been.calledWith(event)
  33. })
  34. it('支持 blur事件',()=>{
  35. vm = new Constructor({}).$mount()
  36. const callback = sinon.fake()
  37. vm.$on('blur',callback)
  38. var event = new Event('blur')
  39. let inputElement=vm.$el.querySelector('input')
  40. inputElement.dispatchEvent(event)
  41. expect(callback).to.have.been.calledWith(event)
  42. })
  43. })

优化上述代码

  1. describe('事件', function () {
  2. var Constructor = _vue.default.extend(_input.default);
  3. var vm;
  4. afterEach(function () {
  5. vm.$destroy();
  6. });
  7. it('支持 change/input/focus/blur事件', () => {
  8. ['change', 'input', 'focus', 'blur'].forEach((eventName) => {
  9. vm = new Constructor({}).$mount();
  10. const callback = sinon.fake();
  11. vm.$on(eventName, callback);
  12. let event = new Event(eventName);
  13. let inputElement = vm.$el.querySelector('input');
  14. inputElement.dispatchEvent(event);
  15. console.log(eventName)
  16. expect(callback).to.have.been.calledWith(event);
  17. });
  18. });

后期如果需要测试更多的事件,只需要在数组中添加即可

让input支持v-model(双向绑定)

举个例子:
通过一个input组件,实现input的时候,message的值可以自动改变,然后添加一个button,让message+1,实现message发生改变,input组件的内容也可以自动改变

  1. //app.js
  2. new Vue({
  3. el: '#app',
  4. data: {
  5. ......
  6. message:'hi'
  7. }
  8. })
  1. //input.vue
  2. //原本我本绑定的是event,如果要完成双向绑定,则需要获取event对应target的值
  3. <div class="wrapper" :class="{'error':error}">
  4. <input type="text" :value="value" :disabled="disable" :readonly="readonly"
  5. @change="$emit('change',$event.target.value)"
  6. @input="$emit('input',$event.target.value)"
  7. @focus="$emit('focus',$event.target.value)"
  8. @blur="$emit('blur',$event.target.value)"
  9. />
  1. //index.html
  2. //原本使用的是:value绑定一个value,现在可以使用v-model的语法糖实现双向绑定
  3. <div class="box">
  4. <g-input v-model="message"></g-input>
  5. <p>{{message}}</p>
  6. <button @click="message+=1">+1</button>
  7. </div>

效果图
Kapture 2020-08-29 at 10.07.11.gif

遇到问题

在跑测试用例的时候,发现报错

  1. //input.test.js
  2. it('支持 change/input/focus/blur事件', () => {
  3. ['change', 'input', 'focus', 'blur'].forEach((eventName) => {
  4. vm = new Constructor({}).$mount()
  5. const callback = sinon.fake()
  6. vm.$on(eventName, callback)
  7. let event = new Event(eventName)
  8. event.target = {//给出一个假定的targe
  9. value:'hi'
  10. }
  11. let inputElement = vm.$el.querySelector('hi')
  12. console.log(event)
  13. inputElement.dispatchEvent(event)
  14. expect(callback).to.have.been.calledWith(event.target.value)//判断target的值
  15. })
  16. })

测试结果给出当前对象是readonly属性,不能添加target
image.png
添加如下代码解决:

  1. //input.test.js
  2. it('支持 change/input/focus/blur事件', () => {
  3. ['change', 'input', 'focus', 'blur'].forEach((eventName) => {
  4. vm = new Constructor({}).$mount()
  5. const callback = sinon.fake()
  6. vm.$on(eventName, callback)
  7. let event = new Event(eventName)
  8. Object.defineProperty(
  9. event,'target',{value:{value:'hi'},enumerable:true})
  10. let inputElement = vm.$el.querySelector('hi')
  11. inputElement.dispatchEvent(event)
  12. expect(callback).to.have.been.calledWith(event.target.value)//判断target的值
  13. })
  14. })

小结:

在之前的测试中,我们calledWith的是event,在测试双向绑定的时候测试的是出啊发event.target的值,但是event缺少target的值,我们无法直接通过event获取,而且在测试的时候是没有这个值的,所以我们可以通过Object.defineProperty这个API让浏览器自动补全的target的值。


网格系统(栅格系统)

伪定义:把一个div分成n个部分(n=12,24,36…),每个部分无空隙或者有空隙,主要用于做横向布局

雏形

使用row和col实现

  1. //col.vue
  2. <template>
  3. <div class="col">
  4. <slot></slot>
  5. </div>
  6. </template>
  7. <style scoped lang="scss">
  8. .col{
  9. height: 100px;
  10. background: grey;
  11. width: 50%;
  12. border:1px solid red;
  13. $class-prefix:col-;//生成class前缀
  14. @for $n from 1 through 24{
  15. &.#{$class-prefix}#{$n}{//scss的插值语法:前缀-n 例如: col-1 col-2
  16. width: ($n / 24) * 100%;
  17. }
  18. }
  19. }
  20. </style>

重点代码

  1. $class-prefix:col-;//生成class前缀
  2. @for $n from 1 through 24{
  3. &.#{$class-prefix}#{$n}{//scss的插值语法:前缀-n 例如: col-1 col-2
  4. width: ($n / 24) * 100%;
  5. }
  6. }
  7. //这一块代码可以通过scss的forEach生成 前缀-n 的class。然后通过class自动生成对应的布局
  1. //index.html
  2. <g-row>
  3. <g-col>1</g-col>
  4. <g-col>2</g-col>
  5. </g-row>
  6. <g-row>
  7. <g-col>1</g-col>
  8. <g-col>2</g-col>
  9. <g-col>3</g-col>
  10. </g-row>
  11. <g-row>
  12. <g-col>1</g-col>
  13. <g-col>2</g-col>
  14. <g-col>3</g-col>
  15. <g-col>4</g-col>
  16. </g-row>

基本效果
image.png
浏览器控制台的代码效果image.png

遇到一个问题

如果是1:11的比例,那么就会无法正常显示
image.png

解决问题

  1. //index.html
  2. <g-row>
  3. <g-col span="2">1</g-col>
  4. <g-col span="22">11</g-col>
  5. </g-row>

这次不使用:span绑定,直接使用span属性,但是这样的话span就是一个字符串,所以需要在props中添加属性,span表示一个对象,type表示属性,[Number,String]表示属性值

  1. <script>
  2. export default {
  3. name:'GuluCol',
  4. props:{
  5. span:{
  6. type:[Number,String]}
  7. }
  8. }
  9. </script>

image.png

小结

如果要做成非对称的布局,只需要添加一个span即可


实现根据用户传入的参数决定中间有空隙的效果

image.png

  1. //index.html
  2. <g-row>
  3. <g-col span="2"></g-col>
  4. <g-col span="20" offset="2"></g-col>
  5. </g-row>
  6. <g-row>
  7. <g-col span="2" offset="10"></g-col>
  8. <g-col span="10" offset="2"></g-col>
  9. </g-row>
  10. <g-row>
  11. <g-col span="2"></g-col>
  12. <g-col span="4" offset="4"></g-col>
  13. <g-col span="4" offset="2"></g-col>
  14. <g-col span="8" ></g-col>
  15. </g-row>

绑定offset(偏移)属性

  1. //col.vue
  2. <template>
  3. <div class="col" :class="[`col-${span}`, `offset-${offset}`]">
  4. <slot></slot>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. name: 'GuluCol',
  10. props: {
  11. span: {
  12. type: [Number, String]
  13. },
  14. offset: {
  15. type: [Number, String]
  16. }
  17. }
  18. }
  19. </script>
  20. $class-prefix:offset-;
  21. @for $n from 1 through 24{
  22. &.#{$class-prefix}#{$n}{
  23. margin-left: ($n/24)*100%;
  24. }
  25. }

指向绑定的offset、声明offset属性、遍历offset

小结

因为偏移、布局的宽度最大值为24,所以这两个值相加的和不能超过24


实现生成固定的空隙

尝试使用margin:0 10px;
image.png
这里没有对齐

改用padding:0 10px;
image.png
此时发生了偏移,而且也对齐了,但是偏移的效果不明显,只能通过对比每个格子中的数字才能看出偏移
image.png
加上border后,方便观察,通过观察添加padding后和背景色的效果发现,border和左右的页面宽度之间有10px的padding,解决的办法(负margin):

  1. //row.vue
  2. <template>
  3. <div class="row">
  4. <slot></slot>
  5. </div>
  6. </template>
  7. <style scoped lang="scss">
  8. .row{
  9. display: flex;
  10. margin:0 -10px;
  11. }
  12. </style>

效果图:
image.png

实现用户自己设置固定大小的间隙

  1. //index.html
  2. <g-row gutter="20">
  3. <g-col>1</g-col>
  4. <g-col>2</g-col>
  5. </g-row>

使用钩子实现基本功能

mounted和created的区别:
created创建完对象以后并没有把对象放进页面中,mounted是在创建完对象的一瞬间把对象放进页面中
mounted和created的顺序:
先创建父元素,然后创建子元素,然后挂载子元素,最后挂载父元素
image.png


模仿淘宝首页

topbar

image.png

  1. //index.html
  2. <style>
  3. .demoBox{
  4. height:50px;
  5. background: grey;
  6. border:1px solid red;
  7. }
  8. </style>
  9. <g-row class="topbar">
  10. <g-col class="demoBox" span="9">
  11. <g-row>
  12. <g-col>1</g-col>
  13. <g-col>2</g-col>
  14. <g-col>3</g-col>
  15. <g-col>4</g-col>
  16. </g-row>
  17. </g-col>
  18. <g-col class="demoBox" span="15">
  19. <g-row>
  20. <g-col>1</g-col>
  21. <g-col>2</g-col>
  22. <g-col>3</g-col>
  23. <g-col>4</g-col>
  24. <g-col>5</g-col>
  25. <g-col>6</g-col>
  26. <g-col>7</g-col>
  27. </g-row>
  28. </g-col>
  29. </g-row>

image.png

小技巧:

在创建标签的时候,让webstorm自动填充内容可以使用以下代码: