方法参数的验证
JavaScript 允许你设置参数的默认值。通过这种方法,可以通过一个巧妙的技巧来验证你的方法参数。
const isRequired = () => { throw new Error('param is required'); };
const print = (num = isRequired()) => { console.log(`printing ${num}`) };
print(2);//printing 2
print()// error
print(null)//printing null
非常整洁,不是吗?
格式化 json 代码
你可能对 JSON.stringify 非常熟悉。但是你是否知道可以用 stringify 进行格式化输出?实际上这很简单。
stringify 方法需要三个输入。 value,replacer 和 space。后两个是可选参数。这就是为什么我们以前没有注意过它们。要对 json 进行缩进,必须使用 space 参数。
console.log(JSON.stringify({name:"John",Age:23},null,'\t'));
>>>
{
"name": "John",
"Age": 23
}
从数组中获取唯一值
要从数组中获取唯一值,我们需要使用 filter 方法来过滤出重复值。但是有了新的 Set 对象,事情就变得非常顺利和容易了。
let uniqueArray = [...new Set([1, 2, 3, 3, 3, "school", "school", 'ball', false, false, true, true])];
>>> [1, 2, 3, "school", "ball", false, true]
从数组中删除虚值(Falsy Value)
在某些情况下,你可能想从数组中删除虚值。虚值是 JavaScript 的 Boolean 上下文中被认定为为 false 的值。 JavaScript 中只有六个虚值,它们是:
undefined
null
NaN
0
"" (空字符串)
false
滤除这些虚值的最简单方法是使用以下函数。
myArray.filter(Boolean);
如果要对数组进行一些修改,然后过滤新数组,可以尝试这样的操作。请记住,原始的 myArray 会保持不变。
myArray
.map(item => {
// Do your changes and return the new item
})
.filter(Boolean);
合并多个对象
假设我有几个需要合并的对象,那么这是我的首选方法。
const user = {
name: 'John Ludwig',
gender: 'Male'
};
const college = {
primary: 'Mani Primary School',
secondary: 'Lass Secondary School'
};
const skills = {
programming: 'Extreme',
swimming: 'Average',
sleeping: 'Pro'
};
const summary = {...user, ...college, ...skills};
这三个点在 JavaScript 中也称为展开运算符。你可以在这里学习更多用法。
对数字数组进行排序
JavaScript 数组有内置的 sort 方法。默认情况下 sort 方法把数组元素转换为字符串,并对其进行字典排序。在对数字数组进行排序时,这有可能会导致一些问题。所以下面是解决这类问题的简单解决方案。
[0,10,4,9,123,54,1].sort((a,b) => a-b);
>>> [0, 1, 4, 9, 10, 54, 123]
这里提供了一个将数字数组中的两个元素与 sort 方法进行比较的函数。这个函数可帮助我们接收正确的输出。
Disable Right Click
禁用右键
你可能想要阻止用户在你的网页上单击鼠标右键。
<body oncontextmenu="return false">
<div></div>
</body>
这段简单的代码将为你的用户禁用右键单击。
使用别名进行解构
解构赋值语法是一种 JavaScript 表达式,可以将数组中的值或对象的值或属性分配给变量。解构赋值能让我们用更简短的语法进行多个变量的赋值。
const object = { number: 10 };
// Grabbing number
const { number } = object;
// Grabbing number and renaming it as otherNumber
const { number: otherNumber } = object;
console.log(otherNumber); //10
获取数组中的最后一项
可以通过对 splice 方法的参数传入负整数,来数获取组末尾的元素。
let array = [0, 1, 2, 3, 4, 5, 6, 7]
console.log(array.slice(-1));
>>>[7]
console.log(array.slice(-2));
>>>[6, 7]
console.log(array.slice(-3));
>>>[5, 6, 7]
等待 Promise 完成
在某些情况下,你可能会需要等待多个 promise 结束。可以用 Promise.all 来并行运行我们的 promise。
const PromiseArray = [
Promise.resolve(100),
Promise.reject(null),
Promise.resolve("Data release"),
Promise.reject(new Error('Something went wrong'))];
Promise.all(PromiseArray)
.then(data => console.log('all resolved! here are the resolve values:', data))
.catch(err => console.log('got rejected! reason:', err))
关于 Promise.all 的主要注意事项是,当一个 Promise 拒绝时,该方法将引发错误。这意味着你的代码不会等到你所有的 promise 都完成。
如果你想等到所有 promise 都完成后,无论它们被拒绝还是被解决,都可以使用 Promise.allSettled。此方法在 ES2020 的最终版本得到支持。
const PromiseArray = [
Promise.resolve(100),
Promise.reject(null),
Promise.resolve("Data release"),
Promise.reject(new Error('Something went wrong'))];
Promise.allSettled(PromiseArray).then(res =>{
console.log(res);
}).catch(err => console.log(err));
//[
//{status: "fulfilled", value: 100},
//{status: "rejected", reason: null},
//{status: "fulfilled", value: "Data release"},
//{status: "rejected", reason: Error: Something went wrong ...}
//]
即使某些 promise 被拒绝,Promise.allSettled 也会从你所有的 promise 中返回结果。
XML(Extensible Markup Language 可扩展标记语言),XML是一个以文本来描述数据的文档。
<?xml version="1.0" encoding="UTF-8"?> <people> <person personid="E01"> <name>Tony</name> <address>10 Downing Street, London, UK</address> <tel>(061) 98765</tel> <fax>(061) 98765</fax> <email>tony@everywhere.com</email> </person> <person personid="E02"> <name>Bill</name> <address>White House, USA</address> <tel>(001) 6400 98765</tel> <fax>(001) 6400 98765</fax> <email>bill@everywhere.com</email> </person> </people>
(1)充当显示数据(以XML充当显示层)
(2)存储数据(存储层)的功能
(3)以XML描述数据,并在联系服务器与系统的其余部分之间传递。(传输数据的一样格式)
从某种角度讲,XML是数据封装和消息传递技术。
3.解析XML:// 创建SAX解析器工厂对象 SAXParserFactory spf = SAXParserFactory.newInstance(); // 使用解析器工厂创建解析器实例 SAXParser saxParser = spf.newSAXParser(); // 创建SAX解析器要使用的事件侦听器对象 PersonHandler handler = new PersonHandler(); // 开始解析文件 saxParser.parse( new File(fileName), handler);
3.2. DOM解析XML:
DOM:Document Object Model(文档对象模型)
DOM的特性:
定义一组 Java 接口,基于对象,与语言和平台无关将 XML 文档表示为树,在内存中解析和存储 XML 文档,允许随机访问文档的不同部分。
DOM解析XML
DOM的优点,由于树在内存中是持久的,因此可以修改后更新。它还可以在任何时候在树中上下导航,API使用起来也较简单。
DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance(); DocumentBuilder db = builder.newDocumentBuilder(); db.parse("person.xml"); NodeList node_person = doc.getElementsByTagName("person");
3.3. JDOM解析XML:
JDOM是两位著名的 Java 开发人员兼作者,Brett Mclaughlin 和 Jason Hunter 的创作成果, 2000 年初在类似于Apache协议的许可下,JDOM作为一个开放源代码项目正式开始研发了。
JDOM 简化了与 XML 的交互并且比使用 DOM 实现更快,JDOM 与 DOM 主要有两方面不同。首先,JDOM 仅使用具体类而不使用接口。这在某些方面简化了 API,但是也限制了灵活性。第二,API 大量使用了 Collections 类,简化了那些已经熟悉这些类的 Java 开发者的使用。
解析步骤: (1)SAXBuilder sax = new SAXBuilder(); (2)Document doc = sax.build(….); (3)Element el = doc.getRootElement();(4)List list = el.getChildren(); (5)遍历内容
解析步骤: (1)SAXReader sax = new SAXReader(); (2)Document doc = sax.read(Thread.currentThread().getContextClassLoader() .getResourceAsStream("person.xml")); (3)Element root = doc.getRootElement(); (4)Iterator iterator = root.elementIterator(); (5)遍历迭代器
public class Person { private String personid; private String name; private String address; private String tel; private String fax; private String email; @Override public String toString() { return "Person{" + "personid='" + personid + '\'' + ", name='" + name + '\'' + ", address='" + address + '\'' + ", tel='" + tel + '\'' + ", fax='" + fax + '\'' + ", email='" + email + '\'' + '}'; } public String getPersonid() { return personid; } public void setPersonid(String personid) { this.personid = personid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getTel() { return tel; } public void setTel(String tel) { this.tel = tel; } public String getFax() { return fax; } public void setFax(String fax) { this.fax = fax; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
<?xml version="1.0" encoding="UTF-8"?> <people> <person personid="E01"> <name>Tony Blair</name> <address>10 Downing Street, London, UK</address> <tel>(061) 98765</tel> <fax>(061) 98765</fax> <email>blair@everywhere.com</email> </person> <person personid="E02"> <name>Bill Clinton</name> <address>White House, USA</address> <tel>(001) 6400 98765</tel> <fax>(001) 6400 98765</fax> <email>bill@everywhere.com</email> </person> </people>
import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import java.util.ArrayList; import java.util.List; /** * Created by Hu Guanzhong * SAX解析的特点: * 1、基于事件驱动 * 2、顺序读取,速度快 * 3、不能任意读取节点(灵活性差) * 4、解析时占用的内存小 * 5、SAX更适用于在性能要求更高的设备上使用(Android开发中) * */ public class PersonHandler extends DefaultHandler{ private List<Person> persons = null; private Person p;//当前正在解析的person private String tag;//用于记录当前正在解析的标签名 public List<Person> getPersons() { return persons; } //开始解析文档时调用 @Override public void startDocument() throws SAXException { super.startDocument(); persons = new ArrayList<>(); System.out.println("开始解析文档..."); } //在XML文档解析结束时调用 @Override public void endDocument() throws SAXException { super.endDocument(); System.out.println("解析文档结束."); } /** * 解析开始元素时调用 * @param uri 命名空间 * @param localName 不带前缀的标签名 * @param qName 带前缀的标签名 * @param attributes 当前标签的属性集合 * @throws SAXException */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); if ("person".equals(qName)){ p = new Person(); String personid = attributes.getValue("personid"); p.setPersonid(personid); } tag = qName; System.out.println("startElement--"+qName); } //解析结束元素时调用 @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(uri, localName, qName); if ("person".equals(qName)) { persons.add(p); } tag = null; System.out.println("endElement--"+qName); } //解析文本内容时调用 @Override public void characters(char[] ch, int start, int length) throws SAXException { super.characters(ch, start, length); if (tag != null) { if ("name".equals(tag)) { p.setName(new String(ch,start,length)); }else if("address".equals(tag)){ p.setAddress(new String(ch,start,length)); }else if("tel".equals(tag)){ p.setTel(new String(ch,start,length)); }else if("fax".equals(tag)){ p.setFax(new String(ch,start,length)); }else if("email".equals(tag)){ p.setEmail(new String(ch,start,length)); } System.out.println(ch); } } }
public class XMLDemo { /** * 使用第三方xstream组件实现XML的解析与生成 */ @Test public void xStream(){ Person p = new Person(); p.setPersonid("1212"); p.setAddress("北京"); p.setEmail("vince@163.com"); p.setFax("6768789798"); p.setTel("13838389438"); p.setName("38"); XStream xStream = new XStream(new Xpp3Driver()); xStream.alias("person",Person.class); xStream.useAttributeFor(Person.class,"personid"); String xml = xStream.toXML(p); System.out.println(xml); //解析XML Person person = (Person)xStream.fromXML(xml); System.out.println(person); } /** * 从XML文件中读取对象 */ @Test public void xmlDecoder() throws FileNotFoundException { BufferedInputStream in = new BufferedInputStream(new FileInputStream("test.xml")); XMLDecoder decoder = new XMLDecoder(in); Person p = (Person)decoder.readObject(); System.out.println(p); } /** * 把对象转成XML文件写入 */ @Test public void xmlEncoder() throws FileNotFoundException { BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.xml")); XMLEncoder xmlEncoder = new XMLEncoder(bos); Person p = new Person(); p.setPersonid("1212"); p.setAddress("北京"); p.setEmail("vince@163.com"); p.setFax("6768789798"); p.setTel("13838389438"); p.setName("38"); xmlEncoder.writeObject(p); xmlEncoder.close(); } /** * DOM4J解析XML * 基于树型结构,第三方组件 * 解析速度快,效率更高,使用的JAVA中的迭代器实现数据读取,在WEB框架中使用较多(Hibernate) * */ @Test public void dom4jParseXML() throws DocumentException { //1 创建DOM4J的解析器对象 SAXReader reader = new SAXReader(); InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/vince/xml/person.xml"); org.dom4j.Document doc = reader.read(is); org.dom4j.Element rootElement = doc.getRootElement(); Iterator<org.dom4j.Element> iterator = rootElement.elementIterator(); ArrayList<Person> persons = new ArrayList<>(); Person p = null; while(iterator.hasNext()){ p = new Person(); org.dom4j.Element e = iterator.next(); p.setPersonid(e.attributeValue("personid")); Iterator<org.dom4j.Element> iterator1 = e.elementIterator(); while(iterator1.hasNext()){ org.dom4j.Element next = iterator1.next(); String tag = next.getName(); if("name".equals(tag)){ p.setName(next.getText()); }else if("address".equals(tag)){ p.setAddress(next.getText()); }else if("tel".equals(tag)){ p.setTel(next.getText()); }else if("fax".equals(tag)){ p.setFax(next.getText()); }else if("email".equals(tag)){ p.setEmail(next.getText()); } } persons.add(p); } System.out.println("结果:"); System.out.println(Arrays.toString(persons.toArray())); } /** * JDOM解析 XML * 1、与DOM类似基于树型结构, * 2、与DOM的区别: * (1)第三方开源的组件 * (2)实现使用JAVA的Collection接口 * (3)效率比DOM更快 */ @Test public void jdomParseXML() throws JDOMException, IOException { //创建JDOM解析器 SAXBuilder builder = new SAXBuilder(); InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/vince/xml/person.xml"); org.jdom2.Document build = builder.build(is); Element rootElement = build.getRootElement(); List<Person> list = new ArrayList<>(); Person person = null; List<Element> children = rootElement.getChildren(); for(Element element: children){ person = new Person(); String personid = element.getAttributeValue("personid"); person.setPersonid(personid); List<Element> children1 = element.getChildren(); for (Element e: children1){ String tag = e.getName(); if("name".equals(tag)){ person.setName(e.getText()); }else if("address".equals(tag)){ person.setAddress(e.getText()); }else if("tel".equals(tag)){ person.setTel(e.getText()); }else if("fax".equals(tag)){ person.setFax(e.getText()); }else if("email".equals(tag)){ person.setEmail(e.getText()); } } list.add(person); } System.out.println("结果:"); System.out.println(Arrays.toString(list.toArray())); } /** * DOM解析XML * 1、基于树型结构,通过解析器一次性把文档加载到内存中,所以会比较占用内存,可以随机访问 * 更加灵活,更适合在WEB开发中使用 */ @Test public void domParseXML() throws ParserConfigurationException, IOException, SAXException { //1、创建一个DOM解析器工厂对象 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); //2、通过工厂对象创建解析器对象 DocumentBuilder documentBuilder = factory.newDocumentBuilder(); //3、解析文档 InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/vince/xml/person.xml"); //此代码完成后,整个XML文档已经被加载到内存中,以树状形式存储 Document doc = documentBuilder.parse(is); //4、从内存中读取数据 //获取节点名称为person的所有节点,返回节点集合 NodeList personNodeList = doc.getElementsByTagName("person"); ArrayList<Person> persons = new ArrayList<>(); Person p = null; //此循环会迭代两次 for (int i=0;i<personNodeList.getLength();i++){ Node personNode = personNodeList.item(i); p = new Person(); //获取节点的属性值 String personid = personNode.getAttributes().getNamedItem("personid").getNodeValue(); p.setPersonid(personid); //获取当前节点的所有子节点 NodeList childNodes = personNode.getChildNodes(); for (int j = 0;j<childNodes.getLength();j++){ Node item = childNodes.item(j); String nodeName = item.getNodeName(); if ("name".equals(nodeName)) { p.setName(item.getFirstChild().getNodeValue()); }else if("address".equals(nodeName)){ p.setAddress(item.getFirstChild().getNodeValue()); }else if("tel".equals(nodeName)){ p.setTel(item.getFirstChild().getNodeValue()); }else if("fax".equals(nodeName)){ p.setFax(item.getFirstChild().getNodeValue()); }else if("email".equals(nodeName)){ p.setEmail(item.getFirstChild().getNodeValue()); } } persons.add(p); } System.out.println("结果:"); System.out.println(Arrays.toString(persons.toArray())); } /** * SAX解析的特点: * 1、基于事件驱动 * 2、顺序读取,速度快 * 3、不能任意读取节点(灵活性差) * 4、解析时占用的内存小 * 5、SAX更适用于在性能要求更高的设备上使用(Android开发中) * @throws ParserConfigurationException * @throws SAXException * @throws IOException */ @Test public void saxParseXML() throws ParserConfigurationException, SAXException, IOException { //1、创建一个SAX解析器工厂对象 SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); //2、通过工厂对象创建SAX解析器 SAXParser saxParser = saxParserFactory.newSAXParser(); //3、创建一个数据处理器(需要我们自己来编写) PersonHandler personHandler = new PersonHandler(); //4、开始解析 InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/vince/xml/person.xml"); saxParser.parse(is,personHandler); List<Person> persons = personHandler.getPersons(); for (Person p:persons){ System.out.println(p); } } }
1. 加载和执行
尽量将所有的<script>标签放在</body>标签之前,确保脚本执行前页面已经完成了渲染,避免脚本的下载阻塞其他资源(例如图片)的下载。
合并脚本,减少页面中的<script>标签
使用<script>标签的defer和async属性(两者的区别见这里)
通过Javascript动态创建<script>标签插入文档来下载,其不会影响页面其他进程
2.数据存取
由于作用域链的机制,访问局部变量比访问跨作用域变量更快,因此在函数中若要多次访问跨作用域变量,则可以用局部变量保存。
避免使用with语句,其会延长作用域链
嵌套的对象成员会导致引擎搜索所有对象成员,避免使用嵌套,例如window.location.href
对象的属性和方法在原型链的位置越深,访问的速度也越慢
3.Dom编程
进行大段HTML更新时,推荐使用innerHTML,而不是DOM方法
HTML集合是一个与文档中元素绑定的类数组对象,其长度随着文档中元素的增减而动态变化,因此避免在每次循环中直接读取HTML集合的length,容易导致死循环
使用节点的children属性,而不是childNodes属性,前者访问速度更快,且不包含空白文本和注释节点。
浏览器的渲染过程包括构建DOM树和渲染树,当DOM元素的几何属性变化时,需要重新构造渲染树,这一过程称为“重排”,完成重排后,浏览器会重新绘制受影响的部分到屏幕中,这一过程称为“重绘”。因此应该尽量合并多次对DOM的修改,或者先将元素脱离文档流(display:none、文档片段),应用修改后,再插入文档中。
每次浏览器的重排时都会产生消耗,大多数浏览器会通过队列化修改并批量执行来优化重排过程,可当访问元素offsetTop、scrollTop、clientTop、getComputedStyle等一系列布局属性时,会强制浏览器立即进行重排返回正确的值。因此不要在dom布局信息改变时,访问这些布局属性。
当修改同个元素多个Css属性时,可以使用CssText属性进行一次性修改样式,减少浏览器重排和重绘的次数
当元素发生动画时,可以使用绝对定位使其脱离文档流,动画结束后,再恢复定位。避免动画过程中浏览器反复重排文档流中的元素。
多使用事件委托,减少监听事件
4.算法和流程控制
for循环和while循环性能差不多,除了for-in循环最慢(其要遍历原型链)
循环中要减少对象成员及数组项的查询次数,可以通过倒序循环提高性能
循环次数大于1000时,可运用Duff Devices减少迭代次数
switch比if-else快,但如果具有很多离散值时,可使用数组或对象来构建查找表
递归可能会造成调用栈溢出,可将其改为循环迭代
如果可以,对一些函数的计算结果进行缓存
5.字符串和正则表达式
进行大量字符串的连接时,+和+=效率比数组的join方法要高
当创建了一个正则表达式对象时,浏览器会验证你的表达式,然后将其转化为一个原生代码程序,用户执行匹配工作。当你将其赋值给变量时,可以避免重复执行该步骤。
当正则进入使用状态时,首先要确定目标字符串的起始搜索位置(字符串的起始位置或正则表达式的lastIndex属性),之后正则表达式会逐个检查文本和正则模式,当一个特定的字元匹配失败时,正则表达式会试着回溯到之前尝试匹配的位置,然后尝试其他路径。如果正则表达式所有的可能路径都没有匹配到,其会将起始搜索位置下移一位,重新开始检查。如果字符串的每个字符都经历过检查,没有匹配成功,则宣布彻底失败。
当正则表达式不那么具体时,例如.和[\s\S]等,很可能会出现回溯失控的情况,在js中可以应用预查模拟原子组(?=(pattern))\1来避免不必要的回溯。除此之外,嵌套的量词,例如/(A+A+)+B/在匹配"AAAAAAAA"时可能会造成惊人的回溯,应尽量避免使用嵌套的量词或使用预查模拟原子组消除回溯问题。
将复杂的正则表达式拆分为多个简单的片段、正则以简单、必需的字元开始、减少分支数量|,有助于提高匹配的效率。
setTimeout(function(){ process(todo.shift()); if (todo.length > 0) { setTimeout(arguments.callee, 25); } else { callback(); } })
setTimeout(function(){ let start = +new Date(); do { process(todo.shift()); } while(todo.length > 0 && (+new Date() - start) < 50) if (todo.length > 0) { setTimeout(arguments.callee, 25); } else { callback(); } })
WebWork
进行计算
Expires: Mon,28 Jul 2018 23:30:30 GMT
eval
、Function
进行双重求值
Object
/Array
字面量定义,不要使用构造函数
if (i & 1) { className = 'odd'; } else { className = 'even'; }
Math
对象等
背景
这一个因为滚动条占据空间引起的bug, 查了一下资料, 最后也解决了,顺便研究一下这个属性, 做一下总结,分享给大家看看。
正文
昨天, 测试提了个问题, 现象是一个输入框的聚焦提示偏了, 让我修一下, 如下图:
image.png
起初认为是红框提示位置不对, 就去找代码看:
<Input
// ...
onFocus={() => setFocusedInputName('guidePrice')}
onBlur={() => setFocusedInputName('')}
/>
<Table
data-focused-column={focusedInputName}
// ...
/>
代码上没有什么问题, 不是手动设置的,而且, 在我和另一个同事, 还有PM的PC上都是OK的:
image.png
初步判断是,红框位置结算有差异, 差异大小大概是17px, 但是这个差异是怎么产生的呢?
就去测试小哥的PC上看, 注意到一个细节, 在我PC上, 滚动条是悬浮的:
image.png
在他PC上, 滚动条是占空间的:
image.png
在他电脑上, 手动把原本的 overscroll-y: scroll 改成 overscroll-y: overlay 问题就结局了。
由此判定是: 滚动条占据空间 引起的bug。
overscroll-y: overlay
CSS属性 overflow, 定义当一个元素的内容太大而无法适应块级格式化上下文的时候该做什么。它是 overflow-x 和overflow-y的 简写属性 。
/* 默认值。内容不会被修剪,会呈现在元素框之外 */
overflow: visible;
/* 内容会被修剪,并且其余内容不可见 */
overflow: hidden;
/* 内容会被修剪,浏览器会显示滚动条以便查看其余内容 */
overflow: scroll;
/* 由浏览器定夺,如果内容被修剪,就会显示滚动条 */
overflow: auto;
/* 规定从父元素继承overflow属性的值 */
overflow: inherit;
官方描述:
overlay 行为与 auto 相同,但滚动条绘制在内容之上而不是占用空间。 仅在基于 WebKit(例如,Safari)和基于Blink的(例如,Chrome或Opera)浏览器中受支持。
表现:
html {
overflow-y: overlay;
}
兼容性
没有在caniuse上找到这个属性的兼容性, 也有人提这个问题:
image.png
问题场景以及解决办法
1. 外部容器的滚动条
这里的外部容器指的是html, 直接加在最外层:
html {
overflow-y: scroll;
}
手动加上这个特性, 不论什么时候都有滚动宽度占据空间。
缺点: 没有滚动的时候也会有个滚动条, 不太美观。
优点: 方便, 没有兼容性的问题。
2. 外部容器绝对定位法
用绝对定位,保证了body的宽度一直保持完整空间:
html {
overflow-y: scroll; // 兼容ie8,不支持:root, vw
}
:root {
overflow-y: auto;
overflow-x: hidden;
}
:root body {
position: absolute;
}
body {
width: 100vw;
overflow: hidden;
}
3. 内部容器做兼容
.wrapper {
overflow-y: scroll; // fallback
overflow-y: overlay;
}
总结
个人推荐还是用 overlay, 然后使用scroll 做为兜底。
内容就这么多, 希望对大家有所启发。
文章如有错误, 请在留言区指正, 谢谢。
之前花了些时间将gatsby-theme-gitbook迁移到 Typescript,以获得在 VSCode 中更好的编程体验.
整体差不多已经完成迁移,剩下将 Gatsby 的 API 文件也迁移到 TS,这里可以看到 gatsby#21995 官方也在将核心代码库迁移到 Typescript,准备等待官方将核心代码库迁移完成,在迁移 API 文件.
这篇文章用XYShaoKang/gatsby-project-config,演示如何将 gatsby 迁移到 TypeScript,希望能帮到同样想要在 Gatsby 中使用 TS 的同学.
迁移步骤:
TS 配置
配置 ESLint 支持 TS
完善 GraphQL 类型提示
初始化项目
gatsby new gatsby-migrate-to-typescript XYShaoKang/gatsby-project-config
cd gatsby-migrate-to-typescript
yarn develop
TS 配置
安装typescript
添加typescript.json配置文件
修改 js 文件为 tsx
补全 TS 声明定义
安装typescript
yarn add -D typescript
添加配置文件tsconfig.json
// https://www.typescriptlang.org/v2/docs/handbook/tsconfig-json.html
{
"compilerOptions": {
"target": "esnext", // 编译生成的目标 es 版本,可以根据需要设置
"module": "esnext", // 编译生成的目标模块系统
"lib": ["dom", "es2015", "es2017"], // 配置需要包含的运行环境的类型定义
"jsx": "react", // 配置 .tsx 文件的输出模式
"strict": true, // 开启严格模式
"esModuleInterop": true, // 兼容 CommonJS 和 ES Module
"moduleResolution": "node", // 配置模块的解析规则,支持 node 模块解析规则
"noUnusedLocals": true, // 报告未使用的局部变量的错误
"noUnusedParameters": true, // 报告有关函数中未使用参数的错误
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true, // 支持装饰器上生成元数据,用来进行反射之类的操作
"noEmit": true, // 不输出 js,源映射或声明之类的文件,单纯用来检查错误
"skipLibCheck": true // 跳过声明文件的类型检查,只会检查已引用的部分
},
"exclude": ["./node_modules", "./public", "./.cache"], // 解析时,应该跳过的路晋
"include": ["src"] // 定义包含的路径,定义在其中的声明文件都会被解析进 vscode 的智能提示
}
将index.js改成index.tsx,重新启动服务,查看效果.
其实 Gatsby 内置了支持 TS,不用其他配置,只要把index.js改成index.tsx就可以直接运行.添加 TS 依赖是为了显示管理 TS,而tsconfig.json也是这个目的,当我们有需要新的特性以及自定义配置时,可以手动添加.
补全 TS 声明定义
打开index.tsx,VSCode 会报两个错误,一个是找不到styled-components的声明文件,这个可以通过安装@types/styled-components来解决.
另外一个错误绑定元素“data”隐式具有“any”类型。,这个错误是因为我们在tsconfig.json中指定了"strict": true,这会开启严格的类型检查,可以通过关闭这个选项来解决,只是我们用 TS 就是要用它的类型检查的,所以正确的做法是给data定义类型.
下面来一一修复错误.
安装styled-components的声明文件
yarn add -D @types/styled-components
修改index.tsx
import React, { FC } from 'react'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import { HomeQuery } from './__generated__/HomeQuery'
const Title = styled.h1`
font-size: 1.5em;
margin: 0;
padding: 0.5em 0;
color: palevioletred;
background: papayawhip;
`
const Content = styled.div`
margin-top: 0.5em;
`
interface PageQuery {
data: {
allMarkdownRemark: {
edges: Array<{
node: {
frontmatter: {
title: string
}
excerpt: string
}
}>
}
}
}
const Home: FC<PageQuery> = ({ data }) => {
const node = data.allMarkdownRemark.edges[0].node
const title = node.frontmatter?.title
const excerpt = node.excerpt
return (
<>
<Title>{title}</Title>
<Content>{excerpt}</Content>
</>
)
}
export default Home
export const query = graphql`
query HomeQuery {
allMarkdownRemark {
edges {
node {
frontmatter {
title
}
excerpt
}
}
}
}
`
这时候会出现一个新的错误,在excerpt: string处提示Parsing error: Unexpected token,这是因为 ESLint 还无法识别 TS 的语法,下面来配置 ESLint 支持 TS.
配置 ESLint 支持 TypeScript
安装依赖
yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
配置.eslintrc.js
module.exports = {
parser: `@typescript-eslint/parser`, // 将解析器从`babel-eslint`替换成`@typescript-eslint/parser`,用以解析 TS 代码
extends: [
`google`,
`eslint:recommended`,
`plugin:@typescript-eslint/recommended`, // 使用 @typescript-eslint/eslint-plugin 推荐配置
`plugin:react/recommended`,
`prettier/@typescript-eslint`, // 禁用 @typescript-eslint/eslint-plugin 中与 prettier 冲突的规则
`plugin:prettier/recommended`,
],
plugins: [
`@typescript-eslint`, // 处理 TS 语法规则
`react`,
`filenames`,
],
// ...
}
在.vscode/settings.json中添加配置,让VSCode使用ESLint扩展格式化ts和tsx文件
// .vscode/settings.json
{
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
完善 GraphQL 类型提示
// index.tsx
import React, { FC } from 'react'
// ...
interface PageQuery {
data: {
allMarkdownRemark: {
edges: Array<{
node: {
frontmatter: {
title: string
}
excerpt: string
}
}>
}
}
}
const Home: FC<PageQuery> = ({ data }) => {
// ...
}
export default Home
export const query = graphql`
query HomeQuery {
allMarkdownRemark {
edges {
node {
frontmatter {
title
}
excerpt
}
}
}
}
`
我们看看index.tsx文件,会发现PropTypes和query结构非常类似,在Gatsby运行时,会把query查询的结果作为组件prop.data传入组件,而PropTypes是用来约束prop存在的.所以其实PropTypes就是根据query写出来的.
如果有依据query自动生成PropTypes的功能就太棒了.
另外一个问题是在query中编写GraphQL查询时,并没有类型约束,也没有智能提示.
总结以下需要完善的体验包括:
GraphQL 查询编写时的智能提示,以及错误检查
能够从 GraphQL 查询生成对应的 TypeScript 类型.这样能保证类型的唯一事实来源,并消除 TS 中冗余的类型声明.毕竟如果经常需要手动更新两处类型,会更容易出错,而且也并不能保证手动定义类型的正确性.
实现方式:
通过生成架构文件,配合Apollo GraphQL for VS Code插件,实现智能提示,以及错误检查
通过graphql-code-generator或者apollo生成 TS 类型定义文件
如果自己去配置的话,是挺耗费时间的,需要去了解graphql-code-generator的使用,以及Apollo的架构等知识.
不过好在社区中已经有对应的 Gatsby 插件集成了上述工具可以直接使用,能让我们不用去深究对应知识的情况下,达到优化 GraphQL 编程的体验.
尝试过以下两个插件能解决上述问题,可以任选其一使用
gatsby-plugin-codegen
gatsby-plugin-typegen
另外还有一款插件gatsby-plugin-graphql-codegen也可以生成 TS 类型,不过配置略麻烦,并且上述两个插件都可以满足我现在的需求,所以没有去尝试,感兴趣的可以尝试一下.
注意点:
Apollo不支持匿名查询,需要使用命名查询
第一次生成,需要运行Gatsby之后才能生成类型文件
整个项目内不能有相同命名的查询,不然会因为名字有冲突而生成失败
下面是具体操作
安装vscode-apollo扩展
在 VSCode 中按 Ctrl + P ( MAC 下: Cmd + P) 输入以下命令,按回车安装
ext install apollographql.vscode-apollo
方式一: 使用gatsby-plugin-codegen
gatsby-plugin-codegen默认会生成apollo.config.js和schema.json,配合vscode-apollo扩展,可以提供GraphQL的类型约束和智能提示.
另外会自动根据query中的GraphQL查询,生成 TS 类型,放在对应的tsx文件同级目录下的__generated__文件夹,使用时只需要引入即可.
如果需要在运行时自动生成 TS 类型,需要添加watch: true配置.
安装gatsby-plugin-codegen
yarn add gatsby-plugin-codegen
配置gatsby-config.js
// gatsby-config.js
module.exports = {
plugins: [
// ...
{
resolve: `gatsby-plugin-codegen`,
options: {
watch: true,
},
},
],
}
重新运行开发服务生成类型文件
yarn develop
如果出现以下错误,一般是因为没有为查询命名的缘故,给查询添加命名即可,另外配置正确的话,打开对应的文件,有匿名查询,编辑器会有错误提示.
fix-anonymous-operations.png
这个命名之后会作为生成的类型名.
修改index.tsx以使用生成的类型
gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.
import { HomeQuery } from './__generated__/HomeQuery' // 引入自动生成的类型
// ...
// interface PageQuery {
// data: {
// allMarkdownRemark: {
// edges: Array<{
// node: {
// frontmatter: {
// title: string
// }
// excerpt: string
// }
// }>
// }
// }
// }
interface PageQuery {
data: HomeQuery // 替换之前手写的类型
}
// ...
将自动生成的文件添加到.gitignore中
apollo.config.js,schema.json,__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.
# Generated types by gatsby-plugin-codegen
__generated__
apollo.config.js
schema.json
方式二: 使用gatsby-plugin-typegen
gatsby-plugin-typegen通过配置生成gatsby-schema.graphql和gatsby-plugin-documents.graphql配合手动创建的apollo.config.js提供GraphQL的类型约束和智能提示.
根据GraphQL查询生成gatsby-types.d.ts,生成的类型放在命名空间GatsbyTypes下,使用时通过GatsbyTypes.HomeQueryQuery来引入,HomeQueryQuery是由对应的命名查询生成
安装gatsby-plugin-typegen
yarn add gatsby-plugin-typegen
配置
// gatsby-config.js
module.exports = {
plugins: [
// ...
{
resolve: `gatsby-plugin-typegen`,
options: {
outputPath: `src/__generated__/gatsby-types.d.ts`,
emitSchema: {
'src/__generated__/gatsby-schema.graphql': true,
},
emitPluginDocuments: {
'src/__generated__/gatsby-plugin-documents.graphql': true,
},
},
},
],
}
//apollo.config.js
module.exports = {
client: {
tagName: `graphql`,
includes: [
`./src/**/*.{ts,tsx}`,
`./src/__generated__/gatsby-plugin-documents.graphql`,
],
service: {
name: `GatsbyJS`,
localSchemaFile: `./src/__generated__/gatsby-schema.graphql`,
},
},
}
重新运行开发服务生成类型文件
yarn develop
修改index.tsx以使用生成的类型
gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.
// ...
// interface PageQuery {
// data: {
// allMarkdownRemark: {
// edges: Array<{
// node: {
// frontmatter: {
// title: string
// }
// excerpt: string
// }
// }>
// }
// }
// }
interface PageQuery {
data: GatsbyTypes.HomeQueryQuery // 替换之前手写的类型
}
// ...
将自动生成的文件添加到.gitignore中
__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.
# Generated types by gatsby-plugin-codegen
__generated__
Canvas 是 HTML5 提供的一个用于展示绘图效果的标签. Canvas 原意为画布, 在 HTML 页面中用于展示绘图效果. 最早 Canvas 是苹果提出的一个方案, 今天已经在大多数浏览器中实现。
canvas 的使用领域
游戏
大数据可视化数据
banner 广告
多媒体
模拟仿真
远程操作
图形编辑
判断浏览器是否支持 canvas 标签
var canvas = document.getElementById('canvas')
if (canvas.getContext) {
console.log('你的浏览器支持Canvas!')
} else {
console.log('你的浏览器不支持Canvas!')
}
canvas 的基本用法
1、使用 canvas 标签, 即可在页面中开辟一格区域,可以设置其宽高,宽高为 300 和 150
<canvas></canvas>
2、获取 dom 元素 canvas
canvas 本身不能绘图. 是使用 JavaScript 来完成绘图. canvas 对象提供了各种绘图用的 api。
var cas = document.querySelector('canvas')
3、通过 cas 获取上下文对象(画布对象!)
var ctx = cas.getContext('2d')
4、通过 ctx 开始画画(设置起点 设置终点 连线-描边 )
ctx.moveTo(10, 10)
ctx.lineTo(100, 100)
ctx.stroke()
绘制线条
设置开始位置: context.moveTo( x, y )
设置终点位置: context.lineTo( x, y )
描边绘制: context.stroke()
填充绘制: context.fill()
闭合路径: context.closePath()
canvas 还可以设置线条的相关属性,如下:
CanvasRenderingContext2D.lineWidth 设置线宽.
CanvasRenderingContext2D.strokeStyle 设置线条颜色.
CanvasRenderingContext2D.lineCap 设置线末端类型,'butt'( 默认 ), 'round', 'square'.
CanvasRenderingContext2D.lineJoin 设置相交线的拐点, 'miter'(默认),'round', 'bevel',
CanvasRenderingContext2D.getLineDash() 获得线段样式数组.
CanvasRenderingContext2D.setLineDash() 设置线段样式.
CanvasRenderingContext2D.lineDashOffset 绘制线段偏移量.
封装一个画矩形的方法
function myRect(ctxTmp, x, y, w, h) {
ctxTmp.moveTo(x, y)
ctxTmp.lineTo(x + w, y)
ctxTmp.lineTo(x + w, y + h)
ctxTmp.lineTo(x, y + h)
ctxTmp.lineTo(x, y)
ctxTmp.stroke()
}
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
myRect(ctx, 50, 50, 200, 200)
绘制矩形
fillRect( x , y , width , height) 填充以(x,y)为起点宽高分别为 width、height 的矩形 默认为黑色
stokeRect( x , y , width , height) 绘制一个空心以(x,y)为起点宽高分别为 width、height 的矩形
clearRect( x, y , width , height ) 清除以(x,y)为起点宽高分别为 width、height 的矩形 为透明
绘制圆弧
绘制圆弧的方法有
CanvasRenderingContext2D.arc()
CanvasRenderingContext2D.arcTo()
6 个参数: x,y(圆心的坐标),半径,起始的弧度(不是角度 deg),结束的弧度,(bool 设置方向 ! )
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.arc(100, 100, 100, 0, degToArc(360))
ctx.stroke()
// 角度转弧度
function degToArc(num) {
return (Math.PI / 180) * num
}
绘制扇形
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.arc(300, 300, 200, degToArc(125), degToArc(300))
// 自动连回原点
ctx.closePath()
ctx.stroke()
function degToArc(num) {
return (Math.PI / 180) * num
}
制作画笔
声明一个变量作为标识
鼠标按下的时候,记录起点位置
鼠标移动的时候,开始描绘并连线
鼠标抬起的时候,关闭开关
点击查看效果图
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
var isDraw = false
// 鼠标按下事件
cas.addEventListener('mousedown', function () {
isDraw = true
ctx.beginPath()
})
// 鼠标移动事件
cas.addEventListener('mousemove', function (e) {
if (!isDraw) {
// 没有按下
return
}
// 获取相对于容器内的坐标
var x = e.offsetX
var y = e.offsetY
ctx.lineTo(x, y)
ctx.stroke()
})
cas.addEventListener('mouseup', function () {
// 关闭开关了!
isDraw = false
})
手动涂擦
原理和画布相似,只不过用的是clearRect()方法。
点击查看效果图
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.fillRect(0, 0, 600, 600)
// 开关
var isClear = false
cas.addEventListener('mousedown', function () {
isClear = true
})
cas.addEventListener('mousemove', function (e) {
if (!isClear) {
return
}
var x = e.offsetX
var y = e.offsetY
var w = 20
var h = 20
ctx.clearRect(x, y, w, h)
})
cas.addEventListener('mouseup', function () {
isClear = false
})
刮刮乐
首先需要设置奖品和画布,将画布置于图片上方盖住,
随机设置生成奖品。
当手触摸移动的时候,可以擦除部分画布,露出奖品区。
点击查看效果图
<div>
<img src="./images/2.jpg" alt="" />
<canvas width="600" height="600"></canvas>
</div>
css
img {
width: 600px;
height: 600px;
position: absolute;
top: 10%;
left: 30%;
}
canvas {
width: 600px;
height: 600px;
position: absolute;
top: 10%;
left: 30%;
border: 1px solid #000;
}
js
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
var img = document.querySelector('img')
// 加一个遮罩层
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, cas.width, cas.height)
setImgUrl()
// 开关
var isClear = false
cas.addEventListener('mousedown', function () {
isClear = true
})
cas.addEventListener('mousemove', function (e) {
if (!isClear) {
return
}
var x = e.offsetX
var y = e.offsetY
ctx.clearRect(x, y, 30, 30)
})
cas.addEventListener('mouseup', function () {
isClear = false
})
function setImgUrl() {
var arr = ['./images/1.jpg', './images/2.jpg', './images/3.jpg', './images/4.jpg']
// 0-3
var random = Math.round(Math.random() * 3)
img.src = arr[random]
}
更多demo,请查看 github.com/Michael-lzg…
简单来说,v-if 的初始化较快,但切换代价高;v-show 初始化慢,但切换成本低
都是动态显示DOM元素
(1)手段:
v-if是动态的向DOM树内添加或者删除DOM元素;
v-show是通过设置DOM元素的display样式属性控制显隐;
(2)编译过程:
v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;
v-show只是简单的基于css切换;
(3)编译条件:
v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载);
v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
(4)性能消耗:
v-if有更高的切换消耗;
v-show有更高的初始渲染消耗;
(5)使用场景:
v-if适合运营条件不大可能改变;
v-show适合频繁切换。
虽然Vue 3还没有正式发布,但是热爱新技术的我早已按捺不住自己的内心,开始尝试在小项目中使用它了。
根据这篇《今日凌晨Vue3 beta版震撼发布,竟然公开支持脚手架项目!》我搭建了一个Vue 3的脚手架项目,用这种方式搭建的脚手架项目不仅仅只有vue是新版的,就连vue-router、vuex都是的。
给大家截一下package.json的图:
可以看到vue-router和vuex都已经开启4.0时代啦!
不过其实我并没有去了解过vue-router 4.0的新用法什么的,因为我觉得它不像vue 3.0都已经进行到beta的版本不会有特别大的变动。
而vue-router 4.0还是alpha的阶段,所以我认为现在去学习它有些为时尚早。但却就是它!差点酿成了一场惨剧。
旧版vue + vue-router的使用方式
假如你在路由里面定义了一个动态参数通常都会这么写:
{
path: '/:id'
}
然后用编程式导航的时候通常会这样去写:
this.$router.push('/123')
在组件中是这样获取这个参数的:
this.$route.params.id
我以为的新版vue + vue-router的使用方式
由于vue 3.0的Composition API中没有this了,所以我想到了通过获取组件实例的方式来获取$route:
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent((props, context) => {
const { ctx } = getCurrentInstance()
console.log(ctx.$route)
})
没想到打印出来的居然是undefined!
这是咋回事呢?
于是我又打印了一遍ctx(ctx是当前组件上下文):
没有$的那些字段是我在组件中自己定义的变量,带$的这些就是vue内置的了,找了半天发现没有$route了,只剩下了一个$router,估计vue-router 4.0把当前路由信息都转移到$router里面去了。
带着猜想,我点开了$router:
currentRoute! 看名字的话感觉应该就是它了!于是乎我:
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent((props, context) => {
const { ctx } = getCurrentInstance()
console.log(ctx.$router.currentRoute.value.params.id)
})
果然获取到了!好开心!
实际的新版vue + vue-router用法
在接下来的过程中我用ctx.$router代替了原来的this.$router、用ctx.$router.currentRoute.value代替了原先的this.$route。
尽管在接下来的进度中并没有出现任何的bug,程序一直都是按照我所设想的那样去运行的。
但在项目打包后却出现了意想不到的bug:在跳转路由的时候报了一个在undefined上面没有push的错误。
奇了怪了,在开发阶段程序都没有任何的报错怎么一打包就不行了呢?根据我多年的开发经验,我很快就定位到了是vue-router的错误。
难道这样写是错的吗?可是我打印了ctx,它里面明明有一个$router、$router里面明明就有currentRoute、currentRoute里面明明就有一个value、value里面明明就有params、params里面我一点开明明就看到了传过来的参数啊:
估计可能是vue-router的bug,果然alpha阶段的产物不靠谱,我开始后悔使用新版的vue脚手架项目了。
vue-router里的hooks
不过这时我突然灵光一现,vue 3不是受到了react hooks的启发才产生了Composition API的吗?
那么估计vue-router肯定也会受到react-router的启发了!
还好我学过react,果然技多不压身啊!估计里面肯定是有一个useXxx,就像这样:
import { useXxx } from 'vue-router'
那么应该是use什么呢?按理来说应该会尽量的和以前的API保持一定的联系,我猜应该是useRoute和useRouter吧!
为了验证我的想法,我打开了node_modules找到了vue-router的源码:
果不其然,在第2454和第2455行我发现它导出了useRoute和useRouter,那么就是它了:
import { defineComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent(_ => {
const route = useRoute()
const router = useRouter()
console.log(route.params.id)
router.push('/xxx/xxx')
})
使用这种方式不但可以成功跳转路由,也同样可以获取到路由传过来的参数,这次再打包试了一下,果然就没有之前的那个报错了。
结语
估计以后的vue全家桶要开启全民hooks的时代了,在翻看源码的同时我发现他们把一些示例都写在了vue-router/playground文件夹下了,在里面我发现了一些有趣的用法。
如果有时间的话我会仔细研究一下然后出一篇更加深入的文章给大家,当然如果已经有小伙伴等不及我出新文章的话可以直接进入vue-router-next的github地址:
https://github.com/vuejs/vue-router-next
它的示例都放在了playground这个文件夹下,期待你们研究明白后出一篇更加深入的文章!
将要分析的竞品排了个期,从最难最不熟悉的开始。为什么从最难的开始,可能是个人习惯吧,吃掉最难的那个,后面就会更上手。突然想起之前读的一本书「吃掉那只青蛙」,很不错的一本书,有时间去温习下。
一个产品,其实会有很多功能点,有核心的主要功能,也有一些辅助功能,也会有一些让你忽略,但关键时刻很需要的应急功能,而这些点都需要去整理出来。
这一点很重要,要先熟悉产品。如果对产品都不熟悉,那还是先不要做竞品分析。因为很难判断竞品的功能和风格是否也适合当前产品,因为对产品的不熟悉,会产生误判。
当然,产品的目标人群,产品定位,适用范围等等,都会影响产品分析。
所以,花时间熟悉自己负责的产品,是不能跳过的。
1. 制定时间规划
最好事先做好时间规划,可以有一整块的时间,这样分析产品时,思绪也会比较完整和连续,可以更专注。计算大概分析一个产品需要花费的时间,最好不要用零碎时间来做,这样只会增加时间上的代价,也会增加挫折感;
2. 确定分析的目的
在「竞品分析」中,想要得到的结论和重点是什么。比如重点可能是产品的报表功能、产品的代码审核功能等等,目的的确定能让分析更有针对性,减少干扰。无目的随意分析,得到的结果也会是零乱不堪,最后只是在浪费时间。
3. 寻找帮助者
每个产品,都有其不一样的特性和产品逻辑,你不一定能够完全 cover 到,甚至有些点就是比较难理解的,特别是偏技术性的名词,这时若有技术同学的帮助,就会如虎添翼。所以最好可以事先找一位产品相关的技术同学,询问这段时间是否有空,帮助你解答一些问题。
个人建议:能够在网上查到的资料,就不要先问人,除非时间成本特别高。一方面也是提升自己解决问题的能力,另一方面,也是节省彼此的时间。对方愿意帮你解决问题,不代表你要把所有问题一股脑倒给他,自己了解后再问,也是对对方的尊重,大家的时间都同样宝贵。
4. 其他tips
如果是内部公司产品,提前确认是否需要权限,提前申请好,减少正式开始后,还要等待审批时间。外部产品可以提前找好网站,可以咨询的客服入口,如果是付费竞品,咨询是否可以向财务申请报销等等。
1. 像个用户一样去使用产品
很多时候,设计师的职业病,会让我们过多注重视觉享受,而忽略作为用户,想要的有时候只是功能可用。今天不管你把「扫一扫」功能做得多美,美得像个艺术品一样,可是当扫码付款的时候,怎么也扫不出来,那种站在店家前面忐忑不安,怎么也无法完成付款,后面一堆人等你,你仿佛听见后面其他顾客窃窃私语地讨论着发生什么事情。那种场景我相信你不想经历,同样我们也不应该让用户来经历。
我的项目主管,一直都有提醒我,要像个小白来使用和设计我们的产品。这句建议,也一直在提醒着我。如果站在高姿态来俯视用户,我们就很难真正的「懂」用户,进而很难设计出真正满足用户需要的产品。
这是竞品分析,但是我们也需要转换自己的角色,变成用户。这样能更明白究竟竞品带给用户是便利,还是麻烦。有时适时抽离「设计师」的角色,会让你更能去体会用户的感受。
所以,先去用这个产品吧,然后才会有然后。
2. 如何去使用竞品
一个产品的使用,总是有它的使用场景,手机端的就更多样了,简直无所不在。B 端产品可能会相对少,一般是在办公场景或是特定场景。
可以像个编剧一样,给自己写点剧本,加点情节,塑造一个角色,假设竞品是电商方向,你可以想像,自己是一个刚毕业的社会新人,你可能没多少钱,你可能刚拿到你人生第一桶金,你想买件衣服犒劳自己,或许你会是数码控,你关注已久的佳能单反在双 11 中有优惠等等,然后再去预想接下去的情节,在购物方面会考虑的问题,或许是好用,或许是有趣等等。
也可以做任务式去使用产品,比如以电商为例,任务可以是买件喜欢的衣服,从搜索产品,到找到喜欢的衣服,添加购物车,提交订单,等待发货,收货,确认收货。这一个完整的流程走下来,就会体验产品功能是否好用,搜索结果是否符合预期等等。
3. 记录
使用产品的过程中,会遇到很多情况,有些是可预期的,有些是不可预期的。有些让人觉得很好用,有些却会让人受挫。将这些情况都记录下来,有助于分析产品的可用性程度和满意度。
上面这些只是举例说明,在竞品当中可能遇到的一些问题,也可以去反思自己的产品是否也会这样让用户感到困惑。有时候,太熟悉自己的产品,会自认为产品很完美,会理所当然认为「大家都这么认为」……
记录问题、原因,感受并截图为证(有必要可录屏),后期可追溯。写得越详细越好,后面整理的时候会更清晰。
4. 各个击破-功能了解
在熟悉整个产品后,就需要对产品的各个功能进行分析了解、梳理。了解竞品的核心功能是什么,核心功能在解决用户什么问题,是否真的解决了用户的痛点,其他功能又在整个产品当中充当什么样的角色。
将竞品的功能与本产品功能对比,不只是对比有无,更进一步地去想,为什么有这个功能,为什么没有这个功能,有或没有是否会提高用户的使用效率,用户的留存,用户的体验等等。
功能多不代表好,如果功能不能给用户带来益处,其实它的存在只是增加开发成本而已。
其实竞品分析中,最难的是总结归纳。做了一堆的分析后,结论是什么呢,这个结论如何写呢?
可以先从设立分析目的开始,找到中心轴线,然后再慢慢延展开来。在要做总结报告时,你会欣喜地发现最初设立目标是多么的重要。
文章来源:优设 作者:箴盐设计
财务和金融相关的应用是一个相对专业的分支,在这个领域当中创造体验优异的设计并不是一件简单的事情。身为资深设计师Taras Bakusevych 在这篇文章当中,分享了10个确保这类产品足够优秀的核心设计原则。
蓝蓝 http://www.lanlanwork.com