简单轮子:文本输入框
一、如何书写README
当我们项目中某个文件夹中有很多文件,并且是配置文件时,比如node_module文件,webstorm读取时会耗费大量时间,而且还可能会造成webstorm卡顿。这时我们可以让webstorm忽略这个文件夹。
一般我们会将dist、node_modules
目录排除在外,这样不会影响webstorm性能
README文件应该包含哪些东西?
# G-Design-of-Vue
## 介绍
## 开始使用
### 安装
## 文档
## 提问
## 变更记录
## 联系方式
## 贡献代码
1.我们要给项目添加持续集成的标,搜索下如circleci badge
如下图中vue的持续集成的标
我们只需要连接地址中的查询参数,换成我们自己的分支的名字即可
2.添加npm的标 搜索npm上的包名
3.添加 css 样式
i.使用本组件库中,请在css中开启 border-box
*,::before,::after{margin: 0;padding: 0;box-sizing: border-box;}
(IE 8 及以上浏览器都支持此样式)
ii. 设置默认颜色变量(后续会改为scss变量)
html{
--button-height: 32px;
--font-size: 14px;
--button-bg: white;
--button-active-bg: #eee;
--border-radius: 4px;
--color: #333;
--border-color: #999;
--border-color-hover: #666;
--border-active-bg: #d3d2d2;
}
(IE 15 及以上浏览器都支持此样式)
4.安装 g-design-of-vue
npm i g-design-of-vue --save
- 引入 g-design-of-vue
import { GButton,GButtonGroup,GIcon } from 'g-design-of-vue';
import '../node_modules/g-design-of-vue/dist/index.css'
export default {
name: 'App',
components: {
'g-button': GButton,
'g-icon': GIcon,
'g-button-group': GButtonGroup
}
}
- 接下来,要让用户不引入iconfont中的 svg symbol 链接地址,也可以使用我们的icon
如何做到用户不引入这个地址,也能使用icon呢?
我们需要帮助用户引入
在根目录下新建 svg.js
,将iconfont中 symbol地址在浏览器打开,copy里面的代码,放到svg.js中
接着在Icon.vue组件中引入svg.js
即可,我们不需要导出什么,因为引入之后就会将svg放到页面中
下次我们如果要往iconfont中添加icon,只需要重新生成 svg symbol 地址,浏览器中打开这个地址,复制地址中的代码到svg.js
更新svg即可
在我们的项目中运行npx parcel --no-cache index.html
,访问浏览器
从中可以看出页面中已经有项目中的7个svg了
二、如何解决奇怪的bug
原因
问题出现的原因运行npx parcel
默认启动的是npx parcel index.js
而不是npx parcel index.html
过程
出现问题,第一时间一定要记得提交一次代码,这样我们才不会把代码弄丢
git log
或者 git reflog
查看历史提交记录
git reset --hard + 上图中commit的id
将会覆盖当前本地磁盘上的代码(在这之前一定要commit提交代码,否则如果做错了,就找不到之前的代码了)
git show + 上图中commit的id
就可以看到对应提交版本,文件的变更情况
解决问题
将npx parcel index.html
添加到package.json
中的script
脚本命令中,下次直接运行npm start
就可以了
"scripts": {
"start": "npx parcel index.html --no-cache",
"dev-test": "parcel watch test/* --no-cache && karma start",
"test": "parcel build test/* --no-cache --no-minify && karma start --single-run"
},
记得将之前发布到npm时修改的main,重新改到index.html作为入口文件
三、input组件需求分析
1. 用例分析
2. 确定input的状态
i. 正常的 normal 状态
ii. 鼠标悬浮时候的 hover 状态
iii. 鼠标键入时的focus 状态
iV. 禁止输入的 disabled 状态
V. 同disabled类似的只读状态 readonly 状态,区别是:readonly态的input是可以focusd,但是disabled不可以focus
以上五种状态都可以有下面的2种状态
i. error 失败状态
ii. success 成功状态
因此一共有10种状态
四、写input 样式
- 大部分时候组件中的name是没有用的,当我们在浏览器中装一个vue开发者工具插件才能看区别
export default {
name: 'g-input'
};
装上这个插件之后我们就可以,以组件的形式展示页面结构
因此组件中写name的作用是:在开发者工具下对组件进行自定义命名,方便调试
-
vue 中的模板语法是支持html语法,而
<g-input/>
这种标签自闭合的写法是xml的语法,因此我们要写成<g-input></g-input>
-
样式加上
scoped
的作用是让css也有作用域,加上scoped
后css样式只作用于当前组件,即使是其他组件也有一样的class,也不会出现相互覆盖的情况
<style lang="scss" scoped>
</style>
我们可以看到加了scoped后,组件样式后面都会加一个id,这样即使其他的组件也加同一名字,但是因为这个唯一id的存在就不会重命了
这个id是每个组件的唯一id,每个组件有不同的id,组件中的所有标签都会被同一个id标记
- scss中声明变量的方式是
$height: 32px;
- 查看源代码的方法:
- 如果子选择器中的样式想从父选择器中继承样式,可以在父选择器中写好样式,子选择器用
inherit
- vue中给组件传值
一种是:·<g-input value="张三" :disabled="true"></g-input>
另外一种是简写形式: <g-input value="李四" disabled></g-input>
:disabled="true"
和disabled="true"
的区别:
有冒号的,true是boolen
没冒号的,true是字符串
cursor: not-allowed;
的样式就是鼠标悬浮时会有禁止的提示- 写组件的思路: 在props添加组件可以拥有的属性并绑定这个属性,在调用组件的地方给组件传属性值
- vue中添加样式的对象写法
:class="{'error': error}
,意识如果error存在就添加error样式,它可以简写成:class="{error}
- 关于
template
标签
<icon v-if="error" name="setting"></icon>
<span v-if="error">{{error}}</span>
如果不想写2次v-if
就要在外面添加div
<div v-if="error">
<icon name="setting"></icon>
<span >{{error}}</span>
<div>
但是这样引入了一个无用的冗余标签,这样还要给div再额外的添加css
将div换成template,这样就既不会引入额外的div,又可以写if
<tempalte v-if="error">
<icon name="setting"></icon>
<span >{{error}}</span>
<template>
- webstorm中如果想折叠css样式,需要按2次shift,搜索
join lines
,快捷键ctrl + shift + j,如果不想要格式化的时候又恢复成多行,我们需要修改下格式化的配置
五、测试驱动开发(TDD)
监听input中的事件
input中最重要的就是change事件
// input.vue
<input type="text" :value="value" :disabled="disabled" :readonly="readonly"
@change="$emit('change',$event,'Hi!')"/>
// 使用input.vue
<g-input value="王五" @change="inputChange"></g-input>
methods:{
inputChange(e,param){
console.log(e.target.value,param);
}
}
我们发现只有添加参数$event
,才能获取到事件触发时的详细信息
解释一下上面代码的意思:
原生input触发了一个change事件,外面监听原生input的change事件,触发了自己的change事件执行inputChange
函数,参数e就是触发是传的$event
,$event
是浏览器给的,就是用户在触发change事件时的所有信息
因此的参数为$emit('事件名',传给监听者的第一个参数,传给监听者的第二个参数...)
<input type="text" :value="value" :disabled="disabled" :readonly="readonly"
@change="$emit('change',$event)"
@input = "$emit('input',$event)"
@focus = "$emit('focus',$event)"
@blur = "$emit('blur',$event)"
/>
<g-input value="王五" @change="inputChange"></g-input>
methods:{
inputChange(e,param){
console.log(e.target.value,param);
}
...
}
与其写代码去监听,还不如写测试用例
在写测试用例的时候,我们要测组件拥有的属性和需要监听的方法都要测
需要测试的属性有:value(input中的值)、disable(禁止输入)、readonly(只读)、error(报错)
新建test/input.test.js
备注
- 在测试readonly属性的时候,only要大写
expect(inputElement.readOnly).to.equal(true);
- 测试error的时候要分别测试 error icon 和 erro text
const useElement = vm.$el.querySelector('use');
expect(useElement.getAttribute('xlink:href')).to.equal('#i-error');
const errorMessage = vm.$el.querySelector('.error-message');
expect(errorMessage.innerText).to.equal('errorMessage');
-
describe 中嵌套describe的作用是使用
describe
对测试用例进行分组,如测试组件属性,测试组件方法等分组 -
『测试驱动开发(TDD)的含义:』先进行测试,如果测试有错误就去写代码,如果测试没有错误就继续测试
-
对于测试中重复的代码,我们尝试抽离出相同的代码,我们不妨在nocha.js中搜索关键字
beforEach
其中before
的意思是在所有的之前,beforeEach
是在每个测试用例之前,记得提前vm变量的作用域
- 测试事件稍微麻烦点,不光要测试监听事件被触发,而且要测试触发后的回调callback中的参数可以获取到时间的类型为change
describe('events', () => {
const Constructor = Vue.extend(Input);
let vm;
it('支持 change 事件', () => {
vm = new Constructor({}).$mount();
const callback = sinon.fake();
vm.$on('click', callback);
// 触发input的change事件??
expect(callback).to.have.been.called;
});
});
那么如何主动的触发一个事件呢?
在stackoverflow中搜索关键字js trigger event
得到的答案是使用dispachEvent
接着使用MDN搜索dispachEvent的用法,知道了inputElement.dispatchEvent(event)
这样就可以触发一个事件
那么如何测试回调函数callback中的参数可以获取到事件类型为change
呢?
由于我们测试是用sinon-chai
(sinon-chai
将sino
和chai
拉到一起合作) 和 karma
(提供了一个测试的环境)来做的
搜索sinon-chai的文档,知道它提供了一个calledWith
方法
describe('events', () => {
const Constructor = Vue.extend(Input);
let vm;
it('支持 change 事件', () => {
vm = new Constructor({}).$mount();
const callback = sinon.fake();
vm.$on('change', callback);
// 触发input的change事件
let event = new Event('change')
const inputElement = vm.$el.querySelector('input');
inputElement.dispatchEvent(event)
expect(callback).to.have.been.calledWith(event);
});
});
最后的断言的意思就是:当触发change事件后callback回调被调用,同时callback回调函数的第一个参数是event(change事件的详情)
结果测试都通过了
测试其他的事件也都是一个道理了
最后优化一下代码,去除重复的代码,下次如果有新的事件要测试,只要继续往数组中添加即可
describe('events', () => {
const Constructor = Vue.extend(Input);
let vm;
it('支持 change/input/focus/blur 事件', () => {
['change','input','focus','blur'].forEach((eventName)=>{
vm = new Constructor({}).$mount();
const callback = sinon.fake();
vm.$on(eventName, callback);
// 触发input的change事件
let event = new Event(eventName)
const inputElement = vm.$el.querySelector('input');
inputElement.dispatchEvent(event)
expect(callback).to.have.been.calledWith(event);
})
});
});
备注:karma.mocha 和 chai是干嘛的?
karma有一个配置文件叫karma.conf.js
,它主要是打开浏览器并引入一些基础库
看配置文件可知,karma
打开chrome的无头浏览器
引入mocha
,引入mocha后我们就拥有descirbe和it函数(挂载windows上的全局函数不用去引)
引入sinon-chai
,就是同时引入sinon 和 chai,sino就是用来做fake函数用的(类似spies)。chai提供expect函数
为什么 sinon 可以和 chai.js 合作呢?因为有sinon-chai
,它会提供一个API叫calledWith
然后kama还引入了dist/test文件中的测试用例引入到浏览器中
最后还要reporter
,即测试后命令行显示的提示文字
六、让input支持v-model
目前的组件支持传参和事件,并不支持双向绑定
v-model的本质
v-model是语法糖
当我们写了v-model
时实际上是做了2件事情
- 绑定value
- 监听了input事件,事件名必须是input,事件的第一个参数必须是value的值
<input type="text" :value="message" @input="message = $event.target.value"/>
// input组件
<input type="text" :value="value" :disabled="disabled" :readonly="readonly"
@input = "$emit('input',$event.target.value)"
/>
如果要让我们的input组件要支持v-model,第一它必须接受一个叫value的属性,第二它必须接收一个input事件,目前我们的组件已经支持这2点要求了,也就是说已经支持了v-model
备注:
- 写了v-model就相当于在input上给了value
支持v-model后,最后记得修改测试用例,测试事件触事件向外传的值($event.target.value)
google js new event set target
describe('events', () => {
const Constructor = Vue.extend(Input);
let vm;
it('支持 change/input/focus/blur 事件', () => {
['change', 'input', 'focus', 'blur'].forEach((eventName) => {
vm = new Constructor({}).$mount();
const callback = sinon.fake();
vm.$on(eventName, callback);
// 触发input的change事件
let event = new Event(eventName);
Object.defineProperty(event, 'target',
{
value: {value: 'hi'},
enumerable: true
}
);
let inputElement = vm.$el.querySelector('input');
inputElement.dispatchEvent(event);
expect(callback).to.have.been.calledWith('hi');
});
});
});
备注
Object.defineProperty()
静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
转载自:https://juejin.cn/post/7255265868065882168